迁移学习与模型微调
从头训练一个深度学习模型需要海量数据和算力。迁移学习的核心思路是:用别人已经学好的特征作为起点,只在自己的任务上做小幅调整。这是现代深度学习最重要的实践技巧之一。
为什么迁移学习有效
在 ImageNet 上训练的 ResNet,浅层学到的是边缘、颜色、纹理,这些是通用视觉特征,对几乎所有图像任务都有用。微调就是:保留通用底层特征,只让顶层适应新任务。
预训练模型(在 100 万张图像上训练好)
│
├── 底层卷积(边缘/颜色)── 冻结,不更新
├── 中层卷积(纹理/形状)── 可选冻结
└── 顶层全连接 ── 替换 + 重新训练两种基本策略:
- 特征提取(Feature Extraction):冻结所有层,只训练新的分类头。数据极少时用。
- 全量微调(Full Fine-tuning):先训练分类头,再解冻全部层一起训练,学习率极小。数据够用时精度更高。
图像分类迁移学习(torchvision)
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import models, transforms, datasets
from torch.utils.data import DataLoader
# 1. 加载预训练模型(自动下载 ImageNet 权重)
model = models.resnet50(weights=models.ResNet50_Weights.DEFAULT)
# 2a. 策略一:特征提取(冻结所有层)
for param in model.parameters():
param.requires_grad = False
# 替换最后的分类头(原来是 ImageNet 的 1000 类,改成自己的类别数)
num_classes = 5
model.fc = nn.Sequential(
nn.Dropout(0.5),
nn.Linear(model.fc.in_features, num_classes)
)
# 此时只有 model.fc 的参数 requires_grad=True
# 2b. 策略二:分层学习率微调(更推荐)
# 底层用更小的学习率,顶层用较大的学习率
param_groups = [
{"params": model.layer1.parameters(), "lr": 1e-5},
{"params": model.layer2.parameters(), "lr": 1e-5},
{"params": model.layer3.parameters(), "lr": 1e-4},
{"params": model.layer4.parameters(), "lr": 1e-4},
{"params": model.fc.parameters(), "lr": 1e-3},
]
optimizer = optim.AdamW(param_groups, weight_decay=1e-2)
# 3. 数据增强(必须用 ImageNet 的均值/标准差)
train_transform = transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),
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]),
])
# 4. 训练(两阶段:先训分类头,再解冻全部微调)
def train_phase1(model, train_loader, val_loader, epochs=10):
"""阶段一:只训练分类头"""
for param in model.parameters():
param.requires_grad = False
for param in model.fc.parameters():
param.requires_grad = True
optimizer = optim.AdamW(model.fc.parameters(), lr=1e-3, weight_decay=1e-2)
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)
criterion = nn.CrossEntropyLoss()
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)
best_acc = 0.0
for epoch in range(epochs):
model.train()
for X, y in train_loader:
X, y = X.to(device), y.to(device)
optimizer.zero_grad()
nn.CrossEntropyLoss()(model(X), y).backward()
optimizer.step()
scheduler.step()
# 验证
model.eval()
correct = total = 0
with torch.no_grad():
for X, y in val_loader:
X, y = X.to(device), y.to(device)
pred = model(X).argmax(1)
correct += (pred == y).sum().item()
total += len(y)
acc = correct / total
print(f"Phase1 Epoch {epoch+1}: Val Acc = {acc:.4f}")
if acc > best_acc:
best_acc = acc
torch.save(model.state_dict(), "phase1_best.pt")
return best_acc
def train_phase2(model, train_loader, val_loader, epochs=20):
"""阶段二:解冻全部层,用极小学习率微调"""
for param in model.parameters():
param.requires_grad = True
# 分层学习率
param_groups = [
{"params": [p for n, p in model.named_parameters()
if "layer" in n and int(n.split(".")[0][-1]) <= 2], "lr": 1e-5},
{"params": model.layer3.parameters(), "lr": 5e-5},
{"params": model.layer4.parameters(), "lr": 1e-4},
{"params": model.fc.parameters(), "lr": 5e-4},
]
optimizer = optim.AdamW(param_groups, weight_decay=1e-2)
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)
# ... 训练循环同上Hugging Face:NLP 迁移学习
对于文本任务,Hugging Face Transformers 是标准工具,几行代码完成微调:
from transformers import (
AutoTokenizer, AutoModelForSequenceClassification,
Trainer, TrainingArguments
)
from datasets import Dataset
import torch
# 1. 加载预训练模型(自动选择分词器和模型权重)
model_name = "bert-base-chinese" # 中文任务
# model_name = "Qwen/Qwen2.5-0.5B" # 也可用国产模型
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSequenceClassification.from_pretrained(
model_name, num_labels=2 # 二分类
)
# 2. 准备数据集
texts = ["这个产品太棒了!", "非常失望,质量很差", "还行,一般般", "强烈推荐!"]
labels = [1, 0, 0, 1]
def tokenize(examples):
return tokenizer(examples["text"], truncation=True, padding="max_length",
max_length=128)
dataset = Dataset.from_dict({"text": texts, "label": labels})
dataset = dataset.map(tokenize, batched=True)
train_ds, val_ds = dataset.train_test_split(test_size=0.2).values()
# 3. 训练配置
training_args = TrainingArguments(
output_dir="./results",
num_train_epochs=3,
per_device_train_batch_size=16,
per_device_eval_batch_size=32,
learning_rate=2e-5,
weight_decay=0.01,
warmup_ratio=0.1,
lr_scheduler_type="cosine",
evaluation_strategy="epoch",
save_strategy="epoch",
load_best_model_at_end=True,
fp16=torch.cuda.is_available(), # 自动开启混合精度
report_to="none",
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_ds,
eval_dataset=val_ds,
)
trainer.train()
trainer.save_model("./fine_tuned_model")LoRA:参数高效微调(大模型场景)
全量微调 70B 的模型需要 140GB+ 显存,大多数人负担不起。LoRA(Low-Rank Adaptation)的思路:冻结原始权重,只训练低秩分解的小矩阵,参数量只有原来的 0.1%–1%,效果接近全量微调。
原始权重矩阵 W(d×k,冻结)
额外训练两个小矩阵:A(d×r)和 B(r×k),r≪d
前向传播:output = W·x + BA·x
只训练 A 和 B,参数量 = r×(d+k),比 d×k 小几百倍from peft import get_peft_model, LoraConfig, TaskType
lora_config = LoraConfig(
task_type=TaskType.SEQ_CLS,
r=16, # 低秩维度(越大效果越好,参数越多)
lora_alpha=32, # 缩放因子,通常设为 r 的 2 倍
lora_dropout=0.1,
target_modules=["query", "value"], # 在哪些层加 LoRA(注意力的 Q/V 矩阵)
)
# 用 PEFT 包装原始模型
model = AutoModelForSequenceClassification.from_pretrained("bert-base-chinese", num_labels=2)
lora_model = get_peft_model(model, lora_config)
# 查看参数量
lora_model.print_trainable_parameters()
# trainable params: 294,912 || all params: 102,660,610 || trainable%: 0.29迁移学习策略选择
根据「数据量 × 任务与预训练任务的相似度」来选策略:
| 数据量 | 任务相似 | 推荐策略 |
|---|---|---|
| 少(< 1K 样本) | 高 | 只训练分类头,冻结全部底层 |
| 少(< 1K 样本) | 低 | 只训练分类头 + 最顶层 1-2 层 |
| 中等(1K–10K) | 高 | 两阶段微调(先头,再解冻) |
| 中等(1K–10K) | 低 | 全量微调,较大学习率 |
| 多(> 10K) | 任意 | 全量微调 或 LoRA(大模型) |
一句话小结
迁移学习 = 用预训练模型的特征作起点,只在自己的任务上微调。视觉用 torchvision 的 ResNet/EfficientNet;NLP 用 Hugging Face;大模型用 LoRA 省参数。从两阶段微调(先训头,再解冻)开始,是数据有限时最稳健的策略。
最后更新于