Linux深入浅出PyTorch(二)Pytorch主要组成模块及基础实战

PyTorch主要组成模块

1. 深度学习流程

(1)机器学习流程

  1. 数据的预处理:数据格式的统一和必要的数据变换等,并划分训练集和测试集
  2. 选择模型,设定损失函数和优化方法,并设定超参数(或者使用一些机器学习库中自带的损失函数和优化器)
  3. 用模型去拟合训练数据,并在验证集/测试集上计算模型表现

(2)深度学习流程

流程与机器学习类似,但有以下区别:

  1. 深度学习所需样本量极大,一次加载完所有数据可能会超出内存;同时存在批量(batch)训练等提高模型表现的策略,所以深度学习在数据加载上需要专门的设计;
  2. 模型实现上:深度神经网络层数较多,且有一些实现特定功能的层(如:卷积层,池化层,等等),所以深度网络一般需要逐层搭建,或者预先定义好可以实现特定功能的模块,再将它们组装起来。
  3. 损失函数和优化器的设定:保证反向传播能够在用户自定义的模型结构上实现。
  4. GPU的配置和操作:程序默认在CPU上运行,需要手动把模型和数据放到GPU上运算,同时也要确保损失函数和优化器在GPU上也可以工作,多GPU还需要考虑模型和数据分配、整合问题问题;后续计算还需要把数据放回CPU。

深度学习中训练和验证过程最大的特点在于读入数据是按批的,每次读入一个批次的数据放入GPU中训练,然后将损失函数反向传播回网络最前面的层,同时使用优化器调整网络参数。这里会涉及到各个模块配合的问题。训练/验证后还需要根据设定好的指标计算模型表现。[1]

2. 基本配置

在使用PyTorch的过程中需要导入一些python的包和调用一些PyTorch自身的模块来帮助我们实现功能。

首先导入一些必须的包:

# python
import os
import numpy as np # 一个开源的Python库,主要用在数据分析和科学计算
import torch 
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
import torch.optim as optimizer

注:

  1. os:“operating system”的缩写,顾名思义,os模块提供的就是各种 Python 程序与操作系统进行交互的接口。通过使用os模块,一方面可以方便地与操作系统进行交互,另一方面页可以极大增强代码的可移植性。[3]
  2. torch模块: 本身包含了PyTorch经常使用的一些激活函数,以及PyTorch张量的一些操作。[2]
  3. torch.nn: 是一个非常重要的模块,是PyTorch神经网络模块化的核心。这个模块定义了一系列模块,包括卷积层和线性层(全连接层)nn.Linear等。当构建深度学习模型的时候,可以通过继承nn.Module类并重写forward方法来实现一个新的神经网络。另外,torch.nn中也定义了一系列的损失函数,包括平方损失函数(torch.nn.MSELoss)、交叉熵损失函数(torch.nn.CrossEntropyLoss)等。一般来说,torch.nn里定义的神经网络模块都含有参数,可以对这些参数使用优化器进行训练。[2]
  4. torch.utils.data: 引入了数据集(Dataset)和数据载入器(DataLoader)的概念,前者代表包含了所有数据的数据集,通过索引能够得到某一条特定的数据,后者通过对数据集的包装,可以对数据集进行随机排列(Shuffle)和采样(Sample),得到一系列打乱数据顺序的迷你批次。[2]
  5. torch.optim模块: 定义了一系列的优化器,还包含学习率衰减的算法的子模块,即torch.optim.lr_scheduler,这个子模块中包含了诸如学习率阶梯下降算法torch.optim.lr_scheduler.StepLR和余弦退火算法torch.optim.lr_scheduler.CosineAnnealingLR等学习率衰减算法。[2]

然后可以预先设置好一些超参数:

  • batch size
  • 初始学习率
  • 总的训练次数(max_epochs)
  • GPU配置
# python
batch_size = 16 # 每次投入训练的数据条数
lr = 1e-4 # 优化器的初始学习率
max_epochs = 100 # 训练回合数

GPU两种常见的设置方式:
(1)使用os.environ,这种情况如果使用GPU后面不需要再设置

# python
os.environ['CUDA_VISIBLE_DEVICES'] = '0, 1'

(2)使用“device”,后续对要使用GPU的变量用 .to(device) 即可

# python
device = torch.device("cuda:1" if torch.cuda.is_available() else "cpu")

根据不同的需求,可能还会有一些其他模块和参数需要设置。

3. 数据读入

PyTorch中数据读入是通过torch.utils.data模块中的 Dataset + DataLoader类的方式完成的,流程如下:

  1. 把原始数据转变成 torch.utils.data.Dataset 类;
  2. 把得到的torch.utils.data.Dataset 类实例当作一个参数传递给 torch.utils.data.DataLoader 类,得到一个数据加载器,这个数据加载器每次可以返回一个batch(批次)的数据供模型训练使用。

注:这一过程通常可以把一张生图通过标准化、resize等操作转变成我们需要的 [B, C, H, W] 形状的 Tensor。[4]

(1) 可以直接用PyTorch的子模块torchvision准备好的数据

torchvision模块一般会随着pytorch的安装一起安装到本地,直接import torchvision 就可以使用了。
torchvision包含:

  • 常用数据集(提供的已经是Dataset类的数据集,torchvision.datasets就是专门提供各类常用数据集的模块)
  • 常用模型框架
  • 数据转换方法

注:torchvision可供使用的数据集有[4]:

# pytorch
['CIFAR10', 'CIFAR100', 'Caltech101', 'Caltech256', 'CelebA']
['Cityscapes', 'CocoCaptions', 'CocoDetection', 'DatasetFolder', 'EMNIST']
['FakeData', 'FashionMNIST', 'Flickr30k', 'Flickr8k', 'HMDB51']
['ImageFolder', 'ImageNet', 'KMNIST', 'Kinetics400', 'LSUN']
['LSUNClass', 'MNIST', 'Omniglot', 'PhotoTour', 'Places365']
['QMNIST', 'SBDataset', 'SBU', 'SEMEION', 'STL10']
['SVHN', 'UCF101', 'USPS', 'VOCDetection', 'VOCSegmentation']
['VisionDataset']

(2) 自定义Dataset类进行数据读取及初始化

  • 自定义dataset类需要继承PyTorch自身的Dataset类[4]
  • PyTorch自身的Dataset类包含三个函数:
    • __init__ 函数:读取数据文件
    • __getitem__ :支持下标访问
    • __len__ :返回自定义数据集的大小,方便后期遍历

注:自定义Dataset类只需要我们做到 1个父类继承,3个函数:

  • 一般__init__负责加载全部原始数据,初始化之类的
  • __getitem__负责按索引取出某个数据,并对该数据做预处理(但是对于如何加载原始数据以及如何预处理数据完全是由自己定义的,包括我们用 dataset[index] 取出的数据的组织形式都是完全自行定义的)。[4]

以 cifar10 数据集为例,给出构建Dataset类的方式:

import torch
from torchvision import datasets
train_data = datasets.ImageFolder(train_path, transform=data_transform)
val_data = datasets.ImageFolder(val_path, transform=data_transform)
# data_transform:可以对图像进行一定的变换,如翻转、裁剪等操作,可自己定义

注:
ImageFolder假设所有的文件按文件夹保存,每个文件夹下存储同一个类别的图片,文件夹名为类名,其构造函数如下[5]:

ImageFolder(root, transform=None, target_transform=None, loader=default_loader)

它主要有4个参数:

  • root: 在 root 指定的路径下寻找图片,root 对应图片存放的目录,目录下包含若干子目录,每个子目录对应属于同一个类的图片
  • transform:对 PIL Image 进行的转换操作,transform的输入是使用loader读取图片的返回对象
  • target_transform:对 label 的转换
  • loader:给定路径后如何读取图片,默认读取为RGB格式的 PIL Image 对象

label是按照文件夹名顺序排序后存成字典,即{类名: 类序号(从0开始)},一般来说最好直接将文件夹命名为从0开始的数字,这样会和ImageFolder实际的label一致。

如下面的例子,图片存放在一个文件夹,另外一个csv文件给出了图片名称对应的标签(label),这种情况下需要自己来定义Dataset类:

class MyDataset(Dataset):
    def __init__(self, data_dir, info_csv, image_list, transform=None):
        """
        Args:
            data_dir: path to image directory.
            info_csv: path to the csv file containing image indexes with corresponding labels.
            image_list: path to the txt file contains image names to training/validation set
            transform: optional transform to be applied on a sample.
        """
        label_info = pd.read_csv(info_csv) # 读取label
        image_file = open(image_list).readlines() # 读取图片名字
        self.data_dir = data_dir # 图片文件夹目录
        self.image_file = image_file # 图片名字文件
        self.label_info = label_info # 标签信息
        self.transform = transform # 图片的转换操作方式

    def __getitem__(self, index): # 自定义加载及取出原始数据的形式
        """
        Args:
            index: the index of item
        Returns:
            image and its labels
        """
        image_name = self.image_file[index].strip('\n')
        raw_label = self.label_info.loc[self.label_info['Image_index'] == image_name]
        label = raw_label.iloc[:,0]
        image_name = os.path.join(self.data_dir, image_name)
        image = Image.open(image_name).convert('RGB')
        if self.transform is not None: # 自定义对数据预处理的形式
            image = self.transform(image)
        return image, label

    def __len__(self): # 自定义数据集的大小
        return len(self.image_file)

然后就可以使用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)

DataLoader类为Dataset类对象提供了[6]:

  • 批量读取数据
  • 打乱数据顺序
  • 使用multiprocessing并行加载数据

DataLoader类的构造函数如下[7]:

DataLoader(dataset, batch_size=1, shuffle=False, sampler=None,
           batch_sampler=None, num_workers=0, collate_fn=None,
           pin_memory=False, drop_last=False, timeout=0,
           worker_init_fn=None)

DataLoader的主要参数如下[7]:

  • dataset:就是将上面 torch.utils.data.Dataset 类得到的实例作为输入;
  • batch_size: 每一个批次需要加载的训练样本个数,这里注意每个batch(子集)里的长度一定要一致,否则会报错[8];
  • shuffle:如果设置为 True 表示训练样本数据会被随机打乱,默认值为 False。一般会设置为 True;
  • sampler:自定义从数据集中取样本的策略,如果指定这个参数,那么 shuffle 必须为 False 。如果指定了该参数,同时 shuffle 设定为 True,DataLoader 的 init 函数就会抛出一个异常;
  • batch_sampler:与 sampler 类似,但是一次只返回一个 batch 的 indices(索引),需要注意的是,一旦指定了这个参数,那么 batch_size,shuffle,sampler,drop_last 就不能再指定了。源码中同样做了限制;
  • num_workers:表示会使用多少个线程来加载训练数据;默认值为 0,表示数据加载直接在主线程中进行;
  • collate_fn:这个参数传入的是一个函数,这个函数主要是对每个batch进行处理,最终输出一个batch的返回值[8];也就是对每一个 batch 的数据做一些你想要的操作,比如 padding 之类的;
  • pin_memory:把数据转移到和 GPU 相关联的 CPU 内存,加速 GPU 载入数据的速度;
  • drop_last:这个是对最后的少于 batch_size 的数据来说的。比如你的batch_size设置为 32,而一个 epoch 只有 100 个样本;如果设置为 True,那么训练的时候后面的 4 个就被扔掉了。如果为 False(默认),那么会继续正常执行,只是最后的 batch_size 会小一点;
  • timeout:加载一个 batch 数据的超时时间;
  • worker_init_fn:指定每个数据加载线程的入口函数。

