4. 中间层特征蒸馏

任务目标

本案例实现中间层特征蒸馏:训练一个教师网络(较大模型)后,提取其某一中间层的特征图,设计蒸馏损失使学生网络(较小模型)的对应中间层特征图向教师网络对齐,从而提升学生网络在图像分类任务上的性能。

环境与依赖

需要 Python 3.8+、PyTorch 1.10+、torchvision。数据集使用 CIFAR-10,代码会自动下载。

核心实现

完整代码如下,包含教师网络、学生网络、特征蒸馏损失函数以及训练/验证主流程。教师网络与学生网络均使用简单CNN,教师网络更宽更深。蒸馏发生在第2个卷积块之后,通过一个1×1卷积将学生特征图通道数对齐到教师特征图通道数,然后计算MSE损失。

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader

# ---------- 网络定义 ----------
class ConvBlock(nn.Module):
    def __init__(self, in_c, out_c, pool=True):
        super().__init__()
        self.conv1 = nn.Conv2d(in_c, out_c, 3, padding=1)
        self.bn1 = nn.BatchNorm2d(out_c)
        self.conv2 = nn.Conv2d(out_c, out_c, 3, padding=1)
        self.bn2 = nn.BatchNorm2d(out_c)
        self.pool = nn.MaxPool2d(2) if pool else nn.Identity()

    def forward(self, x):
        x = F.relu(self.bn1(self.conv1(x)))
        x = F.relu(self.bn2(self.conv2(x)))
        return self.pool(x)

class TeacherNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.block1 = ConvBlock(3, 64)
        self.block2 = ConvBlock(64, 128)
        self.block3 = ConvBlock(128, 256)
        self.fc = nn.Linear(256 * 4 * 4, 10)

    def forward(self, x):
        x = self.block1(x)
        x = self.block2(x)
        x = self.block3(x)
        x = x.view(x.size(0), -1)
        return self.fc(x)

class StudentNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.block1 = ConvBlock(3, 32)
        self.block2 = ConvBlock(32, 64)
        self.block3 = ConvBlock(64, 128)
        self.fc = nn.Linear(128 * 4 * 4, 10)

    def forward(self, x):
        x = self.block1(x)
        x = self.block2(x)
        x = self.block3(x)
        x = x.view(x.size(0), -1)
        return self.fc(x)

# ---------- 特征蒸馏损失 ----------
class FeatureDistillLoss(nn.Module):
    def __init__(self, student_channel=64, teacher_channel=128):
        super().__init__()
        # 将学生特征图通道对齐到教师特征图通道
        self.align = nn.Conv2d(student_channel, teacher_channel, 1)

    def forward(self, s_feat, t_feat):
        s_feat = self.align(s_feat)
        return F.mse_loss(s_feat, t_feat.detach())

# ---------- 数据加载 ----------
transform = transforms.Compose([
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616))
])
trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
train_loader = DataLoader(trainset, batch_size=64, shuffle=True, num_workers=2)
test_loader = DataLoader(testset, batch_size=64, shuffle=False, num_workers=2)

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# ---------- 训练教师网络 ----------
def train_teacher():
    teacher = TeacherNet().to(device)
    opt = optim.Adam(teacher.parameters(), lr=1e-3)
    criterion = nn.CrossEntropyLoss()
    for epoch in range(10):
        teacher.train()
        for x, y in train_loader:
            x, y = x.to(device), y.to(device)
            opt.zero_grad()
            loss = criterion(teacher(x), y)
            loss.backward()
            opt.step()
    torch.save(teacher.state_dict(), 'teacher.pth')
    return teacher

# ---------- 特征提取钩子 ----------
def get_feature_hook(model, layer_name):
    features = {}
    def hook_fn(module, input, output):
        features[layer_name] = output
    for name, module in model.named_modules():
        if name == layer_name:
            module.register_forward_hook(hook_fn)
    return features

# ---------- 训练学生网络(含特征蒸馏) ----------
def train_student_with_distill(teacher):
    teacher.eval()
    student = StudentNet().to(device)
    opt = optim.Adam(student.parameters(), lr=1e-3)
    ce_loss = nn.CrossEntropyLoss()
    distill_loss_fn = FeatureDistillLoss().to(device)

    # 注册钩子:block2输出(即第2个卷积块后的特征图)
    teacher_feats = get_feature_hook(teacher, 'block2')
    student_feats = get_feature_hook(student, 'block2')

    alpha = 0.5  # 蒸馏损失权重
    for epoch in range(10):
        student.train()
        for x, y in train_loader:
            x, y = x.to(device), y.to(device)
            opt.zero_grad()
            out = student(x)
            loss_ce = ce_loss(out, y)
            # 教师前向传播(不计算梯度)
            with torch.no_grad():
                teacher(x)
            loss_distill = distill_loss_fn(student_feats['block2'], teacher_feats['block2'])
            loss = loss_ce + alpha * loss_distill
            loss.backward()
            opt.step()
    return student

# ---------- 验证 ----------
def evaluate(model):
    model.eval()
    correct = 0
    total = 0
    with torch.no_grad():
        for x, y in test_loader:
            x, y = x.to(device), y.to(device)
            pred = model(x).argmax(dim=1)
            correct += (pred == y).sum().item()
            total += y.size(0)
    return correct / total

if __name__ == '__main__':
    # 1. 训练教师网络
    teacher = train_teacher()
    teacher_acc = evaluate(teacher)
    print(f'Teacher accuracy: {teacher_acc:.4f}')

    # 2. 训练学生网络(带特征蒸馏)
    student = train_student_with_distill(teacher)
    student_acc = evaluate(student)
    print(f'Student (with distill) accuracy: {student_acc:.4f}')

    # 3. 对比:训练不带蒸馏的学生网络
    student_no_distill = StudentNet().to(device)
    opt = optim.Adam(student_no_distill.parameters(), lr=1e-3)
    criterion = nn.CrossEntropyLoss()
    for epoch in range(10):
        student_no_distill.train()
        for x, y in train_loader:
            x, y = x.to(device), y.to(device)
            opt.zero_grad()
            loss = criterion(student_no_distill(x), y)
            loss.backward()
            opt.step()
    student_no_distill_acc = evaluate(student_no_distill)
    print(f'Student (no distill) accuracy: {student_no_distill_acc:.4f}')

运行方式

将上述代码保存为 feature_distill.py,直接运行:

python feature_distill.py

首次运行会自动下载CIFAR-10数据集到 ./data 目录。代码依次执行:训练教师网络(10轮)→ 训练带蒸馏的学生网络(10轮)→ 训练不带蒸馏的学生网络(10轮),最后打印三个模型的测试集准确率。

验证方法与判定标准

运行结束后观察终端输出的三个准确率。判定通过的依据:

  • 教师网络准确率明显高于不带蒸馏的学生网络(通常差距在5%以上);
  • 带蒸馏的学生网络准确率高于不带蒸馏的学生网络,说明中间层特征蒸馏确实带来了性能提升。

由于随机种子未固定,每次运行准确率会有小幅波动,但上述相对关系在大多数情况下成立。若带蒸馏的学生准确率未超过不带蒸馏版本,可尝试增大 alpha 至0.7或增加训练轮数至20轮。

关键说明

本案例的蒸馏点选择在 block2 输出(即第2个卷积块后的特征图),教师特征图尺寸为 128×8×8,学生为 64×8×8。通过 FeatureDistillLoss 中的1×1卷积将学生通道从64对齐到128,再计算MSE损失。教师特征图使用 detach() 阻断梯度回传,确保只优化学生网络。若需蒸馏其他层,只需修改 get_feature_hook 中的层名并调整 FeatureDistillLoss 的通道参数即可。