• 第四章 深度学习中的损失函数(工具)


    概述

    官网:torch.nn - PyTorch中文文档 (pytorch-cn.readthedocs.io)

    损失函数torch.nn用途特点应用场景
    交叉熵损失CrossEntropyLoss多分类问题当模型对真实类别的预测概率低时,损失迅速增加适用于分类任务,特别是输出层使用Softmax函数时
    二元交叉熵损失BCELoss
    BCEWithLogitsLoss
    二分类问题计算模型预测的概率与实际标签之间的差异常用于二分类任务,如垃圾邮件检测、医学诊断等
    负对数似然损失NLLLoss多分类问题需要在应用此损失之前对模型输出应用Softmax函数与Softmax一起使用时,适用于多类分类任务
    均方误MSELoss回归任务对异常值非常敏感,误差的平方随着误差的增加而增加适用于需要预测连续数值的任务,如房价预测、股价预测等
    平均绝对误差损失L1Loss回归任务相比于MSE,对异常值不那么敏感当数据含有异常值时,通常优于MSE

    解析CrossEntropyLoss

    0.Quick Start

    简单定义两个Tensor,其中pre为模型的预测值,tgt为类别真实标签,采用one-hot形式表示。

    import torch.nn as nn
    loss_func = nn.CrossEntropyLoss()
    pre = torch.tensor([0.8, 0.5, 0.2, 0.5], dtype=torch.float)
    tgt = torch.tensor([1, 0, 0, 0], dtype=torch.float)
    print(loss_func(pre, tgt))
    
    • 1
    • 2
    • 3
    • 4
    • 5

    输出为:

    tensor(1.1087)
    
    • 1

    1.参数

    torch.nn.CrossEntropyLoss(weight=None,size_average=None,ignore_index=-100,reduce=None,reduction=‘mean’,label_smoothing=0.0)

    最常用的参数为 reduction(str, optional) ,可设置其值为 mean, sum, none ,默认为 mean。该参数主要影响多个样本输入时,损失的综合方法。mean表示损失为多个样本的平均值,sum表示损失的和,none表示不综合。其他参数读者可查阅官方文档。

    loss_func_none = nn.CrossEntropyLoss(reduction="none")
    loss_func_mean = nn.CrossEntropyLoss(reduction="mean")
    loss_func_sum = nn.CrossEntropyLoss(reduction="sum")
    pre = torch.tensor([[0.8, 0.5, 0.2, 0.5],
                        [0.2, 0.9, 0.3, 0.2],
                        [0.4, 0.3, 0.7, 0.1],
                        [0.1, 0.2, 0.4, 0.8]], dtype=torch.float)
    tgt = torch.tensor([[1, 0, 0, 0],
                        [0, 1, 0, 0],
                        [0, 0, 1, 0],
                        [0, 0, 0, 1]], dtype=torch.float)
    print(loss_func_none(pre, tgt))
    print(loss_func_mean(pre, tgt))
    print(loss_func_sum(pre, tgt))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    输出为:

    tensor([1.1087, 0.9329, 1.0852, 0.9991])
    tensor(1.0315)
    tensor(4.1259)
    
    • 1
    • 2
    • 3

    2.计算过程

    我们还是使用Quick Start中的例子。

    loss_func = nn.CrossEntropyLoss()
    pre = torch.tensor([0.8, 0.5, 0.2, 0.5], dtype=torch.float)
    tgt = torch.tensor([1, 0, 0, 0], dtype=torch.float)
    print("手动计算:")
    print("1.softmax")
    print(torch.softmax(pre, dim=-1))
    print("2.取对数")
    print(torch.log(torch.softmax(pre, dim=-1)))
    print("3.与真实值相乘")
    print(-torch.sum(torch.mul(torch.log(torch.softmax(pre, dim=-1)), tgt), dim=-1))
    print()
    print("调用损失函数:")
    print(loss_func(pre, tgt))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    输出为:

    手动计算:
    1.softmax
    tensor([0.3300, 0.2445, 0.1811, 0.2445])
    2.取对数
    tensor([-1.1087, -1.4087, -1.7087, -1.4087])
    3.与真实值相乘
    tensor(1.1087)
    
    调用损失函数:
    tensor(1.1087)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    由此可见:

    ①交叉熵损失函数会自动对输入模型的预测值进行softmax。因此在多分类问题中,如果使用nn.CrossEntropyLoss(),则预测模型的输出层无需添加softmax层。

    ②nn.CrossEntropyLoss()=nn.LogSoftmax()+nn.NLLLoss().

    其实官方文档中说的很明白了:

    The input is expected to contain the unnormalized logits for each class (which donotneed to be positive or sum to 1, in general)
    Note that this case is equivalent to the combination of LogSoftmax and NLLLoss.

    3.损失函数输入及输出的Tensor形状

    为了直观显示函数输出结果,我们将参数reduction设置为none。此外pre表示模型的预测值,为4*4的Tensor,其中的每行表示某个样本的类别预测(4个类别);tgt表示样本类别的真实值,有两种表示形式,一种是类别的index,另一种是one-hot形式。

    loss_func = nn.CrossEntropyLoss(reduction="none")
    pre_data = torch.tensor([[0.8, 0.5, 0.2, 0.5],
                             [0.2, 0.9, 0.3, 0.2],
                             [0.4, 0.3, 0.7, 0.1],
                             [0.1, 0.2, 0.4, 0.8]], dtype=torch.float)
    tgt_index_data = torch.tensor([0,
                                   1,
                                   2,
                                   3], dtype=torch.long)
    tgt_onehot_data = torch.tensor([[1, 0, 0, 0],
                                    [0, 1, 0, 0],
                                    [0, 0, 1, 0],
                                    [0, 0, 0, 1]], dtype=torch.float)
    print("pre_data: {}".format(pre_data.size()))
    print("tgt_index_data: {}".format(tgt_index_data.size()))
    print("tgt_onehot_data: {}".format(tgt_onehot_data.size()))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    输出为:

    pre_data: torch.Size([4, 4])
    tgt_index_data: torch.Size([4])
    tgt_onehot_data: torch.Size([4, 4])
    
    • 1
    • 2
    • 3

    3.1简单情况(一个样本)

    构造数据:

    pre = pre_data[0]
    tgt_index = tgt_index_data[0]
    tgt_onehot = tgt_onehot_data[0]
    print(pre)
    print(tgt_index)
    print(tgt_onehot)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    输出为:

    tensor([0.8000, 0.5000, 0.2000, 0.5000])
    tensor(0)
    tensor([1., 0., 0., 0.])
    
    • 1
    • 2
    • 3

    **pre形状为Tensor©;两种tgt的形状分别为Tensor(), Tensor© 。**此时①手动计算损失②损失函数+tgt_index形式③损失函数+tgt_onehot形式:

    print(-torch.sum(torch.mul(torch.log(torch.softmax(pre, dim=-1)), tgt_onehot), dim=-1))
    print(loss_func(pre, tgt_index))
    print(loss_func(pre, tgt_onehot))
    
    • 1
    • 2
    • 3

    输出为:

    tensor(1.1087)
    tensor(1.1087)
    tensor(1.1087)
    
    • 1
    • 2
    • 3

    可见torch.nn.CrossEntropyLoss()接受两种形式的标签输入,一种是类别index,一种是one-hot形式,官方文档中的描述是:

    Input: Shape ©, (N,C) or (N,C,d1,d2,…,dK) with K≥1 in the case of K-dimensional loss.
    Target: If containing class indices, shape (), (N) or(N,d1​,d2​,…,dK​) with K≥1 in the case of K-dimensional loss where each value should be between [0,C). If containing class probabilities, same shape as the input and each value should be between [0,1].

    3.2多个样本(一个batch)

    构造数据:

    pre = pre_data[0:2]
    tgt_index = tgt_index_data[0:2]
    tgt_onehot = tgt_onehot_data[0:2]
    print(pre)
    print(tgt_index)
    print(tgt_onehot)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    输出为:

    tensor([[0.8000, 0.5000, 0.2000, 0.5000],
            [0.2000, 0.9000, 0.3000, 0.2000]])
    tensor([0, 1])
    tensor([[1., 0., 0., 0.],
            [0., 1., 0., 0.]])
    
    • 1
    • 2
    • 3
    • 4
    • 5

    **pre形状为Tensor(N, C);两种tgt的形状分别为Tensor(N), Tensor(N, C) 。**此时①手动计算损失②损失函数+tgt_index形式③损失函数+tgt_onehot形式:

    print(-torch.sum(torch.mul(torch.log(torch.softmax(pre, dim=-1)), tgt_onehot), dim=-1))
    print(loss_func(pre, tgt_index))
    print(loss_func(pre, tgt_onehot))
    
    • 1
    • 2
    • 3

    输出为:

    tensor([1.1087, 0.9329])
    tensor([1.1087, 0.9329])
    tensor([1.1087, 0.9329])
    
    • 1
    • 2
    • 3

    3.3三维情况(多样本+多通道)

    构造数据:

    pre = torch.stack((pre_data[0:2].transpose(0, 1), pre_data[2:4].transpose(0, 1)))
    tgt_index = torch.stack((tgt_index_data[0:2], tgt_index_data[2:4]))
    tgt_onehot = torch.stack((tgt_onehot_data[0:2].transpose(0, 1), tgt_onehot_data[2:4].transpose(0, 1)))
    print(pre)
    print(tgt_index)
    print(tgt_onehot)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    输出为:

    tensor([[[0.8000, 0.2000],
             [0.5000, 0.9000],
             [0.2000, 0.3000],
             [0.5000, 0.2000]],
    
            [[0.4000, 0.1000],
             [0.3000, 0.2000],
             [0.7000, 0.4000],
             [0.1000, 0.8000]]])
    tensor([[0, 1],
            [2, 3]])
    tensor([[[1., 0.],
             [0., 1.],
             [0., 0.],
             [0., 0.]],
    
            [[0., 0.],
             [0., 0.],
             [1., 0.],
             [0., 1.]]])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    pre形状为Tensor(N, C, d);两种tgt的形状分别为Tensor(N, d), Tensor(N, C, d) 。

    在构造数据时,我们使用了stack、transpose函数,不熟悉的读者可以自行查阅,在这里我们只需要知道应用这些函数后,我们构造了三个Tensor。

    形状为(N, C, d)的Tensor有什么意义呢?N表示batch的大小,我们单独拿出batch中的一个样本,这时Tensor的形状为(C, d)。在图像分类时,我们可以用d表示不同的通道;在自然语言处理中,d可以表示句子中不同位置的词语。比如刚才我们构造的Tensor(2, 4, 2):

    tensor([[[0.8000, 0.2000],
             [0.5000, 0.9000],
             [0.2000, 0.3000],
             [0.5000, 0.2000]],
    
            [[0.4000, 0.1000],
             [0.3000, 0.2000],
             [0.7000, 0.4000],
             [0.1000, 0.8000]]])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在自然语言处理的背景下,可以将其看作两个句子。拿出第一个句子来,其形状为Tensor(4, 2):

    tensor([[0.8000, 0.2000],
             [0.5000, 0.9000],
             [0.2000, 0.3000],
             [0.5000, 0.2000]])
    
    • 1
    • 2
    • 3
    • 4

    2表示句子由两个词语组成,每一个词语的预测值形状为Tensor(4),即词表的大小为4。

    此时①手动计算损失(注意这里dim=1, 表示在第2个维度上softmax)②损失函数+tgt_index形式③损失函数+tgt_onehot形式:

    print(-torch.sum(torch.mul(torch.log(torch.softmax(pre, dim=1)), tgt_onehot), dim=1))
    print(loss_func(pre, tgt_index))
    print(loss_func(pre, tgt_onehot))
    
    • 1
    • 2
    • 3

    输出为:

    tensor([[1.1087, 0.9329],
            [1.0852, 0.9991]])
    tensor([[1.1087, 0.9329],
            [1.0852, 0.9991]])
    tensor([[1.1087, 0.9329],
            [1.0852, 0.9991]])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在三维输入Tensor(N, C, d)的情况下,torch.nn.CrossEntropyLoss()在第二个维度,也就是维度C上求CE损失,最后得到的损失输出为Tensor(N, d)。

    但通常我们的惯性思维是,一维是Tensor©,二维是Tensor(N, C),那三维自然应该是Tensor(d, N, C),这样输入CE函数后得到的loss形状为Tensor(d, N)。但是在torch.nn.CrossEntropyLoss()损失函数的背景下,这样的思维方式是错误的,一定要注意。遇见“Tensor形状不匹配”的错误还好,更可怕的是凑巧形状匹配,但计算结果怎么也不对。

    对比三种交叉熵损失函数

    首先,这几个类分别对应的函数为:

    nn.CrossEntropyLoss()` ——》`nn.functional.cross_entropy()
    nn.BCEloss()` ——》`nn.functional.binary_cross_entropy()
    nn.BCEWithLogitsLoss()` ——》`nn.functional.binary_cross_entropy_with_logits()
    
    • 1
    • 2
    • 3

    类及其对应的函数,它们的label的形式都是一致的;同时类BCEloss()和类BCEWithLogitsLoss()label形式是完全一致的。所以后面我直接用了函数形式,以此便于读者阅读。

    CrossEntropyLoss()是可以进行多分类的交叉熵。BCEloss()是指的二分类的交叉熵。首先在我的个人臆测中,如果做二分类的话,那既可以用CrossEntropyLoss()也可以用BCEloss(),即使输入方式不同,应该达到相同效果才对。

    但是实际上,CrossEntropyLoss()内部将input做了softmax后再与label进行交叉熵!BCEloss()内部啥也没干直接将inputlabel做了交叉熵!BCEWithLogitsLoss()内部将input做了sigmoid后再与label进行交叉熵!

  • 相关阅读:
    浏览器消息通知代码
    【excel】列转行
    解决AnyViewer干扰控制端输入法的问题
    《乔布斯传》英文原著重点词汇笔记(八)【 chapter six 】
    Android HIDL(1) ---- 概述
    Jeecg Online代码生成器--一对多代码生成
    双臂二指魔方机器人的制作(三)--还原控制
    【剑指 Offer II 003】前 n 个数字二进制中 1 的个数 c++
    Vue 3.0 中重置 reactive 定义的响应式对象数据,恢复为初始值
    路由是什么
  • 原文地址:https://blog.csdn.net/weixin_44302770/article/details/134470321