垃圾模型输出故障排除 | AI生成和翻译
问题: 生成输出大部分是空白字符,偶尔出现 <div>、}、;、<、* 这样的 token。给定一个在脏乱的 GitHub 代码上训练的模型,这是预期的吗?
回答:
是的——根据你诊断中的所有信息,这正是你可以预测的失败模式。但这里存在两种不同的问题,只有一种是“预期/良性的”。让我将它们区分开。
预期的情况(良性)
空白字符以及 }、;、<div>、*、< 是你的语料库中频率最高的 token。 一个高困惑度(PPL ~32)的模型如果无法可靠地区分上下文,就会退化为无条件的一元分布——它只输出平均出现频率最高的内容,而忽略上下文。在 GitHub 代码中,这些就是:空格、换行、缩进、闭合括号、分号以及 HTML/javadoc 片段(<div> 直接来自你约 12% 的 HTML 污染)。因此,垃圾 token 的身份与“模型退回到先验分布”是一致的。这部分是预期的。
这与语言模型在训练不足时输出“the the the and of”的现象相同——它输出的是边际分布,而不是条件分布。
什么不是预期的——这才是真正的信号
看看你的张量形状:
x after transformer: (1, 3, 768) | logits: (1, 1, 50304)
两个危险信号:
1. 序列长度是 3。 你给模型输入了一个 3 token 的提示,transformer 的输出是 (1, 3, 768)。这本身没问题。但这意味着在生成过程中,你是在极短的上下文上操作的。如果你的生成循环没有将采样到的 token 追加回输入中(或者你的 KV cache / 上下文窗口坏了),模型就永远不会看到真正的前缀——它每一步几乎是在空上下文的基础上预测。在一个 1-3 token 的上下文中,即使是好的模型也会严重依赖先验分布。请验证你的自回归循环是否确实在增长上下文:
for _ in range(max_new):
idx_cond = idx[:, -block_size:] # 必须包含已生成的 token
logits, _ = model(idx_cond)
logits = logits[:, -1, :] / temperature # 只取最后一个位置
probs = F.softmax(logits, dim=-1)
next_id = torch.multinomial(probs, 1)
idx = torch.cat([idx, next_id], dim=1) # <-- 追加,否则你会从头重新生成
如果缺少那个 torch.cat 或者 idx_cond 设置错误,你就会看到这种现象:无论模型训练得有多好,都会无休止地生成无结构的高频 token。
2. logits: (1, 1, 50304) —— 词表大小是 50304,而不是 50257。 这是 GPT-2 的词表填充到 64 的倍数(nanoGPT 为了效率使用 50304)。如果训练时也使用了相同的填充词表,那就没问题。但值得确认的是,你的分词器和模型的 vocab_size 在训练和推理之间是否完全匹配——不匹配会偏移每个 token ID,并精确地产生这种“有效的 token,但无连贯性”的输出。事实上,生成的 token 仍然是合理的代码 token(不是随机的 Unicode),这暗示映射关系可能是正确的,但还是确认一下。
如何判断你遇到的是哪个问题(一个测试)
在指责数据之前,用一个长的、符合分布的前缀进行提示,并贪婪采样(temperature → 0,即 argmax):
prompt = "def quicksort(arr):\n if len(arr) <= 1:\n return arr\n pivot = "
# 编码,输入完整提示,argmax 解码 50 个 token
- 如果在真实代码前缀上进行贪婪解码能产生一定程度合理的后续内容(更多的缩进、一个标识符、
arr[)→ 模型学到了东西;你之前的垃圾输出是采样温度和短上下文导致的,剩下的修复是数据/分词器质量(你的诊断仍然成立)。 - 如果在长前缀上进行贪婪解码仍然输出空格和
<div>→ 模型确实没有学到条件结构,或者你的推理循环/词表坏了。考虑到训练损失从随机的 50000 PPL 基线下降到 3.46,模型确实学到了可测量到的东西,所以在好前缀下仍然输出纯垃圾将指向推理循环,而不是权重。
总结
垃圾的内容(空白字符、大括号、<div>)是语料库中出现频率最高的 token,这对于高困惑度的模型来说是预期的。但“PPL 为 32 时大部分是空白”也是从接近空上下文中采样或生成循环出错的经典症状,而你的 seq_len=3 让我想先排除这种情况——只需 5 分钟检查,且修复成本远低于重新 token 化 50GB 数据。
所以:先运行长前缀贪婪解码测试。如果还是垃圾,在碰数据之前,请检查 torch.cat 的追加操作以及训练/推理的 vocab_size 是否匹配。数据清洗仍然值得做——但不要让数据清洗掩盖一个可能的推理错误。