赞
踩
赞
踩
从现在开始,我们就要正式开始向大家讲解斯坦福大学CS224n作业的实现了。我们首先业看作业关于softmax函数实现部分。我们在这里将先向大家介绍softmax函数的具体应用场景和物理意义,以及采用numpy和python实现中需要注意的地方,在下一篇文章中,我们再向大家介绍CS224n作业1中softmax的具体实现。之所以这样安排,是因为数学是一个非常优雅的建模工具,可以非常优雅的描述物理过程,但是由于数学这方面太过优雅,也容易使人们只关注数学模型,而对后面的物理过程反而忽略了。所以在这里我们首先强调物理过程,然后才是数学原理,最后是具体实现细节。
我们首先来看softmax函数的典型应用场景。softmax函数最典型的应用场景是作为多分类问题的神经网络输出层的激活函数。这句话很难理解,我们可以以一个具体的例子来加深同学们的理解。
我们以大家都很熟悉的MNIST手写数字识别数据集为例,这个数据集基本相当于深度学习领域的Hello World。如下图所示:
如图所示,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个数字之一。如下图所示:
如上图所示,图中下面一层的圆圈代表神经网络的输出,每个神经元的输出表明该神经元所代表的数字出现的概率,这时我们还以第一个神经元为例,其值为0.15,表明数字0出现的概率是15%,所以识别结果就不太可能是数字0。出现概率最大的神经元是代表数字5的神经元,因此这个神经网络的识别结果就会是数字5。神经网络的识别结果是不是正确的呢?因为我们是监督学习,我们是有正确识别结果的,在上图中就是最上面一层圆圈所示的结果,其中为1的圆圈就是这个训练样本对应的正确识别结果。我们看到正确识别结果是6,而我们神经网络的识别结果却是5,这表明我们神经网络的识别结果是错误,需要训练我们的神经网络,才能产生正确的结果。所以在实际应用中,会对上面两层圆圈分别对应的神经网络输出和正确输出,做交叉熵(Cross Entropy),然后采用例如随机梯度下降算法,对神经网络的参数进行调整,达到能够输出正确识别结果的目的。这些内容我们将在后续课程中详细讲解,在本节中重点是向大家介绍softmax函数,大家也只需关注softmax函数相关内容即可,其他内容大致有个了解即可。
我们用
z
i
l
z^l_i
zil代表输出层为神经网络的第
l
l
l层第
i
i
i神经元的输出信号,则softmax函数定义为:
y
^
i
=
s
o
f
t
m
a
x
(
z
i
)
=
e
z
i
∑
j
=
1
K
e
z
j
\hat{y}_i = softmax(z_i)=\frac{e^{z_i}}{\sum_{j=1}^{K}e^{z_j}}
y^i=softmax(zi)=∑j=1Kezjezi
我们习惯称神经网络的输出为
y
^
\hat{y}
y^,而正确的结果为
y
y
y。
有了上面的函数定义,我们可以很容易的写出求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()
运行结果为:
这个程序非常简单,同学们可能会有选这门课上当了的感觉。但是如果只有这么简单,那么我们开这门课还会有什么意义呢?我们来看下面这个程序:
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了,我们运行一下,结果如下所示:
这是怎么回事呢?为了探究这个问题的原因,我们先来看一下指数函数曲线,如下所示:
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来绘制图形,这个库我们在课程后面会经常用到,功能非常强大,我们会在用到的时候再详细给大家讲解,这里就用其最基本的绘图功能。绘制出来的曲线如下所示:
如图所示,我们看到,当x的值大于5左右时,函数的值就开始剧烈增长了,当x=200000时,可想而知是一个多大的值了。我们知道计算机表示的数值是一定范围的,对200000取e为底的指数时,计算机会产生溢出,会得到一个无穷大的结果。我们接着对这个数再做运算时,就会产生Not a Number错误,就是运行结果中的nan。这说明我们上面的softmax函数实现是有问题的。那么怎么来解决这个问题呢?其实斯坦福大学的老师在作业里已经给了我们解决方案,大家看作业1的assignment1.pdf中,有这样一个需要大家证明的问题:
对于这个问题的证明,我们将在课程稍后时间来讲解,这里先给大家讲解一下怎么来用这个性质来解决我们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的每一项均减一下最大值,我们来看一下运行结果:
我们看这样就可以得到正确的结果了。我们可以庆祝一下,我们终于做出了一个正确的softmax函数。但是其实即使是这个函数,我们也还是有可以改进的地方,如
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()
接下来我们证明我们解决方案的正确性:
证明过程如下所示:
y
^
i
=
s
o
f
t
m
a
x
(
z
i
+
C
)
=
e
z
i
+
C
∑
j
=
1
K
e
z
j
+
C
=
e
C
⋅
e
z
i
∑
j
=
1
K
e
C
⋅
e
e
j
=
e
C
⋅
e
z
i
e
C
⋅
∑
j
=
1
K
e
e
j
=
e
z
i
∑
j
=
1
K
e
z
j
\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}}\\
y^i=softmax(zi+C)=∑j=1Kezj+Cezi+C=∑j=1KeC⋅eejeC⋅ezi=eC⋅∑j=1KeejeC⋅ezi=∑j=1Kezjezi
在下一节中,我们将带领大家实现建立Python虚拟开发环境,将作业由python2移植到python3,采用in place方式提高计算效率,最后简单介绍一下作业最后的测试驱动开发(TDD)的理念。
如果大家觉得观看文章不够直观,请移步到我们的视频课程:斯坦福自然语言处理习题课(https://study.163.com/course/introduction.htm?courseId=1006361019&share=2&shareId=400000000383016)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。