本教程翻译自微软教程:https://learn.microsoft.com/en-us/training/paths/pytorch-fundamentals/
初次编辑:2024/3/1;最后编辑:2024/3/4
本教程包含以下内容:
- 介绍pytorch基础和张量操作
- 介绍数据集
- 介绍归一化
- 介绍构建模型层的基本操作
- 介绍自动微分相关知识
- 介绍优化循环(optimization loop)相关知识
- 介绍加载与运行模型预测的相关知识
- 总结
另外本人还有pytorch CV相关的教程,见专题:
https://blog.csdn.net/qq_33345365/category_12578430.html
介绍
大多数机器学习工作流程都涉及处理数据、创建模型、使用超参数优化模型、保存和推断已训练的模型。本模块介绍在 PyTorch 中实现的完整机器学习(ML)工作流程,PyTorch 是一种流行的 Python ML 框架。
本教程使用 FashionMNIST 数据集来训练一个神经网络模型,该模型可以识别图像,如 T 恤/上衣、裤子、套衫、连衣裙、外套、凉鞋、衬衫、运动鞋、包包或短靴。
在构建模型之前,会展示构建神经网络模型的关键概念。
学习目标:
- 学习如何在 CPU 和 GPU 上使用张量(Tensors)
- 理解如何管理、扩展和规范化数据集
- 使用神经网络构建图像识别模型
- 学习如何优化模型
- 学习如何提高模型推理性能
先修要求:
基本的 Python 知识
1.什么是张量 Tensor
张量
张量是一种专门的数据结构,非常类似于数组和矩阵。PyTorch使用张量来编码模型的输入和输出,以及模型的参数。张量类似于NumPy
数组和ndarrays
,但是张量可以在GPU或其他硬件加速器上运行。事实上,张量和NumPy数组通常可以共享相同的底层内存地址,具有称为bridge-to-np-label
的功能,这消除了复制数据的需要。张量还针对自动微分进行了优化。
设置基本环境:
import torch
import numpy as np
初始化一个张量
张量可通过多种方式初始化,见如下例子:
1. 直接通过数据初始化
张量可以直接从数据中创建。数据类型是自动推断的。
data = [[1, 2],[3, 4]]
x_data = torch.tensor(data)
2. 从一个NumPy数组
张量可以从NumPy数组创建,反之亦然。由于numpy格式的np_array
和张量格式x_np
在这里共享相同的内存位置,改变其中一个的值将会改变另一个的值。
np_array = np.array(data)
x_np = torch.from_numpy(np_array)print(f"Numpy np_array value: \n {np_array} \n")
print(f"Tensor x_np value: \n {x_np} \n")np.multiply(np_array, 2, out=np_array)print(f"Numpy np_array after * 2 operation: \n {np_array} \n")
print(f"Tensor x_np value after modifying numpy array: \n {x_np} \n")
输出是:
Numpy np_array value: [[1 2][3 4]] Tensor x_np value: tensor([[1, 2],[3, 4]], dtype=torch.int32) Numpy np_array after * 2 operation: [[2 4][6 8]] Tensor x_np value after modifying numpy array: tensor([[2, 4],[6, 8]], dtype=torch.int32)
3. 从其他张量
x_ones = torch.ones_like(x_data) # retains the properties of x_data
print(f"Ones Tensor: \n {x_ones} \n")x_rand = torch.rand_like(x_data, dtype=torch.float) # overrides the datatype of x_data
print(f"Random Tensor: \n {x_rand} \n")
输出是:
Ones Tensor: tensor([[1, 1],[1, 1]]) Random Tensor: tensor([[0.7907, 0.9041],[0.4805, 0.0954]])
4. 使用随机数或常量
shape
由张量维度的元组定义,它设置了张量的行数和列数。在下面的函数中,shape
确定了输出张量的维度。
shape = (2,3)
rand_tensor = torch.rand(shape)
ones_tensor = torch.ones(shape)
zeros_tensor = torch.zeros(shape)print(f"Random Tensor: \n {rand_tensor} \n")
print(f"Ones Tensor: \n {ones_tensor} \n")
print(f"Zeros Tensor: \n {zeros_tensor}")
输出:
Random Tensor: tensor([[0.5893, 0.4485, 0.6525],[0.9083, 0.2913, 0.0752]]) Ones Tensor: tensor([[1., 1., 1.],[1., 1., 1.]]) Zeros Tensor: tensor([[0., 0., 0.],[0., 0., 0.]])
张量的属性 Attributes of a tensor
张量属性描述了他们的形状、数据类型和所处的设备。
tensor = torch.rand(3,4)print(f"Shape of tensor: {tensor.shape}")
print(f"Datatype of tensor: {tensor.dtype}")
print(f"Device tensor is stored on: {tensor.device}")
输出是:
Shape of tensor: torch.Size([3, 4])
Datatype of tensor: torch.float32
Device tensor is stored on: cpu
有100多个张量操作,包括算术运算、线性代数、矩阵操作(如转置、索引和切片)。可以在这找到全面的描述。
这些操作中的每一个都可以在GPU上运行(通常比在CPU上速度更快)。
-
CPU的核心数少于100个。核心是执行实际计算的单元。每个核心按顺序处理任务(一次处理一个任务)。
-
GPU有数千乃至上万个核心。GPU核心以并行处理的方式处理计算,任务被分割并在不同核心上处理。这就是为什么在大多数情况下GPU比CPU更快的原因。GPU在处理大数据时表现比小数据更好,因为更有利于并行加速。GPU通常用于图形或神经网络的高强度计算。
-
PyTorch可以使用Nvidia CUDA库来利用其GPU卡。
GPU并不总是优于CPU,因为CPU的单核性能强,因此在处理小数据时可能更占优势。
默认情况下,张量在CPU上创建。张量也可以移动到GPU;为此,需要使用.to
方法将它们移动(在检查GPU可用性后)。当然,由于CPU和GPU之间的传输跨设备,这意味着传输上的巨大开销。
# 如果GPU可用则将tensor移动到GPU上,GPU可用需要保证1. CUDA安装;2. 安装对应CUDA版本的pytorch,见https://pytorch.org/,选择合适的pytorch安装方式
if torch.cuda.is_available():tensor = tensor.to('cuda')
类似numpy的标准索引和切片 Standard numpy-like indexing and slicing
tensor = torch.ones(4, 4)
print('First row: ',tensor[0])
print('First column: ', tensor[:, 0])
print('Last column:', tensor[..., -1])
tensor[:,1] = 0
print(tensor)
输出是:
First row: tensor([1., 1., 1., 1.])
First column: tensor([1., 1., 1., 1.])
Last column: tensor([1., 1., 1., 1.])
tensor([[1., 0., 1., 1.],[1., 0., 1., 1.],[1., 0., 1., 1.],[1., 0., 1., 1.]])
拼接张量
可以使用 torch.cat
函数沿着指定维度连接一个张量序列。torch.stack
是一个相关的张量拼接方法,它沿着新的维度将一个张量序列堆叠起来。以下例子,维度有四个可选值:
t1 = torch.cat([tensor, tensor, tensor], dim=1)
print(t1)
dim=1;输出:
tensor([[1., 0., 1., 1., 1., 0., 1., 1., 1., 0., 1., 1.],[1., 0., 1., 1., 1., 0., 1., 1., 1., 0., 1., 1.],[1., 0., 1., 1., 1., 0., 1., 1., 1., 0., 1., 1.],[1., 0., 1., 1., 1., 0., 1., 1., 1., 0., 1., 1.]])
dim=0,输出:
tensor([[1., 0., 1., 1.],[1., 0., 1., 1.],[1., 0., 1., 1.],[1., 0., 1., 1.],[1., 0., 1., 1.],[1., 0., 1., 1.],[1., 0., 1., 1.],[1., 0., 1., 1.],[1., 0., 1., 1.],[1., 0., 1., 1.],[1., 0., 1., 1.],[1., 0., 1., 1.]])
dim=-1,输出:
tensor([[1., 0., 1., 1., 1., 0., 1., 1., 1., 0., 1., 1.],[1., 0., 1., 1., 1., 0., 1., 1., 1., 0., 1., 1.],[1., 0., 1., 1., 1., 0., 1., 1., 1., 0., 1., 1.],[1., 0., 1., 1., 1., 0., 1., 1., 1., 0., 1., 1.]])
dim=-2,输出:
tensor([[1., 0., 1., 1.],[1., 0., 1., 1.],[1., 0., 1., 1.],[1., 0., 1., 1.],[1., 0., 1., 1.],[1., 0., 1., 1.],[1., 0., 1., 1.],[1., 0., 1., 1.],[1., 0., 1., 1.],[1., 0., 1., 1.],[1., 0., 1., 1.],[1., 0., 1., 1.]])
数学操作:
# 这计算了两个张量之间的矩阵乘法。Y1 y2 y3的值是一样的
y1 = tensor @ tensor.T
y2 = tensor.matmul(tensor.T)y3 = torch.rand_like(tensor)
torch.matmul(tensor, tensor.T, out=y3)# 它计算元素级乘积,Z1 z2 z3的值是一样的
z1 = tensor * tensor
z2 = tensor.mul(tensor)z3 = torch.rand_like(tensor)
torch.mul(tensor, tensor, out=z3)print(y3)
print(z3)
输出:
tensor([[3., 3., 3., 3.],[3., 3., 3., 3.],[3., 3., 3., 3.],[3., 3., 3., 3.]])
tensor([[1., 0., 1., 1.],[1., 0., 1., 1.],[1., 0., 1., 1.],[1., 0., 1., 1.]])
单元素张量
如果您有一个单元素张量,例如,通过将张量的所有值聚合为一个值,您可以使用item()将其转换为Python数值:
agg = tensor.sum()
agg_item = agg.item()
print(agg_item, type(agg_item))
输出:
12.0 <class 'float'>
原地操作 in-place operations
将结果存储到操作数(operand)中的操作称为原地操作。它们通常用下划线_
作为后缀。例如:x.copy_(y)
,x.t_(),会改变x
的值。
注意: 就地操作可以节省一些内存,但在计算导数时可能会出现问题,因为它们会立即丢失历史记录。因此,不建议使用它们。
print(tensor, "\n")
tensor.add_(5)
print(tensor)
输出:
tensor([[1., 0., 1., 1.],[1., 0., 1., 1.],[1., 0., 1., 1.],[1., 0., 1., 1.]]) tensor([[6., 5., 6., 6.],[6., 5., 6., 6.],[6., 5., 6., 6.],[6., 5., 6., 6.]])
与Numpy桥接 Bridge with Numpy
NumPy数组和CPU上的张量可以共享它们的底层内存位置,改变其中一个将改变另一个。
张量到NumPy数组的转换
t = torch.ones(5)
print(f"t: {t}")
n = t.numpy()
print(f"n: {n}")
t.add_(1)
print(f"t: {t}")
print(f"n: {n}")
输出:
t: tensor([1., 1., 1., 1., 1.])
n: [1. 1. 1. 1. 1.]
t: tensor([2., 2., 2., 2., 2.])
n: [2. 2. 2. 2. 2.]
NumPy数组到张量的转换
n = np.ones(5)
t = torch.from_numpy(n)
np.add(n, 1, out=n)
print(f"t: {t}")
print(f"n: {n}")
输出:
t: tensor([2., 2., 2., 2., 2.], dtype=torch.float64)
n: [2. 2. 2. 2. 2.]
代码汇总
import torch
import numpy as npdata = [[1, 2], [3, 4]]
x_data = torch.tensor(data)np_array = np.array(data)
x_np = torch.from_numpy(np_array)print(f"Numpy np_array value: \n {np_array} \n")
print(f"Tensor x_np value: \n {x_np} \n")np.multiply(np_array, 2, out=np_array)print(f"Numpy np_array after * 2 operation: \n {np_array} \n")
print(f"Tensor x_np value after modifying numpy array: \n {x_np} \n")# 从其他张量
x_ones = torch.ones_like(x_data) # retains the properties of x_data
print(f"Ones Tensor: \n {x_ones} \n")x_rand = torch.rand_like(x_data, dtype=torch.float) # overrides the datatype of x_data
print(f"Random Tensor: \n {x_rand} \n")# random and constant
shape = (2, 3)
rand_tensor = torch.rand(shape)
ones_tensor = torch.ones(shape)
zeros_tensor = torch.zeros(shape)print(f"Random Tensor: \n {rand_tensor} \n")
print(f"Ones Tensor: \n {ones_tensor} \n")
print(f"Zeros Tensor: \n {zeros_tensor}")# attributes
tensor = torch.rand(3, 4)print(f"Shape of tensor: {tensor.shape}")
print(f"Datatype of tensor: {tensor.dtype}")
print(f"Device tensor is stored on: {tensor.device}")if torch.cuda.is_available():tensor = tensor.to('cuda')tensor = torch.ones(4, 4)
print('First row: ', tensor[0])
print('First column: ', tensor[:, 0])
print('Last column:', tensor[..., -1])
tensor[:, 1] = 0
print(tensor)t1 = torch.cat([tensor, tensor, tensor], dim=-1)
print(t1)# This computes the matrix multiplication between two tensors. y1, y2, y3 will have the same value
y1 = tensor @ tensor.T
y2 = tensor.matmul(tensor.T)y3 = torch.rand_like(tensor)
torch.matmul(tensor, tensor.T, out=y3)# This computes the element-wise product. z1, z2, z3 will have the same value
z1 = tensor * tensor
z2 = tensor.mul(tensor)z3 = torch.rand_like(tensor)
torch.mul(tensor, tensor, out=z3)print(y3)
print(z3)agg = tensor.sum()
agg_item = agg.item()
print(agg_item, type(agg_item))print(tensor, "\n")
tensor.add_(5)
print(tensor)t = torch.ones(5)
print(f"t: {t}")
n = t.numpy()
print(f"n: {n}")
t.add_(1)
print(f"t: {t}")
print(f"n: {n}")n = np.ones(5)
t = torch.from_numpy(n)
np.add(n, 1, out=n)
print(f"t: {t}")
print(f"n: {n}")
2 数据集与数据加载器 Datasets and Dataloaders
处理数据样本的代码可能会变得复杂且难以维护。通常希望数据集代码与模型训练代码解耦,以获得更好的可读性和模块化性。PyTorch提供了两个数据原语:torch.utils.data.DataLoader
和torch.utils.data.Dataset
,它们使您能够使用预加载的数据集以及您自己的数据。Dataset存储样本及其对应的标签,而DataLoader则在Dataset周围包装了一个可迭代对象,以便轻松访问样本。
PyTorch库提供了许多预加载的样本数据集(例如FashionMNIST),它们是torch.utils.data.Dataset
的子类,并实现了特定于特殊数据的功能。用于模型原型设计(prototyping)和基准测试(benchmark)的样本包括:
- 图像数据集
- 文本数据集
- 音频数据集
加载数据集
此处使用TorchVision来加载Fashion-MNIST数据集。Fashion-MNIST是Zalando的服装图像数据集,包含60,000个训练样本和10,000个测试样本。每个样本包括一个28×28的灰度图像和一个来自10个类别中的关联标签。
- 每个图像高度为28个像素,宽度为28个像素,总共有784个像素。
- 这10个类别告诉图像是什么类型,例如:T恤/上衣、裤子、套头衫、连衣裙、包、短靴等。
- 灰度像素的值介于0到255之间,用于衡量黑白图像的强度。强度值从白色到黑色递增。例如:白色的颜色值为0,而黑色的颜色值为255。
我们使用以下参数加载FashionMNIST数据集:
- root 是存储训练/测试数据的路径。
- train 指定训练或测试数据集。
- download=True 如果数据在
root
中不可用,则从互联网下载数据。 - transform 和
target_transform
指定特征和标签的转换。
加载训练和测试数据集的代码如下所示:
import torch
from torch.utils.data import Dataset
from torchvision import datasets
from torchvision.transforms import ToTensor, Lambda
import matplotlib.pyplot as plttraining_data = datasets.FashionMNIST(root="data",train=True,download=True,transform=ToTensor()
)test_data = datasets.FashionMNIST(root="data",train=False,download=True,transform=ToTensor()
)
数据集的迭代与可视化
可以像列表一样手动索引Datasets
: training_data[index]
。此处使用matplotlib对训练数据中的一些样本进行可视化。代码如下所示:
labels_map = {0: "T-Shirt",1: "Trouser",2: "Pullover",3: "Dress",4: "Coat",5: "Sandal",6: "Shirt",7: "Sneaker",8: "Bag",9: "Ankle Boot",
}
figure = plt.figure(figsize=(8, 8))
cols, rows = 3, 3
for i in range(1, cols * rows + 1):sample_idx = torch.randint(len(training_data), size=(1,)).item()img, label = training_data[sample_idx]figure.add_subplot(rows, cols, i)plt.title(labels_map[label])plt.axis("off")plt.imshow(img.squeeze(), cmap="gray")
plt.show()
得到如下图片:
使用数据加载器(DataLoaders)准备自定义数据
Dataset
逐个样本检索数据集的特征和标签。在训练模型时,通常希望以“minibatch”的形式传递样本,在每个周期重混洗(reshuffle)数据以减少模型过拟合,并使用Python的多进程加速数据检索。
在机器学习中,需要指定数据集中的特征和标签。**特征(Feature)**是输入,**标签(label)**是输出。我们训练特征,然后训练模型以预测标签。
- 特征是图像像素中的模式。
- 标签是10个类别类型:T恤,凉鞋,连衣裙等。
DataLoader
是一个可迭代对象,用简单的API抽象了这种复杂性。为了使用DataLoader,我们需要设置以下参数:
- data 用于训练模型的训练数据,以及用于评估模型的测试数据。
- batch size 每个批次要处理的记录数。
- shuffle 按索引随机抽取数据样本。
使用DataLoader
处理两个数据集的代码如下所示:
from torch.utils.data import DataLoadertrain_dataloader = DataLoader(training_data, batch_size=64, shuffle=True)
test_dataloader = DataLoader(test_data, batch_size=64, shuffle=True)
使用DataLoader进行迭代
我们已将数据集加载到 DataLoader
中,现在可以根据需要迭代数据集。 下面的每次迭代都会返回一个批次的 train_features
和 train_labels
(分别包含 batch_size=64
个特征和标签)。由于我们指定了 shuffle=True
,在遍历完所有批次后,数据将被混洗(shuffle),以更精细地控制数据加载顺序。
展示图片和标签的代码如下所示:
# Display image and label.
train_features, train_labels = next(iter(train_dataloader))
print(f"Feature batch shape: {train_features.size()}")
print(f"Labels batch shape: {train_labels.size()}")
img = train_features[0].squeeze()
label = train_labels[0]
plt.imshow(img, cmap="gray")
plt.show()
label_name = list(labels_map.values())[label]
print(f"Label: {label_name}")
得到图片:
Datasets和DataLoader的区别是:前者检索单个数据,后者批量处理数据
3 归一化
标准化是一种常见的数据预处理技术,用于对数据进行缩放或转换,以确保每个特征都有相等的学习贡献。例如,灰度图像中的每个像素的值都介于0和255之间,这些是特征。如果一个像素值为17,另一个像素为197,则像素重要性的分布将不均匀,因为较高的像素值会偏离学习。标准化改变了数据的范围,而不会扭曲其特征之间的区别。这种预处理是为了避免:
- 减少预测精度
- 模型学习困难
- 特征数据范围的不利分布
Transforms
数据并不总是以最终处理过的形式呈现,这种形式适合训练机器学习算法。可以使用transforms来操作数据,使其适合训练。
所有 TorchVision 数据集都有两个参数(transform
用于修改特征,target_transform
用于修改标签),这些参数接受包含转换逻辑的可调用对象。torchvision.transforms
模块提供了几种常用的转换。
FashionMNIST 的特征是 PIL 图像格式,标签是整数。 对于训练,需要将特征转换为归一化的张量,将标签转换为 one-hot 编码的张量。 为了进行这些转换,将使用 ToTensor
和 Lambda
。
from torchvision import datasets
from torchvision.transforms import ToTensor, Lambdads = datasets.FashionMNIST(root="data",train=True,download=True,transform=ToTensor(),target_transform=Lambda(lambda y: torch.zeros(10, dtype=torch.float).scatter_(0, torch.tensor(y), value=1))
)
解释上述代码:
ToTensor()
ToTensor 将PIL图像或NumPy ndarray
转换为FloatTensor,并将图像的像素强度值缩放到范围 [0., 1.]内。
Lambda transforms
Lambda transforms 应用任何用户定义的lambda函数。在这里,我们定义了一个函数来将整数转换为一个one-hot编码的张量。它首先创建一个大小为10的零张量(数据集中标签的数量),然后调用scatter,根据标签y的索引为其分配值为1。也可以使用torch.nn.functional.one_hot
的方式来实现这一点。
构建模型层
4 什么是神经网络
神经网络是由神经元(neurons)
通过层连接而成的集合。每个神经元是一个小型计算单元,执行简单的计算以共同解决问题。神经元分布在三种类型的层中:输入层、隐藏层和输出层。隐藏层和输出层包含多个神经元。神经网络模仿人类大脑处理信息的方式。
一个层包含多个神经元,一个神经网络包含多层,
神经网络的组件
-
**激活函数(activation function)**决定神经元是否应该被激活。神经网络中的计算包括应用激活函数。如果一个神经元被激活,那么意味着输入是重要的。有不同类型的激活函数,选择使用哪种激活函数取决于您想要的输出是什么。激活函数的另一个重要作用是为模型添加非线性。
Binary
:输出节点被设置为1(如果函数结果为正)或0(如果函数结果为零或负)。 f ( x ) = { 0 , if x < 0 1 , if x ≥ 0 f(x)= {\small \begin{cases} 0, & \text{if } x < 0\\ 1, & \text{if } x\geq 0\\ \end{cases}} f(x)={0,1,if x<0if x≥0Sigmoid
:用于预测输出节点的概率在0到1之间。 f ( x ) = 1 1 + e − x f(x) = {\large \frac{1}{1+e^{-x}}} f(x)=1+e−x1Tanh
用于预测输出节点是否在1和-1之间,适用于分类用例。 f ( x ) = e x − e − x e x + e − x f(x) = {\large \frac{e^{x} - e^{-x}}{e^{x} + e^{-x}}} f(x)=ex+e−xex−e−xReLU
(修正线性激活函数)用于将输出节点设置为0(如果函数结果为负)并保持结果值(如果结果为正)。 f ( x ) = { 0 , if x < 0 x , if x ≥ 0 f(x)= {\small \begin{cases} 0, & \text{if } x < 0\\ x, & \text{if } x\geq 0\\ \end{cases}} f(x)={0,x,if x<0if x≥0
-
**权重(Weight)**影响网络输出与预期输出值之间的接近程度。当输入进入神经元时,它会乘以一个权重值,得到的输出结果要么被观察到,要么传递到神经网络中的下一层。
-
一个层中所有神经元的权重被组织成一个张量。
-
偏差构成了激活函数输出与其预期输出之间的差异。低偏差表明网络对输出形式做出了更多假设,而高偏差值则表示对输出形式做出了较少的假设。
可以说,具有权重 W W W和偏差 b b b的神经网络层的输出 y y y是输入乘以权重加上偏差的总和。$x = \sum{(weights * inputs) + bias} ,其中 ,其中 ,其中f(x)$是激活函数。
构建神经网络
神经网络由层和模块组成,这些模块对数据执行操作。torch.nn命名空间提供了构建自己的神经网络所需的所有构建块。PyTorch中的每个模块都是nn.Module的子类。神经网络本身是一个模块,它由其他模块(层)组成。这种嵌套结构使得可以轻松构建和管理复杂的架构。
在接下来的部分中,将构建一个神经网络来对FashionMNIST数据集中的图像进行分类。下面是需要使用到的类:
import os
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
取得训练的硬件设备
我们希望能够在类似GPU这样的硬件加速器上训练我们的模型,如果有的话。让我们检查一下torch.cuda
是否可用;如果不可用,我们将继续使用CPU。
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print('Using {} device'.format(device))
定义类
我们通过子类化 nn.Module
来定义我们的神经网络,并在 __init__
中初始化神经网络层。每个 nn.Module
子类在 forward
方法中实现对输入数据的操作。
我们的神经网络由以下部分组成:
- 输入层具有 28x28 或 784 个特征/像素。
- 第一个线性模块接受 784 个特征的输入,并将其转换为具有 512 个特征的隐藏层。
- 在转换中应用 ReLU 激活函数。
- 第二个线性模块接受来自第一个隐藏层的 512 个特征作为输入,并将其转换为具有 512 个特征的下一个隐藏层。
- 在转换中应用 ReLU 激活函数。
- 第三个线性模块接受来自第二个隐藏层的 512 个特征作为输入,并将这些特征转换为具有 10 个特征的输出层,这是类别的数量。
- 在转换中应用 ReLU 激活函数。
class NeuralNetwork(nn.Module):def __init__(self):super(NeuralNetwork, self).__init__()self.flatten = nn.Flatten()self.linear_relu_stack = nn.Sequential(nn.Linear(28*28, 512),nn.ReLU(),nn.Linear(512, 512),nn.ReLU(),nn.Linear(512, 10),nn.ReLU())def forward(self, x):x = self.flatten(x)logits = self.linear_relu_stack(x)return logits
下面,创建NeuralNetwork
的实例,并将其移到device
上并打印结构。
model = NeuralNetwork().to(device)
print(model)
输出为:
NeuralNetwork((flatten): Flatten()(linear_relu_stack): Sequential((0): Linear(in_features=784, out_features=512, bias=True)(1): ReLU()(2): Linear(in_features=512, out_features=512, bias=True)(3): ReLU()(4): Linear(in_features=512, out_features=10, bias=True)(5): ReLU())
)
要使用模型,需要将输入数据传递给它。这将执行模型的forward
,以及一些背景操作(background operation)。但是,请不要直接调用model.forward()
!在输入上调用模型会返回一个具有每个类别的原始预测值的10维张量。
模型的背景操作是指在执行前向传播(forward pass)时发生的内部计算或处理步骤。这些操作可能包括参数初始化、梯度计算、损失函数的计算、优化器的更新等。这些操作在模型的
forward
方法内部进行,通常是在将输入数据传递给模型时自动执行的,而不需要用户显式调用。这些操作的目的是在训练过程中优化模型参数,使其能够更好地拟合数据并提高性能。
我们通过将其传递给nn.Softmax
的实例来获取预测密度。
X = torch.rand(1, 28, 28, device=device)
logits = model(X)
pred_probab = nn.Softmax(dim=1)(logits)
y_pred = pred_probab.argmax(1)
print(f"Predicted class: {y_pred}")
输出是:
Predicted class: tensor([2], device='cuda:0')
权重与偏置
nn.Linear
模块随机初始化每一层的weight
和bias
并在内部将值存储在张量中。
print(f"First Linear weights: {model.linear_relu_stack[0].weight} \n")
print(f"First Linear biases: {model.linear_relu_stack[0].bias} \n")
模型层
让我们分解一下FashionMNIST模型中的层。为了说明这一点,我们将取一个大小为28x28的样本小批量,其中包含3张图像,然后看看当我们将其通过网络传递时会发生什么。
input_image = torch.rand(3,28,28)
print(input_image.size())
输出是:
torch.Size([3, 28, 28])
nn.Flatten
我们初始化nn.Flatten
层,将每个2D的28x28图像转换为一个连续的包含784个像素值的数组,即,小批量维度(在dim=0处)保持不变。每个像素都传递到神经网络的输入层。
flatten = nn.Flatten()
flat_image = flatten(input_image)
print(flat_image.size())
输出:
torch.Size([3, 784])
nn.Linear
线性层是一个模块,它使用其存储的权重和偏置对输入进行线性变换。输入层中每个像素的灰度值将连接到隐藏层中的神经元进行计算。用于变换的计算是 w e i g h t ∗ i n p u t + b i a s {{weight * input + bias}} weight∗input+bias。
layer1 = nn.Linear(in_features=28*28, out_features=20)
hidden1 = layer1(flat_image)
print(hidden1.size())
输出:
torch.Size([3, 20])
nn.ReLU
非线性激活函数是在模型的输入和输出之间创建复杂映射的关键。它们被应用在线性变换之后,引入非线性(nonlinearity),帮助神经网络学习各种现象。在这个模型中,我们在线性层之间使用nn.ReLU
,但还有其他激活函数可以引入非线性到模型中。
ReLU激活函数接收线性层计算的输出,并用零替换负值。
线性输出: ${ x = {weight * input + bias}} $.
ReLU:
f ( x ) = { 0 , if x < 0 x , if x ≥ 0 f(x)=\begin{cases} 0, & \text{if } x < 0\\ x, & \text{if } x\geq 0\\ \end{cases} f(x)={0,x,if x<0if x≥0
print(f"Before ReLU: {hidden1}\n\n")
hidden1 = nn.ReLU()(hidden1)
print(f"After ReLU: {hidden1}")
输出:
Before ReLU: tensor([[ 0.2190, 0.1448, -0.5783, 0.1782, -0.4481, -0.2782, -0.5680, 0.1347,0.1092, -0.7941, -0.2273, -0.4437, 0.0661, 0.2095, 0.1291, -0.4690,0.0358, 0.3173, -0.0259, -0.4028],[-0.3531, 0.2385, -0.3172, -0.4717, -0.0382, -0.2066, -0.3859, 0.2607,0.3626, -0.4838, -0.2132, -0.7623, -0.2285, 0.2409, -0.2195, -0.4452,-0.0609, 0.4035, -0.4889, -0.4500],[-0.3651, -0.1240, -0.3222, -0.1072, -0.0112, -0.0397, -0.4105, -0.0233,-0.0342, -0.5680, -0.4816, -0.8085, -0.3945, -0.0472, 0.0247, -0.3605,-0.0347, 0.1192, -0.2763, 0.1447]], grad_fn=<AddmmBackward>)After ReLU: tensor([[0.2190, 0.1448, 0.0000, 0.1782, 0.0000, 0.0000, 0.0000, 0.1347, 0.1092,0.0000, 0.0000, 0.0000, 0.0661, 0.2095, 0.1291, 0.0000, 0.0358, 0.3173,0.0000, 0.0000],[0.0000, 0.2385, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.2607, 0.3626,0.0000, 0.0000, 0.0000, 0.0000, 0.2409, 0.0000, 0.0000, 0.0000, 0.4035,0.0000, 0.0000],[0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0247, 0.0000, 0.0000, 0.1192,0.0000, 0.1447]], grad_fn=<ReluBackward0>)
nn.Sequential
nn.Sequential
是一个有序的模块容器。数据按照它们定义的顺序通过所有模块。您可以使用顺序容器来组合一个快速网络,如下所示seq_modules
。
seq_modules = nn.Sequential(flatten,layer1,nn.ReLU(),nn.Linear(20, 10)
)
input_image = torch.rand(3,28,28)
logits = seq_modules(input_image)
nn.Softmax
神经网络的最后一个线性层返回logits
(在[-infty
, infty
]范围内的原始值),这些值被传递给nn.Softmax
模块。Softmax激活函数用于计算神经网络输出的概率。它只用于神经网络的输出层。结果被缩放到[0,1]的值,表示模型对每个类别的预测密度。dim
参数指示结果值必须在哪个维度上求和为1。具有最高概率的节点预测所需的输出。
softmax = nn.Softmax(dim=1)
pred_probab = softmax(logits)
模型参数
神经网络中的许多层都是参数化的,即,这些层具有关联的权重和偏置,在训练期间进行优化。子类化nn.Module
会自动跟踪模型对象内部定义的所有字段,并使用模型的parameters()
或named_parameters()
方法使所有参数可访问。
在这个例子中,我们遍历每个参数,并打印其大小和值的预览。
print("Model structure: ", model, "\n\n")for name, param in model.named_parameters():print(f"Layer: {name} | Size: {param.size()} | Values : {param[:2]} \n")
输出是:
Model structure: NeuralNetwork((flatten): Flatten()(linear_relu_stack): Sequential((0): Linear(in_features=784, out_features=512, bias=True)(1): ReLU()(2): Linear(in_features=512, out_features=512, bias=True)(3): ReLU()(4): Linear(in_features=512, out_features=10, bias=True)(5): ReLU())
) Layer: linear_relu_stack.0.weight | Size: torch.Size([512, 784]) | Values : tensor([[-0.0320, 0.0326, -0.0032, ..., -0.0236, -0.0025, -0.0175],[ 0.0180, 0.0271, -0.0314, ..., -0.0094, -0.0170, -0.0257]],device='cuda:0', grad_fn=<SliceBackward>) Layer: linear_relu_stack.0.bias | Size: torch.Size([512]) | Values : tensor([-0.0134, 0.0036], device='cuda:0', grad_fn=<SliceBackward>) Layer: linear_relu_stack.2.weight | Size: torch.Size([512, 512]) | Values : tensor([[-0.0262, 0.0072, -0.0348, ..., -0.0374, 0.0345, 0.0374],[ 0.0439, -0.0101, 0.0218, ..., -0.0419, 0.0212, -0.0081]],device='cuda:0', grad_fn=<SliceBackward>) Layer: linear_relu_stack.2.bias | Size: torch.Size([512]) | Values : tensor([ 0.0131, -0.0289], device='cuda:0', grad_fn=<SliceBackward>) Layer: linear_relu_stack.4.weight | Size: torch.Size([10, 512]) | Values : tensor([[ 0.0376, -0.0359, -0.0329, ..., -0.0057, 0.0040, 0.0307],[-0.0196, -0.0440, 0.0250, ..., 0.0335, 0.0024, -0.0207]],device='cuda:0', grad_fn=<SliceBackward>) Layer: linear_relu_stack.4.bias | Size: torch.Size([10]) | Values : tensor([-0.0287, 0.0321], device='cuda:0', grad_fn=<SliceBackward>)
代码汇总
import os
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, transformsdevice = 'cuda' if torch.cuda.is_available() else 'cpu'
print('Using {} device'.format(device))class NeuralNetwork(nn.Module):def __init__(self):super(NeuralNetwork, self).__init__()self.flatten = nn.Flatten()self.linear_relu_stack = nn.Sequential(nn.Linear(28 * 28, 512),nn.ReLU(),nn.Linear(512, 512),nn.ReLU(),nn.Linear(512, 10),nn.ReLU())def forward(self, x):x = self.flatten(x)logits = self.linear_relu_stack(x)return logitsmodel = NeuralNetwork().to(device)
print(model)X = torch.rand(1, 28, 28, device=device)
logits = model(X)
pred_probab = nn.Softmax(dim=1)(logits)
y_pred = pred_probab.argmax(1)
print(f"Predicted class: {y_pred}")print(f"First Linear weights: {model.linear_relu_stack[0].weight} \n")print(f"First Linear biases: {model.linear_relu_stack[0].bias} \n")input_image = torch.rand(3, 28, 28)
print(input_image.size())flatten = nn.Flatten()
flat_image = flatten(input_image)
print(flat_image.size())layer1 = nn.Linear(in_features=28 * 28, out_features=20)
hidden1 = layer1(flat_image)
print(hidden1.size())print(f"Before ReLU: {hidden1}\n\n")
hidden1 = nn.ReLU()(hidden1)
print(f"After ReLU: {hidden1}")seq_modules = nn.Sequential(flatten,layer1,nn.ReLU(),nn.Linear(20, 10)
)
input_image = torch.rand(3, 28, 28)
logits = seq_modules(input_image)softmax = nn.Softmax(dim=1)
pred_probab = softmax(logits)print("Model structure: ", model, "\n\n")for name, param in model.named_parameters():print(f"Layer: {name} | Size: {param.size()} | Values : {param[:2]} \n")
5 自动微分
使用torch.autograd
自动微分 Automaic differentiation
在训练神经网络时,最常用的算法是反向传播(back propagation)。在这个算法中,参数(模型权重)根据损失函数相对于给定参数的梯度进行调整。损失函数(loss function)计算神经网络产生的预期输出和实际输出之间的差异。目标是使损失函数的结果尽可能接近零。该算法通过神经网络向后遍历以调整权重和偏差来重新训练模型。这就是为什么它被称为反向传播。随着时间的推移,通过反复进行这种回传和前向过程来将损失(loss)减少到0的过程称为梯度下降。
为了计算这些梯度,PyTorch具有一个内置的微分引擎,称为torch.autograd
。它支持对任何计算图进行梯度的自动计算。
考虑最简单的单层神经网络,具有输入x
,参数w
和b
,以及某些损失函数。可以在PyTorch中如下定义:
import torchx = torch.ones(5) # input tensor
y = torch.zeros(3) # expected output
w = torch.randn(5, 3, requires_grad=True)
b = torch.randn(3, requires_grad=True)
z = torch.matmul(x, w)+b # z = x*w +b
loss = torch.nn.functional.binary_cross_entropy_with_logits(z, y)
张量、函数与计算图(computational graphs)
在这个网络中,w
和b
是参数,他们会被损失函数优化。因此,需要能够计算损失函数相对于这些变量的梯度。为此,我们将这些张量的requires_grad
属性设置为True。
**注意:**您可以在创建张量时设置
requires_grad
的值,也可以稍后使用x.requires_grad_(True)
方法来设置。
我们将应用于张量的函数(function)用于构建计算图,这些函数是Function
类的对象。这个对象知道如何在前向方向上计算函数,还知道在反向传播步骤中如何计算其导数。反向传播函数
的引用存储在张量的grad_fn
属性中。
print('Gradient function for z =',z.grad_fn)
print('Gradient function for loss =', loss.grad_fn)
输出是:
Gradient function for z = <AddBackward0 object at 0x00000280CC630CA0>
Gradient function for loss = <BinaryCrossEntropyWithLogitsBackward object at 0x00000280CC630310>
计算梯度
为了优化神经网络中参数的权重,需要计算损失函数相对于参数的导数,即我们需要在某些固定的x
和y
值下计算 ∂ l o s s ∂ w \frac{\partial loss}{\partial w} ∂w∂loss和 ∂ l o s s ∂ b \frac{\partial loss}{\partial b} ∂b∂loss。为了计算这些导数,我们调用loss.backward()
,然后从w.grad
和b.grad
中获取值。
loss.backward()
print(w.grad)
print(b.grad)
输出是:
tensor([[0.2739, 0.0490, 0.3279],[0.2739, 0.0490, 0.3279],[0.2739, 0.0490, 0.3279],[0.2739, 0.0490, 0.3279],[0.2739, 0.0490, 0.3279]])
tensor([0.2739, 0.0490, 0.3279])
注意: 只能获取计算图中设置了
requires_grad
属性为True
的叶节点的grad
属性。对于计算图中的所有其他节点,梯度将不可用。此外,出于性能原因,我们只能对给定图执行一次backward
调用以进行梯度计算。如果我们需要在同一图上进行多次backward
调用,我们需要在backward
调用中传递retain_graph=True
。
禁用梯度追踪 Disabling gradient tracking
默认情况下,所有requires_grad=True
的张量都在跟踪其计算历史并支持梯度计算。然而,在某些情况下,我们并不需要这样做,例如,当我们已经训练好模型并且只想将其应用于一些输入数据时,也就是说,我们只想通过网络进行前向计算。我们可以通过将我们的计算代码放在一个torch.no_grad()
块中来停止跟踪计算:
z = torch.matmul(x, w)+b
print(z.requires_grad)with torch.no_grad():z = torch.matmul(x, w)+b
print(z.requires_grad)
输出是:
True
False
另外一种产生相同结果的方法是在张量上使用detach
方法:
z = torch.matmul(x, w)+b
z_det = z.detach()
print(z_det.requires_grad)
有一些理由你可能想要禁用梯度跟踪:
- 将神经网络中的某些参数标记为冻结参数(frozen parameters)。这在微调预训练网络的情况下非常常见。
- 当你只进行前向传播时,为了加速计算,因为不跟踪梯度的张量上的计算更有效率。
计算图的更多知识
概念上,autograd 在一个有向无环图 (DAG) 中保留了数据(张量)和所有执行的操作(以及生成的新张量),这些操作由 Function 对象组成。在这个 DAG 中,叶子节点是输入张量,根节点是输出张量。通过从根节点到叶子节点追踪这个图,你可以使用链式法则(chain rule)自动计算梯度。
在前向传播中,autograd 同时执行两件事情:
- 运行所请求的操作以计算结果张量,并且
- 在 DAG 中维护操作的 梯度函数(gradient function)。
当在 DAG 根节点上调用 .backward()
时,反向传播开始。autograd
然后:
- 从每个
.grad_fn
计算梯度, - 将它们累积在相应张量的
.grad
属性中,并且 - 使用链式法则一直传播到叶子张量。
PyTorch 中的 DAG 是动态的
一个重要的事情要注意的是,图是从头开始重新创建的;在每次 .backward()
调用之后,autograd 开始填充一个新的图。这正是允许您在模型中使用控制流语句的原因;如果需要,您可以在每次迭代中更改形状、大小和操作。
代码汇总:
import torchx = torch.ones(5) # input tensor
y = torch.zeros(3) # expected output
w = torch.randn(5, 3, requires_grad=True)
b = torch.randn(3, requires_grad=True)
z = torch.matmul(x, w) + b
loss = torch.nn.functional.binary_cross_entropy_with_logits(z, y)print('Gradient function for z =', z.grad_fn)
print('Gradient function for loss =', loss.grad_fn)loss.backward()
print(w.grad)
print(b.grad)z = torch.matmul(x, w) + b
print(z.requires_grad)with torch.no_grad():z = torch.matmul(x, w) + b
print(z.requires_grad)z = torch.matmul(x, w) + b
z_det = z.detach()
print(z_det.requires_grad)
6 优化循环 optimization loop
现在有了一个模型和数据,是时候通过优化其参数来训练、验证和测试我们的模型了。训练模型是一个迭代的过程;在每个迭代(周期)中,模型对输出进行猜测,计算其猜测的错误(损失),收集关于其参数的错误的导数(如前面教程所示),并使用梯度下降(gradient descent)优化这些参数。
之前教程的代码
这里加载之前教程的代码,包括加载数据集和模型构建,如下所示:
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import ToTensor, Lambdatraining_data = datasets.FashionMNIST(root="data",train=True,download=True,transform=ToTensor()
)test_data = datasets.FashionMNIST(root="data",train=False,download=True,transform=ToTensor()
)train_dataloader = DataLoader(training_data, batch_size=64)
test_dataloader = DataLoader(test_data, batch_size=64)class NeuralNetwork(nn.Module):def __init__(self):super(NeuralNetwork, self).__init__()self.flatten = nn.Flatten()self.linear_relu_stack = nn.Sequential(nn.Linear(28*28, 512),nn.ReLU(),nn.Linear(512, 512),nn.ReLU(),nn.Linear(512, 10),nn.ReLU())def forward(self, x):x = self.flatten(x)logits = self.linear_relu_stack(x)return logitsmodel = NeuralNetwork()
设置超参数
超参数是可调参数,用于控制模型优化过程。不同的超参数值可以影响模型训练和精度水平。
我们定义以下用于训练的超参数:
- Epochs数量 - 整个训练数据集通过网络传递的次数。
- Batch Size - 模型在每个周期中看到的数据样本数量。迭代完成一个周期所需的批次数量。
- 学习率(learning rate) - 模型在搜索产生更高模型准确度的最佳权重时所匹配的步长大小。较小的值意味着模型需要更长的时间来找到最佳权重。较大的值可能导致模型跨越并错过最佳权重,从而在训练过程中产生不可预测的行为。
learning_rate = 1e-3
batch_size = 64
epochs = 5
添加一个优化循环
一旦设置了超参数,就可以使用优化循环来训练和优化我们的模型。优化循环的每个迭代称为一个epoch。
每个周期包括两个主要部分:
- 训练循环 - 迭代训练数据集并尝试收敛到最优参数。
- 验证/测试循环 - 迭代测试数据集以检查模型性能是否在提高。
让我们来研究一下训练循环中使用的一些概念。在前一教程给出了优化循环的完整实现。
添加损失函数
当给定一些训练数据时,未训练网络很可能不能给出正确答案。损失函数衡量了获取的结果与目标值之间的不相似程度,而在训练过程中希望最小化的就是这个损失函数。为了计算损失,可以使用给定数据样本的输入进行预测,然后将其与真实数据标签值进行比较。
常见的损失函数包括:
nn.MSELoss
(均方误差),用于回归任务nn.NLLLoss
(负对数似然),用于分类任务nn.CrossEntropyLoss
(交叉熵损失),结合了nn.LogSoftmax
和nn.NLLLoss
将模型的输出 logits 传递给 nn.CrossEntropyLoss
,它将对 logits 进行归一化并计算预测误差。
# Initialize the loss function
loss_fn = nn.CrossEntropyLoss()
优化传入 optimization pass
优化是调整模型参数以减少每个训练步骤中模型误差的过程。优化算法定义了这个过程是如何执行的(本示例使用随机梯度下降(Stochastic Gradient Descent))。 所有优化逻辑都封装在optimizer
对象中。这里使用SGD优化器;PyTorch中还有许多不同的优化器,如ADAM
和RMSProp
,它们适用于不同类型的模型和数据。
我们通过注册需要训练的模型参数,并传入学习率超参数来初始化优化器。
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
在训练循环中,优化过程分为三个步骤:
- 调用
optimizer.zero_grad()
来重置模型参数的梯度。梯度默认会累加;为了防止重复计数,我们在每次迭代时明确将其归零。 - 使用
loss.backward()
进行预测损失的反向传播。PyTorch 会将损失相对于每个参数的梯度存储起来。 - 一旦有了梯度,就调用
optimizer.step()
来根据反向传播收集到的梯度来调整参数。
完整实现
我们定义一个 train_loop 函数,该函数循环执行我们的优化代码,以及一个 test_loop 函数,该函数评估模型在测试数据上的性能。
def train_loop(dataloader, model, loss_fn, optimizer):size = len(dataloader.dataset)for batch, (X, y) in enumerate(dataloader): # Compute prediction and losspred = model(X)loss = loss_fn(pred, y)# Backpropagationoptimizer.zero_grad()loss.backward()optimizer.step()if batch % 100 == 0:loss, current = loss.item(), batch * len(X)print(f"loss: {loss:>7f} [{current:>5d}/{size:>5d}]")def test_loop(dataloader, model, loss_fn):size = len(dataloader.dataset)test_loss, correct = 0, 0with torch.no_grad():for X, y in dataloader:pred = model(X)test_loss += loss_fn(pred, y).item()correct += (pred.argmax(1) == y).type(torch.float).sum().item()test_loss /= sizecorrect /= sizeprint(f"Test Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")
此处初始化损失函数和优化器,并将其传递给train_loop
和test_loop
。 随意增加周期数以跟踪模型性能的改善。
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)epochs = 10
for t in range(epochs):print(f"Epoch {t+1}\n-------------------------------")train_loop(train_dataloader, model, loss_fn, optimizer)test_loop(test_dataloader, model, loss_fn)
print("Done!")
输出是:
Epoch 1
-------------------------------
loss: 2.301450 [ 0/60000]
loss: 2.290315 [ 6400/60000]
loss: 2.276943 [12800/60000]
loss: 2.274104 [19200/60000]
loss: 2.265006 [25600/60000]
loss: 2.258067 [32000/60000]
loss: 2.241945 [38400/60000]
loss: 2.234483 [44800/60000]
loss: 2.235555 [51200/60000]
loss: 2.214317 [57600/60000]
Test Error: Accuracy: 37.2%, Avg loss: 0.034860 Epoch 2
-------------------------------
loss: 2.216829 [ 0/60000]
loss: 2.212970 [ 6400/60000]
loss: 2.179734 [12800/60000]
loss: 2.192833 [19200/60000]
loss: 2.164904 [25600/60000]
loss: 2.167122 [32000/60000]
loss: 2.133708 [38400/60000]
loss: 2.123012 [44800/60000]
loss: 2.132578 [51200/60000]
loss: 2.087934 [57600/60000]
Test Error: Accuracy: 38.6%, Avg loss: 0.033089 Epoch 3
-------------------------------
loss: 2.102114 [ 0/60000]
loss: 2.099246 [ 6400/60000]
loss: 2.033731 [12800/60000]
loss: 2.066001 [19200/60000]
loss: 2.012894 [25600/60000]
loss: 2.030210 [32000/60000]
loss: 1.971226 [38400/60000]
loss: 1.965057 [44800/60000]
loss: 1.990206 [51200/60000]
loss: 1.912654 [57600/60000]
Test Error: Accuracy: 38.9%, Avg loss: 0.030683 Epoch 4
-------------------------------
loss: 1.951076 [ 0/60000]
loss: 1.954955 [ 6400/60000]
loss: 1.850126 [12800/60000]
loss: 1.891714 [19200/60000]
loss: 1.838581 [25600/60000]
loss: 1.836865 [32000/60000]
loss: 1.803531 [38400/60000]
loss: 1.791677 [44800/60000]
loss: 1.788134 [51200/60000]
loss: 1.659078 [57600/60000]
Test Error: Accuracy: 44.9%, Avg loss: 0.027540 Epoch 5
-------------------------------
loss: 1.766038 [ 0/60000]
loss: 1.781725 [ 6400/60000]
loss: 1.659930 [12800/60000]
loss: 1.729604 [19200/60000]
loss: 1.598470 [25600/60000]
loss: 1.646501 [32000/60000]
loss: 1.615664 [38400/60000]
loss: 1.610186 [44800/60000]
loss: 1.593459 [51200/60000]
loss: 1.492508 [57600/60000]
Test Error: Accuracy: 50.8%, Avg loss: 0.025097 Epoch 6
-------------------------------
loss: 1.593179 [ 0/60000]
loss: 1.627773 [ 6400/60000]
loss: 1.514483 [12800/60000]
loss: 1.623364 [19200/60000]
loss: 1.433579 [25600/60000]
loss: 1.532538 [32000/60000]
loss: 1.498388 [38400/60000]
loss: 1.499874 [44800/60000]
loss: 1.474976 [51200/60000]
loss: 1.393706 [57600/60000]
Test Error: Accuracy: 53.0%, Avg loss: 0.023498 Epoch 7
-------------------------------
loss: 1.475844 [ 0/60000]
loss: 1.525665 [ 6400/60000]
loss: 1.406005 [12800/60000]
loss: 1.548480 [19200/60000]
loss: 1.324354 [25600/60000]
loss: 1.450784 [32000/60000]
loss: 1.411867 [38400/60000]
loss: 1.415346 [44800/60000]
loss: 1.389313 [51200/60000]
loss: 1.321387 [57600/60000]
Test Error: Accuracy: 54.2%, Avg loss: 0.022255 Epoch 8
-------------------------------
loss: 1.383708 [ 0/60000]
loss: 1.444837 [ 6400/60000]
loss: 1.316864 [12800/60000]
loss: 1.489333 [19200/60000]
loss: 1.245268 [25600/60000]
loss: 1.384567 [32000/60000]
loss: 1.343524 [38400/60000]
loss: 1.346866 [44800/60000]
loss: 1.321173 [51200/60000]
loss: 1.261818 [57600/60000]
Test Error: Accuracy: 55.5%, Avg loss: 0.021229 Epoch 9
-------------------------------
loss: 1.308065 [ 0/60000]
loss: 1.376466 [ 6400/60000]
loss: 1.239852 [12800/60000]
loss: 1.438008 [19200/60000]
loss: 1.186274 [25600/60000]
loss: 1.328856 [32000/60000]
loss: 1.289778 [38400/60000]
loss: 1.292222 [44800/60000]
loss: 1.265508 [51200/60000]
loss: 1.211885 [57600/60000]
Test Error: Accuracy: 56.5%, Avg loss: 0.020382 Epoch 10
-------------------------------
loss: 1.244786 [ 0/60000]
loss: 1.317880 [ 6400/60000]
loss: 1.174213 [12800/60000]
loss: 1.393624 [19200/60000]
loss: 1.140713 [25600/60000]
loss: 1.282823 [32000/60000]
loss: 1.248486 [38400/60000]
loss: 1.249433 [44800/60000]
loss: 1.220410 [51200/60000]
loss: 1.170350 [57600/60000]
Test Error: Accuracy: 57.4%, Avg loss: 0.019699 Done!
Saved PyTorch Model State to model1.pth
你可能已经注意到,模型最初的表现并不是很好(这没关系!)。尝试增加更多的epochs
或调整learning_rate
到更大的数字。也可能是我们选择的模型配置对这种问题来说不是最佳的(确实不是)。
保存模型
当你对模型的性能满意时,你可以使用torch.save
来保存它。PyTorch模型将学到的参数存储在一个内部状态字典中,称为state_dict
。这些可以使用torch.save
方法持久化保存:
torch.save(model.state_dict(), "data/model.pth")print("Saved PyTorch Model State to model.pth")
代码汇总:
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import ToTensor, Lambdatraining_data = datasets.FashionMNIST(root="data",train=True,download=True,transform=ToTensor()
)test_data = datasets.FashionMNIST(root="data",train=False,download=True,transform=ToTensor()
)train_dataloader = DataLoader(training_data, batch_size=64)
test_dataloader = DataLoader(test_data, batch_size=64)class NeuralNetwork(nn.Module):def __init__(self):super(NeuralNetwork, self).__init__()self.flatten = nn.Flatten()self.linear_relu_stack = nn.Sequential(nn.Linear(28 * 28, 512),nn.ReLU(),nn.Linear(512, 512),nn.ReLU(),nn.Linear(512, 10),nn.ReLU())def forward(self, x):x = self.flatten(x)logits = self.linear_relu_stack(x)return logitsmodel = NeuralNetwork()learning_rate = 1e-3
batch_size = 64
epochs = 5# Initialize the loss function
loss_fn = nn.CrossEntropyLoss()optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)def train_loop(dataloader, model, loss_fn, optimizer):size = len(dataloader.dataset)for batch, (X, y) in enumerate(dataloader):# Compute prediction and losspred = model(X)loss = loss_fn(pred, y)# Backpropagationoptimizer.zero_grad()loss.backward()optimizer.step()if batch % 100 == 0:loss, current = loss.item(), batch * len(X)print(f"loss: {loss:>7f} [{current:>5d}/{size:>5d}]")def test_loop(dataloader, model, loss_fn):size = len(dataloader.dataset)test_loss, correct = 0, 0with torch.no_grad():for X, y in dataloader:pred = model(X)test_loss += loss_fn(pred, y).item()correct += (pred.argmax(1) == y).type(torch.float).sum().item()test_loss /= sizecorrect /= sizeprint(f"Test Error: \n Accuracy: {(100 * correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)epochs = 10
for t in range(epochs):print(f"Epoch {t + 1}\n-------------------------------")train_loop(train_dataloader, model, loss_fn, optimizer)test_loop(test_dataloader, model, loss_fn)
print("Done!")torch.save(model.state_dict(), "data/model1.pth")print("Saved PyTorch Model State to model1.pth")
7 加载和运行模型预测
加载模型
本单元将介绍如何加载一个带有持久化参数状态和推断模型预测的模型。下面是本教程使用的类:
import torch
import onnxruntime
from torch import nn
import torch.onnx as onnx
import torchvision.models as models
from torchvision import datasets
from torchvision.transforms import ToTensor
为了加载模型,需要定义模型类,其中包含用于训练模型的神经网络的状态和参数。
class NeuralNetwork(nn.Module):def __init__(self):super(NeuralNetwork, self).__init__()self.flatten = nn.Flatten()self.linear_relu_stack = nn.Sequential(nn.Linear(28*28, 512),nn.ReLU(),nn.Linear(512, 512),nn.ReLU(),nn.Linear(512, 10),nn.ReLU())def forward(self, x):x = self.flatten(x)logits = self.linear_relu_stack(x)return logits
在加载模型权重时,需要首先实例化模型类,因为类定义了网络的结构。接下来,使用load_state_dict()
方法加载参数。
model = NeuralNetwork()
model.load_state_dict(torch.load('data/model.pth'))
model.eval()
**注意:**在进行推理之前,请确保调用
model.eval()
方法,将dropout和批量归一化层(batch normalization layers)设置为评估模式(evaluation mode)。否则,您将看到不一致的推理结果。
模型推理
将神经网络模型优化的在各种平台和编程语言上运行是很困难的。在不同的框架和硬件组合中最大化性能非常耗时。**ONNX(Open Neural Network Exchange)**运行时为您提供了一种解决方案,您可以在任何硬件、云或边缘设备上训练一次并加速推理。
ONNX是一种通用格式,受多个供应商支持,用于共享神经网络和其他机器学习模型。您可以使用ONNX格式在其他编程语言和框架上进行推理,例如Java、JavaScript、C#和ML.NET。
输出模型到ONNX格式
PyTorch也具有原生的ONNX导出支持。然而,由于PyTorch执行图的动态性质,导出过程必须遍历执行图以产生持久化的ONNX模型。因此,在导出过程中应传入一个适当大小的测试变量(在我们的情况下,我们将创建一个正确大小的零张量作为虚拟数据。您可以使用shape
函数在训练数据集上获取大小,如tensor.shape
):
input_image = torch.zeros((1,28,28))
onnx_model = 'data/model.onnx'
onnx.export(model, input_image, onnx_model)
此处使用测试数据集作为样本数据,从ONNX模型进行推理以进行预测。
test_data = datasets.FashionMNIST(root="data",train=False,download=True,transform=ToTensor()
)classes = ["T-shirt/top","Trouser","Pullover","Dress","Coat","Sandal","Shirt","Sneaker","Bag","Ankle boot",
]
x, y = test_data[0][0], test_data[0][1]
可以使用onnxruntime.InferenceSession
创建一个推理会话。要推理ONNX模型,调用run并传入想要返回的输出列表(如果想要全部输出,则留空)和输入值的映射。结果是一个输出列表。
session = onnxruntime.InferenceSession(onnx_model, None)
input_name = session.get_inputs()[0].name
output_name = session.get_outputs()[0].nameresult = session.run([output_name], {input_name: x.numpy()})
predicted, actual = classes[result[0][0].argmax(0)], classes[y]
print(f'Predicted: "{predicted}", Actual: "{actual}"')
输出是:
Predicted: "Ankle boot", Actual: "Ankle boot"
ONNX模型使您能够在不同的平台和不同的编程语言中运行推理。
代码汇总
import torch
import onnxruntime
from torch import nn
import torch.onnx as onnx
import torchvision.models as models
from torchvision import datasets
from torchvision.transforms import ToTensorclass NeuralNetwork(nn.Module):def __init__(self):super(NeuralNetwork, self).__init__()self.flatten = nn.Flatten()self.linear_relu_stack = nn.Sequential(nn.Linear(28 * 28, 512),nn.ReLU(),nn.Linear(512, 512),nn.ReLU(),nn.Linear(512, 10),nn.ReLU())def forward(self, x):x = self.flatten(x)logits = self.linear_relu_stack(x)return logitsmodel = NeuralNetwork()
model.load_state_dict(torch.load('data/model1.pth'))
model.eval()input_image = torch.zeros((1, 28, 28))
onnx_model = 'data/model.onnx'
onnx.export(model, input_image, onnx_model)test_data = datasets.FashionMNIST(root="data",train=False,download=True,transform=ToTensor()
)classes = ["T-shirt/top","Trouser","Pullover","Dress","Coat","Sandal","Shirt","Sneaker","Bag","Ankle boot",
]
x, y = test_data[0][0], test_data[0][1]session = onnxruntime.InferenceSession(onnx_model, None)
input_name = session.get_inputs()[0].name
output_name = session.get_outputs()[0].nameresult = session.run([output_name], {input_name: x.numpy()})
predicted, actual = classes[result[0][0].argmax(0)], classes[y]
print(f'Predicted: "{predicted}", Actual: "{actual}"')
8 总结
在本专题中,我们介绍了使用神经网络构建机器学习模型的关键概念,并使用PyTorch实现了这些概念。我们构建了一个图像识别模型,可以对图像进行分类,例如:T恤/上衣、裤子、套衫、连衣裙、外套、凉鞋、衬衫、运动鞋、包包和短靴。
您学习了以下关键领域:
- 如何在CPU和GPU上使用张量
- 如何管理、缩放和规范化数据集
- 如何使用神经网络构建模型
- 如何优化模型
- 如何优化模型推理