Claude Media
Claude Codeで論文の手法を再実装し、表の数値と突き合わせる

Claude Codeで論文の手法を再実装し、表の数値と突き合わせる

論文の表の数値を、表示桁から決めた許容誤差つきのテストにして先に置き、Claude Codeに式から再実装させる手順です。合わないときの切り分けの順番と、結果の残し方も扱います。

Claude Codeで論文の手法を再実装し、表の数値と突き合わせる

論文の手法を自分のコードで動かすとき、難所は実装そのものより「合っているのか」の判定です。論文の表には数値が載っているので、それを期待値にしたテストを先に置けば、Claude Codeに実装を任せても、一致したかどうかを出力で確かめられます。この記事は、表の数値を許容誤差つきのテストに落とす方法、合わないときの切り分け、Claude Codeへの指示の置き方、結果の残し方を順に扱います。

論文の表は「丸め幅つきの期待値」としてテストに置く

論文の表の数値は、丸められた値です。0.918と書かれていれば、真の値は0.9175以上0.9185未満のどこかにあります。再実装の出力と0.918をぴったり比べるテストは、正しい実装でも落ちます。

そこで、許容誤差は表示桁から決めます。小数第n位まで表示された値なら、絶対誤差は0.5 * 10**-n以内です。numpy.testing.assert_allcloseはabs(actual - desired) <= atol + rtol * abs(desired)を全要素で判定し、既定はrtol=1e-07、atol=0です。表の値と比べるときはrtol=0にして、atolだけを丸め幅で指定します。

表の表示許容する絶対誤差(atol)
小数第2位(0.92)許容する絶対誤差(atol)0.005
小数第3位(0.918)許容する絶対誤差(atol)0.0005
小数第4位(0.9182)許容する絶対誤差(atol)0.00005

期待値は、論文のどの表のどのセルかが分かる形で置きます。Claude Codeが失敗メッセージを読んだとき、論文のどこと食い違ったかをそのまま報告できるからです。

# tests/test_paper_table.py
import numpy as np
import pytest
from port.ridge import fit_ridge, X_APPENDIX, Y_APPENDIX
 
# 論文 Table 2(係数、小数第3位まで表示)。値は人が論文と見比べて写す
TABLE2 = {
    "lambda=1.0": {"beta": [0.918, 0.990], "decimals": 3},
}
 
 
@pytest.mark.parametrize("case", TABLE2)
def test_table2_coefficients(case):
    row = TABLE2[case]
    lam = float(case.split("=")[1])
    out = fit_ridge(X_APPENDIX, Y_APPENDIX, lam)
    np.testing.assert_allclose(
        out, row["beta"], rtol=0, atol=0.5 * 10 ** -row["decimals"],
        err_msg=f"Table 2 / {case}",
    )

期待値の転記は、人が論文と見比べて行います。Claudeに論文PDFから表を読ませて期待値にすると、転記ミスと実装ミスが同じ側に混ざり、テストが通っても「両方同じ方向に間違えた」可能性が残ります。実装を検証する基準は、実装とは独立に作ります。

PDFの数表を別の資料と照合する手順はClaudeで論文の統計値を生データと照合して検証するにあります。ここでは、期待値が確定した後の再実装を扱います。

式から実装して、最初の不一致を読む

実例として、リッジ回帰を再実装する場面を使います。論文が「説明変数と目的変数を平均で中心化したうえで、(XᵀX + λI)⁻¹Xᵀyを解く」と書き、付録に5行のデータと、λ=1のときの係数を小数第3位で載せているという想定です。論文もデータも架空の例で、数値は手元のNumPy 2.5.4で計算した出力です。

中心化を実装した場合と、切片の列を足して切片にも罰則をかけてしまった場合を比べます。

import numpy as np
 
X = np.array([[1,2],[2,1],[3,5],[4,3],[5,6]], float)
y = np.array([3,3,8,7,11], float)
 
def ridge(X, y, lam, center=True):
    if center:
        Xc = X - X.mean(0); yc = y - y.mean()
        return np.linalg.solve(Xc.T @ Xc + lam * np.eye(2), Xc.T @ yc)
    Xa = np.c_[np.ones(len(X)), X]
    return np.linalg.solve(Xa.T @ Xa + lam * np.eye(3), Xa.T @ y)[1:]

center=Trueの係数は[0.9182, 0.99]で、小数第3位に丸めると表の[0.918, 0.990]と一致し、atol=0.0005のテストを通ります。center=Falseは[0.9459, 1.0]となり、次のように落ちます。

Not equal to tolerance rtol=0, atol=0.0005
 
Mismatched elements: 2 / 2 (100%)
Mismatch at indices:
 [0]: 0.9459459459459453 (ACTUAL), 0.918 (DESIRED)
 [1]: 1.0000000000000002 (ACTUAL), 0.99 (DESIRED)
Max absolute difference among violations: 0.02794595

差は0.028で、丸め幅の0.0005より2桁近く大きい値です。丸めの問題ではなく、式の読み違いだと分かります。「差が丸め幅の内側か外側か」は、不一致の原因を丸めと実装に切り分ける最初の物差しになります。Claude Codeには、失敗の出力をそのまま読ませ、差の大きさから原因を推論させます。

数値が合わないときの切り分けの順番

不一致が出たら、実装を直す前に原因の候補を上から順に潰します。順序は、疑うコストの低い順です。

切り分け

論文の数値と合わないときの確認順

  1. Q差は表示桁の丸め幅(0.5×10のマイナスn乗)より大きいか?
    答えが丸め幅の内側

    内側なら、実装は合っている可能性が高いです。atolが表示桁に合っているか、期待値の桁を写し間違えていないかを見ます。

    答えがはい
  2. Q期待値を論文の表と見比べて、写し間違いや単位違いはないか?
    答えが写し間違いあり

    表の取り違え(別の列、別の条件の行)を直して再実行します。

    答えがはい
  3. Q同じデータ、同じ前処理(欠損の扱い、標準化、中心化)を使っているか?
    答えが前処理が違う

    前処理を式の記述に合わせます。論文が前処理を書いていないなら、候補ごとに試した結果を記録します。

    答えがはい
  4. Q式の読み違い(切片への罰則、割る数、自由度)はないか?
    答えが読み違いあり

    上のリッジの例のように、式の項を1つずつ論文の記述と照らします。標準偏差のddofもここで確かめます。

    答えがはい
  5. Q確率的な手法なら、乱数の種と試行回数は論文と揃っているか?
    答えが揃っていない

    種を固定し、試行回数を論文に合わせます。種が書かれていないなら、複数の種で分布を取って比べます。

    答えがはい

それでも合わないなら

論文に書かれていない条件が差の原因である可能性が残ります。次の節で、その場合の記録の取り方を扱います。

確率的な手法は、1つの値でなく分布で比べます。NumPyの乱数生成器は、種となる整数を与えると同じ列を再現できる決定的な列で、種を与えないdefault_rng()は実行ごとに異なる数を返します。手元でdefault_rng(0)とdefault_rng(1)から3個ずつ正規乱数を取ると、[0.1257, -0.1321, 0.6404]と[0.3456, 0.8216, 0.3304]で、default_rng(0)を作り直すと最初の列がそのまま戻ります。論文が「平均0.82、標準偏差0.03」のように書いているなら、種を10個ほど変えて平均と標準偏差を出し、論文の値が許容範囲に入るかをテストにします。

@pytest.mark.parametrize("seed", range(10))
def test_run_is_reproducible(seed):
    a = run_experiment(seed=seed)
    b = run_experiment(seed=seed)
    assert a == b  # 同じ種なら同じ値(これが崩れたら乱数の使い方を疑う)
 
 
def test_mean_matches_paper():
    scores = [run_experiment(seed=s) for s in range(30)]
    np.testing.assert_allclose(np.mean(scores), 0.82, rtol=0, atol=0.005 + 0.03)

最後の許容幅(丸め幅+論文の標準偏差)は、一例です。標準偏差を許容幅に足してよいかは分野と論文の主張によるので、足す根拠はテストのコメントに残します。

Claude Codeに渡す指示

最初のプロンプトは「テストが落ちること」までを頼む

ベストプラクティスは、Claudeに実行できる確認手段を渡すことを最初の助言に置き、その例にテスト、ビルド、固定データとの差分を取るスクリプトを挙げています。再実装でも、最初の依頼を実装でなくテストにします。

論文 Table 2 の係数を期待値にした pytest を tests/test_paper_table.py に先に書いて。
期待値は TABLE2 に書いてある値を使い、atol は表示桁から 0.5 * 10**-decimals にして。
まだ実装は書かず、port/ridge.py が無いので ImportError で落ちるところまで確認して。
その後、論文の式(3)の記述だけに従って fit_ridge を実装し、pytest を実行して出力を見せて。

式の記述だけに従わせるのは、テストを通すために式から外れた調整を入れさせないためです。ベストプラクティスは、不確かなときは計画段階を挟むことも勧めています。論文の手法が複数のファイルにまたがるなら、プランモード(claude --permission-mode plan)で論文の式と実装する関数の対応表を先に作らせます。差分が一文で説明できる小さな修正なら、計画は省いて構いません。

CLAUDE.mdには「テストを緩めて通さない」を書く

再実装で最も起きやすい失敗は、不一致を許容誤差を広げて消すことです。CLAUDE.mdに、変えてはいけないものを書いておきます。

