用Keras进行超参数优化
通过适当的流程,为给定的预测任务找到最先进的超参数配置并不困难。在三种方法中 - 手动,机器辅助和算法 - 本文将重点介绍机器辅助。
关于性能的几句话
关于性能的第一点涉及精度问题(以及其它更稳健的度量),作为衡量模型性能的方法。以f1 score为例。如果你有一个具有1%阳性的二元预测任务,那么一个使所有东西都为0的模型将接近完美的f1分数和准确性。这可以通过一些更改来处理,比如f1的分数处理诸如“全部0”、“全部1”和“没有真正的积极”等。“但这是一个很大的话题,超出了本文的范围,所以现在我只想说明,这个问题是让系统的超参数优化工作的一个非常重要的部分。”我们在这个领域有很多研究,但是研究更多地集中在算法上,而不是基础上。实际上,您可以拥有世界上最奇特的算法——通常也非常复杂——基于一个没有意义的度量来做决策。这对于处理“现实生活中的”问题并不是很有用。
即使我们的性能指标是正确的,我们也需要考虑在优化模型过程中会发生什么。我们有一个训练集,然后我们有一个验证集。一旦我们开始查看验证结果,并开始基于此进行更改,我们就开始对验证集产生bias。现在我们得到的训练结果是机器的bias的产物,我们有验证结果,这是我们的bias的产物。换言之,我们得到的模型不具有良好的广义模型的性质。相反,它偏离了被概括的状态。所以记住这一点是非常重要的。
关于超参数优化的一个更高级的全自动化(无监督)方法的关键在于,首先要解决这两个问题。一旦解决了这两个问题,并且有很多方法可以做到这一点,最终的结果将需要作为一个单独的分数来实现。然后,该分数成为优化超参数优化过程的指标。否则,世界上任何算法都不会有帮助,因为它将优化到其他东西,而不是我们所追求的东西。我们又在做什么?一个将完成预测任务的模型。不仅仅是一个案例的模型(在论文中经常涉及到这个主题),而是各种各样的模型,用于各种预测任务。这就是像Keras这样的解决方案允许我们做的事情,并且任何试图将使用像Keras这样的工具的过程自动化的尝试都应该接受这个想法。
我使用了什么工具?
对于本文中的所有内容,我使用Keras模型和Talos,这是我构建的超参数优化解决方案。好处是它不需要引入任何新的语法就可以按原样公开Keras。它让我可以在几分钟内完成过去需要几天的时间,而不是痛苦的重复。
你可以自己尝试一下:
pip install talos
自动化超参数优化和相关工具的一个更突出的问题是,你通常倾向于远离你习惯的工作方式。成功预测任务的关键在于不确定超参数优化问题,因为所有复杂的问题都是人与机器之间的合作。每一个实验都是一个了解更多关于深度学习的实践和技术的机会(在这种情况下,Keras)。不应错过过程自动化的机会。同时,我们应该能够夺走这个过程中明显多余的部分。考虑在Jupyter做移位几百次,在每次迭代之间等待一两分钟。总而言之,在这一点上,目标不应该是完全自动化的方法来找到正确的模型,而是在最小化人类负担的程序冗余。机器不是自己操作机器,而是自己运转。而不是逐一分析各种模型配置的结果,我想分析成千上万或数十万。每天有超过80000秒的时间,并且在这个时间里可以覆盖很多参数空间,而不需要我做任何事情。
Let’s Get Scannin’
为了举例,我将首先提供我在本文介绍的实验中使用的代码。我使用的数据集是威斯康星州乳腺癌数据集(https://www.kaggle.com/uciml/breast-cancer-wisconsin-data)
# first we have to make sure to input data and params into the function
def breast_cancer_model(x_train, y_train, x_val, y_val, params):
# next we can build the model exactly like we would normally do it
model = Sequential()
model.add(Dense(10, input_dim=x_train.shape[1],
activation=params['activation'],
kernel_initializer='normal'))
model.add(Dropout(params['dropout']))
# if we want to also test for number of layers and shapes, that's possible
hidden_layers(model, params, 1)
# then we finish again with completely standard Keras way
model.add(Dense(1, activation=params['last_activation'],
kernel_initializer='normal'))
model.compile(loss=params['losses'],
# here we add a regulizer normalization function from Talos
optimizer=params['optimizer'](lr=lr_normalizer(params['lr'],params['optimizer'])),
metrics=['acc', fmeasure])
history = model.fit(x_train, y_train,
validation_data=[x_val, y_val],
batch_size=params['batch_size'],
epochs=params['epochs'],
verbose=0)
# finally we have to make sure that history object and model are returned
return history, model
一旦Keras模型被定义,是时候决定初始参数边界了。然后将字典输入到进程中,以一种方式挑选单个置换,然后忽略。
# then we can go ahead and set the parameter space
p = {'lr': (0.5, 5, 10),
'first_neuron':[4, 8, 16, 32, 64],
'hidden_layers':[0, 1, 2],
'batch_size': (2, 30, 10),
'epochs': [150],
'dropout': (0, 0.5, 5),
'weight_regulizer':[None],
'emb_output_dims': [None],
'shape':['brick','long_funnel'],
'optimizer': [Adam, Nadam, RMSprop],
'losses': [logcosh, binary_crossentropy],
'activation':[relu, elu],
'last_activation': [sigmoid]}
根据我们希望在扫描中包含的损失,优化器和激活,我们需要首先从Keras中导入这些函数/类。接下来,随着模型和参数的准备就绪,是时候开始实验了。
# and run the experiment
t = ta.Scan(x=x,
y=y,
model=breast_cancer_model,
grid_downsample=0.01,
params=p,
dataset_name='breast_cancer',
experiment_no='1')
请注意,我不会分享更多的代码,因为我所做的只是更改参数字典中与以下部分中提供的见解相关的参数。为
因为在第一轮实验中有很多排列(总共超过180,000),我随机挑选总数的1%,剩下1,800个排列。
超参数扫描可视化
对于本文使用威斯康星州乳腺癌数据集,我已经建立了实验,假设没有关于最佳参数或数据集的先前知识。我已经通过删除一列来准备数据集,并通过转换其余所有元素,使每个特征的均值为0,标准偏差为1。
在最初运行1,800个排列之后,是时候看看结果,并决定如何限制(或改变)参数空间。
一个简单的排名顺序相关性表明,batch_size对我们的性能指标有最强的影响,在这种情况下,它是val_acc(验证准确性)。对于这个数据集,val_acc可以,因为有很多正数。对于False和Positives之间存在显着差异的数据集,精度不是一个好的指标。看起来,hidden_layers,lr(学习率)和dropout都与val_acc有显着的负相关。一个更简单的网络在这项任务中可以做得更好。对于正相关,epochs的数量是唯一突出的。让我们仔细观察一下。在下面的图表中,我们在x轴上有epochs(50,100和150),在y轴上有val_acc,列中的学习率和dropouts为色调。这种趋势似乎与相关性所暗示的一般相似; 较小的dropouts比较大的dropouts要好。
另一种查看dropout的方法是通过核密度估计。在这里,我们可以看到val_acc有一个小幅下降趋势,即0或0.1下降,以及val_acc下降的趋势(0.6标志附近)。
下一轮扫描的第一个动作项目是完全摆脱较高的dropout rates,并关注0到0.2之间的值。下面让我们更仔细地看看学习速率。注意,在优化器之间,学习速率是标准化的,其中1表示该优化器的Keras默认值。
情况非常清楚;较小的学习率对两种损失函数都很有效,这种差异在logcosh中特别明显。但是由于二元交叉熵在所有学习率的水平上明显优于其他的,这将是我们在剩下的实验中选择的损失。不过,还需要进行一次完整性检查。如果我们所看到的不是考虑到训练数据的过度拟合?一个简单的回归分析表明事实并非如此。这种趋势是,训练和验证损失都接近于零。
现在我们已经足够了解了; 是时候进行下一轮实验了!作为参考点,下一个实验的参数空间如下所示:
p = {'lr': (0.8, 1.2, 3),
'first_neuron':[4, 8, 16, 32, 64],
'hidden_layers':[0, 1, 2],
'batch_size': (1, 5, 5),
'epochs': [50, 100, 150],
'dropout': (0, 0.2, 3),
'weight_regulizer':[None],
'emb_output_dims': [None],
'shape':['brick','long_funnel'],
'kernel_initializer': ['uniform','normal'],
'optimizer': [Adam, Nadam, RMSprop],
'losses': [binary_crossentropy],
'activation':[relu, elu],
'last_activation': [sigmoid]}
除了细化学习率、dropout和batch size boundaries之外,我还添加了kernel_initializer的统一。记住,在这个阶段,目标是学习预测任务,而不是过于专注于寻找解决方案。这里的关键点是对整个过程的实验和学习,除了学习特定的预测挑战。
第二轮-增加对结果的关注。
一开始,我们对结果的关注越少(在过程中越多),我们就越有可能得到一个好的结果。这就像下棋;如果一开始你太专注于赢得比赛,你就不会专注于开局和中期。竞争国际象棋是在游戏的终局中获胜的,它的基础是一个强大的开始和中间。如果事情进展顺利,在超参数优化过程中的第二个迭代是中间的。我们还没有完全专注于赢得比赛,但它已经帮助我们关注了这个奖项。在我们的例子中,第一轮(94.1%验证精度)的结果表明,在给定的数据集和设置的参数边界上,有一些预测。
在这种情况下,这里的预测任务是说乳腺癌是良性的还是恶性的。这种预测是一种很重要的观点认为假阳性和假阴性都很重要。预测错误会对人的生活产生负面影响。
第二轮的结果是96%的验证精度。下面的相关性表明,在这一点上唯一突出的是,epochs的数量,所以对于第三轮,这是我要改变的一件事。
如果你只看相关性,就会发现在更大的情况下遗漏了一些东西。在超参数优化中,大图是关于给定参数中的单个值,以及它们与所有其他值的相互连接。现在我们已经消除了logcosh损失函数,并且在参数空间中只有一个损失(binary_cross熵),我想了解一下不同的优化器是如何在epochs上下文中执行的。
这与关联建议关于epochs(现在在X轴上)完全相同。由于RMSprop在100和150两者都表现不佳,我们也会在下一轮中放弃。
在继续之前,让我们简单考虑一个与超参数优化相关的基本问题,作为优化挑战。我们试图达到什么目标?答案可以用两个简单的概念来概括;
预测最佳
结果熵
预测最佳是我们有一个既精确又广义的模型。结果熵是熵尽可能接近零(最小)的地方。结果熵可以理解为结果集内所有结果(经历n个排列的一轮)之间的相似性度量。理想情况是预测最优值为1,即100%预测性能和100%通用性,并且得到的熵为0.这意味着无论我们在超参数空间内做什么,我们每次只能得到完美的结果。由于多种原因,这是不可行的,但有助于记住优化超参数优化过程的目标。另一种回答问题的方式是通过三个层面的考虑;
预测任务,其目标是找到一个为任务提供解决方案的模型
超参数优化任务,其目标是为预测任务找到最佳模型(尽力而为)
超参数优化任务优化任务,其目标是找到寻找预测任务最佳模型的最佳方法的最佳方法
然后你可能会问,如果这导致我们无限进展,那么我们需要优化器之上的优化器,答案是肯定的。在我看来,使超参数优化问题变得有趣的是它将解决方案引入“构建模型的模型”问题的方式。但这会使我们远离本文的讨论范围。
考虑到第二个,特别是第三个方面,我们需要考虑过程的计算效率。我们浪费的计算资源越少,为了找到关于方面1和方面2的最佳结果,我们就越有用。从这个角度考虑下面的图表。
第二轮KDE看起来要更好一些,因为我们需要它们的资源分配。它们在x轴上更接近1,而对于0的“溢出”则很少。无论什么计算资源进入扫描,它们都在做重要的工作。理想的图像是一条直线与x的值为1。
第3轮 - 泛化和性能
让我们开始吧。现在最高验证准确率为97.1%,看起来我们正朝着正确的方向前进。我犯了一个错误,就是最多添加了175个纪元,并且基于下面的内容。看起来我们必须走得更远。至少在这种配置下。这让我想......也许在最后一轮也是最后一轮,我们应该尝试一些令人惊讶的事情。
正如前面所讨论的那样,重要的是要考虑泛化。每次我们看结果时,我们的见解开始影响实验。最终的结果是我们开始获得较少的与验证数据集一起工作的广义模型,但可能不适用于“现实生活”数据集。在这种情况下,我们没有一个好的方法来测试这种偏见,但至少我们可以采取措施来评估我们所拥有的伪泛化程度。我们先看看训练和验证的准确性。
尽管这并没有给我们一个确定的概括性模型,事实上,它没有达到很好的效果; 回归分析结果不会好得多。那么让我们看看损失。
它甚至更好。事情看起来不错。对于上一轮,我将增加epochs的数量,但我也将尝试另一种方法。到目前为止,我只有很小的批量,需要花费很多时间来处理。在第三轮中,我只包括批量大小1到4.对于下一个,我将抛出30或什么,看看有什么。
关于提早停止的几句话。Keras提供了一种通过EarlyStopping功能使用回调的非常方便的方法。正如你可能已经注意到的,我没有使用它。一般来说,我会推荐使用它,但它不像我们迄今为止所做的一切那么微不足道。以不限制您找到最佳可能结果的能力来正确设置设置并非易事。最重要的方面与指标有关; 我想要首先创建一个自定义指标,然后将其用作EarlyStopping模式(而不是使用val_acc或val_loss)。也就是说,EarlyStopping和一般的回调函数提供了一种非常强大的方法来添加到超参数优化过程中。
第4轮 - 最终结果
在深入研究结果之前,让我们从上一轮结果中再看一个可视化图。这次是5维的。我希望看到剩下的参数 - 内核初始化程序,批处理大小,隐藏层和时代 - 全部在相同的情况下与验证准确性和丢失进行比较。第一精度。
大多数情况下,这是neck-to-neck,但有些事情确实很突出。首先,如果隐藏图层值(hue)失效,则在大多数情况下,其隐藏的图层是一层。对于批量大小(列),很难说,就像内核初始化程序(行)一样。我们接下来看看Y轴上的验证损失,看看我们是否可以从那里学到更多。请记住,我们正在寻找更小的值; 我们试图使每个参数排列的损失函数最小化。
统一内核初始化器在保持整个epoch,批量大小和隐藏层变化的损失方面做得非常出色。但是因为结果有点不一致,所以我会保留两个初始化程序直到结束。
最终获胜者是…
获胜的组合来自最后一分钟的想法,尝试更大的批量以节省时间,并以更少的时间进行):
小批量的最高结果是验证准确率为97.7%。采用更大规模的批量生产方式,模型的收敛速度也非常快。还有一件事是我想分享的,因为它与我们已经讨论过的不同观点有关的熵概念。熵可以是评估过度拟合的有效方法(因此也是泛化的代理)。在这种情况下,我使用KL散度测量val_loss和val_acc熵,分别针对训练损失和准确度。
过程摘要
尽可能简单而广泛地开始
尝试尽可能多地了解实验和你的假设
尽量不要专注于第一次迭代的最终结果
确保你的表现指标是正确的
记住,性能是不够的,因为它往往会导致你远离一般性
每次迭代应该减少参数空间和模型复杂性
不要害怕尝试,毕竟这是一个实验
使用可以理解的方法,例如清晰可见的描述性统计