本博客主要内容为图书《神经网络与深度学习》和National Taiwan University (NTU)林轩田老师的《Machine Learning》的学习笔记,因此在全文中对它们多次引用。初出茅庐,学艺不精,有不足之处还望大家不吝赐教。
本节结合之前几节《改进神经网络的学习方法》系列中提到的方法进行实现。我们将写出一个新的程序,network2.py, 这是一个对第一章中开发的 network.py 的改进版本。获取完整的代码请点击这里,我们首先看看函数的初始化。
class Network(object):
def __init__(self, sizes, cost=CrossEntropyCost):
"""The list ``sizes`` contains the number of neurons in the respective
layers of the network. For example, if the list was [2, 3, 1]
then it would be a three-layer network, with the first layer
containing 2 neurons, the second layer 3 neurons, and the
third layer 1 neuron. The biases and weights for the network
are initialized randomly, using
``self.default_weight_initializer`` (see docstring for that
method).
"""
self.num_layers = len(sizes)
self.sizes = sizes
self.default_weight_initializer()
self.cost=cost
其中self.default_weight_initializer()
是默认权重初始化函数,它采用的为在《八、改进神经网络的学习方法(4):权重初始化》所提到的方法,使用均值为0,方差为 1n√ 的高斯随机分布初始化权重,其中要注意 n 的取值,在这里 n 是指对应的输入连接个数,即权重是第 l 层的神经元的权重,那么 n 是第 l−1 层的神经元的权重;列表sizes
中包含了各层神经元的个数,具体代码如下
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:])]
在上面的代码中,self.sizes[1:]
返回的是一个列表,列表中是从第二层神经元开始到最后一层输出层结束,每一层中神经元的个数;self.sizes[:‐1]
返回的是一个列表,列表中是从第一层神经元开始到倒数第二层输出层结束,每一层中神经元的个数;因为之前已经有强调过 n 是指对应的输入连接个数,所以代码中np.sqrt(x)
。
作为 defaultweightinitializer 的补充,我们同样包含了一个 largeweightinitializer 方法,这个方法使用了第一章中的观点初始化了权重和偏置。
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 the cost associated with an output ``a`` and desired output
``y``. Note that np.nan_to_num is used to ensure numerical
stability. In particular, if both ``a`` and ``y`` have a 1.0
in the same slot, then the expression (1-y)*np.log(1-a)
returns nan. The np.nan_to_num ensures that that is converted
to the correct value (0.0).
"""
return np.sum(np.nan_to_num(-y*np.log(a)-(1-y)*np.log(1-a)))
@staticmethod
def delta(z, a, y):
"""Return the error delta from the output layer. Note that the
parameter ``z`` is not used by the method. It is included in
the method's parameters in order to make the interface
consistent with the delta method for other cost classes.
"""
return (a-y)
在这里是使用了一个类来实现的交叉熵函数,并没有使用一个实例化的方法,主要原因在于交叉熵函数扮演了两种不同的角色。第一个是输出激活值 a 和目标输出 y 差距优劣的度量;第二个是要计算网络输出误差 δL ,并且 δL 的形式依赖于代价函数的选择:不同的代价函数,输出误差的形式就不同。所以定义了第二个方法CrossEntropyCost.delta
,目的就是让网络知道如何进行输出误差的计算,然后我们将这两个组合在一个包含所有需要知道的有关代价函数信息的类中。注意np.nantonum
调用确保了 Numpy 正确处理接近 0 的对数值。类似地,network2.py 还包含了一个表示二次代价函数的类。
class QuadraticCost(object):
@staticmethod
def fn(a, y):
"""Return the cost associated with an output ``a`` and desired output
``y``.
"""
return 0.5*np.linalg.norm(a-y)**2
@staticmethod
def delta(z, a, y):
"""Return the error delta from the output layer."""
return (a-y) * sigmoid_prime(z)
QuadraticCost.fn
方法是关于网络输出 a 和目标输出 y 的二次代价函数的直接计算结果。由QuadraticCost.delta
返回的值基于二次代价函数的误差表达式。注意其中np.linalg.norm
中的“linalg”是“linear algebra”,即线性代数的意思,而“norm”是规范化的意思,因此这个函数作用是求取括号内的二范数(默认情况下);而**2
表示平方的意思。
有个更加有趣的变动就是在代码中增加了 L2 规范化,尽管在实现中其实相当简单。对大部分情况,仅仅需要传递参数 lambda 到不同的方法中,主要是 Network.SGD 方法。实际上的工作就是一行代码的事在 Network.update_mini_batch 的倒数第四行。这就是我们改动梯度下降规则来进行权重下降的地方。尽管改动很小,但其对结果影响却很大!
另一个微小却重要的改动是随机梯度下降方法的几个标志位的增加。这些标志位让我们可以对在代价和准确率的监控变得可能。这些标志位默认是 False 的,但是在我们例子中,已经被置为 True 来监控 Network 的性能。另外, network2.py 中的 Network.SGD 方法返回了一个四元组来表示监控的结果。我们可以这样使用
def SGD(self, training_data, epochs, mini_batch_size, eta,
lmbda = 0.0,
evaluation_data=None,
monitor_evaluation_cost=False,
monitor_evaluation_accuracy=False,
monitor_training_cost=False,
monitor_training_accuracy=False):
比如 evaluation_cost 将会是一个 30 个元素的列表其中包含了每个epoch{}在验证集合上的代价函数值。这种类型的信息在理解网络行为的过程中特别有用。比如,它可以用来画出展示网络随时间学习的状态。然而要注意的是如果任何标志位都没有设置的话,对应的元组中的元素就是空列表。
另一个增加项就是在 Network.save 方法中的代码,用来将 Network 对象保存在磁盘上,还有一 个载回内存的函数。这两个方法都是使用 JSON 进行的,而非 Python 的 pickle 或者 cPickle 模 块——这些通常是 Python 中常见的保存和装载对象的方法。使用 JSON 的原因是,假设在未来某天,我们想改变 Network 类来允许非 sigmoid 的神经元。对这个改变的实现,我们最可能是 改变在 Network.init 方法中定义的属性。如果我们简单地 pickle 对象,会导致 load 函数出错。使用 JSON 进行序列化可以显式地让老的 Network 仍然能够 load。