Datawhale AI夏令营——催化反应产率预测方向Task2

小白学习笔记,如有错误请各位大佬温和指正

使用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

RDKit

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)优点和缺点

优点:

       能够处理变长的输入序列,适合序列建模任务。具有记忆功能,可以捕捉长期依赖关系。参数共享,使得模型比较轻量。

缺点:

        长期依赖问题:在长序列中,难以捕捉较长时间跨度的依赖关系,可能出现梯度消失或爆炸问题。计算效率低:每个时间步的计算依赖于前一个时间步的输出,不利于并行计算。对输入序列的顺序敏感:输入的顺序对最终的结果有较大影响。

参考文献

  1. Hochreiter, S., & Schmidhuber, J. (1997). Long short-term memory. Neural computation, 9(8), 1735-1780.

  2. Graves, A., & Schmidhuber, J. (2005). Framewise phoneme classification with bidirectional LSTM and other neural network architectures. Neural networks, 18(5-6), 602-610.

  3. Cho, K., et al. (2014). Learning phrase representations using RNN encoder-decoder for statistical machine translation. arXiv preprint arXiv:1406.1078. -

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值