Transformer 架构原理与自注意力机制详解
Transformer 是一种基于自注意力机制的深度学习模型,用于序列到序列任务。文章介绍了其核心组件,包括多头注意力、位置编码、残差连接及层归一化。详细阐述了编码器与解码器的结构差异,特别是 Masked Multi-Head Attention 的作用。通过 PyTorch 代码示例演示了自注意力机制中 Q、K、V 矩阵的计算流程,涵盖从输入嵌入到输出概率分布的全过程。此外,还分析了 Transformer 的优缺点及潜在应用场景。

Transformer 是一种基于自注意力机制的深度学习模型,用于序列到序列任务。文章介绍了其核心组件,包括多头注意力、位置编码、残差连接及层归一化。详细阐述了编码器与解码器的结构差异,特别是 Masked Multi-Head Attention 的作用。通过 PyTorch 代码示例演示了自注意力机制中 Q、K、V 矩阵的计算流程,涵盖从输入嵌入到输出概率分布的全过程。此外,还分析了 Transformer 的优缺点及潜在应用场景。

Transformer 是一种用于自然语言处理(NLP)和其他序列到序列(sequence-to-sequence)任务的深度学习模型架构,它在 2017 年由 Vaswani 等人首次提出。Transformer 架构引入了自注意力机制(self-attention mechanism),这是一个关键的创新,使其在处理序列数据时表现出色,彻底改变了 NLP 领域的研究范式。
Transformer 是一种用于自然语言处理(NLP)和其他序列到序列(sequence-to-sequence)任务的深度学习模型架构,它在 2017 年由 Vaswani 等人首次提出。Transformer 架构引入了自注意力机制(self-attention mechanism),这是一个关键的创新,使其在处理序列数据时表现出色。
以下是 Transformer 的一些重要组成部分和特点:
在原始论文中,Nx = 6,Encoder block 由 6 个 encoder 堆叠而成。图中的一个框代表的是一个 encoder 的内部结构,一个 Encoder 是由 Multi-Head Attention 和全连接神经网络 Feed Forward Network 构成。
简略结构(每一个编码器都对应上图的一个 encoder 结构):
Transformer 的编码组件是由 6 个编码器叠加在一起组成的,解码器同样如此。所有的编码器在结构上是相同的,但是它们之间并没有共享参数。
从编码器输入的句子首先会经过一个自注意力层,这一层帮助编码器在对每个单词编码的时候时刻关注句子的其它单词。解码器中的解码注意力层的作用是关注输入句子的相关部分,类似于 seq2seq 的注意力。原结构中使用到的是多头注意力机制(Multi-Head Attention),我们先从基础——自注意力机制开始讲起。
自注意力的作用:随着模型处理输入序列的每个单词,自注意力会关注整个输入序列的所有单词,帮助模型对本单词更好地进行编码。在处理过程中,自注意力机制会将对所有相关单词的理解融入到我们正在处理的单词中。更具体的功能如下:
自注意力的计算流程:
自注意力层的完善——'多头'注意力机制:
对应整体结构图中的 Multi-Head Attention。
前馈层只需要一个矩阵,则把得到的 8 个矩阵拼接在一起,然后用一个附加的权重矩阵 $W^O$ 与它们相乘。
总结整个流程:
编码 it 一词时,不同注意力的头集中在哪里,当我们编码 it 这个单词时:(图中只列举出了两个注意力):
为什么要用位置编码?
为了让模型理解单词的顺序,我们添加了位置编码向量,这些向量的值遵循特定的模式。位置编码公式如下: $$PE_{(pos, 2i)} = \sin(pos / 10000^{2i/d_{model}})$$ $$PE_{(pos, 2i+1)} = \cos(pos / 10000^{2i/d_{model}})$$ 其中 pos 是位置,i 是维度。这种设计使得模型能够学到相对位置信息。
在经过多头注意力机制得到矩阵 Z 之后,并没有直接传入全连接神经网络,而是经过了一步 Add&Normalize。
Add & Norm 层由 Add 和 Norm 两部分组成,其计算公式如下: $$LayerNorm(x + Sublayer(x))$$ 其中 X 表示 Multi-Head Attention 或者 Feed Forward 的输入,MultiHeadAttention(X) 和 FeedForward(X) 表示输出 (输出与输入 X 维度是一样的,所以可以相加)。
Add(残差连接) Add,就是在 z 的基础上加了一个残差块 X,加入残差块的目的是为了防止在深度神经网络的训练过程中发生退化的问题,退化的意思就是深度神经网络通过增加网络的层数,Loss 逐渐减小,然后趋于稳定达到饱和,然后再继续增加网络层数,Loss 反而增大。
为了了解残差块,我们引入 ResNet 残差神经网络,神经网络退化指的是在达到最优网络层数之后,神经网络还在继续训练导致 Loss 增大,对于多余的层,我们需要保证多出来的网络进行恒等映射。只有进行了恒等映射之后才能保证这多出来的神经网络不会影响到模型的效果。残差连接主要是为了防止网络退化。
上图就是构造的一个残差块,X 是输入值,F(X)是经过第一层线性变换后并且激活的输出,在第二层线性变化之后,激活之前,F(X)加入了这一层输入值 X,然后再进行激活后输出。
要恒等映射,我们只需要让 F(X)=0 就可以了。x 经过线性变换(随机初始化权重一般偏向于 0),输出值明显会偏向于 0,而且经过激活函数 Relu 会将负数变为 0,过滤了负数的影响。 这样当网络自己决定哪些网络层为冗余层时,使用 ResNet 的网络很大程度上解决了学习恒等映射的问题,用学习残差 F(x)=0 更新该冗余层的参数来代替学习 h(x)=x 更新冗余层的参数。
Normalize(层归一化) 归一化目的:
使用到的归一化方法是 Layer Normalization。 LN 是在同一个样本中不同神经元之间进行归一化,而 BN 是在同一个 batch 中不同样本之间的同一位置的神经元之间进行归一化。 BN 是对于相同的维度进行归一化,但是咱们 NLP 中输入的都是词向量,一个 300 维的词向量,单独去分析它的每一维是没有意义地,在每一维上进行归一化也是适合地,因此这里选用的是 LN。
全连接层是一个两层的神经网络,先线性变换,然后 ReLU 非线性,再线性变换。 这两层网络就是为了将输入的 Z 映射到更加高维的空间中然后通过非线性函数 ReLU 进行筛选,筛选完后再变回原来的维度。 经过 6 个 encoder 后输入到 decoder 中。
和 Encoder Block 一样,Encoder 也是由 6 个 decoder 堆叠而成的,Nx=6。包含两个 Multi-Head Attention 层。第一个 Multi-Head Attention 层采用了 Masked 操作。第二个 Multi-Head Attention 层的 K, V 矩阵使用 Encoder 的编码信息矩阵 C 进行计算,而 Q 使用上一个 Decoder block 的输出计算。
Masked Multi-Head Attention 与 Encoder 的 Multi-Head Attention 计算原理一样,只是多加了一个 mask 码。mask 表示掩码,它对某些值进行掩盖,使其在参数更新时不产生效果。Transformer 模型里面涉及两种 mask,分别是 padding mask 和 sequence mask。为什么需要添加这两种 mask 码呢?
padding mask 什么是 padding mask 呢?因为每个批次输入序列长度是不一样的也就是说,我们要对输入序列进行对齐。具体来说,就是给在较短的序列后面填充 0。但是如果输入的序列太长,则是截取左边的内容,把多余的直接舍弃。因为这些填充的位置,其实是没什么意义的,所以我们的 attention 机制不应该把注意力放在这些位置上,所以我们需要进行一些处理。 具体的做法是,把这些位置的值加上一个非常大的负数 (负无穷),这样的话,经过 softmax,这些位置的概率就会接近 0!
sequence mask sequence mask 是为了使得 decoder 不能看见未来的信息。对于一个序列,在 time_step 为 t 的时刻,我们的解码输出应该只能依赖于 t 时刻之前的输出,而不能依赖 t 之后的输出。因此我们需要想一个办法,把 t 之后的信息给隐藏起来。这在训练的时候有效,因为训练的时候每次我们是将 target 数据完整输入进 decoder 中地,预测时不需要,预测的时候我们只能得到前一时刻预测出的输出。 那么具体怎么做呢?也很简单:产生一个上三角矩阵,上三角的值全为 0。把这个矩阵作用在每一个序列上,就可以达到我们的目的。
注意:
Output 如图中所示,首先经过一次线性变换(线性变换层是一个简单的全连接神经网络,它可以把解码组件产生的向量投射到一个比它大得多的,被称为对数几率的向量里),然后 Softmax 得到输出的概率分布(softmax 层会把向量变成概率),然后通过词典,输出概率最大的对应的单词作为我们的预测输出。
优点:
缺点:
import torch
x = [
[1, 0, 1, 0], # Input 1
[0, 2, 0, 2], # Input 2
[1, 1, 1, 1] # Input 3
]
x = torch.tensor(x, dtype=torch.float32)
print("Input Tensor:", x)
输出: tensor([[1., 0., 1., 0.], [0., 2., 0., 2.], [1., 1., 1., 1.]])
Note:Q、K、V 矩阵在神经网络初始化的过程中,一般都是随机采样完成并且比较小,可以根据想要输出的维度来确定 Q、K、V 矩阵的维度。
w_key = [
[0, 0, 1],
[1, 1, 0],
[0, 1, 0],
[1, 1, 0]
]
w_query = [
[1, 0, 1],
[1, 0, 0],
[0, 0, 1],
[0, 1, 1]
]
w_value = [
[0, 2, 0],
[0, 3, 0],
[1, 0, 3],
[1, 1, 0]
]
w_key = torch.tensor(w_key, dtype=torch.float32)
w_query = torch.tensor(w_query, dtype=torch.float32)
w_value = torch.tensor(w_value, dtype=torch.float32)
print("Weights for key: \n", w_key)
print("Weights for query: \n", w_query)
print("Weights for value: \n", w_value)
输出: Weights for key: tensor([[0., 0., 1.], [1., 1., 0.], [0., 1., 0.], [1., 1., 0.]]) Weights for query: tensor([[1., 0., 1.], [1., 0., 0.], [0., 0., 1.], [0., 1., 1.]]) Weights for value: tensor([[0., 2., 0.], [0., 3., 0.], [1., 0., 3.], [1., 1., 0.]])
keys = x @ w_key
querys = x @ w_query
values = x @ w_value
print("Keys: \n", keys)
print("Querys: \n", querys)
print("Values: \n", values)
下图为得到的 key,query 和 value: (此处省略图片,实际应用中应查看矩阵数值变化)
attn_scores = querys @ keys.T
print(attn_scores)
输出: tensor([[ 2., 4., 4.], [ 4., 16., 12.], [ 4., 12., 10.]])
from torch.nn.functional import softmax
attn_scores_softmax = softmax(attn_scores, dim=-1)
print(attn_scores_softmax)
# 为了使得后续方便,这里简略将计算后得到的分数赋予了一个新的值
# For readability, approximate the above as follows
attn_scores_softmax = [
[0.0, 0.5, 0.5],
[0.0, 1.0, 0.0],
[0.0, 0.9, 0.1]
]
attn_scores_softmax = torch.tensor(attn_scores_softmax)
print(attn_scores_softmax)
输出: tensor([[0.0000, 0.5000, 0.5000], [0.0000, 1.0000, 0.0000], [0.0000, 0.9000, 0.1000]])
weighted_values = values[:,None] * attn_scores_softmax.T[:,:,None]
print(weighted_values)
输出: tensor([[[0.0000, 0.0000, 0.0000], [0.0000, 0.0000, 0.0000], [0.0000, 0.0000, 0.0000]],
[[1.0000, 4.0000, 0.0000],
[2.0000, 8.0000, 0.0000],
[1.8000, 7.2000, 0.0000]],
[[1.0000, 3.0000, 1.5000],
[0.0000, 0.0000, 0.0000],
[0.2000, 0.6000, 0.3000]]])
(此处省略图片,实际计算为对 weighted_values 沿特定维度求和)
重复步骤 4-7,获取到 input2、input3 的结果向量
至此,我们完成了 Self-Attention 的基本实现逻辑。在实际工程中,通常会结合 PyTorch 的 nn.MultiheadAttention 模块来优化性能,并处理 Batch 维度及 Padding 掩码等细节。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online