Datawhale AI夏令营-AI+物质化学 Task2 学习笔记

代码概述

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调用。几乎所有的与化学信息学相关的内容都可以在上面找到。常用的功能包括但不限于:

  1. 读和写分子;

  2. 循环获取分子中原子、键、环的信息;

  3. 修饰分子;

  4. 获取分子指纹;

  5. 计算分子相似性;

  6. 将分子绘制为图片;

  7. 子结构匹配和搜索;

  8. 生成和优化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),进而增加了模型的容量,如果数据集足够大且包含足够复杂的模式,那么增加模型容量可能有助于模型更好地拟合数据,从而提高性能。当然也可能是训练过程的随机性导致的这次结果的上升,因为上升的幅度并不明显。

  • 7
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值