赞
踩
该系列笔记主要参考了小土堆的视频教程,传送门:P1. PyTorch环境的配置及安装(Configuration and Installation of PyTorch)【PyTorch教程】_哔哩哔哩_bilibili
涉及到的文件/数据集网盘:
链接:https://pan.baidu.com/s/1aZmXokdpbA97qQ2kHvx_JQ?pwd=1023 提取码:1023
在前面的两篇文章中,我们学会了对数据的处理、加载和数据集的使用,在本文以及后续的文章中我们将会会来学习如何使用pytorch搭建一个神经网络,让它能够使用到我们准备好的数据。
关于神经网络中激活函数、卷积层、池化层等底层原理,我不会在本文中详解,但是关于pytorch中如何使用对应的方法实现这些层的功能我会进行解释,如果你想要了解一些关于神经网络底层的只是,我十分推荐你去看一下吴恩达老师的深度学习课程,我后续也会推出他的课程视频。
在pytorch中,想要自定义网络的话,需要使用pytorch.nn中的Module类。Module类是所有神经网络模块的基类,即自己编写的所有模块都需要继承Module类。
下面给出官方文档中的示例代码:
import torch.nn as nn
import torch.nn.functional as F #nn.functional中包含了神经网络所需要用到的大量函数,比如各种激活函数、损失函数等。
class Model(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 20, 5)
self.conv2 = nn.Conv2d(20, 20, 5)
def forward(self, x):
x = F.relu(self.conv1(x))
return F.relu(self.conv2(x))
在继承nn.Module后,必须重写两个方法,_init_
和forward
,在使用_init_
时又必须先引用调用父类的初始化方法,即super().__init__()
,然后在forward
前向传播中使用自定义的网络模块,上述示例中的conv2d
方法将在后面介绍
【代码示例】 搭建一个自己的module子类,不需要实现神经网络的功能
import torch from torch import nn class Mymodule(nn.Module): def __init__(self): super().__init__() def forward(self, input): output = input + 1 return output mymodule = Mymodule() x = torch.tensor(1.0) output = mymodule(x) print(output)
主要语法如下:
nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0)
# in_channels : 输入数据的通道数
# out_channels : 输出数据的通道数
# kernel_size : 卷积核的大小
# stride: 步长
# padding: 边缘填充·
【代码示例】对CIFAR10数据集使用3*3卷积,stride和padding为默认,CIFAR10中数据大小为3*32*32
,卷积完成后,应该为3*30*30
,通过图片检验你的代码结果是否正确
import torchvision from torch import nn from torch.nn import Conv2d from torch.utils.data import DataLoader dataset = torchvision.datasets.CIFAR10("dataset/CIFAR10", train=False, transform=torchvision.transforms.ToTensor(), download=True) dataloader = DataLoader(dataset, batch_size=64) class myModule(nn.Module): def __init__(self): super(myModule, self).__init__() # 在此处写自定义的神经网络层 self.conv1 = Conv2d(in_channels=3, out_channels=6, kernel_size=3, stride=1, padding=0) def forward(self, input): # 在此处调用神经网络层 input = self.conv1(input) return input mymodule = myModule() step = 0 for data in dataloader: imgs, targets = data output = mymodule(imgs) print(imgs.shape) # torch.Size([64, 3, 32, 32]) 64为batch_size大小,即每轮读取64张图片 print(output.shape) # torch.Size([64, 6, 30, 30]) -> [xxx, 3, 30, 30] exit(0)
输出结果:
torch.Size([64, 3, 32, 32])
torch.Size([64, 6, 30, 30])
池化层主要用于缩小输入的特征数量,池化有平均池化和最大值池化。现在最大值池化更加常用,其主要语法如下:
MaxPool2d(kernel_size=3, ceil_mode=False)
# kernel_size: 池化窗口的大小。它可以是一个整数(表示高和宽相等的正方形窗口)或者一个包含两个整数的元组(kernel_height, kernel_width),分别指定窗口的高度和宽度。
# stride: 窗口在输入数据上滑动的步长,默认等于kernel_size。它可以是一个整数(表示高度和宽度上的步长相同)或一个包含两个整数的元组(stride_height, stride_width)。
# padding: 在输入的每个边缘填充的大小。这可以是单个整数(表示所有边缘的相同填充)或一个包含两个整数的元组(pad_height, pad_width)。默认为0,意味着没有填充。
# return_indices: 如果设置为True,除了返回池化后的输出张量外,还会返回每个最大值在输入张量中的索引。这对于某些操作,如nn.MaxUnpool2d反池化操作,可能很有用。默认为False。
# ceil_mode: 当设为True,会计算输出尺寸时使用向上取整,而不是默认的向下取整。这会影响到输出的尺寸计算,特别是在使用非1的步长或填充时。默认为False。
【代码示例】对CIFAR10数据集使用最大池化,并将最终结果进行可视化呈现
import torchvision from torch import nn from torch.nn import MaxPool2d from torch.utils.data import DataLoader from torch.utils.tensorboard import SummaryWriter dataset = torchvision.datasets.CIFAR10("dataset/CIFAR10", train=False, download=True, transform=torchvision.transforms.ToTensor()) dataloader = DataLoader(dataset, batch_size=64) class myModule(nn.Module): def __init__(self): super().__init__() self.maxpool1 = MaxPool2d(kernel_size=3, ceil_mode=False) def forward(self, input): output = self.maxpool1(input) return output myModule = myModule() writer = SummaryWriter("logs_maxpool") step = 0 for data in dataloader: imgs, targets = data writer.add_images("input", imgs, step) output = myModule(imgs) writer.add_images("output", output, step) step = step + 1 writer.close()
运行结果:最大池化相当于是邻近几个像素中只选取一个最大值作为特征,所以呈现结果就是图片减小,变成类似马赛克的效果
主要使用的是nn.relu
和 nn.sigmoid
两个方法,没有特殊的参数需求,直接调用即可
【代码示例】实现激活函数并进行可视化
import torch import torchvision from torch import nn from torch.nn import ReLU, Sigmoid from torch.utils.data import DataLoader from torch.utils.tensorboard import SummaryWriter input = torch.tensor([[1, -0.5], [-1, 3]]) input = torch.reshape(input, (-1, 1, 2, 2)) print(input.shape) dataset = torchvision.datasets.CIFAR10("dataset/CIFAR10", train=False, download=True, transform=torchvision.transforms.ToTensor()) dataloader = DataLoader(dataset, batch_size=64) class myModule(nn.Module): def __init__(self): super().__init__() self.relu1 = ReLU() self.sigmoid1 = Sigmoid() def forward(self, input): output = self.sigmoid1(input) return output mymodule = myModule() writer = SummaryWriter("logs_relu") step = 0 for data in dataloader: imgs, targets = data writer.add_images("input", imgs, global_step=step) output = mymodule(imgs) writer.add_images("output", output, step) step += 1 writer.close()
线性变换主要有两个函数:
nn.flatten
用来将图片展开为一维信息
nn.Linear
用来进行全连接的线性变换
【代码示例】实现nn.flatten
展开和nn.Linear
线性变换
import torch import torchvision from torch import nn from torch.nn import Linear from torch.utils.data import DataLoader dataset = torchvision.datasets.CIFAR10("dataset/CIFAR10", train=False, transform=torchvision.transforms.ToTensor(), download=True) dataloader = DataLoader(dataset, batch_size=64, drop_last=True) class myModule(nn.Module): def __init__(self): super().__init__() # in_feature 为输入特征数量 # out_feature 为输出特征数量 self.linear1 = Linear(196608, 10) def forward(self, input): output = self.linear1(input) return output mymodule = myModule() for data in dataloader: imgs, targets = data print(imgs.shape) output = torch.flatten(imgs) # flatten将图片变为一维信息 print(output.shape) output = mymodule(output) print(output.shape)
相信你还记得我们在上一章中使用到的CIFAR10数据集,官方为我们提供了一个卷积神经网络的架构实现对其进行分类,其架构图如下所示:
图例说明:a@b*c 中,a为通道数,b和c为图像的宽高
【代码示例】自己动手实现上面的卷积神经网络并测试其输出内容和大小
import torch from torch import nn # 搭建CIFAR10神经网络 class myModule(nn.Module): def __init__(self): super().__init__() self.my_conv1 = nn.Conv2d(3, 32, 5, 1, 2) self.my_pool1 = nn.MaxPool2d(2) self.my_conv2 = nn.Conv2d(32, 32, 5, 1, 2) self.my_pool2 = nn.MaxPool2d(2) self.my_conv3 = nn.Conv2d(32, 64, 5, 1, 2) self.my_pool3 = nn.MaxPool2d(2) self.my_flat = nn.Flatten() self.my_line1 = nn.Linear(64*4*4, 64) self.my_line2 = nn.Linear(64, 10) def forward(self, input): output = self.my_conv1(input) output = self.my_pool1(output) output = self.my_conv2(output) output = self.my_pool2(output) output = self.my_conv3(output) output = self.my_pool3(output) output = self.my_flat(output) output = self.my_line1(output) output = self.my_line2(output) return output if __name__ == '__main__': mymodule = myModule() print(mymodule) # 查看自己创建的结构 # 验证最后的输出大小是否为自己想要的 input = torch.ones((64, 3, 32, 32)) output = mymodule(input) print(output.shape)
运行结果:
myModule(
(my_conv1): Conv2d(3, 32, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
(my_pool1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(my_conv2): Conv2d(32, 32, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
(my_pool2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(my_conv3): Conv2d(32, 64, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
(my_pool3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(my_flat): Flatten(start_dim=1, end_dim=-1)
(my_line1): Linear(in_features=1024, out_features=64, bias=True)
(my_line2): Linear(in_features=64, out_features=10, bias=True)
)
torch.Size([64, 10])
在PyTorch中,nn.Sequential
是一个容器模块,用于将多个模块按照顺序串联起来形成一个有序的神经网络结构。这意味着数据会按照添加到nn.Sequential
中的顺序依次通过每个模块(层)。在上一个例子中,我们要手写forward中所有的调用,使用nn.Sequential
时,你不需要显式地定义前向传播(forward
)方法,因为这个容器会自动将输入传递给序列中的第一个模块,然后将前一个模块的输出自动作为下一个模块的输入,依此类推,直到序列结束。这样不仅是不需要手写forward,同时也不需要在Sequential中定义每一层的名字了。这对于定义那些没有分支或者跳连的简单前馈网络特别方便。
【代码示例】使用Sequential改写上面的例子
import torch from torch import nn # 搭建神经网络 class myModule(nn.Module): def __init__(self): super().__init__() self.model = nn.Sequential( nn.Conv2d(3, 32, 5, 1, 2), nn.MaxPool2d(2), nn.Conv2d(32, 32, 5, 1, 2), nn.MaxPool2d(2), nn.Conv2d(32, 64, 5, 1, 2), nn.MaxPool2d(2), nn.Flatten(), nn.Linear(64*4*4, 64), nn.Linear(64, 10) ) def forward(self, x): x = self.model(x) return x if __name__ == '__main__': mymodule = myModule() input = torch.ones((64, 3, 32, 32)) output = mymodule(input) print(output.shape) # 运行结果不变,代码更加简洁
主要用到的三种损失函数如下:
在PyTorch中的L1Loss
以及其他许多损失函数中,reduction
参数是用来指定如何将每个样本或批次中元素的损失进行汇总的方式。这个参数决定了最终返回的损失值是单个标量还是保持为向量/张量的形式。reduction
参数可以取以下几个值:
【代码示例】 在上面创建的CIFAR10卷积神经网络中使用交叉熵损失函数观察其预测结果和正确结果之间的差距
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("dataset/CIFAR10", train=False, transform=torchvision.transforms.ToTensor(), download=True) dataloader = DataLoader(dataset, batch_size=1) class myModule(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() mymodule = myModule() for data in dataloader: imgs, targets = data outputs = mymodule(imgs) result_loss = loss(outputs, targets) print(result_loss)
运行结果:
tensor(2.3523, grad_fn=<NllLossBackward0>)
tensor(2.2240, grad_fn=<NllLossBackward0>)
...
PyTorch提供了多种内置的优化器,用于在训练过程中更快地更新模型参数,主要作用在反向传播过程中以下是一些常用的优化器及其特点:
我会在后续专门的文章中给大家详细介绍关于优化器地底层原理以及它们的选择,敬请期待
【代码示例】使用随机梯度下降(SGD)实现模型的训练
import torch import torchvision from torch import nn from torch.nn import Sequential, Conv2d, MaxPool2d, Flatten, Linear from torch.optim.lr_scheduler import StepLR from torch.utils.data import DataLoader dataset = torchvision.datasets.CIFAR10("dataset/CIFAR10", train=False, transform=torchvision.transforms.ToTensor(), download=True) dataloader = DataLoader(dataset, batch_size=1) class myModule(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() # 交叉熵损失函数 mymodule = myModule() optim = torch.optim.SGD(mymodule.parameters(), lr=0.01) # 随机梯度下降优化器 for epoch in range(20): running_loss = 0.0 for data in dataloader: imgs, targets = data outputs = mymodule(imgs) result_loss = loss(outputs, targets) optim.zero_grad() # 每次进行反向传播前,将优化器的梯度置零,避免梯度积累 result_loss.backward() # 根据计算出的损失函数进行反向传播,计算梯度 optim.step() # 更新参数 running_loss = running_loss + result_loss # 累加损失,方便对每一轮的损失进行监控 print(running_loss)
运行结果:
tensor(18725.9844, grad_fn=<AddBackward0>)
tensor(16125.2676, grad_fn=<AddBackward0>)
tensor(15378.4766, grad_fn=<AddBackward0>)
tensor(16010.2520, grad_fn=<AddBackward0>)
tensor(17944.1855, grad_fn=<AddBackward0>)
tensor(20080.8281, grad_fn=<AddBackward0>)
tensor(21999.3105, grad_fn=<AddBackward0>)
tensor(23399.3027, grad_fn=<AddBackward0>)
tensor(23927.0293, grad_fn=<AddBackward0>)
tensor(24855.1445, grad_fn=<AddBackward0>)
tensor(25396.4512, grad_fn=<AddBackward0>)
现在我们已经会自己搭建一个模型并进行训练,但通常情况下我们都不需要自己从头搭建一个网络,而是在前人研究的基础上,进行改进,得到效果更好的/更适用于当前任务的网络模型,如何修改别人已经搭建好的模型就成为了一个问题。
就像之前用到的CIFAR10之类的数据集一样,pytorch官网也为我们提供了一些公开的网络模型,我们在这里使用vgg16进行演示。
# pretrained表示是否使用预训练好的参数,选择True就是使用作者已经为我们训练好的参数,False则是随机初始化参数,自己从头开始进行训练
vgg16_true = torchvision.models.vgg16(pretrained=True)
Downloading: "https://download.pytorch.org/models/vgg16-397923af.pth" to C:\Users\13563/.cache\torch\hub\checkpoints\vgg16-397923af.pth
100.0%
此时如果打印我们实例化的vgg16对象,就可以看到它的整个网络结构:
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) (7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (8): ReLU(inplace=True) (9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) (10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (11): ReLU(inplace=True) (12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (13): ReLU(inplace=True) (14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (15): ReLU(inplace=True) (16): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) (17): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (18): ReLU(inplace=True) (19): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (20): ReLU(inplace=True) (21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (22): ReLU(inplace=True) (23): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) (24): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (25): ReLU(inplace=True) (26): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (27): ReLU(inplace=True) (28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (29): ReLU(inplace=True) (30): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) )
首先要知道,大部分复杂的神经网络都分为了很多部分,比如:
首先到达你需要修改网络的正确部分,添加层使用add_module
,修改某个层使用它的下表定位后重新制定Module即可
【代码示例】修改下载的vgg16模型并观察其网络结构的变化
import torchvision from torch import nn vgg16_false = torchvision.models.vgg16(pretrained=False) vgg16_true = torchvision.models.vgg16(pretrained=True) print(vgg16_true) print("*"*30) vgg16_true.classifier.add_module('add_linear', nn.Linear(1000, 10)) print(vgg16_true) print("*"*30) print(vgg16_false) print("*"*30) vgg16_false.classifier[6] = nn.Linear(4096, 10) print(vgg16_false)
当我们写好一个模型,训练好其参数后,谁也不想下次开机又要重新来一遍这个漫长的过程,这个时候就需要将训练好的模型进行保存,并在下一次需要使用它时候进行读取。
模型的保存和读取有两种方法:
保存模型结构+模型参数
# 保存
torch.save(vgg16, "vgg16_method1.pth")
# 读取
model = torch.load("vgg16_method1.pth")
用这个方法得到的模型对象直接答应就是其网络结构
保存模型参数
# 保存
torch.save(vgg16.state_dict(), "vgg16_method2.pth")
# 读取 需要先创建好对象,然后给其传入参数
vgg16 = torchvision.models.vgg16(pretrained=False)
vgg16.load_state_dict(torch.load("vgg16_method2.pth"))
model = torch.load("vgg16_method2.pth")
print(model) # 直接读取的话就是所有参数的字典
print(vgg16)
推荐使用第二种方法,第一种方法的陷阱:
# 保存一个自定义网络 class myModule(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 mymodule = myModule() torch.save(mymodule, "mymodule.pth") # 读取 model = torch.load('mymodule.pth') print(model)
这种情况下在另一个文件中使用第一种方法进行读取就会报错,因为在新的文件中没有定义关于自定义模型的结构。这时候在新的文件中导入自定义模块的类,使用第二种方法创建对象,然后再传参就不会有问题了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。