機械学習をつかって「今日なに着てく?」を教えてもらうためのモック

tech.kayac.com Advent Calendar 2015 14日目担当の @_somtd です。

最近暑かったり、寒かったりどうもおかしな気候がつづきますね。
若いエンジニアがどんどん増えて立場が危うい昨今。気候の変化に柔軟に対応し、身体だけは健康でいたいと願っています。
そんな中、今回のアドベントカレンダーでは機械学習をつかって「今日なに着てく?」を教えてもらうためのモックを試します。
問題を簡単にするために(時間がなくてリアルなデータを用意できなかったので)以下のデータセットを用意します。

データ

100日分の「なにを着たか?」というデータ
sample_wear.txt

  • temperature:気温

  • rainfall:降水確率

  • outer:アウター(コート等)
    (0:着ない/1:ジャケット/2:ナイロンパーカー/3:ダウンジャケット)
    outer

  • tops:トップ(上着)
    (0:Tシャツ/1:シャツ/2:パーカー/3:セーター)
    tops

  • bottoms:ボトム(ズボン)
    (0:ハーフパンツ/1:チノパン/2:ジーパン)
    bottoms

推定するデータ

今日の気温降水確率を入力するとこれまでの「なにを着たか?」の情報から今日なに着てくかを教えてくれる。

でははじめに

アウター、トップス、ボトムスのカテゴリー毎に 横軸に気温縦軸に降水確率をマッピングしたものを表示させます。
データの読み込みを行い、

import numpy as np
from matplotlib import pyplot as plt

data = np.genfromtxt('sample_wear.txt', delimiter=',', skiprows=1)

temperature=data[:,0]
rainfall=data[:,1]
outer    = data[:,2]
tops     = data[:,3]
bottoms  = data[:,4]

データのプロットを行います。

# http://matplotlib.org/api/markers_api.html
plt.figure(figsize=(12,3))
plt.subplots_adjust(bottom=.2)

plt.subplot(1,3,1)
plt.xlim(-10., 110.)
plt.ylim(-10., 40.)
plt.xlabel("rainfall(%)")
plt.ylabel("temperature")
plt.title("outer", fontsize='small')
for t,marker,c in zip(xrange(4),"ox^D",'bgrc'):
     plt.scatter(data[outer == t,1],
                 data[outer == t,0],
                 marker=marker,
                 c=c)
# tops, bottomsのプロットは省略

plt.show()

outer_plot
アウターです。適当に作ったデータのため、存在しない降水確率があったりしますね。

tops_plot
トップです。

bottoms_plot
ボトムです。

気温や降水確率と着ていく服が、なんとなく分かれているような、分かれていないような感じになっています。

機械学習をさせてみる

ここからが本題です。プロットされたデータを眺めてみて着ている服の分類が何とかできないか考えてみます。
本記事では、scikit-learn(sklearn)というPythonで実装された機械学習ライブラリを用います。
用意されているアルゴリズムも豊富で比較的手軽に試すことができるのが特長です。
導入自体も容易で、今回のようなモックの用途にも適していると思います。

今回はサポートベクターマシン(SVM)による教師あり機械学習を行い、分類を試みてみます。

サポートベクターマシンをざっくりと補足しておくと、
サポートベクターマシンの特徴として、「マージン最大化」が挙げられます。

学習データの中で最も他クラスと近い位置にいるもの(これをサポートベクトルと呼ぶ)を基準として,
そのユークリッド距離が最も大きくなるような位置に識別境界を設定する
参考

ということだそうです。

では実際に分類を行っていきます。ここではアウターついての例を見ていきます。

import numpy as np
from sklearn import svm
from matplotlib import pyplot as plt
          
data = np.genfromtxt('sample_wear.txt', delimiter=',', skiprows=1)

outer    = data[:,2]
tops     = data[:,3]
bottoms  = data[:,4]

#outer 分類
#outerなし
no_outer_temp = data[outer == 0.,0]
no_outer_rain = data[outer == 0.,1]
no_outer = np.c_[no_outer_temp,no_outer_rain]

#outerジャケット
jkt_outer_temp = data[outer == 1.,0]
jkt_outer_rain = data[outer == 1.,1]
jkt_outer = np.c_[jkt_outer_temp,jkt_outer_rain]

#outerナイロンパーカー
nyln_outer_temp = data[outer == 2.,0]
nyln_outer_rain = data[outer == 2.,1]
nyln_outer = np.c_[nyln_outer_temp,nyln_outer_rain]

#outerダウンジャケット
dwn_outer_temp = data[outer == 3.,0]
dwn_outer_rain = data[outer == 3.,1]
dwn_outer = np.c_[dwn_outer_temp,dwn_outer_rain]

ここでは、各アウターの種類毎に、着用した時の気温や降水確率を特徴量として取り出しています。
以下はデータ例です。上着を着なかったときの特徴量、no_outerのデータです。

