二、计算机视觉的基础 -- 卷积神经网络(CNN)

说明:在解析 卷积神经网络(CNN) 之前,这里默认你已经对基本的 Bp神经网络有了比较清楚的理解,如激励函数、学习率、反向传播、梯度下降等基础概念不再进行解释。

        在上一篇关于Bp神经网络的文章结尾提到过:受限于Bp网络的结构本身,和粗暴的计算方式,它能够解决的问题是有限的,在图像问题,序列问题上的表现实在是有限。针对图像类的问题,大神们结合图像处理中的卷积化运算,改进了网络的架构,提出了 卷积神经网络(CNN) 这种神奇的架构,大大推动了计算机视觉的发展。

        这里必须提前说明一下,CNN这部分内容涉及到的数学知识和概念相对还是比较多的,我还是尽量用人话来进行解释,下面我们来一起手撕 卷积神经网络CNN 。

第 1 部分  图像与卷积

1.1 图像

        考虑到有些小伙伴从来没有了解过图像处理的相关问题,这里先对图像进行一下说明。

        首先来看一下图像在计算机里到底是什么 。我们知道世界上所有的颜色都可以用红、绿、蓝(R、G、B)三种颜色的组合而成,只是组合时这三种光的明暗程度各不相同。计算机通常把每种颜色的深浅度分为256个色阶,最小为 0(完全不发光) ,最大为 255(调至最亮)。而计算机中,图像的本质是 由各个像素点的颜色信息组成的一个数组。通常,灰度图,也就是我们常说的黑白图片,各个像素上只有明暗度的信息,没有彩色的信息,因此我们把灰度图作为一个只有 明暗通道 的三维数组(前两维是图像的高度和宽度,第三维是色彩通道) 。而彩色图片的每个像素上,则有三个颜色通道的信息(R、G、B),因此把彩色图片作为一个 三色彩通道 的三维数组,可以理解为彩色图是由红、绿、蓝三种颜色的明暗图叠加而成的。

    例如下面这张四个像素组成的黑白图像,可以表示为数组:\begin{bmatrix}0 & 85\\ 170 & 255 \end{bmatrix} 。右边彩色图像则可以表示为\begin{bmatrix}[255,128,192] & [128,128,64]\\ [128,255,128] & [255,255,128] \end{bmatrix} 这样的形式。

                                                                                                            

1.2 卷积

     1.2.1 卷积的数学算法

       理解了什么是图像我们再来看什么是卷积,这个概念听起来有点玄乎,我记得大学时候某门课里的某节内容提到了这个概念,我当时就把书合上了,看着就头疼。不过理解了以后其实也还好,我尽量解释的通俗一点。

        首先卷积分为两种:外卷积和内卷积。我们先来定义两个矩阵 A 和 B ,大小分别为 M x N 和 m x n , 这里的M>m,N>n 。我们定义 它们的内卷积 C = A\check{ * } B ,外卷积 F = A\hat{*} B ,则:

        对于 内卷积矩阵 C ,内卷积之后的大小变为了 [M-m+1 x N-n+1] ,矩阵C中的任意的一个元素  c_{i,j} 有以下关系:

        c_{i,j} = \sum_{s=1}^{m}\sum_{t=1}^{n}a_{i+m-s,j+n-t} \cdot b_{s,t}        

        上式中 , a 和 b 分别为矩阵 A 与 B 中的元素。根据上式计算出 C 中每一个 元素 c_{i,j} , 就可以得到内卷积以后的矩阵 C 。

        再来说一下外卷积,对于外卷积矩阵 F ,F = A\hat{*} B 运算后的大小为 [M+2m-2 x N+2n-2] ,别管为什么,就是这样,计算的方法如下:

        设 矩阵\hat{A}_{B} 中的元素  \hat{a}_{i,j} = \left\{\begin{matrix} a_{i-m+1,j-n+1} &,m\leqslant i\leqslant M+m-1, n\leqslant j\leqslant N+n-1\\ 0& ,else \end{matrix}\right.

        F = A\hat{*} B=\hat{A}_{B}\check{*}B

        太久没有学数学的朋友可能看着就晕了,但是没关系,这只是计算的方法,感兴趣的小伙伴可以自己写个程序来实现一下,代码不长。当然不感兴趣的朋友也不要紧,因为早就有人帮我们写好了,直接用就行。

 1.2.2 卷积的图像意义      

        这里 重点 讲一下卷积的在图像处理中的实际意义。我们假定有一张图片 A(根据之前的内容,A可以被当成是一个数组),再定义一个比 A 小的框 B(也是一个矩阵) ,那么在内卷积运算 A\check{ * } B 时,可以将A图片称为 被卷积的矩阵 ,而B可以称为 卷积核(kernel),一定要记住 卷积核(kernel)个概念。

        简单地来说,内卷积的过程就是用一个带有数值的 滑动框(也就是卷积核),在图片上滑动计算,来实现把将图片信息压缩的过程(貌似不是人话,往下看...)。还是举例说明,我们假设图片A为一个 5x5 像素的图片,B为一个 3x3 的卷积核:

                                         A=\begin{bmatrix} 3& 3&2 &1 &0 \\ 0& 0&1 &3 &1 \\ 3& 1& 2& 2& 3\\ 2& 0& 0& 2& 2\\ 2& 0& 0& 0&1 \end{bmatrix}                               B=\begin{bmatrix} 0& 1&2 \\ 2& 2& 0 \\ 0& 1& 2\end{bmatrix}       

        内卷积运算 A\check{ * } B 可以被理解为以下过程(图片来自百度):

        是不是瞬间觉得理解了,就是让划框B(也就是卷积核)在 A上一格一格的滑动,再用A中被框住的数据与卷积核B的数据一一相乘再求和就可以了。经过以上一通操作之后,我们已发现,得到了新的,大小为3x3的图片(上图中绿色的图片),这个绿色的矩阵就是内卷积后得到的结果。为什么要这么做?

        先来看卷积之前的图像A中包含的信息,A的大小为 5x5 ,因此描述A需要用到 25 个数来记录A的信息。卷积后图片的大小变成了 3x3 = 9 个数,得到这个图片则需要借助卷积核 B ,也是3x3 = 9 个数。总共是9+9=18个数,而这18个数所能够包含的信息近似等效于原图A中的25个数,因为很显然我们可以 逆向的 通过卷积核B将 卷积后得到的图片再展开为一个与 A 很类似的图像。说到这里你也许明白了,卷积后的 18 个数居然能够保存原本的 25 个数所含有的信息。同样的信息量,使用卷积活活少了 7 个数,多么的神奇... 利用卷积能够大大地降低表达信息的参数,加速进一步的计算过程,不要忘了,神经网络中的计算量都是及其庞大的,简化信息是十分必须要的。

        总结一下:卷积是一种既能保留原始数据特征,又能简化数据的计算方法。 

        那么外卷积是什么?你可能已经猜到了,就是内卷积的反向过程,是将简化的图像通过卷积核展开的过程,就不再细讲了,领会精神即可。

        这里还要稍微说明一下:上边的例子中,并不是原始的卷积计算方法,如果利用1.2.1中给出的原始的计算公式来计算的话,结果与图中将并不一致。这是因为上图是一种简化的卷积运算,并非最纯正、最原始的卷积过程,但是核心思想和实际意义是相同的。事实上目前主流的计算方法用的就是上图中的类卷积运算,计算公式为c_{i,j} = \sum_{s=1}^{m}\sum_{t=1}^{n}a_{i+s-1,j+t-1} \cdot b_{s,t},可以看到与原计算公式差别很小,只是元素相乘的次序不同。当然如果这段话你不明白我在说什么,完全无所谓,只是给特别细心的朋友解释一下,不懂请忽略···

1.3 采样

     1.3.1 上采样 与 下采样

        采样这个词听起来有点像是概率论中的说法,它也被称为 池化,一般可以分为 平均池化(average pooling)最大值池化(maximum pooling) 。实际上在图像处理过程中,用处很多,也很好理解。

        先来说一下 下采样,所谓下采样就是对一个图片进行压缩的过程 ,比如我们有一张 100x100 像素的图片,那么通过一个核为 2x2 的下采样,则可以将图片变为 50x50 像素 。也就是说,下采样是将图片信息进行压缩的一种方法,它在卷积神经网络中的意义在于简化信息,提高处理信息的速度。

        上采样,是下采样的逆过程,就是将一张图片放大为原图的一定倍数,比如 一张 100x100 的像素的图片,通过一个核为 2x2的上采样,就变成了 200x200 的图 ,它主要用于卷积神经网络的逆向传播中。

     1.3.2 上采样 与 下采样的计算方法

        下采样(池化):

        对 图像信息 A 的 下采样(池化)记为 down(A)  ,则计算过程如下:

        先将图片 A 分为 i x j 块 (这里选 i=2 ,j = 2,即核为 2x2),再分别求每块的平均值(或最大值),如下图:

        即 :down(\begin{bmatrix} 4& 5&1 &6 \\ 8& 1&7 &2 \\ 3& 3& 2&9\\ 1& 4& 3& 0\end{bmatrix}) = (\begin{bmatrix} 9& 8 \\ 7& 7 \end{bmatrix})

        [注] 这里所列出的为平均池化 ,最大池化就是将求平均值变为求最大值。

        上采样:

        对 图像信息 A 的 上采样记为 up(A)  ,则计算过程如下:

        up(A)=A\otimes 1_{i\times j} 

        \otimes 表示克罗内克积,1_{i\times j} 表示元素全为 1的 ixj 的矩阵。

        这里需要特别说一下克罗内克积:

        A\otimes B = \begin{bmatrix} a_{11}B &... & a_{1n}B\\ .& &. \\ .& &. \\ .& &. \\ a_{m1}B&... &a_{mn}B \end{bmatrix}

        

第 2 部分  卷积神经网络 -- CNN

2.1 卷积神经网络 CNN 概述

        现在我们已经知道了利用卷积能够实现压缩图像信息的作用,那么我们把卷积与神经网络结合起来,就是能够得到卷积神经网络,下文中将用简写 CNN 来代表。

     2.1.1 Bp 神经网络回顾

        首先我们来回顾一下 Bp 网络,假如我们把一张 28 x 28 像素的图像,作为Bp网络的输入,则首先需要将二维的数组展开为一维的向量,即 长度为 784 的向量 (维度:1 x 784)。现在我们设计一个隐藏层为两层,节点数分别为24x24x6=3456个和8x8x12=768个(先不管为什么设计为这么多),输出层节点为10个的Bp网络,先不管这个网络作用是什么,只来看看会发生什么。直接看下面的结构图:

        原谅我实在没有勇气把线都画出来,只画了前两层的连接。我们来计算一下这个网络需要多少个权值和阈值才能表达这个网络。首先 input 层到 h_1 层的连接数(权值数)为784*3456=2709504,h_1的阈值数与节点数相同,为3456个,h_1与h_2之间的连接数3456*786=2716416个,阈值786个,h_2 到 output 的连接数为786*10=7860,阈值10个。累加可知,共5438032个参数,一个如此简单的网络,居然需要 500多万 个参数,想要训练这样一个网络,可以实在是困难,至少对于一台普通的用CPU跑的程序来说,那是门儿都没有···

        那么这个网络怎样才能升级一下,变成一个卷积神经网络,睁大你的眼睛,我要开始变形了...

     2.1.2 CNN 中的输入层

        这里主要用类比Bp的方式来改造网络。首先来改造输入层(input),我们当前的输入是一个 28x28 的图像,如果不把它展开为长度784的向量,而是直接作为一个整体输入给网络,则输入层节点数就变成了一个,如下图:

        这里要注意,图中的方形代表一张图片,圆形代表一个普通的神经元节点,以后默认这样表示

        那么 这个图片 要如何与 h_1 层进行连接呢?答案是通过卷积运算,将 h_1 层也变成图片。这里需要说明一下,我们知道在Bp网络中,两个节点之间的连接参数是权值(一个具体的实数),它只能连接两个普通的节点,进行乘法运算。那么两个二维图片之间怎么连?当然是用卷积,因为2维的图片卷积后还是一个2维的图片,只是每个维度的大小变了。卷积运算过程中的卷积核(滑动框),就相当于是Bp网络中两节点之间的权值。只不过Bp的权值是1维的,而CNN中的卷积核是2维的,他们扮演的角色是相同的。现在你可能理解了,CNN的基本思路是用二维的图片做神经元节点,用二维的卷积核来做权值参数,一个卷积核就相当于是一个权值。

     2.1.3 CNN 中的卷积层与池化层

        基于以上思路,我们来改造 h_1 层 。我们选取 6 个大小为 5x5 的卷积核(卷积核中的值先随机来赋)来作为这一层的权值,那么对输入图片进行 6 次卷积运算之后就能够得到 6 张大小为 24x24 的图,得到的每一张图都被称为特征图。也就是说 当前层有几个卷积核,就有几个特征图。改造后的网络如下:

        改造后的 h_1 层变为了 6张 24x24 的特征图。我们来分析一下这 6 张特征图,首先每张图中的每个像素其实都是一个神经元节点,那么这一层所含有的神经元数就是 24x24x6=3456 个,有没有发现恰好等于之前设计的 Bp 网络中该层的节点数(这个不是什么神奇的操作,只是故意这么设计,来跟Bp作比较)。也就是说改造前与改造后的网络在这一层所拥有的信息量是一样的,而权值的个数由Bp网络的 2709504 个变成了 6x5x5=150个(6个卷积核,每个卷积核5x5=25个参数)。这是一个上万倍的差距,利用卷积大大减少了网络中参数的数量。这是因为同一特征图上的节点的信息,取决于同一个卷积核,也就是说它们的 权值是共享 的,而共享权值 就是卷积网络的核心思想。改造后的每一个特征图可以类比为 Bp 网络中的一个节点,改造后的这一层也就被叫做是 卷积层

        这一层的任务到这里其实还没有结束,跟 Bp 网络中一样,我们还要给每个特征图加一个阈值矩阵(大小与特征图相同,阈值矩阵中所有的值都是相同的,起初可以全部赋值为0),然后再用 激励函数 处理一下,这个过程跟 Bp 是一样一样的,就不再赘述了。

        下面来看 CNN 中的另一个很重要的层:采样层(也叫池化层),它的功能是减少上一层特征图的信息量,使特征图的信息进一步被压缩,以便更快更好地训练和使用神经网络。实现的方法就是对上一层的每一个特征图做下采样计算,算法上一部分已经说明。经过池化层之后,特征图的数量不变,尺寸变小。这里需要强调的是,池化过程会使特征一定程度上的丢失,因此如何选取合适的池化层参数,还要小伙伴们在之后的学习中不断总结。

        回到刚才的网络,现在我们给已经改造成卷积层的 h_1 加一个池化层h_1_s,池化的核的大小设为 2x2 ,相当于将原本 24x24 的特征图缩小为了 12x12 ,很好理解对吧。来看一下加上池化层的网络:

        下面老看一下如何把 h_2 层也变成卷积层,当然还是在上一层的基础上,也就是 h_1_s 层上做卷积运算,但是这里稍微有点不同。在之前 h_1 的改造过程中,由于h_1的上一层就是输入层,且输入的图片只有一张,所以每一张特征图的信息完全由输入层输入的这一张图像分别跟不同的卷积核做卷积运算就可以。但是对于 h_2 层来说,它的上一层有 6 张特征图,因此 h_2 的每一张特征图都必须包含上一层每一个特征图中的信息,如何操作呢?我们来看一下,现在我们将 h_2 层的特征图数量设计为 12 张 ,则需要的卷积核为 12x6 = 72个,画个图就明白了:

        可以看到,我们将 h_2 层也改造成了 有12个特征图的卷积层。这里只画出了 h_2 的第一个特征图的连线,每个连线代表进行一次卷积运算,也就是说 h_2 的第一张特征图需要对 h_1_s 中的6个特征图都进行一次卷积运算,自然也就需要 6 个对应的卷积核,再将卷积后得到的6个矩阵直接加起来(这里说的加是指矩阵相加),就可以得到 h_2 的第一张特征图。以此类推,分别计算出 h_2 层中的其他特征图,就得到了完整的12个特征图信息,共需要72个卷积核。跟刚才一样,我们选取5x5的卷积核和2x2的池化核,再给每个特征图加上阈值矩阵,并且用 激励函数 处理,再进行下采样,就可以得到下面的网络结构:

        上图中的 conv 代表卷积运算,pool代表下采样运算,可以看到最终经过两次卷积运算和两次下采样运算,使输入层和隐藏层全部变为了 CNN 需要的形式,最后一层隐藏层 h_2_s 的输出结果为 12 张 4x4 的特征图。事实上,这个网络到这儿就已经成型了,输出层是不需要变为特征图的形式,因为通常情况下,输出层的结果为一组向量,与数据集中真实值进行求损失,用以训练网络。

     2.1.4 CNN 中的输出层

        这里要特别说一下  h_2_s 层的特征图如何与输出层(output )进行连接。显然特征图作为2维的矩阵是无法与一维的输出向量直接进行连接,因此需要对特征图进行展开,使其变成一个一维向量,做法其实简单粗暴,直接把特征图(特征矩阵)中的每一行都强行拉平为一行(拉平的例子在本段下方),这样总共就能够得到长度为 4x4x12=192 的向量,相当于一个节点数为 192 Bp网络层 。这样就能够与输出层进行连接,连接方式还是用到 权值、阈值以及激励函数。这种连接方式被称为 全连接 ,输出层(output )被称为是一层全连接层。事实上,CNN中经常会出现多层全连接层,全连接层本质上就是连接在卷积层后边的 Bp 网络。

        【注:】特征图展开过程示例: \left.\begin{matrix} \begin{bmatrix} 1 & 2&3 \\ 4& 5&6 \\ 7& 8& 9 \end{bmatrix} \\\begin{bmatrix} 10 & 11&12 \\ 13& 14&15 \\ 16& 17& 18 \end{bmatrix} \end{matrix}\right\} \Rightarrow [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18]

        以上就是一个完整的 CNN 模型。

     2.1.5 本节小节

        总结:

        a. CNN 是用来处理2维图像问题的基础架构,它的核心思想是利用权值共享来简化网络中的参数,以实现对图像问题的解决。

        b. CNN中的卷积层主要用来提取输入数据的特征,全连接层则是对这些特征的组合,以拟合出逼近现实的网络。

        c. 全连接层就是连接在卷基层和采样层之后的 Bp 网络层 , 可以只有一层,仅作为输出层。

        d. 决定网络的参数为卷基层的 卷积核(kernel) 和 阈值矩阵(b)中的参数。

2.2 卷积神经网络 CNN 的反向传播

        正向传播不用说了,就是按照网络的架构,从输入端计算至输出端。重点来看一下反向传播,还是类比 Bp 网络。应该还记得,Bp 网络反向传播的信息是 损失(Loss),那么同样的 CNN 中反向传播的当然也是损失,整个反向传播的过程与 Bp 也很类似,也是先求各个 网络参数(权值与阈值) 的梯度 , 再将这些参数加上梯度,使其更新即可。

        不过特别需要强调的是,与 Bp 网络不同的是, CNN 中利用了多次卷积运算和采样运算,因此反向传播时,要将这一过程也反向运算,反向运算的方法你应该猜到了,就是 外卷积 和 上采样。

        这节说到这就可以了,你应该有个大概的思路了,但是凭借我这些混乱的语言和笼统的总结,应该还是无从下手。但是没关系,你只需要记住这两个点就好:

        a. CNN 反向传播的信息也是损失(Loss),损失由损失函数计算得到。

        b. 卷积运算的反向传播过程为外卷积,下采样的反向传播过程为上采样。

2.3 卷积神经网络 CNN 的数学算法

        之前两节主要是介绍网络的原理和构成,以及反向传播的计算思想。下面来手撕CNN的算法过程,直接上公式,对整个过程进行一个推演。首先我们来约定一些符号:

        h :前向传播过程中,某个特征图的信息(一个特征图矩阵)。

        \delta :反向传播过程中,某个特征图的误差信息(某个特征图的误差矩阵)

        N :训练样本 的 样本数量

        l :样本的编号(作为上标),如 x^{l} 表示训练集中,第 l 个样本的输入信息 。

        k :CNN 的层编号, 如 h_{k} 表示第 k 层的信息。

        j :当前这一层中,特征图的编号,如 h_{k,j} 表示第 k 层的第 j 个特征图的信息。

        W_{ij}^{k}  :第 k 层卷基层上,连接 (k-1)层第 i 个特征图 与 k 层第 j 个特征图的卷积核(权值矩阵)。

        b_{j}^{k} : k 层第 j 个特征图的阈值矩阵。

        \sigma () :激励函数。

        {\sigma}' () :激励函数的导数。

        rot180 () :把一个矩阵水平翻转一次,再垂直翻转一次。如 rot180(\begin{bmatrix} 1 & 2\\ 3 & 4 \end{bmatrix} ) = \begin{bmatrix} 4 & 3\\ 2 & 1 \end{bmatrix} 。

        .* :矩阵按位乘积,也叫阿达马积。如  \begin{bmatrix} 1 & 2\\ 3 & 4 \end{bmatrix} .*\begin{bmatrix} 0 & 1\\ 3 & 2 \end{bmatrix} = \begin{bmatrix} 1\times 0 & 2\times 1\\ 3\times 3 & 4\times 2 \end{bmatrix} = \begin{bmatrix} 0 & 2\\ 9 & 8 \end{bmatrix} 。

        前向传播计算:

        若第 k 层为卷基层,则该层第 j 个特征图矩阵为:

        h_{k,j}^{l} = f(\sum_{i}h_{k-1,i}^{l} \breve{*}W_{ij}^{k} + b_{j}^{k})

        若第 k 层为采样层,则该层第 j 个特征图矩阵为:

        h_{k,j}^{l} = down(h_{k-1,j}^{l})

        反向传播计算:

        若第 k 层为卷基层,则该层第 j 个特征图的误差矩阵为:

        \delta _{k,j}^{l} = {f}'(\sum_{i}h_{k-1,i}^{l} \breve{*}W_{ij}^{k} + b_{j}^{k}).*up(\delta _{k+1,j}^{l})

        若第 k 层为采样层,则该层第 j 个特征图的误差矩阵为:

        \delta _{k,j}^{l} =\delta _{k+1,j}^{l}\hat{*}rot180(W_{j}^{k+1})

        梯度计算:

        卷积核梯度 :

        dW_{ij}^{k} = \frac{\partial L_{N}}{\partial W_{ij}^{k}}=\sum_{l=1}^{N}\delta _{k,j}^{l}\breve{*}h_{k-1,i}^{l}

        阈值矩阵梯度 :

        db_{j}^{k} = \frac{\partial L_{N}}{\partial b_{j}^{k}}=\sum_{l=1}^{N}\delta _{k,j}^{l}

        计算得到卷积核与阈值矩阵的梯度后,将其乘以学习率,更新原来的阈值与卷积核即可。

 

第 3 部分  CNN 实例 -- 识别手写数字

        利用神经网络来识别手写数字,应该是我们很自然能够想到的一个问题。这也是神经网络这个领域中最经典的问题之一,因为这个问题相对简单,并且非常能够体现神经网络的特点。

        下面就带小伙伴们来看一看如何利用神经网络来实现一个具体地功能。这里主要讲解一下 python 版的实现过程。

3.1 准备数据

        这里用到的数据主要是 MNIST 手写数字集 ,这个数据集在 AI 界的著名程度就像数学里的乘法口诀表,可以说任何一个做 AI 的工程师手里,都有这个数据集。

        下面来简单介绍一下 mnist 数据集 ,它包含 60000 张 28x28像素的手写数字(0-9)图片,并且都已归类,每张图的数据都有自己的标签。此外还有 10000张标签好的图片,作为测试样本。小伙伴们可以从 这个网址 找资源下载(如果资源有问题,可以给我发邮箱 542139844@qq.com)。下载后是一个压缩文件,将其解压缩后,得到一个文件夹,名称为mnist,打开后里边有4个压缩文件,如下图:

        这里我建议把这个文件夹单独放到一个地方,作为储备,因为以后经常会用到。把这 4 个文件都解压出来,得到下面几个文件:

        解压得到的四个文件就是可以供 python 直接调用的数据集,具体怎么使用,大家可以看后边的代码。

3.2 设计卷积神经网络(CNN)

        1. 输入层:

        由于 mnist 中提供的数据为手写数字的灰度图,因此每张图片只有一个通道(灰度通道),因此输入层的节点数为 1 ,即只有一个图像矩阵作为输入。

        2. 卷基层与采样层:

        这里采用与之前例子中相同的结构,即 --卷基层--采样层--卷基层--采样层 的形式,两层卷基层的卷积核均为 5x5 ,采样的核都为 2x2 。

        其中第一层卷基层的 特征图 设计为数为 6 张 ,第二层设计为 12 张 。结构与2.1.3中完全相同。

        3. 输出层:

        由于 mnist 中的数字为 0-9 ,共十个类别,因此输出层的节点设为 10 个 ,用 向量[1,0,0,0,0,0,0,0,0,0] 表示 数字0 ,用[0,1,0,0,0,0,0,0,0,1] 表示数字1 ,以此类推...

        设计完成后的网络结构为:

3.3 创建项目

        a. 这里先创建一个文件夹 , 名称为 CNN_mnist 。

        b. 将刚才得到的mnist数据的文件夹整体考入到 CNN_mnist 文件夹中。

        c. 再创建一个文件夹,命名为 KB (用来存放网络模型的参数 卷积核与阈值)。

        d. 在项目文件夹下分别创建以下 10 个 .py 文件 (每个文件的代码在图片附在下方)。

            -- ActivationFunction.py       // 激励函数 代码

            -- Calculation_Convolve.py  // 卷积运算代码

            -- Calculation_Sample.py     // 采样运算代码

            -- cnnff.py                             // 前向传播代码

            -- cnnbp.py                           // 反向传播代码

            -- cnnpredict.py                    // 推测代码

            -- Layers_ICSBp.py             // 用来创建 CNN 各层(输入层、卷积层、采样层、全连接层)的 代码

            -- load_mnist.py                   // 推测代码

            -- ReadWrite_Array.py         // 保存和读取 模型参数(卷积核 与 阈值)的代码

            -- run.py                               // 运行文件代码。(主文件)

        ActivationFunction.py 代码 :

# 激励函数 AF

import numpy as np;

def AF(p,AFKind):
	if AFKind==1:        # sigmod		
		return 1.0/(1.0+np.exp(-p))
	elif AFKind == 2:    # tanh
		return (np.exp(p) - np.exp(-p))/(np.exp(p) + np.exp(-p))
	elif AFKind == 3:    # ReLU
		return np.where(p<0,0,p)
		
def dAF(p,AFKind):
	if AFKind==1:        # sigmod
		return (np.exp(-p))/((1.0+np.exp(-p))**2)
	elif AFKind == 2:    # tanh
		return 1 - AF(p,2)*AF(p,2)
	elif AFKind == 3:    # ReLU
		return np.where(p<0,0,1)

        Calculation_Convolve.py 代码 :

import numpy as np;
import scipy.signal as sc;

a = np.array([[1,2,3],[4,5,6],[7,8,9]])

b = np.array([[2,3],[4,5]])

c = sc.convolve(a, b,'full')   # 外卷积

d = sc.convolve(a,b,'valid')   # 内卷积

        Calculation_Sample.py 代码 :

import numpy as np;
import scipy.signal as sc;

a = (np.arange(36)+1).reshape(6,6)

def downsample_averge(a,i,j):  # 平均下采样 a 为被采样的数组(numpy.array格式), i 为分块的行数, j 为分块的列数

	b = np.ones((i,j))
	c = sc.convolve(a,b/(i*j),'valid')   
	d = c[0:c.shape[0]:i,0:c.shape[1]:j]
	return d

def upsample_averge(a,i,j):    # 平均上采样 a 为被采样的数组(numpy.array格式), i 为上采样扩展行数, j 为上采样扩展列数 
	d = a.repeat(i,axis = 0)
	d = d.repeat(j,axis = 1)		
	return d

        cnnff.py 代码 :

# 神经网络的前向传播 cnnff
import numpy as np
import struct
import matplotlib.pyplot as plt
import random
import pickle
import scipy.signal as sc;

import Calculation_Sample
import ActivationFunction as AF

def cnnff(layers,x,AFKind):           # layers 为 卷积网络(一个包含网络结构信息的列表) ; x 为待训练样本(图像信息 batch_x(batchsize,28,28)) ;AFKind 激励函数类型
	n = len(layers)    # 层数
	
	#### Input 层 ####
	
	batchsize = x.shape[0]
	
	layers[0].a = x.reshape((((batchsize,layers[0].inputmaps,layers[0].inputmapsize,layers[0].inputmapsize))))	
	
	#print(batchsize)
	
	
	#### C - S 层 ####
	for c in range(1,n-1,1):         
	
		if layers[c].type_L == 'c':                     # 卷基层的前向计算		
			for b in range(0,batchsize,1):
				for j in range(0,layers[c].outputmaps,1):
					z = np.zeros((layers[c].outputmapsize,layers[c].outputmapsize))
					for i in range(0,layers[c].inputmaps,1):
						zz = sc.convolve(layers[c-1].a[b][i],layers[c].k[i][j],'valid')
						z = z + zz
						
					layers[c].ai[b][j] = z + layers[c].b[j]
					layers[c].a[b][j] = AF.AF(layers[c].ai[b][j],AFKind)
					

		elif layers[c].type_L == 's':                   # 采样层的前向计算			
			for b in range(0,batchsize,1):
				for j in range(0,layers[c].outputmaps,1):
					layers[c].a[b][j] = Calculation_Sample.downsample_averge(layers[c-1].a[b][j],layers[c].scale,layers[c].scale)

	
	#### Bp 层 ####	
	layers[n-1].fv = layers[n-2].a.swapaxes(2,3).reshape((batchsize,layers[n-1].fvnum))
			
	layers[n-1].oi = np.dot(layers[n-1].ffw,layers[n-1].fv.swapaxes(0,1)).swapaxes(0,1) + np.tile(layers[n-1].ffb,(batchsize,1))
	layers[n-1].o = AF.AF(layers[n-1].oi,AFKind)

        cnnbp.py 代码 :

# 神经网络的前向传播 cnnbp
import numpy as np
import struct
import matplotlib.pyplot as plt
import random
import pickle
import scipy.signal as sc;

import Calculation_Sample
import ActivationFunction as AF
import ReadWrite_Array as rw

def rot180(p):
	
	t = p[::-1]
	t = t.swapaxes(0,1)
	t = t[::-1]
	t = t.swapaxes(0,1)	
	return t
	

def cnnbp(layers,y,AFKind,alpha):   # layers 为 卷积网络(一个包含网络结构信息的列表) ; y 为待训练样本(图像信息 batch_y(batchsize,onum)) ;AFKind 激励函数类型 ; L 均方差 ;alpha 学习率
	n = len(layers)    # 层数
	batchsize = layers[0].batchsize
	
	#### Bp层 ####
	layers[n-1].e = layers[n-1].o - y
		
	L = (layers[n-1].e * layers[n-1].e).sum()/batchsize/2.0
	
	layers[n-1].od = layers[n-1].e * AF.dAF(layers[n-1].oi,AFKind)
	
	layers[n-1].fvd = np.dot(layers[n-1].od,layers[n-1].ffw)
	
	
	#### C - S 层 ####

	layers[n-2].ab = layers[n-1].fvd.reshape((((int(batchsize),int(layers[n-2].outputmaps),int(layers[n-2].outputmapsize),int(layers[n-2].outputmapsize))))) # 最后一层 S 层
	
	for c in range(n-3,0,-1):
		
		if layers[c].type_L == 'c':
			for b in range(0,batchsize,1):
				
				for j in range(0,layers[c].outputmaps,1):
				
					xx = AF.dAF(layers[c].ai[b][j],AFKind)
					zz = Calculation_Sample.upsample_averge(layers[c+1].ab[b][j],layers[c+1].scale,layers[c+1].scale) / (layers[c+1].scale**2)
					layers[c].ab[b][j] = xx*zz
					
			
		elif layers[c].type_L == 's':
			for b in range(0,batchsize,1):
				
				for i in range(0,layers[c].outputmaps,1):
					
					z = np.zeros((int(layers[c].outputmapsize),int(layers[c].outputmapsize)))
					
					for j in range(0,layers[c+1].outputmaps,1):
						
						z = z + sc.convolve(layers[c+1].ab[b][j],rot180(layers[c+1].k[i][j]),'full')
					
					layers[c].ab[b][i] = z
					
	#### 计算梯度 ####
	
	for c in range(1,n-1,1):
		if layers[c].type_L == 'c':
			
			for j in range(0,layers[c].outputmaps,1):
				for i in range(0,layers[c].inputmaps,1):
				
					kk = np.zeros((layers[c].kernelSize,layers[c].kernelSize))
					for b in range(0,batchsize,1):
						aa = layers[c-1].a[b][i]
						dd = layers[c].ab[b][j]
						kk = kk + sc.convolve(rot180(aa),dd,'valid')
					kk = kk/batchsize
					
					layers[c].dk[i][j] = kk
					
				bb = layers[c].ab.swapaxes(0,1)
				
				layers[c].db[j] = bb[j].sum()/batchsize
				
	
	layers[n-1].dffw = np.dot(layers[n-1].od.T,layers[n-1].fv)/batchsize	
	layers[n-1].dffb = layers[n-1].od.mean(axis=0)
		
	#### 更新 K B ####
	for c in range(1,n-1,1):
	
		if layers[c].type_L == 'c':
		
			path_c = layers[c].type_L + str(int((c+1)/2))
			
			layers[c].k = layers[c].k - alpha*layers[c].dk
			layers[c].b = layers[c].b - alpha*layers[c].db
						
	layers[n-1].ffw = layers[n-1].ffw - alpha*layers[n-1].dffw 
	layers[n-1].ffb = layers[n-1].ffb - alpha*layers[n-1].dffb
				
	return L	# 返回值为均方差损失				
						

        cnnpredict.py 代码 :

# 神经网络的前向传播 cnnpredict

import numpy as np
import struct
import matplotlib.pyplot as plt
import random
import pickle
import scipy.signal as sc;

import Calculation_Sample
import ActivationFunction as AF

def predict(layers,x,AFKind):           # layers 为 卷积网络(一个包含网络结构信息的列表) ; x 为待训练样本(图像信息 batch_x(batchsize,28,28)) ;AFKind 激励函数类型
	n = len(layers)    # 层数
	
	#### Input 层 ####
	
	batchsize = x.shape[0]
	
	layers[0].a = x.reshape((((batchsize,layers[0].inputmaps,layers[0].inputmapsize,layers[0].inputmapsize))))	
	
	#### C - S 层 ####
	for c in range(1,n-1,1):         
		
		if layers[c].type_L == 'c':                     # 卷基层的前向计算	
		
			layers[c].ai = np.zeros((((batchsize,layers[c].outputmaps,layers[c].outputmapsize,layers[c].outputmapsize))))
			layers[c].a = np.zeros((((batchsize,layers[c].outputmaps,layers[c].outputmapsize,layers[c].outputmapsize))))
			
			for b in range(0,batchsize,1):
				for j in range(0,layers[c].outputmaps,1):
					z = np.zeros((layers[c].outputmapsize,layers[c].outputmapsize))
					for i in range(0,layers[c].inputmaps,1):
						zz = sc.convolve(layers[c-1].a[b][i],layers[c].k[i][j],'valid')
						z = z + zz
					
					
					layers[c].ai[b][j] = z + layers[c].b[j]
					layers[c].a[b][j] = AF.AF(layers[c].ai[b][j],AFKind)

		elif layers[c].type_L == 's':                   # 采样层的前向计算
			layers[c].a = np.zeros((((int(batchsize),int(layers[c].outputmaps),int(layers[c].outputmapsize),int(layers[c].outputmapsize)))))
			for b in range(0,batchsize,1):
				for j in range(0,layers[c].outputmaps,1):
					layers[c].a[b][j] = Calculation_Sample.downsample_averge(layers[c-1].a[b][j],layers[c].scale,layers[c].scale)
	
	#### Bp 层 ####	
	layers[n-1].fv = layers[n-2].a.swapaxes(2,3).reshape((batchsize,layers[n-1].fvnum))
			
	layers[n-1].oi = np.dot(layers[n-1].ffw,layers[n-1].fv.swapaxes(0,1)).swapaxes(0,1) + np.tile(layers[n-1].ffb,(batchsize,1))
	layers[n-1].o = AF.AF(layers[n-1].oi,AFKind)
	
	return layers[n-1].o

        Layers_ICSBp.py 代码 :

# 输入层 卷基层 采样层 Bp层

import numpy as np
import struct
import matplotlib.pyplot as plt
import random
import pickle
import ReadWrite_Array as rw

class Layer_Input(object):
	def __init__(self):
		self.name = 'L_input'
		self.inputmaps = 1
		self.inputmapsize = 28
		self.batchsize = 1
		
		self.Path_KB = 'f'
		
		self.I_in = np.zeros(((self.batchsize,self.inputmapsize,self.inputmapsize)))
	
	def setup(self):
		self.inputmapsize = self.I_in.shape[2]
		self.batchsize = self.I_in.shape[0]
		self.outputmaps = self.inputmaps
		self.outputmapsize = self.inputmapsize
		self.a = self.I_in.reshape((((self.batchsize,self.inputmaps,self.inputmapsize,self.inputmapsize))))

		
class Layer_Convolve(object):
	def __init__(self):
		self.name = 'c'
		self.type_L = 'c'
		
		self.style_getkb = 1
		
		self.Path_KB = 'f'
		
		self.inputmaps = 1
		self.inputmapsize = 1
		
		self.outputmaps = 1
		self.outputmapsize = 1
		
		self.batchsize = 1
		
		self.kernelSize = 1
		
		self.fan_out = 1
		self.fan_in = 1
		
		self.C_in = np.zeros((((self.batchsize,self.inputmaps,self.inputmapsize,self.inputmapsize))))
		
	def setup(self):
		self.batchsize = self.C_in.shape[0]
		
		self.inputmaps = self.C_in.shape[1]
		self.inputmapsize = self.C_in.shape[2]
		
		self.outputmapsize = self.inputmapsize-self.kernelSize+1
		
		self.fan_in = self.inputmaps*self.kernelSize*self.kernelSize
		self.fan_out = self.outputmaps*self.kernelSize*self.kernelSize
		
		self.a = np.zeros((((self.batchsize,self.outputmaps,self.outputmapsize,self.outputmapsize))))
		self.ai = np.zeros((((self.batchsize,self.outputmaps,self.outputmapsize,self.outputmapsize))))
		self.ab = np.zeros((((self.batchsize,self.outputmaps,self.outputmapsize,self.outputmapsize))))
		
		#self.k = np.zeros((((self.inputmaps,self.outputmaps,self.kernelSize,self.kernelSize))))
		self.k = np.ones((((self.inputmaps,self.outputmaps,self.kernelSize,self.kernelSize))))*0.1
		
		self.dk = np.zeros((((self.inputmaps,self.outputmaps,self.kernelSize,self.kernelSize))))
		
		self.b = np.zeros(self.outputmaps)
		self.db = np.zeros(self.outputmaps)
		
		#### 设置 KB ####
		if self.style_getkb == 1:   # 随机设定
			self.k = (np.random.rand(self.inputmaps,self.outputmaps,self.kernelSize,self.kernelSize))-0.5
			
			
		elif self.style_getkb == 2:
			self.k = rw.ReadArray_4D(self.Path_KB+self.name+'k',self.inputmaps,self.outputmaps)
			self.b = rw.ReadArray_1D(self.Path_KB+self.name+'b.txt')

			
class Layer_Sample(object):
	
	def __init__(self):
		self.name = 's'
		self.type_L = 's'
		
		self.inputmaps = 1
		self.inputmapsize = 1
		
		self.outputmaps = 1
		self.outputmapsize = 1
		
		self.batchsize = 1
		
		self.scale = 1
		
		self.S_in = np.zeros((((self.batchsize,self.inputmaps,self.inputmapsize,self.inputmapsize))))
		
	def setup(self):
		self.batchsize     = self.S_in.shape[0]
		
		self.inputmaps     = self.S_in.shape[1]
		self.inputmapsize  = self.S_in.shape[2]
		
		self.outputmaps = self.inputmaps		
		self.outputmapsize = self.inputmapsize/self.scale
		
		self.a =  np.zeros((((self.batchsize,self.outputmaps,int(self.outputmapsize),int(self.outputmapsize)))))
		self.ab = np.zeros((((self.batchsize,self.outputmaps,int(self.outputmapsize),int(self.outputmapsize)))))


class Layer_Bp(object):
	
	def __init__(self):
		self.name = 'bp'
		self.type_L = 'bp'
		
		self.style_getkb = 1
		
		self.Path_KB = 'f'
		
		self.inputmaps = 1
		self.inputmapsize = 1
		
		self.onum = 10
		self.fvnum = 1
		
		self.batchsize = 1
			
		self.Bp_in = np.zeros((((self.batchsize,self.inputmaps,self.inputmapsize,self.inputmapsize))))
		
	def setup(self):
		self.batchsize     = self.Bp_in.shape[0]		
		self.inputmaps     = self.Bp_in.shape[1]
		self.inputmapsize  = self.Bp_in.shape[2]
		
		self.fvnum = self.inputmaps*self.inputmapsize*self.inputmapsize
		
		self.e = np.zeros((self.batchsize,self.onum))
		
		self.fv = np.zeros((self.batchsize,self.fvnum)) 		
		self.fvd = np.zeros((self.batchsize,self.fvnum))
		
		self.o = np.zeros((self.batchsize,self.onum))
		self.oi = np.zeros((self.batchsize,self.onum))
		self.od = np.zeros((self.batchsize,self.onum))
		
		self.ffb = np.zeros(self.onum)
		self.dffb = np.zeros(self.onum)
		
		#self.ffw = np.zeros((self.onum,self.fvnum))
		self.ffw = np.ones((self.onum,self.fvnum))*0.1
		
		self.dffw = np.zeros((self.onum,self.fvnum))
		
		#### 设置 K b ####
		
		if self.style_getkb == 1:   # 随机设定
			self.ffw = (np.random.rand(self.onum,self.fvnum))-0.5
			
			
		elif self.style_getkb == 2:
			self.ffw = rw.ReadArray_2D(self.Path_KB + 'ffw.txt')
			self.ffb = rw.ReadArray_1D(self.Path_KB + 'ffb.txt')

        load_mnist.py 代码 :

# 读取mnist数据

import numpy as np
import struct
import matplotlib.pyplot as plt
import random
import pickle

#############################
onum = 10
#############################

def read_images(filename):                     # 读取图像信息,返回array数组
	binfile = open(filename, 'rb')
	buf = binfile.read()
	index = 0
	magic, img_num, numRows, numColums = struct.unpack_from('>IIII', buf, index)	
	#print (magic, ' ', img_num, ' ', numRows, ' ', numColums)
	t = np.zeros((img_num, numRows * numColums))
	index += struct.calcsize('>IIII')
	for i in range(img_num):
		im = struct.unpack_from('>784B', buf, index)
		index += struct.calcsize('>784B')
		im = np.array(im)
		im = im.reshape(1, numRows * numColums)
		t[ i , : ] = im	
	t = t.reshape(((img_num,numRows,numColums)))
	return t
	
def read_labels(filename):                      # 读取数字信息,返回array数组
	binfile = open(filename, 'rb')
	index = 0
	buf = binfile.read()
	binfile.close()
	magic, label_num = struct.unpack_from('>II', buf, index)
	t = np.zeros((label_num,onum))
	index += struct.calcsize('>II')
	for i in range(label_num):
		# for x in xrange(2000):
		label_item = int(struct.unpack_from('>B', buf, index)[0])
		t[i, label_item:(label_item+1)] = [1]
		index += struct.calcsize('>B')
	return t

        ReadWrite_Array.py 代码 :

# txt文件操作
import numpy as np

def ReadArray_1D(path):   #   读取1维矩阵。  path:文件路径
	f = open(path,'r')
	a = f.readline()
	p = [float(i) for i in a.split()]
	p = np.array(p)
	f.close()
	return p



def ReadArray_2D(path):   #   读取2维矩阵。  path:文件路径
	f = open(path,'r')
	p   = []	
	while True:
		a = f.readline()
		if a == '' or a=='\n':
			f.close()
			break
		p_r = [float(i) for i in a.split()]
		p.append(p_r)
	p = np.array(p)
	f.close()
	return p

def ReadArray_3D(path):   #   读取3维矩阵。  path:文件路径
	f = open(path,'r')
	p_r = []
	p = []
	while True:
		a = f.readline()
		if a == '':
			if p_r != []:
				p.append(p_r)
				break			
			elif p_r == []:		
				break
			
		elif a == '\n':
			if p_r != []:
				p.append(p_r)
				p_r = []
			else:		
				pass

		else:	
			p_g = [float(i) for i in a.split()]
			p_r.append(p_g)
	p = np.array(p)
	f.close()
	return p
	
def ReadArray_4D(path,row,colume):   #   读取4维矩阵。row:四维矩阵的第一维。 colume:四维矩阵的第二维 。 path:文件路径
	p = []

	for i in range(0,row,1):
		pr = []
		for j in range(0,colume,1):
			
			tpath = path + str(i) + '_' + str(j) + '.txt'
			
			pt = ReadArray_2D(tpath)
			
			pr.append(pt)
			
		p.append(pr)
		
	p = np.array(p)	
	
	return p
	
def SaveArray_1D(path,p):          # 保存一维数组 p(array) 到 路径path 下
	f = open(path,'w')
	for i in p:
		f.write(str(i)+' ')
	
	f.close()
	
def SaveArray_2D(path,p):          # 保存二维数组 p(array) 到 路径path 下
	f = open(path,'w')
	for i in p:
		for j in i:
			f.write(str(j) + ' ')
		f.write('\n')
	f.close()
		
def SaveArray_3D(path,p):          # 保存三维数组 p(array) 到 路径path 下
	f = open(path,'w')
	for i in p:
		for j in i:
			for k in j:
				f.write(str(k)+' ')
			f.write('\n')
		f.write('\n')
	f.close()
	
def SaveArray_4D(path,p):          # 保存四维数组 p(array) 到 路径path 下	
	for i in range(0,p.shape[0],1):		
		for j in range(0,p.shape[1],1):
			tpath = path + str(i) + '_' + str(j) + '.txt'		
			SaveArray_2D(tpath,p[i][j])

        run.py 代码 :

# 卷积神经网络

import numpy as np
import struct
import matplotlib.pyplot as plt
import random
import pickle
import time

import ReadWrite_Array as rw
import load_mnist
import Layers_ICSBp as icsb
import cnnff
import cnnbp
import cnnpredict

#### 超参数设定 ############################################################################################################

numepochs = 100                  # 训练次数
style_getKB = 2                  # 读取 卷积核(k) 阈值(B) 的方式  : { 1 : 随机初始化 K,B.  2 : 读取现有  K B }
AFKind = 1                       # 激励函数类型  1:sigmod   2:tanh   3:ReLU
alpha  = 0.01                    # 学习率
batchsize = 600                  # 样本批次大小

#### 数据集参数 ####
num_train_samples = 60000        # 训练样本数量
num_test_samples  = 10000        # 测试样本数量

inputmapsize = 28                # 输入图像的大小
inputmaps = 1                    # 输入图像数目
onum = 10                        # 分类的类别数

#### 路径设置 ############################################################################################################

Path_train_x = './mnist/train-images.idx3-ubyte'     # 训练样本的图片信息 路径
Path_train_y = './mnist/train-labels.idx1-ubyte'     # 训练样本的文本信息 路径
Path_test_x = './mnist/t10k-images.idx3-ubyte'       # 测试样本的图片信息 路径
Path_test_y = './mnist/t10k-labels.idx1-ubyte'       # 测试样本的文本信息 路径

Path_KB = './KB/'                                    # K B 路径

#### 中间变量 ############################################################################################################
L = 0.0                         # 均方差损失
numbatches = int(num_train_samples/batchsize)  # 批次数
#### 数组 ############################################################################################################
print('正在加载样本数据...')
time_load_mnist = time.time()

train_x = (load_mnist.read_images(Path_train_x))/255.0   # 训练数组(图片信息)    [60000,28,28]
train_y = (load_mnist.read_labels(Path_train_y))         # 训练数组(文本信息)    [60000,10]
test_x = (load_mnist.read_images(Path_test_x))/255.0     # 测试数组(图片信息)    [10000,28,28]
test_y = (load_mnist.read_labels(Path_test_y))           # 测试数组(文本信息)    [10000,10]

time_load_mnist = time.time() - time_load_mnist

print('加载样本数据完成,耗时:%.4f 秒' %time_load_mnist)

batch_x = np.zeros(((batchsize,inputmapsize,inputmapsize)))    # 单次训练数组(图片信息)
batch_y = np.zeros((batchsize,onum))                           # 单次训练数组(文本信息)

#### 设置 并 初始化网络 ############################################################################################################

l_in = icsb.Layer_Input()   # 实例化输入层
l_in.I_in = batch_x	
l_in.inputmaps = inputmaps
l_in.setup()

#### 用户设置 ####
c1 = icsb.Layer_Convolve()  # 实例化卷基层
c1.outputmaps = 6
c1.kernelSize    = 5

s1 = icsb.Layer_Sample()  # 实例化卷基层
s1.scale = 2

c2 = icsb.Layer_Convolve()  # 实例化卷基层
c2.outputmaps = 12
c2.kernelSize    = 5

s2 = icsb.Layer_Sample()  # 实例化卷基层
s2.scale = 2

layers = [l_in,c1,s1,c2,s2]

#### 初始化网络 ####

num_layers = len(layers)   # 网络层数 (不包括Bp层)

num_c = 1
for i in range(1,num_layers,1):
	
	if layers[i].type_L == 'c':      # 卷基层
		layers[i].name = 'c' + str(num_c)
		layers[i].style_getkb = style_getKB
		layers[i].C_in = layers[i-1].a
		layers[i].Path_KB = Path_KB
		layers[i].setup()
		
	elif layers[i].type_L == 's':    # 采样层
		layers[i].name = 's' + str(num_c)
		num_c += 1
		layers[i].S_in = layers[i-1].a
		layers[i].setup()
			
bp = icsb.Layer_Bp()
bp.name = 'bp'
bp.style_getkb = style_getKB
bp.onum = onum
bp.Bp_in = layers[num_layers-1].a
bp.Path_KB = Path_KB
bp.setup()

layers.append(bp)            # 将bp层添加到网络中
num_layers = num_layers+1    # 网络层数 (包括Bp层)

print('\n网络设置完成 !')		
#### 训练网络 train ####

for i in range(0,numepochs,1):    # 

	for n in range(1,num_layers,1):   # 初始化
		layers[n].setup()
		
		
	print('\n    正在进行第 %d 次训练...'%(i+1))
	print()
	
	time_train = time.time()
	
	sequence_random = np.random.permutation(num_train_samples)   # 产生随机序列(60000个)
	
	for j in range(0,int(numbatches),1):    # 
	
		time_train_batch = time.time()
		
		batch_x = train_x[sequence_random[j*batchsize:(j+1)*batchsize]]
		batch_y = train_y[sequence_random[j*batchsize:(j+1)*batchsize]]

		cnnff.cnnff(layers,batch_x,AFKind)
		L = cnnbp.cnnbp(layers,batch_y,AFKind,alpha)
			
		time_train_batch = time.time() - time_train_batch
		
		print('        第 %d 次训练,第 %d batch 训练结果: 均方差损失:%.8f , 耗时: %.4f 秒'%(i+1,j+1,L,time_train_batch))
		
		
		
	time_train = time.time() - time_train
	
	print()
	print('    已训练 %d 次 , 均方差损失:%.8f ; 本次训练耗时: %.4f 秒'%(i+1,L,time_train))
	print()
	
	#### 保存 K B ####
	print('    正在保存卷积核(k)与阈值(b)...')
	for c in range(1,num_layers-1,1):
	
		if layers[c].type_L == 'c':
			
			path_c = layers[c].type_L + str(int((c+1)/2))
			
			rw.SaveArray_1D(layers[c].Path_KB + path_c + 'b.txt',layers[c].b)  # 保存阈值 b
			rw.SaveArray_4D(layers[c].Path_KB + path_c + 'k',layers[c].k)      # 保存卷积核 k
			
	rw.SaveArray_1D(layers[num_layers-1].Path_KB + 'ffb.txt',layers[num_layers-1].ffb)  # 保存阈值 b
	rw.SaveArray_2D(layers[num_layers-1].Path_KB + 'ffw.txt',layers[num_layers-1].ffw)  # 保存权值 ffw
	
	print('    卷积核(k)与阈值(b)保存完成!\n')
	
	#### 检测结果 ####
	print('    正在测试第 %d 次训练效果...'%(i+1))
	layers_test = layers
	
	time_predict = time.time()

	t_result = cnnpredict.predict(layers_test,test_x[0:1000],AFKind).argmax(axis=1)
	t_real = test_y[0:1000].argmax(axis=1)
	tt = t_result - t_real
	tt = np.where(tt != 0,1,0) 
	num_wrong = tt.sum()

	time_predict = time.time() - time_predict

	print('    本次预测耗时: %.4f 秒'%(time_predict))
	print('    当前测试结果的准确率: %.2f' %((1000-num_wrong)/1000*100) + '%')
	print()
	

#### 检测训练效果 ####
print("正在测试模型训练效果 ... 请稍等 ...")

time_predict = time.time()

t_result = cnnpredict.predict(layers,test_x[0:10000],AFKind).argmax(axis=1)

t_real = test_y[0:10000].argmax(axis=1)

tt = t_result - t_real

tt = np.where(tt != 0,1,0) 

num_wrong = tt.sum()

time_predict = time.time() - time_predict

print('预测耗时: %.4f 秒'%(time_predict))

print('准确率 : %.2f' %((10000-num_wrong)/10000*100) + '%')

print()

3.4 运行代码

        以上文件都配置好后,就可以来运行代码,实现自己的手写数字识别程序。下面说一下具体地运行方法:

        首先打开 run.py 文件 ,设置 训练次数、学习率、获取参数方式等,然后执行代码。正常的话,就可看到程序会 读取数据、训练网络、检测训练效果,不过小伙伴们要注意,这个训练过程可能比较漫长,一定要有耐心,通常将 numepoch 设置为100,就能够有比较好的识别效果。

        这里需要强调一个值:style_getKB  。 如果是首次训练网络的话(或者想要重新训练网络),就将这个值设置为1 ,则会随机给 卷积核与阈值赋值,来训练网络。 若之前已经对网络训练过一定次数,并且想要在此基础继续训练网络,则将这个值设为 2 。

        细心的小伙伴应该能发现,代码中并不是将 60000 组训练数据全都直接扔给网络训练,而是将其分成了 大小为600的批次(batch,batchsize为每个批次的数据的组数,小伙伴可以自己随意设置,感受训练效果),这样做能够使网络训练的更快、更好。实验证明,当样本很多时,使用 batch ,对于特征的提取效果更好,更容易收敛。此外,如果不使用 batch ,会导致内存的极大消耗,理论上虽然可行,但是实际运行起来却非常困难。

        程序运行中的样子:

        当程序运行完成后,会出现以下结果:

        这是对 10000 组测试样本进行推测得到的结果。可以看到,目前这个网络训练的效果相对理想,能够 正确分辨出 86.47%的手写数字图片。如果利用更加合理的架构,目前对于mnist数据集的训练效果已经可以轻易的超过 99% 的识别率。

        此外还需要说明的是,此段代码中使用的是均方差损失,实际上这是一个分类问题,应该使用交叉熵损失,能够得到更高的识别率,这部分内容会在以后的文章中专门讲解。

        但是看到这儿,你可能会觉得不够直观,那就看一下 用 C# 实现的一个识别效果:

        经过试验,识别的效果还是相当不错。想要 C# 这段代码的小伙伴可以留邮箱给我。

 

第 4 部分  CNN 后续

        CNN 是利用 AI 来解决计算机视觉问题的基础,目前主流的架构基本都是它的拓展与变种,因此这一部分的内容对于 AI 工程师来说,是基础中的基础,可谓十分重要。

        这里需要向小伙伴们说明一下,由于本文的目的在于系统地认识 CNN ,因此在代码实现的过程中,没有用到任何 神经网络的 API (如 tensorflow)。实际上,在工程应用中,并不需要这么繁琐,直接利用 python + tensorflow 搭建网络结构,就可以进行建立模型、保存模型、调用模型、训练模型等工作,可以说十分方便,完全不需要去写 前向传播 与 反向传播 这些复杂的过程,计算的效率也远远超过我们自己所写的代码。

        在之后的内容中,我将讲解一下如何利用 tensorflow 来实现具体的应用,以及带大家看一看 CNN 发展过程中的几个重要架构。

 

  • 40
    点赞
  • 128
    收藏
    觉得还不错? 一键收藏
  • 27
    评论
智慧校园整体解决方案是响应国家教育信息化政策,结合教育改革和技术创新的产物。该方案以物联网、大数据、人工智能和移动互联技术为基础,旨在打造一个安全、高效、互动且环保的教育环境。方案强调从数字化校园向智慧校园的转变,通过自动数据采集、智能分析和按需服务,实现校园业务的智能化管理。 方案的总体设计原则包括应用至上、分层设计和互联互通,确保系统能够满足不同用户角色的需求,并实现数据和资源的整合与共享。框架设计涵盖了校园安全、管理、教学、环境等多个方面,构建了一个全面的校园应用生态系统。这包括智慧安全系统、校园身份识别、智能排课及选课系统、智慧学习系统、精品录播教室方案等,以支持个性化学习和教学评估。 建设内容突出了智慧安全和智慧管理的重要性。智慧安全管理通过分布式录播系统和紧急预案一键启动功能,增强校园安全预警和事件响应能力。智慧管理系统则利用物联网技术,实现人员和设备的智能管理,提高校园运营效率。 智慧教学部分,方案提供了智慧学习系统和精品录播教室方案,支持专业级学习硬件和智能化网络管理,促进个性化学习和教学资源的高效利用。同时,教学质量评估中心和资源应用平台的建设,旨在提升教学评估的科学性和教育资源的共享性。 智慧环境建设则侧重于基于物联网的设备管理,通过智慧教室管理系统实现教室环境的智能控制和能效管理,打造绿色、节能的校园环境。电子班牌和校园信息发布系统的建设,将作为智慧校园的核心和入口,提供教务、一卡通、图书馆等系统的集成信息。 总体而言,智慧校园整体解决方案通过集成先进技术,不仅提升了校园的信息化水平,而且优化了教学和管理流程,为学生、教师和家长提供了更加便捷、个性化的教育体验。
评论 27
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值