一、卷积的信号处理原理
1.1 卷积为什么有效
1.2 卷积的起源:信号处理
虽然我们说卷积神经网络(CNN)是受到哺乳动物视觉神经系统的启发,但卷积(convolution)这个词和概念本身来自于信号处理(signal processing)。可以认为:信号处理这门课虽然没有上过,但在我们这里所需要用到的概念其实极少。
1.3 线性时不变系统(LTI System)
在信号处理系统中,卷积描述的是这样一个过程:我有一个输入信号 $X[n]$($n$ 表示时刻),通过一个系统(system),输出另一个信号 $Y[n]$。这个系统完成了两个信号之间的映射。
如果这个系统满足所谓的线性时不变(Linear Time-Invariant, LTI)条件,那么输出信号可以写成:
\[Y[n] = \sum_{m=-\infty}^{+\infty} X[m] \cdot H[n - m]\]这里有两个关键性质:
- 线性(Linear):$Y[n]$ 是 $X$ 信号的某种加权组合——$H$ 对 $X$ 信号进行加权,这种加权形式就是线性的。
- 时不变(Time-Invariant):这种转换关系完全由一组 $H$ 信号所决定,与时刻 $n$ 无关。也就是说,在不同的时刻上,信号的转换关系都不发生变化。
总结来看,Convolution 这件事情实际上最早就是在离散信号处理的线性时不变系统中提出的。
1.4 卷积公式详解
为什么有负号($n - m$)
卷积公式中 $H[n-m]$ 的减号意味着要将卷积核 $H$ 翻转过来,然后在对应位置相乘求和。
用具体例子说明:假设我要计算第10个时刻的响应 $Y[10]$,当 $m=1$ 时,$H[10-1] = H[9]$;当 $m=2$ 时,$H[10-2] = H[8]$。所以信号 $X[1]$ 和 $H[9]$ 做点积,$X[2]$ 和 $H[8]$ 做点积——相当于把 $H$ 信号翻转过来,再在对应的位置相乘。
为什么要用减号? 减号主要是在理论分析上的一个方便性。只有写成减号,两个信号的卷积才满足交换律(commutativity):
\[X * H = H * X\]同时也满足结合律。这使得理论推导更加方便。
为什么求和从负无穷到正无穷
从负无穷到正无穷只是一个数学理论上的通用形式,这样可以表示任何长度的信号。但实际上信号是有限的——只要在某些位置上把信号取0,求和就自然截断了。
从离散到连续
如果是连续信号,求和变成积分:
\[Y(t) = \int_{-\infty}^{+\infty} X(\tau) \cdot H(t - \tau) \, d\tau\]注意:看到这种连续的符号不用怕,它不过是离散形式的连续版本——把点积的求和变成了积分。
1.5 卷积的直觉理解:模板匹配
用一个动画例子来解释卷积的直觉含义:
假设卷积核是一个正方形信号,输入信号也是一段蓝色的信号。卷积的过程就是:卷积核从一端开始,与输入信号对齐,算出积分值,然后不断滑动,每次移过去时算出对应时刻两个信号卷积后的结果。
关键观察:两个信号完全重合时,积分值(响应)最大。
- 当两个信号完全不重叠时,点积为零,积分面积为零;
- 当部分重叠时,有一定的积分面积;
- 当完全重合时,响应达到最大值。
核心直觉:卷积核其实就是一个模板,它在信号中滑动,寻找与模板形状最像的部分。两个信号完全重合时响应最大。
1.6 音叉比喻与滤波效果
用一个非常形象的比喻来说明卷积的滤波(filter)效果:
想象你在一个房间里放了一个音叉(固定频率)。你在房间里弹钢琴或者演奏任何乐器,乐器的信号就好像输入信号。当你演奏的信号和音叉的频率一致的时候,就会产生共鸣——这就是响应最大的情况。
也就是说,这个固定频率的音叉只会对你弹奏音乐中与它频率一致的部分产生响应。这就产生了滤波的效果——跟它不像的信号被滤掉,与它更像的信号被保留下来。
所以卷积核有时候也被称为 filter(滤波器),这就是”滤波”这个词的来源。
1.7 神经网络中的”卷积”实际是互相关
在神经网络的卷积操作中,公式变成了:
\[Y[n] = \sum_{m} X[m] \cdot H[n + m]\]注意这里是加号而不是减号。这意味着不需要翻转卷积核,直接在对应位置相乘求和。
严格来说,神经网络中的”卷积”并不是数学意义上的卷积,而是互相关(cross-correlation)。因为真正的卷积要求有减号以满足交换律。但”卷积”这个叫法在行业中已经约定俗成了,不再深究,只需知道它在数学上是不严谨的。
1.8 互相关 = 内积 = 相似度度量
互相关的操作本质上就是:用一组卷积核在图像输入的不同位置上,与对应区域进行逐元素相乘再求和——这就是两个向量的内积(dot product / inner product)。
两个向量的内积可以表示为:
\[\mathbf{a} \cdot \mathbf{b} = |\mathbf{a}| \cdot |\mathbf{b}| \cdot \cos\theta\]其中 $\theta$ 是两个向量之间的夹角。这实际上就是一个相似度度量——当两个向量方向越接近($\cos\theta$ 越大),内积越大。
所以互相关本质上衡量的是:卷积核模板与对应区域的相似程度。
1.9 Padding 的作用
Padding(填充)是在特征图的外围补零(或其他值),它有两个重要作用:
- 保持输入输出特征图尺寸一致:做完卷积后,输出的特征图不会缩小。
- 避免边缘信号被忽略:如果不加 padding,边缘位置的像素参与卷积的次数明显少于中间位置的像素,导致边缘信息被相对忽视。
早期最早的卷积神经网络还没有提出 padding 的概念,是后来慢慢发展才引入的。
二、图像卷积效果与多通道卷积
2.1 三个经典卷积核的效果(灰度图示例)
将一张原始的 RGB 彩色图转为灰度图,分别用三种卷积核进行操作:
(1)边缘检测核(Edge Detection Kernel)
\[\begin{bmatrix} -1 & -1 & -1 \\ -1 & 8 & -1 \\ -1 & -1 & -1 \end{bmatrix}\]原理:中间位置分配了最大的权重 8,周围八个位置都是 $-1$。
- 如果卷积核滑过一个平滑区域(中心像素与周围像素值相同),则 $8 \times v - 8 \times v = 0$,不产生响应——平滑区域被 cancel 掉。
- 如果卷积核滑过一个边缘(中心像素与周围像素差异大),则周围的 $-1$ 无法抵消中间 $8$ 的贡献,边缘信号的强度被放大。
所以这个核能够提取图像的轮廓信息。
(2)锐化核(Sharpening Kernel)
\[\begin{bmatrix} 0 & -1 & 0 \\ -1 & 5 & -1 \\ 0 & -1 & 0 \end{bmatrix} = \underbrace{\begin{bmatrix} 0 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 0 \end{bmatrix}}_{\text{恒等算子}} + \underbrace{\begin{bmatrix} 0 & -1 & 0 \\ -1 & 4 & -1 \\ 0 & -1 & 0 \end{bmatrix}}_{\text{拉普拉斯算子}}\]锐化核可以分解为两个算子的叠加:
- 恒等算子(Identity Operator):保留原信号不变。
- 拉普拉斯算子(Laplacian Operator):检测信号的变化。
效果分析:
- 在平滑区域:拉普拉斯算子的中心 4 与四个 $-1$ 正好抵消,结果为零。此时只剩恒等算子,保留原信号。
- 在变化剧烈的区域(如从暗到亮的过渡):拉普拉斯算子无法抵消,会增强中心像素与周围像素的差异,使得亮的更亮、暗的更暗——即增强对比度。
所以锐化核的效果是:在保留原有信号的基础上,加强对比度,使图像更加清晰。这就是它叫”锐化”而不叫”边缘检测”的原因——它不像边缘检测核那样把中间的权重放得极大(8),而是在保留原信号的同时增强边缘对比。
(3)模糊核(Blur Kernel)
模糊核本质上是对窗口内的像素进行加权平均,结果就是图像变得更加模糊。这很好理解——平均化操作抹平了像素之间的差异。
2.2 算子的概念
补充一个数学概念:
算子(Operator) 是在原空间上进行的线性变换,即变换前后维度不变。如果变换后维度发生了变化(比如从5维变成10维或2维),那就是一般意义上的线性变换(linear transformation),而不叫算子。
2.3 信号处理 vs 神经网络:卷积核的关键区别
这三个卷积核的例子只是为了说明卷积的工作原理。在实际应用中,有一个至关重要的区别:
| 信号处理 | 神经网络 | |
|---|---|---|
| 卷积核 | 人为指定的(手工设计) | 可学习的参数(通过训练自动优化) |
在信号处理中,卷积核是人根据需求设计的模板;而在神经网络中,卷积核的参数是通过反向传播自动学习得到的——这是最核心的区别。
2.4 多通道卷积
输入多通道的情况
一般来讲,输入图像有多个通道(如 RGB 三通道)。那么多通道情况下如何做卷积?
通道数 vs 特征图数量
一个容易混淆的概念:
- 输入侧:强调的是通道数(channel number)
- 输出侧:强调的是特征图数量(number of feature maps)
二者本质上是可以互换的:本层输出的特征图数量 = 下一层输入的通道数。
产生一张特征图的过程
要产生一张特征图:
- 对输入的每个通道,各用一个对应的卷积核分别做卷积
- 将所有通道的卷积结果相加
- 再加上一个 bias(偏置项)
为什么要相加:不同的通道反映了原图不同的语义特征。在得到一个新的特征图时,我们当然要融合不同通道所捕捉到的各个侧面的语义信息,这样的卷积结果才是有用的。
因此,多通道卷积的卷积核不是一个二维矩阵,而是一个张量(tensor),其形状为:
\[\text{卷积核形状} = \text{输入通道数} \times \text{核高度} \times \text{核宽度}\]参数量计算
一层卷积的参数量:
\[\text{参数量} = \text{输出特征图数} \times (\text{输入通道数} \times \text{核大小} + 1)\]其中 $+1$ 是因为每个卷积核还带有一个 bias 参数。
举例:如果输入有3个通道,卷积核大小为 $3 \times 3$,要产生一张特征图,则参数量为 $3 \times 3 \times 3 + 1 = 28$。
Tensor 的 shape 问题
在代码中,张量的 shape 通常是四维的:
\[(B, C, H, W)\]分别表示 batch size、通道数、高度、宽度。
注意:不同的深度学习框架中,这些维度的排列顺序是不一样的!有的框架 batch size 在最后一位,有的 channel 在中间。千万不要搞错了——如果恰好两个维度的数值相同,搞错了甚至不会报错,但结果完全是错的。
整个深度学习在看代码的时候,最容易弄错的就是 tensor 的 shape。
三、激活函数、池化操作与LeNet-5
3.1 卷积层的三个连续操作
卷积层并不是单独一个操作,而是三个操作连续发生:
- 卷积(仿射变换):严格来说不是线性变换,因为要加一个bias,加上bias之后就是仿射变换
- 非线性激活函数:对卷积结果施加非线性变换
- 池化(Pooling)操作:下采样,降低分辨率
3.2 Sigmoid函数的两个缺点
早期非线性激活函数主要使用sigmoid,因为sigmoid有生物学基础。但sigmoid有两个关键缺点:
缺点一:非零对称(Non-zero Centered)
这个东西其他教科书上不一定说得那么清楚。
如果上一层神经元用sigmoid进行输出,那么上一层的输出都会呈现出0到1之间的值,它们都是恒正的。
详细推导:当梯度传播到某个神经元时,该神经元指向下一层的每条权重的梯度计算公式为:
\[\frac{\partial L}{\partial w_i} = \delta \cdot x_i\]其中 $\delta$ 是从后面传来的误差项,$x_i$ 是上一层的输出。由于经过sigmoid后 $x_i$ 都是正的,所以这一组权重的梯度符号完全由 $\delta$ 决定——要么全正,要么全负。
这意味着:
- 同一层所有权重在一次调整中要么全都按正方向调整,要么全都按负方向调整
- 严重限制了模型的优化能力
- 训练过程呈现zig-zag震荡:要么在第一象限调整,要么在第三象限调整
你想想就知道了,这完全限制了模型调整的能力。如果有些权重可以向正的方向调整,有些权重可以向负的方向调整,这个调节能力肯定会更强。
缺点二:饱和区导致梯度消失
sigmoid函数很容易落入饱和区(saturation region)。当输入的绝对值大于约3.75~4时:
- 输出接近0或接近1(都是常数)
- 对常数求导数当然是0
- 网络就调不动了——这就是梯度消失
所以注意,sigmoid有两个不足:第一,非零对称导致同一层权重要么都为正要么都为负,严重限制优化效率;第二,很容易进入饱和区。
3.3 Tanh:解决了一个问题,保留了一个问题
为了克服第一个问题,引入了tanh函数。tanh的输出有正有负,解决了非零对称问题。
但tanh仍然保留了sigmoid的一个缺点——饱和区:
- 理论上绝对值超过3就进入饱和区
- 有效建模范围比sigmoid还要更窄
3.4 ReLU与Normalization:完整的解决方案
后来大部分架构把sigmoid和tanh都干掉了,留下了ReLU。
ReLU的优势:在大于0的区域基本是线性的,所以不存在饱和区。只是它会把小于零的部分全部置零。
Leaky ReLU其实意义不大。有观点认为Leaky ReLU把零点往下移,使输出有正有负。但后来发明了一个关键技术——Normalization:
- Layer Normalization 或 Batch Normalization:对这一层输出的多个样本,先减均值、除方差
- 减均值除方差之后,输出就正负都有了
有时候,我用ReLU以后好像性能也不咋地。那是因为ReLU仍然是非中心化的。只有配合Layer Normalization或Batch Normalization,才能配合ReLU解决我们刚才那两个问题。
“ReLU + BN/LN 才是完整方案”:
- ReLU使得右侧(大于0区域)没有饱和区
- BN/LN使得输出有正有负,不会导致权重只能同方向调整的问题
所以后面什么Leaky ReLU其实不用管,因为Leaky ReLU无非就是想让输出有正有负,但后面的Layer Normalization或Batch Normalization已经能做到这一点了。
3.5 池化(Pooling)与卷积的两个区别
经过卷积和非线性激活后,最后一步就是Pooling。Pooling和卷积有两个关键区别:
区别一:Pooling不加padding
为什么卷积外面要加padding,pooling外面不加padding?
原因是pooling本身就是一个下采样(降低分辨率)操作。你已经在降低分辨率了,还给它增加外面的边缘?这不是跟下采样的目的违背吗?
我本来就是要进行特征下采样,你还给我添堵。
区别二:Pooling的stride = kernel size
卷积的stride一般取1(滑动窗口一步一步走),但pooling的stride跟pooling的kernel size是一样的。比如pooling size是2x2,stride就是2。
原因也是下采样——这样才能有效快速地降低分辨率。比如4x4的特征图经过2x2的pooling后直接降为2x2。
3.6 三种Pooling的比较
实际中pooling有三种:Max Pooling、Average Pooling、Min Pooling。
Min Pooling不可行:配合ReLU会发生什么?ReLU把负值全部置零,特征图上只有0和正值。取min的话,全部都是0——看到的都是背景信息,完全无用。
Average Pooling被淘汰:Average Pooling相当于一个模糊滤波器,把本来非常鲜明的信号给平滑掉了。我本来就要提取非常鲜明的信号,你给我做平均把它淡化了。
Max Pooling胜出:提取区域内最显著的特征(most salient),这才是我们想要的。
历史上有很多方案,但我们走着走着,技术就开始收敛到我们真正想做的事情上了。
3.7 平移等变性与局部平移不变性
这里有两个重要概念必须区分清楚:
卷积:平移等变性(Translation Equivariance)
常有人误以为卷积是平移不变性,注意,卷积是平移等变性!
如果要捕捉的特征在输入中出现在位置A,它会在特征图的对应位置被捕捉到;如果特征平移到了位置B,它在特征图中也会做相同的平移。特征在输入中平移,在特征图中同样平移——这叫等变性,而不是不变性。
比如你要捕捉一只猫的脸,猫脸出现在这里和出现在那里,都会被同一个卷积核捕捉到。
Max Pooling:局部平移不变性(Local Translation Invariance)
如果要捕捉的信号仍然在max pooling窗口范围之内,即使它在窗口内发生了平移,提取的特征保持不变。
这意味着:输入图像中的轻微扰动是可以对抗的。图像中如果在局部范围内有一些变动,提取的信号不变——这对模型的泛化能力有极强的作用。
二者结合
这两个加起来,一定程度上使得卷积神经网络既能够提取有效的特征,又能够对输入的图像有一定的鲁棒性。
3.8 卷积 vs 全连接:参数量的巨大优势
卷积的平移等变性同时带来一个重要好处——极其节约参数。
全连接的问题:如果用全连接网络实现同样的功能,你想捕捉的特征可能出现在输入视野中的任何位置。这就要求全连接的学习权重在每个位置上都要有一组重复的参数去捕捉同样的特征。参数量 = 需要识别的特征数量 $\times$ 可能出现的各个位置——组合爆炸。
卷积的解决方案:同一个卷积核在整个特征空间上滑动,不管特征出现在任何位置都能被捕捉到。不需要在所有位置上重复学习相同参数,所以参数量非常小。
3.9 LeNet-5详解
LeNet-5是最早成名的卷积神经网络。以下是其结构详解(经过了现代改进):
网络结构
| 层 | 操作 | 输出尺寸 | 说明 |
|---|---|---|---|
| 输入 | - | 32x32x1 | 灰度图 |
| 卷积1 | 5x5卷积,无padding | 28x28x6 | 6个特征图 |
| 池化1 | 2x2 max pooling | 14x14x6 | 下采样 |
| 卷积2 | 5x5卷积 | 10x10x16 | 16个特征图 |
| 池化2 | 2x2 max pooling | 5x5x16 | 下采样 |
| 展平(Flatten) | - | 400维向量 | 16x5x5=400 |
| 全连接1 | FC | 120维 | - |
| 全连接2 | FC | 84维 | - |
| 输出层 | Softmax | 10类 | 分类 |
为什么28x28?
当时padding还没有流行。5x5卷积核需要在四周各补 $(5-1)/2=2$ 个像素才能保持尺寸不变。不补的话,前后总共失掉4个像素:$32 - 4 = 28$。
参数量计算
第一层卷积:
- 输入通道数1,卷积核5x5,加1个bias
- 每个特征图的参数:$1 \times 5 \times 5 + 1 = 26$
- 共6个特征图:$26 \times 6 = 156$ 个参数
- 相比全连接(32x32映射到28x28),参数量减少非常明显
第二层卷积:
- 输入通道数6,卷积核5x5,加1个bias
- 每个特征图的参数:$6 \times 5 \times 5 + 1 = 151$
- 共16个特征图:$151 \times 16 = 2416$ 个参数
池化层:不需要额外参数,只是一个最大/最小/平均操作。
注意,卷积层是三组配套的——卷积、非线性变换、池化,这是一组。所以这个网络的主干只有两套”卷积+池化”。
展平层到全连接:
- 400维降到120维:$400 \times 120 + 120 = 48120$ 个参数
- 120维降到84维,84维降到10维
输出与训练
- 在原始LeNet-5中,最后一层使用高斯连接(Gaussian Connection),后来不再使用
- 现代做法:最后加一个softmax,把logits变成概率分布
- 整个网络用交叉熵损失函数
- 最后用反向梯度传播算法一次性训练
四、循环神经网络RNN的设计动机与公式
4.1 设计哲学
还是那句老话:简单知道它们的设计是没有用的。最重要的是它们为什么要这样设计,为什么技术会这么演化。这才是重要的。
4.2 从需求出发推导RNN
问题:设计一个神经网络,使其具有时序信号的处理能力。
如果要处理一个变长的时序信号,一下子能想到的是什么?
第一步:需要一个记忆单元
如果要处理时序,而且时序可能是变长的,我肯定需要一个向量去建模/融合我看到过的历史信息。这个向量就是隐变量(hidden variable) $H_t$,也可以理解为一个记忆单元(memory)。
第二步:确定依赖关系
$H_t$ 一定会依赖于两个东西:
- 前一个时刻积累的历史信息 $H_{t-1}$
- 当前的输入 $X_t$
设计循环神经网络的思路肯定是这样的:我通过更新一个记忆单元,这个记忆单元受到前一个时刻记忆的影响和当前输入的影响。
第三步:最简单的线性组合
按照神经网络的思路,最简单的方式:
\[H_t = W \cdot H_{t-1} + U \cdot X_t + b\]第四步:为什么必须加非线性?
这个线性变换不够。因为这个递推式会不断往前展开——随着时间步 $t$ 增大,$H_t$ 会不断代入前一步,展开后就变成了所有输入的线性组合,表达能力退化。
所以必须加上非线性函数:
\[H_t = \sigma(W \cdot H_{t-1} + U \cdot X_t + b)\]恭喜,这就是最原始的循环神经网络(Recurrent Neural Network)。
你一旦这么去思考这个问题——我要搞一个神经网络,它能建模比较长序列的数据;我一定会有一个记忆单元;然后最简单的更新方式加上非线性——你很容易就写出这样一个式子来。
4.3 RNN的图形理解
从图形上看RNN的信息流:
- $X_t$ 通过矩阵 $U$ 融入到新的记忆单元 $H_t$
- 上一步的记忆 $H_{t-1}$ 通过循环连接(矩阵 $V$/$W$)更新到新的 $H_t$
- 当然要经过一个非线性变换
4.4 输出与损失函数
输出:每个时刻的输出
\[O_t = W_{out} \cdot H_t\]损失函数:假设是拟合问题,每个时刻的loss为
\[L_t = (O_t - Y_t)^2\]但有一点要注意:网络不一定在每个时刻上都有ground truth引导信号。有可能某些时刻有输出标签,某些时刻没有。
假设通用情况下每个时刻都有loss,则整个网络的总loss为:
\[L = \sum_{t=1}^{T} L_t\]4.5 可训练参数
RNN中可以调的参数有四组:
- $U$:输入到隐状态的权重矩阵
- $W$(或 $V$):隐状态到隐状态的循环权重矩阵
- $W_{out}$:隐状态到输出的权重矩阵
- $b$:偏置项
4.6 反向传播(BPTT)与梯度消失/爆炸
训练RNN需要求整个loss $L$ 对每个参数的偏导数。关键难点在于按时间展开求导。
核心推导:
设 $H_t = \sigma(Z_t)$,其中 $Z_t = W \cdot H_{t-1} + U \cdot X_t + b$ 是净输入(net input)。
当要求 $\frac{\partial L}{\partial H_{t-1}}$ 时:
- 先对非线性函数求导:$\sigma’(Z_t)$
- 再乘以循环权重 $W$
这是后面理解LSTM为什么会工作的关键要素。
如果多步向前推导(从时刻 $t$ 推到时刻 $t-K$),梯度形成连乘:
\[\frac{\partial H_t}{\partial H_{t-K}} = \prod_{k=1}^{K} \sigma'(Z_{t-k+1}) \cdot W\]为什么会有梯度问题?
关键在于:所有时刻共享同一组参数(不同时刻的 $U$、$W$、$b$ 都是一样的)。
每一步递归都是:先过非线性函数求导得到 $\sigma’$,再乘以 $W$。连乘很多步后:
- 如果 $|\sigma’ \cdot W|$ 的norm大于1 → 梯度爆炸
- 如果 $|\sigma’ \cdot W|$ 的norm小于1 → 梯度消失
特别是 $\sigma’$(sigmoid或tanh的导数)的scale本身就小于1,多步连乘后梯度迅速消失。
这个连乘式非常让人头痛——序列越长,梯度要么变成0,要么爆炸。这就是传统RNN的根本困难,也是后续LSTM等架构要解决的核心问题。
五、LSTM 与 GRU
5.1 Vanilla RNN 的训练与梯度问题
回顾传统循环神经网络(Vanilla RNN)的核心公式:
\[h_t = \sigma(V \cdot h_{t-1} + U \cdot x_t + b)\]其中 $V$ 是循环权重矩阵,$U$ 是输入权重矩阵,$\sigma$ 是非线性激活函数(如 sigmoid 或 tanh)。每个时刻的输出为:
\[\hat{y}_t = W \cdot h_t\]训练时,定义每个时刻的损失:
\[L_t = \| \hat{y}_t - y_t \|^2\]总损失为所有时刻之和:
\[L = \sum_{t=1}^{T} L_t\]关键:不是每个时刻都有 ground truth 引导信号,有些时刻可能没有输出目标。
可调参数包括 $U$、$V$、$W$、$b$ 四项,使用反向传播算法(BPTT,沿时间展开的反向传播)求梯度。
5.2 梯度消失与爆炸的根源
求梯度时,由于 $h_t$ 是 $h_{t-1}$ 的复合函数(先线性变换,再非线性变换),链式法则展开后得到连乘形式:
\[\frac{\partial h_t}{\partial h_{t-k}} = \prod_{i=t-k+1}^{t} \sigma'(z_i) \cdot V\]关键问题:$\sigma’$ 的值始终小于 1(对于 sigmoid,导数最大为 0.25),多个小于 1 的数相乘,连乘很多次后必然趋近于 0 – 这就是梯度消失。反之,如果矩阵 $V$ 的 norm 大于 1,则连乘后梯度会爆炸。
总结:传统 RNN 只能建模大约 10 步左右的相关性,更远的信息由于梯度衰减就”什么都忘了”,参数调不过去,信息就丢失了。
5.3 梯度爆炸的解决:Gradient Clipping
梯度爆炸相对好解决 – 使用 gradient clipping(梯度裁剪):
当梯度值超过阈值时,将其裁剪到如 $[-5, 5]$ 的范围内。这是工程上的常用技术。
5.4 梯度消失的解决思路:走向 LSTM
梯度消失才是真正的难题。问题根源在于:
- 非线性函数与权重矩阵耦合:$h_t = \sigma(V \cdot h_{t-1} + \ldots)$ 中,$\sigma$ 和 $V$ 缠绕在一起
- 前后 memory 之间的信息流通像”弯弯曲曲的水管”
比喻:传统 RNN 的 memory 之间,信息要经过线性变换 + 非线性变换,就好像水管里面一拐一拐的。水在弯弯曲曲的管子里流过很多弯以后,水流量就流光了 – 这就是梯度消失。
关键 insight:让下一时刻的记忆与上一时刻的记忆之间保持线性关系(用加号,而非复合函数):
\[C_t = \alpha \cdot C_{t-1} + \text{新输入的内容}\]加号的关键性:当对 $C_{t-1}$ 求偏导时,只剩下系数 $\alpha$,不再经过非线性函数!这就像把弯弯曲曲的水管换成了高速公路 – 信号可以直接流过去。
5.5 LSTM 的三个门控机制
LSTM(Long Short-Term Memory)引入了记忆单元(memory cell)$C_t$ 和三个门控:
1. Forget Gate(遗忘门)$f_t$:决定上一时刻记忆中哪些可以被忘掉、哪些保留
\[f_t = \sigma(W_f \cdot [h_{t-1}, x_t] + b_f)\]2. Input Gate(输入门)$i_t$:决定当前输入的信息中哪些可以融入新的 memory,哪些是噪声直接干掉
\[i_t = \sigma(W_i \cdot [h_{t-1}, x_t] + b_i)\]3. Output Gate(输出门)$o_t$:决定 memory 中哪些信息用于当前时刻的预测
\[o_t = \sigma(W_o \cdot [h_{t-1}, x_t] + b_o)\]三个门的公式几乎一样:都是输入 $x_t$ 和上一层隐层 $h_{t-1}$ 的线性变换加偏置,再过一个 sigmoid 函数。用 sigmoid 是因为门控值需要在 $[0, 1]$ 之间 – 0 表示”不放”,1 表示”完全放”。
5.6 LSTM 记忆单元更新的关键公式
\[C_t = f_t \odot C_{t-1} + i_t \odot \tilde{C}_t\]其中 $\odot$ 是 element-wise 乘法(对应元素相乘),$\tilde{C}t = \tanh(W_c \cdot [h{t-1}, x_t] + b_c)$ 是候选记忆。
隐层输出为:
\[h_t = o_t \odot \tanh(C_t)\]加号是关键:这个加号表明下一时刻的 memory 和上一时刻的 memory 之间是门控与 memory 的点乘形式。在梯度链式法则中,对 $C_{t-1}$ 求偏导时,剩下的就是门控 $f_t$ – 不再经过非线性函数的导数。
梯度的连乘变成了门控值的连乘:
\[\frac{\partial C_t}{\partial C_{t-k}} = \prod_{i} f_i\]- 门控 = 1:上一时刻的重要信息全部留在下一个 memory 中(高速公路全开,信号直接流过去)
- 门控 = 0:直接截断(导数也截断,不会产生无意义的衰减)
对比传统 RNN:sigmoid 导数每次产生一个 0 到 1 之间的小数,多个小数相乘一定衰减。而 LSTM 门控的连乘都是接近 0 或 1 的值,不会像 sigmoid 导数那样持续衰减。
5.7 Output Gate 的作用
举例:假设网络需要同时建模周期为 2 和周期为 5 的信号。memory 中同时保存着两种周期的信息。Output gate 的作用是:在需要预测周期为 2 的信号时,把相应信息放出去;在需要预测周期为 5 的信号时,把对应信息放出去。不是所有 memory 信息都对当前预测有效。
5.8 结合图示理解 LSTM
LSTM 内部结构可以这样对照公式理解:
- $C_{t-1}$(上一时刻的记忆)经过 forget gate $f_t$ 的筛选 $\rightarrow$ $f_t \odot C_{t-1}$
- 新输入经过 input gate $i_t$ 与候选记忆 $\tilde{C}_t$(tanh 产生)相乘 $\rightarrow$ $i_t \odot \tilde{C}_t$
- 两者相加得到新的记忆 $C_t$(这个加号是核心)
- $C_t$ 经过 $\tanh$ 后与 output gate $o_t$ 相乘 $\rightarrow$ 输出 $h_t$
5.9 LSTM 的建模能力
关键:LSTM 实践表明可以建模大概 100 步到数百步的数据相关性。再远的信息也无能为力了。传统 RNN 只有 10 步。但问题是:对于几千步甚至几万步的信息,LSTM 说白了还是用一个高维向量来压缩看到过的东西,不管从左往右扫还是从右往左扫,本质上就是这么个东西。
5.10 GRU:砍掉 Output Gate 的简化版 LSTM
GRU(Gated Recurrent Unit)就是把 LSTM 中的 output gate 砍掉的简化版本。
- 性能与 LSTM 基本相当
- 参数量更少,训练更快
注意:面试经常会问 GRU 是什么。记住:GRU = LSTM 去掉 output gate,性能差不多但参数更少。
六、双向 LSTM(BiLSTM / ELMo)与 Transformer 引入
6.1 RNN 总结
循环神经网络的本质:
- 在不同时间点展开同一个网络
- 所有时间点共享一套参数
- 在展开图上进行反向传播
总结:这个事情非常简单。一套参数在不同时刻展开,不同时刻共享同一个参数,然后在展开图上反向传播。LSTM 已经进入历史的洪流当中了,但我们不得不提它早期有一个影响深远的工作。
6.2 BiLSTM(ELMo)结构
ELMo(Embeddings from Language Models)使用两组 LSTM 构建双向结构:
- 一组 LSTM 从左往右扫:在每个位置上,输出压缩了从左边到当前位置的上下文信息
- 一组 LSTM 从右往左扫:在每个位置上,输出压缩了从右边到当前位置的上下文信息
- 对应位置拼接(concatenation):将两个方向的表征拼接起来
从左往右扫得到的是这个词受到前面词影响后的语义表示;从右往左扫得到的是受到后面词影响后的语义表示。两个拼起来,就相当于这个词在整个句子中受到左右两边影响后的上下文表征。
Stacking(堆叠多层)
可以将多层 BiLSTM 堆叠起来:
- 底层 LSTM:建模比较局部的(local)上下文依赖关系(如字母 $\rightarrow$ 单词级别)
- 高层 LSTM:建模越来越抽象的语义层次(如单词 $\rightarrow$ 句子级别语义)
举例:如果输入的是字母,第一层 LSTM 可以认为是把字母变成了单词的表征;第二层就把单词表征综合成上下文语义表征。每堆叠一层,语义的抽象层次越来越高。
训练方法:Masked Language Model
ELMo 启发了后来的 Masked Language Model(掩码语言模型):
- 对输入句子挖掉几个单词
- 在相应位置上,根据上下文去预测被挖掉的词
- 这些样本不需要人工标注 – 所有句子都可以拿来训练
ELMo 的历史贡献
关键:ELMo 为什么在历史上这么有名?因为它在以前只预训练词的 embedding 的基础上,还预训练了网络参数。这是思想上做出的非常重要的一步 – 预训练不仅得到 embedding,还得到整个网络的参数。
6.3 ELMo / BiLSTM 被淘汰的原因
原因一:数据依赖 $\rightarrow$ 无法并行
GPU 最重要的优势是并行计算。但 BiLSTM 存在严重的数据依赖性:
- 下一个位置的表征必须等前一个位置算完才能计算
- 不同位置的 embedding 上下文语义表征不可能通过 GPU 加速
- 再堆再多的 GPU,这个架构也不能加速训练
原话:Google 的 Transformer 设计者也强调,当时提出 Transformer 架构,最重要的就是适配当时的 GPU。
原因二:参数化压缩 $\rightarrow$ 长序列信息必然丢失
LSTM / RNN 的根本问题:用一个高维向量来压缩看到过的所有历史信息。
- LSTM 最多建模数百步的依赖
- 几千步的信息一定会被忘掉
- 本质上是一种”有损压缩”
6.4 引出 Transformer 的核心思想
关键问题:如果我要建模几千步甚至几万步的依赖,有什么好办法?
一句话回答:检索。
- 不压缩历史:所有前文信息都保留在那里,始终都在
- 直接检索:当需要某个 token 的上下文信息时,去前文中检索与当前 token 相关的信息
- 检索到的信息不会丢失 – 因为你不是在压缩,而是在查找
这才是后面神经网络发展最重要的一个思想突破。我不需要参数化去融合历史信息产生一个浓缩后的 summary,我在计算的时候就去检索前面的那些特征,哪些对我当前这个特征有帮助的,我就把它检索过来融进来就完了。
这就是 Self-Attention(自注意力机制)。
6.5 Self-Attention 的语义消歧作用
Self-Attention 的另一个重要作用是语义消歧 – 将上下文无关的表示变为上下文相关的表示:
举例(apple 的例子):假设 “apple” 在向量空间中有一个初始位置。apple 有两个意思:水果和苹果公司。
- 如果上下文出现了 “eat”(吃),通过 self-attention 把 “eat” 的部分信息融入,”apple” 的向量就会向水果方向偏移
- 如果上下文出现了 “iPhone”,”apple” 的向量就会向科技公司方向偏移
融入上下文向量本质上就是向量的加法 – 原始表征加上相关上下文的加权信息,使得向量在空间中朝对应语义方向移动。
总结:Self-Attention 这个设计其实并不稀奇,核心就是:通过检索把跟我相关的内容检索出来,这样长程信息就不会丢;检索完后通过加权方式把想要的信息融进来,修正当前的表示。在语言学中,就相当于从一个上下文无关的表示变成了上下文相关的表示。这才是 Transformer 最重要的设计。
七、QKV 机制详解
7.1 QKV 三个向量的含义
Transformer 的核心操作就是检索。每个输入 token 的 embedding 会生成三个向量:Q(Query)、K(Key)、V(Value)。
用字典检索比喻:你的上下文就是一本字典,字典里记录了出现了哪些上下文、每个上下文的表示是什么。
| 向量 | 含义 | 解释 |
|---|---|---|
| Q(Query) | 我去查别人时用什么东西查 | “你去上下文查的时候,你用什么东西去查” |
| K(Key) | 我被别人查时提供什么关键信息 | “你被别人查的时候,你提供什么东西让人家查”(类比 KV store) |
| V(Value) | 查出来的内容是什么 | “查出来是什么东西” |
关键:每个 token 的 embedding 都有对应的 Q、K、V 三个向量,它们的分工是不一样的。一个是我去查人家时用什么;一个是人家查我时我的关键信息是什么;另一个是查出来以后的内容是什么。
7.2 检索过程
以第 5 个 token 为例,前面有 4 个 token:
第一步:用 Q 与其他 token 的 K 做点积(内积)
\[\text{score}_i = Q_5 \cdot K_i \quad (i = 1, 2, 3, 4, 5)\]点积(内积)是衡量相似度/相关性最快的方式 – 本质上就是余弦距离去掉模长。点积越大,说明这个上下文 token 跟当前 token 越相关。
注意:自己也可以检索自己!因为形成上下文语义更新时,肯定要把自己的主要部分也扔进去,否则自己的信息就丢了。
第二步:对点积结果做 Softmax
\[\text{attn}_i = \text{softmax}(\text{score}_i) = \frac{e^{\text{score}_i}}{\sum_j e^{\text{score}_j}}\]举例:假设第 5 个 token 与前 4 个 token 做点积得到 $[2, 3, 4, 5]$。这些数值需要相互竞争重要性 – 通过 softmax 将其转化为概率分布。
Softmax 的结果相当于:对上下文中各 token 对当前 token 的语义贡献打了一个概率分布。比如:”你对我的影响大概是 20%,你对我是 10%,你对我 20%…”
第三步:用 Softmax 权重对 V 做加权平均
\[\text{output}_5 = \sum_i \text{attn}_i \cdot V_i\]得到更新后的上下文相关表示。
7.3 Scaling 的工程技巧
点积完后会除以维度的平方根 $\sqrt{d_k}$:
\[\text{score} = \frac{Q \cdot K^T}{\sqrt{d_k}}\]解释:两个高维向量做点积后值会非常大,后面要做 softmax(指数操作),如果值太大 softmax 会爆掉。所以点积完后除以维度的根号,防止 softmax 之前的值过大。这是个工程技巧。
7.4 Multi-Head Attention(多头注意力)
刚才的 QKV 操作可以做 $H$ 次,称为多头注意力机制:
解释:一个头只能捕捉一种语义信息。多个头就可以在很长的历史序列当中捕捉到多种语义信息。比如一次检索关注语法信息,另一次检索关注语义信息。头越多建模能力越强,当然复杂性也越高。
7.5 为什么叫 “Self”-Attention
因为是在一个句子内部自己对自己做 attention – 自己的词对其他词算注意力。
7.6 Attention 机制总结
总结的逻辑链:
- 要解决长程依赖问题
- 长程依赖最有效的方法是检索(而非压缩)
- 检索最有效的方法是点积(衡量相似度)
- 点积越大 $\rightarrow$ 注意力分配越多;点积越小 $\rightarrow$ 关注越少
- 点积的结果做 softmax 后作为加权系数,用于对 V 做加权求和
- 为了捕捉多侧面的语义信息 $\rightarrow$ 多头注意力