Skip to content

Repository files navigation

contamlab

LLM ベンチマークの汚染を、検定・信頼区間・検出力・多重比較補正つきで測る。

ベンチマークスコアは「能力」ではなく 「能力 + 記憶」の合計値 である。 この2つを分離するには仮説検定が要る。

依存パッケージなし(Python 3.10+ の標準ライブラリのみ)。

[!note] ★ 何が分かったかを先に読むなら → docs/findings.md(2026-08-27・最終 / ★ 2026-09-05 追記) 11 の発見・⛔ 言えないことの一覧 16 項目・失敗と事故の記録・費用の全体を 1 枚にまとめてある。 ⛔ 本プロジェクトは「あるベンチマークが汚染されている」と結論した実験を 1 つも持たない。 🔴 ★ 2026-08-27 をもって、この線での探索を締めた —— 残っていた 3 つの道 (n を増やす / 摂動を強くする / 印を作る)が、すべて算術で閉じたため(⛔ 合計 $0)。 ★ 「引き継ぐ人へ」の 5 項目が、この報告のいちばん実用的な取り分である。

[!warning] 🔴 2026-08-27: 追っていた「素のベースの +1.4129pt」を降ろしたn を増やす路線は成立しない —— JMMLU は 6,664 問しか無い。 実測の ψ = 0.2285 で、全部使っても見える最小は 1.470pt(Holm×3 なら 1.753pt)。 追いたい +0.9911pt には 14,575〜23,000 問要る。 🔴 現行 n=4,742 での検出力は +1.4129pt で 0.451 / +0.9911pt で 0.232 —— ⛔ detected = False は「落ちていない」証拠ではない。docs/power-wall-2026-08-27.md

🔴 そして +1.4129pt という数字そのものが、装置の床の高さだった —— **b = 492 は Holm×3 の棄却境界とちょうど同じ。**選択を補正した当ては 0.4638pt で、 比較相手の帯 [+0.0422, +0.5061]pt の内側である。 🔴 加えて、同じ重み・同じ Q4_K_M でも GGUF の作り方が違うと未摂動の 7.49% で正誤が変わる (正答率 1.2020pt —— 追っている drop より大きい。⛔ 環境の寄与はゼロだった)。 ★ 買えるのは大きさではなく局在(効果/要求 0.48 → 2.16)。 → docs/redesign-2026-08-27.md

その局在の「印」を課金ゼロで作る道は、2 候補とも閉じた —— JMMLU は 53 科目すべて同日公開で日付の軸が無くllm-jp-corpus-v3(2.1T トークン)には **公開の全文検索索引が無い。**★ 印は m ≥ 259 問ないと使えないという要求仕様が出ている。 → docs/flag-feasibility-2026-08-27.md


なぜ作ったか

既存の汚染研究のほとんどは、この形で終わっている。

「摂動版で正答率が 10% 落ちた。汚染の疑いがある」

検定がない。信頼区間がない。検出力分析がない。 多重比較補正もない。

検出力の話が特に効く。McNemar 検定の必要標本数を計算すると:

$ contamlab power --n 100 --discordant-rate 0.2
問題数        : 100
不一致率 ψ    : 0.200
有意水準      : 0.05(片側)
目標検出力    : 0.80
検出可能な最小: 11.00 ポイント

100問では 11 ポイント未満の汚染は見えない。 5ポイントを見るには 493 問要る。

[!warning] ⚠️ この2つの数字は楽観側にずれている(2026-08-19 の外部照合で判明) power --n 100 が返す 11.00 pt / 493 問は、McNemar の正規近似検定の検出力である。 だが contamlab が判定に使うのは厳密条件付き検定で、そちらは保守的なぶん検出力が落ちる。 実際に測り直すと 11.50 pt / 527 問であった(厳密列挙・乱数なし)。 **正しい向きは「もっと問題が要る」**であって、主張(小標本では汚染は見えない)は弱まらない。 詳細と再現手順: docs/power-verify-2026-08-19.md

にもかかわらず、多くの汚染研究は 100〜200 問で実験し、有意差が出なければ 「汚染は検出されなかった」と結論している。それは「汚染がない」ではなく 「見えるだけの標本がなかった」である。

