Hello-Agents
记录回顾一下Hello-Agents的笔记。
初始智能体
什么是智能体
- 什么是智能体?
- 人工智能领域被定义为可以感知所处环境,并自主通过执行器采取行动以达到特定目标的实体。
- 传统视角下的智能体
- 反射智能体:决策核心为明确的“条件-动作”规则构成。完全依赖当前感知输入,不具备记忆或者预测能力。
- 基于模型的反射智能体:引入“状态”,构建一个世界模型,试图使智能体理解世界。
- 基于目标的智能体:不再是被动对环境作出回应,而是主动、有遇见向某个方向行动。
- 基于效用的智能体:回答哪种行为能够获得最大的效用。
- 学习型智能体:不再依赖预设,而是通过与环境交互自主学习。强化学习是其代表性路径。
- 大语言模型驱动的新范式
- 核心不再是编写代码,而是转向引导一个通用的“大脑”去规划、行动和学习。
- 智能体分类:
- 基于内部决策架构的分类:之前的传统智能体,基于模型、基于目标、基于效用、基于学习
- 基于时间与反应性的分类:追求速度的反应性与追求最优解的规划性之间的平衡。
- 反应式智能体:速度快、计算开销低,“短视”
- 规划式智能体:决策的战略性和远见,高昂的时间和计算成本
- 混合式智能体:LLM智能体便是一种体现,既包含思考也包含决策反馈。
- 基于知识表示的分类:
- 符号主义:智能体源于对符号的逻辑操作。追求可解释性,但存在“知识获取瓶颈”
- 亚符号主义:知识不是显示的规则,而是隐藏在神经网络中,是从海量数据之中学习来的,伴随不透明性。
- 神经符号主义:想将上述两者进行融合,混合智能体。大模型驱动的智能体也是?思考过程为黑盒网络,采取的决策却是清晰可追溯。
智能体的构成和运行原理
- 任务环境
- 运行机制:智能体循环
- 感知:观察
- 思考:思考过程中会包含规划和工具选择
- 行动
智能体协作模式
- 开发者工具:Copilot、Cursor
- 自主协作者:一些智能体开发框架
工作流和智能体差异
- workflow是让AI按部就班执行指令。
- agent则是赋予AI自由度去自主达成目标。
智能体发展史
基于符号与逻辑的早期智能体
- 信念:人类的智能,尤其是逻辑推理能力,可以被形式化的符号体系所捕获和复现。
- 智能体的行为核心是基于一套明确的规则对符号进行操作,其智慧来源于设计者预先编码的知识库和推理规则,而非通过自主学习获得。
- 物理符号系统假说:理论依据,大胆宣称智能的本质,就是符号的计算与处理。
- 专家系统:最重要、最成功的应用成果,将专家的知识和经验编码成计算机程序。
- 知识库与推理机
- 知识库包含一系列IF-THEN
- 推理机根据用户事实,在知识库中寻找并应用相关规则
- 符号主义根本挑战
- 常识知识与知识获取瓶颈
- 框架问题和系统脆弱性
基于规则的聊天机器人
- 构建基于规则的聊天机器人
- ELIZA的设计思想
- 从不正面回答问题,而是识别关键词,应用转换规则,将用户陈述转为开放问题。
- 证明通过句式转换,机器在不了解对话内容情况下,营造出虚假的智能和共情。
- 核心就是基于模式匹配与文本替换
- 直接印证先前挑战:
- 缺乏语义理解
- 无上下文记忆
- 规则扩展性问题
心智社会
- 对单一整体智能模型的反思
- “理解”是什么?单一能力还是多种心智过程协同工作的结果。
- “常识”是什么?是规则逻辑知识库还是简单规则交织形成的网络。
- 智能体应该如何构建?追求完整、统一的逻辑系统,还是承认智能本身就不是完美的?
- 从而将智能体看作扁平化的、充满互动与协作的“社会”
- 作为协作体的智能
- 简单的智能体组织起来、形成功能更加强大的机构。
- 智能体之间通过去中心化的激活和抑制信号相互影响,形成动态的控制流。
- 涌现?复杂的、有目的性的智能行为,并非由某个高级智能体预先规划,而是大量简单底层智能体的局部交互中自发产生。
- 对多智能体系统的理论启发
学习范式的演进与现代智能体
如果智能无法被完全设计,是否可以被学习出来?
- 从符号到联结
- 知识的分布式表示:知识是以连接权重形式存储在神经元之间。
- 简单的处理单元:每个神经元只执行简单计算,通过一个激活函数进行处理,将结果输出给下一个神经元。
- 通过学习调整权重:智能来源于学习过程,通过某种学习算法,自动、迭代调整神经元之间的连接权重。
- 联结主义赋予智能体强大的感知和模式识别能力,使其能够从原始数据中理解世界。如何使得智能体学会在与环境的动态交互中做出最优的序贯决策?强化学习
- 基于强化学习的智能体
- 联结主义主要解决了感知问题,例如图片中有什么?
- 智能体更核心的任务是进行决策,例如该情况下,我应该做什么?
- 强化学习正是专注于解决序贯决策问题的学习范式,并非从标注好的静态数据集中学习,而是通过智能体与环境直接交互,在试错中学习如何最大化长期收益。AlphaGo
- 强化学习框架核心要素:
- 智能体
- 环境
- 状态
- 行动
- 奖励
- 智能体的学习目标,并非最大化某一时间步的即时奖励,而是最大化当前时刻开始到未来的累计奖励,也称为回报。
- 基于大规模数据的预训练
- 强化学习赋予了智能体从交互中学习决策的能力,但通常需要海量的、特定任务的交互数据,导致智能体学习之初缺乏先验知识。
- 如何使得智能体在开始学习具体任务前,具备对于世界的广泛理解?基于大规模数据的预训练。
- 预训练与微调范式的提出
- 大语言模型的诞生与涌现能力
- 基于大语言模型的智能体
- 感知-观察模块:智能体决策的起点,处理后被传递给思考阶段。
- 思考:智能体的认知核心,进行规划分解、推理和决策
- 行动:执行模块解析思考阶段生成的工具调用指令,调用工具来与环境交互。
- 观察与循环:工具结果会产生全新环境状态,从而进行新一轮观察感知循环。
大语言模型基础
语言模型与 Transformer 架构
- 从 N-gram 到 RNN
- 统计语言模型与 N-gram 的思想
- 深度学习兴起之前,统计方法是语言模型的主流。
- 核心思想:一个句子出现的概率,等于该句子中每个词出现的条件概率的连乘,也称为链式法则。
- 完整计算链式法则几乎不可能,由此提出了马尔可夫假设,近似认为,一个词的出现概率只与前有限的n-1个词有关,称为N-gram模型。
- 其中的各个概率可以通过语料库中的统计量进行最大似然估计来计算。
- 数据稀疏性:语料库中从未出现则概率估计为0
- 泛化能力差:模型无法理解词与词之间的语义相似性,例如无法理解agent与robot在语义上的相似性。
- 神经网络语言模型与词嵌入
- N-gram 模型根本缺陷在于将词视为孤立、离散的符号。使用连续向量表示词?前馈神经网络语言模型
- 核心思想:构建一个语义空间,将词汇表中的每个词映射为空间中的一个点,称为词嵌入,语义相近向量在空间中相近。学习从上下文到下一个词的映射,利用神经网络强大的拟合能力,学习一个函数,输入为前n-1个词的词向量,输出是词汇表中每个词在当前上下文后出现的概率分布。
- 词嵌入为模型训练过程中自动学习得到,为了完成“预测下一个词”这个任务,会不断调整每个词的向量位置,最终使得向量能够蕴含丰富的语义信息。
- 我们可以使用例如余弦相似度来计算两个向量的相似性。
- 神经网络模型通过词嵌入,解决了泛化能力差的问题,但依旧受限于固定的上下文窗口。
- 循环神经网络RNN和长短时记忆网络LSTM
- 为了打破固定窗口的限制,RNN应运而生。其核心是为网络增加“记忆”能力。
- 引入一个隐藏状态向量,用以表示网络的短期记忆,类似于一个小本本,不断记录并传递。
- 处理序列每一步的时候,会结合当前输入和小本本中内容,生成一个新的记忆并传给下一步。
- 问题:长期依赖问题,训练过程中,模型需要通过反向传播算法根据输入端误差来调整网络深处权重,对于RNN而言,序列长度就是网络深度。序列很长时,反向传播经过多次连乘,会导致梯度消失或者梯度爆炸问题。梯度消失会使得模型无法有效学习到序列早期信息对后期输出的影响,难以捕捉长距离的依赖关系。网络能不能“记住开头”,取决于误差信号能不能成功传回开头。
- 为了解决长期依赖问题,长短时记忆网络LSTM被设计,核心创新为引入细胞状态Cell State和一套精密的门控机制。细胞状态可以看作独立于隐藏状态的信息通路,允许信息在时间步之间更加顺畅传递。门控机制由几个小型神经网络组成,学习如何有选择地让信息通过,从而控制细胞状态的更新,包括遗忘门,输入门,输出门。
- 统计语言模型与 N-gram 的思想
- Transformer 架构解析
- RNN和LSTM引入循环一定程度上解决了捕捉长距离以来的问题。但是,必须按时间步顺序处理数据,无法进行大规模的并行计算。
- Transformer 完全摒弃了循环结构,转为依赖注意力机制来捕获序列内的依赖关系,实现并行计算。
- Encoder-Decoder整体结构
- 最初视为机器翻译任务设计,遵循经典的编码器-解码器架构。
- 编码器Encoder:理解整个输入句子,读取所有输入词元并为每个token生成富含上下文信息的向量表示。
- 解码器Decoder:任务是“生成”目标句子,参考自己已经生成的前文,并“咨询”编码器的理解结果,来生成下一个词。
- 从自注意力到多头注意力
- 骨架中最关键模块:注意力机制。
- 人脑阅读句子“The agent learns because it is intelligent.”读取到it时会将更多注意力放到agent上,自注意力机制就是对此的数学建模。
- 允许模型处理每一个词的时候,兼顾句子中所有的其他词,并为这些词分配不同的“注意力权重”。
- 权重越高,代表与当前词关联性越强,其信息在当前词的表示中也要占据更大的比重。
- 为此,自注意力机制为每个输入的词元向量引入三个可学习角色:
- 查询Q:代表当前词元,主动查询其他词元以获取信息。
- 键K:代表句子中可被查询词元的“标签”或“索引”
- 值V:代表词元本身携带的“内容”或信息。
- 上述三个向量都是由原始的词嵌入向量乘以三个不同的、可学习的权重矩阵得到。Q、K、V发挥作用的过程可以想象成下面的一场开卷考试:
- 准备考题和资料,对于句子中的每个词,通过权重矩阵生成Q、K、V向量。
- 计算相关性得分:要计算词A的在整个句子中的新表示,我们需要其他词对A的影响。于是,我们使用A的Q向量,去和句子中所有词(包括A本身)的K向量进行点积运算。该得分反映其他词对于理解A的重要性。
- 稳定化与归一化:将得到的所有分数除以一个缩放因子,防止梯度过小,高维向量点积的数值会随维度膨胀 → 让 Softmax 输出“非0即1”的极端值 → Softmax 在极端区的导数 ≈ 0 → 梯度传不回来。然后用 softmax 函数将分数转换为总和为1的权重,也就是归一化的过程。
- 加权求和:将上一步得到的权重分别乘以每个词对应的V向量,然后将所有结果相加。最终得到的向量,就是词A融合了全局上下文信息后的新表示。
- 只进行一次注意力计算(单头),模型可能只学会关注一种类型的关联。但语言中关系是复杂的,我们希望模型能够同时关注多种关系(如指代关系、时态关系、从属关系等)。多头注意力机制应运而生,简单的核心思想:将一次做完的分成几组,分开做,再合并。
- 将原始Q/K/V向量在维度上切分为h份,每一份单独进行一次单头注意力计算。好比让h个不同专家从不同的角度审视句子,每个专家都能捕获一种不同特征关系。最后,将h个专家意见拼接起来,再通过一个线性变化进行整合,由此得到最终输出。
- 由此能够让模型共同关注来自不同位置、不同表示子空间的信息,极大增强了模型表达能力。
- 前馈神经网络
- 每个Encoder和Decoder层中,多头注意力子层后都跟着一个逐位置前馈网络(Position-wise Feed-Forward Network,FFN)
- 注意力层作用为从整个序列中“动态地聚合”相关信息,前馈网络地作用则是从这些聚合后的信息中提取更高阶的特征。
- 关键:逐位置。这个前馈网络会独立地作用于序列的每一个词元向量。
- 实际被调用
seq_len次,每次处理一个词元。 - 所有位置共享同一组网络权重,既保持了对每个位置进行独立加工的能力,又大大减少了模型的参数量。
- 由两个线性变换和一个ReLU激活函数构成,通常第一个线性层输出维度
d_ff会远大于输入维度d_model,经过ReLU激活函数后再通过第二个线性层映射回d_model维度。这种“先扩大再缩小”的模式,被认为有助于模型学习更为丰富的特征表示。
- 残差连接与层归一化
- 所有子模块都被一个 Add & Norm 操作包裹,该组合是为了保证能够稳定训练。
- 两个部分组成:
- 残差连接(Add):将子模块输入x直接加到该子模块输出 Sublayer(x) 上。该结构解决了深度神经网络中的梯度消失问题。在反向传播时,梯度可以绕过子模块直接反向传播,从而保证即使网络层数很深,模型也能得到有效训练。它是一条“恒等高速公路”:不学特征、只传误差,确保深层网络底层始终能收到教学信号。
- 层归一化(Norm):对单个样本所有特征进行归一化,使其均值为0,方差为1。解决了模型训练过程中的内部协变量偏移问题,使得每一层的输入分布保持稳定,从而加速模型收敛并提高训练的稳定性。
- 位置编码
- 自注意力机制通过计算序列中任意两个词元之间的关系来捕捉依赖,但其本身不包含任何关于词元顺序或位置的信息,由此
agent learns和learns agent完全等价,由此引入了位置编码(Positional Encoding) - 核心思想:为输入序列每一个词元嵌入向量,都额外加上一个能够代表其绝对位置和相对位置信息的“位置向量”。这个位置向量不是通过学习得到,而是固定数学公式直接计算所得。
- 由此,即使两个词元嵌入完全相同,由于在句子中的位置不同,最终输入带Transformer模型中的向量也会因为加上了不同的位置编码而独一无二。
- 自注意力机制通过计算序列中任意两个词元之间的关系来捕捉依赖,但其本身不包含任何关于词元顺序或位置的信息,由此
- Decoder-Only架构
- Transformer模型在许多端到端场景表现出色,但任务是对话、创作时,我们并不需要如此复杂结构。
- Transformer设计哲学为“先理解,再生成”。编码器负责深入理解输入的整个句子,形成一个包含全局信息的上下文记忆,而后基于解码器进行翻译。
- GPT提出了一个更加简单的思想,语言的核心任务,就是预测下一个最有可能出现的词。
- 基于此思想,GPT进行了大胆简化,完全抛弃了编码器,只保留了解码器部分,也就是Decoder-Only架构。
- 这种架构的工作模式称为自回归,描述了一个简单过程:
- 给模型一个起始文本
- 模型预测下一个最可能的词
- 模型将自己新预测生成出的词添加到输入文本末尾,形成新的输入。
- 模型基于新输入,再次预测下一个词
- 不断重复上述过程,直到生成完整的句子或者达到终止条件。
- 由此,模型就像在玩一个“文字接龙”游戏,不断回顾自己已经写下的内容,然后思考下一个字该写什么。
- 解码器的掩码自注意力机制
- 保证预测第t个词时,不去偷看第t+1个词的答案。
- 保证模型在预测下一个词的时候,能且仅能依赖已经见过、位于当前位置之前的所有信息。
- Decoder-Only架构的优势
- 训练目标统一:模型的唯一任务就是“预测下一个词”,这个简单的目标非常适合在海量的无标注文本数据上进行预训练。
- 结构简单,易于扩展:更少的组件意味着更容易进行规模化扩展。
- 天然适合生成任务
- 这一简单范式,开启了大语言模型时代
关于Transformer架构更多内容可参考:https://zhuanlan.zhihu.com/p/338817680
RNN
import torch
import torch.nn as nn
class SimpleRNNCell(nn.Module):
def __init__(self, input_size, hidden_size):
super().__init__()
# 对应公式里的 W 和 U,nn.Linear 自带权重矩阵和偏置 b
self.W = nn.Linear(input_size, hidden_size)
self.U = nn.Linear(hidden_size, hidden_size)
def forward(self, x, h_prev):
# x: (batch, input_size) 当前输入
# h_prev: (batch, hidden_size) 上一步的“小本本”
h_new = torch.tanh(self.W(x) + self.U(h_prev)) # 核心:新记忆 = 当前输入 + 旧记忆
return h_new
🔍 普通NN vs RNN:处理同一个句子,到底哪里不一样?
我用一个具体句子 "我爱吃火锅",从输入形状 → 处理流程 → 输出结果,全程对比给你看。
📦 第一步:输入数据长什么样?
假设每个词已经转成向量(embedding),维度是 4:
我 → [0.1, 0.2, 0.3, 0.4]
爱 → [0.5, 0.6, 0.7, 0.8]
吃 → [0.2, 0.4, 0.6, 0.8]
火锅 → [0.9, 0.8, 0.7, 0.6]
🔸 普通神经网络(MLP)的输入
# 把4个词"拍扁"成一个大向量
x_mlp = [0.1,0.2,0.3,0.4, 0.5,0.6,0.7,0.8, 0.2,0.4,0.6,0.8, 0.9,0.8,0.7,0.6]
# 形状: (16,) 或 (1, 16)
👉 特点:
- 所有词"同时"喂进去
- 网络不知道"我"在"爱"前面,只知道这16个数在一起
- 如果句子变长(比如10个词),输入维度变成40,网络结构要重设计 ❌
🔸 RNN 的输入
# 保持"时间顺序",一个时间步喂一个词
x_rnn = [
[0.1, 0.2, 0.3, 0.4], # t=1: "我"
[0.5, 0.6, 0.7, 0.8], # t=2: "爱"
[0.2, 0.4, 0.6, 0.8], # t=3: "吃"
[0.9, 0.8, 0.7, 0.6], # t=4: "火锅"
]
# 形状: (4, 4) → (seq_len=4, input_size=4)
👉 特点:
- 词按顺序一个个来
- 网络结构固定,不管句子多长,
input_size永远是4 ✅ - 每一步都知道"现在是第几个词"
⚙️ 第二步:处理过程怎么跑?(核心对比)
🔸 普通NN:一次性"吞",垂直流动
输入层(16维) → 隐藏层(8维) → 输出层(2维)
↓ ↓ ↓
[16个数一起算] → [8个神经元一起激活] → [输出结果]
代码示意:
x = torch.randn(1, 16) # 拍扁的句子
h = torch.relu(x @ W1 + b1) # 一层全连接
out = h @ W2 + b2 # 输出
🔍 问题在哪?
- "我"和"火锅"在输入向量里相隔12个位置,网络很难学到它们的关联
- 如果换顺序成"火锅吃爱我",输入向量只是数字重排,网络输出几乎不变 → 词序信息丢失 ❌
🔸 RNN:一步步"读",水平+垂直流动
t=1: "我" → h1 = tanh(W·x1 + U·h0) → 记住"主语是我"
t=2: "爱" → h2 = tanh(W·x2 + U·h1) → 记住"我在表达喜欢"
t=3: "吃" → h3 = tanh(W·x3 + U·h2) → 记住"喜欢的动作是吃"
t=4: "火锅" → h4 = tanh(W·x4 + U·h3) → 记住"整句: 我爱吃火锅"
代码示意:
h = torch.zeros(1, hidden_size) # 初始记忆
for x_t in x_rnn: # 逐个时间步
h = torch.tanh(W @ x_t + U @ h + b) # 新记忆 = 当前词 + 旧记忆
out = h # 最后一步的h代表整句理解
🔍 优势在哪?
- 处理"火锅"时,
h3里已经包含了"我爱吃"的信息 → 上下文融合 ✅ - 换顺序成"火锅吃爱我",每一步的
h都会变,输出自然不同 → 词序敏感 ✅
🎯 第三步:输出结果有什么不同?
假设任务:判断句子情感(正面/负面)
| 句子 | 普通NN可能输出 | RNN可能输出 | 原因 |
|---|---|---|---|
| "我爱吃火锅" | ✅ 正面 | ✅ 正面 | 都学到了"爱""火锅"是正向词 |
| "火锅不爱吃我" | ❌ 可能还是正面(词都在) | ✅ 负面(顺序变了意思变) | RNN能捕捉"主谓宾"结构 |
| "我昨天今天明天都爱吃火锅" | ❌ 输入太长要截断/重训 | ✅ 正常处理(循环复用参数) | RNN天然支持变长序列 |
🖼️ 一图看懂流程差异
🔸 普通NN(像"拍照"):
[我][爱][吃][火锅] → 拍成一张大图 → 一次性识别
↓
丢失顺序,只认"图里有什么"
🔸 RNN(像"读字幕"):
[我]→读+记 → [爱]→读+记 → [吃]→读+记 → [火锅]→读+记→输出
↓
边读边理解,知道"谁对谁做了什么"
💡 关键总结(3句话记住区别)
- 输入形状:普通NN要"拍扁"成固定向量;RNN保持
(时间步, 特征)的二维序列 - 计算顺序:普通NN所有输入同时算;RNN按时间步串行算,每步带着上一步的记忆
- 顺序敏感性:普通NN对词序不敏感(除非手动加位置编码);RNN天然感知顺序
✨ 一句话比喻:
普通NN = 看一眼整句话的快照 📸
RNN = 逐字朗读并理解 📖
🧪 想亲手试试?(3行代码对比)
import torch, torch.nn as nn
# 普通NN:必须指定输入长度
mlp = nn.Linear(4*4, 2) # 4个词×4维=16维输入
out1 = mlp(torch.randn(1, 16)) # 只能处理4词句子
# RNN:输入长度随便变
rnn = nn.RNN(4, 8, batch_first=True) # 输入永远是4维/词
out2, _ = rnn(torch.randn(1, 10, 4)) # 10个词?没问题!
out3, _ = rnn(torch.randn(1, 100, 4)) # 100个词?照样跑!
📉 RNN 为什么“梯度不更新” = “记不住开头”?(因果链)
我们用一条清晰的逻辑链拆开:
- 梯度 ≈ 0 → 反向传播到第1步时,误差信号已经微弱到看不见。
- 参数不更新 → 共享权重 W, U 没有因为“第1个词”而做任何调整。
- 没学会关联 → 网络根本不知道 x₁(开头词)和最终答案有什么关系。
- 功能退化 → 为了降低整体误差,网络会自发学会“只依赖最后几个词”,因为那些词的梯度大、好调整。
- 实际表现 → 推理时,开头词输入进去,对最终输出几乎没影响 → “记不住开头” ✅
生活比喻:改长作文的老师
- 假设你让学生写一篇 1000 字的议论文,你只批改最后一段:
- 学生不知道开头论点写得好不好、逻辑通不通。
- 几次之后,学生发现:“反正开头写啥都不影响分数,我随便糊弄,把精力全放在最后一段就行。”
- 结果:作文开头越来越敷衍,看起来像“失忆”了。
直接说结论:RNN 只有一个状态 h,它既要当记忆又要当输出;LSTM 把它拆成了两个独立状态:c(细胞状态)专管长期记忆,h(隐藏状态)专管短期输出。 它们不是替代关系,而是分工协作。
🔍 RNN和LSTM核心区别(3个维度)
| 维度 | RNN 的隐藏状态 h |
LSTM 的细胞状态 c |
LSTM 的隐藏状态 h |
|---|---|---|---|
| 职责 | 一身兼两职:记忆 + 当前步输出 | 只负责长期记忆,像仓库底层货架 | 只负责当前步表示,像前台展示柜 |
| 更新方式 | tanh 全量覆盖:h = tanh(Wx + Uh_prev) |
线性加法 + 门控筛选:c = f·c_prev + i·c̃ |
从 c 过滤而来:h = o·tanh(c) |
| 信息损耗 | 每步都被 tanh 强行压缩,早期信息易丢失 |
加法路径让信息原样保留,损耗极小 | 按需暴露,不破坏 c 里的原始记忆 |
🧠 直观比喻:整理档案室
- RNN 的
h像一块白板:每次来新文件,你得擦掉旧内容重新写。写多了,第一页的内容早就模糊不清。 - LSTM 的
c像一条带抽屉的传送带:文件放上去,除非你主动拉开“遗忘门”扔出去,否则它会平稳传到末尾。中间只往抽屉里加新文件(输入门),不破坏旧文件。 - LSTM 的
h像传送带旁边的“当前摘要台”:从传送带上挑出此刻需要的部分,打包成报告传给下一步。下一步的门控计算,看的也是这个h,不是c。
📐 公式/代码对照(一眼看懂差异)
# 🔸 RNN:只有 h,直接覆盖
h = torch.tanh(W @ x + U @ h_prev)
# 🔸 LSTM:c 和 h 分离
c = f * c_prev + i * c_tilde # 细胞状态:加法为主,信息平稳传递
h = o * torch.tanh(c) # 隐藏状态:从细胞状态“提炼”出的当前表示
🔑 关键设计:
c的更新路径没有非线性激活函数(只有+和*),梯度可以像坐高铁一样直接传回开头 → 解决梯度消失h依然保留tanh,保证输出值稳定在(-1,1),方便下一步门控计算
⚠️ 常见误区澄清
| 误区 | 真相 |
|---|---|
“LSTM 用 c 替换了 h” |
❌ LSTM 同时有 c 和 h。c 是暗线(长期记忆库),h 是明线(当前步特征) |
“下一步门控用的是 c” |
❌ 遗忘门/输入门/输出门的计算全部依赖 h(h = o·tanh(c) 才是暴露给外界的窗口) |
“c 可以直接当输出” |
❌ 实际任务(如分类、生成)的输出通常接在 h 上,c 只在内部传递,不直接暴露 |
💡 为什么这样设计?(工程视角)
- 防梯度消失:
c的加法路径让反向传播时梯度近似1×1×1...,不连乘衰减 - 解耦记忆与表达:长期记忆(
c)不需要每步都变形,短期表达(h)按需过滤,各司其职 - 硬件友好:
c的更新全是逐元素操作(+,*),GPU 并行效率极高
🎯 终极一句话总结
RNN 的
h是“记忆+输出”混合体,每步重写易失真;
LSTM 的c是“纯记忆传送带”,加法保真;h是“当前步摘要”,按需过滤。
两者并存,分工明确,这才是 LSTM 能记住长序列的根本原因。
LSTM伪码解析
# 1. 忘记门:决定旧记忆留多少
f = torch.sigmoid(Wf @ x + Uf @ h_prev + bf)
# 2. 输入门:决定新信息记多少
i = torch.sigmoid(Wi @ x + Ui @ h_prev + bi)
# 3. 候选记忆:提炼当前输入的关键信息
c_tilde = torch.tanh(Wc @ x + Uc @ h_prev + bc)
# 4. 更新长期记忆(cell state)
c_new = f * c_prev + i * c_tilde
# 5. 输出门:决定当前暴露给下一步的隐藏状态
o = torch.sigmoid(Wo @ x + Uo @ h_prev + bo)
h_new = o * torch.tanh(c_new)
这些公式看着吓人,其实拆开就是 “5个开关 + 2个仓库” 的流水线操作。我们先用符号词典认脸,再用一个带数字的真实例子跑一遍,保证你一眼看懂。
📖 第一步:符号词典(每个字母是干嘛的?)
| 符号 | 真实身份 | 形状 | 作用 |
|---|---|---|---|
x |
当前看到的词向量 | (input_size,) |
比如“包子”转成的数字列表 |
h_prev |
上一步的短期记忆/输出 | (hidden_size,) |
上一步总结的“当前上下文” |
c_prev |
上一步的长期记忆 | (hidden_size,) |
之前记下的所有重要信息(核心仓库) |
W, U, b |
训练出来的权重矩阵/偏置 | 形状匹配 | 相当于“滤镜”或“打分器”,决定网络看重什么 |
@ |
矩阵乘法 | - | 把输入投影到隐藏维度(线性变换) |
* 和 + |
逐位乘/加(不是矩阵运算!) | 向量对向量 | 每个位置独立计算,不交叉 |
sigmoid |
压缩机 → 0~1 |
向量逐位 | 生成“开关强度” |
tanh |
压缩机 → -1~1 |
向量逐位 | 生成“标准化内容” |
💡 关键前提:假设我们的隐藏维度
hidden_size = 3。所有向量都是 3 个数,比如[0.2, -0.5, 0.8]。
🧪 第二步:带入真实例子跑一遍
假设当前处理到句子:“我早上吃了 包子”
此时:
x= “包子”的向量 →[0.1, 0.9, 0.3]c_prev= 长期仓库(之前记下了“我”“早上”“吃了”)→[0.8, 0.6, 0.2]h_prev= 短期输出(上一步的总结)→[0.7, 0.4, 0.1]
网络经过训练,W, U, b 已经学会了怎么组合这些数。我们直接看计算结果:
🔹 1. 遗忘门 f:旧记忆留多少?
# 网络对 h_prev 和 x 打分,再 sigmoid 压缩到 0~1
f = sigmoid(Wf @ x + Uf @ h_prev + bf)
# → 假设算出: [0.9, 0.3, 0.8]
👉 人话:仓库有3个抽屉。第1个抽屉保留 90%,第2个只留 30%(快忘了),第3个留 80%。
🔹 2. 输入门 i:新信息记多少?
i = sigmoid(Wi @ x + Ui @ h_prev + bi)
# → 假设算出: [0.2, 0.9, 0.1]
👉 人话:当前词“包子”很重要,但只有第2个抽屉值得重点记(90%),其他两个不太相关。
🔹 3. 候选记忆 c_tilde:新信息具体长啥样?
c_tilde = tanh(Wc @ x + Uc @ h_prev + bc)
# → 假设算出: [0.1, 0.85, -0.3]
👉 人话:把“包子”和上下文揉在一起,提炼出原始内容。tanh 保证值在 -1~1,不爆炸。
🔹 4. 更新仓库 c_new:旧记忆 + 新记忆
c_new = f * c_prev + i * c_tilde
# 逐位计算:
# 第1位: 0.9*0.8 + 0.2*0.1 = 0.72 + 0.02 = 0.74
# 第2位: 0.3*0.6 + 0.9*0.85 = 0.18 + 0.765 = 0.945
# 第3位: 0.8*0.2 + 0.1*(-0.3) = 0.16 - 0.03 = 0.13
# → c_new = [0.74, 0.945, 0.13]
👉 人话:这就是 LSTM 的核心魔法。用 f 过滤旧仓库,用 i 注入新内容,加法拼接,不覆盖、不丢失。长期记忆平滑更新了。
🔹 5. 输出门 o & 生成短期记忆 h_new
o = sigmoid(Wo @ x + Uo @ h_prev + bo)
# → 假设算出: [0.8, 0.4, 0.6]
h_new = o * tanh(c_new)
# tanh(c_new) ≈ [0.63, 0.74, 0.13]
# 逐位乘 o:
# [0.8*0.63, 0.4*0.74, 0.6*0.13] ≈ [0.50, 0.30, 0.08]
# → h_new = [0.50, 0.30, 0.08]
👉 人话:c_new 是完整仓库,但下一步不需要看全部。o 决定“此刻对外展示哪些抽屉”,h_new 就是展示出来的短期摘要,传给下一个词。
🔍 为什么公式要这么设计?(设计意图)
| 公式行 | 设计目的 | 如果去掉会怎样? |
|---|---|---|
f = sigmoid(...) |
主动遗忘无关旧信息 | 仓库塞满垃圾,后期全是噪声 |
i = sigmoid(...) |
控制新信息注入量 | 重要信息被冲淡,或无关信息乱入 |
c_tilde = tanh(...) |
生成稳定范围的新内容 | 数值爆炸或消失,训练崩溃 |
c_new = f*c_prev + i*c_tilde |
加法更新,保留梯度通道 | 变成 RNN 的 tanh 覆盖 → 梯度消失 |
o * tanh(c_new) |
按需暴露,不污染仓库 | 每次全量输出,浪费计算且干扰门控 |
📦 终极比喻:智能档案管理员
把 LSTM 想象成一个带透明隔板的档案柜:
c(细胞状态)= 主档案柜(长期记忆),隔板是透明的,信息直接穿过h(隐藏状态)= 前台接待台(短期输出),只放当前需要的文件f(遗忘门)= 碎纸机强度旋钮(0~1),决定旧文件粉碎多少i(输入门)= 盖章笔力度(0~1),决定新文件盖多深c_tilde= 新文件草稿o(输出门)= 前台展示柜玻璃透明度,决定外面能看到多少
管理员每来一个新文件(x),就:
- 调碎纸机处理旧档案(
f * c_prev) - 调盖章笔处理新草稿(
i * c_tilde) - 两者相加放进主柜(
c_new) - 调玻璃透明度,把当前摘要推到前台(
h_new)
💎 一句话记住 LSTM 公式
f决定忘什么,i决定记什么,c_tilde提供新内容,加法更新仓库c,o决定对外输出什么h。
全是对齐的 0~1 开关和逐位运算,没有玄学,只有精准的流量控制。
📚 知识点整理:静态向量 vs 动态向量
一句话核心结论:
静态向量 = 词的"通用身份证"(有语义,但脱离语境)
动态向量 = 词在句中的"临时工作证"(语义随上下文动态调整)
两者都有语义,但粒度、适应性、用途完全不同。
🔹 知识点1:静态向量(Tokenizer + Embedding 输出)
✅ 它是什么?
- 输入:原始文本 → Tokenizer 切词 → 得到
token_id - 处理:
token_id查 Embedding 表 → 得到固定向量 - 输出形状:
(seq_len, hidden_size),但每个词的向量与句子无关
✅ 它包含哪些语义?(不是没语义!)
| 语义类型 | 例子 | 来源 |
|---|---|---|
| 🔤 字面/形态相似 | "跑"≈"跳";"苹果"≈"香蕉" | 共现统计、子词结构(BPE) |
| 🌍 领域/主题倾向 | "量子"靠近"物理";"梯度"靠近"优化" | 同领域词高频共现 |
| 🔗 浅层关系 | king - man + woman ≈ queen |
向量空间线性结构 |
| 🎭 多义混合 | "打" = 击打+拨打+打字...的平均压缩 | 所有语境下的"打"被训练成一个向量 |
⚠️ 核心局限
- 脱离语境:同一个词在任何句子里向量完全相同
- 多义混淆:无法区分"苹果(水果)"vs"苹果(公司)"
- 无法推理:处理不了否定、指代、长距离依赖等需要"理解句子"的任务
🔹 知识点2:动态向量(Encoder 输出)
✅ 它是什么?
- 输入:静态向量(Embedding 输出)
- 处理:经过 N 层 Self-Attention + FFN,每个词"看到"句中所有其他词
- 输出形状:同样是
(seq_len, hidden_size),但每个词的向量随句子内容动态变化
✅ 它额外获得了什么语义?
| 能力 | 例子 | 实现机制 |
|---|---|---|
| 🎭 消歧义 | "苹果"在"吃苹果"中偏向水果,在"苹果发布"中偏向公司 | Self-Attention 让词聚焦相关上下文 |
| 🔁 指代解析 | "他"的向量融入"张三"的特征 | "他"Attention 到前文实体 |
| ⚡ 否定/反转 | "不无聊"整体编码为正面 | "不"和"无聊"互相融合,联合表示 |
| 🧵 长程依赖 | 句首主语和句尾谓语直接关联 | Attention 让任意两词"直连",不管距离 |
🔑 核心设计
- 不是替换静态向量,而是加工升级
- 静态向量是"原材料",Encoder 是"语境加工厂"
- 输出向量 = 原始词义 + 语法角色 + 语义倾向 + 逻辑关系
🆚 终极对比表(收藏版)
| 维度 | 静态向量(Embedding) | 动态向量(Encoder 输出) |
|---|---|---|
| 是否依赖上下文 | ❌ 否,查表即得,全局固定 | ✅ 是,每句重算,动态生成 |
| 多义词处理 | ❌ 所有义项混合成一个向量 | ✅ 自动聚焦当前语境义项 |
| 生成方式 | 查表(O(1)) | 多层 Attention 计算(O(n²)) |
| 信息含量 | 词级先验统计 | 词+句级推理结果 |
| 典型用途 | 关键词匹配、轻量检索、初始化 | 分类、生成、问答、翻译等复杂任务 |
| 计算成本 | 极低 | 较高(但值得) |
| 可解释性 | 可用 t-SNE 看词聚类 | 可用 Attention 权重看词间关系 |
🧪 验证思路(3行代码看清区别)
from transformers import AutoTokenizer, AutoModel
import torch.nn.functional as F
# 加载模型
tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese")
model = AutoModel.from_pretrained("bert-base-chinese")
# 两个含"苹果"的句子
sent1 = "我喜欢吃苹果" # 水果
sent2 = "苹果发布了新手机" # 公司
# 1. 静态向量:查表得到,完全相同
ids1 = tokenizer(sent1, return_tensors="pt")["input_ids"]
ids2 = tokenizer(sent2, return_tensors="pt")["input_ids"]
static1 = model.embeddings.word_embeddings(ids1)
static2 = model.embeddings.word_embeddings(ids2)
# → "苹果"位置的向量余弦相似度 = 1.000
# 2. 动态向量:Encoder 计算,明显不同
with torch.no_grad():
out1 = model(ids1).last_hidden_state
out2 = model(ids2).last_hidden_state
# → "苹果"位置的向量余弦相似度 ≈ 0.5~0.7
# 3. 结论:静态认字,动态懂句
🎯 应用场景指南
| 任务类型 | 推荐用向量 | 原因 |
|---|---|---|
| 🔍 关键词检索/匹配 | ✅ 静态向量 | 快、够用、无需理解语境 |
| 📝 文本分类(短文本) | ⚠️ 两者都可 | 简单任务静态够用,复杂任务动态更准 |
| 🤖 问答/翻译/生成 | ✅ 动态向量 | 必须理解上下文、指代、逻辑 |
| 🧠 语义相似度计算 | ✅ 动态向量 | 需要消歧义、捕捉句级含义 |
| 🚀 轻量级部署/嵌入式 | ⚠️ 静态向量+简单聚合 | 权衡效果与计算成本 |
💡 终极比喻(帮助记忆)
📚 静态向量 = 字典里的词条目
• "打"字下面列20条释义,固定不变
• 优点:查得快,覆盖广
• 缺点:脱离句子,不知道此刻用哪条
🧠 动态向量 = 你听到句子时的瞬间理解
• 听到"打电话",大脑自动高亮"通讯"义项
• 听到"打篮球",自动切换"运动"义项
• 优点:精准、灵活、能推理
• 缺点:计算慢,需要"思考"
🔗 关系:字典是基础,大脑是引擎。
没有字典,大脑无从理解;
只有字典,无法应对真实对话。
✨ 一句话复习清单
- ✅ 静态向量有语义,是预训练学到的"词的通用统计特征"
- ✅ 动态向量更精准,是 Encoder 结合语境"实时推理"的结果
- ✅ 静态是"原材料",动态是"加工品",两者配合才能既懂词又懂句
- ✅ 选哪个?看任务:轻量匹配用静态,复杂理解用动态
🎁 彩蛋记忆法:
静态向量 = 食材(土豆、牛肉)
Encoder = 厨师 + 菜谱 + 火候
动态向量 = 端上桌的"土豆烧牛肉"(味道取决于搭配和语境)
🔍 Q, K, V 机制是如何发挥作用的?
一句话总结:
Q、K、V 是 Self-Attention 的“信息检索三剑客”。
它们让每个词能:按需提问(Q) → 全局匹配(K) → 精准提取(V),从而一次性融合整句话的上下文,生成动态向量。
下面用比喻 + 步骤拆解 + 矩阵形状 + 代码直觉,帮你彻底看透它的工作原理。
📦 符号词典:Q、K、V 到底是谁?
| 符号 | 真实身份 | 作用 | 形状 |
|---|---|---|---|
| Q (Query) | 当前词发出的“问题” | “我想从上下文里找什么?” | (seq_len, d_k) |
| K (Key) | 每个词自带的“标签” | “我有什么特征可供别人检索?” | (seq_len, d_k) |
| V (Value) | 每个词携带的“内容” | “我的真实信息长什么样?” | (seq_len, d_v) |
💡 关键设计:Q 和 K 负责“算相似度”(匹配),V 负责“存信息”(内容)。把“怎么找”和“找什么”拆开,是 Attention 灵活性的核心。
🔄 5步工作流(以句子 “猫 追 老鼠” 为例)
假设输入是3个词的静态向量,隐藏维度 d_k = d_v = 4。
① 投影:生成 Q, K, V
# 输入 X: (3, 4) → [猫, 追, 老鼠] 的静态向量
Q = X @ W_Q # 用学习到的权重矩阵,把每个词转成“提问向量”
K = X @ W_K # 转成“标签向量”
V = X @ W_V # 转成“内容向量”
👉 此时 Q、K、V 形状都是 (3, 4)。每个词都有了属于自己的 Q、K、V。
② 匹配:算出“谁该关注谁”
scores = Q @ K.T # 形状: (3, 3)
📊 矩阵含义:scores[i][j] 表示 第 i 个词对第 j 个词的关注原始分。
- 例:
scores[0][2]= “猫”对“老鼠”的匹配度 - 例:
scores[1][0]= “追”对“猫”的匹配度
③ 缩放 + 归一化:变成概率分布
weights = softmax(scores / sqrt(4)) # 形状: (3, 3)
/ sqrt(d_k):防止数值过大导致 softmax 变成“非0即1”的硬选择softmax:每行加起来 = 1,变成注意力权重(0~1)- 例:第一行
[0.1, 0.2, 0.7]→ “猫” 10%关注自己,20%关注“追”,70%关注“老鼠”
④ 加权提取:融合上下文信息
output = weights @ V # 形状: (3, 4)
🔍 这是最关键的一步:
- 每个词的输出 =
所有词的 V 向量 × 对应权重的加权和 - “猫”的输出 =
0.1*V猫 + 0.2*V追 + 0.7*V老鼠 - 结果就是富含上下文的动态向量!
⑤ 输出:交给下一层
# output 就是 Encoder 这一层的输出,形状 (3, 4)
# 传入下一层 Attention,反复提纯 → 最终得到完整上下文表示
💡 为什么要把“匹配”和“内容”分开?(QK vs V)
| 设计 | 如果合并(只用 X) | 分开设计(Q, K, V) |
|---|---|---|
| 灵活性 | 匹配和内容绑定,无法区分“我想找什么”和“我有什么” | Q 决定搜索意图,K 决定被检索特征,V 决定实际内容 |
| 表达能力 | 只能做“对称匹配” | 可做“非对称匹配”(如:主语关注动词,动词关注宾语) |
| 多任务适配 | 固定模式 | 同一组词,在不同任务中可学习出不同的 Q/K/V 投影 |
🎯 生活比喻:相亲网站
- 你填的“择偶条件” → Q
- 别人主页的“个人标签” → K
- 别人主页的“详细资料/照片” → V
- 系统用你的 Q 去匹配所有人的 K,算出匹配度,再按匹配度加权展示 V → 得到你的“推荐列表”
📐 代码/矩阵形状直观展示
import torch
import torch.nn.functional as F
# 1. 输入 (seq=3, d_model=4)
X = torch.randn(3, 4)
# 2. 投影到 Q, K, V
W_Q = torch.randn(4, 4)
W_K = torch.randn(4, 4)
W_V = torch.randn(4, 4)
Q = X @ W_Q # (3, 4)
K = X @ W_K # (3, 4)
V = X @ W_V # (3, 4)
# 3. 注意力计算
scores = Q @ K.T # (3, 3) 原始匹配分
weights = F.softmax(scores / (4**0.5), dim=-1) # (3, 3) 归一化权重
output = weights @ V # (3, 4) 融合后的上下文向量
🔑 形状变化一目了然:
输入(3,4) → Q/K/V(3,4) → 权重(3,3) → 输出(3,4)
句子长度变长?只是矩阵变大,公式不变,天然支持并行。
🆚 对比 RNN / 静态向量:为什么是“降维打击”?
| 维度 | RNN | 静态 Embedding | QKV Self-Attention |
|---|---|---|---|
| 信息融合方式 | 串行传递 h,易衰减 |
无融合,各词独立 | 全量并行匹配,任意两词直连 |
| 长距离依赖 | 靠 h 一步步传,易丢失 |
不存在 | 直接 Q@K^T,距离再远也是一步 |
| 并行计算 | ❌ 必须按时间步串行 | ✅ 但无上下文 | ✅ 纯矩阵运算,GPU 跑满 |
| 动态语义 | 有,但受限于顺序和梯度 | ❌ 固定不变 | ✅ 强,每词按需提取上下文 |
⚠️ 常见误区澄清
| 误区 | 真相 |
|---|---|
| “Q、K、V 是固定的词向量” | ❌ 它们是每个词经过可学习线性投影后的临时表示,随训练更新 |
| “Q 只能匹配自己的 K” | ❌ Self-Attention 是全连接匹配,每个 Q 和所有 K 算分 |
| “多头注意力是多个模型” | ❌ 是同一层内多组独立的 Q/K/V 投影,分别捕捉语法、语义、位置等不同关系,最后拼接 |
| “softmax 后权重都很平均” | ❌ 训练后通常会极度稀疏(1~2个词权重接近1,其余≈0),实现精准聚焦 |
🎯 终极总结(3句话记住 QKV)
- Q 是“问题”,K 是“标签”,V 是“内容”。用 Q 去碰所有 K,算出关注度。
- 关注度归一化后,加权求和所有 V,得到当前词的上下文动态向量。
- 全并行、全连接、可学习,让模型摆脱顺序限制,真正实现“见词知意”。
✨ 一句话比喻:
QKV = 智能搜索引擎
你输入搜索词(Q) → 引擎比对网页标题/标签(K) → 按相关度抓取正文(V) → 返回精准摘要。
为什么是V而不是V转置
直接回答:因为矩阵乘法维度必须对齐,且 weights @ V 的数学操作正好是“按权重对词的内容向量做加权求和”。用 V.T 会维度报错,且语义完全错乱。
下面用 维度对齐 → 计算目的 → 代码验证 三步拆透,保证你看完不会再有疑问。
📐 1. 维度对齐证明(为什么只能是 V?)
假设标准输入形状(PyTorch/HF 默认):
seq_len = 3(句子3个词)d_v = 4(向量维度4)
| 张量 | 形状 | 含义 |
|---|---|---|
weights(注意力权重) |
(3, 3) |
每行是某个词对其他3个词的关注比例 |
V(值矩阵) |
(3, 4) |
每一行是一个词的内容向量 |
V.T(转置后) |
(4, 3) |
每一列变成一个词的内容向量 |
✅ weights @ V → (3, 3) × (3, 4) = (3, 4)
👉 维度完美对齐!输出形状和输入一致,可直接传给下一层。
❌ weights @ V.T → (3, 3) × (4, 3)
👉 内维不匹配(3 ≠ 4),PyTorch/NumPy 直接抛 RuntimeError: mat1 and mat2 shapes cannot be multiplied。
🎯 2. 为什么 K 要转置,V 不用?(计算目的不同)
| 操作 | 公式 | 目的 | 为什么转置? |
|---|---|---|---|
| 算相似度 | Q @ K.T |
让每个 Q 和每个 K 做点积,得到“谁该关注谁” | K 转置后,行变列,才能和 Q 的每一行做内积(行×列) |
| 加权融合 | weights @ V |
用算好的权重,把 V 的向量按行加权求和 | V 不转置,保持行是词向量,权重矩阵乘上去正好实现加权平均 |
🔍 关键洞察:
Q @ K.T是 “匹配阶段”:需要两两算分 → 必须转置让行列对齐做点积weights @ V是 “提取阶段”:需要加权求和 → 保持原样让权重直接乘在词向量行上
🧠 3. 语义层面:用 V 是“跨词融合”,用 V.T 是“维度大乱炖”
假设 weights 第一行是 [0.1, 0.2, 0.7](词1的关注分布),V 三行分别是 v1, v2, v3。
✅ weights @ V 的第一行输出:
out1 = 0.1*v1 + 0.2*v2 + 0.7*v3
👉 物理意义:词1的新表示 = 10%自己 + 20%词2内容 + 70%词3内容。跨词融合,完美符合注意力本质。
❌ 如果强行用 V.T(假设维度碰巧能乘):
权重会乘在列上,变成:
out1_dim0 = 0.1*v1_dim0 + 0.2*v2_dim1 + 0.7*v3_dim2
👉 物理意义:把词1的第0维、词2的第1维、词3的第2维混在一起。维度交叉污染,向量空间结构彻底破坏,模型根本学不出任何有效特征。
🧪 4. 3行代码直观验证
import torch
W = torch.tensor([[0.1, 0.2, 0.7]]) # (1, 3) 注意力权重
V = torch.randn(3, 4) # (3, 4) 三个词的内容向量
print((W @ V).shape) # → torch.Size([1, 4]) ✅ 合法:加权求和
# print(W @ V.T) # → RuntimeError: size mismatch, 3 != 4 ❌ 直接报错
🔍 看数值也能验证:
manual = 0.1*V[0] + 0.2*V[1] + 0.7*V[2]
print(torch.allclose(W @ V, manual)) # → True
@ V 就是线性代数里的“加权行组合”,数学和代码完全一致。
📜 附:为什么有些资料写成 V^T?
极少数早期论文或特定数学推导中,会把序列放在第二维,即形状为 (d_v, seq_len)。在那种排版下,公式会写成 V^T @ weights^T 来保持维度对齐。
但现代所有框架(PyTorch nn.MultiheadAttention、HuggingFace、JAX)统一采用 (seq_len, d_v),所以标准公式永远是:
Attention = softmax(Q @ K.T / sqrt(d_k)) @ V
💎 终极一句话总结
K.T是为了“算点积匹配”,V不转置是为了“按行加权求和”。
维度必须对齐,语义必须是“词向量加权融合”,用V.T既会报错又会破坏特征空间。
归根结底,行向量与矩阵相乘,就是把矩阵的行向量按行向量提供的系数做线性组合,分块矩阵乘法也能解释。
🔍 多头注意力(Multi-Head) vs 单头注意力(Single-Head)
一句话总结:
单头 = 用一套 Q/K/V 全局看一遍,只能捕捉一种主导关系;
多头 = 把隐藏维度拆成多份,每份独立算 Attention,各自专注不同子空间的关系,最后拼接融合。
本质是“分工并行 + 信息集成”,不是单纯“多算几遍”。
📐 1. 结构对比(形状与流程)
假设 d_model = 8(隐藏维度),seq_len = 3
🔸 单头注意力
Q = X @ W_Q # (3, 8)
K = X @ W_K # (3, 8)
V = X @ W_V # (3, 8)
out = softmax(Q @ K.T / √8) @ V # (3, 8)
👉 全程只有一套投影权重,所有关系混在一个 8 维空间里竞争注意力。
🔸 多头注意力(假设 h = 4 头)
# 1. 投影到总维度 (3, 8)
Q_all = X @ W_Q_all
K_all = X @ W_K_all
V_all = X @ W_V_all
# 2. 拆成 4 个头 (关键:reshape + transpose)
Q = Q_all.view(3, 4, 2).transpose(1, 2) # (3, 4, 2) → (4, 3, 2)
K = K_all.view(3, 4, 2).transpose(1, 2) # 每个头维度: d_k = d_v = 8/4 = 2
V = V_all.view(3, 4, 2).transpose(1, 2)
# 3. 每个头独立算 Attention(框架底层高度并行,无显式 for)
# head_i: (3, 2) → softmax(Q_i@K_i.T/√2) @ V_i
head_outs = [] # 列表存4个 (3, 2)
# 4. 拼接 + 输出投影(必不可少的一步)
concat = torch.cat(head_outs, dim=-1) # (3, 8)
out = concat @ W_O # (3, 8)
🔑 关键设计:
d_k = d_v = d_model / h:保证总参数量与单头接近,不盲目膨胀W_O输出投影:不是摆设,负责把多头的“专项报告”重新混合成统一表示
🧠 2. 为什么一定要拆多头?(核心动机)
高维向量(如 768 维)里同时编码了多种信息:语法结构、语义相似、指代关系、位置顺序、领域特征等。
| 设计 | 信息处理方式 | 结果 |
|---|---|---|
| 单头 | 所有关系在同一个 768 维空间里“抢”注意力 | 强势关系(如词频高的语义相似)会压制弱势关系(如远距离指代),模型被迫“平均化”或“妥协” |
| 多头 | 拆成 12 个 64 维子空间,每个头独立学习投影权重 | 头1专盯主谓一致,头2专盯代词指代,头3专盯同义替换,头4专盯位置关系……互不干扰,各尽所长 |
💡 本质:多头不是“增加容量”,而是“解耦信息通道”。就像把一条拥挤的单行道拆成多条专用车道,通行效率反而更高。
🎨 3. 直观比喻(秒懂分工逻辑)
| 场景 | 单头注意力 | 多头注意力 |
|---|---|---|
| 🔍 破案 | 一个全能侦探:既要找时间线,又要查人物关系,还要核对地点,容易顾此失彼 | 侦探团队:头1盯资金流,头2盯通讯记录,头3盯监控轨迹,头4盯社交关系。队长(W_O)最后汇总成完整案情 |
| 🎧 听交响乐 | 一只耳朵听所有乐器混音,很难分离小提琴和定音鼓 | 多只麦克风分频段收音,后期调音台(W_O)重新混音,层次清晰 |
| 📊 数据分析 | 用主成分分析(PCA)降成1维,丢失大量结构 | 保留多个正交子空间,信息完整 |
📊 4. 实际效果对比(为什么现代模型标配多头?)
| 维度 | 单头 | 多头(典型 8~16 头) |
|---|---|---|
| 表达能力 | 只能捕捉一种主导关系 | 多子空间并行,语法/语义/指代/位置可分离 |
| 鲁棒性 | 某个关系占优会压制其他,易过拟合噪声 | 头部可互补/冗余,对局部扰动更稳定 |
| 计算成本 | 1 次 Attention | h 次,但完全并行,GPU 吞吐不降 |
| 参数量 | 少 | 略多(约 +h/(h-1) 倍),但信息密度高 |
| 现代实践 | 仅用于极简/嵌入式场景 | Transformer 标配(BERT:12, GPT-2:12~16, LLaMA:32) |
⚠️ 5. 常见误区澄清
| 误区 | 真相 |
|---|---|
| “多头就是把同一个句子跑 h 遍” | ❌ 是同一层内并行,用不同的可学习投影矩阵 W_Q_i, W_K_i, W_V_i |
| “头越多效果一定越好” | ❌ 有收益递减。d_k 太小(如 <16)会导致子空间表达力不足,且注意力分布变平滑 |
| “每个头必须有明确人类可解释的语义” | ❌ 不需要。网络自动学习分工,头之间可能高度纠缠,只要整体表征有效即可 |
“去掉 W_O 拼接结果就行” |
❌ W_O 是跨头信息融合器。没有它,多头只是独立通道的简单堆叠,无法交互 |
💎 终极总结(3句话记住)
- 单头是“全能但妥协”,多头是“分工再融合”。拆维度是为了让不同关系在独立子空间里充分表达。
W_O不是冗余:它把多头的“专项输出”重新投影回原空间,允许跨头信息交叉。- 现代大模型不用单头:因为自然语言/代码/多模态数据的关系是多维交织的,单头无法解耦,多头是性价比最高的结构升级。
✨ 一句话比喻:
单头 = 用一支笔画所有细节
多头 = 用一套彩色铅笔分层上色,最后透明叠加
颜色不串味,画面更立体。
很多人学多头注意力时的核心困惑:
“总维度没变,凭什么说表达能力更强?是不是只是把一个大矩阵拆成几个小矩阵,最后又拼回去,纯属多此一举?”
答案是:维度总数相同 ≠ 表达能力相同。关键差别不在“容量大小”,而在“信息如何组织、梯度如何更新、关系如何解耦”。
下面用 优化动力学 → 表示结构 → 架构设计 三层拆透,保证你看完不再觉得“换汤不换药”。
🔪 1. 优化视角:单头是“梯度打架”,多头是“并行修路”
假设 d_model = 512,句子包含三种关系:主谓结构、代词指代、同义替换。
| 机制 | 梯度更新过程 | 结果 |
|---|---|---|
| 单头 | 所有关系挤在 512 维空间里竞争注意力。训练时,梯度要同时优化三种目标,方向可能冲突(如主谓要求权重集中,指代要求分散)→ 梯度互相抵消或震荡 | 模型被迫学出“平均化/妥协”的表示,收敛慢,易卡在局部最优 |
| 多头(8头×64维) | 每个头有独立的 W_Q, W_K, W_V,独立算 Softmax、独立回传梯度。头1专学主谓,头2专学指代,头3专学同义… → 梯度路径隔离,互不干扰 |
每个子空间都能快速收敛到适合自己任务的流形,整体训练更稳更快 |
💡 关键洞察:神经网络不是静态的数学空间,而是动态优化的过程。同样的维度,组织方式不同,优化难度天差地别。
🧩 2. 表示视角:独立 Softmax 防止“赢家通吃”
这是最容易被忽略的结构差异:
- 单头:
softmax在 512 维全局归一化。如果“语义相似”关系很强,注意力权重会被它吸走大半,其他关系直接饿死。 - 多头:每个头在 64 维局部独立归一化。头1可以 100% 关注语法,头2 可以 100% 关注位置,互不抢夺。
👉 这就像:
- 单头 = 全国只有一个高考分数线,文科理科一起卷,偏科生直接淘汰
- 多头 = 文理分科独立划线,特长生各展所长,最后总分相加
同样的总维度,多头通过“局部归一化”强制模型保留多样性表示。
🔗 3. 架构视角:W_O 不是拼接,而是“跨通道混合器”
你提到“最后合并还是这么多维”。注意:多头不是简单 cat 完就结束,而是:
out = concat(head_1, ..., head_h) @ W_O # W_O 是 d_model × d_model 的全连接层
W_O的作用是让不同头的特征交叉融合。- 例:头1输出“主语位置”,头2输出“动词类型”,
W_O学会把它们组合成“主谓搭配是否合理”。 - 单头没有这个显式的跨子空间交互层,所有混合必须在前面的非线性里隐式完成,极难优化。
🔍 数学本质:
单头学习的是 f(X) = softmax(QK^T)V
多头学习的是 g(X) = W_O · concat( softmax(Q₁K₁ᵀ)V₁, ..., softmax(QₕKₕᵀ)Vₕ )
这两个函数的假设空间(Hypothesis Space)完全不同。多头的函数更“平滑”、更“可分解”,对复杂语言结构拟合能力更强。
📊 实验与可视化证据(不是玄学)
| 证据来源 | 发现 |
|---|---|
| 📐 参数效率对比(Vaswani et al. 2017) | 同等参数量下,8头比单头在翻译任务上 BLEU 高 1.5~2.0 |
| 🔍 头部角色可视化(Clark et al. 2019) | BERT 的 12 个头自动分化:有的专盯标点/句法,有的专盯实体共指,有的专盯否定词 |
| 📉 消融实验 | 去掉多头(改用单头+同等维度):下游任务准确率下降 3%~8%,长文本生成连贯性明显变差 |
🎯 终极对照表(收藏版)
| 维度 | 单头注意力 | 多头注意力 |
|---|---|---|
| 归一化范围 | 全局 d_model 维 Softmax |
每个头 d_k 维独立 Softmax |
| 梯度路径 | 单一路径,多目标竞争 | 多路并行,目标隔离 |
| 表示结构 | 一锅乱炖,易赢家通吃 | 分层解耦,多样性保留 |
| 跨关系交互 | 隐式、难优化 | 显式 W_O 线性混合,易学习 |
| 函数空间 | 高度耦合的非线性流形 | 可分解的子流形拼接 |
💎 一句话总结
多头不是“增加维度”,而是“改造维度的使用方式”。
把大空间拆成独立子空间 → 独立归一化防内卷 → 独立梯度易优化 →W_O负责跨头融合。
同样的 512 维,单头是“一条拥挤的单行道”,多头是“多车道+智能立交”,通行效率自然不同。
与大模型交互
- 提示工程
- 模型采样参数
- 本质是通过调整模型对“概率分布”的采样策略,让输出匹配具体场景需求。
- 更改了Softmax概率分布,根据不同策略“重新调整”或“截断”分布,从而改变大模型输出的下一个token。
- 温度、Top-k、Top-p
- 温度设置为0,其余参数都毫无意义,因为只会讲最有可能的Token作为下一个预测的Token。
- Top-k设置为1,其余参数也毫无意义。
- 零样本、单样本与少样本提示
- 指令调优:微调技术,通过“指令-回答”格式数据进一步训练模型,从而能够更好地理解并遵循用户的指令。
- 基础提示技巧:角色扮演、上下文示例
- 思维链
- 模型采样参数
- 文本分词
- 计算机本质只能理解数字,我们需要将自然语言文本转换成模型能够处理的数字格式,称为分词。
- 将原始文本切分成的一个个最小的单元,我们称为词元token。
- 简单分词策略:
- 按词分词:直接用空格或者标点分词
- 词表爆炸与未登录词OOV问题
- 语义关联确实,模型难以捕获词形相近词之间的语义关系。
- 按字符分词:
- 单个字符不具备独立语义,学习效率低下
- 按词分词:直接用空格或者标点分词
- 子词分词
- 常见词保留完整词元
- 不常见词拆分为多个有意义的子词片段
- 算法
- BPE,字节对编码,可理解为一个贪心的合并过程
- 首先,初始化,将词表初始化为所有语料库中出现过的基本字符
- 迭代合并:语料库上,统计相邻词元对的出现频率,找出频率最高的一对,合并为一个新的词元,加入词表。
- 重复迭代合并过程,直到达到词表大小预设值。
- 优化算法
- WordPiece:合并标准不是简单看“谁出现最频繁”,而是用语言模型似然衡量——优先合并那些能最大程度提升语料库整体预测概率的词元对(如 "un" + "##happy" 比 "unh" + "appy" 更能帮助模型理解词根与派生关系),本质是让分词结果服务于下游建模效果。
- SentencePiece:直接把原始文本(含空格、标点)当作字符序列处理,空格也被编码为特殊符号(如
▁),使得分词→编码→解码→还原全程严格可逆,且无需依赖语言特定的预处理规则(如英文空格切分、中文无空格),真正实现语言无关、端到端、损失无损的文本数字化。
- BPE,字节对编码,可理解为一个贪心的合并过程
- 分词对于agent开发者意义?
- 上下文窗口限制是按照token计算
- API成本
- 解决模型表现异常,可能就是token的划分影响了模型理解。
- 开源模型和闭源模型选择
缩放法则与局限性
- 缩放法则
- 在资源允许范围内,尽可能扩大模型规模和训练数据量
- “能力的涌现”,模型规模达到一定阈值,展现出全新能力
- 模型幻觉
- 事实性幻觉、忠实性幻觉、内在幻觉
- 解决
- 数据层面、模型层面、推理与生成层面
智能体经典范式构建
三种代表性的智能体范式:
- ReAct:一种将“思考”和行动紧密结合的范式,让智能体边想边做,动态调整。
- Plan-and-Solve:一种“三思而后行”的范式,通过自我批判和修正来优化结果。
-
Reflection:一种赋予智能体“反思”能力的范式,通过自我批评和修正来优化结果。
-
ReAct
- 核心思想是模仿人类解决问题的方式,将推理(Reasoning)与行动(Acting)显式地结合起来,形成一个“思考-行动-观察”的循环。
- 工作流程,不断重复执行“思考、行动、观察”的循环,将新的观察结果追加到历史记录中,形成不断增长的上下文,直到思考中觉得已经找到最终答案,输出结果。
- 推理使得行动更具有目的性,而行动则为推理提供了事实依据。
- 历史记录中可以不记录此前思考结果,而只记录原始问题以及所有步骤的“行动-观察”历史轨迹。
- 适用场景:
- 需要外部知识的任务:查询实时天气、搜索专业领域的知识等。
- 需要精确计算的任务
- 需要与API交互的任务
- 关键也就是需要使得智能体具备调用外部工具的能力。
- 实现一个ReAct智能体:
- 系统提示词设计:
- 角色定义:告知任务并提醒可以使用工具
- 工具清单:告知有哪些可用的“手脚”
- 格式规约:最重要部分,强制LLM的输出具有结构性,使得我们能够通过代码精确解析意图。
- 动态上下文:将原始问题和不断积累交互历史注入,使得LLM基于完整上下文进行决策。
- 核心循环实现:不断“格式化提示词->调用LLM->执行动作->整合结果”,直到任务完成或达到最大步数限制。
- 解析输出结果:解析思考和行动,进一步解析行动,提取出工具名和参数
- 工具调用与执行
- 检查是否Finish,否则调用对应工具并得到observation。
- 观察结果整合
- 将Action和Observation添加回历史记录,为下一轮循环提供新的上下文。
- 系统提示词设计:
- 主要特点:
- 高可解释性:Thought链让我们看到智能体决策的心路历程。
- 动态规划与纠错能力:走一步,看一部,动态调整后续的Thought和Action。
- 工具协同能力:LLM思考与外部工具执行能力的结合。
- 局限性:
- 对LLM自身能力强依赖
- 执行效率问题:多次调用,串行循环
- 提示词脆弱:整个机制建立在精心设计提示词模板上
- 可能局部最优:步进式,缺乏一个全局的、长远的规划
- Plan-and-Solve
- 将任务处理明确分为两个阶段:先规划,后执行
- 工作原理:
- 核心动机是为了解决思维链在处理多步骤、复杂问题时容易偏离轨道问题
- 两个核心阶段:
- 规划阶段:将问题分解,制定一个清晰、分步骤地行动计划
- 执行阶段:严格按照计划中步骤,逐一执行,直到计划中的所有步骤都完成,最终得出答案。
- 对于第i步骤,其解决方案依赖于原始问题、完整计划以及之前所有步骤的执行结果。
- 最终答案就是最后一个步骤的执行结果。
- 适用场景:结构性强、可以被清晰分解的复杂任务
- 多步数学应用题
- 需要整合多个信息源的报告撰写
- 代码生成:需要先构思好函数、类和模块的结构,再逐一实现
- 规划阶段目标是让大模型接收原始问题,输出一个清晰、分步骤地行动计划,这个计划必须是结构化的。
- 例如要求输出计划为Python列表形式,简化解析工作,使得比解析自然语言更稳定、更可靠。
- 执行器与状态管理
- 执行器的提示词与规划器不同,其目标是在已有上下文基础上,专注解决当前一个步骤。
- 提示词包括:原始问题、完整计划、历史步骤与结果、当前步骤
- 整体流程:先计划,后执行,两套提示词,仅此而已。
- Reflection
- 上述范式,智能体一旦完成任务,其工作流程结束。
- 核心思想:引入一种事后的自我校正循环,审视工作,发现不足,并进行迭代优化。
- 核心流程:执行->反思->优化,不断循环。
- 执行:可以使用上述范式尝试完成任务,得到“初稿”
- 反思:独立、特殊提示的大模型扮演该角色进行事实性、逻辑、效率等评估
- 优化:将初稿和反馈作为上下文,根据反馈进行修正,生成更完善的“修订版”
- 特点:
- 内部纠错回路、将一次性任务转变为持续优化过程、为智能体提供“短期记忆”的宝贵经验
- 记忆管理机制:例如可以记录反思和优化轨迹,并取出作为上一轮结果
- 智能体设计
- 提示词设计:
- 初始执行提示词:起点,相对简单
- 反思提示词:对上一轮回答生成批判性分析,提供具体的、可操作的反馈
- 优化提示词:依据反馈,进行修改和优化
- 迭代循环:反思和优化
- 提示词设计:
- 成本收益
- 每一轮迭代至少调用两次大模型,串行过程,任务延迟显著提高
- 核心收益
- 解决方案质量的跃迁,自我纠错循环提升了最终结果的可靠性。
- 适用于对最终结果的质量、准确性和可靠性有极高要求,且对任务完成的实时性要求相对宽松的场景。