注意力机制入门:Transformer的前世今生

1 阅读1分钟

在前面的章节中,我们学习了处理序列数据的循环神经网络(RNN)及其改进版本LSTM和GRU。虽然这些模型在许多序列任务中表现出色,但它们也存在一些固有的局限性,如难以并行化训练和处理长距离依赖关系。

注意力机制(Attention Mechanism)的提出彻底改变了序列建模的方式。它允许模型在处理序列时动态关注输入的不同部分,从而更好地捕捉长距离依赖关系。基于注意力机制的Transformer架构更是成为了现代自然语言处理的基石,催生了BERT、GPT等强大的预训练语言模型。

本节将深入探讨注意力机制的原理、Transformer架构以及它们如何革新了深度学习领域。

注意力机制的诞生背景

在传统的序列到序列(Seq2Seq)模型中,编码器将整个输入序列压缩为一个固定长度的上下文向量,然后解码器基于这个向量生成输出序列。这种方法在处理长序列时效果不佳,因为固定长度的向量难以包含所有必要信息。

注意力机制的提出解决了这个问题,它允许解码器在生成每个输出时动态关注输入序列的不同部分:

graph TD
    A[传统Seq2Seq] --> B[固定上下文向量<br/>难以处理长序列]
    C[注意力机制] --> D[动态关注输入<br/>处理长序列]
    
    style A fill:#e63946,stroke:#333
    style B fill:#e63946,stroke:#333
    style C fill:#2a9d8f,stroke:#333
    style D fill:#2a9d8f,stroke:#333

注意力机制原理

基本概念

注意力机制的核心思想是:在处理序列时,不是对所有输入给予同等关注,而是根据当前任务的需要动态分配注意力权重。

对于给定的查询(Query)、键(Key)和值(Value),注意力机制的计算过程如下:

  1. 计算注意力分数score(q,k)=qTk\text{score}(q, k) = q^T k

  2. 计算注意力权重α=softmax(score(Q,K))\alpha = \text{softmax}(\text{score}(Q, K))

  3. 加权求和Attention(Q,K,V)=αV\text{Attention}(Q, K, V) = \alpha V

注意力机制的可视化

graph LR
    A[查询Q] --> D[注意力分数]
    B[键K] --> D
    D --> E[Softmax]
    E --> F[注意力权重α]
    F --> G[加权求和]
    C[值V] --> G
    G --> H[输出]
    
    style A fill:#a8dadc
    style B fill:#a8dadc
    style C fill:#a8dadc
    style D fill:#457b9d
    style E fill:#457b9d
    style F fill:#f4a261
    style G fill:#e76f51
    style H fill:#e63946

自注意力机制

自注意力机制(Self-Attention)是注意力机制的一种特殊形式,其中查询、键和值都来自同一序列。它允许序列中的每个位置关注序列中的其他位置。

计算过程

对于输入序列 X={x1,x2,...,xn}X = \{x_1, x_2, ..., x_n\}

  1. 线性变换Q=XWQ,K=XWK,V=XWVQ = XW_Q, \quad K = XW_K, \quad V = XW_V

  2. 计算注意力分数Attention(Q,K,V)=softmax(QKTdk)V\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V

其中 dkd_k 是键向量的维度,用于缩放点积以防止梯度消失。

多头注意力

多头注意力机制通过并行计算多个注意力头来捕获不同子空间的信息:

MultiHead(Q,K,V)=Concat(head1,...,headh)WO\text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1, ..., \text{head}_h)W^O

其中每个头计算为: headi=Attention(QWiQ,KWiK,VWiV)\text{head}_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V)

Transformer架构

Transformer由Vaswani等人在2017年提出,完全基于注意力机制,摒弃了RNN和CNN结构。

Transformer整体结构

graph TD
    A[输入序列] --> B[编码器层1]
    B --> C[编码器层2]
    C --> D[...]
    D --> E[编码器层N]
    E --> F[解码器层1]
    A --> F
    F --> G[解码器层2]
    G --> H[...]
    H --> I[解码器层N]
    I --> J[输出序列]
    
    style A fill:#a8dadc
    style B fill:#457b9d
    style C fill:#457b9d
    style D fill:#457b9d
    style E fill:#457b9d
    style F fill:#e76f51
    style G fill:#e76f51
    style H fill:#e76f51
    style I fill:#e76f51
    style J fill:#e63946

编码器结构

每个编码器层包含两个子层:

  1. 多头自注意力机制
  2. 位置前馈网络

两个子层都使用残差连接和层归一化:

LayerNorm(x+Sublayer(x))\text{LayerNorm}(x + \text{Sublayer}(x))

解码器结构

每个解码器层包含三个子层:

  1. 掩码多头自注意力机制
  2. 多头注意力机制(编码器-解码器注意力)
  3. 位置前馈网络

位置编码

由于Transformer不包含循环或卷积结构,需要添加位置信息来表示序列顺序:

PE(pos,2i)=sin(pos100002i/dmodel)PE_{(pos,2i)} = \sin\left(\frac{pos}{10000^{2i/d_{\text{model}}}}\right) PE(pos,2i+1)=cos(pos100002i/dmodel)PE_{(pos,2i+1)} = \cos\left(\frac{pos}{10000^{2i/d_{\text{model}}}}\right)

动手实现注意力机制

让我们用Python和PyTorch实现一个简单的注意力机制:

import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import matplotlib.pyplot as plt

# 检查CUDA是否可用
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

# 缩放点积注意力
class ScaledDotProductAttention(nn.Module):
    def __init__(self, d_k):
        super(ScaledDotProductAttention, self).__init__()
        self.d_k = d_k
    
    def forward(self, Q, K, V, mask=None):
        # 计算注意力分数
        scores = torch.matmul(Q, K.transpose(-2, -1)) / np.sqrt(self.d_k)
        
        # 应用掩码(如果有)
        if mask is not None:
            scores = scores.masked_fill(mask == 0, -1e9)
        
        # 计算注意力权重
        attn = F.softmax(scores, dim=-1)
        
        # 加权求和
        context = torch.matmul(attn, V)
        
        return context, attn

# 多头注意力
class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, n_heads):
        super(MultiHeadAttention, self).__init__()
        self.d_model = d_model
        self.n_heads = n_heads
        self.d_k = d_model // n_heads
        self.d_v = d_model // n_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)
        
        self.attention = ScaledDotProductAttention(self.d_k)
    
    def forward(self, Q, K, V, mask=None):
        batch_size = Q.size(0)
        
        # 线性变换并分割为多头
        Q = self.W_Q(Q).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
        K = self.W_K(K).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
        V = self.W_V(V).view(batch_size, -1, self.n_heads, self.d_v).transpose(1, 2)
        
        # 计算注意力
        context, attn = self.attention(Q, K, V, mask)
        
        # 合并多头
        context = context.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)
        
        # 输出线性变换
        output = self.W_O(context)
        
        return output, attn

# 位置前馈网络
class PositionwiseFeedForward(nn.Module):
    def __init__(self, d_model, d_ff):
        super(PositionwiseFeedForward, self).__init__()
        self.fc1 = nn.Linear(d_model, d_ff)
        self.fc2 = nn.Linear(d_ff, d_model)
    
    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return x

# 位置编码
class PositionalEncoding(nn.Module):
    def __init__(self, d_model, max_len=5000):
        super(PositionalEncoding, self).__init__()
        
        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * 
                            (-np.log(10000.0) / d_model))
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        pe = pe.unsqueeze(0).transpose(0, 1)
        
        self.register_buffer('pe', pe)
    
    def forward(self, x):
        x = x + self.pe[:x.size(0), :]
        return x

# 简单的Transformer编码器层
class TransformerEncoderLayer(nn.Module):
    def __init__(self, d_model, n_heads, d_ff, dropout=0.1):
        super(TransformerEncoderLayer, self).__init__()
        self.self_attn = MultiHeadAttention(d_model, n_heads)
        self.ffn = PositionwiseFeedForward(d_model, d_ff)
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.dropout = nn.Dropout(dropout)
    
    def forward(self, x, mask=None):
        # 多头自注意力
        attn_out, attn_weights = self.self_attn(x, x, x, mask)
        x = self.norm1(x + self.dropout(attn_out))
        
        # 位置前馈网络
        ffn_out = self.ffn(x)
        x = self.norm2(x + self.dropout(ffn_out))
        
        return x, attn_weights

# 示例:可视化注意力权重
def visualize_attention(attn_weights, sentence, title="Attention Weights"):
    """可视化注意力权重"""
    fig, ax = plt.subplots(figsize=(10, 8))
    
    # 只显示第一个头的注意力权重
    attn = attn_weights[0, 0].cpu().detach().numpy()
    
    im = ax.imshow(attn, cmap='Blues')
    
    # 设置标签
    ax.set_xticks(range(len(sentence)))
    ax.set_yticks(range(len(sentence)))
    ax.set_xticklabels(sentence, rotation=45)
    ax.set_yticklabels(sentence)
    
    # 添加颜色条
    plt.colorbar(im)
    plt.title(title)
    plt.tight_layout()
    plt.show()

# 简单示例
if __name__ == "__main__":
    # 创建示例数据
    d_model = 512
    n_heads = 8
    d_ff = 2048
    seq_len = 10
    batch_size = 1
    
    # 随机输入
    x = torch.randn(seq_len, batch_size, d_model).to(device)
    
    # 创建Transformer编码器层
    encoder_layer = TransformerEncoderLayer(d_model, n_heads, d_ff).to(device)
    
    # 前向传播
    output, attn_weights = encoder_layer(x)
    
    print(f"Input shape: {x.shape}")
    print(f"Output shape: {output.shape}")
    print(f"Attention weights shape: {attn_weights.shape}")
    
    # 创建示例句子进行可视化
    example_sentence = ["The", "cat", "sat", "on", "the", "mat", ".", "<PAD>", "<PAD>", "<PAD>"]
    
    # 可视化注意力权重
    visualize_attention(attn_weights, example_sentence[:7], "Self-Attention Weights in Transformer")

使用Hugging Face Transformers

在实际应用中,我们可以使用Hugging Face Transformers库来快速使用预训练的Transformer模型:

# 需要先安装: pip install transformers torch
from transformers import BertTokenizer, BertModel
import torch

# 加载预训练的BERT模型和分词器
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = BertModel.from_pretrained('bert-base-uncased')

# 示例文本
text = "Hello, how are you today?"

# 分词和编码
inputs = tokenizer(text, return_tensors='pt')

# 前向传播
with torch.no_grad():
    outputs = model(**inputs)

# 获取最后一层隐藏状态
last_hidden_states = outputs.last_hidden_state

print(f"Input text: {text}")
print(f"Tokenized input shape: {inputs['input_ids'].shape}")
print(f"Last hidden states shape: {last_hidden_states.shape}")

注意力机制的应用

机器翻译

注意力机制最初在机器翻译任务中取得了显著成功,使得翻译质量大幅提升。

文本摘要

通过注意力机制,模型可以关注输入文档中的重要句子和段落来生成摘要。

问答系统

在问答系统中,注意力机制帮助模型关注问题相关的文本片段。

图像描述生成

结合CNN和注意力机制,可以生成更准确的图像描述。

Transformer的变体

BERT

BERT(Bidirectional Encoder Representations from Transformers)采用双向编码器结构,通过掩码语言模型和下一句预测任务进行预训练。

GPT

GPT(Generative Pre-trained Transformer)采用解码器-only结构,通过自回归语言模型进行预训练。

T5

T5(Text-to-Text Transfer Transformer)将所有NLP任务统一为文本到文本的转换任务。

总结

注意力机制和Transformer架构彻底改变了深度学习领域,特别是在自然语言处理方面。本节我们:

  1. 深入理解了注意力机制的原理和计算过程
  2. 学习了自注意力和多头注意力机制
  3. 掌握了Transformer的整体架构和关键组件
  4. 动手实现了注意力机制的核心组件
  5. 了解了Transformer的变体和应用

Transformer及其变体已成为现代AI系统的核心组件,掌握它们对于深入学习大语言模型至关重要。

在下一节中,我们将深入探讨自注意力机制的更多细节,这是理解Transformer的关键。

练习题

  1. 实现带掩码的多头注意力机制,用于解码器
  2. 添加位置编码到你的实现中,并可视化其效果
  3. 使用Hugging Face Transformers库尝试不同的预训练模型
  4. 研究Transformer在计算机视觉中的应用(如Vision Transformer)