Tensor

几何代数中定义的张量是基于向量和矩阵的推广,比如我们可以将标量视为零阶张量,矢量可以视为一阶张量,矩阵就是二阶张量。

在这里插入图片描述


API:

在这里插入图片描述

  • 说白了,torch.tensor就相当于numpy里面的np.Array

索引:

即矩阵索引方式

维度变换

  1. view()共享内存
  2. reshape():新的一块空间,互不影响

广播机制

当对两个形状不同的 Tensor 按元素运算时,可能会触发广播(broadcasting)机制:先适当复制元素使这两个 Tensor 形状相同后再按元素运算

  • 维度不同,元素复制到相同再运算
  • 前提,两个元素是一维的!!!

Autograd

  1. 当完成计算后可以通过调用 .backward(),来自动计算所有的梯度。这个张量的所有梯度将会自动累加到.grad属性

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

  3. Tensor 和 Function 互相连接生成了一个无环图 (acyclic graph),它编码了完整的计算历史。每个张量都有一个.grad_fn属性,该属性引用了创建 Tensor 自身的Function(除非这个张量是用户手动创建的,即这个张量的grad_fn是 None )

x = torch.randn(3, requires_grad=True)

这行代码中的requires_grad表示是否需要记录计算历史,用来反向传播求导

grad在反向传播过程中是累加的(accumulated),这意味着每一次运行反向传播,梯度都会累加之前的梯度,所以一般在反向传播之前需把梯度清零

在这里插入图片描述

  • 不清零
    在这里插入图片描述

requires_grad属性:看你能否自动求导,True才能调用backward求导
grad_fn属性:是否是计算得到的tensor,不能是自己定义的tensor

求导的结果:从原tensor那里获取,x.grad()

如果要改变计算过程中的一步,但是不改变求导的过程
x.data进行操作,提取值而不影响关系

关于计算

参数是什么,调参的目的是什么?

模型参数是模型的参数,模型基本上都是基于某种假设或者某种分布提出的,所以必定有未知参数,一般的方法是极大似然法、EM、HMM等算法估计参数

更常见的参数是损失函数和优化函数中的参数

正则化系数、学习率等

调参的目的是什么?

最小化损失函数,即经验误差最小化

神经网络块

复杂的神经网络是由很多重复的层组组成的,输入-隐藏层(group of layers)-输出

  • 块由类(class)表示。 它的任何子类都必须定义一个将其输入转换为输出的前向传播函数, 并且必须存储任何必需的参数

  • 为了计算梯度,块必须具有反向传播函数。 在定义我们自己的块时,由于自动微分(在 2.5节 中引入) 提供了一些后端实现,我们只需要考虑前向传播函数和必需的参数

通过net(X)调用我们的模型来获得模型的输出。 这实际上是net._ _ call _ _(X)的简写。 这个前向传播函数非常简单: 它将列表中的每个块连接在一起,将每个块的输出作为下一个块的输入

Pytorch架构

torchvision

六大组件:

torchvision.datasets *

torchvision.models *

torchvision.tramsforms *

torchvision.io

torchvision.ops

torchvision.utils

torchvision.transforms

作用:

  1. 数据预处理:计算机视觉中处理的数据集有很大一部分是图片类型的,如果获取的数据是格式或者大小不一的图片,则需要进行归一化和大小缩放等操作
  2. 数据增强:当图片数据有限时,我们还需要通过对现有图片数据进行各种变换,如缩小或放大、水平或垂直翻转

torchvision.models

PyTorch官方也提供了一些预训练好的模型供我们使用

图像分类:

在这里插入图片描述
语义分割、关键点检测、都有

torchvision.io

在torchvision.io提供了视频、图片和文件的 IO 操作的功能,它们包括读取、写入、编解码处理操作。随着torchvision的发展,io也增加了更多底层的高效率的API

图像:read_image
视频:

除了read_video()等方法,torchvision.io为我们提供了一个细粒度的视频API torchvision.io.VideoReader() ,它具有更高的效率并且更加接近底层处理。
在使用时,我们需要先安装ffmpeg然后从源码重新编译torchvision我们才能我们能使用这些方法。

  • 在使用Video相关API时,我们最好提前安装好PyAV这个库

torchvision.opt

  • 实现特定于计算机视觉的运算符、损失和层
  • 用于物体检测和分割操作,只是一部分。。。

torchvision.utils

torchvision.utils 为我们提供了一些可视化的方法,可以帮助我们将若干张图片拼接在一起、可视化检测和分割的效果

必要包:

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

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

import torch
from torchvision import datasets
train_data = datasets.ImageFolder(train_path, transform=data_transform)
val_data = datasets.ImageFolder(val_path, transform=data_transform)

这里使用了PyTorch自带的ImageFolder类的用于读取按一定结构存储的图片数据(path对应图片存放的目录,目录下包含若干子目录,每个子目录对应属于同一个类的图片)。

