深入浅出Pytorch-阅读笔记②(下)【和B站视频学习】

341 阅读13分钟

推荐学习视频: www.bilibili.com/video/BV1Vx…

干货满满,一起学习😀

📌本文的代码都放在GitHub上啦,可以根据本文的链接在repository中查找

阅读笔记 ② —— 第2章 Pytorch深度学习框架简介

基本模块和功能

本文的许多概念可参考文章:blog.csdn.net/qq_56591814…

模块类

模块本身是一个类nn.Module,PyTorch的模型通过继承该类,在类的内部定义子模块的实例化,通过前向计算调用子模块,最后实现深度学习模型的搭建。

整个模块函数有2部分构成:1.__init__方法初始化整个模型 2. forward方法对该模型进行前向计算

import torch.nn as nn

#继承nn.Module的方法构建模块
class Model(nn.Module):
    def __init__(self, ...):#初始化整个模型
        super(Model,self).__init__()#需要调用父类nn.Module的初始化方法,使用super函数来获取当前类的父类(即nn.Module),然后调用父类的构造函数,从而初始化一些必要的变量和参数
        pass #根据传入的参数来定义子模块(在类的内部初始化子模块)
    
    def forward(self, ...):#forward调用这些初始化的子模块,最后输出结果张量
        ret = ... #根据传入的张量和子模块计算返回张量
        return ret

🌰示例-基于模块类的简单线性回归类定义,实例化以及方法调用 【LinearModel】


计算图和自动求导机制

自动求导机制

PyTorch会根据计算过程自动生成动态图,根据动态图的创建过程进行反向传播(反向传播也是一个计算过程,可以动态创建计算图),计算得到每个节点的梯度值。

记录张量的梯度,在创建张量的时候设置一个参数requires_grad = True, 意味着这个张量会加入到计算图中,作为计算图的叶子节点参与计算,通过一系列的计算,最后输出结果张量,即根节点。

每个张量都有一个grad_fn方法,这个方法包含着创建该张量的运算的导数信息。

在反向传播的过程中,通过传入后一层的神经网络的梯度,grad_fn函数会计算出参与运算的所有张量的梯度

grad_fn本身也携带着计算图的信息,还有一个next_functions属性(包含连接该张量的其他张量的grad_fn)

通过不断反向传播回溯到中间张量的计算节点,可以得到所有张量的梯度。一个张量的梯度张量的信息保存在该张量的grad属性中

对于一个可求导的张量,可直接调用该张量内部的backward方法来自动求导。

单个张量清零梯度的方法:t.grad.zero_()

PyTorch专门自动求导的包——torch.autograd(包含重要函数s:1.torch.autograd.backward;2. torch.autograd.grad )

torch.autograd.backward通过传入根节点张量,以及初始梯度张量(形状和当前张量相同)——可计算产生该根节点所有对应的叶子节点的梯度【设置叶子节点的grad属性,最后求出梯度张量】

torch.autograd.grad不会改变叶子节点grad属性,若不需要求出当前张量对所有产生该张量叶子节点的梯度,可使用~

  • 张量为标量张量时(scalar,即只有一个元素的张量),可以不传入初始梯度张量,默认会设置初始梯度张量为1
  • 计算梯度张量(即梯度)的时候,原先建立起来的计算图会自动释放(如果需要再次做自动求导,因为计算图已经不存在了,就会报错)若要在反向传播保留计算图,设置retrain_graph = True

自动求导的时候,默认不建立反向传播的计算图(因为反向传播也是一个计算过程,会动态创建计算图)

计算图的控制

  • 使用torch.no_grad 上下文管理器,在这个上下文管理器的作用域里进行的神经网络计算不会构建任何计算图
  • 对于一个张量,在反向传播的时候可能不需要让梯度通过这个张量的节点,即新建的计算图要和原来的计算图分离(使用t.detach()和原来的计算图分离)

损失函数和优化器

损失函数

PyTorch损失函数:

  • torch.nn.functional库中的函数(通过传入神经网络预测值和目标值来计算损失函数)
  • torch.nn库里的模块(新建一个模块的实例,调用模块的方法来计算)

由于训练数据一般以迷你批次(batchsize)的形式输入神经网络,最后预测值也是以迷你批次输出损失函数最后输出结果是一个标量张量

因此,需要对batchsize的归约一般有2种:

  • 对batchsize的损失函数求和
  • 对batchsize的损失函数求平均【常用】

神经网络处理的预测问题分为回归问题(连续值)和分类问题(离散值)两种

  • 回归问题:一般用torch.nn.MSELoss模块【均方损失】

image.png

通过创建这个模块的实例(一般使用默认参数,即在类的构造函数中不传入任何参数,输出损失函数对batchsize的平均

  • 如果要输出batchsize的每个损失函数,可以指定参数reduction='none'
  • 如果要输出batchsize的损失函数的和,可以指定参数reduction='sum'
  • 分类问题:
  1. 若为多分类,使用torch.nn.CrossEntropyLoss(),在损失函数中整合了softmax输出概率以及对概率取对数输出损失函数
  2. 若为二分类,可以使用交叉熵损失函数,即torch.nn.BCELoss模块实现

偷懒方式:可以使用对数(Logits)交叉熵损失函数 torch.nn.BCEWithLogitsLoss(这个函数包含了sigmoid的计算部分哈哈哈)

image.png

关于上图的criterion(BCELoss)要注意,第一个张量pre:正分类标签的概率值 第二个张量target:以0为负分类标签,1为正分类标签的目标数据值【两个张量都是要浮点型】

优化器

optim模块详解

  • SGD为例

momentum参数理解: image.png 🌰示例-基于简单线性回归类+以SGD为例的优化器使用 【Optimizer】

其余常用的优化器可参考文章

数据的输入和预处理

PyTorch数据的载入使用的是torch.utils.data.DataLoader

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)
  • dataset:torch.utils.data.Dataset类的实例
  • batch_size是迷你批次的大小
  • shuffle:数据会不会被打乱
  • sampler:自定义的采样器,返回是一个下标索引(batch_sampler返回的是一个迷你批次的数据索引)
  • num_workers: 数据载入器使用的进程数目,默认为0,即使用单进程来处理输入数据
  • collate_fn: 定义如何把一批dataset的实例转换为包含迷你批次数据的张量
  • pin_memory: 把数据转移到和GPU内存相关联的CPU内存(Pinned Memory)中,从而加快GPU载入数据的速度
  • drop_last: 决定了是否要把最后一个迷你批次的数据丢弃掉
  • 若timeout>0,则决定在多进程情况下对数据的等待时间
  • worker_init_fn: 决定了每个数据载入的子进程开始时运行的函数,这个函数运行在随机种子设置以后、数据载入之前。

映射类型的数据集

为了能够使用DataLoader类,首先需要构造关于单个数据的torch.utils.data.Dataset类

  • 数据集类型
  1. 映射类型(Map-style),对于这个类型,每个数据有一个对应的索引,通过输入具体的索引,能得到对应的数据 【代码如下所示】
  • 这个类型的数据集有2个需要重写的方法:
  1. def __getitem__:具体看下方的Dataset类
  2. def __len__:方法返回数据的总数(如果一个Dataset类重写了该方法,可以通过使用len内置函数来获得数据的数目)
class Dataset(object):
    def __getitem__(self,index):#Python内置的操作符方法,对应的操作符是索引操作符[]
        #index: 整数数据索引(为0至N-1,N为数据总数目)
        #返回具体的某一条数据记录
    def __len__(self):
        #返回数据的数目
  1. 可迭代类型的数据集(Iterable-style),相比映射的数据集,这个数据集不用实现上述的两个方法,其本身更想一个Python迭代器。
  • 在使用多进程载入数据的情况下(DataLoader中的参数num_workers>1),因为索引之间相互独立,所以多个进程可以独立分配索引;迭代器在使用的过程中,因为索引之间有前后顺序,需要考虑如何分割数据,使得不同的进程得到不同的数据

num_workers是Dataloader的概念,默认值是0. 是告诉DataLoader实例要使用多少个子进程进行数据加载(和CPU有关,和GPU无关)

如果num_worker=0,意味着每一轮迭代时,dataloader不再有自主加载数据到RAM这一步骤(因为没有worker了),而是在RAM中找batch,找不到时再加载相应的batch。缺点当然是速度慢

当num_worker不为0时,每轮到dataloader加载数据时,dataloader一次性创建num_worker个worker,并用batch_sampler将指定batch分配给指定worker,worker将它负责的batch加载进RAM

  • Pors&Cons:

  • num_worker设置得大,好处是寻batch速度快,因为下一轮迭代的batch很可能在上一轮/上上一轮...迭代时已经加载好了。

  • 坏处是内存开销大,也加重了CPU负担(worker加载数据到RAM的进程是CPU复制的嘛)。num_workers的经验设置值是自己电脑/服务器的CPU核心数,如果CPU很强、RAM也很充足,就可以设置得更大些。

num_worker小了的情况,主进程采集完最后一个worker的batch。此时需要回去采集第一个worker产生的第二个batch。如果该worker此时没有采集完,主线程会卡在这里等。(这种情况出现在,num_works数量少或者batchsize 比较小,显卡很快就计算完了,CPU对GPU供不应求。) 即:num_workers的值和模型训练快慢有关,和训练出的模型的performance无关

模型的并行化

前面的模型和张量计算都是在一个节点上运行的。

在很多情况下,我们需要在多个机器节点上运行模型的张量的计算(每个节点可能包含多个GPU),这时就要考虑模型的并行化。

PyTorch模型的并行化主要分为模型并行(Model Parallel)和数据并行(Data Parallel)

  • 模型并行:将模型的计算图放入不同的计算节点中(不同GPU上或不同CPU节点上),然后不同节点并行计算计算图的不同部分
  • 数据并行【PyTorch主要支持】:每个节点都有一份模型的计算图的副本,通过不同节点输入不同迷你批次的数据来对节点进行前向和反向计算,最后归约反向计算的梯度,并对模型进行优化和权重更新。(需要注意:如果model达到一个节点装不下的时候,数据并行并不适用)

数据并行化(Data Parallel,DP)

  • 使用的是torch.nn.DataParallel类

代码会自动对一个迷你批次进行分割,将其均匀分布到多个GPU上进行计算,最终将结果归约到某个GPU上

分布式数据并行化(Distributed Data Parallel,DDP)

  • 使用的是torch.nn.parallel.DistributedDataParallel类

基于Python线程的,通过不同线程在不同GPU上异步运行模型来得到最终的结果。

其它

PyTorch模型的保存和加载

在深度学习模型的训练过程中, 需要周期性地对模型做存档(Checkpoint)

对于训练好的模型,经常需要用这些模型对实际数据进行预测predict,或称之为推理 Inference

这时就要求模型的权重以一定的格式保存到硬盘中,方便后续使用模型的时候直接载入原来的权重。

保存使用的是torch.save函数,加载使用的是torch.load函数

save函数:传入的第一个参数是PyTorch中可以被序列化的对象;第二个参数是存储文件的路径,序列化的结果会被保存在这个路径里

load函数:torch.load的默认行为是先把模型载入CPU中,再转移到保存时的GPU中。

TensorBoard的使用

🌰 教程样例查看

跑出以下结果的样例:【TensorBoard】

QQ截图20221002133158.png

遇到的一些问题

安装奇怪的问题: image.png 解决方案(如黄框所示):blog.csdn.net/m0_56729179…

使用奇怪的问题:【一定要writer.close()image.png 解决方案:blog.csdn.net/weixin_4498…

TensorBoard的界面中:

  1. SCALARS——主要是损失函数随着训练步数变化的曲线
  2. DISTRIBUTIONS——主要显示权重值的最大和最小的边界随着训练步数的变化过程
  3. HISTOGRAMS——显示权重的直方图随着训练步数变化的过程

一般而言 损失函数的趋势是:逐渐下降的

如果损失函数上升 or 振荡,说明学习率(lr)选择偏大,可以尝试调低学习率

如果损失函数下降比较缓慢,说明学习率(lr)偏小,可以尝试适当增加学习率

注:

  • 由于使用的是迷你批次进行优化,对每个迷你批次而言,实际的损失函数可能不一定下降,所以可通过观察平滑后的损失函数是否下降来验证

【具体可以在SCALARS界面拖动左边的Smoothing的滑块来实现,当系数越大(最大为0.999),则代表平均的迷你批次越多

  • 参数的分布和直方图 代表这个模型的某个参数是否能得到训练

【如果训练过程中参数的分布一直没有发生改变,那么有可能模型结构有问题,或者梯度无法在模型中得到反向传播】

第二章小结

torch.tensor类

PyTorch为张量建立了一个基础的torch.tensor类,这个类本质上是内存里的一段连续张量数据的包装

nn.module抽象类

深度学习的模型构建方面,提供了一系列模块,包括nn.module抽象类

通过继承这个类,并且在创建类的时候构建子模块,PyTorch实现了深度学习的模块化

损失函数和计算图构建,自动微分

PyTorch使用的是动态计算图,该计算图的特点是:灵活。

虽然在构建计算图的时候有性能开销,PyTorch本身的优化抵消了一部分开销,尽可能让计算图的构建和释放过程的代价最小

因此,相对静态图的框架来说,PyTorch本身的运算速度并不慢

有了计算图之后,可以很方便地通过自动微分机制来进行反向传播计算,从而获取计算图叶子节点的梯度

训练深度学习模型的时候,可以通过对损失函数的反向传播,计算所有参数的梯度,随后在优化器中优化这些梯度

数据的抽象类,数据载入器类

通过继承数据的抽象类,可以构造出针对某一个特殊数据的类的实例然后输入数据载入器中,数据载入器可以自动对数据进行多进程处理,最后输出数据的张量供深度学习模型使用