使用torchvision官方预训练权重,适配自定义数据集(猫狗/CIFAR/小分类),替换最后FC层,工业标准微调流程。
两段式微调的本质逻辑
预训练模型 = 强大的特征提取器(conv) + 随机分类器(fc) 你的任务 = 需要 【你的特征】 + 【你的分类器】 两段式 = 1. 先固定特征提取器 (conv)→ 训练分类器(fc) 2. 再轻微调整个模型(conv)+ (fc)→ 让模型更贴合专项数据
最直观的比喻
-
预训练卷积 = 一位经验丰富的老师傅(会看图)
-
FC = 新来的学徒(不会分类)
两段式训练:
-
先让老师傅不动,学徒快速学习分类
-
再让老师傅稍微调整经验,配合学徒一起提升
结果:又快又稳,精度最高
微调策略
-
方案1:冻结backbone,只训练最后全连接(特征提取,小数据集防过拟合) 冻结
conv1~layer4所有卷积BN,只更新fc层。 -
方案2:解冻全部参数全量微调(大数据/相似数据集) 全部参数放开训练,学习率比预训练小10~100倍。

【原始预训练ResNet18(ImageNet)】 ↓ ┌─────────────────────────────────────────────────┐ │主干Conv+BN:已学好通用特征(边缘/纹理/轮廓) 宝贵参数 │ │FC层:1000分类→替换成自定义N分类【随机初始化】 │ └───────────────┬─────────────────────────────────┘ │ ▼阶段1:冻结主干,仅训练FC ┌─────────────────────────────────────────────────────────┐ │1.冻结所有卷积层 requires_grad=False(锁住通用特征,参数不动)│ │2.只放开FC参数,大学习率 lr=1e-3 │ │3.数据前向传播→只更新FC权重,快速适配新数据集分类规则 │ │4.训练至val稳定→保存中间权重stage1.pth │ │★效果:主干不变,分类头学会基于预训练特征做分类 │ └───────────────┬─────────────────────────────────────────┘ │ ▼阶段2:加载stage1权重,全网络解冻微调 ┌───────────────────────────────────────────────────┐ │1.全部Conv+BN+FC:requires_grad=True 全部可更新 │ │2.小学习率 lr=1e-4(阶段1的1/10,防止梯度冲垮预训练特征)│ │3.全局轻微迭代:通用特征 → 适配当前数据集的专属特征 │ │4.监控验证集,保存最优模型best.pth │ │★效果:特征+分类头双向优化,模型精度进一步提升 │ └───────────────┬───────────────────────────────────┘ │ ▼最终:落地推理部署
微调核心原理(先冻主干→再全量微调)
官方预训练的 ResNet18 已经学会了通用视觉特征(边缘、纹理、形状、物体结构),非常珍贵;而最后的 FC 层是随机初始化的,完全不懂你的数据集。 所以必须分两步:
-
先稳住珍贵的预训练特征(冻结通用特征) →只训 FC 快速适配你的任务(训练分类头)
-
再用极小学习率精细调整全部特征 → 让模型更贴合你的数据
阶段 1:冻结主干,只训练 FC
原理:保护底层通用特征,只让分类头快速学习
ResNet18 前面的卷积层(conv1 ~ layer4)是在 ImageNet 128万张图 上学到的:
-
边缘
-
纹理
-
颜色
-
形状
-
物体结构
这些特征通用、强大、珍贵,不能乱动。而最后的全连接层 fc 是随机初始化的,对你的任务一无所知。
所以第一阶段做的事:
-
冻结卷积层:不让梯度更新,保住通用特征
-
只训练 FC:让分类头快速学会“你的类别”
-
学习率大(1e-3):快速收敛
结果:
模型不会破坏底层特征,又能快速适配你的数据集。
阶段 2:解冻全部,全量微调
原理:小学习率精细调优,让通用特征 → 变成你的任务专用特征
第一阶段只是让 FC 学会分类,但卷积层还是 ImageNet 的通用特征。
你的任务和 ImageNet 不完全一样,所以:
-
需要轻微调整卷积特征
-
但不能大改(一改就破坏预训练)
所以第二阶段做的事:
-
全部解冻:卷积、BN、FC 一起更新
-
学习率极小(1e-4 是阶段1的 1/10) 防止大梯度冲毁预训练特征
结果:
通用特征 → 你的任务专用特征 精度大幅提升!
为什么不能直接全部一起训?
如果一开始就全部解冻 + 大学习率训练:
-
随机初始化的 FC 会产生巨大梯度
-
梯度会直接冲毁前面珍贵的预训练卷积特征
-
模型直接发散、过拟合、精度暴跌
这就是为什么必须先冻后解。
两段微调的核心优势
-
不破坏预训练的优质特征
-
训练稳定,不容易过拟合
-
小数据集也能训得很好
-
收敛更快,精度更高
-
工业界最标准的微调方案
实操流程
步骤1:下载预训练权重
resnet18(weights="IMAGENET1K_V1")自动下载imagenet预训练参数,不要自己随机初始化。
步骤2:替换最后一层FC
预训练FC是512→1000,改成512→你的类别数N,唯一需要改动的分类层。
步骤3:选择冻结策略
-
小数据集(几百~几千张):冻结全部卷积,只训FC
-
lr:
1e-3 ~ 5e-3,epoch:10~20
-
-
中大数据(上万张):先冻FC训5轮,再全放开微调
两阶段微调: ① 冻骨干训FC,收敛后保存权重 ② 加载权重,解冻全部参数,lr缩小至
1e-4继续微调
步骤4:超参配置
| 微调方式 | 学习率 | batch | epoch |
|---|---|---|---|
| 只训FC | 1e-3 | 16/32 | 15~25 |
| 全参数微调 | 1e-4~5e-5 | 16/32 | 25~40 |
步骤5:保存&推理
# 推理加载
net = resnet18()
net.fc = nn.Linear(512, num_classes)
net.load_state_dict(torch.load("resnet18_finetune_best.pth"))
net.eval()
完整微调代码
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
from torchvision.models import resnet18
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 1、数据预处理(沿用ImageNet归一化,和预训练一致)
mean = [0.485, 0.456, 0.406]
std = [0.229, 0.224, 0.225]
train_transform = transforms.Compose([
transforms.Resize((256,256)),
transforms.RandomCrop(224),
transforms.RandomHorizontalFlip(p=0.5),
transforms.ToTensor(),
transforms.Normalize(mean, std)
])
val_transform = transforms.Compose([
transforms.Resize((224,224)),
transforms.ToTensor(),
transforms.Normalize(mean, std)
])
# 2、数据集(替换为你的数据集路径,ImageFolder格式)
train_dataset = datasets.ImageFolder(root="./train", transform=train_transform)
val_dataset = datasets.ImageFolder(root="./val", transform=val_transform)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=2)
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=2)
# 3、加载预训练ResNet18
model = resnet18(weights="IMAGENET1K_V1") # 加载官方imagenet预训练
# ======== 【关键1:修改分类头,替换全连接层】 ========
num_classes = 2 # 改成你的类别数:猫狗=2、CIFAR=10
in_features = model.fc.in_features
model.fc = nn.Linear(in_features, num_classes)
# ======== 【关键2:选择微调模式】 ========
# 模式A:冻结骨干,只训练fc(小样本首选)
for name, param in model.named_parameters():
if "fc" not in name:
param.requires_grad = False
# 模式B:全部解冻全量微调,注释上面代码,打开下面
# for param in model.parameters():
# param.requires_grad = True
model.to(device)
# 4、优化器&损失
# 冻结骨干:lr设大一点 1e-3;全量微调:lr设很小 1e-4~1e-5
if any(p.requires_grad for n,p in model.named_parameters() if "fc" in n):
lr = 1e-3
else:
lr = 1e-4
optimizer = optim.SGD(filter(lambda p: p.requires_grad, model.parameters()),
lr=lr, momentum=0.9, weight_decay=1e-4)
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=30)
criterion = nn.CrossEntropyLoss()
# 5、训练&验证函数
def train_one_epoch():
model.train()
total_loss = 0.0
correct = 0
total = 0
for img, label in train_loader:
img, label = img.to(device), label.to(device)
optimizer.zero_grad()
out = model(img)
loss = criterion(out, label)
loss.backward()
optimizer.step()
total_loss += loss.item()
pred = torch.argmax(out, dim=1)
correct += (pred == label).sum().item()
total += label.size(0)
return total_loss/len(train_loader), correct/total
def val_one_epoch():
model.eval()
total_loss = 0.0
correct = 0
total = 0
with torch.no_grad():
for img, label in val_loader:
img, label = img.to(device), label.to(device)
out = model(img)
loss = criterion(out, label)
total_loss += loss.item()
pred = torch.argmax(out, dim=1)
correct += (pred == label).sum().item()
total += label.size(0)
return total_loss/len(val_loader), correct/total
# 6、开始微调
epoch_nums = 30
best_acc = 0.0
for epoch in range(epoch_nums):
train_loss, train_acc = train_one_epoch()
val_loss, val_acc = val_one_epoch()
scheduler.step()
print(f"Epoch:{epoch+1:2d}|TrainLoss:{train_loss:.3f},Acc:{train_acc:.3f}|ValLoss:{val_loss:.3f},Acc:{val_acc:.3f}")
# 保存最优模型
if val_acc > best_acc:
best_acc = val_acc
torch.save(model.state_dict(), "resnet18_finetune_best.pth")
print(f"Best Val Acc:{best_acc:.3f}")
240



被折叠的 条评论
为什么被折叠?



