参考https://www.rethink.fun/
RNN 循环神经网络
RNN是最早的NLP任务SOTA。核心思想是循环,文本数据的一个重要特征是有序性,也就是token出现的顺序会影响语义的理解,对于这种具有时序的数据,经典处理方法都是RNN。
经典的时序任务有:
- NLP
- 语音识别
- 时序预测,比如股票
- 视频理解
RNN的关键是,会在隐藏层传递记忆,单看隐藏层的话,是一个循环的过程,这也是循环神经网络名字的由来。为了处理下游任务,在隐藏层基础上加任务层,比如分类任务就加个线性层分类。
如图所示,这是一个NER实体识别任务,也就是对每个token要把他归类为某一类实体,一个token进入RNN后的过程是:
- 经过嵌入层映射为词向量
- 经过隐藏层,输出为RNN最关键的内容,隐藏层状态,也可以称为记忆
- 记忆一方面接入下游的线性分类层,输出NER结果
- 记忆同时还和下一个token的词向量拼接,作为下一步的RNN输入,这样下一次就不仅有当时的token,还能看到前一步的记忆,可以理解上下文。
详细公式则为:
ht=tanh([ht−1∣xt]wh+bh)h_t=tanh([h_{t-1}|x_t]w_h+b_h)ht=tanh([ht−1∣xt]wh+bh)
yt=softmax(htwy+by)y_t=softmax(h_tw_y+b_y)yt=softmax(htwy+by)
其中hth_tht是ttt时刻的隐藏层输出,wh,bhw_h,b_hwh,bh是隐藏层的参数,xtx_txt是输入,yty_tyt是输出,wy,byw_y,b_ywy,by是任务层的参数
RNN的应用
RNN实际上能解决我们前一节讲的所有NLP任务,这也是RNN及其变种在很长时间内是NLP SOTA的原因
多对一任务
输入一个文本序列,只输出一个值,比如文本分类,文本回归。在最后一个token再加上任务层,或者只允许最后一个token的输出有效即可
一对多
输入一个文本类型,直接输出一整段文本。这是文本生成任务,只有第一次的输入是真实输入,后面每一次的输入都是隐藏层+模型生成的上一个token
多对多
如果输入输出长度一致,类似前面的NER任务,每一步的输入都是前一步记忆+当前输入
如果长度不一致,比如翻译,摘要问题。则引入一个重要的思想,编码器-解码器架构,先用多次循环读入全部输入,这被称为编码阶段,这个阶段没有输出,再把编码阶段的记忆传递下去,每一步都输出一个token,然后新的输入为上一步的记忆+上一步的输出token。这样能实现变长输出的同时,还参考了输入的记忆。
RNN的缺点和LSTM
序列长时,前面的token信息被压缩了很多次,加上隐状态本身维度不大,跟token大小是一个数量级的,一般也就512,256,不足以保存全部信息,于是长文本时对前面的信息记忆会逐渐下降。
这个问题的本质是隐状态是短期记忆,越远的信息保存的越少,越近的信息保存的越多。那么为了记住远处信息,一个解决方法就是再设置一个长期记忆。这就是LSTM(Long Short-Term Memory)名字的由来,既有短期记忆,也有长期记忆。
具体架构如下图:
- 对每一轮的输入xtx_txt,做线性层和激活。
- 同时输入也传入一个输入门,经过线性层和sigmoid激活,得到一个激活向量,和输入相乘,相当于对这一轮的输入选择性接受
- 输入用类似的方式再产生一个遗忘向量,和旧的长期记忆ct−1c_{t-1}ct−1相乘,相当于把旧的长期记忆遗忘一部分。再加上这一轮选择性接受的输入信息,作为新的长期记忆ctc_tct
- 输入再产生一个输出向量,输出向量和长期记忆乘起来,得到这一轮的短期记忆也就是隐状态输出hth_tht,hth_tht可以直接传到下一轮,也可以接上任务层,做这一轮的输出
- ctc_tct直接传到下一轮
- 这里的W−h,Wi,Wf,WoW-h,W_i,W_f,W_oW−h,Wi,Wf,Wo都是可训练的矩阵
把多个这样的网络拼起来,就是处理一个序列的LSTM结构,如下图。这样的优点是,长期记忆实际上没有经过任何线性层和激活层,类似resnet的残差链连接,更容易把长期记忆传递下去。
这个网络略复杂,但确实是transformer之前最好的NLP模型,可见注意力机制诞生之前为了让模型保持长期记忆有多困难
GRU
LSTM效果虽然好,但是网络太复杂,推理慢,GRU是一个基于LSTM思想,但是实现更简单的RNN变体。GRU推理更快,效果接近LSTM
网络如下:
- 去掉了长期记忆ctc_tct
- 但是对短期记忆hth_tht增加了LSTM中的更新门和重置门(类似于遗忘门)。这里的思想是,既然短期记忆的容量有限,与其在循环中被动压缩,不如用可训练的Wr,WuW_r,W_uWr,Wu模块来决定每一轮遗忘什么,记住什么,学习什么
双向RNN(BiRNN)
前面的RNN很容易发现一个问题,推理都是从前往后读的,也就是生成第i个token的隐状态时,只能看到前i个token,但现实文本很容易出现的情况是,一个词的含义不仅要看上文,还要看下文,比如
- “我喜欢苹果味的汽水。”
- “我喜欢苹果手机。”
这里苹果的含义需要看下文的被修饰词是什么才能确定。
但RNN本身必须是单向的,像同时看到上下文,只能用一个正序RNN,一个逆序RNN来实现,如下图,两个RNN看到前iii个的正向RNN,和看到后n−i+1n-i+1n−i+1个逆向RNN的隐状态拼接,就是看到全文时,第i个token的隐状态。
深度RNN
提高模型性能的一个无脑办法就是堆参数,于是一个优化是,增加多层RNN,如下图,把第一个隐藏层的隐状态,作为第二个隐藏层的输入。这样的优点是增加了模型的抽象能力,可以类比生产一个工艺品时,增加工序,可以实现更精细的加工,只有一个工序的话,这个工序做的再好也是有上限的。
当然,普通的线性层也是可以增加的