利用神经网络识别手写数字详解(仅Numpy)-附程序函数架构思维导图

利用神经网络识别手写数字

0 整体功能概述

这段代码实现了一个前馈神经网络(Feedforward Neural Network),并使用**小批量随机梯度下降(Stochastic Gradient Descent, SGD)和反向传播算法(Backpropagation)**对其进行训练。整体结构清晰、简洁,适用于学习神经网络基本原理。下面是代码的完整功能和模块梳理:

1.定义网络结构:通过 sizes 列表指定每层神经元数量(如 [784, 30, 10] 表示输入层784个神经元,隐藏层30个,输出层10个)。
2.初始化参数:随机初始化权重和偏置。
3.前向传播(Feedforward):计算输入在网络中的输出。
4.训练(SGD):使用小批量随机梯度下降进行训练。
5.反向传播(Backpropagation):计算每层的梯度。
6.权重/偏置更新:基于梯度进行参数调整。
7.模型评估:测试集上评估准确率。

1 模块与函数结构梳理

1.1 模块导入

import random
import numpy as np
from neural_networks_and_deep_learning.src import mnist_loader

1.2 神经网络初始化

神经网络代码的核心功能是一个 Network 类,我们用它来表示神经网络。以下是我们用于初始化 Network 对象的代码:

class Network(object):

    def __init__(self, sizes):
        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:])]

在此代码中,列表sizes包含相应层中的神经元数量。例如,如果我们想创建一个 Network 对象,第一层有 2 个神经元,第二层有 3 个神经元,最后一层有 1 个神经元,我们将使用以下代码来执行此作:

net = Network([2, 3, 1])

Network 对象中的偏置(biases)和权重(weights)都是随机初始化的,使用 Numpy np.random.randn 函数生成均值为 0 的高斯分布和标准差1。这种随机初始化为我们的随机梯度下降算法提供了一个起点。请注意,Network初始化代码假定第一层神经元是 input 层,并且省略了为这些神经元设置任何偏差,因为偏差仅用于计算后续层的输出。
另请注意,偏置(biases)和权重(weights)存储为 Numpy 矩阵列表。因此,例如 net.weights[1] 是一个 Numpy 矩阵,存储连接第二层和第三层神经元的权重(它不是第一层和第二层,因为 Python 的列表索引从 0 开始)。由于 net.weights[1] 相当冗长,我们只表示矩阵 w
。它是一个矩阵,使得 w j k w_jk wjk是连接第二层第 k个神经元和第三层第j个神经元。指数j 和k的这种排序可能看起来很奇怪 - 当然,交换 j和 k可能会更合理?使用这种排序的最大优点是,这意味着第三层神经元的激活向量是:
a ′ = σ ( w ∙ a + b ) \begin{align} a^\prime=\sigma(w\bullet a+b) \end{align} a=σ(wa+b)

1.3 激活函数

考虑到所有这些,编写代码来计算 Network 实例的输出就很容易了。我们首先定义 sigmoid 函数:

def sigmoid(z):
    return 1.0/(1.0+np.exp(-z))

注意,当输入 z 是向量或 Numpy 数组时,Numpy 会自动逐元素地应用 sigmoid 函数,也就是说,以矢量化的形式进行。

1.4 前向传播算法

然后我们为网络类添加了一种前馈方法,该方法在给定网络输入 a 时返回对应的输出。该方法只是对每一层应用方程(1):

def feedforward(self, a):
        """Return the output of the network if "a" is input."""
        for b, w in zip(self.biases, self.weights):
            a = sigmoid(np.dot(w, a)+b)
        return a

1.5 随机梯度下降算法

当然,我们希望我们的网络对象主要是学习。为此,我们将给它们提供一个实现随机梯度下降的SGD方法。

    def SGD(self, training_data, epochs, mini_batch_size, eta, test_data=None):
        """Train the neural network using mini-batch stochastic gradient descent. The "training_data" is a list of
        tuples "(x, y)" representing the training inputs and the desired outputs. The other non-optional parameters are
        self-explanatory. If "test_data" is provided then the network will be evaluated against the test data after each
        echo, and partial progress printed out. This is useful for tracking progress, but slows things down
        substantially"""
        if test_data: n_test = len(test_data)
        n = len(training_data) # n 是训练数据的总数
        for j in range(epochs):
            random.shuffle(training_data)
            mini_batchs = [training_data[k:k+mini_batch_size] for k in range(0, n, mini_batch_size)]
            for mini_batch in mini_batchs:
                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))

训练数据是一个元组列表 (x, y),表示训练输入和相应的期望输出。变量 epochs 和 mini_batch_size 是你所期望的——训练的轮数,以及在采样时使用的小批量大小。eta 是学习率,η。如果提供了可选参数 test_data,则程序将在每个训练轮次结束后评估网络,并输出部分进度。这对于跟踪进展非常有用,但会显著减慢速度。
代码的工作原理如下。在每个周期中,它首先随机打乱训练数据,然后将其分成合适大小的小批量。这是一种从训练数据中随机抽样的简单方法。然后对于每个小批量,我们应用一次梯度下降的步骤。这是通过代码self.update_mini_batch(mini_batch, eta) 完成的,该代码根据梯度下降的一次迭代更新网络的权重和偏差,只使用 mini_batch 中的训练数据。以下是 update_mini_batch 方法的代码:

def update_mini_batch(self, mini_batch, eta):
        """Update the network's weights and biases by applying
        gradient descent using backpropagation to a single mini batch.
        The "mini_batch" is a list of tuples "(x, y)", and "eta"
        is the learning rate."""
        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)

1.6 反向传播算法

这会调用一种称为反向传播算法的东西,这是一种快速计算成本函数梯度的方法。因此,update_mini_batch 通过计算 mini_batch 中每个训练样本的这些梯度,然后相应地更新 self.weights 和 self.biases 来简单地工作。
该行使用 backprop 方法来计算偏导数 ( ∂ C ( X j   ) ) / ( ∂ b k   ) (\partial C_(X_j\ ))/(\partial b_k\ ) (C(Xj ))/(bk ) ( ∂ C ( X j   ) ) / ( ∂ w k   ) (\partial C_(X_j\ ))/(\partial w_k\ ) (C(Xj ))/(wk )。这种变化是为了利用 Python 的一个特性,即使用负索引从列表的末尾向后计数,例如, l [ − 3 ] l[-3] l[3]是列表 l l l中倒数第三个条目。下面是 backprop 的代码,以及一些辅助函数,这些函数用于计算 σ 函数、导数 σ′ 以及成本函数的导数。

def backprop(self, x, y):
        """Return a tuple "(nabla_b, nabla_w)" representing the
        gradient for the cost function C_x.  "nabla_b" and
        "nabla_w" are layer-by-layer lists of numpy arrays, similar
        to "self.biases" and "self.weights"."""
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        nabla_w = [np.zeros(w.shape) for w in self.weights]
        # 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
            zs.append(z)
            activation = sigmoid(z)
            activations.append(activation)
        # 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())
        # Note that the variable l in the loop below is used a little
        # differently to the notation in Chapter 2 of the book.  Here,
        # l = 1 means the last layer of neurons, l = 2 is the
        # second-last layer, and so on.  It's a renumbering of the
        # scheme in the book, used here to take advantage of the fact
        # that Python can use negative indices in lists.
        for l in xrange(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)
def cost_derivative(self, output_activations, y):
        """Return the vector of partial derivatives \partial C_x /
        \partial a for the output activations."""
        return (output_activations-y) 

2 基于随机梯度下降学习步骤

