
说到机器学习势函数MLIPMachine Learning Interatomic Potential我这个做第一性原理计算的人最初是典型的“手搓脚本派”。拿到一套 DFT 数据想比较几种主流模型那就得在 DeePMD-kit、NEP、DPA 这些各自独立的代码之间来回切——接口不一样、数据格式不一样、checkpoint 不互通换个模型基本等于换一套脚本再折腾一遍环境依赖。这种状态大概持续了两个月我实在忍不了了。真正卡住我的不是模型设计能力而是缺少一个能把数据清洗、描述符构建、训练、验证、部署串成完整链路的东西。后来我换到 AI2Pot 这个基于 PyTorch 的机器学习势函数框架才算是把日常工作理顺了。AI2Pot 这个名字很好理解AI 到 Potential目标就是把“从 DFT 数据到可用势函数”这条路上零散的环节收拢到同一个框架里。这篇文章我不打算给你贴官方文档而是从实际使用者的视角把我换框架的动机、跑通的最小流程、遇到的坑以及最后模型上线到 LAMMPS 的整套经验写清楚。如果你是做材料模拟、计算化学或者刚想入门 MLIP可以参考我的路径直接上手。1. 我为什么从单模型脚本转向 AI2Pot 这类统一框架1.1 产出一个可用 MLIP远不止“把网络训练收敛”这么简单很多人第一次接触 MLIP 时会觉得这东西原理上不就是“输入原子坐标输出能量和力”嘛训练一个神经网络就行。表面看确实如此但等你真正动手就会发现一个项目里至少有五件事是绕不开的。第一是数据格式的解析和清洗。DFT 计算出来的数据可能是 VASP 的 OUTCAR、CP2K 的轨迹、LAMMPS dump 出来的 xyz不同来源的单位、原子顺序、力的字段名都不一样。每换一个框架就有一版数据预处理脚本要重写。第二是近邻原子表的构建。MLIP 模型几乎都是基于局部环境做预测那就得指定截断半径 rcut、每个元素的最大邻居数 sel还要正确处理周期性边界条件。第三是描述符和网络结构的选择这一步决定了模型表达能力的上限。第四是损失函数的多目标平衡训练过程中要同时拟合总能量、每个原子受到的力以及可选的维里项权重一旦给得不对模型就很容易收敛到“能量不错但力离谱”的状态。第五是训练完成后把模型导出成 MD 引擎能读取的势函数文件这一步看似不起眼却是真正让模型产生价值的地方。在此之前我通常是训练一个模型起一套环境DeePMD-kit 有它自己的训练器NEP 有另外的独立程序验证脚本还得自己判断 RMSE 阈值。说白了我百分之六十的时间都花在协调“代码之间的语言不通”上真正思考物理和误差的时间反而被压缩了。我要的不是多一个模型实现而是一个能把这些环节抽象成流程的东西。1.2 AI2Pot 的设计取舍一个入口一套配置多个模型AI2Pot 吸引我的第一点是它把所有工作都收拢到 PyTorch 生态下。对我这种已经被 PyTorch 的调试习惯绑架的人来说这比接触一套全新的训练器要舒服太多。它提供的是一个偏“编排层”的东西数据加载、邻居列表、模型定义、loss 加权、checkpoint 管理、导出接口都以模块方式存在要换模型不需要重写整套流程只需要在配置文件里切换模型注册的名字和对应参数。这套设计的直接好处是我做对比实验的成本变得很低。同一份训练集我可以先跑一个计算量小的短程描述符模型作为基线再换更强的注意力机制模型看精度提升多少。所有模型产出的 checkpoint 格式一致验证脚本可以通用部署流程也可以复用。对一个需要频繁试错的研究场景来说这种“平台感”比某一具体模型的极致精度更重要。说实话把时间和心思省下来去分析力误差来源和数据覆盖盲区比反复在多个框架间做搬运工有价值得多。2. AI2Pot 工作流拆解从数据准备到部署产物要经过哪些环节2.1 数据不是丢一个 xyz 文件就行字段、单位和标签都得对齐进入 AI2Pot 这套流程之后我重新梳理了数据集的规范。现代 MLIP 训练一般推荐使用扩展 xyz 格式extxyz每一帧固定包含三部分晶格信息、原子坐标、以及参考文献里给定的一组标量和矢量标签。以我常用的数据为例一个帧的注释行里至少要有pbc [T, T, T]、cell [...]、energy -352.12这样的字段而每个原子行除了坐标数值之外还要带上forces [...](单位 eV/Å)。如果想要让模型学会应力注释行里还得有virial [...]或者stress [...]单位要统一成 eV/ų。这里我重点提醒一件事单位体系。不同 DFT 程序出来的能量单位可能都是 eV但也有可能是 Hartree 或者 kcal/mol力的单位也可能是 Ry/Bohr稍不注意就会混进训练集。实际操作时我会在送入框架之前先把所有数据转成同一套单位坐标用 Å能量用 eV力用 eV/Å应力用 eV/ų。转换脚本里面写死一个单位换算表每次处理完数据顺手打印一帧的总能量和第一行原子力做抽查。这个习惯帮我避免了很多次“训练看起来正常、部署到 MD 就飘”的问题后面避坑章节我还会再展开。2.2 模型层描述符把“局部环境”编码成网络能用的向量AI2Pot 里模型名看似很多但底层逻辑本质上都是“构建局部环境表示 拟合能量面”。所谓局部环境表示就是截断半径 rcut 内所有邻居原子的种类、相对位置、以及它们与中心原子的关系通过图或者连续函数编码成一个对称不变的向量。这个向量必须满足平移、旋转和原子置换不变性否则同样的物理结构坐标换一个坐标系预测就不一样了。常见的描述符里Deep Potential 系列的se_e2_a是基于径向和角向嵌入做平滑截断复杂度适中适合作为大多数体系的起点se_e3会引入更多的三体信息表达能力更强但训练开销也更大带注意力机制的一类模型则试图用 Transformer 的思路描述非局域相互作用适合长程作用比较重要的体系。我最初的认知误区是“模型越强越好”但实际中如果训练数据只有几百帧复杂模型反而容易出现过度拟合和力场不稳定的情况。框架里模型切换足够廉价之后我更倾向于从简单模型开始逐步增加复杂度。2.3 损失函数为什么不把能量、力和维里按同一权重去优化AI2Pot 这类框架的训练损失一般长这样( L w_E \cdot \text{MSE}(E_\text{pred}, E_\text{ref}) w_F \cdot \text{MSE}(F_\text{pred}, F_\text{ref}) w_V \cdot \text{MSE}(V_\text{pred}, V_\text{ref}) )很多人第一次接触时会觉得能量是最重要的物理量权重应该设置得最高。但实际上标准做法往往相反。每一帧只有一个总能量标签而同一帧里有成百上千个原子力标签。如果能量权重压过力权重模型会倾向于“总能量算对、原子受力完全失真”而分子动力学模拟里面的运动行为恰恰是由原子受力决定的力不准轨迹就是错的。我用的初始权重通常是能量权重 0.02力权重 1000.0维里权重 0.02。这并不意味着能量不重要而是因为力的监督信号数量多、梯度信息更丰富给大力权重能让模型先学会局部受力再通过能量项约束整体势能面的起伏。训练后期有些框架还支持动态权重让能量监督的比重逐渐上来从而压低长时间动力学中的能量漂移。如果你只关注结构优化而不是跑长时间的 MD能量权重可以适当调高但不要高到让力的 RMSE 失控。2.4 从 checkpoint 到部署文件模型闭环的最后一步训练过程中保存的 checkpoint 严格来说只是训练残留物真正要进入 LAMMPS 这类 MD 程序还需要导出为对应的势函数文件。AI2Pot 的导出接口一般会读取最佳 checkpoint再结合配置文件里的描述符参数、元素顺序、网络结构定义打包成一个自包含的部署文件。我习惯在导出之前先做一遍推理端自测加载导出的文件对验证集里随机几帧计算能量和力与训练时记录的预测值比对一遍。这一步能提前暴露“部署代码和训练代码使用了不同精度”的隐患比如 PyTorch 默认的 float32 和 MD 引擎里的 float64 就很容易在应力计算中出现细微差异。部署文件生成之后再按 LAMMPS 要求的 pair_style 配置去调用模型就真正从“训练玩具”变成了“生产工具”。3. 从零跑通一次实践我用 AI2Pot 部署一套 50 帧数据的完整记录3.1 环境与安装先把 PyTorch 和 CUDA 版本对齐我的环境是 Ubuntu 22.04一张单卡 RTX 4090Python 用的 3.10。安装部分我走了比较常规的 conda 路径conda create -n ai2pot python3.10 conda activate ai2pot pip install torch --index-url https://download.pytorch.org/whl/cu118 pip install ai2pot这里有个经验AI2Pot 的版本演进很快不同小版本的入口命令和参数名偶尔会变。所以我的建议是先装到环境里然后跑一下ai2pot --help或者对应模块的--help确认你机器上实际可用的命令长什么样。别死记教程里的命令看帮助信息是最快的对齐方式。3.2 数据准备50 帧怎么组织最不容易出错我这次用的是一套含有 32 个原子的体系共 50 帧 DFT 结构。量虽然不大但用来跑通流程足够了。我的文件目录结构如下project/ ├── dataset/ │ ├── train.xyz │ └── valid.xyz ├── input.yaml ├── model/ └── log/从 50 帧里随机抽 40 帧做训练10 帧做验证。抽帧时注意先按结构类型分层不要把某一种化学环境全部抽到验证集里。扩展 xyz 的第一行必须写清楚单元格信息和能量第二行是原子数之后每一行是元素名加坐标以及forces字段。type_map 这个参数我单独提出来说它定义了元素顺序比如[Li, O]意味着模型内部把原子类型编号为 0 和 1。这个顺序必须在训练和部署阶段保持一致否则导出的力场放到 LAMMPS 里牛头不对马嘴。3.3 写一个最小可训练配置我第一版配置写得非常克制只保留最核心的选项所有花哨的功能先关掉。大致长这样dataset: train: dataset/train.xyz valid: dataset/valid.xyz type_map: [Li, O] seed: 42 model: name: deepmd descriptor: se_e2_a rcut: 6.0 sel: [40, 40] numb_filters: 32 numb_layers: 3 training: epochs: 600 batch_size: 4 optimizer: adam learning_rate: 1.0e-3 decay_steps: 200 decay_rate: 0.96 loss: energy: 0.02 force: 1000.0 virial: 0.02sel的写法要注意它对每个元素分别指定最大邻居数比如 32 个原子的体系里Li 周围最多可能有多少 O 邻居需要根据径向分布函数大概估算。rcut 我选 6.0 Å是因为这套体系径向分布函数的第一配位壳层在 2.0 Å 附近第二壳层大概到 4.5 Å 就结束了6.0 Å 给了足够的缓冲又不至于近邻数爆炸。3.4 启动训练看日志和损失曲线时到底该关注什么配置写好后我执行训练命令比如ai2pot train -c input.yaml日志里每一轮会输出训练集和验证集上能量、力、维里的 RMSE。我盯训练状态的方式很简单力 RMSE 是不是在稳定下降验证集误差和训练集误差的差距有没有越拉越大。差距小但两者误差都高说明模型容量不够两者都低但差距突然变大则说明开始过拟合。这套小数据集训练到大约 250 轮之后训练集的力 RMSE 降到 0.13 eV/Å 左右验证集稳定在 0.21 eV/Å 附近。对一个只有 50 帧的数据集来说这个水平不算惊喜但作为流程验证已经合格了。我不急着追精度先确认整套链路是畅通的后面再加数据、调模型才有意义。配合 TensorBoard 或者其他可视化工具我还会观察 loss 曲线有没有周期性突变。周期性的尖峰通常是 batch 采样不均匀个别帧的应力标签特别大导致的不一定是网络问题。遇到这种情况我的第一反应不是改网络而是回头查数据里有没有异常帧。3.5 验证和导出误差统计与最终部署训练结束之后我用独立的验证命令重新算一遍误差得到一张类似下表的统计指标验证集 RMSE验证集 MAE能量 (meV/atom)12.48.1力 (meV/Å)21.315.2维里 (meV/atom)18.612.9对于初步可用性我自己的经验阈值是力 RMSE 低于 100 meV/Å 的结构优化基本可信低于 50 meV/Å 才敢拿去做长时间的分子动力学。这个阈值不是通用标准但能帮我快速筛选失败的训练。数据合格之后执行导出命令把最优 checkpoint 转成 LAMMPS 能加载的势函数文件。我在 LAMMPS 里的调用方式大概是这样pair_style deepmd model.pb pair_coeff * * Li O注意pair_coeff里的元素顺序必须和训练时的 type_map 一致这一点我吃过亏后面会细说。4. 实操过程中我踩过的几个明显又隐蔽的坑4.1 单位体系混用导致能量漂移训练日志看着正常部署到 MD 就发散我第一次跑一个跨程序的数据集把 CP2K 的数据和 VASP 的数据混在一个训练集里自以为都是能量单位 eV训练出来的模型单点能量验证也马马虎虎。结果放到 LAMMPS 里做 NPT 模拟体系温度直接飘到离谱能量在每个时间步上持续上涨。排查了很久才发现问题在力的单位上。VASP 的 OUTCAR 里力可以很方便转成 eV/Å但 CP2K 默认输出有时候是 a.u. 单位我在预处理脚本里漏做了一个单位的换算。两个程序的力一混相当于训练标签里同时存在两套尺度神经网络只能学一个折中局部受力自然就不对。从那以后我的预处理脚本里强制加入一步“单位自检”随机抽三帧打印总能量、第一个原子的力分量以及晶格常数和 DFT 输出原始值人工比对确认没有数量级差异再进入训练。4.2 sel 和 rcut 设置不当近邻列表比你想的更敏感另一个让我浪费了一整天的坑是sel设得太小。小体系训练时近邻数不多模型像模像样等我把几个构型拼接成更大的无定形结构后有些原子周围的实际邻居数超过了训练时的最大邻居数则邻居数截断有些原子近邻信息被硬性忽略预测自然失真。最典型的症状是单点能量误差很小但结构优化时某些原子的受力输出突然跳变、能量曲线出现不自然的拐点。后来我养成了先分析径向分布函数再定参数的习惯。具体做法是从训练数据里挑几帧代表结构统计每个元素周围不同距离上的配位数画一个配位数随距离变化的曲线观察第一个极小值落在哪里然后让 rcut 至少越过这个极小值sel再留 30% 左右的余量。这样一来近邻列表的覆盖就不太可能成为瓶颈。4.3 损失权重失衡能量拟合得很好力却一塌糊涂我也踩过纯新手坑。有一阵子我觉得能量是最重要的宏观量于是把能量权重调成和力权重同一数量级结果训练出来的模型能量误差确实很漂亮荷载到 MD 却完全跑不动连结构优化都收敛不了。原因前面讲过一帧只有一个能量标签但有几十到几百个力标签能量信号太强模型就放弃了对原子受力的精细拟合而动力学最依赖的恰恰是力。我现在一般先固定力权重为首位验证集力 RMSE 稳定之后再考虑要不要提高能量权重来抑制长程模拟中的能量漂移。框架本身支持动态权重的话优先用“初期大力、后期保能量”的策略。4.4 type_map 顺序不一致训练和部署之间最容易出现的“隐性事故”最后这个坑非常隐蔽因为训练过程完全正常误差也很漂亮但把模型部署到 LAMMPS 后所有原子受力对不上。问题几乎总是出在元素类型映射上。训练配置里type_map: [O, Li]模型内部把 O 编成 0Li 编成 1但 LAMMPS 的 data 文件里原子类型顺序是 Li1O2pair_coeff又没按训练顺序重新声明模型拿到的类型编号就和训练语义完全错位了。这种事不会报错只是无声地给每一个原子分配一个错误的元素身份导致能量和力场一片混乱。解决办法就是固化 type_map 顺序最好直接放到导出的势函数文件元信息里部署时用脚本检查两边的元素顺序是否一致。我在自己的项目脚本里加了一个断言每次导出后读回 model 的 type_map和 LAMMPS 输入文件中的原子类型列表比对不一致就中止。5. 模型选择与下一步扩展简单体系用简单模型复杂体系再上重武器5.1 DeepMD 系描述符够用的时候不必急着上注意力模型以我目前测试过的体系来看约 60% 以上可以先用se_e2_a这类短程描述符拿到合格结果。它的优势是训练快、数据需求相对少、移植稳定性好。注意力模型确实擅长捕捉非局域效应比如带电体系的库仑相互作用在一定距离上仍有明显贡献或者体系中存在长程极化但随之而来的问题是训练更慢、调参维度更多、对数据覆盖要求更高。如果你只有几百帧数据注意力模型很可能出现过拟合式的“记忆”而不是真正的泛化。所以我的选型原则是先跑简单模型拿到性能基线再看验证集上的误差瓶颈是“表达能力不足”还是“数据不足”。数据不足就扩数据扩不了数据再考虑用复杂模型硬扛。5.2 主动学习和两阶段训练框架价值真正被放大的场景AI2Pot 这类框架对我最有吸引力的地方其实是在模型上线之后可以继续反哺数据。用框架训练一个低配模型放进 LAMMPS 里跑一批短 MD从轨迹里挑出“模型力误差波动大”的构型再把这些构型拿去做 DFT 单点计算把新数据合并进训练集继续训练。这就是主动学习的标准闭环。闭环跑起来之后你会发现框架的 checkpoint 管理和统一的验证接口特别省事。我不用再手动维护“哪些数据是哪一批模型训练的”只需要在导入数据时打标签记录来源批次。下一步我还打算在 AI2Pot 里接入更多描述符类型看看同一个流程下不同模型的误差分布有什么差异。模型之间的对比结论反过来也能指导 DFT 采样方向比如哪一类构型让模型集体失准我就在那附近多补采样点。最后分享一个我自己的习惯每次跑新体系不管数据多小都先完整走一遍“数据检查—短程描述符训练—验证—导出—LAMMPS 单点测试”的链路。这条链路跑通了后面再换复杂模型、加数据、做主动学习都只是在同一套流程上更换零件。工具的意义不是替你想问题而是让那些真正需要想的问题变得可见。AI2Pot 对我而言的价值恰好就在这里。