跳到正文
Elaine Blog
返回

大模型怎样学会回答:训练、指令微调与生成策略

更新于:
LLM 基础

我们已经知道,Transformer 可以把一句输入变成下一个 Token 的分数。但是上一部分的小模型用的是随机参数,无论公式多么完整,都不会自然知道“质量问题”和“运费承担”有什么关系。

这些关系需要从数据中学习。模型先做出预测,再根据预测与训练目标的差异调整参数。反复训练后,原本随机的 Embedding、Attention 和 FFN 才逐渐形成能够处理语言的表示。

这也带来几个经常混在一起的问题:预训练到底教了什么,聊天模型为何会遵循要求,微调与提供资料有什么区别,为什么同一个问题还可能得到不同回答?它们分别发生在参数学习和生成选择的不同阶段。本文沿一条小训练样本,把这些阶段连接起来。

一句话怎样提供多个训练目标

先假设“商家”“承担”“运费”各自都是一个教学 Token,并加上开始、结束标记:

完整序列:<BOS> 商家 承担 运费 <EOS>
输入位置:<BOS> 商家 承担 运费
预测目标: 商家  承担 运费 <EOS>

输入与目标错开一个位置。模型读到 <BOS> 时预测“商家”,读到“商家”时预测“承担”,读到“承担”时预测“运费”。它不是只在句末获得一次反馈,每个有效位置都可以提供学习信号。

训练时整段文本已经存在,因此所有位置可以放进一个批次计算。因果 mask 保证预测“承担”的位置不能读取后面的“承担”和“运费”。矩阵运算虽然并行执行,信息范围仍然受到约束。

这种训练常称为使用真实前缀:预测第三个 Token 时,前面的输入来自训练文本,而不是模型刚刚猜出的第二个 Token。生成时则必须把自己选出的 Token 接回去,因此早期错误可能继续影响后面。例如先错误生成“买家”,后续会在这个前缀上续写,可能让整段错误更连贯。

框架通常让你传完整序列和标签,再在模型内部完成移位;也有代码要求你手动移位。不能两边都做一次,否则模型学习的就可能是跳过一个位置的目标。理解这一点,比记住某段训练代码里的切片更重要。

交叉熵怎样告诉模型“这次预测差在哪里”

假设某个位置的候选只有“商家”“买家”“未知”,当前概率是 [0.2, 0.5, 0.3],训练目标是“商家”。常见损失是正确目标概率的负对数:

L = −log p(商家 | 当前前缀)

当正确概率为 0.2,损失约为 1.609;提高到 0.8,损失约为 0.223。正确 Token 得到更高概率,损失就更低。它不是简单记录“猜对或猜错”,还区分一个模型给正确答案 0.01 和 0.49 的情况。

虽然公式只写正确目标的概率,其他候选仍然参与了竞争,因为 Softmax 分母包括全部候选。提高一个候选的 logit 会改变整个分布。对交叉熵和 Softmax 的组合,一个 logit 的梯度可以写成 pⱼ − yⱼ,其中正确目标对应的 y 为 1,其余为 0。

对上面这组概率,梯度是 [-0.8, 0.5, 0.3]。梯度下降用参数减去学习率乘梯度,因此正确候选的分数倾向于上升,另外两个倾向于下降。这是一个局部导数解释,真实训练更新的是产生 logits 的模型参数,而不是为每个输入永久保存一张独立分数表。

一段文本的损失通常把多个有效位置汇总。Padding 不应充当真实目标;SFT 中也常只对助手回答计算损失。若不区分有效位置,长短不同的样本或大量填充就可能改变统计含义。

困惑度可以理解为平均交叉熵取指数,是语言建模的一种指标。平均损失更低说明在这组目标文本上分配了更高概率,但并不直接等于售后问答更可靠。把一篇错误但流畅的文章作为目标,模型也可以通过学会复述它降低损失。

梯度怎样传回 Embedding、Attention 和 FFN

