前言

本系列学习笔记基于《Neural Networks and Deep Learning》。本人在之前对于深度学习方面有一定的了解与实战,但是阅读此书算是第一次比较系统的进行学习。本篇记录阅读过程中的一些总结与思考。本篇博客所有代码已同步到https://github.com/Aron00123/NN-DLlearning.git

Chapter 1

Perceptrons & Sigmoid Neuron

​Perceptrons(感知器)通常由多个二进制输入和一个二进制输出构成,每个输入与一个权重值配对(权重值为实数),输入组成的向量与对应权重组成的向量经过点积生成一个结果,当结果高于某个特定值时输出为1,否则输出为0。多个、多层感知器组成的网络可以做出较为复杂、微妙的决定,每层感知器的输入是上一层感知器的输出(除了第一层)。

​ Sigmoid Neuron与Perceptrons类似,主要区别在于输入与输出不再局限于二进制数字,而是0和1之间的任何值。具体来说,其中。当z趋于正无穷时,趋近于1,负无穷趋近于0。由于sigmoid函数的平滑性,使得可以对权重进行微调来得到输出结果的微小变化,即

A Simple Network to Classify Handwritten Digits

​对于解决此问题的神经网络设计为3层,第一层为单个像素点的灰度,第三层为最终的输出,包含10个神经元,第i个神经元输出约为1代表识别结果为i-1。中间层为隐藏层,假设有n=15个神经元。中间层抽象的理解为可以识别数字的某些特征(如笔画等)。

Gradient Descent

如何训练所需要的神经网络呢?具象的说,我们希望找到一个函数,其中x为所有像素灰度组成的向量,y为输出的10个神经元的值组成的向量。训练的过程中,定义成本函数,其中表示所有权重的集合,b表示所有偏差,n是训练输入的总数,a是网络应该输出的正确向量。我们需要不断调参使得成本函数最小化,其中使用的算法为Gradient Descent(梯度下降法)。梯度下降即在多维空间中的曲面,沿着梯度方向的反方向下降,到达局部最低点的速度是最快的。梯度下降如何应用在训练中呢?用公式表示每一次调参,即为:​。

一种加速学习的方法是随机梯度下降。此方法指的是在计算梯度时可以随机选择小批量的输入(m个批次)来代替全部变量,估计函数C的梯度,。体现到具体的调参中即为:​。

Implement

1
2
3
4
5
6
7
8
9
'''
Chapter1_1.py
'''
import mnist_loader
import network

training_data, validation_data, test_data = mnist_loader.load_data_wrapper()
net = network.Network([784, 30, 10])
net.SGD(training_data, 30, 10, 3.0, test_data=test__data)

其中的部分代码

mnist_loader:

  • load_data() & load_data_wrapper():读取数据集

network:

  • __init__():sizes中第i个元素代表第i层的神经元数量,biases和weights随机初始化
  • feedforward():前向传播,使用sigmoid函数
  • SGD():分成多个小批次递归下降
  • backprop():先调feedforward()再反向传播

Chapter 2

Warm Up

为了叙述方便,使用表示神经网络中第层中第k神经元到层中第j神经元的连接权重,用表示第层中第j神经元的偏差,用表示第层中第j神经元的激活值,则。表示成向量、矩阵形式有。记中间量为层l中神经元的加权输入(weighted input),​。

Two Assumptions about Cost Function

回顾成本函数:。为了使反向传播发挥作用,需要制定两个假设:

  • 成本函数可以写为单个训练示例成本函数的平均值
  • 成本函数可以写成输出激活的函数。

关于以上两个假设的解释:对于这两个假设,都可以从表达式本身推理得到,不再过多赘述。

Four Functions behind Backpropagation

反向传播的关键部分是计算,为此,我们引入一个中间量,它表示第层底j神经元的误差度量,为(通过给添加修改量,来完成微调,最终成本变化量为​)。

  • An equation for the error in the output layer, : 。容易看出方程右侧的变量都容易通过计算得到。基于矩阵重写方程得到:。又有在二次成本的情况下,从而方程完全基于矩阵形式为:​​。

  • Proof:由的定义,运用链式法则可以得到:。然而当时右侧第二项为0,故可以简写为。而由可得等式右侧第二项等价于,从而代入可得郑。

  • An equation for the error in terms of the error in the next layer, : ​。根据公式的形式可以理解为误差的向后传播的过程。

  • Proof

  • An equation for the rate of change of the cost with respect to any bias in the network: ,这个方程可以简写为:​。

  • An equation for the rate of change of the cost with respect to any weight in the network: ,同上可以简写为:。其中是权重对应的神经元输入的激活值,是权重​对应的神经元输出的误差。

Backpropagation Algorithm

反向传播算法具体由一下过程组成:

  • Input x:为输入层设置相应激活
  • Feedforward:对于每个,计算
  • Output error
  • Backpropagate the error:对于每个计算
  • Output:成本函数的梯度由得出。

The Code for Backpropagation

主要内容在于network.py中的backprop(self, x, y):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def backprop(self, x, y):
"""Return a tuple ``(nabla_b, nabla_w)`` representing the
gradient for the cost function C_x. ``nabla_b`` and
``nabla_w`` are layer-by-layer lists of numpy arrays, similar
to ``self.biases`` and ``self.weights``."""
nabla_b = [np.zeros(b.shape) for b in self.biases]
nabla_w = [np.zeros(w.shape) for w in self.weights]
# feedforward
activation = x
activations = [x] # list to store all the activations, layer by layer
zs = [] # list to store all the z vectors, layer by layer
for b, w in zip(self.biases, self.weights):
z = np.dot(w, activation)+b
zs.append(z)
activation = sigmoid(z)
activations.append(activation)
# backward pass
delta = self.cost_derivative(activations[-1], y) * \
sigmoid_prime(zs[-1])
nabla_b[-1] = delta
nabla_w[-1] = np.dot(delta, activations[-2].transpose())
# Note that the variable l in the loop below is used a little
# differently to the notation in Chapter 2 of the book. Here,
# l = 1 means the last layer of neurons, l = 2 is the
# second-last layer, and so on. It's a renumbering of the
# scheme in the book, used here to take advantage of the fact
# that Python can use negative indices in lists.
for l in range(2, self.num_layers):
z = zs[-l]
sp = sigmoid_prime(z)
delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
nabla_b[-l] = delta
nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())
return (nabla_b, nabla_w)

Chapter 3

The Cross-Entropy Cost Function

网络模型在调参时若误差过大会出现学习速度缓慢的现象,为了解决这种现象,我们可以使用与二次成本不同的Cost Function(交叉熵)来解决问题。交叉熵成本函数定义为:,其中n为输入总数,y是对应的期望输出,​)是神经元的输出。

可以注意到有两个属性可以使交叉熵解释为成本函数:

  • 非负
  • 如果神经元的实际输出接近期望输出,则交叉熵接近于零

这个成本函数有效的解决了学习速度减慢的问题,关于证明可以由下得出:将代入交叉熵成本函数并应用链式法则两次,可得:。由sigmoid函数的定义不难得到,所以上式可化简为:。从这个式子可以看出权重学习的速率由控制,即输出中的误差,误差越大,神经元学习速度越快。类似地,。这两个式子都与无关,所以避免了二次Cost Function的学习速度减慢的问题。

从单个神经元的交叉熵推广到多神经元、多网络的交叉熵很容易:(就是对所有输出神经元求和)。

下面讨论使用交叉熵对MNIST数字进行分类。代码如下:

1
2
3
4
5
6
7
8
9
10
'''
Chapter3_1.py
'''
import mnist_loader
import network2

training_data, validation_data, test_data = mnist_loader.load_data_wrapper()
net = network2.Network([784, 30, 10], cost=network2.CrossEntropyCost)
net.large_weight_initializer()
net.SGD(training_data, 30, 10, 0.5, evaluation_data=test_data, monitor_evaluation_accuracy=True)

Chapter1_1.py最大的不同就是使用的成本函数不同,Network2.py中关于两种成本函数的表示为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# quadratic cost
class QuadraticCost(object):
@staticmethod
def fn(a, y):
return 0.5 * np.linalg.norm(a - y) ** 2

@staticmethod
def delta(z, a, y):
return (a - y) * sigmoid_prime(z)

# cross-entropy cost
class CrossEntropyCost(object):
@staticmethod
def fn(a, y):
return np.sum(np.nan_to_num(-y * np.log(a) - (1 - y) * np.log(1 - a)))

@staticmethod
def delta(z, a, y):
return (a - y)

Softmax

额外介绍一下softmax神经元。softmax的输入与sigmoid相同,为;但是softmax的输出与之不同,定义为,分母是所有输出的求和。容易看出来sofrmax所有的输出为正且总和为1,也就是sofemax层的输出可以看作概率分布,也可以把softmax层看做把原来的输出转化为概率分布的工具。

softmax层能够解决学习变慢的问题吗?定义log-likelihood cost为:。其中y是所需要的输出,例如期望输出7,则若真正输出很大概率是7,即很大(接近于一),对应的成本就会很小接近于0;相反则成本会很大,由此可知log-likelihood cost与我们需要的成本函数的行为一致。由此函数,可以推导得到:。这与前面所说的交叉熵的表达式类似,从而也可以相似地解决学习速度减慢的问题。

Overfitting and Regularization

Chapter1_1.pyChapter3_1.py中,除了training_data、test_data外还有一组数据validation_data,这组数据用来检测模型训练的过拟合。模型的过拟合会导致很多问题,包括模型精度下降、普适性变差等等。不用test_data进行过拟合检测是因为这会影响模型的参数向着适合test_data的方向发展,从而导致模型的泛化能力不能够很好地得到衡量。

正则化(Regularization)可以减少过度拟合现象的发生。L2正则化是常用的正则化方法之一,具体来说是在成本函数中添加一个正则化项,例如正则化交叉熵:,其中称为正则化参数。直观上来看,正则化的作用是使网络在其他条件相同的情况下更偏向于学习较小的权重,也即正则化是寻找小权重和最小化原始成本函数的折衷:当较小时,倾向于最小化原始成本函数;当较大时,倾向于更小的权重。

可以通过方程来观察正则化神经网络中是如何应用梯度下降的。有:。由此可看出偏差bias的梯度下降学习规则没有变化,而权重的变为。随机梯度下降也类似,由相关公式,可以得出。下面将正则化应用到具体例子中来观察其如何改变神经网络的性能:

1
2
3
4
5
6
7
8
9
10
'''
Chapter3_2.py
'''
import mnist_loader
import network2

training_data, validation_data, test_data = mnist_loader.load_data_wrapper()
net = network2.Network([784, 30, 10], cost=network2.CrossEntropyCost)
net.large_weight_initializer()
net.SGD(training_data, 400, 10, 0.5,evaluation_data=test_data, lmbda = 0.1,monitor_evaluation_cost=True,monitor_evaluation_accuracy=True,monitor_training_cost=True,monitor_training_accuracy=True)

通过相关准确率的检测可以发现正则化有效地抑制了过拟合现象,并且与之前的结果相比峰值准确率也有所提高。

正则化为什么有助于减少过拟合呢?假设神经网络模型大部分具有较小的权重,也即意味着如果输入改变很小,神经网络的行为也相应地不会改变太多。这表示正则化网络会很少受到局部噪声的影响。相反,正则化网络学习对训练集中经常出现的类型进行相应。以上比较合理地解释了正则化减少过拟合的原因。

除了L2正则化外,也有很多其他正则化方法:

  • L1 Regularization:
  • Dropout: 大概流程是先随机暂时删除神经网络中半数的隐藏神经元,再执行前向传播和反向传播,然后恢复隐藏神经元,不断重复此流程。这看起来与正则化手段关系不大,但也可以有效的减少过拟合现象的发生。(“这项技术减少了神经元复杂的共同适应,因为神经元不能依赖于特定其他神经元的存在。因此,它被迫学习更强大的特征,这些特征与其他神经元的许多不同的随机子集结合使用是有用的”)
  • Aritificially Expanding the Training Data: 增加训练数据。

Weight Initialization

前面在创建神经网络时,通过使用独立的高斯随机变量选择权重和偏差,标准化为均值为0,标准差为1。接下来具一个具体例子说明可以通过优化初始化方法来使模型表现更好。考虑一个具有1000个输入的神经元,假设一半输入x为0,另一半为1,则是501归一化高斯随机变量的总和(算上偏差b),所以z的分布为均值为零标准差为,从而z的取值很可能变得很大或很小,这会减慢模型的学习速度。而当我们将初始化的权重的标准差改为后不难得到z的分布的标准差变为了。这相比于之前的情况好了很多。应用到实际例子中,使用后者初始化方法进行训练,有:

1
2
3
4
5
6
7
8
9
10
11
'''
Chapter3_3.py
'''
import mnist_loader
import network2

training_data, validation_data, test_data = \
mnist_loader.load_data_wrapper()
net = network2.Network([784, 30, 10], cost=network2.CrossEntropyCost)
# net.large_weight_initializer() Network2中默认初始化方法即为所需初始化方法
net.SGD(training_data, 30, 10, 0.1, lmbda = 5.0, evaluation_data=validation_data, monitor_evaluation_accuracy=True)

更加具体的,两种初始化方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def large_weight_initializer(self):
"""Initialize the weights using a Gaussian distribution with mean 0
and standard deviation 1. Initialize the biases using a
Gaussian distribution with mean 0 and standard deviation 1.

Note that the first layer is assumed to be an input layer, and
by convention we won't set any biases for those neurons, since
biases are only ever used in computing the outputs from later
layers.

This weight and bias initializer uses the same approach as in
Chapter 1, and is included for purposes of comparison. It
will usually be better to use the default weight initializer
instead.

"""
self.biases = [np.random.randn(y, 1) for y in self.sizes[1:]]
self.weights = [np.random.randn(y, x)
for x, y in zip(self.sizes[:-1], self.sizes[1:])]

def default_weight_initializer(self):
"""Initialize each weight using a Gaussian distribution with mean 0
and standard deviation 1 over the square root of the number of
weights connecting to the same neuron. Initialize the biases
using a Gaussian distribution with mean 0 and standard
deviation 1.

Note that the first layer is assumed to be an input layer, and
by convention we won't set any biases for those neurons, since
biases are only ever used in computing the outputs from later
layers.

"""
self.biases = [np.random.randn(y, 1) for y in self.sizes[1:]]
self.weights = [np.random.randn(y, x)/np.sqrt(x)
for x, y in zip(self.sizes[:-1], self.sizes[1:])]

根据训练结果,新的权重初始化方法在第一个epoch结束时,模型精度比之前高了6%。

How to Choose a Neural Network’s Hyper-Parameters

关于如何调试模型的训练参数,如等,本节给出了一个启发式方法。

Broad Strategy:在模型训练过程中进行实时监测,并对每个参数不断调大调小进行试验。

Learning Rate :一般来说,在较小时,成本会平稳下降;在较大时,成本会有很大的波动,由于可以看做梯度下降的步长,从而这种现象很容易理解。在模型训练过程中,对实施随时调整对于模型是有利的——在训练初期,权重误差较大,可以通过调高使得权重快速变化;在后期对权重进行微调时,可以调小使得权重的调整更加精准。

The Regularization Parameter :对于正则化参数,建议现在不进行正则化的情形下确定,再根据的值调整

Mini-Batch Size:选取小批量大小其实是一个折中的方案:如果太小,则无法充分利用针对快速硬件优化的良好矩阵库的优势;太大则不能频繁地更新权重,这往往也需要不断的监测以及调试。

Chapter 4

Two Caveats

  • 神经网络虽然可以计算任何函数,但是不意味着可以用来进行精确计算
  • 神经网络只能描述连续函数

University with Inputs and Outputs

对于单输入单输出的函数,证明是较为方便的。假设神经网络的隐藏层有很多神经元,每个神经元的激活函数设为某种阶跃函数(例如权重很大的sigmoid可以近似看作阶跃函数),则隐藏层的输入改变时,各隐藏层神经元的输出也会发生改变,因此输入不同,最后的输出也不同,从而当神经元很多时,就可以以很高的拟合度反应函数的变化。如下图;

单输入单输出

下面考虑多输入的情况,不妨考虑有两个输入的情况。对于单输入的情况我们可以看作一个二维平面上的曲线,相应地,两输入可以看做空间中的曲面,原理一样,如下图:

多输入

对于某个输入,正权重和负权重的组合可以形成一个凸起,所以两个输入组合起来可以组成一个塔状突起,再将所有塔状突起组合就可以生成任意的曲面。

对于类似sigmoid之类的激活函数,再大的权重的情况下也会与真正的阶跃函数有一定不同——体现函数值逐渐变化的窗口必然存在。然而,当我们在拟合函数时(例如拟合​),当不同神经元的窗口相互叠加,就可以十分近似的把窗口处值的变化消除,从而达到拟合的目的。

Chapter 5

the Vanishing Gradient Problem

以识别数字的神经网络为例,设法让隐藏层增多再训练模型,我们会发现模型的准确率并没有提高,甚至会有微弱的下降:

1
2
3
4
5
6
7
8
9
10
'''
Chapter5_1.py
'''
import mnist_loader
import network2

training_data, validation_data, test_data = \
mnist_loader.load_data_wrapper()
net = network2.Network([784, 30, 30, 30, 10], cost=network2.CrossEntropyCost)
net.SGD(training_data, 30, 10, 0.1, lmbda = 5.0, evaluation_data=validation_data, monitor_evaluation_accuracy=True)

这种现象带来的直观感受是多添加的隐藏层没有任何作用,但是事实并非如此。下面通过可视化网络的形式探讨问题所在。下图是神经网络中的两个隐藏层,每个隐藏层有30个隐藏神经元。每个神经元上的黄色条形代表神经元随着网络学习而变化的速度,也即

变化速度

值得注意的是,第二个隐藏层中的条形大多比第一个隐藏层中的条形大得多。为了确定这个现象是否是巧合,采用采用一种全局方法来比较第一和第二隐藏层的学习速度会有所帮助。将第l层中第j个神经元的梯度表示为。我们可以将梯度视为一个向量,其条目决定第一个隐藏层的学习速度,而作为一个向量,其条目决定第二个隐藏层的学习速度。然后,我们将使用这些向量的长度粗略地作为各层学习速度的全局度量。因此,例如,长度衡量第一个隐藏层学习的速度,而长度衡量第二个隐藏层学习的速度。经过计算,确实得出第二个隐藏层的神经元的学习速度比第一个隐藏层的快得多。

经过观察,在一些深度神经网络中,当我们向后方移动隐藏层时,梯度往往会变小,即前面层的神经元比后面层的神经元学习慢很多。这种现象就是the vanishing gradient problem(梯度消失问题)。类似地也有梯度爆炸等问题。

the Cause? Unstable Gradients in Deep Neural Nets

我们不妨考虑最简单的情况:有三个隐藏层的网络,每层只有一个神经元:

简单情况

例如第一个隐藏层的神经元的表达式为:

$\frac{\partial C}{\partial b_1}=\sigma’(z_1)w_2\sigma’(z_2)w_3\sigma’(z_3)w_4\sigma’(z_4)*\frac{\partial C}{\partial a_4}$​

易知函数在0处取得最大值,而权重一般满足,所以​在大部分情况下成立,这也就导致了层数越小,速度越慢。而对于权重比较大的情况,则会出现前面所说的梯度爆炸问题,原理同梯度消失。还有一种问题被称为The unstable gradient problem(梯度不稳定问题),这种问题一般表现为每层学习速度差距较大导致前面层梯度不稳定。

Chapter 6

Convolutional Networks

在之前的识别数字的神经网络中,使用的是全连接层的结构,这种结构下训练出的模型虽然精度已经很高,但是全连接的方式忽略了数字图像的空间结构。为了更好的利用数字图像的特点,提出了卷积神经网络。卷积神经网络有三个基本思想:local receptive fields, shared weights, pooling。

  • Local Receptive Fields

​​以下假设识别一个28*28像素的图片。在全连接层中,输入被看作784个垂直输入,即输入层的神经元与第一个隐藏层的神经元二者个数相等;在卷积神经网络中,我们将输入看作2828的神经元正方形。现在,只考虑将输入图像的各个小局部区域中的神经元与某个隐藏神经元连接,例如将一个5\5的正方形中神经元与第一个隐藏层中的一个神经元连接(比如正方形对应的左上角),这样第一个隐藏层就有24*24个神经元。5*5的区域就称为隐藏神经元的local receptive fields。

local receptive fields

  • Shared Weights and Biases

对于每个隐藏神经元,考虑使用相同的权重和偏差,即:。其中代表位置x, y处的输入激活。有了共享的权重和偏差,也就意味着这个隐藏层中的所有神经元检测的是完全相同的特征,唯一的区别是检测的特征位于图像的不同位置。这种映射关系称为特征映射。一个完整的卷积层通常需要多种不同的特征,所以一个完整的卷积层通常由几个不同的特征图组成。

完整卷积层

共享权重和偏差的一个很大的优点是减少了卷积网络中涉及的参数数量。对于上述的数字识别例子,假设需要20个特征图,则需要20(26+1)=520个参数,而原来的结构,假设隐藏层有30个神经元,则需要784\(30+1)=23550个参数。

  • Pooling Layers

除了卷积层,卷积神经网络还包含池化层。池化层通常紧接在卷积层之后使用,以简化卷积层输出的信息。具体来说,池化层获取每个特征图卷积层的输出并准备一个压缩的特征图。例如,池化层的每个单元可以取前一层中的某个正方形神经元的区域(这个过程称为最大池化)。

最大池化

我们可以将最大池化视为网络询问是否在图像区域中的任何位置找到给定特征的一种方式。池化的好处是池化特征要比原特征少很多,有助于进一步减少参数数量。与最大池化类似,还有L2池化等方法。

  • Putting It All Together

将所有内容聚集到一起,就可以形成完整的卷积神经网络。

卷积神经网络

Practice

将上述的卷积神经网络运用到数字识别中,如下:

1
2
3
4
5
6
7
8
9
10
11
12
'''
Chapter6_1.py
'''
import network3
from network3 import Network
from network3 import ConvPoolLayer, FullyConnectedLayer, SoftmaxLayer
training_data, validation_data, test_data = network3.load_data_shared()
mini_batch_size = 10
net = Network([
FullyConnectedLayer(n_in=784, n_out=100),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
net.SGD(training_data, 60, mini_batch_size, 0.1, validation_data, test_data)

最终结果准确度为97.80%。

下面考虑使用更深的神经网络架构:在池化层后添加一个额外的全连接层,如下图:

更深的网络

体现到具体代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
'''
Chapter6_2.py
'''
import network3
from network3 import Network
from network3 import ConvPoolLayer, FullyConnectedLayer, SoftmaxLayer
training_data, validation_data, test_data = network3.load_data_shared()
mini_batch_size = 10
net = Network([
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2)),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2)),
FullyConnectedLayer(n_in=40*4*4, n_out=100),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
net.SGD(training_data, 60, mini_batch_size, 0.1, validation_data, test_data)

再尝试插入第二个convolutional-pooling layer。再现有的卷积池层和全连接隐藏层中插入同样的卷积池层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
'''
Chapter6_3.py
'''
import network3
from network3 import Network
from network3 import ConvPoolLayer, FullyConnectedLayer, SoftmaxLayer
training_data, validation_data, test_data = network3.load_data_shared()
mini_batch_size = 10
net = Network([
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2)),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2)),
FullyConnectedLayer(n_in=40*4*4, n_out=100),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
net.SGD(training_data, 60, mini_batch_size, 0.1, validation_data, test_data)

此时的准确率达到了99.06%。对于第二个卷积池层,可以理解为对第一个卷积池层输出的1212的图像进行特征识别。这时出现了另外一个问题:第一个卷积池层输出的是20个12\12的图像(以现在的例子),如果只像第一个卷积层一样每一个神经元只对应一张图的5*5正方形,那么第二张图的神经元的数量将不可估量。解决办法是每一个神经元对应20*5*5的长方体,这样就可以保证神经元的数量不会过多。

  • Using Rectified Linear Units

首先,在原先模型的基础上,将神经元的激活函数从sigmoid函数修改为ReLU函数,即。修改后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
'''
Chapter6_4.py
'''
import network3
from network3 import Network
from network3 import ConvPoolLayer, FullyConnectedLayer, SoftmaxLayer
from network3 import ReLU
training_data, validation_data, test_data = network3.load_data_shared()
mini_batch_size = 10
net = Network([
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
FullyConnectedLayer(n_in=40*4*4, n_out=100, activation_fn=ReLU),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
net.SGD(training_data, 60, mini_batch_size, 0.03, validation_data, test_data, lmbda=0.1)

以上代码训练出的神经网络准确率高达99.23%!观察得到使用ReLU比使用sigmoid函数效果更好。

  • Expanding the Training Data

通过将训练图像向四周扩展一个元素,可以实现训练数据的“微扩展”。在expand_mnist.py中实现了这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
f = gzip.open("mnist.pkl.gz", 'rb')
training_data, validation_data, test_data = pickle.load(f, encoding='latin1')
f.close()
expanded_training_pairs = []
j = 0 # counter
for x, y in zip(training_data[0], training_data[1]):
expanded_training_pairs.append((x, y))
image = np.reshape(x, (-1, 28))
j += 1
if j % 1000 == 0: print("Expanding image number", j)
# iterate over data telling us the details of how to
# do the displacement
for d, axis, index_position, index in [
(1, 0, "first", 0),
(-1, 0, "first", 27),
(1, 1, "last", 0),
(-1, 1, "last", 27)]:
new_img = np.roll(image, d, axis)
if index_position == "first":
new_img[index, :] = np.zeros(28)
else:
new_img[:, index] = np.zeros(28)
expanded_training_pairs.append((np.reshape(new_img, 784), y))
random.shuffle(expanded_training_pairs)
expanded_training_data = [list(d) for d in zip(*expanded_training_pairs)]
print("Saving expanded data. This may take a few minutes.")
f = gzip.open("mnist_expanded.pkl.gz", "w")
pickle.dump((expanded_training_data, validation_data, test_data), f)
f.close()

将扩展后的训练图像数据应用到模型训练中,有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
'''
Chapter6_5.py
'''
import network3
from network3 import Network
from network3 import ConvPoolLayer, FullyConnectedLayer, SoftmaxLayer
from network3 import ReLU
expanded_training_data, validation_data, test_data = network3.load_data_shared("mnist_expanded.pkl.gz")
mini_batch_size = 10
net = Network([
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
FullyConnectedLayer(n_in=40*4*4, n_out=100, activation_fn=ReLU),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
net.SGD(expanded_training_data, 60, mini_batch_size, 0.03, validation_data, test_data, lmbda=0.1)

最终的准确度带到了99.37%。可见一个微不足道的图像扩展就可以提升模型的准确度。

  • Inserting an Extra Fully-Connected Layer

一方面先尝试扩大已有全连接层的大小,从100扩展到300或者1000。最终得到的结果是99.46%和99.43%,结果不尽人意。另一方面再考虑添加额外的全连接层,比如在已有的全连接层之前插入一个全连接层。经过测试模型的准确度上升也不多。接下来再考虑还有没有别的优化方案,比如对最后的那个全连接层运用dropout技术。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
'''
Chapter6_6.py
'''
import network3
from network3 import Network
from network3 import ConvPoolLayer, FullyConnectedLayer, SoftmaxLayer
from network3 import ReLU
expanded_training_data, validation_data, test_data = network3.load_data_shared("mnist_expanded.pkl.gz")
mini_batch_size = 10
net = Network([
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
FullyConnectedLayer(
n_in=40*4*4, n_out=1000, activation_fn=ReLU, p_dropout=0.5),
FullyConnectedLayer(
n_in=1000, n_out=1000, activation_fn=ReLU, p_dropout=0.5),
SoftmaxLayer(n_in=1000, n_out=10, p_dropout=0.5)], mini_batch_size)
net.SGD(expanded_training_data, 40, mini_batch_size, 0.03, validation_data, test_data)

使用dropout后,模型的准确率达到了99.60%!

  • Using an Ensemble of Networks

更进一步提高模型准确度的方法是同时创建多个神经网络,并以投票的方式确定最佳分类。例如同时训练5个神经网络,每个网络的准确率接近99.60%,尽管准确率接近,但是每个模型犯的错误不一定相同。所以在它们之中进行投票理论上的确可以减少错误的发生,提高准确率。

  • Why Only Applied Dropout to Fully-Connected Layers

首先回顾一下dropout技术的作用:防止模型过度学习训练数据中的局部特性,从而抑制过拟合现象。然而对于卷积层来说,共享权重意味着卷积层被迫从完整图像的角度进行特征学习,所以卷积层具有强大的内置抵抗过度拟合的能力。

the Code for Convolutional Networks

这一部分着重分析卷积神经网络的代码network3.py。首先看全连接层FullyConnectedLayer、卷积层ConvPoolLayer和Softmax层SoftmaxLayer的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
'''
ConvPoolLayer
'''
class FullyConnectedLayer(object):

def __init__(self, n_in, n_out, activation_fn=sigmoid, p_dropout=0.0):
self.n_in = n_in
self.n_out = n_out
self.activation_fn = activation_fn
self.p_dropout = p_dropout
# Initialize weights and biases
self.w = theano.shared(
np.asarray(
np.random.normal(
loc=0.0, scale=np.sqrt(1.0/n_out), size=(n_in, n_out)),
dtype=theano.config.floatX),
name='w', borrow=True)
self.b = theano.shared(
np.asarray(np.random.normal(loc=0.0, scale=1.0, size=(n_out,)),
dtype=theano.config.floatX),
name='b', borrow=True)
self.params = [self.w, self.b]

def set_inpt(self, inpt, inpt_dropout, mini_batch_size):
self.inpt = inpt.reshape((mini_batch_size, self.n_in))
self.output = self.activation_fn(
(1-self.p_dropout)*T.dot(self.inpt, self.w) + self.b)
self.y_out = T.argmax(self.output, axis=1)
self.inpt_dropout = dropout_layer(
inpt_dropout.reshape((mini_batch_size, self.n_in)), self.p_dropout)
self.output_dropout = self.activation_fn(
T.dot(self.inpt_dropout, self.w) + self.b)

def accuracy(self, y):
"Return the accuracy for the mini-batch."
return T.mean(T.eq(y, self.y_out))

其中,__init__()用于全连接层的初始化,set_inpt()用于设置该层的输入,并计算输出。ConvPoolLayer和SoftmaxLayer与此接近,不再过多阐述相同部分。二者与全连接层不同的是输出激活的计算,可以通过Theano库进行实现:

1
2
3
4
5
6
7
8
9
10
'''
ConvPoolLayer
'''
self.output = self.activation_fn(pooled_out + self.b.dimshuffle('x', 0, 'x', 'x'))
self.output_dropout = self.output # no dropout in the convolutional layers
'''
SoftmaxLayer
'''
self.output = softmax((1-self.p_dropout)*T.dot(self.inpt, self.w) + self.b)
self.output_dropout = softmax(T.dot(self.inpt_dropout, self.w) + self.b)

下面再看Network的__init__()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Network(object):

def __init__(self, layers, mini_batch_size):
"""Takes a list of `layers`, describing the network architecture, and
a value for the `mini_batch_size` to be used during training
by stochastic gradient descent.

"""
self.layers = layers
self.mini_batch_size = mini_batch_size
self.params = [param for layer in self.layers for param in layer.params]
self.x = T.matrix("x")
self.y = T.ivector("y")
init_layer = self.layers[0]
init_layer.set_inpt(self.x, self.x, self.mini_batch_size)
for j in xrange(1, len(self.layers)):
prev_layer, layer = self.layers[j-1], self.layers[j]
layer.set_inpt(
prev_layer.output, prev_layer.output_dropout, self.mini_batch_size)
self.output = self.layers[-1].output
self.output_dropout = self.layers[-1].output_dropout

self.params = [param for layer in ...] 将每个层的参数收集到一个列表中。 Network.SGD() 方法将使用 self.params 来确定 Network 中的哪些变量可以学习。 self.x = T.matrix("x")self.y = T.ivector("y") 行定义了名为 x 和 y 的 Theano 符号变量。这些将用于表示网络的输入和所需的输出。接下来的几行代码定义了网络的符号输出。我们首先使用init_layer.set_inpt(self.x, self.x, self.mini_batch_size)将输入设置为初始层,注意到函数两次传递输入 self.x :这是因为我们可能以两种不同的方式使用网络(带或不带 dropout)。然后, for 循环通过 Network 的各层向前传播符号变量 self.x 。这允许我们定义最终的 output 和 output_dropout 属性,它们表示 Network 的输出。

接下来分析Network有关的SGD()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
def SGD(self, training_data, epochs, mini_batch_size, eta,
validation_data, test_data, lmbda=0.0):
"""Train the network using mini-batch stochastic gradient descent."""
training_x, training_y = training_data
validation_x, validation_y = validation_data
test_x, test_y = test_data

# compute number of minibatches for training, validation and testing
num_training_batches = int(size(training_data)/mini_batch_size)
num_validation_batches = int(size(validation_data)/mini_batch_size)
num_test_batches = int(size(test_data)/mini_batch_size)

# define the (regularized) cost function, symbolic gradients, and updates
l2_norm_squared = sum([(layer.w**2).sum() for layer in self.layers])
cost = self.layers[-1].cost(self)+\
0.5*lmbda*l2_norm_squared/num_training_batches
grads = T.grad(cost, self.params)
updates = [(param, param-eta*grad)
for param, grad in zip(self.params, grads)]

# define functions to train a mini-batch, and to compute the
# accuracy in validation and test mini-batches.
i = T.lscalar() # mini-batch index
train_mb = theano.function(
[i], cost, updates=updates,
givens={
self.x:
training_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],
self.y:
training_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]
})
validate_mb_accuracy = theano.function(
[i], self.layers[-1].accuracy(self.y),
givens={
self.x:
validation_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],
self.y:
validation_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]
})
test_mb_accuracy = theano.function(
[i], self.layers[-1].accuracy(self.y),
givens={
self.x:
test_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],
self.y:
test_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]
})
self.test_mb_predictions = theano.function(
[i], self.layers[-1].y_out,
givens={
self.x:
test_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size]
})
# Do the actual training
best_validation_accuracy = 0.0
for epoch in range(epochs):
for minibatch_index in range(num_training_batches):
iteration = num_training_batches*epoch+minibatch_index
if iteration % 1000 == 0:
print("Training mini-batch number {0}".format(iteration))
cost_ij = train_mb(minibatch_index)
if (iteration+1) % num_training_batches == 0:
validation_accuracy = np.mean(
[validate_mb_accuracy(j) for j in range(num_validation_batches)])
print("Epoch {0}: validation accuracy {1:.2%}".format(
epoch, validation_accuracy))
if validation_accuracy >= best_validation_accuracy:
print("This is the best validation accuracy to date.")
best_validation_accuracy = validation_accuracy
best_iteration = iteration
if test_data:
test_accuracy = np.mean(
[test_mb_accuracy(j) for j in range(num_test_batches)])
print('The corresponding test accuracy is {0:.2%}'.format(
test_accuracy))
print("Finished training network.")
print("Best validation accuracy of {0:.2%} obtained at iteration {1}".format(
best_validation_accuracy, best_iteration))
print("Corresponding test accuracy of {0:.2%}".format(test_accuracy))

SGD()方法中包含初始化、正向传播、反向传播等方法。重点关注一下几行:

1
2
3
4
5
6
7
8
9
10
11
12
13
# define the (regularized) cost function, symbolic gradients, and updates
l2_norm_squared = sum([(layer.w**2).sum() for layer in self.layers])
cost = self.layers[-1].cost(self)+\
0.5*lmbda*l2_norm_squared/num_training_batches
grads = T.grad(cost, self.params)
updates = [(param, param-eta*grad)
for param, grad in zip(self.params, grads)]
'''
cost()
'''
def cost(self, net):
"Return the log-likelihood cost."
return -T.mean(T.log(self.output_dropout)[T.arange(net.y.shape[0]), net.y])

这一部分使用了log-likelihood cost计算成本,并计算梯度中有关的导数、参数等。之后通过train_mb()函数使用updates更新Network的参数。同样, validate_mb_accuracy 和 test_mb_accuracy 计算任何给定小批量验证或测试数据的 Network 的准确性。通过对这些函数进行平均,我们将能够计算整个验证和测试数据集的准确性。

Other Approaches to Deep Neural Nets

  • Recurrent Neural Networks(RNNs)

与前馈神经网络不同,RNN具有一定的“记忆”能力,它一方面接受当前时间的输入数据另一方面接受前一时间的隐状态或输出。总的来说RNN的总体思想是存在一些随时间动态变化的概念的神经网络。正因如此,RNNs在分析随时间变化的数据或者过程时特别有用,例如自然语言处理(NLP)等。

  • Long Short-Term Memory Units(LSTMs)