Dataloader 作为迭代器,最基本的使用过程是[7]:

  • 传入一个 Dataset 对象
  • 通过 Dataset 类里面的 getitem 函数获取单个的数据
  • 根据参数 batch_size 的值组合成一个 batch 的数据
  • 传递给 collate_fn 所指定的函数对这个 batch 做一些操作,比如 padding 之类的操作

如果想看到上面加载的数据大致情况,可以做图出来:

import matplotlib.pyplot as plt
images, labels = next(iter(val_loader))
print(images.shape)
plt.imshow(images[0].transpose(1,2,0))
plt.show() # 在窗口显示图像

注:

  • next()、iter()这两个函数一般配套使用[10]:
    • iter( object ):生成可迭代对象的迭代器;object必须是可迭代对象,比如list、tuple、dict等;
    • next( iter, end_num ):每执行依次,按顺序每次从迭代器中提取一个元素。
    • next( iter(object) )就是从可迭代对象object中依次取元素,和直接用索引来取元素没有本质的区别;但是这种取法的好处是next()会自动记录已经取到第几个值了,无论何地使用这条语句,它都会自动继续往下取值。
  • imshow:利用matplotlib包对图片进行绘制,绘制成功后,返回一个matplotlib类型的数据,函数格式为[9]:
matplotlib.pyplot.imshow(X, cmap=None)
# X:要绘制的图像或数组
# cmap: 颜色图谱(colormap), 默认绘制为RGB(A)颜色空间

4. 模型构建

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

(1)神经网络的构造

Module 类是 nn 模块里提供的一个模型构造类,是所有神经⽹网络模块的基类。在定义自已的网络的时候,需要继承nn.Module类,并重新实现构造函数__init__(用于创建模型参数)和构造函数forward(定义前向计算)这两个方法。但有一些注意的点 [11]:

  • 一般把网络中具有可学习参数的层(如全连接层、卷积层等)放在构造函数__init__()中,当然也可以把不具有参数的层也放在里面;
  • 一般不具有可学习参数的层(如ReLU、dropout、BatchNormanation层)可放在构造函数中,也可不放在构造函数中,如果不放在构造函数__init__里面,则在forward方法里面可以使用nn.functional来代替;
  • forward方法是必须要重写的,它是实现模型的功能,实现各个层之间的连接关系的核心。

如继承Module类构造多层感知机:

import torch
from torch import nn

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

X = torch.rand(2,784)
net = MLP()
print(net)
net(X)

用前一节建的 pytorchenv 虚拟环境,在Jupyter Notebook运行结果如下:
在这里插入图片描述
注:pytorch中其实一般没有特别明显的Layer和Module的区别,不管是自定义层、自定义块、自定义模型,都是通过继承Module类完成的。

(2)神经网络中常见的层

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

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

结果:
在这里插入图片描述

2. 含模型参数的层

还可以自定义含模型参数的自定义层,模型参数可以通过训练学出。
Parameter 类其实是 Tensor 的子类,Parameter作为Module类的参数,可以自动的添加到Module类的参数列表中,并且可以使用Module.parameters()提供的迭代器获取到[12]。如果一 个 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])
        return x
net = MyListDense()
print(net)

结果:
在这里插入图片描述

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)

结果:
在这里插入图片描述

3. 常见的神经网络的一些层

1)二维卷积层

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

如下:

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

注:

  • 卷积窗口形状为 p ∗ q p*q pq 的卷积层称为 p ∗ q p*q pq 卷积层;
  • p ∗ q p*q pq 卷积或 p ∗ q p*q pq 卷积核说明卷积核的高和宽分别为 p p p q q q

填充(padding)是指在输⼊高和宽的两侧填充元素(通常是0元素)。
例如创建一个⾼和宽为3的二维卷积层,然后设输⼊高和宽两侧的填充数分别为1。给定一 个高和宽为8的输入,我们发现输出的高和宽也是8:

import torch
from torch import nn

import torch
from torch import nn

# 定义一个函数来计算卷积层。它对输入和输出做相应的升维和降维
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)

X = torch.rand(8, 8)
comp_conv2d(conv2d, X).shape

结果是:
在这里插入图片描述
当卷积核的高和宽不同时,我们也可以通过设置高和宽上不同的填充数使输出和输入具有相同的高和宽:

 使用高为5、宽为3的卷积核。在⾼和宽两侧的填充数分别为21
conv2d = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=(5, 3), padding=(2, 1))
comp_conv2d(conv2d, X).shape

结果:
在这里插入图片描述
在二维互相关运算中,卷积窗口从输入数组的最左上方开始,按从左往右、从上往下 的顺序,依次在输⼊数组上滑动。我们将每次滑动的行数和列数称为步幅(stride):

conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
comp_conv2d(conv2d, X).shape

结果:
在这里插入图片描述
总结:

  • 填充可以增加输出的高和宽。这常用来使输出与输入具有相同的高和宽;
  • 步幅可以减小输出的高和宽,例如输出的高和宽仅为输入的高和宽的 步幅分之一( 步幅为大于1的整数)。

