tml lang="zh" data-theme="light">

知乎 - 知乎

Caffe-fast-rcnn源碼分析(一)

來自專欄目標檢測1 人贊了文章

轉載請註明出處!

Caffe-fast-rcnn源碼分析(一)

————————

接著上一篇文章Faster R-CNN(一),現在我們看Layer的Forward方法,以cpu模式為例,方法處理邏輯為,

Dtype loss = 0;
Reshape(bottom, top);
Forward_cpu(bottom, top);
for (int top_id = 0; top_id < top.size(); ++top_id) {
if (!this->loss(top_id)) { continue; }
const int count = top[top_id]->count();
const Dtype* data = top[top_id]->cpu_data();
const Dtype* loss_weights = top[top_id]->cpu_diff();
loss += caffe_cpu_dot(count, data, loss_weights);
}

這個Reshape函數先驗證數據blob的shape(各維度尺寸)是否合理,然後計算輸出shape。

數據blob的shape為(N,C,H,W),分別表示一個batch中image的數量N,一個image中通道數C,image的高H 以及image的寬W,可見通道維位置索引channel_axis_=1,第一個表示空間尺寸的維度為高H,其位置索引為first_spatial_axis=channel_axis_+1=2,表示空間尺寸維度的數量num_spatial_axes_=2

前向傳播計算Forward_cpu結束之後 ,對本層的輸出(數組)乘以係數(數組)作為本層貢獻的損失,這個係數(或者稱損失權重)存儲在Blob對象的diff_中,具體可參考Layer類中的SetLossWeights函數 。

卷積層

template <typename Dtype>
void ConvolutionLayer<Dtype>::compute_output_shape() {
const int* kernel_shape_data = this->kernel_shape_.cpu_data();
const int* stride_data = this->stride_.cpu_data();
const int* pad_data = this->pad_.cpu_data();
const int* dilation_data = this->dilation_.cpu_data();
this->output_shape_.clear();
for (int i = 0; i < this->num_spatial_axes_; ++i) {
// i + 1 to skip channel axis
// input_shape(i)=>(*bottom_shape_)[channel_axis_ + i];
const int input_dim = this->input_shape(i + 1); // 依次獲取輸入空間尺寸(高和寬)
// 獲取膨脹後的卷積核尺寸
const int kernel_extent = dilation_data[i] * (kernel_shape_data[i] - 1) + 1;
// 輸出大小: (w+2p-k)/s+1
const int output_dim = (input_dim + 2 * pad_data[i] - kernel_extent)
/ stride_data[i] + 1;
this->output_shape_.push_back(output_dim);
}
}

然後還要獲取輸出的batch大小以及通道數,

vector<int> top_shape(bottom[0]->shape().begin(),
bottom[0]->shape().begin() + channel_axis_); // 添加batch大小
top_shape.push_back(num_output_); // 添加輸出channel,即卷積核數量
for (int i = 0; i < num_spatial_axes_; ++i) {
top_shape.push_back(output_shape_[i]); // 添加輸出空間的高和寬
}

可知,輸入(bottom[0])shape為(N, in_c, in_h, in_w),輸出為(N, out_c, out_h, out_w),關係為,

k=dilation*(k-1)+1  quad quad (1)\ out=(in+2p-k)/s+1 quad quad (2) \ 	ext{out} 
ightarrow 	ext{ out_h/out_w, in} 
ightarrow 	ext{in_h/in_w, k} 
ightarrow 	ext{k_h/k_w}

卷積核的weight shape為(out_c, in_c/group_, k_h, k_w),分別表示輸出通道,輸入通道除以分組數,卷積核高,卷積核寬, 可見是考慮了分組卷積的。卷積核的偏置bias shape為(out_c)即卷積核的數量,為了表示方便,記:

  1. g:分組數量
  2. N:batch中image數量
  3. iw,ih,ic為輸入寬高通道數
  4. ow,oh,oc為輸出寬高通道數
  5. k為卷積核邊長(寬高相等的情況)
  6. bd:單個image的輸入數據大小,因為bottom作為輸入,代碼中對應的是bottom_dim_,(參考下面圖1最左邊的長方體)
  7. td:單個image的輸出數據大小,因為top作為輸出,代碼中對應的是top_dim_,(參考圖1最右邊所有藍色部分)
  8. osd:單個輸出平面的空間尺寸大小,可參考圖1中右邊單個藍色平面,代碼中對應conv_out_spatial_dim_
  9. oo:分組後,每組中所有輸出大小,參考圖2上面那組右邊的藍色部分,代碼中對應output_offset_
  10. kd:分組後(圖2)單個卷積核位於每組中的大小(圖2中間單個黃綠色長方體大小),代碼中對應的是kernel_dim_
  11. wo:分組後(圖2)每組中所有卷積核大小,可參考圖2中間的上面那組所有黃綠色長方體大小,代碼中對應的是weight_offset_
  12. co:

kd=ic/g*k*k   quad quad (3)\ bd=ic*ih*iw quad quad(4) \ td=oc*oh*ow quad quad (5) \ wo=oc/g*kd  quad quad (6) \ osd=oh*ow quad quad (7) \ oo=oc/g * osd quad quad (8)

分組卷積過程由圖1,2很直觀地給出。

圖1 不分組卷積

圖2 分組卷積,圖中為分成上下兩組

卷積層的前向傳播計算代碼為,

template <typename Dtype>
void ConvolutionLayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top) {
const Dtype* weight = this->blobs_[0]->cpu_data(); // 卷積核參數
for (int i = 0; i < bottom.size(); ++i) {
const Dtype* bottom_data = bottom[i]->cpu_data();// 第 i 個輸入blob數據
Dtype* top_data = top[i]->mutable_cpu_data(); // 第 i 個輸出blob數據
for (int n = 0; n < this->num_; ++n) { // num_: 批次中的image數量,即 N
// 輸入輸出blob數據結構均為 (N, c, h, w)
this->forward_cpu_gemm(bottom_data + n * this->bottom_dim_,// 第n個image的輸入數據起始地址
weight,
top_data + n * this->top_dim_ // 第n個image的輸出數據起始地址
);
if (this->bias_term_) {
const Dtype* bias = this->blobs_[1]->cpu_data();
this->forward_cpu_bias(top_data + n * this->top_dim_, bias);
}
}
}
}
template <typename Dtype>
void BaseConvolutionLayer<Dtype>::forward_cpu_gemm(const Dtype* input,
const Dtype* weights, Dtype* output, bool skip_im2col) {
const Dtype* col_buff = input;
if (!is_1x1_) {
if (!skip_im2col) {
conv_im2col_cpu(input, col_buffer_.mutable_cpu_data());
}
col_buff = col_buffer_.cpu_data();
}
for (int g = 0; g < group_; ++g) { // 分組卷積,參考圖2
caffe_cpu_gemm<Dtype>(CblasNoTrans, CblasNoTrans,
conv_out_channels_ / group_, // oc/g
conv_out_spatial_dim_, // oh*ow
kernel_dim_, // ic/g*k*k
(Dtype)1., weights + weight_offset_ * g,// 第g個分組的參數地址,wo是每個分組中所有卷積核大小
col_buff + col_offset_ * g,
(Dtype)0., output + output_offset_ * g);// 第g個分組對應輸出地址,oo是每個分組的所有輸出大小
}
}
template<>
void caffe_cpu_gemm<float>(const CBLAS_TRANSPOSE TransA,
const CBLAS_TRANSPOSE TransB, const int M, const int N, const int K,
const float alpha, const float* A, const float* B, const float beta,
float* C) {
int lda = (TransA == CblasNoTrans) ? K : M;
int ldb = (TransB == CblasNoTrans) ? N : K;
cblas_sgemm(CblasRowMajor, TransA, TransB, M, N, K, alpha, A, lda, B,
ldb, beta, C, N);
}

圖3 分組後,某一組的卷積過程

根據前文卷積,可知圖2中某一組的卷積,將張量重組為矩陣為圖3(不分組的情況可看作分組數量為1),從而卷積過程轉化為矩陣相乘,使用cblas庫計算,計算函數為,

// layout表示矩陣主行還是主列。C/C++主行,Fortran主列
// TransA/TransB 是否轉置A/B
// M為A和C的行數,N為B和C的列數,K為A的列數B的行數
// lda為A的第一維度值,這裡是A的列數
// ldb為B的第一維度值,這裡是B的列數
// ldc為C的第一維度值,即C的列數
// sgemm 表示單精度single,dgemm表示雙精度double
// sgemm中的mm表示兩個矩陣matrix相乘,sgemv表示矩陣與向量相乘
void cblas_sgemm (const CBLAS_LAYOUT layout,
const CBLAS_TRANSPOSE TransA, const CBLAS_TRANSPOSE TransB,
const int M, const int N, const int K,
const float alpha, const float *A, const int lda, const float *B, const int ldb,
const float beta, float *C, const int ldc);

於是卷積計算過程結束,如果需要加上偏置,則類似地有下圖,

圖4 卷積計算之後再添加偏置

ReLU層

輸出與輸入shape相同,且輸出與輸入的關係為,

top_data[i] = std::max(bottom_data[i], Dtype(0))
+ negative_slope * std::min(bottom_data[i], Dtype(0));

數學表達式為

y=egin{cases} x & xge 0 \ k*x & x<0 end{cases}  quad (9)

Pooling層

kernel_size,即執行池化操作的視窗大小,還可以通過 kernel_h/kernel_w分別指定視窗的高和寬。 pad/stridekernel_size類似,這三種參數與卷積的三種參數含義相同,設輸入尺寸(N,c,h,w),那麼池化後的尺寸為,

h=(h+2*p-k)/s+1   quad (10)\ w=(w+2*p-k)/s+1 quad (11) \ N=N  quadquad (12) \ c=c quadquad (13)

要得到池化後的數值,根據池化後的某點位置,反推其在池化之前數據平面上的視窗位置,然後根據max_poolavg_pool計算池化後的數值,例如圖4,輸出平面的一點坐標記為(i,j),其在輸出數據數組中的下標為index=i*w+j,可以得到視窗左上角位置(i,j)和右下角位置(ie,je)

圖4 池化

i=i*s-p ,  j=j*s-p quad(14) \ ie=i+k,  je=j+k quad(15)

這裡坐標是從0開始,並且(ie,je)是exclusive。

由於有padding,所以對於:

  1. max_pool,檢查坐標是否超過輸入平面的範圍,

i=max(i,0),  j=max(j,0) \ ie=min(ie,h),  je=min(je,w)

然後找出此視窗中數值最大的點,記坐標為(im,jm), 在輸入數據(一維)數組的下標為index=im*w+jm, 記錄坐標映射max_id_[index]=index,反向梯度傳播時會用到這個坐標映射。

2. ave_pool, 檢查坐標是否超過輸入平面加上padding之後的範圍[-p,h+p), [-p,w+p),顯然i,j不需要檢查, 最小就是-p,所以首先令,

ie=min(ie,h+p),  je=min(je,w+p)

然後由於要求平均,所以需要先計算視窗大小,

size=(ie-i)*(je-j)

然後再檢查(i,j)(ie,je)的範圍,令其在輸入平面範圍之內,從而能取輸入數據值,對於[-p,0)U(h,h+p)範圍內填充值,由於是0填充,所以可以略去,直接計算有效範圍內的數據點值之和,然後處以size即可,坐標檢查如下,

i=max(i,0),  j=max(j,0) \ ie=min(ie,h),  je=min(je,w)

Reshape層

重組輸入數據的shape,例如bottom blob數據結構為(N,C,H,W)四維,記維度數D=4reshape層的配置參數axisnum_axes控制bottom blob shape中的哪些部分需要重組, 記需要重組的起始維度(inclusive)下標為s, 截止維度(exclusive)下標為e, 那麼,

s=egin{cases} axis & axis ge 0 \ D+1+axis & axis <0 end{cases} \ e=egin{cases} D & num\_axes=-1 \ s+num\_axes & 	ext{otherwise} end{cases}

顯然,se需要有一個範圍限制,

0le sle D, quad sle e le D

從而[s,e)範圍內的維度值需要重組,重組之後的維度值由配置參數shape指定,例如s=0,e=D=4,且

shape { dim: 0 dim: 2 dim: -1 dim: 0 }

注意shape中的維度數必須等於e-s, 維度值有以下幾種情況:

  1. dim=-1標定dim=-1最多只能一次,表示維度值 需要推斷出來,多於一次就沒法準確推斷
  2. dim=0表示這個維度值等於原來對應的維度值
  3. dim>0表示重組後就是這個維度值

對上述例子應用上面3點規則,不難推斷重組後的維度為(N,2, C*H/2,W),雖然blob的shape變了,但是blob的數據在一維數組中順序並沒有改變, 如圖5所示,

圖5 Reshape

Softmax層

配置參數axis指定沿著這個維度執行softmax操作, 比如axis=1表示沿著通道這個維度求softmax,記blob結構為(N,C,H,W),blob中某一image的空間平面坐標(i,j),那麼沿著通道方向共有C個數據,記數據值為d1,d2,...,dC,那麼softmax計算過程為,

d_m=max(d_1,d_2,...d_C)  quad (16)\ d_c leftarrow d_c-d_m  quad (17)\ p_s=sum_C exp(d_c)  quad (18)\ p_c=frac {exp(d_c)}{p_s} quad (19) \ c=1,2,...,C

然而這只是計算了一個空間坐標點(i,j),對於整個平面一起計算, 採用矩陣演算法,過程如圖6,

圖6 softmax

圖6中還差最後一步即(19)式的計算,可將圖6中的MC矩陣的每一行向量與ps向量按元素相除(in-place)計算。


推薦閱讀:
查看原文 >>
相关文章