其中“data_transform”可以对图像进行一定的变换,如翻转、裁剪等操作,可自己定义。

DataSet

主要这三个函数:

  • _ _ init _ _: 用于向类中传入外部参数,同时定义样本集

  • _ _getitem _ _: 用于逐个读取样本集合中的元素,可以进行一定的变换,并将返回训练/验证所需的数据

  • _ _ len_ _: 用于返回数据集的样本数

DataLoader

from torch.utils.data import DataLoader

train_loader = torch.utils.data.DataLoader(train_data, batch_size=batch_size, num_workers=4, shuffle=True, drop_last=True)
val_loader = torch.utils.data.DataLoader(val_data, batch_size=batch_size, num_workers=4, shuffle=False)

  • batch_size:样本是按“批”读入的,batch_size就是每次读入的样本数

  • num_workers:有多少个进程用于读取数据

  • shuffle:是否将读入的数据打乱

  • drop_last:对于样本最后一部分没有达到批次数的样本,使其不再参与训练

Module

Module 类是 nn 模块里提供的一个模型构造类,是所有神经网络模块的基类,我们可以继承它来定义我们想要的模型。下面继承 Module 类构造多层感知机。这里定义的 MLP 类重载了 Module 类的 init 函数和 forward 函数。它们分别用于创建模型参数和定义前向计算。前向计算也即正向传播

模型变量net放入tensor后,net(X)后会调用 MLP 继承⾃自 Module 类的 call 函数,这个函数将调⽤用 MLP 类定义的forward 函数来完成前向计算,反向传播函数不用定义,通过自动微分计算

自定义含模型参数的自定义层。其中的模型参数可以通过训练学出

Parameter 类其实是 Tensor 的子类,如果一 个 Tensor 是 Parameter ,那么它会⾃动被添加到模型的参数列表里。所以在⾃定义含模型参数的层时,我们应该将参数定义成 Parameter ,除了直接定义成 Parameter 类外,还可以使⽤ ParameterList 和 ParameterDict 分别定义参数的列表和字典。

"""
带参数的自定义层
"""
class MyListDense(nn.Module):
    def __init__(self):
        super(MyListDense, self).__init__()
        
        #列表形式
        self.params = nn.ParameterList([nn.Parameter(torch.randn(4, 4)) for i in range(3)])
        self.params.append(nn.Parameter(torch.randn(4, 1)))
        
        """
        字典形式:
        self.params = nn.ParameterDict({
                'linear1': nn.Parameter(torch.randn(4, 4)),
                'linear2': nn.Parameter(torch.randn(4, 1))
        })
        self.params.update({'linear3': nn.Parameter(torch.randn(4, 2))}) # 新增
        
        """
        
        
    def forward(self, x):
        for i in range(len(self.params)):
            x = torch.mm(x, self.params[i])
        return x
    
net = MyListDense()
print(net)

二维卷积


# 卷积运算(二维互相关)
def corr2d(X, K): 
    h, w = K.shape
    X, K = X.float(), K.float()
    #Y是X和K两个矩阵的乘积
    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


"""
填充(padding)是指在输⼊高和宽的两侧填充元素(通常是0元素)。
kernel_size(卷积核):创建一个⾼和宽为3的二维卷积层,然后设输⼊高和宽两侧的填充数分别为1。
in_channel:
stride:我们将每次滑动的行数和列数称为步幅(stride),
在二维互相关运算中,卷积窗口从输入数组的最左上方开始,按从左往右、从上往下 的顺序,依次在输⼊数组上滑动。


"""

# 定义一个函数来计算卷积层。它对输入和输出做相应的升维和降维
def comp_conv2d(conv2d, X):
    # (1, 1)代表批量大小和通道数
    X = X.view((1, 1) + X.shape)
    Y = conv2d(X)
    return Y.view(Y.shape[2:]) # 排除不关心的前两维:批量和通道


# 注意这里是两侧分别填充1⾏或列,所以在两侧一共填充2⾏或列
conv2d = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3,padding=1)

#给定一 个高和宽为8的输入,我们发现输出的高和宽也是8
X = torch.rand(8, 8)
comp_conv2d(conv2d, X).shape

  • 填充可以增加输出的高和宽。这常用来使输出与输入具有相同的高和宽。

  • 步幅可以减小输出的高和宽,例如输出的高和宽仅为输入的高和宽的 ( 为大于1的整数)。

池化层

池化层每次对输入数据的一个固定形状窗口(⼜称池化窗口)中的元素计算输出。

  • 不同于卷积层里计算输⼊和核的互相关性,池化层直接计算池化窗口内元素的最大值或者平均值。该运算也 分别叫做最大池化或平均池化。

