漫谈大模型原理:从分词到注意力的科普教程

作者:da吃一鲸8862026.11.13 03:44浏览量:0

简介:本教程以科普加动手实践的方式,带读者拆解大模型“预测下一个词”的全过程,包括分词与嵌入、注意力机制、多层堆叠、概率采样,以及用缩放点积、因果掩码、行和为1等校验方法确认每一步都正确。学完后,读者既能理解模型为何会“答非所问”,也能掌握调节生成稳定性、排查生成异常和降低使用成本的通用方法。

漫谈大模型原理:从分词到注意力的科普教程

很多技术文章讲大模型时,开篇就是长篇公式和推导。其实大模型最底层的目标相当朴素:根据前面的文字,预测下一个字。把这个任务做好,再配合足够的数据和参数规模,翻译、总结、写代码、推理、对话等能力就会逐渐出现。这类由规模扩大而显现的综合能力,通常被称为“涌现”。

本教程用尽量少的公式,配合示意性的伪代码,带你完成一次“原理级”的小实践:

  1. 把一段中文变成模型能处理的数字;
  2. 亲手计算一次注意力,看清模型如何“回看”前文;
  3. 观察模型如何把“预测”变成概率分布并采样出下一个词;
  4. 学会验证每个环节是否正确、出问题时如何排查。

学完之后,你应该能够回答这些问题:模型为什么不认识汉字、注意力到底在算什么、同样的输入为什么会生成不同的回答、生成的文字突然重复或离题时应该从哪一步查起。

一、教程目标与适用读者

1. 本教程要完成什么

最终目标是走通一条从文字到文字的链路:输入文本 → 分词 → 嵌入 → 注意力计算 → 输出概率 → 采样下一个词 → 重复生成。链路中的每一步都能讲清“做了什么、为什么这么做、如何验证”。

2. 适合哪些读者

  • 应用开发者:希望在接入大模型能力之前,先理解“下一个词预测”如何解释模型的日常表现。
  • 算法入门者和学生:已经了解机器学习的基本概念,希望把注意力机制的公式与实际计算对应起来。
  • 技术负责人:需要从原理层面判断幻觉、重复、响应慢等问题的成因,从而选择合适的优化手段。
  • 完全零基础的读者:可以先阅读第一、二部分建立整体认识,再决定是否动手实践。

3. 一个重要前提

大模型并不“理解”你在说什么,也没有先查资料再作答的过程。它学会的是一个统计规律:在见过的海量文本里,给定前文时,下一个词通常是什么。所谓“理解”,来自这种统计规律在海量数据上的极致压缩与泛化。

二、核心原理:把语言变成一个“猜下一个字”的游戏

1. 语言建模是什么

一段文本可以拆成一连串词元(token),模型学习的是每个位置上的条件概率。对于长度为 n 的文本,它出现的概率可以链式分解为:

P(文本) = P(第1个词) × P(第2个词 | 第1个词) × P(第3个词 | 前2个词) × … × P(第n个词 | 前 n-1 个词)

每一步都只做一件事:看过前面所有词,给出下一个词的概率分布。

2. 为什么简单任务能长出复杂能力

  • 训练信号极其丰富:任何文本都能用来训练,无需人工标注“这句话是什么意思”。
  • 被迫学习语法和常识:要准确预测下一个词,模型必须顺带学会主谓一致、搭配习惯、事实关联和格式规律。
  • 规模带来涌现:当参数量、训练数据量和计算量达到一定规模后,翻译、归纳总结、代码补全、多步推理等能力会明显显现。

三、前置准备

1. 基础环境

  • 运行环境:Python 3 或同类语言,任意支持矩阵运算的张量计算库即可,无需高端显卡。
  • 模型素材:一个可公开获取的小型预训练语言模型,以及与之配套的分词器和词表文件。三者必须来自同一模型,不能混用。
  • 基础概念:了解词向量、矩阵乘法、Softmax 函数的含义。

2. 实践时的两点约定

  • 本文代码为通用示意伪代码,重点展示数据形状(shape)和计算流程,不能直接运行。
  • 词元编号均为方便讲解而虚构,与任何真实模型无关。

四、实施步骤:完整拆解一条生成链路

步骤一:分词——把文字变成编号

做什么:把输入文本切成一串 token,再按词表把每个 token 映射为整数编号。

为什么这么做:计算机底层只能处理数字,分词是连接“人类语言”与“机器计算”的第一道关口。同时,分词结果决定了后续计算的粒度。

怎么做:现代大模型普遍不直接以单字或单词为单位,而是采用子词切分,常见做法包括字节对编码(BPE)等。它们的思路大致如下:

  1. 从字符或字节出发;
  2. 在语料中反复把出现最频繁的相邻单元合并成一个更大的单元;
  3. 形成一份词表,覆盖常见词、生僻字和符号。

以一句话为例,示意切分结果为:

“机器学习 很 有趣 。” → [101, 20048, 6, 35127, 7]

其中“机器学习”整体是一个 token,“很”“有趣”以及句号各自是一个 token。

注意事项:

  • 同一个模型必须始终使用同一个词表和同一套分词规则。
  • 低频词、罕见的人名地名可能被切成多个片段,这类情况往往是“输出夹生字”的根源之一。
  • 多数模型需要输入开始标志和结束标志,分别用于告知模型“从头开始生成”和“内容到此结束”。
  • 文本还有长度上限。如果输入与已生成内容之和超过上下文窗口,较早的内容会被截断。

步骤二:嵌入——给每个 token 一组“语义坐标”

做什么:用编号到词表中取出对应的向量,得到一个二维矩阵,形状为 [序列长度, 向量维度]。

为什么这么做:语义接近的词,向量也应更接近。此时“猫”和“狗”的向量比较接近,“猫”和“汽车”则相差较远,模型才能靠距离和角度判断语义关系。

怎么做:嵌入本质上是一张巨大的查表。示意过程如下:

  • 词表大小约为 5 万;
  • 向量维度为 512;
  • 编号 35127 取出的是嵌入表中第 35127 行,共 512 个数。

示意伪代码:

  1. input_ids = [101, 20048, 6, 35127, 7]
  2. embedding_table = 随机初始化的矩阵,形状为 [50000, 512]
  3. x = embedding_table[input_ids] # 结果形状:[5, 512]

还需要位置信息:只靠嵌入表,模型无法区分“我打你”和“你打我”,因为这两个词的向量完全相同,交换顺序后输入不变。因此模型会额外加入位置信息,常见做法有:

  • 正弦余弦式位置编码:用不同频率的正弦、余弦函数生成位置向量;
  • 可学习的位置嵌入:与词嵌入一样,把位置编号查表得到向量。

以可学习位置嵌入为例:

  1. positions = [0, 1, 2, 3, 4]
  2. position_table = 随机初始化的矩阵,形状为 [最大长度, 512]
  3. x = embedding_table[input_ids] + position_table[positions]

注意事项:位置嵌入的表格长度决定了模型支持的最大上下文长度,超出部分没有对应的位置向量。

步骤三:注意力机制——模型如何在句子内部“回看”

做什么:让每个位置的 token 根据与前文各 token 的相关程度,把前文信息按权重汇总到自己的表示中。

为什么这么做:理解一个词,往往离不开上下文。例如下面两句话:

  • 句子一:“小李把书放在桌上,因为它很旧。”
  • 句子二:“小李把书放在桌上,因为他很忙。”

代词“它”指向“书”,“他”指向“小李”。模型要做出正确的下一个词预测,就必须把“它”与“书”联系起来。

怎么做:可以把注意力理解成句子内部的一次“小型搜索”:

  • 当前 token 发出查询(Query):“我需要什么样的信息?”
  • 每个 token 持有键(Key):类似内容的标签,用于被匹配;
  • 每个 token 还持有值(Value):真正携带的信息。

查询与各键逐一匹配得到相关性分数,分数越高,对应的值被采纳得越多,最终混合成一个新的表示。其中查询、键、值都由同一份输入表示,再分别乘以三个可学习的权重矩阵得到,这种设计称为“自注意力”。

其经典公式为:

Attention(Q, K, V) = softmax(QKᵀ / √d_k) V

式中 d_k 是键向量的维度。示意伪代码如下:

  1. def self_attention(x, Wq, Wk, Wv, Wv_value, d_k):
  2. Q = x × Wq # 形状:[序列长度, d_k]
  3. K = x × Wk # 形状:[序列长度, d_k]
  4. V = x × Wv_value # 形状:[序列长度, d_k]
  5. scores = Q × K的转置 / d_k的平方根 # 形状:[序列长度, 序列长度]
  6. scores = 施加因果掩码(scores) # 上三角区域置为负无穷
  7. weights = softmax(scores, 按行计算) # 每一行的和为 1
  8. return weights × V # 形状:[序列长度, d_k]

以句子一中的“它”为例:

  • 它与“书”的相关性较高,权重约为 0.6;
  • 与“旧”的相关性次之,权重约为 0.3;
  • 与“小李”“放”“上”“桌”“因为”的相关性较低,合计约 0.1。

于是“它”的新表示中,“书”所携带的信息占了大头。

三个关键细节:

  1. 除以 √d_k 是为了稳定数值:维度越高,两个随机向量的点积波动越大,分数容易过大。Softmax 一旦进入饱和区,梯度会趋近于零,训练就会停滞。
  2. 因果掩码保证“不偷看未来”:生成式模型只能依据前文预测下文。掩码会把当前 token 右侧所有位置的分数设为负无穷,经过 Softmax 后权重变为 0。
  3. 多头注意力:上述过程并行做多组,每组使用独立的权重矩阵,称为一个“头”。不同头可能分别关注指代关系、句法搭配或固定搭配。各组结果拼接后再做一次线性变换,得到一个表示。

注意事项:下三角的对角线也保留在掩码内,允许每个 token 关注自身。实际训练中使用的因果掩码形式因实现而异,不影响基本原理。

步骤四:堆叠多层与残差连接——从字面到语境

做什么:把“自注意力 + 前馈网络”组合成一个层,再堆叠数十层乃至上百层。

为什么这么做:

  • 前馈网络对每个位置单独做非线性变换,负责加工和存储知识。经验上,模型中大部分参数集中在这一部分。
  • 残差连接把输入直接加到输出上,让深层网络的梯度能够顺畅回传。
  • 层归一化稳定每一层的数值分布,使训练更快收敛。

示意伪代码如下:

  1. for 每一层:
  2. h = x + 自注意力(层归一化(x))
  3. x = h + 前馈网络(层归一化(h))

直观理解:浅层更多捕捉局部搭配,深层逐渐整合句子乃至段落层面的信息,最终得到的表示已经“读懂了上下文”。

注意事项:实现细节各异,有的在子层之前归一化,有的在残差之后归一化。理解原理时不必纠结先后顺序,关键是“注意力负责交流信息,前馈负责加工信息,残差负责传递信息”。

步骤五:输出概率与采样——让模型“说出”下一个词

做什么:取出最后一个位置的表示,映射到整个词表,得到每个 token 的打分(logits),再转换成概率分布并采样。

为什么这么做:模型的输出不是某个确定的词,而是一个覆盖全词表的概率分布。如何通过采样把分布变成文本,直接决定了生成结果的风格。

示意伪代码:

  1. last_vector = 最后一层输出[-1] # 形状:[向量维度]
  2. logits = 输出投影(last_vector) # 形状:[词表大小]
  3. probs = softmax(logits / temperature)
  4. next_id = 按概率分布采样(probs, top_k, top_p)

常用的采样控制项及其作用:

  • 温度(temperature):小于 1 时分布变陡,输出更确定;大于 1 时分布变平,输出更随机。
  • Top-k:只保留概率最高的 k 个候选,其余全部排除。
  • Top-p(核采样):按概率从高到低累加,取累计概率达到 p 的最小候选集合。
  • 重复惩罚:降低已出现 token 的概率,缓解啰嗦重复。
  • 结束标志:一旦采样到结束 token,说明模型认为内容已完成,应停止生成。

步骤六:自回归循环——一个词一个词地生成

做什么:把刚生成的 token 追加到输入末尾,重新前向计算,预测下一个 token,如此循环直到结束或达到长度上限。

为什么这么做:这正是自回归的含义,即依据自身此前的输出继续生成。因此一句话往往是一步步“写”出来的。

示意的完整流程如下:

  1. def generate(prompt, max_new_tokens):
  2. ids = 分词(prompt) # 依赖步骤一
  3. for _ in range(max_new_tokens):
  4. logits = model_forward(ids) # 依赖步骤二至步骤五
  5. next_id = 采样(logits)
  6. if next_id == 结束标志:
  7. break
  8. ids.append(next_id)
  9. return 反分词(ids) # 还原为文本

注意事项:有两点需要特别留意:

  1. 朴素实现每生成一个词,都要把全部历史重新计算一遍,开销随长度快速增长。
  2. 应用层反复调用模型服务时,如果每次都从原始提示词重新计算,相当于重复支付同一段前缀的计算成本。

二者都可以通过缓存历史计算结果来缓解,详见第六部分。

五、结果验证:如何确认自己真的理解对了

1. 分词是否正确

  • 可还原:把编号通过词表反查回文本,结果应与原文完全一致,包括空格和标点。
  • 粒度合理:常用词尽量保持完整,很少被切碎。如果“机器学习”总是被拆成“机器”和“学习”,说明词表与模型不匹配,需要检查。

2. 嵌入是否正确

  • 形状对齐:输出形状应为 [序列长度, 向量维度]。
  • 语义相近:随机抽取若干词,检查语义相近的词是否拥有更接近的向量。

3. 注意力是否正确

  • 权重矩阵行和为 1:每个位置对前文的关注权重之和应为 1,允许 10⁻⁶ 级别的浮点误差。
  • 因果性成立:位置 i 对位置 j 的权重,在 j 大于 i 时应为 0。
  • 关键位置占比更高:在“小李把书放在桌上,因为它很旧”中,“它”对“书”的权重应明显高于对无关词的权重。

4. 整体生成是否合理

  • 续写流畅:提示词输入“床前明月光,疑是地上”,模型应续写出“霜”一类的字。
  • 风格可调:把温度调低,同一提示词的输出更稳定;调高则更具变化。

六、常见问题与排查

1. 输出中出现无意义的生僻字

原因:模型输入或采样参数不当,例如温度过高,导致低概率 token 被选中。

排查:检查输入的文本编码是否正确、分词是否与模型配套、采样参数是否过于激进。可将温度调低至接近 0.7 或更低,重新生成后对比。

2. 输出重复、陷入死循环

原因:某些短语一旦生成,模型便倾向于继续生成同样的内容。

排查:启用重复惩罚,并设置生成长度上限。必要时结合结束标志或停止词强制中断。

3. 记不住前面的长文本

原因:超出上下文窗口,较早的内容被截断。

排查:统计输入加已生成内容的 token 总数,精简提示词,或对长文先做分段摘要再处理。

4. 答非所问、前后矛盾

原因:这是统计预测的本质表现。模型追求的是“看起来合理的下一个词”,而不是事实正确。

排查:在提示词中明确角色和输出格式,给出少量示例,并借助外部知识检索为模型提供依据。事实类信息仍应由使用者自行核验。

5. 生成速度越来越慢

原因:朴素实现每步都要重复计算全部历史。

排查:确认推理侧是否启用了键值缓存,并检查请求是否携带了无关的历史对话。

6. 批量请求相互干扰

原因:不同请求长度差异较大,被填充到相同长度后一起计算,造成算力浪费,或掩码实现有误。

排查:按长度分桶,并重点检查掩码是否正确隔离了每个请求各自的历史。

七、优化建议

1. 性能与成本

  • 启用键值缓存:缓存每个位置已算出的键和值,生成新 token 时只计算增量部分。前缀相同的请求还可复用同一份缓存。
  • 模型压缩:通过量化、剪枝、蒸馏等手段减小模型体积,以少量精度换取更低延迟和成本。
  • 控制长度:用更简洁的提示词和摘要代替长篇原文,直接减少计算量。
  • 缓存常见结果:把问题到回答的映射缓存起来,重复请求无需再次计算。
  • 流式输出:首字返回更快,用户体验更好,也便于随时中断。

2. 安全与稳定

  • 限制输入长度,过滤异常字符。
  • 防范提示注入,不把系统指令直接拼接在用户输入之后。
  • 对输出做敏感内容审核和格式校验。
  • 对日志做脱敏处理,不记录完整原文。

3. 可维护性

  • 提示词模板、模型参数、分词规则都纳入版本管理,便于复现和回滚。
  • 固定随机种子,使同一输入在同一参数下的输出可复现。
  • 建立评测集,把“续写是否流畅”“摘要是否覆盖要点”等指标纳入例行回归。

4. 接入云端模型服务时的通用事项

  • 身份认证:通过密钥或访问令牌完成身份认证,并妥善保管,不写入代码仓库。
  • 配额与超时:为调用设置合理的配额、超时和重试策略,防止异常时反复重试放大故障。
  • 日志监控:接入日志服务与监控告警,跟踪调用量、延迟、错误率等通用指标,发现异常及时处理。

八、总结

本教程沿着“文本 → 编号 → 向量 → 注意力表示 → 概率分布 → 文本”的链路,拆解了大模型“预测下一个词”的完整过程:

  • 分词与嵌入:解决模型“认字”的问题;
  • 位置编码:解决模型“分清顺序”的问题;
  • 注意力:解决模型“回看前文、锁定重点”的问题;
  • 多层堆叠:解决模型“从字面到语境逐层加工”的问题;
  • 概率采样:解决模型“如何落笔成文”的问题。

理解了这条链路,模型的许多日常表现都能得到解释:它为什么会“一本正经地胡说”、为什么换个温度风格就不同、为什么长对话会“失忆”、为什么推理慢且花费高。

后续可以继续关注的方向包括:

  • 通过人类反馈或偏好对齐等方式,让模型回答更符合预期;
  • 借助检索增强为模型提供外部知识;
  • 围绕特定任务进行微调;
  • 通过量化、蒸馏等手段进一步降低成本。

对大多数应用开发者来说,理解原理的最终目的不是从零训练模型,而是知道每一步可能产生什么问题、该在哪里调整。先读懂原理,再去做接入和优化,往往能少走很多弯路。