Datawhale AI夏令营(笔记③)
玖奈汐 2024-09-01 15:01:17 阅读 68
#Datawhale AI夏令营# #AI# #夏令营#
今天的主要内容是学习如何用Transformer建模SMILES进行反应产率预测。Task2(即笔记②)中,我们学习使用RNN建模SMILES,且发现了RNN在处理这类问题的局限性。那么今天,我们就跟着本节内容,看看Transformer是如何解决RNN的缺陷的。
一. 回顾RNN的缺点
在上一个笔记中我搜索资料得出以下RNN的缺点:
梯度消失/梯度爆炸问题:
在训练RNN时,由于网络是循环的,梯度在反向传播过程中会多次乘以权重矩阵。这可能导致梯度变得非常小(梯度消失)或非常大(梯度爆炸),使得网络难以学习长期依赖关系。梯度消失问题尤为常见,因为当序列很长时,早期的信息对后面的影响会逐渐减弱。难以学习长期依赖:
由于梯度消失问题,RNN在捕获序列中的长期依赖关系方面存在困难。这意味着RNN可能无法有效地处理需要记住长时间之前信息的任务。计算成本高:
对于较长的序列,RNN在训练和推理时的计算成本都相对较高。因为每个时间步都需要进行前向传播和可能的反向传播,序列越长,计算量就越大。模型容量有限:
传统的RNN模型容量相对有限,可能无法充分捕捉序列数据的复杂性和多样性。尽管可以通过增加隐藏层的数量或隐藏单元的数量来增加模型容量,但这也会增加梯度消失/爆炸的风险和计算成本。对输入顺序敏感:
RNN在处理序列数据时,对输入的顺序非常敏感。这既是优点也是缺点。优点在于它可以捕捉序列中的时间依赖性,但缺点在于它可能无法处理输入序列的微小变化或噪声,因为这些变化可能会改变整个序列的顺序和含义。
然而,循环神经网络(RNN)的序列到序列建模方法最主要的缺点是在建模文本长程依赖方面都存在一定的局限性。
循环神经网络(RNN):由于所有的前文信息都蕴含在一个隐向量里面,这会导致随着序列长度的增加,编码在隐藏状态中的序列早期的上下文信息被逐渐遗忘。通俗来说,想象一下你正在读一本长篇小说,随着故事的展开,你会记住一些关键情节和角色,但随着故事的继续,你可能会忘记一些早期的细节,尤其是那些与当前情节关联不大的部分。RNN在处理长序列时也面临类似的问题:它试图通过其隐藏状态来记住并传递序列中的信息,但随着序列的推进,早期的信息可能会因为新的输入信息而被覆盖或变得模糊。这是因为RNN在更新其隐藏状态时,通常会使用一种“遗忘”机制(尽管在标准的RNN中这种机制并不明确,但在其改进版本如LSTM和GRU中则更为显著)。这种机制允许网络在必要时丢弃一些不重要的信息,以便为新的信息腾出空间。然而,这也可能导致网络在需要时无法回忆起序列中早期的关键信息。
卷积神经网络(CNN):受限的上下文窗口在建模长文本方面天然地存在不足。如果需要关注长文本,就需要多层的卷积操作。通俗地解释,CNN在处理文本时,是通过一系列的卷积操作来提取文本中的特征。这些卷积操作通常是在一个固定大小的窗口(也称为卷积核或过滤器)上进行的,该窗口在文本上滑动,每次处理一小部分文本(即窗口内的单词或字符)。这种机制使得CNN能够捕捉文本中的局部特征,如单词的组合、短语的模式等。然而,当文本非常长时,CNN的这种局部处理方式就显得有些力不从心了。因为每个卷积操作只能看到窗口内的内容,而无法直接获取到更广泛的上下文信息。这意味着,如果文本中的某个重要信息点距离当前窗口较远,那么CNN可能就无法捕捉到这一信息点与其他部分的关联。
二.系统学习Transformer
1.Transformer概念
1. 定义
Transformer是一种面向序列到序列(Seq2Seq)任务的模型架构,它通过自注意力机制(Self-Attention Mechanism)来计算输入和输出的表示,无需依赖传统的循环神经网络(RNN)或卷积神经网络(CNN)结构。
2. 核心组件
自注意力机制:这是Transformer的核心,允许模型在处理输入序列时同时考虑序列中的所有位置,从而捕捉序列中的依赖关系。自注意力机制通过计算输入序列中各个位置之间的相关性(即注意力权重),来捕捉序列中的依赖关系。具体来说,它使用查询(Query)、键(Key)和值(Value)三个向量来计算注意力权重,并将权重应用于值向量上,从而得到加权后的表示。而多头注意力(Multi-Head Attention)则是为了增强模型的表示能力,Transformer将自注意力机制扩展为多头注意力,即使用多组查询、键和值向量并行计算注意力权重,并将结果拼接后得到最终的输出。编码器-解码器结构:标准的Transformer模型包括编码器(Encoder)和解码器(Decoder)两部分。编码器负责处理输入序列,并将其转换为中间表示。编码器由多个相同的编码器层堆叠而成,每个编码器层包括自注意力机制层和前馈神经网络层。自注意力机制层负责捕捉输入序列中的依赖关系,前馈神经网络层则对自注意力机制层的输出进行进一步处理。解码器同样由多个相同的解码器层堆叠而成,但解码器层在编码器层的基础上增加了一个额外的自注意力机制层(通常称为“掩码自注意力层”),用于处理输出序列的生成,根据中间表示生成输出序列。解码器中的自注意力机制层在计算注意力权重时会进行掩码操作,以确保在生成输出序列时只能依赖于已生成的序列部分。
2.基本架构示意图
简略结构如下:
1.嵌入层(embedding layer)
将token转化为向量表示。模型认识的只是向量,所以需要将每个切分好的token转化为向量。这个过程中,与RNN不同的是,我们在Transformer的嵌入层,会在词嵌入中加入位置编码(Positional Encoding)。位置编码:由于Transformer模型本身不包含序列的顺序信息,因此需要引入位置编码(Positional Encoding)来为模型提供输入序列中元素的位置信息。位置编码通常与输入嵌入相加后作为模型的输入。
2.自注意力机制(self-attention)
计算过程:①对输入序列的每个元素创建查询向量、键向量和值向量。②计算查询向量与键向量之间的点积,并通过softmax函数将其归一化为概率分布。③将归一化后的概率分布与值向量相乘,得到加权后的值向量,即自注意力向量。
自注意力的作用:随着模型处理输入序列的每个单词,自注意力会关注整个输入序列的所有单词,帮助模型对本单词更好地进行编码。在处理过程中,自注意力机制会将对所有相关单词的理解融入到我们正在处理的单词中。更具体的功能如下:
①序列建模:自注意力可以用于序列数据(例如文本、时间序列、音频等)的建模。它可以捕捉序列中不同位置的依赖关系,从而更好地理解上下文。这对于机器翻译、文本生成、情感分析等任务非常有用。
②并行计算:自注意力可以并行计算,这意味着可以有效地在现代硬件上进行加速。相比于RNN和CNN等序列模型,它更容易在GPU和TPU等硬件上进行高效的训练和推理。(因为在自注意力中可以并行的计算得分)
③长距离依赖捕捉:传统的循环神经网络(RNN)在处理长序列时可能面临梯度消失或梯度爆炸的问题。自注意力可以更好地处理长距离依赖关系,因为它不需要按顺序处理输入序列。
(引用博主的原文链接:https://blog.csdn.net/2401_84205765/article/details/140487757)
3.前馈层 (Feed Forward layer)
前馈层,如其名,是一个前向传播的线性层,通常后面跟着一个非线性激活函数(如ReLU)。在这个上下文中,前馈层首先通过一个线性变换(即权重矩阵和偏置向量的乘积)将数据映射到一个新的空间,然后通过非线性激活函数增加非线性,使模型能够学习复杂的函数映射。
实验和理论研究表明,增大前馈层隐状态的维度(即增加W1和W2的列数)可以提高模型的表达能力,并有助于提升最终任务(如机器翻译)的性能。这是因为更大的隐状态维度允许模型在内部表示中捕获更多的信息,从而使其能够更准确地理解输入数据并生成更精确的输出。
4.残差连接
基本架构图中每个(LayerNorm layer)都有一个(Add),这就是一个残差连接。
在传统的神经网络中,信息通过一系列的层进行前向传播,每一层的输出都直接作为下一层的输入。然而,随着网络深度的增加,信息在传播过程中可能会逐渐丢失或变得难以学习,这导致深度网络难以训练,容易出现梯度消失或梯度爆炸的问题。
残差连接通过引入“捷径”(shortcut)或“跳跃连接”(skip connection)来解决这个问题。这些连接允许输入直接跳过一层或多层,直接加入到后续层的输出中。这样,即使某一层的权重没有学到任何有用的信息(即输出接近于输入),由于残差连接的存在,网络仍然能够保持一定的信息流,从而有助于梯度在网络中的传播。
假设某个残差块的输入为x,经过两层网络(或其他任意数量的层)后的输出为F(x),则残差块的最终输出为:
y=F(x)+x
这里,F(x)是残差部分,即除了直接传递的输入x之外,网络学习到的新特征。通过加法操作将F(x)和x合并,形成残差块的输出y。
5.层归一化 (Layernorm)
层归一化是一种针对神经网络中每个隐藏层的输入或输出进行归一化的技术。它通过对每个样本在同一层的所有特征上进行归一化操作,使得这些特征的均值接近0,方差接近1,从而稳定网络的学习过程,提高训练速度和性能。
层归一化的具体步骤如下:
计算均值和方差:对于神经网络中某一层的输入或输出(假设为向量形式),首先计算该向量中所有元素的均值和方差。
归一化操作:使用上一步计算得到的均值和方差,对该层的输入或输出进行归一化处理,即将每个元素减去均值后除以标准差(加上一个小的常数以防止除零错误)。
可学习参数:与批归一化类似,层归一化也引入了两个可学习的参数——缩放因子(scale)和平移因子(shift),用于对归一化后的结果进行线性变换,以恢复数据的表达能力。
归一化操作的计算表达式:
其中μ 和σ分别表示均值和方差,用于将数据平移缩放到均值为 0,方差为 1 的标准分布,μ 和σ 是可学习的参数。层归一化技术可以有效地缓解优化过程中潜在的不稳定、收敛速度慢等问题。
目的:为了进一步使得每一层的输入输出范围稳定在一个合理的范围内。
3.思考Transformer与赛题的联系
序列建模能力:Transformer模型以其强大的序列建模能力而闻名。在催化反应中,底物、添加剂和溶剂等可以视为一种序列或集合,其中每个元素(如底物分子、催化剂类型等)都包含丰富的化学信息和结构特征。虽然这些不是传统意义上的文本序列,但可以通过适当的特征编码(如SMILES字符串、分子指纹、描述符等)将它们转换为Transformer模型可以处理的序列形式。
自注意力机制:Transformer中的自注意力机制允许模型在处理序列时考虑到序列中所有位置的信息,这对于捕捉催化反应中不同成分之间的复杂相互作用非常有用。例如,底物与催化剂之间的相互作用、溶剂对反应速率和产率的影响等,都可以通过自注意力机制来建模。
迁移学习与微调:虽然可能没有现成的、专门用于催化反应产率预测的Transformer模型,但可以利用预训练的Transformer模型(如在自然语言处理任务上训练的模型)进行迁移学习和微调。通过将催化反应数据转换为适合Transformer处理的形式,并使用这些数据对模型进行微调,可以开发出针对特定催化反应产率预测的定制模型。
特征编码与嵌入:为了将催化反应数据输入到Transformer模型中,需要进行适当的特征编码和嵌入。这包括将底物、添加剂和溶剂等转换为向量表示,这些向量能够捕捉它们的化学结构和性质。这可以通过使用化学信息学中的方法(如SMILES字符串的嵌入、分子描述符的计算等)来实现。
三.研究与优化代码
在课程中教育团队给出了以下的这些意见:
1.调整epoch:epoch越大,训练得越久,一般而言模型得性能会更好。但是也有可能会出现过拟合现象。考虑到时间成本和过拟合现象,我选择了E_POCH=0。
所谓过拟合,就是模型过分学习了训练数据,导致泛化能力减弱的现象。一个极端的例子是:模型完全死记硬背记住了所有训练数据,因此在训练数据上的预测结果为满分。但是对于没在训练过程中见过的数据,却完全无能为力。
2.调整模型大小:中间向量的维度、模型得层数、注意力头的个数等( I即NPUT_DIM ,D_MODEL,NUM_HEADS ,FNN_DIM ,NUM_LAYERS等 )。一般而言,模型越大学习能力越强,但是同样的也有可能出现过拟合。
3.数据:对数据做清洗,调整数据分布,做数据增广。对于SMILES一个可行的增广思路是:将一个SMILES换一种写法。(由于现生忙,这一个暂时还没有研究)
4.采用学习率调度策略:在训练模型的过程中,我们发现往往约到后面,需要更小的学习率。例如下图:学习到后面,我们需要收敛的局部最小值点的两边都比较“窄”,如果现在学习率太大,那么在梯度下降的时候,就有可能翻过局部最小点了。因此需要调整学习率变小。在Pytorch中已经定义好了一些常用的学习率调度方法,需要的学习者可以自己从官网上查看如何使用。实例如下:
<code>import torch.optim as optim
optimizer = optim.SGD(model.parameters(), lr=0.1, momentum=0.9)
# 创建StepLR调度器,每30个epoch学习率衰减为原来的0.1倍
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)
for epoch in range(100):
train(...)
validate(...)
scheduler.step()
5.集成学习:训练多个不同初始化或架构的模型,并使用集成方法(如投票或平均)来产生最终翻译。这可以减少单一模型的过拟合风险,提高翻译的稳定性。(我的理解是用不同的模型进行训练,然后通过投票和平均进行更稳定的预测,明天研究)
以下是我对一些参数进行的调优(最终得分是0.1213,应该是有某个参数没有调好,以及学习率没有调整,明天尝试进行集成学习):
def train():
## super param
N = 20
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 = 30
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=64, 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)code>
criterion = nn.MSELoss()
best_valid_loss = float('inf')
for epoch in range(N_EPOCHS):
epoch_loss = 0
loss_in_a_epoch = 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.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()
明天将更新集成学习的学习情况。
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。