Pytorch学习笔记

准备阶段

Anaconda + PyTorch + PyCharm

检测是否已经安装成功:

python 进入python

import torch 无报错

torch.cuda.is_available() 返回True

Python两大函数

dir()打开某个工具箱(package)如Pytorch,即展示Pytorch内部有哪些模块

help()帮助函数,在jupyter中也可以使用()??来查看具体用法

Pytorch读取数据

两大类

  • Dataset提供一种方式去获取数据及其label,主要实现以下两种功能
    • 如何获取每一个数据及其label
    • 输入共有多少个数据
  • Dataloader为后面的网络提供不同的数据形式

Dataset用法

from torch.utils.data import Dataset导入Dataset类

Dataset是一个虚拟类,每次使用需要用户重新定义其成员函数__getitem__()&__len__()

以下是一个常规定义pip install opencv-python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from torch.utils.data import Dataset
from PIL import Image ## 也可以使用cv2来读取图片
import os

class MyData(Dataset):

def __init__(self, root_dir, label_dir): # 为整个类提供一些全局变量
self.root_dir = root_dir
self.label_dir = label_dir
self.path = os.path.join(root_dir, label_dir) # 将根路径和类别路径 拼接
self.img_path = os.listdir(self.path) # 使用列表存储path下的所有文件名

def __getitem__(self, index):
"""
返回指定index的图片和label
"""
img_name = self.img_path[index]
img_item_path = os.path.join(self.path, img_name)
img = Image.open(img_item_path) # 使用Image读取图片
label = self.label_dir
return img, label

def __len__(self):
return len(self.img_path)

定义类之后的使用方式:

1
2
3
4
5
6
root_dir = "DataSet/hymenoptera_data/hymenoptera_data/train"
ants_label_dir = "ants"
bees_label_dir = "bees"
ants_dataset = MyData(root_dir, ants_label_dir) # 创建类的示例 img, label = ants_dataset[index]
bees_dataset = MyData(root_dir, bees_label_dir)
train_dataset = ants_dataset + bees_dataset # 可以直接相加用于拼接 dataset的集合

Dataloader用法

from torch.utils.data import DataLoader载入DataLoader工具箱

用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
test_data = torchvision.datasets.CIFAR10("./Dataset/CIFAR10_dataset", train=False, transform=torchvision.transforms.ToTensor())
# 先使用Dataset读取文件,或是用torchvision.dataset选取数据集
test_loader = DataLoader(dataset=test_data, batch_size=64, shuffle=True, num_workers=0, drop_last=False)
# drop_last=False时,当前batch不足size时仍然保留
# shuffle=True时,每次选取的顺序都是打乱的

writer = SummaryWriter("test_loader")
for epoch in range(2):
step = 0
for data in test_loader:
imgs, targets = data
# 这里的image就是一个batch
# image和target是一个tensor,(batch_size, 3, high, width)
writer.add_images("test_image{}".format(epoch), imgs, step)
step += 1
writer.close()

Tensorboard的使用

主要功能是可以展示深度学习中某一步的输出pip install tensorboard

使用from torch.utils.tensorboard import SummaryWriter导入SummaryWriter类

创建实例**writer = SummaryWriter("logs")**将内容保存到logs文件夹

writer.add_scalar()的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def add_scalar(
self,
tag,
scalar_value,
global_step=None,
walltime=None,
new_style=False,
double_precision=False,
):
"""Add scalar data to summary.
Args:一般只关心前三个参数
tag (str): Data identifier 图片标题
scalar_value (float or string/blobname): Value to save 图的纵坐标
global_step (int): Global step value to record 图的横坐标
walltime (float): Optional override default walltime (time.time())
with seconds after epoch of event
new_style (boolean): Whether to use new style (tensor field) or old
style (simple_value field). New style could lead to faster data loading.

具体用法:

1
2
3
4
5
6
from torch.utils.tensorboard import SummaryWriter
writer = SummaryWriter("logs") # 创建示例,内容保存到logs文件夹
# writer.add_image()
for i in range(1, 100): # 自己画曲线图
writer.add_scalar("y = x", i, i) # 更改标题即生成新的图片,否在将在原有图片中重叠
writer.close()

打开logs文件的方法:

在终端运行,–logdir为文件名,–port为端口名

1
tensorboard --logdir="logs" --port=6007

writer.add_image()的说明:

writer.add_image()输入的图像需要是tensor类型、numpy型或字符串等,需要使用opencv读取图像(numpy型),不能使用Image读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
def add_image(
self, tag, img_tensor, global_step=None, walltime=None, dataformats="CHW"
):
"""Add image data to summary.
Note that this requires the ``pillow`` package.
Args:
tag (str): Data identifier 图像标题
img_tensor (torch.Tensor, numpy.ndarray, or string/blobname): Image data 图像
global_step (int): Global step value to record 这是第几步的图像
walltime (float): Optional override default walltime (time.time())
seconds after epoch of event
dataformats (str): Image data format specification of the form
CHW, HWC, HW, WH, etc. 高H宽W通道C,需要说明图像数据的顺序,opencv读取的图像是HWC格式

torchvision中的transforms使用

使用from torchvision import transforms导入工具箱

transforms.ToTensor

用法:一般是将由opencv或Image读取的图片转为tensor

1
2
tensor_trans = transforms.ToTensor()  # 实例化对象
tensor_img = tensor_trans(img)

tensor的数据类型可以理解为包装了神经网络所需要的一些信息,如后向传递、梯度、梯度方法

transforms.Normalize

用法:正态分布的标准化,均值为0,方差为1 ?? 加快梯度下降的方法

output[channel] = (input[channel] - mean[channel]) / std[channel])

1
2
trans_norm = transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) # mean, std
img_norm = trans_norm(img_tensor)

transforms.Resize

可以处理PIL Image的图像(现在应该可以直接处理tensor)

1
2
trans_resize = transforms.Resize((h, w))  
img_trans = trans_resize(img)

transforms.Compose

用法:将多个变换整合到一个序列中

Compose()的输入是一个列表,列表中的数据是transforms的类型

1
2
3
trans_resize = transforms.Reize(512)  # 短边缩放至512,长宽比不变
trans_compose = transforms.Compose([trans_resize, trans_toTensor])
img_compose = trans_compose(img)

PyTorch官方文档和数据集

torchvision — Torchvision main documentation (pytorch.org)

torchvision属于pytorch的一个版本

torchvision数据集的使用

下载和存储dataset的样例。其中train_set&test_set都是Dataset类的对象

1
2
3
4
5
import torchvision

train_set = torchvision.datasets.CIFAR10(root="./Dataset/CIFAR10_dataset", train=True, download=True)
test_set = torchvision.datasets.CIFAR10(root="./Dataset/CIFAR10_dataset", train=False, download=True)
img, target = test_set[0]

使用print(test_set.classes)来显示所有成员

Python中torch关于神经网络的API

torch.nn — PyTorch 2.0 documentation

Containers 容器

Module

torch.nn.Moudle所有神经网络模块的基类,需要自定义一个子类来继承它

1
2
3
4
5
6
7
8
9
10
11
12
import torch.nn as nn
import torch.nn.functional as F

class Model(nn.Module):
def __init__(self):
super().__init__() # 对Module的初始化
self.conv1 = nn.Conv2d(1, 20, 5)
self.conv2 = nn.Conv2d(20, 20, 5)

def forward(self, x): # 前向传播
x = F.relu(self.conv1(x)) # 先卷积,然后Relu
return F.relu(self.conv2(x))

Sequential 网络结构序列

torch.nn.Sequential(arg: OrderedDict[str, Module])

将网络结构列成一个序列,然后input按照这个序列执行

1
2
3
4
5
6
model = nn.Sequential(OrderedDict([
('conv1', nn.Conv2d(1,20,5)),
('relu1', nn.ReLU()),
('conv2', nn.Conv2d(20,64,5)),
('relu2', nn.ReLU())
]))

Convolution Layers 卷积层

有两种调用方法,第一种是使用torch.nn.Conv2d调用 Conv2d — PyTorch 2.0 documentation

也可以通过import torch.nn.functional as F可以直接调用F.conv2d()

torch.nn.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)

  • kernel_size:定义卷积核的大小,一个整数或元胞数组,整数则是nXn的,数组(n,m)则是nXm的
  • in_channel:指的是输入的图像有几层
  • out_channel:指的是使用几个卷积核来卷积,一个卷积核作为一个通道的输出

dilation默认为1,可以根据这个公式计算论文中需要的一些参数

F.conv2d()

参数说明 torch.nn.functional.conv2d — PyTorch 2.0 documentation

  • stride卷积核的移动步长,可以是一个常数或者一个元组(横向步长,纵向步长)
  • input需要输入(batch_size,通道数channel,图像高,图像宽),如果只是一个图像,则需要使用torch.reshape(input,(input_size))来修改size
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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))

output = F.conv2d(input, kernel, stride=1)
output1 = F.conv2d(input, kernel, stride=1, padding=1)

使用nn.Conv2d初步构建神经网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import torch
import torchvision
from torch import nn
from torch.utils.tensorboard import SummaryWriter
from torch.utils.data import DataLoader

dataset = torchvision.datasets.CIFAR10("DataSet/CIFAR10_dataset", train=False,
transform=torchvision.transforms.ToTensor())
dataloader = DataLoader(dataset, batch_size=64, shuffle=True, drop_last=True)

class MyNet(nn.Module):
def __init__(self):
super(MyNet, self).__init__()
self.conv1 = nn.Conv2d(in_channels=3, out_channels=6, kernel_size=3, stride=1, padding=0)

def forward(self, input):
output = self.conv1(input)
return output

myNet = MyNet()
writer = SummaryWriter("nn_Conv2d_test")
step = 0
for data in dataloader:
imgs, targets = data
output = myNet.forward(imgs)
writer.add_images("test", imgs, step)
# 因为6通道数无法显示图像,所以使用torch.reshape来修改通道数
output = torch.reshape(output, (128, 3, 30, 30))
# -1的位置可以根据其他尺寸的调整而自适应调整
writer.add_images("con2d_test", output, step)
step += 1

writer.close()

使用print可以打印神经网络的结构:

1
2
3
MyNet(
(conv1): Conv2d(1, 6, kernel_size=(3, 3), stride=(1, 1))
)

Pooling Layers 池化层

MaxPooling

torch.nn.MaxPool2d(kernel_size, stride=None, padding=0, dilation=1, return_indices=False, ceil_mode=False)

  • dilation:指的是空洞卷积的间隔尺寸(各种卷积方式见conv_arithmetic/README.md at master · vdumoulin/conv_arithmetic · GitHub

  • ceil_mode:设为True则为ceil,即向上取整,否则为floor,即向下取整

  • stride:在池化层中,默认步长为卷积核大小。当一个3X3的池化核到边界时,可能只剩下一个3X2的区域,这时如果ceil_mode为True,则保留这个区域并进行池化,否则不保留

1
self.maxpool1 = nn.MaxPool2d(kernel_size=3, ceil_mode=True)

Non-liner activate 非线性激活

ReLu&Sigmoid,非线性变换就是给网络引入非线性特征,非线性越多就可以拟合更复杂的曲线

ReLu

torch.nn.ReLU(inplace=False)

  • inplace:当其为True时,会将input的数据直接进行替换且没有输出。通常令其为False

用法

1
2
3
m = nn.ReLU()
input = torch.randn(2)
output = m(input)

Normalization Layers 正则化层

torch.nn.BatchNorm2d(num_features, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True, device=None, dtype=None)BatchNorm2d — PyTorch 2.0 documentation

在神经网络中,正则化层是一种用于控制模型复杂度和减小过拟合的技术。正则化层通过添加额外的约束或惩罚项来限制模型的学习能力,从而提高其泛化能力。

常见的正则化层包括L1正则化(L1 regularization)和L2正则化(L2 regularization)。

  1. L1正则化(L1 regularization):通过添加L1范数作为正则化项来惩罚模型中的参数。L1正则化有助于稀疏化模型,即将一些参数置为零,从而减少模型的复杂度。
  2. L2正则化(L2 regularization):通过添加L2范数作为正则化项来惩罚模型中的参数。L2正则化有助于减小参数的值,使其更加平滑,并减少模型对个别训练样本的敏感性。

正则化层的作用主要有以下几个方面:

  1. 控制模型复杂度:正则化层通过限制模型的参数空间,避免模型过于复杂,从而减少过拟合的风险。
  2. 减小过拟合:过拟合是指模型在训练数据上表现很好,但在未见过的测试数据上表现较差。正则化层通过约束模型的学习能力,使其更加一般化,提高模型在未知数据上的性能。
  3. 提高泛化能力:正则化层可以帮助模型学习到更具有普适性的特征,从而提高模型在新样本上的泛化能力。

总之,正则化层在神经网络中起到一种正则化和约束模型的作用,有助于防止过拟合,提高模型的泛化性能。

Linear Layers 线性化层

torch.nn.Linear(in_features, out_features, bias=True, device=None, dtype=None)Linear — PyTorch 2.0 documentation

前一个层的某一个神经元为x,当前层的一个神经元为y,这两个神经元相连,计算方式是y=xAT+by=xA^T+b。线性层就是将展平后的数组缩短nn.Linear(输入的个数, 输出的个数)

torch.flatten(input)将input摊平为一维数组

一个简单的网络结构和tensorboard的可视化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import torch
from torch import nn
from torch.utils.data import DataLoader
import torchvision
from torchvision import datasets
from torch.utils.tensorboard import SummaryWriter

# test_set = datasets.CIFAR10("DataSet/CIFAR10_dataset", train=False, transform=torchvision.transforms.ToTensor())

# test_dataset = DataLoader(test_set, batch_size=64, shuffle=True)

class MyNet(nn.Module):
def __init__(self):
super(MyNet, self).__init__()
# self.conv1 = nn.Conv2d(3, 32, 5, stride=1, padding=2)
# self.maxPooling1 = nn.MaxPool2d(2)
# self.conv2 = nn.Conv2d(32, 32, 5, stride=2, padding=2)
# self.maxPooling2 = nn.MaxPool2d(2)
# self.conv3 = nn.Conv2d(32, 64, 5, stride=1, padding=2)
# self.maxPooling3 = nn.MaxPool2d(2)
# self.flatten1 = nn.Flatten()
# self.liner1 = nn.Linear(1024, 64)
# self.liner2 = nn.Linear(64, 10)
# 下列Sequential与上面的部分是等效的,可以直接调用x = self.model1(x)
self.model1 = nn.Sequential(
nn.Conv2d(3, 32, 5, stride=1, padding=2),
nn.MaxPool2d(2),
nn.Conv2d(32, 32, 5, stride=1, padding=2),
nn.MaxPool2d(2),
nn.Conv2d(32, 64, 5, stride=1, padding=2),
nn.MaxPool2d(2),
nn.Flatten(),
nn.Linear(1024, 64),
nn.Linear(64, 10)
)
def forward(self, input):
output = self.model1(input)
return output

myNet = MyNet()
print(myNet)
# 检验网络正确性,即是否按照预想的过程进行,可以定义个全是1的数组作为输入
input = torch.ones((64, 3, 32, 32))
output = myNet(input)
# 可视化网络结构
writer = SummaryWriter("logs/nn_seq")
writer.add_graph(myNet, input)
writer.close()

Loss Functions 损失函数

torch.nn — PyTorch 2.0 documentation

L1Loss

orch.nn.L1Loss(size_average=None, reduce=None, reduction=‘mean’)L1Loss — PyTorch 2.0 documentation

1
2
loss = nn.L1Loss()
output = loss(input, target)

要求input和target是同维度的数组

  • reduction:有’mean’和’sum’,mean是总的loss除以总的个数,sum是总的loss相加

[MSELoss 平方差损失](MSELoss — PyTorch 2.0 documentation)

[CrossEntropyLoss 交叉熵损失](CrossEntropyLoss — PyTorch 2.0 documentation)

torch的优化器

torch.optim — PyTorch 2.0 documentation

优化器的使用方法

构造一个优化器

1
2
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
optimizer = optim.Adam([var1, var2], lr=0.0001)

使用案例

1
2
3
4
5
6
7
8
for input, target in dataset:
def closure():
optimizer.zero_grad() # 清空上一轮的梯度
output = model(input)
loss = loss_fn(output, target)
loss.backward()
return loss
optimizer.step(closure)

对现有网络模型进行修改

完整的模型验证套路_哔哩哔哩_bilibili

对模型的处理

保存和加载模型

  • torch.save(vgg16, "path") & model = torch.load("path.pth")既保存了结构也保存了参数

  • torch.save(vgg16.state_dict(), "path") & model = torch.load("path.pth")只保存了模型参数(model是字典形式)

第二种方法是官方推荐的,文件会小一些,参数以字典的形式保存,因此需要对参数进行加载:

1
vgg16.load_state_dict(torch.load("path"))

同时,第一种方法在另外的文件中导入时,仍然需要定义模型的类(module的那个),也可以直接import 这个类

一个完整的模型训练套路

CIFAR10 的网络结构

GPU的使用

只有网络模型初始化,损失函数初始化,和输入的imgs&targets需要使用GPU(即cuda)(有两种用法)

1
2
3
4
5
device = torch.device("cuda:0")  # torch.device("cuda:0" if torch.cuda.is_available else "cpu")
myNet = myNet.to(device)
loss = loss.to(device)
imgs = imgs.to(device)
targets = targets.to(device)

注:Google提供了一种免费的GPU(有限时)https://colab.research.google.com/

完整网络结构的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import torch
from torch import nn
import torchvision
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter

train_set = torchvision.datasets.CIFAR10("DataSet/CIFAR10_dataset", train=True, transform=torchvision.transforms.ToTensor())
test_set = torchvision.datasets.CIFAR10("DataSet/CIFAR10_dataset", train=False, transform=torchvision.transforms.ToTensor())

# 查看数据集的长度 len()
len_train = len(train_set)
len_test = len(test_set)
print("训练集和测试集的长度分别为{}和{}".format(len_train, len_test))

train_dataset = DataLoader(train_set, batch_size=64, shuffle=True)
test_dataset = DataLoader(test_set, batch_size=64, shuffle=True)

class MyNet(nn.Module):
def __init__(self):
super(MyNet, self).__init__() # 初始化父类
self.model1 = nn.Sequential(
nn.Conv2d(3, 32, 5, stride=1, padding=2),
nn.MaxPool2d(2),
nn.Conv2d(32, 32, 5, stride=1, padding=2),
nn.MaxPool2d(2),
nn.Conv2d(32, 64, 5, stride=1, padding=2),
nn.MaxPool2d(2),
nn.Flatten(),
nn.Linear(64*4*4, 64),
nn.Linear(64, 10)
)

def forward(self, input):
output = self.model1(input)
return output

device = torch.device("cuda:0")
myNet = MyNet()
myNet = myNet.to(device)
# 使用一个样例对网络结构进行测试
# test_sample = torch.ones((64, 3, 32, 32))
# test_output = myNet(test_sample)


writer = SummaryWriter("logs/train")
train_step = 0
test_step = 0
epoch = 10
loss = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(myNet.parameters(), lr=0.005)

for i in range(epoch):
print("------------------当前训练轮数:{}------------------".format(i + 1))

# 训练步骤开始 每个batch的训练
# myNet.train() 只对Dropout 和 BatchNorm有作用
for data in train_dataset:
optimizer.zero_grad()
imgs, targets = data
imgs = imgs.to(device)
targets = targets.to(device)
output = myNet.forward(imgs)
train_loss = loss(output, targets)
train_loss = train_loss.to(device)
# 优化器调优
train_loss.backward()
optimizer.step()
train_step += 1
if train_step % 100 == 0:
print("训练次数:{},Loss:{}".format(train_step, train_loss.item()))
writer.add_scalar("train_100", train_loss, train_step)

# 测试步骤开始 with代码块内设置torch不使用梯度,也就是不训练
# myNet.eval() 只对Dropout 和 BatchNorm有作用
total_test_loss = 0
test_step += 1
total_precision = 0
with torch.no_grad():
for data in test_dataset:
imgs, targets = data
imgs = imgs.to(device)
targets = targets.to(device)
output = myNet(imgs)
# output的每行是对一个图像属于哪一类的概率
# argmax(0)是返回每列的最大值,argmax(1)是返回每行的最大值
precision = (output.argmax(1) == targets).sum()
total_precision += precision
test_loss = loss(output, targets)
test_loss = test_loss.to(device)
total_test_loss += test_loss

print("整个训练集的Loss:{}".format(total_test_loss))
writer.add_scalar("test_loss", total_test_loss, test_step)
print("整个训练集的Precision:{}".format(total_precision / len_test))
writer.add_scalar("test_precision", total_precision / len_test, test_step)

torch.save(myNet.state_dict(), "model/myNet_{}.pth".format(i))
print("第{}轮模型已保存".format(i))

writer.close()

图像的标注

labelimg软件 以及 VOC格式

补充内容

在当前文件夹下读取另一个文件夹的图片

使用../../意味着从当前文件夹路径返回根目录(返回上一层目录),然后在根目录后重新访问另一个文件夹


Pytorch学习笔记
http://example.com/2023/05/17/Pytorch学习笔记/
作者
Mr.Yuan
发布于
2023年5月17日
许可协议