Skip to content

基础②:实用AI工具

核心路径 — 本讲是课程中唯一的纯实操讲座。目标是让你能够独立使用 PyTorch 和 HuggingFace 搭建、训练和调试一个简单的深度学习模型

学习目标

完成本讲后,你应该能够:

  1. 使用 PyTorch 的张量、自动求导、模块和优化器完成一个训练循环
  2. 调用 HuggingFace 加载预训练模型和数据集
  3. 应用 系统化的机器学习调试方法
  4. 遵循 Karpathy 的训练配方最佳实践

一、PyTorch 核心概念

1.1 张量(Tensor)

PyTorch 的张量是 NumPy 数组的 GPU 版本——支持自动微分、设备迁移和丰富的数学运算。

python
import torch

# 从列表创建张量
x = torch.tensor([[1, 2], [3, 4]], dtype=torch.float32)

# 特殊张量
zeros = torch.zeros(3, 4)
randn = torch.randn(3, 4)       # 标准正态分布
arange = torch.arange(10)       # 0..9

# 设备迁移
device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
x = x.to(device)                # 放到 GPU/MPS/CPU

# 形状操作
x.view(4, -1)                   # 重塑(-1 自动推断)
x.unsqueeze(0)                  # 加 batch 维度
x.squeeze()                     # 去掉长度为 1 的维度

区别对比:

操作NumPyPyTorch
创建np.array([1,2])torch.tensor([1,2])
设备仅 CPUCPU / CUDA / MPS
求导.requires_grad_()
广播支持支持

1.2 自动求导(Autograd)

PyTorch 通过 autograd 自动记录计算图并计算梯度。这是训练所有深度学习模型的基础。

python
x = torch.tensor([2.0], requires_grad=True)
y = x ** 2 + 3 * x + 1
y.backward()                     # 自动计算梯度
print(x.grad)                    # dy/dx = 2x + 3 = 7.0

关键理解:

  • requires_grad=True 标记需要梯度的张量
  • backward() 从标量反向传播
  • grad 累积——每次 backward() 前需手动清零(optimizer.zero_grad()
  • .detach() 从计算图中分离张量(推理或可视化时常用)
  • with torch.no_grad(): 上下文管理器内不追踪梯度(评测时务必使用)

1.3 模块(nn.Module)

所有神经网络层的基类。核心在于 __init__ 中定义层,forward 中定义前向传播。

python
import torch.nn as nn

class TwoLayerNet(nn.Module):
    def __init__(self, in_dim, hidden_dim, out_dim):
        super().__init__()
        self.fc1 = nn.Linear(in_dim, hidden_dim)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(hidden_dim, out_dim)

    def forward(self, x):
        return self.fc2(self.relu(self.fc1(x)))

model = TwoLayerNet(784, 256, 10).to(device)
print(model)

常用层速查:

用途
nn.Linear(in, out)全连接层
nn.Conv2d(C_in, C_out, K)2D 卷积
nn.BatchNorm1d/2d批归一化
nn.Dropout(p=0.5)Dropout 正则化
nn.Embedding(V, D)词嵌入
nn.TransformerEncoderTransformer 编码器

1.4 损失函数与优化器

python
# 常见损失函数
criterion = nn.CrossEntropyLoss()     # 分类
criterion = nn.MSELoss()              # 回归
criterion = nn.BCEWithLogitsLoss()    # 二分类

# 常见优化器
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
optimizer = torch.optim.Adam(model.parameters(), lr=3e-4)
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=0.01)

优化器选择经验:

  • Adam:默认选择,学习率 3e-4 是多数 CV/NLP 任务的良好起点
  • AdamW:Adam + 解耦权重衰减,Transformer 系模型的标配
  • SGD + Momentum:CV 分类任务仍然流行,调好学习率策略后可达到更好泛化

1.5 完整训练循环

python
def train_one_epoch(model, dataloader, criterion, optimizer, device):
    model.train()
    total_loss = 0

    for batch_idx, (inputs, labels) in enumerate(dataloader):
        inputs, labels = inputs.to(device), labels.to(device)

        outputs = model(inputs)
        loss = criterion(outputs, labels)

        optimizer.zero_grad()
        loss.backward()

        # 可选:梯度裁剪——防止梯度爆炸
        nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

        optimizer.step()
        total_loss += loss.item()

    return total_loss / len(dataloader)

@torch.no_grad()
def evaluate(model, dataloader, criterion, device):
    model.eval()
    total_loss, correct = 0, 0

    for inputs, labels in dataloader:
        inputs, labels = inputs.to(device), labels.to(device)
        outputs = model(inputs)
        loss = criterion(outputs, labels)
        total_loss += loss.item()
        correct += (outputs.argmax(1) == labels).sum().item()

    avg_loss = total_loss / len(dataloader)
    accuracy = correct / len(dataloader.dataset)
    return avg_loss, accuracy

训练循环中的常见陷阱:

  1. 忘记 model.train() / model.eval() —— BatchNorm 和 Dropout 在两种模式下行为不同
  2. 忘记 optimizer.zero_grad() —— 梯度会累积
  3. 忘记 @torch.no_grad() —— 评测时仍跟踪计算图,浪费显存

二、HuggingFace 生态

HuggingFace 是当代 AI 开发的"GitHub + PyPI"——你可以在上面找到几乎所有主流模型、数据集和 tokenizer,并一行代码加载使用。

2.1 Transformers 库

python
from transformers import (AutoTokenizer, AutoModelForSequenceClassification,
                          pipeline)

# 一行加载模型和 tokenizer
model_name = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSequenceClassification.from_pretrained(model_name, num_labels=2)

# 使用 pipeline 简化推理
classifier = pipeline("sentiment-analysis", model=model, tokenizer=tokenizer)
result = classifier("I love learning AI tools!")
print(result)  # [{'label': 'POSITIVE', 'score': 0.999}]

# 手动 tokenize
inputs = tokenizer(
    ["Hello world", "How to AI everything"],
    padding=True,
    truncation=True,
    max_length=128,
    return_tensors="pt"
)
outputs = model(**inputs)

from_pretrained 的实用参数:

参数用途
torch_dtype=torch.float16半精度加载,显存减半
device_map="auto"自动分配到可用设备
load_in_8bit=True8-bit 量化,进一步减少显存
trust_remote_code=True加载非官方模型(如 Qwen、LLaMA 微调版)

2.2 Datasets 库

python
from datasets import load_dataset

# 加载数据集——默认自动缓存到 ~/.cache/huggingface/datasets
dataset = load_dataset("imdb", split="train[:5%]")
print(dataset[0])       # {'text': '...', 'label': 0}

# 数据集操作
dataset = dataset.train_test_split(test_size=0.2)
train_data = dataset["train"]
eval_data = dataset["test"]

# 使用 map 进行预处理
def preprocess(example):
    return tokenizer(example["text"], truncation=True, padding="max_length", max_length=256)

train_data = train_data.map(preprocess, batched=True)

# 格式化为 PyTorch
train_data.set_format(type="torch", columns=["input_ids", "attention_mask", "label"])
train_loader = torch.utils.data.DataLoader(train_data, batch_size=16, shuffle=True)

数据处理提示:

  • datasets 背后的数据是内存映射(memory-mapped)的——可以加载远超内存大小的数据集
  • 多进程加速:map(..., num_proc=4)
  • 需要移除不需要的列:.remove_columns([...])

2.3 Tokenizers 库

python
from tokenizers import Tokenizer
from tokenizers.models import BPE
from tokenizers.trainers import BpeTrainer

# 从零训练一个 BPE tokenizer
tokenizer = Tokenizer(BPE(unk_token="[UNK]"))
trainer = BpeTrainer(special_tokens=["[UNK]", "[CLS]", "[SEP]", "[PAD]", "[MASK]"])
tokenizer.train(files=["corpus.txt"], trainer=trainer)
tokenizer.save("my-tokenizer.json")

原则: 99% 情况下直接用 AutoTokenizer.from_pretrained() 即可。只有当你训练自己的模型时才需要自定义 tokenizer。

三、机器学习调试方法论

3.1 Karpathy 训练配方

Andrej Karpathy 的经典文章 A Recipe for Training Neural Networks 提出了系统化的训练方法论,核心步骤:

第一步:先过拟合一个 batch(在损失下降之前不要做任何事)

python
# 取一个单 batch 的数据
batch_data, batch_labels = next(iter(train_loader))
batch_data, batch_labels = batch_data.to(device), batch_labels.to(device)

# 在这个单 batch 上反复训练
for step in range(1000):
    outputs = model(batch_data)
    loss = criterion(outputs, batch_labels)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    if step % 100 == 0:
        print(f"Step {step}, Loss: {loss.item():.4f}")

目标:loss 应降至接近 0(过拟合成功 = 前向传播和后向传播没有问题)。

第二步:常见问题诊断

现象可能原因检查方法
loss 不下降学习率太大/太小尝试 lr=3e-4(Adam)或 1e-3(SGD)
loss 为 NaN梯度爆炸加梯度裁剪、检查是否有除零
loss 震荡剧烈batch size 太小 / 学习率太大增大 batch size 或降低 lr
验证 loss 不降过拟合 / 数据泄露检查数据切分、加正则化
训练慢数据加载瓶颈增加 num_workers,检查 IO
GPU 利用率低batch size 太小 / 数据加载瓶颈增大 batch size / 预读取

第三步:可视化一切

python
# 可视化梯度分布(排查梯度消失/爆炸)
total_norm = 0.0
for p in model.parameters():
    if p.grad is not None:
        param_norm = p.grad.data.norm(2)
        total_norm += param_norm.item() ** 2
total_norm = total_norm ** 0.5
print(f"Gradient norm: {total_norm:.4f}")

推荐使用 Weights & Biases (wandb)TensorBoard 记录实验:

python
from torch.utils.tensorboard import SummaryWriter

writer = SummaryWriter("runs/experiment_01")
for epoch in range(num_epochs):
    train_loss = train_one_epoch(...)
    eval_loss, eval_acc = evaluate(...)
    writer.add_scalar("Loss/train", train_loss, epoch)
    writer.add_scalar("Loss/eval", eval_loss, epoch)
    writer.add_scalar("Accuracy/eval", eval_acc, epoch)

3.2 调试检查清单

当模型不work时,按此顺序排查:

  1. [ ] 单batch过拟合 —— loss 能降到接近 0 吗?
  2. [ ] 数据加载正确 —— 读取的样本和标签是否正确对应?
  3. [ ] 预处理无误 —— 归一化、padding、tokenize 是否正确?
  4. [ ] 损失函数选择正确 —— 分类用 CrossEntropy,回归用 MSE?
  5. [ ] 学习率合理 —— 尝试 3e-4(Adam)、1e-3(SGD)、schedule
  6. [ ] 梯度正常 —— 是否有梯度爆炸/消失?
  7. [ ] 初始化合理 —— 确保最后一层输出分布合理
  8. [ ] 标签平衡 —— 分类任务检查类别分布
  9. [ ] 模型容量足够 —— 先大后小:先用大模型确保能学到,再精简
  10. [ ] 随机种子固定 —— 确保实验可复现
python
def set_seed(seed: int = 42):
    import random, numpy as np
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
    torch.backends.cudnn.deterministic = True
    torch.backends.cudnn.benchmark = False

set_seed(42)

四、训练配方最佳实践

4.1 完整训练脚本框架

python
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from datasets import load_dataset

# 1. 配置
config = {
    "model_name": "distilbert-base-uncased",
    "batch_size": 16,
    "lr": 2e-5,
    "num_epochs": 3,
    "device": "cuda" if torch.cuda.is_available() else "cpu",
    "max_length": 128,
}
set_seed(42)

# 2. 数据
dataset = load_dataset("imdb", split="train[:1000]").train_test_split(test_size=0.2)
tokenizer = AutoTokenizer.from_pretrained(config["model_name"])