contamlab は、その設計を実行前に拒否する。狙う効果の大きさに対して問題数が足りなければ、測定を始める前に止まる(以下、この仕組みを「門番」と呼ぶ)。


使い方

# 1. まず検出力。何問必要かを計算してから問題を集める
contamlab power --effect 0.05 --discordant-rate 0.30

# 2. 測定装置の健全性チェック
contamlab verify

# 3. 摂動を目で確かめる(正解が壊れていないこと)
#    同梱の data/example.jsonl は★架空の8問。実在のベンチマークではない
contamlab perturb --benchmark data/example.jsonl --seed dev-seed --limit 3

# 4. 一通り動かす。合成問題なのでデータも API キーも要らない(課金 0 回)
contamlab run --synthetic 1000 --seed dev-seed \
  --target-effect 0.05 --expected-discordant-rate 0.30 --k 1 \
  --model fake:demo:0.6 --json reports/run.json

# 5. ★門番(問題数が足りない実験を実行前に止める仕組み)が働くことを見る。8問では拒否され、exit 2 で止まる
contamlab run --benchmark data/example.jsonl --seed dev-seed \
  --target-effect 0.05 --expected-discordant-rate 0.30 --k 1 --model fake:demo:0.6
#   → 「5.0 ポイントの汚染を検出力 0.80 で見るには 740 問要るが、手元には 6 問しかない」

[!important] 問題インスタンスは同梱していない。 data/ に追跡されているのは目録(jmmlu.manifest.json)と上の架空8問だけである。 本物のベンチマークは手元で組み立てる ——

python tools/ingest_jmmlu.py --out data/jmmlu.jsonl

取得元・commit SHA・各 CSV の sha256・採用/除外の内訳が manifest に入っているので、 **同じ JSONL を誰でも再現できる。**配らない理由は下の「中心にある逆説」を見よ。

実 API を使う

Copy-Item .env.example .env    # ANTHROPIC_API_KEY などを書く

# 1. まず見積もりだけ出す(--yes を付けなければ課金されない)
contamlab run --benchmark data/jmmlu.jsonl --seed dev-seed `
  --target-effect 0.05 --expected-discordant-rate 0.30 `
  --model anthropic:claude:claude-opus-5 `
  --model openai:gpt:gpt-4o `
  --model compat:local:swallow:http://localhost:11434/v1

# 2. 内容を確認してから実行
contamlab run ... --yes --rate-limit 50
安全装置 内容
--yes が無ければ走らない 呼び出し回数の見積もりだけ出して exit 3
呼び出し上限 既定は必要回数ちょうど。超えたら BudgetExceededError で止まる
応答キャッシュ 追記専用 JSONL。同じ問い・同じモデルなら2回目以降は課金ゼロ
レート制限 --rate-limit で毎分の上限を守る
リトライ 429 / 5xx を指数バックオフ。401 は即座に諦める(空回りで上限を食わないため)
温度 0 が既定 非決定的な応答は、汚染でないのに不一致ペアを生んで検出力を食う

API キーは 環境変数から読む。モデル指定文字列に書かない(シェル履歴とログに残る)。

⚠️ --temperature / --max-tokens を変えると測っているものが変わる。 同じモデルでも別の測定になるので、preregister.md に書いてから変えること。

ベンチマークの形式(JSONL・1行1問)

{"id":"q001","question":"水素の元素記号は?","answer":"H","choices":["H","He","Li","O"],"published_at":"2024-05-01","source":"JMMLU"}

published_atas_of(その問題がいつから世に存在するか)。モデルのカットオフと 突き合わせる時点法で使う。不明なものは黙って捨てず、件数を報告する。


判定の仕方

「汚染あり」と言うには、2つを両方満たす必要がある。

条件 何を補正しているか
割引後の効果量下限 adjusted_lcb > 0 標本誤差 + 摂動器を K 種類試したことによる選択バイアス
Holm 補正後 p < α モデルを M 本並べたことによる多重比較

片方だけでは足りない。実際、汚染のないモデルを3本並べただけで、下限のみの判定は 偽陽性を出した(program.md の「前回までの学び」参照)。

K の割引は Bailey & López de Prado の期待最大値による:

E[max] ≈ (1 − γ)·Φ⁻¹(1 − 1/K) + γ·Φ⁻¹(1 − 1/(K·e))
K 5 10 20 30 50 100
E[max]/σ 1.19 1.57 1.90 2.07 2.28 2.53

金融のデフレーテッド・シャープレシオと同じ道具立て。LLM 評価には対応物が存在しない。


差の差(DiD)— 「摂動が難しくなっただけ」を排除する

摂動版で落ちても、それが汚染とは限らない。摂動そのものが問題を難しくしただけかもしれない。

観測 解釈
全モデルが同じだけ落ちた 摂動が難易度を上げた疑い。汚染とは結論できない
一部のモデルだけ落ちた 汚染

モデル間の低下幅の不均一さを Cochran の Q で検定する。対照モデル(汚染されていないことが 分かっているモデル)を用意できなくても判別できる。

⚠️ 限界。「全モデルが一律に落ちた」は「摂動が難しくなった」と「全モデルが同程度に 汚染されている」の両方と整合する。この設計では区別できない。報告に必ず書くこと。


中心にある逆説

汚染検出ツールを公開した瞬間、そのテストセットが汚染される。

公開 → スクレイプ → 次世代モデルの訓練データに入る → 検査が機能しなくなる。

解法は 問題インスタンスを配らず、決定論的生成器とシードを配ること。

公開するもの  : 摂動器のコード + 公開シード(再現用)
配らないもの  : 摂動済みの問題インスタンス

公開シードで誰でも再現できるので科学的再現性は保たれる。一方、完成した問題文が どこにも平置きされないのでスクレイプされにくい。

[!warning] ⚠️ 訂正(2026-08-08)— ラン 03 に非公開シードは使われていない この節はかつて「秘匿するもの: ホールドアウト用シード」と書いていた。それは誤りなので撤回する。

分割(SPLIT_SALT)・抽出(SAMPLE_SALT)・摂動シード(dev-seed)は いずれも本リポジトリに書かれた公開値である(contamlab/benchmark.py:37,42scripts/lib.sh:36)。 したがって HOLDOUT の摂動済み集合は、本リポジトリと原典コミットだけで第三者が完全に再構成できる。

この設計が実際に達成しているのは1点だけ —— 摂動済みの問題文を平文としてリポジトリに置かないこと。 原典(JMMLU)は元から公開されているので、これはスクレイプ耐性の追加であって ホールドアウトの秘匿ではない。

「1構成・1回だけ開封」は、外部からの汚染を防ぐ機構ではなく、著者自身の多重検定を防ぐ規律である。 前者を主張するには、原典を公開しない独自問題集と、実際に秘匿された摂動シードの両方が要る。

摂動は hashlib.sha256 から乱数種を作るので、いつどのマシンで実行しても同じ。 組み込みの hash() は文字列に対してプロセスごとにランダム化されるので使っていない (tests/test_perturb.py に別プロセスでの再現性テストがある)。


構成

contamlab/
  benchmark.py   (1) 問題 + published_at(as_of)。正解は位置ではなく中身で持つ
  perturb.py     (2) 摂動器【ここだけ書き換えてよい】
  runner.py      (3) モデル抽象・採点・追記専用キャッシュ・FakeModel
  clients.py     (3) 実 API(Anthropic / OpenAI 互換)。★ここだけが金を使う
  harness.py     (4) 固定評価系【編集禁止】検出力の門番・多重比較・自己検査
  report.py      結果の整形(点推定を単独で出さない)
  stats/
    distributions.py  chi2 / 二項 / Clopper-Pearson 厳密区間
    mcnemar.py        対応のある二値データの厳密検定
    power.py          Connor (1987) の標本サイズ式(⚠️ 欠陥 P-1 あり → docs/power-verify-2026-08-19.md)
    multiplicity.py   Holm / BH / E[max]-σ 割引
    heterogeneity.py  Cochran の Q(差の差)

program.md      実験指示書(人間が育てる)
preregister.md  事前確約(結果を見る前に書く)
docs/           ランごとの報告
  findings.md           ★ 何が分かったか(外向きの報告)。ここから読む
  NEXT.md               作業ログ(非公開)の案内。本文から参照されているが中身は公開していない
  positive-control-arc.md  陽性対照を自作するまでの 11 ラン(約 $103)
  power-verify-2026-08-19.md  power.py の外部照合と、そこで見つかった欠陥 P-1
  power-wall-2026-08-27.md    ❌ 検出力の帳簿。「n を増やす」路線が JMMLU 内では成立しないこと
  redesign-2026-08-27.md      ★ 問いの立て直し。選択バイアスと、GGUF 変換経路の大きさの実測
  flag-feasibility-2026-08-27.md ❌ 「印」の候補の実現性。課金ゼロの 2 候補が閉じたこと
tools/          検証用スクリプト(⛔ contamlab/stats/ は編集禁止なので、当てる側はここに置く)

これまでのラン

ラン 日付 結果
jmmlu-shuffle-02 2026-08-06 中止
jmmlu-shuffle-03 2026-08-07 完走。汚染ありとは結論しない
陽性対照アーク(11 ラン) 2026-08-08〜17 陽性対照が 1 本手に入った
calibration-curve-01 2026-08-18 停止
detector-firstlight-01 2026-08-21 完走。検出器を実モデルに初めて当てた
perturbation-floor-01 2026-08-24 停止
perturbation-floor-02 2026-08-24 完走。摂動の「床」は一様ではなかった
format-sensitivity-01 2026-08-26 完走。欠陥 G を閉じた

jmmlu-shuffle-02 —— 中止(2026-08-06)

  • 参加条件を満たすモデルが 2 本揃わず、検査を実行していない。
  • 汚染の有無には答えていない。
  • HOLDOUT 1,922 問は未使用。

jmmlu-shuffle-03 —— 完走(2026-08-07)。汚染ありとは結論しない

  • 書式 C・n=1,097・shuffle_choices(K=1)。
  • 検出できる下限は 4.2pt(swallow)/ 5.1pt(13b)で、それを超える低下は無かった。
  • 「汚染なし」ではない。
  • HOLDOUT は開封・消費済み。

陽性対照アーク(11 ラン) —— 2026-08-08〜17・約 $103

  • JMMLU 1,896 問を LoRA で注入し、推論時に λ=0.8 に絞ることで、汚染の効果(差 +13.08 pt)を保ったまま指示追従の破壊を消した。
  • 合格条件 a・b・c を同時に満たす陽性対照が 1 本手に入った。
  • ⛔ ただし手に入ったのは「検出器が反応すべき既知の陽性」であって、検出器を当てた結果ではない。
  • 70-positive-control.sh は 11 ラン全体で**1 度も走らせていない。**HOLDOUT も開けていない。

calibration-curve-01 —— 停止(2026-08-18)・≒$4

  • 汚染率を振って較正曲線を引こうとしたが、総トークンを揃えるための埋め草(日本語 Wikipedia)を 100% 詰めたアームで解釈不能率 64.75%
  • 事前登録の異常検知に該当し、残り 5 アームは学習していない。
  • 曲線は 1 点も引けていない(一次記録は preregister.md)。
  • かつてここに「$28」と書いていたのは誤り(一次記録は ≒$4 = $1.99/h × 約 2h。2026-08-25 に訂正)。

detector-firstlight-01 —— 完走(2026-08-21)・≒$26.5。★ 検出器を実モデルに初めて当てた

  • 仕込んだ汚染アームは捕まえた(低下 +4.4707pt / p_holm 8.61e-11)。
  • ⛔ だが、汚染していないはずの素のベースも鳴った(+1.4129pt / p_holm 0.0146)。
  • n=4,742 では臨界値が ≒1.25pt しかなく、「片側下限 > 0」は汚染の印として働かない。

perturbation-floor-01 —— 停止(2026-08-24)・$3.05

  • パイロットの関門で生存 2/5 本。
  • 脱落はモデルの失敗ではなかった —— 非空の解釈不能 37/37 件が出力書式の雛形のリテラル X の丸写し
  • 残る 1 本は instruct 版でないベースモデルで、150/150 が空応答。

perturbation-floor-02 —— 完走(2026-08-24)・$5.97。★ 摂動の「床」は一様ではなかった

  • 手を加えていない 3 社のモデルの低下は +0.04 / +0.19 / +0.51pt で、3 本とも検出されず
  • df1 の +1.4129pt はその帯の外側にある。
  • 「ベースは汚染されている」とは結論しない。
  • ⚠️ D3(変換経路)は規則としては通過したが、「変換経路は無関係」とは読めない —— 書式が交絡しており(df1 は書式 C・本ランは書式 A)、CI の幅 2.73pt が +0.51pt と +1.41pt の両方を含む(2026-08-25 に訂正。docs/findings.md 発見 6)。

format-sensitivity-01 —— 完走(2026-08-26)・≒$8.6。★ 欠陥 G を閉じた

  • 書式を揃えて測り直しても、pf2 の 3 つの答えは 1 つも変わらなかった。
    • F1 ✅(書式 C の mmngaswallow31-8b の CI [−0.34, +2.32]pt が +1.4129pt を含む)
    • F2 ❌(帯 [+0.04, +0.99]pt の外側)
    • F3 ❌(0/3 本)
  • 「変換経路(findings.md の仮説3)は容疑から外れた」とは読めない —— CI の幅は 2.66pt で、言えるのは「棄却できなかった」まで。
  • 書式 A は pf2 を小数 4 桁まで再現した(別ドライバ・別 CUDA・⛔ 判定には使わない)。
  • 🔴 rakuten-7b64 件/分で他の 9 分の 1 —— ハード期限を 1 時間延ばした(⛔ 停止条件 $15 は不変)。

02 が中止で終わったのは、モデルが「正しい選択肢の記号だけを答えてください」という 出力書式に乗らなかったからである。03 は書式を選び直した。これは 「落ちたモデルが通る条件を、落ちたのを見てから選ぶ」危険と隣り合わせなので、 選定は解釈不能率だけで行い、摂動後の応答を1件も見ず、DEV だけで完結させ、 候補を3つに凍結して合格ゼロなら本番を実行しないと先に宣言してあった。 選ばれたのは C(先頭を「答え: X」に固定)で、03 は規則を1つも書き換えずに全段を通した。

外部依存を持たないのは、統計層が成果物の中心だから。scipy に隠れた実装を信用する 代わりに式をそのまま書き、既知の表値への回帰テストで固定している。

[!warning] ⚠️ ただし、外部の公表値に固定できているのは一部だけである(2026-08-08 に明記)

固定先 モジュール
外部の公表値 distributions.py(カイ二乗分布表・Clopper-Pearson の公表値・Wilson 区間・標準正規分位点)、multiplicity.benjamini_hochberg(Benjamini & Hochberg 1995 の例)、power.py(2026-08-19 に R パッケージ MESS と照合)
⚠️ 自作の手計算値(検証可能だが外部出典なし) distributions.binomial_sf*multiplicity.holmmcnemar.*
外部照合なし heterogeneity.cochran_qmultiplicity.expected_max_of_k

2026-08-19 に power.py の外部照合を実施した(→ docs/power-verify-2026-08-19.md)。 疑っていた式の転記ミスは否定された —— R パッケージ MESS(Connor 1987 の正規近似)の 公表出力と小数第4位まで一致し、128 ケースの格子照合でも差はゼロだった。 代わりに別の欠陥が出た: power.py が検出力を計算しているのは正規近似検定だが、 mcnemar.py が判定に使うのは厳密条件付き検定であり、両者を突き合わせるテストが1本も無い。 看板の設定で検出力 0.80 のはずが実際は 0.774(モンテカルロ 20,000 試行と厳密列挙が一致)。 ⛔ contamlab/stats/ は編集禁止領域なので直さず、欠陥として記録した。

expected_max_of_k の固定先は依然として Bailey & López de Prado の公表値ではなく、 著者自身の別プロジェクトの表である。heterogeneity.cochran_q の外部照合もまだ無い。


