今日目标 :掌握迁移学习的两种范式、冻结策略选择、用预训练模型在10分钟内训练一个高精度分类器
预计阅读 :10分钟 | 动手操作:40分钟
一、迁移学习:90%的AI项目都在用
markdown
从零训练一个ResNet-50(ImageNet):
- 需要120万张图片
- 需要8张V100训练2-3天
- 需要大量调参经验
迁移学习:
- 下载预训练权重(1分钟)
- 在你自己的几百张图微调(10分钟)
- 就能达到不错的准确率
这就是站在巨人肩膀上的力量!
python
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import DataLoader, Dataset
from torchvision import datasets, transforms, models
import numpy as np
from collections import OrderedDict
torch.manual_seed(42)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
二、核心知识点
2.1 两种范式:特征提取 vs 微调
python
"""
迁移学习的两大范式:
┌────────────────┬─────────────────────┬─────────────────────┐
│ │ 特征提取 │ 微调 │
│ │ (Feature Extraction)│ (Fine-tuning) │
├────────────────┼─────────────────────┼─────────────────────┤
│ 怎么做 │ 冻结backbone, │ 解冻backbone, │
│ │ 只训练分类头 │ 全部参数参与训练 │
│ 适用场景 │ 数据少(<1000张) │ 数据多(>1000张) │
│ │ 和预训练任务相似 │ 和预训练任务有差异 │
│ 训练速度 │ 快(只训练几层) │ 慢(训练全部参数) │
│ 过拟合风险 │ 低 │ 中等 │
│ 准确率上限 │ 中等 │ 高 │
└────────────────┴─────────────────────┴─────────────────────┘
端侧AI推荐:
1. 先做特征提取(快,baseline)
2. 如果准确率不够,再做微调(提升上限)
3. 数据量决定解冻多少层
"""
2.2 冻结层的基础操作
python
def freeze_unfreeze_demo():
"""演示冻结和解冻层的各种操作"""
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
# ===== 方法1:冻结所有层 =====
for param in model.parameters():
param.requires_grad = False
# 只训练最后的全连接层
model.fc = nn.Linear(512, 10) # 新的分类头,默认requires_grad=True
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
total = sum(p.numel() for p in model.parameters())
print(f"特征提取模式: {trainable:,}/{total:,} 参数可训练 ({trainable/total*100:.1f}%)")
# ===== 方法2:只解冻最后几层 =====
model2 = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
# 先全部冻结
for param in model2.parameters():
param.requires_grad = False
# 解冻layer4和fc
for param in model2.layer4.parameters():
param.requires_grad = True
model2.fc = nn.Linear(512, 10)
trainable = sum(p.numel() for p in model2.parameters() if p.requires_grad)
total = sum(p.numel() for p in model2.parameters())
print(f"部分解冻模式: {trainable:,}/{total:,} 参数可训练 ({trainable/total*100:.1f}%)")
# ===== 方法3:全部解冻(完整微调)=====
model3 = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
model3.fc = nn.Linear(512, 10)
trainable = sum(p.numel() for p in model3.parameters() if p.requires_grad)
total = sum(p.numel() for p in model3.parameters())
print(f"完整微调模式: {trainable:,}/{total:,} 参数可训练 ({trainable/total*100:.1f}%)")
freeze_unfreeze_demo()
2.3 冻结策略:解冻多少层?
python
def demonstrate_freeze_strategies():
"""
三种冻结策略,根据数据量选择
数据量和冻结策略的关系:
┌──────────────────┬──────────────────────┬──────────────────┐
│ 数据量 │ 推荐策略 │ 解冻范围 │
├──────────────────┼──────────────────────┼──────────────────┤
│ < 100张 │ 特征提取 │ 仅fc层 │
│ 100-1000张 │ 浅层冻结 │ layer4 + fc │
│ 1000-10000张 │ 逐层解冻 + 差分lr │ layer3/4 + fc │
│ > 10000张 │ 完整微调 │ 全部 │
└──────────────────┴──────────────────────┴──────────────────┘
"""
model = models.resnet50(weights=models.ResNet50_Weights.DEFAULT)
# 策略1:特征提取(最少数据)
def freeze_all_except_fc(model, num_classes):
for param in model.parameters():
param.requires_grad = False
model.fc = nn.Linear(model.fc.in_features, num_classes)
return model
# 策略2:逐层解冻(中等数据)
def gradual_unfreeze(model, num_classes, unfreeze_from=4):
"""从第unfreeze_from层开始解冻"""
for param in model.parameters():
param.requires_grad = False
# ResNet的层级结构
layer_groups = [
['conv1', 'bn1'], # 0
['layer1'], # 1
['layer2'], # 2
['layer3'], # 3
['layer4'], # 4
]
for group in layer_groups[unfreeze_from:]:
for name in group:
for param in getattr(model, name).parameters():
param.requires_grad = True
model.fc = nn.Linear(model.fc.in_features, num_classes)
return model
# 策略3:差分学习率(较多数据)
def discriminative_lr(model, num_classes, lr_mult=0.1):
"""
不同层用不同的学习率
浅层(通用特征)→ 小学习率
深层(任务相关)→ 大学习率
"""
for param in model.parameters():
param.requires_grad = True
model.fc = nn.Linear(model.fc.in_features, num_classes)
# 分组设置不同学习率
param_groups = [
{'params': model.conv1.parameters(), 'lr': 1e-4 * lr_mult},
{'params': model.bn1.parameters(), 'lr': 1e-4 * lr_mult},
{'params': model.layer1.parameters(), 'lr': 1e-4 * lr_mult},
{'params': model.layer2.parameters(), 'lr': 1e-4 * lr_mult * 2},
{'params': model.layer3.parameters(), 'lr': 1e-4 * lr_mult * 5},
{'params': model.layer4.parameters(), 'lr': 1e-4},
{'params': model.fc.parameters(), 'lr': 1e-3},
]
return model, param_groups
print("冻结策略选择:")
print(" < 100张: 冻结全部,只训练fc")
print(" 100-1k张: 解冻layer4 + fc")
print(" 1k-10k张: 逐层解冻 + 差分学习率")
print(" > 10k张: 全部解冻,完整微调")
demonstrate_freeze_strategies()
2.4 预训练模型加载
python
def load_pretrained_models():
"""演示各种预训练模型加载方式"""
# ===== 1. torchvision自带模型(最常用)=====
# ResNet系列
resnet18 = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
resnet50 = models.resnet50(weights=models.ResNet50_Weights.DEFAULT)
# EfficientNet
efficientnet = models.efficientnet_b0(weights=models.EfficientNet_B0_Weights.DEFAULT)
# MobileNet(端侧AI常用)
mobilenet_v3_small = models.mobilenet_v3_small(weights=models.MobileNet_V3_Small_Weights.DEFAULT)
mobilenet_v3_large = models.mobilenet_v3_large(weights=models.MobileNet_V3_Large_Weights.DEFAULT)
# Vision Transformer
vit = models.vit_b_16(weights=models.ViT_B_16_Weights.DEFAULT)
# ===== 2. 加载本地权重文件 =====
# model = models.resnet50()
# model.load_state_dict(torch.load('path/to/weights.pth'))
# ===== 3. 只加载部分权重(不匹配的层跳过)=====
# pretrained_dict = torch.load('weights.pth')
# model_dict = model.state_dict()
# pretrained_dict = {k: v for k, v in pretrained_dict.items() if k in model_dict}
# model_dict.update(pretrained_dict)
# model.load_state_dict(model_dict)
# ===== 4. 加载不同输入尺寸的模型 =====
# 原始ImageNet: 224×224
# 你的数据: 64×64
# 只需要改第一层卷积的stride!
# model = models.resnet18(weights=None)
# model.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False)
# model.maxpool = nn.Identity()
# 然后加载预训练权重(conv1部分跳过)
models_info = {
'ResNet-18': (sum(p.numel() for p in resnet18.parameters()), 'CNN'),
'ResNet-50': (sum(p.numel() for p in resnet50.parameters()), 'CNN'),
'EfficientNet-B0': (sum(p.numel() for p in efficientnet.parameters()), 'CNN'),
'MobileNetV3-Small': (sum(p.numel() for p in mobilenet_v3_small.parameters()), '端侧CNN'),
'MobileNetV3-Large': (sum(p.numel() for p in mobilenet_v3_large.parameters()), '端侧CNN'),
'ViT-B/16': (sum(p.numel() for p in vit.parameters()), 'Transformer'),
}
print("常用预训练模型:")
for name, (params, arch_type) in models_info.items():
print(f" {name:<20s}: {params:>12,} 参数 ({arch_type})")
load_pretrained_models()
三、动手实践
3.1 实战:完整迁移学习Pipeline
python
class TransferLearningPipeline:
"""
完整的迁移学习Pipeline
支持三种模式:
1. feature_extraction: 冻结backbone,只训练分类头
2. partial_finetune: 解冻最后几层
3. full_finetune: 训练全部参数
"""
def __init__(self, model_name='resnet50', num_classes=10,
mode='feature_extraction', device='cuda'):
self.device = torch.device(device if torch.cuda.is_available() else 'cpu')
self.mode = mode
self.num_classes = num_classes
self.model = self._build_model(model_name)
self.best_acc = 0
def _build_model(self, model_name):
"""构建模型并设置训练模式"""
# 加载预训练模型
model = models.resnet50(weights=models.ResNet50_Weights.DEFAULT)
# 获取预训练时的预处理参数
self.preprocess_mean = [0.485, 0.456, 0.406]
self.preprocess_std = [0.229, 0.224, 0.225]
if self.mode == 'feature_extraction':
# 冻结所有层
for param in model.parameters():
param.requires_grad = False
model.fc = nn.Linear(model.fc.in_features, self.num_classes)
elif self.mode == 'partial_finetune':
# 冻结前3个stage
for param in model.parameters():
param.requires_grad = False
for param in model.layer4.parameters():
param.requires_grad = True
model.fc = nn.Linear(model.fc.in_features, self.num_classes)
elif self.mode == 'full_finetune':
model.fc = nn.Linear(model.fc.in_features, self.num_classes)
return model.to(self.device)
def get_optimizer(self, lr=0.001):
"""根据模式选择优化器"""
if self.mode == 'feature_extraction':
# 只训练fc,可以用大学习率
return optim.AdamW(self.model.fc.parameters(), lr=lr, weight_decay=1e-4)
elif self.mode == 'partial_finetune':
# 差分学习率
params = [
{'params': self.model.layer4.parameters(), 'lr': lr * 0.1},
{'params': self.model.fc.parameters(), 'lr': lr},
]
return optim.AdamW(params, weight_decay=1e-4)
else: # full_finetune
# 差分学习率:backbone小lr,fc大lr
backbone_params = []
fc_params = []
for name, param in self.model.named_parameters():
if 'fc' in name:
fc_params.append(param)
else:
backbone_params.append(param)
params = [
{'params': backbone_params, 'lr': lr * 0.1},
{'params': fc_params, 'lr': lr},
]
return optim.AdamW(params, weight_decay=1e-4)
def train_one_epoch(self, loader, optimizer, criterion):
self.model.train()
total_loss, correct, total = 0, 0, 0
for x, y in loader:
x, y = x.to(self.device), y.to(self.device)
optimizer.zero_grad()
loss = criterion(self.model(x), y)
loss.backward()
optimizer.step()
total_loss += loss.item() * x.size(0)
correct += (self.model(x).argmax(1) == y).sum().item()
total += y.size(0)
return total_loss / total, correct / total
@torch.no_grad()
def evaluate(self, loader, criterion):
self.model.eval()
total_loss, correct, total = 0, 0, 0
for x, y in loader:
x, y = x.to(self.device), y.to(self.device)
output = self.model(x)
loss = criterion(output, y)
total_loss += loss.item() * x.size(0)
correct += output.argmax(1).eq(y).sum().item()
total += y.size(0)
return total_loss / total, correct / total
def fit(self, train_loader, val_loader, epochs=20):
optimizer = self.get_optimizer()
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)
criterion = nn.CrossEntropyLoss()
# 打印训练信息
trainable = sum(p.numel() for p in self.model.parameters() if p.requires_grad)
total = sum(p.numel() for p in self.model.parameters())
print(f"模式: {self.mode}")
print(f"可训练参数: {trainable:,}/{total:,} ({trainable/total*100:.1f}%)")
print(f"{'Epoch':<6s} {'Train Loss':<12s} {'Train Acc':<10s} {'Val Loss':<12s} {'Val Acc':<10s}")
print('-' * 55)
for epoch in range(epochs):
train_loss, train_acc = self.train_one_epoch(train_loader, optimizer, criterion)
val_loss, val_acc = self.evaluate(val_loader, criterion)
scheduler.step()
if val_acc > self.best_acc:
self.best_acc = val_acc
torch.save(self.model.state_dict(), 'best_transfer_model.pth')
if epoch % 5 == 0:
print(f"{epoch+1:<6d} {train_loss:<12.4f} {train_acc:<10.2%} "
f"{val_loss:<12.4f} {val_acc:<10.2%}")
print(f"\n最佳准确率: {self.best_acc:.2%}")
self.model.load_state_dict(torch.load('best_transfer_model.pth'))
return self.model
# 演示:在CIFAR-10上迁移学习
def demo_transfer_learning():
"""对比三种迁移学习模式"""
# 数据准备
train_transform = transforms.Compose([
transforms.Resize(224), # ImageNet预训练输入是224×224
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
])
val_transform = transforms.Compose([
transforms.Resize(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
])
train_dataset = datasets.CIFAR10('./data', train=True, download=True, transform=train_transform)
val_dataset = datasets.CIFAR10('./data', train=False, transform=val_transform)
# 只用少量数据模拟真实场景
indices = np.random.choice(len(train_dataset), 2000, replace=False)
train_subset = torch.utils.data.Subset(train_dataset, indices)
train_loader = DataLoader(train_subset, batch_size=64, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=64, shuffle=False)
modes = ['feature_extraction', 'partial_finetune', 'full_finetune']
for mode in modes:
print(f"\n{'='*50}")
print(f"模式: {mode}")
print(f"{'='*50}")
pipeline = TransferLearningPipeline(
model_name='resnet50', num_classes=10, mode=mode, device='cuda'
)
pipeline.fit(train_loader, val_loader, epochs=20)
# 运行(可选,需要GPU)
# demo_transfer_learning()
3.2 实战:自定义数据集迁移学习
python
def custom_dataset_transfer():
"""
模板:在自己的数据集上做迁移学习
假设你的数据是这样的:
data/
├── train/
│ ├── class_0/
│ │ ├── img001.jpg
│ │ └── ...
│ ├── class_1/
│ └── ...
└── val/
├── class_0/
└── ...
"""
# 1. 数据加载
data_dir = './data/my_dataset'
train_transform = transforms.Compose([
transforms.Resize(256),
transforms.RandomCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ColorJitter(0.1, 0.1, 0.1, 0.05),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
])
val_transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
])
# 2. 加载预训练模型
model = models.resnet50(weights=models.ResNet50_Weights.DEFAULT)
# 3. 获取数据集的类别数
# num_classes = len(os.listdir(data_dir + '/train'))
num_classes = 10 # 示例
# 4. 替换分类头
model.fc = nn.Sequential(
nn.Linear(model.fc.in_features, 512),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(512, num_classes),
)
# 5. 冻结策略
print("自定义数据集迁移学习模板:")
print(" 1. 数据路径: ImageFolder格式")
print(" 2. 预处理: Resize → CenterCrop → Normalize (ImageNet统计量)")
print(" 3. 分类头: 替换fc层,可以加Dropout")
print(" 4. 训练: 差分学习率 + AdamW + CosineAnnealing")
print(" 5. 保存: 只保存state_dict,不保存整个模型")
return model
custom_dataset_transfer()
3.3 实战:端侧模型迁移学习
python
def edge_transfer_learning():
"""
端侧AI的迁移学习策略
与服务器端的不同:
1. 选轻量模型:MobileNetV3/EfficientNet-lite/ShuffleNet
2. 考虑量化:训练时做QAT(量化感知训练)
3. 输入尺寸:不一定是224×224,端侧更小
4. 推理速度优先:准确率差不多就行
"""
# 端侧推荐模型
edge_models = {
'MobileNetV3-Small': models.mobilenet_v3_small,
'MobileNetV3-Large': models.mobilenet_v3_large,
'EfficientNet-B0': models.efficientnet_b0,
'ShuffleNetV2': models.shufflenet_v2_x1_0,
}
print("端侧AI迁移学习推荐:")
for name, model_fn in edge_models.items():
model = model_fn(weights='DEFAULT')
params = sum(p.numel() for p in model.parameters())
print(f" {name:<20s}: {params:>10,} 参数")
print("\n端侧迁移学习要点:")
print(" 1. 选MobileNet/EfficientNet,不要用ResNet-50")
print(" 2. 输入尺寸可以缩小到160×160或128×128")
print(" 3. 如果有数据,完整微调效果更好")
print(" 4. 训练后考虑量化,进一步减小模型")
print(" 5. 用ONNX导出,方便端侧部署")
edge_transfer_learning()
四、迁移学习黄金法则
python
"""
迁移学习黄金法则:
1. 数据量决定策略
- 数据越少,冻结越多
- 数据越多,解冻越多
2. 相似度决定学习率
- 和预训练任务越相似,backbone学习率越小
- 和预训练任务差异越大,backbone学习率越大
3. 分类头学习率是backbone的10倍
- backbone: lr=1e-4
- fc: lr=1e-3
4. 先用小学习率,看loss趋势
- 如果loss下降慢 → 提高学习率
- 如果loss震荡 → 降低学习率
5. 模型保存只保存state_dict
- 不要保存整个模型(依赖代码结构)
- 保存时带上类别数和预处理参数
6. 端侧AI的特殊考虑
- 优先选轻量backbone
- 考虑量化部署
- 输入尺寸可以缩小
"""
五、常见坑点
坑1:忘记用预训练模型的预处理
python
# ❌ 错误:用了预训练模型,但数据没做ImageNet归一化
# preprocess = transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
# ✅ 正确:必须用和预训练时相同的归一化
# preprocess = transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
坑2:换了分类头但忘记初始化
python
# ✅ 好的做法:显式初始化新添加的层
model.fc = nn.Linear(512, 10)
nn.init.kaiming_normal_(model.fc.weight)
nn.init.zeros_(model.fc.bias)
# PyTorch的Linear默认就是Kaiming初始化,但显式写更安全
坑3:冻结所有层后分类头没设requires_grad
python
# ❌ 错误顺序
for param in model.parameters():
param.requires_grad = False
model.fc = nn.Linear(512, 10) # 新层默认requires_grad=True ✓
# 但如果是先改fc再冻结:
model.fc = nn.Linear(512, 10)
for param in model.parameters():
param.requires_grad = False # ❌ fc也被冻结了!
坑4:小数据集上完整微调
python
# 如果你只有200张图,完整微调ResNet-50 → 严重过拟合
# 200张图 → 特征提取模式(只训练fc)
# 2000张图 → 可以解冻layer4
# 20000张图 → 完整微调
六、今日作业
- 特征提取练习:用ResNet-18做特征提取,在CIFAR-10上训练最后一个fc层,记录准确率
- 冻结策略对比:对比"仅训练fc" vs "解冻layer4+fc" vs "全部微调",记录准确率和训练时间
- 端侧思考:你的端侧AI项目适合用哪个预训练模型?为什么?
- 打卡 :评论区发你的对比结果,格式:"Day 27/100 打卡:迁移学习已掌握!"
今日小结
今天你学会了:
✅ 迁移学习两大范式:特征提取 vs 微调
✅ 冻结/解冻层的基础操作
✅ 三种冻结策略:全部冻结/逐层解冻/差分学习率
✅ 数据量决定策略(<100张→100-1k→1k-10k→>10k)
✅ 常用预训练模型加载(ResNet/EfficientNet/MobileNet/ViT)
✅ 完整TransferLearningPipeline
✅ 自定义数据集迁移学习模板
✅ 端侧AI迁移学习策略
✅ 6条黄金法则 + 4个经典坑点
明日预告
Day 28:目标检测入门 --- 两阶段vs单阶段
R-CNN系列、YOLO系列、SSD、Anchor机制、NMS、mAP
🔥 关注我,每天解锁一个端侧AI技能!
微信公众号:xxx | 小红书:xxx | CSDN:xxx
评论区打卡,一起坚持100天!
附:小红书图文版
封面标题建议:迁移学习 | 10分钟训练高精度模型 🎯
P1 --- 封面
标题:迁移学习与微调
副标题:特征提取 / 微调 / 冻结策略
关键词:迁移学习 / Fine-tuning / 预训练
P2 --- 两大范式
特征提取:冻结backbone,只训练分类头
→ 数据少,速度快,过拟合少
微调:解冻backbone,全部训练
→ 数据多,准确率高,上限高
P3 --- 冻结策略选择
<100张: 只训练fc
100-1k张: 解冻layer4
1k-10k张: 逐层解冻+差分lr
10k张: 完整微调
P4 --- 差分学习率
backbone: lr=1e-4 (小)
fc: lr=1e-3 (大)
浅层更小,深层更大
通用特征→小lr,任务相关→大lr
P5 --- 端侧AI迁移学习
选MobileNet/EfficientNet,不用ResNet-50
输入可以缩小到160×160
训练后考虑量化
ONNX导出方便部署
P6 --- 今日作业
冻结策略对比实验
评论区打卡 Day 27/100
标签:#迁移学习 #FineTuning #预训练模型 #深度学习
CSDN发布提示:CSDN版本建议在两大范式部分放对比图,在冻结策略部分放ResNet层级结构图,在差分学习率部分放不同层学习率分配的示意图。