本篇主要参考资料及源代码出处:neural networks and deep learning
参考博客:neural networks and deep learning阅读笔记
在用代码实现最简单的full connected神经网络之前,需要了解神经网络中最核心的back propagation算法。
符号定义
wljk:第l层的第j个神经元与第(l-1)层的第k个神经元之间的权重。
blj:第l层的第j个神经元的bias。
zlj:第l层第j个神经元的输入,即
$$
z^l_j=\sum_kw^l_{jk}a^{l-1}_k+b^l_j
$$
alj:第l层第j个神经元的输出,即
$$
a^l_j=\sigma(z^l_j)
$$
其中σ表示激活函数。
目前我们采用的损失函数是二次代价函数,即
$$
C=\frac{1}{2n}\sum_x||y(x)-a^L(x)||^2
$$
对于一个输入样本x,我们有损失函数
$$
C=\frac{(y(x)-a)^2}{2}
$$
另外,我们定义第l层的第j个神经元中产生的error定义为
$$
\delta^l_j≡\frac{\partial{C}}{\partial{z^l_j}}
$$
BP算法
BP算法主要用到了如下四个公式:
$$
\delta^L=\nabla_aC\bigodot\sigma’(z^L)
$$
$$
\delta^l=((w^{l+1})^T\delta^{l+1})\bigodot\sigma’(z^l)
$$
$$
\frac{\partial{C}}{\partial{w^l_{jk}}}=a^{l-1}_k\delta^l_j
$$
$$
\frac{\partial{C}}{\partial{b^l_j}}=\delta^l_j
$$
公式一是用于计算网络最后一层的error,
$$
\because\delta^L_j=\frac{\partial{C}}{\partial{z^L_j}}=\frac{\partial{C}}{\partial{}a^L_j}·\frac{\partial{a^L_j}}{\partial{z^L_j}}
$$
$$
\therefore\delta^L=\frac{\partial{C}}{\partial{a^L}}\bigodot \frac{\partial a^L}{\partial z^L}=\nabla_aC\bigodot\sigma’(z^L)
$$
注意到这里其实就是把原本的标量形式写成了张量形式,因为在实际操作中往往用张量表示神经网络每一层的参数。
公式二是相邻两层网络error的递推关系,证明如下:
$$
\therefore \delta^l=((w^{l+1})^T\delta^{l+1})\bigodot\sigma’(z^l)
$$
这里同样写成了张量的形式,有了这个公式,就可以从最后一层递推求得任意一层的error了。
最后两个公式分别是计算weights和bias的梯度,这样根据梯度下降法就可以调整weights和bias从而最小化cost,证明如下:
$$
\frac{\partial C}{\partial b^l_j}=\frac{\partial C}{\partial z^l_j}\cdot \frac{\partial z^l_j}{\partial b^l_j}=\delta^l_j
$$
不难看出,上面四个公式本质上是复合函数求导的chain-rule。
有了上面的公式,就可以搭建神经网络了,任务是进行手写数字的识别,用的是mnist数据集,基本思路如下:
1.输入训练集进行前向传播,得到最后的output。
2.利用公式一计算输出层的error。
3.利用公式二反向传播error。
4.利用梯度下降法训练参数:
$$
w^l→w^l-\frac{\eta}{m}\sum_x\delta^{x,l}(a^{x,l-1})^T
$$
$$
b^l→b^l-\frac{\eta}{m}\sum_x\delta^{x,l}
$$
代码实现
代码中使用的是随机梯度下降算法,即每一次从training_set里面随机选出一部分数据进行权重的更新,training_data是经过处理的图片灰度数据和分类结果组成的元组。
1 | def SGD(self, training_data, epochs, mini_batch_size, eta,test_data=None): |
可以看出其中的关键代码就是self.update_mini_batch(mini_batch, eta)
,即每次由mini_batch更新权重和bias。
1 | def update_mini_batch(self, mini_batch, eta): |
关键代码是delta_nabla_b, delta_nabla_w = self.backprop(x, y)
,最后更新权重和bias的步骤就是BP算法最后提到的更新公式。
对于BP算法,首先是正向传播,将每一层的输出和输入分别保存在列表activations和zs中,前者的元素个数比后者多一,因为我们默认input layer没有输入,它起到将原始数据输入第一个hidden layer中的作用。
1 | def backprop(self, x, y): |
接下来是反向传播,首先计算最后一层的error,同时得到最后一层权重和bias的“更新值”(可能有更准确的说法)
1 | # backward pass |
这里cost_derivative定义如下
1 | def cost_derivative(self, output_activations, y): |
由于损失函数采用的是二次代价函数,求导之后就是(a-y),sigmoid_prime函数就是sigmoid函数的导数。
接下来就是更新每一层的weight和bias。
1 | for l in range(2, self.num_layers): |
采用如下代码,神经网络层次为784×30×10,mini_batch大小设为30,学习率(步长)为3.0
1 | import network |
最后的测试结果如下
可以看到accuracy还是不高的,之后我会进行改进。