softmax loss函数和求导以及数值稳定性讨论

转:

https://zhuanlan.zhihu.com/p/27223959

目录

背景与定义

导数

softmax的计算与数值稳定性

Loss function

对数似然函数

交叉熵

Loss function求导


Softmax函数

背景与定义

在Logistic regression二分类问题中,我们可以使用sigmoid函数将输入Wx + b映射到(0, 1)区间中,从而得到属于某个类别的概率。将这个问题进行泛化,推广到多分类问题中,我们可以使用softmax函数,对输出的值归一化为概率值。

这里假设在进入softmax函数之前,已经有模型输出C值,其中C是要预测的类别数,模型可以是全连接网络的输出a,其输出个数为C,即输出为a_{1}, a_{2}, ..., a_{C}

所以对每个样本,它属于类别i的概率为:

y_{i} = \frac{e^{a_i}}{\sum_{k=1}^{C}e^{a_k}} \ \ \ \forall i \in 1...C

通过上式可以保证\sum_{i=1}^{C}y_i = 1,即属于各个类别的概率和为1。

导数

对softmax函数进行求导,即求

\frac{\partial{y_{i}}}{\partial{a_{j}}}

i项的输出对第j项输入的偏导。
代入softmax函数表达式,可以得到:

\frac{\partial{y_{i}}}{\partial{a_{j}}} = \frac{\partial{ \frac{e^{a_i}}{\sum_{k=1}^{C}e^{a_k}} }}{\partial{a_{j}}}

用我们高中就知道的求导规则:对于

f(x) = \frac{g(x)}{h(x)}

它的导数为

f'(x) = \frac{g'(x)h(x) - g(x)h'(x)}{[h(x)]^2}

所以在我们这个例子中,

g(x) = e^{a_i} \\ h(x) = \sum_{k=1}^{C}e^{a_k}

上面两个式子只是代表直接进行替换,而非真的等式。

e^{a_i}(即g(x))对a_j进行求导,要分情况讨论:

  1. 如果i = j,则求导结果为e^{a_i}
  2. 如果i \ne j,则求导结果为0

再来看\sum_{k=1}^{C}e^{a_k}a_j求导,结果为e^{a_j}

所以,当i = j时:

\frac{\partial{y_{i}}}{\partial{a_{j}}} = \frac{\partial{ \frac{e^{a_i}}{\sum_{k=1}^{C}e^{a_k}} }}{\partial{a_{j}}}= \frac{ e^{a_i}\Sigma - e^{a_i}e^{a_j}}{\Sigma^2}=\frac{e^{a_i}}{\Sigma}\frac{\Sigma - e^{a_j}}{\Sigma}=y_i(1 - y_j)

i \ne j时:

\frac{\partial{y_{i}}}{\partial{a_{j}}} = \frac{\partial{ \frac{e^{a_i}}{\sum_{k=1}^{C}e^{a_k}} }}{\partial{a_{j}}}= \frac{ 0 - e^{a_i}e^{a_j}}{\Sigma^2}=-\frac{e^{a_i}}{\Sigma}\frac{e^{a_j}}{\Sigma}=-y_iy_j

其中,为了方便,令\Sigma = \sum_{k=1}^{C}e^{a_k}

对softmax函数的求导,我在两年前微信校招面试基础研究岗位一面的时候,就遇到过,这个属于比较基础的问题。

softmax的计算与数值稳定性

在Python中,softmax函数为:

def softmax(x):
    exp_x = np.exp(x)
    return exp_x / np.sum(exp_x)

传入[1, 2, 3, 4, 5]的向量

>>> softmax([1, 2, 3, 4, 5])
array([ 0.01165623,  0.03168492,  0.08612854,  0.23412166,  0.63640865])

但如果输入值较大时:

>>> softmax([1000, 2000, 3000, 4000, 5000])
array([ nan,  nan,  nan,  nan,  nan])

这是因为在求exp(x)时候溢出了:

import math
math.exp(1000)
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# OverflowError: math range error

一种简单有效避免该问题的方法就是让exp(x)中的x值不要那么大或那么小,在softmax函数的分式上下分别乘以一个非零常数:

y_{i} = \frac{e^{a_i}}{\sum_{k=1}^{C}e^{a_k}}= \frac{Ee^{a_i}}{\sum_{k=1}^{C}Ee^{a_k}}= \frac{e^{a_i+log(E)}}{\sum_{k=1}^{C}e^{a_k+log(E)}}= \frac{e^{a_i+F}}{\sum_{k=1}^{C}e^{a_k+F}}

这里log(E)是个常数,所以可以令它等于F。加上常数F之后,等式与原来还是相等的,所以我们可以考虑怎么选取常数F。我们的想法是让所有的输入在0附近,这样e^{a_i}的值不会太大,所以可以让F的值为:

F = -max(a_1, a_2, ..., a_C)

这样子将所有的输入平移到0附近(当然需要假设所有输入之间的数值上较为接近),同时,除了最大值,其他输入值都被平移成负数,e为底的指数函数,越小越接近0,这种方式比得到nan的结果更好。

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

>>> softmax([1000, 2000, 3000, 4000, 5000])
array([ 0.,  0.,  0.,  0.,  1.])

当然这种做法也不是最完美的,因为softmax函数不可能产生0值,但这总比出现nan的结果好,并且真实的结果也是非常接近0的。

 

UPDATE(2017-07-07):

有同学问这种近似会不会影响计算结果,为了看原来的softmax函数计算结果怎么样,尝试计算`softmax([1000, 2000, 3000, 4000, 5000])`的值。由于numpy是会溢出的,所以使用Python中的bigfloat库。

import bigfloat

def softmax_bf(x):
	exp_x = [bigfloat.exp(y) for y in x]
	sum_x = sum(exp_x)
	return [y / sum_x for y in exp_x]

res = softmax_bf([1000, 2000, 3000, 4000, 5000])
print('[%s]' % ', '.join([str(x) for x in res]))

结果:

[6.6385371046556741e-1738, 1.3078390189212505e-1303, 2.5765358729611501e-869, 5.0759588975494576e-435, 1.0000000000000000]

可以看出,虽然前四项结果的量级不一样,但都是无限接近于0,所以加了一个常数的softmax对原来的结果影响很小。

 

Loss function

对数似然函数

机器学习里面,对模型的训练都是对Loss function进行优化,在分类问题中,我们一般使用最大似然估计(Maximum likelihood estimation)来构造损失函数。对于输入的x,其对应的类标签为t,我们的目标是找到这样的\theta使得p(t|x)最大。在二分类的问题中,我们有:

p(t|x) = (y)^t(1-y)^{1-t}

其中,y = f(x)是模型预测的概率值,t是样本对应的类标签。

将问题泛化为更一般的情况,多分类问题:

p(t|x) = \prod_{i=1}^{C}P(t_i|x)^{t_i} = \prod_{i=1}^{C}y_i^{t_i}

由于连乘可能导致最终结果接近0的问题,一般对似然函数取对数的负数,变成最小化对数似然函数。

-log\ p(t|x) = -log \prod_{i=1}^{C}y_i^{t_i} = -\sum_{i = i}^{C} t_{i} log(y_{i})

交叉熵

说交叉熵之前先介绍相对熵,相对熵又称为KL散度(Kullback-Leibler Divergence),用来衡量两个分布之间的距离,记为D_{KL}(p||q)

\begin{split}D_{KL}(p||q) &= \sum_{x \in X} p(x) log \frac{p(x)}{q(x)} \\& =\sum_{x \in X}p(x)log \ p(x) - \sum_{x \in X}p(x)log \ q(x) \\& =-H(p) - \sum_{x \in X}p(x)log\ q(x)\end{split}

这里H(p)p的熵。

假设有两个分布pq,它们在给定样本集上的交叉熵定义为:

CE(p, q) = -\sum_{x \in X}p(x)log\ q(x) = H(p) + D_{KL}(p||q)

从这里可以看出,交叉熵和相对熵相差了H(p),而当p已知的时候,H(p)是个常数,所以交叉熵和相对熵在这里是等价的,反映了分布pq之间的相似程度。关于熵与交叉熵等概念,可以参考该博客再做了解。

回到我们多分类的问题上,真实的类标签可以看作是分布,对某个样本属于哪个类别可以用One-hot的编码方式,是一个维度为C的向量,比如在5个类别的分类中,[0, 1, 0, 0, 0]表示该样本属于第二个类,其概率值为1。我们把真实的类标签分布记为p,该分布中,t_i = 1i属于它的真实类别c。同时,分类模型经过softmax函数之后,也是一个概率分布,因为\sum_{i = 1}^{C}{y_i} = 1,所以我们把模型的输出的分布记为q,它也是一个维度为C的向量,如[0.1, 0.8, 0.05, 0.05, 0]。
对一个样本来说,真实类标签分布与模型预测的类标签分布可以用交叉熵来表示:

l_{CE} = -\sum_{i = 1}^{C}t_i log(y_i)

可以看出,该等式于上面对数似然函数的形式一样!

最终,对所有的样本,我们有以下loss function:

L = -\sum_{k = 1}^{n}\sum_{i = 1}^{C}t_{ki} log(y_{ki})

其中t_{ki}是样本k属于类别i的概率,y_{ki}是模型对样本k预测为属于类别i的概率。

Loss function求导

对单个样本来说,loss functionl_{CE}对输入a_j的导数为:

\frac{\partial l_{CE}}{\partial a_j} = -\sum_{i = 1}^{C}\frac {\partial t_i log(y_i)}{\partial{a_j}} = -\sum_{i = 1}^{C}t_i \frac {\partial log(y_i)}{\partial{a_j}} = -\sum_{i = 1}^{C}t_i \frac{1}{y_i}\frac{\partial y_i}{\partial a_j}

上面对\frac{\partial{y_{i}}}{\partial{a_{j}}}求导结果已经算出:

i = j时:\frac{\partial{y_{i}}}{\partial{a_{j}}} = y_i(1 - y_j)

i \ne j时:\frac{\partial{y_{i}}}{\partial{a_{j}}} = -y_iy_j

所以,将求导结果代入上式:

\begin{split}-\sum_{i = 1}^{C}t_i \frac{1}{y_i}\frac{\partial y_i}{\partial a_j}&= -\frac{t_i}{y_i}\frac{\partial y_i}{\partial a_i} - \sum_{i \ne j}^{C} \frac{t_i}{y_i}\frac{\partial y_i}{\partial a_j} \\& = -\frac{t_j}{y_i}y_i(1 - y_j) - \sum_{i \ne j}^{C} \frac{t_i}{y_i}(-y_iy_j) \\& = -t_j + t_jy_j + \sum_{i \ne j}^{C}t_iy_j = -t_j + \sum_{i = 1}^{C}t_iy_j \\& = -t_j + y_j\sum_{i = 1}^{C}t_i = y_j - t_j\end{split}

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值