微调神经网络超参数
神经网络的灵活性其实也是它的缺点:有太多的超参数需要调整。神经网络的灵活性可以让我们使用任何想象中的网络架构,但是即使一个简单的MLP,我们也要考虑层的数量,每层神经元的数量,每层使用的激活函数的类型,权重初始化逻辑等等。那么我们要怎么知道那种超参数组合最适合解决问题呢?
一个解决方式就是尝试各种超参数组合方式然后看那种组合在验证集上的表现最好(或者使用K-fold交叉验证)。为此,我们可以使用网格搜索或者随机搜索方法,探索超参数空间。我们需要将Keras模型打包成一个对象,就像一般的Scikit-Learn回归器一样。第一步我们需要创建一个函数,用于创建和编译Keras模型,然后输入一系列超参数:
def build_model(n_hidden=1, n_neurons=30, learning_rate=3e-3, input_shape=[8]):
model = keras.models.Sequential()
model.add(keras.layers.InputLayer(input_shape=input_shape))
for layer in range(n_hidden):
model.add(keras.layers.Dense(n_neurons, activation="relu"))
model.add(keras.layers.Dense(1))
optimizer = keras.optimizers.SGD(lr=learning_rate)
model.compile(loss="mse", optimizer=optimizer)
return model
这个函数创建了一个简单的单变量回归Sequential模型,并向其传递了输入形状,层数和每层的神经元数,然后用SGD优化器编译并设置了学习率。代码中向模型提供了尽可能多的超参数默认值,这通常是比较好的做法。
接下来,我们创建一个基于上面的build_model()函数的KerasRegressor:
keras_reg = keras.wrappers.scikit_learn.KerasRegressor(build_model)
KerasRegressor对象是使用build_model()构建的Keras模型的一个简单包装。因为我们在创建它时没有指定任何超参数,所以它将使用我们在build_model()中定义的默认超参数值。现在我们可以像使用常规的Scikit-Learn回归器一样使用这个对象:我们可以使用fit()方法来训练它,然后使用score()方法来评估,并使用predict()方法进行预测。具体操作如下所示:
keras_reg.fit(X_train, y_train, epochs=100,
validation_data=(X_valid, y_valid),
callbacks=[keras.callbacks.EarlyStopping(patience=10)])
mse_test = keras_reg.score(X_test, y_test)
y_pred = keras_reg.predict(X_new)
注意,传递给fit()方法的任何额外的参数都将传递给底层的Keras模型。另外,score()方法的结果与MSE的相反,因为Scikit-Learn的理念是计算分数,而不是损失(即得分越高越好)
当然我们的目的并不是训练和评估一个模型,而是训练成百上千的超参数组合然后看哪种超参数组合在验证集上的表现最好。那么既然有那么多超参数需要调整,选用随机搜索的方法比网格搜索更佳。让我们尝试探索一下隐藏层和神经元的数量以及学习率:
from scipy.stats import reciprocal
from sklearn.model_selection import RandomizedSearchCV
param_distribs = {
"n_hidden": [0, 1, 2, 3],
"n_neurons": np.arange(1, 100),
"learning_rate": reciprocal(3e-4, 3e-2),
}
rnd_search_cv = RandomizedSearchCV(keras_reg, param_distribs, n_iter=10, cv=3)
rnd_search_cv.fit(X_train, y_train, epochs=100,
validation_data=(X_valid, y_valid),
callbacks=[keras.callbacks.EarlyStopping(patience=10)])
我们将额外的参数传递给fit()方法,并将它们传递给底层的Keras模型。注意RandomizedSearchCV
使用K-fold交叉验证,因此它不使用X_valid和y_valid,这两个值只用于早停。
超参数的探索可能需要几个小时,具体取决于硬件,数据量,模型复杂度,n_iter
和cv
的值。在运行结束之后,你就会得到最佳的超参数,得分,以及训练好的Keras模型:
>>> rnd_search_cv.best_params_
{'learning_rate': 0.0033625641252688094, 'n_hidden': 2, 'n_neurons': 42}
>>> rnd_search_cv.best_score_
-0.3189529188278931
>>> model = rnd_search_cv.best_estimator_.model
现在你就可以保存这个模型,在测试集上进行评估,如果你对这个结果比较满意,那么就可以部署生产了。使用随机搜索并不难,并且对于许多简单的问题表现得很好。但当训练速度很慢的时候(比如问题很复杂,数据量又很大),这个方法只能探索很小范围的超参数空间。我们可以手动帮助这个探索过程,让它稍微有所改善:首先使用大范围的超参数空间,快速运行一次随机搜索,然后在第一次的运行结果中找出最佳的超参数值,再在这个超参数值附近更小的空间运行一次随机搜索。这种方法可以手动缩小至一组较好的超参数。但是,这个方法仍然非常花时间,而且也不是最值得花时间的地方。
非常幸运的是,现在已经有很多技术可以帮助我们更高效地探索超参数空间了。这些技术的核心思路也很简单:当一个区域的超参数空间的结果不错,那么应该对这个区域进行更多的探索。这些技术帮助我们处理了缩小搜索空间的事情,并且用更少的时间得出更好的解决方案。下面列出了一下可以用来调参的python库:
- Hyperopt:用于优化各种复杂的搜索空间的Python库(比如学习率,离散值,隐藏层数)
- Hyperas,kopt,Talos:为Keras模型优化超参数而开发的库(前两个是基于Hyperopt)
- Keras Tuner:谷歌为Keras开发的很容易使用的超参数优化库,并且带有可视化和分析托管服务
- Scikit-Optimize(skopt):是一个通用的优化库。BayesSearchCV类执行贝叶斯优化,并且其接口与GridSearchCV十分相似
- Spearmint:一个贝叶斯优化库
- Hyperband:基于Lisha Li等人的论文《Hyperband: A Novel Bandit-Based Approach to Hyperparameter Optimization》开发的快速超参数调优库
- Sklearn-Deap:一个基于进化算法的超参数优化库,其接口也与GridSearchCV十分相似
另外,许多公司还提供超参数优化服务。我们在未来的文章中将讨论谷歌云AI平台的超参数调优服务。还有公司会提供超参数优化的API,比如Arimo,SifOpt和Oscar等。
超参数调优仍然是一个活跃的研究领域。进化算法最近又卷土重来了。比如,DeepMind在2017年发表了一篇优秀的论文:《Population Based Training of Neural Networks》,在论文中作者综合优化了一些模型以及它们的超参数。谷歌还使用了一种进化方法,不仅用于搜索超参数,还用于寻找解决问题的最佳神经网络体系结构。这个进化方法被称为AutoML,并已经可以在云端使用。也许这个技术会是人工构建神经网络的终结?有兴趣的朋友可以看谷歌关于这个项目的文章:
https://ai.googleblog.com/2018/03/using-evolutionary-automl-to-discover.html
实际上,进化算法已经替代无处不在的梯度下降法成功用于训练单个神经网络了。Uber就于2017年在官网上发表了一篇介绍他们深度神经进化技术的文章。
虽然我们有了这些令人兴奋的技术发展,还有各种工具和服务,但是了解每个超参数的合理范围仍然是有帮助的。这样我们就可以构建一个快速的原型并限制超参数搜索空间。
下一篇文章将讲述如何在MLP中选择隐藏层和神经元数量,以及选择其他重要超参数合适的值。
敬请期待啦!