在二维最⼤池化中,池化窗口从输入数组的最左上方开始,按从左往右、从上往下的顺序,依次在输⼊数组上滑动。当池化窗口滑动到某⼀位置时,窗口中的输入子数组的最大值即输出数组中相应位置的元素

小结:

我们可以使用torch.nn包来构建神经网络。我们已经介绍了autograd包,nn包则依赖于autograd包来定义模型并对它们求导。一个nn.Module包含各个层和一个forward(input)方法,该方法返回output。

注意:
torch.nn只支持小批量处理 (mini-batches)。整个 torch.nn 包只支持小批量样本的输入,不支持单个样本的输入。比如,nn.Conv2d 接受一个4维的张量,即nSamples x nChannels x Height x Width 如果是一个单独的样本,只需要使用input.unsqueeze(0) 来添加一个“假的”批大小维度。

  • torch.Tensor - 一个多维数组,支持诸如backward()等的自动求导操作,同时也保存了张量的梯度。

  • nn.Module - 神经网络模块。是一种方便封装参数的方式,具有将参数移动到GPU、导出、加载等功能。

  • nn.Parameter - 张量的一种,当它作为一个属性分配给一个Module时,它会被自动注册为一个参数。

  • autograd.Function - 实现了自动求导前向和反向传播的定义,每个Tensor至少创建一个Function节点,该节点连接到创建Tensor的函数并对其历史进行编码。

神经网络搭建过程

一个神经网络的典型训练过程如下:

  1. 定义包含一些可学习参数(或者叫权重)的神经网络

  2. 在输入数据集上迭代

  3. 通过网络处理输入

  4. 计算 loss (输出和正确答案的距离)

  5. 将梯度反向传播给网络的参数

  6. 更新网络的权重,一般使用一个简单的规则:weight = weight - learning_rate * gradient

AlexNet

在这里插入图片描述

class AlexNet(nn.Module):
    def __init__(self):
        super(AlexNet, self).__init__()
        self.conv = nn.Sequential(
            nn.Conv2d(1, 96, 11, 4), # in_channels, out_channels, kernel_size, stride, padding
            nn.ReLU(),
            nn.MaxPool2d(3, 2), # kernel_size, stride
            # 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
            nn.Conv2d(96, 256, 5, 1, 2),
            nn.ReLU(),
            nn.MaxPool2d(3, 2),
            # 连续3个卷积层,且使用更小的卷积窗口。除了最后的卷积层外,进一步增大了输出通道数。
            # 前两个卷积层后不使用池化层来减小输入的高和宽
            nn.Conv2d(256, 384, 3, 1, 1),
            nn.ReLU(),
            nn.Conv2d(384, 384, 3, 1, 1),
            nn.ReLU(),
            nn.Conv2d(384, 256, 3, 1, 1),
            nn.ReLU(),
            nn.MaxPool2d(3, 2)
        )
         # 这里全连接层的输出个数比LeNet中的大数倍。使用丢弃层来缓解过拟合
        self.fc = nn.Sequential(
            nn.Linear(256*5*5, 4096),
            nn.ReLU(),
            nn.Dropout(0.5),
            nn.Linear(4096, 4096),
            nn.ReLU(),
            nn.Dropout(0.5),
            # 输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的1000
            nn.Linear(4096, 10),
        )

    def forward(self, img):
        feature = self.conv(img)
        output = self.fc(feature.view(img.shape[0], -1))
        return output

在这里插入图片描述

权值初始化init

权重的初始值极为重要。一个好的权重值,会使模型收敛速度提高,使模型准确率更精确

  • PyTorch也在torch.nn.init中为我们提供了常用的初始化方法
 1 . torch.nn.init.uniform_(tensor, a=0.0, b=1.0) 
 2 . torch.nn.init.normal_(tensor, mean=0.0, std=1.0) 
 3 . torch.nn.init.constant_(tensor, val) 
 4 . torch.nn.init.ones_(tensor) 
 5 . torch.nn.init.zeros_(tensor)
 6 . torch.nn.init.eye_(tensor) 
 7 . torch.nn.init.dirac_(tensor, groups=1)
 8 . torch.nn.init.xavier_uniform_(tensor, gain=1.0) 
 9 . torch.nn.init.xavier_normal_(tensor, gain=1.0) 
10 . torch.nn.init.kaiming_uniform_(tensor, a=0, mode='fan__in', nonlinearity='leaky_relu') 
11 . torch.nn.init.kaiming_normal_(tensor, a=0, mode='fan_in', nonlinearity='leaky_relu')
12 . torch.nn.init.orthogonal_(tensor, gain=1) 
13 . torch.nn.init.sparse_(tensor, sparsity, std=0.01) 
14 . torch.nn.init.calculate_gain(nonlinearity, param=None)

这些函数除了calculate_gain,所有函数的后缀都带有下划线,意味着这些函数将会直接原地更改输入张量的值

"""
遍历当前模型的每一层,然后判断各层属于什么类型,然后根据不同类型层,设定不同的权值初始化方法
"""
def initialize_weights(self):
    for m in self.modules():
        # 判断是否属于Conv2d
        if isinstance(m, nn.Conv2d):
                torch.nn.init.xavier_normal_(m.weight.data)
                # 判断是否有偏置
                if m.bias is not None:
                    torch.nn.init.constant_(m.bias.data,0.3)
        elif isinstance(m, nn.Linear):
            torch.nn.init.normal_(m.weight.data, 0.1)
            if m.bias is not None:
                torch.nn.init.zeros_(m.bias.data)
        elif isinstance(m, nn.BatchNorm2d):
            m.weight.data.fill_(1)  
            m.bias.data.zeros_()
            
# 模型的定义
class MLP(nn.Module):
  # 声明带有模型参数的层,这里声明了两个全连接层
  def __init__(self, **kwargs):
    # 调用MLP父类Block的构造函数来进行必要的初始化。这样在构造实例时还可以指定其他函数
    super(MLP, self).__init__(**kwargs)
    self.hidden = nn.Conv2d(1,1,3)
    self.act = nn.ReLU()
    self.output = nn.Linear(10,1)
    
   # 定义模型的前向计算,即如何根据输入x计算返回所需要的模型输出
  def forward(self, x):
    o = self.act(self.hidden(x))
    return self.output(o)

mlp = MLP()
print(list(mlp.parameters()))
print("-------初始化-------")

initialize_weights(mlp)
print(list(mlp.parameters()))

自定义初始化参数

栗子,我们使用以下的分布为任意权重参数定义初始化方法:
在这里插入图片描述

def my_init(m):
    if type(m) == nn.Linear:
        print("Init", *[(name, param.shape)
                        for name, param in m.named_parameters()][0])
        nn.init.uniform_(m.weight, -10, 10)
        m.weight.data *= m.weight.data.abs() >= 5

net.apply(my_init)
net[0].weight[:2]

共享参数

有时我们希望在多个层间共享参数: 我们可以定义一个稠密层,然后使用它的参数来设置另一个层的参数:

# 我们需要给共享层一个名称,以便可以引用它的参数
shared = nn.Linear(8, 8)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
                    shared, nn.ReLU(),
                    shared, nn.ReLU(),
                    nn.Linear(8, 1))
net(X)
# 检查参数是否相同
print(net[2].weight.data[0] == net[4].weight.data[0])
net[2].weight.data[0, 0] = 100
# 确保它们实际上是同一个对象,而不只是有相同的值
print(net[2].weight.data[0] == net[4].weight.data[0])

第三个和第五个神经网络层的参数是绑定的。 它们不仅值相等,而且由相同的张量表示。 因此,如果我们改变其中一个参数,另一个参数也会改变

当参数绑定时,梯度会发生什么情况?

答案是由于模型参数包含梯度,因此在反向传播期间第二个隐藏层 (即第三个神经网络层)和第三个隐藏层(即第五个神经网络层)的梯度会加在一起。

好处

共享参数通常可以节省内存,并在以下方面具有特定的好处:

  • 对于图像识别中的CNN,共享参数使网络能够在图像中的任何地方而不是仅在某个区域中查找给定的功能。
  • 对于RNN,它在序列的各个时间步之间共享参数,因此可以很好地推广到不同序列长度的示例。
  • 对于自动编码器,编码器和解码器共享参数。 在具有线性激活的单层自动编码器中,共享权重会在权重矩阵的不同隐藏层之间强制正交

损失函数

二分类交叉熵损失函数:

torch.nn.BCELoss(weight=None, size_average=None, reduce=None, reduction='mean')

功能:计算二分类任务时的交叉熵(Cross Entropy)函数。在二分类中,label是{0,1}。对于进入交叉熵函数的input为概率分布的形式。一般来说,input为sigmoid激活层的输出,或者softmax的输出。

weight:每个类别的loss设置权值

size_average:数据为bool,为True时,返回的loss为平均值;为False时,返回的各样本的loss之和。

reduce:数据类型为bool,为True时,loss的返回是标量。

交叉熵损失函数

torch.nn.CrossEntropyLoss(weight=None, size_average=None, ignore_index=-100, reduce=None, reduction='mean')
功能:计算交叉熵函数

在这里插入图片描述

主要参数:

weight:每个类别的loss设置权值。

size_average:数据为bool,为True时,返回的loss为平均值;为False时,返回的各样本的loss之和。

ignore_index:忽略某个类的损失函数。

reduce:数据类型为bool,为True时,loss的返回是标量

L1损失函数

torch.nn.L1Loss(size_average=None, reduce=None, reduction='mean')

  • 计算输出y和真实标签target之间的差值的绝对值。

