java寫卷積神經網絡---卷積神經網絡(CupCnn)的數據結構

前言

我在寫CupCnn的時候,一個困擾我很久的問題,就是如何組織卷積神經網絡的數據結構。尤其是卷積層和全連接層之間的銜接問題。卷積層至少需要四維的數據結構(batch+channel+height+width),而全連接層則只需一個二維的數據即可(batch+數據)。
CupCnn是我用java實現的一個卷積神經網絡,它的源碼可以從github下載:
點擊下載CupCnn

卷積神經網絡模型

這裏寫圖片描述
這張圖片是一個很典型的卷積神經網絡模型,在CupCnn中識別手寫數字的例子中使用的模型和它非常類似。從圖中可以看出,卷積層和全連接層有很大的不同,全連接層用一維數組就可以表示(加上batch就是二維),而卷積層的數據則至少需要一個三維的數據結構(加上batch就是四維,batch是指訓練的時候,一次送入神經網絡的圖片的數量。)。同一個神經網絡,使用統一的數據接口會使編程更加容易,因此,我們必須使用統一的四維模型來裝載一切的數據。

在CupCnn中,這個數據接口叫Blob,它的實現如下:

public class Blob implements Serializable{
    /**
     * 
     */
    private static final long serialVersionUID = 1L;
    private double[] data;
    private int numbers;
    private int channels;
    private int width;
    private int height;
    private int id;

    public Blob(int numbers,int channels,int height,int width){
        this.numbers = numbers;
        this.channels = channels;
        this.height = height;
        this.width = width;
        data = new double[getSize()];
    }


    //獲取第n個number的第channels個通道的第height行的第width列的數
    public double getDataByParams(int numbers,int channels,int height,int width){
        return data[numbers*get3DSize()+channels*get2DSize()+height*getWidth()+width];
    }

    public int getIndexByParams(int numbers,int channels,int height,int width){
        return (numbers*get3DSize()+channels*get2DSize()+height*getWidth()+width);
    }

    public int getWidth(){
        return width;
    }

    public int getHeight(){
        return height;
    }

    public int getChannels(){
        return channels;
    }

    public int getNumbers(){
        return numbers;
    }

    public int get2DSize(){
        return width * height;
    }

    public int get3DSize(){
        return channels*width*height;
    }

    public int get4DSize(){
        return numbers*channels*width*height;
    }

    public int getSize(){
        return get4DSize();
    }

    public void setId(int id){
        this.id = id;
    }

    public int getId(){
        return id;
    }

    public double[] getData(){
        return data;
    }

    public void fillValue(double value){
        for(int i=0;i<data.length;i++){
            data[i] = value;
        }
    }

    public void cloneTo(Blob to){
        to.numbers = this.numbers;
        to.channels = this.channels;
        to.height = this.height;
        to.width = this.width;
        double[] toData = to.getData();
        for(int i=0;i<data.length;i++){
            toData[i] = this.data[i];
        }
    }

}

Blob的實現中,所有的數據都存儲在一個一維的數組中,通過四個變量batch,channel,height,width來分別記錄它各個維度的大小,此外,還導出了get(x)DSize()這樣獲取維度大小的接口。
卷積神經網絡工作的工程中,數據的變化如下:
這裏寫圖片描述
對於一個64*64大小的三維圖片,經過一個卷積層+一個池化層後,圖片的大小變爲一半(卷積方式爲same),但是通道卻極大的增多了,注意,這裏要強調的是通道的增加。在CupCnn的實現過程中,假如指定的batch爲10,那麼每個層,它的batch都是10,至始自終不會改變,卷積層主要會增加channel,池化層不會增加channel,但會使圖像減小。
如果解釋的還不清楚,再來看下面這張圖:
這裏寫圖片描述
注意圖片中的連線,圖中,第一個卷積層有4個卷積核,分別對原始圖片做卷積,得到了4個28*28的圖像,這裏顯然是使用了valide的方式進行的卷積,如果使用的是same的方式,卷積後大小仍爲32*32。池化不會再增加通道,而是將每一個圖像都變小了。至於卷積和池化的具體工作流程,這裏不再展開。
注意:圖中的數據沒有添加batch的概念,加上batch後會更加複雜。但是隻要高清了這幅圖中的工作機制,相信理解加上batch後的卷積神經網絡也就不是事了。

卷積層與全連接層的銜接

用一個一維的數組保存所有的數據除了速度上的優勢之外,還有個很大的便利就是在卷積層和全連接層進行銜接的時候,由於數據本來就是存儲在一維數組上的,我們完全可以忘記它是四維的數據結構,而把它當成一個一維的數據結構。這樣就可以輕易的實現卷積到全連接的過度。

Blob的傳遞

在卷積神經網絡中,這一層的輸出便是下一層的輸入。CupCnn中數據流動的就是Blob這個結構。爲了方便下一個層獲取上一個層的輸出,CupCnn中的每一個層都有一個id,這個id是他在卷積神經網絡中的位置,或者序號。比如第一個輸入層它的id=0,第二個層它的id=1。此外,每一個層都有一個network的引用,因爲所有的數據都由network統一管理,擁有network的引用,可以輕易的通過id索引獲取任意一層的數據,包括輸出和diff。
一開始就創建所有的需要的數據結構:

    public Network(){
        datas = new ArrayList<Blob>();
        diffs = new ArrayList<Blob>();
        layers = new ArrayList<Layer>();
    }

根據每一個層的配置參數創建層,每一層的輸出Blob和殘差Blob:

    public void prepare(){
        for(int i=0;i<layers.size();i++){
            BlobParams layerParams = layers.get(i).getLayerParames();
            assert (layerParams.getNumbers()>0 && layerParams.getChannels()>0 && layerParams.getHeight()>0 && layerParams.getWidth() >0):"prapare---layer params error";
            Blob data = new Blob(batch,layerParams.getChannels(),layerParams.getHeight(),layerParams.getWidth());
            datas.add(data);
            Blob diff = new Blob(data.getNumbers(),data.getChannels(),data.getHeight(),data.getWidth());
            diffs.add(diff);
            layers.get(i).setId(i);
            layers.get(i).prepare();
        }
    }

通過id獲取指定層的數據:

@Override
    public void forward() {
        // TODO Auto-generated method stub
        Blob input = mNetwork.getDatas().get(id-1);
        Blob output = mNetwork.getDatas().get(id);
        double [] outputData = output.getData();
        double [] zData = z.getData();
        ...

寫在最後

寫卷積神經網絡的時候,建議先寫全連接層,因爲寫完全連接層就可以驗證神經網絡的正確性。這個時候,大家還是要注意數據結構一開始就用四維的,爲以後和卷積層銜接做準備。如果您在寫代碼的過程中遇到什麼困惑或者有什麼興奮的改進,都可以家下面的QQ羣互相交流:

機器學習 QQ交流羣:704153141

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章