体系 › RAG・検索拡張
·無料·論文·13分で読めます

推薦システムと埋め込み — RAGと同じ数学、違う目的

「あなたへのおすすめ」の中身は、RAGのベクトル検索とほとんど同じ数学です。協調フィルタリングの行列分解から二塔モデル、ANN検索の使い回しまでを前提知識ゼロから解説し、同じ道具を使いながら評価も落とし穴も別物になる理由を整理します。

要点

  • 協調フィルタリングは、内容を理解せず他のユーザーとの行動の重なりだけで好みを推測する考え方です。
  • 推薦は巨大な穴あき表の空欄を予測する問題で、表は低ランクなので掛け算で近似でき、ユーザーとアイテムが同じ空間の点になります。
  • 素朴な行列分解にはコールドスタート、特徴量の入口がない、暗黙フィードバックには正例しかないという3つの穴があります。
  • 二塔モデルはIDの引き当てをニューラルネットの出力に替え、全アイテムのベクトルを事前計算してインデックスに詰められます。
  • 推薦はRAGと同じANN検索を使えますが、正解が時間で動き、自分のログで学び、多様性まで評価する点が違います。
対象textタスクretrieval

BPR: Bayesian Personalized Ranking from Implicit Feedback

一次資料 — この記事の根拠

この解説の公開 2026-08-22

BPR: Bayesian Personalized Ranking from Implicit FeedbackarXiv:1205.2618論文ページ·PDF
Neural Collaborative FilteringarXiv:1708.05031論文ページ·PDF

「あなたへのおすすめ」は誰が決めているのか

行きつけの古本屋を想像してください。店主が棚から一冊抜いて「これ、たぶん好きですよ」と差し出してくる。店主はその本を読んでいません。読んだのは中身ではなく、あなたと似た本棚を持つ他の客が、何を買っていったかです。

これが協調フィルタリングの発想そのものです。内容を一切理解しなくても、行動の重なりだけで好みは推測できる。「この商品を買った人はこんな商品も買っています」は、この考え方をそのまま日本語にしたものです。

対になるのが内容ベースで、こちらはあらすじやジャンルを実際に読んで似たものを探します。RAG(検索拡張生成)の文書検索は完全にこちら側です。現代の推薦は両方を同じ数式の中で混ぜ、出てくるのは埋め込み(Embedding)を1から理解するとまったく同じ意味を持った数値の並びです。RAGを作ったことがある人は、推薦システムの半分をすでに知っています。

直感: 巨大な穴あき表を埋める問題

行にユーザー、列にアイテムを並べた表を作ります。マスの中身は評価点でもクリックの有無でも構いません。実サービスなら数百万行×数百万列で、そしてほとんどが空欄です。1人が触るアイテムは全体のごく一部だからで、この空欄を予測することが推薦です。

なぜ予測できるのでしょうか。表がでたらめな数字の集まりなら埋めようがありません。埋められるのは、「SFが好きな人はSFを買う」といった傾向が数十個もあれば、大半のマスに説明がついてしまうからです。数学の言葉でいえば、この表は低ランク——縦長の行列と横長の行列の掛け算で近似できます。

r^ui=pu⋅qi=∑f=1dpuf qif\hat{r}_{ui} = \mathbf{p}_u \cdot \mathbf{q}_i = \sum_{f=1}^{d} p_{uf}\, q_{if}
(1)

r^ui\hat{r}_{ui} はユーザー uu がアイテム ii をどれくらい好みそうかの予測値、pu\mathbf{p}_u はユーザーの好みを表す dd 個の数の並び、qi\mathbf{q}_i はアイテムの性質を表す dd 個の数の並びです。式全体は、dd 本の隠れた軸それぞれについて「この人がどれだけ求めているか」と「この作品がどれだけ持っているか」を掛けて、全部足すと言っています。つまり r^ui\hat{r}_{ui} は、その人の好みリストと作品の性質リストを上から順に突き合わせ、合っていた分だけ点を足していった合計点だということです。

