X運用 · 公開アルゴリズム · 第5回

おすすめができるまでの9ステップ

おすすめは、点数だけでは決まらない。

まず、ざっくりした全体像から。Xのおすすめは、AIが1回で全部決めているのではありません。「候補を集める」→「ふるいにかける」→「AIが点数を付ける」→「上位だけ残す」と、係を順番に通していく工場のラインのような作りです。

そのラインを細かく見ると、9つの工程に分かれています。数字だけ先に出すと、こうです。

候補源の上限を単純に足すと3,000件 → 点数を付けて上位50件 → 表示できるものだけ残して、最大35件。

点数を付ける前に18個のふるいがあり、点数を付けた後にも「上位50件」の関門と表示可否のチェックがある。どこで落ちるかが分かると、伸びない理由も見えてきます。

そして、この記事の芯は1つです。「候補に入る」「順位が高い」「実際に表示される」は、全部別の話だということ。この3つを混ぜて考えると、おすすめは分からなくなります。

あなたの投稿がおすすめに載るまでに、何が起きているか。①Thunder、②Phoenix、③セマンティックID、④SimClustersと、4回かけて部品を1つずつ見てきました。今回は、その部品が「どういう順番でつながっているか」を1本の流れにします。公開コードの、おすすめを組み立てているプログラム(home-mixer)を上から順に読んだ結果です。

全体の流れ(1枚)

おすすめができるまでの全体像
集める → 情報を付ける → ふるう → 予測 → 点数化 → 並べ替え → 上位50件 → 安全チェック → 画面へ

順番はコードに書かれた通りで、次の9ステップです。

  • 1. あなたの情報を集める(17種類) —— 注文者の情報をそろえる
  • 2. 候補を集める(既定で動くのは3つの係) —— 材料を集める
  • 3. 候補に情報を付ける(12種類) —— 材料にラベルを貼る
  • 4. ふるいにかける(18個のフィルタ) —— 条件外をはじく
  • 5. AIが「どう反応するか」を予測する —— 採点係が予測値を出す
  • 6. 重みを掛けて1つの点数にする —— 点数にまとめる
  • 7. 並べ替えモデルにかける —— もう一度並べ直す
  • 8. 上位50件だけを選ぶ —— 一次選抜
  • 9. 表示可否の最終チェック → 最大35件に絞る → 画面を組み立てる —— 出荷前の最終検品

1つずつ、短く見ていきます。

① あなたの情報を集める(17種類)

候補を探す前に、まず「あなた」の情報がそろえられます。コードに並んでいるのは17種類。

  • 行動履歴(順位付け用と、候補探し用の2種類)
  • フォローしている人、購読している人、相互フォローの相手
  • ブロック・ミュートしている人
  • すでに見た投稿(インプレッションの履歴)
  • 最近反応した投稿(いいね・リポスト・返信・ブックマーク・共有・自分の投稿・写真の拡大・動画視聴)
  • フォロー中のトピック、スターターパック
  • 国・言語などの属性、推定された性別、IP、インストール済みのアプリ
集められる情報
根拠のコード(読み飛ばしてOKです)。候補を探す前に集められる17種類の情報
https://github.com/xai-org/x-algorithm/blob/28e414f/home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs#L226-L276

集めただけでは意味がないので、それぞれがこの先のどこで使われるかを並べておきます。ここが分かると、残りの8工程がぐっと読みやすくなります。

【候補集め(②)で使うもの】

  • フォローしている人 → Thunderが「この人たちの投稿」を持ってくる
  • 行動履歴(候補探し用) → Phoenix検索が「あなたに近い投稿」を探す入口になる
  • 最近反応した投稿(9種類) → SimClustersの手がかり。反応した投稿1件ずつが検索の起点になる
  • フォロー中のトピック → トピックを指定したリクエストのときの検索に使う

【ふるい(④)で使うもの】

  • ブロック・ミュートしている人 → その相手の投稿を落とす
  • すでに見た投稿、すでに配信した投稿 → 同じものを二度出さない
  • 購読している人 → 購読者限定の投稿を出してよいかの判定
  • ミュートキーワード → 含む投稿を落とす

【点数付け(⑤⑥)で使うもの】

  • 行動履歴(順位付け用) → Phoenixがあなたの反応を予測するための材料
  • フォローしている人 → フォロー外かどうかの判定。フォロー外なら0.75倍
  • 相互フォローの相手 → 返信の予測に掛ける重みが上乗せされる(公開値では返信の重みに +15.0。滞在時間側は既定0)
  • 国・言語・年齢層・性別・推定性別・インストール済みのアプリ・フォロー中のトピック・スターターパック・IPから割り出した位置 → まとめて「利用者の情報」としてPhoenixに渡されます

