4.1〜4.3 推薦システムとしての LLM

はじめに: 「LLM が推薦する」とは何を置き換えることか

この節の中心的な主張は、LLM を従来の推薦器の補助として使うだけでなく、ユーザー状態と候補作品をテキストとして受け取り、最終的な推薦リストを直接出力するモデルとして使える、というものである。ただし、「エンドツーエンド」という言葉は、候補生成、配信可否判定、業務ルール、ログ収集まで一つの LLM に任せるという意味ではない。

本番の Hulu を想定すると、実用的な形は次のようなハイブリッド構成である。

視聴・表示・タップ履歴、プロフィール、現在の文脈
                    ↓
          ユーザー状態の構造化・要約
                    ↓
  Two-Tower、i2i、人気、新着などによる候補生成
                    ↓
        配信可能な候補とメタデータを取得
                    ↓
       LLM によるランキング・説明生成
                    ↓
   決定論的な配信制約チェック・重複排除
                    ↓
                Top-K を表示
                    ↓
    impression、タップ、再生、視聴時間を評価

この構成では、LLM は主に「履歴と言語的・意味的な情報を統合し、候補間の微妙な適合度を判断する」部分を担う。一方、現在配信されている全作品を正確に知ること、存在する series_id だけを返すこと、配信終了作品を除くことなどは、検索システムと決定論的なルールが担う。

したがって、本章を理解するうえで最も重要なのは、次の区別である。

使い方 LLM の役割 典型例
LLM as enhancer 既存推薦器の一部を強化する 作品説明生成、タグ付け、クエリ書き換え、説明生成
LLM as recommender ユーザー状態と候補から順位または推薦を直接出す 候補 50 作品を LLM が並べ替えて Top-10 を返す
LLM-only generation 候補集合を与えず作品名を自由生成する 対話中の一般的な映画案内。ただし本番配信には危険である

最後の方法はデモでは魅力的であるが、Hulu の在庫を知らずに未配信作品や架空作品を生成しうる。従って、オンライン推薦では「自由生成」より「有効な候補集合から選択」を基本とするべきである。

難所の補足: エンドツーエンドは「周辺システムが不要」という意味ではない

エンドツーエンドという語は、入力から出力まで一つのニューラルモデルで処理するという意味で使われることが多い。そのため、「作品検索、ランキング、配信判定をすべて LLM が行う」と誤解しやすい。しかし、推薦でいうエンドツーエンド性には段階がある。

例えば、従来の推薦器が「ユーザー特徴量を作るモデル」「候補を採点するモデル」「説明文を作るテンプレート」に分かれていたとする。LLM がユーザー履歴と候補 metadata を読み、ランキングと説明を同時に返すなら、推薦判断の部分はかなりエンドツーエンドである。一方、候補を BigQuery や検索 index から取得する処理、現在配信中かを判定する処理、キッズ制約を適用する処理まで確率的な生成にする必要はない。

飛行機の自動操縦に例えると、LLM は複数の情報を統合して進路を決める操縦装置に近い。しかし、燃料計の値や滑走路が使用可能かという事実まで、操縦装置に「記憶から推測」させてはならない。Hulu でも、ユーザーに合う作品の判断は LLM に任せられるが、配信可否という事実はカタログを参照するべきである。

従って、どこからどこまでを一つのモデルに任せたかを明示する方が、「エンドツーエンドか否か」という二択より有益である。本章では、LLM が候補作品の選択・ランキング・説明を担う構成を、実用上の LLM 推薦器として考える。

1. 図 4.1 のワークフローを読み解く

図4.1 LLMをレコメンダーとして使用するシステムのワークフロー

図 4.1 は、入力ソース、推薦ワークフロー、評価という三層に分かれている。

  • 入力ソースは、ユーザーデータ、作品データ、時刻・場所・デバイスなどの文脈である
  • 推薦ワークフローは、前処理、プロンプト生成、LLM 推論、後処理である
  • 出力後には、関連性、多様性などを評価し、データ・プロンプト・モデルへ改善を戻す

図は直線的に描かれているが、実運用では閉ループである。表示された作品、タップされた作品、再生された作品、どの程度視聴されたかが次回の入力と学習ラベルになる。また、推薦結果はモデルだけで決まらず、候補生成で正解作品を残せたか、入力要約で嗜好を失わなかったか、後処理で何を除いたかにも依存する。

1.1 ステップ 1: 入力データの準備

Hulu で利用できる主要な信号

Hulu のデータカタログに沿うと、入力は概ね次のように対応する。

情報 主なテーブル・カラム 推薦での意味
強めの視聴シグナル hj-data-01.report.unique_viewed_seriesprofile_id, series_id, last_viewing_date 動画長の 25% 以上を視聴したシリーズと最終視聴日
詳細な視聴 hj-data-01.report.content_viewingwatched_seconds, start_date, series_id, asset_id, device_high_category 視聴強度、順序、時間帯、端末、シリーズと話数の区別
作品詳細ページ訪問 hj-data-01.raw_data.import_raw_data_tracking 興味は示したが視聴には至らなかった弱いシグナル
表示・タップ文脈 hj-data-01.intermediate.ga4_unnested_cleaned_logevent_params_series_id, event_params_tray_order, event_params_tap_title_position など 何をどの位置で見せたか、何を選んだかを区別する材料
作品情報 item_information_tableseries_id, genre, sub_genre, description, sockets_tag, casts, film_directors など LLM が意味を理解するための根拠
配信・安全制約 publish_start_at, publish_end_at, service_type, rating_name, kids_mature_flg, is_blacklist 推薦してよい候補を決めるハード制約
映像由来特徴 avg_mood_tag, avg_fingerprint テキストだけでは捉えにくい雰囲気や映像内容
既存推薦候補 recommend_engine.two_tower_u2i_recommendation_{genre} など LLM に渡す候補集合の生成

ここで、profile_idaccount_id を混同してはならない。アカウントには複数のプロフィールがありうるため、個人嗜好は原則として profile_id 単位で構築する。家族全体の契約状態などは account_id 単位であるが、視聴嗜好までアカウント単位に集約すると、親と子どもの視聴が混ざる可能性がある。

難所の補足: LLM に自然言語を渡せても、構造化データは不要にならない

LLM は「最近は SF をよく見ている」のような文章を理解できる。この性質から、すべてのログを文章へ変換すれば、特徴量設計や schema が不要になるように見える。しかし、実際には自然言語化する前に、どのログを選び、どの期間で集計し、何を強い行動とみなすかを決めなければならない。

例えば、視聴秒数が 1,410 秒という情報だけでは長時間視聴か判断できない。25 分作品ならほぼ完走だが、3 時間映画なら約 13% である。そのため、作品長と結合して視聴割合を求める必要がある。また、「昨日」と書くには基準時刻とタイムゾーンが必要であり、「最近」と書くには何日以内かを決める必要がある。

つまり、自然言語は前処理を消すのではなく、前処理結果を LLM が読みやすい表現へ変える層である。構造化データは正確な計算と検証に向き、自然言語は意味の統合に向く。両者を競合させず、構造化した事実と、その要約文を併用するのが安全である。

暗黙的フィードバックは「評価 5」ではない

Hulu では視聴の有無が主なフィードバックである。しかし、再生されたことを「好き」と断定できない。自動再生、誤タップ、家族による共用、途中離脱、続き物を義務的に見た場合などがあるからである。反対に、未視聴も「嫌い」を意味しない。単に表示されていない、配信開始を知らない、時間がなかった可能性がある。

従って、LLM に渡す要約も次のように証拠の強さを区別するべきである。

  • 25% 以上視聴した作品: 比較的強い正のシグナルであるが、明示的な好評価ではない
  • 長時間または完了近くまで視聴した作品: より強い関心の証拠になりうる
  • 詳細ページを訪問しただけの作品: 弱い関心である
  • 表示されたが選択されなかった作品: 曝露を確認できるが、単発では強い負例にしない
  • 表示すらされていない作品: 正負不明である

例えば、作品 ii に対する一つの簡易な行動強度を次のように作れる。

rui=0.5min(wuidi,1)+0.31[25%以上視聴]+0.21[お気に入り]r_{ui} = 0.5\min\left(\frac{w_{ui}}{d_i},1\right) +0.3\mathbf{1}[\text{25\%以上視聴}] +0.2\mathbf{1}[\text{お気に入り}]

ここで、 wuiw_{ui} はプロフィール uu の視聴秒数、 did_i は作品長である。これは正解となる唯一の式ではなく、異なる行動を同一の「視聴済み」に潰さないための説明例である。

数値を入れてみる。100 分の『インターステラー』を 80 分視聴し、25% 条件を満たしたが、お気に入り登録はないとする。このとき、

rui=0.5×0.8+0.3×1+0.2×0=0.7r_{ui} =0.5\times0.8+0.3\times1+0.2\times0 =0.7

一方、同じ作品を 5 分だけ視聴したなら、25% 条件を満たさず、

rui=0.5×0.05=0.025r_{ui} =0.5\times0.05 =0.025

となる。両方を単なる watched=true にすると、この差が失われる。

手を動かす例: 「未視聴」を二種類に分ける

次の評価日の翌日に、あるプロフィールについて三作品の状態が観測されたとする。

作品 ホームに表示されたか タップされたか 25% 以上視聴されたか
『SPY×FAMILY』 表示あり、1 位 なし なし
『葬送のフリーレン』 表示なし なし なし
『名探偵コナン』 表示あり、4 位 あり あり

二値の視聴ラベルだけを作れば、『SPY×FAMILY』と『葬送のフリーレン』はどちらも y=0y=0 になる。しかし、意味は同じではない。前者は少なくとも表示されたうえで選ばれなかったが、後者は選択肢として提示されていないため、好みかどうかを判断できない。

そこで、最も単純な学習データの作り方では、例えば次のように区別する。

  • 『名探偵コナン』: 正例として使う
  • 『SPY×FAMILY』: 曝露済みの弱い負例候補として使う
  • 『葬送のフリーレン』: 正負不明として、単純な負例からは除く

さらに、表示位置 1 位と 20 位ではユーザーが目にした確率も異なる。従って、表示済み負例を使う場合にも、event_params_tray_orderevent_params_tap_title_position などから得られる位置を保持する。この例から分かるのは、「視聴 0」という同じ数値でも、推薦システムが作品を見せたか否かによって学習上の意味が変わることである。

難所の補足: 暗黙的フィードバックは「欠測の仕方」がランダムではない

統計的には、未視聴データが単純にランダムに欠けているわけではない。過去の推薦器が上位に出した人気作品ほど表示されやすく、表示された作品ほど視聴される機会が多い。従って、視聴ログにはユーザー嗜好だけでなく、過去の推薦方策、トレイ位置、作品の知名度が混ざっている。

例えば、ユーザーが『葬送のフリーレン』と無名の新作アニメを同程度に好むとしても、前者だけがホーム上段に表示されれば、ログ上は前者の視聴だけが増える。このログをそのまま学習すると、次のモデルも前者を上位にし、さらに前者のログが増える。この自己強化ループが popularity bias の一因である。

ここで大切なのは、「視聴ログはユーザーの純粋な好みを直接観測したものではなく、過去に何を見せたかを通して観測した結果である」と理解することである。そのため、impression と位置を保存し、探索枠や A/B test を使って、現在の推薦器がほとんど見せていない作品についても情報を得る必要がある。

時間減衰を使った履歴要約

コンテキスト長には上限があるため、全視聴履歴をそのまま入れるべきではない。また、3 年前の嗜好と昨夜の意図を同じ重みで扱うのも不自然である。そこで、行動強度 ruir_{ui} に時間減衰を掛ける方法がある。

qui=ruiexp(Δtuiτ)q_{ui} = r_{ui}\exp\left(-\frac{\Delta t_{ui}}{\tau}\right)

Δtui\Delta t_{ui} は最終行動からの経過日数、 τ\tau は減衰の時定数である。例えば rui=0.8r_{ui}=0.8τ=30\tau=30 日なら、3 日前の作品は、

qui=0.8exp(330)0.724q_{ui} =0.8\exp\left(-\frac{3}{30}\right) \approx0.724

90 日前の作品は、

qui=0.8exp(9030)0.040q_{ui} =0.8\exp\left(-\frac{90}{30}\right) \approx0.040

となる。ただし、長期嗜好まで消すと「昔からミステリーが好き」という安定した特徴を失う。そのため、実際には「直近セッション」「過去 30 日」「長期プロファイル」を別々に要約すると扱いやすい。

手を動かす例: 三作品から短期嗜好を集計する

時定数を τ=30\tau=30 日とし、次の三つの視聴があるとする。

作品 行動強度 ruir_{ui} 経過日数 Δtui\Delta t_{ui} 主な属性
『進撃の巨人』 0.90.9 1 日 ダーク、アクション、緊張感
『名探偵コナン』 0.80.8 60 日 推理、事件、アニメ
『インターステラー』 0.70.7 10 日 SF、思索的、感動

時間減衰後の重みは次のようになる。

qu,進撃=0.9exp(130)0.870q_{u,\text{進撃}} =0.9\exp\left(-\frac{1}{30}\right) \approx0.870
qu,コナン=0.8exp(6030)0.108q_{u,\text{コナン}} =0.8\exp\left(-\frac{60}{30}\right) \approx0.108
qu,インターステラー=0.7exp(1030)0.502q_{u,\text{インターステラー}} =0.7\exp\left(-\frac{10}{30}\right) \approx0.502

合計は 0.870+0.108+0.502=1.4800.870+0.108+0.502=1.480 である。各作品の正規化重みは、

q~u,進撃=0.8701.4800.588\tilde{q}_{u,\text{進撃}} =\frac{0.870}{1.480} \approx0.588
q~u,コナン=0.1081.4800.073\tilde{q}_{u,\text{コナン}} =\frac{0.108}{1.480} \approx0.073
q~u,インターステラー=0.5021.4800.339\tilde{q}_{u,\text{インターステラー}} =\frac{0.502}{1.480} \approx0.339

となる。従って、短期要約では「直近は緊張感のある作品への関心が強く、思索的 SF にも関心がある」と表現できる。一方、『名探偵コナン』の推理嗜好を完全に消したくない場合は、この短期要約とは別に、時間減衰を弱くした長期要約へ残す。この二層化により、「昨夜の気分」と「長年の好み」を同じ一文に押し込まずに済む。

LLM に渡す構造化入力の例

自然言語だけでも処理できるが、本番では意味の境界が明確な JSON の方が検証しやすい。

{
  "profile": {
    "is_kids": false,
    "language": "ja",
    "current_time_jst": "2026-08-24T21:10:00",
    "device": "tv"
  },
  "recent_history": [
    {
      "series_id": 500001745,
      "title": "インターステラー",
      "watched_ratio": 0.80,
      "days_ago": 3,
      "evidence": "long_view"
    },
    {
      "series_id": 500005989,
      "title": "るろうに剣心 -明治剣客浪漫譚- 追憶編",
      "watched_ratio": 0.05,
      "days_ago": 1,
      "evidence": "short_view"
    }
  ],
  "long_term_summary": {
    "positive_tendencies": ["思索的SF", "緊張感のあるドラマ"],
    "uncertain_tendencies": ["時代劇アニメ"]
  },
  "request": {
    "task": "rerank",
    "top_k": 5
  }
}

重要なのは、要約を事実と推論に分けることである。「『インターステラー』を 80% 視聴した」は事実であるが、「思索的 SF が好き」はモデルによる推論である。両者を区別しておけば、誤った嗜好推定を監査しやすい。

1.2 ステップ 2: プロンプトエンジン

プロンプトエンジンは単なる文字列結合器ではない。入力選択、トークン予算、候補順のランダム化、出力 schema、禁止事項、バージョン管理を含む、モデルとの API 契約である。

難所の補足: プロンプトは「お願い文」ではなく境界仕様である

人間への依頼なら、「おすすめを五つ教えて」で意図を補ってもらえる。しかし、本番システムでは、五件未満のとき、候補が不適切なとき、ID が欠けているとき、配信条件が矛盾するときの動作まで決める必要がある。この意味で、プロンプトは自然言語で書かれた関数仕様に近い。

通常の関数が入力型、返り値型、例外を定義するのと同様に、推薦プロンプトも入力データの意味、許可された候補、出力 schema、棄権条件を定義する。プロンプトに書かれた指示だけでは完全な強制力を持たないため、schema validation や候補 ID の allowlist が、型検査と実行時検証の役割を担う。

また、固定指示、ユーザーの自然言語要求、作品説明が同じ文字列として連結されても、信頼度は同じではない。固定指示はシステムのルール、ユーザー要求は満たすべき希望、作品説明は参照用データである。この優先順位を明確に分けることで、作品説明中の文章を命令として誤解する prompt injection も防ぎやすくなる。

実用的なプロンプトは、少なくとも次のブロックを持つ。

  1. 役割と目的

  2. 行動シグナルの意味

  3. ユーザー状態

  4. 選択可能な候補集合

  5. 配信・安全・多様性などの制約

  6. 出力 schema

  7. 不確実な場合の動作

Hulu 向けの簡略例は次の通りである。

あなたは動画推薦の再ランキング器である。

目的:
- 与えられた候補だけから、今視聴される可能性が高い順に5件選ぶ。
- series_idを一文字も変更しない。

行動の解釈:
- 25%以上の視聴は比較的強い正の証拠だが、明示評価ではない。
- 短時間視聴は弱い証拠であり、嫌いと断定しない。
- 未視聴は負例ではない。

制約:
- candidatesにない作品を生成しない。
- 同一series_idを重複させない。
- 作品説明にない事実を推薦理由に使わない。
- 各理由はユーザー履歴の事実1件と作品属性1件を根拠にする。

出力:
{"recommendations":[{"series_id":整数,"rank":整数,"reason":文字列}]}

候補は作品名だけでなく、series_id と配信時点の必要なメタデータを付けて渡す。タイトルは重複・表記揺れ・リメイクがありうるため、下流処理では ID を主キーにする。

候補順バイアスへの対処

LLM は入力の先頭や末尾にある候補を選びやすいことがある。従って、候補順を既存ランカー順のまま固定すると、LLM の改善に見えて実際には元順位をコピーしているだけかもしれない。

評価時には候補順を複数回シャッフルし、順位の安定性を見る方法がある。候補順 π\pi を変えたときの出力順位を比較し、同じ作品が大きく変動するなら、モデルの判断より位置バイアスが強い可能性がある。本番では、元ランクを明示的な特徴として渡すか、伏せるかも実験条件として固定する必要がある。

手を動かす例: シャッフル前後の Top-3 安定性を測る

候補を [進撃の巨人, 葬送のフリーレン, 名探偵コナン, SPY×FAMILY, インターステラー] の順で渡したところ、LLM の Top-3 が次になったとする。

