Day 160 — NLP系コンペへの応用⑪(シリーズ総括)— Iterative Pseudo Labelingの設計フローチャートを作ったら、最初の分岐は「疑似ラベルを使わない」だった

2026-09-23 金 / Phase 6(Grandmaster、総復習後の発展回) 戦略→コーディング 根拠の監査+ラベル40件だけのfold内CVで測り直し

📚 背景知識(読んでから問題へ)

Day151〜159の10日間で分かったことを、次に同じ状況に出会ったとき「どの順番で何を決めるか」という設計手順にまとめる日。まず材料を並べる。

Day問い実測で分かったこと
151テキストでもPseudo Labelingは効くか閾値3種すべてでbaseline(40件のみ 0.7418)を下回る。疑似ラベル精度89.95%でもtest 0.6236
152クラスタで多様性を持たせれば直るかpost-filter 0.6291。top-N(0.6236)とほぼ同じ
153フィルタ前クラスタリングなら直るかpre-filter 0.6327。同数ランダム選択が0.6800で最良
154クラスタ内の選び方を変えたら(G)クラスタ内Uncertainty 0.6800。疑似ラベル精度0.5906なのにtestは最良
155反復でもGの優位は続くかD 0.6327→0.5455(-13.8%)、G 0.6800→0.6455(-5.1%)
156quota配分ルールで結論は変わるかgapが1ポイント前後動くだけで結論は不変
157打ち切りはオラクルと検証accのどちらかval_esはD・Gとも全seedでround1打ち切り。oracle_esはround2を採用し0.5705
158「round1が最適」は一般構造か選択基準ごとに残プールの難易度が逆方向に偏る(D難化、G易化)
159Dの劣化は新規エラーか既存エラー強化か両方(137 vs 112件)。既存誤答の確信度低下は0件
🔎
統合する前の監査で見つかったこと。 Day157の本文はval_esを「真のラベル不要、実運用でも使える」と説明していたが、コードは次のようにtest_dfの真のラベルでaccを計算して打ち切っていた。Kaggleなら「Private LBを見ながらラウンド数を決める」のと同じで、提出前には原理的にできない。さらにround1を無条件採択していたので、「疑似ラベルを1件も足さない(r=0)」が比較候補に入っていなかった。
acc_after, _, _ = train_and_eval(new_texts, new_labels, test_df)   # ← 最終報告に使う test_df そのもの
if stop_mode == "val" and prev_acc is not None and acc_after < prev_acc:
    break
🌊
世界の実物で確認する(2026-09-23参照)。
  • Oliver ほか "Realistic Evaluation of Deep Semi-Supervised Learning Algorithms"(NeurIPS 2018, arXiv:1804.09170): ラベルなしデータを使わない単純なbaselineが十分に報告されていないと指摘。ラベルが少ない設定で大きな検証セットを使う非現実性も論じる
  • Arazo ほか "Pseudo-Labeling and Confirmation Bias in Deep Semi-Supervised Learning"(IJCNN 2020, arXiv:1908.02983): 素朴な疑似ラベリングはconfirmation biasで誤った疑似ラベルに過学習する。Day155・159の現象の一次資料

🗂️ データ・評価スキーマ

データ/用語件数・型役割
labeled_df40件(text, label)真のラベルがある唯一のデータ。今日はrの選択にもこれだけを使う
unlabeled_df300件(textのみ使用)疑似ラベルを付けて学習に足す候補プール
test_df110件最終報告だけに使う。選択には使わない(test覗きは比較用の上限としてのみ計算)
rint 0〜5疑似ラベルを足したラウンド数。r=0は疑似ラベルなし
test曲線float[6]40件全部から始め、各rのモデルでtest_dfを測ったacc
CV曲線float[5, 6] → 平均 float[6]40件を5-fold(1fold 8件)。学習側32件から疑似ラベルを作り直し、検証8件で測る
rの選び方testのラベルを使うかr=0を候補に含むか
Day157ルール使う(打ち切り判断に)含まない
test覗き(peek)使う(argmax)含む — 提出前には使えない上限
正直なCV使わない含む(同点なら小さいr)

