1.分类问题
一个简单的图像分类问题,输入图像的高和宽均为2像素,色彩为灰度。
图像中的4像素分别记为 x 1 , x 2 , x 3 , x 4 x_1,x_2,x_3,x_4 x1,x2,x3,x4。
假设真实标签为狗、猫或者鸡,这些标签对应的离散值为 y 1 , y 2 , y 3 y_1,y_2,y_3 y1,y2,y3。
我们通常使用离散的数值来表示类别,例如 y 1 = 1 , y 2 = 2 , y 3 = 3 y_1=1,y_2=2,y_3=3 y1=1,y2=2,y3=3。
2.权重矢量
3.神经网络图
下图用神经网络图描绘了上面的计算。softmax回归同线性回归一样,也是一个单层神经网络。由于每个输出 o 1 , o 2 , o 3 o_1,o_2,o_3 o1,o2,o3的计算都要依赖于所有的输入 x 1 , x 2 , x 3 , x 4 x_1,x_2,x_3,x_4 x1,x2,x3,x4,softmax回归的输出层也是一个全连接层。
既然分类问题需要得到离散的预测输出,一个简单的办法是将输出值 o i o_i oi当作预测类别是 i i i的置信度,并将值最大的输出所对应的类作为预测输出,即输出 a r g m a x i o i arg\ \underset{i}{max}\ o_i arg imax oi 。例如,如果 o 1 , o 2 , o 3 o_1,o_2,o_3 o1,o2,o3分别为0.1,10,0.1,由于 o 2 o_2 o2最大,那么预测类别为2,其代表猫。
4.输出问题
直接使用输出层的输出有两个问题:
softmax运算符(softmax operator)解决了以上两个问题。它通过下式将输出值变换成值为正且和为1的概率分布:
其中
容易看出 y 1 ^ + y 2 ^ + y 3 ^ = 1 \hat{y_1}+\hat{y_2}+\hat{y_3}=1 y1^+y2^+y3^=1且 0 ≤ y 1 ^ , y 2 ^ , y 3 ^ 0\leq \hat{y_1}, \hat{y_2}, \hat{y_3} 0≤y1^,y2^,y3^,因此 y 1 ^ , y 2 ^ , y 3 ^ \hat{y_1}, \hat{y_2}, \hat{y_3} y1^,y2^,y3^是一个合法的概率分布。这时候,如果 y 2 ^ = 0.8 \hat{y_2}=0.8 y2^=0.8,不管 y 1 ^ \hat{y_1} y1^和 y 3 ^ \hat{y_3} y3^的值是多少,我们都知道图像类别为猫的概率是80%。此外,我们注意到
a r g m a x i o i = a r g m a x i y i ^ arg\ \underset{i}{max}\ o_i=arg\ \underset{i}{max}\ \hat{y_i} arg imax oi=arg imax yi^
因此softmax运算不改变预测类别输出。
5.计算效率
6.小批量矢量计算表达式
为了进一步提升计算效率,我们通常对小批量数据做矢量计算。广义上讲,给定一个小批量样本,其批量大小为n,输入个数(特征数)为d,输出个数(类别数)为q。设批量特征为 X ∈ R n × d X\in R^{n\times d} X∈Rn×d。假设softmax回归的权重和偏差参数分别为 W ∈ R d × q W\in R^{d\times q} W∈Rd×q和 b ∈ R 1 × q b\in R^{1\times q} b∈R1×q。softmax回归的矢量计算表达式为
其中的加法运算使用了广播机制, O , Y ^ ∈ R n × q O,\hat{Y}\in R^{n\times q} O,Y^∈Rn×q且这两个矩阵的第i行分别为样本i的输出 o ( i ) o^{(i)} o(i)和概率分布 y ^ ( i ) \hat{y}^{(i)} y^(i)。
对于样本 i i i,我们构造向量 y ( i ) ∈ R q y^{(i)}\in R^q y(i)∈Rq ,使其第 y ( i ) y^{(i)} y(i)(样本 i i i类别的离散数值)个元素为1,其余为0。这样我们的训练目标可以设为使预测概率分布 y ^ ( i ) \hat{y}^{(i)} y^(i)尽可能接近真实的标签概率分布 y ( i ) y^{(i)} y(i)。
然而,想要预测分类结果正确,我们其实并不需要预测概率完全等于标签概率。例如,在图像分类的例子里,如果 y ( i ) = 3 y^{(i)}=3 y(i)=3,那么我们只需要 y ^ 3 ( i ) \hat{y}_3^{(i)} y^3(i)比其他两个预测值 y ^ 1 ( i ) \hat{y}_1^{(i)} y^1(i)和 y ^ 2 ( i ) \hat{y}_2^{(i)} y^2(i)大就行了。即使 y ^ 3 ( i ) \hat{y}_3^{(i)} y^3(i)值为0.6,不管其他两个预测值为多少,类别预测均正确。而平方损失则过于严格,例如 y ^ 1 ( i ) = y ^ 2 ( i ) = 0.2 \hat{y}_1^{(i)}=\hat{y}_2^{(i)}=0.2 y^1(i)=y^2(i)=0.2比 y ^ 1 ( i ) = 0 \hat{y}_1^{(i)}=0 y^1(i)=0, y ^ 2 ( i ) = 0.4 \hat{y}_2^{(i)}=0.4 y^2(i)=0.4的损失要小很多,虽然两者都有同样正确的分类预测结果。
改善上述问题的一个方法是使用更适合衡量两个概率分布差异的测量函数。其中,交叉熵(cross entropy)是一个常用的衡量方法:
其中带下标的 y ^ j ( i ) \hat{y}_j^{(i)} y^j(i)是向量 y ( i ) \textbf{y}^{(i)} y(i)中非0即1的元素,需要注意将它与样本 i i i类别的离散数值,即不带下标的区分。在上式中,我们知道向量 y ( i ) \textbf{y}^{(i)} y(i)中只有第 y ( i ) y^{(i)} y(i)个元素 y ( i ) y ( i ) y^{(i)}y^{(i)} y(i)y(i)为1,其余全为0,于是 H ( y ( i ) , y ^ ( i ) ) = − l o g y ^ y ( i ) ( i ) \textbf{H}(y^{(i)},\hat{y}^{(i)})=-log\hat{y}_{y^{(i)}}^{(i)} H(y(i),y^(i))=−logy^y(i)(i)。也就是说,交叉熵只关心对正确类别的预测概率,因为只要其值足够大,就可以确保分类结果正确。当然,遇到一个样本有多个标签时,例如图像里含有不止一个物体时,我们并不能做这一步简化。但即便对于这种情况,交叉熵同样只关心对图像中出现的物体类别的预测概率。
假设训练数据集的样本数为 n n n,交叉熵损失函数定义为
其中 Θ \Theta Θ代表模型参数。同样地,如果每个样本只有一个标签,那么交叉熵损失可以简写成
从另一个角度来看,我们知道最小化 l ( Θ ) l(\Theta) l(Θ)等价于最大化
即最小化交叉熵损失函数等价于最大化训练数据集所有标签类别的联合预测概率。
在训练好softmax回归模型后,给定任一样本特征,就可以预测每个输出类别的概率。通常,我们把预测概率最大的类别作为输出类别。如果它与真实类别(标签)一致,说明这次预测是正确的。在3.6节的实验中,我们将使用准确率(accuracy)来评价模型的表现。它等于正确预测数量与总预测数量之比。
在介绍softmax回归的实现前我们先引入一个多类图像分类数据集。它将在后面的章节中被多次使用,以方便我们观察比较算法之间在模型精度和计算效率上的区别。图像分类数据集中最常用的是手写数字识别数据集MNIST[1]。但大部分模型在MNIST上的分类精度都超过了95%。为了更直观地观察算法之间的差异,我们将使用一个图像内容更加复杂的数据集Fashion-MNIST[2]。
我这里我们会使用torchvision包,它是服务于PyTorch深度学习框架的,主要用来构建计算机视觉模型。torchvision主要由以下几部分构成:
# import needed package
%matplotlib inline
from IPython import display
import matplotlib.pyplot as plt
import torch
import torchvision
import torchvision.transforms as transforms
import time
import sys
sys.path.append("/home/kesci/input")
import d2lzh1981 as d2l
print(torch.__version__)
print(torchvision.__version__)
1.3.0
0.4.1a0+d94043a
get dataset
mnist_train = torchvision.datasets.FashionMNIST(root='/home/kesci/input/FashionMNIST2065', train=True, download=True, transform=transforms.ToTensor())
mnist_test = torchvision.datasets.FashionMNIST(root='/home/kesci/input/FashionMNIST2065', train=False, download=True, transform=transforms.ToTensor())
class torchvision.datasets.FashionMNIST(root, train=True, transform=None, target_transform=None, download=False)
# show result
print(type(mnist_train))
print(len(mnist_train), len(mnist_test))
60000 10000
# 我们可以通过下标来访问任意一个样本
feature, label = mnist_train[0]
print(feature.shape, label) # Channel x Height x Width
torch.Size([1, 28, 28]) 9
如果不做变换输入的数据是图像,我们可以看一下图片的类型参数:
mnist_PIL = torchvision.datasets.FashionMNIST(root='/home/kesci/input/FashionMNIST2065', train=True, download=True)
PIL_feature, label = mnist_PIL[0]
print(PIL_feature)
# 本函数已保存在d2lzh包中方便以后使用
def get_fashion_mnist_labels(labels):
text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat',
'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']
return [text_labels[int(i)] for i in labels]
def show_fashion_mnist(images, labels):
d2l.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)
plt.show()
X, y = [], []
for i in range(10):
X.append(mnist_train[i][0]) # 将第i个feature加到X中
y.append(mnist_train[i][1]) # 将第i个label加到y中
show_fashion_mnist(X, get_fashion_mnist_labels(y))
# 读取数据
batch_size = 256
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)
start = time.time()
for X, y in train_iter:
continue
print('%.2f sec' % (time.time() - start))
4.95 sec
# 加载各种包或者模块
import torch
from torch import nn
from torch.nn import init
import numpy as np
import sys
sys.path.append("/home/kesci/input")
import d2lzh1981 as d2l
print(torch.__version__)
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, root='/home/kesci/input/FashionMNIST2065')
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): # x 的形状: (batch, 1, 28, 28)
y = self.linear(x.view(x.shape[0], -1))
return y
# net = LinearNet(num_inputs, num_outputs)
class FlattenLayer(nn.Module):
def __init__(self):
super(FlattenLayer, self).__init__()
def forward(self, x): # x 的形状: (batch, *, *, ...)
return x.view(x.shape[0], -1)
from collections import OrderedDict
net = nn.Sequential(
# FlattenLayer(),
# LinearNet(num_inputs, num_outputs)
OrderedDict([
('flatten', FlattenLayer()),
('linear', nn.Linear(num_inputs, num_outputs))]) # 或者写成我们自己定义的 LinearNet(num_inputs, num_outputs) 也可以
)
init.normal_(net.linear.weight, mean=0, std=0.01)
init.constant_(net.linear.bias, val=0)
Parameter containing:
tensor([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], requires_grad=True)
loss = nn.CrossEntropyLoss() # 下面是他的函数原型
# class torch.nn.CrossEntropyLoss(weight=None, size_average=None, ignore_index=-100, reduce=None, reduction='mean')
optimizer = torch.optim.SGD(net.parameters(), lr=0.1) # 下面是函数原型
# class torch.optim.SGD(params, lr=, momentum=0, dampening=0, weight_decay=0, nesterov=False)
num_epochs = 5
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer)
epoch 1, loss 0.0031, train acc 0.751, test acc 0.795
epoch 2, loss 0.0022, train acc 0.813, test acc 0.809
epoch 3, loss 0.0021, train acc 0.825, test acc 0.806
epoch 4, loss 0.0020, train acc 0.833, test acc 0.813
epoch 5, loss 0.0019, train acc 0.837, test acc 0.822