AI课程学习:这次怎么落地的

第二次把 ResNet50 从零训到 80% 准确率时,我觉得可以出去吹牛。

刚开始学的时候,能达到 baseline 就已经很有成就感了。

为什么 AI 课程这么难学

最先卡住的其实是知识跳跃,不是某个模型本身有多难。典型路径大概是这样:

  1. 先学 Python 基础,以为能上手了
  2. 跑了几个 hello world 级别的例子,信心满满
  3. 开始看论文里的公式,突然发现数学符号都不认识
  4. 以为实现完就结束了,结果调参调到怀疑人生
  5. 终于训出个模型,一到真实场景就翻车
# 第一个"我以为很简单"的坑:线性回归
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 之后立刻撞论文公式,弃坑率很高。

这里有个简单的难度递进框架:

graph LR A[线性回归<br/>单变量预测] --> B[逻辑回归<br/>二分类] B --> C[多层感知机<br/>特征学习] C --> D[卷积网络<br/>图像处理] D --> E[循环网络<br/>序列建模] E --> F[Transformer<br/>注意力机制] F --> G[大模型微调<br/>实际部署] style A fill:#e8f5e9 style B fill:#c8e6c9 style C fill:#a5d6a7 style D fill:#81c784 style E fill:#66bb6a style F fill:#4caf50 style G fill:#43a047

真正实践的时候,每一步都要配相应的数据集和评估指标。比如线性回归用波士顿房价数据集,逻辑回归用 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/) 转载或引用必须申明原指尖魔法屋来源及源地址!