深度学习入门:基于python的理论与实践

第3章 神经网络

对于计算机进行的复杂处理,感知机(理论上也可以将其表示出来)。但设定权重的工作,即确定合适的、能符合预期的输入与输出的权重,现在还是由人工进行的。神经网络的出现就是为了解决这个问题,具体来讲,神经网络的一个重要性质是它可以自动地从数据中学习到合适的权重参数

3.1 从感知机到神经网络

①神经网络例子:

图中的网络一共由三层神经元构成,但实质上只有2层神经元有权重,因此将其称为“2层网络”。

有的书也会根据狗曾网络的层数,把图中的网络称为“3层网络”。本书将根据实质上拥有权重的层数(输入层、隐藏层、输出层的总数减去1后的数量)来表示网络的名称。

②感知机的例子:

3.2 激活函数

其中h(x)函数会将输入信号的综合转换为输出信号,这种函数一般称为激活函数,激活函数的作用在于决定如何来激活输入信号的总和。激活函数是连接感知机和神经网络的桥梁。

一般而言,“朴素感知机”是指单层网络,指的是激活函数使用了阶跃函数的模型。“多层感知机”指的是神经网络,即使用sigmoid函数等平滑的激活函数的多层网络。

3.2.1 阶跃函数

阶跃函数是指一旦输入超过阈值,就切换输出的函数。

阶跃函数的实现:

法一:实现简单,但参数x只接受实数(浮点数)。 也就是说,允许形如step_function(3.0)的调用, 但不允许参数取NumPy数组,例如step_function(np.array([1.0,2.0]))。

def step_function(x):
    if x>0:
        return 1
    else:
        return 0

 法二:支持NumPy数组

def step_function_01(x):
    # x=np.array([-1.0,1.0,2.0])
    # y=x>0
    # y=array([false,true,true],dtype=bool)
    y=x>0
    # astype()方法:转换NumPy数组类型,通过参数指定期望获得的类型
    # true转换为1,false转换为0
    return y.astype(np.int)

 使用matplotlib库展示阶跃函数的图形

import numpy as np
import matplotlib.pylab as plt


def step_function_00(x):
    return np.array(x>0,dtype=np.int)

x=np.arange(-5.0,5.0,0.1)
y=step_function_00(x)
plt.plot(x,y)
plt.ylim(-0.1,1.1)
plt.show()

3.2.2 sigmoid函数

神经网络中经常使用的一个激活函数就是下式的sigmoid函数:

sigmoid函数的实现: 

def sigmoid(x):
    return 1/(1+np.exp(-x))

sigmoid函数可以支持numpy数组的原因在于numpy的广播功能。如果在标量和numpy数组之间进行运算,则标量会和numpy数组的各个元素进行运算。

 使用matplotlib库展示sigmoid函数的图形

import numpy as np
import matplotlib.pylab as plt

# sigmoid函数的实现
def sigmoid(x):
    return 1/(1+np.exp(-x))

x=np.arange(-5.0,5.0,0.1)
y=sigmoid(x)
plt.plot(x,y)
plt.ylim(-0.1,1.1)
plt.show()

 3.2.3 阶跃函数和sigmoid函数的图像比较

神经网络的激活函数必须使用非线性函数,不能使用线性函数。

3.2.4 ReLU函数

在神经网络发展的历史上,sigmoid函数很早就开始被使用了,而最近则主要使用ReLU(Rectified Linear Unit)函数。

ReLU函数在输入大于0时,直接输出该值,在输入小于等于0时,输出0

relu函数的实现
def relu(x):
    return np.maximum(0,x)

这里使用了np.maximum()函数,会从输入的数值中选择较大的那个数值进行输出。

使用matplotlib库展示relu函数的图形 

import numpy as np
import matplotlib.pylab as plt

# relu函数的实现
def relu(x):
    return np.maximum(0,x)

x=np.arange(-5.0,5.0,0.1)
y=relu(x)
plt.plot(x,y)
plt.ylim(-0.1,1.1)
plt.show()

3.3多维数组的运算 

 

 神经网络的内积

C=np.dot(A,B)

3.4 三层神经网络的实现 

三层神经网络的代码实现

# 实现三层神经网络
import numpy as np


def init_network():
    """权重和偏置的初始化"""
    # network字典变量中保存了每一层所需的参数
    network={}
    network['W1']=np.array([[0.1,0.3,0.5],
                            [0.2,0.4,0.6]])
    network['b1']=np.array([0.1,0.2,0.3])
    network['W2'] = np.array([[0.1, 0.4],
                              [0.2, 0.5],
                              [0.3, 0.6]])
    network['b2'] = np.array([0.1, 0.2])
    network['W3'] = np.array([[0.1, 0.3],
                              [0.2, 0.4]])
    network['b3'] = np.array([0.1, 0.2])

    return network

def forward(network,x):
    """封装了将输入信号转换为输出信号的过程"""
    W1,W2,W3=network['W1'],network['W2'],network['W3']
    b1,b2,b3=network['b1'],network['b2'],network['b3']

    a1=np.dot(x,W1)+b1
    z1=sigmoid(a1)
    a2 = np.dot(z1, W2) + b2
    z2 = sigmoid(a2)
    a3 = np.dot(z2, W3) + b3
    y=identity_function(a3)
    return y

# sigmoid函数的实现
def sigmoid(x):
    return 1/(1+np.exp(-x))

# identity_function函数
def identity_function(x):
    return x


network=init_network()
x=np.array([1.0,0.5])
y=forward(network,x)
print(y)    #[0.31682708 0.69627909]

3.5 输出层的设计 

 输出层所用的激活函数,要根据求解问题的性质决定。一般地,回归问题可以使用恒等函数,二元分类问题可以使用sigmoid函数,多元分类问题可以使用softmax函数。

机器学习的问题大致可以分为分类问题和回归问题。分类问题是数据属于哪一个类别的问题。而回归问题是根据某个输入预测一个(连续的)数值的问题。比如,根据一个人的图像预测这个人的体重的问题就是回归问题(类似“57.4kg”这样的预测)。

3.5.1 恒等函数

恒等函数会将输入按原样输出,对于输入的信息,不加任何改动,直接输出。

 3.5.2 softmax函数

 采用softmax函数,计算第k个神经元的输出yk,公式如下:

softmax函数的输出通过箭头和所有的输入信号相连,这是因为,从上式中可以看出,输出层的各个神经元都受到输入信号的影响。 

实现softmax函数

def softmax(a):
    exp_a=np.exp(a)
    sum_exp_a=np.sum(exp_a)
    y=exp_a/sum_exp_a
    return y
a=np.array([0.3,2.9,4.0])
y=softmax(a)
print(y)    #[0.01821127 0.24519181 0.73659691]

注意事项:溢出问题。softmax函数的实现要进行指数函数的运算,但是此时指数函数的值很容易变得非常大,如果在这些超大值之间进行除法运算,结果会出现“不确定”的情况。

对softmax函数的实现进行改进:

这里的C‘ 可以使用任何值,但是为了防止溢出,一般会使用输入信号中的最大值的相反数。

实现改进后的softmax函数

def softmax(a):
    c=np.max(a)
    exp_a=np.exp(a-c)   #溢出对策
    sum_exp_a=np.sum(exp_a)
    y=exp_a/sum_exp_a
    return y
a=np.array([0.3,2.9,4.0])
y=softmax(a)
print(y)    #[0.01821127 0.24519181 0.73659691]

softmax函数的特征:输出总是0.0到1.0之间的实数。 输出值总和为1。

因此把softmax函数的输出解释为“概率”。

即便使用softmax函数,各个元素之间的大小关系也不会改变。这是因为指数函数(y=exp(x))是单调递增函数。一般而言,神经网络只把输出值最大的神经元所对应的类别作为识别结果。因此,神经网络在进行分类时,输出层的softmax函数可以省略。在实际问题中,由于指数函数的运算需要一定的计算机运算量,因此输出层的softmxa函数一般会被省略。

求解机器学习问题的步骤可以分为“学习”和“推理”两个阶段。首先在学习阶段进行模型的学习(训练),然后在推理阶段,用学到的模型对未知的数据进行推理(分类)。

输出层的神经元数量需要根据待解决的问题来决定。对于分类问题,输出层的神经元数量一般设定为类别的数量。

3.6 手写数字的识别

假设学习已全部结束,我们使用学习到的参数,先实现神经网络的“推理处理”(也称神经网络的前向传播)

3.6.1MINST数据集

MNIST手写数字图像集是机器学习领域最有名的数据集之一。MNIST数据集是由0到9的数字图像构成的。训练图像有6万张,测试图像有1万张,这些图像可以用于学习和推理。MNIST的图像数据是28像素X28像素的灰度图像(1通道),各个像素的取值在0到255之间。每个图像数据都相应标有“7”“2”“1”等标签。

# coding: utf-8
import sys, os

# 将父目录(上一级目录)添加到系统路径中,这样做的目的是确保可以导入父目录中的文件或模板
sys.path.append(os.pardir)  # 为了导入父目录的文件而进行的设定
import numpy as np
# 从dataset.mist模块中导入load_mist函数,这个函数用于加载MINIST数据集,
# 其中mnist是一个目录或者包含MINIST数据集相关函数的,模块
from dataset.mnist import load_mnist
from PIL import Image


def img_show(img):
    # 将img转换为PIL图像对象
    # np.unit8将img转换为unit8类型(无符号8位整数),然后Image.fromarray将其转换为PILtux
    pil_img = Image.fromarray(np.uint8(img))
    # 显示PIL图像
    pil_img.show()


# load_mnist(flatten=True,normalize=False):调用load_mnist函数加载MNIST数据集。
# flatten=True表示将图像展平为一维数组,normalize=False表示不对图像像素进行归一化处理
# 该函数返回的是一个元组(x_train, t_train), (x_test, t_test)
# 其中x_train和x_test是训练集和测试集的图像数据,t_train和t_test是对应的标签(即图像对应的数字标签)
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=False)

# 从训练集取出第一张图像
img = x_train[0]
# 从训练集标签中取出第一个标签
label = t_train[0]
print(label)  # 5

# 打印出变量img的形状,由于flatten=True,因此img是一个长度为784的一维数组
print(img.shape)  # (784,)
img = img.reshape(28, 28)  # 把图像的形状变为原来的尺寸
print(img.shape)  # (28, 28)

img_show(img)

load_mnist(normalize=True,flatten=True,on_hot_label=False)

第一个参数normalize设置是否将输入图像正规化为0.0到1.0的值。

          如果将该参数设置为False,则输入图像的像素会保持原来的0到255

第二个参数flatten设置是否展开输入图像(变成一维数组)

          如果将该参数设置为False,则输入图像为1X28X28的三维数组

          如果设置为True,则输入图像会保存为由784个元素构成的一维数组

第三个参数one_hot_label设置是否将标签保存为one-hot表示(one-hot representation)

,one-hot表示是仅正确解标签为1,其余皆为0的数组。就像[0,0,1,0,0,0,0,0,0,0]这样。当one_hot_label为False时,只是像7、2这样简单保存正确解标签。

load_mnist()函数内部使用了pickle功能(在第二次及以后读入时)。利用pickle功能,可以高效的完成MNIST数据的准备工作。

python有pickle这个便利的功能,这个功能可以将程序运行中的对象保存为文件。如果加载保存过的pickle文件,可以立刻复原之前程序运行中的对象。

np.unit8() 将img转换为unit8类型(无符号8位整数)

Image.fromarray()将NumPy数组的图像数据转换为PIL用的数据对象。

pil_img=Image.fromarray(np.unit8(img))

3.6.2 神经网络的推理处理

神经网络的输入层有784个神经元(图像大小28X28=784),输出层有10个神经元(数字0到9,共10个类别),这个神经网络有2个隐藏层,第一个隐藏层有50个神经元,第二个隐藏层有100个神经元。这个50和100可以设置为任何值。

处理MNIST数据集的神经网络的实现

# coding: utf-8
import sys, os
# 将父目录(上一级目录)添加到系统路径中,这样做的目的是确保可以导入父目录中的文件或者模块
sys.path.append(os.pardir)  # 为了导入父目录的文件而进行的设定
import numpy as np
# 导入python标准库中的pickle模块,用于序列化和反序列化对象
# 这里用于加载保存的神经网络参数
import pickle
# load_mnist函数用于加载MNIST数据集
from dataset.mnist import load_mnist


# sigmoid函数的实现
def sigmoid(x):
    return 1/(1+np.exp(-x))

def softmax(a):
    c=np.max(a)
    exp_a=np.exp(a-c)   #溢出对策
    sum_exp_a=np.sum(exp_a)
    y=exp_a/sum_exp_a
    return y

def get_data():
    """用于从MNIST数据集中获取测试数据集"""
    # load_mnist函数加载MNIST数据集
    # normalize=True表示将图像像素归一化(处理为0-1之间)
    # flatten=True表示将图像展平为一维数组
    # one_hot_label=False表示标签不使用独热编码(而是单个数字表示)
    (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, flatten=True, one_hot_label=False)
    return x_test, t_test


def init_network():
    """用于初始化神经网络"""
    # 打开名为sample_weight.pkl的文件,以二进制读取模式(‘rb’)
    with open("sample_weight.pkl", 'rb') as f:
        # 使用pickle加载文件中的数据,这些数据包含了预训练好的神经网络的权重和偏置
        network = pickle.load(f)
    # 返回加载的神经网络参数
    return network


def predict(network, x):
    """用于对输入数据x进行预测"""
    # 从network中获取神经网络的权重参数
    W1, W2, W3 = network['W1'], network['W2'], network['W3']
    # 从network中获取神经网络的偏置参数
    b1, b2, b3 = network['b1'], network['b2'], network['b3']
    # 计算每一层的加权和,并加上偏置
    a1 = np.dot(x, W1) + b1
    z1 = sigmoid(a1)
    a2 = np.dot(z1, W2) + b2
    z2 = sigmoid(a2)
    a3 = np.dot(z2, W3) + b3
    y = softmax(a3)

    return y


# 获取数据集x和对应的标签t
x, t = get_data()
# 初始化神经网络,将返回的网络参数保存到变量中
network = init_network()
# 初始化正确预测的计数器
accuracy_cnt = 0

# 遍历测试数据集中的每一个样本
for i in range(len(x)):
    # 对第i个样本进行预测
    y = predict(network, x[i])
    # 获取预测最高的类别的索引
    p= np.argmax(y)
    if p == t[i]:
        accuracy_cnt += 1

print("Accuracy:" + str(float(accuracy_cnt) / len(x)))

 首先通过get_data()函数获得MNIST测试数据集x以及对应的标签t。

前面假设学习已经完成,学习到的参数假设保存在sample_weight.pkl文件中,通过init_network()函数获得network,里面包括预训练好的神经网络的权重和偏置参数。

通过predict()函数对测试数据集x中的每一个样本逐一进行分类。predict()函数以NumPy数组的形式输出各个标签对应的概率。比如说,输出[0.1,0.3,0.2,....,0.04]的数组,该数组表示“0”的概率为0.1,“1”的概率为0.3等等。 然后,通过np.argmax(y)取出y这个概率列表中最大值的索引,作为预测结果。

最后将x中的第i个预测结果和t中第i个值(即x[i]所对应的正确标签)进行比较。

执行上方的代码后,会显示“Accuracy:0.9352”这表示有93.52%的数据被正确分类了。

预处理:对神经网络的输入数据进行某种既定的转换。

正规化:把数据限定到某个范围的处理。是预处理的一种。

数据白化:将数据整体的分布形状均匀化的方法。

3.6.3 批处理

输入一张图像的数组形状变化

批处理的数组形状变化,将100张图像打包作为输入数据

输入数据的集合称为批。通过以批为单位进行推理处理,能够实现高速运算。 

批处理对计算机的运算大有利处,可以大幅度缩短每张图像的处理时间。相对于数据读入,可以将更多的时间用在计算上。批处理一次性计算大型数组要比分开逐步计算各个小型数组的速度更快。

基于批处理实现神经网络(处理MNIST数据集)

# coding: utf-8
import sys, os
# 将父目录(上一级目录)添加到系统路径中,这样做的目的是确保可以导入父目录中的文件或者模块
sys.path.append(os.pardir)  # 为了导入父目录的文件而进行的设定
import numpy as np
# 导入python标准库中的pickle模块,用于序列化和反序列化对象
# 这里用于加载保存的神经网络参数
import pickle
# load_mnist函数用于加载MNIST数据集
from dataset.mnist import load_mnist


# sigmoid函数的实现
def sigmoid(x):
    return 1/(1+np.exp(-x))

def softmax(a):
    c=np.max(a)
    exp_a=np.exp(a-c)   #溢出对策
    sum_exp_a=np.sum(exp_a)
    y=exp_a/sum_exp_a
    return y

def get_data():
    """用于从MNIST数据集中获取测试数据集"""
    # load_mnist函数加载MNIST数据集
    # normalize=True表示将图像像素归一化(处理为0-1之间)
    # flatten=True表示将图像展平为一维数组
    # one_hot_label=False表示标签不使用独热编码(而是单个数字表示)
    (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, flatten=True, one_hot_label=False)
    return x_test, t_test


def init_network():
    """用于初始化神经网络"""
    # 打开名为sample_weight.pkl的文件,以二进制读取模式(‘rb’)
    with open("sample_weight.pkl", 'rb') as f:
        # 使用pickle加载文件中的数据,这些数据包含了预训练好的神经网络的权重和偏置
        network = pickle.load(f)
    # 返回加载的神经网络参数
    return network


def predict(network, x):
    """用于对输入数据x进行预测"""
    # 从network中获取神经网络的权重参数
    W1, W2, W3 = network['W1'], network['W2'], network['W3']
    # 从network中获取神经网络的偏置参数
    b1, b2, b3 = network['b1'], network['b2'], network['b3']
    # 计算每一层的加权和,并加上偏置
    a1 = np.dot(x, W1) + b1
    z1 = sigmoid(a1)
    a2 = np.dot(z1, W2) + b2
    z2 = sigmoid(a2)
    a3 = np.dot(z2, W3) + b3
    y = softmax(a3)

    return y


# 获取数据集x和对应的标签t
x, t = get_data()
# 初始化神经网络,将返回的网络参数保存到变量中
network = init_network()

# 批数量
batch_size=100

# 初始化正确预测的计数器
accuracy_cnt = 0

# 遍历测试数据集中的每一个样本
for i in range(len(x)):
    # 对第i个样本进行预测
    y = predict(network, x[i])
    # 获取预测最高的类别的索引
    p= np.argmax(y)
    if p == t[i]:
        accuracy_cnt += 1


for i in range(0,len(x),batch_size):
    x_batch=x[i:i+batch_size]
    y_batch=predict(network,x_batch)
    p=np.argmax(y_batch,axis=1)
    accuracy_cnt+=np.sum(p==t[i:i+batch_size])
    
print("Accuracy:" + str(float(accuracy_cnt) / len(x)))

argmax():

    用于 获取值最大的元素的索引。对于多维数组,axis参数指定了在哪个轴上寻找最大值。默认情况下,通常在整个数组中寻找最大值的索引。在上面的代码中,我们给定参数axis=1,这制定了在100X10的数组中,沿着第1维的方向找到值最大的元素的索引。矩阵的第0维是列方向,第一维是行方向。

下面是一个例子:

 在NumPy数组之间使用比较运算符(==)生成由True/False构成的布尔型数组

 第4章 神经网络的学习

本章的主题是神经网络的学习,这里的“学习”是指从训练数据中自动获取最优权重参数的过程。为了使神经网络能进行学习,将导入损失函数这一指标。学习的目的就是以该损失函数为基准,找出能使它的值达到最小的权重参数。为了找出尽可能小的损失函数的值,本章介绍利用了函数斜率的梯度法。

4.1 从数据中学习

神经网络的特征就是可以从数据中学习,即可以由数据自动决定权重参数的值。

先从图像中提取特征量,再用机器学习技术学习这些特征量的模式。特征量是指可以从输入数据(输入图像)中准确地提取本质数据(重要数据)的转换器。图像的特征量通常表示为向量的形式。在计算机视觉领域,常用的特征量包括SIFT、SURF和HOG等。使用这些特征量将图像数据转换为向量,然后对转换后的向量使用机器学习中的SVM、KNN等分类器进行学习。

深度学习有时也称端到端机器学习(end-to-end machine learning)。这里所说的端到端是指从一端到另一端的意思,也就是从原始数据(输入)中获得目标结果(输出)的意思。

神经网络的优点是对所有的问题都可以用同样的流程解决。与待处理的问题无关,神经网络可以将数据直接作为原始数据,进行“端对端”的学习。

在机器学习中,为了追求模型的泛化能力,一般将训练数据和测试数据两部分来进行学习和实验。首先,使用训练数据进行学习,寻找最优的参数;然后,使用测试数据评价训练得到的模型的实际能力。 训练数据也可以称为监督数据。

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

过拟合(over fitting):只对某个数据集过度拟合的状态。

4.2 损失函数

损失函数是表示神经网络性能的“恶劣程度”的指标,即当前的神经网络对监督数据在多大程度上不拟合,在多大程度上不一致。这个损失函数可以使用任意函数,但一般使用均方误差和交叉熵误差等。

4.2.1 均方误差

import numpy as np
#均方误差
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]
print(mean_squared_error(np.array(y),np.array(t)))  #0.09750000000000003

y=[0.1,0.05,0.1,0.0,0.05,0.1,0.0,0.6,0.0,0.0]
print(mean_squared_error(np.array(y),np.array(t)))  #0.5975

上面是两个例子,例1中,正确解是“2”,神经网络的输出的最大值是“2”;例2中,正确解是“2”,神经网络输出的最大值是“7” 。例1的损失函数值更小,和监督数据之间的误差较小,均方误差显示第一个例子的输出结果和监督数据更吻合。

4.2.2 交叉熵误差

交叉熵误差的值是由正确解标签所对应的输出结果决定的。例如,假设正确解标签的索引是“2”,与之对应的神经网络的输出是0.6,则交叉熵误差是-log0.6=0.51;若“2‘对应的输出是0.1,则交叉熵误差是-log0.1=2.30.

def cross_entropy_error(y,t):
    delta=1e-7
    return -np.sum(t*np.log(y+delta))

# 设“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]
print(cross_entropy_error(np.array(y),np.array(t)))  #0.510825457099338

y=[0.1,0.05,0.1,0.0,0.05,0.1,0.0,0.6,0.0,0.0]
print(cross_entropy_error(np.array(y),np.array(t)))  #2.302584092994546

 这里,函数内部在计算np.log时,加上了一个微小值delta。这是因为,当出现np.log(0)时,np.log(0)会变成负无限大的-inf,这样一来就会导致后续计算无法进行。作为保护性对策,添加一个微小值可以防止负无限大的发生。

4.2.3 mini-batch学习

Mini-batch 学习是深度学习中常用的一种训练方法,特别适用于大规模数据集和复杂模型。它是将训练数据集分成若干小批次(mini-batch),每次从中随机选择一个批次来进行训练。以下是详细的解释:

优点

  • 内存效率:相比于全数据集的训练,mini-batch 学习可以降低内存需求,因为不需要同时将整个数据集加载到内存中。
  • 计算效率:通过并行处理和向量化操作,可以利用现代硬件(如GPU)加速训练过程。
  • 泛化能力:随机选择小批次有助于模型更好地泛化到新数据,因为模型不会过于依赖于任何一个单独的批次。

实现过程

在训练过程中,每个迭代周期(epoch)中的步骤如下:

  1. 数据洗牌(Shuffling):首先,对整个数据集进行洗牌,以确保随机性。

  2. 分批训练:将洗牌后的数据集划分为多个小批次。

  3. 迭代训练:对每个小批次进行前向传播、计算损失、反向传播、参数更新的步骤。

  4. 参数更新:在每个小批次的损失上计算梯度,并根据选择的优化算法(如随机梯度下降)更新模型的参数。

 MNIST数据集的训练数据有6000个,如何从这个训练数据中随机抽取10笔数据?

可以使用NumPy的np.random.choice(),这个函数可以从指定的数字中随机选择想要的数字。

比如,np.random.choice(60000,10)会从0到59999之间随机选择10个数字,返回一个包含被选数据的索引的数组。

4.2.4 mini-batch 版交叉熵误差的实现

def cross_entropy_error_01(y,t):
    """监督数据是one-hot表示时,y:神经网络的输出,t:监督数据"""
    # y的维度是1时,即求单个数据的交叉熵误差时,需要改变数据的姓张。
    if y.ndim==1:
        t=t.reshape(1,t.size)
        y=y.reshape(1,y.size)
    batch_size=y.shape[0]
    return -np.sum(t*np.log(y+1e-7))/batch_size

def cross_entropy_error_02(y,t):
    """监督数据是标签形式(非ont-hot表示),y:神经网络的输出,t:监督数据"""
    # y的维度是1时,即求单个数据的交叉熵误差时,需要改变数据的姓张。
    if y.ndim==1:
        t=t.reshape(1,t.size)
        y=y.reshape(1,y.size)

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

4.2.5 为何要设定损失函数

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

4.3 数值微分

数值微分 numerical differentitation,用数值方法近似求解函数的导数的过程。

4.3.1 导数

导数就是表示某个瞬间的变化量,可以定义成下面的式子:

参考上式,实现求函数的导数的程序

#不好的实现实例

def numerical_diff(f,x):

         h=10e-50

         return (f(x+h)-f(x))/h

上述代码有两处需要改进的地方:

①在上面的实现中,因为想要把尽可能小的值赋给h,这里使用了10e-50这个微小值。但会产生

舍入误差(rounding error)。

舍入误差:因省略小数的精细部分的数值(比如,小数点后第8位以后的数值)而造成最终的计算结果上的误差。

如图所示,如果用float32类型(32位的浮点数)来表示1e-50,就会变成0.0,无法正确表示出来。

这是第一个需要改进的地方,即将微小值h改为1e-4。 

② 函数f 的差分。“真的导数”对应函数在x处的斜率(称为切线),但上述实现中计算的导数对应的是(x+h)和x之间的斜率。 这个差异的出现是因为h不可能无限接近0 。

如图所示,数值微分含有误差,为了缩小这个误差,我们可以计算函数f在(x+h)和(x-h)之间的差分。因为这种计算方式以x为中心,计算它左右两边的差分,所以也成为中心差分。(而(x+h)和x之间的差分称为前向差分)。

下面是我们基于两个要改进的点来实现的数值微分(数值梯度)

def numerical_diff(f,x):

         h=1e-4

         return (f(x+h)-f(x-h))/(2*h) 

 利用微小的差分求倒数的过程称为数值微分。

而基于数学式的推导求导数的过程,则用“解析式”一词,称为“解析式求解”或者"解析性求导"。

4.3.2 数值微分的例子

import numpy as np
import matplotlib.pylab as plt


def function_1(x):
    return 0.01*x**2+0.1*x

def numerical_diff(f,x):
    h=1e-4
    return (f(x+h)-f(x-h))/(2*h)

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

print(numerical_diff(function_1,5)) #0.1999999999990898
print(numerical_diff(function_1,10)) #0.2999999999986347

虽然严格意义上不相等,但误差非常小。

4.3.3 偏导数

使用python实现

def function_2(x):

         return x[0]**2+x[1]**2

         #或者return np.sum(x**2)

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

4.4 梯度

这样的由全部变量的偏导数汇总而成的向量称为梯度。

梯度用python实现:

def numerical_gradient(f,x):
    h=1e-4
    
    # 生成和x形状相同的数组
    grad=np.zeros_like(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

梯度会指向各点处的函数值降低的方向。更严格的讲,梯度指示的方向是各点处的函数值减小最多的方向。

4.4.1 梯度法

梯度表示的是各点处的函数值减小最多的方向。因此,无法保证梯度所指的方向就是函数的最小值或者真正应该前进的方向。实际上,在复杂的函数中,梯度指示的方向基本上都不是函数最小处

函数的极小值、最小值以及被称为鞍点的地方,梯度为0 。

极小值是局部最小值,也就是限定在某个范围内的最小值。

鞍点是从某个方向上看是极大值,从另一个方向看则是极小值的点。

虽然梯度法是要寻找梯度为0的地方,但是那个地方不一定就是最小值(也有可能是极小值或者鞍点)。

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

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

严格地讲,寻找最小值地梯度法称为梯度下降法。寻找最大值的梯度法称为梯度上升法。

但是,通过反转损失函数的符号,求最小值的问题和求最大值的问题会变成相同的问题。

一般来说,神经网络(深度学习)中,梯度法主要是指梯度下降法。

下面是使用数学式来表示梯度法:

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

下面使用python来实现梯度下降法:

import numpy as np

def numerical_gradient(f, x):
    """梯度"""
    h = 1e-4

    # 生成和x形状相同的数组
    grad = np.zeros_like(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


def gradient_decent(f,init_x,lr=0.01,step_num=100):
    """梯度下降法,求函数的极小值,顺利的话,还可以求函数的最小值"""
    #初始值
    x=init_x
    #step_num指定更新操作重复的次数
    for i in range(step_num):
        grad=numerical_gradient(f,x)
        #梯度乘以学习率得到的值进行更新操作
        x -= lr*grad

    return x

学习率过大的话,会发散成一个很大的值

学习率过小的话,基本上没怎么更新就结束了

像学习率这样的参数称为超参数,这是一种和神经网络的参数(权重和偏置)性质不同的参数。相对于神经网络的权重参数是通过训练数据和学习算法自动获得的,学习率这样的超参数则是人工设定的。一般来说,超参数需要尝试多个值,一边找到一种可以使学习顺利进行的设定。

4.4.2 神经网络的梯度

神经网络的学习也需要梯度(损失函数关于权重参数的梯度)。

形状为2X3的权重W的神经网络,损失函数用L表示。

下面以一个简单的神经网络为例,来实现求梯度的代码。

import numpy as np


class simpleNet:
    def __init__(self):
        # 用高斯分布进行初始化
        # 初始化一个2X3的权重矩阵,权重值是从标准正态分布(均值为0,方差为1)中随机生成的
        self.W=np.random.randn(2,3)

    def predict(self,x):
        """用于计算预测值,神经网络的预测结果"""
        return np.dot(x,self.W)

    def cross_entropy_error(self,y, t):
        if y.ndim == 1:
            t = t.reshape(1, t.size)
            y = y.reshape(1, y.size)

        # 监督数据是one-hot-vector的情况下,转换为正确解标签的索引
        if t.size == y.size:
            t = t.argmax(axis=1)

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



    def softmax(self,a):
        """用于将网络的输出转换为概率分布,a是输入的原始数据(通常是网络的输出)"""
        c = np.max(a)
        exp_a = np.exp(a - c)  # 溢出对策
        sum_exp_a = np.sum(exp_a)
        y = exp_a / sum_exp_a
        return y

    def loss(self,x,t):
        """计算损失值"""
        z=self.predict(x)
        y=self.softmax(x)
        loss=self.cross_entropy_error(y,t)
        return loss

net=simpleNet()
print(net.W)    #权重参数
# [[-0.14668415 -2.52400621 -0.25773264]
#  [ 0.12617726 -0.41741201  1.27810688]]

x=np.array([0.6,0.9])
p=net.predict(x)
print(p)    #[ 0.02554904 -1.89007453  0.99565661]

print(np.argmax(p))     #2


t=np.array([0,0,1]) #正确解标签
print(net.loss(x,t))    #2.263065089352068

4.5 学习算法的实现

神经网络的学习分为下面4个步骤

步骤1(mini-batch)

从训练数据中随机选出一部分数据,这部分数据称为mini-batch。我们的目标是减小mini-batch的损失函数的值。

步骤2(计算梯度)

为了减小mini-batch的损失函数的值,需要求出各个权重参数的梯度。梯度表示损失函数的值减小最多的方向。

步骤3(更新参数)

将权重参数沿着梯度方向进行微小更新。使用梯度下降法更新参数,但是神经网络的学习使用的数据是随机选择的mini-batch数据,所以又称随机梯度下降法(stochastic gradient descent)。随机梯度下降法一般由一个名为SGD的函数来实现的。

步骤4(重复)

重复步骤1、步骤2、步骤3

下面来实现手写数字识别的神经网络,以2层神经网络(隐藏层为1层的网络)为对象,使用MNIST数据集进行学习。

4.5.1 2层神经网络的类

如何设置权重参数的初始值这个问题是关系到神经网络能否成功学习的重要问题。后面会详细讨论权重参数的初始化。这里只需知道,权重使用符合高斯分布的随机数进行初始化,偏置使用0进行初始化。

import sys,os

import numpy as np

sys.path.append(os.pardir)
# sigmoid函数的实现
def sigmoid(x):
    return 1/(1+np.exp(-x))
def softmax(a):
    """用于将网络的输出转换为概率分布,a是输入的原始数据(通常是网络的输出)"""
    c = np.max(a)
    exp_a = np.exp(a - c)  # 溢出对策
    sum_exp_a = np.sum(exp_a)
    y = exp_a / sum_exp_a
    return y
def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)

    # 监督数据是one-hot-vector的情况下,转换为正确解标签的索引
    if t.size == y.size:
        t = t.argmax(axis=1)

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

def numerical_gradient(f, x):
    """梯度"""
    h = 1e-4

    # 生成和x形状相同的数组
    grad = np.zeros_like(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


class TwoLayerNet:
    def __init__(self,input_size,hidden_size,output_size,weight_init_std=0.01):
        """初始化权重"""
        self.params={}
        # params['W1']是第1层权重,params['b1']第1层的偏置
        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

    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

    def numerical_gradient(self,x,t):
        """基于数值微分计算权重参数的梯度"""
        loss_W=lambda W:self.loss(x,t)
        grads={}
        #grads['W1']是第1层权重的梯度,grads['b1']第1层的偏置的梯度
        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

4.5.2 mini-batch的实现

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

(x_train,t_train),(x_test,t_test)=load_mnist(normalize=True,one_hot_laobel=True)

train_loss_list=[]

# 超参数
iters_num=10000
train_size=x_train.shape[0]
batch_size=100
learning_rate=0.1

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

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)
    
    #更新参数
    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)

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

4.5.3 基于测试数据的评价

神经网络学习地最初目标是掌握泛化能力,因此,要评价神经网络地泛化能力,就必须使用不包含在训练数据中的数据。每经过一个qpoch,我们都会记录下训练数据和测试数据的识别精度。

epoch:是一个单位,一个epoch表示学习中所有训练数据均被使用过一次的更新次数。比如,对于1000笔训练数据,用大小为100笔数据的mini-batch进行学习时,重复随机梯度下降法100次,此时,100次就是一个epoch。

import sys,os

import numpy as np

sys.path.append(os.pardir)
from dataset.mnist import load_mnist



def numerical_gradient(f, x):
    h = 1e-4  # 0.0001
    grad = np.zeros_like(x)

    it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        idx = it.multi_index
        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  # 还原值
        it.iternext()

    return grad

def sigmoid_grad(x):
    return (1.0 - sigmoid(x)) * sigmoid(x)
# sigmoid函数的实现
def sigmoid(x):
    return 1/(1+np.exp(-x))
# def softmax(a):
#     """用于将网络的输出转换为概率分布,a是输入的原始数据(通常是网络的输出)"""
#     c = np.max(a)
#     exp_a = np.exp(a - c)  # 溢出对策
#     sum_exp_a = np.sum(exp_a)
#     y = exp_a / sum_exp_a
#     return y
def softmax(x):
    """"对输出层进行处理,将原始分数转换为类别预测的概率分布"""
    # 二维,批处理
    if x.ndim == 2:
        # 当输入x是一个二维数组时,通常情况下每行表示一个样本,每列表示不同特征或者预测输出。
        # 转置前,每一行是一个批次,一共100行,每一列是不同特征(0-9),共10列。
        # softmax函数通常应用于每个样本的预测输出,而不是整个批次的输出
        x = x.T
        # 转置后,每一列是一个批次,一共100列,每一行是不同特征(0-9),共10行。
        # 对转置后的x,沿着第0轴(每列/每个批次)减去该列的最大值。这一步是为了数值稳定性,防止指数函数的溢出
        x = x - np.max(x, axis=0)
        # 对调整后的x应用softmax操作。
        # np.exp(x)计算每个元素的指数值,然后除以每列(每个批次)元素指数之和,得到每个类别的预测概率
        y = np.exp(x) / np.sum(np.exp(x), axis=0)
        # 最后将结果转置回来,以保持与输入格式一致并返回
        return y.T
    # 单次
    # 如果x是一维的,直接对x减去最大值,这是为了防止指数函数的溢出。
    x = x - np.max(x) # 溢出对策
    return np.exp(x) / np.sum(np.exp(x))

def cross_entropy_error(y, t):
    """交叉熵损失函数的计算,适用于多分类问题,交叉熵损失通常用于衡量分类模型预测与真实标签之间的差异,越小表示模型预测得越准确"""
    # y的维度为1,即单个样本的预测结果,而不是一个批次的结果
    if y.ndim == 1:
        # 将t和yreshape成二维的形式,这样做是为了统一处理单个样本和批量样本的情况
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)

    # 监督数据是one-hot-vector的情况下,转换为正确解标签的索引
    if t.size == y.size:
        t = t.argmax(axis=1)
    # 计算批次的大小
    batch_size = y.shape[0]
    # 计算平均交叉熵损失
    # 通过索引np.arange(batch_size)和正确类别得索引t,从预测的概率分布y中获取对应类别的预测概率,并取对数
    # 添加1e-7是为了避免取对数时出现的数值不稳定性,即当预测概率接近0时避免出现无穷大。
    # 对所有样本的对数概率求和,然后取负数,这是交叉熵损失函数的一般形式
    # 最后总和除以批次大小,得到平均交叉熵损失。这样可以使损失函数的量级与样本数量无关
    return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size



class TwoLayerNet:
    def __init__(self,input_size,hidden_size,output_size,weight_init_std=0.01):
        """初始化权重,weight_init_std用来设置权重初始化的标准差,"""
        # weight_init_std:用来设置初始化的标准差,初始化权重时,通常会使用一个小的随机数来避免权重值过大或者过小,weight_init_std控制了这个随机数的大小哦。
        self.params={}
        # params['W1']是第1层权重,params['b1']第1层的偏置
        # np.random.randn(a,b):生成一个形状为(a,b)的随机数矩阵,矩阵中的每个元素都是从标准正态分布中随机抽取的数值。
        # 使用标准正态分布的随机数来初始化神经网络的权重,有助于优化过程的顺利进行。
        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
        # 对第一层的加权输入a1应用激活函数,将加权输入转换为神经网络第一层的激活值z1
        z1=sigmoid(a1)
        # 计算第二层(即输出层)的加权输入
        a2=np.dot(z1,W2)+b2

        # 输出层预测:对输出层的加权输入应用softmax函数
        # softmax函数将加权输入转换为概率分布,表示每个类别的预测概率
        y=softmax(a2)
        return y

    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

    def numerical_gradient(self,x,t):
        """基于数值微分计算权重参数的梯度"""
        loss_W=lambda W:self.loss(x,t)
        grads={}
        #grads['W1']是第1层权重的梯度,grads['b1']第1层的偏置的梯度
        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


(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.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))

第5章 误差反向传播法

通过数值微分计算神经网络的权重参数的梯度(严格来说,是损失函数关于权重参数的梯度)。

数值微分虽然简单易实现,但计算上比较费时间。

本章节学习一个能够高效计算权重参数的梯度的方法----误差反向传播法。

5.1 计算图

计算图将计算过程用图形(数据结构图)表示出来。

5.1.1 用计算图求解

计算图通过节点和箭头表示计算过程,节点用⚪表示,⚪中是计算的内容。将计算的中间结果写在箭头上方,表示各个节点的计算结果从左向右传递。

问题:100x2x1.1。一个苹果100日元,买2个苹果,消费税是10%。

只用⚪表示乘法运算“X”也是可行的

问题:(100x2+150x3)x1.1

 使用计算图解题的情况下,需要按照如下流程进行:

①构建计算图

②在计算图上,从左向右进行计算(正向传播,即从计算图出发点到结束点的传播)

反向传播:从图上看,就是从右向左。反向传播在导数计算中发挥重要作用。

5.1.2 局部计算

计算图的特征是可以通过传递“局部计算”来获得最终结果。

“局部”:与自己相关的某个小范围

局部计算:无论全局发生了什么,都能只根据与自己相关的信息输出接下来的结果。

例子:在超市买了很多苹果和其他很多东西。

各个节点处的计算都是局部计算。这意味着,例如苹果和其他很多东西的求和运算(4000+200------>4200) 并不关心4000这个数字是如何计算而来的,只要把这两个数字相加就可以了。换言之,各个节点处只需进行与自己有关的计算,不用考虑全局。

5.1.3 为何用计算图解题

计算图优点:

         ①局部运算。无论全局是多么复杂的计算,都可以通过局部计算使各个节点致力于简单的计算,从而简化问题。

         ②利用计算图可以将中间的计算过程全部保存起来。

         ③可以通过正向传播和反向传播高效计算各个变量的导数值。

5.2 链式法则

反向传播将局部导数向正方向的反方向(从右到左)传递,传递这个局部导数的原理,是基于链式法则(chain rule)。

5.2.2 计算图的反向传播

5.2.2 什么是链式法则

链式法则是关于复合函数的导数性质,定义如下:

如果某个函数由复合函数表示,则该复合函数的导数可以用构成复合函数的各个函数的导数的成绩表示。

5.2.3 链式法则和计算图

根据计算图的反向传播的结果是2(x+y)

5.3 反向传播

5.3.1 加法节点的反向传播

5.3.2 乘法节点的反向传播

5.3.3 苹果的例子

如图所示,苹果的价格的导数是2.2,苹果的个数的导数是110,消费税的导数是200,这可以解释为:如果消费税和苹果的价格增加相同的值,则消费税将对最终价格产生200倍大小的影响,苹果的价格将产生2.2倍大小的影响。不过,因为这个例子中消费税和苹果的价格的量纲不同,才形成了这样的结果(消费税的1是100%,苹果的价格的1是1日元)

5.4 简单层的实现

“层”是神经网络功能的单位。比如,负责sigmoid函数的Sigmoid、负责矩阵乘积的Affine等,都以层为单位进行实现。

接下来,我们把构建神经网络的“层”实现为一个类。

层的实现有两个共通的方法(接口)forward()和backward(),forward()对应正向传播,backward()对应反向传播。

5.4.1 乘法层的实现

class MulLayer:
    def __init__(self):
        self.x=None
        self.y=None

    def forward(self,x,y):
        self.x=x
        self.y=y
        out=x*y
        return out

    def backward(self,dout):
        dx=dout*self.y
        dy=dout*self.x
        return dx,dy

# 苹果价格
apple=100
# 苹果数量
apple_num=2
# 消费税
tax=1.1

#layer
mul_apple_layer=MulLayer()
mul_tax_layer=MulLayer()

#forward
apple_price=mul_apple_layer.forward(apple,apple_num)
price=mul_tax_layer.forward(apple_price,tax)
print(price)    #220

#backward
dprice=1
dapple_price,dtax=mul_tax_layer.backward(dprice)
dapple,dapple_num=mul_apple_layer.backward(dapple_price)

print(dapple,dapple_num,dtax)   #2.2 110.00000000000001 200

5.4.2 加法层的实现

class AddLayer:
    def __init__(self):
        pass
    def forward(self,x,y):
        out=x+y
        return out

    def backward(self,dout):
        dx=dout*1
        dy=dout*1
        return dx,dy

class MulLayer:
    def __init__(self):
        self.x=None
        self.y=None

    def forward(self,x,y):
        self.x=x
        self.y=y
        out=x*y
        return out

    def backward(self,dout):
        dx=dout*self.y
        dy=dout*self.x
        return dx,dy



apple = 100
apple_num = 2
orange = 150
orange_num = 3
tax = 1.1
# layer
mul_apple_layer = MulLayer()
mul_orange_layer = MulLayer()
add_apple_orange_layer = AddLayer()
mul_tax_layer = MulLayer()
# forward
apple_price = mul_apple_layer.forward(apple, apple_num) #(1)
orange_price = mul_orange_layer.forward(orange, orange_num) #(2)
all_price = add_apple_orange_layer.forward(apple_price,
orange_price) #(3)
price = mul_tax_layer.forward(all_price, tax) #(4)
# backward
dprice = 1
dall_price, dtax = mul_tax_layer.backward(dprice) #(4)
dapple_price, dorange_price =add_apple_orange_layer.backward(dall_price) #(3)
dorange, dorange_num = mul_orange_layer.backward(dorange_price) #(2)
dapple, dapple_num = mul_apple_layer.backward(dapple_price) #(1)
print(price) # 715
print(dapple_num, dapple, dorange, dorange_num, dtax) # 110 2.2 3.3 165 650

5.5 激活函数层的实现

5.5.1 ReLU层

激活函数ReLU由下式表示:

y关于x的导数为:

如果正向传播时的输入x大于0,则反向传播会将上游的值原封不动地传给下游。相反,如果正向传播时的x小于等于0,则反向传播中传给下游的信号将停在此处。

class ReLU:
    def __init__(self):
        self.mask=None

    def forward(self,x):
        self.mask=(x<=0)
        out=x.copy()
        out[self.mask]=0
        return out

    def backward(self,dout):
        dout[self.mask]=0
        dx=dout
        return dx

 如果正向传播的输入值小于等于0,则反向传播的值为0 。因此,反向传播中会使用正向传播时保存的mask,将从上游传来的dout的mask中的元素为True的地方设为0

5.5.2 Sigmoid层

sigmoid函数由下列式子表示

一步步进行计算可以得到下图: 

class Sigmoid:
 def __init__(self):
 self.out = None
 def forward(self, x):
 out = 1 / (1 + np.exp(-x))
 self.out = out
 return out
 def backward(self, dout):
 dx = dout * (1.0 - self.out) * self.out
 return dx

5.6 Affine层/Softmax层的实现

5.6.1 Affine层

神经网络的正向传播中进行的矩阵的乘积运算在几何学领域中被称为”仿射变换“(几何中,仿射变换包括一次线性变换和一次平移,分别对应神经网络的加权和运算与加偏置运算)。因此这里,将进行仿射变换的处理实现为”Addine层“。

5.6.2 批版本的Affine层

class Affine:
    def __init__(self,W,b):
        self.W=W
        self.b=b
        self.x=None
        self.dW=None
        self.db=None

    def forward(self,x):
        self.x=x
        out=np.dot(x,self.W)+self.b

        return out

    def backward(self,dout):
        dx=np.dot(dout,self.W.T)
        self.dW=np.dot(self.x.T,dout)
        self.db=np.sum(dout,axis=0)

        return dx

5.6.3 Softmax-with-Loss层

最后介绍一下输出层的softmax函数。Softmax层将输入值正规化(将输出值的和调整为1)之后再输出。神经网络的推理通常不用Softmax层。神经网络的学习阶段需要。

下面来实现Softmax层。考虑到这里包含作为损失函数的交叉熵误差(cross entropy error),所以也称为"Softmax-with-Loss层"

计算图如下:

需要注意的是反向传播的结果。(y1-t1,y2-t2,y3-t3)是Softmax层的输出和教师标签的差分。神经网络的反向传播会把这个差分表示的误差传递给前面的层,这是神经网络学习中的重要性质。

神经网络学习的目的就是通过调整权重参数,使神经网络的输出(Softmax的输出)接近教师标签。因此,必须将神经网络的输出与教师标签的误差高效的传递给前面的层。

import numpy as np


def cross_entropy_error(y, t):
    """交叉熵损失函数的计算,适用于多分类问题,交叉熵损失通常用于衡量分类模型预测与真实标签之间的差异,越小表示模型预测得越准确"""
    # y的维度为1,即单个样本的预测结果,而不是一个批次的结果
    if y.ndim == 1:
        # 将t和yreshape成二维的形式,这样做是为了统一处理单个样本和批量样本的情况
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)

    # 监督数据是one-hot-vector的情况下,转换为正确解标签的索引
    if t.size == y.size:
        t = t.argmax(axis=1)
    # 计算批次的大小
    batch_size = y.shape[0]
    # 计算平均交叉熵损失
    # 通过索引np.arange(batch_size)和正确类别得索引t,从预测的概率分布y中获取对应类别的预测概率,并取对数
    # 添加1e-7是为了避免取对数时出现的数值不稳定性,即当预测概率接近0时避免出现无穷大。
    # 对所有样本的对数概率求和,然后取负数,这是交叉熵损失函数的一般形式
    # 最后总和除以批次大小,得到平均交叉熵损失。这样可以使损失函数的量级与样本数量无关
    return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size
def softmax(x):
    """"对输出层进行处理,将原始分数转换为类别预测的概率分布"""
    # 二维,批处理
    if x.ndim == 2:
        # 当输入x是一个二维数组时,通常情况下每行表示一个样本,每列表示不同特征或者预测输出。
        # 转置前,每一行是一个批次,一共100行,每一列是不同特征(0-9),共10列。
        # softmax函数通常应用于每个样本的预测输出,而不是整个批次的输出
        x = x.T
        # 转置后,每一列是一个批次,一共100列,每一行是不同特征(0-9),共10行。
        # 对转置后的x,沿着第0轴(每列/每个批次)减去该列的最大值。这一步是为了数值稳定性,防止指数函数的溢出
        x = x - np.max(x, axis=0)
        # 对调整后的x应用softmax操作。
        # np.exp(x)计算每个元素的指数值,然后除以每列(每个批次)元素指数之和,得到每个类别的预测概率
        y = np.exp(x) / np.sum(np.exp(x), axis=0)
        # 最后将结果转置回来,以保持与输入格式一致并返回
        return y.T
    # 单次
    # 如果x是一维的,直接对x减去最大值,这是为了防止指数函数的溢出。
    x = x - np.max(x) # 溢出对策
    return np.exp(x) / np.sum(np.exp(x))

class SoftmaxWithLoss:
     def __init__(self):
         self.loss = None # 损失
         self.y = None # softmaxⲴ输出
         self.t = None # ⴁⶓ数据(one-hot vector)
     def forward(self, x, t):
         self.t = t
         self.y = softmax(x)
         self.loss = cross_entropy_error(self.y, self.t)
         return self.loss
     def backward(self, dout=1):
         batch_size = self.t.shape[0]
         dx = (self.y - self.t) / batch_size
         return dx

注意:反向传播时,将要传播的值除以批的大小(batch_size)后,传递给前面的层的是单个数据的误差。

5.7 误差反向传播法的实现

5.7.1 神经网络学习的全貌图

神经网络中有合适的权重和偏置,调整权重和偏置以便拟合训练数据的过程称为学习。神经网络的学习分为下面四个步骤:

步骤1(mini-batch)

从训练数据中随机选择一部分数据

步骤2(计算梯度)

计算损失函数关于各个权重参数的梯度。(误差反向传播法求梯度)

步骤3(更新参数)

将权重参数沿着梯度方向进行微小的更新

步骤4(重复)

重复步骤1、2、3

5.7.2 对应误差反向传播法的神经网络的实现

5.7.3 误差反向传播法的梯度确认

5.7.4 使用误差反向传播法的学习

第7章 卷积神经网络

本章的主题是卷积神经网络(Convolutional Neural Network,CNN).CNN被用于图像识别、语音识别等各种场合,在图像识别的比赛中,基于深度学习的方法几乎都是以CNN为基础。

7.1 整体结构

CNN相较于之前介绍的神经网络,新出现了卷积层(Convolution层)和池化层(Pooling层)。

全连接(fully-connected):相邻层的所有神经元之间都有连接。

7.2 卷积层

7.2.1 全连接层(Affine层)存在的问题

全连接层存在的问题就是数据的形状被忽视了。比如,输入数据是图象时,图像通常是高、长、通道方向上的三维形状,但是,向全连接层输入时,需要将三维的数据拉平为一维的数据。(前面使用MNIST数据集的例子中,输入图像是1通道、高28像素、长28像素(1,28,28)形状,但却被排成1列,以784个数据的形式输入到最开始的Affine层)

全连接层会忽视形状,将全部的输入数据作为相同的神经元(同一维度的神经元)处理,无法利用与形状相关的信息。

而卷积层可以保持形状不变,当输入数据是图像时,卷积层会以三维数据的形式接收输入数据,并同样以三维数据的形式输出至下一层。

另外,CNN中,有时将卷积层的输入输出数据称为特征图(feature map)。其中,卷积层的输入数据称为输入特征图,输出数据称为输出特征图。

7.2.2 卷积运算

卷积层进行的处理就是卷积运算。卷积运算相当于图像处理中的滤波器运算。滤波器也称为“核”

在之前的学习中,以及学习过卷积运算的原理,这里不再赘述。

在全连接的神经网络中,除了权重参数,还存在偏置。CNN中,老板勃起的参数就对应之前的权重,并且CNN中也存在偏置。

7.2.3 填充

在进行卷积层的处理之前,有时要向输入数据周围填入固定的数据(比如0),这称为填充(padding)。

下面是对大小为(4,4)的输入数据应用了幅度为1的填充。

使用填充的主要目的是调整输出的大小。

7.2.4 步幅

应用滤波器的位置间隔称为步幅(stride)。

增大步幅后,输出大小会变小。而增大填充后,输出大小会变大。

输入大小为(H,W),滤波器大小为(FH,FW),输出大小为(OH,OW),填充为P,步幅为S/此时,输出大小可通过下式计算。

7.2.5 3维数据的卷积运算

在3维数据的卷积运算中,输入数据和滤波器的通道数要设相同的值。上图中,均为3 。

7.2.6 结合方块思考

在上图中,数据输出是一张特征图。1张特征图就是通道数为1的特征图。

上图中,通过应用FN个滤波器,输出特征图也生成了FN个。如果将这FN个特征图汇集在一起,就得到了形状为(FN,OH,OW)的方块,如果将这个方块传递给下一层,就是CNN的处理流。

7.2.7 批处理

需要将在各层间传递的数据保存为4维数据。具体地讲,就是按照(batch_num,channel,height,width)的顺序保存数据。

批处理讲N次处理汇总成了1次进行。

7.3 池化层

池化是缩小高、长方向上的空间的运算。

下图是将2X2的区域集约成1个元素的处理,缩小空间大小。Max池化就是获取最大值的运算。

除了Max池化之外,还有Average池化。相对于Max池化是从目标区域中取出最大值,Average池化则是计算目标区域的平均值。在图像识别领域,主要使用Max池化。

池化层的特征:

没有要学习的参数

         池化只是从目标区域中获取最大值(或者平均值),所以不存在要学习的参数。

通道数不发生变化

         经过池化运算,输入数据和输出数据的通道数不会发生变化。计算是按照通道独立进行的。

对微小的位置变化具有鲁棒性(健壮)

 7.4 卷积层和池化层的实现

7.4.1 4维数据

如前所述,CNN中各层间传递的数据是4维数据。数据形状是(10,1,28,28),对应的是10个高为28、长为28、通道为1的数据。

7.4.2 基于im2col的展开

im2col(image to colum)是一个函数,将输入数据展开以适合滤波器(权重)。

下图是对包含批数量的4维的输入数据应用im2col后,数据转换为2维矩阵。

对于输入数据,将应用滤波器的区域(3维方块)横向展开为1列。

在上图中,为了便于观察,将步幅设置得很大,以便于滤波器的应用区域不重叠,而在实际的卷积运算中,滤波器的应用区域几乎都是重叠的。在这种情况下,展开后的元素个数会多于原方块的元素个数。因此,使用im2col的实现存在比普通实现消耗更多内存的缺点。但是汇总成一个大的矩阵计算,可以有效地利用线性代数库。

7.4.3 卷积层的实现

im2col代码实现:

def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
    """

    Parameters
    ----------
    input_data : 由(数据量, 通道, 高, 长)的4维数组构成的输入数据
    filter_h : 滤波器的高
    filter_w : 滤波器的长
    stride : 步幅
    pad : 填充

    Returns
    -------
    col : 2维数组
    """
    N, C, H, W = input_data.shape
    out_h = (H + 2*pad - filter_h)//stride + 1
    out_w = (W + 2*pad - filter_w)//stride + 1

    img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant')
    col = np.zeros((N, C, filter_h, filter_w, out_h, out_w))

    for y in range(filter_h):
        y_max = y + stride*out_h
        for x in range(filter_w):
            x_max = x + stride*out_w
            col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]

    col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N*out_h*out_w, -1)
    return col

在进行卷积层的反向传播时,必须进行im2col的逆处理。可是使用下面的col2im函数来进行。

def col2im(col, input_shape, filter_h, filter_w, stride=1, pad=0):
    """

    Parameters
    ----------
    col :
    input_shape : 输入数据的形状(例:(10, 1, 28, 28))
    filter_h :
    filter_w
    stride
    pad

    Returns
    -------

    """
    N, C, H, W = input_shape
    out_h = (H + 2*pad - filter_h)//stride + 1
    out_w = (W + 2*pad - filter_w)//stride + 1
    col = col.reshape(N, out_h, out_w, C, filter_h, filter_w).transpose(0, 3, 4, 5, 1, 2)

    img = np.zeros((N, C, H + 2*pad + stride - 1, W + 2*pad + stride - 1))
    for y in range(filter_h):
        y_max = y + stride*out_h
        for x in range(filter_w):
            x_max = x + stride*out_w
            img[:, :, y:y_max:stride, x:x_max:stride] += col[:, :, y, x, :, :]

    return img[:, :, pad:H + pad, pad:W + pad]
class Convolution:
    def __init__(self, W, b, stride=1, pad=0):
        self.W = W
        self.b = b
        self.stride = stride
        self.pad = pad
        
        # 中间数据(backward时使用)
        self.x = None   
        self.col = None
        self.col_W = None
        
        # 权重和偏置参数的梯度
        self.dW = None
        self.db = None

    def forward(self, x):
        FN, C, FH, FW = self.W.shape
        N, C, H, W = x.shape
        out_h = 1 + int((H + 2*self.pad - FH) / self.stride)
        out_w = 1 + int((W + 2*self.pad - FW) / self.stride)

        col = im2col(x, FH, FW, self.stride, self.pad)
        col_W = self.W.reshape(FN, -1).T

        out = np.dot(col, col_W) + self.b
        out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)

        self.x = x
        self.col = col
        self.col_W = col_W

        return out

    def backward(self, dout):
        FN, C, FH, FW = self.W.shape
        dout = dout.transpose(0,2,3,1).reshape(-1, FN)

        self.db = np.sum(dout, axis=0)
        self.dW = np.dot(self.col.T, dout)
        self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW)

        dcol = np.dot(dout, self.col_W.T)
        dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)

        return dx

7.4.4 池化层的实现

池化层的实现和卷积层相同,都使用im2col展开数据。不过,池化情况下,在通道方向上是独立的。

像这样展开之后,只需对展开的矩阵求各行的最大值,并转换成合适的形状即可。

代码实现

class Pooling:
    def __init__(self, pool_h, pool_w, stride=1, pad=0):
        self.pool_h = pool_h
        self.pool_w = pool_w
        self.stride = stride
        self.pad = pad
        
        self.x = None
        self.arg_max = None

    def forward(self, x):
        N, C, H, W = x.shape
        out_h = int(1 + (H - self.pool_h) / self.stride)
        out_w = int(1 + (W - self.pool_w) / self.stride)

        col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
        col = col.reshape(-1, self.pool_h*self.pool_w)

        arg_max = np.argmax(col, axis=1)
        out = np.max(col, axis=1)
        out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)

        self.x = x
        self.arg_max = arg_max

        return out

    def backward(self, dout):
        dout = dout.transpose(0, 2, 3, 1)
        
        pool_size = self.pool_h * self.pool_w
        dmax = np.zeros((dout.size, pool_size))
        dmax[np.arange(self.arg_max.size), self.arg_max.flatten()] = dout.flatten()
        dmax = dmax.reshape(dout.shape + (pool_size,)) 
        
        dcol = dmax.reshape(dmax.shape[0] * dmax.shape[1] * dmax.shape[2], -1)
        dx = col2im(dcol, self.x.shape, self.pool_h, self.pool_w, self.stride, self.pad)
        
        return dx

7.5 CNN的实现

第8章 深度学习

深度学习是加深了层的深度神经网络。基于之前介绍的网络,只需通过叠加层,就可以创建深度网络。

8.1 加深网络

8.1.1 向更深的网络出发

 这个网络使用He初始值作为权重的初始值,使用Adam更新权重参数。

这个网络有如下特点:

基于3x3的小型滤波器的卷积层

激活函数是ReLU

全连接层的后面使用Droput层

基于Adam的最优化

使用He初始值作为权重初始值

实现的源代码:deep_convent.py

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 为了导入父目录的文件而进行的设定
import pickle
import numpy as np
from collections import OrderedDict
from common.layers import *


class DeepConvNet:
    """识别率为99%以上的高精度的ConvNet

    网络结构如下所示
        conv - relu - conv- relu - pool -
        conv - relu - conv- relu - pool -
        conv - relu - conv- relu - pool -
        affine - relu - dropout - affine - dropout - softmax
    """
    def __init__(self, input_dim=(1, 28, 28),
                 conv_param_1 = {'filter_num':16, 'filter_size':3, 'pad':1, 'stride':1},
                 conv_param_2 = {'filter_num':16, 'filter_size':3, 'pad':1, 'stride':1},
                 conv_param_3 = {'filter_num':32, 'filter_size':3, 'pad':1, 'stride':1},
                 conv_param_4 = {'filter_num':32, 'filter_size':3, 'pad':2, 'stride':1},
                 conv_param_5 = {'filter_num':64, 'filter_size':3, 'pad':1, 'stride':1},
                 conv_param_6 = {'filter_num':64, 'filter_size':3, 'pad':1, 'stride':1},
                 hidden_size=50, output_size=10):
        # 初始化权重===========
        # 各层的神经元平均与前一层的几个神经元有连接(TODO:自动计算)
        pre_node_nums = np.array([1*3*3, 16*3*3, 16*3*3, 32*3*3, 32*3*3, 64*3*3, 64*4*4, hidden_size])
        wight_init_scales = np.sqrt(2.0 / pre_node_nums)  # 使用ReLU的情况下推荐的初始值
        
        self.params = {}
        pre_channel_num = input_dim[0]
        for idx, conv_param in enumerate([conv_param_1, conv_param_2, conv_param_3, conv_param_4, conv_param_5, conv_param_6]):
            self.params['W' + str(idx+1)] = wight_init_scales[idx] * np.random.randn(conv_param['filter_num'], pre_channel_num, conv_param['filter_size'], conv_param['filter_size'])
            self.params['b' + str(idx+1)] = np.zeros(conv_param['filter_num'])
            pre_channel_num = conv_param['filter_num']
        self.params['W7'] = wight_init_scales[6] * np.random.randn(64*4*4, hidden_size)
        self.params['b7'] = np.zeros(hidden_size)
        self.params['W8'] = wight_init_scales[7] * np.random.randn(hidden_size, output_size)
        self.params['b8'] = np.zeros(output_size)

        # 生成层===========
        self.layers = []
        self.layers.append(Convolution(self.params['W1'], self.params['b1'], 
                           conv_param_1['stride'], conv_param_1['pad']))
        self.layers.append(Relu())
        self.layers.append(Convolution(self.params['W2'], self.params['b2'], 
                           conv_param_2['stride'], conv_param_2['pad']))
        self.layers.append(Relu())
        self.layers.append(Pooling(pool_h=2, pool_w=2, stride=2))
        self.layers.append(Convolution(self.params['W3'], self.params['b3'], 
                           conv_param_3['stride'], conv_param_3['pad']))
        self.layers.append(Relu())
        self.layers.append(Convolution(self.params['W4'], self.params['b4'],
                           conv_param_4['stride'], conv_param_4['pad']))
        self.layers.append(Relu())
        self.layers.append(Pooling(pool_h=2, pool_w=2, stride=2))
        self.layers.append(Convolution(self.params['W5'], self.params['b5'],
                           conv_param_5['stride'], conv_param_5['pad']))
        self.layers.append(Relu())
        self.layers.append(Convolution(self.params['W6'], self.params['b6'],
                           conv_param_6['stride'], conv_param_6['pad']))
        self.layers.append(Relu())
        self.layers.append(Pooling(pool_h=2, pool_w=2, stride=2))
        self.layers.append(Affine(self.params['W7'], self.params['b7']))
        self.layers.append(Relu())
        self.layers.append(Dropout(0.5))
        self.layers.append(Affine(self.params['W8'], self.params['b8']))
        self.layers.append(Dropout(0.5))
        
        self.last_layer = SoftmaxWithLoss()

    def predict(self, x, train_flg=False):
        for layer in self.layers:
            if isinstance(layer, Dropout):
                x = layer.forward(x, train_flg)
            else:
                x = layer.forward(x)
        return x

    def loss(self, x, t):
        y = self.predict(x, train_flg=True)
        return self.last_layer.forward(y, t)

    def accuracy(self, x, t, batch_size=100):
        if t.ndim != 1 : t = np.argmax(t, axis=1)

        acc = 0.0

        for i in range(int(x.shape[0] / batch_size)):
            tx = x[i*batch_size:(i+1)*batch_size]
            tt = t[i*batch_size:(i+1)*batch_size]
            y = self.predict(tx, train_flg=False)
            y = np.argmax(y, axis=1)
            acc += np.sum(y == tt)

        return acc / x.shape[0]

    def gradient(self, x, t):
        # forward
        self.loss(x, t)

        # backward
        dout = 1
        dout = self.last_layer.backward(dout)

        tmp_layers = self.layers.copy()
        tmp_layers.reverse()
        for layer in tmp_layers:
            dout = layer.backward(dout)

        # 设定
        grads = {}
        for i, layer_idx in enumerate((0, 2, 5, 7, 10, 12, 15, 18)):
            grads['W' + str(i+1)] = self.layers[layer_idx].dW
            grads['b' + str(i+1)] = self.layers[layer_idx].db

        return grads

    def save_params(self, file_name="params.pkl"):
        params = {}
        for key, val in self.params.items():
            params[key] = val
        with open(file_name, 'wb') as f:
            pickle.dump(params, f)

    def load_params(self, file_name="params.pkl"):
        with open(file_name, 'rb') as f:
            params = pickle.load(f)
        for key, val in params.items():
            self.params[key] = val

        for i, layer_idx in enumerate((0, 2, 5, 7, 10, 12, 15, 18)):
            self.layers[layer_idx].W = self.params['W' + str(i+1)]
            self.layers[layer_idx].b = self.params['b' + str(i+1)]

训练用的代码:train_deepnet.py

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 为了导入父目录而进行的设定
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
from deep_convnet import DeepConvNet
from common.trainer import Trainer

(x_train, t_train), (x_test, t_test) = load_mnist(flatten=False)

network = DeepConvNet()  
trainer = Trainer(network, x_train, t_train, x_test, t_test,
                  epochs=20, mini_batch_size=100,
                  optimizer='Adam', optimizer_param={'lr':0.001},
                  evaluate_sample_num_per_epoch=1000)
trainer.train()

# 保存参数
network.save_params("deep_convnet_params.pkl")
print("Saved Network Parameters!")

8.1.2 进一步提高识别精度

对于大规模的一般物体的识别情况,问题复杂,通过加深层可以提高识别精度。

进一步提高识别精度的技术和线索:集成学习、学习率衰减、Data Augmentation(数据扩充)等都有助于提高识别精度。尤其是Data Augmentation,虽然方法很简单,但在提高识别精度上效果显著。

Data Augmentation基于算法“人为地”扩充输入图像(训练图像)。例如对于输入图像,通过施加旋转、垂直或水平方向地移动等微小变化,增加图像的数量。还可以通过其他各种方法扩充图像,比如图像的“crop 处理‘、将图像翻转的”flip处理“等。对于一般的图像,施加亮度等外观上的变化、放大或缩小等尺度上的变化也是有效的。

8.1.3 加深层的动机

层越深,识别性能越好。

加深层的一个好处就是可以减少网络的参数数量。(与没有加深层的网络相比,加深了层的网络可以用更少的参数达到同等水平(或者更强)的表现力)。

一次5X5的卷积运算的区域可以由两次3X3的卷积运算抵充。前者的参数数量25(5X5),后者的参数数量是18(2X3X3),通过叠加卷积层,参数数量减少了。

叠加小型滤波器来加深网络的好处是可以减少参数的数量,扩大感受野(receptive field,给神经元施加变化的某个局部空间区域)。并且,通过叠加层,将ReLU等激活函数夹在卷积层的中间,进一步提高了网络的表现力。这是因为向网络添加了基于激活函数的“非线性”表现力,通过非线性函数的叠加,可以表现更加复杂的东西。

加深层的另一个好处就是使学习更加高效。

通过加深层,可以分层次地传递信息,可以将各层要学习地问题分解成容易解决地简单问题。

8.2 深度学习小历史

8.2.1 ImageNet

ImageNet是拥有100万张图像地数据集。包含了各种各样地图像,并且每张图像都被关联了标签。

实践中经常会灵活应用ImageNet这个巨大的数据集学习到的权重数据,这称为迁移学习。将学习完的权重(的一部分)复制到其他神经网络,进行再学习。迁移学习在手头数据集较少时非常有效。

每年都会举办使用这个巨大数据集的ILSVRC图像识别大赛。以2012年为界,之后基于深度学习的方法一直居于首位。

8.2.2 VGG

VGG是由卷积层和池化层构成的基础的CNN。它的特点是将有权重的层(卷积层或者全连接层)叠加至16层(或者19层),具备了深度(根据层的深度,有时也称VGG16或者VGG19)。VGG中需要注意的地方是,基于3X3的小型滤波器的卷积层的运算是连续进行的。如图所示,重复进行“卷积层重叠2次到4次,再通过池化层将大小减半”的处理,最后经由全连接层输出结果。

8.2.3 GoogLeNet

和CNN结构基本相同,不过GoogLeNet的特征是,网络不仅在纵向上有深度,在横向上也有深度(广度)。GoogLeNet在横向上有宽度,这称为“Inception结构”

8.2.4 ResNet

ResNet是微软团队的开发的网络,它的特征在于具有比以前的网络更深的结构。

在深度学习中,过度加深层的话,很多情况下学习将不能顺利进行,导致最终性能不佳。ResNet中,为了解决这类问题,导入了“快捷结构”(也称为“捷径”或“小路”)。导入这个快捷结构之后,就可以随着层的加深不断提高性能了(当然,层的加深也是有限度的)。原因在于,通过快捷结构,反向传播时信号可以无衰减的传递。

基于快捷结构,不用担心梯度会变小(或者变大),能够向前一层传递“有意义的梯度”。

8.3 深度学习的高速化

8.3.1 需要努力解决的问题

卷积层地处理占比时间高,卷积层中的运算可以追溯至乘积累加运算,深度学习的高速化主要课题就变成了如何高速、高效地进行大量地成绩累加运算。

8.3.2 基于GPU的高速化

GPU原本是作为图像专用的显卡使用的,但最近不仅用于图像处理,也用于通用的数值计算。由于GPU可以高速的进行并行数值计算,因此GPU计算的目标就是将这种压倒性的计算能力用于各种用途。

GPU计算:基于GPU进行通用的数值计算的操作。

深度学习中需要进行大量的乘积累加运算(或者大型矩阵的乘积运算)。这种大量的并行运算正式GPU所擅长的(CPU比较擅长连续的、复杂的运算)。

GPU主要由NVIDIA和AMD两家公司提供,虽然两家的GU都可以用于通用的数值计算,但实际上大多数深度学习框架之受益于NVDIA的GPU。这是因为深度学习的框架中使用了NVIDIA提供的CUDA这个面向GPU计算的综合开发环境。

8.3.3 分布式学习

虽然通过GPU可以实现深度学习运算的高速化,但即便如此,当网络较深时,学习还是需要几天到几周的时间。为了进一步提高深度学习所需的计算的速度,可以考虑在多个GPU上或者多台机器上进行分布式计算。“如何进行分布式计算”是一个非常难的课题,包括了机器间的通信、数据同步等多个无法轻易解决的问题。可以将这些难题都交给TensorFlow等优秀框架。关于分布式学习的细节和技术,可以参考学习TensorFlow的技术论文等。

8.3.4 运算精度的位数缩减

在深度学习的高速化中,除了计算量之外,内存容量、总线带宽等也有可能成为瓶颈。关于内存容量,需要考虑将大量的权重参数或中间数据放在内存中。关于总线带宽,当流经GPU(或CPU)总线的数据超过某个限制时,就会成为瓶颈。考虑到这些情况,我们希望尽可能减少流经网络的数据的位数。

计算机中为了表示实数,主要使用64位或者32位的浮点数。通过使用较多的位来表示数字,使数值计算时的误差造成的影响变小了,但计算的处理成本、内存使用量却增加了,同时也给总线带来了负荷。

神经网络的健壮性:即使输入的图像附有一些小的噪声,输出结果也仍然不变。正因为这个健壮性,深度学习并不那么需要数值精度的位数。

16位的半精度浮点数,可以顺利地进行学习。python中一般使用64位地浮点数,NumPy提供了16位的半精度浮点数类型(不过,只有16位类型的存储,运算本身并不用16位进行),识别精度也不会下降。

8.4 深度学习的应用案例

8.4.1 物体检测

物体检测是从图像中确定物体的位置,并进行分类的问题。

在使用CNN进行物体检测的方法中,R-CNN特别有名。

这里,首先以某种方法找出形似物体的区域,然后对提取出的区域应用CNN进行分类。R-CNN中会将图像变成正方形,或者在分类时使用SVM(支持向量机),实际的处理流会稍微复杂一些。不过,从宏观上看,也是由(2.Extract region proposals候选区域的提取和3.Compute CNN features CNN特征的计算)这两个处理构成的。 

8.4.2 图像分割

图像分割是指在像素水平上对图像进行分类。使用以像素为单位对各个对象分别着色的监督数据进行学习,在推理时,对输入图像的所有像素进行分类。

在像素水平,要基于神经网络进行图像分割,嘴贱大的方法是以所有像素为对象,对每个像素执行推理处理,这样的方法需要按照像素数量进行相应次的forward处理,需要浪费大量的时间。FCN(Fully Convolutional Network)方法 可以通过一次forward处理,对所有的像素进行分类。

FCN字面意思是“全部由卷积层构成的网络”。相对于一般的CNN包含全连接层,FCN将全连接层替换成发挥相同作用的卷积层。在物体识别中使用的网络的全连接层中,中间数据的空间容量被作为排成一列的节点进行处理,而只由卷积层构成的网络中,空间容量可以保持原样直到最后的输出。

 8.4.3 图像标题的生成

一个基于深度学习生成图像标题的代表性方法是被称为NIC((Neural Image Caption)的模型。

NIC由深层的CNN和处理自然语言的RNN构成。

Neural Image Caption的整体结构如下

NIC基于CNN从图像中提取特征,并将这个特征传递给RNN。RNN以CNN提取出的特征为初始值,递归地生成文本。基于NIC ,可以生成惊人的高精度的图像标题。

多模态处理:组合图像和自然语言等多种信息进行的处理

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值