@atry
2017-11-16T09:55:04.000000Z
字数 4630
阅读 2028
神经网络与函数式编程
在本系列的上一篇文章More than Machine Learning(一)从CEO的水晶球到神经网络编程中,我介绍了开源项目DeepLearning.scala的动机是成为一门具有学习能力的编程语言,即神经网络上的编程语言。在本篇文章中,我将探讨一个问题:怎样看待神经网络?
神经网络这个词,原本出自描述生物神经系统的神经科学,指的是生物体内神经元相互连接组成的网络。
1951年,马文·闵斯基借鉴“大脑模型”设计了最早的人工神经网络SNARC。在他设想中,大脑模型是个分形结构。每个神经元都有自己的输入和输出,若干互相连接的神经元组成一个小的神经网络。神经网络的输入和输出要比单个神经元复杂一些。而小的神经网络之间互相连接又组成了更大的神经网络。以此类推,最终这个“大脑模型”就是一个超大规模的神经网络。
类比生物神经系统,这是看待神经网络的第一种方式。
在机器学习领域,神经网络则被用来提取特征。这种视角下,每个神经元是一个特征。使用一个神经网络处理一条数据样本时,其中的神经元会有不同的激活程度。如果把处理一个样本时的多个神经元的激活程度看成一个特征向量,那么这个特征向量就是神经网络对这一个样本的编码。
神经网络的每一个激活模式对应了一个概念。假如两个样本都会导致同几个神经元被激活,那么就表示两个样本很类似,比如可能是近义词或者属于相同的分类。
比如,假如有一个word2vec模型,当用这个模型编码词语“Scala”时,编码后的特征向量中,“函数式编程”、“面向对象编程”、“JVM”等特征可能就会激活,而这些特征共同构成了“Scala”这一概念。
特征提取,这是看待神经网络的第二种方式。
两年前,谷歌大脑的科学家克里斯多夫·欧拉写了一篇文章Neural Networks, Types, and Functional Programming,他认为神经网络的结构和函数式编程是一样的。函数式编程可能会成为30年后人们看待神经网络的方式。
尽管克里斯多夫·欧拉指出了函数式编程和神经网络的对应关系,但当时这还只是个构想。而现在,在DeepLearning.scala中,他的构想已经成为现实。各种类型的神经网络都可以通过各种函数的相互组合构造出来。
接下来,我会用DeepLearning.scala用函数式编程的方式写一些神经网络的例子。这些例子我全部采用元素级别的高阶函数来编写。元素级别的高阶函数操作要比其他框架常用的向量操作更灵活、更强大,而且有可能更接近神经网络的本质。但是,DeepLearning.scala 2.x版本的元素级别操作不支持GPU,要比向量操作慢成千上万倍,只适合小数据量的算法原型。我们正在开发的DeepLearning.scala 3.0将会支持在GPU上运行的高性能元素级别操作。
假如我们要用神经网络做一个智商测试机器人,可以解答类似这样问题:
3,4,5 - 下一個數字是?
13,19,25 - 下一個數字是?
如果用DeepLearning.scala的话,我们可以把这个机器人设计成如下签名的函数:
def guessNextNumber(question: Seq[Double]): DoubleLayer = ???
然后可以这样使用机器人,让机器人作答:
println(guessNextNumber(Seq(3, 4, 5)).predict.blockingAwait)
println(guessNextNumber(Seq(13, 19, 25)).predict.blockingAwait)
我们可以发现,在调用智商测试机器人的predict方法时,guessNextNumber
可以接受普通的参数,像普通函数调用一样执行神经网络的推理。
这就是我们的第一个结论:神经网络是函数。
然而,从机器学习的定义上讲,神经网络用起来不像纯函数,因为纯函数要求对相同的输入值一定产生相同的輸出。然而,当我们构造智商测试机器人的时候,我们并不期待机器人一诞生就能工作得很好。它应该事先不懂得任何规律,要通过做题训练之后才逐渐学会做题。
比如这样:
def lossFunction(robotAnswer: DoubleLayer, expectedAnswer: Double): DoubleLayer = ???
def iqTestRobotTrainer(question: Seq[Double], expectedAnswer: Double): DoubleLayer = {
val robotAnswer = guessNextNumber(question)
lossFunction(robotAnswer, expectedAnswer)
}
iqTestRobotTrainer(Seq(3, 4, 5), 6).train.blockingAwait
iqTestRobotTrainer(Seq(13, 19, 25), 31).train.blockingAwait
以上代码中的lossFunction
是惩罚函数,用来评估智商测试机器人做题做得好不好。通过调用guessNextNumber
和lossFunction
,我们编写了一个新函数iqTestRobotTrainer
。向iqTestRobotTrainer
中输入一道题目和期待的答案,iqTestRobotTrainer
就返回一个表示惩罚值的DoubleLayer
。
注意:我们的智商测试机器人的惩罚函数lossFunction
本身也是一个子神经网络,同时也是个可以调用的函数。
“惩罚函数是个函数”听起来是一句废话。不幸的是,在其他框架中惩罚函数并不能可以随意调用的函数。比如在TensorFlow中,你可能会这样训练一个模型:
train_step = tf.train.GradientDescentOptimizer(0.5).minimize(loss_function)
sess.run(train_step, feed_dict=your_minibatch)
这两行代码中loss_function
必须要被TensorFlow特殊处理才能让TensorFlow启动训练。
而在DeepLearning.scala中,惩罚函数就是普通的Scala函数,返回的DoubleLayer
除了可以求值以外,还可以训练。
理想情况下,当iqTestRobotTrainer
的train
方法被反复调用时,智商测试机器人会逐渐学到训练数据中的规律,因而机器人的作答会逐渐接近正确答案,惩罚值也随之缩小。
这是第二个结论:神经网络是可以学习的函数。可以学习的函数暗示了副作用的存在,在本系列的后续篇章里,我会介绍DeepLearning.scala是如何用Monad封装这种副作用的。
神经网络是可以学习的函数,但并不能保证学会任意规律。比如智商测试机器人能学会哪些规律就取决于我们怎么编写惩罚函数lossFunction
和预测函数guessNextNumber
。
比如我们可以这样写:
// 平方惩罚函数
def lossFunction(robotAnswer: DoubleLayer, expectedAnswer: Double): DoubleLayer = {
val difference: DoubleLayer = robotAnswer - expectedAnswer
difference * difference
}
// 惰性初始化的权重列表
val weights: Stream[DoubleWeight] = Stream.continually(DoubleWeight(math.random))
// 用map/reduce编写的多项式加法预测函数
def guessNextNumber(question: Seq[Double]): DoubleLayer = {
(question zip weights).map {
case (element, weight) => element * weight
}.reduce(_ + _)
}
相比普通Scala代码,用DeepLearning.scala写的函数可以在内部引用一些Weight
。比如说上面的weights
中是个惰性初始化的列表,其中有无限多个DoubleWeight
。在训练过程中,这些Weight
会为了缩小loss
而朝着梯度下降的方向逐渐改变。
所以用DeepLearning.scala写的函数和普通函数之间的关系有点像PHP和HTML之间的关系,可以看成是为了生成代码而编写的代码模板。虽然整体代码结构由人类手写,但其中的Weight
部分则是机器自动学到的知识。
比如此处惩罚函数loss
是机器人作答和正确答案的差的平方,预测函数guessNextNumber
是个系数为weights
的多项式加法,只能拟合线性函数。所以它是个线性回归模型。一般机器学习算法往往会用向量运算来表达多项式求和等线性运算,但这些向量运算其实也可以用我们此处所用的函数式编程的map
/reduce
高阶函数表达。
DeepLearning.scala同样也支持向量运算,向量版的智商测试机器人的完整例子可以在Getting Started中查看。
TODO: 此处缺一张线性回归的网络结构的示意图
现在我们得出了第三个结论:神经网络是高阶函数模板。神经网络的能学到什么规律取决于人类手写模板的拟合能力。
从上面贴出的DeepLearning.scala的代码可以看出,神经网络是具备学习能力的函数模板。恕我直言,这从定义上已经等价于半自动的编程机器人了。
人类将来也许能造出胜任软件开发完整生命周期的机器人,但就眼下而言,软件开发机器人涉及的领域太多。特别是分析需求和架构设计对人工智能来说,需要的背景知识太多,问题域太开放,短时间内很难实现。
编程机器人,只解决在业务需求已经明确,软件模块已经划分好之后的一个小问题:
已知软件规格,如何写出相符的代码?
而软件规格,在采用TDD实践的前提下,可以通过测试用例来描述。
所以,编程机器人要解决的这个问题也可以表达为:
给定测试用例,如何写出能通过测试的代码?
在深度学习的场合,如果把上述定义中的“测试用例”用“训练数据”代替,把“通过测试”用“减小惩罚值”代替,就变成:
给定训练数据,如何让神经网络所拟合的函数的惩罚值最小?
那么,从定义上说,神经网络就是“由人类提供函数模板(即网络结构)”,“由机器提供权重”的半自动编程机器人。比如,能识别手写数字图片的神经网络,就是由机器自动编写的识别手写数字图片的程序。
之所以说是半自动编程机器人,是因为目前的人工神经网络仍然需要人类根据不同的训练数据选择不同的网络结构。假如有一天,所有的人工神经网络,不论面对任何数据,都能使用一种通用的网络结构,拟合出不亚于人类手动编写的函数的性能,那么就可以称得上是全自动编程机器人了。
在本篇文章中,我们讨论了看待神经网络的几种方式。最后提出了我们自己的视角,把神经网络看成具有学习能力的函数模板,等价于半自动辅助函数式编程的机器人。我将在下一篇文章中考察近年来广为使用的几种神经网络结构,以及与这些结构对应的DeepLearning.scala的高阶函数模板。