原文:You Don't Really Know Softmax - 2020.04.26

Softmax 函数是分类模型中常用的主要函数之一. 其最早是在机器学习中提出的.

Softmax 函数将输入作为一个固定长度为 d 的实值向量,并将其归一化为概率分布. 其易于理解和解释的,但其核心仍有值得深入弄明白之处,比如其实际实现、数值稳定性及应用.

1. 介绍

Softmax 是非线性函数,主要用于 multi-class classification 任务中分类器的输出端.

给定向量 $[x_1, x_2, x_3, ..., x_d]$,softmax 函数形式为:

$$ sm(x_i) = \frac{e^{x_i}}{\sum_{j=1}^{d} e^{x_j}} $$

$i = 1, 2, ..., d$,d 为类别数量.

$\sum_{j=1}^{d} e^{x_j}$ 所有指数值的和,是一个归一化常数值,其有助于确保概率分布的特点,比如,

[1] - 数值之和必须是 1.

[2] - 每个值都在 [0, 1] 区间范围内.

例如,给定向量 $x = [10, 2, 40, 4]$,计算每个元素的 softmax值:

  • 每个元素值的指数 $e^x = [e^{10}, e^2, e^{40}, e^4]$
  • 计算元素值的指数值之和 $\sum e^x = e^{10} + e^2 + e^{40} + e^4 = 2.353...e^{17}$
  • 每个元素的指数值除以全部元素的指数值之和 $sm(x) = [9.35762297e^{-17}, 3.13913279e^{-17}, 1.00000000e^{+00}, 2.31952283e^{-16}]$

其实现如:

import numpy as np

def softmax(x):
    exp_x = np.exp(x)
    sum_exp_x = np.sum(exp_x)
    sm_x = exp_x/sum_exp_x
    return sm_x
   
#
x = np.array([10, 2, 40, 4])
print(softmax(x))
#[9.35762297e-14 3.13913279e-17 1.00000000e+00 2.31952283e-16]

问题:

  • 从输出观察出了什么?
  • 输出值之和是 1 吗?

2. Softmax 的数值稳定性

机器学习 - 计算 Log-Sum-Exp - AIUAI

从上面 softmax 的概率值可以看出,当元素值范围非常大时,容易出现数值不稳定性. 比如,修改上面向量的第三个元素值为 10000,并重新计算 softmax:

x = np.array([10, 2, 10000, 4])
print(softmax(x))
#[0.0,  0.0, nan,  0.0]

nan 表示 not-a-number,往往出现在过拟合(overflow) 和 欠拟和(underflow) 中. 但是,Softmax 为什么会输出这样的结果呢?是不能得到向量的概率分布吗?

问题: 能找出是什么所导致过拟合吗?

一个非常大的数值的指数会是非常、非常大的值,如 $2^{10000}$, 导致过拟合.

问题: 可以做的更好吗?

当然.

根据Softmax函数的原始形式:

$$ sm(x_i) = \frac{e^{x_i}}{\sum_{j=1}^{d} e^{x_j}} $$

从 $x_i$ 中减去一个常数 $c$,

$$ sm(x_i) = \frac{e^{x_i - c}}{\sum_{j=1}^{d} e^{x_j - c}} $$

即为 $x_i$ 平移一个常数,如果该平移常数 $c$ 为向量的最大值 $max(x)$,则可以使得 softmax 计算的稳定性.

问题: 可以得到与原始 softmax 相同的答案吗?

根据公式推导其等价性.

$$ sm(x_i) = \frac{e^{x_i - c}}{\sum_{j=1}^{d} e^{x_j - c}} $$

$$ sm(x_i) = \frac{e^{x_i}e^{ - c}}{\sum_{j=1}^{d} e^{x_j} e^{ - c}} $$

$$ sm(x_i) = \frac{e^{x_i}e^{ - c}}{e^{ - c} \sum_{j=1}^{d} e^{x_j}} $$

即可得到相同的初始 softmax 函数:

$$ sm(x_i) = \frac{e^{x_i}}{\sum_{j=1}^{d} e^{x_j}} $$

其实现:

def softmax(x):
    max_x = np.max(x)
    exp_x = np.exp(x - max_x)
    sum_exp_x = np.sum(exp_x)
    sm_x = exp_x/sum_exp_x
    return sm_x

#
x = np.array([10, 2, 10000, 4])
print(softmax(x))
#[0., 0., 1., 0.]

可以看出,nan 问题解决了.

问题: 为什么 softmax 的其他值都是 0? 其是不是意味着没有发生的概率?

3. Log Softmax

Softmax 计算的一个关键评估显示了指数计算和除法计算的模式. 是否可以简化这些计算呢? 可以通过优化 log softmax 来代替. 其具有如下更优的特点:

[1] - 数值稳定性

[2] - log softmax 的梯度计算为加法计算,因为$log(a/b) - log(a) - log(b)$

[3] - 除法和乘法计算被转换成加法,更少的计算量和计算成本

[4] - log 函数是单调递增函数,可以更好的利用该特点.

关于 log softmax 比 softmax , stackoverflow 上的一个回答:

log softmax 比 softmax 有很多更适合使用的优势,比如实际应用中的更优的数值计算性能和梯度优化. 这些优势在实现中非常重要,尤其是在模型训练中. 其关键在于 log 概率的使用,具有更好的信息理论可解释性. 当 log softmax 用于分类问题中,无法预测正确的类别时,其会对模型进行严重惩罚. 而这种惩罚是否会有助于问题,取决于测试结果. 所以,log softmax 和 softmax 都是值得使用的

softmax 和 log softmax 的计算:

x = np.array([10, 2, 10000, 4])
softmax(x)
#[0., 0., 1., 0.]

np.log(softmax(x))
#[-inf, -inf,   0., -inf]

回到数值稳定性问题,实际上,log softmax 数值欠拟合.

问题: 为什么会这样?

在对每个元素计算 log 计算时,$log(0)$ 是未定义的.

是否可以做的更好呢?当然.

4. Log-Softmax 变形

$$ sm(x_i) = \frac{e^{x_i - c}}{\sum_{j=1}^{d} e^{x_j - c}} $$

$$ log \ sm(x_i) = log \ \frac{e^{x_i - c}}{\sum_{j=1}^{d} e^{x_j - c}} $$

$$ log \ sm(x_i) = x_i - c - \log \ \sum_{j=1}^{d} e^{x_j-c} $$

如何回到原来的概率呢? 可以采用对 log softmax 或 log 概率进行指数化和归一化的方式:

$$ sm(x_i) = \frac{e^{log \ probs}}{\sum_{j=1}^{d} e^{log \ probs}} $$

实现如:

def logsoftmax(x, recover_probs=True):
    # LogSoftMax Implementation 
    max_x = np.max(x)
    exp_x = np.exp(x - max_x)
    sum_exp_x = np.sum(exp_x)
    log_sum_exp_x = np.log(sum_exp_x)
    max_plus_log_sum_exp_x = max_x + log_sum_exp_x
    log_probs = x - max_plus_log_sum_exp_x

    # Recover probs
    if recover_probs:
        exp_log_probs = np.exp(log_probs)
        sum_log_probs = np.sum(exp_log_probs)
        probs = exp_log_probs / sum_log_probs
        return probs

    return log_probs
#  
x = np.array([10, 2, 10000, 4])
print(logsoftmax(x, recover_probs=True))
#[0., 0., 1., 0.]

5. Softmax Temperature

NLP 领域,softmax 被用于分类器的输出,以获得 tokens 的概率分布. softmax 过度自信于其预测,使得其他 words 很难被采样到.

例如,有如下 statement:

The boy _ to the market.

可能的答案为:[goes,go,went,comes]. 假设分类器的输出数值为 [38,20,40,39],则其 softmax 结果为:

x = [38, 20, 40, 39]
softmax(x)
#[0.09, 0.00, 0.6, 0.24]

如果从该分布采样,60% 的可能为 went,但填空的答案根据上线文也可能是 goes 和 comes. 分类器的 words 的初始值是比较接近的,但 softmax 并不是这样.

对此,temperature 超参数 $\tau$ 被引入到 softmax 函数中,

$$ sm(x_i) = \frac{e^{\frac{x_i - c}{\tau}}}{\sum_{j=1}^{d} e^{\frac{x_i - c}{\tau}}} $$

其中,$\tau \in (0, inf]$. 其增加了 softmax 对于低概率候选项的敏感度,有助于得到更优的结果.

例如,设置不同的 $\tau$ 值:

[1] - $\tau \rightarrow 0 $,如$\tau = 0.001$

softmax(x/0.001)
#[0., 0., 1., 0.]

得到结果是,更自信的预测,更少的可能采样到不可能的候选项.

[2] - $\tau \rightarrow inf$,如 $\tau = 100$

softmax(x/100)
#[0.25869729, 0.21608214, 0.26392332, 0.26129724] 

得到的结果是,更平滑(softer)的概率分布,并得到更广泛的采样.

6. 结论

softmax 是有一个有意思的函数,值得深入理解探究.

Last modification:June 17th, 2020 at 10:45 am