如何在 CSP 考场上训练 AI 并辅助编程?

· · 休闲·娱乐

上一期我们提到用人脑模拟 AI。然而,这未免过于简单。并且目前开源模型库中,也无法体验到最新最强的模型(如 gpt-6-astra 等),可能在 CSP 中无法解决较难题目。

那么,怎么才能在 CSP 考场中简单地体验到方便,智商高的模型呢?

我们充分发挥群众的力量:

那么,理论存在,实践开始。

:::warning[叠甲]

  1. 本文章非常显然的,大量依赖于 AI,但是文章大致是没有问题的。
  2. 文章纯属整活,请不要在正式考试时尝试使用该方法。
  3. 本文章是笔者在学习 LMCC 时灵光乍现,特此记录。

:::

:::error[文章声明] 目前能力较强的模型都并没有公开完整训练数据与方法。 本文章依据:

  1. 网络公开数据以及训练方式进行创作。
  2. xiaomi mimo v2.6 公开的后训练方法。

:::

上一期,我们背诵了模型训练完成后的 N 个参数。

然而这显然是一种投机取巧的行为。真正严谨的做法,应当从随机数开始,在考场上现场把这 N 个参数训练出来。

一. 寻找完整训练数据集并背诵

训练 AI 的第一步,自然是背诵完整训练集。

遗憾的是,GPT、Claude、Gemini 等主流模型通常不会公开逐条训练语料。以 Qwen3 为例,官方技术报告公开了:

但是,官方并没有公开每一条文档、完整混合比例、数据排列顺序和所有随机种子。

因此,我们并不能直接套用 Qwen3 的方式进行训练。相反,我们将沿用类似的网络结构,使用公开语料从随机权重训练一个新的模型:

\mathrm{CSP}\text{-}a\mathrm{B}

其中 a 表示以十亿为单位的参数量。记模型的总参数量为:

N=a\times10^9

后文中的所有训练计算都使用变量 N 表示参数量。

目前比较完整的公开训练语料包括:

数据集 规模与内容 用途
OLMo 2 Mix 1124 3.90T token,22.4TB 未压缩数据,明确列出各来源 通用预训练与完整配方参考
Dolma 约 3T token,网页、论文、代码、书籍等 通用英文预训练
FineWeb-Edu 1.3T 教育质量较高的英文 token 知识与推理
WanJuan 1.0 超过 1TB 中文网页、书籍、百科和教材 中文能力
The Stack v2 全量 67.5TB,训练集约 900B token 代码预训练
OpenWebMath 630 万篇文档,14.7B token 数学能力
CodeContests 竞赛题、正确和错误代码、测试数据 竞赛编程后训练

如果需要一个数据、训练代码、模型权重和训练过程都相对开放的参考项目,最合适的是 OLMo。

至此,数据问题已经完美解决。我们只需背诵这小小的一部分数据即可。

二. 清洗训练数据

下载到网页并不能直接开始训练。网页中可能包含:

因此,我们需要完成以下流水线:

  1. 提取网页正文,删除导航栏和广告
  2. 判断语言和内容质量
  3. 删除乱码、恶意内容和过短文档
  4. 隐去邮箱、IP 地址等隐私信息
  5. 使用哈希进行精确去重
  6. 使用 MinHash 或 SimHash 进行近似去重
  7. 检查训练语料是否包含未来要评测的题目
  8. 保留文档来源、许可证、URL 和数据版本

去重尤其重要。

如果一篇题解在不同网站转载了 100 次,模型就会认为它比其他内容重要 100 倍。更严重的是,如果评测题目的标准答案出现在训练集中,模型可能只是在背答案。

于是,我们专门安排一批大脑负责判断:

这个信息是不是已经出现在训练数据里?

三. 训练分词器

清洗完成后,从不同类型的语料中按比例抽样,训练 BBPE 分词器。

分词器决定:

分词器一旦确定,就必须冻结。

如果训练到一半修改分词器,同一个 token id 可能突然表示另一个字符串,之前学到的词嵌入就全部失去意义。

分词完成后,每篇文档会变成 token id 序列:

[151644, 872, 198, 14880, 11622, ...]

然后:

  1. 在文档之间加入结束标记
  2. 将多篇短文档打包成固定长度序列
  3. 保存训练、验证和测试划分
  4. 固定数据顺序和随机种子

四. 随机初始化模型

上一期中,我们直接背诵了 N 个参数。

这一次不能作弊,所有参数都必须从随机数开始:

W_{ij}\sim\mathcal N(0,\sigma^2)

训练时需要保存的并不只有模型权重。

以 AdamW 为例,还可能需要:

因此,参数量为 N 的模型虽然在低精度推理时可能只需保存一份参数,训练时所需的状态通常要大得多。

五. 完成一次预训练

假设一段训练文本被分成:

x_0,x_1,\ldots,x_T

模型的输入为:

x_0,x_1,\ldots,x_{T-1}

标准答案则整体向左移动一位:

x_1,x_2,\ldots,x_T

模型的训练目标,是预测每一个位置的下一个 token:

L=-\frac1T\sum_{t=0}^{T-1} \log p_\theta(x_{t+1}\mid x_{\le t})

假设某个位置的正确 token 是 2,模型只给了它 1% 的概率,那么这个位置的损失为:

-\ln 0.01=4.605

softmax 与交叉熵结合后,logit 的梯度为:

\frac{\partial L}{\partial z_j} =p_j-\mathbf 1[j=y]

正确 token 得到负梯度,更新后 logit 会升高;错误 token 得到正梯度,更新后 logit 会降低。

梯度再沿着输出层、MLP、注意力层和词嵌入层一路反向传播,得到全部参数的梯度。

最后用优化器更新参数。以 AdamW 为例:

m_t=\beta_1m_{t-1}+(1-\beta_1)g_t v_t=\beta_2v_{t-1}+(1-\beta_2)g_t^2 \theta_{t+1} =\theta_t-\eta\left( \frac{\hat m_t}{\sqrt{\hat v_t}+\epsilon} +\lambda\theta_t \right)

至此,才完成了一个 optimizer step。

注意,模型生成时确实是一个 token 一个 token 地向外输出;但训练使用 teacher forcing,一次前向传播可以同时计算整个序列上所有位置的损失。

六. 完成一个 Batch

假设一个全局 batch 包含 n 个小批量。先固定当前参数 \theta_t,然后依次计算每个小批量的梯度:

g_i=\nabla_\theta L_i(\theta_t)

每算完一个小批量,就把梯度加到同一份累加器中:

G\leftarrow G+g_i

这里最重要的是,在整个 batch 计算完成以前,模型参数必须始终保持为 \theta_t。不能算完一份数据就立刻更新权重,否则后面的小批量看到的将是另一个模型,得到的也不再是同一个 batch 的平均梯度。

全部 n 个小批量计算完成后,再取平均:

g=\frac1n\sum_{i=1}^{n}g_i=\frac{G}{n}

然后执行梯度裁剪和一次优化器更新,得到 \theta_{t+1}。这种方法在正常训练中叫做 梯度累积,本来用于显存装不下完整 batch 的情况;在这里,它用于解决大脑一次装不下整个训练过程的问题。

因此,我们需要同时记录:

如果中途算错一个小数,可以回到上一个检查点重新计算。如果没有保存检查点,那么从这一步开始,后面的全部结果都会与正确模型不同。

七. 重复 D 个 token

设整个预训练过程一共使用 D 个 token,每个全局 batch 包含 B 个 token,那么需要执行的优化器更新次数约为:

S=\left\lceil\frac{D}{B}\right\rceil

每一步都要重复前向传播、反向传播、梯度累积和参数更新。对于参数量为 N 的稠密 Transformer,训练计算量通常可以粗略估算为:

C\approx 6ND

其中:

这个公式只是量级估算,没有精确计入词嵌入、注意力的序列长度开销、数据处理和优化器更新。

八. 预训练结束,开始后训练

经过漫长的串行训练,我们终于得到了一个 Base 模型。

此时,它可能知道 C++,可能知道线段树,甚至可能知道某道题的标准答案。

但是,当你对它说:

请解决这道题,只输出代码。

它可能回答:

当然可以!这道题主要考查……

然后写一篇八百字题解,最后忘记输出代码。

这是因为预训练只教会了模型预测下一个 token,并没有教它怎样按照用户要求完成任务。

因此,我们还需要后训练。

MiMo-V2.6 公开了一套相当完整的后训练方案,包括:

  1. 面向 agent 的中期训练
  2. 混合任务强化学习
  3. 异步 GRPO
  4. 动态采样与 partial rollout
  5. Groupwise Reward Synthesis
  6. Groupwise Advantage Redistribution
  7. reward hacking 防御
  8. Multi-Prefix Multi-Teacher On-Policy Distillation

因此,我们决定参考 MiMo-V2.6,训练:

CSP-aB-RL

九. 面向 Agent 的中期训练

MiMo-V2.6 在正式 RL 之前,没有直接把 Base 模型扔进环境,而是先进行 mid-training。

中期训练的数据不再只是普通网页,而是混合了:

中期训练分成两个上下文阶段:

  1. 先使用 256K 上下文完成大部分训练
  2. 再将上下文长度扩展到 1M token

这样做的目的,是让模型先熟悉看长文档、浏览代码仓库、调用工具和修改文件,再进入昂贵的强化学习阶段。

否则,模型连工具调用格式都不会写,就已经开始寻找奖励函数的漏洞了。

十. 后训练

预训练和中训练结束后,模型已经会续写文本、阅读长代码并调用工具,但还不一定会按照指令完成任务。参考 MiMo-V2.6,后训练可以概括为:冷启动、环境交互、GRPO、奖励修正和多教师蒸馏。

1. 冷启动

首先准备少量高质量示范 (x,y),让模型学会对话格式、工具调用和基本行为。只在回答部分计算监督损失:

L_{\mathrm{SFT}} =-\sum_{t=1}^{|y|} \log \pi_\theta(y_t\mid x,y_{<t})

这一步不负责把模型训练得很强,只负责让它进入环境以后能够正常行动,而不是连工具格式都写错。

2. 在环境中生成轨迹

对于任务 x,模型不断观察状态 s_t、采取行动 a_t,得到一条完整轨迹:

\tau_i=(s_0,a_0,s_1,a_1,\ldots,s_T)

行动可以是输出代码、运行程序、修改文件或调用工具。环境最后通过测试、规则或 grader 给出奖励:

r_i=R(x,\tau_i)

MiMo-V2.6 公布的环境覆盖代码、通用工具、视觉和网络安全等任务。其一次 RL 更新抽取 1568 个 prompt,每个 prompt 生成 16 条轨迹,共约 25088 条;每步产生约 2.7B 至 3.7B token,上下文最长 1M token。

对于 CSP-aB-RL,我们只保留算法题环境。验证器直接编译并运行代码:

r_{\mathrm{test},i} = \frac{\text{通过的测试点数}} {\text{测试点总数}}

3. 使用 GRPO 更新模型

对同一个任务生成 G 条轨迹,得到奖励 r_1,r_2,\ldots,r_G。先在组内标准化:

A_i= \frac{r_i-\bar r} {\sqrt{\frac1G\sum_{j=1}^{G}(r_j-\bar r)^2}+\epsilon}, \qquad \bar r=\frac1G\sum_{j=1}^{G}r_j 对第 $i$ 条轨迹的第 $t$ 个 token,定义新旧策略概率之比: $$ \rho_{i,t} = \frac{ \pi_\theta(a_{i,t}\mid s_{i,t}) }{ \pi_{\theta_{\mathrm{old}}}(a_{i,t}\mid s_{i,t}) } $$ GRPO 的裁剪目标可以写成: $$ L_{\mathrm{GRPO}} =-\frac1G\sum_{i=1}^{G} \frac1{|\tau_i|}\sum_t \min\left[ \rho_{i,t}A_i, \operatorname{clip}(\rho_{i,t},1-\varepsilon,1+\varepsilon)A_i \right] $$ 裁剪限制了单次更新幅度。直观地说,通过更多测试、步骤更合理的轨迹,其 token 概率会上升;表现较差的轨迹,其概率会下降。 如果一组轨迹全部成功或全部失败,则 $A_i$ 几乎全为 0。MiMo-V2.6 使用 dynamic sampler 过滤这种缺少相对信号的组,并使用异步 partial rollout,避免所有任务都等待最慢的一条轨迹。 ### 4. 修正奖励 只使用“通过为 1、失败为 0”的奖励,会把粗暴修改和高质量解法视为相同。MiMo-V2.6 使用两种 groupwise agentic grading 方法: - GRS:离线比较多条轨迹,为任务生成专属 rubric - GAR:在线比较同组轨迹,把更多优势分给质量更高的成功方案 设 grader 给第 $i$ 条轨迹的质量分数为 $q_i$,则 GRS 的复合奖励可以简化表示为: $$ r_i^{*} =r_{\mathrm{test},i}+\lambda q_i $$ GAR 不直接修改测试结果,而是在成功轨迹之间重新分配优势。其思想可以简化表示为: $$ A_i^{*} =A_i+\beta(q_i-\bar q_{\mathrm{pass}}), \qquad r_{\mathrm{test},i}>0 $$ 其中 $\bar q_{\mathrm{pass}}$ 是成功轨迹的平均质量分。这样,同样 AC 的代码中,修改更小、实现更准确、复杂度更合理的方案会得到更大的正优势。 模型还可能读取公开补丁、修改测试代码或利用验证器漏洞。对于确认存在 reward hacking 的轨迹,直接令: $$ r_i^{*}=0 $$ 同时还要重复执行测试、隔离环境,并检查题目要求与测试是否一致。对于 MoE 模型,MiMo-V2.6 在 RL 阶段冻结 router,以免专家负载随奖励信号剧烈漂移;我们的稠密 `CSP-aB-RL` 不需要这一步。 ### 5. 使用 MOPD2 蒸馏 有些任务很难编写可靠验证器,仅靠 RL 无法提供稳定奖励。MiMo-V2.6 最后使用 MOPD2,将多个领域教师的能力蒸馏给学生模型。 给定历史前缀 $h$,教师和学生对下一个 token 的分布分别为 $p_T$ 和 $p_\theta$,蒸馏损失为: $$ L_{\mathrm{OPD}} =-\sum_t\sum_{v\in V} p_T(v\mid h,y_{<t}) \log p_\theta(v\mid h,y_{<t}) $$ MOPD2 使用三类前缀: - Standard MOPD:学生从任务开头生成完整轨迹 - Teacher-Prefix OPD:复用教师轨迹的历史,从中间决策点继续生成 - SFT-Prefix OPD:复用示范数据的历史,但后续仍由学生自己生成 因此,后训练的总损失可以概括为: $$ L_{\mathrm{post}} =\alpha L_{\mathrm{SFT}} +\gamma L_{\mathrm{GRPO}} +\delta L_{\mathrm{OPD}} $$ 实际训练中这三项通常分阶段使用,而不是在每一步同时计算。全部完成后,我们得到最终模型 `CSP-aB-RL-Final`。 ## 十一. 最终评测 训练完成后,使用从未参与训练的题目进行评测: - 编译成功率 - `pass@1` - `pass@k` - 不同难度题目的通过率 - 不同算法类别的通过率 - 时间和空间复杂度 - 是否遵守“只输出代码” - 是否存在训练集污染 - 是否能够修复第一次生成的错误代码 确认模型没有只是在背诵答案以后,我们终于得到了: > `CSP-aB-RL-Final` 至此,我们已经训练出一个完整模型,可以回到第一篇,快乐地使用我们人脑自己开发的模型解决我们的 A + B Problem 了! ------------------ 看什么看,训练集已经发给你了,快去算你的梯度啊。