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

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

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

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

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

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


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

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


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

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

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

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


      UBC_OCEAN_Dataset
      UBC_OCEAN_Dataset_CSV

    • 以后你可能还会遇到更复杂的目标检测的数据集, 这种数据集会有一些固定格式 , 如 VOC格式 , COCO格式
    • 在数据集方面 , 需要明确三个概念——训练集、验证集和测试集 , 请务必明确这三个概念 , 这是基本中的基本
      • 训练集(Train) : 字如其名 , 简单来说就是知道数据 , 也知道标签 的数据 , 我们用其进行训练
      • 验证集(Valid) : 验证集测试集 是非常容易混淆的概念 , 简单来说 , 验证集就是我们也知道数据和标签 , 但是我们的一般不将这些数据用于训练 , 而是将他们当作我们的测试集 , 即我们已经站在了出题人的角度 , 给出参赛者输入数据 , 而我们知道这个数据对应的输出 , 但是我们不让模型知道
      • 测试集(Test) : 测试集就是 , 我们不知道输入数据的输出标签 , 只有真正的出题人知道 , 一般来说 , 我们无法拿到测试集 , 测试集是由出题人掌控的
      • 需要注意的是 , 如果你通过某种途径知道了所有的测试集的标签时 , 不可使用测试集进行训练 , 这是非常严重的学术不端行为 , 会被学术界和工业界唾弃
# 现在我们已经有了一个数据集 , 我将以 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 代码示例 :
      # 下面演示使用 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) 来完成这个操作

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

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
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])
])
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)
# 查看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. 构建模型
    • 构建模型是比较重要的一部分, 一般来说做好数据集之后, 最重要的事情就是修改模型, 通过训练结果改进模型, 判断自己的模型的正确性, 这里就是整个你要用到的神经网络的部分 , 需要注意的是 , 这里指定什么输入 , 推理的时候就要指定什么输入
    • 简单用几个符号说明一下就是: T r a i n m o d e l ( i n p u t X , i n p u t Y , . . . ) ^{Train} model (inputX, inputY, ...) Trainmodel(inputX,inputY,...) V a l i d m o d e l ( i n p u t X , i n p u t Y , . . . ) ^{Valid} model (inputX, inputY, ...) Validmodel(inputX,inputY,...)
      • 如何确定输入是什么: 看 forward() 的输入是啥模型的输入就是啥
    • 我下面展现了我复现的 ResNet50 , 用这种方式可以顺便教你如何复现网络结构
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)
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 部分的代码因人而异, 基本上每个人的写法都可能不同, 没有固定的写法
    • 对于训练完的模型我们需要对其进行评价, 一般来说, 训练和验证都是放在一起的, 不可分开的
    • 记得保存一下训练后的模型, 使用如下代码保存/加载整个模型
# 保存模型
model_path = "xxxx.pth"        # xxxx 表示一个你喜欢的名字
torch.save(model, model_path)  # 使用 torch.save(model, model_path) 保存模型

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

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

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('训练结束')

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

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. 推理模型
    • 很高兴, 如果你到这一步, 你的水平肯定已经有了质的飞跃, 这里已经是最后一步了, 结束这个部分, 你就要开始自己的探索之路了
    • 推理模型很简单, 我在上面说过, 构造模型时指定什么输入 , 推理的时候就要指定什么输入, 这里就是对应的部分了
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))
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值