简介:本教程以科普加动手实践的方式,带读者拆解大模型“预测下一个词”的全过程,包括分词与嵌入、注意力机制、多层堆叠、概率采样,以及用缩放点积、因果掩码、行和为1等校验方法确认每一步都正确。学完后,读者既能理解模型为何会“答非所问”,也能掌握调节生成稳定性、排查生成异常和降低使用成本的通用方法。
很多技术文章讲大模型时,开篇就是长篇公式和推导。其实大模型最底层的目标相当朴素:根据前面的文字,预测下一个字。把这个任务做好,再配合足够的数据和参数规模,翻译、总结、写代码、推理、对话等能力就会逐渐出现。这类由规模扩大而显现的综合能力,通常被称为“涌现”。
本教程用尽量少的公式,配合示意性的伪代码,带你完成一次“原理级”的小实践:
学完之后,你应该能够回答这些问题:模型为什么不认识汉字、注意力到底在算什么、同样的输入为什么会生成不同的回答、生成的文字突然重复或离题时应该从哪一步查起。
最终目标是走通一条从文字到文字的链路:输入文本 → 分词 → 嵌入 → 注意力计算 → 输出概率 → 采样下一个词 → 重复生成。链路中的每一步都能讲清“做了什么、为什么这么做、如何验证”。
大模型并不“理解”你在说什么,也没有先查资料再作答的过程。它学会的是一个统计规律:在见过的海量文本里,给定前文时,下一个词通常是什么。所谓“理解”,来自这种统计规律在海量数据上的极致压缩与泛化。
一段文本可以拆成一连串词元(token),模型学习的是每个位置上的条件概率。对于长度为 n 的文本,它出现的概率可以链式分解为:
P(文本) = P(第1个词) × P(第2个词 | 第1个词) × P(第3个词 | 前2个词) × … × P(第n个词 | 前 n-1 个词)
每一步都只做一件事:看过前面所有词,给出下一个词的概率分布。
做什么:把输入文本切成一串 token,再按词表把每个 token 映射为整数编号。
为什么这么做:计算机底层只能处理数字,分词是连接“人类语言”与“机器计算”的第一道关口。同时,分词结果决定了后续计算的粒度。
怎么做:现代大模型普遍不直接以单字或单词为单位,而是采用子词切分,常见做法包括字节对编码(BPE)等。它们的思路大致如下:
以一句话为例,示意切分结果为:
“机器学习 很 有趣 。” → [101, 20048, 6, 35127, 7]
其中“机器学习”整体是一个 token,“很”“有趣”以及句号各自是一个 token。
注意事项:
做什么:用编号到词表中取出对应的向量,得到一个二维矩阵,形状为 [序列长度, 向量维度]。
为什么这么做:语义接近的词,向量也应更接近。此时“猫”和“狗”的向量比较接近,“猫”和“汽车”则相差较远,模型才能靠距离和角度判断语义关系。
怎么做:嵌入本质上是一张巨大的查表。示意过程如下:
示意伪代码:
input_ids = [101, 20048, 6, 35127, 7]embedding_table = 随机初始化的矩阵,形状为 [50000, 512]x = embedding_table[input_ids] # 结果形状:[5, 512]
还需要位置信息:只靠嵌入表,模型无法区分“我打你”和“你打我”,因为这两个词的向量完全相同,交换顺序后输入不变。因此模型会额外加入位置信息,常见做法有:
以可学习位置嵌入为例:
positions = [0, 1, 2, 3, 4]position_table = 随机初始化的矩阵,形状为 [最大长度, 512]x = embedding_table[input_ids] + position_table[positions]
注意事项:位置嵌入的表格长度决定了模型支持的最大上下文长度,超出部分没有对应的位置向量。
做什么:让每个位置的 token 根据与前文各 token 的相关程度,把前文信息按权重汇总到自己的表示中。
为什么这么做:理解一个词,往往离不开上下文。例如下面两句话:
代词“它”指向“书”,“他”指向“小李”。模型要做出正确的下一个词预测,就必须把“它”与“书”联系起来。
怎么做:可以把注意力理解成句子内部的一次“小型搜索”:
查询与各键逐一匹配得到相关性分数,分数越高,对应的值被采纳得越多,最终混合成一个新的表示。其中查询、键、值都由同一份输入表示,再分别乘以三个可学习的权重矩阵得到,这种设计称为“自注意力”。
其经典公式为:
Attention(Q, K, V) = softmax(QKᵀ / √d_k) V
式中 d_k 是键向量的维度。示意伪代码如下:
def self_attention(x, Wq, Wk, Wv, Wv_value, d_k):Q = x × Wq # 形状:[序列长度, d_k]K = x × Wk # 形状:[序列长度, d_k]V = x × Wv_value # 形状:[序列长度, d_k]scores = Q × K的转置 / d_k的平方根 # 形状:[序列长度, 序列长度]scores = 施加因果掩码(scores) # 上三角区域置为负无穷weights = softmax(scores, 按行计算) # 每一行的和为 1return weights × V # 形状:[序列长度, d_k]
以句子一中的“它”为例:
于是“它”的新表示中,“书”所携带的信息占了大头。
三个关键细节:
注意事项:下三角的对角线也保留在掩码内,允许每个 token 关注自身。实际训练中使用的因果掩码形式因实现而异,不影响基本原理。
做什么:把“自注意力 + 前馈网络”组合成一个层,再堆叠数十层乃至上百层。
为什么这么做:
示意伪代码如下:
for 每一层:h = x + 自注意力(层归一化(x))x = h + 前馈网络(层归一化(h))
直观理解:浅层更多捕捉局部搭配,深层逐渐整合句子乃至段落层面的信息,最终得到的表示已经“读懂了上下文”。
注意事项:实现细节各异,有的在子层之前归一化,有的在残差之后归一化。理解原理时不必纠结先后顺序,关键是“注意力负责交流信息,前馈负责加工信息,残差负责传递信息”。
做什么:取出最后一个位置的表示,映射到整个词表,得到每个 token 的打分(logits),再转换成概率分布并采样。
为什么这么做:模型的输出不是某个确定的词,而是一个覆盖全词表的概率分布。如何通过采样把分布变成文本,直接决定了生成结果的风格。
示意伪代码:
last_vector = 最后一层输出[-1] # 形状:[向量维度]logits = 输出投影(last_vector) # 形状:[词表大小]probs = softmax(logits / temperature)next_id = 按概率分布采样(probs, top_k, top_p)
常用的采样控制项及其作用:
做什么:把刚生成的 token 追加到输入末尾,重新前向计算,预测下一个 token,如此循环直到结束或达到长度上限。
为什么这么做:这正是自回归的含义,即依据自身此前的输出继续生成。因此一句话往往是一步步“写”出来的。
示意的完整流程如下:
def generate(prompt, max_new_tokens):ids = 分词(prompt) # 依赖步骤一for _ in range(max_new_tokens):logits = model_forward(ids) # 依赖步骤二至步骤五next_id = 采样(logits)if next_id == 结束标志:breakids.append(next_id)return 反分词(ids) # 还原为文本
注意事项:有两点需要特别留意:
二者都可以通过缓存历史计算结果来缓解,详见第六部分。
[序列长度, 向量维度]。原因:模型输入或采样参数不当,例如温度过高,导致低概率 token 被选中。
排查:检查输入的文本编码是否正确、分词是否与模型配套、采样参数是否过于激进。可将温度调低至接近 0.7 或更低,重新生成后对比。
原因:某些短语一旦生成,模型便倾向于继续生成同样的内容。
排查:启用重复惩罚,并设置生成长度上限。必要时结合结束标志或停止词强制中断。
原因:超出上下文窗口,较早的内容被截断。
排查:统计输入加已生成内容的 token 总数,精简提示词,或对长文先做分段摘要再处理。
原因:这是统计预测的本质表现。模型追求的是“看起来合理的下一个词”,而不是事实正确。
排查:在提示词中明确角色和输出格式,给出少量示例,并借助外部知识检索为模型提供依据。事实类信息仍应由使用者自行核验。
原因:朴素实现每步都要重复计算全部历史。
排查:确认推理侧是否启用了键值缓存,并检查请求是否携带了无关的历史对话。
原因:不同请求长度差异较大,被填充到相同长度后一起计算,造成算力浪费,或掩码实现有误。
排查:按长度分桶,并重点检查掩码是否正确隔离了每个请求各自的历史。
本教程沿着“文本 → 编号 → 向量 → 注意力表示 → 概率分布 → 文本”的链路,拆解了大模型“预测下一个词”的完整过程:
理解了这条链路,模型的许多日常表现都能得到解释:它为什么会“一本正经地胡说”、为什么换个温度风格就不同、为什么长对话会“失忆”、为什么推理慢且花费高。
后续可以继续关注的方向包括:
对大多数应用开发者来说,理解原理的最终目的不是从零训练模型,而是知道每一步可能产生什么问题、该在哪里调整。先读懂原理,再去做接入和优化,往往能少走很多弯路。