Day 130 — SOTA手法の追跡・論文実装⑤ — Knowledge Distillationで教師の「暗黙知」を生徒へ伝える

2026-08-20 金 / Phase 6(Grandmaster、テーマ1/5 の 5日目・最終回) 理論→コーディング 数式→実装の翻訳 / Teacher-Student学習 / 温度付きKLダイバージェンス

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

🏁
Phase6テーマ1「SOTA手法の追跡・論文実装」も今日が最終日(5/5)です。Day126のLabel Smoothing(損失関数の改造)、Day127のSAM(Optimizerの改造)、Day128のEMA(学習ループ外での重みの後処理)、Day129のMixup(入力データそのものの改造)ときて、今日のKnowledge Distillation(知識蒸留)は5つ目の全く新しい切り口です。これまでは「1つのモデルをどう賢く学習させるか」でしたが、今日は初めて「2つのモデル(教師と生徒)が協調して学習する」という構造そのものが変わる手法を扱います。
🎓
なぜ「教師モデルの出力」を丸ごと学習に使うと効果があるのか: 通常の学習では、正解ラベルは「これはクラスA」という1点だけを指す情報(one-hot)しか持ちません。しかしすでに学習済みの大きく賢い教師モデルにこの入力を見せると、出力は[0.7, 0.2, 0.05, 0.03, 0.02]のような「クラスAが70%、でもクラスBの可能性も20%くらいある」という豊かな情報を持っています。「◯か×か」だけの採点と、「ほぼ正解だがクラスBの考え方と少し混同している」という添削コメント付きの採点の違いです。この添削コメントにあたる情報をHintonら(2015)はdark knowledge(暗黙知)と呼びました。
Teacher
(hidden=128)
acc 高
出力分布 →
Student
(hidden=8)
KDで学習
≈
baselineより
Teacherに
近づく

今日の題材: Knowledge Distillation(Hinton et al., 2015)の核心の数式:

L = α · CE(zs, y) + (1−α) · T² · KL( σ(zt/T) ‖ σ(zs/T) )

日本語で言い換えると:「①正解ラベルへの普通の交差エントロピー損失」と「②教師の出力分布(温度Tでやわらかくした確率)に生徒の出力分布を近づけるKLダイバージェンス損失」をα対(1−α)で足し合わせるだけ。料理人の弟子入りで言えば、①レシピ通り作れるかの採点、②師匠の味付けのニュアンスにどれだけ近づけたかの採点、を同時に受けるようなもの。

🪄 KDの適用ステップ(今日の学習ループの流れ)

1

①Teacherを先に学習

大きめのMLPを正解ラベルだけで通常学習させておく

2

②Teacherをno_gradで推論

Studentの学習中はTeacherに勾配を流さない

3

③温度T付きsoftmax

Teacher/Studentの両方のlogitをTで割ってやわらかくする

4

④hard+soft損失を合成

α·CE + (1−α)·T²·KLでStudentのみ更新

5

⑤3者を比較

Teacher/Student(baseline)/Student(KD)のtest精度を並べる

Mixup(Day129)が「1つのモデル・1バッチ内で完結する処理」だったのに対し、KDは初めて「2つの独立したモデルが登場し、片方は固定・片方だけ学習する」という構造上の違いを持つ。

🎯 問題

Day126〜129と同じ合成分類データに対し、大きな教師モデル(Teacher)をまず普通に学習させ、その後小さな生徒モデル(Student)を2通りの方法で学習させて比較します。1つ目は正解ラベルだけで普通に学習させる方法(baseline)、2つ目はTeacherの出力分布も使ったKnowledge Distillationです。

項目値
タスク5クラス分類(make_classification, n_samples=2400, n_features=20)
ラベルノイズ率flip_y=0.03(3%程度)
train / test先頭1800件 / 残り600件
Teacherhidden=128・3層MLP、epochs=60で正解ラベルのみ通常学習
Studenthidden=8・1層MLP。baseline / KD(T=4, alpha=0.3) をepochs=60で比較
評価軸Teacher・Student(baseline)・Student(KD)のtest精度 + 温度T感度(1/2/4/8)
import numpy as np
from sklearn.datasets import make_classification

# --- 合成データ生成(5クラス分類。Day126〜129と同系統) ---
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,
)

TRAIN_LEN = 1800   # 先頭1800件をtrain、残り600件をtest
# Teacher: hidden=128の3層MLP、epochs=60で先に学習させておく
# Student: hidden=8の小さな1層MLP。baseline / KD(alpha=0.3, T=4) の2通りで学習し比較する
1
Teacherモデルの用意: hidden=128の大きめのMLP(Teacher)を正解ラベルだけで普通にepochs=60学習させ、test精度を確認する
2
数式→コードの翻訳(メイン): KDの数式を、生徒のlogit z_s・教師のlogit z_t・正解ラベルy・温度T・重みalphaを受け取り損失を返すdistillation_loss(z_s, z_t, y, T, alpha)関数として実装する
3
2種類のStudent学習: hidden=8の小さなMLP(Student)を、baseline学習とT=4・alpha=0.3のKD学習、それぞれepochs=60で行う
4
比較実験: Teacher・Student(baseline)・Student(KD)の3つのtest精度を並べて比較する
5
温度Tの感度確認: Tを[1, 2, 4, 8]の4通りで試し、Student(KD)のtest精度の変化を確認する(alpha=0.3固定)
6
(考察): KDがStudentの性能にどう影響し、温度Tがその効果にどう関わるかを100字程度で結論づける

💡 ヒント

ヒント1方向性

hidden=8という小さなStudentモデルは表現力が低く、正解ラベルだけで学習させてもhidden=128のTeacherには到底届きません。しかしTeacherの出力する確率分布には「クラスAが正解だが、クラスCとも少し似ている」といった、正解ラベル1点には存在しない追加の情報(クラス間の類似度の手がかり)が含まれています。KDはこの追加情報を小さなStudentに教え込む手法です。実装上の最重要ポイントは、Teacherは学習させず、勾配が流れるのはStudent側だけにすることです。

ヒント2アプローチ
  • PyTorchのnn.KLDivLoss(reduction="batchmean")は「入力側は対数確率、target側は確率」を要求する非対称なAPI。kl_loss(F.log_softmax(z_s/T, dim=1), F.softmax(z_t/T, dim=1))という順番で呼び出す(逆にすると誤った値になる)
  • 数式のT²という係数を忘れない。これは飾りではなく、Hintonらの論文が明示的に指摘した必要な補正
  • Teacherの出力は必ずteacher.eval()+torch.no_grad()で計算し、Teacher自身のパラメータに勾配が流れないようにする
  • alphaは「正解ラベルからどれだけ厳密に学ぶか」と「教師の"雰囲気"からどれだけ学ぶか」のバランス。alpha=1なら通常学習(KDなし)と同じになる
ヒント3コード骨格
import torch
import torch.nn.functional as F

def distillation_loss(z_s, z_t, y, T=4.0, alpha=0.3):
    hard_loss = F.cross_entropy(z_s, y)
    soft_loss = F.kl_div(
        F.log_softmax(z_s / T, dim=1),
        F.softmax(z_t / T, dim=1),
        reduction="batchmean",
    ) * (T ** 2)
    return alpha * hard_loss + (1 - alpha) * soft_loss

# 学習ループ内(Student側):
# with torch.no_grad():
#     z_t = teacher(xb)          # Teacherは推論のみ、勾配なし
# z_s = student(xb)
# loss = distillation_loss(z_s, z_t, yb, T=4.0, alpha=0.3)

✅ 模範解答

記号↔変数の対応表

論文の記号意味コード上の変数
zs生徒モデルのlogit(softmax適用前)student(xb)
zt教師モデルのlogitteacher(xb)(no_grad)
T温度(大きいほど確率分布がなだらかになる)T
σ(z/T)温度付きsoftmaxF.softmax(z / T, dim=1)
αハード損失とソフト損失の重みalpha
CE(zs, y)正解ラベルへの通常の交差エントロピーhard_loss
KL(σ(zt/T)‖σ(zs/T))教師分布と生徒分布のKLダイバージェンスsoft_loss
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 = 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,
)
TRAIN_LEN = 1800

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 = torch.tensor(y[:TRAIN_LEN], dtype=torch.long)
X_test = torch.tensor(X_scaled[TRAIN_LEN:], dtype=torch.float32)
y_test = torch.tensor(y[TRAIN_LEN:], dtype=torch.long)

train_loader = DataLoader(TensorDataset(X_train, y_train), batch_size=32, shuffle=True)

class MLP(nn.Module):
    """hiddenの大きさでTeacher/Studentを両方表現する"""
    def __init__(self, in_dim=20, hidden=32, num_classes=5, n_layers=2):
        super().__init__()
        layers = [nn.Linear(in_dim, hidden), nn.ReLU()]
        for _ in range(n_layers - 1):
            layers += [nn.Linear(hidden, hidden), nn.ReLU()]
        layers += [nn.Linear(hidden, num_classes)]
        self.net = nn.Sequential(*layers)
    def forward(self, x):
        return self.net(x)

def evaluate(model):
    model.eval()
    with torch.no_grad():
        preds = model(X_test).argmax(dim=1)
    return (preds == y_test).float().mean().item()

# --- ステップ1: Teacher(hidden=128, 3層)を通常学習 ---
def train_plain(model, epochs=60, lr=1e-3):
    torch.manual_seed(0)
    optimizer = torch.optim.Adam(model.parameters(), lr=lr)
    for epoch in range(epochs):
        model.train()
        for xb, yb in train_loader:
            optimizer.zero_grad()
            loss = F.cross_entropy(model(xb), yb)
            loss.backward()
            optimizer.step()
    return model

teacher = MLP(hidden=128, n_layers=3)
teacher = train_plain(teacher)
teacher.eval()
acc_teacher = evaluate(teacher)

# --- ステップ2: KD損失の実装(数式をそのまま翻訳) ---
def distillation_loss(z_s, z_t, y, T=4.0, alpha=0.3):
    hard_loss = F.cross_entropy(z_s, y)
    soft_loss = F.kl_div(
        F.log_softmax(z_s / T, dim=1),
        F.softmax(z_t / T, dim=1),
        reduction="batchmean",
    ) * (T ** 2)
    return alpha * hard_loss + (1 - alpha) * soft_loss

# --- ステップ3: Student(baseline) / Student(KD) の学習 ---
def train_student(use_kd=False, T=4.0, alpha=0.3, epochs=60, lr=1e-3):
    torch.manual_seed(0)
    student = MLP(hidden=8, n_layers=1)
    optimizer = torch.optim.Adam(student.parameters(), lr=lr)

    for epoch in range(epochs):
        student.train()
        for xb, yb in train_loader:
            optimizer.zero_grad()
            z_s = student(xb)
            if use_kd:
                with torch.no_grad():
                    z_t = teacher(xb)          # Teacherは推論のみ、勾配は流さない
                loss = distillation_loss(z_s, z_t, yb, T=T, alpha=alpha)
            else:
                loss = F.cross_entropy(z_s, yb)
            loss.backward()
            optimizer.step()

    return evaluate(student)

acc_student_base = train_student(use_kd=False)
acc_student_kd = train_student(use_kd=True, T=4.0, alpha=0.3)

# --- ステップ4: 3つを比較 ---
print(f"Teacher          : acc={acc_teacher:.4f}")
print(f"Student(baseline): acc={acc_student_base:.4f}")
print(f"Student(KD)      : acc={acc_student_kd:.4f}")
# 出力例(乱数・実行環境により変動):
# Teacher          : acc=0.7517
# Student(baseline): acc=0.6217
# Student(KD)      : acc=0.6783
# → Student(KD)はStudent(baseline)よりTeacherに近づいた(Teacherには届かないが差が縮まる)

# --- ステップ5: 温度Tの感度確認 ---
for T in [1, 2, 4, 8]:
    acc = train_student(use_kd=True, T=T, alpha=0.3)
    print(f"T={T:<3} Student(KD) acc={acc:.4f}")
# T=1  acc=0.6417  (ほぼ通常のsoftmaxのまま。分布が鋭くdark knowledgeが伝わりにくい)
# T=2  acc=0.6650  (なだらかになり始め、改善が出てくる)
# T=4  acc=0.6783  (ちょうどよくなだらか。baselineとの差が最大)
# T=8  acc=0.6533  (なだらかすぎてクラス間の情報がぼやけ、やや悪化)
🧭
タスク6・考察: Knowledge Distillationにより、小さなStudentモデルはbaseline(正解ラベルのみ)よりtest精度が明確に向上し、Teacherとbaselineの差の一部を埋めることができた。ただしTeacher自体には届かず、温度Tを上げすぎる(8)と教師分布がなだらかになりすぎて情報がぼやけ、逆に効果が下がることも確認できた。T=4前後が今回の設定では最も効果的だった。

📊 結果の可視化(3者比較 と 温度T感度)

Teacher / Student(baseline) / Student(KD) の test精度 0.0 0.8 Teacher Student (baseline) Student (KD, T=4) → Student(KD)はStudent(baseline)より明確に高く、Teacherとの差の一部を埋めている
温度T別: Student(KD)のtest精度(山型のトレードオフ) 0.60 0.70 T=1 T=2 T=4 T=8 → T=4付近が山のピーク。小さすぎると鋭い分布で暗黙知が伝わらず、大きすぎると分布がぼやける
🔍
これらの図は模式的なイメージ(実際の数値は実行環境により変動)。重要なのは「Student(KD)はbaselineとTeacherの中間に位置する」点と、「Tには精度を最大化する適度な値が存在し、両端では効果が弱まる山型の関係」という2つの構造である。

🪜 Step-by-Step 解説

1なぜT²という係数が必要なのか

soft_loss = F.kl_div(...) * (T ** 2)
🔗
なぜこうするか: 温度Tでsoftmaxをやわらかくすると、そのsoftmax出力に対する勾配の大きさはおよそ1/T²のスケールに縮小されてしまう。これを補正せずにhard_loss(Tの影響を受けない通常のCE損失)とそのまま足し合わせると、Tが大きいほど「ソフト損失側の勾配が実質的に極端に弱くなる」という意図しない現象が起きる。Hintonらの原論文はこの点を明示的に指摘しており、T²を掛けることで「Tを変えてもソフト損失側の勾配の実効的な大きさがハード損失側と同程度に保たれる」ようにしている。数式中の唐突なT²は、Tを変えても損失のバランスが崩れないようにするための地味だが重要な補正項である。

2KLDivLossの「入力は対数確率、targetは確率」という非対称な仕様

soft_loss = F.kl_div(
    F.log_softmax(z_s / T, dim=1),   # 入力: 対数確率
    F.softmax(z_t / T, dim=1),        # target: 確率(対数を取らない)
    reduction="batchmean",
)
🧩
なぜこうするか: PyTorchのKLDivLossは数値計算上の効率のため、入力側だけ先に対数を取った状態で渡すことを要求する。ここを間違えて両方にsoftmax、あるいは両方にlog_softmaxを使うと、値そのものは何かしら計算されてしまうため気づきにくいバグになる。「入力=生徒(log_softmax)、target=教師(softmax)」という順番と非対称性を機械的に覚えておく必要がある。

3なぜTeacherにtorch.no_grad()をかけるのか

with torch.no_grad():
    z_t = teacher(xb)
🎯
なぜこうするか: Knowledge Distillationは「すでに学習が完了した固定のTeacher」から「これから学習するStudent」へ一方向に知識を渡す仕組みである。no_grad()を忘れるとTeacherの出力z_tも計算グラフに含まれ、backward()のタイミングでTeacherのパラメータにまで勾配が伝わろうとしてしまう(optimizerに登録していないので実際には更新されないが、無駄な計算グラフの構築でメモリと計算時間を浪費する)。「TeacherはStudentにとっての固定された観測対象であり、学習対象ではない」という役割分担をコード上で明示的に守る必要がある。

4温度Tが確率分布の「なだらかさ」をどう変えるか

⚖️
なぜこうするか: softmaxの式σ(z/T)i = exp(zi/T) / Σjexp(zj/T)を見ると、Tが大きいほど各logitの差が割り算で圧縮され、出力分布は0〜1の間でより均等に近づく。T=1(通常のsoftmax)では教師の分布はほぼ「正解クラスがほぼ1、残りはほぼ0」という鋭い分布になりやすく、この状態ではdark knowledge(残りのクラスへの微妙な確信度の差)がほとんど見えなくなる。Tを上げることで「クラスBともクラスCとも少し似ている」という情報が数値として見えるようになるが、上げすぎると全クラスがほぼ均等(一様分布)に近づき、「どのクラスに似ているか」という情報自体がぼやけて消える。今日の実験のT=8での悪化は、まさにこの「情報がぼやけすぎた」状態である。

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

📷
「温度」を、写真のコントラスト調整で理解する:

softmaxの温度Tは、写真編集アプリの「コントラスト」のつまみだとイメージしてください。Tが小さい(低温)ほどコントラストが強くなり、白は真っ白・黒は真っ黒にはっきり分かれます(=教師の確信度が「ほぼ100%正解、残りはほぼ0%」という鋭い分布)。Tを大きくする(高温にする)ほどコントラストが弱まり、グレーの階調が見えるようになります(=教師の「実はクラスBとも少し似ている」という中間的な確信度が数値として見えてくる)。ただしコントラストを弱めすぎると画像全体がベタッとしたグレー一色に近づき何が写っているか分からなくなるように、Tを上げすぎると確率分布が一様分布に近づき、クラス間の違いの情報自体が失われます。
🌤️
「KLダイバージェンス」を、2つの天気予報の"ズレ"で理解する:

KLダイバージェンスKL(P‖Q)は「2つの確率分布PとQがどれくらい違うか」を測る指標です。ベテラン予報士(教師)が「明日の天気は晴れ70%・曇り20%・雨10%」と予報し、新人予報士(生徒)が「晴れ40%・曇り40%・雨20%」と予報したとします。KLダイバージェンスは、この2つの予報の「確信度の配分のズレ」を数値化したものです。KD式のKL(σ(zt/T)‖σ(zs/T))は、まさに「教師の天気予報に、生徒の天気予報をどれだけ近づけられているか」を測り、そのズレを小さくするように生徒モデルを学習させている、という意味になります。

🏆 Kaggleでの実践的な使い方

よく使われるコンペカテゴリ: ☐ 表形式データ(Tabular) / ☑ 自然言語処理(NLP) / ☑ 画像認識(CV) / ☐ 時系列(Time Series)

実例

⚡ 推論時間制約のあるコンペでの軽量化

アンサンブル→単一モデルへの蒸留

複数の大きなモデルをアンサンブル(Teacher役)し、その知識を単一の軽量モデル(Student役)に蒸留することで、精度をあまり落とさずに推論を高速化する。

応用

🔁 Self-Distillation(自己蒸留)

同じアーキテクチャでも効果あり

「1回目に学習したモデル」をTeacherに、「もう一度ゼロから学習するモデル」をStudentにすると2回目の性能が上がることがある(Born-Again Networks)。

発展

🏷️ 半教師あり学習との組み合わせ

Noisy Studentなどの疑似ラベル手法

ラベルなしデータにTeacherが出したsoft labelをそのまま疑似ラベルとしてStudentに学習させるKDの発展形。大量のラベルなしデータがある状況で強力。

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
Teacherの出力にも勾配を流してしまい、TeacherとStudentが一緒に学習されてしまうtorch.no_grad()やmodel.eval()を付け忘れるKDは「完成したTeacher」から「学習中のStudent」への一方向の知識伝達。Teacher側は常に固定し、no_grad()で推論専用にする必要がある
KLDivLossの引数の順番(どちらにlog_softmax、どちらにsoftmaxを使うか)を逆にしてしまう「同じsoftmaxなら順番はどちらでもよさそう」という思い込みPyTorchのKLDivLossは入力側だけ対数を取った状態を要求する非対称なAPI。逆にすると誤った値が計算され、気づきにくいバグになる
T²の補正を省略してしまう数式の中のT²が「地味な係数」に見えて見落とされやすいT²を省略すると温度Tを変えるたびにソフト損失側の勾配の実効的な強さが変わってしまい、alphaで意図したハード/ソフトのバランスが崩れる。原論文が明示的に要求している補正であり省略できない
温度Tを上げれば上げるほどKDの効果が高まると考えてしまう「情報をやわらかくするほど伝わりやすくなる」という直感的な類推Tを上げすぎると教師の確率分布が一様分布に近づき、クラス間の違いという肝心の情報自体がぼやけて消える。今日の実験でもT=8でT=4より精度が悪化した。適度なTを選ぶ必要がある

🚀 次のステップ

  • 発展: alpha(ハード損失とソフト損失の重み)も[0.1, 0.3, 0.5, 0.7, 0.9]のように変化させ、Tとalphaの2軸でどの組み合わせがStudent(KD)の精度を最大化するかグリッドサーチしてみましょう。TとalphaはKDでは独立ではなく相互作用することが多く、両方を動かして初めて見える傾向があります
  • 次回予告: Phase6テーマ1「SOTA手法の追跡・論文実装」は今日で完走です(Label Smoothing → SAM → EMA → Mixup → Knowledge Distillation の5題)。Day 131からはPhase6テーマ2「カスタムアーキテクチャ設計」に入ります。1つ目のテーマとして、表形式データ向けのResidual接続を持つMLPブロックの自作設計(層を深くしても学習が壊れにくくする、Skip Connectionの直感と実装)を扱います

Phase 6 の学習マップ(全20テーマ)

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

Day126でLabel Smoothing(損失関数の改造)、Day127でSAM(Optimizerの改造)、Day128でEMA(学習ループ外での重みの後処理)、Day129でMixup(入力データそのものの改造)、Day130でKnowledge Distillation(2つのモデルの協調学習)と、「論文→実装」ワークフローを5つの異なる切り口で反復完走。次はいよいよカスタムアーキテクチャ設計へ。

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

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