介
Burp Decoder是Burp Suite中一款编码解码工具,将原始数据转换成各种编码和散列。
模块说明
输入域:输入需要解码的原始数据。
输出域:对输入域进行解码的结果进行显示。
Decode as 解码
Encode as 编码
Hash 散列
编码解码类型:URL、HTML、Base64、ASCII、16进制、8进制、2进制、GZIP。
Hash:SHA、SHA-224、SHA-224、SHA-256、SHA-384、SHA-512、MD2、MD5。
可以进行多次编码解码
Smart decoding进行智能解码。
键词:Transformer,Decoder解码器,注意力机制
在之前系列的篇章中已经介绍了Transformer的Encoder编码器网络结构,要点知识包括
从本节开始本系列将对Transformer的Decoder解码器部分进行深入分析,解码器也是采用Self Attention作为模型基座,但是解码器不仅要关注自身输入的表征,还要联合编码器的结果做交互,同时对接预测任务,因此解码器相比于编码器要更加复杂。
理解Transformer的解码器首先要了解Encoder-Decoder框架。在原论文中Transformer用于解决机器翻译任务,机器翻译这种Seq2Seq问题通常以Encoder-Decoder框架来解决,Transformer的网络结构也是基于encoder-decoder框架设计的。这种框架的模型分为两部分编码器Encoder和解码器Decoder,编码器负责将原文本数据编码为中间状态向量,该状态向量传递给解码器生成输出。示意图如下
Encoder-Decoder框架
以机器翻译场景为例,期望将某种语言的句子X翻译成另一种语言的句子Y,句子被表征为每个位置的字符id输入,则给定X=(x1,x2,x3,x4...)输入给模型,期望模型预测出Y=(y1,y2,y3,y4...),模型架构如下
机器翻译场景的Encoder-Decoder
编码器会对完整的输入句子通过各种复杂非线性变换生成State,代表原始输入被编码器编码之后形成的中间语义状态,形如公式
编码器输出中间语义向量
而解码器层需要融合解码器产出的中间状态State,和解码器已经生成出的信息Y1,Y2...Yi-1,来生成i时刻需要生成的单词Yi。
解码器融合解码器语义和历史解码信息
解码器是从第一个单词开始,逐位预测下一个单词,最终实现了从X翻译到Y的任务。
在实际网络中会在解码器中增加注意力机制,如果不添加注意力则对于任何位置Yi的预测中间状态state都是一样的,显然源文本中每个位置的字符应该和目标翻译文本各位置字符存在一定的对照关系,因此源文本的编码器state向量应该在每个位置对于当下要预测的Yi有不一样的权重分配,公式如下
带有注意力机制的Decoder
注意力机制工作的方式是将当下需要预测的单词位置的隐向量,和编码器输出的每个输入位置的状态向量,一一通过一个对齐函数(Attention)来计算目标单词和输入中某单词对齐的可能性大小,可能性越大赋予更大的权重,代表当下预测单词应该更加关注源文本中其对照单词的信息,最终中间状态向量state会给该对照单词位置处的分量给予更多的权重,从而更好地预测出该位置的目标单词。带有注意力机制的Encoder-Decoder示意图如下
带有注意力机制的Encoder-Decoder
Transformer通过逐位依次预测完成Seq2Seq的任务。Transformer解码器结构如下
Transformer的解码器
右侧部分为Decoder解码器,将期望预测的目标文本添加start和end标识位置,底部将目标文本作为输入,顶部将目标文本右移一格(shifted right)作为预测输出,编码器的输出和输出是错位设计的,以编码器输入为“I love you”,解码器输出为“我爱你”为例,在训练过程中编码器的输入和预测目标分别为
错位训练
每次总是以前面已经出现的单词加上编码器的中间状态,来预测下一个单词,比如红色阴影部分使用"<start>+我"来预测下一个单词“爱”,以此类推该条样本可以分为预测“我”,“爱”,“你”,“end”四个任务,Decoder的目标是输出“我”,“爱”,“你”,“end”四个位置的embedding,这四个任务的预测准确度作为整条样本的预测目标。
从输入输出的角度来看,"<start>"位置经过Decoder输出的向量embedding服务于“我”,"我"位置经过Decoder输出的向量embedding服务于“爱”,以此类推,当前词的Decoder结果用于预测它右边那个词的概率,这就是shifted right的体现,理解这点很重要。
理解shifted right错位训练
shifted right移位训练仅仅解决了预测目标的问题,移位训练实施起来比一般的分类任务要复杂,分为训练和预测两种场景。
在训练场景下答案数据集会提前给到,令一个批次数量为B,文本长度为L,输出embedding长度为D,我们只需要将前L-1的文本作为Decoder的输入,将后L-1的文本作为Decoder的预测目标即可,永远用前一个词的embedding来预测后一个词的概率分布,此时输入是[B,L-1,D],输出也是[B,L-1,D],再加上Transformer这种Self Attention天然地支持所有词并行输入训练,因此在训练场景可以将答案文本全局移位,然后全部一齐输入训练,考验模型在前词和更早之前的词确定的前提下,对后面一个词的预测能力,将一个完成的句子拆成一个个单词的预测任务。
在预测场景下不存在答案文本,只能从<start>位置开始逐位预测,因此预测场景的解码器必定是串行的。将新预测的单词和历史预测单词合并作为解码器的输入来预测下一个新单词,重复这个过程直到预测结果为<end>截止,预测阶段输入文本是一个一个单独输入的,同时会配合在此之前的历史预测单词完成自注意力机制。
预测阶段解码器串行工作方式 预测阶段解码器串行工作方式
解码器主要包含两个注意力模块,分别是自注意力层和交互注意力层,自注意力层是对历史已经预测的单词序列做特征表征,交互注意力层是融合历史预测单词序列和编码器输出的特征表征。在自注意力层有两个要点,首先Q,K,V在训练和预测阶段怎么分配,另外是它独有的下三角掩码。
解码器的自注意力机制和编码器中的网络结构一致,都是基于Self Attention,通过原始embedding加上位置编码来作为Decoder的输入,自注意力层包含Q,K,V注意力计算,残差链接,层归一化,前馈传播模块,mask机制等。
解码器的自注意力机制在训练阶段Q,K,V相同,都是带有mask掩码的答案文本embedding,而在预测阶段由于只需要用Decoder的最后一维(也就是最后一个token)embedding做概率分布,因此只需要将当前前单词的信息作为Q,将当前词和之前所有的词的信息作为K和V,对最后一个token位置单独做Self Attention即可,如果这点难以理解请回看上一段的shifted right训练方式。
在编码器中仅需要对padding位置进行掩码,因为padding位置的信息不需要带有权重去干扰有实词位置的embedding表征,而在解码器模块不仅要考虑padding导致的mask,还要考虑后词偷看问题。由于答案是一齐输入的,而实际的部署场景是步进预测的,理论上当前步长是看不到当前步长之后的词的信息的,解决方案是使用下三角掩码,将答案中当前位置之后的单词全部mask为0,这样答案文本依旧可以一齐输入,在Keras的Transformer源码中实现如下
def GetSubMask(s):
# TODO 生成一批下三角矩阵,就是斜对角线以下部分全是1
len_s=tf.shape(s)[1]
bs=tf.shape(s)[:1]
mask=K.cumsum(tf.eye(len_s, batch_shape=bs), 1)
return mask
令s为一个[batch_size,5,6]每个文本最大长度为5,每个单词映射维度为6,调用GetSubMask生成mask如下
>>> a=tf.reshape(tf.convert_to_tensor(list(range(30))), [1, 5, 6])
>>> GetSubMask(a)
>>> <tf.Tensor: shape=(1, 5, 5), dtype=float32, numpy=array([[[1., 0., 0., 0., 0.],
[1., 1., 0., 0., 0.],
[1., 1., 1., 0., 0.],
[1., 1., 1., 1., 0.],
[1., 1., 1., 1., 1.]]], dtype=float32)>
该下三角掩码每一行代表当前位置,每一行的纵向只有当前位置和之前位置为1,代表自注意力使用该词,否则为0代表该词还看不到不能使用,以句子序列ABCD为例图示如下
下三角掩码
例如在计算C单词的自注意力表征的时候,只能使用候选的ABC三个词的V信息,C和D的注意力权重必须干预改成0。
掩码中1代表计算出的Q,K相似度保留原值,而0位置代表Q,K相似度改为一个极负的值,使得注意力权重为0,如图所示
下三角掩码对自注意力的影响
考虑到在训练过程中答案本身会进行该批次下的统一padding,因此还需要再叠加padding的mask掩码,杜绝padding单词对实词的表征影响,这个和编码器中的掩码一致,在源码中实现如下
# TODO 输出该批次下每个文本样本,在每个词步长下的mask向量,由于pad和词步长无关,所以每个步长下的mask向量相同,就是pad位置的是0
self_pad_mask=Lambda(lambda x: GetPadMask(x, x))(tgt_seq)
# TODO 只允许该词和该词前面的词纳入计算,下三角 [batch_size, seq_len-1, seq_len-1]
# TODO 输出该批次下每个文本样本,在每个词步长下的mask向量,由于是生成模型,只能基于当下词和前词进行计算,所以是个下三角
self_sub_mask=Lambda(GetSubMask)(tgt_seq)
# TODO 只要两个有一个为0则为0 mask掉
self_mask=Lambda(lambda x: K.minimum(x[0], x[1]))([self_pad_mask, self_sub_mask])
其中self_pad_mask为答案句子的padding掩码,对于答案中每个单词,该掩码是相同的,例如ABCD四个单词组成的答案,其中D词为padding,有词位置仅有ABC,则self_pad_mask如下
编码器层的padding掩码
源码使用K.minimum将两个掩码合并,每个位置取最小值,相当于两个掩码只要有任意一种情况需要被遮蔽则就应该被遮蔽,如图所示
解码器的自注意力最终掩码
通过掩码机制,并行输入整个文本得到的每个单词的自注意力表征和一个一个逐位循环预测进行的表征效果等同。
自注意力层是解码器输入自身的特征表征,而交互注意力层用到了编码器的输出,将Decoder和Encoder信息进行融合。交互注意力层和编码器中的注意力层网络结构基本没有差异,但是由于有两方进行交互因此Q,K,V的分配上需要单独设计,解码器交互注意力层的特写如下
交互注意力层
前文提到在Encoder-Decoder框架中会使用对齐函数来计算目标单词和编码器输出的每个单词对齐的可能性大小,而在Transformer中使用点乘注意力来作为对齐函数,解码器和编码器作为该对齐函数的输入,来比对当前解码器位置应该更多地关注哪个源文本位置,进一步将源文本信息携带到当前编码位置,因此解码器交互注意力层Q,K,V安排如下
在训练之前需要对所有源文本和目标文本进行单独padding到最大长度seq_length,因此交互注意层计算的注意矩阵大概率不是一个方阵,计算示意图如下
交互注意力计算
以解码层的单词A为例,A需要融合编码器中的a,b,c,d四个单词的信息表征,其中得分权重分别为(3.2,1.3,0.9,-1),同样的交互注意力也需要mask掩码,掩码的维度和注意力权重矩阵维度相同,在源码中实现如下
def GetPadMask(q, k):
'''
shape: [B, Q, K]
'''
# TODO [batch_size, seq_len - 1]=> [batch_size, seq_len - 1, 1]
ones=K.expand_dims(K.ones_like(q, 'float32'), -1)
# TODO [batch_size, 1, seq_len - 1]
mask=K.cast(K.expand_dims(K.not_equal(k, 0), 1), 'float32')
# TODO [batch_size, seq_len-1, seq_len-1],相当于对mask直接复制
# TODO 输出该批次下每个文本样本,在每个词步长下的mask向量,由于pad和词步长无关,所以每个步长下的mask向量相同,就是pad位置的是0
mask=K.batch_dot(ones, mask, axes=[2, 1])
return mask
# TODO 参数1决定步长,参数二决定pad
enc_mask=Lambda(lambda x: GetPadMask(x[0], x[1]))([tgt_seq, src_seq])
此处的掩码根据padding机制生成,其中GetPadMask的第一个参数tgt_seq决定文本步长,第二个参数决定padding的依据,显然使用了源文本的padding信息,例如在源文本abcd中d为padding位置,则mask矩阵如下
交互注意力掩码
代表解码器层中A,B,C都需要携带编码器中的a,b,c,d信息,但是A,B,C每个位置计算的时候都需要舍弃源文本中的d信息,因为d信息是padding的干扰项。
交互注意力层计算解码器输入每个单词位置相对于编码器源文本的表征,解码器每个输入本身通过下三角mask机制代表当前和之前位置信息,而编码器源文本是完整可见的,因此解码器每个位置都可以和全部编码器输出计算注意力,只需要主要编码器的padding部分在交互注意力的时候同样需要删除,源文本中的padding信息不能带入到解码器中,示意图如下
解码器中mask工作流程
解码器经过自注意力层提取当前预测位置的表征,经过交互注意力层以当前预测位置的表征和编码器层的中间状态进行对齐,融合编码器中的信息到解码器中来,令该批次样本数为B,解码器最大文本长度为L,embedding维度为D,则解码器最终输出一个三维向量,维度为[B,L-1,D],其中L-1是在训练过程中使用错位训练策略导致。
Transformer在解码器的输出层加入线性层Linear使每个位置的embedding表征映射到预测词库中每个词的概率,以英文到德文翻译的数据集为例,输出为3665个样本中德文单词的得分,源码实现如下
target_layer=TimeDistributed(Dense(o_tokens.num(), use_bias=False))
# TODO decode out [batch_size, seq_len-1, 256]
dec_output=self.decoder(tgt_emb, tgt_seq, src_seq, enc_output, active_layers=active_layers)
# TODO final_output [batch_size, seq_len-1, 3665]
final_output=target_layer(dec_output)
因为错位训练的存在,L-1代表从源文本中除去<start>之外,第2个单词到最后<end>位置的信息表征,只需要将Linear的结果和实际的错位单词id进行比对即可计算该条样本的损失,源码中采用softmax交叉熵来计算每个L-1位置的loss,如果该位置实际为padding则忽略loss,最终采用所有实词位置的loss均值作为该样本的总损失,采用该批次的平均损失作为该批次的总损失。
def get_loss(y_pred, y_true):
y_true=tf.cast(y_true, 'int32')
# loss=[None, len_seq],输出每个样本,在每个词位置的softmax loss
loss=tf.nn.sparse_softmax_cross_entropy_with_logits(labels=y_true, logits=y_pred)
# TODO 对于padding位置的预测,该预测预测,但是不记入loss
mask=tf.cast(tf.not_equal(y_true, 0), 'float32')
# 对非mask位置求均值
loss=tf.reduce_sum(loss * mask, -1) / tf.reduce_sum(mask, -1)
loss=K.mean(loss)
return loss
全文完毕,本节基于训练过程探究Decoder的原理,下一节会基于预测部分来完整理解Decoder的工作方式。
来 发自 凹非寺
量子位 报道 | 公众号 QbitAI
4月9日,英伟达x量子位分享了一期nlp线上课程,来自NVIDIA的GPU计算专家、FasterTransformer 2.0开发者之一的薛博阳老师,与数百位开发者共同探讨了:
应读者要求,我们将分享内容整理出来,与大家一起学习。文末附有本次直播回放、PPT链接,大家也可直接观看。
以下为本次分享的内容整理:
大家好,今天为大家介绍的是FasterTransformer 2.0的原理与应用。
首先,参加本期直播的朋友对Transformer架构应该都有了一定了解。这个架构是在“Attention is All You Need”这篇论文中提出的。在BERT Encoder中使用了大量Transformer,效果很好。因此,Transformer已成为 NLP 领域中非常热门的深度学习网络架构。
但是,Transformer计算量通常是非常大的。因此,Transformer的时延往往难以满足实际应用的需求。
△Attention is All You Need截图
Transformer架构可以应用于Encoder或Decoder上。在Encoder中,Transformer包含1个multi-head attention和1个feed forward网络,在Decoder中,包含2个multi-head attention和1个feed forward网络。
其中,纯Encoder架构在目前的很多应用中都有很好的表现,比如Q&A系统、广告推荐系统等,因此,针对Encoder的优化是非常有必要的。
而在一些场景中,如翻译场景,我们需要Encoder和Decoder架构。在这种架构下,Decoder消耗的时间占比是非常高的,可能达到90%以上,是推理的主要瓶颈。因此,针对Decoder的优化也是一项重要的工作,能带来明显的加速效果。
实际应用中,FasterTransformer 1.0版本针对BERT中的Encoder为目标做了很多优化和加速。在2.0版本中,则主要新增了针对Decoder的优化,其优越的性能将助力于翻译、对话机器人、文字补全修正等多种生成式的场景。
上表比较了Encoder和Decoder计算量的不同。当我们需要编解码一个句子的时候,Encoder可以同时编码很多个字,甚至可以直接编码一个句子。
但是Decoder是一个解码的过程,每次只能解码一个字,因此,解码一个句子时我们需要多次Decoder的forward,对GPU更不友善。
△Faster Transformer框架
上图列出了FasterTransformer中针对BERT优化的模块。在编码方面,以BERT为基准,提供了一个单层的、等价于BERT Transformer 的模块,供使用者做调用。当我们需要多层的Transformer时,只需调用多次Encoder即可。
解码方面更为复杂,为了兼顾灵活性与效率,我们提供两个不同大小和效果的模块:
Decoder(黄色区块) 由单层的 Transformer layer 组成,它包含两个attention和一个feed forward 网络;而Decoding(蓝色区块)除了包含多层的 Transformer layer 之外,还包括了其他函数,例如 embedding_lookup、beam search、position Encoding 等等。
我们用一个简单的虚拟码展示Decoder和Decoding的区别。
在Decoding中通常有两个终止条件,一是是否达到预先设定的最大的sequence length,第二个条件是所有的句子是否都已经翻译完毕,未终止时会不断循环。
以句子长度为128的句子翻译场景为例,若其 Decoder 是由6层的 Transformer layer 组成的,总共需要调用 128x6=768 次的Decoder;如果是使用 Decoding 的话,则只需要调用一次Decoding,因此Decoding的推理效率更高。
小结
首先,FasterTransformer提供了高度优化过的Transformer layer:在Encoder方面是基于BERT实现的;在Decoder方面基于OpenNMT-TensorFlow开源的库做为标准;Decoding包含了翻译的整个流程,也是基于OpenNMT-TensorFlow。
其次,FasterTransformer 2.0的底层由CUDA和cuBLAS实现,支持FP16 和 FP32 两种计算模式,目前提供C++ API和TF OP。
现在,FasterTransformer 2.0已经开源,大家可以在DeepLearningExamples/FasterTransformer/v2 at master · NVIDIA/DeepLearningExamples · GitHub获取全部源代码。
先以Encoder为例。
△TF Encoder Transformer layer
参数:no XLA,batch size 1,12 heads,size per head 64,FP 32
图中蓝色方块表示GPU在实际运行,空白的表示GPU在闲置,因此GPU在很多时间是闲置状态。造成GPU闲置的原因是kernels太小,GPU要不断闲置以等待CPU启动kernel的时间,这也称为kernel launch bound问题。
如何解决这个问题?
我们尝试开启TF的XLA,其他参数不变。图中我们看到,从原本计算1层Transformer layer需要50个kernel缩减到24个左右。大部分kernel变得比较宽,虽有加速,但是闲置的时间还是比较多。
因此,我们提出FasterTransformer针对Encoder进行优化。
首先,我们把矩阵计算部分挑选出来,用NVIDIA高度优化的库cuBLAS 来计算,此外的部分,我们把能融合的kernel都尽可能融合起来。
最终的结果如上图右边,经过整体的优化后,我们只需要8个矩阵计算加6个kernel就可以完成单层Transformer layer计算,也就是说所需kernel从24个减少到14个。
我们可以看到,优化后每一个 kernel 都相对比较大,时间占比小的kernel也减少了。但还是有很多空白的片段。
我们直接调用C++ API,如图,GPU闲置的时间几乎没有了。因此,小batch size情况下,我们推荐使用C++ API以获得更快的速度。当batch size比较大时,GPU闲置时间会比较少。
接下来我们看下Decoder。
参数:no XLA,batch size 1,8 heads,size per head 64,FP32
经过统计,TF需要使用70个左右kernel来计算1层Transformer layer。直观来看,非常小、时间占比非常短的kernel更多。因此,batch size比较小的情况下,优化效果会更明显。
Decoder的优化同上述Encoder,特别之处是,Decoder里面的矩阵计算量非常少,因此我们把整个multi-head attention以一个kernel来完成。经过优化之后,原本需要70个kernel才能完成的计算,只需要使用16个kernel就能够完成。
在更大的Decoding模块中,另一个时间占比较多的kernel是beam search,这里我们针对top k做出优化。在GPU中可以同时执行多个block和多个warp,并行运算,大大节省时间。
△更多优化细节
大家可以在DeepLearningExamples/FasterTransformer/v2 at master · NVIDIA/DeepLearningExamples · GitHub根目录下找到对应资料:
针对 Decoder 和 Decoding,FasterTransformer 分别提供了 C++ 和 TensorFlow OP 这两种接口。
C++接口
首先创建一个Eecoder,超参数如图:
其次,设定训练好的模型权重;
设置好后,直接调用forward即可。
TF OP接口
首先,我们需要先载入OP。这里以Decoder为例,会自动创建TF需要使用的库,调用接口时先导入.so文件(图中已标红):
然后调用Decoder,放入input、权重、超参数,然后针对out put 做Session run。
这里需要注意的是,参数里有一个虚拟的输入 (pseudo input)。这个输入是为了避免 TensorFlow 的 decoder 和 FasterTransformer Decoder 发生并行,因为我们发现并行执行时,Decoder中的memory可能会被污染。实际应用的时候可以将这个输入拿掉。
最后我们来看下优化的效果如何。首先测试环境设置:
使用的GPU是NVIDIA的Tesla T4和V100。
Encoder模块在Tesla V100的结果
超参数设置:12 layers,32 sequence length,12 heads,64 size per head(BERT base),under FP 16
结果如上图,batch size从100逐步增加到500的过程中,FasterTransformer对比TF开启XLA,大概可以提供1.4倍的加速。
Decoder和Decoding模块在Tesla T4的结果
超参数设置:Batch size 1,beam width 4,8 heads,64 size per head,6 layers,vocabulary size 30000,FP 32
结果如上图,不同的sequence length下,相比于TF,FasterTransformer Decoder可以带来3.4倍左右的加速效果,Decoding可以带来7-8倍的加速,效率更高。
超参数设置:Batch size 256,sequence length 32,beam width 4,8 heads,64 size per head,6 layers,vocabulary size 30000
结果如上图,把batch size固定在较高值时,不同的FP下,FasterTransformer Decoder和Decoding也带来一定的加速效果。
最后,本次直播的PPT获取连接:「链接」
本次直播回放:NVIDIA Webinar
*请认真填写需求信息,我们会在24小时内与您取得联系。