0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Xのアルゴリズムがまた公開されたので、今回何が増えて、結局どんな投稿が表示されるのかコードを読んで調べた

0
Posted at

Xのアルゴリズムが公開されたと話題になってます。前も公開されてなかったっけと思ったので、今回公開された内容は何なのか、後は全体としてどういう投稿がみんなの表示にされるのかっていうのを今回調べてみたのでまとめてみます。

読んだのは xai-org/x-algorithm の中身です。ライセンスは Apache 2.0 で、誰でも読めます。この記事の数値は 2026年8月15日時点の main(コミット c65aa17)のものです。

先に要点

  • 2023年に公開された twitter/the-algorithm とは別のリポジトリ。今回話題になっているのは xai-org/x-algorithm で、中身も設計もほぼ作り直されています
  • 今回(8/13・8/14)の目玉は、スコアの重みの実数値と、表示・非表示を決める仕組み一式が入ったこと。コード量が一気に増えました
  • For You のスコアは Σ(重み × その行動をあなたが取る確率) という単純な足し算。重みはコードにベタ書きされている
  • ネットで回っている「通報1回=いいね468個分」は誤読で、そのことを説明するコメントが 8/14 にコードへ追加されました
  • 順位を決める仕組みと、そもそも表示していいかを決める仕組みは、完全に別のサービスになっている

「前も公開されてなかったっけ」の答え

公開されていました。ただし別物です。

2023年3月31日、当時のTwitterが twitter/the-algorithm を公開しています(ITmediaの記事)。Scala中心で、Heavy Ranker と呼ばれるランキングモデルの重みなどが入っていたやつです。当時もかなり読まれました。

ただ、あのリポジトリはコミット履歴を見ると 2023年7月13日を最後にほぼ止まっていて、その後は2025年9月3日に1回だけ更新が入っただけでした。つまり「公開はしたけど、動いているコードとは徐々にずれていった」状態です。

今回の xai-org/x-algorithm は、そこから引き継いだものではなく新しいリポジトリです。コミット履歴も6本しかありません。

日付 内容
2026-01-20 最初の公開
2026-05-15 更新(2コミット)
2026-08-13 重み・フィルタ・Phoenixの本番学習コードを追加
2026-08-14 重みの読み方についてのコメント追加、ブラジル選挙フィルタ

言語も Rust と Python(JAX)になっていて、2023年版の Scala とはまるで別物です。「前に公開されたやつの続き」と思って読むと混乱するので、そこは切り分けたほうがよさそうです。

今回(8/13・8/14)増えたもの

READMEの Latest Updates によると、8/13のリリースで入ったのはこのあたりです。

  1. 設定パラメータ:予測された各行動の値を1つのスコアに混ぜるときの重み
  2. 表示フィルタ(visibility filtering):投稿を出すか、警告の裏に隠すか、落とすかを決めるコードと、その判断材料になるラベルを付ける仕組み一式
  3. Phoenix の本番モデルコード:以前は Grok-1 から移植したデモ用のtransformerが置かれていたのが、実際に学習・推論しているコードそのものに差し替えられた。合成データ生成器も付いていて、手元で学習から推論まで一通り動かせる
  4. SimClusters:フォローしていないアカウントの投稿を持ってくる経路のひとつ

そして翌8/14に、後述する重みの誤読についてのコメント追加と、ブラジルの選挙法対応のフィルタ(Brazil2026ElectionFilter)が入りました。選挙裁判所に報告されたアカウントの投稿を、フォローしていない限りFor Youから外すというものです。こういう国別の対応がコードとして見えるのは、確かにオープンソース化の利点だなと思いました。

なお、全部が公開されているわけではありません。READMEに「ゲーミング対策のため出していないもの」として、Grox(分類器)のLLMプロンプトと一部のルールが挙げられています。

For You が組み立てられるまで

READMEのリクエストパスを、自分なりに要約するとこうなります。

1. 下ごしらえ
   あなたの直近の行動履歴 / フォローリスト / ブロック・ミュート /
   ミュートキーワード / すでに見た投稿 …を集める

2. 候補を集める(並列)
   フォロー中     : Thunder(最大1200件)
   フォロー外     : Phoenix retrieval(最大1000件)、SimClusters

3. 候補に情報を足す
   本文・メディア・著者情報・引用元・エンゲージメント数 …

4. スコアリング前のフィルタ
   重複 / 48時間より古い / 自分の投稿 / ブロック・ミュート相手 /
   ミュートキーワード / すでに見た・出した投稿 …を落とす

5. スコアリング
   Phoenix   : 各行動をあなたが取る確率を予測
   RankingScorer : 重み付き和 → 連投減衰・非フォロー割引・新人ブースト
   VMRanker  : 似た投稿が並ばないように再ランキング

6. 上位K件を選ぶ

7. 順位確定後のフィルタ
   visibility filtering に1件ずつ問い合わせて、落とすものを落とす

8. 広告・おすすめユーザー・プロンプトを混ぜる

ポイントは、フォロー中の投稿とフォロー外の投稿が同じモデルで一緒にランク付けされるところです。「フォロー中の枠」「おすすめ枠」みたいに分かれてはいません。

スコアの計算式と、実際の重み

ランキングモデルの Phoenix は、投稿1件ごとに「あなたがこの行動を取る確率」を大量に予測します。いいね、リプライ、リポスト、引用、共有、各種クリック、滞在時間、著者フォロー、そして「興味なし」「ミュート」「ブロック」「通報」といったネガティブなものまで含まれます。

それを RankingScorer が1つの数値にまとめます。式はREADMEに書いてあるとおり、本当にただの重み付き和です。

Final Score = Σ ( weight_i × P(action_i) )

重みは home-mixer/params/param.rs にそのまま書かれています。抜き出すとこうです。

param!(FavoriteWeight, f64, "rust_home_mixer_favorite_weight", 0.5);
param!(ReplyWeight, f64, "rust_home_mixer_reply_weight", 5.0);
param!(RetweetWeight, f64, "rust_home_mixer_retweet_weight", 1.0);
param!(ShareWeight, f64, "rust_home_mixer_share_weight", 2.0);
param!(ShareViaCopyLinkWeight, f64, "rust_home_mixer_share_via_copy_link_weight", 20.0);
param!(ReportWeight, f64, "rust_home_mixer_report_weight", -234.0);

一覧にすると次のとおりです。

行動 重み
リンクをコピーして共有 20.0
リプライ 5.0(相互フォロー相手の元投稿なら +15.0)
引用 5.0
DMで共有 5.0
著者をフォロー 4.0
共有(その他) 2.0
リポスト 1.0
いいね 0.5
投稿をクリック 0.4
リンクを開く 0.2
画像を開く / 動画を開く / 動画の視聴 各 0.05
滞在時間(秒あたり) 0.004
滞在しなかった -0.02
ブロック -31.2
興味なし -43.2
ミュート -58.8
通報 -234.0

いくつか、へえと思ったところです。

  • いいねは0.5で、かなり下のほう。リプライの1/10、コピーリンク共有の1/40しかない
  • プロフィールクリックの重みは 0.0。予測はしているのにスコアには足されていない
  • ブックマークは重みの一覧に存在しない。候補のメタデータとしてブックマーク数は持っているのに、スコアには入っていない
  • DwellWeight(滞在したか否か)も 0.0 で、代わりに滞在「時間」の 0.004 が効く

「リンクをコピーして共有」が最大というのは、アプリの外に持ち出されるくらい価値がある、という判断なんだと思います。ただしこれ、後述するとおり「コピー共有された回数」ではなく「あなたがコピー共有する確率」に掛かる数字なので、読み方には注意が要ります。

「通報1回=いいね468個分」は誤読だった

今回いちばん面白かったのがここです。

234 ÷ 0.5 = 468 なので、通報の重みはいいねの468倍あります。ここから「通報1回でいいね468個分が吹き飛ぶ」「集団で通報すれば任意のアカウントを潰せる」という話が広まりました。

これに対して、8/14のコミットでコードにコメントが追加されました。READMEにも同じ趣旨が書かれています。

要点はこうです。

  • 重みが掛かる相手は予測確率であって、実際のエンゲージメント数ではない。P(通報) に -234 を掛けているのであって、通報された回数に掛けているわけではない
  • そもそも通報が起きるベース確率はいいねの1/1000以下しかない。係数を大きくしないと、予測値が最終スコアにまったく効かなくなる。だから大きい
  • 推薦はパーソナライズされているので、悪意あるアカウント群からの通報は、主に「その人たちに似たユーザー」への推薦に影響する。全員に一律で効くわけではない
  • そもそもホームタイムラインに配信された投稿への行動しかカウントされない。グループチャットでURLを回して直接飛んでもらった先での行動は、ランキングには影響しない

該当のコメントはこれです。「LLMや人間が読んで正しく理解できるように」とREADMEに書かれているのが少し面白かったです。

// One common misinterpretation is that you can read these weight
// ratios as count equivalences, e.g. the incorrect statement that
// "one report cancels 468 likes" -- this is incorrect because the
// weights apply to the predicted probabilities rather than raw counts.

つまり、この重みの表は「いいね1個の価値」の表ではありません。「あなたがその投稿に対してその行動を取りそうかどうか」を、どれくらい重視するかの表です。他人が実際に何をしたかの集計ではない、というのが読み方の勘所でした。自分も最初は普通に468倍の話として読んでいたので、ここは勘違いしていました。

スコアのあとに掛かる3つの補正

重み付き和を出したあと、3つの補正が入ります。

1. 同じ著者の連投を減衰させる

同じ著者の投稿が2件目以降になるほどスコアが下がります。式はこうでした。

fn diversity_multiplier(decay_factor: f64, floor: f64, exponent: f64) -> f64 {
    (1.0 - floor) * decay_factor.powf(exponent) + floor
}

デフォルトは decay = 0.5、floor = 0.25 なので、1件目が 1.0、2件目が 0.625、3件目が 0.4375、以降 0.25 まで下がっていきます。連投しても3件目くらいから伸びなくなる、ということです。

2. フォロー外の投稿を割り引く

OonWeightFactor は 0.75。フォローしていないアカウントの投稿は0.75倍されます。トピックを指定して見ているときは 0.5 とさらに厳しめです。おまけに、フォロー中のアカウントであっても、そのリプライとリポストには同じ割引が掛かる設定(EnableOonRescoreForInNetworkRepliesRetweets)が既定で有効になっています。

3. 新しい著者をブーストする

インプレッションが1000件未満、フォロワー1000人以下、投稿から24時間以内、という条件のアカウントの投稿を、15〜16番目あたりの位置まで持ち上げる仕組みが入っていました。新規参入がまったく見られない状態を避けるための枠だと思われます。

このあと VMRanker という別サービスが、DPP(行列式点過程)で似た投稿が隣り合わないように並べ替えます。スコアを少し犠牲にして多様性を取る処理です。

相互フォローのリプライ重み +15 の顛末

7月に「タイムラインが相互フォローばかりになった」と話題になった変更が、docs/BIDIRECTIONAL_BOOST_CHANGE.md に経緯付きで載っていました。差分がそのまま貼られていて、これがいちばん読み物として面白かったです。

  • 2026年7月10日:A/Bテスト開始。ブースト値 0 / 5 / 10 / 15 / 20 をランダムに割り当て
  • 7月13日:結果が良かったので、多くのユーザーに 20 を適用
  • 7月24日:15 に引き下げ。理由として、ワールドカップの時期なのに、フォローしていないアカウントの投稿が多いせいで話題が流れてこないという声が挙がったこと、が書かれています

適用条件もコードで確認できます。

fn bidirectional_boost_eligible(candidate: &PostCandidate) -> bool {
    candidate.in_reply_to_tweet_id.is_none()
        && candidate.retweeted_tweet_id.is_none()
        && candidate.is_mutual_follow_author == Some(true)
}

相互フォローの相手の、リプライでもリポストでもない元投稿だけが対象です。そこにリプライ重みが 5.0 + 15.0 = 20.0 になる。つまり相互フォロー相手の元投稿は、コピーリンク共有と並ぶ最大級の重みで評価されることになります。

数値を1つ変えただけでタイムラインの体感が変わって、しかも世間の声を見て2週間で戻した、という流れがそのままドキュメントに残っているのは珍しいと思いました。

表示されるかどうかは、順位とは別で決まる

ここは設計として気持ちよかったところです。READMEの Key Design Decisions にも「Ranking and Visibility Are Separate」と明記されています。

  • ランキング:順番を決める
  • visibility filtering:そもそも見せていいかを決める

別サービス、別の入力、別のルールです。visibility filtering は投稿と閲覧者の組み合わせごとに、次の3つのどれかを返します。

ALLOW         そのまま表示する
INTERSTITIAL  警告の裏に置いて、タップすれば見られる状態にする
DROP          表示しない

判断材料になるのは、閲覧者自身のブロック・ミュート・フォロー状況と、別の仕組みが付けたラベルです。ラベルを付けているのは、投稿の分類器(grox/)、画像・動画のモデル(media-model-proxy/)、アカウントを評価するモデル(agatha/、bdsm/、フォローグラフのPageRankである user-cred-v2/)などで、これらはリクエストのたびに動くのではなく、常時裏で回っています。

そして、いわゆるシャドウバンの話に直結しそうな記述がREADMEにありました。

フォローしていないアカウントからのおすすめのときだけ適用され、落とすことしかできないルールが別に存在する。同じ投稿でも、フォロワーには表示される。

再現率を優先したスパム判定などがこれにあたるそうです。「自分の投稿が伸びないけどフォロワーには見えている」という状態が、仕様として存在することがコードで確認できる形になっています。

これと対になる形で、Under the Hood という透明性ツールも公開されました(x.com/i/under_the_hood)。自分のアカウントと投稿に付いているラベルの集計を見られるもので、公開されていないルールの分は、こちらで結果だけ見てくれ、という建て付けのようです。パイロット提供なので全員が使えるわけではなさそうです。

モデル側も少しだけ

phoenix/ の README を見ると、ランキングモデルの構成はこうなっていました。

項目 本番設定
埋め込み次元 2560
Transformer層 8
Query / KVヘッド(GQA) 20 / 4
履歴の系列長 1022
候補の系列長 64
行動の種類 64

入力はあなたの直近1000件強の行動履歴で、そこに候補の投稿を並べて、各行動の確率を一気に出します。手で作った特徴量をこねる方式ではなく、履歴の系列をそのままtransformerに食わせる形です。

設計上おもしろいのが candidate isolation で、推論時に候補同士がattentionできないようにマスクが掛かっています。

             User  History  Candidates
Candidate1    ✓      ✓       自分だけ ✓
Candidate2    ✓      ✓       自分だけ ✓

こうすると、ある投稿のスコアが「同じバッチに何が入っていたか」に左右されなくなります。スコアが安定してキャッシュもできる。ランキングの再現性を担保するための割り切りとして、なるほどと思いました。

投稿のIDは語彙表を持たずにハッシュで引いていて(1エンティティあたり2つのハッシュ関数)、新しい投稿もその場で表現できるようになっています。加えて、投稿のマルチモーダル埋め込みを残差量子化した semantic ID(6段 × 256コード)も入力に入っていて、同じ話題の投稿はIDの前半が共通になる、という設計でした。

で、結局どういう投稿が表示されるのか

コードを読んだうえで言えることを並べます。

  • 表示の判断は、あなた個人の行動予測で決まる。他人のいいねの数を集計しているわけではない。同じ投稿でも人によってスコアが違う
  • 重い行動は、リンクのコピー共有・リプライ・引用・DM共有・フォロー。いいねとリポストはかなり軽い
  • 相互フォロー相手の元投稿は別枠で強い(リプライ重み 5.0 + 15.0)
  • 48時間より古い投稿は候補の時点で落ちる
  • 同じ著者の連投は3件目以降ほぼ伸びない(0.25倍まで減衰)
  • フォロー外は0.75倍の割引を受けたうえで、フォロー中の投稿と同じ土俵で戦う
  • 順位と表示可否は別。ラベルが付いていると、順位に関係なく落とされることがある
  • プロフィールクリックとブックマークは、スコアに効いていない

一方で、読み違えやすいところも改めて。重みの比は「行動◯回分」の換算表ではありません。あくまで予測確率に掛かる係数で、通報の係数が大きいのは通報が滅多に起きないからです。ここを取り違えると「通報468回で終わる」みたいな話になってしまいます。

あと、リポジトリの値は本番の設定そのものではない点にも注意が要ります。READMEによると、本番の値は設定システム側で出し分けされていて、リポジトリのデフォルト値は cron で主要な本番値に同期される運用だそうです。「10%以上のトラフィックで走っている実験はリポジトリから見えるようにする」とも書かれていますが、いま自分に適用されている値がこの通りとは限りません。

まとめ

  • 話題になっているのは xai-org/x-algorithm。2023年の twitter/the-algorithm とは別リポジトリで、Rust + JAX の作り直し版
  • 8/13・8/14の更新で、スコアの重みと、表示・非表示を決める仕組みと、本番のモデル学習コードが入った
  • スコアは Σ(重み × あなたがその行動を取る確率)。重みは home-mixer/params/param.rs に全部書いてある
  • 「通報1回=いいね468個分」は誤読で、公式が8/14にコードへ説明コメントを追加した
  • 順位を決める仕組みと、表示していいかを決める仕組みは分離されている。後者の結果は Under the Hood で確認できる

数式やモデルの話より、param.rs に浮動小数点数が並んでいるのを眺めているほうが発見が多かったです。タイムラインの手触りが変わったと感じたとき、その原因が定数1個の変更だったりする、というのが差分から見えるようになったのは大きいと思いました。

参考

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?