Datawhale AI 暑期夏令营 Task2笔记

分子指纹技术

这是一种用于描述和比较分子结构的方法,通常应用于化学、药物研究以及计算化学领域。它基于分子结构的特征生成一种数字化的表示,这种表示能够捕捉分子的结构信息,用于比较、分类和预测分子性质。
原理和方法:

  1. 特征提取:
    分子指纹技术的核心是将分子的结构映射为一系列具有代表性的特征。这些特征可以是分子中原子的排列方式、键的类型和长度、环的存在以及其他结构细节。常见的分子指纹包括结构键合指纹(Structure Key Fingerprints)、环境敏感的分子指纹(Environmentally Sensitive Molecular Fingerprints)、拓扑分子指纹(Topological Molecular Fingerprints)等。
  2. 编码方式:
    分子指纹可以以二进制、整数或其他编码方式表示。每种编码方式都有其独特的生成算法,可以根据需求来选择合适的编码方式。例如,对于二进制分子指纹,分子中的每个特征(如每种键类型、环的存在等)可以用1或0表示其存在或缺失。
  3. 应用:
  • 相似性比较和搜索:分子指纹可以用来比较两个或多个分子的相似性,以快速筛选候选化合物或寻找类似结构的化合物。
  • 药物设计:在药物发现中,可以使用分子指纹来预测分子的生物活性,设计新的化合物或者优化已有化合物的结构。
  • 毒性预测:通过比较分子指纹,可以预测潜在的毒性或生物活性,以帮助筛选出对人体更安全的化合物。
  1. 类型:
  • 基于结构的指纹:通过分析原子间的连接方式和距离来生成指纹,如MACCS键合指纹、Daylight键合指纹等。
  • 基于子结构的指纹:关注分子中特定子结构的存在与否,如SMARTS模式指纹。
  • 基于拓扑结构的指纹:考虑分子中原子的拓扑特性,如E-state指纹。

SMILES与分子指纹的关系

SMILES(Simplified Molecular Input Line Entry System)是一种用来表示分子结构的字符串表示法,它描述了分子中原子的连接方式和原子之间的键。与此相对,分子指纹是一种数值化的表示方法,用于描述和比较分子结构的特征。
关系总结:

  1. SMILES描述分子结构,SMILES是一种基于文本的字符串表示方法,通过描述分子中的原子和键的连接方式来唯一标识分子的结构。例如,SMILES可以直观地表示分子的拓扑结构,如环、双键等。而分子指纹捕捉分子特征,是一种数字化的表示方法,通过捕捉分子的结构特征生成一个数值化的向量或字符串,可以根据不同的算法和需求生成,例如基于分子结构的键合、子结构或拓扑特征。
  2. 应用和互补性上,SMILES主要用于表示和存储分子结构,以便于计算机处理和化学信息系统中的查找和索引。分子指纹则常用于描述和比较分子之间的相似性、毒性预测、药物设计等应用。在某些应用中,可以将SMILES转换为特定类型的分子指纹,以便进行结构比较和分析。

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__()
        
        # Embedding 层,用于将输入的整数索引映射为密集向量表示
        self.embed = nn.Embedding(num_embed, input_size)
        
        # RNN 层的定义,这里使用的是简单的单层双向RNN
        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(),  # Sigmoid激活函数,用于输出层的非线性变换
                                nn.Linear(output_size, 1),
                                nn.Sigmoid())  # 最终输出层的Sigmoid激活函数

    def forward(self, x):
        # x : [bs, seq_len],输入的张量形状,bs是batch size,seq_len是序列长度
        
        # 对输入的整数序列进行嵌入表示
        x = self.embed(x)
        # x : [bs, seq_len, input_size],嵌入后的形状,input_size是嵌入后的向量维度
        
        # 将嵌入后的序列输入到RNN中
        _, hn = self.rnn(x)
        # hn : [2*num_layers, bs, h_dim],RNN的最后隐藏状态,包括双向RNN的所有层的状态
        
        # 调整隐藏状态的维度顺序,将batch size放在第一维
        hn = hn.transpose(0, 1)
        
        # 将调整后的隐藏状态展平成一个二维张量
        z = hn.reshape(hn.shape[0], -1)
        # z shape: [bs, 2*num_layers*h_dim],其中h_dim是每层RNN的隐藏状态维度
        
        # 将展平后的隐藏状态通过全连接层进行映射,并进行Sigmoid激活,最终得到输出
        output = self.fc(z).squeeze(-1)
        # output shape: [bs, 1],最终输出的形状,squeeze(-1)用于去掉最后一维中的大小为1的维度
        
        return output

