斯坦福自然语言处理习题课2---softmax函数详解

从现在开始,我们就要正式开始向大家讲解斯坦福大学CS224n作业的实现了。我们首先业看作业关于softmax函数实现部分。我们在这里将先向大家介绍softmax函数的具体应用场景和物理意义,以及采用numpy和python实现中需要注意的地方,在下一篇文章中,我们再向大家介绍CS224n作业1中softmax的具体实现。之所以这样安排,是因为数学是一个非常优雅的建模工具,可以非常优雅的描述物理过程,但是由于数学这方面太过优雅,也容易使人们只关注数学模型,而对后面的物理过程反而忽略了。所以在这里我们首先强调物理过程,然后才是数学原理,最后是具体实现细节。
我们首先来看softmax函数的典型应用场景。softmax函数最典型的应用场景是作为多分类问题的神经网络输出层的**函数。这句话很难理解,我们可以以一个具体的例子来加深同学们的理解。
我们以大家都很熟悉的MNIST手写数字识别数据集为例,这个数据集基本相当于深度学习领域的Hello World。如下图所示:
斯坦福自然语言处理习题课2---softmax函数详解
如图所示,MNIST手写数据集中,每个训练样本是一个黑底白字,分辨率为2828的黑白图片,将这个图片784(784=2828)个像素点,组成一个向量,就是神经网络的输入信号。神经网络可以取各种神经网络,如多层感知器MLP、卷积神经网络CNN等,这些网络的输出层,通常设计有10个神经元,分别代表0~9这10个数字,并且我们可以训练网络,使这10个神经元的值越大,表明该神经元所代表的数字出现的可能性越大,我们可以取这10个神经元中输出值最大的那个神经元所代表数字作为识别结果。这就是一个神经网络多分类系统的一个简单的描述。
但是直接使用输出层神经元的输出值,比较不直观,这个数值本身没有意义,只有与其他神经元的输出值相比较后,才有意义。例如上图中,第一个输出层神经元的输出为225,这个神经元代表数字0,仅知道第一个神经元的输出为225,我们不能得出任何结论。比如说如果其他输出层神经元输出值只有几个或几十的话,这个值就比较大了,所以可以判定识别结果是数字0。但是如果其他输出层神经元的输出都是几千几万,那么225这个值就很小了,那么识别结果就不可能为数字0了。由此可见,直接使用神经元的输出信号比较麻烦,这时我们就可以引入softmax函数。
softmax函数就是将输出层神经元的输出值,转化为该神经元所代表的数字出现的概率,并且所有神经元的概率之和等于1,因为我们研究的问题性质决定识别结果必定是0~9这10个数字之一。如下图所示:
斯坦福自然语言处理习题课2---softmax函数详解
如上图所示,图中下面一层的圆圈代表神经网络的输出,每个神经元的输出表明该神经元所代表的数字出现的概率,这时我们还以第一个神经元为例,其值为0.15,表明数字0出现的概率是15%,所以识别结果就不太可能是数字0。出现概率最大的神经元是代表数字5的神经元,因此这个神经网络的识别结果就会是数字5。神经网络的识别结果是不是正确的呢?因为我们是监督学习,我们是有正确识别结果的,在上图中就是最上面一层圆圈所示的结果,其中为1的圆圈就是这个训练样本对应的正确识别结果。我们看到正确识别结果是6,而我们神经网络的识别结果却是5,这表明我们神经网络的识别结果是错误,需要训练我们的神经网络,才能产生正确的结果。所以在实际应用中,会对上面两层圆圈分别对应的神经网络输出和正确输出,做交叉熵(Cross Entropy),然后采用例如随机梯度下降算法,对神经网络的参数进行调整,达到能够输出正确识别结果的目的。这些内容我们将在后续课程中详细讲解,在本节中重点是向大家介绍softmax函数,大家也只需关注softmax函数相关内容即可,其他内容大致有个了解即可。

softmax函数

我们用zilz^l_i代表输出层为神经网络的第ll层第ii神经元的输出信号,则softmax函数定义为:
y^i=softmax(zi)=ezij=1Kezj \hat{y}_i = softmax(z_i)=\frac{e^{z_i}}{\sum_{j=1}^{K}e^{z_j}}
我们习惯称神经网络的输出为y^\hat{y},而正确的结果为yy
有了上面的函数定义,我们可以很容易的写出求softmax函数的程序:

import numpy as np 

def main():
    z = np.array([3, 2, 1], dtype=np.float32)
    z = np.exp(z)
    denominator = np.sum(z)
    z /= denominator
    print(z)

if '__main__' == __name__:
    main()

运行结果为:
斯坦福自然语言处理习题课2---softmax函数详解
这个程序非常简单,同学们可能会有选这门课上当了的感觉。但是如果只有这么简单,那么我们开这门课还会有什么意义呢?我们来看下面这个程序:

import numpy as np 

def main():
    z = np.array([3, 2000000, 1], dtype=np.float32)
    z = np.exp(z)
    denominator = np.sum(z)
    z /= denominator
    print(z)

if '__main__' == __name__:
    main()

程序一点没变,只是数组z的第2维由原来的2变为200000了,我们运行一下,结果如下所示:
斯坦福自然语言处理习题课2---softmax函数详解
这是怎么回事呢?为了探究这个问题的原因,我们先来看一下指数函数曲线,如下所示:

import numpy as np 
import matplotlib.pyplot as plt

def main():
    x = np.linspace(-10, 10, 100)
    y = np.exp(x)
    plt.plot(x, y)
    plt.show()

if '__main__' == __name__:
    main()

在这个程序中我们使用matplotlib来绘制图形,这个库我们在课程后面会经常用到,功能非常强大,我们会在用到的时候再详细给大家讲解,这里就用其最基本的绘图功能。绘制出来的曲线如下所示:
斯坦福自然语言处理习题课2---softmax函数详解
如图所示,我们看到,当x的值大于5左右时,函数的值就开始剧烈增长了,当x=200000时,可想而知是一个多大的值了。我们知道计算机表示的数值是一定范围的,对200000取e为底的指数时,计算机会产生溢出,会得到一个无穷大的结果。我们接着对这个数再做运算时,就会产生Not a Number错误,就是运行结果中的nan。这说明我们上面的softmax函数实现是有问题的。那么怎么来解决这个问题呢?其实斯坦福大学的老师在作业里已经给了我们解决方案,大家看作业1的assignment1.pdf中,有这样一个需要大家证明的问题:
斯坦福自然语言处理习题课2---softmax函数详解
对于这个问题的证明,我们将在课程稍后时间来讲解,这里先给大家讲解一下怎么来用这个性质来解决我们softmax函数实现中的BUG。既然在softmax函数的每一项上加一下常量,softmax函数的值不变,那么在每一项上减一个常数,softmax值也不会变。那么我们可以在每一项上减去所有项的最大值,这样softmax函数的每一项就变最大为0的数值了,这样就不会出现溢出的问题了,基于这个思路,我们就有了第二版的softmax函数实现:

import numpy as np 

def main():
    z = np.array([3, 200000, 1], dtype=np.float32)
    z -= np.max(z)
    z = np.exp(z)
    denominator = np.sum(z)
    z /= denominator
    print(z)

if '__main__' == __name__:
    main()

可以看到,我们的程序并没有进行大的修改,只是把z的每一项均减一下最大值,我们来看一下运行结果:
斯坦福自然语言处理习题课2---softmax函数详解
我们看这样就可以得到正确的结果了。我们可以庆祝一下,我们终于做出了一个正确的softmax函数。但是其实即使是这个函数,我们也还是有可以改进的地方,如\ref{c000004}的第6行,我们使用z=np.exp(z)的形式,这样就会返回一个与z维度相同的数组,元素为z中元素取以e为底指数的值。为了提高效率,我们可以直接将取以e为底指数的值放到原始数组z中,如下所示:

import numpy as np 

def main():
    z = np.array([3, 200000, 1], dtype=np.float32)
    z -= np.max(z)
    np.exp(z, z)
    denominator = np.sum(z)
    z /= denominator
    print(z)

if '__main__' == __name__:
    main()

softmax函数性质证明

接下来我们证明我们解决方案的正确性:
斯坦福自然语言处理习题课2---softmax函数详解
证明过程如下所示:
y^i=softmax(zi+C)=ezi+Cj=1Kezj+C=eCezij=1KeCeej=eCezieCj=1Keej=ezij=1Kezj \hat{y}_i = softmax(z_i+C)=\frac{e^{z_i+C}}{\sum_{j=1}^{K}e^{z_j+C}}\\ =\frac{e^C \cdot e^{z_i}}{\sum_{j=1}^{K}e^C \cdot e^{e_j}}\\ =\frac{e^C \cdot e^{z_i}}{e^C \cdot \sum_{j=1}^{K}e^{e_j}}\\ =\frac{e^{z_i}}{\sum_{j=1}^{K}e^{z_j}}\\
在下一节中,我们将带领大家实现建立Python虚拟开发环境,将作业由python2移植到python3,采用in place方式提高计算效率,最后简单介绍一下作业最后的测试驱动开发(TDD)的理念。
如果大家觉得观看文章不够直观,请移步到我们的视频课程:斯坦福自然语言处理习题课https://study.163.com/course/introduction.htm?courseId=1006361019&share=2&shareId=400000000383016)