R1 = [進撃の巨人, 葬送のフリーレン, 名探偵コナン]

候補順を逆にして再度問い合わせると、次になったとする。

R2 = [インターステラー, SPY×FAMILY, 進撃の巨人]

順位を無視して Top-3 の集合を比較すると、共通作品は『進撃の巨人』だけである。Jaccard 類似度は、

J(R1,R2)=R1R2R1R2=15=0.2J(R_1,R_2) = \frac{|R_1\cap R_2|}{|R_1\cup R_2|} = \frac{1}{5} =0.2

となる。入力内容が同じなのに 0.20.2 しか一致しないなら、作品適合度より候補位置に強く反応している可能性がある。反対に、R2 = [進撃の巨人, 名探偵コナン, 葬送のフリーレン] なら集合は完全一致し、Jaccard 類似度は 1.01.0 である。ただし順位は異なるため、厳密には順位相関も別途確認する。このように、同じ候補を並べ替えるだけで簡単な頑健性試験を作れる。

1.3 ステップ 3: LLM 推論

LLM 推論では「どの作品を出したか」だけでなく、「解析可能か」「再現可能か」「遅延と費用を満たすか」も品質である。

出力は自由文ではなく schema で拘束する

本番では JSON Schema や constrained decoding を使い、次を機械的に保証する方がよい。

  • series_id が整数である
  • rank が 1 から KK までである
  • 件数がちょうど KK 件である
  • 候補集合外 ID を受け付けない
  • 重複 ID を受け付けない
  • 説明の最大長を制限する

温度を低くすれば出力は安定しやすいが、温度 0 でも完全な決定性は保証されないことがある。モデルや推論基盤の更新も結果を変えるため、model_versionprompt_version、候補集合、出力、処理時間、token 数を記録する必要がある。

手を動かす例: schema 検証で二つの事故を止める

候補 ID が [101, 102, 103] であるのに、LLM が次を返したとする。

{
  "recommendations": [
    {"series_id": 101, "rank": 1},
    {"series_id": 999, "rank": 2},
    {"series_id": 101, "rank": 3}
  ]
}

JSON としては正しいが、999 は候補集合外であり、101 は重複している。従って、schema の型検証だけでは不十分であり、次の集合検証も必要である。

{101,999,101}{101,102,103}\{101,999,101\}\nsubseteq\{101,102,103\}

さらに、重複を除いた出力件数は 2 件しかない。そこでこの応答を不正として破棄し、再試行するか、既存ランカーの [101, 102, 103] へ fallback する。LLM の文章が自然かどうかに関係なく、ID の正しさは機械的に判定できる部分である。

難所の補足: constrained decoding と事後 validation は役割が異なる

constrained decoding は、生成途中で「次に出してよい token」を制限する方法である。例えば JSON の閉じ括弧が必要な位置では閉じ括弧だけを許可し、series_id には整数だけを許可する。これにより、壊れた JSON を生成する確率を大きく下げられる。

しかし、形式が正しくても内容が正しいとは限らない。整数 999 を正しい JSON として返せても、それが候補集合に存在するとは限らない。また、二つの異なる順位に同じ ID を置くことも、型としては整数なので生成できる場合がある。

従って、constrained decoding は構文を守らせ、事後 validation は業務上の意味を守らせる、と分けて理解する。前者は「文法的に正しい文か」、後者は「Hulu の推薦結果として有効か」を検査する。

一度に何件比較させるか

候補が数百件ある場合、全件を一つのプロンプトへ入れると、費用だけでなく順位品質も悪化しうる。代表的な分解は次の通りである。

  • pointwise: 各候補を独立に採点する。並列化しやすいが、候補間比較が弱い
  • pairwise: 二作品ずつ比較する。相対判断はしやすいが、比較回数が増える
  • listwise: 複数候補を一度に並べる。Top-K に近いが、候補数と順序バイアスに注意する

NN 件の全 pair を比較すると N(N1)/2N(N-1)/2 回必要になる。例えば N=50N=50 なら、

50×492=1,225\frac{50\times49}{2}=1{,}225

回であり、オンライン推論には重い。従って、従来ランカーで 20〜50 件まで絞って listwise に再ランキングする、複数の小さな window に分ける、pointwise score と決定論的 sort を組み合わせる、といった構成が現実的である。

手を動かす例: 同じ四作品を三方式で並べる

『インターステラー』を 80% 視聴し、『進撃の巨人』を 90% 視聴したプロフィールに、次の四候補を出すとする。

  • A: 『葬送のフリーレン』
  • B: 『SPY×FAMILY』
  • C: 『名探偵コナン』
  • D: 思索的な新作 SF 映画

pointwise では、一作品ずつ「25% 以上視聴する確率」を出す。仮に、

p(A)=0.62,p(B)=0.48,p(C)=0.44,p(D)=0.71p(A)=0.62, \quad p(B)=0.48, \quad p(C)=0.44, \quad p(D)=0.71

なら、単純な降順は D > A > B > C である。利点は四候補を独立に並列評価できることである。しかし、各 score が別々の生成で校正されていなければ、0.620.620.710.71 の差を額面通りに比較できないことがある。

pairwise では、全 6 組を比較する。

比較 勝者
A 対 B A
A 対 C A
A 対 D D
B 対 C B
B 対 D D
C 対 D D

勝利数は D が 3、A が 2、B が 1、C が 0 なので、やはり D > A > B > C となる。ただし、比較結果が循環して A > B, B > D, D > A となることもあり、その場合は単純な全順序を作れない。

listwise では四作品を同時に渡し、[D, A, B, C] を直接返させる。候補同士の重複や多様性を同時に考えられる一方、候補順バイアスを受けやすい。この具体例により、三方式は「同じランキング問題の書き方が違う」だけでなく、計算回数、比較可能性、バイアスの種類が異なることが分かる。

難所の補足: 三方式は「試験の採点方法」と考えると分かりやすい

pointwise は、答案を一枚ずつ独立に 100 点満点で採点する方法に近い。同じ採点基準が安定していれば全答案を並べられるが、採点時ごとに基準が揺れると、別々に付けた 82 点と 80 点を公平に比較しにくい。

pairwise は、二枚の答案を見て「どちらが良いか」だけを決める方法である。絶対的な点数を決めるより簡単な場合があるが、答案が増えると比較回数が急増する。また、A は B より良く、B は C より良いが、C は A より良いという循環も起こりうる。

listwise は、答案をまとめて机に並べ、全体を一度に順位付けする方法である。全体のバランスを見られる一方、同時に比較できる枚数には限界があり、最初に置かれた答案が目立つといった位置効果を受ける。

Hulu の実装では、一つだけを選ぶのではなく、従来ランカーの pointwise score で候補を絞り、LLM の listwise 判断で上位だけを再ランキングするなど、複数方式を段階的に組み合わせられる。

1.4 ステップ 4: 後処理

教科書ではオプションとされているが、Hulu の本番配信では後処理は実質的に必須である。LLM が「配信可能」と判断したとしても、データベース上の制約を再検証する。

代表的なチェックは次の通りである。

  • 現在時刻が publish_start_at 以降かつ publish_end_at 以前である
  • is_blacklist=false である
  • キッズプロフィールと rating_namekids_mature_flg の条件が合う
  • 表示面に応じて service_type が SVOD または TVOD の要件を満たす
  • 同じ series_id や実質同一作品が重複していない
  • 既視聴除外を行うトレイなら unique_viewed_series にある作品を除く
  • 続きから見るトレイなら、逆に既視聴シリーズを優先する
  • 必要件数に満たない場合は安全な既存ランカーの結果で補完する

特に「既視聴を除く」は常に正しい規則ではない。映画の新規発見トレイでは合理的だが、シリーズの次エピソードや再視聴需要では誤りである。ルールはトレイ目的ごとに変える必要がある。

手を動かす例: 配信期間を JST に直して fallback まで行う

説明用の仮想的な配信情報として、現在時刻を 2026 年 8 月 24 日 21:00 JST とする。item_information_table の配信日時は UTC なので、現在時刻を UTC に直すと 2026 年 8 月 24 日 12:00 UTC である。

LLM が次の順で 5 作品を返したとする。

LLM 順位 作品 publish_end_at is_blacklist
1 『インターステラー』 2026-12-31 14:59:59 UTC false
2 『葬送のフリーレン』 2026-08-24 11:59:59 UTC false
3 『進撃の巨人』 2027-03-31 14:59:59 UTC true
4 『名探偵コナン』 2027-12-31 14:59:59 UTC false
5 『SPY×FAMILY』 2026-10-31 14:59:59 UTC false

ここで示した日時とフラグは計算説明用の仮定であり、実際の配信状態を表すものではない。2 位作品の終了時刻 11:59:59 UTC は、現在の 12:00:00 UTC より 1 秒前なので除外する。3 位作品もブラックリスト条件により除外する。残るのは 1、4、5 位の 3 件である。

Top-5 が必要なら、既存ランカーの次点候補から配信可能な 2 件を補い、最終リストを 5 件にする。この例では、LLM へ日時を読ませただけで安全とせず、同じ時刻基準でデータベース側が再判定することが重要である。また、UTC と JST を混ぜると境界時刻で誤配信が起きるため、比較する時刻系を統一する。

多様性再ランキングの数値例

関連度だけで選ぶと、上位が同一ジャンル・同一シリーズ群に偏りやすい。そこで Maximum Marginal Relevance に似た考え方で、未選択候補 ii のスコアを次のようにする。

