月別アーカイブ: 2017年9月

SinatraでFlash(1回きりのセッションデータ)を用いる

備忘録

前提

以下の環境で動作確認済み

$ ruby -v
ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-linux]
$ gem -v
2.4.5
$ bundle -v
Bundler version 1.13.4

FLASHとは

直後のHTTPリクエストのみで使用するセッションデータ。
POSTでデータを登録した後にGETにリダイレクトし、登録の結果メッセージを表示するのに使ったりする

rack-flash3をインストール

gem install rack-flash3

bundlerを使う場合
Gemfileに以下を追加し、bundle install

 gem "rack-flash3"

プログラム例

‘/’ 経由で’/hoge’にアクセスした場合に、’flash data’を出力。
直接’/hoge’にアクセスしたり、リダイレクト後に再読込した場合に’no data’を出力。

require 'sinatra/base'
require 'rack/flash'

class App < Sinatra::Base

  configure do
    use Rack::Flash
  end

  get '/' do
    flash[:hoge] = 'flash data'
    redirect '/hoge'
  end

  get '/hoge' do
    if flash[:hoge]
      flash[:hoge]
    else
      'no data'
    end
  end

end

MySQLで枠線/列名を表示せずにSQLを実行する

備忘録

通常

 mysql -u root --password=pw db_name -e "select id , created_at from user"
+----+---------------------+
| id | created_at          |
+----+---------------------+
|  1 | 2016-02-14 17:39:06 |
|  2 | 2016-02-14 17:39:28 |
|  3 | 2016-02-14 17:39:33 |
|  4 | 2016-02-14 17:39:38 |
|  5 | 2016-02-18 00:07:45 |
|  6 | 2016-04-27 23:09:30 |
|  8 | 2016-05-17 22:45:14 |
|  9 | 2016-05-28 18:33:53 |
| 10 | 2016-09-03 20:59:53 |
+----+---------------------+

枠線なし

-sオプションを付与するだけ

$ mysql -u root --password=pw db_name -e "select id , created_at from user" -s
id	created_at
1	2016-02-14 17:39:06
2	2016-02-14 17:39:28
3	2016-02-14 17:39:33
4	2016-02-14 17:39:38
5	2016-02-18 00:07:45
6	2016-04-27 23:09:30
8	2016-05-17 22:45:14
9	2016-05-28 18:33:53
10	2016-09-03 20:59:53

列名なし

-Nオプションを付与するだけ

$ mysql -u root --password=pw db_name -e "select id , created_at from user" -s -N
1	2016-02-14 17:39:06
2	2016-02-14 17:39:28
3	2016-02-14 17:39:33
4	2016-02-14 17:39:38
5	2016-02-18 00:07:45
6	2016-04-27 23:09:30
8	2016-05-17 22:45:14
9	2016-05-28 18:33:53
10	2016-09-03 20:59:53

使用例

ユーザのIDリストをDBから取得し、それに対して何らかの処理を行うスクリプトに標準入力で引き渡す

$ mysql -u root --password=pw db_name -e "select id from user" -s -N | script

DebianでMeCab(日本語形態素解析システム)を利用する

備忘録

MeCab(日本語形態素解析システム)

インストール

$ sudo apt-get update
$ sudo apt-get install libmecab2 libmecab-dev mecab mecab-ipadic mecab-ipadic-utf8 mecab-utils

動作例

インタラクティブ

$ mecab
テスト
テスト	名詞,サ変接続,*,*,*,*,テスト,テスト,テスト
EOS
世界に一つだけの花
世界	名詞,一般,*,*,*,*,世界,セカイ,セカイ
に	助詞,格助詞,一般,*,*,*,に,ニ,ニ
一つ	名詞,一般,*,*,*,*,一つ,ヒトツ,ヒトツ
だけ	助詞,副助詞,*,*,*,*,だけ,ダケ,ダケ
の	助詞,連体化,*,*,*,*,の,ノ,ノ
花	名詞,一般,*,*,*,*,花,ハナ,ハナ

パイプを用いて

$ echo "花屋の店先に並んだ" | mecab
花屋	名詞,一般,*,*,*,*,花屋,ハナヤ,ハナヤ
の	助詞,連体化,*,*,*,*,の,ノ,ノ
店先	名詞,一般,*,*,*,*,店先,ミセサキ,ミセサキ
に	助詞,格助詞,一般,*,*,*,に,ニ,ニ
並ん	動詞,自立,*,*,五段・バ行,連用タ接続,並ぶ,ナラン,ナラン
だ	助動詞,*,*,*,特殊・タ,基本形,だ,ダ,ダ
EOS

末尾のEOSを消したい

-E オプションにて末尾の文字を指定

$ echo "色んな花を見ていた" | mecab -E ""
色んな	連体詞,*,*,*,*,*,色んな,イロンナ,イロンナ
花	名詞,一般,*,*,*,*,花,ハナ,ハナ
を	助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
見	動詞,自立,*,*,一段,連用形,見る,ミ,ミ
て	助詞,接続助詞,*,*,*,*,て,テ,テ
い	動詞,非自立,*,*,一段,連用形,いる,イ,イ
た	助動詞,*,*,*,特殊・タ,基本形,た,タ,タ

単語に分割するだけ

$ echo "人それぞれ好みはあるけど" | mecab -F"%m\n" -E ""
人
それぞれ
好み
は
ある
けど

WebPack + Vueで、「Element」導入時にハマった点

前提

以下環境にて確認

要素 バージョン
debian 8.6
node 8.2.1
npm 5.3.0

概要

Webpackを用いてVueアプリを開発している中で、Vue2.0用のUIライブラリであるElementを導入しようとしたら、Webpack周りでハマったので導入手順と解決手順を備忘録に残す。

備忘録が主な目的なので、Webpack,Vue,Elementなどに関する説明は割愛

Elementの導入

npmでインストールする

npm install element-ui -s

エントリポイントでElement関連をimport

import ElementUI  from 'element-ui'
import locale     from 'element-ui/lib/locale/lang/ja'
import 'element-ui/lib/theme-default/index.css'

Elementの利用を宣言。localeを指定することで日本語が有効になるらしい

Vue.use(ElementUI, {locale});

エラー① Element内のCSSファイルが解決できない

この状態でwebpackを回すと、以下のエラーが出る

ERROR in ./node_modules/element-ui/lib/theme-default/index.css
Module parse failed: /root/degulog/node_modules/element-ui/lib/theme-default/index.css Unexpected character '@' (1:0)
You may need an appropriate loader to handle this file type.

どうやらElementに付随するCSSファイルをバンドルできないらしい。

原因は単純に、これまでscssだけ使ってたのでcssに対するローダーをwebpackで設定していなかったため。
なのでwebpack.config.jsのloadersに以下を追加すれば解決

{ test: /\.css$/, loader: 'style-loader!css-loader' },

エラー② Element内のwoffファイルが解決できない

続けて以下のエラーが発生した

ERROR in ./node_modules/element-ui/lib/theme-default/fonts/element-icons.woff?t=1472440741
Module parse failed: /root/degulog/node_modules/element-ui/lib/theme-default/fonts/element-icons.woff?t=1472440741 Unexpected character '' (1:4)
You may need an appropriate loader to handle this file type.

問題の本筋は前項と同様。woffを解決するローダが設定されていなかった。

url-loaderを適用すれば良いが、まずそれが入っていなかたのでインストールする。

$ npm install url-loader

で、webpack.config.jsonのloadersに以下を追加。woff意外にも対応できるように汎用的に記述。

{
  test: /\.(otf|eot|svg|ttf|woff|woff2)(\?.+)?$/,
  loader: 'url-loader'
},

動作確認

依存解決も上手く行って、以下のようにElementが利用できた。
例はDatetimePickerコンポーネント

<el-date-picker
  v-model="value1"
  type="datetime"
  placeholder="Select date and time">
</el-date-picker>

[Python3] scikit-learnによる機械学習4: 学習が完了したモデルを永続化する

前提

以下の環境で実装、動作確認済み

要素 バージョン
debian 8.6
python 3.6.2

scikit-learnによる機械学習シリーズ

機械学習も数学もPythonもよくわからなくても機械学習はデキるシリーズ

概要

前回、自動車の情報から価格を予測する学習モデルを構築し、それを評価した。

しかし、予測を行おうとスクリプトを実行するたびに学習モデルを構築しているのは無駄でしかないので、一度作成した学習モデルをシリアライズし、使いまわせるようにする。以降では、シリアライズされた学習モデルをデシリアライズして予測を行うことで、学習作業を省略することができる。

シリアライズ

今回は、scikit-learnのjoblibを用いてシリアライズする。以下のimport文が必要

from sklearn.externals import joblib

学習が完了したモデル(clf)を、cars.plkのファイル名で保存する場合は以下のようにする

joblib.dump(clf, 'cars.pkl')

実行するとcars.plkが生成される。可読性のあるものではないので中身は気にしなくて良い

$ file cars.pkl
cars.pkl: 8086 relocatable (Microsoft)

デシリアライズ

シリアライズされた学習モデルは、load関数でデシリアライズすることができる。以降ではclfを用いれば従来通りpredictメソッドを使って予測を行うことができる

clf = joblib.load('cars.pkl');

ソースコード

自動車の情報と価格をランダムフォレストで学習し、シリアライズするスクリプト

import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn import preprocessing
from sklearn.externals import joblib

# CSVファイルを読み込む
df = pd.read_csv('cars.csv')

# 学習に不要な行、列を削除
del df['normalized-losses']
for col in ['num-of-doors', 'price', 'bore', 'stroke', 'horsepower', 'peak-rpm']:
 df = df[df[col] != '?']

# 説明変数列と目的変数列に分割
label = df['price']
data  = df.drop('price', axis=1)

# 文字列データを数値化
le = preprocessing.LabelEncoder()
for col in ['make', 'fuel-type', 'aspiration', 'num-of-doors', 'body-style', 'drive-wheels', 'engine-location', 'engine-type', 'num-of-cylinders', 'fuel-system']:
  data[col] = le.fit_transform(data[col])

# 学習する
clf = RandomForestClassifier()
clf.fit(data, label)

# 学習結果をシリアライズ
joblib.dump(clf, 'cars.pkl')

シリアライズされた学習モデルをデシリアライズし、予測、評価するスクリプト

import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn import preprocessing
from sklearn.externals import joblib

# CSVファイルを読み込む
df = pd.read_csv('cars.csv')

# 学習に不要な行、列を削除
del df['normalized-losses']
for col in ['num-of-doors', 'price', 'bore', 'stroke', 'horsepower', 'peak-rpm']:
 df = df[df[col] != '?']

# 説明変数列と目的変数列に分割
label = df['price']
data  = df.drop('price', axis=1)

# 文字列データを数値化
le = preprocessing.LabelEncoder()
for col in ['make', 'fuel-type', 'aspiration', 'num-of-doors', 'body-style', 'drive-wheels', 'engine-location', 'engine-type', 'num-of-cylinders', 'fuel-system']:
  data[col] = le.fit_transform(data[col])

# 学習モデルをデシリアライズ
clf = joblib.load('cars.pkl');

# 評価する
predict = clf.predict(data)
rate_sum = 0
for i in range(len(label)):
  t = int(label.iloc[i])
  p = int(predict[i])
  rate_sum += int(min(t, p) / max(t, p) * 100)
print(rate_sum / len(label))