肝心なのは、この軸の意味を人間が決めないことです。「SF度」も誰も定義しません。穴埋め誤差が小さくなるよう学習を回すと勝手に現れる——特異値分解と低ランク近似と同じ話です。そして副産物として、ユーザーとアイテムが同じ dd 次元空間の点になります。人と物が同じ地図に載る。これが推薦における埋め込みです。

FIG 1rを上げると、少ない成分だけで元の行列がみるみる復元されていきます。評価行列も同じで、数十本の軸だけで大半が説明できるから空欄を埋められる。パラメータ数の欄も見てください——表の全マスを持つより、両側の埋め込みを持つほうがずっと小さい

素朴な行列分解が現実に負けるところ

この形はNetflix Prizeの時代に主役でしたが、そのままでは現代のサービスに載りません。穴が3つあります。

コールドスタート。 新規登録したばかりのユーザーは行がまるごと空で、pu\mathbf{p}_u を学習する材料がありません。昨日公開された新作も列が空です。ところが新作こそ一番出したい。

特徴量の入口がない。 この式が見ているのはIDだけです。年齢もデバイスも時間帯もアイテムのタイトル文も、式のどこにも入り込めません。

そもそも「☆5」がない。 星を几帳面に付ける利用者はごく少数で、実際に貯まるのはクリック・再生・購入といった暗黙フィードバックです。しかもこれは正例しかありません。押さなかったのが「嫌い」なのか「画面に出ていなかった」のかを、ログは区別してくれないのです。

この3つ目の扱い方が、推薦システムでいちばん事故が起きる場所です。

暗黙フィードバックと、負例のつくり方

正例しかないデータで好き嫌いは学習できません。負例は自分で作るしかなく、作り方が結果を左右します。定番はバッチ内負例で、同じミニバッチにいる他のユーザーの正例を、自分にとっての負例に流用します。追加の読み込みが要らず、アイテム数が多いときの標準手法です。

もう一つは、スコアの絶対値を当てにいくのをやめる方向です。BPR(Bayesian Personalized Ranking)は「見たものは、見なかったものより上」というペアの順序だけを学びます。

L=−∑(u,i,j)ln⁡σ ⁣(r^ui−r^uj)\mathcal{L} = -\sum_{(u,i,j)} \ln \sigma\!\left(\hat{r}_{ui} - \hat{r}_{uj}\right)

ii が正例、jj が負例、σ\sigma はシグモイド関数です。正例のスコアが負例より大きければ損失が小さくなると言っているだけで、何点かには一切こだわりません。つまりこの式は、「押されたものが、押されなかったものより一段でも上に来ていればそれでよい」とだけ要求しているということです。出力は結局「上から20件」なので、順序さえ合えばよいという割り切りです。

ただしバッチ内負例には副作用があります。人気アイテムほど負例に選ばれる回数が多く、繰り返し押し下げられて不当に沈む。これを打ち消すため、サンプリング確率の対数をスコアから引く補正(logQ補正)がよく併用されます。

二塔モデル: 行列分解に、特徴量の入口を付ける

3つの穴のうち最初の2つをまとめて塞ぐのが二塔モデル(two-tower model)です。ユーザー塔 f(u)f(u) はID・属性・直近の行動履歴から dd 次元ベクトルを出し、アイテム塔 g(i)g(i) はID・カテゴリ・タイトル文の埋め込みから同じ dd 次元ベクトルを出す。スコアは変わらず内積です。

s(u,i)=f(u)⋅g(i)τs(u, i) = \frac{f(u) \cdot g(i)}{\tau}
(2)

τ\tau は温度で、小さくするほどスコアの差が強調され、学習時の分布が尖ります。つまり s(u,i)s(u,i) は、人の側のベクトルと物の側のベクトルがどれだけ同じ方向を向いているかを、温度でコントラストを調整しながら1つの数値にしたもの、ということです。骨格は pu⋅qi\mathbf{p}_u \cdot \mathbf{q}_i と同じです。二塔モデルは、pu\mathbf{p}_u と qi\mathbf{q}_i を、ID表の引き当てからニューラルネットの出力に差し替えただけのもの。IDしか使えなかった場所に任意の特徴量が流し込め、新作でもタイトルとカテゴリから g(i)g(i) が計算できます。

では、なぜ塔を2本に分けるのか。1本にまとめたほうが表現力は上です。答えは運用にあります。塔が分かれていれば、アイテム側の計算がユーザーに依存しない。つまり全アイテムの g(i)g(i) を事前に計算してインデックスに詰めておける。リクエスト時は f(u)f(u) を1回計算して近傍を探すだけです。ユーザーとアイテムを一緒に入力するモデル(cross encoder)は候補1件ごとに推論が必要で、数百万件の候補生成には使えません。

この「速いが粗い検索器で絞り、遅いが賢いモデルで並べ直す」構図は、RAGの基礎と設計パターンのretriever→rerankerと同じです。名前が違うだけで、解いている制約が同じなのです。

ベクトル検索は、そのまま使い回せる

学習が終わると、手元に残るのは dd 次元ベクトルの山です。ここから先はRAGと同じ土管を通ります。全アイテムのベクトルをANN(近似最近傍)インデックスに載せ、ユーザーのベクトルで上位 kk 件を引く。FAISS・ScaNN・HNSWという道具立ても共通です。ただし、類似度の選び方の意味だけは変わります。

FIG 2qをドラッグするとtop-5の顔ぶれが変わります。「内積」に切り替えると、向きが多少ずれていても長いベクトルが割り込んでくる。推薦でこれが起きると「人気作ばかり出る」になり、コサインに変えると人気という情報ごと捨ててしまう。どちらが正しいかは目的次第です

RAGでは、ベクトルの長さは文書の書きぶり由来のノイズであることが多く、正規化してコサインを使うのが素直です。ところが推薦では長さが人気度という意味を持つことがあります。よく買われるアイテムは学習中に何度も引っ張られて長くなるからで、正規化するとその情報ごと捨てることになります。

コードで書く、二塔の学習とサービング

学習側は、バッチ内負例を使うと驚くほど短く書けます。

import numpy as np

def two_tower_scores(user_feats, item_feats, f, g, tau=0.05):
    U = f(user_feats)          # (B, d) ユーザー塔の出力
    I = g(item_feats)          # (B, d) 同じバッチに入っている正例アイテム
    return U @ I.T / tau       # (B, B) 対角が正例、非対角がバッチ内負例

def in_batch_loss(scores, log_q):
    scores = scores - log_q             # 人気アイテムの下駄を外す(logQ補正)
    labels = np.arange(len(scores))     # i行目の正解はi列目
    return cross_entropy(scores, labels)

U @ I.T の1行で B×BB \times B のスコア表ができ、対角成分だけが正しいペアです。あとは各行を「正解は自分の列」という多クラス分類として解けばよく、追加の負例を一切読まずに B−1B-1 個の負例が手に入ります。

サービング側は非対称です。g はバッチ処理で夜のうちに、f はリクエストのたびにオンラインで——同じモデルの2本の塔が、まったく違う場所で動くわけです。

index.add(g(all_items))              # 全アイテムを事前計算してインデックスへ
u = f(current_user)                  # リクエストごとに1回だけ
cands = index.search(u, k=500)       # 候補生成: 数百万 → 数百
cands = drop_unavailable(cands)      # 在庫切れ・非公開・既読を除外
ranked = ranker.score(current_user, cands)[:20]   # 精ランキング

同じ数学、違う目的

推薦とRAGは、埋め込み・内積・ANN・二段構えを共有しています。違うのは目的のほうです。

RAG 推薦
クエリ 文(質問) 人(履歴と文脈)
正解 原理的に定まる 主観的で、時間とともに動く
典型的な失敗 もっともらしい嘘をつく 飽きられる
評価 recall@k・nDCGと回答の正しさ CTR・視聴時間・多様性、最終判断はA/B

正解が動きます。 「この規程は第何条か」の答えは昨日も今日も同じですが、あなたが今日見たい映画は先週と違う。だから推薦は定期的な再学習が前提で、インデックスも作り直し続ける運用が要ります。

自分が作ったデータで学びます。 推薦した結果がログになり、それが次の学習データになる。ここでポジションバイアス——上に表示したから押された、という効果——を補正しないと、「昔たまたま上に出したもの」が永久に強くなり続けます。RAGの検索に、このループは基本的にありません。

多様性そのものが指標です。 RAGは関連度の高い文書ばかり返して構いませんが、推薦で同じ作者の本が20冊並んだら失敗です。似たものを意図的に間引く仕組みを、検索の後段に別途置くことになります。

現場ではこう使う

推薦・検索エンジニアがこれを触るのは、新規サービスに「関連アイテム」欄や「あなたへのおすすめ」欄を立ち上げるときです。定石は、ログがない初期はアイテム塔だけ(=内容ベース)で始め、行動ログが貯まってから協調フィルタリング側を足す順番。最初から二塔を組もうとすると、学習する材料がなくて詰まります。MLOpsエンジニアは再学習とインデックス再構築のサイクルを設計し、データエンジニアは学習時と推論時で特徴を一致させる仕組み(フィーチャーストア)を見ます。

手を入れるつまみはこのあたりです。埋め込み次元 dd は32〜256程度から始めるのが扱いやすく、上げるとメモリとレイテンシに直撃します。ANN側は、FAISSのIVFなら nlist と nprobe、HNSWなら M・efConstruction・efSearch。いずれも上げると再現率が上がって遅くなるトレードオフです。候補生成の kk は、大きくすると取りこぼしが減る代わりに後段のランカーの負荷が増えます。ライブラリは implicit(ALS)、LightFM、TensorFlow Recommenders あたりが入口です。

知らないと事故になる落とし穴を、起きやすい順に。

  1. 両塔は同じ空間にいる。 ユーザー塔だけ新しくしてアイテムインデックスを古いまま置くと、内積が意味を失います。片側だけのデプロイはできません。
  2. 正規化の不一致。 学習は内積・検索はコサイン、というズレです。定義が違うので順位が変わるのに、オフライン評価が同じ実装を通っていると気づけず、本番でだけ結果が変わります。
  3. ログの生存者バイアス。 画面に出なかったものはクリックされようがありません。「押されなかった=嫌い」として学習すると、露出のなかったアイテムが永久に埋もれます。
  4. オフラインとオンラインの乖離。 過去ログの recall@k が上がってもCTRが上がらないのは珍しくありません。最終判断はA/Bに預けます。
  5. 消したはずのものが出る。 非公開・在庫切れのアイテムがANNインデックスに残る事故です。フィルタは検索の後段で必ずかけ、落とす分を見込んで多めに引きます。

設計レビューで問われるのは、たいてい2つ。「なぜ二塔なのか」——アイテム側を事前計算してANNに載せるためで、精度が要る最終段だけcross encoderにする。「オフラインで勝ったのにA/Bで負けた、何を疑うか」——ポジションバイアス、未来の情報が漏れている特徴、評価期間のずれ。

まとめ

参考文献

  1. BPR: Bayesian Personalized Ranking from Implicit Feedback. arXiv:1205.2618論文ページ·PDF
  2. Neural Collaborative Filtering. arXiv:1708.05031論文ページ·PDF

本記事は上記論文の本文にもとづいて執筆しています。数値・主張は原典を優先してください。

運営: TechAthletes · 編集方針 · 出典の付け方 · 訂正の記録

コメント

コメントにはログインが必要です