输出概率依赖 logits,logits 依赖最终隐藏状态,隐藏状态依赖前面的每一层。反向传播沿这条计算图应用链式法则,计算每个参数改变一点会怎样影响损失。

可以把一个简单链条写成 w → h → z → L。想知道 w 对 L 的影响,就把各段局部变化关系组合起来。深度学习框架记录张量操作并自动求导,开发者不需要手写整个 Transformer 的导数,但必须保证损失连接到希望训练的参数。

优化器随后使用梯度更新参数。普通 SGD 可以写成 w_new = w_old − η × gradient,η 是学习率。学习率太小,更新慢;太大,可能越过合适区域甚至不稳定。Adam 一类优化器还维护梯度的一阶、二阶统计,因此训练内存除了权重与激活,还包含优化器状态等内容。

一个 batch 把多个样本的信号合起来。例如有的样本要求“无理由退货由买家承担”,有的要求“核实质量问题后由商家承担”,模型需要学习条件差异,而不能只把“退货”都接到“商家”。数据分布、标注质量与训练目标决定它收到什么反馈。

下面用一个极小的下一 Token 分类器展示一次参数更新。输入只是人工构造的二维特征,模型只有线性层;它没有 Transformer,也不是微调真实语言模型。下载:training_step.py下载

import torch
from torch import nn

# 用零初始化让第一次分布易于手算:三个候选的概率均为 1/3。
# 二维输入是教学特征,不是人为定义的真实语义坐标。
features = torch.tensor([[1.0, 0.0]])
layer = nn.Linear(2, 3, bias=False)
nn.init.zeros_(layer.weight)
optimizer = torch.optim.SGD(layer.parameters(), lr=0.3)
target = torch.tensor([0], dtype=torch.long)  # 第 0 类是本例正确目标“商家”。

logits = layer(features)
# CrossEntropyLoss 内部处理 log-softmax,输入应为原始 logits,不先做 softmax。
loss = nn.CrossEntropyLoss()(logits, target)
print("更新前概率:", logits.detach().softmax(dim=-1))
print("更新前损失:", loss.item())

# 清理旧梯度 → 反向传播 → 参数更新;本例只有一次更新,不宣称已经收敛。
optimizer.zero_grad()
loss.backward()
print("权重梯度:", layer.weight.grad)
optimizer.step()
with torch.no_grad():
    print("更新后概率:", layer(features).softmax(dim=-1))

按这组零初始化数据推导,更新前概率都是 1/3,损失为 log(3)。更新后输入所对应的三个 logits 为 [0.2, -0.1, -0.1],正确候选概率提高到约 0.403。这些是给定初始化和学习率下的公式推导值,不代表真实语言模型训练的收益。

第二个输入维度是 0,因此这次更新不会为第二列权重提供信号。这也说明,训练只会从实际参与计算的数据和目标中获得反馈,缺失的事实不会因为模型参数多就自动进入训练。

预训练学续写,为什么后来能回答问题

预训练从大量文本中学习下一 Token 的条件分布。为了降低损失,模型有机会学到词语搭配、句法、知识关联和部分推理模式。它们都体现在可用于预测的参数与表示里,不是把每一篇网页原样放进一个可逐项查询的数据库。

例如语料反复出现“巴黎是法国的首都”,训练会提高相关上下文中“巴黎”或“法国”等续写的概率。但模型能输出这句话,不代表它保存了可靠的来源指针;对罕见知识、冲突资料或新近事实,它可能缺少稳定依据。

基础模型只学习续写时,遇到“请解释运费责任”,可能续写成另一段问题、论坛讨论或标准答案。聊天模型需要更多训练,学习如何在角色结构中完成用户任务,而不是单纯模仿任意文本的后续样式。

指令微调,也常称 SFT,使用“指令与上下文 → 期望回答”的样本继续训练。例如输入给出虚构政策,目标回答先确认期限,再说明需要核实质量问题。损失仍可以是下一 Token 交叉熵,但数据组织和计算损失的位置改变了。

若只训练助手部分,用户问题作为上下文参与前向计算,却不要求模型把用户的每句话复述成目标。标签 mask 只决定哪些位置贡献损失,不等于把这些位置从 Attention 输入里删除。一个位置可以参与理解,却不被当作需要预测评分的答案部分。

这时还要区分“正确格式”与“事实正确”。如果训练样本总是自信作答,缺少证据不足的示范,模型可能学会在不确定时仍写得肯定。补充“资料缺失时说明缺口”的样本,是让行为与任务要求更一致,不是赋予模型一个绝不会错的事实检测器。

偏好优化怎样让两个都通顺的回答分出高下

对同一个售后问题,两段回答都可能语法正确:A 直接说“商家承担”;B 说“按给定政策,仍需核实是否属于质量问题”。当证据未齐时,人工标注可能更偏好 B。

偏好数据常组织成同一个输入下的优选回答与较差回答。基于奖励模型的训练路线可以先学习一个评分器,再用强化学习方法优化生成策略,并通过约束避免偏离参考模型过远。评分器学习的是标注偏好,不能把它输出的高分直接当成已核实事实。

DPO 则利用成对偏好直接训练策略模型,通过比较优选与较差回答相对参考模型的对数概率来构造目标,不需要在这个优化步骤中另跑传统的奖励模型加策略采样循环。它仍依赖偏好数据、参考关系和优化设置,并没有消除数据偏差。DPO 论文解释了这一路线。

如果标注者总偏好更长的回答,模型可能学到“写长更容易得分”;如果数据忽略条件,模型也可能变得更自信而非更准确。因此“经过对齐”描述的是训练过程,实际任务仍要观察证据使用与错误类型。

一些任务还可以用可验证结果作为反馈,例如程序是否通过给定检查、数学答案是否满足条件。反馈能约束结果的某些方面,但覆盖不全的判定器也会遗漏问题。额外推理计算同样不保证每个答案都正确,不能把输出更长视为推理更可靠。

全量微调与 LoRA:改变参数的范围不同

全量微调允许更新全部或大部分模型权重。它提供较大的调整空间,也需要保存梯度和优化器状态,资源开销往往高于单纯推理。若只想让模型适应某种输出风格,可能没必要对所有矩阵自由更新。

LoRA 冻结原有权重 W₀,学习一个低秩增量。采用输入行向量约定时,可写成 W = W₀ + sAB,其中 A 为 d_in × r,B 为 r × d_out,r 比输入输出维度小,s 是缩放系数。对输入 x,输出成为 xW₀ + sxAB

假设原矩阵是 4096 × 4096,约 1678 万参数。若 r 为 8,两张小矩阵一共 4096×8 + 8×4096 = 65536 个参数,是该矩阵全量参数的约 1/256。这个比例只针对所适配的矩阵,不代表整个训练过程的内存自动缩小 256 倍:基础权重、激活与其他状态仍在。

可以把低秩更新理解为限制调整的自由度。一个大矩阵的任意变化有很多独立方向,小矩阵乘积只允许其中较少的组合方向。如果任务所需变化可以在这些方向中表达,就能用较少训练参数完成适配;若约束过强或数据不合适,效果仍会受限。LoRA 论文介绍了这个思路。

“低秩”也不等于“低精度”。LoRA 控制可训练增量的结构,量化控制数值表示精度。它们可以结合,但解决的是不同资源问题。

微调、Prompt、RAG 分别改变了什么

继续用店铺规则举例。把规则放在本次消息里,改变的是输入上下文;模型权重不因此更新。用许多规则问答继续训练,改变的是参数,可能让模型更习惯这种任务,但某条政策下周变更后,还需要考虑怎样更新与核实。

RAG 在回答前选择外部资料,再把它放入上下文。它可以提供更新的政策和来源,但检索可能找错、漏掉条件,生成也可能误读。微调可以改善如何使用资料,却不能替代资料版本与检索过程。

例如要让回答固定包含“依据、结论、缺少信息”,可以先用 Prompt 和示例;若大量任务持续需要某种行为,可再评估微调。要回答“今天这家店的退货政策”,通常首先需要获得今天的规则,而不是指望一次训练永久记住变化。

已经有概率,为什么还要设置 temperature 和 top-p

模型给出 logits 后,生成策略决定怎样选下一项。假设候选是“商家”“买家”“暂时”,logits 为 [2, 1, 0]。贪心解码每次选最高分,第一步会选“商家”。采样则按概率抽取,同一个输入可能走向不同后续。

温度 T 通常把 logits 除以 T 后再计算 Softmax。T 为 1 时概率约为 [0.665, 0.245, 0.090];T 为 0.5 时等效 logits 为 [4, 2, 0],分布约为 [0.867, 0.117, 0.016];T 为 2 时约为 [0.506, 0.307, 0.186]。小温度让分布更集中,大温度让分布更平缓。

T 必须大于零才能直接使用这条公式。接口中 temperature=0 的行为通常是特殊约定,不能在自己的程序里直接除以零。需要确定性选择时,可以显式关闭采样;硬件和服务实现差异也意味着不应把“关闭采样”宣传成跨环境逐字一致的保证。

Top-k 固定只保留分数最高的 k 项。Top-p 则按概率从高到低累计,保留累计概率达到 p 的最小候选集合,再重新归一化。例如 [0.665, 0.245, 0.090] 在 p=0.8 时保留前两项,因为第一项不足 0.8,前两项合计约 0.910;重新归一化后约为 [0.731, 0.269]

Top-p 不是删除“概率低于 p”的单项。若这样误写,0.8 会把上述候选全删掉。不同过滤策略一起启用时还要知道执行顺序,排查行为时可以先只改变一个因素。

下面直接计算分布,不加载模型,下载:sampling_walkthrough.py下载

import math

def probabilities(logits, temperature):
    # 温度必须为正;贪心选择应另外使用 argmax,而不是把温度设为零做除法。
    if temperature <= 0:
        raise ValueError("温度必须大于零")
    scaled = [x / temperature for x in logits]
    # 同时减去最大值保持分布不变,并减小指数溢出的风险。
    peak = max(scaled)
    masses = [math.exp(x - peak) for x in scaled]
    return [x / sum(masses) for x in masses]

def nucleus(probs, threshold):
    if not 0 < threshold <= 1:
        raise ValueError("top-p 必须位于 (0,1]")
    # 保留跨过阈值的那一项;至少有一个候选,不对单项概率设 threshold 门槛。
    chosen, total = [], 0.0
    for index in sorted(range(len(probs)), key=lambda i: probs[i], reverse=True):
        chosen.append(index)
        total += probs[index]
        if total >= threshold:
            break
    return {index: probs[index] / total for index in chosen}

logits = [2.0, 1.0, 0.0]
for temperature in [0.5, 1.0, 2.0]:
    print("温度:", temperature, "概率:", probabilities(logits, temperature))
print("top-p=0.8 后:", nucleus(probabilities(logits, 1.0), 0.8))

这些计算说明采样能改变选择范围,却没有增加任何事实。如果政策没有提供,低温度可能让模型稳定重复错误;高温度则可能让错误说法更多样。幻觉的一部分根源在于续写目标与事实核验之间的差距,不能仅靠调一个生成参数解决。

选出的 Token 会成为下一轮输入,直到结束标记、停止规则、长度上限或外部取消。长度上限截断的回答可能句子完整,也可能只有半个 JSON,因此应用必须观察结束状态,而不是仅凭“收到了一段文字”就当作成功。

现在可以把学习与使用分开:训练通过目标和梯度改变参数,推理根据当前上下文计算分布,解码策略选择具体续写。下一篇把这一切放回聊天窗口,解释上下文、KV Cache 与流式生成


分享这篇文章:

上一篇
Transformer 怎样处理一句话:从 Attention 到完整计算过程
下一篇
一次对话怎样运行:上下文、KV Cache 与流式生成