Transformer理解

参考博客:
https://jalammar.github.io/illustrated-transformer/
https://github.com/aespresso/a_journey_into_math_of_ml
https://www.tensorflow.org/tutorials/text/transformer#encoder_and_decoder

       学了 TextCNN、LSTM后,谈起如今NLP最流行、最热的模型,当然是Transformer、bert,语言模型、命名实体识别、机器翻译等任务,很多都开始用Transformer,或者说是bert预训练模型来做,在机器阅读理解榜单中(SQuAD2.0),机器成绩已经超越人类表现!
这些天看了几个经典博客、视频,最后读了一遍源码,加深了对模型的理解,整体结构也基本上理顺了。

背景:

        transformer是谷歌大脑在2017年底发表的论文 attention is all you need中所提出的seq2seq模型。现在已经取得了大范围的应用和扩展,而BERT就是从 transformer中衍生出来的预训练语言模型。
        其中主要的应用的方式是2步——先进行预训练语言模型——然后把预训练的模型适配给下游任务(分类、生成、标记等)。其中:预训练模型非常重要,预训练的模型的性能直接影响下游任务。

原理

先上图吧:

        整个Transformer结构分为:Encoding(编码器)和Decoding(解码器)两大部分;而编码器又有N个编码器层,解码器也有N个解码器层;
        那么先把一个编码器层搞清楚,串联N个就能理解了;理解好了编码器,解码器就快了。
一个编码器层包含五个组成部分:
$\begin{cases} 1. Positional Encoding\\2.Multi-Head Attention\\3. Add\&Norm\\4.FeedForward\\5.Add\&Norm\end{cases}$
看似很复杂,一个一个来就不怕:

1.Positional Encoding

        首先得清楚为何Transformer用位置嵌入?在LSTM中我们用一个词一个词灌进去,从而学习了时序关系,但是transformer模型没有循环神经网络的迭代操作, 它是将所有词一起喂进去,并行操作的。所以我们必须提供每个字的位置信息给transformer, 才能识别出语言中的顺序关系。
        位置嵌入的定义其实就是作者自定义的一个函数,来做到区别每个词在句子中的位置,仅此而已。
定义 :
        位置嵌入的维度为$[max \ sequence \ length, \ embedding \ dimension]$, 嵌入的维度同词向量的维度, $max \ sequence \ length$属于超参数, 指的是限定的最大单个句长.
        在transformer模型中,一般以字为单位,不需要分词了, 首先我们要初始化字向量为$[vocab \ size, \ embedding \ dimension]$, $vocab \ size$为总共的字库数量, $embedding \ dimension$为字向量的维度, 也是每个字的向量。(这里的理解和之前的TextCNN LSTM中的$Embedding$一致!)
        在这里论文中使用了$sine$和$cosine$函数的线性变换来提供给模型位置信息:
$$PE_{(pos,2i)} = sin(pos / 10000^{2i/d_{\text{model}}}) \quad PE_{(pos,2i+1)} = cos(pos / 10000^{2i/d_{\text{model}}})$$
上式中$pos$指的是句中字的位置, 取值范围是$[0, \ max \ sequence \ length)$, $i$指的是词向量的维度, 取值范围是$[0, \ embedding \ dimension)$, 上面有$sin$和$cos$一组公式, 也就是对应着$embedding \ dimension$维度的一组奇数和偶数的序号的维度, 例如$0, 1$一组, $2, 3$一组, 分别用上面的$sin$和$cos$函数做处理, 从而产生不同的周期性变化。
看源码(就是对应的上面的公式):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def get_angles(pos, i, d_model):
angle_rates = 1 / np.power(10000, (2 * (i//2)) / np.float32(d_model))
return pos * angle_rates
def positional_encoding(position, d_model):
angle_rads = get_angles(np.arange(position)[:, np.newaxis],
np.arange(d_model)[np.newaxis, :],
d_model)

# 将 sin 应用于数组中的偶数索引(indices);2i
angle_rads[:, 0::2] = np.sin(angle_rads[:, 0::2])

# 将 cos 应用于数组中的奇数索引;2i+1
angle_rads[:, 1::2] = np.cos(angle_rads[:, 1::2])

pos_encoding = angle_rads[np.newaxis, ...]

return tf.cast(pos_encoding, dtype=tf.float32)

输出这个周期性的矩阵图:

1
2
3
4
5
6
7
8
9
pos_encoding = positional_encoding(50, 512)
print (pos_encoding.shape)

plt.pcolormesh(pos_encoding[0], cmap='RdBu')
plt.xlabel('Depth')
plt.xlim((0, 512))
plt.ylabel('Position')
plt.colorbar()
plt.show()

2.Muti-Head Attention

       在前面的基础上,我们已经有了词向量矩阵和位置嵌入了,例如有一些样本。维度是:$[batch size, \ sequence \ length]$,再在字典中找到对应的字向量,变为:$[batch size, \ sequence \ length, \ embedding \ dimension]$,同时我们再加上位置嵌入(位置嵌入维度一致,直接元素相加即可),相加后的维度还是$[batch size, \ sequence \ length, \ embedding \ dimension]$
        要了解Muti-Head Attention,首先要知道self-attention,Multi无非是在其基础上并行了多个头而已。

2.1 self Attention

        Attention机制的创新点就在于这里,为了学到多重含义的表示,我们想让一个字的向量包含这句话所有字的一个相关程度(后面还会说),那么首先初始化三个权重矩阵$W_Q、W_K、W_V$,然后将$X_{embedding}$与这三个权重矩阵相乘,得到$Q、K、V$
也就是:
$$\begin{cases}Q=X_{embedding} W_Q \ K=X_{embedding} W_K \ V=X_{embedding} W_V\end{cases}$$
下面用图来理解更舒适!

得到了$Q、K、V$之后 那么我们用$q\times k$也就是对于一个字(中文是字,英文是词)它的score包含所有的自身$q$和别的字的$k$相乘,当然这里相乘肯定是和$k$的转置相乘哈!从而就可以得到一个注意力矩阵!(点积:两个向量越相似,点积则越大!)这里你会观察到,对角线上也就是每个字,自己对自己的相关程度,一行就是一个字中所有字与它的相关性。然后再对每一行做归一化($softmax$),这样就保证对一个字来说,所有字与它的相关程度概率和为1!

然后论文中又除了一个$\sqrt{d_k}$,是为了把注意力矩阵变成标准正态分布,使得softmax归一化后的结果更加稳定,以便于反向传播时候获取平衡的梯度,最后将注意力矩阵给$V$加权,为啥要给$V$加权,其实就是因为注意力矩阵维度是$[batch \ size, \ sequence \ length, \ sequence \ length]$,而$V$维度是$[batch \ size ,\ sequence \ length, \ embedding \ dimension]$为了使得维度保持不变,则乘以$V$后为: $[batch \ size ,\ sequence \ length, \ embedding \ dimension]$,从而再次和$X_{embedding}$的维度相同了,是不是很妙!
$${Attention(Q, K, V) = softmax_k(\frac{QK^T}{\sqrt{d_k}}) V} $$
下图是论文中对$d_k$的解释:
论文

源码分析:

包含$Q,K$相乘,注意到相乘的时候有个转置操作,以及后面对$V$加权,和我们给出的公式其实是一致的,这里的mask语句后面会讲。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def scaled_dot_product_attention(q, k, v, mask):
"""计算注意力权重。
q, k, v 必须具有匹配的前置维度。
k, v 必须有匹配的倒数第二个维度,例如:seq_len_k = seq_len_v。
虽然 mask 根据其类型(填充或前瞻)有不同的形状,
但是 mask 必须能进行广播转换以便求和。

参数:
q: 请求的形状 == (..., seq_len_q, depth)
k: 主键的形状 == (..., seq_len_k, depth)
v: 数值的形状 == (..., seq_len_v, depth_v)
mask: Float 张量,其形状能转换成
(..., seq_len_q, seq_len_k)。默认为None。

返回值:
输出,注意力权重
"""

matmul_qk = tf.matmul(q, k, transpose_b=True) # (..., seq_len_q, seq_len_k)

# 缩放 matmul_qk
dk = tf.cast(tf.shape(k)[-1], tf.float32)
scaled_attention_logits = matmul_qk / tf.math.sqrt(dk)

# 将 mask 加入到缩放的张量上。
if mask is not None:
scaled_attention_logits += (mask * -1e9)

# softmax 在最后一个轴(seq_len_k)上归一化,因此分数 (对注意力矩阵每一行归一化,则每个字的注意力向量一行,就是与其余字的相关程度,和为1)
# 相加等于1。
attention_weights = tf.nn.softmax(scaled_attention_logits, axis=-1) # (..., seq_len_q, seq_len_k) 注意力矩阵

output = tf.matmul(attention_weights, v) # (..., seq_len_q, depth_v) 再把注意力矩阵乘V

return output, attention_weights
2.2 Multi-Head Attention

        那么对于多头其实是一样的,只是在开始的时候将$embedding \ dimension$分割成了$h$份(头的个数),这里每个头权重都不同,多头训练效果理论上肯定更好(反正想着就是这样,至于为什么,也不好解释)
,最后再把它及联拼接。大佬的图展现的很好:
多头级联

源码分析:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class MultiHeadAttention(tf.keras.layers.Layer):
def __init__(self, d_model, num_heads):
super(MultiHeadAttention, self).__init__()
self.num_heads = num_heads
self.d_model = d_model

assert d_model % self.num_heads == 0

self.depth = d_model // self.num_heads #本来QKV维度:【batch size,seq.length,embed dim】,拆分后就是【batch size,seq length,h,embed dim/h】
# depth 就是用embed dim 除头的个数

self.wq = tf.keras.layers.Dense(d_model) #初始化qkv矩阵
self.wk = tf.keras.layers.Dense(d_model)
self.wv = tf.keras.layers.Dense(d_model)

self.dense = tf.keras.layers.Dense(d_model)

def split_heads(self, x, batch_size):
"""拆分embedding dimension维度到 (num_heads, depth),
这里的 depth=embed dim/h
转置结果使得形状为 (batch_size, num_heads, seq_len, depth)
"""
x = tf.reshape(x, (batch_size, -1, self.num_heads, self.depth))
return tf.transpose(x, perm=[0, 2, 1, 3])

def call(self, v, k, q, mask):
batch_size = tf.shape(q)[0]

q = self.wq(q) # (batch_size, seq_len, d_model)
k = self.wk(k) # (batch_size, seq_len, d_model)
v = self.wv(v) # (batch_size, seq_len, d_model)

q = self.split_heads(q, batch_size) # (batch_size, num_heads, seq_len_q, depth)
k = self.split_heads(k, batch_size) # (batch_size, num_heads, seq_len_k, depth)
v = self.split_heads(v, batch_size) # (batch_size, num_heads, seq_len_v, depth)

# scaled_attention.shape == (batch_size, num_heads, seq_len_q, depth) 之前没有分头的时候是:【batch size,seq
#length,embed dim】
# attention_weights.shape == (batch_size, num_heads, seq_len_q, seq_len_k)
scaled_attention, attention_weights = scaled_dot_product_attention(
q, k, v, mask)

scaled_attention = tf.transpose(scaled_attention, perm=[0, 2, 1, 3]) # (batch_size, seq_len_q, num_heads, depth) 转置操作

concat_attention = tf.reshape(scaled_attention,
(batch_size, -1, self.d_model)) # (batch_size, seq_len_q, d_model) 级联操作 看下图

output = self.dense(concat_attention) # (batch_size, seq_len_q, d_model)

return output, attention_weights
2.3 Mask

有两个mask:$\begin{cases}1.padding \ mask \\2.lookahead mask(翻译任务中预测文本时(decoder部分))\end{cases}$

2.3.1 padding mask

        当我们在确定$Max \ length$时候,对于不够长的句子肯定要做$padding$但是对于为0的那一部分在$softmax$时候会变为1:
        回顾$softmax$函数:

$$
\sigma (\mathbf {z} )_{i}= \frac {e^{z_i} } {\sum _{j=1} ^ {K} e^ {z_j} }
$$

$$ $$

$e^0$是1, 是有值的, 这样的话 $softmax$ 中被 $padding$ 的部分就参与了运算, 就等于是让无效的部分参与了运算,这样肯定不对, 这时就需要做一个$mask$让这些无效区域不参与运算, 我们一般给无效区域加一个很大的负数的偏置, 也就是:

$$z_{illegal}=z_{illegal}+bias_{illegal}$$
$$bias_{illegal}\to-\infty$$
$$e^{z_{illegal}}\to0$$

源码分析:
1
2
3
4
5
6
def create_padding_mask(seq):
seq = tf.cast(tf.math.equal(seq, 0), tf.float32)

# 添加额外的维度来将填充加到
# 注意力对数(logits)。
return seq[:, tf.newaxis, tf.newaxis, :] # (batch_size, 1, 1, seq_len)

输出效果(把原本为0的地方变成了1):

1
2
x = tf.constant([[7, 6, 0, 0, 1], [1, 2, 3, 0, 0], [0, 0, 0, 4, 5]])
create_padding_mask(x)
最后再在一句代码中体现,如果mask不是None,则在此处乘以负无穷:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def scaled_dot_product_attention(q, k, v, mask):
"""计算注意力权重。
q, k, v 必须具有匹配的前置维度。
k, v 必须有匹配的倒数第二个维度,例如:seq_len_k = seq_len_v。
虽然 mask 根据其类型(填充或前瞻)有不同的形状,
但是 mask 必须能进行广播转换以便求和。

参数:
q: 请求的形状 == (..., seq_len_q, depth)
k: 主键的形状 == (..., seq_len_k, depth)
v: 数值的形状 == (..., seq_len_v, depth_v)
mask: Float 张量,其形状能转换成
(..., seq_len_q, seq_len_k)。默认为None。

返回值:
输出,注意力权重
"""

matmul_qk = tf.matmul(q, k, transpose_b=True) # (..., seq_len_q, seq_len_k)

# 缩放 matmul_qk
dk = tf.cast(tf.shape(k)[-1], tf.float32)
scaled_attention_logits = matmul_qk / tf.math.sqrt(dk)

# 将 mask 加入到缩放的张量上。
if mask is not None:
scaled_attention_logits += (mask * -1e9)

# softmax 在最后一个轴(seq_len_k)上归一化,因此分数 (对注意力矩阵每一行归一化,则每个字的注意力向量一行,就是与其余字的相关程度,和为1)
# 相加等于1。
attention_weights = tf.nn.softmax(scaled_attention_logits, axis=-1) # (..., seq_len_q, seq_len_k) 注意力矩阵

output = tf.matmul(attention_weights, v) # (..., seq_len_q, depth_v) 再把注意力矩阵乘V

return output, attention_weights
2.3.2 Lookahead mask

        这里我也不知道咋翻译(前瞻遮罩?)这个mask操作就是在翻译的时候,要预测第三个词,将仅使用第一个和第二个词,与此类似,预测第四个词,仅使用第一个,第二个和第三个词,依此类推。

源码分析:
1
2
3
def create_look_ahead_mask(size):
mask = 1 - tf.linalg.band_part(tf.ones((size, size)), -1, 0)
return mask # (seq_len, seq_len)

输出结果:
(每一行是一个时刻,第一个时刻,遮盖了后两个(遮盖操作后也就是变为了1)),用第一个字预测第二个字;第二个时刻,遮盖了第三个字,用第1、2个字预测第三个字;第三个时刻,则是用前三个字去预测结束符……当然这里其实每次预测的时候解码器还加上了编码器输出的embedding向量,这一点后面会详细说!

1
2
3
4
x = tf.random.uniform((1, 3))
temp = create_look_ahead_mask(x.shape[1])
print(x)
temp

3.Add&Norm

3.1残差连接

归纳:模型太深,需要避免梯度消失
        我们在上一步得到了经过注意力矩阵加权之后的$V$, 也就是$Attention(Q, \ K, \ V)$, 我们对它进行一下转置, 使其和$X_{embedding}$的维度一致, 也就是$[batch \ size, \ sequence \ length, \ embedding \ dimension]$, 然后把他们加起来做残差连接, 直接进行元素相加, 因为他们的维度一致:
$$X_{embedding} + Attention(Q, \ K, \ V)$$
在之后的运算里, 每经过一个模块的运算, 都要把运算之前的值和运算之后的值相加, 从而得到残差连接, 训练的时候可以使梯度直接走捷径反传到最初始层:
$$X + SubLayer(X) $$

3.2 $LayerNorm$:

归纳:加速收敛
        $Layer Normalization$的作用是把神经网络中隐藏层归一为标准正态分布, 也就是$i.i.d$独立同分布, 以起到加快训练速度, 加速收敛的作用:
$$ \mu_i=\frac {1} {m}\sum^{m} _ { i=1 } x _ {ij} $$
上式中以矩阵的行$(row)$为单位求均值;
$$\sigma^{2} _ { j } =\frac { 1 } { m } \sum^ { m } _ { i=1 } (x _ { ij } -\mu_ { j } )^ { 2 } $$
上式中以矩阵的行$(row)$为单位求方差;
$$LayerNorm(x)=\alpha \odot \frac{x_{ij}-\mu_{i}}
{\sqrt{\sigma^{2}_{i}+\epsilon}} + \beta \tag{eq.6}$$
然后用每一行每一个元素减去这行的均值, 再除以这行的标准差, 从而得到归一化后的数值, $\epsilon$是为了防止除$0$;
之后引入两个可训练参数$\alpha, \ \beta$来弥补归一化的过程中损失掉的信息, 注意$\odot$表示元素相乘而不是点积, 我们一般初始化$\alpha$为全$1$, 而$\beta$为全$0$.

源码分析:

在源码中就是一个加号代表了一切!out1 = self.layernorm1(x + attn_output)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class EncoderLayer(tf.keras.layers.Layer):
def __init__(self, d_model, num_heads, dff, rate=0.1):
super(EncoderLayer, self).__init__()

self.mha = MultiHeadAttention(d_model, num_heads)
self.ffn = point_wise_feed_forward_network(d_model, dff) # 前向传播

self.layernorm1 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
self.layernorm2 = tf.keras.layers.LayerNormalization(epsilon=1e-6)

self.dropout1 = tf.keras.layers.Dropout(rate)
self.dropout2 = tf.keras.layers.Dropout(rate)

def call(self, x, training, mask):

attn_output, _ = self.mha(x, x, x, mask) # (batch_size, input_seq_len, d_model) 三个x代表上一层的输出,N个encoder是串联的
attn_output = self.dropout1(attn_output, training=training)
out1 = self.layernorm1(x + attn_output) # (batch_size, input_seq_len, d_model) 残差连接

ffn_output = self.ffn(out1) # (batch_size, input_seq_len, d_model)
ffn_output = self.dropout2(ffn_output, training=training)
out2 = self.layernorm2(out1 + ffn_output) # (batch_size, input_seq_len, d_model)

return out2

4.FeedForward

       前向传播层,就是一个全连接,dff设置了内部全连接层数,和以往的没什么区别,不多说了。

源码分析:
1
2
3
4
5
def point_wise_feed_forward_network(d_model, dff): # dff内部层维数
return tf.keras.Sequential([
tf.keras.layers.Dense(dff, activation='relu'), # (batch_size, seq_len, dff)
tf.keras.layers.Dense(d_model) # (batch_size, seq_len, d_model)
])

Transformer encoder整体结构

1). 字向量与位置编码:
$$X = EmbeddingLookup(X) + PositionalEncoding $$
$$X \in \mathbb{R}^{batch \ size \ * \ seq. \ len. \ * \ embed. \ dim.} $$
2). 自注意力机制:
$$Q = Linear(X) = XW_{Q}$$
$$K = Linear(X) = XW_{K} $$
$$V = Linear(X) = XW_{V}$$
$$X_{attention} = SelfAttention(Q, \ K, \ V) $$
3). 残差连接与$Layer \ Normalization$
$$X_{attention} = X + X_{attention} $$
$$X_{attention} = LayerNorm(X_{attention}) $$
4). $FeedForward$, 其实就是两层线性映射并用激活函数激活, 比如说$ReLU$:
$$X_{hidden} = Activate(Linear(Linear(X_{attention})))$$
5). 重复3).:
$$X_{hidden} = X_{attention} + X_{hidden}$$
$$X_{hidden} = LayerNorm(X_{hidden})$$
$$X_{hidden} \in \mathbb{R}^{batch \ size \ * \ seq. \ len. \ * \ embed. \ dim.} $$

