📚 背景知識(読んでから問題へ)
| ノイズ摂動(Day138) | マスキング(今日) | |
|---|---|---|
| やること | 全特徴量に小さなランダムな揺らぎを足す | 一部の特徴量を丸ごと列平均に置き換える |
| 元の情報 | ほぼ保たれる(値が少しズレるだけ) | その特徴量については完全に失われる |
| 直感 | 測定誤差でわずかに違う値が出た | その特徴量だけ欠損していた |
問い1 — マスキングTTAはノイズTTAと同じ「微妙なレベル」にとどまるか? それとも、はっきりした方向の変化が出るか?
問い2 — 違う結果が出るならなぜか? RandomForestが特徴量の閾値で分岐を作る仕組みと関連付けられるか?
問い3 — ノイズとマスキングを同時に使うと良いとこ取りになるか、悪い方に引っ張られるか?
| キーワード | 直感的な説明 |
|---|---|
| 特徴量マスキング(feature masking) | 一部の特徴量の値を意図的に代表値(平均など)に置き換えて「欠損」に近い状態を作る摂動方法 |
| 軸並行分岐(axis-aligned split) | 決定木が「特徴量Xがしきい値tより大きいか小さいか」で分岐を作る仕組み。値を大きく変えると複数の分岐のしきい値を一度にまたぐ可能性がある |
| 効果量 vs ブレの比較 | Day138では「改善」を主張してよいかの判定に使った同じモノサシを、今日は「悪化」を主張してよいかの判定に使う |
🪄 実験デザイン(Day136-138と同じ分割・同じベースモデル)
再学習は一切行わない。ベースモデルはDay136-138と全く同じなので単一予測の精度も同じ0.7967になる。
rng.choice(n_features, size=n_mask, replace=False)でサンプルごとに独立に選ぶ。n_mask = max(1, round(mask_ratio × 20))とすることで、mask_ratioが小さくても最低1列はマスクされる。置換先はX_labeled.mean(axis=0)(列平均)——「その特徴量について何も情報がないときの最も無難な代入値」。🎯 問題
Day136-138と同じ合成分類データ・同じ分割方法(ラベル付き300件・プール1500件・test 600件)、同じベースモデルを使います。
| 項目 | 値 |
|---|---|
| タスク | 5クラス分類(make_classification, n_samples=2400, n_features=20) |
| ベースモデル | RandomForestClassifier(n_estimators=300, random_state=0) を labeled 300件で学習 |
| 摂動方法 | 特徴量マスキング: 各サンプルごとにmask_ratio×20列をrng.choiceで選び、列平均で置換 |
| スイープ対象 | mask_ratio [0.05〜0.50] / n_aug [1〜50] / 乱数シード [0〜4] |
| 比較対象 | Day138のノイズTTA(scale_ratio=0.10) / ノイズ+マスキング併用 |
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)
💡 ヒント
Day138では「delta(改善幅)が乱数シードによる標準偏差より十分大きいか」を、改善を主張してよいかの判定に使いました。今日はその同じモノサシを、悪化を主張してよいかの判定にも使えることに気づくのがポイントです。「効果が誤差に埋もれて分からない」ケースと「効果ははっきり分かるが、望ましくない方向だった」ケースは、統計的な意味では対称です。
- マスクする列は
rng.choice(n_features, size=n_mask, replace=False)でサンプルごとに選ぶ(全サンプル共通の列ではなく、サンプルごとに独立に選ぶ) n_mask = max(1, int(round(mask_ratio * n_features)))で、mask_ratioが小さくても最低1列はマスクされるようにするfill_valuesはX_labeled.mean(axis=0)(学習データの列平均)を使う- 併用実験では、同じX_augに対して「まずノイズを足す→次にその上から一部列をマスクする」の順で両方の操作を1つの拡張サンプルに適用する
def run_tta_mask(model, X_test, n_aug, mask_ratio, fill_values, 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 n_samples, n_features = X_test.shape n_mask = max(1, int(round(mask_ratio * n_features))) for i in range(n_aug): X_aug = X_test.copy() for row in range(n_samples): cols = rng.choice(n_features, size=n_mask, replace=False) X_aug[row, cols] = fill_values[cols] 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_mean = X_labeled.mean(axis=0) for mask_ratio in [0.05, 0.10, 0.20, 0.30, 0.50]: rng = np.random.default_rng(0) pred, _ = run_tta_mask(baseline_model, X_test, n_aug=10, mask_ratio=mask_ratio, fill_values=feat_mean, rng=rng) acc = accuracy_score(y_test, pred) print(mask_ratio, acc, acc - single_acc)
✅ 模範解答
# --- ステップ0: ベースライン(Day136-138と同一) --- 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) # 実測: single_acc = 0.7967 feat_std = X_labeled.std(axis=0) feat_mean = X_labeled.mean(axis=0) # --- ステップ1: マスキングTTA関数 --- def run_tta_mask(model, X_test, n_aug, mask_ratio, fill_values, 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 n_samples, n_features = X_test.shape n_mask = max(1, int(round(mask_ratio * n_features))) for i in range(n_aug): X_aug = X_test.copy() for row in range(n_samples): cols = rng.choice(n_features, size=n_mask, replace=False) X_aug[row, cols] = fill_values[cols] 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 # --- ステップ2: マスク率のスイープ(n_aug=10固定) --- for mask_ratio in [0.05, 0.10, 0.20, 0.30, 0.50]: rng = np.random.default_rng(0) pred, _ = run_tta_mask(baseline_model, X_test, n_aug=10, mask_ratio=mask_ratio, fill_values=feat_mean, rng=rng) acc = accuracy_score(y_test, pred) print(f"mask_ratio={mask_ratio:.2f}: tta_acc={acc:.4f} delta={acc-single_acc:+.4f}") # 実測結果: # mask_ratio=0.05 (n_mask=1): tta_acc=0.7933 delta=-0.0033 # mask_ratio=0.10 (n_mask=2): tta_acc=0.7767 delta=-0.0200 # mask_ratio=0.20 (n_mask=4): tta_acc=0.7600 delta=-0.0367 # mask_ratio=0.30 (n_mask=6): tta_acc=0.7450 delta=-0.0517 # mask_ratio=0.50 (n_mask=10): tta_acc=0.6867 delta=-0.1100 # → マスク率を上げるほど悪化幅が単調に拡大する「量反応関係」がはっきり出ている # --- ステップ4: 乱数シードによる安定性チェック --- for mask_ratio in [0.05, 0.10, 0.20, 0.30]: accs = [] for seed in range(5): rng = np.random.default_rng(seed) pred, _ = run_tta_mask(baseline_model, X_test, n_aug=10, mask_ratio=mask_ratio, fill_values=feat_mean, rng=rng) accs.append(accuracy_score(y_test, pred)) print(f"mask_ratio={mask_ratio:.2f}: mean={np.mean(accs):.4f} std={np.std(accs):.4f} mean_delta={np.mean(accs)-single_acc:+.4f}") # 実測結果: # mask_ratio=0.05: mean_tta_acc=0.7900 std=0.0024 mean_delta=-0.0067 # mask_ratio=0.10: mean_tta_acc=0.7797 std=0.0022 mean_delta=-0.0170 # mask_ratio=0.20: mean_tta_acc=0.7673 std=0.0086 mean_delta=-0.0293 # mask_ratio=0.30: mean_tta_acc=0.7453 std=0.0062 mean_delta=-0.0513 # → mask_ratio=0.05(1列だけマスク)の時点で既にdelta(-0.0067)がstd(0.0024)の約3倍 # 「悪化している」と誤差の範囲を超えて言い切れる # 参考: Day138のノイズTTA(同じ枠組みで再計算) # scale_ratio=0.10: mean_tta_acc=0.8000 std=0.0037 mean_delta=+0.0033 # → ノイズTTAはdeltaとstdが同程度(言い切れない)。マスキングTTAはdeltaがstdの3〜6倍(言い切れる、ただし悪化) # --- ステップ5: ノイズTTAとの直接対決 --- rng_mask = np.random.default_rng(0) pred_mask, _ = run_tta_mask(baseline_model, X_test, n_aug=10, mask_ratio=0.20, fill_values=feat_mean, rng=rng_mask) rng_noise = np.random.default_rng(0) pred_noise, _ = run_tta_noise(baseline_model, X_test, n_aug=10, noise_scale=0.10 * feat_std.mean(), rng=rng_noise) fixed_mask = ((single_pred != y_test) & (pred_mask == y_test)).sum() broken_mask = ((single_pred == y_test) & (pred_mask != y_test)).sum() fixed_noise = ((single_pred != y_test) & (pred_noise == y_test)).sum() broken_noise = ((single_pred == y_test) & (pred_noise != y_test)).sum() agree_rate = (pred_mask == pred_noise).mean() # 実測: # mask: fixed=6 broken=28 (net -22) # noise: fixed=7 broken=3 (net +4) # mask-TTA と noise-TTA の予測一致率: 0.9217 # → マスキングは「壊す」方に大きく偏り、ノイズは「直す」方にわずかに偏る。方向が逆 # --- ステップ6: 併用実験 --- combined_accs = [] for seed in range(5): rng = np.random.default_rng(seed) pred, _ = run_tta_combined(baseline_model, X_test, n_aug=10, noise_scale=0.10 * feat_std.mean(), mask_ratio=0.20, fill_values=feat_mean, rng=rng) combined_accs.append(accuracy_score(y_test, pred)) # 実測: combined(noise 0.10 + mask 0.20): mean_tta_acc=0.7690 std=0.0039 mean_delta=-0.0277 # → noise単体(+0.0037)・mask単体(-0.0293)の単純平均(-0.0128)よりmask単体に近い # 「良いとこ取り」にはならず、マスキングの悪影響がノイズの微小な効果を押しつぶす
📊 結果の可視化(マスク率の量反応関係 と ノイズTTAとの対比)
delta(平均改善/悪化幅)vs シード間の標準偏差(マスキングはdeltaがstdを明確に上回る=「言い切れる」):
■ 赤=|delta|(マスキング・悪化幅) / 緑=|delta|(ノイズ・参考) / 灰=乱数シード間の標準偏差。マスキングは最小強度でも赤バーが灰バーを大きく超え、ノイズは緑と灰がほぼ同じ長さ。
| 設定 | test精度 | 単一予測比 | |delta|/std |
|---|---|---|---|
| 単一予測(n_estimators=300) | 0.7967 | baseline | — |
| ノイズTTA(scale=0.10, 5シード平均) | 0.8000 | +0.0033 | 0.9倍(言い切れない) |
| マスキングTTA(ratio=0.05, 5シード平均) | 0.7900 | -0.0067 | 2.8倍(明確に悪化) |
| マスキングTTA(ratio=0.20, 5シード平均) | 0.7673 | -0.0293 | 3.4倍(明確に悪化) |
| 併用(noise0.10 + mask0.20, 5シード平均) | 0.7690 | -0.0277 | mask単体に近い |
| mask_ratio | マスク列数(/20) | TTA精度(seed=0) | delta |
|---|---|---|---|
| 0.05 | 1 | 0.7933 | -0.0033 |
| 0.10 | 2 | 0.7767 | -0.0200 |
| 0.20 | 4 | 0.7600 | -0.0367 |
| 0.30 | 6 | 0.7450 | -0.0517 |
| 0.50 | 10 | 0.6867 | -0.1100 |
20特徴量中10列(半分)をマスクすると精度は0.6867まで低下する。マスク列数が増えるほど、悪化幅がほぼ単調に拡大する量反応関係。
🪜 Step-by-Step 解説
1なぜマスク率を上げるほど悪化幅が「単調に」拡大するのか
# mask_ratio=0.05: delta=-0.0033 → 0.10: -0.0200 → 0.20: -0.0367 → 0.30: -0.0517 → 0.50: -0.1100
2なぜ同じ「delta vs std」のモノサシで、今回は「言い切れる」と判断できるのか
# mask_ratio=0.05: mean_delta=-0.0067 std=0.0024 → |delta|/std ≈ 2.8
# 参考(Day138) scale_ratio=0.10: mean_delta=+0.0033 std=0.0037 → |delta|/std ≈ 0.9
3なぜ併用実験で「良いとこ取り」にならなかったのか
# noise単体: mean_delta=+0.0037
# mask単体: mean_delta=-0.0293
# combined: mean_delta=-0.0277 (単純平均の-0.0128よりmask単体に近い)
🧮 数学・統計の補足(文系向け)
ある人が地図を見ながら目的地に向かっているとします。「ノイズ」は、地図の縮尺をほんの少しだけ間違って伝えるようなものです——目的地への大まかな方向はほぼ変わらないので、多くの場合ちゃんと着けます。一方「マスキング」は、地図の一部(例えば目印になる交差点の名前)を丸ごと消して「分かりません」に置き換えるようなものです。その交差点で曲がるべきかどうかの判断材料がまるごと無くなるので、間違った方向に進んでしまう危険がずっと大きくなります。RandomForestの決定木も同じで、「この特徴量がこの値を超えているかどうか」で道を選んでいくため、値そのものを丸ごと別の値(列平均という「どこか別の場所」)に置き換えられると、正しい道を選べなくなる可能性が跳ね上がります。
Day138では「体重が62.3kg→62.1kgに変わったが、体重計の測定誤差(±0.2kg程度)の範囲内かもしれない」という状況でした。今日のマスキングは、体重計に乗るたびに「靴を履いたまま測る」という操作を一部の測定に混ぜ込むようなものです。その影響(+1.5kgくらいの誤差)は、体重計本来の測定誤差(±0.2kg)よりずっと大きいので、「これは測定誤差では説明できない、明確な原因がある変化だ」とすぐに分かります。変化の大きさが「測定のブレ」を明確に上回っているかどうかが結論の確信度を決めるという点は、改善でも悪化でも同じです。
🏆 Kaggleでの実践的な使い方
よく使われるコンペカテゴリ: ☐ 表形式データ(Tabular) / ☑ 自然言語処理(NLP) / ☑ 画像認識(CV) / ☐ 時系列(Time Series)
✂️ cutout / random erasingは「学習時」の正則化が定石
推論時に使うと今日のように逆効果になりやすい
今日試したマスキングは画像コンペのcutoutに相当するが、実務では推論時(TTA)ではなく学習時のデータ拡張として使われることが多い。学習中にランダムな領域を隠すことで頑健なモデルを育てる効果があるが、推論時に情報を破壊する形で使うと、今日確認したように逆効果になりやすい。
🔌 欠損値シミュレーションとしての応用
頑健性テストへの転用
特徴量マスキングは、実運用で一部のセンサー値やAPIレスポンスが欠損する状況を模した頑健性テストとして使える。「どの特徴量が欠けたときに予測精度がどれだけ落ちるか」を今日のようなスイープで事前に把握しておくことは、本番運用のリスク管理に直結する。
🧭 「効果の方向」を先に確認する
山型か単調悪化かを早期に見分ける
まず弱い強度で「改善方向か悪化方向か」を確認し、悪化方向であれば強度を上げるスイープを続ける必要はない。Day136・Day138のように弱→中→強で山型になる場合と、今日のように単調に悪化し続ける場合を早い段階で見分けることで、無駄な実験を減らせる。
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| TTAという名前がついている手法は、種類が違っても似たような(小さな)効果しか持たないと思い込む | Day138のノイズTTAの「微差」という結果を、TTA全般の性質だと一般化してしまう | 今日の実測ではマスキングTTAが最小強度でも明確な悪化(delta -0.0067、stdの約3倍)を示した。摂動の「種類」によって効果の大きさも方向もまるで異なりうる |
| 「効果が誤差に埋もれて判断できない」ことと「効果がない」ことを同じだと考える | 「有意差なし」を「差がない」と早合点してしまう典型的な誤り | Day138のノイズTTAは「改善するともしないとも言い切れない」だっただけ。マスキングTTAのように、同じ枠組みでも十分な効果量があれば方向まではっきり判定できる |
| 2つの摂動方法を混ぜれば、良い効果と悪い効果が平均化されて中間くらいの結果になると考える | 「ブレンドすれば中和される」という単純な直感を適用してしまう | 今日の併用実験ではcombined(-0.0277)がnoise単体(+0.0037)とmask単体(-0.0293)の単純平均(-0.0128)よりmask単体に近い結果になった。情報を破壊する操作は非対称に強い影響力を持ちうる |
| マスキング(cutout系)は画像コンペの定番技術だから、表形式データでも推論時に同じように使えば安全だと考える | 手法名だけを見て、適用フェーズ(学習時か推論時か)の違いを見落としてしまう | cutout系の手法は本来「学習時の正則化」として設計されていることが多く、推論時に情報を破壊する形で流用すると明確な悪化を招くことがある |
🚀 次のステップ
- 発展: 今日は列平均での置き換えのみを試しましたが、「学習データからランダムに1件選んでその値で置き換える」「近傍サンプルの値で置き換える」など、他の置き換え方法でも同じように悪化するのか、それとも列平均という「情報量ゼロの代表値」を使ったことが特に悪影響だったのかを切り分けてみましょう
- 次回予告: Phase6テーマ3の5日目(このテーマ群の最終日)。Day136-139で得たPseudo Labeling・ノイズTTA・マスキングTTAの知見を整理し、「どの手法をどんな条件で使うべきか」の判断基準をまとめたうえで、2段階アンサンブルの基礎であるStacking(メタモデルによる予測の統合)に着手します
Phase 6 の学習マップ(全20テーマ)
Day136で単発Pseudo Labelingを、Day137でIterative Pseudo Labelingを、Day138でノイズ摂動TTAを、Day139でFeature Masking TTAを実装。次はテーマ群の総括とStackingに進む。
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: