跳至主要内容
课程笔记约 32 分钟

小土堆 PyTorch 课程笔记

从数据集、TensorBoard 与图像变换开始,整理 PyTorch 网络搭建、训练、保存和 GPU 使用流程。

#PyTorch#TensorBoard#神经网络#模型训练
本文目录
  1. 配置 conda 环境
  2. 使终端前有括号表示环境的操作
  3. 检验是否可用 GPU
  4. python 学习中的两大法宝函数
  5. 加载数据
  6. Tensorboard 的使用
  7. Transforms 的使用
  8. 常见的 Transforms
  9. __call__ 方法介绍
  10. ToTensor 的使用
  11. Normalize 的使用
  12. Resize 的使用
  13. Compose() 用法
  14. torchvision 中的数据集使用
  15. DataLoader 的使用
  16. 神经网络的基本骨架-nn.Module 的使用
  17. 神经网络-卷积层
  18. 神经网络-最大池化的使用
  19. 神经网络-非线性激活
  20. 神经网络-线性层及其他层介绍
  21. 神经网络-搭建小实战和 sequential 的使用
  22. 损失函数与反向传播
  23. L1 损失函数
  24. 平方差损失函数(MSELoss)
  25. 交叉熵损失函数(CrossEntropyLoss)
  26. 反向传播
  27. 优化器
  28. 现有网络模型的使用及修改
  29. 网络模型的保存与读取
  30. 保存
  31. 读取
  32. 完整模型训练套路
  33. 利用 GPU 进行训练
  34. 方法一
  35. 方法二(更常用)
  36. 完整的模型验证套路

配置 conda 环境

conda 的路径选择:D:\program\miniconda\Scripts\conda.exe

使终端前有括号表示环境的操作

先将 conda 初始化:conda init

再新开一个终端就好了

检验是否可用 GPU

import torch
print(torch.cuda.is_available())

安装 jupyter:conda install -c conda-forge nb_conda

安装好后终端输入 jupyter notebook 即可打开

python 学习中的两大法宝函数

每一个 package 可以看作一个工具箱,下面两个函数就是探索工具箱的法宝:

  • dir():打开、看见——让我们知道工具箱里以及工具箱的分隔区里有什么东西
  • help():说明书——让我们知道每个工具的使用方法

加载数据

环境配置好后,正式进入 PyTorch 的第一步:把数据加载进来。先认识几个相关概念:

  • dataset:提供一种方式去获取数据及其 label。
  • dataloader:对 dataset 的数据做一个”打包”,为后面的网络提供不同的数据形式。
  • os:operating system,操作系统。
  • PIL:python image library。

在数据准备阶段使用以下代码:首先创建文件夹,把所有图片按以下逻辑分类;再创建一个类,使我可以使用一个变量就调用其中一个数据集,如 ants_dataset = MyData(root_dir, image_ants, label_ants, transform),这里 ants_dataset 就可以直接调用蚂蚁的数据集了。

小土堆 PyTorch 课程笔记图示

代码的主要功能和意图

  1. 自定义数据集MyData,继承父类 Dataset):

    • 作用: 从文件系统中加载图像和对应的标签。

    • 方式: 图像和标签存储在不同的文件夹中,通过文件名来匹配图像和标签。

    • 实现:

      • 重写了 Dataset__init__, __getitem__, 和 __len__ 方法。

      • 使用 os.listdir 列出图像和标签文件,并通过排序保证一一对应。

      • 使用 Image.open 加载图像,并应用预定义的变换(如调整大小、转换为张量)。

      • 标签通过读取文件内容(f.readline())获取。

  2. 数据集合并:

    • 作用: 将两个不同的数据集(ants_datasetbees_dataset)合并为一个训练数据集。

    • 方式: 使用 + 将两个数据集对象合并为一个新的数据集。

  3. 数据加载DataLoader):

    • 作用: 将数据集分成批次(batches)并加载到内存中。

    • 方式:

      • 设置 batch_size=1num_workers=2,表示每次加载 1 个样本,并使用多线程加速数据加载。

      • 使用 torchvision.transforms 对图像进行预处理(调整大小为 256x256,转换为张量)。

  4. TensorBoard 可视化:

    • 作用: 将数据可视化,以检查数据加载是否正确。

    • 方式:

      • 使用 SummaryWriter 创建日志文件。

      • 通过 add_image 方法将指定索引的样本(train_dataset[119])写入 TensorBoard 日志。

      • 使用 writer.close() 关闭日志写入。

  5. 调试和测试:

    • 意图: 检查数据加载和预处理是否正确。

    • 方式:

      • 通过注释掉的 for 循环,逐个批次打印数据的形状(j['img'].shape)和类型(type(j))。
from torch.utils.data import Dataset, DataLoader
import numpy as np
from PIL import Image
import os
from torchvision import transforms
from torch.utils.tensorboard import SummaryWriter
from torchvision.utils import make_grid

writer = SummaryWriter("logs")

class MyData(Dataset):

    def __init__(self, root_dir, image_dir, label_dir, transform):
        self.root_dir = root_dir
        self.image_dir = image_dir
        self.label_dir = label_dir
        self.label_path = os.path.join(self.root_dir, self.label_dir)
        self.image_path = os.path.join(self.root_dir, self.image_dir)
        self.image_list = os.listdir(self.image_path)
        self.label_list = os.listdir(self.label_path)
        self.transform = transform
        # 因为label 和 Image文件名相同,进行一样的排序,可以保证取出的数据和label是一一对应的
        self.image_list.sort()
        self.label_list.sort()

    def __getitem__(self, idx):
        img_name = self.image_list[idx]
        label_name = self.label_list[idx]
        img_item_path = os.path.join(self.root_dir, self.image_dir, img_name)
        label_item_path = os.path.join(self.root_dir, self.label_dir, label_name)
        img = Image.open(img_item_path)

        with open(label_item_path, 'r') as f:
            label = f.readline()

        # img = np.array(img)
        img = self.transform(img)
        sample = {'img': img, 'label': label}
        return sample

    def __len__(self):
        assert len(self.image_list) == len(self.label_list)
        return len(self.image_list)

if __name__ == '__main__':
    transform = transforms.Compose([transforms.Resize((256, 256)), transforms.ToTensor()])
    root_dir = "dataset/train"
    image_ants = "ants_image"
    label_ants = "ants_label"
    ants_dataset = MyData(root_dir, image_ants, label_ants, transform)
    image_bees = "bees_image"
    label_bees = "bees_label"
    bees_dataset = MyData(root_dir, image_bees, label_bees, transform)
    train_dataset = ants_dataset + bees_dataset

    # transforms = transforms.Compose([transforms.Resize(256, 256)])
    dataloader = DataLoader(train_dataset, batch_size=1, num_workers=2)

    writer.add_image('error', train_dataset[119]['img'])
    writer.close()
    # for i, j in enumerate(dataloader):
    #     # imgs, labels = j
    #     print(type(j))
    #     print(i, j['img'].shape)
    #     # writer.add_image("train_data_b2", make_grid(j['img']), i)
    #
    # writer.close()

Tensorboard 的使用

数据能加载之后,还需要一个能”看见”训练过程的工具。Tensorboard 是用来查看训练过程的图像变化的,能够实时监控训练过程。

  • 安装 Tensorboard 命令:pip install tensorboard
  • 设置 Tensorboard 日志存放的文件夹:终端输入 tensorboard --logdir=logs——logdir 等号后面接的是 tensorboard 日志目录名称
  • 指定主机对应的 Tensorboard 端口:tensorboard --logdir=logs --port=6007——这样 Tensorboard 打开的窗口就变成了 http://localhost:6007/

小结:我对于 Tensorboard 的感觉就是一个强力的辅助工具,在训练一个网络时,我可以在代码中添加几行 Tensorboard 的代码,使训练过程被实时记录。

from torch.utils.tensorboard import SummaryWriter
import numpy as np
from PIL import Image

writer=SummaryWriter("logs")#设置tensorboard日志目录名称
image_path="hymenoptera_data/hymenoptera_data/train/bees/16838648_415acd9e3f.jpg"
img_PIL=Image.open(image_path)
img_array=np.array(img_PIL)
writer.add_image("bee",img_array,1,dataformats='HWC')#看函数名,‘添加图片’,然后传参
for i in range(100):
    writer.add_scalar("y=2x",2*i,i)#看函数名,‘添加数值图表’,设置损失函数的图也用这个

writer.close()

Transforms 的使用

数据读进来之后往往还要做预处理,这就轮到 Transforms 了。它实质指的是 Transforms.py 这个预先定义好的模块,相当于一个工具箱,需要什么功能直接调用即可。

那么如何使用 Transforms 呢?用如下代码进行解释。

from PIL import Image
from torchvision import transforms

img_path="hymenoptera_data/hymenoptera_data/train/ants/0013035.jpg"
img = Image.open(img_path)
tensor_trans = transforms.ToTensor()
tensor_img = tensor_trans(img)
print(tensor_img)

使用的逻辑是:tensor_trans 是我们借助 transforms 里的 ToTensor 类打造的一个工具,它的作用就是将传入的图片转换为张量,tensor_img 就是将 img 转化为张量的结果。

常见的 Transforms

熟悉了 Transforms 的基本用法后,这一节逐个过一遍常用的几个变换。在此之前先补充一个 Python 小知识——__call__ 方法。

__call__ 方法介绍

以如下代码为例:

class person:
    def __call__(self, name):
        self.name = name
        print("__call__"+"hello"+name)
    def hello(self, name):
        print("hello"+name)
person = person()
person("zhangsan")
person.hello("lisi")

运行结果为:

__call__hellozhangsan
hellolisi

小结:__call__ 方法能够让创建的实例可以不用打点调用,可以直接后加括号调用。

ToTensor 的使用

from PIL import Image
from torch.utils.tensorboard import SummaryWriter
from torchvision import transforms

writer = SummaryWriter("logs")#这里writer就成了tensorboard的一个实例,“logs”是设置的tensorboard日志目录名称
img = Image.open("图片路径")
print(img)

trans_totensor = transforms.ToTensor()
img_tensor = trans_totensor(img)
writer.add_image("totensor", img_tensor)
writer.close()

这里的流程:

  1. 创建一个 tensorboard 的实例 writer
  2. 创建一个图片变量 img
  3. 使用 transforms 的 ToTensor 创建一个工具 trans_totensor
  4. 使用这个工具将 img 转换为张量 img_tensor
  5. 再调用 tensorboard 的函数 add_image,将图片张量加载到 tensorboard 中;
  6. 关闭。

