Pic2SMILES / MolMini 技术报告¶
1. 任务与交付¶
目标:训练一个小型光学化学结构识别(OCSR)模型——输入 2D 化学结构图,输出规范 SMILES——能在 16GB M3 MacBook Pro 上轻松运行,采用当前的小模型工程实践,并给出 可复现的报告。
交付物:MolMini,image-to-SMILES 编码器-解码器,三档规模(3.6M / 12.1M / 23.8M 参数),在 262 万条 ChEMBL 分子上从零训练(图像实时渲染),外加一套在完全 相同的冻结图像上对比 MolScribe 与两个参照点的评测框架。
chembl_test_linux
| model | params | checkpoint | n | valid | exact | exact/no-stereo | tanimoto | measured on |
|---|---|---|---|---|---|---|---|---|
| oracle | 0 | - | 3000 | 1.000 | 1.000 | 1.000 | 1.000 | Darwin-arm64 |
| molmini | 23.8M | step 240,000 | 3000 | 0.997 | 0.897 | 0.937 | 0.965 | Darwin-arm64 |
| molmini | 12.1M | step 240,000 | 3000 | 0.998 | 0.857 | 0.927 | 0.958 | Darwin-arm64 |
| molscribe | pretrained external | - | 900 | 0.934 | 0.647 | 0.760 | 0.892 | Linux-x86_64 |
| image_hash_nn | 0 | - | 3000 | 1.000 | 0.000 | 0.000 | 0.118 | Darwin-arm64 |
2. 起点评估¶
原仓库只有评测框架,没有模型。唯一的训练代码是对 60 条硬编码 SMILES 的闭集分类
器,在自己的合成数据上 exact match 5.6%;11 个注册"模型"中有 6 个是占位适配器,
只能输出 skipped。_TinyViT 把层存在普通属性上,导致 parameters() 返回未注册
的参数集,.to(device) 移动的是影子副本。
闭集分类器无法通过调参变成开放词表识别器——输出空间就是错的——所以建模部分重写。 保留的部分:规范 SMILES 评分、"不可用系统记为 skipped 而非失败"的契约、冻结 manifest 格式。
3. 方法¶
3.1 输出表示¶
采用自回归 SMILES 解码,而非图解码。这是由可获得的监督信号决定的:训练图像来自 RDKit 渲染,每张图有精确的 SMILES 但没有原子坐标,而图解码器(MolGrapher、 MolScribe 的 graph head)需要原子级坐标监督。图解码在该领域顶端是更强的形式; 这里的选择基于监督可得性,不是说序列解码更好。
3.2 架构¶
384x384x1
-> conv 7x7 stride 4 -> 96x96x64
-> conv stride 2 + 3 个 ConvNeXt block -> 48x48x128
-> conv stride 2 + 3 个 ConvNeXt block -> 24x24x256
-> linear + RMSNorm -> 576 tokens x 384
-> 4 层 transformer,轴向 2D RoPE
-> 6 层因果解码器,1D RoPE,交叉注意力,绑定词嵌入
用卷积把像素降到 24x24 的 token 网格,因为 OCSR 的早期特征(笔画、交叉点、字符) 都是局部的;全局注意力只作用在剩下的 576 个 token 上——识别环闭合这类需要关联图中 远距离部分的任务,才真正需要注意力。组件:RMSNorm、SwiGLU、QK-norm、fan-in 缩放 初始化、WSD 学习率、权重 EMA。
分辨率由声明的范围决定:224px 下,稠密稠环体系里的原子标签和电荷已经无法辨认。
3.3 数据¶
训练图像每步实时生成。渲染一张约 2ms CPU,远低于一步 GPU 的开销,所以冻结图像集
没有收益,只会损失泛化。每次渲染随机化布局引擎、旋转、键宽、字体、原子配色、缩写
基团(Ph、OMe)和漫画风格;随后叠加笔画形态学、模糊、降采样、JPEG、透视畸变、
纸张纹理和椒盐噪声。
必须明说的推论:增强分布就是规格说明。任何不在其中的作图约定或退化模式,在推理 时都是分布外,训练多久都没用。
3.4 范围与泄漏控制¶
| constraint | value |
|---|---|
| heavy atoms | 4 to 48 |
| elements | B, Br, C, Cl, F, I, N, O, P, S, Se, Si |
| formal charges | allowed |
| multi-fragment | excluded |
| curation | count |
|---|---|
| read | 2,897,819 |
| rejected (unparseable or out of scope) | 269,119 |
| duplicate by canonical SMILES | 141 |
| removed by the DECIMER leakage guard | 210 |
| kept | 2,628,559 |
| train / val / test | 2,618,349 / 5,000 / 5,000 |
| vocabulary | 88 tokens |
有 210 个分子同时出现在 ChEMBL 和 DECIMER 手绘评测集中。在划分数据前,已按 规范 SMILES 从训练语料中剔除。否则"能泛化到真实手绘结构"这个结论会有一部分其实是 在测量记忆。
4. 一次训练失败的完整记录¶
前两次训练在约 1 万步被中止。loss 正常下降、teacher-forced token 准确率达 0.77, 但贪心解码对每张图都输出同一个 SMILES 字符串。
实测:两张不同图像的编码器 memory 余弦相似度 1.00004。编码器无论输入都输出同一个 常量向量,解码器学成了无条件 SMILES 语言模型——SMILES 本身的可预测性就能给到 0.6–0.77 的 token 准确率,所以 loss 曲线什么都看不出来。
8 组样本过拟合测试记住了 8/8,排除架构 bug。按模块统计第 0 步梯度定位到原因:
| 第 0 步梯度范数 | |
|---|---|
| 编码器 | 108,675 |
| 解码器 | 12.2 |
init_weights 对所有层用固定 trunc_normal_(std=0.02)。这个常数是按 GPT-2 的宽度
校准的;用在 fan-in 576 的卷积上,激活衰减到 std 0.017,而归一化层的反向增益正比于
1/RMS(输入),于是微小激活变成巨大梯度。clip_grad_norm_(1.0) 再把整体梯度除以约
1e5——解码器几乎没更新,编码器在第一步就被打死。
修复:fan-in 缩放初始化、stem 内逐图像标准化、stem 投影后加 RMSNorm、交叉注意力排除 在深度残差缩放之外、ConvNeXt LayerScale 从 1e-5 提到 0.1(1e-5 低于它所加到的激活的 bf16 尾数精度)。
| 相同的 800 步对照 | memory 余弦 | token 离散度 |
|---|---|---|
| 修复前 | 1.00000(塌缩) | 0.0056,持续衰减 |
| 修复后 | 0.61,持续下降 | 0.70,持续增长 |
可推广的教训:对于一侧能独立解决部分任务的编码器-解码器结构,loss 下降并不能 证明两侧都在学习。诊断必须直接测量条件信号是否被使用。
防线:tests/test_init_health.py(已验证在修复前代码上失败)和
Trainer.encoder_health——每个日志间隔记录 memory 余弦,超过 0.995 直接中止训练。
5. 评测设计¶
三个让数字有意义的性质:
- 冻结图像。评测图像用固定种子写盘一次。若评测图实时生成,性能退化和一次 倒霉的渲染无法区分。
- 经过验证的评分路径。
oracle直接读 manifest 里的答案,必须精确得到 1.000。 在两个评测集全部 3000 张图上确实如此。 - 记忆下界。
image_hash_nn检索最近的训练图像,得分 0.000。模型高于这条线的 部分才是识别,而不是查表。
三个难度档:clean、varied、degraded,外加 DECIMER 手绘集作为分布外测试。
字体对照。两台机器的字体族完全不相交(Linux 上是 DejaVu,macOS 上是 Arial/Georgia)。没有掩盖,而是用同样的分子和种子在两台机器上各建一套评测集。 MolScribe 在两套上分别得 0.647 和 0.646——相差 0.001——这说明字体族不会影响一个 成熟的 OCSR 模型,因此 MolMini 若在两者间出现差距,那就是它自身的字体敏感性。
留出分子到底有多"新"?¶
数据划分按同分异构规范 SMILES 不相交,并在构建时断言。但对照全部 2,618,349 条 训练分子测量后,这个保证比听上去弱得多:
| 统计量 | 数值 |
|---|---|
| 到训练集的最大 Morgan(r=2) Tanimoto 中位数 | 0.806 |
| 95 分位 | 1.000 |
| 存在近似孪生体的比例 (>= 0.9) | 0.158 |
| 存在相近类似物的比例 (>= 0.7) | 0.833 |
| 真正新颖的比例 (< 0.5) | 0.007 |
83% 的留出分子在训练集中有相近类似物。95 分位为 1.000 意味着至少 5% 的分子指纹 完全相同:Morgan 指纹默认忽略立体化学,所以训练分子的一个立体异构体是不同的 同分异构 SMILES,能通过不相交断言,而指纹完全一致。
精确量化:5,000 条测试分子中有 409 条(8.2%)与某个训练分子共享去立体化学后的 骨架。 对这些分子,模型已经见过其 2D 结构,真正新的只有楔形/虚线键的读取。这不 是纯粹的泄漏——立体化学本来就是 OCSR 模型必须从图上读出的内容——但每十二条测试分子 中就有一条,其骨架识别部分是被记住的。
这就是 ChEMBL 的性质——同系列化合物成批收录——随机划分不可能产生新颖骨架。自然的 推论是:渲染测试集上的 exact match 很大程度上测量的是同系列内的插值,而不是对陌生 骨架的识别能力。
这个推论可以在不重训的情况下检验,而且它没有通过检验。965 条(19.3%) Bemis-Murcko 骨架在训练语料中完全不存在的测试分子被单独评分:
| 评测集 | 分子数 | exact |
|---|---|---|
| 完整测试集 | 1000 | 0.779 |
| 仅新颖骨架 | 965 | 0.811 |
差值为 +0.033,95% 区间 ±0.036——1.8 sigma,不显著。所以诚实的结论是"在未见 骨架上没有可检测到的性能下降",而不是新颖骨架更容易。混杂因素已检查:新颖骨架子集 的分子略大(29.2 vs 28.4 重原子)、骨架更大(25 vs 22 原子),环数、立体中心密度和 SMILES 长度都相当。
MolScribe 也在两个子集上评了分,作为外部标定。它从未在本项目的 ChEMBL 划分上训练 过,因此它的分数独立于任何在此训练的模型,探测的是两个子集本身的难度:
| 模型 | 完整测试集 | 新颖骨架 | 差值 |
|---|---|---|---|
| MolScribe(外部) | 0.647 | 0.644 | -0.002 |
| MolMini | 0.779 | 0.811 | +0.033 |
MolScribe 认为两者难度相当,所以新颖骨架子集本身并不更容易,MolMini 的结果不是子集 难度造成的假象。两个模型——一个在本语料上训练、一个没有——都没有在未见骨架上出现 性能下降,这正是"OCSR 本质上是视觉任务"所预期的:读一张图不需要事先见过那个骨架。
这令人安心——模型似乎在做视觉识别,而不是从记住的化学词表里检索——但它只回答了骨架 这一个维度。上面的指纹相似度结果和立体异构体通道并未因此改变。
同样不受影响:图像仍然是未见过的,degraded 档仍然测量抗退化能力,DECIMER 手绘
结果仍然是对陌生输入迁移能力的诚实度量。
这对数据划分意味着什么。 低骨架新颖度是数据的真实性质,本报告如实说明而非掩盖; 但唯一能直接检验的那个维度——骨架熟悉度——并未对准确率产生可检测的影响。在训练阶段 采用骨架划分或相似度上限划分仍然值得做,因为它还能覆盖本次检验未触及的指纹相似度和 立体异构体两个维度。
这个基准测量什么、不测量什么¶
请在阅读结果表之前先读这一节。
渲染评测集是由与训练完全相同的渲染器和增强管线生成的。分子是留出的——划分按 规范 SMILES 不相交并在构建时断言——但渲染分布是相同的。所以这些评测集测量的是:
给定来自 MolMini 训练分布的作图、以及它从未见过的分子,它读得多准?
这是个真实且有用的问题。但它不是"MolMini 是不是比 MolScribe 更好的 OCSR
系统"这个问题。任何外部系统在这里都是在自己训练分布之外被评分,而本项目的模型是在
主场被评分。特别是 degraded 档上的大幅领先,反映的是那套退化管线属于 MolMini
而不属于 MolScribe。
DECIMER 手绘集是保持诚实的对照:它对 MolMini 是陌生的,反而更接近 MolScribe 的 设计目标。那上面的排序是交错的,而不是干净的反转 —— 12.1M 模型高于 MolScribe (0.125 对 0.099),23.8M 模型低于它(0.088)—— 而且绝对水平上没有任何一个系统 读得好。两个方向永远一起报告。
6. 结果¶
chembl_test_linux
| model | params | checkpoint | n | valid | exact | exact/no-stereo | tanimoto | measured on |
|---|---|---|---|---|---|---|---|---|
| oracle | 0 | - | 3000 | 1.000 | 1.000 | 1.000 | 1.000 | Darwin-arm64 |
| molmini | 23.8M | step 240,000 | 3000 | 0.997 | 0.897 | 0.937 | 0.965 | Darwin-arm64 |
| molmini | 12.1M | step 240,000 | 3000 | 0.998 | 0.857 | 0.927 | 0.958 | Darwin-arm64 |
| molscribe | pretrained external | - | 900 | 0.934 | 0.647 | 0.760 | 0.892 | Linux-x86_64 |
| image_hash_nn | 0 | - | 3000 | 1.000 | 0.000 | 0.000 | 0.118 | Darwin-arm64 |
| style | model | params | checkpoint | n | valid | exact | tanimoto |
|---|---|---|---|---|---|---|---|
| clean | image_hash_nn | 0 | - | 1000 | 1.000 | 0.000 | 0.129 |
| clean | molmini | 12.1M | step 240,000 | 1000 | 0.999 | 0.917 | 0.996 |
| clean | molmini | 23.8M | step 240,000 | 1000 | 0.998 | 0.952 | 0.997 |
| clean | molscribe | pretrained external | - | 300 | 0.977 | 0.823 | 0.995 |
| clean | oracle | 0 | - | 1000 | 1.000 | 1.000 | 1.000 |
| degraded | image_hash_nn | 0 | - | 1000 | 1.000 | 0.000 | 0.112 |
| degraded | molmini | 12.1M | step 240,000 | 1000 | 0.995 | 0.748 | 0.882 |
| degraded | molmini | 23.8M | step 240,000 | 1000 | 0.997 | 0.806 | 0.901 |
| degraded | molscribe | pretrained external | - | 300 | 0.863 | 0.350 | 0.678 |
| degraded | oracle | 0 | - | 1000 | 1.000 | 1.000 | 1.000 |
| varied | image_hash_nn | 0 | - | 1000 | 1.000 | 0.000 | 0.114 |
| varied | molmini | 12.1M | step 240,000 | 1000 | 0.999 | 0.905 | 0.996 |
| varied | molmini | 23.8M | step 240,000 | 1000 | 0.996 | 0.934 | 0.997 |
| varied | molscribe | pretrained external | - | 300 | 0.963 | 0.767 | 0.978 |
| varied | oracle | 0 | - | 1000 | 1.000 | 1.000 | 1.000 |
chembl_test_novel_scaffold
| model | params | checkpoint | n | valid | exact | exact/no-stereo | tanimoto | measured on |
|---|---|---|---|---|---|---|---|---|
| molmini | 23.8M | step 240,000 | 2895 | 0.996 | 0.894 | 0.936 | 0.965 | Darwin-arm64 |
| molmini | 12.1M | step 240,000 | 2895 | 0.993 | 0.866 | 0.927 | 0.960 | Darwin-arm64 |
| molscribe | pretrained external | - | 900 | 0.910 | 0.644 | 0.757 | 0.898 | Linux-x86_64 |
| style | model | params | checkpoint | n | valid | exact | tanimoto |
|---|---|---|---|---|---|---|---|
| clean | molmini | 12.1M | step 240,000 | 965 | 0.998 | 0.917 | 0.993 |
| clean | molmini | 23.8M | step 240,000 | 965 | 0.998 | 0.938 | 0.994 |
| clean | molscribe | pretrained external | - | 300 | 0.987 | 0.827 | 0.988 |
| degraded | molmini | 12.1M | step 240,000 | 965 | 0.989 | 0.777 | 0.895 |
| degraded | molmini | 23.8M | step 240,000 | 965 | 0.995 | 0.808 | 0.908 |
| degraded | molscribe | pretrained external | - | 300 | 0.797 | 0.347 | 0.692 |
| varied | molmini | 12.1M | step 240,000 | 965 | 0.994 | 0.904 | 0.993 |
| varied | molmini | 23.8M | step 240,000 | 965 | 0.996 | 0.936 | 0.993 |
| varied | molscribe | pretrained external | - | 300 | 0.947 | 0.760 | 0.978 |
chembl_test_macfonts
| model | params | checkpoint | n | valid | exact | exact/no-stereo | tanimoto | measured on |
|---|---|---|---|---|---|---|---|---|
| oracle | 0 | - | 3000 | 1.000 | 1.000 | 1.000 | 1.000 | Darwin-arm64 |
| molmini | 23.8M | step 240,000 | 3000 | 0.998 | 0.888 | 0.926 | 0.962 | Darwin-arm64 |
| molmini | 12.1M | step 240,000 | 3000 | 0.997 | 0.859 | 0.925 | 0.959 | Darwin-arm64 |
| molscribe | pretrained external | - | 900 | 0.913 | 0.646 | 0.757 | 0.910 | Linux-x86_64 |
| image_hash_nn | 0 | - | 3000 | 1.000 | 0.000 | 0.000 | 0.119 | Darwin-arm64 |
| style | model | params | checkpoint | n | valid | exact | tanimoto |
|---|---|---|---|---|---|---|---|
| clean | image_hash_nn | 0 | - | 1000 | 1.000 | 0.000 | 0.130 |
| clean | molmini | 12.1M | step 240,000 | 1000 | 0.996 | 0.917 | 0.996 |
| clean | molmini | 23.8M | step 240,000 | 1000 | 0.997 | 0.949 | 0.997 |
| clean | molscribe | pretrained external | - | 300 | 0.977 | 0.823 | 0.996 |
| clean | oracle | 0 | - | 1000 | 1.000 | 1.000 | 1.000 |
| degraded | image_hash_nn | 0 | - | 1000 | 1.000 | 0.000 | 0.114 |
| degraded | molmini | 12.1M | step 240,000 | 1000 | 0.994 | 0.771 | 0.890 |
| degraded | molmini | 23.8M | step 240,000 | 1000 | 0.997 | 0.794 | 0.901 |
| degraded | molscribe | pretrained external | - | 300 | 0.803 | 0.357 | 0.721 |
| degraded | oracle | 0 | - | 1000 | 1.000 | 1.000 | 1.000 |
| varied | image_hash_nn | 0 | - | 1000 | 1.000 | 0.000 | 0.113 |
| varied | molmini | 12.1M | step 240,000 | 1000 | 1.000 | 0.890 | 0.990 |
| varied | molmini | 23.8M | step 240,000 | 1000 | 0.999 | 0.922 | 0.987 |
| varied | molscribe | pretrained external | - | 300 | 0.960 | 0.757 | 0.981 |
| varied | oracle | 0 | - | 1000 | 1.000 | 1.000 | 1.000 |
decimer_hdm
按 issue #4 修正。 这组手绘数字是在 image_to_tensor 不再拉伸非方形输入之后
重测的;抽样的 200 张 DECIMER 图里有 179 张不是方形,也就是说它们此前全部是被拉伸
着送进模型的。在同样的 1000 张图、同样的 checkpoint 上,exact match 从 0.037 变为
0.088(23.8M),从 0.057 变为 0.125(12.1M)。渲染评测集是 384x384 方形图,补边在
那里是空操作,所以那些数字没有变化。MolScribe 走自己的预处理,不受影响。
| model | params | checkpoint | n | valid | exact | exact/no-stereo | tanimoto | measured on |
|---|---|---|---|---|---|---|---|---|
| molscribe | pretrained external | - | 1000 | 0.852 | 0.099 | 0.126 | 0.392 | Linux-x86_64 |
| molmini | 12.1M | step 240,000 | 1000 | 0.934 | 0.125 | 0.170 | 0.403 | Darwin-arm64 |
| molmini | 23.8M | step 240,000 | 1000 | 0.950 | 0.088 | 0.112 | 0.336 | Darwin-arm64 |
| style | model | params | checkpoint | n | valid | exact | tanimoto |
|---|---|---|---|---|---|---|---|
| hand_drawn | molmini | 12.1M | step 240,000 | 1000 | 0.934 | 0.125 | 0.403 |
| hand_drawn | molmini | 23.8M | step 240,000 | 1000 | 0.950 | 0.088 | 0.336 |
| hand_drawn | molscribe | pretrained external | - | 1000 | 0.852 | 0.099 | 0.392 |
关于 degraded 档的一个说明。退化管线的参数是独立采样的,在极端组合下会产生
连人也无法辨认的图像——强纸张纹理叠加模糊,可以把细键结构完全淹没。因此
degraded 档的部分失败并不是模型读不出一张本可读的图。这一档应理解为"包含直至
不可辨认程度的抗退化能力",而不是真实扫描件性能的干净下界。
7. 局限¶
- 随机划分无法测试新颖骨架。83% 的留出分子在训练集中有相近类似物(见第 5 节)。 在骨架确实未见过的子集上准确率没有下降,所以这一点看来并未抬高数字;但指纹相似度 和立体异构体两个维度仍未被检验,需要在训练阶段采用骨架划分或相似度上限划分。
- 渲染基准是本模型的主场。任何外部系统在其上都是在自己的训练分布之外被评分。 手绘集是对冲,在那上面差距缩小到几个百分点,方向随模型大小而变 —— 没有任何一个 系统能读好这些手绘图。
- 手绘图是分布外的。RDKit 的 comic 模式能画出抖动笔画,但复现不了人类的断笔、 不一致的键长和错位的标签。
- Markush 结构、R 基团、反应式、多分子页面:不在语料中、不渲染、不支持。
- 范围受限。超出声明的元素集和重原子数范围,报告的准确率不迁移。
- 训练不是位级确定的。GPU kernel 调度本身不确定;重跑会接近但不会精确复现。
- 训练日志里的
[eval]行不能和评测表对比——分子不同、像素不同、每次重新采样。
8. 复现¶
全部命令见 docs/REPRODUCE.md。每个设计选择的依据和背后的实测数据见
docs/DECISIONS.md。