Prince Home Stay Follish, Stay Hungry

Softmax函数求导详解


Softmax函数有和Sigmoid函数类似的功能,Sigmoid常常被用于二分类问题的顶层,作为类标为1的概率。当问题被推广为多分类问题时,Sigmoid函数就不能使用了,此时往往使用Softmax函数。作为机器学习中多分类的基本函数,掌握其梯度的计算方法是非常重要的,因此本篇博客重点详解Softmax函数的计算方法,并使用numpy实现Softmax配合L2损失函数的求导方法,最后给出了常用的softmax配合cross-entropy损失函数的计算方法。

Softmax求导

前言

Softmax函数有和Sigmoid函数类似的功能,Sigmoid常常被用于二分类问题的顶层,作为类标为1的概率。当问题被推广为多分类问题时,Sigmoid函数就不能使用了,此时往往使用Softmax函数。作为机器学习中多分类的基本函数,掌握其梯度的计算方法是非常重要的,因此本篇博客重点详解Softmax函数的计算方法,并使用numpy实现Softmax配合L2损失函数的求导方法,最后给出了常用的softmax配合cross-entropy损失函数的计算方法。

1. Softmax函数的定义和基本求导

对于C个类别的分类问题,可以把输出结果定义为一个C维的向量$z^C = [z_0, z_1 … z_C]$,其中,如$C=3$可以认为输出结果是一个3维向量$[2, 3, 4]$,但是我们需要的是概率,所以要使用Softmax把向量转换为概率,Softmax函数的定义如下:

如上例,把$z$转换为$[\frac{e^{z_0}}{e^{z_0}+e^{z_1}+e^{z_2}}, \frac{e^{z_1}}{e^{z_0}+e^{z_1}+e^{z_2}}, \frac{e^{z_2}}{e^{z_2}+e^{z_1}+e^{z_2}}]$,也就是$[\frac{e^{2}}{e^{2}+e^{3}+e^{5}}, \frac{e^{3}}{e^{2}+e^{3}+e^{5}}, \frac{e^{4}}{e^{2}+e^{3}+e^{3}}]$,然后就可以当做概率与真正的$y$求出loss,反向传播。

对于softmax的求导其实比较简单,根据除法求导法则

有导数:

所以对$z_k$求偏导可得:

由于求偏导数,

所以:

但是,由于不光是$a_k$对$z_k$有导数,其实$a_0$对$z_1$也是有导数的,为什么呢,因为在$a_0$的计算式子中也有$z_1$嘛,所以当$i \neq j$时有

2. Softmax与任意损失函数的梯度计算

上节我们推导了softmax函数中$\frac{\partial{a_i}}{\partial{z_i}}$和$\frac{\partial{a_i}}{\partial{z_j}}$的的计算,但是实际上在神经网络中,softmax都是配合着损失函数来使用的,因此需要加上loss函数$L$进行求导,又$L$向$a$反向传播梯度,然后再由$a$向$z$传播,最终才能实现梯度下降,

通过softmax的前向计算我们发现,每一个$a_i$计算都是有所有的$z_1,z_2,z_3$完成的,因此梯度的计算我们也应该将每一个$L$的梯度反传到每一个$z$上,仍然使用上面的例子,我们可以计算$\frac{\partial{L}}{\partial{z_1}}$:

然后代入上面的两种情况,等式右边三个元素的第一个属于$i = j$的情况,使用$a_1 (1 - a_1)$代换,其余两项使用$-a_2 a_1$和$-a_3 a_1$代换,得到:

进一步化简,把$a_1$提出来,得到:

对于$a_2$和$a_3$我们也能得到相似的结果,最终可以得到泛化的求导表达式:

在实现层面,$z$代表网络输出的logit,$a$代表$z$经过softmax变换后的0~1的概率,而$\frac{\partial{L}}{\partial{a_i}}$代表损失函数对于概率向量的梯度dout,由于每个损失函数不同,梯度的计算方法也不一样,这里我们使用L2损失函数做例子给出实现:

import numpy as np

# 先定义softmax函数,使用最大值做rescale,让数值更稳定
def softmax(logits):
    max_value = np.max(logits)
    exp = np.exp(logits - max_value)
    exp_sum = np.sum(exp)
    dist = exp / exp_sum
    return dist

# 定义L2损失函数计算
def l2_loss(pred, label):
    """
    pred: 概率,0~1
    label: one-hot编码后的label
    """
    diff = np.abs(pred - label) ** 2
    return diff.mean()

# 前向传播函数
def forward(z, label):
    a = softmax(z)
    loss = l2_loss(a, label)
    return loss, (z, a, label) # 返回cache用于计算梯度

# 反向传播函数
def backward(loss, cache):
    z, a, label = cache
    # 计算dL / da
    dLda = (a - label) * 2 / a.shape[0] 
    
    # 计算dL / dz,这里就用到了上面推导的公式
    dLdz = a * (dLda - (dLda * a).sum())
    return dLdz

# 最后定义一个使用数值法求导函数检验公式的正确性
def backward_naive(z, label):
    delta = 1e-10
    _grad = np.zeros_like(z)
    
    for i, zi in enumerate(z):
        # 把z的每一个值都加减一个很小的偏移量delta
        origin = zi
        
        # 减偏移量
        z[i] = origin - delta
        p1 = softmax([z]).flatten()
        loss1 = l2_loss(p1, label)
        
        # 加偏移量
        z[i] = origin + delta
        p2 = softmax(z)
        loss2 = l2_loss(p2, label)
        
        # 求斜率作为导数
        _grad[i] = (loss2 - loss1) / (2 * delta)
        z[i] = origin
        
    return _grad

# 测试主函数
z = np.array([1, 2, 3, 0, 1], dtype=np.float)  # 任意生成一个logit
label = np.array([0., 0., 0., 1., 0])  # one-hot编码的label

# 计算梯度
loss, cache = forward(z, label)
grad = backward(loss, cache)  
#Out[]: array([-0.00969502, -0.01434905,  0.0496989 , -0.01595981, -0.00969502])
grad_naive = backward_naive(z, label)
#Out[]: array([-0.0096953 , -0.01434908,  0.04969886, -0.01596001, -0.00969502])

输出几乎一致,说明我们的计算方法是正确的。

3. 当损失函数为cross-entropy时简化求导计算

softmax的一个典型配合的loss函数是Cross Entropy,这个函数是对多分类的一个loss函数,用来计算,输出的概率和目标0/1标签的差距,其公式是: 其中,$y$表示one-hot编码的label,求导起来比较简单,因为除了label为1的那个第$t$位置导数为$-\frac{1}{a_t}$,其他的loss计算公式全都是0,所以也就没有梯度,此时,只有label为1的位置向前传梯度,我们假设label为1的位置为$a_t$,用上面那个例子,softmax的结果是$a = [0.2, 0.3, 0.5]$,而目标的$y = [0, 1, 0]$,那么$L = 0 \cdot log(0.2) + 1 \cdot log(0.3) + 0 \cdot log(0.5)$, 其导数$dL = [0, -\frac{1}{0.3} ,0]$,传下去。

刚才讲到,Cross Entropy的函数的求导,用等式表示其实就是

与Softmax的导数连起来,对于$k=t$的位置的梯度,利用上面的第一个梯度:

而对于其他$k \ne t$的位置来说:

我们简单整理一下上面的推导:label为1的位置概率减1,其他位置概率不变,也就是对z的梯度了。

还是用上面那个例子$z = [2, 3, 4]$,经过softmax结果为$a = [0.0900, 0.2447, 0.6652]$,我们计算出cross-entropy为$L(a, y) = 0 \cdot log(0.0900) + 1 \cdot log(0.2447) + 0 \cdot log(0.6652) = 1.4076$,那么$L$对$z$的导数为:$\frac {\partial L}{\partial z_k} = [0.0900, -0.7553, 0.6652]$,就是把label为1的位置概率减1就是梯度。

由于实现中一般Cross Entropy前面还会除以一个n(batch size),所以,最后的$da_k$一般也会除以一个n

具体求导实现

参考CS231n中softmax的方法我们可以写一个简单的softmax回归代码:

#Train a Linear Classifier

# initialize parameters randomly
W = 0.01 * np.random.randn(D, K)
b = np.zeros((1, K))

# some hyperparameters
step_size = 1e-0
reg = 1e-3 # regularization strength

# gradient descent loop
num_examples = X.shape[0]
for i in xrange(200):
  
  # evaluate class scores, [N x K]
  scores = np.dot(X, W) + b 
  
  # compute the class probabilities
  exp_scores = np.exp(scores)
  probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True) # [N x K]
  
  # compute the loss: average cross-entropy loss and regularization
  corect_logprobs = -np.log(probs[range(num_examples), y])
  data_loss = np.sum(corect_logprobs) / num_examples
  reg_loss = 0.5 * reg * np.sum(W * W) # L2 regularization
  loss = data_loss + reg_loss

  if i % 10 == 0:
    print "iteration %d: loss %f" % (i, loss)
  
  # compute the gradient on scores!!!这里就是求softmax+cross entropy的具体实现
  dscores = probs
  dscores[range(num_examples), y] -= 1
  dscores /= num_examples
  
  # backpropate the gradient to the parameters (W,b)
  dW = np.dot(X.T, dscores)
  db = np.sum(dscores, axis=0, keepdims=True)
  
  dW += reg * W # regularization gradient
  
  # perform a parameter update
  W += -step_size * dW
  b += -step_size * db

参考博客


Similar Posts

下一篇 CTC详解

Comments