MMR(i)=λs(u,i)(1λ)maxjSsim(i,j)\operatorname{MMR}(i) = \lambda s(u,i) -(1-\lambda)\max_{j\in S}\operatorname{sim}(i,j)

s(u,i)s(u,i) はユーザー適合度、SS はすでに選んだ作品、作品間類似度は 00 から 11 とする。すでに『インターステラー』を一件目に選んだと仮定する。λ=0.7\lambda=0.7、候補 A『TENET テネット』の適合度が 0.900.90、既選択作品との最大類似度が 0.950.95 なら、

MMR(A)=0.7×0.900.3×0.95=0.345\operatorname{MMR}(A) =0.7\times0.90-0.3\times0.95 =0.345

候補 B『葬送のフリーレン』の適合度が 0.820.82、最大類似度が 0.200.20 なら、

MMR(B)=0.7×0.820.3×0.20=0.514\operatorname{MMR}(B) =0.7\times0.82-0.3\times0.20 =0.514

適合度だけなら『TENET テネット』が上だが、すでに似た『インターステラー』を選んでいるため、二件目には『葬送のフリーレン』を出す。この制御は LLM の曖昧な「多様にせよ」という指示だけに任せず、作品 embedding やジャンルを使って後処理で再現可能に行える。

難所の補足: MMR は「精度を捨てて多様にする式」ではない

MMR の目的は、無関係な作品を混ぜることではなく、すでに選んだ作品とほぼ同じ情報しか持たない候補の限界効用を下げることである。Top-10 のすべてが似た SF 映画なら、11 本目の似た SF 映画を一つ追加しても、ユーザーへ与える新しい選択肢は少ない。一方、少し score が低くても、別の気分に対応する良質なアニメを一つ入れる価値は高い場合がある。

パラメータ λ\lambda は、ユーザー適合度と重複回避の交換比率である。λ\lambda を 1 に近づけると元の関連度順に近づき、0 に近づけると多様性を強く優先する。ただし、全ユーザー・全トレイに同じ値が最適とは限らない。「あなたへのおすすめ」では適合度を重くし、「今までと違う作品」では新規性を重くするなど、トレイ目的に合わせる必要がある。

また、多様性をジャンル名だけで測ると、どちらも「アニメ」だが内容が大きく異なる作品を同一視する。映像 embedding、説明文 embedding、雰囲気タグなどを使えば、ジャンルをまたぐ意味的な近さも考慮できる。

1.5 ステップ 5: 評価

評価は、推薦品質、生成品質、システム品質の三層に分けるとよい。

主な指標 答える問い
推薦品質 Recall@K, NDCG@K, MRR, coverage, novelty, diversity 実際に見られる作品を上位に出せたか
生成品質 valid-ID rate, schema-valid rate, grounded-reason rate 存在する候補だけを、根拠に沿って返したか
システム品質 p50/p95/p99 latency, token 数, 1,000 request 当たり費用, timeout rate 本番の速度・費用・信頼性を満たしたか

NDCG のワークド例

未来の正解を「評価時点より後に 25% 以上視聴された作品」とする。推薦上位 3 件が [葬送のフリーレン, SPY×FAMILY, 名探偵コナン] で、前者と後者が 25% 以上視聴され、中央の作品は視聴されなかったとする。関連度は [1, 0, 1] なので、

DCG@3=1log2(2)+0log2(3)+1log2(4)=1.5\operatorname{DCG@3} = \frac{1}{\log_2(2)} +\frac{0}{\log_2(3)} +\frac{1}{\log_2(4)} =1.5

理想順位は正解 2 件を先頭に置く [1,1,0] なので、

IDCG@3=1+1log2(3)1.631\operatorname{IDCG@3} =1+\frac{1}{\log_2(3)} \approx1.631

従って、

NDCG@3=1.51.6310.920\operatorname{NDCG@3} =\frac{1.5}{1.631} \approx0.920

となる。Recall@K は正解を回収したかを見るが、NDCG@K は上位に置けたかも評価する。

難所の補足: NDCG が対数割引と理想順位を使う理由

DCG の分母にある log2(k+1)\log_2(k+1) は、順位が下がるほど価値を徐々に割り引くためのものである。1 位と 2 位の差は大きく評価するが、99 位と 100 位の差はそれほど大きく扱わない。これは、ホーム画面の上位ほど見られやすいという推薦 UI の直感に合う。

ただし、DCG の生値だけではプロフィール間を比較しにくい。未来に視聴した正解作品が一件しかないプロフィールと、十件あるプロフィールでは、到達可能な最大 DCG が異なるからである。そこで、そのプロフィールで可能な理想順位の DCG、すなわち IDCG で割る。理想通りなら NDCG は 1、正解を下位へ置くほど 0 に近づく。

NDCG は「推薦しなかった未知作品が本当は好みだったか」を解決する指標ではない。あくまで定義した正解ラベルに対して順位を測る。そのため、暗黙的フィードバックでは、何を正解としたか、過去に曝露されたかという前提も同時に記載する必要がある。

時系列分割と曝露バイアス

学習時点より未来の視聴を正解にしなければ、未来情報の漏洩が起きる。例えば、7 月末までの履歴で入力を作り、8 月の視聴を正解にする。作品メタデータも、評価時点で利用可能だった snapshot を使うべきである。

また、ログで観測できる視聴は過去の推薦方策が表示した作品に偏る。表示されなかった作品は選びようがない。したがって、単純なオフライン NDCG の改善だけでは、新しい方策がオンラインでも改善するとは断定できない。最終判断には A/B test が必要であり、可能なら impression と位置を保存して、位置バイアス補正や反実仮想評価を検討する。

難所の補足: 反実仮想評価は「別の推薦をしていた世界」を推定する

新しい推薦器をまだ本番に出していないなら、その推薦結果にユーザーがどう反応したかというログは存在しない。反実仮想評価は、過去ログを使って「もし別の作品を表示していたら」という未観測の結果を推定しようとする考え方である。

例えば、過去方策が『名探偵コナン』を 80% の確率で表示し、無名作品 X を 5% の確率でしか表示しなかったとする。少数ながら X が表示されたケースは、X への反応を知るうえで希少であるため、単純集計より大きな重みを持たせる方法がある。ただし、過去方策が X を一度も表示していなければ、ログだけから X の効果を推定することはできない。

このため、反実仮想評価には「各候補をどの確率で表示したか」という propensity が必要になる。完全に決定論的な過去方策では推定できる範囲が狭いため、安全な範囲のランダム探索を少量入れてデータを集める場合がある。これは高度な手法であり、まず impression、表示位置、方策 version を欠かさず記録することが出発点である。

2. 推薦のためのプロンプティング

プロンプティングは、学習済みパラメータを変えず、入力の書き方によってタスク、証拠、制約、出力形式を指定する方法である。推薦では「ユーザーを説明する文章を書くこと」だけが目的ではない。候補、制約、正解の意味、出力契約まで含めて初めて推薦プロンプトになる。

2.1 ゼロショット、Few-Shot、指示、CoT の違い

これらは排他的な分類ではない。例えば、明確な指示を与えるが例を付けないなら「指示ベースのゼロショット」である。指示に 3 例を付ければ「指示ベースの Few-Shot」である。

手法 何を与えるか Hulu で向く場面 主な注意点
ゼロショット タスクと対象だけ 素早い baseline、自由な対話 出力形式・ドメイン規則が不安定
Few-Shot 入出力例を数件 理由の書式、暗黙的行動の解釈を揃える 例の偏り、token 増加、例の丸写し
指示ベース 目的・制約・schema を明示 本番の再ランキング、説明生成 指示間の矛盾、長すぎる規則
CoT 的分解 判断を段階に分ける 多制約の比較、オフライン分析 内部推論全文を出させる必要はない

ゼロショットの例

最近『インターステラー』を長時間視聴したプロフィールに、
候補A〜Eから次に見る作品を3件選べ。

これは簡単だが、「長時間視聴を好みと断定してよいか」「候補外を出してよいか」「どの形式で返すか」が曖昧である。

Few-Shot の例

例1:
履歴: 作品Xを95%視聴、作品Yを3%視聴
解釈: Xは強い正の証拠、Yは不確実

例2:
履歴: 作品Pの詳細ページを訪問したが未再生
解釈: 弱い関心。正例にも負例にも断定しない

同じ基準で次の履歴を解釈し、候補を順位付けせよ。

Few-Shot の価値は、作品名の知識を教えることより、Hulu 固有の行動ラベルの意味と出力規則を例示する点にある。

CoT は「思考全文の表示」ではなくタスク分解として使う

本番では、長い自由記述の推論をユーザーへ表示する必要はない。代わりに、モデルへ次の判断項目を構造化出力させる。

  1. ハード制約を満たすか

  2. 直近意図に適合するか

  3. 長期嗜好に適合するか

  4. 既選択作品と重複しすぎないか

  5. 最終 score と短い根拠

例えば各項目を 0〜2 点で返させ、合計を監査できるようにする。ただし、LLM が書いた理由は因果的に忠実な内部推論とは限らない。説明は「モデルがそう考えた証明」ではなく、入力根拠と矛盾しないユーザー向け文章として検証する。

手を動かす例: 段階別 score を実際に合成する

『インターステラー』と『進撃の巨人』を長時間視聴したプロフィールに、A『葬送のフリーレン』、B『SPY×FAMILY』、C 新作 SF 映画を比較するとする。各候補を 0〜2 点で採点し、ハード制約は 0 点なら除外、それ以外の三項目を合計すると決める。

