【学习日志】202402w4\Self-attention+transformer理论+code实现

学习内容参考:

2024/02/27

一. Attention mechanism

1. 注意力分类

  • 聚焦式注意力(focus attention):有目的的。比如在嘈杂的环境里你也能关注到和你谈话的人的说话内容。
  • 基于显著性的注意力(saliency-based attention):受外界刺激驱动。比如别人叫你的名字,会引起你的注意力。

2. 相关参数

  • 查询向量 q \boldsymbol{q} q,和特定任务相关的表示;
  • 注意力分布 α n \alpha_n αn,在给定 q \boldsymbol{q} q X \mathbf{X} X下,选定第 n n n个输入向量的概率。
  • 打分函数 s ( x n , q ) s(\boldsymbol{x}_n, \boldsymbol{q}) s(xn,q),有多种计算方式选择,如
    • 加性模型: s ( x , q ) = v ⊤ t a n h ( W x + U q ) s(\boldsymbol{x}, \boldsymbol{q})=\boldsymbol{v}^{\top}tanh(\mathbf{W}\boldsymbol{x}+\mathbf{U}\boldsymbol{q}) s(x,q)=vtanh(Wx+Uq)
    • 点积模型: s ( x , q ) = x ⊤ q s(\boldsymbol{x}, \boldsymbol{q})=\boldsymbol{x}^{\top}\boldsymbol{q} s(x,q)=xq
    • 缩放点积模型: s ( x , q ) = x ⊤ q D s(\boldsymbol{x}, \boldsymbol{q})=\frac{\boldsymbol{x}^{\top}\boldsymbol{q}}{\sqrt{D}} s(x,q)=D xq,D是输入向量的维度。(为什么有缩放因子?为了归一化,D很大是,softmax(xq)的分布会集中在元素绝对值大的区域,乘上缩放因子使得分子xq的方差变为1,使得在训练过程中梯度值保持稳定。)
    • 双线性模型: s ( x , q ) = x ⊤ W q s(\boldsymbol{x}, \boldsymbol{q})=\boldsymbol{x}^{\top}\mathbf{W}\boldsymbol{q} s(x,q)=xWq
      其中,点积模型相对加性模型计算效率更高;缩放点积解决(D较大时,点积模型方差较大,导致softmax函数的梯度较小)问题;双线性模型可理解为分别对 x \boldsymbol{x} x q \boldsymbol{q} q做线性变换后计算点积,在计算相似度时引入了非对称性。

3. 计算步骤

输入向量 X = [ x 1 , … , x N ] ∈ R D × N \mathbf{X}=[\boldsymbol{x}_1,\dots,\boldsymbol{x}_N]\in\mathbb{R}^{D\times N} X=[x1,,xN]RD×N

  • 在所有输入信息上计算注意力分布;
    在给定 q \boldsymbol{q} q X \mathbf{X} X下,第 n n n个输入向量 x n \boldsymbol{x}_n xn受关注的程度:
    α n = p ( x n ∣ X , q ) = s o f t m a x ( s ( x n , q ) ) . \alpha_n=p(\boldsymbol{x}_n|\mathbf{X}, \boldsymbol{q})=softmax(s(\boldsymbol{x}_n, \boldsymbol{q})). αn=p(xnX,q)=softmax(s(xn,q)).
  • 根据注意力分布来计算输入信息的加权平均。
    软性注意力机制: a t t ( X , q ) = ∑ n = 1 N α n x n = E x n ∼ p ( x n ∣ X , q ) [ x n ] . 软性注意力机制: att(\mathbf{X,\boldsymbol{q}})=\sum^N_{n=1}\alpha_n\boldsymbol{x}_n=\mathbb{E}_{\boldsymbol{x}_n\sim p(\boldsymbol{x}_n|\mathbf{X,\boldsymbol{q}})}[\boldsymbol{x}_n]. 软性注意力机制:att(X,q)=n=1Nαnxn=Exnp(xnX,q)[xn].

4. 注意力机制变体

  • 硬性注意力:选取最高概率的输入向量:
    a t t ( X , q ) = x n ^ , 其中 n ^ = arg max ⁡ n = 1 , … , N α n att(\mathbf{X,\boldsymbol{q}})=\boldsymbol{x}_{\hat{n}}, 其中\hat{n}=\argmax_{n=1,\dots,N} \alpha_n att(X,q)=xn^,其中n^=n=1,,Nargmaxαn
    损失函数和注意力分布之间不可导,无法使用反向传播进行训练,通常使用强化学习进行训练。

  • 键值对注意力:key-计算注意力分布 α n \alpha_n αn;Value-计算聚合信息。输入 ( K , V ) = [ ( k 1 , v 1 ) , … , ( k N , v N ) ] (\mathbf{K},\mathbf{V})=[(\boldsymbol{k}_1,\boldsymbol{v}_1), \dots, (\boldsymbol{k}_N,\boldsymbol{v}_N)] (K,V)=[(k1,v1),,(kN,vN)]:
    a t t ( [ K , V ] ) = ∑ n = 1 N α n v n = ∑ n = 1 N s o f t m a x ( k n , q ) v n . att([\mathbf{K},\mathbf{V}])=\sum^N_{n=1}\alpha_n\boldsymbol{v}_n=\sum^N_{n=1}softmax(\boldsymbol{k}_n,\boldsymbol{q})\boldsymbol{v}_n. att([K,V])=n=1Nαnvn=n=1Nsoftmax(kn,q)vn.

  • 多头注意力:利用多个查询向量 Q = [ q 1 , … , q M ] \mathbf{Q}=[\boldsymbol{q}_1, \dots, \boldsymbol{q}_M] Q=[q1,,qM]并行地从输入信息中选取多组信息,每个注意力头关注输入信息的不同部分:
    a t t ( [ K , V ] , Q ) = a t t ( [ K , V ] , q 1 ) ⊕ ⋯ ⊕ a t t ( [ K , V ] , q 1 ) att([\mathbf{K},\mathbf{V}],\mathbf{Q})=att([\mathbf{K},\mathbf{V}],\boldsymbol{q}_1)\oplus\cdots\oplus att([\mathbf{K},\mathbf{V}],\boldsymbol{q}_1) att([K,V],Q)=att([K,V],q1)att([K,V],q1)
    其中 ⊕ \oplus 表示向量拼接。

  • 结构化注意力:输入信息 本身具有层次(Hierarchical)结构,比如文本有词、句子、段落、篇章等不同粒度的层次,这种情况下可以使用层次化的注意力来进行更好的信息选择。

  • 指针网络:Seq2Seq模型,通过注意力分布“指出”相关信息的位置,输出序列索引。

二. Self-Attention model

  • 建立输入输出序列之间长距离依赖关系:增加网络层数(通过一个深层网络来获取远距离信息交互)、利用全连接网络(无法处理长度可变输入序列)。
  • 不同输入长度的连接权重大小也不同,考虑利用self-attention model “动态”生成不同连接的权重。

1. 采用查询-键-值模式(Query-Key-Value)

计算过程
在这里插入图片描述

  • 将输入 X \mathbf{X} X线性映射到三个不同的空间获得查询向量 q n ∈ R D k \boldsymbol{q}_n\in \mathbb{R}^{D_k} qnRDk、键向量 k n ∈ R D k \boldsymbol{k}_n\in \mathbb{R}^{D_k} knRDk、值向量 v n ∈ R D v \boldsymbol{v}_n\in \mathbb{R}^{D_v} vnRDv
  • 对于每个 q n \boldsymbol{q}_n qn,利用注意力机制求得输出向量 h n ∈ R D v \boldsymbol{h}_n\in \mathbb{R}^{D_v} hnRDv h n = a t t ( [ K , V ] , q n ) = ∑ j = 1 N α n , j v j = ∑ j = 1 N s o f t m a x ( s ( k j , q n ) ) v j . \boldsymbol{h}_n=att([\mathbf{K},\mathbf{V}],\boldsymbol{q}_n)=\sum^N_{j=1}\alpha_{n,j}\boldsymbol{v}_j=\sum^N_{j=1}softmax(s(\boldsymbol{k}_j,\boldsymbol{q}_n))\boldsymbol{v}_j. hn=att([K,V],qn)=j=1Nαn,jvj=j=1Nsoftmax(s(kj,qn))vj.
    α n , j = s o f t m a x ( s ( k j , q n ) ) \alpha_{n,j}=softmax(s(\boldsymbol{k}_j,\boldsymbol{q}_n)) αn,j=softmax(s(kj,qn))表示第 n n n个输出关注到第 j j j个输入的权重。
  • 注意力打分函数采用缩放点积,输出向量为: H = V s o f t m a x ( K ⊤ Q D k ) ∈ R D v × N . \mathbf{H}=\mathbf{V}softmax(\frac{\mathbf{K}^{\top}\mathbf{Q}}{\sqrt{D_k}})\in\mathbb{R}^{D_v \times N}. H=Vsoftmax(Dk KQ)RDv×N.

2. 位置编码信息

只关注查询向量 q \boldsymbol{q} q和键向量 k \boldsymbol{k} k的相关性,忽略了位置信息。通常需要添加位置编码信息来修正。

import torch
import torch.nn
from math import sqrt

class Self_Attention(nn.module)
	'''
	input: batch_size * seq_len * input_dim
	q: batch_size * input_dim * dim_k
	k: batch_size * input_dim * dim_k
	v: batch_size * input_dim * dim_v
	output: batch_size * dim_v * input_dim
	'''
	def __init__(self, input_dim, dim_k, dim_v):
		super(Self_Attention,self).__init__()
		self.q = nn.Linear(input_dim, dim_k)
		self.k = nn.Linear(input_dim, dim_k)
		self.v = nn.Linear(input_dim, dim_v)
		self._norm_fact = 1/sqrt(dim_k)

	def forward(self, x)
		Q = self.q(x)  # Q: batch_size * seq_len * dim_k
		K = self.k(x)
		V = self.v(x)

		atten = nn.Softmax(dim=-1)(torch.bmm(Q,K.permute(0,2,1))) * self._norm_fact

		output = torch.bmm(atten,V)

		return output
  • nn.Softmax(dim=-1) ——Softmax激活函数;dim=-1:最后一个维度;按照指定维度将输入张量的每个元素转换为0-1之间的值。
  • (torch.bmm(Q,K.permute(0,2,1)))——bmm(batch matrix multiplication)批量矩阵乘法操作;.permute(0,2,1):转置,将1和2维度进行交换。
  • atten = nn.Softmax(dim=-1) (…)——将上述bmm结果作为输入,通过softmax激活函数进行处理,得到atten 。
class MultiHeadAttention(nn.module):
	'''
	方式一:先将X映射到dim_v空间,再dim_v/num_heads 拆解多头
	'''
	def __init__(self, input_dim, dim_k,dim_v,num_heads):
		super(Self_Attention_Multi_Head).__init__()
		assert dim_k % num_heads ==0
		assert dim_v % num_heads ==0
		self.q = nn.Linear(input_dim,dim_k)
		self.k = nn.Linear(input_dim,dim_k)
		self.v = nn.Linear(input_dim,dim_v)
		
		self.num_heads = num_heads
		self.dim_k = dim_k
		self.dim_v = dim_v
		self._norm_fact = 1/sqrt(dim_k)

	def forward(self, x)
		Q = self.q(x).reshape(-1, x.shape[0], x.shape[1], dim_k//self.num_heads )# //整除,dim_k//self.h 代表head的维度
		K = self.k(x).reshape(-1, x.shape[0], x.shape[1], dim_k//self.num_heads )
		V = self.v(x).reshape(-1, x.shape[0], x.shape[1], dim_v//self.num_heads )
		print(x.shape)
		print(Q.size())

		atten = nn.Softmax(dim = -1)(torch.matmul(Q,K.permute(0,1,3,2)))
		output = torch.matmul(atten,V).reshape(x.shape[0], x.shape[1],-1)

		return output
	


  multi_attention=MultiHeadAttention(embed_dim, num_heads)
  attn_output, attn_output_weights = multi_attention(query, key, value)

三. Transformer model

优点:

  • 完全依赖attention,解决了长距离以依赖问题;
  • 可并行,减少计算资源损耗。

在这里插入图片描述

1. Embedding

1.1 context embedding(输入序列信息)
1.2 positional embedding(位置编码信息)

弥补了Attention机制无法捕捉sequence中token位置信息的缺点。在这里插入图片描述
transformer的特性使得输入encoder的向量之间完全平等(不存在RNN的recurrent结构),token(NLP中指文本数据的基本单位,一个单词或者字词)的实际位置于位置信息编码唯一绑定。Positional Encoding的引入使得模型能够充分利用token在sequence中的位置信息。

2. Encoder(引入multi_head机制,可并行计算)

2.1 MultiHeadAttention
  • 计算输入序列之间的相关性,帮助模型更好地学习上下文语义。
  • 引入MultiHeadAttention,每个head关注输入序列的不同位置,增强了attention关注序列内部“单词”之间的表达能力。
    左:缩放点积注意力在这里插入图片描述
2.2 FF(Feed forward network)

前馈神经网络引入非线性变换,增强模型拟合能力。
每一层经过attention后,进入FFN,包含2层linear transformation层,中间的激活函数是ReLu。
F F N ( x ) = m a x ( 0 , W 1 x + b 1 ) W 2 + b 2 . FFN(\boldsymbol{x})=max(0, \mathbf{W}_1 \boldsymbol{x}+\boldsymbol{b}_1)\mathbf{W}_2+ \boldsymbol{b}_2. FFN(x)=max(0,W1x+b1)W2+b2.

2.3 Layer Normalization

保证数据特征分布的稳定性,将数据标准化到ReLU激活函数的作用区域,可以使得激活函数更好的发挥作用
在这里插入图片描述

  • Normalization技术:
    • Batch normalization: 在输入数据上进行归一化,在batch(批次)维度上计算均值和方差。
    • Layer Normalization:在输出数据上进行归一化,feature(特征)维度上计算均值和方差。
2.4 Residual connection

残差网络,以使得网络只关注到当前差异的部分,使当模型中的层数较深时仍然能得到较好的训练效果。
在这里插入图片描述

3. Decoder(串行计算)

3.1 Masked MultiHeadAttention

mask 掩码,掩盖某些值使其在参数更新时不产生效果。padding mask+sequence mask。

  • padding mask:所有缩放点积注意力中需要。不同批次序列长度不同,补短的截长的,把没意义的地方进行掩码(加上非常大的负数,经过softmax后,该位置概率接近为0)。
  • sequence mask:只在decoder中自注意力模型中需要。防止信息泄露。做法:产生一个上三角矩阵,上三角的值全为0。把这个矩阵作用在每一个序列上,就可以达到目的。
    在这里插入图片描述
3.2 FF
3.3 LayerNorm
3.4 Residual connection

5. Output

Linear_Softmax

解码组件最后会输出一个实数向量。我们如何把浮点数变成一个单词?
  • Linear层可以把向量投射到比它大得多的对数几率(logits)向量里。比如从训练集中学习1w个不同的英文单词(输出词表),则对数几率向量为1w个单元格长度的向量,每个单元对应某一个单词的分数。
  • Softmax层将分数变成概率。概率最高的单元格被选中,对应的单词作为该时间步的输出。
    在这里插入图片描述

6. transformer code实现

import torch
import torch.nn as nn
import numpy as np
import math


class Config(object):
    def __init__(self):
        self.vocab_size = 6

        self.d_model = 20
        self.n_heads = 2

        assert self.d_model % self.n_heads == 0
        dim_k  = d_model % n_heads
        dim_v = d_model % n_heads

        self.padding_size = 30
        self.UNK = 5
        self.PAD = 4

        self.N = 6
        self.p = 0.1

config = Config()

class Embedding(nn.Module):
    def __init__(self,vocab_size):
        super(Embedding, self).__init__()
        # 一个普通的 embedding层,可通过设置padding_idx=config.PAD 来实现论文中的 padding_mask
        self.embedding = nn.Embedding(vocab_size,config.d_model,padding_idx=config.PAD)


    def forward(self,x):
        # 根据每个句子的长度,进行padding,短补长截
        for i in range(len(x)):
            if len(x[i]) < config.padding_size:
                x[i].extend([config.UNK] * (config.padding_size - len(x[i]))) # 注意 UNK是你词表中用来表示oov的token索引,这里进行了简化,直接假设为6
            else:
                x[i] = x[i][:config.padding_size]
        x = self.embedding(torch.tensor(x)) # batch_size * seq_len * d_model
        return x

class Positional_Encoding(nn.Module):

    def __init__(self,d_model):
        super(Positional_Encoding,self).__init__()
        self.d_model = d_model

    def forward(self,seq_len,embedding_dim):
        positional_encoding = np.zeros((seq_len,embedding_dim))
        for pos in range(positional_encoding.shape[0]):
            for i in range(positional_encoding.shape[1]):
                positional_encoding[pos][i] = math.sin(pos/(10000**(2*i/self.d_model))) if i % 2 == 0 else math.cos(pos/(10000**(2*i/self.d_model)))
        return torch.from_numpy(positional_encoding)

class Mutihead_Attention(nn.Module):
    def __init__(self,d_model,dim_k,dim_v,n_heads):
        super(Mutihead_Attention, self).__init__()
        self.dim_v = dim_v
        self.dim_k = dim_k
        self.n_heads = n_heads

        self.q = nn.Linear(d_model,dim_k)
        self.k = nn.Linear(d_model,dim_k)
        self.v = nn.Linear(d_model,dim_v)

        self.o = nn.Linear(dim_v,d_model)
        self.norm_fact = 1 / math.sqrt(d_model)

    def generate_mask(self,dim):
        # 此处是 sequence mask ,防止 decoder窥视后面时间步的信息。
        # padding mask 在数据输入模型之前完成。
        matirx = np.ones((dim,dim))
        mask = torch.Tensor(np.tril(matirx))

        return mask==1    #函数返回一个布尔类型的张量,表示掩码矩阵中所有值是否等于 1。
                          #这样就得到了一个布尔类型的掩码,用于在解码器中限制注意力只关注当前位置之前的 token。

    def forward(self,x,y,requires_mask=False):
        assert self.dim_k % self.n_heads == 0 and self.dim_v % self.n_heads == 0
        # size of x : [batch_size * seq_len * batch_size]
        # 对 x 进行自注意力
        Q = self.q(x).reshape(-1,x.shape[0],x.shape[1],self.dim_k // self.n_heads) # n_heads * batch_size * seq_len * dim_k
        K = self.k(x).reshape(-1,x.shape[0],x.shape[1],self.dim_k // self.n_heads) # n_heads * batch_size * seq_len * dim_k
        V = self.v(y).reshape(-1,y.shape[0],y.shape[1],self.dim_v // self.n_heads) # n_heads * batch_size * seq_len * dim_v
        # print("Attention V shape : {}".format(V.shape))
        attention_score = torch.matmul(Q,K.permute(0,1,3,2)) * self.norm_fact
        if requires_mask:
            mask = self.generate_mask(x.shape[1])
            attention_score.masked_fill(mask,value=float("-inf")) # 注意这里的小Trick,不需要将Q,K,V 分别MASK,只MASKSoftmax之前的结果就好了
        output = torch.matmul(attention_score,V).reshape(y.shape[0],y.shape[1],-1)
        # print("Attention output shape : {}".format(output.shape))

        output = self.o(output)
        return output

class Feed_Forward(nn.Module):
    def __init__(self,input_dim,hidden_dim=2048):
        super(Feed_Forward, self).__init__()
        self.L1 = nn.Linear(input_dim,hidden_dim)
        self.L2 = nn.Linear(hidden_dim,input_dim)

    def forward(self,x):
        output = nn.ReLU()(self.L1(x))
        output = self.L2(output)
        return output

class Add_Norm(nn.Module):
    def __init__(self):
        self.dropout = nn.Dropout(config.p)  # 随机失活,防止过拟合
        super(Add_Norm, self).__init__()

    def forward(self,x,sub_layer,**kwargs):
        sub_output = sub_layer(x,**kwargs)
        # print("{} output : {}".format(sub_layer,sub_output.size()))
        x = self.dropout(x + sub_output)   # 残差连接,直接学习输入与子层输出之间的差异

        layer_norm = nn.LayerNorm(x.size()[1:])
        out = layer_norm(x)   #对每个样本的特征维度进行归一化 提高训练稳定性
        return out

# 将encoder中所有模块 拼接作为Encoder
class Encoder(nn.Module):
    def __init__(self):
        super(Encoder, self).__init__()
        self.positional_encoding = Positional_Encoding(config.d_model)
        self.muti_atten = Mutihead_Attention(config.d_model,config.dim_k,config.dim_v,config.n_heads)
        self.feed_forward = Feed_Forward(config.d_model)

        self.add_norm = Add_Norm()


    def forward(self,x): # batch_size * seq_len 并且 x 的类型不是tensor,是普通list

        x += self.positional_encoding(x.shape[1],config.d_model)
        # print("After positional_encoding: {}".format(x.size()))
        output = self.add_norm(x,self.muti_atten,y=x)
        output = self.add_norm(output,self.feed_forward)

        return output

# 将decoder中所有模块 拼接作为Decoder
class Decoder(nn.Module):
    def __init__(self):
        super(Decoder, self).__init__()
        self.positional_encoding = Positional_Encoding(config.d_model)
        self.muti_atten = Mutihead_Attention(config.d_model,config.dim_k,config.dim_v,config.n_heads)
        self.feed_forward = Feed_Forward(config.d_model)
        self.add_norm = Add_Norm()

    def forward(self,x,encoder_output): # batch_size * seq_len 并且 x 的类型不是tensor,是普通list
        # print(x.size())
        x += self.positional_encoding(x.shape[1],config.d_model)
        # print(x.size())
        # 第一个 sub_layer
        output = self.add_norm(x,self.muti_atten,y=x,requires_mask=True)
        # 第二个 sub_layer
        output = self.add_norm(output,self.muti_atten,y=encoder_output,requires_mask=True)
        # 第三个 sub_layer
        output = self.add_norm(output,self.feed_forward)


        return output


# 组装transformer
class Transformer_layer(nn.Module):
    def __init__(self):
        super(Transformer_layer, self).__init__()
        self.encoder = Encoder()
        self.decoder = Decoder()

    def forward(self,x):
        x_input,x_output = x
        encoder_output = self.encoder(x_input)
        decoder_output = self.decoder(x_output,encoder_output)
        return (encoder_output,decoder_output)


class Transformer(nn.Module):
    def __init__(self,N,vocab_size,output_dim):
        super(Transformer, self).__init__()
        self.embedding_input = Embedding(vocab_size=vocab_size)
        self.embedding_output = Embedding(vocab_size=vocab_size)

        self.output_dim = output_dim
        self.linear = nn.Linear(config.d_model,output_dim)
        self.softmax = nn.Softmax(dim=-1)
        self.model = nn.Sequential(*[Transformer_layer() for _ in range(N)])


    def forward(self,x):
        x_input , x_output = x
        x_input = self.embedding_input(x_input)
        x_output = self.embedding_output(x_output)

        _ , output = self.model((x_input,x_output))

        output = self.linear(output)
        output = self.softmax(output)

        return output

总结问题

  • 建立输入输出序列之间长距离依赖关系的方式?
    增加网络层数(通过一个深层网络来获取远距离信息交互)、利用全连接网络(无法处理长度可变输入序列)。

  • Self-Attention model优点?
    不同输入长度的连接权重大小也不同,考虑利用self-attention model “动态”生成不同连接的权重。

  • 多头注意力的作用?
    让模型关注不同方面的信息,捕捉更丰富的特征、信息。

  • 相比RNN、LSTM的优势
    可并行,解决长距离依赖问题(RNN t时刻输出依赖于t-1时刻,有序列依赖关系);

  • 与seq2seq相比
    seq2seq:将encoder端所有信息压缩到一个固定长度向量(上下文向量c),作为decoder端第一个隐藏状态的输入,来预测decoder端第一个token的隐藏状态。在输入序列较长时,会损失encoder端的信息。且不利于特征信息的重点关注。
    transformer:对上述缺点进行改进,利用多头注意力机制关注到多方面信息,FFN也增强模型表达能力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值