📚 背景知識(読んでから問題へ)
通常の分類問題では正解ラベルをone-hot(正解クラスだけ1、他は0)として学習させます。これは「モデルに100%の自信を持って正解クラスを言い切らせる」書き方です。しかし現実のデータにはラベルミスが一定割合含まれることが多く、one-hotで学習させ続けると、モデルは誤ったラベルにも100%の自信で適合するよう訓練され、過信(overconfidence)という副作用を起こしやすくなります。
論文の核心の数式($K$=クラス数、$\epsilon$=平滑化の強さ、$q_k$=元のone-hotラベル):
q'k = (1 − ε) · qk + ε / K
日本語で言い換えると:「正解クラスに100%賭けるのではなく、90%だけ正解クラスに賭け、残り10%は全クラスに均等に薄く配っておく」という考え方。テストで「絶対にこれが正解!」と言い切るのではなく、「たぶんこれだけど他の可能性もわずかに残しておく」慎重な答え方をモデルに教え込むイメージ。
🔁 論文→実装の翻訳ワークフロー(今日から繰り返し使う型)
Abstract/Intro
「何が問題で、何を解決する手法か」を1〜2文で言えるようにする(数式は読まない)
核心の数式を特定
論文は数式だらけに見えても、実装上重要なのは通常1〜2個の核心的な式だけ
記号↔変数の対応づけ
qk→targets、ε→epsilonのように、記号1つずつを具体的なテンソル名に置き換える
最小の再現コード
本番データにいきなり適用せず、小さな合成データで狙った効果が出るか先に確認する
効果の方向性を検証
効果が出ない/逆効果なら、③の対応づけを疑って読み直してから本番投入する
今日はこのワークフローを、実際に有名な論文の数式1本(Label Smoothing)を題材に体験する。
🎯 問題
ラベルに一定割合のミス(ノイズ)が混入した合成分類データセットに対し、通常のCrossEntropyとLabel Smoothing付きCrossEntropyをそれぞれ実装して訓練し、精度と「自信の妥当さ(キャリブレーション)」の両方を比較します。
| 項目 | 値 |
|---|---|
| タスク | 5クラス分類(make_classification, n_samples=3000, n_features=20) |
| ラベルノイズ率 | 10%(正解クラス以外にランダムに付け替え) |
| train / test | 先頭2400件 / 残り600件(testはノイズなしのy_trueで評価) |
| 比較対象 | epsilon=0.0(通常CE相当) vs epsilon=0.1(Label Smoothing) |
| 評価軸 | test精度 + 確信度ビン別の実際の正解率(簡易キャリブレーション) |
import numpy as np from sklearn.datasets import make_classification # --- 合成データ生成(5クラス分類、ラベルノイズ10%混入) --- X, y_true = make_classification( n_samples=3000, n_features=20, n_informative=12, n_classes=5, n_clusters_per_class=1, random_state=42, ) rng = np.random.default_rng(42) noise_rate = 0.10 y_noisy = y_true.copy() noise_idx = rng.choice(len(y_true), size=int(len(y_true) * noise_rate), replace=False) for i in noise_idx: other_classes = [c for c in range(5) if c != y_true[i]] y_noisy[i] = rng.choice(other_classes) TRAIN_LEN = 2400
(batch, num_classes) を受け取り平滑化後のソフトラベルを返すlabel_smoothing_targets(targets_onehot, epsilon, num_classes)として実装するsoft_cross_entropy(logits, soft_targets)を実装する(log_softmaxとソフトラベルの内積で自作)💡 ヒント
論文の数式は1つだけです。難しいのは数式の理解そのものより「qkは具体的にどのテンソルか」「Kはコード中のどの変数か」を1つずつ丁寧に対応づける作業です。nn.CrossEntropyLossをそのまま使えない点がポイント。PyTorchのnn.CrossEntropyLossは内部で「整数のクラスIndex」を前提にしているため、Label Smoothing後の「小数を含むソフトラベル」を渡すには、log_softmaxから自分で損失を組み立てる必要があります。
label_smoothing_targetsは、整数ラベルをF.one_hot(targets, num_classes)でone-hotに変換してから、数式(1 - epsilon) * onehot + epsilon / num_classesをそのままテンソル演算で書けばよいsoft_cross_entropy(logits, soft_targets)は、log_probs = F.log_softmax(logits, dim=1)を計算したあと、-(soft_targets * log_probs).sum(dim=1).mean()とすれば、通常のCrossEntropyの定義 −Σqklog pk をソフトラベル版にそのまま拡張できる- epsilon=0のとき
soft_cross_entropyが通常のnn.CrossEntropyLossと数学的に一致することを、小さなテンソルで手計算して確認しておくと実装ミスに気づきやすい - キャリブレーションのビン分けは
np.digitizeのような形で機械的に処理できる。各ビン内でconfidences.mean()と(preds == y_true).mean()を比べる
import torch import torch.nn as nn import torch.nn.functional as F def label_smoothing_targets(targets: torch.Tensor, epsilon: float, num_classes: int) -> torch.Tensor: onehot = F.one_hot(targets, num_classes=num_classes).float() soft = (1 - epsilon) * onehot + epsilon / num_classes return soft def soft_cross_entropy(logits: torch.Tensor, soft_targets: torch.Tensor) -> torch.Tensor: log_probs = F.log_softmax(logits, dim=1) return -(soft_targets * log_probs).sum(dim=1).mean() # 訓練ループ内: # soft_targets = label_smoothing_targets(y_batch, epsilon, 5) # loss = soft_cross_entropy(model(x_batch), soft_targets)
✅ 模範解答
記号↔変数の対応表(ステップ③の実践)
| 論文の記号 | 意味 | コード上の変数 |
|---|---|---|
| qk | 元のone-hotラベル | onehot |
| ε | 平滑化の強さ(0.1前後) | epsilon |
| K | クラス数 | num_classes |
| q'k | 平滑化後のソフトラベル | soft |
| −Σqklog pk | クロスエントロピー本体 | soft_cross_entropy(...) |
import numpy as np import torch import torch.nn as nn import torch.nn.functional as F from sklearn.datasets import make_classification from torch.utils.data import TensorDataset, DataLoader torch.manual_seed(0) # --- サンプルデータ生成(問題文と同じ) --- X, y_true = make_classification( n_samples=3000, n_features=20, n_informative=12, n_classes=5, n_clusters_per_class=1, random_state=42, ) rng = np.random.default_rng(42) noise_rate = 0.10 y_noisy = y_true.copy() noise_idx = rng.choice(len(y_true), size=int(len(y_true) * noise_rate), replace=False) for i in noise_idx: other_classes = [c for c in range(5) if c != y_true[i]] y_noisy[i] = rng.choice(other_classes) TRAIN_LEN = 2400 X_mean, X_std = X[:TRAIN_LEN].mean(axis=0), X[:TRAIN_LEN].std(axis=0) X_scaled = (X - X_mean) / X_std X_train = torch.tensor(X_scaled[:TRAIN_LEN], dtype=torch.float32) y_train_noisy = torch.tensor(y_noisy[:TRAIN_LEN], dtype=torch.long) X_test = torch.tensor(X_scaled[TRAIN_LEN:], dtype=torch.float32) y_test_true = torch.tensor(y_true[TRAIN_LEN:], dtype=torch.long) # 評価は「ノイズなし」の正解で行う train_loader = DataLoader(TensorDataset(X_train, y_train_noisy), batch_size=64, shuffle=True) # --- ステップ1: Label Smoothingの数式をそのままコードに翻訳 --- def label_smoothing_targets(targets: torch.Tensor, epsilon: float, num_classes: int) -> torch.Tensor: """q'_k = (1 - epsilon) * q_k + epsilon / K を one-hot ターゲットに適用する""" onehot = F.one_hot(targets, num_classes=num_classes).float() soft = (1 - epsilon) * onehot + epsilon / num_classes return soft # --- ステップ2: ソフトラベルに対応する自作CrossEntropy --- def soft_cross_entropy(logits: torch.Tensor, soft_targets: torch.Tensor) -> torch.Tensor: """-sum_k q_k * log(p_k) をソフトラベル版に拡張したもの""" log_probs = F.log_softmax(logits, dim=1) return -(soft_targets * log_probs).sum(dim=1).mean() class MLP(nn.Module): def __init__(self, in_dim=20, hidden=32, num_classes=5): super().__init__() self.net = nn.Sequential( nn.Linear(in_dim, hidden), nn.ReLU(), nn.Linear(hidden, num_classes), ) def forward(self, x): return self.net(x) def train_model(epsilon: float, epochs: int = 60, lr: float = 0.01) -> nn.Module: torch.manual_seed(0) model = MLP() optimizer = torch.optim.Adam(model.parameters(), lr=lr) for epoch in range(epochs): model.train() for xb, yb in train_loader: optimizer.zero_grad() soft_targets = label_smoothing_targets(yb, epsilon, num_classes=5) loss = soft_cross_entropy(model(xb), soft_targets) loss.backward() optimizer.step() return model # --- ステップ3: epsilon=0(通常CE相当)と epsilon=0.1(Label Smoothing)を比較 --- model_baseline = train_model(epsilon=0.0) model_smoothed = train_model(epsilon=0.1) def evaluate(model): model.eval() with torch.no_grad(): probs = F.softmax(model(X_test), dim=1) confidences, preds = probs.max(dim=1) acc = (preds == y_test_true).float().mean().item() return acc, confidences.numpy(), preds.numpy() acc_base, conf_base, pred_base = evaluate(model_baseline) acc_smooth, conf_smooth, pred_smooth = evaluate(model_smoothed) print(f"baseline (epsilon=0.0) test accuracy = {acc_base:.3f}") print(f"smoothed (epsilon=0.1) test accuracy = {acc_smooth:.3f}") # 出力例(ノイズ率10%・実行環境により変動): # baseline (epsilon=0.0) test accuracy = 0.782 # smoothed (epsilon=0.1) test accuracy = 0.810 # → ラベルノイズが存在する状況では、Label Smoothingがノイズラベルへの # 「過剰適合」を和らげ、ノイズなしの正解に対する精度がやや上回りやすい # --- ステップ4: 簡易キャリブレーション測定(Reliability Diagram簡易版) --- def calibration_table(confidences, preds, y_true_np): bins = [0.0, 0.2, 0.4, 0.6, 0.8, 1.0] correct = (preds == y_true_np) rows = [] for lo, hi in zip(bins[:-1], bins[1:]): mask = (confidences > lo) & (confidences <= hi) if mask.sum() == 0: continue rows.append({ "bin": f"({lo:.1f}, {hi:.1f}]", "n": int(mask.sum()), "avg_confidence": confidences[mask].mean(), "actual_accuracy": correct[mask].mean(), }) return rows # baseline: 確信度0.9台のビンで実際の正解率が0.75前後 → 「確信度 > 実際の正解率」=過信 # smoothed: 確信度0.7〜0.8台に分布が寄り、確信度と実際の正解率の差が縮む → 過信が和らぐ
📊 キャリブレーションの可視化(確信度 vs 実際の正解率)
🪜 Step-by-Step 解説
1論文の数式の各記号を、実装のテンソルに1対1で対応づける
def label_smoothing_targets(targets, epsilon, num_classes): onehot = F.one_hot(targets, num_classes=num_classes).float() soft = (1 - epsilon) * onehot + epsilon / num_classes return soft
onehot、ε→epsilon、K→num_classesと、記号1つずつをコード中の変数に対応づけるだけで、ほぼそのままの見た目でPyTorchのテンソル演算に翻訳できる。この「対応表を作ってから書く」手順を飛ばして雰囲気で実装すると、εを全体に足すのか一部だけに足すのかといった細部を間違えやすい。2nn.CrossEntropyLossが使えない理由と、自作損失で置き換える方法
def soft_cross_entropy(logits, soft_targets): log_probs = F.log_softmax(logits, dim=1) return -(soft_targets * log_probs).sum(dim=1).mean()
nn.CrossEntropyLossは本来「整数のクラスIndexを受け取り、内部でone-hot相当の計算をする」設計になっており、小数を含むソフトラベルをそのまま渡せない。CrossEntropyの定義 −Σqklog pk をlog_softmaxとソフトラベルの要素積の和で書き直せば、qkが0/1のone-hotでも0.02のような小数でも同じ式でそのまま計算できる。3epsilon=0のとき通常のCrossEntropyと一致することを確認する
soft = onehotとなり、soft_cross_entropyは定義上nn.CrossEntropyLossと数学的に完全に一致する。この一致を確認してからepsilon=0.1を試すことで、「精度が変わったのはLabel Smoothingの効果であり、実装バグではない」と自信を持って言える。4確信度を5段階のビンに分け、確信度と実際の正解率のズレを見る
mask = (confidences > lo) & (confidences <= hi)
row = {"avg_confidence": confidences[mask].mean(), "actual_accuracy": correct[mask].mean()}
🧮 数学・統計の補足(文系向け)
模試の答案に「この問題は絶対にAだ」と100%の確信で書き込む生徒と、「たぶんAだけど、B・C・D・Eの可能性もわずかに残しておく」と考えながら解く生徒がいるとします。もし正解が実はBだった場合、前者は「完全に外した」という強いショック(大きな誤差)を受けますが、後者は「本命は外したが、他の可能性も残していた分だけダメージが小さい」状態になります。Label Smoothingは、モデルに対して常に少しだけ「疑いの余地」を残させることで、正解ラベルそのものが間違っていた場合(=アノテーションミス)の悪影響を和らげる働きをします。
pkはモデルが「クラスkである確率はこれくらいだと思う」と出力した値、qkは「本当の正解の重み」です。qkが1に近いクラスに対してpkが小さい(自信を持って外した)と、−log pkの値が急激に大きくなり、強いペナルティが課されます。one-hotラベルでは正解クラス以外のqkがすべて0のため、正解クラス1点だけに全ての採点基準が集中しますが、Label Smoothingでqkを全クラスにわずかに配ることで、「正解クラス以外を完全に無視してよい」という極端な採点基準を緩和しています。
🏆 Kaggleでの実践的な使い方
よく使われるコンペカテゴリ: ☐ 表形式データ(Tabular) / ☐ 自然言語処理(NLP) / ☑ 画像認識(CV) / ☐ 時系列(Time Series)
🌿 Cassava Leaf Disease Classification
アノテーションノイズが多い5クラス分類
葉の病気を5クラスに分類するコンペで、アノテーション自体にノイズが多いことがDiscussionで指摘されており、上位解法の多くがLabel Smoothing(epsilon=0.1前後)を標準的に採用していた。
🔬 Human Protein Atlas系
専門家でも意見が割れるラベル
顕微鏡画像のマルチラベル分類では、ラベル付けが専門家でも意見が割れることがあり、「ラベルそのものが不確実」な状況でLabel Smoothingやその発展系が精度向上に寄与した報告が複数ある。
🏭 実務での応用
クラウドソーシング由来のラベル
作業者ごとに判断がぶれやすいクラウドソーシングのアノテーションを学習に使う場面全般で、Label Smoothingは「アノテーションの質を完全には信用しない」前提を損失関数に組み込む安価な手段として使われる。
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| Label Smoothingを使えば精度が必ず上がると思ってしまう | 「有名な論文の手法だから効くはず」という権威バイアス | ラベルがほぼ正確でノイズがほとんどないデータでは効果が薄い、あるいはわずかに精度を落とすこともある。必ずbaselineと比較して確認する |
| εを大きくするほど良いと考えて0.5のような大きな値を試してしまう | 「弱めるなら思い切り弱めた方が効きそう」という直感 | εが大きすぎると正解ラベルの情報そのものが薄まりすぎ、学習が進まなくなる。実務・論文では0.05〜0.2程度が一般的で、まずは0.1から試すのが定石 |
| 論文の数式を読まずに、他人の実装をそのままコピーして使う | 数式を読むより実装を探す方が速いという誘惑 | 他人の実装には自分のタスク向けの前提が紛れ込んでいることが多い。数式との対応を自分で確認する経験が、次に別の論文を読むときの速度を上げる |
| キャリブレーションが良い=精度が高いと同じ意味だと思ってしまう | どちらも「モデルの良さ」を表す指標だという漠然とした理解 | 精度は「予測が当たっているか」、キャリブレーションは「自信の大きさが妥当か」という別の軸の指標。Loglossのような確率の正確さも問う評価指標では、キャリブレーションの良さが直接スコアに効いてくる |
🚀 次のステップ
- 発展: 今日の
calibration_tableをさらに一歩進め、全ビンの「確信度と実際の正解率の差」をサンプル数で加重平均したExpected Calibration Error(ECE)を1つのスカラー値として実装し、epsilon=0とepsilon=0.1のECEを直接数値で比較してみましょう。「キャリブレーションが良い」を1つの数字で語れるようになると、複数手法の比較がしやすくなります - 次回予告: Day 127 — SOTA手法の追跡・論文実装②。引き続きPhase6テーマ1の枠内で、今日と同じ「論文の数式→実装」ワークフローを別の題材(汎化性能を高めるOptimizerの工夫など)に適用し、論文を読んで実装に落とし込む力をもう一段鍛えます
Phase 6 の学習マップ(全20テーマ・本日開始)
Phase5(Day110〜125)でDL基礎・CV・時系列DL・マルチモーダル・ソロGold戦略を完了し、Day126からPhase6(Grandmaster)に突入。まずは「論文→実装」の翻訳スキルを5日間かけて鍛える。
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: