基础②:实用AI工具
⭐ 核心路径 — 本讲是课程中唯一的纯实操讲座。目标是让你能够独立使用 PyTorch 和 HuggingFace 搭建、训练和调试一个简单的深度学习模型。
学习目标
完成本讲后,你应该能够:
- 使用 PyTorch 的张量、自动求导、模块和优化器完成一个训练循环
- 调用 HuggingFace 加载预训练模型和数据集
- 应用 系统化的机器学习调试方法
- 遵循 Karpathy 的训练配方最佳实践
一、PyTorch 核心概念
1.1 张量(Tensor)
PyTorch 的张量是 NumPy 数组的 GPU 版本——支持自动微分、设备迁移和丰富的数学运算。
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 的维度区别对比:
| 操作 | NumPy | PyTorch |
|---|---|---|
| 创建 | np.array([1,2]) | torch.tensor([1,2]) |
| 设备 | 仅 CPU | CPU / CUDA / MPS |
| 求导 | 无 | .requires_grad_() |
| 广播 | 支持 | 支持 |
1.2 自动求导(Autograd)
PyTorch 通过 autograd 自动记录计算图并计算梯度。这是训练所有深度学习模型的基础。
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 中定义前向传播。
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.TransformerEncoder | Transformer 编码器 |
1.4 损失函数与优化器
# 常见损失函数
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 完整训练循环
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训练循环中的常见陷阱:
- 忘记
model.train()/model.eval()—— BatchNorm 和 Dropout 在两种模式下行为不同 - 忘记
optimizer.zero_grad()—— 梯度会累积 - 忘记
@torch.no_grad()—— 评测时仍跟踪计算图,浪费显存
二、HuggingFace 生态
HuggingFace 是当代 AI 开发的"GitHub + PyPI"——你可以在上面找到几乎所有主流模型、数据集和 tokenizer,并一行代码加载使用。
2.1 Transformers 库
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=True | 8-bit 量化,进一步减少显存 |
trust_remote_code=True | 加载非官方模型(如 Qwen、LLaMA 微调版) |
2.2 Datasets 库
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 库
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(在损失下降之前不要做任何事)
# 取一个单 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 / 预读取 |
第三步:可视化一切
# 可视化梯度分布(排查梯度消失/爆炸)
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 记录实验:
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时,按此顺序排查:
- [ ] 单batch过拟合 —— loss 能降到接近 0 吗?
- [ ] 数据加载正确 —— 读取的样本和标签是否正确对应?
- [ ] 预处理无误 —— 归一化、padding、tokenize 是否正确?
- [ ] 损失函数选择正确 —— 分类用 CrossEntropy,回归用 MSE?
- [ ] 学习率合理 —— 尝试 3e-4(Adam)、1e-3(SGD)、schedule
- [ ] 梯度正常 —— 是否有梯度爆炸/消失?
- [ ] 初始化合理 —— 确保最后一层输出分布合理
- [ ] 标签平衡 —— 分类任务检查类别分布
- [ ] 模型容量足够 —— 先大后小:先用大模型确保能学到,再精简
- [ ] 随机种子固定 —— 确保实验可复现
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 完整训练脚本框架
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 实验记录三件套
- 代码版本:每个实验保存在 git branch 或 commit 中
- 超参数记录:实验开始前用字典写明全部超参数(如上
config) - 结果日志:训练 loss、验证 loss、准确率、grad norm 全部记录
4.4 实用工具
| 工具 | 用途 | 何时用 |
|---|---|---|
tqdm | 进度条 | 每个训练循环的迭代 |
torchinfo.summary() | 模型结构+参数量 | 搭建模型后确认 |
torch.cuda.memory_summary() | GPU 显存分析 | OOM 时 |
torch.jit.script / torch.compile | 加速模型执行 | 调试完成后 |
| HuggingFace Hub | 保存/分享模型 | 模型训练完 |
关键概念
| 概念 | 定义 |
|---|---|
| 张量(Tensor) | 支持 GPU 计算和自动微分的多维数组 |
| Autograd | PyTorch 的自动求导引擎,记录计算图并计算梯度 |
| 单 batch 过拟合 | 在一条数据上过拟合到零损失的调试技巧 |
| Pipeline | HuggingFace 封装好的端到端推理接口 |
| Training Recipe | Karpathy 提出的系统化神经网络训练方法论 |
讨论问题
- 你在调试模型时最常见的 bug 是什么?用本讲的调试清单排查过吗?
- HuggingFace 的 pipeline 方式 vs. 手动控制 tokenizer + model——各自适用什么场景?
- 如果模型训练 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-课程概览/教学大纲|教学大纲]]
