こんにちは、Diverse Developer Blog です。
今回は、YYC のマッチング検索に、SageMaker 上で学習した Two-Tower Model のユーザー Embedding を使ったベクトル検索 (Solr KNN) を追加した話をします。
チャンスタイムやデイリーミッションといった既存のルールベースの行動導線は引き続き動かしつつ、「自分と温度感が近い相手」を ML ベースの検索で並行して返せるようにする、という構成です。
TL;DR
- これまで YYC のマッチング機会は、チャンスタイム(新規登録直後)やデイリーミッション(継続期)といったルールベースの行動導線で設計していました
- ただ、ユーザーの目的が多様化するにつれて「全員一律の導線」では受け止めきれない部分が出てきたため、
温度感という軸を決定論的に定義し、それを教師情報として 64 次元のユーザー Embedding を学習する仕組みを追加しました - 学習は Amazon SageMaker 上の Two-Tower Model で行い、推論結果は Solr の Dense Vector フィールドに投入、KNN 検索で「温度感の近い相手」を返せるようにしています
- 既存のルールベース導線とは置き換えではなく併用で、検索結果の一部を ML 側から差し込む形で運用しています
1. これまでの設計
YYC には、ユーザーのライフサイクルに応じた行動導線がいくつかあります。
- チャンスタイム(新規登録直後): チュートリアル完了時に時間制限で、一定時間のあいだプロフィール閲覧やメッセージ送信が無料になる施策です。初動のアクションを後押しして、最初の 1 通までの距離を縮める役割を担っています。
- デイリーミッション(継続期): 一定の条件で抽出された相手に対してあいさつ(初回メッセージ)が無料で送れる仕組みで、日々の行動を後押しして継続利用につなげる役割を担っています。
- 検索ロジックのルールベースの並び替え :「新規ユーザー優先」「最終ログインが近い順」など、経験則ベースの並び替えです。
いずれも「誰にどの動線を踏ませるか」をルール側で先に決め打つ設計で、登録直後はチャンスタイム、その後はデイリーミッションという役割分担になっていました。
ルールベースの強み
先に書いておくと、ルールベースの設計自体に問題があるわけではありません。数年単位で YYC を支えてきた土台で、以下のような強みがあります。
- 説明可能性が高い: 「なぜこの動線が出たか」をコードと config で完全に説明できます。運営や CS からの問い合わせにもすぐ答えられます。
- 即効性がある: 新しい施策を思いついてから数日で本番に出せます。
- 数値設計が容易: 送信上限や有効時間などのパラメータを A/B で直接調整できます。
- 障害耐性が高い: 壊れても原因が特定しやすく、切り戻しも簡単です。
- 運用知見が蓄積しやすい: 長年の施策で「どの数字を動かすと何が起きるか」が体感値として溜まっています。
新規プロダクトや、シグナルが十分に溜まっていないフェーズでは、ルールベースは今でも最短ルートだと思います。
見えてきた課題
一方で、ユーザー層と目的の多様性が広がってくると、次のような課題が見えてきました。
- ユーザー多様性への追従が遅い: 新しい利用目的や層が出てくるたびに、ルールを増やすか既存ルールを改修するしかありません。
- 個別最適ができない: 全員一律のルールなので、同じ温度感を持つ人同士をピンポイントで繋ぐことができません。
- トラブルの事後学習ができない: ブロックや通報があっても、ルール側に自動で反映されるわけではなく、改善のたびに人間が仮説を立てて手を動かす必要があります。
- 初動と継続で導線が分断する: チャンスタイム(新規)とデイリーミッション(継続)の接続が、ロジックとしても体験としても連続しません。
- ルールの複雑化: これが一番重くなっていく課題です。
ルールの複雑化についてもう少し書くと、
- 例外が例外を呼ぶ: 「このユーザー群だけ除外」「この時間帯だけ特別扱い」「このアプリバージョン以降はこう」といった分岐が、一度入ると消せずに積もっていきます。
- ルール同士の相互作用が読めなくなる: 単体では正しいルールが、他のルールと組み合わさることで予期しない動作をします。
- 変更のリスクが非対称: 新しいルールの追加は安全に見える一方、既存ルールの削除や改修は副作用が読めず怖くなり、結果として削除されないコードが溜まり続けます。
ルールベースは初速こそ速いのですが、メンテコストは時間とともに増えていきます。ある時点からは、新しい施策を入れるより「既存ルールと衝突しないか確認する」ほうが時間を食うようになります。
ユーザーの目的が多様化するほど、全員一律の導線では受け止めきれない領域が広がってきたので、ここを ML 検索で補完することにしました。
2. 温度感を 1 次元スコアとして定義する
まず取り組んだのは、モデルに学習させる前に「温度感とは何か」を自分たちで定義することです。
ユーザーの「出会いに対する姿勢」は、実は 1 本の軸にかなり綺麗に乗ります。
じっくり ←─────────────────────────────→ カジュアル 0.0 1.0
- 左寄り: じっくり話したい、ゆっくり関係を築きたい層
- 中央: 一般的な出会い目的の層
- 右寄り: 気軽につながりたい、カジュアル志向の層
さらに、それぞれのユーザーには「自分のスコア位置」だけでなく「どの範囲の相手なら噛み合うか」という希望レンジもあります。ざっくり書くとこういうイメージです。
じっくり カジュアル
0.0 1.0
├──────────────────────────────────────────────────────┤
◆ ← ユーザー A
├───────┤ A の希望レンジ
◆ ← ユーザー B
├───────────────┤ B の希望レンジ
◆ ← ユーザー C
├──────────────────────────┤ C の希望レンジ
ポイントは、希望レンジがユーザー (カテゴリ) ごとに形が違うという点です。左寄りの層は同じ左寄りの相手を狭めに希望することが多く、中央・右寄りの層は逆に広めのレンジを持ちやすい、といった非対称な分布になります。実際にはこれをもっと細かい複数カテゴリに分けていますが、構造としては同じです。
この軸を決定論的なロジックとして実装しています。
スコア算出のしくみ
- フラグ優先判定: 悪質行為フラグや属性フラグなど、確実にカテゴリが決まる条件を最優先で判定します。
- 目的ベーススコア: フラグに該当しないユーザーは、プロフィールの「出会いの目的」から平均スコアを算出します。
- 希望レンジ: 自分のスコアに対して「どの範囲の相手なら噛み合うか」を希望の中心値として定義します。
この段階ではまだ機械学習は出てきません。プロフィール・属性・目的コードから、決定論的に score と match_center の 2 値を出しているだけです。
ただ、この 2 値が後段の学習モデルの教師情報になります。
3. Two-Tower Model で 64 次元に埋め込む
次に、各ユーザーを 64 次元のベクトル (Embedding) に変換する学習パイプラインを、Amazon SageMaker 上に構築しました。採用したのは、推薦系で定番の Two-Tower Model です。
[User Tower] [Target Tower]
│ │
▼ ▼
64 次元ベクトル ──コサイン類似度──▶ マッチングスコア
- User Tower と Target Tower は同じ構造の別重みです
- 入力は
user_id / sex / score / match_centerの 4 カラムのみ - 出力は 64 次元の密ベクトルです
なぜ学習させるのか
ここで「score と match_center が決定論的に出るなら、その 2 値でソートすればいいのでは?」と思われるかもしれません。
実は、Embedding の先頭 2 次元はスコアをそのままピン留めしているので、そこだけ見ればロジック単体でも再現できます。ML を使う価値は、むしろ残りの 62 次元のほうにあります。
この 62 次元は実際のユーザー行動 (特にブロック) から学習されるので、
- 温度感スコアが同じでも、「実際にはブロックされがちな特徴」を持つ人は遠ざけられます
- プロフィールに明示されていない共通点で近い人同士は、自然と近くなります
- 過去の行動パターンから、「この相手は希望に合いそうか」という期待度合いを織り込めます
静的な属性ベースのスコアリングに、行動データから学習した「振る舞いの近さ」を重ねる、ということです。先頭 2 次元で説明可能性を担保しつつ、残り 62 次元で汎化を稼ぐ、という役割分担になっています。
損失関数
学習の損失関数は 2 つの項を組み合わせています。
1 つ目は、推薦系で標準的に使われる InfoNCE です。バッチ内のペアを正例・負例としてコントラストさせ、良い組み合わせは近く、悪い組み合わせは遠くなるように学習させます。これに加えて、後述するブロックを明示的な負例として投入しています。
2 つ目は、Embedding の先頭 2 次元を score と match_center に誘導する MSE 項です。解釈可能なスコアがあるので、それを Embedding の先頭次元にそのまま載せてしまおう、という発想です。これによって、
- 「なぜ近いのか説明できない」マッチングを返しにくくなります
- 温度感レンジでのフィルタリングを、ベクトル検索側で低コストに再現できます
ブロックを負例として使う
ユーザー間の明示的なブロックは、マッチングでは強い負例シグナルです。学習パイプラインでは、直近 90 日のブロックを weight = -1.0 の負例インタラクションとして投入しています。
ただし、この負例が担っているのは「ブロック当人同士を離す」ことではなく、「ブロック相手に似た別人を遠ざける」ことです。
ブロック関係にある当人同士は、Solr の検索フィルタ側で確定的に除外しているので、設計としては二層になっています。
| 層 | 担当 | 効果 |
|---|---|---|
| Solr の検索フィルタ | 確定フィルタ | ブロック当人同士は 100% 出ない |
負例 -1.0 |
ML 学習 | ブロック相手に似た別人を Embedding 空間で遠ざける |
フィルタで当人は既に落ちているので、ML 側の負例は「似た傾向の他人がまた現れる」事故を確率的に減らす役割に専念できます。片方が外れても最低限の安全性は担保されます。
4. 推論結果の配信(Solr KNN)で配信する
学習した Embedding は Solr の Dense Vector フィールドに投入し、KNN 検索で「自分のベクトルに近いユーザーを k 件返す」形で配信しています。
既存の属性フィルタ (性別・年齢・エリア・ブロック除外など) は Graph Pre-Filter として併用できるので、ベクトル検索と条件検索のハイブリッドになります。
5. 何が変わるのか
今回追加した温度感ベクトル KNN 検索と、これまでのルールベース検索との違いは次のとおりです。
| 観点 | これまで (ルールベース) | 追加した ML 検索 (温度感ベクトル KNN) |
|---|---|---|
| 推薦の基準 | 新規ユーザー優先・最終ログイン順などの並び替え | ユーザー同士の温度感の近さ |
| 新規目的への対応 | ルール追加や改修 | プロフィール更新 → 次回学習で反映 |
| トラブル抑止 | 人手でルールを足す | ブロック負例でモデルが学習 |
| 説明可能性 | ルールを辿れば説明可 | 先頭 2 次元にスコアを固定して担保 |
| 検索性能 | 属性 INDEX 頼り | Solr KNN で高速ハイブリッド検索 |
6. 直近のネクスト: 数字を見ながら ML 側に寄せていく
ここまでで「決定論的スコア + Embedding ベースの類似検索」という土台はできましたが、現時点では既存のルールベース導線と ML 検索を併用しながら効果を計測している段階です。マッチング検索全体を ML 側に寄せるかどうかは、数字を見ながら判断していくことになります。
直近のネクストは、
- マッチ成立率、メッセージ継続率といったプロダクト側の KPI で ML 検索の効果を計測する
- 結果を見ながら、学習データ・特徴量・損失設計を改善していく
- 数字が安定して良くなったところで、ML 検索の比率を上げていき、最終的にはマッチング検索をルールベース側から ML 側に寄せていく
という流れを想定しています。ML モデルは学習データや特徴量を入れ替えれば継続的に改善できるので、しばらくはルールベースと併用しながら、数字を見て少しずつ振り方を ML に寄せていく構成で考えています。
その先の改善余地としては、以下のような項目も見えています。
- シグナル設計の余地: 現状はブロックを負例として使っているのみで、ポジティブな行動 (メッセージ継続、マッチング成立など) をどう反映させるか?
- コールドスタート: 行動ログがない新規ユーザーに向けてどう最適化するか?
- 再学習サイクル: 現状は日次バッチですが、更新頻度と計算コストのバランスをどう取るか?
まとめ
今回は、出会いの「温度感」を マッチング検索に乗せるために、Two-Tower Model と Solr KNN でやったことを書きました。
- 温度感を決定論的なスコアとして定義した上で、それを教師情報として Two-Tower Model を学習させ、Solr KNN で配信する構成にしました
- Embedding の先頭 2 次元にスコアをピン留めすることで、説明可能性を保ちつつ、残り 62 次元で行動データからの学習を効かせています
- ブロックは確定フィルタ (Solr) と ML 負例の二層で効かせる設計にしました
- ML 検索の効果はまだ計測中で、既存のルールベース導線と併用しながらチューニングを続けていく予定です