PyTorch:主要组成模块

0. 引言

一个典型的机器学习工作流程如下图所示:

深度学习是机器学习的子集,工作流程是类似的,但在代码实现上有较大的差异:

  • 深度学习所需的样本量很大,一次加载全部数据运行可能会超出内存容量而无法实现 → 使用批(Batch)处理每次载入固定数量的样本送入模型,这种方式还能提高模型的性能。
  • 深度神经网络层数往往较多,同时会有一些用于实现特定功能的层,模型结构多样化 → “逐层”搭建,或者预先定义好可以实现特定功能的模块,再把这些模块组装起来。这种“定制化”方式对代码实现提出了新的要求。
  • 由于模型设定的灵活性,因此损失函数和优化器要能够保证反向传播能够在用户自行定义的模型结构上实现。
  • 在代码实现中,需要把模型和数据 “放到” GPU上去做运算,同时还需要保证损失函数和优化器能够在GPU上工作。

下面我们将进一步了解一下PyTorch是如何实现各个部分的,以及PyTorch作为一个深度学习框架拥有的模块化特点。

1. 基本配置

1.1 导入必须的包

下面是比较常见的导入方式:

import os  # 关于系统操作的库,主要用来对文件路径操作
import numpy as np # 支持大量的维度数组与矩阵运算,此外也针对数组运算提供大量的数学函数库
import torch # 包含了多维张量的数据结构以及基于其上的多种数学操作,也可以有效地对张量和任意类型进行序列化
import torch.nn as nn # 包含很多种神经网络的层,比如卷积层、池化层、循环层等,是“定制化”的基本模块
from torch.utils.data import Dataset, DataLoader # 数据集定义(提供一种方式,获取数据和label)和数据载入(分批载入数据去训练和测试)
import torch.optim as optimizer # 模型参数估计所使用的优化器,包括Adam、Adagrad等

也有其他可能需要导入的包:pandas(表格的读入)、matplotlib(可视化)、sklearn(下游分析和指标计算)

1.2 超参数设置

batch_size = 16 
lr = 1e-4
max_epochs = 100
# 若有一些其他模块或用户自定义模块会用到的参数,也可以在一开始进行设置

# GPU 设置
# 方案一:使用os.environ,后续如果使用 GPU 不需要特别设置
os.environ['CUDA_VISIBLE_DEVICES'] = '0,1'

# 方案二:使用“device”,后续对要使用 GPU 的变量用.to(device)即可
device = torch.device("cuda:1" if torch.cuda.is_available() else "cpu")

2. 数据读入

PyTorch数据读入是通过 Dataset + DataLoader 的方式完成的。

2.1 Dataset

我们需要定义自己的 Dataset 类来实现灵活的数据读取(很灵活,具体要根据原始数据的形式而定,但最终要转换为后续模型支持的格式,可以理解为机器学习任务中的预处理)。

class CustomDataset(data.Dataset): # 需要继承data.Dataset
    def __init__(self):  # 用于向类中传入外部参数,同时定义样本集
        # TODO
        # 1. Initialize file path or list of file names.
        pass
    def __getitem__(self, index):  # 用于逐个读取样本集合中的元素,可以进行一定的变换,并将返回训练/验证所需的数据
        # TODO
        # 1. Read one data from file (e.g. using numpy.fromfile, PIL.Image.open).
        # 2. Preprocess the data (e.g. torchvision.Transform).
        # 3. Return a data pair (e.g. image and label).
        #这里需要注意的是,第一步:read one data,是一个data
        pass
    def __len__(self): # 用于返回数据集的样本数
        # You should change 0 to the total size of your dataset.
        return 0

举个例子,当使用 Customdataset[idx] 命令时,可以在你的硬盘中读取你的数据集中第 idx 张图片以及其标签(如果有的话); len(dataset)则会返回这个数据集的容量。

下面我们看一下官方MNIST数据集的例子:

class MNIST(data.Dataset):
    """`MNIST <http://yann.lecun.com/exdb/mnist/>`_ Dataset.
    Args:
        root (string): Root directory of dataset where ``processed/training.pt``
            and  ``processed/test.pt`` exist.
        train (bool, optional): If True, creates dataset from ``training.pt``,
            otherwise from ``test.pt``.
        download (bool, optional): If true, downloads the dataset from the internet and
            puts it in root directory. If dataset is already downloaded, it is not
            downloaded again.
        transform (callable, optional): A function/transform that  takes in an PIL image
            and returns a transformed version. E.g, ``transforms.RandomCrop``
        target_transform (callable, optional): A function/transform that takes in the
            target and transforms it.
    """
    urls = [
        'http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz',
        'http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz',
        'http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz',
        'http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz',
    ]
    raw_folder = 'raw'
    processed_folder = 'processed'
    training_file = 'training.pt'
    test_file = 'test.pt'
    classes = ['0 - zero', '1 - one', '2 - two', '3 - three', '4 - four',
               '5 - five', '6 - six', '7 - seven', '8 - eight', '9 - nine']
    class_to_idx = {_class: i for i, _class in enumerate(classes)}

    @property
    def targets(self):
        if self.train:
            return self.train_labels
        else:
            return self.test_labels

    def __init__(self, root, train=True, transform=None, target_transform=None, download=False):
        self.root = os.path.expanduser(root)
        self.transform = transform
        self.target_transform = target_transform
        self.train = train  # training set or test set

        if download:
            self.download()

        if not self._check_exists():
            raise RuntimeError('Dataset not found.' +
                               ' You can use download=True to download it')

        if self.train:
            self.train_data, self.train_labels = torch.load(
                os.path.join(self.root, self.processed_folder, self.training_file))
        else:
            self.test_data, self.test_labels = torch.load(
                os.path.join(self.root, self.processed_folder, self.test_file))

    def __getitem__(self, index):
        """
        Args:
            index (int): Index
        Returns:
            tuple: (image, target) where target is index of the target class.
        """
        if self.train:
            img, target = self.train_data[index], self.train_labels[index]
        else:
            img, target = self.test_data[index], self.test_labels[index]

        # doing this so that it is consistent with all other datasets
        # to return a PIL Image
        img = Image.fromarray(img.numpy(), mode='L')

        if self.transform is not None:
            img = self.transform(img)

        if self.target_transform is not None:
            target = self.target_transform(target)

        return img, target

    def __len__(self):
        if self.train:
            return len(self.train_data)
        else:
            return len(self.test_data)

    def _check_exists(self):
        return os.path.exists(os.path.join(self.root, self.processed_folder, self.training_file)) and \
            os.path.exists(os.path.join(self.root, self.processed_folder, self.test_file))

    def download(self):
        """Download the MNIST data if it doesn't exist in processed_folder already."""
        from six.moves import urllib
        import gzip

        if self._check_exists():
            return

        # download files
        try:
            os.makedirs(os.path.join(self.root, self.raw_folder))
            os.makedirs(os.path.join(self.root, self.processed_folder))
        except OSError as e:
            if e.errno == errno.EEXIST:
                pass
            else:
                raise

        for url in self.urls:
            print('Downloading ' + url)
            data = urllib.request.urlopen(url)
            filename = url.rpartition('/')[2]
            file_path = os.path.join(self.root, self.raw_folder, filename)
            with open(file_path, 'wb') as f:
                f.write(data.read())
            with open(file_path.replace('.gz', ''), 'wb') as out_f, \
                    gzip.GzipFile(file_path) as zip_f:
                out_f.write(zip_f.read())
            os.unlink(file_path)

        # process and save as torch files
        print('Processing...')

        training_set = (
            read_image_file(os.path.join(self.root, self.raw_folder, 'train-images-idx3-ubyte')),
            read_label_file(os.path.join(self.root, self.raw_folder, 'train-labels-idx1-ubyte'))
        )
        test_set = (
            read_image_file(os.path.join(self.root, self.raw_folder, 't10k-images-idx3-ubyte')),
            read_label_file(os.path.join(self.root, self.raw_folder, 't10k-labels-idx1-ubyte'))
        )
        with open(os.path.join(self.root, self.processed_folder, self.training_file), 'wb') as f:
            torch.save(training_set, f)
        with open(os.path.join(self.root, self.processed_folder, self.test_file), 'wb') as f:
            torch.save(test_set, f)

        print('Done!')

    def __repr__(self):
        fmt_str = 'Dataset ' + self.__class__.__name__ + '\n'
        fmt_str += '    Number of datapoints: {}\n'.format(self.__len__())
        tmp = 'train' if self.train is True else 'test'
        fmt_str += '    Split: {}\n'.format(tmp)
        fmt_str += '    Root Location: {}\n'.format(self.root)
        tmp = '    Transforms (if any): '
        fmt_str += '{0}{1}\n'.format(tmp, self.transform.__repr__().replace('\n', '\n' + ' ' * len(tmp)))
        tmp = '    Target Transforms (if any): '
        fmt_str += '{0}{1}'.format(tmp, self.target_transform.__repr__().replace('\n', '\n' + ' ' * len(tmp)))
        return fmt_str

训练数据和验证数据等要分别使用 Dataset 来获取。

# CustomData 是我们自己定义的 Dataset 类,下面只是读取的一种方式。根据 Dataset 类定义的不同,所设置的超参数的数量和类型都会有所不同。
train_data = MyData(train_path, train_label)
val_data = MyData(val_path, val_label)

2.2 DataLoader

DataLoader 用 iterative 的方式不断读入批次数据(不用重定义,调用并设置超参数即可)。

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:对于样本最后一部分没有达到批次数的样本,使其不再参与训练

PyTorch中的 DataLoader 的读取可以使用 next 和 iter 来完成

3. 模型构建

3.1 神经网络的构造

PyTorch中神经网络构造一般是基于 Module 类的模型来完成的,它让模型构造更加灵活。

Module 类是 nn 模块里提供的一个模型构造类,是所有神经⽹网络模块的基类,我们可以继承它来定义我们想要的模型(层和模型的一部分等)。例如:

import torch
from torch import nn

class MLP(nn.Module):
  # 声明带有模型参数的层,这里声明了两个全连接层
  def __init__(self, **kwargs):
    # 调用MLP父类的构造函数来进行必要的初始化
    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 类得到模型变量 net 。下⾯的代码初始化 net 并传入输⼊数据 X 做一次前向计算。其中, net(X) 会调用 MLP 继承⾃自 Module 类的 call 函数,这个函数将调⽤用 MLP 类定义的forward 函数来完成前向计算。

X = torch.rand(2,784)
net = MLP() # 会随机初始化权重等参数,所以这几行代码每次运行时,结果都会不同
print(net)
net(X)

# MLP(
#   (hidden): Linear(in_features=784, out_features=256, bias=True)
#   (act): ReLU()
#   (output): Linear(in_features=256, out_features=10, bias=True)
# )
# tensor([[ 0.0149, -0.2641, -0.0040,  0.0945, -0.1277, -0.0092,  0.0343,  0.0627,
#          -0.1742,  0.1866],
#         [ 0.0738, -0.1409,  0.0790,  0.0597, -0.1572,  0.0479, -0.0519,  0.0211,
#          -0.1435,  0.1958]], grad_fn=<AddmmBackward>)

3.2 神经网络中常见的层

深度学习的一个魅力在于神经网络中各式各样的层,例如全连接层、卷积层、池化层与循环层等等。虽然PyTorch提供了⼤量常用的层,但有时候我们依然希望⾃定义层。这里我们会介绍如何使用 Module 来自定义层,从而可以被反复调用。

3.2.1 不含模型参数的层

下⾯构造的 MyLayer 类通过继承 Module 类自定义了一个将输入减掉均值后输出的层,并将层的计算定义在了 forward 函数里。

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()

layer = MyLayer()
layer(torch.tensor([1, 2, 3, 4, 5], dtype=torch.float))
# tensor([-2., -1.,  0.,  1.,  2.])

3.2.2 含模型参数的层

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)))

    def forward(self, x):
        for i in range(len(self.params)):
            x = torch.mm(x, self.params[i])  # torch.mm 矩阵乘法 (n,m) * (m,p) → (n,p)
        return x
net = MyListDense()
print(net)

X = torch.rand(10, 4)
net = MyListDense()
net(X).shape  # torch.Size([10, 1])
class MyDictDense(nn.Module):
    def __init__(self):
        super(MyDictDense, self).__init__()
        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, choice='linear1'):
        return torch.mm(x, self.params[choice])

net = MyDictDense()
print(net)

X = torch.rand(10, 4)
net = MyDictDense()
net(X).shape  # torch.Size([10, 4])
net(X, choice = "linear2").shape  # torch.Size([10, 1])
net(X, choice = "linear3").shape  # torch.Size([10, 2])

3.2.3 二维卷积层

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

import torch
from torch import nn

# 卷积运算(二维互相关)
def corr2d(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))  # 此时默认步长为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

# 二维卷积层(自己设计的,非Pytorch中内嵌的 nn.Conv2D)
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

Pytorch内置的 nn.Conv2D 如下所示:

CLASStorch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True, padding_mode='zeros', device=None, dtype=None)
# in_channels (int) – Number of channels in the input image
# out_channels (int) – Number of channels produced by the convolution
# kernel_size (int or tuple) – Size of the convolving kernel
# stride (int or tuple, optional) – Stride of the convolution. Default: 1
# padding (int, tuple or str, optional) – Padding added to all four sides of the input. Default: 0

# example
input = torch.randn(20, 16, 50, 100) # batch_size, channel_num, height, width

# With square kernels and equal stride
m = nn.Conv2d(16, 33, 3, stride=2)
m(input).shape # torch.Size([20, 33, 24, 49])

# non-square kernels and unequal stride and with padding
m = nn.Conv2d(16, 33, (3, 5), stride=(2, 1), padding=(4, 2))
m(input).shape # torch.Size([20, 33, 28, 100])

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

3.2.4 池化层

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

下面把池化层的前向计算实现在pool2d函数里:

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))  # 步长此时为 1,可以调整,一般等于 pool_size
    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))
#tensor([[4., 5.],
#	[7., 8.]])

3.3 模型示例

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

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)) # torch.Size([1, 16 * 5 * 5])
        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)
# Net(
#   (conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
#   (conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
#   (fc1): Linear(in_features=400, out_features=120, bias=True)
#   (fc2): Linear(in_features=120, out_features=84, bias=True)
#   (fc3): Linear(in_features=84, out_features=10, bias=True)
# )

我们只需要定义 forward 函数,backward函数会在使用autograd时自动定义,backward函数用来计算导数。我们可以在 forward 函数中使用任何针对张量的操作和计算。

一个模型的可学习参数可以通过net.parameters()返回

params = list(net.parameters())
print(len(params))  # 10
print(params[0].size())  # conv1的权重 torch.Size([6, 1, 5, 5])

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

4. 模型初始化

在深度学习模型的训练中,权重的初始值极为重要。一个好的权重值,会使模型收敛速度提高,使模型准确率更精确。为了利于训练和减少收敛时间,我们需要对模型进行合理的初始化。PyTorch也在torch.nn.init中为我们提供了常用的初始化方法。

4.1 常见的初始化函数

很多与张量创建的一些初始化函数一致,比如uniform、normal等

  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)

4.2 初始化函数的使用

我们通常使用 isinstance 来进行判断模块属于什么类型,然后选择对应的初始化函数。

import torch
import torch.nn as nn

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

isinstance(conv,nn.Conv2d)  # TRUE
isinstance(linear,nn.Conv2d)  # FALSE

对于不同的类型层,我们就可以设置不同的权值初始化的方法。

# 对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
tensor([[[[ 0.3249, -0.0500,  0.6703],
          [-0.3561,  0.0946,  0.4380],
          [-0.9426,  0.9116,  0.4374]]],
        [[[ 0.6727,  0.9885,  0.1635],
          [ 0.7218, -1.2841, -0.2970],
          [-0.9128, -0.1134, -0.3846]]],
        [[[ 0.2018,  0.4668, -0.0937],
          [-0.2701, -0.3073,  0.6686],
          [-0.3269, -0.0094,  0.3246]]]])
tensor([[0.3000, 0.3000, 0.3000, 0.3000, 0.3000, 0.3000, 0.3000, 0.3000, 0.3000,0.3000]])

人们常常将各种初始化方法定义为一个initialize_weights()的函数并在模型初始后进行使用。

# 遍历当前模型的每一层,然后判断各层属于什么类型,再根据不同类型层,设定不同的权值初始化方法。
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_()	

5. 损失函数

一个模型想要达到很好的效果需要学习,也就是我们常说的训练。一个好的训练离不开优质的负反馈,这里的损失函数就是模型的负反馈。

下面是 PyTorch 中常用的损失函数(一般通过torch.nn调用)。当然,PyTorch的损失函数还远不止这些,在解决实际问题的过程中需要进一步探索、借鉴现有工作,或者设计自己的损失函数。

6. 训练和评估

首先应该设置模型的状态:如果是训练状态,那么模型的参数应该支持反向传播的修改;如果是验证/测试状态,则不应该修改模型参数。在PyTorch中,模型的状态设置非常简便,如下的两个操作二选一即可:

model.train()   # 训练状态
model.eval()   # 验证/测试状态

训练过程如下所示:

for data, label in train_loader:
  # 将数据放到GPU上用于后续计算,此处以.cuda()为例
  data, label = data.cuda(), label.cuda()
  
  # 开始用当前批次数据做训练时,应当先将优化器的梯度置零
  optimizer.zero_grad()

  # 将data送入模型中训练
  output = model(data)

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

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

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

验证/测试的流程基本与训练过程一致,不同点在于:

  • 需要预先设置torch.no_grad,以及将model调至eval模式
  • 不需要将优化器的梯度置零
  • 不需要将loss反向回传到网络
  • 不需要更新optimizer

一个完整的图像分类的训练过程如下所示:

def train(epoch):
    model.train()
    train_loss = 0
    for data, label in train_loader:
        data, label = data.cuda(), label.cuda()
        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))

对应的,一个完整图像分类的验证过程如下所示:

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))

7. 可视化

在PyTorch深度学习中,可视化是一个可选项,指的是某些任务在训练完成后,需要对一些必要的内容进行可视化,比如分类的ROC曲线,卷积网络中的卷积核,以及训练/验证过程的损失函数曲线等等。

8. 优化器

深度学习的目标是通过不断改变网络参数,使得参数能够对输入做各种非线性变换拟合输出,本质上就是一个函数去寻找最优解,只不过这个最优解是一个矩阵,而如何快速求得这个最优解是深度学习研究的一个重点,以经典的resnet-50为例,它大约有2000万个系数需要进行计算,那么我们如何计算出这么多系数,有以下两种方法:

  • 第一种是直接暴力穷举一遍参数,这种方法实施可能性基本为0,堪比愚公移山plus的难度。
  • 为了使求解参数过程更快,人们提出了第二种办法,即BP+优化器逼近求解。

因此,优化器是根据网络反向传播的梯度信息来更新网络的参数,以起到降低loss函数计算值,使得模型输出更加接近真实标签。

Pytorch很人性化的给我们提供了一个优化器的库torch.optim,在这里面提供了一些常用的优化器。

通过一个实例来了解 optimizer 的使用方法:

import os
import torch

# 设置权重,服从正态分布  --> 2 x 2
weight = torch.randn((2, 2), requires_grad=True)

# 设置梯度为全1矩阵  --> 2 x 2
weight.grad = torch.ones((2, 2))

# 输出现有的weight和data
print("The data of weight before step:\n{}".format(weight.data))
 # tensor([[-0.3077, -0.1808],
 #         [-0.7462, -1.5556]])

print("The grad of weight before step:\n{}".format(weight.grad))
 # tensor([[1., 1.],
 #        [1., 1.]])

# 实例化优化器
optimizer = torch.optim.SGD([weight], lr=0.1, momentum=0.9)

# 进行一步操作
optimizer.step()

# 查看进行一步后的值,梯度
print("The data of weight after step:\n{}".format(weight.data))
 # tensor([[-0.4077, -0.2808],
 #         [-0.8462, -1.6556]])

print("The grad of weight after step:\n{}".format(weight.grad))
 # tensor([[1., 1.],
 #        [1., 1.]])

# 权重清零
optimizer.zero_grad()

# 检验权重是否为0
print("The grad of weight after optimizer.zero_grad():\n{}".format(weight.grad))
 # tensor([[0., 0.],
 #        [0., 0.]])

# 输出参数
print("optimizer.params_group is \n{}".format(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}]

# 添加参数:weight2
weight2 = torch.randn((3, 3), requires_grad=True)
optimizer.add_param_group({"params": weight2, 'lr': 0.0001, 'nesterov': True})

# 查看现有的参数
print("optimizer.param_groups is\n{}".format(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()
print("state_dict before step:\n", opt_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]}]}

# 进行5次step操作
for _ in range(50):
    optimizer.step()

# 输出现有状态信息
print("state_dict after step:\n", 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]}]}


# 保存参数信息
torch.save(optimizer.state_dict(),os.path.join(r"D:\pythonProject\Attention_Unet", "optimizer_state_dict.pkl"))
print("----------done-----------")

# 加载参数信息
state_dict = torch.load(r"D:\pythonProject\Attention_Unet\optimizer_state_dict.pkl") # 需要修改为你自己的路径
optimizer.load_state_dict(state_dict)
print("load state_dict successfully\n{}".format(state_dict))

# 输出最后属性信息
print("\n{}".format(optimizer.defaults))
 # {'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}

print("\n{}".format(optimizer.state))
 # defaultdict(<class 'dict'>, {tensor([[-1.3031, -1.1761],
 #       [-1.7415, -2.5510]], requires_grad=True): {'momentum_buffer': tensor([[0.0052, 0.0052],
 #       [0.0052, 0.0052]])}})

print("\n{}".format(optimizer.param_groups))
 # [{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False, 'params': [tensor([[-1.3031, -1.1761],
 #        [-1.7415, -2.5510]], requires_grad=True)]}, {'lr': 0.0001, 'nesterov': True, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'params': [tensor([[ 0.4539, -2.1901, -0.6662],
 #        [ 0.6630, -1.5178, -0.8708],
 #        [-2.0222,  1.4573,  0.8657]], requires_grad=True)]}]

参考资料

  1. Datawhale组织的 PyTorch 教程第三章
posted @ 2022-05-18 09:32  Junwei_Kuang  阅读(245)  评论(0编辑  收藏  举报