电子密度这个词,做计算化学的人天天见,但把它当成一门语言来建模,用 GPT 式的自回归架构去生成、补全和条件采样,是最近一段时间才真正跑通的路子。我手上这个模拟项目 X 做的就是这样一件事:把分子体系的电子密度场切块、量化、排成序列,喂给一个解码器-only 的 Transformer,让模型在给定原子坐标和元素组成的条件下逐块重建密度,再拿密度去做性质预测和反应性筛查。目前在我们内部测试集上,电子密度重建的余弦相似度中位数稳定在 0.98 以上,下游几个性质任务的误差也比纯坐标输入的基线低了一截。这篇就聊聊这套东西为什么值得做、具体怎么落地,以及我在数据构造和评估环节踩过的坑。
1. 电子密度加 GPT 到底解决什么问题:先别急着追 98%
1.1 电子密度为什么是化学研究里的“硬通货”
分子坐标只告诉你有几个原子、分别在哪里,电子密度告诉你的东西要多得多:电子在空间里怎么分布、哪些区域富集、哪些区域稀疏、成键区域和孤对区域的形状长什么样。密度泛函理论有一个很基础的结论,基态的可观测性质由电子密度唯一决定。换句话说,密度不是坐标的附属品,它本身就是一份信息量极高的“全息档案”。偶极矩是密度的一阶矩,静电势由密度直接积分得到,反应性位点往往对应密度梯度变化剧烈的区域,分子间作用力也离不开密度 tail 的行为。
问题在于,这份档案太贵了。做一次中等精度密度泛函计算,小分子几分钟到几十分钟,稍微大一点的体系动辄几小时,还得考虑构象采样。化学研究里真正想做的事——大规模虚拟筛选、反应路径扫描、材料配方搜索——都被这个计算成本卡住。用机器学习做密度代理模型,思路很自然:从坐标直接映射到密度,推理时一次前向就能出结果。但早期的纯回归方案有个通病,模型倾向于输出“平均密度”,高密度区域被抹平,低密度尾部被忽略,最后看起来 MAE 不大,可拿去做性质预测就露馅。
把密度当成序列来建模,解决的正是这个“平均化”问题。自回归模型学的是条件分布,不是单点均值。它可以在给定原子环境后,逐块决定下一块密度该长什么样,遇到多峰分布时不会简单取平均。这一点对过渡金属体系、激发态、强相关区域尤其重要,因为这些地方的密度往往不是单峰、对称、好猜的。
1.2 GPT 式序列建模切中的四个痛点
第一个痛点是可变尺寸。分子有大有小,密度网格从几十的三次方到几百的三次方不等,直接用 3D 卷积网络,最后总得池化到一个固定维度,尺寸一变大就难受。序列模型天然支持变长输入,块多了序列就长,块少了序列就短,结构上不用改。
第二个痛点是长程关联。密度场里,远离原子核的尾部区域看起来数值很小,但它对偶极矩、极化率、分子间作用有实打实的贡献。3D 卷积的感受野靠堆层数扩,成本高;注意力机制在序列上做全局关联更直接。我们把密度切成 patch 之后,一个 patch 和远处另一个 patch 的关系可以通过注意力直接建模,不需要一层层卷过去。
第三个痛点是生成和补全。化学研究里经常遇到“已知部分结构,想补全电子结构”的场景:比如只给了核心骨架,想看不同取代基的密度变化;或者做逆问题,从密度反推可能的局部环境。自回归框架天然支持遮罩补全和条件采样,这是纯回归头做不到的。
第四个痛点是预训练复用。密度序列和分子序列在形式上都是 token 序列,今天在小分子上预训练的主干,明天可以拿去做更大体系的继续训练,或者冻结一部分接下游任务。这个可迁移性,是纯坐标模型很难比的。
当然,序列化不是没有代价。3D 结构被压成 1D 顺序,旋转和平移不变性丢了,块与块之间的边界也可能不连续。这些问题后面都要用位置编码、几何条件和重建头去补。
1.3 98% 准确率的口径不说清楚就是自欺欺人
我看到“准确率 98%”这种说法,第一反应不是兴奋,而是先问:什么指标?在哪个分布上?怎么划分的?电子密度模型的“准确率”至少有三种常见口径,差别非常大。
| 指标口径 | 具体定义 | 我们模拟项目里的值 | 注意事项 |
|---|---|---|---|
| 体素级余弦相似度 | 预测密度与参考密度展平后的余弦相似度 | 中位数 0.983 | 背景零多,容易虚高,必须看加权版本 |
| 积分电荷误差 | 预测密度积分与真实电子数的相对误差 | 平均 0.4% | 比余弦更接近物理意义 |
| 下游性质准确率 | 偶极、gap、反应性分类等任务 | 分类任务 0.96 左右 | 跟密度重建不是一回事,别混着说 |
我们说的 98% 是体素级余弦相似度的中位数,而且是在同分布测试集上。换成按分子骨架划分的 OOD 测试集,这个数字会掉到 0.95 附近;如果只保留密度大于某个阈值的区域重新算,数字还会再变。所以我建议任何做这个方向的人,先把评估口径写死:用不用加权、截断阈值多少、按什么划分、随机种子几个。口径不统一,98% 和 95% 之间根本没有可比性。
还有一个容易被忽略的点:密度重建得好,不代表下游性质一定准。偶极矩对密度尾部敏感,你截断到 1e-4 和截断到 1e-6,余弦相似度可能只差零点几,偶极误差能差出一倍。所以我在项目里坚持把“密度指标”和“性质指标”分开报,绝不拿一个 98% 糊弄过去。
2. 密度场怎么变成模型能读的“句子”:表示方案与选型
2.1 三条密度表示路线的取舍
把 3D 密度送进模型,常见路线有三条,我一开始三条都试过,最后选了混合方案。
第一条是原始网格加 3D 卷积或 U-Net。优点是保真,密度场空间连续,卷积核天然适合局部特征。缺点是长程建模弱,序列化困难,而且不同大小的盒子要对齐,推理时显存吃紧。我们试过直接回归 128 的三次方网格,单样本前向就要几个 GB,训练 batch 只能开到 2,收敛极慢。
第二条是基组系数表示。把密度展开到一组基函数上,存系数而不是存网格。优点是紧凑,几十到几百个系数就能描述一个分子。缺点是依赖基组选择,不同元素、不同体系之间系数不对齐,跨体系迁移很难。而且系数和化学直觉之间的对应关系弱,做条件生成时不好加约束。
第三条是分块加向量量化,也就是把密度场切成小块,每块用一个学习到的码本向量表示,排成 token 序列。优点是序列长度可控、词表统一、能用标准 Transformer、支持生成和补全。缺点是量化误差和块边界不连续。我们的做法是用它做主干序列建模,再挂一个轻量重建头把 token 解回连续密度,用重建损失把量化误差压下去。这样既拿到了序列模型的灵活性,又不至于把密度精度丢掉。
| 路线 | 序列化难度 | 长程建模 | 保真度 | 跨体系迁移 | 适用场景 |
|---|---|---|---|---|---|
| 原始网格 + 3D CNN | 高 | 弱 | 高 | 一般 | 固定尺寸、小体系 |
| 基组系数 | 中 | 中 | 中 | 弱 | 单体系高精度拟合 |
| 分块 + 量化 token | 低 | 强 | 中高 | 强 | 生成、补全、大规模预训练 |
2.2 分块大小、网格步长与词表设计
分块大小不是拍脑袋定的。我们一开始用 1 Å 的 patch,序列长度直接爆掉:一个 20 Å 的盒子切成 20 的三次方,8000 个 token,注意力矩阵 8000 的平方,单层就要 256 MB 的分数矩阵,12 层根本跑不动。后来改成 2 Å 的 patch,序列长度降到 1000 左右,训练才稳定下来。
网格步长也要匹配。密度在原子核附近变化剧烈,步长太粗会丢核心区域;步长太细,patch 里的体素数暴涨。我们最后定在 0.2 Å,一个 2 Å 的 patch 包含 10 的三次方,也就是 1000 个体素。这个分辨率下,密度梯度的主要结构能保住,显存也还能接受。对于更精确的 core 区域,我们在重建头里做一次局部上采样,不在 token 层面硬堆分辨率。
词表设计上,除了密度码本,我们还放了原子类型 token、坐标分箱 token、以及几个特殊 token:序列开始、序列结束、遮罩、填充。原子类型用元素周期表前若干号元素的 embedding,坐标按 0.5 Å 分箱,避免连续值直接进离散词表造成信息损失。码本大小我们试过 256、512、1024,512 是性价比最高的点:再小重建明显模糊,再大码本利用率上不去,很多码字成了死码。
这里有个实操细节:码本不是一次性定死的。我们先用一个小型 3D 编码器在密度 patch 上做自监督重建,预训练出一个初始码本,再把它冻结,拿去做序列模型的 token 词表。如果码本和序列模型一起从零训,前期 token 分配极不稳定,loss 会来回跳。
2.3 3D 位置编码与几何条件怎么注入
序列化最大的损失是空间结构。同样一串密度 token,打乱顺序之后,模型应该能感知到“这不是原来的分子”。我们的做法是给每个 token 加一个可学习的 3D 位置编码:把 patch 中心坐标做傅里叶特征展开,过一个小 MLP,加到 token embedding 上。这样模型既知道 token 在序列里的顺序,也知道它在空间里的位置。
原子信息用条件 token 注入。具体做法是把每个原子的元素类型和坐标编码成若干个 prefix token,放在密度序列前面。解码器在生成每个密度 token 时,可以通过注意力回看这些原子 token。这样做的好处是条件信息不依赖序列顺序,原子加几个、减几个,prefix 长度跟着变就行。
旋转等变我们没有做严格约束。严格等变需要把注意力里的几何特征全部换成不变量,工程量大,而且会限制序列模型的表达。我们用的是数据增强加几何特征近似:训练时随机旋转整个体系,位置编码和坐标分箱跟着转;同时在注意力里加相对距离偏置,让模型对“两个 patch 离多远”有直接感知。实测下来,这种做法在 OOD 旋转测试上的误差比不加增强低 30% 左右,虽然比不上严格等变网络,但够用。
2.4 自回归与纯回归:为什么最后选了混合方案
纯回归头一次输出所有 patch 的密度,推理快,训练也简单。但它有两个硬伤:一是多峰分布会被平均掉,二是没法做生成和补全。我们试过纯回归,密度重建余弦能到 0.97,但偶极误差明显偏大,原因就是尾部被平均了。
纯自回归的问题是推理慢。1000 个 token,逐 token 采样要 1000 次前向,即使有 KV cache,也不适合大规模筛选。而且自回归有暴露偏差,训练时看真实前缀,推理时看自己生成的前缀,误差会累积。
最后我们做的是混合:训练阶段用自回归损失,让模型学会条件分布;推理阶段分两种模式。做密度重建时,用教师强制并行解码,一次前向出所有 token,快;做生成和补全时,用自回归采样,慢但灵活。这样既保留了生成能力,又保证了筛选场景的吞吐。代码结构上,解码器输出头同时接两个分支:一个 token 分类头算交叉熵,一个连续重建头算密度 L1。两个损失加权求和,权重用验证集调。
3. 从构型到 token:一份能照着跑的数据构造清单
3.1 构型采样与参考密度计算
数据质量决定上限。我们第一版数据只用了能量极小构型,模型在平衡结构上表现很好,一放到分子动力学轨迹上就崩。后来改成混合采样:一部分是优化后的极小构型,一部分是 300 K 和 500 K 下的短轨迹快照,还有一部分是手动扰动的键长键角。这样模型见过“不那么完美”的密度,泛化明显好一些。
参考密度统一用同一套密度泛函设置算,泛函、基组、收敛阈值、网格精度全部固定。这一点必须强调:不同泛函给出的密度有系统差异,混在一起训,模型会学到“平均泛函”,下游性质全乱。我们曾经因为一批数据用了不同基组,验证 loss 怎么都下不去,查了两天才发现是数据源不统一。
数据量上,模拟项目 X 里我们用了五万个左右的构型起步。听起来多,但按分子骨架去重之后,实际覆盖的化学空间有限。我的建议是先用一千个构型把 pipeline 跑通,确认 token 化、训练、评估都能走通,再扩量。一上来就堆五万个,中间哪个环节有 bug,你根本定位不到。
3.2 网格对齐、归一化与低密度截断
密度网格的对齐方式直接影响模型学什么。我们试过两种:按原子坐标质心对齐,按电子密度质心对齐。前者简单,后者对极性分子更稳。最后选了原子质心,因为推理时输入只有坐标,密度质心拿不到,用前者训练和推理条件一致。
归一化我们做两步:先除以总电子数,让不同大小的分子在数值尺度上接近;再做一次 log 变换,把动态范围压下来。密度值跨好几个数量级,核心区域和尾部差得远,不压缩的话,重建损失会被核心区域主导,尾部学不好。log 变换之后,尾部的小值也能贡献梯度。
低密度截断是个坑。我们一开始把小于 1e-4 的密度全部置零,余弦相似度好看很多,但偶极矩误差变大。原因是尾部虽然数值小,积分起来对偶极有贡献。后来改成保留 10 Å 的 padding,截断阈值降到 1e-6,并且只在训练损失里对低密度区域降权,不直接置零。这个改动让偶极 MAE 从 0.2 D 降到了 0.12 D。
| 预处理项 | 我们的设置 | 作用 | 常见错误 |
|---|---|---|---|
| 盒子 padding | 分子包围盒外扩 6 到 10 Å | 保留密度尾部 | padding 太小,偶极偏差 |
| 网格步长 | 0.2 Å | 平衡精度与显存 | 步长 0.5 Å 以上丢核心结构 |
| 归一化 | 除以总电子数再 log | 稳定数值尺度 | 只做 min-max,尾部被压没 |
| 截断阈值 | 1e-6,损失降权不置零 | 保留尾部贡献 | 直接置零导致积分电荷漂移 |
3.3 序列打包与显存预算怎么算
序列长度直接决定显存。给一个粗略公式:注意力分数矩阵的显存大约是batch × heads × seq_len² × 4字节。假设 batch 为 8、heads 为 8、序列长度 1024,单层就是 8×8×1024²×4,约 256 MB;12 层叠加就是 3 GB 左右,这还没算 MLP 激活和梯度。实际训练时,我们用梯度检查点把激活显存压到三分之一左右,batch 才敢开到 8。
如果序列长度上到 2048,注意力分数显存是四倍,单层就 1 GB,12 层直接爆。所以我们的策略是:小分子控制在 1024 token 以内;大分子做层次化,先粗粒度 4 Å patch 出 500 个 token,再在关键区域做局部细粒度解码,不把整场拉成超长序列。这个取舍很现实,别指望一个模型端到端吃下所有尺寸。
训练配置上,优化器用 AdamW,学习率 3e-4,warmup 5%,weight decay 0.01,混合精度开起来。损失三项:token 交叉熵、密度重建 L1、总电子数约束。权重分别是 1.0、0.5、0.1。总电子数约束这一项很便宜,但能明显抑制采样时的电荷漂移。
3.4 训练配置与收敛判断
收敛判断不能只看训练 loss。我们看三个信号:验证集 token 准确率、验证集密度余弦相似度、以及固定随机种子下的采样稳定性。token 准确率到 60% 左右就基本饱和了,再往上主要是过拟合;密度余弦到 0.95 以上,才开始看下游性质。
早停策略用验证集密度余弦,耐心 10 个 epoch。学习率降到 1e-5 以下还没提升就停。这里有个经验:密度余弦在 0.97 附近会有一个平台期,loss 几乎不动,但下游性质还在慢慢变好。如果你只盯密度指标,可能会过早停掉一个对性质有用的模型。我的做法是同时监控一个轻量下游头,两个指标都不动才停。
4. 核心环节实现:重建头、采样约束与评估
4.1 密度重建头的结构与损失函数
解码器输出的 token 先查码本,得到每个 patch 的码字向量,再用一个小的 3D 转置卷积网络把码字上采样成 patch 内的体素密度。这个重建头很轻,参数量不到主干的十分之一,但它决定了最终密度精度。我们试过直接用码字向量做最近邻插值,密度块边界会有明显台阶;换成转置卷积之后,边界过渡自然很多。
损失函数三项加权:L = L_ce + 0.5 × L_density + 0.1 × L_charge。L_density用 L1 而不是 L2,因为密度分布长尾,L2 会让模型过度关注高密度核心区。L_charge是预测密度积分与真实电子数的相对误差平方,这一项能防止模型在采样时把总电荷弄丢。另外加了一个很轻的平滑项,只在 patch 边界处算,权重 0.01,用来压边界不连续。
# 伪代码:重建损失 def reconstruction_loss(pred_tokens, true_tokens, pred_density, true_density): ce = cross_entropy(pred_tokens, true_tokens) l1 = l1_loss(pred_density, true_density) charge_err = ((sum(pred_density) - sum(true_density)) / sum(true_density)) ** 2 smooth = boundary_smoothness(pred_density) return ce + 0.5 * l1 + 0.1 * charge_err + 0.01 * smooth4.2 自回归采样与化学约束
采样模式用在补全和生成上。给定原子条件 token,从序列开始位置逐 token 采样,温度设 0.8,top-k 设 50。温度太低,生成多样性不够,补全出来永远是同一种密度;温度太高,块边界容易断裂。0.8 是我们试下来保真和多样性的平衡点。
化学约束不能只在采样后做后处理。我们试过先采样再按比例缩放满足总电子数,结果局部密度被整体拉伸,键临界点位置偏了。后来改成两步:一是在 logits 层加电荷约束,把明显会导致电荷偏离的 token 概率压低;二是采样后做一次很小的投影修正,只调整低密度尾部,不动核心区。这样既满足了约束,又不破坏局部结构。
补全任务上,我们把已知 patch 的 token 固定住,只对遮罩位置采样。这里要注意顺序:如果从左到右逐块补,模型会过度依赖左侧上下文,右侧信息用不上。我们的做法是随机顺序补全,训练时也随机遮罩,让模型习惯各种上下文方向。
4.3 评估协议与防泄漏设计
评估协议我们踩过最大的坑是数据泄漏。同一分子的不同构象如果随机分到训练集和测试集,模型其实是在背分子的密度,测试集分数虚高。后来改成按分子骨架划分,同一骨架的所有构象只出现在一个集合里。换成骨架划分之后,密度余弦从 0.99 掉到 0.96,这才是真实水平。
OOD 测试我们设了三档:更大分子、含未见过元素、不同构象分布。三档分开报,不混在一起。指标上,除了余弦相似度,还报积分电荷误差、关键区域误差、以及下游性质误差。关键区域指密度梯度大的地方,比如键临界点附近,这些地方的误差比全局平均更能说明问题。
| 评估项 | 划分方式 | 我们模拟项目里的结果 | 用途 |
|---|---|---|---|
| 同分布密度余弦 | 随机划分 | 0.983 | 看拟合能力,容易虚高 |
| 骨架划分密度余弦 | 按骨架 | 0.961 | 看真实泛化 |
| 积分电荷相对误差 | 骨架划分 | 0.4% | 看物理一致性 |
| 偶极 MAE | 骨架划分 | 0.12 D | 看尾部密度质量 |
| gap MAE | 骨架划分 | 0.08 eV | 看电子结构质量 |
4.4 下游性质迁移的轻量做法
密度主干训好之后,下游任务不要从头训。我们的做法是冻结主干,接一个轻量注意力池化头,把密度 token 聚合成一个向量,再接一个小 MLP 预测性质。池化用可学习的 query 注意力,让模型自己决定哪些 patch 对当前性质重要。偶极任务上,模型确实学会了关注分子两端;gap 任务上,关注的是 frontier orbital 附近区域。
小样本微调时,学习率降到 1e-4,只训池化头和 MLP,主干冻结。数据量几百条就能看到效果。这里有个细节:密度 token 里已经包含了原子类型和坐标信息,但下游头最好还是把原子条件 token 也拼进去,否则模型可能忽略一些全局信息。我们试过只用密度 token,偶极 MAE 是 0.15 D;加上原子条件之后降到 0.12 D。
5. 常见问题排查实录:从 loss 震荡到评估虚高
5.1 训练不收敛的六种典型现象
| 现象 | 可能原因 | 排查方法 | 解决 |
|---|---|---|---|
| loss 剧烈震荡 | 学习率太大或 batch 太小 | 打印每步梯度范数 | 降学习率,加梯度裁剪 |
| token 准确率卡在 30% | 码本死码太多 | 统计码字使用频率 | 重新初始化码本,加 commitment loss |
| 密度重建模糊 | 重建头太弱或 L1 权重太低 | 单独看重建头输出 | 加深重建头,调高 L1 权重 |
| 总电子数漂移 | 缺少电荷约束 | 统计预测密度积分 | 加电荷损失,采样时加约束 |
| 显存 OOM | 序列太长或 batch 太大 | 用公式估算注意力显存 | 梯度检查点,缩短序列 |
| 验证好测试差 | 数据泄漏或过拟合 | 换骨架划分重测 | 按骨架划分,加正则 |
这张表里的每一条我都至少遇到过一次。最坑的是“验证好测试差”,一开始以为是模型过拟合,加了 dropout、weight decay 都没用,最后发现是同一分子的构象跨了集合。改成骨架划分之后,问题直接消失。所以我现在做任何化学数据划分,第一件事就是确认分子骨架有没有跨集。
5.2 评估虚高的四个隐蔽来源
第一个来源是背景零。密度场大部分区域接近零,余弦相似度会被这些零拉高。一个只输出零的模型,在某些体系上也能拿到 0.9 以上的余弦。解决办法是算加权余弦,权重用真实密度,或者只在密度大于阈值的区域算。
第二个来源是归一化统计量泄漏。如果你用全量数据的均值方差做归一化,测试集信息就进了训练。正确做法是只用训练集统计量,测试集用同样的参数变换。
第三个来源是 patch 重叠。如果分块时有重叠,评估时又把重叠区域算进去,信息会重复计算。我们的做法是评估时用非重叠网格,或者对重叠区域做加权平均。
第四个来源是随机种子选择。密度模型对初始化敏感,只报一个种子最好的结果没有意义。我们至少跑三个种子,报均值和标准差。标准差大于 0.01 的指标,我一般不敢拿去对外说。
5.3 泛化到更大体系时的边界
小分子上训的模型,直接推大分子,序列长度先爆。我们的做法是滑动窗口加层次化:粗粒度先扫一遍,找出高密度区域,再在局部做细粒度解码。这样序列长度可控,但窗口边界会有不一致。解决边界问题靠重叠窗口加权,重叠 25% 左右,过渡比较自然。
元素泛化是另一个边界。训练集里没有的元素,模型只能靠位置编码和原子 embedding 外推,效果肯定掉。我们的做法是保留一个“未知元素” token,遇到没见过的元素映射到它,同时在下游任务上做小样本微调。完全零样本外推到新元素,目前不现实,别抱太高期望。
长程关联也是边界。序列模型再强,注意力也有距离衰减。对于大分子里相距很远的两个区域,模型很难学到它们的关联。我们的做法是加一个全局记忆 token,把分子级描述符(比如元素组成、总电子数)压进去,让远处 patch 通过它间接交互。这个改动在大于 50 个原子的体系上有提升,小分子上不明显。
6. 这套方案目前适合谁,以及我自己的使用边界
6.1 先把指标口径钉死,再谈模型
我现在看任何“密度模型准确率 98%”的说法,第一件事就是让对方把评估脚本发过来。不是不信任,是这个领域的指标太容易虚高。余弦相似度、积分电荷、下游性质,三个口径能讲出三个不同的故事。你要是拿余弦 0.98 去对外说“化学精度 98%”,懂行的人一眼就能看出问题。
所以我的建议是,动手之前先写评估规范:数据怎么划分、指标怎么定义、报几个种子、OOD 怎么设。规范写完了再训模型,后面省很多扯皮。模拟项目 X 里我们吃过这个亏,早期为了追一个好看的余弦,把截断阈值调高,结果下游偶极任务怎么都调不上去,回头重做数据又花了两周。
6.2 密度模型不是替代 DFT,而是筛选器
这套东西的定位要摆正。它不是在替代密度泛函计算,而是在做筛选和预判。五万个构型里,先用模型扫一遍,挑出几百个可疑的、或者性质预测有希望的,再送去做高精度计算。这个流程下,模型哪怕有 5% 的误差也能接受,因为它把计算量降了一个数量级。
真正不能接受的,是模型在关键区域系统性偏差。比如过渡金属体系的自旋态,密度模型如果训练数据里覆盖不够,预测出来的密度看着平滑,实际上把自旋分布抹平了,下游性质全错。这种时候,宁可让模型输出一个高不确定性标记,也不要硬给一个看似合理的密度。我们在项目里加了一个不确定性头,用 token 似然的熵做代理,熵高的样本直接跳过,不送下游。这个机制比单纯追准确率有用得多。
最后分享一个我自己的用法:我习惯先用小体系把整条链路跑通,从构型生成、密度计算、token 化、训练到评估,全部走一遍,确认每个环节的输入输出都对得上,再扩数据。密度模型的坑大多不在模型结构,而在数据管线和评估口径。把这两块钉死,剩下的调参反而没那么玄。