数据是深度学习的主要组成部分,让我们的神经网络从数据中学习是我们的任务。
了解一个数据集需要知道的一些知识?
MNIST数据集是美国国家标准与技术研究所(National Institute of Standards and Technology)修改后的数据库,是一个著名的手写数字数据集,通常用于培训机器学习的图像处理系统。NIST代表国家标准与技术研究所。
M 在MNIST代表modified,这是因为NIST有一个原始的数字数据集,它被修改为MNIST。
MNIST因数据集的使用频率而闻名。这很常见,有两个原因:
Fashion MNIST顾名思义就是一个时装项目的数据集。具体而言,该数据集包含以下十类时尚项目:
Fashion MNIST基于Zalando网站上的分类。Zalando是一家总部位于德国的跨国时尚商业公司,成立于2008年。这就是为什么我们可以在GitHub URL中看到 zalando research,在那里可以下载Fashion MNIST数据集。Zalando Research是创建该数据集的公司内部的团队。
fashion MNIST数据集以MNIST命名的原因是创作者试图用fashion MNIST替换MNIST。出于这个原因,时装数据集被设计成尽可能接近原始MNIST数据集,同时由于其数据比手写图像更复杂,导致训练难度更高。将在本文中看到Fashion MNIST镜像原始数据集的具体方式,但我们已经看到了类的数量。MNIST–有10个类(每个数字0-9对应一个)Fashion MNIST-有10个类(这是有意的)
论文地址:https://arxiv.org/pdf/1708.07747.pdf
数据集地址:https://github.com/zalandoresearch/fashion-mnist.
在这篇论文的摘要中可以看到:我们展示了Fashion MNIST,这是一个新的数据集,包含来自10个类别的70000件时装产品的28×28灰度图像,每个类别有7000张图像。训练集有60000张图像,测试集有10000张图像。Fashion MNIST旨在作为原始MNIST数据集的直接替代品,用于对机器学习算法进行测试,因为它共享相同的图像大小、数据格式以及训练和测试拆分结构。
另外,MNIST如此受欢迎的原因与它的规模有关,它允许深度学习研究人员快速检查并原型化他们的算法。此外,所有机器学习库(如scikit learn)和深度学习框架(如Tensorflow、PyTorch)都提供了辅助函数和方便的示例,可以使用MNIST开箱即用。
PyTorch确实为我们提供了一个名为torchvision的软件包,使我们能够轻松开始MNIST和Fashion MNIST。
与MNIST数据集不同,时装集不是手工绘制的,但数据集中的图像是Zalando网站上的真实图像。然而,它们已被转换为更符合MNIST规范。这是网站上每张图片的一般转换过程:
提取、转换和加载即Extract, Transform, And Load (ETL)
建立一个项目的一般过程:
我们将从准备数据开始。为了准备数据,我们将遵循一个大致被称为ETL的过程。
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import torchvision
import torchvision.transforms as transforms
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.metrics import confusion_matrix
#from plotcm import plot_confusion_matrix
数据准备:
Extract –从源文件中获取时尚Fashion MNIST图像数据。
Transform – 将数据转换为张量形式
Load – 将数据放入对象中,使其易于访问.
为此,PyTorch提供了两个类:
torchvision包提供了下面的具体功能:
class FashionMNIST(MNIST):
"""`Fashion-MNIST `_ Dataset.
Args:
root (string): Root directory of dataset where ``processed/training.pt``
and ``processed/test.pt`` exist.
train (bool, optional): If True, creates dataset from ``training.pt``,
otherwise from ``test.pt``.
download (bool, optional): If true, downloads the dataset from the internet and
puts it in root directory. If dataset is already downloaded, it is not
downloaded again.
transform (callable, optional): A function/transform that takes in an PIL image
and returns a transformed version. E.g, ``transforms.RandomCrop``
target_transform (callable, optional): A function/transform that takes in the
target and transforms it.
"""
urls = [
'http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/train-images-idx3-ubyte.gz',
'http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/train-labels-idx1-ubyte.gz',
'http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/t10k-images-idx3-ubyte.gz',
'http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/t10k-labels-idx1-ubyte.gz',
]
利用torchvision可以简化操作
加载数据集只需要执行以下的代码:
train_set = torchvision.datasets.FashionMNIST(
root='./data'
,train=True
,download=True
,transform=transforms.Compose([
transforms.ToTensor()
])
)
参数介绍:
PyTorch DataLoader
创建DataLoader:
train_loader = torch.utils.data.DataLoader(train_set
,batch_size=1000
,shuffle=True
)
参数介绍:
从上文中我们已经得到了两个对象实例:train_set和train_loader
要查看训练集中有多少图像,我们可以使用Python中的len()函数检查数据集的长度
如果想查看每个图像的标签可以使用 train_set.targets。
如果想查看数据集中每个标签的数量,可以使用PyTorch中bincount()函数
len(train_set)
train_set.targets
train_set.targets.bincount() #表示每一类有6000张图像。
#tensor([6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000])
类的平衡性:
通过上面的数据表明Fashion MNIST数据集在每个类中的样本数量上是一致的。这意味着每个类有6000个样品。因此,这个数据集被认为是平衡的。如果类具有不同数量的样本,我们会将该集合称为不平衡数据集。
类不平衡是一个常见的问题,但在Fashion MNIST数据集中,数据集是平衡的。
访问训练集中的数据:
为了从训练集中访问单个元素,我们首先将train_set对象传递给Python的iter()内置函数,该函数返回一个表示数据流的对象。
对于数据流,我们可以使用Python内置的next()函数来获取数据流中的下一个数据元素。我们希望从中得到一个样本,因此我们将相应地命名结果:
sample = next(iter(train_set))
len(sample)#2
将样本传递给len()函数后,我们可以看到样本包含两个项,这是因为数据集包含图像标签对。我们从训练集中检索的每个样本都包含作为张量的图像数据和作为张量的相应标签。(特征值,标签)的形式
由于样本是 sequence类型(tuple),我们可以使用sequence unpacking来分配图像和标签。现在,我们将检查图像和标签的类型,将其分解成PyTorch.Tensor类型
sample = next(iter(train_set))
len(sample)
type(sample)
image, label = sample
type(image)#torch.Tensor
type(label)#int
查看数据的大小:
image是shape为[1, 28, 28]的张量,
label是标量张量值。
print(image.shape)#torch.Size([1, 28, 28])
print(torch.tensor(label).shape)#torch.Size([])
其中1表示的是颜色通道数为1 ,即为灰度图像。讲image压缩到28*28,然后绘制这张图像得到下面的结果,就是第一张图像的标签值为9,也就是第九类。
image.squeeze().shape #torch.Size([28, 28])
plt.imshow(image.squeeze(), cmap="gray")
torch.tensor(label)
#tensor(9)
在上文中,得到图像的方式是通过使用 iter()、next()对train_set进行操作,我们也可以利用data_loader进行批量操作图像。
重新定义了一个加载器,batch=10值小一点,我们加载器中得到一批,就像我们在训练集中看到的那样。我们使用iter()和next()函数进行访问。在使用数据加载器时,有一件事需要注意。如果shuffle=True,则每次调用next时,批处理都会不同。当shuffle=True时,训练集中的第一个样本将在第一次调用next时返回。默认情况下,shuffle功能处于关闭状态。
由于batch_size=10,因此正在处理一批10幅图像和10个相应的标签。这就是为什么我们在变量名上使用复数。这些类型是我们期望的张量。然而,形状与我们在单个样本中看到的不同。我们没有使用单个标量值作为标签,而是使用了一个具有10个值的秩为1张量。包含图像数据的张量中每个维度的大小由以下每个值定义:(batch size, number of color channels, image height, image width)
display_loader = torch.utils.data.DataLoader(
train_set, batch_size=10
)
batch = next(iter(display_loader))
print('len:', len(batch)) #len: 2
images, labels = batch
print('types:', type(images), type(labels)) #types:
print('shapes:', images.shape, labels.shape) #shapes: torch.Size([10, 1, 28, 28]) torch.Size([10])
images[0].shape #torch.Size([1, 28, 28])
labels[0] #9
要绘制一批图像,我们可以使用torchvision.utils.make_grid()函数创建一个网格:
grid = torchvision.utils.make_grid(images, nrow=10)
plt.figure(figsize=(15,15))
plt.imshow(np.transpose(grid, (1,2,0)))
# plt.imshow(grid.permute(1,2,0))
print('labels:', labels)#labels: tensor([9, 0, 0, 3, 0, 2, 7, 2, 5, 5])
使用PyTorch中的DataLoader绘制图像
这是另外一种绘制图像的方法。
how_many_to_plot = 20
train_loader_iter = torch.utils.data.DataLoader(
train_set, batch_size=1, shuffle=True
)
plt.figure(figsize=(50,50))
for i, batch in enumerate(train_loader_iter, start=1):
image, label = batch
plt.subplot(10,10,i)
plt.imshow(image.reshape(28,28), cmap='gray')
plt.axis('off')
plt.title(train_set.classes[label.item()], fontsize=28)
if (i >= how_many_to_plot): break
plt.show()
准备好数据之后,就是建立模型,通常我们也说网络
为了在PyTorch中建立神经网络,我们扩展了Torch.nn.module的PyTorch类。这意味着我们需要在Python中使用一点面向对象编程(OOP)。更多关于Python的面向对象知识:https://docs.python.org/3/tutorial/classes.html
当我们编写程序或构建软件时,有两个关键组件,代码和数据。通过面向对象编程,我们将程序设计和结构定位于对象。对象是使用类在代码中定义的。类是定义对象的规范或规范,该规范指定类的每个对象应具有的数据和代码。
当我们创建一个类的对象时,我们称该对象为该类的实例,给定类的所有实例都有两个核心组件:
class Lizard: #class declaration
def __init__(self, name): #class constructor (code)
self.name = name #attribute (data)
def set_name(self, name): #method declaration (code)
self.name = name #method implementation (code)
第一行声明类并指定类名,在本例中为testCNN。
第二行定义了一个称为类构造函数的特殊方法。在创建类的新实例时,会调用类构造函数。参数有self和name。
test = testCNN('deep')
print(test.name)#deep
test.set_name('CNN')
print(test.name)#CNN
我们通过指定类名(testCNN)并传递构造函数参数(deep)来创建类的对象实例。构造函数将接收这些参数,并且构造函数代码将运行以保存传递的名称。
然后我们可以访问并打印名称,还可以调用set_name()方法来更改名称。一个程序中可以存在多个testCNN实例,每个实例都包含自己的数据。
从面向对象的角度来看,这个设置的重要部分是属性和方法被组织并包含在一个对象中。
现在让我们切换一下,看看面向对象编程如何与Py Torch相适应。
在构建神经网络的时候,我们通常会利用PyTorch中的torch.nn软件包,里面包含了构建网络的基本组件,导入方法如下:
import torch.nn as nn
构建神经网络最主要的组件就是层,在PyTorch的神经网络库包含帮助我们构建层的类。nn.Module类
深层神经网络是使用多层结构构建的。这就是网络深度的原因。神经网络中的每一层都有两个主要组成部分:
前向传播:当我们把一个张量作为输入传递给我们的网络时,张量通过每一层变换向前流动,直到张量到达输出层。张量通过网络向前流动的过程称为向前传递。
每一层都有自己的transformation(代码),张量向前通过每一层。每一层中的转换函数定义了整个网络的整体前向传播过程。
前向传播的目标是将输入转换或映射到正确的预测输出类,并且在训练过程中,各层权重(数据)的更新方式会导致映射进行调整,以使输出更接近正确的预测。
这意味着在PyTorch的nn.Module类中都有一个forward()方法,所以当我们构建层和网络时,我们必须提供forward()方法的实现。前向传播的过程实际就是这种数据变换的过程。
当我们实现nn.Module子类的forward()方法时,我们通常使用nn.functional中的函数,这个软件包为我们提供了许多可以用于构建层的神经网络操作。事实上,许多nn.Module类使用nn.functional中函数执行其操作的功能。
nn.functional包中包含nn.Module 类的方法用于实现其forward()函数。
建立神经网络的步骤:
class Network:
def __init__(self):
self.layer = None
def forward(self, t):
t = self.layer(t)
return t
这为我们提供了一个简单的Network类,它在构造函数中有一个虚拟层,并为forward函数提供了一个虚拟实现。forward()函数的实现采用张量t,并使用虚拟层对其进行变换。转换张量后,返回新的张量。
但是该类尚未extend nn.Module类。让Network类extend到 nn.Module类中,我们还必须做两件事:
class Network(nn.Module): # line 1
def __init__(self):
super().__init__() # line 3
self.layer = None
def forward(self, t):
t = self.layer(t)
return t
上面的 Network类由于extend了nn.Module类,因此具有nn.Module的所有功能。
目前,我们的网络类有一个单独的虚拟层作为属性。现在让我们用PyTorch的nn库中为我们预先构建的一些真实层来替换它。我们正在构建一个CNN,所以我们将使用两种类型的层,即线性层和卷积层。
class Network(nn.Module):
def __init__(self):
super().__init__() #父类的构造函数
#两个卷积层
self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5)
self.conv2 = nn.Conv2d(in_channels=6, out_channels=12, kernel_size=5)
#两个全连接层
self.fc1 = nn.Linear(in_features=12 * 4 * 4, out_features=120)
self.fc2 = nn.Linear(in_features=120, out_features=60)
#输出层
self.out = nn.Linear(in_features=60, out_features=10)
def forward(self, t):
# implement the forward pass
return t
现在,我们有一个名为Network的Python类,它extend了PyTorch的nn.Module类。在我们的网络类中,我们有五个定义为属性的层。我们有两个卷积层self.conv1和self.conv2和三个线性层self.fc1,self.fc2,self.out.
我们在fc1和fc2中使用缩写fc,因为线性层也称为全连接层。它们还有第三个名字,我们有时可能会听到它叫“稠密”。所以线性、密集和全连接都是指同一类型的层。PyTorch使用单词linear,因此使用nn.linear类名来进行实现。out作为输出层。
在上文中,我们定义了2个卷积层和3个线性层,我们的每一层都扩展了PyTorch的神经网络Module类。对于每一层,有两个主要项目封装在里面,一个正向函数定义和一个权重张量。每个层中的权重张量包含在网络在训练过程中学习时更新的权重值,这就是我们将层指定为Network类中的属性的原因。
PyTorch的Module类跟踪每层中的权重张量。执行此跟踪的代码存在于nn.Module类中,由于我们extend了神经网络模块类,我们自动继承了这个功能。继承是我们上次讨论过的面向对象概念之一。我们所要做的就是利用这个功能,在我们的网络模块中将我们的层指定为属性,模块基类将看到这一点,并将权重注册为我们网络的可学习参数。
parameter和argument这两个词有什么区别呢?
parameter在函数定义中用作占位符,而argument是传递给函数的实际值。parameter可以看作是函数内部的局部变量。
在我们的网络中,names是parameter,指定的值(value)是argument。
Convolutional layers
in_channels
out_channels
kernel_size
Linear layers
in_features
out_features
一般来说,超参数是指其值是手动和任意选择的参数。作为神经网络程序员,我们选择超参数值主要基于尝试和错误,并越来越多地利用过去证明有效的值。为了构建CNN层,这些是我们手动选择的参数。这意味着我们只需选择这些参数的值。在神经网络编程中,这是很常见的,我们通常测试和调整这些参数,以找到最有效的值。
在深度学习中经常交替使用filter和kernel这两个词。然而,这两个概念在技术上有区别。
Kernel是2D张量(单个Kernel),Filter是包含内核集合的3D张量(包含多少个Kernel)。我们对单个通道应用内核,对多个通道应用过滤器。
self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5)
self.conv2 = nn.Conv2d(in_channels=6, out_channels=12, kernel_size=5)
self.fc1 = nn.Linear(in_features=12 * 4 * 4, out_features=120)
self.fc2 = nn.Linear(in_features=120, out_features=60)
self.out = nn.Linear(in_features=60, out_features=10)
可学习参数是在训练过程中学习其值的参数。对于可学习的参数,我们通常从一组任意值开始,然后随着网络学习,这些值以迭代方式更新。事实上,当我们说一个网络正在学习时,我们的具体意思是网络正在学习可学习参数的适当值。适当的值是使损失函数最小化的值。
那这些可学习的参数在哪里?
这些可学习参数是网络的权重,存在于网络中的每一层。
在上文中定义的Network类,我们可以实例化:
network = Network()
实例化Network类的对象时,我们需要键入类名,后跟括号。当执行此代码时,会执行__init__构造函数中的代码,将我们的层指定为属性,然后返回。因此我们就可以通过Network类的实例变量访问该对象。
可以通过print函数打印出网络的结构。因为Network继承了nn.Module类,里面有方法实现了这个打印功能(类似tostring功能)。如果不继承nn.Module,打印出来的结果就不是这个
print(network)
# Network(
# (conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
# (conv2): Conv2d(6, 12, kernel_size=(5, 5), stride=(1, 1))
# (fc1): Linear(in_features=192, out_features=120, bias=True)
# (fc2): Linear(in_features=120, out_features=60, bias=True)
# (out): Linear(in_features=60, out_features=10, bias=True)
# )
所有Python类都会自动继承Object类。如果我们想为我们的对象提供自定义字符串表示,我们可以这样做,但我们需要引入另一个面向对象的概念,称为覆盖(override)。当我们继承一个类时,我们得到了它的所有功能,为了补充这一点,我们可以添加额外的功能。然而,我们也可以通过改变现有功能来改变其行为,从而覆盖现有功能。重写__repr__方法即可。
def __repr__(self):
return "structure"
在OOP编程时,有构造函数__init__方法,以及它是如何构造对象的一种特殊Python方法。我们还会遇到其他一些特殊的方法,repr;就是其中之一。所有特殊的OOP Python方法通常都有双下划线的前置和后置.
和很多的编程语言类似,python中也是通过点(.)来获得对象的属性方法。
下面时通过network对象访问网络中的每一层。
network.conv1
# Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
network.conv2
# Conv2d(6, 12, kernel_size=(5, 5), stride=(1, 1))
network.fc1
# Linear(in_features=192, out_features=120, bias=True)
network.fc2
# Linear(in_features=120, out_features=60, bias=True)
network.out
# Linear(in_features=60, out_features=10, bias=True)
得到每一层之后可以访问每一层中的权重
network.conv1.weight
# Parameter containing:
# tensor([[[[ 0.0692, 0.1029, -0.1793, 0.0495, 0.0619],
# [ 0.1860, 0.0503, -0.1270, -0.1240, -0.0872],
# [-0.1924, -0.0684, -0.0028, 0.1031, -0.1053],
# [-0.0607, 0.1332, 0.0191, 0.1069, -0.0977],
# [ 0.0095, -0.1570, 0.1730, 0.0674, -0.1589]]],
#
# [[[-0.1392, 0.1141, -0.0658, 0.1015, 0.0060],
# [-0.0519, 0.0341, 0.1161, 0.1492, -0.0370],
# [ 0.1077, 0.1146, 0.0707, 0.0927, 0.0192],
# [-0.0656, 0.0929, -0.1735, 0.1019, -0.0546],
# [ 0.0647, -0.0521, -0.0687, 0.1053, -0.0613]]],
#
# [[[-0.1066, -0.0885, 0.1483, -0.0563, 0.0517],
# [ 0.0266, 0.0752, -0.1901, -0.0931, -0.0657],
# [ 0.0502, -0.0652, 0.0523, -0.0789, -0.0471],
# [-0.0800, 0.1297, -0.0205, 0.0450, -0.1029],
# [-0.1542, 0.1634, -0.0448, 0.0998, -0.1385]]],
#
# [[[-0.0943, 0.0256, 0.1632, -0.0361, -0.0557],
# [ 0.1083, -0.1647, 0.0846, -0.0163, 0.0068],
# [-0.1241, 0.1761, 0.1914, 0.1492, 0.1270],
# [ 0.1583, 0.0905, 0.1406, 0.1439, 0.1804],
# [-0.1651, 0.1374, 0.0018, 0.0846, -0.1203]]],
#
# [[[ 0.1786, -0.0800, -0.0995, 0.1690, -0.0529],
# [ 0.0685, 0.1399, 0.0270, 0.1684, 0.1544],
# [ 0.1581, -0.0099, -0.0796, 0.0823, -0.1598],
# [ 0.1534, -0.1373, -0.0740, -0.0897, 0.1325],
# [ 0.1487, -0.0583, -0.0900, 0.1606, 0.0140]]],
#
# [[[ 0.0919, 0.0575, 0.0830, -0.1042, -0.1347],
# [-0.1615, 0.0451, 0.1563, -0.0577, -0.1096],
# [-0.0667, -0.1979, 0.0458, 0.1971, -0.1380],
# [-0.1279, 0.1753, -0.1063, 0.1230, -0.0475],
# [-0.0608, -0.0046, -0.0043, -0.1543, 0.1919]]]],
# requires_grad=True
# )
torch.Size([6, 1, 5, 5])
输出是一个张量,表示第一层卷积层的权值,当进行网络训练时,这些权重值会以这样一种方式更新,即损失函数最小化。
为了跟踪网络中的所有权重张量。PyTorch有一个名为Parameter的特殊类。Parameter类继承了tensor类,因此每个层中的权重张量都是这个参数类的一个实例。这就是为什么我们在字符串表示输出的顶部看到Parameter containing:。
我们可以在Pytorch源代码中看到,Parameter类通过将Parameter containing:前置到常规张量类表示输出,来重写__repr__;函数。
def __repr__(self):
return 'Parameter containing:\n' + super(Parameter, self).__repr__()
PyTorch的nn.Module类基本上是寻找任何属性,这些属性的值是Parameter类的实例,当它找到Parameter类的实例时,它会跟踪它。
所有这些都是在幕后进行的技术细节。
这些权重的shape是怎么得到的呢?
卷积层的权重
对于卷积层,权重值存在于过滤器内部,在代码中,过滤器实际上是权重张量本身。层内的卷积运算是层内输入通道和滤波器之间的运算。这意味着我们真正拥有的是两个张量之间的运算。
对于第一个conv层,我们有一个颜色通道,应该由6个5x5大小的过滤器进行卷积,以产生6个输出通道。这就是我们如何解释层构造函数中的值。但在我们的层中,我们并没有明确地为6个过滤器中的每一个设置6个权重张量。实际上,我们用一个权重张量来表示所有6个过滤器。
第一个卷积层的权重张量的形状[6, 1, 5, 5]表明我们有一个秩4的权重张量。第一个轴的长度为6,这是6个过滤器的原因。表示有6个输出通道,1表示单个输入通道,后面的[5,5]表示的是卷积核大小。
第二个conv层有12个滤波器,因此有12个输出通道,而不是卷积单个输入通道,而是来自前一层的6个输入通道。
network.conv2.weight.shape
#torch.Size([12, 6, 5, 5])
(Number of filters, Depth, Height, Width)
全连接层的权重
对于全连接层,我们将秩为1的张量作为输入和输出。我们将全连接层中的in_features转换为out_features的方法是使用秩2张量,通常称为权重矩阵。这是因为权重张量的秩为2,具有高度和宽度轴。
network.fc1.weight.shape
#torch.Size([120,192]) = = > torch.Size([out_features,in_features])
全连接层的权重矩阵就是通常情况下的y=Ax+B的形式,A为权重矩阵,shape=[out_features,in_features]。
linear函数的源码如下:
# torch/nn/modules/linear.py (version 1.0.1)
def __init__(self, in_features, out_features, bias=True):
super(Linear, self).__init__()
self.in_features = in_features
self.out_features = out_features
self.weight = Parameter(torch.Tensor(out_features, in_features))
if bias:
self.bias = Parameter(torch.Tensor(out_features))
else:
self.register_parameter('bias', None)
self.reset_parameters()
下面是一段代码,通过输入in_features来调用linear层。我们可以这样调用对象实例,因为神经网络模块是可调用的Python对象。我们确实得到了一个包含三个元素的一维张量。然而,产生了不同的价值观。这是因为PyTorch创建了一个权重矩阵,并使用随机值对其进行初始化。这意味着两个例子中的线性函数是不同的,所以我们使用不同的函数来产生这些输出。
in_features = torch.tensor([1,2,3,4], dtype=torch.float32)
weight_matrix = torch.tensor([
[1,2,3,4],
[2,3,4,5],
[3,4,5,6]
], dtype=torch.float32)
weight_matrix.matmul(in_features)
#tensor([30., 40., 50.])
fc = nn.Linear(in_features=4, out_features=3, bias=False)
fc(in_features) #tensor([1.3524, 4.0868, 0.1020], grad_fn=)
下面这种形式就是属于可调用层,可以进行调用的原因就是在PyTorch的模块类中实现了一种特殊的函数__call__(),如果某个类实现了__call__()方法,那么在调用对象实例的时候就会调用这个方法。
fc(in_features)
我们不直接调用forward()方法,而是调用对象实例。调用对象实例后,在引擎盖下调用__call__()方法,call()反过来调用forward()方法。这适用于所有神经网络模块,即网络和层。在PyTorch源代码中看到这一点。
# torch/nn/modules/module.py (version 1.0.1)
def __call__(self, *input, **kwargs):
for hook in self._forward_pre_hooks.values():
hook(self, input)
if torch._C._get_tracing_state():
result = self._slow_forward(*input, **kwargs)
else:
result = self.forward(*input, **kwargs)
for hook in self._forward_hooks.values():
hook_result = hook(self, input, result)
if hook_result is not None:
raise RuntimeError(
"forward hooks should never return any values, but '{}'"
"didn't return None".format(hook))
if len(self._backward_hooks) > 0:
var = result
while not isinstance(var, torch.Tensor):
if isinstance(var, dict):
var = next((v for v in var.values() if isinstance(v, torch.Tensor)))
else:
var = var[0]
grad_fn = var.grad_fn
if grad_fn is not None:
for hook in self._backward_hooks.values():
wrapper = functools.partial(hook, self)
functools.update_wrapper(wrapper, hook)
grad_fn.register_hook(wrapper)
return result
模型构建的一般步骤如下:现在需要进行的就是建立模型阶段的第三个阶段
class Network(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5)
self.conv2 = nn.Conv2d(in_channels=6, out_channels=12, kernel_size=5)
self.fc1 = nn.Linear(in_features=12 * 4 * 4, out_features=120)
self.fc2 = nn.Linear(in_features=120, out_features=60)
self.out = nn.Linear(in_features=60, out_features=10)
def forward(self, t):
# implement the forward pass
return t
任何神经网络的输入层都由输入数据决定。例如,如果我们的输入张量包含三个元素,那么我们的网络将在其输入层中包含三个节点。
因此,我们可以将输入层视为身份转换。从数学上来说,这就是函数f(x)=x.
可以通过下面的方式显示标识,但是其实是没有必要的。
# (0) input layer
t = t
这两个隐藏的卷积层在执行转换方面非常相似。在深度学习中,所有不是输入或输出层的层都被称为隐藏层,这就是为什么我们将这些卷积层称为隐藏层。
为了进行卷积运算,我们将张量传递给第一个卷积层的forward函数,self.conv1。我们已经了解了所有的PyTorch神经网络模块如何具有forward()方法,以及何时调用nn.Mudule中forward()方法,我们有一种特殊的调用方式。
当你想调用nn.Mudule中forward()方法时,我们调用实际实例,而不是直接调用forward()方法。
不是self.conv1.forward(tensor),而是self.conv1(tensor)。这就是前文中所讲到的可调用层。让我们继续添加实现这两个卷积层所需的所有调用。
# (1) hidden conv layer
t = self.conv1(t)
t = F.relu(t)
t = F.max_pool2d(t, kernel_size=2, stride=2)
# (2) hidden conv layer
t = self.conv2(t)
t = F.relu(t)
t = F.max_pool2d(t, kernel_size=2, stride=2)
当我们在卷积层中移动时,我们的输入张量t被转换。第一个卷积层有一个卷积操作,然后是一个relu激活操作,其输出随后被传递给一个最大池操作, kernel_size_size=2,stride=2。
然后,第一个卷积层的输出张量t被传递到下一个卷积层,结构与第一个卷积层相同。
这些层中的每一层都由一组权重(数据)和一组收集操作(代码)组成。权重封装在nn.Conv2d()类的实例中。relu()和max_pool2d()调用只是纯粹的操作。它们都没有权重,这就是为什么我们直接从nn.functional中调用API。
有时我们可能会看到称为池化层的池操作。有时我们甚至会听到被称为激活层的激活操作。然而,层与操作的区别在于层具有权重。由于池操作和激活函数没有权重,我们将它们称为操作,并将它们视为添加到层操作集合中。
例如,我们将说网络中的第一层是一个卷积层,它包含一组权重,并执行三个操作,一个卷积操作、relu激活操作和最大池操作。
请注意,这里的规则和术语并不严格。这只是描述网络的一种方式。还有其他方式来表达这些想法。我们需要注意的主要问题是,哪些操作是使用权重定义的,哪些操作不使用任何权重。一般来说,使用权重定义的操作就是我们所说的层。后来,其他操作被添加到混合中,比如激活函数和池操作,这在术语上造成了一些混乱。从数学上讲,整个网络只是函数的组合,而函数的组合就是函数本身。所以网络只是一个函数。所有的术语,如层、激活函数和权重,都只是用来帮助描述不同的部分。
在我们将输入传递到第一个隐藏的线性层之前,我们必须reshape()或flatten张量。当我们把卷积层的输出作为输入传递给线性层时,情况就是这样。由于第3层是第一个线性层,我们将把reshape操作作为第3层的一部分。
# (3) hidden linear layer
t = t.reshape(-1, 12 * 4 * 4)
t = self.fc1(t)
t = F.relu(t)
# (4) hidden linear layer
t = self.fc2(t)
t = F.relu(t)
我们在前文中可以知道,reshape操作中的数字12由来自前一个卷积层的输出通道数决定。然而,44是一个没有解释的问题。现在让我们来揭示答案。44实际上是12个输出通道中每个通道的高度和宽度。
我们从一个1×28×28的输入张量开始。这给出了一个单色通道,28 x 28的图像,当我们的张量到达第一个线性层时,尺寸已经改变了。通过卷积和池运算,高度和宽度尺寸已从28 x 28减小到4 x 4。卷积和合并操作是高度和宽度维度上的缩减操作。其中原理就是卷积操作之后特征矩阵的一个变化公式。在张量被reshape之后,我们将张量传递给线性层,并将结果传递给relu()激活函数。
网络的第五层也是最后一层是线性层,我们称之为输出层。当我们把张量传递到输出层时,结果就是预测张量。由于我们的数据有十个预测类,我们知道我们的输出张量将有十个元素。
# (6) output layer
t = self.out(t)
#t = F.softmax(t, dim=1)
十个输出将对应于我们每个预测类的预测值。在网络内部,我们通常使用relu()作为我们的非线性激活函数,但对于输出层,每当我们试图预测一个类别时,我们就使用softmax()。softmax函数为每个预测类返回正概率,概率总和为1。
然而,在我们的例子中,我们不会使用softmax(),因为我们将使用的损失函数F.cross_entropy()会隐式地对其输入执行softmax()操作,所以我们只返回上一次线性变换的结果。
最后,将上面的各层放在forward()函数中。
def forward(self, t):
# (1) input layer
t = t
# (2) hidden conv layer
t = self.conv1(t)
t = F.relu(t)
t = F.max_pool2d(t, kernel_size=2, stride=2)
# (3) hidden conv layer
t = self.conv2(t)
t = F.relu(t)
t = F.max_pool2d(t, kernel_size=2, stride=2)
# (4) hidden linear layer
t = t.reshape(-1, 12 * 4 * 4)
t = self.fc1(t)
t = F.relu(t)
# (5) hidden linear layer
t = self.fc2(t)
t = F.relu(t)
# (6) output layer
t = self.out(t)
#t = F.softmax(t, dim=1)
return t
为了更好的理解前向传播的整个过程,理解深度神经网络的实现,在进入训练之前,需要对前向传播中的部分细节进行解释。
什么是前向传播?
正向传播是将输入张量转换为输出张量的过程。神经网络的核心是将输入张量映射到输出张量的函数,而前向传播只是将输入传递到网络并从网络接收输出的过程的一个特殊名称。
正如我们所见,神经网络以张量的形式对数据进行操作。前向传播的概念用于表示输入张量数据通过网络向前传输。这就是为什么forward()方法名为forward,forward()的执行是向前传播的过程。如果您正在关注这个系列,我们现在知道,我们不直接调用forward()方法,而是调用网络实例(前文中讲到的可调用层)。还有一种反向传播(backpropagation)的概念,在训练过程中,反向传播发生在正向传播之后。
在我们的例子中,从实际的角度来看,前向传播是将输入图像张量传递给我们在上一集中实现的forward()方法的过程。这个输出是网络的预测。在关于数据集和数据加载器的一集中,我们看到了如何从我们的训练集中访问单个样本图像张量,更重要的是,如何从数据加载器访问一批图像张量。现在我们已经定义了网络并实现了forward()方法,将图像传递到网络以获得预测。
下面的代码可以控制是否启动梯度更新的功能,false表示不启动,true表示启动,在正式训练的时候,是需要开启的,因为训练过程需要进行梯度的更新。
torch.set_grad_enabled(False)
下面让我们构建一个简单的预测过程,将一张图片传入神经网络进行预测。
sample = next(iter(train_set))
image, label = sample
image.shape
#torch.Size([1, 28, 28])
由train_set得到的单个图像是[1, 28, 28],但是神经网络的输入需要(batch_size, in_channels, height, width)形式的输入,所以要对单张输入图像进行unsqueeze(),得到[1, 1, 28, 28],将其输入神经网络中进行预测。
# Inserts an additional dimension that represents a batch of size 1
image.unsqueeze(0).shape
#torch.Size([1, 1, 28, 28])
完整的预测代码如下:
network = Network()
sample = next(iter(train_set))
image, label = sample
pred = network(image.unsqueeze(0))
# image shape needs to be (batch_size × in_channels × H × W)
pred
#tensor([[0.0991, 0.0916, 0.0907, 0.0949, 0.1013, 0.0922, 0.0990, 0.1130, 0.1107, 0.1074]])表示每个类别的预测值
label
#9 表示该图片的真实标签值
pred.argmax(dim=1) #查看预测类别中概率最大的下标即为预测的类别,为7
从上面的过程可以看出,最后的预测tensor是([1, 10]),表示有一张图片和10个预测类别,即(batch size, number of prediction classes),对于批次中的每个输入,以及每个预测类,我们都有一个预测值。如果我们想让这些值成为概率,我们可以只使用nn.functional中的softmax()函数即可
F.softmax(pred, dim=1)
#tensor([[0.1096, 0.1018, 0.0867, 0.0936, 0.1102, 0.0929, 0.1083, 0.0998, 0.0943, 0.1030]])
F.softmax(pred, dim=1).sum()
#tensor(1.) 概率值的总和为1
上面的预测结果是错误的,因为网络中的权重是随机生成的,并且没有进行迭代的更新梯度,所以预测结果不准确。
net1 = Network()
net2 = Network()
net1(image.unsqueeze(0))
#tensor([[ 0.0855, 0.1123, -0.0290, -0.1411, -0.1293, -0.0688, 0.0149, 0.1410, -0.0936, -0.1157]])
net2(image.unsqueeze(0))
#tensor([[-0.0408, -0.0696, -0.1022, -0.0316, -0.0986, -0.0123, 0.0463, 0.0248, 0.0157, -0.1251]])
前向传播过程矩阵shape的变换:
在上面的讲述中,我们实现了单个图像的预测,在本小节中,我们将一个批次传递到网络中进行预测。
准备工作和上面的一样。
最后我们将返回的图像shape是(batch size, input channels, height, width)
data_loader = torch.utils.data.DataLoader(
train_set, batch_size=10
)
batch = next(iter(data_loader))
images, labels = batch
images.shape
#torch.Size([10, 1, 28, 28])
labels.shape
#torch.Size([10])
preds = network(images)
preds.shape
#torch.Size([10, 10]) #输出的shape(batch size, number of prediction classes)
preds
tensor(
[
[ 0.1072, -0.1255, -0.0782, -0.1073, 0.1048, 0.1142, -0.0804, -0.0087, 0.0082, 0.0180],
[ 0.1070, -0.1233, -0.0798, -0.1060, 0.1065, 0.1163, -0.0689, -0.0142, 0.0085, 0.0134],
[ 0.0985, -0.1287, -0.0979, -0.1001, 0.1092, 0.1129, -0.0605, -0.0248, 0.0290, 0.0066],
[ 0.0989, -0.1295, -0.0944, -0.1054, 0.1071, 0.1146, -0.0596, -0.0249, 0.0273, 0.0059],
[ 0.1004, -0.1273, -0.0843, -0.1127, 0.1072, 0.1183, -0.0670, -0.0162, 0.0129, 0.0101],
[ 0.1036, -0.1245, -0.0842, -0.1047, 0.1097, 0.1176, -0.0682, -0.0126, 0.0128, 0.0147],
[ 0.1093, -0.1292, -0.0961, -0.1006, 0.1106, 0.1096, -0.0633, -0.0163, 0.0215, 0.0046],
[ 0.1026, -0.1204, -0.0799, -0.1060, 0.1077, 0.1207, -0.0741, -0.0124, 0.0098, 0.0202],
[ 0.0991, -0.1275, -0.0911, -0.0980, 0.1109, 0.1134, -0.0625, -0.0391, 0.0318, 0.0104],
[ 0.1007, -0.1212, -0.0918, -0.0962, 0.1168, 0.1105, -0.0719, -0.0265, 0.0207, 0.0157]
]
)
预测结果的形状是10乘10,这给了我们两个轴,每个轴的长度是10。这反映了这样一个事实:我们有十幅图像,对于这十幅图像中的每一幅,我们有十个预测类。
第一维度的元素是长度为10的数组。这些数组元素中的每一个都包含对应图像的每个类别的十个预测。
第二维度的元素是数字。每个数字都是特定输出类的赋值。输出类由索引编码,因此每个索引代表一个特定的输出类。最大的那个索引就是预测的类别。
我们使用argmax()函数找出哪个索引包含最高的预测值。一旦我们知道哪个索引具有最高的预测值,我们就可以将索引与标签进行比较,看看是否存在匹配。为此,我们对预测张量调用argmax()函数,并指定第二维度。
preds.argmax(dim=1)
#tensor([5, 5, 5, 5, 5, 5, 4, 5, 5, 4])
labels
#tensor([9, 0, 0, 3, 0, 2, 7, 2, 5, 5])
#查看预测结果是否匹配
preds.argmax(dim=1).eq(labels)
# tensor(
# [False, False, False, False, False, False, False, False, True, False]
# )
#预测正确的个数
preds.argmax(dim=1).eq(labels).sum()
#tensor(1)
我们可以将最后一个调用封装到一个名为get_num_correct()的函数中,该函数接受预测和标签,并使用item()方法返回正确预测的Python数量。
def get_num_correct(preds, labels):
return preds.argmax(dim=1).eq(labels).sum().item()
get_num_correct(preds, labels)
#1
在完整的训练过程中需要计算损失值,梯度,更新权值。
上面针对单批次进行了实现,现在需要对每个批次重复这个过程,直到我们涵盖了培训集中的每个样本。在我们完成了所有批次的这个过程,并通过了训练集中的每个样本之后,我们说一个epoch已经完成。我们用epoch这个词来表示一个时间段,在这个时间段内,我们的整个训练集都被覆盖了。
在整个训练过程中,我们需要以下的一些步骤:
在正式的训练过程中,我们要开启梯度跟踪功能,(默认情况下是打开的)。
torch.set_grad_enabled(True)
训练过程
cross_entropy()函数返回一个标量值,因此我们使用item()方法将损失打印为Python数字。我们100个预测中有9个是正确的,因为我们有10个预测类,这是我们随机猜测的结果。
network = Network()
train_loader = torch.utils.data.DataLoader(train_set, batch_size=100) #batch=100
batch = next(iter(train_loader)) # Getting a batch
images, labels = batch
preds = network(images)#预测
loss = F.cross_entropy(preds, labels) #计算损失
loss.item()#取出具体的值2.307542085647583
get_num_correct(preds, labels) #获得预测准确的个数 9
梯度计算:
使用PyTorch计算梯度非常容易。因为我们的网络是nn.Module,PyTorch创建了一个计算图。当我们的张量在网络中向前流动时,所有的计算都被添加到了图表中。然后,PyTorch使用计算图计算损失函数相对于网络权重的梯度。在计算梯度之前,让我们先确认conv1层中当前没有梯度。梯度是可以在每个层的权重张量的grad(梯度的缩写)属性中访问的张量。
network.conv1.weight.grad#None
为了计算梯度,我们对loss张量调用backward()方法,如下所示:
loss.backward() # Calculating the gradients
network.conv1.weight.grad.shape
# 可以得到每一层的梯度值:torch.Size([6, 1, 5, 5])
更新梯度
优化器使用这些梯度来更新相应的权重。为了创建优化器,我们使用torch.optim包中有许多优化算法的实现,我们可以使用。我们将以Adam为例。
我们将网络参数传递给Adam类构造函数(优化器就是这样访问梯度的),然后传递学习率。最后,我们要做的就是更新权重,告诉优化器使用梯度,朝着损失函数的最小值前进。
optimizer = optim.Adam(network.parameters(), lr=0.01)
optimizer.step() # Updating the weights
调用step()函数时,优化器使用存储在网络参数中的梯度更新权重。这意味着,如果我们再次通过网络传递同一批,我们的损失应该会减少。检查一下,我们可以看到事实确实如此
preds = network(images)
loss = F.cross_entropy(preds, labels)
loss.item()#2.262690782546997
get_num_correct(preds, labels)#15
所以,单个批次训练两次的代码如下:
network = Network()
train_loader = torch.utils.data.DataLoader(train_set, batch_size=100)
optimizer = optim.Adam(network.parameters(), lr=0.01)
batch = next(iter(train_loader)) # Get Batch
images, labels = batch
preds = network(images) # Pass Batch
loss = F.cross_entropy(preds, labels) # Calculate Loss
loss.backward() # Calculate Gradients
optimizer.step() # Update Weights
print('loss1:', loss.item())
preds = network(images)
loss = F.cross_entropy(preds, labels)
print('loss2:', loss.item())
#loss1: 2.3034827709198
#loss2: 2.2825052738189697
现在,为了使用train_loader中的所有批处理进行培训,我们需要做一些更改,并添加一行额外的代码:增加一个循环,以及记录loss和正确率的变量:下面是所有训练集上的一次完整训练(an epoch)
network = Network()
train_loader = torch.utils.data.DataLoader(train_set, batch_size=100)
optimizer = optim.Adam(network.parameters(), lr=0.01)
total_loss = 0
total_correct = 0
for batch in train_loader: # Get Batch
images, labels = batch
preds = network(images) # Pass Batch
loss = F.cross_entropy(preds, labels) # Calculate Loss
optimizer.zero_grad()
loss.backward() # Calculate Gradients
optimizer.step() # Update Weights
total_loss += loss.item()
total_correct += get_num_correct(preds, labels)
print(
"epoch:", 0,
"total_correct:", total_correct,
"loss:", total_loss
)
#epoch: 0 total_correct: 42104 loss: 476.6809593439102
total_correct / len(train_set)
#0.7017333333333333
我们将创建一个for循环,迭代所有批次,而不是从数据加载器中获取单个批次。由于我们的训练集中有60000个样本,我们将有60000/100=600次迭代。出于这个原因,我们将从循环中删除print语句,并跟踪总损失以及最终打印它们的正确预测总数。
训练多个epoch:
network = Network()
train_loader = torch.utils.data.DataLoader(train_set, batch_size=100)
optimizer = optim.Adam(network.parameters(), lr=0.01)
for epoch in range(10):
total_loss = 0
total_correct = 0
for batch in train_loader: # Get Batch
images, labels = batch
preds = network(images) # Pass Batch
loss = F.cross_entropy(preds, labels) # Calculate Loss
optimizer.zero_grad() #每各batch之后都要讲梯度清零
loss.backward() # Calculate Gradients
optimizer.step() # Update Weights
total_loss += loss.item()
total_correct += get_num_correct(preds, labels)
print(
"epoch", epoch,
"total_correct:", total_correct,
"loss:", total_loss
)
最后训练之后的结果:我们可以看到正确值的数量增加,损失减少。
epoch 0 total_correct: 43301 loss: 447.59147948026657
epoch 1 total_correct: 49565 loss: 284.43429669737816
epoch 2 total_correct: 51063 loss: 244.08825492858887
epoch 3 total_correct: 51955 loss: 220.5841210782528
epoch 4 total_correct: 52551 loss: 204.73878084123135
epoch 5 total_correct: 52914 loss: 193.1240530461073
epoch 6 total_correct: 53195 loss: 184.50964668393135
epoch 7 total_correct: 53445 loss: 177.78808392584324
epoch 8 total_correct: 53629 loss: 171.81662507355213
epoch 9 total_correct: 53819 loss: 166.2412590533495
混淆矩阵将向我们显示模型混淆的地方。更具体地说,混淆矩阵将向我们显示模型正确预测的类别以及模型错误预测的类别。对于错误的预测,我们将能够看到模型预测的类别,这将告诉我们哪些类别进行了错误的预测。
构建函数以获得所有样本的预测
我们将创建一个名为get_all_preds()的函数,并传递一个model和一个loader。model将用于获得预测,loader将用于提供训练集中的批次。该函数需要做的只是在loader上迭代,将批传递给模型,并将每个批的结果连接到一个预测张量,该预测张量将返回给调用者。
利用torch.cat将每一个批次的结果连接成一个张量。
@torch.no_grad()
def get_all_preds(model, loader):
all_preds = torch.tensor([])
for batch in loader:
images, labels = batch
preds = model(images)
all_preds = torch.cat(
(all_preds, preds)
,dim=0
)
return all_preds
这个函数内部会产生一个空的张量,所有这些张量都用来保存输出预测。然后,它迭代来自loader的batch,并将输出预测与all_preds张量连接起来。最后,所有的预测都返回给调用者。
注意,在顶部,我们使用@torch.no_grad()对函数进行了注释。这是因为我们希望这个函数的执行忽略梯度跟踪。这是因为梯度跟踪使用内存,在推理过程中(在不训练的情况下获得预测),不需要跟踪计算图。这个注解是在执行特定功能时局部关闭渐变跟踪功能的一种方式。
我们现在准备好调用获取训练集的预测。我们需要做的就是创建一个具有合理批量大小的数据加载器,并将模型和数据加载器传递给get_All_preds()函数。with torch.no_grad()表示关闭梯度跟踪。
with torch.no_grad():
prediction_loader = torch.utils.data.DataLoader(train_set, batch_size=10000)
train_preds = get_all_preds(network, prediction_loader)
现在,我们有了预测张量,我们可以将它传递给我们在上一集中创建的get_num_correct()函数,以及训练集标签,以获得正确预测的总数。
preds_correct = get_num_correct(train_preds, train_set.targets)
print('total correct:', preds_correct)
print('accuracy:', preds_correct / len(train_set))
#total correct: 53578
#accuracy: 0.8929666666666667
构造混淆矩阵
我们构建混淆矩阵的任务是根据真实值(目标)计算预测值的数量。这将创建一个矩阵,作为热图,告诉我们预测值相对于真实值的下降位置。
train_set.targets#真实值
#tensor([9, 0, 0, ..., 3, 0, 5])
train_preds.argmax(dim=1) #预测值
#tensor([9, 0, 0, ..., 3, 0, 5])
现在,如果我们在元素方面比较这两个张量,我们可以看到预测的标签是否与目标匹配。此外,如果我们计算预测标签与目标标签的数量,两个张量内的值将作为矩阵的坐标。让我们把这两个张量沿二维叠加,这样我们就可以有60000个有序对。
stacked = torch.stack(
(
train_set.targets
,train_preds.argmax(dim=1)
)
,dim=1
)
stacked.shape
#torch.Size([60000, 2]) #2表示的一列是真实值,一列是预测值
stacked
# tensor([
# [9, 9],
# [0, 0],
# [0, 0],
# ...,
# [3, 3],
# [0, 0],
# [5, 5]
# ])
stacked[0].tolist()
#[9, 9]
现在,我们可以迭代这些对,计算矩阵中每个位置的出现次数。让我们创建矩阵。因为我们有十个预测类别,我们将有一个十乘十的矩阵。
cmt = torch.zeros(10,10, dtype=torch.int64)
cmt
# tensor([
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# ])
for p in stacked:
tl, pl = p.tolist()
cmt[tl, pl] = cmt[tl, pl] + 1
cmt
# tensor([
# [5637, 3, 96, 75, 20, 10, 86, 0, 73, 0],
# [ 40, 5843, 3, 75, 16, 8, 5, 0, 10, 0],
# [ 87, 4, 4500, 70, 1069, 8, 156, 0, 106, 0],
# [ 339, 61, 19, 5269, 203, 10, 72, 2, 25, 0],
# [ 23, 9, 263, 209, 5217, 2, 238, 0, 39, 0],
# [ 0, 0, 0, 1, 0, 5604, 0, 333, 13, 49],
# [1827, 7, 716, 104, 792, 3, 2370, 0, 181, 0],
# [ 0, 0, 0, 0, 0, 22, 0, 5867, 4, 107],
# [ 32, 1, 13, 15, 19, 5, 17, 11, 5887, 0],
# [ 0, 0, 0, 0, 0, 28, 0, 234, 6, 5732]
# ])
混淆矩阵的绘制
将实际的混淆矩阵生成为numpy.ndarray,我们使用sklearn.metrics中的confusion_matrix()函数
import matplotlib.pyplot as plt
from sklearn.metrics import confusion_matrix
from resources.plotcm import plot_confusion_matrix
对于from resources.plotcm import plot_confusion_matrix导入,请注意plotcm是一个名为plotcm.py的文件位于当前目录中名为resources的文件夹中。在plotcm.py文件中,我们将调用一个名为plot_confusion_matrix()的函数。
我们可以这样生成混淆矩阵:
cm = confusion_matrix(train_set.targets, train_preds.argmax(dim=1))
print(type(cm))
cm
#和上面的混淆矩阵不同,由于在不同的时间运行的
#
# array([[5431, 14, 88, 145, 26, 7, 241, 0, 48, 0],
# [ 4, 5896, 6, 75, 8, 0, 8, 0, 3, 0],
# [ 92, 6, 5002, 76, 565, 1, 232, 1, 25, 0],
# [ 191, 49, 23, 5504, 162, 1, 61, 0, 7, 2],
# [ 15, 12, 267, 213, 5305, 1, 168, 0, 19, 0],
# [ 0, 0, 0, 0, 0, 5847, 0, 112, 3, 38],
# [1159, 16, 523, 189, 676, 0, 3396, 0, 41, 0],
# [ 0, 0, 0, 0, 0, 99, 0, 5540, 0, 361],
# [ 28, 6, 29, 15, 32, 23, 26, 14, 5827, 0],
# [ 0, 0, 0, 0, 1, 61, 0, 107, 1, 5830]],
# dtype=int64)
plt.figure(figsize=(10,10))
plot_confusion_matrix(cm, train_set.classes)
# Confusion matrix, without normalization
# [[5431 14 88 145 26 7 241 0 48 0]
# [ 4 5896 6 75 8 0 8 0 3 0]
# [ 92 6 5002 76 565 1 232 1 25 0]
# [ 191 49 23 5504 162 1 61 0 7 2]
# [ 15 12 267 213 5305 1 168 0 19 0]
# [ 0 0 0 0 0 5847 0 112 3 38]
# [1159 16 523 189 676 0 3396 0 41 0]
# [ 0 0 0 0 0 99 0 5540 0 361]
# [ 28 6 29 15 32 23 26 14 5827 0]
# [ 0 0 0 0 1 61 0 107 1 5830]]