难负样本阶段被忽略的三个细节:为什么你的CLM打分总是不如官方
【免费下载链接】CLMContrastive Language Models (CLMs) are a new class of System One model trained with a contrastive learning objective that connects states and actions.项目地址: https://gitcode.com/gh_mirrors/clm2/CLM
CLM-8B 大概是最近一周 Agent 圈讨论密度最高的开源模型之一:它把 Agent 的动作决策从"生成式"整体改造成"状态-动作向量匹配",用冻结的 Qwen3-8B 编码器加两个约 20M 参数的投影头(projection head)做对比学习打分,在计算机使用、游戏与工具调用任务上与 Jev 持平的同时延迟最高降低约 9 倍;作为 verifier(best-of-N 选择器),轻量微调后在 Terminal-Bench 2.1 与 DeepSWE 上分别刷到 87.6% 和 81.6%,而 Jev 在这些长程任务上连 pass@1 都打不过。热度一高,复现的人就多了,随之而来的典型抱怨是:"我按文档微调/部署了,为什么打分分布、top-1 准确率和官方数字对不上?"
这个问题大多不是玄学,而是出在数据配方与评分协议上。本文不写口号,直接对照 README.md 的 Data Recipe 与仓库里的训练、评估、服务端源码,拆三件事:难负样本到底怎么构造的、正负样本配比与温度参数如何直接决定分布形态,以及从"打分不如官方"反推出来的三个最常被忽略的细节。
难负样本的构造方式:相似但错误,从合成到批内
CLM 的训练不是单一阶段。官方配方(README.md 的 Data Recipe 一节)是三段式:
- 预训练:约 6000 万条 Nemotron DQA 问答对,问题是 state、答案是 action,用双向 InfoNCE 学宽泛语义表示;
- 难负样本强化(mid-training):约 3000 万条合成难负样本,由 Gemini 2.5 Flash-Lite 生成——语义上与正确答案相似、但确实是错误的答案,被逐条加进 InfoNCE 损失;
- Agent 轨迹后训练:约 100 万条 ADP(Agent Data Protocol)轨迹步骤,外加 Endless-Terminals、LiteCoder-Terminal-SFT 的终端轨迹,每个轨迹步就是一个 (state, action) 对。
关键在第二阶段。难负样本不是简单地往 batch 里塞随机句,而是只作用于 state→action 这一个方向,且只针对当前正样本做归一化:
L_{s \rightarrow a}^{\mathrm{hard}}=-\frac{1}{B}\sum_i\log\frac{\exp\left(s_i^\top a_i / \tau\right)}{\exp\left(s_i^\top a_i / \tau\right)+\sum_k\exp\left(s_i^\top h_{ik}^{(a)} / \tau\right)}其中 $h_{ik}^{(a)}$ 是状态 $s_i$ 的第 $k$ 个难负样本动作。也就是说,模型被要求"在若干'看着都对'的错误答案里把正确的挑出来",这是 fine-grained discrimination 的来源——也是"为什么你的打分看起来钝"的第一个伏笔:如果你的评估集里没有这种相似但错误的干扰项,模型在分布层面的区分力根本无从体现。
更有意思的是训练时序。README 明确给出了一个反直觉的消融:在约 10 万条留出问题上(1 个标准答案 + 10 个难负样本),纯预训练不看任何难负样本就能到52.1%top-1;加一段短 mid-training 直接跳到69.2%;而"从一开始就带难负样本训练"虽然前期涨得快,峰值只有62.4%就会过拟合。换句话说,难负样本是预训练之上的"精修",而不是替代品——两阶段配方在同一预算下高出约 7 个点。如果你图省事把难负样本直接混进初始训练,或者跳过 mid-training 直接微调,你的打分能力天然就少了这 7 个点。
后训练阶段还有一个容易忽略的防遗忘设计:40% 的 Nemotron DQA replay + 60% Agent 轨迹。带 replay 时 Nemotron 难负样本 top-1 只从 69% 微降到 68.5%;而纯 Agent 数据训同样步数会掉到56.2%。很多复现者微调时把 replay 洗掉、只喂轨迹数据,打分在通用语义上就这样悄悄塌了。
正负样本配比与温度参数:分布形态的隐形开关
训练侧的"正负样本配比"由两件事决定:batch 大小(in-batch negative 数量)和分组掩码。train/finetune.py 的_clm_loss是核心:
state_z = F.normalize(state_head(states), dim=-1) action_z = F.normalize(action_head(actions), dim=-1) logits = logit_scale.exp().clamp(max=100.0) * state_z @ action_z.t() labels = torch.arange(len(states), device=states.device) same = codes.unsqueeze(0) == codes.unsqueeze(1) same.fill_diagonal_(False) forward = F.cross_entropy(logits.masked_fill(same, float("-inf")), labels) backward = F.cross_entropy(logits.t().masked_fill(same, float("-inf")), labels) return (forward + backward) / 2, logits, labelsclm 任务默认--batch 2048,即每个正样本面对约 2047 个批内负样本;codes按(task_id, step_idx)分组,同一轨迹内的状态-动作对彼此屏蔽,避免"作弊负样本"。这就是官方校准的环境:数千个随机负样本的检索任务。微调时--tasks-per-batch 4、--sampler random/task-blocked进一步控制批内任务构成。choice 任务默认--batch 256,且--loss可选infonce(批内双向)或softce(每个问题自己的候选上做 softmax)、--targets可选soft(标注者分布)/hard(gold label)——这些参数任何一个换掉,训练出的分布性质都不同。
温度参数是第二个开关。对比学习里的温度在仓库里表现为可训练参数logit_scale:train/finetune.py 初始化为log(1/0.07)(约 2.66),作为nn.Parameter参与训练,前向与服务端都 clamp 到上限 100(src/clm/heads.py 加载时同样exp().clamp(max=100))。而推理侧还有第二个温度:POST /v1/systemone的temperature字段默认 1,在 softmax 之前除logits(src/clm/engine.py):
answers[qid] = answer_from_logits(questions[qid], keys, (scale * cos / temperature).tolist())这意味着最终分布 =softmax(exp(logit_scale) * cos / temperature)。logit_scale 是在"千级负样本 + 3000 万难负样本"的语境下学出来的;把它套到只有 2~5 个候选的在线问题上,分布天然会更尖锐。仓库里的 T-Rex 实测最能说明"分数分布不等于决策质量":examples/t_rex/results/clm_realtime.json 与 examples/t_rex/results/jev_realtime.json 显示,同一个 Choice 问题(jump/duck/run 三选一),CLM 与物理规划器"最佳动作"的一致性中位数只有 0.658,Jev 高达 0.987,但两者都是 5/5 存活、0 死亡;CLM 的 p50 端到端延迟 16.5ms(模型部分 2.6ms),Jev 是 149.8ms(模型 131.9ms)。分布不同 ≠ 能力差,但如果你拿着 CLM 的概率分布去对照 Jev 的分布习惯,或者对照官方论文表格里的数字,就一定会得出"不如官方"的结论。
从"打分不如官方"反推:最常被忽略的三个细节
细节一:官方 verifier 打分用的是余弦相似度,不是 softmax 概率
这是最隐蔽、最致命的一条。CLM 的 API 返回的是 softmax 概率(answer_from_logits),但官方 best-of-N 评估 evaluation/bon_eval.py 根本不用 softmax:它逐轨迹步计算投影后向量的点积(L2 归一化后即余弦相似度),然后取末尾窗口的均值作为整条轨迹的分数:
zq = F.normalize(sh(se[i:i + chunk].float().to(device)), dim=-1) za = F.normalize(ah(ae[i:i + chunk].float().to(device)), dim=-1) out.append((zq * za).sum(-1).cpu()) # ... def aggregate(scores, window): tail = scores[-window:] return sum(tail) / len(tail)随后best_of_n按均值分选最优候选,平局按均匀期望处理。README 复现命令里也写得很清楚:--n 4 --window 12(每组 4 个候选、取末尾 12 步均值)。官方"打分"= 投影空间的余弦相似度时序均值,既没有 logit_scale 也没有 temperature,更没有 softmax。训练侧选 checkpoint 用的指标同样不是概率校准,而是within_task_top1/within_task_meanrank(train/finetune.py 的_evaluate_clm),即检索排序质量。所以:拿 API 的 probabilities 做阈值、做 best-of-N、或直接跟官方 81.6%/87.6% 对表,都是在拿两套不同的量纲硬比。想复现官方数字,请用bon_eval.py的余弦相似度聚合协议。
细节二:嵌入与文本配方必须逐字对齐,截断是最常见的隐性偏差
投影头绑定的是"文本如何变成 token"这件事。仓库把配方写死在 train/embed_utils.py 的Recipe里:状态文本走 Qwen3 的 chat template 且取尾(state_ids),动作/答案文本不加特殊 token 且取头(text_ids(keep="head")),choice 任务则全部取尾。选项文本"是什么就嵌什么"——src/clm/schema.py 明确注释候选不添加任何前缀,Noul 的默认候选是"Yes. This is true: …"/"No. This is false: …"这种模板句。任何一步改掉,嵌入就变了,打分就偏了。
更隐蔽的是截断不一致:训练侧--max-len默认 8192(README 的 DeepSWE 预计算),而 src/clm/server.py 的clm-serve默认--max-tokens 2048,超长状态会被截断后才送编码器;README 明确提示"states longer than 2048 tokens are truncated",要同时调大 vLLM 的--max-model-len和clm-serve --max-tokens。如果你评估的 Agent 轨迹动辄几千 token,服务端默认配置下你打的分和官方在 8K 上下文下训练/评估的分,根本不是同一批向量。此外 head 与编码器强绑定:"a head only makes sense with the encoder and pooling it was trained against"——Qwen3-8B last-token pooling。换了编码器、换了池化方式、或用了clm-raw(原始编码器空间的余弦,scale 固定 100),分数都没有可比性。
细节三:难负样本是"强化"而非"起点",候选集构成决定分数可比较性
把第一节的消融数字翻译成复现清单,就是三条硬约束:难负样本要在预训练之后加(不然 62.4% vs 69.2%);Agent 后训练必须保留 replay(不然 56.2% vs 68.5%);微调时要用官方命令——注意 README 给出的 DeepSWE 微调示例是--batch 512,不是默认的 2048,而学习率由规则2e-3 * sqrt(1024/width) * sqrt(batch/1024)随 batch 联动(train/finetune.py),改 batch 不改 lr 等于同时改了两个超参:
python train/finetune.py --task clm --init-ckpt "$(clm-download)" --out-dir runs/deepswe \ --holdout-tasks heads/deepswe/heldout_tasks.json --batch 512 python evaluation/bon_eval.py --hf-dataset Contrastive-LM/deepswe-clm-embeddings-8k \ --checkpoint heads/deepswe/best_head.pt \ --tasks-file heads/deepswe/heldout_tasks.json --n 4 --window 12更进一步,候选集构成本身就是评分协议的一部分:官方在"千级批内负样本 + 30M 合成难负样本"下校准了 logit_scale,评估时若你的候选全是易负样本(或只有正样本加随机句),分布会明显变钝;若候选全是极难负样本,top-1 会骤降但排序未必差。这与 examples/t_rex/README.md 里的做法一致——T-Rex 用物理规划器把每个动作标注为 Safe/Unsafe 再拼进 criteria 描述,等于人为构造了"语义相近但含正确性信息"的候选池。候选描述写得好不好、有没有标注安全信息,直接决定 CLM 在这组候选上的分布质量。
结语
"打分不如官方"的归因路径其实很清晰:先确认你比的是余弦相似度还是 softmax 概率(细节一),再确认嵌入配方、截断长度与池化方式是否逐字一致(细节二),最后确认难负样本的时序、replay 与 batch/lr 规则是否照着官方配方来(细节三)。三条都对齐了,CLM 才会给你它训练时所承诺的那份分数;对不齐,差的就不是"运气",而是协议。
【免费下载链接】CLMContrastive Language Models (CLMs) are a new class of System One model trained with a contrastive learning objective that connects states and actions.项目地址: https://gitcode.com/gh_mirrors/clm2/CLM
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考