化学科学催化反应产率预测之RNN建模SMILES(Datawhale AI 夏令营)

将化学知识转化为计算机可识别的形式

        化学学科包含许多复杂的知识,如元素周期表、分子式、化学反应方程等,这些内容通常用文本和化学专有符号记录,计算机难以直接理解。然而,随着计算机技术的发展,化学知识的数字化存储方法也在不断进步。

AI4Chemistry知识点补充

AI4Science的早期历史

        AI4Science的发展经历了三个阶段:

  1. 将化学知识以计算机形式存储,并构建数据库;
  2. 机器学习;
  3. 深度学习。

第一个阶段: 人们尝试使用不同的方法将化学知识和信息以计算机的形式进行存储,并开始构建数据库。例如,用一些字符表示分子或其他化学符号,保存分子的原子、键的位置信息等。

第二个阶段: 使用手动的特征工程对已有数据进行编码、特征提取等操作。例如,使用分子指纹(molecule fingerprint)作为编码方式,并辅以传统的机器学习方法进行预测。

第三个阶段: 各种深度神经网络开始被广泛使用,学习各种特征并对分子进行向量化。这也导致了许多新型的分子指纹的出现。

SMILES:序列类型数据表示方法

        SMILES(Simplified Molecular Input Line Entry System)是一种将化学分子用ASCII字符表示的方法。SMILES在化学信息学领域有着重要的作用,并被广泛用于分子和化学式的储存形式。常见的分子/反应数据库如ZINC、ChemBL、USPTO等都采用这种形式。

SMILES表示法:

  • 原子由化学符号表示。
  • 双键用=表示,三键用#表示。
  • 侧基或特殊原子用[]表示。

        SMILES可以将分子表示为序列类型的数据,但它无法表示空间信息。

分子指纹:分子向量化

        分子指纹是用于表示特定分子的固定长度的位向量,其中每个为1的值表示分子具有某些特定的化学结构。分子指纹的维度通常为上千,记录了上千个子结构是否出现在分子中。

RDkit:强大的化学信息工具

        RDkit是化学信息学中的主要工具,是一个开源项目,支持多个平台和编程语言。它提供了丰富的功能,包括读写分子、获取分子信息、修饰分子、计算分子指纹、绘制分子图片、子结构匹配和搜索、生成和优化3D结构等。

机器学习与深度学习

机器学习:

  • 分类任务(Classification):模型预测的结果是离散值,如类别。
  • 回归任务(Regression):模型预测的结果是连续值,如房价。

深度学习:

  • 通过神经网络学习数据的特征和分布,不需要繁琐的特征工程。
  • SMILES被视为一种“化学语言”,可以使用NLP中的方法对其进行建模。

使用RNN对SMILES建模

        RNN(Recurrent Neural Network)适用于处理序列数据,通过引入循环连接捕捉序列中的时间依赖关系。然而,RNN对长序列的记忆能力较弱,并行能力差,容易出现梯度消失或梯度爆炸问题。

导入必要的库

        首先,我们导入了必要的库,包括文件操作、深度学习框架、数据处理和模型评估等工具。

import re
import time
import pandas as pd
from typing import List, Tuple
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader, Subset
定义RNN模型

        定义一个RNN模型,用于处理SMILES序列数据。该模型包括嵌入层、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 = self.embed(x)
        _, hn = self.rnn(x)
        hn = hn.transpose(0,1)
        z = hn.reshape(hn.shape[0], -1)
        output = self.fc(z).squeeze(-1)
        return output
数据处理和tokenizer

        定义一个tokenizer,用于将SMILES序列分割成token,并将这些token映射为索引。此外,还定义了一个数据处理函数。

class Smiles_tokenizer():
    def __init__(self, pad_token, regex, vocab_file, max_length):
        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

    def _regex_match(self, smiles):
        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):
        tokens = self._regex_match(smiles)
        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):
        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):
        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))]
    
    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)]
    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
模型训练

        定义模型训练函数,训练RNN模型。

def train():
    N = 10
    NUM_EMBED = 294
    INPUT_SIZE = 300
    HIDDEN_SIZE = 512
    OUTPUT_SIZE = 512
    NUM_LAYERS = 10
    DROPOUT = 0.2
    CLIP = 1
    N_EPOCHS = 100
    LR = 0.0001
    
    start_time = time.time()
    device = torch.device('cuda' if torch.cuda.is_available() else '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.L1Loss()

    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()
生成结果文件

        定义函数生成预测结果文件,并保存提交文件。

def predict_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!!!")
    
predict_and_make_submit_file("../model/RNN.pth", "../output/RNN_submit.txt")

结语

        在本篇文章中,我们探讨了如何将化学知识转化为计算机可识别的形式,介绍了SMILES和分子指纹的基本概念及其在化学信息学中的应用。同时,我们展示了如何使用RNN对SMILES序列进行建模,并提供了详细的代码实现。希望通过本篇文章,读者能够对AI在化学中的应用有更深入的了解,并能够将所学知识应用到实际项目中。

如果你觉得这篇博文对你有帮助,请点赞、收藏、关注我,并且可以打赏支持我!

欢迎关注我的后续博文,我将分享更多关于人工智能、自然语言处理和计算机视觉的精彩内容。

谢谢大家的支持!

  • 9
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

会飞的Anthony

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值