[Python3] scikit-learnによる機械学習3: 自動車の情報から価格を予測する

前提

以下の環境で実装、動作確認済み

要素 バージョン
debian 8.6
python 3.6.2

scikit-learnによる機械学習シリーズ

機械学習も数学もPythonもよくわからなくても機械学習はデキるシリーズ

概要

scikit-learnによる機械学習がなんたるかがほんの少しわかってきたので、足し算などという実用性皆無な例でなく、もう少し実用的な使い方をしてみる。

今回は、自動車の各種情報と、その自動車の価格を学習させ、新たに情報を与えると価格を予測してくれるスクリプトを作成する。なお、学習データにはMicrosoftのクラウド機械学習サービスである、Azure Maschine Learningより、そのチュートリアルで使用されているサンプルデータを拝借する。

学習用データについて

今回使用する学習データは以下のようなCSVファイル。

画像をだとわかりづらいが、以下の列で構成されている。列の内容は列名から概ね察せるが、最終列にあるprice列の値(自動車の価格)を、他の列の情報を元に予測するということがわかれば問題ない。

  • symboling
  • normalized-losses
  • make
  • fuel-type
  • aspiration
  • num-of-doors
  • body-style
  • drive-wheels
  • engine-location
  • wheel-base
  • length
  • width
  • height
  • curb-weight
  • engine-type
  • num-of-cylinders
  • engine-size
  • fuel-system
  • bore
  • stroke
  • compression-ratio
  • horsepower
  • peak-rpm
  • city-mpg
  • highway-mpg
  • price

学習用データはいつもどおりPandasを用いてDataFrameに展開する

df = pd.read_csv('cars.csv')

無効な行/列の削除

CSVのスクリーンショットを見てもらうとわかるが、全体的に値が”?”になっているセルが散らばっている。これは情報が無いという意味を表していると思うので、それらを取り除いていく。

まず、’normalized-losses'(2列目)は、列全体で”?”が多いので、思い切ってこの列自体を削除する。列についではdel文で削除できるので削除してしまう。

del df['normalized-losses']

次に、他の”?”についてだが、それらについては一部の行のみ”?”になっているだけなので、列全体の削除はせず、”?”が一つでも含まれている行を削除するという方針にする。

“?”が存在する列は’num-of-doors’, ‘price’, ‘bore’, ‘stroke’, ‘horsepower’, ‘peak-rpm’の6列なので、これらのいずれの列にも”?”が含まれていない行のみ取り出して再代入するという処理を行う。PandasのDataFrameでは、行の抽出に、列を対象とした条件式を指定することができるので、”?”が含まれている行を排除できる。

for col in ['num-of-doors', 'price', 'bore', 'stroke', 'horsepower', 'peak-rpm']:
 df = df[df[col] != '?']

おそらくPandasを使ってもっとスマートに同じことができると思うので、良い方法があったらご教授お願いします。

文字列を数値化する

今回も前回までと同様、scikit-learnの学習モデルのfitメソッドを用いて学習を行うが、fitメソッドはfloat型の値しか受け付けない。しかし、今回のCSVには文字列も多く含んでいるので、それを数値化する必要がある。

そこで今回は、scikit-learnのLabelEncoderクラスを用いて各文字列を数値に置き換えることで、学習可能のデータに変換する。変換が必要な列は、’make’, ‘fuel-type’, ‘aspiration’, ‘num-of-doors’, ‘body-style’, ‘drive-wheels’, ‘engine-location’, ‘engine-type’, ‘num-of-cylinders’, ‘fuel-system’の10列なので、それぞれに対して、以下のようにLabelEncoderオブジェクトのfit_transformメソッドを適用することで文字列を数値に変換できる。

le = preprocessing.LabelEncoder()
for col in ['make', 'fuel-type', 'aspiration', 'num-of-doors', 'body-style', 'drive-wheels', 'engine-location', 'engine-type', 'num-of-cylinders', 'fuel-system']:
  data[col] = le.fit_transform(data[col])

例として、body-style列の、変換前後を出力すると以下のようになり、文字列が数値に変換されていることがわかる

変換前

0      convertible
1      convertible
2        hatchback
3            sedan
4            sedan
5            sedan
6            sedan
7            wagon
8            sedan
(以下略)

変換後

0      0
1      0
2      2
3      3
4      3
5      3
6      3
7      4
8      3
(以下略)

わざわざ変換する列名を配列でツラツラ書かなくてももっとスマートに書ける方法がありそうです。こちらも良い方法ありましたらご教授お願いします。

学習用データとテストデータの分割

ここは前回一緒なのでコードだけ載せて割愛

train_data, test_data, train_label, test_label = train_test_split(data, label)

ランダムフォレストで学習させる

前回までは線形回帰アルゴリズムを用いて足し算の学習を行わせたが、今回は別の学習アルゴリズムを使うことにする。どのアルゴリズムをどのような場合に使えば良いかはまだ調査中だが、とりあえず線形回帰とは異なるアルゴリズムを使う場合の例として使ってみる。

ランダムフォレスト(英: random forest, randomized trees)は、2001年に Leo Breiman によって提案された[1]機械学習のアルゴリズムであり、分類、回帰、クラスタリングに用いられる。決定木を弱学習器とする集団学習アルゴリズムであり、この名称は、ランダムサンプリングされたトレーニングデータによって学習した多数の決定木を使用することによる。対象によっては、同じく集団学習を用いるブースティングよりも有効とされる。(Wikipedia引用)

といっても、scikit-learnではどの学習アルゴリズムを使おうと、その使い方が共通化されているので、線形回帰の時とほとんど同じコードで動かすことができる。
必要なモジュールをインポートした状態で以下のようにすることランダムフォレストで学習を実行できる。

clf = RandomForestClassifier()
clf.fit(train_data, train_label)