Transformer decoder部分

        还是这个图最直观,注意观察编码器和解码器的差异,最下面的一块其实差不多,只是解码器加了一个Mask,这个mask当然是Lookahead mask,因为翻译任务里面,我们是在解码器中输入一个词,解码器拿着编码器最终隐藏层输出的向量来预测下一个词,所以需要去遮盖后面的词: **解码器预测过程:** 第1时刻——输入'I,解码器拿着编码器输出的embedding向量去预测'am'。 第2时刻——输入'am',解码器拿着embedding向量 + 'I' 去预测 'a' 第3时刻——输入'a',编码器拿着embedding向量 + 'I' + 'a' 去预测 'student'...
源码分析:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class DecoderLayer(tf.keras.layers.Layer):
def __init__(self, d_model, num_heads, dff, rate=0.1):
super(DecoderLayer, self).__init__()

self.mha1 = MultiHeadAttention(d_model, num_heads) #decoder中有两个MultiHeadAttention,最下面一个有Lookahead mask,上面一个有padding mask
self.mha2 = MultiHeadAttention(d_model, num_heads)

self.ffn = point_wise_feed_forward_network(d_model, dff)

self.layernorm1 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
self.layernorm2 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
self.layernorm3 = tf.keras.layers.LayerNormalization(epsilon=1e-6)

self.dropout1 = tf.keras.layers.Dropout(rate)
self.dropout2 = tf.keras.layers.Dropout(rate)
self.dropout3 = tf.keras.layers.Dropout(rate)


def call(self, x, enc_output, training,
look_ahead_mask, padding_mask):
# enc_output.shape == (batch_size, input_seq_len, d_model)

attn1, attn_weights_block1 = self.mha1(x, x, x, look_ahead_mask) # (batch_size, target_seq_len, d_model)下面一个,这里的输入和encoder一样 也是三个x
attn1 = self.dropout1(attn1, training=training)
out1 = self.layernorm1(attn1 + x)

attn2, attn_weights_block2 = self.mha2(
enc_output, enc_output, out1, padding_mask) # (batch_size, target_seq_len, d_model) 上面一个,这里的输入不同,要注意:是两个encoder输出和一个decoder输出;但是维数都是一样的
attn2 = self.dropout2(attn2, training=training)
out2 = self.layernorm2(attn2 + out1) # (batch_size, target_seq_len, d_model)

ffn_output = self.ffn(out2) # (batch_size, target_seq_len, d_model)
ffn_output = self.dropout3(ffn_output, training=training)
out3 = self.layernorm3(ffn_output + out2) # (batch_size, target_seq_len, d_model)

