Deeplearning4j 實戰(4):Deep AutoEncoder進行Mnist壓縮的Spark實現
影象壓縮,在影象的檢索、影象傳輸等領域都有著廣泛的應用。事實上,影象的壓縮,我覺得也可以算是一種影象特徵的提取方法。如果從這個角度來看的話,那麼在理論上利用這些壓縮後的資料去做影象的分類,影象的檢索也是可以的。影象壓縮的演算法有很多種,這裡面只說基於神經網路結構進行的影象壓縮。但即使把範圍限定在神經網路這個領域,其實還是有很多網路結構進行選擇。比如:
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:
從圖中可以看出,aggregate是做引數更新時候進行的聚合操作,這個action在基於Spark的大規模機器學習演算法中也是很常用的。至於有takeSample的action,主要是之前所說的,在訓練的過程中會抽取一部分資料來看效果。下面的圖就是直觀的比較
訓練過程中,資料的直觀比對
這張圖是剛開始訓練的時候,歐式距離會比較大,當經過100~200輪的訓練後,歐式距離平均在1.0左右。也就是說,每個畫素點原始值和預測值的差值在0.035左右,應該說比較接近了。最後來看下視覺化介面展現的圖以及他們的距離計算
原始圖片和重構圖片對比以及他們之間的歐式距離
第一張圖左邊的原始圖,右邊是用訓練好的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提交的命令這裡沒有寫,不過在只之前的文章裡有提到,需要的同學可以參考。至於模型的儲存,都有相應的介面可以呼叫,這裡就不贅述了。。。