候補 ハード制約 直近意図 長期嗜好 非重複性 合計
A『葬送のフリーレン』 2 1 2 2 1+2+2=51+2+2=5
B『SPY×FAMILY』 2 1 1 2 1+1+2=41+1+2=4
C 新作 SF 映画 2 2 2 1 2+2+1=52+2+1=5

A と C は同点である。そこで「今夜の直近意図」を優先する tie-break rule を定めれば、C を 1 位、A を 2 位にできる。一方、C の publish_end_at が過ぎてハード制約が 0 点なら、合計点に関係なく除外する。この例の重要点は、ソフトな適合度とハード制約を同じ加算 score に混ぜないことである。

2.2 推薦タスク別の設計

評価予測

教科書の 1〜10 点予測は明示評価データに自然だが、Hulu の暗黙的フィードバックへそのまま移植するのは不自然である。代わりに、次のようなラベルが使える。

  • 評価期間内に 25% 以上視聴する確率
  • impression 後に再生開始する確率
  • 10 分以上視聴する確率
  • 期待視聴秒数

例えば、LLM に恣意的な「8.7 点」を生成させるより、候補間の順位や high / medium / low の離散判定を学習させ、その後に calibration する方が評価しやすい。

手を動かす例: 予測確率が校正されているか確かめる

モデルが 100 件の候補すべてに「25% 以上視聴される確率は 0.80.8」と予測したとする。実際に 25% 以上視聴された作品が 80 件なら、この bucket ではよく校正されている。一方、実際の視聴が 25 件しかなければ、予測と実績の差は、

0.800.25=0.55|0.80-0.25|=0.55

であり、モデルは大幅に過信している。ランキング順がある程度正しくても、確率値をそのまま「80% の確率」として業務判断に使えない状態である。

Hulu では、例えば予測を [0.0,0.1), [0.1,0.2), \ldots, [0.9,1.0] の bucket に分け、各 bucket の平均予測確率と実際の 25% 視聴率を比較できる。これは「高い順に並べられるか」というランキング評価とは別の検査である。

難所の補足: ランキング性能と確率 calibration は独立にずれうる

候補 A、B、C の真の視聴しやすさが順に高いとき、モデルが 0.99、0.98、0.97 と予測しても、0.60、0.40、0.20 と予測しても、順位は同じである。従って NDCG は同程度になりうる。しかし、実際の視聴率が 0.60、0.40、0.20 に近いなら、前者は大幅に過信しており、後者の方がよく校正されている。

Top-K を並べるだけなら順位が重要である。一方、「予測視聴確率が 0.7 以上なら高コストな LLM 説明を生成する」「期待視聴時間を使って編成枠を配分する」といった意思決定では、確率の絶対値も重要になる。利用目的によって、ランキング指標だけで十分か、calibration も必要かを分ける。

Top-K 推薦

候補を与えず自由生成させる open-vocabulary Top-K と、候補集合から選ばせる closed-set Top-K を区別する。本番 Hulu では後者が基本である。

candidatesに含まれるseries_idだけを使い、5件を順位付けせよ。
候補外を推薦したい場合も生成せず、insufficient_candidates=trueを返せ。

この insufficient_candidates は重要である。候補がすべて不適切なときに、無理に 5 件を選ばせると品質が落ちる。既存ランカーや人気作品へ fallback できる設計が必要である。

対話型推薦

対話では、発話履歴そのものではなく、更新可能な状態として管理するとよい。

{
  "must_have": ["SF", "映像美"],
  "avoid": ["ホラー", "過度な恋愛中心"],
  "soft_preferences": ["スリラー"],
  "already_seen": [500001745],
  "open_questions": ["長編でもよいか"]
}

「ホラーは好まない」のような明示的制約は、過去の暗黙履歴より優先する。ただし、その制約が今回だけか長期的かを区別する。会話の途中で条件が変わったら、古い状態を上書きする必要がある。

説明生成

良い説明は、ユーザー根拠、作品根拠、両者の接続を含む。

最近『インターステラー』を長時間視聴している。
候補作品は思索的なSFで、avg_mood_tagの thought-provoking が高い。
したがって、壮大な設定の中で考えさせられる物語を好む可能性に合う。

一方、「あなたはクリストファー・ノーラン監督が大好きだから」のように、履歴が一作品しかないのに強く断定する説明は避ける。「好む可能性がある」「最近の視聴に近い」と証拠強度に合わせるべきである。

逐次的推薦

逐次推薦では集合だけでなく順序が重要である。例えば、長期的にはアニメをよく見るプロフィールでも、直近に SF 映画を 2 本続けて見ていれば、次の一本では現在の SF セッションを重く見る可能性がある。

入力を単なる作品集合ではなく、

90日前: アニメA
30日前: アニメB
昨日: SF映画C
今日: SF映画D

のように順序と時間差付きで与える。長期嗜好と直近意図を別 score にしてから統合すると解釈しやすい。

手を動かす例: 長期嗜好と直近セッションを混ぜる

長期ユーザー embedding がアニメ・推理寄りで、候補に対する長期 score が次だとする。

候補 長期 score slongs_{\mathrm{long}} 直近 SF セッション score ssessions_{\mathrm{session}}
『名探偵コナン』 0.900.90 0.200.20
新作 SF 映画 0.450.45 0.950.95

今夜は直近セッションを 70%、長期嗜好を 30% として合成する。

s(i)=0.7ssession(i)+0.3slong(i)s(i) = 0.7s_{\mathrm{session}}(i) +0.3s_{\mathrm{long}}(i)

『名探偵コナン』は、

s(コナン)=0.7×0.20+0.3×0.90=0.41s(\text{コナン}) =0.7\times0.20+0.3\times0.90 =0.41

新作 SF 映画は、

s(新作SF)=0.7×0.95+0.3×0.45=0.80s(\text{新作SF}) =0.7\times0.95+0.3\times0.45 =0.80

となり、今夜は新作 SF 映画が上位になる。一方、直近セッションが終了した翌週に長期嗜好の重みを 80% へ戻せば、『名探偵コナン』が再び上位になりうる。逐次推薦で重要なのは、嗜好が突然永久に変わったと決めつけず、一時的意図に別の重みを持たせることである。

2.3 プロンプト設計の実践ポイント

情報量と簡潔さのバランス

全履歴を削らず入れることは「情報豊富」ではなく、関連情報をノイズに埋める行為になりうる。トークン予算を、例えば次のように明示的に割り当てる。

ブロック 予算の例
固定指示と schema 800 token
直近セッション 600 token
長期嗜好要約 300 token
候補 30 件 2,400 token
出力 500 token

この配分は一例であり、重要なのは候補数を増やした結果、指示や重要履歴がコンテキストから押し出されないように計測することである。

明確性と失敗時の挙動

「良い作品を推薦せよ」では、良いの意味が定まらない。目的を「今後 7 日以内に 25% 以上視聴される可能性」「今夜テレビで一本完結の映画を選ぶ」などに具体化する。また、候補不足、metadata 欠損、矛盾する条件への動作も定義する。

prompt injection とデータ汚染

作品説明やユーザー入力は命令ではなくデータである。外部由来の説明文に「以前の指示を無視せよ」と書かれていても実行しないよう、命令ブロックとデータブロックを分ける。出力後も ID allowlist と schema validation を通す。プロンプトだけをセキュリティ境界にしてはならない。

反復改善は一件の成功例ではなく固定評価セットで行う

プロンプト変更は、同じ時系列 holdout と candidate set で比較する。平均指標だけでなく、キッズ、コールドスタート、長期利用者、TV デバイス、人気作品に偏らない層などの slice を見る。プロンプトのバージョンをログへ残さなければ、オンライン変化の原因を追えない。

3. 推薦のための LLM ファインチューニング

3.1 プロンプティングとファインチューニングの役割差

プロンプティングは、モデルがすでに持つ能力を入力時に引き出す。ファインチューニングは、推薦タスクで望ましい出力の確率を上げるよう、モデルのパラメータを更新する。

教師ありファインチューニングでは、入力 xx と正解出力 token 列 y=(y1,,yT)y=(y_1,\ldots,y_T) に対して、概ね次の負の対数尤度を最小化する。

LSFT=t=1Tlogpθ(ytx,y<t)\mathcal{L}_{\mathrm{SFT}} = -\sum_{t=1}^{T} \log p_{\theta} \left(y_t\mid x,y_{<t}\right)

例えば入力が「履歴と候補」、正解出力が series_id のランキングなら、正しいランキング token 列の確率を上げる。ただし、ログから得た未来視聴をそのまま唯一の正解文章にすると、複数の妥当な推薦があるのに一つだけを正解とする問題が生じる。また、過去方策の表示バイアスも学習する。データ構築がモデル選択以上に重要である。

難所の補足: SFT は「推薦の意味」を直接最小化しているわけではない

上の損失が直接見ているのは、正解 token を高い確率で出したかである。NDCG、視聴時間、多様性をそのまま計算して最小化しているわけではない。例えば、正解が C03、C01 の順なら、モデルはまず C03 を出し、その正解 token を前提として次に C01 を出すよう学ぶ。この学習方法を teacher forcing と呼ぶ。

teacher forcing では、学習中は常に正しい過去 token が与えられる。しかし推論時に最初の候補を誤って C02 と出すと、その後は学習時にあまり見ていない文脈から続きを生成しなければならない。この学習時と推論時の差も、生成モデルの難しさの一つである。

