Transformer深入理解(持续更新)
编码器:原文是6个编码器堆叠(数字6没有什么神奇之处,你也可以尝试其他数字)
解码组件部分也是由相同数量(与编码器对应)的解码器(decoder)组成的。
所有的编码器在结构上都是相同的,但它们没有共享参数。每个编、解码器都可以分解成两个子层:自注意力层和前馈神经网络,
我们首先将每个输入单词通过词嵌入算法转换为词向量,每个单词都被嵌入为512维的向量
将输入序列进行词嵌入之后,每个单词都会流经编码器中的两个子层。
接下来我们看看Transformer的一个核心特性,在这里输入序列中每个位置的单词都有自己独特的路径流入编码器。在自注意力层中,这些路径之间存在依赖关系。而前馈(feed-forward)层没有这些依赖关系。因此在前馈(feed-forward)层时可以并行执行各种路径。
位置编码操作:
位置编码操作的scale
self.embed_scale = tf.math.sqrt(tf.cast(embed_dim, tf.float32))
embedded_tokens = embedded_tokens * self.embed_scale
embedded_positions = self.position_embeddings(positions)
return embedded_tokens + embedded_positions
代码在这里进行了一个放大的操作,乘上一个数得到更大的结果,可能位置编码的影响就减少了
理解编码器:
1、词嵌入:每个单词被嵌入为E维向量
2、位置编码:词嵌入与未知编码向量相加得到基于时间步的嵌入,输入到注意力层,这里可以使用函数式位置编码,也可以使用嵌入式位置编码向量
3、自注意力层:自注意力计算:词向量和三个权重矩阵(E×K)相乘计算三个K维向量(QKV)查询向量q、键向量k、值向量v;q分别和各个词向量的k进行点乘,得到一个分数,除以缩放以后,通过softmax输出结果,这里如果有mask,需要注意力结果进行遮盖,各个词向量的v再呈上softmax分数,最后求和得到q对应的最终K维向量
4、多头注意力:它给出了注意力层的多个“表示子空间”,多个查询/键/值权重矩阵集,8个头的话,就形成8个最终K维向量,然后拼接后形成8K维的向量,再和一个权重矩阵(8K×E)相乘,得到E维的向量
5、前向反馈网络:每个单词的自注意力结果向量再输入到一个全连接前馈网络,这个全连接有两层,第一层的激活函数是ReLU,第二层是一个线性激活函数。
理解解码器:
1、词嵌入、位置编码、mask的输入
2、自注意力层:需要对每个位置的单词进行注意力,也是有mask的标签遮蔽以及序列pad遮蔽
3、编码-解码注意力层:输入的query是自注意力层的输出,key和value是编码器的输出。
4、前馈神经网络:如前
理解多头注意力和自注意力:
https://zhuanlan.zhihu.com/p/231631291
著名的Transformer是基于Attention机制构建的,当前最流行的Attention机制是Scaled-Dot Attention,数学公式为:
通俗理解是:本来有两个句子,现需要对比两个句子;于是第一个句子用矩阵 表示出来,第二个句子用矩阵 和矩阵 表示出来;计算的时候先用 和 点积,再经过softmax激活,得到两个句子的相似性,值的大小也表示着我需要把注意力放在 的哪个位置上;再用得到的结果乘
其中:
: : 表示的是seq_len,也就是第一个句子的长度;表示的是第一个句子中每个字的特征向量维度,在BERT中是768;
: : 表示的是seq_len,也就是第二个句子的长度 ; 这里 和中的一样;
: : 表示的是seq_len,也就是第二个句子的长度; 表示第二个句子中每个字的特征向量维度。
:缩放因子起到调节作用,使得内积不至于太大(太大的话softmax后就非0即1了,不够“soft”了)。
这里有几个点需要注意:
- Q和K在特征向量维度上是一致的,因为在点积计算的时候两个句子虽然长度不一样,但是特征维度需要一样;
- K和V在seq_len维度上是一致的,因为K和V 表示的是同一个句子,但是Q和K的矩阵shape不一样
- 以上的结论表示的通用情况下,自注意力是两个一样的句子比较,也就是我注意我自己,是一种特殊情况。
矩阵计算如下:
Q和K计算后为: ,缩放因子和softmax并不改变矩阵shape,再和 V 计算后 , 总的计算路径为 也就是说,经过这层计算后,是把 序列变成了 的序列。
理解mask:
什么是掩码张量:
掩代表遮掩,码就是我们张量中的数值,它的尺寸不定,里面一般只有1和0的元素,代表位置被遮掩或者不被遮掩,至于是0位置被遮掩还是1位置被遮掩可以自定义,因此它的作用就是让另外一个张量中的一些数值被遮掩,也可以说被替换, 它的表现形式是一个张量
掩码张量的作用:
在transformer中, 掩码张量的主要作用在应用attention(将在下一小节讲解)时,有一些生成的attention张量中的值计算有可能已知了未来信息而得到的,未来信息被看到是因为训练时会把整个输出结果都一次性进行Embedding,但是理论上解码器的的输出却不是一次就能产生最终结果的,而是一次次通过上一次结果综合得出的,因此,未来的信息可能被提前利用. 所以,我们会进行遮掩. 关于解码器的有关知识将在后面的章节中讲解.
transformer中的mask主要分为两个部分:padding mask 和 sequence mask。
padding mask 处理不定长数据
padding mask就是去除padding部分的影响。比如在nlp中,每个句子的长度不一样,但是框架中只支持矩阵运算,因此要想办法弄成相同的长度,长的剪掉,短的用零补够。
可是attention的本意实在句子中的词间进行操作,如果直接attention,却有可能把注意力放在了补零的位置,这是无意义的,所以要把他事先排除掉。具体的做法是,把这些位置加上一个非常大的负数(可以是负无穷),这样的话,经过softmax,这些位置的概率就会接近0.
sequence mask 处理防止标签外漏
sequence mask是为了使decoder不能看到未来的信息。也就是对于一个序列,在时间步t,我们的解码输出应该只依赖于t时刻之前的输出,而不能依赖于t之后的输出。因此我们需要把t之后的信息隐藏起来。怎么做呢?只需产生一个下三角矩阵,上三角的值为0,下三角的值全为1,对角线也是1.把这个矩阵作用在一个序列上,即达到了我们的目的。
sequence mask 一般是通过生成一个上三角为0的矩阵来实现的,上三角区域对应要mask的部分。
对于decoder的self-attention,同时需要padding mask 和 sequence mask,具体实现就是两个mask相加。对于其他情况,只用padding mask。
对于Decoder,在batch训练时会同时需要padding mask和sequence mask。
在测试时为单例,仅需要加上sequence mask,这样可以固定住每一步生成的词,让前面生成的词在self-attention时不会包含后面词的信息,这样也使测试和训练保持了一致。
不过,在测试过程中,预测生成的句子长度逐步增加,因此每一步都会要生成新的sequence mask矩阵的,是维度逐步增加的下三角方阵。
keras源码解释:
mask是个加法操作,-inf 负无穷在softmax的时候是0,代表注意力是0,就是没注意到
def _masked_softmax(self, attention_scores, attention_mask=None):
# Normalize the attention scores to probabilities.
# `attention_scores` = [B, N, T, S]
if attention_mask is not None:
# The expand dim happens starting from the `num_heads` dimension,
# (<batch_dims>, num_heads, <query_attention_dims, key_attention_dims>)
# 这里是扩展维度,比如输入的补齐mask需要扩展到softmax的维度
mask_expansion_axes = [-len(self._attention_axes) * 2 - 1]
for _ in range(len(attention_scores.shape) - len(attention_mask.shape)):
attention_mask = array_ops.expand_dims(
attention_mask, axis=mask_expansion_axes)
return self._softmax(attention_scores, attention_mask)
softmax源码:这里是个进行mask是个加法操作。
@keras_export('keras.layers.Softmax')
class Softmax(Layer):
"""Softmax activation function.
Example without mask:
>>> inp = np.asarray([1., 2., 1.])
>>> layer = tf.keras.layers.Softmax()
>>> layer(inp).numpy()
array([0.21194157, 0.5761169 , 0.21194157], dtype=float32)
>>> mask = np.asarray([True, False, True], dtype=bool)
>>> layer(inp, mask).numpy()
array([0.5, 0. , 0.5], dtype=float32)
Input shape:
Arbitrary. Use the keyword argument `input_shape`
(tuple of integers, does not include the samples axis)
when using this layer as the first layer in a model.
Output shape:
Same shape as the input.
Args:
axis: Integer, or list of Integers, axis along which the softmax
normalization is applied.
Call arguments:
inputs: The inputs, or logits to the softmax layer.
mask: A boolean mask of the same shape as `inputs`. Defaults to `None`. The
mask specifies 1 to keep and 0 to mask.
Returns:
softmaxed output with the same shape as `inputs`.
"""
def __init__(self, axis=-1, **kwargs):
super(Softmax, self).__init__(**kwargs)
self.supports_masking = True
self.axis = axis
def call(self, inputs, mask=None):
if mask is not None:
# Since mask is 1.0 for positions we want to keep and 0.0 for
# masked positions, this operation will create a tensor which is 0.0 for
# positions we want to attend and -1e.9 for masked positions.
adder = (1.0 - math_ops.cast(mask, inputs.dtype)) * (
_large_compatible_negative(inputs.dtype))
# Since we are adding it to the raw scores before the softmax, this is
# effectively the same as removing these entirely.
inputs += adder
if isinstance(self.axis, (tuple, list)):
if len(self.axis) > 1:
return math_ops.exp(inputs - math_ops.reduce_logsumexp(
inputs, axis=self.axis, keepdims=True))
else:
return backend.softmax(inputs, axis=self.axis[0])
return backend.softmax(inputs, axis=self.axis)
训练和推理:
训练和推理使用了完整的一次attention mask
推理输出和训练输出一样,只不过推理的时候一次次利用上一个输出填充进序列而已,因为不知道decoder的下一个输入是什么样的,但是训练的时候因为知道完整的序列,所以使用mask直接就可以全部一次性输出