电子说
编者按:还认为调参是“玄学”?快来看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散度),它们是一种有效评估过拟合的方法(因此也是间接评估概括性的方法)。
总结
尽可能保持简单和广泛
从试验和假设中分析出尽可能多的结果
在初次迭代时不用在意最终结果
确保采用了恰当的表现测度
记住表现本身并不是全部,提升表现的同时往往会削弱概括性
每次迭代都应该缩减超参数空间和模型复杂性
别害怕尝试,毕竟这是试验
使用你可以理解的方法,例如,清晰的可视化描述性统计
全部0条评论
快来发表一下你的评论吧 !