1 序列数据表示

1.1 简述

语音、文字等有先后顺序,属于序列数据,将时序列据表达为能处理的形式就叫Sequence Representation。如对文字而言,PyTorch中没有对string的支持,所以要将其表示成数值形式,相应的方法就是Word Embedding

通常将一个序列表示为[元素数、每个元素的向量长]的Tensor形式:
[seq_len, feature_len][seq_len, feature_len]

注意,这个表示方法对不同的具体序列数据的类型有不同的具体解释,上面的seq_len既是序列的长度,也是序列的特征的数量,因为序列中每个元素就是序列的一个特征,所以也可以理解成feature_num

1.1.1 文本数据

例如,每句话有5个单词,每个单词表示为长度100的词向量,那么一句话表示为Tensor后的shape就是[5,100]了。

1.1.2 数值数据

例如,房价时序数据,有100个月的房价,每个月的房价是一个数值,那么一个房价时序数据的Tensor的shape就应该是[100,1]。房价本身就是一个可以处理的数值数据,也就不需要做embedding了。

1.1.3 数字图像数据

数字图像数据可以用前面的CNN处理,但是也可以从扫描数字图像的角度来处理。看图片可以是逐行扫描的,那么图片从上到下就成了一个序列数据,每行是序列中的一个特征。如28*28的图像数据,每次扫描一行作为序列中的一个元素,其作为序列数据的Tensor的shape就是[28,28](28个特征,每个特征是28个像素值组成的向量)。

不过强行这样处理也往往没有CNN那样做得更好。

1.2 加入batch size后的序列数据

为了训练效率,每次输入的不一定是一个序列样本,可以是多个序列样本,也就要在Tensor里加入一个batch size的维度b,这样输入的Tensor的shape可以有下面两种表示法:

  1. [seq_len, b, feature_len][seq_len, b, feature_len]
  2. [b, seq_len, feature_len][b, seq_len, feature_len]

其中b就是一次送入多少个序列。这两种方式中前面一种是比较常用的,因为最外层循环的就应该是一个个特征(特征数即序列长度)。如给出三条房价曲线,都是100个月的房价,那么就是月份编号从0到99,样本从0到2,房价维度是1。

具体使用哪种方式存储数据,在PyTorch中只要使用batch_front标志位即可。

2 循环神经网络

循环(Recurrent)神经网络将序列数据时间上展开处理。以下简称循环网络,为了避免和递归神经网络混淆,不使用RNN这个简称。循环网络可以用于序列数据的分析、生成和转换。

2.1 共享参数的引入

对于序列数据最直观的处理方法是对序列中的每个特征单独处理,如下图,是对一句话的每个词向量用独立的线性层处理,最后再将结果聚合:
【DL学习笔记】3:循环神经网络(Recurrent Neural Network)
但这样的处理方式会带来两个问题:

  1. 参数量太大,因为序列可能很长(如长句子序列会有很多单词)
  2. 没有使用前面时刻的语境信息(如句子"喜欢"和"不 喜欢"意思是不一样的)

可以采用权值共享的方式,所有的特征共享权值矩阵WW和偏置bb
【DL学习笔记】3:循环神经网络(Recurrent Neural Network)
这样可以解决上面的第一个问题,但是这样做对语境信息处理得还不好。

2.2 循环网络的结构

循环网络和传统的前馈式网络的区别就是,循环网络的输入是分散在各个时刻上的,而不是一次性输入的。
【DL学习笔记】3:循环神经网络(Recurrent Neural Network)
图中所有的A都是同一个处理单元,里面的运算是一样的,参数也是共享的。hi1h_{i-1}保存了网络在ii时刻之前学习到的语境信息,具体地,每层综合之前信息和当前输入计算得到hih_i
hi=Tanh(Uhi1+Wxi+b)h_i = Tanh(Ucdot h_{i-1} + Wcdot x_i + b)

其中Uhi1Ucdot h_{i-1}部分即是对之前的信息的处理,而Wxi+bWcdot x_i + b则是对新输入的特征xix_i的处理。这就需要一个hh单元用来保存语境信息,最开始可以将其初始化为全0的Tensor。


而这一层的输出yiy_i是可选的,有的循环网络中有输出,有的可以没有输出。最简单的方式就是用感知器的方式,对hih_i进行一个线性层处理再**一下:
yi=σ(Wohi+bo)y_i = sigma(W_o cdot h_i + b_o)

这样的循环网络叫Elman网络,目前用得较少了。

3 循环网络下的语言模型

3.1 n-gram模型

如对句子S=w1w2...wnS=w_1w_2...w_n,估算句子出现概率P(S)P(S),可以将其用条件概率分解:
P(S)=P(w1w2...wn)=P(w1)P(w2  w1)...P(wn  w1w2...wn1)begin{aligned}P(S) &= P(w_1w_2...w_n) \&= P(w_1)P(w_2 | w_1)...P(w_n | w_1w_2...w_{n-1})end{aligned}

可见其中存在P(wn  w1w2...wn1)P(w_n | w_1w_2...w_{n-1})这样的项,这样的项条件部分太多,在现实中没法做到准确的估计,可以在一定程度上进行独立性假设,n元文法(n-gram)模型就是解决类似的问题,例如:
P(wn  w1w2...wn1)P(wn  w1w2)P(w_n | w_1w_2...w_{n-1}) approx P(w_n | w_1w_2)

即是认为一个词的出现仅依赖于前面的两个词,这样就是三元文法(Tri-gram)模型。

3.2 困惑度(Preplexity)

困惑度是评价训练好的语言模型的一种理论方法,对测试集中的数据D=w1w2...wnD=w_1w_2...w_n,困惑度的计算方法是:
PPL(D)=21Ninlog2p(wi)PPL(D) = 2^{- frac{1}{N} sum_{i}^n log_2p(w_i)}

困惑度本质上是测试数据上的经验分布pp和模型上的概率分布qq的交叉熵:
H(p~,q)=xp(x~)log2q(x)H(tilde{p}, q) = - sum_{x} p(tilde{x}) cdot log_2q(x)

3.3 循环网络的语言模型结构

使用循环网络构建语言模型,输入和输出都是自然语言的词,上一时刻的输出直接作为下一时刻的输入:
【DL学习笔记】3:循环神经网络(Recurrent Neural Network)
图中特殊词<bos>表示begin of sentence,相应的还有<eos>在句子结尾。


在语言模型中,因为每个时刻要输出的yiy_i需要是一个概率分布,以来表达输出的是每个词的概率,所以不能再像前面的Elman网络那样使用sigmoid**,而是采用softmax来将输出归一化成多类的概率向量的形式
yi=softmax(Vhi+bo)y_i = softmax(V cdot h_i + b_o)

【DL学习笔记】3:循环神经网络(Recurrent Neural Network)
在类别只有两类的时候,softmax和sigmoid是等价的,但是显然语言中的词汇有很多很多,不止两类。

4 循环网络的参数训练——BPTT算法

BPTT算法,即梯度在循环网络中按时间线回传,以更新网络中的参数。

4.1 每个时刻单独优化(旧的方法)

要进行参数训练,可以用SGD,这就要为循环网络定义一个损失函数。以前面的语言模型为例,最开始时刻的输入是y0y_0,输出是y1y_1(会作为下一时刻的输入),那么想要最大化的就是在参数θtheta情形下,输入y0y_0时输出为y1y_1的概率,即最大化P(y1  y0,θ)P(y_1 | y_0,theta),所以可以将损失函数定义为对数似然度的相反数
J(y1,y0)=log P(y1  y0,θ)J(y_1,y_0)=-log P(y_1 | y_0,theta)

在训练时就是去优化模型的参数θtheta使得损失函数越小越好:
θ=argminθJ(y1,y0)theta^* = argmin_{theta} J(y_1,y_0)


李沐老师在这里特别提到,对softmax本身求梯度比较复杂,但和对数似然函数的损失函数复合之后(softmax log-loss)的梯度的梯度却比较简单:

Jxi={p(yt)1,ytlabelp(yt)0,yt=labelbegin{aligned}frac{partial J}{partial x_i} =begin{cases}p(y_t)-1, y_t neq label \p(y_t)-0, y_t = labelend{cases}end{aligned}

它就等于期望输出这个词的概率,减去当前是否是输出这个词。也就是说在梯度下降中的调整正好是当前值和目标值的差。


在最初的循环网络中,计算损失和计算梯度都是按时刻依次计算的 ,先算第一个时刻再算第二个时刻。而从第二个时刻开始,就会开始涉及上一时刻的计算图,如下图:
【DL学习笔记】3:循环神经网络(Recurrent Neural Network)
这时还要将梯度从y1y_1传到y0y_0的部分,来更新网络参数,所以这个算法叫BPTT(Back Propagation Through Time),即是经过时间线来反向传播。

同理,在第三个时刻,有:
J(y3  y2,y1,y0)=log P(y2  y2,y1,y0,θ)J(y_3 | y_2,y_1,y_0)=-log P(y_2 | y_2,y_1,y_0,theta)

4.2 整体优化(新的方法)

有了计算图工具,不用一个时刻一个时刻的分别计算梯度,可以把整个计算图做完,把所有的损失加在一起,求出一个总的损失再整体优化循环网络

【DL学习笔记】3:循环神经网络(Recurrent Neural Network)
以图中序列长为3为例,总的损失为:
J(y3  y2,y1,y0)=J(y1  y0)+J(y2  y0,y1)+J(y3  y0,y1,y2)J(y_3 | y_2,y_1,y_0)=J(y_1 | y_0) + J(y_2 | y_0,y_1) + J(y_3 | y_0,y_1,y_2)

优化模型参数θtheta以最小化损失:
θ=argminθJ(y3,y2,y1  y0,θ)theta^* = argmin_{theta} J(y_3,y_2,y_1 | y_0,theta)

最终的优化结果和4.1中的单独优化的结果是等价的。但这样可以简化程序(把计算图搭建完一步计算梯度)、提高效率4.1中的方法较前面的层要更新很多次,但用现在的方法只要更新一次)。

5 梯度消失/梯度爆炸问题

循环网络训练过程中,把梯度从最后的时刻传到最前面的时刻,要经过的传导路径很长,也就是链式法则展开后是非常多的偏导数相乘,如前面的序列长为3的例子中:
Jy0=Jh3h3h2h2h1h1y0frac{partial J}{partial y_0} = frac{partial J}{partial h_3} cdot frac{partial h_3}{partial h_2} cdot frac{partial h_2}{partial h_1} cdot frac{partial h_1}{partial y_0}

所以就可能导致梯度消失和梯度爆炸问题,后面会学习的LSTM就较好的解决了这个问题。