模块 6 - 通往深度学习 · 第 24 课
阅读 D2L 代码与准备度检查
用一组可验证的能力测试判断自己是否可以停止 Python 前置学习并开始 D2L。
这不是“读完即通过”
READY 需要你能读、能预测、能运行、能解释。每题先写答案,再运行代码。建议把结果记录在一个独立的 readiness_check.py 或 notebook 中。
能力测试
1. Python code reading
解释下面代码的输出顺序,并指出 records 的类型:
records = [{"name": "a", "score": 0.8}, {"name": "b", "score": 0.9}]
best = max(records, key=lambda record: record["score"])
print(best["name"], best["score"])
2. 容器、函数与类
完成以下检查:
def summarize(values):
return min(values), max(values)
low, high = summarize([0.2, 0.7, 0.4])
assert (low, high) == (0.2, 0.7)
class Meter:
def __init__(self):
self.total = 0
def add(self, value):
self.total += value
meter = Meter()
meter.add(3)
assert meter.total == 3
你应该能解释列表/字典、tuple unpacking、函数参数/返回值、instance、attribute、method、self 和 __init__。
3. NumPy shape、axis 与广播
预测后运行:
import numpy as np
x = np.arange(24).reshape(4, 3, 2)
assert x[:, 1, :].shape == (4, 2)
assert x.mean(axis=2).shape == (4, 3)
assert (x + np.ones((1, 3, 2))).shape == (4, 3, 2)
说明为什么 (4, 3, 2) 与 (1, 3, 2) 兼容,以及为什么 x.mean(axis=2) 会移除最后一个轴。
4. Tensor indexing、reshape 与乘法
import torch
a = torch.arange(12).reshape(3, 4)
assert a[:, 1:3].shape == (3, 2)
assert a.reshape(2, 6).shape == (2, 6)
left = torch.ones((2, 3))
right = torch.ones((3, 4))
assert (left @ right).shape == (2, 4)
另外解释 left * right 为什么不是这里的矩阵乘法:逐元素乘法要求可广播的 shape,而 @ 遵循内维相等的矩阵规则。
5. Autograd 与 batch shape
import torch
from torch.utils.data import DataLoader, Dataset
w = torch.tensor(0.5, requires_grad=True)
features = torch.tensor([2.0, 4.0])
targets = torch.tensor([1.0, 3.0])
loss = ((w * features - targets) ** 2).mean()
loss.backward()
assert w.grad is not None
assert w.grad.item() != 0
class Tiny(Dataset):
def __init__(self):
self.x = torch.arange(6, dtype=torch.float32).reshape(6, 1)
self.y = 2 * self.x.squeeze(1)
def __len__(self):
return len(self.x)
def __getitem__(self, index):
return self.x[index], self.y[index]
loader = DataLoader(Tiny(), batch_size=2, shuffle=False)
batch_x, batch_y = next(iter(loader))
assert batch_x.shape == (2, 1)
assert batch_y.shape == (2,)
你应该能说出 requires_grad、计算图、标量 loss、backward()、.grad、Dataset 的 __len__/__getitem__ 和 DataLoader 的 batch_size 分别做什么。
D2L 风格代码阅读(D2L-style code reading)
def train_epoch(model, data_iter, learning_rate):
losses = []
for X, y in data_iter:
pred = model(X)
loss = ((pred - y) ** 2).mean()
loss.backward()
losses.append(loss.item())
return sum(losses) / len(losses)
回答四个问题:X 和 y 从哪里来?为什么 losses 是列表而不是 Tensor?为什么 loss.backward() 放在循环中?如果 data_iter 没有 batch,哪些 shape 信息会改变?能用前面课程的知识回答这些问题,才算完成代码阅读部分。
Before D2L Mini Challenge
下面是一段最小但完整的训练流。先不运行,逐项回答问题,再复制运行:
import torch
from torch import nn
from torch.utils.data import DataLoader, Dataset
class LineDataset(Dataset):
def __init__(self):
self.X = torch.tensor([[1.0], [2.0], [3.0], [4.0]])
self.y = torch.tensor([[2.0], [4.0], [6.0], [8.0]])
def __len__(self):
return len(self.X)
def __getitem__(self, index):
return self.X[index], self.y[index]
class LinearModel(nn.Module):
def __init__(self):
super().__init__()
self.linear = nn.Linear(1, 1)
def forward(self, X):
return self.linear(X)
data_loader = DataLoader(LineDataset(), batch_size=2, shuffle=False)
model = LinearModel()
loss_fn = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.05)
for X, y in data_loader:
prediction = model(X)
loss = loss_fn(prediction, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
break
请回答:
- Dataset 的一个 sample 是什么?
- DataLoader 的一个 batch 是什么?
X的 shape 是什么?y的 shape 是什么?model是什么对象?model(X)为什么可以执行?- 实际调用的是哪个
forward()? prediction的 shape 是什么?- 为什么
loss最好是 scalar? backward()产生什么?- gradients 保存在哪里?
zero_grad()为什么存在?optimizer.step()做什么?- 下一轮 batch 又会发生什么?
评分与 gate
总共 14 个检查点:Python 代码阅读 2 个、容器/函数/类 2 个、NumPy 3 个、Tensor 3 个、Autograd 2 个、Dataset/DataLoader 2 个。每个能运行且能解释的检查点得 1 分。
- BEGINNER(0–8):继续学习本课程中得分最低的模块,并重新运行对应练习。
- READY(9–11,且第 4、5 组各至少 2 分):可以停止 Python prerequisite 学习,正式开始 Dive into Deep Learning;遇到细节再查阅。
- 另外,Before D2L Mini Challenge 至少答对 12/14 题;否则先回到 Lesson 17 或 23 的训练流练习。
- STRONG(12–14):可以开始 D2L,并能独立调试一个小型 notebook 或 batch 迭代。
READY 不是永久证书,而是一个行动门槛:你已经具备阅读 D2L 基础代码的最低能力,不需要把 Python 学成百科全书。
逐 checkpoint 参考答案
- 输出是
b 0.9;max(..., key=...)比较每条字典的score,所以最后留下分数更高的记录。 records的类型是list[dict];列表保留顺序,字典用name和score键保存字段。summarize返回(0.2, 0.7);两个名称按顺序解包 tuple。meter是Meterinstance,total是它的 attribute;add(3)通过 method 把状态改为 3。x[:, 1, :]的 shape 是(4, 2);索引掉中间轴,保留 batch 和最后一轴。x.mean(axis=2)的 shape 是(4, 3);聚合最后一轴后该轴被移除。(4, 3, 2)与(1, 3, 2)从末轴比较时分别为2=2、3=3、4与1,所以广播结果为(4, 3, 2)。a[:, 1:3]的 shape 是(3, 2);a.reshape(2, 6)的 shape 是(2, 6),元素总数仍为 12。left @ right的 shape 是(2, 4),因为内维3相等;left * right是逐元素运算,要求可广播 shape,不能替代矩阵乘法。requires_grad=True让运算记录计算图;backward()从 scalar loss 反向计算每个参数的导数。.grad保存当前梯度;这个例子中w.grad非空且非零,因为预测误差会随w改变。- Dataset 的
__len__返回样本数,__getitem__返回一个(x, y)sample;DataLoader 通过它们组织索引。 batch_size=2每次取两条 sample;第一批batch_x.shape == (2, 1)、batch_y.shape == (2,)。train_epoch每轮从data_iter取一个 batch,计算 prediction 和 loss,收集loss.item()这个 Python 标量,最后返回平均值;没有 batch 时,X/y会失去首个 batch 维度,代码中的 shape 契约也会改变。
Mini Challenge 参考答案
- 一个 sample 是
LineDataset.__getitem__返回的(self.X[index], self.y[index]),各自 shape 为(1,)。 - 一个 batch 是 DataLoader 把两个 sample 沿首轴堆叠后的
(X, y),不是单个 sample。 X.shape == (2, 1):batch 中有 2 条样本,每条 1 个特征。y.shape == (2, 1):每条样本有 1 个回归目标,和 prediction 对齐。model是LinearModelinstance,也是nn.Moduleinstance。nn.Module对象是 callable;model(X)进入 Module call path,而不是普通属性读取。- 调用的是这个实例的
LinearModel.forward(X),其中再调用self.linear(X)。 prediction.shape == (2, 1),因为线性层把 1 个输入特征映射成 1 个输出。- scalar loss 只有一个数,
backward()才能直接从它沿计算图聚合出参数梯度。 backward()计算 loss 对 model parameters 的梯度。- 梯度保存在每个 registered parameter 的
.grad属性中,例如model.linear.weight.grad。 - 梯度默认会累积;
zero_grad()在当前 batch 反向传播前清掉上一轮残留值。 optimizer.step()读取.grad,按学习率更新 optimizer 管理的 parameters。- 下一批再次经过 DataLoader →
model(X)→ loss →zero_grad()→backward()→step();参数已经是上一批更新后的值。
把你的得分、一个失败的断言和修复过程保存下来。如果达到 READY,现在就可以打开 D2L 的第一章;如果没有达到,回到对应 lesson 完成 ★★★ 综合练习后再测一次。