一、自注意力机制基础:从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 自注意力的计算过程

自注意力的计算分三步走:

  1. 生成Q、K、V:每个输入向量通过三个不同的线性变换,得到Query、Key、Value三个向量
  2. 计算注意力分数:用Query和所有Key做点积,得到分数矩阵
  3. 加权求和:分数经过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的整体架构,以及多头注意力是怎么工作的。到时候咱们再细聊。