写在前面
在上一篇博客,即重启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
人生易老天难老,岁岁重阳,
今又重阳,战地黄花分外香。
一年一度秋风劲,不似春光,
胜似春光,寥廓江天万里霜。