Softmax与交叉熵手撕

softmax函数在实际工程中常常和交叉熵一起使用,同时也因为写起来比较简单,可以作为面试手撕题目之一。

为什么需要Softmax

在之前的学习中,我们已经知道神经网络的输出可以是一个连续值,比如预测房价、预测温度等回归问题。但是,在分类问题中,我们需要输出一个概率分布,比如判断一张图片是猫还是狗,这时候应该怎么做呢?

首先,我们需要让神经网络的输出变成一个概率分布。所谓概率分布,就是所有的输出都在0到1之间,而且所有的输出加起来等于1。这样,我们就可以把输出理解为每个类别的概率。

这就是Softmax函数的作用。它的公式是:

softmax(zi)=ezi∑j=1nezj\text{softmax}(z_i)=\frac{e^{z_i}}{\sum_{j=1}^{n}e^{z_j}}softmax(zi)=∑j=1nezjezi

其中,ziz_izi是神经网络第iii个输出,nnn是类别的总数。可以看到,Softmax函数会把所有的输出都变成正数(这正是exe^xex函数的作用),然后除以它们的总和,这样所有的输出就在0到1之间,而且加起来等于1。

但是,这里有一个问题:如果ziz_izi是一个很大的正数,那么ezie^{z_i}ezi就会非常大,甚至会溢出。为了解决这个问题,我们需要使用数值稳定版本的Softmax:

python 复制代码
import numpy as np

def softmax(x):
    #数值稳定版本:减去最大值防止溢出
    x_shifted = x - np.max(x, axis=-1, keepdims=True)
    exp_x = np.exp(x_shifted)
    return exp_x / np.sum(exp_x, axis=-1, keepdims=True)

为什么要减去最大值呢?这是因为:

ezi−max⁡(z)=ezi⋅e−max⁡(z)e^{z_i-\max(z)}=e^{z_i}\cdot e^{-\max(z)}ezi−max(z)=ezi⋅e−max(z)

这样,所有指数的最大值就是e0=1e^0=1e0=1,其他的都会小于1,就不会溢出了。

代码里对于axiskeepdim的要求是这样的,由于我们的输入一般是多个小批次,所以一般出来的结果也就是二维矩阵;本来是一维数组的,但是我们为了加速运算,用二维数组而不是循环来计算他们。

交叉熵

有了Softmax输出的概率分布之后,我们还需要一个损失函数来衡量模型预测的好坏。这就是交叉熵出场的时候。

在回归问题中,我们使用均方误差(MSE)作为损失函数,它衡量的是预测值和真实值之间的平方差。但在分类问题中,我们需要衡量的是两个概率分布之间的差异。

交叉熵的公式是:

CE=−∑i=1nyilog⁡(y^i)\text{CE}=-\sum_{i=1}^{n}y_i\log(\hat{y}_i)CE=−i=1∑nyilog(y^i)

其中,yyy是真实标签(通常是one-hot编码),y^\hat{y}y^是模型预测的概率。

这个公式看起来有点抽象,让我们来解释一下它的含义。假设我们有三个类别,真实标签是[0,1,0][0,1,0][0,1,0](即第二个类别是正确答案),模型预测的概率是[0.2,0.7,0.1][0.2,0.7,0.1][0.2,0.7,0.1]。那么交叉熵就是:

−(0⋅log⁡(0.2)+1⋅log⁡(0.7)+0⋅log⁡(0.1))=−log⁡(0.7)≈0.357-(0\cdot\log(0.2)+1\cdot\log(0.7)+0\cdot\log(0.1))=-\log(0.7)\approx0.357−(0⋅log(0.2)+1⋅log(0.7)+0⋅log(0.1))=−log(0.7)≈0.357

如果模型预测的概率是[0.1,0.8,0.1][0.1,0.8,0.1][0.1,0.8,0.1],那么交叉熵就是:

−log⁡(0.8)≈0.223-\log(0.8)\approx0.223−log(0.8)≈0.223

可以看到,模型预测得越准确,交叉熵就越小。这正是我们想要的。

这个公式不仅仅奖励了正确的输出,同时还可以选择对错误输出进行惩罚,这就是他的一些变种的思路,我们这里按下不表。

交叉熵的代码实现如下:

python 复制代码
def cross_entropy(y_pred, y_true, epsilon=1e-15):
#防止log(0)
    y_pred = np.clip(y_pred, epsilon, 1 - epsilon)
    return -np.sum(y_true * np.log(y_pred), axis=-1)

Softmax与交叉熵的联合

如果我们分别计算Softmax和交叉熵,会有什么问题呢?

问题在于,当我们反向传播的时候,需要计算Softmax的导数,这个导数会比较复杂,而且数值不稳定。更重要的是,如果我们把Softmax和交叉熵放在一起计算,可以得到一个非常简洁的结果。

让我们来推导一下:

Loss=−log⁡(eztrue∑jezj)=−ztrue+log⁡(∑jezj) \text{Loss}=-\log\left(\frac{e^{z_{true}}}{\sum_{j}e^{z_j}}\right)=-z_{true}+\log\left(\sum_{j}e^{z_j}\right) Loss=−log(∑jezjeztrue)=−ztrue+log(j∑ezj)

其中,ztruez_{true}ztrue是真实类别对应的log⁡\loglog值。

这就是联合损失函数的核心公式。代码实现如下:

python 复制代码
def softmax_cross_entropy(logits,y_true):

    logits_shifted = logits - np.max(logits, axis=-1, keepdims=True)

    log_sum_exp = np.log(np.sum(np.exp(logits_shifted), axis=-1, keepdims=True))

    loss = -logits_shifted * y_true + log_sum_exp
    return np.sum(loss,axis=-1)

反向传播

最重要的部分来了:反向传播的梯度应该怎么计算?

经过数学推导,我们得到一个非常简洁的结论:

∂Loss∂zi=y^i−yi\frac{\partial\text{Loss}}{\partial z_i}=\hat{y}_i-y_i∂zi∂Loss=y^i−yi

也就是说,梯度就等于预测概率减去真实标签!

这就是为什么联合计算如此重要:单独计算的话,梯度会非常复杂;但联合计算后,梯度变得异常简洁。

代码实现如下:

python 复制代码
def softmax_cross_entropy_backward(y_pred,y_true):

    return y_pred - y_true
相关推荐
咕噜企业分发小米2 小时前
GPUStack × MaxKB:打造强大易用的开源企业级智能体平台(下)
人工智能
WitsMakeMen2 小时前
RoPE 算法原理?算法为什么只和相对位置有关
人工智能·算法·llm
热点速递2 小时前
理想汽车“寒冬”未退,业绩小幅回暖掩盖深层阵痛
人工智能·汽车·业界资讯
有Li2 小时前
基于几何映射的二维自然图像到四维fMRI脑图像的迁移学习/文献速递-大模型与图像分割在医疗影像中应用
人工智能·深度学习·文献·医学生
学而要时习2 小时前
拒绝 API 堆砌:当“AI 龙虾”打破传统软件工程的确定性边界
人工智能·软件工程
weixin_505154462 小时前
Bowell Studio:重塑工业互联网时代的装配制造与运维检修
运维·数据库·人工智能·制造·数字孪生·3d产品配置器·3d交互展示
八角Z2 小时前
AI短视频创作实战心得:从玩具到生产力工具亲测
人工智能·机器学习·服务发现·音视频
Sylvia33.2 小时前
OpenClaw + 数眼智能:Windows/Mac 双系统部署与特价模型接入实战指南
大数据·人工智能
YangYang9YangYan2 小时前
2026大专财富管理学习数据分析指南
人工智能