零基础认识大语言模型(LLM)工作原理(4.Attention:模型是怎么看懂上下文的?)
零基础认识大语言模型(LLM)工作原理(4.Attention:模型是怎么看懂上下文的?)
在前几篇文章中,我们了解了词嵌入(Word Embedding)和Transformer的基本结构。但有一个关键问题一直悬而未决:模型是如何理解“他”指代“小明”?如何知道“银行”是指金融机构还是河岸?答案就藏在Attention 机制中。本文将用最直观的方式,配合可运行的Python代码,带你深入理解Attention的工作原理。—## 1. 为什么需要Attention?—— 一个简单的例子假设我们有一个句子:> “小明去银行存钱,他带了很多现金。”人类很容易知道“他”指“小明”,“银行”是金融机构。但传统的循环神经网络(RNN)在处理长距离依赖时,信息会逐渐衰减,难以把“小明”和“他”联系起来。Attention机制则允许模型在生成“他”时,直接“回头看”前面的“小明”,并分配更高的权重。核心思想:对于当前需要理解的词语,模型会计算它与句子中所有其他词语的“关联度”,然后根据关联度加权整合信息。—## 2. 从零实现一个简单的Attention机制我们先实现最基本的缩放点积注意力(Scaled Dot-Product Attention),这是Transformer中Attention的基础。pythonimport numpy as npdef scaled_dot_product_attention(query, key, value, mask=None): """ 缩放点积注意力计算 参数: query: 查询矩阵 (batch_size, seq_len, d_k) key: 键矩阵 (batch_size, seq_len, d_k) value: 值矩阵 (batch_size, seq_len, d_v) mask: 可选掩码,用于屏蔽某些位置 返回: output: 注意力输出 (batch_size, seq_len, d_v) attention_weights: 注意力权重矩阵 """ # 计算点积:Q * K^T # 这里的 seq_len_q 和 seq_len_k 可能不同,但通常相同 d_k = query.shape[-1] scores = np.matmul(query, key.transpose(0, 2, 1)) # 形状: (batch, seq_len_q, seq_len_k) # 缩放:除以 sqrt(d_k) 防止梯度消失 scores = scores / np.sqrt(d_k) # 如果提供了掩码,将掩码位置设为负无穷(Softmax后为0) if mask is not None: scores = scores + (mask * -1e9) # Softmax归一化得到注意力权重 # 对最后一个维度(键序列长度)做softmax exp_scores = np.exp(scores - np.max(scores, axis=-1, keepdims=True)) # 数值稳定性 attention_weights = exp_scores / np.sum(exp_scores, axis=-1, keepdims=True) # 加权求和:权重 * V output = np.matmul(attention_weights, value) return output, attention_weights# 测试:模拟一个句子有3个词,每个词用4维向量表示np.random.seed(42)batch_size = 1seq_len = 3d_k = 4d_v = 4# 随机生成Q、K、V(实际中由输入通过线性变换得到)query = np.random.randn(batch_size, seq_len, d_k)key = np.random.randn(batch_size, seq_len, d_k)value = np.random.randn(batch_size, seq_len, d_v)output, attn_weights = scaled_dot_product_attention(query, key, value)print("注意力权重矩阵(3个词对3个词的关联度):")print(np.round(attn_weights[0], 3))print("\n输出向量:")print(np.round(output[0], 3))输出示例:注意力权重矩阵(3个词对3个词的关联度):[[0.329 0.324 0.347] [0.349 0.339 0.312] [0.368 0.316 0.316]]输出向量:[[ 0.18 -0.007 -0.266 -0.273] [ 0.178 -0.012 -0.262 -0.282] [ 0.176 -0.005 -0.259 -0.28 ]]关键点:- 注意力权重矩阵的每一行代表一个“查询词”对“所有键词”的注意力分布- 权重之和为1(因为Softmax)- 输出向量是“值向量”的加权和,权重就是注意力分布—## 3. 多头注意力(Multi-Head Attention)—— 从多个角度理解上下文单一注意力只能学习一种关联模式。实际上,我们需要从不同角度理解关系:比如语法关系(主谓宾)、语义关系(同义词)、位置关系(距离)。多头注意力通过并行学习多个注意力头,捕捉不同类型的依赖关系。pythonclass MultiHeadAttention: """ 多头注意力机制实现 """ def __init__(self, d_model=8, num_heads=2): self.d_model = d_model # 模型总维度 self.num_heads = num_heads # 注意力头数 self.d_k = d_model // num_heads # 每个头的维度 # 初始化权重矩阵(实际训练中会学习) self.W_q = np.random.randn(d_model, d_model) * 0.1 self.W_k = np.random.randn(d_model, d_model) * 0.1 self.W_v = np.random.randn(d_model, d_model) * 0.1 self.W_o = np.random.randn(d_model, d_model) * 0.1 def split_heads(self, x): """将输入拆分为多个头""" batch_size, seq_len, d_model = x.shape # 重塑为 (batch, seq_len, num_heads, d_k) 然后转置为 (batch, num_heads, seq_len, d_k) x = x.reshape(batch_size, seq_len, self.num_heads, self.d_k) return x.transpose(0, 2, 1, 3) def combine_heads(self, x): """合并多个头""" batch_size, num_heads, seq_len, d_k = x.shape # 转置回 (batch, seq_len, num_heads, d_k) 然后重塑 x = x.transpose(0, 2, 1, 3) return x.reshape(batch_size, seq_len, self.d_model) def forward(self, query, key, value, mask=None): # 1. 线性变换得到 Q、K、V Q = np.dot(query, self.W_q) # (batch, seq_len, d_model) K = np.dot(key, self.W_k) V = np.dot(value, self.W_v) # 2. 拆分为多头 Q = self.split_heads(Q) # (batch, num_heads, seq_len, d_k) K = self.split_heads(K) V = self.split_heads(V) # 3. 对每个头独立计算注意力 # 这里复用之前的scaled_dot_product_attention,但需要适配多头维度 # 实际中我们会为每个头单独调用 head_outputs = [] for i in range(self.num_heads): # 提取第i个头的数据 q_head = Q[:, i, :, :] # (batch, seq_len, d_k) k_head = K[:, i, :, :] v_head = V[:, i, :, :] # 计算注意力 output, _ = scaled_dot_product_attention(q_head, k_head, v_head, mask) head_outputs.append(output) # 4. 拼接所有头的输出 concatenated = np.stack(head_outputs, axis=1) # (batch, num_heads, seq_len, d_k) concatenated = self.combine_heads(concatenated) # (batch, seq_len, d_model) # 5. 最后的线性变换 output = np.dot(concatenated, self.W_o) return output# 测试多头注意力np.random.seed(123)batch_size = 1seq_len = 4d_model = 8num_heads = 2# 模拟输入:4个词,每个词8维向量x = np.random.randn(batch_size, seq_len, d_model)# 创建多头注意力层mha = MultiHeadAttention(d_model, num_heads)# 前向传播output = mha.forward(x, x, x) # 自注意力:Q=K=Vprint("输入形状:", x.shape)print("输出形状:", output.shape)print("\n第一个词的输出向量(8维):")print(np.round(output[0, 0, :], 4))输出示例:输入形状: (1, 4, 8)输出形状: (1, 4, 8)第一个词的输出向量(8维):[-0.0035 0.0026 0.0004 -0.0009 0.0005 0.0018 -0.0029 0.0018]多头注意力的优势:- 头1可能专注于语法关系(主语-动词)- 头2可能专注于语义相似性(同义词)- 实际模型(如BERT)通常使用12或16个头—## 4. 自注意力与掩码注意力在实际LLM中,有两种常见变体:### 4.1 自注意力(Self-Attention)Q、K、V都来自同一个输入序列,用于理解句子内部关系。### 4.2 掩码注意力(Masked Attention)在生成文本时,模型不能“偷看”未来的词。因此需要掩码,让当前位置只能看到自己和之前的词。pythondef create_causal_mask(seq_len): """创建因果掩码:上三角矩阵,防止看到未来位置""" mask = np.triu(np.ones((seq_len, seq_len)), k=1) # 上三角为1 return mask # 1表示需要屏蔽的位置# 演示掩码效果seq_len = 5mask = create_causal_mask(seq_len)print("因果掩码(1表示屏蔽):")print(mask)# 应用掩码的注意力权重# 假设没有掩码时的注意力权重是均匀分布scores = np.ones((1, seq_len, seq_len)) # 假分数# 应用掩码masked_scores = scores + (mask * -1e9)# Softmax后,屏蔽位置的权重变为0exp_scores = np.exp(masked_scores - np.max(masked_scores, axis=-1, keepdims=True))attention_weights = exp_scores / np.sum(exp_scores, axis=-1, keepdims=True)print("\n掩码后的注意力权重(每行只能看到自己和左边):")print(np.round(attention_weights[0], 3))输出示例:因果掩码(1表示屏蔽):[[0. 1. 1. 1. 1.] [0. 0. 1. 1. 1.] [0. 0. 0. 1. 1.] [0. 0. 0. 0. 1.] [0. 0. 0. 0. 0.]]掩码后的注意力权重:[[1. 0. 0. 0. 0. ] [0.5 0.5 0. 0. 0. ] [0.333 0.333 0.333 0. 0. ] [0.25 0.25 0.25 0.25 0. ] [0.2 0.2 0.2 0.2 0.2 ]]解释:- 第0个词只能看到自己(权重1)- 第1个词能看到第0和第1个词(各0.5)- 第4个词能看到所有5个词(各0.2)这正是自回归语言模型(如GPT系列)生成文本时的行为。—## 5. 注意力机制的实际意义通过上述代码演示,我们可以理解Attention如何解决“看懂上下文”的问题:1.长距离依赖:无论两个词在句子中相隔多远,Attention都能直接建立联系(如“他”和“小明”)2.权重分配:模型可以学习到哪些词更重要。例如在“苹果很好吃”中,“好吃”可能给予“苹果”高权重;在“苹果公司发布新品”中,“公司”和“发布”可能给予“苹果”高权重3.并行计算:与RNN不同,Attention可以同时计算所有位置之间的关系,大大提升训练效率4.可解释性:注意力权重可以直接可视化,告诉我们模型在关注哪些词—## 总结本文从零开始实现了Attention的核心机制,并通过代码演示了:-缩放点积注意力:计算查询与键的相似度,加权求和值向量-多头注意力:并行学习多种关联模式-自注意力与掩码注意力:理解全局上下文与生成式限制Attention机制是LLM能“看懂上下文”的基石。它让模型能够动态地、有选择地关注输入序列中的不同部分,从而理解复杂的语义关系。下一篇文章,我们将讨论完整的Transformer架构如何将这些组件组合起来,实现真正的语言理解与生成。思考题:如果让你设计一个注意力层,除了点积,你还能想到哪些计算相似度的方法?