darknet源碼分析(二):卷積層實現過程分析im2col部分

layer的內容很多,包括卷積層、反捲積層、池化層、shortcut層、損失函數層、全鏈接層等等……本文分析卷積層、反捲積、YOLO層、BN層

typedef enum {
    CONVOLUTIONAL,
    DECONVOLUTIONAL,
    CONNECTED,
    MAXPOOL,
    SOFTMAX,
    DETECTION,
    DROPOUT,
    CROP,
    ROUTE,
    COST,
    NORMALIZATION,
    AVGPOOL,
    LOCAL,
    SHORTCUT,
    ACTIVE,
    RNN,
    GRU,
    LSTM,
    CRNN,
    BATCHNORM,
    NETWORK,
    XNOR,
    REGION,
    YOLO,
    ISEG,
    REORG,
    UPSAMPLE,
    LOGXENT,
    L2NORM,
    BLANK
} LAYER_TYPE;

layer結構體定義

struct layer;
typedef struct layer layer;

struct layer{
    LAYER_TYPE type; /*指定layer的類型*/
    ACTIVATION activation;/*指定激活層激活函數的類型*/
    COST_TYPE cost_type;/*指定損失函數的類型,這通常是最後一層才用*/
    void (*forward)   (struct layer, struct network);
    void (*backward)  (struct layer, struct network);
    void (*update)    (struct layer, update_args);
    void (*forward_gpu)   (struct layer, struct network);
    void (*backward_gpu)  (struct layer, struct network);
    void (*update_gpu)    (struct layer, update_args);
    int batch_normalize;
    int shortcut;
    int batch;
    int forced;
    int flipped;
    int inputs;
    int outputs;
    int nweights;
    int nbiases;
    int extra;
   /* 根據region_layer.c判斷,這個變量表示一張圖片含有的真實值的個數,對於檢測模型來說,一個真實的標籤含有5個值,
    ** 包括類型對應的編號以及定位矩形框用到的w,h,x,y四個參數,且在darknet中,固定每張圖片最大處理30個矩形框(可查看max_boxes參數),
    ** 因此,在region_layer.c的make_region_layer()函數中,賦值爲30*5*/
    int truths;
    int h,w,c;
    int out_h, out_w, out_c;
    int n;
    int max_boxes;
    /*這個參數目前僅發現用在softmax_layer中,含義是將一張圖片的數據分成幾組,具體的值由網絡配置文件指定,如未指定默認爲1(見parse_softmax())*/
    int groups;
    /*kernel size*/
    int size;
    int side;
    int stride;
    int reverse;
    int flatten;
    int spatial;
    int pad;
    int sqrt;
    int flip;
    int index;
    int binary;
    int xnor;
    int steps;
    int hidden;
    int truth;
    float smooth;
    float dot;
    float angle;
    float jitter;
    float saturation;
    float exposure;
    float shift;
    float ratio;
    float learning_rate_scale;
    float clip;
    int noloss;
    int softmax;
    int classes;/*基本適用於識別問題中,指物體類別種數*/
    int coords;/*用於檢測任務,通常設爲4指檢測框要用的四個座標*/
    int background;
    int rescore;
    int objectness;
    int joint;
    int noadjust;
    int reorg;
    int log;
    int tanh;
    int *mask;
    int total;

    float alpha;
    float beta;
    float kappa;

    float coord_scale;
    float object_scale;
    float noobject_scale;
    float mask_scale;
    float class_scale;
    int bias_match;
    int random;
    float ignore_thresh;
    float truth_thresh;
    float thresh;
    float focus;
    int classfix;
    int absolute;

    int onlyforward;
    int stopbackward;
    int dontload;
    int dontsave;
    int dontloadscales;
    int numload;

    float temperature;
    float probability;/*dropout的概率*/
    /*
     ** 比例因子,爲保留概率的倒數
     ** 模型使用了dropout layer,訓練的時候只有佔比爲 p 的隱藏層單元參與訓練,那麼在預測的時候,如果所有的隱藏層單元都需要參與進來,則得到的結果相比訓練時平均要大 1/p ,
     ** 爲了避免這種情況,就需要測試的時候將輸出結果乘以 p 使下一層的輸入規模保持不變。而利用inverted dropout,我們可以在訓練的時候直接將dropout後留下的權重擴大1/p 倍
     ** 這樣在預測的時候也不用做額外的操作了,更方便一些。
    */
    float scale;

    char  * cweights;
    int   * indexes;
    int   * input_layers;
    int   * input_sizes;
    int   * map;
    int   * counts;
    float ** sums;
    float * rand;
    float * cost;
    float * state;
    float * prev_state;
    float * forgot_state;
    float * forgot_delta;
    float * state_delta;
    float * combine_cpu;
    float * combine_delta_cpu;

    float * concat;
    float * concat_delta;

    float * binary_weights;

    float * biases;
    float * bias_updates;

    float * scales;
    float * scale_updates;


    float * weights;/*當前層所有權重係數(連接當前層和上一層的係數,但記在當前層上),對於卷積層,維度爲l.n*l.c*l.size*l.size,即卷積核個數乘以卷積核尺寸再乘以輸入通道數*/
    float * weight_updates;

    float * delta;
    float * output;
    float * loss;
    float * squared;
    float * norms;

    float * spatial_mean;
    float * mean;
    float * variance;

    float * mean_delta;
    float * variance_delta;

    float * rolling_mean;
    float * rolling_variance;

    float * x;
    float * x_norm;

    float * m;
    float * v;
    
    float * bias_m;
    float * bias_v;
    float * scale_m;
    float * scale_v;

    /*cpu參數*/
    float *z_cpu;
    float *r_cpu;
    float *h_cpu;
    float * prev_state_cpu;

    float *temp_cpu;
    float *temp2_cpu;
    float *temp3_cpu;

    float *dh_cpu;
    float *hh_cpu;
    float *prev_cell_cpu;
    float *cell_cpu;
    float *f_cpu;
    float *i_cpu;
    float *g_cpu;
    float *o_cpu;
    float *c_cpu;
    float *dc_cpu; 

    float * binary_input;

    struct layer *input_layer;
    struct layer *self_layer;
    struct layer *output_layer;

    struct layer *reset_layer;
    struct layer *update_layer;
    struct layer *state_layer;

    struct layer *input_gate_layer;
    struct layer *state_gate_layer;
    struct layer *input_save_layer;
    struct layer *state_save_layer;
    struct layer *input_state_layer;
    struct layer *state_state_layer;

    struct layer *input_z_layer;
    struct layer *state_z_layer;

    struct layer *input_r_layer;
    struct layer *state_r_layer;

    struct layer *input_h_layer;
    struct layer *state_h_layer;
	
    struct layer *wz;
    struct layer *uz;
    struct layer *wr;
    struct layer *ur;
    struct layer *wh;
    struct layer *uh;
    struct layer *uo;
    struct layer *wo;
    struct layer *uf;
    struct layer *wf;
    struct layer *ui;
    struct layer *wi;
    struct layer *ug;
    struct layer *wg;

    tree *softmax_tree;

    size_t workspace_size;

#ifdef GPU
    int *indexes_gpu;

    float *z_gpu;
    float *r_gpu;
    float *h_gpu;

    float *temp_gpu;
    float *temp2_gpu;
    float *temp3_gpu;

    float *dh_gpu;
    float *hh_gpu;
    float *prev_cell_gpu;
    float *cell_gpu;
    float *f_gpu;
    float *i_gpu;
    float *g_gpu;
    float *o_gpu;
    float *c_gpu;
    float *dc_gpu; 

    float *m_gpu;
    float *v_gpu;
    float *bias_m_gpu;
    float *scale_m_gpu;
    float *bias_v_gpu;
    float *scale_v_gpu;

    float * combine_gpu;
    float * combine_delta_gpu;

    float * prev_state_gpu;
    float * forgot_state_gpu;
    float * forgot_delta_gpu;
    float * state_gpu;
    float * state_delta_gpu;
    float * gate_gpu;
    float * gate_delta_gpu;
    float * save_gpu;
    float * save_delta_gpu;
    float * concat_gpu;
    float * concat_delta_gpu;