影响RNN应用的一个重要因素是早期模型难以训练,甚至比深度前馈网络还要困难。主要原因是Chapter 5中讨论的不稳定梯度的问题。这个问题通常表现为梯度在通过层向后传播时变得越来越小从而靠后的层的学习速度极其缓慢。在RNN中,梯度不仅通过层向后传播,而且还随着时间向后传播,所以这个问题对RNN带来的影响更加严重。如果网络运行很长时间,可能会导致梯度极其不稳定并且难以学习。一个比较可靠的解决办法是,可以将长短期记忆单元 (LSTM) 的想法融入 RNN 中。这些单位由 Hochreiter 和 Schmidhuber 于 1997 年引入,其明确目的是帮助解决不稳定的梯度问题。

LSTM 单元包括四个主要部分,每部分都有特定的作用,由不同的神经网络层和特定的激活函数组成:

遗忘门(Forget Gate):决定哪些信息应该被遗忘或丢弃。

遗忘门通过查看当前输入和前一个时间步的隐状态,输出一个介于0到1之间的数值(通过sigmoid激活函数)。这个数值乘以之前的单元状态,决定保留多少旧信息。

输入门(Input Gate):决定哪些新的信息被存储在单元状态中。

输入门同样使用sigmoid层来决定更新哪些值,然后一个tanh层创建一个新的候选值向量,这将被加到单元状态。

单元状态(Cell State):携带相关的信息整个数据序列处理流程,从一个单元传递到下一个单元。

单元状态是通过结合遗忘门的输出(决定丢弃哪些旧信息)和输入门的输出(添加新的信息候选值)来更新。

输出门(Output Gate):基于当前的单元状态,决定此时间步的输出。

输出门先使用sigmoid层决定单元状态的哪一部分将输出,然后将单元状态通过tanh激活(规范化到-1到1之间),并乘以sigmoid门的输出,以产生最终的输出。

  • Deep Belief Nets, Generative Models, and Boltzmann Machines

​在前馈网络中,我们指定输入激活,它们确定网络中稍后特征神经元的激活。像DBN这样的生成模型可以以类似的方式使用,但也可以指定一些特征神经元的值,然后“向后运行网络”,生成输入激活的值。更具体地说,在手写数字图像上训练的DBN也可以用于生成看起来像手写数字的图像。换句话说,DBN在某种意义上是在学习写作。在这一点上,生成模型很像人脑:它不仅可以读取数字,还可以写入数字。

DBN还有一个有趣的地方在于它们可以进行无监督和半监督学习。例如,当使用图像数据进行训练时,即使训练图像未标记,DBN也可以学习有用的特征来理解其他图像。进行无监督学习的能力非常有趣,无论是出于基本的科学原因,还是对于实际应用(如果它能够足够好地工作)。

总结

以上就是有关本书的大部分知识点,部分内容加入了我自己的一些理解,如有偏差请谅解。深度学习近几年的发展十分迅速,想要深入了解光靠读这一本书肯定是远远不够的,必须要在平时做到随时学习。