📚 背景知識(読んでから問題へ)
Pseudo Labelingとは何か(5ステップ)
| キーワード | 直感的な説明 |
|---|---|
| 確信度(confidence) | モデルが出す予測確率の最大値。「このクラスだと思う度合い」の目安(ただし必ずしも真の正しさとは一致しない、という点が今日の核心) |
| confirmation bias | モデルの間違った予測を正解として学習することで、その間違いがさらに強化されてしまう現象 |
🪄 実験デザイン(データ分割 2400件)
300件(正解ラベルあり)
1500件(ラベルを隠す)
600件(最終評価のみ)
合計2400件(Day131〜135と同一の合成データ生成条件)。POOLは実際にはy_pool_trueを持つが、Pseudo Labelingの学習・採用判定には一切使わない。検証(疑似ラベル正解率の計算)にのみ特別に使用する。
🎯 問題
Day131〜135と同じ合成分類データを再利用しますが、今日は「ラベル付きデータが少ない」状況を再現するために分割方法を変えます。
| 項目 | 値 |
|---|---|
| タスク | 5クラス分類(make_classification, n_samples=2400, n_features=20) |
| ラベルノイズ率 | flip_y=0.03(3%程度) |
| labeled / pool / test | 先頭300件 / 次の1500件 / 残り600件 |
| ベースモデル | RandomForestClassifier(n_estimators=300, random_state=0) |
| 閾値候補 | [0.99, 0.95, 0.90, 0.80, 0.60, 0.0] |
| 評価軸 | 採用件数 + 疑似ラベル正解率 + test精度 + ベースライン比の改善幅 |
import numpy as np from sklearn.datasets import make_classification from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score # --- 合成データ生成(Day131〜135と同一の生成条件) --- X, y = make_classification( n_samples=2400, n_features=20, n_informative=12, n_classes=5, n_clusters_per_class=1, random_state=42, flip_y=0.03, ) LABELED_LEN = 300 # 先頭300件だけを「手持ちの正解ラベル付きデータ」として使う POOL_LEN = 1500 # 次の1500件を「ラベルなしプール」(実際にはyがあるが今回は隠して使う) # 残り600件をtest(最終評価用、pseudo labelingには一切使わない) X_labeled, y_labeled = X[:LABELED_LEN], y[:LABELED_LEN] X_pool = X[LABELED_LEN:LABELED_LEN + POOL_LEN] y_pool_true = y[LABELED_LEN:LABELED_LEN + POOL_LEN] # 検証用にのみ使う「本当は見えないはずの正解」 X_test, y_test = X[LABELED_LEN + POOL_LEN:], y[LABELED_LEN + POOL_LEN:]
💡 ヒント
Pseudo Labelingは「データを増やす」テクニックですが、増やしたデータの質は保証されていません。閾値を極端に高くする(例えば0.999)と採用件数がほぼ0になり効果が出ませんし、逆に極端に低くする(0.0、全件採用)とモデルの誤った予測もそのまま正解として取り込んでしまいます。「採用件数(量)」と「疑似ラベル正解率(質)」の両方が最終的なtest精度にどう効いてくるかを、まず頭の中で予想してから実験に入ってください。
- predict_proba(X)は各サンプルに対して「各クラスである確率」を返す(n_samples, n_classes)の配列。np.max(proba, axis=1)が確信度、np.argmax(proba, axis=1)が予測ラベル
- 拡張trainデータの作成はnp.vstack([X_labeled, X_pool[mask]])・np.concatenate([y_labeled, pseudo_labels[mask]])のように、ブールマスクで閾値以上のサンプルだけを抜き出して連結する
- 疑似ラベルの正解率はaccuracy_score(y_pool_true[mask], pseudo_labels[mask])で計算できる(実務ではこの答え合わせはできないが、今日は「裏側で何が起きているか」を可視化するための特別な検証)
- 閾値0.0はマスク条件をconfidence >= 0.0とすれば自然に「全件採用」になる(softmax確率の最大値は必ず1/n_classes以上なので実質全件が対象になる)
from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score import numpy as np # --- ステップ1: ベースライン --- baseline_model = RandomForestClassifier(n_estimators=300, random_state=0) baseline_model.fit(X_labeled, y_labeled) baseline_acc = accuracy_score(y_test, baseline_model.predict(X_test)) # --- ステップ2: プールへの確信度付き予測 --- pool_proba = baseline_model.predict_proba(X_pool) pool_confidence = pool_proba.max(axis=1) pool_pseudo_label = pool_proba.argmax(axis=1) # --- ステップ3: 閾値ごとの実験 --- thresholds = [0.99, 0.95, 0.90, 0.80, 0.60, 0.0] rows = [] for th in thresholds: mask = pool_confidence >= th n_adopted = mask.sum() if n_adopted == 0: rows.append((th, 0, None, baseline_acc)) continue pseudo_acc = accuracy_score(y_pool_true[mask], pool_pseudo_label[mask]) X_aug = np.vstack([X_labeled, X_pool[mask]]) y_aug = np.concatenate([y_labeled, pool_pseudo_label[mask]]) aug_model = RandomForestClassifier(n_estimators=300, random_state=0) aug_model.fit(X_aug, y_aug) test_acc = accuracy_score(y_test, aug_model.predict(X_test)) rows.append((th, n_adopted, pseudo_acc, test_acc))
✅ 模範解答
import numpy as np from sklearn.datasets import make_classification from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score # --- 合成データ生成(Day131〜135と同一の生成条件) --- X, y = make_classification( n_samples=2400, n_features=20, n_informative=12, n_classes=5, n_clusters_per_class=1, random_state=42, flip_y=0.03, ) LABELED_LEN = 300 POOL_LEN = 1500 X_labeled, y_labeled = X[:LABELED_LEN], y[:LABELED_LEN] X_pool = X[LABELED_LEN:LABELED_LEN + POOL_LEN] y_pool_true = y[LABELED_LEN:LABELED_LEN + POOL_LEN] X_test, y_test = X[LABELED_LEN + POOL_LEN:], y[LABELED_LEN + POOL_LEN:] # --- ステップ1: ベースライン(300件のみで学習) --- baseline_model = RandomForestClassifier(n_estimators=300, random_state=0) baseline_model.fit(X_labeled, y_labeled) baseline_acc = accuracy_score(y_test, baseline_model.predict(X_test)) print(f"ベースライン(labeled 300件のみ)精度: {baseline_acc:.4f}") # 出力例: ベースライン(labeled 300件のみ)精度: 0.6883 # --- ステップ2: プールへの確信度付き予測 --- pool_proba = baseline_model.predict_proba(X_pool) pool_confidence = pool_proba.max(axis=1) pool_pseudo_label = pool_proba.argmax(axis=1) print(f"プール1500件の確信度分布:") print(f" 平均: {pool_confidence.mean():.4f} 最小: {pool_confidence.min():.4f} 最大: {pool_confidence.max():.4f}") # 出力例: # 平均: 0.5127 最小: 0.1867 最大: 1.0000 # 0.99以上: 210件 / 0.90以上: 498件 / 0.60以上: 1035件 # --- ステップ3: 閾値ごとの実験 --- thresholds = [0.99, 0.95, 0.90, 0.80, 0.60, 0.0] rows = [] for th in thresholds: mask = pool_confidence >= th n_adopted = int(mask.sum()) if n_adopted == 0: rows.append((th, 0, None, baseline_acc)) continue pseudo_acc = accuracy_score(y_pool_true[mask], pool_pseudo_label[mask]) X_aug = np.vstack([X_labeled, X_pool[mask]]) y_aug = np.concatenate([y_labeled, pool_pseudo_label[mask]]) aug_model = RandomForestClassifier(n_estimators=300, random_state=0) aug_model.fit(X_aug, y_aug) test_acc = accuracy_score(y_test, aug_model.predict(X_test)) rows.append((th, n_adopted, pseudo_acc, test_acc)) # --- ステップ4: トレードオフの表 --- print(f"{'閾値':>6} {'採用件数':>8} {'疑似ラベル正解率':>16} {'test精度':>10} {'改善幅':>8}") for th, n, pseudo_acc, test_acc in rows: pseudo_str = f"{pseudo_acc:.4f}" if pseudo_acc is not None else " N/A" print(f"{th:>6.2f} {n:>8d} {pseudo_str:>16} {test_acc:>10.4f} {test_acc-baseline_acc:>+8.4f}") # 出力例(乱数・実行環境により変動): # 閾値 採用件数 疑似ラベル正解率 test精度 改善幅 # 0.99 210 0.9762 0.7100 +0.0217 # 0.95 356 0.9522 0.7183 +0.0300 # 0.90 498 0.9016 0.7250 +0.0367 # 0.80 701 0.8231 0.7133 +0.0250 # 0.60 1035 0.6976 0.6950 +0.0067 # 0.00 1500 0.5533 0.6683 -0.0200
📊 結果の可視化(採用件数・疑似ラベル正解率・test精度)
疑似ラベル正解率(閾値を下げるほど質が単調に低下):
■ ランダム推測の期待正解率(5クラスなので20%)は大きく上回るが、閾値0.60未満では「4割前後間違っている疑似ラベル」を大量に正解として学習することになる。
| 閾値 | 採用件数 | 疑似ラベル正解率 | test精度 | ベースライン比 |
|---|---|---|---|---|
| 0.99 | 210 | 97.6% | 0.7100 | +0.0217 |
| 0.95 | 356 | 95.2% | 0.7183 | +0.0300 |
| 0.90 | 498 | 90.2% | 0.7250 | +0.0367 ★最良 |
| 0.80 | 701 | 82.3% | 0.7133 | +0.0250 |
| 0.60 | 1035 | 69.8% | 0.6950 | +0.0067 |
| 0.00 | 1500 | 55.3% | 0.6683 | -0.0200 |
🪜 Step-by-Step 解説
1なぜ「ラベル付き300件だけ」から始めるのか
2確信度は「モデルの主観的な自信」であり「客観的な正しさ」ではない
pool_confidence = pool_proba.max(axis=1) # モデルの「自己申告」 pseudo_acc = accuracy_score(y_pool_true[mask], pool_pseudo_label[mask]) # 客観的な正しさ
3なぜ「量」と「質」の両方を1つの表に並べる必要があるのか
🧮 数学・統計の補足(文系向け)
10人が一列に並んで、最初の人が見た出来事を隣の人に伝言していくゲームを想像してください。最初の人が「自信満々に」間違った内容を伝えると、2番目の人もそれを疑わず、さらに確信を持って3番目の人に伝えます。最初の間違いが「自信満々」であればあるほど、途中で疑われることなく最後まで伝わってしまいます。Pseudo Labelingで確信度の低い予測(自信なさげな伝言)を弾いても、確信度が高いのに間違っている予測(自信満々の間違った伝言)だけは弾けません。これがconfirmation biasの正体で、「自信の高さ」でフィルタリングしても「間違いの有無」を完全にはフィルタリングできない、という限界を表しています。
天気予報士が「降水確率99%」と言った日は、ほぼ確実に雨が降ります(今日の例なら疑似ラベル正解率97.6%)。しかし「降水確率60%」と言った日は、6割方雨は降るものの4割はハズレです(疑似ラベル正解率69.8%)。「60%以上の日は全部『雨』として扱おう」と決めてしまうと、4割の「実は晴れだった日」を「雨だった」と誤って記録することになり、後で統計を取り直したときにデータが歪みます。今日の実験の「閾値0.90」は、この天気予報でいえば「ある程度当たる確率が高い予報だけを信頼する」という境界線に相当します。
🏆 Kaggleでの実践的な使い方
よく使われるコンペカテゴリ: ☑ 表形式データ(Tabular) / ☑ 自然言語処理(NLP) / ☑ 画像認識(CV) / ☐ 時系列(Time Series)
🖼️ 画像コンペでの定番手法
上位解法の頻出テクニック
画像分類・セグメンテーションのKaggleコンペでは、public testデータに対して高確信度の予測を疑似ラベルとしてtrainに加え、再学習するというPseudo Labelingが上位解法で頻繁に使われる。特にラベル付きデータが数千枚程度しかない医療画像系コンペで効果が大きい。
📝 NLPでのSemi-Supervised Learning
ラベルなしテキストの活用
ラベル付きの少数データと、ラベルなしの大量テキストデータが両方存在する場合(感情分析コンペでラベル付きレビューが少なくラベルなしレビューが大量にある状況など)、Pseudo Labelingで疑似的に学習データを拡張する手法がよく使われる。
🔧 閾値のチューニングはCVで決める
今日の「答え合わせ」は特例
実際のコンペでは今日のように「本当の正解」で疑似ラベル正解率を確認することはできない。代わりにholdoutやCV foldを使って「この閾値で疑似ラベルを追加したときにCVスコアが上がるか」を確認し、閾値自体をハイパーパラメータとしてチューニングするのが定石。
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| 「確信度が高い予測=必ず正しい予測」だと思い込む | softmaxの出力を素朴に「確率」として字義通り受け取ってしまう | モデルの確信度は「較正(キャリブレーション)」されていない場合が多く、実際の正解率とズレることがある。今日の実験でも確信度0.90台でも疑似ラベル正解率は100%ではなかった |
| 閾値は低いほど多くのデータを使えて良いと考える | 「データは多いほど良い」という直感を無条件に適用してしまう | 今日の結果が示す通り、閾値を下げすぎると疑似ラベルの質が急落し、量の増加を上回ってtest精度を悪化させることがある。量と質のトレードオフを必ず両方確認する必要がある |
| Pseudo Labelingを1回やれば十分だと考え、閾値を固定して終わらせる | 「1回試して効果があったから完了」という判断で止めてしまう | 実務では閾値自体をCVでチューニングし、さらに複数ラウンド繰り返す(Iterative Pseudo Labeling)ことで効果が変わる場合がある。1回・1閾値の結果だけで結論を出すのは早計 |
| trainデータ(ラベル付き)とpseudo labelデータを完全に等しく扱ってよいと考える | 両者ともtrainに混ぜてしまえば区別がなくなるため、意識しないと気づかない | pseudo labelデータは「不確実な正解」であるため、実務では損失関数の重みを下げる・比率を制限するなど、本物のラベルより弱く扱う工夫が必要になる場合が多い(今日は基本形の紹介のため単純な等価混合とした) |
🚀 次のステップ
- 発展: 今日は「1回だけ」疑似ラベルを付けて再学習しましたが、再学習したモデルでもう一度プールを予測し直し、さらに疑似ラベルを追加する——という操作を複数ラウンド繰り返すIterative Pseudo Labelingを試してみましょう。ラウンドを重ねるほど精度が単調に上がり続けるのか、それとも途中でconfirmation biasが蓄積して悪化に転じるのかを確認すると、今日学んだトレードオフがより鮮明に見えてきます
- 次回予告: Phase6テーマ3の2日目。今日の単発Pseudo Labelingを拡張し、Iterative Pseudo Labelingを実際にラウンドを重ねて実装し、「どのラウンドでconfirmation biasが顕在化し始めるか」を実測します
Phase 6 の学習マップ(全20テーマ)
Day131〜135でHybridMLPBlockまでのカスタムアーキテクチャ設計を完了。今日からはPhase6テーマ3「高度なアンサンブル」に入り、Pseudo LabelingとTTA(Test Time Augmentation)を扱う。
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: