深度学习入坑篇-卷积及numpy实现

前言

  卷积神经网络(ConvNets或CNNs)作为一类神经网络,托起cv的发展,本文主要介绍卷积神经网络的灵魂——卷积操作,其原理,并以小白视角,完成卷积从0到1的numpy实现。

1

   卷积神经网络(ConvNets或CNNs)作为人工智能的入门神经网络,已被广泛用于图像识别和分类等领域。除了为机器人和自动驾驶汽车提供视觉之外,ConvNets 在识别人脸、物体和交通标志方面也应用广泛。其中卷积操作作为cnn的灵魂,其出现加快了人工智能的发展。

  卷积一词一开始出现在数学中,为了表示一个函数对另外一个函数所有微量的响应的总叠加效应;其本质上是一种过滤器,在一维信号上完成对信号的过滤,在二维图像上完成为图像的一种过滤,过滤掉其不重要部分,提取主要特征,这里主要详解图像的卷积操作,图像卷积并不是出最近才出现,而在传统图像处理中就有sobel过滤器做边缘检测时就用到了卷积。

2

  了解卷积首先要了解其操作对象:图像,图像本质上为像素的矩阵:
在这里插入图片描述

  如上图所示,上图所示的灰度图,为单通道,而一般图像会有RGB三通道,每个通道中,每个像素点的值在0~255之间,0为黑色,255为白色。卷积(conv)为卷积算子,为了方便理解,我们下图5*5矩阵作为图像像素:
在这里插入图片描述
  考虑另一个 3 X 3矩阵为卷积矩阵:
图片
然后可以计算5x5图像和3x3矩阵的乘积:
在这里插入图片描述

  将卷积矩阵在图像上依次从左到右从上到下滑动1个像素(也称stride),对于每个位置,对卷积矩阵和对应图像的像素点相乘后相加,得到最终的整数,作为输出矩阵中的单个元素,上图为单通道图像,对于一般的三通道图像,kernel的in_channel也需要保证为3通道。计算过程如下图,在计算完每层的feature后,每层的feature对应位置相加后得到最终输出feature层。在这里插入图片描述

  在CNN中,3×3矩阵称为**”滤波器**“、”内核“或”特征检测器“,输出的矩阵称为”卷积特征“、”feature map“,从上述动图中看出,不停的kernel会对同一种输入图像产生不同的特征图,如下图,先输入一张原始图片:
在这里插入图片描述
  选择不同的滤波器矩阵对图像做卷积操作,实现边缘检测、锐化和模糊等操作–检测图像的不同特征,如边缘、曲线等:
在这里插入图片描述
  实际卷积操作特征形成如下图:
在这里插入图片描述

  红色和绿色框为两个卷积kernel,在输入图像上滑动、卷积分别生成两张特征图,如图所示。而因为卷积kernel的size确定了他只能获得图像局部的依赖关系(当然你可以将kernel的size设成图像大小。。),在实际CNN中,需要通过训练来学习这些kernel的值以确定他们需要从图像中提取哪些特征。

3

  conv卷积算子的实现torch、tensorflow等框架中均已封装好,拿来即用,非常方便,这边是方便自己理解,通过numpy 从0实现conv。思路如下,因为考虑conv算子不仅需要前向计算,还需要反向更新,所以先建个Layers类:

import numpy as np
import os

class Layers():
     def __init__(self, name):
         self.name = name 
 
     def forward(self, x):
        pass

     def zero_grad(self):
        pass 

     def backward(self, grad_out):
        pass

     def update(self, lr):
        pass

conv卷积算子集成Layer类,前向和反向实现如下:

 import numpy as np
 from module import Layers

 class Con2d(Layers):
      """
      卷积前向:
      输入:input:[b, cin, h, w]
           weight:[cin, cout, ksize, ksize], stride, padding 
     计算过程:
         1.  将权重拉平成:[cout, cin*ksize*ksize] self.weight 先transpose(1, 0, 2,3) 再reshpe(cout, -1)
         2.  将输入整理成:[b*hout*wout,cin*ksize*ksize]: 
             先根据hin和win 通过pad, ksize和stride计算出hout和wout (h+2*pad-ksize)//stride + 1 (b, cout, hout, wout)
             再根据img展平,整理成自己的:img  (b, hout, wout, cin*kszie*ksize)  -> (b*hout*wout, cin*kszie*ksize)
         3. 两者相乘后,np.dot 再去reshape (cout, b*hout*wout) -> (b, cout, hout*wout)
     """
     """
     卷积反向:
     输入:input:[b, cout, hout, wout] -loss 
     计算过程: 
         1. 将输入换成输出格式: [b, cout, hout, wout] -> [cout, b, hout, wout] ->[cout, b*hout*wout] 
         2. 计算的输入与之前的图相乘: (cout, b*hout*wout) * (b*hout*wout, cin*kszie*ksize) -> (cout, cin*kszie*ksize) 得到更新后的权重
         3. 将更新后的权重与图相乘,
 
     """
     def __init__(self,name, in_channel, out_channel, kernel_size, padding, stride=1 ):
         super(Con2d,self).__init__(name)
         self.in_channel = in_channel
         self.out_channel = out_channel
         self.ksize = kernel_size
         self.padding = padding
         self.stride = stride
 
         self.weights = np.random.standard_normal((out_channel, in_channel, kernel_size, kernel_size))
         self.bias = np.zeros(out_channel)
         self.grad_w = np.zeros(self.weights.shape)
         self.grad_b = np.zeros(self.bias.shape)
 
     def img2col(self, x, ksize, strid):
        b,c,h,w = x.shape # (5, 3, 34, 34)
         img_col = []
         for n in range(b): # 5
                 for i in range(0, h-ksize+1, strid):
                     for j in range(0, w-ksize+1, strid):
                         col = x[n,:, i:i+ksize, j:j+ksize].reshape(-1) # (1, 3, 4, 4) # 48
                         img_col.append(col)
         return np.array(img_col) # (5, 3, 31, 31, 48)
 
     def forward(self, x):
         self.x = x #(5, 3, 34,34)
         weights = self.weights.reshape(self.out_channel, -1) # (12, 3*4*4)
         x = np.pad (x, ((0,0), (0,0), (self.padding, self.padding), (self.padding, self.padding)), "constant") # (5, 3, 34, 34)
         b, c, h, w = x.shape
         self.out = np.zeros((b, self.out_channel, (h-self.ksize)//self.stride+1, (w-self.ksize)//self.stride+1))# (5, 12, 31, 31)
         self.img_col = self.img2col(x, self.ksize, self.stride) #  (5, 31, 31, 48) #(4805, 48)
         out = np.dot(weights, self.img_col.T).reshape(self.out_channel, b, -1).transpose(1, 0,2) # (12 ,48) *(48, 4805) = (12, 4805) =(12, 5, 961) =(5, 12, 961)
         self.out = np.reshape(out, self.out.shape) 
         return self.out
 
     def backward(self, grad_out):
         b, c, h, w = self.out.shape
         grad_out_ = grad_out.transpose(1, 0, 2, 3 )
         grad_out_flag = np.reshape(grad_out_,[self.out_channel, -1]) # [cout, b*h*w]
         self.grad_w = np.dot(grad_out_flag, self.img_col).reshape(c, self.in_channel, self.ksize, self.ksize) #  (cout, cin*kszie*ksize)  -权重值
         self.grad_b = np.sum(grad_out_flag, axis=1) # [cout] -偏置值
         tmp = self.ksize -self.padding -1
         grad_out_pad = np.pad(grad_out, ((0,0),(0,0),(tmp, tmp),(tmp,tmp)),'constant')
         weights = self.weights.transpose(1, 0, 2, 3).reshape([self.in_channel, -1]) # [cin. cout*ksize*ksize]
         col_grad = self.img2col(grad_out_pad, self.ksize, 1) # 
         next_eta = np.dot(weights, col_grad.T).reshape(self.in_channel, b, -1).transpose(1, 0, 2)
         next_eta = np.reshape(next_eta, self.x.shape)
         return next_eta
 
     def zero_grad(self):
         self.grad_w = np.zeros_like(self.grad_w)  
         self.grad_b = np.zeros_like(self.grad_b)

     def update(self, lr=1e-3):
        self.weights -= lr*self.grad_w
        self.bias -= lr*self.grad_b 

if __name__ == '__main__':
     x = np.ones([2,3,32,32])
     conv = Con2d('conv1',3,12,3,1,1)
     for i in range(100):
       y = conv.forward(x)
       loss =abs( y - 1)
       x = conv.backward(loss)
       lr = 1e-4 
       conv.update(lr)
       print(np.sum(loss))

  从卷积的实现发现,卷积操作中一大时间花费在将图像转成矩阵img2col函数上,这也是后面在模型轻量化过程中,mobilenet深度可分离卷积 + 1x1卷积中,1x1卷积无需img2col,易于在芯片端部署,加快卷积速度。

更多小白入坑篇请关注公众号【所向披靡的张大刀

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值