LLM系列:2.pytorch入门:8.神经网络的损失函数(criterion)

神经网络的损失函数(criterion)

一. 机器学习中的优化思想

在神经网络中,数据流向通常是从左至右的"正向传播",但学习过程则是通过比较"预测值"与"真实值"的差异,通过"反向传播"来调整权重。这个衡量差异的指标,就是损失函数。

1. 核心逻辑

神经网络的训练本质上是一个寻找目标函数最小值的过程:

  1. 正向传播:输入 X→w,bz→f(z)y^X \xrightarrow{w,b} z \xrightarrow{f(z)} \hat{y}Xw,bzf(z)y^(预测值)。
  2. 计算损失:使用损失函数 L(y^,y)L(\hat{y}, y)L(y^,y) 评估预测差距。
  3. 反向传播:根据损失值计算梯度,利用优化算法迭代更新权重 w,bw, bw,b

2. 优化流程复习

image-20260424163500258
  1. 提出基本模型,明确目标

    我们的基本模型就是我们自建的神经网络架构,我们需要求解的就是神经网络架构中的权重向量 www

  2. 确定损失函数/目标函数

    我们需要定义某个评估指标,用以衡量模型权重为 www 的情况下,预测结果与真实结果的差异。当真实值与预测值差异越大时,我们就认为神经网络学习过程中丢失了许多信息,丢失的这部分被形象地称为"损失",因此评估真实值与预测值差异的函数被我们称为"损失函数"。

    关键概念:损失函数

    衡量真实值与预测结果的差异,评价模型学习过程中产生的损失的函数。
    在数学上,表示为以需要求解的权重向量 www 为自变量的函数 L(w)L(w)L(w)
    如果损失函数的值很小,则说明模型预测值与真实值很接近,模型在数据集上表现优异,权重优秀
    如果损失函数的值大,则说明模型预测值与真实值差异很大,模型在数据集上表现差劲,权重糟糕

    我们希望损失函数越小越好,以此,我们将问题转变为求解函数 L(w)L(w)L(w) 的最小值所对应的自变量 www。但是,损失函数往往不是一个简单的函数,求解复杂函数就需要复杂的数学工具。在这里,我们使用的数学工具可能包含两部分:

    • 将损失函数 L(w)L(w)L(w) 转变成凸函数的数学方法,常见的有拉格朗日变换等
    • 在凸函数上求解 L(w)L(w)L(w) 的最小值对应的 www 的方法,也就是以梯度下降为代表的优化算法
  3. 确定适合的优化算法

  4. 利用优化算法,最小化损失函数,求解最佳权重 www (训练)

之前我们在线性回归上走过这个全流程。对线性回归,我们的损失函数是SSE,优化算法是最小二乘法和梯度下降法,两者都是对机器学习来说非常重要的优化算法。但遗憾的是,最小二乘法作为入门级优化算法,有较多的假设和先决条件,不足以应对神经网络需要被应用的各种复杂环境。梯度下降法应用广泛,不过也有很多问题需要改进。接下来,我将主要以分类深层神经网络为例来介绍神经网络中所使用的入门级损失函数及优化算法。

二. 回归任务:误差平方和(SSE)

1. 理论基础

(1).误差平方和-SSE

对于回归问题(预测连续数值),最基础的损失函数是误差平方和。

SSE=∑i=1m(yi−y^i)2SSE = \sum_{i=1}^{m}(y_i - \hat{y}_i)^2SSE=i=1m(yiy^i)2

(2).均方误差-MSE

在 PyTorch 的官方类中,常用的是 MSE (Mean Squared Error),即均方误差:

MSE=1m∑i=1m(yi−y^i)2MSE = \frac{1}{m}\sum_{i=1}^{m}(y_i - \hat{y}_i)^2MSE=m1i=1m(yiy^i)2

2. nn.MSELoss类 - 均方误差损失函数(L2 Loss)

nn.MSELoss类同样是nn.Module类的子类

(1).nn.MSELoss类类型

作用:

nn.MSELoss 是 PyTorch 中最基础、最常用的损失函数之一,用于计算均方误差 (Mean Squared Error, 也就是平方 L2 范数)。它主要用于评估回归任务 (Regression) 中,模型预测值 y^\hat{y}y^ 与真实标签 yyy 之间的差异程度。损失越小,代表模型的预测越精准。

torch.nn.MSELoss(reduction='mean')

核心参数:

  • 聚合方式 (reduction):决定了计算出每个样本的误差后,如何对整批数据输出最终的 Loss 结果。支持三种字符串输入:
    • 'mean' (默认值): 计算所有元素误差的平均值。工业界最常用,因为 Loss 的大小不会随着 Batch Size 的改变而剧烈波动,便于设置固定的学习率。
    • 'sum': 计算所有元素误差的总和。这在数学上完全等价于我们之前学线性回归时提到的 SSE (误差平方和)。
    • 'none': 不进行任何聚合运算,直接返回与输入形状完全相同的张量,里面包含着每个样本各自的误差值。(常用于极其复杂的自定义求导机制中)。

理论与底层代码的对齐 (极其重要):

在理论推导中,均方误差的公式为:

MSE=1n∑i=1n(y^i−yi)2MSE = \frac{1}{n}\sum_{i=1}^{n}(\hat{y}_i - y_i)^2MSE=n1i=1n(y^iyi)2

在 PyTorch 底层,由于 xxx (即预测值 y^\hat{y}y^) 和 yyy (真实值) 通常是以 Batch 形式传入的张量,底层计算逻辑为:

Batch=NBatch = NBatch=N

l(x,y)=L={l1,…,lN}⊤,ln=(xn−yn)2l(x, y) = L = \{l_1,\dots,l_N\}^\top, \quad l_n = \left( x_n - y_n \right)^2l(x,y)=L={l1,,lN},ln=(xnyn)2

reduction='mean',则最终输出标量:ℓ(x,y)=mean⁡(L)\ell(x, y) = \operatorname{mean}(L)(x,y)=mean(L)

reduction='sum',则最终输出标量:ℓ(x,y)=sum⁡(L)\ell(x, y) = \operatorname{sum}(L)(x,y)=sum(L)

补充 (史诗级避坑警告):

张量形状的隐式广播陷阱 (Broadcasting Trap):

在使用 nn.MSELoss 时,预测值张量 y_hat 和真实值张量 y 的形状 (Shape) 必须 100% 严格一致!

假设你的网络输出 y_hat 形状是 [500, 1],但你的真实标签 y 是一维向量 [500]。当你把它们同时扔进 MSELoss 时,PyTorch 绝对不会报错! > 它会极其"自作聪明"地触发广播机制:把两者都扩展成 [500, 500] 的巨大矩阵,然后算出一个完全荒谬的错误 Loss,最后把你的网络梯度彻底带偏!

工业界铁律:在计算 Loss 之前,永远习惯性地执行一步 y = y.view(-1, 1) 或者保证 y_hat.shape == y.shape

(2). __call__ - 实例调用(执行损失计算)

作用:接收模型的预测结果和真实的标签数据,执行前向传播计算出 Loss 值,并为其在底层构建好用于反向传播(.backward())的动态计算图。同理,作为 nn.Module 的子类,我们严禁直接调用 .forward(y_hat, y),必须直接把实例对象当作函数调用。

我们常定义nn.MSELoss类型对象叫criterion或loss_fn

注意真实值和预测值位置,在MSE没有影响,但其他的损失函数可能会导致最终结果错误。

# 实例化后,直接像调用函数一样调用该对象
criterion(input, target)

参数:

  • 预测张量 (input): 模型前向传播输出的预测结果 (Tensor),即 y^\hat{y}y^。要求必须是浮点型 (float32 等)。
  • 真实张量 (target): 也就是真实标签数据 (Tensor),即 yyy。要求与 input 形状绝对相同,且同为浮点型。

返回值:

  • 成功: 如果 reduction'mean''sum',返回一个 0 维的标量张量 (0-D Tensor),代表全局的损失值。并且自动带有 grad_fn=<MseLossBackward0>,代表它已接入计算图,随时可以求导。

示例:

# 1. 模拟数据准备 (假装这是网络的输出和真实的标签)
# 注意:一定要保证形状都是 [3, 1],且都是 float 浮点型!
y_hat = torch.tensor([[1.5], 
                      [2.0], 
                      [3.8]], dtype=torch.float32)
                      
y_true = torch.tensor([[1.0], 
                       [2.0], 
                       [3.0]], dtype=torch.float32)

# 2. 实例化损失函数 (通常习惯命名为 criterion 或 loss_fn)
# 我们实例化两个不同 reduction 模式的 loss 做对比
criterion_mean = nn.MSELoss(reduction='mean')
criterion_sum = nn.MSELoss(reduction='sum')

# 3. 计算损失:触发 __call__ 
# 底层计算: (1.5-1)^2 + (2-2)^2 + (3.8-3)^2 = 0.25 + 0 + 0.64 = 0.89
loss_mean = criterion_mean(y_hat, y_true)  # mean: 0.89 / 3 ≈ 0.2967
loss_sum = criterion_sum(y_hat, y_true)    # sum: 0.89

print("Mean MSE Loss:", loss_mean)
# 返回: tensor(0.2967)

print("Sum MSE Loss (SSE):", loss_sum)
# 返回: tensor(0.8900)

# 验证自动求导图是否连接
print("是否接入计算图:", loss_mean.requires_grad) 
# 返回: False (因为我们的假数据没设requires_grad=True,若y_hat是网络生成的,loss_mean.requires_grad就是True)

三. 二分类任务:交叉熵损失(BCE)

1. 理论基础

(1).交叉熵损失(BCE)

二分类交叉熵损失函数(Binary Cross Entropy Loss),也叫做对数损失(log loss)。这个损失函数被广泛地使用在任何输出结果是二分类的神经网络中,即不止限于单层神经网络,还可被拓展到多分类中,因此理解二分类交叉熵损失是非常重要的一环。大多数时候,除非特殊声明为二分类,否则提到交叉熵损失,我们会默认算法的分类目标是多分类。

二分类交叉熵损失函数是由极大似然估计推导出来的,对于有m个样本的数据集而言,在全部样本上的平均损失写作:

L(w)=−∑i=1m(yi∗ln⁡(σi)+(1−yi)∗ln⁡(1−σi))L(w) = - \sum_{i=1}^{m} (y_i * \ln(\sigma_i) + (1 - y_i) * \ln(1 - \sigma_i))L(w)=i=1m(yiln(σi)+(1yi)ln(1σi))

在单个样本的损失写作:

L(w)i=−(yi∗ln⁡(σi)+(1−yi)∗ln⁡(1−σi))L(w)_i = -(y_i * \ln(\sigma_i) + (1 - y_i) * \ln(1 - \sigma_i))L(w)i=(yiln(σi)+(1yi)ln(1σi))

其中,www表示求解出来的一组权重(在等号的右侧,wwwσ\sigmaσ里),mmm是样本的个数,yiy_iyi是样本iii上真实的标签,σi\sigma_iσi是样本iii上,基于参数www计算出来的sigmoid函数的返回值,xix_ixi是样本iii各个特征的取值。我们的目标,就是求解出使L(w)L(w)L(w)最小的www取值。注意,在神经网络中,特征张量X是自变量,权重是www。但在损失函数中,权重www是损失函数的自变量,特征xxx和真实标签yyy都是已知的数据,相当于是常数。不同的函数中,自变量和参数各有不同,因此大家需要在数学计算中,尤其是求导的时候避免混淆。


(2). 极大似然估计求解二分类交叉熵损失

极大似然估计(Maximum Likelihood Estimate, MLE)的感性认识

如果一个事件的发生概率很大,那这个事件应该很容易发生。相应的,如果依赖于权重www的任意事件的发生就是我们的目标,那我们只要寻找令其发生概率最大化的权重www就可以了。寻找相应的权重www,使得目标事件的发生概率最大,就是极大似然估计的基本方法。
其步骤如下:
1、构筑似然函数P(w)P(w)P(w),用于评估目标事件发生的概率,该函数被设计成目标事件发生时,概率最大
2、对整体似然函数取对数,构成对数似然函数ln⁡P(w)\ln P(w)lnP(w)
3、在对数似然函数上对权重www求导,并使导数为0,对权重进行求解

**在二分类的例子中,我们的"任意事件"就是每个样本的分类都正确,对数似然函数的负数就是我们的损失函数。**接下来,我们来看看逻辑回归的对数似然函数是怎样构筑的。

  1. 构筑对数似然函数

    二分类神经网络的标签是[0,1],此标签服从伯努利分布(即0-1分布),因此可得:

    样本iii在由特征向量xi\boldsymbol{x}_ixi和权重向量w\boldsymbol{w}w组成的预测函数中,样本标签被预测为1的概率为:

    P1=P(y^i=1∣xi,w)=σP_1 = P(\hat{y}_i = 1 | \boldsymbol{x}_i, \boldsymbol{w}) = \sigmaP1=P(y^i=1∣xi,w)=σ

    对二分类而言,σ\sigmaσ就是sigmoid函数的结果。

    样本iii在由特征向量xi\boldsymbol{x}_ixi和权重向量w\boldsymbol{w}w组成的预测函数中,样本标签被预测为0的概率为:

    P0=P(y^i=0∣xi,w)=1−σP_0 = P(\hat{y}_i = 0 | \boldsymbol{x}_i, \boldsymbol{w}) = 1 - \sigmaP0=P(y^i=0∣xi,w)=1σ

    P1P_1P1的值为1的时候,代表样本iii的标签被预测为1,当P0P_0P0的值为1的时候,代表样本iii的标签预测为0。P1P_1P1P0P_0P0相加是一定等于1的。

    假设样本iii的真实标签yiy_iyi为1,并且P1P_1P1也为1的话,那就说明我们将iii的标签预测为1的概率很大,与真实值一致,那模型的预测就是准确的,拟合程度很高,信息损失很少。相反,如果真实标签yiy_iyi为1,我们的P1P_1P1却很接近0,这就说明我们将iii的标签预测为1的概率很小,即与真实值一致的概率很小,那模型的预测就是失败的,拟合程度很低,信息损失很多。当yiy_iyi为0时,也是同样的道理。所以,当yiy_iyi为1的时候,我们希望P1P_1P1非常接近1,当yiy_iyi为0的时候,我们希望P0P_0P0非常接近1,这样,模型的效果就很好,信息损失就很少。

    真实标签 yi被预测为1的概率 P1被预测为0的概率 P0样本被预测为?与真实值一致吗?拟合状况信息损失
    1010
    1101
    0010
    0101

    将两种取值的概率整合,我们可以定义如下等式:

    P(y^i∣xi,w)=P1yi∗P01−yiP(\hat{y}_i | \boldsymbol{x}_i, \boldsymbol{w}) = P_1^{y_i} * P_0^{1 - y_i}P(y^ixi,w)=P1yiP01yi

    这个等式代表同时代表了P1P_1P1P0P_0P0,在数学上,它被叫做逻辑回归的假设函数

    • 当样本iii的真实标签yiy_iyi为1的时候,1−yi1 - y_i1yi就等于0,P0P_0P0的0次方就是1,所以P(y^i∣xi,w)P(\hat{y}_i | \boldsymbol{x}_i, \boldsymbol{w})P(y^ixi,w)就等于P1P_1P1,这个时候,如果P1P_1P1为1,模型的效果就很好,损失就很小。
    • 同理,当yiy_iyi为0的时候,P(y^i∣xi,w)P(\hat{y}_i | \boldsymbol{x}_i, \boldsymbol{w})P(y^ixi,w)就等于P0P_0P0,此时如果P0P_0P0非常接近1,模型的效果就很好,损失就很小。

    所以,为了达成让模型拟合好,损失小的目的,我们每时每刻都希望P(y^i∣xi,w)P(\hat{y}_i | \boldsymbol{x}_i, \boldsymbol{w})P(y^ixi,w)的值等于1。而P(y^i∣xi,w)P(\hat{y}_i | \boldsymbol{x}_i, \boldsymbol{w})P(y^ixi,w)的本质是样本由特征向量xi\boldsymbol{x}_ixi和权重w\boldsymbol{w}w组成的预测函数中,预测出所有可能的y^i\hat{y}_iy^i的概率,因此1是它的最大值。也就是说,每时每刻,我们都在追求P(y^i∣xi,w)P(\hat{y}_i | \boldsymbol{x}_i, \boldsymbol{w})P(y^ixi,w)的最大值。而寻找相应的参数www,使得每次得到的预测概率最大,正是极大似然估计的基本方法,不过P(y^i∣xi,w)P(\hat{y}_i | \boldsymbol{x}_i, \boldsymbol{w})P(y^ixi,w)是对单个样本而言的,因此我们需要将其拓展到多个样本上。

    P(y^i∣xi,w)P(\hat{y}_i | \boldsymbol{x}_i, \boldsymbol{w})P(y^ixi,w)是对单个样本而言的函数,对一个训练集的m个样本来说,我们可以定义如下等式来表达所有样本在特征张量X和权重向量w\boldsymbol{w}w组成的预测函数中,预测出所有可能的y^\hat{y}y^的概率P为:

    P=∏i=1mP(y^i∣xi,w)P = \prod_{i=1}^{m} P(\hat{y}_i | \boldsymbol{x}_i, \boldsymbol{w})P=i=1mP(y^ixi,w)
    =∏i=1m(P1yi∗P01−yi)= \prod_{i=1}^{m} (P_1^{y_i} * P_0^{1 - y_i})=i=1m(P1yiP01yi)
    =∏i=1m(σiyi∗(1−σi)1−yi)= \prod_{i=1}^{m} (\sigma_i^{y_i} * (1 - \sigma_i)^{1 - y_i})=i=1m(σiyi(1σi)1yi)

    这个函数就是逻辑回归的似然函数

  2. 构成对数似然函数

    对该概率P取以e为底的对数,再由 log⁡(A∗B)=log⁡A+log⁡B\log(A*B) = \log A + \log Blog(AB)=logA+logBlog⁡AB=Blog⁡A\log A^B = B \log AlogAB=BlogA 可得到逻辑回归的对数似然函数:

    ln⁡P=ln⁡∏i=1m(σiyi∗(1−σi)1−yi)\ln P = \ln \prod_{i=1}^{m} (\sigma_i^{y_i} * (1 - \sigma_i)^{1 - y_i})lnP=lni=1m(σiyi(1σi)1yi)
    =∑i=1mln⁡(σiyi∗(1−σi)1−yi)= \sum_{i=1}^{m} \ln(\sigma_i^{y_i} * (1 - \sigma_i)^{1 - y_i})=i=1mln(σiyi(1σi)1yi)
    =∑i=1m(ln⁡σiyi+ln⁡(1−σi)1−yi)= \sum_{i=1}^{m} (\ln \sigma_i^{y_i} + \ln(1 - \sigma_i)^{1 - y_i})=i=1m(lnσiyi+ln(1σi)1yi)
    =∑i=1m(yi∗ln⁡(σi)+(1−yi)∗ln⁡(1−σi))= \sum_{i=1}^{m} (y_i * \ln(\sigma_i) + (1 - y_i) * \ln(1 - \sigma_i))=i=1m(yiln(σi)+(1yi)ln(1σi))

    这就是我们的二分类交叉熵函数。为了数学上的便利以及更好地定义"损失"的含义,我们希望将极大值问题转换为极小值问题,因此我们对ln⁡P\ln PlnP取负,并且让权重www作为函数的自变量,就得到了我们的损失函数L(w)L(w)L(w)

    L(w)=−∑i=1m(yi∗ln⁡(σi)+(1−yi)∗ln⁡(1−σi))L(w) = - \sum_{i=1}^{m} (y_i * \ln(\sigma_i) + (1 - y_i) * \ln(1 - \sigma_i))L(w)=i=1m(yiln(σi)+(1yi)ln(1σi))

    现在,我们已经将模型拟合中的"最小化损失"问题,转换成了对函数求解极值的问题。这就是一个,基于逻辑回归的返回值σi\sigma_iσi的概率性质以及极大似然估计得出的损失函数。在这个函数上,我们只要追求最小值,就能让模型在训练数据上的拟合效果最好,损失最低。

  3. 求导

    下一节讲这么求最小值。


2. nn.BCEWithLogitsLoss类 (与nn.BCELoss) - 二元交叉熵损失函数

二者同样是 nn.Module 类的子类。在工业界实战中,强烈建议直接无脑使用 nn.BCEWithLogitsLoss

(1). nn.BCEWithLogitsLoss类 & nn.BCELoss类类型

作用:

在 PyTorch 中,处理二分类交叉熵有两个高频使用的类,它们的核心区别在于"是否自带 Sigmoid 激活函数":

  • nn.BCELoss (基础版): 输入张量input必须是已经经过 Sigmoid 处理后的概率值(严格限制在 0~1 之间),因此需要输入sigma与真实标签,且顺序不能变化。。
  • nn.BCEWithLogitsLoss (工业界首选): 输入张量input是未经任何激活函数处理的原始输出zzz (也称为 Logits)。它在底层将 Sigmoid 操作和 BCE 计算安全地合并在了一起,因此需要输入zhat与真实标签,且顺序不能变化,zhat必须在前。。

同时,这两个函数都要求预测值与真实标签的数据类型以及结构(shape)必须相同,否则运行就会报错。

# 基础版
torch.nn.BCELoss(weight=None, reduction='mean')
# 进阶版 (推荐)
torch.nn.BCEWithLogitsLoss(weight=None, reduction='mean', pos_weight=None)

核心参数:

  • 样本权重 (weight): 传入一个与当前Batch形状匹配的张量,用于为 Batch 中的每个独立样本单独分配权重。例: 如果你确定某些样本是强噪声数据,可以给它们极低的权重;或者某些样本的数据来源极度可靠,可以给它们更高的权重。默认为不加权 (None)。
  • 聚合方式 (reduction): 决定了对整批数据的 Loss 结果如何聚合,支持三种字符串输入:
    • 'mean' (默认,求均值)
    • 'sum' (求误差总和)
    • 'none' (不聚合,返回原形状张量)。
  • 正样本权重 (pos_weight):(仅 BCEWithLogitsLoss 支持)。传入一个一维张量,专门用于处理正负样本严重不平衡的情况。比如欺诈检测中正样本极少,可以通过设置该值大于 1 来加大对正样本分类错误的惩罚力度。

理论与底层代码的对齐 (极其重要):

在理论推导中,损失公式为:

L(w)=−∑i=1m(yi∗ln⁡(σi)+(1−yi)∗ln⁡(1−σi))L(w) = - \sum_{i=1}^{m} (y_i * \ln(\sigma_i) + (1 - y_i) * \ln(1 - \sigma_i))L(w)=i=1m(yiln(σi)+(1yi)ln(1σi))

BCEWithLogitsLoss底层利用了对数指数技巧 (Log-Sum-Exp) 进行了数学化简。这有效避免了由于 zzz 的极大或极小导致 Sigmoid 结果直接四舍五入变成 0 或 1,从而在求 log⁡(0)\log(0)log(0) 时触发数学溢出(NaN 错误)的问题。这也是为什么它的数值稳定性远高于先算 Sigmoid 再算 BCELoss 的原因。

(2). __call__ - 实例调用(执行损失计算)

作用:接收预测结果和真实的标签数据,执行损失计算,并接入自动求导计算图。

我们常定义损失函数对象叫 criterion 或 loss_fn。
强烈注意:在 BCE 中,必须严格遵守 criterion(预测值, 真实值) 的传参顺序,绝不能像算 MSE 一样瞎传,传反会导致完全荒谬的对数计算!

# 实例化后,直接像调用函数一样调用该对象
criterion(input, target)

参数:

  • 预测张量 (input): 模型前向传播输出的结果。若用 BCELoss,它必须介于 0~1 之间;若用 BCEWithLogitsLoss,它可以是任意实数。要求必须是浮点型 (float32)。
  • 真实张量 (target): 二分类真实标签数据(全为 0 或 1)。要求与 input 形状绝对相同,且同为浮点型 (float32)。

返回值:

  • 成功: 如果 reduction'mean''sum',返回一个带有 grad_fn 的 0 维标量张量 (0-D Tensor),代表全局的损失值。

示例:

import torch
import torch.nn as nn

# 模拟数据准备
# 假设这是网络最后线性层的原始输出 (Logits),未经过Sigmoid 处理!
# 注意形状 [3, 1],且必须是 float 浮点型
z = torch.tensor([[1.2],
                  [-0.5],
                  [2.8]], dtype=torch.float32)

# 真实的二分类标签 (0 代表负类,1 代表正类)
# 注意:即便只有0和1,也必须写成 .0 以确保是 float32!且形状同为 [3, 1]
y = torch.tensor([[1.],
                  [0.],
                  [1.]], dtype=torch.float32)

criterion = nn.BCEWithLogitsLoss(reduction='mean')
# 计算损失 (内部自动极其安全地帮你做 Sigmoid)
loss = criterion(z, y)
print(loss)
# tensor(0.2655)

