《零基础实践深度学习》2.5 手写数字识别之损失函数

本文涉及的产品
函数计算FC,每月15万CU 3个月
简介: 这篇文章详细探讨了手写数字识别任务中损失函数的选择和优化,解释了为何均方误差不适用于分类任务,并介绍了Softmax函数和交叉熵损失函数在分类问题中的应用,以及如何使用PaddlePaddle框架实现这些概念来提升模型性能。

2.5 手写数字识别之损失函数

第2.4节我们尝试通过更复杂的模型(经典的全连接神经网络和卷积神经网络),提升手写数字识别模型训练的准确性。本节我们继续将“横纵式”教学法从横向展开,如图1所示,探讨损失函数的优化对模型训练效果的影响。

图1:“横纵式”教学法 — 损失函数优化

损失函数是模型优化的目标,用于在众多的参数取值中,识别最理想的取值。损失函数的计算在训练过程的代码中,每一轮模型训练的过程都相同,分如下3步:1)先根据输入数据正向计算预测输出;2)再根据预测值和真实值计算损失;3)最后根据损失反向传播梯度并更新参数。

2.5.1 分类任务的损失函数

第2.4节我们复用了房价预测模型的损失函数:均方误差。从预测效果来看,虽然损失不断下降,模型的预测值逐渐逼近真实值,但模型的最终效果不够理想。究其根本,**不同的深度学习任务需要有各自适宜的损失函数**。我们以房价预测和手写数字识别两个任务为例,详细剖析其中的缘由有如下3点:

(1)房价预测是回归任务,而手写数字识别是分类任务,使用均方误差作为分类任务的损失函数存在逻辑和效果上的缺欠

(2)房价可以是大于0的任何浮点数,而手写数字识别的输出只可能是0~9之间的10个整数,相当于一种标签

(3)在房价预测的案例中,由于房价本身是一个连续的实数值,因此以模型输出的数值和真实房价差距作为损失函数是符合道理的。但对于分类问题,真实结果是分类标签,而模型输出是实数值,导致以两者相减作为损失不具备物理含义

那么,什么是分类任务的合理输出呢?分类任务本质上是“某种特征组合下的分类概率”,下面以一个简单案例说明,如图2所示。

图2:观测数据和背后规律之间的关系

在本案例中,医生根据肿瘤大小xx作为肿瘤性质yy的参考判断(判断的因素有很多,肿瘤大小只是其中之一),那么我们观测到该模型判断的结果是xx和yy的标签(1为恶性,0为良性)。而这个数据背后的规律是不同大小的肿瘤,属于恶性肿瘤的概率。观测数据是真实规律抽样下的结果,分类模型应该拟合这个真实规律,输出属于该分类标签的概率。

2.5.2 Softmax函数

如果模型能输出10个标签的概率,对应真实标签的概率输出尽可能接近100%,而其他标签的概率输出尽可能接近0%,且所有输出概率之和为1。这是一种更合理的假设!与此对应,真实的标签值可以转变成一个10维度的one-hot向量,在对应数字的位置上为1,其余位置为0,比如标签“6”可以转变成[0,0,0,0,0,0,1,0,0,0]。

为了实现上述思路,需要引入Softmax函数,它可以将原始输出转变成对应标签的概率,公式如下,其中CC是标签类别个数。

从公式的形式可见,每个输出的范围均在0~1之间,且所有输出之和等于1,这是这种变换后可被解释成概率的基本前提。对应到代码上,需要在前向计算中,对全连接网络的输出层增加一个Softmax运算outputs = F.softmax(outputs)

图3是一个三个标签的分类模型(三分类)使用的Softmax输出层,从中可见原始输出的三个数字3、1、-3,经过Softmax层后转变成加和为1的三个概率值0.88、0.12、0。

图3:网络输出层改为softmax函数

上文解释了为何让分类模型的输出拟合概率的原因,但为何偏偏用Softmax函数完成这个职能? 下面以二分类问题(只输出两个标签)进行原理的探讨。

对于二分类问题,使用两个输出接入Softmax作为输出层,等价于使用单一输出接入Sigmoid函数。如图4所示,利用两个标签的输出概率之和为1的条件,Softmax输出0.6和0.4两个标签概率,从数学上等价于输出一个标签的概率0.6。

图4:对于二分类问题,等价于单一输出接入Sigmoid函数

图5是肿瘤大小和肿瘤性质的数据图。从图中可发现,往往尺寸越大的肿瘤几乎全部是恶性,尺寸极小的肿瘤几乎全部是良性。只有在中间区域,肿瘤的恶性概率会从0逐渐到1(绿色区域),这种数据的分布是符合多数现实问题的规律。如果我们直接线性拟合,相当于红色的直线,会发现直线的纵轴0-1的区域会拉的很长,而我们期望拟合曲线0-1的区域与真实的分类边界区域重合。那么,观察下Sigmoid的曲线趋势可以满足我们对个问题的一切期望,它的概率变化会集中在一个边界区域,有助于模型提升边界区域的分辨率。

图5:使用Sigmoid拟合输出可提高分类模型对边界的分辨率

这就类似于公共区域使用的带有恒温装置的热水器温度阀门,如图6所示。由于人体适应的水温在34度-42度之间,我们更期望阀门的水温条件集中在这个区域,而不是在0-100度之间线性分布。

图6:热水器水温控制

2.5.2 交叉熵函数

在模型输出为分类标签的概率时,直接以标签和概率做比较也不够合理,人们更习惯使用交叉熵误差作为分类问题的损失衡量。交叉熵损失函数的设计是基于最大似然思想:最大概率得到观察结果的假设是真的。如何理解呢?举个例子来说,如图7所示。有两个外形相同的盒子,甲盒中有99个白球,1个蓝球;乙盒中有99个蓝球,1个白球。一次试验取出了一个蓝球,请问这个球应该是从哪个盒子中取出的?

图7:体会最大似然的思想

相信大家简单思考后均会得出更可能是从乙盒中取出的,因为从乙盒中取出一个蓝球的概率更高(P(D∣h)),所以观察到一个蓝球更可能是从乙盒中取出的(P(h∣D))。D是观测的数据,即蓝球白球;h是模型,即甲盒乙盒。这就是贝叶斯公式所表达的思想:

P(h∣D)∝P(h)⋅P(D∣h)


说明:

自然对数的函数曲线可由如下代码实现。

In [2]

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等于1时,y为0;随着x向0靠近,y逐渐变小。因此,正确解标签对应的输出越大,交叉熵的值越接近0;当输出为1时,交叉熵误差为0。反之,如果正确解标签对应的输出越小,则交叉熵的值越大

2.5.3 交叉熵函数的代码实现

在手写数字识别任务中,通过如下修改就可以将在现有模型的损失函数替换成交叉熵:

1)网络定义部分,将输出层改成“输出十个标签的概率”的模式。

2)在训练过程部分,将损失函数从均方误差换成交叉熵。

在数据处理部分,float32改为int64:

In [3]

from data_process import get_MNIST_dataloader

train_loader, test_loader = get_MNIST_dataloader()

在网络定义部分,需要修改输出层结构,代码如下所示。

  • 从:self.fc = Linear(in_features=980, out_features=1)
  • 到:self.fc = Linear(in_features=980, out_features=10)

In [4]

# 定义 SimpleNet 网络结构
import paddle
from paddle.nn import Conv2D, MaxPool2D, Linear
import paddle.nn.functional as F
# 多层卷积神经网络实现
class MNIST(paddle.nn.Layer):
     def __init__(self):
         super(MNIST, self).__init__()

         # 定义卷积层,输出特征通道out_channels设置为20,卷积核的大小kernel_size为5,卷积步长stride=1,padding=2
         self.conv1 = Conv2D(in_channels=1, out_channels=20, kernel_size=5, stride=1, padding=2)
         # 定义池化层,池化核的大小kernel_size为2,池化步长为2
         self.max_pool1 = MaxPool2D(kernel_size=2, stride=2)
         # 定义卷积层,输出特征通道out_channels设置为20,卷积核的大小kernel_size为5,卷积步长stride=1,padding=2
         self.conv2 = Conv2D(in_channels=20, out_channels=20, kernel_size=5, stride=1, padding=2)
         # 定义池化层,池化核的大小kernel_size为2,池化步长为2
         self.max_pool2 = MaxPool2D(kernel_size=2, stride=2)
         # 定义一层全连接层,输出维度是10
         self.fc = Linear(in_features=980, out_features=10)

    # 定义网络前向计算过程,卷积后紧接着使用池化层,最后使用全连接层计算最终输出
    # 卷积层激活函数使用Relu
     def forward(self, inputs):
         x = self.conv1(inputs)
         x = F.relu(x)
         x = self.max_pool1(x)
         x = self.conv2(x)
         x = F.relu(x)
         x = self.max_pool2(x)
         x = paddle.reshape(x, [x.shape[0], 980])
         x = self.fc(x)
         return x

