RNN

对于自然语言处理这种前后文之间有联系的任务,一般的前馈网络只能单独处理每个词,没有记忆(即无法联系前后文进行语义理解),即前馈网络处理不了 “序列依赖” 。

对于自然语言,在输入给模型前需要先将其转换为数值向量,共有两种方式:

  • 1-of-N Encoding(One-Hot编码):
    将可能出现的词都写入一个字典,如 {apple, bag, cat, dog, elephant}
    每个词对应一个维度,该词位置为 1,其余为 0,如apple = [1,0,0,0,0],bag = [0,1,0,0,0]
    这种方式虽然直观,但是词典大时向量极长,不适合作为模型的输入通常用作损失函数的标签。
    Dimension for “Other”:
    在One-Hot基础上的改进,因为One-Hot 编码只能处理词典里见过的词,遇到词典里没有的词(未登录词,OOV)One-Hot 就没法编码了。
    所以给One-Hot增加一个维度,遇到词典外的词直接把 Other 维度设为 1,其余为 0,如Gandalf = [0,0,0,0,0,1]
  • Word Hashing(词哈希):
    One-Hot 编码词典越大,向量维度越高(比如词典有 10 万个词,One-Hot 就是 10 万维),计算量爆炸。Word Hashing 不是精确识别是哪个词,而是用字符片段的特征来表征词,用字符级的小片段来描述单词,既解决维度爆炸,又能处理未登录词。通常作为模型输入使用。
    • 第一步,给单词加边界符,标记单词的开头和结尾。比如apple → <apple>
    • 第二步,把单词拆成字符级 n-gram(通常是 3-gram,即连续 3 个字符),例如
      <apple> → <ap, app, ppl, ple, le>,<Taipei> → <Ta, Tai, aip, ipe, pei, ei>
      (边界符<和>也属于一个字符)
    • 第三步,把这些 n-gram 映射到固定低维向量。Word Hashing 的 3-gram 词典是语料里实际见过的片段集合,而非所有字母组合的全集,所以实际上Word Hashing 维度通常在几千维,完全可控。
      假设 3-gram 词典只有 8 个维度:[<ap, app, <Ta, Tai, <Ga, Gan, <ar, arr]
      <apple>
      的向量:[1,1,0,0,0,0,0,0](只包含 <ap, app>
      <Taipei> 的向量:[0,0,1,1,0,0,0,0](只包含 <Ta, Tai>
      <arrive> 的向量:[0,0,0,0,0,0,1,1](只包含 <ar, arr>
      所以模型不是认Taipei这个词,而是认字符片段,只要片段相似,模型就会认为单词的角色相似。

在训练一个能处理序列的模型时,通过 Word Hashing 将输入单词进行编码再喂给模型。在序列标注 / 槽填充 / 语言建模等分类任务中,通常用 One-Hot 来作为标签值,如某槽填充任务中有 “time”,“destination”,“name”三个槽位,此模型处理某个句子时会通过计算将它认为正确的槽位输出,比如 “Beijing” 属于 “destination” ,则在处理后模型输出[0, 1, 0]。

输入一句自然语言,如“I would like to arrive Taipei on November 2”,Word Hashing将其中的每个词转换为对应的数字向量,然后依次作为模型输入向量来计算得到输出。对于前馈网络,每个词单独作为模型输入,最终输出只是单个词的标签,比如它只知道自己看到了 “Taibei” 而无法将它与上下文联系起来理解。
如果再输入一句自然语言,如“I would like to leave Taipei on November 2”,对于前馈网络来说,它依然只知道 “Taipei” 而不知道是“leave” 还是 “arrive”。

神经网络必须要有 “记忆” ,才能记住前面的词(arrive/leave),从而正确判断当前词的正确角色。RNN(Recurrent Neural Network,循环神经网络)的核心思路就是通过循环连接(把上一步的输出作为下一步的输入)让网络在处理当前输入时,能记住之前所有输入的信息,并把这些信息传递到下一步,让模型能建模序列里的上下文依赖,比如文本里的语序、时间序列里的先后关系。

RNN的具体实现方式如下图所示。假设输入向量只有x_{1},x_{2}两个特征,Neural_A1和Neural_B1拿到输入x_{1},x_{2}后计算得到两个输出a_{1},a_{2},RNN的创新点在于它不仅将a_{1},a_{2}作为Neural_A2和Neural_B2的输入继续沿神经网络前馈,并且还将Neural的输出a_{1},a_{2}存储起来​作为下一个向量\vec{x}输入神经网络时的输入。即下一个向量(如“Taipei”)输入神经网络时,神经网络的输入不仅有此向量的x_{1},x_{2}还有上一个向量计算得到的a_{1},a_{2}。由此可以得到RNN 的状态更新公式为:

这就是 RNN 的 “记忆” ,把历史信息压缩到状态向量里,供后续步骤使用。

示例演示:

假设所有的权重均等于 1 ,无偏置。初始时存储的a_{1},a_{2}均为0,即h_{0}=[0,0]

input sequence = \begin{bmatrix} 1 \\ 1 \end{bmatrix},\begin{bmatrix} 1\\ 1 \end{bmatrix},\begin{bmatrix} 2\\ 2 \end{bmatrix}
 

第一步输入x_{1}=[1,1]h_{1}=h_{0}+x_{1}=[0+1+1,0+1+1]=[2,2],将h_{1}=[2,2]储存,并继续前馈计算得到输出\vec{y}

第二步输入x_{2}=[1,1]h_{2}=h_{1}+x_{2}=[2+2+1+1,2+2+1+1]=[6,6],将h_{2}=[6,6]储存,并继续前馈计算得到输出\vec{y}

第三步输入x_{3}=[2,2]h_{3}=h_{2}+x_{3}=[6+6+2+2,6+6+2+2]=[16,16],将h_{3}=[16,16]储存,并继续前馈计算得到输出\vec{y}

最终得到输出序列

output sequence=\begin{bmatrix} 4 \\ 4 \end{bmatrix},\begin{bmatrix} 12\\ 12 \end{bmatrix},\begin{bmatrix} 32\\ 32 \end{bmatrix}。如果调换input sequence\begin{bmatrix} 1 \\ 1 \end{bmatrix},\begin{bmatrix} 1\\ 1 \end{bmatrix},\begin{bmatrix} 2\\ 2 \end{bmatrix}的位置,得到的输出序列就会不相同,也就体现了RNN可以处理序列依赖问题。

对于之前的例子,“arrive Taipei on November 2”在RNN模型中的过程如下。将词 “arrive” 输入RNN模型,存储中间Neural计算得到的a^{1}再作为下一步的输入,依此类推,将“Taipei”、“on”等依次输入同一个RNN模型,得到最终的输出y^{3},如果是不同的词语组合,则最终得到的输出值就不同,就可以区分不同的语义。

上述的RNN模型只有一个Hidden Layer(隐藏层),当然可以多加几个Hidden Layer将模型设计的更深。如下图所示,思路相同,将每一个Hidden Layer计算得到的输出值存储起来作为下一个时间点Hidden Layer的输入。

早期的RNN主要用两种:

上面主要介绍的是Elman Network,它是将Hidden Layer的输出存储起来作为下一个时间点同样位置的Hidden Layer的输入。而Jordan Network是将上一个时间点的模型输出y^{t}和下一个时间点的向量一起作为输入。

  • Elman Network: 
                                      
    • h_{t-1}:上一时间点Hidden Layer计算得到的值。
    • h_{t}:此时间点同样位置的Hidden Layer计算得到的值。其输入包括正常前馈的输入x_{t}h_{t-1}
  • Jordan Network:
                                       
    • y_{t-1}:上一时间点模型输出。
    • h_{t}:此时间点第一个隐藏层计算得到的值。其输入包括词向量x_{t}y_{t-1}

Elman Network循环路径更长,易梯度消失 / 爆炸。擅长捕捉输入序列内部的复杂依赖,适用于需要丰富上下文细节的任务,比如自然语言处理(理解句子语序、语义依赖)、序列标注(给每个词打标签)。

Jordan Network循环路径更短,梯度传播更稳定。擅长捕捉输出序列之间的时序依赖,适用于输出本身有强时序依赖的任务,比如时间序列预测(明天的销量依赖今天的销量)、文本生成(下一个词依赖上一个生成的词)。

激活函数的选择

 RNN使用 tanh(双曲正切) 做激活函数,tanh 是 S 型单调奇函数:

Sigmoid函数:

sigmoid(x)=\sigma (x)=\frac{1}{1+e^{-x}}

ReLU函数:

ReLU(x)=max(0,x)

tanh和Sigmoid函数之间的关系是:

tanh(x)=2\sigma (2x)-1

tanh的导数:

tanh'(x)=1-tanh^{2}(x)

  • 在 x=0导数最大为 1
  • |x|\rightarrow \infty时,函数值趋近于\pm 1,导数趋近于0

Sigmoid的导数:

\sigma '(x)=\sigma (x)(1-\sigma (x))

  • 在 x=0 处导数最大为 0.25
  • |x|\rightarrow \infty时,函数值趋近于1 或 0,导数趋近于0

ReLU的导数:

  • 输入为正数,则输出等于输入,导数为 1 
  • 输入为负数,则输出为 0 ,导数为 0


RNN之所以使用tanh,原因如下:

  • tanh 的导数最大值为 1(sigmoid 仅为 0.25),在循环梯度传递中,梯度衰减速度比 sigmoid 更温和,一定程度缓解梯度消失。
  • tanh输出范围 (−1,1),均值为 0,相比 sigmoid(输出 (0,1),均值 0.5),能减少数据分布偏移,让梯度传播更稳定。
  • ReLU不会像tanh一样将输出值压缩在(-1,1)之间。如果中的权重W_{h}大于1,那每一步都乘W_{h}会导致梯度指数级爆炸训练直接崩。
    并且ReLU 导数只有 0 和 1 两个可能,只要某一步 h_{t-1}\leqslant 0,导数直接变 0,直接导致梯度消失。

损失函数的选择

RNN 处理序列数据,损失函数是各时间步局部损失的累加或平均,具体形式由任务类型决定:
 

类别一:序列标注 / 槽填充 / 语言建模(分类任务,逐时间步输出类别)

这类任务中,每个时间步 t 输出类别概率分布(经 softmax 激活),使用交叉熵损失(Cross-Entropy)作为损失函数。单时间步损失(第 t 步):

L_{t}=-\sum_{c=1}^{C}y_{t,c}log\hat{y}_{t,c}

  • C:类别总数(如槽填充的 dest/time/other,或语言建模的词典大小)
  • y_{t,c}:第 t 步真实标签的 one-hot 编码(类别 c 为 1,其余为 0)
  • \hat{y}_{t,c}:模型在第 t 步对类别 c 的预测概率(softmax 输出)

同一个模型同一套参数对句子里每一个位置 t,都输出一个分类概率,并且对每一个位置 t,都算一次交叉熵损失 L_{t}

总损失为(序列长度为 T):

L=\sum_{t=1}^{T}L_{t}=-\sum_{t=1}^{T}\sum_{c=1}^{C}y_{t,c}log\hat{y}_{t,c}

训练时常用平均值稳定梯度:

L=\frac{1}{T}\sum_{t=1}^{T}L_{t}

用这个总损失反向传播来更新参数。

类别二:序列分类(整个序列对应一个类别,如情感分析)

只在最后一个时间步 T 输出类别概率,损失为:

L=-\sum_{c=1}^{C}y_{c}log\hat{y}_{T,c}


类别三:序列回归(如时间序列预测,输出连续值)

使用均方误差(MSE),总损失是各时间步误差的平方和 / 平均。

单时间步损失:

L_{t}=(y_{t}-\hat{y}_{t})^{2}

总损失:

L=\sum_{t=1}^{T}(y_{t}-\hat{y}_{t})^{2}

以分析句子 “I love AI” 为例,使用Elman Network来说明RNN的完整训练流程:

  1. 数据预处理:
    1. 分词:将句子 “I love AI”分成3 个时间步单词 → [I, love, AI]
    2. 词向量编码:将单词转为向量(使用Word Hashing编码),这里为了简便,直接将其简化为二维向量
      I → x_{1}=[1,0]
      love → x_{2}=[0,1]
      AI → x_{3}=[1,1]
    3. 输入:x_{1},x_{2},x_{3}
      待训练参数:W_{xh}输入→隐藏层权重
                            W_{hh}隐藏层→下一时间步隐藏层权重
                            W_{hy}隐藏层→输出权重
                            偏置b_{h},b_{y}
  2. 前向传播流程:
    RNN 是时序循环结构,每一步都会复用同一套参数,并把上一步的隐藏状态传给下一步。

    时间步总数 T=3,流程如下:

    1. 时间步 t=1(输入 I ):
      计算隐藏状态h_{1}=tanh(W_{xh}\cdot x_{1}+W_{hh}\cdot h_{0}+b_{h})
      计算输出y'_{1}=W_{hy}\cdot h_{1}+b_{y}
      计算损失L_{1}=loss(y'_{1},y_{1})

    2. 时间步 t=2(输入 Iove ):
      计算隐藏状态h_{2}=tanh(W_{xh}\cdot x_{2}+W_{hh}\cdot h_{1}+b_{h})
      计算输出y'_{2}=W_{hy}\cdot h_{2}+b_{y}
      计算损失L_{2}=loss(y'_{2},y_{2})

    3. 时间步 t=3(输入 AI ):
      计算隐藏状态h_{3}=tanh(W_{xh}\cdot x_{3}+W_{hh}\cdot h_{2}+b_{h})
      计算输出y'_{3}=W_{hy}\cdot h_{3}+b_{y}
      计算损失L_{3}=loss(y'_{3},y_{3})

    4. 总损失L_{total}=L_{1}+L_{2}+L_{3}

  3. BPTT反向传播:
    BPTT(Back Propagation Through Time)是 RNN 专用的反向传播算法,核心是把时序展开的 RNN 当成深度神经网络,从最后一步往第一步反向求导,更新共享参数。
    RNN前向传播公式: 
                                           
                                          
                                           

    1. t=3反向传播:
      输出层梯度\frac{\partial L_{3}}{\partial y'_{3}}
      隐藏层梯度\frac{\partial L_{3}}{\partial h_{3}}=\frac{\partial L_{3}}{\partial y'_{3}}\cdot \frac{\partial y'_{3}}{\partial h_{3}}=\frac{\partial L_{3}}{\partial y'_{3}}\cdot W_{hy}
      激活函数梯度(tanh)\frac{\partial L_{3}}{\partial h_{3_{pre}}}=\frac{\partial L_{3}}{\partial y'_{3}}\cdot \frac{\partial y'_{3}}{\partial h_{3}}\cdot \frac{\partial h_{3}}{\partial h_{3_{pre}}}=\frac{\partial L_{3}}{\partial y'_{3}}\cdot(1-tanh^{2}(h_{3}))
      最终得参数梯度
      \frac{\partial L_{3}}{\partial W_{xh}}=\frac{\partial L_{3}}{\partial y'_{3}}\cdot \frac{\partial y'_{3}}{\partial h_{3}}\cdot \frac{\partial h_{3}}{\partial h_{3_{pre}}}\cdot \frac{\partial h_{3_{pre}}}{\partial x_{3}}=\frac{\partial L_{3}}{\partial h_{3_{pre}}}\cdot x_{3}^{T}
      \frac{\partial L_{3}}{\partial W_{hh}}=\frac{\partial L_{3}}{\partial y'_{3}}\cdot \frac{\partial y'_{3}}{\partial h_{3}}\cdot \frac{\partial h_{3}}{\partial h_{3_{pre}}}\cdot \frac{\partial h_{3_{pre}}}{\partial W_{hh}}

    2. t=2反向传播:
      输出层梯度\frac{\partial L_{2}}{\partial y'_{2}}
      隐藏层梯度\frac{\partial L_{2}}{\partial h_{2}}=\frac{\partial L_{2}}{\partial y'_{2}}\cdot \frac{\partial y'_{2}}{\partial h_{2}}=\frac{\partial L_{2}}{\partial y'_{2}}\cdot W_{hy}
      激活函数梯度(tanh)\frac{\partial L_{2}}{\partial h_{2_{pre}}}=\frac{\partial L_{2}}{\partial y'_{2}}\cdot \frac{\partial y'_{2}}{\partial h_{2}}\cdot \frac{\partial h_{2}}{\partial h_{2_{pre}}}=\frac{\partial L_{2}}{\partial y'_{2}}\cdot(1-tanh^{2}(h_{2}))
      最终得参数梯度
      \frac{\partial L_{2}}{\partial W_{xh}}=\frac{\partial L}{\partial y'_{2}}\cdot \frac{\partial y'_{2}}{\partial h_{2}}\cdot \frac{\partial h_{2}}{\partial h_{2_{pre}}}\cdot \frac{\partial h_{2_{pre}}}{\partial x_{2}}=\frac{\partial L_{2}}{\partial h_{2_{pre}}}\cdot x_{2}^{T}
      \frac{\partial L_{2}}{\partial W_{hh}}=\frac{\partial L_{2}}{\partial y'_{2}}\cdot \frac{\partial y'_{2}}{\partial h_{2}}\cdot \frac{\partial h_{2}}{\partial h_{2_{pre}}}\cdot \frac{\partial h_{2_{pre}}}{\partial W_{hh}}

    3. t=1反向传播:
      输出层梯度\frac{\partial L_{2}}{\partial y'_{2}}
      隐藏层梯度\frac{\partial L_{1}}{\partial h_{1}}=\frac{\partial L_{1}}{\partial y'_{1}}\cdot \frac{\partial y'_{1}}{\partial h_{1}}=\frac{\partial L_{1}}{\partial y'_{1}}\cdot W_{hy}
      激活函数梯度(tanh)\frac{\partial L_{1}}{\partial h_{1_{pre}}}=\frac{\partial L_{1}}{\partial y'_{1}}\cdot \frac{\partial y'_{1}}{\partial h_{1}}\cdot \frac{\partial h_{1}}{\partial h_{1_{pre}}}=\frac{\partial L_{1}}{\partial y'_{1}}\cdot(1-tanh^{2}(h_{1}))
      最终得参数梯度
      \frac{\partial L_{1}}{\partial W_{xh}}=\frac{\partial L}{\partial y'_{1}}\cdot \frac{\partial y'_{1}}{\partial h_{1}}\cdot \frac{\partial h_{1}}{\partial h_{1_{pre}}}\cdot \frac{\partial h_{1_{pre}}}{\partial x_{1}}=\frac{\partial L_{1}}{\partial h_{1_{pre}}}\cdot x_{1}^{T}
      \frac{\partial L_{1}}{\partial W_{hh}}=\frac{\partial L_{1}}{\partial y'_{1}}\cdot \frac{\partial y'_{1}}{\partial h_{1}}\cdot \frac{\partial h_{1}}{\partial h_{1_{pre}}}\cdot \frac{\partial h_{1_{pre}}}{\partial W_{hh}}
    4. 所以总损失对参数的梯度:
      \frac{\partial L}{\partial W_{xh}}=\frac{\partial L_{1}}{\partial W_{xh}}+\frac{\partial L_{2}}{\partial W_{xh}}+\frac{\partial L_{3}}{\partial W_{xh}}=\sum_{t=1}^{3}\frac{\partial L_{t}}{\partial h_{t_{pre}}}\cdot x_{t}^{T}
      \frac{\partial L}{\partial W_{hh}}=\frac{\partial L_{1}}{\partial W_{hh}}+\frac{\partial L_{2}}{\partial W_{hh}}+\frac{\partial L_{3}}{\partial W_{hh}}
      \frac{\partial L}{\partial W_{hy}}=\sum_{t=1}^{3}\frac{\partial L_{t}}{\partial y'_{t}}\cdot h_{t}^{T}
  4. 参数更新:
    W=W-\eta \cdot \frac{\partial L}{\partial W}

RNN梯度消失 / 爆炸 的问题出现的原因:

下图展示了RNN模型的训练对比图。蓝色曲线是正常训练的损失曲线,随 Epoch 增加平稳下降并收敛,符合语言模型训练的预期规律;绿色曲线为异常训练的损失曲线,出现剧烈震荡、多次陡升陡降,最终在 Epoch14 处发生梯度爆炸(损失瞬间飙升至无穷大),训练完全崩溃。绿色曲线就是出现了RNN 训练中 BPTT 算法导致的梯度爆炸问题,这是 RNN最经典的训练缺陷。

下图是RNN模型参数与损失关系图(以两个参数为例)。展现了RNN 训练崩溃(梯度爆炸 / 消失)的几何本质,RNN训练时经常出现这种曲面局部极陡以及曲面局部极平的问题,从而导致训练崩溃。

之所以会出现这种局部极陡及局部极平的情况,是因为 RNN 在 BPTT 训练时 W_{hh}​ 的累乘特性导致损失曲面出现极端的非凸结构。具体解释如下:

在上面介绍分析句子 “I love AI”的示例时,我们在最后得出了总损失对参数的梯度,即

\frac{\partial L}{\partial W_{hh}}=\frac{\partial L_{1}}{\partial W_{hh}}+\frac{\partial L_{2}}{\partial W_{hh}}+\frac{\partial L_{3}}{\partial W_{hh}}

其中\frac{\partial L_{3}}{\partial W_{hh}}=\frac{\partial L_{3}}{\partial y'_{3}}\cdot \frac{\partial y'_{3}}{\partial h_{3}}\cdot \frac{\partial h_{3}}{\partial h_{3_{pre}}}\cdot \frac{\partial h_{3_{pre}}}{\partial W_{hh}}
\frac{\partial L_{2}}{\partial W_{hh}}=\frac{\partial L_{2}}{\partial y'_{2}}\cdot \frac{\partial y'_{2}}{\partial h_{2}}\cdot \frac{\partial h_{2}}{\partial h_{2_{pre}}}\cdot \frac{\partial h_{2_{pre}}}{\partial W_{hh}}

\frac{\partial L_{1}}{\partial W_{hh}}=\frac{\partial L_{1}}{\partial y'_{1}}\cdot \frac{\partial y'_{1}}{\partial h_{1}}\cdot \frac{\partial h_{1}}{\partial h_{1_{pre}}}\cdot \frac{\partial h_{1_{pre}}}{\partial W_{hh}}

\frac{\partial L_{t}}{\partial W_{hh}}=\frac{\partial L_{t}}{\partial y'_{t}}\cdot \frac{\partial y'_{t}}{\partial h_{t}}\cdot \frac{\partial h_{t}}{\partial h_{t_{pre}}}\cdot \frac{\partial h_{t_{pre}}}{\partial W_{hh}}

其中\frac{\partial L_{t}}{\partial y'_{t}},\frac{\partial y'_{t}}{\partial h_{t}}这两项可以通过损失函数和模型结构算出,\frac{\partial h}{\partial h_{t_{pre}}}就是激活函数tanh的导数,这也是已知的。累乘特性体现在\frac{\partial h_{t_{pre}}}{\partial W_{hh}}这一项中,之所以没有直接给出,是因为这是一个复杂的公式。下面我们只介绍它的累乘特性,不会给出详细的公式:

RNN隐藏状态公式是h_{t}=tanh(W_{xh}x_{t}+W_{hh}h_{t-1}+b_{h}),h_{t_{pre}}=W_{xh}x_{t}+W_{hh}h_{t-1}+b_{h},这意味着 h_{t} 依赖于 h_{t-1}​,进而依赖于 h_{t-2}​,形成一条 h_{0}\leftarrow h_{1}\leftarrow h_{2}\leftarrow \cdots \leftarrow h_{t}的链式依赖。对于\frac{\partial h_{3_{pre}}}{\partial W_{hh}},根据h_{t_{pre}}=W_{xh}x_{t}+W_{hh}h_{t-1}+b_{h},你可能会认为\frac{\partial h_{3_{pre}}}{\partial W_{hh}}=h_{2},但实际上这是一个嵌套公式,W_{hh}h_{2}看似是一个线性公式,实际上h_{2}=tanh(W_{xh}x_{t}+W_{hh}h_{1}+b_{h})仍是一个与W_{hh}的公式,同理h_{1}也仍是一个与W_{hh}有关的公式,如此嵌套下去,\frac{\partial h_{3_{pre}}}{\partial W_{hh}}=\frac{\partial h_{3_{pre}}}{\partial h_{2}}\cdot \frac{\partial h_{2}}{\partial h_{2_{pre}}}\cdot \frac{\partial h_{2_{pre}}}{\partial h_{1}}\cdot \frac{\partial h_{1}}{\partial h_{1_{pre}}}\cdot \frac{\partial h_{1_{pre}}}{\partial h_{0}}是一个很复杂的式子,其中\frac{\partial h_{t}}{\partial h_{t_{pre}}}是激活函数tanh的导数,每出现一次\frac{\partial h_{_t{pre}}}{\partial h_{t-1}}都会乘一次W_{hh},如果这个模型处理的时间步t足够大的话,累乘项会指数级放大 / 缩小梯度,直接导致梯度爆炸 / 消失。具体体现为:

  • \left | \left | W_{hh} \right | \right |>1:曲面局部极陡,引发梯度爆炸,训练崩溃。
  • \left | \left | W_{hh} \right | \right |<1:曲面局部极平,引发梯度消失,模型无法学习。

针对梯度爆炸问题,工程上常使用梯度裁剪(Gradient Clipping)的方法,核心思路是给梯度的最大模长设一个「刹车阈值」,一旦梯度超过这个阈值,就把它按比例剪到阈值以内,防止参数更新步长失控。主流的梯度裁剪方法包括范数裁剪(Norm Clipping)和值裁剪(Value Clipping),这里不做过多介绍。

关于其他解决方法,下面会一一介绍。

(选读)多维输入下 RNN梯度消失 / 爆炸 的问题出现的原因(矩阵版):


下面通过公式推导说明 普通 RNN 在处理长序列时会出现梯度消失 / 爆炸 的问题:

标准 RNN 前向传播公式:

h_{t}=tanh(W_{ih}x_{t}+W_{hh}h_{t-1}+b_{h})

y_{t}=W_{ho}h_{t}+b_{o}

L=\sum_{t=1}^{T}L_{t}(y_{t},\hat{y}_{t})

  • x_{t}:第 t 步输入
  • h_{t-1}:第 t-1 步Hidden Layer计算得到的值。
  • h_{t}:第 t 步同样位置的Hidden Layer计算得到的值。其输入包括正常前馈的输入x_{t}h_{t-1}
  • W_{ih} :输入→隐藏层权重
  • W_{hh}:隐藏→隐藏层循环权重
  • W_{ho}:隐藏→输出层权重

根据求导链式法则,要计算总损失 L 对循环权重 W_{hh} 的梯度需要先计算 L 对 h_{t}的导数:

\delta _{t}=\frac{\partial L}{\partial h_{t}} 

由于h_{t}会影响当前步输出y_{t},这将直接影响L_{t},除此之外还会影响后续所有隐藏状态h_{t+1},\cdots ,h_{T},间接影响L_{t+1},\cdots ,L_{T}。所以:

\delta _{t}=\frac{\partial L}{\partial h_{t}}=\frac{\partial L_{t}}{\partial y_{t}}\cdot \frac{\partial y_{t}}{\partial h_{t}}+\frac{\partial L_{t+1}}{\partial y_{t+1}}\cdot \frac{\partial y_{t+1}}{\partial h_{t+1}}\cdot \frac{\partial h_{t+1}}{\partial h_{t}}+\frac{\partial L_{t+2}}{\partial y_{t+2}}\cdot \frac{\partial y_{t+2}}{\partial h_{t+2}}\cdot \frac{\partial h_{t+2}}{\partial h_{t+1}}\cdot \frac{\partial h_{t+1}}{\partial h_{t}}+\cdots

写成递推形式,即:

  • \delta _{t+1}=\frac{\partial L}{\partial h_{t+1}}
  • 当 t=T 时,无后续步骤,故\delta _{T}=\frac{\partial L_{T}}{\partial y_{T}}\cdot W_{ho}^{T}

由式h_{t}=tanh(W_{ih}x_{t}+W_{hh}h_{t-1}+b_{h})得:

  • \bigodot表示两个同维度矩阵 / 向量,对应位置的元素分别相乘。tanh'(x)=1-tanh^{2}(x)=1-h_{t+1}\bigodot h_{t+1}
  • 对于多维输入输出,上式的\vec{h}_{t},\vec{h}_{t+1}均为D维向量,则\frac{\partial h_{t+1}}{\partial h_{t}}为 D\times D的矩阵,tanh'(x)=1-h_{t+1}\bigodot h_{t+1}D维向量,要和D\times DW_{hh}^{T}相乘。(W_{hh}^T)_{ij}\cdot (1-h_{t+1,j}^{2})就是W_{hh}^{T}的每一列都和 tanh' 向量的对应元素相乘。

梯度递推公式:

  • 这里为了方便表示,省略当前步梯度\frac{\partial L_{t}}{\partial h_{t}},只保留未来步回传,不影响核心结论

将上式递推展开(从 t=T 到t=k):

t=T:\, \, \, \delta _{T}=\frac{\partial L_{T}}{\partial h_{T}}

t=T:\, \, \, \delta _{T-1}=\delta _{T}\cdot \frac{\partial h_{T}}{\partial h_{T-1}}

t=T:\, \, \, \delta _{T-2}=\delta _{T-1}\cdot \frac{\partial h_{T-1}}{\partial h_{T-2}}=\delta _{T}\cdot \frac{\partial h_{T}}{\partial h_{T-1}}\cdot \frac{\partial h_{T-1}}{\partial h_{T-2}}

所以任意早期步 t=k :

\delta _{k}=\delta _{T}\cdot \prod_{i=k+1}^{T}\frac{\partial h_{i}}{\partial h_{i-1}}

将上述\frac{\partial h_{t+1}}{\partial h_{t}}代入得:

\delta _{k}=\delta _{T}\cdot \prod_{i=k+1}^{T}[W_{hh}^{T}\cdot (1-h_{i}\bigodot h_{i})]

总损失对 W_{hh} ​的梯度是所有时间步贡献之和:

\frac{\partial L}{\partial W_{hh}}=\sum_{t=1}^{T}\frac{\partial L_{t}}{\partial W_{hh}}=\sum_{t=1}^{T}\frac{\partial L_{t}}{\partial h_{t}}\cdot \frac{\partial h_{t}}{\partial W_{hh}}

  • \frac{\partial L_{t}}{\partial h_{t}}=\delta _{t}
  • \frac{\partial h_{t}}{\partial W_{hh}}是隐藏状态对循环权重的梯度,由前向公式求导得到。

求解\frac{\partial h_{t}}{\partial W_{hh}}:1

h_{t}=tanh(z_{t}),\frac{\partial h_{t}}{\partial W_{hh}}=\frac{\partial tanh(z_{t})}{\partial z_{t}}\cdot \frac{\partial z_{t}}{\partial W_{hh}}

tanh 的导数为逐元素运算:

\frac{\partial tanh(z_{t})}{\partial z_{t}}=diag(1-h_{t}\bigodot h_{t})

  • z_{t}=[z_{t,1},z_{t,2},\cdots ,z_{t,D}]^{T}是 D 维列向量。
  • h_{t}=tanh(z_{t})=[tanh(z_{t,1}),tanh(z_{t,2}),\cdots ,tanh(z_{t,D})]^{T}也是 D 维列向量
  • 两个列向量之间求偏导,得到的是雅可比矩阵(D × D 维),即 
                                         
  • 由于 tanh 是逐元素独立的激活函数,即h_{t,i}=tanh(z_{t,i})h_{t,i}仅由z_{t,i}决定,所以雅可比矩阵的非对角元全为0,仅对角元为 tanh 的导数: 
                                       
  • diag 的作用就是把向量转成对角矩阵。我们先算出tanh的导数向量:
                        d_{t}=1-h_{t}\bigodot h_{t}=[1-h_{t,1}^{2},1-h_{t,2}^{2},\cdots ,1-h_{t,D}^{2}]^{T}
    diag(d_{t})就是将向量 d_{t} 放在矩阵对角线上,其余位置补 0,得到的 D\times D 对角矩阵,才能和后续的 W_{hh}^{T} 做矩阵乘法。:
                            

z_{t} 对 W_{hh}​ 的导数,z_{t}=W_{hh}h_{t-1}+other,因此对 W_{hh} 求导时,仅保留 ​W_{hh}h_{t-1} 项:

\frac{\partial z_{t}}{\partial W_{hh}}=h_{t-1}^{T}\bigotimes I_{D}

  •  z_{t}的第 i 个元素为,因此对W_{hh,ij}的偏导为: 
                                                  
    对其他 W_{hh,ik}(k\neq j) 的偏导为 0。

  • 克罗内克积(Kronecker Product) \bigotimes 是两个矩阵的张量积,用于将矩阵维度扩展。
    A\in \mathbb{R}^{m\times n},B\in \mathbb{R}^{p\times q},则A\bigotimes B\in \mathbb{R}^{mp\times nq}。举一个简单的例子:
                              
  • W_{hh}\in \mathbb{R}^{D\times D}z_{t}\in \mathbb{R}^{D\times 1},根据矩阵求导规则,\frac{\partial z_{t}}{\partial W_{hh}}应该是一个D\times D\times D的三阶向量(每个 W_{hh}​ 元素对应 z_{t} 每个元素的导数)。
    h_{t-1}^{T}\in \mathbb{R}^{1\times D},I_{D}\in \mathbb{R}^{D\times D},克罗内克积后得到\mathbb{R}^{D\times D\times D}的矩阵
    所以用矩阵表示这个关系就是\frac{\partial z_{t}}{\partial W_{hh}}=h_{t-1}^{T}\bigotimes I_{D}

所以

\frac{\partial h_{t}}{\partial W_{hh}}=\frac{\partial tanh(z_{t})}{\partial z_{t}}\cdot \frac{\partial z_{t}}{\partial W_{hh}}=diag(d_{t})\cdot (h_{t-1}^{T}\bigotimes I_{D})

\frac{\partial L_{t}}{\partial W_{hh}}=\frac{\partial L_{t}}{\partial h_{t}}\cdot \frac{\partial h_{t}}{\partial W_{hh}}=\delta _{t}^{T}\cdot diag(d_{t})\cdot (h_{t-1}^{T}\bigotimes I_{D})

  • diag(d_{t}) 是对角矩阵,行向量左乘对角矩阵等价于逐元素相乘:         
                                             \delta _{t}^{T}\cdot diag(d_{t})=(\delta _{t}\bigodot d_{t})^{T}
                 
  • 根据克罗内克积的核心性质,得: 
                        (\delta _{t}\bigodot d_{t})^{T} \cdot (h_{t-1}^{T}\bigotimes I_{D})=[(\delta _{t}\bigodot d_{t})\cdot h_{t-1}^{T}]^{T}
  • 因矩阵转置不改变梯度更新的本质,所以直接使用外积形式
                                                       (\delta _{t}\bigodot d_{t})\cdot h_{t-1}^{T}


将所有时间步(从 t=1 到 t=T)的贡献求和,最终得到:

\frac{\partial L}{\partial W_{hh}}=\sum_{t=1}^{T}(\delta _{k}\bigodot d_{t})\cdot h_{t-1}^{T}

参数更新公式为:


观察式子\delta _{k}=\delta _{T}\cdot \prod_{i=k+1}^{T}[W_{hh}^{T}\cdot (1-h_{i}\bigodot h_{i})],其中W_{hh}的谱半径\rho (W_{hh})(最大特征值的绝对值)决定了乘积的趋势:

  • 如果\rho (W_{hh})<1,随着T-k增大,乘积项指数级衰减,最终趋近于 0,导致梯度消失
  • 如果\rho (W_{hh})>1,随着T-k增大,乘积项指数级增长,最终趋近于无穷大,导致梯度爆炸

普通 RNN 只有一个隐藏状态h_{t},所有信息都混在其中,梯度在跨时间步传递时会被反复乘以W_{hh},最终必然走向指数级衰减或增长。

RNN的应用场景

Many-to-Many(多对多,等长序列):输入序列 → 等长输出序列。输入输出一一对应,每个时间步的输入对应一个时间步的输出,RNN 在每个时间步都输出结果。
例如,槽位填充(Slot Filling)任务中,用户输入语句序列(arrive、Taipei、on、November、2nd),RNN经过处理后分别预测每个词在槽位中的概率(如「Taipei」对应「目的地」槽位,「November」对应「时间」槽位)。

Many-to-One(多对一):输入序列 → 单个输出。输入是长度为 T 的向量序列,输出仅为1 个向量 / 1 个分类结果,RNN 会将整个序列的信息压缩到最后一个时间步的隐藏状态中,再做后续预测。
例如,情感分析(Sentiment Analysis)任务中,用户输入一段文字“这部电影太糟了”,经过RNN处理后输出单个情感分类结果(Positive/Negative)。
再比如,关键词提取(Key Term Extraction)任务中,输入整篇文档(document),使用双向 LSTM(图中上下两条 RNN 链)分别建模正向 / 反向上下文,最后一个时间步o_{T}​整合全文信息;下方注意力层(\alpha _{1},\alpha _{2},\cdots \alpha _{T}​)自动学习每个词的重要性权重,最后输出单个全局向量,用于提取文档的核心关键词。

Many-to-Many(多对多,不等长序列):

案例一:语音识别任务。输入长时序语音特征序列(如 MFCC、FBank 特征,对应下方图的波形→蓝色向量序列),长度远大于输出文本长度(语音采样率高,1 秒语音对应上百帧特征)。输出是更短的文本字符序列(如中文「好棒」),输入输出长度不固定、无严格对齐关系。

普通 RNN 等长 Many-to-Many 要求输入输出一一对应,但语音识别中语音帧和文字没有天然的硬对齐关系(比如「好」可能对应 3 帧语音,「棒」对应 5 帧,且不同人发音时长不同),直接训练无法收敛。如上图所示输入 8 帧语音特征,RNN 逐帧输出「好 好 好 棒 棒 棒 棒 棒」,为了提取出输出,简单的方法是直接去重,称为Trimming,最终得到输出「好棒」

这种直接去重有明显缺陷,它无法处理连词,如“绿油油”会直接被处理为“绿油”。

CTC是解决语音识别对齐问题的核心技术,它在字符集里新增一个特殊符号「空白符 φ」(空白符不代表任何实际字符,只用来表示 “这一帧没有对应有效字符”),对于目标标签 Y = [好, 棒],任何满足 “去重 + 删空白符后等于 Y” 的中间序列,都被视为正确路径(CTC 在训练时,会把所有合法路径的概率加起来,作为模型输出标签 Y 的总概率),例如(语音输入为 6 帧):

训练完成后,模型会输出每帧的「字符 / 空白」概率分布,解码时取每帧概率最高的符号(比如好 φ 好 φ 棒 φ φ 棒),然后合并连续相同的非空白字符并删除所有空白符,最终得到目标文本(“好棒”)。

CTC完整流程如下:

  1. 训练阶段:模型自主学习 “什么时候输出什么”。
    以语音识别 “好棒”为例,输入 8 帧语音特征:
    1. 输入语音帧x_{1}\sim x_{8},RNN 输出每帧的「字符 / 空白」概率分布
      (比如x_{1}​:好 0.8,  φ 0.1,  棒 0.1)
    2. 枚举所有合法路径。所有能拼成 好棒的中间序列(比如好 φ φ 棒 φ φ φ φ好 好 φ 棒 φ φ φ φ等)
    3. 累加所有合法路径的概率,得到模型输出好棒的总概率。
    4. 计算CTC损失。L_{CTC}=-logP(Y|X),其中P(Y|X)是所有合法路径的概率之和
    5. 反向传播更新参数,调整 RNN 权重,让合法路径的总概率最大化(损失最小化)
    6. 迭代训练,模型逐渐学会 的语音帧输出的语音帧输出,过渡帧输出φ
  2. 推理阶段:训练完成后,输入新的语音帧x_{1}\sim x_{8}
    1. RNN输出每帧概率,得到 8 帧的「字符 / 空白」概率分布
    2. 贪心取每帧最高概率得到中间序列:好 好 φ 棒 棒 φ φ φ
    3. 合并连续重复字符,得到好 φ 棒 φ φ φ
    4. 删除所有空白符,最终输出 好棒

通过训练模型会自己发现只有当字符出现在对应语音帧、空白出现在过渡帧时,合法路径的总概率最大,损失最小。所以模型会自发地在对应字符的语音帧,输出该字符的高概率,在字符的过渡、静音、重复发音帧,输出空白符的高概率。
对于目标标签 “好棒”,模型会学习到中间序列好 φ 棒 φ φ φ,去重删空白后是好棒
对于目标标签“好棒棒”,模型会学习到中间序列好 φ 棒 φ 棒 φ φ,去重删空白后是好棒棒
因为不同标签的合法路径集合完全不同,损失函数会驱动模型输出对应标签的路径。

案例二:机器翻译。输入任意长度的源语言序列(如英文 machine learninghow much is the breakfast?),任意长度的目标语言序列(如中文 机器学习、法语 combien coûte le petit déjeuner?)。输入和输出长度完全不固定、无强制对应关系,是最通用的序列建模场景,也被称为 Sequence-to-Sequence (Seq2Seq) 学习。

以英文 “machine learning” 翻译为中文 “机器学习” 为例:

  1. Encoder(编码器):作用是把源语言序列压缩为全局上下文向量。
    英文词序列 machine ,learning依次输入双向 / 单向 RNN(LSTM/GRU),RNN 逐时间步传递隐藏状态,最后一个时间步的隐藏状态,就是整合了整句语义的上下文向量(Context Vector)。(额外添加断句符号 ===(<sos>/<eos>),标记源语言序列的结束,通知编码器完成编码)
  2. Decoder(解码器):作用是从上下文向量生成目标语言序列。
    以编码器的上下文向量为初始状态,逐时间步生成目标语言词。每一步的输入是上一步生成的词(如第一步输入 <sos>,生成 ;第二步输入 机,生成 器,以此类推),直到生成 <eos> 结束符,停止解码

但是上述这种只使用基础Seq2Seq的方法,会有一个核心缺陷,即只能处理短序列信息,因为编码器把整句信息压缩到一个固定长度的上下文向量中,对于长句子而言上下文向量无法承载全部信息,早期输入的信息会被后期信息覆盖,导致翻译错误。为了解决这个问题,通常在Seq2Seq的基础上加上注意力机制(Attention),这个我们后续介绍。

案例三:句法分析(Syntactic Parsing)。句法分析是自然语言处理(NLP)的基础任务,目标是给一段自然语言句子,分析出它的语法结构(句法树),明确句子中单词的词性、短语成分和层级关系。

例如,输入句子“John has a dog”。下面给出了两种结构表示方式:

  • 树形结构表示:用层级树展示语法结构。
    根节点 s 代表整个句子;分支节点代表短语成分,NP表示名词短语,VP表示动词短语;叶子节点表示 单词+词性 标签,NNP表示专有名词(对应 John),VBZ表示动词第三人称单数(对应 has),DT表示限定词(对应 a),NN表示普通名词(对应 dog),.表示标点符号。
  • 括号序列表示:为了让深度学习模型能处理,把树形结构线性化成括号序列。用括号嵌套表示层级关系,比如 (NP NNP )NP 表示「名词短语包含专有名词」

Sequence-to-sequence(简称 Seq2Seq,序列到序列) 是一类输入为序列、输出也为序列的深度学习建模框架,核心是用 “编码器 - 解码器(Encoder-Decoder)” 结构,将一个任意长度的输入序列,映射为另一个任意长度的输出序列,完美适配自然语言、时序信号等非固定长度的序列数据。上述介绍的案例二与案例三均为Seq2Seq的使用示例。
 

Seq2Seq可以解决词序不同导致的语义不同的问题,具体如下:

  • 句子一:white blood cells destroying an infection(白细胞消灭感染)
                  句子一为正面语义,描述免疫系统的正常功能。
  • 句子二:an infection destroying white blood cells(感染消灭白细胞)
                  句子二是负面语义,描述感染入侵的病理状态。

上述两个句子的词袋(Bag-of-Words)完全相同,即单词集合完全一致,仅词序不同。但是词义确是相反的。对于传统的词袋模型(Bag-of-Words) 完全忽略词序,仅统计单词出现频率,因此无法区分这两个语义完全相反的句子。而 Seq2Seq(基于 RNN/LSTM 的序列建模) 可以建模词序依赖,精准捕捉句子的上下文语义,完美解决词袋模型的致命缺陷,这是 Seq2Seq 在 NLP 领域的核心价值。


基础版 Seq2Seq 自编码器(Sequence-to-sequence Auto-encoder):

使用 RNN/LSTM 实现 “编码 - 解码”,目标是把输入句子压缩为语义向量,再还原出原句子,从而学习句子的语义表示。
自编码器的训练目标是最小化 “输入句子” 和 “解码输出句子” 的重构误差,让编码器学习到能精准还原原句的语义向量,从而实现句子的语义表示。

  • Encoder(编码器):
    输入原句 Mary was hungry . she didn’t find any food(9 个词的序列)
    用 RNN 逐时间步处理输入序列,将整句的语义压缩为最后一个时间步的隐藏状态(红框标注的紫色块),这个隐藏状态就是句子的语义向量(Context Vector),承载了整句的全部语义信息。
  • Decoder(解码器):
    以编码器的语义向量为初始隐藏状态
    用 RNN 逐时间步生成输出序列,最终还原出原句 food any find didn’t she . hungry was Mary(词序反转,是 RNN 解码的典型特性)
    softmax 层,将解码器的隐藏状态映射为单词概率,逐词生成输出。

层级化 Seq2Seq 自编码器(Hierarchical Seq2Seq Auto-encoder):

基础版 Seq2Seq 自编码器只能处理单句,而层级化 Seq2Seq 自编码器(来自论文《A hierarchical neural autoencoder for paragraphs and documents》)可以处理长文档 / 段落,通过 ”词 - 句 - 段“ 的双层编码,实现长文本的语义建模。

训练目标是最小化 “输入段落” 和 “解码输出段落” 的重构误差(交叉熵损失),让编码器学习到精准的层级化语义表示。        

与基础版 Seq2Seq 自编码器的单层编码单层解码的结构不同,层级化 Seq2Seq 自编码器使用 “词级 - 句级” 双层编码  +  “句级 - 词级” 双层解码结构。

以输入段落Mary was hungry . she didn’t find any food. She went to the restaurant.(2 个句子,共 16 个单词)为例说明其工作过程:

  • Encode-Word(词级编码):
    • 输入段落中所有单词的序列(棕色块)
      [Mary, was, hungry, ., she, didn’t, find, any, food, ., She, went, to, the, restaurant, .]
    • 按词序依次输入 词级RNN ,每一步接收当前单词的词向量 + 上一步的隐藏状态(捕捉单词的上下文依赖,完整保留词序信息)
    • 逐时间步生成词级隐藏状态,每个单词对应一个隐藏向量
    • 输出长度为 16 的词级隐藏状态序列 [h₁, h₂, ..., h₁₆]
  • Encode-Sentence(句级编码):
    • 输入词级隐藏状态序列,按句子拆分:
      句子 1:[h₁(Mary), h₂(was), ..., h₉(food), h₁₀(.)]
      句子 2:[h₁₁(She), h₁₂(went), ..., h₁₆(.)]
    • 对每个句子的词级状态序列,用 句级 RNN 逐时间步处理,取每个句子最后一个时间步的隐藏状态,作为该句子的 句级语义向量
    • 输出长度为 2 的句级语义向量序列 [s₁(句1), s₂(句2)],承载了整个段落的层级化语义
  • Decode-Sentence(句级解码):
    • 输入句级语义向量序列 [s₁, s₂]
    • 以句向量为初始状态,用 句级2RNN 逐时间步生成句级解码隐藏状态
    • 每个句级状态对应一个句子的词级状态序列,还原句子的结构信息(将句向量还原为词级状态,为最终单词生成做准备)
    • 输出长度为 16 的词级解码隐藏状态序列 [d₁, d₂, ..., d₁₆]
  • Decode-Word(词级解码):
    • 输入词级解码隐藏状态序列[d₁, d₂, ..., d₁₆]
    • 对每个词级状态,用 Softmax 计算词汇表中所有单词的概率
    • 取概率最大的单词作为输出,逐词生成完整段落
    • 最终输出:food any find didn’t she . hungry was Mary . restaurant the to went She .(RNN 解码的典型反转词序,可通过双向 RNN 修正)

基于 Seq2Seq 自编码器的语音特征提取与检索系统:

语音数据是天然的变长序列,同一个单词(如dog),不同人发音、不同语速,对应的语音片段长度完全不同;传统手工特征(如固定帧长截断、MFCC 统计量)无法适配变长输入,也无法有效保留语音的语义 / 发音相似性。
 

下图展示了输入多组变长语音片段(左:dog/dog/dogs,右:never/never/never,下:ever/ever)(每个片段是不同长度的语音波形),在通过 Seq2Seq 自编码器处理后,将每个变长语音片段映射为一个固定长度的向量(图中黑色圆点,每个点是一个 D 维向量,维度 D 固定,与输入语音长度无关)。可以看到所有dog相关语音的向量聚集在左上区域,所有never相关语音的向量聚集在右上区域,ever相关语音的向量聚集在下方区域,相似语音的向量距离近,不同语音的向量距离远,完美保留了语音的相似性。

语音检索系统的完整落地架构,分为 “离线建库” 和 “在线检索” 两个阶段:

  • 离线阶段(Off-line)构建语音向量库:
    • 输入大规模音频档案(Audio archive),包含大量变长语音片段(如语音库中的单词、语句片段)。
    • 通过Audio Segment to Vector模块(即 Seq2Seq 自编码器的 RNN 编码器),将每个变长语音片段转换为固定长度的向量,存储为向量库(图中彩色竖条,每个竖条对应一个语音片段的向量)。(一次性处理所有存档语音,生成可快速检索的向量索引。)
  • 在线阶段(On-line)语音查询与检索:
    • 输入用户的语音查询(Spoken Query),是一个变长语音片段(如用户说dog,检索库中所有dog相关语音)。
    • 用同一个Audio Segment to Vector模块,将查询语音转换为固定长度向量。
    • 将查询向量与离线向量库的所有向量计算相似度(如余弦相似度、欧氏距离)
    • 输出相似度最高的语音片段,作为搜索结果(Search Result)。


编码器结构(变长语音→固定向量):

对于一个语音片段(蓝色波形),首先提取声学特征序列(如 MFCC、FBank、Log-Mel 谱),将语音波形转换为帧序列x_{1},x_{2},x_{3},x_{4},\cdots,每个x_{t}是一帧的特征向量,序列长度等于语音帧数。

编码器由 RNN/LSTM/GRU 单元 组成(绿色方块,循环结构),逐时间步处理声学特征序列,输入x_{1}生成隐藏状态h_{1},输入x_{2}和上一步h_{1}生成h_{2},依此类推,最后生成最终隐藏状态h_{t}(红框标注的绿色块),RNN 的循环结构会完整保留语音的时序信息,最终的h_{t}(固定长度向量)承载了整个语音片段的全部语义 / 发音信息,也就是我们需要的 “语音向量”。

Seq2Seq 自编码器的联合训练流程(编码器和解码器联合训练,同时优化参数):

  • RNN Encoder(编码器):输入声学特征序列x_{1}\sim x_{4},输出最终隐藏状态(固定长度向量),承载整个语音的信息。
  • RNN Decoder(解码器):由 RNN 单元组成(黄色方块,循环结构),以编码器的最终隐藏状态为初始状态,逐时间步生成输出序列y_{1}\sim y_{4}

解码器的核心目标尽可能精准地还原编码器的输入声学特征序列,即让y_{t}\approx x_{t}

训练完成后,解码器可直接丢弃,仅保留编码器,输入任意变长语音片段,编码器的最终隐藏状态就是我们需要的固定长度语音向量,该向量可直接用于语音检索、相似度计算、语音分类、说话人识别等下游任务。

下图即为使用语音向量的一种应用场景,Seq2Seq 架构在对话系统领域的经典应用,用 LSTM 作为编码器和解码器,实现 “用户输入问句 → 机器人自动生成回复” 的端到端对话:

这类基于 LSTM 的 Seq2Seq 聊天机器人,是 Transformer 大模型(如 ChatGPT)的前身,后续的大模型用 Transformer 的注意力机制替代了 LSTM,解决了长序列的并行计算和长依赖问题,但核心的「编码器 - 解码器」Seq2Seq 架构,依然是当前大语言模型的基础结构。

Bi-RNN

普通单向 RNN(比如 Elman、Jordan)只能  从左到右(或从右到左) 处理序列,处理到第 t 个词 x_{t}​ 时,只能利用 x_{1}\sim x_{t} 的信息(过去 / 左边的上下文),完全看不到 x_{t+1}\sim x_{T}​ 的信息(未来 / 右边的上下文)。但在很多任务里,一个词的含义 / 标签需要同时由前后文共同决定。为了解决这个问题,于是出现了双向循环神经网络(Bi-RNN),双向循环神经网络(Bidirectional RNN, Bi-RNN)是对普通单向 RNN 的改进,核心是让模型在处理序列时,同时利用  “过去(左边)”  和  “未来(右边)”  的上下文信息。

它的设计思路是用两个独立的单向 RNN,一个正向(从左到右)处理序列,一个反向(从右到左)处理序列,然后把两个方向的隐藏状态合并,让每个位置的输出都能看到完整的上下文(过去 + 未来):

正向RNN:

反向RNN:

状态合并:

最终输出:

虽然双向Bi-RNN可以完整感知上下文,但这同时意味着计算量翻倍(需要训练两个独立的 RNN,参数量和计算量约为单向 RNN 的 2 倍),并且必须拿到完整序列才能计算反向 RNN 的状态,不适合实时 / 在线任务(比如实时语音识别,需要边接收边输出)。

LSTM

LSTM(Long Short-Term Memory,长短期记忆网络) 是一种特殊的循环神经网络(RNN),专门为解决普通 RNN 无法处理长期依赖的问题而设计。

普通 RNN 在处理长序列时会出现梯度消失 / 爆炸,导致模型记不住太久远的信息;而 LSTM 通过门控机制精细控制信息的流动,能有效保留重要的长期记忆,同时丢弃无用的短期噪声。

普通 RNN 只有一个隐藏状态,所有信息都混在一起,梯度在反向传播时会快速衰减,无法记住太久远的信息。LSTM 的核心创新是:

  1. 引入独立的  “记忆细胞(Memory Cell)” :记忆库,负责长期保存信息。只有门控能修改它,避免信息被随意覆盖。
  2. 设计三个 “门控(Gate)”
    1. 遗忘门(Forget Gate):决定要丢弃哪些旧记忆
    2. 输入门(Input Gate):决定要加入哪些新信息
    3. 输出门(Output Gate):决定要输出哪些记忆到当前隐藏状态

通过这三个门,LSTM 实现了   "选择性记忆" ,只保留对任务有用的长期信息,过滤掉无关的短期干扰,从根本上解决了 RNN 的长期依赖问题。

LSTM计算流程

  • z为候选状态输入,用于生成存储细胞状态c
    z_{i}为输入门输入,控制输入门的开关,决定新信息的流入量;
    z_{f}为遗忘门输入,控制遗忘门的开关,决定旧细胞状态的保留量;
    z_{o}为输出门输入,控制输出门的开关,决定当前隐藏状态的输出量。
    z,z_{o},z_{f},z_{i}是 LSTM 单元的线性变换输入,由当前时刻输入 x_{t} 和上一时刻隐藏状态 h_{t-1}​ 共同计算得到,是所有门控和状态更新的基础。所有输入均通过仿射变换(线性层 + 偏置)计算得到:

    所有参数W,b均通过反向传播学习得到。
  • LSTM 的核心是细胞状态 c(图中蓝色圆柱),它像一条 “信息传送带”,在序列中传递长期信息;三个门控(遗忘门、输入门、输出门)负责控制信息的流入、保留与流出,各门均采用Sigmoid作为激活函数,取值在[0,1],表示门的开关程度。
  • 输入门与候选细胞状态:输入z经过一个激活函数g(\cdot )(通常为 tanh函数,输出范围(-1,1))生成候选的新细胞状态 \tilde{c}_{t}。输入门输入z_{i},经过一个激活函数f(\cdot )(Sigmoid函数),输出f(z_{i})\in (0,1),代表 “新信息的流入比例”。
  • 遗忘门:输入z_{f}经过一个激活函数f(\cdot )(Sigmoid函数),输出f(z_{f})\in (0,1),代表 “保留比例”。将将上一时刻的细胞状态 c​ 与 ​f(z_{f}) 逐元素相乘(图中 cf(z_{f})),决定保留多少历史信息。若f(z_{f})\approx 1表示完全保留该维度的历史信息;若f(z_{f})\approx 0表示完全遗忘该维度的历史信息。
  • 细胞状态更新:将遗忘后的旧状态与输入门过滤后的新候选状态相加,得到当前时刻的细胞状态c',计算式为:
                                                  c'=f(z_{f})\bigodot c+f(z_{i})\bigodot \tilde{c}_{t}
  • 输出门:输入z_{o}经过一个激活函数f(\cdot )(Sigmoid函数),输出f(z_{o})\in (0,1),代表 “细胞状态的输出比例”
  • 隐藏状态(输出)计算:先对更新后的新细胞状态 c'​ 做 tanh 激活(将值压缩到 (−1,1),对应图中 h(c')),再与输出门 f(z_{o})​ 逐元素相乘,得到当前时刻的隐藏状态 h_{t}​(即 LSTM 的输出,图中为 a)。h_{t}​ 会作为下一时刻的输入 hh_{t},同时作为当前时刻的模型输出,用于后续任务。

为了方便表示,将LSTM的图像简化如下:

LSTM网络结构:LSTM实际上是将每个传统神经元替换为一个完整的 LSTM Block。假设输入只有三个特征,即\vec{x}=[x_{1},x_{1},x_{1}]^{T},将\vec{x}经过线性变换后得到向量z,z_{f},z_{i},z_{o},这几个向量的特征均为3,将其分别输入到单层神经网络模型中,最终得到输出\vec{y}
LSTM Block就是一个神经元,与传统的神经元相比它的输入有4个,也就是说LSTM的参数量是普通RNN的四倍。

为了方便表示,将上图网络结构改成下图所示。将上述并联处理三维向量的三个LSTM Block用一个来LSTM Block来表示,其内部细胞状态值为\vec{c}_{t}

LSTM处理序列:每个时间步 t 对应一个 LSTM Block,共享同一组参数(W_{xf},W_{xi},W_{xc},W_{xo},b_{f},b_{i},b_{c},b_{o}) 。共传递两种状态给下一个时间步:

  • 细胞状态c_{t-1}\rightarrow c_{t}\rightarrow c_{t+1}\rightarrow \cdots,沿时间轴传递长期记忆,解决长序列依赖。
  • 隐藏状态h_{t-1}\rightarrow h_{t}\rightarrow h_{t+1}\rightarrow \cdots,沿时间轴传递短期上下文,作为下一时间步的输入。

多层LSTM:多层 LSTM 的每一层都是一个独立的标准 LSTM 单元。
第 k 层输入 = 第 k-1 层在同一时间步 t 的隐藏状态输出 h_{t}^{(k-1)} + 第 k 层上一时间步 t-1 的隐藏状态 h_{t-1}^{(k)}。并且每一层都需要独立计算4 组线性变换(遗忘门、输入门、候选状态、输出门),拥有独立的权重矩阵和偏置,层间参数不共享。(下图以两层LSTM为例)

LSTM Peephole Connection(窥视孔连接):

Peephole(窥视孔连接)是标准 LSTM 的一种经典扩展结构,核心改进是 让三个门控(遗忘门、输入门、输出门)可以直接 “窥视” 上一时刻的细胞状态 c_{t-1}(以及当前时刻的细胞状态 c_{t}​),而不再仅依赖输入 x_{t}​ 和隐藏状态 h_{t-1}


标准LSTM连接的门控公式:

Peephole LSTM(带窥视孔)的门控公式在每个门控中,新增了细胞状态的线性投影项,让门控直接感知细胞状态:

在图像上就是第 t 时间步的遗忘门、输入门,直接接入了上一时刻的细胞状态 c_{t-1},第 t 时间步的输出门,直接接入了当前时刻更新后的细胞状态 c_{t}。第 t+1 时间步的门控,同理接入 c_{t},以此类推。(下图为简化画法。上一时刻的细胞状态 c_{t-1}只作用于遗忘门、输入门,输出门在计算当前时刻更新后的细胞状态 c_{t}后,再将c_{t}加入计算中)

对于超长序列,细胞状态 ​c_{t} 是唯一的长期记忆载体。Peephole 让门控直接与 c_{t} 交互,进一步强化了 LSTM 对长期依赖的捕捉能力,尤其适合需要精确记忆历史状态的任务(如语音识别、时序预测)。

LSTM 主要解决的是 RNN 的「梯度消失问题」,同时能显著缓解梯度爆炸,但无法从根本上消除梯度爆炸。

传统 RNN 的隐藏状态更新为,每出现一次\frac{\partial h_{_t{pre}}}{\partial h_{t-1}}都会乘一次W_{hh},梯度反向传播时会因多次矩阵乘法 W_{h} 指数衰减,导致长期依赖失效。
而LSTM 的细胞状态更新为加法形式\odot表示逐元素相乘),与\frac{\partial h_{_t{pre}}}{\partial h_{t-1}}对应,LSTM梯度沿时间步传递由\frac{\partial C_{t}}{\partial C_{t-1}}决定:

\frac{\partial C_{t}}{\partial C_{t-1}}=f_{t}\bigodot 1+C_{t-1}\bigodot \frac{\partial f_{t}}{\partial C_{t-1}}+\tilde{C_{t}}\bigodot \frac{\partial i_{t}}{\partial C_{t-1}}+i_{t}\bigodot \frac{\partial \tilde{C_{t}}}{\partial C_{t-1}}

其中后三项中的\frac{\partial f_{t}}{\partial C_{t-1}},\frac{\partial i_{t}}{\partial C_{t-1}},\frac{\partial \tilde{C_{t}}}{\partial C_{t-1}}是门控 / 候选状态对 C_{t-1}​ 的间接耦合,根据公式得\frac{\partial f_{t}}{\partial C_{t-1}}=\frac{\partial f_{t}}{\partial h_{t-1}}\cdot \frac{\partial h_{t-1}}{\partial C_{t-1}},\frac{\partial i_{t}}{\partial C_{t-1}}=\frac{\partial i_{t}}{\partial h_{t-1}}\cdot \frac{\partial h_{t-1}}{\partial C_{t-1}},\frac{\partial \tilde{C_{t}}}{\partial C_{t-1}}=\frac{\partial \tilde{C_{t}}}{\partial h_{t-1}}\cdot \frac{\partial h_{t-1}}{\partial C_{t-1}},其大小由\frac{\partial h_{t-1}}{\partial C_{t-1}}=o_{t-1}\bigodot (1-tanh^{2}(C_{t-1}))决定,当|C_{t-1}|很大时,tanh(C_{t-1})\approx 1(1-tanh^{2}(C_{t-1}))\approx 0,因此\frac{\partial h_{t-1}}{\partial C_{t-1}}\approx 0,即后三项趋近于0,\frac{\partial C_{t}}{\partial C_{t-1}}\approx f_{t};当|C_{t-1}|较小时,后三项有一定数值,但 LSTM 训练的核心目标是让长依赖信息以大绝对值存储在 C_{t}​ 中(即 |C_{t}| 大),因此在长序列的梯度传递中,后三项的影响远小于第一项。

因此上式可近似为:

\frac{\partial C_{t}}{\partial C_{t-1}}\approx f_{t}

对于长度为 T 的序列,梯度从 C_{T} 传递到 ​C_{t} 的累乘项为:

f_{t}=\sigma (\cdot )\in (0,1),但 LSTM 会通过学习让长依赖相关的信息对应的 f_{t}\approx 1(即 “记住” 长序列中的关键信息),当 f_{t}\approx 1 时,累乘项\prod_{k=t+1}^{T}f_{k}\approx 1,不会随时间步指数衰减。

工程上仍需配合梯度裁剪(Gradient Clipping) 彻底解决梯度爆炸问题。

GRU

GRU(Gated Recurrent Unit,门控循环单元)是 LSTM 的轻量化简化变体,同为改良版 RNN,解决 RNN BPTT 梯度消失、无法建模长距离时序依赖问题;相比 LSTM参数更少、计算更快、训练更省资源,效果多数场景和 LSTM 持平。

LSTM 有 3 个门(遗忘门、输入门、输出门)+ 独立细胞态 C_{t},结构冗余、参数量大、前向反向计算开销高、小数据集易过拟合。GRU的思路是把LSTM  遗忘门 和  输入门 合并为一个,称为更新门 (Update gate),并废除独立细胞态 ​C_{t},只用单一隐藏态 h_{t} 承载长期记忆。其具体结构区别如下:

其工作过程如下(x_{t} 表示当前时刻输入,h_{t-1}表示上一时刻隐藏状态):

  1. 重置门(Reset gate,r_{t}):控制对上一刻旧记忆的遗忘程度,捕捉短期依赖
    r_{t}=\sigma (W_{xr}x_{t}+W_{hr}h_{t-1}+b_{r})
    r_{t}\rightarrow 0,清空过往隐藏信息,重开短期记忆
    r_{t}\rightarrow 1,完整保留过往隐藏信息
  2. 候选隐藏状态\tilde{h}_{t}:用重置门筛选后的旧记忆,生成当前新候选记忆
    \tilde{h}_{t}=tanh(W_{xh}x_{t}+W_{hh}(r_{t}\bigodot h_{t-1})+b_{h})
  3. 更新门(Update gate,z_{t}):等价融合 LSTM 遗忘门 + 输入门 ,控制保留多少旧记忆、写入多少新记忆
    z_{t}=\sigma (W_{xz}x_{t}+W_{hz}h_{t-1}+b_{z})
  4. 当前时刻最终隐藏态 h_{t}​(核心融合):
    h_{t}=(1-z_{t})\bigodot h_{t-1}+z_{t}\bigodot \tilde{h_{t}},其中(1-z_{t})\bigodot h_{t-1}表示保留大部分历史长期记忆,z_{t}\bigodot \tilde{h_{t}}表示掺入当前新的候选信息

模型可自主学到:关键长依赖位置z_{t}\approx 0\Rightarrow \frac{\partial h_{t}}{\partial h_{t-1}}\approx 1

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