📏 評価の仕組み — 40件CVは何件分の差まで見分けられるか

評価指標はaccuracy(正解数÷件数)。ただし検証1foldが8件だと、accは 0/8, 1/8, … 8/8 の0.125刻み、5fold平均でも0.025刻みにしかならない。下の図は、今日の実測差をこの目盛りに重ねたもの。

test上のr=0とr=1の差 と 40件CVの1目盛り(0.025=1件) D: r0−r1 0.109 G: r0−r1 0.062 G seed2 誤判断 CV差 0.05 = 2件 G seed4 誤判断 CV差 0.025 = 1件 0 .025 .050 .075 .100
📐
r=0でのfold間標準偏差は0.141(実測)。CVが誤った2回は、どちらも1〜2目盛り(1〜2件)の差で決まっていた。Dのように4目盛り以上離れていれば5/5で正しく選べた。

🎯 問題

タスクA(戦略・監査)

1
Day151〜159の表からtestのラベルを見ないと出せなかった結論を1つ挙げ、それがフローチャートの分岐としてそのまま使えない理由を2〜3文で述べる
2
40件の5-fold CV(1fold 8件)で「どのrが最良か」を正しく選べるかを予想する。r=0とr=1の差がDとGでどれくらいだったかを手がかりに、DとGで選びやすさが違うかも述べる

タスクB(コーディング)

B1
ipl_curve(...): r=0〜5の各時点のモデルでeval_dfsの各データのaccを返す。プール枯渇・n_added=0なら残りを直前値で埋める
B2
cv_curve(...): StratifiedKFoldで40件を分け、foldごとに学習側だけからipl_curveを回す
B3
day157_val_es(test_curve)とargmax_first(同点なら小さいr)を実装する
B4
D・G × 5seedで、r=0/1/5・Day157ルール・test覗き・正直なCVのaccを集計する
B5
assert 6個: (a) r=1がDay155/157の0.6327/0.6800を再現 (b) r=0がDay151の0.7418を再現 (c) Day157ルールは全runでr=1 (d) D・Gともr=0 > Day157ルール (e) 正直なCVはDで5/5 r=0 (f) D・Gとも正直なCV > Day157ルール
B6
(戦略) 設計フローチャートを6ノード以内で書く。各ノードに根拠のDay番号と実測値を1つずつ添える

💡 ヒント

ヒント1方向性
「r=0を候補に入れる」だけで比較の意味が変わる。Day152以降はほとんど「疑似ラベルを使う手法どうし」の比較だった。また、40件全部で学習したモデルの疑似ラベルを各foldで使い回すと、検証foldのラベル情報が疑似ラベル経由で漏れる。
ヒント2アプローチ
Day159のループをrange(0, n_rounds+1)にして、ループ先頭のモデルを全評価データで測る。CVは0.025刻みで同点が多いので、同点なら小さいr(手間が少なく壊れにくい方)を選ぶ。test覗きは「上限」として並べ、CVがどれだけ近づけるかを見る。
ヒント3コード骨格
def cv_curve(labeled_df, unlabeled_df, select_fn, seed, n_rounds=5, n_splits=5):
    skf = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=seed)
    fold_curves = []
    for tr, va in skf.split(labeled_df["text"], labeled_df["label"]):
        tr_df, va_df = labeled_df.iloc[tr], labeled_df.iloc[va]
        fold_curves.append(ipl_curve(tr_df["text"], tr_df["label"], unlabeled_df,
                                     select_fn, seed, {"val": va_df}, n_rounds)["val"])
    return np.vstack(fold_curves)

✅ 模範解答(要点。全文はMDファイル)

# make_text_dataset / split_seed / クラスタ選択(D,G) は Day151〜159 と同じ
def ipl_curve(lab_texts, lab_labels, unlabeled_df, select_fn, seed, eval_dfs,
              n_rounds=5, threshold=0.60, n_clusters=8):
    cur_t, cur_l = list(lab_texts), list(lab_labels)
    remaining = unlabeled_df.reset_index(drop=True)
    curves = {k: [] for k in eval_dfs}
    for r in range(0, n_rounds + 1):          # ★ r=0 (疑似ラベルなし) から測る
        vec, model = fit(cur_t, cur_l)
        for k, d in eval_dfs.items():
            curves[k].append(acc_on(vec, model, d))
        if r == n_rounds: break
        stop = len(remaining) == 0
        if not stop:
            proba = model.predict_proba(vec.transform(remaining["text"]))[:, 1]
            n_added = int(((proba >= threshold) | (proba <= 1 - threshold)).sum())
            stop = n_added == 0
        if stop:
            for k in curves: curves[k] += [curves[k][-1]] * (n_rounds - r)
            break
        mask, pl = select_fn(model, vec, remaining, n_added, n_clusters, seed)
        cur_t += remaining.loc[mask, "text"].tolist(); cur_l += pl.tolist()
        remaining = remaining.loc[~mask].reset_index(drop=True)
    return {k: np.array(v) for k, v in curves.items()}

def argmax_first(a):
    return int(np.flatnonzero(a == a.max())[0])        # 同点なら小さい r

def day157_val_es(test_curve):                          # Day157 の再現: r=1 から開始
    r = 1
    while r + 1 < len(test_curve) and test_curve[r + 1] >= test_curve[r]:
        r += 1
    return r

# D・G × seed 0..4 で full = ipl_curve(40件, {"test": test_df, ...}), cv = cv_curve(...)
# r_cv = argmax_first(cv.mean(axis=0)),  r_peek = argmax_first(full["test"]),  r_157 = day157_val_es(full["test"])
assert (res["r_day157"] == 1).all()
assert (m["acc_r0"] > m["acc_day157"]).all()
assert (res.loc[res.method == "D", "r_cv"] == 0).all()
assert (m["acc_cv"] > m["acc_day157"]).all()
print("[OK] 6個のassertすべて成立")
# ===== 実測(2026-09-23 / Python 3.13, scikit-learn 1.9.1, pandas 3.0.6, numpy 2.5.3 / 約21秒) =====
[手法別 5seed平均 test acc]
          r=0     r=1     r=5    Day157ルール  test覗き(上限)  正直なCV
   D   0.7418  0.6327  0.5455       0.6327        0.7418        0.7418
   G   0.7418  0.6800  0.6455       0.6800        0.7491        0.7255

選ばれたr:  Day157ルール  D [1,1,1,1,1]  G [1,1,1,1,1]
            test覗き      D [0,0,0,0,0]  G [0,0,0,0,1]
            正直なCV      D [0,0,0,0,0]  G [0,0,2,0,0]
[OK] 6個のassertすべて成立
🎯
発見1: 10日間のどの手法も、疑似ラベルなし(r=0, 0.7418)に勝っていなかった。 10run中、r=0を上回ったのはG seed4のr=1(0.7364 vs 0.7000)の1件だけ。Day152以降は手法どうしの比較に移り、Day157の打ち切りもr=1から始めていたため、r=0が土俵の外にいた。
⚠️
発見2: testを見ない正直なCVの方が、testを見ていたDay157ルールより良かった。 D +0.1091、G +0.0455。覗き見の害より、候補からr=0を落としていた害の方が大きかった。
🔍
発見3: 40件CVは大差は見分けるが小差は見分けない。 Dは5/5正解。Gは2回誤判断(seed2: 2件分の差でr=2を選び −0.0818、seed4: 1件分の差でr=1の +0.0364 を逃す)。

📊 結果の可視化

test acc と r(5seed平均)— 最大値は r=0 0.500.550.60 0.650.700.75 r=012 345 疑似ラベルなし 0.7418 G 0.6800 D 0.6327 ← Day157ルールが選んだ点 G 0.6455 D 0.5455 赤=D(クラスタ内確信度優先) 黄=G(クラスタ内Uncertainty) r=0は両者共通
rの選び方ごとの test acc(軸は0.50始まり) D Day157ルール0.6327 正直なCV0.7418 (+0.1091) test覗き(上限)0.7418 G Day157ルール0.6800 正直なCV0.7255 (+0.0455) test覗き(上限)0.7491 0.50 0.80

🧭 設計フローチャート(タスクB6の解答例)

N0 評価の土台: 検証はラベル付きデータだけ。疑似ラベルはfoldごとに作り直す 根拠: Day157のval_esはtest_dfで打ち切っていた(今日の監査) N1 r=0(疑似ラベルなし)を必ず候補に入れ、CVで比較する 根拠: r=0 0.7418 > D最良 0.6327 / G最良 0.6800。CVはDで5/5 r=0(Day160) r=0が最良 or 差 < 0.05 → 疑似ラベルは使わない。終了 G seed2: 2件差でr=2を選び −0.0818 r>0 が 0.05 以上上回る → N2 へ進む N2 選び方: クラスタ内確信度優先(D)は使わない。ランダムかクラスタ内Uncertainty 根拠: D 0.6327 / ランダム 0.6800 / G 0.6800(Day153・154) N3 反復回数: CV曲線で選ぶ。quota配分の工夫は後回し 根拠: Dはround1以降単調悪化 0.6327→0.5455(Day155)。quotaは±1pt程度(Day156) N4 止める指標: 疑似ラベル精度ではなくCVのacc 根拠: 疑似ラベル精度0.5758のround2を採用したoracle_esは0.5705(Day157) N5 監視: 件数の少ない回の指標は信じない。誤答の確信度の片方向化に注意 根拠: n_added 1.5件で精度1.000(Day156)、確信度低下0/112件(Day159)
✂️
入れなかった分岐と、この図が当てはまらない前提。 閾値の選び方(Day151で3つとも負け、分岐にする根拠がない)とabs_margin(Day158、合成データの生成規則を知っているから計算できた診断指標)は入れていない。今日の結論は「ラベル40件・TF-IDF+ロジスティック回帰・未ラベル300件・語彙37語の合成データ」でのもの。ラベルなしデータが桁違いに多い、モデルが大きい、強い拡張を併用する(Arazoらはmixupでconfirmation biasを抑えている)前提ではN1で「r>0」側に進む可能性が高い。一般化してよいのはN0・N1の手順であって、「r=0が勝つ」という結論ではない。

🪜 Step-by-Step 解説

1
結論を並べる前に、根拠の出どころを監査する。 説明文(「真のラベル不要」)とコード(test_dfでacc計算)が食い違っていた。文章ではなくコードを読み、どのラベルを見て判断しているかを確かめるのが監査の中身。
2
r=0を曲線の最初の点にする。 ループをrange(0, …)にするだけで「疑似ラベルを使う価値があるか」が他のrと同じ物差しで測れる。実測ではこの1点(0.7418)が曲線全体の最大値だった。
3
CVの中で疑似ラベルを作り直す。 40件全部で学習したモデルの疑似ラベルを使い回すと、検証foldの情報が漏れてCVが楽観的になる。計算は5倍(約21秒)だが、この規模なら迷う理由はない。
4
CVが外れた2回を件数に直して読む。 どちらも1〜2件で決まった判断だった。ここからN1の「差0.05(2件)未満ならr=0」を書いた。ただしこのしきい値は今日の10runだけから決めたもので、別データでは確かめていない。

迷った他案と採らなかった理由

