神经网络理论介绍及实现

神经网络架构

假设我们的神经网络结构如下图所示,每一个圆都代表一个神经元,第一层被称为输入层,最后一层被称为输出层,位于中间的被称为隐藏层。输入层和输出层的设计往往是非常直接的。以我们需要学习的MNIST数据集为例,想要验证一张手写的数字图片是否为9,假设图片大小为 6464 ,那么就有 6464 个输入神经元,输出层则只有一个神经元,当输出值大于 0.5 时表明输入的图片是9,输出值小于 0.5 时,表明输入的图片不是9。

反向传播算法

与回归问题一样,我们也需要通过最小化代价函数来优化预测精度,但是由于神经网络包含了多个隐藏层,每个隐藏层都会输出预测,因此无法通过传统的梯度下降方法来最小化代价函数,而需要逐层考虑误差,并逐层优化。因此,在多层神经网络里面,我们需要通过反向传播算法优化预测精度。

算法流程

在实际应用中,我们一般将反向传播算法与学习算法一起使用,例如Stochatic Gradiant Decent。结合之后的算法流程总结如下:

  1. 输入 n 个训练样本
  2. 对于每一个训练样本xi,i{1,2,,n}:设置输入层的对应激活值为 a1i ,然后执行以下步骤:
    • 前向传播: 对于 l{2,3,,L} ,分别计算 zli=wlal1i+bl ali=σ(zli)
    • 输出层误差 δL :计算 δLi=aC.σ(zLi)
    • 反向传播误差:对于 l{L1,L2,,2} ,分别计算 δli=((wl+1)Tδl+1i).σ(zli)
    • 梯度下降:对于 l{L,L1,,2} ,更新 wlwlαniδli(al1i)T blblαniδli

理论推导

我们的最终目标是计算 minw,bC(w,b) ,即找到一组参数 (w,b) 使得代价函数 C 最小。因此我们需要计算Cw Cb ,从而结合梯度下降算法求得 C 的最小值。接下来将以计算Cw为例进行说明。

  • 计算输出层 L 的偏导CwL。根据链式法则,我们可以得到下式:
    CwL=CaLaLzLzLwL.(1)
  • 计算隐藏层 L1 的偏导 CwL1 。根据链式法则,我们可以得到下式:
    CwL1=CaLaLzLzLaL1aL1zL1zL1wL1.(2)

观察公式 (1),(2) ,很明显用红框圈出来的是两个式子共有的一部分,通常我们称之为 δL ,表达式如公式 (3) 所示。我们可以用 δL 来计算输出层前一层的偏导。

δL=CaLaLzL.(3)

同理,隐藏层之间也有类似的共有部分,例如我们可以用 δL1 来计算隐藏层最后一层的前一层的偏导,表达式如公式 (4) 所示。

δL1=CaLaLzLzLaL1aL1zL1=δLzLaL1aL1zL1.(4)

通过公式 (3),(4) ,公式 (1),(2) 可以改写为以下形式:

CwLCwL1==δLzLwL,δL1zL1wL1.(5)(6)

假设激活函数为 σ(z) ,对公式 (3)(6) 进行详细的计算。

δLδL1CwLCwL1===========CaLaLzLaCσ(zL),δLzLaL1aL1zL1δL(wLaL1+bL)aL1σ(zL1)δLwLσ(zL1),δLzLwLδL(wLaL1+bL)wLδLaL1,δL1zL1wL1δL1(wL1aL2+bL1)wL1δL1aL2.(7)(8)(9)(10)

按照相同的原理,我们可以推得:

CbL=δLzLbL=δL(wLaL1+bL)bL=δL.(11)

将公式 (9),(10) 合并,并用 l,l+1 分别替换 L1,L ,则公式 (7)(11) 可总结为以下三个式子:

δl={aCσ(zL),δl+1wl+1σ(zl),if l=L,if l{L1,L2,,2},Cwl=δlal1,l{L,L1,,2},Cbl=δl,l{L,L1,,2}.(12)(13)(14)

观察公式 (12)(14) 可以看出,当前层的代价函数偏导,需要依赖于后一层的计算结果。这也是为什么这个算法的名称叫做反向传播算法

应用实践

接下来我们将用反向传播算法对MNIST手写数字数据集进行识别。这个问题比较简单,数字共有10种可能,分别为 {0,1,,9} ,因此是一个10分类问题。

完整代码请参考GitHub: machine-learning-notes(python3.6)

载入数据

首先我们从MNIST手写数字数据集官网下载训练集和测试集,并解压到data文件夹中,data文件夹中应该包含t10k-images.idx3-ubyte, t10k-labels.idx1-ubyte, train-images.idx3-ubyte, train-labels.idx1-ubyte这四个文件。接下来通过python-mnist包对数据集进行导入。如果尚未安装该包,可通过以下命令进行安装:

pip install python-mnist

使用python-mnist包载入数据,代码如下所示:

import numpy as np
from mnist import MNIST
from sklearn.preprocessing import MinMaxScaler

def vectorized_result(j):
    """
    将数字(0...9)变为one hot向量
    输入:
        j: int,数字(0...9)
    输出:
        e: np.ndarray, 10维的向量,其中第j位为1,其他位都为0。
    """
    e = np.zeros((10, 1));
    e[j] = 1.0
    return e

def load_data_wrapper(dirpath):
    """
    载入mnist数字识别数据集,并对其进行归一化处理
    输入:
        dirpath: str, 数据所在文件夹路径
    输出:
        training_data: list, 包含了60000个训练数据集,其中每一个数据由一个tuple '(x, y)'组成,
                        x是训练的数字图像,类型是np.ndarray, 维度是(784,1)
                        y表示训练的图像所属的标签,是一个10维的one hot向量
        test_data: list, 包含了10000个测试数据集,其中每一个数据由一个tuple '(x, y)'组成,
                        x是测试的数字图像,类型是np.ndarray, 维度是(784,1)
                        y表示测试的图像所属标签,int类型,是一个(0...9)的数字
    """
    mndata = MNIST(dirpath)
    tr_i, tr_o = mndata.load_training()
    te_i, te_o = mndata.load_testing()
    min_max_scaler = MinMaxScaler()
    tr_i = min_max_scaler.fit_transform(tr_i)
    te_i = min_max_scaler.transform(te_i)
    training_inputs = [np.reshape(x, (784, 1)) for x in tr_i]
    training_outputs = [vectorized_result(y) for y in tr_o]
    training_data = list(zip(training_inputs, training_outputs))
    test_inputs = [np.reshape(x, (784, 1)) for x in te_i]
    test_data = list(zip(test_inputs, te_o))
    return training_data, test_data

training_data, test_data = load_data_wrapper("../data/")

执行时,你可能会遇到下面的错误:

FileNotFoundError: [Errno 2] No such file or directory: '../data/t10k-images-idx3-ubyte'

这是因为python-mnist包中批量载入数据集时默认的文件名为t10k-images-idx3-ubyte,而从官网下载的数据集文件名为t10k-images.idx3-ubyte,因此只需要修改data文件夹中的文件名即可成功运行。

构建神经网络

  1. 网络初始化
    搭建网络的基本框架,包括神经网络各个层的数目,以及初始化参数。

    class Network(object):
        def __init__(self, sizes):
            """初始化神经网络
            1. 根据输入,得到神经网络的结构
            2. 根据神经网络的结构使用均值为0,方差为1的高斯分布初始化参数权值w和偏差b。
            输入:
            sizes: list, 表示神经网络各个layer的数目,例如[784, 30, 10]表示3层的神经网络。
                        输入层784个神经元,隐藏层只有1层,有30个神经元,输出层有10个神经元。
            """
            self.num_layers = len(sizes)
            self.sizes = sizes
            self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
            self.weights = [np.random.randn(y, x) for x, y in zip(sizes[:-1], sizes[1:])]
  2. 随机梯度下降

        def SGD(self, training_data, epochs, mini_batch_size, alpha, test_data=None):
            """随机梯度下降
            输入:
            training_data:是由tuples ``(x, y)``组成的list,x表示输入,y表示预计输出
            epoches:int, 表示训练整个数据集的次数
            mini_batch_size: int, 在SGD过程中每次迭代使用训练集的数目
            alpha: float, 学习速率
            test_data: 是由tuples ``(x, y)``组成的list,x表示输入,y表示预计输出。
                        如果提供了``test_data``,则每经过一次epoch,都计算并输出当前网络训练结果在测试集上的准确率。
                        虽然可以检测网络训练效果,但是会降低网络训练的速度。
            """
            if test_data:
                n_test = len(test_data)
            m = len(training_data)
            for j in range(epochs):
                np.random.shuffle(training_data)
                mini_batches = [training_data[k:k+mini_batch_size]
                            for k in range(0, m, mini_batch_size)]
                for mini_batch in mini_batches:
                    self.update_mini_batch(mini_batch, alpha)
                if test_data:
                    print("Epoch {0}: {1} / {2}".format(j, self.evaluate(test_data), n_test))
                else:
                    print("Epoch {0} complete".format(j))
    • 更新权值 w 和偏差b

          def update_mini_batch(self, mini_batch, alpha):
              """每迭代一次mini_batch,根据梯度下降方法,使用反向传播得到的结果更新权值``w``和偏差``b``
              输入:
              mini_batch: 由tuples ``(x, y)``组成的list
              alpha: int,学习速率
              """
              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_nable_w = self.back_prop(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_nable_w)]
              self.weights = [w-(alpha/len(mini_batch))*nw
                          for w, nw in zip(self.weights, nabla_w)]
              self.biases = [b-(alpha/len(mini_batch))*nb
                          for b, nb in zip(self.biases, nabla_b)]
    • 反向传播

          def back_prop(self, x, y):
              """反向传播
              1. 前向传播,获得每一层的激活值
              2. 根据输出值计算得到输出层的误差``delta``
              3. 根据``delta``计算输出层C_x对参数``w``, ``b``的偏导
              4. 反向传播得到每一层的误差,并根据误差计算当前层C_x对参数``w``, ``b``的偏导
              输入:
              x: np.ndarray, 单个训练数据
              y: np.ndarray, 训练数据对应的预计输出值
              输出:
              nabla_b: list, C_x对``b``的偏导
              nabla_w: list, C_x对``w``的偏导
              """
              nabla_b = [np.zeros(b.shape) for b in self.biases]
              nabla_w = [np.zeros(w.shape) for w in self.weights]
      
              # forward prop
              activation = x
              activations = [x]
              zs = []
              for b, w in zip(self.biases, self.weights):
                  z = np.dot(w, activation)+b
                  zs.append(z)
                  activation = sigmoid(z)
                  activations.append(activation)
              # backward prop
              delta = self.cost_derivative(activations[-1], y)*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 = 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)
    • Cx aL 的偏导

          def cost_derivative(self, output_activations, y):
              """代价函数对a的偏导
              输入:
              output_activations: np.ndarray, 输出层的激活值,即a^L
              y: np.ndarray, 预计输出值
              输出:
              output_activations-y: list, 偏导值
              """
              return (output_activations-y)
  3. 准确率计算

        def evaluate(self, test_data):
            """计算准确率,将测试集中的x带入训练后的网络计算得到输出值,
                并得到最终的分类结果,与预期的结果进行比对,最终得到测试集中被正确分类的数目
            输入:
            test_data: 由tuples ``(x, y)``组成的list
            输出:
            int, 测试集中正确分类的数据个数
            """
            test_results = [(np.argmax(self.feed_forward(x)), y) for x, y in test_data]
            return sum(int(x==y) for (x, y) in test_results)
    • 前馈
      根据当前网络训练的结果,对数据 x <script type="math/tex" id="MathJax-Element-57">x</script>进行预测
        def feed_forward(self, a):
            """前馈
            输入:
            a:np.ndarray
            输出:
            a:np.ndarray,预测输出
            """
            for b, w in zip(self.biases, self.weights):
                a = sigmoid(np.dot(w, a)+b)
            return a
  4. 激活函数及其导数

    def sigmoid(z):
        """The sigmoid function"""
        return 1.0/(1.0+np.exp(-z))
    
    def sigmoid_prime(z):
        """Derivative of the sigmoid function"""
        return sigmoid(z)*(1-sigmoid(z))
  5. 训练
    训练全部的数据需要一定的时间(在我实验室的老机器上用时3m32s,仅供参考),如果想要快速的查看训练结果,可以取部分训练集和测试集进行训练和测试。

    net = Network([784, 30, 10])
    net.SGD(training_data, 30, 10, 3.0, test_data=test_data)
    输出:
    Epoch 0: 9121 / 10000
    Epoch 1: 9271 / 10000
    Epoch 2: 9317 / 10000
    Epoch 3: 9371 / 10000
    Epoch 4: 9362 / 10000
    Epoch 5: 9395 / 10000
    Epoch 6: 9393 / 10000
    Epoch 7: 9475 / 10000
    Epoch 8: 9473 / 10000
    Epoch 9: 9473 / 10000
    Epoch 10: 9450 / 10000
    Epoch 11: 9466 / 10000
    Epoch 12: 9477 / 10000
    Epoch 13: 9497 / 10000
    Epoch 14: 9475 / 10000
    Epoch 15: 9477 / 10000
    Epoch 16: 9481 / 10000
    Epoch 17: 9483 / 10000
    Epoch 18: 9498 / 10000
    Epoch 19: 9471 / 10000
    Epoch 20: 9488 / 10000
    Epoch 21: 9486 / 10000
    Epoch 22: 9465 / 10000
    Epoch 23: 9461 / 10000
    Epoch 24: 9499 / 10000
    Epoch 25: 9496 / 10000
    Epoch 26: 9501 / 10000
    Epoch 27: 9498 / 10000
    Epoch 28: 9499 / 10000
    Epoch 29: 9506 / 10000
    

    可以看到经过30轮的训练,准确率已经达到了95.06%(epoch 29)。作为第一次尝试,这个准确率已经非常令人满意了。
    接下来我们增大隐藏层的层数,例如50,来重新训练,看看效果如何。隐藏层增加后,训练速度会变得更加缓慢(用时4m46s),在等待训练完成的过程中,可以去倒杯茶,放松一下身体。

    net = Network([784, 50, 10])
    net.SGD(training_data, 30, 10, 3.0, test_data=test_data)
    输出:
    Epoch 0: 9176 / 10000
    Epoch 1: 9307 / 10000
    Epoch 2: 9406 / 10000
    Epoch 3: 9433 / 10000
    Epoch 4: 9476 / 10000
    Epoch 5: 9508 / 10000
    Epoch 6: 9499 / 10000
    Epoch 7: 9502 / 10000
    Epoch 8: 9528 / 10000
    Epoch 9: 9533 / 10000
    Epoch 10: 9569 / 10000
    Epoch 11: 9573 / 10000
    Epoch 12: 9559 / 10000
    Epoch 13: 9592 / 10000
    Epoch 14: 9566 / 10000
    Epoch 15: 9588 / 10000
    Epoch 16: 9575 / 10000
    Epoch 17: 9588 / 10000
    Epoch 18: 9584 / 10000
    Epoch 19: 9587 / 10000
    Epoch 20: 9583 / 10000
    Epoch 21: 9607 / 10000
    Epoch 22: 9589 / 10000
    Epoch 23: 9595 / 10000
    Epoch 24: 9605 / 10000
    Epoch 25: 9600 / 10000
    Epoch 26: 9600 / 10000
    Epoch 27: 9595 / 10000
    Epoch 28: 9592 / 10000
    Epoch 29: 9599 / 10000
    

    观察结果可以发现准确率上升到了96.07%(epoch 21)。在这个实例下,增加隐藏层提高了训练的准确率。但是并非一直如此,在后续的文章中,我将继续介绍如何提高网络的训练速度和训练效果。

参考文献

Michael A. Nielsen, “Neural network and deep learning”, Determination Press, 2015

作者:mrpanc
博客:http://blog.csdn.net/peter_cpan
Github:https://github.com/mrpanc
2018年1月17号

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值