Task2 学习笔记
Task1遗留的问题
这是一个全新的时代,一个由AI驱动的时代,几乎什么东西都可以用AI去跑,用AI去拟合。
因此有人提出一个问题:"AI会不会有一天取代人的劳动力?"
注意,这里提的是取代,并不是分担。换言之,人能做的事情,AI是不是都能做到?
比如说,这次我们使用AI对催化剂产率的预测,可不可以和人的计算做得一样好?至少我们这些青年学生进行尝试时做得并不够好——这个准确率分数能够到0.4/1就已经是烧高香的地步了,“炼丹”的威名恐怖如斯!
但,至少这个AI能干事,不是吗?对于催化剂作用下的产率,AI至少已经能将预测值锁定在一定的范围内了,那在预算较低的情况下,是否可以直接用AI代替人类?想得挺美。
资本:求求你给我300%的利润吧,我什么都会做的。
在一期关于美国的纪录片中,主持人曾问一个自动化工厂的负责人,为什么在全流水线都已经实现自动化的当下,依然要招募工人?
负责人也只提了一句话:“如果一切都交由机器处理,到时候万一发生了事故,谁来负责?机器人不会感到疼痛,机器人的0和1就是0和1,所谓的True和False都是人类所规定的。”
这就又好像一个区块链,每一个后面加入的区块都由前面的区块证明着可信程度,所以创世区块又将由谁来确认它的可信呢?其实,这只能通过人为规定去解决。
所以,AI并不能完全取代人类,至少在本次AI+化学上,化学的符号也许在人类的视野里可以组合成不同的化学式,可以看到未来生成的化合物。但在机器的眼中,那些化学符号都只是根据人类所制定的标准下的ASCII码所组成的比特流而已。
因此,为了能够AI4Science,人类必须得让AI学会如何正确的运用这些化学式,换言之,就是让AI在进行工作之前,让它先知道什么是化学。
要想解决这个问题,必须对AI4Science追本溯源。
黑格尔:“一切哲学均为哲学史。” 科学同样如此。
AI4Science的补充
AI4Science的发展历史大致也经历这三个阶段:
-
将化学知识以计算机形式存储,并构建数据库;
-
机器学习;
-
深度学习。
第一个阶段
在第一个阶段中,人们主要做的事情就是尝试使用不同的方法,尽可能地将化学知识和信息以计算机的形式进行存储,并以此为基础开始构建数据库。这个时候例如,用一些字符表示分子或者其他化学符号,如何保存一个具有空间结构的分子的原子、键的位置信息等等。
起初,为了能够方便存储,人们更向着线性结构。然后推出了一系列对于化学符号表达式的规范——故SMILES表达式诞生了。SMILES将化学分子中涉及的原子、键、电荷等信息,用对应的ASCII字符表示;环、侧链等化学结构信息,用特定的书写规范表达。以此,几乎所有的分子都可以用特定的SMILES表示,且SMILES的表示还算比较直观。
在本次学习中,通过后面补充的符号表,可以发现SMILES表达式并非十全十美,它还是有以下缺点:
-
无法表达充分的空间信息。
-
一个分子起始原子不同等因素,有多个SMILES。
可见,SMILES表达式是对于分子结构的线性化表述,但是,分子也许有基团,主干之分,分子天生没有所谓的起点,所以,线性化存储的人为痕迹太重了,为了线性化而丢失空间信息,那些因为手性异构分子导致畸形的婴儿不会开心的。
事实上,使用图数据(grpah)表示分子是非常合适的。图网络相比于基于SMILES的序列网络,在某些方面会更胜一筹。感兴趣的同学可以自行探索。
主要图才是真正的多对多结构,这样才最符合分子的组成形式,可是,它在存储上被SMILES表达式完爆了。
第二个阶段
在第二阶段,大家开始使用一些手动的特征工程对已有数据进行编码、特征提取等操作。例如,在baseline中我使用分子指纹(molecule fingerprint)作为编码方式。再辅以传统的机器学习的方法,做一些预测。这个阶段下每个为1的值表示这个分子具有某些特定的化学结构。例如,对于一个只有长度为2的分子指纹,我们可以设定第一个值表示分子是否有甲基,第二个位置的值表示分子是都有苯环,那么[0,1]的分子指纹表示的就是一个有苯环而没有甲基的分子。通常,分子指纹的维度都是上千的,也即记录了上千个子结构是否出现在分子中。
但是,正常情况下上千个子结构的分子一般都只会出现在实验室的中间过程中。现实数据集中这样的向量更多地是一种稀疏向量,它们也会拼接成稀疏矩阵,从数据结构课程可知,这样的矩阵计算其实对于空间的浪费巨大,同时大量无效的遍历也会拖慢速度。
所以,有没有改良方法呢?
底层遍历也是遍历,能省就省,因此,可以考虑将稀疏向量中的1的位置提取出来,形成一个表,将这个表制作成向量然后拼接成矩阵,这样可以大幅加快训练速度的同时,还可以省点存储空间。
这位CPU,你也不希望内存被区区一个矩阵塞满吧~
第三个阶段
在第三阶段,各种各样的深度神经网络也开始被广泛使用。这些网络不仅仅开始学习各种特征,也像word2vec那样,非常多的网络也被拿来对分子进行向量化。这也导致后来又非常多的新型的分子指纹出现。基于seq2seq模型学习表示为序列类型的化学数据、基于diffusion重建分子三维空间结构等等,都是当今的潮流方向。不过我们本次的学习暂时还用不到这些。
我们本次要使用的是RNN,在深度学习上会有更多提及。
从机器学习迈向深度学习
机器学习与特征工程
机器学习按照目标可以分为分类任务(classification)和回归(regression)任务两大类。所谓分类任务,就是模型预测的结果是离散的值,例如类别;那么,回归任务中,模型预测的结果就是连续的值,例如房价等等。
以本次竞赛为例,我们需要预测的目标是反应的产率,最后的输出是0-1之间的一个连续的数值,所以是一个回归任务。
换一个例子,如果我们需要预测一张图片上的生物是猫还是狗,最后的输出势必为一个离散的数值,所以是一个典型的分类任务。
请注意,不要认为离散值和连续值就一定是井水不犯河水的关系。实际上,连续值和离散值之间的转化在通信原理上研究得较为透彻。例如,在连续值转变为离散值的过程中,和通信的定点脉冲抽样相似,也可能是通过傅丽叶变换从而只保留了在实轴上的“离散点”。总之,若要用算法进行描述,这是可以实现的。
传统的机器学习需要需要经历特征工程这一步骤,即将原始数据转化为向量形式。然后通过SVM、Random Forest等算法学习数据的规律。这些方法在处理简单的任务时是比较有效的。
向量的好处我已经多次描述过,这里不多费口舌了。但是至少这些典型的算法在运行效率和准确率上还是有明显差异的。
以曾经的某个项目,将1000条NFC信号的ATQA信号用于预测NFC的合法与否时,进行有监督模型训练的结果如下:
是的,很好,好到让人甚至怀疑是不是过拟合了。但是ATQA是ISO-14443-2中明确规定的必须保证容纳信号所有特征的一个数据包,这个包一旦发生了损坏,那就不是信号合不合法的问题,而是设备之间能不能握手成功的问题了。
即ATQA可以充分利用信号的所有特征,并提前将其进行结构化存储。
由此可见,对于传统的机器学习,特征工程对准确率的影响相当大。
但是我们从中间也可以看到以下区别:
-
随机森林算法最精确,树形结构相较于线性结构在描述数据联系上有着先天优势,最终模型参数的空间占用和遍历用时也是最小的。但是,由于树形结构较复杂,它的训练时间远高于k-NN和SVM。
-
k-NN由于只在k个数据之间进行选择,每轮遍历的数据较少,训练时间最少,但是,它的精度就不太行了,虽然已经够高了。
-
SVM根据选取需要进行分类的平面,会产生不同的效果。由于ATQA本身的数据结构是一个高维度的向量,所有的变量都是同等地位,因此线性平面可以达到最好的效果。最后的运行效率也是处于居中地位。
深度学习:让计算机拥有“神经”
深度学习是机器学习的一个子集,主要通过神经网络学习数据的特征和分布。深度学习的一个重要进化是不再需要繁琐的特征工程,让神经网络自己从里面学习特征。
但是,让深度学习以计算机的方式而非人的方式还原出莎士比亚的所有作品,我也不苟同它就是莎士比亚本人。
在本次的实践中,我们将会使用到RNN。RNN(循环神经网络)是一种深度学习模型,特别适合处理具有序列或顺序依赖的数据。这将有助于我们对于SMILES这种线性,有序的结构进行处理。如此我们便系上了第一粒扣子。
对于RNN每一层的神经元都会进行向量化的运算。激活函数使用tanh或者是ReLU,前者在原点回归,梯度计算简便且精度高,后者的速度甚至快于前者,但是若是不幸达到0点则可能会出现梯度丢失问题。
若是以全局的角度,则它是这样的结构:
在计算机中的代码实现是这样的:
# Efficient implementation equivalent to the following with bidirectional=False
def forward(x, h_0=None):
if batch_first:
x = x.transpose(0, 1)
seq_len, batch_size, _ = x.size()
if h_0 is None:
h_0 = torch.zeros(num_layers, batch_size, hidden_size)
h_t_minus_1 = h_0
h_t = h_0
output = []
for t in range(seq_len):
for layer in range(num_layers):
h_t[layer] = torch.tanh(
x[t] @ weight_ih[layer].T
+ bias_ih[layer]
+ h_t_minus_1[layer] @ weight_hh[layer].T
+ bias_hh[layer]
)
output.append(h_t[-1])
h_t_minus_1 = h_t
output = torch.stack(output)
if batch_first:
output = output.transpose(0, 1)
return output, h_t
实践过程
从现在开始,GPU启动。神经网络的计算量就没一个小的。
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
对于RNN的实现,pytorch已经帮我们实现好了,所以在下方就可以直接调用相关方法。
先对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
然后,对于定义数据处理函数,以及tokenizer,针对SMILES划分token。
现在,数据处理完毕了,开始投入训练!
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 = 10
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.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")
if __name__ == '__main__':
train()
我一开始看到这个占用和GPU的利用率都傻了,不是哥们,你显存占用那么多,怎么占用这么低啊?我专门打的Studio驱动啊!
然后,一个悲伤的故事就来了。
我要是有一张3090至于有这等事吗?显存就差那么一点点……
算了,最后反正也跑完了,太可怕了。
喜提-0.0813,梯度爆炸了。
看来还得继续调参,根据一个CTC-RNN项目的调参经验,为了避免学习卡死在预热阶段,需要将学习率进行调整。
关于调整参数,拟采用贪心算法,对于所有的参数调出最优的结果。
确定学习率的最佳数量级
此处将lr从0.001改为0.01,学习后得到模型分数为-0.0470,很好,离0又近了一步。
再次将lr从0.01改为0.1……好了可以停止了,第一个epoch就已经Loss高达0.369了,这个成绩不会好于0.01的水平的。
尝试将lr从0.001改为0.0001,epoch的Loss均未超过0.2,较0.01又好了很多。
再将lr从0.0001改为0.00001, epoch的Loss和0.0001相差不大。
将lr从0.00001改为0.000001,epoch的Loss再次突破到0.2。
最后再尝试将lr改为1,好了不用看了,越往上Loss越大
所以,确定lr的最优数量级将在0.0001和0.00001中胜出。
故此时尝试lr为0.0002的情况,Loss再一次突破的0.2,0.0001落选。
综上所述,lr的最优调参数量级是0.000010~0.000099上。
确定学习率的较佳数值
对于lr的选取,采用指数增长外加线性增长的方式减少尝试次数。
这里推荐写自动化比对算法,下面是部分运行过程中的细节。
lr = 0.000016,改进偏小,难以识别。
lr = 0.000032,从0.199到0.198,有明显进步
lr = 0.000064,从0.198回到0.199,又退步了
说明接下来在0.000032到0.000064间进行线性增长
lr = 0.00005,0.197->0.196->0.193->0.191->0.191
lr = 0.00004,0.197->0.195->0.193->0.191->0.191
lr接下来在0.00004到0.00005之间选取……
结论,精确到6位小数的情况下,lr = 0.000041 通常为最优。
关于dropout参数
理论上dropout应该为0的,但是随着Google Brain在15年专门发表的文章显示,在非循环阶段使用dropout,将改善过拟合现象。
原因是非循环阶段不存在权重的累乘效应,不会破坏学习过程。
显然,由于定义,dropout在0~1之间。
所以对dropout,同样可以先提取数量级再微调
此时将lr设定为0.000041
dropout = 0.1, 影响还不是很大
dropout = 0.01, 相差依然不是很大
dropout = 0.02, 发现Loss函数出现了很大的改善
最后经调参确定一个dropout = 0.015
关于epochs参数
epochs为循环的轮数,理论来说,轮数越高越好,但太高有过拟合的风险。
所以,epochs是友好的,因为epochs是一个整数,调参有下限,很方便。
至少,10轮是训练不出什么来的。经过同学的反馈,100轮能行,但性能不好,可能是过拟合了。所以,从50轮开始尝试。
epochs = 50, 于28轮时观察到Loss已经降至0.135,且每轮下降0.2~0.3,故epoch的上限约为72轮。
为了保证不会过拟合,epochs = 70进行最后一轮训练。
时间到了,实例被销毁了,理论上epochs=70会更好,但我没法验证了。
这次就这样吧,我累了。
8月1日更新:
尝试了epochs = 70,将Loss从0.091改善到0.073,但是最后的10轮左右很明显已经快跑不动了,平均Loss改善已经不足0.001,看来在epoch的改善上已经快到头了。
综上所述,epochs就取70了,再高估计就会有过拟合的风险了。
今天要是有时间就再尝试layer的调整。