跟着吴恩达学深度学习:用Scala实现神经网络-第三课:消除所有的可变变量,利用scanLeft将神经网络重构为函数式风格

在之前的神经网络实现中,有一些地方用到了修饰符为var的可变变量。比如在神经网络forward的计算过程中,每一层神经层的输入都是上一层的输出。在我固有的编程思维中,不可避免地将变量yPrevious设置成var类型,并且在每一层神经层计算结束后,将本层的计算结果重新赋值给yPrevious以供下一层使用。

 

但是在函数式编程风格中,这种对可变变量使用的情形其实是可以避免的,我们的解决方案就是ScalaCollections中的scanLeft方法。

 

这里要强调的一点是,一个变量的修饰符为val,指该变量所refer的对象是不可变的,但是该对象本身可能是可变的数据结构,如ArrayBuffer。

 

在本项目的神经网络实现中,暂时发现有三个地方可以利用scanLeft对代码进行函数式重构,下面一一说明。本文分为四个部分,首先对scanLeft的功能进行介绍;然后对神经网络的forward、backward和setHiddenLayersStructure三个方法分别进行函数式重构。

 

1.    scanLeft功能简介

 

scanLeft顾名思义,会对一个collection从左至右扫描并执行一定的操作,然后生成一个新的collection。下面用一个例子进行说明会显得更加直观:

vallistA = List(1, 2, 3)

val scanedList = listA
  .scanLeft[
Int, List[Int]](0){
  (a
, b) => a - b
}

 

得到的结果是:scanedList: List[Int] = List(0, -1,-3, -6)。其具体的计算过程为:

1)      首先设置一个初始值,本例中为0;

2)      然后用初始值减去listA的第一个元素,即0-1=-1;

3)      再用上一个阶段的计算结果减去listA的第二个元素,即-1-2=-3;

4)      下一步就是-3-3=-6;

5)      将每一步的结果按照顺序组合成一个新的List,即得到List(0, -1, -3, -6)。

 

其中scanLeft所接受的两个泛型参数[Int, List[Int]]的含义为:Int为初始值和变量a的类型;List[Int]为最终得到的结果的类型。另外变量b的类型为listA的元素类型。

 

接下来看看scanLeft可以对神经网络的实现代码进行怎样的函数式重构。

 

2.    神经网络forward运算的函数式重构

 

首先看看没用scanLeft之前我们的forward代码:

protected def forward(feature: DenseMatrix[Double],
                      params: List[(DenseMatrix[Double],
                        DenseVector[Double])]): List[ForwardRes] = {
  
  var yPrevious = feature 
  params
    .zip(this.allLayers)
    .map{f => 
      val (w, b) = f._1
      val layer = f._2
      val forwardRes = layer.forward(yPrevious, w, b)
      yPrevious = forwardRes.yCurrent
      
      forwardRes 
    }
}

 

可以看到里面使用了修饰符为var的yPrevious变量,并且在map方法中,每一层神经网络的前向计算完毕后,都会将该层的yCurrent结果重新赋值给yPrevious,以供下一层神经网络的前向计算使用。根据我们第一部分的解释可以知道,凡是碰到这种前一轮迭代的计算结果是后一轮迭代的输入参数的时候,都可以用scanLeft将其改造为不用var修饰符的函数式代码。接下来看我们重构后的代码:

 

protected def forward(feature: DenseMatrix[Double],
                      params: List[(DenseMatrix[Double],
                        DenseVector[Double])]): List[ForwardRes] = {

  val initForwardRes = ForwardRes(null, null, feature)
  params
    .zip(this.allLayers)
    .scanLeft[ForwardRes, List[ForwardRes]](initForwardRes){
    (previousForwardRes, f) =>
      val yPrevious = previousForwardRes.yCurrent

      val (w, b) = f._1
      val layer = f._2

      layer.forward(yPrevious, w, b)
    }
    .tail
}

 

在上面的代码中,首先我们构造一个初始值initForwardRes,并将其作为scanLeft的初始值传入;然后在每一层的计算过程中,都将前一层的yCurrent赋值给本层计算的yPrevious,并将其当作参数传入本层layer.forward()中。因为最终我们只需要每一层的计算结果,而不需要初始值initForwardRes,所以在最后取tail将list的首个元素去掉。

 

上面我们了解到怎么使用scanLeft来重构神经网络forward计算过程的代码,理所当然可以想到还有scanRight方法,其功能与scanLeft一模一样,不过是从collection的右边第一个元素开始计算。接下来我们将看到如何使用scanRight对神经网络的backward计算过程进行函数式重构。

 

3.    利用scanRight对神经网络的backward进行函数式重构

 

backward的逻辑比forward要稍微复杂一点点,我们先看看代码:

private def backward(feature: DenseMatrix[Double], label: DenseVector[Double],
                     forwardResList: List[ResultUtils.ForwardRes],
                     paramsList: List[(DenseMatrix[Double], DenseVector[Double])],
                     regularizer: Regularizer): List[BackwardRes] = {
  val yPredicted = forwardResList.last.yCurrent(::, 0)
  val numExamples = feature.rows

  val dYPredicted = -(label /:/ (yPredicted + pow(10.0, -9)) - (1.0 - label) /:/ (1.0 - yPredicted + pow(10.0, -9)))

  val dYHat = DenseMatrix.zeros[Double](numExamples, 1)
  dYHat(::, 0) := dYPredicted

  val initBackwardRes = BackwardRes(dYHat, null, null)

  paramsList
    .zip(this.allLayers)
    .zip(forwardResList)
    .scanRight[BackwardRes, List[BackwardRes]](initBackwardRes){
    (f, previousBackwardRes) =>
      val dYCurrent = previousBackwardRes.dYPrevious

      val (w, b) = f._1._1
      val layer = f._1._2
      val forwardRes = f._2

      val backwardRes = layer.backward(dYCurrent, forwardRes, w, b)

      layer match {
        case _: DropoutLayer => backwardRes
        case _ =>
          new BackwardRes(backwardRes.dYPrevious,
            backwardRes.dWCurrent + regularizer.getReguCostGrad(w, numExamples),
            backwardRes.dBCurrent)
      }
  }
    .dropRight(1)
}

 

现在暂时不用管regularizer.getReguCostGrad()的相关内容,下一章我们会讲将神经网络组件化,即将一个神经网络模型拆分成WeightInitializer, Regularizer, Layer, Optimizer等组件然后可以根据需要自由配置。

 

这里我们重点关注scanRight的使用,注意这里(f, previousBackwardRes)与上文(previousForwardRes,f)的顺序反了。最后我们仍旧要去掉初始值,但是scanRight的初始值在List的最右边,好在scala的List有一个方法dropRight(1),可以返回去掉最右边元素之后的List。

 

接下来,还有一个地方我们可以用scanLeft进行函数式重构,就是seyHiddenLayersStructure方法。下面我们来详细解释。

 

4.    利用scanLeft对setHiddenLayersStructure进行函数式重构

 

因为本次项目更新,我实现了DropoutLayer的神经层。把dropout这个操作抽象成一个layer的挑战在于,其神经元数目不需要也不应该由使用者事先给定,而应该与其前一个layer的神经元数目相同。在setHiddenLayersStructure中我就利用scanLeft判断当前layer是否为DropoutLayer,如果是,则将其神经元数目设置为前一个layer的神经元数目。具体代码如下所示:

 

//以下是一些设定神经网络超参数的setter
def setHiddenLayerStructure(hiddenLayers: List[Layer]): this.type = {
  if (hiddenLayers.isEmpty) {
    throw new IllegalArgumentException("hidden layer should be at least one layer!")
  }

  //重点需要解释的地方,如果某一层是Dropout Layer,则将其神经元数量设置成与前一层神经元数量相同
  //这里重点关注scanLeft的用法!
  val theHiddenLayer: List[Layer] = hiddenLayers
    .scanLeft[Layer, List[Layer]](EmptyLayer){
    (previousLayer, currentLayer) =>
    if (currentLayer.isInstanceOf[DropoutLayer])
      currentLayer.setNumHiddenUnits(previousLayer.numHiddenUnits)
    else currentLayer
  }

  this.hiddenLayers = theHiddenLayer.tail //tail的原因是把第一个EmptyLayer去掉
  this
}

 

代码很明了,无需更多解释。注意Scala中有个方法isInstanceOf[],功能与java中的instanceof完全一致。

 

以上就是利用scanLeft和scanRight方法对神经网络实现过程中的一些代码进行函数式重构的过程。熟练的使用scala collections的一些高级方法能够促使我们更好的写出无需var修饰符的不可变类型的代码,而这也是函数式编程的principle之一:尽可能地使用不可变变量。当我们的代码中都是不可变变量时,也就从根本上解决了并发所会遇到的问题,更有助于我们在大数据时代下快速开发。

 

在下一章我会介绍用Scala实现神经网络的组建化,也即将第二章实现的神经网络进行拆解,一个简单的神经网络大致包含:WeightInitializer, Regularizer, Layer, Optimizer等组件。组件化之后的神经网络模型可以更加灵活地配置组合,从接口设计的角度也更加明了易用。敬请期待!

你可能感兴趣的:(Scala学习者,深度学习学习者,唯品会开发实习生,同济大学硕士在读)