    float * binary_input_gpu;
    float * binary_weights_gpu;

    float * mean_gpu;
    float * variance_gpu;

    float * rolling_mean_gpu;
    float * rolling_variance_gpu;

    float * variance_delta_gpu;
    float * mean_delta_gpu;

    float * x_gpu;
    float * x_norm_gpu;
    float * weights_gpu;
    float * weight_updates_gpu;
    float * weight_change_gpu;

    float * biases_gpu;
    float * bias_updates_gpu;
    float * bias_change_gpu;

    float * scales_gpu;
    float * scale_updates_gpu;
    float * scale_change_gpu;

    float * output_gpu;
    float * loss_gpu;
    float * delta_gpu;
    float * rand_gpu;
    float * squared_gpu;
    float * norms_gpu;
#ifdef CUDNN
    cudnnTensorDescriptor_t srcTensorDesc, dstTensorDesc;
    cudnnTensorDescriptor_t dsrcTensorDesc, ddstTensorDesc;
    cudnnTensorDescriptor_t normTensorDesc;
    cudnnFilterDescriptor_t weightDesc;
    cudnnFilterDescriptor_t dweightDesc;
    cudnnConvolutionDescriptor_t convDesc;
    cudnnConvolutionFwdAlgo_t fw_algo;
    cudnnConvolutionBwdDataAlgo_t bd_algo;
    cudnnConvolutionBwdFilterAlgo_t bf_algo;
#endif
#endif
};

卷積層是怎樣煉成的

首先,darknet的卷積與caffe的卷積相同,都是先使用im2col函數將輸入的特徵圖轉化爲

(輸入通道數*卷積核高*卷積核寬)* (輸出單通道的特徵圖高*輸出單通道的特徵圖寬)

而權重矩陣的大小爲

(輸出通道數)* (輸入通道數*卷積核高*卷積核寬)

這樣通過gemm函數進行矩陣乘法,權重矩陣*轉化後的特徵圖矩陣就得到了最後的輸出,其大小爲 (輸出通道數)* (輸出單通道的特徵圖高*輸出單通道的特徵圖寬)

爲了瞭解這個過程可以看看卷積層的前向傳播過程forward_convolutional_layer(convolutional_layer l, network net)

/*卷積層的前向傳播*/
void forward_convolutional_layer(convolutional_layer l, network net)
{
    int i, j;
    /*
    ** l.outputs即batch中一個輸入對應的輸出特徵總元素的個數
    ** 此函數就是將一個batch中所有輸入對應的輸出特徵都初始化爲0
    */

    fill_cpu(l.outputs*l.batch, 0, l.output, 1);
    /*是否對權重與輸入進行二值化,二值化是一種模型量化的方法,能夠加快模型在硬件上的速度*/
    if(l.xnor){
        binarize_weights(l.weights, l.n, l.c/l.groups*l.size*l.size, l.binary_weights);
        swap_binary(&l);
        binarize_cpu(net.input, l.c*l.h*l.w*l.batch, l.binary_input);
        net.input = l.binary_input;
    }
  
    int m = l.n/l.groups; /*該卷積層卷積核的個數*/
    int k = l.size*l.size*l.c/l.groups; /*卷積核元素的個數*/
    int n = l.out_w*l.out_h; /*該層輸出單通道的特徵圖的尺寸*/
    /*循環batch中的每個輸入*/
    for(i = 0; i < l.batch; ++i){
        for(j = 0; j < l.groups; ++j){
            float *a = l.weights + j*l.nweights/l.groups; /*a是指向當前層所有卷積核的,大小爲(l.n)*(l.c*l.size*l.size)*/
            float *b = net.workspace; /*用於存儲經im2col轉換後的輸入特徵矩陣*/
            float *c = l.output + (i*l.groups + j)*n*m; /*輸出特徵圖個數*/
            float *im =  net.input + (i*l.groups + j)*l.c/l.groups*l.h*l.w;

            if (l.size == 1) {
                /*如果是1*1的卷積,那麼不用對輸入特徵進行轉化*/
                b = im;
            } else {
                im2col_cpu(im, l.c/l.groups, l.h, l.w, l.size, l.stride, l.pad, b); /*對輸入特徵進行轉化*/
            }
            /*進行矩陣乘法得到最終輸出*/
            gemm(0,0,m,n,k,1,a,k,b,n,1,c,n);
        }
    }

    if(l.batch_normalize){
        forward_batchnorm_layer(l, net);
    } else {
        add_bias(l.output, l.biases, l.batch, l.n, l.out_h*l.out_w);/**/
    }

    activate_array(l.output, l.outputs*l.batch, l.activation);
    if(l.binary || l.xnor) swap_binary(&l);
}

