往简单里说、CNN 只是多了卷积层、池化层和 FC 的 NN 而已,虽然卷积、池化对应的前向传导算法和反向传播算法的高效实现都很不平凡,但得益于 Tensorflow 的强大、我们可以在仅仅知道它们思想的前提下进行相应的实现,因为 Tensorflow 能够帮我们处理所有数学与技术上的细节
实现卷积层
回忆我们说过的卷积层和普通层的性质、不难发现它们的表现极其相似,区别大体上来说只在于如下三点:
- 普通层自身对数据的处理只有“激活”()这一个步骤,层与层(、)之间的数据传递则是通过权值矩阵、偏置量(、)和线性变换()来完成的;卷积层自身对数据的处理则多了“卷积”这个步骤(通常来说是先卷积再激活:)、同时层与层之间的数据传递是直接传递的()
- 卷积层自身多了 Kernel 这个属性并因此带来了诸如 Stride、Padding 等属性,不过与此同时、卷积层之间没有权值矩阵
- 卷积层和普通层的
shape
属性记录的东西不同,具体而言:- 普通层的
shape
记录着上个 Layer 和该 Layer 所含神经元的个数 - 卷积层的
shape
记录着上个卷积层的输出和该卷积层的 Kernel 的信息(注意卷积层的上一层必定还是卷积层)
- 普通层的
接下来就看看具体实现:
|
|
上述代码的最后几行对应着下述两个公式、这两个公式在 Tensorflow 里面有着直接对应的实现:
- 当 Padding 设置为 VALID 时,输出的高、宽分别为: 其中,符号“”代表着“向上取整”,stride 代表着步长
- 当 Padding 设置为 SAME 时,输出的高、宽分别为:
同时不难看出、上述代码其实没有把 CNN 的前向传导算法囊括进去,这是因为考虑到卷积层会利用到普通层的激活函数、所以期望能够合理复用代码。所以期望能够把上述代码定义的 ConvLayer 和前文重写的 Layer 整合在一起以成为具体用在 CNN 中的卷积层,为此我们需要利用到 Python 中一项比较高级的技术——元类(元类的介绍可以参见这里):
|
|
在定义好基类和元类后、定义实际应用在 CNN 中的卷积层就非常简洁了。以在深度学习中应用最广泛的 ReLU 卷积层为例:
|
|
实现池化层
池化层比起卷积层而言要更简单一点:对于最常见的两种池化——极大池化和平均池化而言,它们所做的只是取输入的极大值和均值而已、本身并没有可以更新的参数。是故对池化层而言,我们无需维护其 Kernel、而只用定义相应的池化方法(极大、平均)即可,因此我们要求用户在调用池化层时、只提供“高”和“宽”而不提供“Kernel 个数”
注意:Kernel 个数从数值上来说与输出频道个数一致,所以对于池化层的实现而言、我们应该直接用输入频道数来赋值 Kernel 数,因为池化不会改变数据的频道数
|
|
同样的,由于 Tensorflow 已经帮助我们做好了封装、我们可以直接调用相应的函数来完成极大池化和平均池化的实现:
|
|
实现 CNN 中的特殊层结构
在 CNN 中同样有着 Dropout 和 Normalize 这两种特殊层结构。它们的表现和 NN 中相应特殊层结构的表现是完全一致的,区别只在于作用的对象不同
我们知道,CNN 每一层数据的维度要比 NN 中每一层数据的维度多一维:一个典型的 NN 中每一层的数据通常是的,而 CNN 则通常是的、其中是当前数据的频道数。为了让适用于 NN 的特殊层结构适配于 CNN,一个自然而合理的做法就是将个频道的数据当做一个整体来处理、或说将 CNN 中个频道的数据放在一起并视为 NN 中的一个神经元,这样做的话就能通过简易的封装来直接利用上我们对 NN 定义的特殊层结构。封装的过程则仍要用到元类:
|
|
实现 LayerFactory
我们在前三节讲述了 CNN 中卷积层、池化层和特殊层的实现,这一节我们将介绍如何定义一个简单的工厂来“生产”NN 中的层和前文介绍的这些层以方便进行应用(与上个系列中生产优化器的工厂差不多):
|
|
以上是一些准备工作,如果由于特殊需求(比如想实验某种激活函数是否好用)实现了新的 Layer 的话、就需要更新上面对应的字典。
接下来看看核心的方法:
|
|
至此,所有 CNN 会用到的、和层结构相关的东西就已经全部实现完毕了,接下来只需在网络结构上做一些简单的更新后、CNN 的实现便能大功告成
扩展网络结构
将网络结构迁移到 Tensorflow 框架中并扩展出 CNN 的功能这个过程、虽然不算困难却也相当繁琐。本节将会节选出其中比较重要的部分进行说明,对于其余和上个系列实现的网络结构几乎一致的地方则不再进行注释或叙述
首先是初始化,由于我们使用的是 Tensorflow 框架、所以相应变量名的前面会一概加上“tf”两个字母:
|
|
然后我们要解决的就是上篇文章最后遗留下来的、在初始化各个权值矩阵时要把从初始化为 Numpy 数组改为初始化为 Tensorflow 数组、同时要注意兼容 CNN 的问题:
|
|
以上就是和 NN 中网络结构相比有比较大改动的地方、其余的部分则都是一些琐碎的细节。完整的代码可以参见这里,功能更为齐全、在许多细节上都进行了优化的版本则可以参见这里