大神带你分分钟超越最好结果——基于分布式CPU计算的Deeplearning4j迁移学习应用实例
首发地址:https://yq.aliyun.com/articles/114669
2016“2017AI”2017GPUCPUGPUCPUCPUGPU“”GPUDeeplearning4jHadoop
——Apache SparkApache HadoopDeeplearning4jCommodity Hardware
Deeplearning4j
Deeplearning4j2014DL4JSparkGPUSkymindJavadeeplearning4jJVMNd4jCPUGPUJava
Caltech-256
本文介绍如何使用Apache Spark、Apache Hadoop和deeplearning4j来解决图像分类问题。简单来说,就是通过构建一个卷积神经网络来对Caltech-256Caltech-256数据集中,实际上有257个对象类别,每类数量大概是80到800个图像,该数据集总共30,607个图像。值得注意的是,72 - 75下面我将带领大家DL4J轻松超越这个结果。
目前,卷积网络可以有几亿个参数,比如在大型视觉识别挑战 “ImageNet”中表现最佳的神经网络之一,有1.4亿个参数需要训练!这些网络不仅需要大量的计算和存储资源(即使是使用一组GPU,也可能需要几周时间才能完成计算),而且还需要大量数据。而Caltech-25630000多张图像,在这个数据集上训练这样一个复杂的模型是不现实的,因为没有足够的样本来充分学习这么多参数。相反,可以采用一种的方法来实现。简单来说,就是将已学到的知识应用到其它领域,使其能够更好地完成新领域的学习。这是因为卷积神经网络在对图像数据集进行训练时往往会学习,因此这种类型的特征学习通常对其他图像数据集也是通用的。例如,在ImageNet上训练的网络可能已经学会了如何识别形状、面部特征、图案、文本等,这无疑对于Caltech-256数据集是有用的。
下面讲解如何使用训练好的模型来完成自己的任务,以下示例使用VGG16 模型,该模型夺得了2014 ImageNet竞赛中的亚军(网络结构及训练好的参数已公开)。由于使用了不同的图像数据集,所以需要对VGG16模型进行微小修改以适用于Caltech-256预测任务。该模型具有约1.4亿个参数,大约500 MB空间。
首先,获取DL4J可以理解和使用的VGG16型号的版本。事实证明,这种东西是建立在DL4J的API中的,它可以通过几行Scala代码完成。
val modelImportHelper = new TrainedModelHelper(TrainedModels.VGG16)
val vgg16 = modelImportHelper.loadModel()
val savePath = "./dl4j-models/vgg16.zip"
val locationToSave = new File(savePath)
// save the model in DL4J native format, which is faster for future reads
ModelSerializer.writeModel(vgg16, locationToSave, saveUpdater = true)
该模型采用的格式易于DL4J。
val modelFile = new File("./dl4j-models/vgg16.zip")
val vgg16 = ModelSerializer.restoreComputationGraph(modelFile)
println(vgg16.summary())
==================================================================================================
VertexName (VertexType) nIn,nOut TotalParams ParamsShape Vertex Inputs
==================================================================================================
input_2 (InputVertex) -,- - - -
block1_conv1 (ConvolutionLayer) 3,64 1792 b:{1,64}, W:{64,3,3,3} [input_2]
block1_conv2 (ConvolutionLayer) 64,64 36928 b:{1,64}, W:{64,64,3,3} [block1_conv1]
block1_pool (SubsamplingLayer) -,- 0 - [block1_conv2]
block2_conv1 (ConvolutionLayer) 64,128 73856 b:{1,128}, W:{128,64,3,3} [block1_pool]
block2_conv2 (ConvolutionLayer) 128,128 147584 b:{1,128}, W:{128,128,3,3} [block2_conv1]
block2_pool (SubsamplingLayer) -,- 0 - [block2_conv2]
block3_conv1 (ConvolutionLayer) 128,256 295168 b:{1,256}, W:{256,128,3,3} [block2_pool]
block3_conv2 (ConvolutionLayer) 256,256 590080 b:{1,256}, W:{256,256,3,3} [block3_conv1]
block3_conv3 (ConvolutionLayer) 256,256 590080 b:{1,256}, W:{256,256,3,3} [block3_conv2]
block3_pool (SubsamplingLayer) -,- 0 - [block3_conv3]
block4_conv1 (ConvolutionLayer) 256,512 1180160 b:{1,512}, W:{512,256,3,3} [block3_pool]
block4_conv2 (ConvolutionLayer) 512,512 2359808 b:{1,512}, W:{512,512,3,3} [block4_conv1]
block4_conv3 (ConvolutionLayer) 512,512 2359808 b:{1,512}, W:{512,512,3,3} [block4_conv2]
block4_pool (SubsamplingLayer) -,- 0 - [block4_conv3]
block5_conv1 (ConvolutionLayer) 512,512 2359808 b:{1,512}, W:{512,512,3,3} [block4_pool]
block5_conv2 (ConvolutionLayer) 512,512 2359808 b:{1,512}, W:{512,512,3,3} [block5_conv1]
block5_conv3 (ConvolutionLayer) 512,512 2359808 b:{1,512}, W:{512,512,3,3} [block5_conv2]
block5_pool (SubsamplingLayer) -,- 0 - [block5_conv3]
flatten (PreprocessorVertex) -,- - - [block5_pool]
fc1 (DenseLayer) 25088,4096 102764544 b:{1,4096}, W:{25088,4096} [flatten]
fc2 (DenseLayer) 4096,4096 16781312 b:{1,4096}, W:{4096,4096} [fc1]
predictions (DenseLayer) 4096,1000 4097000 b:{1,1000}, W:{4096,1000} [fc2]
--------------------------------------------------------------------------------------------------------------------------------------------
Total Parameters: 138357544
Trainable Parameters: 138357544
Frozen Parameters: 0
==================================================================================================
上面代码显示VGG16ConvolutionLayer
VGG16具有13个卷积层,中间间隔放置最大池化层以收缩图像,降低计算复杂度。卷积层中的权重实际上是过滤器,可以学习从图像中挑选出视觉特征,当使用最大池化层时,它们会“收缩”图像,这意味着后来的卷积层中的滤波器实际上提取更加抽象的特征。这样,卷积层的输出是输入图像的,如“这个图像中有脸吗?”还是“有日落?”卷积层的输出被馈送到连续的三个全连接层,全连接层能够学习这些视觉特征与输出之间的非线性关系。
另外卷积网络的关键性质之一是允许我们进行——可以通过已经训练好的VGG16网络传递新的图像数据,并获取每个图像的特征。一旦提取了这些特征,就只需要送人最后的预测网络就可以完成相应的任务,这在计算和复杂度上都是非常容易解决的问题。
VGG16
数据集可以从Caltech-256 网站下载,/验证/测试数据集,并存储在HDFS中。一旦完成该步骤,下一步就是将整个图像数据集传递到网络的所有卷积层和第一个全连接HDFS。
样做的原因是是因为卷积网络中的大多数内存占用和耗时计算都是发生在卷积层中,VGG16中的大多数参数(权重)调用发生在全连接层。迁移学习利用预先训练的卷积层来获取关于新输入图像的特征,这意味着只有原始模型的一小部分——层被重新训练。其余的参数是静态不变的。通过这种操作,迁移学习可以节省大量的训练时间和计算量。
首先提取用于特征化步骤的网络部分,Deeplearning4j具有内置的迁移学习API任务。即拆分VGG16模型,在拆分之前和之后获取整个图层列表,代码如下。
val modelFile = new File("./dl4j-models/vgg16.zip")
val vgg16 = ModelSerializer.restoreComputationGraph(modelFile)
val (frozenLayers: Array[Layer], unfrozenLayers: Array[Layer]) = {
vgg16.getLayers.splitAt(vgg16.getLayers.map(_.conf().getLayer.getLayerName).indexOf("fc2") + 1)
}
现在使用org.deeplearning4j.nn迁移学习包来提取全连接“fc2”包括“fc2”,如下图所示:垂线左边部分。
val builder = new TransferLearning.GraphBuilder(model)
.setFeatureExtractor(frozenLayers.last.conf().getLayer.getLayerName)
// remove all the unfrozen layers, leaving just the un-trainable part of the model
unfrozenLayers.foreach { layer =>
builder.removeVertexAndConnections(layer.conf().getLayer.getLayerName)
}
builder.setOutputs(frozenLayers.last.conf().getLayer.getLayerName)
val frozenGraph = builder.build()
接下来是读取数据库中的图像文件。在这种情况下,这些文件被单独保存到HDFS作为JPEG文件。图像被组织成子目录,其中每个子目录包含属于特定类的一组图像。首先通过使用sc.binaryFiles 加载存储在HDFS中的图像,并使用DataVec库(DL4J的ETL库)中的图像处理工具将它们转换为INDArrays,这是DL4J处理的本机张量表示(此处为完整代码)。最后,使用上图中的冻结网络部分对输入图像进行特征提取,本质上是将它们传递到VGG16前。
val finalOutput = Utils.getPredictions(data, frozenGraph, sc)
val df = finalOutput.map { ds =>
(Nd4j.toByteArray(ds.getFeatureMatrix), Nd4j.toByteArray(ds.getLabels))
}.toDF()
df.write.parquet("hdfs:///user/leon/featurizedPredictions/train")
经过上述操作后,得到一个HDFS数据集。接下来可以开始构建使用这种特征化数据的传输学习模型,从而大大减少训练时间和计算复杂度。在上述示例中,得到的新数据集由30607个长度为4096的向量组成(这是由于VGG16)。
VGG16
VGG16是在ImageNet数据集上进行训练的,而ImageNet数据集具有1000种不同对象类别。在典型的图像分类神经网络中,输出层的最后一层使用其输入来为数据集中的每个对象生成概率(哪一类的概率大就判断为哪一类)。因此,该输入可以被认为是关于图像的抽象视觉特征,提供关于其包含的对象的有用信息。直观地说,上述步骤生成的新数据集于Caltech-256数据集中识别对象应该是有用的。因此,定义一个新的模型“f2”,将维度从原先的1000,正好对应Caltech256数据集的257个类别。
val conf = new NeuralNetConfiguration.Builder()
.seed(42)
.optimizationAlgo(OptimizationAlgorithm.STOCHASTIC_GRADIENT_DESCENT)
.iterations(1)
.activation(Activation.SOFTMAX)
.weightInit(WeightInit.XAVIER)
.learningRate(0.01)
.updater(Updater.NESTEROVS)
.momentum(0.8)
.graphBuilder()
.addInputs("in")
.addLayer("layer0",
new OutputLayer.Builder(LossFunction.NEGATIVELOGLIKELIHOOD)
.activation(Activation.SOFTMAX)
.nIn(4096)
.nOut(257)
.build(),
"in")
.setOutputs("layer0")
.backprop(true)
.build()
val model = new ComputationGraph(conf)
直观图如下,可以看到只是改变了预测层的维度:
该模型现在已准备好使用DL4J进行大量计算,而且还使用Spark进行规模化。简单来说是切分大规模的数据集,然后将分片交给spark使用Spark RDD聚合操作对每个核心上学习的不同模型进行平均,实现分布式训练。
val tm = new ParameterAveragingTrainingMaster.Builder(1)
.averagingFrequency(5)
.workerPrefetchNumBatches(2)
.batchSizePerWorker(32)
.rddTrainingApproach(RDDTrainingApproach.Export)
.build()
val model = new SparkComputationGraph(sc, graph, tm)
现在针对具体的迭代次数训练SparkComputationGraph训练统计数据以跟踪进度。
model.setListeners(new ScoreIterationListener(1))
(1 to param.numEpochs).foreach { i =>
logger4j.info(s"epoch $i starting")
model.fit(trainRDD)
// print model accuracy and score on entire train and validation sets every 5 iterations
if (i % 5 == 0) {
logger4j.info(s"Train score: ${model.calculateScore(trainRDD, true)}")
logger4j.info(s"Train stats:\n${Utils.evaluate(model.getNetwork, trainRDD, 16)}")
if (validRDD.isDefined) {
logger4j.info(s"Validation stats:\n${Utils.evaluate(model.getNetwork, validRDD.get, 16)}")
logger4j.info(s"Validation score: ${model.calculateScore(validRDD.get, true)}")
}
}
}
最后,通过spark提交训练工作,然后使用DL4J webui监控进度并诊断问题。下图绘制的是模型得分与迭代次数的关系,注意到分数是minibatch的负对数似然率,分数越小,效果越好。
这次将学习率调低后,该模型似乎能比Imagenet更快地学习,因为这次使用的特征ImageNet概率更具预测性。
17/05/12 16:06:12 INFO caltech256.TrainFeaturized$: Train score: 0.6663876733861492
17/05/12 16:06:39 INFO caltech256.TrainFeaturized$: Train stats:
Accuracy: 0.8877570632327504
Precision: 0.8937314411403346
Recall: 0.876864905154427
17/05/12 16:07:17 INFO caltech256.TrainFeaturized$: Validation stats:
Accuracy: 0.7625918867410836
Precision: 0.7703367671469078
Recall: 0.7383574179140013
17/05/12 16:07:26 INFO caltech256.TrainFeaturized$: Validation score: 1.08481537405921
由于训练准确率为88.8%,但验证准确率仅为76.3%,从结果上看该模型似乎已经过拟合了。为了确保模型不会过拟合到验证集,在测试集上评估该模型。
Accuracy: 0.7530218882718066
Precision: 0.7613121478786196
Recall: 0.7286152891276695
虽然准确率有所降低,但是Hadoop商用CPU深度学习架构仍然打破了该数据集的最好结果!虽然这可能不是一个突破性的成就,但这仍然是一个令人兴奋的结果。
虽然deeplearning4j只是许多深度学习可用的工具之一,但它具有本机Apache Spark集成,并且采用Java编写,使其特别适合整个Hadoop生态系统。由于现有的企业数据已经通过Hadoop进行了大量访问,而且在Sparkdeeplearning4j的定位是花费更少的时间部署和减少开销,从而企业公司可以立即开始从深度学习中提取数据。它利用ND4J进行大量计算,这是一种高度优化的库,可与商用CPU配合使用,但在需要性能提升时也支持GPUDeeplearning4j提供了一个全功能的深度学习库,具有从采集到部署的工具,可
用于各种任务,如图像/视频识别,音频处理等。
Nisha MuktewarCloudera
Seth Hendrickson,以前是电气工程师,现在是数据科学家和软件工程师,研究方向是分布式机器学习。
Deep learning on Apache Spark and Apache Hadoop with Deeplearning4j | Cloudera Engineering BlogSeth Hendrickson
最后更新:2017-07-27 09:32:42