深度学习项目的步骤或者过程:
准备数据
提取——从数据源获取Fashion-MNIST图像数据;
转换——把数据转换成张量的形式;
加载——把我们的数据放入一个对象,使它容易访问。
构建模型
训练模型
分析模型结果
从数据源中提取数据
将数据转换为所需的格式
将数据加载到适当的结构中
ETL过程可以被认为是一个分形过程( fractal process),因为它可以应用于各种规模。该流程可以小规模应用,比如单个程序,也可以大规模应用,一直到企业级别,在企业级别有处理每个单独部分的大型系统。
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
包描述:
Packge | Description |
---|---|
torch | 顶级的pytorch包和张量库 |
torch.nn | 包含用于构建神经网络的模块和可扩展类的子包 |
torch.optim | 包含标准优化操作(如SGD和Adam)的子包 |
torch.nn.functional | 一个函数接口,包含用于构建神经网络的典型操作,如loss函数和卷积 |
torchvision | 为计算机视觉提供对流行数据集、模型体系结构和图像转换访问的包 |
torchvision.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
import pdb
import os
torch.set_printoptions(linewidth=120)
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
pdb是Python调试器,注释的部分是一个本地文件,以后会用到,最后一行是打印的设置选项
准备数据时的最终目标是做以下(ETL):
提取——从数据源获取Fashion-MNIST图像数据。
转换——把数据转换成张量的形式。
加载——把我们的数据放入一个对象,使它容易访问。
出于这些目的,PyTorch为我们提供了两个类:
Class | Desciption |
---|---|
torch.utils.data.Dataset | 表示数据集的抽象类 |
torch.utils.data.DataLoader | 包装数据集并提供对底层数据的访问 |
抽象类是一个python类,它有我们必须实现的方法,所以我们可以通过创建一个子类来扩充dataset类的功能类创建一个自定义数据集
为了使用PyTorch创建自定义数据集,我们通过创建实现这些所需方法的子类来扩展dataset。这样做之后,我们的新子类就可以传递给一个PyTorch DataLoader对象。
我们将使用内置在torchvision包中的fashion-MNIST数据集,因此我们的项目不需要这样做。只需知道Fashion-MNIST内建的dataset类正在幕后做这件事。
torchvision软件包使我们可以访问以下资源:
Datasets (like MNIST and Fashion-MNIST)
Models (like VGG16)
Transforms
Utils
作为MNIST(手写数字图片集)的补充,把时装分为了10类,70000条数据,其中包含60000条训练样本和10000条测试样本。
要使用torchvision获取FashionMNIST数据集的实例,我们只需创建一个类似的实例:
train_set = torchvision.datasets.FashionMNIST(
root = "./data",
train=True,
download=True,
transform=transforms.Compose([
transforms.ToTensor()])
)
我们需要指定以下参数:
Parameter | Description |
---|---|
root | 磁盘上数据所在的位置 |
train | 数据集是否为训练集 |
download | 数据集是否要下载 |
transform | 应该在数据集元素上执行的转换组合 |
因为我们希望我们的图像被转换成张量,所以我们使用了内置的transform . totensor() 转换,并且由于这个数据集将用于训练,我们将把实例命名为train_set。
当我们第一次运行这段代码时,Fashion-MNIST数据集将在本地下载。随后的调用在下载之前检查数据。因此,我们不必担心重复下载或重复网络调用。
外网抽风下载不了,这里我从本地加载。 把root改为本地数据集所在目录即可,注意:不要加FashionMNIST这个目录
train_set = torchvision.datasets.FashionMNIST(
root = "./data",
train=True,
download=False,
transform=transforms.ToTensor()
)
为训练数据集创建一个DataLoader包装器。
train_loader = torch.utils.data.DataLoader(
train_set,
batch_size=1000,
shuffle=True
)
我们只是传递train_set作为参数。现在,我们可以利用加载器的完成这个任务,否则手动实现相当复杂:
batch_size 批次的大小
shuffle 是否将数据打乱
num_workers 默认为0,表示将使用主线程
从ETL的角度来看,我们在创建数据集时,已经实现了使用torchvision进行提取和转换:
Extract —从web中提取原始数据。
Transform——原始图像数据变换成一个张量。
Load——train_set被(加载到)数据加载器包装,使我们能够访问底层数据。
现在,我们应该很好地理解了PyTorch提供的torchvision 模块,以及如何在PyTorch的 torch.utils.data 使用Datasets 和 DataLoaders 来简化ETL任务。
查看训练集有多少张图像
len(train_set)
60000
查看图片的标签
train_set.train_labels # Before torchvision 0.2.2
tensor([9, 0, 0, ..., 3, 0, 5])
train_set.targets # Starting with torchvision 0.2.2
tensor([9, 0, 0, ..., 3, 0, 5])
值的编码代表类名或者标签,比如9是短靴,而0是t恤。使用bicount函数查看每个标签的数量:
train_set.targets.bincount()
tensor([6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000])
「Class Imbalance: Balanced And Unbalanced Datasets」 由上面的输出可以看出,Fashion-MNIST数据集在每个类的样本数量是一致的,每个类别都有6000个样本。因此这个数据集是平衡的。如果类具有不同数量的样本,则该集合称为不平衡数据集。关于在深度学习中减轻不平衡数据集的方法,请看这篇论文:卷积神经网络中的类不平衡问题的系统研究。
要访问训练集中的单个元素,需要将train_set对象传递给python内置的iter()函数,该函数返回数据流对象。
对于数据流,通过使用next()函数可以获取数据流的下一个数据元素。如果不能理解请去了解python的迭代器。
sample = next(iter(train_set))
len(sample)
2
求长度输出为2表示样例中包含两个样本,即<图像, 标签>的值对,训练集中每个样本都包含一个张量的图像数据和相应的张量标签。
由于样本是一个序列类型(sequence type),因此可以使用序列解压(sequence unpacking)来分配图像和标签,参考序列封包与序列解包。下面将查看图像的类型和标签:
type(sample[0])
torch.Tensor
type(sample[1])
int
sample[0].shape
torch.Size([1, 28, 28])
由上面的输出可以推断,sample[0]是形状为[1, 28, 28]的张量,是图片。sample[1]是标签值。
其中1代表的是通道数,比如RGB就有3个颜色通道,但是在Fashion-MNIST数据集中,得到的图像是经过:
Converted to PNG (转换为PNG)
Trimmed (裁剪)
Resized (调整大小)
Sharpened (锐化)
Extended (拓展)
Negated (取反)
Gray-scaled (灰度化)
的图片,在这次的实验中我们希望看到的是 28 × 28 28 \times 28 28×28形状,因此需要进行维度挤压操作。下面进行维度挤压和显示图片。
plt.imshow(sample[0].squeeze(dim=0), cmap="gray")
torch.tensor(sample[1])
tensor(9)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IYQIPcxF-1619794781647)(output_41_1.png)]
创建一个新的数据加载器,批处理大小为10,来演示处理流程。
display_loader = torch.utils.data.DataLoader(
train_set,
batch_size=10
)
每次loader都会获得一个大小为10的batch,对于每个元素使用iter()和next()函数访问。 如果shuffle = True,则在第一次调用next时将返回训练集中的第一个样本。
注意:如果shuffle=True,则每次调用next时都不同,默认情况下处于关闭状态。
# 当shuffle=True时,每个batch都不同
batch = next(iter(display_loader))
print('element len:', len(batch), 'batch len:', len(display_loader))
element len: 2 batch len: 6000
检查返回batch的长度,就像训练集一样,我们得到2。让我们拆开每个batch,看看两个张量及其形状.
images, labels = batch # 序列解包
print('types:', type(images), type(labels))
print("shapes:", images.shape, labels.shape)
types:
shapes: torch.Size([10, 1, 28, 28]) torch.Size([10])
由于batch_size = 10,我们知道我们正在处理一批10张图像和10个相应的标签。这就是为什么我们对变量名使用复数形式的原因。
类型是我们期望的张量。但是,形状与我们在单个样品中看到的形状不同。我们没有一个标量值作为标签,而是有一个带有10个值的一阶张量。张量中包含图像数据的每个维度的大小由以下每个值定义:
批量大小为10,这就是为什么现在张量的第一个尺寸为10,每个图像是一个索引。下面是第一个值。
plt.imshow(images[0].squeeze())
print(images[0].shape, labels[0])
torch.Size([1, 28, 28]) tensor(9)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L8nEv0N7-1619794781648)(output_49_1.png)]
如果要绘制一批图像,可以使用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)))
print("labels: ", labels)
labels: tensor([9, 0, 0, 3, 0, 2, 7, 2, 5, 5])
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HIcOjezH-1619794781649)(output_51_1.png)]
上面的np.transpose(grid, (1,2,0)可以换成pytorch里的permute方法。permute函数参考Pytorch之permute函数
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])
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8Hg2zxOR-1619794781650)(output_53_1.png)]
各个标签的类名如下:
Index | Label |
---|---|
0 | T-shirt/top |
1 | Trouser |
2 | Pullover |
3 | Dress |
4 | Coat |
5 | Sandal |
6 | Shirt |
7 | Sneaker |
8 | Bag |
9 | Ankle boot |
how_many_to_plot = 20
train_loader = torch.utils.data.DataLoader(
train_set, batch_size=1, shuffle=True
)
mapping = {
0:'Top', 1: 'Trousers', 2:'Pullover', 3:'Dress', 4:'Coat',
5:'Sandal', 6:'Shirt', 7:'Sneaker', 8:'Bag', 9:'Ankle Boot'
}
plt.figure(figsize=(50, 50)) # 创建画布
for i, batch in enumerate(train_loader, start=1):
image, label = batch
plt.subplot(10, 10, i) # 10代表划分的大小,每行每列10个,i代表子图的编号
fig = plt.imshow(image.reshape(28, 28), cmap='gray')
fig.axes.get_xaxis().set_visible(True)
fig.axes.get_yaxis().set_visible(False)
plt.title(mapping[label.item()], fontsize=28)
if (i >= how_many_to_plot): break
plt.show()
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9tlxjhG3-1619794781651)(output_56_0.png)]
matplotlib.pylot的入门和进阶参考知乎matplotlib.pyplot的使用总结大全(入门加进阶)
下面的内容假设您已经具备python的面对对象编程基础和已经掌握CNN工作原理
torch.nn是Pytorch的神经网络库,导入该库的通常写法为:
import torch.nn as nn
因此我们在使用神经网络包时使用nn的别名。该神经网络包包含了用于构建神经网络所需的所有典型组件,而构建神经网络所需的主要组件就是神经网络层。
深层神经网络是由多层结构构成的,这也是网络「深」的原因。神经网络的每一层都有两个主要组成部分:
转换(代码)
一组权重(数据)
这种结构特性,使OOP成为表示神经网络层的最佳方式。
在Pytorch的神经网络包中,类nn.Module是所有神经网络模块的基类,包括层。这意味着Pytorch中的所有层都扩展了nn.Module类,并继承了Pytorch在nn.Module中的所有内置功能。也就是面对对象编程中的继承这一概念。
因此在Pytoch中构建新层或者神经网络时,我们必须扩展nn.Module类。
当我们把一个张量作为输入传递给网络时,张量通过每一层变换向前流动,直到张量到达输出层。这个张量通过网络向前流动的过程被称为向前传递。
在实现nn.Module子类的forward()方法时,通常将使用nn.functional包中的函数。该软件包为我们提供了许多可用于构建层的神经网络操作。实际上,许多nn.Module层类都使用nn.functional函数来执行操作。
nn.functional软件包中包含了nn.Module用于实现其forward()函数的方法。稍后,通过查看nn.Conv2d卷积层类的PyTorch源代码,来观察一个示例。
构建步骤:
精简版:
扩展nn.Module基类;
将层定义为类属性
实现forwork方法
更详细的版本:
创建一个扩展nn.Module基类神经网络类;
在类构造函数中,使用torch.nn中的预构建层网络的图定义为类属性。
使用网络的层属性以及nn.functional API中的操作来定义网络的向前传播。
class Network:
def __init__(self):
self.layer = None
def forward(self, t):
t = self.layer(t)
return t
这是一个简单类,该类在构造函数内部具有单个虚拟层,并且对forwork函数具有虚拟实现。
forwork函数的实现采用张量t并使用虚拟层对其进行转换。张量转换后,将返回新的张量。接下来让该类扩展nn.Module类。这需要做另外两件事:
在第一行的括号中指定nn.Module类。
在构造函数内部的第3行上插入对super类构造函数的调用.
实现如下:
class Network(nn.Module):
def __init__(self):
super().__init__()
self.layer = None
def forward(self, t):
t = self.layer(t)
return
这样我们的Network类就具有了Pytorch nn.Module类的所有功能。
目前,我们的Network类具有单个虚拟层作为属性,现在使用Ptyroch的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
return t
下面查看Network的结构:
net = Network()
print(net)
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)
)
由上面可以知道网络的详细参数,步幅默认为1,填充默认为无填充。在这个网络中第一个卷积层把一张图片输入输出6个vector,这输出通道是可以任意指定的,也是滤波器的个数即卷积核的个数,这6个卷积核的参数初始是随机生成的权重,在反向传播时会更新这些参数。
至此,我们有了一个名为Network的Python类,该类扩展了PyTorch的nn.Module类。 在Network类内部,我们有五个定义为属性的层。 我们有两个卷积层,self.conv1和self.conv2,以及三个线性层,self.fc1,self.fc2,self.out.
我们在fc1和fc2中使用了缩写fc,因为线性层也称为完全连接层。它们也有一个我们可能会听到的叫做 dense 的名字。 因此,linear, dense, 和 fully connected都是指同一类型的层的所有方法。 PyTorch使用线性这个词,因此使用nn.Linear类名。
我们将名称out用作最后一个线性层,因为网络中的最后一层是输出层。
在CNN网络中,每一层都扩展了Pytorch的神经网络Module类,对于每一层,内部封装了两个主要项目,即formark函数的定义和权重张量。
每层内部的权重张量包含随着网络在训练过程中学习而更新的权重值,这就将各层指定为Network类中的属性的原因。
Pytorch的神经网络Module类跟踪每层内部的权重张量,进行跟踪的代码位于nn.Module类内部,并且由于正在扩展神经网络模块类,因此会自动继承此功能,我们只需关心如何构建网络的各层,Module基类会将各层的权重作为网络的可学习参数。
parameter和argument的区别:在函数定义中的参数名是parameter,传给函数实际值的是argument
在构造层时的两种类型参数:
超参数(Hyperparameters)
数据相关的超参数(Data dependent hyperparameters)
在深度学习中,许多术语是宽松的,但是关于任何类型的参要记住的主要事情就是,该参数是一个占位符,它最终将保存或者具有一个值。
构造层时。我们将每个参数的值传递给层的构造函数。在本文的卷积网络中,对于卷积来说,有三个参数,线性层有两个参数。此外,conv类还定义了 一些卷积神经网络常用的参数,比如填充(Padding)、步幅(stride)。
in_channels
out_chennels
kernel_size
in_features
out_features
接下来观察如何确定参数的值。
通常,超参数是手动和任意选择其值的参数。
对于构建CNN层,下面这些是我们手动选择的参数:
in_channels
out_chennels
kernel_size
这意味着我们只需为这些参数选择值。在神经网络编程中,我们的目的就是要测试和调整这些参数,找到让结果最好的值。
Parameter | Description |
---|---|
kernel_size | 设置filter的大小,其中kernel和filter可以互换 |
out_channels | 设置filter的数量,一个filter产生一个输出通道 |
out_features | 设置输出张量的大小 |
在经常出现的模式中,我们在添加额外的conv层时增加out_channels,在切换到线性层之后,在过滤到输出类的数量时缩小out_features。
所有这些参数都会影响我们的网络架构。具体来说,这些参数直接影响层内的权值张量。
数据相关超参数是其值依赖于数据的参数。突出的前两个数据相关超参数是第一个卷积层的in_channels和输出层的out_features。
你看,第一个convolutional layer的in_channels取决于训练集中图像中出现的彩色通道的数量。因为我们处理的是灰度图像,所以我们知道这个值应该是 1.
输出层的out_features取决于训练集中的类的数量。因为Fashion-MNIST数据集中有10个服装类,所以我们知道我们需要10个输出特性。
通常,一层的输入是上一层的输出,所以conv层中的所有in_channels和线性层中的in_features都依赖于上一层的数据。
当我们从一个conv层转换到一个线性层时,我们必须使我们的张量变平。这就是为什么我们有12 * 4 * 4。12是前一层输出通道的数量,为什么我们有两个4 呢?我们将在下文中讨论如何获得这些值。
可学习参数是指在训练过程中学习的参数值。对于可学习参数,通常从一组随机值开始,然后随着网络的学习,以迭代的方式更新这些值。实际上,我们训练神经网络的目的就是找出这些可学习参数的适当值,并且这些值是能使损失函数最小化的值。
在网络中可学习参数在哪?
可学习参数是网络内部的权重,它们存在于每一层中。
在Pytorch中,可以直接检查权重,首先获取网络类中的一个实例并查看它。
network = Network()
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)
)
print()函数打印了网络的详细结构,这是如何实现的呢?或者说是怎么回事?
网络是从Pytorch Module基类继承而来的,因此,试着停止扩展神经网络模块查看会发生什么。
class testNet:
def __repr__(self):
return "ri"
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
return t
新构建了一个与Network一模一样的类,但是不同的是,该类并没有继承Module基类,接下来查看该类。
network2 = testNet()
print(network2)
ri
可以看到,输出的是该对象在计算机存储的位置,并没有输出网络的结构。造成这种差异的原因是什么呢?
在面向对象的编程中,我们通常希望在类中提供对象的字符串表示形式,以便在打印对象时获得有用的信息。这种字符串表示形式来自Python的默认基类object(对象)。
How Overriding Works
所有Python类都会自动扩展对象类。如果我们想为我们的对象提供一个自定义的字符串表示形式,我们可以做到,但是我们需要引入另一个面向对象的概念,称为overriding(覆盖)。
当我们扩展一个类时,我们获得了它的所有功能,作为补充,我们可以添加其他功能。但是,我们也可以通过将现有功能更改为不同的行为来覆盖现有功能。
我们可以使用 repr 函数覆盖Python的默认字符串表示。这个名称是representation(表示)的缩写。
def __repr__(self):
return "lizardnet"
在testNet中重载该函数后。
class testNet:
def __repr__(self):
return "lizardnet"
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
return t
network3 = testNet()
print(network3)
lizardnet
对于卷积层,kernel_size参数是一个Python元组(5,5),尽管我们只在构造函数中传递了数字5。这是因为我们的滤波器实际上有一个高度和宽度,当我们传递一个数字时,该层构造函数中的代码假设我们需要一个方形滤波器(filter)。
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)
)
stride是一个我们可以设置的额外参数,但是我们把它省略了。当在层构造函数中没有指定stride时,层会自动设置它。
stride 告诉conv层,在整个卷积中,每个操作之后滤波器应该滑动多远。这个元组表示当向右移动时滑动一个单元,向下移动时也滑动一个单元。
对于Linear 层,我们有一个额外的参数bias,它的默认参数值为true。可以通过将它设置为false来关闭它。
是用点符号访问对象的属性和方法。
print(network.conv1, "\n")
print(network.conv2, "\n")
print(network.fc1, "\n")
print(network.fc2, "\n")
print(network.out)
Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
Conv2d(6, 12, kernel_size=(5, 5), stride=(1, 1))
Linear(in_features=192, out_features=120, bias=True)
Linear(in_features=120, out_features=60, bias=True)
Linear(in_features=60, out_features=10, bias=True)
现在我们已经访问了每一层,我们可以访问每一层中的权重。我们来看看第一个卷积层。
network.conv1.weight
Parameter containing:
tensor([[[[-0.0420, -0.0902, 0.0061, 0.0513, -0.0587],
[-0.0190, -0.0423, -0.1689, -0.0091, -0.0183],
[-0.1020, -0.1186, 0.1480, 0.0026, 0.1455],
[-0.0610, -0.1373, 0.1385, -0.1971, 0.0111],
[-0.0162, -0.0648, -0.0815, 0.0376, -0.1747]]],
[[[ 0.1781, 0.1717, -0.1093, 0.1000, -0.1314],
[ 0.1999, -0.0572, -0.0637, 0.1101, 0.0130],
[ 0.1695, -0.0799, -0.1227, -0.0930, -0.1753],
[-0.0229, 0.0160, 0.1243, -0.1920, -0.1127],
[-0.1050, 0.0016, 0.1624, -0.0581, 0.1043]]],
[[[ 0.0584, -0.1991, -0.0016, 0.0977, -0.0403],
[ 0.0949, -0.1363, 0.0414, 0.1039, -0.0231],
[ 0.1511, 0.0814, 0.1382, 0.0085, 0.1879],
[-0.0786, 0.1570, 0.0656, 0.1823, -0.1407],
[-0.1624, 0.1534, -0.0988, 0.1957, -0.0801]]],
[[[-0.0527, 0.0970, 0.1928, -0.1642, -0.0416],
[ 0.1783, 0.1434, 0.1469, 0.0520, 0.1019],
[-0.1915, -0.0406, 0.0375, -0.0945, -0.0439],
[-0.1345, 0.0489, 0.0342, -0.1485, -0.0369],
[-0.0719, -0.1570, 0.0889, 0.1690, -0.1823]]],
[[[ 0.1220, -0.1000, -0.0588, 0.0533, -0.1199],
[-0.0938, 0.0014, 0.1511, 0.0126, -0.0624],
[ 0.0644, -0.1439, -0.1908, -0.1453, -0.0767],
[-0.1938, -0.1970, 0.1265, 0.1539, -0.0350],
[-0.0387, -0.1807, 0.0192, -0.0771, 0.0898]]],
[[[ 0.1996, -0.0019, -0.0366, 0.1949, -0.1128],
[ 0.0886, 0.1442, -0.0012, -0.1384, -0.1956],
[-0.0960, -0.1538, -0.0023, 0.1232, 0.0517],
[-0.0579, -0.0158, 0.1922, -0.1261, -0.0221],
[-0.1511, 0.0705, -0.1407, 0.0017, -0.1093]]]], requires_grad=True)
输出时一个张量,包含了6个张量,也就是6个filter。这些参数并不是固定不变的,当训练网络时,这些参数的值会以使损失函数最小化的方式更新。
在Pytorch中有一个特殊的类,称为Parameter,Parameter类扩展了张量类,所以每层中的权张量就是这个Parameter类的一个实例。这就是为什么能在字符串表示输出的顶部看到包含文本的参数信息。
在Pytorch的源码内部,Parameter类通过将包含正则张量类表示输出的文本参数放在前面,从而覆盖了repr函数。
def __repr__(self):
return 'Parameter containing:\n' + super(Parameter, self).__repr__()
Pytorch的nn,Module类基本上是在寻找其值是Parameter类的任何属性,当它找到参数类的实例是,就会对其进行跟踪。
在卷积层中,权重值位于filter内部,这个滤波器实际上是只是方便的说法,在代码中,filter实际上是权重张量本身。
网络层内的卷积运算就是该层的输入通道与层内的filter之间的运算。这意味着我们真正拥有的是两个张量之间的运算。
张量的形状编码了需要了解的有关张量的的所有信息,对于第一个conv层,有1个颜色通道,应由6个 5 × 5 5\times5 5×5大小的filter进行卷积产生6个输出通道。
network.conv1
Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
不过在层的内部,并不意味着有6个权重张量,实际上,在内部只使用单个权重张量标识所有6个filter。
network.conv1.weight.shape
torch.Size([6, 1, 5, 5])
第二个轴的长度为1,代表单个输入通道,最后两个轴的长度和宽度代表滤波器。
考虑这一点的方式就像我们将所有滤波器打包到一个张量中一样。
现在,第二个conv层具有12个滤波器,不是单个输入通道,而是有6个来自上一层的输入通道。
network.conv2.weight.shape
torch.Size([12, 6, 5, 5])
可以把这里的值6看作是给每个滤镜一定深度。我们的过滤器没有使用可以迭代卷积所有通道的过滤器,而是使用与通道数量匹配的深度。
关于这些卷积层的两个主要结论是,我们的滤波器使用一个单一的张量表示,张量内的每个滤波器也有一个深度,用于说明卷积的输入通道。
所有filter均使用单个张量表示;
filter具有深度用于输入通道。
我们的张量是4阶张量。第一个轴代表滤波器的数量。第二个轴代表每个滤波器的深度,它对应于卷积的输入通道数。
在线性层中,会将张量展平成一阶张量作为输入和输出。
network.fc1.weight.shape
torch.Size([120, 192])
network.fc2.weight.shape
torch.Size([60, 120])
network.out.weight.shape
torch.Size([10, 60])
在线性层的内部,权重以2阶张量的形式表示,第一个轴表示out_feature,第二个轴表示in_feature。
第一种方法,在训练过程中更新权重时经常使用它来遍历权重。
for param in network.parameters():
print(param.shape)
torch.Size([6, 1, 5, 5])
torch.Size([6])
torch.Size([12, 6, 5, 5])
torch.Size([12])
torch.Size([120, 192])
torch.Size([120])
torch.Size([60, 120])
torch.Size([60])
torch.Size([10, 60])
torch.Size([10])
第二种方法是可以显示该权重的名称。
for name, param in network.named_parameters():
print(name, '\t\t', param.shape)
conv1.weight torch.Size([6, 1, 5, 5])
conv1.bias torch.Size([6])
conv2.weight torch.Size([12, 6, 5, 5])
conv2.bias torch.Size([12])
fc1.weight torch.Size([120, 192])
fc1.bias torch.Size([120])
fc2.weight torch.Size([60, 120])
fc2.bias torch.Size([60])
out.weight torch.Size([10, 60])
out.bias torch.Size([10])
在Linear层中,它将in_feature大小的张量转换为_out_feature大小的张量,在神经网络中in_feature的大小一般大于out_feature的大小,也就是说它的主要的作用就是将输入空间映射到输出空间。
在Pytorch中是通过Linear的派生LinearLayer类来实现这种转换的。LinearLayer类的构造函数如下:
def __init__(self, in_features, out_features, bias=True):
super(Linear, self).__init__()
self.in_feature = in_features
self.out_feature = 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)
return reset_parameters()
fc = nn.Linear(in_features=4, out_features=3, bias=False)
in_feature = torch.tensor([1, 2, 3, 4], dtype=torch.float32)
fc(in_feature)
tensor([ 1.1145, -0.4786, -1.1787], grad_fn=)
在上面中,对输入是4维的in_feature,输出了3维的out_feature,这里并没有指定权重矩阵并没有指定特定的值,而是在创建fc对象的时候随机生成的,在调用fc对象进行转换时,in_feature与权重进行乘积运算,最终这种实现转换。
fc.weight
Parameter containing:
tensor([[-0.0123, -0.0893, -0.1574, 0.4443],
[-0.1082, -0.0518, -0.0691, -0.0149],
[-0.3935, -0.1979, -0.3519, 0.1666]], requires_grad=True)
fc.weight.matmul(in_feature) == fc(in_feature)
tensor([True, True, True])
print(fc)
Linear(in_features=4, out_features=3, bias=False)
在上面的示例中,往Linear类对象fc传递一个张量,它会自动计算结果,这在别的面对对象编程里面是一件不可思议的事,别的编程语言里,要想实现这种操作一般会把实现放到构造函数里,但在python里,是实现一个特殊的函数_call()_。 果一个类实现了_call_()方法,那么只要对象实例被调用,这个特殊的调用方法就会被调用。这个事实是一个重要的PyTorch概念,因为在我们的层和网络中,_call _()与forward()方法交互的方式是用的。
我们不直接调用forward()方法,而是调用对象实例。在对象实例被调用之后,在底层调用了_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
PyTorch在_call_()方法中运行的额外代码就是我们从不直接调用forward()方法的原因。如果我们这样做,额外的PyTorch代码将不会被执行。因此,每当我们想要调用forward()方法时,我们都会调用对象实例。这既适用于层,也适用于网络,因为它们都是PyTorch神经网络模块。
任何神经网络的输入层都是由输入数据确定的, 在神经网络中,输入层可以视为恒等变换(identity transformation)。从数学上讲是下面的函数:
在神经网络中,不是输入或输出的所有层都称为隐藏层,在CNN中,两个卷积层之间的池化层就是一个隐藏层,在执行卷积操作时,会将输入张量传递给第一层卷积层self.conv1的forward()方法。由上面的分析知道,在调用nn.Module实例的forward()方法时,将调用实际实例,而不是直接调用forward()方法。
下面在Network的forward()方法中添加池化层(隐藏层)的操作。
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
#(2)hidden conv layer
t = self.conv1(t)
t = F.relu(t) # 激活函数
t = F.max_pool2d(t, kernel_size=2, stride=2) # 最大池化
# (3) hideden conv layer
t = self.conv2(t)
t = F.relu(t)
t = F.max_pool2d(t, kernel_size=2, stride=2)
return t
正如我们在这里看到的那样,当我们在卷积层中移动时,输入张量将发生变换。第一卷积层具有卷积运算,然后是 relu 激活运算,其输出随后传递到kernel_size = 2和stride = 2的最大池化中。
然后将第一个卷积层的输出张量 t 传递到下一个卷积层,除了我们调用self.conv2()而不是self.conv1()以外,其他卷积层均相同。
这些层中的每一个都由权重(数据)和收集操作(代码)组成。权重封装在nn.Conv2d() 类实例中。relu() 和max_pool2d() 调用只是纯运算。这些都不具有权重,这就是为什么我们直接从nn.functional API调用它们的原因。
有时,我们可能会看到称为池化层的池化操作。有时我们甚至可能听到称为激活层的激活操作。
但是,使层与操作区分开的原因在于层具有权重。由于池操作和激活功能没有权重,因此我们将它们称为操作,并将其视为已添加到层操作集合中。
例如,我们说网络中的第二层是一个卷积层,其中包含权重的集合,并执行三个操作,即卷积操作,relu激活操作和最大池化操作。
请注意,此处的规则和术语并不严格。这只是描述网络的一种方式。还有其他表达这些想法的方法。我们需要知道的主要事情是哪些操作是使用权重定义的,哪些操作不使用任何权重。
从历史上看,使用权重定义的操作就是我们所说的层。后来,其他操作被添加到mix中,例如激活功能和池化操作,这引起了术语上的一些混乱。
从数学上来说,整个网络只是函数的组合,函数的组合就是函数本身。因此,网络只是一种函数。诸如层,激活函数和权重之类的所有术语仅用于帮助描述不同的部分。
不要让这些术语混淆整个网络只是函数的组合这一事实,而我们现在正在做的就是在forward()方法中定义这种组合。
在将输入传递到第一个隐藏的Linear 层之前,我们必须reshape() 或展平我们的张量。每当我们将卷积层的输出作为Linear层的输入传递时,都是这种情况。
由于第4层时第一个Linear层,因此将reshape操作作为第4层的一部分。
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
#(2)hidden conv layer
t = self.conv1(t)
t = F.relu(t) # 激活函数
t = F.max_pool2d(t, kernel_size=2, stride=2) # 最大池化
# (3) hideden 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)
return t
隐层的第一层输入为什么是 4 × 4 4\times4 4×4的呢?这实际上是12个输出通道中每个通道的高度和宽度。
们从1 x 28 x 28输入张量开始。这样就给出了一个单一的彩色通道,即28 x 28的图像,并且在我们的张量到达第一 Linear 层时,尺寸已经改变。
通过卷积和池化操作,将高度和宽度尺寸从28 x 28减小到4 x 4。卷积和池化操作是对高度和宽度尺寸的化简操作。
网络的最后一层是Liear层,也叫输出层。张量传递到输出层时,结构将是预测张量。由于数据具有十个预测类别,因此输出张量将具有十个元素。
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
#(2)hidden conv layer
t = self.conv1(t)
t = F.relu(t) # 激活函数
t = F.max_pool2d(t, kernel_size=2, stride=2) # 最大池化
# (3) hideden 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)
t.shape
# (6)output layer
t = self.out(t)
# F.softmax(t, dim=1)
return t
十个组件中的每个组件内的值将对应于我们每个预测类的预测值。
在网络内部,我们通常使用relu() 作为我们的非线性激活函数,但是对于输出层,每当我们尝试预测一个类别时,我们就使用softmax()。softmax函数针对每个预测类返回正概率,并且概率之和为1。
但是,在本例中,我们不会使用softmax(),因为我们将使用的损失函数F.cross_entropy()在其输入上隐式执行softmax()操作,因此我们只返回最后的线性变换。
这意味着我们的网络将使用softmax操作进行训练,但是当训练过程完成后将网络用于推理时,无需计算额外的操作。
正向传播 前向传播是将一个输入张量转化为一个输出张量的过程。就其核心而言,神经网络是一个将输入张量映射到输出张量的函数,而正向传播只是将输入传给网络并从网络接收输出的过程的一个特殊名称。
正如我们所看到的,神经网络是以张量的形式操作数据的。前向传播的概念是用来表示输入张量数据在网络中以向前的方向传输。
对于我们的网络来说,这意味着简单地将我们的输入张量传递给网络并接收输出张量。为了做到这一点,我们将我们的样本数据传递给网络的forward()方法。
这就是为什么,forward()方法的名字是forward,forward()的执行是向前传播的过程。
forward这个词,是很直接的。
然而,传播这个词的意思是通过某种媒介移动或传输。在神经网络的情况下,数据通过网络的各层传播。
也有一个向后传播(backpropagation)的概念,这使得前向传播这个词适合作为第一步。在训练过程中,反向传播发生在前向传播之后。
在我们的案例中,从实际的角度来看,前向传播是将输入图像张量传递给我们在上一节实现的forward()方法的过程。这个输出是网络的预测。
在关于数据集和数据加载器的那一集里,我们看到了如何从我们的训练集中访问单个样本图像张量,更重要的是,如何从数据加载器中访问一批图像张量。现在我们已经定义了我们的网络,并且实现了forward()方法,把一张图片传给我们的网络,以获得预测结果。
在此之前,我们将关闭PyTorch的梯度计算功能。这将阻止PyTorch在我们的张量流经网络时自动建立一个计算图。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RElZRIbZ-1619794781653)(attachment:image.png)]
计算图通过跟踪每一次发生的运算来保持对网络映射的跟踪。在训练过程中,该图被用来计算损失函数相对于网络权重的导数(梯度)。
由于我们还没有训练网络,我们不打算更新权重,所以我们不需要梯度计算。当训练开始时,我们将重新打开这个功能。
关闭它并不是严格意义上的必要,但关闭该功能可以减少内存的消耗,因为图形并不存储在内存中。这段代码将关闭该功能。
torch.set_grad_enabled(False)
重新创建Network实例.
network = Network()
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)
)
接下来,我们从训练集中获得一张简单的图片,解包这张图片和标签。并且验证这张图片的形状。
sample = next(iter(train_set))
image, label = sample
image.shape
torch.Size([1, 28, 28])
plt.imshow(image.squeeze(dim=0))
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5YdM8gNF-1619794781653)(output_163_1.png)]
图像张量的形状表明我们有一个单通道图像,它的高度是28,宽度是28。这就是我们所期待的。
现在,在简单地把这个张量传递给我们的网络之前,还有第二步我们必须预先准备。当我们向网络传递一个张量时,网络需要一批张量,所以即使我们想要传递一张图像,我们仍然需要一批张量。
这不是问题。我们可以创建一个包含单个图像的batch。所有这些都将被打包成一个单一的四维张量,它反映了以下维度。
网络的这一要求来自于nn.Conv2d卷积层类中的forward()方法希望其张量具有4个维度。这是非常标准的,因为大多数神经网络的实现都是处理成批的输入样本而不是单个样本。
为了将我们的单样本图像张量放入一个大小为1的批次中,我们只需要对张量进行unsqueeze()来增加一个额外的维度。我们在之前的节目中看到了如何做到这一点。
# Inserts an additional dimension that represents a batch of size 1
image.unsqueeze(dim=0).shape
torch.Size([1, 1, 28, 28])
利用这个,我们现在可以将未经挤压的图像传递给我们的网络,并获得网络的预测结果。
pred = network(image.unsqueeze(dim=0)) # image shape needs to be (batch_size x in_channels x H x W)
pred
tensor([[-0.0019, -0.0143, 0.0918, -0.0321, -0.0294, -0.1447, 0.0687, 0.1067, 0.0393, -0.0427]])
pred.shape
torch.Size([1, 10])
label
9
pred.argmax(dim=1)
tensor([7])
现在为止,我们已经用我们的正向方法从网络中得到了一个预测。该网络已经返回了一个预测张量,其中包含了对十类服装中每一类的预测值。
预测张量的形状是1×10。这告诉我们,第一个轴的长度为1,而第二个轴的长度为10。对这一点的解释是,我们的批次中有一个图像和十个预测类。
对于批次中的每个输入,以及每个预测类,我们都有一个预测值。如果我们想让这些值成为概率,我们可以只使用nn.functional包中的softmax()函数。
F.softmax(pred, dim=1)
tensor([[0.0991, 0.0979, 0.1089, 0.0962, 0.0965, 0.0860, 0.1064, 0.1105, 0.1033, 0.0952]])
F.softmax(pred, dim=1).sum()
tensor(1.)
训练集的标签是9,但使用argmax()函数,预测的张量最大值是在张量索引为3的位置。在这个索引中,每一个都对应者一个标签。
Index | Label |
---|---|
0 | T-shirt/top |
1 | Trouser |
2 | Pullover |
3 | Dress |
4 | Coat |
5 | Sandal |
6 | Shirt |
7 | Sneaker |
8 | Bag |
9 | Ankle boot |
前面也强调过,层里面的权值是随机生成的,因此每次创建网络的新实例是,网络的权值将是不同的。
net1 = Network()
net2 = Network()
print(net1(image.unsqueeze(0)), '\n', net2(image.unsqueeze(0)))
tensor([[ 0.0544, 0.0811, 0.1150, 0.0900, -0.1405, 0.1027, -0.0466, -0.0054, 0.0400, 0.1460]])
tensor([[-0.0690, -0.0676, 0.0851, 0.0041, 0.0562, -0.1174, 0.0314, 0.1082, -0.0325, 0.0428]])
到目前为止,我们完成了以下工作:
import.
training set.
Network class definition.
To disable gradient tracking.
A Network class instance.
现在我们使用训练集创建一个新的DataLoader实例,并且设置batch_size=10,所以输出将更容易管理。
data_loader = torch.utils.data.DataLoader(
train_set, batch_size=10
)
接下来从数据加载器中读取一个批次,并从批次中解压图片和标签张量。我们将使用复数形式命名变量,因为我们知道当我们调用数据加载器迭代器的下一步时,数据加载器将返回一批10张图像。
batch = next(iter(data_loader))
images, labels = batch
这将得到两个张量,一个张量是image一个是对应标签的张量。
在上一章里,当我们从训练集里抽出一张图片时,我们必须对张量进行unsqueeze(),以增加另一个维度,这将有效地把单子图片转化为大小为1的批次。现在我们正在使用数据加载器,我们默认是在处理批处理,所以不需要进一步处理。
数据加载器返回一批图像,这些图像被打包成一个单一的张量,其形状反映了以下轴线。
意思是在解压的时候,图像的形状已经是好的了,不用使用unsqueeze()。
images.shape
torch.Size([10, 1, 28, 28])
labels.shape
torch.Size([10])
让我们来解释这两个形状。图像张量的第一个轴告诉我们,我们有一批十张图像。这十张图像有一个单色通道,高度和宽度都是二十八。
标签张量有一个单轴,形状为十,对应于我们这批图像中的十张图像。每个图像有一个标签。
好了。让我们通过将图像张量传递给网络来获得一个预测。
preds = network(images)
preds.shape
torch.Size([10, 10])
preds
tensor([[-0.0019, -0.0143, 0.0918, -0.0321, -0.0294, -0.1447, 0.0687, 0.1067, 0.0393, -0.0427],
[ 0.0016, 0.0030, 0.0841, -0.0225, -0.0411, -0.1256, 0.0623, 0.1242, 0.0451, -0.0480],
[ 0.0056, 0.0034, 0.0785, -0.0354, -0.0233, -0.1095, 0.0754, 0.1003, 0.0401, -0.0471],
[ 0.0048, 0.0006, 0.0786, -0.0342, -0.0220, -0.1144, 0.0716, 0.1091, 0.0341, -0.0477],
[ 0.0027, 0.0135, 0.0869, -0.0401, -0.0397, -0.1148, 0.0655, 0.1309, 0.0409, -0.0363],
[-0.0034, 0.0003, 0.0849, -0.0341, -0.0414, -0.1184, 0.0513, 0.1245, 0.0398, -0.0518],
[ 0.0036, -0.0052, 0.0912, -0.0163, -0.0460, -0.1170, 0.0754, 0.1099, 0.0564, -0.0345],
[-0.0014, -0.0027, 0.0768, -0.0370, -0.0456, -0.1188, 0.0424, 0.1377, 0.0375, -0.0513],
[-0.0120, -0.0112, 0.0826, -0.0224, -0.0127, -0.1199, 0.0821, 0.0951, 0.0426, -0.0540],
[-0.0196, -0.0196, 0.0919, -0.0261, -0.0076, -0.1271, 0.0818, 0.0986, 0.0376, -0.0523]])
预测张量的形状为10乘10,这给了我们两个轴,每个轴的长度为10。这反映了一个事实,即我们有十张图像,对于这十张图像中的每一张,我们有十个预测类。
第一个维度的元素是长度为10的数组。这些数组元素中的每一个都包含了对相应图像的每个类别的十个预测值。
第二个维度的元素是数字。每个数字是特定输出类别的分配值。输出类别由索引编码,所以每个索引代表一个特定的输出类别。这个映射是由这个表格给出的。
Fashion MNIST Classes
Index | Label |
---|---|
0 | T-shirt/top |
1 | Trouser |
2 | Pullover |
3 | Dress |
4 | Coat |
5 | Sandal |
6 | Shirt |
7 | Sneaker |
8 | Bag |
9 | Ankle boot |
为了对照标签检查预测结果,我们使用argmax()函数来计算哪个索引包含最高的预测值。一旦我们知道哪个索引有最高的预测值,我们就可以将该索引与标签进行比较,看是否存在匹配。
要做到这一点,我们在预测张量上调用argmax()函数,并指定第二维。
第二维是我们预测张量的最后一维。请记住,在我们所有关于张量的工作中,张量的最后一维总是包含数字,而其他每一维都包含其他更小的张量。
在我们预测张量的案例中,我们有十组数字。argmax()函数所做的是在这十组数字中寻找每一组的最大值,并输出其索引。
对于每个组的十个数字:
寻找最大值;
输出索引。
对这一点的解释是,对于这批图像中的每一张,我们都要找到预测值最高的类别。这就是网络预测最强的类别。
preds.argmax(dim=1)
tensor([7, 7, 7, 7, 7, 7, 7, 7, 7, 7])
labels
tensor([9, 0, 0, 3, 0, 2, 7, 2, 5, 5])
argmax()函数的结果是一个10个预测类别的张量。每个数字是发生最高值的索引。我们有十个数字,因为有十个图像。一旦我们有了这个最高值索引的张量,我们就可以将它与标签张量进行比较。
preds.argmax(dim=1).eq(labels)
tensor([False, False, False, False, False, False, True, False, False, False])
preds.argmax(dim=1).eq(labels).sum()
tensor(1)
为了实现比较,我们使用eq()函数。eq()函数计算了argmax输出和标签张量之间的等价元素操作。
如果argmax输出中的预测类别与标签相匹配,则返回True,否则返回False。
最后,如果我们对这个结果调用sum()函数,我们可以将输出减少到这个标量值张量中的单个正确预测数量。
我们可以将这最后一个调用封装成一个叫做get_num_correct()的函数,该函数接受预测和标签,并使用item()方法返回正确预测的数字。
def get_num_correct(preds, labels):
return preds.argmax(dim=1).eq(labels).sum().item()
调用函数,可以看到返回数字。
get_num_correct(preds, labels)
1
之前的网络:
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)
)
算上输入层总共6层:
当一个张量输入到输入层,我们有:
test_loader = torch.utils.data.DataLoader(
train_set, batch_size=1
)
test_data = next(iter(test_loader))
t, _ = test_data
t.shape
torch.Size([1, 1, 28, 28])
每个维度的值代表下面的值:
由于输入层只是恒等函数,所以输出形状不会改变。
当张量进入这一层时,我们有:
t.shape
torch.Size([1, 1, 28, 28])
经过第一层的self.conv1卷积操作,我们有:
t = network.conv1(t)
t.shape
torch.Size([1, 6, 24, 24])
批量大小仍然是1。这是有道理的,因为我们不会期望我们的批处理量发生变化,而这将是整个前向传递的情况。
颜色通道的数量已经从1增加到6。在我们通过第一个卷积层后,我们不再将这些通道视为颜色通道。我们只是把它们看作是输出通道。我们有6个输出通道的原因是我们在创建self.conv1时指定了out_channels的数量。
正如我们所看到的,这个数字6是任意的。out_channels参数指示nn.Conv2d层生成六个滤波器,也被称为核,形状为5乘5,初始化值为随机的。这些滤波器被用来生成六个输出通道。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kuLAfWkj-1619794781654)(attachment:image.png)]
在使用权重张量(过滤器)对输入张量进行卷积后,其结果就是输出通道。
输出通道的另一个名字是特征图。这里的术语是可以互换的。这是由于随着权重的更新而出现的模式检测代表了像边缘和其他更复杂的模式的特征。
算法:
颜色通道被传入;
使用权重张量(滤波器)进行卷积;
产生特征图并向前传递。
在概念上,我们可以认为权重张量是不同的。然而,我们在代码中真正拥有的是一个单一的权重张量,它有一个out_channels(过滤器)维度。我们可以通过检查权重张量的形状看到这一点。
network.conv1.weight.shape
torch.Size([6, 1, 5, 5])
这个张量的形状是:
对relu()函数的调用删除了任何负值并将其替换为零。我们可以通过检查调用前后张量的min()来验证这一点。
t.min().item()
-0.47941166162490845
t = F.relu(t)
t.min().item()
0.0
relu()函数的数学表达式为:
池化操作通过提取张量内每个2x2位置的最大值来进一步减小我们张量的形状。
t.shape
torch.Size([1, 6, 24, 24])
t = F.max_pool2d(t, kernel_size=2, stride=2)
t.shape
torch.Size([1, 6, 12, 12])
卷积层的输入和输出的张量形状由以下几点给出。
输入形状。[1, 1, 28, 28]
输出形状。[1, 6, 12, 12]
发生的每个操作的要点:
卷积层使用六个随机初始化的5x5过滤器对输入张量进行卷积。
relu激活函数操作将所有负值映射为0。
max pooling操作从六个特征图的每个2x2部分提取最大值,这些特征图是由卷积创建的。
假设我们有一个 n × n n \times n n×n的输入。
假设我们有一个 f × f f \times f f×f的过滤器。
假设我们有一个padding为 p p p和一个stride为 s s s。
输出大小由这个公式给出:
这个值将是输出的高度和宽度。然而,如果输入或过滤器不是一个正方形,这个公式需要应用两次,一次是宽度,一次是高度。
假设我们有一个 n h × n w n_h \times n_w nh×nw的输入。
假设我们有一个 f h × f w f_h \times f_w fh×fw的过滤器。
假设我们有一个padding为 p p p和一个stride为 s s s。
则高的输出大小 O h O_h Oh由以下公式计算:
宽的输出大小 O w O_w Ow由以下公式计算:
第二个隐藏卷积层self.conv2,对张量进行了与self.conv1相同的变换,并进一步减少了高度和宽度的维度。在我们进行这些转换之前,让我们检查一下self.conv2的权重张量的形状。
network.conv2.weight.shape
torch.Size([12, 6, 5, 5])
这次我们的权重张量有12个高度为5、宽度为5的滤波器,但不是只有一个输入通道,而是有6个通道,这让滤波器有了深度。这就说明了第一个卷积层的六个输出通道。最终的输出将有12个通道。
现在让我们运行这些操作:
t.shape
torch.Size([1, 6, 12, 12])
t = network.conv2(t)
t.shape
torch.Size([1, 12, 8, 8])
t.min().item()
-0.38865917921066284
t = F.relu(t)
t.min().item()
0.0
t = F.max_pool2d(t, kernel_size=2, stride=2)
t.shape
torch.Size([1, 12, 4, 4])
self.conv2的输出结果的形状让我们看到为什么我们在将张量传递给第一个线性层self.fc1之前用 12 ∗ 4 ∗ 4 12*4*4 12∗4∗4来改变张量形状。
正如我们在过去所看到的,这种特殊的变形被称为张量的扁平化。扁平化操作将张量的所有元素放入一个单一维度。
t = t.reshape(-1, 12*4*4)
t.shape
torch.Size([1, 192])
得到的形状是 1 × 192 1\times192 1×192。在这种情况下,1代表批次大小,192是张量中现在处于同一维度的元素的数量。
现在,我们只是有一系列的线性层,然后是非线性激活函数,直到我们到达输出层。
t = network.fc1(t)
t.shape
torch.Size([1, 120])
t = network.fc2(t)
t.shape
torch.Size([1, 60])
t = network.out(t)
t.shape
torch.Size([1, 10])
t
tensor([[-0.0483, -0.0084, 0.0476, -0.0967, -0.0723, -0.1335, 0.0928, 0.1367, 0.1115, -0.0238]])
本表总结了改变形状的操作和每种操作产生的形状:
Operation | Output Shape |
---|---|
恒等变换函数 | torch.Size([1, 1, 28, 28]) |
卷积( 5 × 5 5 \times 5 5×5) | torch.Size([1, 6, 24, 24]) |
最大池化( 2 × 2 2 \times 2 2×2) | torch.Size([1, 6, 12, 12]) |
卷积( 5 × 5 5 \times 5 5×5) | torch.Size([1, 12, 8, 8]) |
最大池化( 2 × 2 2 \times 2 2×2) | torch.Size([1, 12, 4, 4]) |
扁平化(reshape) | torch.Size([1, 192]) |
线性变换 | torch.Size([1, 120]) |
线性变换 | torch.Size([1, 60]) |
线性变换 | torch.Size([1, 10]) |
现在我们应该对输入张量如何被卷积神经网络转化,如何在PyTorch中调试神经网络,以及如何检查所有层的权重张量有了充分的了解。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YR2WV3x9-1619794781655)(attachment:image.png)]
在下一章,我们将开始训练我们的网络,这将使我们的权重张量的值被更新,以使我们的网络的正向方法将输入映射到正确的输出类别。
在训练期间,我们做了一个前向传递,但然后呢?我们会假设得到一个批次,并通过网络向前传递。一旦获得输出,我们就将预测输出与实际标签进行比较,一旦我们知道预测值与实际标签有多接近,我们就调整网络内部的权重,使网络预测的值更接近真实值(标签)。
所有这些都是针对一个批次的,我们对每一个批次都重复这个过程,直到我们覆盖了训练集中的每个样本。当我们完成了所有批次的这一过程,并通过了我们训练集中的每一个样本,我们就说一个阶段已经完成。我们用epoch这个词来表示我们的整个训练集已经被覆盖的时间段。
在整个训练过程中,我们根据需要做尽可能多的epochs,以达到我们所期望的准确度水平。有了这个,我们就有了以下步骤:
从训练集中获取批次。
将批次传递给网络。
计算损失(预测值和真实值之间的差)。
计算损失函数的梯度与网络权重的关系。
使用梯度更新权重以减少损失。
重复步骤1-5,直到完成一个epoch。
重复步骤1-6,直到达到最小损失所需的epoch。
接下来让我们看看这在代码中是如何完成的。
在上一张中,我们禁用了PyTorch的梯度跟踪功能,所以我们需要确保将其重新打开(默认情况下是打开的)。
torch.set_grad_enabled(True)
我们已经知道如何获得一个批次并通过网络向前传递。让我们看看正向传播完成后我们做需要什么。
我们将从以下几个方面开始:
创建一个我们的网络类的实例。
创建一个数据加载器,从我们的训练集提供大小为100的批次。
从这些批次中的一个中解压图像和标签。
network = Network()
train_loader = torch.utils.data.DataLoader(
train_set, batch_size=100
)
batch = next(iter(train_loader)) # get a batch
images, labels = batch
接下来,我们准备通过网络向前传递我们的一批图像,并获得输出预测。一旦我们有了预测张量,我们就可以使用预测值和真实标签来计算损失。
要做到这一点,我们将使用PyTorch的nn.functional API中的cross_entropy()损失函数。一旦我们得到了损失,我们就可以打印它,并使用我们在前一篇文章中创建的函数检查正确的预测数量。
preds = network(images)
loss = F.cross_entropy(preds, labels) # Calculating the loss
loss.item()
2.304513692855835
get_num_correct(preds, labels)
11
cross_entropy()函数返回了一个标量值的tensor,因此我们用item()方法将损失打印成Python数字。我们得到了100个中正确的个数,由于我们有10个预测类,因此这只是我们随机预测的结果。
使用PyTorch计算梯度是非常容易的。由于我们的网络是一个PyTorch nn.Module,PyTorch已经在引擎盖下创建了一个计算图。当我们的张量通过我们的网络向前流动时,所有的计算都被添加到图中。然后,PyTorch使用该计算图来计算损失函数相对于网络权重的梯度。
在我们计算梯度之前,让我们验证一下,我们目前在conv1层内没有梯度。梯度在每个层的权重张量的grad(梯度的简称)属性中可以访问的张量。
print(network.conv1.weight.grad)
None
为了计算梯度,我们在损失张量上调用backward()方法,像这样:
loss.backward() # Calculating the gradients
现在,损失函数的梯度已被储存在权重张量内。
network.conv1.weight.grad.shape
torch.Size([6, 1, 5, 5])
这些梯度被优化器用来更新各自的权重。为了创建我们的优化器,我们使用torch.opt包,它有许多优化算法的实现,我们可以使用。我们将使用Adam作为我们的例子。
在Adam类的构造函数中,我们需要传递网络参数(这是优化器能够访问梯度的方式),并传递学习率。
最后,我们所要做的就是告诉优化器使梯度向损失函数的最小值方向步进,以更新权重。
optimizer = optim.Adam(network.parameters(), lr=0.01)
optimizer.step() # Updating the weight
如果不记得了前面讲的parameters()函数,可以通过下面的代码查看。
for name, param in network.named_parameters():
print(name, '\t\t', param.shape)
conv1.weight torch.Size([6, 1, 5, 5])
conv1.bias torch.Size([6])
conv2.weight torch.Size([12, 6, 5, 5])
conv2.bias torch.Size([12])
fc1.weight torch.Size([120, 192])
fc1.bias torch.Size([120])
fc2.weight torch.Size([60, 120])
fc2.bias torch.Size([60])
out.weight torch.Size([10, 60])
out.bias torch.Size([10])
当step()函数被调用时,优化器使用存储在网络参数中的梯度来更新权重。这意味着,如果我们再次将相同的批次通过网络,我们应该期望我们的损失会减少。通过检查,我们可以看到情况确实如此。
preds = network(images)
print('未更新前的loss: ', loss.item())
loss = F.cross_entropy(preds, labels)
print('更新之后的loss: ', loss.item())
print('预测正确的个数: ', get_num_correct(preds, labels))
未更新前的loss: 2.304513692855835
更新之后的loss: 2.278359889984131
预测正确的个数: 17
我们可以用以下方式来总结单批训练的代码:
network = Network()
train_loader = torch.utils.data.DataLoader(train_set, batch_size=1000)
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 weight
print('loss1:', loss.item())
preds = network(images)
loss = F.cross_entropy(preds, labels)
print('loss2:', loss.item())
loss1: 2.3056843280792236
loss2: 2.294330596923828
现在我们应该对训练过程有了很好的理解。在下一节,我们将看到这些想法是如何通过构建训练循环来完成这个过程的。下一集见!
现在,为了用我们的数据加载器中的所有批次进行训练,我们需要做一些改变,并增加一行代码。
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)
loss = F.cross_entropy(preds, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
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: 46680 loss: 346.34768933057785
我们将创建一个for循环,对所有批次进行迭代,而不是从数据加载器中获取单一批次。
由于我们的训练集有60,000个样本,我们将有60,000/100=600次迭代。出于这个原因,我们将从循环中删除打印语句,并跟踪总损失和正确预测的总数,在最后打印出来。
关于这600次迭代,需要注意的是,在循环结束时,我们的权重将被更新600次。如果我们增大batch_size,这个数字会下降,如果我们减少batch_size,这个数字会上升。
最后,在我们对损失张量调用backward()方法后,我们知道梯度将被计算出来并添加到我们网络参数的梯度属性中。出于这个原因,我们需要将这些梯度归零。我们可以用优化器自带的一个名为zero_grad()的方法来做这件事。
我们准备好运行这段代码了。这一次代码将花费更长的时间,因为循环是在600个批次上工作。
我们得到了结果,我们可以看到,在60,000个样本中,正确的总人数。
total_correct / len(train_set)
0.778
计算准确率,仅仅经过一个epoch(对数据的一次完整传播),这已经很不错了。尽管我们做了一个epoch,但我们仍然要记住,权重被更新了600次,而这个事实取决于我们的批次大小。如果让我们的batch_batch大小更大,比如说10,000,那么权重将只被更新6次,结果就不那么好了。
要做多个epoch,我们要做的就是把这段代码放到一个for循环中。我们还需要在打印语句中加入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)
loss = F.cross_entropy(preds, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
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: 47721 loss: 326.21062056720257
epoch: 1 total_correct: 51710 loss: 227.46136574447155
epoch: 2 total_correct: 52321 loss: 208.98761811852455
epoch: 3 total_correct: 52639 loss: 199.79103867709637
epoch: 4 total_correct: 52837 loss: 194.605075314641
epoch: 5 total_correct: 52975 loss: 190.11616092175245
epoch: 6 total_correct: 53259 loss: 183.56069585680962
epoch: 7 total_correct: 53277 loss: 183.18500002473593
epoch: 8 total_correct: 53267 loss: 180.34344045817852
epoch: 9 total_correct: 53280 loss: 183.23165196180344
运行这段代码后,我们得到了每个epoch的结果。我们可以看到,正确值的数量上升了,而损失下降了。
把所有这些放在一起,我们可以把网络、优化器和train_loader从训练循环单元中拉出来。
network = Network()
network = network.to(device) # 将模型放入GPU
print(network)
# 将所需的参数放入cuda
lr = torch.tensor(0.01)
lr.to(device)
optimizer = optim.Adam(network.parameters(), lr=0.01)
train_loader = torch.utils.data.DataLoader(
train_set
,batch_size=100
,shuffle=True
)
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)
)
cuda_gpu = torch.cuda.is_available()
print(cuda_gpu)
True
这使得我们可以在不重设网络权重的情况下运行训练循环。
# 设置为GPU运行
print("train on", device)
for epoch in range(10):
total_loss = 0
total_correct = 0
for batch in train_loader: # Get Batch
images, labels = batch
Images = images.to(device)
Labels = labels.to(device)
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.cpu(), labels)
print(
"epoch", epoch,
"total_correct:", total_correct,
"loss:", total_loss
)
train on cuda
epoch 0 total_correct: 46558 loss: 352.42505829036236
epoch 1 total_correct: 51224 loss: 236.13366067409515
epoch 2 total_correct: 51800 loss: 218.45309729874134
epoch 3 total_correct: 52143 loss: 208.66436203569174
epoch 4 total_correct: 52534 loss: 199.67975756525993
epoch 5 total_correct: 52742 loss: 195.56866421550512
epoch 6 total_correct: 52752 loss: 194.73266977071762
epoch 7 total_correct: 52798 loss: 195.70754154026508
epoch 8 total_correct: 53082 loss: 185.96505503356457
epoch 9 total_correct: 53143 loss: 188.44492967426777
---------------------------------------------------------------------------
ModuleNotFoundError Traceback (most recent call last)
in
----> 1 import paddle
ModuleNotFoundError: No module named 'paddle'