import re

class Smiles_tokenizer():
    def __init__(self, pad_token, regex, vocab_file, max_length):
        """
        初始化tokenizer类
        
        参数:
        - pad_token: 用于填充的token,通常是'<PAD>'或类似的标记
        - regex: 正则表达式,用于匹配SMILES字符串中的子串
        - vocab_file: 包含词汇表的文件路径,每行一个token
        - max_length: 最大序列长度,用于截断超长序列
        """
        self.pad_token = pad_token
        self.regex = regex
        self.vocab_file = vocab_file
        self.max_length = max_length

        # 从vocab_file中读取词汇表,构建词汇字典
        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):
        """
        使用正则表达式对SMILES字符串进行分词
        
        参数:
        - smiles: 输入的SMILES字符串列表
        
        返回值:
        - tokenised: 分词后的列表,每个元素是一个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字符串列表进行tokenize
        
        参数:
        - smiles: 输入的SMILES字符串列表
        
        返回值:
        - tokens: tokenized后的序列列表,每个序列是由token组成的列表
        - token_idx: tokenized后的序列的索引表示,用于模型输入
        """
        tokens = self._regex_match(smiles)
        # 在每个token序列的开头和结尾添加特殊token,如'<CLS>'和'<SEP>'
        tokens = [["<CLS>"] + token + ["<SEP>"] for token in tokens]
        # 使用pad_token进行填充,使得所有序列长度相同
        tokens = self._pad_seqs(tokens, self.pad_token)
        # 将token序列转换为对应的索引序列
        token_idx = self._pad_token_to_idx(tokens)
        return tokens, token_idx

    def _pad_seqs(self, seqs, pad_token):
        """
        对序列列表进行填充,使得每个序列的长度相同
        
        参数:
        - seqs: 输入的序列列表,每个序列是由token组成的列表
        - pad_token: 用于填充的token
        
        返回值:
        - padded: 填充后的序列列表
        """
        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):
        """
        将token序列转换为对应的索引序列
        
        参数:
        - tokens: 输入的token序列列表,每个序列是由token组成的列表
        
        返回值:
        - idx_list: 转换后的索引序列列表,每个序列是由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])  # 将token转换为对应的索引
                else:
                    self.vocab_dic[i] = max(self.vocab_dic.values()) + 1  # 如果token不在词汇表中,分配一个新的索引
                    tokens_idx.append(self.vocab_dic[i])
            idx_list.append(tokens_idx)
        
        return idx_list

# 读数据并处理
def read_data(file_path, train=True):
    """
    读取数据文件并处理成模型需要的输入格式
    
    参数:
    - file_path: 数据文件路径
    - train: 是否为训练模式,用于确定是否包含Yield字段
    
    返回值:
    - output: 处理后的数据列表,每个元素包括输入和对应的输出
    """
    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()  # 如果是训练模式,读取Yield字段
    else:
        react_yield = [0 for i in range(len(reactant1))]  # 否则设为0
    
    # 将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)]

    # 返回处理后的数据
    return output

class ReactionDataset(Dataset):
    def __init__(self, data: List[Tuple[List[str], float]]):
        """
        初始化数据集对象。

        Args:
            data (List[Tuple[List[str], float]]): 包含SMILES表示和输出标签的数据列表。
                                                  每个元组包含一个SMILES表示的列表和一个浮点型输出标签。
        """
        self.data = data
        
    def __len__(self):
        """
        返回数据集的长度。

        Returns:
            int: 数据集中元素的数量。
        """
        return len(self.data)

    def __getitem__(self, idx):
        """
        根据给定的索引返回对应的数据样本。

        Args:
            idx (int): 数据样本的索引。

        Returns:
            Tuple[torch.Tensor, torch.Tensor]: 包含SMILES序列和输出标签的元组。
                                               SMILES序列被tokenize后转换为torch.Tensor。
        """
        return self.data[idx]
    
def collate_fn(batch):
    """
    自定义的collate函数,用于处理一个batch的数据。

    Args:
        batch (List): 一个包含多个数据样本的列表,每个数据样本是ReactionDataset返回的元组。

    Returns:
        Tuple[torch.Tensor, torch.Tensor]: 包含tokenized的SMILES序列和对应输出标签的元组。
    """
    # 正则表达式用于分割SMILES字符串
    REGEX = r"\[[^\]]+]|Br?|Cl?|N|O|S|P|F|I|b|c|n|o|s|p|\(|\)|\.|=|#|-|\+|\\\\|\/|:|~|@|\?|>|\*|\$|\%[0-9]{2}|[0-9]"
    
    # 初始化SMILES tokenizer
    tokenizer = Smiles_tokenizer("<PAD>", REGEX, "../vocab_full.txt", max_length=300)
    
    smi_list = []
    yield_list = []
    
    # 将batch中的数据分别保存到smi_list和yield_list中
    for i in batch:
        smi_list.append(i[0])  # 提取SMILES表示
        yield_list.append(i[1])  # 提取输出标签
    
    # 使用tokenizer对smi_list中的SMILES表示进行tokenize,并转换为torch.Tensor
    tokenizer_batch = torch.tensor(tokenizer.tokenize(smi_list)[1])
    
    # 将yield_list转换为torch.Tensor
    yield_list = torch.tensor(yield_list)
    
    return tokenizer_batch, yield_list

import time
from typing import List, Tuple
import torch
import torch.optim as optim
import torch.nn as nn
from torch.utils.data import Dataset, Subset, DataLoader

def train():
    ## 超参数设置
    N = 10  # 或者你可以设置为数据集大小的一定比例,如 int(len(dataset) * 0.1)
    NUM_EMBED = 294  # Embedding的维度
    INPUT_SIZE = 300  # 输入序列的长度
    HIDDEN_SIZE = 512  # RNN模型隐藏层的大小
    OUTPUT_SIZE = 512  # 输出层的大小
    NUM_LAYERS = 10  # RNN模型的层数
    DROPOUT = 0.2  # Dropout的概率
    CLIP = 1  # 梯度裁剪阈值
    N_EPOCHS = 10  # 训练的总epoch数
    LR = 0.001  # 学习率
    
    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.L1Loss()  # 使用L1损失函数
    
    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)
        
        # 打印每个epoch的训练损失
        print(f'Epoch: {epoch+1:02} | Train Loss: {loss_in_a_epoch:.3f}')
        
        # 如果当前epoch的损失比最佳损失还低,则保存模型
        if loss_in_a_epoch < best_loss:
            torch.save(model.state_dict(), '../model/RNN.pth')
            best_loss = loss_in_a_epoch
            
    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 predicit_and_make_submit_file(model_file, output_file):
    NUM_EMBED = 294  # Embedding的维度
    INPUT_SIZE = 300  # 输入序列的长度
    HIDDEN_SIZE = 512  # RNN模型隐藏层的大小
    OUTPUT_SIZE = 512  # 输出层的大小
    NUM_LAYERS = 10  # RNN模型的层数
    DROPOUT = 0.2  # Dropout的概率
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')  # 设置设备为GPU或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}')  # 每行格式为 "test序号,预测结果"
    
    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")
posted @   qaz961501  阅读(14)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示