好きな事で生きていく

統計的機械学習、因果推論、マーケティングサイエンス

pixivの2019春インターンでレコメンドモデルを作りました

概要

2/20から3/8にかけて、Pixiv Spring Boot Campというpixivの春インターン機械学習コースに参加しました。pixiv本体(イラスト交流サイト)のレコメンドモデルを改良するための技術検証を行いました。

選考フロー

書類選考+面接でした。コーディング試験があって、AtCoderのB問題相当*1の問題をホワイトボードに解きました。面接帰りに自分が書いたコードの間違いに気づいて落ち込みましたが、結果的に通ったのは幸いでした。

インターンでやった事

pixiv本体のレコメンドモデルを作りました。
pixivでは

などでベクトル化したイラストの類似度を計算して、イラスト毎に距離が近いイラストを推薦しています。 協調フィルタリングや行列分解は「このイラストをブックマークした人がブックマークしそうなイラストを推薦する」というカテゴリベースの推薦で、CNNを使ったものは「このイラストと見た目が近いイラストを推薦する」という見た目ベースの推薦です。
インターンでは、カテゴリベースの推薦と見た目ベースの推薦に使用する特徴量を全て投入する事で、カテゴリと見た目双方を考慮したハイブリッドで優れたレコメンドモデルを作成する事を目指しました。
Big QueryからAWSのEC2にデータを落として、SQLiteの学習済データと合わせて、Jupyter notebook上でTensorflowのモデルを構築して、モデルから取り出したイラストベクトルを元に社内ツール上でレコメンド結果を定性的に評価する、という流れです。

モデル

最終的に作ったのは「あるユーザーがあるイラストをブックマークするかどうか」を分類するモデルです。このモデルを学習させた後、イラストに対応するベクトル(Dense(20)の出力)を取り出して距離を計算します。

f:id:hanon05:20190309153145p:plain
model

イラスト特徴量とイラストタグは既に計算されたものを使います。イラストはVGG, タグはword2vecが使われたそうです。

工夫点1

ユーザーIDとイラストIDのEmbeddingの内積を計算するNNのモデルは行列分解と式の形が似ています。そのため、行列分解と同じようなタスクを解く事が出来ると考えました。しかも今回作ったNNのモデルは最終的にユーザーIDのベクトルとイラストIDのベクトルの長さが揃えば良いので、イラストIDのEmbedding以外の特徴量を投入する事ができます。今回は時間が無くて試せませんでしたが、ユーザー情報も入れればもっと精度が上がるかもしれません。

工夫点2

作成モデルは分類精度を最大化するのが目的ではなく、イラストに対応するベクトルを取り出すのが目的です(アイテムベースのレコメンドを行いたいから)。なので、ユーザーベクトルとイラストベクトルを合わせる場所はDenseではなく内積を用いて、イラスト特徴量系は内積計算前に投入してDenseでよしなに整えます。イラストに関する情報が内積直前のDense(20)に集約される事を期待しました。
このモデルでは「あるユーザーがあるイラストをブクマするかどうか」の確率が得られるのでパーソナライズされたレコメンドが可能ですが、サービスに組み込む際の実装コストを考慮して、今回はイラスト情報のみによるレコメンドを想定しました。

工夫点3

イラストIDの重み行列は対応するイラストIDが学習データに含まれている部分しか誤差が逆伝搬しないので、イラスト特徴量やイラストタグに比べて学習が遅くなります(多分)。学習の際にバッチサイズが128や512だと、イラスト特徴量ばかり更新されたりその下のDense(20)がイラスト特徴量のみを重要視するように学習されたせいか、valid lossが落ちず直ぐに上がっていきました。しかし、バッチサイズを16000くらいにするとvalid lossがちゃんと下がりました。Embeddingレイヤーの重み更新がミニバッチに対してどのような実装になっているかはっきりとした事は言えませんが、ミニバッチ内で使用されたイラストID毎にそのイラストIDを経由して計算されたLossの平均を逆伝搬していると仮定すると、バッチサイズを上げればバッチ内で使用されるイラストIDの数が増えて、イラストIDの重み行列全体におけるそれなりの割合が同一の逆伝搬計算で更新されるはずです。するとイラスト特徴量の重み行列の更新回数との差分が縮小し、イラストIDの重み行列の学習が進んだ可能性があります*2。 イラストIDのEmbeddingの重み行列を他の特徴量の重みと揃えていい感じに学習させるには、イラストIDのユニーク数などを考慮してバッチサイズをかなり大きめにとると良いかもしれません*3