我们需要知道的是,reduction参数决定了计算模式。有三种计算模式可选:

  1. none:逐个元素计算。
  2. sum:所有元素求和,返回标量。
  3. mean:加权平均,返回标量。

如果选择none,那么返回的结果是和输入元素相同尺寸的。默认计算方式是求平均

MSE损失函数

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

平滑L1 (Smooth L1)损失函数

torch.nn.SmoothL1Loss(size_average=None, reduce=None, reduction='mean', beta=1.0)

  • L1的平滑输出,其功能是减轻离群点带来的影响
    在这里插入图片描述
    参数同上

目标泊松分布的负对数似然损失

torch.nn.PoissonNLLLoss(log_input=True, full=False, size_average=None, eps=1e-08, reduce=None, reduction='mean')

log_input:输入是否为对数形式,决定计算公式。

full:计算所有 loss,默认为 False。

eps:修正项,避免 input 为 0 时,log(input) 为 nan 的情况。

在这里插入图片描述
在这里插入图片描述

KL散度

torch.nn.KLDivLoss(size_average=None, reduce=None, reduction='mean', log_target=False)

功能: 计算KL散度,也就是计算相对熵。用于连续分布的距离度量,并且对离散采用的连续输出空间分布进行回归通常很有用。

reduction参数:

none:逐个元素计算。

sum:所有元素求和,返回标量。

mean:加权平均,返回标量。

batchmean:batchsize 维度求平均值。

在这里插入图片描述

MarginRankingLoss

torch.nn.MarginRankingLoss(margin=0.0, size_average=None, reduce=None, reduction='mean')

功能: 计算两个向量之间的相似度,用于排序任务。该方法用于计算两组数据之间的差异。

margin:边界值, 与 之间的差异值。

reduction:计算模式,可为 none/sum/mean。

在这里插入图片描述

二分类损失函数

torch.nn.SoftMarginLoss(size_average=None, reduce=None, reduction='mean')torch.nn.(size_average=None, reduce=None, reduction='mean')

在这里插入图片描述

多分类的折页损失

torch.nn.MultiMarginLoss(p=1, margin=1.0, weight=None, size_average=None, reduce=None, reduction='mean')

reduction:计算模式,可为 none/sum/mean。

p:可选 1 或 2。

weight:各类别的 loss 设置权值。

margin:边界值

余弦相似度

torch.nn.CosineEmbeddingLoss(margin=0.0, size_average=None, reduce=None, reduction=‘mean’)

reduction:计算模式,可为 none/sum/mean。

margin:可取值[-1,1] ,推荐为[0,0.5] 。

在这里插入图片描述

  • 对于两个向量,做余弦相似度。将余弦相似度作为一个距离的计算方式,如果两个向量的距离近,则损失函数值小,反之亦然

CTC损失函数

torch.nn.CTCLoss(blank=0, reduction='mean', zero_infinity=False)

  • 功能: 用于解决时序类数据的分类

计算连续时间序列和目标序列之间的损失。CTCLoss对输入和目标的可能排列的概率进行求和,产生一个损失值,这个损失值对每个输入节点来说是可分的。输入与目标的对齐方式被假定为 “多对一”,这就限制了目标序列的长度,使其必须是≤输入长度

reduction:计算模式,可为 none/sum/mean。

blank:blank label。

zero_infinity:无穷大的值或梯度值为

自定义损失函数

以函数方式定义

事实上,损失函数仅仅是一个函数而已,因此我们可以通过直接以函数定义的方式定义一个自己的函数,如下所示:

