前向传播和反向传播_反向传播之六:CNN 卷积层反向传播

fd44b4884316ee23deff92507c962376.png

最好的学习方法就是把内容给其他人讲明白。

如果你看了我的文章感觉一头雾水,那是因为我还没学透。


CNN卷积层的反向传播相对比较复杂一点。

一、首先来看看前向传播算法

(1)单通道---极简情况

为了简单起见,设输入X为3* 3,单通道,卷积核K为2*2,输出Y为2*2,单通道。

,即

这里

所以,卷积运算最终转化为矩阵运算。即X、K、Y变形在之后对应矩阵变为XC、KC、YC,则

Y和K只要reshape一下就可以了,但X需要特别处理,这个处理过程叫im2col(image to column),就是把卷积窗口中的数拉成一行,每行

列,共(X.w-k+1)(X.h-k+1)行(一般X.w=X.h)。
#这是一个非常朴素的实现,优点是简单易懂,缺点是性能,可以慢到你想哭,后面再谈谈优化算法
def im2col(image, ksize, stride):
    # image is a 4d tensor([batchsize, width ,height, channel])
    image_col = []
    for i in range(0, image.shape[1] - ksize + 1, stride):
        for j in range(0, image.shape[2] - ksize + 1, stride):
            col = image[:, i:i + ksize, j:j + ksize, :].reshape([-1])
            image_col.append(col)
    image_col = np.array(image_col)

    return image_col

(2)多通道---真实情况

下面是一张被广泛引用的说明图,图中显示的输入是3通道(3层,比如R、G、B共3个channel),输出是2通道(channel),于是总共有3*2=6个卷积核,每个核有4个元素,3*4=12,所以6个卷积核排成一个12*2的核矩阵,即为权重矩阵,把这6个KC的组合(权重矩阵)记为WC(^_^)。

图中最底下一行表示两个矩阵乘积运算,就是卷积层的前向传播算法。实际编码时还会加上偏置,而且还要考虑Batchs。

cb3be073ead35f01bbde05aa9f7d9100.png

图中显示,如果

(channel在最后一个维度),

那么,这个图显示的矩阵乘法的维度是:

然后

即可。

如果

,即channel在前面,则上面的乘法需要反过来乘,WC在X的前面,维度也要相应的做调整。pytorch就是BCHW顺序。

二、反向传播

反向传播只与前向传播相关,在看完了前向传播之后,我们来看反向传播。

为了书写方便,记

,在反向传播中,
是从后面一层(一般是激活函数层或池化层)传过来的,是一个已知量,在此基础上求

1、

比较容易求

只要rehsape一下就可以得到

这跟全连接网络没多大区别。

也是一样。

2、求

,这个区别就大了

根据反向传播公式,

但是, 从

还原到
并非易事,im2col的逆映射计算复杂度高得不能接受,要计算
还得另寻它途。下面是新的计算方式的推导。

根据前向传播

可以计算每个

的导数:

所以

设上面三个矩阵分别为

,即

从而可见

还是一个卷积计算。

不过是对

进行卷积,从后向前卷积,我看到有的文章成为逆向卷积。

计算过程:

(1)把

四周填充0,是一个pad操作;再由im2col映射到
,

(2)把K映射到

有两种方法:

方法一:做中心对称(旋转180度),flipup(fliplr(K))(先左右对称,再上下对称),再reshape得到

。这个方法的缺点是flipup和fliplr只作用到一二维上,对后面维度不起作用,有的时候不是很方便。

方法二:先reshape,再在这个维度上取逆序。请看代码片段:

#self.weights维度为(k,k,self.input_channels, self.output_channels)       
#方法一
        # flip_weights = np.flipud(np.fliplr(self.weights))
        # flip_weights = flip_weights.swapaxes(2, 3)

#方法二
        flip_weights=self.weights.reshape([-1,self.input_channels,self.output_channels])
        flip_weights=flip_weights[::-1,...]
        flip_weights = flip_weights.swapaxes(1, 2)
        
#以下相同
        col_flip_weights = flip_weights.reshape([-1, self.input_channels])

注:在numpy文档中已经说明:flipud(m)等价于m[::-1,...],等价于flip(m,0),而fliplr(m)等价于m[:,::-1,...],等价于flip(m,1)。显然flip(m,i)和“::-1”这个操作可以作用到任何维度上,所以更加灵活。

(3)由

计算出
,再把
reshape一下即可得到

计算

并非必须,如果是第一层就没必要,只有在中间层才需要向前传播梯度

三、

卷积

卷积核为

,其实就只有一个数,所以
,卷积就是在图片的一个channel上乘同一个数。

此时

的结果也就是
,c是X的通道数(channel)。

所谓的图片的通道(channel)就是图片的深度(depth)。

一张图片其实是三维的立体:高度、宽度、深度,shape是(heigth,width,depth),其中(heigth,width)构成一张图片的一层,比如有R、G、B就有三层,depth=3,或者说channel=3.

卷积核就一个数,在每一层都乘一个数,把这d(depth或channel)层求和,得到一个输出层,即一个输出层是前面d层的一个线性组合。如果有m个输出层,就重复m次。这样就有d*m个卷积核,这d*m个核组成一个
矩阵,即权重矩阵
,是一shape为(d,m)的矩阵。

输入X(h,w,d)-->变换到

(h*w,d),

(这里h*w表示乘积,是一个数值,
表示两个维度)

再把 输出

reshape为

可见

卷积就是
。从维度上看,没有改变高度和宽度,但改变了深度。

四、same和valid

正常情况下(valid),一个大小为

的卷积核对大小为
的图像进行卷积,结果是大小为
的图像。

例如:

(5,5)--conv(5,5)-->(1,1)

(5,5)--conv(3,3)-->(3,3)--conv(3,3)-->(1,1)

结论是:一个conv(5,5)相当于两个conv(3,3),但是一个conv(5,5)有25个参数,两个conv(3,3)只有18个参数,所以节省25-18=7个参数。

再如:

(11,11)--conv(11,11)-->(1,1)

(11,11)--conv(3,3)-->(9,9)--conv(3,3)-->(7,7)--conv(3,3)-->(5,5)--conv(3,3)-->(3,3)--conv(3,3)-->(1,1)

可见一个conv(11,11)相当于5个conv(3,3),而11*11-5*3*3=76,所以现在一般倾向于用小的卷积核,但多弄几层。

但是层数多了会产生梯度消失或爆炸,这个问题被残差神经网络(ResNet)很好的解决了,残差神经网络搞上千层都没问题。

但是,ResNet由于每隔两层要直接加X(input),所以要保持大小不变(same)。

卷积完了要保持大小不变(same),就是在四周填充0(padding),比如宽度从w减少到w-k+1,减少了k-1,所以在两边各填充(k-1)/2个0,从而k必须是奇数。所以现在看到的卷积核基本都是(3,3)。上面已经讨论过(1,1)卷积主要作用是整形,改变图片深度。

另外,ResNet由于每隔两层要直接加X,必然会梯度爆炸,所以必须有一个BatchNormal层,对数据正则化,下一篇讨论BatchNormal的反向传播。

五、速度优化

卷积层的绝大部分(几乎全部)时间都耗在im2col上,所以其他优化措施作用都很小。

1、把涉及batch的循环用矩阵乘法代替,可以减少10%不到的时间:

    def forward(self, x):
        col_weights = self.weights.reshape([-1, self.output_channels])
        if self.method == 'SAME':
            x = np.pad(x, (
                (0, 0), (self.ksize // 2, self.ksize // 2), (self.ksize // 2, self.ksize // 2), (0, 0)),
                'constant', constant_values=0)

        self.col_image=im2col(x, self.ksize, self.stride)
        conv_out =np.dot(self.col_image, col_weights) + self.bias       
        conv_out= np.reshape(conv_out,np.hstack(([self.batchsize], self.eta[0].shape)))       
        return conv_out

    def gradient(self, eta):
        self.eta = eta
        col_eta = np.reshape(eta, [ -1, self.output_channels])
        self.w_gradient = np.dot(self.col_image.T,
                                    col_eta).reshape(self.weights.shape)
        self.b_gradient = np.sum(col_eta, axis=0)

        # deconv of padded eta with flippd kernel to get next_eta
        if self.method == 'VALID':
            pad_eta = np.pad(self.eta, (
                (0, 0), (self.ksize - 1, self.ksize - 1), (self.ksize - 1, self.ksize - 1), (0, 0)),
                'constant', constant_values=0)

        if self.method == 'SAME':
            pad_eta = np.pad(self.eta, (
                (0, 0), (self.ksize // 2, self.ksize // 2), (self.ksize // 2, self.ksize // 2), (0, 0)),
                'constant', constant_values=0)
        
        flip_weights=self.weights[::-1,...]
        flip_weights = flip_weights.swapaxes(1, 2)      
        col_flip_weights = flip_weights.reshape([-1, self.input_channels])
        
        col_pad_eta=im2col(pad_eta, self.ksize, self.stride)
        next_eta = np.dot(col_pad_eta, col_flip_weights)
        next_eta = np.reshape(next_eta, self.input_shape)
        return next_eta

def im2col(image, ksize, stride):
    # image is a 4d tensor([batchsize, width ,height, channel])
    image_col = []
    for b in range(image.shape[0]):
        for i in range(0, image.shape[1] - ksize + 1, stride):
            for j in range(0, image.shape[2] - ksize + 1, stride):
                col = image[b,i:i + ksize, j:j + ksize, :].reshape([-1])
                image_col.append(col)
    image_col = np.array(image_col)

    return image_col

2、换掉im2col方法

im2col是最常用的方法,比如caffe也是用这种方法。

不过有的研究表明,winograd方法可能更快,不过我还不懂,还请移步百度吧。

参见:

卷积神经网络中的Winograd快速卷积算法 - Mr-Lee - 博客园​www.cnblogs.com
48aee3fb7f3c6c1cb629f2879eaed98a.png

3、使用numpy的as_strided函数实现im2col,

def split_by_strides(self, x):
        # 将数据按卷积步长划分为与卷积核相同大小的子集,当不能被步长整除时,不会发生越界,但是会有一部分信息数据不会被使用
        N, H, W, C = x.shape
        oh = (H - self.ksize) // self.stride + 1
        ow = (W - self.ksize) // self.stride + 1
        shape = (N, oh, ow, self.ksize, self.ksize, C)
        strides = (x.strides[0], x.strides[1] * self.stride, x.strides[2] * self.stride, *x.strides[1:])
        return np.lib.stride_tricks.as_strided(x, shape=shape, strides=strides)
def forward(self, x):
        if self.method == 'SAME':
            x = np.pad(x, (
                (0, 0), (self.ksize // 2, self.ksize // 2), (self.ksize // 2, self.ksize // 2), (0, 0)),
                'constant', constant_values=0)
        conv_out=self.split_by_strides(x)
        conv_out=np.tensordot(conv_out,self.weights, axes=([3,4,5],[0,1,2]))
        return conv_out

在np里这样可以大大提升性能,图片比较大的时候可以达到100X的性能提升

详情请看

永远在你身后:卷积算法另一种高效实现,as_strided详解​zhuanlan.zhihu.com

4、使用CUDA实现,比如用pycuda或numba加速。不过话又说回来了,用numpy写不就是为了算法简单透明吗,当性能优先需要用到CUDA加速的时候,还有什么理由不用pytorch或者TensorFlow呢?

五、参考:

(1)卷积神经网络(CNN)反向传播算法

(2)反向传播原理 & 卷积层backward实现<一>

(3)

sebgao/cTensor​github.com
1421be3d227f4ea33ce16394b2b3f169.png

(4)

leeroee/MNN​github.com
9177a1f5de87679b2f8ac43f3f52ed1e.png

(5)

ddbourgin/numpy-ml​github.com
0b64c716433daba401cb1d26621f2ca9.png

六、源码:https://github.com/fmscole/backpropagation

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值