深度学习 _BP 神经网络

1. 说明

现在使用深度学习算法都以调库为主,但在使用库之前,先用 python 写一个最基本的神经网络的程序,也非常必要,它让我们对一些关键参数:学习率,批尺寸,激活函数,代价函数的功能和用法有一个直观的了解。

2. 原理

1) BP 神经网络

BP 神经网络是一种按照误差逆向传播算法训练的多层前馈神经网络.这又前馈又逆向的把人绕晕了.先看看什么是前馈神经网络,回顾一下《深度学习 _ 简介》中的图示:

图片.png

这是一个典型的前馈神经网络,数据按箭头方向数据从输入层经过隐藏层流入输出层,因此叫做前馈.前馈网络在模型的输出和模型之间没有反馈,如果也包含反馈,则是循环神经网络,将在后续的 RNN 部分介绍.

前向网络和循环网络的用途不同,举个例子,比如做玩具狗,前馈是用不同材料和规格训练N次,各次训练之间都没什么关系,只是随着训练,工人越来越熟练.而循环网络中,要求每次做出来的狗都是前次的加强版,因此前次的结果也作为一种输入参与到本次的训练之中.可把循环网络理解成前馈网络的升级版.本篇讲到的 BP 神经网络,以及处理图像常用的卷积神经网络都是前馈网络,而处理自然语言常用的 RNN 则是循环网络.

误差逆向传播是指通过工人做出的狗(预测结果)与玩具狗规格(实际结果)的误差来调整各个工人的操作(权重 w),这个例子具体见前篇《简介》,¬由于误差的传播的顺序是:输出层 ->隐藏层 2->隐藏层 1,所以叫逆向传播.

综上,前馈指的是数据流向,逆向指的是误差流向.

2) 训练过程

简单回忆一下(详见《简介》篇)训练过程:对于每个训练样本,BP 算法先将输入样例提供给给输入神经元,然后逐层将信号向前传播,直到产生输出层的结果,然后对照实际结果计算输出层的误差,再将误差逆向传播到隐层神经元,然后根据神经元的误差来对连接权值和与偏置进行调整优化。向前传数据很简单,只包含加法乘法和激活函数(具体计算见代码),相对的难点在于逆向传误差,当得到了输出层的误差后,调整 w3 中各个 w 的具体方法是什么呢?这里用到了梯度下降算法.此处也是代码中的最理解的部分.

下面先看一下代码,梯度下降算法见之后的"关键概念"部分.

3. 代码分析

1) 程序说明

程序实现了通过 MNIST 数据集中 60000 个实例训练对手写数字的识别,使用一个输入层,一个隐藏层,一个输出层的方式构建 BP 神经网络.

因代码较长,把它分成两块:算法实现和处部调用(运行程序时把它们粘在一起即可)。注释有点多哈:p

2) 算法实现

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# -*- coding: utf-8 -*-

import numpy as np
import random
import os, struct
from array import array as pyarray
from numpy import append, array, int8, uint8, zeros
from keras.datasets import mnist

class NeuralNet(object):
# 初始化神经网络,sizes包含了神经网络的层数和每层神经元个数
def __init__(self, sizes):
self.sizes_ = sizes
self.num_layers_ = len(sizes) # 三层:输入层,一个隐藏层(8个节点), 输出层
# zip 函数同时遍历两个等长数组的方法
self.w_ = [np.random.randn(y, x) for x, y in zip(sizes[:-1], sizes[1:])] # w_、b_初始化为随机数
self.b_ = [np.random.randn(y, 1) for y in sizes[1:]]
# w_是二维数组,w_[0].shape=(8,784), w_[1].shape=(10, 8),权值, 供矩阵乘
# b_是二维数组,b_[0].shape=(8, 1), b_[1].shape=(10, 1),偏移, 每层间转换的偏移

# Sigmoid函数,激活函数的一种, 把正负无穷间的值映射到0-1之间
def sigmoid(self, z):
return 1.0/(1.0+np.exp(-z))

# Sigmoid函数的导函数, 不同激活函数导函数不同
def sigmoid_prime(self, z):
return self.sigmoid(z)*(1-self.sigmoid(z))

# 向前传播:已知input,根据w,b算output,用于预测
def feedforward(self, x):
for b, w in zip(self.b_, self.w_):
x = self.sigmoid(np.dot(w, x)+b)
return x # 此处的x是0-9每个数字的可能性

# 单次训练函数,x是本次训练的输入,y是本次训练的实际输出
# 返回的是需调整的w,b值
def backprop(self, x, y):
# 存放待调整的w,b值,nabla是微分算符
nabla_b = [np.zeros(b.shape) for b in self.b_] # 与b_大小一样,初值为0
nabla_w = [np.zeros(w.shape) for w in self.w_] # 与w_大小一样,初值为0

activation = x # 存放层的具体值, 供下层计算
activations = [x] # 存储每层激活函数之后的值
zs = [] # 存放每层激活函数之前的值
for b, w in zip(self.b_, self.w_):
z = np.dot(w, activation)+b # dot是矩阵乘法, w是权值,b是偏移
zs.append(z)
activation = self.sigmoid(z) # 激活函数
activations.append(activation)

# 计算输出层的误差,cost_derivative为代价函数的导数
delta = self.cost_derivative(activations[-1], y) * \
self.sigmoid_prime(zs[-1]) #原理见梯度下降部分
nabla_b[-1] = delta
nabla_w[-1] = np.dot(delta, activations[-2].transpose())

# 计算隐藏层的误差
for l in range(2, self.num_layers_):
z = zs[-l]
sp = self.sigmoid_prime(z)
delta = np.dot(self.w_[-l+1].transpose(), delta) * sp
nabla_b[-l] = delta
nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())
return (nabla_b, nabla_w)

# 对每批中的len(mini_batch)个实例,按学习率eta调整一次w,b
def update_mini_batch(self, mini_batch, eta):
# 累计调整值
nabla_b = [np.zeros(b.shape) for b in self.b_] # 与b_大小一样,值为0
nabla_w = [np.zeros(w.shape) for w in self.w_] # 与w_大小一样,值为0
for x, y in mini_batch: # 100个值,分别训练
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)]
# eta是预设的学习率(learning rate),用来调节学习的速度. eta越大,调整越大
# 用新计算出的nable_w调整旧的w_, b_同理
self.w_ = [w-(eta/len(mini_batch))*nw for w, nw in zip(self.w_, nabla_w)]
self.b_ = [b-(eta/len(mini_batch))*nb for b, nb in zip(self.b_, nabla_b)]

# 训练的接口函数
# training_data是训练数据(x, y);epochs是训练次数;
# mini_batch_size是每次训练样本数; eta是学习率learning rate
def SGD(self, training_data, epochs, mini_batch_size, eta, test_data=None):
if test_data:
n_test = len(test_data)

n = len(training_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)]
# 把所有训练数据60000个分成每100个/组(mini_batch_size=100)
for mini_batch in mini_batches:
self.update_mini_batch(mini_batch, eta) # 分批训练
if test_data:
print("Epoch {0}: {1} / {2}".format(j, self.evaluate(test_data), n_test))
else:
print("Epoch {0} complete".format(j))

# 计算预测的正确率
def evaluate(self, test_data):
# argmax(f(x))是使得 f(x)取得最大值所对应的变量x
test_results = [(np.argmax(self.feedforward(x)), y) for (x, y) in test_data]
return sum(int(x == y) for (x, y) in test_results)

# 代价函数的导数, 对比实际输出与模拟输出的差异, 此时y也是个数组
def cost_derivative(self, output_activations, y):
return (output_activations-y)

# 预测
def predict(self, data):
value = self.feedforward(data)
return value.tolist().index(max(value))

3) 外部调用

外部调用主要实现了主函数,load 数据,以及调用神经网络的接口

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
# 将输入数据转换为神经网络能处理的格式
def load_samples(image, label, dataset="training_data"):
X = [np.reshape(x,(28*28, 1)) for x in image] # 手写图分辨率28x28
X = [x/255.0 for x in X] # 灰度值范围(0-255),转换为(0-1)

# 把y从一个值转成一个数组,对应输出层0-9每个数字出现的概率
# 5 -> [0,0,0,0,0,1.0,0,0,0]; 1 -> [0,1.0,0,0,0,0,0,0,0]
def vectorized_Y(y):
e = np.zeros((10, 1))
e[y] = 1.0
return e

if dataset == "training_data":
Y = [vectorized_Y(y) for y in label]
pair = list(zip(X, Y))
return pair
elif dataset == 'testing_data':
pair = list(zip(X, label))
return pair
else:
print('Something wrong')


if __name__ == '__main__':
INPUT = 28*28 # 每张图像28x28个像素
OUTPUT = 10 # 0-9十个分类
net = NeuralNet([INPUT, 8, OUTPUT])

# 从mnist提供的库中装载数据
(x_train, y_train), (x_test, y_test) = mnist.load_data()
# 格式转换
test_set = load_samples(x_test, y_test, dataset='testing_data')
train_set = load_samples(x_train, y_train, dataset='training_data')

#训练
net.SGD(train_set, 13, 100, 3.0, test_data=test_set)

#计算准确率
correct = 0;
for test_feature in test_set:
if net.predict(test_feature[0]) == test_feature[1]:
correct += 1
print("percent: ", correct/len(test_set))

4. 关键概念

1) 误差函数

误差函数也叫代价函数或损失函数,它计算的是实际结果和预测结果之间的差异,误差函数记作 L(Y, f(x)).

上例中代价函数用的是方差再取二分之一的方法 (SSE):(1/2)*(o-y)^2,它的导数是 o-y,即 output_activations-y,其中 output_activations 为预测结果,y 为实际结果。上面没有直接写误差函数,而是给出了它的导数(cost_derivative).

误差函数还有均方误差,绝对值均差等,具体请见参考中的《目标函数 objectives》。

2) 梯度下降

回顾一下导数的概念,假设我们有一个函数 y = f (x),这个函数的导数记为 f’(x),它描述了如何改变 x,能在输出获得相应的变化:

f (x +ε) ≈ f (x) +ε*f’(x)

此时我们希望 f() 向小的方向变化(等号左侧),则需要对 f(x) 加一个负数,即ε*f’(x)<=0,那么ε与 f’(x) 符号不同。换言之,可以将 x 往导数的反方向移动一小步ε来减小 f (x)。这种技术被称为梯度下降.可通过下图,获得更直观的理解.

(图片引自《深度学习》"花书")

梯度下降算法在上例的 backprop() 部分实现,我们想知道如何改变权重 w,能使误差函数L变小,于是求 L 对于 w 的导数,然后将 w 向导数的反方向移动一小步,即可使L变小.

损失函数的计算由下式得出:

L = (1/2)*(g(wx+b)–y)^2,其中 y 是实际结果,g() 是激活函数,wx+b 是对上一步 x 的线性变换,对 L 求导用到了复合函数的链试法则,因此有程序中分别使用了激活函数的导数(sigmoid_prime),损失函数的导数(cost_derivative),再乘以上一步的 x(activations).以上就是求权重 w 变化的原理,偏置 b 同理.

另外,需要注意的是这里求出的 nable_w 是权重的梯度,并不是具体的权重值.

3) 批尺寸

批尺寸是每训练多少个数据调整一次权重.上例中由 mini_batch 指定为每次 100 个实例;如果每训练一次就调整一次,不但会增加运算量,还会使 w 变化波动加俱,使其难以收敛;如果一次训练太多,则会占用较大内存,有时正负波相互抵消,最终使 w 无法改进.因此选择批尺寸时,需要在考虑内存和运算量的情况下尽量加大批尺寸.

4) 学习率

学习率也叫学习因子,简单地说,就是每次算出来的梯度对权值的影响的大小。学习率大,影响就大。学习率决定了参数移动到最优值的速度快慢。如果学习率过大,很可能会越过最优值;反而如果学习率过小,优化的效率可能过低,使得长时间算法无法收敛。

学习率在上例中是 eta 数值.

学习率的选择与误差函数相关,上例中使用 SSE 作为误差函数,批尺寸越大,梯度数据累加后越大,因此在计算时除以了批尺寸大小.我们可以选择一个不被训练集样本个数影响的误差函数 (比如 MSE).另外,输入特征的大小也对学习率有影响,所以处理前最好先归一化.

还可以在学习中动态地调整学习率,常用的有学习率有 sgd,adadelta 等.具体见参考中的《各种优化方法总结比较》

5) 激活函数

激活函数也叫激励函数,它的功能是将线性变换转成非线性变换,以提供非线性问题的解决方法.

上例中使用了 sigmoid 函数作为激活函数.常用的激活函数还有 tanh,RelU 等.其中 ReLU 是当前流行的激活函数 y=max(0,x),它将大于 0 的值留下,否则一律为 0。常用于处理大多数元素为 0 的稀疏矩阵。具体请见参考中的《神经网络之激活函数》

5. 参考

  1. 神经网络入门之 bp 算法,梯度下降

http://blog.csdn.net/u013230651/article/details/75909596

  1. 使用 Python 实现神经网络

http://blog.csdn.net/u014365862/article/details/53868414

  1. 目标函数 objectives

http://keras-cn.readthedocs.io/en/latest/other/objectives/

  1. 各种优化方法总结比较

http://blog.csdn.net/luo123n/article/details/48239963

  1. 机器学习中的损失函数

http://blog.csdn.net/shenxiaoming77/article/details/51614601

  1. Deep Learning 学习随记(七)Convolution and Pooling -- 卷积和池化

http://blog.csdn.net/zhoubl668/article/details/24801103

  1. 神经网络之激活函数 (sigmoid、tanh、ReLU)

http://blog.csdn.net/suixinsuiyuan33/article/details/69062894?locationNum=4&fps=1