http://neuralnetworksanddeeplearning.com python3描述,针对问题MNIST 数字分类

一、基本的神经网络


1、神经网络基本概念

(1)神经元

概念上 神经网络的基本单元,神经网络网络由神经元连接而来。 数学上 基本运算单元,包含多个输入和1个输出 s型神经元运算公式如下: $$ a = \sigma(WX + b) \
\sigma(z) = \frac{1}{1+e^{-z}} \
z = WX + b $$ 符号说明:

  • a代表输出
  • \(\sigma\)一种激活函数
  • W权重
  • b偏置
  • z带权输入

(2)层

  • 输入层:代表数据的层,没有权重和偏置
  • 输出层:最后一层,输出的结果为最终结果,有权重和偏置参数
  • 隐藏层:输入输出中间的计算层,有权重和偏置参数

(3)向量化计算

假设全连接型神经元形状为[3,4,3]权重和偏置参数矩阵W、b

                 k:0 1 2
   ┌        0   ┌ [ , , ] ┐   ┌ [ , , , ] ┐   ┐
W  |      j:1   | [ , , ] |   | [ , , , ] |   |
   |        2   | [ , , ] |   └ [ , , , ] ┘   |
   └        3   └ [ , , ] ┘,                  ┘

l:       0           1              2


   ┌           0   ┌  ┐           ┌  ┐   ┐
b  |         j:1   |  |           |  |   |
   |           2   |  |           └  ┘   |
   └           3   └  ┘,                 ┘

符号描述:

  • l-层数,
  • j-神经元,
  • k-上层神经元的输出

数学表述 $$ a^0_j = x_j \
a^l_j = \sigma( (\sum_{k} a^{l-1}_k*W^l_{jk}) + b ) $$

2、神经网络的程序的简单设计

(1)基本要求

  • 对于新的输入可以得出计算结果
  • 可以进行训练

(2)程序结构

初始化神经网络: 指定层数和结构,初始化权重和偏置矩阵 神经网络计算 给定一个输入,得到网络输出结果 训练网络 给定多组输入和正确标签,使神经网络输出正确性提高 测试网络 给定测试数据,给出神经网络的性能评价

类与函数,调用关系说明:

  • 全局函数
    • sigmoid(z):神经元的激活函数,参数z为神经元的带权输入
    • sigmoid_prime(z):激活函数的导数,参数z为神经元的带权输入
  • Network:神经网络的封装
    • __init__(self,sizes):构造函数,size为一个list,表示各层的神经元个数
    • feedforward(self,a):给一个输入a,计算网络的输出
    • SGD(self, training_data, epochs, mini_batch_size, eta, test_data=None):神经网络训练——梯度下降算法
      • update_mini_batch(self, mini_batch, eta):下批次随机下降
        • backprop(self, x, y):方向传播算法
          • cost_derivative(self, output_activations, y):计算\(\frac{\partial C}{\partial a^L_j}\)
      • evaluate(self, test_data):评估模型

3、获取神经网络的输出

(1)初始化神经网络

class Network(object):
    def __init__(self,sizes):
        """
        列表 sizes 包含各层神经元的数量。
        例如,如果我们想创建⼀个在第⼀层有2个神经元,
        第⼆层有3个神经元,最后层有1个神经元的 Network 对象,
        我们应这样写代码:net = Network([2, 3, 1])

        """
        self.num_layers = len(sizes) #成员变量,代表神经网络的层数
        self.sizes = sizes #成员变量,代表神经网络的尺寸
        self.biases = [np.random.randn(y,1) for y in sizes[1:]] #偏置
        #print(self.biases)
        self.weights = [np.random.randn(y,x) #权重
                        for x,y in zip(sizes[:-1],sizes[1:])]
        #print(self.weights)

(3)获取神经网络的输出

    def feedforward(self,a):
        """向前传播,给定输入a,计算网络的输出,并返回"""
        for b, w in zip(self.biases, self.weights):
            a = sigmoid(np.dot(w,a)+b)
        return a

4、训练神经网络反向传播

(1)代价函数

从整体上看: 是一个关于神经网络输出和期望输出的函数,这个函数要能描述出,模型在训练数据上的性能,关系是:模型准确性越高代价函数值越低,反之…。 从训练模型上看: 是一个关于权重和偏置的函数,在训练过程中,要改变权重和偏置使代价函数值变小

一个很容易构造的代价函数:二次代价函数 $$ \begin{eqnarray} C(w,b) \equiv \frac{1}{2n} \sum_x | y(x) - a|^2 \end{eqnarray} $$

(2)梯度下降

训练的实质是: 改变权重和偏置使代价函数值变小 => 假设代价函数减小的值为 \(\Delta C\),根据导数基本公式\(f’(x)=\frac{f(x+\Delta x) - f(x)}{\Delta x}\) 得: $$ \Delta C =\Delta W \frac{\partial C}{\partial W} + \Delta b \frac{\partial C}{\partial b} $$ => 更改权重和偏置的值就可以使代价函数的值减小 $$ \begin{eqnarray} w_k & \rightarrow & w_k’ = w_k-\eta \frac{\partial C}{\partial w_k} \
b_l & \rightarrow & b_l’ = b_l-\eta \frac{\partial C}{\partial b_l} \end{eqnarray} $$

  • 其中\(\eta\)称之为学习率,在训练模型时根据情况指定

进一步简化:

$$ \begin{eqnarray} \nabla C \equiv \left(\frac{\partial C}{\partial v_1}, \ldots, \frac{\partial C}{\partial v_m}\right)^T \
v \rightarrow v’ = v-\eta \nabla C \end{eqnarray} $$

  • \(\nabla C\) 称之为梯度
  • \(v\)泛指函数的因变量

随机梯度下降

使用条件: 代价函数可以写成这种形式:\(C = \frac{1}{n} \sum_x C_x\) 这样可以使用每次选取一小批次\(X_j\)计算梯度

$$ \begin{eqnarray} w_k & \rightarrow & w_k’ = w_k-\frac{\eta}{m} \sum_j \frac{\partial C_{X_j}}{\partial w_k} \
b_l & \rightarrow & b_l’ = b_l-\frac{\eta}{m} \sum_j \frac{\partial C_{X_j}}{\partial b_l}, \end{eqnarray} $$ 更进一步的在线学习 每次选取一个训练样本 $$ \begin{eqnarray} w_k & \rightarrow & w_k’ = w_k-\frac{\eta}{n} \sum_i \frac{\partial C_{X_i}}{\partial w_k} \
b_l & \rightarrow & b_l’ = b_l-\frac{\eta}{n} \sum_i \frac{\partial C_{X_i}}{\partial b_l}, \end{eqnarray} $$

编程实现:

    def SGD(self, training_data, epochs, mini_batch_size, eta,

            test_data=None):
        """使用小批次随机梯度下降训练神经网络。

        ``training_data``:
            是一个(x,y)元组的列表,代表着训练数据和标签
        ``epochs``:表示迭代次数
        ``mini_batch_size``:随机批次大小
        ``eta``:学习率
        ``test_data``:

            是一个(x,y)元组的列表,代表着测试数据和标签,检验模型效果
        """
        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:
                acc = self.evaluate(test_data)
                print("步数 %d%d / %d  正确率%f"  % (
                    j, self.evaluate(test_data), n_test, acc/n_test))
            else:
                print("步数 %d 已完成" % j)

    def update_mini_batch(self, mini_batch, eta):
        """更新神经网络的权重和偏置,运用小批次数据通过使用反向传播的梯度下降算法

        ``mini_batch``:
            是一个(x,y)元组的列表,代表着一个批次的数据和标签
        ``eta``:学习率
        """
        nabla_b = [np.zeros(b.shape) for b in self.biases] #创建全为0的b
        nabla_w = [np.zeros(w.shape) for w in self.weights] #创建全为0的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.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)]

    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)

(3)反向传播四个基本公式

梯度下降问题的关键就是求梯度\(\nabla C\)(也就是说关于W和b的偏导数)

反向传播就是解决此问题的快速算法

前提条件

  • 代价函数可以写成这个形式\(C = \frac{1}{n} \sum_x C_x\)
  • 代价函数可以写成关于神经网络输出的函数\(C = f(a^L)\)

准备公式

$$ \begin{eqnarray} a^{l}_j = \sigma\left( \sum_k w^{l}_{jk} a^{l-1}_k + b^l_j \right) \end{eqnarray} $$

  • \(a^{l}_j\) 表示第l层第j个神经元的输出(激活值)

向量化

$$ \begin{eqnarray} a^{l} = \sigma(w^l a^{l-1}+b^l). \end{eqnarray} $$

  • 其中\(z^l \equiv w^l a^{l-1}+b^l \)

预先定义

$$ \begin{eqnarray} \delta^l_j \equiv \frac{\partial C}{\partial z^l_j} \end{eqnarray} $$

  • 定义 l 层的第 j 个神经元上的误差为: \(\delta^l_j\)

公式1

输出层误差的⽅程

$$ \begin{eqnarray} \delta^L_j = \frac{\partial C}{\partial a^L_j} \sigma’(z^L_j) \end{eqnarray} $$

  • \(\delta^L\) 输出层误差

若使用二次代价函数\(C = \frac{1}{2} \sum_j (y_j-a^L_j)^2\),化简后的向量化公式

$$ \begin{eqnarray} \delta^L = (a^L-y) \odot \sigma’(z^L) \end{eqnarray} $$

  • 其中 \(\odot\)表示按元素乘积:[1,2]⊙[3,4]=[1∗3,2∗4]=[3,8]

公式2

递推公式

$$ \begin{eqnarray} \delta^l = ((w^{l+1})^T \delta^{l+1}) \odot \sigma’(z^l) \end{eqnarray} $$

公式3

计算关于偏置b的偏导数

$$ \begin{eqnarray} \frac{\partial C}{\partial b^l_j} =\delta^l_j \end{eqnarray} $$

公式4

计算关于权重W的偏导数 $$ \begin{eqnarray} \frac{\partial C}{\partial w^l_{jk}} = a^{l-1}_k \delta^l_j \end{eqnarray} $$

反向传播算法描述(索引从1开始)

  • 输⼊ x:计算激活值\(a^1\)
  • 前向传播:对每个 l = 2, 3, …, L 计算\(z^{l} = w^l a^{l-1}+b^l\)和\(a^{l} = \sigma(z^{l})\)
  • 输出层误差\(\delta^{L}\):计算向量\(\delta^{L} = \nabla_a C \odot \sigma’(z^L)\)
  • 反向误差传播:对每个 l = L − 1, L − 2, …, 2,计算\(\delta^{l} = ((w^{l+1})^T \delta^{l+1}) \odot \sigma’(z^{l})\)
  • 输出:代价函数的梯度由\(\frac{\partial C}{\partial w^l_{jk}} = a^{l-1}_k \delta^l_j\)和\(\frac{\partial C}{\partial b^l_j} = \delta^l_j\)得出

编程实现

def sigmoid_prime(z): #全局函数
    """s型函数的导数"""
    return sigmoid(z)*(1-sigmoid(z))


    def backprop(self, x, y):
        """返回一个元组``(nabla_b, nabla_w)``代表C_x的梯度
        ``nabla_b`` 和 ``nabla_w``类型类似于
        ``self.biases`` 和 ``self.weights``"""
        nabla_b = [np.zeros(b.shape) for b in self.biases] #拷贝b和w的形状
        nabla_w = [np.zeros(w.shape) for w in self.weights]
        # 向前传播
        activation = x # 初始化激活值
        activations = [x] # 储存每一层的激活值的列表
        zs = [] # 储存每一层的z向量的列表
        for b, w in zip(self.biases, self.weights):
            z = np.dot(w, activation)+b # 计算当前层的z向量
            zs.append(z)
            activation = sigmoid(z) # 计算激活值
            activations.append(activation)
        # 反向传播
        delta = self.cost_derivative(activations[-1], y) * \
            sigmoid_prime(zs[-1]) # 计算delta^L
        nabla_b[-1] = delta #b的偏导数 = delta
        nabla_w[-1] = np.dot(delta, activations[-2].transpose())
        # 注意在这里,l=1表示最后一层,l=2表示倒数第2层以此类推。
        for l in range(2, self.num_layers):
            z = zs[-l] #从后往前数,得到z
            sp = sigmoid_prime(z) #计算s型函数的导数
            delta = np.dot(self.weights[-l+1].transpose(), delta) * sp #计算出delta
            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):
        """返回C_x的偏导数值"""
        return (output_activations-y)

二、改进神经网络


1、使用交叉熵代价函数

(1)二次代价函数的问题

按照直观启发,当网络性能极差时,学习速度应该很快,但是二次代价函数在网络性能极差时学习速度可能很慢

(2)交叉熵代价函数

$$ \begin{eqnarray} C = -\frac{1}{n} \sum_x \left[y \ln a + (1-y ) \ln (1-a) \right] \end{eqnarray} $$

(3)优点

  • 学习速度符合人的直观感受
  • 在计算最后一层的误差时效率更高约掉了\(\sigma’(z)\)项

反向传播公式1化简形式 根据\(\sigma’(z) = \sigma(z)(1-\sigma(z))\)和\(a=\sigma(z)\)化简公式1 $$ \delta^L_j = a^{l}_j - y_j $$

(4)优化代码

程序结构修改:

  • 将代价函数相关的代码抽象成一个类,作为Network的构造函数的参数传入
  • 修改backprop方法的代码,将计算输出层的误差改为调用(self.cost).delta函数

    #### 定义二次和交叉熵代价函数对象
    
    class QuadraticCost(object):
    '''二次代价函数'''
    @staticmethod
    def fn(a, y):
        """返回代价函数值,用于观测网络
        ``a`` :神经网络的输出
        ``y`` :数据精确值
        np.linalg.norm求向量的模,也就是就是长度|v| = sqrt(x1^2 + x2^2 + ... + xn^2)
        """
        return 0.5*np.linalg.norm(a-y)**2
    
    @staticmethod
    def delta(z, a, y):
        """返回输出层的误差,公式为:代价函数关于输出层神经元输出的偏导数 * 激活函数关于带权输入的导数"""
        return (a-y) * sigmoid_prime(z)
    
    
    class CrossEntropyCost(object):
    '''交叉熵函数'''
    @staticmethod
    def fn(a, y):
        """返回代价函数值,用于观测网络
        ``a`` :神经网络的输出
        ``y`` :数据精确值
        np.nan_to_num表示,使用近似数字代替inf
        """
        return np.sum(np.nan_to_num(-y*np.log(a)-(1-y)*np.log(1-a)))
    
    @staticmethod
    def delta(z, a, y):
        """返回输出层的误差,公式为:代价函数关于输出层神经元输出的偏导数 * 激活函数关于带权输入的导数"""
        return (a-y)
    
    
    def __init__(self, sizes, cost=CrossEntropyCost):
        #....略
        self.cost=cost
    
    def backprop(self, x, y):
    				#...略
    				# 反向传播
    				# delta = self.cost_derivative(activations[-1], y) * \
    				#   sigmoid_prime(zs[-1]) # 计算delta^L
        delta = (self.cost).delta(zs[-1],activations[-1], y)

2、柔性最⼤值(softmax)

(1)定义

softmax函数类似于\(\sigma(z) = \frac{1}{1+e^{-z}}\)的作用,一般用于输出层,定义如下 $$ \begin{eqnarray} a^L_j = \frac{e^{z^L_j}}{\sum_k e^{z^L_k}} \end{eqnarray} $$

说明:

  • \(a^L_j\) 表示:第L层第j个神经元的输出
  • \(z^L_j = \sum_{k} w^L_{jk} a^{L-1}_k + b^L_j\) 表示:第L层第j个神经元的带权输入
  • \(z^L_k\) 表示:分⺟中的求和是在所有的输出神经元上进⾏的,表示第L层神经元的第k个神经元的带权输入

(2)特性

  • 加入柔性最大值的神经网络层,的各个神经元输出和=1
  • 某个神经元的带权输入增加,他的输出也会增加,其他神经元的输出将减少
  • 可以解决使用s型函数学习缓慢问题
  • 柔性最⼤值层的输出可以被看做是⼀个概率分布
  • 柔性最⼤值的⾮局部性,:任何特定的输出激活值依赖所有的带权输⼊

(3)使用柔性最大值的有对数似然代价函数

$$ \begin{eqnarray} C = -\ln a^L_y \end{eqnarray} $$ 理解:y为标签为1的对应神经元的编号

(4)使用柔性最大值的反向传播

$$ \begin{eqnarray} \delta^L_j = a^L_j -y_j \end{eqnarray} $$

(5)如何选择输出层和代价函数

几种方案:

  • 交叉熵代价函数 + S 型输出层
  • 对数似然代价函数 + 柔性最⼤值输出层

以上方案效果都很好

(6)编程实现

目前未实现,目前使用交叉熵代价函数 + S 型输出层的组合

3、过拟合和规范化

(1)过拟合的一些特性

  • 训练数据上,代价函数值迭代进行一直下降
  • 测试数据上,分类准确率没有持续上升,在迭代中后期停止增长,一直波动
  • 测试数据上,代价函数值没有持续下降,在迭代中后期开始上升,图像表现为
  • 训练数据上,分类准确率持续上升,接近100%

(2)过拟合产生的原因

过度拟合是神经⽹络的⼀个主要问题。这在现代⽹络中特别正常,因为⽹络权重和偏置数量巨⼤,导致模型的泛化能力很差

(3)检测过拟合

跟踪测试数据集合上的准确率随训练变化情况,如果我们看到测试数据上的准确率不再提升,那么我们就停⽌训练。

提前停止

使用validation_data验证数据集而不是test_data测试数据集,计算分类准确率不再提升,那么我们就停⽌训练。

validation_data的作用

衡量不同的超参数(如迭代期,学习速率,最好的⽹络架构等等)的选择的效果

test_data的作用

纯粹测试模型的性能

增大训练数据规模可以抑制出现过拟合

(4)规范化(正规化)

最常见的规范化——权重衰减(weight decay)或者L2 规范化

L2 规范化的想法是增加⼀个额外的项到代价函数上,这个项叫做规范化项。下⾯是规范化的交叉熵:

$$ \begin{eqnarray} C = -\frac{1}{n} \sum_{xj} \left[ y_j \ln a^L_j+(1-y_j) \ln (1-a^L_j)\right] + \frac{\lambda}{2n} \sum_w w^2 \end{eqnarray} $$

  • 第⼀个项就是常规的交叉熵的表达式
  • 第⼆个现在加⼊的就是所有权重的平⽅的和
  • λ/2n进行量化调整
  • λ > 0 可以称为规范化参数
  • n 就是训练集合的大小

⼆次代价函数。类似的规范化的形式如下:

$$ \begin{eqnarray} C = \frac{1}{2n} \sum_x |y-a^L|^2 + \frac{\lambda}{2n} \sum_w w^2 \end{eqnarray} $$ 两者都可以写成这样: $$ \begin{eqnarray} C = C_0 + \frac{\lambda}{2n} \sum_w w^2 \end{eqnarray} $$

(5)使用规范化的梯度下降

对于偏置b $$ \begin{eqnarray} b & \rightarrow & b -\eta \frac{\partial C_0}{\partial b} \end{eqnarray} $$

对于权重W $$ \begin{eqnarray} w & \rightarrow & w-\eta \frac{\partial C_0}{\partial w}-\frac{\eta \lambda}{n} w \
& = & \left(1-\frac{\eta \lambda}{n}\right) w -\eta \frac{\partial C_0}{\partial w}. \end{eqnarray} $$

(6)合适的规范化作用

  • 防止出现过拟合,提高模型的泛化程度,以至于提高模型的性能

(7)优化代码

程序结构修改:

  • 修改梯度下降方法SGD:
    • 方法参数
      • 添加lmbda参数
      • test_data改为evaluation_data
      • 添加相关监视器方法开关,监视在每个训练迭代的验证集代价函数值、精确度和训练集代价函数值、精确度和
    • 方法逻辑
      • 添加正规化项
      • 添加监视器相关代码
  • 修改方法update_mini_batch,添加lmbdan关于正规化的参数和逻辑
  • 在Network添加方法:
    • accuracy(self, data, convert=False):计算针对数据data模型输出的正确的数目
    • total_cost(self, data, lmbda, convert=False):计算针对数据data模型的代价函数值
  • 添加全局函数:
    • vectorized_result(j,c)向量化标签
  • 删除Network方法:

    • evaluate(self, test_data)该方法由新添加的accuracy(self, data, convert=False)代替

      def vectorized_result(j,c):
      """将标量转换为向量表示
      ``j``:表示标量值
      ``c``:表示分类的数目,分成c个不同的类别
      """
      e = np.zeros((c, 1))
      e[j] = 1.0
      return e
      
      def SGD(self, training_data, epochs, mini_batch_size, eta,
          lmbda = 0.0,
          evaluation_data=None,
          monitor_evaluation_cost=False,
          monitor_evaluation_accuracy=False,
          monitor_training_cost=False,
          monitor_training_accuracy=False):
      """使用小批次随机梯度下降训练神经网络。
      
      ``training_data``:
          是一个(x,y)元组的列表,代表着训练数据和标签
      ``epochs``:表示迭代次数
      ``mini_batch_size``:随机批次大小
      ``eta``:学习率
      ``lmbda``:正规化参数
      ``evaluation_data``:
          是一个(x,y)元组的列表,代表着验证集的数据和标签,检验模型效果
      ``monitor_evaluation_cost``:是否监视验证集代价函数变化
      ``monitor_evaluation_accuracy``:是否监视验证集准确率变化
      ``monitor_training_cost``:是否监视训练集代价函数变化
      ``monitor_training_accuracy``:是否监视验证集准确率变化
      """
      if evaluation_data: n_data = len(evaluation_data)
      n = len(training_data)
      evaluation_cost, evaluation_accuracy = [], []
      training_cost, training_accuracy = [], []
      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, lmbda, len(training_data))
      
          print("步数 %d 已完成" % j)
      
          if monitor_training_cost:
              cost = self.total_cost(training_data, lmbda)
              training_cost.append(cost)
              print ("训练集代价函数: %f" % (cost))
          if monitor_training_accuracy:
              accuracy = self.accuracy(training_data, convert=True)
              training_accuracy.append(accuracy)
              print ("训练集精确率: %d / %d = %f" % (
                  accuracy, n, accuracy/n))
          if monitor_evaluation_cost:
              cost = self.total_cost(evaluation_data, lmbda, convert=True)
              evaluation_cost.append(cost)
              print ("验证集代价函数: %f" % (cost))
          if monitor_evaluation_accuracy:
              accuracy = self.accuracy(evaluation_data)
              evaluation_accuracy.append(accuracy)
              print ("验证集精确率: %d / %d = %f" % (
                  accuracy, n_data, accuracy/n_data))
      
      def accuracy(self, data, convert=False):
      """计算针对数据data模型输出的正确的数目
      ``data``:
          是一个(x,y)元组的列表,代表着一个批次的数据和标签
      ``convert``:
          True表示数据的标签y是向量化的(训练集)
          False表示数据的标签y是标量为0~m(评估集和测试集)
      """
      if convert:
          results = [(np.argmax(self.feedforward(x)), np.argmax(y))
                     for (x, y) in data]
      else:
          results = [(np.argmax(self.feedforward(x)), y)
                      for (x, y) in data]
      return sum(int(x == y) for (x, y) in results)
      
      def total_cost(self, data, lmbda, convert=False):
      """计算针对数据data模型的代价函数值
      ``data``:
          是一个(x,y)元组的列表,代表着一个批次的数据和标签
      ``lmbda``:正规化参数
      ``convert``:
          True表示数据的标签y是向量化的(训练集)
          False表示数据的标签y是标量为0~m(评估集和测试集)
      """
      cost = 0.0
      for x, y in data:
          a = self.feedforward(x)
          if convert: y = vectorized_result(y,self.sizes[-1])
          cost += self.cost.fn(a, y)/len(data)
      cost += 0.5*(lmbda/len(data))*sum(
          np.linalg.norm(w)**2 for w in self.weights)
      return cost

4、权重初始化

(1)标准正太分布初始化问题

因为输入像素值取值为[0,1],若使用标准正太吩咐初始化权重,那么带权输入的分布将是非常宽的正太分布。带入s型激活函数,权重输出将近似于0或1,表示隐藏神经元会饱和,此时改变权重,代价函数将改变很小,造成学习缓慢问题

(2)解决方法

使用均值为0,标准差为\(1 / \sqrt{n_{\rm in}}\)的正太分布

(3)优化代码

程序结构修改:

  • 将权重初始化从构造函数中,提取成为成员函数:

    • 在Network中新建权重初始化方法:
      • default_weight_initializer(self):新的权重初始化
      • large_weight_initializer(self):原来的权重初始化
    • 删除__init__(self, sizes, cost=CrossEntropyCost)中的权重和偏置初始化代码,改为调用default_weight_initializer()

      def __init__(self, sizes, cost=CrossEntropyCost):
      """
      列表 sizes 包含各层神经元的数量。
      例如,如果我们想创建⼀个在第⼀层有2个神经元,
      第⼆层有3个神经元,最后层有1个神经元的 Network 对象,
      我们应这样写代码:net = Network([2, 3, 1])
      cost:表示代价函数类型
      """
      self.num_layers = len(sizes) #成员变量,代表神经网络的层数
      self.sizes = sizes #成员变量,代表神经网络的尺寸
      self.default_weight_initializer()
      self.cost=cost
      
      def default_weight_initializer(self):
      """使用均值为0,标准差为1/sqrt(直接输入个数)初始化权重"""
      self.biases = [np.random.randn(y, 1) for y in self.sizes[1:]]
      self.weights = [np.random.randn(y, x)/np.sqrt(x)
                      for x, y in zip(self.sizes[:-1], self.sizes[1:])]
      
      def large_weight_initializer(self):
      """使用均值为0标准差为1的随机数初始化权重偏置。
      注意,第一层为输入层,按照惯例不会有权重偏差。
      此方法,仅用作比较,应由以上方法替代
      """
      self.biases = [np.random.randn(y, 1) for y in self.sizes[1:]]
      self.weights = [np.random.randn(y, x)
                      for x, y in zip(self.sizes[:-1], self.sizes[1:])]

5、添加神经网络的保存与修改

程序结构修改:

  • 导入json、sys库
  • 在Network类中添加方法
    • save(self, filename):序列化对象,并保存到文件中
  • 全局方法

    • load(filename):将对象从文件中加载

      def save(self, filename):
      """保存神经网络,定制化保存到filename文件中"""
      data = {"sizes": self.sizes,
              "weights":[w.tolist() for w in self.weights],
              "biases":[b.tolist for b in self.biases],
              "cost":str(self.cost.__name__)}
      f = open(filename,"w")
      json.dump(data,f)
      f.close()
      
      #### 加载神经网络
      def load(filename):
      """从filename文件中加载神经网络,返回一个Network实例"""
      f = open(filename,"r")
      data = json.load(f)
      f.close()
      cost = getattr(sys.modules[__name__],data["cost"])
      net = Network(data["sizes"], cost=cost)
      net.weights = [np.array(w) for w in data["weights"]]
      net.biases = [np.array(b) for b in data["biases"]]
      return net

6、如何选择神经⽹络的超参数

(1)如何设置学习速率η(常量)

  • 首先选中一个阈值:在训练集上代价函数值立即开始下降,而不是震荡或者增加
  • 从η=0.01开始试验
    • 如果代价在训练的前面若干回合开始下降,逐步增加η的值,直到开始震荡或者增加
    • 如果代价在训练的前面若干回合开始震荡或者增加,逐步减小η的值,直到开始下降
  • η 实际值不应该比阈值大,应该比阈值小一些,如阈值一半

(2)使用提前停止确定迭代期数目

如果分类准确度在近 10 个回合都没有提升的时候,我们将其终止。当然也可以选定20,30等

(4)学习速率自动调整

  • 设定一个初始值
  • 当验证集准确性开始变差时,按照某常量降低学习速率,如10或2
  • 直到变为学习率的1/1024或者1/1000,此时中止

可变学习速率虽然可以提升性能,但是也会产生大量可能的选择,开始试验,应使用固定的学习速率

(5)小批量数据大小

(6)自动设置超参数技术

通常的技术就是网格搜索(grid search)

7、其他优化技术