協調フィルタリング入門:映画推薦の精度を高める記憶ベース・モデルベース手法の違いと実践例

Published on: | Last updated:

映画レコメンドの精度を今より上げたい人向けの、すぐ試せる具体的な一歩集。

  1. MovieLens 100kをダウンロードして、2日以内に最低10人分の評価データを見てみて。

    実際に手を動かすと協調フィルタリングの仕組みが感覚でつかめる。2日後に「知らない映画が推される率」を自分でチェックしてみて。

  2. PyTorchで埋め込み層+ドット積モデルを15分くらいで書き始めてみよう。

    実装すれば、どこがブラックボックスなのか肌でわかるし、15分で1回動けば自信がつく。(30分後にRMSEかMAEで予測精度を確かめよう)

  3. ユーザー・映画バイアスを足して、3回以上結果を比べてみて。

    バイアスがあると「変な推し方」が減る。3回以上試すと、スコア変化や順位変動が見えるはず。(1時間後にトップ5推薦が前と違うか見てみる)

  4. 行列が疎すぎると感じたら、隠れ要因の数を2〜8の間で増減してみて。

    隠れ要因が増えると似てるユーザーや映画が細かく分かれる。2と8で結果を見比べると「ベストな分割数」がなんとなく見える。(5分後にRMSEの違いを確認)

映画レコメンドの課題をどう解決する?協調フィルタリング基本ガイド

みんなさ、絶対一回は経験してるでしょ?「え、映画とかドラマ観終わったあと…次何観よう?」ってなるやつ!ほんとに、選択肢ありすぎて迷子案件!!いや、私もよくそこで悩んで止まるの。でもさー、こういう場面こそレコメンダーシステムが登場する理由じゃんね!

でさ、NetflixとかAmazonも超有名だけど、めちゃくちゃ数ある映画・ドラマの中から、自分の好みに合うやつをちゃんと探しやすくしてくれる…それがレコメンデーションシステムなのよ!なんか技術的には色々方法あってさ、「コンテンツベース」アプローチだと過去に好きだった作品に似たタイプをおすすめしてくれる感じ。「『ブレードランナー』好きな人には『Dune/デューン』も良いんじゃない?」みたいな流れね〜。意外とこれ精度高かったり。

……まぁでも!今回ちょっと語りたい(というか熱弁したい)のは、「協調フィルタリング」っていう手法!正直ここがめっちゃ面白ポイント!この仕組みのヤバい所って、自分が過去どんな作品を観てたか知らなくても、他のユーザーの評価データだけで機能しちゃう点。ね?変わってるよね!?これからfastaiライブラリとか実際に動かすコード、その基礎からイマドキなディープラーニングモデルまでガッツリ解説するっぽいので要注目!

そもそも協調フィルタリングって何ぞや?端的に言うと「もしAさんとBさんが同じモノへの評価近ければ、新しいアイテムでもAさんはBさん寄りになりやすい」理論らしい。簡単そうだけど奥深い気がする。これ結構大事な発想なので記憶の隅っこ置いといて!

例えばさ、自分とほぼ感性そっくりな誰か別ユーザーがいたとして、その人と自分が未視聴タイトルをどう評価するか、この手法だと「たぶん好みに近づく確率高め」って扱われる仕組みなんだよ〜。「協調」=つまり複数ユーザー全体のみんなの傾向まるごと活用しちゃう発想。それこそ、この方式ならでは最大の武器だと思う。

ジャンルとか監督情報とか別になくても平気。「あ、この人私と好み似てる!」となったら、「じゃあその人オススメまだ知らない作品にも共感できそう」…そんな感じ。なんか単純明快だけど案外奥行き深い話だと思わない?笑

直感的な例で学ぶ協調フィルタリングの仕組みとは

1. まあ、ざっくり例出すと、Aliceいるじゃん。あの人、映画『Inception』『The Matrix』『Interstellar』全部好きみたい。てかBobも一緒で、全く同じラインナップ好きなんだわ。なんか偶然だよね。けどCharlieはそこ違うっぽいな。ロマンス系しか観ないらしい。例えば、『The Notebook』とか『Titanic』とか『Pride & Prejudice』だし、とりあえずそんな傾向。

