Datawhale AI夏令营--第二届世界科学智能大赛物质科学赛道:催化反应产率预测(3)

一、Transformer建模SMILES进行反应产率预测

本次练习围绕Transformer建模建模来进行预测,先来了解一下吧。

Transformer模型是一种基于‌自注意力机制的神经网络模型,主要用于处理序列数据,特别是在自然语言处理领域。其建模过程主要包括以下几个关键部分:

  1. 基本结构:Transformer模型由‌编码器和解码器两部分组成,每个部分都包含多个相同的Transformer块。编码器负责处理输入序列,解码器负责生成输出序列。每个Transformer块内部包含自注意力机制、前馈神经网络以及‌残差连接和‌层归一化。‌

  2. 自注意力机制:自注意力机制是Transformer模型的核心。它通过计算‌查询(Query)、‌(Key)和‌(Value)之间的相似度来捕捉输入序列中的依赖关系。具体来说,对于输入序列中的每个位置,自注意力机制都会计算该位置与其他所有位置的相似度分数,并根据这些分数对输入信息进行加权求和,从而得到新的表示。‌

  3. 位置编码:由于自注意力机制本身并不包含位置信息,因此在输入表示中需要添加位置编码(Positional Encoding)。位置编码通常通过正弦和余弦函数生成,以便模型能够理解序列中单词的顺序关系。‌

  4. 多头注意力:为了捕捉输入序列中不同位置的多种表示,Transformer采用了多头注意力机制。通过将注意力头数设置为多个,模型可以从不同的子空间中学习到输入的不同表示,从而增强模型的表示能力。‌

  5. 其他组件:‌嵌入层负责将输入文本转换为向量表示;残差连接和层归一化则用于加速训练过程并提高模型的稳定性。‌

二、基本架构示意图

 三、代码详细解释

 具体操作步骤和第一次的相同,我在这里就不再写一次了。具体看我的第一篇。

