# 深度学习之《深度学习入门》学习笔记（四）神经网络的学习

> 作者：zning（https://ning.ae） · 发布：2019/10/04 10:52 上午 (UTC+8) · 原文：https://zning.me/15691207315042.html

最近学习吴恩达《Machine Learning》课程以及《深度学习入门：基于Python的理论与实现》书，一些东西总结了下。现就后者学习进行笔记总结。本文是本书的学习笔记（四）神经网络的学习。

* [深度学习之《深度学习入门》学习笔记（一）Python入门](./15656172902685.html)

* [深度学习之《深度学习入门》学习笔记（二）感知机](./15656174993229.html)

* [深度学习之《深度学习入门》学习笔记（三）神经网络](./15656175642754.html)

* [深度学习之《深度学习入门》学习笔记（四）神经网络的学习](./15691207315042.html)

本章标题所说的“学习”是指从训练数据中自动获取最优权重参数的过程。学习的目的就是以损失函数为基准，找出能使它的值达到最小的权重参数。

## 从数据中学习

神经网络的特征就是可以从数据中学习。**所谓“从数据中学习”，是指可以由数据自动决定权重参数的值**。

数据是机器学习的命根子。数据是机器学习的核心。这种数据驱动的方法，也可以说脱离了过往以人为中心的方法。

而机器学习的方法是极力避免人为介入的，尝试从收集到的数据中发现答案（模式）。**神经网络或深度学习则比以往的机器学习方法更能避免人为介入**。

例如手写数字识别，考虑通过有效利用数据来解决这个问题：先从图像中提取特征量，再用机器学习技术学习这些特征量的模式。“特征量”是指可以从输入数据（输入图像）中准确地提取本质数据（重要的数据）的转换器。图像的特征量通常表示为向量的形式。在计算机视觉领域，常用的特征量包括SIFT、SURF和HOG等。使用这些特征量将图像数据转换为向量，然后对转换后的向量使用机器学习中的SVM、KNN等分类器进行学习。

机器学习的方法中，由机器从收集到的数据中找出规律性。但是，**将图像转换为向量时使用的特征量仍是由人来设计的**。即使使用特征量和机器学习的方法，也需要针对不同的问题人工考虑合适的特征量。

> 深度学习有时也称为端到端机器学习。

神经网络的优点是对所有的问题都可以用同意的流程来解决。神经网络都是通过不断地学习所提供的数据，尝试发现带求解问题的模式。也就是说，与待处理的问题无关，神经网络可以 将数据直接作为原始数据，进行“端对端”的学习。

机器学习中，一般将数据分为**训练数据**和**测试数据**两部分来进行学习和实验等。未来正确评价模型的**泛化能力**，就必须划分训练数据和测试数据，训练数据也可以成为监督数据。

泛化能力是指处理未被观察过的数据（不包含在训练数据中的数据）的能力。获得泛化能力是机器学习的最终目标。

只对某个数据集过度拟合的状态称为**过拟合（over fitting）**。避免过拟合也是机器学习的一个重要课题。

## 损失函数

神经网络以某个指标为线索寻找最优权重参数。神经网络的学习中所用的指标称为**损失函数**。这个损失函数可以使用任意函数，但**一般用均方误差和交叉熵误差**等。损失函数是表示神经网络性能的“恶劣程度”或者“性能有多好”的指标，即当前的神经网络对监督数据在多大程度上不拟合，在多大程度上不一致。

### 均方误差

均方误差（mean squared error）由下式表示:

$$E = \frac{1}{2} \sum_{k}^{}(y_{k}-t_{k})^2$$

$y_{k}$表示神经网络的输出，$t_{k}$表示监督数据，$k$表示数据的维度。其中，$t$表示监督数据，将正确的解标签设为1，其他均设为0，**其他标签表示为0的表示方法称为one-hot表示**。

均方误差会计算神经网络的输出和正确解监督数据的各个元素之差的平方，再求总和。以下是代码实现：

```python
# 均方误差定义函数
def mean_squared_error(y, t):
    return 0.5 * np.sum((y-t)**2)

# 设“2”为正解
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]

#例1：“2”的概率最高的情况（0.6）
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
mean_squared_error(np.array(y),np.array(t))
```

>    0.09750000000000003

```python
# 例2：“7”的概率最高的情况（0.6）
y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]

mean_squared_error(np.array(y),np.array(t))
```

