Day 156 — NLP系コンペへの応用⑦ — クラスタのquota配分ルールを確信度非依存にすると、gapはどう変わるのか

2026-09-15 金 / Phase 6(Grandmaster、総復習後の発展回) コーディング→分析 quota配分ルールを切替 / gapは1〜1.3pt縮むが質的結論は不変

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

🔁
Day155では、クラスタ内選択基準を「確信度優先(D)」から「Uncertainty Sampling(G)」に変えると、Iterative Pseudo Labelingでgap(G-D)がround1の+0.0473からown-final-roundで+0.1000へと2倍以上に拡大することを発見した。ただしこの実験は、クラスタへの取得件数(quota)の余り(remainder)を「平均確信度が高いクラスタから優先的に+1件ずつ」配分するDay152由来のルールを固定したままだった。

(D)にとってこの旧ルールは「確信度優先」という自身の方針と一致し均質化を後押ししうる一方、(G)にとっては「クラスタ内は不確実性優先なのに枠配分は確信度優先」という方針の食い違いになっている。今日はこのquota配分ルールを確信度に一切依存しない均等配分に変え、Day155のgap拡大がこの副次的な設計選択にどれだけ依存していたのかを実測する。

🧮
実験前の見積もり: remainderはn_target mod k(k=8)なので理論上最大でも7件。round1〜2のn_targetは平均90〜144件あるため、quota配分ルールが影響できる範囲は1ラウンドあたり最大でも約5〜8%に限られる。この上限を先に計算しておくことで、結果を見る前に「変化は小さいはず」という当たりをつけられる。
用語意味
quota_mode="confidence"Day152〜155と同じ。余り(remainder)を平均確信度が高いクラスタから+1ずつ配分
quota_mode="equal"Day156で追加。確信度を一切参照せず、クラスタID順に余りを+1ずつ配分
remaindern_target - (n_target // k) * k。理論上0〜k-1(=7)件
expansion_ratiogap_final / gap_round1。gapが反復で何倍に拡大したかを表す比率指標

🗂️ データ・分割のスキーマ

変数名型内容
df (n=450)pd.DataFrameDay151-155と同じmake_text_dataset()で生成した合成テキスト全体
test_df (110件)pd.DataFrame最終評価専用のホールドアウト。全設定で共通に使う
labeled_df (40件)pd.DataFrame最初の学習データ。ラウンドを追うごとに疑似ラベル付きサンプルが追記される
remaining_dfpd.DataFrameまだ採用していない未ラベルプール。ラウンドごとに採用分が除外され縮小する
threshold    = 0.60  # 各ラウンドのn_addedを決める基準(Day151-155と同一)
n_clusters   = 8     # KMeansのクラスタ数(D_iter/G_iter・両quota_mode共通)
n_rounds     = 5     # 最大ラウンド数(採用0件になれば早期終了)
quota_mode   = "confidence" | "equal"  # ★Day156で追加した切替パラメータ

🎯 問題

タスクA(理論)

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

1
remainderは最大でもn_targetの5〜8%という見積もりを踏まえ、quota配分ルールを確信度非依存に変えたときのgapの変化幅は「大きい(Day155の結論が覆るレベル)」「小さい(数値はやや動くが結論は変わらない)」のどちらだと予想されるか、理由とともに述べる
2
(D)と(G)のどちらが、quota配分ルールの変更による影響をより強く受けると予想されるか(あるいは両者同程度と予想されるか)を、Day155の「(D)は確信度優先で選び続けることで均質化する」という発見と関連づけて予想する

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

B1
make_text_dataset・split_seed・train_and_eval・select_confident_pseudo_labels・run_iterativeの骨格はDay151-155と同じ実装を再利用する
B2
_cluster_and_quota(..., quota_mode)に切替を追加。"confidence"は既存動作、"equal"は確信度を参照せずクラスタID順に余りを配分する
B3
select_diverse_prefilter(D)・select_diverse_prefilter_uncertainty(G)・run_iterativeにquota_modeを引き渡せるようにする
B4
quota_mode∈{"confidence","equal"}×select_fn∈{D,G}の4通りを5シード・最大5ラウンドで実行し、Day155と同じown-final-round比較(gap_round1・gap_final・reversed件数)を計算する
B5
(a) quota_mode="equal"でもown-final-roundでG_iterがD_iterを上回ること (b) 両手法ともown-final-roundの平均test精度がround1を下回ること、をassert文で確認する
B6
追加診断: confidence/equalでgap_round1・gap_finalの絶対値の差、expansion_ratio(=gap_final/gap_round1)の大小、reversed件数の一致・不一致を比較する
B7
(考察): タスクAの2つの予想が実測とどの程度一致したかを、remainderの絶対件数の上限という視点と関連づけて述べる

💡 ヒント

ヒント1方向性

_cluster_and_quotaのうちquota = n_target // kの部分はconfidence/equalどちらのモードでも共通で、変わるのはremainder(0〜7件)の配り先だけである。「大部分の枠は最初からconfidenceに関係なく均等配分されており、quota_modeが影響するのは全体のごく一部」という構造を理解してからタスクAの予想を立てるとよい。

ヒント2アプローチ
  • _cluster_and_quotaにif quota_mode == "confidence": ... elif quota_mode == "equal": ...の分岐を追加するだけでよく、選択関数・run_iterativeは引数を1つ追加して素通しするだけでよい
  • equalモードの余り配分はfor c in range(remainder): cluster_quota[c] += 1のように確信度を一切参照せずクラスタID順に割り振る
  • gap_final / gap_round1の拡大率を計算する際、gap_round1が小さいほど比率は不安定・過大に出やすい点に注意する
  • ラウンド終盤(round4〜5)は採用件数(n_added)が数件まで減ることがあるため、その回のround_pseudo_accは分母が小さくノイズが大きいことを念頭に置く
ヒント3コード骨格
def _cluster_and_quota(X_un, proba, n_target, n_clusters, seed, quota_mode="confidence"):
    n = X_un.shape[0]
    n_target = min(n_target, n)
    k = min(n_clusters, n)
    km = KMeans(n_clusters=k, random_state=seed, n_init=10)
    cluster_ids = km.fit_predict(X_un)
    confidence = np.abs(proba - 0.5)
    quota = n_target // k
    remainder = n_target - quota * k
    cluster_quota = {c: quota for c in range(k)}
    if quota_mode == "confidence":
        cluster_conf_rank = pd.Series(confidence, index=cluster_ids).groupby(level=0).mean().sort_values(ascending=False)
        for c in list(cluster_conf_rank.index)[:remainder]:
            cluster_quota[c] += 1
    elif quota_mode == "equal":
        for c in range(remainder):   # ★確信度を一切参照しない
            cluster_quota[c] += 1
    else:
        raise ValueError(quota_mode)
    return cluster_ids, cluster_quota, k, n_target, confidence

✅ 模範解答

# make_text_dataset / train_and_eval / split_seed / select_confident_pseudo_labels は
# Day151-155と同一(省略)

def _cluster_and_quota(X_un, proba, n_target, n_clusters, seed, quota_mode="confidence"):
    n = X_un.shape[0]; n_target = min(n_target, n); k = min(n_clusters, n)
    km = KMeans(n_clusters=k, random_state=seed, n_init=10)
    cluster_ids = km.fit_predict(X_un)
    confidence = np.abs(proba - 0.5)
    quota = n_target // k; remainder = n_target - quota * k
    cluster_quota = {c: quota for c in range(k)}
    if quota_mode == "confidence":
        rank = pd.Series(confidence, index=cluster_ids).groupby(level=0).mean().sort_values(ascending=False)
        for c in list(rank.index)[:remainder]: cluster_quota[c] += 1
    elif quota_mode == "equal":
        for c in range(remainder): cluster_quota[c] += 1   # 確信度非依存
    else: raise ValueError(quota_mode)
    return cluster_ids, cluster_quota, k, n_target, confidence

# select_diverse_prefilter(D)・select_diverse_prefilter_uncertainty(G) は
# quota_modeを_cluster_and_quotaへ素通しするだけ(Day155のコードに引数を1つ追加、省略)
# run_iterativeも同様にquota_modeを素通しする(省略)

SEEDS = [0, 1, 2, 3, 4]; N_ROUNDS = 5
results = {}
for quota_mode in ["confidence", "equal"]:
    all_d, all_g = [], []
    for s in SEEDS:
        df_d = run_iterative(s, select_diverse_prefilter, quota_mode, n_rounds=N_ROUNDS); df_d["seed"]=s; all_d.append(df_d)
        df_g = run_iterative(s, select_diverse_prefilter_uncertainty, quota_mode, n_rounds=N_ROUNDS); df_g["seed"]=s; all_g.append(df_g)
    results[quota_mode] = (pd.concat(all_d, ignore_index=True), pd.concat(all_g, ignore_index=True))

summary = {}
for quota_mode, (df_d_all, df_g_all) in results.items():
    mean_d_r1 = df_d_all[df_d_all["round"]==1]["test_acc"].mean()
    mean_g_r1 = df_g_all[df_g_all["round"]==1]["test_acc"].mean()
    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_final, mean_g_final = df_final["d_final_acc"].mean(), df_final["g_final_acc"].mean()

    assert mean_g_final > mean_d_final, f"[{quota_mode}] own-final-roundでG_iterがD_iterを上回っていません"
    assert mean_d_final < mean_d_r1, f"[{quota_mode}] D_iterがconfirmation biasで劣化していません"
    assert mean_g_final < mean_g_r1, f"[{quota_mode}] G_iterがconfirmation biasで劣化していません"

    summary[quota_mode] = {"gap_round1": df_final["gap_round1"].mean(), "gap_final": df_final["gap_final"].mean(),
                            "reversed": int(df_final["reversed"].sum())}
    print(f"[OK: {quota_mode}] own-final-roundでもG_iterの優位性は消えていない")
# ============ 実測結果(実際にPython 3で実行して確認済み) ============
# [quota_mode="confidence"(Day155と同一の再現実行)]
#  round  n_added(D)  round_pseudo_acc(D)  test_acc(D)  |  n_added(G)  round_pseudo_acc(G)  test_acc(G)
#      1        90.6              0.867442     0.632727 |      90.6               0.590581     0.680000
#      2       140.8              0.551895     0.572727 |     143.6               0.701816     0.649091
#      3        54.4              0.418792     0.549091 |      59.0               0.804038     0.645455
#      4        11.8              0.344933     0.547273 |       6.4               0.803175     0.643636
#      5         4.0              0.152778     0.527273 |       2.0               0.500000     0.590909
# gap_round1=+0.0473 / gap_final=+0.1000 / reversed=1/5(seed3) / expansion_ratio=2.114
#
# [quota_mode="equal"(Day156: 確信度非依存の均等配分)]
#  round  n_added(D)  round_pseudo_acc(D)  test_acc(D)  |  n_added(G)  round_pseudo_acc(G)  test_acc(G)
#      1        90.6              0.855373     0.621818 |      90.6               0.591109     0.656364
#      2       143.2              0.552967     0.572727 |     142.6               0.709357     0.643636
#      3        52.4              0.378164     0.549091 |      58.4               0.801146     0.636364
#      4        11.6              0.256731     0.547273 |       7.8               0.863333     0.638182
#      5         5.0              0.416667     0.531818 |       1.5               1.000000     0.672727  ← n_added=1.5件、ノイズ大
# gap_round1=+0.0345 / gap_final=+0.0909 / reversed=1/5(seed3) / expansion_ratio=2.635
#
# [quota_modeどうしの比較]
# gap_round1: confidence +0.0473 -> equal +0.0345(-0.0127pt)
# gap_final : confidence +0.1000 -> equal +0.0909(-0.0091pt)
# 拡大率(expansion_ratio): confidence 2.11倍 -> equal 2.63倍(拡大率としてはequalの方が大きい)
# reversed件数: 両quota_modeともseed3のみ 1/5 で一致
# mean_d: confidence 0.6327->0.5455(-13.8%) / equal 0.6218->0.5473(-12.0%)
# mean_g: confidence 0.6800->0.6455(-5.1%)  / equal 0.6564->0.6382(-2.8%)
#
# [OK: confidence] own-final-roundでもG_iterの優位性は消えていない
# [OK: equal]      own-final-roundでもG_iterの優位性は消えていない
🎯
重要な発見1(事前の上限計算が実測とよく一致した): remainderは最大でもn_targetの5〜8%という上限から立てた「変化は小さいはず」という予想通り、gap_round1・gap_finalはともに1〜1.3ポイントしか動かなかった。Gがown-final-roundでDを上回るという結論(assert)、reversed件数(seed3のみ1/5)は両quota_modeで完全に一致し、Day155の質的結論はquota配分ルールという副次的な設計選択に依存していなかったことが確認できた。
⚖️
重要な発見2(拡大率だけを見ると逆の印象を受ける): gapの絶対値はequalモードの方が小さいのに、拡大率(gap_final/gap_round1)はconfidence(2.11倍)よりequal(2.63倍)の方が大きい。これはgap_round1という分母がequalの方が小さいことによる比率の見かけ上の効果であり、絶対値と比率のどちらで語るかによって「どちらのquota_modeがより不安定か」の印象が逆転しうることを示した。
🔍
重要な発見3(round5のpseudo_acc=1.000は額面通りに受け取れない): equalモードのround5でGのround-local疑似ラベル精度が1.000を記録したが、その回の採用件数は平均1.5件しかない。分母(採用件数)が極端に小さい回の精度指標はノイズが大きく、他のラウンドと同列に比較すべきではない。

📊 結果の可視化

① Day155の発見

gap(G-D)はround1→own-final-roundで+0.047→+0.100へ2倍以上に拡大

② 見積もり

remainderは最大7件・n_targetの5〜8%。影響は小さいと予想

③ quota_mode切替

confidence(既存) vs equal(確信度非依存)で4通り×5シードを実行

④ 結果

gapは1〜1.3pt縮小するが質的結論は不変。拡大率だけequalの方が大きい逆転現象も発見

gap(G-D) の比較:round1 vs own-final-round(quota_mode別) 0.00 0.11 0.0473 0.1000 confidence 0.0345 0.0909 equal gap_round1(1回だけの比較、Day154と同型) gap_final(own-final-round、反復後)
指標confidence(Day152-155由来)equal(Day156: 確信度非依存)解釈
gap_round1(G-D、1回だけの比較)+0.0473+0.0345equalの方が0.0127pt小さい
gap_final(G-D、own-final-round)+0.1000+0.0909equalの方が0.0091pt小さいが、両方とも明確に正
expansion_ratio(gap_final/gap_round1)2.114倍2.635倍拡大"率"だけ見るとequalの方が大きい(分母効果)
reversed件数(round1劣勢→final優勢)両quota_modeともseed3のみ(1/5)で完全一致質的結論はquota配分ルールに依存しない
mean_dの劣化率 / mean_gの劣化率-13.8% / -5.1%-12.0% / -2.8%equalの方がD・Gともわずかに劣化が緩やか

🪜 Step-by-Step 解説

1「影響は小さいはず」という見積もりが、実測でほぼそのまま裏付けられた

remainder上限        = n_clusters - 1 = 7件
round1〜2のn_target   = 90〜144件
理論上の最大影響割合  = 7 / 90 ≈ 7.8%(最も影響が大きいラウンドでも1割未満)

gap_round1: confidence +0.0473 -> equal +0.0345   (差 -0.0127pt)
gap_final : confidence +0.1000 -> equal +0.0909   (差 -0.0091pt)
📋
なぜこうするか: _cluster_and_quotaのquota * k件(大部分)はconfidence/equalどちらのモードでも全く同じ計算をしており、両モードの違いはremainder(0〜7件)の配り先だけである。この構造を先に把握しておけば、「quota配分ルールを変えたら劇的に結果が変わるのでは」という過大な期待を持たずに済む。コードを書く前に「この変更が動かせる範囲の上限」を計算しておくことは、実験結果を解釈するときの物差しになる。

2絶対値では縮んだのに、拡大率では逆に大きくなった理由

gap_round1(confidence) = 0.0473, gap_final(confidence) = 0.1000  -> 拡大率 2.114倍
gap_round1(equal)      = 0.0345, gap_final(equal)      = 0.0909  -> 拡大率 2.635倍
拡大幅(gap_final - gap_round1): confidence = +0.0527pt, equal = +0.0564pt(拡大幅自体はequalの方がわずかに大きい)
💡
なぜこうするか: 拡大率はgap_final / gap_round1という比の指標であり、分母(gap_round1)が小さくなると同じ拡大幅でも比率は大きく出る。今回はequalモードの方が拡大幅(+0.0564pt)自体もconfidenceモード(+0.0527pt)よりわずかに大きく、かつ分母も小さいため、拡大率が2.11倍→2.63倍へと余計に強調される結果になった。「何倍に拡大したか」という比率だけで語ると、分母の違いによって実態以上に大きな変化に見えることがあるため、絶対値(ポイント差)と比率の両方を併記して報告することが望ましい。

3round5のpseudo_acc=1.000を鵜呑みにしないための確認手順

equalモード round5: G_iter round_pseudo_acc = 1.000, n_added(平均) = 1.5件
equalモード round4: G_iter round_pseudo_acc = 0.863, n_added(平均) = 7.8件
🧭
なぜこうするか: 反復実験ではラウンドが進むほどプールが枯渇し、採用件数が一桁台まで減ることがある(Day155でも同様の現象があった)。round_pseudo_accのような比率指標は、分母のn_addedが小さいときに極端な値(0.0や1.0)を取りやすく、統計的な裏付けが乏しい。比率の指標を報告するときは、必ずその分母(サンプルサイズ)を併記し、分母が小さい回の数値は参考値として扱うべきである。

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

📝
「remainderは最大7件」という上限計算を「割り勘の端数」のたとえで理解する:

90人分のランチ代をちょうど8つのテーブルに均等に割り振ろうとすると、1テーブルあたり11人ずつ座れて、余りは90 - 11×8 = 2人だけ生まれる。この「余りの2人をどのテーブルに追加で座らせるか」というルールをどう変えても、影響を受けるのは90人中たった2人分でしかなく、全体の配置がひっくり返るようなことは起こりにくい。今日の実験のremainder(最大7件)も同じ構造で、「余りの配り方」という一見細かいルールが、全体の結論を覆すほどの影響力を持たないことを、件数の上限を先に計算しておくことで見積もれる。
📊
「絶対値の変化」と「比率の変化」が食い違う現象を「値引き額と割引率」のたとえで理解する:

1000円の商品が900円になった(100円引き、10%オフ)場合と、10円の商品が5円になった(5円引き、50%オフ)場合を比べると、「値引き額」は前者の方が大きいのに「割引率」は後者の方が大きい。今日のgap_round1とgap_finalの関係も同様で、「ポイント差(値引き額に相当)」で見るか「何倍か(割引率に相当)」で見るかによって、どちらのquota_modeの方が"変化が大きい"と感じるかが逆転する。片方の指標だけで結論を語らないことが重要である。

🏆 Kaggleでの実践的な使い方

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

原則

📐 副次的パラメータは影響範囲の上限を先に計算する

実験前に当たりをつける

「remainderは最大でもn_clusters-1件」のような上限を先に計算しておけば、実験結果を見たときに「予想通り小さい変化だった」のか「予想に反して大きく動いた」のかをすぐに判断できる。チューニング候補が多いとき、どのパラメータから検証すべきかの優先順位づけにも使える。

実例

⚖️ 絶対値と比率の両方を報告する

分母効果で印象が逆転しうる

Discussionやwrite-upで改善効果を語るとき、「+0.01ポイント改善」と「2.6倍に拡大」のどちらか一方だけを見せると誤った印象を与えることがある。分母が小さくなる場面では特に、両方の指標を併記する習慣が誠実な報告につながる。

実務

🔬 終盤ラウンド・少数サンプルの指標は参考値扱い

分母を必ず併記する

Iterative Pseudo Labelingやearly-stoppingを伴うパイプラインでは、末尾のラウンドほど採用件数が減り、その回の精度指標は信頼区間が広い。ログに採用件数と精度を並べて記録し、少数サンプルの回を自動的に警告する仕組みを入れておくと誤った結論を避けられる。

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
「余りの配分ルール」のような細部を変えれば、結果も大きく変わるはずだと考えるパラメータを1つ変えれば結果も比例して大きく動くはず、という直感が働く影響範囲の理論的な上限(remainderは最大でもn_clusters-1件、全体の5〜8%程度)を先に計算しておけば、変化が小さいことは実験前から予想できる。コードを書く前に「この変更がどこまで動かせるか」を見積もる価値がある
拡大率(何倍か)だけを見て「equalモードの方が悪化がひどい」と結論づける比率という単一の指標だけで変化の大小を判断しがちである拡大率は分母(gap_round1)が小さいと過大に見える。絶対値(ポイント差)ではequalモードの方がgapは小さいままであり、比率と絶対値のどちらで語るかで結論が逆に見えることがある
反復実験の最終ラウンドの精度指標(100%など)をそのまま信頼する数値だけを見ると「完璧な精度」のように見えて印象に残りやすい終盤ラウンドは採用件数が一桁まで減ることがあり、その回の精度は分母が小さくノイズが大きい。必ず採用件数(分母)と一緒に確認する習慣が必要である
quota配分ルールのような「選択の周辺部分」の設計は、選択基準そのもの(D/Gの違い)と同程度に結果を左右すると考えるどちらも「選び方」に関わるルールなので、同格の影響力を持つと錯覚しやすい今日の実測が示す通り、クラスタ内の選択基準(D vs G)がgapに与える影響(Day154: +0.047)は、quota配分ルールの違いが与える影響(今日: 最大でも0.013pt程度)よりも一桁近く大きい。同じ「選び方」でも影響力の大きさは階層が異なる

🚀 次のステップ

  • 発展: 今日はn_clusters=8を固定していたが、n_clusters自体を4や16に変えるとremainderの絶対上限(n_clusters-1)自体が変わるため、quota配分ルールの影響力がより大きく出る可能性がある(n_clustersが大きいほどremainderの上限も大きくなる)
  • 次回予告: Day157では、Day155で発見した(D)の「採用件数急増×正解率急落」という悪循環の早期警告シグナル(round-local疑似ラベル精度や採用件数の急変)を使い、(D)側にラウンドの早期打ち切りルール(early stopping)を導入した場合、(D)の劣化がどこまで緩和できるか、その結果G_iterとのgapがどう変わるかを検証する

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

1-23
Phase6主要テーマ6項目・総復習(Day126-149、完了)
発展①〜⑦
NLP系コンペへの応用(Day154: 単発の改善/Day155: Iterative化でも拡大/Day156: quota配分ルールという副次パラメータの影響上限を実測し、質的結論の頑健性を再確認)

Day156は「結果を動かしそうな別の設計選択を変えてみたが、実は主結論を覆すほどの影響力はなかった」ことを、事前の上限計算と実測の両方で裏付けたロバストネス確認の回。次回はいよいよ(D)側への介入(early stopping)で、劣化そのものを緩和できるかを検証する。

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

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