Contents

datawhalechina PyTorch tutorial

Beautiful landscape

Nature's Beauty

Explore the wonders of the world

参考资料

记录原因

总结常用 pytorch 知识点
上述参考资料内容完整,示例丰富,适合学习

发展历史

  • 人工智能(artificial intelligence)1950~
    • 机器学习(machine learning)1980~
      • 深度学习(deep learning)2010~

模型评价指标

混淆矩阵

也叫误差矩阵

真实结果
正样本(positive class) 负样本(negative class)
预测结果 正样本(positive class) TP(true positive) FP(false positive)
负样本(negative class) FN(false negative) TN(true negative)

其中 TP 表示模型预测为正样本并且预测准确,FN 表示模型预测为负样本但预测错误的情况
TN 和 FP 也类似的

可以利用 sklearn 的函数快速计算混淆矩阵
sklearn.metrics.confusion_matrix(y_true, y_pred, labels=None, sample_weight=None)
y_true: 是样本真实分类结果
y_pred: 是样本预测分类结果
labels:是所给出的类别,通过这个可对类别进行选择
sample_weight : 样本权重

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from sklearn.metrics import confusion_matrix


def compute_confusion_matrix(true_labels_list, pred_labels_list, labels):
    true_labels_list = np.asarray(true_labels_list)
    pred_labels_list = np.asarray(pred_labels_list)
    matrix = confusion_matrix(true_labels_list,
                              pred_labels_list,
                              labels=labels)
    return matrix

多类别的混淆矩阵同理
得到混淆矩阵后,可以使用热力图可视化
在此基础上可以计算 Accuracy,Precision,Recall,F1 Score 等衡量模型的评价指标

Overall Accuracy

表示所有预测正确的样本占所有预测样本总数的比例

$\rm{OA} =\frac{\rm{TP+TN}}{\rm{TP+TN+FP+FN}} = \frac{N_{correct}}{N_{total}}$

准确率是描述模型最简单的指标
但是在样本正负样本极端不平衡时,这个评价指标其实是没有意义的
例如正样本远多于负样本,而模型全部预测为正,这样的准确率固然很高,但是并不代表模型很优秀

1
2
3
4
5
6
def compute_oa(matrix):
    """
    计算总体准确率
    OA=(TP+TN)/(TP+TN+FP+FN)
    """
    return np.trace(matrix) / np.sum(matrix)

Average Accuracy

表示的是平均精度,平均精度计算的是每一类预测正确的样本与该类总体数量之间的比值,最终再取每一类的精度的平均值

$\rm{AA} =\frac{1}{2} (\frac{TP}{\rm{TP+FN}} + \frac{TN}{\rm{FP+TN}})$

1
2
3
4
5
6
7
def compute_aa(matrix):
    """
    计算平均精度
    AA=(TP/(TP+FN)+TN/(FP+TN))/2
    """
    # 将混淆矩阵的对角线元素取出,并且对于混淆矩阵进行列求和,用对角线元素除以求和后的结果,最后对结果计算求出平均值
    return np.mean(np.diag(matrix) / np.sum(matrix, axis=1))

Kappa 系数

一个用于一致性检验的指标,也可以用于衡量分类的效果
对于分类问题而言,一致性就是 模型预测结果和实际分类结果是否一致

$\rm{Kappa} = \frac{p_o-p_e}{1-p_e}$
其中的 $p_o = \rm{OA}$,$p_e = \frac{\sum_{i} (x_i \cdot x_j)}{(\sum_{j=0}^n \sum_{i=0}^{n} x_{ij})^2}$
取值为 -1 到 1 之间,通常大于 0

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def compute_kappa(matrix):
    """
    计算kappa系数
    """
    po = compute_oa(matrix)
    pe = 0
    for i in range(len(matrix)):
        pe += np.sum(matrix[i]) * np.sum(matrix[:, i])
    pe = pe / np.sum(matrix) ** 2
    return (po - pe) / (1 - pe)

Recall

也称召回率,代表了实际为正样本并且也被正确识别为正样本的数量占样本中所有为正样本的比例
用于衡量模型正确识别所有正样本的能力

$\rm{Recall} = \frac{\rm{TP}}{\rm{TP + FN}}$

Precision

也称精准率,代表的是在全部预测为正的结果中,被预测正确的正样本所占的比例

$\rm{Precision} = \frac{\rm{TP}}{\rm{TP + FP}}$

相较于准确率,Precision 更能较好的反应出模型对于正样本的识别能力
和 Recall 不同的是,Precision 代表了预测结果中有多少样本是分类正确的
当我们的希望在于模型尽可能少的漏掉负样本,此时认为模型 Precision 显得更为重要

F1

$\rm{F_1} = 2 \cdot \frac{P \times R}{P+R} = \frac{2TP}{FP+FN+2TP}$
F1 可以理解为 Recall 和 Precision 的加权平均,F1 越高,说明模型鲁棒性越好

人们希望有一种更加广义的方法定义 F-score,希望可以改变 P 和 R 的权重,于是有
$\rm{F_{\beta}} = \frac{\left(1 + \beta^{2}\right) \times P \times R}{\left(\beta^{2} \times P\right)+R}$

当 $\beta > 1$ 时,更偏好召回(Recall)
当 $\beta < 1$ 时,更偏好精准(Precision)
当 $\beta = 1$ 时,平衡精准和召回,即为 F1

PR 曲线

横轴是 Recall,纵轴是 Precision
P-R 曲线上的一个点代表着,在某一阈值下,模型将大于该阈值的结果判定为正样本,小于该阈值的结果判定为负样本,此时返回结果对应的召回率和精确率
整条 P-R 曲线是通过将阈值从高到低移动而生成的
一个曲线所包围的面积越大,性能越优

IOU

雅卡尔指数( Jaccard index),交并比,也是我们在目标检测和语义分割领域经常见到的 IOU

IOU 可以去除目标检测的冗余框进行后处理优化并且还可以计算 Loss 达到优化模型的效果。 $( \rm{IOU} = \frac{A\cap B}{A\cup B}=\frac{S_{交集}}{S_{并集}} )$ 分子部分是模型预测框与真实标注框之间的重叠区域,分母部分是两者的并集,预测框和实际框所占有的总区域,在实际模型识别时会根据我们自己设定合适的阈值来判定正负样本。

AP

