A simple full connected neural network

本篇主要参考资料及源代码出处: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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def SGD(self, training_data, epochs, mini_batch_size, eta,test_data=None):
training_data = list(training_data)
n = len(training_data)

if test_data:
test_data = list(test_data)
n_test = len(test_data)

for j in range(epochs):
random.shuffle(training_data)
mini_batches = [
training_data[k:k+mini_batch_size]
for k in range(0, n, mini_batch_size)]
for mini_batch in mini_batches:
self.update_mini_batch(mini_batch, eta)
if test_data:
print("Epoch {} : {} / {}".format(j,self.evaluate(test_data),n_test) else:
print("Epoch {} complete".format(j))

可以看出其中的关键代码就是self.update_mini_batch(mini_batch, eta),即每次由mini_batch更新权重和bias。

1
2
3
4
5
6
7
8
9
10
11
def update_mini_batch(self, mini_batch, eta):
nabla_b = [np.zeros(b.shape) for b in self.biases]
nabla_w = [np.zeros(w.shape) for w in self.weights]
for x, y in mini_batch:
delta_nabla_b, delta_nabla_w = self.backprop(x, y)
nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
self.weights = [w-(eta/len(mini_batch))*nw
for w, nw in zip(self.weights, nabla_w)]
self.biases = [b-(eta/len(mini_batch))*nb
for b, nb in zip(self.biases, nabla_b)]

关键代码是delta_nabla_b, delta_nabla_w = self.backprop(x, y),最后更新权重和bias的步骤就是BP算法最后提到的更新公式。

对于BP算法,首先是正向传播,将每一层的输出和输入分别保存在列表activations和zs中,前者的元素个数比后者多一,因为我们默认input layer没有输入,它起到将原始数据输入第一个hidden layer中的作用。

1
2
3
4
5
6
7
8
9
10
def backprop(self, x, y):
# 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#input of each layer
zs.append(z)
activation = sigmoid(z)#output of each layer
activations.append(activation)

接下来是反向传播,首先计算最后一层的error,同时得到最后一层权重和bias的“更新值”(可能有更准确的说法)

1
2
3
4
5
# 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())

这里cost_derivative定义如下

1
2
def cost_derivative(self, output_activations, y):
return (output_activations-y)

由于损失函数采用的是二次代价函数,求导之后就是(a-y),sigmoid_prime函数就是sigmoid函数的导数。
接下来就是更新每一层的weight和bias。

1
2
3
4
5
6
7
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)

采用如下代码,神经网络层次为784×30×10,mini_batch大小设为30,学习率(步长)为3.0

1
2
3
4
5
6
import network
import mnist_loader

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)

最后的测试结果如下
测试结果

可以看到accuracy还是不高的,之后我会进行改进。

0%