深度学习入门学习笔记

学习来源:《深度学习入门——基于Python的理论与实现》 学习时间:2021年12月23日

记录得比较杂,主要是一些额外补充的知识。

1 感知机

1.1 感知机是什么

感知机(perceptron)是由美国学者Frank Rosenblatt在1957年提出来的。感知机也是作为神经网络(深度学习)的起源的算法。

下图是一个接收两个输入信号的感知机的例子。x1、x2是输入信号,y是输出信号,w1、w2是权重w是weight的首字母)。图中的○称为“神经元”或者“节点”。输入信号被送往神经元时,会被分别乘以固定的权重(w1x1、w2x2)。神经元会计算传送过来的信号的总和,只有当这个总和超过了某个界限值时,才会输出1。这也称为“神经元被激活”。这里将这个界限值称为阈值,用符号θ表示。

image-20220105232728152

数学表达为:

image-20220105232857434

θ换成-b,则有:

image-20220105233110956

此处,b称为偏置(biase)w1和w2称为权重。

1.2 多层感知机

  • 感知机的局限:感知机的局限性就在于它只能表示由一条直线分割的线性空间。而对于非线性空间则无能为力。例如单个感知机不能实现异或门的功能。

image-20220105233422475

上图中的感知机总共由3层构成,但是因为拥有权重的层实质上只有2层(第0层和第1层之间,第1层和第2层之间),所以称为“2层感知机”。不过,有的文献认为上图的感知机是由3层构成的,因而将其称为“3层感知机”。

1.3 小结

  • 感知机是具有输入和输出的算法。给定一个输入后,将输出一个既定的值。

  • 感知机将权重和偏置设定为参数。

  • 使用感知机可以表示与门和或门等逻辑电路。

  • 异或门无法通过单层感知机来表示。

  • 使用2层感知机可以表示异或门。

  • 单层感知机只能表示线性空间,而多层感知机可以表示非线性空间。

  • 多层感知机(在理论上)可以表示计算机

2 神经网络

具体地讲,神经网络的一个重要性质是它可以自动地从数据中学习到合适的权重参数。

2.1 从感知机到神经网络

2.1.1 神经网络的例子

image-20220105233701957

用图来表示神经网络的话,如上图所示。我们把最左边的一列称为输入层,最右边的一列称为输出层,中间的一列称为中间层。中间层有时也称为隐藏层。“隐藏”一词的意思是,隐藏层的神经元(和输入层、输出层不同)肉眼看不见。另外,本书中把输入层到输出层依次称为第0层、第1层、第2层(层号之所以从0开始,是为了方便后面基于Python进行实现)。图3-1中,第0层对应输入层,第1层对应中间层,第2层对应输出层。

顺便提一下,在上图的网络中,偏置b并没有被画出来。如果要明确地表示出b,可以像下图那样做:添加了权重为b的输入信号1。这个感知机将x1、x2、1三个信号作为神经元的输入,将其和各自的权重相乘后,传送至下一个神经元。在下一个神经元中,计算这些加权信号的总和。如果这个总和超过0,则输出1,否则输出0。另外,由于偏置的输入信号一直是1,所以为了区别于其他神经元,我们在图中把这个神经元整个涂成灰色。

image-20220105233956655

现在回过头来看一下感知机的数学表达式:

image-20220105234025255

现在将式(3.1)改写成更加简洁的形式。为了简化式(3.1),用一个函数来表示这种分情况的动作(超过0则输出1,否则输出0)。引入新函数h(x),将式(3.1)改写成下面的式(3.2)和式(3.3)。

image-20220105234139450

image-20220105234148436

2.1.2 激活函数引入

刚才登场的hx)函数会将输入信号的总和转换为输出信号,这种函数一般称为激活函数(activation function)。如“激活”一词所示,激活函数的作用在于决定如何来激活输入信号的总和。

现在来进一步改写式(3.2)。式(3.2)分两个阶段进行处理,先计算输入信号的加权总和,然后用激活函数转换这一总和。因此,如果将式(3.2)写得详细一点,则可以分成下面两个式子。

image-20220105234430626