是 PR 曲线所包含的面积
mAP 即所有类别的 AP 值的平均值

基础知识

张量

什么是张量?
可以看作是一个数据容器(可以包含字符串)

张量维度 代表含义
0 维张量 代表的是标量(数字)
1 维张量 代表的是向量
2 维张量 代表的是矩阵
3 维张量 时间序列数据

还有 4 维的图像5 维的视频

  • 如何随机初始化一个 4x3 的张量矩阵

    1
    2
    3
    4
    
    import torch
    
    
    x = torch.rand(4, 3)
    
  • 不想随机初始化,而是想得到一个全为 0 或者 1 的张量矩阵该怎么做

    1
    2
    
    torch.zeros(4, 3, dtype=torch.long)
    torch.ones(4, 3, dtype=torch.double)
    

    注意这里通过 dtype 设置数据类型

  • 像构建数组一样,构建一个张量

    1
    
    torch.tensor([1.0, 2, 3, 4.0])
    
  • 基于已有的一个张量,创建一个相同结构的新张量

    1
    
    new_tensor = x.randn_like(x, dtype=torch.float)
    

    float 和 double 都是浮点数类型
    前者有效小数位为 6-7 位,占用 4 字节(32 位)
    后者有效小数位为 15-16 位,占用 8 字节(64 位)

  • 范围控制

    1
    2
    3
    4
    5
    6
    
    # 从s到e,步长为step
    torch.arange(s, e, step)
    
    
    # 从s到e,均匀分成step份
    torch.linspace(s, e, steps)
    
  • 张量的加法

    1
    2
    3
    4
    5
    
    # 可以直接使用加号
    torch.add(x, y)
    
    # in-place 原值修改,直接加到 y 张量上
    y.add_(x)
    
  • 索引操作

    1
    2
    
    # 取第二列,类似 np 的切片操作
    x[:, 1]
    

    注意
    索引得到的结果与原数据 共享内存
    考虑使用 copy() 等方法复制一个新对象

  • 改变维度的大小

    1
    2
    3
    4
    
    x = torch.randn(4, 4)
    y = x.view(16)
    z = x.view(-1, 8)    # 指定列维度为 8,行维度等于 -1 表示其由列维度决定
    print(x.size(), y.size(), z.size())
    

    注意此时 y、z 也是和 x 共享内存的
    x 被修改,则 y、z 也会改变
    故推荐先用 clone() 创造一个张量副本,再使用 torch.view() 进行函数维度变换

  • 获取张量的值

    1
    2
    
    # 获得该张量的 value,而不获得其他性质例如梯度
    x.item()
    
  • 广播机制 broadcasting

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    # 当对两个形状不同的 Tensor 【按元素运算】时,可能会触发
    x = torch.arange(1, 3).view(1, 2)
    # tensor([[1, 2]])
    
    y = torch.arange(1, 4).view(3, 1)
    # tensor([[1],
    #         [2],
    #         [3]])
    
    x + y
    # tensor([[2, 3],
    #         [3, 4],
    #         [4, 5]])
    

    x 和 y 都进行了相应的广播(复制),使得加法可以进行
    得到一个 3 行 2 列的矩阵【按元素相加】

自动求导

若参数 requires_grad 为 True,那么将会追踪对于该张量的所有操作
当完成计算后可以通过调用 .backward(),来自动计算所有的梯度
张量的所有梯度将会自动累加到 .grad 属性

阻止一个张量被跟踪历史,可以调用 .detach() 方法
将其与计算历史分离,并阻止它未来的计算记录被跟踪

with torch.no_grad(): 在评估模型时特别有用
因为模型可能具有 requires_grad = True 的可训练的参数
但是我们不需要在评估过程中对他们进行梯度计算

若没有指定 requires_grad = True 则默认为 False

反向传播

若张量是一个标量,则 .backward() 和 .backward(torch.tensor(1.)) 等价

数学上,若有向量函数 $\vec{y}=f(\vec{x})$,那么 $\vec{y}$ 关于 $\vec{x}$ 的梯度 就是一个雅可比矩阵:

$$ J=\left(\begin{array}{ccc}\frac{\partial y_{1}}{\partial x_{1}} & \cdots & \frac{\partial y_{1}}{\partial x_{n}} \ \vdots & \ddots & \vdots \ \frac{\partial y_{m}}{\partial x_{1}} & \cdots & \frac{\partial y_{m}}{\partial x_{n}}\end{array}\right) $$

$\vec{y}$ 对 $\vec{x_1}$ 求导得到矩阵 $J$ 第一列,对 $\vec{x_2}$ 求导得到矩阵 $J$ 第二列…
torch.autograd 这个包就是用来计算一些雅可比矩阵的乘积的

例如,如果 $v$ 是一个标量函数 $l = g(\vec{y})$ 的梯度:

$$ v=\left(\begin{array}{lll}\frac{\partial l}{\partial y_{1}} & \cdots & \frac{\partial l}{\partial y_{m}}\end{array}\right) $$

由链式法则,我们可以得到:

$$ v J=\left(\begin{array}{lll}\frac{\partial l}{\partial y_{1}} & \cdots & \frac{\partial l}{\partial y_{m}}\end{array}\right)\left(\begin{array}{ccc}\frac{\partial y_{1}}{\partial x_{1}} & \cdots & \frac{\partial y_{1}}{\partial x_{n}} \ \vdots & \ddots & \vdots \ \frac{\partial y_{m}}{\partial x_{1}} & \cdots & \frac{\partial y_{m}}{\partial x_{n}}\end{array}\right)=\left(\begin{array}{lll}\frac{\partial l}{\partial x_{1}} & \cdots & \frac{\partial l}{\partial x_{n}}\end{array}\right) $$

注意
grad 在反向传播过程中是累加的(accumulated)
这意味着每次反向传播得到的梯度,都是与之前的梯度累加的结果
所以一般在反向传播之前需把【梯度清零】

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 反向传播
x = torch.ones(2, 2, requires_grad=True)
out2 = x.sum()
out2.backward()
print(x.grad)

# 注意若不将梯度清零,grad 是累加的
out3 = x.sum()
# x.grad.data.zero_()
out3.backward()
print(x.grad)

如果我们想要修改 tensor 的数值
但是又不希望被 autograd 记录(即不会影响反向传播)
那么可以对 tensor.data 进行操作

并行计算简介

  • 并行计算适用场景
    遇到数据量较大无法在单块 GPU 上完成
    需要提升计算速度的场景

假设已经拥有一块 NVIDIA GPU 并安装了相关的驱动
使用 nvidia-smi 命令来查看 GPU 信息和使用情况

  • 为什么需要 CUDA
    CUDA 是 NVIDIA 提供的一种 GPU 并行计算框架
    使用 CUDA 语言实现 GPU 编程
    但是,在我们使用 PyTorch 编写深度学习代码时,使用 .cuda() 是让我们的模型或者数据从 CPU 迁移到 GPU 上,通过 GPU 计算

市面上的 GPU 并不是都支持 CUDA,只有 部分 NVIDIA 的 GPU 才支持,AMD 的 GPU 编程接口采用的是 OpenCL,在现阶段 PyTorch 并不支持
数据在 GPU 和 CPU 之间进行传递时会比较耗时,我们应当尽量避免数据的切换
GPU 运算很快,但是在进行简单的操作时,我们应该尽量使用 CPU 去完成
当我们的服务器上有多个 GPU,我们应该指明具体使用哪一块 GPU
如果不设置的话,tensor.cuda() 会默认将 tensor 保存到第一块 GPU 上,等价于 tensor.cuda(0),这将有可能导致 out of memory 错误

  • 常见的并行的方法

    • 网络结构分布到不同的设备中(Network partitioning)
      主要思路:将一个模型的各个部分拆分,然后将不同的部分放入到不同 GPU 来做不同任务的计算
      问题:不同模型组件在不同的 GPU 上时,GPU 之间的通信是一个考验。但是 GPU 的通信在这种密集任务中很难办到
    • 同一层的任务分布到不同数据中(Layer-wise partitioning)
      主要思路:同一层的模型做一个拆分,让不同的 GPU 去训练同一层模型的部分任务
      问题:在需要大量的训练,同步任务加重的情况下,会出现和第一种方式一样的问题
    • 不同的数据分布到不同的设备中,执行相同的任务(Data parallelism)
      不再拆分模型,但是将输入的数据拆分
      同一个模型在不同 GPU 中训练一部分数据,然后再分别计算一部分数据之后,只需要将输出的数据做一个汇总,然后再反传
      是现在的主流数据并行方式
  • 单卡训练

    1
    2
    3
    4
    5
    6
    7
    
    model = Net()
    model.cuda()    # 模型转移到 CUDA 上
    
    for image,label in dataloader:
        # 图像和标签转移到 CUDA 上
        image = image.cuda()
        label = label.cuda()
    
  • 多卡训练

    • DataParallel,简称 DP
      将计算任务划分成多个子任务,并在多个 GPU 卡上同时执行这些子任务
    • DistributedDataParallel,简称 DDP

AI 硬件加速设备

  • CPU
    即 Central Processing Unit,中文名为中央处理器
    在现代计算机体系结构中,CPU 对计算机的所有硬件资源(如存储器、输入输出单元)进行控制调配、执行通用运算
    计算机系统中所有软件层的操作,最终都将通过 指令集 映射为 CPU 的操作

  • GPU
    即 Graphics Processing Unit,中文名为图形处理单元
    在传统的冯·诺依曼结构中,CPU 每执行一条指令都需要 从存储器中读取数据,根据指令对数据进行相应的操作
    从这个特点可以看出,CPU 的主要职责并不只是数据运算,还需要执行存储读取、指令分析、分支跳转等命令
    深度学习算法通常需要进行海量的数据处理,用 CPU 执行算法时,CPU 将花费大量的时间在数据/指令的读取分析上,而 CPU 的频率、内存的带宽等条件又不可能无限制提高
    而 GPU 的控制相对简单,大部分的晶体管可以组成各类专用电路、多条流水线,使得 GPU 的计算速度远高于 CPU;同时 GPU 拥有了 更加强大的浮点运算 能力,可以缓解深度学习算法的训练难题

    GPU 没有独立工作的能力,必须由 CPU 进行控制调用才能工作,且 GPU 的功耗一般比较高

  • ASIC
    即 Application-Specific Integrated Circuit,专用集成电路
    是为实现特定要求而定制的芯片
    定制的特性有助于提高 ASIC 的性能功耗比。但是由于需要定制,相对开发周期长,功能难以扩展

  • TPU
    即 Tensor Processing Unit,中文名为张量处理器
    谷歌在 2015 年 6 月的 IO 开发者大会上推出了为优化自身的 TensorFlow 框架而设计打造的一款计算神经网络专用芯片。它主要用于进行搜索,图像,语音等模型和技术的处理

    整个 TPU 中最重要的计算单元是右上角黄色的矩阵乘单元“Matrix Multiply Unit”
    MMU 采用了与传统 CPU 和 GPU 截然不同的脉动阵列(systolic array)结构来加速 AI 运算
    脉动阵列能够在一个时钟周期内处理数十万次矩阵运算,在每次运算过程中,TPU 能够将多个运算逻辑单元(ALU)串联在一起,并复用从一个寄存器中取得的结果 这种设计,不仅能够将 数据复用 实现最大化,减少芯片在运算过程中的内存访问次数,提高 AI 计算效率,同时也降低了内存带宽压力,进而降低内存访问的能耗

  • NPU
    即 Neural-network Processing Unit,中文名为神经网络处理器
    它采用“数据驱动并行计算”的架构,特别擅长处理视频、图像类的海量多媒体数据
    它模仿生物神经网络而构建,由若干人工神经元结点互联而成。神经元之间通过 突触 两两连接,突触记录了神经元间联系的权值强弱

主要组成模块

深度学习中训练和验证过程最大的特点在于读入数据是按批的,每次读入一个批次的数据,放入 GPU 中训练
然后将损失函数反向传播回网络最前面的层,同时使用优化器调整网络参数

基本配置

导入常用的包

1
2
3
4
5
6
import os
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
import torch.optim as optimizer

超参数设置
可以直接将超参数设置在训练的代码里
也可以使用 yaml、json 等配置文件来存储超参数,方便后续的调试和修改

1
2
3
4
5
6
7
8
# 批次的大小
batch_size = 16

# 优化器的学习率
lr = 1e-4

# 模型迭代次数
max_epochs = 100

GPU 的配置
数据和模型如果没有经过 显式指明 设备,默认会存储在 CPU 上

