PyTorch深度学习实战(3)——使用PyTorch构建神经网络
- 0. 前言
- 1. PyTorch 构建神经网络初体验
- 1.1 使用 PyTorch 构建神经网络
- 1.2 神经网络数据加载
- 1.3 模型测试
- 1.4 获取中间层的值
- 2. 使用 Sequential 类构建神经网络
- 3. PyTorch 模型的保存和加载
- 3.1 模型保存所需组件
- 3.2 模型状态
- 3.3 模型保存
- 3.4 模型加载
- 小结
- 系列链接
0. 前言
我们已经学习了如何从零开始构建神经网络,神经网络通常包括输入层、隐藏层、输出层、激活函数、损失函数和学习率等基本组件。在本节中,我们将学习如何在简单数据集上使用 PyTorch
构建神经网络,利用张量对象操作和梯度值计算更新网络权重。
1. PyTorch 构建神经网络初体验
1.1 使用 PyTorch 构建神经网络
为了介绍如何使用 PyTorch
构建神经网络,我们将尝试解决两个数字的相加问题。
(1) 初始化数据集,定义输入 (x
) 和输出 (y
) 值:
import torch
x = [[1,2],[3,4],[5,6],[7,8]]
y = [[3],[7],[11],[15]]
在初始化的输入和输出变量中,输入中的每个列表的值之和就是输出列表中对应的值。
(2) 将输入列表转换为张量对象:
X = torch.tensor(x).float()
Y = torch.tensor(y).float()
在以上代码中,将张量对象转换为浮点对象。此外,将输入 ( X
) 和输出 ( Y
) 数据点注册到 device
中:
device = 'cuda' if torch.cuda.is_available() else 'cpu'
X = X.to(device)
Y = Y.to(device)
(3) 定义神经网络架构。
导入 torch.nn
模块用于构建神经网络模型:
from torch import nn
创建神经网络架构类 MyNeuralNet
,继承自 nn.Module
,nn.Module
是所有神经网络模块的基类:
class MyNeuralNet(nn.Module):
在类中,使用 __init__
方法初始化神经网络的所有组件,调用 super().__init__()
确保类继承 nn.Module
:
def __init__(self):
super().__init__()
使用以上代码,通过指定 super().__init__()
可以利用为 nn.Module
编写的所有预构建函数,初始化的组件将用于 MyNeuralNet
类中的不同方法。
定义神经网络中的网络层:
self.input_to_hidden_layer = nn.Linear(2,8)
self.hidden_layer_activation = nn.ReLU()
self.hidden_to_output_layer = nn.Linear(8,1)
在以上代码中,指定了神经网络的所有层——一个全连接层 (self.input_to_hidden_layer
),使用 ReLU
激活函数 (self.hidden_layer_activation
),最后仍是一个全连接层 (self.hidden_to_output_layer
)。
将初始化后的神经网络组件连接在一起,并定义网络的前向传播方法 forward
:
def forward(self, x):
x = self.input_to_hidden_layer(x)
x = self.hidden_layer_activation(x)
x = self.hidden_to_output_layer(x)
return x
必须使用 forward
作为前向传播的函数名,因为 PyTorch
保留此函数作为执行前向传播的方法,使用其他名称都会引发错误。
通过打印 nn.Linear
方法的输出了解函数作用:
print(nn.Linear(2, 7))
# Linear(in_features=2, out_features=8, bias=True)
在以上代码中,全连接层以 2
个值作为输入并输出 7
个值,且包含与之关联的偏置参数。
(4) 通过执行以下代码访问每个神经网络组件的初始权重。
创建 MyNeuralNet
类对象的实例并将其注册到 device
:
mynet = MyNeuralNet().to(device)
可以通过类似代码访问每一层的权重和偏置:
print(mynet.input_to_hidden_layer.weight)
代码输出结果如下:
Parameter containing:
tensor([[ 0.0984, 0.3058],
[ 0.2913, -0.3629],
[ 0.0630, 0.6347],
[-0.5134, -0.2525],
[ 0.2315, 0.3591],
[ 0.1506, 0.1106],
[ 0.2941, -0.0094],
[-0.0770, -0.4165]], device='cuda:0', requires_grad=True)
每次执行时输出的值并不相同,因为神经网络每次都使用随机值进行初始化。如果希望每次在执行相同代码时保持相同输出,则需要在创建类对象的实例之前使用 Torch
中的 manual_seed
方法指定随机种子 torch.manual_seed(0)
。
(5) 可以通过以下代码获得神经网络的所有参数:
mynet.parameters()
以上代码会返回一个生成器对象,最后通过生成器循环获取参数:
for param in mynet.parameters():
print(param)
代码输出结果如下:
Parameter containing:
tensor([[ 0.2955, 0.3231],
[ 0.5153, 0.1734],
[-0.6359, -0.1406],
[ 0.3820, -0.1085],
[ 0.2816, -0.2283],
[ 0.4633, 0.6564],
[-0.1605, -0.4450],
[ 0.0788, -0.0147]], device='cuda:0', requires_grad=True)
Parameter containing:
tensor([[ 0.2955, 0.3231],
[ 0.5153, 0.1734],
[-0.6359, -0.1406],
[ 0.3820, -0.1085],
[ 0.2816, -0.2283],
[ 0.4633, 0.6564],
[-0.1605, -0.4450],
[ 0.0788, -0.0147]], device='cuda:0', requires_grad=True)
Parameter containing:
tensor([-0.4761, 0.6370, 0.6744, -0.4103, -0.3714, 0.1491, -0.2304, 0.5571],
device='cuda:0', requires_grad=True)
Parameter containing:
tensor([[-0.0440, 0.0028, 0.3024, 0.1915, 0.1426, -0.2859, -0.2398, -0.2134]],
device='cuda:0', requires_grad=True)
Parameter containing:
tensor([-0.3238], device='cuda:0', requires_grad=True)
该模型已将这些张量注册为跟踪前向和反向传播所必需的特殊对象,在 __init__
方法中定义 nn
神经网络层时,它会自动创建相应的张量并同时进行注册,也可以使用 nn.parameter(<tensor>)
函数手动注册这些参数。因此,本节定义的神经网络类 myNeuralNet
等价于以下代码:
class MyNeuralNet(nn.Module):
def __init__(self):
super().__init__()
self.input_to_hidden_layer = nn.parameter(torch.rand(2,8))
self.hidden_layer_activation = nn.ReLU()
self.hidden_to_output_layer = nn.parameter(torch.rand(8,1))
def forward(self, x):
x = x @ self.input_to_hidden_layer
x = self.hidden_layer_activation(x)
x = x @ self.hidden_to_output_layer
return x
(6) 定义损失函数,由于需要预测连续输出,因此使用均方误差作为损失函数:
loss_func = nn.MSELoss()
通过将输入值传递给神经网络对象,然后计算给定输入的损失函数值:
_Y = mynet(X)
loss_value = loss_func(_Y,Y)
print(loss_value)
# tensor(127.4498, device='cuda:0', grad_fn=<MseLossBackward>)
在以上代码中,mynet(X)
根据给定输入通过神经网络计算输出值,loss_func
函数计算对应于神经网络预测 (_Y
) 和实际值 (Y
) 的 MSELoss
值。需要注意的是,根据 PyTorch
约定,在计算损失时,我们总是首先传入预测结果,然后传入实际标记值。
(7) 定义用于降低损失值的优化器,优化器的输入是与神经网络相对应的参数(权重和偏差)以及更新权重时的学习率。本节,我们使用随机梯度下降 (Stochastic Gradient Descent
, SGD
)。从 torch.optim
模块中导入 SGD
方法,然后将神经网络对象 (mynet
) 和学习率 (lr
) 作为参数传递给 SGD
方法:
from torch.optim import SGD
opt = SGD(mynet.parameters(), lr = 0.001)
(8) 一个 epoch
训练过程包含以下步骤:
- 计算给定输入和输出对应的损失值
- 计算参数对应的梯度
- 根据参数的学习率和梯度更新权重
- 更新权重后,确保在下一个
epoch
计算梯度之前刷新上一步中计算的梯度
opt.zero_grad()
loss_value = loss_func(mynet(X),Y)
loss_value.backward()
opt.step()
使用 for
循环重复执行上述步骤。在以下代码中,执行 50
个 epoch
,此外,在 loss_history
列表中存储每个 epoch
中的损失值:
loss_history = []
for _ in range(50):
opt.zero_grad()
loss_value = loss_func(mynet(X),Y)
loss_value.backward()
opt.step()
loss_history.append(loss_value)
绘制损失随 epoch
的变化情况:
import matplotlib.pyplot as plt
plt.plot(loss_history)
plt.title('Loss variation over increasing epochs')
plt.xlabel('epochs')
plt.ylabel('loss value')
plt.show()
1.2 神经网络数据加载
批大小 (batch size
) 是神经网络中的重要超参数,批大小是指计算损失值或更新权重时考虑的数据样本数。假设数据集中有数百万个数据样本,一次将所有数据点用于一次权重更新并非最佳选择,因为内存可能无法容纳如此多数据。使用抽样样本可以充分代表数据,批大小可以用于获取具有足够代表性的多个数据样本。在本节中,我们指定计算权重梯度时要考虑的批大小以更新权重,然后计算更新后的损失值。
(1) 导入用于加载数据和处理数据集的方法:
from torch.utils.data import Dataset, DataLoader
import torch
import torch.nn as nn
(2) 导入数据,将数据转换为浮点数,并将它们注册到相应设备中:
x = [[1,2],[3,4],[5,6],[7,8]]
y = [[3],[7],[11],[15]]
X = torch.tensor(x).float()
Y = torch.tensor(y).float()
device = 'cuda' if torch.cuda.is_available() else 'cpu'
X = X.to(device)
Y = Y.to(device)
(3) 创建数据集类 MyDataset
:
class MyDataset(Dataset):
在 MyDataset
类中,存储数据信息以便可以将一批 (batch
) 数据点捆绑在一起(使用 DataLoader
),并通过一次前向和反向传播更新权重。
定义 __init__
方法,该方法接受输入和输出对并将它们转换为 Torch
浮点对象:
def __init__(self,x,y):
self.x = x.clone().detach() # torch.tensor(x).float()
self.y = y.clone().detach() # torch.tensor(y).float()
指定输入数据集的长度 (__len__
):
def __len__(self):
return len(self.x)
__getitem__
方法用于获取指定数据样本:
def __getitem__(self, ix):
return self.x[ix], self.y[ix]
在以上代码中,ix
表示要从数据集中获取的数据索引。
(4) 创建自定义类的实例:
ds = MyDataset(X, Y)
(5) 通过 DataLoader
传递数据集实例,从原始输入输出张量对象中获取 batch_size
个数据点:
dl = DataLoader(ds, batch_size=2, shuffle=True)
在以上代码中,指定从原始输入数据集 (ds
) 中获取两个 (batch_size=2
) 随机样本 (shuffle=True
)数据点。
循环遍历 dl
获取批数据信息:
for x, y in dl:
print(x, y)
输出结果如下所示:
tensor([[3., 4.],
[5., 6.]], device='cuda:0') tensor([[ 7.],
[11.]], device='cuda:0')
tensor([[1., 2.],
[7., 8.]], device='cuda:0') tensor([[ 3.],
[15.]], device='cuda:0')
可以看到以上代码生成了两组输入-输出
对,因为原始数据集中共有 4
个数据点,而指定的批大小为 2
。
(6) 定义神经网络类:
class MyNeuralNet(nn.Module):
def __init__(self):
super().__init__()
self.input_to_hidden_layer = nn.Linear(2,8)
self.hidden_layer_activation = nn.ReLU()
self.hidden_to_output_layer = nn.Linear(8,1)
def forward(self, x):
x = self.input_to_hidden_layer(x)
x = self.hidden_layer_activation(x)
x = self.hidden_to_output_layer(x)
return x
(7) 定义模型对象 (mynet
)、损失函数 (loss_func
) 和优化器 (opt
):
mynet = MyNeuralNet().to(device)
loss_func = nn.MSELoss()
from torch.optim import SGD
opt = SGD(mynet.parameters(), lr = 0.001)
(8) 最后,循环遍历批数据点以最小化损失值:
import time
loss_history = []
start = time.time()
for _ in range(50):
for data in dl:
x, y = data
opt.zero_grad()
loss_value = loss_func(mynet(x),y)
loss_value.backward()
opt.step()
loss_history.append(loss_value)
end = time.time()
print(end - start)
# 0.08548569679260254
虽然以上代码与上一小节中使用的代码非常相似,但与上一小节相比,每个 epoch
更新权重的次数变为原来的 2
倍,因为本节中使用的批大小为 2
,而上一小节中的批大小为 4
(即一次使用全部数据点)。
1.3 模型测试
在上一小节中,我们学习了如何在已知数据点上拟合模型。在本节中,我们将学习如何利用上一小节训练的 mynet
模型中定义的前向传播方法 forward
来预测模型没有见过的数据点(测试数据)。
(1) 创建用于测试模型的数据点:
val_x = [[10,11]]
新数据集 (val_x
) 与输入数据集相同,也是由列表数据组成的列表。
(2) 将新数据点转换为张量浮点对象并注册到 device
中:
val_x = torch.tensor(val_x).float().to(device)
(3) 通过训练好的神经网络 (mynet
) 传递张量对象,与通过模型执行前向传播的用法相同:
print(mynet(val_x))
# tensor([[20.0105]], device='cuda:0', grad_fn=<AddmmBackward>)
以上代码返回模型对输入数据点的预测输出值。
1.4 获取中间层的值
在实际应用中,我们可能需要获取神经网络的中间层值,例如风格迁移和迁移学习等,PyTorch
提供了两种方式获取神经网络中间值。
一种方法是直接调用神经网络层,将它们当做函数使用:
print(mynet.hidden_layer_activation(mynet.input_to_hidden_layer(X)))
需要注意的是,我们必须安装模型输入、输出顺序调用相应神经网络层,例如,在以上代码中 input_to_hidden_layer
的输出是 hidden_layer_activation
层的输入。
另一种方法是在 forward
方法中指定想要查看的网络层,虽然以下代码与上一小节中的 MyNeuralNet
类基本相同,但 forward
方法不仅返回输出,还返回激活后的隐藏层值 (hidden2
):
class MyNeuralNet(nn.Module):
def __init__(self):
super().__init__()
self.input_to_hidden_layer = nn.Linear(2,8)
self.hidden_layer_activation = nn.ReLU()
self.hidden_to_output_layer = nn.Linear(8,1)
def forward(self, x):
hidden1 = self.input_to_hidden_layer(x)
hidden2 = self.hidden_layer_activation(hidden1)
x = self.hidden_to_output_layer(hidden2)
return x, hidden2
通过使用以下代码访问隐藏层值,mynet
的第 0
个索引输出是网络前向传播的最终输出,而第 1
个索引输出是隐藏层激活后的值:
print(mynet(X)[1])
2. 使用 Sequential 类构建神经网络
我们已经学习了如何通过定义一个类来构建神经网络,在该类中定义了层以及层之间的连接方式。然而,除非需要构建一个复杂的网络,否则,只需要利用 Sequential
类并指定层和层堆叠的顺序即可搭建神经网络,本节继续使用简单数据集训练神经网络。
(1) 导入相关库并定义使用的设备:
import torch
import torch.nn as nn
import numpy as np
from torch.utils.data import Dataset, DataLoader
device = 'cuda' if torch.cuda.is_available() else 'cpu'
(2) 定义数据集与数据集类 (MyDataset
):
x = [[1,2],[3,4],[5,6],[7,8]]
y = [[3],[7],[11],[15]]
class MyDataset(Dataset):
def __init__(self, x, y):
self.x = torch.tensor(x).float().to(device)
self.y = torch.tensor(y).float().to(device)
def __getitem__(self, ix):
return self.x[ix], self.y[ix]
def __len__(self):
return len(self.x)
(3) 定义数据集 (ds
) 和数据加载器 (dl
) 对象:
ds = MyDataset(x, y)
dl = DataLoader(ds, batch_size=2, shuffle=True)
(4) 使用 nn
模块中 Sequential
类定义模型架构:
model = nn.Sequential(
nn.Linear(2, 8),
nn.ReLU(),
nn.Linear(8, 1)
).to(device)
在以上代码中,我们定义了与上小节相同的网络架构,nn.Linear
接受二维输入并为每个数据点提供八维输出,nn.ReLU
在八维输出之上执行 ReLU
激活,最后,使用 nn.Linear
接受八维输入并得到一维输出。
(5) 打印模型的摘要 (summary
),查看模型架构信息。
为了查看模型摘要,需要使用 pip
安装 torchsummary
库:
pip install torchsummary
安装完成后,导入库 torchsummary
:
from torchsummary import summary
打印模型摘要,函数接受模型名称及模型输入大小(需要使用整数元组)作为参数:
print(summary(model, (2,)))
输出结果如下所示:
----------------------------------------------------------------
Layer (type) Output Shape Param #
================================================================
Linear-1 [-1, 8] 24
ReLU-2 [-1, 8] 0
Linear-3 [-1, 1] 9
================================================================
Total params: 33
Trainable params: 33
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.00
Forward/backward pass size (MB): 0.00
Params size (MB): 0.00
Estimated Total Size (MB): 0.00
----------------------------------------------------------------
以第一层输出为例,其形状为 (-1, 8)
,其中 -1
表示 batch size
,8
表示对于每个数据点会得到一个 8
维输出,得到形状为 batch size x 8
的输出。
(6) 接下来,定义损失函数 (loss_func
) 和优化器 (opt
) 并训练模型:
loss_func = nn.MSELoss()
from torch.optim import SGD
opt = SGD(model.parameters(), lr = 0.001)
import time
loss_history = []
start = time.time()
for _ in range(50):
for ix, iy in dl:
opt.zero_grad()
loss_value = loss_func(model(ix),iy)
loss_value.backward()
opt.step()
loss_history.append(loss_value)
end = time.time()
print(end - start)
(7) 训练模型后,在验证数据集上预测值。
定义验证数据集:
val = [[8,9],[10,11],[1.5,2.5]]
将验证数据转换为浮点数,然后将它们转换为张量对象并将它们注册到 device
中,通过模型传递验证数据预测输出:
val = torch.tensor(val).float()
print(model(val.to(device)))
"""
tensor([[16.7774],
[20.6186],
[ 4.2415]], device='cuda:0', grad_fn=<AddmmBackward>)
"""
3. PyTorch 模型的保存和加载
神经网络模型处理的一个重要方面是在训练后保存和加载模型,保存模型后,我们可以利用已经训练好的模型中进行推断,只需要加载已经训练过的模型,而无需再次对其进行训练。
3.1 模型保存所需组件
首先了解保存神经网络模型所需的完整组件:
- 每个张量(参数)的唯一名称(键)
- 网络中张量间的连接的方式
- 每个张量的值(权重/偏置值)
第一个组件是在定义的 __init__
阶段处理的,而第二个组件是在前向计算方法定义期间处理的。默认情况下,张量中的值在 __init__
阶段随机初始化,但加载预训练模型时我们需要加载一组在训练模型时学习到的固定权重值,并将每个值与特定名称相关联。
3.2 模型状态
model.state_dict()
可以用于了解保存和加载 PyTorch
模型的工作原理,model.state_dict()
中的字典 (OrderedDict
) 对应于模型的参数名称(键)及其值(权重和偏置值),state
指的是模型的当前快照,返回的输出中,键是模型网络层的名称,值对应于这些层的权重:
print(model.state_dict())
"""
OrderedDict([('0.weight', tensor([[-0.4732, 0.1934],
[ 0.1475, -0.2335],
[-0.2586, 0.0823],
[-0.2979, -0.5979],
[ 0.2605, 0.2293],
[ 0.0566, 0.6848],
[-0.1116, -0.3301],
[ 0.0324, 0.2609]], device='cuda:0')), ('0.bias', tensor([ 0.6835, 0.2860, 0.1953, -0.2162, 0.5106, 0.3625, 0.1360, 0.2495],
device='cuda:0')), ('2.weight', tensor([[ 0.0475, 0.0664, -0.0167, -0.1608, -0.2412, -0.3332, -0.1607, -0.1857]],
device='cuda:0')), ('2.bias', tensor([0.2595], device='cuda:0'))])
"""
3.3 模型保存
使用 torch.save(model.state_dict(), 'mymodel.pth')
可以将模型以 Python
序列化格式保存在磁盘上,其中 mymodel.pth
表示文件名。在调用 torch.save
之前最好将模型传输到 CPU
中,将张量保存为 CPU
张量有助于将模型加载到任意机器上:
save_path = 'mymodel.pth'
torch.save(model.state_dict(), save_path)
3.4 模型加载
加载模型首先需要初始化模型,然后从 state_dict
中加载权重:
(1) 使用与训练时相同的代码创建一个空模型:
model = nn.Sequential(
nn.Linear(2, 8),
nn.ReLU(),
nn.Linear(8, 1)
).to(device)
(2) 从磁盘加载模型并反序列化以创建一个 OrderedDict
值:
state_dict = torch.load('mymodel.pth')
(3) 加载 state_dict
到模型中,并将其注册到 device
中,执行预测任务:
model.load_state_dict(state_dict)
model.to(device)
val = [[8,9],[10,11],[1.5,2.5]]
val = torch.tensor(val).float()
model(val.to(device))
小结
在本节中,我们使用 PyTorch
在简单数据集上构建了一个神经网络,训练神经网络来映射输入和输出,并通过执行反向传播来更新权重值以最小化损失值,并利用 Sequential
类简化网络构建过程;介绍了获取网络中间值的常用方法,以及如何使用 save
、load
方法保存和加载模型,以避免再次训练模型。
系列链接
PyTorch深度学习实战(1)——神经网络与模型训练过程详解
PyTorch深度学习实战(2)——PyTorch基础