首先回答这样一个问题,为什么我们要学 CNN,或者说 CNN 为什么在很多领域收获成功?还是先拿 MNIST 来当例子说。MNIST 数据结构不清楚的话自行百度。。

我自己实验用两个 hidden layer 的 DNN(全连接深度神经网络) 在 MNIST 上也能取得不错的成绩 (98.29%)。下面是一个三个 hidden layer 的网络结构图
CNN详解 - 图1

盗图 1

全连接深度神经网络,顾名思义,每个神经元都与相邻层的神经元连接。在这个实验中,每个数字的 image 是 2828,也就是 784(=2828) 个数值,每个数值对应一个像素值,值的大小反应像素点的强度。这就意味着我们网络的输入层有 784 个神经元。输出层呢?由于我们是预测 0-9 这几个数字,输出层当然就是 10 个神经元了。至于隐藏层节点的个数我们可以自行选定,本实验中选的是 500.

我们想想为什么 DNN 在训练后能够正确地分类?那肯定是它学到了东西,学到什么东西呢?它学到了图片中的某些空间结构,不同数字它们的空间结构肯定是不一样的,而这样的空间结构就是由像素点与像素点之间的关系形成。我们再仔细看 DNN 输入层和第一个隐藏层,发现它对我们输入的 784 个像素点是同等对待的,也就是说它此时并没有考虑像素点与像素点之间的关系。有没有一个好点的模型能够考虑到这点呢?那就是 CNN

CNN 有三个几本思想,局部感受野 (local receptive fields) 权值共享 (shared weights) 池化 (pooling)

刚刚我们在 DNN 中是把输入层 784 个神经元排成了一条长线,这里我们还原图片原来的样子 (28*28),如下图

强迫症的同学就不要数每行每列几个了,我已经数过了是 28 了。。偷笑
CNN详解 - 图2

盗图 2

DNN 中,我们会把输入层的每个神经元都与第一个隐藏层的每个神经元连接 (看看盗图 1)。而在 CNN 中我们这样做的,第一个隐藏层的神经元只与局部区域输入层的神经元相连。下图就是第一个隐藏层的某个神经元与局部区域输入层的神经元相连的情况。
CNN详解 - 图3

盗图 3

这里的局部区域就是局部感受野,它像一个架在输入层上的窗口。你可以认为某一个隐藏层的神经元学习分析了它”视野范围 “(局部感受野) 里的特征。图中一个隐藏层的神经元有 5*5 个权值参数与之对应。

我们移动这样一个窗口使它能够扫描整张图,每次移动它都会有一个不同的节点与之对应。我们从输入层左上角开始,如下
CNN详解 - 图4

盗图 4

然后,我们一个像素往右滑动一个像素,如下
CNN详解 - 图5

盗图 5

以此类推可以形成第一个隐藏层,注意我们的图片是 2828 的,窗口是 55 的,可以得到一个 24*24(24=28-5+1) 个神经元的隐藏层

这里我们的窗口指滑动了一个像素,通常说成一步 (stride),也可以滑动多步,这里的 stride 也是一个超参,训练是可以根据效果调整,同样,窗口大小也是一个超参。

权值共享 (Shared weights and biases)
上一节中提到一个隐藏层的神经元有 55 个权值参数与之对应。这里要补充下,这 2424 个隐藏层的神经元它们的权值和偏移值是共享的 用公式描述下

σ(b+∑l=04∑m=04wl,maj+l,k+m)

σ代表的是激活函数,如 sigmoid 函数等,b 就是偏移值,w 就是 55 个共享权值矩阵,我们用矩阵 a 表示输入层的神经元,ax,y 表示第 x+1 行第 y+1 列那个神经元 (注意,这里的下标默认都是从 0 开始计的,a0,0 表示第一行第一列那个神经元)所以通过矩阵 w 线性 mapping 后再加上偏移值就得到公式中括号里的式子,表示的是隐藏层中第 j+1 行 k+1 列那个神经元的输入。有点晕的话就参照上面的图,图 4 就是 j=k=0 的情况,图 5 是 j=0,k=1. 最后加上激活函数就表示该隐藏神经元的输出了。这部分原理和 DNN 是一样的,如果把 w 改成 2828 的矩阵就变成了全连接,就是 DNN 了。

我们能不能简化一下这个公式呢

a1=σ(b+w∗a0)

a1 表示隐藏层的输出,a0 表示隐藏层的输入,而∗就表示卷积操作 (convolution operation) 这也正是卷积神经网络名字的由来。

由于权值共享,窗口移来移去还是同一个窗口,也就意味着第一个隐藏层所有的神经元从输入层探测 (detect) 到的是同一种特征(feature),只是从输入层的不同位置探测到(图片的中间,左上角,右下角等等),必须强调下,一个窗口只能学到一种特征!另外,窗口还有其他叫法:卷积核(kernal), 过滤器(filter)。我们在做图像识别时光学习一个特征肯定是不够的,我们想要学习更多的特征,就需要更多的窗口。如果用三个窗口的话如下图
CNN详解 - 图6

盗图 6

窗口与窗口间的 w 和 b 是不共享的,三个窗口就表示有三个 w 矩阵和三个偏移值 b,结果是从整张图片的各个位置学到三种不同的特征。到这里肯定有人会问,你说学到特征了,怎么证明学到了呀?现在我们用 20 个窗口来学习 MNIST 里的图片特征,我们只看 20 个窗口里的权值矩阵 w,如果把这 20 个 w 画成 20 张黑白图,每张图片都是 5*5(一个权值代表一个像素点),如下图所示
CNN详解 - 图7

盗图 7

盯着其中的一张看,白色区域表示权值比较小,说明窗口的这部分对输入层的神经元不敏感 (responds less),相反黑色部分表示权值比较大,说明窗口的这部分对输入层的神经元敏感 (responds more). 每张图片都有明显的黑白区域,这也能够说明 CNN 确实学到一些和空间结构相关的特征。究竟学的是什么特征呢?这个很难回答清楚,此处暂不深究,更好理解的话可以参考 Visualizing and Understanding Convolutional Networks

权值共享还有一个很大的好处,就是可以大大减少模型参数的个数。我们的例子中,一个窗口参数个数是 26(55+1),20 个窗口就是 520 个参数,如果换成全连接的话就是 785(2828+1)个参数,比 CNN 多了 265 个参数。可能你觉得 265 嘛,对计算机来说完全不算什么。如果我们是 30 个隐藏层的 DNN 的话 (深度学习里很常见的),需要 23550(785*30) 个参数,是 CNN 的 45 倍多。。当然我们也不能光光去比较它们参数的个数,毕竟两个模型本质原理上就相差甚远,但是直觉上我们可以感受到,CNN 可以依靠更少的参数来获得和 DNN 相同的效果,更少的参数就意味着更快的训练速度,这可是谁都想要的。

CNN 还有一个重要思想就是池化,池化层通常接在卷积层后面。池化这个词听着就很有学问,其实引入它的目的就是为了简化卷积层的输出。通俗地理解,池化层也在卷积层上架了一个窗口,但这个窗口比卷积层的窗口简单许多,不需要 w,b 这些参数,它只是对窗口范围内的神经元做简单的操作,如求和,求最大值,把求得的值作为池化层神经元的输入值,如下图,这是一个 2*2 的窗口
CNN详解 - 图8

盗图 8

值得注意的是,我们此时的窗口每次移动两步,采用的是求最大值的方法,所有称之为 max-pooling,刚刚卷积层含有 2424 个神经元,经过池化后到池化层就是 1212 个神经元。通常卷积层的窗口是多个的,池化层的窗口也是多个的。简单来说,卷积层用一个窗口去对输入层做卷积操作,池化层也用一个窗口去对卷积层做池化操作。但是注意这两个操作的本质区别。下面来看一个用三个卷积窗口和跟随其后的池化窗口长啥样。
CNN详解 - 图9

盗图 9

怎么理解 max-pooling 呢?由于经过了卷积操作,模型从输入层学到的特征反映在卷积层上,max-pooling 做的事就是去检测这个特征是否在窗口覆盖范围的区域内。这也导致了,它会丢失这种特征所在的精准位置信息,所幸的是池化层可以保留相对位置信息。而后者相比而言比前者更重要。不理解上面的话也没关系,但是需要记住池化层一个最大的好处:经过池化后,大大减少了我们学到的特征值,也就大大减少了后面网络层的参数 (上图可以看出池化层的神经元数明显少于卷积层神经元数)。

max-pooling 技术只是池化技术的一种,还有一种比较常用的是 L2-pooling, 与 max-pooling 唯一的区别就是在池化窗口扫过的区域里做的操作不是求最大值,而是所有神经元平方后求和再开根号,这和我们 L2 正则对权值参数的操作是一样的。实际操作中,这两种方式都是比较常用的。池化操作方式的选择也是我们调参工作的一部分,我们可以根据 validation data 集来调节,选择更好的池化操作。

介绍完 CNN 的三个几本思想概念后我们把它串起来看下。
CNN详解 - 图10

盗图 10
从左往右依次是输入层,卷积层,池化层,输出层。输入层到卷积层,卷积层到池化层已经详细介绍过了。池化层到输出层是全连接,这和 DNN 是一样的。
整体上把我 CNN 的网络架构,其实和 DNN 很相似,都是一层一层组合起来的,层与层之间的行为也是有对应的权值 w 和偏移值 b 决定的,并且它们的目的也是一致的: 通过 training data 来学习网络结构中的 w 和 b,从而能把输入的图片正确分类。很简单吧。

到此为之,CNN 的基本原理大致介绍完毕了,如果只需对 CNN 做大致了解的话上面的内容我想应该足够了。下面主要介绍下其数学原理了。

DNN 的 BP 算法

介绍 CNN 的 BP 算法之前还是先看下 DNN 的吧,两者有很多相似的地方。
这里我假设大家都理解了 DNN 的网络结构了,首先引入一些数学符号。

al: 第 l 层神经元的输出
zl: 第 l 层神经元的输入
Wl: 从 l-1 层 mapping 到 l 层权值矩阵
bl: 与上面参数对应的偏移值
x:train data 的输入
y:train data 正确的 label
这里设我们的输出层为第 L 层,对应 aL,采用均方差来度量损失,那么对应的损失函数就是

J(W,b,x,y)=12||aL−y||22(1)

好了,有了损失函数就开始用梯度下降法了。记住我们的目的是为了求出每层的 W 和 b
先来看下输出层的输出

aL=σ(zL)=σ(WLaL−1+bL)(2)

我们把 (2) 代入 (1) 中得到

J(W,b,x,y)=12||aL−y||22=12||σ(WLaL−1+bL)−y||22(3)

我们利用链式求导法,求得 W,b 的梯度

∂J(W,b,x,y)∂WL=∂J(W,b,x,y)∂zL∂zL∂WL=(aL−y)⊙σ′(zL)(aL−1)T(4)

∂J(W,b,x,y)∂bL=∂J(W,b,x,y)∂zL∂zL∂bL=(aL−y)⊙σ′(zL)(5)

⊙表示两个向量的内积
上面两个式子有个公共的部分那就是∂J(W,b,x,y)∂zL 我们可以单独把它拎出来看

δL=∂J(W,b,x,y)∂zL=(aL−y)⊙σ′(zL)(6)

我们这里引入了一个新的符号δl: ∂J(W,b,x,y)∂zl, 这样的话我们就可以简写下 (4),(5) 两个式子

∂J(W,b,x,y)∂Wl=∂J(W,b,x,y)∂zl∂zl∂Wl=δl(al−1)T(7)

∂J(W,b,x,y)∂bl=∂J(W,b,x,y)∂zl∂zl∂bl=δl(8)

简单多了吧,我们只需要把δl 求出来,W,b 就 OK 了
仔细看下式子 (6), 我们的数据按网络结构正向传播到输出层的时候δL 可以求出来,我们能不能依靠δL 来求δL−1,δL−2,δL−3 呢?继续使用链式求导法可以得出:

δl=∂J(W,b,x,y)∂zl=∂J(W,b,x,y)∂zl+1∂zl+1∂zl=δl+1∂zl+1∂zl(9)

可见,想通过δl+1 来求δl 关键是看怎么求∂zl+1∂zl, 好,我们再来看 zl+1 和 zl 的关系,由式子 (2) 可以得出

zl+1=Wl+1al+bl+1=Wl+1σ(zl)+bl+1(10)

那么:

∂zl+1∂zl=(Wl+1)T⊙σ′(zl)(11)

(11)代入 (9) 中得到

δl=δl+1∂zl+1∂zl=(Wl+1)Tδl+1⊙σ′(zl)(12)

我们根据δL,再根据上式的递推关系就可以求出每层的δl, 也就可以求出每层的 W,b 的梯度了。

下面是 DNN BP 算法伪代码 (随机梯度下降法)

Max: 最大迭代次数
α: learning rate
{(x1,y1),(x2,y2),…,(xm,ym)}: 输入的样本对

for iter = 1 to Max
for i = 1 to m:
set a1=xi
for l=2 to L, 前向传播计算 al=σ(zl)=σ(Wlal−1+bl)
for l=L to 2, 反向传播计算δl=(Wl+1)Tδl+1⊙σ′(zl)
for l=2 to L, 更新第 l 层的 Wl,bl:
Wl=Wl−α∑i=1mδi,l(ai,l−1)T
bl=bl−α∑i=1mδi,l
检查 W,b 的变化值,如果小于停止迭代的阀值,就跳出循环
输出各层的 W,b, 结束。

CNN 的 BP 算法

现在再来看看 CNN 的 BP 算法,由于 CNN 可以分为卷积层,池化层和全连接层,全连接层和 DNN 一样,不需要再说了。主要看另外两层,首先看看卷积层。

在说 CNN 基本思想的时候我们已经给出了卷积层正向传播的公式了:

al=σ(zl)=σ(al−1∗Wl+bl)(13)

仔细看这个公式,和 DNN 正向传播的公式唯一的区别就有卷积操作∗, 注意这不是乘号* 我们还沿用和上述 DNN 相同的损失函数。公式 (7),(8),(9) 同样适用于卷积层, 但是 zl 和 zl−1 的关系变了:

zl=al−1∗Wl+bl=σ(zl−1)∗Wl+bl(14)

这里和 DNN 的区别就是卷积操作,对于含有卷积的式子求导,卷积核 (W) 被旋转 180 度,意思就是上下翻转,然后左右翻转,因此我们的公式 (12) 变成了:

δl−1=δl∂zl∂zl−1=δl∗rot180(Wl)⊙σ′(zl−1)(15)

对卷积求导原理感兴趣的同学可以自行百度。
有了对卷积求导的知识就可以很轻易写出:

∂J(W,b)∂Wl=∂J(W,b)∂zl∂zl∂Wl=δl∗rot180(al−1)(16)

刚刚说公式 (8) 同样适合卷积层其实不够严谨,对于 b 的梯度不能像 DNN 那样直接等于δl, 因为 b 是一个向量,而这里的因为经过了卷积求导后变成了一个三维的张量,所以可以近似地用一个误差向量代替:

∂J(W,b)∂bl=∑u,v(δl)u,v

到这里我们已经解决了如何求卷积层上的 W,b 的梯度了,就剩池化层了。
池化层上 W,b 的梯度怎么求呢?不对,基本思想里说过池化层压根就没有 W,b 呀。那是不是可以直接忽略池化层呢?不行!CNN 网络结构是一个整体,BP 算法运行时,数据流肯定会经过池化层,误差在经过池化层后也会发生相应的变化。那求它的什么呢?想想,卷积层在求δ时要依赖后一层的δ, 卷积层的后一层不正是池化层吗?所以我们要求出池化层的δ。

在基本思想里面讲到,前向传播时池化层一般的操作是 MAX 或 Average 等,我们现在是要从压缩过的误差δl 来还原一个较大区域的误差。我们首先把δl 所有的子矩阵大小还原成池化前的大小,如果用的是 MAX 操作的话,就把所有子矩阵各个池化局部的值放在之前做前向传播时得到最大值的位置。这个过程叫做 unsample。
举个例子就好理解了,假设δl 第 k 个子矩阵是:

δkl=(2846)

假设我们池化窗口大小是 2*2, 则将其还原成原来大小就是:

(0000028004600000)

这里假设我们之前前向传播时记录的最大值的位置分别是左上,右下,右上,左下,unsample 后就是:

(2000000804000060)

由于我们池化层是对卷积层的输出做池化的,所以在求δ时不同于卷积层和全连接层,链式求导时先是对上一层的 a 求偏导,再乘上上一层的 a 对 z 的偏导,公式如下:

δkl−1=∂J(W,b)∂akl−1∂akl−1∂zkl−1=upsample(δkl)⊙σ′(zkl−1)

概括下:

δl−1=upsample(δl)⊙σ′(zl−1)

这里不同的池化操作也对应着不同的 unsample 操作。
不禁感叹下,不管情况多么复杂,数学公式最后的解总是那么简洁。

后续会更新 CNN 在中文句子分类上的应用原理和 source coding

参考:
Neural Networks and Deep Learning
卷积神经网络 (CNN) 反向传播算法
https://blog.csdn.net/m0_37490039/article/details/79378143