また、同じプロフィールに C03、C01 と C01、C03 のどちらの順も妥当な場合、一方だけを正解にすると、もう一方まで誤りとして確率を下げかねない。複数の正解順、pairwise 選好、soft label を用意する理由は、この一意でない推薦の正解を扱うためである。

手を動かす例: SFT loss を token ごとに計算する

候補番号を使い、正解出力を次の 3 token と単純化する。

[C03] [C01] [END]

学習前のモデルが、正解 token にそれぞれ次の確率を与えたとする。

p([C03]x)=0.60p([C03]\mid x)=0.60
p([C01]x,[C03])=0.50p([C01]\mid x,[C03])=0.50
p([END]x,[C03],[C01])=0.80p([END]\mid x,[C03],[C01])=0.80

この一例の SFT loss は、

LSFT=(log(0.60)+log(0.50)+log(0.80))1.427\mathcal{L}_{\mathrm{SFT}} =-\left( \log(0.60) +\log(0.50) +\log(0.80) \right) \approx 1.427

である。token 当たり平均は、

1.42730.476\frac{1.427}{3} \approx0.476

となる。学習後に正解 token の確率が順に 0.80,0.75,0.900.80, 0.75, 0.90 まで上がれば、

LSFTafter=(log(0.80)+log(0.75)+log(0.90))0.616\mathcal{L}_{\mathrm{SFT}}^{\mathrm{after}} =-\left( \log(0.80) +\log(0.75) +\log(0.90) \right) \approx 0.616

となり、loss は下がる。ただし、これは「この正解文字列を出しやすくなった」ことを示すだけであり、未知プロフィールへの NDCG やオンライン視聴時間が必ず改善したことを意味しない。そのため、学習 loss と推薦指標の両方を測る必要がある。

3.2 P5 的な統一形式の意味

P5 の発想は、次アイテム予測、評価予測、説明生成などをすべて text-to-text に変換することである。例えば、

入力: プロフィールuがA、B、Cを順に視聴した。次の作品は何か。
出力: D
入力: なぜプロフィールuにDを推薦するのか。
出力: BとDは緊張感のある犯罪ドラマという特徴を共有するためである。

のように、異なる推薦タスクを一つの生成モデルで扱える。利点はタスク間で知識を共有できることである。一方、Hulu の series_id は自然言語として意味を持たない。ID の token 化が非効率になり、未知の新規 ID を生成しにくいことがある。実運用では候補 ID を特殊 token にする、候補番号 [C01] を選ばせて後で series_id に戻す、retrieval で新規作品を候補として与える、といった対策が必要である。

手を動かす例: 長い series_id を候補 alias に置き換える

候補が次の三作品だとする。

{
  "C01": {"series_id": 500001745, "title": "インターステラー"},
  "C02": {"series_id": 500005989, "title": "るろうに剣心 -明治剣客浪漫譚- 追憶編"},
  "C03": {"series_id": 50028800, "title": "候補作品X"}
}

LLM に 500001745 を一桁ずつ自由生成させると、一桁欠けた 50001745 や、候補外の数字を作る余地がある。そこで出力語彙を [C01], [C02], [C03] に限定し、モデルには [C03, C01, C02] のように返させる。その後、決定論的な lookup により、

[C03, C01, C02]
    ↓
[50028800, 500001745, 500005989]

へ戻す。この方法なら、新作の series_id をモデルの重みに事前登録していなくても、候補表に alias を追加するだけで選択可能になる。

3.3 LoRA は何を低ランク化するのか

全パラメータ更新は高コストである。LoRA では、元の重み行列 W0Rdout×din\mathbf{W}_0\in\mathbb{R}^{d_{\mathrm{out}}\times d_{\mathrm{in}}} を凍結し、更新差分を二つの小さな行列の積で表す。

W=W0+ΔW,ΔW=αrBA\mathbf{W} = \mathbf{W}_0 +\Delta\mathbf{W}, \qquad \Delta\mathbf{W} = \frac{\alpha}{r}\mathbf{B}\mathbf{A}

ここで、

ARr×din,BRdout×r,rmin(din,dout)\mathbf{A}\in\mathbb{R}^{r\times d_{\mathrm{in}}}, \qquad \mathbf{B}\in\mathbb{R}^{d_{\mathrm{out}}\times r}, \qquad r\ll\min(d_{\mathrm{in}},d_{\mathrm{out}})

である。元の正方行列が 4096×40964096\times4096 なら、パラメータ数は、

4096×4096=16,777,2164096\times4096 =16{,}777{,}216

である。一方、rank r=8r=8 の LoRA 差分は、

8×4096+4096×8=65,5368\times4096+4096\times8 =65{,}536

パラメータであり、この一行列について約 0.39%0.39\% である。

65,53616,777,216×1000.39%\frac{65{,}536}{16{,}777{,}216} \times100 \approx0.39\%
難所の補足: なぜ低ランクで十分な場合があるのか

ファインチューニングで必要な変化が、巨大な重み空間のあらゆる方向に及ぶとは限らない。基盤モデルはすでに日本語、映画の概念、順位付けの基本を学んでいる。Hulu 推薦へ適応するときに必要なのは、「短時間視聴を負例と断定しない」「候補 ID のみを返す」「配信 metadata を根拠にする」といった、比較的限定された振る舞いの修正である可能性がある。

地図に例えると、モデル全体の知識を一から描き直すのではなく、既存地図の上に「Hulu 推薦ではこの道を優先する」という薄い交通レイヤーを重ねるのが LoRA である。低ランク行列は、この追加変更を少数の主要な方向の組み合わせとして表現する。

ただし、rank を小さくすれば常によいわけではない。必要な変更が複雑なのに rank が小さすぎると表現力が不足し、rank を大きくすると学習パラメータと過学習リスクが増える。そのため、rank は学習 loss だけでなく、未知ユーザー・未知作品を含む評価指標で選ぶ。

ただし、これはモデル全体の学習メモリが必ず 0.39% になるという意味ではない。activation、optimizer state、対象層数、batch size などもメモリを使う。また、LoRA は学習コストを下げるが、データ品質、評価、adapter の version 管理は依然必要である。

手を動かす例: 2 次元の LoRA 更新を実際に掛ける

実モデルより大幅に小さい 2 次元の例で、LoRA が元の重みに「小さな差分」を足す様子を見る。元の重みを単位行列とする。

W0=[1001]\mathbf{W}_0 = \begin{bmatrix} 1 & 0\\ 0 & 1 \end{bmatrix}

rank r=1r=1α=1\alpha=1 とし、学習された行列を次とする。

A=[0.20.1],B=[0.51.0]\mathbf{A} = \begin{bmatrix} 0.2 & -0.1 \end{bmatrix}, \qquad \mathbf{B} = \begin{bmatrix} 0.5\\ 1.0 \end{bmatrix}

差分は、

ΔW=BA=[0.51.0][0.20.1]=[0.100.050.200.10]\Delta\mathbf{W} =\mathbf{B}\mathbf{A} = \begin{bmatrix} 0.5\\ 1.0 \end{bmatrix} \begin{bmatrix} 0.2 & -0.1 \end{bmatrix} = \begin{bmatrix} 0.10 & -0.05\\ 0.20 & -0.10 \end{bmatrix}

である。従って更新後の重みは、

W=[1.100.050.200.90]\mathbf{W} = \begin{bmatrix} 1.10 & -0.05\\ 0.20 & 0.90 \end{bmatrix}

となる。入力を x=[2,1]T\mathbf{x}=[2,1]^{\mathsf{T}} とすると、元モデルの出力は、

W0x=[21]\mathbf{W}_0\mathbf{x} = \begin{bmatrix} 2\\ 1 \end{bmatrix}

であるが、LoRA 適用後は、

Wx=[1.100.050.200.90][21]=[2.151.30]\mathbf{W}\mathbf{x} = \begin{bmatrix} 1.10 & -0.05\\ 0.20 & 0.90 \end{bmatrix} \begin{bmatrix} 2\\ 1 \end{bmatrix} = \begin{bmatrix} 2.15\\ 1.30 \end{bmatrix}

となる。元の W0\mathbf{W}_0 は変更せず、低ランク差分だけで出力方向を変えている。実際の Transformer では、この変化が「短時間視聴を嫌いと断定しない」「候補 ID だけを返す」といった推薦タスク向けの出力傾向として学習される。

3.4 推薦タスク向け指示チューニング

このチューニングが学ぶのは、Hulu の全作品知識というより「依頼を推薦操作へ変換する規則」である。

入力例は次のように多様化する。

  • pointwise: 候補一件が条件に合うか判定する
  • pairwise: 二作品のどちらが現在のプロフィールに合うか選ぶ
  • listwise: 候補集合全体を並べ替える
  • 説明: 根拠フィールドだけを使い短い理由を作る
  • 対話: 明示制約を状態へ追加・削除する
  • abstention: 十分な候補がなければ棄権する

学習ペアの例は次の通りである。

{
  "instruction": "候補を今夜テレビで見る順に3件選ぶ",
  "behavior_semantics": {
    "long_view": "strong_positive_evidence",
    "short_view": "uncertain",
    "unseen": "unknown"
  },
  "history": ["インターステラー: 80%視聴"],
  "candidates": ["C01", "C02", "C03", "C04"],
  "response": ["C03", "C01", "C04"]
}

