CS231n Softmax 分类器与交叉熵梯度(1)
Softmax 分类器与交叉熵梯度
[!abstract] 这篇笔记整理 CS231n assignment1 中 Softmax loss 的前向计算、交叉熵含义、梯度推导和 NumPy 实现。相关课程索引见 [[CS231n]]。
变量形状
| 符号 | 形状 | 含义 |
|---|---|---|
| X | N \times D | 一个 minibatch 的输入数据 |
| x_i | 1 \times D | 第 i 个样本 |
| W | D \times C | 线性分类器权重 |
| y_i | 标量 | 第 i 个样本的正确类别 |
| S | N \times C | 所有样本在所有类别上的 score |
| P | N \times C | softmax 后的类别概率 |
其中 $N$ 是样本数,$D$ 是特征维度,$C$ 是类别数。CIFAR-10 中 $C=10$。
前向计算
对单个样本 $x_i$,线性分类器先计算每个类别的分数:
$$
s_i = x_i W
$$
第 $j$ 个类别的分数是:
$$
s_{ij} = x_i W_{:,j}
$$
Softmax 把分数转成概率:
$$
p_{ij} = \frac{\exp(s_{ij})}{\sum_{k=1}^{C}\exp(s_{ik})}
$$
代码里会先做数值稳定处理:
$$
s_i \leftarrow s_i - \max_j s_{ij}
$$
这是合法的,因为对任意常数 $a$:
$$
\frac{\exp(s_{ij} - a)}{\sum_{k=1}^{C}\exp(s_{ik} - a)} = \frac{\exp(s_{ij})}{\sum_{k=1}^{C}\exp(s_{ik})}
$$
也就是说,整体平移 scores 不会改变 softmax 的概率。
Loss 的含义
第 $i$ 个样本的 Softmax 交叉熵 loss 是正确类别概率的负对数:
$$
L_i = -\log p_{i,y_i}
$$
展开可得:
$$
L_i = -\log \frac{\exp(s_{i,y_i})}{\sum_{k=1}^{C}\exp(s_{ik})}
$$
进一步化简:
$$
L_i = -s_{i,y_i} + \log \sum_{k=1}^{C}\exp(s_{ik})
$$
这个形式很适合推导梯度。
[!tip] 初始 loss 为什么接近 $-\log(0.1)$ 初始化时 $W$ 很小,各类别 score 接近,softmax 后每类概率大约是 $\frac{1}{10}$。因此 CIFAR-10 的初始单样本 loss 约为:
$$
-\log \frac{1}{10} = \log 10 \approx 2.3026
$$
对 score 的梯度
从化简后的单样本 loss 出发:
$$
L_i = -s_{i,y_i} + \log \sum_{k=1}^{C}\exp(s_{ik})
$$
对某个类别分数 $s_{ij}$ 求导:
$$
\frac{\partial L_i}{\partial s_{ij}} = \frac{\partial}{\partial s_{ij}} \left( -s_{i,y_i} + \log \sum_{k=1}^{C}\exp(s_{ik}) \right)
$$
第一项给出:
$$
\frac{\partial (-s_{i,y_i})}{\partial s_{ij}} = \begin{cases} -1, & j = y_i \ 0, & j \ne y_i \end{cases}
$$
第二项给出:
$$
\frac{\partial}{\partial s_{ij}} \log \sum_{k=1}^{C}\exp(s_{ik}) = \frac{\exp(s_{ij})}{\sum_{k=1}^{C}\exp(s_{ik})} = p_{ij}
$$
所以:
$$
\frac{\partial L_i}{\partial s_{ij}} = p_{ij} - \mathbf{1}(j = y_i)
$$
等价地:
$$
\frac{\partial L_i}{\partial s_{ij}} = \begin{cases} p_{ij} - 1, & j = y_i \ p_{ij}, & j \ne y_i \end{cases}
$$
对权重 $W$ 的梯度
因为:
$$
s_{ij} = \sum_{d=1}^{D} x_{id} W_{dj}
$$
所以:
$$
\frac{\partial s_{ij}}{\partial W_{dj}} = x_{id}
$$
链式法则得到逐元素梯度:
$$
\frac{\partial L_i}{\partial W_{dj}} = x_{id}\left(p_{ij} - \mathbf{1}(j = y_i)\right)
$$
写成列向量形式:
$$
\nabla_{W_{:,j}} L_i = x_i^\top\left(p_{ij} - \mathbf{1}(j = y_i)\right)
$$
如果令 $Y$ 是 one-hot 标签矩阵:
$$
Y_{ij}=\mathbf{1}(j=y_i)
$$
则整个 batch 的平均 loss 为:
$$
L = -\frac{1}{N}\sum_{i=1}^{N}\log P_{i,y_i} + \lambda \sum_{d=1}^{D}\sum_{j=1}^{C} W_{dj}^{2}
$$
对应梯度是:
$$
\nabla_W L = \frac{1}{N}X^\top(P-Y) + 2\lambda W
$$
[!warning] 正则化系数 CS231n 这里的 loss 写成 $\lambda \sum W^2$,所以正则项梯度是 $2\lambda W$。如果 loss 写成 $\frac{1}{2}\lambda \sum W^2$,梯度才是 $\lambda W$。
Naive 实现
在 softmax_loss_naive 的循环里,已经得到单个样本的概率向量 p 后,可以直接构造:
循环结束后做平均并加正则项:
这里 np.outer(X[i], dscores) 的形状是:
$$
D \times C
$$
正好和 $W$ 的形状一致。
向量化实现
向量化代码对应公式:
$$
S = XW
$$
$$
P = \operatorname{softmax}(S)
$$
$$
\nabla_W L = \frac{1}{N}X^\top(P-Y) + 2\lambda W
$$
LinearClassifier 中的 SGD
LinearClassifier 是线性分类器的基类,负责通用训练流程;具体使用 SVM loss 还是 Softmax cross-entropy loss 由子类决定:
训练时每一步不是使用全部训练集,而是随机采样一个 minibatch:
其中有放回采样允许同一个样本在一个 batch 中出现多次。这样每一步得到的是整体梯度的随机近似,计算更快。
权重更新是标准梯度下降:
$$
W \leftarrow W - \eta \nabla_W L
$$
代码对应:
这里 grad 与 self.W 形状相同。若 $W \in \mathbb{R}^{D \times C}$,则:
$$
grad \in \mathbb{R}^{D \times C}
$$
矩阵写法等价于逐元素更新:
$$
W_{dj} \leftarrow W_{dj} - \eta \frac{\partial L}{\partial W_{dj}}
$$
[!warning] 更新公式的常见错误
self.W = learning_rate * grad会直接用梯度覆盖权重,训练会退化到接近随机猜测。正确写法必须保留旧权重:
> self.W -= learning_rate * grad > ``` ## 权重矩阵 的含义 在线性分类器中: 如果 ,,则 。 的每一列对应一个类别的线性模板: 第 类的分数为: 因此预测时取分数最高的类别: ```python scores = X.dot(self.W) y_pred = np.argmax(scores, axis=1)
对于 CIFAR-10,若使用 bias trick,通常有:
$$
D = 32 \times 32 \times 3 + 1 = 3073,\quad C=10
$$
所以:
$$
W \in \mathbb{R}^{3073 \times 10}
$$
正则化如何进入 loss 与梯度
总 loss 包含数据损失和 L2 正则化项:
$$
L = L_{\text{data}} + \lambda \sum_{d=1}^{D}\sum_{j=1}^{C}W_{dj}^{2}
$$
其中 $L_{\text{data}}$ 是模型在训练样本上的分类损失。对 Softmax 来说:
$$
L_{\text{data}} = -\frac{1}{N}\sum_{i=1}^{N}\log \frac{\exp(s_{i,y_i})}{\sum_{j=1}^{C}\exp(s_{ij})}
$$
正则项的梯度为:
$$
\frac{\partial}{\partial W} \left( \lambda \sum_{d=1}^{D}\sum_{j=1}^{C}W_{dj}^{2} \right) = 2\lambda W
$$
所以代码中正则化同时体现在 loss 和梯度:
超参数搜索与验证集
learning_rate 控制每次沿负梯度方向走多大一步,reg 控制权重惩罚强度。CS231n 中通常用验证集选择这两个超参数:
选择依据应是验证集准确率 val_accuracy,不是训练集准确率。训练集准确率高但验证集准确率低,通常意味着过拟合或超参数不合适。
权重可视化
Softmax 分类器学到的每个类别权重 $w_c$ 可以 reshape 成 $32 \times 32 \times 3$ 的图像。它通常看起来像一个模糊的类别模板,而不是清晰图片。
这是因为分类分数是点积:
$$
s_c = x^\top w_c
$$
若输入图像与某个类别的权重模板匹配,该类别分数就会更高。权重图可能带有类别常见背景或颜色,例如飞机与船可能出现蓝色区域,动物类别可能出现绿色或棕色区域。
这些模板模糊的原因是线性分类器每个类别只有一个模板,无法表达同一类别中姿态、位置、背景和外观的复杂变化。
易错点
scores -= np.max(scores)只是数值稳定处理,不改变 softmax 结果。softmax.py里 “normalized hinge loss” 的注释不准确,这里实际是 Softmax cross-entropy loss。- 正确类别处需要执行
dscores[y[i]] -= 1,因为 $\frac{\partial L_i}{\partial s_{ij}} = p_{ij} - \mathbf{1}(j=y_i)$。 - 正则化 loss 使用
reg * np.sum(W * W)时,梯度必须加2 * reg * W。 LinearClassifier.train()中要写self.W -= learning_rate * grad,不要把W覆盖成learning_rate * grad。- notebook 修改外部
.py后若表现仍像旧代码,应重启 kernel 或用importlib.reload确认实际加载的文件。
本地材料
- Notebook:
/Users/keynary/Code/course/CS231n/assignment1/softmax_zh.ipynb - Softmax loss:
/Users/keynary/Code/course/CS231n/assignment1/cs231n/classifiers/softmax.py - 线性分类器训练框架:
/Users/keynary/Code/course/CS231n/assignment1/cs231n/classifiers/linear_classifier.py
关联
- [[CS231n]]
- [[交叉熵]]
- [[梯度下降]]
- [[线性分类器]]Softmax 分类器与交叉熵梯度