「CS231n:计算机视觉与深度学习」学习笔记(Lec 02 - Lec 04)

本篇为《CS231n: Deep Learning for Computer Vision》课程的学习笔记,课程版本为 Spring 2025。该篇笔记涵盖了 Lec 02 - Lec 04。

主要参考了 CS231n 官网B 站课程视频,相关代码和作业实现可以参考 个人完成记录

前置说明:基础知识可参考 《PyTorch深度学习实践》。本文默认读者已掌握该课程的基础常识,故不再进行赘述。

这一系列笔记为个人随手记录,且是直接从 Obsidian 强行复制过来的,不保证排版和可读性()


导航链接:


Lecture 2: Image Classification with Linear Classifiers

kNN

一种比较 trivial 的图像分类方法,通过定义图像之间的距离,并对于 test 集中的每一张图像,找出 train 集中距离前 \(k\) 小的图像,取其中出现次数最多的类别作为答案。 比较显而易见的,该方法的泛化能力较差。

常见的距离有下列两种:

  • L1 距离: \[ d_{1}(I_{1}, I_{2}) = \sum\limits_{p} \left| I^{p}_{1} - I^{p}_{2} \right|. \]
  • L2 距离: \[ d_{2}(I_{1}, I_{2}) = \sqrt{ \sum_{p} \left( I^{p}_{1} - I^{p}_{2} \right)^{2} }. \]

交叉验证

一般来说,会把数据集分为训练集(train)、验证集(validation)和测试集(test),一般来说,使用训练集进行训练,验证集进行调参(调整超参数等),并在调参完成后,使用测试集进行效果测试。 需要注意,不可用测试集进行调参,否则会影响模型的泛化能力,或也可认为结果在 cheating。所以说,只可在最后使用测试集对模型进行评估,且只可使用一次。

在数据集较小时,为避免验证集无法反映全体数据,可采用交叉验证。即将训练集分段(fold),假设分为 \(n\) 段,然后让每个 fold 分别当一次验证集,其余的 fold 合并起来当训练集,如此执行 \(n\) 次数,让验证集覆盖所有的 fold,最后求 accuracy 的平均数即可。


Lecture 3: Regularization and Optimization

正则化

对于损失函数 (loss function),如果不同的模型可以得到相同的结果(例如以 \(f(x_{i}, W) = Wx_{i}\) 为例, 在 \(L = 0\) 时,\(W\) 可能不唯一),于是我们需要对其进行正则化 (regularization) 取得相对来说更简单的 \(W\)(一般来说,更简单的模型泛化能力更强)。

于是我们可以在损失函数中添加惩罚项 \(R(W)\) 来实现这一点,例如使用 L2 范数控制权重: \[ R(W) = \sum\limits_{k} \sum\limits_{l} W_{k, l}^{2} \] 也可用 L1 范数,具体用哪个看问题需求(一般来说,L2 范数更常用): \[ R(W) = \sum\limits_{k} \sum\limits_{l} |W_{k, l}| \]

自此,我们可得到完整的损失函数: \[ L = \underbrace{ \frac{1}{N} \sum\limits_{i} L_{i} }_{ \text{data loss} } + \underbrace{ \lambda R(W) }_{ \text{regularization loss} } \] 其中 \(\lambda\) 为超参数,需要通过验证集确定。

优化

随机梯度下降(SGD)

\[ x_{t + 1} = x_{t} - \alpha \nabla f(x_{t}) \]

一般会使用 minibatch,大小通常为 32 / 64 / 128

SGD + Momentum

\[ \begin{align*} v_{t + 1} &= \rho v_{t} + \nabla f(x_{t}) \\ x_{t + 1} &= x_{t} - \alpha v_{t + 1} \end{align*} \]

增加了速度一项,可避免陷入局部最小值(有惯性可以冲出来),其中摩擦系数(friction,\(\rho\))通常设为 \(0.9\)\(0.99\)

SGD + Momentum 有不同的表达形式,例如: \[ \begin{align*} v_{t + 1} &= \rho v_{t} - \alpha \nabla f(x_{t}) \\ x_{t + 1} &= x_{t} - v_{t + 1} \end{align*} \] 这几种表达形式实际上是等价的。

RMSProp

为了加速梯度下降过程,并减小垂直于目标方向的剧烈梯度的副作用,可以考虑对学习率进行自适应地逐项缩放。

1
2
3
4
5
grad_squared = 0
while True:
dx = compute_gradient(x)
grad_squared = decay_rate * grad_squared + (1 - decay_rate) * dx * dx
x -= learning_rate * dx / (np.sqrt(grad_squared) + 1e-7)

由此,可使其在梯度“陡峭”更新减缓,在梯度“平缓”时更新加剧。

Adam

实际上就是 Momentum 和 RMSProp 的结合。

1
2
3
4
5
6
7
first_moment = 0
second_moment = 0
while True:
dx = compute_gradient(x)
first_moment = beta1 * first_moment + (1 - beta1) * dx // Momentum
second_moment = beta2 * second_moment + (1 - beta2) * dx * dx // RMSProp
x -= learning_rate * first_moment / (np.sqrt(second_moment) + 1e-7)

不过实际上,由于一般来说 beta2 取值会接近 \(1\),第一次计算 second_moment 时该值很可能会很小,从而导致第一步的步长极大。

为解决该问题,可添加偏差项:

1
2
3
4
5
6
7
8
9
first_moment = 0
second_moment = 0
for t in range(1, num_iterations):
dx = compute_gradient(x)
first_moment = beta1 * first_moment + (1 - beta1) * dx
second_moment = beta2 * second_moment + (1 - beta2) * dx * dx
first_unbias = first_moment / (1 - beta1 ** t)
second_unbias = second_moment / (1 - beta2 ** t)
x -= learning_rate * first_unbias / (np.sqrt(second_unbias) + 1e-7)

从经验来说,Adam 和 AdamW 基本是最常用的优化器,且对于绝大部分模型,可从默认值 beta1 = 0.9, beta2 = 0.999, learning rate = 1e-3 or 5e-4 开始

AdamW

和 Adam 的区别在于如何对待正则项(例如 L2),Adam 会在计算梯度时就加入 L2 项;而 AdamW 计算梯度时只会算 data loss,然后在更新权重时加入 L2 项。

AdamW 实际上意图为让动量取决于损失函数,与正则项独立。在大多数情况下,AdamW 表现更好。

学习率(Learning Rate,LR)

理想的学习率能快速降低 loss,但继续训练时仍能看到持续改进。较大的学习率可能在开始时 loss 下降更快,但无法收敛(可能在局部最小值徘徊),也有可能让 loss 变得非常大(可能在 loss landscape 震荡了);较小的学习率会使收敛速度变慢。

实际上,在整个训练过程中,不一定始终使用同一个学习率,比如有以下例方法,根据实际情况选用即可:

  • 经过固定次数迭代后,LR 降为 \(\frac{1}{10}\)(例如应用在 ResNet);
  • 余弦学习率衰减:\(\alpha_{t} = \frac{1}{2}\alpha_{0}\left( 1 + \cos\left( \frac{t\pi}{T} \right) \right)\)\(\alpha_{0}\) 为初始学习率,\(\alpha_{t}\) 为在 epoch \(t\) 时的学习率,\(T\) 为 epoch 个数),学习率呈现半余弦曲线(类似于 \(y = \cos x + 1\)\(\left[ 0, \pi \right]\) 的拉伸),可在训练中间阶段得到较好的效果;
  • 线性学习率衰减:\(\alpha_{t} = \alpha_{0}\left( 1 - \frac{t}{T} \right)\);倒数平方根方法:\(\frac{\alpha_{0}}{\sqrt{ t }}\)

可以使用一种叫做 线性预热 的策略:先用固定迭代次数预热,逐步线性提升到最大值,然后继续使用后续的调度器。

一个经验性的结论(线性缩放定律):若批量大小增大 \(n\) 倍,学习率也应增大 \(n\) 倍。

二阶优化方法

不仅可以利用梯度进行优化,也可以利用 Hessian 矩阵(即二阶导数)进行优化,此时可以用二次多项式代替梯度所带来的线性函数,加速寻找最小值的过程。

不过该方法并不常用,一方面该方法需要二阶可导,求解成本太高,且当参数较多时,Hessian 矩阵会过大,存储需要 \(O(n^{2})\),求逆则需 \(O(n^{3})\)。这也导致了该方法智能适用于小模型,且不太在意求解时间的时候。

在较为大型的模型中,与其求解 Hessian 矩阵,不如在训练时增强数据。


Lecture 4: Neural Networks and Backpropagation

激活函数

以下是常见的激活函数(activation functions):

  • ReLU:\(\max(0, x)\)
    • 基本可以说是最常用的激活函数,不过存在有时生成死神经元的问题;
  • Leaky ReLU:\(\max(0.1x, x)\)
    • 避免 ReLU 的死神经元问题;
  • ELU:\(\begin{cases} x & x \geq 0 \\ \alpha(e^{x} - 1) & x < 0 \end{cases}\)
    • 避免 ReLU 的死神经元问题,且是更好的零中心化函数(在零点可导),所以比 Leaky ReLU 更好;
  • GELU:\(x \cdot \phi(x)\)
    • 高斯误差线性单元,同样是 ReLU 的变种,常用于新架构(如 Transformer);
  • SiLU:\(f(x) = x \cdot \sigma(x)\)
    • S 形加权线性单元,即 Sigmoid 型线性单元,常用于一些现代 CNN 架构;
  • Sigmoid:\(\sigma(x) = \frac{1}{1 + e^{-x}}\)
  • Tanh:\(\tanh(x) = \frac{e^{x} - e^{-x}}{e^{x} + e^{-x}}\)

对于 Sigmoid 和 Tanh,存在将值压缩到狭窄范围的问题,有时会导致梯度消失,因此通常不在神经网络中间层适用该两种激活函数,而常用于后期层,例如需要二进制输出等场景。

选择激活函数是经验性的。在通常情况下,可使用 ReLU 作为默认选择(或采用特定架构常用的激活函数)。

2 层神经网络

通常情况下,更多的神经元意味着更强的学习复杂函数的能力,但过多的神经元(即赋予网络大量容量)会导致过拟合问题。

有一个经验法则,不要将网络规模作为正则化手段,我们通常不将其作为超参数进行精细调整(过大地增加网络复杂度可能会导致问题)。 通常会使用比实际稍大一点的网络(从小网络逐步增大,直到出现一定程度的过拟合),然后使用正则器,调整超参数 \(\lambda\) 来减小过拟合。即通常调整的是正则化和正则化超参数,而不一定是网络本身的大小。