获取与读取数据

使用先前提到过的Fashion-MNIST数据集,使用包中提供的函数初始化迭代器

  1. import torch
  2. import torchvision
  3. import sys
  4. import d2lzh
  5. batch_size = 256
  6. train_iter, test_iter = d2lzh.load_data_fashion_mnist(batch_size)

模型与参数初始化

与线性回归类似,还是采用向量描述各个样本,由于图像分类数据集为2828的灰度图像,因此通道数为1,无需考虑色彩问题。所以输入向量的维度为2828=784,由于我们需要分类的结果为10个,因此输出向量为10维。与线性回归类似,设样本数量为n,输入向量为n784维,由于我们需要得到n10维的矩阵(n个10分类结果),因此我们构建模型的参数W的维数应为78410,这样输入向量乘以W得到的维数就与输出矩阵的维数相同。此外,由于偏置项需要直接加在W与输入矩阵的中间结果上,所以维数要与中间结果维数一致,为n1维列向量。这里初始化为0。
补充:Pytorch中rand,randn, random以及normal的区别

  1. num_inputs = 784 # 图像高与宽均为28像素,因此输入向量长度为28*28=784
  2. num_outputs = 10 # 输出10个类别
  3. W = torch.normal(0, 0.01, [num_inputs, num_outputs], dtype=torch.float, requires_grad=True)
  4. b = torch.zeros(num_outputs, dtype=torch.float, requires_grad=True)

实现Softmax

按维度操作张量

给定一个多维Tensor,可以只对其中一列或同一行的元素求和(求平均)
可以在结果中选择是否保留行和列这两个维度,若选择否,则不管是行还是列,都会输出一个行向量(1*n)
计算出的结果并不是引用类型。在对操作后的结果进行运算时,也不会对原张量造成影响

  1. X = torch.tensor([[1, 2, 3], [4, 5, 6]])
  2. tmp = X.sum(dim=1, keepdim=True)
  3. tmp += 1
  4. print(tmp)
  5. print(X)
  6. 结果:
  7. tensor([[ 6, 8, 10]])
  8. tensor([[1, 2, 3],
  9. [4, 5, 6]])

Softmax运算

在下面的函数中,矩阵X的行数是样本数,列数是输出个数。为了表达样本预测各个输出的概率,softmax运算会先通过exp函数对每个元素做指数运算,再对exp矩阵同行元素求和,最后令矩阵每行各元素与该行元素之和相除。这样一来,最终得到的矩阵每行元素和为1且非负。因此,该矩阵每行都是合法的概率分布。softmax运算的输出矩阵中的任意一行元素代表了一个样本在各个输出类别上的预测概率。

所以说所谓softmax,其实就是对于每一个样本,给出它取每个输出类别的概率,达到多分类的目的。其实尚未经过softmax处理的数值也能一定程度上表达样本取各个输出类别的概率大小。而softmax其实就是对样本进行了规范化及归一化处理。比方说两个特征值若为-9000与1,经过softmax后输出的可能就是0.001与0.999,不再有数值差异过大,符号不同,不是绝对概率的问题出现。
那么怎么做呢,其实很简单,对样本中每一个元素进行e的幂运算。对于单个样本,计算经过运算后元素的和,再拿进行幂运算之后的结果(样本中每个元素)除以和后,就得到了最终结果。

  1. def softmax(x):
  2. x_exp = x.exp() # 将矩阵中每个元素变为以e为底,x为指数的值
  3. print(x_exp)
  4. partition = x_exp.sum(dim=1, keepdim=True) # 分别计算矩阵每一行的和
  5. print(partition)
  6. return x_exp / partition #这一步实际上应用了广播机制
  7. x = torch.rand((2, 5))
  8. print(x)
  9. X_prob = softmax(X)
  10. print(X_prob)
  11. print(X_prob.sum(dim=1))
  12. 输出如下:
  13. tensor([[0.1976, 0.4458, 0.1911, 0.9689, 0.2368],
  14. [0.9857, 0.6690, 0.6809, 0.6560, 0.0973]]) 这是随机生成的矩阵
  15. tensor([[1.2185, 1.5618, 1.2106, 2.6349, 1.2671],
  16. [2.6797, 1.9522, 1.9756, 1.9270, 1.1021]]) 这是经过e的次方运算后得到的矩阵
  17. tensor([[7.8929],
  18. [9.6368]]) 对上面结果每一行求和得到的矩阵
  19. tensor([[0.1544, 0.1979, 0.1534, 0.3338, 0.1605],
  20. [0.2781, 0.2026, 0.2050, 0.2000, 0.1144]]) 第二步的矩阵除以第三步的矩阵得到的矩阵,每一行之和为1
  21. tensor([1.0000, 1.0000])每一行之和为1

定义模型

有了softmax运算,我们可以定义上节描述的softmax回归模型了。这里通过view函数将每张原始图像改成长度为num_inputs的向量。

  1. # 将图像矩阵转为列向量输入
  2. def net(x):
  3. return softmax(torch.mm(x.view((-1, num_inputs)), W) + b)

定义损失函数

gather函数

gather(dim, index)函数是为了在每个样本中选择指定下标的值。其中,dim表示维度,index表示在该维度下选择的下标
参见:Pytorch之gather的用法(易懂的方式解释)

  1. y_hat = torch.tensor([[0.1, 0.3, 0.6], [0.4, 0.2, 0.5]])
  2. y = torch.LongTensor([1, 2])
  3. 意为选择y_hat第一个样本中第2个特征值,第二个样本中第3个特征值
  4. print(y_hat.gather(1, y.view(-1, 1)))
  5. 结果:
  6. tensor([[0.3000],
  7. [0.5000]])
  8. y_hat = torch.tensor([[0.1, 0.3, 0.6], [0.4, 0.2, 0.5]])
  9. y = torch.LongTensor([0, 1])
  10. print(y_hat.gather(0, y.view(-1, 1)))
  11. 结果:
  12. tensor([[0.1000],
  13. [0.4000]])
  14. 为啥会这样?只能选择第一列的元素吗?LongTensor定义三列也会报错

交叉熵损失函数

为了得到标签的预测概率,我们可以使用gather函数。在下面的例子中,变量y_hat是2个样本在3个类别的预测概率,变量y是这2个样本的标签类别。通过使用gather函数,我们得到了2个样本的标签的预测概率。与3.4节(softmax回归)数学表述中标签类别离散值从1开始逐一递增不同,在代码中,标签类别的离散值是从0开始逐一递增的。

交叉熵损失函数公式为-yi*log(yi_hat)之和,但考虑到样本的label,yi只在一个维度上值为1,别的维度都为0,因此它与y_hat的乘积实际上就是选择了对应维度的输出类别值,所以只需计算选定维度y_hat的ln值即可。所以,y_hat对应类别的概率值越高,损失函数值越小。所以若正确的标签预测概率较小,就需要对模型进行优化。

  1. def cross_entropy(y, y_hat):
  2. return - torch.log(y_hat.gather(1, y.view(-1, 1)))

计算分类准确率

argmax(self, dim=None, keepdim=False)函数可以在返回一个在指定维度上取值最大的索引。Tensor.argmax()返回值在于另一个Tensor进行比较后并不会返回bool类型,而是会返回一个包含bool类型元素的Tensor。所以,要计算准确率,还需要使用.float(),.mean()以及.item()计算出确切的浮点数值。

  1. # 返回一个样本中值最大的索引
  2. print(y_hat.argmax(dim=1))
  3. # 返回bool类型的Tensor
  4. print((y_hat.argmax(dim=1) == y))
  5. # .float()函数用于将bool类型的张量转换为float类型(True->0.,False->1.),.mean()通过均值计算总体正确率,.item将Tensor类型转换为普通float类型
  6. print((y_hat.argmax(dim=1) == y).float().mean().item())
  7. 结果:
  8. tensor([2, 2])
  9. tensor([False, True])
  10. 0.5

所以,我们可以通过如下方法计算准确率

y_hat.argmax(dim=1)返回矩阵y_hat每行中最大元素的索引,且返回结果与变量y形状相同。相等条件判断式(y_hat.argmax(dim=1) == y)是一个类型为ByteTensorTensor,我们用float()将其转换为值为0(相等为假)或1(相等为真)的浮点型Tensor

Tensor.shape[i]的值为该张量在某一维度上的大小,算是常用命令

  1. # 计算一个批量数据集上的准确率
  2. def accuracy(y, y_hat):
  3. return (y_hat.argmax(dim=1) == y).float().mean().item()
  4. def evaluate_accuracy(data_iter, net):
  5. acc_per, n = 0.0, 0
  6. for x, y in data_iter:
  7. acc_per += accuracy(y, net(x))
  8. # Tensor.shape[i]的值为该张量在某一维度上的大小,这里是为了统计样本总量
  9. n += 1
  10. return acc_per / n
  11. # 参数随机初始化,因此输出接近于0.1的解
  12. print(evaluate_accuracy(test_iter, net))
  13. # 教程给的解法,本质都一样
  14. def evaluate_accuracy(data_iter, net):
  15. acc_sum, n = 0.0, 0
  16. for X, y in data_iter:
  17. acc_sum += (net(X).argmax(dim=1) == y).float().sum().item()
  18. n += y.shape[0]
  19. return acc_sum / n
  20. 结果:
  21. 0.0912109375

训练模型

与线性回归类似

  1. def sgd(params, lr, batch_size):
  2. for param in params:
  3. param.data -= lr * param.grad / batch_size # 注意这里更改param时用的param.data
  4. def train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size,
  5. params=None, lr=None, optimizer=None):
  6. for epoch in range(num_epochs):
  7. train_l_sum, train_acc_sum, n = 0.0, 0.0, 0
  8. for X, y in train_iter:
  9. y_hat = net(X)
  10. l = loss(y, y_hat).sum()
  11. # 梯度清零
  12. if optimizer is not None:
  13. optimizer.zero_grad()
  14. elif params is not None and params[0].grad is not None:
  15. for param in params:
  16. param.grad.data.zero_()
  17. l.backward()
  18. if optimizer is None:
  19. sgd(params, lr, batch_size)
  20. else:
  21. optimizer.step() # 更新参数
  22. train_l_sum += l.item()
  23. train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()
  24. n += y.shape[0]
  25. test_acc = evaluate_accuracy(test_iter, net)
  26. print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f'
  27. % (epoch + 1, train_l_sum / n, train_acc_sum / n, test_acc))

预测

  1. if __name__ == "__main__":
  2. train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, batch_size, [W, b], lr)
  3. X, y = iter(test_iter).next()
  4. true_labels = d2lzh.get_fashion_mnist_labels(y)
  5. pred_labels = d2lzh.get_fashion_mnist_labels(net(X).argmax(dim=1))
  6. titles = [true + '\n' + pred for true, pred in zip(true_labels, pred_labels)]
  7. d2lzh.show_fashion_mnist(X[0:9], titles[0:9]) # 注意要把包中函数最后一行plt.show()去掉注释,以便显示图像

完整代码

  1. import torch
  2. import d2lzh
  3. batch_size = 256
  4. train_iter, test_iter = d2lzh.load_data_fashion_mnist(batch_size)
  5. num_inputs = 784 # 图像高与宽均为28像素,因此输入向量长度为28*28=784
  6. num_outputs = 10 # 输出10个类别
  7. W = torch.normal(0, 0.01, [num_inputs, num_outputs], dtype=torch.float, requires_grad=True)
  8. b = torch.zeros(num_outputs, dtype=torch.float, requires_grad=True)
  9. def softmax(x):
  10. x_exp = x.exp() # 将矩阵中每个元素变为以e为底,x为指数的值
  11. partition = x_exp.sum(dim=1, keepdim=True)
  12. return x_exp / partition
  13. # 将图像矩阵转为列向量输入
  14. def net(x):
  15. return softmax(torch.mm(x.view((-1, num_inputs)), W) + b)
  16. # 交叉熵损失函数公式为-yi*log(yi_hat)之和,但考虑到样本的label,yi只在一个维度上值为1,别的维度都为0,因此它与y_hat的乘积实际上就是选择了对应维度的输出类别值,所以只需计算选定维度y_hat的Ln值即可。所以,y_hat对应类别的概率值越高,损失函数值越小。
  17. def cross_entropy(y, y_hat):
  18. return - torch.log(y_hat.gather(1, y.view(-1, 1)))
  19. print(y_hat.gather(1, y.view(-1, 1)))
  20. print(cross_entropy(y, y_hat))
  21. # 计算一个批量数据集上的准确率
  22. def accuracy(y, y_hat):
  23. return (y_hat.argmax(dim=1) == y).float().mean().item()
  24. def evaluate_accuracy(data_iter, net):
  25. acc_per, n = 0.0, 0
  26. for x, y in data_iter:
  27. acc_per += accuracy(y, net(x))
  28. # Tensor.shape[i]的值为该张量在某一维度上的大小,这里是为了统计样本总量
  29. n += 1
  30. return acc_per / n
  31. def sgd(params, lr, batch_size):
  32. for param in params:
  33. param.data -= lr * param.grad / batch_size # 注意这里更改param时用的param.data
  34. # 参数随机初始化,因此输出接近于0.1的解
  35. print(evaluate_accuracy(test_iter, net))
  36. num_epochs, lr = 5, 0.1
  37. # 本函数已保存在d2lzh包中方便以后使用
  38. def train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size,
  39. params=None, lr=None, optimizer=None):
  40. for epoch in range(num_epochs):
  41. train_l_sum, train_acc_sum, n = 0.0, 0.0, 0
  42. for X, y in train_iter:
  43. y_hat = net(X)
  44. l = loss(y, y_hat).sum()
  45. # 梯度清零
  46. if optimizer is not None:
  47. optimizer.zero_grad()
  48. elif params is not None and params[0].grad is not None:
  49. for param in params:
  50. param.grad.data.zero_()
  51. l.backward()
  52. if optimizer is None:
  53. sgd(params, lr, batch_size)
  54. else:
  55. optimizer.step() # 更新参数
  56. train_l_sum += l.item()
  57. train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()
  58. n += y.shape[0]
  59. test_acc = evaluate_accuracy(test_iter, net)
  60. print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f'
  61. % (epoch + 1, train_l_sum / n, train_acc_sum / n, test_acc))
  62. if __name__ == "__main__":
  63. train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, batch_size, [W, b], lr)
  64. X, y = iter(test_iter).next()
  65. true_labels = d2lzh.get_fashion_mnist_labels(y)
  66. pred_labels = d2lzh.get_fashion_mnist_labels(net(X).argmax(dim=1))
  67. titles = [true + '\n' + pred for true, pred in zip(true_labels, pred_labels)]
  68. d2lzh.show_fashion_mnist(X[0:9], titles[0:9])