softmax函数
它接受一个向量(或者一组变量)作为输入,每个变量指数化后除以所有指数化变量之和,(顺便说一下,sigmoid函数就是其输入数为2时的特例),有点类似于对输入进行归一化,事实上它就叫做归一化指数函数
为了更直观的理解先来看一个栗子,现有一个向量如下
将其作为softamax的输入,先对其进行指数化:
最后就是:
得到的向量所有元素之和为1,每一个元素表示其概率。例如,在MNIST识别手写数字(0到9)的任务中,
就是一个包含10个元素的向量,
计算的结果的每个元素的值表示所要识别的数字为该数字的概率,取概率最高的作为结果
基本上的原理和功能都说明白了,不过在实际计算过程中有事会遇到一些问题,比如遇到
为这种情况:
碰到这种情况再计算
的话就可能因为数字过大而导致溢出,所以在进行计算之前需要读输入
进行一些处理,将
的每个元素都减上
的最大值,再进行Softmax计算:
可以看到,将所有元素都减去同一个值并不会改变最后的结果。以上,是softmax是计算过程
交叉熵损失函数(CrossEntropyLoss)
网络的训练需要有损失函数,而softmax对应的损失函数就是交叉熵损失函数,它多作做分类任务中,计算公式如下:
上式中,
是softmax的计算结果;
是训练样本的标签,表示该样本正确的分类类型,如果以向量表示的话,其中只有一个元素为1,其余元素都为0:
基于这个特性,所以损失大小只与网络判断正确分类的概率有关。举个栗子,一个样本的正确分类是第t类,即
,
,所以损失的公式可以简化为:
来看看对数函数的图像:
因为
是网络判断该样本属于第t类的概率,
,所以
的值越接近1,损失越小(趋近0),所以
的值越接近0,损失越大(趋近无穷大)
梯度计算
首先是损失关于
的梯度。因为损失是一个标量(数值),而
是一个向量,所以损失关于
的梯度也是一个向量
因为公式损失的计算公式很简单,所以梯度推导也不复杂,下面直接写公式:
最后得到:
接下来是是softmax梯度的推导,先回顾下softmax函数的公式:
将其计算路径如下图所示:
观察上图中的计算路径,可以将
看作
的多元复合函数
所以,该函数的导数偏导计算公式如下:
(1)
为了方便理解,顺便贴一下求导路径:
式(1)分为两部分,分别来计算:
整理得到:
(2)
然后是另一部分:
(3)
将(2),(3)的结果带入(1)中,得到:
(4)
在实现中,如果是将softmax实现为独立的层(Layer)的话,根据到这一步为止的讨论已经可以写出softmax的代码了:
class Softmax:
def forward(self, x):
v = np.exp(x - x.max(axis=-1, keepdims=True))
self.a = v / v.sum(axis=-1, keepdims=True)
return self.a
def backward(self, y):
return self.a * (eta - np.einsum('ij,ij->i', eta, self.a, optimize=True))
上面代码中反向计算的实现使用了爱因斯坦求和约定,不太了解的可以参考这个链接
然而在一些网络的实现中,softmax层并不包含在其中,例如,有一个分类任务,所有的样本分别属于三类,假设对于某个样本,网络的最后一层的输出为:
显然,现在已经知道网络对该样本类别的推断了(softmax不过是进一步计算各个类别的概率而已)
所以,交叉熵损失的梯度计算中会包含softmax的这部分,所以把
的具体值代入到(4)中:
因为
只有一个元素为1,其余都为0,所以
,最终得到:
最后,是交叉熵损失函数的实现代码:
class CrossEntropyLoss:
def __init__(self):
# 内置一个softmax作为分类器
self.classifier = Softmax()
def backward(self):
return self.classifier.a - self.y
def __call__(self, a, y):
'''a: 批量的样本输出y: 批量的样本真值return: 该批样本的平均损失'''
a = self.classifier.forward(a) # 得到各个类别的概率
self.y = y
loss = np.einsum('ij,ij->', y, np.log(a), optimize=True) / y.shape[0]
return -loss