Current: Week 5

0%

Week 5

Week 5 - Tensor 与PyTorch:把手算交给计算机

Key question怎样让计算机自动执行大规模 forward/backward?

Learning objectives

  • 理解并应用 Week 5 - Tensor 与 PyTorch:把手算交给计算机

45 min estimated reading time

Week progress: 0 of 19 sections (0%)Course progress: 0 of 321 sections (0%)

本周不换数据、不换损失、不换学习率,只把已经会的计算交给 PyTorch。共同基线是 Week 2 四样本:x=[1,2,3,4]、y=[3,5,7,9]、w=b=0、mean MSE、learning_rate=0.01。

先记住可核对的一步:Loss=41,dw=−35、db=−12;更新后 w=0.35、b=0.12,再 forward 的 Loss=28.45315。框架如果给出别的数,先检查初值、shape、平均方式与数据,而不是认为自动求导使用另一种数学。

Scroll horizontally to view all columns.

Course data table
分次安排把什么交给 PyTorch
一:Tensor 与接口明确各轴、dtype、device;区分逐元素乘法与矩阵乘法。
二:Autograd对照手算,看 requires_grad、graph、.grad 各保存什么。
三:完整一步清梯度、forward、backward、step;用状态表核对。
四:Module 与诊断运行 course_examples/week05_three_ways.py,制造并解释一次错误广播。

CPU 足够。安装方式见下载包 README。完成后你应能指出哪一行改变参数,并解释为什么 eval() 和 no_grad() 不是同一个开关。下一周输入变成 token ID,但这套训练步骤不变。

Week 5 核心目标

Tensor → 保存和批量计算数字

Autograd → 自动执行Chain Rule

nn.Module → 组织Parameters 和Forward

Optimizer → 更新Parameters

本周重点只有四个:Tensor、Shape、Autograd、Training Loop。

1. Tensor 到底是什么

Tensor 可以先理解为:

带有shape、dtype、device 等信息的多维数字容器。

这个定义里的每一部分都有实际作用:

  • 数字内容告诉operation 要计算什么;
  • shape 告诉operation 数据怎样组织;
  • dtype 决定每个数字怎样存储和可以参加哪些运算;
  • device 决定运算在CPU、GPU 或其他加速器上执行;
  • autograd metadata 记录它是否需要gradient 以及怎样回到前面的operations。
python
import torch

scalar = torch.tensor(3.0)
vector = torch.tensor([1.0, 2.0, 3.0])
matrix = torch.tensor([
    [1.0, 2.0, 3.0],
    [4.0, 5.0, 6.0],
])

对应shape:

scalar: []

vector: [3]

matrix: [2, 3]

Tensor 不是一种新数学。它把Week 1 的scalar、vector 和matrix 放进一个统一接口。

为什么Python List 不够

Python list 可以保存数字,却不是为大规模数值计算设计的。对百万个元素逐个执行Python loop,会产生大量解释器开销;list 也没有统一的shape、device 和automatic differentiation 语义。

Tensor 把同类数字排列成规则内存,并把整个operation 交给经过优化的底层kernel:

Python 发出一次matrix multiplication 请求

底层并行计算大量乘加操作

Autograd 同时记录这次依赖关系

因此Tensor 是数学、硬件加速和Backpropagation 的共同数据载体。

2. Dimension 与Shape

假设一个batch 有四套房子,每套有三个features:

python
x = torch.tensor([
    [0.8, 0.6, 0.2],
    [0.5, 0.4, 0.9],
    [0.9, 0.7, 0.1],
    [0.6, 0.8, 0.4],
])
print(x.shape)  # torch.Size([4, 3])

dimension 0 = batch

dimension 1 = features

后面处理文本时常见:

[B, T, C]

  • B:Batch size;
  • T:Sequence length;
  • C:Channel / Embedding dimension。

读PyTorch 代码时,先追shape,再追公式。

Axis 的语义不能靠猜

[B,T,C] 里的三个字母不是PyTorch 强制规定的,只是项目中的约定。同一个[4,8,32] tensor,如果某段代码认为axis 1 是token,而另一段认为axis 1 是channel,运算可能照样运行却产生错误含义。

建议在复杂代码旁写shape trace:

python
# x: [batch, sequence, embedding]
x = token_embedding(token_ids)

它相当于给动态tensor 增加可阅读的type annotation。

3. Dtype 与Device

Dtype

python
x = torch.tensor([1.0, 2.0], dtype=torch.float32)
token_ids = torch.tensor([3, 8, 2], dtype=torch.long)

神经网络计算通常使用浮点数;Embedding lookup 的token IDs 通常使用整数long。

为什么token ID 要是整数?因为它用于选择Embedding Matrix 的某一行,本质上是index。为什么weights 通常是浮点数?因为训练需要连续地做小幅parameter update,整数无法表达这些细微变化。

Device

python
device = "cuda" if torch.cuda.is_available() else "cpu"
x = x.to(device)

参与同一个operation 的tensors 必须在兼容的device 上。常见错误:model 在GPU,input 还在CPU。

把tensor 移到GPU 不是给它加一个标签,而是把真实数据复制到另一块内存。频繁在CPU 和GPU 之间搬运会有成本,所以常见做法是把model、inputs、targets 长时间放在同一device,只把最终统计值移回CPU。

Scroll horizontally to view all columns.

Course data table
代码读法普通数字/含义
1e-30.001;e 在科学计数法里表示乘 10 的幂
1e-80.00000001
float32每个元素用 32 位浮点格式近似存储
torch.long本课用于整数类别 ID,不是“更精确的小数”
NaN / Inf不是可用的有限数:可能来自非法运算或溢出

这里的 1e-3 与 math.exp(-3) 完全不同:前者是 10⁻³,后者是自然指数 e⁻³。检查计算结果时用允许小误差的 assert_close;不要要求不同计算顺序的浮点结果逐位相同。

4. 用Tensor 完成一个Layer

公式:

Z=XW+bZ=XW+b

假设:

X: [4, 3] 4 个examples,每个3 个features

W: [3, 2] 3 个inputs 连接到2 个neurons

b: [2]

每个neuron 一个bias

输出:

Z: [4, 2]

代码:

python
X = torch.tensor([
    [0.8, 0.6, 0.2],
    [0.5, 0.4, 0.9],
    [0.9, 0.7, 0.1],
    [0.6, 0.8, 0.4],
])
W = torch.tensor([
    [0.7, 0.2],
    [0.5, -0.1],
    [-0.4, 0.8],
])
b = torch.tensor([0.1, -0.2])

Z = X @ W + b
print(Z.shape)  # torch.Size([4, 2])

@ 是matrix multiplication。

一行代码实际完成了多少计算

X @ W 中的四个examples 会同时进入两个neurons,因此一共执行4 × 2 = 8 个dot products。Bias 再被加到每一行。代码虽然只有一行,背后已经批量完成了整个layer 的forward。

这正是Tensor abstraction 的价值:业务代码描述高层数据变换,底层kernel 负责安排大量相同的小运算。

5. Broadcasting:为什么[2] 能加到[4,2]

Z 的shape 是[4,2],b 的shape 是[2]。

PyTorch 会把同一个bias vector 应用到batch 中每一行:

[4, 2]

+ [2]

-------

[4, 2]

这叫Broadcasting。

现在只要记住:PyTorch 会尝试从最后一个dimension 开始对齐;维度相同或其中一个为1 时通常可以广播。

Broadcasting 为什么存在

Broadcasting 的危险

能广播不等于语义正确。例如[B,T,C] + [T,1] 可能运行成功,但你原本也许想加的是[C] bias。遇到意外结果时,先把两个shapes 从右往左对齐,并逐维说明“这一维为什么应该复用”。

Knowledge check

prediction:[4,1],target:[4],相减结果为什么不是你想要的四个误差?

6. 用Tensor 重写Week 2

现在回到 Week 2 第 17 节的四个样本:x=[1,2,3,4]、y=[3,5,7,9],w=b=0,MSE 取平均,η=0.01。不改变这些条件,就应得到同样的第一步:loss=41,dw=−35,db=−12,更新后 w=0.35、b=0.12。Tensor 只是把四项同时算,不是另一种学习规律。

数据:

python
x = torch.tensor([[1.0], [2.0], [3.0], [4.0]])
y = torch.tensor([[3.0], [5.0], [7.0], [9.0]])

真实关系是:

y=2x+1y=2x+1

初始化parameters:

python
w = torch.tensor([[0.0]], requires_grad=True)
b = torch.tensor([0.0], requires_grad=True)

Forward:

python
prediction = x @ w + b
loss = ((prediction - y) ** 2).mean()

数学仍然是:

y^=Xw+b\hat y=Xw+b
MSE=1n(y^y)2\mathrm{MSE}=\frac{1}{n}\sum(\hat y-y)^2

7. requires_grad=True

python
w = torch.tensor([[0.0]], requires_grad=True)

意思是:

请追踪所有依赖w 的operations,以便以后计算Loss 对w 的gradient。

Forward 时,PyTorch 动态建立Computational Graph。

w, b

prediction

error

square

mean

loss

哪些Tensor 应该追踪Gradient

通常只有需要学习的浮点parameters 设置requires_grad=True。Inputs 和targets 默认不需要被optimizer更新,因此不必追踪它们的gradient。

requires_grad 不是“这个tensor 已经有gradient”,而是“如果后面的scalar loss 依赖它,请记录足够信息,以便未来计算gradient”。真正的数值要等backward() 后才会出现在.grad。

Leaf Tensor 与中间Tensor

直接创建并作为parameter 使用的w、b 通常是leaf tensors。prediction、error、loss 是operations 产生的中间tensors,并带有grad_fn 指向它们怎样被计算出来。

Optimizer 主要持有leaf parameters;中间tensors 用于连接graph,不是长期保存的模型状态。

8. .backward()

python
loss.backward()

这行代码从scalar loss 开始执行Week 4 的反向传播。

结果保存在:

python
print(w.grad)
print(b.grad)

它们分别对应:

LosswLossb\frac{\partial\mathit{Loss}}{\partial w}\qquad\frac{\partial\mathit{Loss}}{\partial b}

PyTorch 并没有“猜” gradient。它按照记录下来的graph,调用每个operation 的局部导数,再使用Chain Rule。

为什么通常对Scalar Loss 调用Backward

loss.backward() 隐含的起点是:

LossLoss=1\frac{\partial\mathit{Loss}}{\partial\mathit{Loss}}=1

如果对一个多元素tensor 直接backward,就必须额外指定每个元素怎样被加权,因为“一个vector 对parameters 的gradient”没有唯一方向。把batch errors 求mean 得到scalar loss,既定义了统一目标,也提供了清晰的backward 起点。

.grad 表示什么

w.grad 不是新weight,也不是本次应该直接加多少。它是当前位置:

Lossw\frac{\partial\mathit{Loss}}{\partial w}

Optimizer 还会结合learning rate、momentum 等规则决定真正update。

loss.backward() 不传额外参数时,通常要求 loss 只有一个元素。若你保留了每个样本的 loss 向量,可以先 .mean() 或 .sum() 明确你要优化什么,再 backward。向量本身也能求导,但必须指定各输出如何贡献反向信号;不是“只有标量才能求导”。

9. 为什么要清空Gradient

PyTorch 默认会累加gradients:

new grad = old grad + current grad

这对某些算法有用,但标准training loop 每一步通常要先清零:

python
w.grad.zero_()
b.grad.zero_()

使用optimizer 时:

python
optimizer.zero_grad()

如果忘记,下一步update 会混入前面batches 的gradients。

为什么默认设计成累加?因为同一个parameter 可能通过多条路径被使用,或用户希望用多个小batches模拟一个大batch。Autograd 无法假设你何时想结束累加,所以把清零时机交给training loop。

标准顺序通常是先zero_grad() 再forward。只要每个step 恰好清一次,放在上一步末尾也能工作;放在开头更容易读出“本step 从干净gradient 开始”。

Scroll horizontally to view all columns.

Course data table
两种相加为什么发生如何处理
同一次计算图中多个分支同一参数确实影响多个位置反向传播必须汇总,不能删掉分支
前后两次 backward 调用.grad 默认继续累加普通独立 step 先 zero_grad;有意积累则另定窗口

在第一次 backward 前,parameter.grad 通常是 None,不能无条件调用 parameter.grad.zero_()。使用 optimizer.zero_grad(set_to_none=True) 可以清理已有梯度;None 表示当前没有存储梯度,与数值全零是不同状态。Week 11 再讲有意的梯度累积。