1
2
device = torch.device("cuda:1" if torch.cuda.is_available() else "cpu")
# 指明调用的GPU为1号

数据读入

PyTorch 的数据读入是通过 Dataset + DataLoader 的方式完成的
Dataset 定义好数据的格式和数据变换形式
DataLoader 用 iterative 的方式不断读入批次数据

可以自定义 Dataset 类来实现灵活的数据读取
Dataset 类需要继承 PyTorch 自身的 Dataset 类,主要包含三个函数:
__init__: 用于向类中传入外部参数,同时定义样本集
__getitem__: 用于逐个读取样本集合中的元素,可以进行一定的变换,并将返回训练/验证所需的数据
__len__: 用于返回数据集的样本数

示例: 假设将 图像路径类别标签 记录在 csv 文件中,内容形如:
image1.jpg, 0
image2.jpg, 1
……
image9.jpg, 9

 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
# 自定义 Dataset
import os
import pandas as pd
import matplotlib.pyplot as plt
from torch.utils.data import Dataset, DataLoader
from torchvision.io import read_image


class MyDataset(Dataset):
    def __init__(self, annotations_file, img_dir, transform=None, target_transform=None):
        """
        Args:
            annotations_file (string):带注释的csv文件的路径。
            img_dir (string):包含所有图像的目录。
            transform(可调用,可选):应用于样本的转换。
            target_transform(可调用,可选):应用于标签的转换。
        """
        self.img_labels = pd.read_csv(annotations_file)
        self.img_dir = img_dir
        self.transform = transform
        self.target_transform = target_transform

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

    def __getitem__(self, idx):
        img_path = os.path.join(self.img_dir, self.img_labels.iloc[idx, 0])

        # 读取图片
        image = read_image(img_path)
        label = self.img_labels.iloc[idx, 1]

        # 根据传入的参数对图像和标签进行转换
        if self.transform:
            image = self.transform(image)
        if self.target_transform:
            label = self.target_transform(label)
        return image, label
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# 批量读入数据
train_loader = DataLoader(
    train_data,
    batch_size=batch_size,
    num_workers=4,    # 用于读取数据的进程数
    shuffle=True,
    drop_last=True    # 忽略掉剩余达不到批次数的样本
)
val_loader = DataLoader(
    val_data,
    batch_size=batch_size,
    num_workers=4,
    shuffle=False    # 验证集的数据无需打乱
)

# 利用迭代器获得一个样本和对应标签
images, labels = next(iter(val_loader))
print(images.shape)
# 可视化图片,transpose调整颜色通道顺序
plt.imshow(images[0].transpose(1,2,0))
plt.show()

模型构建

人工智能的第三次浪潮受益于 卷积神经网络的出现BP 反向传播算法 的实现

PyTorch 中神经网络构造一般是基于 nn.Module 类来完成的
nn.Module 类是 torch.nn 模块里提供的一个 模型构造类
是所有神经网络模块的基类,我们可以继承它来定义我们想要的模型

下面通过重载 Module 类的 init 函数和 forward 函数来自定义多层感知机 MLP 类

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


class MLP(nn.Module):
    def __init__(self, **kwargs):
        super(MLP, self).__init__(**kwargs)
        self.hidden = nn.Linear(784, 256)
        self.act = nn.ReLU()
        self.output = nn.Linear(256,10)

    # 定义模型的正向传播(前向计算),即如何根据输入 x 计算返回所需要的模型输出
    # 这里先经过一层隐藏层,然后再使用激活函数,最后经过线性层
    def forward(self, x):
        o = self.act(self.hidden(x))
        return self.output(o)

以上的 MLP 类中无须定义反向传播函数
系统将通过【自动求梯度】⽽【自动生成】反向传播所需的【backward 函数】

1
2
3
4
X = torch.rand(2, 784)    # 生成随机的输入张量
net = MLP()    # 实例化模型
print(net)    # 打印模型结构
net(X)    # 正向传播

继承于 Module 类的子类,既可以是一个层,也可以是一个模型或者模型中的一个部分

常见的层

PyTorch 提供了⼤量常用的层,例如全连接层、卷积层、池化层与循环层等等
我们还可以自定义层

  • 不含模型参数的层
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import torch
from torch import nn


class MyLayer(nn.Module):
    def __init__(self, **kwargs):
        super(MyLayer, self).__init__(**kwargs)

    # 在这里定义层的计算
    def forward(self, x):
        # 该层所做计算为输入减去均值
        return x - x.mean()


tmp_in = torch.tensor([1, 2, 3, 4, 5], dtype=torch.float)
# 实例化该层,然后做前向计算
layer = MyLayer()
layer(tmp_in)
  • 含模型参数的层
    模型参数可以通过训练学出
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import torch
from torch import nn


class MyListDense(nn.Module):
    def __init__(self):
        super(MyListDense, self).__init__()
        # 模型参数列表,初始化3个服从标准正态分布的4x4随机张量
        self.params = nn.ParameterList(
            [nn.Parameter(torch.randn(4, 4)) for i in range(3)]
        )
        # 额外添加一个服从标准正态分布的4x1随机张量
        self.params.append(nn.Parameter(torch.randn(4, 1)))

    def forward(self, x):
        for i in range(len(self.params)):
            # 将输入与参数一一进行矩阵乘法
            x = torch.mm(x, self.params[i])
        return x

net = MyListDense()
print(net)

除了 nn.ParameterList,类似的还有 ParameterDict,用于定义参数字典

  • 二维卷积层
    二维卷积层将输入和卷积核做 互相关运算,并加上一个 标量偏差 来得到输出
    卷积层的模型参数包括了卷积核和标量偏差。在训练模型的时候,通常我们先对卷积核随机初始化,然后不断迭代卷积核和偏差

    在二维互相关运算中,卷积窗口从输入数组的最左上方开始,按从左往右、从上往下 的顺序,依次在输⼊数组上滑动

 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
import torch
from torch import nn


# 卷积运算(二维互相关)
def corr2d(X, K):
    # 输入 X 和卷积核 K
    h, w = K.shape
    X, K = X.float(), K.float()
    # 初始化卷积运算后的返回张量
    Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))
    for i in range(Y.shape[0]):
        for j in range(Y.shape[1]):
            # 与卷积核对应位置元素相乘,最后求和
            Y[i, j] = (X[i: i + h, j: j + w] * K).sum()
    return Y

# 二维卷积层
class Conv2D(nn.Module):
    def __init__(self, kernel_size):
        super(Conv2D, self).__init__()
        # 随机初始化卷积核参数和标准偏差
        self.weight = nn.Parameter(torch.randn(kernel_size))
        self.bias = nn.Parameter(torch.randn(1))

    def forward(self, x):
        return corr2d(x, self.weight) + self.bias
  • 池化层
    池化层每次对输入数据的一个固定形状窗口(⼜称池化窗口)中的元素计算输出
    对窗口内元素计算均值、最大值等,常见的池化包括最大池化或平均池化
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import torch
from torch import nn


# 默认使用最大池化
def pool2d(X, pool_size, mode='max'):
    p_h, p_w = pool_size
    Y = torch.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1))
    for i in range(Y.shape[0]):
        for j in range(Y.shape[1]):
            if mode == 'max':
                Y[i, j] = X[i: i + p_h, j: j + p_w].max()
            elif mode == 'avg':
                Y[i, j] = X[i: i + p_h, j: j + p_w].mean()
    return Y

X = torch.tensor([[0, 1, 2], [3, 4, 5], [6, 7, 8]], dtype=torch.float)
pool2d(X, (2, 2))
pool2d(X, (2, 2), 'avg')

LeNet

什么是前馈神经网络(feed-forward network)?
它接受一个输入,然后将它送入下一层,一层接一层的传递,最后给出输出

一个神经网络的典型训练过程
首先定义包含一些可学习参数(或者叫权重)的网络
然后在输入数据集上迭代
计算 loss(输出和正确答案的距离)
将梯度反向传播给网络的参数以进行更新
常见更新参数的方式:weight = weight - learning_rate * gradient

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import torch
import torch.nn as nn
import torch.nn.functional as F


class Net(nn.Module):

    def __init__(self):
        super(Net, self).__init__()
        # 输入图像的 channel 为 1
        # 输出 channel 为 6
        # 卷积核大小为 5x5
        self.conv1 = nn.Conv2d(1, 6, 5)
        self.conv2 = nn.Conv2d(6, 16, 5)
        # an affine operation: y = Wx + b
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        # 2x2 Max pooling
        x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
        x = F.max_pool2d(F.relu(self.conv2(x)), 2)
        x = x.view(-1, self.num_flat_features(x))
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

    def num_flat_features(self, x):
        size = x.size()[1:]  # 忽略批处理维度
        num_features = 1
        for s in size:
            num_features *= s
        return num_features


net = Net()
print(net)
# 打印模型参数
params = list(net.parameters())
print(len(params))
print(params[0].size())  # conv1 的权重

# 随机生成测试输入
input = torch.randn(1, 1, 32, 32)
out = net(input)
print(out)
# 清零所有参数的梯度缓存,然后进行随机梯度的反向传播
net.zero_grad()
out.backward(torch.randn(1, 10))

我们可以在 forward 函数中对张量进行任何操作和计算
autograd.Function - 实现了自动求导前向和反向传播的定义,每个 Tensor 至少创建一个 Function 节点,该节点连接到创建 Tensor 的函数并对其历史进行编码
backward 函数会在使用 autograd 时自动定义

注意
torch.nn 只支持小批量处理(mini-batches),不支持单个样本的输入
比如,nn.Conv2d 接受一个 4 维的张量,即 n_Samples x n_Channels x Height x Width
如果是一个单独的样本,只需要使用 input.unsqueeze(0) 在第 0 维来添加一个“假的”批大小维度
unsqueeze 函数用于在张量的指定位置添加一个大小为 1 的新维度。这个操作不会改变张量的数据,但会改变其形状

模型初始化

一个好的权重初始值,会使模型收敛速度提高,使模型准确率更高
一般情况下,我们不使用全 0 初始值训练网络,因为这样会导致 梯度消失,从而影响模型的训练效果
可以将参数初始化为一个很小的值,如 0.01,0.1 等
torch.nn.init 提供了诸多的初始化方法

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

conv = nn.Conv2d(1,3,3)
linear = nn.Linear(10,1)

# 对conv进行kaiming初始化
torch.nn.init.kaiming_normal_(conv.weight.data)
conv.weight.data

# 对linear进行常数初始化
torch.nn.init.constant_(linear.weight.data,0.3)
linear.weight.data

可以定义一个 initialize_weights 函数,输入参数为一个模型
函数中遍历当前模型的每一层,然后判断各层属于什么类型,然后根据不同类型层,设定不同的权值初始化方法
实列化模型后,通过 mymodel.apply(initialize_weights),即可完成参数初始化

损失函数

一个好的训练离不开优质的 负反馈,这里的损失函数就是模型的负反馈
模型产生的结果与真实标签的评价指标,我们的模型可以按照损失函数的目标来 做出改进

常见损失函数

  • 交叉熵损失函数

    $$ \begin{aligned} \operatorname{loss}(x, \text { class }) &= -\log \left(\frac{\exp (x[\text { class }])}{\sum*{j} \exp (x[j])}\right) \ &= -x[\text { class }]+\log \left(\sum*{j} \exp (x[j])\right) \end{aligned} $$

    1
    2
    3
    4
    5
    
    loss = nn.CrossEntropyLoss()
    input = torch.randn(3, 5, requires_grad=True)
    target = torch.empty(3, dtype=torch.long).random_(5)
    output = loss(input, target)
    output.backward()
    
  • L1 损失函数
    $$ L*{n} = |x*{n}-y_{n}| $$

    1
    
    loss = nn.L1Loss()
    
  • MSE 损失函数
    $$ l*{n}=\left(x*{n}-y_{n}\right)^{2} $$

    1
    
    loss = nn.MSELoss()
    
  • KL 散度
    也就是计算相对熵

    $$ \begin{aligned} D*{\mathrm{KL}}(P, Q) &= \mathrm{E}{X \sim P}\left[\log \frac{P(X)}{Q(X)}\right] \ &= \mathrm{E}{X \sim P}[\log P(X)-\log Q(X)] \ &= \sum{i=1}^{n} P\left(x{i}\right)\left(\log P\left(x*{i}\right)-\log Q\left(x_{i}\right)\right) \end{aligned} $$

    1
    
    loss = nn.KLDivLoss()
    
  • 余弦相似度 $$ \operatorname{loss}(x, y)=\left{\begin{array}{ll} 1-\cos \left(x_{1}, x_{2}\right), & \text { if } y=1 \ \max \left{0, \cos \left(x_{1}, x_{2}\right)-\text { margin }\right}, & \text { if } y=-1 \end{array}\right. $$

    1
    
    loss = nn.CosineEmbeddingLoss()
    

训练和评估

完成了模型的训练后,需要在测试集/验证集上完成模型的验证
以确保我们的模型具有泛化能力、不会出现过拟合等问题

  • 图像分类模型的训练过程

     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
    
    def train(epoch):
        # 设置为训练状态
        model.train()
        train_loss = 0
    
        # 用 for 循环读取 DataLoader 中的全部数据
        for data, label in train_loader:
    
            # 将数据放到 GPU 上用于后续计算
            data, label = data.cuda(), label.cuda()
    
            # 开始用当前批次数据做训练时,应当先将优化器的梯度置零
            optimizer.zero_grad()
    
            # 将 data 送入模型中训练
            output = model(data)
    
            # 计算损失
            loss = criterion(output, label)
    
            # 将 loss 反向传播回网络
            loss.backward()
    
            # 使用优化器【更新模型参数】
            optimizer.step()
    
            train_loss += loss.item()*data.size(0)
        train_loss = train_loss/len(train_loader.dataset)
        print('Epoch: {} \tTraining Loss: {:.6f}'.format(epoch, train_loss))
    
  • 模型验证过程,与训练过程类似

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    def val(epoch):
        # 设置为评估状态
        model.eval()
        val_loss = 0
    
        # 不需要梯度
        with torch.no_grad():
            for data, label in val_loader:
                data, label = data.cuda(), label.cuda()
                output = model(data)
                preds = torch.argmax(output, 1)
                loss = criterion(output, label)
                val_loss += loss.item()*data.size(0)
                running_accu += torch.sum(preds == label.data)
        val_loss = val_loss/len(val_loader.dataset)
        print('Epoch: {} \tTraining Loss: {:.6f}'.format(epoch, val_loss))
    
  • 获取模型的分类报告,模型的准确率、召回率、F1 值等指标

    1
    2
    3
    4
    5
    6
    7
    8
    
    from sklearn.metrics import classification_report
    
    
    classification_report(
        labels.cpu(),
        preds.cpu(),
        target_names=class_names
    )
    

优化器

深度学习的目标是通过不断改变网络参数,使得参数能够对输入做各种 非线性变换拟合 输出
本质上就是一个函数去寻找最优解

以经典的 resnet-50 为例,它大约有 2000 万个系数需要进行计算
有以下两种方法:
1、直接暴力穷举参数,这种方法从理论上行得通,但是实施上可能性基本为 0,因为参数量过于庞大
2、BP 反向传播 + 优化器逼近求解
优化器根据网络反向传播的梯度信息来更新网络的参数
以起到降低 loss 函数计算值,使得模型输出更加接近真实标签

优化器需要根据模型实际进行选择,不存在绝对的好坏之分
PyTorch 优化器库 torch.optim 提供了多种优化器

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


# 随机初始化权重,服从正态分布
weight = torch.randn((2, 2), requires_grad=True)
>>> tensor([[-0.3077, -0.1808],
            [-0.7462, -1.5556]])

# 设置梯度为全1矩阵
weight.grad = torch.ones((2, 2))
>>> tensor([[1., 1.],
            [1., 1.]])
1
2
3
4
5
6
7
8
# 实例化优化器
optimizer = torch.optim.SGD([weight], lr=0.1, momentum=0.9)
optimizer.step()
# 执行一步优化后更新的权重与梯度
>>> tensor([[-0.4077, -0.2808],
            [-0.8462, -1.6556]])
>>> tensor([[1., 1.],
            [1., 1.]])
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 进行梯度清零,权重的梯度变为全 0
optimizer.zero_grad()

# 输出参数
optimizer.param_groups
>>> [{'params': [tensor([[-0.4077, -0.2808],
            [-0.8462, -1.6556]], requires_grad=True)], 'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}]

# 查看参数位置
id(optimizer.param_groups[0]['params'][0])
>>> 1841923407424

id(weight)
>>> 1841923407424
# 注意到optimizer中的权重和weight变量在内存中的地址是一样的
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 添加参数:weight2
weight2 = torch.randn((3, 3), requires_grad=True)
optimizer.add_param_group({"params": weight2, 'lr': 0.0001, 'nesterov': True})
# 查看现有的参数
optimizer.param_groups
>>> [{'params': [tensor([[-0.4077, -0.2808],
        [-0.8462, -1.6556]], requires_grad=True)], 'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}, {'params': [tensor([[ 0.4539, -2.1901, -0.6662],
        [ 0.6630, -1.5178, -0.8708],
        [-2.0222,  1.4573,  0.8657]], requires_grad=True)], 'lr': 0.0001, 'nesterov': True, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0}]

# 查看当前状态信息
opt_state_dict = optimizer.state_dict()
>>> {'state': {0: {'momentum_buffer': tensor([[1., 1.],
        [1., 1.]])}}, 'param_groups': [{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False, 'params': [0]}, {'lr': 0.0001, 'nesterov': True, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'params': [1]}]}
1
2
3
4
5
6
# 进行5次step操作
for _ in range(50):
    optimizer.step()
optimizer.state_dict()
>>> {'state': {0: {'momentum_buffer': tensor([[0.0052, 0.0052],
        [0.0052, 0.0052]])}}, 'param_groups': [{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False, 'params': [0]}, {'lr': 0.0001, 'nesterov': True, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'params': [1]}]}
1
2
3
4
5
6
7
8
9
# 保存参数信息
torch.save(
    optimizer.state_dict(),
    os.path.join(r"D:\python_project\pytorch_learning", "optimizer_state_dict.pkl")
)

# 加载参数信息
state_dict = torch.load(r"D:\python_project\pytorch_learning\optimizer_state_dict.pkl")
optimizer.load_state_dict(state_dict)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 给网络不同的层赋予不同的优化器参数
from torchvision.models import resnet18


net = resnet18()
optimizer = optim.SGD(
    [
        {'params':net.fc.parameters()},
        {'params':net.layer4[0].conv1.parameters(), 'lr':1e-2}
    ],
    lr=1e-5
)

模型定义

可以通过 Sequential,ModuleList 和 ModuleDict 三种方式定义 PyTorch 模型

Sequential

下面我们定义一个前向计算为 简单串联各个层 的模型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from collections import OrderedDict


class MySequential(nn.Module):
    def __init__(self, *args):
        super(MySequential, self).__init__()
        # 如果传入的是一个 OrderedDict
        if len(args) == 1 and isinstance(args[0], OrderedDict):
            for key, module in args[0].items():
                self.add_module(key, module)
        # 传入的是列表形式的 Module
        else:
            for idx, module in enumerate(args):
                self.add_module(str(idx), module)

    def forward(self, input):
        # self._modules 返回一个 OrderedDict,保证会按照成员添加时的顺序遍历
        for module in self._modules.values():
            input = module(input)  # 简单串联
        return input

add_module 方法会将 module 添加进 self._modules

通过 Sequential 类可以更加简单地实现上述模型

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


net = nn.Sequential(
    nn.Linear(784, 256),
    nn.ReLU(),
    nn.Linear(256, 10),
)
print(net)

使用 Sequential 定义的模型不需要再写 forward,因为其入参顺序即各层的串联顺序
但这也会使得模型定义丧失灵活性,按需选择

ModuleList

ModuleList 接收一个子模块(或层,需属于 nn.Module 类)的列表作为输入
子模块或层的权重也会自动添加到网络中来

ModuleList 只是将不同的模块储存在一起,其中元素的先后顺序并不代表其在网络中的真实位置顺序

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


class model(nn.Module):
    def __init__(self, ...):
        super().__init__()
        self.modulelist = nn.ModuleList([nn.Linear(784, 256), nn.ReLU()])
        self.modulelist.append(nn.Linear(256, 10))    # 类似List的append操作
        ...

    def forward(self, x):
        for layer in self.modulelist:
            x = layer(x)
        return x

ModuleDict

和 ModuleList 的作用类似
只是更方便地为神经网络的层 添加名称

1
2
3
4
5
nn.ModuleDict({
    'linear': nn.Linear(784, 256),
    'act': nn.ReLU(),
})
net['output'] = nn.Linear(256, 10)    # 类似字典的添加操作

利用模型块快速搭建复杂网络

当模型的深度非常大时候,使用 Sequential 一层一层定义模型结构就不合适了
对于大部分模型,虽然模型层数很多,但是也存在很多重复结构
我们可以将重复出现的结构定义为模块,即可利用模块来高效搭建复杂网络

推荐先定义模型块,再定义模型块之间的连接顺序和计算方式
类似搭积木或者拼乐高

动手试试

下面以 U-Net 为例:
组成 U-Net 的模型块主要有如下几个部分:

  • 每个子块内部的两次卷积(Double Convolution)
  • 左侧模型块之间的下采样连接,即最大池化(Max pooling)
  • 右侧模型块之间的上采样连接(Up sampling)
  • 输出层的处理
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 两次卷积
import torch
import torch.nn as nn
import torch.nn.functional as F


class DoubleConv(nn.Module):
    """(convolution => [BN] => ReLU) * 2"""

    def __init__(self, in_channels, out_channels, mid_channels=None):
        super().__init__()
        if not mid_channels:
            mid_channels = out_channels
        self.double_conv = nn.Sequential(
            nn.Conv2d(in_channels, mid_channels, kernel_size=3, padding=1, bias=False),
            nn.BatchNorm2d(mid_channels),
            nn.ReLU(inplace=True),
            nn.Conv2d(mid_channels, out_channels, kernel_size=3, padding=1, bias=False),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True)
        )

    def forward(self, x):
        return self.double_conv(x)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 下采样
class Down(nn.Module):
    """Downscaling with maxpool then double conv"""

    def __init__(self, in_channels, out_channels):
        super().__init__()
        self.maxpool_conv = nn.Sequential(
            nn.MaxPool2d(2),
            DoubleConv(in_channels, out_channels)
        )

    def forward(self, x):
        return self.maxpool_conv(x)
 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
