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

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

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

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

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


导航链接:


Lecture 5: Image Classification with CNNs

线性分类器的问题

从视觉视角上,权重矩阵实则对应着图像模板,即每一行对应一个固定的模板。 从几何视角上看,即为超平面划分空间。

图像特征

见 Assignment 1。 不过该方法大多已过时,已经是十多年前的成就了。

  • 颜色直方图(Color Histogram):关注图像的颜色分布,即统计每种颜色在像素中出现的次数;但该方法破坏了图像所有的空间结构;
  • 方向梯度直方图(HoG):忽略颜色信息,只关注图像的结构信息,关注图中每个点的方向,以及该局部区域周围图像中边缘的局部方向。

通常对一张图,会提取多种图像特征,并组合在一起使用(即将其连接为一个大的表示特征的向量)。

图像特征由人为规定,很难说出哪些特征重要,哪些特征不重要,且很难写出完美的特征提取器,这就体现出了深度学习从数据中自主学习的优势。

相较于设计特征提取器关注图像特征,设计神经网络则需关注网络架构。

神经网络

如 2 层神经网络的一个问题在于其破坏了图像的空间结构(即纯粹地展平为一维向量)。 以此衍生,设计神经网络架构时如何进行网络以及计算原语(computational primitive)的设计以更好地尊重原本数据的原有特征(如图像的二维结构等等)。

卷积神经网络

即包含多个卷积层堆叠起来的神经网络。

需要注意,卷积层是线性的(卷积层同样可以看作是点积),所以在卷积层后加入激活函数是必要的。

类似于线性分类器的学习结果可以可视化为图像模板,卷积层的学习结果则可视化为对于图像的小子块的空间结构特征提取模板(例如若用第一层卷积层举例,则可能包含识别对立颜色,识别轮廓等)。

显而易见地,使用多个堆叠的卷积层时,会放大有效感受野(effective reception field,即多少个原始图像的像素会影响下游网络的一个元素)(感受野(reception field)则说的是上一层对下一层的影响),从而达到更好的效果。

有时为了扩大有效感受野,可引入步幅(stride),即让卷积核每次移动步幅个像素,而不是每次移动一个像素,此时卷积层得到的结果维度会变小(除以步幅),此时有效感受野可指数型增长。

池化层

卷积层核池化层都是神经网络有效的进行内部下采样(downsample,即压低数据或特征空间的分辨率或采样率)的方法。 下采样十分有用,是能在网络中更快地建立感受野的方法。

相比于卷积层需要大量计算,池化层则所需的计算量很小。

池化层所用的下采样机制有多种选择,最常用的为最大池化层。

相交于卷积层可选用填充(padding),池化层通常不会使用填充(例如你在最大池化层后加了 ReLU,那填充完全多余)。 步幅为超参数,但通常不对其进行过多的调整(做池化是为了下采样,最常见就是 \(2 \times 2\) 步幅 \(2\),有时也会有 \(4 \times 4\) 步幅 \(2\);不过绝大多数情况都是为了将采样率降低 \(2\) 倍)。

通常不会接受不同尺寸图像的输入,若有则会事先进行图像调整(例如进行填充),或针对不同长宽比的图像独立地运行神经网络(即做纵横比分组,即同一批次的图像纵横比相同,但对整个训练可能会有不同纵横比的图像;该方法可见于一些更大型的系统)。

池化层可能是非线性的,例如最大池化层是非线性的,所以在一些使用了最大池化层的 CNN 中就不会在卷积周围使用 ReLU,但平均池就是线性的。

平移等变性(translation equivariance)

卷积层核池化层都满足平移等变性,即若先对图像进行操作再进行平移,和先对图像进行平移再进行操作所得到的结果相同。

平移等变性反映了其尊重图像的二维空间结构,同时也说明了关于图像的一个重要直觉:从图像提取的特征应只取决于图像的内容,而不是图像中的绝对位置。


Lecture 6: CNN Architectures

构建 CNN

归一化层(Normalization Layers)

例如 LayerNorm 层,其学习可用于对输入数据进行缩放 / 平移的参数,并进行如下操作:

  1. 对输入数据进行归一化(均值位 \(0\),标准差为 \(1\));
  2. 使用学习到的参数对数据进行缩放 / 平移。

所有的归一化层都会采用相同的方法,区别在于如何计算统计量(例如均值和方差)。其中,LayerNorm 最常用(尤其在 Transformer 当中)。

LayerNorm 对于数据 \(x \in \mathbb{R}^{N \times D}\),对每个样本分别计算均值和标准差 \(\mu, \sigma \in \mathbb{R}^{N \times 1}\),我们同时有已学习的均值和标准差的参数 \(\gamma, \beta \in \mathbb{R}^{1 \times D}\),则会有该公式: \[ y = \frac{\gamma (x - \mu)}{\sigma} + \beta. \]

归一化也有其他方法,例如:

  • Batch Norm:对每个通道分别计算均值和标准差,对批次中的所有数据取平均;
  • Layer Norm:对每个样本的所有特征计算均值和标准差;
  • Instance Norm:每个样本、每个通道单独计算均值和标准差;
  • Group Norm:把通道分成 \(G\) 组,然后对于每个样本,在每组内计算均值和标准差。

Dropout

Dropout 是一种在 CNN 中的正则化层,其旨在训练时添加随机性,而在测试时移除,从而让模型难以过拟合训练数据,增强模型的泛化能力。故其为有效的正则化方法。

具体地,Dropout 会在正向传播时以固定地概率随机地将该层地某些输出或激活值归零。 Dropout 只有一个超参数,即为值被归零的概率。其中 \(0.5\) 最为常用,\(0.25\) 也比较常见。

对于归零的神经元,其不需要参与计算,故可用掩码进行处理或利用掩码置零。

Dropout 更多的是一种经验性的结论。从直觉来讲,由于某些值会被随机丢弃,Dropout 会迫使网络学习冗余的表征,使得模型不会过度依赖某些特征的存在或特征的共适应关系(例如如果看到爪子和耳朵就认为必定是猫),而需要学习更广泛的特征与输出的对应关系。

在测试阶段,Dropout 则不会丢弃任何值,所以模型会在测试阶段表现更好。 此外,如果设置了 \(50\%\) 的丢失率,换句话来说在测试阶段则会较训练阶段多 \(50\%\) 的值,则需要在测试阶段对数据进行缩放,即若 Dropout 概率为 \(p\),则需对数据乘上 \(1 - p\),从而保证训练阶段和测试阶段中各层输入的幅度不变。

激活函数

对于 Sigmoid 函数 \(\sigma(x) = \frac{1}{1 + e^{-x}}\),其有问题,导致在现在该函数不常作为激活函数:由于在 \(x\) 极大或极小时,Sigmoid 变化极其平缓,导致在经过多层 Sigmoid 后,反向传播的梯度会越来越小(可能会导致梯度消失问题)。

而对 ReLU,其在正区域则没有该问题,且计算速度比 Sigmoid 快,使其成为了现在最常用的激活函数。但对于任意的负输入,其梯度为零,这导致了 ReLU 神经元死亡(Dead ReLUs)问题。

为解决 ReLU 神经元死亡问题,产生了例如 GELU 和 SILU 等新的激活函数,其均使图像在 \(x = 0\) 时仍保持平滑可导,有非零梯度,但在无穷远点仍收敛于 ReLU。

在 CNN 中,对于几乎所有的线性操作(前馈层 / 全连接层、卷积层)的后面,均会加上激活函数。

VGGNet

一种相当经典的 CNN 结构,不过其就是为 AlexNet 多添加了几层卷积层。其只涉及卷积层、最大池化层和全连接层。

在其中使用了连续三层的 \(3 \times 3\) conv,其有效感受野等效于一个 \(7 \times 7\) conv,但其参数量更小,且是一个更加“非线性”的更复杂的模型,故其比直接使用更大的 conv(例如 \(7 \times 7\) conv)更好。

ResNet

其包含一个直觉,就是深层网络包含浅层网络,所以说深层网络的 acc 比浅层网络低意味着深层网络更难优化,而不是深层网络比浅层网络差。 所以需要设计结构,使得深层网络至少有表达出浅层网络的能力,即需要构造能够较为轻松地模拟浅层网络的模型。于是可得到残差连接。

残差连接可以帮助建模更复杂的结构,从而同时使其能使用更多数据,故例如 Transformer 等大量模型均会使用该方法。而不加残差连接时,复杂模型容易陷入局部最小值困境,故而使其就算增加 epoch 也无法很好地收敛。

使用残差连接后,增加层数确实会提升训练效果,不过当层数达到一定程度后也会趋于收敛。

在具体实现中,ResNet 结构如下:

  • 其为若干个残差块的堆叠;
  • 每个残差块包含 \(2\)\(3 \times 3\) conv;
  • 周期性地,在若干模块后,将滤波器(卷积核)数量翻倍,并将空间维度减半(使用步长为 \(2\) 的卷积进行下采样);
  • 在网络开头加一个较大的卷积层(\(7 \times 7\) conv)(起始层,stem),这更多是一个经验性的发现。

权重初始化

在开始训练前,需要设置合适的初始值(例如 W = std * np.random.randn(Din, Dout))。当初始值过小时(如 std = 0.01),随着层数增加,数据的均值和标准差会越来越小;相应地,当初始值过大时(如 std = 0.05),随着层数增加,数据的均值和标准差会越来越大(而理想状态下,均值和方差需保持基本相同,以使其更易被优化)。

为解决该问题,在使用 ReLU 激活函数的情况下,可采用 Kaiming 初始化,其与维度有关,初始化为 W = np.random.randn(Din, Dout) * np.sqrt(2/Din),即 \(\text{std} = \sqrt{ \frac{2}{\text{Din}} }\)。 对于 CNN,初始化的 Din 即为卷积核乘通道的大小。

