📚 背景知識(読んでから問題へ)
[0.7, 0.2, 0.05, 0.03, 0.02]のような「クラスAが70%、でもクラスBの可能性も20%くらいある」という豊かな情報を持っています。「◯か×か」だけの採点と、「ほぼ正解だがクラスBの考え方と少し混同している」という添削コメント付きの採点の違いです。この添削コメントにあたる情報をHintonら(2015)はdark knowledge(暗黙知)と呼びました。(hidden=128)
acc 高
(hidden=8)
KDで学習
Teacherに
近づく
今日の題材: Knowledge Distillation(Hinton et al., 2015)の核心の数式:
L = α · CE(zs, y) + (1−α) · T² · KL( σ(zt/T) ‖ σ(zs/T) )
日本語で言い換えると:「①正解ラベルへの普通の交差エントロピー損失」と「②教師の出力分布(温度Tでやわらかくした確率)に生徒の出力分布を近づけるKLダイバージェンス損失」をα対(1−α)で足し合わせるだけ。料理人の弟子入りで言えば、①レシピ通り作れるかの採点、②師匠の味付けのニュアンスにどれだけ近づけたかの採点、を同時に受けるようなもの。
🪄 KDの適用ステップ(今日の学習ループの流れ)
①Teacherを先に学習
大きめのMLPを正解ラベルだけで通常学習させておく
②Teacherをno_gradで推論
Studentの学習中はTeacherに勾配を流さない
③温度T付きsoftmax
Teacher/Studentの両方のlogitをTで割ってやわらかくする
④hard+soft損失を合成
α·CE + (1−α)·T²·KLでStudentのみ更新
⑤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件 |
| Teacher | hidden=128・3層MLP、epochs=60で正解ラベルのみ通常学習 |
| Student | hidden=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通りで学習し比較する
distillation_loss(z_s, z_t, y, T, alpha)関数として実装する💡 ヒント
hidden=8という小さなStudentモデルは表現力が低く、正解ラベルだけで学習させてもhidden=128のTeacherには到底届きません。しかしTeacherの出力する確率分布には「クラスAが正解だが、クラスCとも少し似ている」といった、正解ラベル1点には存在しない追加の情報(クラス間の類似度の手がかり)が含まれています。KDはこの追加情報を小さなStudentに教え込む手法です。実装上の最重要ポイントは、Teacherは学習させず、勾配が流れるのはStudent側だけにすることです。
- 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なし)と同じになる
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 | 教師モデルのlogit | teacher(xb)(no_grad) |
| T | 温度(大きいほど確率分布がなだらかになる) | T |
| σ(z/T) | 温度付きsoftmax | F.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 (なだらかすぎてクラス間の情報がぼやけ、やや悪化)
📊 結果の可視化(3者比較 と 温度T感度)
🪜 Step-by-Step 解説
1なぜT²という係数が必要なのか
soft_loss = F.kl_div(...) * (T ** 2)
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",
)
3なぜTeacherにtorch.no_grad()をかけるのか
with torch.no_grad():
z_t = teacher(xb)
4温度Tが確率分布の「なだらかさ」をどう変えるか
🧮 数学・統計の補足(文系向け)
softmaxの温度Tは、写真編集アプリの「コントラスト」のつまみだとイメージしてください。Tが小さい(低温)ほどコントラストが強くなり、白は真っ白・黒は真っ黒にはっきり分かれます(=教師の確信度が「ほぼ100%正解、残りはほぼ0%」という鋭い分布)。Tを大きくする(高温にする)ほどコントラストが弱まり、グレーの階調が見えるようになります(=教師の「実はクラスBとも少し似ている」という中間的な確信度が数値として見えてくる)。ただしコントラストを弱めすぎると画像全体がベタッとしたグレー一色に近づき何が写っているか分からなくなるように、Tを上げすぎると確率分布が一様分布に近づき、クラス間の違いの情報自体が失われます。
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テーマ)
Day126でLabel Smoothing(損失関数の改造)、Day127でSAM(Optimizerの改造)、Day128でEMA(学習ループ外での重みの後処理)、Day129でMixup(入力データそのものの改造)、Day130でKnowledge Distillation(2つのモデルの協調学習)と、「論文→実装」ワークフローを5つの異なる切り口で反復完走。次はいよいよカスタムアーキテクチャ設計へ。
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: