深度学习被你忽略的细节系列篇——SoftmaxLogSumExp和Sigmoid

Posted AI蜗牛之家

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深度学习被你忽略的细节系列篇——SoftmaxLogSumExp和Sigmoid相关的知识,希望对你有一定的参考价值。

平时我们基本用pytorch或者tensorflow框架时,基本对特别底层的函数实现关注不多,仅限于知道公式的原理。但是很多大佬往往自己会实现一些源码(比如ListNet复现),在看这些源码时,经常出现各种有点难以理解的代码,本来很简单的东西,莫名其妙的各种转换,化简完之后可能感觉是一样的,这么费劲周折的折腾啥?殊不知还是对底层的实现原理了解少了,虽然有些源码不需要我们从底层造轮子(完全从底层造轮子也影响效率),但是能理解其原理在我们debug以及看一些源码时不至于太多疑惑(毕竟国外很多大佬都喜欢实现一些底层utils)。

今天我们来重新认识一下我们经常用的Softmax、LogSumExp和Sigmoid

1. 背景概要

我们知道编程语言中的数值都有一个表示范围的,如果数值过大,超过最大的范围,就是上溢;如果过小,超过最小的范围,就是下溢。
今天要讨论的Softmax、LogSumExp和Sigmoid,就面临着上述溢出的问题,下面的一些梳理也主要用来解决计算Softmax或CrossEntropy时出现的上溢(overflow)或下溢(underflow)问题。

2. Softmax

在机器学习中,计算概率输出基本都需要经过Softmax函数,它的公式应该很熟悉了吧
Softmax ( x i ) = exp ⁡ ( x i ) ∑ j = 1 n exp ⁡ ( x j ) (1) \\textSoftmax(x_i) = \\frac\\exp(x_i)\\sum_j=1^n \\exp(x_j) \\tag1 Softmax(xi)=j=1nexp(xj)exp(xi)(1)
但是Softmax存在上溢和下溢大问题。如果 x i x_i xi太大,对应的指数函数也非常大,此时很容易就溢出,得到nan结果;如果 x i x_i xi太小,或者说负的太多,就会导致出现下溢而变成0,如果分母变成0,就会出现除0的结果。
此时我们经常看到一个常见的做法是(其实用到的是指数归一化技巧, exp-normalize),先计算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 ) \\beginaligned \\textSoftmax(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) \\\\ &= \\textSoftmax(x_i - b) \\endaligned 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)
这种转换是等价的,经过这一变换,就避免了上溢,最大值变成了 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])

关于softmax的另外实现,参加下文。

3. LogSumExp

什么是LSE?
LSE被定义为参数指数之和的对数:
LSE ( x 1 , ⋯   , x n ) = log ⁡ ∑ i = 1 n exp ⁡ ( x i ) = log ⁡ ( exp ⁡ ( x 1 ) + ⋯ + exp ⁡ ( x n ) ) \\textLSE(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维的向量,输出是一个标量。

什么场景下要到LogSumExp呢?
交叉熵loss大家肯定不陌生,其中就有一项是log p p p,比如多分类场景里面就需要在对softmax的结果做对数处理:
log ⁡ ( Softmax ( x i ) ) = log ⁡ exp ⁡ ( x i ) ∑ j = 1 n exp ⁡ ( x j ) = x i − log ⁡ ∑ j = 1 n exp ⁡ ( x j ) \\beginaligned \\log \\left( \\textSoftmax(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) \\\\ \\endaligned log(Softmax(xi))=logj=1nexp(xj)exp(xi)=xilogj=1nexp(

以上是关于深度学习被你忽略的细节系列篇——SoftmaxLogSumExp和Sigmoid的主要内容,如果未能解决你的问题,请参考以下文章

被你忽略掉的 Java 细节知识

被你忽略掉的 Java 细节知识

点云深度学习系列博客: Point Transformer方法概述

原创 深度学习与TensorFlow 动手实践系列 - 3第三课:卷积神经网络 - 基础篇

右键的秘密之被你忽略的样式布局

重磅|深度学习ResNet网络模型发明者解析结构细节