📚 背景知識(読んでから問題へ)
TTAの基本発想(画像コンペでの定番・4ステップ)
問い1 — ノイズTTAは単一予測よりtest精度を改善するか? 閾値と同じく「強すぎ・弱すぎ」のトレードオフがあるか?
問い2 — RandomForestは300本の決定木の多数決=内部にすでに平均化を内蔵している。外側からさらにTTAを重ねる意味はあるか? それとも単に
n_estimatorsを増やす方が安上がりか?| キーワード | 直感的な説明 |
|---|---|
| TTA(Test Time Augmentation) | 学習は1回のまま、予測時にだけ入力を複数バージョンに摂動させ、平均して最終予測とする手法 |
| 摂動(perturbation) | 元データの意味を変えない範囲でわずかに加える変化。今日はガウスノイズを使う |
| 予測の分散(variance) | 同じ「真の」入力に対し、測定誤差やノイズでモデルの予測がどれだけブレるか。TTAはこのブレを平均化で抑えようとする |
🪄 実験デザイン(Day136-137と同じ分割・同じベースモデル)
再学習は一切行わない。ベースモデルはDay136-137と全く同じ(labeled 300件・RandomForestClassifier(n_estimators=300, random_state=0))なので単一予測の精度も同じ0.7967になる。
🎯 問題
Day136-137と同じ合成分類データ・同じ分割方法(ラベル付き300件・プール1500件・test 600件)、同じベースモデルを使います。
| 項目 | 値 |
|---|---|
| タスク | 5クラス分類(make_classification, n_samples=2400, n_features=20) |
| ベースモデル | RandomForestClassifier(n_estimators=300, random_state=0) を labeled 300件で学習 |
| 摂動方法 | ガウスノイズ: X_test + N(0, scale_ratio × feat_std.mean()) |
| スイープ対象 | noise scale比率 [0.05〜1.20] / n_aug [1〜50] / 乱数シード [0〜4] |
| 比較対象 | n_estimators [50, 100, 300, 600, 1000] を単一予測のみで測定 |
import numpy as np from sklearn.datasets import make_classification from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score X, y = make_classification( n_samples=2400, n_features=20, n_informative=12, n_classes=5, n_clusters_per_class=1, random_state=42, flip_y=0.03, ) LABELED_LEN = 300 POOL_LEN = 1500 X_labeled, y_labeled = X[:LABELED_LEN], y[:LABELED_LEN] X_test, y_test = X[LABELED_LEN + POOL_LEN:], y[LABELED_LEN + POOL_LEN:] baseline_model = RandomForestClassifier(n_estimators=300, random_state=0) baseline_model.fit(X_labeled, y_labeled)
💡 ヒント
TTAは「モデルを再学習しない」という点でPseudo Labelingとは根本的に違います。やることは「同じモデルに、同じ意味のデータを複数の見え方で見せて、その予測を平均する」だけです。今日の核心は「効果があるかないか」を確認することそのものよりも、「効果の大きさ」を「乱数によるブレの大きさ」と比べて、本当に意味のある改善と言えるかを判断するところにあります。Day136-137の閾値実験と同じく、1回の実行結果だけを見て即断しないことが重要です。
- ノイズは
rng.normal(loc=0.0, scale=noise_scale, size=X_test.shape)で生成し、X_test + noiseを作る predict_probaは(n_samples, n_classes)の確率配列を返すので、include_original込みで集めた確率配列を単純に+=で足し合わせ、最後に集めた個数で割ればよい- 標準偏差の基準値は
X_labeled.std(axis=0).mean()で計算し、それに倍率を掛けてnoise_scaleとする - fixed/brokenの集計は3つのブールマスクを組み合わせる:
((single_pred != y_test) & (pred_tta == y_test)).sum()
def run_tta(model, X_test, n_aug, noise_scale, rng, include_original=True): proba_sum = np.zeros((X_test.shape[0], model.n_classes_)) n_terms = 0 if include_original: proba_sum += model.predict_proba(X_test) n_terms += 1 for i in range(n_aug): noise = rng.normal(loc=0.0, scale=noise_scale, size=X_test.shape) X_aug = X_test + noise proba_sum += model.predict_proba(X_aug) n_terms += 1 proba_avg = proba_sum / n_terms pred = proba_avg.argmax(axis=1) return pred, proba_avg feat_std = X_labeled.std(axis=0) for scale_ratio in [0.05, 0.10, 0.20, 0.30, 0.50, 0.80, 1.20]: scale = scale_ratio * feat_std.mean() rng = np.random.default_rng(0) pred, _ = run_tta(baseline_model, X_test, n_aug=10, noise_scale=scale, rng=rng) acc = accuracy_score(y_test, pred) print(scale_ratio, acc, acc - single_acc)
✅ 模範解答
import numpy as np from sklearn.datasets import make_classification from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score X, y = make_classification( n_samples=2400, n_features=20, n_informative=12, n_classes=5, n_clusters_per_class=1, random_state=42, flip_y=0.03, ) LABELED_LEN = 300 POOL_LEN = 1500 X_labeled, y_labeled = X[:LABELED_LEN], y[:LABELED_LEN] X_test, y_test = X[LABELED_LEN + POOL_LEN:], y[LABELED_LEN + POOL_LEN:] # --- ステップ1: 単一予測のベースライン --- baseline_model = RandomForestClassifier(n_estimators=300, random_state=0) baseline_model.fit(X_labeled, y_labeled) single_pred = baseline_model.predict(X_test) single_acc = accuracy_score(y_test, single_pred) print(f"single_acc = {single_acc:.4f}") # 実測: single_acc = 0.7967(Day136-137と同一条件なので同じ値) feat_std = X_labeled.std(axis=0) # 実測: feature std mean = 2.0302 # --- ステップ2: TTA関数 --- def run_tta(model, X_test, n_aug, noise_scale, rng, include_original=True): proba_sum = np.zeros((X_test.shape[0], model.n_classes_)) n_terms = 0 if include_original: proba_sum += model.predict_proba(X_test) n_terms += 1 for i in range(n_aug): noise = rng.normal(loc=0.0, scale=noise_scale, size=X_test.shape) X_aug = X_test + noise proba_sum += model.predict_proba(X_aug) n_terms += 1 proba_avg = proba_sum / n_terms pred = proba_avg.argmax(axis=1) return pred, proba_avg # --- ステップ3: ノイズ強度のスイープ(n_aug=10固定) --- for scale_ratio in [0.05, 0.10, 0.20, 0.30, 0.50, 0.80, 1.20]: scale = scale_ratio * feat_std.mean() rng = np.random.default_rng(0) pred, _ = run_tta(baseline_model, X_test, n_aug=10, noise_scale=scale, rng=rng) acc = accuracy_score(y_test, pred) print(f"scale_ratio={scale_ratio:.2f}: tta_acc={acc:.4f} delta={acc-single_acc:+.4f}") # 実測結果: # scale_ratio=0.05: tta_acc=0.7983 delta=+0.0017 # scale_ratio=0.10: tta_acc=0.8033 delta=+0.0067 # scale_ratio=0.20: tta_acc=0.7967 delta=+0.0000 # scale_ratio=0.30: tta_acc=0.8000 delta=+0.0033 # scale_ratio=0.50: tta_acc=0.8017 delta=+0.0050 # scale_ratio=0.80: tta_acc=0.7833 delta=-0.0133 # scale_ratio=1.20: tta_acc=0.7683 delta=-0.0283 # --- ステップ5: 乱数シードによる安定性チェック --- for scale_ratio in [0.10, 0.20, 0.30, 0.50]: scale = scale_ratio * feat_std.mean() accs = [] for seed in range(5): rng = np.random.default_rng(seed) pred, _ = run_tta(baseline_model, X_test, n_aug=10, noise_scale=scale, rng=rng) accs.append(accuracy_score(y_test, pred)) print(f"scale_ratio={scale_ratio:.2f}: mean={np.mean(accs):.4f} std={np.std(accs):.4f} mean_delta={np.mean(accs)-single_acc:+.4f}") # 実測結果: # scale_ratio=0.10: mean_tta_acc=0.8000 std=0.0037 mean_delta=+0.0033 # scale_ratio=0.20: mean_tta_acc=0.8003 std=0.0045 mean_delta=+0.0037 # scale_ratio=0.30: mean_tta_acc=0.8000 std=0.0024 mean_delta=+0.0033 # scale_ratio=0.50: mean_tta_acc=0.7990 std=0.0045 mean_delta=+0.0023 # → deltaの大きさ(+0.002〜+0.004)が、シード間の標準偏差(0.002〜0.005)と同程度かそれ以下 # # 参考: 高ノイズ域は劣化がシード間ブレより明確に大きい # scale_ratio=0.80: mean_tta_acc=0.7883 std=0.0061 mean_delta=-0.0083 # scale_ratio=1.20: mean_tta_acc=0.7683 std=0.0091 mean_delta=-0.0283 # --- ステップ6: 単一予測とTTA予測の食い違い分析 --- rng = np.random.default_rng(0) pred_tta, _ = run_tta(baseline_model, X_test, n_aug=10, noise_scale=0.20 * feat_std.mean(), rng=rng) disagree = (pred_tta != single_pred) fixed = ((single_pred != y_test) & (pred_tta == y_test)).sum() broken = ((single_pred == y_test) & (pred_tta != y_test)).sum() print(f"disagreement: {disagree.sum()} / {len(single_pred)} fixed={fixed} broken={broken}") # 実測: disagreement: 26 / 600 fixed=10 broken=10 # --- ステップ7: n_estimatorsを増やすだけとの比較 --- for n_est in [50, 100, 300, 600, 1000]: m = RandomForestClassifier(n_estimators=n_est, random_state=0) m.fit(X_labeled, y_labeled) acc = accuracy_score(y_test, m.predict(X_test)) print(f"n_estimators={n_est}: acc={acc:.4f}") # 実測結果: # n_estimators= 50: acc=0.7683 # n_estimators= 100: acc=0.7900 # n_estimators= 300: acc=0.7967 (今日のbaseline) # n_estimators= 600: acc=0.8000 # n_estimators=1000: acc=0.8067 # → 300→1000本に増やすだけで+0.0100。TTA最良設定の平均改善(+0.0037前後)より大きい
📊 結果の可視化(deltaの大きさ vs 乱数シードのブレ / n_estimators比較)
delta(平均改善幅)vs シード間の標準偏差(弱ノイズ域では両者がほぼ同じ大きさ = 誤差に埋もれる):
■ 緑=平均delta / 灰=乱数シード間の標準偏差。バーの長さがほぼ同じ=改善幅が誤差の範囲内で「効果あり」と断定できない状態。
| 設定 | test精度 | 単一予測比 |
|---|---|---|
| 単一予測(n_estimators=300) | 0.7967 | baseline |
| TTA最良(scale=0.10, n_aug=10, seed=0) | 0.8033 | +0.0067(誤差の範囲内) |
| TTA平均(5シード, scale=0.20) | 0.8003 | +0.0037(std=0.0045) |
| n_estimators=1000(TTAなし) | 0.8067 | +0.0100 ★最も安定した改善 |
n_estimatorsを単純に増やすだけの改善(+0.0100)はTTAの平均改善より大きく、追加の実装(摂動関数・複数回推論)も不要。| n_aug | TTA精度(scale=0.20) | delta |
|---|---|---|
| 1 | 0.7950 | -0.0017 |
| 3 | 0.7950 | -0.0017 |
| 5 | 0.7967 | 0.0000 |
| 10 | 0.7967 | 0.0000 |
| 20 | 0.7917 | -0.0050 |
| 50 | 0.7983 | +0.0017 |
n_augを増やしても改善幅は単調に伸びず、±0.005程度の範囲で揺れ続ける。「回数を増やせば増やすほど良い」という単純な関係にはなっていない。
🪜 Step-by-Step 解説
1なぜノイズの強さを「絶対値」ではなく「特徴量の標準偏差に対する倍率」で指定するのか
feat_std = X_labeled.std(axis=0) scale = scale_ratio * feat_std.mean()
2なぜ「乱数シードを変えたときのブレ」と比較する必要があるのか
3なぜ「fixed」と「broken」を分けて数える必要があるのか
fixed = ((single_pred != y_test) & (pred_tta == y_test)).sum() # 10件 broken = ((single_pred == y_test) & (pred_tta != y_test)).sum() # 10件
🧮 数学・統計の補足(文系向け)
体重計に1回だけ乗って「62.3kg」と表示されたとします。次の日、同じ体重計に3回連続で乗って平均を取ったら「62.1kg」になりました。この「-0.2kg」という差は、本当に体重が減ったからでしょうか? それとも体重計自体の測定誤差の範囲内に収まる、ただのブレでしょうか? これを判断するには、同じ条件で何度も測り直して「差の大きさ」と「測るたびのブレの大きさ」を比べる必要があります。今日のTTAの実験もこれと同じで、「delta=+0.0037」という数字だけを見て「効果があった」と判断するのではなく、乱数シードを変えたときの標準偏差(0.0045)と比較して初めて、「この差は測定誤差の範囲内かもしれない」と気づくことができます。
クラス40人に「今日の天気は晴れか雨か」を聞いて多数決を取ったとします。すでに40人分の意見を集約した「多数決の結果」に対して、さらに「その40人にもう一度、少し違う聞き方で聞き直して多数決を取り直す」としても、得られる情報はほとんど増えません。すでに集団の意見のばらつきは1回目の多数決でほぼ均されているからです。RandomForestの300本の決定木による多数決も同じで、すでに「複数の視点を平均する」という仕事を内部でやり終えているため、外側から入力にノイズを加えてもう一段階平均化を重ねても、得られる追加の情報はわずかしかありません。
🏆 Kaggleでの実践的な使い方
よく使われるコンペカテゴリ: ☐ 表形式データ(Tabular) / ☑ 自然言語処理(NLP) / ☑ 画像認識(CV) / ☐ 時系列(Time Series)
🖼️ 画像コンペでのTTAは逆に大きく効く
今日の結果とは対照的
画像分類・セグメンテーションコンペでは、CNNやVision Transformerに水平反転・複数スケールクロップ・色調変換などのTTAを組み合わせることで、public LBスコアが着実に0.1〜0.5%改善するケースが多い。単一の決定的な推論経路しか持たない深層学習モデルは、今日のRandomForestと違い予測のブレを内部で吸収する仕組みを持たないため、外側からのTTAが素直に効いてくる。
📝 NLPでのTTA
Attentionの内部平均化との兼ね合い
同義語への置き換え・語順のわずかな入れ替え・トークナイズ方法を変えるなどの摂動を使い、複数の予測を平均する手法が使われる。ただしBERT系モデルも内部にAttentionによる一定の平均化機構を持つため、画像ほど劇的な効果は出ないことが多いと報告されている。
🔧 TTAの費用対効果を事前に見積もる
「内部で何本平均しているか」を先に見る
今日の実験のように、まず「そのモデルは予測時に内部でどれだけ平均化を行っているか」(決定木の本数、アンサンブルの有無など)を考えることで、TTAに計算時間を投資する価値があるかを事前に見積もることができる。木ベースモデルではn_estimatorsを増やす方が同じ計算コストでより安定した改善を得やすい。
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| TTAは「複数回予測して平均する」だけなので、常に精度が上がると思い込む | 「多く測るほど正確になる」という直感をそのまま適用してしまう | 今日の実測では改善幅(+0.002〜+0.004)が乱数シードによるブレ(標準偏差0.002〜0.005)と同程度で、効果があると言い切れなかった。TTAは万能の改善策ではない |
| 1回の実行結果でdeltaがプラスだったら、そのまま「効果がある」と結論づける | 複数シードで再現性を確認する手間を省いてしまう | 乱数シードを変えて複数回測定し、効果の大きさとブレの大きさを比較して初めて、その効果が偶然なのか体系的なものなのかを判断できる |
| ノイズは強ければ強いほど、多様な視点が増えて良いと考える | Pseudo Labelingの閾値と違い「量を増やす」話ではないため直感が働きにくい | 今日の実測では倍率0.80以上でtest精度が明確に悪化した(delta -0.0083〜-0.0283)。摂動が強すぎると元のサンプルとは意味が変わってしまい、逆効果になる |
| どんなモデルに対してもTTAは同じように効くと考える | TTAを画像コンペの文脈だけで学び、モデルの内部構造の違いを意識しないため | RandomForestのように内部で既に多数決(平均化)を行っているモデルでは、外側からのTTAが重複した平均化になり効果が薄い。単一の決定的な推論経路しか持たないモデルほどTTAの効果が出やすい傾向がある |
🚀 次のステップ
- 発展: 今日はガウスノイズによる摂動のみを試しましたが、表形式データには他にも「一部の特徴量をランダムに欠損値にして予測する(feature masking)」「特徴量の値を近傍のサンプルの値と一部入れ替える」といった摂動方法が考えられます。これらの摂動方法がノイズ摂動よりも体系的な改善につながるか、今日と同じ「乱数シードによるブレとの比較」の枠組みで検証してみましょう
- 次回予告: Phase6テーマ3の4日目。今日試したノイズ摂動の代わりに、特徴量ドロップアウト(feature masking)によるTTAを実装し、なぜ・どのような摂動方法であればノイズ摂動より安定した改善につながるのか(あるいはつながらないのか)を比較検証します
Phase 6 の学習マップ(全20テーマ)
Day136で単発Pseudo Labelingを、Day137でIterative Pseudo Labelingを、Day138でノイズ摂動TTAを実装。次はfeature maskingによるTTAの比較に進む。
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: