transformer组成模块介绍——自注意力机制

目录


前言

transformer笔记,用于学习或回顾transformer中的注意力机制,主要学习其中的公式与代码,浅显易懂。


自注意力机制

自注意力机制其实从属于注意力机制,那么注意力机制是什么呢?

注意力机制是一种用于捕捉输入序列不同位置之间相关性的技术 。其核心思想是通过为每个单词分配不同的权重 ,突出与当前任务关联性较强的信息。

通俗理解:依赖于给定任务,机器能够分清输入的重点内容更好完成任务。如翻译。

自注意力从属于注意力,根据其开头增加的一个自字 ,我们可以大概明白该机制更加依赖于输入的序列本身。

通俗理解就是能够结合上下文理解当前语境

例:

  1. 小明摔倒了,他感觉痛并大哭。
  2. 小明摔倒了,因为他感觉痛并且他年纪还小所以他大哭。

传统的神经网路如RNN按照单词的顺序分析,难以捕捉长句子中词语间关系 。可能在第一句能分析痛和哭的关系,而在第二个句子会受到年纪还小影响无法分析痛和哭。

通过自注意力机制,会计算痛与其他单词间的相关性,即使在第二个句子中痛和哭的距离较远,注意力机制依旧可以通过权重调整捕捉到痛和哭的关系。

注意力公式介绍

在注意力机制中,有这三个非常重要的向量
Q:查询是主观意识的特征向量,外生给定
K:键是物体的突出特征信息向量,外生给定
V:值则是代表物体本身的特征向量(输入一个句子机器无法直接理解,所以需要词向)

注意力机制涉及到了两个公式

  1. 相关性计算 F ( Q , K ) F(Q,K) F(Q,K):
    • 点积: S i m i l a r i t y ( Q , K ) = Q ∗ K Similarity(Q, K) = Q * K Similarity(Q,K)=Q∗K,通过点积可以提取出语义信息的方向相关性,计算起来简单。
    • 余弦相似度: S i m i l a r i t y ( Q , K ) = Q K ∣ Q ∣ ∣ K ∣ Similarity(Q, K) = \frac{Q K}{|Q| |K|} Similarity(Q,K)=∣Q∣∣K∣QK,与点积的不同在于除以了二者的模长,相当于两个方向向量的相关性提取,更加专注语义信息的方向相关性
  2. 类softmax:
    先将注意力得分根据维度缩放再进行softmax归一化 ,维度缩放让输出更加平滑
    S i m i = S i d i Sim_{i} = \frac{S_{i}}{\sqrt{d_{i}}} Simi=di Si
    a i = S o f t m a x ( S i m i ) = e S i m i Σ j e S i m j a_{i} = Softmax(Sim_{i}) = \frac{e^{Sim_{i}}}{\Sigma_{j} e^{Sim_{j}}} ai=Softmax(Simi)=ΣjeSimjeSimi

自注意力公式介绍

从属于注意力的自注意力自然少不了QKV,但其QK与注意力机制的QK不同

从上图注意力机制计算流程,我们知道计算权重的计算过程X并没有参与

而对于自注意力,因为其专注于自,所以Q与K在计算的时候X也参与了进去。
Q = X W Q Q = X W_{Q} Q=XWQ( W Q W_{Q} WQ是权重矩阵,会对X进行线性变换,其参数会随着神经网络的优化而不断更新),K也有权重矩阵 W K W_{K} WK

下面这张图是一个自注意力机制图

刚接触可能会比较懵,我们加上矩阵大小变换来讲可能能帮助理解

公式过程中矩阵的大小变换(括号内为该矩阵大小)

PS:该图中简化计算,QK大小相同,但在实际运用如英译中,源语言序列长度(len_k)和目标语言序列长度(len_q)通常是不同的

  1. X: (6, 3)

    代表了1个句子由6个词组成,一共6个词向量 ,每个词向量有3个维度

  2. W q , W k , W v W_{q}, W_{k}, W{v} Wq,Wk,Wv: (4, 6)

    大小原因: Q = W q X Q = W_{q}X Q=WqX,图中 Q Q Q的大小为(4, 3),所以这里的 W q W_{q} Wq大小为(4, 6)

    (矩阵乘法的基本知识 ( 4 , 6 ) ⋅ ( 6 , 3 ) = ( 4 , 3 ) (4, 6) \cdot (6, 3) = (4, 3) (4,6)⋅(6,3)=(4,3))
    K与V的乘法与Q一致

  3. Q , K , V Q,K,V Q,K,V:(4, 3)

  4. a t t e n t i o n = S o f t m a x ( K T ⋅ Q D k ) attention = Softmax(\frac{K^{T} \cdot Q}{\sqrt{D_k}}) attention=Softmax(Dk KT⋅Q):(3, 3)

    大小原因: K T ( 3 , 4 ) ⋅ Q ( 4 , 3 ) = ( 3 , 3 ) K^{T}(3, 4) \cdot Q(4, 3) = (3, 3) KT(3,4)⋅Q(4,3)=(3,3)

    根据Q和K使用点乘计算相关性 ,接着除以词向量维度的根号进行特征缩放 ,最后进行softmax(与注意力机制的公式一样

  5. H = V ⋅ a t t e n t i o n H = V\cdot attention H=V⋅attention: (4, 3)

    大小原因: V ( 4 , 3 ) ⋅ a t t e n t i o n ( 3 , 3 ) = H ( 4 , 3 ) V(4, 3) \cdot attention(3, 3) = H(4, 3) V(4,3)⋅attention(3,3)=H(4,3)
    采用缩放点积作为注意力打分函数

多头自注意力机制

多头自注意力是对单头自注意力的扩展 ,通过并行计算多个自注意力机制 ,能够捕获不同子空间的信息,提高模型的表达能力和性能。其实就是计算多个H并merge在一起,最后需要进行一个线性变换哦转换成单头自注意力的H大小

掩码矩阵

因为自注意力机制本质上会涉及到这个序列的所有内容 ,而在进行输出的时候我们是不希望未来的词组数据泄露 给当前词组的,所以引入了掩码矩阵。该矩阵对目标序列中未来位置的注意力分数设置为负无穷,从而将这些分数的 softmax 结果变为零。其作用体现在 a t t e n t i o n attention attention的计算中
a t t e n t i o n = S o f t m a x ( K T ⋅ Q + M D k ) attention = Softmax(\frac{K^{T} \cdot Q + M}{\sqrt{D_k}}) attention=Softmax(Dk KT⋅Q+M)M为掩码矩阵

这个掩码矩阵如下面的公式所示
M i j = { 0 if i ≥ j − ∞ if i < j M_{ij} = \begin{cases} 0 & \text{if } i \geq j \\ -\infty & \text{if } i < j \end{cases} Mij={0−∞if i≥jif i<j

M = ( 0 − ∞ − ∞ − ∞ 0 0 − ∞ − ∞ 0 0 0 − ∞ 0 0 0 0 ) M = \begin{pmatrix} 0 & -\infty & -\infty & -\infty \\ 0 & 0 & -\infty & -\infty \\ 0 & 0 & 0 & -\infty \\ 0 & 0 & 0 & 0 \end{pmatrix} M= 0000−∞000−∞−∞00−∞−∞−∞0

个人理解:每一行都代表了一个词向量 ,所以当前( i i i)位置的词向量只能看到过去位置( i > = j i >= j i>=j)的词向量的 a t t e n t i o n attention attention ,而不能看到之后词向量 的attention,所以当 i < j i < j i<j时其attention应该为0

自注意力代码

分为

python 复制代码
# 掩码矩阵
# 构造一个和注意力得分一样大小矩阵,说明哪个位置是PAD部分,之后在计算计算softmax之前会把这里置为无穷大;
# 一定需要注意的是这里得到的矩阵形状是batch_size x len_q x len_k,我们是对k中的pad符号进行标识,并没有对q中的做标识
# seq_q 和 seq_k 不一定一致,在交互注意力,q来自解码端,k来自编码端,seq是向量序列哦
def get_attn_pad_mask(seq_q, seq_k):
    batch_size, len_q = seq_q.size()
    batch_size, len_k = seq_k.size()
    # eq(0)函数留下了值为0的坐标
    pad_attn_mask = seq_k.data.eq(0).unsqueeze(1)  # batch_size x 1 x len_k
    return pad_attn_mask.expand(batch_size, len_q, len_k)  # batch_size x len_q x len_k
    
# 计算多头注意力得分
class ScaledDotProductAttention(nn.Module):
    def __init__(self):
        super(ScaledDotProductAttention, self).__init__()

    def forward(self, Q, K, V, attn_mask):
        """
        QKV的大小应该都一样
        :param Q: (batch_size, n_heads, len_q, d_k),代表了每一个多头下的所有词向量的中间矩阵
        :param K: (batch_size, n_heads, len_k, d_k)
        :param V: (batch_size, n_heads, len_v, d_k)
        :param attn_mask: (batch_size, n_heads, len_q, len_k)
        :return:
        """
        ## 输入进来的维度分别是 [batch_size x n_heads x len_q x d_k]  K: [batch_size x n_heads x len_k x d_k]  V: [batch_size x n_heads x len_k x d_v]
        ##首先经过matmul函数得到的scores形状是 : [batch_size x n_heads x len_q x len_k]
        # matmul就是矩阵乘法,其中对于多维向量会先提取出batch(会广播到最大batch),反正要压缩到最后的一个二维矩阵进行相乘
        scores = torch.matmul(Q, # (batch_size, n_head, len_q, d_k)
                              K.transpose(-1, -2)  # (batch_size, n_head, d_k, len_k) 相当于k的转置,矩阵乘法
                              ) / np.sqrt(d_k)  # 除以根号d_k

        ## 下面这个就是用到了我们之前重点讲的attn_mask,把被mask的地方置为无限小,softmax之后基本就是0,对q的单词不起作用
        # attn_mask是上三角矩阵,已经是False和True的了,True的位置会被填values,也就是1e-9
        # score的大小为(batch_size, n_head, len_q, len_k)
        # attn_mask大小为(batch_size, n_head, len_q, len_k)
        scores.masked_fill_(attn_mask, -1e9)  # 将上三角未来信息进行隐藏,越往下代表了这个词在句子中的位置越靠后
        attn = nn.Softmax(dim=-1)(scores)  # 进行激活(batch_size, n_head, len_q, len_k)
        context = torch.matmul(attn, V)  # 最后计算(batch_size, n_head, len_q, len_k) X (batch_size, n_head, len_v, d_k), len_k = len_v
        # context = (batch_size, n_head, len_q, d_k)
        # attn = (batch_size, n_head, len_q, len_k)
        return context, attn

# 多头注意力机制
class MultiHeadAttention(nn.Module):
    def __init__(self):
        super(MultiHeadAttention, self).__init__()
        ## 我们会使用映射linear做一个映射得到参数矩阵W_q, W_k,W_v
        # d_k 代表向量维度一般取64,固定值
        # n_head是多头的数量
        # 单个自注意力机制的组成是由:attention = softmax(Q * K转置 / sqrt(d_k))
        self.W_Q = nn.Linear(d_model, d_k * n_heads)  # 输入一个(loc, d_model)的词向量,将其转换为(loc, d_k * n_head)
        self.W_K = nn.Linear(d_model, d_k * n_heads)  # 同Q,K输出大小与Q一致
        self.W_V = nn.Linear(d_model, d_v * n_heads)
        self.linear = nn.Linear(n_heads * d_v, d_model)  # 将注意力表格再次转换为原来规定的维度,权重矩阵的线性变换
        self.layer_norm = nn.LayerNorm(d_model)  # 进行层标准化,一行代码的事,输入层大小

    def forward(self, Q, K, V, attn_mask):
        """
        
        :param Q: X,这里的Q需要经过Wq线性变换才能变为目标Q
        :param K: X,需要经过Wk线性变换
        :param V: X,需要经过Wv线性变换
        :param attn_mask: 是掩码矩阵,防止未来信息泄露,就是一个上三角矩阵!!!(batch_size, len_q, len_k)
        :return: 下一个待处理的层
        """
        ## 这个多头分为这几个步骤,首先映射分头,然后计算atten_scores,然后计算atten_value;
        ## 输入进来的数据形状: Q: [batch_size x len_q x d_model], K: [batch_size x len_k x d_model], V: [batch_size x len_k x d_model]
        residual, batch_size = Q, Q.size(0)  # 将观测值提取出来,因为Q会经过下面的变换,Batch_size 没有问题
        ## 下面这个就是先映射,后用view分头;一定要注意的是q和k分头之后维度是一致,所以一看这里都是dk
        # 以q_s距离,探讨大小变换
        q_s = (self.W_Q(Q)  # 当前Q(batch_size, len_q, d_k * n_heads)
               .view(batch_size, -1, n_heads, d_k)  # 将d_k和n_heads分开,每一个词向量都有一个n_heads行,d_k列的中间内容(batch_size, len_q, d_k, n_heads)
               .transpose(1, 2))  # 将第二和第三维互换,(batch_size, n_heads, len_q, d_k)多头注意力的每一个头都有词向量len_q行,d_k列
        # q_s: [batch_size x n_heads x len_q x d_k]
        k_s = self.W_K(K).view(batch_size, -1, n_heads, d_k).transpose(1,
                                                                       2)  # k_s: [batch_size x n_heads x len_k x d_k]
        v_s = self.W_V(V).view(batch_size, -1, n_heads, d_v).transpose(1,
                                                                       2)  # v_s: [batch_size x n_heads x len_k x d_v]
        ## [batch_size x n_heads x len_q x len_k],就是把pad信息重复了n个头上

        attn_mask = (attn_mask.unsqueeze(1)  # (batch_size, 1, len_q, len_k)代表了一个注意力机制下的的掩码矩阵,关注的是Q和K,而不是原始的词向量
                     .repeat(1, n_heads, 1, 1))  # repeat就是在某个维度上重复n次
                    # 输出为(batch_size, n_heads, lqn_q, len_k), 代表了每一个自注意力机制的掩码矩阵

        ## 得到的结果有两个:context: [batch_size x n_heads x len_q x d_v], attn: [batch_size x n_heads x len_q x len_k]
        context, attn = ScaledDotProductAttention()(q_s, k_s, v_s, attn_mask)
        context = (context.transpose(1, 2)  # (batch_size, len_q, n_heads, d_v)  相当于每一个词都有对应的d_k,这俩是一样的
                   .contiguous()  # 深拷贝
                   .view(batch_size, -1, n_heads * d_v))  # context(batch_size, len_q, n_head * d_v)降低维度,恢复成最初的V矩阵
        # context: [batch_size x len_q x n_heads * d_v]
        output = self.linear(context)  # 乘上权重矩阵,返回初始的输入大小(batch_size, len_q, d_model), 其中len_q在一开始就判断过,其值大小与loc相同
        # 残差链接:F(x) = H(x) - x,这里的F(x)是残差网络的值,x是真实值,H(x)是预测值,残差是预测值和观测值之间的差距,残差+预测值=真实值
        # 下一层的输入output + residual = (batch_size, loc, d_model)
        # attn = (batch_size, n_head, len_q, len_n)
        return self.layer_norm(output + residual), attn

## 模型参数
d_model = 512  # Embedding Size,代表词嵌入的维度,每个词都会有512维
d_k = d_v = 64  # dimension of K(=Q), V
n_heads = 8  # 多头注意力机制的个数

总结

文章讨论了自注意力机制与多头自注意力机制,并使用掩码矩阵防止未来信息泄露。

注意pytorch代码中,最好标注出每一次输入输出的张量大小变换,更好理解其中的意义

PS:作为新手分享经验,如果有讲得不好的地方或者错误麻烦在评论区指出,我会努力修改学习的Orz

相关推荐
霍格沃兹测试开发学社测试人社区几秒前
OpenAI Chatgpt 大语言模型
软件测试·人工智能·测试开发·语言模型·chatgpt
闰土_RUNTU5 分钟前
Pytorch分布式训练print()使用技巧
人工智能·pytorch·python·分布式训练·训练技巧
m0_7482347130 分钟前
【大模型】Ollama+open-webuiAnything LLM部署本地大模型构建RAG个人知识库教程(Mac)
人工智能·macos
deephub33 分钟前
ORCA:基于持续批处理的LLM推理性能优化技术详解
人工智能·深度学习·性能优化·llm
roman_日积跬步-终至千里41 分钟前
【人工智能基础06】人工神经网络(练习题):神经网络的计算、激活函数的选择与神经网络的退化
人工智能·深度学习·神经网络
一勺汤1 小时前
YOLO11改进-模块-引入多尺度差异融合模块MDFM
人工智能·深度学习·yolo·目标检测·模块·改进·yolov11
湖南罗泽南1 小时前
交叉熵损失函数(Cross-Entropy Loss)
人工智能
A Genius2 小时前
Pytorch实现MobilenetV2官方源码
人工智能·pytorch·python
道友老李2 小时前
【OpenCV】直方图
人工智能·opencv·计算机视觉
通信仿真实验室2 小时前
Google BERT入门(5)Transformer通过位置编码学习位置
人工智能·深度学习·神经网络·自然语言处理·nlp·bert·transformer