(https://blog.csdn.net/m0_56875011/article/details/140714771)

同样,在这次的建模还是需要一个词汇表的文件(vocab_full.txt),我放在附件了。

现在开始我们来看一下关键代码:

1、定义一个基于PyTorch的Transformer编码器模型(TransformerEncoderModel

它主要用于处理序列数据(如文本、时间序列等),并输出一个标量值(可能用于分类、回归等任务)。

初始化方法 __init__

参数

input_dim: 输入的词汇表大小(即嵌入层的输入维度)。

d_model: Transformer模型中嵌入层的输出维度,也是编码器层的输入/输出维度。

num_heads: 自注意力机制中多头注意力的头数。

fnn_dim: 前馈网络(Feedforward Neural Network, FNN)的维度,即编码器层中两层线性变换之间的维度。

num_layers: 编码器层的堆叠数。

dropout: Dropout比率,用于防止过拟合。

组件

embedding: 嵌入层,将输入的整数(词汇索引)转换为固定大小的密集向量。

layerNorm: 层归一化层,用于稳定训练过程,通常放在编码器层之前(但在本例中,它实际上没有被直接用于此目的,因为nn.TransformerEncoder内部已经包含了层归一化)。

encoder_layer: 单个Transformer编码器层,包含多头自注意力机制和前馈网络。

transformer_encoder: Transformer编码器,由多个encoder_layer堆叠而成。

dropout: Dropout层,用于在嵌入层之后减少过拟合。

lc: 线性分类器(Linear Classifier),由多个线性层和Sigmoid激活函数组成,用于将Transformer的输出转换为最终的标量输出。

前向传播方法 forward

输入:src(源序列),形状为[batch_size, src_len],其中batch_size是批次大小,src_len是序列长度。

过程:

1、将输入src通过嵌入层转换为嵌入向量,并应用Dropout。

2、将嵌入向量传递给Transformer编码器,获取编码器层的输出。

3、从编码器的输出中选取每个序列的第一个位置(或可以使用torch.sum等聚合函数,但这里选择第一个位置可能意味着关注序列的开始部分)。

4、将选取的向量通过线性分类器(lc),最终输出一个标量值。

输出:一个形状为[batch_size]的张量,表示每个输入序列对应的预测值(可能是分类任务的概率或回归任务的预测值)。

# 模型
'''
直接采用一个transformer encoder model就好了
'''
class TransformerEncoderModel(nn.Module):
    def __init__(self, input_dim, d_model, num_heads, fnn_dim, num_layers, dropout):
        super().__init__()
        self.embedding = nn.Embedding(input_dim, d_model)
        self.layerNorm = nn.LayerNorm(d_model)
        self.encoder_layer = nn.TransformerEncoderLayer(d_model=d_model, 
                                                        nhead=num_heads, 
                                                        dim_feedforward=fnn_dim,
                                                        dropout=dropout,
                                                        batch_first=True,
                                                        norm_first=True # pre-layernorm
                                                        )
        self.transformer_encoder = nn.TransformerEncoder(self.encoder_layer, 
                                                         num_layers=num_layers,
                                                         norm=self.layerNorm)
        self.dropout = nn.Dropout(dropout)
        self.lc = nn.Sequential(nn.Linear(d_model, 256),
                                nn.Sigmoid(),
                                nn.Linear(256, 96),
                                nn.Sigmoid(),
                                nn.Linear(96, 1))

    def forward(self, src):
        # src shape: [batch_size, src_len]
        embedded = self.dropout(self.embedding(src))
        # embedded shape: [batch_size, src_len, d_model]
        outputs = self.transformer_encoder(embedded)
        # outputs shape: [batch_size, src_len, d_model]

        # fisrt
        z = outputs[:,0,:]
        # z = torch.sum(outputs, dim=1)
        # print(z)
        # z shape: [bs, d_model]
        outputs = self.lc(z)
        # print(outputs)
        # outputs shape: [bs, 1]
        return outputs.squeeze(-1)

2、训练数据集

这里是训练数据集,需要很长的时间。

这段代码是一个用于训练基于Transformer编码器的模型的函数,主要用于处理序列到单个值(可能是回归任务)的预测。

参数设置

N: 这里被硬编码为10,但在实际应用中,它应该根据数据集的大小来设置,例如int(len(dataset) * 0.1)来取数据集的10%作为训练集(如果N用于此目的的话)。但在这个函数中,N似乎并未被正确使用来限制训练集的大小。

INPUT_DIM, D_MODEL, NUM_HEADS, FNN_DIM, NUM_LAYERS, DROPOUT: 这些是Transformer编码器模型的超参数。

CLIP: 梯度裁剪的值,用于防止梯度爆炸。

N_EPOCHS, LR: 分别表示训练轮数和初始学习率。

数据加载和预处理

使用read_data函数从CSV文件中读取数据,并创建一个ReactionDataset实例。

Subset类的使用似乎是为了限制训练集的大小,但实际上它并没有被正确应用到train_loader中,因为train_loader是直接使用dataset创建的,而不是subset_dataset。

DataLoader用于批量加载数据,shuffle=True表示在每个epoch开始时打乱数据,collate_fn=collate_fn指定了一个自定义的批处理函数(尽管collate_fn的具体实现在代码中没有给出)。

模型、优化器和损失函数

创建了一个TransformerEncoderModel实例,并将其移动到指定的设备上(GPU或CPU)。

使用AdamW优化器,并设置了学习率衰减(虽然在这段代码中并没有直接使用学习率衰减的调度器来调整学习率,而是使用了ReduceLROnPlateau,它根据验证集上的损失来动态调整学习率)。

使用了均方误差(MSE)作为损失函数。

训练过程

1、在每个epoch开始时,将模型设置为训练模式。

2、遍历训练数据加载器,对于每个批次的数据:

3、将数据移动到指定的设备上。

4、清零优化器的梯度。

5、通过模型进行前向传播,计算输出和损失。

6、进行反向传播,计算梯度。

7、使用梯度裁剪来防止梯度爆炸。

8、更新模型的参数。

9、累加损失值,并在每50个批次后打印当前的训练损失。

在每个epoch结束时,计算并打印该epoch的平均训练损失,并使用ReduceLROnPlateau调度器根据平均损失来调整学习率(但这里需要注意,由于调度器是在平均损失上被调用的,而不是在验证集损失上,因此它可能不会按预期工作,除非训练集和验证集非常相似)。

训练循环结束部分

计算平均训练损失

loss_in_a_epoch = epoch_loss / len(train_loader)

这一行代码计算了当前epoch中所有批次损失的平均值。epoch_loss是在遍历完一个epoch的所有批次后累积得到的总损失,而len(train_loader)给出了一个epoch中批次的总数。

调整学习率

scheduler.step(loss_in_a_epoch)

这里使用ReduceLROnPlateau调度器根据平均训练损失来调整学习率。但是,请注意,ReduceLROnPlateau通常期望的是一个验证集上的损失,以便在验证集性能不再提高时减少学习率。然而,这里它被用于训练集损失,这可能不是最佳实践,因为训练集损失可能会随着训练的进行而持续下降,而不一定反映模型在未见过的数据上的泛化能力。

print(f'Epoch: {epoch+1:02} | Train Loss: {loss_in_a_epoch:.3f}')

打印当前epoch编号和平均训练损失。

保存最佳模型

if loss_in_a_epoch < best_valid_loss:  

    best_valid_loss = loss_in_a_epoch 

        torch.save(model.state_dict(), '../model/transformer.pth')

    这里有一个问题:best_valid_loss被用作与训练损失进行比较的基准,但实际上它应该代表验证集上的最佳损失。然而,由于代码中没有包含验证集的处理,这里直接使用了训练损失。如果目的是保存训练过程中损失最小的模型,那么这段代码是可行的,但它并不反映模型在未见过的数据上的性能。

脚本执行流程

    定义train函数:

    您定义了一个名为train的函数,该函数包含了模型训练的全部逻辑。

   条件执行:

if __name__ == '__main__': 

    train()

这是Python脚本的常见模式,用于在直接运行该脚本时(而不是作为模块导入时)执行特定的代码块。在这里,它确保了当脚本被直接执行时,会调用train函数来开始训练过程。

时间测量

在train函数的开始和结束处,您分别测量了时间,以便计算并打印整个训练过程所花费的时间(以分钟为单位)。

# 训练
def train():
    ## super param
    N = 10  #int / int(len(dataset) * 1)  # 或者你可以设置为数据集大小的一定比例,如 int(len(dataset) * 0.1)
    INPUT_DIM = 292 # src length
    D_MODEL = 512
    NUM_HEADS = 4
    FNN_DIM = 1024
    NUM_LAYERS = 4
    DROPOUT = 0.2
    CLIP = 1 # CLIP value
    N_EPOCHS = 40
    LR = 1e-4
    
    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 = TransformerEncoderModel(INPUT_DIM, D_MODEL, NUM_HEADS, FNN_DIM, NUM_LAYERS, DROPOUT)
    model = model.to(device)
    model.train()
    
    optimizer = optim.AdamW(model.parameters(), lr=LR, weight_decay=0.01)
    scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.1, patience=10)
    criterion = nn.MSELoss()

    best_valid_loss = 10
    for epoch in range(N_EPOCHS):
        epoch_loss = 0
        # adjust_learning_rate(optimizer, epoch, LR) # 动态调整学习率
        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.detach().item()
            
            if i % 50 == 0:
                print(f'Step: {i} | Train Loss: {epoch_loss:.4f}')
                
        
        loss_in_a_epoch = epoch_loss / len(train_loader)
        scheduler.step(loss_in_a_epoch)
        print(f'Epoch: {epoch+1:02} | Train Loss: {loss_in_a_epoch:.3f}')
        if loss_in_a_epoch < best_valid_loss:
            best_valid_loss = loss_in_a_epoch
            # 在训练循环结束后保存模型
            torch.save(model.state_dict(), '../model/transformer.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()         

运行过程

 

3、官网提交文件后结果

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值