Transformer原理

论文地址:Attention Is All You Need

Transformer的提出最早是针对自然语言处理领域,在这之前用的大多是时序网络,如RNN和LSTM。对于RNN,通过前一时间步的隐藏状态和历次输入的共同作用来得到输出,对于简单的文本序列比较友好,由于参数量少,其推理速度快,但是面对长文本序列时,如针对大篇幅的文章摘要,开头关键词的重要性会随着文章段落的增长而逐渐被稀释。LSTM在RNN基础上进行改进,对长文本序列有了一定的处理能力,通过门控机制,来记忆关键信息。但是面对短文本容易出现过拟合现象,并且其参数量是RNN的4倍,训练耗时长。

RNN循环层
LSTM隐藏层内部架构

对LSTM而言如果只是参数量大还好,问题在于其当前时间步的输出是由前一个时间步来作用的,因此无法实现并行化训练,这样更慢了。而在自然语言处理领域,Transformer的提出就替代了LSTM,优势主要为以下两点:

  • 理想情况(不考虑硬件设备),可处理的文本无限长。
  • 可以实现并行化训练,效率大幅提升。

Transformer的结构图如下:

其中两个非常重要的模块就是Self-Attention(自注意力)和Multi-Head Attention(多头注意力),它们可以关联内部序列,同时提供并行化操作的可行性。

Self-Attention

自注意力是让序列中的每个元素(如文本中的词、图像中的像素)关注序列中其他元素,并根据关联强度分配权重,最终生成融合全局信息的特征表示。下面是对具体原理的理解

公式中的Q、K和V并不是文本序列直接输入的,而是经过高维映射和全连接得到的,具体的操作如下图:

输入的文本序列 x x x经过高维映射成一个个的向量 a a a,接着和参数矩阵(不同的输入文本共享这些参数矩阵)相乘得到 q q q k k k v v v三个键值序列,同类键值各自组成的矩阵也就是Q、K和V,当然实际情况还要考虑偏置项 b b b。那么从这里可以看出,由于使用了矩阵乘法且参数矩阵是共享的,因此训练过程相比LSTM是可以实现并行化的,实际计算是利用线性层。

接着就要利用我们上方公式里的操作,其中 d d d代表的是 k k k的维度,softmax中的 Q K T QK^T QKT则是单个 q i q_i qi k 1 k_1 k1 k n k_n kn进行点乘操作,比如 q 1 q_1 q1所得到的 α 11 α_{11} α11 α 1 n α_{1n} α1n q 2 q_2 q2所得到的 α 21 α_{21} α21 α 2 n α_{2n} α2n,而刚刚说到Q和K是 q q q k k k键值序列组成的矩阵,因此通过将K转置,利用矩阵相乘可以直接计算得到 α α α矩阵(除以一个维度量是防止点乘得到的数值过大,导致softmax后梯度变小)。

接着公式中经过softmax后和V矩阵相乘,即 α α α矩阵通过softmax计算得到 α ˙ \dot{α} α˙矩阵后, α i 1 ˙ \dot{α_{i1}} αi1˙ α i n ˙ \dot{α_{in}} αin˙依次和 v 1 v_1 v1 v n v_n vn相乘得到的 b i b_i bi所组成的矩阵B。经过以上的运算,各个向量之间就做到了相融,这也就是“注意力”。

Multi-Head Attention

多头注意力是自注意力的扩展:将输入特征分割为多个“子空间”,每个子空间独立计算自注意力(即 “注意力头”),最后拼接所有头,以增强模型对不同类型关联的捕捉能力。

相比于Self-Attention,Multi-Head Attention在获取Q、K和V的步骤是一致的,但引入了注意力头机制,根据头的数目来对 q q q进行拆分,比如有两个head,则就要将 a 1 ∗ W q a_1*W^{q} a1Wq所得结果 q 1 q_1 q1拆成 q 11 q_{11} q11 q 12 q_{12} q12,需要注意的是,这里的拆分不是平均拆分,而是通过线性映射而得到的。同理 k k k v v v也是如此,将同一类的 q q q k k k v v v归为一个head做self-attention,最后合并。

其实简单来讲,有点类似于分组卷积,本质就是将参数矩阵给拆分成head数量,分别组成不同组的V、K和Q来各自做self-attention,最后合并后和一个可训练的参数 W o W^o Wo相乘。

但到这里针对文本的处理仍有一个问题,从该多头注意力机制的计算方式中可以看到,如果将 x 2 x_2 x2 x 3 x_3 x3进行调换,并不会影响 b 1 b_1 b1的计算,因此又引入了位置编码来矫正。

Vision Transformer

论文地址:TRANSFORMERS FOR IMAGE RECOGNITION AT SCALE

Transformer不仅在自然语言处理领域有很好的表现,其ViT版本在CV的表现也非常好,如下图是在不同数据集下和以往的模型进行对比:

ViT的模型架构如下图,将一个图片分成好几个patch,将patch展平后,也就是利用映射转换成一个个的向量(注意上面提到的位置差异,因此要添加位置编码);接着输入到编码层(下图右侧),内含注意力头机制,重复堆叠L次;最后的MLP-head用于图像分类。

  • embedding层:Transformer模块的输入是2维矩阵。因此,在代码实现中,如对一个三通道[224, 224]大小的输入图像,通过768个16*16的卷积核做步幅为16的卷积操作,在将高和宽进行合并展平,得到[196, 768]的二维数据,拼接分类的token信息用于分类,那就是concat([1, 768], [196, 768]) --> [197, 768],再加上可训练的位置编码信息,最终大小[197, 768]。
  • Transformer encoder:
    • layer norm: 针对自然语言处理所进行的标准化操作,和图像领域有所区别,其是对每个token进行标准化(因为输入的序列长度不固定,没法像batch_normalization那样固定batch大小)。
    • Multi-Head Attention,和上面讲的一样,用注意力头机制提取融合之后的token信息,相比于初始输入的token,经过多头注意力机制融合交互后,所输出的每个token都具有其它token的特征信息,这些特征信息之间的关联强度由注意力(权重)所决定。比如“小明喜欢小丽,但是她不喜欢他”,关键字’她‘在初始输入时对其它token并没有关联,但是经过注意力机制后,’她‘会重点融合’小明‘和’小丽‘的信息。
    • Dropout:丢弃部分神经元,防止过拟合
    • 类残差结构:经过Dropout后的数据与原始数据相加,保留部分特征信息。
    • MLP Block:两次全连接和Dropout,中间穿插激活函数。
  • MLP-head:一开始的embedding层我们拼接了一个分类的token,此时我们只需要将这个token拿出来,对大数据集,用全连接和tanh激活+全连接得到分类结果;小数据集则直接使用全连接+softmax得到每个类别的概率。

代码关键部分解读–ViT ( Vision Transformer )

Embedding 层

class PatchEmbed(nn.Module): 
    def __init__(self, img_size=224, patch_size=16, in_c=3, embed_dim=768, norm_layer=None):
        # 部分定义省略
        img_size = (img_size, img_size)  # 原2D图像大小
        patch_size = (patch_size, patch_size)  # 每个patch的大小
		# 卷积实现:
		# 输入3通道图片,输出深度为768,卷积核16*16,步长也为16
        self.proj = nn.Conv2d(in_c, embed_dim, kernel_size=patch_size, stride=patch_size)  # [224, 224, 3] --> [14, 14, 768]
        # 层归一化
        self.norm = norm_layer(embed_dim) if norm_layer else nn.Identity()

    def forward(self, x):
        B, C, H, W = x.shape

        # 展平: [768, 14, 14] -> [768, 196]
        # 维度变换: [768, 196] -> [196, 768]  
        x = self.proj(x).flatten(2).transpose(1, 2)
        x = self.norm(x)
        return x

多头注意力机制

class Attention(nn.Module):
    def __init__(self,
                 dim,   # 输入token的dim
                 num_heads=8,  # 注意力头的数目
                 qkv_bias=False,  # 生成qkv是否添加偏置项
                 qk_scale=None,  
                 attn_drop_ratio=0.,  # 注意力丢弃比例
                 proj_drop_ratio=0.):  # 
        # 部分定义省略
        head_dim = dim // num_heads  # 单个token通过全连接拆分的份数,也就是每个head的qkv对应的dim
        self.scale = qk_scale or head_dim ** -0.5  # attention公式中的分母,对k的维度开根号
        self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)  # 输入为dim个token,输出为初始q、k和v
        self.attn_drop = nn.Dropout(attn_drop_ratio)
        self.proj = nn.Linear(dim, dim)  # 多头注意力中,和最后负责特征融合的Wo参数矩阵相乘
        self.proj_drop = nn.Dropout(proj_drop_ratio)

    def forward(self, x):
        # [batch_size, num_patches + 1, total_embed_dim]
        B, N, C = x.shape

        # qkv(): -> [batch_size, num_patches + 1, 3 * total_embed_dim] 
        # reshape: -> [batch_size, num_patches + 1, 3, num_heads, embed_dim_per_head], 3代表的是qkv这3个,这里以q为例:num_heads 代表的是q1...qn(经过全连接拆分成和注意力头数目相同),embed_dim_per_head则是q1向量的具体数值
        # permute: -> 调整数据顺序 [3, batch_size, num_heads, num_patches + 1, embed_dim_per_head]
        qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4)  # 得到qkv,并根据注意力头的数目进行拆分
        q, k, v = qkv[0], qkv[1], qkv[2]
		
		# 单个注意力头部的attention计算
        # transpose: 转置操作;@: multiply 矩阵乘法
        attn = (q @ k.transpose(-2, -1)) * self.scale  
        attn = attn.softmax(dim=-1)
		
		# 对每一行的数据(各个head)与v的数据相乘
		# reshape:将不同head的对应位置进行拼接
        x = (attn @ v).transpose(1, 2).reshape(B, N, C)
        x = self.proj(x) # 全连接实现Wo参数矩阵相乘,使特征更好地融合
        x = self.proj_drop(x)  # dropout
        return x
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