工作台课程

CS221 · 人工智能:原理与技术

Lecture 1 · 张量、梯度与监督学习

Lecture 1 · 张量、梯度与监督学习

CS221: Artificial Intelligence — Principles and Techniques · Stanford · Autumn 2025
📺 课程视频 · 💻 可执行讲义仓库tensors.py / backpropagation.py / linear_regression.py

一句话总览:张量是现代机器学习的”原子”,梯度告诉我们如何改进函数,而监督学习 = 假设类 + 损失函数 + 优化算法。


承上启下

这是整门课的第一讲,没有”上一讲”可承。课程不从搜索或逻辑讲起,而是直接给出现代 AI 的三块技术基石,三者按依赖关系依次出场:

  1. 张量(tensor) 是表示层:数据、模型参数、梯度、中间激活值,全部统一表示成多维数组。有了统一表示,才能把五花八门的计算交给同一套高度优化的硬件与软件栈。
  2. 梯度(gradient) 是改进层:它量化了「函数值对每个参数有多敏感」。知道了敏感度,才知道参数该往哪个方向调、调多少。反向传播就是在任意复杂函数上自动算梯度的通用算法。
  3. 监督学习(supervised learning) 是任务层:把「从数据中学出一个预测器」拆解成假设类、损失函数、优化算法三个相互正交的设计决策,前两块基石在这里合流——参数是张量,优化靠梯度。

下一讲 Lecture 2 · 线性回归 会把本讲末尾跑通的一维玩具例子抽象成一条可复用的机器学习流水线:引入特征提取 $\phi(x)$、把梯度下降升级为随机梯度下降(SGD),并首次讨论泛化问题。


1. 张量(Tensor)

张量是多维数组,是数据、模型参数、梯度、中间激活值的统一表示形式。

提示

为什么机器学习要把一切都表示成张量?因为这带来两个决定性的好处:

  1. 硬件效率:GPU 擅长的就是对大块规整数组做并行运算。只要计算被表达成少量张量运算,就能自动享受几个数量级的加速(见 §1.5)。
  2. 统一的求导基底:数据、参数、梯度形状规则、类型统一,自动微分系统(§2.3)才能对任意由张量运算组合出的函数机械地求梯度。

换句话说:张量是「表示」与「计算」之间的通用接口——这门课后面的一切(回归、分类、神经网络)都建立在它上面。

1.1 张量的阶(Order / Rank)

阶(order) 名称 例子 shape
0 标量 scalar np.array(42) ()
1 向量 vector np.array([1, 2, 3]) (3,)
2 矩阵 matrix np.array([[1,2,3],[4,5,6]]) (2, 3)
n n 阶张量 图像批次、注意力权重…… (d₁, …, dₙ)

一个张量有”阶”个轴(axis):对矩阵来说,轴 0 是行,轴 1 是列。shape 记录每个轴的长度,是读写张量代码时最重要的心智工具——看不懂一段张量代码时,第一步永远是把每个变量的 shape 标出来。

1.2 创建、切片与视图

讲义 tensors.py 还演示了几类日常操作,值得记住:

import numpy as np

# 结构化创建
np.zeros((2, 3)); np.ones((2, 3))      # 全 0 / 全 1
np.random.randn(2, 3)                  # 标准正态随机数(初始化权重常用)
np.eye(3); np.diag([1, 2, 3])          # 单位阵 / 对角阵

# 切片:逐层剥掉一个轴
x = np.random.randn(2, 2, 3)
x[1]          # shape (2, 3)
x[1][0]       # shape (3,)
x[1][0][2]    # 标量

# 视图(view):不复制数据
row = x[0]            # 取行
col = x[:, 1]         # 取列
xt  = x.transpose(1, 0, 2)  # 交换轴

# 持久化
np.save("x.npy", x);  x = np.load("x.npy")

注意

切片、转置得到的是视图而非副本——它们与原张量共享同一块内存。修改视图会同时改掉原张量,反之亦然。需要独立副本时要显式 .copy()。这是 NumPy/PyTorch 新手最常踩的坑之一。

1.3 机器学习中的典型张量

import numpy as np

N, L, D = 3, 4, 2            # 样本数、序列长度、特征维度
x = np.ones(D)               # 单个数据点          (D,)
x = np.ones((N, D))          # 一批数据点          (N, D)
x = np.ones((N, L, D))       # 语言建模:批×序列   (N, L, D)
x = np.ones((N, 2, 2, 3))    # 视觉:批×高×宽×通道 (N, H, W, C)
w = np.ones((3, 2))          # 权重矩阵:Din → Dout

这几种 shape 会贯穿全课:(N, D)Lecture 2 · 线性回归Lecture 3 · 线性分类 的数据矩阵;(N, L, D)Lecture 17 · 语言模型 处理序列的标准形状;权重矩阵 (Din, Dout) 的作用是把 Din 维的表示线性变换到 Dout 维。

说明

神经网络的参数本质上就是一组张量的集合。比如 DeepSeek-V3 的全部权重文件,就是几百个命名张量按层组织起来的清单——「模型」在磁盘上不过如此。

1.4 逐元素运算与广播

  • 逐元素运算(elementwise)x + yx * yx ** 2np.sqrt(x) 等对每个位置独立作用,输出与输入同形状。
  • 广播(broadcasting):形状不完全相同的张量运算时,长度为 1(或缺失)的轴会被自动”复制”扩展。例如 (2, 4, 6) @ (6, 3) → (2, 4, 3):同一个权重矩阵被自动应用到批次里的每个切片,不必手写循环。
  • np.triu() / np.tril() 取上/下三角部分——这正是 Transformer 里构造因果注意力掩码的工具(详见 CS336 的 Lecture 3 · 架构与超参数(未找到对应页面))。

1.5 为什么要用张量运算?——效率

同一个计算往往有多种写法,尽量用张量运算表达(底层高度优化,GPU 上更快):

# 慢:Python 三重循环手写矩阵乘法
for i in range(N):
    for j in range(N):
        for k in range(N):
            y[i, j] += x[i, k] * w[k, j]

# 快:一行搞定,快几个数量级
y = x @ w

原因有因果链条:Python 解释器逐条执行循环体,每次乘加都伴随大量解释开销;而 @ 调用的是编译好的 BLAS 内核,能利用向量化指令与缓存友好的内存访问;换到 GPU 上还能上万核并行。数据越大差距越悬殊。所以讲义的建议是:把计算改写成少量张量运算是一种”解谜”,需要练习,就像学一门新语言

1.6 einops:给轴命名

传统写法容易搞混维度(transpose(-2, -1) 里的 -2、-1 到底是什么?)。
einops 的核心思想:像给变量命名一样给轴命名——名字描述这个轴代表什么。
例如”行是数据点”的矩阵,轴名就是 example feature。(CS336 的 Lecture 2 · PyTorch 与资源核算(未找到对应页面) 在真实 Transformer 代码里大量使用这套记号。)

einsum 是一个能表达大量运算的单一函数——不只是矩阵乘法的一种推广

向量上的例子

from einops import einsum
x = np.array([0, 1, 10])

y = einsum(x, "i -> i")            # 恒等:       y[i] = x[i]
y = einsum(x, "i ->")              # 求和:       y = Σᵢ x[i]
y = einsum(x, x, "i, i -> i")      # 逐元素乘:   y[i] = x[i]·x[i]
y = einsum(x, x, "i, i ->")        # 点积:       y = Σᵢ x[i]·x[i]
y = einsum(x, x, "i, j -> i j")    # 外积:       y[i,j] = x[i]·x[j]
y = einsum(x, x, x, "i, i, i -> i")    # 三重逐元素乘
y = einsum(x, x, x, "i, j, k -> i j k")  # 三重外积

矩阵上的例子

m = np.array([[0, 1, 2], [1, 10, 0]])

y = einsum(m, "i j ->")            # 全部求和
y = einsum(m, "i j -> i")          # 行和(j 被求和掉)
y = einsum(m, "i j -> j")          # 列和(i 被求和掉)
y = einsum(m, "i j -> j i")        # 转置
y = einsum(m, x, "i j, j -> i")    # 矩阵 × 向量
y = einsum(m, m, "i k, j k -> i j")  # m·mᵀ
y = einsum(m, m, "k i, k j -> i j")  # mᵀ·m

通用原则(读懂任何 einsum 的钥匙)

einsum 的输入是若干张量及其命名轴(名字可以重叠),输出是一个张量及其轴列表(输入轴的子集)。语义只有一条规则:

对输入轴的每一种取值组合,把各张量对应位置的元素相乘、累加到输出的对应位置;没有出现在输出里的轴,就被求和掉。

例如 einsum(m, m, "i k, j k -> i j") 的含义写成公式就是

$$y_{ij} = \sum_{k} m_{ik}\, m_{jk}$$

其中 $i, j$ 出现在输出中所以保留,$k$ 没有出现所以被求和——这正是 $M M^\top$。

提示

einsum 本质上只是乘法 + 加法 + 记账(bookkeeping)。所有那些名字唬人的运算——点积、外积、矩阵乘法、批量注意力分数——都是同一件事的特例:选哪些轴对齐相乘、哪些轴求和掉。掌握了这条规则,就再也不用背运算 API 了。

配套的另外两个函数:

from einops import reduce, rearrange

y = reduce(x, "... hidden -> ...", "sum")   # 沿某轴归约(sum/mean/max/min)
x = rearrange(x, "... (heads d) -> ... heads d", heads=2)  # 拆分/合并轴

reduce 取代 x.sum(dim=-1) 这类写法,把”沿哪个轴归约”写在名字里;rearrange 负责拆分与合并轴(如把 (seq, 8) 拆成 (seq, 2, 4) 做多头处理再合回来),... 通配任意批次轴。

📖 延伸阅读:einops 官方教程


2. 梯度(Gradient)

2.1 目标函数:从张量运算到一个标量

用张量运算组合出目标函数(objective function):输入张量 → 输出一个标量。以线性回归为例:

x = np.array([[1, 2, 0], [0, -1, 1]])  # (n, d) 数据矩阵
y = np.array([0, 3])                   # (n,)   目标值
w = np.array([1, 0, 1])                # (d,)   权重

def objective(w: np.ndarray) -> float:
    loss = np.sum((x @ w - y) ** 2)    # 预测 → 残差 → 平方 → 求和
    return loss

写成数学形式:

$$\mathcal{L}(w) = \sum_{i=1}^{n} \big(x^{(i)} \cdot w - y^{(i)}\big)^2 = \lVert Xw - y \rVert_2^2$$

其中 $X \in \mathbb{R}^{n \times d}$ 是数据矩阵(每行一个样本),$w \in \mathbb{R}^d$ 是权重向量,$y \in \mathbb{R}^n$ 是目标值;$x^{(i)} \cdot w$ 是第 $i$ 个样本的预测值,减去真值 $y^{(i)}$ 得到残差,平方后求和就是总损失。

代入不同的 $w$ 就能算出不同的损失值。最终目标:找到使 $\mathcal{L}(w)$ 最小的 $w$。 当前的问题是:给定一个 $w$,该往哪个方向微调才能让损失变小?

2.2 梯度的含义

  • 偏导数(partial derivative):只改变输入张量的某一个元素、其余保持不动时,函数值变化多快。形式化地,对第 $j$ 个分量:

$$\frac{\partial f}{\partial w_j}(w) = \lim_{\varepsilon \to 0} \frac{f(w + \varepsilon e_j) - f(w)}{\varepsilon}$$

其中 $e_j$ 是第 $j$ 个单位向量(只有第 $j$ 位是 1)。分子是”轻推一下第 $j$ 个元素后函数值的变化”,除以推的幅度 $\varepsilon$ 就是变化率。

  • 梯度:所有偏导数组成的张量 $\nabla f(w) = \left(\frac{\partial f}{\partial w_1}, \dots, \frac{\partial f}{\partial w_d}\right)$。它与输入同形状——输入有多少个元素,就有多少个偏导数,一一对应地放回原来的位置。正因为形状相同,参数更新才能写成逐元素的 w -= lr * grad

  • 梯度指向函数值上升最快的方向,因此负梯度方向是下降最快的方向——这就是所有梯度类优化算法的出发点。

例子

对 $f(x_1, x_2) = (x_1 + x_2)^2$,两个偏导数都是 $\frac{\partial f}{\partial x_1} = \frac{\partial f}{\partial x_2} = 2(x_1 + x_2)$。
在点 $(1, 2)$ 处梯度为 $\nabla f = (6, 6)$:往 $(6, 6)$ 方向走 $f$ 增长最快,往 $(-6, -6)$ 方向走下降最快;而沿 $(1, -1)$ 方向走(与梯度垂直)$f$ 几乎不变。

说明

  1. 训练:优化深度模型的参数使损失最小——最常见的用途,本讲 §3 与后续所有学习算法都靠它。
  2. 对抗样本(adversarial examples):反过来固定参数、优化输入使误差最大。梯度告诉攻击者”往哪个方向扰动图片最能骗过模型”,往往肉眼不可察觉的扰动就足以让分类器出错。
  3. 数据混合比例:把多个数据集的采样权重当作可微变量来优化。这说明”可以被求导的对象”远不止模型参数——凡是影响标量目标的量都行。

2.3 自动微分(Autodiff)与计算图

手工求梯度繁琐且易错。好在再复杂的函数也是由基本运算(+、×、exp、log…)复合而成,于是可以:

  1. 把函数显式构建成一张计算图(computation graph)
  2. 沿图递归地用链式法则(chain rule)算偏导

这就是反向模式自动微分(PyTorch / JAX 的核心,可追溯到 Werbos 1974)。

以 $f(x_1, x_2) = (x_1 + x_2)^2$,$x_1 = 2,\ x_2 = 3$ 为例:

graph BT
    x1["x1<br/>value = 2<br/>grad = 10"] --> s["sum = x1 + x2<br/>value = 5<br/>grad = 10"]
    x2["x2<br/>value = 3<br/>grad = 10"] --> s
    s --> y["y = sum²<br/>value = 25<br/>grad = 1"]

    style y fill:#ffe0b2,stroke:#e65100
    style s fill:#e1f5fe,stroke:#0277bd
    style x1 fill:#e8f5e9,stroke:#2e7d32
    style x2 fill:#e8f5e9,stroke:#2e7d32
  • 叶子节点($x_1$, $x_2$):固定的输入值
  • 中间节点:对依赖项执行一个基本运算
  • 根节点($y$):最终计算结果
  • grad 的含义:这个节点的值变化 $\varepsilon$ 时,根节点变化多少倍(根节点对自己求导,恒有 $\frac{\partial y}{\partial y} = 1$)

图里的数字可以手算验证一遍。令中间量 $s = x_1 + x_2 = 5$,则 $y = s^2 = 25$。反向走:

$$\frac{\partial y}{\partial s} = 2s = 10, \qquad \frac{\partial y}{\partial x_1} = \frac{\partial y}{\partial s} \cdot \frac{\partial s}{\partial x_1} = 10 \times 1 = 10$$

链式法则说的是:$x_1$ 对 $y$ 的影响要经过 $s$ 传递,所以总影响 = 「$s$ 对 $y$ 的影响」×「$x_1$ 对 $s$ 的影响」。局部导数逐边相乘、沿路径传播,这正是反向传播每一步做的事。

提示

为什么从根往叶子算(反向),而不是从叶子往根算(前向)?因为机器学习里目标函数是「百万级参数 → 1 个标量损失」。反向模式一次遍历就能得到所有参数的偏导(代价约等于 2~3 次前向计算,与参数个数无关);若用前向模式或数值差分,每个参数都要单独传播一次,百万参数就要百万次遍历——完全不可行。「输出少、输入多」的场景天然适合反向模式。

2.4 前向与反向传播

flowchart LR
    subgraph F["① 前向传播 forward"]
        direction LR
        A["叶子 → 根<br/>逐节点计算 value"]
    end
    subgraph B["② 反向传播 backward"]
        direction LR
        C["根 → 叶子<br/>用链式法则累积 grad"]
    end
    F --> B
前向 forward 反向 backward
遍历方向 叶子 → 根(拓扑序) 根 → 叶子(逆拓扑序)
计算内容 每个节点的 value 每个节点的 grad
数学依据 函数复合 链式法则
初始化 叶子的值已给定 root.grad = 1,其余为 0

拓扑排序(topological sort) 保证「依赖在前、使用在后」:前向按此序算值时,每个节点的输入必然已经就绪;反向按逆序传梯度时,每个节点从下游收到的梯度必然已经收齐。

每种运算节点只需定义两件事——如何算值、如何把梯度传给依赖项(mini-PyTorch,讲义中还实现了 InputSubtractMultiplyDotProduct 等节点):

class Add(Node):                      # z = a + b
    def forward(self):
        a, b = self.dependencies
        self.value = a.value + b.value
    def backward(self):               # dz/da = 1, dz/db = 1
        a, b = self.dependencies
        a.grad += self.grad
        b.grad += self.grad

class Squared(Node):                  # z = a²
    def forward(self):
        a, = self.dependencies
        self.value = a.value ** 2
    def backward(self):               # dz/da = 2a
        a, = self.dependencies
        a.grad += 2 * a.value * self.grad

完整的反向传播算法只有四步:

def backpropagation(root: Node):
    nodes = topological_sort(root)        # 1. 拓扑排序(依赖在前)
    for node in nodes:                    # 2. 前向:算所有 value
        node.forward()
    for node in nodes:                    # 3. grad 清零,根节点设为 1
        node.grad = np.zeros_like(node.value)
    root.grad = np.ones_like(root.value)
    for node in reversed(nodes):          # 4. 反向:累积所有 grad
        node.backward()

注意

注意 backward 里都是 += 而不是 =:一个节点可能被多个下游节点使用,每条路径都会传回一份梯度,总梯度是所有路径贡献之和(多元链式法则)。例如 $y = x \cdot x$ 中 $x$ 被用了两次,两条边各传回 $x$ 的梯度,累加后才是正确的 $\frac{\partial y}{\partial x} = 2x$。这也是 PyTorch 每步训练前要 zero_grad() 的原因——梯度默认累加,不清零就会混入上一步的残留。

这套机制在 Lecture 4 · 深度学习 里将原样作用于多层神经网络——网络再深,也只是计算图更大而已。


3. 监督学习(Supervised Learning)

3.1 从预测器到学习

预测器(predictor):把输入映射到输出的函数。
例:根据学习小时数(输入)预测考试分数(输出)。

问题是:预测器从哪来?——从数据中出来:

flowchart LR
    D["训练数据<br/>(输入, 输出) 样本对"] --> L["学习算法"]
    L --> P["预测器 f"]
    X["新输入 x"] --> P
    P --> Y["预测输出 y"]

    style D fill:#e8f5e9,stroke:#2e7d32
    style L fill:#e1f5fe,stroke:#0277bd
    style P fill:#ffe0b2,stroke:#e65100
training_data = [
    Example1D(input=1, output=4),
    Example1D(input=2, output=6),
    Example1D(input=4, output=7),
]

提示

“学习”在这里的确切含义是:把「写程序」变成「给例子」。传统编程是人直接写出输入到输出的规则;监督学习是人只提供 (输入, 输出) 样本对,由算法自动找出规则。当规则难以言传(识别猫、翻译句子)而例子容易收集时,后者的优势是压倒性的。

3.2 机器学习的三个关键设计问题

# 问题 答案
1 哪些预测器是可能的 假设类(hypothesis class)
2 一个预测器有多好 损失函数(loss function)
3 如何找到最好的预测器? 优化算法(optimization algorithm)

这三个问题相互正交:换假设类不必换优化算法,换损失函数不必换假设类。整门课的机器学习部分(直到 Lecture 4 · 深度学习)都是在这个模板里替换零件。

① 假设类:参数化预测器

不再只看一个固定函数,而是定义一函数,让算法从中挑最好的。
对线性预测器,参数 = 权重(weight)+ 偏置(bias)

$$f_{w,b}(x) = w \cdot x + b$$

其中 $w$ 控制斜率(输入每增加 1,预测增加多少),$b$ 控制截距(输入为 0 时的基准预测值)。

@dataclass(frozen=True)
class Parameters1D:
    weight: float
    bias: float

def f(params: Parameters1D, x: float) -> float:
    return params.weight * x + params.bias

假设类 = 遍历所有参数取值能得到的全部预测器的集合,即 $\mathcal{F} = \{ f_{w,b} : w \in \mathbb{R},\ b \in \mathbb{R} \}$。
对应到深度学习:假设类 ↔ 模型架构,预测器 ↔ 模型,参数 ↔ 一组张量。

② 损失函数:平方损失

单个样本的损失取”预测与真值差距”的平方,训练损失取所有样本的平均:

$$\ell(w, b; x, y) = \big(f_{w,b}(x) - y\big)^2, \qquad \mathrm{TrainLoss}(w, b) = \frac{1}{n} \sum_{i=1}^{n} \big(f_{w,b}(x^{(i)}) - y^{(i)}\big)^2$$

其中 $(x^{(i)}, y^{(i)})$ 是第 $i$ 个训练样本,$n$ 是样本总数。取平方的好处:误差无论正负都被惩罚、离得越远罚得越重,而且处处可微——这为下一步用梯度优化铺路。

def compute_loss(params, example) -> float:
    residual = f(params, example.input) - example.output
    return residual ** 2                       # 平方损失

def compute_train_loss(params, training_data) -> float:
    return np.mean([compute_loss(params, ex) for ex in training_data])

③ 优化算法:梯度下降

给定一组参数就能算出训练损失 ⇒ 「找最好的预测器」正式成为一个优化问题:$\min_{w,b} \mathrm{TrainLoss}(w, b)$。
梯度指向损失上升最快的方向,所以往反方向走就行

$$\begin{aligned} w &\leftarrow w - \eta \cdot \frac{\partial\, \mathrm{TrainLoss}}{\partial w} \\ b &\leftarrow b - \eta \cdot \frac{\partial\, \mathrm{TrainLoss}}{\partial b} \end{aligned}$$

其中 $\eta$(eta)是学习率(learning rate),控制每一步走多远。

flowchart TD
    A["初始化参数 w"] --> B["计算训练损失 L(w)"]
    B --> C["计算梯度 ∇L(w)"]
    C --> D["更新参数<br/>w ← w − η·∇L(w)"]
    D --> E{"收敛 / 达到步数?"}
    E -- 否 --> B
    E -- 是 --> F["输出最终预测器"]

    style D fill:#ffe0b2,stroke:#e65100
params = Parameters1D(weight=0, bias=1)
learning_rate = 0.01                   # η: 学习率

for step in range(10):
    grad = compute_gradient_train_loss(params, training_data)
    params = Parameters1D(
        weight=params.weight - learning_rate * grad[0],
        bias=params.bias   - learning_rate * grad[1],
    )   # 每一步之后训练损失都在下降

说明

  • 学习率控制”开多快”——太小收敛慢,太大会在谷底两侧震荡甚至发散,是速度 vs 稳定性的权衡。
  • 梯度下降对凸函数保证收敛到全局最优(无正则的线性回归正是凸的);深度学习的损失面非凸,没有理论保证,但实践中依然有效。
  • 常用变体:随机梯度下降(SGD)、Adam 等——SGD 的动机与细节在 Lecture 2 · 线性回归 展开。

4. 本讲总结

mindmap
  root((Lecture 1))
    张量
      表示一切:数据/参数/梯度
      用张量运算表达计算(快)
      einops:给轴命名,代码更可读
    梯度
      偏导数:改一个元素,函数变多少
      梯度与输入同形状,负方向下降最快
      计算图 + 链式法则 = 反向传播
    监督学习
      预测器:输入 → 输出
      假设类 / 损失函数 / 优化算法
      梯度下降 + 学习率
  • 张量表示一切;尽量用少量张量运算表达计算(这是个”谜题”,需要练习,就像学一门新语言)
  • einsum:对输入轴的每种取值组合做乘法,累加到输出——没出现在输出里的轴被求和
  • 梯度/偏导数:输入变化时函数值变化多少;反向传播是在计算图上求梯度的通用算法,一次反向遍历得到全部参数的梯度
  • 监督学习流水线:训练数据 → 学习算法 → 预测器;三大设计决策 = 假设类 + 损失函数 + 优化算法
  • 梯度下降:沿负梯度方向迭代更新参数,学习率控制步长

复习自测

题目

前者是 $y_{ij} = \sum_k m_{ik} m_{jk}$,即 $M M^\top$:$k$ 没出现在输出里所以被求和,$i, j$ 保留。后者连 $i, j$ 也被求和,得到一个标量 $\sum_{i,j,k} m_{ik} m_{jk}$——也就是 $M M^\top$ 所有元素之和。规则始终只有一条:不在输出里的轴被求和掉。

题目

因为一个节点可能被多个下游节点使用,总梯度是所有路径贡献之和。例如 $y = x \cdot x$:$x$ 通过两条边进入乘法节点,每条边各传回一份 $\frac{\partial y}{\partial x}$ 的贡献(各为 $x$),累加得 $2x$ 才正确;若用 =,后一条路径会覆盖前一条,得到错误的 $x$。

题目

前向:$s = 1 + 2 = 3$,$y = s^2 = 9$。反向:$y.\mathrm{grad} = 1$;$s.\mathrm{grad} = 2s \cdot 1 = 6$;$x_1.\mathrm{grad} = x_2.\mathrm{grad} = 6 \times 1 = 6$。与 §2.2 直接求偏导的结果 $\nabla f = (6, 6)$ 一致。

题目

梯度是”每个输入元素各配一个偏导数”,一一对应放回原位置,所以形状必然相同。正因如此,参数更新才能写成逐元素运算 $w \leftarrow w - \eta \nabla \mathcal{L}(w)$——每个参数用自己的偏导数、按同一学习率独立更新。

题目

数值差分求 $d$ 个参数的梯度需要约 $d + 1$ 次前向计算,百万参数就是百万次前向,代价随参数数线性爆炸(且有数值误差)。反向模式自动微分一次前向加一次反向(约 2~3 倍前向成本)就能同时得到所有参数的精确梯度,代价与参数个数基本无关。


参考资料