从零开始的深度学习(一) 经典CNN网络 LeNet-5

从零开始的深度学习(一) 经典CNN网络 LeNet-5

之前的四篇博客围绕着一个大作业项目来进行的入门,由于小白初涉,因此行文中有时侧重于某些并不重要的东西,同时也忽略了许多其实蛮重要的东西,再加上存在些许小错漏,是以第五篇和第六篇就这样胎死腹中了hhh。

但是不打紧,走过的弯路是走回正路的基础,虽然以我浅薄的认识恐怕目前还是在弯路上,害,总会走上正确的路的。

经过前面大作业项目的学习,再加上最近读了一些文献,也实际接触、跟着大佬学习了一些例如检测、语义分割之类的实际的项目,接触了除PyTorch之外的如caffeTersorFlow等框架。推倒了之前建立的认识,重新建立了新的脑子里的认识与知识框架hhh,所以,从头再来,小牛就是要,反刍!

那么在这一新启动的系列中,将会弱化PyTorch这一框架的篇幅,将中心侧重到原理性的东西上,着重讨论大量的概念问题与数学问题,当然,在具体的代码实现部分使用的还是用的最熟的PyTorch

那么闲言少叙,先理一下文章的框架,这一篇文章将从(a). 什么是神经网络、(b). 卷积神经网络两个部分来详述。

神经网络 (NN, Neural Network)

神经网络的结构

大家在高中学生物时想必都学过神经元这个东西,神经元由细胞体和突起两部分组成,作为神经系统最基本的结构和功能单位,具有联络和整合输入信息并传出信息的作用。(别问,问就是百度百科)

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第1张图片
图源 百度百科

外行人简单来理解就是,细胞体用来整合输入信息并输出新的信息,而突起则负责进行传递和接收,在这其中,最为神奇的是,在神经系统中传导的明明只是简单的信号,但是我们大脑中大量的神经元协同工作之后,却能指挥我们完成如此多的事情。

那么,以此为背景,是否我们也可以尝试在计算机上构建出一个类似的神经网络,其全部由简单结构组成,每一个简单结构也只进行十分基础简单的工作,但是聚合在一起,却完成一个复杂的工作呢?

由此,神经网络被提了出来。

首先,我们定义一个圆圈,来代表一个神经元模型,为其命名为感知器(perceptron)

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第2张图片

再创建一个感知器后,我们用有向线段来连接他们,如下所示。

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第3张图片

那么我们想通过这个来做什么呢?

假定说我们指定小蓝内存储了6这个值,然后我们指定有向线段要做的事情就是,给小蓝内的值乘3,那么显而易见,我们知道,小橙内的值就会是18,由此,我们建立了一个映射。

那么我们继续添加,得到一个这样的结构,

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第4张图片

我们给小蓝内的值分别命名为 x 1 x_1 x1 x 2 x_2 x2 x 3 x_3 x3,然后我们延续刚刚定义的,有向线段的作用是给小蓝乘上一个值,于是我们分别给这个值命名为 w 1 w_1 w1 w 2 w_2 w2 w 3 w_3 w3

于是,小橙的值便显而易见了,我们称其为 y y y,那么就有 y = x 1 × w 1 + x 2 × w 2 + x 3 × w 3 y={x_1}\times{w_1}+{x_2}\times{w_2}+{x_3}\times{w_3} y=x1×w1+x2×w2+x3×w3

ok,有了这个基础之后我们继续下定义,我们将相同颜色的结点称之为一个层,于是我们再添加一个层进去,于是就有,

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第5张图片

如果用矩阵来表示的话,我们称小蓝这个 3 × 1 {3}\times{1} 3×1的矩阵为 A 1 A_1 A1,小紫这个 4 × 1 {4}\times{1} 4×1的矩阵为 A 2 A_2 A2,小橙也算作一个名为 A 3 A_3 A3的矩阵,在小蓝和小紫之间的有向线段我们定义一个 4 × 3 {4}\times{3} 4×3的矩阵,为其命名为为 W 1 W_1 W1,每行对应一个小蓝分别指向四个小紫的有向线段,同样的,我们为小紫和小橙之间的有向线段定义一个名为 W 2 W_2 W2 1 × 4 {1}\times{4} 1×4的矩阵。

由此,我们就可以得到说, A 2 = W 1 A 1 A_2={W_1}{A_1} A2=W1A1 A 3 = W 2 A 2 = W 2 W 1 A 1 A_3={W_2}{A_2}={W_2}{W_1}{A_1} A3=W2A2=W2W1A1

那么这时候,有心的小伙伴就会发现了一个问题,那如果这样的话,其实多加的隐含层并没有什么意义呀,按照你这样的线性关系的话,我完全可以将 W 2 W 1 {W_2}{W_1} W2W1合并起来命名为 W W W,这样子就可以省略掉隐含层,直接得到 A 3 = W A 1 A_3={W}{A_1} A3=WA1呀,那添加的这个层有什么意义呢?

同时呢,另外的有心的小伙伴也发现了另一个问题,相比较与线性情况,实际情况下非线性情况是占据了大部分的情况的,那你这个模型岂不是,没什么意义。

好问题!因此,我们需要对这个模型,再加上一点点的佐料——激活函数

我们把目光聚焦到小紫身上,我们发现,相比较与只向外输出值的小蓝和只接收值的小橙,可怜的小紫似乎,又承担了接收的任务,又有输出的任务,这可太累了,那怎么办呢?

外包!

回到我们只有小蓝和小橙的情况下,小橙找来了外包公司小绿,让小绿来做接收的任务,这样如果小橙后面还有层的话,小橙就可以只做输出的任务了。

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第6张图片

同时呢,我们给小绿和小橙之间的有向线段定义一个非线性的映射,我们使用常用的一个激活函数,名为Sigmoid的函数为例。

Sigmoid函数能够将变量映射到[0, 1]之间,一般常用来做二分类。

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第7张图片

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第8张图片

这样一来,一个简单的神经网络就构建出来了。

ok,我们现在有了模型了,既然我们是人工智能,那么,它到底要怎么来使用才能显得,很智能呢?

我们回过头来再看一下我们的模型,在这个模型当中,我们未知的东西,有各结点的值,和节点之间有向线段上的权重值 w w w,而一旦这些值全部都知晓了,那么自然,计算机一定是能够去输出一个结果的。

根据上面的结构,似乎我们的神经网络是可以输出任意一个值的,那么只要我们将实际情况和输出的值做一个绑定,例如假如我们想要判断当前这个图片到底是猫还是狗,计算机输出的结果如果大于0那么这个图片就是猫,小于0就是狗。

虽然是人工智能,但是它倒也没智能到,能猜到你脑子里想去输入的东西是什么,所以,我们需要给定输入层结点的值,那么,似乎智能的地方,就落在了权重值上。

那么,计算机有没有智能到,能够自动将权重值计算出来呢?

回想一下人是如何学习的,我们在牙牙学语的时候,爸爸妈妈告诉我们,这个是猫从零开始的深度学习(一) 经典CNN网络 LeNet-5_第9张图片,这个是狗从零开始的深度学习(一) 经典CNN网络 LeNet-5_第10张图片,可能看一遍我们还是分不清,但是当我们看得次数多了,那么我们就认识了什么是猫而什么是狗。

生(百)物(度)学告诉我们,我们认识的过程就是不断的强化相关的突触,最终形成了一个能够正确认识的神经结构。

那么,计算机是否也可以这样子来学习呢?是否可以通过不断的学习来使得我们的神经网络中的权重值,修正到能够智能地去达成我们的目的呢?

反向传播 BP算法

BP(Back Propagation)算法为自动修正学习权重值提供了理论可能性。

我们像学习新东西一样,首先任意给定权重值以初始值,然后送给它学习大礼包——五年中考 三年模拟王后雄。将习题送入网络,初时,网络自然像个学渣,输出出来的结果和参考答案中间差了一个马里亚纳海沟。

我们称这个差距为损失,那么自然,我们需要想办法去把这个巨大的损失给降下去。

这个时候,BP算法就像一位好老师,通过多元函数的链式法则这一利器,利用梯度下降法逐渐去降低损失,直至把学渣训练成学霸。

接下来我们用一个实例来感受一下什么是BP算法。

我们给定一个两层三个结点的简单结构。

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第11张图片

在激活函数部分我们选用上述的Sigmoid函数,同时我们在损失部分选取一个同样简单的,名为均方误差(MSE, Mean Square Error)的损失函数来举例。

l o s s = 1 N × ∑ i = 1 N ( y i − y i ^ ) 2 loss=\frac{1}{N}\times\sum_{i=1}^N{(y_i-\hat{y_i})}^2 loss=N1×i=1N(yiyi^)2

其中 y y y代表我们模型的当前输出,而 y ^ \hat{y} y^代表模型的预期输出。

那么这个损失函数的定义理解起来就较为容易了,即将所有的当前输出与预期输出作差后将其平方值相加取平均,因此,这个值越大就说明离目标越远,符合我们上述的损失的定义。

此时,我们就可以来看这个模型,用数学语言描述便是下式:

z = x 1 × w 1 + x 2 × w 2 + b z=x_1\times w_1+x_2\times w_2+b z=x1×w1+x2×w2+b
y = s i g m o i d ( z ) = 1 1 + e − z y=sigmoid(z)=\frac{1}{1+e^{-z}} y=sigmoid(z)=1+ez1
l o s s = ( y − y ^ ) 2 loss=(y-\hat{y})^2 loss=(yy^)2

由于我们的输入值实际上是固定的常数,因此可以发现,这里面的变量就是权重值 w w w和偏置值 b b b,同时,我们如果将偏置值结点的值看作是1,那么其原本的值也可看作是权重值,因此下文将直接用权重值来描述。

因此上式完全可以看作, l o s s = f ( w 1 , w 2 ) loss=f(w_1,w_2) loss=f(w1,w2),而我们的目的就是去最小化 l o s s loss loss,因此我们可以去求偏导去寻找 l o s s loss loss的最小值或极小值。

∂ l o s s ∂ w 1 = ∂ l o s s ∂ y × ∂ y ∂ z × ∂ z ∂ w 1 \frac{\partial{loss}}{\partial{w_1}}=\frac{\partial{loss}}{\partial{y}}\times \frac{\partial{y}}{\partial{z}}\times \frac{\partial{z}}{\partial{w_1}} w1loss=yloss×zy×w1z

而这拆开的三项又分别可以快速计算得出:

∂ z ∂ w 1 = x 1 \frac{\partial{z}}{\partial{w_1}}=x_1 w1z=x1
∂ y ∂ z = y × ( 1 − y ) \frac{\partial{y}}{\partial{z}}=y\times(1-y) zy=y×(1y)
∂ l o s s ∂ y = 2 × ( y − y ^ ) \frac{\partial{loss}}{\partial{y}}=2\times(y-\hat{y}) yloss=2×(yy^)

由此便可计算出:

∂ l o s s ∂ w 1 = ∂ l o s s ∂ y × ∂ y ∂ z × ∂ z ∂ w 1 \frac{\partial{loss}}{\partial{w_1}}=\frac{\partial{loss}}{\partial{y}}\times \frac{\partial{y}}{\partial{z}}\times \frac{\partial{z}}{\partial{w_1}} w1loss=yloss×zy×w1z

          = 2 × ( y − y ^ ) × y × ( 1 − y ) × x 1 \ \ \ \ \ \ \ \ \ =2\times(y-\hat{y})\times{y}\times(1-y)\times{x_1}          =2×(yy^)×y×(1y)×x1

那么到了这里,又产生一个新的问题, ∂ l o s s ∂ w \frac{\partial{loss}}{\partial{w}} wloss又是如何去调整 l o s s loss loss,亦或者是将其调整到哪里算是合适呢?

Gradient Descent

我们知道,现在的目的是最小化我们的损失函数,举一个形象化的例子,我们作为一个伞兵,目的地是一座名为损失函数的高山中,它的山洼处最低点,但是这个任务艰巨的地方是什么呢,我们在跳伞前眼镜碎了,高度近视的我们,并不清楚这座山的全貌,以及到底山洼最低点在哪里,我们只能看到身边半径五米大小的环境,而超出五米的地方,俗称,“战争迷雾”。坏消息总是接踵而至,我们在下落的时候遇到了强风,所以我们的伞兵一号只知道自己在山上,却并不清楚自己到底在山的哪里。

那么伞兵一号应该如何去寻找自己的任务目标——山洼处最低点呢?

他环顾了一下自己能看到的半径五米大小的范围内,找到了一个较低的方向,朝着这个方向走了一步,然后不断重复这一过程,那么大概率来说,他最终会找到任务目标的。

在这个例子中,伞兵不知道自己所处的位置,就像我们随机给定的权重初始值,而我们如何去找一个所谓的“视野范围内”的较低位置的方向呢?

梯度,即 ▽ f ( x , y ) = { ∂ f ∂ x , ∂ f ∂ y } = f x ( x , y ) i ⃗ + f y ( x , y ) j ⃗ \bigtriangledown{f(x,y)}=\{\frac{\partial{f}}{\partial{x}},\frac{\partial{f}}{\partial{y}}\}=f_x(x,y)\vec{i}+f_y(x,y)\vec{j} f(x,y)={xf,yf}=fx(x,y)i +fy(x,y)j ,其表示函数在该点处沿着该方向(此梯度的方向)变化最快,变化率最大(为该梯度的模)。

而梯度下降法本质上其实也就是,通过一步步的迭代求解,得到最小化的损失函数以及模型参数值。

因此,假如我们给定一个人为设置的学习速率 η \eta η,也可以理解为步长,还拿伞兵的例子来形象化举例的话,就是他可以选择是走十步再停下来环顾四周重新确定方向,也可以只走五步就停下来环顾四周确定方向。那么这两种选择的优劣就在于,走十步的话,停下来找方向的次数就少,那么整体效率会高,但是可能在方向的选取上就不如只走五步的方向选取更加精确了。

类比于此,我们取梯度的负值作为每次更新的方向,于是我们可以得到这样的算式:

w i ∗ = w i − η × ∂ l o s s ∂ w i w_i^*=w_i-\eta\times\frac{\partial{loss}}{\partial{w_i}} wi=wiη×wiloss

结论

综上所述,我们就可以得窥神经网络的基础了。

首先我们构建好一个神经网络结构,首先对其进行训练,送入大量的训练数据,网络经过前向传播计算出损失,再通过反向传播用损失去调整网络中的权重值,不断迭代训练后我们就可以得到一个训练好了的模型。

然后我们就可以拿我们并不知道结果的数据送入模型当中,希望模型能够给我们以正确的结果。

整个过程就像,我们高中学习了三年,用有标准答案的习题训练,直至最后,我们去参加高考,并希望通过我们这三年的学习在高考卷子上做出的答案和标准答案的差距,足够小。

实例:异或门

我们知道,一个逻辑门是接收0或1的信号,经过处理后输出一个0或1的信号的,而异或门的功能就是,接收两个信号,信号相同输出0,信号不同则输出1。

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第12张图片

根据其逻辑电路的启发,我们尝试建立一个三层的神经网络来实现。

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第13张图片

其中, z z z x x x之间的有向线段表示非线性的激活函数,而 x x x z z z之间的有向线段则是线性的,同时添加结点 b b b作为偏置值,这样我们的线性函数的表现性将会更好。

那么,我们就希望,在我们给定 x 11 x_{11} x11 x 12 x_{12} x12两个结点的值之后,计算机可以自动的告诉我们,这个异或门的输出结果。

同样,我们还是取基础的SigmoidMSE来便于理解。

使用PyTorch的部分代码如下:

模型定义部分:

class xorGate(nn.Module):
    def __init__(self):
        super(xorGate, self).__init__()
        self.fc1 = nn.Linear(2, 2)
        self.fc2 = nn.Linear(2, 1)

    def forward(self, x):
        result = torch.sigmoid(self.fc1(x))
        result = torch.sigmoid(self.fc2(result))
        return result

训练部分:

train_data = Variable(train_data)
target_data = Variable(target_data, requires_grad=False)
output = xor_gate(train_data)
batch_loss = loss_fn(output, target_data)
training_loss += batch_loss.data
optimizer.zero_grad()
batch_loss.backward()
optimizer.step()

下图展现了当每一轮次迭代次数分别为10、100、1000时的结果,可以看到,在总数为4000的测试数据中,随着训练次数的增加,正确率明显上升。

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第14张图片从零开始的深度学习(一) 经典CNN网络 LeNet-5_第15张图片从零开始的深度学习(一) 经典CNN网络 LeNet-5_第16张图片

问题补充

泛化误差(Generalization Error)

过拟合

在我们上文的讨论中,或许会有好奇的小伙伴提出一个问题,看起来训练数据量越大,我们的效果就越好,那么为什么不直接就把数据量放到特别大呢?

因此,我们在这里先给出一个名为过拟合的概念。

世界上没有两片相同的叶子,而我们的训练过程,说白了,就是让我们的模型尽可能去拟合我们的训练数据集的一些特征,但是,凡事都需要有一个度。

假如说我们在教一个小孩子辨认马,结果动物园里并没有白色的马,小孩每天都去相同的动物园,对园里褐色的马反复去看,结果妈妈指着电视上的唐僧说,唐僧骑的是不是马呢?小孩看了看颜色,摇摇头说,那不是马,马都是褐色的。这个当代的白马非马,就属于过拟合了。

当然,如果这个小孩子去动物园就瞟了一眼马就跑去看大象了,那下次他或许会指着山羊说,看,大马。这叫做,欠拟合。

因此,我们要选择大的数据集来训练出我们的参数,但是也要注意并非越大越好。

偏差-方差分解(bias-variance decompostion)

这里引入一个新的概念,泛化误差,它被用来衡量一个学习机器推广未知数据的能力,即根据从样本数据中学习到的规则能够应用到新数据的能力。

泛化误差越小,表示其推广能力越强,泛化性能越优。

再引入三个概念,偏差、方差、噪声。

偏差度量了学习算法的期望预测与真实结果的偏离程度,即刻画了学习算法本身的拟合能力。

方差度量了同样大小的训练集的变动所导致的学习性能的变化,即刻画了数据扰动所造成的影响。

噪声表达了在当前任务上任何学习算法所能达到的期望泛化误差的下界,即刻画了学习问题本身的难度。

那么我们可以通过偏差-方差分解对学习算法的期望泛化错误率进行拆解,得到:泛化误差可分解为偏差、方差、噪声之和

推导过程如下:

x x x为测试样本, D D D为数据集, y D y_D yD x x x在数据集 D D D上的标记, y y y为真实标记。
f ( x ; D ) f(x;D) f(x;D)为由训练集 D D D学得的模型 f f f在测试样本 x x x上的预测输出, f ˉ ( x ) \bar{f}(x) fˉ(x)为模型 f f f x x x的期望预测。

因此有:

针对不同的数据集 D D D f f f x x x的预测值取期望,

f ˉ ( x ) = E D [ f ( x ; D ) ] \bar{f}(x)=E_D[f(x;D)] fˉ(x)=ED[f(x;D)]

使用样本数相同的不同训练集产生的方差,

v a r ( x ) = E D [ ( f ( x : D ) − E D [ f ( x ; D ] ) 2 ] = E D [ ( f ( x ; D ) − f ˉ ( x ) ) 2 ] var(x)=E_D[(f(x:D)-E_D[f(x;D])^2]=E_D[(f(x;D)-\bar{f}(x))^2] var(x)=ED[(f(x:D)ED[f(x;D])2]=ED[(f(x;D)fˉ(x))2]

噪声 ϵ = y − y D \epsilon=y-y_D ϵ=yyD~ N ( 0 , σ 2 ) N(0,{\sigma}^2) N(0,σ2),即假定 E ϵ = 0 E{\epsilon}=0 Eϵ=0

算法的期望预测与真实结果之间的差值,偏差,

b i a s = y − f ˉ ( x ) bias=y-\bar{f}(x) bias=yfˉ(x)

那么我们就可以通过这些来对泛化误差进行分解:

E ( f ; D ) = E D [ ( f ( x ; D ) − y D ) 2 ] E(f;D)=E_D[(f(x;D)-y_D)^2] E(f;D)=ED[(f(x;D)yD)2]
              = E D [ ( f ( x ; D ) − f ˉ ( x ) + f ˉ ( x ) − y D ) 2 ] \ \ \ \ \ \ \ \ \ \ \ \ \ =E_D[(f(x;D)-\bar{f}(x)+\bar{f}(x)-y_D)^2]              =ED[(f(x;D)fˉ(x)+fˉ(x)yD)2]
              = E D [ ( f ( x ; D ) − f ˉ ( x ) ) 2 ] + E D [ ( f ˉ ( x ) − y D ) 2 ] + 2 E D [ ( f ( x ; D ) − f ˉ ( x ) ) ( f ˉ ( x ) − y D ) ] \ \ \ \ \ \ \ \ \ \ \ \ \ =E_D[(f(x;D)-\bar{f}(x))^2]+E_D[(\bar{f}(x)-y_D)^2]+2E_D[(f(x;D)-\bar{f}(x))(\bar{f}(x)-y_D)]              =ED[(f(x;D)fˉ(x))2]+ED[(fˉ(x)yD)2]+2ED[(f(x;D)fˉ(x))(fˉ(x)yD)]
              = v a r ( x ) + E D [ ( f ˉ ( x ) − y + y − y D ) 2 ] \ \ \ \ \ \ \ \ \ \ \ \ \ =var(x)+E_D[(\bar{f}(x)-y+y-y_D)^2]              =var(x)+ED[(fˉ(x)y+yyD)2]
              = v a r ( x ) + E D [ ( f ˉ ( x ) − y ) 2 ] + E D [ ( y − y D ) 2 ] + 2 E D [ ( f ˉ ( x ) − y ) ( y − y D ) ] \ \ \ \ \ \ \ \ \ \ \ \ \ =var(x)+E_D[(\bar{f}(x)-y)^2]+E_D[(y-y_D)^2]+2E_D[(\bar{f}(x)-y)(y-y_D)]              =var(x)+ED[(fˉ(x)y)2]+ED[(yyD)2]+2ED[(fˉ(x)y)(yyD)]
              = v a r ( x ) + b i a s 2 ( x ) + ϵ 2 \ \ \ \ \ \ \ \ \ \ \ \ \ =var(x)+bias^2{(x)}+{\epsilon}^2              =var(x)+bias2(x)+ϵ2

其中,

E D [ ( f ( x ; D ) − f ˉ ( x ) ) ( f ˉ ( x ) − y D ) ] = E D ( f ( x ; D ) f ˉ ( x ) ) − E D ( f ˉ 2 ( x ) ) + E D ( y D ( f ˉ ( x ) − f ( x ; D ) ) ) E_D[(f(x;D)-\bar{f}(x))(\bar{f}(x)-y_D)]=E_D(f(x;D){\bar{f}(x)})-E_D(\bar{f}^2(x))+E_D(y_D(\bar{f}(x)-f(x;D))) ED[(f(x;D)fˉ(x))(fˉ(x)yD)]=ED(f(x;D)fˉ(x))ED(fˉ2(x))+ED(yD(fˉ(x)f(x;D)))
                                                         = f ˉ ( x ) E D ( f ( x ; D ) ) − f ˉ 2 ( x ) + E D ( ( y + ϵ ) ( f ˉ ( x ) − f ( x ; D ) ) ) \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ =\bar{f}(x)E_D(f(x;D))-\bar{f}^2(x)+E_D((y+\epsilon)(\bar{f}(x)-f(x;D)))                                                         =fˉ(x)ED(f(x;D))fˉ2(x)+ED((y+ϵ)(fˉ(x)f(x;D)))
                                                         = f ˉ 2 ( x ) − f ˉ 2 ( x ) + E D ( y f ˉ ( x ) ) − E D ( y f ( x ; D ) ) + E D ( ϵ ( f ˉ ( x ) − f ( x ; D ) ) ) \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ =\bar{f}^2(x)-\bar{f}^2(x)+E_D(y\bar{f}(x))-E_D(yf(x;D))+E_D({\epsilon}(\bar{f}(x)-f(x;D)))                                                         =fˉ2(x)fˉ2(x)+ED(yfˉ(x))ED(yf(x;D))+ED(ϵ(fˉ(x)f(x;D)))
                                                         = 0 \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ =0                                                         =0

E D [ ( f ˉ ( x ) − y ) ( y − y D ) ] = E D ( f ( x ; D ) f ˉ ( x ) ) − E D ( f ˉ 2 ( x ) ) + E D ( y D ( f ˉ ( x ) − f ( x ; D ) ) ) E_D[(\bar{f}(x)-y)(y-y_D)]=E_D(f(x;D){\bar{f}(x)})-E_D(\bar{f}^2(x))+E_D(y_D(\bar{f}(x)-f(x;D))) ED[(fˉ(x)y)(yyD)]=ED(f(x;D)fˉ(x))ED(fˉ2(x))+ED(yD(fˉ(x)f(x;D)))
                                         = 2 ( f ˉ ( x ) − y ) E D ( y − y D ) \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ =2(\bar{f}(x)-y)E_D(y-y_D)                                         =2(fˉ(x)y)ED(yyD)
                                         = 2 ( f ˉ ( x ) − y ) E D ( ϵ ) \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ =2(\bar{f}(x)-y)E_D({\epsilon})                                         =2(fˉ(x)y)ED(ϵ)
                                         = 0 \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ =0                                         =0

由上式可知,偏差与方差的值在一定程度上呈现反相关,这被称作偏差-方差窘境(bias-variance dilemma)。

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第17张图片
图源 周志华 西瓜书

也就是说,当给定学习任务时,假定我们能控制学习算法的训练程度。

  1. 训练不足时,学习器的拟合能力不够强,训练数据的扰动不足以使学习器显著变化,偏差主导泛化错误率
  2. 训练程度加深,学习器的拟合能力增强,训练数据的扰动被学习器学到,方差主导泛化错误率
  3. 训练充足之后,学习器的拟合能力非常强,训练数据的轻微扰动都使学习器显著变化,过拟合

MSESigmoid不合适

有的时候,一加一的结果会小于二,不合适就是不合适,倒也不必硬挤,MSESigmoid就是这样的一对儿,二者配合之后,在理论上来说,模型初始学习速度会很慢。

在反向传播中,我们会需要去考虑 ∂ l o s s ∂ w \frac{\partial{loss}}{\partial{w}} wloss的大小问题,而当MSESigmoid配合时,如上文我们推导过的一样,其式为:

∂ l o s s ∂ w 1 = [ 2 N × ∑ i = 1 N ( y i − y i ^ ) ] × y × ( 1 − y ) × x 1 \frac{\partial{loss}}{\partial{w_1}}=[\frac{2}{N}\times\sum_{i=1}^N{(y_i-\hat{y_i})}]\times y\times(1-y)\times x_1 w1loss=[N2×i=1N(yiyi^)]×y×(1y)×x1

那么我们观察这一公式,当 y y y的值在接近 0 0 0 1 1 1时, ∂ l o s s ∂ w \frac{\partial{loss}}{\partial{w}} wloss的值将会接近于 0 0 0,而 ∂ l o s s ∂ w \frac{\partial{loss}}{\partial{w}} wloss的值与学习速度成正比,因此理论上来说,MSESigmoid的组合将会使得模型一开始的学习速度很慢。

交叉熵(CE, Cross Entropy)

这里补充一个新的激活函数——CE(Cross Entropy)交叉熵。

首先抛出结论:
若某事件仅有两种结果,则有简化的 l o s s loss loss ∂ l o s s ∂ w \frac{\partial{loss}}{\partial{w}} wloss

l o s s = − y ^ × l n ( y ) − ( 1 − y ^ ) × l n ( 1 − y ) loss=-\hat{y}\times ln(y)-(1-\hat{y})\times ln(1-y) loss=y^×ln(y)(1y^)×ln(1y)

∂ l o s s ∂ w 1 = ( y − y ^ ) × x 1 \frac{\partial{loss}}{\partial{w_1}}=(y-\hat{y})\times x_1 w1loss=(yy^)×x1

可以看到,相较之于MSE而言,CE只会受到当前值与期望值的差值所影响,且差值越大, ∂ l o s s ∂ w \frac{\partial{loss}}{\partial{w}} wloss也越大,那么学习速度也就越快,显然克服了MSE的问题。

那么,MSE的公式看起来似乎很容易理解,甚至它的名字,均方误差,听起来都很好理解,可是无论是交叉熵这个名字,还是说它的公式,似乎都很难直观的去看出来这个损失函数提出的原理,也因此,在此需要着重解释一下什么是交叉熵,以及它是如何推导出来的。

信息量

首先,我们给出一个新的概念,信息量

举个简单的例子:

事件一:Doinb拿AP佐伊斩获五杀。

事件二:Doinb决胜局拿了卡尔玛璐璐。

那么显而易见,事件一的信息量要远大于事件二,因为事件二发生的概率很大,而事件一几乎不可能发生。那么当一件越不可能的事情发生了,我们获取到的信息量就越大。也就是说,事件发生的概率和其信息量的大小应该成反比。

假设 X X X是一个离散型随机变量,其概率分布函数为 p ( x ) p(x) p(x),那么我们定义事件 X = x 0 X=x_0 X=x0的信息量为:

I ( x 0 ) = − l n ( p ( x 0 ) ) I(x_0)=-ln(p(x_0)) I(x0)=ln(p(x0))

在本节的开始为使公式更为明了简单,故预设了前提为事件仅有两种结果,而在此处我们把可能性推广到两种以上,即假设有 n n n种可能性,则每一种可能性都有一个概率 p ( x i ) p(x_i) p(xi)

举一个三种可能性的例子,FPX前打野BO的下场:

序号 事件 概率 p p p 信息量 I I I
A 终身禁赛 0.7 − l n ( 0.7 ) = 0.36 -ln(0.7)=0.36 ln(0.7)=0.36
B 禁赛三年 0.2 − l n ( 0.2 ) = 1.61 -ln(0.2)=1.61 ln(0.2)=1.61
C 原地复出还赶得上打季后赛 0.1 − l n ( 0.1 ) = 2.30 -ln(0.1)=2.30 ln(0.1)=2.30

熵用来表示所有信息量的期望
H = − ∑ i = 1 n p ( x i ) × l n ( p ( x i ) ) H=-\sum_{i=1}^n{p(x_i)\times ln(p(x_i))} H=i=1np(xi)×ln(p(xi))

      = 0.7 × 0.36 + 0.2 × 1.61 + 0.1 × 2.30 \ \ \ \ \ =0.7\times 0.36+0.2\times 1.61+0.1\times 2.30      =0.7×0.36+0.2×1.61+0.1×2.30

      = 0.804 \ \ \ \ \ =0.804      =0.804

相对熵(KL散度)

同一个随机变量 X X X有两个单独的概率分布 p ( x ) p(x) p(x) q ( x ) q(x) q(x),则用KL散度来衡量二者的差异。

通常用 p p p来表示真实分布,即预期输出 y ^ \hat{y} y^,用 q q q来表示预测分布,即模型当前输出 y y y

D K L ( p ∣ ∣ q ) = ∑ i = 1 n p ( x i ) × l n ( p ( x i ) q ( x i ) ) D_{KL}(p||q)=\sum_{i=1}^n{p(x_i)}\times ln(\frac{p(x_i)}{q(x_i)}) DKL(pq)=i=1np(xi)×ln(q(xi)p(xi))

D K L D_{KL} DKL值越小,则 p p p分布和 q q q分布越接近。

交叉熵

D K L ( p ∣ ∣ q ) = ∑ i = 1 n p ( x i ) × l n ( p ( x i ) ) − ∑ i = 1 n p ( x i ) × l n ( q ( x i ) ) D_{KL}(p||q)=\sum_{i=1}^n{p(x_i)}\times ln(p(x_i))-\sum_{i=1}^n{p(x_i)}\times ln(q(x_i)) DKL(pq)=i=1np(xi)×ln(p(xi))i=1np(xi)×ln(q(xi))

                    = − H ( p ( x ) ) + [ − ∑ i = 1 n p ( x i ) × l n ( q ( x i ) ) ] \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ =-H(p(x))+[-\sum_{i=1}^n{p(x_i)}\times ln(q(x_i))]                    =H(p(x))+[i=1np(xi)×ln(q(xi))]

即 相对熵 = − p -p p的熵 + 交叉熵

如上文所述,一般将 p p p用来表示真实分布 y ^ \hat{y} y^,故而其为一定值,因此 H ( p ( x ) ) H(p(x)) H(p(x))也为一常数,因此交叉熵即可用来直接反映真实分布和预测分布的差距。

Gradient Descent中的BGD、SGD、MBGD

批量梯度下降(Batch Gradient Descent,BGD)

BGD其实就是最朴素的梯度下降方法,其实现方法就是所有的训练数据一起进行梯度的更新,那么可以做一个简单的数学题,假如说是一个含一个隐含层的三层模型,输入层 m m m个元素,隐含层 n n n个元素,输出层 k k k个元素,那么这个模型中就会存在 m × n + n × k m\times n+n\times k m×n+n×k 个参数(theta/weights),那么如果继续增加隐含层的数量呢?同时我们也知道,对一个模型的训练也并非一次两次迭代就可以完成的,因此,即便是像MNIST这样的入门级小量数据,也可想而知其计算量将会多么庞大。

同时,BGD还存在另一个十分致命的问题,即,导数值为零时的鞍点和较差的局部最优点。

Image from Wikipedia
鞍点(Saddle point)
从零开始的深度学习(一) 经典CNN网络 LeNet-5_第18张图片
Image from Baidu
较差的局部最优点
从零开始的深度学习(一) 经典CNN网络 LeNet-5_第19张图片

那么当出现了上述的导数为零的情况时,算法将会出现不知道将向哪里继续前进的窘境。

当然,BGD也并非毫无可取之处,形象来理解的话,由于它使用的是全部的训练数据,它将会始终朝着理论上的最优方向前进,当然,相较之于如上述两大缺点来说,其优点属实有些不太够看。

随机梯度下降(Stochastic Gradient Descent,SGD)

考虑到BGD的两个最主要的致命缺陷——数据量太大和导数为零,如果仔细来分析这两个问题,数据量太大是因为一次迭代就需要所有的数据,导数为零是因为用的所有的数据所以方向始终朝着最优方向前进也因此而绕不开导数为零的点。

这样分析下来,似乎两个问题可以通过一个方法来解决——数据量。

因此,SGD算法应运而生,SGD的想法就是,每次拿出训练集中的一个数据,去进行对参数的训练,然后再拿下一个数据来对刚刚修改过的参数进行训练,直到所有数据都被输入进网络之后,这被称作一轮次。

那么SGD算法由于每次只输入一个数据,因此其迭代速度极快,而且由于每次输入数据不同,那么每个数据的所谓最优方向都是不同的,形象化理解就是,BGD每次考虑的所有数据,因此朝着整体最优解方向笔直前进,而SGD则是每此前进方向都是单个数据的最优解方向,那么参数的前进方向直观理解的话就是蜿蜒逶迤。

当然,SGD的缺点也就是这个优点,即前进方向并非笔直前进,其中包含了大量的噪声,因此经常会出现”抖动“现象。

小批量梯度下降(Mini-Batch Gradient Descent,MBGD)

综合以上两种算法的优缺点,MBGD应运而生,其每次选取一组数据称之为Mini-Batch,这样既使得一次处理的数据量较小,同时又使得不会受到某个训练样本的噪声而引起神经网络的剧烈抖动。

Image from Wikipedia
BGD SGD MBGD
红线为BGD,紫线为SGD,绿线为MBGD。(其中紫线和绿线都为本人随手绘制添加,表明其方向随意杂乱。)
从零开始的深度学习(一) 经典CNN网络 LeNet-5_第20张图片

卷积神经网络 (CNN, Convolution Neural Network)

卷积 (Convolution)

我们在前文中详细叙述了什么是神经网络,那么这一部分我们将在NN的前面,加上一个字母C,那么会带来什么变化呢?

首先,我们需要先来了解什么是卷积

先来抛出结论,卷积有提取特征的功能

位图图像

一般来说,我们常见的图像都是由一个个单独的像素所组成的位图图像,常见的图像又根据其色彩而有所划分。

  1. 灰度图,灰度图中每个像素的值都介于0~255之间,255为白色,0为黑色,而中间的值为颜色深度不同的灰色,由于其每个像素只有一个值,故称之为灰度图。

  2. RGBRGBA的彩色图,RGB为红绿蓝三个英文单词的首字母,是一个三通道的图像,同时有时还会有第四通道,即A通道,为透明度通道。它的每一个像素都是由三个0~255的值共同决定的,例如对于一个像素,其R通道为255,GB通道都为0,那么这个像素点的颜色便为红色。

卷积原理

了解了位图与像素之后,我们给定一个图像用来卷积:

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第21张图片

然后我们再指定一个小一点的图像,称之为卷积核(kernel)

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第22张图片

那么我们要做的事情就是让小橙去对应小绿上同样大小的位置,并计算一个值出来,从而形成一张新的图片,说起来不好理解,所以我们现在来操作一下。

本例中采用卷积后图像大小与原图像一致,以及卷积时不处理边框,默认值为0。

我们第一个将要处理的是原图像的(1,1)处的元素:

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第23张图片

将对应位置的元素值进行相乘后得到如下图所示:

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第24张图片

因此 ( 1 , 1 ) (1,1) (1,1)处的元素应为 − 1 − 1 − 1 + 0 + 0 + 0 + 0 + 0 + 1 = − 2 -1-1-1+0+0+0+0+0+1=-2 111+0+0+0+0+0+1=2即:

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第25张图片

如此反复处理整张图片后,即可得到最终卷积后的图像。

卷积实例

在这一实例中,我们选取两个卷积核,分别为:

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第26张图片

然后我们对一个RGB三通道彩图处理后结果如下:

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第27张图片

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第28张图片

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第29张图片

明显可以看出,对于同样一张图,采用不同的卷积核卷积出的图像明显不同,对于第一种卷积核,明显将原图中铅垂的轮廓给提取了出来,而对于第二种卷积核,明显提取出了水平轮廓。

而联想我们上文所述的神经网络,那么似乎,我们输入某一类图片,利用卷积提取出某一类事物的共有特征,并且提取的足够多,那么自然就可以轻易识别出这一事物了,研究也表明,这一过程与我们人类的认知其实是共通的,我们在辨认一个事物时,同样也是提取出了足够多的特征,凭借我们之前的认识,从而得到这个事物的属性的结论。

由此,卷积神经网络被提了出来。

卷积加入神经网络豪华午餐的三个前提

本节参考(搬运)自李宏毅老师的机器学习课程

为什么需要加入卷积

阅读过不好理解的卷积原理之后,可能会有小伙伴不耐烦地说,上文说过的神经网络不是很好吗?为什么要画蛇添足再加一个卷积呢?

好问题!

我们来考虑一个问题,不同于上述的异或门这么简单的东西,我们想要一些看起来稍微高级点的东西,尤其是,这些东西更对我们的实际生活还近一些那就更棒了。

有一天小牛想要把自己的笔记放到电脑上存储一个备份,但是小牛是一个又懒又要求很多的人,他既不想自己一个字一个字输入到电脑上,他又不想只是照一下照片放到电脑上,小牛躺在床上冥思苦想,怎么办呢?

上世纪九十年代,美国的银行就引入了一个系统来识别支票上的手写数字,而这便是大神Yann LeCun在1998年设计的用于手写数字识别的卷积神经网络——LeNet-5

Example from Wikipedia
从零开始的深度学习(一) 经典CNN网络 LeNet-5_第30张图片

形如上图所示的数字们,大小均为 28 × 28 28{\times}28 28×28的二值图(二值图为仅有黑白两个颜色的图片),我们当然可以使用前文所述的普通神经网络来训练。

那么我们来分析一下这个任务。

首先是要给定一个图片,我们去判断它是0~9中的哪一个类别,也就是说是一个分类的问题,听起来似乎和我们的异或门差不多,也是给定一组数据,然后来判断输出是0或者1的分类问题。

ok,似乎可行,那然后我们来看我们的输入,对于我们的异或门来说,输入的数据是一个两个元素的向量,可是到了我们的图片时,分辨率仅仅是一个 28 × 28 28{\times}28 28×28的二值图,这已经很小很小了,可是我们将其展开为一个向量,其元素个数就达到了惊人的 28 × 28 = 784 28{\times}28=784 28×28=784个元素,也就是说,输入层的结点个数就是 784 784 784个,而我们的输出是十个类别,那么我们的输出层结点个数就应该是 10 10 10个,假定说我们一层隐含层都不加,那么我们的这个网络里的权重个数就高达 784 × 10 = 7840 784{\times}10=7840 784×10=7840个,而事实上是,我们连异或门这样简单的结构都有一层隐含层,那,听起来更高级的图像识别,可能不含隐含层吗?那这样一来,其权重数量之庞大,计算机也想罢工。

同时呢,根据我们前文所讨论的东西,我们说我们在训练神经网络的时候,我们希望我们设置的网络结构每一层甚至于每一个结点都不是多余的,其都代表了一个基本的分类器,就像我们的异或门,为什么要这样去设置呢?

我们参考了其逻辑电路从零开始的深度学习(一) 经典CNN网络 LeNet-5_第31张图片,一个异或门是由逻辑非、逻辑或和逻辑与来共同组成的,因此我们设计的模型也是一个类似的从零开始的深度学习(一) 经典CNN网络 LeNet-5_第32张图片结构。

事实上,在文献上,根据训练的结果,也有很多人得到这样的结论,以下图来举例:

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第33张图片

第一个隐含层的结点们,最简单的分类器,它们做的事情就是检测有没有诸如绿色、黄色或者斜条纹的出现等基础元素。

第二个隐含层呢,它做的事情是去检测更复杂的东西,根据前一层的输出,它如果看到直线和横线,那这就是窗框的一部分。如果看到棕色的直条纹那就识别为木纹。

而第三个隐含层就根据前一层的输出,能做到更复杂的事情,比如用来识别蜂巢、轮胎、人的上半身等结点的存在。

根据我们的讨论,简单的神经网络过于庞大的参数,以及我们卷积能够提取特征这样的特性恰好符合我们的需求,因此CNN就这样被提了出来。

每一个结点都和下一层的所有结点进行连接,这样的层被称作全连接层(fully connected),而我们即将介绍的卷积层,将会极大减少这样的连接。

第一个前提——大多数的特征都远小于整幅图像

我们现在有了一个共识,就是说我们想要提取图片中的诸多特征,那这些特征通常来说在整幅图像中占比并不会很大,以下图为例,假定我们想要检测出这只小鸟的鸟嘴,那么它其实只占据了这幅图片中很小的一块地方。

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第34张图片

检测鸟嘴这样的事情,其实并不需要看整张图,我们只要给结点看这个小的红框里面的区域,它就可以知道这是不是一个鸟嘴,其实对人来说也是一样,我们只要看这个小的区域你就会知道说这是鸟嘴,因此,每一个结点其实只要连接到一小块的区域就足够了,并不需要连接到整张完整的图,因此这将极大减少参数的数量

第二个前提——同样的特征不同的图片中会出现在不同的位置上

我们得到的图片,基本不会存在说,相同的特征就一定会在同样的位置上,以下图为例:

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第35张图片

图片中分别有一个处于左上角的鸟嘴和一个处于中央的鸟嘴,它们有同样的形状、代表的是同样的含义,因此完全不必要去训练两个不同的探测器去探测说,左上角是否存在鸟嘴与中央是否存在鸟嘴这两件事情,我们完全可以用同样的结点、同样的参数,

但是因此它们也可以用同样的neuron、同样的参数,被同一个detector检测出来
但你并不需要训练两个不同的detector去专门侦测左上角有没有鸟嘴和中央有没有鸟嘴这两件事情,这样做太冗余了,我们要cost down(降低成本),我们并不需要有两个neuron、两组不同的参数来做duplicate(重复一样)的事情,所以我们可以要求这些功能几乎一致的neuron共用一组参数,它们share同一组参数就可以帮助减少总参数的量

第三个前提——对一幅图像进行下采样并不会对图像特征造成影响

如果我们对一幅图片进行下采样(subsampling),即将其奇数行、偶数列的像素点全部删除,那么可想而知,我们的图像将会变成原来的四分之一大小,然而神奇的是,整幅图像在我们的人眼中,并不会发生多么大的变化。

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第36张图片

那么,这一张图对于我们辨识这张图像来说并不存在什么问题,但是极大减少了图片的像素数量,自然,连带着也减少了所需的参数数量。

即,利用下采样把影像变小,以减少需要的参数量

CNN结构

基于我们前面的三个讨论,那么我们就有了充足的理由来说,我们可以将卷积引入我们的神经网络大家庭里了。

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第37张图片

那么按照我们所提出的这样的结构,我们来分析一下一张可爱的猫猫图片的大冒险。

首先猫猫将会遇到第一关,名为卷积层(convolution layer)的关卡。

卷积层

卷积层这一关,顾名思义,我们要去对我们的猫猫图像进行一个卷积操作,根据我们前文所述的机器学习的概念,我们可以把我们的卷积核看作是我们学习到的参数,那么我们假定在已经学习之后,所得到的卷积核为下图左上角的Filter 1,可以看到我们的这个卷积核所要做的就是提取一个主对角线的特征,那么我们可以看到我们用此卷积核去卷积左图时,得到的右下角的图中,便是有两个值为3的元素,即为检测出的主对角线的图案。

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第38张图片

可以看到,首先我们这一实例就满足了第一个前提,也就是我们的特征是小于我们的整幅图像的,同时在一副图中用同一个探测器(卷积核)检测到了两个相同的特征,这也就满足了我们的第二个前提。

池化层(Max Pooling)

经过了第一关卷积后,我们的猫猫图片被提取出了许多有趣的特征,那么接下来我们的第二关,猫猫图片面对的是一个被称作池化层的大魔王。

相较于卷积层,池化层是比较简单的,实际上就是来做下采样,继续承接上一个例子,我们根据filter 1得到了一个 4 × 4 4{\times}4 4×4的矩阵,我们把这个矩阵中,每四个元素分为一组,每一组里面通过选取平均值或最大值的方式,把原来4个元素合成一个元素,相当于说是在影像中每相邻的四块区域内都挑出一块来检测,从而可以缩小我们的影响,根据我们的第三个前提,我们知道这并不会影响我们的影响特征,并且可以有效去减少我们的参数量。

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第39张图片

如此循环往复,我们就可以把我们最后提取出来的特征送入我们前述的全连接层中,去得到一个较为有效的训练结果。

原理剖析

卷积与全连接的关系

接下来我们要讨论的的是,卷积与全连接有什么关系。根据我们前文的叙述,可能会有一种感觉说,它是一个很特别的操作,感觉跟神经网络其实没一点关系,但是其实呢,它就是一个彻头彻尾的,神经网络。

卷积其实就是全连接层时,把一些权重拿掉而已,以下图为例,用绿色方框标出的卷积结果和隐含层,其实在本质上是一个东西。

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第40张图片

我们接下来详细叙述一下原因,如下图所示,我们在做卷积的时候,把卷积核放在图像的左上角,卷积后得到一个值 3 3 3。那么对于这件事情,我们可以理解为,我们现在把这个图像的 6 × 6 6{\times}6 6×6的矩阵拉直变成右边这个等同于输入层的矢量,然后,我们的 3 × 3 3{\times}3 3×3的卷积核用九种颜色来标识,换成权重的形式的话,用同样颜色的有向线段来表示,那么这些输入神经元经过这九个权重之后输入到下一层的结点值便为 3 3 3

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第41张图片

相比较而言,全连接层的下一层的结点是必须连接到所有 36 36 36个输入结点上的,但是,我们现在只用连接 9 9 9个输入结点,因为我们知道要检测一个特征,并不需要看整张图片,看 9 9 9个像素就够了,因此,我们在使用卷积的时候,就能够减少较多的权重值。

共享参数

我们继续来观察我们的卷积,当我们对我们的卷积核进行步长stride 1 1 1的移动时,可以得到如下图所示的结果。

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第42张图片

可以发现值为 3 3 3 − 1 -1 1的这两个结点,它们分别去检测在图像的两个不同位置上是否存在某个图像,因此在全连接层里它们做的是两件不同的事情,每一个结点应该有自己独立的权重值。

但是,当我们做卷积的时候,首先我们把每一个结点前面连接的权重值减少了,然后我们强迫某些结点(比如上图中输出值为 3 3 3 − 1 -1 1的两个结点),它们一定要共享一组权重,虽然这两个结点连接到的像素对象各不相同,但它们用的权重都必须是一样的,即卷积核里面的元素值,这件事情就叫做共享参数,那么当我们做这件事情的时候,用到的参数又会比原来更少一些。

CNN学到了什么

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第43张图片

在这里我们抛出来一个简单的CNN,来简单做一个讨论。

要分析第一个卷积层的卷积核是比较容易的,因为在第一个卷积层内,每一个卷积核就是一个 5 × 5 5{\times}5 5×5的矩阵,所以你只要当作一个图片来看这个矩阵,就可以知道它在探测什么东西,因此第一层的卷积核是很容易理解的。

但是当我们到了第二个卷积层的时候,它的卷积核就很难直观去理解了,它们是 16 16 16个同样为 5 × 5 5{\times}5 5×5的卷积核,但是这些卷积核的输入并不是像素,而是做完卷积再做池化的结果,因此卷积核考虑的范围并不是 5 × 5 = 25 5{\times}5=25 5×5=25个像素,而是一个长宽为 5 × 5 5{\times}5 5×5,高为 16 16 16的立方体,卷积核实际在图像上看到的范围是远大于 25 25 25个像素的,所以你就算把它拿出来,也不知道它在做什么。

卷积核做了什么

我们知道在第二个卷积层里面的 16 16 16个卷积核,每一个卷积核的输出就是一个 11 × 11 11{\times}11 11×11的矩阵,假设我们现在把第 k k k个卷积核的输出拿出来,这个矩阵里的每一个元素,我们叫它 a i j k a_{ij}^{k} aijk,上标 k k k表示这是第 k k k个卷积核,下标 i j ij ij表示它在这个矩阵里的第 i i irow,第 j j jcolumn

接下来我们定义第 k k k个卷积核的激活程度为 a k a^k ak,直观来讲就是描述现在输入的东西跟第 k k k个卷积核有多接近,激活程度有多少。

据此,第 k k k个卷积核的激活程度 a k a^k ak就定义成,它与输入进行卷积得到的输出中,所有元素之和,即:

a k = ∑ i = 1 n ∑ j = 1 n a i j k a^k=\sum_{i=1}^{n}\sum_{j=1}^{n}a_{ij}^{k} ak=i=1nj=1naijk

也就是说,我们输入一张图片,然后把这个卷积核和图像进行卷积所输出的 11 × 11 11{\times}11 11×11个值全部加起来,当作现在这个卷积核被激活的程度。

接下来,我们要讨论的是,第 k k k个卷积核的作用是什么,那我们就要找一张图片,这张图片可以让第 k k k个卷积核被激活的程度最大。因此我们现在的关键问题就是,找一个图片 x x x,它可以让我们定义的激活程度 a k a^k ak最大,即:

x ∗ = arg ⁡ max ⁡ x a k x^*=\arg \max\limits_x{a^k} x=argxmaxak

之前我们求最小值用的是梯度下降法,那现在我们求最大值用梯度上升法(gradient ascent)就可以了。

也就是说,我们现在是把输入的 x x x作为要找的参数,对它去用梯度上升法进行更新,原来在训练CNN的时候,输入是固定的,模型的参数是要用梯度下降法去找出来的。但是现在是反过来的,在这个任务中模型的参数是固定的,我们要用梯度上升法去更新这个 x x x,让它可以使激活程度 a k a^k ak最大。

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第44张图片

上图即为得到的结果, 50 50 50个卷积核理论上可以分别找 50 50 50张图像使对应的激活程度最大,这里仅挑选了其中的 12 12 12张图像作为展示,这些图像有一个共同的特征,它们里面都是一些反复出现的某种纹路,比如说第三张图像上布满了小小的斜条纹,这意味着第三个卷积核的工作就是检测图上有没有斜条纹,要知道现在每个卷积核检测的都只是图上一个小小的范围而已,所以图中一旦出现一个小小的斜条纹,这个卷积核就会被激活,相应的输出也会比较大,所以如果整张图像上布满这种斜条纹的话,这个时候它会最兴奋,卷积核的激活程度是最大的,相应的output值也会达到最大

因此每个卷积核的工作就是去检测某一种图案,检测某一种线条,上图所示的卷积核所检测的就是不同角度的线条,所以今天输入有不同线条的话,某一个卷积核会去找到让它兴奋度最高的匹配对象,这个时候它的输出值就是最大的。

全连接层做了什么

首先这里先补充一个我们上文并未详述的地方,我们前面说过,反复多次卷积+池化后,我们会将我们的结果送入全连接层中去,那么,这个地方其实是有一些问题的,因为我们的全连接层的每一层,其实都是一个一维向量,但是我们的卷积+池化所输出的结果,其实是一个多维数组,因此我们这个地方应该先做一个展开,将其从多维数组展开为一个一维向量。

那么我们自然需要去探究一下,全连接层的结点们,都起到了什么作用,那么我们就对刚刚的做法稍稍修改一下拿来继续用。

我们定义当前的某一隐含层的第 j j j个结点的输出值为 a j a_j aj,则有:

x ∗ = arg ⁡ max ⁡ x a j x^*=\arg \max\limits_x{a_j} x=argxmaxaj

用梯度上升法去找一张图像 x x x,把它丢到神经网络里面就可以算出来最大化的 a j a_j aj

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第45张图片

找到的结果如上图所示,同理这里仅取出其中的 9 9 9张图像作为展示,能够发现的是,这 9 9 9张图跟之前卷积核时所观察到的是很不一样的,刚才我们观察到的是类似纹路的东西,那是因为每个卷积核考虑的只是图上的一小部分,所以它检测的是一种纹路;但是在放入神经网络以后,每一个结点不再是只看整张图的一小部分,它开始看整张图,所以对每一个结点来说,让它最兴奋的图像,不再是纹路,而是一个完整的图形。

最终的输出结果是什么

我们这里呢,采用的是手写数字识别的例子,因此整个网络的输出结果就是 10 10 10维,我们把某一维拿出来,然后同样去找一张图像 x x x,使这个维度的输出值最大,即

x ∗ = arg ⁡ max ⁡ x a j x^*=\arg \max\limits_x{a_j} x=argxmaxaj

既然现在输出值的每一个维度就对应到一个数字,那如果我们去找一张图像 x x x,它可以让对应到数字 1 1 1的那个输出层的结点的输出值最大,那这张图像显然应该看起来会像是数字 1 1 1,甚至可以说,说不定用这个方法就可以让计算机自动画出数字 1 1 1

但实际上,我们得到的结果如下图所示:

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第46张图片

上面的每一张图分别对应着数字 0 ∼ 8 0{\sim}8 08,神奇的是,可以让数字 1 1 1对应结点的输出值最大的图像其实长得一点也不像 1 1 1,并不只是数字 1 1 1,没有一个是我们能够直接去看懂的,然而经过实际验证,也就是我们拿这个东西作为测试数据放入整个CNN中去做预测,得到的分类结果还真的确实是 0 ∼ 8 0{\sim}8 08

可见,我们今天的这个神经网络,它所学到的东西其实跟我们人类一般的认知是完全不一样的。

那有没有办法,让上面这个图看起来更像数字呢?

人类手写出来的东西不会是长这个样子的,所以我们要对这个 x x x做正则化(regularization),以及我们要对找出来的 x x x做一些约束限制(constraint),我们应该告诉计算机说,虽然有一些 x x x可以让你的 y y y很大,但是它们不是数字。

那我们应该加上什么样的约束限制呢?最简单的想法是说,画画的时候,假如白色代表的是有墨水、有笔画的地方,那么对于一个字符来说,整张图像上涂白的区域是有限的,像上面这些整张图基本都是白色的情况的话,那它一定不会是数字。

假设图像里的每一个像素都用 x i j x_{ij} xij表示,我们把所有像素值取绝对值并求和,也就是 ∑ i , j ∣ x i j ∣ \sum_{i,j}{|x_{ij}|} i,jxij,这一项其实就是之前提到过的正则化,再用 y i y^{i} yi减去这一项,得到

x ∗ = arg ⁡ max ⁡ x ( y i − ∑ i , j ∣ x i j ∣ ) x^*=\arg \max\limits_x (y^i-\sum\limits_{i,j} |x_{ij}|) x=argxmax(yii,jxij)

我们希望再找一个图像 x x x,它可以让 y i y^{i} yi最大的同时,也要让 ∑ i , j ∣ x i j ∣ \sum_{i,j}{|x_{ij}|} i,jxij的值越小越好,也就是说我们希望找出来的图像 x x x,大部分的地方是没有涂颜色的,只有少数有数字笔画在的地方才有颜色出现。

加上这个约束限制以后,得到的结果会像下图右侧所示一样,已经隐约有些可以看出来是数字的形状了。

从零开始的深度学习(一) 经典CNN网络 LeNet-5_第47张图片

实例:手写数字识别

框架:PyTorch
数据集:MNIST
模型:LeNet-5

模型相关代码:

class LeNet5(nn.Module):

    def __init__(self):

        super(LeNet5, self).__init__()
        self.conv1 = nn.Sequential(
            nn.Conv2d(1, 6, 5, 1, 2),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )
        self.conv2 = nn.Sequential(
            nn.Conv2d(6, 16, 5),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )
        self.fc1 = nn.Sequential(
            nn.Linear(16 * 5 * 5, 120),
            nn.ReLU()
        )
        self.fc2 = nn.Sequential(
            nn.Linear(120, 84),
            nn.ReLU()
        )
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):

        x = self.conv1(x)
        x = self.conv2(x)
        x = x.view(x.size()[0], -1)
        x = self.fc1(x)
        x = self.fc2(x)
        x = self.fc3(x)
        return x

训练相关代码:

def train(epoch):

    model.train()
    training_loss = 0  # 设置初始的total_loss
    for batch_id, (data, target) in enumerate(train_data_loader):
        data = data.cuda()
        target = target.cuda()
        data, target = Variable(data), Variable(target, requires_grad=False)
        optimizer.zero_grad()
        output = model(data)
        training_loss = loss_fn(output, target)
        training_loss.backward()
        optimizer.step()


def validation():
    model.eval()
    validation_loss = 0  # 设置初始的total_loss
    correct = 0  # 初始化预测正确的数据个数为0
    for data, target in validation_data_loader:
        data = data.cuda()
        target = target.cuda()
        data, target = Variable(data), Variable(target, requires_grad=False)
        output = model(data)
        batch_loss = loss_fn(output, target).item()
        validation_loss += batch_loss
        pred = output.data.max(1, keepdim=True)[1]  # get the index of the max log-probability
        correct += pred.eq(target.data.view_as(pred)).cpu().sum()  # 对预测正确的数据个数进行累加
    validation_loss /= len(validation_data_loader.dataset)  # 因为把所有loss值进行过累加,所以最后要除以总得数据长度才得平均loss
    print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
        validation_loss, correct, len(validation_data_loader.dataset),
        100. * correct / len(validation_data_loader.dataset)))


model = LeNet5().cuda()
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate, momentum=momentum)  # 初始化优化器
loss_fn = nn.CrossEntropyLoss()

测试相关代码:

if __name__ == '__main__':
    model = LeNet5()
    model_dict = model.load_state_dict(torch.load('model_params.pt'))
    model = model.cuda()
    model.eval()  # 把模型转为test模式
    img = cv2.imread("4.jpg")  # 读取要预测的图片
    img = 255 - img
    trans = transforms.Compose([
            transforms.ToTensor(),
            transforms.Normalize((0.1307,), (0.3081,))])

    img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)  # 图片转为灰度图,因为mnist数据集都是灰度图
    img = trans(img)
    img = img.cuda()
    img = img.unsqueeze(0)  # 图片扩展多一维,因为输入到保存的模型中是4维的[batch_size,通道, 长, 宽],而普通图片只有三维,[通道, 长, 宽]
    # 扩展后,为[1,1,28,28]
    output = model(img)
    prob = Fc.softmax(output, dim=1)
    prob = Variable(prob)
    prob = prob.cpu().numpy()  # 用GPU的数据训练的模型保存的参数都是gpu形式的,要显示则先要转回cpu,再转回numpy模式
    print(prob)  # prob是10个分类的概率
    pred = np.argmax(prob)  # 选出概率最大的一个
    print(pred.item())

结果展示:

在这里插入图片描述

你可能感兴趣的:(人工智能,人工智能)