尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

【Bug已解决】Understanding loss in Training LLM 解决方案

【Bug已解决】Understanding loss in Training LLM 解决方案 【Bug已解决】Understanding loss in Training LLM 解决方案一、现象长什么样训练自己的 LLM用transformers的Trainer或自己写的训练循环时遇到一类「看不懂 loss」的问题loss 数值异常大比如 10、20怎么调学习率都下不来loss 看上去在降但模型生成全是乱码、复读验证集 loss 比训练集还低或者两边都不正常切换到不同tokenizer/ 不同 padding 策略后loss 量级突然变了但模型结构没动。最典型的复现把一批长短不一的样本 pad 到同一长度送进模型直接用input_ids当labels算交叉熵发现 loss 被 padding 位置严重拉高训练目标其实是「学会预测padding token」而不是「学会预测下一个真实 token」。这类问题不报错但训练出来的模型就是「不懂人话」——因为 loss 的含义从一开始就算错了。二、背景自回归 LLM 的训练目标是「给定前 i 个 token预测第 i1 个 token」。交叉熵 loss 对每个位置算一次再平均。关键点padding 位置不该参与 loss。transformers的model(**inputs)在传入labels时会自动对labels -100的位置跳过用ignore_index。但很多人这么写# 错误写法直接把 input_ids 当 labels outputs model(input_idsbatch, labelsinput_ids) loss outputs.loss如果 batch 里有 paddinginput_ids含pad_token_id那么 padding 位置也被当成「要预测的真实标签」模型被迫去预测pad_token_id这些位置的 loss 被算进平均。后果loss 被 padding 稀释/拉高短样本多的 batchpadding 占比大loss 大部分在「学 padding」真实语义信号被淹没。训练目标错位模型花大量精力拟合 padding生成时容易吐 padding 或复读。跨 tokenizer 不可比不同 tokenizer 的 pad 比例不同loss 量级跟着变你以为换了模型其实只是 pad 多了。正确理解 loss 的前提就是「让 padding 不参与 loss」。三、根因根因一句话训练时labels没有把 padding 位置标成ignore_index-100导致交叉熵把 padding 也当成要预测的目标loss 含义错误、训练目标错位。三点展开padding 参与计算labels input_ids让 pad 位置进入 lossignore_index没生效。平均基准错loss 平均的分母包含 padding 位置数真实 token 的梯度被稀释。3.缺校验没有在送入模型前断言labels里 padding 已被-100覆盖于是错误静默存在。不是模型不会学是「学什么」被 padding 污染了。四、最小可运行复现不依赖真实大模型用一个最小交叉熵演示 padding 如何污染 lossimport torch import torch.nn.functional as F vocab, seq 10, 6 pad_id 0 logits torch.randn(1, seq, vocab) # 模型输出未归一化 targets_raw torch.tensor([[1, 2, 3, pad_id, pad_id, pad_id]]) # 含 padding # 错误直接拿含 pad 的 target 算 loss loss_with_pad F.cross_entropy( logits.view(-1, vocab), targets_raw.view(-1) ) # 正确padding 标成 ignore_index-100 targets_masked targets_raw.clone() targets_masked[targets_raw pad_id] -100 loss_no_pad F.cross_entropy( logits.view(-1, vocab), targets_masked.view(-1), ignore_index-100 ) print(含 padding 的 loss:, round(loss_with_pad.item(), 4)) print(忽略 padding 的 loss:, round(loss_no_pad.item(), 4)) print(两者是否相同:, torch.isclose(loss_with_pad, loss_no_pad))跑出来含 padding 的 loss 把 3 个 pad 位置也学进去了数值和「只看真实 3 个 token」的 loss 明显不同pad 多时差异更大。这就是「loss 算错」的精确复现。五、解决方案第一层最小直接修复最小修复构造labels时把所有pad_token_id位置替换成-100再送进模型。import torch from transformers import AutoModelForCausalLM, AutoTokenizer tokenizer AutoTokenizer.from_pretrained(your-model) model AutoModelForCausalLM.from_pretrained(your-model) def make_labels(input_ids: torch.Tensor) - torch.Tensor: labels input_ids.clone() # 关键padding 位置标成 -100交叉熵忽略它 labels[labels tokenizer.pad_token_id] -100 return labels # 训练循环 for batch_input_ids in dataloader: labels make_labels(batch_input_ids) outputs model(input_idsbatch_input_ids, labelslabels) loss outputs.loss # 现在只统计真实 token loss.backward() optimizer.step() optimizer.zero_grad()如果做「下一 token 预测」且输入已经是「输入标签移位」的格式注意自回归模型内部会自己处理移位你只需保证labels里 padding 是-100不要把labels再做一次[:, 1:]移位那会和模型内部的 shift 重复。要点labels[labels pad_token_id] -100一行解决 padding 污染。model(..., labelslabels)内部用ignore_index-100自动跳过。loss 现在只反映「真实 token 的预测质量」量级和训练目标都正确。这一步单独就让 loss 回归正确含义。六、解决方案第二层结构性改进第一层是「在循环里加一行」。但训练脚本里多个数据路径SFT、预训练、带 mask 的指令数据都构造 labels容易漏。更稳的做法把「labels 如何正确屏蔽 padding / 特殊 token」收敛成单一策略对象。from dataclasses import dataclass, field from typing import List, Optional import torch dataclass class LlmLossAuditor: LLM 训练 loss 标签屏蔽的单一策略。 # 需要忽略的 token id 集合padding、特殊 token 等 ignore_ids: List[int] field(default_factorylist) # 是否同时忽略序列左侧prompt只学回答SFT 常用 train_on_completion_only: bool False # completion 起始标记SFT 用 response_start_id: Optional[int] None def build_labels(self, input_ids: torch.Tensor) - torch.Tensor: labels input_ids.clone() for ig in self.ignore_ids: labels[labels ig] -100 if self.train_on_completion_only and self.response_start_id is not None: # 找到每个样本里 response_start 的位置其之前全标 -100 mask (input_ids self.response_start_id) # 用 cumsumstart 之前为 0之后为 1 pos mask.cumsum(dim-1) labels[pos 0] -100 return labels def check(self, labels: torch.Tensor): # 防御整行全 -100 意味着该样本无监督信号 all_ignored (labels -100).all(dim-1) if all_ignored.any(): print(f[LlmLossAuditor] 警告: {int(all_ignored.sum())} 条样本整行被忽略) # 用法 auditor LlmLossAuditor(ignore_ids[tokenizer.pad_token_id, tokenizer.bos_token_id]) for ids in dataloader: labels auditor.build_labels(ids) auditor.check(labels) loss model(input_idsids, labelslabels).loss loss.backward(); optimizer.step(); optimizer.zero_grad()结构收益单一策略padding、特殊 token、SFT「只学回答」的屏蔽都集中在一处。-可校验check抓出「整行无监督」的废样本。可扩展加新的忽略规则只改LlmLossAuditor不动训练循环。七、解决方案第三层断言 / CI 守护写 pytest 守三条(1) padding 被标-100(2) 真实 token 不被误标(3) 计算出的 loss 与「仅真实 token」一致。import torch import torch.nn.functional as F import pytest from your_lib import LlmLossAuditor pytest.fixture def auditor(): return LlmLossAuditor(ignore_ids[0]) # 假设 pad_id0 def test_pad_masked_to_neg100(auditor): ids torch.tensor([[1, 2, 0, 0]]) labels auditor.build_labels(ids) assert labels[0, 2].item() -100 assert labels[0, 3].item() -100 def test_real_tokens_kept(auditor): ids torch.tensor([[1, 2, 0, 0]]) labels auditor.build_labels(ids) assert labels[0, 0].item() 1 assert labels[0, 1].item() 2 def test_loss_ignores_pad(): vocab, seq 10, 4 pad_id 0 logits torch.randn(1, seq, vocab) raw torch.tensor([[1, 2, pad_id, pad_id]]) masked raw.clone(); masked[raw pad_id] -100 l_pad F.cross_entropy(logits.view(-1, vocab), raw.view(-1)) l_mask F.cross_entropy(logits.view(-1, vocab), masked.view(-1), ignore_index-100) assert not torch.isclose(l_pad, l_mask), 含 pad 的 loss 应与忽略 pad 的不同 def test_completion_only_mode(): a LlmLossAuditor(ignore_ids[0], train_on_completion_onlyTrue, response_start_id5) ids torch.tensor([[1, 5, 6, 7]]) # 5 之后才是回答 labels a.build_labels(ids) assert labels[0, 0].item() -100 # prompt 部分忽略 assert labels[0, 1].item() -100 # response_start 本身可忽略 assert labels[0, 2].item() 6 # 回答部分保留CI 常驻跑这四条后任何「padding 又混进 loss」「真实 token 被误标」的回归都会立刻爆红。八、排查清单训练 LLM「loss 看不懂」时按顺序查先打印labels里pad_token_id是否还在——在就说明 padding 参与了 loss。确认labels是input_ids的克隆并做了-100替换而不是直接用input_ids。确认用的loss来自model(..., labelslabels).loss而不是自己手写的、没传ignore_index的F.cross_entropy。SFT 场景确认是否「只学回答」prompt 部分应标-100否则模型在学复述问题。确认ignore_index-100与模型内部一致transformers 默认就是 -100别改成别的。换 tokenizer 后重新核对pad_token_id不同 tokenizer 的 pad id 可能不同。批量打印几个样本的labels肉眼确认除 pad/特殊位外真实 token 都保留。九、小结「训练 LLM 但 loss 看不懂 / 模型学不会」的常见根子是labels没把 padding 标成ignore_index-100交叉熵把 padding 也当成学习目标loss 含义错位、训练目标被污染。修复三层次第一层构造 labels 时labels[labelspad_token_id] -100第二层用LlmLossAuditordataclass 把 padding/特殊 token/SFT「只学回答」的屏蔽收敛为单一策略并加整行忽略校验第三层用 pytest 守「padding 被标 -100」「真实 token 保留」「含 pad 与忽略 pad 的 loss 不同」「completion-only 正确」。工程启示自回归训练里「labels 怎么构造」决定了「模型学什么」。padding 必须-100、promptSFT 时必须-100、特殊 token 通常也要-100。任何训练脚本上线前先肉眼看一眼labels再训比训完发现模型废了再回头查省事得多。
返回列表