PyTorch深度学习入门(CIFAR10分类)

  1. 1. 常用函数
  2. 2. 数据加载
    1. 2.1 Dataset
    2. 2.2 DataLoader
  3. 3. TensorBoard
    1. 3.1 add_scalar
    2. 3.2 add_image
  4. 4. Transform
    1. 4.1 Transform的概念与基本用法
    2. 4.2 Transform的常用类
  5. 5. Torchvision数据集使用方法
  6. 6. 神经网络Torch.NN基本骨架的使用
  7. 7. Convolution Layers与Pooling Layers
    1. 7.1 Convolution Layers
    2. 7.2 Pooling Layers
  8. 8. Non-linear Activations与Linear Layers
    1. 8.1 Non-linear Activations
    2. 8.2 Linear Layers
  9. 9. 神经网络模型搭建小实战
    1. 9.1 Sequential
    2. 9.2 小实战
  10. 10. 损失函数与反向传播
    1. 10.1 Loss Functions
    2. 10.2 Backward
    3. 10.3 Optimizer
  11. 11. 现有网络模型的使用及修改
    1. 11.1 VGG16模型的使用
    2. 11.2 模型的保存与读取
  12. 12. 完整训练模型的方法
    1. 12.1 训练模型时的注意事项
    2. 12.2 使用GPU进行训练
    3. 12.3 CIFAR10_Net_Simple_v3

通过 CIFAR10 数据集的分类问题初入门 Deep Learning,也是开坑 AI 系列的第一篇文章。
相关环境的搭建可以转至:Anaconda 与 PyTorch 安装教程

1. 常用函数

(1)路径函数

os 模块中常用的路径相关函数有:

  • os.listdir(path):将 path 目录下的内容列成一个 list
  • os.path.join(path1, path2):拼接路径:path1\path2

例如:

1
2
3
4
5
6
7
import os

dir_path = 'dataset/hymenoptera_data/train/ants_image'
img_path_list = os.listdir(dir_path)
img_full_path = os.path.join(dir_path, img_path_list[0])
print(img_path_list) # ['0013035.jpg', '1030023514_aad5c608f9.jpg', ...]
print(img_full_path) # dataset/hymenoptera_data/train/ants_image\0013035.jpg

(2)辅助函数

  • dir():不带参数时,返回当前范围内的变量、方法和定义的类型列表;带参数时,返回参数的属性、方法列表。
  • help(func):查看函数 func 的使用说明。

例如:

1
2
3
4
import torch

print(dir(torch)) # ['AVG', 'AggregationType', ..., 'cuda', ...]
help(torch.cuda.is_available) # Help on function is_available in module torch.cuda: is_available() -> bool...

2. 数据加载

2.1 Dataset

数据读取和预处理是进行机器学习的首要操作,PyTorch 提供了很多方法来完成数据的读取和预处理。

其中 Dataset 表示数据集,torch.utils.data.Dataset 是代表这一数据的抽象类。你可以自己定义你的数据类,继承和重写这个抽象类,非常简单,只需要定义 __len____getitem__ 这个两个函数即可,例如:

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
from torch.utils.data import Dataset
from PIL import Image
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(self.root_dir, self.label_dir + '_image')
self.img_path_list = os.listdir(self.path)

def __getitem__(self, idx):
img_path = self.img_path_list[idx]
img_full_path = os.path.join(self.root_dir, self.label_dir + '_image', img_path)
img = Image.open(img_full_path)
label = self.label_dir
return img, label

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

root_dir = 'dataset/hymenoptera_data/train'
ants_label_dir = 'ants'

ants_data = MyData(root_dir, ants_label_dir)
img, label = ants_data[0]
print(img, label)
img.show()

通过上面的方式,可以定义我们需要的数据类,可以通过迭代的方式来获取每一个数据,但这样很难实现取 batch、shuffle 或者是多线程去读取数据。

2.2 DataLoader

torch.utils.data.DataLoader 构建可迭代的数据装载器,我们在训练的时候,每一个 for 循环,每一次 iteration,就是从 DataLoader 中获取一个 batch_size 大小的数据的。打个比方如果 Dataset 是一副完整的扑克牌,那么 DataLoader 就是抽取几张组成的一部分扑克牌。

DataLoader 的参数很多,但我们常用的主要有以下几个:

  • datasetDataset 类,决定从哪个数据集读取数据。
  • batch_size:批大小。
  • num_works:是否多进程读取机制。
  • shuffle:每个 Epoch 是否乱序。
  • drop_last:当样本数不能被 batch_size 整除时,是否舍弃最后一批数据。

要理解这个 drop_last,首先,得先理解 Epoch、Iteration 和 Batch_size 的概念:

  • Epoch:所有训练样本都已输入到模型中,称为一个 Epoch。
  • Iteration:一批样本输入到模型中,称为一个 Iteration。
  • Batch_size:一批样本的大小,决定一个 Epoch 有多少个 Iteration。

DataLoader 的作用就是构建一个数据装载器,根据我们提供的 batch_size 的大小,将数据样本分成一个个的 Batch 去训练模型,而这个分的过程中需要把数据取到,这个就是借助 Dataset__getitem__ 方法。

例如:

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

class MyData(Dataset):
def __init__(self, root_dir, label_dir, transform):
self.root_dir = root_dir
self.label_dir = label_dir
self.path = os.path.join(self.root_dir, self.label_dir + '_image')
self.img_path_list = os.listdir(self.path)
self.transform = transform # transform 的方式

def __getitem__(self, idx):
img_path = self.img_path_list[idx]
img_full_path = os.path.join(self.root_dir, self.label_dir + '_image', img_path)
img = Image.open(img_full_path).convert('RGB') # 先将图片转换成三通道
if self.transform is not None:
img = self.transform(img)
label = self.label_dir
return img, label

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

root_dir = 'dataset/hymenoptera_data/train'
ants_label_dir = 'ants'

trans_dataset = transforms.Compose([
transforms.Resize((83, 100)), # tensor 大小必须统一
transforms.ToTensor()
])

ants_data = MyData(root_dir, ants_label_dir, trans_dataset)

train_loader = DataLoader(dataset=ants_data, batch_size=10, shuffle=True, num_workers=0, drop_last=False)

for i, data in enumerate(train_loader):
imgs, labels = data
print(type(imgs))
print(imgs[0])
print(labels)
print(labels[0])

接下来使用 CIFAR10 数据集再展示一次 DataLoader 的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from torchvision import transforms, datasets
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter

test_set = datasets.CIFAR10(root='dataset/CIFAR10', train=False, transform=transforms.ToTensor(), download=True)

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

writer = SummaryWriter('logs')

for epoch in range(2): # 循环两个 epoch
for step, data in enumerate(test_loader): # step 表示第几个 batch
imgs, targets = data
writer.add_images('Epoch_{}'.format(epoch), imgs, step) # 注意是 add_images,图像默认格式为 NCHW

writer.close()

PS:部分看不懂的代码可以先去学后面的 transform 以及 tensorboard

3. TensorBoard

3.1 add_scalar

TensorBoard 原本是 TensorFlow 的可视化工具,PyTorch 从1.2.0开始支持 TensorBoard。之前的版本也可以使用 TensorBoardX 代替。

先进入 Anaconda 的 PyTorch 环境,安装 TensorBoard:

1
2
conda activate PyTorch
pip install tensorboard

在项目根目录下新建一个文件夹 logs,TensorBoard 的工作流程简单来说是将代码运行过程中的,某些你关心的数据保存在这个文件夹中(由代码中的 writer 完成),再读取这个文件夹中的数据,用浏览器显示出来(在命令行运行 TensorBoard 完成)。

我们先绘制一个 y = x 的图像,运行以下代码:

1
2
3
4
5
6
7
8
from torch.utils.tensorboard import SummaryWriter

writer = SummaryWriter('logs')

for x in range(100):
writer.add_scalar('y=x', x, x) # tag='y=x', scalar_value=x, global_step=x

writer.close()

add_scalar 函数主要有三个参数:

  • tag:数据标识符,可以理解为数据图像的标题。
  • scalar_value:保存的值,即纵轴上的值。
  • global_step:记录的步长,即横轴的值,一般会设置一个不断增加的 step

运行后会看到 logs 文件夹下生成了一个文件,然后我们在 PyCharm 终端的 PyTorch 环境中打开 TensorBoard(要在当前项目中进入 PyTorch 环境,否则 --logdir 的路径就不能用相对路径了):

1
tensorboard --logdir logs

打开 http://localhost:6006/ 即可看到绘制的图像。

如果因为某些原因导致端口冲突可以指定端口:

1
tensorboard --logdir logs --port 6007

3.2 add_image

add_image 函数主要有三个参数:

  • tag:同 add_scalar
  • img_tensor:图像数据,类型必须是 torch.Tensornumpy.ndarrystring/blobname
  • global_step:同 add_scalar

可以看到传入的图片数据有类型限制,目前还没学到 torch.Tensor 类型,以 numpy.ndarry 为例,因此我们需要先安装一下 NumPy,还是在 PyTorch 环境中安装:

1
pip install numpy

使用 PIL 打开一个图像,将其转换成 NumPy 数组:

1
2
3
4
5
6
7
8
from PIL import Image
import numpy as np

img_path = 'dataset/hymenoptera_data/train/ants_image/0013035.jpg'
img_PIL = Image.open(img_path)
img_array = np.array(img_PIL)
print(type(img_array)) # <class 'numpy.ndarray'>
print(img_array.shape) # (512, 768, 3)

可以看到图片的形状是三维的数据,前两个数据分别表示高度和宽度,第三个数据表示通道数,可以记为 (H, W, C),简写为 HWC

add_image 函数传入图片时格式默认为 CHW,如果格式不匹配需要设定函数中的 dataformats 参数,例如:

1
2
3
4
5
6
7
8
9
10
11
12
from torch.utils.tensorboard import SummaryWriter
from PIL import Image
import numpy as np

writer = SummaryWriter('logs')
img_path = 'dataset/hymenoptera_data/train/ants_image/0013035.jpg'
img_PIL = Image.open(img_path)
img_array = np.array(img_PIL)

writer.add_image('img_test', img_array, 1, dataformats='HWC')

writer.close()

运行后打开 TensorBoard 即可在 IMAGES 页面下看到图片。

4. Transform

4.1 Transform的概念与基本用法

transforms 在计算机视觉工具包 torchvision 下,包含了很多种对图像数据进行变换的类,这些都是在我们进行图像数据读入步骤中必不可少的,通过图像变换可以将图片变成不同的类型,或者可以通过旋转、裁切等手段对图像数据集的图像进行变换,起到扩充数据集与数据增强的作用。

transforms 主要使用的类为:transforms.ToTensor,该类能够将 PIL.Image 或者 ndarray 类型的数据转换为 tensor,并且归一化至 [0, 1]。注意归一化至 [0, 1] 是直接除以255,若自己的 ndarray 数据尺度有变化,则需要自行修改。

为什么需要 tensor 数据类型?因为它包装了反向传播神经网络所需要的一些基础的参数,因此在神经网络中需要将图片类型转换为 tensor 类型进行训练。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from PIL import Image
from torchvision import transforms
import cv2

img_path = 'dataset/hymenoptera_data/train/ants_image/0013035.jpg'
img_PIL = Image.open(img_path) # <class 'PIL.JpegImagePlugin.JpegImageFile'>

tensor_trans = transforms.ToTensor() # 创建 ToTensor 的实例对象
img_tensor1 = tensor_trans(img_PIL) # 将 PIL Image 转换成 tensor
print(type(img_tensor1)) # <class 'torch.Tensor'>

img_cv = cv2.imread(img_path) # <class 'numpy.ndarray'>
img_tensor2 = tensor_trans(img_cv) # 将 OpenCV Image 转换成 tensor
print(type(img_tensor2))

4.2 Transform的常用类

  • transforms.ComposeCompose 能够将多种变换组合在一起。例如下面的代码可以先将 PIL.Image 中心裁切,然后再转换成 tensor
1
2
3
4
5
6
7
8
9
img_path = 'dataset/hymenoptera_data/train/ants_image/0013035.jpg'
img_PIL = Image.open(img_path)

trans = transforms.Compose([
transforms.CenterCrop(100),
transforms.ToTensor()
])

img_trans = trans(img_PIL)
  • transforms.CenterCrop:需要传入参数 size,表示以 (size, size) 的大小从中心裁剪,参数也可以为 (height, width)。例如:
1
2
3
4
5
img_PIL.show()

trans_centercrop = transforms.CenterCrop((100, 150))
img_centercrop = trans_centercrop(img_PIL)
img_centercrop.show()
  • transforms.RandomCrop:需要传入参数 size,表示以 (size, size) 的大小随机裁剪,参数也可以为 (height, width)
  • transforms.Normalize(mean, std):对数据按通道进行标准化,即先减均值 mean,再除以标准差 std,注意是 HWC 格式,处理公式为:output[channel] = (input[channel] - mean[channel]) / std[channel],例如:
1
2
3
4
5
6
7
trans_tensor = transforms.ToTensor()
img_tensor = trans_tensor(img_PIL)

# 如果 input 的范围是[0, 1],那么用该参数归一化后的范围就变为[-1, 1]
trans_norm = transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
img_norm = trans_norm(img_tensor)
print(img_norm)
  • transforms.Resize:需要传入参数 (height, width) interpolation,表示重置图像的分辨率为 (h, w),也可以传入一个整数 size,这样会将较短的那条边缩放至 size,另一条边按原图大小等比例缩放。interpolation 为插值方法选择,默认为 PIL.Image.BILINEAR,例如:
1
2
3
4
5
6
7
8
9
10
11
12
trans_tensor = transforms.ToTensor()
img_tensor = trans_tensor(img_PIL)

print(img_tensor.size()) # torch.Size([3, 512, 768]),tensor 图像使用 size() 获取大小,PIL 图像使用 size

trans_resize = transforms.Resize((256, 300))
img_resize = trans_resize(img_tensor)
print(img_resize.size()) # torch.Size([3, 256, 300]),修改比例

trans_resize = transforms.Resize(30)
img_resize = trans_resize(img_tensor)
print(img_resize.size()) # torch.Size([3, 30, 45]),与原图等比例
  • transforms.ToPILImage::将 tensor 或者 ndarray 的数据转换为 PIL.Image 类型数据,参数 mode 默认为 None,表示1通道, mode=3 表示3通道,默认转换为 RGB,4通道默认转换为 RGBA

5. Torchvision数据集使用方法

Torchvision 官方文档 Torchvision 中的 torchvision.datasets 就是 Torchvision 提供的标准数据集,其中有很多已经构建和训练好的网络模型,在不同的领域下各自有着很优秀的性能。

我们以 CIFAR10 为例,该数据集包括了60000张32*32像素的图像,总共有10个类别,每个类别有6000张图像,其中有50000张图像为训练图像,10000张为测试图像。其使用说明如下图所示:

  • root:数据集存放的路径。
  • train:如果为 True,创建的数据集就为训练集,否则创建的数据集就为测试集。
  • transform:使用 transforms 中的变换操作对数据集进行变换。
  • target_transform:对 target 进行 transform
  • download:如果为 True,就会自动从网上下载这个数据集,否则就不会下载。

例如:

1
2
3
4
5
6
import torchvision

train_data = torchvision.datasets.CIFAR10(root='dataset/CIFAR10', train=True, download=True)
test_data = torchvision.datasets.CIFAR10(root='dataset/CIFAR10', train=False, download=True)

print(train_data[0]) # (<PIL.Image.Image image mode=RGB size=32x32 at 0x24011FC4F40>, 6)

刚开始运行时可以看到正在从网上下载数据集,如果下载速度非常慢可以复制链接去迅雷之类的地方下载,下载好后自己创建设定的路径,将数据集放过来即可。

然后设置断点,用 Debug 模式运行一下代码,我们可以查看一下数据集的内容,数据集 train_data 中的 classes 表示图像的种类,classes_to_idx 表示将种类映射为整数,targets 表示每张图像对应的种类编号,试着输出一下第一张图的信息:

1
2
3
4
5
img, target = train_data[0]
print(img) # <PIL.Image.Image image mode=RGB size=32x32 at 0x1EEAEC32190>
print(target) # 6
print(train_data.classes[target]) # frog
img.show() # 图像显示为青蛙

现在展示如何使用 transform 参数,假设我们需要将数据集的图像都转换成 tensor 类型:

1
2
3
4
5
6
7
8
9
trans_dataset = torchvision.transforms.Compose([
torchvision.transforms.ToTensor()
])

train_data = torchvision.datasets.CIFAR10(root='dataset/CIFAR10', train=True, transform=trans_dataset, download=True)
test_data = torchvision.datasets.CIFAR10(root='dataset/CIFAR10', train=False, transform=trans_dataset, download=True)

img, target = train_data[0]
print(type(img)) # <class 'torch.Tensor'>

6. 神经网络Torch.NN基本骨架的使用

torch.nn 能够帮助我们更优雅地训练神经网络,使神经网络代码更加简洁和灵活。官方文档:Torch.NN

在文档中可以看到第一块内容叫做 Container(容器),这就相当于神经网络的骨架,Container 之后的东西就用于往骨架里面填充,如 Convolution Layers(卷积层)、Pooling Layers(池化层),有卷积神经网络基础的小伙伴对这些词应该都很熟悉了。

Container 中有六个模块:ModuleSequentialModuleListModuleDictParameterListParameterDict,其中最常用的为 Module,这是所有神经网络的最基本的类,其基本的构造方式如下:

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__()
self.conv1 = nn.Conv2d(1, 20, 5)
self.conv2 = nn.Conv2d(20, 20, 5)

def forward(self, x): # 前向传播
x = F.relu(self.conv1(x)) # 将 x 进行第一层卷积后用 ReLU 激活函数输出
return F.relu(self.conv2(x)) # 将处理后的 x 再进行第二层卷积后用 ReLU 处理后返回最后结果

现在我们尝试自己创建一个简单的神经网络,并输出前向传播的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import torch
import torch.nn as nn

class Network(nn.Module):
def __init__(self): # 初始化
super(Network, self).__init__()

def forward(self, input):
output = input + 1
return output

network = Network()
x = torch.tensor(1.0) # x 为 tensor 类型
output = network(x) # Module 中的 __call__ 函数会调用 forward 函数
print(output) # tensor(2.)

我们以 Conv2d 函数为例,该函数的官方文档:TORCH.NN.FUNCTIONAL.CONV2D

该函数有以下几个参数:

  • input:输入的图像,size(mini_batch, in_channels, height, width)
  • weight:卷积核的大小,size(out_channels, in_channels/groups, height, width)
  • bias:偏置,默认为 None
  • stride:步长,用来控制卷积核移动间隔,如果为 x 则水平和竖直方向的步长都为 x,如果为 (x, y) 则竖直方向步长为 x,水平方向步长为 y
  • padding:在输入图像的边沿进行扩边操作,以保证图像输入输出前后的尺寸大小不变,在 PyTorch 的卷积层定义中,默认的 padding 为零填充,即在边缘填充0。
  • padding_mode:扩边的方式。
  • dilation:设定了取数之间的间隔。

例如:

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
import torch
import torch.nn.functional as F

input = torch.tensor([
[1, 2, 3, 0],
[0, 1, 2, 3],
[3, 0, 1, 2],
[2, 3, 0, 1]
])

kernel = torch.tensor([
[2, 0, 1],
[0, 1, 2],
[1, 0, 2]
])

input = torch.reshape(input, (1, 1, 4, 4)) # batch_size = 1,channel = 1
kernel = torch.reshape(kernel, (1, 1, 3, 3))

output = F.conv2d(input, kernel, stride=1)
print(output)
# tensor([[[[15, 16],
# [ 6, 15]]]])

output = F.conv2d(input, kernel, stride=1, bias=torch.tensor([3])) # 注意 bias 必须也是矩阵
print(output)
# tensor([[[[18, 19],
# [ 9, 18]]]])

7. Convolution Layers与Pooling Layers

由于图像是二维的,因此基本上最常用到的就是二维的卷积层和池化层:torch.nn.Conv2dtorch.nn.MaxPool2d,官方文档:torch.nn.Conv2dPooling Layers

7.1 Convolution Layers

卷积运算能够提取输入图像的不同特征,第一层卷积层可能只能提取一些低级的特征如边缘、线条和角等层级,更多层的网络能从低级特征中迭代提取更复杂的特征。

torch.nn.Conv2d 的主要参数有以下几个:

  • in_channels:输入图像的通道数,彩色图像一般都是三通道。
  • out_channels:通过卷积后产生的输出图像的通道数。
  • kernel_size:可以是一个数或一个元组,表示卷积核的大小,卷积核的参数是从数据的分布中采样得到的,这些数是多少无所谓,因为在神经网络训练的过程中就是对这些参数进行不断地调整。
  • stride:步长。
  • padding:填充。
  • padding_mode:填充模式,有 zerosreflectreplicatecircular,默认为 zeros
  • dilation:可以是一个数或一个元组,表示卷积核各个元素间的距离,也称空洞卷积。
  • group:一般设置为1,基本用不到。
  • bias:偏置,一般设置为 True

例如以下代码构建了一个只有一层卷积层的神经网络,该卷积层的输入和输出通道数都为三通道,卷积核大小为3*3,步长为1,无填充,然后用 CIFAR10 测试数据集进行测试:

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
from torchvision import transforms, datasets
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
import torch.nn as nn

test_data = datasets.CIFAR10('dataset/CIFAR10', train=False, transform=transforms.ToTensor())

data_loader = DataLoader(test_data, batch_size=64)

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

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

network = Network()
print(network) # Network((conv1): Conv2d(3, 6, kernel_size=(3, 3), stride=(1, 1)))

writer = SummaryWriter('logs')

for step, data in enumerate(data_loader):
imgs, targets = data
output = network(imgs)
writer.add_images('input', imgs, step)
writer.add_images('output', output, step)

writer.close()

运行后可以打开 TensorBoard 查看一下效果。

7.2 Pooling Layers

Pooling Layers 中的 MaxPool 表示最大池化,也称上采样;MaxUnpool 表示最小池化,也称下采样;AvgPool 表示平均池化。其中最常用的为 MaxPool2d,官方文档:torch.nn.MaxPool2d

最大池化的目的是保留输入数据的特征,同时减小特征的数据量

torch.nn.MaxPool2d 的主要参数有以下几个:

  • kernel_size:用来取最大值的窗口(池化核)大小,和之前的卷积核类似。
  • stride:步长,注意默认值为 kernel_size
  • padding:填充,和 Conv2d 一样。
  • dilation:池化核中各个元素间的距离,和 Conv2d 一样。
  • return_indices:如果为 True,表示返回值中包含最大值位置的索引。注意这个最大值指的是在所有窗口中产生的最大值,如果窗口产生的最大值总共有5个,就会有5个返回值。
  • ceil_mode:如果为 True,表示在计算输出结果形状的时候,使用向上取整,否则默认向下取整。

输出图像的形状的计算公式可以在官方文档中查看。

接下来我们用代码实现这个池化层:

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
from torchvision import transforms, datasets
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
import torch.nn as nn
import torch

class Network(nn.Module):
def __init__(self):
super(Network, self).__init__()
self.maxpool1 = nn.MaxPool2d(kernel_size=2)

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

input = torch.tensor([
[1, 2, 1, 0],
[0, 1, 2, 3],
[3, 0, 1, 2],
[2, 4, 0, 1]
], dtype=torch.float32) # 注意池化层读入的数据需要为浮点型

input = torch.reshape(input, (1, 1, 4, 4))

network = Network()
print(network) # Network((maxpool1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False))

output = network(input)
print(output)
# tensor([[[[2., 3.],
# [4., 2.]]]])

我们用图像来试试效果:

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
from torchvision import transforms, datasets
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
import torch.nn as nn

test_data = datasets.CIFAR10('dataset/CIFAR10', train=False, transform=transforms.ToTensor())

data_loader = DataLoader(test_data, batch_size=64)

class Network(nn.Module):
def __init__(self):
super(Network, self).__init__()
self.maxpool1 = nn.MaxPool2d(kernel_size=2)

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

network = Network()

writer = SummaryWriter('logs')

for step, data in enumerate(data_loader):
imgs, targets = data
output = network(imgs)
writer.add_images('input', imgs, step)
writer.add_images('output', output, step)

writer.close()

运行后可以打开 TensorBoard 查看一下效果。

8. Non-linear Activations与Linear Layers

8.1 Non-linear Activations

非线性激活的目的是为了在网络中引入一些非线性特征,因为非线性特征越多才能训练出符合各种曲线(特征)的模型。

非线性激活函数官方文档:Non-linear Activations

有深度学习基础的同学应该知道最常用的非线性激活函数就是 ReLU 和 Sigmoid 函数,多分类问题会在输出层使用 Softmax 函数(如果损失函数使用的是交叉熵误差函数 CrossEntropyLoss 则会自动计算 Softmax,无需创建 Softmax 层)。这三个函数在 PyTorch 中分别为 nn.ReLUnn.Sigmoidnn.Softmax

这两个函数的输入都是只需指明 batch_size 即可,在 PyTorch1.0 之后的版本任何形状的数据都能被计算,无需指定 batch_size

nn.ReLU 只有一个需要设置的参数 inplace,如果为 True 表示计算结果直接替换到输入数据上,例如:

1
2
3
input = -1
nn.ReLU(input, inplace=True)
# input = 0

构建 ReLU 层代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import torch
import torch.nn as nn

class Network(nn.Module):
def __init__(self):
super(Network, self).__init__()
self.relu1 = nn.ReLU()

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

network = Network()

input = torch.tensor([
[1, -0.5],
[-1, 3]
])

output = network(input)
print(output)
# tensor([[1., 0.],
# [0., 3.]])

由于 ReLU 对图像处理的直观效果不明显,我们使用 Sigmoid 对图像进行处理:

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
from torchvision import transforms, datasets
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
import torch.nn as nn

class Network(nn.Module):
def __init__(self):
super(Network, self).__init__()
self.sigmoid1 = nn.Sigmoid()

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

test_data = datasets.CIFAR10('dataset/CIFAR10', train=False, transform=transforms.ToTensor())
data_loader = DataLoader(test_data, batch_size=64)

network = Network()

writer = SummaryWriter('logs')

for step, data in enumerate(data_loader):
imgs, targets = data
output = network(imgs)
writer.add_images('input', imgs, step)
writer.add_images('output', output, step)

writer.close()

8.2 Linear Layers

线性层官方文档:Linear Layers

