おいしい健康 開発者ブログ

株式会社おいしい健康で働くエンジニア・デザイナーが社内の様子をお伝えします。

BigQueryで協調フィルタリングを使って使用食材が似たレシピを探す話

こんにちは。おいしい健康エンジニアの花井です。

今年の8月に入社しまして、iOS(クライアント)、API(サーバーサイド)、データ分析など幅広くやっています。 言語で言うと、Ruby(Ruby on Rails), Swift, Pythonですね。

今回は、食材が似たレシピを探す話、と題しておいしい健康のデータ分析の話をします。

概要

おいしい健康で最も人気がある下記レシピに対して、使用食材が似ているレシピを探します。 oishi-kenko.com BigQueryはクエリを気軽に実行して試行錯誤できるので最適化問題のソルバーとしてとても優秀です!

おいしい健康のデータ分析まわり

f:id:oishi-kenko:20181109142033p:plain

おいしい健康ではデータウェアハウスとしてBigQueryを使っています。

BigQueryへのデータコピーには、WebやアプリのログではFluentd, データベースに保存しているレシピや食品成分表等のデータではEmbulkを使っています。

データ可視化にはGoogle Data Studio*1を使っており、レポートやダッシュボードをサクッと作り社内会議で共有しています。 Google Data Studioでは表現できない複雑な図表を作成するときはGoogle Colaboratory*2を使っています。

使用食材が似ているレシピを探す

レシピには食材・調理手順・調理器具など様々な要素がありますが、今回は食材のみ着目します。

定義

食材(Ingredient)の集合を {I = (i_1, i_2, \cdots, i_n)}, レシピ(Recipe)の集合を {R = (r_1, r_2, \cdots, r_m)}と表記します。

レシピ {r_i}に対する食材 {j}の使用量を {w_{r_i,j}}と定義し、 レシピ {r_i}に対する食材使用率ベクトル {v_{r_i}}を次のように定義します。

\begin{align} \vec{v_{r_i}} = \frac{1}{\sum_{k=1}^n w_{r_i,k}} (w_{r_i,1}, w_{r_i,2}, \cdots, w_{r_i,n}) \end{align}

今回のデータ分析では食品成分表*3を利用し、調味料等を除外して食材の使用量を算出しています。

クエリでは、次のように実装できます。

協調フィルタリング

協調フィルタリングは、ユーザ間やアイテム間の類似性に基づいて推薦アイテムを決定する推薦アルゴリズムです。今回は類似性の指標としてコサイン類似度を採用し、設計します。

レシピ {r_i, r_j}に対するコサイン類似度 {s_{r_i r_j}}を次のように定義します。

\begin{align} s_{r_i r_j}= \frac{\sum_{k=1}^n v_{r_i,k} v_{r_j,k}}{\sqrt{\sum_{k=1}^n v_{r_i,k}^2} \sqrt{\sum_{k=1}^n v_{r_j,k}^2}} \end{align}

具体例を見てみましょう。

f:id:oishi-kenko:20181112185426p:plain

食材の集合 {I = (i_1, i_2, \cdots, i_n)}に対してそれぞれのレシピで使用してる食材は数種類なので、食材使用率ベクトルのほとんどの成分はゼロです。例にあげている2つのレシピに対する使用食材の成分だけ取り出すると次のようになります。

f:id:oishi-kenko:20181113114048p:plain

大雑把に言えば、この2つの図形がどのくらい一致するか、という指標がコサイン類似度です。 ベクトル同士の成す角度の近さを表現しており、1に近づくほど似ている、0に近づくほど似ていない、となります。

この例ではコサイン類似度は0.94958となり、よく似ていると判断できます。

まとめると、使用食材が似たレシピを探す話は、あるレシピ  {r_i}に対してコサイン類似度  {s_{r_i r_j}}が最大となるようなレシピ {r_j}を求める最適化問題となります。

\begin{align} arg\max_{r_j \in R} s_{r_i r_j} \end{align}

