0%

Transformer

Transformer 架构由 Google 在 NIPS 2017 提出,之后成为各种大模型的基础架构。一个完整的 Transformer 由 Encoder 和 Decoder 组成,整个架构图如下:
alt text

首先,整体概述 Transformer 的流程:

输入序列首先作 Input Embedding & Positional Embedding, 映射为矩阵,之后经过 $N$ 个 Encoder 子模块,每个模块都由 Self-Attention 和 Feed Forward Network 组成,并且每个模块后都进行残差连接和 LayerNorm. 最后得到整个 Encoder 模块的输出矩阵 $X$, 以待 Decoder 中 Cross-Attention 模块使用。

将已经输出的序列作 Output Embedding & Positioinal Embedding, 映射为矩阵,之后经过 $N$ 个 Decoder 子模块,每个模块都有一个 Masked-Attention, 一个 Cross-Attention, 一个 Feed Forward Attention 组成。同样每个模块之后都有残差连接和 LayerNorm.

最后解码器输出的结果经过线性映射,再经过 Softmax 映射为概率。从概率最大的几个 token 中按概率抽样,转为文本,输出。(取决于 top_p, temperature 等参数)

Embedding

Input Embedding

由于 Transformer 最早应用于机器翻译领域,这里以机器翻译为示例。输入是一长段文本,首先要进行 tokenize, 就是把一长串文本化为 token, 每一个 token 可能是一个单词,也可能长于一个单词(如固定搭配表示特殊的语义),也可能短于一个单词(如词根词缀也能表示一定的独立语义)。

Tokenize 之后,要对 token 进行 embedding, 就是把 token 映射到向量。这一步可以基于一个词表做,例如 Word2Vec. 不过训练较为复杂的模型时,初始的 embedding 词表其实并不重要,随机初始化的效果与基于 Word2Vec 的效果差别不大。

Positional Embedding

由于 Transformer 是把一串文本打散了之后输入,这样就会丢失某一个词在原始序列中的位置信息,因此在 Input Embedding 之后要做一步 Positional Embedding, 把初始的位置信息融入到嵌入向量中。具体公式如下:t 就是 token 在序列中的位置,i 表示 Positional Embedding 的第 i 维,H 表示 Embedding 的维数。

alt text

这样生成 Positional Embedding $p_k$ 与 Input Embedding $v_t$ 直接相加,得到总的 Embedding $x_t$. 这样输入从文本就转为一个矩阵 $x = [x_1, x_2,…, x_n]^T $.

Residual Connection

残差连接,就是把这一层的输出再加上这一层的输入。它旨在缓解梯度消失的问题,使得信息直接跨模块传递。

在梯度反向传播时,对于某一层的参数的偏导数,由于链式法则,是一系列偏导的乘积。连乘容易出现数值很小的情况,从而使参数更新缓慢,这一现象就是梯度消失。有了残差,求偏导就会多一个 1, 这样就避免了数值接近 0 的情况。

Normalization

Methods of Normalization

归一化的方法有很多,常见的有 BatchNorm, LayerNorm, RMSNorm, DeepNorm.

  • BatchNorm (BN): 批次归一化,对于每次训练的 Batch,求得每一个维度的均值和标准差,进行缩放。然而对于小批量数据,这种用一个 Batch 的均值和标准差作为整体标准进行处理很容易误差较大,效果不好(NLP 任务中并不常用)。
    \[
    BatchNorm(x_i) = \frac{x_i - \mu_i}{\sigma_i} \cdot \gamma_i + \beta_i
    \]

\[
\mu_i = \frac{\sum_j x_i^{(j)}}{BatchSize}, \sigma = \sqrt{\frac{\sum_j (x_i^{(j)} - \mu_i)^2}{BatchSize}}
\]

  • LayerNorm (LN): 层归一化,对于每一个数据,求其所有维度数据的均值和方差进行缩放。这样每一个数据的缩放仅与自己有关,解决了 BN 在小样本上效果不佳的问题。

\[
LayerNorm(x) = \frac{x - \mu}{\sigma} \cdot \gamma + \beta
\]

\[
\mu = \frac{\sum x_i}{H}, \sigma = \sqrt{\frac{\sum (x_i - \mu)^2}{H}}
\]

  • RMSNorm: RMS 归一化,可以看作是 Layer Norm 的变种,主要针对于 LN 要计算均值和标准差,计算量较大的问题。RMSNorm 直接令样本中每一个维度除以样本所有维度的方均值再缩放,RMSNorm 训练的速度和效果都比 LayerNorm 要好。

\[
RMSNorm(x) = \frac{x}{\sqrt{\sum x_i^2} } \cdot \gamma
\]

  • DeepNorm: 深度归一化,是一种 Post-Norm 方法,其实就是在残差连接中对之前的激活值 x 进行了缩放,残差计算之后再用 LayerNorm. MicroSoft 的研究人员发现这样的归一化可以使 Transformer 的训练更稳定,训练 1000 层以上的神经网络不再困难。

    \[
    DeepNorm(x) = LayerNorm( \alpha x + Sublayer(x))
    \]

注:可以发现在 BN, LN, RMSNorm 中都有一些 learnable 参数(如 $\gamma, \beta$),这可以调整归一化的强度,未必彻底的归一化才适合。特别的,当 $\gamma = \sigma$, $\beta = \mu$ 时,相当于并没有归一化。

Position of NormLayer

归一化的位置也有很多种不同的选择,总结如下,其中每种归一化的优劣引用了《大语言模型》(赵鑫等)的总结:

  • Post-Norm: 后向归一化,最原始的Transformer中选择的是 Post-Norm,即一个 Sublayer(如 Attention, FeedForward) 之后做残差计算,残差之后再做归一化。

    \[
    PostNorm(x) = Norm(x + Sublayer(x))
    \]
    “在原理上,后向归一化具有很多优势。首先,有助于加快神经网络的训练收敛速度,使模型可以更有效地传播梯度,从而减少训练时间。其次,后向归一化可以降低神经网络对于超参数(如学习率、初始化参数等)的敏感性,使得网络更容易调优,并减少了超参数调整的难度。然而,由于在输出层附近存在梯度较大的问题,采用 Post-Norm 的 Transformer 模型在训练过程中通常会出现不稳定的现象。”

  • Pre-Norm: 前向归一化,对 Sublayer 的输入做归一化,之后再过 Sublayer 层,算残差。

    \[
    PreNorm(x) = x + Sublayer(Norm(x))
    \]

    “相较于 Post-Norm,Pre-Norm 直接把每个子层加在了归一化模块之后,仅仅对输入的表示进行了归一化,从而可以防止模型的梯度爆炸或者梯度消失现象。虽然使用了 Pre-Norm 的 Transformer 模型在训练过程中更加稳定,但是性能却逊色于采用了 Post-Norm 的模型。”

  • Sandwich-Norm: 三明治归一化,就是 Pre-Norm 和 Post-Norm 的结合,虽然想法比较理想,从理论上应该性能更好,但是实际中研究人员发现这种办法并不能使训练完全稳定,有时候甚至会崩溃。
    \[
    SandwichNorm(x) = x + Norm(Sublayer(Norm(x)))
    \]

Feed Forward Neural Network

前馈神经网络,就是信息流动始终由前一层流向后一层,各层间信息没有反馈。比如 CNN, MLP 都属于前馈神经网络,而 RNN 不是。原始的 Transformer 这里用的是两层神经网络(两层的 MLP)。

Attention

Attention 机制是 Transformer 的独创性的重大突破,直接建模了任意距离的词元之间的语义关联,实现了在上下文中理解语义信息(解决了一词多义等问题),从而提高了模型理解复杂文本的能力。

Multi-head Attention

Multi-head Attention 计算过程:
先将输入矩阵 $X$ 映射为 $Q, K, V$:

\[
Q = X W^Q
\]

\[
K = X W^K
\]

\[
V = X W^V
\]

然后计算 Attention:

\[
Attention(Q, K, V) = Softmax(\frac{QK^T}{\sqrt{D}})V
\]

注意在 Decoder 中的 Attention 是 Masked-Attetion, 即要在计算 Attention 之前,把上三角元全部设为 -inf,这是因为使用词元预测的方法训练时,要使 Decoder 只能使用当前已生成的信息,而不能使用“未知”的信息。

在 Multi-head Attention 中,有 $N$ 组结构相同,而参数不同的 $W^Q, W^K, W^V$ 映射矩阵,把每一组参数作 Attention 之后的结果拼接起来,再通过一个矩阵映射得到最终的结果。

\[
head_i = Attention(XW^Q_i, XW^K_i, XW^V_i)
\]

\[
MHA(X) = Concat(head_1, head_2,…,head_n) W^O
\]

alt text

