本节摘要:Fine-tuning(微调)是在预训练模型参数的基础上,用目标任务数据继续训练、小幅更新权重的工序。关键字在"微":小学习率、有限轮数、常常只动部分层。本节讲清微调与从零训练、与特征提取的差别,用参数更新量的代码演示"微"在哪里,并给出微调的三个标配动作。
工程队里说"微调",指的是:旧宅主体保留,把墙面重新粉刷、线路局部改线——房子还是那栋房子,但住起来合你的需求。放到模型上,就是以预训练权重为初始值,在你的目标数据上继续跑梯度下降。对比之下:
三者不是互斥的教条,而是同一光谱上的三档力度。第 4 章会把这三档展开成完整的施工模式选择,本节先把"微调"这一档的机理讲透。
微调最容易误解的地方是以为"继续训练"就等于微调。其实微调的精髓在约束,三个控制阀缺一不可:
阀一:小学习率。 从零训练常用千分之几量级的学习率;微调预训练骨干通常只用它的十分之一甚至更小。原因直白:预训练权重已经在一个好位置上,大步长会把好不容易学到的特征震碎。
import torch import torch.nn as nn # 演示学习率对预训练权重的破坏程度 torch.manual_seed(0) w = torch.ones(1000) # 假想这是已学好的某层权重 g = torch.randn(1000) * 0.1 # 一次典型的梯度 for lr in [1e-1, 1e-2, 1e-3, 1e-4]: w_new = w - lr * g drift = (w_new - w).norm().item() / w.norm().item() print(f"学习率 {lr:.0e}: 单步权重相对漂移 {drift:.4%}") # 输出: # 学习率 1e-01: 单步权重相对漂移 0.3162% # 学习率 1e-02: 单步权重相对漂移 0.0316% # 学习率 1e-03: 单步权重相对漂移 0.0032% # 学习率 1e-04: 单步权重相对漂移 0.0003% # 一次更新看似都小,但一个轮次上千步累积,大学习率的破坏是乘性放大的
阀二:有限轮数加早停。 微调通常几个轮次就收敛,训得越久越过拟合。第 4.6 节的监理工具就是为此准备的。
阀三:限定更新范围。 浅层冻结、深层微调,或者用极小的分层学习率,让"承重墙"几乎不动。
两档力度的差别用参数量看最直观。以 ResNet-18 换十分类头为例:
import torch.nn as nn from torchvision import models def count_trainable(model): return sum(p.numel() for p in model.parameters() if p.requires_grad) # 力度一:特征提取(冻结骨干,只训新头) m1 = models.resnet18() for p in m1.parameters(): p.requires_grad = False # 整栋旧宅上保护罩 m1.fc = nn.Linear(512, 10) # 新门厅 print(f"特征提取可训练参数: {count_trainable(m1):,}") # 输出: 5,130 # 力度二:微调(骨干可训练 + 新头) m2 = models.resnet18() m2.fc = nn.Linear(512, 10) print(f"全量微调可训练参数: {count_trainable(m2):,}") # 输出: 11,231,130 # 差距约 2190 倍:微调翻新的是整栋楼的墙面,特征提取只装了个新门
读数:微调的可训练参数约一千一百二十三万,特征提取只有五千出头。参数量差三个数量级,但这不代表微调一定更好——数据只有几百张时,大参数量意味着大过拟合风险,此时"少动"反而安全。选择逻辑留给第 4 章,这里先建立手感。
不管视觉还是语言,一次标准微调流程都由三个动作构成:
# 三个动作的最小骨架(视觉十分类) import torch import torch.nn as nn from torchvision import models model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) model.fc = nn.Linear(model.fc.in_features, 10) # 动作一:换头 optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4) # 动作二:小学习率续训 criterion = nn.CrossEntropyLoss() # model.train() # for x, y in dataloader: # 动作二:常规训练循环(略) # optimizer.zero_grad() # loss = criterion(model(x), y) # loss.backward(); optimizer.step() best_acc = 0.0 # for epoch in range(num_epochs): # 动作三:每轮验证,存最优(略) # acc = evaluate(model, val_loader) # if acc > best_acc: # best_acc = acc # torch.save(model.state_dict(), "best") print("三个动作:换头、小学习率续训、验证存优。骨架如此,细节在第4章。")
💡 关键直觉:微调不是"再训练一遍",而是"带着保护措施的继续训练"。保护措施(小步长、早停、冻结)才是它与从零训练的本质区别。