「CS231n:计算机视觉与深度学习」学习笔记(Lec 02 - Lec 04)
本篇为《CS231n: Deep Learning for Computer Vision》课程的学习笔记,课程版本为 Spring 2025。该篇笔记涵盖了 Lec 02 - Lec 04。
主要参考了 CS231n 官网 和 B 站课程视频,相关代码和作业实现可以参考 个人完成记录。
前置说明:基础知识可参考 《PyTorch深度学习实践》。本文默认读者已掌握该课程的基础常识,故不再进行赘述。
这一系列笔记为个人随手记录,且是直接从 Obsidian
强行复制过来的,不保证排版和可读性()。
导航链接:
- Lec 02 - Lec 04:线性分类器进行图像分类,正则化与优化,神经网络与反向传播
- Lec 05 - Lec 11:基于 CNN 的图像分类,CNN 架构,循环神经网络,注意力机制与 Transformer,目标检测、图像分割与可视化,视频理解,大规模分布式训练
- Lec 12 - Lec 17:自监督学习,生成模型(一),生成模型(二),三维视觉,视觉与语言,机器人学习
- Assignments:作业合集
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 | grad_squared = 0 |
由此,可使其在梯度“陡峭”更新减缓,在梯度“平缓”时更新加剧。
Adam
实际上就是 Momentum 和 RMSProp 的结合。
1 | first_moment = 0 |
不过实际上,由于一般来说 beta2 取值会接近 \(1\),第一次计算 second_moment
时该值很可能会很小,从而导致第一步的步长极大。
为解决该问题,可添加偏差项:
1 | first_moment = 0 |
从经验来说,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\) 来减小过拟合。即通常调整的是正则化和正则化超参数,而不一定是网络本身的大小。