查看原文
其他

《神经网络和深度学习》系列文章二十九:再看手写识别问题:代码

Nielsen 哈工大SCIR 2021-02-05

出处: Michael Nielsen的《Neural Network and Deep Learning》,点击末尾“阅读原文”即可查看英文原文。

声明:我们将在每周四连载该书的中文翻译。

本节译者:朱小虎 、张广宇。转载已获得译者授权,禁止二次转载。


  • 使用神经网络识别手写数字

  • 反向传播算法是如何工作的

  • 改进神经网络的学习方法

    • 改进神经网络的学习方式

    • 交叉熵损失函数

    • 用交叉熵解决手写数字识别问题

    • 交叉熵意味着什么?它从哪里来?

    • Softmax

    • 过拟合

    • 正则化

    • 为什么正则化能够降低过拟合?

    • 其他正则化技术

    • 权重初始化

    • 重温手写数字识别:代码

    • 如何选择神经网络的超参数

    • 其他技术

  • 神经网络能够计算任意函数的视觉证明

  • 为什么深度神经网络的训练是困难的

  • 深度学习

让我们实现本章讨论过的这些想法。我们将写出一个新的程序,network2.py, 这是一个对第一章中开发的 network.py 的改进版本。如果你没有仔细看过 network.py,那你可能会需要重读前面关于这段代码的讨论。仅仅 74 行代码,也很易懂。

和 network.py 一样,主要部分就是 Network 类了,我们用这个来表示神经网络。使用一个 sizes 的列表来对每个对应层进行初始化,默认使用交叉熵作为代价 cost 参数:

class Network(object): def init(self, sizes, cost=CrossEntropyCost): self.num_layers = len(sizes) self.sizes = sizes self.defaultweightinitializer() self.cost=cost

最开始几行里的 __init__ 方法的和 network.py 中一样,可以轻易弄懂。但是下面两行是新的,我们需要知道他们到底做了什么。

我们先看看 defaultweightinitializer 方法,使用了我们新式改进后的初始化权重方法。如我们已经看到 的,使用了均值为 0 而标准差为 ,n 为对应的输入连接个数。我们使用 均值为 0 而标准差为 1 的高斯分布来初始化偏置。下面是代码:

def defaultweightinitializer(self): 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:])]

为了理解这段代码,需要知道 np 就是进行线性代数运算的 Numpy 库。我们在程序的开头会 import Numpy。同样我们没有对第一层的神经元的偏置进行初始化。因为第一层其实是输入层,所以不需要引入任何的偏置。我们在 network.py 中做了完全一样的事情。

作为 defaultweightinitializer 的补充,我们同样包含了一个 largeweightinitializer 方法。这个方法使用了第一章中的观点初始化了权重和偏置。代码也就仅仅是和 defaultweightinitializer 差了一点点了:

def largeweightinitializer(self): 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:])]

我将 largerweightinitializer 方法包含进来的原因也就是使得跟第一章的结果更容易比较。我并没有考虑太多的推荐使用这个方法的实际情景。

初始化方法 __init__ 中的第二个新的东西就是我们初始化了 cost 属性。为了理解这个工作的原理,让我们看一下用来表示交叉熵代价的类(如果你不熟悉 Python 的静态方法,你可以忽略 @staticmethod 装饰符,仅仅把 fn 和 delta 看作普通方法。如果你想知道细节,所有的 @staticmethod 所做的是告诉 Python 解释器其随后的方法完全不依赖于对象。这就是为什么 self 没有作为参数传入 fn 和 delta

class CrossEntropyCost(object): @staticmethod def fn(a, y): return np.sum(np.nantonum(-ynp.log(a)-(1-y)np.log(1-a))) @staticmethod def delta(z, a, y): return (a-y)

让我们分解一下。第一个看到的是:即使使用的是交叉熵,数学上看,就是一个函数,这里我们用 Python 的类而不是 Python 函数实现了它。为什么这样做呢?答案就是代价函数在 我们的网络中扮演了两种不同的角色。明显的角色就是代价是输出激活值 a 和目标输出 y 差距优劣的度量。这个角色通过 CrossEntropyCost.fn 方法来扮演(意,np.nantonum 调用确保了 Numpy 正确处理接近 0 的对数值)。但是代价函数其实还有另一个角色。回想第二章中运行反向传播算法时,我们需要计算网络输出误差,deltaL。这种形式的输出误差依赖于代价函数的选择:不同的代价函数,输出误差的形式就不同。对于交叉熵函数,输出误差就如公式(66)所示:


所以,我们定义了第二个方法,CrossEntropyCost.delta,目的就是让网络知道如何进行输出误差的计算。然后我们将这两个组合在一个包含所有需要知道的有关代价函数信息的类中。

类似地,network2.py 还包含了一个表示二次代价函数的类。这个是用来和第一章的结果进行对比的,因为后面我们几乎都在使用交叉函数。代码如下。 QuadraticCost.fn 方法是关于网络输出 a 和目标输出 y 的二次代价函数的直接计算结果。由 QuadraticCost.delta 返回的值基于二次代价函数的误差表达式(30),我们在第二章中得到它。

class QuadraticCost(object): @staticmethod def fn(a, y): return 0.5np.linalg.norm(a-y)*2 @staticmethod def delta(z, a, y): return (a-y) * sigmoid_prime(z)

现在,我们理解了 network2.py 和 network.py 两个实现之间的主要差别。都是很简单的东西。还有一些更小的变动,下面我们会进行介绍,包含 L2 规范化的实现。在讲述规范化之前,我们看看 network2.py 完整的实现代码。你不需要太仔细地读遍这些代码,但是对整个结构尤其是文档中的内容的理解是非常重要的,这样,你就可以理解每段程序所做的工作。当然,你也可以随自己意愿去深入研究!如果你迷失了理解,那么请读读下面的讲解,然后再回到代码中。不多说了,给代码链接:

有个更加有趣的变动就是在代码中增加了 L2 规范化。尽管这是一个主要的概念上的变动,在实现中其实相当简单。对大部分情况,仅仅需要传递参数 lambda 到不同的方法中,主要是 Network.SGD 方法。实际上的工作就是一行代码的事在 Network.update_mini_batch 的倒数第四行。这就是我们改动梯度下降规则来进行权重下降的地方。尽管改动很小,但其对结果影响却很大!

其实这种情况在神经网络中实现一些新技术的常见现象。我们花费了近千字的篇幅来讨论规范化。概念的理解非常微妙困难。但是添加到程序中的时候却如此简单。精妙复杂的技术可以通过微小的代码改动就可以实现了。

另一个微小却重要的改动是随机梯度下降方法的几个标志位的增加。这些标志位让我们可以对在代价和准确率的监控变得可能。这些标志位默认是 False 的,但是在我们例子中,已经被置为 True 来监控 Network 的性能。另外, network2.py 中的 Network.SGD 方法返回了一个四元组来表示监控的结果。我们可以这样使用:

>>> evaluationcost, evaluationaccuracy, … trainingcost, trainingaccuracy = net.SGD (training_data, 30, 10, 0.5, … lambda = 5.0, … evaluationdata=validationdata, … monitorevaluationaccuracy=True, … monitorevaluationcost=True, … monitortrainingaccuracy=True, … monitortrainingcost=True)

所以,比如 evaluation_cost 将会是一个 30 个元素的列表其中包含了每个epoch{}在验证集合上的代价函数值。这种类型的信息在理解网络行为的过程中特别有用。比如,它可以用来画出展示网络随时间学习的状态。其实,这也是我在前面的章节中展示性能的方式。然而要注意的是如果任何标志位都没有设置的话,对应的元组中的元素就是空列表。

另一个增加项就是在 Network.save 方法中的代码,用来将 Network 对象保存在磁盘上,还有一个载回内存的函数。这两个方法都是使用 JSON 进行的,而非 Python 的 pickle 或者 cPickle 模块——这 些通常是 Python 中常见的保存和装载对象的方法。使用 JSON 的原因是,假设在未来某天,我们想改变 Network 类来允许非 sigmoid 的神经元。对这个改变的实现,我 们最可能是改变在 Network.__init__ 方法中定义的属性。如果我们简单地 pickle 对象,会导致 load 函数出错。使用 JSON 进行序列化可以显式地让老的 Network 仍然能够 load。

其他也还有一些微小的变动。但是那些只是 network.py 的微调。结果就是把程序从 74 行增长到了 152 行。

问题

更改上面的代码来实现 L1 规范化,使用 L1 规范化使用 30 个隐藏元的神经网络对 MNIST 数字进行分类。你能够找到一个规范化参数使得比无规范化效果更好么? 看看 network.py 中的 Network.costderivative 方法。这个方法是为二次代价函数写的。怎样修改可以用于交叉熵代价函数上?你能不能想到可能在交叉熵函数上遇到的问题?在 network2.py 中,我们已经去掉了 Network.costderivative 方法,将其集成进了CrossEntropyCost.delta 方法中。请问,这样是如何解决你已经发现的问题的?



  • “哈工大SCIR”公众号

  • 编辑部:郭江,李家琦,徐俊,李忠阳,俞霖霖

  • 本期编辑:俞霖霖


长按下图并点击“识别图中二维码”,即可关注哈尔滨工业大学社会计算与信息检索研究中心微信公共号:”哈工大SCIR”。点击左下角“阅读原文”,即可查看原文。


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存