分析与解释:

  1. $K, Q, V$ 各自的含义?
    $Q$ 表示当前查询的位置,$K$ 表示被查询的其他位置,$V$ 表示实际的语义信息。用 $Q$ 与 $K$ 点乘计算当前查询的位置与其他位置的相似度,再用 Softmax 转为概率,最后乘 $V$, 其实是各个位置语义信息作加权平均。
  2. 为什么用多头注意力?
    (1)允许模型再不同位置联合处理表达不同子空间信息能力,增强非线性表达能力(将输入向量投影到不同的子空间中,每个子空间可能对应不同任务/特征,比如翻译任务中,有的头关注主谓一致,有的头关注动宾搭配,有的头关注主句和从句的关系等等);
    (2)缓解秩塌缩问题(有论文推导 Transformer 的秩表达式,多次注意力层堆叠后,注意力矩阵的秩逐渐降低至1,模型的表达能力退化,趋于固定分布),多头注意力可以将注意力矩阵的秩直接扩大 $N$ 倍,一定程度上可以缓解秩塌缩的问题。
  3. Attention 计算为什么除以 $\sqrt{D}$ ?
    点积计算中数据随向量维度增大而增大,容易处在 Softmax 饱和区,梯度较小不易训练。

Self-Attention & Cross-Attention

Self-Attention, 自注意力,用于建模同一模态信息内部之间的关联,有助于理解文本的上下文语义。Self-Attention 中的 $K, Q, V$ 全部来自于 Input/Output.

Cross-Attention, 交互注意力,用于实现跨模态之间的语义对齐。Cross Attention 用在解码器中,$Q$ 来自于 Output, 而 $K,V$ 来自于 Input.

在 Decoder 中,每一个子模块先包含了 Masked-Self Attention, 这一模块用于理解已输出的信息,而此模块后续的 Cross-Attention 用于理解未输出的原始模态的信息(例如翻译任务,Masked-Self Attention 关注”It is a”, Cross Attention 关注“猫”).

未完待续

Vision Transformer

Vision Transformer 旨在直接利用 Transformer 处理计算机视觉的任务,而不依赖于传统的 CNN。ViT 的主要思想是把一张图片打成 patch, 每一个 patch 相当于 NLP 任务中的一个 token. 这样就直接照搬 Transformer 中的 embedding, positional encoding, attention 等模块处理。

未完待续。

Camera Imaging Model

相机模型解释了如何从世界坐标系下的一个 3D 点映射到成像平面中的一个 2D 点。我们假设相机为线性模型(即相机是原始的小孔相机,显然不合理,但是先这样吧)。成像的过程如下图:

alt text

Extrinsic Parameters

首先要清楚相机相对于世界坐标下的位姿。这就用到了在机器人学里学的齐次坐标变换矩阵:
\[
T =
\begin{bmatrix}
r_{11} & r_{12} & r_{13} & t_{x} \\
r_{21} & r_{22} & r_{23} & t_{y} \\
r_{31} & r_{32} & r_{33} & t_{z} \\
0 & 0 & 0 & 1 \\
\end{bmatrix}
\]

其中,
\[
R =
\begin{bmatrix}
r_{11} & r_{12} & r_{13} \\
r_{21} & r_{22} & r_{23} \\
r_{31} & r_{32} & r_{33} \\
\end{bmatrix}
\]

就是旋转矩阵,表示世界坐标轴在相机坐标中的位姿,而向量

\[
t =
\begin{bmatrix}
t_{x} \\
t_{y} \\
t_{z} \\
\end{bmatrix}
\]

表示平移向量,即世界坐标原点在相机坐标中的位置。这样有了齐次坐标变换矩阵,我们就可以把世界坐标转换为相机坐标了。

\[
\begin{bmatrix}
x_c \\
y_c \\
z_c \\
1
\end{bmatrix}
=\begin{bmatrix}
r_{11} & r_{12} & r_{13} & t_{x} \\
r_{21} & r_{22} & r_{23} & t_{y} \\
r_{31} & r_{32} & r_{33} & t_{z} \\
0 & 0 & 0 & 1 \\
\end{bmatrix}
\begin{bmatrix}
x_w \\
y_w \\
z_w \\
1
\end{bmatrix}
\]

即:

\[
\textbf{x}_c = M_{ext} \textbf{x}_w
\]

而这个齐次坐标变换矩阵,被称为相机的外参矩阵。

Intrinsic Parameters

相机的内参矩阵解决如何将相机坐标系下的 3D 点映射到成像平面的 2D 点,即 $(x_c, y_c, z_c) \rightarrow (u, v) $. 根据几何关系

\[
\frac{z_c}{f} = \frac{x_c}{u}
\]

\[
\frac{z_c}{f} = \frac{y_c}{v}
\]

注意在相机坐标中,单位常选作 pixel (而非米), 且坐标的原点常选在左上角,因此要对结果作缩放和平移:

\[
u = m_x f \frac{x_c}{z_c} + o_x = f_x \frac{x_c}{z_c} + o_x
\]

\[
u = m_y f \frac{y_c}{z_c} + o_y = f_y \frac{y_c}{z_c} + o_y
\]

$(f_x, f_y, o_x, o_y)$ 被称为相机的内参。然而这个方程并非线性,难以用矩阵表示。因此我们采用了升维的办法(即齐次坐标)。

\[
\begin{bmatrix}
u \\
v \\
1 \\
\end{bmatrix}
= \begin{bmatrix}
\textbf{u} \\
\textbf{v} \\
\textbf{w} \\
\end{bmatrix}
= \begin{bmatrix}
f_x & 0 & o_x & 0 \\
0 & f_y & o_y & 0 \\
0 & 0 & 1 & 0
\end{bmatrix}
\begin{bmatrix}
x_c \\
y_c \\
z_c \\
1
\end{bmatrix}
\]

注意,这个矩阵乘出来最后一维不一定是1,这时候将齐次坐标缩放,将最后一维调为1. 这个矩阵称为相机的内参矩阵,记为 $M_{int}$。因此:

\[
\textbf{u} = M_{int} \textbf{x}_c = M_{int} M_{ext} \textbf{x}_w = P \textbf{x}_w
\]

即可实现从世界坐标到相机坐标的变换,$P = M_{int} M_{ext}$ 称为投影矩阵。

Camera Calibration

相机工作前,首先要进行内外参标定。观察$P = M_{int} M_{ext}$,可以发现内参矩阵为一个上三角阵,而外参矩阵是一个正交阵,因此给定投影矩阵 $P$, 我们可以利用 RQ 分解确定唯一的一组上三角阵与正交阵,使他们乘积为 $P$, 这样也就确定了所有的外参和内参。因此问题转化为如何确定投影矩阵 $P$.

首先,我们找一个几何形状已知的物体,这样它在世界坐标系下的坐标和在相机坐标系下的坐标就都是已知的,如下:
alt text
把这个式子全部展开,换一种写法,写成:

\[
\textbf{A} \textbf{p} = \textbf{0}
\]

其中 $\textbf{A}, \textbf{p}$ 由下面给出:
alt text

这就变成了求齐次线性方程组的解了,然而这显然有无穷多组解(且它们线性相关)。这对应着我们把物体放大一倍,且拉远一倍,所得到的像不变。或者从齐次坐标去理解,齐次坐标扩大 $k$ 倍,不影响实际的 $u, v$ 值,因为总是要归一化的。因此解此方程需要加上规范条件,有两种:

\[
p_{34} = 1
\]

或:

\[
||\textbf{p}|| ^ 2 = 1
\]

采用第二种规范,问题等价于这样的优化问题:

\[
\min_{\textbf{p}} ||\textbf{Ap}|| ^ 2 \\
\mathrm{s.t.} ||\textbf{p}|| ^ 2 = 1 \\
\]

等价于:

\[
\min_{\textbf{p}} \textbf{p}^\textbf{T}\textbf{A}^\textbf{T} \textbf{Ap} \\
\mathrm{s.t.} \textbf{p}^\textbf{T}\textbf{p} = 1 \\
\]

等价于优化这个损失函数:

\[
L(\textbf{p}, \lambda) = \textbf{p}^\textbf{T}\textbf{A}^\textbf{T} \textbf{Ap} - \lambda (\textbf{p}^\textbf{T}\textbf{p} - 1)
\]

使导数为0,得到:

\[
\textbf{A}^\textbf{T} \textbf{Ap} = \lambda \textbf{p}
\]

这就是一个求特征值的问题了,我们求得 $\textbf{A}^\textbf{T} \textbf{A}$ 的特征值后,找到最小的那个(这里有问题,不知道为啥最小的那个特征值对应着损失函数的极小值,这里没有给出证明,姑且先记住吧),然后求其特征向量,再将特征向量归一化,重新排布,就可以得到 $P$ 矩阵,再作 RQ 分解,即可确定相机的全部外参和内参。

Stereo Matching

相机标定完成后,我们想办法利用相机的二维图像重建三维世界,一种简单的想法是,拿同样一个相机,在平移前后拍两张照片,然后根据同一点在两张照片上的不同位置 (disparity) 推算出这一点的真实位置。原理如下:

alt text

直接根据小孔相机模型写出两种相机下某一点的坐标,然后假设内参和相机坐标系下的坐标已知,解算出真实世界中的坐标。

alt text

值得注意的是,$z$ 与 disparity 成反比,这也与我们的经验相符。比如手指放在双眼中间,当手指离眼睛比较远的时候,两眼看到的手指位置几乎一样,而当手指很贴近鼻子的时候,两眼看到的手指位置完全相反。因此,我们只要找到两张图片中同一个点,就能根据以上公式算出它的位置。找到这两个点的过程被称为 stereo matching. 有以下几点细节:

  1. 由于$v_r$ 与 $v_l$ 一样,所以两张照片上相同的点必然纵坐标相同,因此只用扫描横着的一排区域即可。

  2. Stereo matching 匹配并不是一个点,而是一个区域,区域的大小选择是一个要调的参数,区域太大了,那么确定点的过程误差较大,区域太小了,匹配的准确性不好,有很多区域可能算出来差别都很小。

  3. 计算区域与区域的差别就是单点的差别再对区域求和,常见的几种差别计算方法如下:

alt text

3D Vision 数据表征方法

Voxel

Voxel (译为体素),是一种结构化的数据表征方法,就是把空间划为网格,每个网格有占据/空置两种状态,如果是占据状态,则可以用 RGB 三通道来表征它的颜色。当然,一个点除了 RGB 之外,还有法向量等特征。用公式写出:

\[
x_i = (x, y, z, o, r, g, b, …)
\]

其中,$o$ 可取 0 或 1,表示是否占据。一些情况下,也可以取 0 至 1 中的某些数,表示占据的概率。一个例子如下图:

alt text

Voxel 的好处是结构化,形状规则,坏处是内存开销较大,分辨率恒定(在一些精细结构上,恒定的分辨率就显得不够用了)。当然,为了解决分辨率的问题,还有一种 Octree (八叉树结构), 采用树结构进行存储。一个空间被分为 8 份(8 个立方体),如果这一份里有信息,就继续细分,如果这一份里没有信息,就不再分。这样有助于节约存储空间,实现动态的分辨率。如下图:
alt text

Mesh

Mesh 也是一种结构化存储三维数据的方法。它由顶点、边和面构成。它首先存储一个顶点列表:$x_i = (x, y, z)$,再存储一个面索引列表:$f_s = (x_i, x_j, x_k,…)$. Mesh 可以按照面的形状分类,有三角形 (Triangle Mesh)、四边形 (Quad Mesh)、不定形 (Polygon Mesh) 等等。

alt text

3D CNN

3D CNN 就是把二维的卷积神经网络拓展到三维,最早的 3D CNN 被用于处理时空序列信号(如视频,见:Tran, Du, et al. “Learning spatiotemporal features with 3d convolutional networks.” Proceedings of the IEEE international conference on computer vision. 2015.),当然,3D CNN 也可用于处理 3D Vision.

下面介绍这篇文章中 3D CNN 的结构:与二维卷积类似,每一个卷积层仍有一个卷积核,在空间上尺寸为 3*3*3,通道数为 3 ,每次移动的 stride 为 1(在三个维度上的移动 stride 一致),卷积核的个数在图中列出。

每一个卷积层的后面都紧跟着一个 max pooling(PointNet也有),就是在一个 pooling kernel 的范围内,选择最大的数值以代表整个 pooling kernel. Pooling 是 CNN 的常见操作,有助于降低计算复杂度、降噪、提取到更高级/抽象的特征。当然,适不适合 pooling 要根据具体问题选择,比如 AlphaGo 在处理围棋的棋局信息时,显然就不适合用 pooling. Max pooling 可以参考下图。
alt text

最后先要把三维特征图展平为一维,然后经过两个 FC 网络,目的是把图像的特征映射为分数向量,实现分类。整个流程图如下:

alt text

注:

  1. 自己学习 CNN 的时候一开始错误地理解了卷积核的个数和通道数的关系,总结一下:上一层的卷积核的个数作为这一层的输入通道数,这一层的每一个卷积核对所有的输入通道求和,这一层的输出通道数等于这一层的卷积核的个数。
  2. 3D CNN 由于其处理的特性,它只能接受结构化的数据(如 Voxel 这样),对于非结构化的数据(如点云,它需要先把点云转为 Voxel.

PointNet

PointNet 直接处理点云数据,点云数据的特点:

  • 无序性:每个点之间独立,N 个点输入是无序的,即模型比如对 N! 种不同排列的输入给出同样的输出。
  • 几何变换不变性:对点云在空间上做旋转变换(相当于传感器的位置变化),模型在做分类、分割等任务时应给出同样的结果。

PointNet 的思路:点云数据的无序性导致我们应该采用一种对称函数,同时这样的函数要有足够强的拟合能力。于是就有了 PointNet,其整体思路是:对于不同的点先采用同样的多层感知机将其映射到高维空间,然后对其作 max pooling (这显然是一个对称函数), 得到一个向量,可以作为整个点云的语义特征。下面根据不同的任务分别讨论:

  • 分类任务:直接把这个整体的语义特征过一个 MLP 就得到打分,进行分类即可(输出是概率向量)。公式化如下,式中 $\gamma$, $h$ 均为 MLP:

    \[
    f(x_1, x_2,…, x_n) = \gamma(\max_{i = 1, 2,…, n}{h(x_i)})
    \]

  • 分割任务:分割任务要求对每一个点都要进行分类,所以我们要基于整体语义理解每一个点的信息,因此做一个 concat,即把单点向量与整体语义向量直接拼接后经过 MLP 得到单点特征,再过一个 MLP 得到每一个点的打分(输出是矩阵)。

至于空间变换不变性,PointNet 采取了对输入点云预处理的方法以将输入对齐,具体来说:先用一个 T-Net (整个 PointNet 的缩小版,仍含有 MLP, max pooling 等基本结构) 预测一个 3*3 矩阵,再直接将这个矩阵乘以输入点云的位置维度,相当于把输入点云做了一次旋转变换。在点云经过 MLP 提取特征之后,对特征进行同样的操作(这里的矩阵就是特征的维度*特征的维度,这会带来参数量较大,容易过拟合,不易学习的问题,因此作者在这里又针对这个矩阵设置了正则项)。

alt text

注:映射到高维空间可以增加复杂度增强模型的表达能力,并且原始论文论证了当维度足够高时,PointNet 可以拟合任意的连续集合函数。

PointNet++

PointNet 处理的是 3D 视觉的问题,却抛开了视觉最常用的 CNN 结构,很有创新,但是如果能借鉴一些 CNN 的思想,或许模型的效果还会提高,这就有了 PointNet++。

宏观上看,PointNet++ 是在点云局部递归地调用 PointNet, 使其能发现局部特征并且得到不同层级的特征信息(神似 CNN)。

Sampling

在每一次调用 PointNet 时,首先对输入的点云进行采样和分组。采样一般采用最远点采样策略(Farthest Point Sampling, FPS),描述如下:

  1. 随机选取一个点作为中心点
  2. 计算每个点到中心点集中所有点的最近距离
  3. 选出最近距离最远的那个点作为下一个中心点,重复 2, 直至选出 N 个中心点。

相比于随机采样,或 KNN 采样,这样计算量小且中心点分布更均匀,覆盖性好。

Grouping

至于分组方法,有两种办法:球查询和 KNN 分组。

  • 球查询(Query Ball):设定一个欧氏空间的距离 r, 对于所有与中心点距离小于 r 的点,都归入这个中心点的组,这样适合点云密度均匀的情景,如果密度不均,可能出现一些组点数很少,一些组点数太多。

  • KNN 分组:与中心点最近的 k 个点归入这个中心点的组,这样保证每个组的半径一样。

在 PointNet++ 论文中,每一组的点云数量为 K,是一个固定值,这是先经过原始分组之后,若小于 K, 则利用 padding 方法补充,若大于 K, 则直接截断。

采样分组之后就对每一个组运用 PointNet, 得到每个组的一个特征,再把这些特征视作点云(当然输入的维数增加了很多),重复采样-分组- PointNet 这个循环即可实现多层特征的提取。(论文中称之为 Sampling Layer - Grouping Layer - PointNet Layer)

从形状上理解:输入为 $N*(d + C)$, d 为 3 , C 为原始特征(如法向量、RGB等),经过 Sampling Layer, 得到 $N’*(d+C)$, 经过 Grouping Layer, 得到 $N’*K*(d+C)$, 再经过 PointNet Layer, 得到 $N’*(d+C’)$ ($C’$ 为抽取的高维特征)。

MSG/MRG

在 Grouping Layer 中还有一个遗留问题,即对于密度非均匀的点云,Query Ball/KNN 都不能很好的处理,在稠密点云上抽取特征的方法可能不能很好地泛化到稀疏点云上。而实际上 LiDAR 传回的点云数据往往是非均匀的,距离近的位置点云稠密,距离远的位置点云稀疏。

对此,作者提出了 Multi-scale grouping (MSG) 和 Multi-resolution grouping (MRG) 两种办法。MSG 就是在每一层都作不同大小的半径进行分组,然后经过 PointNet Layer 抽取的特征作 concat, 结果为总特征。

然而 MSG 相当于每一个循环里的 Grouping 和 PointNet 都要重复很多次,且包含了在较低层级上对较大的半径作 PointNet, 这样的计算开销很大,因此,作者又提出了 MRG. 融合了不同层级的特征.

MRG 思路如下:每一层的特征依然是两个特征的 concat, 第一个特征是这一层的子特征作 PointNet(left), 第二个特征是这一层的前一层的点直接作 PointNet(right). 在稀疏点云中,右侧的特征更可信,稠密点云中,左侧的特征更可信。把这两种不同的特征融合起来,可以使模型更适应不同密度的点云。

下图中 (a) 是 MSG,(b) 是 MRG.

alt text

PointNet++ 的整体流程图如下:

alt text

论文中提到,这种分组方法相比于 CNN 的卷积核滑动的办法,能减少 overlap, 保证每个分组之间的独立性,效果更好。

Classification & Segmentation

对于视觉的分类/分割任务,PointNet++ 给出了两种不同的办法:

  • 分类:持续抽取特征,直至抽取为一个向量,这就是全局的特征。再把全局特征经过 FC, 就可以得到不同类别的打分向量。
  • 分割:要对每一个点给出一个打分向量进行分类,这就要用到逆距离加权插值的办法。当 PointNet++ 抽取到高级特征之后,设高级特征的点为 $P_s$, 需要将这些特征点回传到原始点 $P_0$. 插值的公式如下:
    alt text
    其中归一化权重如下:
    alt text
    论文中提到,一般 $k$ 取 3, $\alpha$ 取 2. 最后将插值的结果经过 MLP 层得到打分矩阵,就可以将每个点进行分类。

维数灾难

在之前的问题中,我们用了 Q-Learning, DQN, Actor-Critic, REINFORCE 等一系列办法,但是策略网络的输出始终是一个向量,每个维度的数值表示对对应动作的打分,这样的动作是离散的,向量的维数等于动作空间的个数。当我们要研究连续控制问题(比如机器臂的转角),我们只能把控制量离散化。但是假设这个机器人的自由度很多,每个控制量都直接离散化研究,这必然使得动作空间极大,很难计算。所以,我们需要研究连续控制算法,这主要包含 DDPG 和 TD3, 在实践中 TD3 运用较多。

DDPG

DDPG(深度确定性策略网络)的整个流程可以如下所示,值得注意的是:
alt text

  1. 策略网络的输出还是一个向量,不过此向量的维数等于自由度,每一个元素是控制量的一个取值,这就是确定性策略,输出什么执行什么。
  2. DDPG 仍然是一种 AC 类算法,有一个策略网络和一个价值网络。价值网络按照 TD 算法更新,而策略网络依据价值网络更新。
  3. DDPG 是一种异策略,它的目标策略不同于行为策略,可以跑完很多步之后从内存中取出四元组(batch)进行训练,即可以经验回放。
  4. 其实从上面几点就可以看出 DDPG 简直是四不像,它融合了之前几乎所有的办法。它有点像 Q学习(或DQN),因为都是异策略,用经验回放更新价值网络;它有点像策略学习,因为采用的是策略网络;它又是一种 AC 类算法。
  5. DDPG 仍然使用了目标网络的办法来降低高估的问题。
  6. DDPG 实践效果并不好,实际中应该用它的改良版 TD3。

下面贴一个 DDPG 的伪代码:

alt text

TD3

TD3 (Twin Delayed Deep Deterministic Policy Gradient),译为孪生延迟深度确定性策略梯度算法。TD3 是 DDPG 的改良版,主要做了以下改进:

  1. 截断的 Double Q-Learning:通过学习两个 Q 值函数,用类似 Double Q-Learning 的方式更新critic 网络。这个目的还是降低高估,训练两个价值网络,让他们做预测,然后选较小的那个作为 TD Target.
  2. 延迟策略更新:更新过程中,策略网络的更新频率低于 Q 值网络。这是实验发现,也可以这么解释:当裁判员的打分还不够精确的时候,我们应该先训练裁判员,使之打分准确,然后拿这个裁判去训练选手。所以策略网络应该比价值网络更新的慢一些,而目标网络为了降低高估,更新的也应该慢一些。这样一来,每轮都更新的只有价值网络,而三个目标网络和策略网络是隔几轮更新一次。
  3. 目标策略平滑:在目标策略的输出动作中加入噪声,以此平滑 Q 值函数的估计,避免过拟合。这个噪声用的是截断正态分布。

TD3 的流程图如下:
alt text

下面贴一个 TD3 的伪代码:
alt text

(以上伪代码摘自《深度强化学习:基础、研究与应用》)

Policy Learning and Value Learning

深度学习的目标是找到最优的策略,通常有两种办法:策略学习和Q学习。Policy Learning 的优化对象就是策略函数 $\pi(a|s)$, 而 Value Learning 的优化对象是 $Q^*(s,a)$, 目标是找到最准确的 Optimal Action Value 函数,从而据此选择最合适的策略。

Deep Q Network (DQN)

Deep Q-Learning 的目标是,给定状态和动作,预测其可能最高获得回报的期望(最高意味着选择最好的策略)。我们用一个神经网络来预测这个期望。Deep Q-Learning 的方法是,先把 state 输入卷积层,抽取为特征之后,再输入全连接层,最后返回一个分数向量,经过 softmax 函数之后转化为概率值,最后选择概率最大的那一个 action 作为执行动作。(有个小问题:在 LLM 中,为了使生成的文本更灵活丰富,往往不会单纯选择概率最大的输出,而是根据 temperature 和 top_p 参数,从概率最大的几个中采样取出,这样的思路是否能用到 DQN 中呢?这会不会使得 DQN 在训练初期表现得更好?)

alt text

Deep Q-Learning 网络可以记作 $Q(s,a;\textbf{w})$, 式中 $\textbf{w}$ 表示神经网络的参数(也就是训练的对象)。要训练神经网络,我们应该找出一个损失函数,比如关于预测的回报和真实的回报$y$,我们取:
\[
L(\textbf{w}) = \frac{1}{2} (Q(s,a ; \textbf{w}) - y)^2
\]
然后求梯度:
\[
\nabla_{\textbf{w}}L(\textbf{w}) = (Q(s,a ; \textbf{w}) - y) \cdot \nabla_{\textbf{w}} Q(s,a ; \textbf{w})
\]
注意,对于一个标量求关于矢量的梯度,梯度的形状与矢量的形状一致。然后做梯度下降(取学习率为 $\alpha$ ):
\[
\textbf{w} \leftarrow \textbf{w} - \alpha \cdot \nabla_{\textbf{w}}L(\textbf{w})
\]
然而这种办法存在一种弊端:每次跑完一个 trajectory(假设episodic)才能做一次更新,优化的效率太低了,有没有其他的办法实现每一步更新一次呢?这就需要 Temporal Difference 算法(时间差分算法)。

Temporal Difference Algorithm

时间差分算法实现了每运行一步更新一次神经网络参数。具体方法如下:

  1. 观察所处的状态 $s_t$
  2. 根据 $Q(s_t,a_t;\textbf{w})$, 选择预期回报最大的动作执行
  3. 执行一次之后收到了奖励 $r_{t}$, 并且更新到状态 $s_{t+1}$
  4. 根据 $Q(s_{t+1},a_{t+1};\textbf{w})$ 预测,并选择出预期回报最大的动作,但是不执行
  5. 计算 TD error $\delta_{t} = Q(s_{t},a_{t};\textbf{w}) - [r_{t} + \gamma \cdot Q(s_{t+1},a_{t+1};\textbf{w})]$,其中,$Q(s_{t+1},a_{t+1};\textbf{w})$ 称为 TD Target
  6. 计算损失函数:
    \[
    L(\textbf{w}) = \frac{1}{2} (Q(s_{t},a_{t};\textbf{w}) - (r_{t} + \gamma \cdot Q(s_{t+1},a_{t+1};\textbf{w})))^2
    \]
  7. 对损失函数求梯度(这里假装 TD Target 关于 $\textbf{w}$ 是一个常数):
    \[
    \nabla_{\textbf{w}}L(\textbf{w}) = (Q(s_{t},a_{t};\textbf{w}) - (r_{t} + \gamma \cdot Q(s_{t+1},a_{t+1};\textbf{w}))) \cdot \nabla_{\textbf{w}} Q(s,a ; \textbf{w})
    \]
    \[
    \nabla_{\textbf{w}}L(\textbf{w}) = \delta_{t} \cdot \nabla_{\textbf{w}} Q(s,a ; \textbf{w})
    \]
  8. 梯度下降:
    \[
    \textbf{w} \leftarrow \textbf{w} - \alpha \cdot \delta_{t} \cdot \nabla_{\textbf{w}} Q(s,a ; \textbf{w})
    \]

理解与解释:
时间差分算法是比较在每一步操作前和操作后对于最大回报期望的估计,每一步前后的区别是:每一步执行前,对未来的回报估计是纯的估计,而每一步执行后,都能够得到一个立刻的奖励(这个奖励是真实发生的),把这一步的真实奖励和这一步后对未来的估计结合起来,就能得到这一步前对未来的另一个估计。后者的这个估计比前者的估计要准确(毕竟有一部分是真实观测的),所以我们努力使得前者的估计贴近后者的估计,而这就可以用一个误差函数去优化了。

此处还有一个问题:为什么要设置 TD Target: $r_{t} + \gamma \cdot Q(s_{t+1},a_{t+1};\textbf{w})$ 有这样的形式?

回忆一下回报的定义:\[ U_t = \sum_{k=t}^{n} \gamma^{k-t} \cdot R_k \] \[ U_{t+1} = \sum_{k=t+1}^{n} \gamma^{k-t-1} \cdot R_k\]由 $ U_t $ 和 $ U_{t+1}$的定义可得:

\[
U_t = R_t + \gamma \cdot \sum_{k=t+1}^{n} \gamma^{k-t-1} \cdot R_k
\]

回忆一下,Optimal Action Value Function 可以写成

\[
Q_* (s_t, a_t) = \max_{\pi} \mathbb{E} [U_t | S_t = s_t, A_t = a_t]
\]

再回忆一下Bellman Optimal Equation, 可以发现:

\[
\begin{aligned}
Q_* (s_t, a_t) &= \mathbb{E}_{S_{t+1} \sim p(\cdot | s_t, a_t)} [R_t + \gamma \cdot \max_{a \in \mathcal{A}} Q_* (S_{t+1}, a) | S_t = s_t, A_t = a_t].
\end{aligned}
\]

我们可以用 Monte Carlo 算法对上式进行估计:
\[
Q_* (s_t, a_t) \approx r_t + \gamma \cdot \max_{a \in \mathcal{A}} Q_* (S_{t+1}, a)
\]

再结合到 DQN 中,我们用 $Q(s_{t},a_{t};\textbf{w})$ 来估计 $Q_*(s_{t},a_{t})$, 这就不难理解 TD Target 的形式了。

Training Process

应用 TD 算法进行训练,步骤如下:

  1. 收集训练数据:我们可以用任何策略函数 $\pi$ 去控制智能体与环境交互,这个 $\pi$ 就叫做行为策略(behavior policy)。比较常用的是 $\epsilon$-greedy 策略:
    \[
    a_t =
    \begin{cases}
    \arg \max_a Q(s_t, a; w), & \text{以概率 } (1 - \epsilon); \\
    \text{均匀抽取 } A \text{ 中的一个动作}, & \text{以概率 } \epsilon.
    \end{cases}
    \]
    把智能体在一局游戏中的轨迹记作:
    \[
    s_1, a_1, r_1, s_2, a_2, r_2, \cdots, s_n, a_n, r_n.
    \]
    把一条轨迹划分成 $n$ 个 $(s_t, a_t, r_t, s_{t+1})$ 这种四元组,存入数组,这个数组叫做经验回放数组(replay buffer)。

  2. 更新 DQN 参数 $w$:

    1. 随机从经验回放数组中取出一个四元组,记作 $(s_j, a_j, r_j, s_{j+1})$。设 DQN 当前的参数为 $w_{\text{now}}$,执行下面的步骤对参数做一次更新,得到新的参数 $w_{\text{new}}$。
    2. 对 DQN 做正向传播,得到 Q 值:
      \[
      \hat{q}_j = Q(s_j, a_j; w_{\text{now}}) \quad \text{和} \quad \hat{q}_{j+1} = \max_{a \in A} Q(s_{j+1}, a; w_{\text{now}}).
      \]
    3. 计算 TD 目标和 TD 误差:
      \[
      \hat{y}_j = r_j + \gamma \cdot \hat{q}_{j+1} \quad \text{和} \quad \delta_j = \hat{q}_j - \hat{y}_j.
      \]
    4. 对 DQN 做反向传播,得到梯度:
      \[
      g_j = \nabla_w Q(s_j, a_j; w_{\text{now}}).
      \]
    5. 做梯度下降更新 DQN 的参数:
      \[
      w_{\text{new}} \leftarrow w_{\text{now}} - \alpha \cdot \delta_j \cdot g_j.
      \]

智能体收集数据、更新 DQN 参数这两者可以同时进行。可以在智能体每执行一个动作之后,对 $w$ 做几次更新。也可以在每完成一局游戏之后,对 $w$ 做几次更新。

Introduction

本节介绍 Value Iteration, Policy Iteration 和 Truncated Policy Iteration. 这三种办法都是解决 Bellman Optimal Equation 的方法,而且都是在 Model-Based 的情况下的算法。

Value Iteration

下面给出 Value Iteration 的基本流程:

  1. 给定 State Value 的初始值$v_c(s)$
  2. 进入循环:

    1. Policy Update
      基于上一轮更新的 State Value, 选定最优策略(贪心策略),即:
      \[
      \begin{align}
      \pi_{k+1} = \mathrm{arg} \max\limits_{\pi}(r_{\pi} + \gamma P_{\pi} v_k) \\
      \begin{cases}
      \pi_{k+1}(a|s) = 1, a = a_k^*(s) \\
      \pi_{k+1}(a|s) = 0, a \neq a_k^*(s) \\
      \end{cases}
      \end{align}
      \]
      式中,$a_k^*(s) = \mathrm{arg} \max\limits_{a}q_k(a,s)$.
    2. Value Update
      在新的 Policy 基础上,更新 State Value,即:
      \[
      v_{k+1} = r_{\pi_{k+1}} + \gamma P_{\pi_{k+1}} v_{k}
      \]

      由于我们采用了贪心策略,因此,上式其实就是:
      \[
      v_{k+1} = \max \limits_{a} q(a,s)
      \]

    3. Check
      检查是否满足 $||v_{k+1} - v_{k}|| < \delta $, 其中 $\delta$ 是一个事先指定的很小的正数。如果满足,则退出循环,求解完毕。如果不满足,则继续循环。

Policy Iteration

下面给出 Policy Iteration 的基本流程:

  1. 给定 Policy 的初始值 $\pi_0$
  2. 进入循环:
    1. Policy Evaluation
      Policy Evaluation 基于上一轮更新的 Policy 解 Bellman Equation.
      \[
      v_{\pi_k} = r_{\pi_{k}} + \gamma P_{\pi_{k}} v_{\pi_k}
      \]
      一个有趣的问题:此处如何解 Bellman Equation? 方法就是在第2节中所展示的迭代方法,即:
      \[
      v_{\pi_k}^{(j+1)} = r_{\pi} + \gamma P_{\pi}v_{\pi_k}^{(j)}
      \]
      将这个迭代方法进行很多次,直至 $||v_{\pi_k}^{(j+1)} - v_{\pi_k}^{(j)}|| < \epsilon $, $\epsilon$ 是一个事先指定的很小的正数。
    2. Check
      检查是否满足 $||v_{\pi_k} - v_{\pi_{k-1}}|| < \delta $, 其中 $\delta$ 是一个事先指定的很小的正数。
      1. 如果此条件满足,则退出循环。
      2. 如果此条件不满足,则进行 Policy Improvement.
        基于求解的 State Value 更新 Policy.
        \[
        \begin{align}
        \pi_{k+1} = \mathrm{arg} \max\limits_{\pi}(r_{\pi} + \gamma P_{\pi} v_{\pi_k}) \\
        \begin{cases}
        \pi_{k+1}(a|s) = 1 \ \ \ a = a_{\pi_k}^*(s) \\
        \pi_{k+1}(a|s) = 0 \ \ \ a \neq a_{\pi_k}^*(s) \\
        \end{cases}
        \end{align}
        \]

Truncated Policy Iteration

通过观察可以发现,Policy Iteration 和 Value Iteration 的区别在于,Value Iteration 每次更新 State Value 时,只根据 Bellman Equation 迭代了一次,而 Policy Iteration 从理论上迭代了无穷多次,求出了 Bellman Equation 的一个精确解。因此,我们可以设想有这样一种办法,它每次根据 Bellman Equation 迭代了有穷多次(不妨记为$j$次),这就是 Truncated Policy Iteration, 如同将 Policy Iteration 截断。 Value Iteration 和 Policy Iteration 可以分别视为 Truncated Policy Iteration 的两个特殊情况。

下面给出 Truncated Policy Iteration 的基本流程:

  1. 给定 Policy 的初始值 $\pi_0$
  2. 进入循环:
    1. Policy Evaluation
      Policy Evaluation 基于上一轮更新的 Policy 解 Bellman Equation.
      \[
      v_{\pi_k} = r_{\pi_{k}} + \gamma P_{\pi_{k}} v_{\pi_k}
      \]
      设定$j = 0$, 进入子循环,检查是否有$j \leq j_{max}$:
      1. 如果$j \leq j_{max}$, 则继续迭代:
        \[
        v_{\pi_k}^{(j+1)} = r_{\pi} + \gamma P_{\pi}v_{\pi_k}^{(j)}
        \]
        并且更新$j$值:$j = j+1$
      2. 如果$j > j_{max}$ 则退出子循环,最终:$v_{\pi_k}= v_{\pi_k}^{j_{max}} $.
    2. Check
      检查是否满足 $||v_{\pi_k} - v_{\pi_{k-1}}|| < \delta $, 其中 $\delta$ 是一个事先指定的很小的正数。
      1. 如果此条件满足,则退出循环。
      2. 如果此条件不满足,则进行 Policy Improvement.
        基于求解的 State Value 更新 Policy.
        \[
        \pi_{k+1} = \mathrm{arg} \max\limits_{\pi}(r_{\pi} + \gamma P_{\pi} v_{\pi_k})
        \]
        根据贪心策略:
        \[
        \begin{align}
        \begin{cases}
        \pi_{k+1}(a|s) = 1, a = a_{\pi_k}^*(s) \\
        \pi_{k+1}(a|s) = 0, a \neq a_{\pi_k}^*(s) \\
        \end{cases}
        \end{align}
        \]

观察 Truncated Policy Iteration 的流程,我们可能有这样的疑问:State Value 的最大迭代次数 $j_{max}$ 到底取多少比较合适呢?

Policy Evaluation

Bellman Optimal Equation 是在 Bellman Equation 的基础上演变而来。Bellman Equation 阐述了在一只策略和环境的情况下,State Value 所满足的公式。有了State Value之后可以做什么呢?Policy Evaluation! (毕竟强化学习的目标是寻找最优的策略)

下面定义什么是好的策略,如果对于两个策略$\pi_{1}$, $\pi_{2}$ ,下面的式子恒成立:

\[
v_{\pi_1}(s) > v_{\pi_2}(s), \forall s \in \mathcal{S}
\]

则称$\pi_{1}$是比$\pi_{2}$更优的策略。强化学习的目标是寻找最优的策略,而最优的策略又依赖于选择最合适的状态作为下一次迁移的目标(依赖于State Value),这就需要把 State Value 和 Policy 同时优化,这就是 Bellman Optimal Equation.

Bellman Optimal Equation

Bellman Equation 如下:

\[
\begin{align}
v_{\pi}(s)
& = \sum \limits_{a}^{} \pi(a | s) \left[\sum \limits_{r} r \cdot p(r | s, a) + \gamma \sum \limits_{s’} v_{\pi}(s’) p(s’|s, a)\right] \\
\end{align}
\]

Bellman Optimal Equation 如下:

\[
\begin{align}
v_{\pi}(s)
& = \max \limits_{\pi} \sum \limits_{a}^{} \pi(a | s) \left[\sum \limits_{r} r \cdot p(r | s, a) + \gamma \sum \limits_{s’} v_{\pi}(s’) p(s’|s, a)\right] \\
& = \max \limits_{\pi} \sum \limits_{a}^{} \pi(a | s) \cdot q(s, a) \\
\end{align}
\]

其实就是进行了一步优化,它表达的是在最优策略下的 State Value 之间的关系。这里面有两个未知量,一个是策略$\pi$,一个是 State Value, 现在要对这两个未知量同时优化

首先要解决的是如果已知 Action Value, 如何找到最合适的策略的问题,这就是一个简单的线性规划问题。这里我们以 3 种 Action 的情况为例。

\[
\begin{align}
\max\limits_{\pi(a_i | s)} \pi(a_1 | s)q(s, a_1) + \pi(a_2 | s)q(s, a_2) + \pi(a_3 | s)q(s, a_3) \\
\mathrm{s.t.} \ \pi(a_1 | s) + \pi(a_2 | s) + \pi(a_3 | s) = 1
\end{align}
\]

如果 Action Value 给定,且已知:

\[
q(s, a_1) \geq q(s, a_2) \geq q(s, a_3)
\]

那么,显然最优解在 $\pi(a_1 | s) = 1$, $\pi(a_j | s) = 0 (j = 2,3) $ 处取到。因此我们把这种办法(其实是一种deterministic policy)称为 Greedy Policy. 即:

\[
\begin{align}
\begin{cases}
\pi(a|s) = 1 \ \ (a = a^*) \\
\pi(a|s) = 0 \ \ (a \neq a^*) \\
\end{cases}
\end{align}
\]

式中,$a^*= \mathrm{arg} \max \limits_{a} q(s,a) $,那么 Bellman Optimal Equation 可以重新写作:

\[
\begin{align}
v_{\pi}(s)
& = \max\limits_{a \in \mathcal{A}} q(s, a) \\
& = q(s, a^*) \\
\end{align}
\]

Motivating Example

Calculating return is important to evaluate a policy. 因此,为了比较以下三个策略,利用Discounted Return,很容易分析得,第一个策略是最好的。
alt text

对于第三个策略的Return,其实是一种期望。这可以作为理解Bellman Equation的一个引子。

Bootstrapping

Bootstrapping是一种计算Discounted Return的办法。设置一个较为简单的循环policy作为演示:
alt text

根据Discounted Return的定义,可以列出:
\[
\begin{align}
v_1 = r_1 + \gamma r_2 + \gamma^2 r_3 + \dots \\
v_2 = r_2 + \gamma r_3 + \gamma^2 r_4 + \dots \\
v_3 = r_3 + \gamma r_4 + \gamma^2 r_1 + \dots \\
v_4 = r_4 + \gamma r_1 + \gamma^2 r_2 + \dots \\
\end{align}
\]
仔细观察之后,可以发现有一种整体替代的办法,把无穷项化为有限项:
\[
\begin{align}
v_1 = r_1 + \gamma v_2 \\
v_2 = r_2 + \gamma v_3 \\
v_3 = r_3 + \gamma v_4 \\
v_4 = r_4 + \gamma v_1 \\
\end{align}
\]
这种办法可以用矩阵表达,如下:
\[
\begin{pmatrix}
v_1 \\
v_2 \\
v_3 \\
v_4 \\
\end{pmatrix}
=\begin{pmatrix}
r_1 \\
r_2 \\
r_3 \\
r_4 \\
\end{pmatrix}+\gamma
\begin{pmatrix}
0 & 1 & 0 & 0 \\
0 & 0 & 1 & 0 \\
0 & 0 & 0 & 1 \\
1 & 0 & 0 & 0 \\
\end{pmatrix}
\begin{pmatrix}
v_1 \\
v_2 \\
v_3 \\
v_4 \\
\end{pmatrix}
\]
也可以写作:
\[
\textbf{v} = \textbf{r} + \gamma \textbf{Pv}
\]
这其实就是对于deterministic cases的Bellman Equation,它的解为:
\[
\textbf{v} = (\textbf{I} - \gamma \textbf{P})^{-1}\textbf{r}
\]
这种整体替代的办法就是Bootstrapping,其核心就是每一个state的Discounted Return依赖于其他state的Discounted Return。

State Value

这一小节当中,我们要从deterministic cases过渡到stochastic cases中去,对于这两种情况,强化学习的各种概念的含义是保持一致的,只是每一个定量都变为了随机变量。而相应的,Discounted Return的概念要过渡到State Value,这是强化学习最核心的概念之一。

Notations

把RL中的定量重新定义成随机变量,考虑MDP中的一步:
\[
S_t \xrightarrow{A_t} R_{t+1}, S_{t+1}
\]

  • $t$, $t+1$表示离散化的时间。
  • $S_t$ 表示时间$t$时,agent的状态。
  • $A_t$ 表示时间$t$时,agent的所采取的行动。
  • $R_{t+1}$ 表示时间$t$时,agent的状态从$S_t$迁移至$S_{t+1}$所获得的奖励,有时也写作$R_{t}$.
    注意,$S_t$, $A_t$, $R_{t+1}$均是随机变量。这一步演化过程受概率分布的控制。
  • $ S_t \xrightarrow{} A_{t} $ is governed by $\pi(A_t = a | S_t = s)$
  • $ S_t, A_{t} \xrightarrow{} R_{t+1} $ is governed by $p(R_{t+1} = r | S_t = s, A{t} = a)$
  • $ S_t, A_{t} \xrightarrow{} S_{t+1} $ is governed by $p(S_{t+1} = s’ | S_t = s, A{t} = a)$

这里有一个很有趣的问题,既然演化过程中所采取的行动、下一步的状态、所获得奖励都受概率分布的控制,那么我们为什么不统一地用$p(\dots | \dots)$表达,而对于策略专门用了$\pi$表示呢?笔者思考了一下,认为决定的所采取的行动的策略($\pi(A_t = a | S_t = s)$)从本质上不同于$p(R_{t+1} = r | S_t = s, A{t} = a)$, $p(S_{t+1} = s’ | S_t = s, A{t} = a)$. 后两者描述的是agent采取行动之后环境与agent的交互情况,这实际上是取决于环境本身(比如grid-world当中网格的构造,哪里是target area,哪里是forbidden area)以及人们基于训练目标所设定的激励政策。这些内容可能在一定程度上是固定的(比如一个trajectory中),虽然其分布有可能知道,也有可能不知道(后面会解释)。而策略$\pi$则完全由agent学习而得,在学习过程中应当是不断变化的,才能取得进步。

Definition of State Value

考虑一个trajectory:
\[
S_t \xrightarrow{A_t} R_{t+1}, S_{t+1} \xrightarrow{A_{t+1}} R_{t+2}, S_{t+2} \xrightarrow{A_{t+2}} R_{t+3}, S_{t+3} \dots
\]
Discounted Return 是:
\[
G_t = R_{t+1} + \gamma R_{t+2} + \gamma^2 R_{t+3} + \gamma^3 R_{t+4} + \dots
\]
显然,$R_{i}$是一个随机变量,那么$G_t$也是一个随机变量。而它的期望就被定义为在状态$s$下,基于策略$\pi$的State Value: $v_{\pi}(s)$.
\[
v_{\pi}(s) = \mathbb{E}[G_t | S_t = s]
\]

  • State Value 是关于状态$s$的函数,评价了一个状态的价值。
  • State Value 也是策略$\pi$的函数,它的评价是基于目前选择的这个策略的。状态给定的情况下,基于某一个策略得到的State Value更高,证明这个策略更好,follow这条策略更有可能得到更高的return.
  • Discounted Return 和 State Value 的关系:对于 deterministic cases, Discounted Return 和 State Value 没有区别,而对于 stochastic cases, State Value 就是 Discounted Return 的期望。

Establishment of Bellman Equation

由上一小节,Discounted Return 是:
\[
G_t = R_{t+1} + \gamma R_{t+2} + \gamma^2 R_{t+3} + \gamma^3 R_{t+4} + \dots
\]
使用Bootstrapping方法,可得:
\[
G_t = R_{t+1} + \gamma G_{t+1}
\]
再结合State Value的定义,可得:
\[
\begin{align}
v_{\pi}(s) & = \mathbb{E}[G_t | S_t = s] \\
& = \mathbb{E}[R_{t+1} + \gamma G_{t+1} | S_t = s] \\
& = \mathbb{E}[R_{t+1} | S_t = s] + \gamma \mathbb{E}[G_{t+1}| S_t = s]\\
\end{align}
\]
下面分别计算:
\[
\begin{align}
\mathbb{E}[R_{t+1} | S_t = s] = \sum \limits_{a}^{} \pi(a | s) \sum \limits_{r} r \cdot p(r | s, a)
\end{align}
\]
\[
\begin{align}
\mathbb{E}[G_{t+1}| S_t = s] & = \sum \limits_{s’} \mathbb{E}[G_{t+1}| S_t = s, S_{t+1} = s’] p(s’|s) \\
& = \sum \limits_{s’} \mathbb{E}[G_{t+1}| S_{t+1} = s’] p(s’|s) \\
& = \sum \limits_{s’} v_{\pi}(s’) p(s’|s) \\
& = \sum \limits_{s’} v_{\pi}(s’) \sum \limits_{a} p(s’|s, a) \pi(a|s) \\
\end{align}
\]
因此:
\[
\begin{align}
v_{\pi}(s) & = \mathbb{E}[R_{t+1} | S_t = s] + \gamma \mathbb{E}[G_{t+1}| S_t = s]\\
& = \sum \limits_{a}^{} \pi(a | s) \sum \limits_{r} r \cdot p(r | s, a) + \gamma \sum \limits_{a} \pi(a|s) \sum \limits_{s’} v_{\pi}(s’) p(s’|s, a) \\
& = \sum \limits_{a}^{} \pi(a | s) \left[\sum \limits_{r} r \cdot p(r | s, a) + \gamma \sum \limits_{s’} v_{\pi}(s’) p(s’|s, a)\right] \\
\end{align}
\]
这就得到了Bellman Equation

  • 对于每一个状态均可以写出来一个Bellman Equation,因此,这一组方程不同状态的State Value之间的关系,即Bootstrapping方法。

  • Bellman Equation是基于策略$\pi(a|s)$的,因此,求解Bellman Equation又被称作policy evaluation.

  • $p(s’|s, a), p(r | s, a)$这两项指的是我们对于agent处于某一状态下,采取了某一行动之后,所获得奖励和所迁移到的下一状态的概率分布。我们对这个概率分布是否完全掌握,从本质上反映了我们对于与agent交互的环境是否有完全的了解(即Environment Model是否建立)。如果对所处的环境没有完全掌握,agent只能通过不断试验,观测所处的环境。从这一判断标准出发,强化学习可分为Model-Based RL和Model-Free RL.

Matrix-Vector Form of the Bellman Equation

Rewrite the Bellman Equation in Matrix-Vector Form

对Bellman Equation作如下代换:
\[
\begin{align}
r_{\pi}(s) = \sum \limits_{a} \pi(a|s) \sum \limits_{r} p(r|s,a) \cdot r \\
p_{\pi}(s’ | s) = \sum \limits_{a} \pi(a|s) p(s’|s,a)
\end{align}
\]

式中,$p_{\pi}(s’ | s)$是follow策略$\pi$下,从状态$s$迁移到$s’$的概率;$r_{\pi}(s)$是follow策略$\pi$下,状态$s$这一步得到奖励的期望。因此,可以将Bellman Equation写成:
\[
v_{\pi}(s_i) = r_{\pi}(s_i) + \gamma \sum \limits_{s_j} p_{\pi}(s_j | s_i) v_{\pi}(s_j)
\]

写成矩阵形式为:
\[
\textbf{v}_{\pi} = \textbf{r}_{\pi} + \gamma \textbf{P}_{\pi}\textbf{v}_{\pi}
\]

式中,$[\textbf{P}_{\pi}]_{ij} = p_{\pi}(s_j | s_i)$,被称为State Transition Matrix.

Solution of the Bellman Equation

从数学上,Bellman Equation的解可写作:
\[
\textbf{v}_{\pi} = (\textbf{I} - \gamma \textbf{P}_{\pi}) ^{-1} \textbf{r}_{\pi}
\]

而实际上,由于$\textbf{v}_{\pi}$常常维数很高,上面的逆矩阵很难用计算机求解,我们常采用迭代的方法求解:
\[
\textbf{v}_{k+1} = \textbf{r}_{\pi} + \gamma \textbf{P}_{\pi}\textbf{v}_{k}
\]

从数学上,可以证明这种迭代的方法下,最终会收敛到Bellman Equation的解(证明略)。

Basic Concepts

A grid-world example

  • State: The status of the agent with respect to the environment.
    • 在grid-world示例中,agent的位置就是state.
  • State Space: 所有state构成的一个集合就是State Space.
  • Action: For each state, there are some possible actions.
    • 在grid-world示例中,agent每一步可采取的行动(如向上走、向下走)就是action.
  • Action Space:所有action构成的一个集合就是Action Space.
    Action Space与当前的state有关。
  • State Transition: When taking an action, the agent may move from one state to another. Such a process is called state transition.
    \[
    s_1 \xrightarrow{a_2} s_2
    \]
  • Tabular reprentation: 用表格的形式表达状态转移,但是只能表达deterministic cases。
  • State transition probability: 用条件概率才能表达stochastic cases.
    \[
    p(s_2 | s_1, a_2) = 0.4 \\
    p(s_1 | s_1, a_2) = 0.6
    \]

  • Policy: Policy tells the agent what actions to take at a state.

    • 基于policy可以生成trajectory.
    • 仍然用条件概率表达stochastic policy.
      \[
      \pi(a_1 | s_1) = 0.5
      \]
    • Tabular representation of a policy 既可以描述deterministic policy,也可以描述stochastic policy.
    • 编程时用随机数描述概率的问题。
  • Reward: A real number we get after taking an action.
    • 正的reward表示鼓励,负的reward表示惩罚。
    • Reward可以理解为人机交互的一个接口。
    • Tabular representation of a reward 只能描述deterministic reward.
      \[
      p(r = -1 | s_1, a_1) = 1
      \]
    • 努力学习应该会获得奖励,但是奖励的多少是不一定的。
    • 鼓励行动,而非鼓励下一个状态。(注重过程而非结果)
  • Trajectory: A trajectory is a state-action-reward chain.
    \[
    s_1 \xrightarrow{a_2, r=0} s_2 \xrightarrow{a_3, r=0} s_5 \xrightarrow{a_3, r=0} s_8 \xrightarrow{a_2, r=1} s_9
    \]
  • Return: 在一个trajectory中所有reward的和即为return.
    • 用于评估一个策略的好坏。
  • Discounted Return:
    \[
    \mathrm{Discounted\ Return} = \Sigma \gamma^{i}r_i
    \]
    • $\gamma$ 被称为Discounted Rate, $\gamma$ 减少,可能会更加近视;$\gamma$ 增加,可能会较为远视。
  • Episode: When interacting with the environment following a policy, the agent may stop at some terminal states. The resulting trajectory is called an episode(or a trial).
    • Episode is a finite trajectory.(有限的任务被称为episodic tasks,永远进行下去的任务是continuing tasks)
    • 有两种将episodic tasks转化为continuing tasks的办法:
      • 第一种设置terminal state的action space只有留在原地,且奖励始终为0.
      • 第二种设置terminal state仍然是一个正常的state, 仍然有可能再离开terminal state, 再次进入时会获得正的奖励。
      • 第二种方法更加鼓励积极的探索,学习效果更好。

Markov Decision Process (MDP)

Key Elements

  • Sets:
    • Set of States: $\mathcal{S}$
    • Set of Actions: $\mathcal{A}(s)$ is associated for state $s\in \mathcal{S}$.
    • Set of Rewards: $\mathcal{R}(s,a)$
  • Probability Distribution:
    • State transition probability: $p(s’|s,a)$
    • Reward Probability: $p(r |s,a)$
  • Policy: $\pi(a|s)$
  • Markov Property: memoryless property
    \[
    \begin{align}
    p(s_{t+1}|a_{t+1}, s_{t},\dots,a_1,s_0) = p(s_{t+1}|a_{t+1}, s_{t}) \\
    p(r_{t+1}|a_{t+1}, s_{t},\dots,a_1,s_0) = p(r_{t+1}|a_{t+1}, s_{t})
    \end{align}
    \]

项目背景

在视频内容分析领域,我们常常需要从长视频中快速定位特定场景或画面。传统方法依赖于人工逐帧查看或基于元数据的简单搜索,效率低下且无法应对复杂语义查询。CLIP(Contrastive Language-Image Pretraining)模型的出现为这一领域带来了新的可能性,它能够理解图像与文本之间的语义关联。

笔者基于HuggingFace的CLIP模型开发了智能视频帧匹配系统,该系统可实现:

  1. 自动解析视频文件(支持MP4/AVI/MOV等常见格式)
  2. 实时计算文本描述与视频帧的语义相似度
  3. 智能定位最佳匹配帧并支持可视化预览
  4. 一键保存关键帧为JPG图片

开发思路

本地模型加载模块

考虑到实际部署时的网络稳定性问题,系统采用本地化模型加载方案:

  • 使用transformers库加载预训练的CLIP模型(clip-vit-base-patch32)
  • 设置本地模型缓存路径(E:\clip),包含完整的模型文件:
    1
    2
    LOCAL_MODEL_DIR = r"E:\clip"
    model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32").to(device)
  • 异常处理机制:模型加载失败时,提示缺失文件清单

多模态编码模块

文本编码

文本编码是将用户输入的英文描述转化为高维语义向量的过程。CLIP模型使用Transformer架构对文本进行编码,具体步骤如下:

  • 文本预处理:将用户输入的英文描述(如 “a waitress standing in front of a restaurant”)进行分词和标准化处理。
  • Tokenization:将文本转换为模型可理解的Token序列,每个Token对应一个唯一的ID。
  • 特征提取:通过多层Transformer编码器,将Token序列映射为固定长度的语义向量(本项目中维数为512)。
1
2
inputs = processor(text=texts, return_tensors="pt", padding=True).to(device)
text_features = model.get_text_features(**inputs)

图像编码

图像编码是将视频帧转化为视觉特征向量的过程。CLIP模型使用Vision Transformer(ViT)架构对图像进行编码,具体步骤如下:

  • 图像预处理:将视频帧从BGR格式转换为RGB格式,并调整大小为模型输入要求。
  • 分块处理:将图像划分为多个小块(Patch),每个小块作为一个输入单元。
  • 特征提取:通过多层Transformer编码器,将图像块序列映射为固定长度的视觉特征向量(本项目中维数为512)。
1
2
inputs = processor(images=[pil_image], return_tensors="pt", padding=True).to(device)
image_features = model.get_image_features(**inputs)

相似度计算

相似度是衡量文本描述与视频帧之间语义匹配程度的关键步骤。CLIP模型通过以下方式实现:

  • 向量对齐:将文本特征向量和图像特征向量映射到同一语义空间。
  • 余弦相似度:计算两个向量的余弦相似度,公式如下:
    alt text
  • 匹配分数:相似度值范围在[-1, 1]之间,值越接近1表示匹配度越高。

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import torch
import numpy as np
from PIL import Image
import cv2
from transformers import CLIPModel, CLIPProcessor

LOCAL_MODEL_DIR = input("请输入你的CLIP模型路径:")

device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Using device: {device}")


try:
model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32").to(device)
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
except Exception as e:
print(f"加载模型失败,请检查本地文件是否完整: {e}")
print(
"必须包含以下文件: config.json, pytorch_model.bin, preprocessor_config.json, tokenizer_config.json, vocab.json, merges.txt")
exit()

video_path = input("请输入你的视频文件路径:")
output_frame_path =input("请输入你的最佳匹配帧保存路径:")

user_text = input("请输入一段英文描述(例如:a waitress standing in front of a restaurant):")
if not user_text.strip():
print("输入不能为空!")
exit()

texts = [user_text]

cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
print(f"无法打开视频文件: {video_path}")
exit()

best_match_score = -1
best_match_frame = None
best_match_text = ""


frame_count = 0
while True:
ret, frame = cap.read()
if not ret:
break

frame_count += 1
print(f"Processing frame {frame_count}...")

frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
pil_image = Image.fromarray(frame_rgb)

try:
inputs = processor(
text=texts,
images=[pil_image],
return_tensors="pt",
padding=True
).to(device)
except RuntimeError as e:
print(f"预处理失败: {e}")
continue

with torch.no_grad():
try:
outputs = model(**inputs)
except RuntimeError as e:
print(f"推理错误: {e}")
continue

logits_per_image = outputs.logits_per_image.cuda().numpy()

if len(texts) == 1:
current_match_score = logits_per_image[0][0]
else:
probs = np.exp(logits_per_image) / np.sum(np.exp(logits_per_image), axis=1, keepdims=True)
current_match_score = probs[0][0]

current_match_text = texts[0]

if current_match_score > best_match_score:
best_match_score = current_match_score
best_match_frame = frame
best_match_text = current_match_text
print(f"更新最佳匹配: '{best_match_text}' (分数: {best_match_score:.4f})")

cap.release()

if best_match_frame is not None:
print(f"\n最佳匹配描述: '{best_match_text}' (分数: {best_match_score:.4f})")
cv2.imshow("Best Matching Frame", best_match_frame)
cv2.waitKey(0)
cv2.destroyAllWindows()

save_frame = input("是否保存最佳匹配帧?(y/n): ").strip().lower()
if save_frame == 'y':
cv2.imwrite(output_frame_path, best_match_frame)
print(f"最佳匹配帧已保存到: {output_frame_path}")
else:
print("未找到匹配的帧。")

使用说明

环境要求:

  • 请安装Python 3.8+
  • 安装依赖库:torch, transformers,opencv-python,pillow
  • 如希望利用显卡进行加速计算,请安装CUDA和CUDNN

准备步骤:

  • 下载CLIP模型文件至本地目录
  • 准备一个您要处理的视频,并确保视频文件路径不包含中文
  • 准备一个保存最佳匹配帧的路径

输出结果示例

笔者以本人所在学院的宣传片为例,其中有一段是游泳的片段,笔者尝试利用该程序找到游泳的片段。

首先按照要求输入CLIP模型的地址、视频地址和导出图片的地址,然后系统开始运行。
alt text

等到处理至游泳片段后,匹配分数不断刷新。
alt text

程序运行完毕,最匹配的那一帧会自动弹出。
alt text
alt text

键盘输入y之后,图片自动保存在设置的路径中。
alt text

技术优势

  • 多模态对齐:CLIP模型通过对比学习实现了文本和图像在语义空间的对齐。
  • 高效计算:利用GPU加速,实时处理视频帧并计算匹配分数。
  • 语义理解:能够捕捉复杂的语义关系,而不仅仅是简单的关键词匹配。

备注

本项目是笔者在学习了一点大模型技术后,利用不到半天时间开发的小型应用。这只是使用多模态大模型的一个小练习,且市场上已经有一些不错的产品实现了本项目的功能(如百度网盘自带的“云一朵”助理)。

这篇文档会先在笔者的个人博客上发布,稍后会整理并发布在我的Github上,欢迎读者在Github社区提交issue讨论改进方案。