return out3, attn_weights_block1, attn_weights_block2 # 再有N个decoder串联

        这里的self.mha1self.mha2就是图中解码器层定义的两个Multi-Head Attention,这里上面的Multi-Head Attention也就是self.mha2,它是只做了padding mask的,这个和编码器的一致,但是下面的这个Multi-Head Attention(self.mha1)就不一样了,它的mask自然是Lookahead mask,用于遮盖后面的词,现在基本上前后就可以串起来了!
注意看两个的输入:

x, x, look_ahead_mask)```
1
```self.mha2(enc_output, enc_output, out1, padding_mask)

        对于下面的Multi-Head Attentionself.mha1,它和编码器层那里的代码一致,都是接收三个相同的x(也就是q、k、v)
        但是对于上面的Multi-Head Attentionself.mha2,它的输入是不同的,它是用的编码器的输出和解码器下面的Multi-Head Attentionself.mha1的输出out1来共同输出out3,之前不理解为什么编码器那里要写三个x,写一个不也可以吗?反正都是一样,现在明白了,是为了和解码器这里的输入做到格式一致!!!
        编码器层、解码器层都理解完了,最后编码器串联N个,解码器串联N个就OK啦!
        一个图说明了一切:

        之前不理解为什么画了这么多的线,一根线不行吗?还真不行,因为每次的解码器在预测的时候需要拿编码器输出的embedding向量呀!!

