1. Introduction
2017年8月,前百度首席科学家吴恩达先生在twitter上宣布自己从百度离职后的第一个动作:在Coursera上推出一门从零开始构建神经网络的Deep Learning课程,一时间广为轰动。
截止到今天(2017年8月17日星期四),本人已经注册该门课程并且完成了两周的课程学习和作业。在前两周的课程中,吴恩达先生利用Logistic Regression来深入浅出的说明了神经网络的工作原理,并且用通俗易懂的语言介绍了反向传播的原理即为链式求导。在第二周的编码作业中,学生被要求利用Python Notebook从头实现Logistic Regression模型,并且利用此模型对所给定的图像集进行二元分类,判断某张图片是否是猫,最终训练好的模型的Test Accuracy能达到70%。吴恩达先生还说,在接下来的课程中我们会进一步学习神经网络的优化方法,以进一步提高猫狗分辨的Accuracy。
在编码测验中,吴恩达先生所强调的重点即为向量化运算,并且用实例说明了Python Numpy包的向量乘法比简单的for循环求和的速度快300多倍,这也意味着1分钟与5个小时的差距。然而,众所周知Python在实际的工程开发中更多是扮演者快速实验idea,快速得到结果的作用,一定程度上不适用于模型的正式开发及上线。本文中使用Scala实现吴恩达先生在Deep Learning课程中布置的所有作业,感谢您的阅读,期望共同进步。
在Python中实现深度学习算法以及向量化运算所依赖的包叫做Numpy,即Number Python。Numpy中提供了Vector与Matrix的实现,以及矩阵的各种运算和分解的函数。对应地,在Scala中我们使用Breeze包,其中也提供了DenseVector和DenseMatrix的数据结构,并且在数据量特别稀疏的情况下还有SparseVector和SparseMatrix可供使用,一定程度上比Numpy更加强大。最重要地,作为静态类型语言Scala是类型安全的,意味着我们不仅可以用Scala来实现算法,还可以用其进行数据预处理和数据清洗,即ETL。
本文分为四个部分。第一部分介绍整个项目结构;第二部分详细解释用Scala实现Logistic Regression的代码;第三部分给出其他功能性代码的解释,如数据预处理,画图工具,和一些其他的helper类;第四部分给出本文的demo结果和数据集的下载地址。另外,本项目的所有代码都可以在GitHub中找到,GitHub项目地址为https://github.com/pan5431333/coursera-deeplearning-practice-in-scala,跟随吴恩达先生的课程进度代码会及时保持更新,欢迎follow。
2. 项目结构
本文拟使用的项目结构分为五个subpackage,分别为data,demo,helper,model和utils。
data包中包含一个类Cat,其类型为Scala中的caseclass,特别适合用来表示真实世界中的一个entity。demo中即为每一节课后作业的运行实例;helper中现在包含两个类,CatDataHelper利用Java中的ImageIO从本地文件系统中读取图片,将其转化为RGB矩阵的表示形式,之后再reshape成向量形式。DlCollection为一个集合泛型类,其提供三个深度学习中常用的方法,分别为split,用来切分训练集和测试集;getFeatureAsMatrix返回算法所需要的特征矩阵;getLabelAsVector返回标签向量。Model包中现在仅包含Logistic Regression Model的实现。Utils包中现在有PlotUtils,其提供一个plotCostHistory方法,用来对cost随着迭代次数的变化情况画图。
下面介绍Logistic Regression算法在Scala中的具体实现。
3. Logistic Regression的Scala实战
首先,定义LogisticRegressionModel类:
classLogisticRegressionModel(){
var learningRate:Double= _
var iterationTime:Int = _
var w: DenseVector[Double] =_
var b:Double = _
val costHistory: mutable.TreeMap[Int, Double] =new mutable.TreeMap[Int,Double]()
此类包含五个InstanceVariables,其中前两个为超参数,learningRate表示学习率,iterationTime表示最大迭代次数;w和b即为模型参数,会随着迭代进行寻优;costHistory是一个用来保存迭代过程中cost变化情况的TreeMap,其key为迭代次数,value为cost值。
接下来是模型超参数的两个setter:
def setLearningRate(learningRate: Double): this.type = { this.learningRate = learningRate this } def setIterationTime(iterationTime: Int): this.type = { this.iterationTime = iterationTime this }
注意这里的setter与Java中的setter不一样,我们采用了链式编程的开发模式,即用户在调用时可以写成:val model = new LogisticRegressionModel().setLearningRate(0.0001).setIterationTime(3000),会使得整个编码过程更加流畅。链式编程也在Spark中被广泛使用,特别是构造数据管道(Pipeline)时会显得很优雅。
接下来是模型训练方法:
def train(feature: DenseMatrix[Double], label: DenseVector[Double]): this.type = { var (w, b) = initializeParams(feature.cols) (1 to this.iterationTime) .foreach{i => val (cost, dw, db) = propagate(feature, label, w, b) if (i % 100 == 0) println("INFO: Cost in " + i + "th time of iteration: " + cost) costHistory.put(i, cost) val adjustedLearningRate = this.learningRate / (log(i/1000 + 1) + 1) w :-= adjustedLearningRate * dw b -= adjustedLearningRate * db } this.w = w this.b = b this }
注意在此方法中我们用了两个私有方法,分别为initializeParams()和propagate(),我们会在下面对这两个方法详细解释。另外,我们对learningRate进行了简单的调整,使其随着迭代次数的增加逐渐减小,以尽量减少寻优时跳过最优解的可能性。
接下来是模型参数初始化的方法:
private def initializeParams(featureSize: Int): (DenseVector[Double], Double) = { val w = DenseVector.rand[Double](featureSize) val b = DenseVector.rand[Double](1).data(0) (w, b) }
这里我们对w和b赋予0到1之间的随机赋值。
接下来是LogisticRegression核心的正向传播与反向传播的实现方法:
private def propagate(feature: DenseMatrix[Double], label: DenseVector[Double], w: DenseVector[Double], b: Double): (Double, DenseVector[Double], Double) = { val numExamples = feature.rows val labelHat = sigmoid(fe