前回までとの違いは、clf変数にどの学習アルゴリズムのオブジェクトを代入するかの違いだけである。どの学習アルゴリズムについてもfitメソッドを使うことができるので、同じように利用することができる。

評価する

学習用データとテスト用データに分割した際の、テスト用データの方を用いて学習モデルの評価を行う。これまで通り、predictメソッドを用いて各自動車の価格を予想する。

predict = clf.predict(test_data)

とりあえず予測した結果と正解を並べて出力してみる。

for i in range(len(test_label)):
  p = int(predict[i])
  t = int(test_label.iloc[i])
  print(p, t)

どうだろうか、かなり近い価格を出せているのではないだろうか。(予測値 正解値)

7898 6918
5572 5572
12764 12964
10198 11694
7299 8249
8949 9549
17450 15250
7198 8358
23875 17710
10245 8495
8238 8058
6338 6488
31600 28248
11199 8449
10295 6785
34028 32528
15985 12940
7299 6849
(以下略)

ただこれだと、どのぐらいの精度が出てるかわかりづらいので、正解に何%近いかを計算して出力するように修正する

for i in range(len(test_label)):
  p = int(predict[i])
  t = int(test_label.iloc[i])
  rate = int(min(t, p) / max(t, p) * 100)
  print(rate)

それなりの精度であることがわかる

$ python cars.py
70
93
79
87
75
97
91
84
90
97
88
89
95
90
100

さらに、個々の精度の平均値を計算し、全体の精度を出力するようにする

for i in range(len(test_label)):
  t = int(test_label.iloc[i])
  p = int(predict[i])
  rate_sum += int(min(t, p) / max(t, p) * 100)
print(rate_sum / len(test_label))

学習データとテストデータの組み合わせによってムラがあるので、何度か実行して確認してみると、概ね87%前後の精度が得られた

$ python cars.py
86.6734693877551
$ python cars.py
87.46938775510205
$ python cars.py
88.59183673469387
$ python cars.py
86.55102040816327
$ python cars.py
86.04081632653062

さらに、アルゴリズムをランダムフォレストから、線形回帰に変更して再評価してみる。変数clfに代入する学習モデルのオブジェクトを、ランダムフォレストから線形回帰に変更するだけで良い

clf = linear_model.LinearRegression()

同じように何度か実行してみると、83%程度と、ランダムフォレストより微妙に精度が下がり、アルゴリズムによって結果が変わることを実感した。

$ python cars.py
82.40816326530613
$ python cars.py
83.79591836734694
$ python cars.py
83.53061224489795
$ python cars.py
85.65306122448979
$ python cars.py
82.46938775510205

なお、学習モデルの評価方法についても、scikit-learnに様々な機能があるが、今回は意図的に自前で計算するようにして感覚を掴んだ。

ソースコード

Github

#
# 車の製品情報と価格を学習し、価格を予測させる
#
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn import preprocessing

# CSVファイルを読み込む
df = pd.read_csv('cars.csv')

# 学習に不要な行、列を削除
del df['normalized-losses']
for col in ['num-of-doors', 'price', 'bore', 'stroke', 'horsepower', 'peak-rpm']:
 df = df[df[col] != '?']

# 説明変数列と目的変数列に分割
label = df['price']
data  = df.drop('price', axis=1)

# 文字列データを数値化
le = preprocessing.LabelEncoder()
for col in ['make', 'fuel-type', 'aspiration', 'num-of-doors', 'body-style', 'drive-wheels', 'engine-location', 'engine-type', 'num-of-cylinders', 'fuel-system']:
  data[col] = le.fit_transform(data[col])

# 学習用データとテストデータにわける
train_data, test_data, train_label, test_label = train_test_split(data, label)

# 学習する
clf = RandomForestClassifier()
clf.fit(train_data, train_label)

# 評価する
predict = clf.predict(test_data)
rate_sum = 0

for i in range(len(test_label)):
  t = int(test_label.iloc[i])
  p = int(predict[i])
  rate_sum += int(min(t, p) / max(t, p) * 100)
print(rate_sum / len(test_label))

[Python3] scikit-learnによる機械学習2: 足し算学習モデルを評価する

前提

以下の環境で実装、動作確認済み

要素 バージョン
debian 8.6
python 3.6.2

scikit-learnによる機械学習シリーズ

機械学習も数学もPythonもよくわからなくても機械学習はデキるシリーズ

概要

前回実装した、足し算を線形回帰学習するモデルがどのような学習をしたかを評価する

線形回帰学習で算出した式を確認する

線形回帰モデルは、目的変数Yを求めるために、説明変数X1,X2,X3…それぞれに偏回帰係数b1,b2,b3…を掛け、最後に切片(誤差)eを付与する。式にすると以下のようになる。

Y = X1*b1 + X2*b2 + X3*b3 … + e

今回の単純な足し算モデルは、結果Yに対して、入力値が2種類なのでX1とX2が存在する。
なので理論上は以下の式が求められれば正確といえる

Y = X1*1 + X2*1 + 0

では各b及びeがどのような値になったか確認する。

学習が完了したモデルのcoef_,intercept_にそれぞれ偏回帰係数と切片が格納されているのでそれを確認する。

print("偏回帰係数1: ", float(model.coef_[0][0]))
print("偏回帰係数2: ", float(model.coef_[0][0]))
print("切片: ", float(model.intercept_))

すると以下のような結果が得られたので

偏回帰係数1:  1.0000000000000002
偏回帰係数2:  1.0000000000000002
切片:  -4.440892098500626e-16

この学習モデルが予測した線形回帰は以下の式で表される

Y = X1*1.0000000000000002 + X2*1.0000000000000002 + -4.440892098500626e-16

小数計算ならともかく、今回は結果をint関数で整数化しているので、このレベルの誤差なら数値が極大出ない限りは誤った答えが出ないことが予想できる。