今回は、この最適化問題のソルバーとしてBigQueryを使い、総当たり*4で求めます。

BigQueryのメリット

Python等でも実装できますが、BigQueryはこんなメリットがあります。

  • 標準SQLが使えるので学習コストが安く、エンジニア間のコミュニケーションが容易。
  • メンテナンスコストも安い
  • 実行速度がとても速いため総当たりで解ける
  • Google製品と簡単に連携できて、結果出力やデータビジュアリゼーションの実行環境の構築コストが安い

コード

いよいよ実装に入ります。 BigQueryでは協調フィルタリングを次のようにシンプルに実装できます。

類似度1位を見てみよう

実は、コサイン類似度が最も大きいレシピはコサイン類似度で例を出したものでした。 oishi-kenko.com

しかし....

このレシピが最も似ているレシピだと納得できるでしょうか。

コサイン類似度が0.94958ですが、似ていると判断できるでしょうか。

「落とし揚げ」のレシピにはレタスも玉ねぎも生姜も食材に含まれていないです。

もう1度、食材使用率ベクトルを見てください。 f:id:oishi-kenko:20181113114048p:plain

豆腐と鶏ひき肉が多く含まれているレシピが選ばれていることが分かります。

つまり、食材使用率の低い食材が無視されてしまっています。

少量の食材でもレシピに含まれてほしいですね。

そこで、定義を変更します。

ingredient.total_gram / SUM(total_gram) OVER(PARTITION BY ingredient.recipe_id) AS gram_per_recipe

食材使用率に1を足し、下駄を履かせます。

少量の食材も考慮されるようにします。

1 + ingredient.total_gram / SUM(total_gram) OVER(PARTITION BY ingredient.recipe_id) AS gram_per_recipe

1位の結果を見てみましょう。 oishi-kenko.com コサイン類似度は0.80864です。

食材使用率ベクトルはこのようになりました。 f:id:oishi-kenko:20181113113904p:plain

5つの食材が被っているレシピが選ばれました。

レシピ類似度のデータビジュアリゼーション

以下はおまけです。 レシピ間の類似度ネットワーク*5を可視化してみます。

可視化対象

すべてのレシピ間を図示してしまうと辺だらけのネットワークになってしまうので、コサイン類似度が0.7以上のレシピ間を可視化します。

頂点 {V} : レシピ集合  {R = (r_1, r_2, \cdots, r_m)}

 {E} : { {(r_i, r_j) | s_{r_i r_j} \geqq 0.7 for \forall r_i, r_j \in R}}

データ取り出し

BigQueryのコンソール画面でGoogleスプレッドシートへ出力できる機能があるのですが、行数が多すぎるため使えません。 そんなときは、Google Colaboratoryが便利です。 下記のようにCSVファイルを簡単に取得できます。

可視化したネットワーク

ネットワーク可視化ソフトウェアプラットフォームCytoscape*6が便利です。

以下のフィルターを適用しました。

  • 頂点の次数*7が大きいほど大きくする描画する
  • 頂点の次数が大きいほど赤く、小さいほど青く描画する

巨大なネットワークと複数の小規模ネットワークができました。

f:id:oishi-kenko:20181112182528p:plain f:id:oishi-kenko:20181112182529p:plain

いくつかクラスターがあり、その中にハブとなるようなレシピが存在していることが分かります。

今回は可視化しただけですが、レシピのネットワーク構造を研究してサービスに活かせそうですね。

*1:https://datastudio.google.com/overview

*2:https://colab.research.google.com/

*3:おいしい健康では食品やレシピの栄養価は、主に文部科学省から食品の栄養価が書かれた一覧表(日本食品標準食品成分表 2015年版(七訂))を用いて算出しています。

*4:力まかせ探索、しらみつぶし探索とも言います

*5:数学用語ではグラフといいます。グラフG, 頂点集合V, 辺集合EのときG=(V,E)と表記します。この記事ではグラフではなくネットワークと書くことにします。

*6:バージョン3.7.0 https://cytoscape.org/

*7:頂点から出ている辺の数