首先,式(3.4)计算加权输入信号和偏置的总和,记为a。然后,式(3.5) 用h()函数将a转换为输出y

之前的神经元都是用一个○表示的,如果要在图中明确表示出式(3.4)和式(3.5),则可以像图3-4这样做。

image-20220105234532058

image-20220105234550334

激活函数是连接感知机和神经网络的桥梁

2.2 激活函数

image-20220105234717493

式(3.3)表示的激活函数以阈值为界,一旦输入超过阈值,就切换输出。这样的函数称为“阶跃函数”。因此,可以说感知机中使用了阶跃函数作为激活函数。也就是说,在激活函数的众多候选函数中,感知机使用了阶跃函数。

如图3-6所示,阶跃函数以0为界,输出从0切换为1(或者从1切换为0)。它的值呈阶梯式变化,所以称为阶跃函数。

image-20220105234858700

2.2.1 sigmoid函数

神经网络中经常使用的一个激活函数就是式(3.6)表示的sigmoid函数(sigmoid function)

image-20220105234751990

2.2.2 sigmoid函数实现

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

image-20220105235052058

2.2.3 非线性函数

阶跃函数和sigmoid函数还有其他共同点,就是两者均为非线性函数。sigmoid函数是一条曲线,阶跃函数是一条像阶梯一样的折线,两者都属于非线性的函数

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

2.2.4 ReLU函数

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

ReLU函数可以表示为下面的式(3.7)。

image-20220105235245185

实现

1
2
def relu(x):
return np.maximum(0, x)

image-20220105235312524

2.2.5 激活函数知识点补充

1) 概述
  1. 神经网络为什么需要激活函数:首先数据的分布绝大多数是非线性的,而一般神经网络的计算是线性的,引入激活函数,是在神经网络中引入非线性,强化网络的学习能力。所以激活函数的最大特点就是非线性。
  2. 不同的激活函数,根据其特点,应用也不同。Sigmoid和tanh的特点是将输出限制在(0,1)和(-1,1)之间,说明Sigmoid和tanh适合做概率值的处理,例如LSTM中的各种门;而ReLU就不行,因为ReLU无最大值限制,可能会出现很大值。同样,根据ReLU的特征,Relu适合用于深层网络的训练,而Sigmoid和tanh则不行,因为它们会出现梯度消失。
2) $sigmoid$

sigmoid函数也称为Logistic函数,因为sigmoid函数可以从Logistic回归(LR)中推理得到,也是LR模型指定的激活函数。

sigmod函数的取值范围在(0, 1)之间,可以将网络的输出映射在这一范围,方便分析。

Sigmoid作为激活函数的特点:

优点:平滑、易于求导。

缺点:

  1. 激活函数计算量大(在正向传播和反向传播中都包含幂运算和除法);
  2. 反向传播求误差梯度时,求导涉及除法;
  3. Sigmoid导数取值范围是[0, 0.25],由于神经网络反向传播时的“链式反应”,很容易就会出现梯度消失的情况。例如对于一个10层的网络, 根据$0.25^{10} \cong 0.000000954 $,第10层的误差相对第一层卷积的参数$W_1$的梯度将是一个非常小的值,这就是所谓的“梯度消失”。
  4. Sigmoid的输出不是0均值(即zero-centered);这会导致后一层的神经元将得到上一层输出的非0均值的信号作为输入,随着网络的加深,会改变数据的原始分布
3) $tanh$

tanh为双曲正切函数,其英文读作Hyperbolic Tangent。tanh和 sigmoid 相似,都属于饱和激活函数,区别在于输出值范围由 (0,1) 变为了 (-1,1),可以把 tanh 函数看做是 sigmoid 向下平移和拉伸后的结果。

tanh公式:

从公式2中,可以更加清晰看出tanh与sigmoid函数的关系(平移+拉伸)。

image-20220106210427338

tanh作为激活函数的特点:

相比Sigmoid函数,

  1. tanh的输出范围时(-1, 1),解决了Sigmoid函数的不是zero-centered输出问题;
  2. 幂运算的问题仍然存在;
  3. tanh导数范围在(0, 1)之间,相比sigmoid的(0, 0.25),梯度消失(gradient vanishing)问题会得到缓解,但仍然还会存在。
4) $ReLU$

Relu(Rectified Linear Unit)——修正线性单元函数:该函数形式比较简单,

公式:$relu=max(0, x)$

image-20220106210530071

从上图可知,ReLU的有效导数是常数1,解决了深层网络中出现的梯度消失问题,也就使得深层网络可训练。同时ReLU又是非线性函数,所谓非线性,就是一阶导数不为常数;对ReLU求导,在输入值分别为正和为负的情况下,导数是不同的,即ReLU的导数不是常数,所以ReLU是非线性的(只是不同于Sigmoid和tanh,relu的非线性不是光滑的)。

ReLU在x>0下,导数为常数1的特点:

导数为常数1的好处就是在“链式反应”中不会出现梯度消失,但梯度下降的强度就完全取决于权值的乘积,这样就可能会出现梯度爆炸问题。解决这类问题:一是控制权值,让它们在(0,1)范围内;二是做梯度裁剪,控制梯度下降强度,如$ReLU(x)=min(6, max(0,x))$

ReLU在x<0下,输出置为0的特点:

描述该特征前,需要明确深度学习的目标:深度学习是根据大批量样本数据,从错综复杂的数据关系中,找到关键信息(关键特征)。换句话说,就是把密集矩阵转化为稀疏矩阵,保留数据的关键信息,去除噪音,这样的模型就有了鲁棒性。ReLU将x<0的输出置为0,就是一个去噪音,稀疏矩阵的过程。而且在训练过程中,这种稀疏性是动态调节的,网络会自动调整稀疏比例,保证矩阵有最优的有效特征。

但是ReLU 强制将x<0部分的输出置为0(置为0就是屏蔽该特征),可能会导致模型无法学习到有效特征,所以如果学习率设置的太大,就可能会导致网络的大部分神经元处于‘dead’状态,所以使用ReLU的网络,学习率不能设置太大

ReLU作为激活函数的特点:

  • 相比Sigmoid和tanh,ReLU摒弃了复杂的计算,提高了运算速度。
  • 解决了梯度消失问题,收敛速度快于Sigmoid和tanh函数,但要防范ReLU的梯度爆炸
  • 容易得到更好的模型,但也要防止训练中出现模型‘Dead’情况。
5) Leaky ReLU, PReLU(Parametric Relu), RReLU(Random ReLU)

为了防止模型的‘Dead’情况,后人将x<0部分并没有直接置为0,而是给了一个很小的负数梯度值$\alpha$

Leaky ReLU中的$\alpha$为常数,一般设置 0.01。这个函数通常比 Relu 激活函数效果要好,但是效果不是很稳定,所以在实际中 Leaky ReLu 使用的并不多。

PRelu(参数化修正线性单元) 中的$\alpha$作为一个可学习的参数,会在训练的过程中进行更新。

RReLU(随机纠正线性单元)也是Leaky ReLU的一个变体。在RReLU中,负值的斜率在训练中是随机的,在之后的测试中就变成了固定的了。RReLU的亮点在于,在训练环节中,aji是从一个均匀的分布U(I,u)中随机抽取的数值。

2.3 一个三层神经网络的实现

这里我们以图3-15的3层神经网络为对象,实现从输入到输出的(前向)处理。在代码实现方面,使用上一节介绍的NumPy多维数组。巧妙地使用NumPy数组,可以用很少的代码完成神经网络的前向处理。

image-20220105235608115

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 初始化网络的参数,实际中这是由神经网络来自己学习的
def init_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

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

这里定义了identity_function()函数(也称为“恒等函数”),并将其作为输出层的激活函数。恒等函数会将输入按原样输出,因此,这个例子中没有必要特意定义identity_function()。这里这样实现只是为了和之前的流程保持统一。另外,图3-20中,输出层的激活函数用σ()表示,不同于隐藏层的激活函数h()(σ读作sigma)。

image-20220106000103000

输出层所用的激活函数,要根据求解问题的性质决定。一般地:

  • 回归问题可以使用恒等函数
  • 二元分类问题可以使用 sigmoid函数
  • 多元分类问题可以使用 softmax函数。

2.4 输出层的实现

神经网络可以用在分类问题和回归问题上,不过需要根据情况改变输出层的激活函数。一般而言,回归问题用恒等函数,分类问题用softmax函数。

2.4.1 恒等函数和softmax函数

恒等函数会将输入按原样输出,对于输入的信息,不加以任何改动地直接输出。因此,在输出层使用恒等函数时,输入信号会原封不动地被输出。另外,将恒等函数的处理过程用之前的神经网络图来表示的话,则如图3-21所示。和前面介绍的隐藏层的激活函数一样,恒等函数进行的转换处理可以用一根箭头来表示。

image-20220106000322029

分类问题中使用的softmax函数可以用下面的式(3.10)表示。

image-20220106000341879

softmax函数的分子是输入信号ak的指数函数,分母是所有输入信号的指数函数的和。

用图表示softmax函数的话,如图3-22所示。图3-22中,softmax函数的输出通过箭头与所有的输入信号相连。这是因为,从式(3.10)可以看出,输出层的各个神经元都受到所有输入信号的影响

image-20220106000447475

softmax函数实现

1
2
3
4
5
def softmax(a):
exp_a = np.exp(a)
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
return y

为防止溢出问题,改进后的softmax函数实现为:

1
2
3
4
5
6
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

2.4.2 softmax函数的特征

softmax函数的输出是0.0到1.0之间的实数。并且,softmax函数的输出值的总和是1。输出总和为1是softmax函数的一个重要性质。正因为有了这个性质,我们才可以把softmax函数的输出解释为“概率”

一般而言,神经网络只把输出值最大的神经元所对应的类别作为识别结果。并且,即便使用softmax函数,输出值最大的神经元的位置也不会变。因此,神经网络在进行分类时,输出层的softmax函数可以省略。在实际的问题中,由于指数函数的运算需要一定的计算机运算量,因此输出层的softmax函数一般会被省略。

注意:求解机器学习问题的步骤可以分为“学习(训练)” 和“推理(预测)”两个阶段。首先,在学习阶段进行模型的学习,然后,在推理阶段,用学到的模型对未知的数据进行推理(分类)。如前所述,推理阶段一般会省略输出层的 softmax函数。在输出层使用 softmax函数是因为它和神经网络的学习有关系。

2.4.3 输出层的神经元数量

输出层的神经元数量需要根据待解决的问题来决定。对于分类问题,输出层的神经元数量一般设定为类别的数量。比如,对于某个输入图像,预测是图中的数字0到9中的哪一个的问题(10类别分类问题),可以像图3-23这样,将输出层的神经元设定为10个。

如图3-23所示,在这个例子中,输出层的神经元从上往下依次对应数字0, 1, …, 9。此外,图中输出层的神经元的值用不同的灰度表示。这个例子中神经元y2颜色最深,输出的值最大。这表明这个神经网络预测的是y2对应的类别,也就是“2”。

image-20220106000906412

2.5 手写数字识别

这里我们来进行手写数字图像的分类。假设学习已经全部结束,我们使用学习到的参数,先实现神经网络的“推理处理”。这个推理处理也称为神经网络的前向传播(forward propagation)。

2.5.1 MNIST数据集

训练图像有6万张,测试图像有1万张。MNIST的图像数据是28像素 × 28像素的灰度图像(1通道),各个像素的取值在0到255之间。每个图像数据都相应地标有“7”“2”“1”等标签。

1
2
3
4
5
6
7
8
9
import sys, os
sys.path.append(os.pardir) # 为了导入父目录中的文件而进行的设定
from dataset.mnist import load_mnist # 第一次调用会花费几分钟 ……
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=False)
# 输出各个数据的形状
print(x_train.shape) # (60000, 784)
print(t_train.shape) # (60000,)
print(x_test.shape) # (10000, 784)
print(t_test.shape) # (10000,)

load_mnist函数以“(训练图像 ,训练标签 ),(测试图像,测试标签 )”的形式返回读入的MNIST数据。

