一、自注意力机制基础:从RNN到Transformer的演进,为什么需要自注意力?
大家好,我是老张。今天咱们聊聊自注意力机制。说实话,我刚接触这个概念时也觉得挺玄乎的。但搞懂了之后你会发现,它其实就是一种更聪明的「注意力分配」方式。
先问大家一个问题:你读一句话时,是不是会不自觉地关注某些关键词?比如「我今天没吃饭,所以很饿」,你自然会把「没吃饭」和「饿」联系起来。自注意力机制干的,就是类似的事——让模型学会自己找重点。
1.1 RNN的困境:长距离依赖问题
在Transformer出现之前,RNN(循环神经网络)是处理序列数据的主力。我2017年做机器翻译项目时,用的就是LSTM。当时觉得LSTM已经很牛了,能记住几十步之前的信息。但实际用起来,问题不少。
RNN的核心问题是:它必须按顺序处理数据。你想想看,读一句话时,你得一个字一个字地读,读完前面的才能读后面的。这就导致了一个致命缺陷——长距离依赖问题。
举个例子:
「我在北京生活了十年,但这座城市依然让我感到陌生。」
这里的「这座城市」指的是「北京」。但RNN处理到句尾时,前面的「北京」信息已经被「稀释」得差不多了。距离越远,信息越弱。
我在项目中遇到过这种情况:训练一个情感分析模型,输入是用户评论。有些评论很长,前面说产品好,后面吐槽服务差。结果模型死活学不会把前后信息关联起来。嗯,这就是RNN的硬伤。
避坑指南:我曾经用双向LSTM试图解决这个问题,效果确实有提升,但计算量翻倍了。而且对于超长序列(比如1000个词以上),双向LSTM也扛不住。说白了,RNN的串行处理方式决定了它的天花板。
1.2 注意力机制的雏形:让模型学会「看哪里」
2014年,Bahdanau等人提出了注意力机制。这个想法很朴素:既然RNN记不住所有信息,那不如让它每次处理时,回头看看输入序列的哪些位置更重要。
打个比方:你在做阅读理解时,看到一个问题,会回头去原文找答案。注意力机制就是让模型学会「回头找答案」。
最初的注意力机制是加在Encoder-Decoder架构上的。编码器把输入序列编码成一系列向量,解码器生成输出时,会计算当前输出与每个输入向量的「相关性分数」,然后加权求和。分数高的位置,就是模型「注意」的地方。
# 注意力分数的计算(简化版)
def attention_score(query, key):
# query: 当前解码器的状态
# key: 编码器的每个时间步的输出
score = dot_product(query, key) # 计算相似度
weights = softmax(score) # 归一化成概率
context = sum(weights * values) # 加权求和得到上下文向量
return context
这个机制的效果立竿见影。我记得当时在翻译任务上,BLEU值直接提升了2-3个点。但注意,这时的注意力还是「外部注意力」——模型看的是输入序列,而不是自己内部。
3.3 自注意力的诞生:自己看自己
2017年,Google的Vaswani等人发表了那篇著名的论文《Attention Is All You Need》。他们提出了一个大胆的想法:为什么不让序列内部的每个位置,都去关注序列内的其他位置?
这就是自注意力(Self-Attention)的核心思想。说白了,就是让序列自己跟自己玩——每个词都去计算它与其他所有词的相关性。
自注意力的优势:
- 并行计算:所有位置可以同时计算,不像RNN必须串行
- 全局视野:每个位置都能直接看到序列中的任何其他位置,距离不再是问题
- 动态权重:权重是根据输入内容动态计算的,不是固定的
你想想看,这有多爽?处理那句「我在北京生活了十年,但这座城市依然让我感到陌生」时,「这座城市」可以直接「看到」前面的「北京」,距离再远也不怕。
3.4 自注意力的计算过程
自注意力的计算分三步走:
- 生成Q、K、V:每个输入向量通过三个不同的线性变换,得到Query、Key、Value三个向量
- 计算注意力分数:用Query和所有Key做点积,得到分数矩阵
- 加权求和:分数经过Softmax归一化后,对Value进行加权求和
import numpy as np
def self_attention(X, W_q, W_k, W_v):
# X: 输入序列 [seq_len, d_model]
# W_q, W_k, W_v: 权重矩阵
Q = np.dot(X, W_q) # [seq_len, d_k]
K = np.dot(X, W_k) # [seq_len, d_k]
V = np.dot(X, W_v) # [seq_len, d_v]
# 计算注意力分数
scores = np.dot(Q, K.T) / np.sqrt(d_k) # 缩放点积
# Softmax归一化
weights = softmax(scores, axis=-1)
# 加权求和
output = np.dot(weights, V)
return output
注意:这里有个缩放因子 1/√d_k。为什么要缩放?因为当d_k很大时,点积的结果会很大,导致Softmax进入梯度饱和区。我刚开始写代码时忘了加这个,结果训练一直不收敛。查了两天才发现是这个问题。
3.5 为什么需要自注意力?
总结一下,自注意力解决了三个核心痛点:
| 痛点 | RNN的表现 | 自注意力的优势 |
|---|---|---|
| 长距离依赖 | 距离越远,信息越弱 | 任意位置直接连接,距离无影响 |
| 并行计算 | 必须串行,无法并行 | 所有位置同时计算 |
| 动态权重 | 固定权重(LSTM的门控也是固定的) | 根据输入内容动态调整 |
我个人习惯把自注意力理解为「让模型自己决定看哪里」。它不像RNN那样被动地接收信息,而是主动地去寻找重要信息。这种主动性,是Transformer能碾压RNN的根本原因。
一个小技巧:如果你刚开始学自注意力,建议先手动算一遍小规模的例子。比如输入3个词,每个词维度是4,自己手算Q、K、V和注意力分数。算一遍比看十遍都管用。
好了,这一章我们聊了自注意力机制的来龙去脉。下一章我会详细讲Transformer的整体架构,以及多头注意力是怎么工作的。到时候咱们再细聊。