@atry
2017-11-16T09:57:19.000000Z
字数 3722
阅读 1703
神经网络与函数式编程
在本系列的上一篇文章神经网络与函数式编程(二)神经网络就是编程机器人?中,我们讨论了看待神经网络的几种视角,最后发现,神经网络其实是具有学习能力的高阶函数模板,可以看成半自动的编程机器人。在本篇文章中,我们会考察近年来广为使用的几种网络结构,以及它们在DeepLearning.scala中对应怎样的函数模板。
这些函数模板和神经网络之间的对应概念是由克里斯多夫·欧拉在Neural Networks, Types, and Functional Programming中指出的。不过,本文贴出的DeepLearning.scala代码真正可以执行,而不仅仅是概念。
循环神经网络编码器可以从序列输入中提取一组特征,比如输入一段淘宝买家写的评论,让编码器预测这段文字是个好评还是差评。
循环神经网络编码器可以用Scala中的foldLeft
实现,其签名如下:
package scala.collection
trait Seq[A] {
def foldLeft[B](z: B)(op: (B, A) => B): B
}
循环神经网络有很多变种,比如GRU和LSTM。我们可以把不同的变种看成是循环体单步执行时采用了不同的step
函数实现。
type Input = Seq[DoubleLayer]
type State = Seq[DoubleLayer]
def step(hiddenState: State, xi: Input): State = ???
def encodingRNN(x: Seq[Input]): State = {
val initialState: State = Seq.empty[DoubleLayer]
x.foldLeft(initialState)(step)
}
比如如果step
采用简单的tanh
的话,可以这样写:
val NumberOfOutputFeatures = 10
val weight: Seq[Seq[DoubleWeight]] = Seq.fill(NumberOfOutputFeatures)(Stream.continually(DoubleWeight(math.random)))
def step(hiddenState: State, xi: Input): State = {
tanh(matrixMultiply(hiddenState ++ xi, weight))
}
解码器接受一个种子参数作为隐藏状态不断迭代,输出一个序列,对应Scalaz的高阶函数unfoldr
。
package scalaz
object DList {
def unfoldr[A, B](b: B, f: B => Option[(A, B)]): DList[A]
}
type State = Seq[DoubleLayer]
type Output = Seq[DoubleLayer]
def step(hiddenState: State): Option[(State, Output)] = ???
def generatingRnn(seed: State): DList[Output] = {
DList.unfoldr(seed)(step)
}
一对一转换的RNN不经过编码器和解码器,而是对输入序列中的每一个元素一一转换,直接生成输出序列。适合用于输入和输出长度相同的场合。
一对一转换的RNN对应Scalaz的高阶函数mapAccumLeft
或者mapAccumRight
。
package scalaz
class IList[A] {
def mapAccumLeft[B, C](c: C)(f: (C, A) => (C, B)): (C, IList[B])
def mapAccumRight[B, C](c: C)(f: (C, A) => (C, B)): (C, IList[B])
}
type State = Seq[DoubleLayer]
type Input = Seq[DoubleLayer]
type Output = Seq[DoubleLayer]
def step(hiddenState: State, xi: Input): (State, Output) = ???
def rnn(x: IList[Input], seed: State): (State, IList[Output]) = {
x.mapAccumLeft(seed)(step)
}
双向循环神经网络简单的把从左到右和从右到左两个子网络输出结果拼起来即可。
type State = Seq[DoubleLayer]
type Input = Seq[DoubleLayer]
type Output = Seq[DoubleLayer]
def leftStep(hiddenState: State, xi: Input): (State, Output) = ???
def rightStep(hiddenState: State, xi: Input): (State, Output) = ???
def bidirectionalRnn(x: IList[Input], leftSeed: State, rightSeed: State): IList[Output] = {
val (_, leftToRight) = x.mapAccumLeft(leftSeed)(leftStep)
val (_, rightToLeft) = x.mapAccumRight(rightSeed)(rightStep)
leftToRight.zip(rightToLeft).map { pair =>
pair._1 ++ pair._2
}
}
卷积神经网络需要用Scala中的zip
和map
组合起来实现。
package scala.collection.immutable
class List {
def map[B](f: A => B): List[B]
def zip[B](that: List[B]): List[(A, B)]
}
type Input = Seq[DoubleLayer]
type Output = Seq[DoubleLayer]
def kernel1x2(xi: Input, xj: Input): Output
def cnn1d(x: List[Input]): List[Output]) = {
x.zip(x.tail).map(kernel)
}
二维卷积神经网络和一维卷积神经网络类似,也可以用Scala中的zip
和map
实现。
type Input = Seq[DoubleLayer]
type Output = Seq[DoubleLayer]
def kernel2x2(xi: Input, xj: Input, xk: Input, xl: Input): Output
def cnn2d(x: List[List[Input]]): List[Output]) = {
val x00 = x
val x01 = x.map(_.tail)
val x10 = x.tail
val x11 = x.tail.map(_.tail)
(x zip x01 zip x10 zip x11).map {
case (((xi, xj), xk), xl) => kernel2x2(xi, xj, xk, xl)
}
}
空间递归神经网络主要用于自然语言处理,对应Scalaz的scanr
。
package scalaz
class Tree {
def scanr[B](g: (A, Stream[Tree[B]]) => B): Tree[B]
}
type Input = Seq[DoubleLayer]
type Output = Seq[DoubleLayer]
def step(x: Input, children: Stream[Output]): Output = ???
def treeNet(x: Tree[Input]): Tree[Output] = {
x.scanr(step)
}
传统机器学习背景的数据科学家常常觉得神经网络是个黑箱,对深度学习的可解释性会有疑虑。然而当我们把这些特定领域适用的网络结构对应的函数模板写下来时,对于有函数式编程经验的工程师来说,神经网络很好解释。
过去工程师会用foldLeft
遍历字符串搜索特定模式,现在神经网络一样是foldLeft
对文本模式匹配。过去写一个编译器需要有多个编译阶段,每个阶段处理前一个阶段的输出,现在用多层神经网络,每一层一样是处理前一层的输出。唯一的区别是,过去工程师硬编码的规则被自动优化的权重所代替。
从实际效果上看,这正体现了神经网络就是半自动编程机器人,让工程师需要编写的程序变少了。
如果想要进一步提高编程机器人的自动化程度,要制造全自动编程机器,则需要发明一种能拟合一切人类手写函数的通用函数模板,让人类不再需要手动编写领域特定的函数模板。“能拟合”不是指数学意义上的“可计算”,而是指实践中能用比人类编程更短的时间训练出比手写代码更优的函数。很容易想到的一种实现通用模板的思路,就是让通用模板中包含各种特定领域的子网络,自动根据样本的特性,有条件的启用适合当前样本的子网络。我将在下一篇文章中介绍如何在DeepLearning.scala中如何用Monad创建有条件启用的子网络。