去年底我接了个二维材料异质结筛选的活儿,第一轮就列了六十多个候选体系,准备算带隙。真动手才发现,单个体系的 DFT 跑下来动不动就是十几个核时,遇到大超胞再带上磁序,一天能算完两个都算顺利。按这个速度,这一批全部跑完怕是项目周期就直接报废了。后来我把流程改成两步:先用机器学习模型从已知二维材料数据里学一套“快速预测规则”,把明显不合要求的候筛选掉,剩下的少量候选再用 DFT 精算。这其实就是二维材料研究里现在最常见的机器学习落地方式:用可承受的计算量换筛选宽度,把昂贵的计算留给最值得的体系。
这篇记录从理论到实战把这条链路完整走一遍,包括二维材料数据集怎么拿、晶体结构怎么转成特征、模型怎么选才不会自己骗自己,最后给一套可以直接跑的带隙初筛代码。如果你是材料、物理背景,刚开始接触机器学习,或者已经会调 sklearn 但没处理过材料结构数据,这篇应该能帮你省下不少试错时间。
1. 二维材料筛选中的机器学习:从“算不起”到“算得起”
这一节先聊清楚一个大家容易忽略的问题:机器学习在二维材料研究里到底解决的是谁的痛点,又是凭什么解决的。
1.1 DFT 计算量随体系膨胀的现实瓶颈
第一性原理计算精度高,但代价也摆在那里。DFT 的复杂度大致随电子数的三次方增长,这意味着体系尺寸一上来,计算资源立刻捉襟见肘。二维材料单体还好说,单层 MoS2 这种小体系,一个 PBE 级别带隙计算通常几分钟到几十分钟就能完成。但一旦进入异质结、缺陷超胞、扭转多层结构,原子数轻松突破一两百,计算成本不是线性上涨,是跳跃式上涨。
更麻烦的是二维材料的候选空间大得吓人。元素周期表里可用的组合数以万计,再加上同一种化学组成可能对应 1T、2H、3R 等不同相,还有异质结堆叠、覆盖层取向、应力应变这些额外自由度。你不可能靠“每个体系都跑一次 DFT”把一个材料族的所有组合都覆盖到,哪怕能做到,也没人有足够的机时等结果。
我自己的经验是,二维材料项目做到中后期,瓶颈往往不是物理建模思路,而是计算吞吐量。这时候你会特别希望有个东西能瞬间回答“这批新材料里哪些更可能是目标带隙范围”,不用精确到 0.01 eV,能筛掉 70% 的低价值候选就是巨大胜利。
1.2 机器学习代理模型进入材料研发流程
机器学习在二维材料里的核心角色,不是替代 DFT,而是当“代理模型”。所谓代理模型,就是先拿一批已经算好的 DFT 数据当训练集,让模型学习结构-性质映射,学习完成后用它在毫秒级时间里预测大量新结构的性质。
一个典型的筛选流程大致是这样:先选一个聚焦的材料空间,比如过渡金属硫族化合物单层;然后从公共数据库导出足够多的样本,清洗后构造特征;用随机森林、高斯过程回归或者图神经网络之类的模型训练性质预测器;最后把候选空间里所有结构都过一遍模型,按预测性质排序,挑出最值得深算的体系交给 DFT 验证。
这套流程的价值非常直接:DFT 的大规模筛选能力可能一天 20 个体系,机器学习模型一秒钟能过几千个。即使准确率不是 100%,只要排序能力可靠,就能把有限的计算资源聚焦到最有可能出结果的体系上。实际做下来,一个项目的计算成本通常能降到原来的十分之一甚至更低。
1.3 不是所有体系都适合机器学习,先看任务特征
也不是所有二维材料项目都适合无脑上机器学习。适合的第一类,是目标性质明确、训练标签充足的任务,比如带隙、形成能、晶格常数、弹性模量、磁性矩,这类性质在数据库里存量丰富,有足够样本可用。第二类是候选空间巨大的筛选任务,典型如元素替换扫描、异质结匹配,这类任务靠 DFT 穷举不现实,最适合机器学习先粗筛。
不适合的也有,比如目标性质极度依赖复杂电子关联效应,或者实验测量条件本身对材料状态影响巨大,导致标签本身就含混不清,这种任务往往模型训练得再漂亮也没法落地。判断标准其实很简单:先问问自己,有没有一批可信的、口径一致的标签数据?如果没有,先不要谈模型选型。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据从哪里来:二维材料公共数据库与清洗细节
如果说用哪个模型是面子,那数据集就是里子。二维材料机器学习项目里,数据准备能占掉整个项目一半以上的时间,而且每一处偷懒都会在后面对模型泛化能力上以极其难看的姿态显现。
2.1 几个常用的二维材料数据来源及其特点
目前大家用得比较多的公共数据源有三个:C2DB、JARVIS-DFT 和 Materials Project。三者各有侧重,了解它们的差异能帮你少走很多弯路。
| 数据库 | 主要覆盖 | 常用量 | 注意事项 |
|---|---|---|---|
| C2DB | 二维单层及部分少层材料 | 大量二维层状材料的结构与性质 | 数据较系统,但泛函和自洽设置需要看说明 |
| JARVIS-DFT | 三维体系为主,包含二维子集 | 高通量结构-性质数据 | 附带较多描述符和 ML 基线,适合做基准测试 |
| Materials Project | 三维和部分二维结构 | 能量、带隙、弹性等 | 二维条目需要自己筛选,不能默认全部适用 |
C2DB 在二维材料圈子里用得最顺,它会给出弛豫后的结构、总体能量、带隙、磁矩等一批性质,而且结构信息相对干净。JARVIS-DFT 的二分子集也很有价值,它的特点是计算参数统一度高,附带的一些描述符和机器学习基线可以直接拿来当参照。Materials Project 里面二维结构相对分散,如果你需要的是元素组成层面的覆盖度,它也能用,但筛起来费点劲。
2.2 最容易翻车的数据一致性问题
二维材料机器学习项目里最隐蔽的坑,不是缺失值,而是数据口径不一致。最常见的就是泛函不一致。PBE 会系统性低估带隙,HSE 或者 GW 计算出的带隙会大不少。如果你的训练集里一部分样本带隙是 PBE 口径,另一部分是 HSE 口径,模型会学到什么?它会尝试用一个不存在的“平均带隙”概念去拟合数据,结果只能是两头都不讨好,预测误差大得离谱。
另外还要注意自旋极化的设置。不少二维材料存在磁性基态,同一结构在自旋极化关闭和开启下给出的能量和带隙完全不同。导出数据时最好逐条记录计算是否开了自旋极化、用的什么泛函、结构是否完成了原子弛豫和力的收敛。数据库里每个材料的元数据字段不是摆设,花十分钟把这些信息摸清楚,能省下后期一个星期的调试时间。
2.3 我的清洗清单,照着做可以少掉长发
清洗步骤我一般固定这么几个动作:第一步,剔除优化不收敛或能量异常的结构,这类数据混进训练集会导致模型学出一堆假规律。第二步,检查带隙分布。带隙为 0 的通常是金属,带隙在零点几电子伏特以下的小带隙半导体也很常见,最好画个直方图看看样本覆盖是否均衡,别让模型只见过带隙集中在 0 附近的体系。第三步,按化学式和空间群做一次去重,去掉重复结构。但要注意,如果同一材料不同磁性序对应不同性质,这种重复不能简单删,要看你的任务是否涉及磁性。
数据归一化之前先做训练集和测试集划分。有人喜欢先把全体特征标准化再切分,这会导致测试集的信息通过均值和方差泄漏进训练流程,属于一种很隐蔽的数据泄漏。正确的做法是只在训练集上计算均值和方差,然后用同样的参数去变换测试集。
3. 从晶体结构到特征:二维材料的“翻译”方法
机器学习模型不认识晶体结构,它只认识数值向量。把原子坐标、元素种类和空间群这些信息转换成模型能消费的数值表示,是二维材料机器学习里最重要也最考验功底的一步。
3.1 手工描述符:把化学直觉直接写进去
手工描述符是入门最容易上手的表示方式。核心思路是先确定一组和电子结构强相关的物理化学量,比如组成元素的电负性差、平均原子半径、价电子数、极化率、d 电子数等,再看需要不需要把结构因素拉进来,比如配位数、最近邻键长、层间距。
为什么这些量有用?以带隙预测为例,电负性差大往往意味着离子性强,价带顶和导带底之间的能量间隙会受到成键离子性的显著影响;d 电子数对过渡金属化合物的金属性和磁性影响直接;原子半径差异则会影响晶格畸变和轨道重叠程度。把这些量组合起来,即使不用深度学习,线性模型也能捕捉一部分物理趋势。
手工描述符的最大优点是透明、可解释,缺点是有时候太粗糙,遗漏了局域配位环境的细节。比如同样化学计量比的 1T 相和 2H 相,元素组成属性完全相同,带隙却天差地别,这时候纯元素统计特征就失灵了,需要额外加入结构描述符。
3.2 自动环境特征:让算法自己看局部环境
为了解决手工描述符不够灵活的问题,材料信息学里发展出一批自动构建的局部环境描述符,比如对称函数、SOAP、MBTR 这类方法。它们的基本逻辑类似:以每个原子为中心,统计它在给定截断半径内的邻居种类、距离和角度分布,再把统计结果编码成旋转和平移不变的向量。
这些描述符的好处是不需要你手动设计具体公式,算法会根据原子局域环境自动构造出一个较完整的指纹。你只需要选好截断半径、宽度参数这类超参数。处理二维材料时要格外注意:如果你用的是面向三维晶体的描述符,截断半径必须大于层厚;而如果任务涉及层间堆叠性质,那还要保证描述符能区分不同层间的原子,而不是把所有邻居一股脑混在一起。
3.3 图神经网络:直接吃下原子结构
图神经网络这两年特别火,代表思路是 CGCNN、MEGNet 这类模型。它们把晶体表示成一个图,节点是原子,边是原子间的键,节点和边上可以携带元素属性、键长、键角之类的信息。通过若干轮信息传递,每个节点逐步聚合邻居信息,最终池化成整个结构的向量用来预测性质。
图神经网络的优势是端到端,不需要你手工设计描述符,模型自己学习原子间的交互规则。但在二维材料这个场景下,图神经网络并不总是最优解。当你的训练数据只有几百到一两千条时,它常常打不过工程调参到位的随机森林。原因并不神秘,深度学习吃数据,而二维材料数据库的规模还没到可以任意挥霍的程度。我自己的经验是,先上一组手工描述符加集成学习模型做基线,效果稳定后,如果数据量确实上来了,再试探图神经网络不迟。
4. 性质预测任务怎么做才不会自己骗自己
模型训练谁都会,真正拉开差距的是任务设计和评估策略。二维材料机器学习项目里,翻车的大多不是模型本身,而是评估方法不自洽,导致模型分数虚高,拿到新体系上却漏洞百出。
4.1 先分清楚是回归、分类还是两阶段任务
很多初学者拿到二维材料带隙数据就开干回归,直接把带隙当连续值做均方误差优化。问题在于数据里混合了大量带隙为 0 的金属体系,回归模型为了压低这些样本的误差,会把整个预测区间往零拉,结果半导体带隙被系统性低估,金属体系又预测出莫名其妙的非零值。
更合理的做法是采用两阶段策略:第一步先训练一个分类模型,区分金属和半导体;第二步只对半导体样本做带隙回归。这样做一是贴合物理直觉,二是可以让两个任务各自聚焦,把“有没有带隙”和“带隙多大”当成两种不同问题看待。实际做下来,两阶段比单一回归模型在排序质量上好不少。
4.2 模型选型的基本逻辑,先看数据量再看结构复杂度
模型选择没有绝对最优,只有适不适合当前数据规模。我的判断逻辑大致是这样的:
| 数据规模 | 结构表示方式 | 推荐模型 |
|---|---|---|
| 300 条以下 | 手工描述符 | 岭回归、高斯过程回归、随机森林 |
| 几百到几千条 | 手工描述符/自动环境特征 | 随机森林、梯度提升树 |
| 数千到上万条 | 图结构直接输入 | 图神经网络(CGCNN 等) |
数据量小时,模型的归纳偏置比模型复杂度更重要。手工描述符已经把一部分物理规律写进特征里,通过随机森林这种非线性模型往往能收获相当不错的结果。反观图神经网络,在数据不足时很容易过拟合,而且调试周期长,入门阶段性价比有限。
4.3 训练集和测试集划分方式,决定了泛化能力的真实面貌
这是我认为全篇最值得停下来细想的一节。很多人直接对二维材料数据做随机划分,比如 80% 训练、20% 测试,得到 R2 = 0.93,非常漂亮。但仔细看测试集你会发现,里面有不少样本和训练集里的某些材料来自同一组成体系,只是晶格常数、原子坐标或超胞大小略作变化。
这种情况下,模型事实上是“记忆”了该材料体系的性质,而不是“理解”了一个新结构的规律。一旦你把模型用到训练集里完全没出现过的元素组合或结构原型上,精度立刻崩掉。真实筛选任务的泛化,指的是对新材料体系的外推,不是对已知材料不同构型的插值。
正确的做法是按组划分。以结构原型或材料体系作为组别,比如所有 MoS2 相关结构同属一组,所有 MoSe2 相关结构同属另一组,使用 GroupShuffleSplit 而不是普通 train_test_split 来切分数据。这种评估方式虽然让分数变难看,但它才更能反映模型在真实筛选场景下的表现。
我给一个实际例子。同一个数据集,如果随机划分,随机森林可能在带隙预测任务上做到 R2 约 0.9,MAE 约 0.1 到 0.2 eV,看起来很能打;但如果按材料体系分组,让测试集全是训练集没见过的组成,R2 掉到 0.6 甚至 0.5 都不稀奇。这不是模型的问题,而是因为跨体系外推本来就比插值难得多。你的论文或者项目报告里如果只报了随机划分的结果,审稿人大概率第一个问题就是这个。
4.4 模型的角色不是万能:不确定性、物理约束、可解释性
选模型的时候还要考虑它会不会告诉你“自己不知道”。随机森林没有天然的不确定性估计,但可以靠多棵树的预测方差近似估计,或者用集成模型的方差做参考。这对主动学习特别重要。筛选过程中,低不确定性但高预测值的结构可以放心放进候选清单,高不确定性的结构则建议优先送给 DFT 验证,因为那正是模型知识的缺口。
另一个常常被忽略的点是物理约束。预测弹性性质时,应力和应变关系必须满足对称性;预测带隙时,物理上不存在负带隙。如果模型输出违背这些基本约束,哪怕训练集指标再好看,也会在物理验证环节被拆穿。做法有几种,可以在输出层加映射函数,也可以通过添加物理正则化项来约束解空间。图神经网络里还能直接把一些全局物理量作为额外输入。
可解释性方面,随机森林和梯度提升树天然支持特征重要性分析,能直接告诉你“电负性差贡献最大”还是“d 电子数贡献最大”。这类结论对材料设计相当有价值,可以帮助你把机器学习的输出重新翻译成可以指导实验的化学直觉。
5. 实战:用随机森林做二维材料带隙初筛
理论讲太多容易飘,下面进入可以直接落地的部分。这一节我会给出完整流程和代码,目标是跑通“读取数据、构造特征、按体系分组划分、训练、评估、输出特征重要性”整个链路。
5.1 环境准备
先创建一个干净的 Python 环境。二分类和回归用到的库很简单,不需要一上来就装 PyTorch 和图神经网络全家桶。这里选 Python 3.10 搭配 scikit-learn,足够完成任务。
bash复制conda create -n matml python=3.10
conda activate matml
pip install pandas numpy scikit-learn matplotlib
先说明一下,为什么第一步不直接上复杂的库?因为这个项目里的核心任务用传统集成学习完全可以胜任,额外引入深度学习框架只会增加环境调试成本,对结果没有实际帮助。如果后面确定要跑图神经网络,再单独建环境安装对应依赖也不迟。
5.2 数据格式与读取
下面默认你已经从 C2DB、JARVIS-DFT 这类来源导出了一批二维材料数据,并整理成一个 CSV。CSV 每一行对应一个结构,列的格式类似这样:
| material | prototype | en_diff | avg_r | d_count | bandgap |
|---|---|---|---|---|---|
| MoS2 | 2H | 0.78 | 1.62 | 5 | 1.8 |
| MoSe2 | 2H | 0.56 | 1.83 | 5 | 1.4 |
| WTe2 | 1T | 0.35 | 1.87 | 4 | 0.0 |
字段含义:material 表示材料名,prototype 是结构原型,en_diff 是阴离子与阳离子电负性差,avg_r 是平均原子半径,d_count 是过渡金属 d 电子数,bandgap 是 DFT 计算得到的带隙。这里 en_diff、avg_r、d_count 就是简单的手工描述符。
下面这段代码负责读取数据并查看基本信息。
python复制import pandas as pd
df = pd.read_csv("two_dim_dataset.csv", encoding="utf-8")
print(df.shape)
print(df.head())
print(df["bandgap"].describe())
print(df["prototype"].value_counts())
读取之后先不要急着训练,认真看一下带隙最小值、最大值、分布形状,以及 prototype 的类别数量。如果带隙大量集中在 0 附近,说明金属样本过多,后面必须考虑两阶段建模而不是直接做回归。
5.3 构造特征和目标标签
把手工描述符列拼成特征矩阵,把带隙列作为标签。这里不处理缺失值,因为你的 CSV 应该已经在前面清洗完成。
python复制feature_cols = ["en_diff", "avg_r", "d_count"]
target_col = "bandgap"
X = df[feature_cols].values
y = df[target_col].values
为什么要用这三个列而不把 material 名直接当特征?因为材料名是离散字符串,机器学习模型没法从字符串本身学到规律,而且如果你把材料名编码成整数喂给模型,等价于让模型死记每个名字对应的带隙,新材料一来就彻底失效。真正能泛化的是材料背后的物理化学特征。
5.4 按结构原型分组切分数据
这一步很关键,为了贴近真实泛化场景,我们不能用普通随机划分,而是以 group 为最小不可分割单位,按 prototype 分组切分。
python复制import numpy as np
from sklearn.model_selection import GroupShuffleSplit
groups = df["prototype"].values
gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
train_idx, test_idx = next(gss.split(X, y, groups=groups))
X_train, X_test = X[train_idx], X[test_idx]
y_train, y_test = y[train_idx], y[test_idx]
这么切以后,同一个结构原型的数据不会同时出现在训练集和测试集里,模型必须面对没见过的新结构组合,评估结果比随机划分诚实得多。你可能会发现分数明显比随机划分低,这是正常现象,别慌。
5.5 训练随机森林回归器并评估
随机森林用默认参数往往已经不错,但建议把树的数量从默认的 100 提升到 300,并固定 random_state,保证结果可复现。
python复制from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import r2_score, mean_absolute_error
model = RandomForestRegressor(
n_estimators=300,
max_depth=None,
random_state=42,
n_jobs=-1
)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
r2 = r2_score(y_test, y_pred)
mae = mean_absolute_error(y_test, y_pred)
print(f"R2 = {r2:.3f}")
print(f"MAE = {mae:.3f} eV")
在我的模拟演示数据上,随机划分的 R2 可能在 0.9 上下,但按 prototype 分组以后会明显下滑。如果你的真实数据也出现类似情况,就说明模型在跨结构原型外推上还没有那么强,需要回到特征设计或数据扩充去改进。
用真实数据做出来的模型,MAE 量级会随数据集复杂度变动。一般来说,控制在 0.3 eV 以内,在初筛环节已经可以接受,因为你要的不是取代 DFT,而是把大部分没希望的体系过滤掉。
5.6 画出预测值和 DFT 真值的对比图
为了让结果更直观,我们画一幅 parity plot,横轴是 DFT 真值,纵轴是模型预测值,理想情况下所有点都落在对角线上。
python复制import matplotlib.pyplot as plt
plt.figure(figsize=(6, 6))
plt.scatter(y_test, y_pred, alpha=0.7, edgecolors="k", linewidths=0.5)
plt.plot([0, y.max()], [0, y.max()], "r--", label="Perfect Prediction")
plt.xlabel("DFT Bandgap (eV)")
plt.ylabel("ML Predicted Bandgap (eV)")
plt.legend()
plt.tight_layout()
plt.savefig("parity_plot.png", dpi=300)
如果图上出现系统性偏差,比如小带隙区域过拟合明显,说明金属/半导体混合建模问题正在显现。这时候你就应该意识到,该把回归任务拆成分类之后再做回归了。
5.7 跑一个金属/半导体二分类
只给回归代码总觉得少了点什么。下面补一段简短分类代码,对应前面提到的两阶段策略。
python复制from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, confusion_matrix
# 定义半导体:带隙大于 0.05 eV,排除数值噪声导致的非零带隙
y_cls = (df["bandgap"] > 0.05).astype(int).values
cls = RandomForestClassifier(
n_estimators=300,
random_state=42,
n_jobs=-1
)
# 这里同样用上面的分组切分索引
cls.fit(X_train, y_cls[train_idx])
y_cls_pred = cls.predict(X_test)
print("Accuracy:", accuracy_score(y_cls[test_idx], y_cls_pred))
print(confusion_matrix(y_cls[test_idx], y_cls_pred))
在实际项目中,我会先用这个分类器把所有候选结构分成“可能有带隙”和“基本是金属”两类,然后只对前者做带隙回归。这个方法比一刀切做回归更符合物理直觉,排序效果也更稳定。
