📚 背景知識(読んでから問題へ)
今日はDay137で扱ったIterative Pseudo Labeling(学習→予測→採用→再学習を複数ラウンド繰り返す枠組み)にDay154の(D)・(G)を拡張し、「pseudo_accが低い(G)は反復するほど誤ったラベルが蓄積し、(D)に追いつかれる・逆転されるのではないか」という懸念を検証する。
| 用語 | 意味 |
|---|---|
| D_iter | (D)クラスタ内確信度優先の選択基準を使ったIterative Pseudo Labeling |
| G_iter | (G)クラスタ内Uncertainty Samplingの選択基準を使ったIterative Pseudo Labeling |
| round-localな疑似ラベル精度 | そのラウンドで新たに採用したサンプルだけの正解率(Day137と同じ定義) |
| own-final-round比較 | D/Gで採用件数がモデル依存で異なり、プールを使い切るタイミングも異なるため、固定round番号ではなく各手法が実際に到達した最終ラウンドどうしを比較する方式 |
🗂️ データ・分割のスキーマ
| 変数名 | 型 | 内容 |
|---|---|---|
| df (n=450) | pd.DataFrame | Day151-154と同じmake_text_dataset()で生成した合成テキスト全体 |
| test_df (110件) | pd.DataFrame | 最終評価専用のホールドアウト。全ラウンド・全設定で共通に使う |
| labeled_df (40件) | pd.DataFrame | 最初の学習データ。ラウンドを追うごとに疑似ラベル付きサンプルが追記される |
| remaining_df (約300件→減少) | pd.DataFrame | まだ採用していない未ラベルプール。ラウンドごとに採用分が除外され縮小する |
threshold = 0.60 # 各ラウンドのn_addedを決める基準(Day151-154と同一) n_clusters = 8 # KMeansのクラスタ数(D_iter/G_iter共通) n_rounds = 5 # 最大ラウンド数(採用0件になれば早期終了、Day137の枠組み)
🎯 問題
タスクA(理論)
以下2点をそれぞれ2〜3文で説明する。
タスクB(コーディング→分析)
make_text_dataset・split_seed・train_and_eval・select_confident_pseudo_labels・_cluster_and_quota・select_diverse_prefilter(D)・select_diverse_prefilter_uncertainty(G)はDay151-154と同じ実装を再利用するrun_iterative(seed, select_fn, threshold=0.60, n_clusters=8, n_rounds=5)を実装。ラウンドごとに現在の累積学習データで再学習→残りプールでn_addedを決定→select_fn(D or G)で実際に選ぶ→採用分をプールから除外し学習データに追加、を採用0件かn_rounds到達まで繰り返すassert文で確認する💡 ヒント
タスクAの1.は、「pseudo_accが低い」ことと「confirmation biasが起きやすい」ことを短絡的に結びつけないよう注意する。Day154の(G)がpseudo_acc最低でもtest精度最高だったのは「決定境界付近のサンプルは情報量が多い」ためであり、この性質はラウンドを重ねても変わらない可能性がある。一方(D)は「モデルが既に得意な、決定境界から遠いサンプル」ばかりを選び続けるため、ラウンドを重ねるにつれ学習データの多様性が乏しくなり、後半のラウンドでモデルが誤った方向に過信するリスクの方が高いかもしれない。
run_iterativeはDay137のループ構造(remaining_idxを縮める)を、remaining_dfをDataFrameのまま.loc[~mask_sel]で縮める形に置き換えればよい- round-local疑似ラベル精度は、その回の
mask_sel・pseudo_selとremaining_df.loc[mask_sel, "label"](検証専用)を比較する - 累積疑似ラベル精度は、これまでの
(true, pseudo)ペアをリストに貯めてnp.concatenateしてから比較する(Day137と同じパターン) - own-final-round比較は各
(seed, method)の結果DataFrameの.iloc[-1]を使う - reversal判定は
(gap_round1 > 0) != (gap_final > 0)で真偽の不一致を見ればよい
def run_iterative(seed, select_fn, threshold=0.60, n_clusters=8, n_rounds=5): labeled_df, unlabeled_df, test_df = split_seed(seed) cur_texts = labeled_df["text"].tolist() cur_labels = labeled_df["label"].tolist() remaining_df = unlabeled_df.copy().reset_index(drop=True) rows = [] cum_true, cum_pred = [], [] for r in range(1, n_rounds + 1): acc, _, vectorizer, model = train_and_eval(cur_texts, cur_labels, test_df) if len(remaining_df) == 0: break mask_conf, _ = select_confident_pseudo_labels(model, vectorizer, remaining_df, threshold) n_added = int(mask_conf.sum()) if n_added == 0: break mask_sel, pseudo_sel = select_fn(model, vectorizer, remaining_df, n_added, n_clusters, seed) true_sel = remaining_df.loc[mask_sel, "label"].to_numpy() round_pseudo_acc = float((pseudo_sel == true_sel).mean()) cum_true.append(true_sel); cum_pred.append(pseudo_sel) cum_pseudo_acc = float((np.concatenate(cum_pred) == np.concatenate(cum_true)).mean()) cur_texts = cur_texts + remaining_df.loc[mask_sel, "text"].tolist() cur_labels = cur_labels + pseudo_sel.tolist() remaining_df = remaining_df.loc[~mask_sel].reset_index(drop=True) # ★採用済みをプールから除外 acc_after, _, _, _ = train_and_eval(cur_texts, cur_labels, test_df) rows.append({"round": r, "n_added": int(mask_sel.sum()), "pool_left": len(remaining_df), "round_pseudo_acc": round_pseudo_acc, "cum_pseudo_acc": cum_pseudo_acc, "test_acc": acc_after}) return pd.DataFrame(rows)
✅ 模範解答
# POSITIVE_WORDS / NEGATIVE_WORDS / NEUTRAL_WORDS / make_text_dataset / train_and_eval / # split_seed / select_confident_pseudo_labels / _cluster_and_quota / # select_diverse_prefilter(D) / select_diverse_prefilter_uncertainty(G) は # Day151-154と同一(省略) def run_iterative(seed, select_fn, threshold=0.60, n_clusters=8, n_rounds=5): """select_fn: (model, vectorizer, remaining_df, n_target, n_clusters, seed) -> (mask, pseudo_labels)""" labeled_df, unlabeled_df, test_df = split_seed(seed) cur_texts = labeled_df["text"].tolist() cur_labels = labeled_df["label"].tolist() remaining_df = unlabeled_df.copy().reset_index(drop=True) rows = [] cum_true, cum_pred = [], [] for r in range(1, n_rounds + 1): acc, _, vectorizer, model = train_and_eval(cur_texts, cur_labels, test_df) if len(remaining_df) == 0: break mask_conf, _ = select_confident_pseudo_labels(model, vectorizer, remaining_df, threshold) n_added = int(mask_conf.sum()) if n_added == 0: break mask_sel, pseudo_sel = select_fn(model, vectorizer, remaining_df, n_added, n_clusters, seed) true_sel = remaining_df.loc[mask_sel, "label"].to_numpy() round_pseudo_acc = float((pseudo_sel == true_sel).mean()) cum_true.append(true_sel); cum_pred.append(pseudo_sel) cum_pseudo_acc = float((np.concatenate(cum_pred) == np.concatenate(cum_true)).mean()) cur_texts = cur_texts + remaining_df.loc[mask_sel, "text"].tolist() cur_labels = cur_labels + pseudo_sel.tolist() remaining_df = remaining_df.loc[~mask_sel].reset_index(drop=True) acc_after, _, _, _ = train_and_eval(cur_texts, cur_labels, test_df) rows.append({"round": r, "n_added": int(mask_sel.sum()), "pool_left": len(remaining_df), "round_pseudo_acc": round_pseudo_acc, "cum_pseudo_acc": cum_pseudo_acc, "test_acc": acc_after}) return pd.DataFrame(rows) SEEDS = [0, 1, 2, 3, 4] N_ROUNDS = 5 all_d, all_g = [], [] for s in SEEDS: df_d = run_iterative(s, select_diverse_prefilter, n_rounds=N_ROUNDS); df_d["seed"] = s; all_d.append(df_d) df_g = run_iterative(s, select_diverse_prefilter_uncertainty, n_rounds=N_ROUNDS); df_g["seed"] = s; all_g.append(df_g) df_d_all = pd.concat(all_d, ignore_index=True) df_g_all = pd.concat(all_g, ignore_index=True) mean_d = df_d_all.groupby("round")[["round_pseudo_acc", "cum_pseudo_acc", "test_acc"]].mean() mean_g = df_g_all.groupby("round")[["round_pseudo_acc", "cum_pseudo_acc", "test_acc"]].mean() # seedごとの own-final-round 比較 rows_final = [] for s in SEEDS: d_s, g_s = df_d_all[df_d_all["seed"]==s], df_g_all[df_g_all["seed"]==s] d_r1, g_r1 = d_s[d_s["round"]==1]["test_acc"].values[0], g_s[g_s["round"]==1]["test_acc"].values[0] d_last, g_last = d_s.iloc[-1], g_s.iloc[-1] rows_final.append({"seed": s, "gap_round1": g_r1 - d_r1, "d_final_acc": d_last["test_acc"], "g_final_acc": g_last["test_acc"], "gap_final": g_last["test_acc"] - d_last["test_acc"]}) df_final = pd.DataFrame(rows_final) df_final["reversed"] = (df_final["gap_round1"] > 0) != (df_final["gap_final"] > 0) mean_d_r1, mean_g_r1 = mean_d.loc[1, "test_acc"], mean_g.loc[1, "test_acc"] mean_d_final, mean_g_final = df_final["d_final_acc"].mean(), df_final["g_final_acc"].mean() assert mean_g_final > mean_d_final, "own-final-roundでG_iterがD_iterを上回っていません" assert mean_d_final < mean_d_r1, "D_iterがconfirmation biasで劣化していません" assert mean_g_final < mean_g_r1, "G_iterがconfirmation biasで劣化していません" print("[OK] own-final-roundでもG_iterの優位性は消えていない") print("[OK] D_iter・G_iterともにconfirmation biasで劣化するが、程度が大きく異なる")
# ============ 実測結果(実際にPython 3で実行して確認済み) ============
# [ラウンド別 平均(固定round番号)]
# round n_added(D) round_pseudo_acc(D) cum_pseudo_acc(D) test_acc(D) | n_added(G) round_pseudo_acc(G) cum_pseudo_acc(G) test_acc(G)
# 1 90.6 0.867442 0.867442 0.632727 | 90.6 0.590581 0.590581 0.680000
# 2 140.8 0.551895 0.676180 0.572727 | 143.6 0.701816 0.657712 0.649091
# 3 54.4 0.418792 0.622854 0.549091 | 59.0 0.804038 0.690196 0.645455
# 4 11.8 0.344933 0.612377 0.547273 | 6.4 0.803175 0.692877 0.643636
# 5 4.0 0.152778 0.602222 0.527273 | 2.0 0.500000 0.656667 0.590909
#
# mean baseline_acc(40件のみ、参考) = 0.7418
# gap(G-D)推移: round1 +0.0473 → round2 +0.0764 → round3 +0.0964 → round4 +0.0964 → round5 +0.0636
#
# [seedごとの own-final-round 比較]
# seed gap_round1 d_final_round d_final_acc g_final_round g_final_acc gap_final reversed
# 0 0.0636 4 0.5636 4 0.6273 0.0636 False
# 1 0.0182 4 0.5818 4 0.6091 0.0273 False
# 2 0.0909 5 0.5364 4 0.7000 0.1636 False
# 3 -0.0636 5 0.5182 5 0.5909 0.0727 True ← D優勢→G優勢に逆転
# 4 0.1273 5 0.5273 4 0.7000 0.1727 False
#
# mean gap_round1 (G-D) = +0.0473
# mean gap_final (G-D) = +0.1000(round1の2倍以上)
# reversed件数 = 1 / 5
#
# mean_d_round1=0.6327 -> mean_d_final=0.5455 (Δ-0.0873, 相対-13.8%)
# mean_g_round1=0.6800 -> mean_g_final=0.6455 (Δ-0.0345, 相対-5.1%)
#
# [OK] own-final-roundでもG_iterの優位性は消えていない
# [OK] D_iter・G_iterともにconfirmation biasで劣化するが、程度が大きく異なる
📊 結果の可視化
① Day154の懸念
(G)はpseudo_acc最低。反復すれば誤りが蓄積し(D)に負けるのでは?
② Iterative化
D_iter・G_iterを最大5ラウンド、5シードで実行
③ own-final-round比較
プール枯渇タイミングがD/Gで異なるため、固定roundでなく各手法の最終到達点で比較
④ 結果
gapは縮小どころか2倍に拡大。seed3では逆転も確認
| 指標 | D_iter(確信度優先) | G_iter(Uncertainty Sampling) | 解釈 |
|---|---|---|---|
| round1 test acc | 0.6327 | 0.6800 | Day154と同一の1回きり比較 |
| own-final-round test acc(5シード平均) | 0.5455 | 0.6455 | gapは+0.0473→+0.1000へ拡大 |
| round1→final の劣化幅 | -0.0873(相対-13.8%) | -0.0345(相対-5.1%) | Dの方が約2.5倍深刻な劣化 |
| round-local疑似ラベル精度の推移 | 0.867→0.153(単調崩壊) | 0.591→0.804(上昇) | Dは自己強化的な悪循環に陥る |
| reversal(round1劣勢→final優勢) | seed3のみ該当(1/5) | 1回比較の優劣は反復で覆りうる | |
🪜 Step-by-Step 解説
1gapが縮小するという予想が外れ、むしろ2倍以上に拡大した理由
gap_round1 = +0.0473 # Day154と同じ、1回だけの比較 gap_final(own) = +0.1000 # 各手法が到達した最終ラウンドどうしの比較 mean_d: 0.6327 -> 0.5455 (Δ-0.0873) # (D)は大きく劣化 mean_g: 0.6800 -> 0.6455 (Δ-0.0345) # (G)は小さく劣化
2(D)のround-local疑似ラベル精度が崩壊する悪循環のメカニズム
D_iter round_pseudo_acc: 0.867 -> 0.552 -> 0.419 -> 0.345 -> 0.153 # 単調に崩壊 D_iter n_added: 90.6 -> 140.8 -> 54.4 -> 11.8 -> 4.0 # round2で採用件数が急増
31つのseedで見られた「1回比較では劣勢だったGが、反復すると逆転する」実例
seed3: gap_round1 = -0.0636(D優勢: d=0.6818, g=0.6182) gap_final = +0.0727(G優勢に逆転: d_final=0.5182, g_final=0.5909)
🧮 数学・統計の補足(文系向け)
ある生徒(D)は、模試で「もう解ける」と自信のある問題だけを繰り返し復習し、その内容を「正解」として自分のノートに書き足していく。ところがその生徒の「自信」自体がところどころ間違った理解に基づいていたとすると、間違った理解のまま「自信のある問題」の範囲がどんどん広がり、気づけば大量の誤った知識を「もう解ける」と思い込んだまま蓄積してしまう——これが(D)がround2で採用件数が急増しながら正解率が急落する現象に対応する。一方、別の生徒(G)は「自分が一番自信のない問題」だけを選んで少しずつ埋めていく。曖昧な部分を優先して埋めるこのやり方は間違いも含むが、自分の理解の穴を計画的に塞いでいくため、繰り返しても自己強化的な思い込みには陥りにくい、というイメージである。
新しい施策Aと施策Bを1回だけ比較したとき、Aの方が良い結果を出したとする。しかし毎週繰り返し運用したとき、Aは「自分の過去の判断を強化し続ける」設計だったために徐々に判断がずれていき、Bは「毎回、自分が一番自信のない部分を見直す」設計だったために安定して機能し続けた、というイメージである。1回きりのA/Bテストの結果だけで「継続運用したときの優劣」まで判断してはいけない、という直感である。
🏆 Kaggleでの実践的な使い方
よく使われるコンペカテゴリ: ☑ 自然言語処理(NLP) / ☑ 表形式データ(半教師あり学習全般に応用可)
🔁 疑似ラベル選択方式は「反復させたとき」で評価する
1回きりの比較では見えない性質がある
1回きりの比較では見えなかった性質(confirmation biasへの強さ)が、複数ラウンド反復させて初めて明らかになることがある。round1の結果だけで採否を決めず、複数ラウンドのシミュレーションで確認する価値がある。
🚨 「採用件数急増×正解率急落」を早期警告シグナルにする
悪循環の兆候をモニタリングする
(D)のround2で見られたパターン(採用件数90.6→140.8件、正解率86.7%→55.2%)は、モデルが誤った自信を持ち始めているサインである。この種の指標をモニタリングすると悪循環が深刻化する前に対策を打てる。
🎯 Active Learning由来の選択基準は反復運用に向く
長期的な精度維持に効く頑健性
Uncertainty Samplingが反復的な自己学習において確信度優先よりも頑健であることを示した。ラベル付けコストの低い自己学習パイプラインを本番で継続運用する場合、この頑健性は重要な性質になりうる。
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| pseudo_accが低い方式は反復するほど不利になると考える | 「誤ったラベルを学習し続ければ、当然その誤りが蓄積して悪化する」という直感が強く働く | 反復への強さを決めるのは疑似ラベルの正確さそのものより、選び方が「自己強化的な悪循環」を生みやすいかどうかという別の軸である。(G)はpseudo_accが低くても悪循環を生みにくい設計だった |
| 1回だけの比較結果を、反復運用した場合の優劣の予測にそのまま使う | A/Bテストのような1回の比較で優劣が決まると考えがちで、反復の影響を考慮し忘れる | seed3の実例が示す通り、1回だけの比較で劣勢だった方式が、反復すると逆転することがある。継続的に再学習する設計を評価する際は、必ず複数ラウンドでのシミュレーションを行うべき |
| 採用件数が増えることを「良いことだ(学習データが増えた)」と単純に評価する | 「データは多い方が良い」という一般的な直感に引きずられる | (D)のround2のように、採用件数の急増が正解率の急落を伴う場合、それはモデルが誤った自信を持ち始めた兆候である可能性が高い。件数だけでなく採用サンプルの質も同時に監視すべき |
| Iterative Pseudo Labelingは常にconfirmation biasで劣化すると一般化する | Day137・今日の実験がいずれもbaselineを下回る結果になったことから、反復は常に有害だと結論づけたくなる | 両方式ともbaseline(0.7418)を下回ったが、劣化の度合いには(D)-13.8%・(G)-5.1%という大きな差があった。「反復すれば必ず悪化する」のではなく「選び方次第で悪化の度合いが大きく変わる」というのがより正確な理解である |
🚀 次のステップ
- 発展: 今日はquota配分ルール(平均確信度が高いクラスタから余りを+1、Day152由来)を固定していた。この配分ルール自体を確信度非依存(均等配分)に変えた場合、(G)の反復への頑健性がさらに強まるのか、あるいは変わらないのかは未検証
- 次回予告: Day156では、上記のquota配分ルールを確信度非依存にした場合のIterative Pseudo Labelingへの影響を検証する。あるいは、今日発見した「採用件数急増×正解率急落」の早期警告シグナルを使って(D)側に早期打ち切りルールを導入した場合、劣化がどこまで緩和できるかを検証する候補もある
Phase 6 の学習マップ(NLP系コンペへの応用フェーズ継続中)
Day154の単発の改善が「たまたま1回だけ良かった」のか「反復しても崩れない性質」なのかを検証したのが今日の位置づけ。結果は後者であり、Phase6のケーススタディとして「1回きりの比較と反復運用での評価は別物である」という教訓を残した。
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: