1. 程式人生 > >Deeplearning4j 實戰(2):Deeplearning4j 手寫體數字識別Spark實現【轉】

Deeplearning4j 實戰(2):Deeplearning4j 手寫體數字識別Spark實現【轉】

from:http://blog.csdn.net/wangongxi/article/details/54616842

在前兩天的部落格中,我們用Deeplearning4j做了Mnist資料集的分類。算是第一個深度學習的應用。像Mnist資料集這樣圖片尺寸不大,而且是黑白的開源圖片集在本地完成訓練是可以的,畢竟我們用了Lenet這樣相對簡單的網路結構,而且本地的機器配置也有8G左右的記憶體。但實際生產中,圖片的數量要多得多,尺寸也大得多,用的網路也會是AlexNet、GoogLenet這樣更多層數的網路,所以往往我們需要用叢集來解決計算資源的問題。由於Deeplearning4j本身基於Spark

實現了神經網路的分散式訓練,所以我們就以此作為我們的解決方案。

我們還是以Mnist資料集為例來做Deeplearning4j的第一個Spark版本的應用。首先需要在上一篇部落格的基礎上,在pom裡面加入新的依賴:

  1. <dependency>
  2. <groupId>org.nd4j</groupId>
  3. <artifactId>nd4j-kryo_${scala.binary.version}</artifactId>
  4. <version>${nd4j.version}</version>
  5. </
    dependency>
這個是為了將Nd4j的序列化形式從Java預設的形式轉到kryo的格式,以此提高序列化的效率。如果在程式碼中不為該類註冊kryo的序列化格式,那麼訓練的時候會拋異常。
接著程式碼分為2個部分,一個部分是將Mnist資料集在本地以JavaRDD<DataSet>的形式存到磁碟並最終推到HDFS上作為Spark job的輸入資料來源。另一個部分則是模型的訓練和儲存。

第一部分的邏輯大致如下:本地建立Spark任務-->獲取所有Mnist圖片的路徑-->讀取圖片並提取特徵,打上標註,以DataSet的形式作為一張圖片的wrapper-->將所有圖片構成的JavaRDD<DataSet>儲存下來。

這裡原始的Mnist資料集是以圖片形式存在,不再是二進位制格式的資料。這個例子這樣處理,也是方便日後用同樣的方式讀取一般的圖片。Mnist的圖片如下:


  1. SparkConf conf = new SparkConf()  
  2.                 .setMaster("local[*]")  //local mode
  3.                 .set("spark.kryo.registrator""org.nd4j.Nd4jRegistrator")  
  4.                 .setAppName("Mnist Java Spark (Java)");  
  5. JavaSparkContext jsc = new JavaSparkContext(conf);  
  6. final List<String> lstLabelNames = Arrays.asList("零","一","二","三","四","五","六","七","八","九");  //Chinese Label
  7. final ImageLoader imageLoader = new ImageLoader(28281);             //Load Image
  8. final DataNormalization scaler = new ImagePreProcessingScaler(01);    //Normalize
  9. String srcPath = args[0];  
  10. FileSystem hdfs = FileSystem.get(URI.create(srcPath),jsc.hadoopConfiguration());    //hdfs read local file system
  11. FileStatus[] fileList = hdfs.listStatus(new Path(srcPath));  
  12. List<String> lstFilePath = new ArrayList<>();  
  13. for( FileStatus fileStatus :  fileList){  
  14.     lstFilePath.add(srcPath + "/" + fileStatus.getPath().getName());  
  15. }  
  16. JavaRDD<String> javaRDDImagePath = jsc.parallelize(lstFilePath);  
  17. JavaRDD<DataSet> javaRDDImageTrain = javaRDDImagePath.map(new Function<String, DataSet>() {  
  18.     @Override
  19.     public DataSet call(String imagePath) throws Exception {  
  20.         FileSystem fs = FileSystem.get(new Configuration());  
  21.         DataInputStream in = fs.open(new Path(imagePath));  
  22.         INDArray features = imageLoader.asRowVector(in);            //features tensor
  23.         String[] tokens = imagePath.split("\\/");  
  24.         String label = tokens[tokens.length-1].split("\\.")[0];       
  25.         int intLabel = Integer.parseInt(label);  
  26.         INDArray labels = Nd4j.zeros(10);                           //labels tensor                     
  27.         labels.putScalar(0, intLabel, 1.0);  
  28.         DataSet trainData = new DataSet(features, labels);          //DataSet, wrapper of features and labels
  29.         trainData.setLabelNames(lstLabelNames);  
  30.         scaler.preProcess(trainData);                               //normalize
  31.         fs.close();  
  32.         return trainData;  
  33.     }  
  34. });  
  35. javaRDDImageTrain.saveAsObjectFile("mnistNorm.dat");        //save training data
這裡有幾點需要解釋。
1.用hdfs.filesystem來獲取檔案。用Java原生態的File來操作也是完全可以的。只不過,這樣讀取檔案的方式,同時適用於讀取本地和HDFS上的檔案。

2.ImageLoader類。這個類是用來讀取圖片檔案的。類似的還有一個類,叫NativeImageLoader。不同的在於,NativeImageLoader是呼叫了OpenCV的相關方法來對圖片做處理的,效率更高,因此推薦使用NativeImageLoader

儲存的RDD的形式如下圖:



然後,講下模型訓練任務的邏輯。讀取HDFS上的以DataSet形式儲存的Mnist檔案-->定義引數中心服務-->定義神經網路結構(Lenet)--> 訓練網路-->儲存訓練好的模型。首先看前兩步的操作:
  1. SparkConf conf = new SparkConf()  
  2.                       .set("spark.kryo.registrator""org.nd4j.Nd4jRegistrator")  //register kryo for nd4j
  3.                       .setAppName("Mnist Java Spark (Java)");  
  4.   final String imageFilePath = args[0];  
  5.   finalint numEpochs = Integer.parseInt(args[1]);  
  6.   final String modelPath = args[2];  
  7.   finalint numBatch = Integer.parseInt(args[3]);  
  8.   //
  9.   JavaSparkContext jsc = new JavaSparkContext(conf);  
  10.   //
  11.   JavaRDD<DataSet> javaRDDImageTrain = jsc.objectFile(imageFilePath);     //load image data from hdfs
  12.   ParameterAveragingTrainingMaster trainMaster = new ParameterAveragingTrainingMaster.Builder(numBatch)   //weight average service
  13.                                                       .workerPrefetchNumBatches(0)  
  14.                                                       .saveUpdater(true)  
  15.                                                       .averagingFrequency(5)  
  16.                                                       .batchSizePerWorker(numBatch)  
這裡我們獲取傳入的一些引數,如檔案的hdfs路徑,最後儲存model的路徑,mini-batch的大小(一般32,62,128這樣的值為好,可以自行嘗試),總的訓練的輪次epoch。
這裡需要解釋的是ParameterAveragingTrainingMaster這個類。這個類的作用是用於將spark worker節點上各自計算的權重收回到driver節點上進行加權平均,並將最新的權重廣播到worker節點上。也即為:將各個工作節點的引數的均值作為全域性引數值。這種分散式機器學習中,資料並行化的一種操作。

下面是定義神經網路結構和訓練網路:

  1. int nChannels = 1;  
  2. int outputNum = 10;  
  3. int iterations = 1;  
  4. int seed = 123;  
  5. MultiLayerConfiguration.Builder builder = new NeuralNetConfiguration.Builder()  //define lenent
  6.         .seed(seed)  
  7.         .iterations(iterations)  
  8.         .regularization(true).l2(0.0005)  
  9.         .learningRate(0.1)  
  10.         .learningRateScoreBasedDecayRate(0.5)  
  11.         .optimizationAlgo(OptimizationAlgorithm.STOCHASTIC_GRADIENT_DESCENT)  
  12.         .updater(Updater.ADAM)  
  13.         .list()  
  14.         .layer(0new ConvolutionLayer.Builder(55)  
  15.                 .nIn(nChannels)  
  16.                 .stride(11)  
  17.                 .nOut(20)  
  18.                 .weightInit(WeightInit.XAVIER)