源码分析:

        编码器和解码器无非就是做了N个编码器层和解码器层,然后这里的training代表的是否训练,因为训练的时候和预测的时候不一样。
编码器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Encoder(tf.keras.layers.Layer):
def __init__(self, num_layers, d_model, num_heads, dff, input_vocab_size,
maximum_position_encoding, rate=0.1):
super(Encoder, self).__init__()

self.d_model = d_model
self.num_layers = num_layers

self.embedding = tf.keras.layers.Embedding(input_vocab_size, d_model)
self.pos_encoding = positional_encoding(maximum_position_encoding,
self.d_model)


self.enc_layers = [EncoderLayer(d_model, num_heads, dff, rate)
for _ in range(num_layers)]

self.dropout = tf.keras.layers.Dropout(rate)

def call(self, x, training, mask):

seq_len = tf.shape(x)[1]

# 将嵌入和位置编码相加。
x = self.embedding(x) # (batch_size, input_seq_len, d_model)
x *= tf.math.sqrt(tf.cast(self.d_model, tf.float32))
x += self.pos_encoding[:, :seq_len, :]

x = self.dropout(x, training=training)

for i in range(self.num_layers):
x = self.enc_layers[i](x, training, mask) #上一层的输出是下一层的输入 体现在这里

return x # (batch_size, input_seq_len, d_model)

解码器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Decoder(tf.keras.layers.Layer):
def __init__(self, num_layers, d_model, num_heads, dff, target_vocab_size,
maximum_position_encoding, rate=0.1):
super(Decoder, self).__init__()

self.d_model = d_model
self.num_layers = num_layers

self.embedding = tf.keras.layers.Embedding(target_vocab_size, d_model)
self.pos_encoding = positional_encoding(maximum_position_encoding, d_model)

self.dec_layers = [DecoderLayer(d_model, num_heads, dff, rate)
for _ in range(num_layers)]
self.dropout = tf.keras.layers.Dropout(rate)

def call(self, x, enc_output, training,
look_ahead_mask, padding_mask):

seq_len = tf.shape(x)[1]
attention_weights = {}

x = self.embedding(x) # (batch_size, target_seq_len, d_model)
x *= tf.math.sqrt(tf.cast(self.d_model, tf.float32))
x += self.pos_encoding[:, :seq_len, :]

x = self.dropout(x, training=training)

for i in range(self.num_layers):
x, block1, block2 = self.dec_layers[i](x, enc_output, training,
look_ahead_mask, padding_mask)

attention_weights['decoder_layer{}_block1'.format(i+1)] = block1
attention_weights['decoder_layer{}_block2'.format(i+1)] = block2

# x.shape == (batch_size, target_seq_len, d_model)
return x, attention_weights