- 参考:动手学深度学习
- 注:本文是 jupyter notebook 文档转换而来,部分代码可能无法直接复制运行!
- 前文介绍的 经典机器学习方法(1)—— 线性回归 适用于连续值预测问题(回归问题),本文介绍适用于离散值预测(分类问题)的 softmax 回归模型,这是一种基于神经网络的经典分类模型
- softmax 回归和线性回归内部一样是线性模型,区别在于
- softmax 回归的输出从一个变成了多个
- softmax 回归中引入了 softmax 运算,使其更适合于离散值的预测和训练
文章目录
- 1. softmax 回归原理
- 1.1 分类问题
- 1.2 softmax 回归模型
- 1.2.1 单样本分类的矢量计算表达式
- 1.2.2 mini-batch 样本分类的矢量计算表达式
- 1.3 交叉熵损失函数
- 1.4 模型预测与评价
- 2. 实现 softmax 回归
- 2.1 数据准备
- 2.2 模型设计
- 2.3 模型评价
- 2.4 模型训练
- 2.4.1 优化算法
- 2.4.2 训练流程
- 2.5 使用模型进行预测
- 2.6 完整代码
- 3. 利用 Pytorch 简洁地实现 softmax 回归
- 3.1 模型设计
- 3.1.2 模型定义
- 3.1.2 模型初始化
- 3.1.3 softmax 和交叉熵损失函数
- 3.2 模型训练
- 3.2.1 优化器
- 3.2.2 训练流程
- 3.3 完整代码
1. softmax 回归原理
1.1 分类问题
- 考虑以下简单的分类问题
- 输入: 尺寸的图像 ,四个像素记为
- 输出:预测标记 ,其中 是大小为 3 的输出空间。我们习惯使用离散的数值来表示类别,比如将其处理为 ,这样需要输出 1,2,3 这三个数字中的一个
- 如果向上面那样简单地使用数值化的标记,仍然可以使用回归模型来处理,将预测值就近离散化到 1、2、3 这三个值即可。但是有两个问题
- 数值化标记有距离关系,1 和 2 比较接近,1 和 3 比较远,但事实上没有这种关系
- 连续值到离散值的转化通常会影响分类质量
所以一般采用专门针对离散值输出的分类模型来解决分类问题,做两个变化
- 样本标记使用 one-hot 向量形式
- 模型输出加一个 softmax 函数,得到概率性的综合 one-hot 预测值
1.2 softmax 回归模型
- softmax 回归模型内部和线性回归模型几乎一致,也是一个简单的单层全连接神经网络,只是在输出层增加了节点,以获得 (构成 one-hot 向量),以 1.1 节的 4 维输入(特征维度为4) 3 维输出(类别总数为3)为例
- 其中每个输出层节点都是输入的线性组合,即
- 为了得到离散的 one-hot 形式的预测输出,把输出值 看作置信度,输出越大的节点,对应的标记越可能是真实标签。使用 softmax 运算将其输出值转换为正且和为 1 的概率分布,即
其中 是如下计算的
- ,因此 softmax 运算不改变预测类别输出
- 由于进行了 变换,softmax 会使数值之间的相对差距放大
- softmax 操作不改变向量尺寸,输出的
- softmax 模块示意图如下
- 考虑二分类问题的特殊情况,这时输出只有两个,设两组参数为 和 ,给定样本 ,输出 为
可见变成了 sigmoid 函数的形式,也就是说:对于二分类问题,softmax 回归等价于 logistic 回归(逻辑回归/对数几率回归)
1.2.1 单样本分类的矢量计算表达式
- 为了提升运算效率,将上述运算都改成矩阵形式,
- softmax 回归的权重和偏置参数为
- 第 个样本特征为
- 输出层输出为
- 预测概率分布为
- 通常把预测概率最大的类别作为预测类别
注意我们习惯使用离散的数值来表示类别,比如将其处理为 ,这样需要输出 1,2,3 这三个数字中的一个 - softmax 回归对样本 进行的运算为
1.2.2 mini-batch 样本分类的矢量计算表达式
- 为了进一步提升计算效率,结合常用的 mini-batch 梯度下降优化算法,我们常常对小批量数据做矢量运算。设一个小批量样本批量大小为 ,输入特征个数为 ,输出个数(类别数为),则
- 批量样本特征为
- 权重参数为
- 偏置参数为
- 矢量计算表达式为
其中加法使用了广播机制, 且其中第 行分别为样本 的输出 和概率分布
1.3 交叉熵损失函数
- 对于某个样本 ,上面我们利用 softmax 运算得到看其预测标记分布 。另一方面,此样本的真实标记也可以用一个输出空间上的分布
比如样本只有一个标记时,可以构造一个 one-hot 向量 ,使其真实标记对应的向量元素设为 1,其他设为 0,从而将真实标记转换为一个输出空间上的分布
- 这样我们的训练目标可以设为使预测概率分布 尽量接近真实概率分布
- 这里不适合使用线性回归的平方损失(MSE 损失) ,因为想得到正确的预测分类结果,只要保证真实类别的预测概率最大即可,平方损失函数要求所有可能类别的预测概率和真实概率都相等,这过于严格
假设真实标记是 ,当 预测值为 0.6 时即可以保证一定预测正确,如果用平方损失,这时 比 ,
- 下面引用一个李宏毅机器学习课程中的例子,左图和右图分别显示使用 MSE 损失和 Cross-entropy 损失导致的 error surface,可见使用交叉熵时梯度比较陡峭,利于做优化;而 MSE 导致的梯度有很大的平坦区域,优化过程很可能卡住(可能必须要用 Adam 等高级的优化方案)
- 关于这个问题其实还有不少可讲的,请参考:分类问题为什么用交叉熵损失不用 MSE 损失
- 我们可以使用衡量两个分布间差异的测量函数作为损失,交叉熵(cross entropy)是一个常用的选择,它将分布 和 的差距表示为
注意其中 是真实标记分布 中非零即一的元素,样本真实标记为 ,因此 中只有 ,其他全为 0,因此上述交叉熵可以化简为
可见最小化交叉熵损失等价于最大化对正确类别的预测概率,它只关心对正确类别的预测概率,这是合理的,因为只要其值足够大就能保证分类正确
当遇到一个多标签样本时,例如图像中含有不止一个物体时,不能做这样的简化,但是这种情况下交叉熵损失也仅仅关心图像中出现的物体类别的预测概率
- 假设训练数据样本量为 ,交叉熵损失函数定义为
其中 是模型参数,如果每个样本只有一个标签,则上述损失可以化简为
最小化 等价于最大化 ,即最小化交叉熵损失函数等价于最大化训练数据集所有标签类别的联合预测概率
1.4 模型预测与评价
- 训练好 softmax 回归模型后,给定任一样本特征,就可以预测每个输出类别的概率
- 通常把预测概率最大的类别作为输出类别,如果它与真实类别(标签)一致,说明这次预测是正确
- 对于分类问题,可以使用
准确率accuracy
来评价模型的表现,它等于正确预测数量与总预测数量之比
2. 实现 softmax 回归
2.1 数据准备
- 使用 Fashion-MNIST 图像分类数据集进行试验,该数据集可以使用
torchvision.datasets
方便地获取和使用,具体请参考:在 pytorch 中加载和使用图像分类数据集 Fashion-MNIST - 先定义好读取小批量数据的方法,构造数据读取迭代器
2.2 模型设计
- 模型参数初始化:输入图像样本尺寸均为 28x28,拉平后输入向量长度为 ;由于图像有 10 个类别,输出层输出向量尺寸为 10,因此权重参数尺寸为 ,偏置参数尺寸为 ,如下初始化
注意设置属性 requires_grad = True
,这样在后续训练过程中才能对这些参数求梯度并迭代更新参数值
- 实现 softmax 运算:下面实参 O 是行数为样本数,列数为输出个数的矩阵,即 (最后加向量 使用了广播机制)。首先调用
.exp()
对矩阵中所有元素求 exp 函数值,然后按列求和得到 的中间向量,最后利用广播机制将每一个元素除以其所在行的中间元素。这样处理后得到的矩阵每行元素和为1且非负,成为合法的概率分布,代表各个样本在各个输出类别上的预测概率
测试一下,假设类别数为 5,样本数为 2
- 定义模型
- 定义损失函数
2.3 模型评价
- 使用分类准确率评价分类模型的性能,假设有 q 个类别 n 个预测样本,和 2.2 节中一样记真实标记向量为
y
(尺寸 torch.Size([n])
),模型输出为 y_hat
(尺寸 torch.Size([n, q])
)
- 计算一批样本的预测准确率:
-
y_hat.argmax(dim=1)
获取所有样本的预测标签,尺寸为 torch.Size([n])
-
y_hat.argmax(dim=1) == y
和样本真实标签比较,得到 bool 型 tensor,尺寸为 torch.Size([n])
-
(y_hat.argmax(dim=1) == y).float()
把 bool 型 tensor 转为取值 0 或 1 的浮点型 tensor,尺寸为 torch.Size([n])
-
(y_hat.argmax(dim=1) == y).float().mean()
计算均值得到准确率,返回尺寸为 torch.Size([])
的浮点型 tensor -
(y_hat.argmax(dim=1) == y).float().mean().item()
将上面这种只有一个元素的 tensor 转为 python 标量
- 计算整个训练集/测试集上的分类准确率:利用前面定义的数据获取迭代器遍历数据集,计算所有样本准确率的均值,从而评估模型
net
在整个数据集上的准确率,如下
编程实践中,通常直接使用第二种方式
2.4 模型训练
2.4.1 优化算法
- 使用小批量随机梯度下降来优化参数
2.4.2 训练流程
- 训练流程和线性回归类似
- 设定超参数
num_epochs
(迭代次数)和 lr
(学习率) - 在每轮迭代中逐小批次地遍历训练集,计算损失 -> 对参数求梯度 -> 做小批量随机梯度下降优化参数
- 训练程序如下
2.5 使用模型进行预测
- 训练完成后就可以用模型对测试图像进行分类了,先定义一些显示结果使用的工具函数
- 下面给定一系列图像,真实标签和模型预测结果分别显示在第一和第二行
预测结果如下
2.6 完整代码
- 整合上述过程,给出完整代码,可以直接粘贴进 vscode 运行
3. 利用 Pytorch 简洁地实现 softmax 回归
- pytorch 中提供了大量预定义的神经网络层,常用损失函数及优化器,可以大大简化 softmax 回归模型的实现
- 数据准备、模型评价、使用模型进行预测三部分和第 2 节实现相同,本节不再重复
3.1 模型设计
3.1.2 模型定义
- 如 1.2 节所示,softmax 回归的输出是一个全连接层,可以使用
torch.nn.Linear
方法定义,如下
注意到原始图像尺寸为 torch.Size([1, 28, 28])
,数据迭代器返回的 batch x
尺寸为 (batch_size, 1, 28, 28),在做前向传播时,必须要把样本都拉平,即把 x
的形状转换为 (batch_size, 1x28x28) 才能送入全连接层
- 按照深度学习的习惯,可以把数据拉平这件事定义成神经网络的一个层,如下
这样就能更符合习惯地,利用 Sequential
容器搭建网络模型
3.1.2 模型初始化
- 利用
torch.nn.init
包提供的方法进行初始化,初始化值同 2.2 节
3.1.3 softmax 和交叉熵损失函数
- 第 2 节中我们按照数学定义分别定义了 softmax 函数和 CrossEntropy 损失,这样做可能导致数据不稳定
- softmax 函数定义为 注意这里都是 exp 运算,一旦网络初始化不当,或输入数值有较大噪音,很可能导致数值溢出( 是很大的正整数会导致上溢,,这时可以用 Log-Sum-Exp Trick(logSoftmax)处理,它在数学上就是在普通 softmax 外面套了一个 log 函数,这不会影响概率排序,但是通过有技巧地实现可以有效解决溢出问题。具体参考 深入理解softmax
- CrossEntropy损失在这里为 当 太小(趋于0)时也可能溢出。观察发现其实这里就是 logSoftmax 取了个负号,所以用了 Log-Sum-Exp 技巧后这里的就不用担心溢出了。注意到整个数据集上的损失为 如果已经用 logSoftmax计算了各个 ,可以直接用负对数似然方法
torch.NLLLoss
方法得到 。NLLLosss 的具体行为可以参考 loss函数之NLLLoss,CrossEntropyLoss
- 总之,我们可以用 logSoftmax + NLLLoss 避免数据溢出,保证数据稳定性,并且得到等价的交叉熵损失,pytorch 中直接把这两个放在一起封装了一个
nn.CrossEntropyLoss
方法,对于一组小批量数据,假设模型输出和真实标签分别为 和 , nn.CrossEntropyLoss
如下计算小批量损失
在这里我们直接使用它来替代前面自己定义的 softmax 函数和 CrossEntropy 损失。可参考 Pytorch中Softmax、Log_Softmax、NLLLoss以及CrossEntropyLoss的关系与区别详解
3.2 模型训练
3.2.1 优化器
- 直接使用 pytorch 提供的小批量随机梯度优化器
torch.optim.SGD
- 基本用法
- 梯度清零:
optimizer.zero_grad()
- 执行一次优化:
optimizer.step()
3.2.2 训练流程
- 和 2.4.2 节完全类似,训练程序只须改一下优化器部分的处理即可
3.3 完整代码
- 整合上述过程,给出完整代码,可以直接粘贴进 vscode 运行