天天看點

【darknet源碼解析-05】im2col.h 和 im2col.c 解析

本系列為darknet源碼解析,本次解析src/im2col.h 與 src/im2col.c 兩個。這塊其實是卷積操作的底層實作。im2col主要是完成矩陣的向量轉換,為了之後的gemm.c做矩陣乘法做準備,而im2col和gemm就是darknet卷積底層實作的核心。其實也是caffe卷積實作的核心。

img2col.h 中的包含的代碼如下:主要就是一個函數im2col_cpu定義,在這裡我們先不涉及gpu那快,先講解cpu這塊的矩陣的向量轉換。

#ifndef IM2COL_H
#define IM2COL_H

void im2col_cpu(float* data_im,
        int channels, int height, int width,
        int ksize, int stride, int pad, float* data_col);

#ifdef GPU

void im2col_gpu(float *im,
         int channels, int height, int width,
         int ksize, int stride, int pad,float *data_col);

#endif
#endif
           

 im2col.c 的詳細分析如下:

#include "im2col.h"
#include <stdio.h>
/**
 * 從輸入多通道數組im(存儲圖像資料)中擷取指定行、列、通道數處的元素值
 * @param im  輸入,所有資料都存成一個一維資料,例如對于3通道而言,每一個通道按行存儲(每一通道所有行合并成一行)
 *                 三通道依次再并成一行
 * @param height 每一通道的高度(即輸入圖像的真正高度,補0之前)
 * @param width
 * @param channels  輸入im的通道數,比如彩色圖為3通道,之後每一次卷積的輸入的通道數等于上一卷積層核的個數
 * @param row 要提取的元素所在的行(二維圖像補0之後的行數)
 * @param col
 * @param channel 要提取的元素所在的通道
 * @param pad 圖像左右上下各補0的長度(四個方向補0的長度一樣)
 * @return  float類型資料,為im中channel通道,row-pad行,col-pad列處的元素值,高,寬;
 *          而row與col則是補0之後,元素所在的行列,是以,要準确擷取im中元素值,首先要減去pad以擷取真實的行列數;
 */
float im2col_get_pixel(float *im, int height, int width, int channels,
                        int row, int col, int channel, int pad)
{
    // 減去補0的長度,得到真實的行數和列數
    row -= pad;
    col -= pad;

    // 如果行列數小于0,則傳回0(剛好是補0的效果)
    if (row < 0 || col < 0 ||
        row >= height || col >= width) return 0;
    // im存儲多通道二維圖像的資料格式為:各通道所有行并後成一行,再多通道一次并成一行;
    // 所在指定通道所在行,再加上col移位到所在列
    return im[col + width*(row + height*channel)];
}

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

/**
 * 将圖檔轉為便于計算的數組格式,這是直接從caffe移植過來的
 * @param data_im 輸入圖像
 * @param channels 輸入圖像的通道數(對于都一層,一般是顔色圖,3通道,中間層通道數為上一層卷積核個數)
 * @param height 輸入圖像的高度
 * @param width
 * @param ksize 卷積核尺寸
 * @param stride 步幅
 * @param pad 補0的個數
 * @param data_col 相當于輸出,為進行格式化重排後的輸入圖像資料
 *
 * 說明:輸出data_col的元素個數與data_im元素個數補相等,一般比data_im的元素個數多。
 *      因為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_layer()函數
    // 已經調用convolutional_out_width(), convolutional_out_height()函數求取這兩個參數,
    // 此處直接調用l.out_h, l.out_w即可,函數參數隻要傳入該層網絡指針即可)
    int height_col = (height + 2*pad - ksize) / stride + 1;
    int width_col = (width + 2*pad - ksize) / stride + 1;

    // 卷積核大小:ksize*ksize是一個卷積核大小,之所有乘以通道數channels,是因為輸入圖像是多通道。每個卷積核在做卷積時
    // 是同時對同一位置多通道的圖像進行卷積運算,這裡為了實作這一目的,将三通道上的卷積核并在一起以便于進行計算,是以卷積核
    // 實際上并不是二維的,而是三維的。比如對于3通道圖像,卷積核尺寸為3*3,該卷積核同時作用在三通道圖像上,這樣并起來就得到含有
    // 27個元素的卷積核,且這27個元素都是獨立的需要訓練的參數。是以在計算訓練參數個數時,一定要注意每一個卷積核的實際訓練參數需要
    // 乘以通道數


    //***********這三層循環之間的邏輯關系,決定了輸入圖像重排後的格式 *********

    // 外循環次數為一個卷積核的尺寸數,循環次數即為最終得到的data_col的總行數
    int channels_col = channels * ksize * ksize;//im2col後的矩陣行數,3*3*3 = 27
    for (c = 0; c < channels_col; ++c) {
        // 列偏移,卷積核是一個二維矩陣,并按行存儲在一維數組中,利用求餘運算擷取對應在卷積核的列數,比如對于
        // 3*3 的卷積核(3通道),當c=0,顯然在第一列,當c=5,顯示在第2列,當c=9時,在第二通道的卷積核的第一列,
        // 當c=26,在第三列(第三通道)。
        int w_offset = c % ksize;

        // 行偏移,卷積核是一個二維矩陣,且是安裝(卷積核所有行并成一行)存儲在一位數組中的,比如對于3*3的卷積核
        // 處理3通道的圖像,那麼一個卷積核具有27個元素,每9個元素對應一個通道上卷積核(互為一樣),每當c為3的倍數,就
        // 意味着卷積核換了一行,h_offset取值為0,1,2,對應3*3卷積核中的第1,2,3行

        int h_offset = (c / ksize) % ksize;

        // 通道偏移,channels_col是多通道的卷積核并在一起的,比如對于3通道,3*3卷積和,每過9個元素就要換一通道數,
        // 當c=0-8時候,c_im=0, c=9-17, c_im=1, c=18-26, c_im=2;
        int c_im = c / ksize / ksize; // 計算目前處理第幾個通道的圖像
        for (h = 0; h < height_col; ++h) {
            // 中循環次數等于該層輸出圖像函數height_col, 說明data_col中的每一行存儲了一張特征圖,這張特征圖又是按行存儲在data_col中某行中
            for (w = 0; w < width_col; ++w) {
                // 由上面可知,對于3*3的卷積核,h_offset取值為0,1,2,當h_offset=0時,會提取出所有與卷積核第一行元素進行運算的像素,
                // 依次類推;加上h×stride是對卷積核進行行移位操作,比如卷積核從圖像(0,0)位置開始做卷積,那麼最先涉及(0,0)——(3,3)
                // 之間的像素值,若stride=2,那麼卷積核進行一次行移位時,下一行的卷積操作是從元素(2, 0)(2為圖像行号,0為列号)開始
                int im_row = h_offset + h * stride;
                // 對于3*3的卷積核,w_offset取值也為0,1,2,當w_offset=1,會提取所有與卷積核中第2列元素進行運算的像素,
                // 比如前一次卷積其實像素元素(0,0),若stride=2,那麼下次卷積元素是從元素(2,0)(0為行号,2為列号)
                int im_col = w_offset + w * stride;
                // col_index為重排後圖像中的像素索引。等于c * height_col * width_col * w(還是按行存儲,所有通道在合并成一行)
                // 對應第c通道,h行,w列元素
                int col_index = (c * height_col + h) * width_col + w;
                // im2col_get_pixel函數擷取輸入圖像data_im,第c_im通道,im_row, im_col的像素值并指派給重排後的圖像,
                // 不是真實輸入圖像中行列号,是以需要減去pad獲得真實的行列号
                data_col[col_index] = im2col_get_pixel(data_im, height, width, channels,
                        im_row, im_col, c_im, pad);
            }
        }
    }
}
           

感覺直接講解代碼還是比較不直覺,下面我們舉個例子來說明im2col到底是怎樣工作的。

輸入:data_im是一個5*5【實際】,單通道的特征圖,卷積核大小3*3,卷積步長stride 為2,補0的個數為pad為1,則傳入參數如下:

data_im = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25};
pad = 1; stride = 2; ksize = 3; height = 5; width =5; chanels=1;
           

而data_im實際的邏輯結構如下,是5*5的二維矩陣。如下圖所示:

【darknet源碼解析-05】im2col.h 和 im2col.c 解析

補0的結果如下:

【darknet源碼解析-05】im2col.h 和 im2col.c 解析

 卷積後輸出特征圖的大小為:

height_col = (height+2*pad – ksize)/stride+1 = (5 +2*1-3)/2+1=3
wight_col = (width+2*pad – ksize)/stride+1 = (5 +2*1-3)/2+1=3
           

輸入特征圖轉換得到的矩陣data_col的行數:卷積核通道數×卷積核尺寸×卷積核尺寸,大家可以想想為什麼行數是這麼多???

chanenls_col = channels * ksize * ksize = 1*3*3 =9
           

輸入特征圖轉換得到的矩陣data_col的列數:卷積後輸出特征圖的寬度×卷積輸出特征圖的高度,這裡也可想想為什麼列數是這麼多???

height_col*weight_col= 3*3 = 9
           

 到這裡我就知道輸入特征圖轉換得到矩陣data_col的大小為 [channels_col, height_col * weight_col] = [9, 9]

那麼接下來,我們就要直到data_col矩陣每個元素是從輸入矩陣data_im怎麼樣轉換過來的。如下圖所示:

【darknet源碼解析-05】im2col.h 和 im2col.c 解析

 C = 0, h = 0, W = 0,1, 2的計算過程如下:黃色部分是對應到data_im坐标【row,col】,藍色是data_col的編号【按行存儲】,紅色部分表示映射得到的值。

【darknet源碼解析-05】im2col.h 和 im2col.c 解析
【darknet源碼解析-05】im2col.h 和 im2col.c 解析

  C = 0, h = 1, W = 0,1, 2的計算過程如下:黃色部分是對應到data_im坐标【row,col】,藍色是data_col的編号【按行存儲】,紅色部分表示映射得到的值。

【darknet源碼解析-05】im2col.h 和 im2col.c 解析
【darknet源碼解析-05】im2col.h 和 im2col.c 解析

  C = 0, h = 2, W = 0,1, 2的計算過程如下:黃色部分是對應到data_im坐标【row,col】,藍色是data_col的編号【按行存儲】,紅色部分表示映射得到的值。 

【darknet源碼解析-05】im2col.h 和 im2col.c 解析
【darknet源碼解析-05】im2col.h 和 im2col.c 解析

  C = 1, h = 0, W = 0,1, 2的計算過程如下:黃色部分是對應到data_im坐标【row,col】,藍色是data_col的編号【按行存儲】,紅色部分表示映射得到的值。

【darknet源碼解析-05】im2col.h 和 im2col.c 解析
【darknet源碼解析-05】im2col.h 和 im2col.c 解析

  C = 1, h = 1, W = 0,1, 2的計算過程如下:黃色部分是對應到data_im坐标【row,col】,藍色是data_col的編号【按行存儲】,紅色部分表示映射得到的值。

【darknet源碼解析-05】im2col.h 和 im2col.c 解析
【darknet源碼解析-05】im2col.h 和 im2col.c 解析

大家看看有沒有找出規律。。。最終轉換結果如下,data_col矩陣:大家data_col矩陣每一列與data_im中卷積核滑動過的位置。是不是有點明白了。

【darknet源碼解析-05】im2col.h 和 im2col.c 解析

完,