补充:torch.nn.functional库中的计算函数,知道有这两个函数就行了。很少会这样用。

  • function F.binary_cross_entropy_with_logits

  • function F.binary_cross_entropy


3. 代码实现二分类交叉熵损失

(1).Tensor版本
import torch
# Loss = -(y * ln(sigma) + (1-y) * ln(1 - sigma))
# y - 真实标签
# sigma - sigmod(z)
# z = Xw
# X,w,m(样本量)

m = 3*pow(10,3)
torch.random.manual_seed(420)
X = torch.rand(size=(m,4),dtype=torch.float32)
w = torch.rand(size=(4,1),dtype=torch.float32)
y = torch.randint(low=0,high=2,size=(m,1),dtype=torch.float32)
zhat = X@w
sigma = torch.sigmoid(zhat)
# -(y * ln(sigma) + (1-y) * ln(1 - sigma))
Loss = -(y * sigma.log() + (1-y) * (1-sigma).log())
print(Loss.shape)
# torch.Size([3000, 1])
print(Loss)
# tensor([[0.3075],
#         [0.3073],
#         [0.9198],
#         ...,
#         [0.3876],
#         [0.4536],
#         [0.3442]])
(2).nn.BCEWithLogitsLoss版本
import torch
import torch.nn as nn
# Loss = -(y * ln(sigma) + (1-y) * ln(1 - sigma))
# y - 真实标签
# sigma - sigmod(z)
# z = Xw
# X,w,m(样本量)

m = 3*pow(10,3)
torch.random.manual_seed(420)
X = torch.rand(size=(m,4),dtype=torch.float32)
w = torch.rand(size=(4,1),dtype=torch.float32)
y = torch.randint(low=0,high=2,size=(m,1),dtype=torch.float32)

zhat = X@w
criterion = nn.BCEWithLogitsLoss()
loss = criterion(zhat,y)
print(loss)
# tensor(0.7962)

四. 多分类任务:交叉熵损失 (CE)

1. 理论基础

(1). 由二分类推广到多分类

二分类交叉熵损失可以被推广到多分类上,但在实际处理时,二分类与多分类却有一些关键的区别。依然使用极大似然估计的推导流程,首先我们来确定单一样本概率最大化后的似然函数。

对于多分类的状况而言,标签不再服从伯努利分布(0-1分布),因此我们可以定义,样本iii在由特征向量xi\boldsymbol{x}_ixi和权重向量w\boldsymbol{w}w组成的预测函数中,样本标签被预测为类别k的概率为:

Pk=P(y^i=k∣xi,w)=σP_k = P(\hat{y}_i = k | \boldsymbol{x}_i, \boldsymbol{w}) = \sigmaPk=P(y^i=kxi,w)=σ

对于多分类算法而言,σ\sigmaσ就是softmax函数返回的对应类别的值。

假设一种最简单的情况:我们现在有三分类[1, 2, 3],则样本i被预测为三个类别的概率分别为:

P1=P(y^i=1∣xi,w)=σ1P_1 = P(\hat{y}_i = 1 | \boldsymbol{x}_i, \boldsymbol{w}) = \sigma_1P1=P(y^i=1∣xi,w)=σ1
P2=P(y^i=2∣xi,w)=σ2P_2 = P(\hat{y}_i = 2 | \boldsymbol{x}_i, \boldsymbol{w}) = \sigma_2P2=P(y^i=2∣xi,w)=σ2
P3=P(y^i=3∣xi,w)=σ3P_3 = P(\hat{y}_i = 3 | \boldsymbol{x}_i, \boldsymbol{w}) = \sigma_3P3=P(y^i=3∣xi,w)=σ3

假设样本的真实标签为1,我们就希望P1P_1P1最大,同理,如果样本的真实标签为其他值,我们就希望其他值所对应的概率最大。在二分类中,我们将yyy(1−y)(1-y)(1y)作为概率PPP的指数,以此来融合真实标签为0和为1的两种状况。但在多分类中,我们的真实标签可能是任意整数,无法使用yyy(1−y)(1-y)(1y)这样的结构来构建似然函数。所以我们认为,如果多分类的标签也可以使用0和1来表示就好了,这样我们就可以继续使用真实标签作为指数的方式。

因此,我们对多分类的标签做出了如下变化:

image-20260425225449378

原本的真实标签y是含有[1, 2, 3]三个分类的列向量,现在我们把它变成了标签矩阵,每个样本对应一个向量(这就是独热编码one-hot也叫做亚变量)。在矩阵中,每一行依旧对应样本,但却由三分类衍生出了三个新的列,分别代表:真实标签是否等于1、等于2以及等于3。在矩阵中,我们使用“1”标出样本的真实标签的位置,使用0表示样本的真实标签不是这个标签。不难注意到,这个标签矩阵的结构其实是和softmax函数输出的概率矩阵的结构一致,并且一一对应的。

回顾下二分类的似然函数:

P(y^i∣xi,w)=P1yi∗P01−yiP(\hat{y}_i | \boldsymbol{x}_i, \boldsymbol{w}) = P_1^{y_i} * P_0^{1 - y_i}P(y^ixi,w)=P1yiP01yi

当我们把标签整合为标签矩阵后,我们就可以将单个样本在总共K个分类情况整合为以下的似然函数:

P(y^i∣xi,w)=P1yi(k=1)∗P2yi(k=2)∗P3yi(k=3)∗⋯∗PKyi(k=K)P(\hat{y}_i | \boldsymbol{x}_i, \boldsymbol{w}) = P_1^{y_{i(k=1)}} * P_2^{y_{i(k=2)}} * P_3^{y_{i(k=3)}} * \dots * P_K^{y_{i(k=K)}}P(y^ixi,w)=P1yi(k=1)P2yi(k=2)P3yi(k=3)PKyi(k=K)

其中P就是样本标签被预测为某个具体值的概率,而右上角的指数就是标签矩阵中对应的值,即这个样本的真实标签是否为当前标签的判断(是就是1,否就是0)。

注意

许多教材和公式中,都会把多分类似然函数概率的指数直接写作yiy_iyi,若你能够理解此处的指数其实是0和1( 0 - 不是真实标签,1 - 是真实标签),而不是真正的标签yiy_iyi,那直接把指数写作yiy_iyi也是没问题的。为避免混淆,在我们的总结中,还是写作yi(k=真实标签序号)y_{i(k=\text{真实标签序号})}yi(k=真实标签序号)

更具体的,小k代表y的真实取值,K代表总共有K个分类(此处不是非常严谨,按道理说若K代表总共有K个类别,则不应该再使用K代表某个具体类别,但在这里,由于我们使用的类别编号与类别本身相同,所以为了公式的简化,使用了这样不严谨的表示方式)。虽然是连乘,但对于一个样本,除了自己所在的真实类别指数yiy_iyi会是1之外,其他类别的指数都为0,所以被分类为其他类别的概率在这个式子里就都为0。所以我们可以将式子简写为:

P(y^i∣xi,w)=Pjyi(k=j), j为样本i所对应的真实标签的编号P(\hat{y}_i | \boldsymbol{x}_i, \boldsymbol{w}) = P_j^{y_{i(k=j)}}, \text{ j为样本}i\text{所对应的真实标签的编号}P(y^ixi,w)=Pjyi(k=j), j为样本i所对应的真实标签的编号

对一个训练集的m个样本来说,我们可以定义如下等式来表达所有样本在特征张量X\boldsymbol{X}X和权重向量w\boldsymbol{w}w组成的预测函数中,预测出所有可能的y^\hat{y}y^的概率P为:

P=∏i=1mP(y^i∣xi,w)P = \prod_{i=1}^{m} P(\hat{y}_i | \boldsymbol{x}_i, \boldsymbol{w})P=i=1mP(y^ixi,w)
=∏i=1mPjyi(k=j)= \prod_{i=1}^{m} P_j^{y_{i(k=j)}}=i=1mPjyi(k=j)
=∏i=1mσjyi(k=j)= \prod_{i=1}^{m} \sigma_j^{y_{i(k=j)}}=i=1mσjyi(k=j)

这就是多分类状况下的似然函数。与二分类一致,似然函数解出来后,我们需要对似然函数求对数:

ln⁡P=ln⁡∏i=1mσjyi(k=j)\ln P = \ln \prod_{i=1}^{m} \sigma_j^{y_{i(k=j)}}lnP=lni=1mσjyi(k=j)
=∑i=1mln⁡(σjyi(k=j))= \sum_{i=1}^{m} \ln(\sigma_j^{y_{i(k=j)}})=i=1mln(σjyi(k=j))
=∑i=1myi(k=j)ln⁡σi= \sum_{i=1}^{m} y_{i(k=j)} \ln \sigma_i=i=1myi(k=j)lnσi

其中σ\sigmaσ就是softmax函数返回的对应类别的值。再对整个公式取负,就得到了多分类状况下的损失函数

L(w)=−∑i=1myi(k=j)ln⁡σiL(w) = - \sum_{i=1}^{m} y_{i(k=j)} \ln \sigma_iL(w)=i=1myi(k=j)lnσi

这个函数就是我们之前提到过的交叉熵函数。不难看出,二分类的交叉熵函数其实是多分类的一种特殊情况。

L(w)=−∑i=1myi(k=j)⏟NLLLossln⁡σi⏟LogSoftmaxL(w) = \underbrace{- \sum_{i=1}^{m} y_{i(k=j)}}_{\text{NLLLoss}} \underbrace{\ln \sigma_i}_{\text{LogSoftmax}}L(w)=NLLLossi=1myi(k=j)LogSoftmaxlnσi

交叉熵函数十分特殊,虽然我们求解过程中,取对数的操作是在确定了似然函数后才进行的,但从计算结果来看,对数操作其实只对softmax函数的结果σ\sigmaσ起效。因此在实际操作中,我们把ln⁡(softmax(z))\ln(softmax(z))ln(softmax(z))这样的函数单独定义了一个功能做logsoftmax,PyTorch中可以直接通过nn.logsoftmax类调用这个功能。同时,我们把对数之外的,乘以标签、加和、取负等等过程打包起来,称之为负对数似然函数(Negative Log Likelihood function),在PyTorch中可以使用nn.NLLLoss来进行调用。也就是说,在计算损失函数时,我们不再需要使用单独的softmax函数了。


总结:

在多分类(KKK 个类别)中,我们通常使用 Softmax 函数将网络输出转化为所有类别的概率分布。多分类交叉熵(Cross Entropy)公式如下:

CE=−∑i=1Kyiln⁡(y^i)CE = -\sum_{i=1}^{K} y_i \ln(\hat{y}_i)CE=i=1Kyiln(y^i)

其中 yiy_iyi 为真实标签的 One-hot 编码(即:只有正确的类别位置上为 1,其余所有错误类别的位置上均为 0)。

由于除了正确类别之外的 yiy_iyi 都是 0,累加项中那些项直接被消除了,因此,在单个样本上该公式可以被极大地简化为:

CE=−ln⁡(y^correct)CE = -\ln(\hat{y}_{correct})CE=ln(y^correct)

核心物理意义:多分类交叉熵只关心模型对"正确类别"预测的概率。只要分配给正确类别的概率 y^correct\hat{y}_{correct}y^correct 越接近 1,−ln⁡(1)-\ln(1)ln(1) 就越接近 0,损失就越小;反之,如果分配给正确类别的概率极低,−ln⁡(0)-\ln(0)ln(0) 会趋近无穷大,产生巨大的惩罚。


(2). pytorch相关类型简介

如果只计算多分类交叉熵,直接使用nn.CrossEntropyLoss即可,nn.CrossEntropyLoss底层是LogSoftmaxNLLLoss的结合体。

在知识蒸馏、强化学习策略梯度或防止序列生成下溢出等高级场景中,我们必须拆开LogSoftmaxNLLLoss独立使用。


2. nn.LogSoftmax类 - 对数归一化指数激活函数

nn.LogSoftmax类是nn.Module类的子类

(1). nn.LogSoftmax类类型

作用:

nn.LogSoftmax 在数学上等价于先对输入张量执行 Softmax 激活,然后再对其结果取自然对数 (ln⁡\lnln)。它在底层巧妙地利用了对数运算的性质,极大程度地避免了直接计算 Softmax 时容易发生的数值上溢出 (Overflow) 或下溢出 (Underflow) 问题,其数值稳定性和计算速度远超 torch.log(torch.softmax(x))

torch.nn.LogSoftmax(dim=None)

核心参数:

  • 操作轴 (dim): 决定了沿着张量的哪一个维度去计算 Softmax 概率分布。对于深度学习中常见的 (BatchSize, Features) 形状的二维张量,通常设置为 dim=1 (即对每一行的所有类别特征计算概率分布) (int 或 None)。

理论与底层代码的对齐 (极其重要):

在数学理论中,第 iii 个元素的 LogSoftmax 公式为:

LogSoftmax(xi)=ln⁡(exi∑jexj)=xi−ln⁡(∑jexj)LogSoftmax(x_i) = \ln \left( \frac{e^{x_i}}{\sum_{j} e^{x_j}} \right) = x_i - \ln \left( \sum_{j} e^{x_j} \right)LogSoftmax(xi)=ln(jexjexi)=xiln(jexj)

通过等号右侧的化简,PyTorch 底层在计算时巧妙地将原本的除法转化为减法,从而极大地提升了数值稳定性。

(2). __call__ - 实例调用

作用:接收未经过任何处理的原始线性输出 (Logits),输出带有负数的对数概率矩阵。

# 实例化后,直接像调用函数一样调用该对象
log_softmax_layer(input)

参数:

  • 预测张量 (input): 模型前向传播的原始输出 Logits。数据类型要求为浮点型 (float32 等)。

返回值:

  • 成功: 返回一个与 input 形状完全相同的新张量。由于正常 Softmax 输出在 (0,1)(0, 1)(0,1) 之间,对其取 ln⁡\lnln 后,输出张量中的所有元素均必定为负数或 0 (0-D to N-D Tensor)。

示例:

z = torch.tensor([[1., 2., 3.]]) # 假设是一个3分类的原始Logits样本

# 实例化并指定按列(即按不同类别)计算
log_softmax = nn.LogSoftmax(dim=1)
log_probs = log_softmax(z)

print(log_probs)
# 返回: tensor([[-2.4076, -1.4076, -0.4076]])
# 注意:这些全是负数,它们对应着真实的概率的对数

3. nn.NLLLoss类 - 负对数似然损失函数

nn.NLLLoss类同样是nn.Module类的子类

(1). nn.NLLLoss类类型

作用:

nn.NLLLoss (Negative Log Likelihood Loss) 的核心作用是在给定的“对数概率矩阵”中,精准地“挑出”真实标签所对应的那个对数概率值,并对其取负号(将其变为正数作为 Loss)。它主要用于配合 nn.LogSoftmax 实现多分类的损失计算。

torch.nn.NLLLoss(weight=None, reduction='mean')

核心参数:

  • 样本权重 (weight): 传入长度等于类别数的一维张量,用于为不同类别分配权重,缓解类别不平衡问题。默认为 None
  • 聚合方式 (reduction): 决定如何聚合一个 Batch 内所有样本的损失,支持 'mean' (默认)、'sum''none'

注意:

  • nn.NLLLoss绝对不需要,而且绝对不能传入独热编码 (One-Hot) 矩阵!
  • nn.NLLLoss以及它的大哥 nn.CrossEntropyLoss对真实标签 target 的要求非常严格:它必须是类别对应的数字索引 (Class Indices)。也就是__call__传入的target张量的shape就是(样本数,),每一行写着是哪类。

理论与底层代码的对齐 (极其重要):

在 PyTorch 底层,假设输入张量 xxx 为对数概率矩阵,真实标签张量 yyy 为类别索引。对于 Batch 中的第 nnn 个样本,底层计算逻辑为极其简单的“查表取负”:

ln=−xn,ynl_n = - x_{n, y_n}ln=xn,yn

reduction='mean',则最终输出标量:ℓ(x,y)=1N∑n=1Nln\ell(x, y) = \frac{1}{N}\sum_{n=1}^{N} l_n(x,y)=N1n=1Nln

补充 (警告):

工业界铁律:nn.NLLLoss 必须且只能与 nn.LogSoftmax 绑定使用!

(2). __call__ - 实例调用(执行损失计算)

作用:接收带有负数的对数概率分布和真实的标签索引,执行“查表取负”并算出最终的 Loss 值。

# 实例化后,直接像调用函数一样调用该对象
criterion(input, target)

参数:

  • 预测张量 (input): 必须是经过 LogSoftmax 处理后的对数概率张量。形状为 (BatchSize, K),要求为浮点型 (float32)。
  • 真实张量 (target): 真实类别的数字索引 (如类别序号 0, 1, 2…)。形状必须是 (BatchSize,)。要求数据类型必须是整型 (torch.longtorch.int64)。

返回值:

  • 成功: 如果 reduction'mean''sum',返回一个带有 grad_fn 的 0 维标量张量 (0-D Tensor),代表全局的损失值。

示例:

# 构造数据: 形状为[2, 3] (2个样本,3个分类)
zhat = torch.rand(2,3)

# 1. 通过LogSoftmax算出来的结果ln(softmax(zhat))结果
log_softmax = nn.LogSoftmax(dim=1)
log_probs = log_softmax(zhat)

# 2. 构造真实标签(第一个样本属于0类,第二个样本属于2类)
# 必须torch.long或torch.int64类型
y = torch.tensor([0, 2], dtype=torch.long)

# 3. 计算NLLLoss
criterion_nll = nn.NLLLoss(reduction='mean')
loss = criterion_nll(log_probs, y)
# 底层数学与物理动作拆解 (从独热编码视角):
# 第一步:将 target 张量 y = [0, 2] 在数学逻辑上展开为独热编码 (One-Hot) 矩阵:
# y_onehot = [[1, 0, 0],   <-- 样本1真实类别为0,对应第0列为1
#             [0, 0, 1]]   <-- 样本2真实类别为2,对应第2列为1
# 第二步:将独热矩阵与对数概率矩阵log_probs进行逐元素相乘:
# 样本1: [ L00*1, L01*0, L02*0 ]  ->  [ L00, 0, 0 ]
# 样本2: [ L10*0, L11*0, L12*1 ]  ->  [ 0, 0, L12 ]
# 第三步:按行求和,并套用 NLLLoss 的取负号操作 (完成单样本 Loss 计算):
# 样本1 Loss = -(L00 + 0 + 0) = -L00
# 样本2 Loss = -(0 + 0 + L12) = -L12
# 第四步:根据 reduction='mean' 参数,对所有样本的 Loss 求批次平均:
# 最终全局 Loss = ( -L00 + -L12 ) / 2
print(loss)
# tensor(0.8079)

4. nn.CrossEntropyLoss类 - 多分类交叉熵损失函数

nn.CrossEntropyLoss类同样是nn.Module类的子类。

nn.CrossEntropyLoss 底层是 LogSoftmaxNLLLoss 的结合体。

(1). nn.CrossEntropyLoss类类型

作用:

nn.CrossEntropyLoss是PyTorch中用于处理多分类任务(分类数 ≥2\ge 22)的绝对核心标准库。它用于衡量模型输出的类别概率分布与真实类别标签之间的差异。

torch.nn.CrossEntropyLoss(weight=None, reduction='mean', label_smoothing=0.0)

核心参数:

  • 样本权重 (weight): 传入一个一维张量(长度等于类别数 KKK),用于处理类别不平衡问题。例如某些罕见病类别样本极少,可以赋予该类别更大的权重。默认为不加权 (None)。
  • 聚合方式 (reduction): 决定了对整批数据的 Loss 结果如何聚合,支持 'mean' (默认,求批次均值)、'sum' (求批次总和)、'none' (不聚合,返回每个样本的 Loss)。
  • 标签平滑 (label_smoothing): (进阶技巧) 传入一个 [0.0,1.0][0.0, 1.0][0.0,1.0] 之间的浮点数。用于防止模型过度自信(Overconfidence),将绝对的 One-hot 标签(如 [0, 1, 0])软化为平滑标签(如 [0.1, 0.8, 0.1]),能有效提升模型的泛化能力。

理论与底层代码的对齐 (重点!史诗级避坑警告):

PyTorch 中的 nn.CrossEntropyLoss() 并不是单纯的交叉熵公式,它是一个"三合一"的终极打包方案!

它内部按顺序自动且极其稳定地执行了以下三步:

  1. Logits →Softmax\xrightarrow{Softmax}Softmax 概率分布 (将原始实数域得分转化为 0~1 概率)
  2. 概率分布 →log⁡\xrightarrow{\log}log 对数概率 (取自然对数)
  3. 对数概率 →NLLLoss\xrightarrow{NLLLoss}NLLLoss 负对数似然损失 (提取正确类别的对数值并取负)

致命铁律:
因为底层自带了 Softmax,你在定义多分类神经网络的 forward 方法时,最后一层绝对、绝对不要手动加 Softmax 层!必须直接把没经过任何处理的原始线性输出 zzz (Logits) 扔给这个损失函数!手动加 Softmax 会导致"二次 Softmax",让梯度严重消失!

(2). __call__ - 实例调用(执行损失计算)

作用:接收多分类预测结果和真实的标签索引,执行"三合一"的损失计算,并接入自动求导计算图。

# 实例化后,直接像调用函数一样调用该对象
criterion(input, target)

参数:

  • 预测张量 (input): 模型前向传播的原始输出 Logits。形状必须是 (BatchSize, K),其中 KKK 是总类别数。数据类型必须是浮点型 (float32)。
  • 真实张量 (target): 真实类别的数字索引 (如类别序号 0, 1, 2…)。
    • 注意:PyTorch 非常贴心,这里你不需要手动把它转成 One-hot 矩阵!直接传一维索引向量即可。
    • 形状必须是 (BatchSize,)
    • 数据类型必须是整型 (torch.longtorch.int64),绝不能是浮点型!

返回值:

  • 成功: 如果 reduction'mean''sum',返回一个带有 grad_fn 的 0 维标量张量 (0-D Tensor),代表全局的损失值。

5. 代码多分类交叉熵损失

(1).nn.LogSoftmax+nn.NLLLoss版本
import torch
import torch.nn as nn

# m是样本量
m = 3*pow(10,3)
torch.random.manual_seed(420)
X = torch.rand(size=(m,4),dtype=torch.float32)
w = torch.rand(size=(4,3),dtype=torch.float32)
y = torch.randint(low=0,high=3,size=(m,),dtype=torch.float32)
zhat = X@w
logsm = nn.LogSoftmax(dim=1)
logsigma = logsm(zhat)
criterion = nn.NLLLoss()
Loss = criterion(logsigma,y.long())
print(Loss.shape)
# torch.Size([])
print(Loss)
# tensor(1.1147)
(2).nn.CrossEntropyLoss版本
import torch
import torch.nn as nn

# m是样本量
m = 3*pow(10,3)
torch.random.manual_seed(420)
X = torch.rand(size=(m,4),dtype=torch.float32)
w = torch.rand(size=(4,3),dtype=torch.float32)
y = torch.randint(low=0,high=3,size=(m,),dtype=torch.float32)
zhat = X@w
criterion = nn.CrossEntropyLoss()
Loss = criterion(zhat,y.long())
print(Loss.shape)
# torch.Size([])
print(Loss)
# tensor(1.1147)

五. 损失函数对比总结

任务类型输出层激活函数常用损失函数类输入要求
回归任务nn.MSELoss连续数值预测 y^\hat{y}y^
二分类任务Sigmoidnn.BCEWithLogitsLoss原始得分 zzz (Logits)
多分类任务Softmaxnn.CrossEntropyLoss原始得分 zzz (Logits)

无论是二分类还是多分类,PyTorch都提供了包含输出层激活函数和不包含输出层激活函数的类两种选择。在实际神经网络建模中,类可以被放入定义好的Model类中去构建神经网络的结构,因此是否包含激活函数,就需要由用户来自行选择。

  • 重视展示网络结构和灵活性,应该使用不包含输出层激活函数的类

通常在Model类中,__init__中层的数量与forward函数中对应的激活函数的数量是一致的,如果我们使用内置sigmoid/logsoftmax功能的类来计算损失函数,forward函数在定义时就会少一层(输出层),网络结构展示就不够简单明了,对于结构复杂的网络而言,结构清晰就更为重要。同时,如果激活函数是单独写的,要修改激活函数就变得很容易,如果混在损失函数中,要修改激活函数就得改掉整个损失函数的代码,不利于维护。

  • 重视稳定性和运算精度,使用包含输出层激活函数的类

如果在一个Model中,很长时间我们都不会修改输出层的激活函数,并且模型的稳定运行更为紧要,我们就使用内置了激活函数的类来计算损失函数。同时,就像之前提到的,内置激活函数可以帮助我们提升运算的精度。

因此,选择哪种损失函数的实现方式,最终还要看我们的需求。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值