PyTorch构建神经网络
- PyTorch神经网络主要使用
torch.nn库,下面我们将围绕这个库进行神经网络的搭建。(可参见官方文档)
土堆的教程只涉及了卷积神经网络,其他神经网络结构类似。
容器(Container)
- 构建一个神经网络,首先需要一个容器。PyTorch中最重要的容器是
Module,它是所有神经网络模块的基类,所有的神经网络模型都是在nn.Module类的基础上进行修改。 - 一个神经网络模型的示例如下:
import torch import torchvision from torch import nn from torch.nn import Conv2d from torch.utils.data import DataLoader from torch.utils.tensorboard import SummaryWriter dataset = torchvision.datasets.CIFAR10("./data", train=False, transform=torchvision.transforms.ToTensor(), download=True) dataloader = DataLoader(dataset, batch_size=64) class Model(nn.Module): def __init__(self): super().__init__() self.conv1 = Conv2d(in_channels=3, out_channels=6, kernel_size=3, stride=1, padding=0) def forward(self, x): x = self.conv1(x) return x model = Model() writer = SummaryWriter("./logs") step = 0 for data in dataloader: imgs, targets = data output = model(imgs) print(imgs.shape) # torch.Size([64, 3, 32, 32]),最后一个batch大小为16,下同 print(output.shape) # torch.Size([64, 6, 30, 30]) writer.add_images("input", imgs, step) output = torch.reshape(output, (-1, 3, 30, 30)) # torch.Size([64, 6, 30, 30]) -> [128, 3, 30, 30] writer.add_images("output", output, step) step = step + 1- 其中
super().__init__()表示沿用父类nn.Module的__init__参数,另外在Model类中必须对forward方法进行修改。具体的属性和方法会在下面进行详细阐释。
- 其中
- 另一个比较重要的容器是
Sequential,使用它构建神经网络就更加简洁,直接按顺序列举所有的函数即可。示例(LeNet-5神经网络):import torch from torch import nn from torch.nn import Conv2d, MaxPool2d, Flatten, Linear, Sequential class Model(nn.Module): def __init__(self): super().__init__() self.model1 = Sequential( Conv2d(3, 32, 5, padding=2), MaxPool2d(2), Conv2d(32, 32, 5, padding=2), MaxPool2d(2), Conv2d(32, 64, 5, padding=2), MaxPool2d(2), Flatten(), Linear(1024, 64), Linear(64, 10) ) def forward(self, x): x = self.model1(x) return x
卷积层(Convolution Layer)
- 在上面的代码中,我们使用了卷积层(
torch.nn.Conv2d),下面就来详细解释其用法(关于卷积神经网络的概念可参见机器学习方法-卷积神经网络,此处作略) Conv2d的格式为:torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True, padding_mode='zeros', device=None, dtype=None),下面对主要参数进行解释:参数 解释 in_channels输入图像通道数(如RGB三通道) out_channels输出图像通道数(等于卷积核数量) kernel_size卷积核大小(可输入单个整数或二元素元组) stride卷积核移动步长 padding卷积边缘填充(默认填充0) bias是否学习偏置 - 其中必要参数为
in_channels、out_channels和kernel_size,使用例可见上(卷积层属性在__init__中定义)。
- 其中必要参数为
在土堆的演示中使用了torch.nn.functional.conv2d这个函数,它与torch.nn.Conv2d的区别在于前者是后者的具体实现(需要输入原始张量数据)。下面是使用这个函数的示例:
import torch
import torch.nn.functional as F
input = torch.tensor([[1, 2, 0, 3, 1],
[0, 1, 2, 3, 1],
[1, 2, 1, 0, 0],
[5, 2, 3, 1, 1],
[2, 1, 0, 1, 1]])
kernel = torch.tensor([[1, 2, 1],
[0, 1, 0],
[2, 1, 0]])
input = torch.reshape(input, (1, 1, 5, 5))
kernel = torch.reshape(kernel, (1, 1, 3, 3))
print(input.shape)
print(kernel.shape)
output = F.conv2d(input, kernel, stride=1, padding=1)
print(output)
# tensor([[[[ 1, 3, 4, 10, 8],
# [ 5, 10, 12, 12, 6],
# [ 7, 18, 16, 16, 8],
# [11, 13, 9, 3, 4],
# [14, 13, 9, 7, 4]]]])其中torch.reshape的功能为改变张量的维数(因为torch.nn.functional.conv2d需要指定mini-batch和group数量)。
- 在前面的例子中,对图像的
output也进行了reshape,这是因为在tensorBoard只支持RGB三通道图像输出,因此需要转换维度。(第一个参数-1表示自动推断该维度的大小)
池化层(Pooling Layer)
- 以最大池化为例:
torch.nn.MaxPool2d(kernel_size, stride=None, padding=0, dilation=1, return_indices=False, ceil_mode=False)。其参数与卷积层基本相同,除了以下几点:- 池化层步长
stride的默认值为kernel_size,即池化核边长。 - 对
dilation参数的补充:dilation可以理解为核与输入的二维图像作池化操作时对应位置的间隔(可参考conv_arithmetic) ceil_mode控制计算输出维度时上取整或下取整。(默认下取整)
- 池化层步长
- 示例代码:
import torch
from torch import nn
from torch.nn import MaxPool2d
input = torch.tensor([[1,2,0,3,1],
[0,1,2,3,1],
[1,2,1,0,0],
[5,2,3,1,1],
[2,1,0,1,1]],dtype = torch.float32)
input = torch.reshape(input, (-1, 1, 5, 5))
class Model(nn.Module):
def __init__(self):
super().__init__()
self.maxpool1 = MaxPool2d(kernel_size=3, ceil_mode=True)
def forward(self, input):
output = self.maxpool1(input)
return output
model = Model()
output = model(input)
print(output)
# tensor([[[[2., 3.],
# [5., 1.]]]])其中input张量指定dtype是因为池化只支持浮点类型数据输入(卷积层同理)。
非线性激活(Non-linear Activations)
- 神经网络中另一个重要的函数就是非线性激活函数。常见的激活函数包括
nn.Sigmoid、nn.Tanh、nn.ReLU等。(更多非线性激活函数可参见官方文档) - 使用例(只给出
Model类,这也是许多模型中只会展示的核心,下同):
import torch
from torch import nn
from torch.nn import ReLU, Sigmoid
class Model(nn.Module):
def __init__(self):
super().__init__()
self.relu1 = ReLU()
self.sigmoid1 = Sigmoid()
def forward(self, input):
output = self.sigmoid1(input)
return output归一化层(Normalization Layer)
- 如
nn.BatchNorm2d,对应神经网络的Batch-Normalization,一般只设置一个参数:num_features,其对应四维输入的Channel(输入通道数)维度。
线性层(Linear Layer)
- 主要用
nn.Linear,包含两个参数:input_features和output_features,分别表示输入样本数与输出样本数(或者也可以理解为全连接神经网络中前后两层神经元的数量) - 使用例:
import torch
import torchvision
from torch import nn
from torch.nn import Linear
from torch.utils.data import DataLoader
class Model(nn.Module):
def __init__(self):
super().__init__()
self.linear1 = Linear(196608, 10)
def forward(self, input):
output = self.linear1(input)
return output
dataset = torchvision.datasets.CIFAR10("../data", train=False, transform=torchvision.transforms.ToTensor(),
download=True)
dataloader = DataLoader(dataset, batch_size=64)
for data in dataloader:
imgs, targets = data
print(imgs.shape) # torch.Size([64, 3, 32, 32])
output = torch.flatten(imgs) # 相当于torch.reshape(imgs, (1, 1, 1, -1))并转为一维张量
print(output.shape) # torch.Size([196608])
output = Model(output)
print(output.shape) # torch.Size([10])- 最后补充一个小点:在验证编写的神经网络是否正确,可以用下面两种方法:
- 自主设定特定维度的输入数据张量(比如
torch.ones,torch.randn等),然后输出输出层维度; - 使用
torchvision.tensorboard的add_graph功能将神经网络可视化。
- 自主设定特定维度的输入数据张量(比如
损失函数(Loss Function)
- 神经网络中损失函数的两大作用:计算实际输出与目标之间的差距、更新权重与输出的依据
torch.nn中提供了多种损失函数,包括:损失函数 表达式 nn.L1Loss或,由 reduction参数决定(下同)nn.MSELoss或 nn.CrossEntropyLoss或取均值 - 注意传入参数均为张量(数据均为浮点数),且第一个维度相同。使用例:
import torch
from torch.nn import L1Loss
from torch import nn
inputs = torch.tensor([1, 2, 3], dtype=torch.float32)
targets = torch.tensor([1, 2, 5], dtype=torch.float32)
inputs = torch.reshape(inputs, (1, 1, 1, 3))
targets = torch.reshape(targets, (1, 1, 1, 3))
loss = L1Loss(reduction='sum')
result = loss(inputs, targets)
loss_mse = nn.MSELoss()
result_mse = loss_mse(inputs, targets)
print(result) # tensor(2.)
print(result_mse) # tensor(1.3333)
x = torch.tensor([0.1, 0.2, 0.3])
y = torch.tensor([1])
x = torch.reshape(x, (1, 3))
loss_cross = nn.CrossEntropyLoss()
result_cross = loss_cross(x, y)
print(result_cross) # tensor(1.1019)- 有了损失函数,我们就能对神经网络的参数进行反向传播更新。示例:
import torchvision
from torch import nn
from torch.nn import Sequential, Conv2d, MaxPool2d, Flatten, Linear
from torch.utils.data import DataLoader
dataset = torchvision.datasets.CIFAR10("../data", train=False, transform=torchvision.transforms.ToTensor(),
download=True)
dataloader = DataLoader(dataset, batch_size=1)
class Model(nn.Module):
def __init__(self):
super().__init__()
self.model1 = Sequential(
Conv2d(3, 32, 5, padding=2),
MaxPool2d(2),
Conv2d(32, 32, 5, padding=2),
MaxPool2d(2),
Conv2d(32, 64, 5, padding=2),
MaxPool2d(2),
Flatten(),
Linear(1024, 64),
Linear(64, 10)
)
def forward(self, x):
x = self.model1(x)
return x
loss = nn.CrossEntropyLoss()
model = Model()
for data in dataloader:
imgs, targets = data
outputs = model(imgs)
result_loss = loss(outputs, targets)
result_loss.backward() # 计算反向传播梯度优化器(Optimizer)
- 有了损失函数和反向传播梯度之后,我们还需要用优化器对神经网络里的参数进行更新。
- 优化器的通用参数为:
params(即需要更新的参数),lr(learning rate,即学习率) torch.optim提供了多种优化器,包括:
|优化器(算法)|部分参数与解释|
|torch.optim.SGD|momentum:动量因子,weight_decay:权重衰减|
|torch.optim.Adam|betas:动量和梯度的惯性系数,eps:分母添加项(提高稳定性)|
|torch.optim.RMSprop|alpha:梯度惯性系数,momentum:动量因子|- 使用例(沿用上述例子):
# ....(代码同上) import torch loss = nn.CrossEntropyLoss() model = Model() optim = torch.optim.SGD(model.parameters(), lr=0.01) for data in dataloader: imgs, targets = data outputs = model(imgs) result_loss = loss(outputs, targets) optim.zero_grad() # 优化器梯度初始化 result_loss.backward() optim.step() # 优化器执行,参数更新 - 当然,上述训练只是一轮(epoch)数据的学习。为了更好地训练神经网络,我们需要进行多轮训练:
from torch.optim.lr_scheduler import StepLR loss = nn.CrossEntropyLoss() model = Model() optim = torch.optim.SGD(model.parameters(), lr=0.01) scheduler = StepLR(optim, step_size=5, gamma=0.1) # 学习率调度器,每隔step_size步将学习率乘上衰减系数 for epoch in range(20): running_loss = 0.0 for data in dataloader: imgs, targets = data outputs = model(imgs) result_loss = loss(outputs, targets) optim.zero_grad() result_loss.backward() # 计算反向传播梯度 scheduler.step() running_loss = running_loss + result_loss print(running_loss) # 得到一轮训练的总损失(会随着epoch增大逐渐减小)
使用现有的模型
模型的下载
- 除了自主构建模型,PyTorch还提供了多种现有的神经网络模型以供使用。
- 以
torchvision为例,其模型库可参阅官方文档。其通常包含以下参数:(已弃用)pretrained:如果为True,会下载一个已经在ImageNet上预训练过的模型。weight:模型的权重,如果想要已经预训练的模型,可设置为torchvision.models.***_Weights.IMAGENET1K_V1progress:如果为True,会在 stderr 上显示下载进度条
- 当然,我们也可以在已有网络模型上进行增加或修改。示例:
import torchvision
from torch import nn
vgg16_true = torchvision.models.vgg16(
weights=torchvision.models.VGG16_Weights.IMAGENET1K_V1
)
print(vgg16_true)
# VGG(
# (features): Sequential(
# (0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
# (1): ReLU(inplace=True)
# (2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
# (3): ReLU(inplace=True)
# (4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
# (5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
# (6): ReLU(inplace=True)
# ...
# (30): MaxPool2d(...)
# )
# (avgpool): AdaptiveAvgPool2d(output_size=(7, 7))
# (classifier): Sequential(
# (0): Linear(in_features=25088, out_features=4096, bias=True)
# (1): ReLU(inplace=True)
# (2): Dropout(p=0.5, inplace=False)
# (3): Linear(in_features=4096, out_features=4096, bias=True)
# (4): ReLU(inplace=True)
# (5): Dropout(p=0.5, inplace=False)
# (6): Linear(in_features=4096, out_features=1000, bias=True)
# )
# )
vgg16_true.classifier.add_module('add_linear', nn.Linear(1000, 10)) # 增加一层
vgg16_true.classifier[6] = nn.Linear(4096, 10) # 直接修改模型的保存与加载
- Pytorch中可使用以下方式保存模型:
import torch import torchvision from torch import nn vgg16 = torchvision.models.vgg16( weights=torchvision.models.VGG16_Weights.IMAGENET1K_V1 ) # 保存方式1:模型结构 + 模型参数 torch.save(vgg16, "vgg16_method1.pth") # 保存方式2:模型参数保存为字典(官方推荐) torch.save(vgg16.state_dict(), "vgg16_method2.pth") - 而当之后需要加载保存的模型时,可如下操作:
import torch # 方式1:对应保存方式1 import torchvision from torch import nn model = torch.load("vgg16_method1.pth") # print(model) # 方式2:对应保存方式2 vgg16 = torchvision.models.vgg16(pretrained=False) vgg16.load_state_dict(torch.load("vgg16_method2.pth")) # print(vgg16) - 另外注意:如果是保存自己构建的模型,那么在加载时就不能只加载
.pth文件,还需要将定义的Model类导入。例: 上面的代码保存为class Model(nn.Module): def __init__(self): super().__init__() self.conv1 = nn.Conv2d(3, 64, kernel_size=3) def forward(self, x): x = self.conv1(x) return x model = Model() torch.save(model, "model.pth")model_save.py,那么加载模型时就需要导入这个文件(同一文件夹下):from model_save import * model = torch.load('model.pth') print(model)
总结
模型训练流程
- 一个完整的神经网络训练流程如下:
- 准备数据集(包括训练集和测试集)
- 加载数据集
- 构建神经网络模型(或使用现有模型+微调)
- 确定损失函数
- 确定优化器
- 设置训练网络的参数(训练次数,训练轮数,测试轮数等)
- epoch循环:
- 每个epoch里循环传入所有数据
- 得到输出,计算loss,优化器更新参数
- 每个epoch结束后,对模型进行测试(使用测试集数据)
- 保存模型
- 6~8步示例:
# 记录训练的次数 total_train_step = 0 # 记录测试的次数 total_test_step = 0 # 训练的轮数 epoch = 10 # 添加tensorboard # writer = SummaryWriter("../logs_train") for i in range(epoch): print("-------第 {} 轮训练开始-------".format(i+1)) # 训练步骤开始 model.train() for data in train_dataloader: imgs, targets = data outputs = model(imgs) loss = loss_fn(outputs, targets) # loss_fn = nn.CrossEntropyLoss() # 优化器优化模型 optimizer.zero_grad() loss.backward() optimizer.step() total_train_step = total_train_step + 1 if total_train_step % 100 == 0: print("训练次数:{}, Loss: {}".format(total_train_step, loss.item())) # writer.add_scalar("train_loss", loss.item(), total_train_step) # 测试步骤开始 model.eval() total_test_loss = 0 total_accuracy = 0 with torch.no_grad(): # 不考虑梯度 for data in test_dataloader: imgs, targets = data outputs = model(imgs) loss = loss_fn(outputs, targets) total_test_loss = total_test_loss + loss.item() accuracy = (outputs.argmax(1) == targets).sum() # outputs是每张图片属于每个类别的预测概率矩阵,argmax(1)表示按行取最大值索引 total_accuracy = total_accuracy + accuracy print("整体测试集上的Loss: {}".format(total_test_loss)) print("整体测试集上的正确率: {}".format(total_accuracy/test_data_size)) # writer.add_scalar("test_loss", total_test_loss, total_test_step) # writer.add_scalar("test_accuracy", total_accuracy/test_data_size, total_test_step) total_test_step = total_test_step + 1 torch.save(tudui, "tudui_{}.pth".format(i)) print("模型已保存")- 注意到上面除了损失函数指标,还使用了准确率指标,这是分类问题特有的训练指标。
- 另外,上述代码中还用了
model.train()和model.eval()这两个语句,它们的作用是设置模型的模式。虽然在这个程序中不写没有影响,但是如果神经网络中有Dropout,BatchNorm等特殊处理,那么就必须明确模型的模式。
使用GPU训练
笔者的电脑没有GPU,不过可以用google colab白嫖GPU算力(
- 使用GPU训练可以加快模型训练速度。要让程序使用GPU训练,主要有以下两种方法:
-
在以下地方加上
.cuda():- 模型本身(
model = model.cuda()) - 损失函数(
loss_fn = loss_fn.cuda()) - 训练与测试数据(
imgs = imgs.cuda(),targets = targets.cuda()) - 当然,为了兼容没有GPU的电脑,也可以改成条件判断:
if torch.cuda.is_available(): model = model.cuda()
- 模型本身(
-
在上面提到的那些地方加上
.to(device),同时在程序开头设置device:【更常用】device = torch.device("cpu")【CPU训练】device = torch.device("cuda")【GPU训练】device = torch.device("cuda:1")【多张显卡】富哥
当然也可以用兼容性写法:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
- 另外,如果要在没有CPU的设备上加载使用过GPU训练的模型,那么在加载时需要加上
map_location=torch.device('cpu')。
模型验证流程
- 模型验证的流程与训练时的测试流程类似,就是先导入训练好的模型,再用一个新的数据集作为输入,检验其效果,故从略。
END
终于把土堆的教程刷完了,已经在收藏夹里吃灰很久了(
虽然现在再看有种49入国军之感,但PyTorch本身应该还是业界常用的工具,DL和NN也尚未过时,所以还是有那么点用吧hh
之后可能会找一些实践项目进行记录,敬请赐候……