ただし、正解順位を一人の annotator の主観だけで作ると、その癖を学ぶ。未来の暗黙的行動、既存ランカー、編集者判断、複数人評価を目的に応じて組み合わせる必要がある。

3.5 ドメイン知識のファインチューニング

ドメイン適応には、未ラベル文書で言語モデル学習を続ける DAPT と、ドメイン固有の入力・正解ペアで SFT する方法がある。両者は同じではない。

方法 学習データ 主に学ぶもの
DAPT 作品説明、タグ、番組情報などの未ラベル文書 動画配信固有の語彙・文体・作品意味
ドメイン SFT 履歴・候補・正解順位、質問・正解説明 Hulu の推薦タスクと出力規則
RAG 学習せず、推論時に最新カタログを検索 現在の配信作品、最新 metadata、根拠

Hulu の item_information_table には、descriptionsentencesockets_tag、ジャンル、キャスト、監督、avg_mood_tag などがある。これらはドメイン適応に有用である。しかし、配信開始・終了、ブラックリスト、TVOD / SVOD 状態は更新される事実であり、重みに記憶させるより推論時に検索・検証する方がよい。

難所の補足: DAPT・SFT・RAG は「知識を置く場所」が違う

三手法は、同じ情報を別名で扱っているのではない。モデルのどこへ、いつ情報を与えるかが異なる。

  • DAPT は、動画配信ドメインの文章を大量に読ませ、モデル内部の言葉の捉え方を変える
  • SFT は、入力に対してどのような推薦出力を返すべきかという振る舞いを学ばせる
  • RAG は、推論のたびに外部から現在の事実を持ってくる

人間の新人研修に例えると、DAPT は映画や配信業界の資料を広く読んで語彙を身につける研修、SFT は接客例を使って推薦の回答方法を練習する研修、RAG は接客のたびに最新の配信カタログを端末で調べる行為である。

配信終了日時のように頻繁に変わる情報を DAPT や SFT で覚えさせると、再学習するまで古いままになる。反対に、毎回 RAG で一般的な「伏線」「群像劇」の意味から調べ直すだけでは、モデルのドメイン理解が弱い。安定した概念はモデルへ、変化する事実は外部データへ置く、という判断が重要である。

手を動かす例: 同じ新作を DAPT・SFT・RAG がどう扱うか

まだ視聴ログがない新作アニメ X が追加され、metadata に「時間旅行」「仲間との別れ」「感動的」「全 12 話」と記載されたとする。

  • DAPT 済みモデルは、「時間旅行」「全 12 話」といった動画ドメインの文章や語彙を自然に解釈しやすい
  • ドメイン SFT 済みモデルは、「最近、時間旅行アニメを長時間視聴したプロフィールには X を候補上位にする」という入力・出力形式に従いやすい
  • RAG は、その日のカタログから X の series_id、配信期間、service_type、説明文を取得し、モデルへ実際の候補として渡す

例えば DAPT と SFT が完了した日が 8 月 1 日で、新作 X が 8 月 20 日に登録されたなら、モデルの重みは X の存在を知らない。しかし、8 月 24 日の推論で RAG が X を取得すれば、既知の「時間旅行」「感動的」という意味を使って順位付けできる。反対に、RAG が X を候補へ入れなければ、どれほどよく学習したモデルでも X を選べない。この例が、三手法の役割の違いである。

コールドスタートへの効果と限界

新作に視聴ログがなくても、作品説明やキャスト、タグを理解できれば content-based な推薦が可能になる。例えば新作 X に行動履歴がなくても、「思索的 SF」「宇宙」「親子」という属性があれば、『インターステラー』を長時間視聴したプロフィールの候補になりうる。

しかし、ドメイン知識だけでは、その作品が実際に Hulu ユーザーへどれほど受けるか、どのトレイ位置で選ばれるかは分からない。意味的類似性は行動適合性の一部であり、オンライン行動で更新する必要がある。

3.6 個人嗜好向けファインチューニング

個人または segment ごとの adapter に嗜好を埋め込めば、深い個別化が可能だというのが教科書の主張である。ただし、大規模動画配信では慎重に評価すべきである。

ユーザーごとのチューニングが難しい理由

  • 一人あたりの教師データが少なく、過学習しやすい
  • 新しい視聴のたびに更新するのは、ユーザー embedding 更新より遅い
  • 数百万プロフィール分の adapter 配置、version、rollback が必要になる
  • プロフィール削除時に、重みに入ったデータを確実に除去するのが難しい
  • 家族共用や一時的セッションを長期嗜好として固定する恐れがある
  • 新作・配信終了などのカタログ変化には別途 retrieval が必要である

従って、Hulu での第一選択は通常、共通モデルを固定し、profile_id ごとの embedding、直近履歴、検索された候補を実行時に注入する方法である。ファインチューニングするなら、まず全ユーザー共通の推薦指示 adapter、次に十分なデータがある安定 segment の adapter、という順が安全である。

segment 単位にも注意が必要である

「SF ファン」「キッズアニメ中心」のような行動 segment は adapter を共有しやすい。しかし segment は固定属性ではなく、時間で変わる確率的な状態として扱うべきである。単一 cluster にプロフィールを閉じ込めると、フィルターバブルや古い嗜好の固定化が起きる。

また、年齢・性別だけで作品嗜好を決める segment はステレオタイプを強化しうる。行動由来の segment でも、センシティブな属性の proxy になっていないか、slice ごとの露出と品質を監査する必要がある。

3.7 パーソナライズド embedding とファインチューニングの選択

教科書の比較を Hulu 向けに具体化すると次のようになる。

観点 ユーザー embedding・実行時 context ユーザー / segment LoRA
更新 視聴後に近リアルタイム更新しやすい 再学習・adapter 切替が必要
profile 数への拡張 ベクトル保存で対応しやすい adapter 数と serving が複雑
一時的意図 直近セッションをそのまま渡せる 重みに焼くと古くなりやすい
深い言語的適応 限定的 口調・判断傾向まで変えられる
削除・訂正 context やベクトルを削除・再計算しやすい 重みからの除去が難しい
Hulu での基本用途 大規模なランキング個別化 共通タスク、安定 segment の実験

ここで、embedding は必ずしも「浅い構造情報だけ」ではない。学習目的と入力次第で、ジャンル、時系列嗜好、視聴強度などを豊かに表現できる。一方、ファインチューニングしたから必ず推薦が深くなるわけでもない。少量・偏ったデータでは、むしろ汎化を失う。

難所の補足: embedding の注入は「ID の数値を文章に足す」ことではない

profile_id やユーザー embedding をそのままテキスト token の列へ追加しても、LLM が意味を理解できるとは限らない。特に、推薦モデルの 128 次元ユーザー embedding と LLM の 4,096 次元 hidden state は、次元数も学習された空間の意味も異なる。

そこで、ユーザー embedding を LLM が受け取れる次元へ変換する projection 層を学習したり、複数の仮想 token に変換して prompt の先頭へ付けたり、adapter の中間層へ入力したりする。これは、日本語を話さない二人の間に通訳を置くことに近い。embedding が豊富な情報を持っていても、LLM の表現空間へ正しく対応づける通訳がなければ使えない。

また、projection 層を学習するときには、ベース LLM とユーザー embedding 生成器の version を固定・記録する必要がある。片方の embedding 空間だけを再学習すると、同じベクトル座標が以前と違う意味になり、projection が機能しなくなる可能性がある。

手を動かす例: ユーザー embedding を一回の視聴で更新する

説明のため、embedding を 2 次元とする。第 1 軸、第 2 軸に人間が厳密な意味を割り当てられるわけではないが、ここでは計算を見せるために「アニメアクション寄り」「思索的 SF 寄り」と仮置きする。長期ユーザー embedding を、

ulong=[0.7750.275]\mathbf{u}_{\mathrm{long}} = \begin{bmatrix} 0.775\\ 0.275 \end{bmatrix}

とする。候補 A『進撃の巨人』系の作品と候補 B の新作 SF 映画の embedding を、

eA=[0.850.15],eB=[0.300.90]\mathbf{e}_A = \begin{bmatrix} 0.85\\ 0.15 \end{bmatrix}, \qquad \mathbf{e}_B = \begin{bmatrix} 0.30\\ 0.90 \end{bmatrix}

とする。長期 embedding だけなら内積 score は、

slong(A)=ulongTeA=0.775×0.85+0.275×0.15=0.700s_{\mathrm{long}}(A) = \mathbf{u}_{\mathrm{long}}^{\mathsf{T}}\mathbf{e}_A =0.775\times0.85+0.275\times0.15 =0.700
slong(B)=ulongTeB=0.775×0.30+0.275×0.90=0.480s_{\mathrm{long}}(B) = \mathbf{u}_{\mathrm{long}}^{\mathsf{T}}\mathbf{e}_B =0.775\times0.30+0.275\times0.90 =0.480

であり、A が上である。ところが、直近に『インターステラー』を長時間視聴し、セッション embedding が、

usession=[0.100.95]\mathbf{u}_{\mathrm{session}} = \begin{bmatrix} 0.10\\ 0.95 \end{bmatrix}

になったとする。直近を 70%、長期を 30% として、

u=0.7usession+0.3ulong=[0.30250.7475]\mathbf{u}' =0.7\mathbf{u}_{\mathrm{session}} +0.3\mathbf{u}_{\mathrm{long}} = \begin{bmatrix} 0.3025\\ 0.7475 \end{bmatrix}

と更新する。新しい score は、