工夫点4

学習に使ったデータはどのユーザーがどのイラストをブックマークしたか、というデータのみを用いました。そうすると負例が用意出来ず、どんな(ユーザー, イラスト)の組を入れても1を返すモデルが出来てしまいます。そこで、ランダムに選んだユーザーとイラストの組を正例と同じ数用意し、負例を作成しました。ブックマークした(ユーザー, イラスト)の組は負例から弾いた方が良いのですが、計算時間の短縮を優先しました。最初は「ただのノイズを加えてるだけでは?」とモヤモヤしていましたが、「ランダムに選んだ(ユーザー, イラスト)の組はブックマークが観測されている(ユーザー, イラスト)の組よりブックマーク確率が低いだろう」という順位を学習する枠組みがあるらしく、ある程度理論的な裏付けが取れて安心しました。Bayesian Personalized Rankingというらしいです。

結果

レコメンドイラストは載せられませんが、カテゴリベースと見た目ベースの推薦のいいとこ取りをしたモデルが出来ました。
カテゴリベースのレコメンドモデルは推薦元のイラストとカテゴリが近いイラストや人気度(ユーザーがそのイラストを見たときにブックマークをする確率の高さ)の高いイラストをレコメンド出来ます。一方で、コールドスタート問題があったり、同じカテゴリでもあまりにもかけ離れた見た目のイラストを推薦してしまうことがあります。
見た目ベースのレコメンドモデルは、風景など見た目で人気度がわかりやすいイラストはしっかりと推薦出来るのでコールドスタート問題に対応出来ます。一方で、人気度を考慮出来なかったり、似た見た目でも全く異なるジャンルのイラストを推薦する場合があります。
作成モデルは、見た目ベースのレコメンドをベースにしつつ、カテゴリ推薦の成分が混ざることでカテゴリー的に離れすぎずより人気度の高いイラストを推薦するようになりました。カテゴリベースと見た目ベースの良いとこどりが出来たので、どんなタイプのイラストに対してもそれなりに安定して推薦をしていました。めっちゃいい感じになって嬉しかったです。

感想

インターン内容について

他のコースと比べて一週間長い17日間のインターンでしたが、時間的にはギリギリでした。まともな結果が出たのが16日目の午前11時で、インターン期間があと1日短かったら何も結果が出ないまま終わる所でした。最後までやってみないと結果が出るかわからないのは、開発系と比べた機械学習やデータ分析の難しい所です。作業をもっと小さく回してピボットを頻繁に行えばもう少し確実に成果を出せたかもしれません。

機械学習全般やSQLの基礎知識はあったもののレコメンドエンジンを作るのは初めてだったので、良い勉強になりました。またBQやSQLite, TF eagerの使い方の知見も貯まりました。

メンターの方がDeep Learningにとても詳しく、モデルの作成や拡張でとてもお世話になりました。ありがとうございます。

レコメンドモデルは奥が深いですね、、。ユーザーのどのような体験を実現したいかによってどのようなレコメンドをするべきかであったり、どの指標を最大化するべきかが変わってくると感じました。また、アプリ内の場所によって行うべき推薦が変わりそうです。例えば、トップページの推薦はカテゴリベースの推薦を行なっていても、イラスト詳細ページの推薦は見た目が似ているイラストを一緒に推薦した方がユーザーは嬉しいかもしれません。アルゴリズムを高度化するだけでなく、ユーザーの目線に立ってUXまで考えられたら最強だなと思いました。

過去のレコメンド履歴のログは取れていたので、レコメンドされたイラストをブックマークしたかどうかを上手く捉えられればより高精度なモデルが学習出来るかと思ったのですが、あまり精度が上がりませんでした。一つのモデルで高精度な推薦を行うより、ユーザーに推薦するイラストの候補を検索するモデルと、ユーザーがそれらのイラスト群のどれを好むかを順位付けするモデルに分割した方が良さそうです。