>    0.5975

很明显，均方误差显示第一个例子的输出结果与监督数据更加吻合。

### 交叉熵误差

交叉熵误差（cross entropy error）由下式表示：

$$E=-\sum_{k}^{} t_{k} ln y_{k}$$

$y_{k}$是神经网络的输出，$t_{k}$是正确解标签。并且，中只有正确解标签的索引为1，其他均为0（one-hot表示）。交叉熵误差的值是由正确解标签所对应的输出结果决定的。

正确解标签对应的输出越大，上式的值越接近0；当输出为1时，交叉熵误差为0。此外，如果正确解标签对应的输出较小，则上式的值较大。以下是代码实现：

```python
# 实现交叉熵误差
def cross_entropy_error0(y, t):
    delta = 1e-7
    return -np.sum(t * np.log(y + delta))
```

说明一下，函数内部在计算`np.log`时，加上了一个微小值delta。这是因为当出现`np.log(0)`时，`np.log(0)`会变为负无穷大的-inf，导致计算无法进行，所以作为保护性对策增加微小值。

```python
# 进行简单计算
# 设“2”为正解
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
#例1：“2”的概率最高的情况（0.6）
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
cross_entropy_error0(np.array(y),np.array(t))
```

>    0.510825457099338

```python
# 例2：“7”的概率最高的情况（0.6）
y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
cross_entropy_error0(np.array(y),np.array(t))
```

>    2.302584092994546

上例结果可以看出与前文讨论是一致的。

### mini-batch学习

机器学习使用训练数据进行学习，严格来说，就是针对训练数据计算损失函数的值，找出使该值尽可能小的参数。因此，**计算损失函数时必须将所有的训练数据作为对象**。

以交叉熵误差为例，如果要求所有训练数据的损失函数的总和，可用如下式子：

$$E=-\frac{1}{N} \sum_{n}^{} \sum_{k}^{} t_{nk} ln y_{nk}$$

上式假设数据有$N$个，$t_{nk}$表示第$n$个数据的第$k$个元素的值（$y_{nk}$是神经网络的输出，$t_{nk}$是监督数据）。本式只是把求单个数据的损失函数的式子扩大到了$N$份数据。最后除以$N$进行正规化（归一化？），并求出单个数据的“平均损失函数”。

我们可以知道，许多数据分析是很大的数据量，这种情况不可能以全部数据为对象计算损失函数。因此，从全部数据选出一部分，作为全部数据的“近似”。神经网络的学习也是从训练数据中选出一批数据（称为mini-batch，小批量），然后对每个mini-batch进行学习。这种学习方式称为**mini-batch学习**。撰写读入MNIST数据集的代码：

```python
# 读入MNIST 代码暂略
import sys, os
sys.path.append(os.pardir)
import numpy as np

# from dataset.mnist import load_mnist

# 使用np.random.choice进行随机选取
np.random.choice(60000, 10)
```

>    array([ 5332, 11993, 16553, 47954, 31537,   4750, 52005,  1159,  6775,
>            46043])

实现一个可以同时处理单个数据和批量数据的交叉熵误差函数：

```python
# 可同时处理单个和批量数据
def cross_entropy_error1(y, t):
    if y.nidm == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)

    delta = 1e-7
    batch_size = y.shape[0]
    return -np.sum(t * np.log(y + delta)) / batch_size
```

当监督数据是标签形式（非ont-hot表示，而是像“2”、“7”这样的具体标签）时，交叉熵误差可以通过如下代码实现：

```python
# 可同时处理单个和批量数据
def cross_entropy_error2(y, t):
    if y.nidm == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)

    delta = 1e-7
    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size), t] + delta)) / batch_size
```

由于one-hot表示中$t$为0的元素的交叉熵误差也为0，因此针对这些元素的计算可以忽略。换言之，如果可以获得神经网络在正确解标签处的输出，就可以计算交叉熵误差。因此，$t$为one-hot表示时通过 `t * np.log(y)`计算的地方，在$t$为标签形式时，可用`np.log(y[np.arange(batch_size), t])`实现相同的处理。

介绍下`np.log(y[np.arange(batch_size), t])`：`np.arange(batch_size`会生产一个从0到batch_size-1的数组。因为t中标签是以`[2, 7, 0, 9, 4]`的形式存储的，所以`y[np.arange(batch_size), t`能抽出各个数据的正确解标签对应的神经网络的输出。

### 为什么要设定损失函数

Q: 为什么要导入损失函数？既然我们的目标是获得识别精度尽可能高的神经网络，那不是应该把识别精度作为指标吗？

A: 在神经网络的学习中，寻找最优参数（权重和偏置）时，要寻找使损失函数的值尽可能小的参数。为了找到使损失函数的值尽可能小的地方，需要计算的参数的导数（确切的讲是梯度），然后以这个导数为指引，逐步更新参数的值。而对权重参数的损失函数求导，表示的是“如果稍微改变这个权重参数的值，损失函数的值会如何变化”。**如果导数的值为负**如，通过使该权重参数向正方向改变，可以减小损失函数的值；反过来，**如果导数的值为正**如，则通过使该权重参数向负方向改变，可以减小损失函数的值。**当导数的值为0时**如，无论权重参数向哪个方向变化，损失函数的值都不会改变，此时该权重参数的更新会停在此处。

总结一下：**在进行神经网络的学习时，不能将识别精度作为指标。因为如果以识别精度为指标，则参数的导数在绝大多数地方都会变成0**。

可以说，识别精度对微小的参数变化基本上没有什么反应，即便有反应，它的值也是不连续地、突然地变化。作为激活函数的阶跃函数也有同样的情况。出于相同的原因，如果使用阶跃函数作为激活函数，神经网络的学习将无法进行。众所周知，阶跃函数的斜率在绝大多数地方都为0，而sigmoid函数的斜率（切线）在任何地方都不为0。

## 数值微分

梯度法使用梯度的信息决定前进的方向。

### 导数

导数是某个瞬间的变化量。

Python中的舍入误差：

```python
np.float32(1e-50)
```

>    0.0

```python
# 对舍入误差减小与使用中心差分实现函数导数程序
def numerical_diff(f, x):
    h = 1e-4 #0.0001
    return (f(x+h) - f(x-h)) / (2*h)
```

利用微小的差分求导数的过程称为数值微分（numerical differentiation）。而基于数学式的推倒求导数的过程，则用解析性（analytic）一词，称为解析性求解或者解析性求导。

### 数值微分的例子

```python
# 实现例子
def function_1(x):
    return 0.01*x**2 + 0.1*x

import numpy as np
import matplotlib.pylab as plt

x = np.arange(0.0, 20.0, 0.1) #以0.1为单位，从0到20的数组x
y = function_1(x)
plt.xlabel("x")
plt.ylabel("y")
plt.plot(x, y)
plt.show()
```

> ![output_118_0](media/15691207315042/output_118_0.png)

```python
#计算上面式子的5,10处导数
numerical_diff(function_1, 5)
```

>    0.1999999999990898

```python
numerical_diff(function_1, 10)
```

>    0.2999999999986347

```python
def tangent_line(f, x):
    d = numerical_diff(f, x)
    print(d)
    y = f(x) - d*x
    return lambda t: d*t + y
```

```python
x = np.arange(0.0, 20.0, 0.1)
y = function_1(x)
plt.xlabel("x")
plt.ylabel("f(x)")

tf = tangent_line(function_1, 5)
tf2 = tangent_line(function_1, 10)
y2 = tf(x)
y22 = tf2(x)

plt.plot(x, y)
plt.plot(x, y2)
plt.show()
plt.plot(x, y)
plt.plot(x, y22)
plt.show()
```

>    0.1999999999990898
>    0.2999999999986347

> ![output_122_1](media/15691207315042/output_122_1.png)

> ![output_122_2](media/15691207315042/output_122_2.png)

### 偏导数

以下代码实现函数$f(x_{0},x_{1}) = x^2_{0} + x^2_{1}$：

```python
# 实现上式的代码
def function_2(x):
    #或者return np.sum(x**2)
    return x[0]**2 + x[1]**2
```

```python
# 求偏导1
def function_tmp1(x0):
    return x0*x0 + 4.0**2.0

numerical_diff(function_tmp1, 3.0)
```

>    6.00000000000378

```python
# 求偏导2
def function_tmp2(x1):
    return 3.0**2.0 + x1*x1

numerical_diff(function_tmp1, 4.0)
```

>    7.999999999999119

如上两式，偏导数和单变量导数一样，都是求某个地方的斜率。不过，偏导数需要将多个变量中的某一个变量定位目标变量，并将其他变量固定位某个值。

