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
相关推荐
蔡俊锋1 天前
用AI实现乐高式大型可插拔系统的技术方案
人工智能·ai工程·ai原子能力·ai乐高工程
自然语1 天前
人工智能之数字生命 认知架构白皮书 第7章
人工智能·架构
大熊背1 天前
利用ISP离线模式进行分块LSC校正的方法
人工智能·算法·机器学习
eastyuxiao1 天前
如何在不同的机器上运行多个OpenClaw实例?
人工智能·git·架构·github·php
诸葛务农1 天前
AGI 主要技术路径及核心技术:归一融合及未来之路5
大数据·人工智能
光影少年1 天前
AI Agent智能体开发
人工智能·aigc·ai编程
ai生成式引擎优化技术1 天前
TSPR-WEB-LLM-HIC (TWLH四元结构)AI生成式引擎(GEO)技术白皮书
人工智能
帐篷Li1 天前
9Router:开源AI路由网关的架构设计与技术实现深度解析
人工智能
新缸中之脑1 天前
在GCP上运行autoresearch
人工智能
Fleshy数模1 天前
OpenCV 实时人脸检测实战:从视频文件到人脸框标注
人工智能·opencv·计算机视觉