AI课程学习:这次怎么落地的
第二次把 ResNet50 从零训到 80% 准确率时,我觉得可以出去吹牛。
刚开始学的时候,能达到 baseline 就已经很有成就感了。
为什么 AI 课程这么难学
最先卡住的其实是知识跳跃,不是某个模型本身有多难。典型路径大概是这样:
- 先学 Python 基础,以为能上手了
- 跑了几个
hello world级别的例子,信心满满 - 开始看论文里的公式,突然发现数学符号都不认识
- 以为实现完就结束了,结果调参调到怀疑人生
- 终于训出个模型,一到真实场景就翻车
# 第一个"我以为很简单"的坑:线性回归
import torch
import torch.nn as nn
# 看着很简单,实际问题多了
model = nn.Linear(10, 1) # 10个特征,1个输出
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
criterion = nn.MSELoss()
# 训练循环看起来更简单
for epoch in range(100):
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
这段代码不到十行,跑起来也不会报错。但问题来了:
- 学习率
lr=0.01怎么来的?为什么不是 0.1 或 0.001? - 损失函数为什么选 MSELoss?什么时候用 CrossEntropyLoss?
- 训练 100 个 epoch 够吗?怎么知道该停在哪?
这就是AI课程的第一个大坑:入门简单,深入难。能跑通代码和"理解为什么这么跑"之间差了一整个数学体系。
课程设计:从线性回归到 Transformer
后来自己做课程设计,才意识到难度得一点点往上抬。知识点堆成一锅粥,学员跑通 hello world 之后立刻撞论文公式,弃坑率很高。
这里有个简单的难度递进框架:
真正实践的时候,每一步都要配相应的数据集和评估指标。比如线性回归用波士顿房价数据集,逻辑回归用 MNIST 的二分类版本,卷积网络用 CIFAR-10 等等。
一个实用的课程数据集配置:
course_stages:
linear_regression:
dataset: boston_housing
features: 13
target: price
metric: rmse
baseline: 10.0
logistic_regression:
dataset: mnist_binary
classes: 2
metric: accuracy
baseline: 0.85
cnn:
dataset: cifar10
classes: 10
image_size: 32x32
metric: accuracy
baseline: 0.70
transformer:
dataset: wikitext2
vocab_size: 33000
metric: perplexity
baseline: 50.0
这里有个坑要注意:baseline 不能设得太高。刚开始学的时候,能达到 baseline 就已经很有成就感了。如果一上来就要求 SOTA 水平,很容易让人直接放弃。
训练策略:超参数和显存
模型选好了,数据准备好了,真正难的是怎么把它训好。这里涉及一堆超参数选择、优化器调优、学习率策略等等。
学习率不是拍脑袋选的
最开始的错误是用固定学习率,要么太小训不动,要么太大直接爆炸。
# 错误示范:固定学习率
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
# 正确做法:学习率调度
from torch.optim.lr_scheduler import CosineAnnealingLR, StepLR
# 方案一:余弦退火(适合长训练)
scheduler = CosineAnnealingLR(optimizer, T_max=100, eta_min=1e-6)
# 方案二:阶梯衰减(适合阶段性训练)
scheduler = StepLR(optimizer, step_size=30, gamma=0.1)
for epoch in range(100):
train_one_epoch(model, data_loader, criterion, optimizer)
scheduler.step()
print(f"Epoch {epoch}, LR: {optimizer.param_groups[0]['lr']}")
实际踩坑:训一个文本生成模型时,一开始用固定学习率 1e-3,前几个 epoch 损失下降很快,但第 10 个 epoch 后直接不降了。后来改成余弦退火,学习率慢慢衰减,损失才能继续下降。
优化器选择要考虑场景
不是所有情况都适合用 Adam,有时候 SGD 加动量反而更好。
# 通用场景:Adam
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-5)
# 计算机视觉任务:SGD + 动量
optimizer = torch.optim.SGD(model.parameters(), lr=0.1,
momentum=0.9,
weight_decay=5e-4)
# 长期训练:AdamW(改进的 Adam)
optimizer = torch.optim.AdamW(model.parameters(), lr=5e-5, weight_decay=0.01)
这里有个经验判断:
- 如果数据集较小、模型较大,用 Adam 之类自适应优化器
- 如果数据集很大、模型很大,用 SGD 加动量可能更稳定
- 做迁移学习或微调时,AdamW 的表现通常更好
批次大小和显存的权衡
显存永远不够用,这是常态。但盲目减小批次大小会影响训练稳定性。
# 常见错误:为了省显存把 batch size 调到 2
dataloader = DataLoader(dataset, batch_size=2, shuffle=True)
# 正确做法:梯度累积
batch_size = 32
accumulation_steps = 4 # 有效批次大小 = 32 * 4 = 128
for i, batch in enumerate(dataloader):
outputs = model(batch)
loss = criterion(outputs, labels) / accumulation_steps
loss.backward()
if (i + 1) % accumulation_steps == 0:
optimizer.step()
optimizer.zero_grad()
梯度累积的坑:忘记除以 accumulation_steps 会导致梯度过大,训练直接不稳定。第一次用的时候,模型损失从 2.0 飙升到 1000+,调了很久才发现是这个问题。
数据组织:比选模型更费时间
模型再好,数据不行也是白搭。数据组织比模型选型更考验基本功。
数据不平衡的几种处理方法
实际数据很少是平衡的,比如做医疗诊断时,阳性样本可能只有 1%。
# 方案一:重采样(简单粗暴)
from torch.utils.data import WeightedRandomSampler
# 计算每个样本的权重
class_counts = [1000, 100] # 负样本 1000,正样本 100
class_weights = 1.0 / torch.tensor(class_counts, dtype=torch.float)
sample_weights = class_weights[labels]
sampler = WeightedRandomSampler(sample_weights, len(sample_weights))
dataloader = DataLoader(dataset, batch_size=32, sampler=sampler)
# 方案二:损失函数加权(推荐)
criterion = nn.CrossEntropyLoss(weight=torch.tensor([1.0, 10.0]))
# 方案三:混合策略
# 训练时用重采样,验证时用原始分布
实际踩坑:做猫狗分类时,猫的图片有 10000 张,狗只有 1000 张。用重采样后,验证准确率上去了,但召回率很差——模型对狗的预测太保守。后来改成重采样 + 损失函数加权,才平衡了准确率和召回率。
数据增强要考虑任务特性
不是所有数据增强都适合所有任务。
from torchvision import transforms
# 计算机视觉通用增强
train_transform = transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(p=0.5),
transforms.ColorJitter(brightness=0.2, contrast=0.2),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
# OCR 任务:不能用颜色抖动、水平翻转
ocr_transform = transforms.Compose([
transforms.Resize((32, 128)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.5], std=[0.5])
])
# 医疗图像:要小心增强,不能改变病灶特征
medical_transform = transforms.Compose([
transforms.RandomRotation(10),
transforms.RandomAffine(degrees=0, translate=(0.05, 0.05)),
transforms.ToTensor()
])
经验教训:
- OCR 任务做数据增强时,不小心用了水平翻转,文字变成反的,模型学了一堆无效特征
- 医疗图像的增强要谨慎,不要引入不存在的"病灶"
数据划分要考虑时序性
时序数据不能用随机划分,否则会数据泄露。
# 错误做法:时序数据随机划分
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
# 正确做法:按时间划分
split_point = int(len(X) * 0.8)
X_train, X_test = X[:split_point], X[split_point:]
y_train, y_test = y[:split_point], y[split_point:]
# 或者用时间窗口划分
def time_split(data, window_size=7):
splits = []
for i in range(0, len(data) - window_size, window_size):
splits.append((data[i:i+window_size], data[i+window_size:i+window_size+1]))
return splits
做股票预测时第一次就用随机划分,回测效果特别好,实盘直接翻车。后来改成按时间划分,发现模型其实只是在"记忆"过去数据,不是真的学会了预测。
部署:离线好看,线上另一回事
模型训好了,部署又是一堆问题。离线评估和线上效果可能差十万八千里。
模型量化不是越省越好
为了节省资源,一开始就想把模型量化到 int8,结果准确率掉了 20%。
# 错误做法:直接量化
import torch.quantization as quant
model_int8 = quant.quantize_dynamic(
model, {nn.Linear}, dtype=torch.qint8
)
# 正确做法:量化感知训练
model.qconfig = quant.get_default_qat_qconfig('fbgemm')
model_prepared = quant.prepare_qat(model, inplace=True)
# 先做量化感知训练
for epoch in range(10):
train_one_epoch(model_prepared, ...)
# 再转换到 int8
model_int8 = quant.convert(model_prepared)
实际经验:大模型量化需要量化感知训练,小模型可以直接动态量化。如果是边缘设备部署,int8 通常够用;如果是云端服务,float16 性价比更高。
批量推理的内存陷阱
部署时为了提高吞吐量,尽量用批量推理,但内存很容易爆炸。
# 错误做法:一次性处理所有数据
all_predictions = model(all_inputs) # 内存不够用
# 正确做法:分批推理
def batch_inference(model, inputs, batch_size=32):
predictions = []
for i in range(0, len(inputs), batch_size):
batch = inputs[i:i+batch_size]
with torch.no_grad():
pred = model(batch)
predictions.append(pred.cpu()) # 及时释放 GPU 内存
return torch.cat(predictions)
# 或者用生成器减少内存占用
def batch_inference_generator(model, inputs, batch_size=32):
for i in range(0, len(inputs), batch_size):
batch = inputs[i:i+batch_size]
with torch.no_grad():
pred = model(batch)
yield pred.cpu()
第一次部署服务时,客户端一发 1000 个请求过来,服务直接 OOM。后来加上批量推理和请求队列,吞吐量反而比单请求处理还高。
模型监控比训练更重要
模型上线后表现会退化,需要持续监控。
# 简单的模型监控框架
class ModelMonitor:
def __init__(self, model, threshold=0.1):
self.model = model
self.threshold = threshold
self.baseline_loss = None
def check_drift(self, inputs, labels):
with torch.no_grad():
outputs = self.model(inputs)
loss = nn.functional.cross_entropy(outputs, labels)
if self.baseline_loss is None:
self.baseline_loss = loss
return True
if abs(loss - self.baseline_loss) > self.threshold:
print(f"Warning: Loss drifted from {self.baseline_loss:.4f} to {loss:.4f}")
return False
return True
# 定期检查
monitor = ModelMonitor(model)
for batch in test_dataloader:
if not monitor.check_drift(batch['input'], batch['label']):
# 触发重训练或告警
trigger_retraining()
实际案例:部署了一个情感分析模型,第一个月表现很好,第二个月准确率突然下降 15%。查了半天发现是因为数据分布变了——用户开始用新的网络用语,模型没见过。后来加了个监控机制,一旦检测到分布漂移就自动触发重训练。
回头看几个节点
入门阶段能跑通代码、知道 loss 在降,离"理解为什么这么跑"还差一截。这个阶段别急着上 Transformer,把线性回归、逻辑回归里的每个超参数问清楚,后面调参少踩很多坑。
能调模型、能处理不平衡数据和增强策略,算是进了一档。我自己的体会是:数据特性比模型架构更决定上限。股票预测用随机划分回测虚高、猫狗不平衡只看重采样不管召回,都是这个阶段会撞的墙。
再往后是系统问题——量化掉点、并发 OOM、分布漂移。模型只是链路里的一环,监控和回滚和训练一样重要。
框架会变,PyTorch 2.x、新 optimizer、新调度器层出不穷。但"数据怎么划分"“指标和体验是否脱节"“上线后怎么发现退化"这些问题,换多少框架都会碰到。学 AI 难,难在每一档都要把上一档的坑真正踩透,而不是急着挂下一个 badge。
版权声明: 本文首发于 指尖魔法屋-AI课程学习:这次怎么落地的(https://blog.thinkmoon.cn/post/228-ai-course-learning-curve-training-strategy/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。