REQLY開発ブログ

プロの料理レシピ専門検索エンジンREQLY(レクリー)の開発ブログです。

ランダムフォレストを用いてスイーツ識別器の作成に挑む

はじめに

こんにちは。エンジニアのkenboです。レシピ検索エンジンREQLYでは、検索フィルタを活用することで検索ワードを入力せずに検索をすることも可能です。例えば、「スイーツ作りにチャレンジしたいけど何を作ろうかな・・・」という場合には、「スイーツ」と「簡単」をフィルタにセットして検索すれば、きっと良いレシピが見つかるはずです。ところで、REQLYではどのようにして各レシピが「スイーツ」であるか判断してるのでしょうか?今回はそんな機械学習の舞台裏をお見せします。

reqly.tokyo


ルールベースによる分類?

どのようにすれば、レシピがスイーツであるか判断できるでしょうか?例えば、多くのスイーツには「砂糖」と「卵」の両方が使われているように思います。まずはこの考えをもとに決定木を作ってみましょう。

f:id:reqly-tokyo:20180704004022p:plain
図1 決定木を用いた分類

残念ながらこの条件では、チョコバナナ、ゼリー、だし巻き卵のような場合にうまく分類できないことがわかります。追加で「チョコ」や「みりん」を加えようと考えるかもしれませんが、それでも上手くいかない例は無数にあるでしょう。このように、スイーツかどうかの判断には複数の材料を考慮する必要がありますが、そのような識別器を人手でルールベースで構築していくことは現実的ではありません。このような問題を解決する手法が、機械学習のRandom Forestという方法です。Random Forestを用いれば、コンピュータが自動でルールを作成してくれます。


Random Forestとは

Random Forestは、コンピュータが決定木を複数作りそれらの多数決を取ることで分類をおこなう手法です。イメージ図を見ながら、分類の流れを確認していきましょう。

f:id:reqly-tokyo:20180705111716p:plain
図2 Random Forestの概念図

まず、決定木をN本作成します。各決定木は、全データ集合から独立に生成されたN個のサブサンプルをそれぞれの教師データとして学習されたものです。この際、それぞれの決定木が使うことができる説明変数もランダムに選ばれます。従ってこれらの決定木は互いに異なり、同じ入力に対しても出力が異なる場合があります。識別の際には、N本の決定木の出力の多数決を取ることで全体としての出力を決定します。このように、独立に学習した決定木を複数用いることで全体としての汎化性能を挙げている点がRandom Forestの大きな特徴です*1。教師データと説明変数の両方をランダムに選択することで、互いに相関の低い決定木を作成し汎化性能をあげています。

他の機械学習アルゴリズムと比較したときのRandom Forestのメリットは、以下のような点が挙げられます。

  • 決定木をベースに構築されているので、特徴量に対するスケーリングが不要
  • 各決定木の学習は独立しているので、並列化が容易
  • モデルに対する特徴量の重要度がわかるので、特徴量選択を行いやすい


Random Forestによるスイーツ識別器

では、実際にRandom Forestを用いてスイーツ識別器を作成してみましょう。

教師データとして、正解ラベルが付与された計3305件(正例と負例がおよそ半分ずつ)のレシピデータを用意しました。それぞれのレシピデータは、各食材がそのレシピに含まれているかどうかの480次元のバイナリ特徴ベクトルに変換できます(図3)。

f:id:reqly-tokyo:20180704122811p:plain
図3 レシピデータを特徴ベクトルに変換

これらのデータに対し、Python機械学習ライブラリであるscikit-learnを用いてスイーツ識別器を作成しましょう。 まずはデフォルトのパラメタで推定してみます。

from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split

# データセット(X: 特徴ベクトル, y: 正解ラベル)をトレーニング用とテスト用に分割
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

# RandomForestのモデル宣言と学習
forest = RandomForestClassifier()
forest.fit(X_train, y_train)

# テストデータを用いて精度評価
print("training accuracy: {}".format(forest.score(X_test, y_test)))

結果Accuracyは95.81%ほどであり、この時点でほとんどのレシピを正しく分類できていることがわかります。

精度はほぼ十分なのですが、念のためもう少し粘ってみます。RandomForestにはユーザーが設定できるパラメタがいくつかあるのですが、これらの組み合わせのうち最も精度が高いものを全探索する方法として、 Grid Searchと呼ばれるものがあります。 Grid Searchを実際に試してみましょう。探索パラメタの候補は、それぞれ以下のように設定しました。

params = {
    'n_estimators': [1, 10, 100, 500],  
    'max_features': [1, 'auto', None],  
    'max_depth': [1, 5, 10, None],  
    'min_samples_leaf': [1, 2, 4,]
    }  

それぞれのパラメータは以下のようになります、

  • n_estimators: 作成する決定木の数
  • max_features: 各ノードで最適な分割を探す際に考慮する数(int: 個数、float: パーセント、auto: sqrt(n_features)、None: n_features)
  • max_depth: 決定木の深さの最大値 (int: 深さ、None: 全ての葉がmin_features以下になるまで分割を続ける)
  • min_samples_leaf: 各葉が持つサンプルの最小値

Grid Searchも、先ほどのscikit-learnを用いることで手軽におこなえます。

from sklearn.grid_search import GridSearchCV

# GridSearch
model = RandomForestClassifier()
cv = GridSearchCV(model, params, cv=10, scoring="accuracy")
cv.fit(X, y)

# 最適なパラメタを出力
print("Best parameters: {}".format(cv.beast_params_))

その結果、最適だと判断されたパラメタと精度は以下のようになりました。

best_param = {
    'n_estimators': 500,
    'max_features':1,
    'max_depth': None,
    'min_samples_leaf': 1
    }

GridSearch Accuracy: 96.93%

このように、Grid Searchを用いることで1%ではありますが精度の改善が見られました。問題設定によっては大幅な改善が見られることもあります。

最後に、どの特徴量がスイーツ識別に重要であるのか上位10件を見てみましょう。 

f:id:reqly-tokyo:20180704133738p:plain
図4 特徴量重要度Top10

予想通り、砂糖が圧倒的に重要な要素となっており、次いでチョコレートや卵などが上位に現れていることが分かります。同時に、醤油やコショウのようにスイーツではないということを判断するのであろう情報も上位に現れているのが興味深いですね。

まとめ

REQLYでは、この他にも様々な機械学習の手法を用いることで、高機能な料理レシピ検索を実現しています。ぜひ一度使ってみてください。 reqly.tokyo

*1:このように複数の弱識別器の結果を統合する方法をアンサンブル学習と言います。