2)池化层

  • 池化层每次对输入数据的一个固定形状窗口(⼜称池化窗口)中的元素计算输出;
  • 不同于卷积层里计算输⼊和核的互相关性,池化层直接计算池化窗口内元素的最大值(最大池化)或者平均值(平均池化);
  • 在二维最⼤池化中,池化窗口从输入数组的最左上方开始,按从左往右、从上往下的顺序,依次在输⼊数组上滑动。当池化窗口滑动到某⼀位置时,窗口中的输入子数组的最大值即输出数组中相应位置的元素。

把池化层的前向计算实现在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))
    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') # 平均池化

结果:
在这里插入图片描述
总结:可以使用torch.nn包来构建神经网络,nn包依赖于autograd包来定义模型并对它们求导;一个nn.Module包含各个层和一个forward(input)方法,该方法返回output。

3)模型示例

  1. LeNet
    在这里插入图片描述
  2. AlexNet
    在这里插入图片描述

5. 模型初始化

(1)torch.nn.init 介绍

  • 在深度学习中,权重的初始值极为重要,好的权重值会使模型收敛速率提高,使模型准确率更精确;
  • PyTorch在 torch.nn.init 中提供了常用的初始化方法,如下:
    • torch.nn.init.uniform_(tensor, a=0.0, b=1.0)
    • torch.nn.init.normal_(tensor, mean=0.0, std=1.0)
    • torch.nn.init.constant_(tensor, val)
    • torch.nn.init.ones_(tensor)
    • torch.nn.init.zeros_(tensor)
    • torch.nn.init.eye_(tensor)
    • torch.nn.init.dirac_(tensor, groups=1)
    • torch.nn.init.xavier_uniform_(tensor, gain=1.0)
    • torch.nn.init.xavier_normal_(tensor, gain=1.0)
    • torch.nn.init.kaiming_uniform_(tensor, a=0, mode=‘fan__in’, nonlinearity=‘leaky_relu’)
    • torch.nn.init.kaiming_normal_(tensor, a=0, mode=‘fan_in’, nonlinearity=‘leaky_relu’)
    • torch.nn.init.orthogonal_(tensor, gain=1)
    • torch.nn.init.sparse_(tensor, sparsity, std=0.01)
    • torch.nn.init.calculate_gain(nonlinearity, param=None)
      注:后缀带有下划线的函数,意味着它们将会直接原地更改输入张量的值

(2)torch.nn.init 使用

通常会根据实际模型使用torch.nn.init进行初始化,通常使用isinstance判断模块属于什么类型。

import torch
import torch.nn as nn

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

print(isinstance(conv,nn.Conv2d)) # isinstance() 函数来判断一个对象是否是一个已知的类型,类似 type()
print(isinstance(linear,nn.Conv2d))

结果:
在这里插入图片描述

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

# 查看随机初始化的conv参数
print(conv.weight.data)
# 查看linear的参数
print(linear.weight.data)

在这里插入图片描述

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

在这里插入图片描述

(3)初始化函数的封装

常常将各种初始化方法定义为一个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_()	

函数流程是遍历当前模型的每一层,然后判断各层属于什么类型,然后根据不同类型层,设定不同的权值初始化方法。例如:

# 模型的定义
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()))

结果如下:
在这里插入图片描述

6. 损失函数

  • 一个模型想要达到很好的效果需要学习,一个好的训练需要优质的负反馈:损失函数。如:
    在这里插入图片描述
  • 损失函数是数据输入到模型当中,产生的结果与真实标签的评价指标;
  • 模型可以设置损失函数为目标来做出改进
  • PyTorch中常用的损失函数如下:

(1)二分类交叉熵损失函数

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

计算公式如下[13]:
在这里插入图片描述
实例:

m = nn.Sigmoid()
loss = nn.BCELoss()
input = torch.randn(3, requires_grad=True)
target = torch.empty(3).random_(2)
output = loss(m(input), target)
output.backward()
print('BCELoss损失函数的计算结果为',output)

在这里插入图片描述

(2)交叉熵损失函数

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

计算公式如下[14]:
在这里插入图片描述
实例:

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

在这里插入图片描述

(3)L1损失函数

L1 Loss也称为平均绝对值误差(MAE),是指模型预测值f(x)和真实值y之间绝对差值的平均值。

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

公式[15]:

实例:

loss = nn.L1Loss()
input = torch.randn(3, 5, requires_grad=True)
target = torch.randn(3, 5)
output = loss(input, target)
output.backward()
print('L1损失函数的计算结果为',output)

在这里插入图片描述

(4)MSE损失函数

L2 Loss也称为均方误差(MSE),是指模型预测值f(x)和真实值y之间差值平方的平均值。

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

公式[15]:
在这里插入图片描述
实例:

loss = nn.MSELoss()
input = torch.randn(3, 5, requires_grad=True)
target = torch.randn(3, 5)
output = loss(input, target)
output.backward()
print('MSE损失函数的计算结果为',output)

在这里插入图片描述

(5)平滑L1 (Smooth L1)损失函数

Smooth L1就是一个平滑版的L1 Loss,是一个分段函数,在[-1,1]之间就是L2损失,解决L1在0处有折点,在[-1, 1]区间以外就是L1损失,解决离群点梯度爆炸问题[15]。

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

公式[15]:
在这里插入图片描述
实例:

loss = nn.SmoothL1Loss()
input = torch.randn(3, 5, requires_grad=True)
target = torch.randn(3, 5)
output = loss(input, target)
output.backward()
print('SmoothL1Loss损失函数的计算结果为',output)

在这里插入图片描述
注:平滑L1与L1的对比

inputs = torch.linspace(-10, 10, steps=5000)
target = torch.zeros_like(inputs)

loss_f_smooth = nn.SmoothL1Loss(reduction='none')
loss_smooth = loss_f_smooth(inputs, target)
loss_f_l1 = nn.L1Loss(reduction='none')
loss_l1 = loss_f_l1(inputs,target)

plt.plot(inputs.numpy(), loss_smooth.numpy(), label='Smooth L1 Loss')
plt.plot(inputs.numpy(), loss_l1, label='L1 loss')
plt.xlabel('x_i - y_i')
plt.ylabel('loss value')
plt.legend()
plt.grid()
plt.show()

在这里插入图片描述
可以看出对于smoothL1来说,在0这个尖端处过渡更为平滑。

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

真实标签服从泊松分布的负对数似然损失,神经网络的输出作为泊松分布的参数 λ \lambda λ

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

公式[16]:

实例:

loss = nn.PoissonNLLLoss()
log_input = torch.randn(5, 2, requires_grad=True)
target = torch.randn(5, 2)
output = loss(log_input, target)
output.backward()
print('PoissonNLLLoss损失函数的计算结果为',output)

在这里插入图片描述

(7)KL散度

考虑某个未知的分布 p(x),假定用一个近似的分布 q(x) 对它进行建模。如果我们使用 q(x) 来建立一个编码体系,用来把 x 的值传给接收者,那么由于我们使用了q(x)而不是真实分布p(x),平均编码长度比用真实分布p(x)进行编码增加的信息量(单位是 nat )为[17]:
在这里插入图片描述
这被称为分布p(x)和分布q(x)之间的相对熵(relative entropy)或者KL散 度( Kullback-Leibler divergence )。

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

实例:

inputs = torch.tensor([[0.5, 0.3, 0.2], [0.2, 0.3, 0.5]])
target = torch.tensor([[0.9, 0.05, 0.05], [0.1, 0.7, 0.2]], dtype=torch.float)
loss = nn.KLDivLoss()
output = loss(inputs,target)

print('KLDivLoss损失函数的计算结果为',output)

在这里插入图片描述

(8)MarginRankingLoss

排序损失函数,对于包含 N N N个样本的batch数据 D ( x 1 , x 2 , y ) , x 1 , x 2 D(x_1,x_2,y), x_1, x_2 D(x1,x2,y),x1,x2 是给定的待排序的两个输入, y y y代表真实的标签,属于{1,-1}。当 y = 1 y=1 y=1时, x 1 x_1 x1应该排在 x 2 x_2 x2之前,当 y = − 1 y=-1 y=1时, x 1 x_1 x1应该排在 x 2 x_2 x2之后。第 n n n个样本对应的loss计算如下[18]:
在这里插入图片描述
x 1 , x 2 x_1, x_2 x1,x2排序正确且 − y ∗ ( x 1 − x 2 ) > margin ⁡ -y *(x 1-x 2)>\operatorname{margin} y(x1x2)>margin, 则loss为0;其他情况下是loss为 − y ∗ ( x 1 − x 2 ) + margin ⁡ -y *(x_1-x_2)+\operatorname{margin} y(x1x2)+margin

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

注:reduction有三种取值: m e a n , s u m , n o n e mean, sum, none mean,sum,none,对应不同的返回 ℓ ( x , y ) \ell(x, y) (x,y)。 默认为 m e a n mean mean,对应于上述 l o s s loss loss的计算( m a r g i n margin margin默认取值0)[18]:
在这里插入图片描述

实例:

loss = nn.MarginRankingLoss()
input1 = torch.randn(3, requires_grad=True)
input2 = torch.randn(3, requires_grad=True)
target = torch.randn(3).sign()
output = loss(input1, input2, target)
output.backward()

print('MarginRankingLoss损失函数的计算结果为',output)

在这里插入图片描述

(9)多标签边界损失函数

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

公式[19]:
在这里插入图片描述
实例:

loss = nn.MultiLabelMarginLoss()
x = torch.FloatTensor([[0.9, 0.2, 0.4, 0.8]])
# for target y, only consider labels 3 and 0, not after label -1
y = torch.LongTensor([[3, 0, -1, 1]])# 真实的分类是,第3类和第0类
output = loss(x, y)

print('MultiLabelMarginLoss损失函数的计算结果为',output)

在这里插入图片描述

(10)二分类损失函数

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

公式[20]:
在这里插入图片描述
y y y表示的是真实的标签, y ^ \hat{y} y^表示模型的输出标签。

实例:

inputs = torch.tensor([[0.3, 0.7], [0.5, 0.5]])  # 两个样本,两个神经元
target = torch.tensor([[-1, 1], [1, -1]], dtype=torch.float)  # 该 loss 为逐个神经元计算,需要为每个神经元单独设置标签

loss_f = nn.SoftMarginLoss()
output = loss_f(inputs, target)

print('SoftMarginLoss损失函数的计算结果为',output)

在这里插入图片描述

(11)多分类的折页损失

多分类合页损失函数(hinge loss),对于一个样本不是考虑样本输出与真实类别之间的误差,而是考虑对应真实类别与其他类别之间的误差[21]。
对于包含 N N N个样本的batch数据 D ( x , y ) D ( x , y ) D(x,y) x x x为神经网络的输出, y y y 是真实的类别标签,假设类别数为 C C C, 0 ≤ y n ≤ C − 1 0 ≤ y_n ≤ C − 1 0ynC1,第 n n n个样本的损失值 l n l_n ln计算如下[21]:
在这里插入图片描述
为了处理多个类别之间的样本不平衡问题,对于每一类可传入相应的权值 w w w[21]:
在这里插入图片描述

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

注:reduction有三种取值 m e a n , s u m , n o n e mean, sum, none mean,sum,none,对应不同的返回 ℓ ( x , y ) \ell(x, y) (x,y) ,默认为 m e a n mean mean,对应于一般情况下整体 l o s s loss loss的计算[21]:
在这里插入图片描述
实例:

inputs = torch.tensor([[0.3, 0.7], [0.5, 0.5]]) 
target = torch.tensor([0, 1], dtype=torch.long) 

loss_f = nn.MultiMarginLoss()
output = loss_f(inputs, target)

print('MultiMarginLoss损失函数的计算结果为',output)

在这里插入图片描述

(12)三元组损失

torch.nn.TripletMarginLoss(margin=1.0, p=2.0, eps=1e-06, swap=False, size_average=None, reduce=None, reduction='mean')

定义三种图像分别为Anchor(A)、Positive§、Negative(N)
定义一个三元组的损失函数为[22]:
在这里插入图片描述
实例:

triplet_loss = nn.TripletMarginLoss(margin=1.0, p=2)
anchor = torch.randn(100, 128, requires_grad=True)
positive = torch.randn(100, 128, requires_grad=True)
negative = torch.randn(100, 128, requires_grad=True)
output = triplet_loss(anchor, positive, negative)
output.backward()
print('TripletMarginLoss损失函数的计算结果为',output)

在这里插入图片描述

(13)HingEmbeddingLoss

torch.nn.HingeEmbeddingLoss(margin=1.0, size_average=None, reduce=None, reduction='mean')

公式[23]:
在这里插入图片描述
注:它输入 x x x y y y(1或者-1), m a r g i n margin margin默认为1。

  1. y = − 1 y=-1 y=1的时候, l o s s = m a x ( 0 , 1 − x ) loss=max(0,1-x) loss=max(0,1x),如果 x > 1 ( m a r g i n ) x>1 (margin) x>1(margin),则 l o s s = 0 loss=0 loss=0;如果 x < 1 x<1 x<1 l o s s = 1 − x loss=1-x loss=1x
  2. y = 1 y=1 y=1 l o s s = x loss=x loss=x

什么时候用[23]:

  • 非线形Embedding
  • 半监督学习
  • 监测两个输入的相似性或者不相似性

实例:

loss_f = nn.HingeEmbeddingLoss()
inputs = torch.tensor([[1., 0.8, 0.5]])
target = torch.tensor([[1, 1, -1]])
output = loss_f(inputs,target)

print('HingEmbeddingLoss损失函数的计算结果为',output)

在这里插入图片描述

(14)余弦相似度

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

公式[23]:
在这里插入图片描述
什么时候用[23]:

  • 非线形Embedding
  • 半监督学习
  • 监测两个输入的相似性或者不相似性

实例:

loss_f = nn.CosineEmbeddingLoss()
inputs_1 = torch.tensor([[0.3, 0.5, 0.7], [0.3, 0.5, 0.7]])
inputs_2 = torch.tensor([[0.1, 0.3, 0.5], [0.1, 0.3, 0.5]])
target = torch.tensor([1, -1], dtype=torch.float)
output = loss_f(inputs_1,inputs_2,target)

print('CosineEmbeddingLoss损失函数的计算结果为',output)

在这里插入图片描述

(15)CTC损失函数

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

什么时候用:序列标注

实例:

# Target are to be padded
T = 50      # Input sequence length
C = 20      # Number of classes (including blank)
N = 16      # Batch size
S = 30      # Target sequence length of longest target in batch (padding length)
S_min = 10  # Minimum target length, for demonstration purposes

# Initialize random batch of input vectors, for *size = (T,N,C)
input = torch.randn(T, N, C).log_softmax(2).detach().requires_grad_()

# Initialize random batch of targets (0 = blank, 1:C = classes)
target = torch.randint(low=1, high=C, size=(N, S), dtype=torch.long)

input_lengths = torch.full(size=(N,), fill_value=T, dtype=torch.long)
target_lengths = torch.randint(low=S_min, high=S, size=(N,), dtype=torch.long)
ctc_loss = nn.CTCLoss()
loss = ctc_loss(input, target, input_lengths, target_lengths)
loss.backward()


# Target are to be un-padded
T = 50      # Input sequence length
C = 20      # Number of classes (including blank)
N = 16      # Batch size

# Initialize random batch of input vectors, for *size = (T,N,C)
input = torch.randn(T, N, C).log_softmax(2).detach().requires_grad_()
input_lengths = torch.full(size=(N,), fill_value=T, dtype=torch.long)

# Initialize random batch of targets (0 = blank, 1:C = classes)
target_lengths = torch.randint(low=1, high=T, size=(N,), dtype=torch.long)
target = torch.randint(low=1, high=C, size=(sum(target_lengths),), dtype=torch.long)
ctc_loss = nn.CTCLoss()
loss = ctc_loss(input, target, input_lengths, target_lengths)
loss.backward()

print('CTCLoss损失函数的计算结果为',loss)

在这里插入图片描述

7. 训练和评估

以上步骤完成就可以训练模型了。首先设置模型的状态:

  • 训练状态:模型的参数应该支持反向传播的修改;
  • 验证/测试状态:不应该修改模型参数。
    在PyTorch中,模型的状态设置如下:
model.train()   # 训练状态
model.eval()   # 验证/测试状态

然后:

for data, label in train_loader: # 用for循环读取DataLoader中的全部数据
data, label = data.cuda(), label.cuda() # 将数据放到GPU上用于后续计算
optimizer.zero_grad() # 开始当前批次训练之前,先将优化器的梯度置0
output = model(data) # 将data送入模型中训练
loss = criterion(output, label) # 根据预先定义的评判标准计算损失函数
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))

8. 可视化

见后续章节

9. PyTorch优化器

(1)PyTorch提供的优化器

深度学习的本质就是训练一个函数去寻找最优解,通过不断的改变网络参数。如何快速求得最优解,可以使用优化器:

  • 优化器是根据网络反向传播的梯度信息来更新网络的参数,以起到降低loss函数计算值,使得模型输出更加接近真实标签。
  • 优化器主要用在模型训练阶段,用于更新模型中可学习的参数。
  • Pytorch一共提供了10种优化器:
    • torch.optim.ASGD
    • torch.optim.Adadelta
    • torch.optim.Adagrad(自适应学习率算法)
    • torch.optim.Adam
    • torch.optim.AdamW
    • torch.optim.Adamax
    • torch.optim.LBFGS
    • torch.optim.RMSprop
    • torch.optim.Rprop
    • torch.optim.SGD(随机梯度下降法)
    • torch.optim.SparseAdam

这些优化算法均继承于Optimizer,它是所有优化器的基类:

class Optimizer(object):
    def __init__(self, params, defaults):        
        self.defaults = defaults
        self.state = defaultdict(dict)
        self.param_groups = []

注:Optimizer有三个属性:

  • defaults:存储的优化器的超参数,如:
{
    
    'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}
  • state:参数的缓存,如:
defaultdict(<class 'dict'>, {
    
    tensor([[ 0.3864, -0.0131],
        [-0.1911, -0.4511]], requires_grad=True): {
    
    'momentum_buffer': tensor([[0.0052, 0.0052],
        [0.0052, 0.0052]])}})
  • 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, 'momentum': 0, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}]

Optimizer还有以下的其他方法:

  1. zero_grad():清空所管理参数的梯度,PyTorch的特性是张量的梯度不自动清零,因此每次反向传播后都需要清空梯度。
  2. step():执行一步梯度更新,参数更新
  3. add_param_group():添加参数组
  4. load_state_dict() :加载状态参数字典,可以用来进行模型的断点续训练,继续上次的参数进行训练
  5. state_dict():获取优化器当前状态信息字典

(2)案例操作

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))
print("The grad of weight before step:\n{}".format(weight.grad))
# 实例化优化器
optimizer = torch.optim.SGD([weight], lr=0.1, momentum=0.9)
# 进行一步操作
optimizer.step()
# 查看进行一步后的值,梯度
print("The data of weight after step:\n{}".format(weight.data))
print("The grad of weight after step:\n{}".format(weight.grad))
# 权重清零
optimizer.zero_grad()
# 检验权重是否为0
print("The grad of weight after optimizer.zero_grad():\n{}".format(weight.grad))
# 输出参数
print("optimizer.params_group is \n{}".format(optimizer.param_groups))
# 查看参数位置,optimizer和weight的位置一样,我觉得这里可以参考Python是基于值管理
print("weight in optimizer:{}\nweight in weight:{}\n".format(id(optimizer.param_groups[0]['params'][0]), id(weight)))
# 添加参数: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))
# 查看当前状态信息
opt_state_dict = optimizer.state_dict()
print("state_dict before step:\n", opt_state_dict)
# 进行5次step操作
for _ in range(50):
    optimizer.step()
# 输出现有状态信息
print("state_dict after step:\n", optimizer.state_dict())
# 保存参数信息
torch.save(optimizer.state_dict(),os.path.join("/home/cloris/anaconda3/envs/pytorchenv", "optimizer_state_dict.pkl"))
print("----------done-----------")
# 加载参数信息
state_dict = torch.load("/home/cloris/anaconda3/envs/pytorchenv/optimizer_state_dict.pkl") # 需要修改为自己的路径
optimizer.load_state_dict(state_dict)
print("load state_dict successfully\n{}".format(state_dict))
# 输出最后属性信息
print("\n{}".format(optimizer.defaults))
print("\n{}".format(optimizer.state))
print("\n{}".format(optimizer.param_groups))

(3)案例结果

在这里插入图片描述
注意:

  • 每个优化器都是一个类,一定要进行实例化才能使用;
  • optimizer在一个神经网络的epoch中需要实现下面两个步骤:
    • 梯度置零
    • 梯度更新
  • 网络不同的层赋予不同的优化器参数;
  • 优化器的选择是需要根据模型进行改变的,不存在绝对的好坏之分。

基础实战——FashionMNIST时装分类

  • FashionMNIST数据集在这里
  • 对10个类别的“时装”图像进行分类,FashionMNIST数据集中包含已经预先划分好的训练集和测试集,其中训练集共60,000张图像,测试集共10,000张图像。每张图像均为单通道黑白图像,大小为28*28pixel,分属10个类别。
    实验代码如下:
# 1. 导入必要的包
import os
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader

# 2. 配置训练环境和超参数
# 配置GPU,这里有两种方式
# 方案一:使用os.environ
os.environ['CUDA_VISIBLE_DEVICES'] = '0'
# 方案二:使用“device”,后续对要使用GPU的变量用.to(device)即可
# device = torch.device("cuda:1" if torch.cuda.is_available() else "cpu")

# 配置其他超参数,如batch_size, num_workers, learning rate, 以及总的epochs
batch_size = 256
num_workers = 4   # 对于Windows用户,这里应设置为0,否则会出现多线程错误
lr = 1e-4
epochs = 20

## 读取方式一:使用torchvision自带数据集,下载需要一段时间
from torchvision import datasets

train_data = datasets.FashionMNIST(root='./', train=True, download=True, transform=data_transform)
test_data = datasets.FashionMNIST(root='./', train=False, download=True, transform=data_transform)

