Encoder-Decoder是为seq2seq(序列到序列)量身打造的一个深度学习框架,在机器翻译、机器问答等领域有着广泛的应用。这是一个抽象的框架,由两个组件:Encoder(编码器)和 Decoder(解码器)组成。对于给定的输入 s o u r c e ( x 1 , x 2 , . . . , x n ) source(x_1,x_2,...,x_n) source(x1,x2,...,xn),首先编码器将其编码成一个中间表示向量 z = ( z 1 , z 2 , . . . , z n ) z = (z_1,z_2,...,z_n) z=(z1,z2,...,zn)。接着,解码器根据 z 和解码器自身前面的输出,来生成下一个单词,如下图所示:
一种标准的 Encoder-Decoder 框架:
class EncoderDecoder(nn.Module):
# A standard Encoder-Decoder architecture. Base for this and many other models.
def __init__(self, encoder, decoder, src_embed, tgt_embed, generator):
super(EncoderDecoder, self).__init__()
self.encoder = encoder
self.decoder = decoder
self.src_embed = src_embed
self.tgt_embed = tgt_embed
self.generator = generator
def forward(self, src, tgt, src_mask, tgt_mask):
"Take in and process masked src and target sequences."
return self.decode(self.encode(src, src_mask), src_mask,
tgt, tgt_mask)
def encode(self, src, src_mask):
return self.encoder(self.src_embed(src), src_mask)
def decode(self, memory, src_mask, tgt, tgt_mask):
return self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)
在实际应用中,编码器和解码器可以有多种组合,比如 (RNN,RNN)、(CNN,RNN) 等等,这就是传统的 seq2seq 框架。后来引入了attention机制,上述框架也被称为 “分心模型”。为什么说他”分心“呢?因为对于解码器来说,他在生成每一个单词的时候,中间向量的每一个元素对当前生成词的贡献都是一样的。Attention 的思想则是对于当前生成的单词,中间向量 z 的每个元素对其贡献的重要程度不同,跟其强相关的赋予更大的权重,无关的则给一个很小的权重。
举个例子: 假如我们要将 ”knowledge is power“ 翻译成中文,在翻译”knowledge“这个单词时, 显然”knowledge“这个单词对翻译出来的”知识“贡献最大,其他两个单词贡献就很小了。这实际上让模型有个区分度,不会被无关的东西干扰到,翻译出来的准确度当然也就更高了。在这里Attention其实还是一个小弟,主角仍然是RNN、CNN这些大佬.
我们不妨先顺着这个思路往下想,attention在这里充当了Encoder和Decoder的一个桥梁,事实证明有很好的效果。既然效果这么好,那在Encoder中是不是也可以用呢?文本自身对自身的编码进行有区分度的表示,事实上,这在以往的很多文本分类的工作中已被采用[2]。这看上去已经是个值得尝试的good idea了。继续开脑洞,Encoder都用了,Decoder能落后吗,好歹人家是一对CP,当然要妇唱夫随了。于是,Encoder和Decoder都用了自注意力(self-attention)。
回想一下,到这里我们已经在三个地方用到了注意力机制了。这时候RNN大佬不愿意了,原本我的名声地盘都被你们分走了,散伙!Attention反正是初生牛犊不怕虎,说好,分分账分道扬镳吧,反正你的序列计算并行不起来一直让人诟病,没你我可能更潇洒。于是两兄弟就分开了。相见时难别亦难,RNN老大哥深谋远虑,临走时不忘嘱咐一句”苟富贵,勿相忘!“。于是一个故事的结束就成了另一个故事的开始,注意力就此开启创业之路,寒来暑往,春去秋来,在黑暗中不断寻找光亮,学习PPT技巧,终于有一天,它的PPT做完了,找到了融资,破茧成蝶,横空出道,并给自己取了个亮闪闪的名字:Transformer, 自此,一个新的时代开始了。
一般认为,BERT 的强大效果,很大一部分原因来源于 Transformer。
Transformer 遵循 Encoder-Decoder 架构:
class Encoder(nn.Module):
"Core encoder is a stack of N layers"
def __init__(self, layer, N):
super(Encoder, self).__init__()
self.layers = clones(layer, N)
self.norm = LayerNorm(layer.size)
def forward(self, x, mask):
"Pass the input (and mask) through each layer in turn."
for layer in self.layers:
x = layer(x, mask)
return self.norm(x)
以上便是 Encoder 的核心实现,它由N个 EncoderLayer 组成。输入一次通过每个 EncoderLayer,然后经过一个归一化层。
EncoderLayer 如下图所示:
实现 EncoderLayer:
class EncoderLayer(nn.Module):
"Encoder is made up of self-attn and feed forward (defined below)"
def __init__(self, size, self_attn, feed_forward, dropout):
super(EncoderLayer, self).__init__()
self.self_attn = self_attn
self.feed_forward = feed_forward
self.sublayer = clones(SublayerConnection(size, dropout), 2)
self.size = size
def forward(self, x, mask):
x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))
return self.sublayer[1](x, self.feed_forward)
残差网络就是在正常的前向传播基础上多了一条通道(图中右边弯的那一条线),这个通道里的 x 可以无损通过,这样就可以避免梯度消失(求导时多了一个常数项)。
最终的输出结果就等于通道里的 x 加上 sublayer 层的前向传播结果,不过需要注意的是,这里输入进来的时候做了个 Norm归一化
实现残差网络:
class SublayerConnection(nn.Module):
"""
A residual connection followed by a layer norm.
Note for code simplicity the norm is first as opposed to last.
"""
def __init__(self, size, dropout):
super(SublayerConnection, self).__init__()
self.norm = LayerNorm(size)
self.dropout = nn.Dropout(dropout)
def forward(self, x, sublayer):
"Apply residual connection to any sublayer with the same size."
return x + self.dropout(sublayer(self.norm(x)))
综上可以看出 EncoderLayer 的结构:其中包含两层(sublayer),一个是 Multi-Head Self-Attention 层,另一个是前馈神经网络(feed-forward)。
Encoder 执行过程:输入 x 先进入 Multi-Head Self-Attention,用一个残差网络加成,接着通过前馈网络,再用一个残差网络加成
对于网上的资料暂时只能理解到这里,后续理解完善再更新。
BERT 的全称是 Bidirectional Encoder Representation from Transformers,即双向 Transformer 的 Encoder,因为decoder是不能获要预测的信息的。模型的主要创新点都在 pre-train 方法上,即用了 Masked LM 和 Next Sentence Prediction 两种方法分别捕捉词语和句子级别的representation。