def my_loss(output, target):
    loss = torch.mean((output - target)**2)
    return loss

    以类的方式实现

    Dice Loss是一种在分割领域常见的损失函数,定义如下:
    在这里插入图片描述

    • 类实现
    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提供的张量计算接口,这样就不需要我们实现自动求导功能并且我们可以直接调用cuda,使用numpy或者scipy的数学运算时,操作会有些麻烦

    训练和评估

    训练过程

    1. 我们前面在DataLoader构建完成后介绍了如何从中读取数据,在训练过程中使用类似的操作即可,区别在于此时要用for循环读取DataLoader中的全部数据。
      for data, label in train_loader:

    2. 之后将数据放到GPU上用于后续计算,此处以.cuda()为例
      data, label = data.cuda(), label.cuda()

    3. 开始用当前批次数据做训练时,应当先将优化器的梯度置零:
      optimizer.zero_grad()

    4. 之后将data送入模型中训练:
      output = model(data)

    5. 根据预先定义的criterion计算损失函数:
      loss = criterion(output, label)

    6. 将loss反向传播回网络:
      loss.backward()

    7. 使用优化器更新模型参数:
      optimizer.step()

    def train(epoch):
        model.train() # 开启训练
        train_loss = 0
        for data, label in train_loader: # 批次遍历训练集中的数据
            data, label = data.cuda(), label.cuda() # 从gpu中读取数据
            optimizer.zero_grad() #优化器梯度清零
            output = model(data) # 数据放入模型,得到输出
            loss = criterion(label, output) # 计算损失函数
            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. 需要预先设置torch.no_grad,以及将model调至eval模式

    2. 不需要将优化器的梯度置零

    3. 不需要将loss反向回传到网络

    4. 不需要更新optimizer

    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)#计算accuracy
                
        val_loss = val_loss/len(val_loader.dataset)
        print('Epoch: {} \tTraining Loss: {:.6f}'.format(epoch, val_loss))

    优化器

    十种优化器:

    1. torch.optim.ASGD

    2. torch.optim.Adadelta

    3. torch.optim.Adagrad

    4. torch.optim.Adam

    5. torch.optim.AdamW

    6. torch.optim.Adamax

    7. torch.optim.LBFGS

    8. torch.optim.RMSprop

    9. torch.optim.Rprop

    10. torch.optim.SGD

    11. torch.optim.SparseAdam

    两种方式写参数:

    1. defaults(字典):
      {‘lr’: 0.1, ‘momentum’: 0.9, ‘dampening’: 0, ‘weight_decay’: 0, ‘nesterov’: False}
    2. param_groups:管理的参数组,是一个list,其中每个元素是一个字典,顺序是params,lr,momentum,dampening,weight_decay,nesterov,例子如下:

    [{'params': [tensor([[-0.1022, -1.6890],[-1.5116, -1.7846]], requires_grad=True)], 'lr': 1, ...}]

    • add_param_group():添加参数组
    • load_state_dict() :加载状态参数字典,可以用来进行模型的断点续训练,继续上次的参数进行训练

    动态学习率

    • torch.optim.lr_scheduler为我们封装好了一些动态调整学习率的方法供我们使用,如下面列出的这些scheduler。

    lr_scheduler.LambdaLR

    lr_scheduler.MultiplicativeLR

    lr_scheduler.StepLR

    lr_scheduler.MultiStepLR

    lr_scheduler.ExponentialLR

    lr_scheduler.CosineAnnealingLR

    lr_scheduler.ReduceLROnPlateau

    lr_scheduler.CyclicLR

    lr_scheduler.OneCycleLR

    lr_scheduler.CosineAnnealingWarmRestarts

    # 选择一种优化器
    optimizer = torch.optim.Adam(...) 
    # 选择上面提到的一种或多种动态调整学习率的方法
    scheduler1 = torch.optim.lr_scheduler.... 
    scheduler2 = torch.optim.lr_scheduler....
    ...
    schedulern = torch.optim.lr_scheduler....
    # 进行训练
    for epoch in range(100):
        train(...)
        validate(...)
        optimizer.step()
        # 需要在优化器参数更新之后再动态调整学习率
    	scheduler1.step() 
    	...
        schedulern.step()

    自定义动态学习率

    • 方法是自定义函数adjust_learning_rate来改变param_group中lr的值
    def adjust_learning_rate(optimizer, epoch):
        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(...)
       

    模型微调

    简单来说,就是我们先找到一个同类的别人训练好的模型,把别人现成的训练好了的模型拿过来,换成自己的数据,通过训练调整一下参数

    流程:

    1. 在源数据集(如ImageNet数据集)上预训练一个神经网络模型,即源模型。

    2. 创建一个新的神经网络模型,即目标模型。它复制了源模型上除了输出层外的所有模型设计及其参数。我们假设这些模型参数包含了源数据集上学习到的知识,且这些知识同样适用于目标数据集。我们还假设源模型的输出层跟源数据集的标签紧密相关,因此在目标模型中不予采用。

    3. 为目标模型添加一个输出⼤小为⽬标数据集类别个数的输出层,并随机初始化该层的模型参数。

    4. 在目标数据集上训练目标模型。我们将从头训练输出层,而其余层的参数都是基于源模型的参数微调得到的。

    在这里插入图片描述

    • 传递pretrained参数

    当pretrained = True,意味着我们将使用在一些数据集上预训练得到的权重

    训练特定层

    在默认情况下,参数的属性.requires_grad = True,如果我们从头开始训练或微调不需要注意这里。但如果我们正在提取特征并且只想为新初始化的层计算梯度,其他参数不进行改变。那我们就需要通过设置requires_grad = False来冻结部分层

    官方API:

    def set_parameter_requires_grad(model, feature_extracting):
        if feature_extracting:
            for param in model.parameters():
                param.requires_grad = False

    举个栗子: 在下面我们仍旧使用resnet18为例的将1000类改为4类,但是仅改变最后一层的模型参数,不改变特征提取的模型参数;

    1. 先冻结模型参数的梯度
    2. 再对模型输出部分的全连接层进行修改,这样修改后的全连接层的参数就是可计算梯度的。
    import torchvision.models as models
    # 冻结参数的梯度
    feature_extract = True
    model = models.resnet18(pretrained=True)#获取模型与训练的权重
    set_parameter_requires_grad(model, feature_extract)
    # 修改模型
    num_ftrs = model.fc.in_features
    model.fc = nn.Linear(in_features=num_ftrs, out_features=4, bias=True)

    • 之后在训练过程中,model仍会进行梯度回传,但是参数更新则只会发生在fc层

    减小内存使用

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

    • PyTorch默认的浮点数存储方式用的是torch.float32,其实大多数情况下float16就足够

    在PyTorch中使用autocast配置半精度训练,同时需要在下面三处加以设置:

    1. 导包
    from torch.cuda.amp import autocast

    1. 使用装饰器,包装forward函数
    @autocast()   
    def forward(self, x):
        ...
        return x

    1. 训练过程
     for x in train_loader:
    	x = x.cuda()
    	with autocast():
            output = model(x)
            ...

    什么时候用?

    半精度训练主要适用于数据本身的size比较大(比如说3D图像、视频等)

    自定义模型

    基于nn.Module,我们可以通过Sequential,ModuleList和ModuleDict三种方式定义PyTorch模型。

    Sequential

    当模型的前向计算为简单串联各个层的计算时, Sequential 类可以通过更加简单的方式定义模型。它可以接收一个子模块的有序字典(OrderedDict) 或者一系列子模块作为参数来逐一添加 Module 的实例,⽽模型的前向计算就是将这些实例按添加的顺序逐⼀计算

    只需要将模型的层按序排列起来即可,根据层名的不同,排列的时候有两种方式:

    1. 直接排列
    net = nn.Sequential(
            nn.Linear(784, 256),
            nn.ReLU(),
            nn.Linear(256, 10), 
            )

    1. 使用OrderedDict:
    import collections
    net2 = nn.Sequential(collections.OrderedDict([
              ('fc1', nn.Linear(784, 256)),
              ('relu1', nn.ReLU()),
              ('fc2', nn.Linear(256, 10))
              ]))
    
    
    • 使用Sequential定义模型的好处在于简单、易读,同时使用Sequential定义的模型不需要再写forward,因为顺序已经定义好了。
    • 但使用Sequential也会使得模型定义丧失灵活性,比如需要在模型中间加入一个外部输入时就不适合用Sequential的方式实现

    ModuleDict

    对应模块为nn.ModuleDict()。

    • ModuleDict能够更方便地为神经网络的层添加名称
    net = nn.ModuleDict({
        'linear': nn.Linear(784, 256),
        'act': nn.ReLU(),
    })
    net['output'] = nn.Linear(256, 10) # 添加
    print(net['linear']) # 访问
    print(net.output)
    print(net)

    argparse调参

    argsparse是python的命令行解析的标准模块,内置于python,不需要安装。这个库可以让我们直接在命令行中就可以向程序中传入参数

    argparse的参数主要可以分为可选参数和必选参数。

    • 可选参数就跟我们的lr参数相类似,未输入的情况下会设置为默认值。

    • 必选参数就跟我们的batch_size参数相类似

    • 当我们给参数设置required =True后,我们就必须传入该参数,否则就会报错

    • 位置参数 ,严格按位置传参数

    parser = argparse.ArgumentParser()
    
    parser.add_argument('name')
    parser.add_argument('age')
    
    args = parser.parse_args()
    
    print(f'{args.name} is {args.age} years old')
    
    

    更方便的一种方式是单独写成config.py的文件,train的时候导入就行了

    # config.py
    
    import argparse  
      
    def get_options(parser=argparse.ArgumentParser()):  
      
        parser.add_argument('--workers', type=int, default=0,  
                            help='number of data loading workers, you had better put it '  
                                  '4 times of your gpu')  
      
        parser.add_argument('--batch_size', type=int, default=4, help='input batch size, default=64')  
      
        parser.add_argument('--niter', 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)')  
        parser.add_argument('--output',action='store_true',default=True,help="shows output")

    保存模型

    保存:

     
    # state_dict : 模型的参数 ,第二个参数是保存文件的名字
    torch.save({'state_dict': model.state_dict()}, 'checkpoint.pth.tar')

    加载:

    # model = Net()
    checkpoint = torch.load('checkpoint.pth.tar')
    model.load_state_dict(checkpoint['state_dict'])


    可视化

    可视化模型参数

    1. 想直接看见自己定义的网络结构,直接print(net)
    2. 更详细的看每一层的输入输出及参数, 用torchinfo.summary()

    方式一:print(net):

    当通过Sequential类定义模型时, 我们可以通过索引来访问模型的任意层。 这就像模型是一个列表一样,每层的参数都在其属性中。 如下所示,我们可以检查第二个全连接层的参数:print(net[2].state_dict())

    net[layer]有两个基本属性:1.weight 2.bias .data访问数据

    named_parameters()访问所有

    另一种方式:net.state_dict()['2.bias'].data


    import torchvision.models as models
    from torchinfo import summary
    resnet18 = models.resnet18() # 实例化模型
    summary(resnet18, (1, 3, 224, 224)) # 1:batch_size 3:图片的通道数 224: 图片的高宽

    Summary API:

    summary(
        model:torch.nn.modules.module.Module,
        input_size:Union[Sequence[Union[int, Sequence[Any], torch.Size]], NoneType]=None,
        input_data:Union[torch.Tensor, Sequence[Any], Mapping[str, Any], NoneType]=None,
        batch_dim:Union[int, NoneType]=None,

    可视化梯度

    #可视化梯度
    import matplotlib.pyplot as plt
    import torchvision.models as models
    from flashtorch.utils import apply_transforms, load_image
    from flashtorch.saliency import Backprop
    
    model = models.alexnet(pretrained=True)
    backprop = Backprop(model)
    
    image = load_image('./images/great_grey_owl.jpg')
    owl = apply_transforms(image)
    
    target_class = 24
    backprop.visualize(owl, target_class, guided=True, use_gpu=True)

    在这里插入图片描述

    TensorBorad

    我们可以将TensorBoard看做一个记录员,它可以记录我们指定的数据,包括模型每一层的feature map,权重,以及训练loss等等。TensorBoard将记录下来的内容保存在一个用户指定的文件夹里,程序不断运行中TensorBoard会不断记录。记录下的内容可以通过网页的形式加以可视化

    • 设置:
    from tensorboardX import SummaryWriter
    writer = SummaryWriter('./runs')
    • 启动:tensorboard --logdir=./runs
    • 都是给定一个输入数据,前向传播后得到模型的结构,再通过TensorBoard进行可视化,使用add_graph:
    notebook.list()
    !kill pid 
    %reload_ext tensorboard
    %tensorboard --logdir './pytorch_tb'


    U-Net

    在这里插入图片描述
    组成U-Net的模型块主要有如下几个部分:

    1)每个子块内部的两次卷积(Double Convolution)

    2)左侧模型块之间的下采样连接,即最大池化(Max pooling)

    3)右侧模型块之间的上采样连接(Up sampling)

    4)输出层的处理

    添加外部输入

    有时候在模型训练中,除了已有模型的输入之外,还需要输入额外的信息。比如在CNN网络中,我们除了输入图像,还需要同时输入图像对应的其他信息,这时候就需要在已有的CNN网络中添加额外的输入变量。基本思路是:将原模型添加输入位置前的部分作为一个整体,同时在forward中定义好原模型不变的部分、添加的输入和后续层之间的连接关系,从而完成模型的修改。

    class Model(nn.Module):
        def __init__(self, net):
            super(Model, self).__init__()
            self.net = net
            self.relu = nn.ReLU()
            self.dropout = nn.Dropout(0.5)
            #定义添加的连接层
            self.fc_add = nn.Linear(1001, 10, bias=True)
            self.output = nn.Softmax(dim=1)
            
        def forward(self, x, add_variable):
            x = self.net(x)
            #添加变量,变换维度,连接tensor
            x = torch.cat((self.dropout(self.relu(x)), add_variable.unsqueeze(1)),1)
            x = self.fc_add(x)
            
            x = self.output(x)
            return x
    • 另外别忘了,训练中在输入数据的时候要给两个inputs:
      outputs = model(inputs, add_var)

    额外输出

    有时候在模型训练中,除了模型最后的输出外,我们需要输出模型某一中间层的结果,以施加额外的监督,获得更好的中间层结果。基本的思路是修改模型定义中forward函数的return变量

    class Model(nn.Module):
        def __init__(self, net):
            super(Model, self).__init__()
            self.net = net
            self.relu = nn.ReLU()
            self.dropout = nn.Dropout(0.5)
            self.fc1 = nn.Linear(1000, 10, bias=True)
            self.output = nn.Softmax(dim=1)
            
        def forward(self, x, add_variable):
        	#倒数第二层
            x1000 = self.net(x)
            #最后一层
            x10 = self.dropout(self.relu(x1000))
            x10 = self.fc1(x10)
            x10 = self.output(x10)
            return x10, x1000

    • out10, out1000 = model(inputs, add_var)

    数据增强-ImgAug

    深度学习最重要的是数据。我们需要大量数据才能避免模型的过度拟合。但是我们在许多场景无法获得大量数据,例如医学图像分析

    在计算视觉领域,生成增强图像相对容易。即使引入噪声或裁剪图像的一部分,模型仍可以对图像进行分类

    • imgaug是计算机视觉任务中常用的一个数据增强的包,相比于torchvision.transforms,它提供了更多的数据增强方法

    官方imgaug教程