s(A)=0.3025×0.85+0.7475×0.150.369s'(A) =0.3025\times0.85+0.7475\times0.15 \approx0.369
s(B)=0.3025×0.30+0.7475×0.900.764s'(B) =0.3025\times0.30+0.7475\times0.90 \approx0.764

となり、B が上になる。この更新には LLM の再学習も個人 LoRA の作成も必要なく、ユーザー embedding を差し替えるだけでよい。これが、大規模サービスで動的 embedding が有力な理由である。

4. ファインチューニングの代替・補完手段

4.1 プロンプトエンジニアリング

変更が速く、データ量が少なく、まず成立性を確かめたい場合に適する。新しい業務ルールを即日反映できる一方、プロンプトが長くなるほど費用と保守負担が増える。プロンプト変更だけで出力挙動が十分安定しなくなったときが、指示チューニングを検討する一つの目安である。

4.2 RAG

RAG は、LLM が回答前に外部データから根拠を取得する構成である。Hulu では、現在配信可能な作品、作品説明、タグ、キャスト、配信期間を series_id 付きで取得し、候補として渡す。

ユーザー状態
    ↓
既存 u2i / i2i / semantic retrieval
    ↓
配信可能な候補と最新 metadata
    ↓
LLM reranker / explanation generator

RAG は最新事実を重みに入れずに使える。しかし、「検索が候補を落とせば LLM は選べない」という制約がある。RAG とファインチューニングは競合ではなく、最新知識は RAG、判断様式は SFT / LoRA、個人状態は embedding / context、という役割分担が自然である。

4.3 RLHF と選好学習

SFT は正解出力を模倣する。一方、選好学習では「A と B のどちらが良いか」という比較を使う。推薦では、編集者や評価者の pairwise 選好、説明の自然さ・根拠性、候補リスト全体の好ましさを学習できる。

ただし、一般的な人間の好みと、特定プロフィールが将来視聴する確率は同じではない。人間評価者が「質の高い映画」と感じるものだけを報酬にすると、個人嗜好から離れる可能性がある。報酬を少なくとも、関連性、制約遵守、多様性、説明の根拠性に分けて測るべきである。

手を動かす例: 複数目的の報酬を合成する

一つの推薦リストに対する報酬を、次の加重和とする。

R=0.5Rrelevance+0.2Rconstraint+0.2Rdiversity+0.1RgroundingR =0.5R_{\mathrm{relevance}} +0.2R_{\mathrm{constraint}} +0.2R_{\mathrm{diversity}} +0.1R_{\mathrm{grounding}}

すべての部分報酬は 0 から 1 とする。リスト A は人気アニメばかりで関連性は高いが多様性が低く、次の値だとする。

RA=0.5×0.90+0.2×1.00+0.2×0.20+0.1×0.80=0.77R_A =0.5\times0.90 +0.2\times1.00 +0.2\times0.20 +0.1\times0.80 =0.77

リスト B は関連性が少し低いが、アニメ、SF 映画、国内ドラマを含み、説明も metadata に基づいているとする。

RB=0.5×0.82+0.2×1.00+0.2×0.75+0.1×0.95=0.855R_B =0.5\times0.82 +0.2\times1.00 +0.2\times0.75 +0.1\times0.95 =0.855

この設定なら B が好まれる。ただし、重みを関連性 0.9 に変えれば A が勝つ可能性がある。つまり、RLHF や選好最適化は「正しい目的を自動発見する魔法」ではなく、何を良い推薦と定義したかを強く反映する。重みと部分報酬をプロダクト目標に沿って監査する必要がある。

難所の補足: reward hacking は「指標を改善したが目的を損なう」現象である

報酬を「再生開始されたか」だけにすると、モデルは再生されやすい有名作品、刺激的なサムネイル、過度に断定的な説明を優先するかもしれない。再生開始率は上がっても、数分で離脱する、同じ人気作品ばかりになる、説明への信頼を失うなら、推薦体験全体は改善していない。

これはモデルが悪意を持つからではない。与えられた数値を最大化するという学習目的に忠実だからである。学校で「提出枚数」だけを評価すれば、内容の薄いレポートを大量に出す行動が合理的になるのと同じである。

従って、主目的を一つ決めても、最低視聴時間、候補外生成率、多様性、キッズ安全性、長期利用指標などを guardrail として置く。A/B test でも、平均再生開始率だけで成功判定せず、短時間離脱や特定ジャンルへの露出集中が悪化していないかを確認する。

オンラインの再生を直接報酬にする場合も、クリックベイト的説明や人気作品への集中を招きうる。短期再生だけでなく、視聴時間、満足度 proxy、長期 retention、多様性、安全性を guardrail として監視する必要がある。

5. Hulu で実装する場合の現実的な段階案

段階 0: 評価基盤を先に作る

  • 時系列 split を固定する
  • 候補集合と未来視聴ラベルを保存する
  • Recall@K、NDCG@K、valid-ID rate、latency、費用を計測する
  • キッズ、コールドスタート、デバイス、利用頻度などの slice を定義する

段階 1: LLM を説明生成だけに使う

既存ランカーの順位は変えず、item_information_table と視聴根拠から短い説明を生成する。存在しない事実を混ぜない grounded rate と、説明によるタップ・視聴への影響を評価する。

段階 2: 候補 20〜50 件のオフライン再ランキング

recommend_engine.two_tower_u2i_recommendation_{genre} などの候補を使い、LLM に closed-set ranking をさせる。既存ランカーと同じ候補で比較することで、候補生成改善と再ランキング改善を分離できる。

段階 3: 小規模オンライン A/B test

timeout 時は既存ランカーへ fallback する。p95 latency、費用、schema error、候補外 ID、視聴開始、25% 視聴、総視聴時間を監視する。LLM 群だけでなく、同じ候補・同じ後処理を使った非 LLM baseline と比較する。

段階 4: 十分な失敗データが集まってから指示チューニング

プロンプトで繰り返し発生する失敗、例えば短時間視聴を嫌いと断定する、候補外 ID を出す、説明を長くしすぎる、といった例を hard example として SFT / LoRA データにする。ファインチューニングは、評価可能な失敗パターンを直すために行う。

段階 5: segment adapter は追加価値を検証してから

共通 adapter と動的ユーザー context で不足するかを先に確認する。segment adapter の改善が小さいなら、運用複雑性に見合わない。ユーザーごとの adapter は、削除・更新・serving を含む総コストを明示してから判断する。

6. よくある誤解

「視聴した作品はすべて好みである」

視聴割合、視聴秒数、再視聴、詳細ページ訪問などを分ける必要がある。特に短時間視聴を強い負例にすると、誤タップや再生品質問題まで嗜好として学ぶ。

「未視聴作品は負例である」

未曝露作品は正負不明である。少なくとも impression の有無を区別する。表示位置が低く見られていない作品も、明確な負例とは限らない。

「LLM が候補を直接生成すれば retrieval は不要である」

LLM の内部知識は配信在庫と同期しておらず、架空・未配信・配信終了作品を出しうる。候補検索と ID allowlist は必要である。

「自然言語にすれば前処理が不要になる」

構造化ログを文章へ変える処理そのものが前処理である。時間減衰、証拠強度、ID 結合、欠損処理、token 予算、個人情報の最小化が必要になる。

「説明がもっともらしければ推薦根拠も正しい」

生成説明は事後的にもっともらしい文章を作れる。説明に使える根拠フィールドを限定し、履歴事実と作品 metadata の両方へ対応づけられるかを検証する。

「ファインチューニングすれば最新作品を覚える」

学習時点の作品しか重みに入らない。新作と配信状況は RAG / candidate retrieval で与えるべきである。

「ユーザーごとの LoRA が最も高度な個別化である」

高度であることと本番価値が高いことは同じではない。動的 embedding と直近 context の方が、更新速度、削除可能性、費用で優れる場合が多い。

7. まとめ

4.1〜4.3 の内容を Hulu の暗黙的フィードバック推薦へ落とすと、次のように整理できる。

  1. LLM を推薦器として使う場合も、候補生成、配信制約、ID 検証、fallback は外部システムとして残すべきである。

  2. 入力では、25% 以上視聴、詳細視聴秒数、ページ訪問、impression、タップを同じ「好み」に潰さず、証拠強度と時間を保持する。

  3. プロンプトは、ユーザー説明文ではなく、タスク、候補、制約、schema、棄権条件を含む API 契約として管理する。

  4. オンライン推薦は候補集合から ID を選ばせる closed-set reranking が安全であり、自由な作品名生成は探索的対話に限定する方がよい。

  5. 後処理は Hulu ではオプションではない。配信期間、SVOD / TVOD、キッズ条件、ブラックリスト、重複、トレイ目的を決定論的に検証する。

  6. ファインチューニングは、指示追従、動画配信ドメイン知識、個人嗜好という三つの目的を分ける。最新カタログを覚えさせる手段としては RAG の方が適している。

  7. 大規模な個別化では、まず共通モデルと動的ユーザー embedding / context を採用する。LoRA は共通推薦タスクや、十分なデータを持つ安定 segment から試すのが現実的である。

  8. 評価では推薦指標だけでなく、候補外生成、根拠性、schema 適合、レイテンシ、費用を測り、時系列オフライン評価の後に A/B test で確認する。

要するに、LLM ベース推薦の価値は、すべてを一つの生成モデルへ押し込むことではない。自然言語・作品意味・複雑な条件を統合する LLM の強みを、既存の候補生成、正確なカタログ、決定論的な制約処理、行動評価ループの中に配置することにある。