百度飞桨架构师手把手带给你零基础实践深度学习-打卡计划
- 总目录
- 【手写数字识别】损失函数
-
- 概述
- 分类任务的损失函数
- Softmax函数
- 交叉熵
- 实现交叉熵代码
以下是课程链接。欢迎报考!这篇文章将继续更新。我只是飞桨的搬运工
话不多说,这么良心的课程赶紧扫码上车!https://aistudio.baidu.com/aistudio/education/group/info/1297?activityId=5&directly=1&shared=1
总目录
【手写数字识别】损失函数
概述
在上一节中,我们试图通过更复杂的模型(经典的全连接神经网络和卷积神经网络)来提高手写数字识别模型训练的准确性。在本节中,我们继续从水平开始水平和垂直教学方法,如 探讨损失函数优化对模型训练效果的影响。
损失函数是模型优化的目标,用于识别许多参数值中最理想的值。在训练过程的代码中计算损失函数,每轮模型训练过程相同,分为以下三个步骤:
- 预测输出应根据输入数据进行正向计算。
- 损失按预测值和真实值计算。
- 最后,根据损失反向传输梯度更新参数。
分类任务的损失函数
在之前的计划中,我们重复使用了房价预测模型的损失函数均方误差。从预测效果来看,虽然损失在下降,模型的预测值逐渐接近真实值,但模型的最终效果并不理想。根本上,不同的深度学习任务需要有自己合适的损失函数。以房价预测和手写数字识别为例,详细分析了原因如下:
- 房价预测是一项回归任务,而手写数字识别是一项分类任务。使用均方误差作为分类任务的损失函数缺乏逻辑和效果。
- 房价可以大于0,而手写数字识别的输出只能是0-9之间的10个整数,相当于一个标签。
- 在房价预测的情况下,由于房价本身是一个连续的实际值,模型输出值与实际房价差距作为损失函数(loss)这是合理的。然而,对于分类问题,真正的结果是分类标签,而模型输出是一个真实的值,导致两者的相减没有物理意义。
那么,分类任务的合理输出是什么呢?分类任务本质上是特征组合下的分类概率。以下是一个简单的案例,如 所示。
在这种情况下,医生根据肿瘤的大小 x x x作为肿瘤性质 y y y参考判断(判断因素很多,肿瘤大小只是其中之一),所以我们观察到模型判断的结果是 x x x和 y y y标签(1是恶性的,0是良性的)。该数据背后的规则是不同大小的肿瘤,属于恶性肿瘤的可能性。观测数据是真实规则抽样的结果,分类模型应拟合真实规则,输出属于分类标签的概率。
Softmax函数
如果模型能够输出10个标签的概率,则真实标签的概率输出应尽可能接近100%,而其他标签的概率输出应尽可能接近0%,所有输出概率之和应为1。这是一个更合理的假设!相应地,真实的标签值可以转换为10维one-hot在对应数字的位置上,向量为1,其余位置为0,如标签6可转换为[0、0、0、0、0、0、1、0、0、0]。
为了实现上述思路,需要引入Softmax函数可以将原始输出转换为相应标签的概率,公式如下 C C C是标签类别的数量。
s o f t m a x ( x i ) = e x i ∑ j = 0 N e j x , i = 0 , . . . , C ? 1 softmax(x_i) = \frac {e^{x_i}}{\sum_{j=0}^N{e^x_j}}, i=0, ..., C-1 softmax(xi)=∑j=0Nejxexi,i=0,...,C−1
从公式的形式可见,每个输出的范围均在0~1之间,且所有输出之和等于1,这是变换后可被解释成概率的基本前提。对应到代码上,我们需要在网络定义部分修改输出层:self.fc = Linear(input_dim=10, output_dim=1, act='softmax')
,即是对全连接层的输出加一个softmax运算。
是一个三个标签的分类模型(三分类)使用的softmax输出层,从中可见原始输出的三个数字3、1、-3,经过softmax层后转变成加和为1的三个概率值0.88、0.12、0。
对于二分类问题,使用两个输出接入softmax作为输出层,等价于使用单一输出接入Sigmoid函数。如 所示,利用两个标签的输出概率之和为1的条件,softmax输出0.6和0.4两个标签概率,从数学上等价于输出一个标签的概率0.6。
在这种情况下,只有一层的模型为 S ( w T x i ) S(w^{T}x_i) S(wTxi), S S S为Sigmoid函数。模型预测为1的概率为 S ( w T x i ) S(w^{T}x_i) S(wTxi),模型预测为0的概率为 1 − S ( w T x i ) 1-S(w^{T}x_i) 1−S(wTxi)。
是肿瘤大小和肿瘤性质的数据图。从图中可发现,往往尺寸越大的肿瘤几乎全部是恶性,尺寸极小的肿瘤几乎全部是良性。只有在中间区域,肿瘤的恶性概率会从0逐渐到1(绿色区域),这种数据的分布是符合多数现实问题的规律。如果我们直接线性拟合,相当于红色的直线,会发现直线的纵轴0-1的区域会拉的很长,而我们期望拟合曲线0-1的区域与真实的分类边界区域重合。那么,观察下Sigmoid的曲线趋势可以满足我们对这个问题的一切期望,它的概率变化会集中在一个边界区域,有助于模型提升边界区域的分辨率。
这就类似于公共区域使用的带有恒温装置的热水器温度阀门,如 所示。由于人体适应的水温在34度-42度之间,我们更期望阀门的水温条件集中在这个区域,而不是在0-100度之间线性分布。
交叉熵
在模型输出为分类标签的概率时,直接以标签和概率做比较也不够合理,人们更习惯使用交叉熵误差作为分类问题的损失衡量。
交叉熵损失函数的设计是基于最大似然思想:最大概率得到观察结果的假设是真的。如何理解呢?举个例子来说,如 所示。有两个外形相同的盒子,甲盒中有99个白球,1个蓝球;乙盒中有99个蓝球,1个白球。一次试验取出了一个蓝球,请问这个球应该是从哪个盒子中取出的?
相信大家简单思考后均会得出更可能是从乙盒中取出的,因为从乙盒中取出一个蓝球的概率更高 ( P ( D ∣ h ) ) (P(D|h)) (P(D∣h)),所以观察到一个蓝球更可能是从乙盒中取出的 ( P ( h ∣ D ) ) (P(h|D)) (P(h∣D))。 D D D是观测的数据,即蓝球白球; h h h是模型,即甲盒乙盒。这就是贝叶斯公式所表达的思想:
P ( h ∣ D ) ∝ P ( h ) ⋅ P ( D ∣ h ) P(h|D) ∝ P(h) \cdot P(D|h) P(h∣D)∝P(h)⋅P(D∣h)
依据贝叶斯公式,某二分类模型“生成” n n n个训练样本的概率:
P ( x 1 ) ⋅ S ( w T x 1 ) ⋅ P ( x 2 ) ⋅ ( 1 − S ( w T x 2 ) ) ⋅ … ⋅ P ( x n ) ⋅ S ( w T x n ) P(x_1)\cdot S(w^{T}x_1)\cdot P(x_2)\cdot(1-S(w^{T}x_2))\cdot … \cdot P(x_n)\cdot S(w^{T}x_n) P(x1)⋅S(wTx1)⋅P(x2)⋅(1−S(wTx2))⋅…⋅P(xn)⋅S(wTxn)
对于二分类问题,模型为 S ( w T x i ) S(w^{T}x_i) S(wTxi), S S S为Sigmoid函数。当 y i y_i yi=1,概率为 S ( w T x i ) S(w^{T}x_i) S(wTxi);当 y i y_i yi=0,概率为 1 − S ( w T x i ) 1-S(w^{T}x_i) 1−S(wTxi)。
经过公式推导,使得上述概率最大等价于最小化交叉熵,得到交叉熵的损失函数。交叉熵的公式如下:
L = − [ ∑ k = 1 n t k log y k + ( 1 − t k ) log ( 1 − y k ) ] L = -[\sum_{k=1}^{n} t_k\log y_k +(1- t_k)\log(1-y_k)] L=−[k=1∑ntklogyk+(1−tk)log(1−yk)]
其中, log \log log表示以 e e e为底数的自然对数。 y k y_k yk代表模型输出, t k t_k tk代表各个标签。 t k t_k tk中只有正确解的标签为1,其余均为0(one-hot表示)。
因此,交叉熵只计算对应着“正确解”标签的输出的自然对数。比如,假设正确标签的索引是“2”,与之对应的神经网络的输出是0.6,则交叉熵误差是 − log 0.6 = 0.51 −\log 0.6 = 0.51 −log0.6=0.51;若“2”对应的输出是0.1,则交叉熵误差为 − log 0.1 = 2.30 −\log 0.1 = 2.30 −log0.1=2.30。由此可见,交叉熵误差的值是由正确标签所对应的输出结果决定的。
自然对数的函数曲线可由如下代码实现。
import matplotlib.pyplot as plt
import numpy as np
x = np.arange(0.01,1,0.01)
y = np.log(x)
plt.title("y=log(x)")
plt.xlabel("x")
plt.ylabel("y")
plt.plot(x,y)
plt.show()
plt.figure()
如自然对数的图形所示,当 x x x等于1时, y y y为0;随着 x x x向0靠近, y y y逐渐变小。因此,“正确解”标签对应的输出越大,交叉熵的值越接近0;当输出为1时,交叉熵误差为0。反之,如果“正确解”标签对应的输出越小,则交叉熵的值越大。
交叉熵的代码实现
在手写数字识别任务中,仅改动三行代码,就可以将在现有模型的损失函数替换成交叉熵(cross_entropy)。
- 在读取数据部分,将标签的类型设置成
int
,体现它是一个标签而不是实数值(飞桨默认将标签处理成“int64”)。 - 在网络定义部分,将输出层改成“输出十个标签的概率”的模式。
- 在训练过程部分,将损失函数从均方误差换成交叉熵。
#修改标签数据的格式,从float32到int64 import os import random import paddle import paddle.fluid as fluid from paddle.fluid.dygraph.nn import Conv2D, Pool2D, Linear import numpy as np from PIL import Image import gzip import json # 定义数据集读取器 def load_data(mode='train'): # 数据文件 datafile = './work/mnist.json.gz' print('loading mnist dataset from {} ......'.format(datafile)) data = json.load(gzip.open(datafile)) train_set, val_set, eval_set = data # 数据集相关参数,图片高度IMG_ROWS, 图片宽度IMG_COLS IMG_ROWS = 28 IMG_COLS = 28 if mode == 'train': imgs = train_set[0] labels = train_set[1] elif mode == 'valid': imgs = val_set[0] labels = val_set[1] elif mode == 'eval': imgs = eval_set[0] labels = eval_set[1] imgs_length = len(imgs) assert len(imgs) == len(labels), \ "length of train_imgs({}) should be the same as train_labels({})".format( len(imgs), len(labels)) index_list = list(range(imgs_length)) # 读入数据时用到的batchsize BATCHSIZE = 100 # 定义数据生成器 def data_generator(): if mode == 'train': random.shuffle(index_list) imgs_list = [] labels_list = [] for i in index_list: img = np.reshape(imgs[i], [1, IMG_ROWS, IMG_COLS]).astype('float32') label = np.reshape(labels[i], [1]).astype('int64') imgs_list.append(img) labels_list.append(label) if len(imgs_list) == BATCHSIZE: yield np.array(imgs_list), np.array(labels_list) imgs_list = [] labels_list = [