当初读这篇论文的目的只有1个:在读Transformer-XL: Attentive Language Models Beyond a Fixed-Length Context这篇文章时,关于infer阶段,作者为啥说Vanilla Transformer每预测一次就要重新计算,而且xl这篇文章的主要比较对象就是Vanilla Transformer,所以才认为读一下这篇Vanilla Transformer是有必要的,论文位置在:Character-Level Language Modeling with Deeper Self-Attention。为了简单起见,我们按照XL这篇文章的称呼习惯称本文要解读的这个模型结构为Vanilla Transformer。
目录
第一部分:Vanilla Transformer的结构
第二部分:Vanilla Transformer训练时作者的一些小trick
第三部分:Vanilla Transformer的相关结果
第四部分:其他
第一部分:Vanilla Transformer的结构
首先,作者要解决的问题是字级别的LM,相比词级别的LM,字级别LM明显需要依赖的距离特别长,比如说一句话某个位置是应该使用she还是he,是依赖于前面的主语情况,这个主语可能距离此单词位置的有十几个单词,每个单词7-8字母长度,那么这就将近100+个字符长度了,作者使用transformer的结构主要原因是他认为该结构很容易做到在任意距离上的信息传递,而相对RNN(LSTM)这种结构,就需要按照时间一步一步的传递信息,不能做到跨越距离。
这篇文章虽然用到了transformer结构,但与Attention is all you need这篇文章(简称原Transformer)是有差异的。原Transformer整体是一个seq2seq结构,具体的细节见此处。而Vanilla Transformer只利用了原Transformer的decode的部分结构,也就是一个带有mask的attention层+一个ff层。
如果将 "一个带有mask的attention层+一个ff层" 称为一个layer,那么Vanilla Transformer一共有64个这样的layer,每一个layer有2个head,model_dim=512,ff层的hidden_units=2048,sequence的长度为512。对于训练语言模型来说,这已经是一个很深的网络了,要知道对于大名鼎鼎的BERT网络的层数也就12层(base)和24层(large)了。
另外,之所以使用mask结构是因为语言模型的定义是p(xi|x0*x1*......xi-1),也就是根据前i个字符预测第i+1个字符,如果你已经提前看到了答案(也就是第i+1个字符甚至更后面的字符内容),那就没有预测的意义了,这里加mask与原Transformer的decode部分的带有mask的self-attention道理都是一样的。
Positional Embeddings:RNN结构的网络对于类似于LM这种序列性的数据编码带有天然的优势,但缺点就是不能并行,必须要step by step。而attention结构最大的优点就是可以实现并行,但它不能表达序列性,所以为了给网络加入识别序列性就要引入 位置编码 Positional Embeddings。在原Transformer中,位置编码的编码信息是固定的,不需要学习,具体编码方式如下,输出为pos embedding。将word embedding + pos embedding整体作为网络的输入,并且仅在第一层加入了位置编码,之后的每层都不会再次加入。而对于Vanilla Transformer,作者认为它的网络深度太深了,如果只在第一层加入pos embedding,那么经过多层传递,这个信息很容易丢失,所以它是每层都会将上一层的输出与pos embedding加在一起作为下一层的输入,而且,pos embedding是需要学习的。所以,光pos embedding模型就要学习 N*L*dim 个参数,其中N是网络的层数(本文64层),L是上下文的长度(本文512),dim是embedding的维度(本文=512)。
def positional_encoding(dim, seq_length, dtype=tf.float32):
"""
:param dim: 编码后的维度
:param seq_length: 序列的最大长度
:param dtype:
:return:
"""
pos_encode = np.array([pos/np.power(10000, 2*i/dim) for pos in range(seq_length) for i in range(dim)])
pos_encode[0::2] = np.sin(pos_encode[0::2])
pos_encode[1::2] = np.cos(pos_encode[1::2])
return tf.convert_to_tensor(pos_encode.reshape([seq_length, dim]), dtype=dtype, name='positional_encoding')
总之,从结构上来说,Vanilla Transformer没有什么太特别的地方,用的组件都是原Transformer这篇论文中用到的,甚至还精简了一些,无非就是Vanilla Transformer的网络深度非常深。这个深度导致在训练的时候很难收敛,个人认为这篇论文中值得学习的就是为了达到收敛目的,作者使用的一些小trick,这些小trick对于我们以后解决类似的问题是很有帮助的。
第二部分:Vanilla Transformer训练时作者的一些小trick
作者在论文中说当网络的深度超过10的时候,就很难让模型收敛,准确率也很低,所以如果大家训练的网络深度超过10的时候就可以部分借鉴这篇论文中的训练方法:引入辅助的loss。
如下图1所示,这个辅助的loss分为3类:Multiple Positions; Intermediate Layer Losses; Multiple Targets
为了方便,我们只以2层来展示,且每一个segment的length=4,原本我们是根据t0~t3的输入,在H节点这个位置预测t4的结果,loss就是H节点的输入计算一个交叉熵。现在辅助loss的第一类loss就是:对于最后一层所有的节点都计算下一步应该预测的字符,即在节点E处根据输入t0,预测输出为t1,在节点F处根据输入为t0和t1,输出是t2,以此类推。然后将每一个Positions处的loss加起来。第一类loss贯穿整个train的全部阶段,不发生衰减。
辅助loss的第二类是除了在最后一层计算交叉熵loss之外,在中间层也要计算,即在节点A处根据输入t0,预测输出为t1,以此类推,但中间层的loss并不贯穿整个train始终,而是随着训练进行,逐渐衰减,衰减的方式是,一共有n层网络,当训练进行到 (k/(2*n))时停止计算第k层loss。也就是说当训练进行到一半的时候,所有的 中间层 都不再贡献loss。
辅助loss的第三类是每次预测时所预测几个字符,在本论文中,每次预测下一步和下下步的字符结果,具体的看下面的图即可,非常清楚。但对于下下步的预测结果产生的loss是要发生衰减的,论文中该loss乘以0.5后再加入到整体的loss中。
图1 3类辅助loss示意图
第三部分:Vanilla Transformer的相关结果
作者使用的数据集有enwik8,lm1b,text8这3个,列举了64层的transformer模型与12层的transformer模型(这个也是作者写的,目的是比较一下是否深度增加效果更好)还有一些RNN结构的模型进行了比较,实践证明该方法是比较好的,具体数据见论文,此处不列出。
但是作者有一个地方的比较结果我认为是很有意义的,这个对于我们以后设计模型有参考性,就是作者这篇论文里提到了加了3种辅助loss帮助训练,还有就是作者使用了momentum优化器训练,使用的pos embedding也是跟之前不同的。那么这些因素到底有没有用,如果有用,哪个用处大,有多大?针对这个问题作者进行了一个比较,比较的基线是上面讲的64层模型。
可以看出,辅助loss中的Multiple Positions和Intermediate Layer Losses效果是最明显的,至于使用了需要学习的pos embedding并没有太大的作用,优化器和Multiple Targets的辅助loss感觉效果都不大。
第四部分:其他
该模型我认为的亮点就是添加了辅助loss帮助训练模型,缺点是计算量非常大,这一点作者自己也提到了,因为在预测阶段,每预测一个字符,就要将所有的结果重新计算一遍,它不能像RNN这种结构,隐节点保存了前面所有时刻的信息(保没保存住是另外一个维度的内容),只要给定的前一个时刻隐节点的信息和该时刻的输入,直接可以计算输出。