大语言模型(LLM)训练的教师强制(Teacher Forcing)方法

大语言模型(LLM)在训练时使用一种名为"教师强制(Teacher Forcing)"的方法,而不是它们在推理(生成文本)时使用的"自回归(Autoregressive)"方法 。阐明关于LLM训练的一些常见问题,例如如何控制输出长度,以及为什么训练效率比推理更高。

训练 vs. 推理:Teacher Forcing vs. 自回归

区分大语言模型在两个不同阶段的运作方式:

  • 自回归(推理/生成阶段):这是LLM在生成文本(即推理)时使用的方法 。它以序列化的方式,逐个token地工作 。为了预测下一个token,模型会将它已经生成的token作为输入 。例如,为了写出"The quick brown fox",模型首先预测出"The",然后用"The"作为输入来预测"quick",再用"The quick"来预测"brown",以此类推。这个过程是天生的序列化过程,因此速度较慢。

  • Teacher Forcing(训练阶段):该方法用于模型的训练阶段 。在训练时,模型不会使用自己先前(可能不正确)的预测作为下一步的输入,而是始终被喂入正确、真实的答案(即"标签"或"ground truth")。为了预测序列中的每一个token,模型会接收来自训练数据中真实的前序token作为输入 。然后,模型会并行地计算出所有位置的输出,再将其预测的输出(如t2', t3', t4')与真实的后续token(如t2, t3, t4)进行比较,以计算损失(Loss)。

使用Teacher Forcing进行训练的主要优点是:

  • 高效率:因为模型不必等待前一个token生成后才能预测下一个,所有token的预测都可以并行计算 。这使得训练过程比自回归推理快得多,并能带来更高的GPU利用率 。
  • 长度控制:由于模型在训练时是"看着答案"来生成的,它被训练去生成一个与所提供的标签长度完全相同的输出,这直接解决了训练期间输出长度如何控制的问题 。

Teacher Forcing是一种有效的训练方法吗?

Teacher Forcing是一种科学上合理的方法,它并不会影响模型的学习目标 。通过数学推导证明了Teacher Forcing、最大似然估计(Maximum Likelihood Estimation, MLE)和交叉熵损失(Cross-Entropy Loss)这三者的优化目标是等价的 。

  • 最大似然估计(MLE) :模型训练的根本目标是最大化模型在给定输入x的条件下,生成正确token序列y的概率 。这可以表示为最大化整个序列的对数似然。

    • 一个序列的概率是其每个token在给定所有前序token的条件下的概率之积:pθ(y1:T∣x)=∏t=1Tpθ(yt∣y<t,x)p_{\theta}(y_{1:T}|x)=\prod_{t=1}^{T}p_{\theta}(y_{t}|y_{<t},x)pθ(y1:T∣x)=∏t=1Tpθ(yt∣y<t,x) 。
    • MLE的目标是最大化所有训练样本的对数概率之和:LMLE(θ)=∑∑log⁡pθ(yt∣y<t,x)\mathcal{L}{MLE}(\theta) = \sum \sum \log p{\theta}(y_{t}|y_{<t},x)LMLE(θ)=∑∑logpθ(yt∣y<t,x) 。
  • Teacher Forcing损失 :在Teacher Forcing中,一个序列的总损失是其每一个token位置上损失的总和 。在每一步t,损失是正确token yty_tyt 在给定真实前序token y<ty_{<t}y<t 条件下的负对数概率。

    • 总损失为:L=−∑t=1Tlog⁡pθ(yt∣y<t,x)L = -\sum_{t=1}^{T}\log p_{\theta}(y_{t}|y_{<t},x)L=−∑t=1Tlogpθ(yt∣y<t,x) 。
  • 交叉熵损失:此损失函数用于衡量两个概率分布之间的差异。在这里,它比较的是模型预测的下一个token的概率分布与真实的概率分布(一个one-hot向量,其中正确token的概率为1)。在整个序列上累加的交叉熵损失,在数学上与Teacher Forcing的损失是完全相同的 。

因此,得出了以下等价关系:
CrossEntropy=LTeacher−Forcing=−LMLE(θ)CrossEntropy = L_{Teacher-Forcing} = - \mathcal{L}_{MLE}(\theta)CrossEntropy=LTeacher−Forcing=−LMLE(θ)

这表明,使用Teacher Forcing来最小化交叉熵损失,等同于最大化训练数据的似然。

Teacher Forcing在代码中如何实现

使用Hugging Face的transformers库,提供了一个实践层面的代码讲解。

  1. 数据准备DataCollatorForLanguageModeling被用来准备数据 。它通过简单地复制inputs来创建用于计算损失的labels
  2. 前向传播与损失计算 :当模型执行其forward方法时,如果传入了labels参数,就会自动触发损失的计算 。
  3. 移位以实现"预测下一个token" :最关键的一步是,在损失函数(例如ForCausalLMLoss)内部,模型的输出(logits)和标签(labels)会被相互错开一位 。标签被移动,使得输入中位置i的token(input_ids)被用来预测标签中位置i+1的token 。这种错位正是"预测下一个token"这一任务的本质。
  4. 计算损失 :最终的损失是通过在模型的预测(logits)和移位后的正确标签(shifted labels)之间计算cross_entropy(交叉熵)得出的 。这个代码实现与Teacher Forcing的示意图完全对应,即模型在每个位置的输出都与真实序列中的下一个token进行比较。

我们跟随一个具体的例子,看看一句话是如何在训练流程中被处理的,以及每一步的数据细节是怎样的。

核心目标:教会模型"接话"

首先要理解,LLM训练的核心任务是预测下一个词(Next Token Prediction)

比如我们给模型一句话"天空是",我们希望它能预测出"蓝色"这个词。训练的本质就是给模型看海量的文本,让它反复练习这个"接话"的游戏,直到它做得非常好。

完整训练流程的详细拆解

让我们以一句话 "大模型爱学习" 为例,看看它在训练中经历了什么。

第1步:数据准备(输入与标签的制作)

在模型开始训练前,我们首先要准备好给它"吃"的数据。

  1. 文本分词 (Tokenization):计算机不认识汉字,只认识数字。所以第一步是把文本切分成最小的单元(Token),然后转换成对应的数字ID。

    • "大模型爱学习" -> อาจจะถูกตัดเป็น -> ["大", "模型", "爱", "学习"]
    • 然后,我们查阅一个预先制作好的词典(Tokenizer),将这些词转换为唯一的数字ID。
    • 假设词典是这样的:{"大": 10, "模型": 25, "爱": 33, "学习": 88}
    • 那么我们的输入数据就变成了 [10, 25, 33, 88]。这个在代码里通常被称为 input_ids
  2. 制作标签 (Labels) :这是最关键的一步。在Teacher Forcing训练模式下,标签(labels)就是输入(inputs)的一个完整复制品

    • 所以,我们现在有两份一模一样的数据:
      • inputs (输入): [10, 25, 33, 88]
      • labels (标签): [10, 25, 33, 88]

你可能会问:为什么输入和标签一样?别急,玄机在后面。

第2步:模型进行并行预测

现在我们把 inputs ([10, 25, 33, 88]) 送进大模型。

  • 与推理时一个一个词往外蹦不同,在训练时,模型会并行地处理整个输入序列 。这意味着它会同时计算出每一个位置的"下一个词预测"。
  • 模型的输出是一系列的logitsLogits可以理解为一个超长的列表,代表了模型对词典里每一个词的预测分数。分数越高,代表模型认为这个词是下一个词的可能性越大。
    • 当模型看到 10 ("大") 时,它会输出一个logits向量,这是对第二个词的预测。
    • 当模型看到 10, 25 ("大", "模型") 时,它会输出另一个logits向量,这是对第三个词的预测。
    • ...以此类推。

所以,输入是一个长度为4的序列,输出也是4组对应的logits预测。

第3步:关键操作 ---"移位",对齐预测和答案

这是整个流程中最巧妙、最核心的部分。现在我们手上有两样东西:

  1. 模型的预测 (Logits) :模型在每个位置上对 下一个 词的预测。
  2. 真实的答案 (Labels) :我们知道每个位置后面 应该 跟哪个词。

我们的目标是:用模型在位置 i 的预测,去对比位置 i+1 的真实答案。

例如:用模型看了"大"之后的预测,去和标准答案"模型"做对比。

为了实现这个目标,代码里会执行一个**移位(Shift)**操作 。

让我们把数据可视化:

输入 (Inputs) 模型看到的内容 模型的预测目标 预测输出 (Logits) 真实答案 (Labels)
10 ("大") "大" "模型" Logits_1 25 ("模型")
25 ("模型") "大", "模型" "爱" Logits_2 33 ("爱")
33 ("爱") "大", "模型", "爱" "学习" Logits_3 88 ("学习")
88 ("学习") "大", "模型", "爱", "学习" (无) Logits_4 (无)

如上表所示,我们需要比较的是"预测输出"和"真实答案"这两列。代码通过简单的数组切片就实现了这个对齐:

  • 取所有位置的预测Logits
  • 取所有位置的、向左移动一位的标签 :即从labels的第二个元素开始取 。
    • labels: [10, 25, 33, 88]
    • shifted_labels: [25, 33, 88]

这样,Logits_1 就会和 25 比较,Logits_2 就会和 33 比较,以此类推。这完美地实现了"预测下一个词"的训练目标。

第4步:计算损失 (Loss)

对齐之后,我们就在每个位置上计算损失。

  • 这个计算是通过交叉熵损失函数 (Cross-Entropy Loss) 完成的 。
  • 你可以把交叉熵理解为衡量"惊讶程度"的指标。
    • 在位置1,模型预测的 Logits_1 如果给正确答案 25 ("模型") 打了很高的分,那么损失就很小(模型不惊讶)。
    • 反之,如果模型给 25 打了很低的分,那么损失就很大(模型很惊讶,它猜错了)。
  • 最后,我们会把所有位置(位置1, 2, 3)的损失加起来,得到这个句子总的损失值 。
第5步:模型学习与更新

计算出的总损失值是一个数字,它告诉我们模型这次"考试"考得有多差。然后,通过反向传播算法,这个损失值会被用来微调模型内部亿万个参数。目的就是让模型下次再看到类似的输入时,能给出更接近正确答案的预测,从而让损失值变得更小。

这个过程会重复数万亿次,模型最终就学会了语言的规律。

总结一下完整流程:

  1. 准备数据 :将一句话"大模型爱学习"转换成数字ID,并复制一份作为标签。
    • inputs: [10, 25, 33, 88]
    • labels: [10, 25, 33, 88]
  2. 并行预测 :模型接收inputs,并行为每个位置输出一个对下一个词的预测(logits)。
  3. 移位对齐 :将labels向左移一位,使得模型的预测logits[i]与正确答案labels[i+1]对齐。
  4. 计算损失:在每个位置上,使用交叉熵比较模型的预测和正确答案,计算出损失。
  5. 累加损失:将所有位置的损失相加,得到总损失 。
  6. 更新模型:根据总损失,使用优化算法(如梯度下降)更新模型的内部参数,完成一次学习。
python 复制代码
import torch
import torch.nn as nn
from torch.optim import AdamW

# ==============================================================================
# 1. 初始化设置 (Setup & Initialization)
# ==============================================================================

# 假设我们有一个预训练好的大语言模型 (LLM) 和它的分词器 (Tokenizer)
# 在实际应用中,这些通常从Hugging Face加载
# model = AutoModelForCausalLM.from_pretrained("some-llm")
# tokenizer = AutoTokenizer.from_pretrained("some-llm")

# 为了演示,我们用模拟的组件代替
class MockTokenizer:
    def __init__(self):
        # 词典:将词映射到ID。-100是Hugging Face中常用的一个特殊值,
        # 用于在计算损失时忽略某些token 。我们后面会用到它。
        self.vocab = {"[PAD]": 0, "我": 1, "爱": 2, "学": 3, "习": 4, "大": 5, "模": 6, "型": 7}
        self.pad_token_id = 0
        self.ignore_index = -100

    def encode(self, text):
        return [self.vocab[char] for char in text]

class MockLLM(nn.Module):
    def __init__(self, vocab_size):
        super().__init__()
        # 模型的词嵌入层和输出头
        self.embedding = nn.Embedding(vocab_size, 768) # 假设隐藏层维度是768
        self.head = nn.Linear(768, vocab_size)

    def forward(self, input_ids, attention_mask=None):
        # 1. 输入的token ID通过嵌入层,变成向量表示
        embedded_vectors = self.embedding(input_ids) # (batch, seq_len) -> (batch, seq_len, hidden_dim)
        
        # 2. 经过模型主体(Transformer层),这里简化处理
        # 在真实模型中,这里会有一系列的Transformer Block
        transformer_output = embedded_vectors # 简化,实际有复杂计算
        
        # 3. 通过输出头,将向量表示转换回词典大小的logits
        logits = self.head(transformer_output) # (batch, seq_len, hidden_dim) -> (batch, seq_len, vocab_size)
        return logits

# 实例化我们的模拟组件
tokenizer = MockTokenizer()
model = MockLLM(vocab_size=len(tokenizer.vocab))
optimizer = AdamW(model.parameters(), lr=5e-5)

# ==============================================================================
# 2. 数据准备 (Data Preparation) - Teacher Forcing的关键起点
# ==============================================================================

# 我们的原始训练数据
sample_text = "我爱学习大模型"

# (2.1) 分词并将文本转为数字ID (Tokenization)
# input_ids: [1, 2, 3, 4, 5, 6, 7]
input_ids = tokenizer.encode(sample_text)

# (2.2) 转换为PyTorch张量(Tensor),并增加一个batch维度(批处理大小为1)
# 形状: [1, 7] (1个样本, 序列长度为7)
input_ids_tensor = torch.tensor([input_ids], dtype=torch.long)

# (2.3) 创建标签(labels)。这是核心步骤之一:
# "这个Label仅仅是input的复制" 。
# labels = inputs.clone() 
labels_tensor = input_ids_tensor.clone()

print(f"原始输入 (input_ids): {input_ids_tensor}")
print(f"原始标签 (labels):    {labels_tensor}")
print("-" * 30)

# ==============================================================================
# 3. 训练步骤 (A Single Training Step)
# ==============================================================================

# 清零之前的梯度
optimizer.zero_grad()

# (3.1) 前向传播 (Forward Pass)
# 将准备好的input_ids送入模型,得到logits
# logits的形状: [batch_size, sequence_length, vocab_size]
# 也就是 [1, 7, 8] -> (1个样本, 7个token, 每个token都有8个可能的分数,对应词典大小)
logits = model(input_ids=input_ids_tensor)

print(f"模型输出Logits的形状: {logits.shape}")
print("-" * 30)


# (3.2) 移位操作 (Shift) - Teacher Forcing的核心实现
# 这是反复强调的最关键细节。
# 我们的目标:用位置i的预测(logits),去和位置i+1的答案(labels)做对比。

# logits形状:  [1, 7, 8]
# labels形状:  [1, 7]

# 截取logits:我们不需要用最后一个token的输出来预测任何东西,所以去掉最后一个。
# 取从第0个到倒数第二个token的预测结果。
# shift_logits形状: [1, 6, 8]
shift_logits = logits[:, :-1, :]

# 截取labels:我们预测的目标是从第1个token开始,所以去掉第0个。
# shift_labels形状: [1, 6]
shift_labels = labels_tensor[:, 1:]

print("--- 移位操作 ---")
print(f"移位前Logits形状: {logits.shape}  (预测了7次)")
print(f"移位后Logits形状: {shift_logits.shape} (我们只关心前6次的预测)")
print(f"移位前Labels形状: {labels_tensor.shape} (7个真实答案)")
print(f"移位后Labels形状: {shift_labels.shape} (我们只关心后6个真实答案)")
print(f"移位后的标签内容: {shift_labels}")
print("-" * 30)


# (3.3) 计算损失 (Loss Calculation)
# 在计算损失前,通常会将数据展平(Flatten) 。
# 将 [batch_size, sequence_length, vocab_size] -> [(batch_size * sequence_length), vocab_size]
# 将 [batch_size, sequence_length] -> [(batch_size * sequence_length)]
# 这样就可以一次性计算所有token的损失。

# 展平后的logits形状: [6, 8]
flat_logits = shift_logits.reshape(-1, model.head.out_features)

# 展平后的labels形状: [6]
flat_labels = shift_labels.reshape(-1)

print("--- 展平操作 ---")
print(f"展平后Logits形状: {flat_logits.shape}")
print(f"展平后Labels形状: {flat_labels.shape}")
print("-" * 30)


# 使用交叉熵损失函数 。
# ignore_index告诉损失函数,如果label是-100,就不要计算这个位置的损失。
# 这在处理padding(为了让句子等长而填充的无意义token)时非常有用。
loss_function = nn.CrossEntropyLoss(ignore_index=tokenizer.ignore_index)

# loss_function会自动比较flat_logits (模型的预测分布) 和 flat_labels (正确答案的ID)。
# 它会计算出模型预测的"惊讶程度",即损失值。
loss = loss_function(flat_logits, flat_labels)


# (3.4) 反向传播 (Backward Pass)
# 根据计算出的损失值,自动计算模型中每个参数的梯度。
loss.backward()

# (3.5) 参数更新 (Optimizer Step)
# 指示优化器根据刚刚计算出的梯度,来更新模型的所有参数。
optimizer.step()

print(f"计算出的总损失 (Loss): {loss.item()}")
print("模型参数已更新!完成一次训练迭代。")
  1. Teacher Forcing的起点 :训练的inputslabels一开始是完全相同的副本 。
  2. 并行计算 :模型一次性处理整个输入序列,并行地为每个输入位置生成一个对"下一个词"的预测(logits)。这解释了为什么训练比推理效率高 。
  3. 核心是移位(Shift) :通过对logitslabels进行简单的切片(一个去掉结尾,一个去掉开头),我们就完美地创造出了"用前一个词预测后一个词"的训练任务,这正是Teacher Forcing的精髓 。
  4. 损失函数:最终使用标准的交叉熵损失函数 来衡量预测与真实答案之间的差距,并驱动模型的学习。
相关推荐
YUQI的博客4 分钟前
小白入门:通过手搓神经网络理解深度学习
人工智能·深度学习·神经网络
笑小枫19 分钟前
Pytorch使用GPU训练全过程,包含安装CUDA、cuDNN、PyTorch
人工智能·pytorch·python
Blossom.11836 分钟前
深度学习中的注意力机制:原理、应用与实践
人工智能·深度学习·神经网络·机器学习·生成对抗网络·计算机视觉·sklearn
飞哥数智坊1 小时前
Cursor替代方案整理,附模型不可用进阶解决方案
人工智能·claude·cursor
摸鱼仙人~1 小时前
现代人工智能综合分类:大模型时代的架构、模态与生态系统
人工智能·分类·数据挖掘
麻雀无能为力1 小时前
CAU数据挖掘第四章 分类问题
人工智能·分类·数据挖掘·中国农业大学计算机
mit6.8241 小时前
[AI-video] 数据模型与架构 | LLM集成
开发语言·人工智能·python·微服务
roman_日积跬步-终至千里1 小时前
【机器学习【6】】数据理解:数据导入、数据审查与数据可视化方法论
人工智能·机器学习
张较瘦_2 小时前
[论文阅读] 人工智能 + 软件工程 | 开源软件中的GenAI自白:开发者如何用、项目如何管、代码质量受何影响?
论文阅读·人工智能·软件工程
非ban必选2 小时前
spring-ai-alibaba之Rag 增强问答质量
java·人工智能·spring