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,就不会溢出了。
代码里对于axis和keepdim的要求是这样的,由于我们的输入一般是多个小批次,所以一般出来的结果也就是二维矩阵;本来是一维数组的,但是我们为了加速运算,用二维数组而不是循环来计算他们。
交叉熵
有了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