神经网络与深度学习笔记(二) 2016-10-04 # 神经网络与深度学习笔记(二) 在[上节笔记](http://midday.me/article/af2a18c404574b22ab15d1e67d309e8d)中介绍了单个感知机,并实现了一个例子。这节按照同样的方法,使用神经网络来解决手写数字识别的问题。其中会遇到很多神经网络的基础知识。 ## Artificial neural network [Artificial neural network(人工神经网络)](https://en.wikipedia.org/wiki/Artificial_neural_network)比我大那么一点点,还和邓小平的人生一样起起伏伏这里面的故事还比较有意思,会有一些传奇人物的故事,这里不瞎扯有兴趣可以去找找。到底什么是神经网络,在一篇博客上看到一句化:**神经网络是多个“神经元”(感知机)的带权级联** 我觉得说得及其简练。但是要怎么理解这句话,等看完下面的应该会有想法。 计算机的最底层应该是各种逻辑电路,直观感觉如果计算机计算各种逻辑运算应该是很合理的事情,但是给我们的感觉是计算机能计算各种运算,加、减、乘、除......导致0,1 组成的一串东西是如此的强大。其中的原理值得去了解的,我这里想类别一下神经网络,感知机就好比计算机底层的逻辑电路,其实在很多情况下感知机就是一个逻辑运算。但是从理论上可以说神经网络可以去模拟任何函数。先看下面的模型  上面是一个有四层的神经网络,每一个圆圈都是一个感知机,不过输入层的是输入数据本身。上图有两个隐含层,一个输出层, 每层的输入是前一层的输出。在这里谈论全连接层,意思就是下一层的输入包含上一层所有神经元的输出。 为什么神经网络能产生效果?我也不知从何解释,先拿来用下试试。 ## 学习算法 神经网络是通过调整网络中神经元(感知机)之间的权重来学习不同数据的。和单个的感知机很类似,只不过此处网络有点复杂。同样的,我要实现手写数字识别,本质上就是一个分类器,由于数字有十个,我们把输出层格式确定为10,比如目标值是5 对应的输出层就是[0,0,0,0,0,1,0,0,0,0] 输入层是图片的像素。中间层就可以随便定义了,之所以说随便是因为没有理论说清到底多少好,但是也并不是完全随便,通常是有一个合适的值,因为中间层多理论上会在训练数据上有比较好的效果,但是会带来计算复杂度,和过拟合等问题。 暂时定为三层,输入层是图片的像素数,一个不确定个数的中间层,输出层是10个神经元。 和感知机模型类似,激活函数使用[Sigmoid](https://en.wikipedia.org/wiki/Sigmoid_function) ,然后需要定义一个误差函数,此处同样使用二次误差函数, ```math E = (1/2)\sum_{k\Subset K}(O_k-t_k)^2 ``` 其中$$K$$是输出层个数,$$O_k$$ 是模型输出值,$$t_k$$ 是目标值。误差的意思就是对输出层所有神经元的误差的和。 如果还是使用梯度下降算法来寻找误差函数的最小值,那该如何计算误差函数相对于各个权重的偏导数就成了最最困难的问题。这就有大牛提出了个[backpropagation](https://en.wikipedia.org/wiki/Backpropagation) 就是传说中的反向传播算法(BP). 为了理解这其中的过程,我找了很多资料,其中个人觉得比较好的列在下面,当然你说去读原论文当然可以,毕竟博客都是别人消化之后,然后产生的(是不是有点儿恶心) 首先看上面的wikipedia的链接,wikipedia 的好处是,能比较系统的了解到相关知识,还能在此处找到一些相关链接,比如Paper,Book等等,所以我也就渐渐地有了看wikipedia 的习惯。 [YouTube 视频:Neural network tutorial: The back-propagation algorithm ](https://www.youtube.com/watch?v=aVId8KMsdUU&index=1&list=PL29C61214F2146796)很少看视频学习东西,但是这个视频真的很赞,时间不长,但是对反向传播算法的推倒很清楚。 和上面视频对应的有一段[Python 代码](https://github.com/mattm/simple-neural-network/blob/master/neural-network.py) 还有个老兄写了篇博客,用一个简单的例子来推导反向传播的过程[A Step by Step Backpropagation Example]](https://mattmazur.com/2015/03/17/a-step-by-step-backpropagation-example/) 有篇[博客我爱自然语言处理列出了一些资料](http://www.52nlp.cn/%E5%8F%8D%E5%90%91%E4%BC%A0%E6%92%AD%E7%AE%97%E6%B3%95%E5%85%A5%E9%97%A8%E8%B5%84%E6%BA%90%E7%B4%A2%E5%BC%95)但是个人觉得上面几个比较靠谱。 要理解什么是BP 算法你可以从上面的一些资料入手。我不打算敲公式,但是说下我个人的理解。 首先抛开反向传播这个词,不管什么是反向传播。和感知机一样我要计算误差函数对于权重的偏导数,来更新权重,那就直接算好了。 使用上面视屏中的符号定义,用到比较多的,是微积分里面的链式法则。隐藏层到输出层权重的偏导数是很好计算的,但是输入曾到隐含层权重的偏导数就比较麻烦了,上面那个Step by Step Backpropagation Example 有比较直观的理解。同时上面的视屏的内容[hankcs](http://www.hankcs.com/ml/back-propagation-neural-network.html)整理在他的博客中有比较号的解释。站在巨人的肩上........ 还推荐一本书,这是我正在看的主要的东西[Neural Networks and Deep Learning](http://neuralnetworksanddeeplearning.com/index.html) 第二章专门解释反向传播的,但是个人感觉作者废话很多,所以放到最后吧。 ### 实现手写数字识别 看完上面这些东西,最好理解其中的来龙去脉,如果实在不理解,也没关系,看看最中的迭代公式你照样可以写代码。 在[Neural Networks and Deep Learning](http://neuralnetworksanddeeplearning.com/chap2.html) 这本书里总结反向传播的四个公式,这就是写代码的核心了,怎么计算偏导数就看懂他们就好了,公式如下:  BP1中的$$z^L$$ 是输出层sigmoid 函数的输入值。其中的$$\bigodot$$运算是[Hadamard product ](https://en.wikipedia.org/wiki/Hadamard_product_(matrices)) 对应到numpy 里面就是两个矩阵之间直接使用$$*$$ 乘法 BP2 是误差函数对隐含曾的Z值取偏导数,所谓Z值就是输入在权重的作用之后未经激活函数的只(这样解释不知道有没说清楚)。它会依赖于后面一层的即$$\delta^{l+1}$$ ,如果只有三层网络,那就是输出层了也就是BP1的值,BP1 的值是可以在前向传导之后直接计算的。 BP3和BP4主要就是我对权重的偏导数了,需要用到BP1,BP2, 所以在前向传导的时候需要保存一些中间值,在误差反向传递的时候需要用到。 看一段代码吧,这段代码基本上是[Neural Networks and Deep Learning](http://neuralnetworksanddeeplearning.com/index.html)书里实现的,但是我字节重新写了一遍。迭代的时候用到了批量梯度下降(SGD)。这段代码和作者提供的代码几乎完全一致,只是很多地方重构了一下,准确率能到90% 几。 ``` import numpy as np import copy class Network(): def __init__(self, sizes): """ sizes, 神经元的输入层数[784,30,10]意思就是输入层784个,隐藏层30个,输出层10个 """ self.weights = [np.random.randn(row, col) for row, col in zip(sizes[1:], sizes[:-1])] self.bias = [np.random.randn(row, 1) for row in sizes[1:]] self.num_layers = len(sizes) def train(self, training_data, epochs, batch_size=10, eta=0.1, test_data=None): n = len(training_data) for step in xrange(epochs): np.random.shuffle(training_data) mini_batchs = [training_data[k: k + batch_size] for k in xrange(0, n, batch_size)] for mini_batch in mini_batchs: self.update_mini_batch(mini_batch, eta) if test_data: test_n = len(test_data) test_result = self.evaluate(test_data) print "Epoch {0}: {1} / {2}".format(step, test_result, test_n) else: print "Epoch {0} compete".format(step) def update_mini_batch(self, mini_batch, eta): part_der_b = [np.zeros(b.shape) for b in self.bias] part_der_w = [np.zeros(w.shape) for w in self.weights] for x, y in mini_batch: delta_part_der_b, delta_part_der_w = self.backprop(x, y) part_der_b = [pdb + dpdb for pdb, dpdb in zip(part_der_b, delta_part_der_b)] part_der_w = [pdw + dpdw for pdw, dpdw in zip(part_der_w, delta_part_der_w)] tmp_eta = float(eta / len(mini_batch)) # 迭代更新权重和误差 self.weights = [w - tmp_eta * dw for w, dw in zip(self.weights, part_der_w)] self.bias = [b - tmp_eta * db for b, db in zip(self.bias, part_der_b)] def backprop(self, x, y): """ 返回样本x带来误差,相对权重的梯度 """ part_der_b = [np.zeros(b.shape) for b in self.bias] part_der_w = [np.zeros(w.shape) for w in self.weights] activation = x activation_list = [x] z_list = [] # 前向传播,并计算每层的z值和激活值存放在z_list 和activation_list 中 for w, b in zip(self.weights, self.bias): z = np.dot(w, activation) + b z_list.append(z) activation = sigmoid(z) activation_list.append(activation) # 输出层的误delta delta = (activation_list[-1] - y) * sigmoid_prime(z_list[-1]) part_der_b[-1] = delta part_der_w[-1] = np.dot(delta, activation_list[-2].transpose()) # 误差反向传播,计算隐藏偏导数 for l in xrange(2, self.num_layers): z = z_list[-l] spz = sigmoid_prime(z) delta = np.dot(self.weights[-l + 1].transpose(), delta) * spz part_der_b[-l] = delta part_der_w[-l] = np.dot(delta, activation_list[-l - 1].transpose()) return (part_der_b, part_der_w) def feedforword(self, x): a = copy.deepcopy(x) for w, b in zip(self.weights, self.bias): a = sigmoid(np.dot(w, a) + b) return a def evaluate(self, test_data): test_result = [(np.argmax(self.feedforword(x)), np.argmax(y)) for x, y in test_data] return sum(int(x == y) for (x, y) in test_result) def sigmoid(z): return 1.0 / (1.0 + np.exp(-z)) def sigmoid_prime(z): return sigmoid(z) * (1 - sigmoid(z)) if __name__ == '__main__': import mnist_loader train_data, evaluate_data, test_data = mnist_loader.DataLoader().load_data() network = Network([784, 30, 10]) network.train(training_data=train_data, epochs=30, batch_size=10, test_data=test_data, eta=3.0) ``` 代码中使用的数据是[MNIST DATABASE](http://yann.lecun.com/exdb/mnist/) 书的作者在他的[GitHub](https://github.com/mnielsen/neural-networks-and-deep-learning) 项目中有,他实现的读取数据的方法也被我改头换面了```mnist_loader.py```如下 ``` import gzip import cPickle import numpy as np class DataLoader(object): def read_data(self): """ load data form gizp file """ f = gzip.open('../data/mnist.pkl.gz', 'rb') training_data, validation_data, test_data = cPickle.load(f) f.close() return (training_data, validation_data, test_data) def format_data(self, input, target): """ reshape inputs and expand target """ training_inputs = [np.reshape(x, (784, 1)) for x in input] training_y = [self.format_vec(i) for i in target] training_data = zip(training_inputs, training_y) return training_data def format_vec(self, i): """ expand a target to target list """ result = np.zeros((10, 1)) result[i] = 1.0 return result def load_data(self): """ load_data """ tr_d , va_d, ts_d = self.read_data() training_data = self.format_data(tr_d[0], tr_d[1]) test_data = self.format_data(ts_d[0], ts_d[1]) validation_data = self.format_data(va_d[0], va_d[1]) return training_data, validation_data, test_data ``` 参考资料: * 1.[Neural Networks and Deep Learning](http://neuralnetworksanddeeplearning.com/index.html) * 2.[Neural network tutorial: The back-propagation algorithm ](https://www.youtube.com/watch?v=aVId8KMsdUU&index=1&list=PL29C61214F2146796) * 3.[A Step by Step Backpropagation Example]](https://mattmazur.com/2015/03/17/a-step-by-step-backpropagation-example/) * 4.[反向传播算法入门资源索引](http://www.52nlp.cn/%E5%8F%8D%E5%90%91%E4%BC%A0%E6%92%AD%E7%AE%97%E6%B3%95%E5%85%A5%E9%97%A8%E8%B5%84%E6%BA%90%E7%B4%A2%E5%BC%95) * 5.[反向传播神经网络极简入门](http://www.hankcs.com/ml/back-propagation-neural-network.html)