# 上采样
class Up(nn.Module):
    """Upscaling then double conv"""

    def __init__(self, in_channels, out_channels, bilinear=False):
        super().__init__()

        # if bilinear, use the normal convolutions to reduce the number of channels
        if bilinear:
            self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)
            self.conv = DoubleConv(in_channels, out_channels, in_channels // 2)
        else:
            self.up = nn.ConvTranspose2d(in_channels, in_channels // 2, kernel_size=2, stride=2)
            self.conv = DoubleConv(in_channels, out_channels)

    def forward(self, x1, x2):
        x1 = self.up(x1)
        # input is CHW
        diffY = x2.size()[2] - x1.size()[2]
        diffX = x2.size()[3] - x1.size()[3]

        x1 = F.pad(x1, [diffX // 2, diffX - diffX // 2,
                        diffY // 2, diffY - diffY // 2])
        # if you have padding issues, see
        # https://github.com/HaiyongJiang/U-Net-Pytorch-Unstructured-Buggy/commit/0e854509c2cea854e247a9c615f175f76fbb2e3a
        # https://github.com/xiaopeng-liao/Pytorch-UNet/commit/8ebac70e633bac59fc22bb5195e513d5832fb3bd
        x = torch.cat([x2, x1], dim=1)
        return self.conv(x)
1
2
3
4
5
6
7
class OutConv(nn.Module):
    def __init__(self, in_channels, out_channels):
        super(OutConv, self).__init__()
        self.conv = nn.Conv2d(in_channels, out_channels, kernel_size=1)

    def forward(self, x):
        return self.conv(x)

训练技巧

自定义损失函数

在自定义损失函数时,涉及到数学运算时,我们最好全程使用 PyTorch 提供的张量计算接口
这样就不需要我们实现自动求导功能并且我们可以直接调用 cuda

损失函数的继承关系:Loss 函数部分继承自_loss,部分继承自_WeightedLoss,而_WeightedLoss 继承自_loss, _loss 继承自 nn.Module
我们可以将其当作神经网络的一层来对待
下面自定义 DiceLoss,继承自 nn.Module 类
$DSC = \frac{2|X∩Y|}{|X|+|Y|}$

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class DiceLoss(nn.Module):
    def __init__(self,weight=None,size_average=True):
        super(DiceLoss,self).__init__()

    def forward(self,inputs,targets,smooth=1):
        inputs = F.sigmoid(inputs)
        inputs = inputs.view(-1)
        targets = targets.view(-1)
        intersection = (inputs * targets).sum()
        dice = (2.*intersection + smooth)/(inputs.sum() + targets.sum() + smooth)
        return 1 - dice


criterion = DiceLoss()
loss = criterion(input,targets)

动态调整学习率

在 PyTorch 中,将动态调整学习率称为 scheduler
训练过程中,逐渐减小学习率,改善收敛速度慢或者振荡的现象,提高我们的精度

官方 torch.optim.lr_scheduler 种类很多,但都是继承自_LRScheduler 类
我们可以通过 help(torch.optim.lr_scheduler)来查看这些类的具体使用方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 自定义 scheduler
def adjust_learning_rate(optimizer, epoch):
    # 每训练 30 轮后,学习率下降为原来的 10%
    lr = args.lr * (0.1 ** (epoch // 30))
    for param_group in optimizer.param_groups:
        param_group['lr'] = lr


optimizer = torch.optim.SGD(model.parameters(),lr = args.lr,momentum = 0.9)
for epoch in range(10):
    train(...)
    validate(...)
    # 每一轮训练结束后,调整学习率
    adjust_learning_rate(optimizer,epoch)

模型微调

随着深度学习的发展,模型的参数越来越大,许多开源模型都是在较大数据集上进行训练的
但在实际应用中,我们的数据集可能只有几千张,这时从头开始训练具有几千万参数的大型神经网络是不现实的
因为越大的模型对数据量的要求越大,仅有几千张数据集无法避免过拟合

为了应对过拟合,一个解决办法是收集更多的数据。然而,收集和标注数据会花费大量的时间和资金

另外一种解决办法是应用 迁移学习(transfer learning),将从源数据集学到的知识迁移到目标数据集上
迁移学习的一大应用场景是模型微调(finetune),我们先找到一个同类的别人训练好的模型,把别人现成的训练好了的模型拿过来,换成自己的数据,通过训练调整一下参数
在 PyTorch 中提供了许多预训练好的网络模型(VGG,ResNet 系列,mobilenet 系列……),这些模型都是 PyTorch 官方在相应的大型数据集训练好的

预训练模型的权重在 Windows 下的默认存储路径是 C:\Users<username>.cache\torch\hub\checkpoint
还可以将自己的权重下载下来放到同文件夹下,

1
2
self.model = models.resnet50(pretrained=False)
self.model.load_state_dict(torch.load('./model/resnet50-19c8e357.pth'))

半精度训练

GPU 的性能主要分为两部分:算力和显存
前者决定了显卡 计算的速度,后者则决定了显卡可以 同时放入多少数据 用于计算

有时候数据本身也比较大(比如 3D 图像、视频等),显存较小的情况下可能甚至 batch size=1 的情况都无法实现
因此,合理使用显存也就显得十分重要

PyTorch 默认的浮点数存储方式用的是 torch.float32
小数点后位数更多固然能保证数据的精确性,但绝大多数场景其实并不需要这么精确
只保留一半的信息也不会影响结果,也就是使用 torch.float16 格式
由于数位减了一半,因此被称为“半精度”
显然半精度能够减少显存占用,使得显卡可以同时加载更多数据进行计算

数据增强

关于过拟合问题,可以考虑 加入正则项 或者 减少模型学习参数
而最简单的避免过拟合的方法是 增加数据
但是在许多场景我们无法获得大量数据,例如医学图像分析

数据增强技术解决了 有限数据问题,提高训练数据集的大小和质量
例如 imgaug 库可以对图像数据进行旋转、放大缩小、切割和添加噪音等等

使用 argparse 进行调参

Argparse 是 Python 的一个标准库
可以解析输入的命令行参数,再传入模型的超参数中

使用步骤

创建 ArgumentParser() 对象
调用 add_argument() 方法添加参数
使用 parse_args() 解析参数

 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
# demo.py
import argparse


# 创建 ArgumentParser() 对象
parser = argparse.ArgumentParser()

# 添加参数
parser.add_argument(
    '-o', '--output',
    action='store_true',
    help="shows output"
)
parser.add_argument(
    '--lr',
    type=float,
    default=3e-5,
    help='select the learning rate, default=1e-3'
)
parser.add_argument(
    '--batch_size',
    type=int,
    required=True,
    help='input batch size'
)
# action=store_true 会将 output 参数记录为 True
# type 规定了参数的格式
# default 规定了默认值
# required 规定可选或者必需

# 使用 parse_args() 解析函数
args = parser.parse_args()


if args.output:
    print("This is some output")
    print(f"learning rate:{args.lr} ")

在命令行使用 python demo.py --lr 3e-4 --batch_size 32 会打印出:

1
2
This is some output
learning rate: 3e-4

高效使用

通常情况下,为了使代码更加简洁和模块化
一般会将有关超参数的操作写在 config.py
然后在 train.py 或者其他文件导入

 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
# config.py
import argparse


def get_options(parser=argparse.ArgumentParser()):
    parser.add_argument('--batch_size', type=int, default=4, help='input batch size, default=64')

    parser.add_argument('--niters', type=int, default=10, help='number of epochs to train for, default=10')

    parser.add_argument('--lr', type=float, default=3e-5, help='select the learning rate, default=1e-3')

    parser.add_argument('--seed', type=int, default=118, help="random seed")

    parser.add_argument('--cuda', action='store_true', default=True, help='enables cuda')

    parser.add_argument('--checkpoint_path',type=str,default='',
    help='Path to load a previous trained model if not empty (default empty)')

    opt = parser.parse_args()

    if opt.output:
        print(f'batch_size: {opt.batch_size}')
        print(f'learning rate : {opt.lr}')
        print(f'manual_seed: {opt.seed}')
        print(f'cuda enable: {opt.cuda}')
        print(f'checkpoint_path: {opt.checkpoint_path}')
    return opt

if __name__ == '__main__':
    opt = get_options()
 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
# train.py
import config


opt = config.get_options()

batch_size = opt.batch_size
lr = opt.lr
manual_seed = opt.seed
niters = opt.niters
checkpoint_path = opt.checkpoint_path

# 随机数的设置,保证复现结果
def set_seed(seed):
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
    random.seed(seed)
    np.random.seed(seed)
    torch.backends.cudnn.benchmark = False
    torch.backends.cudnn.deterministic = True


if __name__ == '__main__':
    set_seed(manual_seed)
    for epoch in range(niters):
        ......