def tokenize(batch):
    return tokenizer(batch["text"], truncation=True, padding="max_length",
                     max_length=config["max_length"])

dataset = dataset.map(tokenize, batched=True)
dataset.set_format("torch", columns=["input_ids", "attention_mask", "label"])

train_loader = DataLoader(dataset["train"], batch_size=config["batch_size"], shuffle=True)
eval_loader = DataLoader(dataset["test"], batch_size=config["batch_size"])

# 3. 模型与优化器
model = AutoModelForSequenceClassification.from_pretrained(
    config["model_name"], num_labels=2
).to(config["device"])
optimizer = torch.optim.AdamW(model.parameters(), lr=config["lr"])

# 4. 训练循环
for epoch in range(config["num_epochs"]):
    train_loss = train_one_epoch(model, train_loader, nn.CrossEntropyLoss(), optimizer, config["device"])
    eval_loss, eval_acc = evaluate(model, eval_loader, nn.CrossEntropyLoss(), config["device"])
    print(f"Epoch {epoch+1}: train_loss={train_loss:.4f}, eval_loss={eval_loss:.4f}, eval_acc={eval_acc:.4f}")

4.2 "先简单后复杂"原则

Karpathy 原话: "The most common mistake I see is people trying to get a neural network to work on a complex task without first verifying that it works on a simpler version."

步骤简化方案验证点
数据用玩具数据集(10-100 样本)过拟合成功
模型用 2 层 MLP 或小 Transformer梯度流正常
任务用二分类替代多分类loss 曲线可预期
优化用 Adam 固定 lr,去掉 schedule收敛稳定

4.3 实验记录三件套

  1. 代码版本:每个实验保存在 git branch 或 commit 中
  2. 超参数记录:实验开始前用字典写明全部超参数(如上 config
  3. 结果日志:训练 loss、验证 loss、准确率、grad norm 全部记录

4.4 实用工具

工具用途何时用
tqdm进度条每个训练循环的迭代
torchinfo.summary()模型结构+参数量搭建模型后确认
torch.cuda.memory_summary()GPU 显存分析OOM 时
torch.jit.script / torch.compile加速模型执行调试完成后
HuggingFace Hub保存/分享模型模型训练完

关键概念

概念定义
张量(Tensor)支持 GPU 计算和自动微分的多维数组
AutogradPyTorch 的自动求导引擎,记录计算图并计算梯度
单 batch 过拟合在一条数据上过拟合到零损失的调试技巧
PipelineHuggingFace 封装好的端到端推理接口
Training RecipeKarpathy 提出的系统化神经网络训练方法论

讨论问题

  1. 你在调试模型时最常见的 bug 是什么?用本讲的调试清单排查过吗?
  2. HuggingFace 的 pipeline 方式 vs. 手动控制 tokenizer + model——各自适用什么场景?
  3. 如果模型训练 loss 正常下降但验证 loss 不降,你会从哪些方面排查?

延伸阅读

  • 必读A Recipe for Training Neural Networks — Andrej Karpathy 的深度学习调试方法论
  • 推荐PyTorch 官方教程 — 60 Minutes Blitz 是最好起点
  • 推荐HuggingFace 课程 — 从零到部署的 NLP 教程
  • 参考:[[02-基础/02-01-数据与结构|数据、结构与信息]] — 第 2 周另一讲座
  • 参考:[[01-AI导论/01-02-AI研究方法|AI研究方法]] — 第 4 节包含 Karpathy 配方初稿

相关笔记

  • [[02-基础/02-01-数据与结构|数据、结构与信息]]
  • [[02-基础/02-03-模型架构|模型架构]]
  • [[02-基础/02-04-本周阅读|第2/4周阅读]]
  • [[01-AI导论/01-02-AI研究方法|AI研究方法]]
  • [[00-课程概览/教学大纲|教学大纲]]

基于 MIT MAS.S60 How to AI (Almost) Anything 翻译改编