使用 LayerNorm 确实可以解决权重初始化问题(就算使用较差的初始化权重,也能有较为可观的效果),但若想得到最好的性能,仍需 Kaiming 初始化等良好的权重初始化。 此外,层归一化是否有用也取决于具体问题(例如需要精确坐标的数学题,层归一化可能反而会影响性能(其丢失了输入空间位置的精确信息))。

训练 CNN

数据预处理:图像归一化

  • 减去每个通道的均值,并除以每个通道的标准差;
  • 需提前基于你的数据集计算每个通道的均值和标准差(例如直接用 ImageNet 的均值和标准差预处理你的数据集,不过该操作依赖于你的数据集)。

正则化

可加入噪声从而协助正则化。

  • 训练阶段:引入随机性 \[ y = f_{W}(x, z); \]
  • 测试阶段:对随机性取平均(有时采用近似计算) \[ y = f(x) = E_{z}[f(x, z)] = \int p(z)f(x, z) \, \mathrm{d}z. \]

Dropout 就是一个很典型的例子。

数据增强(Data Augmentation)

在将图像输入模型前,会先进行数据增强,即对图像进行变换,使其与原图不同但仍可被类别识别。

由于一张图经过变换后可得到多张不一样的图,该方法可有效地扩大数据集,从而提升泛化能力;但由于不会看到重复的样本,模型更难进行死记硬背,即增加模型的记忆难度,故会提高 loss。

数据增强可采用以下方法(针对图像分类):

  • 水平翻转;
  • 调整大小和裁剪(例如进行随机裁剪后调整至目标尺寸,甚至进行二次裁剪);
    • 例如在 ResNet 中,先随机选择 \(L \in [256, 480]\),然后以 \(L\) 为短边对图像进行大小调整,再随机选择 \(224 \times 224\) patch 进行裁剪。该方法成为 随机大小裁剪(由于其效果很好,且保留了图像的相对分辨率,该方法应用广泛);
    • 另一种方法为 测试时增强(Test Time Augmentation):如果要追求最佳表现,可采取多个不同裁剪(不同的 \(L\)、不同的裁剪位置、翻转等),调整大小后运行模型,最后取预测结果的平均。通常来说,收益会逐渐递减,但通过该方法,可得到 \(1\% \sim 2\%\) 左右的不错的性能提升。故若需要极致的性能追求,这是对计算机视觉问题的通用且有效的方法。
  • 颜色抖动(Color Jitter):随机调整图像对比度和亮度;
  • Cutout:直接对图像中的一部分区域覆盖黑色或灰色框(或者直接设为 \(0\))(从直觉上讲类似于摄像头被部分遮挡),该方法并不算特别常用,不过是一种能让程序在遮挡情况下更鲁棒的技巧,所以说对特定场景下也需选择合适的数据增强方法

对于数据增强方法,可尝试不同参数,并观察哪些对图像的分类分布仍保持一致,或从人类来看比较正常,那这就是个好参数。 对于一个项目,可尝试多种不同的数据增强方法,观察哪些可让数据与原始数据不同,但仍可被人类较为容易地识别,那这就是一套较好地增强方法。

迁移学习(Transfer Learning)

对于 CNN 的最后一层(即最后的线性分类层)之前,当将执行得到的向量与数据中的其他该分类的图像比较,应发现这些图像对应的向量较为接近,故可视为对于 CNN 末端的向量的最邻近算法,故可以用 L2 距离进行处理。换句话来说,在此不仅可用线性层进行分类,也可用 kNN 进行分类。

当数据量较小时(且数据仅有 \(C\) 种类别),可拉取一个在大数据集上已经训练好的模型(例如在 ImageNet 上训练好的模型),并 冻结最后一层之前的所有层,替换最后一层为你的数据集的类别数量 \(C\)。在训练模型时,只训练最后一层。 从直觉上讲,冻结的层某种意义上可理解为“特征提取器”。

当拥有更大的数据集时,从经验来说,效果最好的通常是 训练整个模型,但初始化时使用超大规模数据集(如 ImageNet)预训练的参数

这种操作可称为在较小的数据集上 微调 模型,效果会比从头开始训练更好。 显而易见地,该种方法在原本数据集(如 ImageNet)相似的数据集上表现最佳,而对于差异较大的数据集上表现会大幅下降。

与原数据集高度相似 与原数据集差异很大
数据量极少 在模型的最后一层为使用线性分类器 尝试另外的模型,或收集更多数据
数据量充足 微调模型的所有层 微调模型的所有层,或直接从头开始训练

深度学习框架会提供一些包含已经预训练的模型的“模型库”(Model Zoo),以下为链接:

  • PyTorch: https://github.com/pytorch/vision
  • Huggingface: https://github.com/huggingface/pytorch-image-models

超参数选择

Step 1

  1. 检查初始损失值;
  2. 在少量样本上过拟合模型;
  3. 找到能让损失下降的学习率。

若模型训练困难,或初期无法正常运行,最好的方法(同时也是深度学习的默认调试策略)是 过拟合小样本。 此时只需要一个数据点,然后观察训练损失基本趋于零,此时模型应能记住单个训练样本。若无法做到该点,说明代码中存在错误,或没有选择适合该问题的模型类型。

该方法同样可以寻找合适的学习率。

Step 2

  1. 粗粒度超参网格搜索,训练约 \(1 \sim 5\) 个 epoch;
  2. 细化超参网格,延长训练时间;
  3. 观察损失与准确率曲线;
  4. 回到第 5 步。

接下来可尝试粗略的超参数网格,首先尝试并寻找能使 loss 持续下降的学习率,然后进一步调整其他的超参数。

对于学习率,沿用前一步验证过的模型结构,使用全部训练数据,开启小权重衰减,并找到一个能让 loss 在约 100 次迭代内显著下降的学习率即可。 常用于尝试的学习率:\(10^{-1}, 10^{-2}, 10^{-3}, 10^{-4}, 10^{-5}\)

除了 loss,也须注意 acc 曲线(包含 train acc 和 val acc):

  • 若两者均上升时,说明需要继续训练;
  • 若 train acc 上升,val acc 下降,说明发生了过拟合,故需要加入正则化或加入更多数据;
  • 若两者差距很小,说明欠拟合,故需要继续训练直到 train acc 开始偏离 val acc,以达到 val acc 的最高点。

从经验来说,在超参数空间种进行随机搜索的效果比在网格中使用预定的超参数集进行搜索好。 对于原因,可理解为对于重要的超参数,随机搜索可进行更充分的搜索,而在固定搜索中,时间浪费在了反复检查不重要的超参数的几个值。故应当定义要尝试的范围,然后随机搜索该范围内的超参数,直至得到最佳模型。

对于超参数选择,可以使用工具 Weights & Biases、TensorBoard 等等。


Lecture 7: Recurrent Neural Networks

(Vanilla) RNN

\[ \begin{cases} h_{t} = \tanh(W_{hh}h_{t - 1} + W_{xh}x_{t}) \\ y_{t} = W_{hy}h_{t} \end{cases} \] 其使用 \(\tanh\) 作为激活函数,其值位于 \([-1, 1]\),且为零中心化的,可以表示正副值。

对于 \(h_{0}\),可根据问题采用不同的策略进行初始化,也可以让模型学习它。

RNN 的计算图

  • 对于多对多(例如视频字幕生成),只需计算每个 \(y_{t}\) 的 loss 然后相加;
  • 对于多对一(例如视频分类),只需计算最后的 \(y\) 的 loss;
  • 对于一对多(例如生成图像的描述),只有 \(x_{1} = x\),需要填充 \(x_{t}\),根据情况可填入 \(0\) 或者 \(y_{t - 1}\)

直接处理梯度需要存下整张计算图的激活值和梯度,可能会导致 GPU 内存不足的问题。 在此可使用 截断时间反向传播(Truncated Backpropagation through time):从开头开始,每次只选择 RNN 中的一段进行处理,然后将最后得到的 \(h_{t}\) 当成下一段的 \(h_{0}\)(梯度不会向前一段传播),再进行同样的处理,以此类推。 对于多对一,由于只需要看隐层,且当前隐层只看前一个隐层和当前输入,所以从输出从后往前依次求梯度即可。同样地,可以从结尾开始,每次选择一段进行处理,然后将该段的开头的隐藏状态设为前一段的结束的后一个隐藏状态,此时前一段的最后一个隐藏状态只需查询这一段开头的一个隐藏状态的梯度(故可仅储存该隐藏状态的梯度),以此类推即可。 用该种方法,进行梯度计算则需要独立地分多次计算(取决于分的段数),而不是一次性全部计算。该技巧在分布式学习中也较为常见。

嵌入层(Embedding)

嵌入层是可学习的。我们一般倾向于分散权重,故通过将其初始化为非常小的接近零的值(例如使用类似于 Kaiming 初始化的方法),可使处理后的输入对优化更有利。

应用

对于语言生成模型,若每次都使用贪心解码(即采用最大概率的输出),则可能只会不断生成相同的内容,故一般会基于分布进行采样(即由 softmax 输出的概率分布来决定输出的概率)。 也可采用其他方式,例如束搜索(尝试不同选项,并选择序列整体概率最高的)。

神经元的可解释性

可通过查看 RNN 中的单个激活值,并简单地与输入进行对应,从而分析模型跟踪的内容。部分神经元的激活值的意义可能难以解释,但部分神经元也有明确的可追踪的特征且高度可解释(例如引号检测器、行长度追踪单元、条件语句单元、注释检测器、代码深度单元等等)。

RNN 的优劣

优势:

  • 可处理任意长度输入,无上下文长度限制;
  • 理论上可调用来自很早之前的时间步的信息;
  • 模型参数量不会随着序列增长而增加;
  • 所有时间步复用同一套权重,因此输入的处理逻辑具备权重对称性,

劣势:

  • 需要递归计算,故计算速度慢(尤其在训练阶段);
  • 实际上,由于隐藏状态大小固定,很难获取许久时间步之前的信息。

RNN 在 CV 中的应用

例如图像描述,先使用 CNN 或其他编码器对图片进行编码,并将其和之前生成的文本传入 RNN,只需输入一个起始字符 <START> 以开始序列,知道结束字符 <END> 出现时终止即可。

具体来说,类似于迁移学习,去除掉 CNN 最后的线性分类层,然后将其作为隐藏状态的输入 \(v\)。 在此,则需修改公式 \[ h = \tanh(W_{xh}x + W_{hh}h) \]\[ h = \tanh(W_{xh}x + W_{hh}h + W_{ih}v). \] 以加入视觉成分。

该种图像描述在部分场景下表现较好,但在一些物体分布情况下表现不佳(比如表现为认为场景中存在某个训练集中常见的物品的幻觉,而在实际图像中并不存在),这些大多由数据集偏差导致。

另一个应用为视觉问答任务。一种方案为给问题说回答,然后看回答得怎么样;另一种方案为生成文本,然后看选项中每个词出现的概率,再将概率相乘得到整体答案的概率。 更为常见的方案则是将问题和答案都输入模型,然后输出每个选项的概率,即做分类问题。

另外的应用有:视觉对话任务、视觉导航任务等等。

多层 RNN

类似于 CNN 或者全连接层,RNN 也可对隐层进行深度的堆叠,此时隐藏状态则会形成一个网格。 此时计算最终隐藏状态则需计算整个网格的值,可见在训练中该模型过程非常复杂,且效率不高。

LSTM

对 Vanilla RNN 进行变换: \[ h_{t} = \tanh(W_{hh}h_{t - 1} + W_{xh}x_{t}) = \tanh\left( \begin{pmatrix} W_{hh} & W_{hx} \end{pmatrix} \begin{pmatrix} h_{t - 1} \\ x_{t} \end{pmatrix} \right) = \tanh\left( W \begin{pmatrix} h_{t - 1} \\ x_{t} \end{pmatrix} \right). \] 则有: \[ \frac{ \partial h_{t} }{ \partial h_{t - 1} } = \tanh'(W_{hh}h_{t - 1} + W_{xh}x_{t})W_{hh}. \]

若对每个时间步均计算损失,则有: \[ \frac{ \partial L }{ \partial W } = \sum\limits_{t=1}^{T} \frac{ \partial L_{t} }{ \partial W }. \] 且有: \[ \frac{ \partial L_{T} }{ \partial W } = \frac{ \partial L_{T} }{ \partial h_{T} } \frac{ \partial h_{T} }{ \partial h_{T - 1} } \cdots \frac{ \partial h_{1} }{ \partial W } = \frac{ \partial L_{T} }{ \partial h_{T} } \left( \prod\limits_{t=2}^{T} \frac{ \partial h_{t} }{ \partial h_{t - 1} } \right) \frac{ \partial h_{1} }{ \partial W }. \] 即: \[ \frac{ \partial L_{T} }{ \partial W } = \frac{ \partial L_{T} }{ \partial h_{T} } \left( \prod\limits_{t=2}^{T} \tanh'(W_{hh}h_{t - 1} + W_{xh}x_{t}) \right) W_{hh}^{T - 1} \frac{ \partial h_{1} }{ \partial W }. \] 而对于 \(\frac{\mathrm{d}}{\mathrm{d}x} \tanh(x)\),其值始终位于 \([0, 1]\),且只有 \(x = 0\) 时取 \(1\),故可能导致梯度消失。

但就算不采用激活函数(或采用不会发生梯度消失的激活函数),有: \[ \frac{ \partial L_{T} }{ \partial W } = \frac{ \partial L_{T} }{ \partial h_{T} } W_{hh}^{T - 1} \frac{ \partial h_{1} }{ \partial W }. \] 其中有项 \(W_{hh}^{T - 1}\),故考虑 \(W_{hh}\),若其最大奇异值大于 \(1\) 则可能导致梯度爆炸,若其最大奇异值小于 \(1\) 则可能导致梯度消失。 对于梯度爆炸,可对梯度进行缩放而缓解(例如设上限为 \(M\),若梯度 \(\text{grad}\)\(\|\text{grad}\|_{2}^{2} > M\),则只需 \(\text{grad} \gets \text{grad} \cdot \frac{M}{\|\text{grad}\|_{2}^{2}}\) 即可),而对于梯度消失则需要修改 RNN 的结构了(这也是 Vanilla RNN 不常用的原因)。

使用 LSTM 可解决该问题,其公式为: \[ \begin{align*} \begin{pmatrix} i \\ f \\ o \\ g \end{pmatrix} &= \begin{pmatrix} \sigma \\ \sigma \\ \sigma \\ \tanh \end{pmatrix} W \begin{pmatrix} h_{t - 1} \\ x_{t} \end{pmatrix} \\ c_{t} &= f \odot c_{t - 1} + i \odot g \\ h_{t} &= o \odot \tanh(c_{t}) \end{align*} \] 其使用了四种门控来追踪不同值,而不是单一的隐藏状态,并使用这些值来调整隐藏状态,并确定通过不同路径传递的信息(有 hidden state(\(h_{t}\))和 cell state(\(c_{t}\)))。

  • \(i\):输入门(Input gate),决定是否向 cell 写入信息;
  • \(f\):遗忘门(Forget gate),决定遗忘多少先前时间步的信息;
  • \(o\):输出门(Output gate),决定输出到隐藏状态的比例;
  • \(g\):候选门(Gate gate),决定向 cell 写入的隐藏状态。

其中 \(h_{t}\) 是正常的隐藏状态,而 \(c_{t}\) 则像是一条高速公路,其可避免激活函数,故避免了梯度消失,且反向传播至 \(c_{t - 1}\) 时只与 \(f\) 进行了 Hadamard 积运算,故可更容易地传递更长期的信息。该方法在实践中效果显著。

LSTM 相比 RNN 更容易保留跨多个时间步的信息(例如若 \(f = 1, i = 0\),则 cell 中的信息会永久保存,但 Vanilla RNN 很难学到能保留长时间信息的 \(W_{h}\))。 LSTM 并不保证完全解决梯度消失或梯度爆炸的问题,不过其也提供了一种更加简便的学习长距离依赖关系的方式。

此外,LSTM 添加的新的输出 \(c_{t}\) 跳过了一些激活函数和层,这与 ResNet 有些许类似(序列的长度与模型的深度相对比)。

现代 RNN

相比于 Transformer,RNN 优势在于其有无限的上下文长度。且在训练和推理时,计算量随序列长度线性增长(例如 RWKV 和 Mamba 均在强调线性时间建模序列),而 Transformer 为平方级。


Lecture 8: Attention and Transformers

Sequence to Sequence with RNNs: Encoder - Decoder

例如在翻译问题(例如英语翻译为意大利语)中,其处理较为复杂(英语和意大利语的单词没有直接的关系,且输入序列和输出序列可能不等长等),可考虑使用编码器(encoder)解码器(decoder)进行处理。

先将待翻译的内容输入编码器。编码器可用 RNN,其输入为英语文本 \(x_{t}\) 和隐藏状态 \(h_{t - 1}\),得到新的隐藏状态 \(h_{t}\),以此处理可变长序列。计算完成后,编码器会汇总结果,并生成一个总结输入句子内容的向量 \(c\),称为 上下文向量(context vector)(有多种生成方法,例如可直接取最后一个隐藏状态 \(h_{T}\))。

接下来需要将 \(c\) 翻译成目标语言的序列,故需要解码器。解码器也可用 RNN 实现(通常有相同的的架构,但权重矩阵和参数不同),每个单元的输入有前一时间步的输出 \(y_{t - 1}\)、前一个隐藏状态 \(s_{t - 1}\) 和上下文向量 \(c\),处理并输出当前的隐藏状态 \(s_{t}\) 并生成输出 \(y_{t}\)。最后只需整合输出 \(y_{t}\) 即为结果。

可见编码器和解码器只可用 \(c\) 这一固定长度的向量进行通信。故当翻译序列过长时,其存储的信息则完全不够。 故需要修改网络架构,使其不限制于一个固定长度的向量。我们希望能在处理输出序列时,能回看整个输入序列。

Sequence to Sequence with RNNs and Attention

设编码器仍为 RNN,且解码器的初始隐藏状态为 \(s_{0}\)。得到解码器的隐藏状态后,需回看输入序列,计算 对齐分数(alignment scores)(其为标量): \[ e_{t, i} = f_{\text{att}}(s_{t - 1}, h_{i}). \] 其中 \(f_{\text{att}}\) 是一个线性层,表示该解码器状态与输入序列每个词的匹配程度。

接下来,对 \(e_{t, i}\) 进行 softmax 计算,得到 注意力权重(attention weights) \(a_{t, i}\)(有 \(0 < a_{t, i} < 1, \ \sum_{i} a_{t, i} = 1\)),其在给定解码器的隐藏状态下预测了输入单词的概率分布,于是可计算总结了编码器信息的上下文向量: \[ c_{t} = \sum_{i} a_{t, i}h_{i}. \]

最后则与无注意力的 RNN 相同,计算下一个编码器的隐藏状态: \[ s_{t} = g_{U}(y_{t - 1}, s_{t - 1}, c_{t}). \]

对于 \(s_{0}\) 的初始化,其有多种方法:可直接使用 \(h_{T}\) 或对其做一定的线性变换,也可直接设为零等等。

其提供了一个直觉:通过上下文向量 \(c_{t}\) 的调节,解码器 RNN 可关注输入序列中其想要的不同部分。此被称为 注意力机制(Attention)。 且该过程全程可微,故不需对该网络进行监督,可直接用梯度下降进行端到端的学习。

另外,可尝试对注意力权重进行可视化。其反映了网络在解决问题时关注的内容,从而可协助我们理解网路的处理过程。

注意力层(Attention Layer)

从上述步骤中进行抽象,Attention 实则为对于一系列的 data vectors(对应 \(h\)),对其我们需要生成多种输出,每个输出有一个 query vector(对应 \(s\)),我们需对其进行回溯,汇总 data vector 为一个输出向量(对应 \(c\),在上述 RNN 中为上下文向量)。

即有:

  • Query vector:\(q \in \mathbb{R}^{D_{Q}}\)
  • Data vectors:\(X \in \mathbb{R}^{N_{X} \times D_{Q}}\)

进行计算:

  • 相似度:\(e \in \mathbb{R}^{N_{X}}\)\(e_{i} = f_{\text{att}}(q, X_{i})\)
  • 注意力权重(Attention weights):\(a = \operatorname{softmax}(e) \in \mathbb{R}^{N_{X}}\)
  • Output vectors:\(y = \sum_{i} a_{i}X_{i} \in \mathbb{R}^{D_{X}}\)

对于相似度的计算,为使 \(f_{\text{att}}\) 尽量简单且进行泛化,可使用点积。其作为相似度计算方式效果较好。 但单纯使用点积会出问题。当向量维度较高时,点积的值更大(若设 \(\mathbf{E}q = \mathbf{E}X_{i} = 0\)\(\mathbf{Var}q = \mathbf{Var}X_{i} = 1\),则有 \(\mathbf{Var}(q \cdot X_{i}) = D_{Q}\),故 \(q \cdot X_{i} \sim \mathcal{O}(\sqrt{ D_{Q} })\)),则指数函数处理后的值会更极端,使用 softmax 计算会饱和,得到的概率分布过于尖锐,从而导致梯度消失。则需对点积进行缩放。 则有: \[ e_{i} = \frac{q \cdot X_{i}}{\sqrt{ D_{Q} }}. \]

进一步,对于更多的 query vectors \(Q \in \mathbb{R}^{N_{Q} \times D_{Q}}\),有: \[ \begin{align*} E &= \frac{QX^{\top}}{\sqrt{ D_{Q} }} &\in \mathbb{R}^{N_{Q} \times N_{X}}; \\ A &= \operatorname{softmax}(E, \text{dim} = 1) &\in \mathbb{R}^{N_{Q} \times N_{X}}; \\ Y &= AX &\in \mathbb{R}^{N_{Q} \times D_{X}}. \end{align*} \]

上述公式中两次使用 \(X\) 的用途不同。进一步,可分开两次使用 \(X\) 的用途,利用可学习的权重:

  • \(X \in \mathbb{R}^{N_{X} \times D_{X}}\)
  • Key matrix:\(W_{K} \in \mathbb{R}^{D_{X} \times D_{Q}}\)
  • Value matrix:\(W_{V} \in \mathbb{R}^{D_{X} \times D_{V}}\)

\(X\) 投影为:

  • Keys:\(K = XW_{K} \in \mathbb{R}^{N_{X} \times D_{Q}}\)
  • Values:\(V = XW_{V} \in \mathbb{R}^{N_{X} \times D_{V}}\)

其中 keys 将与 query vectors 进行比较,以计算对齐分数;而 values 则是进行线性组合,以生成该层输出的对象。其直觉为:输入(对 data vectors 进行匹配)和输出(从 data vectors 进行获取输出)不一定相同。

于是则有: \[ \begin{align*} E &= \frac{QK^{\top}}{\sqrt{ D_{Q} }} &\in \mathbb{R}^{N_{Q} \times N_{X}}; \\ A &= \operatorname{softmax}(E, \text{dim} = 1) &\in \mathbb{R}^{N_{Q} \times N_{X}}; \\ Y &= AV &\in \mathbb{R}^{N_{Q} \times D_{V}}. \end{align*} \]

故该层有两个输入 \(X\)\(Q\),两个可学习权重 \(W_{K}\)\(W_{V}\),一个输出 \(Y\)。该层也可称为 交叉注意力层(Cross-Attention Layer)。

自注意层(Self-Attention Layer)

对于自注意力层,只有一个输入 \(X \in \mathbb{R}^{N \times D_{\text{in}}}\),即只需处理一个输入向量集合。此时则需将 \(X\) 分别投影至 query、key 和 value:

  • Input vectors:\(X \in \mathbb{R}^{N \times D_{\text{in}}}\)
  • Key matrix:\(W_{K} \in \mathbb{R}^{D_{\text{in}} \times D_{\text{out}}}\)
  • Value matrix:\(W_{V} \in \mathbb{R}^{D_{\text{in}} \times D_{\text{out}}}\)
  • Query matrix:\(W_{Q} \in \mathbb{R}^{D_{\text{in}} \times D_{\text{out}}}\)

进行计算:

  • Queries:\(Q = XW_{Q} \in \mathbb{R}^{N \times D_{\text{out}}}\)
  • Keys:\(K = XW_{K} \in \mathbb{R}^{N \times D_{\text{out}}}\)
  • Values:\(V = XW_{V} \in \mathbb{R}^{N \times D_{\text{out}}}\)
  • Similarities:\(E = \frac{QK^{\top}}{\sqrt{ D_{\text{out}} }} \in \mathbb{R}^{N \times N}\)
  • Attention weights:\(A = \operatorname{softmax}(E, \text{dim} = 1) \in \mathbb{R}^{N \times N}\)
  • Output vector:\(Y = AV \in \mathbb{R}^{N \times D_{out}}\)

实际上,对于 \(Q, K, V\) 的计算,可通过一个矩阵乘法直接计算(相交于多次小矩阵乘法,较少的大矩阵乘法计算更高效): \[ \begin{bmatrix} Q & K & V \end{bmatrix} = X \begin{bmatrix} W_{Q} & W_{K} & W_{V} \end{bmatrix}. \]

对于一些有两种不同类型的数据的问题可用交叉注意力层,例如翻译、图像字幕生成等等;而对于只有一个数据源的问题,则用自注意层,例如图像分类(其只需比较图像自身部分)。

显然,自注意力层具有 置换等变性(permutation equivariant):\(F(\sigma(X)) = \sigma(F(X))\),即当打乱输入顺序,则输出顺序也会以相同的顺序打乱,其反映了自注意力不关心输入顺序。 故为加入输入顺序,可在输入向量中加入额外的数据片段,称为 位置嵌入(positional encoding)。

掩码自注意力层(Masked Self-Attention Layer)

有时,我们不希望在计算时,所有的输入都可以互相看见,这是可添加掩码进行实现:在计算对齐分数后,在需要阻断注意力的地方赋值为 \(-\infty\),此时在 softmax 处理后,该点的概率则为 \(0\),即输出 \(Y\) 不会依赖于该索引处的 value。

使用此机制,可控制哪些输入可相互作用。例如在语言问题中,其递归输出,在输出第 \(t\) 个词时,只能看到前 \(t - 1\) 个词,为使在训练时让模型不可“偷看答案”,则可使用掩码进行处理。

多头自注意力(Multiheaded Self-Attention Layer)

即对一个 \(X\) 同时运行 \(H\) 个独立的自注意力,得到 \(H\) 个输出 \(Y\),然后对 \(Y\) 进行线性投影,融合来自各独立自注意层的输出数据,得到 \(O\)

其可用矩阵乘法进行计算,即有:

  • Input vectors:\(X \in \mathbb{R}^{N \times D}\)
  • Key matrix:\(W_{K} \in \mathbb{R}^{D \times HD_{H}}\)
  • Value matrix:\(W_{V} \in \mathbb{R}^{D \times HD_{H}}\)
  • Query matrix:\(W_{Q} \in \mathbb{R}^{D \times HD_{H}}\)
  • Output matrix:\(W_{O} \in \mathbb{R}^{HD_{H} \times D}\)

其中有 \(H\) 个并行的层(也被称为注意力头),每个层都使用维度 \(D_{H}\) 作为 qkv 的维度,其被称为头维度(head dim),通常设置 \(D_{H} = \frac{D}{H}\),以使输入和输出维度保持一致。

进行计算:

  • Queries:\(Q = XW_{Q} \in \mathbb{R}^{H \times N \times D_{H}}\)
  • Keys:\(K = XW_{K} \in \mathbb{R}^{H \times N \times D_{H}}\)
  • Values:\(V = XW_{V} \in \mathbb{R}^{H \times N \times D_{H}}\)
  • Similarities:\(E = \frac{QK^{\top}}{\sqrt{ D_{H} }} \in \mathbb{R}^{H \times N \times N}\)
  • Attention weights:\(A = \operatorname{softmax}(E, \text{dim} = 1) \in \mathbb{R}^{H \times N \times N}\)
  • Head outputs:\(Y = AV \in \mathbb{R}^{H \times N \times D_{H}} \cong \mathbb{R}^{N \times HD_{H}}\)
  • Outputs:\(O = YW_{O} \in \mathbb{R}^{N \times D}\)

其可使计算量和参数增大,并增大该层的容量和复杂度,从而具有更强的能力。

处理序列的三种方法

RNN 卷积 自注意力
优势 理论上对长序列友好,时间复杂度和空间复杂度均为 \(O(n)\) 可并行计算 可处理长序列,任意向量可互相查看,故一层即有可观的效果;且其高度并行化
劣势 无法并行计算 难以构建大的感受野,对于非常长的序列则需非常大的卷积核,或堆叠多个卷积核 计算成本高,其时间复杂度和空间复杂度均为 \(O(n^{2})\)

对于自注意力的计算成本,可通过使用更多的 GPU 解决。且 \(O(n^{2})\) 意味着更大的计算量,从而具备更强的推理及处理能力,故其不一定是劣势。

事实证明,仅用注意力就可解决大部分问题。

Transformer

Transformer Block

Transformer Block 具有如下结构:

  1. 输入向量组 \(x\)
  2. 对所有向量执行多头自注意力;
  3. 从 1 末尾进行残差连接;
  4. 层归一化;
  5. 对每个向量独立地执行 MLP(也被称为 FFN,其为 2 层神经网络);
  6. 从 4 末尾进行残差连接;
  7. 层归一化;
  8. 输出向量组 \(y\)

其中,MLP 赋予了对向量独立处理的能力。

Transformer

Transformer 即由多个 Transformer Block 堆叠实现。

对于编码器,即为多个 Transformer Block 的堆叠(在堆叠前先对输入 \(x\) 进行位置嵌入),其可将序列进行编码。

对于解码器,则为另一种 Transformer Block 的堆叠(同样在堆叠前先对输入 \(x\) 进行位置嵌入,且在最后会接入全连接层以生成输出),其可进行解码并生成输出:

  1. 输入向量组 \(x\)
  2. 对所有向量执行 掩码 多头自注意力;
  3. 从 1 末尾进行残差连接;
  4. 层归一化;
  5. 执行 多头注意力\(q\) 取前一层的输出,\(k\)\(v\) 取编码器生成的结果);
  6. 从 4 末尾进行残差连接;
  7. 层归一化;
  8. 对每个向量独立地执行 MLP;
  9. 从 7 末尾进行残差连接;
  10. 层归一化;
  11. 输出向量组 \(y\)

Vision Transformer(ViT)

即为 Transformer 在图像中的应用。给定一张图像,将图像分割成 \(N\) 个 patches,并将每个 patch 投射为 tokens(线性投射至 \(D\) 维向量),对其进行坐标嵌入(对每个位置创建一个 \(D\) 维向量表示位置,并相加至 tokens),随后将向量传入 Transformer(无需使用掩码),得到输出 \(N \times D\)

对于 \(C\) 分类问题,则对输出向量进行池化操作,得到 \(1 \times D\),并使用线性层预测类别分数即可。

另一种方法(实则为使用 ViT 进行图像分类的基本和最标准的做法之一)则可在 Transformer 之前再加一个可学习的 token 作为特殊的额外输入,经过 Transformer 处理后得到额外的输出,将其线性投射为 \(C\) 维向量,即可处理为类别的概率分布。其被称为 分类词元(class token)。

Transformer 的微调

Pre-Norm Transformer

在 Transformer 中,Layer Norm 在残差连接之外,这使得 Transformer Block 无法习得恒等映射。

为解决该问题,可修改 Transformer Block,使 Layer Norm 前置:

  1. 输入向量组 \(x\)
  2. 层归一化;
  3. 对所有向量执行多头自注意力;
  4. 从 1 末尾进行残差连接;
  5. 层归一化
  6. 对每个向量独立地执行 MLP(也被称为 FFN,其为 2 层神经网络);
  7. 从 4 末尾进行残差连接;
  8. 输出向量组 \(y\)

RMSNorm

即在 Pre-Norm Transformer 的基础上,替换 Layer Norm 为 RMSNorm(均方根归一化):输入 \(x \in \mathbb{R}^{D}\),输出 \(y \in \mathbb{R}^{D}\),权重 \(\gamma \in \mathbb{R}^{D}\),有: \[ y_{i} = \frac{x_{i}}{\operatorname{RMS}(x)} \times \gamma_{i}. \] 其中: \[ \operatorname{RMS}(x) = \sqrt{ \varepsilon + \frac{1}{N} \sum\limits_{i=1}^{N} x_{i}^{2} }. \]

其可使训练更加稳定。

SwiGLU MLP

在 RMSNorm 基础上,使用 SwiGLU MLP 替换 MLP,其引用了门控非线性。

对于 MLP,输入 \(X \in \mathbb{R}^{N \times D}\),权重 \(W_{1} \in \mathbb{R}^{D \times 4D}, \ W_{2} \in \mathbb{R}^{4D \times D}\),有输出: \[ Y = \sigma(XW_{1})W_{2}. \]

对于 SwiGLU MLP,输入 \(X \in \mathbb{R}^{N \times D}\),权重 \(W_{1}, W_{2} \in \mathbb{R}^{D \times H}, \ W_{3} \in \mathbb{R}^{H \times D}\),有输出: \[ Y = (\sigma(XW_{1}) \odot XW_{2})W_{3}. \]

其引入了更好的非线性性。取 \(H = \frac{8D}{3}\) 时,则与之前的 MLP 的总参数量相同。

混合专家模型(MoE)

在 RMSNorm 基础上,使用多组 MLP 代替原本的一组 MLP,即有 \(W_{1} \in \mathbb{R}^{E \times D \times 4D}, \ W_{2} \in \mathbb{R}^{E \times 4D \times D}\)。这些 MLP 可并行计算。

其中每个 MLP 均为一个 专家(expert)。在该层接收 token 时,每个 token 只会路由到 \(A \ (< E)\) 个专家,这些被选中的称为 激活专家(active experts)。

于是对于该模型,参数量扩大 \(E\) 倍,但计算量只会扩大到 \(A\) 倍。其有助于习得更强大的模型,而不会增加太多的计算量。

如今所有的顶级大语言模型几乎都采用了 MoE 架构。


Lecture 9: Object Detection, Image Segmentation, Visualizing and Understanding

计算机视觉任务

分类(Classification)

详见前几个 Lecture。

语义分割(Semantic Segmentation)

其旨在给图上的每个像素分配一个标签来指代是什么事物(例如对于一张图片,指出哪些区域是猫、是树、是草原、是天空等等)。其只关注像素所属的类别,故若同一类别有两个实例,其无法将这两个实例分开。

对于每个像素,我们取包含它的图片的一个 patch(以获取该像素周围环境的信息),然后将其输入 CNN 或其他用于分类的架构进行训练。 显然,该方法 极其耗时

从另一种角度,考虑训练一个网络,输入图片,直接输出分割图(即对每个像素的标签矩阵)。

完全卷积神经网络(Fully Convolutional)

为实现该网络,可只使用卷积层进行实现,即 完全卷积神经网络(FCN)。对于输入 \(3 \times H \times W\),堆叠若干个卷积层,每个卷积层处理后 图像大小不变(为 \(D \times H \times W\));对于输出,为生成图像,也只使用卷积层,处理为 \(C \times H \times W\),进行 argmax 计算为 \(H \times W\) 即为分割结果。

对于该方法,若输入图像较大,则每一层的输出规模均较大,计算卷积的的成本极高(其为瓶颈)。

为解决该问题,考虑将图像进行 下采样,再进行 上采样。每层的维度有例如如下的变化:

  1. Input:\(3 \times H \times W\)
  2. High-res:\(D_{1} \times \frac{H}{2} \times \frac{W}{2}\)
  3. Med-res:\(D_{2} \times \frac{H}{4} \times \frac{W}{4}\)
  4. Low-res:\(D_{3} \times \frac{H}{4} \times \frac{W}{4}\)
  5. Med-res:\(D_{2} \times \frac{H}{4} \times \frac{W}{4}\)
  6. High-res:\(D_{1} \times \frac{H}{2} \times \frac{W}{2}\)
  7. \(C \times H \times W\)
  8. Predictions:\(H \times W\)

其为最基本最广泛的语义分割算法。

显然,对于训练该模型,loss 定义为每个像素的 softmax 的平均值即可。

对于下采样,可采用池化操作或带步幅的卷积等,而对于上采样,采用 解池(Unpooling)操作。

解池(Unpooling)

有多种操作方法(均为 \(H \times W \to 2H \times 2W\),将 \(H \times W\) 中的每个点扩展成 \(2 \times 2\) 的区域): - 最近邻(Nearest Neighbor):直接将一个点扩展成 \(2 \times 2\) 个与其相同的点; - 钉床(Bed of Nails):将该点扩展成 \(2 \times 2\) 的区域,左上角与其相同,其他位置均为零。

Max Unpooling

若在下采样时使用了 Max Pooling,则可保存最大值出现的位置,在对应的上采样时,复制信息至对应的原先最大值出现的位置,其他位置补零。

可学习上采样(Learnable Upsampling)

即卷积核的逆操作(转置矩阵(Transposed Convolution),即 \(1 \times 1 \to HH \times WW\),同样有 stride 和 padding 等),对于过滤器处理到重叠的部分,求和即可。

U-Net

即对于 FCN 中的先下采样后上采样的操作,将其网络结构画出来呈 U 形,称为 U-Net。

对于下采样过程,其会扩大感受野,并丢失一些空间信息,而上采样过程会恢复图像分辨率。

与 FCN 不同的是,上采样的每一维度在开头会对相应下采样层的结果进行拼接,以此保留原有的结构信息,得到更清晰的输出。

迄今为止,医疗相关的分割任务仍会采用 U-Net 或其变体。

对象检测(Object Detection)

即需要进行分类并标定坐标。对每一个对象计算标签类别分数,并获取边界框的坐标 \((x, y, w, h)\)。 其中,可对类别可使用 softmax loss,对框的坐标使用 L2 loss,将其相加得到该问题的 loss。 显然,在图像中的对象数量较多时,其生成的坐标较多,该方法执行困难,即不具有可扩展性。

另一种方法为,不考虑将整张图像作为输入,而考虑查看边界框。对于每个边界框,其只有一个标签,故可进行分类。于是可考虑将边界框做为滑动窗口,即考虑任意的 \((x, y, w, h)\) 是否可检测到物体,以此可找到每个物体的最大概率边界框。 同样的,边界框组合的数量巨大,且该算法不可扩展。

可以考虑寻找有高概率包含物体的区域,即 候选区域(region proposal)。若能高效地找到候选区域,则该问题解决较易。

R-CNN

对于候选区域,将其提取,并在这块区域运行 CNN,进行分类,且可优化边界框(稍微改变坐标)。 但该算法运行速度慢,其对每个候选区域都会运行一次 CNN,而候选区域的数量就很大(约 2K,且为预先定义的)。

Fast R-CNN

由于 CNN 保留空间信息,考虑直接在整个图像上运行大卷积网络。在生成的特征图(feature map)中,可找到候选区域对应的区域,在这些区域上再运行小的 CNN,并生成输出(盒子偏移(box offset)和对象类别)。

区域候选网络(Region Proposal Network)

先进行 CNN,然后在特征图的每个位置都放置预定大小的锚框(anchor),通过卷积层,得到判断是否有物体和位置修正,可细化可能包含物体的区域,以此对框进行细化,使得该框有高概率包含物体。

在完成后,通常会按照“是否含有物体”的概率进行排序,并取前 \(K\) 个作为候选区域。

尽管如此,完成该任务仍需 RPN 和 Fast R-CNN 两个网络,检测两次物体,且运算量大。

单阶段对象检测器(SSD、YOLO)

就算在如今的工业界,YOLO 仍被作为物体检测的基础被广泛应用,其可快速地进行物体检测,且表现优秀。其只需在图像上看一次即可生成所有的边界框。

其将图片分割成 \(S \times S\) 的网格,并创建了一个 FCN,对网格中的每一个小框,均会输出如下内容:

  • \(P\) (object):边界框包含物体的概率;
  • \(B\) 个边界框 \((x, y, h, w)\),对该框中存在的对象的细化(可能会比这个小框更大);
  • \(P\) (class):该框内的物体属于某个类别的概率。

其可对不同的对象生成多个边界框,且每个边界框均有一个存在物体的概率。对这些不同的边界框和对象可进行阈值处理,即 非最大抑制(non-maximum suppression)及其他一些算法(具体看 YOLO 论文)。

DETR(Object Detection with Transformers)

与 ViT 一样,其会把图像分为 patches,然后将 patch 输入 CNN 得到 token,并加入位置编码,传入 Transformer Encoder。 再将 Transformer Encoder 的输出传入 Transformer Decoder,但对于 Decoder,我们定义 queries 为可学习的参数(其表示“我希望对此 query 输出一个物体”),其传入个数表示在图像中最多寻找多少个要检测的对象,最终分别对每个 query 生成输出。 对于生成的输出,传入 FFN 以生成类别标签和边界框,或说明未检测到物体。

若 queries 的个数小于图像中对象的个数,则大概会输出置信度最高的几个框。

实例分割(Instance Segmentation)

其即为语义分割和对象检测的结合。

Mask R-CNN

结构与 Fast R-CNN 类似,只需在进行 CNN 后(在该 CNN 之后会生成类别和盒子偏移),另外再加一层卷积层(多为 FCN),其对每种物体类别生成该对象像素级别的 masks,其与输入图像对应位置的大小相同,且基于FCN得到的特征图本身。

实践说明,Mask R-CNN 可取得很好的效果,且可应用与识别动作等。

可视化与理解

在线性分类器中,对权重可视化,不难发现其表现出了该类别的模板。该种方法同样可用于神经网络。

而可视化过滤器(例如对于 CNN 的第一层,其具有较少的通道),可发现其学习了基本形状、方向等信息。总而言之,其在学习 模式(patterns),且在后期阶段,学习到的模式会更全面且强大。

显著性(Saliency)

其反映了哪个像素 / 哪一块非常重要。

为实现该点,对于图像分类任务,先进行前向传播生成标签,而现在,我们需要考察每个像素,查看改变像素点的值会对标签的得分产生多大的影响,不难发现其即为梯度。 于是考虑对标签得分进行求导,但求导的对象是像素值,而不是网络的权重。 于是对求到的梯度进行可视化,可找到重要的像素点。

类激活图(Class Activation Mapping,CAM)

对于查看每种类别是如何激活的,则需要 类激活图(CAM)。其为 CNN 中最广泛应用的算法之一,也可应用于其他结构(不过 Transformer 会有更好的方法)。

例如最后几层为 CNN -> Global Average Pooling -> FC 得到每个类别的分数的结构:

  • CNN:\(f \in \mathbb{R}^{H \times W \times K}\)
  • Global Average Pooling:\(F_{k} = \frac{1}{HW}\sum_{h, w} f_{h, w, k} \in \mathbb{R}^{K}\)
  • FC:\(S_{c} = \sum_{k} w_{k, c}F_{k}\)

不难推得: \[ S_{c} = \frac{1}{HW} \sum_{h, w} \sum_{k} w_{k, c}f_{h, w, k}. \]

即每个分数即为 CNN 特征图的加权和,于是每个分数可回溯至特征图在空间中的特定位置。且由于 CNN 的空间一致性,可从特征图再追溯至原图的图像空间。

于是我们可在特征图上查看这些类别是如何被激活的,以及每个类别如何影响特征图的位置,可得到类激活图 \(M \in \mathbb{R}^{C \times H \times W}\)\[ M_{c, h, w} = \sum_{k} w_{k, c}f_{h, w, k}. \]

自此,通过特征图,可将类激活图回溯至原图,并进行可视化,反映图中的区域对类别激活的影响。 不过该种方法只能应用于 最后一个卷积层

梯度加权类(Gradient-Weighted Class Activation Mapping,Grad-CAM)

该种方法则可对任意层进行操作。只需以下操作:

  1. 提取该层的激活值 \(A \in \mathbb{R}^{H \times W \times K}\)
  2. 计算梯度: \[ \frac{ \partial S_{c} }{ \partial A } \in \mathbb{R}^{H \times W \times C}. \]
  3. 对梯度进行全局平均池化,得到权重 \(\alpha \in \mathbb{R}^{K}\)\[ \alpha_{k} = \frac{1}{HW} \sum_{h, w} \frac{ \partial S_{c} }{ \partial A_{h, w, k} }. \]
  4. 计算激活图 \(M^{c} \in \mathbb{R}^{H \times W}\)\[ M^{c}_{h, w} = \operatorname{ReLU}\left( \sum_{k} \alpha_{k}A_{h, w, k} \right). \]

可视化 ViT 权重

Transformer 本身就自带激活图(例如输入和输出的权重热力图),故对每个类别分数,只需在像素空间中画出激活图即可,其即为 ViT 的特征的可视化。


Lecture 10: Video Understanding

即在图像上加了一个维度:\(T \times 3 \times H \times W\)\(3 \times T \times H \times W\)

视频分类

在图像分类中,我们常常关注物体本身的种类,相应地,在视频分类中,我们常常关注动作的种类(例如跑步等等)。

此外,与图像分类不同的一点是,视频文件 很大,这导致了我们无法直接将这类数据传入 GPU。 为解决该问题,常常对视频进行压缩(例如压缩帧率、压缩分辨率等等)。

对于长视频,采用的策略是在视频片段(clips)上进行训练,即对于高帧率长时间的原始视频,有:

  • 训练:采用低帧率,并裁剪为 clip 进行训练;
  • 测试:采样不同 clip 并进行预测,并取预测分数的平均值。

单帧卷积神经网络(Single-Frame CNN)

考虑视频为一系列的图片,可对考虑选取部分帧运行 CNN,即 单帧卷积神经网络,然后可以考虑对预测结果取平均即为最终结果。当视频的不同帧变化不大时,其为非常强大的基线方法(baseline)。 选取帧的采样方法有多种,较为简单的为随机采样。

晚期融合(Late Fusion)

在单帧 CNN 的基础上,可考虑对每一帧的特征进行融合,即 晚期融合。即选取部分帧运行 CNN 提取特征,然后将这些特征图展平为向量并进行拼接,得到一个包含该视频所有信息的巨型向量,并输入 MLP 进行处理并分类。

该方法有个明显的缺点:MLP 的参数量过大。故考虑改进, 可考虑不对特征图展平的向量进行拼接,而是使用池化操作(如平均池化 \(T \times D \times H' \times W' \to D\)),然后输入 MLP。比较明显的,该方法相交于拼接,会有丢失信息的缺点。

早期融合(Early Fusion)

晚期融合可能在 CNN 就已经丢失部分有用的信息(例如在跑步视频中,CNN 可能会丢失双脚交替运动的时间上的特征信息)。 考虑在靠近原始视频帧的早期层提取特征,更有可能保留低层次信息,进行拼接或提取即可分析时间上的运动。而在大量 CNN 后保留的高层信息,即可能丢失这些信息。

于是可采用早期融合,即先将 \(3 \times T \times H \times W\) 整合为 \(3T \times H \times W\),接着使用第一个卷积,转化为 \(D \times H \times W\) 以处理时序信息和所有的视频帧的信息,接下来的操作则为标准流程。

尽管考虑了时序,使用单层卷积进行时序信息的整合,达到理想效果仍较为困难。

3D CNN

即使用 3D CNN 和 3D pooling 在网络中逐步提取信息,逐步压缩时序维度,将时空维度压缩成三维特征图。

对于早期融合,由于在开头直接融合所有时序,其无 时间平移不变性,例如当我们想要学习不同时间点的同一种转变,则需要多个独立的滤波器分别进行学习。 与之对应地,显然 3D CNN 就具有时间平移不变性。

同样地,可对 3D CNN 进行可视化,其与 CNN 可视化类似,且可观测到一些运动模式。

示例视频数据集

与 ImageNet 类似,视频也有示例数据集:Sports-1M,其为体育运动分类的数据集。

在该数据集上,单帧 CNN 的效果已经较好,且 3D CNN 的效果在不断改进。

C3D

即 VGG 的 3D CNN 版本。其已训练的版本曾较常被用于微调。

其有明显的缺陷:直接使用了 \(3 \times 3 \times 3\) conv 等结构,使得计算成本高。

光流(Optical Flow)

另外一种思路,考虑将时空信息分开,即考虑显式建模存在的时序特征(动作)。

考虑使用双流网络分别处理外观和运动信息。显示测量运动信息使用 光流,其通过测量相邻帧中像素运动的变化测量了动作的变化,即计算帧内每个点的运动速度,并给出下一帧中点的位置估计: \[ \begin{align*} F(x) &= (\mathrm{d}x, \mathrm{d}y) \\ I_{t + 1}(x + \mathrm{d}x, y + \mathrm{d}y) &= I_{t}(x, y) \end{align*} \]

同样地,可对其可视化,可分别可视化水平运动 \(\mathrm{d}x\) 和垂直运动 \(\mathrm{d}y\)

双流网络(Two-Stream Networks)

有了光流,可考虑进行 双流网络,分别用单帧模型进行外观分类,用时间流卷积处理多帧光流(对相邻的两帧均计算光流),并进行预测,最后将两种预测结果汇总得到最终预测。

其效果显著,且仅使用运动信息就能达到显著高于仅使用外观流的较好的效果(有一种猜测是运动流更不容易过拟合)。

建模长期时序结构

以上方法只能处理一个小的 clip,于是需要考虑如何建模长期时序结构,以识别更远时间的关联。

与处理序列类似,考虑使用 RNN 实现。先使用 CNN 得到 clip 的特征,然后将这些局部特征输入 RNN 进行处理,并根据问题进行隐藏状态的输出映射。

通常,在训练中会使用预训练的 CNN 提取特征,只训练 RNN,否则网络会十分庞大,难以训练。

循环卷积神经网络(Recurrent Convolutional Network)

考虑结合 CNN 和 RNN,其与多层 RNN 较为类似。

在该网络中,每个特征图均使用维度 \(C \times H \times W\),且第一层为 CNN 分别处理 clip,从第二层开始即为 RNN,并将 RNN 中的所有矩阵乘法替换为二维卷积。

其可实现空间和时间的融合,但其处理速度很慢(无法并行化),故应用较少。

时空自注意力(Spatio-Temporal Self-Attention,非局部块(Nonlocal Block))

可将自注意力改为三维形式。将 clip 输入 3D CNN,处理为 \(C \times T \times H \times W\) 特征图后,可使用 \(1 \times 1 \times 1\) conv 改变通道维度,处理为 \(C' \times T \times H \times W\) queries、keys 和 values,并得到 \(THW \times THW\) attention weights,从而得到输出的 \(C' \times T \times H \times W\) 输出特征图,最后用 \(1 \times 1 \times 1\) conv 映射回 \(C \times T \times H \times W\),并进行残差连接。此为一个三维的自注意力块。

可将该块加入视频处理,例如采用 3D CNN -> Nonlocal Block -> 3D CNN -> Nonlocal Block -> 3D CNN 的架构,更有效地融合时空信息,最终进行分类。

I3D

即考虑将一个原有的 2D CNN 架构膨胀成 3D CNN 架构。 将每个 2D \(K_{h} \times K_{w}\) conv / pool 都替换为 3D \(K_{t} \times K_{h} \times K_{w}\) conv / pool 即可.

更进一步,可考虑迁移已训练的权重,以获得一些较好的先验信息。即初始化膨胀的 3D CNN 权重为原本的 2D CNN 在图像上训练得到的权重(将核复制 \(K_{t}\) 次,并除以 \(K_{t}\))。

实践表明其效果很好,且也可用于膨胀运动流,以得到更好效果。

Vision Transformers for Video

即 ViT 的视频版本,其在视频上做了一些算法优化,表现很好。

可视化

与之前图像分类模型的可视化类似,可用相同的方法对视频模型进行可视化。

时序动作定位(Temporal Action Localization)

即定位动作在视频中发生的时间位置,可使用类似 Fast R-CNN 的架构并生成时间提议进行解决。

时空检测任务(Spatio-Temporal Detection)

即不仅定位动作在视频中发生的时间位置,也定位发生的空间位置。

声音

声音是之前未讨论,但对视频十分重要的维度。

其衍生出了一系列任务,如:

  • 视频信息引导声源分离(Visually-guided audio source separation);
  • 乐器声源分离(Musical instruments source separation);
  • 音视频理解(Audio-Visual Video Understanding);
  • 高效视频理解(Efficient Video Understanding);
  • 多模态第一视角视频理解(Multimodal Egocentric Video Understanding);
  • 视频理解 + LLM。

Lecture 11: Large Scale Distributed Training

GPU

GPU 极其擅长并行运算。且 GPU 在近些年运算速度增长极快(尤其是张量核心),多 GPU 训练也称为了主流。

以 H100 举例,其有计算核心(Compute Cores)和 80 GB 显存(HBM Memory,高带宽内存),这两个是分开的,需要通过总线进行通信,且在计算核心中,有 50MB 的 L2 缓存(L2 Cache),可与计算核心进行更快速地通信,且有很多 SMs(流多处理器)。

对于每个 SM,有 256KB L1 缓存(L1 cache)和 256 KB 寄存器(registers),并有 128 个 FP32 核心(FP32 Cores,其执行广义浮点运算)和 4 个 张量核心(Tensor Cores,其经过优化,执行矩阵乘法,且以混合精度运行(16-bit / 32-bit))。

张量核心为主要吞吐量来源,且为 GPU 程序的最大限度利用目标。其输入为 16-bit,输出为 32-bit。故记得在 PyTorch 层中将模型转为 16-bit,否则其会在浮点核心上运行(运行效率慢 20 倍)。

GPU 集群

GPU 之间的通信速度显著慢于 GPU 内部的通信速度(更一般地,通常来说“越远越慢”)。 例如从 H100 GPU -> Server(8x GPU)-> Rack(2 Servers)-> Pod(192 Raks)-> Cluster(8 Pods),不同位置的 GPU 的通信效率显著降低。

通常来说,对于模型最长训练通常为数月(这一点更多地指的是类似于 GPT 的超大型模型,且更多地取决于人而不是技术)。

TPU

现在也有除了 GPU 的其他计算硬件,例如张量处理单元(TPU),且已有一定应用(如 Gemini)。 其余也有 AMD、AWS 的类似产品,但用的最广的仍然是 NVIDIA,随后是 TPU。

在 GPU 集群上训练

计算机主要进行计算和通信,需考虑如何利用集群中的内存层级,实现通信与计算的重叠执行;且将计算拆分并并行化,以在训练过程中充分利用所有 GPU 资源。

其有几个层级的并行化(其中很多为针对 Transformer 的,其为 \(L\) 个层的堆叠,且每一层均对三维张量(批次大小、序列长度、特征维度)进行操作):

  1. 数据并行(Data Parallelism,DP);
  2. 上下文并行(Context Parallelism,CP);
  3. 流水线并行(Pipeline Parallelism,PP);
  4. 张量并行(Tensor Parallelism,TP)。

数据并行(DP)

有一个想法:对于 minibatch 的每个元素,计算 loss 和梯度相互独立,故可进行并行化。

故若有 \(M\) 个 GPU,则使用 \(MN\) 个元素的 minibatch,并给每个 GPU 加载单独的模型与优化器副本,并分划不同的 \(N\) 个元素形成小的 minibatch,分别计算 loss 和反向传播计算梯度。 计算完梯度后,则每个 GPU 需将梯度广播到所有 GPU,且每个 GPU 需从所有参与训练的 GPU 收集梯度,即 allreduce 操作(其执行效率为 \(O(\log M)\)),即可求平均即为真实梯度,并在对自己的模型副本上进行权重更新。 其中,计算梯度和跨 GPU 传播梯度可并行化(例如 \(L + 1\) 层的通信和 \(L\) 层的反向传播即可并行化),其可在进行计算的同时 隐藏通信的成本(不过这种计算与通信的并行需要手动编写代码,但大多数情况 PyTorch 已提供了可用的方案)。

梯度是线性的,故显然该方法在数学上成立。

对于单 GPU,编写 CUDA 时硬件会自动处理异步传输,但在集群中则不会发生此情况,这导致了单 GPU 和集群的不对称性。

完全分片数据并行(Fully Sharded Data Parallelism,FSDP)

DP 需要每个 GPU 都保存一份模型的副本(需要模型权重,模型的梯度,和优化器状态),但当模型规模较大时,显然是不现实的。 为解决该问题,可考虑将模型权重分配到不同的 GPU 中。

即每个权重 \(W_{i}\) 只会归属至一个 GPU(通常按层划分),且该 GPU 也会掌管该权重的全局梯度和优化器状态。 在进行每一层的前向传播前,需将 \(W_{i}\) 的权重广播至所有 GPU,然后进行前向传播,且删除 \(W_{i}\) 的副本。 该操作的计算与通信同样可以并行化(例如获取 \(W_{i + 1}\) 的权重和计算 \(W_{i}\) 的前向传播的并行)。

对每一层的反向传播,广播 \(W_{i}\) 至所有的 GPU,进行反向传播,然后将梯度传回该层归属的 GPU,并在该 GPU 上计算总梯度,进行权重更新。 该操作同样可并行化(计算 \(W_{i}\) 的反向传播、聚合 \(W_{i + 1}\) 的梯度并进行权重更新、获取 \(W_{i - 1}\) 的权重,这三者可并行化)。

可优化的一点,在前向传播时,所有 GPU 需保存最后一层的权重,这样在反向传播时就可直接用。

混合分片数据并行(Hybrid Shared Data Parallel,HSDP)

即 DP 和 FSDP 的结合。将 \(N = MK\) 张 GPU 分成 \(M\) 个组,每个组 \(K\) 张 GPU。每个组执行 FSDP,并在 \(M\) 个组之间执行 DP。

此被称为 多维并行(Multidimensional parallelism),即在两个不同的维度,用两个不同的策略进行并行化。在此视角下 GPU 组成一个二维网格。 这两种并行化需要不同量的通信:在 FSDP 中会传输三倍网络权重的完整副本量(前向传播权重、反向传播权重、梯度),而在 DP 中只需传输一倍网络权重的完整副本量(梯度),这符合 GPU 集群内部的多层次结构,较好地利用了网络拓扑。

激活检查点(Activation Checkpointing)

但对于超大型模型,就算只存一层的激活值,可能也远超单个 GPU 的显存。

对网络中的每一层,可抽象为:

  • 正向传播:\(A_{i + 1} = F_{i}^{\rightarrow}(A_{i})\)
  • 反向传播:\(G_{i} = F_{i}^{\leftarrow}(A_{i}, G_{i + 1})\)。 假设 \(F_{i}^{\rightarrow}\)\(F_{i}^{\leftarrow}\) 的复杂度均为 \(O(1)\),显然计算前向和反向传播的时间和空间复杂度均为 \(O(N)\)

为解决该问题,考虑不把激活值 \(A_{i}\) 存至内存,而在反向传播时进行重新计算 \(A_{i}\)。 显然该方法时间复杂度为 \(O(N^{2})\),空间复杂度 \(O(N)\)。可见时间复杂度过高。

于是考虑对层进行分块,即每 \(C\) 层建立一个 激活检查点,则只需在每个小块内进行计算。 此时有时间复杂度 \(O\left( \frac{N^{2}}{C} \right)\),空间复杂度 \(O(C)\)。 显然分块可取 \(C = \sqrt{ N }\),则有时间复杂度 \(O(N\sqrt{ N })\),空间复杂度 \(O(\sqrt{ N })\)

多 GPU 训练

可采用此扩展流程:

  1. 对于参数量约 1B 的模型,GPU 数量不超过约 128 张时,使用 DP 即可;
  2. 始终将单卡批次大小设置为能占满 GPU 显存的最大值;
  3. 若模型参数量超过 1B,考虑使用 FSDP;
  4. 使用激活检查点,以支持更大的单卡批次大小;
  5. 若 GPU 超过 256 张,考虑使用 HSDP;
  6. 若 GPU 超过 1000 张、模型参数量超过 50B,或序列长度超过 16K 时,需使用更高级的并行策略(CP、PP、TP)。

对于参数调节,目的为最大化 模型 FLOPs 利用率

硬件 FLOPs 利用率(Hardware FLPOs Utilization,HFU)

HFU 即在设备上进行计算,实际能达到理论最大计算性能的比例。

使用几行 PyTorch 代码即可进行对其进行基准测试(benchmark)。

不过 HFU 并没有考虑到模型需要做的其他事情(例如激活检查点、运行其他模型、数据加载、数据增强等等)。

模型 FLOPs 利用率(Model FLOPs Utilization,MFU)

MFU 即在模型中,前向和反向传播实际使用的算力,占 GPU 理论峰值浮点算力的比例:

  1. 计算 \(\text{FLOP}_{\text{theoretical}}\)(理论模型 FLOPs)= 前向与反向传播中。所有矩阵乘法的总浮点运算次数;
  2. 查询 \(\text{FLOP} / \text{sec}_{\text{theoretical}}\)(硬件理论峰值算力)= 设备的理论最大浮点运算吞吐量;
  3. 计算理论耗时 \(t_{\text{theoretical}} = \frac{\text{FLOP}_{\text{theoretical}}}{\text{FLOP} / \text{sec}_{\text{theoretical}}}\)
  4. 测量实际耗时 \(t_{\text{actual}}\) = 完成一次完整迭代的真实时间(包括数据加载、前向与反向传播、优化器更新等等);
  5. \(\text{MFU} = \frac{t_{\text{theoretical}}}{t_{\text{actual}}}\)

对于分布式训练,我们则需要通过调整参数最大化 MFU。 一般来说,大于 \(30\%\) 的 MFU 已经较好,大于 \(40\%\) 的 MFU 则可称为优秀(基本接近 SOTA)。

不过实际上,由于 GPU 的计算速度增长明显快于通信速度的增长,较新的设备有时会得到更差的 MFU。

上下文并行(CP)

由于以下算法均针对含有大量 GPU 的集群(例如超过一万个 GPU),故可能不太会用到(数百个 GPU 使用 DP 以及衍生算法即可解决)。

CP 即在序列维度上进行拆分,即使用不同的 GPU 处理长序列的不同部分。

其中 LayerNorm、残差连接和 MLP 较为简单,其中 MLP 包含权重,需与 DP 一样进行梯度同步。 Attention 则比较麻烦:其中 QKV 映射较为简单,但 Attention 矩阵计算则较难进行并行化。

可考虑 环形注意力(Ring Attention),即将完整的 Attention 矩阵分成多个块,然后让 GPU 按照正确的顺序独立地并行处理这些块。

另一种方法为 Ulysses 注意力,即对多头注意力,对每个头进行并行处理。

流水线并行(PP)

PP 即在层数上进行拆分。即对多层网络,将层划分给不同的 GPU。

显然这存在顺序依赖关系,朴素算法则会闲置很多 GPU,且对 \(N\)-way PP 的最大 MFU 仅为 \(\frac{1}{N}\)。其种空闲的位置则称为气泡(Bubble),如下表:

Time 1 2 3 4 5 6 7 8
GPU 1 \(\rightarrow\) \(\leftarrow\)
GPU 2 \(\rightarrow\) \(\leftarrow\)
GPU 3 \(\rightarrow\) \(\leftarrow\)
GPU 4 \(\rightarrow\) \(\leftarrow\)

为解决该问题,即缩小气泡,考虑同时运行多个 微批次(microbatches),如下表示例:

Time 1 2 3 4 5 6 7 8 9 10 11 12
GPU 1 \(\rightarrow_1\) \(\rightarrow_2\) \(\rightarrow_3\) \(\rightarrow_4\) \(\leftarrow_4\) \(\leftarrow_3\) \(\leftarrow_2\) \(\leftarrow_1\)
GPU 2 \(\rightarrow_1\) \(\rightarrow_2\) \(\rightarrow_3\) \(\rightarrow_4\) \(\leftarrow_4\) \(\leftarrow_3\) \(\leftarrow_2\) \(\leftarrow_1\)
GPU 3 \(\rightarrow_1\) \(\rightarrow_2\) \(\rightarrow_3\) \(\rightarrow_4\) \(\leftarrow_4\) \(\leftarrow_3\) \(\leftarrow_2\) \(\leftarrow_1\)
GPU 4 \(\rightarrow_1\) \(\rightarrow_2\) \(\rightarrow_3\) \(\rightarrow_4\) \(\leftarrow_4\) \(\leftarrow_3\) \(\leftarrow_2\) \(\leftarrow_1\)

显然,其可显著提高 MFU。 更多的 microbatches 会提高 MFU,但也意味着需要存储更多的激活值,可能需要进行激活检查点。这是需要进行权衡的。

张量并行(TP)

TP 即对模型的维度进行拆分。 在模型中,有大量的矩阵乘法 \(XW = Y\),则需将每个矩阵分拆至多个 GPU,例如拆至 \(4\) 个 GPU: \[ \begin{align*} W &= \begin{bmatrix} W_{1} & W_{2} & W_{3} & W_{4} \end{bmatrix} \\ XW_{i} &= Y_{i} \end{align*} \]

但在进行计算后,需将激活值 \(Y\) 进行聚集。则需用以下技巧以减少通信。 例如对于两个连续的这种层(\(XW = Y, YU = Z\)),对于 \(4\)-way TP,可用该法避免两层之间的聚集: \[ X \begin{bmatrix} W_{1} & W_{2} & W_{3} & W_{4} \end{bmatrix} = \begin{bmatrix} Y_{1} & Y_{2} & Y_{3} & Y_{4} \end{bmatrix} \begin{bmatrix} U_{1} \\ U_{2} \\ U_{3} \\ U_{4} \end{bmatrix} = Z. \] 则有: \[ Z = Y_{1}U_{1} + Y_{2}U_{2} + Y_{3}U_{3} + Y_{4}U_{4}. \] 故对第 \(i\) 个 GPU,只需计算 \(XW_{i} = Y_{i}\)\(Y_{i}U_{i} = Z_{i}\),并在每两层的末尾进行通信。

由于 Transformer 会使用双层 MLP,该技巧效果较好。

ND 并行(ND Parallelism)

对于 SOTA,其使用 ND 并行,即进行四维并行(同时使用 TP、CP、PP 和 DP)。 对不同的并行机制,会有不同的通信要求,故需利用集群中不同的通信速度,在集群中安排这些不同的并行维度。