Datawhale AI夏令营 task2学习笔记

题目概述

本来不想写这篇笔记,因为自己的模型效果并不理想。权当是一次记录吧。

这次的任务是使用RNN模型(循环神经网络)进行深度学习。

具体思路和上一次的机器学习其实很类似,不过这次的思路显然比上次要复杂一些,我主要通过关键函数——训练函数 train 来理清整个代码的逻辑。

train函数源码如下

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")
  1. 首先是一系列超参数的设置,这也是优化的一大方向。
  2. 之后是设置计时器、选择设备等等准备工作。
  3. 下面调用 read_data 函数读取 csv 文件的数据。
  4. 调用 ReactionDataset 实现创建数据集对象,包含数据的读取方法和索引访问方法。
  5. 使用 Subset 选择数据集的一个子集 subset_dataset。
  6. 使用 DataLoader 创建训练数据加载器 train_loader,设置批大小为128,并使用 collate_fn 进行批处理。
  7. 实例化 RNNModel 模型并传入参数,选择使用 Adam 优化器、L1损失函数(MAE)。
  8. 开始训练并输出每个训练轮次的相关信息,具体的操作包括:将输入和目标移到设备上、清零优化器的梯度、前向传播,计算模型输出、计算损失、反向传播、使用梯度裁剪防止梯度爆炸、优化器更新模型参数、累加当前批次的损失到 epoch_loss。

同样还涉及到一些工具函数:

  • read_data:数据读取和处理
    def read_data(file_path, train=True):
        # 从CSV文件中读取反应物、生成物、添加剂、溶剂和产率信息
        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)]
        
    
        # # 统计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
  •  ReactionDataset:定义数据集类
    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]
  •  collate_fn:批处理函数,用于批处理数据,将SMILES字符串转换为token索引,并返回token索引和产率。
    def collate_fn(batch):
        # 正则表达式,匹配SMILES字符串中的各种化学符号
        REGEX = r"\[[^\]]+]|Br?|Cl?|N|O|S|P|F|I|b|c|n|o|s|p|\(|\)|\.|=|#|-|\+|\\\\|\/|:|~|@|\?|>|\*|\$|\%[0-9]{2}|[0-9]"
        # 将SMILES字符串转化为模型可以接受的索引序列
        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])
        # 对SMILES字符串进行tokenize并转换为张量
        tokenizer_batch = torch.tensor(tokenizer.tokenize(smi_list)[1])
        # 将产率列表转换为张量
        yield_list = torch.tensor(yield_list)
        return tokenizer_batch, yield_list
  •  Smiles_tokenizer:将SMILES字符串转化为模型可以接受的索引序列。
    class Smiles_tokenizer():
        # 初始化实例变量。读取词汇表文件,将每个符号与其对应的索引存储在字典 vocab_dic 中
        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
    
        # 使用正则表达式匹配SMILES字符串中的化学符号,将每个符号添加到 tokenised 列表中
        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
    
        # 调用 _regex_match 方法获取 tokenized 的 SMILES 字符串;在每个序列的开头和结尾分别添加特            殊标记 <CLS> 和 <SEP>;调用 _pad_seqs 方法将序列填充到统一长度;调用 _pad_token_to_idx 方法将序列中的符号转化为对应的索引
        def tokenize(self, smiles):
            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
    
        # 计算序列中最长的长度,将每个序列填充到最长长度,使用 pad_token 进行填充
        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
  • 我自己定义的生成token的代码
    import re
    import pandas as pd
    
    # 定义正则表达式用于分词
    REGEX = r"\[[^\]]+]|Br?|Cl?|N|O|S|P|F|I|b|c|n|o|s|p|\(|\)|\.|=|#|-|\+|\\\\|\/|:|~|@|\?|>|\*|\$|\%[0-9]{2}|[0-9]"
    
    # 分词函数
    def tokenize(smiles, regex):
        regex_string = r"(" + regex + r"|.)"
        prog = re.compile(regex_string)
        tokenised = []
        for smi in smiles:
            tokens = prog.findall(smi)
            tokenised.extend(tokens)  # 收集所有的 token
        return tokenised
    
    # 从 CSV 文件读取数据
    file_path = "../dataset/round1_train_data.csv"
    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()
    
    # 将所有 SMILES 字符串收集在一起
    all_smiles = reactant1 + reactant2 + product + additive + solvent
    
    # 对所有 SMILES 字符串进行分词
    all_tokens = tokenize(all_smiles, REGEX)
    
    # 创建词汇表
    vocab = sorted(set(all_tokens))
    
    # 添加特殊标记
    vocab = ["<PAD>", "<CLS>", "<SEP>"] + vocab
    
    # 将词汇表保存到文件
    vocab_file = "../vocab_full.txt"
    with open(vocab_file, "w") as f:
        for token in vocab:
            f.write(token + "\n")

 主要思路

emm,很遗憾,我在人工智能课程学习的是 CNN(卷积神经网络),没有学过RNN……

一、调参

观察给出的初始参数,结合以往使用 CNN 的经验,我觉得网络层数和每层的神经元数量都已经很高,但是学习率偏低,存在过拟合的风险;同时批次大小太低,导致训练时间过长。目前最终调整的参数:学习率LR=0.001,批次大小N=64, DROPOUT=0.5。

当然,目前参数表现很差,后续再继续调整。

二、模型优化

目前 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

后续考虑更换激活函数,现在使用的是 Sigmoid 激活函数,以及添加一些丢弃层、归一化等操作。

三、模型替换

目前没有这个打算,毕竟赛题要求使用 RNN,而且我目前知道的除了 RNN 就是 CNN,可能表现不如 RNN。

总结

最终得分有些低,只有0.1左右,就不贴出来丢人了……

深度学习分数比机器学习低的原因是什么呢?可能自己的模型和参数不太合适吧,也可能是训练数据量太少。看看 Transformer 的表现吧。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值