Day 126 — SOTA手法の追跡・論文実装① — 論文の数式をコードに落とし込むワークフロー(Label Smoothing)

2026-08-16 金 / Phase 6(Grandmaster、テーマ1/5開始) 理論→コーディング 数式→実装の翻訳 / 過信の抑制 / キャリブレーション測定

📚 背景知識(読んでから問題へ)

🏅
Day001〜125(Phase1〜5)では「すでに確立された技術」を体系的に積み上げてきました。今日から始まるPhase6(金帯・Grandmaster)は学び方の質が変わります。Grandmasterクラスの参加者は論文や上位者のsolution writeupで発表されたばかりの手法を、自分で読んで実装に落とし込むことが日常的に求められます。この「論文→実装」の翻訳作業が、今日から5日間のPhase6テーマ1「SOTA手法の追跡・論文実装」の中心スキルです。
🚪
なぜこれがExpert/Masterとの分水嶺になるのか: Expert・Masterまでは「教科書に載っている手法を正しく使いこなす力」で到達できます。しかしGold 3枚(うち1枚はソロ)が要求されるGrandmasterでは、参加者全員が同じ教科書的手法を使うため、そこから頭一つ抜けるには「まだ広く知られていない手法を、いち早く正しく実装して試す」力が効いてきます。この力の核は難しい数学ではなく、論文のMethodセクションの数式を、変数を1つずつ確認しながらコードの1行1行に対応づけていく地道な翻訳作業です。
🎯
今日の題材: Label Smoothing(Szegedy et al., 2016, "Rethinking the Inception Architecture for Computer Vision")

通常の分類問題では正解ラベルをone-hot(正解クラスだけ1、他は0)として学習させます。これは「モデルに100%の自信を持って正解クラスを言い切らせる」書き方です。しかし現実のデータにはラベルミスが一定割合含まれることが多く、one-hotで学習させ続けると、モデルは誤ったラベルにも100%の自信で適合するよう訓練され、過信(overconfidence)という副作用を起こしやすくなります。

論文の核心の数式($K$=クラス数、$\epsilon$=平滑化の強さ、$q_k$=元のone-hotラベル):

q'k = (1 − ε) · qk + ε / K

日本語で言い換えると:「正解クラスに100%賭けるのではなく、90%だけ正解クラスに賭け、残り10%は全クラスに均等に薄く配っておく」という考え方。テストで「絶対にこれが正解!」と言い切るのではなく、「たぶんこれだけど他の可能性もわずかに残しておく」慎重な答え方をモデルに教え込むイメージ。

🔁 論文→実装の翻訳ワークフロー(今日から繰り返し使う型)

1

Abstract/Intro

「何が問題で、何を解決する手法か」を1〜2文で言えるようにする(数式は読まない)

2

核心の数式を特定

論文は数式だらけに見えても、実装上重要なのは通常1〜2個の核心的な式だけ

3

記号↔変数の対応づけ

qk→targets、ε→epsilonのように、記号1つずつを具体的なテンソル名に置き換える

4

最小の再現コード

本番データにいきなり適用せず、小さな合成データで狙った効果が出るか先に確認する

5

効果の方向性を検証

効果が出ない/逆効果なら、③の対応づけを疑って読み直してから本番投入する

今日はこのワークフローを、実際に有名な論文の数式1本(Label Smoothing)を題材に体験する。

🎯 問題

ラベルに一定割合のミス(ノイズ)が混入した合成分類データセットに対し、通常のCrossEntropyLabel 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
1
数式→コードの翻訳(メイン): Label Smoothing数式 q'k = (1-ε)qk + ε/K を、one-hotターゲット (batch, num_classes) を受け取り平滑化後のソフトラベルを返すlabel_smoothing_targets(targets_onehot, epsilon, num_classes)として実装する
2
損失関数への組み込み: ソフトラベルを受け取りsoft_cross_entropy(logits, soft_targets)を実装する(log_softmaxとソフトラベルの内積で自作)
3
比較実験: epsilon=0とepsilon=0.1でノイズ入りtrainラベルを使って学習し、ノイズなしtestラベルへの精度を比較する
4
簡易キャリブレーション測定: 最大softmax確信度を5段階ビン(0.2刻み)に分け、各ビンの「確信度の平均」と「実際の正解率」を比較する
5
(考察): ラベルノイズが存在する状況でLabel Smoothingが精度・キャリブレーションを改善するかを100字程度で結論づける

💡 ヒント

ヒント1方向性

論文の数式は1つだけです。難しいのは数式の理解そのものより「qkは具体的にどのテンソルか」「Kはコード中のどの変数か」を1つずつ丁寧に対応づける作業です。nn.CrossEntropyLossをそのまま使えない点がポイント。PyTorchのnn.CrossEntropyLossは内部で「整数のクラスIndex」を前提にしているため、Label Smoothing後の「小数を含むソフトラベル」を渡すには、log_softmaxから自分で損失を組み立てる必要があります。

ヒント2アプローチ
  • 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()を比べる
ヒント3コード骨格
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台に分布が寄り、確信度と実際の正解率の差が縮む → 過信が和らぐ
🧭
タスク5・考察: ラベルノイズ10%の状況で epsilon=0(通常CE)は、訓練データ中の誤ったラベルにも100%の自信で適合しようとするため過学習・過信を起こしやすい。epsilon=0.1のLabel Smoothingは「100%言い切らない」制約を課すことで誤ラベルへの過剰適合を抑え、実験ではノイズなしtestに対する精度がわずかに上回り、確信度と実際の正解率の差(過信の度合い)も縮小した。ノイズを含む実データが多いKaggleコンペでは、精度・キャリブレーションの両面でLabel Smoothingを試す価値がある。

📊 キャリブレーションの可視化(確信度 vs 実際の正解率)

確信度ビン別: 平均確信度(明るい棒)vs 実際の正解率(濃い棒) baseline (epsilon=0.0) — 過信気味              smoothed (epsilon=0.1) — 過信が和らぐ 0% 100% (0.6,0.8] (0.8,1.0] ギャップ大=過信 (0.6,0.8] (0.8,1.0] ギャップ小=改善 ■薄い=平均確信度 ■濃い=実際の正解率 → smoothed は薄い棒と濃い棒の差(=確信度と実際の正解率の差)が baseline より小さく、過信が和らいでいる
🔍
この図は模式的なイメージ(実際の数値は実行環境により変動)。重要なのは「薄い棒(自己申告の確信度)」と「濃い棒(実際の正解率)」の差が小さいほどキャリブレーションが良いという読み方であり、精度そのものの棒グラフとは別の軸を見ている点。

🪜 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
🔗
なぜこうするか: 論文の q'k = (1−ε)qk + ε/K という式は、qk(元のone-hotラベル)→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 pklog_softmaxとソフトラベルの要素積の和で書き直せば、qkが0/1のone-hotでも0.02のような小数でも同じ式でそのまま計算できる。

3epsilon=0のとき通常のCrossEntropyと一致することを確認する

🧪
なぜこうするか: 新しい実装が正しいかを検証する最も簡単な方法は「特殊なパラメータを入れたときに、既知の正しい実装と一致するか」を確認すること。epsilon=0を代入すると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()}
⚖️
なぜこうするか: 「モデルが95%の確信度で予測したサンプル群」の中で実際に何%が正解しているかを見れば、モデルの自信が数値として妥当かどうかを直接検証できる。確信度95%のサンプル群の実際の正解率が70%しかなければそのモデルは「言い過ぎ」(過信)であり、Public LBのスコアだけを見ていては気づけないモデルの性質を明らかにできる。

🧮 数学・統計の補足(文系向け)

📝
Label Smoothingを「模試の自己採点」で理解する:

模試の答案に「この問題は絶対にAだ」と100%の確信で書き込む生徒と、「たぶんAだけど、B・C・D・Eの可能性もわずかに残しておく」と考えながら解く生徒がいるとします。もし正解が実はBだった場合、前者は「完全に外した」という強いショック(大きな誤差)を受けますが、後者は「本命は外したが、他の可能性も残していた分だけダメージが小さい」状態になります。Label Smoothingは、モデルに対して常に少しだけ「疑いの余地」を残させることで、正解ラベルそのものが間違っていた場合(=アノテーションミス)の悪影響を和らげる働きをします。
クロスエントロピーの式 −Σqklog pk を日本語で言い換える:

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テーマ・本日開始)

1-5
SOTA手法追跡・論文実装(本日1/5)
6-10
カスタムアーキテクチャ設計
11-15
高度なアンサンブル(Pseudo Labeling・TTA)
16-20
コンペ戦略・Gold 3枚達成プラン

Phase5(Day110〜125)でDL基礎・CV・時系列DL・マルチモーダル・ソロGold戦略を完了し、Day126からPhase6(Grandmaster)に突入。まずは「論文→実装」の翻訳スキルを5日間かけて鍛える。

📝 自己評価(解いた後に記入)

自分の回答・気づき・メモ: