📚 背景知識(読んでから問題へ)
Iterative Pseudo Labelingの5ステップ(繰り返し版)
問い1 — ラウンドを重ねると、どこかの時点でtest精度がベースラインを下回る「境目のラウンド」が現れるか?
問い2 — confirmation biasは「疑似ラベルの正解率が低いラウンドでだけ」起きるのか? それとも疑似ラベルの正解率が100%でもtest精度が落ちることがあるのか?
| キーワード | 直感的な説明 |
|---|---|
| Iterative Pseudo Labeling | 学習→予測→採用→再学習、を複数ラウンド繰り返す半教師あり学習の手法 |
| ラウンドローカル正解率 | そのラウンドで新たに採用したサンプルだけの疑似ラベル正解率 |
| 累積正解率 | これまでの全ラウンドで採用してきたサンプル全体の疑似ラベル正解率 |
| selection bias(サンプル選択バイアス) | ラベルの正しさではなく「どのサンプルが選ばれやすいか」という選び方自体に起因する偏り。今日新しく登場する落とし穴 |
🪄 実験デザイン(Day136と同じ2400件分割 + 反復ループ)
プールから採用したサンプルは除外され、二度と重複採用されない。累積で学習データが300件→最大1500件まで増えうる。
predict_probaの分布を確認してから閾値を決める必要がある。今日は実測に基づきTHRESHOLD=0.60で反復実験を行う。🎯 問題
Day136と同じ合成分類データ・同じ分割方法(ラベル付き300件・プール1500件・test 600件)を使います。
| 項目 | 値 |
|---|---|
| タスク | 5クラス分類(make_classification, n_samples=2400, n_features=20) |
| labeled / pool / test | 先頭300件 / 次の1500件 / 残り600件 |
| ベースモデル | RandomForestClassifier(n_estimators=300, random_state=0) |
| 反復設定 | THRESHOLD = 0.60(環境に応じて調整) / N_ROUNDS = 10 |
| 評価軸 | 採用件数 / プール残数 / ラウンドローカル正解率 / 累積正解率 / test精度 / ベースライン比 |
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_pool_full = X[LABELED_LEN:LABELED_LEN + POOL_LEN] y_pool_true_full = y[LABELED_LEN:LABELED_LEN + POOL_LEN] # 検証専用 X_test, y_test = X[LABELED_LEN + POOL_LEN:], y[LABELED_LEN + POOL_LEN:]
💡 ヒント
Day136は「1回」の実験でしたが、今日は「同じ実験をN_ROUNDS回、状態を持ち越しながら繰り返す」だけです。ループの中身自体はDay136とほとんど同じですが、2点だけ新しく気をつける必要があります。1つ目は「すでに採用したサンプルをプールから除外し、二度と同じサンプルを採用しないこと」。2つ目は「ラウンドごとの正解率」と「これまでの累積の正解率」を分けて記録すること(両者が乖離し始めたら、直近のラウンドで質が急落しているサインになります)。
- ループの外で「残っているプールのインデックス」を
remaining_idx = np.arange(POOL_LEN)のように持ち、採用するたびにremaining_idx = remaining_idx[~mask]で更新する - 学習データは
X_train_cur・y_train_curという変数に持たせ、採用のたびにnp.vstack・np.concatenateで追記していく - 累積正解率は、これまで採用してきた(y_true, y_pred)のペアをリストに貯めて
np.concatenateしてからaccuracy_scoreを呼ぶと計算しやすい - タスク5では「ラウンド1だけ」を切り出して、閾値とrandom_stateを二重ループで回せばよい(Iterative Pseudo Labeling全体を再実行する必要はない)
THRESHOLD = 0.60 N_ROUNDS = 10 X_train_cur, y_train_cur = X_labeled.copy(), y_labeled.copy() remaining_idx = np.arange(POOL_LEN) rows = [] cum_true, cum_pred = [], [] for r in range(1, N_ROUNDS + 1): model = RandomForestClassifier(n_estimators=300, random_state=0) model.fit(X_train_cur, y_train_cur) if len(remaining_idx) == 0: break X_rem = X_pool_full[remaining_idx] y_rem_true = y_pool_true_full[remaining_idx] proba = model.predict_proba(X_rem) conf, pred = proba.max(axis=1), proba.argmax(axis=1) mask = conf >= THRESHOLD if mask.sum() == 0: break round_pseudo_acc = accuracy_score(y_rem_true[mask], pred[mask]) cum_true.append(y_rem_true[mask]); cum_pred.append(pred[mask]) cum_pseudo_acc = accuracy_score(np.concatenate(cum_true), np.concatenate(cum_pred)) X_train_cur = np.vstack([X_train_cur, X_rem[mask]]) y_train_cur = np.concatenate([y_train_cur, pred[mask]]) remaining_idx = remaining_idx[~mask] updated_model = RandomForestClassifier(n_estimators=300, random_state=0) updated_model.fit(X_train_cur, y_train_cur) test_acc = accuracy_score(y_test, updated_model.predict(X_test)) rows.append((r, int(mask.sum()), len(remaining_idx), round_pseudo_acc, cum_pseudo_acc, test_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_pool_full = X[LABELED_LEN:LABELED_LEN + POOL_LEN] y_pool_true_full = y[LABELED_LEN:LABELED_LEN + POOL_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) baseline_acc = accuracy_score(y_test, baseline_model.predict(X_test)) print(f"baseline_acc = {baseline_acc:.4f}") # 実測: baseline_acc = 0.7967 # --- ステップ3: 確信度分布の確認(閾値0.90は使えるか?) --- pool_conf_check = baseline_model.predict_proba(X_pool_full).max(axis=1) print(f"確信度: mean={pool_conf_check.mean():.4f} min={pool_conf_check.min():.4f} max={pool_conf_check.max():.4f}") # 実測: mean=0.4944 min=0.2167 max=0.8900 # → 確信度の最大値が0.89までしか届かず、0.90以上のサンプルが1件も存在しない # → THRESHOLD=0.60に調整する # --- ステップ2: Iterative Pseudo Labeling --- THRESHOLD = 0.60 N_ROUNDS = 10 X_train_cur, y_train_cur = X_labeled.copy(), y_labeled.copy() remaining_idx = np.arange(POOL_LEN) rows = [] cum_true, cum_pred = [], [] for r in range(1, N_ROUNDS + 1): model = RandomForestClassifier(n_estimators=300, random_state=0) model.fit(X_train_cur, y_train_cur) if len(remaining_idx) == 0: break X_rem = X_pool_full[remaining_idx] y_rem_true = y_pool_true_full[remaining_idx] proba = model.predict_proba(X_rem) conf, pred = proba.max(axis=1), proba.argmax(axis=1) mask = conf >= THRESHOLD n_adopted = int(mask.sum()) if n_adopted == 0: break round_pseudo_acc = accuracy_score(y_rem_true[mask], pred[mask]) cum_true.append(y_rem_true[mask]); cum_pred.append(pred[mask]) cum_pseudo_acc = accuracy_score(np.concatenate(cum_true), np.concatenate(cum_pred)) X_train_cur = np.vstack([X_train_cur, X_rem[mask]]) y_train_cur = np.concatenate([y_train_cur, pred[mask]]) remaining_idx = remaining_idx[~mask] updated_model = RandomForestClassifier(n_estimators=300, random_state=0) updated_model.fit(X_train_cur, y_train_cur) test_acc = accuracy_score(y_test, updated_model.predict(X_test)) rows.append((r, n_adopted, len(remaining_idx), round_pseudo_acc, cum_pseudo_acc, test_acc)) # 実測結果(THRESHOLD=0.60, random_state=0固定): # round adopted pool_left round_acc cum_acc test_acc delta # 1 343 1157 0.9504 0.9504 0.7633 -0.0333 # 2 255 902 0.9020 0.9298 0.7517 -0.0450 # 3 135 767 0.8000 0.9059 0.7617 -0.0350 # 4 70 697 0.7714 0.8941 0.7600 -0.0367 # 5 50 647 0.7600 0.8863 0.7500 -0.0467 # 6 18 629 0.8333 0.8852 0.7517 -0.0450 # 7 21 608 0.4762 0.8756 0.7517 -0.0450 # 8 14 594 0.5714 0.8709 0.7550 -0.0417 # 9 13 581 0.3846 0.8640 0.7517 -0.0450 # 10 8 573 0.6250 0.8619 0.7467 -0.0500 # 学習データ件数の推移: 300件 → 1227件 # --- ステップ5: 「疑似ラベル100%正解でもtest精度は落ちるか」のミニ実験 --- for th in [0.85, 0.80, 0.75, 0.70]: deltas, pseudo_accs = [], [] for seed in range(5): base = RandomForestClassifier(n_estimators=300, random_state=seed) base.fit(X_labeled, y_labeled) base_acc = accuracy_score(y_test, base.predict(X_test)) proba = base.predict_proba(X_pool_full) conf, pred = proba.max(axis=1), proba.argmax(axis=1) mask = conf >= th if mask.sum() == 0: continue pseudo_acc = accuracy_score(y_pool_true_full[mask], pred[mask]) X_aug = np.vstack([X_labeled, X_pool_full[mask]]) y_aug = np.concatenate([y_labeled, pred[mask]]) aug = RandomForestClassifier(n_estimators=300, random_state=seed) aug.fit(X_aug, y_aug) aug_acc = accuracy_score(y_test, aug.predict(X_test)) deltas.append(aug_acc - base_acc) pseudo_accs.append(pseudo_acc) print(f"threshold={th}: mean_pseudo_acc={np.mean(pseudo_accs):.4f} mean_delta={np.mean(deltas):+.4f}") # 実測結果(5 random_state平均): # threshold=0.85: mean_pseudo_acc=1.0000 mean_delta=-0.0100 # threshold=0.80: mean_pseudo_acc=1.0000 mean_delta=-0.0120 # threshold=0.75: mean_pseudo_acc=0.9948 mean_delta=-0.0120 # threshold=0.70: mean_pseudo_acc=0.9750 mean_delta=-0.0187
📊 結果の可視化(10ラウンドの推移 と 100%正解でも落ちる delta)
ラウンドローカル正解率(急落)vs 累積正解率(緩やかに低下・過去の高得点に引きずられて劣化が見えにくい):
■ 赤/黄=ラウンドローカル(直近だけ) / 青=累積(過去含む全体)。R9ではローカル正解率が38.5%まで落ちているのに、累積で見るとまだ86.4%あるように「見えてしまう」。
| 閾値 | 疑似ラベル正解率(平均) | test精度delta(平均) |
|---|---|---|
| 0.85 | 100.0% | -0.0100 |
| 0.80 | 100.0% | -0.0120 |
| 0.75 | 99.5% | -0.0120 |
| 0.70 | 97.5% | -0.0187 |
| ラウンド | 採用件数 | プール残数 | ローカル正解率 | 累積正解率 | test精度 | delta |
|---|---|---|---|---|---|---|
| 1 | 343 | 1157 | 95.0% | 95.0% | 0.7633 | -0.0333 |
| 2 | 255 | 902 | 90.2% | 93.0% | 0.7517 | -0.0450 |
| 3 | 135 | 767 | 80.0% | 90.6% | 0.7617 | -0.0350 |
| 4 | 70 | 697 | 77.1% | 89.4% | 0.7600 | -0.0367 |
| 5 | 50 | 647 | 76.0% | 88.6% | 0.7500 | -0.0467 |
| 6 | 18 | 629 | 83.3% | 88.5% | 0.7517 | -0.0450 |
| 7 | 21 | 608 | 47.6% | 87.6% | 0.7517 | -0.0450 |
| 8 | 14 | 594 | 57.1% | 87.1% | 0.7550 | -0.0417 |
| 9 | 13 | 581 | 38.5% | 86.4% | 0.7517 | -0.0450 |
| 10 | 8 | 573 | 62.5% | 86.2% | 0.7467 | -0.0500 |
🪜 Step-by-Step 解説
1なぜ「まだ採用していないプール」だけを毎回予測するのか
2なぜラウンドローカル正解率と累積正解率を両方記録するのか
round_pseudo_acc = accuracy_score(y_rem_true[mask], pred[mask]) # このラウンドだけ cum_pseudo_acc = accuracy_score(np.concatenate(cum_true), np.concatenate(cum_pred)) # これまでの全部
3なぜ「疑似ラベル100%正解」でもtest精度が落ちたのか(selection bias)
🧮 数学・統計の補足(文系向け)
模試の過去問を解くとき、「もう完璧に解ける得意な問題(100点満点で必ず正解する問題)」ばかりを繰り返し解いても、本番のテストの点数は上がりません。むしろ、その時間を「まだ自信のない苦手な問題」に使わないと、本番で出る「境界線上の難しい問題」に対応できないままです。今日のRandomForestに起きたのも似たことです。確信度が高い=「モデルにとって簡単な問題」ばかりを追加で"復習"させても、test集合に含まれる「クラスの境目にある紛らわしいサンプル」への対応力は伸びません。むしろ簡単な問題の比重が増えすぎて、木の分岐が「簡単な領域」に最適化され、境界線付近の判断がぼやけてしまうことすらあります。
10回のテストの平均点が90点だったとして、11回目に30点を取っても、11回分の平均点は90点から55点にはならず、まだ85点前後に留まります(過去の高得点が平均を下支えするため)。今日の累積疑似ラベル正解率も同じ仕組みで、初期ラウンドの高い正解率(95.0%)が、後期ラウンドの低い正解率(38.5%〜57.1%)の悪化を「薄めて」しまい、全体を見ているだけでは異変に気づきにくくなります。だからこそ「直近の1回だけ」を見る指標(ラウンドローカル正解率)が別途必要になります。
🏆 Kaggleでの実践的な使い方
よく使われるコンペカテゴリ: ☑ 表形式データ(Tabular) / ☑ 自然言語処理(NLP) / ☑ 画像認識(CV) / ☐ 時系列(Time Series)
🖼️ 画像コンペでのIterative Pseudo Labeling
反復は1〜3ラウンドが定石
Cassava Leaf Disease Classification など各種画像コンペの上位解法では、反復は1〜3ラウンド程度に留めるのが一般的。ラウンドを重ねすぎると今日確認したような劣化リスクが増すため、「何ラウンドで打ち切るか」自体をCVスコアの推移を見ながら決める。
📝 NLPでの多様性確保の工夫
確信度上位だけに頼らない
BERT系モデルのfine-tuningでIterative Pseudo Labelingを行う際は、各ラウンドでサンプリングの多様性を意識的に確保する(確信度上位だけでなく、クラスごとの采配を均等にするなど)ことで、今日確認したselection biasを緩和する工夫がよく使われる。
🔧 直近の質を間接的に監視する
累積指標だけに頼らない
実務では疑似ラベルの正解率を直接測れないことがほとんどだが、代わりに「直近ラウンドで追加したサンプルに対するCVスコアの変化」や「予測エントロピーの推移」を監視し、質の劣化を間接的に検知する手法が使われる。
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| ラウンドを重ねるほどtest精度は単調に改善すると思い込む | 「学習データが増える=良いこと」という直感をそのまま適用してしまう | 今日の実測ではラウンド1の時点で早くも精度が悪化し、10ラウンド通してベースラインを上回らなかった。反復によって改善するとは限らない |
| confirmation biasは「疑似ラベルの誤り」だけが原因だと考える | 「間違ったラベルを学習したから悪化した」という説明が最も直感的でわかりやすいため | 疑似ラベル正解率が100%でも精度が落ちるケースがあり、サンプルの選ばれ方の偏り(selection bias)という、ラベルの正しさとは独立した要因が存在する |
| 累積疑似ラベル正解率だけを見て「まだ質は保たれている」と安心する | 一つの集計指標だけを見て判断してしまう | 累積正解率は過去の高い正解率に引きずられて緩やかにしか下がらないため、直近ラウンドの急激な劣化を見逃す。ラウンドローカルの指標を必ず併記する必要がある |
| 前回(Day136)決めた閾値を、乱数や環境が変わっても同じ値のまま使い回してよいと考える | 「一度決めた閾値は固定して使うもの」という思い込み | 今日の実行環境では確信度の最大値が0.89までしか届かず、昨日「安全」とされた閾値0.90は1件も採用できなかった。閾値は実行のたびに確信度分布を確認し、必要なら調整すべき値である |
🚀 次のステップ
- 発展: 今日はRandomForestで固定の閾値・固定のラウンド数で実験しましたが、実務では「ラウンドごとに閾値を段階的に引き上げる」戦略や、「1ラウンドあたりに追加するサンプル数の上限を決める」戦略もよく使われます。これらを実装し、今日確認したselection biasが緩和されるかを検証してみましょう
- 次回予告: Phase6テーマ3の3日目。今日学んだ「疑似ラベルの質は正解率だけでは測れない」という教訓を踏まえ、複数の予測(元データ+摂動を加えたデータ)を平均して1つの最終予測にまとめるTTA(Test Time Augmentation)に取り組みます
Phase 6 の学習マップ(全20テーマ)
Day136で単発Pseudo Labelingを、Day137でIterative Pseudo Labelingを実装。次はTTA(Test Time Augmentation)に進む。
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: