深度学习 _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 import numpy as npimport randomimport os, structfrom array import array as pyarrayfrom numpy import append, array, int8, uint8, zerosfrom keras.datasets import mnist class NeuralNet (object ): def __init__ (self, sizes ): self.sizes_ = sizes self.num_layers_ = len (sizes) self.w_ = [np.random.randn(y, x) for x, y in zip (sizes[:-1 ], sizes[1 :])] self.b_ = [np.random.randn(y, 1 ) for y in sizes[1 :]] def sigmoid (self, z ): return 1.0 /(1.0 +np.exp(-z)) def sigmoid_prime (self, z ): return self.sigmoid(z)*(1 -self.sigmoid(z)) def feedforward (self, x ): for b, w in zip (self.b_, self.w_): x = self.sigmoid(np.dot(w, x)+b) return x def backprop (self, x, y ): nabla_b = [np.zeros(b.shape) for b in self.b_] nabla_w = [np.zeros(w.shape) for w in self.w_] activation = x activations = [x] zs = [] for b, w in zip (self.b_, self.w_): z = np.dot(w, activation)+b zs.append(z) activation = self.sigmoid(z) activations.append(activation) 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) def update_mini_batch (self, mini_batch, eta ): nabla_b = [np.zeros(b.shape) for b in self.b_] nabla_w = [np.zeros(w.shape) for w in self.w_] 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.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)] 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)] 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 ): 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) 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. 参考
神经网络入门之 bp 算法,梯度下降
http://blog.csdn.net/u013230651/article/details/75909596
使用 Python 实现神经网络
http://blog.csdn.net/u014365862/article/details/53868414
目标函数 objectives
http://keras-cn.readthedocs.io/en/latest/other/objectives/
各种优化方法总结比较
http://blog.csdn.net/luo123n/article/details/48239963
机器学习中的损失函数
http://blog.csdn.net/shenxiaoming77/article/details/51614601
Deep Learning 学习随记(七)Convolution and Pooling --
卷积和池化
http://blog.csdn.net/zhoubl668/article/details/24801103
神经网络之激活函数 (sigmoid、tanh、ReLU)
http://blog.csdn.net/suixinsuiyuan33/article/details/69062894?locationNum=4&fps=1