最後の1行が、意外と重いところです。投稿の中身だけでなく、あなたの属性や環境も、予測モデルの入力に入っている。IPについては、緯度・経度や地域コードに変換されて渡されます。

なお「すでに見た投稿」の履歴は、ブルームフィルタという「たぶん見た」を高速に判定する仕組みで持っています。完全な一覧を持つより軽い代わりに、誤検知が起こりえます。コードにも誤検知率(`false_positive_rate`)の設定があり、テストでは0.01(1%)が使われています。ここでの誤検知は「見ていない投稿を、見たかもしれないと判定する」方向のことです。

② 候補を集める(既定で動くのは3つの係)

材料を集める工程です。1行でまとめると、こうなります。

Thunder 1,200 + Phoenix 1,000 + SimClusters 800 = 最大3,000件

コードには候補を集める係の枠が7つ並んでいますが、通常の「おすすめ」のリクエストで既定で動くのは、これまでの回で見てきたこの3つです。

  • Thunder: フォロー中の人の投稿。最大1,200件(第1回 → 【第1回のURL】)
  • Phoenix検索: 中身と投稿者で近い投稿。最大1,000件(第2回 → 【第2回のURL】、第3回 → 【第3回のURL】)
  • SimClusters: 反応した投稿と、いいねした人たちの層が近い投稿。最大800件(第4回 → https://x.com/FFBuncho/status/2091314098948686194)

残り4つは、既定でオフか、条件付きです。

  • TweetMixer: 別サービスから候補を受け取る枠。最大800件。既定ではオフ(`enable_tweet_mixer_source` = false)。中身は公開コードの外
  • Phoenix MoE: 別モデルによる候補。最大200件。既定ではオフ
  • Phoenixトピック検索: トピックを指定したリクエストのときだけ動く
  • キャッシュ: 直前のリクエストで残った候補があるときだけ動く
候補集めのコード
根拠のコード。候補源の枠は7つ。既定で動くのはThunder・SimClusters・Phoenixの3つ(TweetMixer・MoEは既定オフ、トピックとキャッシュは条件付き)
https://github.com/xai-org/x-algorithm/blob/28e414f/home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs#L317-L326

運用側がTweetMixerやMoEをオンにしていれば、この3,000件に最大1,000件が加わります(既定値は公開コードの値で、本番で何がオンかは分かりません)。この時点では、まだ「集めただけ」です。

③ 候補に情報を付ける(12種類)

集まった候補1件ごとに、判断材料が付けられます。

  • 投稿の基本データ、引用元、メディア情報、言語
  • 投稿者の情報、あなたをブロックしていないか、購読者限定か
  • エンゲージメント数(いいね・返信などの現在の数)
  • セマンティックID(第3回の「中身の住所」→ 【第3回のURL】)
  • フォロー内かどうか、相互フォローかどうか

順番が大事で、この「情報を付ける」が終わってから、次のふるいにかけられます。情報がそろっていない候補は、次で落ちます。

④ ふるいにかける(18個)

条件外をはじく工程です。ここで、条件に合わない候補が落ちていきます。18個を性質で4つに分けると、こうなります(コードに並んでいる順番は、下のコード画像の通りです)。

【重複と古さ】

  • 同じ投稿が重複しているもの(複数の候補源が同じ投稿を持ってきた場合、1つだけ残す)、基本データが取れなかったもの
  • 48時間より古い投稿
  • リポストの重複(同じ元投稿のリポストが複数あれば、1つだけ残す)

【あなたが見たくないもの・もう見たもの】

  • 自分の投稿
  • すでに見た投稿、すでに配信した投稿(3種類)
  • ミュートキーワードを含む投稿
  • ブロック・ミュートしている相手の投稿

【権限と安全】

  • フォロー外の人のリポスト・返信
  • フォロー外でNSFWのSimClusters候補
  • 購読者限定で、あなたに権限がないもの
  • ブラジル2026年選挙の規制対象(選挙裁判所への届出アカウント)

【条件付きの4つ】

  • 動画を除外する設定のときだけ、動画を落とす
  • トピックを指定したときだけ、トピック外を落とす
  • 新規・復帰ユーザー向けの最低エンゲージメント(既定オフ)
  • 運用実験用の間引き(既定オフ)
ふるいのイラスト
18個のふるい(常に動くのは14個)。落ちた投稿には点数すら付かない
18個のふるい
根拠のコード。18個のフィルタがこの順にかかる
https://github.com/xai-org/x-algorithm/blob/28e414f/home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs#L346-L374

18個のうち、通常の「おすすめ」で常に動くのは14個で、残り4つは条件付きか既定オフです(既定値は公開コードの値で、本番で何がオンかは分かりません)。

注目してほしいのは「48時間」です。一部の候補源では候補集めの段階から48時間の制限がかかっていて、さらに候補をまとめたあとにも共通の年齢フィルタがあります。ここを通れないので、2日より古い投稿は、どのルートからもおすすめに入りません。

ただし、この年齢フィルタが見ているのは「候補として渡ってきた投稿そのもの」の時刻です。リポストは、それ自体が新しい投稿として扱われます。元の投稿が1週間前でも、リポストされたのが1時間前なら、リポストは1時間前の投稿として判定される、ということです。引用も同じく、引用した投稿自身の時刻で判定されます。

⑤ AIが「どう反応するか」を予測する

ここから点数付けです。⑤⑥⑦は続けて起きるので、先に一言でまとめておきます。

⑤ AIが行動ごとの予測値を出す → ⑥ 重みを掛けて1つの点数にまとめる → ⑦ 別のモデルでもう一度並べ直す

まず⑤。残った候補に、Phoenixのモデルが点数を付けます。予測しているのは「この人がこの投稿に、いいねする確率」「返信する確率」「どれくらい長く読むかの予測値」など、行動ごとの予測です。いいねや返信は確率ですが、滞在時間のように「確率」ではない予測値も混ざっています。ここは第2回で詳しく書きました(→ 【第2回のURL】)。

ポイントは、この時点ではまだ「行動ごとの予測値の束」であって、1つの点数ではないことです。

⑥ 重みを掛けて1つの点数にする

予測値の束に重みを掛けて足し、1つの点数にします。ここで効くのが、投稿の中身とは関係のない2つの掛け算です。

  • フォロー外の投稿は0.75倍(トピック指定時は0.5倍)
  • 同じ投稿者の2件目以降は減点(著者多様性)

2つ目は仕組みが面白いので、次の節に分けます。

点数にする
行動ごとの確率に重みを掛けて1つの点数に。フォロー外は×0.75、同じ人の2件目以降は減点

⑥の補足1: 同じ人の投稿が続かない理由(著者多様性)

ひとことで言うと、「同じ人の投稿ばかりが並ばないようにする仕組み」です。

④の「重複」と混同しやすいので、先に区別しておきます。④で落とすのは、同じ投稿・同じ元投稿のリポストという「文字どおりのダブり」です。こちらは、中身が別々の投稿でも、投稿者が同じなら2件目以降の点数を下げる、という話。落とすのではなく、点数を割り引きます。

1回のおすすめの選別の中で、候補を点数順に並べたとき、同じ投稿者の投稿が2件目、3件目と出てくると、順番に点数が下げられます。計算式もそのままコードにあります。

それ以前に同じ投稿者が出た回数を k とすると、倍率 = (1 - 0.25) × 0.5^k + 0.25

著者多様性のコード
根拠のコード。k件目の倍率 = (1 - floor) × decay^k + floor
https://github.com/xai-org/x-algorithm/blob/28e414f/home-mixer/scorers/ranking_scorer.rs#L643-L645

数字にすると:

  • 1件目(k=0): 1.0倍(そのまま)
  • 2件目(k=1): 0.625倍
  • 3件目(k=2): 0.4375倍
  • 4件目(k=3): 0.34倍
  • 以降: 0.25倍に近づく(下限0.25)
著者多様性の減点
同じ投稿者の2件目以降は、0.625倍、0.4375倍…と割り引かれる(下限0.25倍)

つまり、どれだけ強い投稿を並べても、同じアカウントの2件目は約6割、3件目は4割強まで割り引かれます。既定でオンです(`enable_author_diversity` = true)。

ただし、これは「その時のおすすめの選別の中では」の話です。減点されるのは、同じリクエストの候補の中に同じ投稿者の投稿が複数並んだとき。次のリクエストではまた最初から計算されますし、候補に1件しか入っていなければ減点はありません。連投そのものが常に不利になる、という意味ではなく、「同じタイミングで同じ人の投稿が何件も候補に並ぶと、2件目以降は割り引かれる」という作りです。連投したときに「1件しか伸びない」ことがあるのは、これと整合します。

そしてもう1つ、この作りから読めることがあります(ここは解釈です)。減点は「その1回の選別の中」だけなので、読み込み直すと順番待ちが解消されます。1回目に出た投稿は「配信済み」になって次は落ち、代わりに減点されていた2件目が「1件目」扱いに戻る。つまり、読み込むたびに同じ人の投稿が1件ずつ出てくる、ということが起こりえます。

投稿する側から見るとどういう意味になるかは、後半の「だからどうなる」で書きます。

同じように、セマンティックIDの階層ごとに「同じ話題がそれ以前に何件あったか」「前回から何順位離れているか」も計算され、次の並べ替えモデルに渡されます。ただし、著者多様性のような明示的な減点式はコードになく、この情報をモデルがどう使っているかは公開コードからは確定できません。第3回(→ 【第3回のURL】)で見た「中身の住所」が、投稿を探すときだけでなく並べ方の判断材料にも渡されている、というところまでが事実です。

⑥の補足2: 小さいアカウント向けの1枠(コールドスタート)

ひとことで言うと、「小さいアカウント向けに、1枠だけ持ち上げる処理」があります。

点数付けのところに置かれていて、フォロワーが少なく、まだ表示回数も少ない投稿の中から、1件だけ点数を引き上げます。

公開コードの既定値で使われている条件は、次の4つです。

  • 投稿者のフォロワー数が1,000人以下(`cold_start_follower_cap` = 1000)
  • その投稿の表示回数が1,000回未満(`cold_start_impression_threshold` = 1000)
  • 返信でもリポストでもない、単独の投稿
  • もともとの点数の順位が、点数が付いた候補の上位85%以内にいること

4つ目に注意してください。「下位に沈んでいる投稿を救い上げる」仕組みではありません。ある程度の点数をすでに持っている投稿が対象で、下位15%は外されます。

条件に合う候補のうち、いちばん点数の高い1件が選ばれ、その点数が「少なくとも今の16位の候補と同じ水準」まで引き上げられます(`cold_start_slot_min` = 15、`cold_start_slot_max` = 16。0から数えるので16位の点数です)。既定でオンです(`enable_viewer_cold_start_boost` = true)。

コールドスタート枠
根拠のコード。フォロワー1,000人以下・返信でもリポストでもない単独投稿が対象(表示1,000回未満・15〜16番目などの条件は同じファイルの別の場所)
https://github.com/xai-org/x-algorithm/blob/28e414f/home-mixer/scorers/author_cold_start.rs#L132-L138

つまり、単純に下位の投稿を持ち上げるのではなく、「ある程度の点数を持っている小規模アカウントの候補に、1件だけ上位へ入るチャンスを与える」仕組みです。小規模アカウントにとっては数少ない追い風で、④SimClustersの「いいねが集まらないと候補にもなれない」構造とは逆向きに働きます。

なお、引き上げられるのは1回の選別につき1件だけです。

⑦ 並べ替えモデルにかける

ここで当然の疑問が出ます。⑥で点数を付けたのに、なぜその順番のまま使わないのか。

理由は、見ているものの範囲が違うからです。

  • ⑥は、Phoenixの行動予測に重みを掛けて足し、フォロー外の0.75倍や著者多様性といった決められた補正を反映して、1つの点数にする
  • ⑦は、その点数と「同じ投稿者が何件目か」「同じ話題が続いているか」を別のモデルに渡して、もう一度点数を付け直す

⑥の時点でも、著者多様性のように「候補の並びの中で何件目か」を見ている補正はあります。ただしそれは、決められた式で機械的に掛けるもの。⑦は、その結果もひっくるめてモデルが判断し直します。たとえば、点数の高い順に並べたら似た投稿ばかりが上位に固まってしまう、といったことを、式ではなくモデル側で調整できる形です。

点数が付いた候補は、「価値モデル」と呼ばれる別のサービスに渡されます。そのとき一緒に渡されるのが、⑥の補足1で出てきた「同じ投稿者が何件目か」「同じ話題がそれ以前に何件あったか」「前回から何順位離れているか」です。公開コードの既定では、DPP(決定的点過程)という、似たものが重ならないように選ぶ方法が指定されています。

返ってきた点数で、元の点数が置き換えられます。既定でオンです。

まとめると、⑥が「各候補の基本的な評価と、決められた補正」、⑦が「並びの文脈まで含めた再評価」。この2段構えになっています。

⑧ 上位50件だけを選ぶ

ここが、今回いちばんはっきりした数字です。

点数順に並べて、**上から50件**だけが選ばれます(`TOP_K_CANDIDATES_TO_SELECT = 50`)。

上位50件のコード
根拠のコード。TOP_K_CANDIDATES_TO_SELECT = 50、RESULT_SIZE = 35
https://github.com/xai-org/x-algorithm/blob/28e414f/home-mixer/params/config.rs#L17

集めた候補は数千件。そこから50件です。しかも、この先の表示可否のチェックでさらに減ります。

冒頭に書いた芯が、ここで効いてきます。候補に入っても、点数が50位に届かなければ出ません。50位に入っても、表示可否のチェックで落ちれば出ません。「候補に入る」「順位が高い」「実際に表示される」は、それぞれ別の関門です。

⑨ 表示可否のチェック → 最大35件 → 画面へ

選ばれた50件に、表示可否(Visibility Filtering)まわりの情報が付けられ、最後のフィルタが3つかかります。

  • VFフィルタ(安全性のラベルによる除外。番外編で書いた「フォロー外だけ落とす」判定もここ → 【番外編(コピペ&過剰リプ)のURL】)
  • 補助のVFフィルタ
  • 同じ会話の重複除去

【寄り道: そのラベルは、どう付くのか】

ここだけ少し寄り道します。表示可否を決める材料の1つとして重要なのが「ラベル」ですが、そのラベルがそもそもどう付くか、という話です。

VF自身がアカウントの信頼度(Cred)を直接読んでいるわけではありません。Credが効くのは1段手前、ラベル付与などを決めるenforcementの側です。公開コードのルールでは、信頼度が高いアカウント(`cred.is_high` または `cred.score >= 50.0`)は `pagerank_skipped` として処理が打ち切られます。フォロワー数が一定以上の場合も、同じようにこの処理をスキップする項目(`high_follower_count`)があります。

順番が大事です。検知の結果はこの手前で出ていて、ルールの後半は「検知結果にこのラベルが入っていたら、こう処理する」という形。信頼度によるスキップは、その前に置かれています。つまり検知されていても、スキップが先に成立すれば、後ろのラベル付与まで進みません。「Credが高い → 一部の処理で一部のラベルが付きにくい → 結果としてVFで落ちにくくなる場合がある」という、間接的なつながりです。

ただし `score.skip_author_credibility_prechecks` が立っている場合、このスキップは効きません(この但し書きは投稿側のルールにあり、アカウント側のルールにはありません)。どの検知でこれが立つのかは、次回の「エンゲージメント稼ぎへの対策」で扱います。

残った候補は、上から最大35件に切られます(`RESULT_SIZE = 35`)。ここまでが「投稿を選ぶ」処理で、コード上の順番は「点数付け → 上位50件を選ぶ → 表示可否の情報を付ける → 3つのフィルタ → 35件に切る」です。

【切られた分はどうなるのか】

50件のうち35件に入らなかった分や、途中で落ちた候補は、消えてなくなるわけではありません。

  • 実際に最終応答へ返された投稿だけが「配信済み」として記録され、次回以降のふるい(④)で落とされます
  • それ以外の候補は「配信済み」になりません。つまり、次のリクエストでまた候補になれます
  • さらに、点数が付いた候補は最大750件までキャッシュに保存されます(保存は3分間)。次のリクエストがこの3分以内なら、候補集め(②)は動かず、このキャッシュから候補が供給されます

なので「35件に入らなかった投稿が優先される枠」のようなものはありません。ただ、配信済みにはならないので、同じ人が3分以内にもう一度読み込めば、前回作った候補の中から改めて選び直される。3分を過ぎれば、また②の候補集めからやり直しです(そのときも48時間以内なら、また候補になれます)。

【④のふるいと何が違うのか】

ここで「④のふるいと何が違うの?」と思った方へ。どちらも「落とす」処理ですが、見ているものが別です。

  • ④(点数付けの前): その読者に出せる候補かどうか。重複、48時間、既読、ブロック・ミュート、購読の権限など、ほぼ機械的な条件です
  • ⑨(上位50件のあと): その投稿を表示してよいかどうか。スパム、NSFW、暴力的な内容、悪質なURL、「増幅しない」指定など、投稿やアカウントに付いた安全性のラベルで判定します

安全に関わる判定の本体は⑨です。ただし④にも安全寄りのものが2つ混ざっています(フォロー外でNSFWのSimClusters候補、ブラジル選挙の規制対象)。「安全チェックは最後だけ」ではない、というのはその意味です。

なお、この表示可否の判定(Visibility Filtering)は、おすすめを組み立てるプログラムとは別のサービスとして切り出されています。ラベルを引くのに追加の問い合わせが必要なので、数千件の候補全部ではなく、選ばれた50件にだけかけている——という順番になっている、と読めます(ここは解釈です)。

【外側での組み立て】

このあと、外側でタイムラインが組み立てられます。広告は投稿の間に差し込まれ、既定の混ぜ方では投稿と広告の列が35件に切られます。その上に、おすすめユーザー(6番目の位置)などのモジュール(4枠)と、フレーム(最大8枠)が載るので、1回の応答全体の上限はコード上47アイテム(35+4+8)です。

画面の組み立て
上位50件 → 表示可否のチェック3つ → 最大35件 → 広告・おすすめユーザー(6番目)などを混ぜて画面へ

数字で追いかけると

数字で見る絞り込み
集める3,000件(既定の3つ) → 18個のふるい → 点数付け → 上位50件 → 投稿は最大35件(応答全体はモジュール込みで最大47)
  • 集める: 3,000件(既定の3つの上限の単純合計。TweetMixer・MoEがオンなら最大+1,000件)
  • ふるい: 18個のフィルタ(常に動くのは14個)で条件外の候補を除外
  • 点数付け: AIの予測 → 重み付け → 並べ替え
  • 選抜: 上位50件
  • 最終: 投稿は最大35件(投稿+広告の列も35件)。モジュールとフレームを含めた応答全体の上限は47アイテム

各候補源の上限を単純合計した3,000件と比べると、画面に出る投稿は最大35件なので、約1%という規模感です。ただし毎回3,000件が集まるわけではありませんし、重複除去や既読除外も入るので、あくまで上限同士を並べた目安です。

ここが誤解されやすい

【「候補に入った」=「表示される」ではない】

④(https://x.com/FFBuncho/status/2091314098948686194)で書いた「番組のランキング800位以内」も、あくまで候補になれるかどうかの話。そこから18個のフィルタと、上位50件の関門があります。

【フォロワー数だけで単純に加点されるわけではない】

Thunderは「フォロー中の人の投稿を運ぶ」だけで、順位を付けるのはPhoenixの予測です。⑥の重み付けにも「フォロワーが多いほど何点加算」という単純な項目はありません。

ただし、⑦の並べ替えモデルには、投稿者のフォロワー数(`author_followers_count`)が入力として渡されています。そこでどう使われているかは、公開コードからは確定できません。「フォロワー数は順位に一切関係ない」とまでは言えない、というのが正確なところです。

【連投は不利になりうる(その時の選別の中では)】

同じ投稿者の2件目以降には、はっきりした減点があります(0.625倍、0.4375倍…)。ただし効くのは、同じリクエストの候補に同じ人の投稿が複数並んだときだけ。同じ時間帯に何件も出すと、その選別では2件目以降が割り引かれた点数で競うことになります。連投が常に不利、という意味ではありません。

だからどうなる

【読む側】

おすすめに出てくる投稿は、数千件から上位50件に絞られ、さらに表示可否のチェックを通った結果です。同じ人の投稿ばかりが1画面に続かないのは、意図的に組み込まれた減点によるものです。ただし減点は1回の選別の中だけなので、読み込み直すと同じ人の別の投稿が出てくることがあります(解釈)。同じ話題の連続状況も、並べ替えモデルに渡す判断材料として組み込まれています。

【投稿する側】

  • 48時間を過ぎた投稿は、どのルートからも候補になりません
  • 同じ選別に同じ人の投稿が複数並ぶと、2件目以降は割り引かれます。判定の単位は投稿の間隔ではなく、その読者の1回の候補選別です。候補の年齢上限が48時間なので、間隔を空けても同じ選別に2本並ぶことはあります。「間隔を空ければ避けられる」とはコードからは言えません
  • 「候補に入る」までの入口は複数あります(フォロワー経由=Thunder、中身=Phoenix、いいねの層=SimClusters)。どれか1つの候補源から拾われ、そのあと④の18個のフィルタを通れば、点数付けの土俵に乗ります

【連投は損か、という話(ここは解釈です)】

ここまでの部品を組み合わせると、こう読めます。

Thunderは、フォロー中の人の投稿を新着順に最大1,200件持ってきます。つまり連投すると、そのフォロワーの候補プールに、あなたの投稿が複数入る。ただし1回の選別では、多様性の減点があるので2件目以降はかなり不利になります。そして「配信済み」として記録されるのは、実際に出た分だけ。次にその人が読み込むと、出た分は落ちて、残っていた投稿が「1件目」扱いに戻ります。

なので、読者が何度も見に来るほど、連投した分が順番に消化されていく、という形になります。「その1回の選別では不利、でも積んだ分が無駄になるとは限らない」というのが、コードから読める姿です。

ただし前提が2つあります。1つは、最終的に表示まで残れるだけの評価を得ていること。点数の元になっているのは「その読者が各行動を起こす予測値」なので、反応の薄い相手には、候補に入っても表示までは届きません。もう1つは48時間以内であること。2日を過ぎた分は、消化されないまま候補から消えます。

【では、いつも反応してもらえる相手ならどうか】

減点は必ずかかりますが、掛け算です。元の点数が高ければ、減点されても上位に残れます。

減点そのものを打ち消すのに、どれだけ余裕が必要かを逆算するとこうなります。

  • 2件目(×0.625): 1件目と同じ位置に立つには、元の点数が約1.6倍必要
  • 3件目(×0.4375): 約2.3倍
  • 4件目(×0.34): 約2.9倍

ただし、これは「この倍率があれば35件に残る」という意味ではありません。この後に⑦の並べ替えと⑧の上位50件、⑨の表示可否のチェックが入るので、⑥の点数から最終的な35件の境界を直接逆算することはできません。ここで言えるのは「減点を相殺するには、これだけの差が要る」というところまでです。

つまり「連投しても不利にならない」のではなく、「不利を上回る差があれば、同じ画面に2件、3件と並ぶこともある」ということです。

ここで大事なのが、その点数の中身です。⑥の点数の元になっているのは、その読者が各行動を起こす予測値で、材料はあなたの行動履歴。だから効くのは「世間的に人気かどうか」ではなく、「この読者にとって反応しやすいかどうか」です。

  • 有名なアカウントでも、あなたが普段スルーしているなら予測は低く、2件目は落ちます
  • フォロワーが少ないアカウントでも、あなたがいつも反応しているなら予測は高く、2件目も残りえます
  • 相互フォローなら、返信の予測に掛ける重みが上乗せされます(公開値では +15.0)

「アカウントが強い」のではなく、「あなたとその人の関係が強い」。この記事で何度か出てくる「フォロワー数だけでは加点されない」と、同じことを別の角度から言っているだけです。

ただし⑦の並べ替えモデルには投稿者のフォロワー数が渡っているので、「有名であること自体は一切効かない」とまでは言い切れません。ここは公開コードからは確定できないところです。

言い換えると、この構造で効いてくるのは「投稿の本数」そのものではなく、「何度も見に来て、反応してくれる読者がどれだけいるか」です。ここまでの回で見てきた、④SimClustersの「同じ層からのいいねが集まらないと候補にもなれない」とも、向きは同じです。

まとめ

  • おすすめは「係を順番に通すライン」。集める → 情報を付ける → ふるう → 予測 → 点数化 → 並べ替え → 上位50件 → 表示可否のチェック → 35件 → 画面の組み立て
  • 候補集めの入口は、既定で3つ(Thunder 1,200 / Phoenix 1,000 / SimClusters 800。合計3,000件)。コード上はほかに4つの枠があるが、既定オフか条件付き
  • 48時間より古い投稿は、候補にならない
  • フォロー外の投稿には0.75倍。同じ選別の中で同じ投稿者の2件目以降は0.625倍、3件目は0.4375倍(下限0.25倍)。連投が常に不利という意味ではない
  • フォロワー1,000人以下・表示1,000回未満・上位85%以内などの条件に合う候補から1件を選び、既定では16位の点数の水準まで引き上げる「コールドスタート」処理がある(既定オン)
  • 「候補に入る」「順位が高い」「実際に表示される」は、全部別。どれか1つ通っても、次で落ちる
  • ランキングで一次選抜されるのは上位50件。その後、表示可否のチェックを通った投稿が最大35件まで残る(投稿+広告の列も35件)。モジュール込みの応答全体は最大47アイテム

次回は、ここまで何度か触れてきた「エンゲージメント稼ぎへの対策」を予定しています。

参照(クリックすると該当箇所が開きます)

おすすめの組み立て全体(候補源・フィルタ・点数付け・選抜の順番):
https://github.com/xai-org/x-algorithm/blob/28e414f/home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs

利用者の情報がPhoenixに渡される部分(年齢層・性別・推定性別・トピック・スターターパック・IPの位置・インストール済みアプリ):
https://github.com/xai-org/x-algorithm/blob/28e414f/home-mixer/util/phoenix_request.rs#L30-L75

相互フォロー相手への重みの上乗せ(返信 +15.0 / 滞在時間は既定0):
https://github.com/xai-org/x-algorithm/blob/28e414f/home-mixer/params/param.rs#L317-L327

あなたの情報を集める処理(17種類):
https://github.com/xai-org/x-algorithm/blob/28e414f/home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs#L226-L276

候補を集める係(7つの枠、既定で動くのは3つ):
https://github.com/xai-org/x-algorithm/blob/28e414f/home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs#L317-L326

18個のフィルタ:
https://github.com/xai-org/x-algorithm/blob/28e414f/home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs#L346-L374

著者多様性の計算式(0.5の減衰・下限0.25):
https://github.com/xai-org/x-algorithm/blob/28e414f/home-mixer/scorers/ranking_scorer.rs#L643-L645

著者多様性の既定値(decay 0.5 / floor 0.25 / 既定オン):
https://github.com/xai-org/x-algorithm/blob/28e414f/home-mixer/params/param.rs#L223-L239

上位50件の選抜:
https://github.com/xai-org/x-algorithm/blob/28e414f/home-mixer/params/config.rs#L17

投稿の上限35件と、応答全体の上限47(35+4+8):
https://github.com/xai-org/x-algorithm/blob/28e414f/home-mixer/params/config.rs#L17-L22

並べ替えモデル(既定の価値モデルはDPP。同じ投稿者が何件目か・同じ話題の連続も渡される):
https://github.com/xai-org/x-algorithm/blob/28e414f/home-mixer/scorers/vm_ranker.rs

候補のキャッシュ(選ばれた分と選ばれなかった分をまとめて最大750件・3分間):
https://github.com/xai-org/x-algorithm/blob/28e414f/home-mixer/side_effects/redis_post_candidate_cache_side_effect.rs

配信済みとして記録されるのは実際に返した分だけ:
https://github.com/xai-org/x-algorithm/blob/28e414f/home-mixer/side_effects/update_served_history_side_effect.rs

実行順(点数付け → 選抜 → 選抜後の情報付け → 選抜後のフィルタ → 件数で切る):
https://github.com/xai-org/x-algorithm/blob/28e414f/candidate-pipeline/candidate_pipeline.rs#L97-L131

コールドスタート(フォロワー1,000人以下・表示1,000回未満・上位85%以内・16位の水準へ):
https://github.com/xai-org/x-algorithm/blob/28e414f/home-mixer/scorers/author_cold_start.rs

コールドスタートの既定値(follower_cap 1000 / impression_threshold 1000 / slot 15-16 / 上位85% / 既定オン):
https://github.com/xai-org/x-algorithm/blob/28e414f/home-mixer/params/param.rs#L653-L695

並べ替えモデルに渡される投稿者のフォロワー数:
https://github.com/xai-org/x-algorithm/blob/28e414f/home-mixer/scorers/vm_ranker.rs#L188

年齢フィルタ(候補そのもののIDで判定。リポストはリポスト自身の時刻):
https://github.com/xai-org/x-algorithm/blob/28e414f/home-mixer/filters/age_filter.rs

投稿の年齢上限48時間:
https://github.com/xai-org/x-algorithm/blob/28e414f/home-mixer/params/config.rs#L36

各候補源の上限と既定のオン/オフ(Thunder 1,200 / Phoenix 1,000 / TweetMixer 800・既定オフ / MoE 200・既定オフ):
https://github.com/xai-org/x-algorithm/blob/28e414f/home-mixer/params/param.rs#L1-L52

https://github.com/xai-org/x-algorithm/blob/28e414f/home-mixer/params/param.rs#L135-L162:

信頼度による審査スキップ(pagerank_skipped / high_follower_count):
https://github.com/xai-org/x-algorithm/blob/28e414f/abuse-enforcement-service/service-lib/rules/enforcement_post.yaml

https://github.com/xai-org/x-algorithm/blob/28e414f/abuse-enforcement-service/service-lib/rules/enforcement_user.yaml:

最後の安全チェックと画面の組み立て:
https://github.com/xai-org/x-algorithm/blob/28e414f/home-mixer/candidate_pipeline/for_you_candidate_pipeline.rs

この記事のもとになったX記事

Xで公開した第5回の記事を、サイト用に読みやすく再構成したものです。

前の回:SimClustersと投稿の位置 次の回:インデックスと最初の1いいね ブログ一覧へ