调参心得:如何优化超参数的,如何证实方法是有效的

电子说

1.3w人已加入

描述

编者按:还认为调参是“玄学”?快来看Mikko Kotila分享的调参心得。

TL;DR

只需采用正确的过程,为给定的预测任务找到顶尖的超参数配置并非难事。超参数优化主要有三种方法:手工、机器辅助、基于算法。本文主要关注机器辅助这一方法。本文将介绍我是如何优化超参数的,如何证实方法是有效的,理解为何起效。我把简单性作为主要原则。

模型表现

关于模型表现,首先需要指出的是,使用精确度(及其他鲁棒性更好的测度)等衡量模型表现可能有问题。例如,假设一个二元预测任务中只有1%的样本值为1,那么预测所有值为0的模型将达到近乎完美的精确度。采用更合适的测度可以克服这类问题,但限于本文的主题,我们不会详细讨论这些。我们想要强调的是,优化超参数的时候,这是一个非常重要的部分。即使我们采用了世界上最酷炫的模型(通常是非常复杂的模型),但如果评估模型所用的是无意义的测度,那到头来不过是白费工夫。

别搞错;即使我们确实正确使用了表现测度,我们仍然需要考虑优化模型的过程中发生了什么。一旦我们开始查看验证集上的结果,并基于此做出改动,那么我们就开始制造倾向验证集的偏差。换句话说,模型的概括性可能不怎么好。

更高级的全自动(无监督)超参数优化方法,首先需要解决以上两个问题。一旦解决了这两个问题——是的,存在解决这两个问题的方法——结果测度需要实现为单一评分。该单一评分将作为超参数优化过程所依据的测度。

工具

本文使用了Keras和Talos。Talos是我创建的超参数优化方案,它的优势在于原样暴露了Keras,没有引进任何新语法。Talos把超参数优化的过程从若干天缩短到若干分钟,也使得优化过程更有意思,而不是充满了痛苦的重复。

你可以亲自尝试Talos:

pip install talos

或者在GitHub上查看它的代码和文档:autonomio/talos

但我打算在本文中分享的信息,提出的观点,是关于优化过程的,而不是关于工具的。你可以使用其他工具完成同样的过程。

自动化超参数优化及其工具最主要的问题之一,是你常常偏离原本的工作方式。预测任务无关的超参数优化的关键——也是所有复杂问题的关键——是拥抱人机之间的协作。每次试验都是一个学习更多(深度学习的)实践经验和技术(比如Keras)的机会。不应该因为自动化过程而失去这些机会。另一方面,我们应该移除优化过程中明显多余的部分。想象一下在Jupyter notebook中按几百次shift-enter(这一快捷键表示执行代码),然后在每次执行时等待一两分钟。总之,在现阶段,我们的目标不应该是全自动方法,而是最小化让人厌烦的重复多余部分。

开始扫描超参数

在下面的例子中,我使用的是Wisconsin Breast Cancer数据集,并基于Keras构建了以下模型:

def breast_cancer_model(x_train, y_train, x_val, y_val, params):

model = Sequential()

model.add(Dense(10, input_dim=x_train.shape[1],

activation=params['activation'],

kernel_initializer='normal'))

model.add(Dropout(params['dropout']))

hidden_layers(model, params, 1)

model.add(Dense(1, activation=params['last_activation'],

kernel_initializer='normal'))

model.compile(loss=params['losses'],

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)

return history, model

定义好Keras模型后,通过Python字典指定初始参数的边界。

p = {'lr': (0.5, 5, 10),

'first_neuron':[4, 8, 16, 32, 64],

'hidden_layers':[0, 1, 2],

'batch_size': (1, 5, 5),

'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]}

一切就绪,到了开始试验的时候了:

t = ta.Scan(x=x,

y=y,

model=breast_cancer_model,

grid_downsample=0.01,

params=p,

dataset_name='breast_cancer',

experiment_no='1')

注意,为了节省篇幅,代码省略了引入语句等非关键性的代码。下文修改超参数字典时也不再贴出代码。

因为组合太多(超过180000种组合),我随机从中抽取了1%的组合,也就是1800种组合。

在我的2015年MacBook Air上,试验1800种组合大约需要10800秒,也就是说,我可以和朋友见一面,喝上一两杯咖啡。

可视化超参数扫描

试验了1800种组合后,让我们看看结果,从而决定如何限制(或者调整)参数空间。

我们使用val_acc(验证精确度)作为评估模型表现的指标。我们所用的数据集类别比较均衡,因此val_acc是个不错的测度。在类别显著失衡的数据集上,精确度就不那么好了。

从上图我们可以看到,似乎hidden_layers(隐藏层数目)、lr(学习率)、dropout对val_acc的影响较大(负相关性),而正相关性比较强的只有epoch数。

我们将val_acc、hidden_layers、lr、dropout这些单独抽出来绘制柱状图:

其中,y轴为精确度,x轴为epoch数,色彩浓淡表示dropout率,刻面(facet)表示学习率。

上面的两张图告诉我们,在这一任务上,看起来相对简单的模型表现较好。

现在让我们通过核密度估计仔细看看dropout。纵轴为精确度,横轴为dropout率。

从图中我们可以看到,dropout为0到0.1时,更可能得到较高的验证精确度(纵轴0.9附近),不太可能得到较低的精确度(纵轴0.6附近)。

所以我们下一回合扫描超参数的时候就可以去掉较高的dropout率,集中扫描0到0.2之间的dropout率。

接下来我们再仔细看看学习率(不同优化算法的学习率经过了归一化处理)。这次我们将绘制箱形图,纵轴为验证精确度,横轴为学习率。

很明显,在两种损失函数上,都是较低的学习率表现更好。在logcosh上,高低学习率的差异尤为明显。另一方面,在所有学习率水平上,交叉熵的表现都超过了logcosh,因此在之后的试验中,我们将选择交叉熵作为损失函数。不过,我们仍需要验证一下。因为我们看到的结果可能是过拟合训练集的产物,也可能两者的验证集损失(val_loss)都很大。所以我们进行了简单的回归分析。回归分析表明,除了少数离散值外,绝大多数损失都聚集在左下角(这正是我们所期望的)。换句话说,训练损失和验证损失都接近零。回归分析打消了我们之前的疑虑。

我觉得我们已经从初次试验中得到足够多的信息,是时候利用这些信息开始第二次试验了。除了上面提到的改动之外,我还额外增加了一个超参数,kernel_initializer。在第一次的试验中,我们使用的都是默认的高斯分布(normal),其实均匀分布(uniform)也值得一试。所以我在第二次试验中补上了这一超参数。

第二回合——进一步关注结果

之前我们分析第一回合的结果时,关注的是超参数和验证精确度的相关性,但并没有提到验证精确度有多高。这是因为,在一开始,我们更少关注结果(更多关注过程),我们最终取得较好结果的可能性就越高。也就是说,在开始阶段,我们的目标是了解预测任务,而并不特别在意找到答案。而在第二回合,我们仍然不会把全部注意力放到结果上,但查看一下结果也是有必要的。

第一回合的验证精确度峰值是94.1%的,而第二回合的验证精确度峰值是96%的。看起来我们的调整还是有效的。当然,峰值可能仅仅源于抽样的随机性,所以我们需要通过核密度分布估计来验证一下:

第一回合的核密度分布估计

第二回合的核密度分布估计

对比核密度分布估计,我们看到,我们的调整确实有用。

下面我们再次绘制相关性热图:

我们看到,除了epoch数以外,没有什么对验证精确度影响非常大的因素了。在下一回合的试验中,我们该调整下epoch数。

另外,热图并没有包含所有超参数,比如上一节中的损失函数。在第一次试验后,我们调整了损失函数,移除了logcosh损失。现在让我们查看一下优化算法。

首先,上面的箱形图再次印证了我们之前提到的,较低的epoch数表现不好。

其次,由于在epoch数为100和150的情形下,RMSprop的表现都不怎么好,所以我们将在下一回合的试验中移除RMSprop。

第三回合——概括性和表现

经过调整之后,第三回合试验的验证精确度峰值提高到了97.1%,看起来我们的方向没错。在第三回合的试验中,epoch数我去掉了50,将最高epoch数增加到了175,另外还在100和150中间加了125。从下图来看,我可能过于保守了,最高epoch数应该再大一点。这让我觉得……也许最后一回合步子可以迈得大一点?

正如我们一开始提到的,在优化超参数时,同时考虑概括性很重要。每次我们查看在验证集上的结果并据此调整时,我们增加了过拟合验证集的风险。模型的概括性可能因此下降,虽然在验证集上表现更好,但在“真实”数据集上的表现可能不好。优化超参数的时候我们并没有很好的测试这类偏差的方法,但至少我们可以评估下伪概括性(pseudo-generalization)。让我们先看下训练精确度和验证精确度。

虽然这并不能确认我们的模型概括性良好,但至少回归分析的结果不错。接下来让我们看下训练损失和验证损失。

这比训练精确度和验证精确度的回归分析看起来还要漂亮。

在最后一回合,我将增加epoch数(之前提到,第三回合的增加太保守)。另外,我还会增加batch尺寸。到目前为止,我仅仅使用了很小的batch尺寸,这拖慢了训练速度。下一回合我将把batch尺寸增加到30,看看效果如何。

另外提下及早停止(early stopping)。Keras提供了非常方便的及早停止回调功能。但你可能注意到我没有使用它。一般来说,我会建议使用及早停止,但在超参数优化过程中加入及早停止不那么容易。正确配置及早停止,避免它限制你找到最优结果,并不那么直截了当。主要是测度方面的原因;首先定制一个测度,然后使用及早停止,效果比较好(而不是直接使用val_acc或val_loss)。虽然这么说,但对超参数优化而言,及早停止和回调其实是很强大的方法。

第四回合——最终结果

在查看最终结果之前,先让我们可视化一下剩下的超参数(核初始化、batch尺寸、隐藏层、epoch数)的效果。

纵轴为验证精确度

大部分结果都是肩并肩的,但还是有一些东西突显出来。如果因为隐藏层层数不同(色彩浓淡)导致验证精确度下降,那么大多数情形下,下降的都是1个隐藏层的模型。至于batch尺寸(列)和核初始化(行)的差别,很难说出点什么。

下面让我们看看纵轴为验证损失的情况:

纵轴为验证损失

在各种epoch数、batch尺寸、隐藏层层数的组合下,均匀核初始化都能将验证损失压得很低。但因为结果不是特别一致,而且验证损失也不如验证精确度那么重要,所以我最后同时保留了两种初始化方案。

最后的赢家

我们在最后时刻想到增加batch尺寸,这个主意不错。

较小的batch尺寸下,验证精确度的峰值是97.7%,而较大的batch尺寸(30)能将峰值提升至99.4%。另外,较大的batch尺寸也能使模型更快收敛(你可以在文末的视频中亲眼见证这一点)。老实说,当我发现较大的batch尺寸效果这么好时,我其实又进行了一次试验。因为只需要改动batch尺寸,不到一分钟我就配置好了这次试验,而超参数扫描则在60分钟内完成了。不过这次试验并没有带来什么新发现,大部分结果都接近100%.

另外我还想分享下精确度熵和损失熵(基于验证/训练精确度、验证/训练损失的KL散度),它们是一种有效评估过拟合的方法(因此也是间接评估概括性的方法)。

总结

尽可能保持简单和广泛

从试验和假设中分析出尽可能多的结果

在初次迭代时不用在意最终结果

确保采用了恰当的表现测度

记住表现本身并不是全部,提升表现的同时往往会削弱概括性

每次迭代都应该缩减超参数空间和模型复杂性

别害怕尝试,毕竟这是试验

使用你可以理解的方法,例如,清晰的可视化描述性统计

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分