[[  13.8    0. ]
 [  14.3    0. ]
 [  15.     0. ]
 [  16.8    0. ]
#省略
 [  30.4   80. ]
 [  32.5    0. ]
 [  32.9   40. ]
 [  36.9   10. ]]

これらの特徴量行列を連携したものと、特徴量に対応したラベルデータを使って、学習を行います。

#トレーニングデータ(気温、降水確率)
training_data = np.r_[no_outer,jkt_outer,nyln_outer,dwn_outer]
#教師データ(気温、降水確率に対応するアウターの種類)
training_labels = np.r_[
        np.zeros(len(no_outer)), #0:なし
        np.ones(len(jkt_outer)) * 1, #1:ジャケット
        np.ones(len(nyln_outer)) * 2, #2:ナイロンパーカー
        np.ones(len(dwn_outer)) * 3, #3:ダウンジャケット
        ]

classifier = svm.SVC(gamma=0.001)
classifier.fit(training_data, training_labels)

例えば、特徴量[13.1,90]に対して、答えは2(ナイロンパーカー)と教えてあげているわけです。

以下の部分で指定しているgammaというパラメータはハイパーパラメータいって分類の精度を上げるために非常に重要なものです。

classifier = svm.SVC(gamma=0.001)

gammaの値が小さいほど単純な決定境界となり, 大きいほど複雑な決定境界となります。 今回は0.001として学習をおこないます。
SVMのハイパーパラメータについては、こちらの記事が参考になります。
SVM(RBFカーネル)のハイパーパラメータを変えると何が起こるの?
※本来であればパラメータチューニングに時間をかけるべきなのですが本記事では省略させていただきます。

学習は以下の記述で行います。

classifier.fit(training_data, training_labels)

学習した結果をもとに、取りうる範囲内のデータを使って推定を行います。
推定は、以下のように行います。

classifier.predict(temperature, rainfall)
training_x_min = training_data[:, 0].min() - 1
training_x_max = training_data[:, 0].max() + 1
training_y_min = training_data[:, 1].min() - 1
training_y_max = training_data[:, 1].max() + 1

grid_interval = 0.02
xx, yy = np.meshgrid(
        np.arange(training_x_min, training_x_max, grid_interval),
        np.arange(training_y_min, training_y_max, grid_interval),
        )
# 取りうる値の範囲内の全データを0.02間隔で生成する
Z = classifier.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)

#contourf:塗りつぶし等高線 (Zはラベルの配列)
plt.contourf(xx, yy, Z, cmap=plt.cm.bone, alpha=0.2)

結果は以下の図になります。

outer_predict

推定してみる

試しに、温度20℃、降水確率50%の場合に何を着るべきかを推定してみます。

classifier.predict([20., 50.])

出力は0 となり、アウターは要らないという結果になりました。 図示されている結果を見てみてもお分かりいただけると思います。

分類の精度を検証する

最後に、今回の分類器の精度を検証してみます。 精度の検証には、K-分割交差検証を行います。

# K-分割交差検証
 kfold = cross_validation.KFold(len(training_data), n_folds=10)
 results = np.array([])
 for training, test in kfold:
     classifier = svm.SVC(gamma=0.001)
     classifier.fit(training_data[training], training_labels[training])
     answers = classifier.predict(training_data[test])
     iscorrect = answers == training_labels[test]
     results = np.r_[results, iscorrect]
 
 correct = np.sum(results)
 N = len(training_data)
 percent = (float(correct) / N) * 100
 print(percent)

実行してみた結果

28.5714285714

な、なんとまさか正答率30%を割り込んでしまいました。
つまり、分類の精度がイマイチということですね。

これだとバツがわるいので前述した、ハイパーパラメータの値を変えることで分類の精度がどう変化するか見てみましょう。

classifier = svm.SVC(gamma=0.001)

gammaの値を0.001から0.01に変更します。
gammaの値が大きいほど複雑な決定境界となるのでしたね。

classifier = svm.SVC(gamma=0.01)

実行してみた結果

42.8571428571

gammaの値を変えることで正答率は大幅にアップしました。
こういったパラメータチューニングを繰り返して、分類器の精度をあげていかないといけないわけですね。

今後の課題

駆け足になってしまいましたが、「今日なに着てく?」を教えてもらうためのモックにはまだまだのようです。
雑ですがソースはこちらに上げておきました。somtd/what2wear

以下に今後の課題をまとめておきます。

  • そもそもデータを増やす。リアルなデータを増やす。

  • シューズなどアイテムもデータとして追加できるようにする。

  • 日々のデータの蓄積の際に、今日着たものについて評価できるようにする。

  • Googleカレンダーなどから当日の予定を元にしたTPOの判定ができるようにする。

  • 服同士の組み合わせも考慮できたりする。

  • あわよくば、「こういう服持っておいたほうがいいんじゃないの?」というリコメンドができるようにする。

以上です。 明日の担当はゴンです。 ゴンはすごいです。