# 构建训练和测试数据集完成后,定义DataLoader类,在训练和测试时用于加载数据
train_loader = DataLoader(train_data, batch_size=batch_size, shuffle=True, num_workers=num_workers, drop_last=True)
test_loader = DataLoader(test_data, batch_size=batch_size, shuffle=False, num_workers=num_workers)

# 数据可视化验证读取的数据是否正确
import matplotlib.pyplot as plt
image, label = next(iter(train_loader))
print(image.shape, label.shape)
plt.imshow(image[0][0], cmap="gray")

# 手搭CNN,模型构建完成后将其放到GPU上用于训练
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv = nn.Sequential(
            nn.Conv2d(1, 32, 5),
            nn.ReLU(),
            nn.MaxPool2d(2, stride=2),
            nn.Dropout(0.3),
            nn.Conv2d(32, 64, 5),
            nn.ReLU(),
            nn.MaxPool2d(2, stride=2),
            nn.Dropout(0.3)
        )
        self.fc = nn.Sequential(
            nn.Linear(64*4*4, 512),
            nn.ReLU(),
            nn.Linear(512, 10)
        )
        
    def forward(self, x):
        x = self.conv(x)
        x = x.view(-1, 64*4*4)
        x = self.fc(x)
        # x = nn.functional.normalize(x)
        return x

model = Net()
model = model.cuda()
# model = nn.DataParallel(model).cuda()   # 多卡训练时的写法

# 设定损失函数
# 使用torch.nn模块自带的CrossEntropy损失
# PyTorch会自动把整数型的label转为one-hot型,用于计算CE loss
# 这里需要确保label是从0开始的,同时模型不加softmax层(使用logits计算),这也说明了PyTorch训练中各个部分不是独立的,需要通盘考虑
criterion = nn.CrossEntropyLoss()
# criterion = nn.CrossEntropyLoss(weight=[1,1,1,1,3,1,1,1,1,1])

# 设定优化器,这里使用Adam优化器
optimizer = optim.Adam(model.parameters(), lr=0.001)

# 训练和测试
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(output, label)
        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
    gt_labels = []
    pred_labels = []
    with torch.no_grad():
        for data, label in test_loader:
            data, label = data.cuda(), label.cuda()
            output = model(data)
            preds = torch.argmax(output, 1)
            gt_labels.append(label.cpu().data.numpy())
            pred_labels.append(preds.cpu().data.numpy())
            loss = criterion(output, label)
            val_loss += loss.item()*data.size(0)
    val_loss = val_loss/len(test_loader.dataset)
    gt_labels, pred_labels = np.concatenate(gt_labels), np.concatenate(pred_labels)
    acc = np.sum(gt_labels==pred_labels)/len(pred_labels)
    print('Epoch: {} \tValidation Loss: {:.6f}, Accuracy: {:6f}'.format(epoch, val_loss, acc)

# 模型保存
# 训练完成后,可以使用torch.save保存模型参数或者整个模型,也可以在训练过程中保存模型
save_path = "./FahionModel.pkl"
torch.save(model, save_path)

训练过程:
在这里插入图片描述

引用:
[1] Datawhale项目: 深入浅出PyTorch(强推)
[2] Pytorch各个模块介绍:https://www.cnblogs.com/liulunyang/p/14356789.html
[3] Python 标准库之 os 模块详解:http://www.ityouknow.com/python/2019/10/09/python-os-demonstration-026.html
[4] Pytorch的第一步:(1) Dataset类的使用: https://www.jianshu.com/p/4818a1a4b5bd
[5] pytorch之ImageFolder: https://blog.csdn.net/weixin_40123108/article/details/85099449
[6] Pytorch中数据加载—Dataset类和DataLoader类:https://blog.csdn.net/u012609509/article/details/81264687
[7] PyTorch 之 Dataset 和 Dataloader: https://zhuanlan.zhihu.com/p/339675188
[8] Pytorch——Dataset类和DataLoader类:https://www.cnblogs.com/CircleWang/p/15496422.html
[9] python数字图像处理(5):图像的绘制: https://www.cnblogs.com/denny402/p/5122594.html
[10] Python:next()和iter()的使用说明: https://www.jianshu.com/p/aa6b17d303e9
[11] 使用Module类来自定义模型:https://blog.csdn.net/qq_27825451/article/details/90550890
[12] pytorch源码阅读系列之Parameter类: https://zhuanlan.zhihu.com/p/101052508
[13] PyTorch学习笔记——二分类交叉熵损失函数: https://zhuanlan.zhihu.com/p/59800597
[14] 交叉熵损失函数(Cross Entropy Loss): https://blog.csdn.net/SongGu1996/article/details/99056721
[15] 目标检测回归损失函数——L1、L2、smooth L1:https://zhuanlan.zhihu.com/p/267688490
[16] loss函数之PoissonNLLLoss,GaussianNLLLoss: https://blog.csdn.net/ltochange/article/details/117935410
[17] KL散度理解: https://zhuanlan.zhihu.com/p/39682125
[18] loss函数之MarginRankingLoss: https://www.jianshu.com/p/ea62b3c9cb44
[19] Distribution-Balanced Loss for Multi-Label Classification in Long-Tailed Datasets: https://zhuanlan.zhihu.com/p/420217021
[20] Pytorch中的分类问题损失函数: https://www.jianshu.com/p/70a8b34e0ace
[21] loss函数之MultiMarginLoss, MultiLabelMarginLoss:https://blog.csdn.net/ltochange/article/details/118001115
[22] https://windmissing.github.io/DeepLearningNotes/CV/Face/Triplet.html
[23] PyTorch中的损失函数–MarginRanking/Hinge/Cosine:https://zhuanlan.zhihu.com/p/83364904

猜你喜欢

转载自blog.csdn.net/weixin_41794514/article/details/126891939