PyTorch 的 nn.Linear 是用于设置网络中的全连接层的,需要注意的是全连接层的输入与输出都是二维张量,一般形状为:[batch_size, size],不同于卷积层要求输入输出是四维张量,因此在将图像传入全连接层之前一般都会展开成一维的。

nn.Linear 有三个参数分别如下:

  • in_features:指的是输入的二维张量的大小,即输入的 [batch_size, size] 中的 size
  • out_features:指的是输出的二维张量的大小,即输出的二维张量的形状为 [batch_size, output_size],当然,它也代表了该全连接层的神经元个数。从输入输出的张量的 shape 角度来理解,相当于一个输入为 [batch_size, in_features] 的张量变换成了 [batch_size, out_features] 的输出张量。
  • bias:偏置,相当于 y = ax + b 中的 b

代码示例如下:

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
import torch
import torch.nn as nn

class Network(nn.Module):
def __init__(self):
super(Network, self).__init__()
self.linear1 = nn.Linear(24, 30)

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

input = torch.tensor([
[1, 2, 3, 0, 1, 2, 3, 0],
[0, 1, 2, 3, 0, 1, 2, 3],
[3, 0, 1, 2, 3, 0, 1, 2],
], dtype=torch.float32)

print(input.shape) # torch.Size([3, 8])

input = torch.flatten(input) # 将 input 拉平成一维

print(input.shape) # torch.Size([24])

network = Network()

output = network(input)
print(output.shape) # torch.Size([30])

9. 神经网络模型搭建小实战

9.1 Sequential

torch.nn.Sequential 是一个 Sequential 容器,能够在容器中嵌套各种实现神经网络中具体功能相关的类,来完成对神经网络模型的搭建。模块的加入一般有两种方式,一种是直接嵌套,另一种是以 OrderedDict 有序字典的方式进行传入,这两种方式的唯一区别是:

  • 使用 OrderedDict 搭建的模型的每个模块都有我们自定义的名字。
  • 直接嵌套默认使用从零开始的数字序列作为每个模块的名字。

(1)直接嵌套方法的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import torch.nn as nn

model = nn.Sequential(
nn.Conv2d(1, 20, 5),
nn.ReLU(),
nn.Conv2d(20, 64, 5),
nn.ReLU()
)

print(model)
# Sequential(
# (0): Conv2d(1, 20, kernel_size=(5, 5), stride=(1, 1))
# (1): ReLU()
# (2): Conv2d(20, 64, kernel_size=(5, 5), stride=(1, 1))
# (3): ReLU()
# )

(2)使用 OrderedDict 的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import torch.nn as nn
from collections import OrderedDict

model = nn.Sequential(OrderedDict([
('Conv1', nn.Conv2d(1, 20, 5)),
('ReLU1', nn.ReLU()),
('Conv2', nn.Conv2d(20, 64, 5)),
('ReLU2', nn.ReLU())
]))

print(model)
# Sequential(
# (Conv1): Conv2d(1, 20, kernel_size=(5, 5), stride=(1, 1))
# (ReLU1): ReLU()
# (Conv2): Conv2d(20, 64, kernel_size=(5, 5), stride=(1, 1))
# (ReLU2): ReLU()
# )

9.2 小实战

由于代码很简单,都是学过的内容进行组装,因此直接看代码:

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
from torch.utils.tensorboard import SummaryWriter
import torch.nn as nn
import torch

class CIFAR10_Network(nn.Module):
def __init__(self):
super(CIFAR10_Network, self).__init__()
self.model = nn.Sequential(
nn.Conv2d(in_channels=3, out_channels=32, kernel_size=5, stride=1, padding=2), # [32, 32, 32]
nn.MaxPool2d(kernel_size=2), # [32, 16, 16]
nn.Conv2d(in_channels=32, out_channels=32, kernel_size=5, stride=1, padding=2), # [32, 16, 16]
nn.MaxPool2d(kernel_size=2), # [32, 8, 8]
nn.Conv2d(in_channels=32, out_channels=64, kernel_size=5, stride=1, padding=2), # [64, 8, 8]
nn.MaxPool2d(kernel_size=2), # [64, 4, 4]
nn.Flatten(), # [1024]
nn.Linear(in_features=1024, out_features=64), # [64]
nn.Linear(in_features=64, out_features=10) # [10]
)

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

network = CIFAR10_Network()

input = torch.randn(64, 3, 32, 32) # 返回一个包含了从标准正态分布中抽取的一组随机数的张量
print(input.shape) # torch.Size([64, 3, 32, 32])
output = network(input)
print(output.shape) # torch.Size([64, 10])

writer = SummaryWriter('logs')
writer.add_graph(network, input) # 生成计算图
writer.close()

使用 add_graph 函数可以在 TensorBoard 中生成神经网络的计算图,通过计算图可以很清晰地看到每一层计算时数据流入流出的结果,双击相应的标签可以进一步深入查看更详细的信息。

10. 损失函数与反向传播

10.1 Loss Functions

具有深度学习理论基础的同学对损失函数和反向传播一定不陌生,在此不详细展开理论介绍。损失函数是指用于计算标签值和预测值之间差异的函数,在机器学习过程中,有多种损失函数可供选择,典型的有距离向量,绝对值向量等。使用损失函数的流程概括如下:

  1. 计算实际输出和目标之间的差距。
  2. 为我们更新输出提供一定的依据(反向传播)。

损失函数的官方文档:Loss Functions

(1)nn.L1Loss:平均绝对误差(MAE,Mean Absolute Error),计算方法很简单,取预测值和真实值的绝对误差的平均数即可。

PyTorch1.13中 nn.L1Loss 数据形状规定如下:

  • Input(*),means any number of dimensions.
  • Target(*),same shape as the input.
  • Output:scalar. If reduction is none, then (*), same shape as the input.

早先的版本需要指定 batch_size 大小,现在不需要了。可以设置参数 reduction,默认为 mean,即取平均值,也可以设置为 sum,顾名思义就是取和。

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import torch.nn as nn
import torch

input = torch.tensor([1.0, 2.0, 3.0])
target = torch.tensor([4.0, -2.0, 5.0])

loss = nn.L1Loss()
result = loss(input, target)

print(result) # tensor(3.)

loss = nn.L1Loss(reduction='sum')
result = loss(input, target)

print(result) # tensor(9.)

(2)nn.MSELoss:均方误差(MSE,Mean Squared Error),即预测值和真实值之差的平方和的平均数。

该损失函数的用法与 nn.L1Loss 相似,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import torch.nn as nn
import torch

input = torch.tensor([1.0, 2.0, 3.0])
target = torch.tensor([4.0, -2.0, 5.0])

loss = nn.MSELoss()
result = loss(input, target)

print(result) # tensor(9.6667)

loss = nn.MSELoss(reduction='sum')
result = loss(input, target)

print(result) # tensor(29.)

(3)nn.CrossEntropyLoss:交叉熵误差,训练分类 C 个类别的模型的时候较常用这个损失函数,一般用在 Softmax 层后面,计算公式较为复杂,可以在官网中查看。

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import torch.nn as nn
import torch

input = torch.tensor([0.1, 0.7, 0.2])
target = torch.tensor(1)

loss = nn.CrossEntropyLoss()
result = loss(input, target)

print(result) # tensor(0.7679)

input = torch.tensor([0.8, 0.1, 0.1])
result = loss(input, target)

print(result) # tensor(1.3897)

10.2 Backward

接下来以 CIFAR10 数据集为例,用上一节搭建的神经网络先设置 batch_size 为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
33
34
35
36
37
38
39
40
41
42
43
44
from torchvision import transforms, datasets
from torch.utils.data import DataLoader
import torch.nn as nn

class CIFAR10_Network(nn.Module):
def __init__(self):
super(CIFAR10_Network, self).__init__()
self.model = nn.Sequential(
nn.Conv2d(in_channels=3, out_channels=32, kernel_size=5, stride=1, padding=2), # [32, 32, 32]
nn.MaxPool2d(kernel_size=2), # [32, 16, 16]
nn.Conv2d(in_channels=32, out_channels=32, kernel_size=5, stride=1, padding=2), # [32, 16, 16]
nn.MaxPool2d(kernel_size=2), # [32, 8, 8]
nn.Conv2d(in_channels=32, out_channels=64, kernel_size=5, stride=1, padding=2), # [64, 8, 8]
nn.MaxPool2d(kernel_size=2), # [64, 4, 4]
nn.Flatten(), # [1024]
nn.Linear(in_features=1024, out_features=64), # [64]
nn.Linear(in_features=64, out_features=10) # [10]
)

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

network = CIFAR10_Network()

test_data = datasets.CIFAR10('dataset/CIFAR10', train=False, transform=transforms.ToTensor())
data_loader = DataLoader(test_data, batch_size=1)

loss = nn.CrossEntropyLoss()

for step, data in enumerate(data_loader):
imgs, targets = data
output = network(imgs)
output_loss = loss(output, targets)
print(output)
print(targets)
print(output_loss)

# tensor([[ 0.1252, -0.1069, -0.0747, 0.0232, 0.0852, 0.1019, 0.0688, -0.1068,
# 0.0854, -0.0740]], grad_fn=<AddmmBackward0>)

# tensor([3])

# tensor(2.2960, grad_fn=<NllLossBackward0>)

现在我们来尝试解决第二个问题,即损失函数如何为我们更新输出提供一定的依据(反向传播)。

例如对于卷积层来说,其中卷积核中的每个参数就是我们需要调整的,每个参数具有一个属性 grad 表示梯度,反向传播时每一个要更新的参数都会求出对应的梯度,在优化的过程中就可以根据这个梯度对参数进行优化,最终达到降低损失函数值的目的。

PyTorch 中对损失函数计算出的结果使用 backward 函数即可计算出梯度:

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
from torchvision import transforms, datasets
from torch.utils.data import DataLoader
import torch.nn as nn

class CIFAR10_Network(nn.Module):
def __init__(self):
super(CIFAR10_Network, self).__init__()
self.model = nn.Sequential(
# Layers
)

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

network = CIFAR10_Network()

test_data = datasets.CIFAR10('dataset/CIFAR10', train=False, transform=transforms.ToTensor())
data_loader = DataLoader(test_data, batch_size=1)

loss = nn.CrossEntropyLoss()

for step, data in enumerate(data_loader):
imgs, targets = data
output = network(imgs)
output_loss = loss(output, targets)
output_loss.backward() # 反向传播

我们在计算反向传播之前设置断点,然后可以在 PyCharm 下方的变量区域通过目录 network/model/Protected Attributes/_modules/'0'/weight/grad 查看到某一层参数的梯度,在反向传播之前为 None,执行反向传播的代码后可以看到 grad 处有数值了。

我们有了各个节点参数的梯度,接下来就可以选用一个合适的优化器,来对这些参数进行优化。

10.3 Optimizer

优化器 torch.optim 的官方文档:TORCH.OPTIM

优化器主要是在模型训练阶段对模型的可学习参数进行更新,常用优化器有:SGD、RMSprop、Adam等。优化器初始化时传入传入模型的可学习参数,以及其他超参数如 lrmomentum 等,例如:

1
2
3
4
import torch.optim as optim

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

在训练过程中先调用 optimizer.zero_grad() 清空梯度,再调用 loss.backward() 反向传播,最后调用 optimizer.step() 更新模型参数,例如:

1
2
3
4
5
6
7
for step, data in enumerate(data_loader):
imgs, targets = data
output = network(imgs)
loss = loss_function(output, targets)
optimizer.zero_grad()
loss.backward()
optimizer.step()

接下来我们来训练20轮神经网络,看看损失函数值的变化:

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
from torchvision import transforms, datasets
from torch.utils.data import DataLoader
import torch.nn as nn
import torch.optim as optim

class CIFAR10_Network(nn.Module):
def __init__(self):
super(CIFAR10_Network, self).__init__()
self.model = nn.Sequential(
# Layers
)

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

network = CIFAR10_Network()

test_data = datasets.CIFAR10('dataset/CIFAR10', train=False, transform=transforms.ToTensor())
data_loader = DataLoader(test_data, batch_size=64)

loss_function = nn.CrossEntropyLoss()
optimizer = optim.SGD(network.parameters(), lr=0.01)

for epoch in range(20): # 学习20轮
total_loss = 0.0
for step, data in enumerate(data_loader):
imgs, targets = data
output = network(imgs)
loss = loss_function(output, targets)
total_loss += loss
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(total_loss)

可以看到每一轮所有 batch 的损失函数值的总和确实在不断降低了。

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

11.1 VGG16模型的使用

我们以 VGG16 为例,该网络模型是用于大规模图像识别的超深度卷积神经网络,官方文档:VGG16

该网络模型主要有以下参数:

  • weights:可以设置成 torchvision.models.VGG16_Weights.DEFAULTDEFAULT 表示自动使用最新的数据。老版本为 pretrained,如果为 True,表示使用预先训练好的权重,在官网可以看到这个权重是在 ImageNet-1K 数据集训练的,默认为不使用预先训练好的权重。
  • progress:如果为 True,则显示下载的进度条,默认为 True

注意,下载网络时默认的下载路径是 C:\Users\<username>\.cache,因此在下载模型前,我们需要修改路径:打开 D:\Anaconda3_Environments\envs\PyTorch\Lib\site-packages\torch 中的 hub.py 文件,搜索 load_state_dict_from_url,然后修改 model_dir 即可:

1
model_dir: Optional[str] = 'D:\\Anaconda3_Environments\\envs\\PyTorch\\Torch-model'

