上一章我们讲了如何使用Scala实现LogisticRegression,这一张跟随着吴恩达的脚步我们用Scala实现基础的深度神经网络。顺便再提一下,吴恩达对于深度神经网络的解释是我如今听过的最清楚的课,感叹一句果然越是大牛知识解释得越清晰明了。
本文分为以下四个部分。按照软件开发top-down的思路,第一部分我先展示一下使用构建好的神经网络对Gas Censor数据进行分类的demo,这一部分重点关注接口的顶层设计,如何达到易用、简洁、逻辑清晰以及新手友好,这是软件工程一个比较难的领域,所以我的接口设计不一定是最优的,欢迎在评论区提出自己的想法,我们共用探讨~
第二个部分给出了NeuralNetworkModel类的具体实现。从接口的角度来看,NeuralNetworkModel实现了名为Model的trait(类似于Java中的interface),所有的model都拥有一些common protocol,在本项目中所有的model都拥有setLearningRate,setIterationTime,train,predict,accuracy,getCostHistory方法,此外NeuralNetworkModel也拥有自己独有的setHiddenLayerStructure,setOutputLayerStructure方法。
第三部分我们介绍了各种神经网络Layer的实现,包含ReluLayer,SigmoidLayer,TanhLayer这三个类。所有的layer都实现了名叫Layer的trait,提供etNumHiddenUnits,forward和backward三个方法。
最后一部分我们介绍了各种Utils类,代码以及功能注释都附在文章最后,感兴趣的可以自己看一下。我的GitHub地址为:https://github.com/pan5431333/coursera-deeplearning-practice-in-scala-remote,欢迎clone代码,指正问题,共同进步!
第一部分:demo介绍
首先我们先看看使用神经网络模型的使用demo。
package org.mengpan.deeplearning.demo
import breeze.stats.{mean, stddev}
import org.mengpan.deeplearning.data.{Cat, GasCensor}
import org.mengpan.deeplearning.helper.{CatDataHelper, DlCollection, GasCensorDataHelper}
import org.mengpan.deeplearning.model.{Model, NeuralNetworkModel, ShallowNeuralNetworkModel}
import org.mengpan.deeplearning.utils.{MyDict, NormalizeUtils, PlotUtils}
/**
* Created by mengpan on 2017/8/15.
*/
object ClassThreeNeuralNetworkDemo extends App{
// Dataset Download Website:http://archive.ics.uci.edu/ml/machine-learning-databases/00224/
//加载Gas Censor的数据集
val data: DlCollection[GasCensor] = GasCensorDataHelper.getAllData
//归一化数据特征矩阵
val normalizedCatData = NormalizeUtils.normalizeBy(data){col =>
(col - mean(col)) / stddev(col)
}
//获取training set和test set
val (training, test) = normalizedCatData.split(0.8)
//分别获取训练集和测试集的feature和label
val trainingFeature = training.getFeatureAsMatrix
val trainingLabel = training.getLabelAsVector
val testFeature = test.getFeatureAsMatrix
val testLabel = test.getLabelAsVector
//初始化算法模型
val nnModel: Model = new NeuralNetworkModel()
.setHiddenLayerStructure(Map(
(200, MyDict.ACTIVATION_RELU),
(100, MyDict.ACTIVATION_RELU)
))
.setOutputLayerStructure((1, MyDict.ACTIVATION_SIGMOID))
.setLearningRate(0.01)
.setIterationTime(5000)
//用训练集的数据训练算法
val trainedModel: Model = nnModel.train(trainingFeature, trainingLabel)
//测试算法获得算法优劣指标
val yPredicted = trainedModel.predict(testFeature)
val trainYPredicted = trainedModel.predict(trainingFeature)
val testAccuracy = trainedModel.accuracy(testLabel, yPredicted)
val trainAccuracy = trainedModel.accuracy(trainingLabel, trainYPredicted)
println("\n The trainaccuracy of this model is: " + trainAccuracy)
println("\n The testaccuracy of this model is: " + testAccuracy)
//对算法的训练过程中cost与迭代次数变化关系进行画图
val costHistory = trainedModel.getCostHistory
PlotUtils.plotCostHistory(costHistory)
}
对于神经网络的模型接口,我们采用了Scala中典型的链式编程法:
//初始化算法模型 val nnModel: Model = new NeuralNetworkModel() .setHiddenLayerStructure(Map( (200, MyDict.ACTIVATION_RELU), (100, MyDict.ACTIVATION_RELU) )) .setOutputLayerStructure((1, MyDict.ACTIVATION_SIGMOID)) .setLearningRate(0.01) .setIterationTime(5000)
setHiddenLayerStructure方法用于设置神经网络隐含层的结构,接受多个二元元祖为参数,二元元祖的第一个参数为该层隐含层层的神经单元个数;第二个参数为该层隐含层层的激活函数类型。如上图中构建的神经网络拥有两个隐含层,第一个隐含层拥有200个神经元,激活函数类型为ReLU,第二个隐含层拥有100个神经元,激活函数类型也为ReLU。setOutputLayerStructure方法接受一个二元元祖为参数,元祖的含义与前面相同。
第二部分:NeuralNetworkModel类的具体实现
首先我把此类中的重要的方法解释一下,完整的代码附在本部分最后面。首先我们来看一下train()方法:
override def train(feature: DenseMatrix[Double], label: DenseVector[Double]): NeuralNetworkModel.this.type = { val numExamples = feature.rows val inputDim = feature.cols logger.debug("hidden layers: " + hiddenLayers) logger.debug("output layer: " + outputLayer) //随机初始化模型参数 var paramsList: List[(DenseMatrix[Double], DenseVector[Double])] = initializeParams(numExamples, inputDim, hiddenLayers, outputLayer) (0 until this.iterationTime).foreach{i => val forwardResList: List[ForwardRes] = forward(feature, paramsList, hiddenLayers, outputLayer) logger.debug(forwardResList) val cost = calCost(forwardResList.last, label) if (i % 100 == 0) { logger.info("Cost in " + i + "th time of iteration: " + cost) } costHistory.put(i, cost) val backwardResList: List[BackwardRes] = backward(feature, label, forwardResList, paramsList, hiddenLayers, outputLayer) logger.debug(backwardResList) paramsList = updateParams(paramsList, this.learningRate, backwardResList, i, cost) } this.paramsList = paramsList this }
可以看到,在神经网络模型的train()方法中,有五个主要的私有功能函数:首先用initializeParams(numExamples, inputDim, hiddenLayers, outputLayer)初始化参数;然后在每一次迭代过程中:
l 先使用forward()计算前向传播的结果;
l 在使用calCost()计算此次的损失函数值;
l 然后使用backward()计算反向传播的结果;
l 最后使用updateParams()更新参数;
接下来看看随机初始化参数的方法init