那麼其實重點就是理解im2col與gemm如何實現

im2col

之所以要先用im2col來解決卷積計算,是因爲這樣能將卷積問題轉換爲一個矩陣乘法問題,這樣會讓卷積的實現更加的高效。

先來看 imcol的實現代碼

/*
** 獲得輸入的特徵圖im的特定行、列、通道的數據
** im: 輸入特徵圖的指針
** height: 輸入特徵圖的高
** width: 輸入特徵圖的寬
** channels: 輸入特徵圖的通道數
** row: 指定的要提取的行
** col: 指定的要提取的列
** channel: 指定的要提取的通道
** pad: pad的大小,這個有影響到最後的結果
*/
float im2col_get_pixel(float *im, int height, int width, int channels,
                        int row, int col, int channel, int pad)
{
    /*因爲當前給定的row和col是加了pad即補0之後的行列號,因此爲了得到真正的行列號,我們需要分別減去pad
    ** 注意,我們做pad時並非真的是在輸入特徵圖上補全0的行與列,而是在im2col轉化的過程中假裝輸入特徵圖裏面有0的行與列,之後在轉化後的結構中插入0
    */
    row -= pad;
    col -= pad;
    /*若出現判斷中的這四種情況,說明我們要取的數據在pad行或列中,最後輸出一定是0*/
    if (row < 0 || col < 0 ||
        row >= height || col >= width) return 0;
    /*若要取得數據不在pad行或者pad列中,說明位於輸入特徵圖中,因此直接取出對應位置的數據就可以*/
    /*首先定位到對應的通道即width*height*channel,之後定位具體位置,即再加上col+width*row*/
    return im[col + width*(row + height*channel)];
}

//From Berkeley Vision's Caffe!
//https://github.com/BVLC/caffe/blob/master/LICENSE

/*注意上面的註釋,說明darknet的卷積操作是從caffe源碼中卷積的操作原理是相同的,但我看了一下,雖然結果是相同的,但caffe的im2col明顯實現的要更加複雜一些
** 因此想研究caffe卷積的可以從darknet的卷積開始瞭解,我個人認爲caffe的實現並沒有比darknet好
** 這裏data_col就是轉換後輸入特徵圖的指針,我們最後的結果都保存到這
** ksize是指卷積核的大小
** stride 是卷積核每次移動的跨度
*/
void im2col_cpu(float* data_im,
     int channels,  int height,  int width,
     int ksize,  int stride, int pad, float* data_col) 
{
    int c,h,w;
    /*得到輸出特徵圖的高和寬,其實這裏是不用算的,因爲在make_convolutional函數中已經算過,直接傳到這裏就好了*/
    int height_col = (height + 2*pad - ksize) / stride + 1;
    int width_col = (width + 2*pad - ksize) / stride + 1;
    /*我們知道卷積運算時,我們是要用卷積對特徵圖所有通道都作卷積運算,因此這裏我們使用輸入通道數乘卷積核的大小,從而代表針對特徵圖同一位置卷積運算要用到的卷積核元素個數
    * 同時該變量也是轉換過後矩陣的行數
    */
    int channels_col = channels * ksize * ksize;
    /*以下三個循環決定了經過轉換的特徵圖矩陣的最終形式*/
    /*第一個循環表示轉換後矩陣的行數:輸入通道數*卷積核高*卷積核寬*/
    for (c = 0; c < channels_col; ++c) {
        /*以下三個偏移的計算就是要算出當前行的第一個元素在卷積核上對應的位置*/
        int w_offset = c % ksize; /*計算列偏移:卷積核是一個二維矩陣,並按行存儲在一維數組中,利用求餘運算獲取對應在卷積核中的列數*/
        int h_offset = (c / ksize) % ksize; /*計算行偏移*/
        int c_im = c / ksize / ksize;/*計算通道偏移*/
        /*接下來兩個循環就是個表示轉換後特徵矩陣的列數,即輸出特徵圖高*輸出特徵圖寬*/
        for (h = 0; h < height_col; ++h) {
            for (w = 0; w < width_col; ++w) {
                int im_row = h_offset + h * stride; /*如果stride不爲1,那麼加上h*stride就是對對卷積核進行了移位操作*/
                int im_col = w_offset + w * stride;
                int col_index = (c * height_col + h) * width_col + w;/*轉換後矩陣位置的索引*/
                data_col[col_index] = im2col_get_pixel(data_im, height, width, channels,
                        im_row, im_col, c_im, pad);
            }
        }
    }
}

      通過畫圖直觀的來看看im2col到底幹了什麼,假設我們的輸入特徵圖是5*5單通道的,卷積核是3*3單通道的,這裏使用單通道是爲了方便講解、畫圖,同時stride=2,那麼我們經im2col轉換後的特徵圖大小應該是(3*3*1)*(2*2)即9*4

           

 

這個轉換過程的就是由代碼中三個循環來完成的,第一個循環體的大小爲channels*ksize*ksize即9,由此可見第一個循環決定的是轉換後矩陣的行數,後兩個循環體的大小就是算出來的輸出特徵圖的高*輸出特徵圖的寬,由此可見後兩個循環完成的是列的填充。

注意到在後兩個循環前,我們還計算了三個偏移,分別是列偏移(w_offset)、行偏移(h_offset)、通道偏移(c_im)這三個偏移可以這樣理解,這三個偏移是爲了得到轉換後特徵圖第c行的所有數據所用到的卷積核的索引。讓我們分析以下轉換後特徵矩陣的第一行數據是怎麼得到的。

經過第一個循環,當前c=0,意味着我們後兩個循環是爲了填充第一行的所有列,此時w_offset = 0%3 = 0, h_offet = (0/3) % 3 = 0, c_im = 0, 也就是說第一行的數據是由卷積核的第一行第一列第一個通道得到的,事實上由於我們的卷積核只有一個通道因此c_im一直爲0.之後進入下面兩個循環,我們可以發現在第3個循環後,又計算了兩個變量,這兩個變量分別代表了原輸入特徵圖的索引,這兩個變量在原先的偏移上分別加上h*stride和w*stride相當於就是在做卷積核的移位操作。由此經過im2col轉換後的特徵圖矩陣的第一行實際上是每次卷積核移位後覆蓋的輸入特徵圖部分的(w_offeset, h_offset)位置的數據,因此第一個循環體完成第一個循環之後,我們得到了第一行的數據,如下圖。

之後便進行第一個循環體的第二次循環,即要得到第二行的數據,此時我們可以計算出w_offset等於1,h_offset等於0,也就是說我們第二行的數據應該用的是每個卷積核第一行第二列在原特徵矩陣上對應的數據,第二個循環體結束之後,我們便得到了第二行的數據

由此經過第一個循環體9次循環,我們便得到了最後的9*4的矩陣。我們注意到源碼的註釋裏寫道caffe的與源碼地址,因此darknet的卷積實現過程實際上是借鑑了caffe的,但是如果看過caffe源碼,會發現其實現和caffe還是有差別的,雖然都是先用ime2col轉換輸入特徵矩陣,之後再用矩陣乘法解決,但是caffe的實現明顯和這個不一樣,darknet的實現過程更好理解一些,但是那個更加高效還不算很清楚,我想之後可以做個檢測看看

這裏需要注意一下就是我們最後轉換出來的輸入特徵圖是按行存儲的且各通道的數據併成一行,是一維矩陣的形式,這個對於理解gemm的實現會有一定的幫助

 

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