NNDL 作业7:第五章课后题(1×1 卷积核 | CNN BP)

目录

习题5-2 证明宽卷积具有交换性,即公式5.13

习题5-3 分析卷积神经网络中用1×1的卷积核的作用

习题5-4 对于一个输入为100×100×256的特征映射组,使用3×3的卷积核,输出为100×100×256的特征映射组的卷积层,求其时间和空间复杂度。如果引入一个1×1卷积核,先得到100×100×64的特征映射,再进行3×3的卷积,得到100×100×256的特征映射组,求其时间和空间复杂度

习题5-7 忽略激活函数,分析卷积网络中卷积层的前向计算和反向传播(公式5.39)是一种转置关系

选做1 推导CNN反向传播算法

选做2 设计简易CNN模型,分别用Numpy、Pytorch实现卷积层和池化层的反向传播算子,并打入数值测试


习题5-2 证明宽卷积具有交换性,即公式5.13

   rot180(W)\widetilde{\otimes }X

=rot180(W)\otimes \widetilde{X}

=\widetilde{X}\otimes rot180(W)

=X\widetilde{\otimes }rot180(W)

=rot180(X)\widetilde{\otimes }W

rot180(W)\widetilde{\otimes }X=rot180(X)\widetilde{\otimes }W     

rot180(\cdot )表示宽卷积运算

宽卷积定义y_{ij}=\sum_{n=1-(m-1)}^{m}\sum_{v=1-(n-1)}^{n}w_{uv}\cdot x_{i+u-1,j+v-1}

y_{ij}=\sum_{u=1}^{m}\sum_{v=1}^{n}w_{uv}\cdot x_{i+u-1,j+v-1}

为了让x的下标形式和w的进行对换,进行变量替换,

令s=i-u+1,t=j-v+1 所以u=s-i+1,v=t-j+1

y_{ij}=\sum_{s=i+1-m}^{i-1+m}\sum_{t=j+1-n}^{j-1+n}w_{st}\cdot x_{s-i+1,t-j+1}

i\in \left [ 1,M \right ]J,J\in \left [ 1,N \right ]

所以对于y_{ij}=\sum_{s=i+1-m}^{i-1+m}\sum_{t=j+1-n}^{j-1+n}w_{st}\cdot x_{s-i+1,t-j+1}由于宽卷积的条件,s,t的变动范围可行

习题5-3 分析卷积神经网络中用1×1的卷积核的作用

1、增加网络的深度,添加非线性

其一:1x1 的卷积核虽小,但也是卷积核,加 1 层卷积,网络深度自然会增加。其实问题往下挖掘,应该是增加网络深度有什么好处?为什么非要用 1x1 来增加深度呢?其它的不可以吗?其实,这涉及到感受野的问题,我们知道卷积核越大,它生成的 featuremap 上单个节点的感受野就越大,随着网络深度的增加,越靠后的 featuremap 上的节点感受野也越大。因此特征也越来越形象,也就是更能看清这个特征是个什么东西。层数越浅,就越不知道这个提取的特征到底是个什么东西。
其二:有的时候,在不增加感受野的情况下,让网络加深,为的就是引入更多的非线性。而 1x1 卷积核,恰巧可以办到。卷积后生成图片的尺寸受卷积核的大小和卷积核个数影响,但如果卷积核是 1x1 ,个数也是 1,那么生成后的图像长宽不变,厚度为1。但通常一个卷积层是包含激活和池化的。也就是多了激活函数,比如 Sigmoid 和 Relu。所以,在输入不发生尺寸的变化下,加入卷积层的同时引入了更多的非线性,这将增强神经网络的表达能力。


2、升维或者降维

卷积过程中:卷积后的的 featuremap 通道数是与卷积核的个数相同的

所以,如果输入图片通道是 3,卷积核的数量是 6 ,那么生成的 feature map 通道就是 6,这就是升维,如果卷积核的数量是 1,那么生成的 feature map 只有 1 个通道,这就是降维度。

值得注意的是,所有尺寸的卷积核都可以达到这样的目的。

为什么是1×1?
原因就是数据量的大小,我们知道在训练的时候,卷积核里面的值就是要训练的权重,3x3 的尺寸是 1x1 所需要内存的 9 倍,其它的类似。所以,有时根据实际情况只想单纯的去提升或者降低 feature map 的通道,1x1 无疑是一个值得考虑的选项。

习题5-4 对于一个输入为100×100×256的特征映射组,使用3×3的卷积核,输出为100×100×256的特征映射组的卷积层,求其时间和空间复杂度。如果引入一个1×1卷积核,先得到100×100×64的特征映射,再进行3×3的卷积,得到100×100×256的特征映射组,求其时间和空间复杂度

M=100;K=3;C_{in}=256;C_{out}=256

时间复杂度:256×100×100×256×3×3=5,898,240,000

空间复杂度:256×100×100=2,560,000

M=100;K_{1}=1;K_{2}=3;C_{in1}=256;C_{out1}=64;C_{in2}=64;C_{out2}=256;

时间复杂度:64×100×100×256+256×100×100×64×3×3=1,638,400,000

空间复杂度:64×100×100+256×100×100=3,200,000

习题5-7 忽略激活函数,分析卷积网络中卷积层的前向计算和反向传播(公式5.39)是一种转置关系

以一个3×3的卷积核为例,输入为X输出为Y

X=\begin{pmatrix} x_{1} &x_{2} &x_{3} &x_{4} \\ x_{5} & x_{6} &x_{7} &x_{8} \\ x_{9}& x_{10}&x_{11} & x_{12}\\ x_{13}&x_{14} & x_{15} & x_{16} \end{pmatrix},W=\begin{pmatrix} w_{00} & w_{01} &w_{02} \\ w_{10} &w_{11} &w_{12} \\ w_{20} &w_{21} &w_{22} \end{pmatrix},Y=\begin{pmatrix} y_{1} & y_{2}\\ y_{3} & y_{4} \end{pmatrix}

 将4×4的输入特征展开为16×1的矩阵,y展开为4×1的矩阵,将卷积计算转化为矩阵相乘
Y_{4\times 1}=C_{4\times 16}\times X_{16\times 1}

Y=\begin{bmatrix} y_{1}\\ y_{2}\\ y_{3}\\ y_{4} \end{bmatrix},C=\begin{bmatrix} w_{20} &w_{01} &w_{02} &0 & w_{10} &w_{11} & w_{12} &... \\ 0 &w_{20} &w_{01} &w_{02} & 0 & w_{10} &w_{11}&... \\ 0& 0 &w_{20} &w_{01} &w_{02} &0 &w_{10} &... \\ 0& 0& 0 &w_{20} &w_{01} &w_{02} & 0 & ... \end{bmatrix},X=\begin{bmatrix} x_{1}\\ x_{2}\\ \vdots \\ x_{4} \end{bmatrix}

因为\frac{\partial loss}{\partial x_{j}}=\sum_{i}^{4}\frac{\partial loss}{\partial y_{i}}\cdot \frac{\partial y_{i}}{\partial x_{j}}    ,所以    y_{i}=\sum_{i=1}^{16}c_{ij}x_{j}  即   \frac{\partial y_{i}}{\partial x_{j}}=c_{ij}

所以\frac{\partial loss}{\partial x}=\begin{bmatrix} \frac{\partial loss}{\partial x_{1}}\\ \frac{\partial loss}{\partial x_{2}}\\ \vdots \\ \frac{\partial loss}{\partial x^{_{16}}} \end{bmatrix}=\begin{bmatrix} c_{1}^{T}\\ c_{2}^{T}\\ \vdots \\ c_{16}^{T} \end{bmatrix}\frac{\partial loss}{\partial Y}=C^{T}\frac{\partial loss}{\partial Y}

从Y=CX中可以发现忽略激活函数时卷积网络中卷积层的前向计算和反向传播是一种转置关系。 

选做1 推导CNN反向传播算法

池化层反向传播

池化层的反向传播比较容易理解,我们以最大池化举例,上图中,池化后的数字6对应于池化前的红色区域,实际上只有红色区域中最大值数字6对池化后的结果有影响,权重为1,而其它的数字对池化后的结果影响都为0。假设池化后数字6的位置delta误差为 δ ,误差反向传播回去时,红色区域中最大值对应的位置delta误差即等于 δ ,而其它3个位置对应的delta误差为0。

因此,在卷积神经网络最大池化前向传播时,不仅要记录区域的最大值,同时也要记录下来区域最大值的位置,方便delta误差的反向传播。

而平均池化就更简单了,由于平均池化时,区域中每个值对池化后结果贡献的权重都为区域大小的倒数,所以delta误差反向传播回来时,在区域每个位置的delta误差都为池化后delta误差除以区域的大小。

卷积层反向传播

虽然卷积神经网络的卷积运算是一个三维张量的图片和一个四维张量的卷积核进行卷积运算,但最核心的计算只涉及二维卷积,因此我们先从二维的卷积运算来进行分析:

我们现在将原图A点位置移动一下,再看看变换位置后A点的delta误差是多少,同样先分析它前向传播影响了卷积结果的哪些结点。经过分析,A点以权重C影响了卷积结果的D点,以权重B影响了卷积结果的E点。那它的delta误差就等于D点delta误差乘上C加上E点的delta误差乘上B。

大家可以尝试用相同的方法去分析原图中其它结点的delta误差,结果会发现,原图的delta误差,等于卷积结果的delta误差经过零填充后,与卷积核旋转180度后的卷积。如下图所示:

 

 接下来用数学公式来对此进行证明

让我们回顾一下delta误差的定义,是损失函数对于当前层未激活输出 z^{l}的导数,我们现在考虑的是二维卷积,因此,每一层的delta误差是一个二维的矩阵。 \delta ^{l}(x,y)表示的是第l层坐标为(x,y)处的delta误差。假设我们已经知道第l+1层的delta误差,利用求导的链式法则,可以很容易写出下式:

 在这里,坐标(x',y')是第l+1层中在前向传播中受第l层坐标(x,y)影响到的点,它们不止一个,我们需要将它们加起来。再利用前向传播的关系式:

我们可以进一步将表达式展开:

 

后面一大串尽管看起来很复杂,但实际上很容易就可以简化:

 

 同时我们得到两个限制条件 x′+a=x 和 y′+b=y

将限制条件代入上式可得:

 我们可以短暂的庆祝一下子了,然而我们目前的结论还只是基于二维卷积,我们还需要把它推广到我们卷积神经网络中张量的卷积中去。

再回顾一下张量的卷积,后一层的每个通道都是由前一层的各个通道经过卷积再求和得到的。

等等,这个关系听起来好像有点熟悉,如果把通道变成结点,把卷积变成乘上权重,这个是不是和全连接神经网络有些类似呢?

上图中每根连线都代表与一个二维卷积核的卷积操作,假设第l层深度为3,第l+1层深度为2,卷积核的维度就应该为2×filter_size×filter_size×3。第l层的通道1通过卷积影响了第l+1层的通道1和通道2,那么求第l层通道1的delta误差时,就应该根据求得的二维卷积的delta误差传播方式,将第l+1层通道1和通道2的delta误差传播到第l层的delta误差进行简单求和即可。

第l层卷积核 w^{l}是一个4维张量,它的维度表示为卷积核个数×行数×列数×通道数。实际上,可以把它视为有卷积核个数×通道数个二维卷积核,每个都对应输入图像的对应通道和输出图像的对应通道,每一个二维卷积核只涉及到一次二维卷积运算。那求得整个卷积核的导数,只需分析卷积核数×通道数次二维卷积中每个二维卷积核的导数,再将其组合成4维张量即可。

所以我们分析二维卷积即可:

 

 可以利用之前的分析方法,卷积核上点A显然对卷积结果每一个点都有影响。它对卷积结果的影响等于将整个原图左上3×3的部分乘上点A的值,因此delta误差反向传播回时,点A的导数等于卷积结果的delta误差与原图左上3×3红色部分逐点相乘后求和。因此二维卷积核的导数等于原图对应通道与卷积结果对应通道的delta误差直接进行卷积。

 

选做2 设计简易CNN模型,分别用Numpy、Pytorch实现卷积层和池化层的反向传播算子,并打入数值测试

卷积层的反向传播实现:

from typing import Dict, Tuple
import numpy as np
import pytest
import torch
 
def conv2d_forward(input: np.ndarray, weight: np.ndarray, bias: np.ndarray,
                   stride: int, padding: int) -> Dict[str, np.ndarray]:
    """2D Convolution Forward Implemented with NumPy
    Args:
        input (np.ndarray): The input NumPy array of shape (H, W, C).
        weight (np.ndarray): The weight NumPy array of shape
            (C', F, F, C).
        bias (np.ndarray | None): The bias NumPy array of shape (C').
            Default: None.
        stride (int): Stride for convolution.
        padding (int): The count of zeros to pad on both sides.
    Outputs:
        Dict[str, np.ndarray]: Cached data for backward prop.
    """
    h_i, w_i, c_i = input.shape
    c_o, f, f_2, c_k = weight.shape
 
    assert (f == f_2)
    assert (c_i == c_k)
    assert (bias.shape[0] == c_o)
    input_pad = np.pad(input, [(padding, padding), (padding, padding), (0, 0)])
 
    def cal_new_sidelngth(sl, s, f, p):
        return (sl + 2 * p - f) // s + 1
 
    h_o = cal_new_sidelngth(h_i, stride, f, padding)
    w_o = cal_new_sidelngth(w_i, stride, f, padding)
    output = np.empty((h_o, w_o, c_o), dtype=input.dtype)
 
    for i_h in range(h_o):
        for i_w in range(w_o):
            for i_c in range(c_o):
                h_lower = i_h * stride
                h_upper = i_h * stride + f
                w_lower = i_w * stride
                w_upper = i_w * stride + f
                input_slice = input_pad[h_lower:h_upper, w_lower:w_upper, :]
                kernel_slice = weight[i_c]
                output[i_h, i_w, i_c] = np.sum(input_slice * kernel_slice)
                output[i_h, i_w, i_c] += bias[i_c]
 
    cache = dict()
    cache['Z'] = output
    cache['W'] = weight
    cache['b'] = bias
    cache['A_prev'] = input
    return cache
 
def conv2d_backward(dZ: np.ndarray, cache: Dict[str, np.ndarray], stride: int,
                    padding: int) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
    """2D Convolution Backward Implemented with NumPy
    Args:
        dZ: (np.ndarray): The derivative of the output of conv.
        cache (Dict[str, np.ndarray]): Record output 'Z', weight 'W', bias 'b'
            and input 'A_prev' of forward function.
        stride (int): Stride for convolution.
        padding (int): The count of zeros to pad on both sides.
    Outputs:
        Tuple[np.ndarray, np.ndarray, np.ndarray]: The derivative of W, b,
            A_prev.
    """
    W = cache['W']
    b = cache['b']
    A_prev = cache['A_prev']
    dW = np.zeros(W.shape)
    db = np.zeros(b.shape)
    dA_prev = np.zeros(A_prev.shape)
 
    _, _, c_i = A_prev.shape
    c_o, f, f_2, c_k = W.shape
    h_o, w_o, c_o_2 = dZ.shape
 
    assert (f == f_2)
    assert (c_i == c_k)
    assert (c_o == c_o_2)
 
    A_prev_pad = np.pad(A_prev, [(padding, padding), (padding, padding),
                                 (0, 0)])
    dA_prev_pad = np.pad(dA_prev, [(padding, padding), (padding, padding),
                                   (0, 0)])
    for i_h in range(h_o):
        for i_w in range(w_o):
            for i_c in range(c_o):
                h_lower = i_h * stride
                h_upper = i_h * stride + f
                w_lower = i_w * stride
                w_upper = i_w * stride + f
 
                input_slice = A_prev_pad[h_lower:h_upper, w_lower:w_upper, :]
                # forward
                # kernel_slice = W[i_c]
                # Z[i_h, i_w, i_c] = np.sum(input_slice * kernel_slice)
                # Z[i_h, i_w, i_c] += b[i_c]
 
                # backward
                dW[i_c] += input_slice * dZ[i_h, i_w, i_c]
                dA_prev_pad[h_lower:h_upper,
                            w_lower:w_upper, :] += W[i_c] * dZ[i_h, i_w, i_c]
                db[i_c] += dZ[i_h, i_w, i_c]
 
    if padding > 0:
        dA_prev = dA_prev_pad[padding:-padding, padding:-padding, :]
    else:
        dA_prev = dA_prev_pad
    return dW, db, dA_prev
 
@pytest.mark.parametrize('c_i, c_o', [(3, 6), (2, 2)])
@pytest.mark.parametrize('kernel_size', [3, 5])
@pytest.mark.parametrize('stride', [1, 2])
@pytest.mark.parametrize('padding', [0, 1])
def test_conv(c_i: int, c_o: int, kernel_size: int, stride: int, padding: str):
 
    # Preprocess
    input = np.random.randn(20, 20, c_i)
    weight = np.random.randn(c_o, kernel_size, kernel_size, c_i)
    bias = np.random.randn(c_o)
 
    torch_input = torch.from_numpy(np.transpose(
        input, (2, 0, 1))).unsqueeze(0).requires_grad_()
    torch_weight = torch.from_numpy(np.transpose(
        weight, (0, 3, 1, 2))).requires_grad_()
    torch_bias = torch.from_numpy(bias).requires_grad_()
 
    # forward
    torch_output_tensor = torch.conv2d(torch_input, torch_weight, torch_bias,
                                       stride, padding)
    torch_output = np.transpose(
        torch_output_tensor.detach().numpy().squeeze(0), (1, 2, 0))
 
    cache = conv2d_forward(input, weight, bias, stride, padding)
    numpy_output = cache['Z']
    assert np.allclose(torch_output, numpy_output)
 
    # backward
    torch_sum = torch.sum(torch_output_tensor)
    torch_sum.backward()
    torch_dW = np.transpose(torch_weight.grad.numpy(), (0, 2, 3, 1))
    torch_db = torch_bias.grad.numpy()
    torch_dA_prev = np.transpose(torch_input.grad.numpy().squeeze(0),
                                 (1, 2, 0))
 
    dZ = np.ones(numpy_output.shape)
    dW, db, dA_prev = conv2d_backward(dZ, cache, stride, padding)
 
    assert np.allclose(dW, torch_dW)
    assert np.allclose(db, torch_db)
    assert np.allclose(dA_prev, torch_dA_prev)

 池化层的反向传播实现:

import numpy as np
from module import Layers 
 
class Pooling(Layers):
    def __init__(self, name, ksize, stride, type):
        super(Pooling).__init__(name)
        self.type = type
        self.ksize = ksize
        self.stride = stride 
 
    def forward(self, x):
        b, c, h, w = x.shape
        out = np.zeros([b, c, h//self.stride, w//self.stride]) 
        self.index = np.zeros_like(x)
        for b in range(b):
            for d in range(c):
                for i in range(h//self.stride):
                    for j in range(w//self.stride):
                        _x = i *self.stride
                        _y = j *self.stride
                        if self.type =="max":
                            out[b, d, i, j] = np.max(x[b, d, _x:_x+self.ksize, _y:_y+self.ksize])
                            index = np.argmax(x[b, d, _x:_x+self.ksize, _y:_y+self.ksize])
                            self.index[b, d, _x +index//self.ksize, _y +index%self.ksize ] = 1
                        elif self.type == "aveg":
                            out[b, d, i, j] = np.mean((x[b, d, _x:_x+self.ksize, _y:_y+self.ksize]))
        return out 
 
    def backward(self, grad_out):
        if self.type =="max":
            return np.repeat(np.repeat(grad_out, self.stride, axis=2),self.stride, axis=3)* self.index 
        elif self.type =="aveg":
            return np.repeat(np.repeat(grad_out, self.stride, axis=2), self.stride, axis=3)/(self.ksize * self.ksize)

 总结:在本次实验中印象最深刻的是推导CNN反向传播算法,在知乎上找到一篇很棒的文章,对卷积神经网络(CNN)反向传播算法推导的推导十分详细,虽然篇幅很长,但内容和叙述非常易于理解,通过对文章的阅读,对我的帮助很大

 ref:

https://blog.csdn.net/yaochunchu/article/details/95527760

https://zhuanlan.zhihu.com/p/61898234

https://blog.csdn.net/qq_38975453/article/details/127181421

http:// 卷积神经网络(CNN)反向传播算法 - 刘建平Pinard - 博客园 (cnblogs.com)

卷积神经网络(CNN)反向传播算法推导 - 知乎 (zhihu.com)

十二、CNN的反向传播 - 知乎 (zhihu.com)

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值