10. 手动更新Parameter

更新时不应该把“更新操作本身”继续记录到下一张graph:

python
with torch.no_grad():
    w -= learning_rate * w.grad
    b -= learning_rate * b.grad

公式仍然是:

parameter:=parameterlearningRate×gradient\mathit{parameter}:=\mathit{parameter}-\mathit{learningRate}\times\mathit{gradient}

这里的w 是requires_grad=True 的leaf tensor;在torch.no_grad() 之外执行原地更新w -= ... 会被autograd 拒绝并抛出RuntimeError。若改写成非原地的w = w - ...,新w 才可能携带历史并把迭代连接起来(同时它不再是leaf)。参数更新应放在torch.no_grad() 中,或交给optimizer.step()。

11. 完整的Tensor Training Loop

python
import torch

x = torch.tensor([[1.0], [2.0], [3.0], [4.0]])
y = torch.tensor([[3.0], [5.0], [7.0], [9.0]])
w = torch.tensor([[0.0]], requires_grad=True)
b = torch.tensor([0.0], requires_grad=True)
learning_rate = 0.01

for step in range(1000):
    # Forward
    prediction = x @ w + b
    loss = ((prediction - y) ** 2).mean()

    # Backward
    loss.backward()

    # Update
    with torch.no_grad():
        w -= learning_rate * w.grad
        b -= learning_rate * b.grad

    # Reset gradients
    w.grad.zero_()
    b.grad.zero_()

print(w.item())  # approximately 2
print(b.item())  # approximately 1

这就是Week 2 和Week 4 的代码化版本。

12. nn.Module:组织Parameters 和Forward

真实模型不应手工管理大量parameters。

python
import torch.nn as nn


class LinearModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = nn.Linear(1, 1)

    def forward(self, x):
        return self.linear(x)

nn.Linear(1,1) 内部已经包含:

weight

bias

model(x) 会调用forward(x),同时保留framework 所需的hooks 和graph 行为。

nn.Module 为什么不仅是代码封装

普通class 也能写forward,但nn.Module 还维护一棵可遍历的模型对象树。把nn.Parameter 或child module 赋值给self 后,PyTorch 才能统一完成:

model.parameters() 收集可训练参数

model.to(device)

递归移动参数和buffers

model.state_dict() 保存可恢复状态

model.train()/eval() 切换子模块运行模式

因此Module registration 是训练框架能够管理模型生命周期的基础。

nn.Linear(1,1) 默认随机初始化,因此直接运行未必和手算第一步相同。若要公平对照 Week 2,先在 no_grad 中把 weight 与 bias 都设为 0。它的 weight shape=[1,1],一般规则是 [输出数,输入数];forward 对行样本做 X @ weight.T + bias。完整三版本对照见 course_examples/week05_three_ways.py。

13. Loss Function 与Optimizer

python
model = LinearModel()
loss_function = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

它们的职责:

model → 产生prediction

loss_function → 衡量prediction 有多差

autograd → 计算gradients

optimizer → 更新parameters

Loss function 和optimizer 被分开,是因为它们回答不同问题:Loss 定义“什么叫变好”;optimizer 决定“根据gradient 怎样走”。同一个model 可以换训练目标,同一个loss 也可以搭配不同optimizer,而Backprop 负责连接二者。

14. 标准训练循环:每一行究竟改变了什么

现在把 Tensor、自动求导、Module 和 optimizer 放回同一件事。下面程序可独立运行;Linear 默认随机初始化,所以先显式设为零,才能与 Week 2 比较。

python
import torch

X = torch.tensor([[1.0], [2.0], [3.0], [4.0]])
y = torch.tensor([[3.0], [5.0], [7.0], [9.0]])
model = torch.nn.Linear(1, 1)
with torch.no_grad():
    model.weight.zero_()
    model.bias.zero_()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

optimizer.zero_grad(set_to_none=True)
prediction = model(X)
if prediction.shape != y.shape:
    raise ValueError("prediction and target must describe the same pairs")
loss = torch.nn.functional.mse_loss(prediction, y, reduction="mean")
loss.backward()
print("before step", loss.item(), model.weight.grad, model.bias.grad)
optimizer.step()
with torch.no_grad():
    new_loss = torch.nn.functional.mse_loss(model(X), y)
print("after step", model.weight.item(), model.bias.item(), new_loss.item())

Scroll horizontally to view all columns.

Course data table
时刻w / blossgrad(w) / grad(b)
清梯度后0 / 0本轮还未计算None / None
forward 后0 / 041None / None
backward 后0 / 0旧 forward 的 41−35 / −12
step 后0.35 / 0.12旧变量仍是 41仍保留 −35 / −12
重新 forward0.35 / 0.12new_loss≈28.45315no_grad 不会清除原有梯度

表中近似值来自同一固定输入的数学计算。实际浮点打印会有尾数差异。把这些步骤放进循环后,每轮第一步清除旧梯度,避免上一轮贡献混入。zero_grad 不会把学到的参数清零;optimizer.step 也不会自动把 grad 清零。

能运行不代表配对正确:prediction 的 shape 是 [4,1],若把 y 写成 [4],广播可能把它们扩成 [4,4],得到样本之间的两两比较。原本只该比较四对数据,却算了十六对。保留显式 shape 检查,比只盯着 scalar loss 更可靠。

下一周预测会变成 [B,T,Vocab],目标是 [B,T],这是分类接口有意规定的不同 shape,不沿用回归的“完全相等”检查。每种检查都要先说明轴的含义和损失函数的输入约定。

Knowledge check

EX05:先预测三种改动的后果:A 删除 step;B 第二轮不清 grad;C 只调用 eval,正常计算 loss.backward。哪些参数或梯度会改变?

15. Shape Debugging 的固定方法

当代码出现matmul、reshape 或loss 错误时,不要盲猜。

在关键位置打印:

python
print("x:", x.shape)
print("weight:", model.linear.weight.shape)
print("prediction:", prediction.shape)
print("target:", y.shape)

逐项确认:

  1. 每个dimension 的业务含义;
  2. Matrix multiplication 的中间维度是否相等;
  3. Prediction 和target 是否对齐;
  4. Batch dimension 是否意外丢失;
  5. dtype 和device 是否一致。

Shape 在AI 工程里相当于接口契约和数据schema。

Scroll horizontally to view all columns.

Course data table
看到的对象先问什么后面典型 shape
整数输入有几行,每行几个 token?[B,T]
浮点表示每个 token 有几个特征?[B,T,C]
候选分数每个位置有几个可选答案?[B,T,V]

B、T、C、V 是轴的名字,不是四种新数学。先把 [2,3,4] 读成“两行、每行三个位置、每个位置四个数”,再看后面的层改变了哪一项。

Knowledge check

从 [2,3] 的 token IDs 查一张 [5,4] 表,为什么结果是 [2,3,4]?

16. train() 与eval():知道用途即可

python
model.train()
model.eval()

它们不是控制Autograd 开关,而是改变Dropout、BatchNorm 等layers 的运行方式。

Validation 常见写法:

python
model.eval()
with torch.no_grad():
    prediction = model(x)

后面训练GPT 时再详细使用。

三个容易混淆的开关:

model.train() → layers 使用training 行为

model.eval() → layers 使用evaluation 行为

torch.no_grad() → 不记录backward graph

eval() 不会自动关闭gradient;no_grad() 也不会自动关闭Dropout。Validation 通常同时使用eval() 和no_grad(),因为两种需求都存在。

17. Week 5 最应该理解的6 件事

  1. Tensor 是带shape、dtype、device 的多维数字容器。
  2. X @ W + b 可以并行处理整个batch。
  3. Shape 是最重要的debugging 信息之一。
  4. requires_grad=True 让PyTorch 记录计算依赖。
  5. loss.backward() 执行Chain Rule 并填充.grad。
  6. 标准training loop 是zero → forward → loss → backward → step。

18. Week 5 → Week 6

目前模型接收的是数字features。

但GPT 的input 是文字。

下一个问题:

怎样把文字变成tensor?模型怎样用这些tensors 预测下一个token?

Week 6 会建立这条路径:

Text

Token IDs

Embeddings

Logits

Cross Entropy Loss