## 梯度

像$(\frac{\partial f}{\partial x_{0}},\frac{\partial f}{\partial x_{1}})$这样的由全部变量的偏导数汇总而成的向量称为梯度（gradient）。

```python
# 梯度的代码实现
def numerical_gradient(f, x):
    h = 1e-4 #0.001
    grad = np.zeros_like(x) #生成和x形状相同的数组
    
    for idx in range(x.size):
        tmp_val = x[idx]
        # f(x+h)的计算
        x[idx] = tmp_val + h
        fxh1 = f(x)
        
        # f(x-h)的计算
        x[idx] = tmp_val - h
        fxh2 = f(x)
        
        grad[idx] = (fxh1 - fxh2) / (2*h)
        x[idx] = tmp_val # 还原值
        
    return grad
```

上面的函数的实现看上去有些复杂，但它执行的处理和求单变量的数值微分基本没有区别。

```python
# 求点(3,4) (0,2) (3,0)处的梯度
numerical_gradient(function_2, np.array([3.0, 4.0]))
```

>    array([6., 8.])

```python
numerical_gradient(function_2, np.array([0.0, 2.0]))
```

>    array([0., 4.])

```python
numerical_gradient(function_2, np.array([3.0, 0.0]))
```

>    array([6., 0.])

```python
import numpy as np
import matplotlib.pylab as plt
from mpl_toolkits.mplot3d import Axes3D

def _numerical_gradient_no_batch(f, x):
    h = 1e-4 # 0.0001
    grad = np.zeros_like(x)
    
    for idx in range(x.size):
        tmp_val = x[idx]
        x[idx] = float(tmp_val) + h
        fxh1 = f(x) # f(x+h)
        
        x[idx] = tmp_val - h 
        fxh2 = f(x) # f(x-h)
        grad[idx] = (fxh1 - fxh2) / (2*h)
        
        x[idx] = tmp_val # 还原值
        
    return grad

def numerical_gradient(f, X):
    if X.ndim == 1:
        return _numerical_gradient_no_batch(f, X)
    else:
        grad = np.zeros_like(X)
        
        for idx, x in enumerate(X):
            grad[idx] = _numerical_gradient_no_batch(f, x)
        
        return grad

def function_2(x):
    if x.ndim == 1:
        return np.sum(x**2)
    else:
        return np.sum(x**2, axis=1)

def tangent_line(f, x):
    d = numerical_gradient(f, x)
    print(d)
    y = f(x) - d*x
    return lambda t: d*t + y
     
if __name__ == '__main__':
    x0 = np.arange(-2, 2.5, 0.25)
    x1 = np.arange(-2, 2.5, 0.25)
    X, Y = np.meshgrid(x0, x1)
    
    X = X.flatten()
    Y = Y.flatten()
    
    grad = numerical_gradient(function_2, np.array([X, Y]) )
    
    plt.figure()
    plt.quiver(X, Y, -grad[0], -grad[1],  angles="xy",color="#666666")#,headwidth=10,scale=40,color="#444444")
    plt.xlim([-2, 2])
    plt.ylim([-2, 2])
    plt.xlabel('x0')
    plt.ylabel('x1')
    plt.grid()
    plt.legend()
    plt.draw()
    plt.show()
```

> ![output_133_0](media/15691207315042/output_133_0.png)

我们发现离最低处越远，箭头越大。梯度会指向各点处的函数值降低的方向，更严格的讲，梯度指示的方向是各点处的函数值减小最多的方向。

### 梯度法

神经网络需在学习时找到最优参数（权重和偏置），这里所说的最优参数是指损失函数取最小值时的参数。通过巧妙地使用梯度来寻找函数最小值（或者尽可能小的值）的方法就是梯度法。

梯度表示的是各点处的函数值减小最多的方向。实际上，在复杂的函数中，梯度指示的方向基本上都不是函数值的最小处。

> 函数的极小值、最小值以及被称为**鞍点（saddle point）**的地方，梯度为0。极小值是局部最小值，也就是限定在某个范围内的最小值。鞍点是从某个方向上看是极大值，从另一个方向上看则是极小值的点。

> 当函数很复杂且呈扁平状时，学习可能会进入一个（几乎）平坦的地区，陷入被称为“学习高原”的无法前进的停滞期。

在寻找函数的最小值（或者尽可能小的值）的位置的任务中，要以梯度的信息为线索，决定前进的方向。