2. で、新作SF映画として『Arrival』公開された場合さ、Aliceは即鑑賞→星5付けたっていう話。うーん、大事かもね。それからBobが「なんかオススメ?」って例のシステムに聞いた時、その仕組みは「あれ、この2人の嗜好ほぼ同じや」って察知してさ、自信たっぷりにBobへ『Arrival』を勧めることになる。逆パターンでCharlieには、多分積極的には推さない気がする、どうかな、この辺わりと単純。

3. 実装方法だけど主に2つに分かれる印象。一つ目:メモリーベース協調フィルタリング(また近傍ベースとも言われる)。正味、考え方超単純。「この映画についてBobがどんな評価しそうか?」→「K人くらい似たようなユーザー」=“ご近所さん”を探してきて、その人達がその映画へ付けた点数とかを回収。その結果をまとめて(重み付き平均だったり?)、Bob用の予測値出す流れだな。

4. 説明する側としては超直感的。ただデカい問題もそこそこあるっぽい…。たとえば利用者数が鬼ほど多いと、ご近所ユーザー検索だけで遅延やばいんよね。それから、データがスカスカの場合「似た奴誰もこの作品観てないじゃん?」みたいな落とし穴も発生しちゃう。この2つ、多分けっこう大きな課題。まあ、ざっとこんな感じでいいかな。

直感的な例で学ぶ協調フィルタリングの仕組みとは

記憶ベースとモデルベース推奨法の違いを理解しよう

モデルベースのフィルタリング!いや、これは超注目!!!今回絶対知っておきたいアプローチだよね。え、モデルベース?どゆこと?ざっくり言うと、直接ユーザー同士を比べるんじゃなくて…あっ、全部のデータセットを丸ごと使って機械学習モデルをがっつり訓練しちゃうイメージなんだな。まあ個人の好みとか行動パターンをモデルで掴んじゃおう!て感じ。訓練さえ終われば予測マジ速いし、ユーザーと映画ペアだけボンっと渡すだけで答え返ってくるからラク。

そうそう、有名なやつある。「行列分解」だね。英語だとMatrix Factorization!いろんなレコメンドシステムで定番中の定番っぽい。

次!!データ!MovieLens 100k!この名前もう覚えておいてほしいレベル。有名なデータセットで、評価は合計100,000件(1~5点)、ユーザー943人×映画1682本集まってる。それぞれuser_id・movie_id・rating・timestampの4カラムしかなくて、まじ表形式で分かりやす!

ツールも書いておくか?今回はPyTorch使うよ。そしてfastaiライブラリも組み合わせる予定~。PyTorch自体めちゃ有名なディープラーニングフレームワーク、その上にできたfastaiはラップしてさらに使いやすいイメージかなぁ。効率重視だけどカスタマイズしたい欲張りにも対応できるんだわ。

最初やること?そりゃもちろんデータ読み込みからだよね笑。pandas DataFrameにロードしてサクッと準備スタート!!

MovieLens 100kデータセットで実践できる準備手順を知る

えっと、このデータさ、ユーザー名・映画タイトル・評価のセットがずーっと並んでるだけなのね。別に凝った感じはないよ、本当に単純。でも、そのまんまだとAIモデルはちゃんと理解してくれないっぽい。だからこそ、まずしっかりした形(構造)にしてやらなきゃいけない。バッチで区切って、学習用と評価用 - つまりトレーニングセット/バリデーションセット - にきっちり分ける必要があるんだよね。

でさ、fastaiのCollabDataLoaders使うと、その辺り全部まとめて自動的に処理してくれるからめっちゃ便利なんだわ。「import」とか書いたあとでDataFrameをdls作成用に渡すだけ。from_dfを呼ぶと勝手にuser列・movie列を見つけて、それぞれのIDを0, 1, 2…みたいな連番整数として認識して変換する感じ!ついでにbs=64ってオプションも指定できて、一度に64個まとめて評価データ扱う設定もサクッとできる!

