Day 158 — NLP系コンペへの応用⑨ — 「round1が大域最適」はプール難易度の入れ替わりという一般構造から来ているのか

2026-09-17 金 / Phase 6(Grandmaster、総復習後の発展回) コーディング→分析 選択に使わない「真の難易度」でDとGを裏付ける

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

🔁
Day155〜157のおさらい: 「確信度優先(D)」はround-local疑似ラベル精度が0.867→0.153へ単調に崩壊し、「Uncertainty Sampling(G)」は劣化が小さい。Day157では「acc_afterが前ラウンドを下回ったら打ち切る」val_esルールをD・G両方に公平に適用したところ、両方ともround1で打ち切られ、round1が事実上の大域最適であることが判明した。
🧭
今日の問い(Day157の次回予告そのもの): この「round1が大域最適」という現象は、この合成データセット固有の性質なのか、それとも初期ラウンドほど確信度の高い"簡単な"サンプルが多く残っており、後のラウンドほど"難しい"サンプルしか残らないという、疑似ラベリング一般に共通する構造によるものなのか。これを検証するには、モデルの確信度とは独立な「真の難易度」の物差しが必要である。

make_text_datasetが生成するテキストはn_pos個のポジティブ語とn_neg個のネガティブ語を混ぜて作られ、ラベルはn_pos>n_negで決まる。abs_margin = abs(n_pos - n_neg)は、そのサンプルが本質的にどれだけ紛らわしいかを表す、選択ロジックには一切使わない診断専用の"神の視点"の難易度指標である(0=拮抗=ハード、2=一方的=イージー)。

用語意味
abs_marginabs(n_pos - n_neg)。診断専用、選択には一切使わない真の難易度
pool_mean_margin_beforeそのラウンドの選択が行われる前の、残プール全体のabs_marginの平均値
selected_mean_marginそのラウンドで実際に採用したサンプルのabs_marginの平均値
POPULATION_MEAN_MARGINデータ生成過程から理論的に導出できる母集団の期待値(=1.0)

🗂️ データ・難易度指標のスキーマ

変数名型内容
df (n=450)pd.DataFrameDay151-157と同じ合成テキスト全体。今日abs_margin列を追加
abs_marginint (0/1/2)abs(n_pos - n_neg)。診断専用、モデル学習にも選択にも使わない
pool_n_beforeintそのラウンド開始時点(選択前)の残プール件数
pool_mean_margin_beforefloat選択前の残プール全体のabs_marginの平均
🧮
母集団の理論値の導出: n_pos・n_negはそれぞれ{0,1,2}から一様に選ばれる(9通りが等確率)。abs_margin=0が2通り(1,1)(2,2)・=1が5通り・=2が2通り(0,2)(2,0) → 期待値 = (0×2+1×5+2×2)/9 = 1.0。round1(選択前、n=300)の実測は mean=0.9947, frac_hard=0.2200, frac_easy=0.2147 で、理論値(1.0, 0.222, 0.222)とほぼ一致した。
threshold   = 0.60  # ラウンドのn_addedを決める基準(Day151-157と同一)
n_clusters  = 8     # KMeansのクラスタ数
quota_mode  = "confidence"  # Day156の実測により固定
stop_mode   = None  # ★今日は早期打ち切りなし。D_base/G_baseのみを診断対象にする

🎯 問題

タスクA(理論)

以下2点をそれぞれ2〜3文で説明する。

1
(D)確信度優先の選択を繰り返すと、残プールのpool_mean_margin_beforeはラウンドを追うごとに「増加(易化)」「横ばい」「減少(難化)」のどれに近づくと予想されるか、理由とともに述べる。(G)Uncertainty Samplingについても同様に予想する
2
「残プールの難易度構成が変わる」ことが実測で確認できたとしても、それだけで(D)のround-local疑似ラベル精度が0.867→0.153まで崩壊した現象の全てを説明できると言えるか。説明しきれない部分があるとすれば何かを予想する

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

B1
make_text_datasetを拡張し各サンプルにabs_margin列を追加(選択ロジックでは一切参照しない)。他の関数はDay151-157と同一を再利用
B2
run_iterative_diag(seed, select_fn, n_rounds=5)を実装。各ラウンドの選択前にpool_n_before・pool_mean_margin_before・pool_frac_hard_before・pool_frac_easy_beforeを、選択後にselected_mean_marginを記録する
B3
D_base・G_base(stop_modeなし)を5シード・最大5ラウンドで実行する
B4
重要な注意: own-final-roundのpool_n_beforeは一桁まで減っており統計的に不安定。round1(n=300)→round2(n≈209)という大標本の遷移を主たる比較対象とする
B5
(a) D: round1→round2でpool_mean_margin_beforeが減少 (b) G: 増加 (c) Dのround1のselected_mean_marginが母集団平均(1.0)を上回る (d) Gは下回る (e) pool_mean_margin_beforeとround_pseudo_accの相関が0.3超 (f) 両手法のown-final-roundのpool_n_beforeが20件未満、をassertで確認する
B6
追加診断: D_baseのround5(selected_mean_margin≈0.96で並程度の難易度なのにround_pseudo_acc=0.153とコイントス以下)を確認し、難易度構成の変化だけでは説明しきれない現象であることを議論する
B7
(考察): タスクAの2つの予想が実測とどの程度一致したかを、round1→round2の頑健な比較結果とround5の異常値の両方に触れながら述べる

💡 ヒント

ヒント1方向性

「最終ラウンドで比較する」という自然な発想は罠である。プールが枯渇に近づくラウンドほど残っている件数が少なく(一桁台)、そこでの平均値は数件のサンプルで決まる不安定な統計量にすぎない。変化が大きく見える比較ほど、それを支える標本サイズを先に確認するという順序を守ることが今日最も重要である。

ヒント2アプローチ
  • 母集団の期待値は、n_pos・n_negが{0,1,2}から一様に選ばれる9通り(等確率)から理論的に導出できる(0=2通り・1=5通り・2=2通り → 期待値1.0)
  • 診断用の記録は「選択が行われる前」に取る。train_and_evalを呼ぶ前にremaining_df["abs_margin"]をスナップショットする
  • selected_mean_marginはremaining_df.loc[mask_sel, "abs_margin"].mean()で計算できる
  • round1→round2比較はgroupby("round")で固定ラウンド番号ごとの平均を取ればよい(可変長のown-final-round処理は不要)
ヒント3コード骨格
def run_iterative_diag(seed, select_fn, threshold=0.60, n_clusters=8, n_rounds=5, quota_mode="confidence"):
    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 = []
    for r in range(1, n_rounds + 1):
        # ★診断: 選択前のプール難易度分布をスナップショット
        pool_n = len(remaining_df)
        pool_mean_margin = float(remaining_df["abs_margin"].mean()) if pool_n else np.nan

        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, quota_mode)
        true_sel = remaining_df.loc[mask_sel, "label"].to_numpy()
        round_pseudo_acc = float((pseudo_sel == true_sel).mean())
        selected_mean_margin = float(remaining_df.loc[mask_sel, "abs_margin"].mean())  # ★診断

        new_texts = cur_texts + remaining_df.loc[mask_sel, "text"].tolist()
        new_labels = cur_labels + pseudo_sel.tolist()
        new_remaining_df = remaining_df.loc[~mask_sel].reset_index(drop=True)
        acc_after, _, _ = train_and_eval(new_texts, new_labels, test_df)

        cur_texts, cur_labels, remaining_df = new_texts, new_labels, new_remaining_df
        rows.append({"round": r, "pool_n_before": pool_n, "pool_mean_margin_before": pool_mean_margin,
                     "selected_mean_margin": selected_mean_margin,
                     "round_pseudo_acc": round_pseudo_acc, "test_acc": acc_after})
    return pd.DataFrame(rows)

✅ 模範解答

# make_text_dataset(abs_margin列を追加) / train_and_eval / split_seed / select_confident_pseudo_labels /
# _cluster_and_quota / select_diverse_prefilter(D) / select_diverse_prefilter_uncertainty(G) /
# run_iterative_diag はヒント3・本文参照(Day151-157の実装 + abs_margin診断を追加)

SEEDS = [0,1,2,3,4]; N_ROUNDS = 5; QUOTA_MODE = "confidence"
POPULATION_MEAN_MARGIN = 1.0  # 導出: (0*2+1*5+2*2)/9 = 1.0

configs = {"D_base": select_diverse_prefilter, "G_base": select_diverse_prefilter_uncertainty}
all_results = {name: pd.concat([run_iterative_diag(s, fn, n_rounds=N_ROUNDS, quota_mode=QUOTA_MODE).assign(seed=s) for s in SEEDS])
               for name, fn in configs.items()}

# round1->round2 (大標本の遷移) での pool_mean_margin_before
d_r1 = all_results["D_base"][all_results["D_base"]["round"]==1]["pool_mean_margin_before"].mean()
d_r2 = all_results["D_base"][all_results["D_base"]["round"]==2]["pool_mean_margin_before"].mean()
g_r1 = all_results["G_base"][all_results["G_base"]["round"]==1]["pool_mean_margin_before"].mean()
g_r2 = all_results["G_base"][all_results["G_base"]["round"]==2]["pool_mean_margin_before"].mean()

pooled = pd.concat([all_results["D_base"], all_results["G_base"]])
corr = np.corrcoef(pooled["pool_mean_margin_before"], pooled["round_pseudo_acc"])[0,1]

assert d_r2 < d_r1   # Dのプールは難化する
assert g_r2 > g_r1   # Gのプールは易化する
assert d_sel_r1 > POPULATION_MEAN_MARGIN  # Dは易しいサンプルを選ぶ
assert g_sel_r1 < POPULATION_MEAN_MARGIN  # Gは難しいサンプルを選ぶ
assert corr > 0.3
assert d_final_n < 20 and g_final_n < 20  # 最終ラウンドは少数サンプルで不安定
print("[OK] 6個のassertすべて成立")
# ============ 実測結果(実際にPython 3で実行して確認済み) ============
#  D_base  round  pool_n_before  pool_mean_margin_before  selected_mean_margin  round_pseudo_acc  test_acc
#       1        300.0                   0.9947                 1.1190            0.8674    0.6327
#       2        209.4                   0.9391                 0.9304            0.5519    0.5727
#       3         68.6                   0.9790                 1.0090            0.4188    0.5491
#       4         14.2                   0.8259                 0.8144            0.3449    0.5473
#       5          4.0                   0.9583                 0.9583            0.1528    0.5273
#
#  G_base  round  pool_n_before  pool_mean_margin_before  selected_mean_margin  round_pseudo_acc  test_acc
#       1        300.0                   0.9947                 0.8893            0.5906    0.6800
#       2        209.4                   1.0440                 1.0371            0.7018    0.6491
#       3         65.8                   1.0575                 1.0783            0.8040    0.6455
#       4          6.8                   0.9883                 0.9984            0.8032    0.6436
#       5          2.0                   0.5000                 0.5000            0.5000    0.5909
#
# [理論値との照合] round1(n=300)実測: mean=0.9947・frac_hard=0.2200・frac_easy=0.2147
#                 理論値(母集団)   : mean=1.0000・frac_hard=0.2222・frac_easy=0.2222  -> ほぼ一致
#
# [頑健な比較: round1(n=300) -> round2(n≈209)]
# D_base: pool_mean_margin_before  0.9947 -> 0.9391 (delta=-0.0556)  ← 易しいサンプルが先食いされ難化
# G_base: pool_mean_margin_before  0.9947 -> 1.0440 (delta=+0.0493)  ← 難しいサンプルが先食いされ易化
#
# [own-final-roundは不安定] 最終ラウンド開始時点の残プール件数(平均): D=4.4件, G=5.0件
# -> 5シードのpool_mean_margin_finalはD側[0.857,0.667,0.875,1.000,1.000]、G側[0.714,1.000,1.000,0.500,1.500]
#    と符号・大きさとも seed によってバラバラで、少数サンプルの平均値として信頼できない
#
# [相関] pool_mean_margin_before と round_pseudo_acc (D+G, n=44) = 0.4088
# [相関] selected_mean_margin と round_pseudo_acc (D+G, n=44) = 0.4873
#
# [OK] 6個のassertすべて成立
🎯
重要な発見1(round1→round2という頑健な窓で、仮説通り逆方向の入れ替わりを確認): 標本数が最大のround1(n=300)→round2(n≈209)で見ると、D_baseは0.9947→0.9391(難化)、G_baseは0.9947→1.0440(易化)と正反対に動いた。選択に一切使っていないabs_marginがこの向きで動いたことは、「round1が大域最適」という現象が、このデータセット固有の偶然ではなく選択基準ごとに難易度層を偏って消費する一般構造の帰結であることの裏付けになる。
⚖️
重要な発見2(「最終ラウンド」比較は標本サイズの罠だった): 最終ラウンド開始時点の残プールはD平均4.4件・G平均5.0件しかなく、そこでの平均難易度はシードごとに符号すらばらついた。「大きな主張をしたいときほど、それを支える標本サイズを先に確認すべき」という教訓が、今日は「診断指標そのものの信頼性」という形で現れた。
🔍
重要な発見3(プールの難易度構成だけでは説明しきれない残差がある): D_baseのround5はselected_mean_margin=0.9583と母集団平均並みなのに、round_pseudo_accは0.1528とコイントス(0.5)を大きく下回った。これは「客観的な難易度」ではなく、蓄積した誤りでモデルの決定境界そのものが歪んだ結果であり、プールの難易度構成の変化とは別の機構がもう1つ働いていることを示している。相関係数が0.41にとどまったのはこの残差のためだと解釈できる。

📊 結果の可視化

① Day157の未解決の問い

round1が大域最適なのはこのデータ固有か、一般構造か

② 診断専用の物差し

abs_margin=|n_pos-n_neg|を導入。選択には一切使わない

③ round1→round2で計測

大標本の遷移でD・Gのプール難易度変化を追跡

④ 結果

Dは難化(-0.056)、Gは易化(+0.049)。正反対の方向で仮説を裏付け

round1→round2での残プール難易度(pool_mean_margin_before)の変化(基準線=母集団平均1.0) 0.85 1.10 母集団平均=1.00 0.9947 D round1 0.9391 D round2(難化) 0.9947 G round1 1.0440 G round2(易化)
指標D_base(確信度優先)G_base(Uncertainty Sampling)
pool_mean_margin_before: round10.99470.9947
pool_mean_margin_before: round20.9391(難化)1.0440(易化)
selected_mean_margin: round11.1190(易しい方を選ぶ)0.8893(難しい方を選ぶ)
round1→round2の残プール件数300 → 209.4300 → 209.4
pool難易度とround_pseudo_accの相関r = 0.41(D+G合算、中程度で1には遠い)

🪜 Step-by-Step 解説

1「最終ラウンドで比較する」という自然な発想を、標本サイズの確認で退けた

d_final_n = 4.4件, g_final_n = 5.0件  # round1(300件)の1.5%程度
📋
なぜこうするか: 直感的には「round1→最終ラウンド」の比較が最も"劇的な変化"を見せてくれそうに思える。しかし残プール件数を確認すると一桁台まで減っており、そこでの平均値は数件のサンプルで決まる不安定な統計量にすぎない。「変化が大きく見える比較ほど、その裏付けとなる標本サイズを疑う」という順序を守ったことで、round1→round2という地味だが頑健な比較にたどり着けた。

2選択には使っていない指標(abs_margin)で、選択基準の"隠れた効果"を裏付ける

D_base: pool_mean_margin_before  0.9947 -> 0.9391 (round1->round2)
G_base: pool_mean_margin_before  0.9947 -> 1.0440 (round1->round2)
🧭
なぜこうするか: abs_marginは選択ロジックのコードからは一切参照されていない。この「選択に使っていない指標」がD・Gで正反対の方向に動いたという事実は、「モデルの確信度」と「データの本質的な難易度」がある程度連動しており、確信度に基づく選択が結果として難易度層の偏った消費を引き起こしていることの、選択ロジックからは独立な裏付けになっている。

3相関係数が1に近づかないことも、立派な発見として扱う

corr(pool_mean_margin_before, round_pseudo_acc) = 0.4088   # 正の相関はあるが強くはない
💡
なぜこうするか: もしこの相関が0.9のように非常に強ければ「難易度構成の変化だけで疑似ラベル精度のほぼ全てが説明できる」と主張できたかもしれない。しかし実際は0.41程度にとどまり、「難易度構成の変化」は変動の一部しか説明していない。この残差を無視せず、round5の異常値まで踏み込んで確認したことで「決定境界の歪み」という2つ目の機構の存在を積極的に示すことができた。

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

📝
「診断専用の指標を選択ロジックから切り離す」ことを「試験の採点基準を受験者本人には教えない」たとえで理解する:

受験者(D・Gの選択ロジック)には「自分がどれだけ自信を持って答えられるか」という感覚しか与えられていない。一方、採点者(今日のabs_margin)は各問題が本質的にどれくらい紛らわしいかを知っている。もし受験者の「自信」と出題者の「紛らわしさ」が実際に連動しているなら、受験者が自信のある問題ばかり選んで解いていくと、自然と紛らわしい問題が後回しになっていくはずである——これが今日確認した現象である。
📐
「相関係数が1ではなく0.4程度である」ことを「身長と体重の関係」のたとえで理解する:

身長が高い人は体重も重い傾向があり相関係数はプラスだが1ではない(体重は筋肉量や年齢にも左右される)。今日のround_pseudo_accも「プールの難易度」(身長)だけでなく「モデル自身の決定境界がどれだけ歪んでいるか」(筋肉量に相当)にも左右されている。相関係数が中程度だったという事実は、「1つの要因では説明しきれない、もう1つの隠れた要因がある」ことを教えてくれるシグナルである。

🏆 Kaggleでの実践的な使い方

よく使われるコンペカテゴリ: ☑ 自然言語処理(NLP) / ☑ 表形式データ(半教師あり学習全般に応用可)

原則具体的な活用例
モデル確信度とは独立な"真の難易度"の代理指標を用意する正解の生成過程は分からなくても、アノテーターの意見が割れやすい特徴量パターンや複数弱学習器の不一致領域を難易度の代理指標にし、確信度に基づく選択がどの難易度層を消費しているか可視化できる
少数サンプルの統計量を、大標本の統計量と同じ重みで解釈しない疑似ラベリングの後半ラウンドはプールが枯渇するのが一般的な現象であり、診断指標は標本サイズを必ず併記して報告すべき
相関係数が中程度にとどまったとき「まだ説明できていない要因がある」と捉える0.4程度の相関を「関係がある」で済ませず、残差の構造を個別事例まで掘り下げて確認する習慣が、モデルの挙動の本当の理解につながる

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
「round1→最終ラウンド」の変化量が一番大きいので、そこを主たる根拠にする変化の大きさ=発見の強さ、という直感に引きずられる最終ラウンドは標本数が一桁まで減っており(D=4.4件・G=5.0件)、統計量はシードによって符号すらばらつく。標本数が十分な遷移(round1→round2)を主たる根拠にすべき
相関係数0.4程度を見て「関係がある」で満足し、残差を調べない統計的に意味のある相関が見つかった時点で分析を終えたくなる相関が1でないことは「他の要因が残っている」という重要なシグナルである。round5の個別事例まで踏み込んだことで、決定境界の歪みという2つ目の機構を発見できた
データ生成過程に基づく「真の難易度」を、選択ロジックにそのまま組み込めば良いと考える良い指標が見つかると、すぐに実用化したくなるabs_marginは合成データでしか計算できない診断専用の指標であり、実運用では正解の生成過程がわからないため直接は使えない。位置づけはあくまで分析ツールである
選択に使っていない指標がある方向に動いたら、それだけで因果関係が証明されたと考える相関と因果を混同しやすい確認できたのは「選択基準とabs_marginの間に一定の連動がある」ことであり、機構的に説明はできるが、他の交絡要因(例えばクラスタリングの偏り)が関与している可能性も完全には排除できていない

🚀 次のステップ

  • 発展: round1のモデルとround5のモデルで同じ固定サンプル集合に対する予測確信度がどう変化したかを比較すれば、決定境界の歪みを直接可視化できる。もしround1では"確信度低"だったサンプルがround5で"確信度高だが不正解"に変わっていれば、歪みの直接証拠になる
  • 次回予告: Day159では、上記の「round1モデル vs round5モデルの予測の食い違い」を固定テストサンプル単位で追跡し、(D)のconfirmation biasが「新しい間違いを生む」のか「既存の間違いを確信度だけ強める」のかを切り分ける検証を行う予定

Phase 6 の学習マップ(NLP系コンペへの応用フェーズ継続中)

1-23
Phase6主要テーマ6項目・総復習(Day126-149、完了)
発展①〜⑨
NLP系コンペへの応用(Day157: 早期打ち切りでround1が大域最適と判明/Day158: 診断専用の真の難易度でその一般性を裏付け、決定境界の歪みという残差も発見)

Day158は「round1が最適」という現象を、選択に使っていない独立な難易度指標で裏付けつつ、それだけでは説明しきれない残差(round5の異常な精度崩壊)も同時に見つけた回。次はこの残差を、モデルの予測そのものの変化を追跡することで直接検証する。

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

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