PyTorch快速上手指南

PyTorch 深度学习框架快速上手指南

PyTorch 可以说是目前最常用的深度学习框架 , 常应用于搭建深度学习网络 , 完成一些深度学习任务 (CV、NLP领域)

要想快速上手 PyTorch , 你需要知道什么 :

  1. 一个项目的完整流程 , 即到什么点该干什么事
  2. 几个常用 (或者说必备的) 组件

剩下的时间你就需要了解 , 完成什么任务 , 需要什么网络 , 而且需要用大量的时间去做这件事情


$^{(e.g.)}$例如 : 你现在有一个图像分类任务 , 完成该任务需要什么网络, 你需要通过查找资料来了解需要查找什么网络。

需要注意的是 , 有一些常识性的问题你必须知道 , 例如: 图像层面无法或很难使用机器学习方法 , 卷积神经网络最多的是应用于图像领域等


下面我将通过一个具体的分类项目流程来讲述到什么点该干什么事

一个完整的 PyTorch 分类项目需要以下几个方面:

  1. 准备数据集
  2. 加载数据集
  3. 使用变换(Transforms模块)
  4. 构建模型
  5. 训练模型 + 验证模型
  6. 推理模型

  1. 准备数据集 一般来说 , 比赛会给出你数据集, 不同数据集的组织方式不同 , 我们要想办法把他构造成我们期待的样子
    • 分类数据集一般比较简单, 一般是将某个分类的文件全都放在一个文件夹中, 例如:
    • 二分类问题 : Fake(文件夹) / Real(文件夹)
    • 多分类问题 : 分类 1(文件夹) / 分类 2(文件夹) / … / 分类 N(文件夹)
    • 当然有些时候他们会给出其他方式 , 如 UBC-OCEAN , 他们将所有的图片放在一个文件夹中 , 并用 csv 文件存储这些文件的路径(或者是文件名) , 然后在 csv 文件中进行标注(如下):

    • 以后你可能还会遇到更复杂的目标检测的数据集, 这种数据集会有一些固定格式 , 如 VOC格式 , COCO格式
    • 在数据集方面 , 需要明确三个概念——训练集、验证集和测试集 , 请务必明确这三个概念 , 这是基本中的基本
      • 训练集(Train) : 字如其名 , 简单来说就是知道数据 , 也知道标签 的数据 , 我们用其进行训练
      • 验证集(Valid) : 验证集测试集 是非常容易混淆的概念 , 简单来说 , 验证集就是我们也知道数据和标签 , 但是我们的一般不将这些数据用于训练 , 而是将他们当作我们的测试集 , 即我们已经站在了出题人的角度 , 给出参赛者输入数据 , 而我们知道这个数据对应的输出 , 但是我们不让模型知道
      • 测试集(Test) : 测试集就是 , 我们不知道输入数据的输出标签 , 只有真正的出题人知道 , 一般来说 , 我们无法拿到测试集 , 测试集是由出题人掌控的
      • 需要注意的是 , 如果你通过某种途径知道了所有的测试集的标签时 , 不可使用测试集进行训练 , 这是非常严重的学术不端行为 , 会被学术界和工业界唾弃
1
2
3
4
5
6
7
8
9
10
11
# 现在我们已经有了一个数据集 , 我将以 FAKE_OR_REAL 数据集为例 , 展示我们数据集的结构
# D:\REAL_OR_FAKE\DATASET
# ├─test --------- 测试集路径, 这里可以放你自己的数据, 你甚至可以将他们分类, 但是请注意, 实际情况下你只能通过这种方式来“得到”测试集
# │ ├─fake ------ 你自己分的类, 开心就好
# │ └─real ------ 同上
# ├─train -------- 训练集路径, 这里面放的是题目给出的数据, 下面有 fake 和 real 两个文件夹, 这两个文件夹中就是两个类别, 我们要用这里面的图片进行分类
# │ ├─fake
# │ └─real
# └─valid -------- 验证集路径, 这里面放的是题目给出的数据, 下面有 fake 和 real 两个文件夹, 这两个文件夹中就是两个类别, 这里面的图片不需要进行训练
# ├─fake
# └─real
  1. 加载数据集
    • 请务必记住 , 不管是什么数据集 , 数据集是如何构成的 , 在使用 PyTorch 框架时 , 我们都要像尽办法将他们加载入 Dataset 类中
    • 简单来说 , Dataset 类就是描述了我们数据的组成的类
    • 需要注意 , PyTorch 实现了许多自己的 Dataset 类 , 这些类可以轻松的加载特定格式的数据集 , 但是我强烈建议所有的数据集都要自己继承Dataset类 , 自行加载 , 这样我们可以跟清晰的指导数据集的组成方式 , 也可以使得我们加载任意格式的数据集
    • 实现 DataSet 类需要我们先继承 Dataset 类 , 在继承 Dataset 类后, 我们只需要实现其中的__init____len____getitem__三个方法 , 即可完成对数据集的加载 , 这三个方法就和他的名字一样 :
      • __init__ 方法是构造函数 , 用于初始化
      • __len__ 方法用于获取数据集的大小
      • __getitem__ 方法用于获取数据集的元素 , 我将从下面的代码中进行更详细的解释
    • 有些数据集并不分别提供 Train训练集 和 Valid验证集, 我们可以使用 random_split() 方法对数据集进行划分
      • 需要注意的是, 每次重新划分数据集时, 必须重新训练模型, 因为 random_split() 方法随机性, 划分后的数据不可能和之前的数据完全重合, 因此会导致数据交叉的情况, 下面一段使用 random_split() 进行划分的 Python 代码示例 :

        1
        2
        3
        4
        5
        6
        7
        8
        # 下面演示使用 random_split 来划分数据集的操作
        # 我们假设已经定义了 CustomImageDataSet
        split_ratio = 0.8 # 表示划分比例为 8 : 2
        dataset = CustomImageDataSet(fake_dir, real_dir) # 定义 CustomImageDataSet 类, 假设此时没有划分训练集和验证集
        train_dataset_num = int(dataset.lens * split_ratio) # 定义训练集的大小
        valid_dataset_num = dataset.lens - train_dataset_num # 定义验证集的大小
        # random_split(dataset, [train_dataset_num, valid_dataset_num]) 表示将 dataset 按照 [train_dataset_num: valid_dataset_num] 的比例进行划分
        train_dataset, valid_dataset = random_split(dataset, [train_dataset_num, valid_dataset_num])
      • 当数据集不是很大的时, 推荐人为的将数据集进行划分, 可以写一个 Python 脚本(.py) 或者 批处理脚本(.bat) 来完成这个操作

完整的数据集加载代码如下:

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
import torch
from torch.utils.data import Dataset
import os
from PIL import Image

# 这里我们定义了一个 CustomImageDataset(...) 类, 括号中的内容表示我们继承了 ... 类
# 因此我们这里 CustomImageDataset(Dataset): 表示我们定义了一个“自定义图片”类, 这个“自定义图片”类继承自 Dataset 类
class CustomImageDataset(Dataset):
# 这里我们实现 __init__ 方法, __init__ 方法其实就是一个类的构造函数, 他也分有参构造和无参构造, 只是在这里我们说无参构造基本没啥意义
# 因此我们常常实现这个类, 使得可以指定这个类的输入输出
# 比如下面我们写的 def __init__(self, fake_dir, real_dir, transform=None):
# self : 自己, 我一般直接理解为 this 指针, 如果有兴趣了解更深层的东西可以查阅一些资料, 这个是必填的
# fake_dir : 用于指定 fake 类型图片的位置的
# real_dir : 用于指定 real 类型图片的位置的
# transform : 用于指定变换, 简单来说就是对输入进行某些操作, 我会在下面的板块中进行详细叙述
def __init__(self, fake_dir, real_dir, transform=None):
self.fake_dir = fake_dir # 这里表示这个类内定义了一个 fake_dir, 其值为传入的 fake_dir
self.real_dir = real_dir # 这里表示这个类内定义了一个 real_dir, 其值为传入的 real_dir
self.transform = transform # 这里表示这个类内定义了一个 transform, 其值为传入的 transform, 当没有传入时, 这个变量为 None

self.fake_images = os.listdir(fake_dir) # 传入的 fake_dir 是一个路径, 我们使用 os.listdir(fake_dir) 可以加载 fake_dir 文件夹下的内容, 也就是所有 fake 图片
self.real_images = os.listdir(real_dir) # 传入的 real_dir 是一个路径, 我们使用 os.listdir(real_dir) 可以加载 real_dir 文件夹下的内容, 也就是所有 real 图片

self.total_images = self.fake_images + self.real_images # 总图片列表, 就是将 fake 图片列表和 real 图片列表进行组合
self.labels = [0]*len(self.fake_images) + [1]*len(self.real_images) # 对图片打标签, fake 为 0, real 为 1
# [0] * 10 得到的结果为 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# [1] * 10 得到的结果为 [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

# 这里我们实现 __len__ 方法, 这个方法用于获取数据集的大小
def __len__(self):
return len(self.total_images) # 这里我们直接返回总图片列表的长度即可, 这里的实现方式不唯一, 只要能做到表示数据集大小即可

# 这里我们实现 __getitem__ 方法, 这个方法用于获取数据集中的某个元素
# 其中 idx 表示索引, 这个参数是必须的, 当然可以起其他名字, 不过最好还是使用 idx
# __getitem__(self, idx) 表示获取 idx 位置的元素
def __getitem__(self, idx):
# 这里表示获取一个元素的逻辑
# 当 idx 位置的标签为 0 时, 图片的路径为 fake_dir + self.total_images[idx], idx 即为图片的索引位置
# 当 idx 位置的标签为 1 时, 图片的路径为 real_dir + self.total_images[idx]
image_path = os.path.join(self.fake_dir if self.labels[idx] == 0 else self.real_dir, self.total_images[idx])

# 使用 PIL 库加载图片, 通过 image_path 打开图片, 并且将图片转化为 RGB 格式
image = Image.open(image_path).convert('RGB')

# 这里是 transform, 表示变换, 当其值为 None 时不进行操作, 当传入自己的 transform 时即为非空, 即对输入数据进行变换
if self.transform:
# 我们将变换后的图片直接保存在原位置
image = self.transform(image)

# 最后函数的返回值为 image 和 self.labels[idx], 即表示索引位置 idx 处的图片和标签
return image, self.labels[idx]
  1. 使用 Transforms
    • 不要简单的使用原始图片进行训练 , 当然如果一定要使用原始图片进行训练, 也可以使用 transforms 模块
    • 一般来说, 训练集和验证集的 transforms 是不同的, 因为我们希望验证集和测试集的图片贴合真实的情况
    • 下面的代码演示了如何定义 transforms
    • 在定义完 transforms 我们就可以完全定义我们的 DatasetDataloader
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
import torch
from torchvision import transforms

# 定义transform
# transforms.Compose(transforms) 实际上就是将多个 transform 方法变为逐步执行, 一般我们直接使用这种方式来对图片进行连续的变换
train_transform = transforms.Compose([
transforms.RandomHorizontalFlip(), # 随机水平翻转
transforms.RandomVerticalFlip(), # 随机垂直翻转
transforms.ColorJitter(brightness=0.1, contrast=0.1, saturation=0.1), # 改变图像的属性, 将图像的brightness亮度/contrast对比度/saturation饱和度/hue色相 随机变化为原图亮度的 10%
transforms.RandomResizedCrop(224, scale=(0.8, 1.0)), # 对图片先进行随机采集, 然后对裁剪得到的图像缩放为同一大小, 意义是即使只是该物体的一部分, 我们也认为这是该类物体
transforms.RandomRotation(40), # 在[-40, 40]范围内随机旋转
transforms.RandomAffine(degrees=0, shear=10, scale=(0.8,1.2)), # 随机仿射变换
transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2), # 色彩抖动
transforms.ToTensor(), # [重点] 将图片转化为 Tensor 张量, 在 PyTorch 中, 一切的运算都基于张量, 请一定将你的输入数据转化为张量
# 请理解什么是张量 : 我们在线性代数中有向量的概念, 简单来说就是张量就是向量, 只不过张量往往具有更高的维度
# 而大家一般习惯将高于三维的向量称为张量, 某些人(比如我)也习惯所有的向量统称为张量
# 可以简单的将数组的维数来界定张量的维度
# 例如 [ ] 为一维张量, [[ ]] 为二维张量, [[[ ]]]为三维张量, [[[[ ]]]]为四维张量
# 对于图像来说, jpg 图像实际为三维矩阵, png 图像实际为四维矩阵, 这个维数是根据图像的通道数进行划分的
# 例如 jpg 有 R、G、B三个通道, png 具有
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), # 归一化, 可以对矩阵进行归一化
# 详细查看这个Blog : https://blog.csdn.net/qq_38765642/article/details/109779370
transforms.RandomErasing() # 随机擦除
])

valid_transform = transforms.Compose([
transforms.Resize((256, 256)), # Resize 操作, 将图片转换到指定的大小
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from torch.utils.data import DataLoader

# 定义 Dataset 实例
train_dataset = CustomImageDataset(fake_dir="./dataset/train/fake", real_dir="./dataset/train/real", transform=train_transform)
valid_dataset = CustomImageDataset(fake_dir="./dataset/valid/fake", real_dir="./dataset/valid/real", transform=valid_transform)

# 创建 DataLoader 实例
# 这里将要涉及到超参数的概念, 什么是超参数: 简单来将, 超参数就是我们自己能指定的一些数据, 超参数的选择将很大程度上影响模型的性能
# 因此 深度学习领域的工程师 常称自己为 炼丹师、调参师等
batch_size = 32 # batch_size 就是一个超参数, batch 即为 “批次”, 表示一次使用 DataLoader 加载多少张图片进行运算
# 这个数值并不是越大越好, 也不是越小越好, 但是往往大一些比较好, 这个数字最大能选择多大和你的图片大小和显卡显存有很大的关系
# 当出现 [Out Of Memery] 错误时往往表明你选取了过大的 batch_size, 导致显卡出现了爆显存的问题
# batch_size : 每次训练时,模型所看到的数据数量。它是决定训练速度和内存使用的重要参数。
# shuffle : 是否在每个训练周期之前打乱数据集的顺序。这对于许多模型(如卷积神经网络)是很有帮助的,因为它可以帮助模型避免模式识别。
# sampler : 定义如何从数据集中抽样。默认情况下,它使用随机采样。但你可以使用其他更复杂的采样策略,如学习率调度采样。
# batch_sampler : 与sampler类似,但它在批处理级别上进行采样,而不是在整个数据集上。这对于内存使用效率更高的场景很有用。
# num_workers : 定义了多少个工作进程用于数据的加载。这可以加快数据加载的速度,但需要注意内存的使用情况。
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
valid_loader = DataLoader(valid_dataset, batch_size=batch_size, shuffle=False)
1
2
3
4
5
6
7
8
9
10
11
# 查看Dataloader数据
# 为了了解Dataloader中的数据, 我们可以使用以下方法来查看:
# 使用 Python 的 len() 函数 : 我们可以直接通过 len() 函数获取 Dataloader 的长度, 即数据集中数据块的数量
# 使用 torch.utils.data.DataLoader.len() 方法 : 这个方法也会返回Dataloader的长度。
# 使用 iter() 函数:Dataloader是一个可迭代对象,我们可以直接通过iter()函数对其进行迭代,以获取每个批次的数据。
# 使用torchvision.utils.save_image()函数 : 如果我们正在处理的是图像数据集,那么可以使用这个函数来保存Dataloader中的图像数据。
len(train_loader) # 401
len(valid_loader) # 100
images, labels = next(iter(train_loader))
print(images)
print(labels)
  1. 构建模型
    • 构建模型是比较重要的一部分, 一般来说做好数据集之后, 最重要的事情就是修改模型, 通过训练结果改进模型, 判断自己的模型的正确性, 这里就是整个你要用到的神经网络的部分 , 需要注意的是 , 这里指定什么输入 , 推理的时候就要指定什么输入
    • 简单用几个符号说明一下就是: $^{Train} model (inputX, inputY, …)$ → $^{Valid} model (inputX, inputY, …)$
      • 如何确定输入是什么: 看 forward() 的输入是啥模型的输入就是啥
    • 我下面展现了我复现的 ResNet50 , 用这种方式可以顺便教你如何复现网络结构
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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import torch.nn as nn
from torch.nn import functional as F

# 这里是对 ResNet50 的实现, 请对照论文来进行对照阅读
# 定义 ResNet50Basic类, 这里并不是完整的模型, 而是模型的一个部分
class ResNet50BasicBlock(nn.Module):
def __init__(self, in_channel, outs, kernerl_size, stride, padding):
# super(ResNet50BasicBlock, self).__init__() 这里是干什么的?
# 1. 首先找到 ResNet50BasicBlock 的父类, 这里是 nn.Module
# 2. 把类 ResNet50BasicBlock 的对象self转换为 nn.Module 的对象
# 3. "被转换"的 nn.Module 对象调用自己的 init 函数
# 简单理解一下就是 : 子类把父类的 __init__ 放到自己的 __init__ 当中, 这样子类就有了父类的 __init__ 的那些东西
super(ResNet50BasicBlock, self).__init__()
# 这里只是定义部分, 在这里的定义并不一定会在推理过程中使用
self.conv1 = nn.Conv2d(in_channel, outs[0], kernel_size=kernerl_size[0], stride=stride[0], padding=padding[0])
self.bn1 = nn.BatchNorm2d(outs[0])
self.conv2 = nn.Conv2d(outs[0], outs[1], kernel_size=kernerl_size[1], stride=stride[0], padding=padding[1])
self.bn2 = nn.BatchNorm2d(outs[1])
self.conv3 = nn.Conv2d(outs[1], outs[2], kernel_size=kernerl_size[2], stride=stride[0], padding=padding[2])
self.bn3 = nn.BatchNorm2d(outs[2])

# 输入是啥看 forward(), 例如这里是 forward(self, x), 则表示输入是 x, 也就是一个
def forward(self, x):
# nn.Conv2d 是卷积层, 请了解[1]什么是卷积层, 以及[2]卷积层是干啥用的, [3]卷积后会变成什么
# 卷积运算的目的是提取输入的不同特征, 第一层卷积层可能只能提取一些低级的特征如边缘、线条和角等层级, 更多层的网路能从低级特征中迭代提取更复杂的特征
out = self.conv1(x)
# [*] 什么是 ReLU, ReLU是激活函数, 请了解 [1]什么是激活函数, [2]为什么要使用激活函数
# [*] 什么是 Batch Normalization层, BN 层是批次归一化层
out = F.relu(self.bn1(out))
out = self.conv2(out)
out = F.relu(self.bn2(out))
out = self.conv3(out)
out = self.bn3(out)
return F.relu(out + x)


# 定义 ResNet50DownBlock类, 这里并不是完整的模型, 而是模型的一个部分
class ResNet50DownBlock(nn.Module):
def __init__(self, in_channel, outs, kernel_size, stride, padding):
super(ResNet50DownBlock, self).__init__()
self.conv1 = nn.Conv2d(in_channel, outs[0], kernel_size=kernel_size[0], stride=stride[0], padding=padding[0])
self.bn1 = nn.BatchNorm2d(outs[0])
self.conv2 = nn.Conv2d(outs[0], outs[1], kernel_size=kernel_size[1], stride=stride[1], padding=padding[1])
self.bn2 = nn.BatchNorm2d(outs[1])
self.conv3 = nn.Conv2d(outs[1], outs[2], kernel_size=kernel_size[2], stride=stride[2], padding=padding[2])
self.bn3 = nn.BatchNorm2d(outs[2])

self.extra = nn.Sequential(
nn.Conv2d(in_channel, outs[2], kernel_size=1, stride=stride[3], padding=0),
nn.BatchNorm2d(outs[2])
)

def forward(self, x):
x_shortcut = self.extra(x)
out = self.conv1(x)
out = self.bn1(out)
out = F.relu(out)

out = self.conv2(out)
out = self.bn2(out)
out = F.relu(out)

out = self.conv3(out)
out = self.bn3(out)
return F.relu(x_shortcut + out)


class ResNet50(nn.Module):
def __init__(self):
super(ResNet50, self).__init__()
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)

# Sequential 类是 torch.nn 模块中的一个容器, 可以将多个层封装在一个对象中, 方便顺序连接
self.layer1 = nn.Sequential(
ResNet50DownBlock(64, outs=[64, 64, 256], kernel_size=[1, 3, 1], stride=[1, 1, 1, 1], padding=[0, 1, 0]),
ResNet50BasicBlock(256, outs=[64, 64, 256], kernerl_size=[1, 3, 1], stride=[1, 1, 1, 1], padding=[0, 1, 0]),
ResNet50BasicBlock(256, outs=[64, 64, 256], kernerl_size=[1, 3, 1], stride=[1, 1, 1, 1], padding=[0, 1, 0]),
)

self.layer2 = nn.Sequential(
ResNet50DownBlock(256, outs=[128, 128, 512], kernel_size=[1, 3, 1], stride=[1, 2, 1, 2], padding=[0, 1, 0]),
ResNet50BasicBlock(512, outs=[128, 128, 512], kernerl_size=[1, 3, 1], stride=[1, 1, 1, 1], padding=[0, 1, 0]),
ResNet50BasicBlock(512, outs=[128, 128, 512], kernerl_size=[1, 3, 1], stride=[1, 1, 1, 1], padding=[0, 1, 0]),
ResNet50DownBlock(512, outs=[128, 128, 512], kernel_size=[1, 3, 1], stride=[1, 1, 1, 1], padding=[0, 1, 0])
)

self.layer3 = nn.Sequential(
ResNet50DownBlock(512, outs=[256, 256, 1024], kernel_size=[1, 3, 1], stride=[1, 2, 1, 2], padding=[0, 1, 0]),
ResNet50BasicBlock(1024, outs=[256, 256, 1024], kernerl_size=[1, 3, 1], stride=[1, 1, 1, 1], padding=[0, 1, 0]),
ResNet50BasicBlock(1024, outs=[256, 256, 1024], kernerl_size=[1, 3, 1], stride=[1, 1, 1, 1], padding=[0, 1, 0]),
ResNet50DownBlock(1024, outs=[256, 256, 1024], kernel_size=[1, 3, 1], stride=[1, 1, 1, 1], padding=[0, 1, 0]),
ResNet50DownBlock(1024, outs=[256, 256, 1024], kernel_size=[1, 3, 1], stride=[1, 1, 1, 1], padding=[0, 1, 0]),
ResNet50DownBlock(1024, outs=[256, 256, 1024], kernel_size=[1, 3, 1], stride=[1, 1, 1, 1], padding=[0, 1, 0])
)

self.layer4 = nn.Sequential(
ResNet50DownBlock(1024, outs=[512, 512, 2048], kernel_size=[1, 3, 1], stride=[1, 2, 1, 2], padding=[0, 1, 0]),
ResNet50DownBlock(2048, outs=[512, 512, 2048], kernel_size=[1, 3, 1], stride=[1, 1, 1, 1], padding=[0, 1, 0]),
ResNet50DownBlock(2048, outs=[512, 512, 2048], kernel_size=[1, 3, 1], stride=[1, 1, 1, 1], padding=[0, 1, 0])
)

self.avgpool = nn.AvgPool2d(kernel_size=7, stride=1, ceil_mode=False)
# self.avgpool = nn.AdaptiveAvgPool2d(output_size=(1, 1))

self.fc = nn.Linear(2048, 10)
# 使用卷积代替全连接
self.conv11 = nn.Conv2d(2048, 10, kernel_size=1, stride=1, padding=0)

def forward(self, x):
out = self.conv1(x)
out = self.maxpool(out)
out = self.layer1(out)
out = self.layer2(out)
out = self.layer3(out)
out = self.layer4(out)
# avgpool 平均池化层, 了解什么是平均池化层
out = self.avgpool(out)
out = self.conv11(out)
out = out.reshape(x.shape[0], -1)
# out = self.fc(out)
return out

# 这里展现了对 ResNet 的一个具体的应用
# x = torch.randn(1, 3, 224, 224) # 这个是我们 ResNet50 期待的输入样子, 可以看到他是 [1] 个 [3] 通道, 宽度为[224], 高度为 [224]的张量
image_path = './dataset/test/fake/test_fake_1.png'
image = Image.open(image_path).convert('RGB') # 图片加载
transform = transforms.ToTensor() # 将图片转化为张量, 此时的 张量的形状为[3, 1024, 1024]
# 当输入数据的维度不足时, 我们可以通过 unsqueeze() 添加维度, 这个东西简单理解一下就是, 在某个维度外面加括号[], 即可拓展出更高的维度
img_tensor = transform(image).unsqueeze(dim=0)

# print(x.shape) 我们可以使用 shape 来查看一个张量的形状
# print(img_tensor.shape)

# 这里加载我们的网络架构
net = ResNet50()

# 这里进行输入, 输入 img_tensor, 进入 forword() 部分, 然后得到最终输出的结果
out = net(img_tensor)
print(out)
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
import torch
from torchvision import models

# 这里为了方便, 我们直接加载 PyTorch 预训练好的 ResNet50 的模型
# PyTorch 已经为我们提供了不少已经预训练好的模型, 我们只需要加载他们与训练好的模型即可
# 但是我还是希望你可以掌握上面这种自定义模型的方法, 这样遇到 PyTorch 未提供的模型, 我们也可以尝试自己实现该模型
model = models.resnet50(pretrained=True)

# 冻结参数 : 即不更新模型的参数
# 可以看到下面的代码, 这里表示冻结了所有层
for param in model.parameters():
param.requires_grad = False

# 但是我们可以通过替换层来接触某些层的冻结
num_ftrs = model.fc.in_features # 这里是获取 ResNet50 的 fc 层的输入特征数
model.fc = torch.nn.Linear(num_ftrs, 2) # 这里是对 fc 层进行修改, Linear(input_feather_num, output_feather_num)
# 这里输入特征数是 num_ftrs, 输出特征数为 2

# 这一行很重要, 指定了模型的位置, cuda 可以理解为 GPU 设备, cuda: 0 表示使用编号为 0 的GPU进行训练
# 当有多块 GPU 时, 可以用其他的方式指定 GPU
# model = torch.nn.DataParallel(model, device_ids=[0, 1, 2]), 当然向我们这种小白(穷B), 当然还是单卡为主
# 为了避免出现多卡的情况, 我在下面放入两篇博客, 有兴趣可以参考这两篇文章进行多卡训练
# https://zhuanlan.zhihu.com/p/102697821
# https://blog.csdn.net/qq_34243930/article/details/106695877
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model = model.to(device)
  1. 训练模型 + 验证模型
    • 这里需要直接对模型进行训练 , 一般来说 , 在训练的过程中我们会加入 tqdm 库使得训练过程可视化 , 有时我们还会在训练过程中保存更好的训练结果 , 并且设置断点训练等操作 , 我只使用最简单的方式进行预测
    • train 部分的代码因人而异, 基本上每个人的写法都可能不同, 没有固定的写法
    • 对于训练完的模型我们需要对其进行评价, 一般来说, 训练和验证都是放在一起的, 不可分开的
    • 记得保存一下训练后的模型, 使用如下代码保存/加载整个模型
      1
      2
      3
      4
      5
      6
      # 保存模型
      model_path = "xxxx.pth" # xxxx 表示一个你喜欢的名字
      torch.save(model, model_path) # 使用 torch.save(model, model_path) 保存模型

      # 加载模型
      model = torch.load(model_path) # 使用 torch.load(model_path) 即可加载模型

完整的”训练模型 + 验证模型”代码如下:

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
from tqdm import tqdm
from sklearn.metrics import f1_score, accuracy_score

# 定义损失函数和优化器
# 这里包含了 PyTorch 的 19 种损失函数 https://blog.csdn.net/qq_35988224/article/details/112911110
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters())

# 计算 F1 值和 准确率
def evaluate(loader, model):
preds = []
targets = []
loop = tqdm(loader, total=len(loader), leave=True)
for images, labels in loop:
images, labels = images.to(device), labels.to(device)
with torch.no_grad():
outputs = model(images)
_, predicted = torch.max(outputs, 1)
preds.extend(predicted.cpu().numpy())
targets.extend(labels.cpu().numpy())

# Update the progress bar
loop.set_description("Evaluating")
return f1_score(targets, preds), accuracy_score(targets, preds)

# 训练循环
best_f1 = 0.0
loss_values = []
num_epochs = 10 # 定义训练的轮次
for epoch in range(num_epochs):
model.train() # 将模型设置为训练模式
loop = tqdm(train_loader, total=len(train_loader), leave=True)
print(loop)
for images, labels in loop:
images, labels = images.to(device), labels.to(device)

# 前向推理
outputs = model(images)
loss = criterion(outputs, labels)

# 反向传播及优化
# 在用 PyTorch训练模型时, 通常会在遍历 Epochs 的过程中依次用到
# optimizer.zero_grad() : 先将梯度归零
# loss.backward() : 反向传播计算得到每个参数的梯度值
# optimizer.step() : 通过梯度下降执行一步参数更新
# 对于这三个函数, 这篇博客写的很好 : https://blog.csdn.net/PanYHHH/article/details/107361827
# 可以简单阅读一遍
optimizer.zero_grad()
loss.backward()
optimizer.step()

# 保存该批次的损失
loss_values.append(loss.item())

# 更新进度条
loop.set_description(f"Epoch [{epoch + 1}/{num_epochs}]")
loop.set_postfix(loss=loss.item())

# 在每轮之后验证模型
model.eval() # 将模型设置为推理模式, 此时模型中的参数不会进行更新, 即完全用于推理/验证
f1_value, accuracy = evaluate(valid_loader, model)
print(f'F1 score: {f1_value:.4f}, Accuracy: {accuracy:.4f}')

# 保存 F1 值最高的模型
if f1_value > best_f1:
best_f1 = f1_value
# 这里和上面 Markdown 的保存方式不同, model.state_dict(), 表示模型的参数, 简单来说呢我们仅仅保存了模型的参数, 但是我们并没有保存模型的结构
# 上面 Markdown 的保存方式是即保存了整个模型的结构, 也保存了模型的参数
torch.save(model.state_dict(), 'best_model.pth')
print('训练结束')

当然我们也可以使用绘图函数,来展示过程中的相关数据。

1
2
3
4
5
6
7
8
9
10
import matplotlib.pyplot as plt

plt.figure(figsize=(12, 8))
plt.plot(loss_values, label='Train Loss')
plt.title('Loss values over epochs')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.grid(True)
plt.show()
  1. 推理模型
    • 很高兴, 如果你到这一步, 你的水平肯定已经有了质的飞跃, 这里已经是最后一步了, 结束这个部分, 你就要开始自己的探索之路了
    • 推理模型很简单, 我在上面说过, 构造模型时指定什么输入 , 推理的时候就要指定什么输入, 这里就是对应的部分了
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
from torchvision.transforms import ToTensor, Resize, Normalize

# predict_by_file 表示推理一个文件, 我们需要传入文件路径以及模型
def predict_by_file(file_path, model):
#
image = Image.open(file_path).convert('RGB')

# 这里的 transform 有与没有都无所谓, 纯看心情
transform = transforms.Compose([
Resize((256, 256)),
ToTensor(),
Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])

image = transform(image)

# 这里和上面一样, 表示在最外面加一层括号, 使 [3, 256, 256] 变为 [1, 3, 256, 256]
image = image.unsqueeze(0).to(device)

model.eval()
with torch.no_grad():
outputs = model(image) # 模型推理
# torch.max(...)
# input (Tensor) – 输入张量
# dim (int) – 指定的维度
_, predicted = torch.max(outputs, 1) # 返回指定维度的最大值, 其实这里只有一维
print(outputs) # tensor([[0.7360, 0.2668]], device='cuda:0')
print(outputs.shape) # torch.Size([1, 2])
return "Fake" if predicted.item() == 0 else "Real"

path = './dataset/test/real/test_real_7.jpg'
print(predict_by_file(path, model))
作者

NilEra

发布于

2024-03-20

更新于

2024-06-22

许可协议

评论