ああ、こんな風景イメージすると分かりやすいかも?すごく巨大な表があるとして……横軸は全映画、縦軸には全ユーザー並んでる。セルごとに「このユーザーがその映画につけた点数」みたいな感じ。それで例えばMovieLensの場合だけど、行943本×列1682本のサイズだって話。でも普通ほぼ全部の人がそんな何百何千も映画見てないし採点もしないから、多分空欄ばっかになるじゃん?それって「スパースマトリックス」って言われる状態なんだよ。

そして結局やりたいことって、その空いているマス(つまりまだ誰にも評価されていないユーザー×映画の組み合わせ)のところを埋める……この未記入エリアへ未来的に予想した点数を書き込むことなの!

さて注目ポイントなんだけど、「潜在因子」ってキーワード聞いたことあるかな?知らなくても大丈夫w。「まだ見ていない作品」があった時、この人ならどう感じそうかな〜という予測をしたくなるじゃん。でも機械側から見ると、“好き/嫌いや性格など色々な隠れパターン”を「共通軸=latent factor」で数値的特徴量として捉えて、人間には見えづらかった関係まで可視化するアプローチだったりするらしい。直感では分からない微妙な関連性までモデルが拾って説明してくれてたりする……多分そんな仕組み!

MovieLens 100kデータセットで実践できる準備手順を知る

疎なユーザーアイテム行列から隠れ要因を抽出する方法

潜在因子ってやつ、あー…なんか言葉がだるいけどまあ要するに、モデル側が勝手に裏で抽象的な特徴とかを学んでくれてるって感じだよね。説明とか全部細かくしなくてもたぶん伝わると思う。映画だったら、「爆発ばっかのアクション」から「じっくり系インディードラマ」みたいな分布になる場合もあるし、あと「お笑い寄り」とか「真面目系」とか、その軸がまた違った因子として出てきたりしてさ…。ま、とりあえずユーザー側にも似たような好みポイントがそれぞれある、そんなイメージだと思うわ。

例えばだけど、「じゃあ5次元でやるぞ」って設定するとするじゃん。そうなると、『ダイ・ハード』みたいなのは `[0.9, 0.8, -0.9, 0.1, -0.2]` とか割り当てられるパターン多そう。「アクションすげえ強めで80年代要素バリ高、ラブ要素はかなり低い」、まぁその程度しか自分も理解してない(笑)。ユーザー例も書いておくと、「俺80年代アクション超好き!」なタイプなら `[0.85, 0.95, -0.7, 0.2, -0.1]` に近くなるし、それとは正反対のロマンチックコメディ勢なら `[-0.8, -0.7, 0.9, 0.1, 0.9]` 、こういう逆ベクトルも普通にいる気がするね。

結局さ、大事なのはそこなのよ。そのユーザーベクトルと作品側ベクトル、この2つがぐっと近づいてれば近いほど…本人的にズドンって好みに当たる確率が上がる。つまり「あ、これはもう最高!」「完全自分向けですわ~」って現象になり得るということ。

…次いこ? で、「分解&ドット積による予測」の説明ね。まああれ、基本“デカくてスッカスカ”なユーザー×アイテム表を、小さめだけど“隙間少ない密集”行列ペアにうまく切り直しちゃうの。でもそんな仰々しい理論より「無茶苦茶データ量多いやつでも計算ガッとラクになる技術」くらい思えば十分じゃね、実際扱える量グッと増えるから…。

PyTorchで埋め込み層とドット積モデルを構築してみよう

ユーザー×因子のUser-Factor Matrix、映画×因子のMovie-Factor Matrix、これがメインなんだよなー。まあ要はこの2つ揃えばOKっぽい。あのさ、ある特定ユーザーがどの映画どう評価しそうか知りたい時、その人のベクトルと映画側ベクトルを抜き出してさ、ドット積計算すれば答え出るって話なんだけど…。まあ正直めっちゃ単純でびっくりした(笑)

あれね、なんでそれで動くかって言うとさ、ドット積って2つのベクトル向き似てたら値デカくなるじゃん?つまりその人の趣味と映画側特徴が“合ってる”ならスコア上がる理屈みたい。それだけ。でも地味にめっちゃ使える気がするわ。

