作业1:理解赛题
🤔:本题任务是什么?这个任务是典型的什么问题?输入与输出是什么?
聚焦于 3D分子构象条件生成, 旨在通过生成式AI技术解决材料科学领域的核心挑战。
相关领域难点
-
实验方法面临 高昂成本 与 复杂操作流程, 尤其在新材料、催化剂设计中分子结构数据严重匮乏。
-
理论计算依赖 海量算力资源 (如量子力学计算),生成效率低,难以快速满足科研需求。
竞赛分为初赛(生成合理分子结构)和复赛(根据能量值定制分子),要求选手以人工智能方法生成符合 价键理论 且具有潜在应用价值的新分子。
赛题解读
初赛任务:生成合理的的 3D 分子结构 。选手通过训练集训练 AI 生成式模型,再用该模型生成 1 万个分子的 3D 结构(由原子元素种类和原子坐标表示,无顺序约束),后台会评测生成分子的合理性以及是否新颖。
输入数据: 分子构象数据,文件格式为 .pkl
,是用来训练模型的分子样本
由一个包含若干训练样本的 list 构成,每个样本包含一个 dict,字段说明如下:
字段 | 中文名 | 数据类型 | 说明 |
| 原子数量 | int | 表示当前样本总共的原子数量 |
| 原子元素种类 | List[str] | 描述当前样本的组成原子种类,第 i 个元素表示第 i 个原子的元素种类 |
| 原子坐标 | List[List[float]] | 描述当前样本每个原子的 3D 位置,顺序与 |
样本数量 | 元素种类 | 单分子最大构象数量 |
4-5 万 | C,H,O,N,F,P,S,Cl,Br | 1 |
输出数据 :分子构象数据,文件格式为 .pkl
,生成的10000个分子结构
应通过基于输入数据训练出来的模型生成。 生成时,分子大小的分布,各元素出现频率的分布须与给定数据集一致;生成最大的分子的原子数不允许超过60,否则会影响最终提交结果及分数。 。文件中,所有分子数据存在一个列表中,每个分子表示为一个dict类,具有三个key:'natoms', 'elements', 'coordinates'。他们的value需要服从以下数据格式要求:
-
'natoms'
: int. # example: 46 -
'elements'
: list[str] # example:['C', 'H', 'O']
-
'coordinates'
: list[list[float, float, float], list[float, float, float], ...] # example:[[1.2, 0.5, -0.2], [0.54, -0.41, 1.45]]
文件中生成的分子数目需严格等于10000。其它数目的提交文件被判作废。
评价指标
首先根据真实的键长键角,判断生成分子中存在的化学键,根据化学键将 3D 分子映射为 2D 的图结构。
-
有效性 :根据价键理论判断原子和分子稳定性;利用 RDKiT 库函数及一些自定义规则判断分子是否合理,统计合理分子占总生成分子数的比例。
-
唯一性 :筛选具有有效性的去除所有重复分子,计算剩下不重复分子占总生成分子数的比例。
-
创新性 :在去重的分子中,统计训练集中未出现的分子占总生成分子数的比例。
-
数据集分布相似性(反作弊评分) :赛事方后台会计算选手提交文件和训练集分布的差异,严重脱离训练集分布的结果会被标记为异常结果,并会影响分数,赛事方会进行相应核查。
最终分数由 1,2,3 分数加权组合得到,对于满足原则的生成结果, 数据集分布相似性不会影响分数 。
🤔:我们需要用到的数据有哪些?长什么样子
import pickle
import sys
import pprint
def print_pkl_content(file_path):
"""打印pkl文件的内容"""
try:
with open(file_path, 'rb') as f:
data = pickle.load(f)
print(f"数据类型: {type(data)}")
if isinstance(data, dict):
print("\n字典键:")
for key in data.keys():
print(f" - {key} (类型: {type(data[key])})")
print("\n详细内容:")
pp = pprint.PrettyPrinter(indent=2)
pp.pprint(data)
elif isinstance(data, list):
print(f"\n列表长度: {len(data)}")
if len(data) > 0:
print(f"第一个元素类型: {type(data[0])}")
print("\n前几个元素:")
pp = pprint.PrettyPrinter(indent=2)
pp.pprint(data)
else:
print("\n内容:")
pp = pprint.PrettyPrinter(indent=2)
pp.pprint(data)
except Exception as e:
print(f"打开文件时出错: {e}")
if __name__ == "__main__":
file_path = "/root/competition/data_all.pkl"
print_pkl_content(file_path)
一开始我们可能会觉得 赛事提供的 data_all.pkl 和提供的 baseline 的中包含的数据不一致,
但事实上是一致的 但是赛事方帮我们把原本的数据进行拆解为三个子集,并且按照训练的输入输出进行了转化,可以从下面的代码去观察整个 baseline 训练数据的分布和数据的输入格式,注意修改 directory_path
import pickle
import numpy as np
import os
import pprint
from pathlib import Path
def print_pickle_content(file_path):
"""打印pickle文件的内容"""
try:
with open(file_path, 'rb') as f:
data = pickle.load(f)
print(f"文件: {os.path.basename(file_path)}")
print(f"数据类型: {type(data)}")
if isinstance(data, dict):
print("\n字典键:")
for key in data.keys():
print(f" - {key} (类型: {type(data[key])})")
# 打印字典值的更多信息
if isinstance(data[key], np.ndarray):
print(f" 形状: {data[key].shape}, 数据类型: {data[key].dtype}")
if data[key].size < 10:
print(f" 内容: {data[key]}")
else:
print(f" 前几个元素: {data[key].flatten()[:5]}...")
elif isinstance(data[key], list):
print(f" 长度: {len(data[key])}")
if len(data[key]) < 5:
print(f" 内容: {data[key]}")
else:
print(f" 前几个元素: {data[key][:5]}...")
# 对于较小的字典,打印完整内容
if len(data) < 5:
print("\n完整内容:")
pp = pprint.PrettyPrinter(indent=2)
pp.pprint(data)
elif isinstance(data, list):
print(f"\n列表长度: {len(data)}")
if len(data) > 0:
print(f"第一个元素类型: {type(data[0])}")
print("\n前几个元素:")
pp = pprint.PrettyPrinter(indent=2)
pp.pprint(data[:5] if len(data) > 5 else data)
else:
print("\n内容:")
pp = pprint.PrettyPrinter(indent=2)
pp.pprint(data)
except Exception as e:
print(f"打开pickle文件时出错: {e}")
def print_npz_content(file_path):
"""打印npz文件的内容"""
try:
data = np.load(file_path)
print(f"文件: {os.path.basename(file_path)}")
print(f"数据类型: {type(data)}")
if isinstance(data, np.lib.npyio.NpzFile):
print("\n数组键:")
for key in data.files:
array = data[key]
print(f" - {key}: 形状 {array.shape}, 数据类型 {array.dtype}")
if array.size < 10:
print(f" 内容: {array}")
else:
if array.ndim > 1:
print(f" 第一行内容: {array[0]} 长度为{len(array[0])}")
else:
print(f" 前几个元素: {array[:5]}...")
else:
print(f"形状: {data.shape}, 数据类型: {data.dtype}")
if data.size < 10:
print(f"内容: {data}")
else:
print(f"前几个元素: {data.flatten()[:5]}...")
except Exception as e:
print(f"打开npz文件时出错: {e}")
def print_directory_contents(directory_path):
"""打印目录中所有文件的内容"""
dir_path = Path(directory_path)
if not dir_path.exists() or not dir_path.is_dir():
print(f"错误: {directory_path} 不是一个有效的目录")
return
files = list(dir_path.glob("*"))
print(f"目录 {directory_path} 中共有 {len(files)} 个文件")
for file_path in files:
print("\n" + "="*50)
print(f"处理文件: {file_path}")
if file_path.suffix.lower() in ('.pickle', '.pkl'):
print_pickle_content(file_path)
elif file_path.suffix.lower() == '.npz':
print_npz_content(file_path)
else:
print(f"不支持的文件类型: {file_path.suffix}")
print("="*50)
if __name__ == "__main__":
directory_path = "/root/competition/data/competition"
print_directory_contents(directory_path)
如果赛事方更新了训练数据,或者希望自己重新切分数据比例,或者针对数据进行增强,需要额外处理数据的格式,在这里我们给出数据切分代码,由 pkl 转为 train、valid、test 的三个 npz 文件。
import pickle
from sklearn.model_selection import train_test_split
import numpy as np
import pandas as pd
from copy import deepcopy
all_data = pickle.load(open('data_all.pkl', 'rb'))
all_data = pd.DataFrame(all_data)
natoms_data = all_data['natoms'].to_numpy()
atom_dict = {
"C": 6,
"H": 1,
"O": 8,
"N": 7,
"F": 9,
"P": 15,
"S": 16,
"Cl": 17,
"Br": 35,
}
elements_data = []
for element in all_data["elements"]:
element_list = []
for atom in element:
element_list.append(atom_dict[atom])
element_list = np.array(element_list)
element_list = np.pad(element_list, (0, 60 - len(element_list)), mode="constant", constant_values=0)
element_list = element_list[:60]
elements_data.append(element_list)
elements_data = np.array(elements_data)
coor_list = []
for element in all_data["coordinates"]:
element = deepcopy(element)
if len(element) < 60:
element.extend([[0.0, 0.0, 0.0]] * (60 - len(element)))
element = np.array(element)
element = element[:60]
coor_list.append(element)
coor_data = np.array(coor_list)
train_natoms, temp_natoms = train_test_split(natoms_data, test_size=0.2, random_state=42)
val_natoms, test_natoms = train_test_split(temp_natoms, test_size=0.5, random_state=42)
train_elements, temp_elements = train_test_split(elements_data, test_size=0.2, random_state=42)
val_elements, test_elements = train_test_split(temp_elements, test_size=0.5, random_state=42)
train_coor, temp_coor = train_test_split(coor_data, test_size=0.2, random_state=42)
val_coor, test_coor = train_test_split(temp_coor, test_size=0.5, random_state=42)
train_data = np.savez_compressed('train.npz', natoms=train_natoms, charges=train_elements, positions=train_coor)
val_data = np.savez_compressed('valid.npz', natoms=val_natoms, charges=val_elements, positions=val_coor)
test_data = np.savez_compressed('test.npz', natoms=test_natoms, charges=test_elements, positions=test_coor)
‘/
num_atoms: [17 21 19 ... 25 32 18] 表示为原子数量
charges:[ 6 6 8 8 6 6 6 7 6 16 1 1 1 1 1 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0] 长度为60 表示为对应的原子序列数 赛事要求生成的原子序列数得小于60
这样的输入符合条件
positions:[[-3.21805733e+00 7.10018754e-01 2.50179573e-01]
[-2.23032949e+00 -4.05445001e-01 1.49888750e-02]
[-2.50381737e+00 -1.55804960e+00 -2.07941942e-01]
[-9.58817155e-01 5.01882687e-02 8.09005480e-02]
[ 7.36242693e-02 -9.30148255e-01 -1.24097884e-01]
[ 1.40729161e+00 -2.75227023e-01 -9.75905600e-04]
[ 2.37120845e+00 -1.26356958e-01 -9.68925145e-01]
[ 3.52556242e+00 5.03332756e-01 -5.88108377e-01]
[ 3.46803644e+00 8.49552271e-01 6.62381934e-01]
[ 1.98850575e+00 4.26801859e-01 1.48299858e+00]
[-3.08777001e+00 1.49176138e+00 -5.14021375e-01]
[-4.23860031e+00 3.11550965e-01 2.09990471e-01]
[-3.03139238e+00 1.17887217e+00 1.22840531e+00]
[-6.09373042e-02 -1.74530731e+00 6.05643289e-01]
[-3.04329331e-02 -1.37704055e+00 -1.12549782e+00]
[ 2.26174413e+00 -4.81177501e-01 -1.99641722e+00]
[ 4.26418121e+00 1.37657377e+00 1.19419709e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00]] 长度为60 表示为原子在3d空间的位置分布
如何来解决这个题目?/
使用赛事官方提供的等变扩散模型(EDM),这是基于 Diffusion Model 的一种变种模型,加入了 E(3) 等变性。
E(3) 等变性'
在固体材料的晶体结构中,存在着一些特定的变化规律。我们想象一个在桌上放着的小球,现在有一个力施加在小球上面,现在将这个小球向右平移 10 厘米,这个力的方向和大小有什么变化?这个小球的势能有什么变化?
答案是:力和势能都不发生变化。
那同样的问题,这时候我们旋转小球呢?小球的势能依旧不变,但是这时候它的受力会跟着坐标轴的旋转而一起旋转。另外还有种情况就是镜像对称,假设有个魔法镜子,能生成一个镜像小球,那这个镜像小球的受力和势能会怎么变化呢?结果应该是势能不变,但是受力会随着镜面(实际是坐标轴)一起对称。
晶体是由许多个原子组成,在不考虑复杂的固体物理知识和宇称不守恒的前提下,我们可以将小球上会发生的事情套用在晶体中的原子上,我们发现,在材料的晶体结构中似乎存在着三种特殊的变化,它们分别与平移、旋转和镜面对称相关,这就是所谓的 E(3) 等变性
🤔:baseline里有哪些文件、有什么作用?吃透需要关注哪些核心函数?
训练脚本 train.sh
注意请不要修改 dataset 的参数
因为"competition"参数默认使用赛事要求的对应的元素种类【C,H,O,N,F,P,S,Cl,Br】 并且限制最大的分子原子数为 60。
参数设计小 tips
如果发现提交结果分数为 0 主要是因为设置的 n_epochs 太小在原本的 baseline 如果 epochs 小于 30 基本都为 0 可以尝试调大 layers 注意 diffusion_steps 没必要调太大(但也不能太小,500 合适) 这样会导致需要耗费很多的时间在后面生成分子上
##train.sh
python main.py --n_epochs 100 \
--exp_name edm_competition \
--n_stability_samples 1000 \
--diffusion_noise_schedule polynomial_2 \
--diffusion_noise_precision 1e-5 \
--diffusion_steps 500 \
--diffusion_loss_type l2 \
--ema_decay 0.9999 \
--dataset "competition" \
--datadir "./data/competition"
生成分子的命令
需要修改的是--model_path 后的路径得是你训练后的路径 一般保存在 outputs 下面 然后 n_samples 不要修改一定要为 10000 赛事要求生成的分子数量得为 10000
python sample.py --model_path ./outputs/edm_competition_20250321_171233 \
--n_samples 10000
进阶Task2:深入赛题 丝滑实现
节省算力 --- 无卡启动
jupyterlab 进入 /root/sais_third_material_baseline 目录
diffusion_steps
应该是扩散过程的总步数,diffusion_noise_schedule
是噪声的分布策略,diffusion_noise_precision
是噪声的精度,diffusion_loss_type
是损失函数的选择
parser.add_argument('--diffusion_steps', type=int, default=1000) # 增加步数
parser.add_argument('--diffusion_noise_schedule', type=str, default='cosine') # 使用余弦调度
parser.add_argument('--diffusion_noise_precision', type=float, default=1e-6) # 增加精度
parser.add_argument('--diffusion_loss_type', type=str, default='vlb') # 使用 VLB 损失
# parser.add_argument("--conditioning", nargs='+', default=[],
# help='arguments : homo | lumo | alpha | gap | mu | Cv' )
一
parser.add_argument("--conditioning", nargs='+', default=['homo', 'lumo', 'gap'],
help='arguments : homo | lumo | alpha | gap | mu | Cv')
二
parser.add_argument("--conditioning", nargs='+', default=['mu', 'Cv'],
help='arguments : homo | lumo | alpha | gap | mu | Cv')
三
parser.add_argument("--conditioning", nargs='+', default=['homo', 'lumo', 'gap'],
help='arguments : homo | lumo | alpha | gap | mu | Cv')
保存并关机
突然变一卡了 ,非常纳闷
可以设一个200min左右的定时关机
edm_competition_resume_xxxx 目录替换到下面的命令中
python sample.py --model_path ./outputs/替换到这里 --n_samples 10000
/competition/outputs/debug_10_20250424_120247
python sample.py --model_path ./outputs/debug_10_20250424_120247 --n_samples 10000
进阶三 - -
Dataset 数据集
Offical Dataset 官方数据集
The offical raw GEOM dataset is avaiable [here].
官方原始 GEOM 数据集可在[此处]获取。
Preprocessed dataset 预处理数据集
We provide the preprocessed datasets (GEOM) in this [google drive folder]. After downleading the dataset, it should be put into the folder path as specified in the dataset
variable of config files ./configs/*.yml
.
我们在此[谷歌驱动文件夹]中提供了预处理的 GEOM 数据集( ./configs/*.yml
)。下载数据集后,应将其放入配置文件中指定的 dataset
变量路径下。
Prepare your own GEOM dataset from scratch (optional)
从头开始准备自己的 GEOM 数据集(可选)
You can also download origianl GEOM full dataset and prepare your own data split. A guide is available at previous work ConfGF's [github page].
您也可以下载原始 GEOM 完整数据集并准备您自己的数据分割。指南可在之前工作的 ConfGF 的[github 页面]找到。
https://dataverse.harvard.edu/dataset.xhtml?persistentId=doi:10.7910/DVN/JNGTDF
原始数据就不下载了
Preprocessed dataset 预处理数据集 下载
python train.py /config/drugs_default.yml
参考——————————————————————————————————————————————————
原笔迹
第三届世界科学智能大赛材料设计赛道:3D分子构象条件生成
http://competition.sais.com.cn/competitionDetail/532312/format
赛题和数据
http://competition.sais.com.cn/competitionDetail/532312/competitionData
Task
然而,在许多特定领域(如新材料设计、催化剂设计等),分子结构数据往往存在严重匮乏的现象。实验方法受限于高昂的成本与复杂性,而理论计算则需要投入大量时间和计算资源,难以快速填补这一空白。为应对这一挑战,本次比赛以“分子设计”为主题,聚焦生成式AI在3D分子结构生成中的应用潜力,期待选手利用生成式模型建立有效的3D结构生成模型,探索如何利用人工智能技术高效生成具有潜在应用价值的分子结构。
选手可以获取赛事方提供的小分子结构数据集,其中包含某一领域分子的结构信息。参赛者需基于该数据,训练生成式AI模型,完成以下目标
初赛
任务:广泛生成可能存在的3D分子结构。生成的分子需符合物理化学规则并保证新颖性。具体任务如下:
-
训练生成模型后,无条件地生成1万个分子的3D结构(由原子元素种类和原子坐标表示,无顺序约束)。后台会用相关指标判断分子的生成质量和新颖性。
PS注意
选手必须通过训练集训练AI生成式模型,再用该模型生成1万个分子的3D结构(由原子元素种类和原子坐标表示,无顺序约束)
每天可提交次数 3 次(提交的结果output.pkl, 不能超过 100M)
competition
https://gitcode.com/gh_mirrors/e3/e3_diffusion_for_molecules
我们先跑一遍baseline,打个分
依据readme创建虚拟环境
## 配置环境
```
conda create -n com python==3.12.0
conda activate com
pip install -r /group_share/3Dmol/competition/requirements.txt
```
cd /group_share/3Dmol/competition
## 训练EDM模型
```
bash train.sh ## 50% A100 左右启动训练
```
## 采样生成数据
```
python sample.py --model_path "./outputs/competition" \
--n_samples
```
Clipped gradient with value 50.3 while allowed 5.9
梯度裁剪(Gradient Clipping)机制正在保护你的模型训练过程
python sample.py --model_path ./outputs/edm_competition_20250328_224703 \ --n_samples 10
查看目录
(com) root@intern-studio-50153054:/group_share/3Dmol# tree -L 3
.
|-- __MACOSX
| `-- competition
|-- attempt.ipynb #新建
|-- competition
| |-- README.md
| |-- __pycache__
| | `-- utils.cpython-310.pyc
| |-- configs
| | |-- __pycache__
| | |-- datasets_config.py
| | `-- qm9_config.yaml
| |-- data
| | `-- competition
| |-- egnn
| | |-- __pycache__
| | |-- egnn.py
| | |-- egnn_new.py
| | `-- models.py
| |-- equivariant_diffusion
| | |-- __init__.py
| | |-- __pycache__
| | |-- distributions.py
| | |-- en_diffusion.py
| | |-- overview.png
| | |-- training.png
| | `-- utils.py
| |-- main.py
| |-- qm9
| | |-- __init__.py
| | |-- __pycache__
| | |-- analyze.py
| | |-- bond_analyze.py
| | |-- data
| | |-- dataset.py
| | |-- losses.py
| | |-- models.py
| | |-- property_prediction
| | |-- rdkit_functions.py
| | |-- sampling.py
| | |-- utils.py
| | `-- visualizer.py
| |-- requirements.txt
| |-- sample.py
| |-- sample.sh
| |-- train.sh
| |-- train_test.py
| `-- utils.py
|-- competition.zip
|-- data
| |-- data_all.pkl
| `-- data_all.zip
`-- data.csv # pkl 转的
17 directories, 35 files
python sample.py --model_path ./outputs/edm_competition_20250329_225950 --n_samples 1000 ## X 10 = 10000
相关模型github地址与文章
https://github.com/MinkaiXu/GeoDiff
https://mp.weixin.qq.com/s/Qx5U0xhdlfu7vUmKQY2Q6A
https://github.com/tencent-ailab/MDM
https://zhuanlan.zhihu.com/p/668287513