天天看點

Deeplearning4j 實戰(4):Deep AutoEncoder進行Mnist壓縮的Spark實作

Eclipse Deeplearning4j GiChat課程:https://gitbook.cn/gitchat/column/5bfb6741ae0e5f436e35cd9f

Eclipse Deeplearning4j 系列部落格:https://blog.csdn.net/wangongxi

Eclipse Deeplearning4j Github:https://github.com/eclipse/deeplearning4j

圖像壓縮,在圖像的檢索、圖像傳輸等領域都有着廣泛的應用。事實上,圖像的壓縮,我覺得也可以算是一種圖像特征的提取方法。如果從這個角度來看的話,那麼在理論上利用這些壓縮後的資料去做圖像的分類,圖像的檢索也是可以的。圖像壓縮的算法有很多種,這裡面隻說基于神經網絡結構進行的圖像壓縮。但即使把範圍限定在神經網絡這個領域,其實還是有很多網絡結構進行選擇。比如:

1.傳統的DNN,也就是加深全連接配接結構網絡的隐層的數量,以還原原始圖像為輸出,以均方誤差作為整個網絡的優化方向。

2.DBN,基于RBM的網絡棧,構成的深度置信網絡,每一層RBM對資料進行壓縮,以KL散度為損失函數,最後以MSE進行優化

3.VAE,變分自編碼器,也是非常流行的一種網絡結構。後續也會寫一些自己測試的效果。

這裡主要講第二種,也就是基于深度置信網絡對圖像進行壓縮。這種模型是一種多層RBM的結構,可以參考的論文就是G.Hinton教授的paper:《Reducing the Dimensionality of Data with Neural Network》。這裡簡單說下RBM原理。RBM,中文叫做受限玻爾茲曼機。所謂的受限,指的是同一層的節點之間不存在邊将其相連。RBM自身分成Visible和Hidden兩層。它利用輸入資料本身,首先進行資料的壓縮或擴充,然後再以壓縮或擴充的資料為輸入,以重構原始輸入為目标進行反向權重的更新。是以是一種無監督的結構。如果我沒記錯,這種結構本身也是Hinton提出來的。将RBM進行多層的堆疊,就形成深度置信網絡,用于編碼或壓縮的時候,被成為Deep Autoencoder。

下面就具體來說說基于開源庫Deeplearning4j的Deep Autoencoder的實作,以及在Spark上進行訓練的過程和結果。

1.建立Maven工程,加入Deeplearning4j的相關jar包依賴,具體如下

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <nd4j.version>0.7.1</nd4j.version>
  	<dl4j.version>0.7.1</dl4j.version>
  	<datavec.version>0.7.1</datavec.version>
  	<scala.binary.version>2.10</scala.binary.version>
  </properties>
  
 <dependencies>
	   <dependency>
	     <groupId>org.nd4j</groupId>
	     <artifactId>nd4j-native</artifactId> 
	     <version>${nd4j.version}</version>
	   </dependency>
	   <dependency>
	    	<groupId>org.deeplearning4j</groupId>
	   		<artifactId>dl4j-spark_2.11</artifactId>
	    	<version>${dl4j.version}</version>
		</dependency>
   		<dependency>
            <groupId>org.datavec</groupId>
            <artifactId>datavec-spark_${scala.binary.version}</artifactId>
            <version>${datavec.version}</version>
     	</dependency>
		<dependency>
	        <groupId>org.deeplearning4j</groupId>
	        <artifactId>deeplearning4j-core</artifactId>
	        <version>${dl4j.version}</version>
	     </dependency>
	     <dependency>
	    	<groupId>org.nd4j</groupId>
	    	<artifactId>nd4j-kryo_${scala.binary.version}</artifactId>
	    	<version>${nd4j.version}</version>
		</dependency>
</dependencies>
           

2.啟動Spark任務,傳入必要的參數,從HDFS上讀取Mnist資料集(事先已經将資料以DataSet的形式儲存在HDFS上,至于如何将Mnist資料集以DataSet的形式存儲在HDFS上,之前的部落格有說明,這裡就直接使用了)

if( args.length != 6 ){
            System.err.println("Input Format:<inputPath> <numEpoch> <modelSavePah> <lr> <numIter> <numBatch>");
            return;
        }
        SparkConf conf = new SparkConf()
                        .set("spark.kryo.registrator", "org.nd4j.Nd4jRegistrator")
                        .setAppName("Deep AutoEncoder (Java)");
        JavaSparkContext jsc = new JavaSparkContext(conf);
        final String inputPath = args[0];
        final int numRows = 28;
        final int numColumns = 28;
        int seed = 123;
        int batchSize = Integer.parseInt(args[5]);
        int iterations = Integer.parseInt(args[4]);
        final double lr = Double.parseDouble(args[3]);
        //
        JavaRDD<DataSet> javaRDDMnist = jsc.objectFile(inputPath);
        JavaRDD<DataSet> javaRDDTrain = javaRDDMnist.map(new Function<DataSet, DataSet>() {

            @Override
            public DataSet call(DataSet next) throws Exception {
                return new DataSet(next.getFeatureMatrix(),next.getFeatureMatrix());
            }
        });
           

由于事先我們已經将Mnist資料集以DataSet的形式序列化儲存在HDFS上,是以我們一開始就直接反序列化讀取這些資料并儲存在RDD中就可以了。接下來,我們建構訓練資料集,由于Deep Autoencoder中,是以重構輸入圖檔為目的的,是以feature和label其實都是原始圖檔。此外,程式一開始的時候,就已經将學習率、疊代次數等等傳進來了。

3.設計Deep Autoencoder的網絡結構,具體代碼如下:

MultiLayerConfiguration netconf = new NeuralNetConfiguration.Builder()
                .seed(seed)
                .iterations(iterations)
                .learningRate(lr)
                .learningRateScoreBasedDecayRate(0.5)
                .optimizationAlgo(OptimizationAlgorithm.LINE_GRADIENT_DESCENT)
                .updater(Updater.ADAM).adamMeanDecay(0.9).adamVarDecay(0.999)
                .list()
                .layer(0, new RBM.Builder()
                              .nIn(numRows * numColumns)
                              .nOut(1000)
                              .lossFunction(LossFunctions.LossFunction.KL_DIVERGENCE)
                              .visibleUnit(VisibleUnit.IDENTITY)
                              .hiddenUnit(HiddenUnit.IDENTITY)
                              .activation("relu")
                              .build())
                .layer(1, new RBM.Builder()
                              .nIn(1000)
                              .nOut(500)
                              .lossFunction(LossFunctions.LossFunction.KL_DIVERGENCE)
                              .visibleUnit(VisibleUnit.IDENTITY)
                              .hiddenUnit(HiddenUnit.IDENTITY)
                              .activation("relu")
                              .build())
                .layer(2, new RBM.Builder()
                              .nIn(500)
                              .nOut(250)
                              .lossFunction(LossFunctions.LossFunction.KL_DIVERGENCE)
                              .visibleUnit(VisibleUnit.IDENTITY)
                              .hiddenUnit(HiddenUnit.IDENTITY)
                              .activation("relu")
                              .build())
                //.layer(3, new RBM.Builder().nIn(250).nOut(100).lossFunction(LossFunctions.LossFunction.KL_DIVERGENCE).build())
                //.layer(4, new RBM.Builder().nIn(100).nOut(30).lossFunction(LossFunctions.LossFunction.KL_DIVERGENCE).build()) //encoding stops
                //.layer(5, new RBM.Builder().nIn(30).nOut(100).lossFunction(LossFunctions.LossFunction.KL_DIVERGENCE).build()) //decoding starts
                //.layer(6, new RBM.Builder().nIn(100).nOut(250).lossFunction(LossFunctions.LossFunction.KL_DIVERGENCE).build())
                .layer(3, new RBM.Builder()
                              .nIn(250)
                              .nOut(500)
                              .lossFunction(LossFunctions.LossFunction.KL_DIVERGENCE)
                              .visibleUnit(VisibleUnit.IDENTITY)
                              .hiddenUnit(HiddenUnit.IDENTITY)
                              .activation("relu")
                              .build())
                .layer(4, new RBM.Builder()
                              .nIn(500)
                              .nOut(1000)
                              .visibleUnit(VisibleUnit.IDENTITY)
                              .hiddenUnit(HiddenUnit.IDENTITY)
                              .activation("relu")
                              .lossFunction(LossFunctions.LossFunction.KL_DIVERGENCE).build())
                .layer(5, new OutputLayer.Builder(LossFunctions.LossFunction.MSE).activation("relu").nIn(1000).nOut(numRows*numColumns).build())
                .pretrain(true).backprop(true)
                .build();
           

這裡需要說明下幾點。第一,和Hinton老先生論文裡的結構不太一樣的是,我并沒有把圖像壓縮到30維這麼小。但是這肯定是可以進行嘗試的。第二,Visible和Hidden的轉換函數用的是Identity,而不是和論文中的Gussian和Binary。第三,學習率是可變的。在Spark叢集上訓練,初始的學習率可以設定得大一些,比如0.1,然後,在代碼中有個機制,就是當損失函數不再下降或者下降不再明白的時候,減半學習率,也就是減小步長,試圖使模型收斂得更好。第四,更新機制用的是ADAM。當然,以上這些基本都是超參數的範疇,大家可以有自己的了解和調優過程。

4.訓練網絡并在訓練過程中進行效果的檢視

ParameterAveragingTrainingMaster trainMaster = new ParameterAveragingTrainingMaster.Builder(batchSize)
                                                            .workerPrefetchNumBatches(0)
                                                            .saveUpdater(true)
                                                            .averagingFrequency(5)
                                                            .batchSizePerWorker(batchSize)
                                                            .build();
        MultiLayerNetwork net = new MultiLayerNetwork(netconf);
        //net.setListeners(new ScoreIterationListener(1));
        net.init();
        SparkDl4jMultiLayer sparkNetwork = new SparkDl4jMultiLayer(jsc, net, trainMaster);
        sparkNetwork.setListeners(Collections.<IterationListener>singletonList(new ScoreIterationListener(1)));
        int numEpoch = Integer.parseInt(args[1]);
        for( int i = 0; i < numEpoch; ++i ){
            sparkNetwork.fit(javaRDDTrain);
            System.out.println("----- Epoch " + i + " complete -----");
            MultiLayerNetwork trainnet = sparkNetwork.getNetwork();
            System.out.println("Epoch " + i + " Score: " + sparkNetwork.getScore());
            List<DataSet> listDS = javaRDDTrain.takeSample(false, 50);
            for( DataSet ds : listDS ){
                INDArray testFeature = ds.getFeatureMatrix();
                INDArray testRes = trainnet.output(testFeature);
                System.out.println("Euclidean Distance: " + testRes.distance2(testFeature));
            }
            DataSet first = listDS.get(0);
            INDArray testFeature = first.getFeatureMatrix();
            double[] doubleFeature = testFeature.data().asDouble();
            INDArray testRes = trainnet.output(testFeature);
            double[] doubleRes = testRes.data().asDouble();
            for( int j = 0; j < doubleFeature.length && j < doubleRes.length; ++j ){
                double f = doubleFeature[j];
                double t = doubleRes[j];
                System.out.print(f + ":" + t + "  ");
            }
            System.out.println();
            
        }
           

這裡的邏輯其實都比較的明白。首先,申請一個參數服務對象,這個主要是用來負責對各個節點上計算的梯度進行聚合和更新,也是一種機器學習在叢集上實作優化的政策。下面則是對資料集進行多輪訓練,并且在每一輪訓練完以後,我們随機抽樣一些資料,計算他們預測的值和原始值的歐式距離。然後抽取其中一張圖檔,輸出每個像素點,原始的值和預測的值。以此,在訓練過程中,直覺地評估訓練的效果。當然,每一輪訓練後,損失函數的得分也要列印出來看下,如果一直保持震蕩下降,那麼就是可以的。

5.Spark集訓訓練的過程和結果展示

Spark訓練過程中,stage的web ui:

Deeplearning4j 實戰(4):Deep AutoEncoder進行Mnist壓縮的Spark實作

從圖中可以看出,aggregate是做參數更新時候進行的聚合操作,這個action在基于Spark的大規模機器學習算法中也是很常用的。至于有takeSample的action,主要是之前所說的,在訓練的過程中會抽取一部分資料來看效果。下面的圖就是直覺的比較

訓練過程中,資料的直覺比對

Deeplearning4j 實戰(4):Deep AutoEncoder進行Mnist壓縮的Spark實作

這張圖是剛開始訓練的時候,歐式距離會比較大,當經過100~200輪的訓練後,歐式距離平均在1.0左右。也就是說,每個像素點原始值和預測值的內插補點在0.035左右,應該說比較接近了。最後來看下可視化界面展現的圖以及他們的距離計算

原始圖檔和重構圖檔對比以及他們之間的歐式距離

Deeplearning4j 實戰(4):Deep AutoEncoder進行Mnist壓縮的Spark實作
Deeplearning4j 實戰(4):Deep AutoEncoder進行Mnist壓縮的Spark實作

第一張圖左邊的原始圖,右邊是用訓練好的Deep Autoencoder預測的或者說重構的圖:圖有點小,不過仔細看,發現基本還是很像的,若幹像素點上明暗不太一樣。不過總體還算不錯。下面的圖,是兩者歐式距離的計算,內插補點在1.4左右。

最後做一些回顧:

用堆疊RBM構成DBN做圖像壓縮,在理論上比單純增加全連階層的效果應該會好些,畢竟每一層RBM本身可以利用自身可以重構輸入資料的特點進行更為有效的壓縮。從實際的效果來看,應該也是還算看得過去。其實圖像壓縮本身如果足夠高效,那麼對圖像檢索的幫助也是很大。是以Hinton老先生的一篇論文就是利用Deep AutoEncoder對圖像進行壓縮後再進行檢索,論文中把這個效果和用歐式距離還有PCA提取的圖檔特征進行了比較,論文中的結果是用Deep AutoEncoder的進行壓縮後在做檢索的效果最佳。不過,這裡還是得說明,在論文中RBM的Hidden的轉換函數是binary,因為作者希望壓縮出來的結果是0,1二進制的。這樣,檢索圖檔的時候,計算Hamming距離就可以了。而且這樣即使以後圖檔的數量急劇增加,檢索的時間不會顯著增加,因為計算Hamming距離可以說計算機是非常快的,底層電路做異或運算就可以了。但是,我自己覺得,雖然壓縮成二進制是個好方法,檢索時間也很短。但是二進制的表現力是否有所欠缺呢?畢竟非0即1,和用浮點數表示的差别,表現力上面應該是差蠻多的。是以,具體是否可以在圖像檢索系統依賴這樣的方式,還有待進一步實驗。另外就是,上面在建構多層RBM的時候,其實有很多超參數可以調整,包括可以增加RBM的層數,來做進一步的壓縮等等,就等有時間再慢慢研究了。還有,Spark送出的指令這裡沒有寫,不過在隻之前的文章裡有提到,需要的同學可以參考。至于模型的儲存,都有相應的接口可以調用,這裡就不贅述了。。。

繼續閱讀