以下是算法基于小批次应用梯度下降学习步骤:

  1. 输入一组训练样本
  2. 对每一个训练样本x: 设置相应的输入激活 a x , 1 a^{x,1} ax,1,并执行以下步骤:
    前馈:对于每一层 l = 2 , 3 , … , L l=2,3,\ldots,L l=2,3,,L计算 z x , l = w l a x , l − 1 + b l z^{x,l}=w^la^{x,l-1}+b^l zx,l=wlax,l1+bl a x , l = σ ( z x , l ) a^{x,l}=\sigma\left(z^{x,l}\right) ax,l=σ(zx,l)
    输出误差 δ x , L \delta^{x,L} δx,L:计算矢量 δ x , L = ▽ a C x ⊙ σ ′ ( Z x , L ) \delta^{x,L}=\bigtriangledown_{a}C_{x}\odot\sigma^{'}\left(Z^{x,L}\right) δx,L=aCxσ(Zx,L)
    反向传播误差:对于每一层 l = L − 1 , L − 2 , … , 2 l=L-1,L-2,\ldots,2 l=L1,L2,,2,计 δ x , l = ( ( w l + 1 ) T δ x , l + 1 ) ⨀ σ ′ ( z x , l ) \delta^{x,l}=\left(\left(w^{l+1}\right)^T\delta^{x,l+1}\right)\bigodot\sigma^\prime\left(z^{x,l}\right) δx,l=((wl+1)Tδx,l+1)σ(zx,l)
  3. 梯度下降:对每一层 l = L − 1 , L − 2 , … , 2 根据 w k → w k ′ = w k − η m ∑ j ∂ C X j ∂ w k l=L-1,L-2,\ldots,2根据w_k\rightarrow w_k^\prime=w_k-\frac{\eta}{m}\sum_{j}\frac{\partial C_{X_j}}{\partial w_k} l=L1,L2,,2根据wkwk=wkmηjwkCXj更新权重,根据 b k → b k ′ = b k − η m ∑ j ∂ C X j ∂ b k b_k\rightarrow b_k^\prime=b_k-\frac{\eta}{m}\sum_{j}\frac{\partial C_{X_j}}{\partial b_k} bkbk=bkmηjbkCXj更新偏置

3 实现手写数字识别效果

这个程序对手写数字的识别效果如何?首先,我们加载MNIST数据。我将使用一个小助手程序mnist_loader.py来完成这项工作,下面将对此进行描述。我们在Python环境中执行以下命令,

>>> import mnist_loader
>>> training_data, validation_data, test_data = mnist_loader.load_data_wrapper()

当然,这也可以在一个单独的Python程序中完成,但如果你在跟随的话,在Python shell中完成可能更容易。
加载MNIST数据后,我们将建立一个有30个隐藏神经元的网络。在导入上面列出的名为network的Python程序后,我们进行此操作。

>>> import network
>>> net = network.Network([784, 30, 10])

最后,我们将使用随机梯度下降在30个周期内从MNIST训练数据中学习,迷你批量大小为10,学习率η=3.0。

>>> net.SGD(training_data, 30, 10, 3.0, test_data=test_data)

一旦我们学习了一组良好的权重和偏差,可以轻松地将其移植到Javascript中在网页浏览器中运行,或在移动设备上作为原生应用程序运行。这里是神经网络一次训练运行的部分输出记录。 该转录显示了神经网络在每个训练周期后正确识别的测试图像数量。如您所见,仅经过一个周期,这一数字已达到10,000中的9,129,并且这个数字还在继续增加。

Epoch 0: 9129 / 10000
Epoch 1: 9295 / 10000
Epoch 2: 9348 / 10000
...
Epoch 27: 9528 / 10000
Epoch 28: 9542 / 10000
Epoch 29: 9534 / 10000

也就是说,训练好的网络给我们的分类率大约是95个百分点 - 在峰值时为95.42个百分点(“第28个时期”)!如果你运行代码,你的结果未必会与我的完全相同,因为我们将使用(不同的)随机权重和偏差来初始化我们的网络。为了生成本章的结果,我进行了三次最佳运行的测试。
附件:手写数字程序(仅Numpy)思维导图

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值