その他諸々

オフィスがめっちゃカラフルでした。TeamLabが監修しているそうです。
徘徊するルンバ
趣味人な方がとても多かったです。終業時刻後もどこかしらで何かしらの会が開かれていて、絵描きの会や麻雀の会、Aqoursライブ上映会などをやってました。会社のつよつよPCでゲームする人、ギターを弾く人、セグウェイを乗り回す人など、賑やかな雰囲気が印象的でした。

美味しいご飯を何度かご馳走になりました。特に美味しかったのはタルタルステーキです。冷たい牛肉のタルタルと熱いポテトの組み合わせが最高でした。

f:id:hanon05:20190309175521j:plain
タルタルステーキ、何が何だかわからないけどとにかく美味しい

tabelog.com

事業会社がどのような考えで事業をやっているのかというお話を聞けたのも大きな収穫でした。自社がサービスの提供を通じて価値を生んでいる領域の将来を予測して、予測した将来からどの段階でどのような事業が必要かを逆算して、その状況を実現するために現在のサービス開発や開発をするための最適な人員配置を考えていくお話はとてもワクワクしました。世の中のサービスにはこのような前提を踏まえて提供されているものがあるという発想は普段あまりしてこなかったので、新しい発見でした。GAFAの上層部は一体どこまで先の未来が見えているのでしょうか。

終わりに

とても充実した17日間でした。楽しかったです!
他にもいろんな事業会社回ってみたいなぁ。

*1:個人の感想です

*2:バッチサイズが小さいと1epochの中で過学習が起きていただけの可能性もあります

*3:今回たまたまそれで上手く行っただけかもしれませんが

KL divergenceの式の意味を理解する

確率分布同士の隔たりを表す式、KL divergenceの式

\begin{align} D_{KL}(P || Q) = - \int p(x) \log \left( \frac{p(x)}{q(x)} \right) dx \label{1} \\ \end{align}

の意味を理解します。

数式を用いつつ、かつ出来る限り噛み砕いて説明します。

情報量

まず、情報量という概念を導入します。これは、確率 p(x)で起こる事象 X xであると知るとき、それがどれだけ情報的に価値があるかを表す指標です。これを関数 h(p(x))とします。 h(p(x))

  1.  h(p(x)) \geq 0が成り立つ
  2. 確率 p(x)が小さい程大きな値をとる
  3. 足し算 h(p(x_1)p(x_2)) = h(p(x_1)) + h(p(x_2))が成り立つ

 という条件を満たすとします。条件2は、情報は希少性が高い程それを知ることによるメリットが大きい、と言う意味を表します。条件3は、独立な複数の事象が同時に起こる確率は個々の事象が起こる確率の掛け算になるのに対し、複数の事象が起きたと知る情報量の総和は、個々の事象が起きたと知る情報量の足し算になる、という意味を表します。

これらの条件を満たす演算子として - \logが挙げられます。

\begin{align} h(p(x)) = - \log(p(x)) \end{align}

とすると、 -log(p(x))は下図のような挙動をします。

f:id:hanon05:20190202160118j:plain

-log(x)の挙動

  - \log (p(x)) 0 \leq p(x) \leq 1の範囲で常に0より大きく、 p(x)が小さい程大きな値を取るので条件1, 2を満たします。また、

\begin{align} - \log(p(x_1)p(x_2)) = - (\log(p(x_1)) + \log(p(x_2))) = -\log(p(x_1)) - \log(p(x_2)) \end{align}

なので、条件3も満たします。よって、 - \log(p(x))は情報量を表す関数として使えます。

エントロピー

次に、事象 Xそのものの情報量を定義します。これは、事象 X xとなる場合の情報量を xとなる確率で重み付け平均すれば良いので、

\begin{align} - \int p(x) \log ( p(x) ) dx \label{2} \end{align}

となります。これをエントロピーと言います。エントロピーはざっくり言うと事象の不確実性を表しています。例えば、表が出る確率が0.5のコインは0.9のコインと比べて表が出るか裏が出るかの不確実性が高いです。表が出る確率が0.5のコインの方がエントロピーは大きくなります。

  確率分布の隔たりをどのように表現するか

