论文原文:Attention Is All You Need
资料参考:
Transformer模型详解(图解最完整版)
Transformers
前排提示:后面代码库里的函数更新了,参数个数对不上,我还没改,所以跑不起来,嘿嘿( ⓛ ω ⓛ )。
- 将Transformer模型简化可以表示为一个由6层编码组成的编码器和6层解码组成的编码器
①嵌入层Embedding
将输入文本转化为对应的向量形式 ,在本模型中,向量长度为512。
在向量构建后会经过一个softmax进行标准化,然后将每一个元素放大
d
m
o
d
e
l
\sqrt{d_{model}}
dmodel ,目的是让向量元素的大小接近于下面将要计算的位置信息编码的元素大小以便更好运算。
②位置编码层
由于注意力机制能够汇总全局信息(也就是即使打乱单词的排列顺序,最后的输出结果也是相同的),不必像RNN必须按照序列进行计算(只需要计算向量/矩阵,具有很强的并行性)。为了方便之后的并行计算,在此处对输入序列中的每个单词位置进行编码,形成一个整体的位置矩阵。本层的向量长度为512,值均在[-1, 1]。
P
E
(
p
o
s
,
2
i
)
=
s
i
n
(
p
o
s
/
1000
0
2
i
/
d
m
o
d
e
l
)
P E(pos,2i) = sin(pos/10000^{2i/d_{model}} )
PE(pos,2i)=sin(pos/100002i/dmodel)
P
E
(
p
o
s
,
2
i
+
1
)
=
c
o
s
(
p
o
s
/
1000
0
2
i
/
d
m
o
d
e
l
)
P E(pos,2i+1) = cos(pos/10000^{2i/d_{model}} )
PE(pos,2i+1)=cos(pos/100002i/dmodel)
- p o s pos pos 代表元素位置; i i i 代表维度
- 通过频率 p o s 1000 0 i / d m o d e l \frac{pos}{10000^{i/d_{model}}} 10000i/dmodelpos 来控制函数的波长,频率不断减小,则波长不断变大,此时函数对位置的变动越不敏感。
使用周期函数进行编码改善了:
- 整形标记位置没有上界;
- 整形标记遇见比训练时所用的序列更长的序列不利于模型的泛化。
- [0,1]范围标记序列长度不同时,token间的相对距离是不一样的。例如在序列长度为3时,token间的相对距离为0.5;在序列长度为4时,token间的相对距离就变为0.33。
- 二进制向量表示在离散空间中不连续的问题。
将 ①嵌入层、②位置编码层 两层的向量做加法,将位置信息与语义信息进行融合形成最终输入编码
③多头自注意力机制层(训练)
将相同输入经过
h
h
h 种不同的线性变换,计算8次(
h
=
8
h=8
h=8)点积自注意力计算。
M
u
l
t
i
H
e
a
d
(
Q
,
K
,
V
)
=
C
o
n
c
a
t
(
h
e
a
d
1
,
.
.
.
,
h
e
a
d
h
)
W
O
MultiHead(Q, K, V ) = Concat(head1, ..., headh)W^O
MultiHead(Q,K,V)=Concat(head1,...,headh)WO
w
h
e
r
e
h
e
a
d
i
=
A
t
t
e
n
t
i
o
n
(
Q
W
i
Q
,
K
W
i
K
,
V
W
i
V
)
where\ head_i = Attention(QW^Q_i , KW^K_i ,VW^V_i)
where headi=Attention(QWiQ,KWiK,VWiV)
其中的W均为可迭代权重矩阵。
将结束自注意力计算后的8个输出(下图记为
Z
i
Z_i
Zi)进行拼接(Concat)继续进行线性变换。
我认为这里是在计算某一个单词与句子中其余单词的相关度(这时能够兼顾计算到所有的词汇,包括该单词之后的单词,这也是为什么在预测层的多头注意力机制中需要增加mask来屏蔽t以及t+1之后的权重)
将 ②位置编码层、③多头自注意力机制层 进行残差连接(加和)
④LayerNorm标准化层
在经过加和后进行标准化。这里比较一下两个标准化方法的区别:BatchNorm和LayerNorm。
BatchNorm相当于取矩阵的列;LayerNorm相当于取矩阵的行。
在二维空间中,我们可以把一组样本记作batch。batch里面的每一个样本组成它的行,记为sample;每一个样本通过一个向量来表示它的各种特征,记为feature,多种feature组成了向量的列。对于每一个样本来讲,它们的feature个数可能并不相同(这也是为什么Transform中没有选择使用BatchNorm),这时会使用0来对较短的向量进行填充。
上面我们已经取了矩阵的行/列,下面就要对它们进行标准化处理,使其变成均值为0,方差为1的一组向量(当然啦,也可以按需求是其他的数字)。具体计算是将这组向量元素减去这组向量的均值再除以这组向量的方差。
由于在进行翻译(一个例子)时,会输入多个句子,每个句子会有多个单词,每个单词会有多个维度的特征,这就构成了一个三维矩阵(seq(序列)→word→feature)。
由于seq不一定等长,这时候取feature进行标准化计算就会有一些弊端:
- 在训练时,如果seq的长度相差过多,将会填充多个0进去,这将会使得经过标准化后的均值和方差在不同的feature上产生较大的抖动;
- 在预测时,如果遇到过长的句子,那么之前计算出来的均值和方差有可能并不合适,将失效。
而由于取sample进行标准化是在本seq(也就是说它的计算长度是固定的)上运算,那么得出的结果将会比较稳定。(有什么弊端我不太清楚)
class AddNorm(nn.Module):
"""残差连接后进行层规范化"""
def __init__(self, normalized_shape, dropout, **kwargs): # 形状 丢弃率
super(AddNorm, self).__init__(**kwargs)
self.dropout = nn.Dropout(dropout)
self.ln = nn.LayerNorm(normalized_shape)
def forward(self, X, Y):
return self.ln(self.dropout(Y) + X)
⑤Position-wise Feed-Forward Networks全连接层
Position-wise是指在输入的句子的每个单词(也就是说全连接层作用在最后一个维度)位置进行全连接层的操作,每个位置的变换相同。由于在attention层已经计算了相关程度(汇聚完成),故在本层仅进行每个点的运算即可。
该全连接层为单隐藏层,由两个线性变换组成,中间有一个 ReLU 激活函数。
F
F
N
(
x
)
=
m
a
x
(
0
,
x
W
1
+
b
1
)
W
2
+
b
2
FFN(x) = max(0, xW1 + b1)W2 + b2
FFN(x)=max(0,xW1+b1)W2+b2
参数数量变化如下:注意力输出层512→隐藏层2048→隐藏层512→下一个注意力输入层
class PositionWiseFFN(nn.Module):
"""基于位置的前馈网络"""
def __init__(self, ffn_num_input, ffn_num_hiddens, ffn_num_outputs,
**kwargs): # **kwargs允许函数或方法接受任意数量的关键字参数
super(PositionWiseFFN, self).__init__(**kwargs)
self.dense1 = nn.Linear(ffn_num_input, ffn_num_hiddens) # 第一层512→2048
self.relu = nn.ReLU()
self.dense2 = nn.Linear(ffn_num_hiddens, ffn_num_outputs) # 第二层2048→512
# 前向传播
def forward(self, X):
return self.dense2(self.relu(self.dense1(X)))
编码器整体代码
class TransformerEncoder(d2l.Encoder):
"""Transformer编码器"""
def __init__(self, vocab_size, key_size, query_size, value_size, # 词汇表大小
num_hiddens, norm_shape, ffn_num_input, ffn_num_hiddens,
num_heads, num_layers, dropout, use_bias=False, **kwargs):
super(TransformerEncoder, self).__init__(**kwargs)
self.num_hiddens = num_hiddens
self.embedding = nn.Embedding(vocab_size, num_hiddens)
self.pos_encoding = d2l.PositionalEncoding(num_hiddens, dropout)
self.blks = nn.Sequential()
for i in range(num_layers):
self.blks.add_module("block"+str(i),
EncoderBlock(key_size, query_size, value_size, num_hiddens,
norm_shape, ffn_num_input, ffn_num_hiddens,
num_heads, dropout, use_bias))
def forward(self, X, valid_lens, *args):
# 因为位置编码值在-1和1之间,
# 因此嵌入值乘以嵌入维度的平方根进行缩放,
# 然后再与位置编码相加。
X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens))
self.attention_weights = [None] * len(self.blks) # 初始化一个None列表,长度等于self.blks子模块的数量
for i, blk in enumerate(self.blks):
X = blk(X, valid_lens)
self.attention_weights[i] = blk.attention.attention.attention_weights # 将第i个编码器块的注意力权重存储在列表的第i个位置上
return X
Encoder
Decoder
⑥带有掩码的多头注意力机制层
掩码的屏蔽原理:将t及之后的概率改为极大的负值,使得计算权重时十分接近0。
Mask操作:
将输入层的最后一个④输出的向量作为Key和Value,输出层第一个⑥作为Query输出层第一个③层
我认为它的过程大概是在编码器中计算原始句子中词汇间的相关度,在输入进解码层后计算原始词汇与新输入词汇的相似度,赋予高相似度词汇更大的权重,同时与该词汇高相关度的词汇也会得到更大的权重。
解码器块
class DecoderBlock(nn.Module):
"""解码器中第i个块"""
def __init__(self, key_size, query_size, value_size, num_hiddens,
norm_shape, ffn_num_input, ffn_num_hiddens, num_heads,
dropout, i, **kwargs):
super(DecoderBlock, self).__init__(**kwargs)
self.i = i
self.attention1 = d2l.MultiHeadAttention(
key_size, query_size, value_size, num_hiddens, num_heads, dropout)
self.addnorm1 = AddNorm(norm_shape, dropout)
self.attention2 = d2l.MultiHeadAttention(
key_size, query_size, value_size, num_hiddens, num_heads, dropout)
self.addnorm2 = AddNorm(norm_shape, dropout)
self.ffn = PositionWiseFFN(ffn_num_input, ffn_num_hiddens,
num_hiddens)
self.addnorm3 = AddNorm(norm_shape, dropout)
def forward(self, X, state):
enc_outputs, enc_valid_lens = state[0], state[1]
# 训练阶段,输出序列的所有词元都在同一时间处理,
# 因此state[2][self.i]初始化为None。
# 预测阶段,输出序列是通过词元一个接着一个解码的,
# 因此state[2][self.i]包含着直到当前时间步第i个块解码的输出表示
if state[2][self.i] is None:
key_values = X
else:
key_values = torch.cat((state[2][self.i], X), axis=1)
state[2][self.i] = key_values
if self.training:
batch_size, num_steps, _ = X.shape
# dec_valid_lens的开头:(batch_size,num_steps),
# 其中每一行是[1,2,...,num_steps]
dec_valid_lens = torch.arange(
1, num_steps + 1, device=X.device).repeat(batch_size, 1)
else:
dec_valid_lens = None
# 自注意力
X2 = self.attention1(X, key_values, key_values, dec_valid_lens)
Y = self.addnorm1(X, X2)
# 编码器-解码器注意力。
# enc_outputs的开头:(batch_size,num_steps,num_hiddens)
Y2 = self.attention2(Y, enc_outputs, enc_outputs, enc_valid_lens)
Z = self.addnorm2(Y, Y2)
return self.addnorm3(Z, self.ffn(Z)), state
解码器整体代码
class TransformerDecoder(d2l.AttentionDecoder):
def __init__(self, vocab_size, key_size, query_size, value_size,
num_hiddens, norm_shape, ffn_num_input, ffn_num_hiddens,
num_heads, num_layers, dropout, **kwargs):
super(TransformerDecoder, self).__init__(**kwargs)
self.num_hiddens = num_hiddens
self.num_layers = num_layers
self.embedding = nn.Embedding(vocab_size, num_hiddens)
self.pos_encoding = d2l.PositionalEncoding(num_hiddens, dropout)
self.blks = nn.Sequential()
for i in range(num_layers):
self.blks.add_module("block"+str(i),
DecoderBlock(key_size, query_size, value_size, num_hiddens,
norm_shape, ffn_num_input, ffn_num_hiddens,
num_heads, dropout, i))
self.dense = nn.Linear(num_hiddens, vocab_size)
def init_state(self, enc_outputs, enc_valid_lens, *args):
return [enc_outputs, enc_valid_lens, [None] * self.num_layers]
def forward(self, X, state):
X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens))
self._attention_weights = [[None] * len(self.blks) for _ in range (2)]
for i, blk in enumerate(self.blks):
X, state = blk(X, state)
# 解码器自注意力权重
self._attention_weights[0][
i] = blk.attention1.attention.attention_weights
# “编码器-解码器”自注意力权重
self._attention_weights[1][
i] = blk.attention2.attention.attention_weights
return self.dense(X), state
@property
def attention_weights(self):
return self._attention_weights
训练
num_hiddens, num_layers, dropout, batch_size, num_steps = 32, 2, 0.1, 64, 10
lr, num_epochs, device = 0.005, 200, d2l.try_gpu()
ffn_num_input, ffn_num_hiddens, num_heads = 32, 64, 4
key_size, query_size, value_size = 32, 32, 32
norm_shape = [32]
train_iter, src_vocab, tgt_vocab = d2l.load_data_nmt(batch_size, num_steps)
encoder = TransformerEncoder(
len(src_vocab), key_size, query_size, value_size, num_hiddens,
norm_shape, ffn_num_input, ffn_num_hiddens, num_heads,
num_layers, dropout)
decoder = TransformerDecoder(
len(tgt_vocab), key_size, query_size, value_size, num_hiddens,
norm_shape, ffn_num_input, ffn_num_hiddens, num_heads,
num_layers, dropout)
net = d2l.EncoderDecoder(encoder, decoder)
d2l.train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)
# 输出:loss 0.030, 5202.9 tokens/sec on cuda:0