大神带你分分钟超越最好结果——基于分布式CPU计算的Deeplearning4j迁移学习应用实例...

    xiaoxiao2022-07-08  165

    首发地址:https://yq.aliyun.com/articles/114669

    更多深度文章,请关注:https://yq.aliyun.com/cloud

             2016年,欧莱礼媒体公司首席数据科学家罗瑞卡宣称:2017年将是数据科学和大数据圈参AI技术合作的一年。2017年之前,对基于GPU深度学习已经渗透到大学和研究机构,但基于CPU分散式深度学习开始在不同的公司和领域得到广泛采用。虽然GPU提供了顶级的数字计算性能,但CPU也在变得更加高效,并且现有的大部分硬件已经有大量可用的CPU计算能力。另外GPU的价格比CPU的价格要相对而言贵好多,相信大家最近一阵也发现显卡的价格暴涨,这源于数字货币比特币的暴涨,而比特币是通过电脑计算得到,计算能力越强,其每天的计算量也就越多,相当于每天挖矿的量。涉及深度学习的研究员都应该了解一个事实,基于GPU跑一个网络和基于CPU跑同一个网络,二者的仿真速度可以达到20倍左右的差距。因此,基于CPU的分散式深度学习也会成为后续研究的一个方向。开源工具Deeplearning4j的出现将快速深度学习扩展到Hadoop堆栈,未来几年影响深度学习的主要催化剂。文将详细介绍如何使用开源工具——Apache SparkApache HadoopDeeplearning4j(DL4J),再加上商用硬件(Commodity Hardware便宜、被广泛使用、容易被买到,能够使用有限的训练集在图像识别任务上获得最先进的结果Deeplearning4j:JVM的深度学习工具集

         Deeplearning4j是许多开源深度学习工具包之一,创建于2014年。DL4J集成了HadoopSpark,设计用于运行在分布式GPU和CPU上的商用环境。它由总部位于旧金山的商用智能和企业软件公司Skymind牵头开发。团队成员包括数据专家、深度学习专家、Java系统工程师和具有一定感知力的机器人。虽然deeplearning4j是为JVM构建的,但它使用高性能原生线性代数库Nd4j,可以对CPUGPU进行大量优化的计算。另外使用Java编写的DL4J API对于熟悉Java虚拟机(JVM)的Java和Scala开发人员特别有吸引力。此外,Spark模型并行训练能力使得我们轻松利用现有的群集资源加快训练时间,而不会牺牲精度。基于Caltech-256图像数据集的对象分类

         本文介绍如何使用Apache SparkApache Hadoopdeeplearning4j来解决图像分类问题。简单来说,就是通过构建一个卷积神经网络Caltech-256数据集中的图像进行分类。在Caltech-256数据集中,实际上有257个对象类别,每类数量大概是80800个图像,该数据集总共30,607个图像。值得注意的是,该数据集上目前最先进的分类精度在72 - 75%范围内。下面我将带领大家使用DL4J和Spark轻松超越结果 

    小数据上的有效深度学习     目前,卷积网络可以有几亿个参数,比如大型视觉识别挑战 ImageNet中表现最佳的神经网络之一,有1.4亿个参数需要训练!这些网络不仅需要大量的计算和存储资源(即使使用一组GPU,也可能需要几周时间才能完成计算),而且还需要大量数据。Caltech-256只有30000张图像,在这个数据集上训练这样一个复杂的模型是不现实的,因为没有足够的样本来充分学习这么多参数。相反,可以采用一种迁移学习的方法来实现。简单来说,就是将已学到的知识应用到其它领域,使其能够更好地完成新领域的学习这是因为卷积神经网络在对图像数据集进行训练时往往会学习非常普遍的特征,因此这种类型的特征学习通常对其他图像数据集也是通用的。例如,在ImageNet上训练的网络可能已经学会了如何识别形状面部特征图案文本等,这无疑对于Caltech-256数据集是有用的。加载预训练的模型     下面讲解如何使用训练好的模型来完成自己的任务,以下示例使用VGG16 模型,该模型夺得了2014 ImageNet竞赛中的亚军(网络结构及训练好的参数已公开)。由使用不同的图像数据集,所以需要对VGG16模型进行微小修改以适用于Caltech-256数据集预测任务。该模型具有约1.4亿个参数,大约占用500 MB空间。 

    首先,获取DL4J可以理解和使用的VGG16型号的版本。事实证明,这种东西是建立在DL4JAPI中的,它可以通过几行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 ==================================================================================================

    上面代码显示VGG16网络的结构及参数,ConvolutionLayer表示卷积层、SubsamplingLayer表示采样层、DenseLayer表示全连接层。下图简明扼要的展示了该网络结构:

               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库(DL4JETL库)中的图像处理工具将它们转换为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模型中的全连接层“f2”维度为4096 

    替换VGG16的预测层         VGG16模型是在ImageNet数据集上进行训练ImageNet数据集具有1000种不同对象类别。在典型的图像分类神经网络中,输出层的最后一层使用其输入来为数据集中的每个对象生成概率(哪一类的概率大就判断为哪一类)。因此,该输入可以被认为是关于图像的抽象视觉特征,提供关于其包含的对象的有用信息。直观地说,上述步骤生成的新数据集于Caltech-256数据集中识别对象应该是有用的。因此,定义一个新的模型,“f2”层前的模型不变,只是替换VGG16模型的最后一层预测层将维度从原先的1000变成257正好对应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群集中的每个工作核心上运行SGD,最后使用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进行了大量访问,而且在Spark上进行处理,所以deeplearning4j的定位花费更少的时间部署和减少开销,从而企业公司可以立即开始从深度学习中提取数据。它利用ND4J进行大量计算,这是一种高度优化的库,可与商用CPU配合使用,但在需要性能提升时也支持GPU。Deeplearning4j提供了一个全功能的深度学习库,具有从采集到部署的工具,可用于各种任务,如图像/视频识别,音频处理等。 

    作者信息

    Nisha Muktewar数据科学家,目前就职于Cloudera的数据科学团队,专注于专业服务、售前工作。

    Seth Hendrickson以前是电气工程师,现在是数据科学家和软件工程师,研究方向是分布式机器学习。

    本文由北邮@爱可可-爱生活老师推荐,阿里云云栖社区组织翻译。

    文章原标题《Deep learning on Apache Spark and Apache Hadoop with Deeplearning4j | Cloudera Engineering Blog》,作者:Nisha MuktewarSeth Hendrickson,译者:海棠,审阅:

    文章为简译,更为详细的内容,请查看原文

    翻译者: 海棠 

    Wechat:269970760  weibo:Uncle_LLD 

    Email:duanzhch@tju.edu.cn

    微信公众号:AI科技时讯

    相关资源:敏捷开发V1.0.pptx
    最新回复(0)