此外,还可以像load_mnist(normalize=True, flatten=True, one_hot_label=False) 这 样,设 置 3 个 参 数。

参数

  • 第 1 个参数normalize设置是否将输入图像正规化为0.0~1.0的值。如果将该参数设置为False,则输入图像的像素会保持原来的0~255。

  • 第2个参数flatten设置是否展开输入图像(变成一维数组)。如果将该参数设置为False,则输入图像为1 × 28 × 28的三维数组;若设置为True,则输入图像会保存为由784个元素构成的一维数组。

  • 第3个参数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这样简单保存正确解标签;当one_hot_label为True时,标签则保存为one-hot表示。

2.5.2 神经网络的推理处理

神经网络的输入层有784个神经元,输出层有10个神经元。

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

下面我们先定义get_data()、init_network()、predict()这3个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 加载数据集,返回预测集数据
def get_data():
(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():
with open("sample_weight.pkl", 'rb') as f:
network = pickle.load(f)
return network

# 前向传播
def predict(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
# softmax函数激活
y = softmax(a3)
return y

main程序

1
2
3
4
5
6
7
8
9
x, t = get_data()
network = init_network()
accuracy_cnt = 0
for i in range(len(x)):
y = predict(network, x[i])
p = np.argmax(y) # 获取概率最高的元素的索引
if p == t[i]:
accuracy_cnt += 1
print("Accuracy:" + str(float(accuracy_cnt) / len(x)))

另外,在这个例子中,我们把load_mnist函数的参数normalize设置成了True。将normalize设置成True后,函数内部会进行转换,将图像的各个像素值除以255,使得数据的值在0.0~1.0的范围内。像这样把数据限定到某个范围内的处理称为正规化(normalization)。此外,对神经网络的输入数据进行某种既定的转换称为预处理(pre-processing)。这里,作为对输入图像的一种预处理,我们进行了正规化。

预处理在神经网络(深度学习)中非常实用,其有效性已在提高识别性能和学习的效率等众多实验中得到证明。在刚才的例子中,作为一种预处理,我们将各个像素值除以 255,进行了简单的正规化。实际上,很多预处理都会考虑到数据的整体分布。比如,利用数据整体的均值或标准差,移动数据,使数据整体以0为中心分布,或者进行正规化,把数据的延展控制在一定范围内。除此之外,还有将数据整体的分布形状均匀化的方法,即数据白化(whitening)等。

2.5.2 批处理

从整体的处理流程来看,图3-26中,输入一个由784个元素(原本是一个28 × 28的二维数组)构成的一维数组后,输出一个有10个元素的一维数组。这是只输入一张图像数据时的处理流程。

image-20220106002333625

现在我们来考虑打包输入多张图像的情形。比如,我们想用predict()函数一次性打包处理100张图像。为此,可以把x的形状改为100 × 784,将100张图像打包作为输入数据。用图表示的话,如图3-27所示。

image-20220106002351277

这种打包式的输入数据称为批(batch)。批有“捆”的意思,图像就如同纸币一样扎成一捆。

批处理实现的main程序

1
2
3
4
5
6
7
8
9
10
x, t = get_data()
network = init_network()
batch_size = 100 # 批数量
accuracy_cnt = 0
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) # 指定在100 × 10的数组中,沿着第1维方向(以第1维为轴)找到值最大的元素的索引
accuracy_cnt += np.sum(p == t[i:i+batch_size])
print("Accuracy:" + str(float(accuracy_cnt) / len(x)))

2.6 小结

  • 神经网络中的激活函数使用平滑变化的sigmoid函数或ReLU函数。
  • 通过巧妙地使用NumPy多维数组,可以高效地实现神经网络。
  • 机器学习的问题大体上可以分为回归问题和分类问题。
  • 关于输出层的激活函数,回归问题中一般用恒等函数,分类问题中一般用softmax函数。
  • 分类问题中,输出层的神经元的数量设置为要分类的类别数。
  • 输入数据的集合称为批。通过以批为单位进行推理处理,能够实现高速的运算。

3 神经网络的学习

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

image-20220106002742117

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

3.1 损失函数

神经网络的学习中所用的指标称为损失函数(loss function)。这个损失函数可以使用任意函数,但一般用均方误差和交叉熵误差等。

3.1.1 均方误差

可以用作损失函数的函数有很多,其中最有名的是均方误差MSE(mean squared error)。均方误差如下式所示。

image-20220106002920904

这里,yk是表示神经网络的输出,tk表示监督数据,k表示数据的维数

手写数字识别的例子中,yk、tk是由如下10个元素构成的数据。

1
2
>>> y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
>>> t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]

实现

1
2
def mean_squared_error(y, t):
return 0.5 * np.sum((y-t)**2)
1
2
3
4
5
6
7
8
9
10
11
12
>>> # 设“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.097500000000000031
>>>
>>> # 例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.59750000000000003

3.1.2 交叉熵误差

除了均方误差之外,交叉熵误差(cross entropy error)也经常被用作损失函数。交叉熵误差如下式所示。

yk是神经网络的输出,tk是正确解标签。并且,tk中只有正确解标签的索引为1,其他均为0(one-hot表示)。

比如,假设正确解标签的索引是“2”,与之对应的神经网络的输出是0.6,则交叉熵误差是log 0.6 = 0.51;若“2”对应的输出是0.1,则交叉熵误差为log 0.1 = 2.30。也就是说,交叉熵误差的值是由正确解标签所对应的输出结果决定的

实现

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

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

1
2
3
4
5
6
7
8
>>> t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
>>> y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
>>> cross_entropy_error(np.array(y), np.array(t))
0.51082545709933802
>>>
>>> y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
>>> cross_entropy_error(np.array(y), np.array(t))
2.3025840929945458

第一个例子中,正确解标签对应的输出为0.6,此时的交叉熵误差大约为0.51。第二个例子中,正确解标签对应的输出为0.1的低值,此时的交叉熵误差大约为2.3。由此可以看出,这些结果与我们前面讨论的内容是一致的。

3.1.3 mini-batch学习

机器学习使用训练数据进行学习。使用训练数据进行学习,严格来说,就是针对训练数据计算损失函数的值,找出使该值尽可能小的参数。因此,计算损失函数时必须将所有的训练数据作为对象。也就是说,如果训练数据有100个的话,我们就要把这100个损失函数的总和作为学习的指标。前面介绍的损失函数的例子中考虑的都是针对单个数据的损失函数。如果要求所有训练数据的损失函数的总和,以交叉熵误差为例,可以写成下面的式(4.3)。

这里,假设数据有N个,tnk表示第n个数据的第k个元素的值(ynk是神经网络的输出,tnk是监督数据)。式子虽然看起来有一些复杂,其实只是把求单个数据的损失函数的式(4.2)扩大到了N份数据,不过最后还要除以N进行正规化

通过除以N,可以求单个数据的“平均损失函数”。通过这样的平均化,可以获得和训练数据的数量无关的统一指标。比如,即便训练数据有1000个或10000个,也可以求得单个数据的平均损失函数。

另外,MNIST数据集的训练数据有60000个,如果以全部数据为对象求损失函数的和,则计算过程需要花费较长的时间。再者,如果遇到大数据,数据量会有几百万、几千万之多,这种情况下以全部数据为对象计算损失函数是不现实的。因此,我们从全部数据中选出一部分,作为全部数据的“近似”。神经网络的学习也是从训练数据中选出一批数据(称为mini-batch,小批量),然后对每个mini-batch进行学习。比如,从60000个训练数据中随机选择100笔,再用这100笔数据进行学习。这种学习方式称为mini-batch学习。

3.1.4 mini-batch版交叉熵误差实现

这里,我们来实现一个可以同时处理单个数据和批量数据(数据作为batch集中输入)两种情况的函数。

1
2
3
4
5
6
def cross_entropy_error(y, t):
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

这里,y是神经网络的输出,t是监督数据。y的维度为1时,即求单个数据的交叉熵误差时,需要改变数据的形状。并且,当输入为mini-batch时,要用batch的个数进行正规化,计算单个数据的平均交叉熵误差。

3.1.5 损失函数知识点补充

3.2 梯度