深度学习基础篇 第一章:卷积

简介: 简要介绍卷积的原理和实现。

参考教程:
https://www.zhihu.com/question/22298352
https://zhuanlan.zhihu.com/p/555957573?utm_id=0
https://zhuanlan.zhihu.com/p/394917827


什么是卷积

卷积的数学意义

首先从数学公式的角度看一下卷积。
从公式来看,卷积有两种形式,一种是连续形式,一种是离散形式。

  • 连续形式
    $$ (f*g)(n) = \int^{\infty}_{-\infty}f(\tau)g(n-\tau)d\tau $$
  • 离散形式
    $$ (f*g)(n) = \sum^{\infty}_{\tau=-\infty}f(\tau)g(n-\tau) $$

两种形式的公式的共通的部分是:
$$ f(\tau)g(n-\tau) $$

$f(\tau)$是一个与$n$无关的函数,它可以理解为某个点$\tau$对应的一个系统结果,与别的因素无关。$g(n-\tau)$衡量了两个点$n$和$tau$之间的关系,$f(\tau)g(n-\tau)$可以理解成$\tau$的结果对$n$的影响。

在连续形式中,做的是一个积分;在离散形式中,则是一个累加和。不管是什么形式,卷积公式都可以看成点$n$受到的其它所有点$\tau$的影响的累加。

在$n$固定的情况下,$\tau$越大,$n-\tau$就越小。$\tau$越小,$n-\tau$就越大。

借用一张知乎上的图片:
image.png

在$n=10$的情况下,$f(\tau)$与$g(10-\tau)$的对应关系如上图,在$\tau=0$时,要计算的是$f(0)g(10)$,在$\tau=10$时,要计算的是$f(10)g(0)$。两者形成一个“卷”的关系,同时求周围所有点对当前点的影响,又需要“积”。整体组合成了一个“卷积”的计算。

图像处理中的卷积

这部分参考:https://www.zhihu.com/question/22298352/answer/228543288

我们的图像一般是二维图像【先不考虑通道数的情况】,我们对图像进行卷积处理,使用的就是二维卷积,用的函数,也是二元函数。

下面给出了一个平滑图像的例子。
image.png

我们的图像是由多个像素点组成的,我们把它用函数表示为$f(x,y) = a_{x,y}$。
同时我们用一个平均矩阵,作为我们的$g(x,y) = b_{x,y}$,用这个矩阵对我们的图像进行卷积操作,来得到一个新的平滑的图像。【这个矩阵在这里其实就是我们常说的卷积核】

image.png

我们可以看到矩阵g中元素的下标取值范围和预想的有点不一样,考虑到一维情况下在进行计算时使用的是$g(n-\tau)$,在二维情况下,我们使用的是$g(u-i,v-j)$,以$u,v$为中心向外辐射再考虑周围的点,这个范围是【-1,1】,也就是中心点和它的八邻域。

写成公式后是
$$ (f*g)(u,v) = \sum_i\sum_jf(i,j)g(u-i,v-j) = \sum_i\sum_ja_{i,j}b_{u-i,v-j} $$
在这个公式中,i和j受到了g(u,v)函数和u,v取值的限制。
image.png

假如$u=1,v=1$,
那么公式表示为:

$$ \begin{align} (f*g)(1,1) &= \sum_{i=0}^2\sum_{j=0}^2f(i,j)g(1-i,1-j)\\ &=a_{0,0}b_{1,1}+a_{0,1}b_{1,0}+a_{0,2}b_{1,-1}+a_{1,0}b_{0,1}+a_{1,1}b_{0,0}\\ &+a_{1,2}b_{0,-1}+a_{2,0}b_{-1,1}+a_{2,1}b_{-1,0}+a_{2,2}b_{-1,-1} \end{align} $$

在这个公式和配图的组合中我们能发现,位于左上角的$a_{0,0}$与位于右下角的$b_{1,1}$相乘,位于右下角的$a_{2,2}$与位于左上角的$b_{-1,-1}$相乘,它们之间也是又有“卷”又有“积”的操作。

随着$u,v$的数值的改变,我们的矩阵g在图像上进行滑动求和,计算出新的数值,从而实现了对图像的卷积操作。一般在使用时,会对g矩阵旋转180°,这样计算起来比较方便。,也就是将g变为:

$$ g = \begin{bmatrix}b_{1,1}&b_{1,0}&b_{1,-1}\\ b_{0,1}&b_{0,0}&b_{0,-1}\\ b_{-1,1}&b_{-1,0}&b_{-1,-1}\\ \end{bmatrix} $$
当前在深度学习中我们不用考虑这个翻转,直接计算就可以。

深度学习中的卷积

深度学习中的卷积,也称为滤波器。它由一组卷积核组成,这个卷积核就类似于在上一节中说到的矩阵g,它具有固定的大小,在输入图像上通过滑动求值。但是和我们使用的具有固定数值的矩阵g不同的是,深度学习中的卷积核的参数是可学习的,会随着训练的进行不断更新。

在深度学习中,我们输入一个多通道的特征图,使用特定数目的卷积核对特征图进行计算, 并得到一个拥有更高层语义信息的输出特征图。

从全连接层说起

一般我们都会进行卷积层核全连接层的对比,相比全连接层,卷积层有着自己的优势。

我们先从一张图看一下在处理二维图像时,全连接层存在什么问题。
image.png

对于一个大小为200x200的输入图像,我们使用全连接层对这个图像直接做分类任务,目标是1000分类。全连接,顾名思义,就是所有的节点都连接在一起,那么它的输入节点数目是200x200=40000,输出节点数目是1000,连接数是40000000,参数量也是如此。

如图中所说,它的参数量太多,计算量比较大。同时,对于200x200的图像,一般是进行flatten操作将其变为40000个节点,这个操作会使得图像的空间信息丢失,并且相邻像素点也会被分开处理。

概括来讲,全连接层的缺点是:

  1. 使用密集连接,计算量大,参数量也大。
  2. 丢失空间信息,空间结构被破坏。

卷积的特性

全连接层会丢失空间信息,但是卷积是在图像中某个点和它的邻域进行计算,空间结构不会被破坏,这是它的一个优点。另一个优点就是相对全连接层来说,它的计算量参数量都要小很多。

这主要依赖于它的两个特性:

  1. 稀疏连接。
  2. 权值共享。

稀疏连接

传统的全连接层使用矩阵乘法计算,每个输入核输出之间进行密集连接,所有的输入和输出单元都要进行交互。而卷积网络则使用的是稀疏连接,输出单元只和部分输入单元有交互,极大的减少了计算量。

如图中所示,假如你使用的卷积核大小为3x3,那么你的每个输出单元只需要和9个输入单元存在交互。
image.png

对于上图,你的输入大小是7x7,输出大小是5x5。假如使用全连接层进行计算,那么你的输入单元数是49,输出单元数是25,连接数是49x25 = 1225。

使用卷积层时,每个输出单元只需要和3x3个输入单元交互,所以连接数是25x9 = 225。相比于全连接层的连接数大大减少。

权值共享

在卷积层中,卷积核的参数是共享的。权值共享是指在计算图层输出时多次使用同样的参数进行运算。对于一个输入的图像,使用卷积核在上面进行滑动遍历,不管遍历多少次,参数的数量都是固定的。

在全连接层中,1225个连接就意味着有1225个权重参数要学习,而对于卷积层,使用一个大小为3x3的卷积核,就意味着要学习的参数有3x3+1(bias)=10个,相比于全连接层也是大大减少的。

其它特性

卷积层还有一些别的特性:

  1. 平移不变性
    当图像中的目标发生偏移时,网络能输出一致的结果。这里主要针对的是分类的任务,对于图像中目标发生位置偏移,图像分类的结果应该保持一致。
  2. 平移等变性
    当输入发生偏移时,网络的输出结果也有相应的偏移。这里主要针对的是目标检测等任务。

卷积的相关概念

  • 卷积核(kernel):卷积基本组成单元,一般使用的卷积核大小为3x3或者5x5,卷积核通常是一个二维的概念。
  • 滤波器(filter):滤波器和卷积核的概念可以互换,但是本质上略有不同,滤波器是多个卷积核堆叠的结果。如果使用的是二维的权重矩阵,那么滤波器和卷积核是等价的,假如是多维的情况下,使用滤波器这个表述更合适。
  • 感受野(Receptive field):当前特征图上的像素点在原始图像上映射的区域大小。
  • 步长(stride):卷积核在输入图像上进行滑动遍历时的步长。一般我们希望得到一个比输入图像尺寸要小的输出,可以通过控制步长来控制输出的大小。
  • 填充(padding):对输入图像边界进行填充,可以改变输入特征图的大小。

卷积的三种模式

常用的卷积有三种模式:full,same,valid。这主要取决出卷积输出特征图的大小,影响输出特征图大小的因素只要有卷积核大小,步长和填充大小。

我们先给一个特征图大小的计算公式:
$$ D_{output} = \frac{D_{input}-D_{kernel}+2\times Padding}{Stride}+1 $$

假如输入特征图大小为5x5,使用大小为3x3的卷积核,步长为2,padding为0,那么直接得到的输出特征图大小为:
$$ \frac{5-3+2\times0}{2}+1 = 2 $$

  • full卷积
    full卷积是一种上采样卷积,它允许卷积核比特征图大。比如说输入是2x2的特征图,使用大小为3x3,步长为1的卷积后,输出大小为4x4的特征图。代入上面的公式,我们可以知道,这种情况下,padding的大小为2。
    实际上,在这种情况下,使用的padding一般都为固定值,即$D_{kernel}-1$
  • same卷积
    same卷积是一种输出特征图和输入特征图大小保持一致的卷积方式。为了保持一致,通常需要你自己计算一下padding的大小。
  • valie卷积
    valid卷积是最常用的下采样卷积,它的特点是卷积核不能超过特征图的范围。在这种情况下,一般不使用padding。

    填充的四种形式

    边界填充手段有多种,常用的一般有四种。
  1. 零填充ZeroPad2d:使用值0进行边界填充。
  2. 常数填充ConstantPad2d:使用指定常数进行填充,零填充可以看作一种特殊的常数填充。
  3. 镜像填充ReflectionPad2d:使用中心对称的对边方向的值进行填充。
  4. 重复填充ReplicationPad2d:使用边缘像素值填充。【和edge填充应该是同一类】

此外还有一些别的方法,比如对称填充:以特征图边界为对称中心进行填充。

卷积的计算

感受野的计算

从下往上计算

我们按照从下往上的计算方法,从最后一层开始递推,传递到第一层。
感受野计算公式:
$$ RF_i = (RF_{i+1}-1)\times Stride_i + K_{size_i} $$

这个公式其实是特征图大小正向计算倒推得到的,不考虑padding的大小。

假如我们现在在第5层,通过递推得到的$RF_0$的值,其实是第五层相对输入的感受野大小。理解上可能有些绕,用一个具体的例子看看。

image.png

如上图,输入大小为7x7的特征图,经过两个3x3卷积后,求最后一层的感受野大小。

Layer kernel Stride
input
conv1 3x3 1
conv2 3x3 1

对于conv2:大小为3。
对于conv1:大小为$(3-1)\times1+3=5$
所以对于最后一层,感受野大小就是5。

从上往下计算

也可以从原始输入出发,逐层迭代到最后。
计算公式是:
$$ RF_{i+1} = (K-1)*\prod_{n=0}^iS_n + RF_i $$
还是使用上面的例子:初始感受野为1。

Layer kernel Stride RF
input 1
conv1 3x3 1 2*1+1 = 3
conv2 3x3 1 2*1+3 = 5

参数量和运算量的计算

在实际使用中,我们一般不会使用单个卷积核进行卷积计算,而是使用多个filter针对多维输入特征图进行运算,并输出一个新的多维特征图。

先给出公式,然后看例子。
对于卷积层,它的参数量计算公式如下:
$$ params = C_o \times (k_w \times k_h \times C_i + 1) $$
对于卷积层,它的运算量计算公式如下:
$$ FLOPs = C_i \times C_o \times k_w \times k_h \times W_o \times H_o $$

我们一步一步来看一下参数量和计算量是怎么计算的。

  1. 一通道输入,多通道输出image.png

    图中给出的例子,输入是一个大小为7x7的特征图,输出为2x5x5的特征图。具体来说,在这一层中使用了2个大小为3x3的filter。(因为是二维filter,所以和kernel是等价的)
    所以这一层的可学习参数量为$2\times(3\times3+1)=20$。
    这一层的连接数为$2\times(3\times3\times5\times5)=450$。

  2. 多通道输入,一通道输出
    image.png

    图中给出的例子,输入是一个大小为3x7x7的特征图,输出为5x5的特征图。具体来说,在这一层中使用了一个大小为3x3x3的filter。
    所以这一层的可学习参数量为$3\times3\times3+1$。
    这一层的连接数为$3\times(3\times3\times5\times5) = 675$。

  3. 多通道输入,多通道输出
    image.png

    图中给出的例子,输入是一个大小为3x7x7的特征图,输出为2x5x5的特征图。具体来说,在这一层中使用了2个大小为3x3x3的filter。
    所以这一层的可学习参数量为$2\times(3\times3\times3+1)$。
    这一层的连接数为$2\times3\times(3\times3\times5\times5) = 1350$。

代码实现

下面给出两个版本的代码实现。

普通版本

下面没有考虑stride,可以自己修改一下

import numpy as np
class Conv:
    def __init__(self,c_in, c_out,kernel):
        self.c_in = c_in
        self.c_out = c_out
        self.kernel = kernel
        self.filter_shape = (c_out, c_in, kernel, kernel)
        self.filter = np.random.randn(*self.filter_shape)


    def forward(self,img):

        self.img = img

        h, w, c = img.shape

        out_h = h - self.kernel + 1
        out_w = h - self.kernel + 1

        output = np.zeros((out_h, out_w, self.c_out))

        for i in range(out_h):
            for j in range(out_w):
                for m in range(self.c_out):
                    for n in range(self.c_in):
                        cur_img = img[i:i+ self.kernel,j:j+self.kernel,n]
                        output[i][j][m] += np.sum(np.multiply(cur_img, self.filter[m][n]))

        return output

    def backward(self, gradient):
        kernel_grad = np.zeros(self.filter_shape)

        h,w,c = gradient.shape
        in_h = h - 1 + self.kernel
        in_w = h - 1 + self.kernel

        in_gradient = np.zeros((in_h,in_w,self.c_in))

        for i in range(self.c_out):
            for j in range(self.c_in):

                for m in range(self.kernel):
                    for n in range(self.kernel):
                        cur_img = self.img[m:m+h, n:n+w,j]
                        kernel_grad[i][j][m][n] += np.sum(np.multiply(cur_img, gradient[:,:,i]))
        new_grad = np.zeros((h+2*self.kernel-2,w*self.kernel-2,c))        

        for i in range(self.c_out):
            for j in range(self.c_in):

                for m in range(in_h):
                    for n in range(in_w):

                        cur_img = new_grad[m:m+self.kernel,n:n+self.kernel,i]
                        in_gradient[m,n,j] += np.sum(np.multiply(cur_img, self.filter[i][j]))



        return kernel_grad, in_gradient

优化版本

下面的版本来自《深度学习入门:基于python的理论和实现》

def im2col(input_data, filter_h, filter_w, stride=1,pad=0):
    N,C,H,W = input_data.shape
    out_h = 1+int((H+2*pad-filter_h)/self.stride)
    out_w = 1+int((W+2*pad-filter_w)/self.stride)

    img = np.pad(input_data,[(0,0),(0,0),(pad,pad),(pad,pad)],'constant')  #四个维度的填充,N和C不填充
    col = np.zeros((N,C,filter_h, filter_w, out_h, out_w))
    for y in range(filter_h):
        y_max = y + stride*out_h
        for x in range(filter_w):
            x_max = x + stride*out_w
            col[:,:,y,x,:,:] = img[:,:,y:y_max:stride, x:x_max:stride]
    col = col.transpose(0,4,5,1,2,3,).reshape(N*out_h*out_w,-1)
    return col
def col2im(col, input_shape,fh, fw, stride=1, pad=0):
    # n*out*out, cff)
    N,C,H,W = input.shape
    out_h = 1+int((H+2*pad-filter_h)/self.stride)
    out_w = 1+int((W+2*pad-filter_w)/self.stride)
    col = col.reshape(N,out_h,out_w,C,fh, fw)

    img = np.zeros((N, C, H + 2*pad + stride - 1, W + 2*pad + stride - 1))
    for y in range(filter_h):
        y_max = y+stride*out_h
        for x in range(filter_w):
            x_max = x+stride*out_w
            img[:,:,y:y_max:stride, x:x_max:stride]+=col[:,:,y,x,,:,:]
    return img[:,:,pad:H+pad,pad:W+pad]

class Convolution:
    def __init__(self,w, b, stride=1, pad=0):
        self.w = w
        # self.w = np.random.randn((out_c, in_c, fh, fw))
        self.b = b
        self.stride = stride
        self.pad = pad
    def forward(self,x):
        oc, C, fh,fw = self.w.shape
        N, C, H, W = x.shape
        out_h = 1+int((H+2*self.pad-fh)/self.stride)
        out_w = 1+int((W+2*self.pad-fw)/self.stride)

        col = im2col(x,fh,fw,self.stride, self.pad)
        self.col = col
        # n*out_h*out_w, c*k*k
        out = col* self.w.reshape(oc,-1).T + self.b
        out = out.reshape(N,out_h,out_w, -1).transpose(0,3,1,2)
        return out
    def backward(self,dout):
        oc,C,fh,fw = self.w.shape
        dout = dout.transpose(0,2,3,1).reshape(-1,oc)
        self.db = np.sum(dout,axis=0)
        self.dw = np.dot(dout,self.col.T)
        self.dw = self.dw.transpose(1,0).reshape(oc,C,fh,fw)
        self.col_w = self.w.reshape(oc,-1).T
        dcol = np.dot(dout,self.col_w.T)
        dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)
        return dx
相关文章
|
6天前
|
机器学习/深度学习 数据可视化 算法框架/工具
深度学习第3天:CNN卷积神经网络
深度学习第3天:CNN卷积神经网络
|
6天前
|
机器学习/深度学习 编解码 计算机视觉
一文读懂深度学习中的各种卷积 !!
一文读懂深度学习中的各种卷积 !!
43 0
|
6天前
|
机器学习/深度学习 人工智能 TensorFlow
人工智能与图像识别:基于深度学习的卷积神经网络
人工智能与图像识别:基于深度学习的卷积神经网络
40 0
|
6天前
|
机器学习/深度学习 TensorFlow 算法框架/工具
基于深度学习的图像分类:使用卷积神经网络实现猫狗分类器
基于深度学习的图像分类:使用卷积神经网络实现猫狗分类器
98 0
|
6天前
|
机器学习/深度学习 算法 数据挖掘
深度学习500问——Chapter05: 卷积神经网络(CNN)(4)
深度学习500问——Chapter05: 卷积神经网络(CNN)(4)
15 1
|
6天前
|
机器学习/深度学习 自然语言处理 计算机视觉
深度学习500问——Chapter05: 卷积神经网络(CNN)(3)
深度学习500问——Chapter05: 卷积神经网络(CNN)(3)
12 1
|
6天前
|
机器学习/深度学习 自然语言处理 计算机视觉
深度学习500问——Chapter05: 卷积神经网络(CNN)(2)
深度学习500问——Chapter05: 卷积神经网络(CNN)(2)
14 2
|
6天前
|
机器学习/深度学习 存储 自然语言处理
深度学习500问——Chapter05: 卷积神经网络(CNN)(1)
深度学习500问——Chapter05: 卷积神经网络(CNN)(1)
18 1
|
6天前
|
机器学习/深度学习 并行计算 算法
深度学习之解构卷积
本文介绍了卷积神经网络(CNN)的基本组成和各种卷积操作。CNN在机器视觉任务中扮演重要角色,其组件包括卷积层、pooling层、激活函数和全连接层。卷积层通过卷积核提取特征,pooling层降低维度,参数共享减少计算量。主要讨论了六种卷积类型:空洞卷积用于增加感受野,1x1卷积用于维度调节和通道融合,反卷积实现上采样,深度可分离卷积减少参数,可变形卷积适应不规则特征,3D卷积处理视频等四维数据。这些卷积操作在现代深度学习模型中广泛应用,提高了效率和性能。5月更文挑战第4天
25 2
|
6天前
|
机器学习/深度学习 自然语言处理 搜索推荐