3.6 门控循环网络

1.让模型自己来管记忆

上一章我们聊过普通的循环网络,它每次都用新输入和旧状态一股脑算出新状态,想删什么、想留什么,它都说了不算,只能被动地把全部信息搅在一起。这么一来,只要序列一长,老信息要么被新输入冲淡,要么在反复相乘里被慢慢压扁,模型到头来基本就是个记不住事的状态,长期依赖这条路也就走不通了。这个毛病我们上一章也提过,叫梯度消失。

门控循环网络就在这一点上做了文章。它给网络加了一批可以学习的控制信号,这些信号叫作门。门的作用是让模型自己拿主意,决定哪些旧信息该留着、哪些新信息该写进去。有了门,模型就从被动记录升级成主动管理,记忆这件事也就变得有取有舍了。

这么说大概有点空,我们一层一层来看,先从结构最精简的GRU讲起。

2.先看GRU:两个门就够用

我们先以GRU(门控循环单元)为例,感受一下门是怎么工作的。GRU结构相对精简,是挺好上手的入门款。

每一个时间步 tttt 就是时间步的下标,表示第 tt 个时刻),GRU会先算出两个门。第一个叫更新门,记作 ztz_t,它的值在0和1之间,管旧状态和新候选状态各按多大比例混进新状态里,计算式是 zt=σ(Wz[ht1,xt])z_t=\sigma(W_z[h_{t-1},x_t])。第二个叫重置门,记作 rtr_t,值也在0和1之间,管算候选状态的时候旧状态要参与多少,计算式是 rt=σ(Wr[ht1,xt])r_t=\sigma(W_r[h_{t-1},x_t])

这一步喂进来的输入是 xtx_txtx_t 就是当前时间步看到的输入),上一时间步留下来的旧状态是 ht1h_{t-1}ht1h_{t-1} 可以理解成模型上一秒的隐藏记忆)。两个门公式里的方括号 [ht1,xt][h_{t-1},x_t] 表示把旧状态和当前输入拼成一个更长的向量,WzW_zWrW_r 是两个可以学习的权重矩阵(模型在训练里自己调出来的参数),σ\sigma 是Sigmoid函数,它能把任意实数压到0和1之间,所以门的值都落在0到1这个区间。

模型还会先算出一个候选状态 h~t\tilde{h}_t(候选状态就是这一步备选的新内容,相当于打草稿),计算式是 h~t=tanh(W[rtht1,xt])\tilde{h}_t=\tanh(W[r_t\odot h_{t-1},x_t])。这里 tanh\tanh 是双曲正切函数,把实数压到 1-111 之间,所以候选状态可能带正负号,WW 是候选状态的权重矩阵,符号 \odot 叫Hadamard积,也叫逐元素相乘,就是把两个同样形状的向量按位置一位一位地乘(比如 [a,b][a,b][c,d][c,d] 的Hadamard积是 [ac,bd][ac,bd]),用它来控制旧状态每一维参与多少。

算出候选状态之后,新的隐藏状态就由更新门来拍板了,公式是 ht=ztht1+(1zt)h~th_t=z_t\odot h_{t-1}+(1-z_t)\odot\tilde{h}_t。这个式子读起来很直白,更新门 ztz_t 接近1,旧状态 ht1h_{t-1} 就保留得多(倾向于保持记忆),接近0,候选状态 h~t\tilde{h}_t 就写进去得多(倾向于接受新内容),两边按门的值做加权平均。重置门 rtr_t 接近1,算候选状态时就多参考旧状态,接近0就基本不看旧状态。

说起来,我们不妨打个比方。GRU有点像在一个本子上记笔记。更新门决定这一页是接着上一页的思路往下写,还是另起一页重写,重置门决定写新内容之前,要不要把上一页翻出来参考参考。两个门一配合,记忆的新旧更替就完成了。

再换一个更贴近日常的例子。小明打开手机,群里有几百条未读消息。更新门 ztz_t 在问,前面那一长串对话的上下文还要不要接着用,接近1就接着用,接近0就基本丢掉重来。重置门 rtr_t 在问,整理当前这条消息的时候,前面的内容要参考多少,接近1就多参考,接近0就基本不看。两个门一拿主意,新消息怎么记、记多少,心里就有数了。

3.LSTM:多了条专跑记忆的快速路

LSTM(长短期记忆网络)比GRU更进一步。它除了隐藏状态 hth_thth_t 是隐藏状态,模型对外的输出就靠它),还额外维护一条专门的细胞状态,记作 ctc_tctc_t 就是细胞状态,可以理解成模型长期藏起来的小金库,外人不直接看)。围绕这条细胞状态,LSTM安排了三个门来照看它。

第一个是遗忘门 ftf_tftf_t 管旧细胞状态保留多少),计算式是 ft=σ(Wf[ht1,xt])f_t=\sigma(W_f[h_{t-1},x_t])。第二个是输入门 iti_titi_t 管新信息写进去多少),计算式是 it=σ(Wi[ht1,xt])i_t=\sigma(W_i[h_{t-1},x_t])。第三个是输出门 oto_toto_t 管当前状态对外暴露多少),计算式是 ot=σ(Wo[ht1,xt])o_t=\sigma(W_o[h_{t-1},x_t])。三个门各管一摊,分工明确,值通通在0和1之间,公式里的 WfW_fWiW_iWoW_o 是各自的权重矩阵,方括号 [ht1,xt][h_{t-1},x_t] 同样是把旧状态和当前输入拼起来。

LSTM还要算一个候选细胞状态 c~t\tilde{c}_t(可以理解成这一步备选的新长期内容),公式是 c~t=tanh(Wc[ht1,xt])\tilde{c}_t=\tanh(W_c[h_{t-1},x_t]),这里的 WcW_c 是候选细胞状态的权重矩阵,tanh\tanh 让它的值落在 1-111 之间。接着细胞状态自己按下式更新:

ct=ftct1+itc~tc_t=f_t\odot c_{t-1}+i_t\odot\tilde{c}_t

这个式子读起来就是,遗忘门 ftf_t 决定上一秒的旧细胞状态 ct1c_{t-1} 各维保留多少,输入门 iti_t 决定候选细胞状态 c~t\tilde{c}_t 各维写进去多少,两块加起来就是新的细胞状态 ctc_t\odot 同样是逐元素相乘。最后隐藏状态由输出门把关,公式是 ht=ottanh(ct)h_t=o_t\odot\tanh(c_t),也就是先用 tanh\tanh 把细胞状态压到 1-111 之间,再由输出门 oto_t 决定每一维对外暴露多少。

这条细胞状态 ctc_t 的妙处在于,它是一条相对平稳的信息通道,加加减减的操作比较多,反复相乘的比较少。还记得梯度消失是怎么来的吧,就是一路相乘给乘没的。LSTM这条细胞状态绕开了频繁的相乘,所以梯度可以沿着它传很远而不被压扁。就这一点设计,让LSTM在长序列任务上稳稳站住了脚,捕捉长距离依赖时比普通循环网络靠谱得多,在当年可以说是序列建模里的绝对主角。

顺带一提,LSTM还有一个挺有意思的变种叫窥孔连接(peephole connection)。标准LSTM的三个门只看 ht1h_{t-1}xtx_t,门对细胞状态本身当前是多少完全没数。窥孔连接的改动很直接,把上一时刻的细胞状态 ct1c_{t-1} 也塞进门的输入里,让门能看一眼小金库里到底攒了多少货。拿遗忘门来说,公式就从 ft=σ(Wf[ht1,xt])f_t=\sigma(W_f[h_{t-1},x_t]) 变成 ft=σ(Wf[ht1,xt,ct1])f_t=\sigma(W_f[h_{t-1},x_t,c_{t-1}]),输入门和输出门同理也把 ct1c_{t-1} 加进去。这么一改,门调节起来就更有针对性,好比管账的得先看一眼账户余额,再决定这笔账怎么记。窥孔连接在某些任务上能再挤出一点效果,不过参数也跟着多一点,算是锦上添花的小改款。

门控网络的核心思想,其实就一句话,把记忆的更新这件事,拆成几个各司其职、可以分别控制的小步骤,让模型自己学着怎么管最划算。

4.门控也是有代价的

门控结构确实治好了长期依赖这个老大难,可天下没有免费的午餐。门一多,参数量和计算量都跟着往上涨,训练起来更费时费显存。我记得有一本讲工程项目经验的书里说过,再精巧的设计,离开了足够的算力和存储也跑不起来。门控网络也是这个道理,参数上去了,就得有GPU和显存来支撑。

所以GRU和LSTM经常被拿来比较。GRU参数少、跑得快,简单任务上够用,LSTM参数多、更稳,长序列和复杂任务上更顶得住。选哪个,得看具体任务和手头资源,没有绝对的优劣。

还有一点特别容易让人误解,得专门澄清一下。门虽然听起来像是开关,但它真正的值,其实是0到1之间的连续小数,远没有人类开关那样非开即关那么干脆。门里头的数值,完全是为预测任务服务、由数据学出来的连续控制信号。模型觉得怎么管记忆对完成任务最有利,就会把门调成什么样,跟我们直觉里的开关闭合差得比较远。

5.GRU和LSTM到底怎么挑

初学者最容易纠结的,大概就是这两个到底选哪个。我们把这件事掰开来说。

先看参数量。GRU有更新门、重置门和候选状态,权重矩阵一共三组,对应 WzW_zWrW_rWW。LSTM有遗忘门、输入门、输出门和候选细胞状态,权重矩阵一共四组,对应 WfW_fWiW_iWoW_oWcW_c。每组的尺寸两家差不多,都是把拼接后的 [ht1,xt][h_{t-1},x_t] 映射回隐藏维度。所以粗算下来,LSTM的参数量大约是GRU的三分之四倍,也就是多出大约三分之一。可别小看这三分之一,隐藏维度一拉大、层数一叠上去,多出来的参数和显存就相当可观了。

再看什么时候偏好哪个。数据量不大、序列不长、显卡又有点紧的场合,GRU是更划算的选择,它训练快、不容易在小数据上过拟合,调起来也省心,属于那种小而美的款。反过来,数据量足、序列长、任务又复杂(比如要建模几十上百步的远距离依赖),LSTM就更顶得住,它那条专门的细胞状态在长程记忆上更稳,能扛住更长的序列。工程实践里有个常用的经验,先拿GRU快速跑个baseline看看效果,不够再换LSTM往上顶,这样性价比最高。小张做毕设那会儿就是这么干的,先GRU快速验证想法行得通,最后要交差了才换LSTM把指标再往上提一截。

其实这两个谁也没把谁彻底比下去。学术界和工业界都做过海量对比,结论大体上是大部分任务上两者效果接近,个别长序列任务LSTM略占优。所以纠结选哪个意义不大,把数据喂够、把超参数调明白,才是真正决定成败的地方。

6.往前看一眼,再叠高几层

光一个单向的LSTM或GRU,有时候还不够用。这里说两个常见的加强思路。

第一个是双向循环网络(Bidirectional RNN)。它让序列从前往后跑一遍,再从后往前跑一遍,两路各自维护一份隐藏状态,最后把同一时刻的两个隐藏状态拼起来对外输出。单向网络只能根据当前时刻之前的信息做判断,双向网络还能参考一下未来的信息,理解得更全面。比如做词性标注,一个词是名词还是动词,光看前面几个词可能定不下来,往后看一眼往往就清楚了。这有点像看推理小说,读到后半段才知道前面某个不起眼的细节是什么意思,回头再看一遍,理解就完整了。双向的代价是得先把整个序列拿到手,那些必须边来边处理的实时在线任务(实时语音流、股票下一秒预测这种)就用不了,只能老老实实用单向。

第二个是把多层循环网络叠起来用,叫堆叠循环网络(Stacked RNN)。第一层的输出当成第二层的输入,第二层的输出再喂给第三层,层数自己定。底层抽局部短程的特征,高层把这些特征再往上抽象一层,表达能力强了不少。不过叠得太高也容易出问题,参数变多、训练变慢、还容易过拟合,有点像运动员训练,加量固然能出成绩,加过了头反而容易受伤。一般叠个两到三层就够用了,再往上性价比就不行了。用PyTorch搭堆叠循环网络很省事,创建LSTM或GRU的时候把num_layers参数设成大于1的数就行。

7.门控循环网络能做什么

学了这一堆门和状态,到底能拿去干些什么,我们点几个典型应用场景,让大家心里有个数。

第一个是语音识别。语音信号本质上是随时间变化的波形,切成帧之后就是一条很长的序列,相邻帧之间还高度相关,正好是循环网络的主场。在Transformer进来之前,LSTM配连接时序分类(CTC)一度是语音识别的标配,比如DeepSpeech那一路,把声学特征帧喂给多层双向LSTM,输出每个时刻对应声学单元的概率,效果相当出彩。就算到了今天,端到端语音识别系统里依然能看到LSTM和GRU的身影。

第二个是时间序列预测。无论是股价走势、电网负荷、还是校园里食堂的人流量,凡是带时间顺序的连续数据,门控循环网络都能搭一把手。它擅长抓住历史里的周期和趋势,再用门控决定哪些老信息对预测下一时刻有用,这在流量预测、设备故障预警、天气预测这类任务上,都是个靠谱的baseline。

第三个是早期的机器翻译。在注意力机制和Transformer火起来之前,神经机器翻译基本靠编码器和解码器两个LSTM撑场面,也就是大名鼎鼎的seq2seq框架。编码器把源语言句子读成一个向量,解码器再一个词一个词地往外吐目标语言。那个年代这套组合拳可以说是封神之作,把传统统计翻译方法甩开了不止一个身位,循环网络也因此风光了好些年。

除了这三个大头,门控循环网络在文本分类、情感分析、命名实体识别这些自然语言处理任务上也都广泛使用过。可以说在Transformer出来之前的那几年,LSTM和GRU就是序列建模的绝对主力,养活了一大批论文和工程。哪怕后来注意力机制成了新宠,门控循环网络作为入门序列模型的核心地位依然没动摇,把它学透了,后面再去看Transformer和注意力那些东西,会觉得顺理成章得多。

练习

Q1. LSTM 多出来的那条细胞状态 ctc_t,凭什么就能治好长期依赖?

因为 ctc_t 这条通道更新时主要是 ct=ftct1+itc~tc_t=f_t\odot c_{t-1}+i_t\odot\tilde{c}_t 这种加加减减的线性叠加,反复相乘的环节少。梯度沿它反传时只要遗忘门 ftf_t 接近 1,梯度就能基本不衰减地一路传回浅层,绕开了普通循环网络里那种连乘把梯度压扁的毛病。所以它能捕捉长距离依赖。

Q2. GRU 的隐藏状态更新式是 ht=ztht1+(1zt)h~th_t=z_t\odot h_{t-1}+(1-z_t)\odot\tilde{h}_t。若某时刻更新门 ztz_t 全等于 1,或全等于 0,新状态分别是什么?又已知 LSTM 权重矩阵有四组、GRU 有三组,两者参数量大致什么比例?

ztz_t 全等于 1 时 ht=ht1h_t=h_{t-1},完全保留旧记忆、忽略新候选内容;ztz_t 全等于 0 时 ht=h~th_t=\tilde{h}_t,完全采用新候选状态。参数量上,每组矩阵尺寸两家差不多,LSTM 四组、GRU 三组,所以 LSTM 的参数量约为 GRU 的三分之四倍,也就是多出大约三分之一。

Q3. 有人把门控循环网络里的"门"理解成一个二值开关,说门开了信息才过、门关了就彻底断,这种说法对吗?

不对。门的值是 Sigmoid 输出,落在 0 到 1 之间的连续小数,不是非开即关的二值开关。它是一组为预测任务服务、由数据学出来的连续控制信号,门值 0.3、0.7 都很常见,决定的是每一维信息按多大比例通过,而不是通断。

Q4.(面试题) 手头有个序列建模任务,序列中等长度、显卡显存也紧,你倾向先上 GRU 还是 LSTM?为什么?再说说双向和堆叠循环网络分别适用什么场合。

一般先上 GRU。它参数少约三分之一、训练快、在小数据上不容易过拟合,先用它快速跑出 baseline 验证思路,不够再换 LSTM 顶上去,这是工程上性价比最高的做法。双向循环网络适合能把整段序列一次性拿到手的场合(如词性标注、机器翻译编码器),它能同时参考前后文;但它要求先看完全程,对实时语音流、下一秒预测这种在线任务就用不了,只能用单向。堆叠循环网络(多层叠加)适合需要更强表达能力的任务,一般叠两到三层,能从局部短程特征往高层抽象特征层层提,但叠太高会过拟合、训练变慢。

相关标签
深度学习LSTMGRU序列