第 1 课编程练习手册
主题
PyTorch 入门:从张量、自动求导到一个简单的 NLP 窗口分类器
对应材料
-
Notebook:
ai-model/1/1-prog/pytorch-tutorial/0-CS224N_PyTorch_Tutorial.ipynb
-
可参考 HTML:
ai-model/1/1-prog/pytorch-tutorial/0-CS224N_PyTorch_Tutorial.html
建议时长
- 课堂跟练:40 到 60 分钟
- 课后复现与整理:30 分钟
练习目标
这次编程练习的目标不是“把所有代码抄一遍”,而是建立三层理解:
- 看懂 PyTorch 最基本的数据对象和运算方式。
- 理解“前向计算 - 损失 - 反向传播 - 参数更新”这条训练主线。
-
用一个很小的 NLP
任务,把数据预处理、Embedding、DataLoader、模型定义和训练循环串起来。
一、整体结构
这个 notebook 可以分成四个部分:
- 张量基础:创建、形状、索引、reshape、与 NumPy 转换。
-
自动求导与神经网络模块:
autograd、nn.Linear、激活函数、nn.Sequential、自定义
nn.Module。
-
优化与训练循环:损失函数、
optimizer.zero_grad()、loss.backward()、optimizer.step()。
-
玩具 NLP 任务:构造语料、建立词表、Embedding、batch
处理、窗口分类模型、训练与预测。
建议不要跳着看。前半段是后半段能看懂的基础。
二、Part A:张量基础(约 10 到 15 分钟)
你会做什么
- 创建
torch.tensor
- 指定数据类型
dtype
- 使用
zeros、ones、arange
- 做基本运算和矩阵乘法
- 查看
.shape
- 用
view() / reshape() 改变形状
-
在
torch.Tensor 和 numpy.ndarray 之间转换
-
做按维度聚合,如
sum(dim=0)、mean(dim=1)
- 做切片和索引
本部分重点理解
-
PyTorch 的核心数据结构是 tensor
- 以后输入、标签、Embedding、参数、梯度,本质上都是 tensor。
-
shape 意识非常重要
- 很多初学者不是“不会训练模型”,而是根本没搞清张量维度。
- 看到一段代码时,要习惯问:这个 tensor 的 shape 是什么?
-
向量化计算是深度学习的基本工作方式
- 不是逐个样本、逐个元素手工循环,而是沿某个维度批量并行计算。
建议你重点试的代码
- 创建一个
2 x 3 的浮点 tensor
- 计算每一行均值、每一列均值
- 取第一行、第一列、一个子矩阵
- 把一维 tensor reshape 成二维矩阵,再确认元素顺序
最容易忽略的问题
float 和 long 的区别
view() 前后 shape 变了,但数据顺序没有变
- 切片得到的结果 shape 可能和你直觉不一样
三、Part B:Autograd 与神经网络模块(约 10 到 15 分钟)
你会做什么
- 创建
requires_grad=True 的 tensor
- 调用
backward() 自动计算梯度
- 观察
x.grad
- 认识梯度累积现象
- 使用
nn.Linear
- 使用
nn.Sigmoid()、nn.ReLU()
- 用
nn.Sequential() 快速搭模块
- 自定义一个
MultilayerPerceptron
本部分重点理解
-
为什么自动求导重要
- 你不需要手推并手写所有梯度,PyTorch 会根据计算图自动完成。
-
梯度会累积
-
notebook 里连续两次
backward() 后,x.grad
会继续累加。
- 这正是训练时每轮都要先
zero_grad() 的原因。
-
nn.Module 是模型组织的基本单位
-
线性层、激活层、Embedding 层、自定义模型,本质上都可以放进
nn.Module 的框架中。
-
forward 定义了“数据怎么流过模型”
- 初学者一定要把“定义层”和“写 forward”这两件事区分清楚。
建议你重点观察
nn.Linear(4, 2) 的输入输出 shape 是怎样变化的
list(model.named_parameters()) 能看到哪些参数
-
Sequential 版本和手写
forward 版本本质上有什么相同点
最容易忽略的问题
- 激活函数不是“可有可无”的装饰,它决定了非线性能力
- 一个模型不仅要能跑,还要能解释每层输入输出的 shape
四、Part C:优化与训练循环(约 10 分钟)
你会做什么
在 notebook 中,作者先构造了一个小玩具任务:
- 目标
y 是全 1 向量
- 输入
x 是在 y 上加入噪声得到的
- 模型要学会把 noisy input 映射回更干净的输出
然后依次完成:
- 定义模型
- 定义优化器
optim.Adam
- 定义损失函数
nn.BCELoss()
- 写训练循环
标准训练循环
你需要把下面这四步记熟:
optimizer.zero_grad()
y_pred = model(x)
loss = loss_function(y_pred, y)
loss.backward() 和 optimizer.step()
本部分重点理解
-
训练循环是深度学习代码的主骨架
- 以后不管是 MLP、CNN、Transformer,训练主线都离不开这几步。
-
zero_grad() 不是形式主义
- 不清零,梯度会在不同 iteration 之间累积,参数更新就会出错。
-
loss 在下降,说明模型正在学
- 但仅看训练 loss 还不够,后面你会看到测试数据也要一起看。
-
训练数据和测试数据不是一回事
-
notebook 里后面专门重新生成
x2 来看模型对新噪声样本的效果,这就是最基础的泛化检查。
建议你重点观察
- 每个 epoch 的 loss 是否稳定下降
- 模型在新生成的测试样本上是否仍接近目标输出
五、Part D:玩具 NLP 任务(约 20 到 30 分钟)
是整份 notebook 最有价值的地方,因为它把 PyTorch
基础和一个小型真实任务连起来了。
任务是什么
作者构造了一个简单的序列标注任务:
- 输入:句子
- 输出:句子中每个词是不是
LOCATION
例如:
-
Paris、Australia、Stanford、Taiwan、Turkey、Ankara
应被标为 1
- 其他词标为 0
这部分做了哪些事情
1. 数据预处理
重点理解:
-
模型不能直接吃原始字符串,任何 NLP 任务都必须先把文本变成结构化输入。
2. 建词表
- 遍历语料得到
vocabulary
- 增加
<unk> 表示未登录词
- 增加
<pad> 表示补齐位置
- 构建
word_to_ix 和 ix_to_word
重点理解:
- 词表是“文本世界”和“数字世界”的桥。
-
<unk> 是在承认现实:测试时一定会见到没见过的词。
<pad> 是为了让不同长度句子能组成 batch。
3. 词转索引,再转 embedding
- 先把 token 变成索引
- 再用
nn.Embedding 查表得到稠密向量
重点理解:
- Embedding 层本质上就是一个可学习的查找表。
-
“词不能直接进神经网络,索引也不能直接表达语义”,所以要再映射成
embedding。
4. 批处理与 DataLoader
- 自定义
collate_fn
- 对不同长度句子做 padding
- 返回 batched inputs、labels 和 lengths
重点理解:
-
DataLoader 不只是“加载数据”,还负责把数据组织成适合批量训练的形式。
-
collate_fn 是 NLP
里非常实用的一个点,因为文本长度经常不一致。
5. 构造窗口
- 用
unfold() 为句子中的每个词取一个局部窗口
- 例如窗口大小为 2 时,每个词会看到左 2 个词、自己、右 2 个词
重点理解:
-
这个模型不是 Transformer,也不是 RNN,而是经典的 window-based
classifier。
- 它的关键假设是:判断当前词是不是地点,附近上下文就已经很有帮助。
6. 定义模型 WordWindowClassifier
模型包括:
nn.Embedding
- 窗口内 embedding 拼接
- 一层隐藏层 +
Tanh
- 输出层
Sigmoid 概率
重点理解:
- 这已经是一个完整的神经网络 NLP 管线了。
- 虽然任务很小,但结构已经包含了现代模型代码中的关键部件。
7. 训练与预测
- 定义优化器
SGD
- 定义二元交叉熵损失
- 按 batch 训练多个 epoch
- 在测试句子
"She comes from Paris" 上做预测
重点理解:
- 训练代码和前面的 toy example 是同一条主线,只是数据更真实了。
-
这正是你需要建立的能力:从“会跑一个例子”过渡到“会组织一个完整任务”。
六、建议课堂练习步骤
步骤 1:先跑通 notebook
目标:
步骤 2:只看张量与 autograd 部分
-
自己解释几段关键代码:
- 为什么要
requires_grad=True
- 为什么梯度会累积
- 为什么
zero_grad() 必须存在
目标:
步骤 3:只看 toy MLP 训练部分
- 把训练循环抄写成你自己的最小模板。
-
尝试改
lr、epoch、隐藏层大小,观察 loss 变化。
目标:
步骤 4:只看 NLP 数据处理部分
-
检查一个句子如何一步步变成:
- token 序列
- index 序列
- padded batch
- window tensor
目标:
步骤 5:看懂 WordWindowClassifier
-
重点追踪
forward():
- 输入 shape 是什么
unfold() 后 shape 是什么
- embedding 后 shape 是什么
- flatten 后 shape 是什么
- 输出 shape 是什么
目标:
步骤 6:做一到两个小改动
建议学生任选一项:
- 把
embed_dim 从 25 改成 10 或 50,比较训练结果。
- 把
hidden_dim 调大或调小,比较 loss。
-
把优化器从
SGD 改成 Adam,观察收敛速度。
- 添加一个新的测试句子,看看模型是否还能识别地点词。
目标:
七、建议学生重点记录的问题
- 在这个 notebook 中,tensor、module、optimizer 各自扮演什么角色?
-
为什么
backward() 之后一定还需要 step()?
- 为什么文本任务要先建词表,再转索引,再做 embedding?
-
<unk> 和 <pad> 分别解决什么问题?
- 这个窗口分类器为什么能做地点词识别?它的局限又是什么?
- 如果句子特别长、依赖特别远,这种 window 方法还够用吗?
八、练习提交建议
可以要求学生提交下面几项内容:
- 一张 notebook 运行截图。
- 一段自己整理的最小训练循环代码。
- 一段对
WordWindowClassifier 的文字说明。
- 至少一个自己改动的实验结果。
- 3 条本次练习的关键收获。
九、本次练习的核心结论
-
PyTorch 的核心不是“会调库”,而是理解 tensor、shape、module 和 gradient。
-
深度学习训练代码虽然看起来很多,但骨架始终是前向、损失、反向、更新。
-
一个真实任务的难点,往往不只在模型,还在数据预处理、批处理和输入表示。
-
即使是一个很小的窗口分类器,也已经包含了现代 NLP
系统的关键思想:词表、Embedding、batch、模型、loss、optimizer、prediction。
-
真正重要的能力不是照着 notebook 跑通,而是能解释“每一步为什么这样写”。