複雑な確率分布 p(x)を、単純な確率分布 q(x)で近似する場合を考えます。どれくらい上手く近似出来ているかを測るには、確率分布 p(x) q(x)の隔たりがわかれば良いでしょう。つまり、 p(x) q(x)が全く同じ形であれば0、そうでなければだんだんと0より大きい値が返る関数 f(p(x) || q(x))を定義します。

式\eqref{2}の情報量を q(x)に置き換えた式、

\begin{align} - \int p(x) \log (q(x)) dx \end{align}

を準備します。これを交差エントロピーと言います。これと元のエントロピーの差分を計算すると

\begin{align}
\left(- \int p(x) \log (q(x)) dx \right) - \left(- \int p(x) \log (p(x)) dx \right)
\end{align}

\begin{align}
= - \int p(x) \{ \log q(x) - \log p(x) \} dx
\end{align}

\begin{align}
= - \int p(x) \log \left( \frac{q(x)}{p(x)} \right) dx
\label{3} \end{align}

 となります。これがKL divergence D_{KL}(P || Q)になります。この差分は、確率分布 p(x) q(x)に置き換えた場合の差分、つまり確率分布同士の情報量の差分によるものです。

KL divergenceの性質

式\eqref{3}を見ると、 p(x) = q(x)となる場合に0を取ることがわかります。また、 D_{KL}(P || Q)は0以上となる(※補足を参照)ので、これが最小の値となります。

結論

よって、KL divergenceは交差エントロピーエントロピーの差分によって導出され、確率分布同士が全く同じなら0、隔たりがあればあるほど大きな値をとる関数となります。確率分布 p(x) q(x)で近似する問題は、 D_{KL}(P || Q)を最小化する問題に置き換えることが出来ます。

 

補足:  D_{KL}(P || Q)が0以上である事の説明

 D_{KL}(P || Q)が0以上になる説明をします。

関数 f(x)が凸関数であるとき、イェンセンの不等式

\begin{align}
f \left(\int x p(x) dx\right) \leq \int f(x) p(x) dx 
\label{4}
\end{align}

が成り立ちます。

証明は他のサイトに譲るとして、Xが離散変数で、 X=(x_1, x_2)となる場合は下図のようなイメージになります。aと比べてbは常に同じか小さい、ということを示しています。

f:id:hanon05:20190202185057j:plain

イェンセンの不等式のイメージ

式\eqref{4}は一般化すると

\begin{align}
f \left( E[x] \right) \leq E[f(x)] 
\end{align}

なので、 x g(x)にしても成り立ちます。

\begin{align}
f \left( E[g(x)] \right) \leq E[f(g(x))] 
\end{align}

これを元の連続変数の形に戻します。

\begin{align}
f \left( \int g(x) p(x) dx \right) \leq \int f(g(x)) p(x) dx
\label{5}
\end{align}

さて、改めて式\eqref{1}を見てみます。

\begin{align}
D_{KL}(P || Q) = - \int p(x) \log \left( \frac{p(x)}{q(x)} \right) dx
\end{align}

 f(x) = - \log(x) g(x)=\frac{q(x)}{p(x)}とおくと、 - \log(x)は凸関数なので、式\eqref{5}が適用出来る事がわかります。
\begin{eqnarray}
D_{KL}(P || Q) &\geq & - \log \left( \int p(x) \frac{q(x)}{p(x)} dx \right) \\
 &=& - \log \left( \int q(x) dx \right) \\
&=& - \log (1) \\
&=& 0
\end{eqnarray}

よって、  D_{KL}(P || Q)は0以上である事がわかります。

参考

PRML上 P54~

エントロピーからKLダイバージェンスまでの話 - HELLO CYBERNETICS

 

2値分類で使われる損失関数、Sigmoid Cross Entropyは何を表しているのか

Deepなモデルは、最適なパラメータを学習するために損失関数を設定して、モデルの予測値と正解値の誤差を最小化します。

回帰系の問題設定でよく使われる二乗誤差などMSE系列は、回帰分析における最小二乗法とそっくりなので何をやりたいかはイメージがつくとして、分類問題におけるcross entropy系は関数を見ただけでは何をやりたいのかがわからないので、整理しました。

多値分類問題で使うsoftmax cross entropyは、あるクラスに属するか否かの分類問題を解いています。2値分類も同じくクラス0に属するのかクラス1に属するのかの分類問題を解くわけですが、クラス1に属する確率を推定するというように回帰問題としても捉えられるので、要するにモデルの最終的な出力を2変数ベクトル(2値分類)にするべきかスカラー([0,1]をとる回帰問題)にするべきかちょっとわからなくなりました。

結論から言うとどちらでやっても問題ないのですが、sigmoid cross entropyをベルヌーイ分布から導出して、確率論や最尤法的観点から損失関数を整理します。

 

0か1の値をとる、あるデータ x_iがパラメータ \thetaから発生するとします。 このような場合、確率分布はベルヌーイ分布を用います。

\begin{align}
p(x_i | \theta) = \theta ^{x_i} (1 - \theta)^{1 - x_i}
\end{align}

パラメータ \theta X=1となる確率を表し、例えば、歪みのないコインなら \theta = 1/2となり、式(1)に代入すれば、 x=1となる確率 p(X = 1 | 0.5) x=0となる確率 p(X = 0 | 0.5)も1/2となることがわかります。

 対数尤度は以下のように変形出来ます。

\begin{align}
l(\theta) = x_i \log \theta + (1 - x_i) \log (1 - \theta)
\end{align}

 最尤法ではデータxが手元にあった場合、対数尤度を最大化するパラメータ \thetaを求めますが、機械学習では観測値 y_iがあった場合の対数尤度を最大化するパラメータ \thetaを求めます。パラメータ \thetaとはこの場合予測値 f(x_i)なので、損失を最小化するようにマイナスをかけて式(2)を変形させ、データ全体の対数尤度を足し合わせると、最小化すべき損失関数は

\begin{align}
Loss(\mathbf{x}, \mathbf{y}) &= - \sum_{i} l(f(x_i), y_i) \\
&= - \sum_i \{y_i \log f(x_i) + (1 - y_i) \log (1 - f(x_i))\}
\end{align}

となり、これがSigmoid Cross Entropyとなります。 f(x_i) x_iをモデルに入力した時に出力される、モデルの予測値です。この誤差に学習率やら学習関数やらを通したものを逆伝搬させて、 f(x)の値を決めるパラメータ Wを更新していくわけです。

式(4)の f(x_i)はベルヌーイ分布のパラメータ \theta、つまり y=1となる確率に相当すると考えられるので、0と1の間にある事が望ましいです。

 

式(4)は、モデルの出力値 f(x)と観測値 yがベクトルであってもスカラー(値)であっても計算が可能です。ベクトルを用いる場合は各次元毎にlossを計算して平均をとれば、ある1つのデータに対する対数尤度(のようなもの)が計算出来ます。観測値 \mathbf{y}はどれか1つの要素が1でそれ以外の要素は0となるので、式(4)より観測値が1の要素の対数尤度のみが損失となり、観測値が0の要素は0が掛けられるので消えます。

従って、回帰としてスカラーで解いた場合でも、分類としてベクトルで解いた場合でも、観測値 yに対応した f(x)の対数尤度が損失として逆伝搬されます。

また、ベクトルが何次元であっても観測値 y=1となる次元は1つだけなので、逆伝搬する対数尤度も y=1に対応する次元の f(x)の対数尤度のみです。

ただ、ベクトル毎のlossを足し合わせる場合、それだけ値が大きくなるので、同じ学習率を使用した場合スカラーよりパラメータの更新量が大きくなる、、はずです。pytorchのBinary Cross Entropyの関数を見た所、size_averageという引数がベクトルの各要素のlossを足し合わせるのか平均をとるのかをコントロールしているようでした。

 

結論をまとめると、2値分類問題はモデルの出力をベクトル(2クラス分類)にしても、スカラー(クラス1に所属する確率を出力する回帰問題)にしても、やっていることは理論的に変わりません。