深度学习笔记(八)

卷积神经网络基础

Posted by Pelhans on April 19, 2019

概览

卷积神经网络 是一种专门用来处理具有类似网格结构的数据的神经网络。CNN 近年来在很多领域都表现优异。卷积神经网络依次的来源是因为该网络使用了卷积这种数学运算。卷积是一种特殊的线性运算。卷积网络是指那些至少在网络的一层中使用卷积运算来替代一般的矩阵乘法运算的神经网络。

卷积网络中一个典型层包含三级。第一级是卷积级,并行地计算多个卷积产生一组线性激活响应。第二级是探测级,每一个线性激活响应都会通过一个非线性的激活函数,如 ReLU。第三极叫池化级,这里通过池化函数来进一步调整输出。更高级的卷积神经网络是这些基本组件的深度结合。

什么是卷积

摘自百度百科:在泛函分析中,卷积是通过两个函数 x 和w 生成第三个函数的一种数学算子,表征函数x与w经过旋转和平移的重叠部分的面积。如果将参加卷积的一个函数看做区间的指示函数,卷积还可以被看做是滑动平均的推广。从公式上来看,假设 f(a)、g(a)是$R^{1}$上的两个可积函数,做积分:

h(t)就称为 函数 x 与 w 的卷积,记为 $h(x) = (x*w)(t) $$

在卷积神经网络术语中,我们把 x 叫做输入,w 叫做核函数,输出有时叫特征映射。当然,在实际的网络中,输入往往是离散化的,因此上述积分可以转化为请求和的形式。因为输入的范围也是有限的,因此上式可以进一步的简化。比如如果把一张二维的图像I 作为输入,用二维卷核 K 做卷积,则:

卷积是可交换的,因此上式还可以写作:

原书中没说 m,n,i,j代表什么意思。我猜测 m,n 是卷积核的索引,i,j 是输入的索引。

上面说的是数学上的卷积,但在深度学习中,许多神经网络库会实现一种相关的函数,成为互相关函数(cross-correlation) 的东西,它和卷积的运算几乎一样但没有对核进行翻转(也就是 i-m这种 m 前带负号的) :

下图给出一个在二维张量上的卷积运算的例子(没有进行核翻转):

为什么卷积要长成这个样子?

怎么理解卷积公式是 这种形式呢?在网上看了一圈,最终觉得冲击响应的解释比较能接受。

大体上来说,假设卷积的一方是冲击响应,另一方是输入信号 f(x),它们的分布如下图所示引自知乎,对于第 n 秒的输出,它叠加了:

  • 第 n 秒的输入和对应相应的乘积:f(n) * h(0)
  • 第 n-1 秒的输入残留到当前时间的响应:f(n-1)*h(1)
  • 第 n-2 秒的输入残留到当前时间的响应: f(n-2) * h(2)
  • ……
  • 第 0 秒的输入残留到当前时间的响应: f(0)*h(n)

将上面这些贡献累加起来,就看到我们熟悉的卷积公式了。

卷积的可交换性是啥意思?

卷积的一种性质,可以从傅立叶展开的角度证明二者的的等价性。也可以唯象的理解一下,个人理解的角度是,原本的卷积公式是:

将卷积核翻转,得到:

再在各维度上平移 i 和 j,则

当然这只是感性的认识,并不严谨。

为什么DL 中用的都是不翻转版本的?

引自知乎回答

  • 数学中卷积,主要是为了诸如信号处理、求两个随机变量和的分布等而定义的运算,所以需要“翻转”是根据问题的需要而确定的
  • 卷积神经网络中“卷积”,是为了提取图像的特征,其实只借鉴了“加权求和”的特点
  • 还有一点一定要说的是:数学中的“卷积核”都是已知的或者给定的,卷积神经网络中“卷积核”本来就是trainable的参数,不是给定的,根据数据训练学习的,那么翻不翻转还有什么关系呢?因为无论翻转与否对应的都是未知参数!

为什么要用卷积

卷积运算通过三个重要的思想来帮助改进机器学习系统:稀疏权重、参数共享、等变表示。

稀疏权重

传统的矩阵运算,如果有 m 个输入和 n 个输出,那么需要 mn 个参数。而采用卷积运算后,由于卷积核的大小一般都不大,如为k,那么此时的输出需要 kn个参数就够了。下图可以解释这个:

除了权重稀疏了之外,从另一个角度看,在卷积网络中,尽管直接连接都是很稀疏的,但处在更深层次的单元可以间接地连接到全部或者大部分输入图像。

参数共享

参数共享是指在一个模型的多个函数中使用相同的参数。在传统的神经网络中,当计算一层的输出时,权重矩阵的元素只使用一次。而卷积运算的参数共享是说核的每一个元素都作用在输入的每一个位置上(说白了就是一个核扫遍整个输入,而不是动一下一个核)。卷积运算中的参数共享保证了我们只需要学习一个参数集合,而不是对于每一个位置都需要学习一个单独的参数集合。

等变表示

对于卷积,参数共享的特殊形式使得神经网络层具有对平移等变的性质。如果一个函数满足输入改变,输出也以同样的形式改变这一性质,我们就说它是等变的。特别低,如果函数 f(x) 与 g(x) 满足 f(g(x)) = g(f(x)) ,我们就说 f(x) 对与变换 g 具有等变性。

对于卷积来说,如果零 g 是输入的任意平移函数,那么卷积函数对于 g 具有等变性。也就是说,通过函数g 把输入I平移后进行卷积得到的结果,与先对I进行卷积再进行平移得到的结果是一样的。

卷积之外的组件-池化

池化函数使用某一位置的相邻输出的总体统计特征来代替网络在该位置的输出,常用的池化函数包含 最大池化、平均池化、L2范数以及基于距中心像素距离的加权平均函数等。

经过池化函数后的大多数输出并不会发生改变,具有平移不变性。局部平移不变性是一个很重要的性质,尤其是当我们关心某个特征是否出现而不关心它出现的具体位置的时候。使用池化可以看做增加了一个无限强的先验:这一层学得的函数必须具有对少量平移的不变性,当这个假设成立时,池化可以级大地提高网络的统计效率。当然,对空间区域进行池化产生了平移不变性,但当我们对分离参数(离散参数?)的卷积进行池化时。特征能够学得应该对于哪种变换具有不变性(就是说你对原始输入直接上池化能学习一定的平移不变性,要是输入经过卷积和非线性后在来池化,应该能学到其他的不变性)。从贝叶斯的角度看,可以把卷积网络当做一个具有无限强先验的全连接网络。其中卷积的使用当做对网络中一层的参数引入了一个无限强的先验概率分布,这个先验说明了该层应该学得的函数只包含局部连接关系并且对平移具有等变性。类似地,池化相当于每一个单元都具有对少量平移不变性的无限强先验(无限强先验需要对一些参数的概率置零并且完全禁止对这些参数赋值)。

除了可以学习不变性之外,池化还有其他的优点和用处。如因为池化综合了全部邻居的反馈,因此池化单元可以用于降采样。另外,对于很多任务来说,如关系抽取的 multi-instance 中,输入的维度是不定的,但经过最大池化后,输出可以达到统一。

CNN 的反向传播

CNN 的前向传播比较容易理解,那么它的参数怎么更新呢?拿来一个实例比较容易理解但是不具有通用性。而且最后的卷积核旋转180度是什么鬼?怎么你把梯度填充零就正好了?这里有一个很棒的博客Convolutional Neural Networks backpropagation: from intuition to derivation。我这里按照我的理解整理出来,方便自己记忆。

我们知道,卷积操作和正常全连接的不同在于接收的输入有限并且参数共享。因此反向传播比较麻烦,但这位小哥将卷积网络转化为减少连接数和权值共享的MLP:

上图中,最右侧的网络就可以看做卷积网络了,其中各个颜色的线表示权值,如紫、蓝、绿、粉四个颜色,因此对应卷积核大小为 2x2,输入单元有有9个,表示3x3的输入。

基础此,我们可以定义如下网络(用不翻转核的卷积):

其中 $\odot$ 表示卷积操作。同时我们定义神经网络的输出 C 对 $z^{l+1}$ 的梯度为:$\delta^{l+1}$,现在我们要求C 对 $z^{l}$ 的导数$\delta_{x,y}^{l} = \frac{\partial C}{\partial z_{x,y}^{l}}$:

上式中,$x^{‘}$ 和 $y^{‘}$表示在 $z^{l+1}$ 层中与输入 x,y 有关的索引,a,b 是核的索引。上式结果中 $w_{-a,-b}^{l+1} = ROT180(w^{l+1})$。

CNN 的变体与应用

细节问题

估计给定 CNN 参数时的输出维度

对于卷积层,假设输入的维度是 ,高度 、输入通道数 。假设一共 个卷积核,每个卷积核的宽度为 ,高度为 。假设沿着宽度方向的步长为 ,沿着高度方向的步长为

则输出的大小为(其中[]表示向下取整):

  • 高度为
  • 高度为

池化的作用

  • 增大感受野
  • 降低纬度
  • 降低优化难度和参数
  • 增加了一个无限强的先验:这一层学得的函数必须具有对少量平移的不变性

Padding 的作用

直观上来看就是对输入周边进行扩充。效果上来看,一方面保证了输入和输出的维度,另一方面减缓边界较少关注的效应,使得原始输入中各点被等机会看待,维持了卷积的平移等变性。

卷积的简单实现

import numpy as np

def conv_naive(x, out_c, ksize, padding=0, stride=1):
    b, h, w, in_c = x.shape
    kernel = np.random.rand(ksize, ksize, in_c, out_c)
    if padding > 0:
        pad_x = np.zeros((b, h+2*padding, w+2*padding, in_c))
        pad_x[:,padding:-padding,padding:-padding,:] = x

    out_h = (h+2*padding-ksize)//stride+1
    out_w = (w+2*padding-ksize)//stride+1
    out = np.zeros((b, out_h, out_w, out_c))

    for i in range(out_h):
        for j in range(out_w):
            roi_x = pad_x[:,i*stride:i*stride+ksize,j*stride:j*stride+ksize,:]
            conv = np.tile(np.expand_dims(roi_x, -1), (1,1,1,1,out_c))*kernel
            out[:,i,j,:] = np.squeeze(np.sum(conv, axis=(1,2,3), keepdims=True), axis=3)
    return out

if __name__ == '__main__':
    x = np.random.rand(1,10,10,3)
    out = conv_naive(x, 15, ksize=3, padding=1, stride=2)
    print(out.shape)