2017年Transformer架构的横空出世,彻底改变了自然语言处理的游戏规则。作为这个革命性架构的核心组件,Self-Attention机制让模型首次具备了真正理解上下文关系的能力。在传统的RNN结构中,模型需要逐个处理序列中的token,这种串行处理方式不仅效率低下,还难以捕捉长距离依赖关系。而Self-Attention通过并行计算所有token之间的关系权重,一举解决了这两个根本性痛点。
我第一次接触Transformer时,最震撼的是其处理句子"I arrived at the bank after crossing the river"中"bank"一词的消歧能力。传统模型往往困惑于"银行"还是"河岸"的含义,而Self-Attention能同时关注"river"和"crossing"这两个关键上下文token,准确判断此处"bank"指代的是河岸。这种基于注意力权重的动态特征提取,远比固定窗口的CNN或缓慢传播信息的RNN来得高效。
Self-Attention的计算过程可以分解为几个关键步骤。假设我们有一个包含n个token的输入序列,每个token的嵌入维度为d。首先,通过三个不同的权重矩阵W_Q、W_K、W_V,将每个token的嵌入向量分别转换为Query、Key和Value三个向量:
code复制Q = X * W_Q # [n, d_k]
K = X * W_K # [n, d_k]
V = X * W_V # [n, d_v]
这里d_k和d_v分别是Key和Value的维度。在实际实现中,为了计算效率,通常会让d_k = d_v = d_model / h,其中h是注意力头的数量。
接下来计算注意力权重:
code复制attention_scores = Q @ K.T / sqrt(d_k) # [n, n]
attention_weights = softmax(attention_scores) # [n, n]
最后将权重应用于Value向量:
code复制output = attention_weights @ V # [n, d_v]
关键细节:除以sqrt(d_k)的操作至关重要。当d_k较大时,点积结果会变得很大,导致softmax函数进入梯度极小的区域。这个缩放因子保持了梯度的稳定性。
Transformer采用的多头注意力(Multi-Head Attention)可以理解为让模型同时从不同子空间学习信息。具体实现上,就是将Q、K、V分别拆分成h份,每份维度变为d_k = d_model / h:
code复制# 假设h=8, d_model=512
Q = Q.reshape(n, h, d_k) # [n, 8, 64]
K = K.reshape(n, h, d_k)
V = V.reshape(n, h, d_k)
每个头独立计算注意力后,再将结果拼接起来:
code复制output = concat([head_1, head_2, ..., head_h]) # [n, d_model]
我在实践中发现,不同头确实会学习到不同的注意力模式。例如在机器翻译任务中,有的头专门关注代词指代关系,有的头则聚焦于动词时态匹配。
Transformer的编码器由N个相同层堆叠而成(原论文N=6),每层包含两个主要子层:
每个子层都采用残差连接和层归一化:
code复制sub_layer_output = LayerNorm(x + Sublayer(x))
这种设计使得深层网络训练成为可能。FFN通常由两个线性变换和一个ReLU激活组成:
code复制FFN(x) = max(0, xW1 + b1)W2 + b2
值得注意的是,编码器的自注意力是"双向"的,即每个位置都能看到序列的所有位置,这与BERT等模型的预训练方式直接相关。
解码器在编码器结构基础上增加了第三个子层 - 编码器-解码器注意力层。这个层允许解码器关注编码器的输出。解码器的自注意力层与编码器有个关键区别:为了防止信息泄露,它使用了掩码机制,确保位置i只能关注到位置1到i的token。
在实现上,这通过在注意力分数矩阵的上三角区域填充负无穷来实现:
code复制mask = torch.triu(torch.ones(seq_len, seq_len), diagonal=1)
masked_scores = attention_scores.masked_fill(mask == 1, -1e9)
由于Self-Attention本身不具备处理序列顺序的能力,Transformer引入了位置编码(Positional Encoding)来注入位置信息。原始论文使用不同频率的正弦和余弦函数:
code复制PE(pos,2i) = sin(pos/10000^(2i/d_model))
PE(pos,2i+1) = cos(pos/10000^(2i/d_model))
这种设计有几个精妙之处:
我在实验中发现,对于较短的序列(<512),可学习的位置嵌入(learned positional embedding)通常表现相当。但对于需要处理超长序列的模型,正弦编码的泛化能力明显更优。
实际应用中,我们经常需要处理变长序列和特殊任务需求,这需要灵活使用注意力掩码。常见的掩码类型包括:
python复制padding_mask = (x != PAD_ID).unsqueeze(1) # [batch, 1, seq_len]
attention_scores = attention_scores.masked_fill(~padding_mask, -1e9)
python复制look_ahead_mask = torch.triu(torch.ones(seq_len, seq_len), diagonal=1)
python复制combined_mask = torch.max(padding_mask, look_ahead_mask)
Transformer训练过程中,梯度爆炸是个常见问题。我通常采用以下策略组合:
python复制torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
python复制lr = d_model^-0.5 * min(step_num^-0.5, step_num * warmup_steps^-1.5)
python复制optimizer = Adam(model.parameters(), lr=1e-4, betas=(0.9, 0.98), eps=1e-9)
在文本生成任务中,不同的解码策略会显著影响结果质量:
| 策略 | 温度参数 | Top-k | Top-p | 特点 |
|---|---|---|---|---|
| 贪婪搜索 | - | - | - | 简单高效但结果单一 |
| 束搜索 | - | - | - | 平衡质量与多样性 |
| 随机采样 | ✓ | ✓ | ✓ | 创造性更强但可能不连贯 |
| 核采样 | - | - | ✓ | 动态调整候选集大小 |
我的经验是,对于技术文档生成这类需要准确性的任务,束搜索(beam_size=4-8)效果最佳;而对于创意写作,温度参数设为0.7-1.0的核采样更能产生有趣的结果。
原始Transformer的O(n²)计算复杂度限制了其在长序列中的应用。近年来出现了多种改进方案:
我在处理长达4096个token的法律文档时,采用块稀疏注意力将内存占用降低了70%,而性能损失不到2%。
从BERT的MLM到GPT的自回归,再到T5的文本到文本统一框架,预训练策略不断演进。最新的趋势包括:
这些技术在实际业务系统中可以组合使用。例如在客服机器人中,我结合了稠密检索和生成模型,既保证了响应准确性,又保持了自然流畅的表达。