跳转至

Info

前情提要以下整理全部出自 Gemini 之手。

CS231n 计算机视觉深度学习课程知识点梳理

约 62154 个字 预计阅读时间 311 分钟

欢迎来到计算机视觉的奇妙世界!这门课程将带你探索如何让计算机“看懂”图像。本篇笔记将为你详细梳理课程前期的核心基础知识,内容涵盖了从图像分类问题定义、基础分类器(KNN、线性分类器),到如何评估和优化模型(损失函数、正则化与最优化)的全过程。


第一章:欢迎来到计算机视觉的世界

核心任务:图像分类 (Image Classification)

我们学习的起点是一个最核心、最基础的任务:图像分类。

任务定义:给定一张输入的图片,计算机需要从一个预设的分类标签集合中(例如:{猫, 狗, 汽车, 飞机...}),为这张图片分配一个最合适的标签。

听起来简单,但这对计算机来说极其困难。


挑战:“语义鸿沟” (Semantic Gap)

我们人类看到一张图片,会立刻识别出这是一只可爱的猫。但计算机看到的,却是一个巨大的三维数字矩阵。

计算机眼中的图像:一张 800x600 像素的彩色图片,在计算机看来是一个 800 x 600 x 3 的矩阵(3代表RGB三个颜色通道)。矩阵中的每一个数字范围在 [0, 255] 之间,代表了不同颜色通道在特定像素点上的强度。

从这一堆冰冷的数字,到“这是一只猫”这个具有高级语义的认知,两者之间存在巨大的鸿沟,这就是“语义鸿沟”。此外,图像识别还面临诸多挑战:

视角变化 (Viewpoint variation):从不同角度拍摄的同一个物体,其像素值会完全不同。

光照变化 (Illumination):光线的强弱、颜色、方向都会极大地改变图像的像素值。

形变 (Deformation):物体可以呈现各种姿态和形状。

遮挡 (Occlusion):物体可能被部分遮挡。

背景混淆 (Background Clutter):物体可能与背景融为一体。

类内差异 (Intra-class variation):同一类别下的物体也千差万别,比如猫有各种品种、颜色和大小。

正是因为这些挑战,我们无法通过编写一套固定的规则(比如“如果图片中有尖耳朵和胡须,那就是猫”)来解决问题。我们需要一种更强大的方法。


第二章:数据驱动方法:两种基础分类器

既然无法硬编码规则,我们可以换一种思路:让计算机从数据中自己学习。这就是数据驱动方法的核心思想。

收集数据:收集大量带有标签的图片(例如,10000张猫的图片,10000张狗的图片...)。

训练模型:利用机器学习算法,让模型在这些数据上进行学习,构建一个分类器。

评估模型:用模型从未见过的新图片(测试集)来评估它的表现。

下面我们介绍两种最基础的分类器。


K-近邻分类器 (K-Nearest Neighbor, KNN)

这是最简单的分类器之一,它的核心思想非常直观:“物以类聚”。

核心思想:要判断一张新图片属于哪个类别,就看在训练数据集中和它“最像”的图片是哪个类别的。

算法流程:

训练 (Train):KNN的训练过程非常简单,就是把所有的训练图片和它们对应的标签全部“背”下来(存入内存)。

预测 (Predict):对于一张新的测试图片,执行以下步骤:

a. 计算它与训练集中每一张图片的“距离”。 b. 找到距离最近的 K 张训练图片。 c. 这 K 张图片各自有自己的标签,我们让它们进行“投票”,票数最多的那个标签就是我们对新图片的预测结果。

关键要素:距离度量 (Distance Metric)

如何衡量两张图片“像不像”?最简单的方法是直接比较它们的像素值。

L1 距离 (曼哈顿距离):将两张图片对应像素点的差值的绝对值加起来。

\[d_1(I_1, I_2) = \sum_p |I_1^p - I_2^p|\]

直观理解:想象在城市网格中从一个点走到另一个点,只能沿着街道走,L1距离就是你走过的总路程。

L2 距离 (欧氏距离):这是我们最熟悉的直线距离。将两张图片对应像素点差值的平方和再开方。

\[d_2(I_1, I_2) = \sqrt{\sum_p (I_1^p - I_2^p)^2}\]

直观理解:“两点之间,直线最短”。

超参数 (Hyperparameters)

K 的取值和距离度量(L1还是L2)的选择,都不是算法自己学习到的,而是需要我们人为设定的参数,我们称之为超参数。

如何选择超参数?

我们不能用训练集来选(因为当K=1时,模型在训练集上的准确率永远是100%,这是一种“过拟合”),更不能用最终的测试集来选(这相当于“考试作弊”,你无法知道模型在真实未知数据上的表现)。

正确的做法是:从训练集中分出一小部分数据,作为验证集 (Validation Set)。我们用训练集训练模型,然后在验证集上尝试不同的超参数组合,看哪个组合表现最好,就最终确定下来。

KNN的优缺点

优点

  • 实现非常简单,思想直观。
  • 训练过程快到飞起(只是记忆数据)。

缺点

  • 预测过程极其缓慢,因为需要和所有训练图片进行比较。
  • 基于像素的距离度量不合理。一张图片轻微的位移、旋转或色调变化,都会导致像素距离变得巨大,但这在语义上仍是同一张图片。
  • 在高维空间中表现不佳(维度灾难)。

由于这些致命缺点,KNN在实际的计算机视觉任务中基本不会被使用,但它为我们引入了数据驱动和超参数调优的重要思想。


线性分类器 (Linear Classifier)

KNN是一种“非参数”方法,它不对数据做任何假设。现在我们来看一种“参数化”的方法,这也是后续所有神经网络的基础。

核心思想:我们不再保留所有训练数据,而是希望学习到一组参数(权重 W 和偏置 b),构建一个函数,这个函数能直接将输入的图片像素映射到每个类别的得分。

数学模型

这个函数是一个简单的线性函数:

\[f(x, W, b) = Wx + b\]

x:输入的图片,被“拉直”成一个长长的列向量。例如,一张 32x32x3 的图片,x 的维度就是 3072x1。

W:权重矩阵。如果有个 C 类别,x 的维度是 D x 1,那么 W 的维度就是 C x D。

b:偏置向量,维度为 C x 1。它让我们的分类边界更加灵活。

f:输出结果,是一个 C x 1 的向量,其中每个元素代表该图片属于对应类别的得分。

三种理解角度

  1. 代数视角 (Algebraic View):这就是一个简单的矩阵乘法和加法。W 的每一行都与输入图片 x 进行点积,再加上对应的偏置,得到该类别的分数。
  2. 视觉视角 (Visual View):W 的每一行可以被看作是一个模板 (Template)。例如,W 的第一行如果是“猫”类别的权重,那么它实际上学习到了一个“猫”的平均模板。当一张新的图片输入时,线性分类器就是在拿这张图片和所有类别的模板进行匹配(通过点积),哪个模板匹配度最高(得分最高),就认为图片属于哪个类别。
  3. 几何视角 (Geometric View):如果把每张图片看作高维空间中的一个点,那么 W 的每一行都定义了一个超平面 (Hyperplane)。线性分类器实际上是用一组超平面将这个高维空间“切割”成不同的区域,每个区域对应一个类别。

局限性

线性分类器非常强大,但它的“线性”也带来了局限性。它只能解决那些线性可分的问题。例如,对于“XOR问题”或者一个环形分布的数据,单一的线性分类器是无能为力的。这也正是我们后续需要引入多层神经网络(非线性)的原因。


第三章:正则化与优化

我们有了一个模型 f(x, W, b),它可以为一张图片打出各个类别的分数。但我们如何知道当前的 W 和 b 是好是坏呢?我们需要一个量化的指标,这就是损失函数 (Loss Function)。

目的:损失函数用于衡量我们的模型在训练集上的表现有多差。损失值越大,说明模型表现越差。我们的目标就是找到一组 W 和 b,使得总损失最小。

对于一个包含 N 个样本的数据集,总损失是所有单个样本损失的平均值:

\[L = \frac{1}{N} \sum_{i=1}^{N} L_i(f(x_i, W, b), y_i)\]

其中 \(L_i\) 是第 \(i\) 个样本的损失。我们介绍两种常用的损失函数。


多类支持向量机损失 (Multiclass SVM Loss / Hinge Loss)

核心思想:对于一张图片,我们希望正确类别的分数,比所有不正确类别的分数都要高出一个安全边际 (margin)(通常设为1)。

数学公式:

\[L_i = \sum_{j \neq y_i} \max(0, s_j - s_{y_i} + 1)\]

\(s_j\) 是第 \(j\) 个(不正确)类别的分数。

\(s_{y_i}\) 是正确类别的分数。

直观解释:

我们遍历所有不正确的类别。

如果 \(s_{y_i} > s_j + 1\),即正确分数比错误分数高出至少1的边际,那么\(s_j - s_{y_i} + 1\)会是负数,max(0, ...) 的结果就是0,表示这部分没有损失。模型已经“满意”了。

如果这个条件不满足,那么损失就是 \(s_j - s_{y_i} + 1\),表示模型需要被惩罚。

特点:SVM Loss 是一种“知足常乐”的损失。一旦安全边际被满足,它就不再关心分数具体是多少,损失就为0。


Softmax 分类器 (Cross-Entropy Loss)

核心思想:SVM Loss 的分数不太好解释,我们更希望模型能输出每个类别的概率。Softmax分类器就是为此而生。

第一步:从分数到概率(Softmax函数)

我们使用 Softmax 函数 将原始的分数向量 s 转换成一个概率分布 P:

\[P(Y=k | X=x_i) = \frac{e^{s_k}}{\sum_j e^{s_j}}\]

\(e^{s_k}\):通过指数函数,将所有分数(无论正负)都映射到正数。

\(\sum_j e^{s_j}\):对所有指数化之后的分数求和,用于归一化。

特性:经过 Softmax 函数处理后,输出向量的每个元素都在 (0, 1) 之间,并且所有元素之和为1,完全符合概率分布的定义。

第二步:定义损失(交叉熵损失)

我们希望模型预测的正确类别的概率尽可能地高(接近1)。因此,我们定义损失为正确类别概率的负对数似然 (Negative Log Likelihood)。

\[L_i = -\log(P_{y_i}) = -\log\left(\frac{e^{s_{y_i}}}{\sum_j e^{s_j}}\right)\]

直观解释:

如果正确类别的概率 \(P_{y_i}\) 接近1,那么 \(\log(P_{y_i})\) 接近0,损失 \(L_i\) 也接近0。

如果正确类别的概率 \(P_{y_i}\) 接近0,那么 \(\log(P_{y_i})\) 趋近于负无穷,损失 \(L_i\) 就趋近于正无穷。

((特点)):Softmax Loss 是一种“永不满足”的损失。即使正确类别的概率已经很高(如0.9),它仍然会努力让它更高,趋近于1。它会促使正确类别的分数远大于其他所有不正确类别的分数。这个损失函数在数学上也等价于交叉熵 (Cross-Entropy)。


如何让模型变得更好?

我们有了损失函数,现在的问题就变成了:如何找到一组最好的 W 和 b,来最小化这个损失函数?但在开始寻找之前,我们还有一个问题要解决。

正则化 (Regularization) - 防止模型“死记硬背”

想象一个模型,它在训练集上表现完美,损失为0,但一遇到新的测试图片就出错。这种情况我们称之为过拟合 (Overfitting)。模型没有学到普适的规律,而是“死记硬背”了训练集中的所有噪声和细节。

为了防止过拟合,我们引入正则化。

核心思想:在损失函数中增加一个惩罚项,这个惩罚项与模型参数 W 的复杂度有关。我们偏好更“简单”的模型(奥卡姆剃刀原理)。

完整损失函数:

\[L = \underbrace{\frac{1}{N} \sum_{i=1}^{N} L_i}_{\text{Data Loss}} + \underbrace{\lambda R(W)}_{\text{Regularization Loss}}\]

\(R(W)\) 是正则化惩罚项。

\(\lambda\) 是一个超参数,称为正则化强度。它控制着我们有多看重“模型简单性”(大的 λ)与“拟合训练数据”(小的 λ)之间的平衡。

常见的正则化方法:

L2 正则化 (权重衰减):最常用的一种。

\[R(W) = \sum_k \sum_l W_{k,l}^2\]

它惩罚的是权重矩阵中所有元素平方和。L2 正则化倾向于让权重的值都比较小而分散,防止模型过度依赖某几个输入特征。

L1 正则化:

\[R(W) = \sum_k \sum_l |W_{k,l}|\]

它惩罚的是权重矩阵中所有元素的绝对值之和。L1 正则化会产生稀疏性,即它会促使模型中很多权重参数变为0。这在某种意义上起到了特征选择的作用。


最优化 (Optimization) - 寻找最好的 W

现在,我们有了完整的、带有正则化项的损失函数 L(W)。我们的最终目标是找到能让 L(W) 最小化的 W。

这是一个最优化问题。想象损失函数是一个连绵起伏的山谷,我们的目标是走到谷底。

核心思想:梯度下降 (Gradient Descent)

梯度 \(\nabla_W L\) 是一个向量,它指向了函数值上升最快的方向。

那么,梯度的反方向 \(-\nabla_W L\) 就是函数值下降最快的方向。

因此,我们只需要从一个随机的 W 出发,不断地沿着梯度的反方向一小步一小步地走,理论上就能最终到达某个谷底(局部最小值)。

核心更新公式:

\[W_{\text{new}} = W_{\text{old}} - \alpha \times \nabla_W L\]

\(\alpha\) 被称为学习率 (learning rate),是一个超参数,它决定了我们每一步走多大。

计算梯度:

数值梯度 (Numerical Gradient):通过有限差分法近似计算,即 \(f'(x) \approx (f(x+h) - f(x)) / h\)。它实现简单,但计算非常慢,而且只是一个近似值。通常只用于梯度检查(验证我们写的解析梯度是否正确)。

解析梯度 (Analytic Gradient):利用微积分直接推导出梯度的数学表达式。它计算速度快,结果精确。在实际训练中,我们总是使用解析梯度。后续课程的反向传播 (Backpropagation) 算法就是一种高效计算解析梯度的算法。

随机梯度下降 (Stochastic Gradient Descent, SGD)

在整个训练集上计算完整的梯度(称为批梯度下降)代价太高。实际中,我们每次只随机抽取一小部分数据,称为一个批次 (minibatch),用这个批次的数据来近似计算梯度,并更新一次 W。这种方法被称为随机梯度下降 (SGD)。虽然每次的更新方向不那么准,但更新速度极快,总体上能更快地收敛。

SGD的挑战与改进(优化器 Optimizers)

朴素的SGD有一些问题:

如果损失函数的形状是一个狭长的山谷,SGD会在峭壁上反复震荡,而在平缓的方向上进展缓慢。

容易陷入局部最小值或鞍点。

小批量带来的梯度噪声可能导致收敛不稳定。

为了解决这些问题,研究者们提出了很多改进的优化算法:

动量 (Momentum):引入一个“速度”变量,它会累积过去的梯度。想象一个从山上滚下来的球,它带有惯性,可以冲过一些小的颠簸和鞍点,并且在正确的方向上加速。

AdaGrad / RMSProp:为每个参数引入自适应的学习率。对于梯度大的参数,学习率会变小;对于梯度小的参数,学习率会变大。这在处理狭长山谷地形时特别有效。

Adam (Adaptive Moment Estimation):目前最常用、最主流的优化器之一。它结合了动量(梯度的一阶矩估计)和RMSProp(梯度的二阶矩估计)的思想,并加入了偏置校正。通常作为各种任务的默认首选优化器。

学习率调度 (Learning Rate Schedules)

学习率 α 是最重要的超参数。在训练初期,我们希望学习率大一些,以便快速下降;在训练后期,我们希望学习率小一些,以便在谷底附近进行微调,避免“跨过”最优点。因此,我们通常会使用一个学习率调度策略,在训练过程中动态地降低学习率,例如步进衰减、余弦退火等。


第四章:神经网络与反向传播

线性分类器为我们打开了参数化模型的大门,但它的表达能力有限。为了解决更复杂、非线性的问题,我们需要更强大的模型——神经网络 (Neural Networks)。


从线性到非线性:多层神经网络

核心思想:将多个线性分类器“堆叠”起来。但仅仅堆叠是不够的,关键在于在每一层之间引入非线性变换。


两层神经网络 (2-Layer Neural Network):

这是最简单的神经网络结构。它的数学表达式如下:

\[f(x) = W_2 \max(0, W_1 x + b_1) + b_2\]

第一层:\(h = W_1 x + b_1\),这和我们之前的线性分类器完全一样。计算结果 h 通常被称为隐藏层 (hidden layer) 的激活值。

非线性激活函数:\(\max(0, h)\)。这是至关重要的一步。我们对 h 的每个元素应用一个非线性函数。这里使用的是 ReLU (Rectified Linear Unit) 函数,它是目前最流行的激活函数。

第二层:\(s = W_2 \max(0, h) + b_2\)。将经过非线性变换后的隐藏层输出,再通过一个线性层,得到最终的类别分数 s。

为什么需要非线性?

如果我们去掉中间的 max(0, ...),那么模型就变成了:

\[f(x) = W_2 (W_1 x + b_1) + b_2 = (W_2 W_1) x + (W_2 b_1 + b_2)\]

\(W' = W_2 W_1\)\(b' = W_2 b_1 + b_2\),那么 \(f(x) = W'x + b'\)

你会发现,无论我们堆叠多少个没有非线性激活的线性层,最终的结果都等价于一个单独的线性层。整个网络的表达能力并没有提升。因此,非线性激活函数是赋予神经网络强大表达能力的关键。


激活函数 (Activation Functions)

激活函数的引入,使得神经网络可以拟合任意复杂的函数。除了 ReLU,还有很多其他的选择:

Sigmoid: \(\sigma(x) = \frac{1}{1+e^{-x}}\)。将输入压缩到 (0, 1) 之间。在历史上很流行,但现在由于梯度消失等问题,在隐藏层中已很少使用。

Tanh: \(\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}\)。将输入压缩到 (-1, 1) 之间。性能通常比 Sigmoid 好,但同样存在梯度消失问题。

Leaky ReLU: \(\max(0.01x, x)\)。对 ReLU 的改进,允许负值部分有一个微小的梯度,避免了神经元“死亡”的问题。(即某些神经元在训练过程中永远输出0,无法更新。)

ReLU (\(\max(0, x)\)) 是目前最常用、最高效的激活函数,通常作为首选。


终极武器:反向传播 (Backpropagation)

我们构建了一个更复杂的函数(神经网络),现在的问题又回到了如何计算梯度上来。对于一个深层的网络,手动推导梯度公式几乎是不可能的。我们需要一个通用的、自动化的方法,这就是反向传播。

核心思想:链式法则 (Chain Rule) 的巧妙应用。


计算图 (Computational Graph)

任何复杂的数学表达式都可以被分解成一系列简单的、基础的运算单元(如加、乘、max等),并将它们组织成一个有向无环图,这就是计算图。

例子: \(f(x, y, z) = (x + y) z\)

这个表达式可以分解为:\(q = x + y\)\(f = q \times z\)


反向传播过程

反向传播分为两个阶段:

  1. 前向传播 (Forward Pass):从输入开始,沿着计算图的方向,一步步计算出每个节点的值,直到最终的输出(损失值 L)。
  2. 反向传播 (Backward Pass):从最终的输出 L 开始,沿着计算图反方向,利用链式法则,一步步计算出 L 对每个中间变量和输入参数的梯度。

链式法则的直观理解:想象一个节点 f,它的输入是 x 和 y,输出是 z。

\(\frac{\partial z}{\partial x}\)\(\frac{\partial z}{\partial y}\) 是局部梯度 (local gradient),它们只关心 f 这个运算本身。

\(\frac{\partial L}{\partial z}\) 是从计算图后续节点传回来的梯度,我们称之为上游梯度 (upstream gradient)。

我们想要求的是 \(\frac{\partial L}{\partial x}\)\(\frac{\partial L}{\partial y}\),即下游梯度 (downstream gradient)。

链式法则告诉我们:

\[\text{下游梯度} = \text{上游梯度} \times \text{局部梯度}\]

具体来说:

\[\frac{\partial L}{\partial x} = \frac{\partial L}{\partial z} \frac{\partial z}{\partial x}\]
\[\frac{\partial L}{\partial y} = \frac{\partial L}{\partial z} \frac{\partial z}{\partial y}\]

反向传播就是从最终的 \(\frac{\partial L}{\partial L} = 1\) 开始,像接力一样,将梯度一层层地向后传递,每个节点接收到上游梯度后,乘以自己的局部梯度,再传递给自己的下游节点。

常见节点的梯度规律

  • 加法门 (add gate):将上游梯度等值分配给所有输入。
  • 乘法门 (mul gate):将上游梯度乘以“另一条边”的值,然后传递下去。
  • Max门 (max gate):将上游梯度传递给前向传播时值最大的那条输入,其他输入梯度为0(梯度路由器)。

总结一下

整个学习过程就是一个不断重复的循环:

  • 前向传播: 让模型根据当前权重进行一次预测,并计算出预测结果与真实答案之间的“误差”(损失 \(L\))。
  • 反向传播: 从误差出发,反向计算出网络中每一个权重对这个误差的“贡献度”或“责任”,这个“责任”就是梯度 \(\frac{\partial L}{\partial W}\)
  • 权重更新: 根据梯度下降公式 \(W_{\text{新}} = W_{\text{旧}} - \eta \cdot \frac{\partial L}{\partial W}\),对网络中的所有权重进行一次微小的、正确的调整。

通过成千上万次这样的循环,模型的所有权重就会被“推”向一个能使总损失最小化的最优组合,模型也就“学会”了如何解决问题。


向量化操作

在实际应用中,我们的操作都是基于向量和矩阵的。反向传播同样适用,只是局部梯度变成了雅可比矩阵 (Jacobian Matrix)。但我们不需要显式地去构造和存储巨大的雅可比矩阵,因为现代深度学习框架(如 PyTorch, TensorFlow)已经将这些矩阵-向量乘法优化为高效的底层操作。我们只需要记住一个原则:梯度 \(\nabla_X L\) 的形状总是和变量 X 本身的形状保持一致。


第六章:卷积神经网络 (CNN)

全连接网络(Fully-Connected Network)有一个致命的弱点:它忽略了图像的空间结构。它在处理图像前,会粗暴地将一个三维的图像矩阵(高 x 宽 x 颜色通道)拉平成一个一维的长向量。这样做丢失了像素之间的邻域关系,并且导致参数量巨大。

卷积神经网络(Convolutional Neural Networks, CNN)通过引入卷积层和池化层,完美地解决了这个问题。


核心组件一:卷积层 (Convolutional Layer)

核心思想:用小的、可学习的滤波器 (Filter) 在图像的局部区域上进行滑动计算,以提取局部特征(如边缘、颜色、纹理)。

工作原理:

  1. 输入与输出皆为“ (Volume)”:CNN中流动的数据不再是向量,而是保持空间结构的三维张量(高 x 宽 x 深度)。输入层就是原始图像(如 32x32x3)。
  2. 局部连接 (Local Connectivity):卷积层的神经元(或说滤波器)不再连接到输入的所有像素,而只连接到一个小的局部区域,这个区域被称为该神经元的感受野 (Receptive Field)。
  3. 滤波器 (Filter / Kernel):滤波器是一个小的权重矩阵(例如 5x5x3)。它的深度必须与输入体的深度相同。它就像一个特征探测器,负责探测特定的局部模式。
  4. 卷积运算:我们将滤波器在输入体上进行滑动。在每一个位置,我们将滤波器与它覆盖的输入区域进行点积(element-wise multiplication then sum),并加上一个偏置项。这个点积的结果就是输出体中一个像素的值。
  5. 激活图 (Activation Map):一个滤波器在整个输入体上滑动一遍后,会生成一个二维的矩阵,称为激活图或特征图 (Feature Map)。这张图上的每个点,都代表了该滤波器在输入图像对应位置的响应强度。
  6. 输出体:一个卷积层通常包含多个不同的滤波器,每个滤波器负责学习一种不同的特征。例如,如果我们有6个 5x5x3 的滤波器,将它们都作用于 32x32x3 的输入图像上,我们就会得到6个不同的激活图。将这6个激活图堆叠起来,就构成了卷积层的输出体(例如 28x28x6)。
  • 参数共享 (Parameter Sharing):这是CNN最核心、最天才的设计之一。一个激活图上的所有神经元,共享同一套滤波器权重和偏置。这意味着,我们用同一个特征探测器去扫描图像的所有位置。这基于一个合理的假设:如果一个特征(比如一个水平边缘)在图像的某个位置很重要,那么它在其他位置也可能很重要

巨大优势:极大地减少了模型的参数量,使得训练深层网络成为可能。


控制空间尺寸的超参数

卷积操作会改变输出体的大小,这由三个超参数控制:

步长 (Stride, S):滤波器每次滑动的距离。步长为1表示逐像素滑动。步长大于1会起到下采样的效果,缩小输出尺寸。

填充 (Padding, P):在输入体的边缘周围填充0。这非常有用,最常见的用途是保持空间尺寸不变。如果我们想让输出和输入的宽高一样,可以设置 \(P = (K-1)/2\)(当步长S=1时),其中K是滤波器尺寸。

滤波器尺寸 (Filter Size, K):通常使用小的正方形滤波器,如 3x3 或 5x5。

输出尺寸计算公式:

对于一个尺寸为 W 的输入,其输出尺寸 W_out 为:

\[W_{\text{out}} = \frac{W - K + 2P}{S} + 1\]

这个公式对高和宽同样适用。


池化层 (Pooling Layer)

核心思想:对特征图进行下采样 (Downsampling),以减小其空间尺寸。

工作原理:

最常见的池化操作是最大池化 (Max Pooling)。它也是在输入体上滑动一个窗口(例如 2x2),但它不是做点积,而是简单地取窗口内的最大值作为输出。

主要作用:

  1. 减小计算量:通过减小空间维度,后续层的参数量和计算量会大幅下降。
  2. 增加感受野:使得后续的卷积层能够看到更广阔的原始输入区域。
  3. 提供平移不变性:使得模型对特征的微小位置变化不那么敏感,增强了模型的鲁棒性。

关键特性:池化层没有可学习的参数,它只是一个固定的下采样操作。


典型的CNN架构

一个典型的卷积神经网络由一系列层堆叠而成,其结构通常如下:

\[\text{INPUT} \rightarrow [[\text{CONV} \rightarrow \text{RELU}]*N \rightarrow \text{POOL} ?]*M \rightarrow [\text{FC} \rightarrow \text{RELU}]*K \rightarrow \text{FC} \rightarrow \text{SOFTMAX}\]

输入层 (INPUT):原始图像。

卷积基 (Convolutional Base):由多个 [CONV-RELU-POOL] 模块堆叠而成。这部分是CNN的特征提取器。

CONV层负责提取局部的、低层次的特征。

RELU引入非线性。

POOL层负责降维和增加不变性。

随着网络的加深,卷积层提取的特征也从简单的边缘、颜色,逐渐变为更复杂的纹理、部件,甚至是物体。

全连接层 (Fully-Connected Layers, FC):在卷积基的最后,我们会将得到的高维特征图“压平”成一个一维向量,然后送入一个或多个全连接层。这部分的作用类似于一个分类器,它将学习到的高级特征进行组合,并最终映射到类别分数上。

输出层 (Output):通常是一个带有 Softmax 的全连接层,用于输出最终的分类概率。

整个CNN是一个端到端 (end-to-end) 的可微函数,我们可以用上一章学到的反向传播和梯度下降来训练网络中的所有可学习参数(主要是卷积层的滤波器权重和全连接层的权重)。


第七章:搭建更好的CNN

我们已经了解了CNN的基本构件。现在,我们将深入探讨如何将这些构件组合成强大而高效的网络架构,并介绍一些让训练过程更稳定、更快速的关键技术。


激活函数的再思考

我们在第五章提到了激活函数的重要性。这里我们深入探讨为什么 ReLU 及其变体在深度网络中远胜于 Sigmoid

  • Sigmoid/Tanh 的问题:梯度消失 (Vanishing Gradients)
  • Sigmoid 函数 \(\sigma(x) = 1/(1+e^{-x})\) 的一个主要问题是,当输入 x 的绝对值很大时(例如 x > 5 或 x < -5),函数的曲线变得非常平坦,其梯度(导数)几乎为0。

在反向传播中,梯度是逐层相乘的(链式法则)。如果网络很深,并且使用了 SigmoidTanh 激活函数,那么梯度在向后传递的过程中会不断地乘以一个接近0的数,导致梯度迅速减小,甚至消失。这使得网络的前几层几乎无法接收到有效的梯度信号,参数更新极其缓慢,网络难以训练。这就是梯度消失问题。

ReLU 的优势与问题

ReLU (\(f(x) = \max(0, x)\)) 的出现极大地缓解了梯度消失问题:

  • 优势:当输入 \(x > 0\) 时,梯度恒为1。这使得梯度在反向传播时能够很好地流动,不会因为连乘而衰减。同时,它的计算非常高效。
  • 问题:“死亡ReLU” (Dying ReLU):如果一个神经元的输入恒为负数,那么它的输出将永远是0,梯度也永远是0。这个神经元将不再对任何数据有响应,也无法通过梯度下降进行更新,就像“死掉”了一样。

ReLU 的变体

为了解决“死亡ReLU”问题,研究者们提出了一些变体:

  • Leaky ReLU: \(f(x) = \max(0.01x, x)\)。它在负半轴引入了一个微小的、固定的斜率(如0.01),确保神经元在输入为负时也能有非零梯度。

  • ELU (Exponential Linear Unit): 在负半轴使用指数函数,使其输出均值更接近0,并且具有一定的鲁棒性。

  • GELU (Gaussian Error Linear Unit): \(f(x) = x \cdot \Phi(x)\) (其中 \(\Phi(x)\) 是高斯分布的累积分布函数)。它的曲线更平滑,在现代的Transformer架构中非常流行。

实践中的选择:优先使用 ReLU,如果遇到“死亡ReLU”问题,可以尝试 Leaky ReLU 等变体。


让训练更稳定:归一化层 (Normalization Layers)

训练深度网络是困难的,因为每一层的参数更新都会改变其后所有层的输入分布,这种现象被称为内部协变量偏移 (Internal Covariate Shift)。这迫使网络需要不断适应新的数据分布,减慢了训练速度。

批量归一化 (Batch Normalization, BN) 是一个里程碑式的工作,它极大地稳定和加速了深度网络的训练。

核心思想:在网络的每一层激活函数之前,强行将输入的数据分布“拉回”到一个标准的正态分布(均值为0,方差为1)。

工作流程(在训练时):对于一个 minibatch 的数据:

  1. 计算这个 minibatch 中数据在每个特征维度上的均值 \(\mu_B\) 和方差 \(\sigma_B^2\)
  2. 对该 minibatch 的每个样本进行归一化:\(\hat{x}_i = \frac{x_i - \mu_B}{\sqrt{\sigma_B^2 + \epsilon}}\) ( \(\epsilon\) 是一个很小的数,防止除以0)。
  3. 进行缩放和平移:\(y_i = \gamma \hat{x}_i + \beta\)。其中 \(\gamma\) (缩放因子) 和 \(\beta\) (平移因子) 是可学习的参数。这一步是为了恢复网络的表达能力,如果网络发现原始分布是最好的,它可以通过学习 \(\gamma = \sqrt{\sigma_B^2}\)\(\beta = \mu_B\) 来还原。

测试时的不同:在测试时,我们可能一次只处理一张图片,没有 minibatch 的概念。因此,我们使用在整个训练过程中通过移动平均计算得到的全局均值和方差来进行归一化。

BN巨大优势

  • 加速训练:允许使用更高的学习率。
  • 稳定训练:减少了对权重初始化的敏感度。
  • 自带正则化效果:由于均值和方差是在 minibatch 上计算的,带有一定的噪声,这为模型训练带来了轻微的正则化效果,有时可以替代 Dropout

赢在起跑线:权重初始化 (Weight Initialization)

权重定义了一个神经元对另一个神经元的影响强度(和方向)。“学习”或“训练”的整个过程,就是不断调整这些权重(\(w\))和偏置项(\(b\))的值。训练的目标是找到一套最优的权重,使得网络对给定的输入数据,能产生最接近正确答案的输出(即最小化损失函数)。这个调整过程通常使用“反向传播”(Backpropagation)算法实现,该算法会计算输出误差对每个权重的“贡献”(即梯度),然后沿着梯度的反方向微调权重。

如果权重初始化不当,深度网络同样难以训练。

  • 太小:如果权重初始值非常接近0,经过多层网络后,激活值会迅速衰减到0,导致梯度也为0(梯度消失)。
  • 太大:如果权重初始值很大,经过多层网络后,激活值会变得极大,导致梯度也极大(梯度爆炸),使得训练发散。

我们需要一种能让各层激活值的方差保持一致的初始化方法。

Kaiming / He 初始化: 这是专门为 ReLU 网络设计的、目前最常用的初始化方法。

核心思想:为了让 ReLU 层的输出方差与输入方差保持一致,权重的采样标准差应该是 \(\sqrt{2/n_{in}}\),其中 \(n_{in}\) 是该层的输入维度。

实现:W = np.random.randn(fan_in, fan_out) * np.sqrt(2.0 / fan_in)


CNN架构的“进化史”

让我们来看几个里程碑式的CNN架构,了解它们是如何一步步变得更深、更强的。

  1. AlexNet (2012):深度学习革命的引爆点。它首次证明了深度 CNNImageNet 大规模图像分类任务上的巨大潜力。

贡献:使用了更深的网络(8层),首次成功应用 ReLUDropout 和数据增强等技术。

  1. VGGNet (2014):探索了深度的重要性。

核心设计:非常简单、统一的架构,只使用 3x3 的小卷积核和 2x2 的最大池化层。通过不断堆叠 3x3 卷积层来增加网络深度(达到16-19层)。

为什么用小的 3x3 卷积核?

  • 等效感受野:堆叠两个 3x3 卷积层的感受野等同于一个 5x5 卷积层,三个 3x3 等同于一个 7x7。
  • 更多非线性:堆叠多层可以引入更多的 ReLU 激活,增强了网络的表达能力。
  • 更少参数:假设输入和输出通道数都为C,三个 3x3 卷积层的参数量为 \(3 \times (3^2 C^2) = 27C^2\),而一个 7x7 卷积层的参数量为 \(7^2 C^2 = 49C^2\),前者更少。
  1. ResNet (Residual Networks, 2015):解决了超深度网络的训练问题。

面临的问题网络退化 (Degradation)

当网络变得非常深时(例如56层 vs 20层),人们发现更深的网络在训练集和测试集上的错误率反而更高了。这并非过拟合,而是因为深度网络难以优化

核心创新:残差块 (Residual Block)

ResNet 的设计者提出了一个天才般的想法:与其让网络层直接学习一个目标映射 \(H(x)\),不如让它学习一个残差映射 (Residual Mapping) \(F(x) = H(x) - x\)

\(x\) 代表在网络进行到这一步时,已经从原始数据中学到的特征图(Feature Map)。

一个具体的例子

假设一个20层的网络,我们把第10层和第11层打包成一个残差块:原始图像输入到第1层、第2层……第9层。第9层会输出一个结果(一个特征图)。这个结果就是第10层残差块的输入 \(x\)。这个 \(x\) 现在兵分两路:

  1. 主路(\(F(x)\)): \(x\) 正常地进入第10层和第11层进行计算(例如:卷积 -> ReLU -> 卷积),得到一个输出,这个输出就是 \(F(x)\)

  2. 快捷连接(\(x\)): \(x\) 什么也不做,直接“跳过”第10层和第11层。

在第11层的出口处,两路汇合:\(H(x) = F(x) + x\)。(即将“主路”的结果 \(F(x)\) 和“跳过来”的原始 \(x\) 逐元素相加)。这个 \(H(x)\) 才是整个残差块的最终输出,它将作为第12层的输入继续传递下去。

原始的映射就变成了 \(H(x) = F(x) + x\)

这通过一个“快捷连接 (Shortcut/Skip Connection)”来实现:将输入 x 直接“跳过”几个层,加到这几个层的输出 \(F(x)\) 上。

为什么有效?

  • 当网络很深时,假设我们已经有了一个很好的特征 \(x\)(比如前20层已经学得很好),我们希望后面的层(比如第21、22层)至少不要把它变差。
  • 如果某几层是不需要的,对于普通网络,它需要学习将这几层变成一个恒等映射 (\(H(x)=x\)),这很难。但对于残差块,它只需要学习让 \(F(x)=0\) 即可,这非常容易(通过权重衰减,权重很容易变为0)。

这个简单的改动,使得梯度可以在反向传播时通过“快捷连接”直接流向前面的层,极大地缓解了梯度消失问题,让训练超过100层甚至1000层的网络成为可能。ResNetCNN 发展史上最重要的里程碑之一。


第八章:训练的“艺术”:实用技巧指南

拥有一个好的架构只是第一步,如何“喂养”和“调教”它,同样决定了模型的最终表现。


数据预处理 (Data Preprocessing)

对于图像数据,最常见的预处理是中心化归一化

操作:计算整个训练集中所有图像在每个颜色通道(R, G, B)上的像素均值和标准差。然后,对于每一张图片,将其每个通道的像素值减去对应通道的均值,再除以标准差。

目的:使得所有输入特征的分布都大致在0附近,方差为1。这有助于优化过程的稳定和快速收敛。


数据增强 (Data Augmentation)与Dropout正则化

数据增强 (Data Augmentation)

这是最常用、最有效的正则化方法之一,它通过在训练时对图像进行随机变换,来人为地创造更多的训练数据。

核心思想:对一张图片进行微小的改动(如翻转),其标签不应改变。这教会了模型什么是“不变性”。

常用方法

  1. 水平翻转 (Horizontal Flips)
  2. 随机裁剪与缩放 (Random Crops and Scales):在训练时,随机地从一张大图上裁剪不同位置、不同大小的区域,然后缩放到固定尺寸输入网络。
  3. 颜色抖动 (Color Jitter):随机改变图像的亮度、对比度、饱和度等。
  4. Cutout / Random Erasing: 随机地在图像上遮掉一小块区域。这强迫模型不能只关注图像的某个局部,而是要从全局理解。

进阶正则化:Dropout

除了 L1/L2 正则化和数据增强,我们还有一种非常强大、思想奇特的正则化方法,专门为神经网络设计,它就是Dropout。

核心思想 (训练时): 在每次前向传播时,随机地“丢弃”(即暂时将输出置为0)网络中的一部分神经元。

操作: 对于某一层神经元,我们以一个预设的概率 \(p\) (称为保留概率, keep probability) 来决定每个神经元是否“工作”。反之,以 \(1-p\) 的概率被“关闭”。\(p\) 是一个超参数,通常设为0.5。

效果: 在每次训练迭代中,我们实际上都在训练一个不同的、“变瘦了”的子网络。

为什么Dropout有效?两大直观解释
  1. 强迫网络学习冗余表示 (Forcing Redundant Representations):

想象一下,网络为了识别“猫”,可能依赖于几个关键神经元,比如一个“耳朵探测器”、一个“胡须探测器”和一个“爪子探测器”。如果“胡须探测器”在某次训练中被随机“丢弃”了,网络为了做出正确的预测,就必须学会利用其他特征(比如“眼睛形状”、“毛茸茸的纹理”)来弥补。这强迫网络不能过度依赖少数几个神经元,而是要学习到更加健壮和冗余的特征组合,从而防止了神经元之间的“共适应 (co-adaptation)”现象,提高了泛化能力

  1. 模型集成 (Ensemble) 的思想:

从另一个角度看,Dropout 可以被视为一种高效的模型集成方法。每次随机丢弃神经元,我们都在训练一个不同的、更小的子网络。一个拥有 \(N\) 个神经元的层,就有 \(2^N\) 种可能的子网络。在整个训练过程中,我们实际上是在训练一个由海量子网络构成的庞大集成模型。

在测试时,我们使用完整的网络(不丢弃任何神经元),这在效果上近似于对所有这些子网络的预测结果进行平均。我们知道,集成多个不同模型的预测结果,通常会比单个模型的性能更好、更稳定。Dropout用一种巧妙的方式实现了这种集成效果,而无需真正独立地训练成千上万个模型。

关键细节:训练与测试的差异

这是理解Dropout最重要的地方。我们在训练时随机丢弃神经元,但在测试时,我们希望使用整个网络的全部能力,因此不会丢弃任何神经元。

但这会带来一个问题:在训练时,一个层的输出激活值(神经元的激活值 (Activation Value) 就是这个神经元的输出值)的期望值,会因为部分神经元被丢弃而变小。例如,如果保留概率 \(p=0.5\),那么该层输出的期望值会是完整网络时的一半。为了补偿这种差异,确保训练和测试时输出的“尺度”一致,我们需要进行缩放。

实现方法:反向 Dropout (Inverted Dropout)

最常用、也是所有现代框架中的标准实现方法是“反向Dropout”。

核心思想: 在训练时进行缩放,而不是在测试时。

操作: 在前向传播时,一个层在随机丢弃了部分神经元后,将其余保留下来的神经元的激活值除以保留概率 \(p\) (即乘以 \(1/p\))。


迁移学习 (Transfer Learning)

如果你自己的任务没有海量的数据(通常情况),从零开始训练一个深度 CNN 是非常困难的。迁移学习是解决这个问题的金钥匙。

核心思想:不要从随机初始化的权重开始训练,而是使用一个已经在大规模数据集(如ImageNet,包含百万级图片和1000个类别)上预训练好 (pre-trained) 的模型作为起点。

为什么有效?

在一个大数据集上训练好的CNN,它的浅层学到的是非常通用的特征(如边缘、颜色、纹理),中层学到的是更复杂的部件(如眼睛、轮子),这些特征对于很多其他的视觉任务都是有用的。

实践策略

  1. 作为固定的特征提取器:对于数据量非常小的新任务,可以“冻结”预训练模型的所有卷积层(不更新它们的权重),只将它们作为特征提取器。然后,只训练最后新加的全连接分类层。
  2. 微调 (Fine-tuning):这是最常用的策略。用预训练的权重来初始化整个网络,然后用你的新数据来继续训练整个网络(或者网络的最后几层)。关键是,在微调时要使用一个非常小的学习率,以免破坏预训练好的权重。
  3. 经验法则:你的数据集越大、与原始数据集(如ImageNet)越相似,你就可以微调越多的层,使用越高的学习率。

超参数调优 (Hyperparameter Tuning)

如何找到最佳的学习率、正则化强度等超参数?这是一个“炼丹”的过程,但也有章可循。

系统化流程

  1. 检查初始损失:在训练开始前,用初始权重计算一下损失值,确保它符合预期(例如,对于 Softmax ,初始损失(也就是瞎猜时的损失扣分)应约为 \(\log(C)\),C是类别数)。
  2. 在小数据上过拟合:取一小部分(如几十张)训练数据,关闭正则化,尝试让网络在这个小数据上达到100%的训练准确率。如果做不到,说明你的模型结构或初始化可能有问题。
  3. 寻找合适的学习率:使用全部数据,配合一个较小的正则化强度,尝试几个数量级的学习率(如 1e-1, 1e-2, ..., 1e-5),找到一个能让损失稳定下降的学习率。
  4. 粗略搜索 -> 精细搜索:围绕找到的最佳学习率和正则化强度,进行更大范围的随机搜索 (Random Search)(通常比网格搜索(系统地尝试所有组合)更高效),训练几个周期(epoch),找到几个有潜力的超参数组合。
  5. 完整训练与观察曲线:用最有潜力的超参数组合进行完整的训练,并密切关注训练集和验证集的准确率/损失曲线。

Train Acc 是训练集上的准确率,Val Acc 是验证集上的准确率。

  • Train/Val Acc 差距大:说明过拟合。对策:增加正则化(L2、Dropout、数据增强)。
  • Train/Val Acc 差距小,但都很低:说明欠拟合。对策:模型容量可能不足,可以尝试更深/更宽的网络;或者训练更长时间。
  • 两条曲线都在平稳上升:说明还可以继续训练。

第九章:循环神经网络(RNN)

这节课我们进入了一个激动人心的新领域:如何让神经网络处理序列数据,比如文字、语音和视频。我们将深入探讨循环神经网络(RNN)的核心思想、它的挑战以及像LSTM这样的高级变体是如何解决这些挑战的。

课堂上有一个很好的问题:如果我的权重初始化得很糟糕,可以用归一化(Normalization)来拯救吗?

归一化不能完全替代良好的权重初始化,但它是处理糟糕初始化的强大工具。 在实践中,我们应该同时使用良好的初始化方法(如Xavier、He初始化)和归一化技术(如BatchNorm、LayerNorm),以确保网络训练得又快又好。

为什么需要“良好”的初始化方法?

好的,这是一个非常好的问题!您在Canvas笔记中读到了“我们应该同时使用良好的初始化方法(如Xavier、He初始化)”,下面我为您详细解释这两种最常用且最重要的权重初始化方法。

如果权重太小,信号(激活值)和梯度会逐层衰减直至消失(梯度消失);如果权重太大,它们又会逐层放大直至爆炸(梯度爆炸)。

一个“良好”的初始化方法,其核心目标就是让信号在网络中前向传播和反向传播时,其方差(可以理解为信号的“能量”或“尺度”)能够尽可能保持不变。这样,信息流就不会在中途“死亡”或“失控”,从而保证网络可以被有效训练。

  1. Xavier / Glorot 初始化

这是由Xavier Glorot和Yoshua Bengio在2010年提出的方法,是早期深度学习领域的开创性工作之一。

核心思想与动机:Xavier初始化主要针对的是对称的激活函数,比如 tanhsigmoid。这些函数的输出均值大约为0。该方法试图在“前向传播时激活值的方差”和“反向传播时梯度的方差”之间取得一个平衡。

数学推导:为了保持方差不变,需要满足两个条件:

  1. 前向传播:为了让输出的方差等于输入的方差,权重的方差需要满足 \(Var(W) = \frac{1}{n_{in}}\)

  2. 反向传播:为了让梯度的方差等于反向输入梯度的方差,权重的方差需要满足 \(Var(W) = \frac{1}{n_{out}}\)

其中:

  • \(n_{in}\) 是输入神经元的数量(也叫 fan-in)。
  • \(n_{out}\) 是输出神经元的数量(也叫 fan-out)。

这两个条件显然是矛盾的,无法同时满足。于是,Xavier做了一个折中,取了两者的平均值:

\[Var(W) = \frac{2}{n_{in} + n_{out}}\]

实用方法:在实践中,我们从以下两种分布中随机采样来初始化权重:

  1. 均匀分布 (Uniform Distribution):
\[W \sim U\left[-\frac{\sqrt{6}}{\sqrt{n_{in} + n_{out}}}, \frac{\sqrt{6}}{\sqrt{n_{in} + n_{out}}}\right]\]
  1. 正态分布 (Normal Distribution):

    权重从均值为0,方差为 \(\sigma^2 = \frac{2}{n_{in} + n_{out}}\) 的高斯分布中采样。

当你的网络主要使用 tanhsigmoid 作为激活函数时,Xavier初始化是一个非常好的选择。

  1. He 初始化

这是由何恺明(Kaiming He)等人在2015年提出的方法,主要是为了解决Xavier初始化在ReLU激活函数上的不足。它也是现代深度网络中最常用的初始化方法。

核心思想与动机:ReLU(及其变体,如Leaky ReLU)成为了现代神经网络的主流选择,但它是一个非对称的激活函数。当输入小于0时,它的输出恒为0。这意味着,在前向传播中,大约一半的神经元输出会变成0,这会导致信号的方差减半。Xavier初始化没有考虑到这一点,因此在使用ReLU时,信号的方差依然会逐层衰减,导致网络过深时仍然会出现梯度消失问题。He初始化的核心思想就是专门为ReLU激活函数进行修正

数学推导:由于ReLU会将方差减半,为了补偿这一点,我们在推导前向传播的方差时,需要在分母上除以2。这样,为了保持输出方差不变,权重的方差需要满足:

\[Var(W) = \frac{2}{n_{in}}\]

这个方法只考虑了前向传播的方差(因为它认为这对于训练更重要),所以公式比Xavier更简单。

实用方法:我们从以下分布中随机采样来初始化权重。正态分布 (Normal Distribution):权重从均值为0,方差为 \(\sigma^2 = \frac{2}{n_{in}}\) 的高斯分布中采样。

当你的网络主要使用 ReLU 及其变体(Leaky ReLU, PReLU等)作为激活函数时,一定要使用He初始化。 这是目前构建大多数现代卷积神经网络和深度前馈网络的标准做法。

到目前为止,我们处理的都是固定大小的输入,比如一张图片。但现实世界中充满了序列数据,比如一句话(单词序列)、一段视频(图像帧序列)。为了处理这类数据,我们需要一种新的网络结构。


为什么需要RNN?

传统的“香草”神经网络(Vanilla Neural Network)是“一对一”的结构(一个输入,一个输出)。但序列任务需要更灵活的结构:

  • 一对多 (One to Many): 输入一个东西,输出一个序列。

例子:图像描述 (Image Captioning)。 输入一张图片,输出一句描述文字(“一只猫坐在垫子上”)。

  • 多对一 (Many to One): 输入一个序列,输出一个东西。

例子:情感分析。 输入一句话(“这部电影太棒了!”),输出一个情感分类(“正面”)。

  • 多对多 (Many to Many): 输入一个序列,输出一个序列。

例子1(同步输出):视频逐帧分类。 输入一段视频,对视频的每一帧都进行分类。

例子2(延迟输出):机器翻译。 输入一句中文(“你好”),输出一句英文(“Hello”)。需要先“读”完整个输入序列,再开始“写”输出序列。

这些任务都无法用简单的CNN或全连接网络直接解决,于是循环神经网络 (Recurrent Neural Network, RNN) 应运而生。


RNN的核心思想:状态与循环

核心思想: RNN引入了一个“内部状态” (Internal State),也叫“隐藏状态” (Hidden State),我们用 \(h\) 表示。这个状态就像是网络的“记忆”,它会在处理序列的每一步中不断被更新

循环结构

RNN的结构图中有一个指向自身的环路。这表示在处理序列的下一个元素时,网络不仅会考虑当前的输入 \(x_t\),还会考虑它上一个时刻的记忆 \(h_{t-1}\)

为了更好地理解,我们通常将这个环路沿时间轴展开(Unrolled),变成一个链式结构。你会发现,这其实是同一个RNN单元在不同时间步的重复使用。


RNN的数学定义

RNN的工作可以被两个核心公式定义:

隐藏状态的更新:

\[h_t = f_W(h_{t-1}, x_t)\]

这个公式是RNN的灵魂。它说明:

  • 在时间步 \(t\) 的新状态 \(h_t\),是由一个函数 \(f_W\) 计算得出的。
  • 这个函数的输入是上一个时间步的旧状态 \(h_{t-1}\) 和当前时间步的输入 \(x_t\)
  • \(W\) 代表这个函数所使用的参数(权重)。
  • 至关重要的一点: 在所有的时间步中(也就是同一个RNN单元在不同时间步的重复使用),我们使用的都是完全相同的函数 \(f_W\) 和完全相同的参数 \(W\)。这就是所谓的“参数共享”,它极大地减少了模型的参数量,并使得模型可以处理任意长度的序列。

输出的生成:

\[y_t = f_{W_y}(h_t)\]
  • 这个公式说明,在时间步 \(t\) 的输出 \(y_t\) 是由当前时刻的隐藏状态 \(h_t\) 决定的。

Vanilla RNN 的具体形式

上面定义的 \(f_W\) 是一个抽象函数,最简单、最基础的实现方式被称为Elman RNN。

它的具体公式如下:

\[h_t = \tanh(W_{hh}h_{t-1} + W_{xh}x_t + b_h)\]
\[y_t = W_{hy}h_t + b_y\]
  • \(x_t\): 当前时间步的输入向量。
  • \(h_{t-1}\): 上一时间步的隐藏状态向量。
  • \(h_t\): 当前时间步的新隐藏状态向量。
  • \(W_{xh}\): 输入到隐藏层的权重矩阵。
  • \(W_{hh}\): 从上一隐藏状态到当前隐藏状态的权重矩阵(循环连接的权重)。
  • \(W_{hy}\): 从隐藏层到输出层的权重矩阵。
  • \(b_h, b_y\): 偏置项 (Bias)。
  • \(\tanh\): 双曲正切函数,是一个激活函数,将输出值压缩到-1到1之间。

手写一个简单的RNN

任务: 给定一个0和1组成的序列,当且仅当连续两个输入都是1时,输出1,否则输出0。

例如,输入 [0, 1, 0, 1, 1, 1, 0],期望输出 [0, 0, 0, 0, 1, 1, 0]。

设计思路

隐藏状态需要记录什么? 为了判断当前和上一个输入是否都为1,我们的“记忆” \(h_t\) 至少需要包含当前输入值和上一个输入值的信息。

设计 \(h_t\) 的结构: 我们将隐藏状态设计成一个3维向量:\(h_t = [\text{当前输入值}, \text{上一个输入值}, 1]^T\)。第三个元素 1 是为了方便引入偏置项。

设计权重矩阵

目标1:把当前输入 \(x_t\) 存入 \(h_t\) 的第一个位置。

\(h_t = \text{ReLU}(W_{hh}h_{t-1} + W_{xh}x_t)\)

我们看 \(W_{xh}x_t\) 这一项。\(x_t\) 是一个标量(0或1)。要把它放到一个3维向量的第一个位置,我们可以让 \(W_{xh} = [1, 0, 0]^T\)。这样,如果 \(x_t=1\), \(W_{xh}x_t=[1,0,0]^T\)

目标2:把 \(h_{t-1}\) 的“当前输入值”传递给 \(h_t\) 的“上一个输入值”位置。

我们看 \(W_{hh}h_{t-1}\) 这一项。\(h_{t-1} = [\text{当前}_{t-1}, \text{上一个}_{t-1}, 1]^T\)。我们需要把 \(\text{当前}_{t-1}\) 复制到新状态的第二个位置。

可以设计 \(W_{hh}\) 的第二行为 [1, 0, 0]。这样 \(W_{hh}h_{t-1}\) 的第二个元素就是 \(\text{当前}_{t-1}\)

目标3:计算输出 \(y_t\)

\(y_t = \text{ReLU}(W_{hy}h_t)\)

我们希望 \(y_t=1\) 的条件是“当前输入=1”且“上一个输入=1”。这相当于计算 ReLU(当前 + 上一个 - 1) (因为只有当两者都为1时,1+1-1=1>0)。

所以,我们可以设计 \(W_{hy} = [1, 1, -1]\)

关键启示: 这个例子告诉我们,RNN的隐藏状态确实可以编码和传递信息,而权重矩阵 \(W\) 定义了信息如何被处理和转换。在实际应用中,我们当然不会手动设计这些权重,而是通过反向传播算法让模型自己学习到这些合适的权重。


如何训练RNN —— BPTT算法

我们已经知道RNN如何工作,那么如何训练它,即如何学习到权重矩阵 \(W\) 呢?答案是随时间反向传播 (Backpropagation Through Time, BPTT)。

计算图视角

将RNN沿时间轴展开后,它就变成了一个非常深的前馈神经网络。关键点在于,所有时间步的RNN单元共享同一套权重 \(W\)

因此,总的损失 \(L\) 是所有时间步损失的总和:\(L = \sum_{t=1}^{T} L_t\)

对共享参数 \(W\) 的总梯度,也是所有时间步计算出的梯度的总和:\(\frac{\partial L}{\partial W} = \sum_{t=1}^{T} \frac{\partial L_t}{\partial W}\)

BPTT算法流程

  1. 前向传播: 将整个输入序列 \(x_1, x_2, \dots, x_T\) 输入展开的RNN,计算出每一步的隐藏状态 \(h_t\)、输出 \(y_t\) 和损失 \(L_t\)
  2. 反向传播: 从最后的总损失 \(L\) 开始,沿着展开的计算图一路向后计算梯度,直到最开始的时间步。
  3. 累加梯度: 将每个时间步计算出的对 \(W\) 的梯度累加起来,得到最终的总梯度。
  4. 更新权重: 使用梯度下降法(如Adam、SGD)更新权重 \(W\)

长序列的挑战与截断BPTT (Truncated BPTT) :

问题: 如果序列非常长(比如一整篇文章),将整个序列展开成一个巨大的计算图,需要极大的内存,并且计算量非常大。

解决方案:截断BPTT。

做法

  1. 将长序列切分成很多个小的数据块 (chunks)。
  2. 前向传播时,正常地处理整个序列,并将每个数据块末尾的隐藏状态 \(h_t\) 传递给下一个数据块作为初始状态。这样,信息依然可以在整个序列中流动。
  3. 反向传播时,只在当前的数据块内部进行。也就是说,梯度只回传一小段固定的步数。

这是一种近似计算,但在实践中非常有效。它允许我们在处理很长的序列时,既能保留一定的长期记忆(通过前向传递的隐藏状态),又能将计算和内存成本控制在可接受的范围内。


实战RNN —— 字符级语言模型

语言模型是RNN最经典、最成功的应用之一。它的任务是:预测序列中的下一个元素。

字符级语言模型 (Character-level Language Model):

任务: 给定一个字符,预测下一个可能出现的字符。

  1. 数据准备
  • 词汇表 (Vocabulary): 训练文本中所有出现过的独立字符,例如 [h, e, l, o]。
  • 输入/目标: 将训练文本(如 "hello")转换成输入序列 ['h', 'e', 'l', 'l'] 和对应的目标序列 ['e', 'l', 'l', 'o']。
  • 编码: 使用 One-hot 编码 将每个字符转换成向量。例如,如果词汇表是 [h,e,l,o],那么 'h' 就是 [1,0,0,0],'e' 就是 [0,1,0,0]。
  1. 模型结构与训练
  • 输入层: 输入当前字符的one-hot向量 \(x_t\)
  • 隐藏层: 根据 \(x_t\)\(h_{t-1}\) 计算出新的隐藏状态 \(h_t\)\(h_t\) 编码了到目前为止的序列信息,比如“刚刚看到了h,现在看到了e”。
  • 输出层: 将 \(h_t\) 通过一个权重矩阵 \(W_{hy}\) 转换成一个大小等于词汇表大小的得分向量 (scores)。这个向量的每一个元素对应词汇表中一个字符的“可能性”得分。
  • Softmax: 将得分向量通过Softmax函数,转换成一个概率分布。这个概率分布告诉我们,下一个字符是'h'的概率是多少,是'e'的概率是多少,等等。
  • 损失函数: 使用交叉熵损失函数,来衡量模型预测的概率分布与真实下一个字符(one-hot目标向量)之间的差距。然后通过BPTT算法来最小化这个损失。

在测试时进行采样 (Sampling):

模型训练好之后,就可以用来生成新的文本。

过程

  1. 给模型一个“种子”字符作为初始输入 \(x_1\) (例如 'h')。
  2. 模型前向传播,计算出下一个字符的概率分布。
  3. 我们从这个概率分布中随机采样一个字符作为 \(y_1\) (比如,采样到了 'e')。
  4. 关键一步: 将刚刚生成的字符 'e' 作为下一个时间步的输入 \(x_2\),反馈给模型。
  5. 重复这个过程,模型就会一个接一个地生成字符,形成新的文本。

Andrej Karpathy 的博客展示了用这种简单的RNN模型,在莎士比亚十四行诗、Linux源代码等不同文本上训练后,都能生成风格非常相似的文本,效果惊人。


RNN的可视化与理解 :

研究人员发现,RNN的隐藏状态中的某些神经元(cell)会自发地学习到一些有意义的、可解释的模式。例如:

  • 引号检测神经元: 在进入和离开引号时,该神经元会被激活或抑制。
  • 行长追踪神经元: 随着一行代码变长,该神经元的值会线性增加。
  • 代码深度神经元: 在if或for循环等代码块内,该神经元会被激活,反映代码的嵌套深度。

这表明RNN不仅仅是黑箱,它的“记忆”确实在学习和编码输入序列的结构性信息。


RNN的“阿喀琉斯之踵”与LSTM的诞生

虽然 Vanilla RNN 很强大,但它有一个致命的缺陷,这限制了它在长序列上的表现。


梯度消失与梯度爆炸问题 (Vanishing/Exploding Gradients)

问题根源

回想一下BPTT,梯度需要从序列的末尾一直反向传播到序列的开头。根据链式法则,在反向传播的过程中,梯度会反复乘以循环权重矩阵 \(W_{hh}\)

具体来说,从时间步 \(t\) 的隐藏状态 \(h_t\) 反向传播到上一步 \(h_{t-1}\) 时,梯度会乘以 \(W_{hh}\)(以及tanh的导数)。

\[\frac{\partial h_t}{\partial h_{t-1}} = \tanh'(...) \cdot W_{hh}\]

如果要将损失从很远的未来(比如 \(L_T\))传播到很久以前(比如 \(h_k\)),就需要将这个矩阵连乘很多次:

\[\frac{\partial L_T}{\partial h_k} \propto (W_{hh})^{T-k}\]

后果

  1. 梯度爆炸 (Exploding Gradients): 如果 \(W_{hh}\) 的最大奇异值大于1,经过多次连乘后,梯度会变得极其巨大,导致训练过程不稳定,参数更新“飞出”了合理的范围。
  2. 梯度消失 (Vanishing Gradients): 如果 \(W_{hh}\) 的最大奇异值小于1(这种情况更常见,尤其是还有个小于1的tanh导数项),经过多次连乘后,梯度会迅速衰减到接近于0。

梯度消失的危害:这意味着来自未来的“误差信号”无法有效地传递到序列的早期部分。模型因此无法学习到长距离的依赖关系。比如,在句子“The cat, which already ate ..., was full.”中,模型很难学习到主语“cat”和谓语“was”之间的单复数对应关系,因为它们之间隔了太多单词。

如何应对?

  • 梯度爆炸: 可以用梯度裁剪 (Gradient Clipping) 解决。设置一个阈值,如果梯度的范数超过这个阈值,就按比例把它缩放回来。这是一个简单有效的工程技巧。
  • 梯度消失: 这个问题更根本,需要改变RNN的架构。

长短期记忆网络 (Long Short-Term Memory, LSTM)

LSTM是一种经过精心设计的RNN变体,专门用来解决梯度消失问题。

核心思想:在RNN的隐藏状态 \(h_t\) 之外,引入一个独立的“细胞状态” (Cell State) \(c_t\)

\(c_t\) 就像一条“信息传送带”,信息可以在上面直接流过,几乎不经过处理。这使得梯度可以很容易地在长序列中传递。

LSTM通过三个精密的“门控” (Gate) 结构来控制这条传送带,决定什么信息应该被遗忘,什么新信息应该被存入。


LSTM的内部结构

每个LSTM单元有四个主要的交互部分,它们都是由一个 sigmoidtanh 激活的全连接层构成。

  1. 遗忘门 (Forget Gate, \(f_t\)):
  • 作用: 决定应该从上一个细胞状态 \(c_{t-1}\) 中丢弃哪些信息。
  • 计算\(f_t = \sigma(W_f \cdot [h_{t-1}, x_t] + b_f)\)

它的输出是一个0到1之间的向量,1代表“完全保留”,0代表“完全丢弃”。

  1. 输入门 (Input Gate, \(i_t\)) 和候选细胞状态 (\(\tilde{c}_t\)\(g_t\)):
  • 作用:决定要往细胞状态里存入什么新信息。
  • 计算

\(i_t = \sigma(W_i \cdot [h_{t-1}, x_t] + b_i)\) (决定更新哪些值)

\(\tilde{c}_t = \tanh(W_c \cdot [h_{t-1}, x_t] + b_c)\) (创建一个候选值向量)

  1. 细胞状态更新
  • 计算\(c_t = f_t \odot c_{t-1} + i_t \odot \tilde{c}_t\)
  • 解读: 首先,用遗忘门 \(f_t\) 乘以旧状态 \(c_{t-1}\),丢掉不想保留的信息。然后,用输入门 \(i_t\) 乘以候选值 \(\tilde{c}_t\),选择性地加入新信息。(\(\odot\) 表示逐元素相乘)
  1. 输出门 (Output Gate, \(o_t\)):
  • 作用: 决定要从细胞状态中输出什么信息作为新的隐藏状态 \(h_t\)
  • 计算

\(o_t = \sigma(W_o \cdot [h_{t-1}, x_t] + b_o)\)

\(h_t = o_t \odot \tanh(c_t)\)


LSTM如何解决梯度消失:

关键在于细胞状态的更新公式:\(c_t = f_t \odot c_{t-1} + \dots\)

在反向传播时,从 \(c_t\)\(c_{t-1}\) 的梯度路径上,只有一次逐元素的乘法 \(\odot f_t\)。没有与权重矩阵 \(W\) 的乘法!

这条“绿色通道”让梯度可以像在传送带上一样,顺畅地回传很多步而不至于消失。只要遗忘门 \(f_t\) 的值接近1,梯度就能几乎无衰减地传递下去。

这种设计思想,和后来出现的ResNet中的“残差连接”有异曲同工之妙,都是为了创造一个通畅的梯度流。


RNN的应用与总结

  1. RNN/LSTM 的更多应用

课程最后展示了更多基于RNN的酷炫应用,它们通常将CNN(用于理解图像)和RNN(用于处理序列)结合起来。

图像描述 (Image Captioning) :

  • 结构: 先用一个预训练好的CNN(如VGG)提取图像的特征向量。然后,将这个特征向量作为RNN的初始隐藏状态或第一个输入,之后RNN就像字符级语言模型一样,一个接一个地生成描述单词。
  • 视觉问答 (Visual Question Answering, VQA) : 输入一张图片和一个关于图片的问题(文字序列),模型需要输出答案。
  • 视觉对话 (Visual Dialog) : 模型可以和人进行多轮关于一张图片的对话。
  • 视觉语言导航 (Visual Language Navigation): 给机器人一个指令(“去客厅”),它需要根据看到的视觉场景和指令,规划并执行一系列动作。
  1. RNN的优缺点总结

优点

  • 能处理任意长度的输入序列。
  • 模型大小不随输入序列长度增加而改变(参数共享)。
  • 理论上,可以利用到很久以前的信息。

缺点

  • 计算速度慢: 循环计算是串行的,无法像CNN那样大规模并行化。
  • 长时依赖问题: 在实践中,Vanilla RNN 很难捕捉到非常久远的依赖关系(梯度消失)。 LSTMGRU 等变体极大地缓解了这个问题,但并未完全根除。
  1. 总结与展望
  • RNN 为处理序列数据提供了一套非常灵活和强大的架构。
  • Vanilla RNN 虽然简单,但因梯度消失问题而难以训练长序列。
  • LSTM 通过引入细胞状态和门控机制,极大地改善了梯度流,成为处理序列问题的标准模型之一。

如今,虽然基于 Attention 机制的 Transformer 模型在很多领域(尤其是自然语言处理)已经取代了 RNN 的主流地位,但 RNN 的核心思想(通过隐藏状态传递序列信息)依然非常重要,并以新的形式(如状态空间模型 SSMMamba 等)继续发展,这些新模型试图结合 RNN 的序列处理能力和 Transformer 的并行计算优势。


第十章:Attention机制与Transformer模型

这一讲我们将学习一种革命性的机制——注意力 (Attention),以及完全基于Attention构建的强大模型——Transformer。它们不仅克服了RNN的许多缺点,更成为了当今几乎所有大型AI模型(如GPT系列)的核心基石。


从RNN的局限性出发

RNN通过其循环结构和隐藏状态 \(h_t\),可以处理各种序列输入输出模式:一对一、一对多、多对一、多对多。

Seq2Seq模型与编码器-解码器架构

Seq2Seq 模型是RNN处理输入序列到输出序列(长度可能不同)任务的标准框架,尤其在机器翻译领域取得了巨大成功。

核心思想: 使用两个RNN,一个作为编码器 (Encoder),另一个作为解码器 (Decoder)。

  • 编码器 (Encoder) :

逐个读取输入序列的元素 \(x_1, x_2, \dots, x_T\)

在每个时间步 \(t\),根据当前输入 \(x_t\) 和上一个隐藏状态 \(h_{t-1}\) 更新其隐藏状态 \(h_t\)

\[h_t = f_W(x_t, h_{t-1})\]

编码器的目标是将整个输入序列的信息压缩进最后一个隐藏状态 \(h_T\)

  • 上下文向量 (Context Vector):

编码器最后一个隐藏状态 \(h_T\) 通常被用作一个固定大小的上下文向量 \(c\)。这个向量被认为是整个输入序列的“语义表示”或“摘要”。

这个 \(c\) 还会用来初始化解码器的第一个隐藏状态 \(s_0\)

  • 解码器 (Decoder) :

接收上下文向量 \(c\) 和一个特殊的起始符 [START] (\(y_0\)) 作为初始输入。

在每个时间步 \(t\),根据上一个隐藏状态 \(s_{t-1}\)、上一个预测的输出 \(y_{t-1}\) 以及固定的上下文向量 \(c\),来生成当前的隐藏状态 \(s_t\) 和预测的输出 \(y_t\)

\[s_t = g_U(y_{t-1}, s_{t-1}, c)\]
\[y_t = \text{softmax}(W_S s_t)\]

解码器会一直生成输出,直到预测出一个特殊的结束符 [STOP] 为止。


瓶颈问题

这个经典 Seq2Seq 模型最大的问题在于:整个输入序列的所有信息都被强行压缩到一个固定大小的上下文向量 \(c\) 中。

想象一下,如果输入序列非常长(比如一篇很长的文章),要把它所有信息无损地压缩到一个几百维的向量里,几乎是不可能的。信息在这个过程中会大量丢失。

这就像让你只用一句话总结一部长篇小说,必然会损失很多细节。

这个信息瓶颈 (Information Bottleneck)严重限制了模型处理长序列的能力。

解决方案:引入注意力机制 (Attention)


既然把所有信息压缩到一个向量里不可行,那么一个自然的想法就是:能不能让解码器在生成每个输出词的时候,“回头看”并“关注”输入序列中与之最相关的部分?

这就是注意力机制 (Attention Mechanism) 的核心思想。


注意力机制详解

Attention 机制最初是为了改进 Seq2Seq 模型而提出的,它允许解码器在生成每个输出时,动态地、有选择地关注输入序列的不同部分。

  1. 带Attention的Seq2Seq模型
  • 编码器保持不变: 仍然逐个处理输入 \(x_t\),生成一系列隐藏状态 \(h_1, h_2, \dots, h_T\)
  • 关键改变: 解码器不再依赖单一的、固定的上下文向量 \(c\)。取而代之的是,在解码的每一步 \(t\),都会动态计算一个专属的上下文向量 \(c_t\)
  • 计算 \(c_t\)过程 (以解码第一步 \(t=1\) 为例):

a. 计算对齐分数 (Alignment Scores) \(e_{1,i}\)

我们需要衡量解码器的当前状态(用 \(s_0\) 代表,即解码器RNN的上一个隐藏状态)与编码器的每一个隐藏状态 \(h_i\) (\(i=1, \dots, T\)) 的“相关性”或“匹配度”。

这个相关性通过一个对齐模型 (Alignment Model) \(f_{att}\) 来计算,通常是一个简单的前馈神经网络或者直接用点积。

\[e_{1,i} = f_{att}(s_0, h_i)\]

我们会得到 \(T\) 个分数 \(e_{1,1}, e_{1,2}, \dots, e_{1,T}\),每个分数表示当前解码状态与输入序列中第 \(i\) 个位置的信息有多相关。


b. 归一化得到注意力权重 (Attention Weights) \(a_{1,i}\)

将计算出的 \(T\) 个对齐分数通过 Softmax 函数进行归一化,得到一组注意力权重 \(a_{1,1}, a_{1,2}, \dots, a_{1,T}\)

\[a_{1,i} = \frac{\exp(e_{1,i})}{\sum_{j=1}^T \exp(e_{1,j})}\]

这组权重有几个重要性质:

  • 每个 \(a_{1,i}\) 都在0到1之间。
  • 它们的总和为1 (\(\sum_{i=1}^T a_{1,i} = 1\))。
  • 它们可以被看作是一个概率分布,表示在生成第一个输出词时,我们应该给予输入序列中第 \(i\) 个位置多大的“关注度”。

c. 计算上下文向量 \(c_1\)

将编码器的所有隐藏状态 \(h_i\) 根据注意力权重 \(a_{1,i}\) 进行加权求和,得到当前解码步专属的上下文向量 \(c_1\)

\[c_1 = \sum_{i=1}^T a_{1,i} h_i\]

直觉理解: 如果 \(a_{1,j}\) 的值很大,说明输入序列的第 \(j\) 个位置与当前解码任务高度相关,那么 \(h_j\)\(c_1\) 的贡献就很大。\(c_1\) 相当于一个根据当前需求,动态地从输入序列中提取的、加权平均后的信息摘要。


d. 解码器使用 \(c_1\) 生成输出:

解码器RNN使用这个动态计算出的 \(c_1\)(而不是固定的 \(c\))、上一个输出 \(y_0\) ([START]) 和上一个隐藏状态 \(s_0\) 来计算新的隐藏状态 \(s_1\),并最终预测出第一个输出词 \(y_1\)

\[s_1 = g_U(y_0, s_0, c_1)\]
\[y_1 = \text{softmax}(W_S s_1)\]

重复过程

在解码第二步 (\(t=2\)) 时,我们重复上述过程:

  • 使用新的解码器状态 \(s_1\) 去和所有的编码器状态 \(h_i\) 计算新的对齐分数 \(e_{2,i}\)
  • 计算新的注意力权重 \(a_{2,i}\)
  • 计算新的上下文向量 \(c_2 = \sum_{i=1}^T a_{2,i} h_i\)
  • 使用 \(c_2, y_1, s_1\) 来计算 \(s_2\)\(y_2\)
  • 这个过程一直持续到解码器输出 [STOP] 符号。

Attention的优势

  1. 解决了信息瓶颈: 解码器不再依赖单一的上下文向量,而是每一步都可以直接访问编码器的所有隐藏状态。
  2. 动态聚焦: 模型可以自动学习在生成不同输出词时,应该关注输入序列的哪些部分。例如,翻译 "we see the sky" 为 "vediamo il cielo",在生成 "vediamo" 时,模型可能会重点关注 "we" 和 "see";在生成 "il" 时,重点关注 "the";在生成 "cielo" 时,重点关注 "sky"。
  3. 可解释性 : 我们可以可视化注意力权重矩阵 \(a_{t,i}\)。矩阵中的亮点表示在生成第 \(t\) 个输出词时,模型主要关注了第 \(i\) 个输入词。这提供了一种直观理解模型“在想什么”的方式。例如,在英法翻译的例子中,可以看到清晰的对角线对齐,以及像 "European Economic Area" 和 "zone économique européenne" 这样词序不同的短语之间的非对角线对齐。
  4. 全过程可微分 : 整个Attention计算过程(包括 \(f_{att}\)、Softmax、加权求和)都是可微分的。这意味着我们不需要对注意力权重进行任何额外的监督,可以直接通过反向传播,端到端地训练整个带有Attention的Seq2Seq模型。

Attention作为一种通用计算原语

Attention机制不仅可以用于Seq2Seq模型,它实际上是一种更通用的、处理一组向量 (Set of Vectors) 的强大计算原语。我们可以从一个更抽象的角度来理解它。

  1. 通用Attention层的定义

一个通用的Attention层可以被描述为以下过程:给定一个查询向量 (Query vector) \(q\) 和一组数据向量 (Data vectors) \(X = \{x_1, x_2, \dots, x_N\}\),计算一个输出向量 \(y\)

计算相似度 (Similarities) \(e_i\) : 对于每一个数据向量 \(x_i\),计算它与查询向量 \(q\) 的相似度或相关性。

\[e_i = f_{att}(q, x_i)\]

这里的 \(f_{att}\) 可以是多种形式,一个常用且高效的选择是缩放点积 (Scaled Dot-Product) :

\[e_i = \frac{q \cdot x_i}{\sqrt{d_k}}\]

\(q \cdot x_i\)\(q\)\(x_i\) 的点积。点积本身就可以衡量两个向量的相似度(方向越接近,点积越大)。

\(d_k\)\(q\)\(x_i\) 的维度。

为什么要除以 \(\sqrt{d_k}\)? 当向量维度 \(d_k\) 很大时,点积 \(q \cdot x_i\) 的结果的方差也会很大,这可能导致某些 \(e_i\) 的值过大。过大的值输入到 Softmax 函数中,会使得Softmax的输出趋近于one-hot形式(某个位置接近1,其他位置接近0),导致其梯度变得非常小(梯度消失),不利于训练。除以 \(\sqrt{d_k}\) 可以将点积的方差稳定在1附近,缓解这个问题。

计算注意力权重 (Attention Weights) \(a_i\) : 将相似度分数通过 Softmax 归一化。

\[a = \text{softmax}(e)\]

计算输出向量 \(y\) : 将数据向量 \(X\) 根据注意力权重 \(a\) 进行加权求和。

\[y = \sum_{i=1}^N a_i x_i\]

向量化计算 :上述过程可以高效地使用矩阵运算实现,特别是当有多个查询向量 \(Q = \{q_1, \dots, q_M\}\) 时:

\(E = \frac{Q X^T}{\sqrt{d_k}}\) (计算所有查询和所有数据向量之间的相似度矩阵)

\(A = \text{softmax}(E, \text{dim=1})\) (沿数据向量维度进行Softmax)

\(Y = A X\) (计算加权和得到输出矩阵)

引入 Key 和 Value :为了增加模型的表达能力,我们通常会对原始的数据向量 \(X\) 做两次线性变换,得到键向量 (Key vectors) \(K\) 和值向量 (Value vectors) \(V\)

\(K = X W_K\)

\(V = X W_V\)

\(W_K\)\(W_V\) 是可学习的权重矩阵。

然后,Attention的计算变为:

  1. 相似度是查询 \(Q\) 和键 \(K\) 之间计算的:\(E = \frac{Q K^T}{\sqrt{d_k}}\)
  2. 注意力权重 \(A\) 仍然由 \(E\) 通过 Softmax 得到。
  3. 最终输出是值 \(V\) 根据注意力权重 \(A\) 的加权和:\(Y = A V\)

直观理解 (QKV三元组):

  • Query (查询): 代表当前我想知道什么?(例如,解码器当前的状态 \(s_{t-1}\)
  • Key (键): 代表输入序列中的每个元素“能提供什么信息”?(例如,编码器的隐藏状态 \(h_i\) 经过 \(W_K\) 变换)
  • Value (值): 代表输入序列中的每个元素“实际包含的信息内容”是什么?(例如,编码器的隐藏状态 \(h_i\) 经过 \(W_V\) 变换)

Attention机制就是用 Query 去和所有的 Key 做匹配,找到最相关的 Key,然后把这些 Key 对应的 Value 按相关程度(注意力权重)加权组合起来,得到最终需要的信息。


自注意力机制 (Self-Attention)

Self-Attention 是 Attention 机制的一种特殊且非常重要的形式。在Self-Attention中,Query、Key 和 Value 都来自于同一组输入向量 \(X\)

计算过程

对输入向量 \(X\) 进行三次线性变换,得到 \(Q, K, V\)

\(Q = X W_Q\)

\(K = X W_K\)

\(V = X W_V\)

(\(W_Q, W_K, W_V\) 是三个独立的可学习权重矩阵)

计算 Attention 输出 \(Y\)

\(E = \frac{Q K^T}{\sqrt{d_k}}\)

\(A = \text{softmax}(E)\)

\(Y = A V\)

核心作用: Self-Attention 让输入序列中的每个元素都可以直接关注序列中的所有其他元素(包括它自己),并根据相关性动态地聚合信息。

输出向量 \(y_i\) 是所有输入向量 \(x_j\) (通过 \(V\) 变换后) 的加权和,权重 \(a_{ij}\) 取决于第 \(i\) 个输入 (Query \(q_i\)) 和第 \(j\) 个输入 (Key \(k_j\)) 的相关性。

这使得模型能够捕捉序列内部的长距离依赖关系,比如句子中代词和它指代的名词之间的关系,而不需要像RNN那样依赖隐藏状态逐步传递信息。

置换等变性 (Permutation Equivariance) :

Self-Attention 对输入向量的顺序不敏感。如果你打乱输入向量 \(X\) 的顺序,那么输出向量 \(Y\) 也会以完全相同的方式被打乱,但每个向量本身的内容不会改变。

\(F(\sigma(X)) = \sigma(F(X))\), 其中 \(\sigma\) 是一个置换操作。

这意味着 Self-Attention 本质上是在处理一个集合 (Set) 而不是一个序列 (Sequence)。


如何让Self-Attention感知顺序?

既然Self-Attention本身不关心顺序,但很多任务(如语言)的顺序又至关重要,我们该怎么办?

解决方案:位置编码 (Positional Encoding)

在将输入向量 \(X\) 输入到Self-Attention层之前,我们给每个输入向量 \(x_i\) 加上一个位置编码向量 \(E(i)\)

\(x'_i = x_i + E(i)\)

位置编码向量 \(E(i)\) 是一个只取决于位置索引 \(i\) 的固定向量(通常使用正弦和余弦函数生成,或者直接学习得到)。

这样,即使两个输入向量 \(x_i\)\(x_j\) 的内容完全相同,它们加上了不同的位置编码后,\(x'_i\)\(x'_j\) 就变得不同了。Self-Attention在计算时就能区分它们的位置。


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

在某些任务中(尤其是自回归生成任务,如语言模型预测下一个词),我们需要限制模型只能关注到当前位置及之前位置的信息,而不能“偷看”未来的信息。

实现方法

在计算相似度矩阵 \(E\) 之后,但在进行 Softmax 之前。将 \(E\) 矩阵中对应于“未来”位置的元素(即 \(E_{ij}\)\(j > i\) 的部分)设置为负无穷大 (-inf)。

这样,在进行 Softmax 时,这些位置的注意力权重 \(a_{ij}\) 就会变成 0 。

最终计算加权和 \(Y = AV\) 时,输出 \(y_i\) 就只会依赖于 \(j \le i\) 的输入信息了。


多头自注意力 (Multi-Headed Self-Attention)

为了让模型能够同时关注来自输入序列的不同子空间(例如,同时关注句法关系和语义关系),研究者提出了多头自注意力。

核心思想: 与其只进行一次Self-Attention计算,不如并行地进行 \(H\) 次独立的Self-Attention计算(称为 \(H\) 个“头” Head)。

计算过程

  1. 对于每个头 \(h=1, \dots, H\),都学习独立的 \(W_Q^{(h)}, W_K^{(h)}, W_V^{(h)}\) 权重矩阵。
  2. 使用这些矩阵,从输入 \(X\) 计算出每个头的 \(Q^{(h)}, K^{(h)}, V^{(h)}\)
  3. 独立地计算每个头的 Attention 输出 \(Y^{(h)} = \text{softmax}\left(\frac{Q^{(h)} (K^{(h)})^T}{\sqrt{d_k/H}}\right) V^{(h)}\)

注意:每个头的维度通常会缩小为 \(d_k/H\)\(d_v/H\),以保持总计算量不变。

  1. \(H\) 个头的输出 \(Y^{(1)}, \dots, Y^{(H)}\) 拼接 (Concatenate) 起来 。
  2. 通过一个额外的线性输出层 \(W_O\) 将拼接后的结果投影回原始维度 。
\[Y_{multihead} = \text{Concat}(Y^{(1)}, \dots, Y^{(H)}) W_O\]

优势: 允许模型在不同的表示子空间中共同学习来自不同位置的信息。比如,一个头可能关注词语的句法依赖,另一个头关注语义相似性。


Self-Attention的计算成本

  • 计算复杂度\(O(N^2 \cdot D)\),其中 \(N\) 是序列长度,\(D\) 是向量维度。主要是因为计算 \(N \times N\) 的相似度矩阵 \(E\)
  • 内存复杂度\(O(N^2)\),主要是存储相似度矩阵 \(E\) 或注意力权重矩阵 \(A\)

问题\(N^2\) 的依赖使得 Self-Attention 在处理非常长的序列(如高分辨率图像或长文档)时变得非常昂贵。

解决方案:FlashAttention : 这是一种优化算法,通过分块计算和利用GPU内存层次结构,可以在不显式构造 \(N \times N\) 矩阵的情况下精确计算 Attention 输出。它将内存复杂度降低到了 \(O(N)\),使得 Transformer 能够处理更长的序列。


Transformer架构

Transformer 模型的核心思想就是:完全抛弃RNN和CNN,只用 Self-Attention 及其变体来构建整个网络。 这篇开创性的论文标题就是 "Attention is All You Need"。

  1. Transformer 模块 (Block) 结构

一个标准的 Transformer 模块包含以下组件:按顺序作用于输入的向量序列 \(X = \{x_1, \dots, x_N\}\)

  • 多头自注意力 (Multi-Headed Self-Attention):所有输入向量之间进行信息交互。
  • 残差连接 (Residual Connection):将 Self-Attention 层的输入直接加到其输出上。即 \(X' = X + \text{SelfAttention}(X)\)。这借鉴了ResNet的思想,有助于缓解梯度消失,让网络更容易训练得更深。
  • 层归一化 (Layer Normalization) :对残差连接后的结果 \(X'\) 进行 LayerNorm。LayerNorm 独立地对每个向量内部的元素进行归一化(计算均值和方差),有助于稳定训练动态。
  • 逐位置前馈网络 (Position-wise Feed-Forward Network, FFN 或 MLP):对 LayerNorm 后的每个向量独立地应用一个相同的前馈神经网络(通常是两层,中间层维度扩大,例如 D -> 4D -> D)。
\[FFN(x) = \text{ReLU}(x W_1 + b_1) W_2 + b_2\]

这一步提供了非线性变换和特征提取能力。

  • 第二个残差连接:将 FFN 的输入加到其输出上。即 \(X'' = X' + FFN(X')\)
  • 第二个层归一化:对第二个残差连接后的结果 \(X''\) 进行 LayerNorm,得到最终的模块输出 \(Y\)

总结 :

  • Self-Attention 负责向量之间的交互。
  • MLP (FFN) 和 LayerNorm 负责独立地处理每个向量。
  • 残差连接贯穿始终,保证信息流畅通。

整个模块只有6个主要的矩阵乘法运算(4个来自多头自注意力,2个来自MLP),非常适合并行计算。


构建完整的Transformer网络

堆叠模块: 一个完整的 Transformer 网络就是简单地将多个(例如6个、12个甚至上百个)这样的 Transformer 模块堆叠起来。上一层的输出作为下一层的输入。

规模的演进: 自2017年提出以来,Transformer 的基本架构变化不大,但其规模(层数 D、隐藏层维度 H、注意力头数 N、参数量)急剧增长,从最初的几亿参数增长到了现在的万亿参数级别(GPT-3, GPT-4等)。


Transformer用于语言建模 (LLM)

Transformer 如何应用于语言模型(预测下一个词)?

  1. 词嵌入 (Word Embedding) :使用一个可学习的嵌入矩阵 (Embedding Matrix) \([V \times D]\) 将输入的词语(通常是整数ID)转换成 \(D\) 维的向量。\(V\) 是词汇表大小。
  2. 位置编码: 将词嵌入向量与位置编码向量相加。
  3. Transformer模块堆叠:将加入位置编码的向量序列输入到堆叠的 Transformer 模块中。

关键: 在每个模块的 Self-Attention 层内部使用掩码 (Masked Self-Attention),确保在预测第 \(t\) 个词时,模型只能看到前 \(t-1\) 个词的信息。

  1. 输出投影 :Transformer最后一层输出的 \(N\)\(D\) 维向量。使用一个线性投影矩阵 \([D \times V]\)(有时会共享词嵌入矩阵的权重)将每个输出向量投影回 \(V\) 维的得分向量 (logits)。
  2. Softmax 和损失 :对得分向量应用 Softmax 得到预测下一个词的概率分布。使用交叉熵损失函数,比较预测概率和真实下一个词的 one-hot 向量,计算损失。通过反向传播训练整个模型。

Transformer的应用与变体

  1. Vision Transformer (ViT)

Transformer 不仅在自然语言处理领域大放异彩,也被成功应用于计算机视觉。

核心思想: 将图像视为一系列的“图像块 (Patches)”。

处理流程

  1. 图像分块 : 将输入图像(例如 \(224 \times 224 \times 3\))分割成不重叠的小块(例如 \(16 \times 16 \times 3\))。
  2. 线性投影 : 将每个图像块展平成一个长向量(例如 \(16 \times 16 \times 3 = 768\) 维),然后通过一个线性层将其投影到模型所需的维度 \(D\) (Slide 的另一种解释:这等价于使用一个 \(16 \times 16\) 的卷积核,步长为16,输出通道为 \(D\))。
  3. 位置编码 : 给每个块向量添加可学习的位置编码,告知模型每个块在原始图像中的2D位置。
  4. Transformer处理 : 将这些带有位置信息的块向量序列输入到标准的 Transformer 编码器中(不使用掩码,因为图像分类任务允许模型看到所有块)。
  5. 分类头 (Pooling):Transformer 输出 \(N\)\(D\) 维的块向量。

通常有两种方式得到最终的图像表示:

  • [CLS] Token: 在输入序列前加入一个特殊的可学习的 [CLS] (classification) 向量,只取 Transformer 输出中对应于这个 [CLS] 位置的向量作为图像的全局表示。
  • 平均池化 (Average Pooling): 将所有输出的块向量进行平均池化,得到一个 \(D\) 维的全局表示。将这个全局表示通过一个线性层进行分类。

效果: ViT 证明了在足够大的数据集上进行预训练时,纯粹基于 Transformer 的架构可以在图像识别任务上达到甚至超过顶尖的 CNN 模型。


  1. Transformer架构的微调

虽然 Transformer 的核心结构很稳定,但研究者们也提出了一些改进和变体,使得训练更稳定或效率更高:

  • Pre-Norm vs. Post-Norm :原始 Transformer 使用 Post-Norm(LayerNorm 在残差连接之后)。

Pre-Norm 将 LayerNorm 移到 Self-Attention 和 MLP 之前,即在残差连接的“分支”上。实践证明 Pre-Norm 训练更稳定,更容易收敛,尤其是在网络很深时。现在 Pre-Norm 是更常见的选择。

  • RMSNorm :一种简化的 Layer Normalization,只根据均方根(Root Mean Square)进行缩放,去掉了减去均值的步骤,并且只学习缩放因子 \(\gamma\),没有学习偏移因子 \(\beta\)。计算更快,效果相似。

\(y_i = \frac{x_i}{\text{RMS}(x)} \cdot \gamma_i\), 其中 \(\text{RMS}(x) = \sqrt{\frac{1}{D}\sum_{j=1}^D x_j^2 + \epsilon}\)

  • SwiGLU MLP :一种替代标准 FFN 的 MLP 变体,引入了门控机制(Gated Linear Unit),通常表现更好。

\(\text{SwiGLU}(X, W_1, W_2, W_3) = (\text{Swish}(X W_1) \odot (X W_2)) W_3\)

Swish 是一种激活函数 \(\sigma(x) = x \cdot \text{sigmoid}(x)\)

  • 专家混合模型 (Mixture of Experts, MoE) :为了在不显著增加计算量的情况下极大增加模型参数量。在每个 Transformer 模块的 MLP 层,不再只有一个 MLP,而是有 \(E\) 个独立的 MLP(称为“专家”)。

对于每个输入的 token,一个门控网络 (Gating Network) 会决定将这个 token 发送给哪 \(A\) 个(通常 \(A \ll E\),比如 \(A=2\))专家进行处理。

最终的输出是这 \(A\) 个专家输出的加权组合。

优势: 参数量可以扩大 \(E\) 倍,但每个 token 的计算量只增加了大约 \(A\) 倍。这使得训练万亿参数级别的模型成为可能。目前最顶尖的大语言模型(如 GPT-4, Gemini 等)普遍被认为采用了 MoE 架构。

总结

Attention 是一种强大的新原语,用于处理向量集合,核心是计算查询向量和数据向量之间的加权组合。

Transformer 是一种完全基于 Attention 构建的神经网络架构,通过堆叠 Self-Attention 和 FFN 模块实现。

Transformer 具有高度并行性和捕捉长距离依赖的能力,已成为自然语言处理、计算机视觉、语音识别等众多领域的主导架构。

它是当今所有大型AI模型的骨干 (Backbone)。


第九章:检测、分割、可视化与理解

回顾:Transformer

课程首先简要回顾了Transformer。

  • Transformer架构 :回顾了经典的Encoder-Decoder(编码器-解码器)架构,这是NLP(自然语言处理)的基石。
  • 序列处理的三种方式 :这是理解后续ViT模型的基础。
  • 循环神经网络 (RNN):
  1. 原理:按顺序处理序列中的每一个元素(token),并将上一个时间步的信息(隐藏状态)传递给下一个。
  2. 优点:计算量和内存随序列长度\(N\)呈线性增长\(O(N)\),理论上适合长序列。
  3. 缺点:无法并行化。必须一个一个token计算,速度很慢。
  • 卷积 (Convolution):
  1. 原理:使用卷积核在序列上滑动,每次只看一个局部窗口。
  2. 优点:可以并行计算,非常高效。
  3. 缺点:对于长序列,需要堆叠非常多层才能建立起大的感受野(让序列两端的token产生关联)。
  • 自注意力 (Self-Attention):
  1. 原理:序列中的每一个token都可以直接“看到”并计算与所有其他token的关联性。
  2. 优点:高度并行化(本质是矩阵乘法),非常适合长序列(一步到位,所有token直接关联)。
  3. 缺点:计算量非常昂贵。计算注意力分数需要一个\(N \times N\)的矩阵,所以计算量是\(O(N^2)\),内存也是。

视觉Transformer (Vision Transformer, ViT)

如何将为1D文本序列设计的Transformer,应用到2D的图像上?

核心思想 :论文标题 "An Image is Worth 16x16 Words"(一张图片等于一堆16x16的单词)已经给出了答案。我们不把像素当作序列,而是把图像块 (Patches)当作序列。


ViT的构建步骤 :

  1. 切分图像 (Image Patching) :将输入的图像(例如 \(224 \times 224 \times 3\))分割成\(N\)个不重叠的小图像块(Patches)。

例如,使用\(16 \times 16\)的 patch,我们会得到 \(N = (224/16) \times (224/16) = 14 \times 14 = 196\) 个图像块。每个块的大小是 \(16 \times 16 \times 3\)

  1. 块投影 (Patch Projection) :Transformer的输入是一个1D的向量序列。我们将每个\(16 \times 16 \times 3\)的图像块“展平”(Flatten),然后通过一个线性投影层(Linear Projection),将其映射成一个\(D\)维的向量(例如\(D=768\))。

现在,我们就得到了一个 \(N \times D\)(即 \(196 \times 768\))的序列,这和NLP中的词向量序列一模一样。

  1. 位置编码 (Positional Embedding) :

为什么需要?

  • Transformer本身是“置换不变”的,它不知道token的顺序。但图像中,“左上角”和“右下角”的位置信息至关重要。

怎么做?

  • 我们创建一组可学习的位置编码向量(\(N \times D\)维),每个位置(1到196)对应一个\(D\)维的向量。将这个位置向量加到对应的图像块向量上。

这样,模型就能在处理时“知道”每个图像块的原始空间位置。

  1. [CLS] Token (分类令牌) :为了进行图像分类,我们模仿BERT模型,在序列的最前面拼接上一个额外的、可学习的 [CLS] 令牌(\(1 \times D\)维)。

这个[CLS]令牌就像一个“汇总员”。在经过Transformer编码器时,它会通过自注意力机制与所有的图像块进行信息交换,“汇总”整张图的信息。

  1. Transformer编码器 :将 "Patches + Positional Embedding + [CLS] Token" 组成的序列(共 \(N+1\) 个token)送入一个标准的Transformer编码器(多层自注意力和MLP)。

  2. 分类头 (Classifier Head) :只取出Transformer输出序列中第一个[CLS]令牌对应的输出向量。

将这个向量送入一个简单的线性层(FC层),输出\(C\)个类别的得分(\(C\)是类别数)。


ViT的另一种分类方式 :这是一个小变种。它不使用[CLS]令牌。在Transformer处理完所有\(N\)个图像块后,将所有的\(N\)个输出向量进行平均池化 (Average Pooling),得到一个\(1 \times D\)的向量。再将这个平均向量送入线性层进行分类。

这两种方式([CLS] token vs. 平均池化)在实践中效果都很好。


Transformer的微调与改进

这一部分介绍了自2017年以来,对原始Transformer架构的一些重要改进,这些改进使得训练更稳定、效果更好。

  1. Pre-Norm (前置归一化) :

原始 (Post-Norm) :x -> SubLayer(x) -> Add(x) -> Norm(x + SubLayer(x))。LayerNorm放在残差连接之后。

问题:梯度在深层网络中容易消失或爆炸,导致训练不稳定。

改进 (Pre-Norm) :x -> Norm(x) -> SubLayer(Norm(x)) -> Add(x + SubLayer(Norm(x)))。LayerNorm放在输入到自注意力和MLP之前。

为什么:Pre-Norm确保了残差路径上的梯度回传更顺畅,极大地稳定了深度Transformer的训练。

  1. RMSNorm (均方根归一化) :

这是LayerNorm的一个替代品,更简单、更快。

LayerNorm公式:\(y = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} \times \gamma + \beta\) (又减均值、又除方差)。

RMSNorm公式:\(y_i = \frac{x_i}{RMS(x)} \times \gamma_i\),其中 \(RMS(x) = \sqrt{\epsilon + \frac{1}{N}\sum_{i=1}^{N}x_i^2}\)

为什么:它只做缩放(scaling),不做中心化(centering)。实践证明在Transformer中同样有效,且计算量更小。

  1. SwiGLU MLP :

这是对Transformer中MLP(前馈网络)层的改进。

经典MLP:\(Y = \text{ReLU}(XW_1)W_2\)

SwiGLU MLP:\(Y = (\text{Swish}(XW_1) \odot XW_2)W_3\),其中 \(\text{Swish}(x) = x \times \text{sigmoid}(x)\)\(\odot\) 是逐元素相乘。

为什么:它使用了一个“门控 (Gated)”机制。\(XW_2\) 路径充当了一个门,来控制\(XW_1\)路径的信息流。这种结构被发现能提升模型效果。

  1. MoE (Mixture of Experts, 专家混合) :

思想:用更多的参数(更大的模型容量),但不增加计算量。

怎么做:将一个MLP层替换为\(E\)个“专家”(\(E\)个独立的MLP)。

路由 (Routing):对于每一个token,一个“门控网络 (Gating Network)”会动态地选择\(A\)个(\(A < E\),例如 \(A=2\))最相关的专家来处理这个token。

输出:token的最终输出是这\(A\)个专家输出的加权和。

  • 好处 :参数量可以扩大\(E\)倍(例如8倍),但计算量只增加了\(A\)倍(例如2倍)。

现状 :现在所有最顶尖的大语言模型(如GPT-4, Claude 3)几乎都采用了MoE架构。


计算机视觉核心任务

在深入研究模型前,我们必须先弄清楚CV的几大任务:

  1. 图像分类 (Classification):

任务:给整张图分配一个标签(例如:“猫”)。

输出:一个类别标签。

  1. 语义分割 (Semantic Segmentation):

任务:给图像中的每一个像素分配一个类别标签(例如:“草地”、“猫”、“树”、“天空”)。

特点:不区分实例 (instance)。如果图中有两只猫,它们的所有像素都会被标记为“猫”,不会区分“猫1”和“猫2”。

  1. 物体检测 (Object Detection):

任务:在图中找到所有物体,并用边界框 (Bounding Box)框出它们的位置,同时给出它们的类别标签。

输出:一系列 (类别, [x, y, w, h]) 的列表。

特点:区分实例(通过不同的框),但只给出一个粗略的框,而不是像素级的掩码。

  1. 实例分割 (Instance Segmentation):

任务:最难的任务。结合了“物体检测”和“语义分割”。

要求:找到图中每一个物体实例,并为每个实例生成一个像素级的分割掩码 (mask)。

特点:既要区分实例(“猫1”、“猫2”),又要给出像素级的精准位置。


语义分割 (Semantic Segmentation)

目标:对每个像素进行分类。


朴素的方法(效率低下)
  1. 方法1:滑动窗口 (Sliding Window) :

思路:在像素周围取一个小图像块(patch),用CNN对这个patch分类,并将结果赋给中心像素。

问题:单个像素没有上下文(context)。使用patch可以加入上下文。

致命缺陷 :极其低效。如果图像是 \(256 \times 256\),就要独立运行 \(256 \times 256\) 次CNN。相邻的patch计算是大量重复的。

  1. 方法2:全卷积(无下采样):

思路:设计一个只有卷积层、没有池化(pooling)或步长(stride)的CNN。这样输入和输出的分辨率始终保持一致。

致命缺陷 :计算量巨大。在高分辨率特征图上做卷积非常昂贵。


现代方法:编码器-解码器 (Encoder-Decoder)

这是目前语义分割的标准范式。

  1. 编码器 (Encoder):

作用:使用标准的CNN架构(如VGG, ResNet)。通过池化(Pooling)和带步长的卷积(Strided Conv)逐步降低空间分辨率(下采样)。

好处:1. 极大降低计算量。 2. 获得巨大的感受野,使网络能理解高级语义(“这是一只牛”)。

坏处:丢失了精确的空间位置信息。

  1. 解码器 (Decoder):

作用:将编码器输出的低分辨率、高语义的特征图逐步恢复到原始图像分辨率(上采样)。

核心问题 :如何进行上采样 (Upsampling)?


上采样技术
  1. 方法1:Unpooling (非学习型) :

最近邻 (Nearest Neighbor):简单地将一个 \(1 \times 1\) 的像素复制成 \(2 \times 2\)

"Bed of Nails":将 \(1 \times 1\) 的像素放在 \(2 \times 2\) 的左上角,其余填0。

缺点:太粗糙,引入了棋盘格效应。

  1. 方法2:Max Unpooling (非学习型) :

思路:在编码器的Max Pooling(下采样)时,记住最大值在 \(2 \times 2\) 窗口中的位置 (index)。

在解码器上采样时,将像素放回它原来的位置,其余填0。

好处:保留了一定的空间位置信息。

  1. 方法3:转置卷积 (Transposed Convolution) (可学习型) :

这是最重要、最常用的方法。它是一种可学习的上采样。

回顾:标准卷积 (Conv) :标准卷积(例如 \(3 \times 3\),步长S=2)是一种“多对一”的映射。它将输入上的 \(3 \times 3\) 区域(9个值)通过点积(dot product)映射到输出上的1个值。

转置卷积 (Transposed Conv):它是一种“一对多”的映射。

工作原理 :取输入上的1个值(例如 \(a\)),乘以一个完整的 \(3 \times 3\) 卷积核(例如 \(W\)),然后将这个 \(3 \times 3\) 的结果“印”到输出特征图上。

当来自不同输入像素的“印章”在输出上重叠时,将它们的值相加。

为什么叫“转置” :如果把标准卷积(前向)写成矩阵乘法 \(y = Cx\),那么转置卷积(反向)在数学上等价于 \(x' = C^T y\),即乘以该矩阵的转置。

注意:它常被误称为“反卷积 (Deconvolution)”,但“转置卷积”是更准确的术语。


U-Net 架构

U-Net是语义分割领域最经典的架构之一,尤其在医学图像分割中大获成功。

结构:一个对称的 "U" 形编码器-解码器结构。

编码器:下采样路径,捕捉上下文(语义)。

解码器:上采样路径,恢复空间分辨率(位置)。

核心创新:跳跃连接 (Skip Connections):

问题:在U型底部,特征图分辨率很低,虽然语义很强,但边缘等细节信息已完全丢失。即使一路“转置卷积”回去,得到的边缘也是模糊不清的。

U-Net的解决办法:在解码器上采样的每一层,都将其特征图与编码器中相同分辨率的特征图在通道维度上拼接 (Concatenate) 起来。

为什么有效:这使得解码器在恢复分辨率的每一步,都能同时获得:

  • 来自上一步的、高语义的、模糊的特征(上采样结果)。
  • 来自编码器的、低语义的、但高分辨率的、清晰的特征(跳跃连接)。

结果:U-Net能生成非常精细和准确的分割边界。


物体检测 (Object Detection)

目标:定位并分类图中的多个物体(输出 [类别, 边界框] 列表)。


单物体检测 (Classification + Localization)

思路:先从最简单的情况开始,假设图中只有1个物体。

模型:拿一个分类CNN(如AlexNet)。

多任务学习 (Multitask Loss):在网络的最后,分出两个“头”:

  • 分类头 (Classification Head):一个FC层,输出\(C\)个类别分数。使用Softmax损失进行训练。
  • 定位头 (Localization Head):一个FC层,输出4个数字 \((x, y, w, h)\) 来表示边界框。

如何训练定位头?这是一个回归 (Regression) 问题。

我们使用L2损失(均方误差),来惩罚预测框 \((x, y, w, h)\) 和真实框 \((x', y', w', h')\) 之间的差距。

\[L_{reg} = || (x,y,w,h) - (x',y',w',h') ||_2^2\]

总损失:\(L_{total} = L_{cls} + \lambda L_{reg}\)\(\lambda\) 是一个平衡权重的超参数)。

  • \(L_{cls}\) 是分类损失(交叉熵)。衡量模型预测的类别与真实类别之间的差距。
  • \(L_{reg}\) 是回归损失(L2损失)。衡量模型预测的边界框与真实框之间的差距。

多物体检测:两阶段法 (Two-Stage)

问题:CNN要求固定的输出数量,但一张图里的物体数量是可变的。

  1. 朴素方法:滑动窗口:

思路:用上面的“单物体检测器”在图像的所有可能位置、所有可能尺度、所有可能长宽比上滑动一遍。

问题:计算量是天文数字,完全不可行。

  1. R-CNN (Region-based CNN)

这是深度学习物体检测的开山之作。

思路:将问题分为两步,先找“可能有物体”的区域,再对这些区域分类。

步骤:

  • 区域提议 (Region Proposals) :使用传统的、快速的CV算法(如 Selective Search),在CPU上找出约2000个“可能包含物体”的区域(Region of Interest, RoI)。
  • 区域缩放 (Warp) :将这2000个不同大小的RoI,强行缩放 (warp) 到固定大小(例如 \(224 \times 224\))。
  • CNN前向传播 :将这2000个缩放后的图像块,独立地送入一个CNN(如AlexNet)提取特征。
  • 分类 :用SVM(支持向量机)对CNN提取的特征进行分类。
  • 框体回归 (BBox Regression) :训练一个线性回归模型,对这2000个框的位置进行微调,使其更准。

致命缺陷 :太慢了! 主要的CNN计算需要执行2000次。

  1. Fast R-CNN

R-CNN的加速版,核心思想:共享计算。

关键洞察:为什么R-CNN慢?因为2000个RoI是高度重叠的,CNN在重叠区域的计算被重复了无数次。

Fast R-CNN 步骤:

  • 共享CNN计算:将整张原图(不是RoI!)一次性送入CNN主干网络(Backbone),得到一个高层特征图(例如 \(512 \times 20 \times 15\))。
  • 区域提议:仍然使用Selective Search产生~2000个RoI。
  • RoI 投影:将这~2000个RoI(在原图坐标上)投影 (project) 到CNN输出的特征图上,得到2000个特征区域。
  • RoI 池化 (RoI Pooling):

问题:这2000个特征区域还是不同大小的(例如 \(512 \times 5 \times 4\), \(512 \times 3 \times 2\) ...)。

解决:RoI池化层将任何大小的特征区域,池化成一个固定大小的特征图(例如 \(512 \times 7 \times 7\))。

原理 :将特征区域划分为 \(7 \times 7\) 的网格,对每个网格内的特征执行Max Pooling。

  • 分类与回归 :将 \(512 \times 7 \times 7\) 的特征图展平,送入两个FC头(分类头+回归头),使用Softmax和L2(回归)损失。

优点:速度极大提升(几十倍)。昂贵的CNN计算只做了一次。

瓶颈 :速度瓶颈转移到了Selective Search(它在CPU上运行)。

  1. Faster R-CNN

关键洞察:为什么我们不用GPU(即CNN本身) 来做区域提议呢?

核心创新:区域提议网络 (Region Proposal Network, RPN)。

RPN工作原理:

输入:Fast R-CNN中的那个共享特征图(例如 \(512 \times 20 \times 15\))。

锚点 (Anchors) :在特征图的每一个空间位置(共 \(20 \times 15\) 个),预设\(K\)个(例如 \(K=9\))不同尺度和长宽比的“锚点框 (Anchor Boxes)”。

RPN头部:用一个 \(3 \times 3\) 卷积滑过特征图,然后分出两个并行的 \(1 \times 1\) 卷积头:

  • 分类头:输出 \(2 \times K\) 个分数,判断这\(K\)个锚点是“前景(物体)”还是“背景”。
  • 回归头:输出 \(4 \times K\) 个值,预测这\(K\)个锚点相对于真实物体框的偏移量 \((dx, dy, dw, dh)\)

输出:RPN会生成约 \(20 \times 15 \times 9 = 2700\) 个经过微调的提议框。我们按“前景分数”排序,取出Top 300个,送入Fast R-CNN的RoI池化层。

完整架构:

  • 图像 \(\rightarrow\) CNN Backbone \(\rightarrow\) 共享特征图。
  • 共享特征图 \(\rightarrow\) RPN \(\rightarrow\) 约300个RoI。
  • (共享特征图, RoI) \(\rightarrow\) RoI Pooling \(\rightarrow\) 固定大小特征。
  • 固定大小特征 \(\rightarrow\) 分类头 & 回归头 \(\rightarrow\) 最终结果。

训练:这是一个优美的端到端系统,使用4个损失联合训练:RPN分类损失、RPN回归损失、最终分类损失、最终回归损失。


单阶段法 (One-Stage) (YOLO / SSD)

思路:两阶段法(RPN + 分类头)还是有点繁琐。我们能不能一步到位,直接从特征图上预测出最终的类别和框?

  1. YOLO (You Only Look Once):

原理:

  • 将图像划分为 \(S \times S\) 网格(例如 \(7 \times 7\))。
  • 如果一个物体的中心落入某个网格,该网格就负责预测这个物体。
  • 每个网格预测 \(B\) 个边界框(例如 \(B=2\))和 \(C\) 个类别概率。

每个框包含5个值:\((x, y, w, h, \text{confidence})\)

\(\text{confidence} = P(\text{Object}) \times IOU(\text{pred}, \text{truth})\),即“框里有物体的概率”乘以“框的准确度”。

输出:一个 \(S \times S \times (B \times 5 + C)\) 的大张量。例如 \(7 \times 7 \times (2 \times 5 + 20) = 7 \times 7 \times 30\)

优点:极快! 因为它只有一个阶段,一次前向传播就得到所有结果。

缺点:每个网格只能预测一个物体,对小物体和拥挤的物体检测效果差。

  1. DETR (DEtection TRansformer)

这是用Transformer思想来做检测的全新范式。

目标:抛弃所有手工设计的组件(锚点、RPN、NMS非极大值抑制...)。

架构 :

  • Backbone:CNN提取图像特征图。
  • Transformer:一个完整的Encoder-Decoder。
  • 对象查询 (Object Queries) :这是关键。我们定义\(N\)个(例如\(N=100\))可学习的输入向量,称为"Object Queries"。

\(N\)个queries被送入解码器 (Decoder)。

在解码器中,它们通过自注意力(queries之间相互通信,避免重复)和交叉注意力(queries去“看”编码器输出的图像特征)来更新自己。

  • FFN (预测头) :

将Transformer解码器输出的\(N\)个向量,各自送入一个FFN(前馈网络)。

每个FFN输出该query对应的类别(例如 "dog")和边界框 \((x, y, w, h)\)

  • 训练:

二分图匹配 (Bipartite Matching):我们得到了\(N\)个预测框,但真实框可能只有\(M\)个(\(M < N\))。如何匹配?

使用匈牙利算法 (Hungarian Algorithm) 找到\(N\)个预测框和\(M\)个真实框之间的最佳一对一匹配。

  • 对匹配上的(预测, 真实)对计算损失(分类损失+框回归损失)。
  • 对其余\(N-M\)个未匹配上的预测,强制它们预测为“无物体 (no object)”。

优点:架构简洁,端到端,不需要NMS。

实例分割 (Instance Segmentation)

目标:检测并分割每一个物体实例。

Mask R-CNN

思路:这是一个非常聪明的扩展。它在Faster R-CNN的基础上(两阶段),再加一个“头”。

架构 :

  1. 第一阶段:使用RPN进行区域提议(同Faster R-CNN)。
  2. 第二阶段:对每个RoI(提议区域),并行地执行三个任务:
  • 分类头:预测类别。
  • 框回归头:微调边界框。
  • 掩码头 (Mask Head):这是一个小型的全卷积网络 (FCN)。它对RoI内的特征进行上采样,并输出一个像素级的二值掩码 (mask)(例如 \(28 \times 28\))。这个掩码是逐类 (per-class) 预测的(例如 \(C \times 28 \times 28\))。

核心改进:RoI Align (RoI对齐)

问题:Fast/Faster R-CNN中的RoI Pool 存在两次量化(取整)操作(投影时取整、划分网格时取整)。

影响:这种取整对分类和框回归影响不大,但对像素级的掩码是致命的,会导致掩码和物体之间有几个像素的错位。

RoI Align 解决办法 :

  1. 不取整:在投影和划分网格时,保留所有的浮点数坐标。
  2. 双线性插值 (Bilinear Interpolation) :当需要在一个非整数坐标(例如 \(x=5.2, y=3.7\))上采样特征值时,使用它周围4个整数坐标的特征值进行双线性插值,来估算出该点的精确特征。

结果:RoI Align 完美地对齐了特征和输入,使得掩码预测非常精确。

结果:Mask R-CNN效果非常好,成为了实例分割的基准。它甚至可以扩展到人体姿态估计(只需将掩码头换成“关键点预测头”)。


模型可视化与理解

目标:我们训练的神经网络是个“黑盒”,如何理解它在想什么?它在看哪里?


方法1:可视化第一层卷积核

做法:直接将第一个卷积层(例如 \(64 \times 3 \times 7 \times 7\))的64个 \(3 \times 7 \times 7\) 权重拿出来,当成图像画出来。

发现:CNN的第一层总是在学习Gabor滤波器(边缘检测)和颜色斑块。这说明它在学习最基础的视觉元。


方法2:显著性图 (Saliency Maps)

问题:对于一个分类为“狗”的图片,输入图像中的哪些像素对这个“狗”的决策贡献最大?

数学原理:

  1. 前向传播,得到类别得分 \(S_c\)\(c\)代表“狗”)。
  2. 反向传播,计算类别得分 \(S_c\) 相对于输入图像 \(I\) 的梯度:\(\frac{\partial S_c}{\partial I}\)

这个梯度 \(\frac{\partial S_c}{\partial I}\) 的大小(绝对值)就代表了该像素的“显著性”。

直观理解:梯度 \(\frac{\partial S_c}{\partial I_i}\) 表示“如果我轻微改变像素 \(I_i\),类别得分 \(S_c\) 会改变多少?”。如果梯度大,说明这个像素对决策很重要。

可视化:将梯度的绝对值(或平方)画出来,就是一张“显著性图”。


方法3:CAM (Class Activation Mapping, 类激活图)

问题:显著性图通常很噪点很多。我们想知道的是一个更平滑的、高语义的“热力图”。

限制:只适用于特定架构的CNN(最后是 卷积层 \(\rightarrow\) 全局平均池化GAP \(\rightarrow\) FC分类层)。

数学原理:

  1. 设最后一个卷积层的特征图为 \(f \in \mathbb{R}^{H \times W \times K}\)\(K\)个通道)。
  2. GAP(Global Average Pooling)层将每个通道 \(f_k\) 平均为1个值 \(F_k\)\(F_k = \frac{1}{HW}\sum_{h,w} f_{h,w,k}\)
  3. FC层计算最终得分:\(S_c = \sum_k w_{k,c} F_k\)\(w_{k,c}\) 是连接第\(k\)个通道和第\(c\)类的权重。
  4. 洞察:\(w_{k,c}\) 代表了第\(k\)个通道对第\(c\)类的重要性。
  5. 类激活图 \(M_c\):我们将这个“重要性” \(w_{k,c}\) 乘回它对应的原始特征图 \(f_k\),再把所有通道加起来:
\[M_{c,h,w} = \sum_k w_{k,c} \cdot f_{h,w,k}\]

直观理解:\(M_c\) 是一张热力图。它显示了那些“对'狗'这个类别很重要的通道”在空间上的激活位置。

缺点:需要修改网络结构(必须用GAP),不通用。


方法4:Grad-CAM (梯度加权的类激活图)

目标:通用化CAM。使其适用于任何CNN架构(如VGG, ResNet)的任何卷积层。

核心问题:对于一个普通的CNN(没有GAP),我们没有 \(w_{k,c}\) 权重。

Grad-CAM的解决办法:用梯度来计算“通道重要性” \(\alpha_k\)

步骤:

  1. 选定一个卷积层(例如 'conv5'),得到其激活 \(A \in \mathbb{R}^{H \times W \times K}\)
  2. 计算类别得分 \(S_c\) 相对于该层激活 \(A\) 的梯度:\(\frac{\partial S_c}{\partial A}\)

这个梯度 \(\frac{\partial S_c}{\partial A_{h,w,k}}\) 反映了 \(A_{h,w,k}\)\(S_c\) 的影响。(这个梯度表示:如果你轻微改变某个空间位置、某个通道的激活值,类别得分会怎么变。)

  1. 计算通道重要性 \(\alpha_k^c\):对梯度图进行全局平均池化(GAP):
\[\alpha_k^c = \frac{1}{HW} \sum_{h,w} \frac{\partial S_c}{\partial A_{h,w,k}}\]

对每个通道 \(k\),把它所有空间位置的梯度做平均(GAP),得到一个数 \(\alpha_k^c\),表示该通道对类别 \(c\) 的整体重要性。这就是“通道重要性”:第 \(k\) 个通道对类别 \(c\) 的平均影响力。

  1. 计算热力图:用这个 \(\alpha_k^c\) 作为权重,对原始激活图 \(A_k\) 进行加权求和,并使用ReLU(只关心正贡献):
\[M_c = \text{ReLU} \left( \sum_k \alpha_k^c A_k \right)\]

结果:Grad-CAM 是一种通用的、强大的可视化工具,能清晰地显示模型是根据图像的哪个区域做出决策的。

ViT的可视化:对于ViT,我们可以可视化它的注意力矩阵,看看[CLS]令牌在“看”哪些图像块,或者图像块之间是如何相互“看”的。


第10章:视频理解

视频是什么?视频本质上就是一系列按时间顺序排列的图像(帧)。所以,它在2D图像的基础上增加了一个维度:时间 (Time)。

如果一张RGB图像的维度是 3 x H x W (通道, 高, 宽),那么一段视频的维度就是 T x 3 x H x W (帧数, 通道, 高, 宽)。它是一个4D张量。

核心任务:视频分类 (Video Classification)也叫“动作识别 (Action Recognition)”。

  • 输入: 一段视频 (e.g., T x 3 x H x W 的张量)。
  • 输出: 一个单一的标签,描述这段视频里的主要动作 (e.g., "Swimming", "Running", "Jumping")。

类比: 这就像图像分类是识别物体,而视频分类是识别动作。


视频数据的挑战与处理策略

挑战:视频数据太“大”了!

视频通常以每秒30帧 (fps) 的速率播放。

我们来算一笔账:一段高清(HD)视频,分辨率为 \(1920 \times 1080\),每个像素3个字节 (RGB)。

  • 每帧大小:\(1920 \times 1080 \times 3 \approx 6.2MB\)
  • 每秒大小:\(6.2MB/帧 \times 30 帧/秒 \approx 186MB/秒\)
  • 每分钟大小: \(186MB/秒 \times 60 秒/分钟 \approx 11GB/分钟\)

结论: 把几分钟的原始视频完整读入内存和显存,然后进行计算,是不现实的。


策略:训练与测试中的“采样”

既然我们处理不了“完整”视频,我们就处理“采样后”的视频。

  1. 训练策略:

目标: 我们在“短视频片段 (Clips)”上训练模型。

做法: 从原始长视频中,随机采样一小段,例如 \(T=16\) 帧。

降维: 为了进一步减少计算量,我们还会:

  • 降低帧率 (Low FPS): 比如从30fps降到5fps。这样16帧就能覆盖 \(16 / 5 = 3.2\) 秒的内容。
  • 降低空间分辨率 (Low Spatial Resolution): 比如从 \(1920 \times 1080\) 降到 \(112 \times 112\)

结果: 我们的输入从一个巨大的张量变成了一个可管理的 \(16 \times 3 \times 112 \times 112\) 的小张量。

  1. 测试策略:

目标: 预测一个(可能很长的)新视频的标签。

做法:

  • 在长视频上,用“滑动窗口”的方式,取出很多个重叠或不重叠的短视频片段 (Clips)。
  • 把每一个短片段都送入我们训练好的模型,得到一个分类预测(例如一个概率分布)。
  • 平均 (Average) 所有短片段的预测结果,作为整个长视频的最终预测。
  1. 视频分类的架构演进

现在,我们有了 \(T \times 3 \times H \times W\) 的短视频片段,该如何设计神经网络来给它分类呢?


架构一:单帧CNN (Single-Frame CNN)

完全无视时间维度。从视频片段的 \(T\) 帧里,随机选1帧 (或者单独处理每一帧)。把它当作一张普通图片,送入一个2D CNN (例如ResNet, VGG)。训练这个2D CNN来预测动作标签。测试时,对所有帧的预测结果进行平均。

优点: 简单,快,能直接用ImageNet预训练好的模型。

缺点: 完全没有利用时序信息。它分不清“跑步”和“站立”(如果某一帧都是人站着的样子),也分不清“开门”和“关门”。

惊人的事实: 尽管如此,这是一个非常强的基准 (Strong Baseline)。为什么?因为很多动作和场景、物体是强相关的。比如,你看到一张有“水池”和“泳衣”的帧,你大概率就能猜到动作是“游泳”,根本不需要看他动起来。


架构二:后期融合 (Late Fusion)

单帧CNN太傻了,我们至少得把所有帧的信息都“融合”一下。

“后期”的意思是,我们让每一帧独立地通过CNN,直到很“后期”(得到高层特征)时,才把它们融合起来。

怎么做:

  1. 输入 \(T\) 帧视频。
  2. 让每一帧都通过一个共享权重的 2D CNN,提取特征。我们会得到 \(T\) 个特征图 (Feature Maps)。
  3. 融合 (Fuse):
  • 方法A: 把这 \(T\) 个特征图全部Flatten(展平)成一个超级长的向量,然后接几个全连接层 (MLP) 来分类。
  • 方法B: 在时空维度上进行平均池化 (Average Pool),把 \(T \times D \times H' \times W'\) 的特征图池化成一个 \(D\) 维的向量,再接一个线性分类器。这个更常用。

优点: 比单帧好,考虑了所有帧。

缺点: 融合得太晚了。每个CNN在提取特征时,都是独立看图的。它们提取的是“人”、“腿”、“路”这种高级语义特征。当你想融合时,你得到的是 \(T\) 组“人、腿、路”的特征。但模型很难从这些高级特征中,去比较底层的运动信息(比如“腿”在 \(t=1\) 时和 \(t=2\) 时的细微位移)。这种底层运动信息在通过CNN时已经丢失了。


架构三:早期融合 (Early Fusion)

既然“后期融合”太晚了,那我们就“早期融合”。在输入端,就把所有帧“黏合”在一起,让网络在第一层就看到所有时序信息。

输入 \(T\) 帧视频,维度 \(T \times 3 \times H \times W\)

在通道维度 (Channel) 上把它们堆叠起来 (Stack)。

输入张量变为 \(1 \times (3 \times T) \times H \times W\)

把它送入一个2D CNN。这个CNN的第一个卷积层很特殊:它的输入通道数是 \(3T\) (e.g., \(16 \times 3 = 48\))。

这个特殊的卷积核(例如 \(D \times (3T) \times 3 \times 3\))在计算时,会同时看到所有 \(T\) 帧的像素。

一旦过了第一层,输出变为 \(D \times H' \times W'\),所有时序信息就被“压扁”了。之后就是个标准的2D CNN。

  • 优点: 确实在底层(像素级)比较了时序信息。
  • 缺点: 融合得太早了。只有第一层在处理时序,后面的层都无法再学习更复杂的时序关系。“一层的时序处理可能是不够的!”

架构四:3D 卷积神经网络 (3D CNN)

使用3D卷积核 (3D Conv) 和 3D池化 (3D Pooling) 的神经网络。

怎么做:

3D 卷积核:

  • 2D 卷积核的形状是 \((C_{in}, C_{out}, K_h, K_w)\)
  • 3D 卷积核的形状是 \((C_{in}, C_{out}, \mathbf{K_t}, K_h, K_w)\)

它不仅在 \(H\)\(W\) 维度上滑动,它也在 \(T\) (时间) 维度上滑动。

核心对比:

  • 后期融合 (Late Fusion): 空间上缓慢融合,时间上一次性融合(在最后)。
  • 早期融合 (Early Fusion): 空间上缓慢融合,时间上一次性融合(在最前)。

3D CNN (Slow Fusion): 空间上缓慢融合,时间上也缓慢融合。

这使得3D CNN可以学习到层级化 (Hierarchical) 的时空特征。例如:

  • 第一层:检测简单的时空边缘(比如一条线向右移动)。
  • 第二层:组合成简单的运动模式(比如“摆动”)。
  • 第三层:组合成复杂的动作基元(比如“挥手”)。

3D CNN vs 2D CNN (早期融合):

这是对 3D CNN 优势的进一步解释。

2D 早期融合: 它的卷积核 \((C_{out}, (C_{in} \times T), 3, 3)\),它没有在时间T维度上滑动。

  • 致命缺陷: 没有时序平移不变性 (Temporal Shift-Invariance)。

这意味着,如果模型学习了一个“在 \(t=1,2,3\) 帧发生的挥手”的检测器,它无法用这个检测器去检测“在 \(t=4,5,6\) 帧发生的同一个挥手”。它必须为每个时间片,重新学习一遍所有模式。这极度浪费参数和数据。

3D CNN: 它的卷积核 \((C_{out}, C_{in}, 3, 3, 3)\),它在时间T维度上滑动。

核心优势: 具有时序平移不变性!模型只需要学习一个“挥手”检测器(一个3D卷积核),这个核在时间维度上滑动,就可以在视频的任何时间点检测到这个动作。这才是正确且高效的学习方式。

可视化: 3D CNN的第一层卷积核,可视化出来就是一些小小的“视频片段”,它们在检测特定的时空运动模式。


架构演进一:C3D

C3D 模型: C3D 是 3D CNN 时代的一个经典模型,就像 VGG 是 2D CNN 的经典一样。

怎么做: 一个纯粹由 \(3 \times 3 \times 3\) 卷积和 \(2 \times 2 \times 2\) 池化(或 \(1 \times 2 \times 2\))堆叠起来的网络。

问题: 3D 卷积的计算量极其巨大。

一个 \(3 \times 3\) 2D 卷积是 9 次乘法。

一个 \(3 \times 3 \times 3\) 3D 卷积是 27 次乘法。

对比:C3D (39.5 GFLOPs) 的计算量是 VGG-16 (13.6 GFLOPs) 的近3倍!

结果: 尽管很贵,但效果很好。在 Sports-1M 数据集上,3D CNN (80.2) 和 C3D (84.4) 的效果,远超了早/后期融合 (76.8 / 78.7)。这证明了“Slow Fusion”和“时序平移不变性”的巨大威力。


架构演进二:双流网络 (Two-Stream Networks)

3D CNN 把“外观”(Appearance) 和“运动”(Motion) 混在一起学习,但这两者可能是可以解耦的。

人类(甚至动物)可以仅凭运动信息就识别出动作。“光点行走者”实验就是证明:你只看几个光点(代表关节)的运动,就能认出这是“人”在“行走”。

3D CNN 的计算量太大了。

一个“双流”架构,明确地把外观和运动分开处理。

  • 流1:空间流 (Spatial Stream): 负责看“长什么样”。
  • 流2:时序流 (Temporal Stream): 负责看“怎么动”。

怎么做:

  1. 空间流 (Spatial Stream):
  • 输入: 1帧 RGB 图像。
  • 网络: 一个标准的 2D CNN。
  • 目标: 学习识别场景和物体(e.g., “这是一个厨房,有个人拿着刀”)。
  1. 时序流 (Temporal Stream):

关键:如何只表示“运动”? 答案是光流 (Optical Flow)。

光流是什么?光流是一个 \(H \times W \times 2\) 的向量场 \(F(x,y) = (dx, dy)\)。它估计了 \(t\) 时刻的像素 \((x,y)\)\(t+1\) 时刻会移动到 \((x+dx, y+dy)\)

\(dx\) 通道表示水平运动,\(dy\) 通道表示垂直运动。光流图只包含运动,抹去了所有外观信息。

  • 输入: 堆叠 \(T\) 帧的光流图(例如 \(2(T-1) \times H \times W\))。
  • 网络: 另一个 2D CNN(用“早期融合”的方式处理堆叠的光流)。
  • 目标: 学习识别运动模式(e.g., “有东西在做上下切割的动作”)。
  • 融合 (Fusion): 在最后,把两个流的分类得分 (Softmax) 进行平均,得到最终预测。

结果: 效果拔群!在UCF-101数据集上:

  • 3D CNN (原文的): 65.4
  • 空间流 (单帧): 73.0 (再次证明了强基准)
  • 时序流 (仅光流): 83.7
  • 双流融合: 88.0

重要洞察: 时序流(运动)的性能 (83.7) 远高于 空间流(外观)(73.0)!这意味着对于动作识别,“怎么动” (Motion) 比“长什么样” (Appearance) 更重要。

缺点: 必须预先计算光流,这是一个非常慢、非常耗时的步骤,它本身就是一个复杂的算法,拖慢了整个流程。


架构演进三:长时序建模 (Long-Term)

为什么: 到目前为止,所有模型 (3D CNN, 双流) 都只处理2-5秒的短视频片段。

问题: 很多动作需要更长的时间尺度才能理解。例如,要区分“打开烤箱”和“关闭烤箱”,你可能需要看到完整的“开-放东西-关”的序列。短片段模型无法捕捉这种长时序依赖 (long-term dependencies)。

核心思想: 我们已经学过了处理序列的利器——循环神经网络 (RNN)!


方案A:CNN + RNN

是什么: 一个两阶段的混合模型。

怎么做:

  1. 阶段一 (CNN 特征提取):
  • 把长视频切成 \(N\) 个不重叠的短片段 (Clips)。
  • 用一个 2D 或 3D CNN (例如 C3D) 作为特征提取器,处理每一个短片段。
  • 得到一个特征向量序列:\(x_1, x_2, ..., x_N\)
  1. 阶段二 (RNN 序列建模):
  • 把这个特征向量序列送入一个 RNN (例如 LSTM)。
  • 多对一 (Many-to-one): 使用RNN的最后一个隐藏状态 \(h_N\),来对整个视频进行分类。
  • 多对多 (Many-to-many): 使用RNN的每一个隐藏状态 \(h_t\),来对当前时刻的动作进行分类(这用于“时序动作定位”)。

  • CNN (局部): 负责学习局部的时空结构(5秒内的短动作)。

  • RNN (全局): 负责学习全局的时序结构(多个动作片段如何组合)。

缺点: 为了节省显存和时间,CNN部分通常是“冻结”的(作为固定特征提取器,不参与反向传播),这限制了模型的表达能力。


方案B:循环卷积网络 (Recurrent ConvNet)

为什么: CNN+RNN 的模型很“割裂”。RNN (LSTM) 是全连接的,它会丢失 CNN 提取的特征图中的空间结构。

怎么做:

回顾: 一个标准RNN的数学公式是:

\[h_t = f_W(h_{t-1}, x_t)\]

其中 \(h_{t-1}\) 是上一时刻的向量, \(x_t\) 是当前时刻的向量。 \(f_W\) 是带参数 \(W\) 的函数(例如 \(tanh(W_h h_{t-1} + W_x x_t)\)),这里的 \(W_h\)\(W_x\) 是矩阵,操作是矩阵乘法 (Matrix Multiply)。

核心思想:把 \(h_t\)\(x_t\) 从“向量”升级为“特征图” (e.g., \(C \times H \times W\))。把“矩阵乘法” (\(W_h h_{t-1}\)) 替换为“卷积操作” (\(W_h * h_{t-1}\))。

公式变为:\(h_t = \tanh(W_h * h_{t-1} + W_x * x_t)\)

“知其所以然”:

  • 这个网络在垂直方向上(层与层之间)是卷积 (2D Conv)。
  • 它在水平方向上(时间与时间之间)是循环 (Recurrent)。

它完美地结合了CNN(保持空间结构)和RNN(无限时序感受野)的优点。

缺点: 只要是RNN,它在时间维度上就必须串行计算(\(t\) 时刻依赖 \(t-1\) 时刻),无法并行化,导致训练非常缓慢。


架构演进四:注意力与Transformer

为什么: RNN太慢了。我们需要一种可以并行、又能建模长时序依赖的机制。

是什么: 自注意力机制 (Self-Attention) (也就是 Transformer 的核心)。

  1. 方案A:非局部网络 (Non-local Block)

回顾自注意力:

目标:计算 \(y_i\) (位置 \(i\) 的新特征)。

\[y_i = \sum_j a_{ij} v_j\]

\(y_i\) (输出) 是所有位置 \(j\)\(v_j\) (Value) 的加权平均。

权重 \(a_{ij}\) (Attention) 来自 \(q_i\) (Query) 和 \(k_j\) (Key) 的匹配度(e.g., \(q_i \cdot k_j\))。

\(y_i\) 的计算直接依赖了所有 \(j\) 的信息,无论 \(i\)\(j\) 相距多远。这就是“长时序依赖”。并且,所有 \(y_i\) 的计算可以完全并行。

怎么做 (Non-local Block):

Non-local Block 就是把自注意力机制应用到视频特征图上。

  • 输入: 一个3D CNN生成的特征图 \(C \times T \times H \times W\)
  • 向量化: 我们不把特征图看作 \(C \times T \times H \times W\),而是看作 \(N\)\(C\) 维向量,其中 \(N = T \times H \times W\)
  • Q, K, V: 用 \(1 \times 1 \times 1\) 的卷积核(等价于一个线性层)来生成 \(Q, K, V\)
  • 注意力图: 计算 \(Q\)\(K\) 的矩阵乘法,得到一个 \(N \times N\) (即 \((THW) \times (THW)\)) 的注意力图。

这个 \(N \times N\) 的图,学到了时空中任意两个点 \((t, h, w)\)\((t', h', w')\) 之间的相关性。

输出: 用这个注意力图去加权平均 \(V\),最后用一个 \(1 \times 1 \times 1\) 卷积还原维度,再加上一个残差连接 (Residual Connection)。

Non-local Block 是一个可以即插即用的模块。

你可以把它插入到任何 3D CNN 架构中(例如 ResNet-3D)。

它让 3D CNN(原本只有 \(3 \times 3 \times 3\) 的局部感受野)获得了全局的时空感受野。

  1. 方案B:I3D (Inflated 3D Networks)

为什么: Non-local 解决了感受野问题,但 3D CNN 的架构设计和预训练仍然是个大问题。视频数据集(几十万)远小于图像数据集(ImageNet 120万)。

I3D (Inflated 3D) 是一种“膨胀”和“初始化” 3D CNN 的绝妙技巧。

怎么做:

  1. 架构“膨胀” (Inflation):

拿一个在 ImageNet 上预训练好的 2D CNN (例如 Inception v1)。

把架构中所有的 2D 卷积 (e.g., \(3 \times 3\)) “膨胀”成 3D 卷积 (e.g., \(3 \times 3 \times 3\))。

把所有的 2D 池化 (e.g., \(3 \times 3\)) “膨胀”成 3D 池化 (e.g., \(3 \times 3 \times 3\))。

  1. 权重“初始化” (Bootstrapping):

怎么初始化 3D 卷积核 \((K_t, K_h, K_w)\) 的权重?

把预训练好的 2D 卷积核 \((K_h, K_w)\) 的权重,在时间维度上复制 \(K_t\) 次。

然后: 除以 \(K_t\)

通过这种初始化,如果你的 3D CNN 的输入是一个“静止”的视频(即所有帧都相同),那么这个 3D CNN 每一层的输出,都将和原来的 2D CNN 的输出完全一致。

这相当于在训练开始时,你的 3D CNN “继承”了 2D CNN 强大的空间特征提取能力。它只需要在视频数据上微调 (finetune),学会时间维度 (\(K_t\)) 的信息就行了。

结果: 这是一个巨大的飞跃。

“从头训练” (Train from scratch, 蓝色) 的效果,总是远远差于 “ImageNet预训练” (Pretrain on ImageNet, 橙色) 的效果。

I3D + Two-Stream(即空间流和时序流都用I3D架构,时序流的输入是光流)达到了当时SOTA (74.2)。

  1. 方案C:视频 Transformer (ViT)

Transformer 在NLP和图像领域都取得了统治地位,视频也不例外。

把视频也当作一个“序列”来处理。

怎么做:

  • 分块 (Patching): 把 \(T \times H \times W\) 的视频,切成一堆 \(P_t \times P_h \times P_w\) 的时空小方块 (Tubelets)。
  • 嵌入 (Embedding): 把每个小方块展平,通过一个线性层,映射成一个“Token”(一个向量)。
  • 加上 [CLS] Token 和时空位置编码 (Position Embedding)。
  • 把这个 Token 序列送入一个标准的 Transformer Encoder(一堆 Self-Attention + MLP)。
  • 最后用 [CLS] Token 的输出来分类。

挑战与优化:

  • 计算量爆炸: 自注意力的计算量是 \(O(N^2)\),而 \(N = (T \times H \times W) / (P_t P_h P_w)\),这个 \(N\) 非常大。
  • 优化方案:

Factorized Attention: 把全时空注意力,分解为“先做空间自注意力” \(\rightarrow\) “再做时间自注意力”。

Pooling / Multiscale (MViT): 在 Transformer 的不同层之间,像CNN一样,通过池化或带步长的卷积,来减少Token的数量,逐层降低计算量。

VideoMAE: 借鉴NLP的BERT和图像的MAE,提出一种高效的预训练方法:随机遮盖 (Mask) 掉 90% 的时空方块,然后让模型去“重建”这些被遮住的方块。这强迫模型学会了时空的内在联系。

结果:

Transformer 架构(如 MViTv2, VideoMAE)全面超越了 3D CNN 和 I3D,成为新的SOTA。VideoMAE V2-g 在 Kinetics-400 上达到了 90% 的惊人准确率。


模型可视化

模型是个黑盒子,我们想知道它到底学到了什么。

  • 用“梯度上升”的方法。从一个空白/噪声图像开始,反向传播最大化某个类的得分(例如“举重”)。

我们可以分别对“空间流”(输入是图像)和“时序流”(输入是光流)来做。

对于时序流,还可以调整正则项,来分别可视化“慢速运动”和“快速运动”关注什么。

结果:

举重 (Weightlifting):

  • 外观流 学到了“杠铃”的圆形外观。
  • 慢速运动流 学到了“杠铃弯曲和抖动”的模式。
  • 快速运动流 学到了“向上推举”的爆发性动作。

画眼妆 (Apply Eye Makeup):

  • 外观流 学到了“人脸”的轮廓。
  • 运动流 学到了“手到眼睛”的精细运动。

结论: 这证明了双流模型确实在按我们的设想工作,空间流看“是什么”,时序流看“怎么动”。


视频中的检测任务

时序动作定位 (Temporal Action Localization):

  • 任务: 不仅要分类,还要告诉我在时间上(第几秒到第几秒)发生了这个动作。
  • 类比: 1D版本的目标检测。
  • 时空动作检测 (Spatio-Temporal Detection):

任务: 终极任务。在时空中定位动作。

输出: 一系列 (x, y, w, h, \(t_start\), \(t_end\), \(action_label\)) 的“动作管道 (Action Tubes)”。


多模态:音视频结合

视频不仅有“视觉”,还有“听觉”。

麦格克效应 (McGurk Effect): 给你听 “Ba” 的声音,但看 “Fa” 的口型,你的大脑会“脑补”出一个“Fa”的声音。

结论: 听觉和视觉是深度耦合的,一起使用效果更好。

应用:

  • 视觉引导的声源分离 (Visual-guided source separation):

  • 视频里有两个人同时说话,音频是混在一起的。模型可以通过看口型,把两个人的声音分离开!

  • 视频里有“小提琴”和“长笛”,模型可以通过看物体,把两种乐器的声音分离开!

架构: 各种 Transformer 架构,例如 Audio-Visual MAE,它们有两个输入分支(一个处理图像,一个处理音频频谱图),并在网络内部进行深度融合。


高效视频理解 (Efficient Video Understanding)

逐帧处理视频(或者用 3D CNN)太慢了,而且很冗余(相邻帧通常高度相似)。

怎么做:

SCSampler: 训练一个“策略网络”,它会智能地采样视频中那些“信息量最大”的关键片段 (Salient Clips),只在这些片段上运行昂贵的 3D CNN。

AdaMML: 自适应多模态学习。模型会动态决定用哪些模态。比如先用“廉价”的音频特征,如果搞不定,再启用“昂贵”的RGB或光流特征。

Listen to Look: 用音频作为“预览”。模型先听音频,如果音频很平淡,就(低功耗)跳过;如果听到“砰”的一声,再(高功耗)启动视觉模型,去“看”发生了什么。


前沿:视频 + 大语言模型 (LLMs)

是什么: 这是目前最火的方向。

怎么做:

用一个强大的视频理解模型(例如 VideoMAE)作为“视频编码器”,把视频转化成一系列特征向量。

把这些特征向量,送入一个大语言模型 (LLM)(例如 LLaMA, GPT)。

通过训练,让 LLM 学会“理解”这些视频特征。

结果: 产生了 Video-ChatGPT, Video-LLaVA 等模型,你可以用自然语言和视频进行“对话”。


第11章:分布式训练与大规模模型

硬件基础 - GPU集群

要谈分布式训练,首先要懂我们的计算工具。我们以NVIDIA H100 GPU为例。

GPU是什么?

GPU (Graphics Processing Unit):图形处理器。它最初是为电脑游戏渲染图像而设计的,但人们发现它内部有成百上千个小核心,非常适合做“并行计算”(同时做很多件简单的数学题),这正是深度学习所需要的。 核心组件 (H100为例):

  • 计算核心 (Compute Cores):真正干活的“计算器”。
  • HBM显存 (HBM Memory):高速内存,容量80GB。模型参数、训练数据、中间结果都存在这里。
  • 带宽 (Bandwidth):数据在“计算核心”和“显存”之间的传输速度,高达 3352 GB/sec。这个速度至关重要,如果带宽太低,计算核心就会“饿肚子”,没事干,效率就低了。

GPU的心脏 - SM和Tensor Core

SM (Streaming Multiprocessor,流式多处理器):GPU计算核心的基本单元。可以把它想象成一个“车间”,一台H100有132个这样的“车间”。

SM内部:

  • FP32核心:传统的计算单元,做32位浮点数计算(比如 \(a \times x + b\))。这种计算精度高,但速度相对慢。
  • Tensor Core (张量核心)这是GPU能高效跑深度学习的秘密武器! 专为矩阵运算(\(A \times X + B\))设计。

为什么Tensor Core这么重要?

  • FP32核心:每周期做一次 \(a \times x + b\),包含一次乘法和一次加法,计为 2 FLOPs (浮点运算次数)。
  • Tensor Core:每周期能处理一个小的矩阵乘法,比如 \([16 \times 4] \times [4 \times 8] + [16 \times 8]\)
  • 这个小矩阵乘法包含 \(16 \times 4 \times 8 = 512\) 次乘法,再加上 \(16 \times 8 = 128\) 次加法。总共的计算量(FLOPs)大约是 \(2 \times 512 \approx 1024\) FLOPs (在深度学习中,乘加运算常合并计算)。
  • Tensor Core (1024 FLOPs/周期) vs FP32核心 (2 FLOPs/周期)。一个Tensor Core的效率是一个FP32核心的几百倍!这就是为什么现代GPU(如V100, A100, H100)的算力图中,Tensor Core的算力(绿线)呈指数级暴涨。

GPU算力的飞跃

  • 从2013年到2025年,GPU算力提升了1000倍,这绝大部分归功于Tensor Core的引入和迭代
  • 这也意味着,为了充分利用GPU,我们的算法必须尽可能地使用低精度(如16位)的矩阵运算来“喂饱”Tensor Core。

从GPU到集群 - 硬件的层级结构

光有一块GPU不够,我们要用几万块。但GPU之间如何连接,决定了我们的训练策略。

层级1:GPU (内部带宽: 3352 GB/sec)

层级2:GPU服务器 (Server)

  • 通常包含8块GPU。
  • GPU之间通过高速总线(如NVLink)连接,带宽极高 (900 GB/sec)。这是GPU之间最快的通信方式。

层级3:服务器机架 (Rack)包含2台服务器(16块GPU)。

层级4:GPU Pod

  • 包含192个机架(3072块GPU)。
  • 注意:不同服务器之间的连接带宽 (50 GB/sec) 远低于服务器内部GPU之间的带宽 (900 GB/sec)。

层级5:GPU集群 (Cluster)

  • 8个Pod(总共24,576块GPU)。
  • Pod之间的带宽更低 (< 50 GB/sec)。

这个硬件层级结构和急剧下降的带宽是理解后续所有并行策略的关键

  • 服务器内 (900 GB/sec):通信“代价小”,可以玩一些需要频繁通信的骚操作(比如张量并行, TP)。
  • 服务器间 (50 GB/sec):通信“代价大”,要尽量减少通信次数和通信量(比如数据并行, DP)。

核心问题与四大并行策略

问题:我们有一个 \(L\) 层的巨大模型,它处理的数据形状为 (Batch, Sequence, Dim)

  • Batch: 批次大小,一次处理多少个样本。
  • Sequence: 序列长度,比如一句话里有多少个词。
  • Dim: 隐藏层维度,一个词用多长的向量表示。

如何把这个巨大的计算任务,拆分到这24,576块GPU上?

有四种“拆分”的维度:

  1. 数据并行 (Data Parallelism, DP):按 Batch 维度拆分。(最常用)
  2. 上下文并行 (Context Parallelism, CP):按 Sequence 维度拆分。(用于超长序列)
  3. 流水线并行 (Pipeline Parallelism, PP):按 L (层) 维度拆分。(用于超深模型)
  4. 张量并行 (Tensor Parallelism, TP):按 Dim 维度拆分。(用于超宽模型)

并行策略详解

数据并行 (DP)

概念与数学原理

概念:这是最简单、最常见的并行方式。

核心思想“人多力量大,每人算一块”

  • 我们有 \(M\) 块GPU。
  • 每块GPU都复制一份完整的模型和优化器。
  • 我们把总共 \(M \times N\) 大小的批次数据,切成 \(M\) 份,每块GPU分到 \(N\) 个数据。

数学原理

  • 我们的目标是计算总损失 \(L\) 对权重 \(W\) 的梯度 \(\frac{\partial L}{\partial W}\)
  • 总损失是所有 \(M \times N\) 个样本损失的平均值:
\[L = \frac{1}{MN} \sum_{i=1}^{M} \sum_{j=1}^{N} l(x_{i,j}, W)\]
  • 由于求导是线性运算,梯度可以写作:
\[\frac{\partial L}{\partial W} = \frac{\partial}{\partial W} \left( \frac{1}{M} \sum_{i=1}^{M} \left( \frac{1}{N} \sum_{j=1}^{N} l(x_{i,j}, W) \right) \right)\]
  • \(\frac{1}{N} \sum_{j=1}^{N} l(x_{i,j}, W)\) 正是第 \(i\) 块GPU上的本地损失 \(L_i\)
  • 所以,总梯度 \(\frac{\partial L}{\partial W} = \frac{1}{M} \sum_{i=1}^{M} \frac{\partial L_i}{\partial W}\)

这个公式告诉我们,我们只需要让每块GPU在自己的数据上算出本地梯度 \(\frac{\partial L_i}{\partial W}\),然后所有GPU把它们的梯度加起来求个平均值,就得到了全局的总梯度。

DP的执行步骤

  1. 复制:每块GPU拿一份完整的模型 \(W\) 和优化器。
  2. 分发:每块GPU拿一份自己的数据(\(x_{1, \dots}\) 给GPU 1, \(x_{2, \dots}\) 给GPU 2)。
  3. 前向传播:每块GPU独立计算自己的损失 \(L_i\)
  4. 反向传播:每块GPU独立计算自己的本地梯度 \(G_i = \frac{\partial L_i}{\partial W}\)
  5. 同步 (关键步骤):所有GPU通过一个叫 All-Reduce 的通信操作,平均它们各自的梯度。\(G_{\text{total}} = \frac{1}{M} \sum G_i\)
  6. 更新:现在,每块GPU上都有了完全相同\(G_{\text{total}}\)。它们各自调用优化器(比如Adam)用 \(G_{\text{total}}\) 更新自己的那份 \(W\)
  • 因为初始 \(W\) 相同, \(G_{\text{total}}\) 也相同,所以更新后的 \(W\) 在所有GPU上依然保持完全一致

DP的致命问题

问题模型大小受限于单块GPU的显存。

为什么? 因为步骤1要求每块GPU都装下一份完整的模型。

训练时,显存里不仅要放模型参数 \(W\) (比如BF16, 占2字节/参数),还要放梯度 \(G\) (2字节/参数),以及优化器状态(比如Adam需要 \(\beta_1\)\(\beta_2\) 两个动量,通常用FP32存,占 \(4+4=8\) 字节/参数)。

  • 总共:\(2 + 2 + 8 = 12\) 字节/参数。
  • 一个 10B (100亿) 参数的模型需要:\(10 \times 10^9 \times 12 \approx 120 \text{ GB}\) 显存。
  • 一块H100只有 80GB 显存,根本装不下!

  • 结论:当模型大到一块GPU装不下时,传统的数据并行(DP)就失效了。


全分片数据并行 (FSDP / ZeRO)

FSDP (Fully Sharded Data Parallelism),也叫 ZeRO (Zero Redundancy Optimizer),是DP的“内存优化版”。

核心思想:既然每块GPU存一份完整的模型太浪费,那我们就把模型参数 \(W\)、梯度 \(G\) 和优化器状态 \(O\) 都切成 \(M\),每块GPU(\(M\)块之一)只“拥有”并负责更新其中的 \(1/M\) 片。

它本质上还是数据并行(每块GPU还是处理自己的数据),但模型状态被分片 (Shard) 存储了。

FSDP的执行步骤 (以2块GPU为例)

这是一个非常精妙的过程,我们慢放一遍:

初始状态:GPU 1 “拥有” \(W_1, W_2\)。GPU 2 “拥有” \(W_3, W_4\)

前向传播 (Forward Pass):

  1. 计算 \(W_1\):GPU 1 是 \(W_1\) 的“主人”,它把 \(W_1\) 广播 (broadcast) 给GPU 2。
  2. 现在两块GPU都有了 \(W_1\)。它们并行地在各自的数据上计算第一层。
  3. 计算完毕,GPU 2(非主人)立即删除 \(W_1\) 释放显存。
  4. 计算 \(W_2\):GPU 1 是 \(W_2\) 的“主人”,它把 \(W_2\) 广播给GPU 2。
  5. 两块GPU并行计算第二层,然后GPU 2删除 \(W_2\)
  6. ...以此类推...
  7. 计算 \(W_3\):GPU 2 是 \(W_3\) 的“主人”,它把 \(W_3\) 广播给GPU 1。
  8. 两块GPU并行计算第三层,然后GPU 1(非主人)删除 \(W_3\)
  • 反向传播 (Backward Pass):
  1. 计算 \(G_4\) ( \(W_4\) 的梯度)
  • GPU 2 ( \(W_4\) 的主人) 广播 \(W_4\) 给GPU 1。
  • 两块GPU都拿到了 \(W_4\),各自计算出本地梯度 \(G_{1,4}\) (GPU 1的) 和 \(G_{2,4}\) (GPU 2的)。
  • 计算完毕,GPU 1 (非主人) 立即删除 \(W_4\)
  1. 规约 (Reduce) \(G_4\)
  • GPU 1 (非主人) 把它的 \(G_{1,4}\) 发送给 GPU 2 (主人)。
  • GPU 2 把它收到的 \(G_{1,4}\) 和自己的 \(G_{2,4}\) 相加,得到总梯度 \(G_4 = G_{1,4} + G_{2,4}\)。(这叫 Reduce-Scatter
  • 非主人 (GPU 1) 立即删除 \(G_{1,4}\) 释放显存。
  1. 计算 \(G_3\):GPU 2 ( \(W_3\) 主人) 广播 \(W_3\),... 两块GPU计算 \(G_{1,3}\)\(G_{2,3}\),... GPU 1 删除 \(W_3\)
  2. 规约 \(G_3\):GPU 1 发送 \(G_{1,3}\) 给 GPU 2,GPU 2 累加得到 \(G_3\),... GPU 1 删除 \(G_{1,3}\)
  3. ...以此类推...

优化器更新 (Optimizer Step):

  • 在反向传播算完 \(G_4\) 并规约后,GPU 2(\(W_4\)的主人)立即\(G_4\) 更新它拥有的 \(W_4\)
  • 在反向传播算完 \(G_3\) 并规约后,GPU 2(\(W_3\)的主人)立即\(G_3\) 更新它拥有的 \(W_3\)
  • 关键:每块GPU只在本地更新它“拥有”的那部分参数。

FSDP的精髓 - 重叠 (Overlap)

  • 上面一步一步看,是不是觉得通信量好大,好慢?
  • 精髓在于:计算和通信是可以重叠的!
  • 前向:在用 \(W_i\) 计算的同时,就可以去预取 (Fetch) 下一层要用的 \(W_{i+1}\) 了。
  • 反向:这是最巧妙的。当在用 \(W_i\) 计算反向传播时,可以同时做三件事:
  1. 计算 \(G_i\) (使用 \(W_i\))。
  2. 通信:把上一层算完的梯度 \(G_{i+1}\) 发送给它的主人 (Reduce-Scatter)。
  3. 通信预取 (Fetch) 下一层要用的 \(W_{i-1}\)
  • 通过这种方式,通信的延迟被巧妙地隐藏在了计算的时间中。

FSDP总结

  • 优点:显存占用被平摊到 \(M\) 块GPU上(\(O(P/M)\)),可以训练无比巨大的模型。
  • 代价:通信量比DP大得多。DP每一步只有1次 All-Reduce,FSDP每层都有 广播 + Reduce-Scatter

混合分片数据并行 (HSDP)

问题:FSDP的通信量太大了 (每层3次通信),如果GPU之间的网络很慢(比如跨服务器、跨Pod),那FSDP的效率会很低。 策略:结合硬件层级,把DP和FSDP混用。 概念:把 \(N\) 块GPU分成 \(M\),每个内有 \(K\) 块GPU。

  • 组内 (K块GPU):这些GPU在同一个服务器/Pod内,网络快 (900 GB/sec),让它们跑 FSDP。它们共同分担一份模型的 \(K\) 片。
  • 组间 (M个组):这些组跨服务器/Pod,网络慢 (50 GB/sec),让它们跑 DP

执行

  1. 组1 (K块GPU) 跑FSDP,算出一个总梯度 \(G_{\text{group 1}}\)
  2. 组2 (K块GPU) 跑FSDP,算出一个总梯度 \(G_{\text{group 2}}\)
  3. ...
  4. 所有 \(M\) 个组,在组与组之间做一次DP的 All-Reduce,算出最终梯度 \(G_{\text{final}} = \frac{1}{M} \sum G_{\text{group_i}}\)

HSDP是一种权衡。它把“高通信量”的FSDP限制在“高速网”内部,把“低通信量”的DP用在“低速网”上,完美匹配了硬件的层级结构。


激活重计算 (Activation Checkpointing)

FSDP解决了模型参数优化器状态的显存问题。但是,训练时还有一个显存大户:激活值 (Activations)

为什么?

  • 前向传播:\(A_1 = F_1(A_0)\), \(A_2 = F_2(A_1)\), ...
  • 反向传播:\(\frac{\partial L}{\partial W_2}\) 需要用到 \(A_1\) ( \(F_2\) 的输入)。\(\frac{\partial L}{\partial W_1}\) 需要用到 \(A_0\)
  • 所以,标准的反向传播算法要求我们保存下前向传播过程中的所有中间激活值 (\(A_1, A_2, \dots, A_N\)),这在模型很深 (L很大) 或序列很长 (Sequence很大) 时,会吃掉海量显存。
  • Llama3-405B的激活值要占63GB,一块H100又满了!

解决方案用计算换显存

  • 思路:前向传播时,不保存所有的激活值,只保存其中几个(称为“检查点”)。
  • 反向传播时:当需要一个没被保存的激活值 \(A_i\) 时,我们从离它最近的上一个检查点 \(A_k\) ( \(k < i\) ) 开始,重新计算前向传播,直到得到 \(A_i\)

两种极端:

  1. 不重计算 (标准训练)
  • 显存:\(O(N)\),保存 \(N\) 层所有激活值。
  • 计算:\(O(N)\),一次前向 + 一次后向。
  1. 完全重计算 (只存输入)
  • 显存:\(O(1)\),只保存最开始的输入 \(A_0\)
  • 计算:\(O(N^2)\)。反向传播到第 \(i\) 层时,都要从 \(A_0\) 重新算 \(i\) 次前向。总计算量是 \(1 + 2 + \dots + N \approx O(N^2)\)。太慢了!
  1. 最优策略 (Checkpointing)
  • 策略:在前 \(N\) 层中,每隔 \(C\) 层(比如 \(C = \sqrt{N}\) 层)保存一个检查点。
  • 显存:只需要保存 \(N/C = N / \sqrt{N} = \sqrt{N}\) 个检查点。显存占用从 \(O(N)\) 降到了 \(O(\sqrt{N})\)
  • 计算:反向传播时,最多只需要从最近的检查点重新计算 \(C = \sqrt{N}\)。总计算量是 \(O(N) \times O(\sqrt{N}) = O(N\sqrt{N})\)
  • 激活重计算是一个时间和空间的美妙权衡 (trade-off)。我们用 \(O(N\sqrt{N})\) 的计算(只比 \(O(N)\) 慢一点点),换来了 \(O(\sqrt{N})\) 的显存(比 \(O(N)\) 好太多了)。

测量效率:模型FLOPs利用率 (MFU)

我们搞了这么多花里胡哨的并行策略(HSDP, Checkpointing...),还引入了额外的通信和重计算。我怎么知道我的训练效率高不高?

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

概念:你的GPU理论上每秒能算 989 TFLOPs (H100的Tensor Core算力),但你的训练代码实际上每秒只让它算了多少“有用的”FLOPs?

HFU (Hardware FLOPs Utilization)

先看一个理想上限。如果我们只跑矩阵乘法,能跑到GPU理论性能的百分之多少?

答案:大概80%。因为还有一些加载、调度开销。这是我们能期待的天花板

MFU (Model FLOPs Utilization)

  • “有用的”FLOPs:我们只关心模型中真正消耗算力的部分,即矩阵乘法(比如全连接层和Attention里的矩阵乘法)。我们忽略非线激活函数(ReLU)、归一化(LayerNorm)等小计算。
  • 如何计算 MFU?
\[MFU = \frac{t_{\text{theoretical}}}{t_{\text{actual}}}\]
  1. \(t_{\text{actual}}\) (实际时间):用秒表卡一下,跑一个完整的训练迭代(加载数据、前向、反向、优化器更新)需要多少秒。
  2. \(t_{\text{theoretical}}\) (理论时间)
  • \(\text{FLOP}_{\text{theoretical}}\) (理论计算量):手动算一下,你的模型跑一次前向+反向,总共包含多少次矩阵乘法浮点运算。(一个粗略估算是:反向 \(\approx 2 \times\) 前向)。
  • \(\text{FLOP/sec}_{\text{theoretical}}\) (理论算力):查H100的规格表,即 989 TFLOP/sec。
  • \(t_{\text{theoretical}} = \frac{\text{FLOP}_{\text{theoretical}}}{\text{FLOP/sec}_{\text{theoretical}}}\)

MFU 总是 < HFU (80%),为什么?

因为 \(t_{\text{actual}}\) (分母) 被很多“浪费”的时间拉长了:

  1. 通信:DP的All-Reduce,FSDP的Broadcast/Reduce-Scatter。
  2. 重计算:激活重计算(Checkpointing)多算了一遍前向。
  3. 小计算:运行ReLU, LayerNorm等“没用”的计算(它们用的是慢的FP32核心)。
  4. 流水线“空泡” (Bubble):后面会讲的PP,GPU在等待。
  5. I/O:等待数据从硬盘加载。

目标:通过调整各种并行策略(DP, FSDP, PP, TP...)的参数,最大化MFU

业界标杆:MFU > 30%算及格,> 40%算优秀。Llama3团队在16K的GPU上跑到了41% ,非常牛。


另外三种并行策略

DP/FSDP 是在 Batch 维度拆。我们还有别的维度可以拆。


上下文并行 (CP)

概念:按 Sequence (序列) 维度拆分。

动机:当序列特别长时(比如 Llama3 的 131,072),一个序列的激活值(形状 [Seq, Dim])可能就爆显存了。

如何拆分

  • 简单部分 (MLP, Norm, Residual):这些操作是“逐个词”独立计算的,所以按序列拆分很简单。每块GPU处理序列的一部分即可。
  • 困难部分 (Attention):Attention的核心是 \(Q \times K^T\),一个词(Q)要和所有其他的词(K)互动。你把序列拆了,它怎么和“别的GPU上”的词互动呢?

解决方案

  • Option 1 (Ring Attention):非常复杂。GPU 1 算完自己的Q/K,然后把K传给GPU 2;GPU 2 把自己的K传给GPU 3... 像击鼓传花一样,让K在GPU之间“转一圈”,这样每个Q就都见到所有的K了。
  • Option 2 (Ulysses):简单点。利用多头注意力 (Multi-Head Attention) 的特性。你有 \(H\) 个头,你就用 \(H\) 块GPU。每块GPU都拿到完整的序列,但是只计算 \(1/H\) 个头。这在计算Attention矩阵时是并行的,但在QKV投影时是DP(需要同步梯度)。

流水线并行 (PP)

概念:按 L (层) 维度拆分。

动机:模型特别深 (比如126层) 时使用。

如何拆分

  • GPU 1 负责 1-32 层。
  • GPU 2 负责 33-64 层。
  • GPU 3 负责 65-96 层。
  • GPU 4 负责 97-126 层。

问题流水线空泡 (Bubble)

  • \(T=1\) 时:GPU 1 计算,GPU 2, 3, 4 空闲
  • \(T=2\) 时:GPU 1 传给 GPU 2,GPU 2 计算,GPU 1, 3, 4 空闲
  • \(T=3\) 时:GPU 2 传给 GPU 3,GPU 3 计算,...
  • 一块GPU在 \(N\) 步中只工作了 1 步, MFU (效率) 只有 \(1/N\)

解决方案微批次 (Microbatches)

  • 把一个大Batch切成 \(K\) 个微批次 (m-batch)。
  • \(T=1\):GPU 1 算 m-batch 1。
  • \(T=2\):GPU 1 算 m-batch 2,同时 GPU 2 算 m-batch 1。
  • \(T=3\):GPU 1 算 m-batch 3,同时 GPU 2 算 m-batch 2,同时 GPU 3 算 m-batch 1。

通过把数据“切碎”了往前送,让GPU“接力”跑起来,大部分时间所有GPU都有活干,就把空泡时间(粉色格子)降到了最低,大大提高了MFU。


张量并行 (TP)

概念:按 Dim (隐藏层维度) 拆分。这是在“一个层内部”进行拆分

动机:模型特别宽 (比如 \(D=16384\)) 时,一个权重矩阵 \(W\) (形状 [D, D] ) 太大,一块GPU的显存或计算能力都顶不住。

如何拆分 (以2层MLP为例)

  • 我们有2层:\(Y = XW\) (Layer 1), \(Z = YU\) (Layer 2)。
  • 我们有4块GPU。
  1. Layer 1 (按列拆 \(W\))
  • \(W\)切成4片:\(W = [W_1 | W_2 | W_3 | W_4]\)
  • \(X\) 广播给所有4块GPU。
  • GPU 1 计算 \(Y_1 = XW_1\)
  • GPU 2 计算 \(Y_2 = XW_2\)
  • ...
  • 结果 \(Y\) 被自动分片成了 \(Y = [Y_1 | Y_2 | Y_3 | Y_4]\)
  1. Layer 2 (按行拆 \(U\))
  • 关键技巧:把 \(U\)切成4片:\(U = [U_1; U_2; U_3; U_4]\)
  • \(Z = YU = [Y_1 | Y_2 | Y_3 | Y_4] \times [U_1; U_2; U_3; U_4]\)
  • 根据矩阵乘法, \(Z = Y_1 U_1 + Y_2 U_2 + Y_3 U_3 + Y_4 U_4\)
  • GPU 1 只用它本地的 \(Y_1\)\(U_1\) 算出 \(Z_1 = Y_1 U_1\)
  • GPU 2 只用它本地的 \(Y_2\)\(U_2\) 算出 \(Z_2 = Y_2 U_2\)
  • ...
  1. 同步
  • Layer 1 计算完 \(Y_i\) 后,不需要通信\(Y_i\) 直接作为 Layer 2 的输入 \(Z_i = Y_i U_i\)
  • Layer 2 计算完 \(Z_i\) 后,需要一次 All-Reduce\(Z_1, Z_2, Z_3, Z_4\) 加起来得到最终的 \(Z\)

  • 通过“列拆-行拆”的精妙组合,TP把两次矩阵运算之间的通信( \(Y_i\) )给消除了。

  • 适用场景:TP在层内部通信非常频繁(每次前向/后向都要 All-Reduce),所以它必须用在最快的连接上(即服务器内部的 900 GB/sec NVLink)。

终极方案 - N维并行

Q: 用哪一个? A: 全都要!

实际训练超级巨兽(如Llama3-405B)时,我们会把所有策略组合起来,把上万块GPU放进一个4D网格中。

Llama3 (131k序列) 的配置:

  • 8-way TP (张量并行):每个服务器内8块GPU,用最快的NVLink跑TP,拆分 Dim 维度。
  • 16-way PP (流水线并行):16个这样的服务器“串联”起来,跑PP,拆分 126 层。
  • 16-way CP (上下文并行):16个这样的“PP链条”并联,跑CP,拆分 131k 的序列。
  • 8-way DP (数据并行):最后,我们有 8 个这样的“CP组” ( \(8 \times 16 \times 16 = 2048\) GPUs),让这8组跑DP (FSDP/HSDP),拆分 Batch 维度。
  • 总计\(8 \times 16 \times 16 \times 8 = 16,384\) 块 GPU。

第12章:自监督学习 (SSL)

为什么需要自监督学习?


计算机视觉的诸多任务

  1. 分类 (Classification): 给整张图一个标签(如 "CAT")。
  2. 语义分割 (Semantic Segmentation): 给图像中的每个像素一个类别标签(如 "GRASS", "CAT", "TREE")。它不区分同类的不同实例(比如两只狗会被涂成一个颜色)。
  3. 目标检测 (Object Detection): 用边界框 (Bounding Box) 框出图中的每个物体,并给出类别(如 "DOG", "DOG", "CAT")。
  4. 实例分割 (Instance Segmentation): 语义分割的进阶版。它不仅要给每个像素分类,还要区分同类的不同实例(比如图中的三只动物会被涂成三种不同颜色)。

分类任务的标签成本相对较低(一张图一个词)。但检测和分割任务的标签成本极其高昂!你需要雇人去精确地画框、甚至在像素级别上勾勒出每个物体的轮廓。

传统的监督学习(Supervised Learning)依赖的正是这些昂贵的人工标签。

一个在ImageNet(一个大型有标签数据集)上训练好的神经网络(如AlexNet),它的倒数第二层(如一个4096维的向量)可以被视为对输入图像的“特征表示” (Feature Representation)

在原始的“像素空间”中,一张测试图的“最近邻”图像看起来和它毫不相干。

在学到的“特征空间”中,一张测试图的“最近邻”图像在语义上都和它非常相似(比如大象的近邻都是大象,南瓜灯的近邻都是南瓜灯)。

这证明了神经网络通过有标签的监督学习,成功地学会了一种“有意义的”特征表示。它把原始的、高维的、混乱的像素数据,映射到了一个低维的、有组织的、语义相关的特征空间。

问题1: "We need a lot of labeled data" (我们需要海量的有标签数据)。

问题2: "Is there a way we can train neural networks without the need for huge manually labeled datasets?" (有没有办法能在不需要海量人工标签的情况下训练神经网络?)

答案: 有!这就是自监督学习 (SSL)。


自监督学习的核心理念

SSL的两阶段范式:


阶段一:预训练 (Pre-training)

  • 数据: 我们使用海量的无标签数据集 (dataset (no labels))。
  • 任务: 我们设计一个“代理任务” (Pretext Task)。这是一个我们编造出来的、可以自动生成标签的任务。
  • 目标: 训练一个编码器 (Encoder)(比如一个ResNet)去解决这个代理任务。
  • 产出: 一个训练好的编码器 (Trained Encoder)。

阶段二:下游任务 (Downstream Task)

  • 数据: 我们使用我们真正关心的、但标签很少的有标签数据集 (dataset (with labels))(比如医学影像、卫星图等)。
  • 方法: 我们把预训练好的Encoder拿过来,"冻结"住它的大部分参数(即不更新它),只在它的末尾接上一个小型的分类器(比如一个全连接层)。
  • 目标: 用我们少量的标签来训练这个小分类器,去完成我们真正想做的任务(如分类)。

核心假设: 为了解决代理任务,编码器必须被迫学会关于这个世界的“常识”或“视觉表征”。

举个例子: 假设代理任务是“给黑白照片上色”。为了能正确地把天空涂成蓝色、草地涂成绿色,模型必须首先认出“天空”和“草地”。

因此,当编码器(Encoder)学会了解决这个“上色”的代理任务时,它顺便就学会了识别物体、理解场景。这个“识别物体”的能力,就是我们想要的“有意义的特征表示”

这个表示是可迁移的 (Transferable)。在下游任务中,即使我们只有100张"猫"和"狗"的标签,这个强大的编码器也能立刻抽取出高质量的特征,我们的小分类器就能轻松学会区分它们。


代理任务 (Pretext Task) 详解

这是对“阶段一”的放大。

无标签数据 -> Encoder -> Learned Representation (学到的特征) -> Decoder/Classifier/Regressor (用于解决代理任务的“头”) -> Labels/outputs automatically generated from data (自动生成的标签)。


下游任务 (Downstream Task) 详解

这是对“阶段二”的放大。

我们把阶段一的 Decoder(代理任务的“头”)扔掉。

我们保留 Encoder 和它产出的 Learned Representation。

我们换上一个新的、小型的 FC(全连接层,下游任务的“头”)。

我们用人工标注的 Labels 来训练这个 FC 层。


代理任务 (Pretext Tasks) 的实例

  • 图像补全 (image completion / inpainting): 把图片挖个洞,让模型去填补。自动标签: 原始的洞里是啥。
  • 旋转预测 (rotation prediction): 把图片随机旋转0, 90, 180, 270度,让模型预测是哪个角度。自动标签: 0, 1, 2, 3。
  • "拼图" (jigsaw puzzle): 把图片切成9块并打乱,让模型排回原序。自动标签: 正确的顺序。
  • 上色 (colorization): 给模型一张黑白图,让模型预测颜色。自动标签: 原始的彩色图。

每一个任务都迫使模型去理解图像的语义内容和空间结构,而不是死记硬背像素。


如何评估SSL方法的好坏?

我们怎么知道哪个代理任务(比如 "拼图" vs "旋转")更好?

  1. 代理任务性能 (Pretext Task Performance): 它解决拼图/旋转任务的准确率?(这个其实不重要,我们不关心它拼图拼得多好)。
  2. 表示质量 (Representation Quality):
  • 线性评估 (Linear Evaluation Protocol): 这是最常用的标准。
  • 聚类 (Clustering): 学到的特征是否能自动把猫和狗聚成两类?
  • t-SNE可视化: 能否在2D上清晰地分开?
  • 鲁棒性和泛化性 (Robustness and Generalization): 在一个数据集上预训练,在另一个数据集上是否依然有效?
  • 计算效率 (Computational Efficiency): 预训练要花多少GPU?
  • 迁移学习和下游任务性能 (Transfer Learning): 这才是最终目的。

评估SSL的“黄金标准”——线性评估

步骤1 : 预训练 (Pre-train)。用海量无标签数据 + 代理任务(如旋转预测)训练一个编码器(ConvNet)。

步骤2 : 评估 (Evaluate)。

  • 把预训练好的编码器(conv部分)冻结 (freeze)!它的参数固定不变。
  • 在它后面接一个全新的、单个的 linear classifier(线性分类器,即一层全连接层)。
  • 使用有标签的下游任务数据(如ImageNet分类),只训练这个线性分类器。

在测试集上看准确率。

线性分类器非常简单(它只能在特征空间里画一条直线/一个平面来分类)。

如果只用一个线性分类器就能在ImageNet上达到很高的准确率(比如60%),这说明编码器产出的特征本身质量非常高,它们已经是“线性可分”的了。

这证明了编码器(conv)真的学到了有意义的语义表示。


基于图像变换的代理任务

SSL不仅用于CV,它还是 NLP (自然语言处理) 和语音领域的主宰。

  • NLP: GPT-4 就是一个SSL模型。它的代理任务是 “预测下一个词 (next token prediction)”。给它一句话 "The cat sat on the...",让它预测 "mat"。自动标签: 语料库中真实的下一个词。
  • 语音: WaveNet 的代理任务是“预测下一个音频采样点”。

这说明SSL的核心思想(从数据自身结构中创造监督信号)是一个通用的强大理念。


代理任务1:旋转预测 (Rotation Prediction)

假设: "一个模型只有在它对物体本来的样子有了'视觉常识'之后,才能识别出物体的正确旋转。"

换句话说,为了能识别出一只猫是“倒立”的(180度),模型必须先学会猫“正立”时是什么样(头在上面,脚在下面)。这种对“正立”的理解,就是一种高级的语义表征。

这是一个标准的4分类问题。

  • 输入 (X): 原始图像 Image X。
  • 数据增广: 通过 g(X, y) 函数,我们(自动地)生成4个版本的图像和标签:
  1. y=0: 旋转0度
  2. y=1: 旋转90度
  3. y=2: 旋转180度
  4. y=3: 旋转270度
  • 模型: ConvNet model F(.)
  • 目标: 对于一张旋转了 \(y\) 度的图像 \(X^y\),模型 \(F(X^y)\) 的输出(一个4维向量)中,对应 \(y\) 的那一维概率应该最高。

损失函数: 这就是一个标准的多分类交叉熵损失 (Cross-Entropy Loss)。

\[L = - \sum_{i=0}^{3} \mathbb{I}(y=i) \log(F^i(X^i))\]

其中 \(F^i(X^i)\) 是模型对类别 \(i\) 的预测概率,\(\mathbb{I}(y=i)\) 是指示函数(如果真实标签是 \(i\) 则为1,否则为0)。


评估 (半监督)
  1. 从零开始(随机初始化权重),只用少量标签数据训练。在只有20个训练样本时,准确率不到40%。
  2. 先用所有无标签数据(整个CIFAR10训练集)做“旋转预测”预训练,然后再用少量的标签数据来微调(fine-tune)模型。准确率就高达65%!这证明了预训练学到的“视觉常识”极大地提升了模型的“数据效率” (sample efficiency)

评估 (迁移学习)

比较了在PASCAL VOC 2007数据集上,不同预训练方法的下游任务(分类、检测、分割)性能。

  • ImageNet labels (第一行): 监督学习的“天花板”。用完整的ImageNet标签预训练。
  • Random (第二行): “地板”。没有预训练,从零开始。
  • (Ours) RotNet (最后一行): SSL方法。

RotNet (分类72.97%) 远远超过 Random (53.3%),并且在不断缩小与 ImageNet labels (79.9%) 的差距。这证明了SSL非常有效。


可视化 (注意力图)

(a) 监督模型倾向于只关注最有区分度的局部(比如狗脸)。(b) 自监督(旋转)模型倾向于关注整个物体(头、身体、腿)。

因为只看狗脸,你没法判断它是否被旋转了90度。你必须结合它的身体和腿的相对位置才能判断。因此,这个代理任务迫使模型去理解物体的整体结构,从而学到了更全面的特征。


代理任务2:拼图 (Jigsaw Puzzles)

将图片切成3x3=9个图块 (patch)。

随机打乱 (shuffle) 这9个图块。

核心难点: 9个图块有 \(9! = 362,880\) 种排列组合。让模型去做一个36万类的分类太难了。

解决方案: 作者们预先定义了一个“排列集合” (Permutation Set),比如只选100种(或64种)有代表性的、比较难的打乱方式。

代理任务: 变成一个100类(或64类)的分类问题:“这张打乱的图,对应的是我定义的100种排列中的哪一种?”

模型架构: 9个图块分别通过一个共享权重 (Shared weights) 的CNN(提取每个图块的特征),然后把9个特征拼接(concatenate)起来,送入FC层,最后做一个Softmax分类。

为了能把拼图拼对(或者识别出是哪种打乱方式),模型必须学会物体部件之间的空间关系。比如它必须知道“眼睛”总是在“鼻子”的上方,“车轮”总是在“车门”的下方。


拼图 (评估)

拼图方法 (Ours) 在分类任务上(67.6%)也取得了很好的效果,远超当时的其他方法。


代理任务3:图像补全/修复 (Inpainting)

这是一种基于自编码器 (Autoencoder) 的方法。

任务: 把图片中间挖一个大洞(比如一个白方块)。

模型:

  • Encoder(编码器):将被挖洞的图片压缩成一个低维的特征向量 (Encoder Features)。
  • Decoder(解码器):尝试从这个特征向量中重建 (reconstruct) 出原始的、完整的图片。

编码器(Encoder)为了能给解码器提供足够的信息来“脑补”出被挖掉的部分(比如一个人的脸),它必须在特征向量中高度浓缩对整个场景的理解(“这似乎是一个在开会的人”)。这个浓缩的特征就是我们想要的。


图像补全 (L2损失的问题)

"reconstruction" 列显示了只用 L2 损失(见下)的结果。

重建的图像非常模糊。这是因为L2损失倾向于产生“平均”的、“安全”的预测。比如模型不确定洞里是草还是沙,它会取一个平均的黄绿色,这样L2损失最小,但看起来很假。


图像补全 (L2 + GAN 损失)

为了让重建的图像更逼真,作者们引入了对抗性损失 (Adversarial Loss),即GAN(生成对抗网络)的G。

损失函数: \(L(x) = L_{recon}(x) + L_{adv}(x)\)

  1. 重建损失 (L2 Loss):
\[L_{recon}(x) = || M \odot (x - F_{\theta}((1-M) \odot x)) ||_{2}^{2}\]
  • x 是原图,M 是掩码(mask,洞的地方是1,其他是0)。
  • (1-M) \odot x 是挖了洞的输入图。
  • \(F_{\theta}(...)\) 是自编码器的重建输出。
  • \(x - F_{\theta}(...)\) 是重建误差。
  • \(M \odot (...)\) 表示我们只关心在洞(mask)内部的重建误差。
  1. 对抗性损失 (Adversarial Loss):
\[L_{adv} = \max_{D} \mathbb{E}[\log(D(x))] + \log(1 - D(F(...)))]\]

我们引入一个判别器 D。D 的任务是区分“真实的图像块”和自编码器“生成的图像块”。

自编码器 \(F\) 的任务是“欺骗” D,让 D 以为它生成的图像块是真实的。

这种对抗博弈迫使 \(F\) 生成的图像块在纹理、细节和真实感上与原图一致,而不仅仅是L2损失下的“模糊平均值”。

"reconstruction" (L2) 很模糊。"adversarial" (GAN) 纹理更清晰但颜色可能不对。"recon + adv" (L2+GAN) 效果最好,既真实又符合上下文。


图像补全 (评估)

Ours (Inpainting) 的性能 (56.5%) 优于 Random (53.3%) 和 Autoencoder (53.8%)(普通的自编码器,任务是重建整张图,而不是填洞)。这证明了“填洞”这个代理任务比“重建全图”更能学到有用的语义特征。


代理任务4:上色 (Colorization)

我们使用 Lab 色彩空间,它把亮度(L)和颜色(a, b)分开了。

  • 输入 (X): L 通道(即黑白图, \(H \times W \times 1\))。
  • 自动标签 (Y): ab 通道(即颜色信息, \(H \times W \times 2\))。
  • 任务: 训练一个CNN \(F\),输入 \(X\),预测 \(\hat{Y} = F(X)\)
  • 损失函数: 预测的 \(\hat{Y}\) 和真实的 \(Y\) 之间的L2损失。
  • 输出: 把输入的 \(X\) (L通道) 和预测的 \(\hat{Y}\) (ab通道) 拼起来,就得到了彩色图。

为了给图34的鱼上色,模型必须认出这是一条鱼,并知道它生活在水里,然后从记忆中(训练数据)知道“这种鱼”通常是黄黑相间的,而“水”通常是蓝绿色的。这个过程迫使模型去识别物体。


"裂脑"自编码器 (Split-Brain Autoencoder)

这是“上色”任务的一种更通用的框架。

思想: 我们把数据的不同通道(或模态)分开,让它们互相预测。

  1. 上色:

X 分裂成 \(X_1\) (L通道) 和 \(X_2\) (ab通道)。

  • 网络 \(\mathcal{F}_1\):看 \(X_1\) (黑白),预测 \(\hat{X_2}\) (颜色)。
  • 网络 \(\mathcal{F}_2\):看 \(X_2\) (颜色),预测 \(\hat{X_1}\) (黑白)。
  1. 多模态:

X 分裂成 \(X_1\) (RGB图像) 和 \(X_2\) (深度图像)。

  • 网络 \(\mathcal{F}_1\):看RGB,预测深度。
  • 网络 \(\mathcal{F}_2\):看深度,预测RGB。

两个网络(“大脑的两个半球”)必须学会一个共同的、底层的特征表示,才能有能力去互相预测对方的信息。


上色 (评估)

Split-Brain Autoencoder 它的性能非常好,在当时超过了许多其他SSL方法,并且在网络的中间层(conv3, conv4)上非常接近监督学习(ImageNet-labels)。


代理任务5:视频上色 (Video Coloring)

将“上色”任务从静态图片扩展到视频。

核心思想: 时间的连续性 (Temporal Coherence)。一个物体(比如一辆车)在连续的视频帧中,它的颜色应该是一致的。

  1. 给模型一个彩色的“参考帧” (reference frame) (在 \(t=0\) 时刻)。
  2. 给模型后续的黑白帧 (在 \(t=1, 2, 3...\) 时刻)。
  3. 让模型给这些黑白帧上色。

为了能把 \(t=0\) 时刻车上的“红色”正确地“复制”到 \(t=3\) 时刻车的位置上,模型必须学会“跟踪” (tracking) 这辆车。


视频上色 (如何工作)

这是一个基于注意力 (Attention) 的“指针网络”。

步骤:

  1. 编码: 将参考帧(黑白)和目标帧(黑白)都送入一个共享权重的 CNN,得到两个特征图(Embeddings)。我们称参考帧的特征为 \(f_i\)(在位置 \(i\)),目标帧的特征为 \(f_j\)(在位置 \(j\))。
  2. 匹配: 计算一个“注意力图” (Attention Map) \(A_{ij}\)
\[A_{ij} = \frac{\exp(f_i^T f_j)}{\sum_k \exp(f_k^T f_j)}\]
  • \(f_i^T f_j\) 是特征 \(f_i\)\(f_j\) 的点积相似度。如果目标帧 \(j\) 位置的物体(比如车轮)和参考帧 \(i\) 位置的物体(也是车轮)在语义上很像,这个值就很高。
  • \(A_{ij}\) 是一个 Softmax,它表示“目标帧 \(j\) 位置的像素,有多大的概率是对应于参考帧 \(i\) 位置的像素”。
  1. 上色: 目标帧 \(j\) 位置的预测颜色 \(y_j\),是参考帧所有位置的真实颜色 \(c_i\) 的加权平均值。
\[y_j = \sum_i A_{ij} c_i\]

如果 \(A_{ij}\) 很高(\(j\) 对应 \(i\)),那么 \(c_i\)\(i\) 处的颜色)就会主导 \(y_j\) 的颜色。这就像一个“软指针”,把 \(i\) 的颜色“复制”到了 \(j\)

  1. 损失: 最小化预测颜色 \(y_j\) 和目标帧真实颜色 \(c_j\) 之间的L2损失。
\[\min_{\theta} \sum_j \mathcal{L}(y_j, c_j)\]

我们只训练了模型去上色,但由于模型内部学会了 \(A_{ij}\) 这个注意力图(它代表了帧与帧之间的像素对应关系),我们可以白嫖这个 \(A_{ij}\) 来做物体跟踪!

只要在第一帧给出一个分割掩码,我们就可以用 \(A_{ij}\) 把它自动传播到后续所有帧。

这就是SSL的魅力:为了解决A任务(上色),模型“涌现”出了B任务(跟踪)的能力。


代理任务6:掩码自编码器 (Masked Auto Encoders, MAE)

它借鉴了NLP中BERT模型的“掩码语言模型”思想,并将其应用在ViT (Vision Transformer)上。

思想: 和 Inpainting 类似,也是“挖洞填洞”。但有两大不同:

  • 极高的掩码率: MAE 随机“挖掉” (mask) 75% 的图像块 (patch)!
  • 重建目标: 它只重建原始的像素值,不需要GAN。

结果: 即使只给模型看25%的零散图块(左列),它也能(模糊地)重建出原始图像(中间列)。


MAE 的非对称架构 (Asymmetric Design)

这是MAE最核心的创新!

传统自编码器: 编码器和解码器是对称的,且都要处理100%的输入。

MAE :

  • 掩码: 将图像 \(16 \times 16 = 256\) 个图块,随机扔掉75% (192个),只保留25% (64个)。
  • 编码器 (Encoder) :只处理可见的 25% 的图块(即64个)。

编码器是一个巨大的ViT(比如ViT-Large)。

  • 效率: 因为只处理1/4的数据,所以训练速度极快。
  • 解码器 (Decoder) :

解码器的输入 = 编码器输出的 25% 的特征 + 75% 的“掩码符” (mask token)(一个可学习的共享向量)。

解码器是一个非常小、非常轻量的Transformer(比如只有8层)。

  • 任务: 让这个小解码器,利用25%的上下文信息,去“填空”,重建出那75%被掩码的图块。
  1. 非对称设计: 繁重的工作(理解语义)由大编码器完成,简单的任务(插值像素)由小解码器完成。
  2. 预训练后: 我们扔掉小解码器,只保留大编码器。
  3. 高掩码率 (75%): 这是一个极难的代理任务。它迫使编码器从极度零散的信息中提炼出高度抽象的语义表征。如果掩码率太低(比如10%),模型“作弊”——直接从旁边的像素插值就行了,学不到高级语义。

MAE 的重建损失

损失函数是 MSE (Mean Squared Error),即L2损失。

损失只在被掩码的图块 (masked patches) 上计算。模型在可见图块上重建得再好也不得分。


评估:线性探测 vs 全微调

评估SSL模型时两种不同的下游任务策略。

  1. 线性探测 (Linear Probing): 冻结 (freeze) 编码器,只训练一个线性层。
  2. 全微调 (Full Fine-tuning): 不冻结编码器,用很小的学习率一起训练编码器和分类头,即“微调”整个模型。

线性探测适合测试那些特征本身已经线性可分的模型。

全微调适合测试那些特征需要一点“适应”的模型。

MAE的作者发现,MAE的特征用线性探测效果不好,但用全微调效果极佳。这说明MAE学到的特征是一种更“底层”但潜力巨大的表示,需要微调来激活。


MAE 实验 (Ablation Studies)
  • 上图 (Masking ratio): 无论是微调还是线性探测,性能都在75%的掩码率达到峰值!这证明了“高掩码率”这个假设是对的。
  • 下表 (Mask sampling): random(随机掩码)远好于 block(挖掉一大块)或 grid(隔一个挖一个)。

MAE 性能对比

MAE (ViT-H) 在ImageNet上达到了 87.8% 的准确率。这是一个SOTA(State-of-the-art)结果。它击败了当时所有的SSL方法(如DINO, MoCo v3),并且首次在一个标准的、无额外数据的SSL设置下,其性能超越了从零开始的监督学习模型。这标志着SSL范式(在ViT架构上)的成熟。


优点: 这些代理任务(旋转、拼图、上色、MAE)能迫使模型学习“视觉常识”。

缺点:

  • 设计这些任务本身很繁琐("tedious"),像“炼金术”。
  • 学到的特征可能过度拟合 (tied to) 特定的代理任务。比如,一个只为“旋转”任务优化的特征,可能对“纹理识别”任务没啥帮助。

引出问题: 我们能找到一个更通用的代理任务吗?


对比表示学习 (Contrastive Representation Learning)

这是本节课的第二大核心,也是目前SSL的主流范式。

"更通用"的代理任务

  • 拉近 (Attract): 同一张图的不同“视图” (views)(比如,原图、裁剪、旋转、上色后的图)应该被编码器映射到特征空间中的相近位置。
  • 推远 (Repel): 不同图片的视图,应该被映射到相远的位置。

这就是对比学习 (Contrastive Learning) 的核心。

我们不再关心“旋转了多少度”或“缺失的像素是什么”。我们只关心一个更本质的问题:“这两个视图是不是来自同一张图?”这是一个通用的目标,它不依赖于任何特定的“手工设计”的代理任务。


对比学习的术语

  • x: 锚点 (anchor) (或 "reference")。
  • \(x^+\): 正样本 (positive)。来自同一张图的不同数据增广(augmentation)版本。
  • \(x^-\): 负样本 (negative)。来自其他图的样本。

对比学习的目标

我们希望学到一个编码器 \(f(\cdot)\),使得:

\[score(f(x), f(x^+)) \gg score(f(x), f(x^-))\]

即:“锚点”与“正样本”的特征相似度,应远大于“锚点”与“负样本”的特征相似度。


InfoNCE 损失函数 (核心中的核心)

这是实现对比学习目标的数学形式。

设置: 给定一个锚点 \(x\),我们有 1 个正样本 \(x^+\) 和 N-1 个负样本 \(x_j^-\)

InfoNCE 损失:

\[L = - \mathbb{E}_x \left[ \log \frac{\exp(s(f(x), f(x^+)))}{\exp(s(f(x), f(x^+))) + \sum_{j=1}^{N-1} \exp(s(f(x), f(x_j^-)))} \right]\]

让我们把 \(s(u,v)\) 看作是相似度得分(比如向量点积)。

  • 分子 \(\exp(s(f(x), f(x^+)))\) 是“锚点与正样本”的得分(exp确保其为正)。
  • 分母 \(\exp(s(f(x), f(x^+))) + \sum \exp(s(f(x), f(x_j^-)))\) 是“锚点与所有N个样本(1正N-1负)”的得分之和。

这个分数 \(\frac{\exp(s_{pos})}{\sum \exp(s_{all})}\) 正是 Softmax 函数的形式!

所以,InfoNCE 损失等价于一个 N分类问题的交叉熵损失 (Cross-Entropy Loss)!

这个代理任务可以被重新描述为:“这里有N个样本(1个正,N-1个负),请你分类出哪一个是正样本。”

为了在这个任务上拿高分(即最小化损失 \(L\)),模型必须学会一种特征表示 \(f(\cdot)\),它能把 \(x\)\(x^+\) 映射到一起,并把 \(x\) 和所有的 \(x_j^-\) 推开。


InfoNCE 的理论解释

InfoNCE 损失是 \(f(x)\)\(f(x^+)\) 之间互信息 (Mutual Information, MI) 的一个下界。

\[MI[f(x), f(x^+)] - \log(N) \ge -L\]

最小化 \(L\)(即最大化 \(-L\)),就等于在最大化互信息。

“最大化互信息”是什么意思?它意味着 \(f(x)\)\(f(x^+)\) 两个特征向量共享的信息尽可能多。

\(x\)\(x^+\) 是同一张图的两个不同增广(比如一个裁剪并变色,一个旋转并模糊)。它们共享的“信息”就是图像的“核心语义内容”(比如“这是一只猫”),而它们不共享的信息就是“增广的噪声”(比如“被裁剪了”、“变色了”)。

因此,最小化InfoNCE损失,就是在迫使编码器 \(f(\cdot)\) 提取出对数据增广“不变”的(invariant)核心语义特征。

关键结论: \(N\)(负样本数量)越大,这个下界越紧。因此,对比学习需要大量的负样本才能学好。


对比学习方法1:SimCLR

核心组件:

  • 数据增广 \(t \sim \mathcal{T}\): 使用非常强的增广(随机裁剪、颜色抖动、高斯模糊等)。一张图 \(x\) 通过两种不同的增广 \(t\)\(t'\) 得到 \(\tilde{x}_i\)\(\tilde{x}_j\),它俩互为正样本对。
  • 编码器 \(f(\cdot)\): 一个标准的ResNet,得到特征 \(h_i = f(\tilde{x}_i)\)
  • 投影头 \(g(\cdot)\): SimCLR的关键创新之一。这是一个小型的MLP(多层感知机),它将 \(h_i\) 进一步映射到 \(z_i = g(h_i)\)。对比损失 (InfoNCE) 是在 \(z\) 空间计算的。

损失函数: InfoNCE 损失。

预训练后: 我们扔掉投影头 \(g(\cdot)\),保留编码器 \(f(\cdot)\) 及其输出 \(h\) 作为下游任务的特征。

SimCLR 算法流程

  1. 输入一个大小为 \(N\) 的小批量 (minibatch) \(\{x_k\}\)
  2. 对每张 \(x_k\) 都做两次随机增广,得到 \(\tilde{x}_{2k-1}\)\(\tilde{x}_{2k}\)。现在我们有了一个大小为 \(2N\) 的“增广批次”。
  3. 把这 \(2N\) 张图全部送入 \(f(\cdot)\)\(g(\cdot)\),得到 \(2N\) 个特征向量 \(\{z_i\}\)

如何定义正负样本?

  • 对于 \(z_i\)(来自 \(x_k\) 的第1次增广),它的正样本是 \(z_j\)(来自 \(x_k\) 的第2次增广)。
  • 它的负样本是批次中所有其他 \(2N-2\) 个向量。

SimCLR的核心:它巧妙地利用了同一个批次中的其他样本作为负样本。

损失函数:

\[l(i,j) = -\log \frac{\exp(s_{i,j} / \tau)}{\sum_{k=1}^{2N} \mathbb{I}_{[k \ne i]} \exp(s_{i,k} / \tau)}\]
  • \(\tau\) (tau) 是温度系数,一个超参数。\(\tau\) 越小,softmax越尖锐,模型会更关注难分的负样本。
  • 总损失: 对批次中所有 \(2N\) 个样本都计算一次损失(把它当锚点),然后取平均。

SimCLR (矩阵视角)计算一个 \(2N \times 2N\) 的相似度("Affinity")矩阵 \(S\)

  • 蓝色方块代表正样本对(比如 (1,2), (2,1), (3,4), (4,3)...)。
  • 对矩阵的每一行 \(i\),我们都希望 \(S_{i,j}\) (正样本) 的值最大,而 \(S_{i,k}\) (所有 \(k \ne j\)) 的值都最小。这等价于 \(2N\)\(2N\)-way 的Softmax分类问题。

SimCLR (评估)

SimCLR(星星)在ImageNet线性评估上远超之前的SSL方法(如Rotation 55%)。

证明了对比学习(通用任务)+ 强增广 + 投影头 + 大量负样本,这个范式是成功的。


SimCLR (半监督评估)

当只用1%或10%的ImageNet标签进行微调时,SimCLR预训练过的模型性能远超从零开始的监督模型。


SimCLR 设计选择 (投影头 Projection Head)

为什么要有 \(g(\cdot)\)

  • 图表: Non-linear (非线性投影头,即SimCLR) 远好于 Linear (线性投影头) 和 None (没有投影头)。

这是SimCLR的第二个关键洞察。

对比损失 \(L\) (在 \(z\) 空间) 的目标是“信息丢弃”:它迫使 \(z\) 对所有增广(颜色、裁剪)都不变。

但是,下游任务(比如分类)可能需要这些信息(比如颜色对分类很重要)。

\(h\)\(g\) 的输入)和 \(z\)\(g\) 的输出)之间有一个 \(g\)(MLP)。

损失函数 \(L\) 作用于 \(z\),迫使 \(z\) 变得“增广不变”。

但它并没有直接迫使 \(h\) 丢弃信息。\(h\) 可以在保留所有有用信息(如颜色、纹理)的同时,让 \(g\) 学会如何“丢弃”它们以得到 \(z\)

因此:\(z\) 是用来“训练”的,\(h\) 才是我们保留下来用于“下游任务”的、真正有用的特征! \(h\) 既具有不变性,又保留了细节。


SimCLR 设计选择 (大批量)

SimCLR的性能极其依赖于大批量 (Large batch size)。

图表: 批量从256 (57.5%) 增长到 8192 (68.5%),性能暴涨。

因为负样本 \(N\) 越大,InfoNCE的效果越好。SimCLR的负样本就是来自同一个批次的其他样本。Batch size \(N\) 越大,负样本数量 \(2N-2\) 就越大。

SimCLR的致命缺点: Batch size 8192 需要天量的GPU/TPU内存,普通人根本无法训练。


对比学习方法2:MoCo (Momentum Contrast)

MoCo (He et al., 2020) 的核心思想是“解耦” (decouple) 批量大小和负样本数量。

SimCLR的负样本必须在当前批次中,所以Batch Size必须很大。MoCo的解决方案是负样本可以来自过去的批次!

如何实现:

  • 队列 (Queue): 维护一个巨大的队列(比如 \(K=65536\) 个负样本)。
  • 编码器:

encoder (Query Encoder, \(f_q\)): 编码当前批次的“锚点”(query)。这是我们正常用反向传播 (BP) 训练的网络。

momentum encoder (Key Encoder, \(f_k\)): 编码当前批次的“正样本”(key) 以及用于填充队列的“负样本”(key)。

  • 损失: InfoNCE 损失,在 \(q\)\(k\)(正样本)以及 queue(\(K\) 个负样本)之间计算。
  • 队列更新: 当前批次的 \(k\) 入队,最早的 \(k\) 出队。

MoCo的难点: 如果 queue 中的负样本是用很久以前的 encoder(比如1000个iteration前)生成的,它们的特征与 \(q\)(由当前 encoder 生成)没有可比性,会导致训练不稳定。

SimCLR如何解决: 暴力。所有 \(q\)\(k\) 都在同一次前向传播中由同一个 encoder 生成,所以绝对一致。

MoCo的巧妙解决: momentum encoder \(f_k\) 的参数 \(\theta_k\) 不通过BP更新 (no_grad)!

而是通过 \(f_q\) 的参数 \(\theta_q\) 进行动量更新 (momentum update):

\[\theta_k \leftarrow m \theta_k + (1-m) \theta_q\]

\(m\) 是一个非常大的动量值(比如 \(m=0.999\))。

这意味着 \(\theta_k\)\(\theta_q\) 的一个“平滑/慢速移动的平均值”

结果: \(\theta_k\) 更新得非常缓慢,这保证了队列中所有 \(K\) 个样本的特征(即使它们来自不同批次)都是由一个高度一致的编码器生成的,从而解决了不一致性问题。


MoCo 算法流程
  1. 拿一个批次 x,做两种增广 x_q 和 x_k。
  • q = f_q(x_q) (有梯度)。
  • k = f_k(x_k) (用动量编码器,无梯度 k.detach())。
  1. 计算 \(q\)\(k\) 的正样本相似度 l_pos。
  2. 计算 \(q\) 和 queue 中所有负样本的相似度 l_neg。
  3. 计算 InfoNCE 损失 (CrossEntropyLoss)。
  • loss.backward() (反向传播,只更新 \(f_q\))。
  • 动量更新 \(f_k\)
  1. k 入队,最老的出队。

MoCo 可以在很小的批量(如256)下,享受到巨大(如65536)的负样本池带来的好处。


"MoCo V2"

MoCo V2 只是在 MoCo V1 的基础上,“抄” 了 SimCLR 的两个成功点子:

  • 加了 MLP 投影头 (\(g(\cdot)\))。
  • 用了更强的数据增广。

MoCo V2 (评估)

加上MLP和强增广 (e) 后,性能从 (a) 60.6% 涨到了 71.1%。

MoCo v2 (batch 256) 达到了 71.1%,而 SimCLR (batch 4096) 只有 69.3%。

MoCo (batch 256) 仅需 5.0G 显存,而 SimCLR (batch 4096) 估计需要 93.0G 显存。

MoCo V2 实现了SOTA的性能和平民化的硬件需求,是一个巨大的胜利。


对比学习方法3:CPC (Contrastive Predictive Coding)
  • 实例对比 (SimCLR, MoCo): 对比“图片A”和“图片B”。
  • 序列对比 (CPC): 对比“正确的序列”和“错误的序列”。

思想: 在潜空间 (latent space) 中预测未来。

任务: 给定序列的“过去”(上下文 \(c_t\)),模型要能从一堆样本中,识别出“真正”的未来\(z_{t+k}\))。

例子: 给定 \(t=1...5\) 的图像块(上下文),从10个候选图像块中(1个真的 \(t=6\),9个假的 \(t=j\)),找出哪个才是真的 \(t=6\)

  • 编码器 \(g_{enc}\) : 将序列中的每一项 \(x_t\)(比如一个10ms的音频片段,或一个图像块)编码成一个特征 \(z_t\)
  • 自回归模型 \(g_{ar}\): (比如一个RNN/GRU) 依次读取 \(z_1, z_2, ..., z_t\),并将它们压缩成一个上下文向量 \(c_t\)(它"记住"了 \(t\) 时刻之前的所有信息)。

预测与损失:

我们想用 \(c_t\) 来预测未来的 \(z_{t+k}\)

  • 正样本: 序列中真实的 \(z_{t+k}\)
  • 负样本: 从其他序列(或当前序列的其他位置)随机抽取的 \(z_j\)

InfoNCE 损失:我们用InfoNCE来判断 \(c_t\) 能否从一堆样本中“认出” \(z_{t+k}\)

得分函数: \(s_k(z_{t+k}, c_t) = z_{t+k}^T W_k c_t\)

注意: 这里不是简单的点积。\(W_k\) 是一个可学习的线性变换矩阵。我们用 \(c_t\)\(W_k\)“预测” \(t+k\) 时刻的特征,然后看这个“预测”与“真实”的 \(z_{t+k}\) 有多像。每个未来步长 \(k\) 都有一个自己专属的 \(W_k\)

总损失: 把 \(k=1, 2, 3...\)(比如预测未来4步)的损失加起来。

CPC在音频(一种天然的序列数据)上效果极好。

在LibriSpeech数据集上,用CPC预训练的特征做说话人分类,准确率高达 97.4%,几乎与监督学习 (98.5%) 持平。

t-SNE可视化显示,不同说话人的特征被完美分开了。

为了预测“下一个音素”,模型被迫学会了剥离内容(说的是什么词)和风格(是谁在说,音色如何)。这个“风格”特征,就是说话人的ID。

如何把CPC用于图像?

  • 方法: 把图像看成一个序列。比如,从上到下,逐行扫描图像块。
  • 任务: 用上面几行的图像块(上下文 \(c_t\)),去预测下面几行的图像块(未来 \(z_{t+k}\))。
  • 评估: 效果还行,但不如后来基于实例的对比学习(SimCLR, MoCo)在图像任务上的表现。

其他SOTA方法

MoCo v3: 只是说明MoCo也适配了Vision Transformer (ViT) 架构。

  • DINO是另一个SOTA模型。

思想: 无标签的自蒸馏 (Self-Distillation with No Labels)。

架构: 类似于 MoCo,也有两个网络:

  • Student (学生网络): 类似 \(f_q\),用BP更新。
  • Teacher (教师网络): 类似 \(f_k\),用 \(Student\) 的动量平均来更新。

DINO的创新 (损失函数):

给一张图 \(x\) 做两种增广:\(x_1\) (给Student) 和 \(x_2\) (给Teacher)。

  • Teacher 输出一个概率分布 \(P_T = \text{Softmax}(f_T(x_2))\)
  • Student 输出一个概率分布 \(P_S = \text{Softmax}(f_S(x_1))\)

损失函数: 最小化 \(P_S\)\(P_T\) 之间的交叉熵。

\[L = - P_T \log(P_S)\]

Teacher (动量平均) 提供了一个更稳定、更平滑的预测目标。

Student 被迫去“追随”(蒸馏)这个更稳定的Teacher。

为了防止模型“崩溃”(即对所有输入都输出同一个分布),DINO还用了“中心化 (Centering)”和“锐化 (Sharpening)”等技巧。

结果: DINO的注意力图(Attention Map)显示,它在没有任何标签的情况下,自动学会了对物体进行精确的分割。这是SSL“涌现”能力的又一个惊人证明。


DINO v2: DINO的升级版,更大更强。


第13章 生成模型 (Generative Models)

回顾自监督学习 (Self-Supervised Learning, SSL)

这是因为自监督学习生成模型(尤其是我们后面会讲的VAE)在思想上有很多共通之处。它们都属于非监督学习的范畴,即我们都试图从没有标签的数据中学习有用的信息

SSL的核心思想

你有一堆没有标签的数据集(Dataset (no labels))。SSL的目标是学习一个好的特征表示(Learned Representation)。它通过“ pretext task”(代理任务)来强迫模型学习。

整个流程是:无标签数据 -> Encoder (编码器) -> 学到的特征 -> Decoder (解码器) -> 自动生成的标签

这里的关键是“标签是自动从数据生成的” 。我们不是在做分类(猫/狗),而是在做一个“和数据本身相关”的任务。

另一种SSL范式:对比学习 (Contrastive Learning)

这是SSL的另一大家族 ,包括SimCLR, MoCo等。

核心思想 :

  1. 从一个Input batch(输入批次)中拿一张图(比如猫)。
  2. 对这张图做两次不同的随机变换(Random transforms ),得到两个“正样本对”(\(x_1, x_2\))。它们内容相同,但看起来不一样。
  3. 批次里的其他图(比如猴子、狗)经过变换后,都算作“负样本”(\(x_3, x_4, ...\))。
  4. 目标: 拉近正样本对在特征空间的距离(similar features ),推远负样本对的距离(dissimilar features )。

MoCo (Momentum Contrast)

MoCo是为了解决SimCLR的大batch size问题而提出的 。

核心改进:

  1. 它维护一个队列 (queue) 。这个队列里存着很多旧的负样本特征(\(k_0, k_1, ...\))。这样,负样本的数量就和batch size解耦了 。
  2. 它有两个Encoder:一个encoder(编码\(q\))和一个momentum encoder(编码\(k\))。
  3. 关键: 计算损失时,梯度通过\(q\)反向传播(no_grad)。
  4. Momentum encoder的参数通过反向传播更新,而是通过动量(momentum)从\(q\)的encoder缓慢更新。
  5. 数学公式: \(\theta_k \leftarrow m\theta_k + (1-m)\theta_q\) 。其中\(m\)是一个很大的动量值(比如0.999),\(\theta_q\)是query encoder的参数,\(\theta_k\)是key encoder的参数。这保证了key encoder是缓慢、平滑演进的。

DINO (自蒸馏,无标签)

DINO是另一种SSL方法,它不用对比损失,而是用了类似“知识蒸馏”的思想。

它也有两个网络:student(学生)和teacher(教师)。

关键点:

  1. 没有梯度流向Teacher: 教师网络的输出用来计算loss ,但它本身不通过这个loss反向传播(sg代表stop-gradient)。
  2. Teacher的更新: 教师网络的参数 \(\theta_{teacher}\) 是学生网络参数 \(\theta_{student}\)指数移动平均 (EMA)
  3. 数学公式: \(\theta_{teacher} \leftarrow \tau \cdot \theta_{teacher} + (1-\tau) \cdot \theta_{student}\)

DINOv2 只是说明DINO这个方法可以被扩展到超大数据集(142M)上,并获得极强的特征 。


什么是生成模型 (Generative Models)?

监督学习 (Supervised Learning) :

  • 数据: (x, y),即数据\(x\)和标签\(y\)
  • 目标: 学习一个函数 \(f\),实现 \(x \rightarrow y\) 的映射 。
  • 例子:
  1. 分类 (Classification) (输入图片\(x\),输出标签\(y\)= "Cat" )。
  2. 图像描述 (Image captioning) (输入图片\(x\),输出句子\(y\)= "A cat sitting on a suitcase..." )。
  3. 物体检测 (Object Detection) (输入图片\(x\),输出\(y\)= [DOG, DOG, CAT] 和它们的坐标 )。
  4. 语义分割 (Semantic Segmentation) (输入图片\(x\),输出\(y\)= 每个像素的标签 )。

非监督学习 (Unsupervised Learning) :

  • 数据: x,只有数据,没有标签
  • 目标: 学习数据中隐藏的结构 。
  • 例子:
  1. 聚类 (Clustering) (比如K-means,把数据分成3簇)。
  2. 降维 (Dimensionality reduction) (比如PCA ,把3维数据压到2维 )。
  3. 密度估计 (Density estimation)
  • 关键联系: “密度估计”即我们想要建模 \(p(x)\) (数据\(x\)的概率分布),这正是生成模型的核心

判别模型 (Discriminative Model) :

  • 目标: 学习条件概率 \(p(y|x)\)
  • 含义: “给定一张图片\(x\),它对应标签\(y\)(比如‘猫’)的概率是多少?” 。
  • 例子: 输入一张猫的图片\(x\),模型输出 \(P(\text{cat}|x) = 0.9, P(\text{dog}|x) = 0.1\)
  • 特点: 对于一个输入\(x\),所有可能的标签\(y\) 之间存在竞争(它们的概率和为1)。模型只关心决策边界在哪里,不关心数据\(x\)本身长什么样。
  • 弱点: 判别模型无法处理“不合理”的输入 。你给它一张抽象画,它也必须给出一个 (cat, dog) 的概率分布。它无法说“你给我的这张图根本不是猫也不是狗”。

生成模型 (Generative Model) :

  • 目标: 学习数据的联合概率 \(p(x)\)
  • 含义: “一张图片\(x\)(比如一张猫的照片)在世界上‘合理’存在的概率是多少?”
  • 特点: 所有可能的图片(猫、狗、猴子、抽象画...)共同竞争总为1的概率质量 。
  • 要求: 这要求模型对世界有深刻的理解 。它必须知道“一张3条腿的狗”比“一只3只手臂的猴子”更可能存在 。
  • 优点: 模型可以“拒绝”不合理的输入 。如果输入一张抽象画\(x_{art}\),模型会给出极低的概率 \(p(x_{art})\) ,因为它在训练数据里没见过这种模式。

条件生成模型 (Conditional Generative Model) :

  • 目标: 学习条件概率 \(p(x|y)\)
  • 含义: “给定一个标签\(y\)(比如‘猫’),生成一张‘猫’的图片\(x\)的概率是多少?” 。
  • 特点: 对于一个标签\(y\),所有可能的图片\(x\) 之间存在竞争 。

三者关系:

  • 贝叶斯公式: \(P(y|x) = \frac{P(x|y)P(y)}{P(x)}\)
  • 判别模型 \(P(y|x)\) 可以由生成模型 \(P(x)\)、条件生成模型 \(P(x|y)\) 和先验 \(P(y)\) 共同构建出来。

用途总结:

  • 判别模型 \(p(y|x)\): 给数据分配标签
  • 生成模型 \(p(x)\): 异常检测(低\(p(x)\)就是异常点)、无标签特征学习生成新数据
  • 条件生成模型 \(p(x|y)\): 根据标签生成数据
  • 注意: 在实际中,"生成模型"这个词通常泛指 \(p(x)\)\(p(x|y)\) 这两种 。

为什么需要生成模型?

核心: 建模模糊性/不确定性 (Modeling ambiguity)

  • 当一个输入\(y\)可能对应很多个合理的输出\(x\)时,我们就需要 \(P(x|y)\)

例子:

  • 文生文: 输入\(y\)=“写一首关于生成模型的诗” ,输出\(x\)=诗 。合理的诗有无数首。
  • 文生图: 输入\(y\)=“一张老师在讲生成模型的图” ,输出\(x\)=图片 。合理的图片也有无数张。
  • 图生视频: 输入\(y\)=一张图片 ,输出\(x\)=“接下来会发生什么”的视频。

生成模型的分类 (Taxonomy)

第一次分裂 (Explicit vs. Implicit)

  • 显式密度 (Explicit density) : 模型可以直接计算出 \(P(x)\) 的值
  • 隐式密度 (Implicit density) : 模型无法计算 \(P(x)\),但它仍然能\(P(x)\) 分布中采样(即生成新样本)。

显式密度的分裂 (Tractable vs. Approximate)

  • 易处理密度 (Tractable density) : 我们可以精确地、真实地计算 \(P(x)\)

例子:自回归模型 (Autoregressive Models)

  • 近似密度 (Approximate density) : 我们无法精确计算 \(P(x)\),只能计算一个 \(P(x)\)近似值(比如一个下界)。

例子:变分自编码器 (VAE)

隐式密度的分裂 (Direct vs. Iterative)

  • 直接 (Direct) : 可以一次性直接采样。

例子:生成对抗网络 (GAN)

  • 迭代 (Iterative) : 需要通过一个迭代过程来逐步生成样本。

例子:扩散模型 (Diffusion Models)


详解自回归模型 (Autoregressive Models)

训练目标:最大似然估计 (MLE)

目标: 我们要写出一个显式的函数 \(p(x) = f(x, W)\)

如何训练? 给定一堆训练数据 \(x^{(1)}, x^{(2)}, ... x^{(N)}\) ,我们要找到一组参数 \(W^*\),使得这组数据出现的概率最大

数学公式 (MLE):

\[W^* = \arg\max_W \prod_i p(x^{(i)})\]
  • 连乘 (\(\prod\)) 在计算上很难处理(数值不稳定)。所以我们用log trick ,因为 \(\log\) 函数是单调递增的,最大化 \(p(x)\) 等于最大化 \(\log p(x)\)

数学公式 (Log-Likelihood):

\[W^* = \arg\max_W \sum_i \log p(x^{(i)})\]
  • 这就是我们的损失函数 。我们用梯度上升(或者最小化负log似然)来优化它。

自回归模型的核心:概率链式法则

  • 我们怎么定义 \(p(x)\) 呢?
  • 我们不把\(x\)(比如一张图片或一句话)看作一个整体,而是看作一个序列\(x = (x_1, x_2, ..., x_T)\) 。(比如一句话就是词的序列,一张图就是像素的序列)。
  • 根据概率的链式法则 (Chain rule) ,一个联合概率分布 \(p(x_1, ..., x_T)\) 可以被分解为一系列条件概率的乘积:
\[p(x) = p(x_1) p(x_2|x_1) p(x_3|x_1, x_2) ... p(x_T|x_1, ..., x_{T-1})\]
  • 自回归模型的定义:
\[p(x) = \prod_{t=1}^T p(x_t | x_1, ..., x_{t-1})\]
  • “自回归” (Autoregressive) 的意思就是“对自身历史的回归”。即 \(t\) 时刻的输出 \(x_t\),依赖于 \(1\)\(t-1\) 时刻的所有历史。

自回归模型的例子

  • RNN : 循环神经网络(RNN)做语言模型 ,就是最经典的自回归模型。它在 \(t\) 时刻预测 \(p(x_t|...)\),它的隐藏状态 \(h_{t-1}\) 编码了 \(x_1, ..., x_{t-1}\) 的所有历史信息。
  • Transformer : 现代的大语言模型 (LLMs)(比如GPT)也是自回归模型 。它们使用带掩码 (masked) 的自注意力机制 ,确保在预测第 \(t\) 个词 \(x_t\) 时,只能看到 \(t\) 之前(\(x_1 ... x_{t-1}\))的词 。

自回归模型用于图像 (PixelRNN / PixelCNN)

  • 怎么把图像变成序列?我们可以按照扫描线顺序 (scanline order) ,把图像看作一个超长的像素序列(比如 R, G, B, R, G, B, ...)。
  • 模型: 预测当前子像素的值(0-255中的一个分类问题),其条件是所有在它之前(左边和上边)的像素 。
  • 问题: 这种方式太昂贵了! 。一张 \(1024 \times 1024\) 的图,序列长度是 \(3 \times 1024 \times 1024 \approx 300\)万 。这在计算上几乎不可行。
  • 解决: (这里跳跃了一下)现代的做法是把图像看作“图像块 (tiles/patches)”的序列,而不是像素序列 。

详解变分自编码器 (VAE)

VAE的定位

* PixelRNN/CNN 可以显式计算 $p(x)$ ,但太慢。
* VAE 定义了一个**无法精确计算 (intractable)** 的 $p(x)$ 。
* **但是**,我们可以优化 $p(x)$ 的一个**下界 (lower bound)** 。

(非变分的)自编码器 (Autoencoder, AE)

在理解VAE之前,必须先懂AE。

AE:

  • 是一个非监督学习特征的方法 。
  • 结构:Input data (x) -> Encoder -> Features (z) -> Decoder -> Reconstructed data (x_hat)
  • 训练: 没有标签 !我们强迫模型“自我编码 (Autoencoding)” 。
  • 损失函数: 重建损失(Reconstruction Loss),通常是 L2 距离:\(||\hat{x} - x||_2^2\)

AE的用途: 训练好后,Encoder 就成了一个特征提取器,可以接一个Classifier 去做下游任务。

AE的局限:

  • AE能生成新图片吗? 理论上,我们可以只用Decoder ,然后喂给它一个新的 \(z\) ,它就能生成一张图片 \(\hat{x}\)
  • 问题: 我们不知道 \(z\) 的分布长什么样 。 \(z\) 的特征空间是混乱、无组织的。我们随便猜一个 \(z\) 喂给解码器,很可能生成一张毫无意义的图。
  • VAE的解决思路: 这就是VAE要解决的!我们能不能强迫 \(z\) 的分布服从一个我们已知的、简单的分布(比如标准正态分布 \(N(0, I)\))?

VAE的核心思想 (概率视角)

  • VAE 就是对AE的一个概率化的改造 。
  • 基本假设: 我们假设训练数据 \(x\) 是由一个我们观测不到的隐变量 \(z\) 生成的 。
  • \(z\) 就代表了数据的“本质” (比如,对于人脸,\(z\) 可能代表了性别、年龄、表情、朝向等)。
  • VAE的生成过程 (Sampling):
  1. 我们首先从一个简单的先验分布 (prior) \(p(z)\) 中采样一个 \(z\) 。我们钦定这个先验分布为标准正态分布 \(p(z) \sim N(0, I)\)
  2. 然后,我们从一个条件分布 \(p_\theta(x|z)\) 中采样一个 \(x\) 。这个 \(p_\theta(x|z)\) 就是我们要学习的解码器 (Decoder)

VAE的训练困境:

  • 我们的目标是最大化 \(p(x)\)(最大似然)。
  • 我们没有 \(z\) ,所以必须把 \(z\) 积分掉(边缘化)
  • 数学公式: \(p_\theta(x) = \int p_\theta(x, z) dz = \int p_\theta(x|z) p_\theta(z) dz\)
  • 我们来分析这个积分:\(p_\theta(x|z)\)解码器\(p_\theta(z)\)先验\(N(0, I)\))。
  • 问题: 这个积分 \(\int ... dz\) 是在一个高维空间(\(z\) 的空间)上进行的,我们根本没法计算 (intractable)

VAE的解决方案:变分推断

  • 思路1: 用贝叶斯公式 \(p_\theta(x) = \frac{p_\theta(x|z)p_\theta(z)}{p_\theta(z|x)}\)
  • 新问题: 我们还是卡住了。分母 \(p_\theta(z|x)\) 叫做后验分布 (posterior),它代表“给定一张图片\(x\),它对应的隐变量\(z\)的分布是什么”。这个东西同样是无法计算的
  • “变分”的精髓:

    • 既然我们算不出真实的后验 \(p_\theta(z|x)\)...
    • ...那我们就用另一个神经网络 \(q_\phi(z|x)\)近似它! 。
    • 这个 \(q_\phi(z|x)\) 就是VAE的编码器 (Encoder)
  • 总结: VAE有两个网络 :

    • Encoder (编码器) \(q_\phi(z|x)\) :输入\(x\),输出一个关于\(z\)概率分布
    • Decoder (解码器) \(p_\theta(x|z)\) :输入\(z\),输出一个关于\(x\)概率分布
  • 网络输出: VAE的Encoder和Decoder输出的不是一个向量,而是概率分布的参数

    • Encoder \(q_\phi(z|x)\) :输出均值 \(\mu_{z|x}\) 和(对角)协方差 \(\Sigma_{z|x}\)
    • Decoder \(p_\theta(x|z)\) :输出均值 \(\mu_{x|z}\)(和一个固定的方差 \(\sigma^2\))。
  • 与AE的联系:

    • Decoder的输出是一个高斯分布 \(N(\mu_{x|z}, \sigma^2)\)
    • 最大化它的对数似然 \(log~p_\theta(x|z) = -\frac{1}{2\sigma^2}||x-\mu_{x|z}||_2^2 + C\) ...
    • ...这等价于最小化 L2 重建损失 \(||x-\mu_{x|z}||_2^2\)
    • 所以VAE的重建部分和AE的损失是一致的

ELBO的推导 (VAE的数学核心)

  • 我们的目标是最大化 \(log~p_\theta(x)\)。这是一个非常漂亮的推导:
  • \(log~p_\theta(x) = log \frac{p_\theta(x, z)}{p_\theta(z|x)}\) (贝叶斯)
  • \(= log \frac{p_\theta(x, z) q_\phi(z|x)}{p_\theta(z|x) q_\phi(z|x)}\) (分子分母同乘 \(q_\phi(z|x)\))
  • \(= log ( \frac{q_\phi(z|x)}{p_\theta(z|x)} ) + log( \frac{p_\theta(x, z)}{q_\phi(z|x)} )\) (拆开log)
  • 现在,我们在等式两边同时对 \(z \sim q_\phi(z|x)\)(即Encoder的输出)取期望 \(E_{z \sim q}[...]\)
  • \(E_{z \sim q}[ log~p_\theta(x) ] = E_{z \sim q}[ log ( \frac{q_\phi(z|x)}{p_\theta(z|x)} ) ] + E_{z \sim q}[ log( \frac{p_\theta(x, z)}{q_\phi(z|x)} ) ]\)
  • 左边: \(log~p_\theta(x)\) 是一个关于\(x\)的常数,和\(z\)无关,所以 \(E_{z \sim q}[ log~p_\theta(x) ] = log~p_\theta(x)\)
  • 右边第一项: \(E_{z \sim q}[ log ( \frac{q_\phi(z|x)}{p_\theta(z|x)} ) ]\)。这就是 \(q_\phi\)\(p_\theta\) 之间的 KL散度 \(D_{KL}(q_\phi(z|x) || p_\theta(z|x))\)
  • 右边第二项: 这就是我们要优化的目标。
  • 所以我们得到:
\[log~p_\theta(x) = E_{z \sim q}[ log( \frac{p_\theta(x, z)}{q_\phi(z|x)} ) ] + D_{KL}(q_\phi(z|x) || p_\theta(z|x))\]
  • 我们再把右边第一项拆开:\(p_\theta(x, z) = p_\theta(x|z)p(z)\)
\[log~p_\theta(x) = E_{z \sim q}[ log~p_\theta(x|z) ] - E_{z \sim q}[ log( \frac{q_\phi(z|x)}{p(z)} ) ] + D_{KL}(q_\phi(z|x) || p_\theta(z|x))\]
  • $ E_{z \sim q}[ log( \frac{q_\phi(z|x)}{p(z)} ) ]$ 这一项是 \(q_\phi\)\(p(z)\) 之间的 KL散度 \(D_{KL}(q_\phi(z|x) || p(z))\)

  • 最终的黄金公式:

\[log~p_\theta(x) = \underbrace{E_{z \sim q_\phi(z|x)}[log~p_\theta(x|z)]}_{\text{重建项}} - \underbrace{D_{KL}(q_\phi(z|x) || p(z))}_{\text{先验匹配项}} + \underbrace{D_{KL}(q_\phi(z|x) || p_\theta(z|x))}_{\text{近似误差}}\]
  • 分析这三项:
  1. 重建项 : (Decoder)最大化从Encoder采样\(z\)后,Decoder重建\(x\)的对数似然。我们刚说过,这等价于最小化L2重建损失 。
  2. 先验匹配项 : (Encoder)让Encoder的输出 \(q_\phi(z|x)\) 尽可能地接近我们钦定的先验 \(p(z) \sim N(0, I)\)。这一项可以闭式计算(两个高斯分布的KL散度有公式)。
  3. 近似误差 :\(q\)\(p\)的差距)这是我们的近似 \(q_\phi\) 和真实后验 \(p_\theta\) 之间的差距。这一项是无法计算的
  • ELBO的诞生:

    • KL散度 \(D_{KL}(...) \ge 0\) 恒成立。
    • 所以,我们可以把那个无法计算\(D_{KL}(q_\phi(z|x) || p_\theta(z|x))\) 直接扔掉
    • \(log~p_\theta(x) \ge E_{z \sim q_\phi(z|x)}[log~p_\theta(x|z)] - D_{KL}(q_\phi(z|x) || p(z))\)
    • 这个不等式的右边,就是我们真正要优化的目标!它叫做证据下界 (Evidence Lower Bound, ELBO)
    • 我们最大化ELBO,就是间接地在最大化 \(log~p_\theta(x)\)

VAE的训练流程 (总结)

  • 目标: 最大化ELBO: \(E_{z\sim q_{\phi}(z|x)}[\log p_{\theta}(x|z)] - D_{KL}(q_{\phi}(z|x) || p(z))\)
  • 步骤:
  1. 拿一个输入\(x\)
  2. \(x\) 通过Encoder \(q_\phi\) 得到 \(\mu_{z|x}\)\(\Sigma_{z|x}\)
  3. 计算Loss 1 (Prior Loss): 计算 \(D_{KL}(N(\mu_{z|x}, \Sigma_{z|x}) || N(0, I))\) 。这一项损失会把 \(\mu_{z|x}\) 拉向0,把 \(\Sigma_{z|x}\) 拉向\(I\)(单位矩阵)。
  4. \(q_\phi(z|x)\) 中采样一个 \(z\)
  5. 关键:重参数技巧 (Reparameterization Trick)
    • 问题: “采样”这个操作是不可导的,梯度传不过去。
    • 解决: 我们不直接从 \(N(\mu, \Sigma)\) 采样。
    • 我们先从一个固定的 \(N(0, I)\) 采样一个噪声 \(\epsilon\)
    • 然后用 \(z = \mu_{z|x} + \epsilon \odot \sqrt{\Sigma_{z|x}}\) 来计算 \(z\)
    • 这样,\(z\) 仍然服从 \(N(\mu, \Sigma)\) 分布,但梯度可以顺利地通过 \(\mu\)\(\Sigma\) 传回Encoder。
  6. \(z\) 通过Decoder \(p_\theta\) 得到 \(\mu_{x|z}\)
  7. 计算Loss 2 (Reconstruction Loss): 计算 \(\log p_\theta(x|z)\),等价于计算 \(||x - \mu_{x|z}||_2^2\)

两个Loss的"斗争"

  • Reconstruction Loss 希望 \(z\) 能完美重建 \(x\)。它希望 \(\Sigma_{z|x}\) 越小越好(=0),\(\mu_{z|x}\) 越分散越好(每个\(x\)都有一个专属\(z\))。
  • Prior Loss 希望 \(z\) "长得像" \(N(0, I)\)。它希望 \(\Sigma_{z|x} = I\)\(\mu_{z|x} = 0\),完全不管 \(x\) 是什么。
  • VAE的训练就是在这两个Loss之间找一个平衡,这使得 \(z\) 的空间既规整(像 \(N(0, I)\))又包含信息(能重建 \(x\))。

VAE的采样 (生成)

  • 训练好之后,我们就可以扔掉Encoder了。
  • 步骤:
  1. 从先验 \(z \sim N(0, I)\) 中采样一个 \(z\)
  2. \(z\) 喂给Decoder \(p_\theta\),得到 \(\mu_{x|z}\),这就是生成的图像 。

VAE的特性:解耦 (Disentangling)

  • 由于Prior Loss强迫 \(z\)各个维度独立\(N(0, I)\)的协方差是对角阵)。
  • 这使得 \(z\) 的不同维度往往会学到数据不同且独立的变化因子 。
  • 例子: 在MNIST手写数字上, \(z_1\) 可能学会了控制“数字是几”(从0到9),而 \(z_2\) 可能学会了控制“数字的倾斜角度” 。

第14章:生成模型(下)

生成对抗网络 (GANs)

GANs 是 2014 年提出的一个革命性想法。它彻底抛弃了"计算 \(p(x)\)" 这个难题。


GAN 的核心思想——“伪钞制造者”与“警察”

GAN 的核心是一个二人零和游戏 (minimax game)。它包含两个神经网络:

生成器 (Generator, \(G\)): 它的角色是伪钞制造者。

  • 输入: 一堆随机噪声 \(z\)(通常来自一个简单的高斯分布 \(p(z)\))。
  • 输出: 一张“假”图片 \(x = G(z)\)
  • 目标: 制造出以假乱真的图片,欺骗判别器 \(D\),让 \(D\) 以为它造的图片是真的。

判别器 (Discriminator, \(D\)): 它的角色是警察。

  • 输入: 一张图片(可能是来自真实数据集 \(p_{data}\) 的真图,也可能是 \(G\) 造的假图)。
  • 输出: 一个概率值 \(D(x)\),表示这张图片是真的概率(\(D(x)=1\) 表示"真的", \(D(x)=0\) 表示"假的")。
  • 目标: 尽力区分出真图片和假图片。

训练过程

\(G\)\(D\) 一起训练,相互对抗、共同进化:

  • \(D\) 努力学习,变得越来越擅长识别假图。
  • \(G\) 面对越来越强的 \(D\),被迫学习生成更逼真的假图来蒙混过关。
  • 最终目标: 达到一个“纳什均衡”。此时 \(G\) 生成的图片 \(p_G\) 与真实数据 \(p_{data}\) 完全无法区分, \(D\) 只能靠猜,对任何图片都输出 \(D(x) = 0.5\)

GAN 的目标函数 (数学核心)

这就是那个著名的 minimax 公式:

\[\min_{G} \max_{D} V(G, D) = \min_{G} \max_{D} (\mathbb{E}_{x \sim p_{data}}[\log D(x)] + \mathbb{E}_{z \sim p(z)}[\log(1 - D(G(z)))])\]
  1. 括号里的 \(V(G, D)\) 是判别器 \(D\) 的损失函数(和二元交叉熵损失函数长得一模一样)。
  • \(D(x)\)\(D\) 认为 \(x\) 是真的概率。
  • \(D(G(z))\)\(D\) 认为 \(G(z)\) (假图)是真的概率。
  • \(1 - D(G(z))\) 就是 \(D\) 认为 \(G(z)\) 是假的概率。
  1. \(\max_{D}\) ... \(D\) 的目标是最大化 (max) \(V(G, D)\)
  • 对于真图 \(x \sim p_{data}\)\(D\) 希望 \(D(x)\) 尽可能接近 1。最大化 \(D(x)\) 也就是最大化 \(\log D(x)\)
  • 对于假图 \(G(z)\)\(D\) 希望 \(D(G(z))\) 尽可能接近 0。这等价于让 \(1 - D(G(z))\) 接近 1。最大化 \(1 - D(G(z))\) 也就是最大化 \(\log(1 - D(G(z)))\)
  • 合起来: \(D\) 的训练目标就是最大化 \(V(G, D)\)
  1. \(\min_{G}\) ... \(G\) 的目标是最小化 (min) \(V(G, D)\)
  • \(G\) 无法影响第一项 \(\mathbb{E}_{x \sim p_{data}}[\log D(x)]\) (因为它动不了真数据)。
  • \(G\) 只能影响第二项。\(G\) 的目标是欺骗 \(D\),让 \(D\) 以为 \(G(z)\) 是真的,即让 \(D(G(z))\) 尽可能接近 1。
  • \(D(G(z)) \to 1\) 时, \(1 - D(G(z)) \to 0\)\(\log(1 - D(G(z))) \to -\infty\)
  • 合起来: \(G\) 的训练目标就是最小化 \(\log(1 - D(G(z)))\) 这一项,从而最小化整个 \(V(G, D)\)

训练 GAN 的一个大坑:梯度消失

在训练初期, \(G\) 的生成能力很差, \(D\) 很容易就能 100% 识别出假图,即 \(D(G(z))\) 非常接近 0。

我们来看 \(G\) 的损失函数(它要最小化的): \(L_G = \log(1 - D(G(z)))\)

\(D(G(z))\) 接近 0 时,这条曲线极其平坦 (flat)。梯度 = 导数 = 斜率。平坦意味着梯度消失!

\(G\) 根本不知道该往哪个方向更新参数才能骗过 \(D\),导致训练卡住。

解决方案:

我们修改 \(G\) 的目标。\(G\) 原本的目标是“最小化 \(D\) 识别出它是假的概率”,我们把它换成“最大化 \(D\) 识别出它是真的概率”。

  • 原目标 (Minimax Loss): \(G\) 最小化 \(L_G = \log(1 - D(G(z)))\)。 (梯度消失)
  • 新目标 (Non-Saturating Loss): \(G\) 最大化 \(L_G' = \log(D(G(z)))\)。 (等价于最小化 \(-\log(D(G(z)))\))

为什么这个新目标更好?

\(-\log(D(G(z)))\)

  • \(D(G(z))\) 接近 0 时 (训练初期),这条曲线极其陡峭。
  • 这意味着 \(G\) 能获得非常大、非常有用的梯度!\(G\) 就能快速学习如何生成更好的图片。

这个小改动是让 GAN 真正能训练起来的关键技巧之一。


GAN 架构和特性

  • DC-GAN: 第一个将 GANs 与卷积神经网络 (CNNs) 结合的成功架构。它使用“转置卷积”(Transposed Convolution) 来实现从低维噪声 \(z\) 到高维图像的上采样。
  • StyleGAN: 一个更先进的架构,能生成超高质量的人脸。

核心思想: 它不直接把 \(z\) 喂给生成器,而是先通过一个“映射网络 \(f\)” 把它转换成一个“风格向量” \(w\)

这个 \(w\) 通过一种叫 AdaIN (Adaptive Instance Normalization) 的技术,在生成网络的每一层被注入进去,用来控制生成图像的“风格”(比如发型、肤色、角度)。

  • AdaIN 公式: \(AdaIN(x, w, b)_i = w_i \frac{x_i - \mu(x)}{\sigma(x)} + b_i\)。它先将 \(x\) 标准化,然后再用学到的风格 \(w\) (scale) 和 \(b\) (shift) 去调整它。

特性:潜在空间插值:GAN 的潜在空间 \(z\) 通常是平滑连续的。这意味着,如果你拿两个噪声 \(z_0\) (生成A) 和 \(z_1\) (生成B),你可以在它们之间做线性插值: \(z_t = t \cdot z_0 + (1-t) \cdot z_1\)。当你把 \(z_t\) 喂给 \(G\) 时, \(x_t = G(z_t)\) 会产生一张从 A 平滑过渡到 B 的“中间”图像。这就是你在视频里看到的那些人脸平滑变化的效果。


GAN 总结

优点: 图像质量非常高(一度是SOTA),生成速度快(一步到位)。

缺点:

  • 没有损失曲线可看: \(G\)\(D\) 的损失是在对抗中浮动的,你没法像监督学习那样看"loss"是否在下降。
  • 训练不稳定: \(G\)\(D\) 必须势均力敌。如果 \(D\) 太强,\(G\) 就梯度消失;如果 \(G\) 太强, \(D\) 就学不到东西。
  • 模式崩溃 (Mode Collapse): \(G\) 可能会“偷懒”,发现只生成一种(或几种)特别逼真的图像就能骗过 \(D\),于是它就只生成那几张图,失去了多样性。

扩散模型 (Diffusion Models)

这是从 2021 年左右开始超越 GANs、目前最火的生成模型,也是 Stable Diffusion, DALL-E 3 和 Sora 的技术基石。


扩散模型的核心直觉

GAN 是一步到位生成图像,而扩散模型是迭代式地生成图像。

它包含两个过程:

  1. 前向过程 (Forward Process):

“人话”: 破坏图像的过程。

  • 从一张真图 \(x_0\) (\(t=0\)) 开始。
  • 逐步、多次(比如 \(T=1000\) 步)往图片上加高斯噪声。
\[x_1, x_2, ..., x_T\]
  • 直到 \(t=T\) 时,原始图片 \(x_T\) 变成了一张纯粹的、无意义的噪声图。
  • 这个过程是固定的、已知的,不需要学习。
  1. 反向过程 (Reverse Process):

“人话”: “雕刻”图像的过程,也是模型要学习的。

  • 目标: 训练一个神经网络 \(f_\theta\),让它学会“去噪”
  • 具体来说, \(f_\theta\) 的任务是:给定一张任意噪声水平 \(t\) 的图片 \(x_t\),它要能预测出 \(x_{t-1}\),即“去除一小步噪声”。

如何生成 (Sampling / Inference):

一旦你训练好了这个“去噪网络” \(f_\theta\)

  • 先从 \(p_{noise}\) (高斯分布) 中采样一张纯噪声图 \(x_T\)
  • \(x_T\) 和时间 \(T\) 喂给 \(f_\theta\),让它预测 \(x_{T-1}\)
  • 再把 \(x_{T-1}\) 和时间 \(T-1\) 喂给 \(f_\theta\),让它预测 \(x_{T-2}\)
  • ...
  • 重复这个迭代过程 \(T\) 次,你就能从一张纯噪声图 "雕刻" 出一张清晰的图像 \(x_0\)

Rectified Flow (一个现代、简洁的扩散框架)

核心思想: 它假设数据点 \(x\)(真图)和噪声点 \(z\)(噪声图)之间存在一个直线路径。

训练过程:

  • 从数据集中拿一张真图 \(x \sim p_{data}\)
  • 从高斯分布中拿一张噪声图 \(z \sim p_{noise}\)
  • 随机选一个时间 \(t \sim \text{Uniform}[0, 1]\)
  • 通过线性插值 (linear interpolation) 制造出“中途”的噪声图 \(x_t\):
\[x_t = (1-t)x + tz\]
  • \(t=0\) 时, \(x_t = x\) (真图)。
  • \(t=1\) 时, \(x_t = z\) (噪声图)。

定义这条直线的速度向量 (velocity vector) \(v\) 为:

\[v = z - x\]

(即从 \(x\) 指向 \(z\) 的向量)

训练目标: 训练一个神经网络 \(f_\theta(x_t, t)\),让它在任意时间 \(t\) 看到 \(x_t\) 时,都能准确预测出这个速度 \(v\)

损失函数 (Loss): 就是一个简单的 L2 (MSE) 回归损失:

\[L = || f_\theta(x_t, t) - v ||_2^2 = || f_\theta(x_t, t) - (z - x) ||_2^2\]

这个目标非常巧妙。网络 \(f_\theta(x_t, t)\) 学会了在任意点 \(x_t\) 预测出“终点” \(z\) 和“起点” \(x\) 之间的方向 \(v\)。这意味着它学会了这条路径。

采样过程:

如果 \(f_\theta\) 学会了从 \(x\)\(z\) 的路径(方向是 \(v\)),那么我们反过来,从 \(z\) 出发,沿着 \(-v\) 的方向 走,不就能回到 \(x\) 吗?

  • 选择一个步数 \(T\) (比如 50 步)。
  • \(x_1 = z \sim p_{noise}\) (纯噪声) 开始。
  • 迭代 \(T\) 次 (从 \(t=1\) 降到 \(t=0\)):
  • 在当前时间 \(t\),计算出模型预测的速度: \(v_t = f_\theta(x_t, t)\)
  • 往反方向走一小步 (步长为 \(1/T\))。这在数学上叫欧拉法 (Euler method) 积分。
\[x_{\text{new}} = x_t - v_t / T\]
  • 更新 \(x_t \leftarrow x_{\text{new}}\)
  • 迭代 \(T\) 步后, \(x_0\) 就是我们生成的图像。

条件生成 与 无分类器引导 (CFG)

这是扩散模型最重要的技巧之一,也是你能在 Stable Diffusion 里输入 "a cat" 就生成猫的关键。

条件生成:

  • 训练时,把条件 \(y\)(比如 "cat" 的文本嵌入)也作为 \(f_\theta\) 的输入: \(v = \text{model}(x_t, y, t)\)
  • 采样时: 告诉模型你要的 \(y\): \(v = \text{model}(\text{sample}, y, t)\)

问题: 我们如何控制模型在多大程度上“遵从”我们的条件 \(y\)?(比如,我想要“非常像猫”还是“有点创意,不太像猫”?)


无分类器引导 (Classifier-Free Guidance, CFG):

在训练时,我们随机地 (比如 50% 的概率)“丢弃”条件 \(y\),用一个空条件 \(y_{\text{null}}\) 来代替。

if random.random() < 0.5: y = y_null

结果: 我们用同一个模型,同时训练了它两种能力:

  • 条件生成: 当输入 \(y\) 时,它知道要生成 \(y\) 对应的图像 (\(p(x|y)\))。
  • 无条件生成: 当输入 \(y_{\text{null}}\) 时,它知道要生成一张任意的、普通的图像 (\(p(x)\))。

在采样时(比如生成 "a cat"),在每一步 \(t\),我们让模型计算两次:

  • 条件预测 \(v_y\): \(v_y = f_\theta(x_t, y, t)\) (比如 \(y=\) "a cat")。这个向量指向“猫”的方向。
  • 无条件预测 \(v_\emptyset\): \(v_\emptyset = f_\theta(x_t, y_{\text{null}}, t)\)。这个向量指向“任意物体”的方向。

核心洞察: \(v_y\)\(v_\emptyset\) 都包含“生成一张图”的信息,但 \(v_y\) 额外包含了“像猫”的信息。

“引导”向量: \((v_y - v_\emptyset)\) 这个向量,就代表了纯粹的、“从任意物体指向猫”的方向。

最终预测 \(v_{cfg}\): 我们可以通过一个引导尺度 (guidance scale) \(w\) (在 UI 里常叫 CFG scale) 来放大这个引导方向:

\[v_{cfg} = v_\emptyset + (1+w) \cdot (v_y - v_\emptyset)\]

效果:

  • 如果 \(w=0\)\(v_{cfg} = v_y\) (普通的条件生成)。
  • 如果 \(w > 0\) (比如 \(w=7.5\)),模型会“更用力地”朝着“猫”的方向去噪,生成的图像会更贴合文本提示,质量更高。

代价: 每次采样都要计算两次,总成本加倍,但效果极好。


潜在扩散模型 (Latent Diffusion Models, LDM)

在像素空间 (Pixel Space) (比如 \(1024 \times 1024 \times 3\)) 上跑扩散模型,计算量极其巨大。因为 \(f_\theta\) (通常是 U-Net 或 Transformer) 必须在每一步都处理这个超高维数据。

我们不在像素空间搞,我们去一个更小、更便宜的潜在空间 (Latent Space) 搞。

步骤:

第一阶段 (训练 VAE): 我们先训练一个自编码器 (Autoencoder) (比如 VAE)。

  • 编码器 (Encoder): \(E(x)\),将 \(H \times W \times 3\) 的大图 \(x\) 压缩成 \(h \times w \times c\) 的小潜在表示 (latent)。(例如 \(512 \times 512 \to 64 \times 64\))
  • 解码器 (Decoder): \(D(z)\),将 \(h \times w \times c\) 的小 latent 还原回 \(H \times W \times 3\) 的大图。

第二阶段 (训练 LDM):

  • 冻结 VAE 的所有参数。
  • 现在,我们训练扩散模型 \(f_\theta\)\(h \times w \times c\) 的潜在空间里进行去噪。

\(f_\theta\) 的输入和输出都是小 \(h \times w \times c\) 的 latent)。

  • LDM 采样:

在高斯分布中采样一个小的噪声 latent \(z_T\) (e.g., \(64 \times 64 \times c\))。

在潜在空间中执行 \(T\) 步去噪 (非常快,因为 latent 很小)。

得到一个干净的 latent \(z_0\)

最后一步: 将 \(z_0\) 仅通过一次 VAE 的解码器 \(D\)\(x_0 = D(z_0)\),还原成 \(512 \times 512 \times 3\) 的高清大图。

如何训练这个 VAE?:

  • 它是一个 VAE。
  • 问题: 普通 VAE 解码的图通常很模糊。
  • 解决方案: 在 VAE 的基础上,再加一个GAN 判别器!

VAE-GAN 混合: 解码器 \(D\) 不仅要最小化“重建损失”(让 \(D(E(x))\) 尽量像 \(x\)),还要扮演 GAN 中生成器的角色,去骗过判别器。

最终: 现代的 LDM (如 Stable Diffusion) 是一个三合一的怪物:

  • 一个 VAE (用于压缩)。
  • 一个 GAN 判别器 (帮 VAE 训练,使其解码更清晰)。
  • 一个 Diffusion Model (在潜在空间中去噪生成)。

扩散模型 + Transformer (DiT)

那个核心的去噪网络 \(f_\theta\) 以前常用 U-Net (一种CNN)。而 DiT (Diffusion Transformer)提出,把 U-Net 换成 Transformer 效果更好,扩展性更强。

Patchify: 就像 ViT 一样,把 \(h \times w \times c\) 的噪声 latent 切成 \(N\) 个 "patches"(图像块)。

加入条件:

  • 时间 \(t\): 把它变成一个 embedding,然后通过预测 scale 和 shift 参数 (即 \(\gamma, \beta\)) 注入到 Transformer 的 LayerNorm 层中。这叫 adaLN (Adaptive LayerNorm)。
  • 文本 \(y\) (Label): 把它变成 embedding,然后通过交叉注意力 (Cross-Attention) 注入到 Transformer 中。

总结:现代文生图/视频系统

目前 SOTA (如 DALL-E 3, Sora) 的完整流程:

  • 输入: 文本 "A cat..."
  • 文本编码器 (Text Encoder): (如 T5, CLIP) 将文本 \(y\) 转换成 Text Embeddings (一堆向量)。
  • LDM 采样开始:
  1. 生成一个噪声 Latent \(x_t\)
  2. 生成一个时间步 \(t\)
  • DiT (核心去噪):

\(x_t\), \(t\), \(y\) (Embeddings) 全部喂给 Diffusion Transformer。

(内部使用 CFG,即计算两次:一次带 \(y\),一次带 \(y_{\text{null}}\))。

  • 输出预测的 "干净 Latent" (或速度 \(v\))。
  • 迭代: 重复 3-4 步 \(T\) 次。
  • 解码: 得到最终的干净 Latent \(x_0\)
  • VAE 解码器 (Decoder): \(x_0 \to \text{Decoder} \to\) 输出高清图像/视频。

总结与其他视角

扩散蒸馏 (Distillation)

问题: 扩散模型采样太慢(要 30-50 步)。

解决: "蒸馏" (Distillation) 技术。用一个训练好的 T 步模型作为“老师”,去教一个“学生”模型如何用更少的步数(比如 T/2 步)达到同样的效果。不断重复这个过程,最终可以“蒸馏”出一个1 步就能生成高质量图像的模型。


扩散模型的“统一框架”

这一段非常数学,它的核心是告诉我们:所有扩散模型都是相通的。

我们学的 Rectified Flow (\(x_t = (1-t)x + tz\),预测 \(v=z-x\)) 只是其中一种。

广义框架:

  • \(x_t = a(t)x + b(t)z\) (加噪过程)
  • \(y_{gt} = c(t)x + d(t)z\) (预测目标)

通过选择不同的 \(a, b, c, d\) 函数,你就可以得到文献中所有不同名字的扩散模型(如 VP, VE, \(\epsilon\)-prediction, x-prediction...)。它们本质上都是在做同一件事:学习一个从数据到噪声的"流",然后再逆转这个"流"。


扩散模型的多种“马甲”

扩散模型为什么这么强?因为它从不同的理论视角来看都是合理的:

  • 是 VAE: 它可以被看作一个有 \(T\) 层的、非常深的 VAE。
  • 是分数函数 (Score Function): 它的去噪网络 \(f_\theta\) 实际上是在学习 \(\nabla_x \log p(x)\),即数据分布的“梯度”(指向更高概率密度的方向)。
  • 是 SDE 求解器: 它可以被看作是在求解一个“随机微分方程”(SDE) 的逆向过程。

重点: 这些理论不是相互矛盾的,而是从不同数学角度对同一个核心思想(从噪声中恢复数据)的完美解释。


自回归模型的“反击”

还记得那个“一个像素一个像素”生成的自回归 (AR) 模型吗?它在像素上太慢了。

新思路: 为什么我们不用它来生成 Latent 呢?

  • VQ-VAE: 这是一种特殊的 VAE,它的潜在空间是离散的(比如 \(32 \times 32\) 个格子,每个格子是1到8192中的一个整数)。
  • AR + VQ-VAE:

用 VQ-VAE 把一张图 \(x\) 压缩成一串离散的整数 ID (比如 \(32 \times 32 = 1024\) 个 ID)。

训练一个大 Transformer (就像 GPT),让它学会自回归地预测这串 ID (预测第 \(n\) 个 ID | 前 \(n-1\) 个 ID)。

采样: 启动 Transformer,让它凭空“写”出一串新的 ID,然后把这串 ID 喂给 VQ-VAE 的解码器,生成新图。

(这是 Google 的 Imagen, Parti 等模型采用的路线,也是扩散模型的主要竞争对手)。


第15章:3D视觉

3D几何表示方法 (3D Geometry Representations)

在计算机中,我们该如何表示一个3D物体?

就像我们在2D中可以用像素、矢量图形等不同方式表示图像一样,3D中也有很多方法。选择哪种方法,取决于我们的需求:

  • 存储与创建:这种表示方式占多少空间?容不容易创建新形状?
  • 操作:是否容易编辑、简化、平滑?
  • 渲染:是否容易把它画出来(比如光栅化、光线追踪)?
  • 动画:是否容易让它动起来?

没有一种表示法是完美的,所以我们有很多选择。课程将它们分成了两大类:

  1. 显式表示 (Explicit Representation):直接描述物体的表面。就像你用积木搭个房子,每一块积木(点、面)都是你明确放上去的。
  2. 隐式表示 (Implicit Representation):通过一个规则(通常是一个函数)来定义物体的空间。就像你定义一个规则“所有距离中心点小于R的点”,这个规则就隐式地定义了一个球体。

显式表示 (Explicit Representations)

点云 (Point Clouds)

这是最简单、最“原始”的3D表示法。

点云就是一大堆点的集合,每个点有它的3D坐标 $ (x, y, z) $。

它没有连接信息。我们只知道一堆点在那里,但不知道哪个点和哪个点是邻居,不知道它们如何构成一个表面。

有时候,点上还会附带额外信息,比如法向量 (normal)(表示这个点朝向哪个方向),带法向量的点被称为Surfels。法向量对于光照和着色至关重要。

它们通常是3D扫描设备(比如LiDAR激光雷达、Kinect深度摄像头)的原始输出。

这些设备从不同角度扫描物体,得到多张“深度图”或“点云扫描”,然后通过一个叫配准 (Registration) 的过程把它们拼在一起。

因此,点云数据往往是有噪声 (noisy) 且不完整的。

优点:

  • 简单,能表示任意几何体。
  • 非常适合存储大型数据集(比如LIDAR扫描的整个城市)。

缺点:

  • 没有拓扑信息 (topological information)!这是最大的问题。我们无法轻易地知道“表面”在哪里,或者这个物体有几个“洞”。
  • 如果采样不足(点很稀疏),很难渲染出平滑的表面。
  • 无法进行平滑、简化等高级操作(因为不知道点与点的关系)。

多边形网格 (Polygonal Meshes)

这是计算机图形学(CG)和游戏领域最常用的表示法。

它在点云的基础上,增加了连接信息。

它由三个核心元素构成:

  • 顶点 (Vertices):就是点云里的点 $ (x, y, z) $。
  • 边 (Edges):连接两个顶点的线。
  • 面 (Faces):由三条或更多边闭合形成的平面,最常见的是三角形网格 (Triangle Mesh)。

网格通过面来描述物体的边界(表面)。

应用?

无处不在。从米开朗基罗的“大卫”雕像的超高精度扫描(几千万个三角形),到Google Earth里的整个城市模型(上万亿个三角形)。

为什么比点云好?

因为它有拓扑结构!我们知道了点、边、面之间的邻接关系。

这使得我们可以对它进行各种强大的操作:

  1. 网格上采样 / 细分 (Upsampling / Subdivision):通过在现有的三角形中插入新的顶点和面,来增加网格的分辨率,使其变得更平滑。
  2. 网格下采样 / 简化 (Downsampling / Simplification):在尽量保持物体原状的前提下,减少顶和面的数量。这在游戏中至关重要(比如LOD, Level of Detail,远处的物体用低模,近处的用高模)。
  3. 网格正则化 (Regularization):优化网格上点的分布,使其更均匀,提高网格质量。

参数化表示 (Parametric Representation)

这也是一种显式表示,但它不再是离散地存储点和面,而是用一个连续函数来生成表面。

是什么?

一个参数化表面是一个从2D映射到3D的函数:$ f: \mathbb{R}^2 \to \mathbb{R}^3 $。

我们有两个参数,通常叫 $ (u, v) $,它们在一个2D的定义域(比如一个正方形)内取值。

对于每一个 \((u, v)\) 对,函数 \(f(u, v)\) 会输出一个3D坐标 $ (x, y, z) $。

比喻:你可以把 \((u, v)\) 想象成一块布料(2D平面)上的坐标,而函数 \(f\) 告诉我们这块布料在3D空间中是如何扭曲和摆放的。

举例:

  • 1D -> 2D (曲线):一个圆。

参数是 \(t\) (在 \([0, 2\pi)\) 范围内)。

函数是 $ p(t) = (r \cdot \cos(t), r \cdot \sin(t)) $。

随着 $ t \(从0变到\) 2\pi $,你就在2D平面上“画”出了一个圆。

  • 2D -> 3D (表面):一个球。

参数是 \((u, v)\) (分别代表经度和纬度)。

$ u \in [0, 2\pi) \(,\) v \in [-\pi/2, \pi/2] $。

函数是 $ s(u, v) = (r \cdot \cos(u)\cos(v), r \cdot \sin(u)\cos(v), r \cdot \sin(v)) $。

通过在 \((u, v)\) 域上取值,你就能“画”出整个球面。

常见的参数化表示:

  • Bézier曲线/曲面:这是CAD(计算机辅助设计)中非常重要的一种。它不是用一个复杂的全局公式,而是用几个控制点 (control points) 来定义一个光滑的曲线或曲面片 (patch)。你通过拖拽控制点,就能像拉扯橡皮筋一样改变表面的形状。
  • NURBS (非均匀有理B样条):Bézier的升级版,更强大,是工业设计的标准。
  • 细分曲面 (Subdivision Surfaces):这是一个混合体。你从一个粗糙的多边形网格(显式)开始,然后迭代地应用一个平滑规则(参数化)来“分裂”和“平滑”这个网格,最终得到一个非常光滑的曲面。

显式表示的优缺点总结

优点:

采样简单 (Sampling Is Easy):如果你想在表面上找到一个点,这太容易了。对于参数化表示,你随便选一个 \((u, v)\) 就能算出一个点。对于网格,你可以在一个三角形内做插值。

缺点:

内外判断困难 (Inside/Outside Test Hard):给你一个任意的3D点 $ P(x, y, z) $,我很难判断这个点是在物体的内部、外部、还是表面上。对于网格,你需要做非常复杂的几何测试(比如射线相交)。


隐式表示 (Implicit Representations)

隐式表示通过一个函数 \(f(x, y, z)\) 来定义一个标量场 (scalar field),这个函数会给空间中的每一个点 \((x, y, z)\) 都赋予一个值。

表面被定义为函数值为0的点的集合,即 $ f(x, y, z) = 0 $。这被称为零水平集 (zero level set)。

比喻:想象整个空间中充满了不同温度的空气。隐式函数 \(f\) 就是测量任意一点 \((x, y, z)\) 的温度。你定义的表面就是所有“温度为0度”的点连接起来形成的那个面。

举例:

一个球体。

函数是 $ f(x, y, z) = x^2 + y^2 + z^2 - r^2 $。

所有满足 \(f = 0\) (即 \(x^2 + y^2 + z^2 = r^2\)) 的点构成了球面。

一个甜甜圈 (Torus):$ f(x, y, z) = (R - \sqrt{x^2 + y^2})^2 + z^2 - r^2 $。


隐式表示的优缺点总结

优点:

内外判断简单 (Inside/Outside Tests Easy):这是它最强的优点!

  • $ f(x, y, z) < 0 $:点在物体内部。
  • $ f(x, y, z) > 0 $:点在物体外部。
  • $ f(x, y, z) = 0 $:点在物体表面。

可以轻松表示非常复杂的拓扑结构,并且在融合、变形时不会自相交。

缺点:

采样困难 (Sampling Can Be Hard):你很难直接“找到”所有 \(f=0\) 的点。你需要通过求解方程或搜索算法(比如 Marching Cubes,我们后面会提到)来提取出表面网格。


隐式表示的类型

  1. 代数曲面 (Algebraic Surfaces):

\(f\) 是一个 \((x, y, z)\) 的多项式。比如球、甜甜圈。

对于简单的形状很棒,但对于一个复杂的“牛”或“兔子”,这个多项式会变得极其复杂,几乎不可能写出来。

构造实体几何 (Constructive Solid Geometry - CSG):

这是一种通过布尔运算(Boolean operations)来组合简单隐式形状(如球体、方块、圆柱体)来创建复杂形状的方法。

  • \(A\)\(B\) 是两个隐式物体(由 \(f_A\)\(f_B\) 定义)。
  • 并集 (Union) (\(A \cup B\)):$ f = \min(f_A, f_B) $
  • 交集 (Intersection) (\(A \cap B\)):$ f = \max(f_A, f_B) $
  • 差集 (Difference) (\(A \setminus B\)):$ f = \max(f_A, -f_B) $

这在CAD和建模软件中非常强大。

  1. 有向距离函数 (Signed Distance Functions - SDF):

这是目前AI领域最重要的一种隐式表示。

\(f(x, y, z)\) 的值不是任意的,它被严格定义为:点 \((x, y, z)\) 到物体表面的最短距离。

并且,它带有一个符号:内部为负,外部为正。

SDF非常好用,因为它的梯度 $ \nabla f $(函数变化最快的方向)的模长恒为1,并且总是指向(或背离)最近的表面。


离散隐式表示:水平集 (Level Sets) 与 体素 (Voxels)

问题:对于“兔子”或“大卫”雕像,我们不可能写出一个漂亮的全局SDF函数 $ f(x, y, z) $。

解决方案:我们不在乎公式!我们直接在空间中划分一个3D网格 (grid),然后只存储这个网格每个单元上的 \(f\) 的值。

水平集 (Level Sets):

  • 这就是一个3D网格,存储了每个点的SDF值(比如 $ -0.5, -0.1, +0.3, ... $)。
  • 表面 \(f=0\) 会穿插在这些网格单元之间。

医学数据 (CT, MRI) 天然就是这种形式(存储的是组织密度)。

体素 (Voxels):

这是水平集的一个简化 / 二值化版本。

我们不存储连续的距离值,只存储 0 或 1。

1 (occupied):这个格子在物体内部。

0 (empty):这个格子在物体外部。

比喻:Voxel就是3D版的像素 (Pixel -> Voxel),或者说,它就是《我的世界》(Minecraft)

  • 优点:结构简单,特别适合用3D卷积神经网络 (3D CNN) 处理。
  • 缺点:内存爆炸!它不关心表面,把内部也存了。一个 \(256 \times 256 \times 256\) 的网格就需要 \(256^3 \approx 1670\) 万个体素,非常占内存。

3D 数据集 (3D Datasets)

我们有了表示法,AI要学习就需要数据。

早期数据集:

  • Princeton Shape Benchmark (PSB):只有约1800个模型,规模很小。

现代大规模数据集:

  • ShapeNet:3D领域的ImageNet。

包含几百万个模型,其中 ShapeNetCore 是一个常用的子集,有约 5.1万个模型,覆盖55个类别(椅子、桌子、汽车等)。

这是训练单个物体理解模型的基石。

  • Objverse:规模更大的合成数据集 (800K ~ 10M)。

  • CO3D:这是一个真实世界数据集,包含50个类别的约1.9万个视频。它用于从多张2D图像重建3D。

超越物体:部件 (Parts) 和 场景 (Scenes)

  • PartNet:更进一步,不仅标注了“这是一个椅子”,还把它拆分成了部件。

它是一个层级化 (Hierarchical) 的标注,比如:椅子 -> [座位, 靠背, 腿, 扶手],腿 -> [轮子, 支柱]。

这对于细粒度的理解至关重要。

  • ScanNet:3D场景领域的ImageNet。

它不是单个物体,而是通过RGBD摄像头扫描的整个室内房间(1500多个扫描)。

它有3D重建的表面、摄像头姿态和实例级语义分割(比如,标出“这是1号椅子”,“这是2号椅子”,“这是桌子”)。


AI + 3D几何任务 (AI + 3D Geometry Tasks)

有了表示法和数据集,我们想让AI干什么?

判别式模型 (Discriminative Models) - $ P(c|S) $:理解3D

给定一个形状 $ S $,预测它的类别 $ c $。

任务:

  • 分类 (Classification):这个物体是“椅子”还是“桌子”?
  • 部件分割 (Part Segmentation):标出椅子的“腿”和“靠背”。
  • 语义分割 (Semantic Segmentation):在ScanNet场景中,标出所有的“椅子”、“桌子”、“地板”。
  • 生成式模型 (Generative Models) - $ P(S) \(或\) P(S|c) $:创建3D

无条件($ P(S) \()或有条件(\) P(S|c) $)地生成一个新形状 $ S $。

任务:

  • 形状生成 (Shape Generation):“给我生成一个椅子”。
  • 形状补全 (Shape Completion):给你一个只有一半的椅子(比如被遮挡了),预测另一半。
  • 2D/3D 联合建模:

这是目前最火的领域,连接2D图像和3D世界。

比如:单张图像重建 (Single-view 3D Reconstruction):给一张2D照片,重建出它的3D模型。


深度学习如何处理3D数据

方法一:基于多视图 (Multi-View CNNs)

核心思想:我们不擅长处理3D,但我们(指2015年左右的AI)已经非常擅长2D图像了(ImageNet, VGG, ResNet)。那么,我们能把3D问题转化成2D问题吗?

流程:

  1. 渲染 (Render):在3D物体周围放置一圈(比如12个)虚拟相机。
  2. 拍照:从每个相机视角拍摄一张2D快照 (View 1, ..., View N)。
  3. 2D特征提取 (\(CNN_1\)):将这 $ N \(张2D图像**分别**输入一个标准的2D CNN(比如AlexNet或VGG,权重共享)中,提取出\) N $ 个特征向量。
  4. 视图池化 (View Pooling):这是关键一步。我们现在有 \(N\) 个特征向量,但我们只想要一个全局特征来代表这个3D物体。我们使用一个对称函数 (symmetric function) 来聚合它们,最简单的就是 Element-wise Max-Pooling(逐个元素取最大值)。
  5. 分类 (\(CNN_2\)):将这个聚合后的全局特征送入第二个CNN(或MLP)中,进行最终分类。

为什么有效?

  • 它成功地利用了强大的预训练2D模型。
  • “View Pooling”这个操作使其具有视角不变性(旋转物体,只要相机能拍到,结果都差不多)。

在2015年,这是ModelNet 40分类榜单的SOTA(State-of-the-art)。

缺点:

  • 这是一个“作弊”的方法,它没有真正理解3D几何,只是看了很多2D投影。
  • 它需要一个“干净”的3D模型才能进行渲染。如果输入是杂乱的、不完整的原始点云,这个方法就失效了。

方法二:基于体素 (Volumetric CNNs)

核心思想:既然Voxel(体素)是3D版的像素,那我们能不能把2D CNN($ k \times k $ 卷积核)直接扩展成 3D CNN($ k \times k \times k $ 卷积核)?

3D ShapeNets:

能! 这就是3D ShapeNets(CVPR 2015)做的事情。

  • 输入是一个二值化的体素网格(比如 $ 30 \times 30 \times 30 $)。
  • 网络结构就是一堆3D卷积层和3D池化层,最后接全连接层。

优点:

这是第一个真正“原生”处理3D数据的CNN架构。

它也可以用于生成,比如 3D-GANs,由一个3D反卷积网络(Decoder)从一个随机向量生成3D体素形状。

巨大缺点:

  • 计算量和内存开销是 \(O(N^3)\) 立方增长的!
  • \(30^3\) 的分辨率太低了,物体表面很多细节都丢失了。
  • 如果想提高到 \(256^3\),内存根本存不下。
  • 效率低下:3D物体是稀疏 (sparse) 的,大部分体素都是空 (empty) 的。在空无一物的地方做卷积是巨大的浪费。

解决方案:八叉树 (Octree)

核心思想:我们只在有物体的地方进行细分和计算。

八叉树 是一种数据结构,它递归地将空间分成8个子块(八叉树因此得名)。如果一个子块是完全空的,或者完全满的,就不再细分。只有在表面附近(混合块)才继续细分下去。

OctNet 和 O-CNN 就是在这种数据结构上定义了卷积操作。

优点:极其节省内存!计算量只和表面积 $ O(N^2) $ 相关,而不是和体积 \(O(N^3)\) 相关。这使得 \(256^3\) 甚至更高分辨率的3D CNN成为可能。


方法三:基于点云 (PointNet)

这是3D深度学习的里程碑式的工作 (CVPR 2017)。

核心思想:体素化(Voxelization)会丢失精度,而且很慢。我们能不能直接处理最原始的点云数据?

面临的核心问题:

  • 无序性 (Unordered):点云是一个集合 (set)。$ {P_1, P_2, P_3} $ 和 \(\{P_3, P_1, P_2\}\) 是同一个物体。但标准的MLP或CNN是有序的(输入顺序改变,输出也会改变)。
  • 数量可变 (Variable size):不同的点云有不同数量的点。
  • 变换不变性 (Transformation Invariance):我们希望模型能识别出同一个物体,即使它被旋转或平移了。

PointNet的解决方案:

如何构造一个对输入顺序不敏感的(即置换不变性 Permutation Invariance)网络?

答案:使用一个对称函数 (Symmetric Function),比如 \(\sum\) (求和) 或 \(\max\) (取最大值)。

PointNet 架构 (用于分类):

  • 输入:$ N \times 3 $ 的点云(N个点,每个点3个坐标)。
  • T-Net (小):先用一个小网络(叫T-Net)预测一个 \(3 \times 3\) 仿射变换矩阵,对齐输入的点云(解决平移旋转问题)。
  • 特征提取 (\(h\)):用一个共享权重的MLP(多层感知机)将每个点独立地从 \(3\) 维映射到高维(比如 \(1024\) 维)。得到一个 \(N \times 1024\) 的特征矩阵。
  • 对称操作 (\(g\)):执行 Max Pooling 操作。在 \(N\) 个点上取Max,把 \(N \times 1024\) 的特征矩阵“压”成一个 \(1 \times 1024\) 的全局特征向量 (global feature)。
  • 分类 (\(\gamma\)):将这个全局特征送入一个MLP,输出分类得分。

为什么 Max Pooling 有效?

\[\max{h(P_1), h(P_2), h(P_3)} = \max{h(P_3), h(P_1), h(P_2)}\]

它提取了所有点中最“突出”的特征,这个特征组合(全局特征向量)代表了整个形状的“签名”。

如何做分割 (Segmentation)?

  • 在得到全局特征后,把它拼接 (concatenate) 回每个点的高维特征上。
  • 这样,每个点既有自己的“局部信息”,又有了“全局信息”(它知道自己是“椅子”的一部分)。

然后再通过几个MLP,对每个点进行分类(比如“椅腿”、“椅背”)。

后续改进 (PointNet++, Graph NNs):

  • PointNet的缺点是它只看全局,不看局部。
  • PointNet++ 和 DGCNN (EdgeConv) 引入了图神经网络的思想,通过在点的邻域 (neighborhood) 内聚合特征,来学习局部几何结构,效果更好。

点云损失函数:

当AI生成一个点云 \(S_1\) 时,如何衡量它和“答案”点云 \(S_2\) 的差距?

  1. Chamfer Distance (CD):
\[d_{CD}(S_1, S_2) = \sum_{x \in S_1} \min_{y \in S_2} ||x-y||_2^2 + \sum_{y \in S_2} \min_{x \in S_1} ||x-y||_2^2\]

含义:对于 \(S_1\) 中的每个点,在 \(S_2\) 中找到它最近的邻居,计算距离并求和;反过来再做一遍。它很常用,计算快。

  1. Earth Mover's Distance (EMD):
\[d_{EMD}(S_1, S_2) = \min_{\phi: S_1 \to S_2} \sum_{x \in S_1} ||x - \phi(x)||_2\]

含义:找到一个一对一的完美匹配 $ \phi $(一个“搬运”方案),把 \(S_1\) 的所有点“搬”到 \(S_2\) 的所有点上,使得总“搬运”距离最小。这个损失函数更严格,但也更难计算。


AI生成3D模型 (AI for 3D Generation)

AtlasNet: 生成参数化表面

问题:用PointNet的方式(比如MLP直接输出 \(N \times 3\) 坐标)生成的点云是无结构的,它只是一个“点汤”,不是一个“表面”。

AtlasNet 思想:我们不直接生成点,我们生成参数化函数。

流程:

  • 编码器(比如CNN)将输入(如一张2D图像)编码为一个潜向量 $ z $ (latent shape representation)。
  • 解码器是一个MLP,但它的输入是 $ MLP(z, u, v) $。

\((u, v)\) 是从一个 2D 正方形上采样的 \(N\) 个随机点。

  • MLP学会了将这个2D正方形“扭曲”成3D物体的一个表面片 (patch)。

为了表示复杂物体(比如椅子),它会同时学习 \(K\) 个MLP(比如 $ K=25 $),生成 \(K\) 个表面片,像“地图集 (Atlas)”一样拼成整个物体。

优点:生成的是有结构的表面,而不是点云。


深度隐式函数 (Deep Implicit Functions)

这是目前最主流的高精度生成方法。

核心思想:我们不生成点,也不生成面。我们让神经网络本身成为那个隐式函数 $ f(x, y, z) $。

流程:

  • 解码器是一个MLP:$ f_\theta(p, z) $。
  • 输入:一个潜向量 $ z $(定义了哪个物体,比如“椅子A”),和一个3D查询点 $ p=(x, y, z) $。
  • 输出:一个标量值 $ s $。

两种主流模型:

  1. Occupancy Networks (ONet):
  • MLP的输出 $ s \(是一个**占据概率** (0到1),表示点\) p $ 在物体内部的概率。
  • 表面就是 \(f_\theta(p, z) = 0.5\) 的等值面。
  1. DeepSDF:
  • MLP的输出 \(s\) 是有向距离 (SDF) 值。
  • 表面就是 \(f_\theta(p, z) = 0\) 的等值面。

如何得到网格?

  • 在训练好 \(f_\theta\) 后,我们创建一个高分辨率的3D网格(比如 $ 256^3 $)。
  • 我们把网格上所有的点 \((x_i, y_j, z_k)\) 全部查询一遍 $ f_\theta $,得到一个 \(256^3\) 的SDF值网格。
  • 然后,我们用经典的Marching Cubes算法,从这个SDF网格中提取出 \(f=0\) 的表面,生成一个精细的三角形网格。

优点:

  • 无限分辨率:因为函数 \(f_\theta\) 是连续的,你可以用任意高的分辨率去查询它(比如 $ 1024^3 $)。
  • 内存高效:你存储的只是MLP的权重 $ \theta $,而不是一个巨大的体素网格。
  • 拓扑灵活:可以表示带孔洞的复杂物体。

缺点:

  • 推理(提取网格)很慢!你需要查询 \(256^3\) (1670万) 次MLP。

NeRF: 神经辐射场 (Neural Radiance Fields)

这是2D/3D结合的巅峰之作 (ECCV 2020)。它解决的不是“形状生成”,而是“新视角合成 (Novel View Synthesis)”。

核心思想:我们用一个MLP来隐式地表示一整个场景(包括几何和光照)。

这个MLP是什么? \(F_\theta\)

  • 输入:一个3D坐标 \((x, y, z)\) 和一个2D视角方向 $ (\theta, \phi) $。
  • 输出:这个点在该视角下发出的颜色 $ (R, G, B) $ 和该点的体积密度 $ \sigma $ (Volume Density)。
  • 密度 \(\sigma\) 告诉我们这个点有多“实”。$ \sigma $ 很高就像一块石头,$ \sigma $ 很低就像一团雾气。

如何训练?

  • 输入:一堆在不同视角拍摄的真实照片,以及它们的相机姿态(位置和朝向)。
  • 损失函数:渲染出的像素颜色和真实照片像素颜色的L2损失。

如何渲染 (Rendering)?

  • NeRF使用体积渲染 (Volume Rendering),这是完全可微分的。
  • 从相机出发,沿着一条光线 (ray) \(r(t) = o + td\) 穿过场景。
  • 在这条光线上采样 \(N\) 个点 $ t_1, ..., t_N $。
  • 把这 \(N\) 个点的坐标 \((x_i, y_i, z_i)\) 和视角 \((\theta, \phi)\) 喂给MLP $ F_\theta $,得到 \(N\) 组 $ (c_i, \sigma_i) $(颜色和密度)。
  • 最后,使用体积渲染公式将这 \(N\) 个点的颜色“混合”起来,得到这个像素的最终颜色 $ C $。
\[C(r) = \sum_{i=1}^{N} T_i \cdot \alpha_i \cdot c_i\]

$ \alpha_i = 1 - \exp(-\sigma_i \delta_i) $:第 \(i\) 个点本身的不透明度(它有多“实”)。

$ T_i = \prod_{j=1}^{i-1} (1 - \alpha_j) \(:**透射率**,即有多少光线**穿过**了它前面的所有点(\) 1 \(到\) i-1 $)而没有被挡住。

含义:最终颜色 \(C\) 是所有采样点 \(c_i\) 的加权平均值。权重 \(T_i \alpha_i\) 意味着,一个点要对最终颜色有贡献,它自己必须不透明 (\(\alpha_i\) 高),并且它不能被它前面的点挡住 (\(T_i\) 高)。

结果:

效果惊人,可以生成照片般逼真的新视角,连反光和透明效果都能模拟。

缺点:

  • 训练极慢:训练一个场景要一两天。
  • 渲染极慢:渲染一张图要几十秒(因为每条光线都要查询MLP几百次)。

它是一个隐式表示(MLP)。


3D高斯溅射 (3D Gaussian Splatting - 3DGS)

这是2023年的SOTA,它解决了NeRF的速度问题。

核心思想:NeRF(隐式)太慢了,我们能不能回归显式表示 (Explicit),但又让它像NeRF一样可微分、效果好?

表示法:

不是MLP!它是一个点云的进化版。

场景被表示为几百万个3D高斯球 (Gaussian blobs)。

每个高斯球(点)有以下属性,都是可训练的参数:

  • 位置 \((x, y, z)\)
  • 协方差矩阵($ 3 \times 3 $):决定了这个“球”的形状和旋转(是扁的、长的、还是圆的)。
  • 颜色 \((R, G, B)\)
  • 不透明度 $ \alpha $

如何渲染 (Splatting)?

不是光线追踪! 它用的是光栅化 (Rasterization),类似传统游戏引擎,所以极快!

把所有的3D高斯球,根据相机姿态,投影 (“Splat”) 到2D的图像平面上,变成一堆2D高斯。

从前到后,把这些2D高斯片“画”到屏幕上,根据它们的 \(\alpha\) 值进行混合。

结果:

  • 质量:比NeRF更高。
  • 训练速度:比NeRF快10-100倍(几十分钟搞定)。
  • 渲染速度:实时 (Real-time)!(>100 FPS)。
  • 它同时拥有了显式表示(快)和隐式表示(可微分、高精度)的优点。

结构化表示 (Structured Representations)

动机:

无论是NeRF还是3DGS,它们都把物体看成一个“连续的场”或“一堆高斯球”。

但我们人类理解物体不是这样的。我们知道一个“椅子”是由部件构成的(有腿、有座、有靠背)。

我们希望AI也能有这种结构化的理解。

方法:

  • 部件集合 (Part Sets):把物体表示为一堆3D基元(比如方块、球)的集合。
  • 关系图 (Relationship Graphs):用图来表示部件,节点是部件,边是“连接”关系。
  • 层级 (Hierarchies):用树状结构来表示,就像PartNet数据集那样。
  • StructureNet:这是一个代表性工作,它使用图神经网络 (GNN) 来编码和解码一个层级图。它能学会“椅子通常有4条腿连着一个座位”这种常识。

程序 (Programs):

终极的结构化表示。

AI不生成形状,而是生成一段代码(比如CAD程序)。

比如,生成 create_box(seat), create_cylinder(leg), attach(leg, seat)。

这提供了最好的可控性和可编辑性,但也是最难的。


第16章:多模态基础模型 (Multi-Modal Foundation Models)

在过去,如果你想做一个AI任务,比如给猫狗分类(任务1),你需要一个专门的猫狗数据集(数据域1)去训练一个专门的分类模型(模型1)。如果你还想做一个翻译任务(任务2),你就得另找一个翻译数据集(数据域2)去从头训练一个翻译模型(模型2)。

问题: 这种方法效率极低。每个模型都是“专才”,无法触类旁通,每次有新任务,都意味着高昂的(人力和算力)成本来重新训练。

现在,我们构建“基础模型” (Foundation Models)

一个全新的流程:

大规模多样化数据集 -> 基础模型 -> [任务1, 任务2, 任务3, 任务4]

这就是“新范式”:一个模型,多个任务。

核心思想变了。我们不再为特定任务训练,而是先用一个超大规模、极其多样化的数据集(比如整个互联网的文本和图片)去“预训练”(Pre-train)一个巨型模型。

这个模型学到了什么? 它不再是只懂猫狗的“专才”,它学到的是关于世界的基本知识、语法、逻辑、常识,成为一个“通才”。这个“通才”模型,就是基础模型。

如何使用? 一旦这个昂贵的“预训练”完成,我们就可以在这个“基础”上,通过非常“便宜”的方式去适配(adapt)各种下游任务,比如:

  • Fine-tuning (微调): 用少量特定任务的数据再训练它一下。
  • Zero-shot (零样本): 完全不训练,直接通过“提示”(Prompting)让它解决新问题。
  • Few-shot (少样本): 只给它看几个例子,它就能学会。

例子 - 语言基础模型 (GPT)

Common Crawl+ (网页大数据) -> GPT (模型) -> [数学问题, 符号推理, 知识问答, 翻译]

GPT就是一个语言基础模型。它在“预训练”阶段阅读了几乎整个互联网(Common Crawl),学会了语言的“通识”。因此,同一个GPT模型,你不需要重新训练它,就可以直接(Zero-shot)让它帮你做数学题、写代码、回答历史知识、翻译语言。


基础模型的分类

把模型分成了几类:

  1. Language (语言): 只处理文本,如 BERT, GPT。
  2. Classification (分类): 本课特指图文分类,如 CLIP。
  3. LM + Vision (语言模型+视觉): 能看图聊天的,如 LLaVA, Flamingo。
  4. And More! (其他): 功能更特定的,如 Segment Anything (分割一切), Dalle (文生图)。

Chaining (链式调用): 把不同模型组合起来,如 VisProg。

如何识别基础模型?

  • 通用性/鲁棒性 (General/robust): 能解决很多种任务。
  • 大规模 (Large # params / Large data): 模型参数多,训练数据量大。
  • 自监督 (Self-supervised): 这是关键!它不是靠人工标注的“猫”/“狗”标签学的,而是从数据自身的结构中学习(比如预测下一句话,或图文是否匹配)。

图文“分类”基础模型 - CLIP

CLIP (Contrastive Language-Image Pre-Training)。它的核心功能是:判断任意一张图和任意一段文字“匹不匹配”。

SimCLR 是一种用于纯图像的自监督学习方法。从一批(Batch)图片中,拿一张图(比如,一只猫 \(x\))。对这张图做两次随机数据增强(比如,一次裁剪+变色 \(x_1\),一次旋转+模糊 \(x_2\))。\(x_1\)\(x_2\) 就构成了一个“正样本对”(Positive Pair),因为它们本质上是同一张图。这批数据中的所有其他图片(比如,一张狗 \(y\))都和 \(x_1\) 构成“负样本对”(Negative Pair)。

目标: 训练一个图像编码器(Image Encoder),让它输出的特征向量(features)中,正样本对(\(x_1, x_2\))的特征在“特征空间”中被“拉近”(Pull Together),而负样本对(\(x_1, y\))的特征被“推开”(Push Apart)

结果: 通过这种“对比学习”(Contrastive Learning),模型不需要任何人工标签,就学会了识别出“猫”的本质特征。它知道 \(x_1\)\(x_2\) 都是猫,而 \(y\) 不是。

希望: 我们希望这个学到的特征空间是通用的,所有关于“猫”的图(照片、素描)都会聚集在一起。


CLIP 的核心思想 (SimCLR -> CLIP)

SimCLR 是“图-图”对比。CLIP 的天才之处在于,它把对比的一方换成了文本。

SimCLR 有两个图像编码器 (Image Encoder)。

CLIP 有一个图像编码器 (Image Encoder) 和一个文本编码器 (Text Encoder)。

目标变了: 我们不再“拉近”两张相似的图,而是“拉近”一张图片(比如,猫的图片)和描述它的文字(比如,“a cute fluffy cat”)。


CLIP 的训练目标 (数学公式)

场景: 我们有一个大小为 \(N\) 的 Batch,包含 \(N\) 个 (图像, 文本) 配对。

  • \(N\) 个图像特征向量:\(u_1, u_2, ..., u_n\)
  • \(N\) 个文本特征向量:\(v_1, v_2, ..., v_n\)

假设 (\(u_i, v_i\)) 是正确的配对(比如 \(u_1\) 是猫的图, \(v_1\) 是 "a photo of a cat")。

目标: 我们希望 \(u_i\)\(v_i\) 的相似度(用点积 \(u_i \cdot v_i\) 表示)尽可能高,而 \(u_i\) 和所有其他 \(v_j\) (其中 \(j \neq i\)) 的相似度尽可能低。

\[\sum_{i=1}^{n}-log(\frac{e^{(u_{i} \cdot v_{i})}}{\sum_{j=1}^{n}e^{(u_{i} \cdot v_{j})}})\]

这个公式叫 InfoNCE Loss,它本质上就是一个交叉熵损失 (Cross-Entropy Loss)。

我们来看 \(\frac{e^{(u_{i} \cdot v_{i})}}{\sum_{j=1}^{n}e^{(u_{i} \cdot v_{j})}}\) 这一部分,它是一个 Softmax!

  • 分母 \(\sum_{j=1}^{n}e^{(u_{i} \cdot v_{j})}\):计算图像 \(u_i\) 与Batch中所有文本 \(v_j\) 的相似度,并用 \(exp\) 放大。
  • 分子 \(e^{(u_{i} \cdot v_{i})}\):计算图像 \(u_i\) 与其对应的正确文本 \(v_i\) 的相似度。
  • 整个 Softmax: 它计算的是“给定图像 \(u_i\),模型认为 \(v_i\) 是正确文本的概率”。

-log(...): 我们希望这个概率接近1,而 \(-log(1) = 0\)。所以,这个损失函数的目标就是最大化模型预测正确的概率。

完整公式只是把两个方向的Loss加起来:

  • Image-to-Text Loss (如上): 对每个图像,在所有文本中找到正确的那个。
  • Text-to-Image Loss (对称的): 对每个文本,在所有图像中找到正确的那个。

Total Loss = Loss(Image->Text) + Loss(Text->Image)


CLIP 的训练数据和过程

数据: CLIP 不使用昂贵的人工标注数据。它从互联网上爬取了4亿 (400M) 个 (图片, alt-text) 对。alt-text就是网页上给图片加的“替换文本”(比如维基百科上的图片描述)。这是“免费”的、海量的、自监督的数据。

过程: 训练过程就是不断地算一个 \(N \times N\) 的相似度矩阵,然后用损失函数强迫这个矩阵的对角线(即正确的(图,文)配对)的值尽可能高,而非对角线的值尽可能低。

如何“使用”CLIP?(Zero-Shot 登场)

  • 老办法: 像 SimCLR 一样,把 CLIP 的图像编码器当特征提取器,在后面接一个小的线性分类器(linear probe)去 fine-tune。效果很好,在很多数据集上SOTA (State-of-the-art)。
  • 新办法: 语言模型 (LLM) 的真正威力在于 Zero-Shot (零样本)。比如,你给LLM一个Prompt:"The movie review 'I hated the movie' is ___",它就能直接预测出 "negative",而不需要为“情感分析”这个任务做任何 fine-tuning。

CLIP 怎么实现图像分类的 Zero-Shot 呢?

CLIP 的 Zero-Shot 分类器

这是 CLIP 最“天才”的地方。

我们不训练新的分类器,我们用文本编码器 (Text Encoder) 本身来充当分类器!

假设你想做一个能分“飞机 (plane)”、“狗 (dog)”、“鸟 (bird)”的分类器:

  • 拿一张新的待测图片(比如一只狗),把它喂给图像编码器 (Image Encoder),得到一个特征向量 \(u_{dog}\)
  • 把你的所有候选类别名字("plane", "dog", "bird")当作文本,分别喂给文本编码器 (Text Encoder),得到三个特征向量:\(v_{plane}\), \(v_{dog}\), \(v_{bird}\)

计算相似度:

  • \(u_{dog} \cdot v_{plane}\) (比如得到 0.13)
  • \(u_{dog} \cdot v_{dog}\) (比如得到 0.27)
  • \(u_{dog} \cdot v_{bird}\) (比如得到 0.09)

选分最高的! 0.27 最高,对应 "dog",所以模型预测这张图是 "dog"。

这为什么是 Zero-Shot? 因为这个过程没有用到任何“飞机/狗/鸟”的训练图片!你不需要为这个新任务训练任何参数。你只是在预训练好的特征空间里,比较“这张图的概念”和“‘狗’这个词的概念”哪个更近。

“开放词汇” (Open-vocabulary): 这意味着你可以分类任何你能用文字描述的东西,你的类别不再局限于训练时的1000类。


提示工程 (Prompt Engineering)

直接用 "dog" 这个词,效果不是最好。因为 CLIP 训练用的是句子(alt-text)。

改进1: 使用“提示模板”(Prompt Template),比如把 "dog" 改成 "A photo of a dog" (一张狗的照片)。这更符合训练数据的形态。这个小技巧在 ImageNet 上提升了 1.3% 的准确率。

改进2: 一张图可能是素描、卡通,不一定是"photo"。

解决方案: “提示集成”(Prompt Ensemble)。我们创建很多个模板(比如 "A photo of a [category]", "A drawing of a [category]", "A sketch of a [category]"...)。

然后,我们把一个类别(比如 "dog")的所有提示("A photo of a dog", "A drawing of a dog"...)的文本向量取平均,得到一个更鲁棒的、代表“狗”这个抽象概念的平均向量 (Mean "Dog" vector)。这又带来了 5% 的提升!


CLIP 的惊人成果
  • ResNet101 (监督学习): 在 ImageNet (128万张带标签的图片) 上专门训练,准确率 76.2%。
  • CLIP (Zero-Shot): 没有在 ImageNet 上训练过一张图,仅凭"A photo of a..."的 Zero-Shot 方法,准确率 76.2%。

结论: 自监督的威力第一次追平了全监督。

对比 ResNet101 和 CLIP 在各种“刁钻”数据集上的表现(比如 ObjectNet - 奇怪角度, ImageNet Sketch - 素描, ImageNet Adversarial - 对抗样本)。

ResNet 的问题: ResNet101 在 ImageNet 上是“死记硬背”的,它只学会了“ImageNet 风格的照片”。一旦换成素描 (Sketch),准确率从 76.2% 暴跌到 25.2%。

但是 CLIP 的准确率非常稳健 (Robust)!在素描 (Sketch) 上依然有 60.2%!

为什么 CLIP 这么强?因为它预训练的4亿张图来自互联网,极其多样化。它早就见过照片、素描、卡通、油画...它学的不是“照片狗”,而是“狗”这个抽象概念。这才是“基础模型”的真正价值——泛化能力 (Generalization)。


CLIP 为什么这么好?

答案: 规模 (Scale)!

模型规模: CLIP (307M) 比 ResNet (44.5M) 大得多。

数据规模: CLIP (400M) 的训练数据比 ImageNet (1.28M) 多了300多倍。


CLIP 的改进与局限

CoCa (Contrastive Captioners)

CoCa 是 CLIP 的一个改进版,它在 CLIP 的基础上增加了一个“生成”任务。

CLIP 的问题: CLIP 只有“对比损失”(Contrastive Loss),它只判断“匹不匹配”,它并不会生成文本。

CoCa 的改进: CoCa 同时做两个任务来训练:

  • 对比损失 (Contrastive Loss): 和 CLIP 一样,用 [CLS] 向量(代表全局特征)去做图文“拉近/推开”。
  • 字幕损失 (Captioning Loss): 增加了一个多模态文本解码器 (Decoder),强迫它去逐字生成 (predict) 对应的文本(比如 "two dogs running in a field")。
  • 结果: 这种“对比”+“生成”的混合训练,让 CoCa 成了新的SOTA,在 ImageNet 上达到了 91.0% 的准确率。

CLIP 的优缺点分析

CLIP 的优点 (总结)

  • 高效 (Efficient): 计算相似度(点积)非常快。
  • 开放词汇 (Open-vocabulary): Zero-shot 能力,能分类任何你能描述的东西。
  • 可链式调用 (Chained): 能和其他模型组合(后面会讲)。
  1. CLIP 的缺点 1 - 组合性 (Compositionality)

这是 CLIP 的致命弱点。

CLIP 分不清 "a mug in some grass" (草地上的杯子) 和 "some grass in a mug" (杯子里的草)。

原因: CLIP 的对比学习像是一个“词袋模型”(Bag-of-words)。它只关心“杯子”、“草”、“在”这几个概念是否同时出现,它不理解它们之间的空间关系或语法顺序。

为什么会这样?因为在它训练的 32K 大小的 Batch 里,它可能见过“杯子”和“狗”是负样本,所以它知道杯子不是狗。但它极不可能在同一个 Batch 里同时看到 "a mug in some grass" 和 "some grass in a mug" 这两个“困难负样本”(Hard Negatives)。它没有被(数据)“教会”这种细微的差别。

这个问题(组合性)是目前多模态研究的一个巨大障碍。

解决组合性?(困难重重)

方法: 专门找一些“困难负样本”来 fine-tune 模型。比如给它看“马在吃草”的图,告诉它 "horse eating grass" 是对的,但 "grass eating horse" (草在吃马) 是错的。

新问题: 这会引入“困难正样本”(Hard Positives) 问题。 "A black cat and a brown dog" (黑猫和棕狗) 和 "A brown dog and a black cat" (棕狗和黑猫) 意思是完全一样的! 但如果你不小心把它们当成了一正一负,模型就彻底晕了。

  1. 缺点2: 监督信号太弱。一张图的 alt-text 可能是 "living room" (客厅)。模型只知道“客厅”和这张图匹配,但它不知道图中哪个是“沙发 (couch)”,哪个是“植物 (house plants)”。

解决方案: 用更精细的“区域字幕”(Region Captions),即 (Bounding Box, Text) 对来训练。

  1. 缺点3: 数据有毒。从互联网爬 4 亿数据,你无法保证数据的“干净”。里面必然会混入大量的偏见、色情、暴力和非法内容 (CSAM)。数据过滤和安全是重中之重。

“看图聊天”基础模型 - LLaVA & Flamingo

我们进入第二大类:LM + Vision (语言模型+视觉)。这类模型的目标是像人一样,能看图聊天、分析、推理。


LLaVA 的动机与历史

动机: LLM (语言模型) 已经是“通才”了(能数学、能推理)。我们能不能让 LLM “长出眼睛”?

历史: 以前有 VILBERT 这样的模型,它有两个独立的 Transformer(一个处理图,一个处理文),中间通过“共同注意力”(Co-Attention) 互相“交流”。

问题: VILBERT 是“旧范式”,它需要为每一个下游任务(比如看图问答 VQA)进行复杂的 fine-tuning,不是通用的“基础模型”。


LLaVA 的核心思想

LLaVA 的思想极其简洁:把图片当作“视觉词汇” (visual words),和文本词汇“拼”在一起,一起喂给 LLM。

一个标准 LLM (Transformer) 的工作是“自回归预测” (Autoregressive)。输入 ["Cats", "are", "so"],它会预测下一个词是 "cute"。

LLaVA 的做法是:在文本前面“插入”图像特征。输入序列变成 [V1, V2, ... VN, "Cats", "are", "so"],然后让 LLM 接着预测 "cute"。

关键问题: V1, V2...VN 这些“视觉词汇”从哪来?

LLaVA 如何获取“视觉词汇”

答案还是 CLIP!我们用 CLIP 的图像编码器 (Vision Transformer, ViT) 来提取图像特征。

CLIP 的 ViT 是怎么工作的?它先把图片切成很多“小块”(Patches),然后把每个小块都变成一个“Patch 向量/Token”。

CLIP 为了做“对比学习”,最后只用了那个特殊的 [CLS] 向量(它代表了整张图的全局特征)。

LLaVA 的洞察: CLIP 扔掉的那些“Patch 向量”其实包含了丰富的空间信息(比如,哪个小块是猫头,哪个是猫尾)。但这些向量可能没被充分训练。

解决方案: 我们不使用最后一层,而是使用 ViT 的倒数第二层 (Penultimate Layer) 的 Patch 向量。实践证明,这些向量保留了最丰富、最适合 LLM 理解的图文和空间信息。


LLaVA 的整体架构与训练

架构:极其简单,像“搭乐高”。

  • 一个预训练好并冻结 (Frozen) 的 CLIP 图像编码器(用来提取 Patch 向量)。
  • 一个预训练好并冻结 (Frozen) 的 LLM(比如 LLaMA,用来生成文本)。
  • 一个新加的、可训练的“桥梁”:线性层 (Linear Layer)。

CLIP 输出的特征(比如 768 维)和 LLM 输入的特征(比如 4096 维)“语言不通”。这个“线性层”的唯一作用就是做一个“翻译”,把 CLIP 的视觉特征空间“投影”到 LLM 的词汇特征空间。

训练(两阶段):

  • 阶段1 (预训练): 冻结 CLIP 和 LLM,只训练中间那个小小的“线性层”。用几十万张 (图, 文) 数据,快速教会 LLM “看懂”视觉信号。
  • 阶段2 (Fine-tuning): 冻结 CLIP,同时训练 LLM 和“线性层”。用更高质量的、对话式的数据,教会 LLM 如何谈论和推理它所看到的内容。

Flamingo (另一种“看图聊天”模型)

Flamingo 是 DeepMind 提出的另一种架构,它不把图像“塞入”LLM 的输入,而是“注入”LLM 的侧面。

LLaVA 的问题: LLaVA 把一大堆(比如 256 个)视觉 Token 全堆在输入的最前面,这可能让 LLM“消化不良”。

Flamingo 的架构:

  • 冻结的 Vision Encoder 和 冻结的 LLM。

新加的 (紫色) 可训练模块:

  • Perceiver Resampler: 这是一个“瓶颈”层。它把 ViT 输出的大量(比如 256 个)视觉 Token,“压缩”成少量固定(比如 64 个)的视觉 Token。
  • Gated XATTN-DENSE: 这是 Flamingo 的核心。它在冻结的 LLM 的每一层之间都“插入”了一个新的“门控交叉注意力”层。

Gated XATTN 如何工作:

  • 在 LLM 正常处理文本时,这个新层会“旁路”进来。
  • 它让文本特征 (Y) 去“看一眼”视觉特征 (X) (通过 Cross-Attention),抓取当前文本最相关的视觉信息。
  • 然后通过一个“门控”(Gating) 机制,把这些视觉信息添加回文本流中。

精妙之处=: 这个“门控”参数 alpha 在训练开始时初始化为 0。这意味着训练刚开始时,y = y + 0 * ...,这些新层完全不起作用,LLM 还是那个 LLM,训练非常稳定。随着训练进行,alpha 慢慢变大,模型才逐渐学会“打开大门”,让视觉信息“流”进来。


Flamingo 的数据与训练

Flamingo 被设计用来处理“交错 (interleaved)”的图文数据(比如网页、PDF)。

数据: 它的输入是 文本A 文本B 文本C...

注意力掩码: 它是“自回归”的,意味着模型在预测一个词时,只能看到它前面出现过的图文。比如,在预测“dignified”时,它可以同时看到 Image 1 和 Image 2;但在预测“grass”时,它只能看到 Image 1。


Flamingo 的强大能力

结果: Flamingo 可以实现复杂的“多轮对话”、“多图比较”和“OCR”。

杀手锏: 上下文学习 (In-context Learning)。

因为 Flamingo 的核心是个 LLM,它继承了 LLM 的“上下文学习”能力,但现在图片也可以成为上下文!

例子:

你给它看:[图A (chinchilla)], "This is a chinchilla."

再给它看:[图B (shiba)], "This is a shiba."

最后你问:[图C (flamingo)], "This is"

模型会从前两个例子中“学习”到你要的“模式”,然后回答:"a flamingo."

它能用这种方式“零样本”地学会看图问答、OCR、甚至看图做数学!

Flamingo (80B) 在很多任务上,用 Few-Shot 就打败了之前为该任务专门 Fine-tune 的SOTA模型。


Molmo (开源的希望)

现状: 现在的排行榜 (Leaderboard) 上,最顶尖的 (GPT-4o, Gemini 1.5) 都是闭源 (API Only) 的。次一等的“开源”模型 (Qwen-VL) 大多是“蒸馏”来的——它们是靠调用 GPT-4 的 API 来训练的,并非真正独立。

Molmo 的突破: Molmo (来自 Allen Institute for AI) 是一个完全开源(代码、数据、权重全开源)的模型,它在性能上却追平甚至超越了顶尖的闭源模型!

Molmo-72B 击败了 Gemini 1.5 Pro。

Molmo-7B (一个7B的小模型) 竟然击败了 GPT-4V!

Molmo 的秘诀1 - "Pointing":

Molmo 不仅输出文本,它还被训练输出了“指向坐标”

当你问它“数一下船”,它不仅回答 "35",它在内部逻辑中是真的一个一个“点”过去的。这种“视觉接地”(Visual Grounding) 能力让它的推理和计数极其准确。

Molmo 的秘诀2 - "Data Quality":

数据质量 > 数据数量 (Quality over quantity)。

LLaMA-3.1V 用了 60亿 图文对,而 Molmo 只用了 7亿。为什么 Molmo 赢了?

答案: 互联网数据是“偶然”的、质量很差 (比如 "pink, japan")。Molmo 用的是“有意”收集的超高密度的 PixMo 数据集。

数据收集技巧: 让人写那么长的文字太难了。AI2 的方法是:让标注员“说”90秒钟,然后用“语音转文字”(ASR) 来自动生成这些高质量的长描述。

Molmo 架构: 它的架构类似 LLaVA,但它的 LLM 词汇表里被扩展,加入了特殊 point Token,所以它能生成坐标。

Molmo 的“指向”能力是实现“具身智能”(Embodied AI)(比如机器人)的关键一步。


“万物可割”与“模型串联”

Segment Anything Model (SAM)

这是一个“分割 (Segmentation)”任务的基础模型。

旧范式: Mask R-CNN 这样的模型,只能在它训练过的 80 个类别(比如 COCO 数据集)上进行分割。

新范式: SAM 可以分割“任何东西”(Anything)

如何实现: 它如何知道你想要什么?靠“提示”(Prompt)


SAM 的架构 (天才般的设计)

架构:

  • Image Encoder (图像编码器): 一个超重量级的 ViT。它只对每张图运行一次,生成一个包含所有信息的“特征图”。这是最“贵”的一步。
  • Prompt Encoder (提示编码器): 一个超轻量级的编码器。它把你的“提示”(可以是一个点、一个框、或文字)编码成一个特征向量。
  • Mask Decoder (掩码解码器): 一个超轻量级的解码器。它把“图像特征”和“提示特征”一融合,瞬间(毫秒级)就能算出你想要的那个物体的“掩码”(Mask)。

为什么这么设计? 为了交互性!“贵”的步骤(算图像)只做一次,“便宜”的步骤(根据你的鼠标点位算掩码)可以实时、反复地做。

歧义性: 当你只点了一个点时,你可能想要“衬衫”、“这个人”或“这个人的上半身”。SAM 的做法是一次性输出 3 个可能的、嵌套的掩码(小、中、大),让你自己选。


SAM 的数据与效果

数据: SAM 团队自己构建了一个10亿 (1 Billion) 掩码的数据集 (SA-1B)。他们先做出模型雏形,然后让标注员用这个模型去“点点点”来标注数据,极大提高了效率。

效果: SAM 真正做到了“分割万物”,泛化能力极强。


链式调用 1 - CuPL

我们开始把不同的基础模型“链接”起来使用。

问题: CLIP 这种模型不认识“稀有概念”,比如 "platypus" (鸭嘴兽)。

CuPL 方案:

第一步: 问 LLM (GPT-3):“鸭嘴兽长啥样?”

LLM 回答: “它有像海狸的尾巴和像鸭子的嘴。”

第二步: 把 LLM 的回答(而不是"platypus"这个词)当作“提示”喂给 CLIP。

CLIP 可能没见过“鸭嘴兽”,但它一定见过“海狸尾巴”和“鸭子嘴”!通过 LLM 的“知识”+ CLIP 的“视觉”,我们解决了稀有概念的分类问题。


链式调用 2 - VisProg (Visual Programming)

这是“链式调用”的终极形态。

问题: "Are there 3 people in the boat?" (船上有3个人吗?) 这个问题太复杂了,它需要 1.定位船 2.定位人 3.判断人在船里 4.计数 5.比较是否=3。

旧范式: 专门训练一个 VQA 模型来回答这种问题。

VisProg 方案:

  1. 把复杂问题 "Are there 3 people in the boat?" 丢给 LLM。
  2. 要求 LLM 不要回答问题,而是“写一段 Python 代码” 来解决这个问题。
  3. LLM 会生成一段“视觉程序” (VisProg),它会调用我们预先定义好的“API库”(比如 LOC - 定位, FILTER - 过滤, COUNT - 计数):
BOAT = LOC(image, "boat")
PEOPLE = LOC(image, "people")
PEOPLE_IN_BOAT = FILTER(PEOPLE, BOAT, "in")
COUNT_PEOPLE = COUNT(PEOPLE_IN_BOAT)
RESULT = COMPARE(COUNT_PEOPLE, "3")
  1. 我们真正执行这段代码,代码中的 LOC 等函数会去调用 SAM 或 CLIP 这样的“专家”模型。
  2. 代码的最终 RESULT 就是问题的答案。

LLM 充当了“大脑”或“总指挥”,它负责推理和拆解任务。而 CLIP/SAM 充当了“眼睛”或“专家”,负责执行具体的视觉感知。这种“链式调用”让我们可以组合简单的模型,去解决无限复杂的、全新的任务,而不需要任何额外的训练!


第17章:机器人学习 (Robot Learning)

在进入“机器人学习”这个新主题之前,我们先快速回顾一下已经学过的两种主要的机器学习范式。

  1. 监督学习 (Supervised Learning)

它是什么? 这是我们最熟悉的机器学习。你给机器一堆“问题”和“标准答案”,机器的任务是学习从问题到答案的映射关系。

  • 数据形式: (x, y)。
  • x 是数据(比如一张图片)。
  • y 是标签(比如图片对应的词“猫”)。
  • 目标 (Goal): 学习一个函数 \(f\),使得 \(f(x) \approx y\)

为什么? 它的目标非常明确,就是“预测”。比如分类、回归、目标检测等。

  1. 自监督学习 (Self-Supervised Learning)

它是什么?当我们有海量数据 x,但没有标签 y 时使用。它的核心思想是“从数据自身中创造监督信号”。

  • 数据形式: x (只有数据,没有标签)。
  • 目标 (Goal): 学习数据中潜在的、有意义的结构或特征。
  • 例子:自编码器 (Autoencoders)

这是一个非常经典的自监督学习例子。它由两部分组成:

  • 编码器 (Encoder): 负责将输入数据 x 压缩成一个低维度的特征向量 z。(\(z = \text{Encoder}(x)\))
  • 解码器 (Decoder): 负责从这个压缩的特征 z 中重建出原始数据,得到 \(\hat{x}\)。(\(\hat{x} = \text{Decoder}(z)\))

它如何学习? 我们并没有“正确”的 z 作为标签。但我们有“正确”的 x!所以,我们把 x 同时作为输入和“标准答案”。

数学公式 (损失函数): 我们强迫模型让重建的 \(\hat{x}\) 和原始的 \(x\) 尽可能接近。最常用的方法是计算它们之间的L2损失 (L2 Loss):

\[\text{Loss} = ||x - \hat{x}||^2\]

这个公式计算了原始图像和重建图像之间每个像素差异的平方和。损失越小,说明重建得越像。

为什么这么做? 为了能成功重建 x,模型必须学会将 x 中所有重要的信息都压缩到 z 里。因此,z 就成了一个非常有价值的、浓缩了 x 精华的特征表示。


机器人学习

它是什么?这是今天要学的新范式。它研究的不是一个被动的“预测”问题,而是一个主动的“决策”问题。

核心循环 (The Loop):

  1. 智能体 (Agent)(比如机器人)观察环境 (Environment)。
  2. 智能体决定并执行一个动作 (Action)。
  3. 环境因为这个动作发生了改变,并反馈给智能体一个奖励 (Reward)(或惩罚)。
  4. 智能体进入新状态,重复这个循环。

目标 (Goal): 学习一个策略 (Policy),告诉智能体在各种情况下应该采取什么动作,以最大化它在未来能获得的累积奖励 (Cumulative Reward)。

为什么它很特别? 机器人学习的核心在于“交互”和“长期目标”,而不是一次性的预测。


问题定义 (Problem Formulation)

核心框架:马尔可夫决策过程 (MDP)

马尔可夫决策过程 (Markov Decision Process, MDP)。这是描述机器人学习问题的标准数学语言。我们来拆解一下:

  • \(s_t\) (State / 状态): 在时间点 \(t\) 时,对世界的一个完整描述。比如棋盘的布局、机器人的关节角度、游戏画面的像素。
  • \(a_t\) (Action / 动作): 智能体在状态 \(s_t\) 时,决定执行的动作。比如走一步棋、给关节施加一个力矩、按游戏手柄的“向左”键。
  • \(r_t\) (Reward / 奖励): 智能体在执行 \(a_t\) 后,环境给予的即时反馈。奖励是定义任务目标的唯一方式。
  • \(r_{t+1}\) 指的是在 \(s_t\) 做出 \(a_t\) 后,转移到 \(s_{t+1}\) 时获得的奖励。
  • \(g\) (Goal / 目标): 任务的最终目标,通常由人类给定。奖励 \(r_t\) 就是围绕这个目标 \(g\) 来设计的。

例子

我们用这个 \((s, a, r)\) 框架来看看各种问题:

  1. 平衡车 (Cart-Pole):
  • 目标: 让杆子保持竖直。
  • State (\(s\)): 杆子角度 \(\theta\)、角速度、车的位置 \(x\)、车速。
  • Action (\(a\)): 对小车施加的水平力 \(F\)(向左或向右)。
  • Reward (\(r\)): 只要杆子还立着,每一步都给 +1 的奖励。
  1. 机器人行走 (Locomotion):
  • 目标: 向前走。
  • State (\(s\)): 所有关节的角度、速度、位置。
  • Action (\(a\)): 施加到每个关节上的力矩 (Torque)。
  • Reward (\(r\)): 保持站立 (+1) + 向前移动的距离。
  1. 雅达利游戏 (Atari Games):
  • 目标: 获得最高分。
  • State (\(s\)): 游戏画面的原始像素。这是一个高维度的状态!
  • Action (\(a\)): 游戏手柄的控制(上、下、左、右、开火)。
  • Reward (\(r\)): 游戏得分的变化。
  1. 围棋 (Go):
  • 目标: 赢棋。
  • State (\(s\)): 棋盘上所有棋子的位置。
  • Action (\(a\)): 下一步棋的位置。
  • Reward (\(r\)): 这是一个“稀疏奖励”(Sparse Reward) 的经典例子。 在游戏过程中 \(r=0\),只有在游戏结束时,赢了 \(r=+1\),输了 \(r=-1\)
  1. 聊天机器人 (Chatbot):
  • 目标: 成为一个好的对话伙伴。
  • State (\(s\)): 整个对话历史。
  • Action (\(a\)): 回答的下一句话。
  • Reward (\(r\)): 人类的反馈 (Human Feedback)。比如用户点的“赞”或“踩”。这就是RLHF (从人类反馈中强化学习) 的基础。
  1. 叠衣服 (Cloth Folding):
  • 目标: 把衣服叠好。
  • State (\(s\)): 摄像机拍到的衣服和机械臂的图像。
  • Action (\(a\)): 机械臂末端执行器的运动。
  • Reward (\(r\)): 比如人类评估(1=叠好了, 0=没有)。

机器人感知 (Robot Perception)

它是什么?感知是智能体如何获取状态 \(s_t\) 的过程。

为什么它很难?真实世界是杂乱、非结构化的。机器人面临:

  • 不完整的知识(比如物体被遮挡)。
  • 不完美的动作(比如机械臂有误差)。
  • 动态的环境(比如有人走过)。

传感器 (Sensors):机器人使用多模态 (multimodal) 传感器来理解世界,就像人类的五官。例如:

  • 相机 (Cameras): 视觉信息(RGB图像)。
  • 激光雷达 (LiDAR) / 深度相机: 3D空间和距离信息。
  • 力/扭矩传感器 (Force/Torque Sensor): 触觉,感知交互的力。
  • 惯性测量单元 (IMU): 姿态和运动(类似你手机里的陀螺仪)。

计算机视觉 (CV) vs. 机器人视觉 (Robot Vision)

  • 传统CV: 通常是被动 (Passive) 的。比如给你一张图,问你“图里有什么?”
  • 机器人视觉: 是主动 (Active) 和具身 (Embodied) 的。
  1. 具身 (Embodied): 机器人拥有实体。它的动作(比如移动)会直接改变它接下里看到的东西(它的感知输入)。
  2. 主动 (Active): 机器人为了完成任务而主动去感知。它会自问:“我应该往哪里看才能找到我需要的杯子?”
  3. 情境 (Situated): 机器人的感知是为了“此时此地”的动作服务的,而不是为了抽象的描述。

感知-动作循环 (Perception-Action Loop)

核心挑战: 机器人学习的关键挑战就是“闭合这个循环”。

(感知 \(s_t\)) \(\to\) (决策 \(a_t\)) \(\to\) (动作改变环境) \(\to\) (感知新状态 \(s_{t+1}\)) \(\to\) ...

这个循环必须快速、鲁棒地运行。


强化学习 (Reinforcement Learning, RL)

它是什么?RL 是一种算法,用来训练智能体通过试错 (trial and error) 与环境交互,学习如何最大化累积奖励。这是解决前面 \((s, a, r)\) 问题的核心方法。

RL vs. 监督学习

为什么 RL 是一个完全不同的问题?

  • 随机性 (Stochasticity): 环境是随机的。在状态 \(s\) 执行动作 \(a\),下次可能得到 \(s'\)\(r'\),下下次可能得到 \(s''\)\(r''\)
  • 信用分配 (Credit Assignment): 奖励可能是延迟的。比如在围棋中(P14),你赢了 (+1 奖励),但这个奖励是最后才给的。RL 必须搞清楚,到底是哪一步棋(可能是几十步前的)导致了最终的胜利。这就是时间信用分配问题。
  • 不可微 (Nondifferentiable): 你不能对物理世界求导! 在监督学习中,我们可以通过反向传播计算 \(\text{Loss}\) 对网络权重的梯度。但在 RL 中,奖励 \(r\) 是由物理世界(或一个黑盒模拟器)给出的,你无法计算 \(\frac{dr_t}{da_t}\)
  • 非平稳 (Nonstationary): 智能体看到的数据分布是非平稳的,因为它取决于智能体自己的行为。如果一个智能体学会了只向左走,它就永远收集不到向右走的数据。这与监督学习中数据集 \(D\) 是固定不变的(i.i.d. 假设)完全不同。
  1. 玩雅达利游戏 (DQN)

DQN (Deep Q-Network) 是 RL 的一个里程碑式算法。

核心思想:Q-Learning (Q学习)

我们定义一个函数 \(Q(s, a)\),它被称为 Q-Value。

  • \(Q(s, a)\) 的含义: “如果我在状态 \(s\) 执行动作 \(a\),并且之后都用最优策略玩下去,我未来能获得的累积奖励总和是多少?”
  • \(Q(s, a)\) 衡量了在状态 \(s\) 时,动作 \(a\) 有多“好”。

如何用DQN解决?

我们用一个深度卷积神经网络 (CNN) 来近似这个 \(Q\) 函数。

  • 输入: 状态 \(s\)
  • 输出: 一个向量,包含了所有可能动作的Q值。例如:\([Q(s, \text{上}), Q(s, \text{下}), Q(s, \text{左}), Q(s, \text{右})]\)

如何决策 (Policy)? 当处于状态 \(s\) 时,智能体只需计算一遍网络,然后选择 Q-Value 最高的那个动作:

\[a^* = \arg\max_a Q(s, a; \theta)\]

(\(\theta\) 代表神经网络的权重)。

如何学习? 通过一种叫做“时序差分 (Temporal Difference, TD)”的方法(原理是让 \(Q(s_t, a_t)\) 逼近 \(r_t + \gamma \max_{a'} Q(s_{t+1}, a')\)\(\gamma\) 是折扣因子)。

  1. 玩围棋 (AlphaGo)

AlphaGo 系列展示了 RL 的强大威力,以及它的进化路径:

  • AlphaGo (2016): 监督学习(模仿人类)+ 强化学习 + 蒙特卡洛树搜索 (MCTS)。
  • AlphaGo Zero (2017): 不再需要人类数据! 完全从零开始,通过自我对弈 (self-play) 进行强化学习。
  • AlphaZero (2018): 推广到国际象棋和将棋。
  • MuZero (2019): 更进一步!它甚至不需要被告知游戏规则! 它在学习下棋的同时,自己学习了一个环境模型(即游戏规则)。这引出了下一个主题。

"模型无关" RL 的问题

像 DQN 这样的算法被称为 模型无关 (Model-Free) RL。它直接学习一个策略 (Policy) 或一个价值函数 (Q-Value),但它并不理解“世界是如何运转的”。它不知道“如果我推这个积木,它会倒”。

  1. 最大问题:样本效率极低 (Sample Inefficiency)。
  • 它需要海量的“试错”数据。AlphaGo Zero 训练了40天,相当于几千年的棋局。
  • 这在模拟器里可行,但在物理世界中是灾难性的。你不可能让一个真实机器人“试错”几百万次(它会坏掉)。
  1. 其他问题: 安全性、可解释性差。

对比: 人类为什么学习快?因为我们脑中有一个“物理模型”。我们可以在脑中“预演”动作的后果,而不需要真的去做。


模型学习 & 基于模型的规划

核心思想: 为了解决 Model-Free 的问题,我们引入基于模型 (Model-Based)RL。

步骤:

  1. 模型学习 (Model Learning): 智能体先与环境交互,收集数据 \((s_t, a_t, s_{t+1})\)。然后,学习一个世界模型 (World Model),也叫动力学模型 (Dynamics Model)。
  2. 数学表示: 这个模型的目标是预测 \(P(s_{t+1} | s_t, a_t)\)。即:“给定当前状态 \(s_t\) 和动作 \(a_t\),预测下一个状态 \(s_{t+1}\) 会是什么?”
  3. 规划 (Planning): 一旦我们有了一个(学到的)模型,我们就可以在“想象中” (in the model) 进行快速、安全的“试错”。智能体可以在这个内部模拟器中预演几千种动作序列,然后选择那个预计奖励最高的序列,最后才在真实世界中执行第一步动作。

状态 \(s_t\) 应该如何表示?

这是 Model-Based RL 的核心问题。

  1. 像素动力学 (Pixel Dynamics):
  • 状态 \(s\): 原始图像 \(I_t\)
  • 模型: 学习一个 CNN+RNN 模型,输入 (当前图像 \(I_t\), 动作 \(a_t\)),输出 (预测的下一帧图像 \(\hat{I}_{t+1}\))。

优点: 非常通用。

缺点: 预测整张图像非常困难且没必要(比如你不需要预测背景天空的变化)。

  1. 关键点动力学 (Keypoint Dynamics):
  • 状态 \(s\): 图像中几个关键点的坐标。
  • 模型: 学习一个模型,输入 (当前关键点 \(k_t\), 动作 \(a_t\)),输出 (预测的下一时刻关键点 \(\hat{k}_{t+1}\))。

优点: 更轻量、更高效。状态空间从 (64x64x3) 像素降低到 (10x2) 坐标。

  1. 粒子动力学 (Particle Dynamics):
  • 状态 \(s\): 一系列粒子 (particles) 的集合,用来表示物体(特别是可形变物体如面团,或颗粒物如米粒)。
  • 模型: 通常使用图神经网络 (Graph Neural Network, GNN)。它把粒子看作图的节点,学习粒子间的相互作用力。输入 (当前粒子状态 \(S_t\), 动作 \(a_t\)),输出 (预测的下一时刻粒子状态 \(\hat{S}_{t+1}\))。

优点: 能模拟复杂的物理交互,如形变、堆叠。


模仿学习 (Imitation Learning, IL)

动机: RL 的“试错”太难了,尤其是奖励设计 (Reward Design) 很复杂。一个更简单的方法是:我们能不能直接“向专家学习”

核心思想: 我们有一堆“专家演示” (Expert Demonstrations) 数据集 \(D = \{(s, a)\}\),由人类专家(比如遥控机器人)提供。我们希望机器人能“模仿”专家的行为。


方法1: 行为克隆 (Behavior Cloning, BC)

它是什么? 这是最简单的模仿学习,它把问题退化成一个监督学习问题。

  • 数据: \(D = \{(s, a)\}\) (专家在状态 \(s\) 时采取了动作 \(a\))。
  • 目标: 训练一个策略网络 \(\pi_\theta(a|s)\),使得 \(\pi_\theta(s) \approx a\)

最大问题:协变量漂移 (Covariate Shift)

  • 策略 \(\pi_\theta\) 不可能100%完美复刻专家。它会犯一点小错误。
  • 这个小错误会导致机器人进入一个新的状态 \(s'\),这个状态 \(s'\) 是专家数据集中从未出现过的。
  • \(s'\) 状态下,策略网络 \(\pi_\theta\) 不知道该怎么办(因为没见过),于是它会犯一个更大的错误,导致自己偏离得更远,最终彻底失败。

就像学车,你只在路的中间开,一旦车轮压线(小错误),你就不知道怎么把方向盘打回来了(因为训练数据里没有“压线后如何开回来”的样本)。


方法2: DAgger (Dataset Aggregation)

它是什么? 这是对 BC 问题的修正。

核心循环:

  1. 用当前数据集 \(D\) 训练一个策略 \(\pi_\theta\)
  2. 让机器人运行这个策略 \(\pi_\theta\)
  3. 机器人会犯错,进入它不认识的状态 \(s'\)
  4. 此时,把机器人“暂停”,请人类专家过来,告诉机器人:“在 \(s'\) 这个状态下,我 (专家) 会采取的正确动作是 \(a_{\text{expert}}\)”。
  5. 把这个新的数据 \((s', a_{\text{expert}})\) 加入到数据集 \(D\) 中。
  6. 回到第1步,重新训练。

为什么? DAgger 通过主动收集“如何从错误中恢复”的数据,解决了 BC 的漂移问题。


方法3: 逆强化学习 (Inverse RL, IRL)

它是什么? 这是一个更深刻的想法。

  • 正向 RL: 给定 (环境, 奖励函数),学习 (最优行为)。
  • 逆向 IRL: 给定 (环境, 最优行为),反推出 (专家脑中的奖励函数)。

为什么? 动作是表象,奖励函数才是任务的本质。

例子: 专家给你演示了10次“把水倒入杯子”。

BC 只会模仿这10次的具体动作。如果换个杯子,它可能就失败了。

IRL 会从这10次演示中反推出,专家的“奖励函数”大概是:(\(R = +100\) * (水进入杯子) - \(1\) * (水洒出来) - \(0.1\) * (时间))。

一旦你学到了这个奖励函数,你就可以用任何 RL 算法(比如 Q-Learning)来自己学一个最优策略,这个策略可能比专家的演示更鲁棒、更优秀。


更先进的方法
  1. 隐式行为克隆 (Implicit Behavior Cloning, IBC):
  • BC (显式策略) 是 \(a = F(o)\),一个输入对应一个输出。
  • IBC (隐式策略) 是学习一个能量函数 (Energy Function) \(E_\theta(o, a)\)

\(E_\theta(o, a)\) 的值越低,表示在状态 \(o\) 下采取动作 \(a\) 越“好”(越像专家)。

决策时: \(\hat{a} = \arg\min_a E_\theta(o, a)\) (寻找能使能量最小的动作 \(a\))。

优点: 能更好地处理多模态 (multi-modal) 行为(即一个状态下有多个“正确”动作,比如抓桌上的任何一个苹果都行)。

  1. 扩散策略 (Diffusion Policies):

这是目前最火的方法之一。它借鉴了 DALL-E 2, Stable Diffusion 等图像生成模型中的扩散模型 (Diffusion Model) 思想。

它将策略学习视为一个“生成”问题:给定状态 \(s\),它能生成一个好的动作 \(a\)

优点: 在建模复杂、多模态的动作分布方面表现极好。


机器人基础模型 (Robotic Foundation Models)

它是什么? 这是该领域的前沿。

类比: 就像 GPT-3 / LLaMA 是语言的基础模型,CLIP / DINO 是视觉的基础模型一样,研究者们希望建立机器人动作的基础模型。

  • 定义: 一个单一的、大型的模型,它在海量的、多样化的机器人数据上进行预训练,然后可以泛化到(或轻松微调到)大量不同的任务、机器人和环境上。
  • 数据: 不再是 \((s, a)\),而是 (状态 \(s\), 目标 \(g\)) \(\to\) 动作 \(a\)

VLA (Vision-Language-Action Models)

这一理念的实现。

  • 输入: 视觉(\(s\),比如摄像头图像)+ 语言(\(g\),比如人类用自然语言说“帮我拿个苹果”)。
  • 输出: 机器人动作 \(a\)

趋势:

  • RT-1 (2022): 证明了用 Transformer 统一处理 (图像, 语言指令, 动作) 是可行的。
  • RT-2 (2023): 惊人发现!他们拿一个预训练好的视觉-语言模型 (VLM)(只在互联网图片和文字上训练过),然后用机器人数据对其进行微调。

RT-2 的意义: VLM 的“常识”(比如“苹果”和“香蕉”是水果)可以迁移到机器人任务上。机器人不仅是在“模仿”,它开始“理解”它在做什么。

  • OpenVLA (2024): 开源的大型 VLA 模型,融合了强大的视觉模型 (DinoV2) 和语言模型 (Llama)。
  • Pi-Zero (2024): 一个最新的工业界模型,展示了“预训练+微调”的强大范式。

预训练 (Pre-training): 在海量的、多样的 (跨机器人、跨任务) 互联网数据上训练一个通用模型。

后训练 (Post-training): 在小规模、高质量的特定任务数据上进行微调。

结果: 实现了强大的零样本泛化 (Zero-shot),即机器人能完成它在训练中从未见过的任务。


剩余挑战 (Remaining Challenges)

我们离终极的通用机器人还有多远?


评估 (Evaluation):

我们怎么知道一个模型是好是坏?

  • 真实世界评估: 代价高、速度慢、有噪声(危险)。
  • 指标问题: 训练损失 (Training Loss) 和真实世界的成功率 (Success Rate) 之间相关性很弱。

仿真 (Simulation):

Sim-to-Real 鸿沟: 模拟器再好,也和真实世界有差距(比如物理参数、摩擦力、柔性物体的模拟)。

我们需要更好的模拟器,以及能自动生成海量、多样化场景的技术。


策略 (Policy) vs. 世界模型 (World Models):

目前大多数基础模型(如 RT-2, Pi-Zero)是策略 (Foundation Policy),它们直接学习 \(s \to a\)

但我们可能更需要世界模型 (Foundation World Models),它们学习 \(s, a \to s_{t+1}\) (即“物理常识”)。

世界模型能提供规划 (Planning) 能力,这被认为是通往更鲁棒、更可解释的 AI 的路径。


具身反馈 (Embodied Feedback)=:

目前的 LLM/VLM 都是在被动的互联网数据上训练的。它们“读过”全世界,但“没摸过”任何东西。

它们缺乏对物理世界交互的具身理解。

未来:我们需要能从物理交互反馈中学习的“具身基础模型”。


适应性 / 终身学习 (Adaptation / Life-Long Learning):

世界是变化的。机器人必须能够:

  • 适应新场景、新物体。
  • 适应不同人类的偏好。
  • 在部署后,通过经验持续自我改进(终身学习)。

系统考量 (Practical Considerations):

机器人是一个复杂的系统工程。

  • 延迟 (Delay): 传感器感知、模型计算、电机执行都有延迟。
  • 多模块协同: 实际系统通常是分层的:

高层策略 (High-Level): (比如 VLM) 运行较慢,负责“思考”长远规划(例如:“我应该先拿黄油,再递过去”)。

低层策略 (Low-Level): (比如 VLA) 运行极快(例如:200Hz),负责“反应”和“执行”(例如:“控制关节 \(X\) 到达 \(Y\) 坐标”)。

如何让这些不同频率、不同功能的模块高效协同,是一个巨大的工程挑战。


评论