# 論文再実装のルール
- tests/test_paper_table.py の期待値(TABLE*)は論文の値。勝手に書き換えない
- atol は表示桁から決める(0.5 * 10**-decimals)。通すために広げない
- 不一致は原因を特定してから直す。式の記述にない調整(定数の追加など)を入れない
- 乱数は種を引数で受け取り、default_rng(seed) だけを使う
- 実装を変えたら pytest を実行し、出力を貼る

「通るまで続ける」仕組みを足す

セッションをまたいで放置したいときは、確認手段を終了条件にします。/goalは、条件を満たすまでClaudeが次のターンに進みます。評価役のモデルは、コマンドを自分では実行せず、会話に表れた出力だけで条件を判定します。条件は、Claude自身の出力で示せる形に書く必要があります。

/goal `python -m pytest tests/test_paper_table.py -q` が exit 0 で終わり、
tests/test_paper_table.py が変更されていない。または20ターンで止める

「テストファイルが変更されていない」を入れたのは、制約を書く例にあるとおり、変えてはいけないものを条件に含めるためです。20ターンの打ち切りも、公式が挙げる、実行時間を区切る書き方です。

決定的な関門にしたいなら、Stopフックにテストを走らせます。Stopフックは、終了コード2で終わるとClaudeが止まるのを防ぎ、標準エラー出力を理由として受け取ります。

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check_paper_table.sh"
          }
        ]
      }
    ]
  }
}
#!/bin/bash
# .claude/hooks/check_paper_table.sh
cd "$CLAUDE_PROJECT_DIR" || exit 0
if ! out=$(python -m pytest tests/test_paper_table.py -q 2>&1); then
  echo "$out" | tail -n 30 >&2
  exit 2
fi

Stopフックの連続した継続には上限(連続8回)があります。論文側の事情で原理的に合わない場合でも、無限に回り続けないということです。上限に達した場合に備え、「通った」と「止まった」を取り違えないよう、最後にpytestの出力を確認します。

最後に別の目で見直す

ベストプラクティスは、実装を終えた後に、新しいコンテキストのサブエージェントへ差分を見直させる使い方を載せています。再実装なら、レビューの観点を「論文の式と一致しているか」に絞ります。

サブエージェントで port/ridge.py を論文の式(3)と照らして見直して。
式の項が抜けていないか、論文に無い定数や前処理が入っていないか、
tests の期待値が書き換えられていないかを確認し、正しさに関わる指摘だけ報告して。

公式も、レビュアーは頼まれれば指摘を出すので、すべてに応えると過剰な設計になると注意しています。「正しさに関わる指摘だけ」と指定するのはそのためです。

落とし穴

テストが通っても、論文の主張を再現したとは限らない

表の数値と一致したのは、その表のその条件についてだけです。論文が示した傾向や有意差まで再現したかは別の確認で、別データでの挙動や、論文が触れていない条件は検証の外に残ります。

合わなかった結果も、論文との差として残す

再実装が完全に一致するとは限りません。そのときは「合わなかった」だけで終わらせず、何を試して何が決まらなかったかを表にします。次は、前の節の架空の例に3行目を足した表です(3行目の数値も架空です)。

項目論文の値再実装差判定
Table 2 / β1 (λ=1)論文の値0.918再実装0.9182差0.0002判定丸め幅の内側
Table 2 / β2 (λ=1)論文の値0.990再実装0.9900差0.0000判定丸め幅の内側
Table 3 / 平均スコア論文の値0.82再実装0.79差0.03判定種の分布の外側。前処理の記載が論文にない

最後の行のように、論文に記載がないために決められない条件は、実装の誤りとは別に書き分けます。論文の記載だけでは数値が決まらないという事実は、再実装して初めて分かる成果です。Claude Codeには、テストの結果を読ませて、この表をREPRODUCTION.mdに更新させます。更新のたびに、pytestの出力を表の根拠として貼らせます。

Claude Scienceの監査可能な成果物の仕組みは、実行環境と成果物を残して後から検証できるようにする側の話で、Claude Scienceの再現性の担保にまとまっています。Claude Codeの再実装は、自分のリポジトリにテストと記録を置いて同じ役割を果たす別の方法です。Anthropicが、36本の原稿を作業環境で回した事例はClaude-shaped scienceで読めます。数値の一致を完了の条件に置く点は、この事例と同じ考え方です。

まとめ

論文の再実装は、表の数値を丸め幅つきのテストにして先に置くところで成否が決まります。許容誤差を表示桁から決め、期待値は人が写し、CLAUDE.mdで「テストを緩めて通さない」と書き、/goalかStopフックで通るまで続けさせると、Claude Codeの実装は出力で検証できる状態になります。一致しなかった項目は、丸め幅との差と、論文に記載のない条件を添えて残すと、再現できなかった理由が次の読者に渡ります。

この記事を共有:XはてブLinkedIn