精度を確認する

前回は標準入力から式を入力して、解答を標準出力していたが、膨大なデータをテストする場合に一々手入力していたらキリがないので、今回は自動で100000件のテストデータを生成して、それぞれについて正しい答えを求められているかを評価する。

まずは以下のような、1~1000のランダムな数値を二種及びその和のセットを戻す関数を実装した

def get_rand_data():
    max = 1000
    a = random.randint(1, max)
    b = random.randint(1, max)
    return [a, b, a + b]

これを、以下のように内包記法を用いて呼び出すことで10000セット、PandasのDataFrame形式で取得する

rand_data = DataFrame([get_rand_data() for i in range(100000)])

ランダムデータの1,2列目が入力になるので、1,2列目を取り出して学習させる

pre = model.predict(rand_data[[0, 1]])

変数preには、1万件分のデータセットに対する和の予測値が格納されているので、それらを順に確認して、本来の解と一致しているかを比較検証する。一致件数と総数から、正解率を算出する。このあたりを勝手に評価してくれるモジュールがあったと思うが、今回は初見なので自前で実装する。

correct_num = 0
for i in range(len(rand_data)):
  if rand_data[2][i] == int(pre[i]):
    correct_num += 1

print("正解率 = ", correct_num / len(rand_data))

すると、以下のように正解率1.0と出力されるので、最低でも1~1000の整数数二種の和は正確に求められていることがわかる。

正解率 =  1.0

備考

  • もちろん計算の桁数が大きくなればなるほど誤差が影響して正解率は落ちる
  • そもそも明確な解のある足し算を機械学習させるのはお門違いだが今回は簡易的に理解できる例として使用した

[Python3] scikit-learnによる機械学習1: 足し算を学習させる

前提

以下の環境で実装、動作確認済み

要素 バージョン
debian 8.6
python 3.6.2

scikit-learnによる機械学習シリーズ

機械学習も数学もPythonもよくわからなくても機械学習はデキるシリーズ

機械学習とは

機械学習(きかいがくしゅう、英: machine learning)とは、人工知能における研究課題の一つで、人間が自然に行っている学習能力と同様の機能をコンピュータで実現しようとする技術・手法のことである。(wikipedia引用)

機械学習とは、言語やゲームなどをはじめとした人間の様々な知的活動の中で、人間が自然と行っているパターン認識や経験則を導き出したりするような活動を、コンピュータを使って実現するための技術や理論、またはソフトウェアなどの総称である。(IT用語時点引用)

まぁ人間っぽい学習をコンピュータにもやらせましょうというお話。
本シリーズでは機械学習の定義とかにあまり拘らずに、一般的に考えられる機械学習を自分でもやってみようという緩い方向で進める。

scikit-learnとは

Pythonにおける事実上標準と言っても過言ではない、機械学習のためのライブラリ。
機械学習と言えばTensorFlowというワードの知名度が高いが、TensorFlowは学習コストも高く、ディープラーニングまで幅広くできるので、本シリーズではお手軽に機械学習ができるsckit-learnを用いる

scikit-learnのインストール

sckit-learnはPythonモジュールとして公開されているので、pipなどを用いてインストールする。pipはPythonをインストールすると標準で入ってる可能性が高い。入っていない場合は別途導入する。あるいはAnacondaなどの別な手段を適用する。

$ pip install scikit-learn

Pandas/Numpyについて

Pythonで機械学習を行う上で切っても切れないライブラリに、以下の2つがある

Pandas
データ解析を支援するためのライブラリ。汎用的なデータ構造の構築や、データ内の演算などをサポートする。

Numpy
科学計算用のライブラリ。特に行列に対する計算は普通にPythonで行うより圧倒的に早い。

以上より、これらのライブラリも初めに導入しておく。

$ pip install numpy pandas

足し算の機械学習について

本記事では、機械学習の第一歩として、sckit-learnの線形回帰アルゴリズムを用いて、足し算を学習させる。

流れとしては以下のようになる

  • 足し算の式と、その答えの組み合わせをいくつか用意する
  • sckit-learnに式と答えの組み合わせを学習させ、学習モデルを生成
  • 標準入力から二種類の整数を学習モデルに与え、予測結果を標準出力する

なお、2数の足し算とは当然演算によって求められるものなので、機械学習の利用法としては適していない。
が、ここでは雰囲気を掴むためにこのテーマにした。

教師あり学習について

教師あり学習は、データの組み合わせと、それに対する答えのセットを複数セット学習させることで、データの組み合わせを与えただけでその答えを予測してもらおうという学習モデル。

今回は足し算を学習させるので、データの組み合わせが式で、答えが和と考えれば良い。

線形回帰について

Wikipediaをそのまま掲載すると以下の通り。なるほど。

何を言ってるかよくわからないが、足し算に置き換えれば非常にシンプルになる。

目的変数Yは、足し算における和である。
説明変数X1,X2は、足し算のそれぞれの数値である。

この場合、線形回帰直線は、Y = X1 + X2の形になるので、
この式を予測することが線形回帰学習である(あってるか極めて怪しい)

実装

モジュールのimport

今回は、Pandasによる学習用データの入力と、scikit-learnの線形回帰モデルを用いるので、以下のimport文を記述する。Numpyは今回出番なし。

from pandas import DataFrame
from sklearn import linear_model

学習データの用意

学習データは、PandasのDataFrameの形式で用意する。DataFrameはデータの集合を二次元で表現する。
sckit-learnでは、学習データとその答えを別々に渡す必要があるので、それぞれformulas,answersに代入する。

formulas = DataFrame([
    [0, 0],
    [0, 1],
    [0, 2],
    [1, 0],
    [1, 1],
    [1, 2],
    [2, 0],
    [2, 1],
    [2, 2]
])
answers = DataFrame([0, 1, 2, 1, 2, 3, 2, 3, 4])

コードを見ての通り、それぞれ、0 + 0 = 0, 0 + 1 = 1, 0 + 2 = 2 … のセットを用意している。
ちなみにこの段階でformulasを標準出力すると以下のようになり、DataFrameがどのようにデータ集合を持っているかのイメージが湧く

   0  1
0  0  0
1  0  1
2  0  2
3  1  0
4  1  1
5  1  2
6  2  0
7  2  1
8  2  2

学習データを学習させる

importしたlinear_modelを用いて、先程用意した学習データを予測する。
LinearRegressionメソッドで学習モデルを取得できるので、それに対してfitメソッドで学習データを割り当て、学習を開始する。

model = linear_model.LinearRegression()
model.fit(formulas, answers)

学習結果を確認する

学習が完了したmodelに対して、predictメソッドを用いることで、新たなデータに対する答えの予測値を取得することができる。
この場合、必ず入力、出力ともに配列である必要があるので注意。

例えば以下では、10 + 20の予測結果を取得する。

predected_answer = model.predict([[10, 20]])

これを用いて、標準入力から得られた2数の和の予測結果を出力し、それを繰り返すコードを以下に示す

while True:

    print('> ', end='')
    x, y = list(map(lambda x: int(x), input().split(' ')))

    predected_answer = model.predict([[x, y]])
    print("{0} + {1} = {2}".format(x, y, int(predected_answer[0][0])))

標準入力と標準出力で小難しいことをやってるように見えるが、とりあえず入力した2数の和を予測して出力していることはわかる。

動作確認

これまでのコードを以下のように整理して実行する
コード全文 (Github)

 #
 # 2つの整数の足し算を機械学習し、任意の足し算を回答させる
 #
 from pandas import DataFrame
 from sklearn import linear_model

 # 足し算の例とその答えを用意する
 formulas = DataFrame([
     [0, 0],
     [0, 1],
     [0, 2],
     [1, 0],
     [1, 1],
     [1, 2],
     [2, 0],
     [2, 1],
     [2, 2]
 ])
 answers = DataFrame([0, 1, 2, 1, 2, 3, 2, 3, 4])

 # 式と答えを線形回帰学習させる
 model = linear_model.LinearRegression()
 model.fit(formulas, answers)
 print('学習完了')

 while True:

     # 標準入力から計算式を取得
     print('> ', end='')
     x, y = list(map(lambda x: int(x), input().split(' ')))

     # 学習モデルを用いて回答を取得し、標準出力
     predected_answer = model.predict([[x, y]])
     print("{0} + {1} = {2}".format(x, y, int(predected_answer[0][0])))

実行後、2秒程度で学習が完了する。コレに対して標準入力で適当に2数を入力すると、その予測結果が出力される。

$ python addition.py
学習完了
> 1 1
1 + 1 = 2
> 10 20
10 + 20 = 30
> 777 333
777 + 333 = 1110

良い感じに計算できているように見える。

本モデルの評価について次回行う。

[Ruby] Twitterから画像を自動収集する

前提

以下の環境で実装、動作確認済み

要素 バージョン
debian 8.6
ruby 2.4.1
gem 2.6.11
twitter_oauth 0.4.94

また、記事内で必要となるTwitterAPI用のトークンは既に取得済みであることを前提とする

概要

Twitter上に特定ワードと共に添付された画像ファイルを、任意の枚数自動でダウンロードをするCUIツールをRubyで実装したお話。
画像収集方法として他に以下を検討したが、それぞれ以下の理由で見送った。

  • Bing Image Search API
    — これまで画像収集でお世話になっていたAPIだが、Azureに統合(?)され、実質有料化したので見送り
  • Instagram API
    — イケると思って調べたが、どうやら他のユーザの投稿写真を取得するためには承認を得る必要がある模様
  • Tumblr API
    — Tumblrがよくわからなかったため
  • Google画像検索などのクローリング
    — Pythonでクローリングしようとも検討したが、あまり美しく無いと思ったので見送り

他にもいくつか調査したが、最終的に普段から利用しているTwitterAPIを使うことにした。TwitterのSearchAPIを用いると、ツイートに添付されている画像に関してもまとめて取得できるようになっているので、比較的簡単にできると踏んだため。

ライブラリのインストール

今回はRubyによるCUIツールの形式で実装する。RubyからTwitterAPIを叩くために、普段から利用しているGemライブラリであるtwitter_oauthを今回も使用する。

以下のコマンドを叩くか、あるいは同様のGemfileを作成する。

$ gem install twitter_oauth

TwitterAPI認証

Twitterクラスを実装し、TwitterAPI認証を行うauthメソッドを実装する。API認証をコンストラクタにしなかったのは、インスタンス生成のタイミングで認証が行われるのが気持ち悪いと個人的に感じたため。

authメソッドでは、前項でインストールしたtwitter_oauthモジュールに含まれるClientクラスのインスタンスを生成する。その際にトークンが必要になるので、今回は環境変数から読み込むようにした。

require 'twitter_oauth'
require 'net/http'
require 'uri'
class Twitter
  #
  # TwitterAPIの認証を行う
  #
  def auth
    @twitter = TwitterOAuth::Client.new(
      :consumer_key    => ENV['TWITTER_API_KEY'],
      :consumer_secret => ENV['TWITTER_API_SECRET'],
    )
    puts "Twitter APIの認証完了"
  end
end

検索ワードに合致する写真一覧を取得

特定ワードでツイートを検索し、その中に画像ファイルが添付されている場合にはURLを配列に追加。画像ファイルの総数が指定した枚数に達するまで、再帰的にツイートの検索を繰り返す。という一連の処理を行うsearch_picturesメソッドを実装する。

また、以下ページを参考に、TwitterAPIのレスポンスから添付画像のURLを取得するextract_pictures_from_tweetsメソッドを実装し、ツイート取得ごとに実行する。
GET search/tweets – ツイートを検索する

@@SLEEP_TIMEはAPI呼び出し毎に挟む待機時間。APIは時間あたりの利用回数が決まっているので、それに引っかからないように調整する。

#
# 検索ワードに合致する写真一覧を取得
#
def search_pictures(word, num = 10, opt = {})
  @twitter or self.auth
  params = {
    lang:        'ja',
    locale:      'ja',
    result_type: 'mixed',
    count:       200,
  }.merge(opt)
  puts "画像検索中(残り#{num}枚)"

  tweets = @twitter.search(word, params)['statuses']
  max_id = tweets[-1]['id']
  pictures = extract_pictures_from_tweets(tweets)

  if num <= pictures.count
    return pictures.take(num)
  else
    sleep @@SLEEP_TIME
    return pictures.concat self.search_pictures(word, num - pictures.count, max_id: max_id)
  end
end
#
# TwitterAPIで取得したツイート一覧からmedia情報を抜き取る
#
def extract_pictures_from_tweets(tweets)
  pictures = tweets.map do |t|
    if media = t['entities']['media']
      media.map {|m| m['media_url']}
    else
      []
    end
  end
  pictures.flatten.uniq
end

画像のダウンロード

前項のsearch_picturesメソッドを用いて取得した画像URL一覧を元に、指定したディレクトリにダウンロードするためのdownload_picturesメソッドを以下のように実装する。

こちらは単純に、個々の画像URLに対してNet::HTTPを用いてダウンロードしている。こちらもマナーとして、ダウンロード1件毎に@@SLEEP_TIME秒待機する。


#
# ツイッター上の画像をまとめてダウンロードする
#
def download_pictures(word, download_dir, num = 10)
  pictures = self.search_pictures(word, num)
  pictures.each_with_index do |picture, idx|
    filename = File.basename(picture)
    filepath = "#{download_dir}/#{filename}"
    open(filepath, 'wb') do |file|
      puts "downloading(#{idx + 1}/#{pictures.count}): #{picture}"
      file.puts(Net::HTTP.get_response(URI.parse(picture)).body)
    end
    sleep @@SLEEP_TIME
  end
end

動作確認

実行方法

irbなんかを起動して、

Twitter.new.download_pictures('#デグー', '/share/images', 20)

と実行することで、”#デグー”が含まれたツイートに含まれている画像20件をダウンロードする。もちろん’#デグー’が含まれたツイートに画像が添付されてない場合もあるので、写真数が20に達するまで再帰的にツイートを検索し続ける。

デモ

ソースコード

今回は諸事情でGithubにリポジトリを上げていないので、以下に全ソースコードを掲載する

require 'twitter_oauth'
require 'net/http'
require 'uri'

class Twitter2

  @@SLEEP_TIME = 1

  #
  # TwitterAPIの認証を行う
  #
  def auth
    @twitter = TwitterOAuth::Client.new(
      :consumer_key    => ENV['TWITTER_API_KEY'],
      :consumer_secret => ENV['TWITTER_API_SECRET'],
    )
    puts "Twitter APIの認証完了"
  end

  #
  # 検索ワードに合致するツイート一覧を取得
  #
  def search_pictures(word, num = 10, opt = {})
    @twitter or self.auth
    params = {
      lang:        'ja',
      locale:      'ja',
      result_type: 'mixed',
      count:       200,
    }.merge(opt)
    puts "画像検索中(残り#{num}枚)"

    tweets = @twitter.search(word, params)['statuses']
    max_id = tweets[-1]['id']
    pictures = extract_pictures_from_tweets(tweets)

    if num <= pictures.count
      return pictures.take(num)
    else
      sleep @@SLEEP_TIME
      return pictures.concat self.search_pictures(word, num - pictures.count, max_id: max_id)
    end
  end

  #
  # ツイッター上の画像をまとめてダウンロードする
  #
  def download_pictures(word, download_dir, num = 10)
    pictures = self.search_pictures(word, num)
    pictures.each_with_index do |picture, idx|
      filename = File.basename(picture)
      filepath = "#{download_dir}/#{filename}"
      open(filepath, 'wb') do |file|
        puts "downloading(#{idx + 1}/#{pictures.count}): #{picture}"
        file.puts(Net::HTTP.get_response(URI.parse(picture)).body)
      end
      sleep @@SLEEP_TIME
    end
  end

  private

    #
    # TwitterAPIで取得したツイート一覧からmedia情報を抜き取る
    #
    def extract_pictures_from_tweets(tweets)
      pictures = tweets.map do |t|
        if media = t['entities']['media']
          media.map {|m| m['media_url']}
        else
          []
        end
      end
      pictures.flatten.uniq
    end

end

[Python3] GithubAPI/TwitterAPIを用いて、最新のコミットログをツイートする

前提

以下の環境で実装、動作確認

要素 バージョン
debian 8.6
python 3.6.2

概要

GithubAPIを用いて、自身がGithub上のリモートリポジトリに対してpushした内容からコミットログを取得し、それに関する情報をTwitterAPIを用いてツイートしたお話。

本記事ではTwitterAPIを利用するためのコンシューマキー、アクセストークンは取得済みであることを前提としている。

同じことはGithubのWebhookを使ってできるしむしろ自然だが、その場合サーバーを用意する必要があるので、今回はあえてpull型で非効率なやり方を採用

動作イメージ

リモートリポジトリがGithub上にあるリポジトリに対して適当にコミット/プッシュ

$ git commit -m "コミットテスト"
[master e6bb9e4] コミットテスト
 1 file changed, 1 insertion(+)
$ git push origin master
Counting objects: 3, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (3/3), done.

今回実装したコマンドを実行する

$ python main.py

以下のように、リポジトリ名、コミットメッセージ、diffへのリンクがツイートされる

GithubAPIについて

GithubAPIは、名前の通りGithubの各種読み書きを行うためのAPI。
公開されているデータの取得に関しては認証無しで手軽に利用できる。

例えば、curlコマンドを用いて以下のURLに対してGETリクエストを送信すると

curl -i https://api.github.com/users/sa2knight/events

以下のように、該当ユーザのGithubでのイベントログを取得することができる

[
  {
    "id": "6516862394",
    "type": "PushEvent",
    "actor": {
      "id": 16274215,
      "login": "Sa2Knight",
      "display_login": "Sa2Knight",
      "gravatar_id": "",
      "url": "https://api.github.com/users/Sa2Knight",
      "avatar_url": "https://avatars.githubusercontent.com/u/16274215?"
    },
    "repo": {
      "id": 96615971,
      "name": "Sa2Knight/degulog2",
      "url": "https://api.github.com/repos/Sa2Knight/degulog2"
    },
    "payload": {
      "push_id": 1946817475,
      "size": 1,
      "distinct_size": 1,
      "ref": "refs/heads/master",
      "head": "4208370eb0cda0a1bab02fd3fbe5bf7e3e6f5f29",
      "before": "afd8b8647c2b810ceb731a74bad20c7388c19974",
      "commits": [
        {
          "sha": "4208370eb0cda0a1bab02fd3fbe5bf7e3e6f5f29",
          "author": {
            "email": "shingo.sasaki.0529@gmail.com",
            "name": "shingo sasaki"
          },
          "message": "バックアップ追加",
          "distinct": true,
          "url": "https://api.github.com/repos/Sa2Knight/degulog2/commits/4208370eb0cda0a1bab02fd3fbe5bf7e3e6f5f29"
        }
      ]
    },
    "public": true,
    "created_at": "2017-08-27T15:22:08Z"
  },
  {
    "id": "6516846556",
    "type": "PushEvent",
(以下省略)

ここで取得できるイベントは、Webで言う以下のような、そのユーザのリポジトリに対する操作全般の情報なので、今回はこのAPIを用いることにする

ライブラリの導入

今回はPython3を用いて実装するので、Pythonのパッケージ管理ツールであるpipを用いて、以下の二種類のライブラリを導入する。

  • requests
    — HTTPライブラリ
  • twitter
    — TwitterAPI用

TwitterAPIの利用には認証も絡んでくるので、専用のライブラリを利用する。GithubAPIは特定URLにGETするだけなのでHTTPライブラリで直接実行することに。

$ pip install requests
$ pip install twitter

ソースコード

最新版はこちら

import twitter
import requests
import json
import os

FILE_NAME      = "last_id"
EVENTS_URL     = "https://api.github.com/users/sa2knight/events"
REPOSITORY_URL = "https://api.github.com/repos"

def tweet(text):
  auth = twitter.OAuth(consumer_key=os.environ['TWITTER_CONSUMER_KEY'],
                       consumer_secret=os.environ['TWITTER_CONSUMER_SECRET'],
                       token=os.environ['TWITTER_ACCESS_TOKEN'],
                       token_secret=os.environ['TWITTER_ACCESS_SECRET'])
  t = twitter.Twitter(auth=auth)
  t.statuses.update(status=text)

def tweet_event(event):
  tweet_text = f"""
  @null
  Githubにコミットをプッシュしました。
  [{event['repository']}]
  「{event['commits'][0]['message']}」
  """.strip()
  if 1 < len(event['commits']):
    tweet_text += f"ほか{len(event['commits']) - 1}件"
  tweet_text += f"\n\n{event['commits'][0]['url']}"
  tweet(tweet_text)

def save_id(id):
  with open(FILE_NAME, 'w') as f:
    f.write(id)

def load_id():
  if os.path.exists(FILE_NAME):
    with open(FILE_NAME, 'r') as f:
      return f.readline()
  else:
    return ''

def is_new_id(id):
  return id != load_id()

def parse_commit_log(repo_name, commit):
  return {
    'url': f"https://github.com/{repo_name}/commit/{commit['sha']}",
    'message': commit['message']
  }

def get_repository_description(repo_name):
  url = f"{REPOSITORY_URL}/{repo_name}"
  response = requests.get(url)
  repository = json.loads(response.text)
  return repository['description']

def get_recent_push_event():
  response = requests.get(EVENTS_URL)
  events   = json.loads(response.text)
  recent_event = list(filter(lambda e: e['type'] == 'PushEvent', events))[0]
  repo_name    = recent_event['repo']['name']
  commits      = list(map(lambda c: parse_commit_log(repo_name, c), recent_event['payload']['commits']))
  return {
    'id':         recent_event['id'],
    'repository': get_repository_description(recent_event['repo']['name']),
    'commits':    commits,
  }

event = get_recent_push_event()
if is_new_id(event['id']):
  tweet_event(event)
  save_id(event['id'])
  print('ツイートを投稿しました')
else:
  print('ツイートは不要です')

スクリプトを実行すると概ね以下の手順でツイートされる
1. GithubAPIを叩いて、自身の最新のコミットを含んだイベントログを取得
2. 直近で取得したイベントIDをファイルに保存しておき、そのIDと比較、一致していた場合変更なしとして終了
3. イベントログを元にツイートするテキストを生成
4. TwitterAPIを用いてツイート

所感

  • 本スクリプトを15分おきぐらいにcronで実行すれば、概ねコミット内容がツイートされる
  • Webhookと違ってpull型になるので、リアルタイムにはできない
  • 単にGithubAPIが使いたくて遊んだだけで実用性はあんまり無い