📚 背景知識(読んでから問題へ)
(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ずつ配分 |
| remainder | n_target - (n_target // k) * k。理論上0〜k-1(=7)件 |
| expansion_ratio | gap_final / gap_round1。gapが反復で何倍に拡大したかを表す比率指標 |
🗂️ データ・分割のスキーマ
| 変数名 | 型 | 内容 |
|---|---|---|
| df (n=450) | pd.DataFrame | Day151-155と同じmake_text_dataset()で生成した合成テキスト全体 |
| test_df (110件) | pd.DataFrame | 最終評価専用のホールドアウト。全設定で共通に使う |
| labeled_df (40件) | pd.DataFrame | 最初の学習データ。ラウンドを追うごとに疑似ラベル付きサンプルが追記される |
| remaining_df | pd.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文で説明する。
remainderは最大でもn_targetの5〜8%という見積もりを踏まえ、quota配分ルールを確信度非依存に変えたときのgapの変化幅は「大きい(Day155の結論が覆るレベル)」「小さい(数値はやや動くが結論は変わらない)」のどちらだと予想されるか、理由とともに述べるタスクB(コーディング→分析)
make_text_dataset・split_seed・train_and_eval・select_confident_pseudo_labels・run_iterativeの骨格はDay151-155と同じ実装を再利用する_cluster_and_quota(..., quota_mode)に切替を追加。"confidence"は既存動作、"equal"は確信度を参照せずクラスタID順に余りを配分するselect_diverse_prefilter(D)・select_diverse_prefilter_uncertainty(G)・run_iterativeにquota_modeを引き渡せるようにするquota_mode∈{"confidence","equal"}×select_fn∈{D,G}の4通りを5シード・最大5ラウンドで実行し、Day155と同じown-final-round比較(gap_round1・gap_final・reversed件数)を計算するquota_mode="equal"でもown-final-roundでG_iterがD_iterを上回ること (b) 両手法ともown-final-roundの平均test精度がround1を下回ること、をassert文で確認するgap_round1・gap_finalの絶対値の差、expansion_ratio(=gap_final/gap_round1)の大小、reversed件数の一致・不一致を比較するremainderの絶対件数の上限という視点と関連づけて述べる💡 ヒント
_cluster_and_quotaのうちquota = n_target // kの部分はconfidence/equalどちらのモードでも共通で、変わるのはremainder(0〜7件)の配り先だけである。「大部分の枠は最初からconfidenceに関係なく均等配分されており、quota_modeが影響するのは全体のごく一部」という構造を理解してからタスクAの予想を立てるとよい。
_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は分母が小さくノイズが大きいことを念頭に置く
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の優位性は消えていない
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配分ルールという副次的な設計選択に依存していなかったことが確認できた。📊 結果の可視化
① 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の方が大きい逆転現象も発見
| 指標 | confidence(Day152-155由来) | equal(Day156: 確信度非依存) | 解釈 |
|---|---|---|---|
| gap_round1(G-D、1回だけの比較) | +0.0473 | +0.0345 | equalの方が0.0127pt小さい |
| gap_final(G-D、own-final-round) | +0.1000 | +0.0909 | equalの方が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件
round_pseudo_accのような比率指標は、分母のn_addedが小さいときに極端な値(0.0や1.0)を取りやすく、統計的な裏付けが乏しい。比率の指標を報告するときは、必ずその分母(サンプルサイズ)を併記し、分母が小さい回の数値は参考値として扱うべきである。🧮 数学・統計の補足(文系向け)
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系コンペへの応用フェーズ継続中)
Day156は「結果を動かしそうな別の設計選択を変えてみたが、実は主結論を覆すほどの影響力はなかった」ことを、事前の上限計算と実測の両方で裏付けたロバストネス確認の回。次回はいよいよ(D)側への介入(early stopping)で、劣化そのものを緩和できるかを検証する。
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: