神经网络算法的基本原理的数据和样本的关系?

神经网络很萌的!0. 分类神经网络最重要的用途是分类,为了让大家对分类有个直观的认识,咱们先看几个例子:垃圾邮件识别:现在有一封电子邮件,把出现在里面的所有词汇提取出来,送进一个机器里,机器需要判断这封邮件是否是垃圾邮件。疾病判断:病人到医院去做了一大堆肝功、尿检测验,把测验结果送进一个机器里,机器需要判断这个病人是否得病,得的什么病。猫狗分类:有一大堆猫、狗照片,把每一张照片送进一个机器里,机器需要判断这幅照片里的东西是猫还是狗。这种能自动对输入的东西进行分类的机器,就叫做分类器。分类器的输入是一个数值向量,叫做特征(向量)。在第一个例子里,分类器的输入是一堆0、1值,表示字典里的每一个词是否在邮件中出现,比如向量(1,1,0,0,0......)就表示这封邮件里只出现了两个词abandon和abnormal;第二个例子里,分类器的输入是一堆化验指标;第三个例子里,分类器的输入是照片,假如每一张照片都是320*240像素的红绿蓝三通道彩色照片,那么分类器的输入就是一个长度为320*240*3=230400的向量。分类器的输出也是数值。第一个例子中,输出1表示邮件是垃圾邮件,输出0则说明邮件是正常邮件;第二个例子中,输出0表示健康,输出1表示有甲肝,输出2表示有乙肝,输出3表示有饼干等等;第三个例子中,输出0表示图片中是狗,输出1表示是猫。分类器的目标就是让正确分类的比例尽可能高。一般我们需要首先收集一些样本,人为标记上正确分类结果,然后用这些标记好的数据训练分类器,训练好的分类器就可以在新来的特征向量上工作了。1. 神经元咱们假设分类器的输入是通过某种途径获得的两个值,输出是0和1,比如分别代表猫和狗。现在有一些样本:大家想想,最简单地把这两组特征向量分开的方法是啥?当然是在两组数据中间画一条竖直线,直线左边是狗,右边是猫,分类器就完成了。以后来了新的向量,凡是落在直线左边的都是狗,落在右边的都是猫。一条直线把平面一分为二,一个平面把三维空间一分为二,一个n-1维超平面把n维空间一分为二,两边分属不同的两类,这种分类器就叫做神经元。大家都知道平面上的直线方程是ax+by+c=0,等式左边大于零和小于零分别表示点(x,y)在直线的一侧还是另一侧,把这个式子推广到n维空间里,直线的高维形式称为超平面,它的方程是:h = a_1x_1+a_2 x_2+...+a_nx_n+a_0=0神经元就是当h大于0时输出1,h小于0时输出0这么一个模型,它的实质就是把特征空间一切两半,认为两瓣分别属两个类。你恐怕再也想不到比这更简单的分类器了,它是McCulloch和Pitts在1943年想出来了。这个模型有点像人脑中的神经元:从多个感受器接受电信号x_1, x_2,...,x_n,进行处理(加权相加再偏移一点,即判断输入是否在某条直线h=0的一侧),发出电信号(在正确的那侧发出1,否则不发信号,可以认为是发出0),这就是它叫神经元的原因。当然,上面那幅图我们是开了上帝视角才知道“一条竖直线能分开两类”,在实际训练神经元时,我们并不知道特征是怎么抱团的。神经元模型的一种学习方法称为Hebb算法:先随机选一条直线/平面/超平面,然后把样本一个个拿过来,如果这条直线分错了,说明这个点分错边了,就稍微把直线移动一点,让它靠近这个样本,争取跨过这个样本,让它跑到直线正确的一侧;如果直线分对了,它就暂时停下不动。因此训练神经元的过程就是这条直线不断在跳舞,最终跳到两个类之间的竖直线位置。2. 神经网络MP神经元有几个显著缺点。首先它把直线一侧变为0,另一侧变为1,这东西不可微,不利于数学分析。人们用一个和0-1阶跃函数类似但是更平滑的函数Sigmoid函数来代替它(Sigmoid函数自带一个尺度参数,可以控制神经元对离超平面距离不同的点的响应,这里忽略它),从此神经网络的训练就可以用梯度下降法来构造了,这就是有名的反向传播算法。神经元的另一个缺点是:它只能切一刀!你给我说说一刀怎么能把下面这两类分开吧。解决办法是多层神经网络,底层神经元的输出是高层神经元的输入。我们可以在中间横着砍一刀,竖着砍一刀,然后把左上和右下的部分合在一起,与右上的左下部分分开;也可以围着左上角的边沿砍10刀把这一部分先挖出来,然后和右下角合并。每砍一刀,其实就是使用了一个神经元,把不同砍下的半平面做交、并等运算,就是把这些神经元的输出当作输入,后面再连接一个神经元。这个例子中特征的形状称为异或,这种情况一个神经元搞不定,但是两层神经元就能正确对其进行分类。只要你能砍足够多刀,把结果拼在一起,什么奇怪形状的边界神经网络都能够表示,所以说神经网络在理论上可以表示很复杂的函数/空间分布。但是真实的神经网络是否能摆动到正确的位置还要看网络初始值设置、样本容量和分布。神经网络神奇的地方在于它的每一个组件非常简单——把空间切一刀+某种激活函数(0-1阶跃、sigmoid、max-pooling),但是可以一层一层级联。输入向量连到许多神经元上,这些神经元的输出又连到一堆神经元上,这一过程可以重复很多次。这和人脑中的神经元很相似:每一个神经元都有一些神经元作为其输入,又是另一些神经元的输入,数值向量就像是电信号,在不同神经元之间传导,每一个神经元只有满足了某种条件才会发射信号到下一层神经元。当然,人脑比神经网络模型复杂很多:人工神经网络一般不存在环状结构;人脑神经元的电信号不仅有强弱,还有时间缓急之分,就像莫尔斯电码,在人工神经网络里没有这种复杂的信号模式。神经网络的训练依靠反向传播算法:最开始输入层输入特征向量,网络层层计算获得输出,输出层发现输出和正确的类号不一样,这时它就让最后一层神经元进行参数调整,最后一层神经元不仅自己调整参数,还会勒令连接它的倒数第二层神经元调整,层层往回退着调整。经过调整的网络会在样本上继续测试,如果输出还是老分错,继续来一轮回退调整,直到网络输出满意为止。这很像中国的文艺体制,武媚娘传奇剧组就是网络中的一个神经元,最近刚刚调整了参数。3. 大型神经网络我们不禁要想了,假如我们的这个网络有10层神经元,第8层第2015个神经元,它有什么含义呢?我们知道它把第七层的一大堆神经元的输出作为输入,第七层的神经元又是以第六层的一大堆神经元做为输入,那么这个特殊第八层的神经元,它会不会代表了某种抽象的概念?就好比你的大脑里有一大堆负责处理声音、视觉、触觉信号的神经元,它们对于不同的信息会发出不同的信号,那么会不会有这么一个神经元(或者神经元小集团),它收集这些信号,分析其是否符合某个抽象的概念,和其他负责更具体和更抽象概念的神经元进行交互。2012年多伦多大学的Krizhevsky等人构造了一个超大型卷积神经网络[1],有9层,共65万个神经元,6千万个参数。网络的输入是图片,输出是1000个类,比如小虫、美洲豹、救生船等等。这个模型的训练需要海量图片,它的分类准确率也完爆先前所有分类器。纽约大学的Zeiler和Fergusi[2]把这个网络中某些神经元挑出来,把在其上响应特别大的那些输入图像放在一起,看它们有什么共同点。他们发现中间层的神经元响应了某些十分抽象的特征。第一层神经元主要负责识别颜色和简单纹理第二层的一些神经元可以识别更加细化的纹理,比如布纹、刻度、叶纹。第三层的一些神经元负责感受黑夜里的黄色烛光、鸡蛋黄、高光。第四层的一些神经元负责识别萌狗的脸、七星瓢虫和一堆圆形物体的存在。第五层的一些神经元可以识别出花、圆形屋顶、键盘、鸟、黑眼圈动物。这里面的概念并不是整个网络的输出,是网络中间层神经元的偏好,它们为后面的神经元服务。虽然每一个神经元都傻不拉几的(只会切一刀),但是65万个神经元能学到的东西还真是深邃呢。[1] Krizhevsky, A., Sutskever, I., & Hinton, G. E. (2012). Imagenet classification with deep convolutional neural networks. In Advances in neural information processing systems (pp. 1097-1105).[2] Zeiler, M. D., & Fergus, R. (2013). Visualizing and understanding convolutional neural networks. arXiv preprint arXiv:1311.2901.
广告,先推广一下自己的书一、卷积我们在 2 维上说话。有两个 \mathcal{R}^2\rightarrow \mathcal{R}的函数 f(x, y) 和 g(x, y) 。所谓 f 和 g 的卷积就是一个新的 \mathcal{R}^2\rightarrow \mathcal{R}的函数 c(x, y) 。通过下式得到:c(x,y)=\int_{-\infty}^{\infty} \int_{-\infty}^{\infty}f(s,t)\times g(x-s,y-t) \ ds \ dt 这式子的含义是:遍览从负无穷到正无穷的全部 s 和 t 值,把 g 在 (x-s, y-t) 上的值乘以 f 在 (s, t) 上的值之后再“加和”到一起(积分意义上),得到 c 在 (x, y) 上的值。说白了卷积就是一种“加权求和”:以 f 为权,以 (x, y) 为中心,把 g 距离中心 (-s, -t) 位置上的值乘上 f 在 (s, t) 的值,最后加到一起。把卷积公式写成离散形式就更清楚了:C(x,y)=\sum_{t=-\infty}^{\infty}\sum_{s=-\infty}^{\infty}F(s,t)\times G(x-s,y-t) \ \Delta s \ \Delta t=\sum_{t=-\infty}^{\infty}\sum_{s=-\infty}^{\infty}F(s,t)\times G(x-s,y-t) 第二个等号成立是因为在这里我们每隔单位长度 1 一采样,\Delta s和\Delta t都是 1 。可以令 G 表示一幅 100 x 100 大小的灰度图像。G(x, y) 取值 [0,255] 区间内的整数,是图像在 (x, y) 的灰度值。x 和 y 坐标取 [0, 99] ,其它位置上 G 值全取 0 。令 F 在 s 和 t 取 {-1, 0, 1} 的位置为特定值,其他位置全取 0 。F 可以看作是一个 3 x 3 的网格。如图 1.1图 1.1图 1.1 中 G 每个小格子里的值就是图像在 (x, y) 的灰度值。F 每个小格子里的值就是 F 在 (s, t) 的取值。图 1.2如图 1.2 所示,将 F 的中心 (0, 0) 对准 G 的 (6, 6) 。把 F 和 G 对应的 9 个位置上各自的值相乘,再将 9 个乘积加在一起,就得到了卷积值 C(6, 6) 。对 G 的每一个位置求 C 值,就得到了一幅新的图像。其中注意三点:F 是上下左右翻转后再与 G 对准的。因为卷积公式中 F(s, t) 乘上的是 G(x-s, y-t) 。比如 F(-1, -1) 乘上的是 G(7, 7) ;如果 F 的所有值之和不等于 1.0,则 C 值有可能不落在 [0, 255] 区间内,那就不是一个合法的图像灰度值。所以如果需要让结果是一幅图像,就得将 F 归一化——令它的所有位置之和等于 1.0 ;对于 G 边缘上的点,有可能它的周围位置超出了图像边缘。此时可以把图像边缘之外的值当做 0 。或者只计算其周围都不超边缘的点的 C 。这样计算出来的图像就比原图像小一些。在上例中是小了一圈,如果 F 覆盖范围更大,那么小的圈数更多。上述操作其实就是对数字图像进行离散卷积操作,又叫滤波。F 称作卷积核或滤波器。不同的滤波器起不同的作用。想象一下,如果 F 的大小是 3 x 3 ,每个格子里的值都是 1/9 。那么滤波就相当于对原图像每一个点计算它周围 3 x 3 范围内 9 个图像点的灰度平均值。这应该是一种模糊。看看效果。图 1.3左图是 lena 灰度原图。中图用 3 x 3 值都为 1/9 的滤波器去滤,得到一个轻微模糊的图像。模糊程度不高是因为滤波器覆盖范围小。右图选取了 9 x 9 值为 1/81 的滤波器,模糊效果就较明显了。滤波器还有许多其他用处。例如下面这个滤波器:+----+----+----+
-1
0
1
+----+----+----+
-2
0
2
+----+----+----+
-1
0
1
+----+----+----+注意该滤波器没有归一化(和不是 1.0 ),故滤出来的值可能不在 [0, 255] 之内。通过减去最小值、除以最大/最小值之差、再乘以 255 并取整,把结果值归一到 [0, 255] 之内,使之成为一幅灰度图像。现在尝试用它来滤 lena 图。图 1.4该滤波器把图像的边缘检测出来了。它就是 Sobel 算子。边缘检测、图像模糊等等都是人们设计出来的、有专门用途的滤波器。如果搞一个 9 x 9 的随机滤波器,会是什么效果呢?图 1.5如上图,效果也类似于模糊。因为把一个像素点的值用它周围 9 x 9 范围的值随机加权求和,相当于“捣浆糊”。但可以看出模糊得并不润滑。这时我们不禁要想,如果不是由人来设计一个滤波器,而是从一个随机滤波器开始,根据某种目标、用某种方法去逐渐调整它,直到它接近我们想要的样子,可行么?这就是卷积神经网络(Convolutional Neural Network, CNN)的思想了。可调整的滤波器是 CNN 的“卷积”那部分;如何调整滤波器则是 CNN 的“神经网络”那部分。二、神经网络人工神经网络(Neural Network, NN)作为一个计算模型,其历史甚至要早于计算机。 W.S. McCulloch 和 W. Pitts 在四十年代就提出了人工神经元模型。但是单个人工神经元甚至无法计算异或。多个人工神经元连接成网络就可以克服无法计算异或的问题。但是对于这样的网络——多层感知机网络,当时的人们没有发现训练它的方法。人工智能领域的巨擘马文.明斯基认为这个计算模型是没有前途的。直到 7、80 年代,人们发现了训练多层感知机网络的反向传播算法(BP)。BP 的本质是梯度下降算法。多层感知机网络梯度的计算乍看十分繁琐,实则有规律。人工神经元就是用一个数学模型简单模拟神经细胞。神经细胞有多个树突和一个伸长的轴突。一个神经元的轴突连接到其他神经元的树突,并向其传导神经脉冲。神经元会根据来自它的若干树突的信号决定是否从其轴突向其他神经元发出神经脉冲。图 2.1一个人工神经元就是对生物神经元的数学建模(下文中“神经元”就指人工神经元,“神经网络”就指人工神经网络)。见图 2.2 。图 2.2p_{1} ,p_{2} , \ ... \ ,p_{n} 是神经元的输入。a 是神经元的输出。神经元将输入p_{1} ,p_{2} , \ ... \ , p_{n} 加权求和后再加上偏置值 b ,最后再施加一个函数 f ,即:a=f(n)=f \left( \sum_{i=1}^{n}{p_iw_i}+b \right) = f \left( \begin{array}{ccc} (w_1,w_2 \cdots w_n) \end{array} \left( \begin{array}{ccc} p_1 \\ p_2 \\ \vdots \\ p_n\end{array} \right)+b \right) = f \left( \mathcal{W}^T\mathcal{P}+b\right) 上式最后是这个式子的向量形式。P 是输入向量,W 是权值向量,b 是偏置值标量 。f 称为激活函数( Activation Function )。激活函数可以采用多种形式。图 2.3 展示了一些常用的激活函数。图 2.3这是单个神经元的定义。神经网络就是把许多这样的神经元连接成一个网络:一个神经元的输出作为另一个神经元的输入。神经网络可以有多种多样的拓扑结构。其中最简单的就是“多层全连接前向神经网络”。它的输入连接到网络第一层的每个神经元。前一层的每个神经元的输出连接到下一层每个神经元的输入。最后一层神经元的输出就是整个神经网络的输出。图 2.4 是一个三层神经网络。它接受 10 个输入,也就是一个 10 元向量。第一层和第二层各有 12 个神经元。最后一层有 6 个神经元,就是说这个神经网络输出一个 6 元向量。神经网络最后一层称为输出层,中间的层称为隐藏层。 图 2.4整个神经网络的计算可以用矩阵式给出。我们给出神经网络单层的式子。每层的神经元个数不一样,输入/输出维度也就不一样,计算式中的矩阵和向量的行列数也就不一样,但形式是一致的。假设我们考虑的这一层是第 i 层。它接受 m 个输入,拥有 n 个神经元( n 个输出),那么这一层的计算如下式所示:\mathcal{O}^i=\left(\begin{array}{ccc} o_1^{i} \\ \vdots \\ o_n^{i} \end{array}\right)=f\left(\left(\begin{array}{ccc} w_{11}^i & \cdots & w_{1m}^i\\ \vdots & \ddots & \vdots\\ w_{n1}^i & \cdots & w_{nm} ^i \end{array}\right) \left(\begin{array}{ccc} o_1^{i-1} \\ \vdots \\ o_m^{i-1} \end{array}\right) +\left(\begin{array}{ccc} b_1^{i} \\ \vdots \\ b_n^{i} \end{array}\right)\right)
上标 i 表示第 i 层。 \mathcal{O}^i是输出向量,n 元,因为第 i 层有 n 个神经元。第 i 层的输入,即第 i-1 层的输出,是 m 元向量。权值矩阵 W 是 n x m 矩阵:n 个神经元,每个神经元有 m 个权值。W 乘以第 i - 1 层输出的 m 向量,得到一个 n 向量,加上 n 元偏置向量 b ,再对结果的每一个元素施以激活函数 f ,最终得到第 i 层的 n 元输出向量。若不嫌繁琐,可以将第 i - 1 层的输出也展开,最终能写出一个巨大的式子。它就是整个全连接前向神经网络的计算式。可以看出整个神经网络其实就是一个向量到向量的函数。至于它是什么函数,就取决于网络拓扑结构和每一个神经元的权值和偏置值。如果随机给出权值和偏置值,那么这个神经网络是无用的。我们想要的是有用的神经网络。它应该表现出我们想要的行为。要达到这个目的,首先准备一个从目标函数采样的包含若干“输入-输出对”的集合——训练集。把训练集的输入送给神经网络,得到的输出肯定不是正确的输出。因为一开始这个神经网络的行为是随机的。把一个训练样本输入给神经网络,计算输出与目标输出的(向量)差的模平方(自己与自己的内积)。再把全部 n 个样本的差的模平方求平均,得到 e :e=\frac{1}{2n} \sum_{i=1}^{n}
o_i^{real}-o_i^{output}
^2 e 称为均方误差 mse 。全部输出向量和目标输出向量之间的距离(差的模)越小,则 e 越小。e 越小则神经网络的行为与想要的行为越接近。目标是使 e 变小。在这里 e 可以看做是全体权值和偏置值的一个函数。这就成为了一个无约束优化问题。如果能找到一个全局最小点,e 值在可接受的范围内,就可以认为这个神经网络训练好了。它能够很好地拟合目标函数。这里待优化的函数也可以是 mse 外的其他函数,统称 Cost Function,都可以用 e 表示。经典的神经网络的训练算法是反向传播算法(Back Propagation, BP)。BP 算法属于优化理论中的梯度下降法(Gradient Descend)。将误差 e 作为全部权值和全部偏置值的函数。算法的目的是在自变量空间内找到 e 的全局极小点。首先随机初始化全体权值和全体偏置值,之后在自变量空间中沿误差函数 e 在该点的梯度方向的反方向前进一个步长。梯度的反方向上函数方向导数最小,函数值下降最快。步长称为学习速率(Learning Rate, LR)。如此反复迭代,最终(至少是期望)解运动到误差曲面的全局最小点(请参考专栏文章:神经网络之梯度下降与反向传播(上))。图 2.5 是用 matlab 训练一个极简单的神经网络。它只有单输入单输出。输入层有两个神经元,输出层有一个神经元。整个网络有 4 个权值加 3 个偏置。图中展示了固定其他权值,只把第一层第一个神经元的权值w_{(1,1)}^1和偏置b_1^1做自变量时候的 e 曲面,以及随着算法迭代,解的运动轨迹。 图 2.5最终算法没有收敛到全局最优解(红 +)。但是解已经运动到了一个峡谷的底部。由于底部过于平缓,解“走不动”了。所得解比最优也差不到哪去。对于一个稍复杂的神经网络,e 对权值和偏置值的函数是一个复杂的函数。求梯度需要计算 e 对每一个权值和偏置值的偏导数。所幸的是求偏导的公式不会因为这个权值或偏置值距离输出层越远而越复杂。对于每个神经元可以计算一个值 delta ,称“局部误差”或“灵敏度”。得到了每个神经元的 delta 就很容易计算 e 对任何一个权值或偏执值的偏导数。计算某神经元的 delta 用到该神经元激活函数的导函数,对于输出层用到输出与目标输出的差;对于隐藏层用到下一层各神经元的 delta 。每个神经元将其 delta 传递给前一层的各神经元。前一层的各神经元收集后一层的神经元的 delta 计算自己的 delta 。这就是“反向传播”名称的由来——“局部误差”或“灵敏度” delta 沿着反向向前传,逐层计算所有权值和偏置值的偏导数,最终得到梯度。详细推导可参考书籍[1]第八章、[2]第十一章、[3]第四章、[4]第三章或[5]第十一章(或参考专栏文章:神经网络之梯度下降与反向传播(下))。梯度下降法有很多变体。通过调整学习速率 LR 可以提高收敛速度;通过增加冲量可以避免陷入局部最优点以及减少震荡。还可以每一次不计算全部样本的 e ,而是随机取一部分样本,根据它们的 e 更新权值。梯度下降是基于误差函数的一阶性质。还有其他方法基于二阶性质进行优化,比如牛顿法等等。优化作为一门应用数学学科是机器学习的一个重要理论基础,在理论和实现上均有众多结论和方法。参考[1]。三、卷积神经网络现在把卷积滤波器和神经网络两个思想结合起来。卷积滤波器无非就是一套权值。而神经网络也可以有(除全连接外的)其它拓扑结构。可以构造如图 3.1 所示意的神经网络。图 3.1该神经网络接受 n x n 个输入,产生 n x n 个输出。图中左边的平面包含 n x n 个格子,每个格子中是一个 [0, 255] 的整数值。它就是输入图像,也是这个神经网络的输入。右边的平面也是 n x n 个格子,每个格子是一个神经元。每个神经元连接到输入上它对应位置周围 3 x 3 范围内的值。每个连接有一个权值。所有神经元都如此连接(图中只画了一个,出了输入图像边缘的连接就认为连接到常数 0 )。右边层的每个神经元将与它连接的 3 x 3 个输入的值乘上连接权重并加和,得到该神经元的输出。n x n 个神经元的输出就是该神经网络的输出。这个神经网络有两点与全连接神经网络不同。首先它不是全连接的。右层的神经元并非连接上全部输入,而是只连接了一部分。这里的一部分就是输入图像的一个局部区域。我们常听说 CNN 能够把握图像局部特征就是这个意思。这样一来权值少了很多,因为连接少了。权值其实还更少,因为每一个神经元的 9 个权值都是和其他神经元共享的。全部 n x n 个神经元都用这共同的一组 9 个权值,并且不要偏置值。那么这个神经网络其实一共只有 9 个参数需要调整。看了第一节的同学们都看出来了,这个神经网络所进行的计算不就是一个卷积滤波器么?只不过卷积核的参数未定,需要我们去训练——它是一个“可训练滤波器”。这个神经网络其实就是一个只有一个卷积层、且该卷积层只有一个滤波器(通道)的 CNN 。试着用 Sobel 算子滤出来的图片作为目标值去训练这个神经网络。给神经网络的输入是灰度 lena 图,目标输出是经过 Sobel 算子滤波的 lena 图,见图 1.4 。这唯一的一对输入输出图片就构成了训练集。神经网络权值随机初始化,训练 2000 轮。如图 3.7 。图 3.2从左上到右下依次为:初始随机滤波器输出、每个 200 轮训练后的滤波器输出( 10 幅)、最后一幅是 Sobel 算子的输出,也就是用作训练的目标图像。可以看到经过最初 200 轮后,神经网络的输出就已经和 Sobel 算子的输出看不出什么差别了。后面那些轮的输出基本一样。输入与输出的均方误差 mse 随着训练轮次的变化。如图 3.3 。图 3.31500 轮过后,mse 基本就是 0 了。训练完成后网络的权值是:+-------+--------+--------+
1.29
0.04
-1.31
+-------+--------+--------+
1.43
0.01
-1.45
+-------+--------+--------+
1.34
-0.07
-1.28
+-------+--------+--------+与 Sobel 算子比较一下:+----+----+----+
-1
0
1
+----+----+----+
-2
0
2
+----+----+----+
-1
0
1
+----+----+----+
注意训练出来的滤波器负数列在右侧而不是左侧。因为计算卷积是把滤波器上下左右翻转反着扣上去的。这并不重要,本质是相同的。关键是一正列、一负列,中间零值列。非零值列三个值之比近似 1:2:1 。我们得到的就是一个近似的 Sobel 算子。我们以训练神经网络的方式把一个随机滤波器训练成了 Sobel 算子。这就是优化的魔力(代码见本文最后)。在 CNN 中,这样的滤波器层叫做卷积层。一个卷积层可以有多个滤波器,每一个叫做一个 channel 。图像是二维信号。信号也可以是其他维度的,比如一维、三维乃至更高维度。那么滤波器相应的也有各种维度。回到二维图像的例子,实际上一个卷积层面对的是多个 channel 的 “一摞” 二维图像。比如一幅 100 x 100 大小的彩色图就会有 RGB 三个 channel ,其数据维度是 3 x 100 x 100 。那么直接连接彩色图像输入的卷积层面对的是 3 x 100 x 100 的数据,这时它的滤波器是 3 维度,第一维等于输入 channel 数(这里是 3)。第 2、3 维度是指定的滤波器大小,例如 5 x 5 。卷积层把输入的多 channel 的一摞二维图像用三维滤波器滤出一幅二维图像。假如这层有 32 个滤波器,那么这层输出 32 个 channel ,每个 channel 是一个二维图像。激活函数构成 CNN 的一种层——激活层,这样的层没有可训练的参数。它为输入施加激活函数,例如 Sigmoid 、Tanh 等。还有一种层叫做 Pooling 层(池化层)。它也没有参数,起到降维的作用。将输入切分成不重叠的一些 n x n 区域。每一个区域就包含 n x n 个值。从这 n x n 个值计算出一个值。计算方法可以是求平均、取最大等等。假设 n = 2,那么 4 个输入变成一个输出。输出图像就是输入图像的 1/4 大小。若把 2 维的层展平成一维向量,后面可再连接一个全连接前向神经网络。通过把这些组件进行组合就得到了一个 CNN 。它直接以原始图像为输入,以最终的回归或分类问题的结论为输出,内部兼有滤波图像处理和函数拟合,所有参数放在一起训练。这就是卷积神经网络。四、举个栗子手写数字识别。数据集中一共有 42000 个 28 x 28 的手写数字灰度图片。十个数字( 0~9 )的样本数量大致相等。为减少训练时间,随机抽取其中 10000 个。图 4.1 展示其中一部分。图 4.1将样本集合的 75% 用作训练,剩下的 25% 用作测试。构造一个结构如图 4.2 的 CNN 。图 4.2该 CNN 共有 9 层(不包括输入层)。它接受 784 元向量作为输入,就是一幅 28 x 28 的灰度图片。并没有将图片先变形成 28 x 28 再输入,因为在 CNN 的第一层放了一个 reshape 层。该层负责将 784 元的输入向量变形成 1 x 28 x 28 的阵列。最开始那个 1 x 表示只有一个通道 ,因为这是灰度图像。如果是彩色图像,就有 RGB 三个通道 。接下来放一个卷积层。它包含 32 个 3 x 3 的滤波器,所以它的输出维度是 32 x 28 x 28 。32 个滤波器搞出来 32 幅图像(通道),每个都是 28 x 28 大小。后续一个 2 x 2 的取平均值 Pooling 层把维度减小一半:32 x 14 x 14 。接着是第二个卷积层。它包含 64 个 32 x 3 x 3 的滤波器。它的输出维度是 64 x 14 x 14 。注意该卷积层的输入是 32 个 channel ,每个 14 x 14 大小。可以看作 32 x 14 x 14 的一个 3 维输入。该层的滤波器是 32 x 3 x 3 的一个 3 维滤波器。该层的输出维度是 64 x 14 x 14 。后面再续一个 2 x 2 的取平均值 Pooling 层,输出维度:64 x 7 x 7 。接着是一个展平层,没有运算也没有参数,只变化一下数据形状:把 64 x 7 x 7 展平成了 3136 元向量。该 3136 元向量送给后面一个三层的全连接神经网络。该网络的结构是 1000 x 1000 x 10 。两个隐藏层各有 1000 个神经元,最后的输出层有 10 个神经元,代表 10 个数字。假如第六个输出为 1 ,其余输出为 0 ,就表示网络判定这个手写数字为 “5”(数字 “0” 占第一个输出,所以 “5” 占第六个输出)。数字 “5” 就编码成了:\left( \begin{array}{ccc} 0\\0\\0\\0\\0\\1\\0\\0\\0\\0 \end{array}\right) 训练集和测试集的数字标签都这么编码( one-hot 编码)。全连接神经网络的隐藏层的激活函数采用 Sigmoid ,输出层的激活函数采用 Linear 。误差函数采用均方误差 mse 。优化算法采用随机梯度下降 SGD 。SGD 是梯度下降的一个变体。它并不是用全体样本计算 e 的梯度,而是每次迭代使用随机选择的一部分样本来计算。学习速率 LR 初始为 0.01 ,每次迭代以 1e-6 的比例衰减。以 0.9 为参数设置冲量。训练过程持续 10 轮( epoch )。注意这里 10 轮不是指当前解在解空间只运动 10 步。一轮是指全部 7500 个训练样本都送进网络迭代一次。每次权值更新以 32 个样本为一个 batch 提交给算法。图 4.3 展示了随着训练进行,mse 以及分类正确率( accuracy )的变化情况( 横坐标取了 log )。图 4.3该 CNN 在测试集上的正确率( accuracy )是 96.12%,各数字的准确率( precision ) / 召回率( recall )/ f1-score 如下:
precision
recall
f1-score
support
0
0.96
0.98
0.97
252
1
0.99
0.99
0.99
281
2
0.98
0.94
0.96
240
3
0.97
0.95
0.96
258
4
0.96
0.92
0.94
239
5
0.98
0.95
0.97
219
6
0.96
0.99
0.97
273
7
0.97
0.97
0.97
259
8
0.92
0.96
0.94
231
9
0.91
0.97
0.94
248
avg / total
0.96
0.96
0.96
2500
训练完成神经网络后,最有趣的是将其内部权值以某种方式展现出来。看着那些神秘的、不明所以的连接强度最后竟产生表观上有意义的行为,不由让我们联想起大脑中的神经元连接竟构成了我们的记忆、人格、情感 ... 引人遐思。在 CNN 上就更适合做这种事情。因为卷积层训练出来的是滤波器。用这些滤波器把输入图像滤一滤,看看 CNN 到底“看到”了什么。图 4.4 是该 CNN 第一卷积层对一个手写数字 “5” 的 32 个输出。图 4.4接下来看一看第二卷积层输出的 64 幅图像。图 4.5这些就是 CNN 经两步滤波后“看到”的信息。现在将展平层的 3136 元输出呈现出来。呈现方式是:“0”~“9” 十个数字各取 100 个(共 1000 个),将对每一个样本的输出作为一行,得到一副 1000 x 3136 大小的图像,根据数值用伪彩色呈现出来。如图 4.6 。图 4.6是否能从中看到 10 个条带,每个条带对应同一个数字的 100 个样本?再把两个全连接层的输出以同样的方式显示出来,是两个 1000 x 1000 的伪彩色图。如图 4.7 。图 4.7经过各卷积层、采样层和全连接层,信息表示的抽象程度逐层提高。CNN 就这样“认出”了手写数字。多层的 CNN 逐层提高了“逻辑深度”,这就是 “Deep Learning” 的含义。最后把代码附上。CNN 实现使用的是 keras 库。数据集来自 kaggle :这里。import pandas as pd
from keras.models import Sequential
from keras.layers import Dense, Flatten, Reshape, AveragePooling2D, Convolution2D, Activation
from keras.utils.np_utils import to_categorical
from keras.utils.visualize_util import plot
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, accuracy_score, confusion_matrix
from keras.callbacks import Callback
from keras.optimizers import SGD
class LossHistory(Callback):
def __init__(self):
Callback.__init__(self)
self.losses = []
self.accuracies = []
def on_train_begin(self, logs=None):
pass
def on_batch_end(self, batch, logs=None):
self.losses.append(logs.get('loss'))
self.accuracies.append(logs.get('acc'))
history = LossHistory()
data = pd.read_csv("train.csv")
data = data.sample(n=10000, replace=False)
digits = data[data.columns.values[1:]].values
labels = data.label.values
train_digits, test_digits, train_labels, test_labels = train_test_split(digits, labels)
train_labels_one_hot = to_categorical(train_labels)
test_labels_one_hot = to_categorical(test_labels)
model = Sequential()
model.add(Reshape(target_shape=(1, 28, 28), input_shape=(784,)))
model.add(
Convolution2D(nb_filter=32, nb_row=3, nb_col=3, dim_ordering="th", border_mode="same", bias=False, init="uniform"))
model.add(AveragePooling2D(pool_size=(2, 2), dim_ordering="th"))
model.add(
Convolution2D(nb_filter=64, nb_row=3, nb_col=3, dim_ordering="th", border_mode="same", bias=False, init="uniform"))
model.add(AveragePooling2D(pool_size=(2, 2), dim_ordering="th"))
model.add(Flatten())
model.add(Dense(output_dim=1000, activation="sigmoid"))
model.add(Dense(output_dim=1000, activation="sigmoid"))
model.add(Dense(output_dim=10, activation="linear"))
with open("digits_model.json", "w") as f:
f.write(model.to_json())
plot(model, to_file="digits_model.png", show_shapes=True)
opt = SGD(lr=0.01, decay=1e-6, momentum=0.9, nesterov=True)
model.compile(loss="mse", optimizer=opt, metrics=["accuracy"])
model.fit(train_digits, train_labels_one_hot, batch_size=32, nb_epoch=10, callbacks=[history])
model.save_weights("digits_model_weights.hdf5")
predict_labels = model.predict_classes(test_digits)
print(classification_report(test_labels, predict_labels))
print(accuracy_score(test_labels, predict_labels))
print(confusion_matrix(test_labels, predict_labels))
用 lena 图训练 sobel 算子的代码:from keras.models import Sequential
from keras.layers import Convolution2D
from keras.callbacks import Callback
from PIL import Image
import numpy as np
from scipy.ndimage.filters import convolve
class LossHistory(Callback):
def __init__(self):
Callback.__init__(self)
self.losses = []
def on_train_begin(self, logs=None):
pass
def on_batch_end(self, batch, logs=None):
self.losses.append(logs.get('loss'))
lena = np.array(Image.open("lena.png").convert("L"))
lena_sobel = np.zeros(lena.shape)
# sobel 算子。
sobel = np.array([
[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]
])
# 计算卷积:用 sobel 算子滤波。结果保存在 lena_sobel 中。
convolve(input=lena, output=lena_sobel, weights=sobel, mode="constant", cval=1.0)
# 将像素值调整到 [0,255] 区间并保存 sobel 算子滤波后的 lena 图。
lena_tmp = np.uint8((lena_sobel - lena_sobel.min()) * 255 / (lena_sobel.max() - lena_sobel.min()))
Image.fromarray(lena_tmp).save("lena_sobel.png")
# 将原始 lena 图和 sobel 滤波 lena 图转换成 (1, 1, width, height) 尺寸。第一个 1 表示训练集只有一个样本。第二个 1 表示样本只有一个 channel 。
X = lena.reshape((1, 1) + lena.shape)
Y = lena_sobel.reshape((1, 1) + lena_sobel.shape)
# 建一个神经网络模型。
model = Sequential()
# 只添加一个卷积层。卷积层只有一个滤波器。滤波器尺寸 3x3 。输入维度顺序是 "th" 表示 (channel, width, height) 。输入尺寸是 (channel, width, height) 。不要偏执置。
model.add(
Convolution2D(nb_filter=1, nb_row=3, nb_col=3, dim_ordering="th", input_shape=X.shape[1:], border_mode="same",
bias=False, init="uniform"))
# 代价函数取 mse 。优化算法取 rmsprop 。
model.compile(loss="mse", optimizer="rmsprop", metrics=["accuracy"])
history = LossHistory()
# 训练 10 轮,每轮保存一下当前网络输出图像。
for i in np.arange(0, 10):
lena_tmp = model.predict(X).reshape(lena.shape)
lena_tmp = np.uint8((lena_tmp - lena_tmp.min()) * 255 / (lena_tmp.max() - lena_tmp.min()))
Image.fromarray(lena_tmp).save("lena_sobel_stage_{:d}.png".format(i))
print("lena_sobel_stage_{:d}.png saved".format(i))
model.fit(X, Y, batch_size=1, nb_epoch=200, verbose=1, callbacks=[history])
print("Epoch {:d}".format(i + 1))
lena_tmp = model.predict(X).reshape(lena.shape)
lena_tmp = np.uint8((lena_tmp - lena_tmp.min()) * 255 / (lena_tmp.max() - lena_tmp.min()))
Image.fromarray(lena_tmp).save("lena_sobel_stage_final.png")
五、参考书目

我要回帖

更多关于 神经网络算法的基本原理 的文章