[关闭]
@atry 2017-11-16T10:01:21.000000Z 字数 4694 阅读 1574

神经网络与函数式编程(五)怎么让神经网络自动调参?

在本系列的上一篇文章神经网络与函数式编程(四)Monadic Deep Learning中,我们学习了如何利用Monad创建支持粗粒度并行计算的动态神经网络,这种动态能力对于创建在所有领域都通用的模型至关重要。然而,仅仅是通用模型的话,仍然需要人类手动调整超参数,在本篇文章中,我们将展示在DeepLearning.scala中如何管理超参数。

两个方向的扩展性

在本系列的前几篇文章,我们探讨了深度学习和函数式编程的对应关系。神经网络结构就是高阶函数,唯一区别是,神经网络是个函数模板,其中包含了一些“权重”。权重有学习能力,会在训练过程中根据惩罚函数而改变。

理想情况下,权重应该自动学习,不用人类干预。不幸的是,现实中权重的学习方式与网络结构紧密关联,必须要人类手动配置优化器的“超参数”才能学得最好效果。

比如,我们在上一篇文章中介绍的通用神经网络内部包含各种各样的子网络,那么这些子网络就需要各种各样不同的超参数。

除了超参数的扩展性以外,我们还希望能够有办法能够扩展DeepLearning.scala的功能,让神经网络支持新网络结构。

换句话说,我们一共需要两个方向的扩展性:
* 增加新网络结构,用于新的子网络
* 针对每一套子网络里的每一个操作,都启用一套新的超参数

在DeepLearning.scala,我们可以把神经网络上的可微分编程看成一门领域特定语言,把原生的Scala看成元语言,用Scala进行可微分编程就是一种元编程。

如何在元语言中,向两个方向扩展领域特定语言?这其实是个很经典的问题,叫做The Expression Problem

对于Scala、Java或者C#这种支持泛型的面向对象语言,可以用Object Algebras来解决The Expression Problem。

比如,在DeepLearning.scala中,你可以创建两套不同的超参数:

  1. val hyperparameters1 = Factory[Builtins with FixLearningRate].newInstance(
  2. learningRate = 0.001
  3. )
  4. val weight1 = hyperparameters1.INDArrayWeight(Nd4j.randn(10, 10))
  5. def layer1[Input: DeepLearning.Aux[?, INDArray, INDArray]](input: Input): hyperparameters1.INDArrayLayer = {
  6. tanh(input dot weight1)
  7. }
  8. val hyperparameters2 = Factory[Builtins with FixLearningRate].newInstance(
  9. learningRate = 0.002
  10. )
  11. val weight2 = hyperparameters2.INDArrayWeight(Nd4j.randn(10, 10))
  12. def layer2[Input: DeepLearning.Aux[?, INDArray, INDArray]](input: Input): hyperparameters2.INDArrayLayer = {
  13. tanh(input dot weight2)
  14. }
  15. def twoLayerNeuralNetwork(input: INDArray): hyperparameters2.INDArrayLayer = {
  16. val layer1Output: hyperparameters1.INDArrayLayer = layer1(input)
  17. layer2(layer1Output)
  18. }

以上代码中,twoLayerNeuralNetwork是个双层神经网络,由layer1layer2两层组成。

hyperparameters1hyperparameters2三套超参在Object Algebras风格的程序中,可以看成两个抽象工厂,能生产不同的INDArrayLayerINDArrayWeight

由于我们把hyperparameters1的学习率配置为0.001,所以它生产出的weight1的学习率为0.001,以此类推,weight2的学习率为0.002layer1layer2是两个全连接层,激活函数都是tanh,权重分别为weight1weight2

与本系列前几篇文章的例子不同,此处layer1layer2是编译时的多态函数,input的类型Input不是具体的INDArray或者INDArrayLayer,而是任何支持DeepLearning.Aux[?, NDArray, NDArray]类型类的类型。DeepLearning是个Aux模式的类型类,用来见证Input是支持可微分的表达式,且其值和导数的类型都是INDArray

比如layer1(input)中的InputINDArray,而layer2(layer1Output)中的Inputhyperparameters1.INDArrayLayer,两处代码的Input类型虽然不同,但都可以编译,是因为无论INDArray还是hyperparameters1.INDArrayLayer都能够召唤对应的DeepLearning.Aux[?, NDArray, NDArray]类型类出来。非常简单!

插件的用法

你可能注意到,三套超参数构造时的参数有些不同。创建hyperparameters1hyperparameters2时,newInstance有一个learningRate参数,而创建hyperparameters3时,则没有向newInstance传入任何参数。

这是因为newInstance的参数是要由启用的插件决定的。一般来说,超参数可以用以下语法创建:

  1. val hyperparameters = Factory[Plugin1 with Plugin2 with Plugin3].newInstance(
  2. hyperparameterKey1 = hyperparameterValue1,
  3. hyperparameterKey2 = hyperparameterValue2,
  4. hyperparameterKey3 = hyperparameterValue3
  5. )

Plugin1Plugin2Plugin3是三个插件,hyperparameterKey1hyperparameterKey2hyperparameterKey3是插件所需的设置项。当选用了不同插件时,所需的设置项也会不同。

比如Builtins插件是DeepLearning.scala的内置插件,不需要任何设置项。

  1. val hyperparameters3 = Factory[Builtins].newInstance()

FixLearningRate插件则需要一个learningRate设置项。

  1. val hyperparameters1 = Factory[Builtins with FixLearningRate].newInstance(learningRate = 0.001)

在DeepLearning.scala中,插件既可以用来提供新功能,也可以用来修改现有功能的行为。以下是能用插件能的一些事情:

事实上,DeepLearning.scala 2.0所有功能全部都是通过插件方式提供的。

创建插件

提供新功能的插件

每个插件都是一个支持混入的Scala特质。在插件上定义的方法,可以被插件的用户所使用,比如你可以创建一个插件来提供softmax函数:

  1. trait Softmax extends Builtins {
  2. def softmax[Scores: DeepLearning.Aux[?, INDArray, INDArray]](scores: Scores): INDArrayLayer = {
  3. val expScores = hyperparameters.exp(scores)
  4. expScores / expScores.sum(1)
  5. }
  6. }

注意:extends Builtins表示Softmax插件依赖Builtins插件。这样一来在实现Softmax时就可以使用Builtins提供的功能了。

然后,用Softmax插件创建一组超参数,就可以调用softmax函数了:

  1. val hyperparameters = Factory[Softmax].newInstance()
  2. val probabilities = hyperparameters.softmax(Nd4j.randn(10, 10))

修改现有功能行为的插件

插件还可以修改现有功能的行为。这是因为,我们在本系列前面文章中所用的INDArrayLayerDoubleWeight等类型,都是可以抽象类型,可以被插件所更改。

比如,本文中用到的学习率插件,可以实现成这样:

  1. trait FixedLearningRate extends Builtins {
  2. def learningRate: Double
  3. trait INDArrayOptimizerApi extends super.INDArrayOptimizerApi { this: INDArrayOptimizer =>
  4. private lazy val delta0: INDArray = super.delta * learningRate
  5. override def delta: INDArray = delta0
  6. }
  7. override type INDArrayOptimizer <: Optimizer with INDArrayOptimizerApi
  8. }

这个插件做了两件事:

首先,声明了一个抽象方法learningRate。在DeepLearning.scala中,插件声明的所有抽象方法会自动成为构造超参数时的设置项,FixedLearningRate的用户可以这样用:

  1. Factory[Builtins with FixLearningRate].newInstance(learningRate = 0.001)

重载了抽象类型INDArrayOptimizer中的delta方法,将其实现为super.delta * learningRateINDArrayOptimizer是DeepLearning.scala中每个批次迭代时,会为每个权重创建的临时对象,负责优化权重。通过重载INDArrayOptimizer就可以改变优化时的行为。

结论

在通向全自动编程机器人的路途上,“自动调参”可能是必须迈过的一道坎。然而,欲速则不达。如果想要减少超参数,可能首先需要提供超参数被定制的可能。DeepLearning.scala的插件系统非常灵活,允许插件提供新功能、修改现有功能的行为,甚至也可以生成其他插件所需的超参数。我猜测,未来的“自动调参”神经网络可能正需要这样的插件系统,每个插件需要一些超参数,同时又为其他插件提供另一些超参数,当把多个插件组合到一起时,最终可以完全消除人类手写的超参。

到本篇文章为止,我们学到了深度学习和函数式编程的对应关系,以及如何用DeepLearning.scala创建函数式风格的神经网络。我将在本系列剩下的几篇文章揭示DeepLearning.scala的内部实现细节,看看DeepLearning.scala有哪些内部机制支撑了函数式风格的神经网络。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注