AIGC入门,新手模型微调认知篇(四):训练过程与原理详解

AIGC入门,新手模型微调认知篇(四):训练过程与原理详解

新手模型微调认知篇(四):训练过程与原理详解

深入理解模型训练的内部机制,包括损失计算、权重更新和自回归生成。


Q1:SFTTrainer的内部是如何完成微调的?

A:SFTTrainertrl库提供的监督微调训练器,内部流程如下:

整体流程

输入数据(对话样本) ↓ 1. Tokenization(分词) ↓ 2. 前向传播(模型预测) ↓ 3. 计算损失(与标准答案对比) ↓ 4. 反向传播(计算梯度) ↓ 5. 更新权重(优化器) ↓ 重复步骤 1-5,直到训练完成

详细步骤

1. Tokenization(分词)

输入:

{"instruction":"什么是机器学习?","output":"机器学习是人工智能的一个分支..."}

格式化为 Llama-3.2 格式:

<|begin_of_text|><|start_header_id|>user<|end_header_id|> 什么是机器学习?<|eot_id|><|start_header_id|>assistant<|end_header_id|> 机器学习是人工智能的一个分支...<|eot_id|>

Tokenize:

input_ids=tokenizer.encode(formatted_text)# [128000, 128006, 882, 128007, 271, 3923, 374, ...]
2. 前向传播(模型预测)

输入:input_ids(token ID 序列)

模型处理:

input_ids ↓ Embedding 层(将 token ID 转换为向量) ↓ Transformer Layer 0 ↓ Transformer Layer 1 ↓ ... ↓ Transformer Layer 15 ↓ Output Layer(预测下一个 token 的概率分布)

输出:logits(每个位置对所有 vocab token 的预测分数)

logits.shape=(batch_size,seq_len,vocab_size)# 例如:(1, 100, 128256)# 表示:100 个位置,每个位置对 128256 个 token 的预测分数
3. 计算损失(Loss)

目标:让模型的预测尽可能接近标准答案

损失函数:Cross-Entropy Loss(交叉熵损失)

# 对于每个位置 ipredicted_logits=logits[i]# 模型对第 i 个位置的预测true_token=input_ids[i+1]# 实际的下一个 token# 计算交叉熵loss=-log(softmax(predicted_logits)[true_token])

直观理解:

标准答案的 token: "机器" 模型的预测概率: "机器": 0.3 → loss = -log(0.3) = 1.2 "学习": 0.5 → 如果预测 "学习" 则 loss = -log(0.5) = 0.69 "人工": 0.1 → 如果预测 "人工" 则 loss = -log(0.1) = 2.3 loss 越小 → 预测越接近标准答案

总损失:对所有位置求平均

total_loss=mean(loss_over_all_positions)
4. 反向传播(Backpropagation)

目标:计算每个参数对损失的贡献(梯度)

过程:

loss ↓ 链式法则(Chain Rule) ↓ 计算每个参数的梯度 ↓ gradient = ∂loss / ∂parameter

梯度含义:

gradient > 0 → 增大该参数会增大 loss → 应该减小该参数 gradient < 0 → 增大该参数会减小 loss → 应该增大该参数 gradient ≈ 0 → 该参数对 loss 影响很小
5. 更新权重(Optimizer)

优化器:AdamW(自适应学习率优化器)

更新公式:

parameter_new=parameter_old-learning_rate*gradient

AdamW 的特点:

  • 自适应学习率(每个参数有自己的学习率)
  • 动量(考虑历史梯度)
  • 权重衰减(防止过拟合)

完整示例

# 1. Tokenizationinput_ids=tokenizer.encode("你好,世界")# [128000, 57668, 53901, 382, 102616, 128009]# 2. 前向传播outputs=model(input_ids)logits=outputs.logits# (1, 6, 128256)# 3. 计算损失loss_fct=CrossEntropyLoss()shift_logits=logits[:,:-1,:]# 去掉最后一个位置shift_labels=input_ids[:,1:]# 去掉第一个 tokenloss=loss_fct(shift_logits.view(-1,128256),shift_labels.view(-1))# 4. 反向传播loss.backward()# 5. 更新权重optimizer.step()optimizer.zero_grad()

Q2:模型的回答与标准答案都是文本,是如何对比的?

A:不是直接对比文本,而是对比token 序列的概率分布

对比机制

标准答案:

"机器学习是人工智能的一个分支" → token IDs: [12345, 67890, 11111, 22222, 33333, 44444, 55555]

模型预测:

对于每个位置,模型输出一个概率分布(对 128256 个 token) 位置 0: 预测 "机器" 的概率: 0.3 预测 "学习" 的概率: 0.5 预测 "人工" 的概率: 0.1 ... 位置 1: 预测 "学习" 的概率: 0.4 预测 "是" 的概率: 0.3 ...

损失计算

对于每个位置:

# 标准答案的 tokentrue_token=12345# "机器"# 模型预测的概率分布predicted_probs=softmax(logits[position])# 标准答案 token 的概率true_prob=predicted_probs[true_token]# 例如 0.3# 损失 = -log(概率)loss=-log(true_prob)# -log(0.3) = 1.2

直观理解:

如果模型预测正确 token 的概率很高: true_prob = 0.9 → loss = -log(0.9) = 0.105 (很小) 如果模型预测正确 token 的概率很低: true_prob = 0.01 → loss = -log(0.01) = 4.6 (很大) loss 越小 → 预测越准确

为什么不是直接对比文本?

问题 1:文本对比太严格

标准答案: "机器学习是人工智能的一个分支" 模型回答: "机器学习是 AI 的一个分支" 文本对比: 完全不同 ❌ 语义对比: 几乎相同 ✅ 直接对比文本会忽略同义词、近义词

问题 2:语言模型的本质是概率预测

语言模型不"理解"语义 它只学习 token 序列的概率分布 训练目标: 让正确 token 的概率尽可能高 而不是让生成的文本和标准答案完全相同

问题 3:自回归生成的累积误差

生成 "机器学习是人工智能的一个分支" 位置 0: 预测 "机器" (正确) 位置 1: 预测 "学习" (正确) 位置 2: 预测 "是" (正确) 位置 3: 预测 "人工" (错误,预测了 "AI") 位置 4: 预测 "的" (基于 "AI" 继续预测) ... 一个错误会导致后续全部偏离 直接对比文本会过度惩罚这种累积误差

实际训练中的处理

Teacher Forcing:

训练时,不使用模型自己的预测,而是使用标准答案作为输入 标准答案: [A, B, C, D, E] 训练输入: [<start>, A, B, C, D] 训练目标: 预测 [A, B, C, D, E] 这样每个位置的预测都是独立的,不会累积误差

损失计算:

# 对于每个位置 iinput_token=input_ids[i]# 标准答案的第 i 个 tokentarget_token=input_ids[i+1]# 标准答案的第 i+1 个 token# 模型基于 input_token 预测 target_tokenpredicted_logits=model(input_token)loss=cross_entropy(predicted_logits,target_token)

Q3:模型是如何回答问题的?返回的各个字符是如何得到的?

A:模型使用自回归生成(Autoregressive Generation)

自回归生成原理

核心思想:每次只预测一个 token,然后将预测的 token 加入输入,再预测下一个。

示例:生成 “你好世界”

初始输入: "<|begin_of_text|>你好" 步骤 1: 输入: "<|begin_of_text|>你好" 模型预测下一个 token 的概率分布 选择概率最高的: "世" (概率 0.4) 步骤 2: 输入: "<|begin_of_text|>你好世" 模型预测下一个 token 的概率分布 选择概率最高的: "界" (概率 0.5) 步骤 3: 输入: "<|begin_of_text|>你好世界" 模型预测下一个 token 的概率分布 选择概率最高的: "<|eot_id|>" (概率 0.8) 生成结束: "你好世界"

详细流程

1. 准备输入
prompt="什么是机器学习?"# 格式化为 Llama-3.2 格式formatted=f"<|begin_of_text|><|start_header_id|>user<|end_header_id|>\n\n"formatted+=f"{prompt}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n"# Tokenizeinput_ids=tokenizer.encode(formatted)# [128000, 128006, 882, 128007, 271, 3923, 374, 128009, 128006, 78191, 128007, 271]
2. 自回归循环
generated=input_ids.copy()forstepinrange(max_new_tokens):# 模型预测outputs=model(generated)logits=outputs.logits[:,-1,:]# 取最后一个位置的预测# 采样策略(temperature, top_p, top_k)probs=softmax(logits/temperature)next_token=sample(probs,top_p=0.9,top_k=50)# 添加到生成序列generated.append(next_token)# 检查是否生成结束符ifnext_token==eos_token_id:break
3. 解码输出
# 去掉输入部分,只保留生成的部分generated_tokens=generated[len(input_ids):]# 解码为文本response=tokenizer.decode(generated_tokens,skip_special_tokens=True)print(response)# "机器学习是人工智能的一个分支,它让计算机能够从数据中学习..."

采样策略

问题:如何选择下一个 token?

策略 1:Greedy(贪心)

next_token=argmax(probs)# 总是选择概率最高的

优点:确定性强
缺点:容易生成重复、无聊的文本

策略 2:Temperature Sampling(温度采样)

# temperature < 1: 使分布更尖锐(更确定)# temperature > 1: 使分布更平坦(更随机)adjusted_probs=softmax(logits/temperature)next_token=sample(adjusted_probs)

策略 3:Top-p Sampling(核采样)

# 只从累积概率达到 p 的最小 token 集合中采样# 例如 top_p=0.9 → 只考虑累积概率前 90% 的 tokensorted_probs=sort(probs,descending=True)cumsum=cumsum(sorted_probs)cutoff=cumsum<top_p filtered_probs=sorted_probs*cutoff next_token=sample(filtered_probs)

策略 4:Top-k Sampling

# 只从概率最高的 k 个 token 中采样top_k_probs=topk(probs,k=50)next_token=sample(top_k_probs)

实际示例

# 配置生成参数generation_config={"max_new_tokens":256,"do_sample":True,"temperature":0.7,"top_p":0.9,"top_k":50}# 生成output=model.generate(input_ids,**generation_config)response=tokenizer.decode(output[0],skip_special_tokens=True)

Q4:当模型的回答与标准答案有差异时,微调是如何做的?

A:通过梯度下降(Gradient Descent)更新权重。

核心思想

不是直接"改成标准答案",而是:

  1. 计算当前预测与标准答案的差异(损失)
  2. 计算每个参数对损失的贡献(梯度)
  3. 沿着减小损失的方向调整参数

详细过程

1. 前向传播:得到预测
# 标准答案target="机器学习是人工智能的一个分支"target_ids=tokenizer.encode(target)# [12345, 67890, 11111, 22222, 33333, 44444, 55555]# 模型预测logits=model(input_ids)# logits.shape = (1, 7, 128256)# 对于每个位置,预测所有 128256 个 token 的分数
2. 计算损失:量化差异
loss=0foriinrange(len(target_ids)-1):predicted=logits[0,i,:]# 位置 i 的预测true_token=target_ids[i+1]# 位置 i+1 的真实 token# 交叉熵损失prob=softmax(predicted)[true_token]loss+=-log(prob)loss=loss/len(target_ids)# 例如 loss = 2.5(表示预测与真实答案有较大差异)
3. 反向传播:计算梯度
loss.backward()# 对于每个参数 W# 计算 ∂loss/∂W(梯度)# 梯度表示:如果增大 W,loss 会如何变化

梯度的含义:

参数 W[i,j] 的梯度 = +0.05 → 增大 W[i,j] 会使 loss 增大 0.05 → 应该减小 W[i,j] 来减小 loss 参数 W[i,j] 的梯度 = -0.03 → 增大 W[i,j] 会使 loss 减小 0.03 → 应该增大 W[i,j] 来减小 loss
4. 更新权重:梯度下降
learning_rate=0.0002# 对于每个参数 WW_new=W_old-learning_rate*gradient# 示例W_old[0,0]=-0.0179443359gradient[0,0]=-0.000441321W_new[0,0]=-0.0179443359-0.0002*(-0.000441321)=-0.0179443359+0.0000000883=-0.0179442476

更新幅度:

learning_rate = 0.0002(很小) gradient 通常也很小(< 1) 所以每次更新的幅度很小(约 0.01% ~ 1%) 这就是"微调"的含义: 不是大幅改变,而是微小调整
5. 重复训练
forepochinrange(num_epochs):forbatchindataloader:# 1. 前向传播logits=model(batch.input_ids)# 2. 计算损失loss=compute_loss(logits,batch.labels)# 3. 反向传播loss.backward()# 4. 更新权重optimizer.step()# 5. 清空梯度optimizer.zero_grad()

直观理解

类比:调整收音机旋钮

目标:调到某个频率(标准答案) 当前:偏离目标频率(模型的预测) 操作: 1. 听一下当前频率的清晰度(计算损失) 2. 判断应该往哪个方向调(计算梯度) 3. 微小地转动旋钮(更新权重) 4. 重复,直到清晰(损失最小)

为什么是"微调"而不是"大改"?

预训练模型已经学会了语言理解能力 微调只需要在特定任务上做微小调整 学习率很小(0.0002): → 每次更新只改变权重的 0.01% ~ 1% → 保留预训练模型的绝大部分能力 → 只调整与特定任务相关的部分

Q5:训练过程中loss是如何变化的?

A:loss通常会逐渐下降,但有波动。

典型的 loss 曲线

Loss 3.0 ┤● │ ● 2.5 ┤ ● │ ● 2.0 ┤ ● ● │ ● ● 1.5 ┤ ● ● ● │ ● ● 1.0 ┤ ● ● ● │ ● ● 0.5 ┤ └────────────────────────────────── 0 100 200 300 400 500 Training Steps

三个阶段

阶段 1:快速下降(0-100 步)

模型从"完全不懂"到"基本理解" loss 从 3.0 快速下降到 1.5 学习率逐渐增大(warmup)

阶段 2:缓慢下降(100-400 步)

模型从"基本理解"到"熟练掌握" loss 从 1.5 缓慢下降到 0.8 学习率达到峰值后逐渐衰减

阶段 3:收敛(400-500 步)

模型接近最优 loss 在 0.5 ~ 0.8 之间波动 学习率很小,更新幅度很小

为什么 loss 会波动?

原因 1:小 batch size

batch_size = 1 每个 batch 只有一个样本 不同样本的难度不同 → loss 波动大

原因 2:学习率调度

学习率按 cosine 调度 → 先增大(warmup) → 达到峰值 → 逐渐衰减到 0 学习率变化 → 更新幅度变化 → loss 波动

原因 3:数据分布

训练数据中: 简单样本: loss 低 困难样本: loss 高 不同 batch 包含不同难度的样本 → loss 波动

如何判断训练效果?

1. 观察训练 loss

✅ 正常: loss 逐渐下降,有小幅波动 ⚠️ 异常: loss 不下降或上升 ❌ 问题: loss 剧烈波动

2. 观察验证 loss

✅ 正常: 验证 loss 也逐渐下降 ⚠️ 过拟合: 训练 loss 下降,验证 loss 上升 ❌ 欠拟合: 训练 loss 和验证 loss 都很高

3. 对比 checkpoint

checkpoint-100: val_loss = 0.52 checkpoint-200: val_loss = 0.51 ← 最佳 checkpoint-300: val_loss = 0.53 ← 开始过拟合 checkpoint-400: val_loss = 0.55 选择 checkpoint-200 作为最终模型

小结

本节深入讲解了训练过程的核心机制:

  1. SFTTrainer 流程= Tokenization → 前向传播 → 计算损失 → 反向传播 → 更新权重
  2. 损失计算= 不是直接对比文本,而是对比 token 概率分布
  3. 自回归生成= 每次预测一个 token,逐步生成完整回答
  4. 权重更新= 通过梯度下降微小调整,不是直接"改成标准答案"
  5. Loss 变化= 通常逐渐下降,但有波动是正常的

下一节将深入讲解 LoRA 的原理和权重合并。