📚 背景知識(読んでから問題へ)
今日はDay153の「発展」候補どおり、クラスタリング対象(全体か候補プールか)ではなく、クラスタ内の選択基準そのものを変更する。(F)クラスタ内ランダム選択と(G)クラスタ内Uncertainty Sampling(確信度が0.5に近い順)の2方式を新たに実装し、Day153のpre-filter方式(D)・同数ランダム選択+自己ラベル(E)と比較する。
| 方式 | クラスタ内の選び方 |
|---|---|
| (D) pre-filter+確信度優先 | Day153。クラスタ内で確信度が高い順にquotaを埋める |
| (F) pre-filter+クラスタ内ランダム選択 | 今日の新方式①。クラスタ内でランダムな順序でquotaを埋める |
| (G) pre-filter+クラスタ内Uncertainty Sampling | 今日の新方式②。クラスタ内で確信度が低い(0.5に近い)順にquotaを埋める |
🗂️ データ・分割のスキーマ
| 変数名 | 型 | 内容 |
|---|---|---|
| df (n=450) | pd.DataFrame | Day151-153と同じmake_text_dataset()で生成した合成テキスト全体 |
| test_df (110件) | pd.DataFrame | 最終評価専用のホールドアウト。全設定で共通に使う |
| labeled_df (40件) | pd.DataFrame | 「ラベル付きデータが少ない」状況を模したベースライン学習セット |
| unlabeled_df (約300件) | pd.DataFrame | ラベルを隠した疑似ラベル候補プール。今日もこの全体がクラスタリング対象 |
threshold = 0.60 # n_addedを決める基準(Day151-153と同一) n_clusters = 8 # KMeansのクラスタ数(D/F/G共通) # 今日変えるのは「クラスタリング対象」ではなく「クラスタ内の選択順序」だけ
🎯 問題
タスクA(理論)
以下2点をそれぞれ2〜3文で説明する。
タスクB(コーディング→分析)
make_text_dataset・split_seed・train_and_eval・select_confident_pseudo_labels・select_diverse_prefilter(D、Day153)はそのまま再利用するselect_diverse_prefilter_random(...) — クラスタリング対象は未ラベル全体のまま、クラスタ内の選択順序をランダムにする(np.random.RandomState(seed).permutation(n))select_diverse_prefilter_uncertainty(...) — クラスタ内は確信度が低い(np.abs(proba-0.5)昇順)順にquotaを埋めるthreshold=0.60・n_clusters=8で、5シードにわたり (A) 確信度固定選択, (D) pre-filter確信度優先, (E) 同数ランダム+自己ラベル, (F) クラスタ内ランダム, (G) クラスタ内Uncertaintyの5パターンを比較するassert文で確認する💡 ヒント
タスクAの1.は、Day153のヒント1「クラスタ内で確信度が高い順に選ぶというルールを維持する限り、あるクラスタの中に確信度0.55以上のサンプルが既にquota分以上存在すれば、そのクラスタから低確信度サンプルが選ばれることは一切ない」という指摘を裏返せばよい。選択順序自体をランダムにする、あるいは確信度の低い順にすれば、そのクラスタの中身の構成に関わらず、低確信度サンプルにも選ばれる機会が生まれる。
- クラスタ内ランダム選択は、
order = np.argsort(-confidence)の代わりにnp.random.RandomState(seed).permutation(n)で生成した順序でループを回せばよい - Uncertainty Samplingは
order = np.argsort(confidence)(-を付けない)にするだけで、確信度が低い順に並び替えられる - overlap計算・平均確信度計算はDay153と同じパターン(
&でAND結合して.sum()、confidence[mask]の平均)を使う - quota配分ロジックは
_cluster_and_quotaのような共通関数に切り出すと3つの選択関数で使い回せる
def select_diverse_prefilter_uncertainty(model, vectorizer, unlabeled_df, n_target, n_clusters, seed): X_un = vectorizer.transform(unlabeled_df["text"]) proba = model.predict_proba(X_un)[:, 1] n = len(unlabeled_df) 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) order = np.argsort(confidence) # ★確信度が低い(0.5に近い)順に並べる(Day153は-confidenceで降順だった) quota = n_target // k remainder = n_target - quota * k cluster_quota = {c: quota for c in range(k)} # ... quota配分・クラスタ内選択のループはDay153と同じ ... selected_idx = np.sort(np.array(selected, dtype=int)) mask = np.zeros(n, dtype=bool) mask[selected_idx] = True pseudo_labels = (proba[mask] >= 0.5).astype(int) # mask経由で取得 return mask, pseudo_labels
✅ 模範解答
# POSITIVE_WORDS / NEGATIVE_WORDS / NEUTRAL_WORDS / make_text_dataset / train_and_eval / # split_seed / select_confident_pseudo_labels / select_diverse_prefilter は # Day151-153と同一(省略) def _cluster_and_quota(X_un, proba, n_target, n_clusters, seed): """3方式(D/F/G)が共有するクラスタリング+quota配分ロジック""" 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)} 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 return cluster_ids, cluster_quota, k, n_target, confidence def select_diverse_prefilter_random(model, vectorizer, unlabeled_df, n_target, n_clusters, seed): """Day154 (F): pre-filter clustering + クラスタ内はランダムに選ぶ""" X_un = vectorizer.transform(unlabeled_df["text"]) proba = model.predict_proba(X_un)[:, 1] n = len(unlabeled_df) cluster_ids, cluster_quota, k, n_target, confidence = _cluster_and_quota(X_un, proba, n_target, n_clusters, seed) rng = np.random.RandomState(seed) order = rng.permutation(n) # ★クラスタ内選択順をランダムにする taken = {c: 0 for c in range(k)} selected = [] for i in order: c = cluster_ids[i] if taken[c] < cluster_quota[c]: selected.append(i) taken[c] += 1 if len(selected) >= n_target: break selected_idx = np.sort(np.array(selected, dtype=int)) mask = np.zeros(n, dtype=bool) mask[selected_idx] = True pseudo_labels = (proba[mask] >= 0.5).astype(int) return mask, pseudo_labels def select_diverse_prefilter_uncertainty(model, vectorizer, unlabeled_df, n_target, n_clusters, seed): """Day154 (G): pre-filter clustering + クラスタ内はUncertainty Sampling(確信度が0.5に近い順)""" X_un = vectorizer.transform(unlabeled_df["text"]) proba = model.predict_proba(X_un)[:, 1] n = len(unlabeled_df) cluster_ids, cluster_quota, k, n_target, confidence = _cluster_and_quota(X_un, proba, n_target, n_clusters, seed) order = np.argsort(confidence) # ★確信度が低い(0.5に近い)順 = 不確実性が高い順 taken = {c: 0 for c in range(k)} selected = [] for i in order: c = cluster_ids[i] if taken[c] < cluster_quota[c]: selected.append(i) taken[c] += 1 if len(selected) >= n_target: break selected_idx = np.sort(np.array(selected, dtype=int)) mask = np.zeros(n, dtype=bool) mask[selected_idx] = True pseudo_labels = (proba[mask] >= 0.5).astype(int) return mask, pseudo_labels def run_comparison(seed, threshold=0.60, n_clusters=8): labeled_df, unlabeled_df, test_df = split_seed(seed) acc_base, _, vectorizer, model = train_and_eval( labeled_df["text"].tolist(), labeled_df["label"].tolist(), test_df) mask_conf, pseudo_conf = select_confident_pseudo_labels(model, vectorizer, unlabeled_df, threshold) n_added = int(mask_conf.sum()) acc_conf, _, _, _ = train_and_eval( labeled_df["text"].tolist() + unlabeled_df.loc[mask_conf, "text"].tolist(), labeled_df["label"].tolist() + pseudo_conf.tolist(), test_df) # (D) Day153: pre-filter diversity(クラスタ内は確信度優先) mask_pre, pseudo_pre = select_diverse_prefilter(model, vectorizer, unlabeled_df, n_added, n_clusters, seed) acc_pre, _, _, _ = train_and_eval( labeled_df["text"].tolist() + unlabeled_df.loc[mask_pre, "text"].tolist(), labeled_df["label"].tolist() + pseudo_pre.tolist(), test_df) # (F) Day154: クラスタ内ランダム選択 mask_f, pseudo_f = select_diverse_prefilter_random(model, vectorizer, unlabeled_df, n_added, n_clusters, seed) acc_f, _, _, _ = train_and_eval( labeled_df["text"].tolist() + unlabeled_df.loc[mask_f, "text"].tolist(), labeled_df["label"].tolist() + pseudo_f.tolist(), test_df) # (G) Day154: クラスタ内Uncertainty Sampling mask_g, pseudo_g = select_diverse_prefilter_uncertainty(model, vectorizer, unlabeled_df, n_added, n_clusters, seed) acc_g, _, _, _ = train_and_eval( labeled_df["text"].tolist() + unlabeled_df.loc[mask_g, "text"].tolist(), labeled_df["label"].tolist() + pseudo_g.tolist(), test_df) # 追加診断: overlap overlap_df = int((mask_pre & mask_f).sum()) overlap_dg = int((mask_pre & mask_g).sum()) return {"seed": seed, "acc_base": acc_base, "acc_conf": acc_conf, "acc_pre": acc_pre, "acc_f": acc_f, "acc_g": acc_g, "overlap_df_frac": overlap_df / n_added, "overlap_dg_frac": overlap_dg / n_added} SEEDS = [0, 1, 2, 3, 4] df = pd.DataFrame([run_comparison(s) for s in SEEDS]) mean_acc_pre = df["acc_pre"].mean() mean_acc_g = df["acc_g"].mean() assert mean_acc_g > mean_acc_pre, "(G)が(D)のtest精度を上回っていません" print("[OK] クラスタ内Uncertainty Samplingはpre-filter(確信度優先)より高いtest精度を示した")
# ============ 実測結果(実際にPython 3で実行して確認済み) ============
# seed n_added acc_base acc_conf acc_pre acc_rand acc_f acc_g pseudo_acc_pre pseudo_acc_rand pseudo_acc_f pseudo_acc_g overlap_df_frac overlap_dg_frac
# 0 94 0.763636 0.581818 0.600000 0.709091 0.736364 0.663636 0.882979 0.723404 0.723404 0.585106 0.340426 0.000000
# 1 125 0.727273 0.590909 0.618182 0.600000 0.590909 0.636364 0.856000 0.672000 0.688000 0.584000 0.416000 0.000000
# 2 81 0.781818 0.645455 0.654545 0.700000 0.700000 0.745455 0.925926 0.740741 0.728395 0.567901 0.283951 0.000000
# 3 75 0.736364 0.672727 0.681818 0.690909 0.663636 0.618182 0.813333 0.760000 0.733333 0.613333 0.240000 0.000000
# 4 78 0.700000 0.627273 0.609091 0.700000 0.672727 0.736364 0.858974 0.717949 0.692308 0.602564 0.256410 0.000000
#
# [まとめ]
# mean acc_base = 0.7418
# mean acc_pre (D: Day153 pre-filter, 確信度優先) = 0.6327 (pseudo_acc=0.8674)
# mean acc_rand (E: 同数ランダム+自己ラベル) = 0.6800 (pseudo_acc=0.7228)
# mean acc_f (F: クラスタ内ランダム選択) = 0.6727 (pseudo_acc=0.7131)
# mean acc_g (G: クラスタ内Uncertainty Sampling) = 0.6800 (pseudo_acc=0.5906)
# acc_f - acc_pre = +0.0400 acc_g - acc_pre = +0.0473 acc_g - acc_rand = 0.0000(完全一致)
# mean overlap_df_frac(DとF) = 0.3074
# mean overlap_dg_frac(DとG) = 0.0000(5シード全てで重なりゼロ)
#
# [OK] クラスタ内Uncertainty Samplingはpre-filter(確信度優先)より高いtest精度を示した
📊 結果の可視化
① Day151 オラクル
正解ラベルを使っても選択バイアスは緩和されない
② Day152-153 対象を広げる
候補プール内→全体、クラスタリング対象を変えても効果なし
③ 原因診断
overlap 84.3%。クラスタ内選択ルール(確信度優先)が効果を打ち消していた
④ Day154 基準を変える
クラスタ内をランダム/Uncertaintyに変更 → 初めて明確な改善
| 設定 | 平均accuracy | 疑似ラベル精度 | 解釈 |
|---|---|---|---|
| baseline(40件のみ) | 0.7418 | — | 追加データなし |
| (E) ランダム選択+自己ラベル(Day153) | 0.6800 | 0.7228 | 単純ランダムでも十分強い基準線 |
| (G) クラスタ内Uncertainty Sampling【今日】 | 0.6800 | 0.5906(最低) | (E)と同率、pseudo_accが最も低いのに最高精度 |
| (F) クラスタ内ランダム選択【今日】 | 0.6727 | 0.7131 | (D)を明確に上回る改善 |
| (D) pre-filter diversity・確信度優先(Day153) | 0.6327 | 0.8674 | 3日間効かなかった対策の到達点 |
| (A) 確信度固定選択(threshold=0.60) | 0.6236 | 0.8995 | 6手法中最低 |
🪜 Step-by-Step 解説
1overlapの違いが「ランダム」と「Uncertainty Sampling」の性質の差を説明する
mean overlap_df_frac = 0.3074 # (D)確信度優先と(F)ランダムの重なり: 偶然30.7%は一致する mean overlap_dg_frac = 0.0000 # (D)確信度優先と(G)Uncertaintyの重なり: 5シード全てで完全にゼロ mean_conf: pre=0.1379 F=0.0707 G=0.0229 # 確信度優先から離れるほど単調に低下
2pseudo_accが史上最低なのにtest accが史上最高タイという逆転の意味
pseudo_acc_g = 0.5906 # (G)の疑似ラベル正確さ(ほぼコイントスに近い、全手法中最低) acc_g = 0.6800 # (G)のtest精度((E)と並び全手法中最高)
34日間の実験(Day151〜154)が最終的にたどり着いた設計指針
Day151: オラクルでも確信度ベースの選択バイアスは緩和されない
Day152: post-filterクラスタリング(対象を広げるだけ)でも緩和されない
Day153: pre-filterクラスタリング(対象を全体に広げるだけ)でも緩和されない
→ 原因診断: クラスタ内選択基準(確信度優先)が効果を打ち消していた
Day154: クラスタ内選択基準をランダム/Uncertaintyに変更 → 初めて明確な改善(+0.04〜+0.047)
→ (G)は(E)単純ランダム選択と同水準まで到達
🧮 数学・統計の補足(文系向け)
Day153では「各地域枠を作っても、各地域の中で結局は学歴が高い人から選ぶルールのままなら、結局同じ顔ぶれが選ばれてしまう」と説明した。今日はそのルール自体を変えた場合を考える。各地域枠の中で「ランダムに選ぶ」あるいは「あえて評価が難しい(学歴だけでは合否を予測しにくい)候補者を優先する」ようにすれば、これまで選ばれなかった層にも確実に選ばれる機会が生まれる、という直感である。しかも今日の実験結果は、「評価が難しい候補者ばかりを集めたグループ」(Uncertainty Sampling)の方が、単なる「地域内ランダム」よりも良い結果につながる可能性を示している——評価が難しい人ほど、実は自分たちがまだ知らない有用な情報を持っていることがある、というイメージである。
ある勉強法(G)は、模試(pseudo_acc)ではほとんど正解できない(ほぼ運任せ)難しい問題ばかり解いていたとする。一見「この勉強法は成果が出ていない」ように見えるが、実際にはその難しい問題こそが「本番でまだ習得できていない論点」を的確に突いており、間違えながらも本番の実力(test acc)は着実に伸びていた、というイメージである。目の前の正答率(pseudo_acc)と最終的な実力向上(test acc)は、必ずしも同じ方向に動くとは限らない、という直感である。
🏆 Kaggleでの実践的な使い方
よく使われるコンペカテゴリ: ☑ 自然言語処理(NLP) / ☑ 表形式データ(半教師あり学習全般に応用可)
🔍 「対策が効かない」ときは変数を1つずつ切り分ける
4日間の流れ(対象範囲→内部基準)
1つの対策アイデアには複数のサブ変数が隠れていることが多い。「効果がなかった」で終わらせず、そのアイデアを構成する各サブ変数を1つずつ変えてボトルネックを特定する姿勢が有効。
🎲 Uncertainty Samplingを疑似ラベル選択に応用する
Active Learning由来の発想
一般的な疑似ラベル選択は「確信度が高いサンプルを信頼する」設計が主流だが、決定境界付近の不確実なサンプルを積極的に選ぶ設計が有効な場面があることを今日の実測が示した。
⚖️ overlap診断で「本当に違うことをしているか」を確認する
overlap_dg_frac=0が象徴する検証法
新しい選択ロジックを実装したとき、旧ロジックとの選択集合の重なりを計算しておくと、意図通りに違うサンプル集合になっているかをすぐ確認でき、実装ミスの早期発見にも役立つ。
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| クラスタリングの対象範囲を変えれば十分だと思い込む(Day152・153の停滞) | 「クラスタリングを導入した」という変更に満足し、クラスタ内部の選び方まで意識が向かない | クラスタリングは「グループ分け」でしかなく、各グループから何を基準に選ぶかというもう1段細かい設計判断まで変えないと効果が出ないことがある |
| pseudo_accが低い方法は使うべきでないと判断する | 「ラベルが不正確な学習データは害になる」という直感が根強い | 今日の(G)のように、pseudo_accが史上最低でもtest精度は史上最高タイになることがある。判断基準はpseudo_accではなく最終的なtest精度(または held-out CV)に置くべき |
| overlapが低ければ良い選択方式だと単純に評価する | 「重ならない=新しい情報を取り込めている」と直感的に結びつけたくなる | 今日のoverlap_dg_frac=0は「意図した設計が正しく機能している証拠」ではあるが、overlapの低さ自体がtest精度を保証するわけではない。(F)もoverlap中程度(30.7%)だが十分な改善を示しており、overlapは補助的な診断指標にとどめるべき |
| Uncertainty Samplingは常にランダム選択より優れると一般化する | 今日(G)が(F)よりわずかに良い結果(0.6800 vs 0.6727)を見て、常に優れる手法だと飛躍する | 5シード平均の差はわずか0.0073であり、シードごとのばらつき(seed=1では(F)0.5909<(G)0.6364だが、seed=3では(F)0.6636>(G)0.6182)を見ると、優劣が安定して逆転しない保証はない。1回の実験結果を過度に一般化しない |
🚀 次のステップ
- 発展: 今日の(F)(G)はいずれもクラスタのquota配分自体は「平均確信度が高いクラスタから余りを+1」というDay152由来のルールのままだった。この配分ルール自体も確信度非依存(均等配分・クラスタサイズ比例)に変えた場合、さらに改善するのか、あるいは今日の改善分が失われるのかは未検証
- 次回予告: Day155では、Day154で発見した「クラスタ内Uncertainty Sampling」の効果が、疑似ラベルの反復適用(複数ラウンドにわたって自己学習を繰り返すIterative Pseudo Labeling、Day137の枠組み)と組み合わせた場合にも安定して持続するか、それとも初回ラウンドだけの効果なのかを検証する
Phase 6 の学習マップ(NLP系コンペへの応用フェーズ継続中)
Day151→154の4日間は、1つの原因(選択バイアス)に対して複数の対策(オラクル比較・post-filter・pre-filter・クラスタ内基準変更)を順番に試し、診断指標で失敗理由を特定しながら最終的に改善へたどり着いた一連のシリーズとして、Phase6のケーススタディに位置づけられる。
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: