0.3 知道错了以后模型怎么改
0.2 给出了一个数:平均损失
本节回答这个问题。起点是一个很朴素的问题:把某个参数改动一点,损失会变化多少。
0.3.1 导数就是敏感度
导数(derivative)回答的问题是:自变量改动一点,函数值跟着改变多少。把损失记作
这一行的含义比它的形式重要。
0.3.2 从一元到多元
模型有
一元的近似式在多维情形下变成
这一行同时回答了几个问题。梯度的形状与参数相同,因为每个参数对应一个偏导数。
0.3.3 梯度下降
沿着负梯度走一小步,再重新计算梯度:
这是梯度下降(gradient descent)的全部结构。后续的 Adam 一类优化器都在此基础上调整步长与方向,核心仍然是这个式子。
0.3.4 链式法则与反向传播
ConVIRT 的损失是一长串复合运算。从参数出发的链条是:
其中
含义是:如果
反向传播做的就是把这件事自动化:从损失出发,沿计算图从右往左逐级应用链式法则,得到每个参数的梯度。PyTorch 的 backward() 在这一步上没有额外的数学原理,它就是沿计算图反复应用链式法则;实现层面还涉及计算图的构建、向量-雅可比积与梯度累积等工程机制。
0.3.5 ConVIRT 实际怎么训练
论文附录给出的设置如下:
- 优化器 Adam,初始学习率
,weight decay ,批次大小固定为 。 - 每
步计算一次验证损失,若连续 次验证损失没有下降,把学习率乘 。 - 评估
次后停止,保留验证损失最低的检查点。 - 图像编码器以 ImageNet 预训练权重初始化,文本编码器以 ClinicalBERT 权重初始化。
- 使用混合精度训练,预训练数据为 MIMIC-CXR v2,约
万对图文。
编码器的初始化容易被误解为随机起点。ConVIRT 的图像编码器以 ImageNet 权重初始化,文本编码器以 ClinicalBERT 权重初始化,文本编码器的前 6 层还被冻结。因此 0.1 里那个相似度矩阵已经带有一定的语义结构。
代码验证
先验证导数确实等于敏感度。以下沿用 0.1 的
import numpy as np
np.set_printoptions(precision=3, suppress=True)
V = np.array([
[0.886, 0.000, 0.000, 0.394, 0.000, 0.000, 0.000, 0.246],
[0.000, 0.886, 0.000, 0.000, 0.394, 0.000, 0.000, 0.246],
[0.000, 0.000, 0.886, 0.000, 0.000, 0.394, 0.000, 0.246],
[0.000, 0.000, 0.886, 0.000, 0.000, 0.000, 0.394, 0.246],
])
U = np.array([
[0.886, 0.000, 0.000, 0.394, 0.000, 0.000, 0.000, -0.246],
[0.000, 0.886, 0.000, 0.000, 0.394, 0.000, 0.000, -0.246],
[0.000, 0.000, 0.886, 0.000, 0.000, 0.394, 0.000, -0.246],
[0.000, 0.000, 0.886, 0.000, 0.000, 0.000, 0.394, -0.246],
])
def normalize(a):
return a / np.linalg.norm(a, axis=1, keepdims=True)
def loss_of(V, U, tau=0.1):
S = normalize(V) @ normalize(U).T
Z = S / tau - (S / tau).max(axis=1, keepdims=True)
P = np.exp(Z) / np.exp(Z).sum(axis=1, keepdims=True)
return float(-np.log(np.diag(P)).mean())
h = 1e-4
gU = np.zeros_like(U)
for i in range(U.shape[0]):
for j in range(U.shape[1]):
Up, Um = U.copy(), U.copy()
Up[i, j] += h
Um[i, j] -= h
gU[i, j] = (loss_of(V, Up) - loss_of(V, Um)) / (2 * h)
i, j, delta = 2, 2, 0.05
Up = U.copy()
Up[i, j] += delta
print(f"u[2][2] 的偏导 = {gU[i, j]:.4f}")
print(f"实际损失变化 = {loss_of(V, Up) - loss_of(V, U):.6f}")
print(f"梯度预测变化 = {gU[i, j] * delta:.6f}")
print("全部 32 个偏导里绝对值最大 =", round(float(np.abs(gU).max()), 4))输出:
u[2][2] 的偏导 = 0.0600
实际损失变化 = 0.003179
梯度预测变化 = 0.003002
全部 32 个偏导里绝对值最大 = 0.1721把
再让梯度下降跑一遍。这里同时对
V, U = V.copy(), U.copy()
eta, steps = 0.6, 300
for step in range(steps):
gV, gU = np.zeros_like(V), np.zeros_like(U)
for A, gA in ((V, gV), (U, gU)):
for i in range(A.shape[0]):
for j in range(A.shape[1]):
Ap, Am = A.copy(), A.copy()
Ap[i, j] += h
Am[i, j] -= h
if A is V:
gA[i, j] = (loss_of(Ap, U) - loss_of(Am, U)) / (2 * h)
else:
gA[i, j] = (loss_of(V, Ap) - loss_of(V, Am)) / (2 * h)
V, U = V - eta * gV, U - eta * gU
if step in (0, 4, 24, 99, steps - 1):
print(f"step {step + 1:4d} loss = {loss_of(V, U):.4f}")
print("训练后 S =")
print(normalize(V) @ normalize(U).T)输出:
step 1 loss = 0.0175
step 5 loss = 0.0044
step 25 loss = 0.0010
step 100 loss = 0.0003
step 300 loss = 0.0002
训练后 S =
[[ 0.9 -0.099 -0.096 -0.096]
[-0.099 0.9 -0.096 -0.096]
[-0.083 -0.083 0.932 0.029]
[-0.083 -0.083 0.029 0.932]]损失从
练习题
- 对
,导数是 。在 处,沿负梯度方向参数会变大还是变小? - 学习率过大会出现什么现象?把
分别取 、 、 、 ,从 出发迭代五次,观察 。 - ReLU 的输入为
时,它的导数是多少?
拷打 AI
- 损失对哪个参数求导?梯度的形状与参数的形状是否一致? 及格标准:每个参数对应一个偏导,梯度与参数张量形状相同。若声称梯度是标量,追问维度。
- 梯度为
是否意味着找到了最小值? 及格标准:不意味着。梯度为 只说明是驻点,可能是极大值或鞍点,在非凸问题上尤其如此。 - 论文说用梯度下降训练,能否推出一定收敛? 及格标准:不能。收敛还需要光滑性、合适的学习率以及问题本身的性质。
- 反向传播经过了哪些中间量? 及格标准:从损失往回依次经过负对数、softmax、温度缩放、余弦相似度、投影、编码器。
参考答案
- 导数为
,梯度为负,沿负梯度方向即正方向移动,参数变大,趋向 。 - 学习率过小收敛缓慢;过大则越过最优点来回振荡,甚至发散。参考数值:
五步后 (损失 ), 五步后恰好落到 (损失 ), 五步后到 (损失 ), 发散到 (损失 )。 - 输入为负时 ReLU 输出恒为
,导数为 。这类单元在后续更新中难以恢复,通常称为不活跃或失活的 ReLU。
暂时不需要掌握
- 极限的
- 定义、一致收敛,见 5.3 函数的极限、连续与初等函数 - 中值定理与泰勒展开的完整理论,见 5.4 一元微分学与局部近似
- 最优性条件、下降引理与收敛速率,见 3.3 无约束优化的最优性条件与梯度法
- 动量、自适应步长与 Adam 的完整推导,见 3.4 一阶方法的进展
常见错误
错误 1 · 梯度为 0 就认为训练完成
原因:在非凸问题上,梯度为 0 的位置可能是鞍点或局部极大值。 解决:结合损失值与多次随机初始化判断,不要只看梯度是否归零。
错误 2 · 链式法则漏乘某一级
原因:复合层数多时容易漏掉中间某一层的局部导数。 解决:用中心差分做数值梯度检验,与解析梯度对照。
错误 3 · 用固定的
原因:差分近似的误差随函数尺度、步长
错误 4 · 把损失里出现 NaN 一律归因于学习率过大
原因:NaN 的来源有多种。 解决:依次排查