修改计算损失的函数,从均方误差(常用于回归问题)到交叉熵误差(常用于分类问题),代码如下所示。

  • 从:loss = paddle.nn.functional.square_error_cost(predict, label)
  • 到:loss = paddle.nn.functional.cross_entropy(predict, label)

In [5]

def evaluation(model, datasets):
    model.eval()

    acc_set = list()
    for batch_id, data in enumerate(datasets()):
        images, labels = data
        images = paddle.to_tensor(images)
        labels = paddle.to_tensor(labels)
        pred = model(images)   # 获取预测值
        acc = paddle.metric.accuracy(input=pred, label=labels)
        acc_set.extend(acc.numpy())

    # #计算多个batch的准确率
    acc_val_mean = np.array(acc_set).mean()
    return acc_val_mean

In [6]

#仅修改计算损失的函数,从均方误差(常用于回归问题)到交叉熵误差(常用于分类问题)
def train(model):
    model.train()
    #调用加载数据的函数
    # train_loader = load_data('train')
    # val_loader = load_data('valid')
    opt = paddle.optimizer.SGD(learning_rate=0.01, parameters=model.parameters())
    EPOCH_NUM = 10
    for epoch_id in range(EPOCH_NUM):
        for batch_id, data in enumerate(train_loader()):
            #准备数据
            images, labels = data
            images = paddle.to_tensor(images)
            labels = paddle.to_tensor(labels)
            #前向计算的过程
            predicts = model(images)

            #计算损失,使用交叉熵损失函数,取一个批次样本损失的平均值
            loss = F.cross_entropy(predicts, labels)
            avg_loss = paddle.mean(loss)

            #每训练了200批次的数据,打印下当前Loss的情况
            if batch_id % 200 == 0:
                print("epoch: {}, batch: {}, loss is: {}".format(epoch_id, batch_id, avg_loss.numpy()))

            #后向传播,更新参数的过程
            avg_loss.backward()
            # 最小化loss,更新参数
            opt.step()
            # 清除梯度
            opt.clear_grad()
        # acc_train_mean = evaluation(model, train_loader)
        # acc_val_mean = evaluation(model, val_loader)
        # print('train_acc: {}, val acc: {}'.format(acc_train_mean, acc_val_mean))   
    #保存模型参数
    paddle.save(model.state_dict(), 'mnist.pdparams')

#创建模型    
model = MNIST()
#启动训练过程
train(model)

W0622 19:15:00.799525 101 device_context.cc:447] Please NOTE: device: 0, GPU Compute Capability: 8.0, Driver API Version: 11.2, Runtime API Version: 11.2

W0622 19:15:00.803115 101 device_context.cc:465] device: 0, cuDNN Version: 8.2.

epoch: 0, batch: 0, loss is: [2.9631767]

epoch: 0, batch: 200, loss is: [0.28771558]

epoch: 0, batch: 400, loss is: [0.17397963]

epoch: 0, batch: 600, loss is: [0.1328078]

epoch: 0, batch: 800, loss is: [0.22312419]...

epoch: 9, batch: 0, loss is: [0.04752279]

epoch: 9, batch: 200, loss is: [0.02498975]

epoch: 9, batch: 400, loss is: [0.0057624]

epoch: 9, batch: 600, loss is: [0.0295256]

epoch: 9, batch: 800, loss is: [0.06521191]

虽然上述训练过程的**损失明显比使用均方误差算法要小,但因为损失函数量纲的变化,我们无法从比较两个不同的损失函数得出谁更加优秀**。怎么解决这个问题呢?我们可以回归到问题的本质,谁的**分类准确率更高**来判断。在后面介绍完计算准确率和作图的内容后,读者可以自行测试采用不同损失函数下,模型准确率的高低**

至此,大家阅读论文中常见的一些分类任务模型图就清晰明了,如全连接神经网络、卷积神经网络,在模型的最后阶段,都是使用Softmax进行处理。

由于我们修改了模型的输出格式,因此使用模型做预测时的代码也需要做相应的调整。从**模型输出10个标签的概率中选择最大的,将其标签编号输出**

In [8]

from PIL import Image

# 读取一张本地的样例图片,转变成模型输入的格式
def load_image(img_path):
    # 从img_path中读取图像,并转为灰度图
    im = Image.open(img_path).convert('L')
    im = im.resize((28, 28), Image.ANTIALIAS)
    im = np.array(im).reshape(1, 1, 28, 28).astype(np.float32)
    # 图像归一化
    im = 1.0 - im / 255.
    return im

# 定义预测过程
model = MNIST()
params_file_path = 'mnist.pdparams'
img_path = 'work/example_0.jpg'
# 加载模型参数
param_dict = paddle.load(params_file_path)
model.load_dict(param_dict)
# 灌入数据
model.eval()
tensor_img = load_image(img_path)
#模型反馈10个分类标签的对应概率
results = model(paddle.to_tensor(tensor_img))
#取概率最大的标签作为预测输出
lab = np.argsort(results.numpy())
print("本次预测的数字是: ", lab[0][-1])

本次预测的数字是: 0

作业 2-2

预习下对于计算机视觉任务,有哪些常见的卷积神经网络(如LeNet-5、AlexNet等)?

相关实践学习
【文生图】一键部署Stable Diffusion基于函数计算
本实验教你如何在函数计算FC上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。函数计算提供一定的免费额度供用户使用。本实验答疑钉钉群:29290019867
建立 Serverless 思维
本课程包括: Serverless 应用引擎的概念, 为开发者带来的实际价值, 以及让您了解常见的 Serverless 架构模式
相关文章
|
12月前
|
机器学习/深度学习 计算机视觉
深度学习常见的损失函数
深度学习常见的损失函数
134 1
深度学习常见的损失函数
|
2月前
|
机器学习/深度学习 算法 数据挖掘
深度学习中常用损失函数介绍
选择正确的损失函数对于训练机器学习模型非常重要。不同的损失函数适用于不同类型的问题。本文将总结一些常见的损失函数,并附有易于理解的解释、用法和示例
45 0
深度学习中常用损失函数介绍
|
4月前
|
机器学习/深度学习 人工智能 测试技术
深度学习中损失函数和激活函数的选择
深度学习中损失函数和激活函数的选择
44 0
|
机器学习/深度学习 PyTorch 算法框架/工具
深度学习05】 交叉熵损失函数
不同的信息,含有不同的信息量,假设下列对阵表中阿根廷的夺冠概率是1/8,A同学告诉我阿根廷夺冠了,那么这个信息量就很大了(因为它包括了阿根廷进了四强,决赛);B同学告诉我阿根廷进决赛了,那么这个信息量就较小。
117 0
|
机器学习/深度学习 文字识别 算法
深度学习基础5:交叉熵损失函数、MSE、CTC损失适用于字识别语音等序列问题、Balanced L1 Loss适用于目标检测
深度学习基础5:交叉熵损失函数、MSE、CTC损失适用于字识别语音等序列问题、Balanced L1 Loss适用于目标检测
|
机器学习/深度学习 搜索推荐 PyTorch
机器学习/深度学习中的常用损失函数公式、原理与代码实践(持续更新ing...)
本文的结构是首先介绍一些常见的损失函数,然后介绍一些个性化的损失函数实例。
机器学习/深度学习中的常用损失函数公式、原理与代码实践(持续更新ing...)
|
机器学习/深度学习 人工智能 自然语言处理
深度学习与CV教程(3) | 损失函数与最优化
本文讲解了损失函数(数据损失与正则损失)、多类 SVM 损失与Softmax损失比较、梯度计算方法(数值梯度与解析梯度)、梯度下降优化算法等【对应 CS231n Lecture 3】
1506 3
深度学习与CV教程(3) | 损失函数与最优化
|
机器学习/深度学习 存储 人工智能
【深度学习之美】损失函数减肥用,神经网络调权重(入门系列之六)
有人开玩笑说,男人有两大烦恼:一是把别人的肚子搞大了,二是把自己的肚子搞大了。对于后者,除了减(jian)肥(shen),似乎别无他法。可你知道吗?这减肥背后的机理,和前馈神经网络利用损失函数,来反向调节各个神经元之间的连接权重,其实是一样一样的。为啥咧?
2992 1