小白学习笔记,如有错误请各位大佬温和指正
使用RNN建模SMILES进行产率预测
一、运行baseline实践流程
1.打开魔塔Notebook,本次使用GPU环境(方式二)
2.整理好文件目录,解压对应压缩包
整理好之后对应目录为以下
AI+化学baseline文件包包含在Task1教程里。
在mp文件夹里需要新建dataset、model、output三个新文件夹,其中将教程网站上的vocab_full.txt词汇表放到mp文件夹下。
打开终端,解压文件。
以下各文件夹包含的内容。
code文件夹里包含压缩包里的Task1部分,以及Datawhale Task2教程页的Task2部分的ipynb代码。
dataset文件夹里盛放两个数据集.csv文件。
model文件夹里盛放RNN模型。
3.点击code文件夹里的Task2_RNN.ipynb代码进行运行。
4.运行完毕,点击output(/mp/output)文件夹,存在RNN_submit.txt文件,将其下载下来,提交到官网查看分数。
二、baseline代码解读
1.引入库
import re #导入了Python的正则表达式模块,可以用来进行文本匹配和处理。匹配注册表字符
import time #导入了时间模块,可以进行时间操作,如获取当前时间、时间格式转换等。
import pandas as pd #导入了pandas库,并使用pd作为别名。pandas是用于数据操作和分析的强大库,提供了数据结构和工具,特别适合处理结构化数据。
from typing import List, Tuple #从typing模块中导入了List和Tuple类型,用于声明变量、函数参数或返回值的类型注解。
import torch #导入了PyTorch深度学习库,PyTorch提供了张量计算、神经网络构建等功能,适合用于机器学习和深度学习任务。
import torch.nn as nn #导入了PyTorch的神经网络模块,包括定义各种层和模型的类。
import torch.optim as optim #导入了PyTorch的优化器模块,提供了各种优化算法,如SGD、Adam等。
from torch.utils.data import Dataset, DataLoader, Subset 从PyTorch的数据处理模块中分别导入Dataset、DataLoader和Subset类。这些类用于处理和加载数据集,是深度学习中常用的数据处理工具。
2.定义RNN模型
# 定义RNN模型
class RNNModel(nn.Module): #类定义
'''
num_embed: 嵌入层的大小,即词嵌入的维度。
input_size: 输入数据的特征维度。
hidden_size: RNN隐藏状态的大小(维度)。
output_size: 输出层的大小,即模型最终输出的维度。
num_layers: RNN的层数。
dropout: Dropout的比例,用于控制模型的过拟合。
device: 指定模型在哪个设备上运行,如CPU或GPU。
'''
def __init__(self, num_embed, input_size, hidden_size, output_size, num_layers, dropout, device):
super(RNNModel, self).__init__()
self.embed = nn.Embedding(num_embed, input_size) #词嵌入层,将输入的词索引映射为密集向量表示。
self.rnn = nn.RNN(input_size, hidden_size, num_layers=num_layers,
batch_first=True, dropout=dropout, bidirectional=True) #RNN层,定义了一个多层双向的循环神经网络。
self.fc = nn.Sequential(nn.Linear(2 * num_layers * hidden_size, output_size),
nn.Sigmoid(),
nn.Linear(output_size, 1),
nn.Sigmoid()) #全连接层,定义了一个顺序的全连接层网络
def forward(self, x): #定义了数据在模型中的正向传播过程。
# x : [bs, seq_len]
x = self.embed(x)
# x : [bs, seq_len, input_size]
_, hn = self.rnn(x) # hn : [2*num_layers, bs, h_dim]
hn = hn.transpose(0,1)
z = hn.reshape(hn.shape[0], -1) # z shape: [bs, 2*num_layers*h_dim]
output = self.fc(z).squeeze(-1) # output shape: [bs, 1]
return output
这段代码定义了一个包含词嵌入层、双向多层RNN和全连接层的神经网络模型。它的输入是一个批量的序列数据,输出是每个序列的二元分类结果。模型在正向传播过程中,将输入数据经过嵌入层和RNN层处理后,通过全连接层输出最终的分类结果。
3.数据处理部分
# import matplotlib.pyplot as plt
## 数据处理部分
# tokenizer,鉴于SMILES的特性,这里需要自己定义tokenizer和vocab
# 这里直接将smiles str按字符拆分,并替换为词汇表中的序号
class Smiles_tokenizer(): #定义类别
def __init__(self, pad_token, regex, vocab_file, max_length): #用于初始化 Smiles_tokenizer类的实例
self.pad_token = pad_token
self.regex = regex
self.vocab_file = vocab_file
self.max_length = max_length
with open(self.vocab_file, "r") as f:
lines = f.readlines()
lines = [line.strip("\n") for line in lines]
vocab_dic = {}
for index, token in enumerate(lines):
vocab_dic[token] = index
self.vocab_dic = vocab_dic
#初始化过程中,它读取词汇表文件并构建vocab_dic字典,将词汇表中的token与其索引对应起来。
def _regex_match(self, smiles): #使用正则表达式self.regex对输入的SMILES字符串进行匹配和分词。它返回一个列表,每个元素是一个列表,包含匹配到的token。
regex_string = r"(" + self.regex + r"|"
regex_string += r".)"
prog = re.compile(regex_string)
tokenised = []
for smi in smiles:
tokens = prog.findall(smi)
if len(tokens) > self.max_length:
tokens = tokens[:self.max_length]
tokenised.append(tokens) # 返回一个所有的字符串列表
return tokenised
def tokenize(self, smiles): #对外暴露的接口,用于将输入的SMILES字符串转换为索引化后的tokens
tokens = self._regex_match(smiles)
# 添加上表示开始和结束的token:<cls>, <end>
tokens = [["<CLS>"] + token + ["<SEP>"] for token in tokens]
tokens = self._pad_seqs(tokens, self.pad_token)
token_idx = self._pad_token_to_idx(tokens)
return tokens, token_idx
def _pad_seqs(self, seqs, pad_token): #接收一个序列列表seqs和填充的特殊符号pad_token,对序列进行填充,使它们长度相同。
pad_length = max([len(seq) for seq in seqs])
padded = [seq + ([pad_token] * (pad_length - len(seq))) for seq in seqs]
return padded
def _pad_token_to_idx(self, tokens): #将填充后的tokens转换为对应的索引列表idx_list,根据词汇表self.vocab_dic将每个token映射为其索引。
idx_list = []
for token in tokens:
tokens_idx = []
for i in token:
if i in self.vocab_dic.keys():
tokens_idx.append(self.vocab_dic[i])
else:
self.vocab_dic[i] = max(self.vocab_dic.values()) + 1
tokens_idx.append(self.vocab_dic[i])
idx_list.append(tokens_idx)
return idx_list
# 读数据并处理
def read_data(file_path, train=True):
df = pd.read_csv(file_path)
reactant1 = df["Reactant1"].tolist()
reactant2 = df["Reactant2"].tolist()
product = df["Product"].tolist()
additive = df["Additive"].tolist()
solvent = df["Solvent"].tolist()
if train:
react_yield = df["Yield"].tolist()
else:
react_yield = [0 for i in range(len(reactant1))]
# 将reactant拼到一起,之间用.分开。product也拼到一起,用>分开
input_data_list = []
for react1, react2, prod, addi, sol in zip(reactant1, reactant2, product, additive, solvent):
input_info = ".".join([react1, react2])
input_info = ">".join([input_info, prod])
input_data_list.append(input_info)
output = [(react, y) for react, y in zip(input_data_list, react_yield)]
# 下面的代码将reactant\additive\solvent拼到一起,之间用.分开。product也拼到一起,用>分开
'''
input_data_list = []
for react1, react2, prod, addi, sol in zip(reactant1, reactant2, product, additive, solvent):
input_info = ".".join([react1, react2, addi, sol])
input_info = ">".join([input_info, prod])
input_data_list.append(input_info)
output = [(react, y) for react, y in zip(input_data_list, react_yield)] #形成一个完整的输入数据字符串input_info,并存储在input_data_list中。
'''
# # 统计seq length,序列的长度是一个重要的参考,可以使用下面的代码统计查看以下序列长度的分布
# seq_length = [len(i[0]) for i in output]
# seq_length_400 = [len(i[0]) for i in output if len(i[0])>200]
# print(len(seq_length_400) / len(seq_length))
# seq_length.sort(reverse=True)
# plt.plot(range(len(seq_length)), seq_length)
# plt.title("templates frequence")
# plt.show()
return output
class ReactionDataset(Dataset): #定义了一个数据集类,用于包装处理好的数据。
def __init__(self, data: List[Tuple[List[str], float]]):
self.data = data
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
return self.data[idx]
def collate_fn(batch): #用于在数据加载器中对批量数据进行处理。
REGEX = r"\[[^\]]+]|Br?|Cl?|N|O|S|P|F|I|b|c|n|o|s|p|\(|\)|\.|=|#|-|\+|\\\\|\/|:|~|@|\?|>|\*|\$|\%[0-9]{2}|[0-9]"
tokenizer = Smiles_tokenizer("<PAD>", REGEX, "../vocab_full.txt", max_length=300)
smi_list = []
yield_list = []
for i in batch:
smi_list.append(i[0])
yield_list.append(i[1])
tokenizer_batch = torch.tensor(tokenizer.tokenize(smi_list)[1])
yield_list = torch.tensor(yield_list)
return tokenizer_batch, yield_list
4.训练集
def train():
## super param
N = 700 #int / int(len(dataset) * 1) # 或者你可以设置为数据集大小的一定比例,如 int(len(dataset) * 0.1)
NUM_EMBED = 294 # nn.Embedding()
INPUT_SIZE = 300 # src length
HIDDEN_SIZE = 512
OUTPUT_SIZE = 512
NUM_LAYERS = 10
DROPOUT = 0.2
CLIP = 1 # CLIP value
N_EPOCHS = 100
LR = 0.0001
'''
N: 用于确定训练数据集大小的一个数值。在代码中并没有直接指定具体数据集的大小,而是使用一个固定的值700作为训练集大小的参考。
NUM_EMBED: 嵌入层的维度,通常用于将输入的离散特征映射到连续向量空间。
INPUT_SIZE: 输入序列的长度。
HIDDEN_SIZE: RNN隐藏层的大小。
OUTPUT_SIZE: RNN输出层的大小。
NUM_LAYERS: RNN的层数。
DROPOUT: Dropout概率,用于模型的正则化。
CLIP: 梯度裁剪的阈值。
N_EPOCHS: 训练的轮数。
LR: 学习率。
'''
start_time = time.time() # 开始计时
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# device = 'cpu'
data = read_data("../dataset/round1_train_data.csv")
dataset = ReactionDataset(data)
subset_indices = list(range(N))
subset_dataset = Subset(dataset, subset_indices)
train_loader = DataLoader(dataset, batch_size=128, shuffle=True, collate_fn=collate_fn)
model = RNNModel(NUM_EMBED, INPUT_SIZE, HIDDEN_SIZE, OUTPUT_SIZE, NUM_LAYERS, DROPOUT, device).to(device)
model.train()
optimizer = optim.Adam(model.parameters(), lr=LR)
# criterion = nn.MSELoss() # MSE
criterion = nn.L1Loss() # MAE
best_loss = 10
for epoch in range(N_EPOCHS):
epoch_loss = 0
for i, (src, y) in enumerate(train_loader):
src, y = src.to(device), y.to(device)
optimizer.zero_grad()
output = model(src)
loss = criterion(output, y)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), CLIP)
optimizer.step()
epoch_loss += loss.item()
loss_in_a_epoch = epoch_loss / len(train_loader)
print(f'Epoch: {epoch+1:02} | Train Loss: {loss_in_a_epoch:.3f}')
if loss_in_a_epoch < best_loss:
# 在训练循环结束后保存模型
torch.save(model.state_dict(), '../model/RNN.pth')
end_time = time.time() # 结束计时
# 计算并打印运行时间
elapsed_time_minute = (end_time - start_time)/60
print(f"Total running time: {elapsed_time_minute:.2f} minutes")
if __name__ == '__main__':
train()
5.生成结果文件
# 生成结果文件
def predicit_and_make_submit_file(model_file, output_file):
NUM_EMBED = 294
INPUT_SIZE = 300
HIDDEN_SIZE = 512
OUTPUT_SIZE = 512
NUM_LAYERS = 10
DROPOUT = 0.2
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
test_data = read_data("../dataset/round1_test_data.csv", train=False)
test_dataset = ReactionDataset(test_data)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False, collate_fn=collate_fn)
model = RNNModel(NUM_EMBED, INPUT_SIZE, HIDDEN_SIZE, OUTPUT_SIZE, NUM_LAYERS, DROPOUT, device).to(device)
# 加载最佳模型
model.load_state_dict(torch.load(model_file))
model.eval()
output_list = []
for i, (src, y) in enumerate(test_loader):
src, y = src.to(device), y.to(device)
with torch.no_grad():
output = model(src)
output_list += output.detach().tolist()
ans_str_lst = ['rxnid,Yield']
for idx,y in enumerate(output_list):
ans_str_lst.append(f'test{idx+1},{y:.4f}')
with open(output_file,'w') as fw:
fw.writelines('\n'.join(ans_str_lst))
print("done!!!")
predicit_and_make_submit_file("../model/RNN.pth",
"../output/RNN_submit.txt")
三、相关知识点补充
1.Rdkit库——一个化学信息学开源工具包
(1)简述
RDKit遵循Apache 2.0许可证,任何人都可以自由使用和贡献代码。支持多种编程语言,如Python、C++和Java,用户可以根据自己的编程习惯和需求选择合适的语言进行开发。提供了简洁而直观的API,以及详细的文档和示例代码,方便用户快速上手并进行开发。集成了分子描述符计算、化学反应预测、分子对接等多种化学信息学工具和算法。兼容Windows、Linux和Mac OS等多种操作系统。
(2)功能
分子表示与操作、分子描述符计算、化学反应处理、药物设计与筛选、化学数据可视化、化学数据库管理、机器学习集成。
(3) 网址
github网站GitHub - rdkit/rdkit: The official sources for the RDKit library
2.RNN模型
(1)介绍
循环神经网络(Recurrent Neural Network, RNN)是一种经典的神经网络架构,专门用于处理序列数据,例如文本、语音、时间序列等。与传统的前馈神经网络不同,RNN具有记忆功能,可以利用前面的信息来帮助处理后面的输入。这种记忆能力使得RNN在许多序列建模任务中表现出色。
(2)框架图
RNN的基本结构包括输入层、隐藏层和输出层。隐藏层的输出会作为下一个时间步的输入,形成循环结构,这也是RNN名称的来源。以下是一个简单的RNN框架图示例:
x_t h_t y_t
| | |
V V V
[ ] --> [ ] --> [ ]
^ ^ ^
| | |
x_{t+1} h_{t+1} y_{t+1}
其中( x_t ) 是第 t 个时间步的输入,(h_t ) 是第 t 个时间步的隐藏状态,(y_t ) 是第 t 个时间步的输出箭头表示数据流动的方向。
(3)优点和缺点
优点:
能够处理变长的输入序列,适合序列建模任务。具有记忆功能,可以捕捉长期依赖关系。参数共享,使得模型比较轻量。
缺点:
长期依赖问题:在长序列中,难以捕捉较长时间跨度的依赖关系,可能出现梯度消失或爆炸问题。计算效率低:每个时间步的计算依赖于前一个时间步的输出,不利于并行计算。对输入序列的顺序敏感:输入的顺序对最终的结果有较大影响。
参考文献
-
Hochreiter, S., & Schmidhuber, J. (1997). Long short-term memory. Neural computation, 9(8), 1735-1780.
-
Graves, A., & Schmidhuber, J. (2005). Framewise phoneme classification with bidirectional LSTM and other neural network architectures. Neural networks, 18(5-6), 602-610.
-
Cho, K., et al. (2014). Learning phrase representations using RNN encoder-decoder for statistical machine translation. arXiv preprint arXiv:1406.1078. -