
2.6 Softmax编码实战
讲了太多的理论知识,接下来我们将动手实现第一个学习模型——Softmax[16]多分类器。该分类器是深度学习中网络输出层的默认分类器,因此也可以认为是最简单的多分类“神经网络”。首先我们会介绍该模型的一些理论知识,然后你需要使用该模型并结合上述我们介绍的机器学习知识,完整地完成CIFAR-10分类图像识别任务。
假如让你来设计一个分类互斥的三分类函数你会怎么设计呢?你可能一开始会有点不知所措,但记住,当我们想要创造点什么的时候,我们首先要思考的其实是我们拥有着什么。
可以把三分类任务当作是三个人来处理,特定的人就代表着擅长于判断特定的类别。那每个人可以做些什么呢?其实很简单,就如同线性回归一样,每个人只是对数据特征进行加权求和。如式(2.21)所示,将数据维度乘以权重再求和,也将其称为评分函数(score function)。

z就表示每个人心中的分类得分,有三个人,也就会有三个z值,选取其中最高分,也就是“最自信”的分值即可。
那这就有一个问题,比如一个识别小猫的任务,得分可能是(50,40,45),也可能是(25,1,2)。那么请问哪一组评分系统好?虽然两组中第一列都是最高得分,但明显后者的相对得分更为突出,那就相当于第二组中的第一列,比其他列有更高的自信心。因此除了关注最高分以外,还要关心其相对比值,也就是将各自得分再除以总分即可。
如式(2.22)所示,就是我们设计的分类公式。非常简单,我们仅仅将所有人的分数之和作为公分母,然后将每个人各自的分数作为分子,最后我们就得到了总和为1的多分类函数,这一步也叫归一化概率(Normalized Probabilities)。

我们将式(2.21)与式(2.22)合并一下,得到式(2.23),就是其完整的表达式,虽然看起来比较复杂,但是所表达含义却非常简单。

上述表达式还缺点东西,因为我们想要预测得分比尽可能的高,但得分函数是线性的,其增长幅度有限。那可不可以在不影响其单调性的情况下,使其相对得分更显著呢?如式(2.24)所示,这就是我们修改之后的表达式,由于指数函数是一个单调函数,因此这样的修改并不会对表达式含义有任何影响。

相应地,我们将这些改变全部写在一起,就是式(2.25)。

我们将其推广至k类,如式(2.26)所示,这就是Softmax函数表达式。

第一眼看见该表达式,可能会有点抓狂,但其实这并不复杂。我们之所以花那么多“无用”的时间来推导出该公式,只是想告诉你,只要你不再害怕,不再抗拒时,静下心来你就会发现这有多简单。
Softmax函数的输出是一个k维向量,比如函数输出为[0.1,0.3,0.7],而该数据的真实标记为第二类,其向量表示就应该是[0,1,0]。那我们如何去刻画其误差呢?你也许会想到将各自的误差加起来,比如使用均方误差作为代价函数,那么该数据的误差就应该如下所示。

但实际上我们多算了一些误差,因为这三个输出是彼此依赖的,调整其中一个,其余都会受到影响。因此正确的代价误差应该如式(2.27)所示。

虽然Softmax的输出为k个输出,但我们仅对其真正关键的那一个输出进行修改就可以。为了简化接下来的描述,我们将引入如式(2.28)所示的示例函数。

其使用方法非常简单,表达式的输出为真则输出1,表达式的输出为假则输出0,例如:I{2+3=4}=0,I{1+1=2}=1。
Softmax代价函数就可表示为式(2.29),其中y表示数据的真实类标,比如y = 3,就表示该数据为第三类数据,该代价函数所表达的含义其实就为遍历所有输出,将真实类标对应的输出的误差,作为我们的代价函数。

对我们来说,更为实际的可能是梯度的计算公式,因为使用代价函数推导梯度计算公式后,我们才能根据其修改权重。这里不进行Softmax的梯度推导,直接给出梯度计算公式,如式(2.30)所示。

其中,,若你不太习惯式(2.30)的写法,可以写成式(2.31),可能更有助于记忆。

这其实就是交叉熵代价函数在多类任务的推广,再将式(2.31)换一种写法就得到式(2.32)。

这就是典型的二分类交叉熵梯度计算公式。
- Softmax参数冗余
Softmax存在着一个问题,那就是参数冗余。假设我们进行二分类任务,Softmax的输出为一个二维向量,就相当于有着两套数据参数,在逻辑回归中,预测生病的概率为0.7,那不生病的概率很自然就是0.3。而使用Softmax的方法使我们计算出生病的概率为0.7,但同样也计算出了不生病的概率为0.3。因此Softmax神经元总是比实际需要多了一套参数,但这相比于神经网络的上亿参数,简直微乎其微,这里仅作为冷门知识了解即可。
2.6.1 编码说明
在本章的练习中我们将要逐步完成以下内容。
1.熟悉使用CIFAR-10数据集;
2.编码softmax_loss_naive函数,使用显式循环计算损失函数以及梯度;
3.编码softmax_loss_vectorized函数,使用向量化表达式计算损失函数及其梯度;
4.编码随机梯度下降算法,训练一个Softmax分类器;
5.使用验证数据选择超参数。
完整的教程请参考“第2章练习-实现softmax.ipynb”文件顺序练习,该处仅仅对重要内容进行解释说明。在本章结尾将会提供参考代码,希望你“走投无路”时再查看这些“锦囊”。请记住“Practice makes perfect”。
首先启动Jupyter:如图2-6所示,启动dos窗口,将路径转换到文件:“第2章练习-实现softmax.ipynb”所在路径,然后输入:jupyter notebook,再按Enter键确认。这时,浏览器会自动启动Jupyer,单击本章练习即可。

图2-6 启动Jupyter
首先,我们需要导入各模块,由于Python 2.7+系列在默认情况下不支持中文字符,因此,你需要在每个文件的开头添加一行“#-*- coding: utf-8 -*-”,才能使用中文注释,否则编译器将会抛出编码异常错误。本章所需完成的代码模块全部存放在文件“DLAction/classifiers/chapter2”中,而诸如数据导入和梯度检验等模块被统一存放在了“DLAction/utils”目录下,读者可自行查看。

2.6.2 熟练使用CIFAR-10 数据集
CIFAR-10是一套包含了60000张,大小为32×32的十分类图片数据集,其中50000张图片被分为了训练数据,10000张图片被分为测试数据,存放在“DLAction/datasets/cifar-10-batches-py”目录下,也可以通过访问互联网地址进行下载http://www.cs.toronto.edu/~kriz/cifar.html。由于我们使用的是彩色图片,因此每张图片都会有三个色道。当我们将磁盘图片导入到内存时,该数据集存放在形状如下的NumPy数组中。

载入CIFAR-10数据的各项操作已经被封装进了load_CIFAR10函数中,只需要载入数据的存放目录,就可以得到划分好的训练数据、训练数据类标、测试数据和测试数据类标。

如果觉得这些数据还是有些抽象,你也可以使用下列的代码块,可视化地查看载入CIFAR-10数据集图片。

其图像如图2-7所示。

图2-7 CIFAR-10数据集
- 数据预处理
数据划分:原始数据量可能稍大,这样不利于我们的编码测试,因此我们会采样250条训练数据作为样本训练数据X_sample,采样100条数据作为样本验证数据X_validation,作为编码阶段测试使用。
数据形状转换:原始的数据集可看成是四维数组(数据个数,宽,高,色道),这样不太方便使用,因此我们将(宽,高,色道)压缩在一维上,其数据形式变为(数据个数,数据维度),而数据维度=宽×高×色道。数据形状转换代码如下所示。

数据归一化(Normalization):在通常情况下,我们需要对输入数据进行归一化处理,也就是使得数据呈均值为零,方差为1的标准正态分布。由于图像的特征范围在[0,255],其方差已经被约束了,我们只需要将数据进行零均值中心化处理即可,不需要将数据压缩在[-1,1]范围(当然,也可以进行此项处理)。因此在处理时,只需要减去其数据均值即可。注意:我们计算的是训练数据的均值,而不是全部数据的均值,你需要时刻警惕不要“偷看期末试卷”,测试数据是“未知的”。数据归一化实现代码如下所示。

添加偏置项:偏置项(bias)也可以看作是线性函数的常数项或截距,在实际中并不特别地区分偏置项参数与权重参数,并且其对于训练效果的影响也非常有限。但为了遵照传统,我们还是将其默认地使用在算法中,通过在数据中增加一维值为常数1的特征作为bias对应的输入特征,然后将其统一到权重参数中,最终的图片数据维度就为dim=32×32×3+1。其实现的代码如下所示。

我们将这些步骤统统封装在get_CIFAR10_data()函数中。运行该函数,将得到以下结果。

2.6.3 显式循环计算损失函数及其梯度
首先使用循环的方式实现Softmax分类器的损失函数(代价函数),打开“DLAction/classifiers/chapter2/softmax_loss.py”文件并实现softmax_loss_naive函数。

注意:请尽量少地使用循环,使用的循环越少,就可以节约更多的计算时间。需要逐渐地适应向量化计算表达,因为采用向量化表达,既书写简洁,使得代码的可读性大大提高,不容易产生错误,又极大地提高了运算效率。例如计算每类得分的指数,就可以直接使用scores_E=np.exp(X[i].dot(W))函数,更多NumPy的用法请参考第1章1.3 Python简易教程。

完成了上述代码后,要怎样判断实现是否正确?一个比较直接的方法就是计算其损失值。在不加入权重惩罚的情况下,所实现的Softmax损失值应该接近于-log(0.1)。为什么是0.1?如果是5分类任务,该损失值又该接近于多少呢?损失值验证代码块如下所示。

其正确结果近似于这样。

接下来我们进行梯度检验,精确的数值梯度是使用极限的方式求解梯度,如式(2.33)所示。

数值梯度的优势是比较精确,但缺点也很明显,那就是速度较慢。因此我们通常求解代价函数的导函数来替换数值梯度,但导函数在人工实现时很容易出错,我们已经实现了以极限方式的数值梯度求解,那么可以使用该函数检验实现的梯度函数。运行下列代码,相对误差应该小于1e-7。

其正确的结果应该如下所示。

2.6.4 向量化表达式计算损失函数及其梯度
完成显式循环计算后,我们将损失函数与梯度的计算过程,使用完全向量形式再完成一遍。向量形式不仅书写简洁,并且也极大地加快了执行效率,对于编程人员来说,一开始使用向量化表达可能十分痛苦,但当慢慢熟悉后会对其着迷。打开“DLAction/classifiers/chapter2/softmax_loss.py”文件,文件实现了softmax_loss_vectorized函数。可以参考第1章中介绍的NumPy广播的用法,不到山穷水尽,请不要偷看参考代码。
提示:比如计算得分时,可以一次性求解所有训练数据scores=np.dot(X,W),此时scores变成形状为(数据个数,分类个数)的矩阵。输入参数y为一个类标向量(数组),若y[i]=2就表示第i条数据的正确分类为2,但Softmax分类器生成的分类得分概率为一个矩阵(二维数组)。因此,需要将类标向量y转换为one-hot(向量中类标位为1,其余为零),比如y[i]=2的类标,转化为one-hot就为[0,0,1,0,0,0,0,0,0,0]。我们可以使用以下代码进行one-hot形式的类标矩阵转换:y_trueClass[range(num_train),y]=1.0,其形状为(数据个数,分类个数),这样就可以在后续的计算中使用向量计算。

接下来,使用前面已实现的softmax_loss_naive与向量化版本进行比较,完全向量化版本应该和显式循环版本的结果相同,但前者的计算效率应该快得很多。运行下列代码进行代码检验。

其检验结果大致应该如下所示。

2.6.5 最小批量梯度下降算法训练Softmax分类器
完成了Softmax的核心代码后,接下来我们就使用最小批量梯度下降算法训练Softmax分类器。在训练阶段,该过程十分简单,基本上就是采样数据,然后调用Softmax函数计算梯度,之后再更新权重,然后重复上述执行过程。如下所示,为该过程的算法伪代码。

需要注意的是,我们的计数是从0开始的,10分类任务其y的最大值为9,因此num_classes=np.max(y)+1。在采样时,重复采样或非重复采样都可以接受,但重复采样的执行效率要高些,可以使用重复采样加快执行效率,并且其对于梯度影响可以忽略不计。接下来就打开“DLAction/classifiers/chapter2/softmax.py”文件,对train()函数进行代码填充工作。

如果编码顺利,那损失函数应该会随着迭代次数的增加而减少。运行以下代码块,检验实现是否正确。

其结果如下所示。

为了更直观地说明,我们将损失函数可视化并观察其变化情况,代码如下所示,损失函数如图2-8所示。


图2-8 Softmax损失函数变化情况
接下来我们编写Softmax的预测代码块。在预测阶段,我们不需要进行归一化概率,仅仅输出最高分所对应的类标号即可,该过程应该会很简单。一行代码就可以完成。

完成上述代码后,就可以测试效果,我们输出训练数据量、验证数据量、训练正确率和验证正确率。其代码块及输出结果如下所示。

通过结果我们发现,训练精度和验证精度都不高,并且还出现了过拟合现象,接下来我们就开始使用超参数进行调整,训练出一个最佳模型。
2.6.6 使用验证数据选择超参数
深度学习工程师,开个玩笑,也可以被称为“调参工程师”,我们有太多的超参数可以选择。就目前而言,学习率、权重衰减惩罚因子、批量大小和迭代次数都可以称为超参数。超参数对最终的训练结果影响显著,并且不同的超参数组合,其结果也千差万别。接下来我们将使用学习率以及权重衰减因子作为超参数进行选择,请让你的机器飞起来吧!
我们将固定批量大小以及迭代次数,其中batch_size=50,num_iters=300。
对于学习率和惩罚因子,使用逐步缩小范围的方式,来挑选超参数。其学习率为:learning_rates=np.logspace(-9,0,num=10)。

惩罚因子为:regularization_strengths=np.logspace(0,5,num=10)。

其完整的训练代码和训练结果如下所示。


为了更直观地展示训练结果,我们将训练精度以及验证精度进行可视化比较,使用颜色表示训练性能,颜色越红则效果越好,颜色越蓝则效果越差。以下为可视化结果的代码块,图2-9为可视化训练结果。


图2-9 可视化训练结果一
从可视化结果可以清晰地看出,学习率大约在[1e-6,1e-4]之间效果显著,惩罚因子在[1e0,1e4]之间效果较好,并且惩罚因子越小可能效果越好。因此我们下一步就在这个范围内继续缩小。代码如下所示,图2-10是新的训练结果。


图2-10 可视化训练结果二
我们再次设置选择范围,如下所示,新的训练结果如图2-11所示。


图2-11 可视化训练结果三
此时我们的训练精度几乎都接近了100%,但最佳的验证精度也只有0.31。那最终的测试结果如何呢?如下所示是我们的Softmax测试结果代码块,非常令人沮丧,测试集精度只有0.215。

实在不好意思,这是恶趣味的作者故意耍的一个小花招,那你知道问题出在哪吗?如果你足够认真和细心的话,可能早就发现了,那就是我们训练的数据量实在太小了。上述的测试中,我们仅仅使用了250条数据进行训练,不管我们如何优化,过拟合情况都很难避免,之所以这么做,是想让你注意一个微小而又重要的知识,那就是数据量的问题。
当数据较少时,过拟合风险就越高,即使非常小心地选择超参数,其效果也不会好,这是一个令人伤感的消息。但其实也有一个好消息,那就是当数据量足够大时,过拟合现象就很难发生,这就使得训练数据、验证数据与测试数据之间的差值会越来越接近。在某些数据量特别巨大的情况下,可能只需关注如何将训练数据错误率降到足够低即可。超参数的选择是一个非常耗时、耗力的过程,因此我们可以先使用较小的数据去粗略地选择超参数的取值范围,这样可以节约训练时间。但在较小数据中表现最好的超参数,不一定在数据量较大时同样表现得更好,因此需要注意数据量的把控,但这是一个非常依赖于经验的问题,需要从大量的实验中自己积累经验,进行不断地尝试。记住,千万不要怕错。
现在我们将重新载入数据集,使用完整的49000条数据进行训练,1000条数据作为验证。现在舞台交给你了,请尽一切可能训练出一个最好的测试结果,你可以尽可能地调整现在所提供的4种超参数,如果计算能力和自己的耐心允许,也可以尝试下交叉验证。如果你足够努力,你的测试正确率可以超过0.35。重新载入数据代码块和训练代码块分别如下所示。


另一种更直观检验模型好坏的方法是可视化模型参数,比如我们识别图片“马”,那模型的参数其实就是图片的“马模板”,也可以通过可视化参数去反推数据情况。比如我们的图片中如果存在大量“马头向左”与“马头向右”的图片,那训练出来的模型参数很可能就变成了“双头马”。同理,如果图片中汽车的颜色大多数都是红色,那训练出的模型就可能是一辆“红车模板”。可视化参数代码块如下所示,可视化训练参数示意图如图2-12所示。


图2-12 可视化训练参数