在梯度法中，函数的取值从当前位置沿着梯度方向前进一定距离，然后在新的地方重新求梯度，再沿着新梯度方向前进，如此反复，不断地沿梯度方向前进。像这样，**通过不断地沿梯度方向前进，逐渐减小函数值的过程就是梯度法（gradient method）**。

严格的讲，寻找最小值的梯度法称为**梯度下降法（gradient descent method）**，寻找最大值的梯度法称为**梯度上升法（gradient ascent method）**。

用数学式表示梯度法：

$$x_{0}=x_{0}-\eta \frac{\partial f}{\partial x_{0}}$$

$$x_{1}=x_{1}-\eta \frac{\partial f}{\partial x_{1}}$$

上式的$\eta$表示更新量，在神经网络的学习中，称为**学习率（learning rate）**。学习率决定在一次学习中，应该学习多少，以及在多大程度上更新参数。上式表示更新一次的式子，这个步骤会反复执行。

像学习率这样的参数成为**超参数**。学习率这样的超参数是人工设定的。一般来说，超参数需要尝试多个值，以便找到一种可以使学习顺利进行的设定。

而学习率需要事先确定为某个值，比如0.01或者0.001.一般而言，这个值过大或过小，都无法抵达一个“好的位置”。在神经网络的学习中，一般会一边改变学习率的值，一遍确认学习是否正确进行了。下面用python实现梯度下降法：

```python
def gradient_descent(f, init_x, lr=0.01, step_num=100):
    """
    参数f是要进行最优化的函数，init_x是初始值，lr是学习率learning rate，step_num是梯度法的重复次数。
    numerical_gradient(f,x)会求函数的梯度，用该梯度乘以学习率得到的值进行更新操作，由step_num指定重复的次数。
    """
    x = init_x
    x_history = []

    for i in range(step_num):
        x_history.append( x.copy() )

        grad = numerical_gradient(f, x)
        x -= lr * grad

    return x, np.array(x_history)
```

```python
# 用梯度法求f(x0+x1)=x0^2+x1^2的最小值
def function_2(x):
    return x[0]**2 + x[1]**2

init_x = np.array([-3.0, 4.0])
x, x_history = gradient_descent(function_2, init_x=init_x, lr=0.1, step_num=100)
```

```python
# 用图像表示上面的函数梯度下降法的步骤
plt.plot( [-5, 5], [0,0], '--b')
plt.plot( [0,0], [-5, 5], '--b')
plt.plot(x_history[:,0], x_history[:,1], 'o')

plt.xlim(-3.5, 3.5)
plt.ylim(-4.5, 4.5)
plt.xlabel("X0")
plt.ylabel("X1")
plt.show()
```

> ![output_138_0](media/15691207315042/output_138_0.png)

学习率过大或者过小都无法得到好的结果。

```python
# 学习率过大的例子：lr=10.0
init_x = np.array([-3.0, 4.0])
x, x_history = gradient_descent(function_2, init_x=init_x, lr=10.0, step_num=100)
x
```

>    array([-2.58983747e+13, -1.29524862e+12])

```python
# 学习率过小的例子：lr=1e-10
init_x = np.array([-3.0, 4.0])
x, x_history = gradient_descent(function_2, init_x=init_x, lr=1e-10, step_num=100)
x
```

>    array([-2.99999994,  3.99999992])

上面实验可以看出，学习率过大的话，会发散成一个很大的值；反过来，学习率过小的话，基本上没怎么更新就结束了。也就是说，设定合适的学习率是一个很重要的问题。

### 神经网络的梯度

```python
# 一个简单的神经网络
# coding: utf-8
import sys, os
sys.path.append('../input/deeplearningfromscratch/deeplearningfromscratch')  # 为了导入父目录中的文件而进行的设定
import numpy as np
from common.functions import softmax, cross_entropy_error
from common.gradient import numerical_gradient

class simpleNet:
    def __init__(self):
        self.W = np.random.randn(2,3)

    def predict(self, x):
        return np.dot(x, self.W)

    def loss(self, x, t):
        z = self.predict(x)
        y = softmax(z)
        loss = cross_entropy_error(y, t)

        return loss
```

```python
net = simpleNet()
print(net.W)

x = np.array([0.6, 0.9])
p = net.predict(x)
print(p)

print(np.argmax(p))

t = np.array([0, 0, 1]) # 正确解标签

f = lambda w: net.loss(x, t)
dW = numerical_gradient(f, net.W)

print(dW)
```

>    [[ 0.61060921  0.39278321 -0.19230499]
>     [-0.57160886  0.59945886 -0.26529107]]
>    [-0.14808245  0.7751829  -0.35414496]
>    1
>    [[ 0.13852712  0.34874166 -0.48726878]
>     [ 0.20779067  0.5231125  -0.73090317]]

## 学习算法的实现

神经网络的学习步骤：

**前提：**神经网络存在合适的权重和偏置，调整权重和偏置以便拟合训练数据的过程称为“学习”。

1. **mini-batch**：从训练数据中随机选出一部分数据，这部分数据称为mini-batch。我们的目标是减小mini-batch的损失函数的值。
2. **计算梯度**：为了减小mini-batch的损失函数的值，需要求出各个权重参数的梯度。梯度表示损失函数的值减小最多的方向。
3. **更新参数**：将权重参数沿梯度方向进行微小更新。
4. **重复**：重复第1、2、3步。

这里使用的数据是随机选择的mini batch数据，所以又被称为**随机梯度下降法（stochastic gradient descent）**。这里的随机指的是随机选择的意思。

随机梯度下降法是“对随机选择的数据进行的梯度下降法”。深度学习的很多框架中，随机梯度下降法一般由一个名为**SGD**的函数来实现。SGD来源于随机梯度下降法的英文名称的首字母。

### 2层神经网络的类

```python
from common.functions import *
from common.gradient import numerical_gradient

class TwoLayerNet:

    def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
        # 初始化权重
        self.params = {}
        self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
        self.params['b1'] = np.zeros(hidden_size)
        self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
        self.params['b2'] = np.zeros(output_size)

    def predict(self, x):
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']
    
        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1)
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2)
        
        return y
        
    # x:输入数据, t:监督数据
    def loss(self, x, t):
        y = self.predict(x)
        
        return cross_entropy_error(y, t)
    
    def accuracy(self, x, t):
        y = self.predict(x)
        y = np.argmax(y, axis=1)
        t = np.argmax(t, axis=1)
        
        accuracy = np.sum(y == t) / float(x.shape[0])
        return accuracy
        
    # x:输入数据, t:监督数据
    def numerical_gradient(self, x, t):
        loss_W = lambda W: self.loss(x, t)
        
        grads = {}
        grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
        grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
        grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
        grads['b2'] = numerical_gradient(loss_W, self.params['b2'])
        
        return grads
        
    def gradient(self, x, t):
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']
        grads = {}
        
        batch_num = x.shape[0]
        
        # forward
        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1)
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2)
        
        # backward
        dy = (y - t) / batch_num
        grads['W2'] = np.dot(z1.T, dy)
        grads['b2'] = np.sum(dy, axis=0)
        
        da1 = np.dot(dy, W2.T)
        dz1 = sigmoid_grad(a1) * da1
        grads['W1'] = np.dot(x.T, dz1)
        grads['b1'] = np.sum(dz1, axis=0)

        return grads
```

```python
# 二层神经网络例子
net = TwoLayerNet(input_size = 784, hidden_size = 100, output_size = 10)
print(net.params['W1'].shape)
print(net.params['b1'].shape)
print(net.params['W2'].shape)
print(net.params['b2'].shape)
```

>    (784, 100)
>    (100,)
>    (100, 10)
>    (10,)

```python
#推理处理的实现如下
x = np.random.rand(100, 784) # 伪输入数据100笔
y = net.predict(x)
t = np.random.rand(100, 10) # 伪正确解标签10笔

# grads = net.numerical_gradient(x, t) # 计算梯度，使用传统的基于数值微分计算参数的梯度
grads = net.gradient(x, t) # 计算梯度，使用误差反向传播算法

print(grads['W1'].shape)
print(grads['b1'].shape)
print(grads['W2'].shape)
print(grads['b2'].shape)
```

>    (784, 100)
>    (100,)
>    (100, 10)
>    (10,)

解析`TwoLayerNet`的实现：

1. `__init__(self, input_size, hidden_size, output_size)`：类的初始化方法，参数依次表示输入层的神经元数、隐藏层的神经元数、输出层的神经元数。

    因为进行手写数字识别时，输入图像的大小是784(28x28)，输出为10个类别，所以指定参数`input_size = 784, output_size = 10`，将隐藏层的个数`, hidden_size`设置为一个合适的值即可（例如这里是100）。

2. `predict(self, x)、accuracy(self, x, t)`同上一章神经网络的推理处理基本一致。

3. `loss(self, x, t)`是计算损失函数值的方法。这个方法会基于`predict()`的结果和正确解标签，计算交叉熵误差。

4. `numerical_gradient(self, x, t)`是计算各个参数的梯度，而`gradient(self, x, t)`是使用误差反向传播法高效计算梯度的方法。

    `numerical_gradient(self, x, t)`是基于数值微分计算参数的梯度。而`gradient(self, x, t)`是使用误差反向传播法高速计算梯度，其求到的梯度和数值微分的结果基本一致，且速度比前者快。

如何设置权重参数的初始值是关系到神经网络是否成功学习的重要问题。权重使用符合高斯分布的随机数进行初始化，偏置使用0进行初始化。

### mini-batch的实现

所谓mini-batch的学习，就是从训练数据中随机选择一部分数据，再以这些数据为对象(mini-batch)，使用梯度法更新参数的过程。

```python
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
# from two_layer_net import TwoLayerNet

# 读入数据
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)

network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

# 超参数
iters_num = 10000  # 适当设定循环的次数
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1

train_loss_list = []
train_acc_list = []
test_acc_list = []

# 平均每个epoch的重复次数
iter_per_epoch = max(train_size / batch_size, 1)

for i in range(iters_num):
    # 获取mini-batch
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]
    
    # 计算梯度
    #grad = network.numerical_gradient(x_batch, t_batch)
    grad = network.gradient(x_batch, t_batch)
    
    # 更新参数
    for key in ('W1', 'b1', 'W2', 'b2'):
        network.params[key] -= learning_rate * grad[key]
    
    # 记录学习过程
    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)
    
    # 计算每个epoch的识别精度
    if i % iter_per_epoch == 0:
        train_acc = network.accuracy(x_train, t_train)
        test_acc = network.accuracy(x_test, t_test)
        train_acc_list.append(train_acc)
        test_acc_list.append(test_acc)
        print("train acc, test acc | " + str(train_acc) + ", " + str(test_acc))

# 绘制图形
markers = {'train': 'o', 'test': 's'}
x = np.arange(len(train_acc_list))
plt.plot(x, train_acc_list, label='train acc')
plt.plot(x, test_acc_list, label='test acc', linestyle='--')
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()
```

> train acc, test acc | 0.10441666666666667, 0.1028
> train acc, test acc | 0.7994333333333333, 0.8037
> train acc, test acc | 0.8809833333333333, 0.8833
> train acc, test acc | 0.9003666666666666, 0.9024
> train acc, test acc | 0.9098833333333334, 0.9125
> train acc, test acc | 0.9164, 0.9178
> train acc, test acc | 0.9214, 0.9244
> train acc, test acc | 0.92525, 0.9277
> train acc, test acc | 0.92875, 0.9321
> train acc, test acc | 0.9321166666666667, 0.9337
> train acc, test acc | 0.9348333333333333, 0.9358
> train acc, test acc | 0.93755, 0.9384
> train acc, test acc | 0.9400833333333334, 0.9407
> train acc, test acc | 0.9424166666666667, 0.9422
> train acc, test acc | 0.9445666666666667, 0.9431
> train acc, test acc | 0.9466666666666667, 0.9459
> train acc, test acc | 0.9480833333333333, 0.9479

> ![output_151_1](media/15691207315042/output_151_1.png)

随着学习的进行，损失函数的值在不断减小。这是学习正常进行的信号，表示神经网络的权重参数在逐渐拟合数据。通过反复地向它浇灌（输入）数据，神经网络整在逐渐向最优参数靠近。

不过损失函数的值，严格的讲是“对训练数据的某个mini-batch的损失函数”的值。训练数据的损失函数值减小的结果是不能说明该神经网络在其他数据集上也一定能有同等程度的表现。

神经网络的学习中，必须确认是否能够知确实别数据意外的其他数据，即虽然训练数据中的内容能够被正确识别，但是不在训练数据的内容却无法被识别，这种现象为**过拟合**。

**epoch**是一个单位，一个epoch表示学习中所有训练数据均被使用过一次时的更新次数。
