一文弄懂LogSumExp技巧

本文介绍了LogSumExp(LSE)技巧,用于解决机器学习中计算Softmax和CrossEntropy时可能出现的数值溢出和下溢问题。通过取向量最大值并进行指数归一化,可以有效地避免这些问题。同时,文章展示了如何实现数值稳定的Softmax和Sigmoid函数,确保在不同数值范围内计算的准确性。
摘要由CSDN通过智能技术生成

引言

今天来学习下LogSumExp(LSE)1技巧,主要解决计算Softmax或CrossEntropy2时出现的上溢(overflow)或下溢(underflow)问题。

我们知道编程语言中的数值都有一个表示范围的,如果数值过大,超过最大的范围,就是上溢;如果过小,超过最小的范围,就是下溢。

什么是LSE

LSE被定义为参数指数之和的对数:
LSE ( x 1 , ⋯   , x n ) = log ⁡ ∑ i = 1 n exp ⁡ ( x i ) = log ⁡ ( exp ⁡ ( x 1 ) + ⋯ + exp ⁡ ( x n ) ) \text{LSE}(x_1,\cdots,x_n) = \log \sum_{i=1}^n \exp(x_i) =\log \left(\exp(x_1) + \cdots + \exp(x_n) \right) LSE(x1,,xn)=logi=1nexp(xi)=log(exp(x1)++exp(xn))
输入可以看成是一个n维的向量,输出是一个标量。

为什么需要LSE

在机器学习中,计算概率输出基本都需要经过Softmax函数,它的公式应该很熟悉了吧
Softmax ( x i ) = exp ⁡ ( x i ) ∑ j = 1 n exp ⁡ ( x j ) (1) \text{Softmax}(x_i) = \frac{\exp(x_i)}{\sum_{j=1}^n \exp(x_j)} \tag{1} Softmax(xi)=j=1nexp(xj)exp(xi)(1)
但是Softmax存在上溢和下溢大问题。如果 x i x_i xi太大,对应的指数函数也非常大,此时很容易就溢出,得到nan结果;如果 x i x_i xi太小,或者说负的太多,就会导致出现下溢而变成0,如果分母变成0,就会出现除0的结果。

此时我们经常看到一个常见的做法是(其实用到的是指数归一化技巧, exp-normalize3),先计算 x x x中的最大值 b = max ⁡ i = 1 n x i b = \max_{i=1}^n x_i b=maxi=1nxi,然后根据
Softmax ( x i ) = exp ⁡ ( x i ) ∑ j = 1 n exp ⁡ ( x j ) = exp ⁡ ( x i − b ) ⋅ exp ⁡ ( b ) ∑ j = 1 n ( exp ⁡ ( x j − b ) ⋅ exp ⁡ ( b ) ) = exp ⁡ ( x i − b ) ⋅ exp ⁡ ( b ) exp ⁡ ( b ) ⋅ ∑ j = 1 n exp ⁡ ( x j − b ) = exp ⁡ ( x i − b ) ∑ j = 1 n exp ⁡ ( x j − b ) = Softmax ( x i − b ) (2) \begin{aligned} \text{Softmax}(x_i) &= \frac{\exp(x_i)}{\sum_{j=1}^n \exp(x_j)} \\ &= \frac{\exp(x_i - b) \cdot \exp(b)}{\sum_{j=1}^n \left (\exp(x_j - b) \cdot \exp(b) \right)} \\ &= \frac{\exp(x_i - b) \cdot \exp(b)}{ \exp(b) \cdot \sum_{j=1}^n \exp(x_j - b) } \\ &= \frac{\exp(x_i - b)}{\sum_{j=1}^n \exp(x_j - b)} \\ &= \text{Softmax}(x_i - b) \end{aligned} \tag{2} Softmax(xi)=j=1nexp(xj)exp(xi)=j=1n(exp(xjb)exp(b))exp(xib)exp(b)=exp(b)j=1nexp(xjb)exp(xib)exp(b)=j=1nexp(xjb)exp(xib)=Softmax(xib)(2)
这种转换是等价的,经过这一变换,就避免了上溢,最大值变成了 exp ⁡ ( 0 ) = 1 \exp(0)=1 exp(0)=1;同时分母中也会有一个1,就避免了下溢。

我们通过实例来理解一下。

def bad_softmax(x):
  y = np.exp(x)
  return y / y.sum()
 
x = np.array([1, -10, 1000])
print(bad_softmax(x)) 
... RuntimeWarning: overflow encountered in exp
... RuntimeWarning: invalid value encountered in true_divide
array([ 0.,  0., nan])

接下来进行上面的优化,并进行测试:

def softmax(x):
  b = x.max()
  y = np.exp(x - b)
  return y / y.sum()
 
print(softmax(x))
array([0., 0., 1.])

我们再看下是否会出现下溢:

x = np.array([-800, -1000, -1000])
print(bad_softmax(x))
# array([nan, nan, nan])
print(softmax(x))
# array([1.00000000e+00, 3.72007598e-44, 3.72007598e-44])

嗯,看来解决了这个两个问题。

202112101632

等等,不是说LSE吗,怎么整了个什么归一化技巧。

好吧,回到LSE。

我们对Softmax取对数,得到:
log ⁡ ( Softmax ( x i ) ) = log ⁡ exp ⁡ ( x i ) ∑ j = 1 n exp ⁡ ( x j ) = x i − log ⁡ ∑ j = 1 n exp ⁡ ( x j ) (3) \begin{aligned} \log \left( \text{Softmax}(x_i) \right) &= \log \frac{\exp(x_i)}{\sum_{j=1}^n \exp(x_j)} \\ &= x_i - \log \sum_{j=1}^n \exp(x_j) \\ \end{aligned} \tag{3} log(Softmax(xi))=logj=1nexp(xj)exp(xi)=xilogj=1nexp(xj)(3)
因为上面最后一项也有上溢的问题,所以应用同样的技巧,得
log ⁡ ∑ j = 1 n exp ⁡ ( x j ) = log ⁡ ∑ j = 1 n exp ⁡ ( x j − b ) exp ⁡ ( b ) = b + log ⁡ ∑ j = 1 n exp ⁡ ( x j − b ) (4) \log \sum_{j=1}^n \exp(x_j) = \log \sum_{j=1}^n \exp(x_j - b) \exp(b) = b + \log \sum_{j=1}^n \exp(x_j - b) \tag{4} logj=1nexp(xj)=logj=1nexp(xjb)exp(b)=b+logj=1nexp(xjb)(4)
b b b同样是取 x x x中的最大值。

这样,我们就得到了LSE的最终表示:
LSE ( x ) = b + log ⁡ ∑ j = 1 n exp ⁡ ( x j − b ) (5) \text{LSE}(x) = b + \log \sum_{j=1}^n \exp(x_j - b) \tag{5} LSE(x)=b+logj=1nexp(xjb)(5)
此时,Softmax也可以这样表示:
Softmax ( x i ) = exp ⁡ ( x i − b − log ⁡ ∑ j = 1 n exp ⁡ ( x j − b ) ) (6) \text{Softmax}(x_i) = \exp \left( x_i - b - \log \sum_{j=1}^n \exp(x_j - b) \right) \tag{6} Softmax(xi)=exp(xiblogj=1nexp(xjb))(6)
对LogSumExp求导就得到了exp-normalize(Softmax)的形式,
∂ ( b + log ⁡ ∑ j = 1 n exp ⁡ ( x j − b ) ) ∂ x j = exp ⁡ ( x i − b ) ∑ j = 1 n exp ⁡ ( x j − b ) (7) \frac{\partial \left (b + \log \sum_{j=1}^n \exp(x_j - b) \right )}{\partial x_j} = \frac{\exp(x_i - b)}{\sum_{j=1}^n \exp(x_j - b)} \tag{7} xj(b+logj=1nexp(xjb))=j=1nexp(xjb)exp(xib)(7)

那我们是使用exp-normalize还是使用LogSumExp呢?

如果你需要保留Log空间,那么就计算 log ⁡ ( Softmax ) \log(\text{Softmax}) log(Softmax),此时使用LogSumExp技巧;如果你只需要计算Softmax,那么就使用exp-normalize技巧。

怎么实现LSE

实现LSE就很简单了,我们通过代码实现一下。

def logsumexp(x):
  b = x.max()
  return b + np.log(np.sum(np.exp(x - b)))
 
def softmax_lse(x):
  return np.exp(x - logsumexp(x))

上面是基于LSE实现了Softmax,下面测试一下:

> x1 = np.array([1, -10, 1000])
> x2 = np.array([-900, -1000, -1000])
> softmax_lse(x1)
array([0., 0., 1.])
> softmax(x1)
array([0., 0., 1.])
> softmax_lse(x2)
array([1.00000000e+00, 3.72007598e-44, 3.72007598e-44])
> softmax(x2)
> array([1.00000000e+00, 3.72007598e-44, 3.72007598e-44])

最后我们看一下数值稳定版的Sigmoid函数

数值稳定的Sigmoid函数

我们知道Sigmoid函数公式为:
σ ( x ) = 1 1 + exp ⁡ ( − x ) (8) \sigma(x) = \frac{1}{1 + \exp(-x)} \tag{8} σ(x)=1+exp(x)1(8)
对应的图像如下:

202112110835png

其中包含一个 exp ⁡ ( − x ) \exp(-x) exp(x),我们看一下 e x e^x ex的图像:

202112110854

从上图可以看出,如果 x x x很大, e x e^x ex会非常大,而很小就没事,变成无限接近 0 0 0

当Sigmoid函数中的 x x x负的特别多,那么 exp ⁡ ( − x ) \exp(-x) exp(x)就会变成 ∞ \infty ​,就出现了上溢;

那么如何解决这个问题呢? σ ( x ) \sigma(x) σ(x)可以表示成两种形式:
σ ( x ) = 1 1 + exp ⁡ ( − x ) = exp ⁡ ( x ) 1 + exp ⁡ ( x ) (9) \sigma(x) = \frac{1}{1 + \exp(-x)} = \frac{\exp(x)}{1 + \exp(x)} \tag{9} σ(x)=1+exp(x)1=1+exp(x)exp(x)(9)
x ≥ 0 x \geq 0 x0时,我们根据 e x e^{x} ex的图像,我们取 1 1 + exp ⁡ ( − x ) \frac{1}{1 + \exp(-x)} 1+exp(x)1的形式;

x < 0 x < 0 x<0时,我们取 exp ⁡ ( x ) 1 + exp ⁡ ( x ) \frac{\exp(x)}{1 + \exp(x)} 1+exp(x)exp(x)

# 原来的做法
def sigmoid_naive(x):
  return 1 / (1 + math.exp(-x))
  
# 优化后的做法
def sigmoid(x):
  if x < 0:
    return math.exp(x) / (1 + math.exp(x))
  else:
    return 1 / (1 + math.exp(-x))
   

然后用不同的数值进行测试:

> sigmoid_naive(2000)
1.0
> sigmoid(2000)
1.0
> sigmoid_naive(-2000)
OverflowError: math range error
> sigmoid(-2000)
0.0

References


  1. The Log-Sum-Exp Trick ↩︎

  2. 一文弄懂交叉熵损失 ↩︎

  3. Exp-normalize trick ↩︎

STM32F103是意法半导体(STMicroelectronics)推出的一款32位单片机系列,属于Cortex-M3内核。下面是一份简要的说明,帮助你了解STM32F103。 1. 内核架构:STM32F103采用ARM Cortex-M3内核,具有较高的性能和低功耗特性。它运行在最高72MHz频率下。 2. 存储器:STM32F103具有不同的存储器选项,包括闪存(Flash)和随机存取存储器(SRAM)。闪存用于存储程序代码和常量数据,SRAM用于存储变量和堆栈。 3. 外设:STM32F103拥有丰富的外设,包括通用定时器、串行通信接口(USART、SPI、I2C)、模数转换器(ADC)、通用输入输出引脚(GPIO)等。这些外设可用于实现各种应用,如控制、通信和传感器接口。 4. 开发环境:对于STM32F103的开发,你可以使用ST提供的官方开发工具——STM32CubeIDE,它是基于Eclipse的集成开发环境。此外,你还可以使用其他第三方软件,如Keil MDK或IAR Embedded Workbench。 5. 编程语言:你可以使用C/C++编程语言进行STM32F103的开发。ST提供了丰富的库函数和示例代码,方便开发者快速上手。 6. 资源:为了更好地了解STM32F103,你可以参考ST官方的技术文档、数据手册和应用笔记。此外,CSDN等网站上也有很多关于STM32F103的教程和案例供你学习参考。 需要注意的是,上述信息只是对STM32F103的一个简要介绍,如果你希望深入了解它的特性和开发方法,建议你查阅更多资料并进行实际的开发练习。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

愤怒的可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值