2-softmax回归线性回归模型适用于输出为连续值的情景,在另一类场景中,模型输出可以是像图像类别这样的离散值。对于离散值预测问题,可以使用诸如softmax回归在内的分类模型。本节以softmax回归模型为例,介绍神经网络中的分类模型。
虽然这里题目是softmax回归,但其实是一个分类问题。
softmax回归模型softmax回归和线性回归一样将输入特征和权重做线性叠加,与线性回归不同之处在于,softmax回归的输出值个数等于标签里的类别数。
如果有四种特征和三种输出动物类别,那么权重包括12个标量(带下标的w w w ),偏差包含三个标量(带下标的b b b ),对每个输入计算o 1 , o 2 , o 3 o_1,o_2,o_3 o 1 , o 2 , o 3 三个输出。
o 1 = x 1 w 11 + x 2 w 21 + x 3 w 31 + x 4 w 41 + b 1 o 2 = x 1 w 12 + x 2 w 22 + x 3 w 32 + x 4 w 42 + b 2 o 1 = x 1 w 13 + x 2 w 23 + x 3 w 33 + x 4 w 43 + b 3 o_1 = x_1w_{11} + x_2w_{21} + x_3w_{31} + x_4w_{41} + b_1 \\ o_2 = x_1w_{12} + x_2w_{22} + x_3w_{32} + x_4w_{42} + b_2 \\ o_1 = x_1w_{13} + x_2w_{23} + x_3w_{33} + x_4w_{43} + b_3 \\ o 1 = x 1 w 1 1 + x 2 w 2 1 + x 3 w 3 1 + x 4 w 4 1 + b 1 o 2 = x 1 w 1 2 + x 2 w 2 2 + x 3 w 3 2 + x 4 w 4 2 + b 2 o 1 = x 1 w 1 3 + x 2 w 2 3 + x 3 w 3 3 + x 4 w 4 3 + b 3
使用矩阵相乘有两种表示方法。
输入特征表示为列向量,则权重矩阵每行表示一个神经元,该层有多少神经元就有多少行,计算式是:w x wx w x 输入特征表示为行向量,则权重矩阵每列表示一个神经元,行数表示前一层输出个数,计算式是:x w xw x w softmax回归是一个单层神经网络,并且softmax回归的输出层也是一个全连接层。
既然分类问题需要得到离散的预测输出,一个简单办法是将输出值o i o_i o i 当作预测类别是i i i 的置信度,并将值最大的输出对应的类作为预测输出。
即arg max i o i \underset{i}{\arg\max}o_i i arg max o i 。
但是,直接使用输出层输出有两个问题。
由于输出层的输出值不确定,难以直观判断这些值的意义 由于真实标签是离散值,这些离散值与不确定范围的输出值之间的误差难以衡量 softmax解决了以上两个问题,它通过下式将输出值变成了值为正且和为1的概率分布。
y ^ 1 , y ^ 2 , y ^ 3 = s o f t m a x ( o 1 , o 2 , o 3 ) \widehat{y}_1,\widehat{y}_2,\widehat{y}_3 = softmax(o_1, o_2, o_3) y 1 , y 2 , y 3 = s o f t m a x ( o 1 , o 2 , o 3 )
其中:
y ^ 1 = e x p ( o 1 ) ∑ i = 1 3 e x p ( o i ) y ^ 2 = e x p ( o 2 ) ∑ i = 1 3 e x p ( o i ) y ^ 3 = e x p ( o 3 ) ∑ i = 1 3 e x p ( o i ) \widehat{y}_1 = \frac{ exp(o_1) }{ \sum_{i=1}^3 exp(o_i) } \\ \widehat{y}_2 = \frac{ exp(o_2) }{ \sum_{i=1}^3 exp(o_i) } \\ \widehat{y}_3 = \frac{ exp(o_3) }{ \sum_{i=1}^3 exp(o_i) } \\ y 1 = ∑ i = 1 3 e x p ( o i ) e x p ( o 1 ) y 2 = ∑ i = 1 3 e x p ( o i ) e x p ( o 2 ) y 3 = ∑ i = 1 3 e x p ( o i ) e x p ( o 3 )
可以看出,y ^ 1 + y ^ 2 + y ^ 3 = 1 \widehat{y}_1+\widehat{y}_2+\widehat{y}_3 = 1 y 1 + y 2 + y 3 = 1 并且0 ≤ y ^ 1 , y ^ 2 , y ^ 3 ≤ 1 0 \le \widehat{y}_1,\widehat{y}_2,\widehat{y}_3 \le 1 0 ≤ y 1 , y 2 , y 3 ≤ 1 。
小批量样本的矢量化为了提高计算效率,通常会对小批量样本做向量计算。假设有一个批量样本X \bf X X ,特征维度为d d d ,样本数量为n n n 。此外,假设在输出中有q q q 个类别,那么小批量样本的特征为X ∈ R n × d \mathbf{X} \in R^{n\times d} X ∈ R n × d ,权重为W ∈ R d × q \mathbf{W} \in R^{d\times q} W ∈ R d × q ,偏置为b ∈ R 1 × q \mathbf{b} \in R^{1\times q} b ∈ R 1 × q ,softmax回归的计算式为:
O = X W + b Y ^ = s o f t m a x ( O ) \mathbf{ O = XW + b } \\ \mathbf{ \widehat{Y} } = softmax(\mathbf{ O} ) O = X W + b Y = s o f t m a x ( O )
X \bf X X 中的每行表示一个样本,权重矩阵每一列代表一个神经元。
损失函数接下来需要一个损失函数来度量预测效果。
softmax运算可以将输出变成一个合法的类别预测分布,实际上,真实标签也可以用类别分布表达,对于样本i i i ,构造向量y i \bf y^i y i ,使得第y ( i ) y^{(i)} y ( i ) 个元素为1,其余元素为0,这样训练目标可以设为使预测概率分布y ^ ( i ) \mathbf{\widehat{y}^{(i)} } y ( i ) 更接近真实的标签概率分布y ( i ) \bf y^{(i)} y ( i ) 。
可以像线性回归那样使用均方误差损失,但是想要预测分类正确,并不需要预测概率完全等于标签概率。
例如,在图像分类中,有三个类型,如果真实标签y ( i ) = 3 y^{(i)} = 3 y ( i ) = 3 ,只需要让预测值y ^ 3 ( i ) \widehat{y}^{(i)}_3 y 3 ( i ) 大于其他两个预测值即可。如果y ^ 3 ( i ) = 0.6 \widehat{y}^{(i)}_3 = 0.6 y 3 ( i ) = 0 . 6 ,那么无论其他两个值是多少,类别预测都正确。而平方损失过于严格。
因此,交叉熵是一个常用的衡量方法。
H ( y ( i ) , y ^ ( i ) ) = − ∑ j = 1 q y j ( i ) log y ^ j ( i ) H( \mathbf{ y^{(i)}, \widehat{y}^{(i)} } ) = -\sum_{j=1}^{q} y_{j}^{(i)} \log \widehat{y}^{(i)}_j H ( y ( i ) , y ( i ) ) = − j = 1 ∑ q y j ( i ) log y j ( i )
其中带下标的y j ( i ) y_{j}^{(i)} y j ( i ) 是向量y ( i ) \bf y^{(i)} y ( i ) 中非0即1的元素,需要注意与样本i类别的离散数值,即不带下标的y ( i ) y^{(i)} y ( i ) 区分。
在上式中,我们知道向量y ( i ) \bf y^{(i)} y ( i ) 中只有第y ( i ) y^{(i)} y ( i ) 个元素值为1,其余都为0,因此式子变为:
H ( y ( i ) , y ^ ( i ) ) = − log y ^ y ( i ) ( i ) H( \mathbf{ y^{(i)}, \widehat{y}^{(i)} } ) = -\log \widehat{y}^{(i)} _ {y^{(i)} } H ( y ( i ) , y ( i ) ) = − log y y ( i ) ( i )
也就是说,交叉熵只关心对正确类别的预测概率,预测概率值越大,损失值越小,概率为1,则交叉熵值为0。
当然遇到一个样本有多个标签时,例如一个图像中有多个物体,则不能做这一步的简化,但即使对于这种情况,交叉熵同样只关心对图像中出现的物体类别的预测概率。
假设训练数据集样本数为n,交叉熵损失函数定义为:
l ( Θ ) = 1 n ∑ i = 1 n H ( y ( i ) , y ^ ( i ) ) \mathcal{l (\Theta) } = \frac{1}{n} \begin{matrix} \sum_{i=1}^n \end{matrix} H( \boldsymbol{ y^{(i)}, \widehat{y}^{(i)} } ) l ( Θ ) = n 1 ∑ i = 1 n H ( y ( i ) , y ( i ) )
其中Θ \Theta Θ 代表模型参数,同样,如果每个样本只有一个标签,那么交叉熵损失可以简写成:
l ( Θ ) = − 1 n ∑ i = 1 n log y ^ y ( i ) ( i ) \mathcal{l (\Theta) } = -\frac{1}{n} \begin{matrix} \sum_{i=1}^n \log\widehat{y}^{(i)} _{ y^{(i)} } \end{matrix} l ( Θ ) = − n 1 ∑ i = 1 n log y y ( i ) ( i )
softmax及其导数由于softmax和相关损失函数很常见,因此需要更好理解它的计算方式。
将softmax公式带入到交叉熵损失式子中,得到:
l ( y , y ^ ) = − ∑ j = 1 q y j log e x p ( o j ) ∑ k = 1 q e x p ( o k ) = ∑ j = 1 q y j log ∑ k = 1 q e x p ( o k ) − ∑ j = 1 q y j o j = log ∑ k = 1 q e x p ( o k ) − ∑ j = 1 q y j o j \begin{aligned} l(\boldsymbol{y, \widehat{y} } ) & = - \sum_{ j=1 }^{ q } y_j \log \frac{ exp(o_j) }{ \sum_{ k=1 }^q exp(o_k) } \\ & = \sum_{j=1}^q y_j \log \sum_{k=1 }^q exp(o_k) - \sum_{ j=1 }^q y_jo_j \\ & = \log \sum_{k=1}^q exp(o_k) - \sum_{j=1}^q y_j o_j \end{aligned} l ( y , y ) = − j = 1 ∑ q y j log ∑ k = 1 q e x p ( o k ) e x p ( o j ) = j = 1 ∑ q y j log k = 1 ∑ q e x p ( o k ) − j = 1 ∑ q y j o j = log k = 1 ∑ q e x p ( o k ) − j = 1 ∑ q y j o j
则关于预测值o j o_j o j 的导数为:
∂ o j l ( y , y ^ ) = e x p ( o j ) ∑ k = 1 q e x p ( o k ) − y j = s o f t m a x ( o ) j − y j \partial_{o_j} l(\boldsymbol{y, \widehat{y} }) = \frac{ exp(o_j) }{ \sum_{k=1}^q exp(o_k) } - y_j = softmax(\mathbf{o})_j - y_j ∂ o j l ( y , y ) = ∑ k = 1 q e x p ( o k ) e x p ( o j ) − y j = s o f t m a x ( o ) j − y j
如果只预测一个类别,则上述式子应变为:
l ( y , y ^ ) = log ∑ k = 1 q e x p ( o k ) − o j ∂ o j l ( y , y ^ ) = s o f t m a x ( o ) j − 1 l(\boldsymbol{y, \widehat{y} }) = \log \sum_{k=1}^q exp(o_k) - o_j \\ \partial_{o_j} l(\boldsymbol{y, \widehat{y} }) = softmax(\mathbf{o})_j - 1 l ( y , y ) = log k = 1 ∑ q e x p ( o k ) − o j ∂ o j l ( y , y ) = s o f t m a x ( o ) j − 1
换句话说,导数是softmax模型分配概率与实际情况(one-hot向量表示)之间的差异。
模型预测和评估在训练softmax回归模型后,给出任何样本特征,可以预测每个输出类别的概率,通常使用预测概率最高的类别作为输出类别。如果预测与实际类别一致,则预测是正确的。接下来将使用精度(accuracy)来评估模型性能,它等于正确预测数与预测总数之间比率。
图像分类数据集在介绍softmax回归实现之前先引入一个多分类图像分类数据集,以方便观察比较算法之间在模型精度和计算效率的区别。图像分类数据集中最常见的是手写数字识别数据集MNIST,但过于简单,大部分模型在MNIST的分类精度都超过了95%,因此为了更直观观察算法之间的差异,会使用一个图像内容更复杂的数据集Fashion-MNIST,它是一个衣服类别数据集,包含了十个类别:t-shirt(T恤)、trouser(裤子)、pullover(套衫)、dress(连衣裙)、coat(外套)、sandal(凉鞋)、shirt(衬衫)、sneaker(运动鞋)、bag(包)和ankle boot(短靴)。
训练集包含60000个样本,测试集包含10000个样本。
本节将使用torchvision
包,主要用来构建计算机视觉模型。它主要有以下几部分组成:
torchvision.datasets
:一些加载数据的函数和常用的数据集接口torchvision.models
:包含常用的模型结构和预训练模型,例如AlexNet,VGG等torchvision.transforms
:常用的图像变换,裁剪、旋转等torchvision.utils
:其他有用的方法 获取数据集首先导入必要的包和模块。
1 2 3 4 5 6 import torchimport torchvisionimport torchvision.transforms as transformsimport matplotlib.pyplot as pltimport timeimport d2lzh_pytorch as d2l
然后,通过torchvision.datasets
下载该数据集,第一次调用时会自动从网上获取数据,通过参数train
来指定获取训练集或测试集。
另外,还指定了参数transform=transforms.ToTensor()
是所有数据转换成Tensor
,如果不进行转换则返回的是PIL图片,transforms.ToTensor()
将所有尺寸为(H×W×C)且数据位于[0,255]的PIL图片或数据类型为np.uint8
的NumPy数组转换成尺寸为(C×H×W)且数据类型为torch.float32
且位于[0.0, 1.0]的Tensor
。
1 2 mnist_train = torchvision.datasets.FashionMNIST(root='./datasets/fashion_mnist' , train=True , download=True , transform=transforms.ToTensor()) mnist_test = torchvision.datasets.FashionMNIST(root='./datasets/fashion_mnist' , train=False , download=True , transform=transforms.ToTensor())
可以通过下标访问样本,例如:
1 2 3 4 5 feature, label = mnist_train[0 ]print (feature.shape, label)
变量feature
对应高和宽均为28的图像,由于使用了transforms.toToTensor()
,所以每个像素值都为[0.0, 1.0]
之间的32位浮点数。
需要注意,feature
尺寸是(C×H×W),第一维是通道数,因为是灰度图像,所以通道数为1,后面分别是高和宽。
定义函数将数值标签转换成文本标签。
1 2 3 4 def get_fashion_mnist_labels (labels ): text_labels = ['T恤' , '牛仔裤' , '套衫' , '连衣裙' ,'外套' , '凉鞋' , '衬衫' , '运动鞋' , '包' , '短靴' ] return [text_labels[int (i)] for i in labels]
查看训练集中前十张图片。
1 2 3 4 5 X, y = [], []for i in range (10 ): X.append(mnist_train[i][0 ]) y.append(mnist_train[i][1 ]) d2l.show_fashion_mnist(X, get_fashion_mnist_labels(y))
其中show_fashion_mnist
实现代码为:
1 2 3 4 5 6 7 8 9 def show_fashion_mnist (images, labels ): use_svg_display() _, figs = plt.subplots(1 , len (images), figsize=(12 , 12 )) for f, img, lbl in zip (figs, images, labels): f.imshow(img.view((28 , 28 )).numpy()) f.set_title(lbl) f.axes.get_xaxis().set_visible(False ) f.axes.get_yaxis().set_visible(False )
输出为:
读取小批量mnist_train
是torch.utils.data.Dataset
的子类,所以可以将其传入torch.util.data.DataLoader
中来创建一个读取小批量数据样本的DataLoader实例。
在实践中,数据读取经常是训练的性能瓶颈,特别是当模型较简单或硬件性能较差时,PyTorch的DataLoader
允许使用多进程来借宿数据读取,使用参数num_workers
来设置进程数量。
windows平台使用多进程读取会出错,因此num_workers应设为0。
1 2 3 4 batch_size =256 num_workers = 0 train_iter = torch.utils.data.DataLoader(mnist_train, batch_size=batch_size, shuffle=True , num_workers = num_workers) test_iter = torch.utils.data.DataLoader(mnist_test, batch_size=batch_size, shuffle=True , num_workers = num_workers)
Softmax回归的从零实现首先导入所需库和模块。
1 2 3 4 5 6 7 8 9 import torchimport torchvisionimport torchvision.transforms as transformsimport matplotlib.pyplot as pltimport timeimport numpy as npimport d2lzh_pytorch as d2l %matplotlib inline plt.rcParams['font.sans-serif' ] = 'SimHei'
Fashion-MNIST数据已经在上节获取到了。
初始化模型参数使用向量表示每个样本,因为每个样本输入都是高宽均为28像素的图像,因此模型的输入向量长度为28×28=784,该向量的每个元素对应图像中每个像素,由于图像有10个类别,因此输出层输出个数为10,。
因此,softmax回归的权重和偏置参数分别为784×10和1×10的矩阵。
1 2 3 4 5 6 7 8 num_inputs = 784 num_outputs = 10 W = torch.tensor(np.random.normal(0 , 0.01 , (num_inputs, num_outputs)), dtype=torch.float ) b = torch.zeros(num_outputs, dtype=torch.float ) W.requires_grad_(True ) b.requires_grad_(True )
实现softmax运算在介绍如何定义回归之前,先讲解下如何对多维Tensor
按维度操作。给定一个Tensor
矩阵X
,可以只对同一列(dim=0
)或同一行(dim=1
)的元素求和,并在结果中保留行和列这两个维度(keepdim=True
)。
1 2 3 4 5 6 7 X = torch.tensor([[1 ,2 ,3 ], [4 ,5 ,6 ]])print (X.sum (dim=0 , keepdim=True ))print (X.sum (dim=1 , keepdim=True ))
X.sum(dim=0, keepdim=True)
输出的维度是(1,3)
,如果keepdim=False
,则输出维度是(3,)
,也就是一维的,不保留列这个维度。
接下来定义softmax运算。在下面函数中,矩阵X
行数是样本数,即每一行表示一个样本,列数是输出个数。
1 2 3 4 def softmax (X ): X_exp = X.exp() partition = X_exp.sum (dim=1 , keepdim=True ) return X_exp / partition
例如矩阵X
维度为(256, 10)
,共有256个样本,首先对矩阵所有元素做指数运算,然后按行求和,得到partition
,一个维度为(256,1)
的向量,最后通过广播机制求得这256个样本的softmax概率分布值。
定义模型有了softmax运算,就可以定义softmax回归模型。
1 2 def net (X ): return softmax(torch.mm(X.view(-1 , num_inputs), W) + b)
通过view
函数将每张图片从28×28
的矩阵改成长度为num_inputs
的向量,再与权重矩阵相乘。
定义损失函数上节中,介绍了softmax回归中使用的交叉熵损失函数,为了得到标签的猜测概率,使用gather
函数。
首先介绍gather
函数,gather
函数作用是根据索引查找,然后将查找结果以张量形式返回。
有两个参数:
dim
:维度,即按哪个维度来进行索引index
:索引矩阵介绍到这,完全不理解gather
函数。
举个例子:
1 2 3 4 5 6 7 y_hat = torch.tensor([[0.1 , 0.3 , 0.6 ], [0.3 , 0.2 , 0.5 ]]) y = torch.LongTensor([0 ,1 ]).view(1 , -1 )print (y, y.shape) y_hat.gather(1 , y)
y_hat
矩阵为[ 0.1 0.3 0.6 0.3 0.2 0.5 ] \begin{bmatrix}0.1 & 0.3 & 0.6 \\ 0.3 & 0.2 & 0.5 \end{bmatrix} [ 0 . 1 0 . 3 0 . 3 0 . 2 0 . 6 0 . 5 ] ,索引矩阵y
为[ 0 1 ] \begin{bmatrix}0 & 1\end{bmatrix} [ 0 1 ] ,按dim=1
,即按行索引。
但索引矩阵只有一行,维度是(1,3)
,那么就将y_hat
矩阵中,第0行的第0个元素和第0行的第1个元素取出,也就是0.1和0.3。
下面分别给出以dim=0
和dim=1
的几个例子。
0为开始索引。
dim=0
索引矩阵为[ 0 1 1 ] \begin{bmatrix}0 & 1 & 1\end{bmatrix} [ 0 1 1 ] ,则将第0列的第0个元素、第1列的第1个元素、第2列的第1个元素取出,得到矩阵[ 0.1 0.2 0.5 ] \begin{bmatrix} 0.1 & 0.2 & 0.5\end{bmatrix} [ 0 . 1 0 . 2 0 . 5 ] ; 索引矩阵为[ 0 1 ] \begin{bmatrix} 0 \\ 1\end{bmatrix} [ 0 1 ] ,按列索引,但索引矩阵只有一列,因此将第0列的第0、1个元素取出,得到矩阵[ 0.1 0.3 ] \begin{bmatrix} 0.1 \\ 0.3\end{bmatrix} [ 0 . 1 0 . 3 ] ; 索引矩阵为[ 0 1 1 2 ] \begin{bmatrix} 0 & 1 \\ 1 & 2\end{bmatrix} [ 0 1 1 2 ] ,按列索引,但索引矩阵第2列的第二个元素是2,表示取y_hat
矩阵第2列的第2个元素,但y_hat
矩阵第2列只有两个元素,因此索引越界,会出错; 索引矩阵为[ 0 1 1 0 ] \begin{bmatrix} 0 & 1 \\ 1 & 0\end{bmatrix} [ 0 1 1 0 ] ,按列索引,将第0列的第0、1个元素,第1列的1、0个元素取出按索引矩阵排列,得到矩阵[ 0.1 0.2 0.3 0.3 ] \begin{bmatrix} 0.1 & 0.2 \\ 0.3 & 0.3\end{bmatrix} [ 0 . 1 0 . 3 0 . 2 0 . 3 ] ; dim=1
索引矩阵为[ 1 2 ] \begin{bmatrix}1 & 2\end{bmatrix} [ 1 2 ] ,就将y_hat
矩阵中,第0行的第1个元素和第0行的第2个元素取出,得到矩阵[ 0.3 0.6 ] \begin{bmatrix} 0.3 & 0.6\end{bmatrix} [ 0 . 3 0 . 6 ] ; 索引矩阵为[ 1 2 ] \begin{bmatrix}1 \\ 2\end{bmatrix} [ 1 2 ] ,索引矩阵有两行,就将y_hat
矩阵中,第0行的第一个元素和第1行的第2个元素取出,得到[ 0.3 0.5 ] \begin{bmatrix} 0.3 \\ 0.5\end{bmatrix} [ 0 . 3 0 . 5 ] 索引矩阵为[ 0 1 1 2 ] \begin{bmatrix}0 & 1 \\ 1 & 2\end{bmatrix} [ 0 1 1 2 ] ,就将y_hat
矩阵中,第0行的第0、1个元素,第1行的第1、2个元素取出,得到矩阵[ 0.1 0.3 0.2 0.5 ] \begin{bmatrix}0.1 & 0.3 \\ 0.2 & 0.5\end{bmatrix} [ 0 . 1 0 . 2 0 . 3 0 . 5 ] ; 简单来说,gather
函数根据dim
参数来决定按哪个维度进行索引,并根据索引矩阵取出对应的值,gather
函数返回值的维度与索引矩阵维度相同。
下面就用gather
定义交叉熵损失函数。
1 2 def cross_entropy (y_hat, y ): return -torch.log(y_hat.gather(1 , y.view(-1 , 1 )))
举个例子,如果有一个样本经过softmax回归得到矩阵:
[ 0.2 0.4 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 ] \begin{bmatrix}0.2 & 0.4 & 0.05& 0.05& 0.05& 0.05& 0.05& 0.05& 0.05& 0.05\end{bmatrix} [ 0 . 2 0 . 4 0 . 0 5 0 . 0 5 0 . 0 5 0 . 0 5 0 . 0 5 0 . 0 5 0 . 0 5 0 . 0 5 ] ,维度为(1, 10)
。
真实标签为2,则y = [ 2 ] y = \begin{bmatrix}2\end{bmatrix} y = [ 2 ] ,y.view(-1,1)
确保有多个样本时,样本标签是一个列向量。
然后dim=1
,按行索引,故取输出矩阵第一行(也只有一行)的第2个元素值0.4,再经过log
函数,得到损失值。
计算分类准确率分类准确率即正确预测数量与总预测数量之比。
1 2 def accuracy (y_hat, y ): return (y_hat.argmax(dim=1 ) == y).float ().mean().item()
举个例子,y_hat
矩阵为[ 0.1 0.9 0.7 0.3 ] \begin{bmatrix}0.1 & 0.9 \\ 0.7 & 0.3\end{bmatrix} [ 0 . 1 0 . 7 0 . 9 0 . 3 ] ,y_hat.argmax(dim=1)
表示按行比较,找出每一行最大值对应的索引,得到矩阵[ 1 0 ] \begin{bmatrix}1 \\ 0\end{bmatrix} [ 1 0 ] ,然后再和y
比较,例如y = [ 0 0 ] y= \begin{bmatrix}0 \\ 0\end{bmatrix} y = [ 0 0 ] ,则y_hat.argmax(dim=1) == y
得到矩阵[ 0 1 ] \begin{bmatrix}0 \\ 1\end{bmatrix} [ 0 1 ] ,然后将元素值转换成浮点数,再求平均值,(0+1)/2 = 0.5得到准确率。
类似地,可以评价模型net
在数据集data_iter
的准确率。
1 2 3 4 5 6 def evaluate_accuracy (data_iter, net ): acc_sum, n = 0.0 , 0 for X, y in data_iter: acc_sum += (net(X).argmax(dim=1 ) == y).float ().sum ().item() n += y.shape[0 ] return acc_sum / n
注意使用的是sum
函数,不是mean
,因为有多个batch,所以不单独算每个batch的准确率,而是遍历完后,算一个总准确率。
计算每个batch的准确预测个数,求和,n是样本总数,最后返回准确率。
训练模型训练softmax回归的实现和线性回归实现非常相似,同样使用小批量随机梯度下降优化模型损失函数,迭代次数num_epochs
和学习率lr
都是可调的超参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 num_epochs = 5 lr = 0.1 def train (net, train_iter, test_iter, loss, num_epochs, batch_size, params=None , lr=None , optimzer=None ): for epoch in range (1 , num_epochs + 1 ): train_loss_sum = 0.0 train_acc_sum = 0.0 n = 0 for X, y in train_iter: y_hat = net(X) l = loss(y_hat, y).sum () if optimzer is not None : optimzer.zero_grad() elif params is not None and params[0 ].grad is not None : for param in params: param.grad.data.zero_() l.backward() if optimzer is None : d2l.sgd(params, lr, batch_size) else : optimzer.step() train_loss_sum += l.item() train_acc_sum += (y_hat.argmax(dim=1 ) == y).sum ().item() n += y.shape[0 ] test_acc = evaluate_accuacy(test_iter, net) print ('epoch %d, loss %.4f, train acc %.3f, test acc %.3f' % (epoch, train_loss_sum / n, train_acc_sum / n, test_acc)) train(net, train_iter, test_iter, cross_entropy, num_epochs, batch_size, [W, b], lr)
输出为:
1 2 3 4 5 epoch 1, loss 0.7866, train acc 0.749, test acc 0.785 epoch 2, loss 0.5701, train acc 0.813, test acc 0.811 epoch 3, loss 0.5268, train acc 0.824, test acc 0.820 epoch 4, loss 0.5005, train acc 0.832, test acc 0.824 epoch 5, loss 0.4856, train acc 0.836, test acc 0.825
预测训练完成后,就可以对图像进行预测。给定一系列图像,比较真实标签和模型预测结果。
1 2 3 4 5 6 X, y = iter (test_iter).next () true_labels = get_fashion_mnist_labels(y.numpy()) pred_labels = get_fashion_mnist_labels(net(X).argmax(dim=1 ).numpy()) titles = [true + '\n' + pred for true, pred in zip (true_labels, pred_labels)] d2l.show_fashion_mnist(X[0 :9 ], titles[0 :9 ])
只显示前9张图像的预测结果。第一行是真实标签,第二行是预测标签。
输出为:
softmax回归的简洁实现使用PyTorch实现softmax回归,首先导入所需包和模块。
d2lzh_pytorch
库封装了之前一些诸如加载图像、评估准确率等函数,这些函数在 从零实现 中全都实现过了。
在后面学习中,还会用到这个库。
1 2 3 4 5 import torchfrom torch import nnfrom torch.nn import initimport numpy as npimport d2lzh_pytorch as d2l
获取数据跟上一节代码基本一样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 batch_size = 256 train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, root='./datasets/fashion_mnist/' )def load_data_fashion_mnist (batch_size, resize=None , root ): """Download the fashion mnist dataset and then load into memory.""" trans = [] if resize: trans.append(torchvision.transforms.Resize(size=resize)) trans.append(torchvision.transforms.ToTensor()) transform = torchvision.transforms.Compose(trans) mnist_train = torchvision.datasets.FashionMNIST(root=root, train=True , download=True , transform=transform) mnist_test = torchvision.datasets.FashionMNIST(root=root, train=False , download=True , transform=transform) if sys.platform.startswith('win' ): num_workers = 0 else : num_workers = 4 train_iter = torch.utils.data.DataLoader(mnist_train, batch_size=batch_size, shuffle=True , num_workers=num_workers) test_iter = torch.utils.data.DataLoader(mnist_test, batch_size=batch_size, shuffle=False , num_workers=num_workers) return train_iter, test_iter
定义和初始化模型softmax回归输出层是一个全连接层,因此只用一个线性模块就可以。
每个batch样本X
的形状是(batch_size, 1, 28, 28)
,所以需要先用view()
函数将形状转换成(batch_size, 784)
才能送入全连接层。
1 2 3 4 5 6 7 8 9 10 11 12 num_inputs = 784 num_outputs = 10 class LinearNet (nn.Module): def __init__ (self, num_inputs, num_outputs ): super (LinearNet, self).__init__() self.linear = nn.Linear(num_inputs, num_outputs) def forward (self, X ): y = self.linear(X.view(X.shape[0 ], -1 )) return y net = LinearNet(num_inputs, num_outputs)
将X
形状转换的功能定义为一个类FlattenLayer
,并记录在d2l
中。
1 2 3 4 5 class FlattenLayer (nn.Module): def __init__ (self ): super (FlattenLayer, self).__init__() def forward (self, X ): return X.view(X.shape[0 ], -1 )
那么就可以这样来定义模型:
1 2 3 4 5 6 7 8 9 10 from collections import OrderedDict net = nn.Sequential( OrderedDict([ ('flatten' , FlattenLayer()), ('linear' , nn.Linear(num_inputs, num_outputs)) ]) )
然后使用均值为0,标准差为0.01的正态分布随机初始化模型的权重参数。
1 2 init.normal_(net.linear.weight, mean=0 , std=0.01 ) init.constant_(net.linear.bias, val=0 )
softmax和交叉熵损失函数上一节中,分别定义softmax运算和交叉熵损失函数可能会造成数值不稳定。PyTorch提供了一个包括softmax运算和交叉熵损失计算的函数,数值稳定性更好。
1 loss = nn.CrossEntropyLoss()
其中CrossEntropyLoss
结合了LogSoftmax
和NLLLoss
。
dim=0
,因此softmax按列计算,每一列只有一个元素,因此softmax运算得到矩阵[1,1,1],取log得0。
将dim
属性改为1,则得到输出tensor([[-2.4076, -1.4076, -0.4076]])
。
NLLLoss
:简单来说,这个损失函数将预测标签中对应真实标签中的值取出,加个负号
输入为(1, category)
的情况:即只有单个样本的预测标签。
1 2 3 4 5 6 7 nllloss = nn.NLLLoss() pred = torch.tensor([[0.2 , 0.5 , 0.3 ]], dtype=torch.float32) label = torch.tensor([1 ], dtype=torch.long) output = nllloss(pred, label)print (output)
将pred张量中第1个(索引为1,顺序为2)元素取出,加个负号,得到-0.5。
输入为(n, category)
,输入多个样本的预测标签。
1 2 3 4 5 6 7 nllloss = nn.NLLLoss() pred = torch.tensor([[0.1 ,0.3 ,0.6 ], [0.4 ,0.5 ,0.1 ]], dtype=torch.float32) label = torch.tensor([0 , 1 ], dtype=torch.long) output = nllloss(pred, label)print (output)
第1个样本预测概率为[0.1, 0.3, 0.6],对应真实标签为0,将第0个位置值取出加负号,为-0.1
第2个样本预测概率为[0.4, 0.5, 0.1],对应真实标签为1,将第1个位置值取出加负号,为-0.5
然后取平均值,得到-0.3。
可以添加属性nn.NLLLoss(reduction='sum')
,这样返回的是多个样本预测概率相反数的和。
因此,CrossEntropyLoss
相当于softmax + log + NLLLoss
。
定义优化算法使用学习率为0.1的小批量随机梯度下降作为优化算法。
1 optimizer = torch.optim.SGD(net.parameters(), lr=0.1 )
训练使用上一节定义的训练函数训练模型。
训练模型代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 def train (net, train_iter, test_iter, loss, num_epochs, batch_size, params=None , lr=None , optimizer=None ): for epoch in range (num_epochs): train_l_sum, train_acc_sum, n = 0.0 , 0.0 , 0 for X, y in train_iter: y_hat = net(X) l = loss(y_hat, y).sum () if optimizer is not None : optimizer.zero_grad() elif params is not None and params[0 ].grad is not None : for param in params: param.grad.data.zero_() l.backward() if optimizer is None : sgd(params, lr, batch_size) else : optimizer.step() train_l_sum += l.item() train_acc_sum += (y_hat.argmax(dim=1 ) == y).sum ().item() n += y.shape[0 ] test_acc = evaluate_accuracy(test_iter, net) print ('epoch %d, loss %.4f, train acc %.3f, test acc %.3f' % (epoch + 1 , train_l_sum / n, train_acc_sum / n, test_acc))
在d2l
中函数名为train_ch3
。
进行训练:
1 2 num_epochs = 5 d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None , None , optimizer)
输出为:
1 2 3 4 5 epoch 1, loss 0.0031, train acc 0.748, test acc 0.790 epoch 2, loss 0.0022, train acc 0.812, test acc 0.809 epoch 3, loss 0.0021, train acc 0.826, test acc 0.810 epoch 4, loss 0.0020, train acc 0.832, test acc 0.812 epoch 5, loss 0.0019, train acc 0.837, test acc 0.825