pooling 是仿照人的視覺系統進行降維(降采樣),用更高層的抽象表示圖像特征,這一部分內容從Hubel&wiesel視覺神經研究到Fukushima提出,再到LeCun的LeNet5首次采用並使用BP進行求解,是一條線上的內容,原始推動力其實就是仿生,仿照真正的神經網絡構建人工網絡。
至於pooling為什么可以這樣做,是因為:我們之所以決定使用卷積后的特征是因為圖像具有一種“靜態性”的屬性,這也就意味着在一個圖像區域有用的特征極有可能在另一個區域同樣適用。因此,為了描述大的圖像,一個很自然的想法就是對不同位置的特征進行聚合統計。這個均值或者最大值就是一種聚合統計的方法。
做窗口滑動卷積的時候,卷積值就代表了整個窗口的特征。因為滑動的窗口間有大量重疊區域,出來的卷積值有冗余,進行最大pooling或者平均pooling就是減少冗余。減少冗余的同時,pooling也丟掉了局部位置信息,所以局部有微小形變,結果也是一樣的。
pooling層通常的作用是:減少空間大小,減少網絡參數,防止過擬合。
pooling 種類
最常見的池化操作為最大池化和平均池化:
最大池化 Max Pooling
前向傳播:選圖像區域的最大值作為該區域池化后的值。
反向傳播:梯度通過最大值的位置傳播,其它位置梯度為0。
平均池化 Average Pooling(也稱mean pooling)
前向傳播:計算圖像區域的平均值作為該區域池化后的值。
反向傳播:梯度取均值后分給每個位置。
對於Average Pooling的輸入\(X=x_1,x_2,...x_n\),輸出\(\displaystyle f(X) = \frac{1}{n} \sum_{i=1}^n x_i\)
Stochastic Pooling
論文Stochastic Pooling for Regularization of Deep Convolutional Neural Networks提出了一種簡單有效的正則化CNN的方法,能夠降低max pooling的過擬合現象,提高泛化能力。對於pooling層的輸入,根據輸入的多項式分布隨機選擇一個值作為輸出。訓練階段和測試階段的操作略有不同。
訓練階段
- 前向傳播
(1)歸一化pooling的輸入,作為每個激活神經元的分布概率值\(p_i={a_i\over\sum_{k\in R_j}a_k}\).
(2)從基於\(p\)的多項式分布中隨機采樣一個位置的值作為輸出。 - 反向傳播
跟max pooling類似,梯度通過被選擇的位置傳播,其它位置為0.
測試階段
如果在測試時也使用隨機pooling會對預測值引入噪音,降低性能。取而代之的是使用按歸一化的概率值加權平均。比使用average pooling表現要好一些。因此在平均意義上,與average pooling近似,在局部意義上,則服從max pooling的准則。
解釋分析
按概率加權的方式可以被看作是一種模型平均融合的方式,在pooling區域不同選擇方式對應一個新模型。訓練階段由於引入隨機性,所以會改變網絡的連接結構,導致產生新的模型。在測試階段會同時使用這些模型,做加權平均。假設網絡有d層pooling層,pooling核大小是n,那么可能的模型有\(n^d\)個。這比dropout增加的模型多樣性要多(dropout率為0.5時相當於n=2)。
在CIFAR-10上三種pooling方法的錯誤率對比:
pooling 選擇與實際應用
通常我們使用Max Pooling,因為使用它能學到圖像的邊緣和紋理結構。而Average Pooling則不能。Max Pooling通常用以減小估計值方差,在方差不太重要的地方可以隨意選擇Max Pooling和Average Pooling。Average Pooling用以減小估計均值的偏移。在某些情況下Average Pooling可能取得比Max Pooling稍好一些的效果。
average pooling會弱化強激活值,而max pooling保留最強的激活值卻容易過擬合。
雖然從理論上說Stochastic Pooling也許能取得較好的結果,但是需要在實踐中多次嘗試,隨意使用可能效果變差。因此並不是一個常規的選擇。
按池化是否作用於圖像中不重合的區域(這與卷積操作不同)分為一般池化(Gerneral Pooling)與重疊池化(OverlappingPooling)。
常見設置是filter大小F=2,步長S=2或F=3,S=2(overlapping pooling,重疊);pooling層通常不需要填充。
代碼實現
caffe cpu版pooling層實現代碼pooling_layer.cpp:
template <typename Dtype>
void PoolingLayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top) {
...
switch (this->layer_param_.pooling_param().pool()) {
case PoolingParameter_PoolMethod_MAX:
const int pool_index = ph * pooled_width_ + pw;
for (int h = hstart; h < hend; ++h) {
for (int w = wstart; w < wend; ++w) {
const int index = h * width_ + w;
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);
} else {
mask[pool_index] = index;
}
}
}
}
case PoolingParameter_PoolMethod_AVE:
...
for (int i = 0; i < top_count; ++i) {
top_data[i] = 0;
}
for (int h = hstart; h < hend; ++h) {
for (int w = wstart; w < wend; ++w) {
top_data[ph * pooled_width_ + pw] +=
bottom_data[h * width_ + w];
}
}
top_data[ph * pooled_width_ + pw] /= pool_size;
...
case PoolingParameter_PoolMethod_STOCHASTIC:
NOT_IMPLEMENTED;
}
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;
}
switch (this->layer_param_.pooling_param().pool()) {
case PoolingParameter_PoolMethod_MAX:
// The main loop
if (use_top_mask) {
top_mask = top[1]->cpu_data();
} else {
mask = max_idx_.cpu_data();
}
for (int n = 0; n < top[0]->num(); ++n) {
for (int c = 0; c < channels_; ++c) {
for (int ph = 0; ph < pooled_height_; ++ph) {
for (int pw = 0; pw < pooled_width_; ++pw) {
const int index = ph * pooled_width_ + pw;
const int bottom_index =
use_top_mask ? top_mask[index] : mask[index];
bottom_diff[bottom_index] += top_diff[index];
}
}
bottom_diff += bottom[0]->offset(0, 1);
top_diff += top[0]->offset(0, 1);
if (use_top_mask) {
top_mask += top[0]->offset(0, 1);
} else {
mask += top[0]->offset(0, 1);
}
}
}
break;
case PoolingParameter_PoolMethod_AVE:
// The main loop
for (int n = 0; n < top[0]->num(); ++n) {
for (int c = 0; c < channels_; ++c) {
for (int ph = 0; ph < pooled_height_; ++ph) {
for (int pw = 0; pw < pooled_width_; ++pw) {
int hstart = ph * stride_h_ - pad_h_;
int wstart = pw * stride_w_ - pad_w_;
int hend = min(hstart + kernel_h_, height_ + pad_h_);
int wend = min(wstart + kernel_w_, width_ + pad_w_);
int pool_size = (hend - hstart) * (wend - wstart);
hstart = max(hstart, 0);
wstart = max(wstart, 0);
hend = min(hend, height_);
wend = min(wend, width_);
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);
top_diff += top[0]->offset(0, 1);
}
}
break;
case PoolingParameter_PoolMethod_STOCHASTIC:
NOT_IMPLEMENTED;
break;
...
}
Stochastic Pooling的前向傳播過程示例theano代碼:stochastic_pool.py
caffe中的Stochastic Pooling實現:
只為GPU做了代碼實現,並需要與 CAFFE engine一塊使用,需要在pooling_param 里邊設置pool類型:STOCHASTIC ,在pooling_param 中設置engine: CAFFE
(如果使用GPU運行,默認引擎是cuDNN).
Stochastic Pooling實現代碼pooling_layer.cu:
void StoPoolForwardTrain(..,Dtype* const rand_idx,..) {
/*
rand_idx是隨機選的pooling核上的位置比例,目前實現方式是使用如下的均勻分布產生函數生成:
caffe_gpu_rng_uniform(count, Dtype(0), Dtype(1),
rand_idx_.mutable_gpu_data());
*/
...
Dtype cumsum = 0.;
const Dtype* const bottom_slice =
bottom_data + (n * channels + c) * height * width;
// First pass: get sum
for (int h = hstart; h < hend; ++h) {
for (int w = wstart; w < wend; ++w) {
cumsum += bottom_slice[h * width + w];
}
}
const float thres = rand_idx[index] * cumsum;
// Second pass: get value, and set index.
cumsum = 0;
for (int h = hstart; h < hend; ++h) {
for (int w = wstart; w < wend; ++w) {
cumsum += bottom_slice[h * width + w];
if (cumsum >= thres) {// 輪盤賭,均勻分布
rand_idx[index] = ((n * channels + c) * height + h) * width + w;
top_data[index] = bottom_slice[h * width + w];
return;
}
}
}
...
}
void StoPoolForwardTest(...){
...
Dtype cumsum = 0.;
Dtype cumvalues = 0.;
const Dtype* const bottom_slice =
bottom_data + (n * channels + c) * height * width;
// First pass: get sum
for (int h = hstart; h < hend; ++h) {
for (int w = wstart; w < wend; ++w) {
cumsum += bottom_slice[h * width + w];// 求和
cumvalues += bottom_slice[h * width + w] * bottom_slice[h * width + w];// 求平方和
}
}
top_data[index] = (cumsum > 0.) ? cumvalues / cumsum : 0.;
...
}
進一步閱讀
LeCun的“Learning Mid-Level Features For Recognition”對前兩種pooling方法有比較詳細的分析對比。