📚 背景知識(読んでから問題へ)
今日はDay152の次回予告どおり、この考察を検証するために「フィルタを適用する前の未ラベルデータ全体」をクラスタリング対象にする方式(pre-filter diversity)を実装する。理屈上は、これによって「そもそも確信度0.55未満で候補プールに入れなかったクラスタ」からも強制的にサンプルが選ばれるようになるはずである。さらに今日は、比較対象に「ランダム選択+モデル自身の疑似ラベル」も追加する。Day151のランダム選択は正解ラベル(オラクル)を使う特殊条件だったため、今日は自己学習の枠組みを揃えたランダム比較を新たに行う。
| 用語 | 意味 |
|---|---|
| post-filter diversity | Day152の方式。確信度でフィルタした後の候補プール内でクラスタリングする |
| pre-filter diversity | 今日の方式。確信度でフィルタする前の未ラベルデータ全体でクラスタリングする |
| クラスタ内確信度優先 | クラスタのquota分を選ぶ際、そのクラスタの中で確信度が高い順に埋めていくルール |
| overlap(選択集合の重なり) | 2つの選択方式(post-filter/pre-filter)がどれだけ同じサンプルを選んでいるかを測る指標 |
🗂️ データ・分割のスキーマ
| 変数名 | 型 | 内容 |
|---|---|---|
| df (n=450) | pd.DataFrame | Day151・152と同じmake_text_dataset()で生成した合成テキスト全体 |
| test_df (110件) | pd.DataFrame | 最終評価専用のホールドアウト。全設定で共通に使う |
| labeled_df (40件) | pd.DataFrame | 「ラベル付きデータが少ない」状況を模したベースライン学習セット |
| unlabeled_df (約300件) | pd.DataFrame | ラベルを隠した疑似ラベル候補プール。今日はこの全体がクラスタリング対象になる |
threshold = 0.60 # n_addedを決める基準(Day151/152と同一) candidate_threshold = 0.55 # post-filter方式(Day152)が候補プールを作る閾値 n_clusters = 8 # KMeansのクラスタ数(post-filter/pre-filter共通)
🎯 問題
タスクA(理論)
以下2点をそれぞれ2〜3文で説明する。
タスクB(コーディング→分析)
make_text_dataset・split_seed・train_and_eval・select_confident_pseudo_labelsはDay151・152と同じ実装を再利用するselect_diverse_postfilter(model, vectorizer, unlabeled_df, candidate_threshold, n_target, n_clusters, seed) — Day152と同じ実装(候補プール内でクラスタリング)を再利用するselect_diverse_prefilter(model, vectorizer, unlabeled_df, n_target, n_clusters, seed) — 確信度による事前フィルタを一切かけず、unlabeled_df全体のTF-IDFベクトルをKMeansでクラスタリングする。各クラスタに均等quotaを割当て(余りは平均確信度が高いクラスタから+1)、クラスタ内は確信度が高い順に選ぶthreshold=0.60・candidate_threshold=0.55・n_clusters=8で、5シードにわたり (A) 固定選択, (C) post-filter, (D) pre-filter, (E) 同数ランダム+自己ラベル、の4パターンを比較する0.02未満であることをassert文で確認する💡 ヒント
タスクAの2.は、「クラスタの中身の構成」を考えるとよい。もしあるクラスタが、たまたま確信度0.55以上のサンプルだけで既にquota分を満たせてしまうなら、「クラスタ内で確信度が高い順に選ぶ」というルールのもとでは、そのクラスタから低確信度サンプルが選ばれる出番は一切来ない。つまり「フィルタ前の全体をクラスタリング対象にする」だけでは不十分で、クラスタ内の選び方そのものが確信度優先である限り、多くのクラスタで実質的にpost-filterと同じサンプルが選ばれてしまう可能性がある。
- overlapは(C)と(D)それぞれのmaskを
&演算子でAND結合し.sum()を取ればよい:overlap = (mask_div & mask_pre).sum() - 平均確信度は
confidence = np.abs(proba - 0.5)を計算し、confidence[mask]の平均を取ればよい - 「候補プールに入らなかった低確信度サンプルがpre-filter選択にどれだけ含まれたか」は
confidence[mask_pre] < (candidate_threshold - 0.5)の件数を数えるとよい - 5シードのループを
run_comparison(seed, ...)のように関数化し、pd.DataFrame([...])でまとめると集計しやすい(Day151・152と同じパターン)
def select_diverse_prefilter(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) # 全データを確信度降順に並べる quota = n_target // k remainder = n_target - quota * k cluster_quota = {c: quota for c in range(k)} # ... quota配分・クラスタ内選択はDay152と同じロジック ... 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_postfilter は # Day151・152と同一(省略) def select_diverse_prefilter(model, vectorizer, unlabeled_df, n_target, n_clusters, seed): """Day153: クラスタリング対象は「未ラベルデータ全体」(確信度フィルタをかける前)""" 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) 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 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, candidate_threshold=0.55, n_clusters=8): labeled_df, unlabeled_df, test_df = split_seed(seed) acc_base, ll_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) # (C) Day152: post-filter diversity(候補プール内クラスタリング) mask_div, pseudo_div = select_diverse_postfilter( model, vectorizer, unlabeled_df, candidate_threshold, n_added, n_clusters, seed) acc_div, _, _, _ = train_and_eval( labeled_df["text"].tolist() + unlabeled_df.loc[mask_div, "text"].tolist(), labeled_df["label"].tolist() + pseudo_div.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) # (E) 参考: 同数ランダム選択 + モデル自身の疑似ラベル(自己学習の枠組みを揃える) rng = np.random.RandomState(seed) rand_idx = np.sort(rng.choice(len(unlabeled_df), size=n_added, replace=False)) X_un = vectorizer.transform(unlabeled_df["text"]) proba = model.predict_proba(X_un)[:, 1] mask_rand = np.zeros(len(unlabeled_df), dtype=bool) mask_rand[rand_idx] = True pseudo_rand = (proba[mask_rand] >= 0.5).astype(int) acc_rand, _, _, _ = train_and_eval( labeled_df["text"].tolist() + unlabeled_df.loc[mask_rand, "text"].tolist(), labeled_df["label"].tolist() + pseudo_rand.tolist(), test_df) # 追加診断: overlap・平均確信度 conf = np.abs(proba - 0.5) overlap = int((mask_div & mask_pre).sum()) return {"seed": seed, "acc_base": acc_base, "acc_conf": acc_conf, "acc_div": acc_div, "acc_pre": acc_pre, "acc_rand": acc_rand, "overlap_frac": overlap / n_added} SEEDS = [0, 1, 2, 3, 4] df = pd.DataFrame([run_comparison(s) for s in SEEDS]) mean_acc_base = df["acc_base"].mean() mean_acc_div = df["acc_div"].mean() mean_acc_pre = df["acc_pre"].mean() assert mean_acc_pre < mean_acc_base, "pre-filter diversity選択がbaselineを悪化させていません" assert abs(mean_acc_pre - mean_acc_div) < 0.02, \ "pre-filterとpost-filterのtest精度差が0.02以上あります" print("[OK] pre-filter化しても選択バイアスは明確には緩和されない")
# ============ 実測結果(実際にPython 3で実行して確認済み) ============
# seed n_added acc_base acc_conf acc_div acc_pre acc_rand overlap_frac n_low_conf_in_pre
# 0 94 0.763636 0.581818 0.590909 0.600000 0.709091 0.861702 0
# 1 125 0.727273 0.590909 0.618182 0.618182 0.600000 0.816000 5
# 2 81 0.781818 0.645455 0.645455 0.654545 0.700000 0.839506 0
# 3 75 0.736364 0.672727 0.663636 0.681818 0.690909 0.840000 0
# 4 78 0.700000 0.627273 0.627273 0.609091 0.700000 0.858974 0
#
# [まとめ]
# mean acc_base = 0.7418
# mean acc_div (Day152 post-filter) = 0.6291 (pseudo_acc=0.8795)
# mean acc_pre (Day153 pre-filter) = 0.6327 (pseudo_acc=0.8674)
# mean acc_rand (同数ランダム+自己ラベル) = 0.6800 (pseudo_acc=0.7228)
# acc_pre - acc_div = 0.0036(0.02未満)
# mean overlap_frac(post-filterとpre-filterの選択集合の重なり) = 0.8432
# mean n_low_conf_in_pre(候補プール外だった低確信度点の混入数)= 1.0 / 平均n_added≈91件
#
# [OK] pre-filter化しても選択バイアスは明確には緩和されない
📊 結果の可視化
① フィルタを外す
候補プールでなくunlabeled_df全体をKMeansでクラスタリング
② それでも確信度優先で選ぶ
クラスタ内では相変わらず確信度が高い順にquotaを埋める
③ overlapで検証
post-filterとの選択集合の重なりを数値で確認し、効果の有無を判定
| 設定 | 平均accuracy | 疑似ラベル精度 | 解釈 |
|---|---|---|---|
| baseline(40件のみ) | 0.7418 | — | 追加データなし |
| (E) ランダム選択+自己ラベル | 0.6800 | 0.7228(最低) | ラベルは不正確でも情報量で優る |
| (D) pre-filter diversity(未ラベル全体クラスタリング) | 0.6327 | 0.8674 | postとほぼ同じ、明確な改善なし |
| (C) post-filter diversity(候補プール内クラスタリング) | 0.6291 | 0.8795 | Day152の結果 |
| (A) 確信度固定選択(threshold=0.60) | 0.6236 | 0.8995 | 4手法中最低 |
🪜 Step-by-Step 解説
1「フィルタを外す」だけでは不十分だった理由を数値で確認する
mean overlap_frac = 0.8432 # post-filterとpre-filterの選択集合の84.3%が同じ mean n_low_conf_in_pre = 1.0 # 候補プール外だったはずの低確信度点はわずか1件混入
2ランダム選択がpseudo_accで劣るのにtest accで勝った理由
pseudo_acc_rand = 0.7228 # ランダム選択の疑似ラベル正確さ(低い) pseudo_acc_div = 0.8795 # post-filter diversityの疑似ラベル正確さ(高い) acc_rand = 0.6800 # ランダム選択のtest精度(高い) acc_div = 0.6291 # post-filter diversityのtest精度(低い)
33日間の実験(Day151〜153)が一貫して示した結論
Day151: オラクルでも選択バイアスは緩和されない (acc 0.7291 ≪ ランダムオラクル 0.8818)
Day152: post-filterクラスタリングでも緩和されない (acc 0.6291、top-Nと差0.0055)
Day153: pre-filterクラスタリングでも緩和されない (acc 0.6327、post-filterと差0.0036)
→ ランダム選択+自己ラベルが唯一baselineとの差を縮めた (acc 0.6800)
🧮 数学・統計の補足(文系向け)
「学歴で一次選考した後の候補者だけで多様性を確保しよう」というやり方(post-filter)がうまくいかなかったので、「全応募者を対象に出身地域ごとにグループ分けして各地域枠を作ろう」というやり方(pre-filter)に変えたとする。しかし各地域グループの中でも「結局は学歴が高い人から選ぶ」というルールを変えていなければ、多くの地域グループには学歴の高い応募者が既に十分な人数いるため、結局ほとんど同じ顔ぶれが選ばれてしまう。「対象を広げる」ことと「選ぶ基準を変える」ことは別の話であり、両方を変えない限り根本的な偏りは解消されない、という直感である。
簡単な問題集(確信度の高いサンプル)を完璧に解けても、もう知っていることの復習にしかならず、実力(汎化性能)は伸びにくい。一方、難しい問題集(ランダムに含まれる決定境界付近のサンプル)は間違えることもあるが、間違えた問題こそ「自分がまだ分かっていないこと」を教えてくれるため、多少の誤答があっても総合的な実力向上には効果的なことがある、という直感である。
🏆 Kaggleでの実践的な使い方
よく使われるコンペカテゴリ: ☑ 自然言語処理(NLP) / ☑ 表形式データ(半教師あり学習全般に応用可)
🔍 「対策が効かなかった理由」まで診断する
overlap_fracのような追加指標
新しい特徴量・対策を追加してスコアが変わらなかったとき、「アイデアが悪かったのか」「別のルールが効果を打ち消していたのか」を切り分ける診断指標を用意しておくと、次の改善につながる。
🎲 確信度ベースの選択に頼りすぎない設計
3日間、確信度ベースの工夫はすべて限定的
閾値調整・post-filterクラスタリング・pre-filterクラスタリングのいずれも選択バイアスの根本解消には至らなかった。ランダム性やUncertainty Samplingを組み合わせる設計の方が有望である可能性が示唆される。
⚖️ 「正確さ」と「情報量」を別軸で評価する
pseudo_acc単独では判断しない
追加データの評価において「正確さ」だけでなく「モデルにとって新しい情報を含むか」も評価軸に入れることで、正確なのに効果が薄いという今日のような逆転現象を見落とさずに済む。
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| 「対象範囲を広げる」変更をすれば効果が出るはずだと考える | 「フィルタを外した」という変更自体に注目し、他のルール(クラスタ内選択基準)によって打ち消される可能性を見落とす | 複数のルールが組み合わさっている処理では、1つのルールだけを変えても他のルールが効果を吸収してしまうことがある。変更後は必ず「意図した効果が実際に発動しているか」を診断指標で確認する |
| 疑似ラベルの正確さ(pseudo_acc)が高いほど追加データの効果も高いと考える | 「正しいラベル=良い学習データ」という直感が強く働く | 正確でも情報量が少ない(モデルが既に知っている領域に偏る)サンプルより、多少不正確でも情報量が多い(決定境界付近の)サンプルの方が汎化性能向上に寄与することがある |
| 同じ結論に複数回到達した実験を「新しい発見がない」と評価する | 目新しい結果(=改善した、という結果)が出ないと実験に価値がないと感じてしまう | 異なる手法(オラクル比較・post-filter・pre-filter)で一貫して同じ結論に到達することは、その結論の信頼性を高める重要な証拠であり、失敗の繰り返しではない |
| クラスタリングを使えば自動的に「多様性」が確保されると考える | KMeansが特徴量空間を分割する手法であることから、分割=多様性確保だと単純化してしまう | クラスタリングは「空間をグループに分ける」だけであり、各グループから何を基準に選ぶか(今日の場合は確信度優先)まで変えない限り、多様性は保証されない |
🚀 次のステップ
- 発展: 今日の考察を踏まえると、真に選択バイアスを緩和する設計は「クラスタ内でも確信度優先ではなく、クラスタごとにランダムに選ぶ」、あるいは「クラスタ内で確信度が中央値(0.5)に近いサンプルを優先するUncertainty Sampling」に切り替えることである可能性が高い
- 次回予告: Day154では、上記の「クラスタ内ランダム選択」または「クラスタ内Uncertainty Sampling」を実装し、3日間(Day151〜153)効果が出なかった選択バイアス対策に、初めて明確な改善をもたらせるかを検証する
Phase 6 の学習マップ(NLP系コンペへの応用フェーズ継続中)
Day150→153と4日連続で、「表形式・画像で効いた手法をそのままテキストに持ち込むと期待通りには機能しない」というパターンが実測された。今日は洗練された対策(pre-filter clustering)ですら単純なランダム選択に及ばないという、直感に反する結果に行き着いた。
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: