零基础认识大语言模型(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, maskNone): 缩放点积注意力计算 参数: 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, keepdimsTrue)) # 数值稳定性 attention_weights exp_scores / np.sum(exp_scores, axis-1, keepdimsTrue) # 加权求和权重 * 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_model8, num_heads2): 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, maskNone): # 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, axis1) # (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) # 自注意力QKVprint(输入形状:, 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-AttentionQ、K、V都来自同一个输入序列用于理解句子内部关系。### 4.2 掩码注意力Masked Attention在生成文本时模型不能“偷看”未来的词。因此需要掩码让当前位置只能看到自己和之前的词。pythondef create_causal_mask(seq_len): 创建因果掩码上三角矩阵防止看到未来位置 mask np.triu(np.ones((seq_len, seq_len)), k1) # 上三角为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, keepdimsTrue))attention_weights exp_scores / np.sum(exp_scores, axis-1, keepdimsTrue)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架构如何将这些组件组合起来实现真正的语言理解与生成。思考题如果让你设计一个注意力层除了点积你还能想到哪些计算相似度的方法