Normalize 的使用

基于上段代码

trans_norm = transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
img_norm = trans_norm(img_tensor)
writer.add_image("norm", img_norm)

流程:

  1. 使用 transforms 的 Normalize 创建一个工具 trans_norm,其中第一个参数为平均值 mean[channel],第二个参数为标准差 std[channel],计算方式为 output[channel] = (input[channel] - mean[channel]) / std[channel]
  2. 将图片张量使用 trans_norm 进行标准化;
  3. 再调用 tensorboard 的函数 add_image,将标准化后的图片张量加载到 tensorboard 中。

Resize 的使用

trans_resize = transforms.Resize((512, 512))
img_resize = trans_resize(img)
img_resize = trans_totensor(img_resize)
writer.add_image("resize", img_resize, 0)
writer.close()

流程:

  1. 使用 transforms 的 Resize 创建一个工具 trans_resize,传的参数为更改之后的图片大小;
  2. 将 img 的尺寸更改为了 512×512;
  3. 将更改尺寸后的 img_resize 使用 trans_totensor 转换成张量;
  4. 再调用 tensorboard 的函数 add_image,将更改尺寸后的图片张量加载到 tensorboard 中。

Compose() 用法

Compose() 中的参数需要是一个列表。Python 中,列表的表示形式为 [数据1, 数据2, …];在 Compose 中,数据需要是 transforms 类型,所以得到 Compose([transforms参数1, transforms参数2, …])。

trans_resize_2 = transforms.Resize(512)
trans_compose = transforms.Compose([trans_resize_2, trans_totensor])
img_resize_2 = trans_compose(img)
writer.add_image("resize_2", img_resize_2, 1)

transforms.Compose的作用是将几个变换(Transform对象)组合成一个单一的变换。这个组合后的变换可以应用于图像数据,按照列表中提供的顺序依次执行每个变换。

这里的流程:

  1. 创建一个变量 trans_resize_2,功能为重新规定尺寸;
  2. 将重新规定尺寸和转为张量组合起来,顺序为先更改尺寸,再转为张量;
  3. 将 img 进行 compose 中的组合操作;
  4. 再调用 tensorboard 的函数 add_image,将处理后的图片张量加载到 tensorboard 中。

也可以不定义变量 trans_resize_2,如下:

transforms.Compose([
    transforms.CenterCrop(10),  # 中心裁剪,尺寸为10x10
    transforms.PILToTensor(),   # 将PIL图像转换为tensor
    transforms.ConvertImageDtype(torch.float),  # 转换图像的数据类型为浮点型
])

torchvision 中的数据集使用

前面加载的是自己整理的数据集,torchvision 里还内置了很多可以直接下载使用的公开数据集。torchvision 是 pytorch 官网中有关视觉的部分:pytorch 官网 —> Docs —> PyTorch Domains —> torchvision(左上角可以选版本号,视频中的版本为 v0.9.0)。

import torchvision

train_set = torchvision.datasets.CIFAR10("../data", train=True, download=True)
test_set = torchvision.datasets.CIFAR10("../data", train=False,  download=True)
print(train_set[0])
print(test_set.classes)

img, target = train_set[0]
print(img)
print(target)

如上,括号里传的参数可以通过官网里找到,如下图:

小土堆 PyTorch 课程笔记图示

这段代码实现了:

  1. 创建了一个训练集,是调用 datasets 中的 CIFAR10,将 train 设置为 True;创建了一个测试集,是调用 datasets 中的 CIFAR10,将 train 设置为 False;
  2. 输出测试集的第一个数据及类型;
  3. 将图像和标签都分别对应;
  4. 输出图像和标签。

这段代码运行结果如下:

Downloading https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz to ../data\cifar-10-python.tar.gz#下载数据集
100%|██████████| 170498071/170498071 [01:43<00:00, 1650531.07it/s]
Extracting ../data\cifar-10-python.tar.gz to ../data
Files already downloaded and verified
(<PIL.Image.Image image mode=RGB size=32x32 at 0x27EA615B1C8>, 6)#print(train_set[0]),此为元组类型,第一个为图像,后一个为标签
['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']#print(test_set.classes),此为输出了图像的所有类别
<PIL.Image.Image image mode=RGB size=32x32 at 0x27EF87874C8>#print(img)
6#print(target)

DataLoader 的使用

数据集有了,还要解决”怎么把数据取出来喂给网络”的问题,这就是 DataLoader 的职责。

import torchvision
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter

test_data = torchvision.datasets.CIFAR10("../data", train=False, transform=torchvision.transforms.ToTensor(),download=False)

test_loader = DataLoader(dataset=test_data,batch_size=64,shuffle=True,num_workers=0,drop_last=False)

writer = SummaryWriter("logs")
i=1
for data in test_loader:
    imgs,targets = data
    writer.add_images("test_data",imgs,i)
    i+=1
writer.close()

这段代码的逻辑:

  1. 实例化数据集,设置各项参数;
  2. 实例化抓取数据,设置各项参数,比如数据集是什么、size 是多大、是否打乱、是否多进程加载数据、是否摒弃不完整的 batch;
  3. 遍历抓取的数据,并将其加载到 tensorboard 中。

神经网络的基本骨架-nn.Module 的使用

数据准备的部分到此告一段落,从这一节开始进入网络搭建,torch.nn 就是搭建神经网络的工具箱。

https://pytorch.org/docs/stable/nn.html,这个网站就是介绍搭建神经网络的工具:

import torch.nn as nn
import torch.nn.functional as F

class Model(nn.Module):
    def __init__(self) -> None:
        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))
  • class Model(nn.Module):这里定义了一个类叫 model,继承了父类 nn.Module;
  • super().__init__():这里是继承父类的初始化。
def forward(self, x):
        x = F.relu(self.conv1(x))
        return F.relu(self.conv2(x))

这里短短三行代码,其实经历的过程为:

输入 x —> conv1 卷积 —> relu 非线性 —> conv2 卷积 —> relu 非线性 —> 输出

神经网络-卷积层

有了 nn.Module 这个骨架,接下来往里面填具体的层,先从最核心的卷积层开始。这里是对 torch.nn 中的 convolution Layers 进行介绍。

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)

Parameters 参数

  • in_channels (int) – 输入图像中的通道数

  • out_channels (int) – 卷积产生的通道数

  • kernel_size (int or tuple) – 卷积内核的大小

  • stride (int or tuple, optional) – 卷积的步幅。默认值:1

  • padding (int, tuple or str, optional) – 添加到输入的所有四个边的填充。默认值:0

  • dilation (int or tuple, optional) – 内核元素之间的间距。默认值:1

  • groups (int, optional) – 从输入通道到输出通道的阻塞连接数。默认值:1

  • bias (bool, optional) – 如果 是True ,则向输出添加可学习的偏差。默认: True

  • padding_mode (str, optional) – 'zeros''reflect' 'replicate''circular' .默认: 'zeros'

这里注意,in_channels 和 out_channels 不一定一样,比如 in_channels=1,out_channels=2,就是两个卷积核对其卷积,形成两个输出就是两个输出通道。

卷积后图片大小计算公式:

小土堆 PyTorch 课程笔记图示

import torch
import torchvision
from torch import nn
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=dataset, batch_size=64, shuffle=True, num_workers=0, drop_last=False)

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

    def forward(self, x):
        x = self.conv1(x)
        return x
mutou = mutou()

writer = SummaryWriter("logs")
i=0
for data in dataloader:
    imgs, targets = data
    print(imgs.shape)
    outputs = mutou(imgs)
    print(outputs.shape)
    writer.add_images("input", imgs, global_step=i)
    outputs = torch.reshape(outputs, (-1, 3, 30, 30))
    writer.add_images("output", outputs, global_step=i)
    i += 1
writer.close()

以上为一个简易应用卷积层的代码。

神经网络-最大池化的使用

卷积之后常接池化层,用来压缩特征、减小数据量。这里是对 torch.nn 中的 pooling Layers 进行介绍。

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

Parameters 参数

  • kernel_size (Union[int, Tuple[int, int]]) – 要选取最大值的窗口大小

  • stride (Union[int, Tuple[int, int]]) – 窗口的步幅。默认值为 kernel_size

  • padding (Union[int, Tuple[int, int]]) – 要在两侧添加的隐式负无穷大填充

  • dilation (Union[int, Tuple[int, int]]) – 控制窗口中元素步幅的参数,窗口不是连在一起的,而是中间有空洞的

  • return_indices (bool) – 如果是 True ,将返回最大索引以及输出。对 torch.nn.MaxUnpool2d 以后有用

  • ceil_mode (bool) – 当 True 时,将使用 ceil 而不是 floor 来计算输出形状

当窗口内的元素不完整时,ceil 为继续进行池化,floor 为舍弃,放弃这个池化

池化后大小:

小土堆 PyTorch 课程笔记图示

在卷积的基础上加了池化,相比上一段代码,变化在于新增了 maxpool

import torch
import torchvision
from torch import nn
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=dataset, batch_size=64, shuffle=True, num_workers=0, drop_last=False)

class mutou(nn.Module):
    def __init__(self):
        super(mutou, self).__init__()
        self.conv1 = nn.Conv2d(in_channels=3, out_channels=6, kernel_size=3, padding=0)
        self.maxpool = nn.MaxPool2d(kernel_size=3, ceil_mode=False)

    def forward(self, x):
        x = self.conv1(x)
        x = self.maxpool(x)
        return x
mutou = mutou()

writer = SummaryWriter("logs")
i=0
for data in dataloader:
    imgs, targets = data
    print(imgs.shape)
    outputs = mutou(imgs)
    print(outputs.shape)
    writer.add_images("input_pool", imgs, global_step=i)
    outputs = torch.reshape(outputs, (-1, 3, 10, 10))
    writer.add_images("output_pool", outputs, global_step=i)
    i += 1
writer.close()

神经网络-非线性激活

接下来是非线性激活,它的作用是给网络引入非线性。这里是对 torch.nn 中的 Non-linear Activations (weighted sum, nonlinearity)Non-linear Activations (other) 进行介绍。

torch.nn.ReLU(inplace=False)

参数 inplace 表示是否原地修改:

  • inplace=True:原地修改。例如 input=-1,执行 ReLU(input, inplace=True) 后,input 变为 0;
  • inplace=False:不改动原值,结果由返回值给出。例如 input=-1,执行 output=ReLU(input, inplace=False) 后,input 仍为 -1,output 为 0。

基于上面代码拓展,新添加了 ReLU

import torch
import torchvision
from torch import nn
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter

dataset = torchvision.datasets.CIFAR10("../data", train=False, transform=torchvision.transforms.ToTensor(), download=False)

dataloader = DataLoader(dataset=dataset, batch_size=64, shuffle=True, num_workers=0, drop_last=False)

class mutou(nn.Module):
    def __init__(self):
        super(mutou, self).__init__()
        self.conv1 = nn.Conv2d(in_channels=3, out_channels=6, kernel_size=3, padding=0)
        self.maxpool = nn.MaxPool2d(kernel_size=3, ceil_mode=False)
        self.relu = nn.ReLU()

    def forward(self, x):
        x = self.conv1(x)
        x = self.maxpool(x)
        x = self.relu(x)
        return x
mutou = mutou()

writer = SummaryWriter("logs")
i=0
for data in dataloader:
    imgs, targets = data
    print(imgs.shape)
    outputs = mutou(imgs)
    print(outputs.shape)
    writer.add_images("input_relu", imgs, global_step=i)
    outputs = torch.reshape(outputs, (-1, 3, 10, 10))
    writer.add_images("output_relu", outputs, global_step=i)
    i += 1
writer.close()

神经网络-线性层及其他层介绍

网络的最后通常要把特征映射成分类输出,这就要用到线性层。这里是对 torch.nn 中的 Linear Layers 进行介绍。

torch.nn.Linear(in_features, out_features, bias=True, device=None, dtype=None)

Parameters 参数

  • in_features (int) – 每个输入样本的大小

  • out_features (int) – 每个输出样本的大小

  • bias (bool) – 如果设置为 False ,则层将不会学习加性偏差。默认: True

import torch
import torchvision
from torch import nn
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter

dataset = torchvision.datasets.CIFAR10("../data", train=False, transform=torchvision.transforms.ToTensor(),
                                       download=False)

dataloader = DataLoader(dataset=dataset, batch_size=64, shuffle=True, num_workers=0, drop_last=True)

class mutou(nn.Module):
    def __init__(self):
        super(mutou, self).__init__()
        self.linear1 = nn.Linear(196608, 10)

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

mutou = mutou()

for data in dataloader:
    imgs, targets = data
    print(imgs.shape)
    output = torch.flatten(imgs)
    print(output.shape)
    output = mutou(output)
    print(output.shape)

线性层其实就是将所有的参数最后浓缩为一个几维的向量,这个几维根据你是几分类来决定。

最后再经过 softmax 函数变为概率。

神经网络-搭建小实战和 sequential 的使用

各种层都认识了,这一节把它们串起来搭一个完整的小网络。这里是对 torch.nn 中的 Containers 中的 Sequential 进行介绍。

torch.nn.Sequential(args: Module*)

这个模块的作用就是一个顺序容器,模块将按照它们在构造函数中传递的顺序添加到其中。

def forward(self, input):
    input = self.conv1(input)
    input = self.maxpool1(input)
    input = self.conv2(input)
    input = self.maxpool2(input)
    input = self.conv3(input)
    input = self.maxpool3(input)
    input = self.flatten(input)
    # input = self.fc(input)
    # input = self.fc2(input)
    return input

xd = xd()
print(xd)
input = torch.ones((64, 3, 32, 32))
output = xd(input)
print(output.shape)

以下是一个简单的卷积神经网络:

import torch
from torch import nn
from torch.utils.tensorboard import SummaryWriter

class xd(nn.Module):
    def __init__(self):
        super(xd, self).__init__()
        self.model = nn.Sequential(
            nn.Conv2d(in_channels=3, out_channels=32, kernel_size=5, stride=1, padding=2),
            nn.MaxPool2d(kernel_size=2),
            nn.Conv2d(in_channels=32, out_channels=32, kernel_size=5, stride=1, padding=2),
            nn.MaxPool2d(kernel_size=2),
            nn.Conv2d(in_channels=32, out_channels=64, kernel_size=5, stride=1, padding=2),
            nn.MaxPool2d(kernel_size=2),
            nn.Flatten(),
            nn.Linear(64 * 4 * 4, 64),
            nn.Linear(64, 10)
        )

    def forward(self, input):
        input = self.model(input)
        return input

xd=xd()
print(xd)
input = torch.ones((64, 3, 32, 32))
output = xd(input)
print(output.shape)

writer = SummaryWriter("logs_seq")
writer.add_graph(xd, input)
writer.close()

这里主要使用了 sequential 的功能,然后使用了 tensorboard 的 add_graph,里面的图很帅!每个框框双击还能展开,里面有详尽的过程图。

小土堆 PyTorch 课程笔记图示

损失函数与反向传播

网络能输出结果之后,还需要衡量输出与目标之间的差距,并据此反向传播梯度,这就是损失函数的工作。这里是对 torch.nn 中的 Loss Functions 进行介绍。

L1 损失函数

torch.nn.L1Loss(reduction='mean')
  • reduction (str, optional) – 指定要应用于输出的缩减: 'none' | 'mean' | 'sum''none' : 不应用缩减, 'mean' : 输出总和除以输出中的元素数, 'sum' : 输出将求和。注意: size_average 并且 reduce 正在被弃用,同时,指定这两个 args 中的任何一个都将覆盖 reduction 。默认: 'mean'
import torch
from torch import nn

inputs =torch.tensor([1,2,3],dtype=torch.float32)
labels = torch.tensor([1,2,5],dtype=torch.float32)

inputs = torch.reshape(inputs, (1,1,1,3))
labels = torch.reshape(labels, (1,1,1,3))

loss = nn.L1Loss()
L1Loss = loss(inputs, labels)
print(L1Loss)

#输出为:tensor(0.6667)

平方差损失函数(MSELoss)

torch.nn.MSELoss(size_average=None, reduce=None, reduction='mean')
import torch
from torch import nn

inputs =torch.tensor([1,2,3],dtype=torch.float32)
labels = torch.tensor([1,2,5],dtype=torch.float32)

inputs = torch.reshape(inputs, (1,1,1,3))
labels = torch.reshape(labels, (1,1,1,3))

loss_mse = nn.MSELoss()
MSELoss = loss_mse(inputs, labels)
print(MSELoss)

#输出为:tensor(1.3333)

交叉熵损失函数(CrossEntropyLoss)

torch.nn.CrossEntropyLoss(weight=None, size_average=None, ignore_index=-100, reduce=None, reduction='mean', label_smoothing=0.0)

调用形式:

loss = nn.CrossEntropyLoss()
Loss = loss(inputs, labels)

反向传播

反向传播其实就一行指令:

loss_result.backward()

整体代码:

import torch
import torchvision
from torch import nn
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter

dataset = torchvision.datasets.CIFAR10("../data", train=False, transform=torchvision.transforms.ToTensor(),
                                       download=False)
dataloader = DataLoader(dataset=dataset, batch_size=64, shuffle=True, num_workers=0, drop_last=True)
class xd(nn.Module):
    def __init__(self):
        super(xd, self).__init__()
        self.model = nn.Sequential(
            nn.Conv2d(in_channels=3, out_channels=32, kernel_size=5, stride=1, padding=2),
            nn.MaxPool2d(kernel_size=2),
            nn.Conv2d(in_channels=32, out_channels=32, kernel_size=5, stride=1, padding=2),
            nn.MaxPool2d(kernel_size=2),
            nn.Conv2d(in_channels=32, out_channels=64, kernel_size=5, stride=1, padding=2),
            nn.MaxPool2d(kernel_size=2),
            nn.Flatten(),
            nn.Linear(64 * 4 * 4, 64),
            nn.Linear(64, 10)
        )

    def forward(self, input):
        input = self.model(input)
        return input

loss = nn.CrossEntropyLoss()
xd=xd()

for data in dataloader:
    imgs, targets = data
    outputs = xd(imgs)
    loss_result = loss(outputs, targets)
    loss_result.backward()
    print('ok')

先设定损失函数,调用损失函数并且计算出结果,然后再反向传播。

优化器

算出了损失、反传了梯度,最后还要靠优化器来真正更新参数。优化器不在 torch.nn 中了,而是集中在 torch.optim 中。

调用方法:

for input, target in dataset:
    optimizer.zero_grad()   #每次循环对梯度清零
    output = model(input)
    loss = loss_fn(output, target)
    loss.backward()
    optimizer.step()        #根据梯度来调整

完整结构中的体现:

import torch
import torchvision
from torch import nn
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter

dataset = torchvision.datasets.CIFAR10("../data", train=False, transform=torchvision.transforms.ToTensor(),
                                       download=False)
dataloader = DataLoader(dataset=dataset, batch_size=64, shuffle=True, num_workers=0, drop_last=True)
class xd(nn.Module):
    def __init__(self):
        super(xd, self).__init__()
        self.model = nn.Sequential(
            nn.Conv2d(in_channels=3, out_channels=32, kernel_size=5, stride=1, padding=2),
            nn.MaxPool2d(kernel_size=2),
            nn.Conv2d(in_channels=32, out_channels=32, kernel_size=5, stride=1, padding=2),
            nn.MaxPool2d(kernel_size=2),
            nn.Conv2d(in_channels=32, out_channels=64, kernel_size=5, stride=1, padding=2),
            nn.MaxPool2d(kernel_size=2),
            nn.Flatten(),
            nn.Linear(64 * 4 * 4, 64),
            nn.Linear(64, 10)
        )

    def forward(self, input):
        input = self.model(input)
        return input

loss = nn.CrossEntropyLoss()
xd=xd()
optim = torch.optim.SGD(xd.parameters(), lr=0.01)  #设置优化器
for epoch in range(10):
    for data in dataloader:
        imgs, targets = data
        outputs = xd(imgs)
        loss_result = loss(outputs, targets)
        optim.zero_grad()  #每次循环对梯度清零   
        loss_result.backward()   
        optim.step()       #根据梯度来调整
        print(loss_result)

现有网络模型的使用及修改

除了从零搭建网络,也可以直接拿 torchvision 提供的现成模型来改。这里是围绕 PyTorch Domains —> torchvision —> Models and pre-trained weights 进行展开介绍。

import torchvision

from torch import nn

vgg16_false = torchvision.models.vgg16(pretrained=False)
vgg16_true = torchvision.models.vgg16(pretrained=True)

print(vgg16_true)

train_data = torchvision.datasets.CIFAR10('../data', train=True, transform=torchvision.transforms.ToTensor(),
                                          download=True)

vgg16_true.classifier.add_module('add_linear', nn.Linear(1000, 10))  #在模型中加层
print(vgg16_true)

print(vgg16_false)
vgg16_false.classifier[6] = nn.Linear(4096, 10)#修改模型中的层
print(vgg16_false)

网络模型的保存与读取

模型改好或训练好之后,需要保存下来,之后再读取使用。保存有两种方式,读取时要与保存方式一一对应。

保存

import torch
import torchvision

vgg16 = torchvision.models.vgg16(pretrained=False)
# 保存方式1,模型结构+模型参数
torch.save(vgg16, "vgg16_method1.pth")
# 保存方式2,模型参数(官方推荐)
torch.save(vgg16.state_dict(), "vgg16_method2.pth")

读取

import torch

# 方式1-->保存方式1,加载模型
model = torch.load("vgg16_method1.pth")
# print(model)

# 方式2,加载模型
vgg16 = torchvision.models.vgg16(pretrained=False)
vgg16.load_state_dict(torch.load("vgg16_method2.pth"))
# model = torch.load("vgg16_method2.pth")
print(vgg16)

完整模型训练套路

前面各节都是零件:数据、网络、损失函数、优化器。这一节把它们组装成一个完整的训练流程,网络结构单独放在 model.py 中。

import torchvision
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter

from model import *

# 准备数据集
train_data = torchvision.datasets.CIFAR10(root="../data", train=True, transform=torchvision.transforms.ToTensor(),
                                          download=True)
test_data = torchvision.datasets.CIFAR10(root="../data", train=False, transform=torchvision.transforms.ToTensor(),
                                         download=True)

# 长度
train_data_size = len(train_data)
test_data_size = len(test_data)
# 如果 train_data_size=10,训练数据集的长度为:10
print("训练数据集的长度为:{}".format(train_data_size))
print("测试数据集的长度为:{}".format(test_data_size))

# 利用 DataLoader 来加载数据集
train_dataloader = DataLoader(train_data, batch_size=64)
test_dataloader = DataLoader(test_data, batch_size=64)

# 创建网络模型
tudui = Tudui()

# 损失函数
loss_fn = nn.CrossEntropyLoss()

# 优化器
optimizer = torch.optim.SGD(tudui.parameters(), lr=0.01)

# 记录训练的次数
total_train_step = 0
# 记录测试的次数
total_test_step = 0

# 添加tensorboard
writer = SummaryWriter("./logs_train")

# 训练的轮数
epoch = 10

for i in range(epoch):
    print("-------第 {} 训练开始-------".format(i + 1))

    # 训练步骤开始
    for data in train_dataloader:
        imgs, targets = data
        outputs = tudui(imgs)
        loss = loss_fn(outputs, targets)

        # 优化器优化模型
        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)

    # 测试步骤开始
    total_test_loss = 0
    total_accuracy = 0
    with torch.no_grad():
        for data in test_dataloader:
            imgs, targets = data
            outputs = tudui(imgs)
            loss = loss_fn(outputs, targets)
            total_test_loss = total_test_loss + loss.item()
            accuracy = (outputs.argmax(1) == targets).sum()
            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, total_test_step)
    total_test_step = total_test_step + 1

    torch.save(tudui, "tudui_{}.pth".format(i))
    print("模型已保存")

writer.close()

整体思路流程:准备数据集 —> 用 DataLoader 加载数据 —> 创建网络模型 —> 设置损失函数和优化器 —> 循环训练(前向计算、求损失、梯度清零、反向传播、更新参数)—> 每轮结束后在测试集上计算整体 Loss 和正确率 —> 保存模型。

利用 GPU 进行训练

训练流程跑通后,可以把计算搬到 GPU 上加速,有两种写法。

方法一

将网络模型、数据、损失函数调用 .cuda(),对应代码中所有 if torch.cuda.is_available(): 的部分:

import torchvision
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter

from model import *

# 准备数据集
train_data = torchvision.datasets.CIFAR10(root="../data", train=True, transform=torchvision.transforms.ToTensor(),
                                          download=True)
test_data = torchvision.datasets.CIFAR10(root="../data", train=False, transform=torchvision.transforms.ToTensor(),
                                         download=True)

# 长度
train_data_size = len(train_data)
test_data_size = len(test_data)
# 如果 train_data_size=10,训练数据集的长度为:10
print("训练数据集的长度为:{}".format(train_data_size))
print("测试数据集的长度为:{}".format(test_data_size))

# 利用 DataLoader 来加载数据集
train_dataloader = DataLoader(train_data, batch_size=64)
test_dataloader = DataLoader(test_data, batch_size=64)

# 创建网络模型
tudui = Tudui()
if torch.cuda.is_available():
    print("使用GPU")
    tudui = tudui.cuda()

# 损失函数
loss_fn = nn.CrossEntropyLoss()
if torch.cuda.is_available():
    print("使用GPU")
    loss_fn = loss_fn.cuda()

# 优化器
optimizer = torch.optim.SGD(tudui.parameters(), lr=0.01)

# 记录训练的次数
total_train_step = 0
# 记录测试的次数
total_test_step = 0

# 添加tensorboard
writer = SummaryWriter("./logs_train")

# 训练的轮数
epoch = 10

for i in range(epoch):
    print("-------第 {} 训练开始-------".format(i + 1))

    # 训练步骤开始
    for data in train_dataloader:
        imgs, targets = data
        if torch.cuda.is_available():
            print("使用GPU")
            imgs = imgs.cuda()
            targets = targets.cuda()
        outputs = tudui(imgs)
        loss = loss_fn(outputs, targets)

        # 优化器优化模型
        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)

    # 测试步骤开始
    total_test_loss = 0
    total_accuracy = 0
    with torch.no_grad():
        for data in test_dataloader:
            imgs, targets = data
            if torch.cuda.is_available():
                print("使用GPU")
                imgs = imgs.cuda()
                targets = targets.cuda()
            outputs = tudui(imgs)
            loss = loss_fn(outputs, targets)
            total_test_loss = total_test_loss + loss.item()
            accuracy = (outputs.argmax(1) == targets).sum()
            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, total_test_step)
    total_test_step = total_test_step + 1

    torch.save(tudui, "tudui_{}.pth".format(i))
    print("模型已保存")

writer.close()

方法二(更常用)

定义一个 device,然后模型、损失函数、数据都调用 .to(device):

对于模型和损失函数:tudui = tudui.to(device)tudui.to(device) 一样,也就是不用再重新赋值。

import torchvision
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter

from model import *
#定义训练设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 准备数据集
train_data = torchvision.datasets.CIFAR10(root="../data", train=True, transform=torchvision.transforms.ToTensor(),
                                          download=True)
test_data = torchvision.datasets.CIFAR10(root="../data", train=False, transform=torchvision.transforms.ToTensor(),
                                         download=True)

# 长度
train_data_size = len(train_data)
test_data_size = len(test_data)
# 如果 train_data_size=10,训练数据集的长度为:10
print("训练数据集的长度为:{}".format(train_data_size))
print("测试数据集的长度为:{}".format(test_data_size))

# 利用 DataLoader 来加载数据集
train_dataloader = DataLoader(train_data, batch_size=64)
test_dataloader = DataLoader(test_data, batch_size=64)

# 创建网络模型
tudui = Tudui()
tuidui = tudui.to(device)

# 损失函数
loss_fn = nn.CrossEntropyLoss()
loss_fn = loss_fn.to(device)
# 优化器
optimizer = torch.optim.SGD(tudui.parameters(), lr=0.01)

# 记录训练的次数
total_train_step = 0
# 记录测试的次数
total_test_step = 0

# 添加tensorboard
writer = SummaryWriter("./logs_train")

# 训练的轮数
epoch = 10

for i in range(epoch):
    print("-------第 {} 训练开始-------".format(i + 1))

    # 训练步骤开始
    for data in train_dataloader:
        imgs, targets = data
        imgs = imgs.to(device)
        targets = targets.to(device)
        outputs = tudui(imgs)
        loss = loss_fn(outputs, targets)

        # 优化器优化模型
        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)

    # 测试步骤开始
    total_test_loss = 0
    total_accuracy = 0
    with torch.no_grad():
        for data in test_dataloader:
            imgs, targets = data
            imgs = imgs.to(device)
            targets = targets.to(device)
            outputs = tudui(imgs)
            loss = loss_fn(outputs, targets)
            total_test_loss = total_test_loss + loss.item()
            accuracy = (outputs.argmax(1) == targets).sum()
            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, total_test_step)
    total_test_step = total_test_step + 1

    torch.save(tudui, "tudui_{}.pth".format(i))
    print("模型已保存")

writer.close()

完整的模型验证套路

模型训练好并保存之后,最后一步是加载它对真实图片做预测,检验实际效果。

import torch
import torchvision
from PIL import Image
from torch import nn
#加载需要识别的图片
image_path ="./imgs/dog.png"
image = Image.open(image_path)
print(image)
#将图片转换为模型能运行的样子
transform =torchvision.transforms.Compose([torchvision.transforms.Resize((32,32)),
                                           torchvision.transforms.ToTensor()])

image = transform(image)
print(image.shape)

class Tudui(nn.Module):
    def __init__(self):
        super(Tudui, self).__init__()
        self.model = nn.Sequential(
            nn.Conv2d(3, 32, 5, 1, 2),  # 输入通道数为3,输出通道数为32,卷积核大小为5x5,步长为1,填充为2
            nn.MaxPool2d(2),  # 最大池化层,池化窗口大小为2x2
            nn.Conv2d(32, 32, 5, 1, 2),  # 输入通道数为32,输出通道数为32,卷积核大小为5x5,步长为1,填充为2
            nn.MaxPool2d(2),  # 最大池化层,池化窗口大小为2x2
            nn.Conv2d(32, 64, 5, 1, 2),  # 输入通道数为32,输出通道数为64,卷积核大小为5x5,步长为1,填充为2
            nn.MaxPool2d(2),  # 最大池化层,池化窗口大小为2x2
            nn.Flatten(),  # 将多维张量展平为一维
            nn.Linear(64 * 4 * 4, 64),  # 全连接层,输入节点数为64 * 4 * 4,输出节点数为64
            nn.Linear(64, 10)  # 连接层,输入节点数为64,输出节点数为10
        )

    def forward(self, x):
        x = self.model(x)
        return x
#加载模型
model=torch.load("tudui_5.pth")
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)
print(model)

image = torch.reshape(image,(1,3,32,32))
image = image.to(device)  # Move image to same device as model
#将图片喂给模型
model.eval()
with torch.no_grad():
     output = model(image)
     print(output)
#输出结果
print(output.argmax(1))