他案利点採らなかった理由前提が変わったら
40件を学習30+検証10のホールドアウトに分ける計算が1/5accが0.1刻みでCVよりさらに粗く、学習側も減るラベルが数千件あればホールドアウトで十分
未ラベルから数十件に人手でラベルを付け検証にする判断が安定する「ラベル40件しかない」前提を変えてしまうアノテーション予算があれば最も確実
testを半分に分け、前半で選び後半で報告覗き見の楽観バイアスを測れるr=0が大差で最良のため両半分とも同じrを選び、バイアスは測れなかった(前半0.7164/後半0.7673の差は分割のばらつき)r同士の差が小さいデータなら有効

🧮 数学・統計の補足(文系向け)

📝
8問の小テストで考える。 8問の小テストを5回やって平均を出すと、点数は1問12.5点刻み、5回平均でも2.5点刻み。2人の平均が2.5点違っても40問中1問の差で、たまたまの可能性が十分ある。Dのように差が10点以上なら見分けられるが、Gのように数点だとよく間違える。
🎟️
testを覗いて選ぶと良く見える理由をくじ引きで考える。 6本のくじ(r=0〜5)を同じ110人に試してもらい一番よかったものを選ぶと、成績には「本当に良い分」と「その110人と相性が良かった分」が混ざる。選ぶのに使った110人の成績をそのまま報告すると、後者の分だけ良く見える。

🏆 Kaggleでの実践的な使い方

原則具体的な活用例
Pseudo Labelingを入れる前に、同じCVで「入れないモデル」を並べる今日のようにr=0が一番良いことは普通にある。差を見ずに入れると、効いているかどうか分からない
疑似ラベルはfoldごとに作る疑似ラベルを作ったモデルが検証foldで学習していると、CVが実際より良く見え、CVとLBがずれる
Public LBを見ながらラウンド数を決めないDay157のval_esはtestを見て止めていた。コンペで同じことをするとPrivate LBでのshake downの原因になる
差が小さいならやらない、を既定値にするCV差がサンプル1〜2件分しかないなら、手間が増え壊れやすい方(疑似ラベルあり)を選ばない

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
総括は過去の結論を並べ直せばよい一度確かめた結論は正しいと思い込む並べる前に根拠の出どころを監査する。今日はDay157の説明とコードの食い違いが見つかった
手法どうしの比較で勝った方を「効く手法」と呼ぶ比較の土俵に何を入れたかを忘れるr=0を入れないと、勝った手法もbaselineに負けていることがある(G 0.6800 < 0.7418)
testを見ないCVは、testを見るより必ず悪い情報が少ない方が不利だと考える正直なCV(D 0.7418)がtestを見たDay157ルール(0.6327)より良かった。候補の作り方の方が効いた
CVで少しでも高いrを選べばよい平均値の大小だけを見る40件CVは0.025刻み、fold間標準偏差0.141。G seed2は2件差でr=2を選び0.0818失った
「疑似ラベルは使わない方がよい」を一般則として覚える総括の結論は一般則だと思いやすい小さな合成データ・線形モデル・拡張なしでの結論。一般化してよいのは手順(N0・N1)

🚀 次のステップ

  • 発展: N1のしきい値「差0.05未満ならr=0」を、seed数50で「CVでの差」と「testでの差」の散布図にして測り直す。Repeated Stratified K-FoldでCVの粗さがどれだけ減るかも確かめる
  • 次回予告: Day161では、ラベル付き件数を40→400に増やしたとき、r=0とr>0の優劣が入れ替わるか、入れ替わるならどの件数からかを測り、今日の結論が「データが少ないとき専用」なのかを確かめる

Phase 6 の学習マップ

1-23
Phase6主要テーマ・総復習(Day126-149、完了)
発展①〜⑪
NLP系コンペへの応用(Day160: 根拠を監査し、設計フローチャートに統合)

📝 自己評価(解いた後に記入)

自分の回答・気づき・メモ: