Skip to content

第二章 Transformer与注意力机制 v1.0 ​

第一部分 背景 ​

一、RNN的困境 ​

在Transformer出现之前,处理序列数据主要靠RNN和它的变体LSTM、GRU

RNN的思路很直观:像读书一样,一个词一个词地读,边读边记。听起来很合理,但有个致命问题:无法并行计算

RNN必须按顺序处理序列,t时刻的计算依赖t-1时刻的输出,这严重限制了训练效率

二、论文贡献 ​

"Attention is All You Need"论文的主要贡献:

  1. 提出了Transformer架构,完全基于注意力机制,抛弃了循环和卷积
  2. 引入了多头注意力机制,允许模型同时关注不同位置的信息
  3. 使用位置编码来表示序列中元素的位置
  4. 在机器翻译任务上取得了当时的最优结果

第二部分 注意力机制 ​

一、核心思想 ​

注意力机制的核心思想是:让模型学会"看哪里"

举个例子,翻译这句话:

"The animal didn't cross the street because it was too tired"

"it"指的是什么?是"animal"还是"street"?

人类一眼就能看出"it"指的是"animal",因为"tired"通常形容动物而不是街道

注意力机制就是让机器也学会这种"一眼看出"的能力

二、自注意力计算 ​

自注意力(Self-Attention)的计算分三步

第一步:生成Q、K、V ​

把每个词向量通过三个不同的线性变换,得到Query、Key、Value三个向量

  • Q (Query):我在找什么?
  • K (Key):我有什么?
  • V (Value):我的具体内容是什么?

第二步:计算注意力分数 ​

用Q和K做点积,得到注意力分数。分数越高,说明两个词之间的关系越强

核心公式:

Attention(Q,K,V)=softmax(QKTdk)V

其中dk是Key向量的维度,除以dk是为了防止点积太大导致softmax梯度消失

python
import torch
import torch.nn.functional as F
import math

def attention(Q, K, V):
    """
    Q: (batch, seq_len, d_k) - 查询
    K: (batch, seq_len, d_k) - 键
    V: (batch, seq_len, d_v) - 值
    """
    d_k = K.size(-1)
    
    # 1. 计算Q和K的相似度
    scores = torch.matmul(Q, K.transpose(-2, -1))
    
    # 2. 缩放(防止点积太大)
    scores = scores / math.sqrt(d_k)
    
    # 3. Softmax归一化,得到注意力权重
    weights = F.softmax(scores, dim=-1)
    
    # 4. 用权重对V加权求和
    output = torch.matmul(weights, V)
    
    return output, weights

# 示例
batch_size, seq_len, d_model = 1, 4, 8
Q = torch.randn(batch_size, seq_len, d_model)
K = torch.randn(batch_size, seq_len, d_model)
V = torch.randn(batch_size, seq_len, d_model)

output, weights = attention(Q, K, V)
print("注意力权重:")
print(weights[0])

第三步:加权求和 ​

用注意力权重对V加权求和,得到最终的输出

三、多头注意力 ​

一个注意力头只能关注一种模式。为了同时关注多种模式,Transformer使用了多头注意力

将Q、K、V分成多个头,分别计算注意力,然后拼接起来

python
import torch
import torch.nn as nn

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, num_heads):
        super().__init__()
        assert d_model % num_heads == 0
        
        self.d_model = d_model
        self.num_heads = num_heads
        self.d_k = d_model // num_heads
        
        # 线性变换层
        self.W_q = nn.Linear(d_model, d_model)
        self.W_k = nn.Linear(d_model, d_model)
        self.W_v = nn.Linear(d_model, d_model)
        self.W_o = nn.Linear(d_model, d_model)
    
    def forward(self, Q, K, V):
        batch_size = Q.size(0)
        
        # 线性变换
        Q = self.W_q(Q)
        K = self.W_k(K)
        V = self.W_v(V)
        
        # 拆分成多个头
        Q = Q.view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
        K = K.view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
        V = V.view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
        
        # 计算注意力
        scores = torch.matmul(Q, K.transpose(-2, -1)) / (self.d_k ** 0.5)
        weights = torch.softmax(scores, dim=-1)
        output = torch.matmul(weights, V)
        
        # 合并多个头
        output = output.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)
        
        # 最终线性变换
        return self.W_o(output)

第三部分 位置编码 ​

RNN天然有位置信息,因为它是一个一个处理的。但Transformer是并行处理的,不知道词的顺序

解决方案:给每个位置加上一个"位置编码"

Transformer使用的是正弦余弦函数:

PE(pos,2i)=sin⁡(pos100002i/dmodel)PE(pos,2i+1)=cos⁡(pos100002i/dmodel)

为什么用正弦余弦?因为这样可以让模型学到相对位置关系:位置4和位置5的差距,应该和位置8和位置9的差距差不多

第四部分 架构详解 ​

一、编码器 ​

每个编码器层包含:

  1. 多头自注意力层
  2. 前馈神经网络层
  3. 残差连接和层归一化

二、解码器 ​

每个解码器层包含:

  1. 带掩码的自注意力层(不能看到后面的词)
  2. 编码器-解码器注意力层
  3. 前馈神经网络层

三、前馈神经网络 ​

前馈神经网络由两个线性变换和一个ReLU激活函数组成:

FFN(x)=max(0,xW1+b1)W2+b2

第五部分 实验结果 ​

论文在机器翻译任务上进行了实验

一、WMT 2014 英德翻译 ​

  • Transformer: 28.4 BLEU
  • 之前最优: 26.4 BLEU
  • 提升: +2.0 BLEU

二、WMT 2014 英法翻译 ​

  • Transformer: 41.8 BLEU
  • 之前最优: 41.0 BLEU
  • 提升: +0.8 BLEU

三、训练效率 ​

Transformer在8个GPU上训练3.5天,而之前的模型需要几周时间

第六部分 影响 ​

Transformer的影响远远超出了NLP领域

  1. BERT:预训练语言模型,刷爆了各项NLP任务
  2. GPT:生成式模型,后来发展成了ChatGPT
  3. Vision Transformer:把Transformer用到了图像领域
  4. DALL-E:文本生成图片