寫在前面
在上一篇博客,即重啓caffe源碼深入學習7中,筆者從最簡單的激活層開始,進行了caffe源碼的解析,尤其講述了梯度反傳的部分。在本篇博客中,筆者將解析另一個基礎層的源碼,即池化層,池化層與激活層類似,其中不包含任何可訓練參數。caffe池化層中包含的最大池化,平均池化等也是在深度學習中使用非常廣泛的一種結構。因此,筆者通過註釋池化層源碼,尤其是反傳部分,向大家講解梯度反傳在caffe框架中的實現,下面正式開啓乾貨。
池化層源碼及註釋
按照筆者的一貫風格,首先放出經過註釋的池化層源碼。
首先是pooling_layer.hpp源碼:
#ifndef CAFFE_POOLING_LAYER_HPP_
#define CAFFE_POOLING_LAYER_HPP_
#include <vector>
#include "caffe/blob.hpp"
#include "caffe/layer.hpp"
#include "caffe/proto/caffe.pb.h"
namespace caffe {
/**
* @brief Pools the input image by taking the max, average, etc. within regions.
*
* TODO(dox): thorough documentation for Forward, Backward, and proto params.
*/
template <typename Dtype>
class PoolingLayer : public Layer<Dtype> {
public:
explicit PoolingLayer(const LayerParameter& param)
: Layer<Dtype>(param) {} //構造函數直接繼承
virtual void LayerSetUp(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top); //需要在cpp文件中實現的LayerSetUp函數
virtual void Reshape(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top); //需要在cpp文件中實現的Reshape函數
virtual inline const char* type() const { return "Pooling"; }
virtual inline int ExactNumBottomBlobs() const { return 1; } //底層的blob數量必須爲1
virtual inline int MinTopBlobs() const { return 1; } //至少輸出一個頂層blob
// MAX POOL layers can output an extra top blob for the mask;
// others can only output the pooled inputs.
virtual inline int MaxTopBlobs() const {
return (this->layer_param_.pooling_param().pool() ==
PoolingParameter_PoolMethod_MAX) ? 2 : 1;
} //如果是最大池化,池化層最多輸出2個blob;否則只能輸出1個blob
protected:
virtual void Forward_cpu(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top); //cpu前傳
virtual void Forward_gpu(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top); //gpu前傳
virtual void Backward_cpu(const vector<Blob<Dtype>*>& top,
const vector<bool>& propagate_down, const vector<Blob<Dtype>*>& bottom); //cpu反傳
virtual void Backward_gpu(const vector<Blob<Dtype>*>& top,
const vector<bool>& propagate_down, const vector<Blob<Dtype>*>& bottom); //gpu反傳
int kernel_h_, kernel_w_; //池化操作所使用的核的高與寬
int stride_h_, stride_w_; //池化操作在高和寬方向上的步長
int pad_h_, pad_w_; //需要在輸入blob高和寬上pad的尺度
int channels_; //輸入blob的通道數
int height_, width_; //輸入blob的寬與高
int pooled_height_, pooled_width_; //輸出blob的寬與高
bool global_pooling_; //是否在高和寬方向上進行全局池化操作
PoolingParameter_RoundMode round_mode_; //池化輸出尺度計算的標準,到底是向上取整還是向下取整
Blob<Dtype> rand_idx_; //隨機池化需要的記錄輸出元素在輸入blob中的位置的blob
Blob<int> max_idx_; //最大池化需要的記錄輸出元素在輸入blob中的位置的blob
};
} // namespace caffe
#endif // CAFFE_POOLING_LAYER_HPP_
然後是pooling_layer.cpp源碼:
#include <algorithm>
#include <cfloat>
#include <vector>
#include "caffe/layers/pooling_layer.hpp"
#include "caffe/util/math_functions.hpp"
namespace caffe {
using std::min;
using std::max;
template <typename Dtype>
void PoolingLayer<Dtype>::LayerSetUp(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top) {
PoolingParameter pool_param = this->layer_param_.pooling_param(); //讀出池化操作的名稱
if (pool_param.global_pooling()) {
CHECK(!(pool_param.has_kernel_size() ||
pool_param.has_kernel_h() || pool_param.has_kernel_w()))
<< "With Global_pooling: true Filter size cannot specified"; //如果是全局池化,不能設置池化核的長寬
} else {
CHECK(!pool_param.has_kernel_size() !=
!(pool_param.has_kernel_h() && pool_param.has_kernel_w()))
<< "Filter size is kernel_size OR kernel_h and kernel_w; not both";
CHECK(pool_param.has_kernel_size() ||
(pool_param.has_kernel_h() && pool_param.has_kernel_w()))
<< "For non-square filters both kernel_h and kernel_w are required."; //如果不是全局池化,必須要在層設置參數中註明池化核的尺寸(長寬可以不一致)
}
CHECK((!pool_param.has_pad() && pool_param.has_pad_h()
&& pool_param.has_pad_w())
|| (!pool_param.has_pad_h() && !pool_param.has_pad_w()))
<< "pad is pad OR pad_h and pad_w are required."; //必須在層設置參數中註明對輸入blob高和寬上的pad(填充)參數
CHECK((!pool_param.has_stride() && pool_param.has_stride_h()
&& pool_param.has_stride_w())
|| (!pool_param.has_stride_h() && !pool_param.has_stride_w()))
<< "Stride is stride OR stride_h and stride_w are required."; //必須在層設置參數中註明池化操作在高和寬方向上的步長
global_pooling_ = pool_param.global_pooling(); //是否做global_pooling
round_mode_ = pool_param.round_mode(); //池化層輸出尺度的取整模式
if (global_pooling_) { //如果是全局池化,那麼池化核寬高就直接是輸入blob的寬高
kernel_h_ = bottom[0]->height();
kernel_w_ = bottom[0]->width();
} else { //如果不是全局池化,那麼池化核寬高就是層參數設置中的核寬高(可以不相同)
if (pool_param.has_kernel_size()) {
kernel_h_ = kernel_w_ = pool_param.kernel_size();
} else {
kernel_h_ = pool_param.kernel_h();
kernel_w_ = pool_param.kernel_w();
}
}
CHECK_GT(kernel_h_, 0) << "Filter dimensions cannot be zero."; //檢驗一下核寬高是否大於零
CHECK_GT(kernel_w_, 0) << "Filter dimensions cannot be zero.";
if (!pool_param.has_pad_h()) { //初始化一下在輸入blob高和寬上的pad尺寸,記錄在pad_h_和pad_w_中
pad_h_ = pad_w_ = pool_param.pad();
} else {
pad_h_ = pool_param.pad_h();
pad_w_ = pool_param.pad_w();
}
if (!pool_param.has_stride_h()) { //初始化一下在輸入blob高和寬上的操作步長,記錄在stride_h_和stride_w_中
stride_h_ = stride_w_ = pool_param.stride();
} else {
stride_h_ = pool_param.stride_h();
stride_w_ = pool_param.stride_w();
}
if (global_pooling_) {
CHECK(pad_h_ == 0 && pad_w_ == 0 && stride_h_ == 1 && stride_w_ == 1)
<< "With Global_pooling: true; only pad = 0 and stride = 1";
} //如果是global_pooling,長寬pad只能爲0,並且長寬stride只能爲1
if (pad_h_ != 0 || pad_w_ != 0) { //查看一下層參數設置中,是否是最大或者平均池化
CHECK(this->layer_param_.pooling_param().pool()
== PoolingParameter_PoolMethod_AVE
|| this->layer_param_.pooling_param().pool()
== PoolingParameter_PoolMethod_MAX)
<< "Padding implemented only for average and max pooling.";
CHECK_LT(pad_h_, kernel_h_); //檢查一下pad的尺度是否小於池化核的尺度
CHECK_LT(pad_w_, kernel_w_);
}
}
template <typename Dtype>
void PoolingLayer<Dtype>::Reshape(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top) {
CHECK_EQ(4, bottom[0]->num_axes()) << "Input must have 4 axes, "
<< "corresponding to (num, channels, height, width)"; //檢查一下輸入的blob是否是4維的(n,c,h,w)
channels_ = bottom[0]->channels(); //channels_記錄輸入blob的通道數
height_ = bottom[0]->height(); //height_記錄輸入blob的高
width_ = bottom[0]->width(); //width_記錄輸入blob的寬
if (global_pooling_) { //如果是全局池化,那麼池化核的寬高直接就是輸入blob的寬高
kernel_h_ = bottom[0]->height();
kernel_w_ = bottom[0]->width();
}
switch (round_mode_) {
case PoolingParameter_RoundMode_CEIL: //如果輸出的尺寸是向上取整,那麼就在除法中使用ceil
pooled_height_ = static_cast<int>(ceil(static_cast<float>(
height_ + 2 * pad_h_ - kernel_h_) / stride_h_)) + 1;
pooled_width_ = static_cast<int>(ceil(static_cast<float>(
width_ + 2 * pad_w_ - kernel_w_) / stride_w_)) + 1;
break;
case PoolingParameter_RoundMode_FLOOR: //如果輸出的儲存室向下取整,那麼就在除法中使用floor
pooled_height_ = static_cast<int>(floor(static_cast<float>(
height_ + 2 * pad_h_ - kernel_h_) / stride_h_)) + 1;
pooled_width_ = static_cast<int>(floor(static_cast<float>(
width_ + 2 * pad_w_ - kernel_w_) / stride_w_)) + 1;
break;
default:
LOG(FATAL) << "Unknown rounding mode.";
}
if (pad_h_ || pad_w_) { //爲了使得高和寬上最末尾的池化操作在原blob內部開始(而不是在pad的元素中開始),需要對池化輸出的尺度進行調整
// If we have padding, ensure that the last pooling starts strictly
// inside the image (instead of at the padding); otherwise clip the last.
if ((pooled_height_ - 1) * stride_h_ >= height_ + pad_h_) { //如果在高方向上pad太多了,就減少高方向池化輸出的尺度
--pooled_height_;
}
if ((pooled_width_ - 1) * stride_w_ >= width_ + pad_w_) { //如果在寬方向上pad太多了,就減少寬方向池化輸出的尺度
--pooled_width_;
}
CHECK_LT((pooled_height_ - 1) * stride_h_, height_ + pad_h_); //檢驗一下,在高方向上,最後一次池化操作有包含輸入blob的內容
CHECK_LT((pooled_width_ - 1) * stride_w_, width_ + pad_w_); //檢驗一下,在寬方向上,最後一次池化操作有包含輸入blob的內容
}
top[0]->Reshape(bottom[0]->num(), channels_, pooled_height_,
pooled_width_); //對池化的輸出進行初始化
if (top.size() > 1) {
top[1]->ReshapeLike(*top[0]); //如果輸出的blob大於一個,則將top[1]初始化爲top[0]的形狀
}
// If max pooling, we will initialize the vector index part.
if (this->layer_param_.pooling_param().pool() ==
PoolingParameter_PoolMethod_MAX && top.size() == 1) {
max_idx_.Reshape(bottom[0]->num(), channels_, pooled_height_,
pooled_width_); //如果是最大池化,並且只有top[0],那麼初始化max_idx_記錄元素選擇下標,在反傳中作用較大。
}
// If stochastic pooling, we will initialize the random index part.
if (this->layer_param_.pooling_param().pool() ==
PoolingParameter_PoolMethod_STOCHASTIC) {
rand_idx_.Reshape(bottom[0]->num(), channels_, pooled_height_,
pooled_width_); //如果是隨機池化,那麼初始化rand_idx_記錄元素選擇下標,在反傳中作用較大。
}
}
// TODO(Yangqing): Is there a faster way to do pooling in the channel-first
// case?
template <typename Dtype>
void PoolingLayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top) { //前傳
const Dtype* bottom_data = bottom[0]->cpu_data(); //輸入blob
Dtype* top_data = top[0]->mutable_cpu_data(); //輸出blob
const int top_count = top[0]->count(); //輸出的數據總量(n×c×h×w)
// We'll output the mask to top[1] if it's of size >1.
const bool use_top_mask = top.size() > 1; //如果是輸出多個top blob,那麼就是使用了top_mask
int* mask = NULL; // suppress warnings about uninitialized variables //定義mask指針,爲int型,因爲記錄的是下標
Dtype* top_mask = NULL; //定義一下top_mask指針
// Different pooling methods. We explicitly do the switch outside the for
// loop to save time, although this results in more code.
switch (this->layer_param_.pooling_param().pool()) { //判斷池化方式
case PoolingParameter_PoolMethod_MAX: //如果是進行最大池化
// Initialize
if (use_top_mask) { //如果要使用top_mask
top_mask = top[1]->mutable_cpu_data();
caffe_set(top_count, Dtype(-1), top_mask); //就全部初始化爲-1
} else {
mask = max_idx_.mutable_cpu_data(); //否則就直接使用max_idx_
caffe_set(top_count, -1, mask); //也全部初始化爲-1
}
caffe_set(top_count, Dtype(-FLT_MAX), top_data); //初始化一下top_data,全部設置爲最小的數(-FLT_MAX)
// The main loop
for (int n = 0; n < bottom[0]->num(); ++n) { //一個batch中逐圖像處理
for (int c = 0; c < channels_; ++c) { //一張圖像的特徵中逐通道處理
for (int ph = 0; ph < pooled_height_; ++ph) { //對於輸出數據blob,在高方向上逐一進行求值
for (int pw = 0; pw < pooled_width_; ++pw) { //對於輸出數據blob,在寬方向上逐一進行求值
int hstart = ph * stride_h_ - pad_h_; //找到在輸入blob上,該次池化操作在高方向的起始點
int wstart = pw * stride_w_ - pad_w_; //找到在輸入blob上,該次池化操作在寬方向的起始點
int hend = min(hstart + kernel_h_, height_); //找到在輸入blob上,該次池化操作在高方向的結束點
int wend = min(wstart + kernel_w_, width_); //找到在輸入blob上,該次池化操作在寬方向的結束點
hstart = max(hstart, 0); //確認一下池化操作在高上的起始點大於等於0
wstart = max(wstart, 0); //確認一下池化操作在寬上的起始點大於等於0
const int pool_index = ph * pooled_width_ + pw; //初始化一下池化輸出的元素在mask中的位置
for (int h = hstart; h < hend; ++h) {
for (int w = wstart; w < wend; ++w) { //這兩個for循環,遍歷一個輸入blob中的單次池化區域
const int index = h * width_ + w; //記錄一下選擇的最大值在輸入blob中的位置
if (bottom_data[index] > top_data[pool_index]) {
top_data[pool_index] = bottom_data[index]; //找到一個區域中的最大的元素
if (use_top_mask) {
top_mask[pool_index] = static_cast<Dtype>(index); //如果使用top_mask,就在top_mask中記錄一下,該位置輸出的數據在輸入blob中的位置
} else {
mask[pool_index] = index; //如果不使用top_mask,就在mask中記錄一下,該位置輸出的數據在輸入blob中的位置
}
}
}
}
}
}
// compute offset
bottom_data += bottom[0]->offset(0, 1); //輸入數據移動一個channel
top_data += top[0]->offset(0, 1); //輸出數據移動一個channel
if (use_top_mask) {
top_mask += top[0]->offset(0, 1); //如果使用了top_mask,那麼就將top_mask移動一個channel
} else {
mask += top[0]->offset(0, 1); //否則就僅將mask移動一個channel
}
}
}
break; //池化操作處理完畢,直接break
case PoolingParameter_PoolMethod_AVE: //如果是進行平均池化
for (int i = 0; i < top_count; ++i) { //首先把輸出數據全部置0
top_data[i] = 0;
}
// The main loop
for (int n = 0; n < bottom[0]->num(); ++n) { //一個batch中逐圖像處理
for (int c = 0; c < channels_; ++c) { //一張圖像的特徵中逐通道處理
for (int ph = 0; ph < pooled_height_; ++ph) { //對於輸出數據blob,在高方向上逐一進行求值
for (int pw = 0; pw < pooled_width_; ++pw) { //對於輸出數據blob,在寬方向上逐一進行求值
int hstart = ph * stride_h_ - pad_h_; //找到在輸入blob上,該次池化操作在高方向的起始點
int wstart = pw * stride_w_ - pad_w_; //找到在輸入blob上,該次池化操作在寬方向的起始點
int hend = min(hstart + kernel_h_, height_ + pad_h_); //找到在輸入blob上,該次池化操作在高方向的結束點
int wend = min(wstart + kernel_w_, width_ + pad_w_); //找到在輸入blob上,該次池化操作在寬方向的結束點
int pool_size = (hend - hstart) * (wend - wstart); //計算一下在多少個輸入數據上求平均值
hstart = max(hstart, 0); //確認一下池化操作在高上的起始點大於等於0
wstart = max(wstart, 0); //確認一下池化操作在寬上的起始點大於等於0
hend = min(hend, height_); //確認一下在輸入blob上,該次操作在高方向的結束點小於輸出blob的高
wend = min(wend, width_); //確認一下在輸入blob上,該次操作在寬方向的結束點小於輸出blob的寬
for (int h = hstart; h < hend; ++h) {
for (int w = wstart; w < wend; ++w) { //這兩個for循環,遍歷一個輸入blob中的單次池化區域
top_data[ph * pooled_width_ + pw] +=
bottom_data[h * width_ + w]; //將一次池化區域的數據全部加起來
}
}
top_data[ph * pooled_width_ + pw] /= pool_size; //然後,將輸出求平均值
}
}
// compute offset
bottom_data += bottom[0]->offset(0, 1); //輸入數據移動一個channel
top_data += top[0]->offset(0, 1); //輸出數據移動一個channel
}
}
break;
case PoolingParameter_PoolMethod_STOCHASTIC: //如果是進行隨機池化
NOT_IMPLEMENTED; //暫時還沒實現前傳
break;
default:
LOG(FATAL) << "Unknown pooling method.";
}
}
template <typename Dtype>
void PoolingLayer<Dtype>::Backward_cpu(const vector<Blob<Dtype>*>& top,
const vector<bool>& propagate_down, const vector<Blob<Dtype>*>& bottom) { //反傳
if (!propagate_down[0]) { //如果不進行反傳
return; //直接return
}
const Dtype* top_diff = top[0]->cpu_diff(); //top_diff記錄了誤差對於輸出數據的梯度
Dtype* bottom_diff = bottom[0]->mutable_cpu_diff(); //bottom_diff記錄了誤差對與輸入數據的梯度
// Different pooling methods. We explicitly do the switch outside the for
// loop to save time, although this results in more codes.
caffe_set(bottom[0]->count(), Dtype(0), bottom_diff); //將輸入數據的梯度全初始化爲0
// We'll output the mask to top[1] if it's of size >1.
const bool use_top_mask = top.size() > 1; //如果是輸出多個top blob,那麼就是使用了top_mask
const int* mask = NULL; // suppress warnings about uninitialized variables //定義mask指針,爲int型,因爲記錄的是下標
const Dtype* top_mask = NULL; //定義一下top_mask指針
switch (this->layer_param_.pooling_param().pool()) { //判斷池化方式
case PoolingParameter_PoolMethod_MAX: //如果是最大池化
// The main loop
if (use_top_mask) { //使用top_mask
top_mask = top[1]->cpu_data(); //就讀出數據
} else {
mask = max_idx_.cpu_data(); //否則直接讀出mask的數據
}
for (int n = 0; n < top[0]->num(); ++n) { //一個batch中逐圖像處理
for (int c = 0; c < channels_; ++c) { //一張圖像的特徵中逐通道處理
for (int ph = 0; ph < pooled_height_; ++ph) { //在輸出blob的高方向上,逐一求得輸入blob的梯度
for (int pw = 0; pw < pooled_width_; ++pw) { //在輸出blob的寬方向上,逐一求得輸入blob的梯度
const int index = ph * pooled_width_ + pw; //初始化一下輸出的元素在mask中的位置
const int bottom_index =
use_top_mask ? top_mask[index] : mask[index]; //求得該輸出的元素在輸入blob中的位置
bottom_diff[bottom_index] += top_diff[index]; //將該輸入元素梯度的位置進行梯度,直接將輸出元素的梯度拿過來加在對應位置上
}
}
bottom_diff += bottom[0]->offset(0, 1); //輸入數據梯度移動一個channel
top_diff += top[0]->offset(0, 1); //輸出數據梯度移動一個channel
if (use_top_mask) {
top_mask += top[0]->offset(0, 1); //如果使用了top_mask,那麼就將top_mask移動一個channel
} else {
mask += top[0]->offset(0, 1); //否則就將mask移動一個channel
}
}
}
break;
case PoolingParameter_PoolMethod_AVE: //如果是進行平均池化
// The main loop
for (int n = 0; n < top[0]->num(); ++n) { //一個batch中逐圖像處理
for (int c = 0; c < channels_; ++c) { //一張圖像的特徵中逐通道處理
for (int ph = 0; ph < pooled_height_; ++ph) { //在輸出blob的高方向上,逐一求得輸入blob的梯度
for (int pw = 0; pw < pooled_width_; ++pw) { //在輸出blob的寬方向上,逐一求得輸入blob的梯度
int hstart = ph * stride_h_ - pad_h_; //找到在輸入blob上,前傳時進行該次池化操作時在高方向的起始點
int wstart = pw * stride_w_ - pad_w_; //找到在輸入blob上,前傳時進行該次池化操作時在寬方向的起始點
int hend = min(hstart + kernel_h_, height_ + pad_h_); //找到在輸入blob上,前傳時進行該次池化操作時在高方向的結束點
int wend = min(wstart + kernel_w_, width_ + pad_w_); //找到在輸入blob上,前傳時進行該次池化操作時在寬方向的結束點
int pool_size = (hend - hstart) * (wend - wstart); //計算一下前傳時在多少個輸入數據上求平均值
hstart = max(hstart, 0); //確認一下前傳時池化操作在高上的起始點大於等於0
wstart = max(wstart, 0); //確認一下前傳時池化操作在寬上的起始點大於等於0
hend = min(hend, height_); //確認一下在輸入blob上,前傳時該次操作在高方向的結束點小於輸出blob的高
wend = min(wend, width_); //確認一下在輸入blob上,前傳時該次操作在寬方向的結束點小於輸出blob的寬
for (int h = hstart; h < hend; ++h) {
for (int w = wstart; w < wend; ++w) { //在前傳時該次池化操作的區域內
bottom_diff[h * width_ + w] +=
top_diff[ph * pooled_width_ + pw] / pool_size; //輸入數據每個位置上的梯度均分輸出的那一個區域對應梯度
}
}
}
}
// offset
bottom_diff += bottom[0]->offset(0, 1); //輸入數據梯度移動一個channel
top_diff += top[0]->offset(0, 1); //輸出數據梯度移動一個channel
}
}
break;
case PoolingParameter_PoolMethod_STOCHASTIC: //如果是進行隨機池化
NOT_IMPLEMENTED; //暫時還沒實現反傳
break;
default:
LOG(FATAL) << "Unknown pooling method.";
}
}
#ifdef CPU_ONLY
STUB_GPU(PoolingLayer);
#endif
INSTANTIATE_CLASS(PoolingLayer);
} // namespace caffe
池化層源碼解析
在上面的池化層源碼中,筆者解析了代碼前傳與反傳中的具體操作,在caffe中的池化層中,池化操作分爲最大池化與平均池化。在進行最大池化與平均池化的過程中,caffe框架都是按照batch->channel->池化操作的具體區域這個順序來實現的。下面,筆者通過畫圖來闡釋caffe中池化層源碼,尤其是反傳時的操作原理。
最大池化
對於一個訓練批次(batch)中一張圖像對應的特徵的一個通道,假設池化核長寬爲3,長寬方向步長爲3,一次最大池化的前傳操作過程如下圖所示:
如上圖所示,左邊是輸入blob,右邊是輸出blob。在輸入blob中第一個3×3的區域就是池化操作的區域,假設紅色的方塊代表最大值,那麼在前傳的時候,該區域輸出的值就是紅色方塊的值。選擇最大值的過程可在Forward_cpu中最大池化操作的最後兩個for循環找到。
for (int h = hstart; h < hend; ++h) {
for (int w = wstart; w < wend; ++w) { //這兩個for循環,遍歷一個輸入blob中的單次池化區域
const int index = h * width_ + w; //記錄一下選擇的最大值在輸入blob中的位置
if (bottom_data[index] > top_data[pool_index]) {
top_data[pool_index] = bottom_data[index]; //找到一個區域中的最大的元素
if (use_top_mask) {
top_mask[pool_index] = static_cast<Dtype>(index); //如果使用top_mask,就在top_mask中記錄一下,該位置輸出的數據在輸入blob中的位置
} else {
mask[pool_index] = index; //如果不使用top_mask,就在mask中記錄一下,該位置輸出的數據在輸入blob中的位置
}
}
}
}
在前傳的過程中,比較重要的是,在輸入blob中選擇數據的位置被記錄在了mask數組裏面,mask數組這將在反傳過程中起到重要的作用。
相應地,一次最大池化的反傳操作過程如下圖所示:
如上圖所示,==在進行反傳時,對應輸出區域的梯度直接傳給了前傳過程中選擇的最大值區域。那麼是怎麼找到這個區域的呢?答案就是依賴前傳過程中的mask數組啦!==體現在源碼中Backward_cpu函數中的下列代碼中:
bottom_diff[bottom_index] += top_diff[index];
到這裏就解釋清楚了,pooling層中最大池化的反傳是如何實現的。下面,我們來看看平均池化。
平均池化
對於一個訓練批次(batch)中一張圖像對應的特徵的一個通道,假設池化核長寬爲3,長寬方向步長爲3,一次平均池化的前傳操作過程如下圖所示:
在caffe平均池化的前傳中,對於一次池化操作的區域,直接求平均值就能得到輸出,在Forward_cpu函數中平均池化的前傳部分可以很容易地找到求均值的代碼。
top_data[ph * pooled_width_ + pw] /= pool_size;
相應地,在caffe平均池化的反傳中,根據求導原則,一次前傳過程中池化操作區域每個元素的梯度之和,就等於輸出元素梯度。那麼,只需要將輸出元素的梯度除以參與池化操作的輸入數據個數。平均池化的反傳操作如下圖所示:
均分頂層梯度的操作在Backward_cpu函數中的平均池化部分也很容易找到。
bottom_diff[h * width_ + w] +=
top_diff[ph * pooled_width_ + pw] / pool_size;
如上就解析了caffe框架中池化(pooling)層的具體實現過程了,本篇博客也接近尾聲了。筆者衷心希望本篇博文能爲大家在深度學習理論與工程方面帶來幫助。
歡迎閱讀筆者後續博客,各位讀者朋友的支持與鼓勵是我最大的動力。
written by jiong
人生易老天難老,歲歲重陽,
今又重陽,戰地黃花分外香。
一年一度秋風勁,不似春光,
勝似春光,寥廓江天萬里霜。