前言
我在寫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