…次、第4部だよ。一旦ここから本気モード入るんだけど、自分でモデル書く番。眠いけどやっとく。fastaiノートブックもあるけど、とりあえず最初は純PyTorchバージョン見て「中身これか~」って思った記憶あるw。「具体的な実装方法?」についてだけどさ、PyTorchだったらEmbedding層使うことでUser-Factor MatrixもMovie-Factor Matrixもそのまま作れるらしい。

Embedding?あー…まあ大きいlookup table的なやつかな。「(n_users, n_factors)」みたいな形でベクトル突っ込んでおいて、例えばuserID5とかだったら5番目引くだけ。そのまま専用ベクトル貰えるシステムよ。

コード貼っとくね↓  
from fastai.torch_basics import *
class DotProduct(Module):
def __init__(self, n_users, n_movies, n_factors):
self.user_factors = Embedding(n_users, n_factors)
self.movie_factors = Embedding(n_movies, n_factors)
def forward(self, x):
users = self.user_factors(x[:,0])
movies = self.movie_factors(x[:,1])


# ドット積やるところ→usersとmovies成分ごと掛け算→sum(dim=1)して完了

return (users * movies).sum(dim=1)

このクラス、一応user側・movie側それぞれEmbeddingレイヤー用意してて、「forward関数」(バッチ流すタイミング)になったら全user/movie IDぶんそれぞれ該当ベクトルを持ってきて、それでdot product計算返して終わり!シンプルすぎて拍子抜けするレベルだけど、中ではちゃんと仕組み通りに動いてる…と思う、多分。

PyTorchで埋め込み層とドット積モデルを構築してみよう

損失関数・最適化アルゴリズムによるモデル精度向上に取り組む

最初ね、embedding行列の中身って全部ランダムな数字で埋まってるだけなんだよ!意味分からない予測しか出せないし、「マジでこれ合ってんの?」みたいになる(笑)。そりゃ困るから、モデル自身にちゃんとデータから賢くパラメータを覚えさせていかないと…ほんとスタートは迷走。

損失関数ってのは要するに「どれぐらい間違えたか」を測るものね。今回は回帰問題──つまり1~5の数字を当てる感じだから、平均二乗誤差(Mean Squared Error/MSE)がおすすめだよ。答えと予想との差、それぞれ2乗して和を取って、その平均。外せば外すほどバツが重くなるシステムなんだわ!

あとさ、オプティマイザって何かというと、embedding行列内の数値たち…それ勝手に自動調整していい感じに更新してくれる魔法アルゴリズムだよね!有名なのはStochastic Gradient Descent(SGD)。最近じゃAdamとかも使われてるっぽい。「おっと危険、この方向進んじゃダメ!」な道避けつつチクチク学習、そんなイメージ!で、それ全部fastaiではLearner一個用意するだけでまとめて管理OK!

実際の流れ例!最初DataLoaders作ったあと、dls.classes['user']や['movie']見るとユーザーとか映画のユニークIDが何種類あるかわかるんよ。n_factors=5なら埋め込み次元5でセット。「DotProduct(n_users, n_movies, n_factors)」これで構築。その後、「Learner(dls, model, loss_func=MSELossFlat())」こうやって組み上げ、「learn.fit_one_cycle(5, 5e-3)」叩くだけ。本当に今どき風なアプローチっぽいし、一瞬でサイクル型学習も済むし、学習率も緩急変えてサクッとなじませてくれて便利!!

ここまで来ればまぁ基本形完成…でも正直それだけじゃ面白くない?うーん理由は単純、“ドット積だけ”だと評価スコア全体を説明しきれない場面多い!だって「ユーザー側が本来持つ偏り」とか、「作品自体みんなから評価されやすい性格」、そういう素性情報全然加味できなくなるじゃん。

想像してみ?中には何見ても厳し目な点付け続ける人いるし、逆に人気映画だったら誰が観ても星高評価になることある。単純な内積オンリー路線だと、その辺反映難しいわ…ほんっと苦戦するよココ。

そこで救世主登場――バイアス導入必須!!ひとりずつのユーザーにも1本ずつの映画にも「バイアス項」を追加投入したほうが遥かに強い!難しく考えることなし、それぞれ一個普通に“ズレ”を加算。“ユーザーバイアス”=この人の点数が平均よりどっち寄り?そんな空気感と思えばいいんじゃないかな〜!!

ユーザー・映画バイアス導入で推薦精度はどこまで高まるか検証する

ポジティブバイアスって何?ざっくり言えば、ユーザーが気前よく高得点つけちゃう傾向、みたいな意味だな。で、逆のネガティブバイアスだと、どうも評価が厳しい人になってる。たぶんそういう雰囲気。

映画バイアスというのは……映画自体に元からある「質」や「人気」を数値化したものらしいね。高評価・人気タイトルはここでプラス側に大きいバイアスが付与される。その影響で、新しく予測式を作る場合にもこの値が加わる形になる。書式としてはprediction = dot_product(user, movie) + user_bias + movie_bias みたいな感じ。

PyTorchでモデル組む場合だけど、一例としてDotProductBiasクラスを書くことができる。それぞれのユーザーや映画ごとにEmbeddingレイヤーで専用のバイアス値を1個ずつ保持させている構造になってて、n_usersとかn_moviesとか指定して作成できる。あとy_range=(0, 5.5)という形で予測範囲を設定する仕様も可能っぽい。

コード一部抜粋:

class DotProductBias(Module):
def __init__(self, n_users, n_movies, n_factors, y_range=(0, 5.5)):
self.user_factors = Embedding(n_users, n_factors)
self.user_bias = Embedding(n_users, 1)
self.movie_factors = Embedding(n_movies, n_factors)
self.movie_bias = Embedding(n_movies, 1)
self.y_range = y_range


def forward(self, x):
users = self.user_factors(x[:,0])
movies = self.movie_factors(x[:,1])


# 内積計算
res = (users * movies).sum(dim=1, keepdim=True)

# バイアス加算
res += self.user_bias(x[:,0]) + self.movie_bias(x[:,1])

# 出力レンジ強制
return sigmoid_range(res, *self.y_range)

これ最後のsigmoid_range関数、大事。出力値がありえない範囲(例えば10点超えたりマイナスだったり)に絶対ならないようカットしてくれる仕組みね。このおかげで実際の映画評価として現実的な数値になる…ということかな。

まあここまで自作するの楽しいところある。ただ、「本番運用いける?」となったらちょっと考えるかも。その時便利なのがfastaiパッケージ。「collab_learner」を使えば、このDotProductBias形式も最初から備えてあるんだよね。API経由ですぐ使えるし、変な下準備とか省略可。

collab_learner導入例:
learn = collab_learner(dls, n_factors=50, y_range=(0, 5.5))
learn.fit_one_cycle(5, 5e-3, wd=0.1)

これホント一行追加レベル。一連のデータロード~Learner作成まで流れ込めてしまうし、このサンプルでは潜在因子n_factors=50採用。そしてwd=0.1って書くことで重み減衰(L2正則化)が効いて過学習防止効果も期待できそう。

もうひとつ面白ポイント有り。この方式だとパラメータ中身そのまま解析できたりするんだよね。つまりユーザー/映画別ベクトルやバイアス情報全部見られる仕様。それぞれ学習後には「どう特徴付いたか?」細かく観察したり比較したりもOK。

特にmovie bias見ることで、「そもそもその作品自体、大衆受けいい?悪い?」みたいなの分かったり。「最高点/最低点」付きリスト抽出など分析もしやすい仕掛けと言えると思う。このプロセスによって意外な発見にも繋がる場面、おそらく出てきそうだよ。

ユーザー・映画バイアス導入で推薦精度はどこまで高まるか検証する

fastai API活用で生産性重視のレコメンダシステムに切り替えよう

映画バイアスの結果、最低バイアスの作品…正直、地味。全然有名じゃないやつ多いし、「誰これ?」レベル多し。まあMovieLensのデータ回すと特に、続編失敗とか微妙評価ばっかり。ああ、マニア向けなんだなって感じ。逆に高バイアストップ見ると…うん、想像どおり。『The Shawshank Redemption』、『Schindler's List』、『The Godfather』みたいな神扱いタイトルが普通に上位固めてて、「やっぱ皆そこ行くよな」みたいなね。

えー、一応整理して言うけど…潜在変数を直感的に理解?難しいっす。抽象的すぎるから。ただPCA(主成分分析)を使えば大丈夫。要するにさ、本来50次元くらいある埋め込みベクトルを2~3次元までギュッと縮めることできるんだ。それで散布図書くとどうなる?一角にはド派手ハリウッド系が密集して、その反対側にはラブコメが固まったりする。ふーん、AIも意外とジャンル分かってるらしいね…とか思った。

まあ本論戻すと、このマトリクス分解は強い。でも基本は線形処理なのね。「全部ドット積(内積)」で計算されてるから、人間の複雑な好みは全部拾えない気がする。そのせいでクセ強ユーザーへの適応は弱いっぽい?

こういう時ディープラーニング便利だよーって話になる。モデル構造そのものに表現力持たせやすいしね。例えばドット積パートをごく普通の小型ニューラルネット(NN)へ置き換えるやり方増えてきたっぽい。まずユーザー埋め込み取って、映画埋め込みも抜いて、それ合体させて1本ベクトル作成。そのまま全結合層+ReLUみたいな非線形関数通して出力。それだけでも従来より柔軟性グンと上げられる……眠……まあそういう仕組みですわ。

ニューラルネット応用型レコメンドで高度な嗜好予測が可能になる理由

一番最後の層、うーん…まあ結局ひとつだけ数字出す。それが予測スコアになるって感じね。ディープラーニング使うと、ユーザーとか映画の隠れた特徴?そういうのをけっこう細かい絡み方までモデルが頑張って拾ってくれるっぽいんだよな。

いやもう眠いけどさ、一応fastaiでモデルをこっち(複雑な方)に切り替える方法もめちゃシンプルなんだわ…。collab_learner呼ぶときに引数ちょこっと付け足せば済むんよ。「learn = collab_learner(dls, use_nn=True, y_range=(0, 5.5), layers=[100, 50])」←はいこれ。そんで「learn.fit_one_cycle(5, 5e-3, wd=0.1)」ってそのまま回せばOKだしね。まあ、このくらいなら全然難しくない。

use_nn=Trueってしとくと、ドット積で終わりのクラシックタイプじゃなくニューラルネット版が勝手に組まれるようになっとる。それにlayers=[100, 50]つければ、最初の結合部分を100ユニットにしてからReLU挟んで50ユニットへ落とす、とか好きに設計できるみたいよ。このやり方、精度も意外と良くなったりするから地味にありがたい…。

思えば「同じものが好きな人はまた似たやつも好き説あるよね」っていうざっくりした発想がさ、今やここまでディープなモデルで映画の評価スコアかなりちゃんと当てられる時代になったんだからすごいわ〜。

えっと、途中ではユーザー×アイテム行列とか潜在因子とかそのへんも触れたし、ドット積ベースの行列分解?それも協調フィルタリング方式の推しポイント一通り自力でまとめた(たぶん)。しかも最初はPyTorch直書きでロジック把握してから、高レベルライブラリ(fastai)入れたら割と秒で構築~訓練~解釈まで回るようになったし!なにも考えずほぼ連打だけなのは楽すぎるw

協調フィルタリング自体はまあ業界じゃ昔から当たり前なんだけど、「埋め込み」「潜在因子」「会話ログから学習」とか、その根本部分って結局他ジャンルMLにもめっちゃ使いまわせるからありがたいヤツなんよなぁ。

そうそう、なんか配信系アプリで「ちょうど見たかった系オススメ出てきた!」みたいなの遭遇した時、それたぶん裏側こういうアルゴリズム&ディープ系の仕掛けが地味に動いてる結果なんじゃない?ほんま面白…ていうか眠いw

最後まで読んだ人いる?多分いない気するけど、お疲れさまです😂もしこの記事まぁまぁだったなら「👏クラップ」とか押しといてね!友達へのシェアや軽めコメントでもぜんぜん喜びます。フィードバック来るだけで適当にモチ上がったりする(笑)。AI事例あれこれラフに探して遊ぼ〜〜!

そういやLinkedIn(Gaurav Shrivastav)もゆるく更新中~気が向いたらフォローでもどうぞ〜。

Related to this topic:

Comments