代码概述
RNN建模SMILES进行反应产率预测
学习目标
能上手跑通baseline
读懂、理解代码
Ai4Chemistry知识点补充
SMILES —— 最流行的将分子表示为序列类型数据的方法
SMILES,是一种将化学分子用ASCII字符表示的方法,在化学信息学领域有着举足轻重的作用。当前对于分子和化学式的储存形式,几乎都是由SMILES(或者它的一些手足兄弟)完成的。
SMILES将化学分子中涉及的原子、键、电荷等信息,用对应的ASCII字符表示;环、侧链等化学结构信息,用特定的书写规范表达。以此,几乎所有的分子都可以用特定的SMILES表示,且SMILES的表示还算比较直观。
表1:一些常见的化学结构用SMILES表示。
表2:化学反应也可以用SMILES表示,用“>>”连接产物即可。
表3:一些常见分子的SMILES例子
在SMILES中,原子由他们的化学符号表示,=表示双键、#表示三键、[]里面的内容表示侧基或者特殊原子(例如[Cu+2]表示带电+2电荷的Cu离子)。通过SMLIES,就可以把分子表示为序列类型的数据了。
(注:SMILES有自己的局限性:例如选择不同的起始原子,写出来的SMILES不同;它无法表示空间信息。)
分子指纹 —— 分子向量化
分子的指纹就像人的指纹一样,用于表示特定的分子。分子指纹是一个具有固定长度的位向量(即由0,1组成),其中,每个为1的值表示这个分子具有某些特定的化学结构。通常,分子指纹的维度都是上千的,也即记录了上千个子结构是否出现在分子中。
特点:
1.非常稀疏。大量的0,少量的1;
2.指纹和分子并一一对应。
但它也有其局限性,例如像氯甲基苯的邻位和对位,采用分子指纹的形式就无法区分出来。
RDkit —— 强大、丰富且高效的化学信息工具
RDkit是化学信息学中主要的工具,是开源的。网址:http://www.rdkit.org,支持WIN\MAC\Linux,可以被python、Java、C调用。几乎所有的与化学信息学相关的内容都可以在上面找到。常用的功能包括但不限于:
-
读和写分子;
-
循环获取分子中原子、键、环的信息;
-
修饰分子;
-
获取分子指纹;
-
计算分子相似性;
-
将分子绘制为图片;
-
子结构匹配和搜索;
-
生成和优化3D结构。
机器学习
机器学习按照目标可以分为分类(classification)任务和回归(regression)任务两大类。
分类任务就是模型预测的结果是离散的值,例如类别;
回归任务就是模型预测的结果是连续的值,例如房价。
此次需要预测的目标是反应的产率,是0-1之间的一个连续的数值,所以是一个回归任务。(注:离散值通过一些处理可以近似认为是连续值)
传统的机器学习需要需要经历特征工程这一步骤,即将原始数据转化为向量形式。然后通过SVM、Random Forest等算法学习数据的规律。这些方法在处理简单的任务时是比较有效的。
图2 决策树 (左)分类型决策树,(右)回归型决策树
在分类任务中,常见的就是信息熵衡量;在回归任务中,可以使用均方误差、绝对误差等进行衡量。
图2 随机森林
将多个决策树结合在一起,训练每个决策树的数据集都是随机有放回地从原数据中选出。预测的时候,输入会通过每个决策树进行预测,然后考虑每个树地输出结果,得到最终的预测值。
深度学习
深度学习可以归为机器学习的一个子集,主要通过神经网络学习数据的特征和分布。深度学习的一个重要进化是不再需要繁琐的特征工程,让神经网络自己从里面学习特征。
SMILES是一种以ASCII组成的序列,可以被理解为一种“化学语言”。既然是一种语言,那么很自然地想到了可以使用NLP中的方法对SMILES进行建模。
使用RNN对SMILES建模是早期的一个主要方法。RNN(Recurrent Neural Network)是处理序列数据的一把好手。RNN的网络每层除了会有自己的输出以外,还会输出一个隐向量到下一层。
图2 RNN的架构示意图
其中,每一层相当于做了一次线性变换。
通过隐向量的不断传递,序列后面的部分就通过“阅读”隐向量,获取前面序列的信息,从而提升学习能力。
但是RNN也有缺点:如果序列太长,那么两个相距比较远的字符之间的联系需要通过多个隐藏向量。这就像人和人之间传话一样,传递的人多了,很容易导致信息的损失或者扭曲。因此,它对长序列的记忆能力较弱。
同时,RNN需要一层一层地传递,所以并行能力差,比较容易出现梯度消失或梯度爆炸问题。
定义RNN模型
# 定义RNN模型
class RNNModel(nn.Module):
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)
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
RNNModel是一个继承自nn.Module
的类,它定义了一个使用循环神经网络(RNN)的模型。nn.Module
是PyTorch中所有神经网络模块的基类,定义的模型需要继承这个类并实现forward
方法,该方法定义了模型的前向传播逻辑。
定义模型训练函数进行模型训练
def train():
## super param
N = 10 #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
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()
N
:从数据集中选取的样本数量,用于调试或小规模测试。它有助于快速迭代模型设计,避免在全部数据上训练时消耗过多时间和资源。较小的N可以快速反馈模型性能,但可能不够代表整个数据集的分布。NUM_EMBED
:嵌入层的大小。嵌入层通常用于将输入序列中的每个元素(如单词、字符等)转换为一个固定大小的密集向量。NUM_EMBED定义了这些向量的维度。较大的嵌入尺寸可以捕获更多的语义信息,但也会增加模型的复杂性和计算成本。INPUT_SIZE
:输入序列的长度。在RNN中,序列中的每个元素都会依次被处理。较长的输入序列意味着模型可以捕获更长的依赖关系,但也会增加计算量和训练难度。HIDDEN_SIZE
:RNN隐藏层的大小。隐藏层是RNN中处理输入序列并产生输出的关键部分。HIDDEN_SIZE定义了隐藏层中神经元的数量。较大的隐藏层可以捕获更复杂的模式,但也可能导致过拟合和训练时间增加。OUTPUT_SIZE
:输出层的大小。取决于任务的需求。NUM_LAYERS
:RNN的层数。多层RNN(也称为深度RNN)可以捕获更高层次的抽象特征,但也可能导致梯度消失或梯度爆炸的问题,特别是在训练深层网络时。DROPOUT
:Dropout比率,用于减少过拟合。Dropout是一种正则化技术,通过在训练过程中随机丢弃(即将输出设置为0)网络中的一部分神经元来减少过拟合。DROPOUT比率指定了被丢弃神经元的比例。适当的Dropout比率可以提高模型的泛化能力。Dropout的取值介于0-0.5,为了效率和结果考虑也常取0.2,最终还是以实际需求和目标为准。CLIP
:梯度裁剪的值,用于防止梯度爆炸。在训练过程中,为了防止梯度爆炸(即梯度值变得异常大),可以设置一个梯度裁剪值。如果梯度的绝对值超过了这个值,就会被裁剪到这个值。这有助于保持训练的稳定性。N_EPOCHS
:训练的轮数。更多的轮数通常意味着模型有更多的机会学习数据中的模式,但也可能导致过拟合。LR
:学习率。学习率决定了在梯度下降(或其他优化算法)中参数更新的步长。较大的学习率可以加速训练过程,但可能导致训练不稳定;较小的学习率则可能使训练过程过于缓慢。选择合适的学习率对于模型的收敛速度和最终性能至关重要。
在构建和训练RNN(或任何深度学习模型)时,上述提到的多个参数都是可以根据需要进行调整以优化模型性能的。
初次使用默认数据进行运行时得到的结果是0.0261,感觉中规中矩,但是运行了超级久,差不多3.6h;然后把Dropout的值改成了0.4,得到的结果大幅度上升,达到了0.0840,可能是减少了过拟合;最后我将HIDDEN_SIZE = 550,NUM_LAYERS = 15,Dropout = 0.5,得到的结果为0.0869,略有上升。
从第二次到第三次尝试变化Dropout的值增大了,增加了正则化的强度;同时增加层数(从10到15)和隐藏层大小(从512到550),进而增加了模型的容量,如果数据集足够大且包含足够复杂的模式,那么增加模型容量可能有助于模型更好地拟合数据,从而提高性能。当然也可能是训练过程的随机性导致的这次结果的上升,因为上升的幅度并不明显。