やらないこと

  • 尤度ベースの手法(MIA / Min-K% Prob / 交換可能性検定)。Claude・OpenAI が log-prob を返さず、かつ MIA は現実設定でほぼランダムと示されている(Duan et al., 2024)
  • 数値・人名の差し替え摂動。 答えの再計算が必要で、壊れていても正答率が下がるだけ。 静かに壊れる摂動器は汚染検出器として最悪である
  • 拡張思考(extended thinking)を有効にした測定。 使うと測っているものが変わる。 必要なら preregister.md に書いてから有効にする

テスト

pip install -e ".[dev]"
pytest

このリポジトリの履歴について

このリポジトリは、非公開の作業リポジトリから git filter-repo で履歴を濾して作った(2026-09-15)。 コミットの日時と順序は元のままなので、preregister.md が「規則は commit dfe9851 で凍結してから当てた」 と書いている箇所は、凍結のコミットと結果のコミットの前後関係で確かめられる。

濾したもの どうしたか 理由
docs/NEXT.md(作業ログ)と、内部の証跡 3 ファイル すべての時点から除いた。docs/NEXT.md は案内だけを置き直した 運用の記録が大半で、個人を特定しうる情報を含むため
CLAUDE.md すべての時点から除き、規約の節だけを最新のコミットに置き直した 冒頭 400 行ほどが日付ごとの運用の記録だったため
ローカルのパス・借りた GPU の IP・AWS のセキュリティグループ ID 伏せ字、または文書用のアドレス(203.0.113.x)に置き換えた 個人情報とインフラの識別子
初回コミットのテストにあった JMMLU の実問題 1 問と、HOLDOUT 由来の選択肢 4 件 2026-08-08 に差し替えた合成例と同じ文に置き換えた HOLDOUT の問題文を再配布しないため
  • ⚠️ そのため 2026-08-08 のコミット(公開前レビュー)の差分は、合成例を別の合成例に差し替えているようにしか見えない。 コミットメッセージが述べている実問題は、公開履歴のどの時点にも無い。
  • 作業ログにしか触れていなかった 20 コミットは公開履歴に無い。文書とコードのコメントにあるコミット SHA は 公開側の SHA に貼り直した。対応表は docs/commit-map-public.txt
  • ⛔ **規則・数値・判定は 1 文字も変えていない。**コードの変更は、既定値として書かれていた IP(scripts/df1_closeout.py)と リポジトリ URL(scripts/df1_sync.py)の置き換えと、コメント中の SHA の貼り直しだけである。
  • ⛔ git のコミット日時は作成者が自由に設定できる。第三者によるタイムスタンプほどの証拠力は無い。

ライセンス

コード: MIT(LICENSE)。

データは同梱していない。 このリポジトリは JMMLU の問題文を1問も再配布しない (.gitignoredata/*.jsonldata/raw/ を除外する)。追跡しているのは data/jmmlu.manifest.json だけで、中身は取得元 URL・ commit SHA・各 CSV の sha256・採用/除外の内訳という事実の記録である。 tools/ingest_jmmlu.py を走らせれば、各自の手元で原典から同じ JSONL が再現できる。

したがって JMMLU の CC BY-SA 4.0 は本リポジトリのコードに及ばない。 ShareAlike は「データセットの翻案物を配布したとき」に働く条件であり、配布していないので発生しない。

⚠️ あなたが作ったベンチマーク JSONL を配る場合は、原典のライセンスがその配布物に及ぶ。 ——ただし、配らないほうがよい。配った瞬間にそのテストセットは汚染される(上記「中心にある逆説」)。

原典

  • JMMLUhttps://github.com/nlp-waseda/JMMLU (CC BY-SA 4.0) commit 762cbf192c9e4588b574d718f95afd64c96fcbf4
  • JMMLU_NC_ND サブセットは CC BY-NC-ND(改変禁止)なので取り込んでいない。 摂動は改変にあたる(除外の記録は data/jmmlu.manifest.jsonexcluded)

About

LLM ベンチマークの汚染を、検定・信頼区間・検出力・多重比較補正つきで測る。JMMLU での実測と「言えないこと」の記録つき(Python 標準ライブラリのみ)/ Measuring LLM benchmark contamination with hypothesis tests, CIs, power analysis and multiplicity correction (JMMLU).

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages