CNN网络中池化层的正向传播与反向传播理解

本文详细介绍了卷积神经网络(CNN)中池化层的概念,包括Max-Pooling和Average-Pooling两种常见类型,并通过实例解释了正向传播的过程。此外,还阐述了池化层在反向传播时的特点,针对Max-Pooling和Average-Pooling的反向传播进行了说明,并引用了Caffe框架中的相关代码实现。

1. 池化定义

通常来说卷积之后的图像虽然在尺寸上有所减小,但是其尺寸还是较大,为了描述大的图像,一个很自然的想法就是对不同位置的特征进行聚合统计,例如,人们可以计算图像一个区域上的某个特定特征的平均值 (或最大值)。这些概要统计特征不仅具有低得多的维度 (相比使用所有提取得到的特征),同时还会改善结果(不容易过拟合)。这种聚合的操作就叫做池化 (pooling),有时也称为平均池化或者最大池化 (取决于计算池化的方法)。
池化的不变性:如果人们选择图像中的连续范围作为池化区域,并且只是池化相同(重复)的隐藏单元产生的特征,那么,这些池化单元就具有平移不变性 (translation invariant)。这就意味着即使图像经历了一个小的平移之后,依然会产生相同的 (池化的) 特征。使得网络具有了一定的鲁棒性。
下面一幅图就是对池化层的形象描述。
这里写图片描述
池化层之后的矩阵数据一般会大小减半,但是其减半的方式也主要有两种Max-Pooling和Average-Pooling。

2. 池化层正向传播

2.1 Max-Pooling

该种类型的池化层是取运算窗口中的最大值作为最后运算的结果,可以表示为:
这里写图片描述
上图中就是以 32 3 ∗ 2 大小的窗口进行运算得到的结果

2.2 Average-Pooling

该种类型的池化层是取运算窗口中的所有值的均值作为最后运算的结果,可以表示为:
这里写图片描述

2.3 Caffe代码

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();
  Dtype* top_data = top[0]->mutable_cpu_data();
  const int top_count = top[0]->count();
  // We'll output the mask to top[1] if it's of size >1.
  const bool use_top_mask = top.size() > 1;
  int* mask = NULL;  // suppress warnings about uninitalized variables
  Dtype* top_mask = NULL;
  // 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: //Max-Pooling
    // Initialize
    if (use_top_mask) {
      top_mask = top[1]->mutable_cpu_data();
      caffe_set(top_count, Dtype(-1), top_mask);
    } else {
      mask = max_idx_.mutable_cpu_data();
      caffe_set(top_count, -1, mask);
    }
    caffe_set(top_count, Dtype(-FLT_MAX), top_data);
    // The main loop
    for (int n = 0; n < bottom[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_);
            int wend = min(wstart + kernel_w_, width_);
            hstart = max(hstart, 0);
            wstart = max(wstart, 0);
            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;
                  }
                }
              }
            }
          }
        }
        // compute offset
        bottom_data += bottom[0]->offset(0, 1);
        top_data += 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: // Average-Pooling
    for (int i = 0; i < top_count; ++i) {
      top_data[i] = 0;
    }
    // The main loop
    for (int n = 0; n < bottom[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) {
                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);
        top_data += top[0]->offset(0, 1);
      }
    }
    break;
  case PoolingParameter_PoolMethod_STOCHASTIC:
    NOT_IMPLEMENTED;
    break;
  default:
    LOG(FATAL) << "Unknown pooling method.";
  }
}

3. 池化成反向传播

池化层一般没有参数,所以反向传播的时候,只需对输入参数求导,不需要进行权值更新。但是具体在计算的时候是要根据Max还是Average来进行区分,进行参数更新的。

3.1 Max-Pooling

如果只看输出矩阵中的一个点y,则有 y = max( x1 , x2, x3, … )。所以对x求导后有(可以理解成分段函数的求导),则有
这里写图片描述
也就是寻找顶层与下层之间的映射关系,之后将上层的结果添加到下层中去。

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];
    }
}

这个xn如果影响多个y,则会叠加起来。具体来说可以使用下面这幅图片进行说明
这里写图片描述

3.2 Average-Pooling

如果只看输出矩阵中的一个点y,则有 y = ( x1 , x2, x3, … ,xn )/n。所以对x求导后有
这里写图片描述
也就是映射过去之后去均值

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;
    }
}

具体来说可以使用下面这幅图片进行说明
这里写图片描述

3.3 Caffe中的完整代码实现

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;
  }
  const Dtype* top_diff = top[0]->cpu_diff();
  Dtype* bottom_diff = bottom[0]->mutable_cpu_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);
  // We'll output the mask to top[1] if it's of size >1.
  const bool use_top_mask = top.size() > 1;
  const int* mask = NULL;  // suppress warnings about uninitialized variables
  const Dtype* top_mask = NULL;
  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;
  default:
    LOG(FATAL) << "Unknown pooling method.";
  }
}

4. 参考资料

  1. 池化
  2. caffe源码 池化层 反向传播
  3. 深度学习笔记5:池化层的实现
<think>我们正在讨论卷积神经网络(CNN)的正向传播反向传播过程。根据引用内容,我们可以逐步构建解释。 首先,CNN正向传播包括多个层次:卷积层、非线性激活函数(如ReLU)、池化层(如最池化或平均池化)、全连接层以及最后的Softmax分类层。反向传播则是通过链式法则计算梯度并更新参数。 ### 正向传播 1. **卷积层(Convolution)**: - 输入: 图像或上一层的特征图,尺寸为$H_{in} \times W_{in} \times C_{in}$(高度、宽度、通道数)。 - 卷积操作: 使用多个卷积核(滤波器)进行局部连接操作。每个卷积核在输入上滑动,计算局部区域的点积并加上偏置。 - 输出: 特征图,尺寸为$H_{out} \times W_{out} \times C_{out}$,其中$C_{out}$是卷积核的数量。 - 计算公式: $$Z^{[l]} = A^{[l-1]} * W^{[l]} + b^{[l]}$$ 其中$*$表示卷积操作,$A^{[l-1]}$是上一层的激活值,$W^{[l]}$和$b^{[l]}$是当前层的权重和偏置。 2. **非线性激活(ReLU)**: - 应用ReLU函数: $A^{[l]} = \max(0, Z^{[l]})$,增加非线性。 3. **池化层(Pooling)**: - 目的: 降维,减少计算量,增强特征不变性。 - 最池化: 取局部区域的最值。 - 平均池化: 取局部区域的平均值。 - 输出尺寸会按池化窗口小和步长缩小。 4. **全连接层(Fully-Connected)**: - 将池化后的特征图展平成一维向量,然后通过全连接层。 - 计算公式: $Z^{[l]} = W^{[l]} A^{[l-1]} + b^{[l]}$,再经过激活函数。 5. **Softmax交叉熵损失**: - 最后一层通常用Softmax函数将输出转换为概率分布: $$\hat{y}_i = \frac{e^{z_i}}{\sum_{j} e^{z_j}}$$ - 交叉熵损失函数: $$\mathcal{L}(\hat{y}, y) = -\sum_{i} y_i \log(\hat{y}_i)$$ ### 反向传播 反向传播的目标是计算损失函数对每一层参数的梯度,以便用梯度下降法更新参数。核心是链式求导。 1. **Softmax层梯度**: - 损失对Softmax输入的梯度: $$\frac{\partial \mathcal{L}}{\partial z_i} = \hat{y}_i - y_i$$ 2. **全连接层梯度**: - 假设全连接层后接Softmax,则其输入$Z^{[l]}$的梯度$\delta^{[l]}$由上一层梯度反向传播得到。 - 权重梯度: $$\frac{\partial \mathcal{L}}{\partial W^{[l]}} = (\delta^{[l]})^T A^{[l-1]}$$ - 偏置梯度: $$\frac{\partial \mathcal{L}}{\partial b^{[l]}} = \delta^{[l]}$$ 3. **卷积层梯度**: - 引用[4]中的代码展示了卷积层反向传播的实现。设当前层输出梯度为$dZ^{[l]}$(即下一层传回的梯度),则: - 输入梯度$dA^{[l-1]}$: 通过将$dZ^{[l]}$卷积核$W^{[l]}$进行全卷积(并填充)得到。 - 权重梯度$dW^{[l]}$: 通过将输入$A^{[l-1]}$$dZ^{[l]}$进行局部卷积得到。 - 偏置梯度$db^{[l]}$: 对$dZ^{[l]}$的每个通道求和。 - 具体公式参考代码注释。 4. **池化层梯度**: - **最池化**: 梯度回传到前一层时,只传递给前向传播时最值所在的位置,其他位置为0。 - **平均池化**: 梯度均匀分布到前一层对应窗口的所有位置。 ### 实现细节 - **卷积层反向传播**(引用[4]代码): - 输入: `dz`(当前层输出梯度),`cache`(存储前向传播的输入、权重、偏置和超参数)。 - 输出: `da_input`(输入梯度),`dw`(权重梯度),`db`(偏置梯度)。 - 过程: - 遍历每个样本、每个输出位置(高度和宽度)、每个输出通道。 - 定位到输入图像的局部区域(slice)。 - 更新输入梯度:`da_temp`在对应slice位置加上$w \times dz$。 - 更新权重梯度:`dw`在对应通道加上输入slice乘以当前`dz`。 - 更新偏置梯度:`db`在对应通道加上当前`dz`。 ### 总结 CNN通过卷积层提取空间特征,池化层降低维度,全连接层进行分类。反向传播利用链式法则逐层计算梯度,更新参数。具体实现中,卷积层的反向传播需要仔细处理局部连接和参数共享的特性。 §§相关问题§§ 1. 卷积神经网络中卷积层反向传播的具体数学推导是什么? 2. 池化层反向传播中如何处理梯度? 3. 如何优化卷积神经网络的训练过程? 4. CNN中常用的正则化方法有哪些? 5. 卷积神经网络在图像识别任务中为什么比全连接网络更有效?</think>### 卷积神经网络CNN)的正向传播反向传播详解 #### **一、正向传播过程** 正向传播通过多层结构提取输入数据的特征,最终输出预测结果。流程如下: 1. **卷积层(Convolution)** - **输入**:图像或特征图(尺寸 $H \times W \times C$) - **操作**: - 卷积核(尺寸 $F \times F \times C$)在输入上滑动,计算局部区域的点积: $$Z_{x,y}^{[l]} = \sum_{i=1}^F \sum_{j=1}^F \sum_{k=1}^C W_{i,j,k}^{[l]} \cdot A_{x+i-1,y+j-1,k}^{[l-1]} + b^{[l]}$$ - 输出特征图尺寸由步长(`stride`)和填充(`pad`)决定: $$H_{\text{out}} = \left\lfloor \frac{H_{\text{in}} - F + 2\text{pad}}{\text{stride}} \right\rfloor + 1$$ - **输出**:特征图(尺寸 $H_{\text{out}} \times W_{\text{out}} \times N$,$N$ 为卷积核数量)[^2][^3]。 2. **非线性激活(ReLU)** - 引入非线性:$A^{[l]} = \max(0, Z^{[l]})$,保留正值,抑制负值[^2]。 3. **池化层(Pooling)** - **最池化**:取局部区域最值,保留显著特征。 - **平均池化**:取局部区域平均值,平滑特征。 - 输出尺寸缩减,例如 $2\times2$ 池化使尺寸减半[^3][^5]。 4. **全连接层(Fully-Connected)** - 将池化后的特征展平为一维向量,通过权重矩阵计算: $$Z^{[l]} = W^{[l]} A^{[l-1]} + b^{[l]}$$ - 输出送入 Softmax 层分类[^2]。 5. **Softmax 损失计算** - Softmax 输出概率分布:$\hat{y}_i = \frac{e^{z_i}}{\sum_j e^{z_j}}$ - 交叉熵损失:$\mathcal{L} = -\sum_i y_i \log(\hat{y}_i)$[^2]。 --- #### **二、反向传播过程** 反向传播通过链式法则计算梯度,从输出层向输入层逐层更新参数: 1. **Softmax 全连接层梯度** - Softmax 输入梯度:$\frac{\partial \mathcal{L}}{\partial z_i} = \hat{y}_i - y_i$ - 全连接层参数梯度: $$\frac{\partial \mathcal{L}}{\partial W^{[l]}} = (\delta^{[l]})^T A^{[l-1]}, \quad \frac{\partial \mathcal{L}}{\partial b^{[l]}} = \delta^{[l]}$$ 其中 $\delta^{[l]} = \frac{\partial \mathcal{L}}{\partial Z^{[l]}}$ 为上层回传梯度[^2][^4]。 2. **池化层梯度** - **最池化**:梯度仅回传到前向传播中最值的位置,其他位置为 0。 - **平均池化**:梯度均匀分配到前向传播的局部区域所有位置[^5]。 3. **卷积层梯度** 设当前层输出梯度为 $dZ$,输入梯度 $dA_{\text{in}}$、卷积核梯度 $dW$、偏置梯度 $db$ 计算如下: ```python def conv_backward(dZ, cache): A_in, W, b, hparams = cache # 前向传播缓存 stride, pad = hparams['stride'], hparams['pad'] dA_in = np.zeros_like(A_in) dW = np.zeros_like(W) db = np.zeros_like(b) for i in range(m): # 遍历样本 for h in range(H_out): # 遍历输出高度 for w in range(W_out): # 遍历输出宽度 for c in range(C): # 遍历输出通道 # 定位输入局部区域 v_start = h * stride v_end = v_start + F h_start = w * stride h_end = h_start + F a_slice = A_in[i, v_start:v_end, h_start:h_end, :] # 计算梯度 dA_in[i, v_start:v_end, h_start:h_end, :] += W[c,:,:,:] * dZ[i, h, w, c] dW[c,:,:,:] += a_slice * dZ[i, h, w, c] db[c] += dZ[i, h, w, c] return dA_in, dW, db ``` - **数学原理**: - $dA_{\text{in}}$:通过 $dZ$ 卷积核 $W$ 的**转置卷积**得到。 - $dW$:$dZ$ 输入局部区域 $A_{\text{in}}$ 的卷积结果。 - $db$:$dZ$ 的通道求和[^4][^5]。 --- #### **三、关键设计原理** 1. **局部感知** 卷积核仅连接输入局部区域,减少参数量并捕捉空间局部特征(如图像边缘、纹理)[^3]。 2. **参数共享** 同一卷积核在输入不同位置复用,提升计算效率并增强平移不变性[^3]。 3. **反向传播核心** 通过链式求导从损失函数反向计算各层梯度,结合梯度下降更新参数: $$W_{\text{new}} = W_{\text{old}} - \alpha \frac{\partial \mathcal{L}}{\partial W}$$ 其中 $\alpha$ 为学习率[^1][^4]。 --- #### **四、应用场景** - **图像识别**:物体分类(ImageNet)、人脸检测。 - **医学影像**:肿瘤分割、X光分析。 - **自然语言处理**:文本分类(卷积核捕捉局部词序列特征)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值