然后我们输出一下这个网络模型:

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
import torchvision

vgg = torchvision.models.vgg16(weights=torchvision.models.VGG16_Weights.DEFAULT)

print(vgg)
# VGG(
# (features): Sequential(
# (0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
# (1): ReLU(inplace=True)
# (2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
# (3): ReLU(inplace=True)
# (4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
# ......
# )
# (avgpool): AdaptiveAvgPool2d(output_size=(7, 7))
# (classifier): Sequential(
# (0): Linear(in_features=25088, out_features=4096, bias=True)
# (1): ReLU(inplace=True)
# (2): Dropout(p=0.5, inplace=False)
# (3): Linear(in_features=4096, out_features=4096, bias=True)
# (4): ReLU(inplace=True)
# (5): Dropout(p=0.5, inplace=False)
# (6): Linear(in_features=4096, out_features=1000, bias=True)
# )
# )

可以看到这个模型的分类结果为1000类,那么假如我们需要分类 CIFAR10 该如何应用这个网络模型呢?一种方法就是直接将最后一层 Linearout_features 改为10,还有一种方法就是再添加一层 in_features=1000, out_features=10Linear

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
from torchvision import transforms, datasets
from torch.utils.data import DataLoader
import torchvision
import torch.nn as nn
import torch.optim as optim

vgg = torchvision.models.vgg16(weights=torchvision.models.VGG16_Weights.DEFAULT)

vgg.classifier.add_module('add_linear', nn.Linear(in_features=1000, out_features=10)) # 在 classifier 中加一层 Linear
# vgg.classifier[6] = nn.Linear(in_features=4096, out_features=10) # 修改 classifier 的最后一层 Linear

test_data = datasets.CIFAR10('dataset/CIFAR10', train=False, transform=transforms.ToTensor())
data_loader = DataLoader(test_data, batch_size=64)

loss_function = nn.CrossEntropyLoss()
optimizer = optim.SGD(vgg.parameters(), lr=0.01)

for epoch in range(20):
total_loss = 0.0
for step, data in enumerate(data_loader):
imgs, targets = data
output = vgg(imgs)
loss = loss_function(output, targets)
total_loss += loss
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(total_loss)

可以看到效果是比之前自己构建的网络模型好很多的。

11.2 模型的保存与读取

我们在对某些模型进行修改后可能想将其保存下来,方便以后用到时无需再构建一遍网络,可以按以下的方式将整个模型保存到路径 models/CIFAR10_VGG16.pth

1
2
3
4
5
6
7
8
import torchvision
import torch.nn as nn
import torch

model = torchvision.models.vgg16(weights=torchvision.models.VGG16_Weights.DEFAULT)
model.classifier.add_module('add_linear', nn.Linear(in_features=1000, out_features=10))

torch.save(model, 'models/CIFAR10_VGG16.pth')

其对应的加载模型的方式为:

1
model = torch.load('models/CIFAR10_VGG16.pth')

还有一种保存方式是将模型中的参数保存成字典的形式,官方建议使用该方式:

1
torch.save(model.state_dict(), 'models/CIFAR10_VGG16_STATE.pkl')

其对应的加载模型的方式为:

1
2
model = torchvision.models.vgg16()
model.load_state_dict(torch.load('models/CIFAR10_VGG16_STATE.pkl'))

注意如果是保存自己构建的网络模型,需要在模型的类的源代码中将该类导入进来,例如在 test_save.py 中用以下代码保存自己的网络:

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

class MyNetwork(nn.Module):
def __init__(self):
super(MyNetwork, self).__init__()
self.conv1 = nn.Conv2d(3, 64, 3)

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

model = MyNetwork()
torch.save(model, 'models/My_Network.pth')

test_load.py 中导入时需要这样写:

1
2
3
4
5
import torch
from test_save import MyNetwork

model = torch.load('models/My_Network.pth')
print(model)

12. 完整训练模型的方法

12.1 训练模型时的注意事项

(1)通常我们会将超参数的设置放在一起,使代码更加直观且方便修改:

1
2
3
BATCH_SIZE = 64
LEARNING_RATE = 0.01
EPOCH = 10

(2)我们在每一轮 epoch 中会先对训练集进行训练,然后使用测试集进行正确率的测试,因此一般我们会记录总共训练的次数 total_train_step 以及总共测试的次数 total_test_step,方便后续绘图使用。

(3)在开始训练之前一般需要将模型设置成训练状态,在测试之前需要设置成评估状态,这两种状态会影响少部分的层例如 DropoutBatchNorm

1
2
3
4
5
model.train()
# training

model.eval()
# evaluation

(4)在分类问题中计算准确率一般用以下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import torch

a = torch.tensor([
[0.3, 0.7],
[0.6, 0.4]
]) # 假设两个物体二分类的结果

b = torch.tensor([0, 0]) # 正确的标签

print(a.argmax(dim=1)) # tensor([1, 0]),在第1维上取最大值,即对每一行求最大值,将最大值作为分类结果

print(a.argmax(dim=1) == b) # tensor([False, True]),与标签进行比较,第一个物体的结果与标签不符,第二个和标签相符

print((a.argmax(dim=1) == b).sum()) # tensor(1),将所有物体与标签的比较结果求和就是 True 的数量,也就是预测正确的数量

(5)测试时不能对模型进行任何干扰,即在测试的时候神经网络不能产生梯度,因此在每次测试前需要加上以下代码:

1
2
with torch.no_grad():
# evaluation

12.2 使用GPU进行训练

前提:电脑有 NVIDIA 显卡,配置好了 CUDA,可以使用 torch.cuda.is_available() 来检查 CUDA 是否可用。

使用 GPU 训练的时候,需要将 Module 对象和 Tensor 类型的数据转移到 GPU 上进行计算,一般来说即为将网络模型、数据、损失函数放到 GPU 上计算。

使用 GPU 训练的方式有两种,第一种是使用 cuda() 函数,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 网络模型
model = MyNetwork()
model = model.cuda()

# 损失函数
loss_function = nn.CrossEntropyLoss()
loss_function = loss_function.cuda()

# 数据
for step, data in enumerate(data_loader):
imgs, targets = data
imgs = imgs.cuda()
targets = targets.cuda()

另一种是使用 to(device)device 就是我们选择用来训练模型的设备,该方式与 cuda() 有一点细微的差别如下:

  • 对于 Tensor 类型的数据(图像、标签等),使用 to(device) 之后,需要接收返回值,返回值才是正确设置了 device 的 Tensor。
  • 对于 Module 对象(网络模型、损失函数),只用调用 to(device) 就可以将模型设置为指定的 device,不必接收返回值,当然接收返回值也是可以的。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')  # 'cuda:0' 表示第 0 号 GPU

# 网络模型
model = MyNetwork()
model.to(device)

# 损失函数
loss_function = nn.CrossEntropyLoss()
loss_function.to(device)

# 数据
for step, data in enumerate(data_loader):
imgs, targets = data
imgs = imgs.to(device)
targets = targets.to(device)

注意如果加载在 GPU 上训练好的模型,然后想在 CPU 上使用,需要映射回 CPU:

1
model = torch.load('models/AFTER_TRAININGS_MODEL.pth', map_location=torch.device('cpu'))

12.3 CIFAR10_Net_Simple_v3

最后放上经过自己调参达到88%左右的正确率的模型和训练代码吧:

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
import torch.nn as nn
import torch

class CIFAR10_Net_Simple_v3(nn.Module):
def __init__(self):
super(CIFAR10_Net_Simple_v3, self).__init__()
self.model = nn.Sequential(
nn.Conv2d(in_channels=3, out_channels=32, kernel_size=3, stride=1, padding=1), # [32, 32, 32]
nn.ReLU(inplace=True),
nn.BatchNorm2d(32),
nn.Conv2d(in_channels=32, out_channels=32, kernel_size=3, stride=1, padding=1), # [32, 32, 32]
nn.ReLU(inplace=True),
nn.BatchNorm2d(32),
nn.MaxPool2d(kernel_size=2), # [32, 16, 16]

nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, stride=1, padding=1), # [64, 16, 16]
nn.ReLU(inplace=True),
nn.BatchNorm2d(64),
nn.Conv2d(in_channels=64, out_channels=64, kernel_size=3, stride=1, padding=1), # [64, 16, 16]
nn.ReLU(inplace=True),
nn.BatchNorm2d(64),
nn.MaxPool2d(kernel_size=2), # [64, 8, 8]

nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, stride=1, padding=1), # [128, 16, 16]
nn.ReLU(inplace=True),
nn.BatchNorm2d(128),
nn.Conv2d(in_channels=128, out_channels=128, kernel_size=3, stride=1, padding=1), # [128, 16, 16]
nn.ReLU(inplace=True),
nn.BatchNorm2d(128),
nn.MaxPool2d(kernel_size=2), # [128, 4, 4]

nn.Flatten(), # [2048]
nn.Dropout(p=0.4, inplace=False),

nn.Linear(in_features=2048, out_features=64), # [64]
nn.ReLU(inplace=True),
nn.Dropout(p=0.4, inplace=False),

nn.Linear(in_features=64, out_features=10) # [10]
)

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

# model = CIFAR10_Net_Simple_v3()
# torch.save(model, '../models/CIFAR10_Net_Simple_v3.pth')
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
from torchvision import transforms, datasets
from torch.utils.data import DataLoader
from torch.utils.data.dataset import ConcatDataset
from torch.utils.tensorboard import SummaryWriter
from util.CIFAR10_Net_Simple_v3 import *
import torch.nn as nn
import torch.optim as optim
import torch

# 超参数
BATCH_SIZE = 32
LEARNING_RATE = 0.01
EPOCH = 150
SHOW_INFO_STEP = 200

# 训练设备
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 'cuda:0' 表示第 0 号 GPU

# 数据增强
trans = transforms.Compose([
transforms.RandomCrop(32, padding=[0, 2, 3, 4]),
transforms.RandomHorizontalFlip(p=0.5),
transforms.ToTensor()
])

# 数据集
train_data = datasets.CIFAR10('dataset/CIFAR10', train=True, transform=trans)
test_data = datasets.CIFAR10('dataset/CIFAR10', train=False, transform=transforms.ToTensor())

# 扩充训练集
# trans_train_data = datasets.CIFAR10('dataset/CIFAR10', train=True, transform=trans)
# train_data = ConcatDataset([train_data, trans_train_data])

# 加载数据
train_dataloader = DataLoader(train_data, batch_size=BATCH_SIZE, shuffle=True)
test_dataloader = DataLoader(test_data, batch_size=BATCH_SIZE)

train_data_len = len(train_data)
test_data_len = len(test_data)

model = torch.load('models/CIFAR10_Net_Simple_v3.pth')

loss_function = nn.CrossEntropyLoss()

optimizer = optim.SGD(model.parameters(), lr=LEARNING_RATE, momentum=0.9)
scheduler = optim.lr_scheduler.MultiStepLR(optimizer, milestones=[8, 16, 24, 32], gamma=0.5)

writer = SummaryWriter('logs/CIFAR10_Net_Simple_v3_Aug_Mom_logs')

model.to(device)
loss_function.to(device)

total_train_step = 0
total_test_step = 0

for epoch in range(EPOCH):
print('---------- The {} epoch of training begins ----------'.format(epoch))
print('Learning rate: {}'.format(optimizer.state_dict()['param_groups'][0]['lr']))

train_loss = 0.0
train_acc = 0.0

model.train()
for step, data in enumerate(train_dataloader):
imgs, targets = data
imgs = imgs.to(device)
targets = targets.to(device)
output = model(imgs)

acc = (output.argmax(dim=1)==targets).float().sum()
loss = loss_function(output, targets)
train_loss += loss.item()
train_acc += acc

optimizer.zero_grad()
loss.backward()
optimizer.step()

total_train_step += 1

if total_train_step % SHOW_INFO_STEP == 0:
print('The number of training: {}, Loss: {}'.format(total_train_step, loss.item()))

train_acc /= train_data_len

writer.add_scalar('train_loss', train_loss, epoch)
writer.add_scalar('train_acc', train_acc, epoch)

print('The number of epoch: {}, train_loss: {}'.format(epoch, train_loss))
print('The number of epoch: {}, train_acc: {}'.format(epoch, train_acc))

test_loss = 0.0
test_acc = 0.0

model.eval()
with torch.no_grad():
for step, data in enumerate(test_dataloader):
imgs, targets = data
imgs = imgs.to(device)
targets = targets.to(device)
output = model(imgs)

acc = (output.argmax(dim=1) == targets).float().sum()
loss = loss_function(output, targets)
test_loss += loss.item()
test_acc += acc

total_test_step += 1

test_acc /= test_data_len

writer.add_scalar('test_loss', test_loss, epoch)
writer.add_scalar('test_acc', test_acc, epoch)

print('The number of epoch: {}, test_loss: {}'.format(epoch, test_loss))
print('The number of epoch: {}, test_acc: {}'.format(epoch, test_acc))

writer.add_scalar('learning_rate', optimizer.state_dict()['param_groups'][0]['lr'], epoch)
scheduler.step()

torch.save(model, 'models/CIFAR10_Net_Simple_v3_Aug_Mom_150TRAININGS.pth')
# torch.save(model.state_dict(), 'models/CIFAR10_Net_Simple_v3_Aug_Mom_STATE.pkl')

writer.close()

至此已经成功入门 PyTorch 啦!可以正式进入 Deep Learning 的学习啦!