pytorch
模型
ConvTranspose2d
id:: 6258d8b8-2557-4a01-ae49-cb96116dd746
逆卷积。
论文中称其为fractionally-strided convolutions,也有的称它为deconvolutions,但是前者表达更为确切。
nn.AdaptiveAvgPool2d
2元(2d)
就是二维数据的意思。
汇聚层(Pooling)
汇聚层,有些地方也翻译成池化层,它主要负责对数据在空间维度(宽度和高度)上进行降采样(downsampling)操作
均值(Avg)
均值(Avg)指定了汇聚层在进行降采样操作时所采用的计算方法。
汇聚层在降采样时,通常会使用最大值抽取样本和均值抽取样本两种手法。用最大值抽取样本的汇聚层一般叫做最大值汇聚层,用均值抽取样本的汇聚层一般叫做均值汇聚层。
自适应(Adaptive)
在实际的项目当中,我们往往预先只知道的是输入数据和输出数据的大小,而不知道核与步长的大小。如果使用上面的方法创建汇聚层,我们每次都需要手动计算核的大小和步长的值。而自适应(Adaptive)能让我们从这样的计算当中解脱出来,只要我们给定输入数据和输出数据的大小,自适应算法能够自动帮助我们计算核的大小和每次移动的步长。相当于我们对核说,我已经给你输入和输出的数据了,你自己适应去吧。你要长多大,你每次要走多远,都由你自己决定,总之最后你的输出符合我的要求就行了。
nn.AvgPool2d
需要至少指定kernel_size,stride,padding 三个参数,使用起来和卷积相似。最后的输出为
AvgPool2d和AdaptiveAvgPool2d: 前者使用方法同卷积操作,而后者只需要指定输出图像的尺度,如HxW,或H(这时会默认为HxH)。
Embedding
函数
1 | torch.nn.Embedding(num_embeddings, embedding_dim, padding_idx=None, max_norm=None, norm_type=2, scale_grad_by_freq=False, sparse=False) |
num_embeddings:字典中词的个数
embedding_dim:嵌入的维度
padding_idx (索引指定填充):如果给定,则遇到padding_idx中的索引,则将其位置填0(0是默认值,事实上随便填充什么值都可以)
max_norm:每一个嵌入矢量,它的norm如果大于max_norm将会被renormalized到max_norm
norm_type: the p of the p-norm to compute for the max_norm option.Default 2
scale_grad_by_freq: this will scale gradients by the inverse of frequence of the words in the min-batch .Default False
sparse:是否使用稀疏矩阵
这个函数的作用就是词嵌入,避免了手工的词袋设计。
🔥 embeddings中的值是正太分布N(0,1)中随机取值。
使用
传统的使用方法
在Sparse R-CNN中用来嵌入框
将框作为嵌入信息使用,使用embeddings层的权重作为可能的位置关系来进行,通过clone权重作为参数,将字典功能的嵌入层变为了特征转换层。Sparse R-CNN/projects/SparseRCNN/sparsercnn/detector.py/131
传统的使用方法
detach
detach能够将数据从对应的网络中提取出来,能够阻断梯度回传。将一个变量从网络中分离出来,还是存放在原来的地方,只是将requires_grad设置为了False。
NMS
原理
这里以hard-nms为例:
- 按类别进行遍历
- 选择当前类别中得分最高的框作为基准来进行nms操作
- 删除与基准iou超过阈值的候选框
- 从剩下的候选框中在选择最高得分的框作为一个新的基准重复操作
多种nms
Hard-nms
直接删除相邻的同类别目标,密集目标的输出不友好。有多种不同的实现:
- or-nms: hard-nms的非官方实现形式,只支持cpu。
- vision-nms: hard-nms的官方实现形式(c函数库),可支持gpu(cuda),只支持单类别输入。
- vision-batched-nms: hard-nms的官方实现形式(c函数库),可支持gpu(cuda),支持多类别输入。
Soft-nms
hard-nms的官方实现形式(c函数库),可支持gpu(cuda),支持多类别输入。 [[soft-nms]]
and-nms
在hard-nms的逻辑基础上,增加是否为单独框的限制,删除没有重叠框的框(减少误检)。
merge-nms
在hard-nms的逻辑基础上,增加是否为单独框的限制,删除没有重叠框的框(减少误检)。
diou-nms
在hard-nms的基础上,用diou替换iou,里有参照diou的优势。
CODE
NMS python实现
1 |
|
IOU计算
1 | def bbox_iou(box1, box2, x1y1x2y2=True, GIoU=False, DIoU=False, CIoU=False): |
正则化,规范化
在卷积操作中,经常会使用到BN操作,由于卷积操作其实也是数据在不同维度投影的操作。这个时候,如果执行从高维到低维的投影,若数据其中某一特征的数值特别大的话,那么它在整个误差计算的比重上就很大。所以将数据投影到低维空间之后,整个投影会去努力逼近数值最大的那一个特征,而忽略数值比较小的特征。
为了“公平”起见,防止过分捕捉某些数值大的特征,我们就可以先对每个特征先进行标准化处理,使得它们的大小都在相同的范围内。
standardization
标准化,将训练集中某一列数值特征的值缩放成均值为0,方差为1的状态。
Normalization
归一化,将训练集中某一列数值特征的值缩放到0和1之间。
池化
平移不变性
自定义OP
流程:
- 注册OP
- 实现OP
- 创建python接口
- 实现OP梯度计算(不需要求导可以直接pass)
统计参数量
1 | total_params = 0 |
反向传播
由于神经网络中求梯度的时候,直接对矩阵进行十分复杂,但是所有的矩阵对矩阵的导数都是可以通过间接的方法,利用求标量导数的那些知识轻松求出来的。而这种间接求导数的方法就是维度分析。
通过维度分析和链式法则简化求导过程。
维度分析
示例
问题
设某一层的Forward Pass为score=XW + b,,X是NxD的矩阵,W是DxC的矩阵,b是1xC的矩阵,那么score就是一个NxC的矩阵。现在上层已经告诉你L对score的导数是多少了,我们求L对W和b的导数。
求解
首先Loss是一个标量,$\frac{dL}{dscore}$一定是一个NxC的矩阵,L对W的导数就就表示为:
$$
\frac{dL}{dW}=\frac{dL}{dscore}\frac{dscore}{dW}
$$
这里score和W都是矩阵,直接进行矩阵对矩阵的求导复杂。
思路
其实我们没有必要直接求score对W的导数,我们可以利用另外两个导数间接地把
$\frac{dscore}{dW}$算出来。首先来看看它的形状。我们知道$\frac{dL}{dW}$一定是DxC的(形状和W一致),而$\frac{dL}{dscore}$是NxC的,所以$\frac{dscore}{dW}$一定是DxN的,因为(DxN)x(NxC)→(DxC),按计算可以看出,求导公式的正式写法应该是:$\frac{dL}{dW}=\frac{dscore}{dW}\frac{dL}{dscore}$。
既然$score=XW+b$,如果都是标量的话,score对W求导,本身就是X;X是NxD的,所以我们需要DxN的形状的数据,顺其自然我们只需要转置一下就可以得到:
$$
\frac{dL}{dW}=X^T\frac{dL}{dscore}
$$
完事。
这样就避免了直接去用$\frac{dscore_{11}}{dW_{11}}$这种细枝末节的一个一个元素求导的方式推导$\frac{dscore}{dW}$,这就是神经网络种求取导数的正确姿势。
实现的关键在于Loss是一个标量,而标量对一个矩阵求导,其大小和这个矩阵的大小永远是一样的。
链式法则
链式法则在神经网络中十分重要!
BN集合
BN依据不同的方式有多种不同的形式
traditional BN
[[BN]]
BatchNorm是针对单个神经元进行的,利用网络训练时一个 mini-batch 的数据来计算该神经元的均值和方差。所以对Batch有强依赖。CxHxW相当于有CxHxW这么多个神经元。
步骤:
- 计算/更新均值
- 计算/更新方差
- 使用均值和方差对每个元素标准化
- BN的可学习参数就是线性变化
统计对象的魔改
Layer Normization
计算C H W上的均值和方差,这样的归一化方式,使 batch 中的每个样本均可利用其本身的数据的进行归一化操作。
更高效方便,也不存在更新均值和方差时,batch 内均值和方差不稳定的问题。
在 NLP 中(Transformer)比较常用,CNN 上作用不大,但是随着Transformer的跨界,LN和MLP的组合变得十分常见。
Instance Normization
计算 (H, W) 上的均值和方差,作用于每个样本的每个通道上的归一化方法
由于其计算均值和方差的粒度较细,在 low-level 的任务中(例如风格迁移)通常表现突出。在 GAN 网络的生成器中使用,IN 会降低生成图像中的网格感,使其更加自然。
Group Normization
LN和IN的均衡操作。
在一定数目的 channel 上进行归一化(即计算 (c, H, W) 上的均值和方差)。当 batch size 小于 8 的时候,通常使用 Group Normalization 代替 Batch Normalization,对训练好的模型 fineture,效果还不错。
Switchable Normalization
BN、LN、IN按一定的比例加权组合,权重系数通过网络学习。
速度慢。
Sync Batch Normalization
跨卡实现同步BN操作,SN 能够帮助我们屏蔽多卡训练的分布式细节,将分散在每个 GPU 上的 batch 合并,视为一个机器上的一个 batch。
其关键是在前向运算的时候,计算并同步全局(所有 GPU 上 batch)的均值和方差;在反向运算时候,同步相应的全局梯度。
实现
2次同步
先同步各卡的均值,计算出全局的均值,然后同步给各卡,接着各卡同步计算方差。
缺点:消耗资源,且影响训练速度。
1次同步
其中m为各卡拿到的数据批次大小$(\frac{batch*size}{nGPU})$。
由上可知,每张卡计算出$\sum_{i=1}^{m}X_i$和$\sum_{i=1}^{m}x_i^2$,然后进行求和,即可计算出全局的方差,同时,全局的均值可通过各卡的$\sum_{i=1}^{m}x_i$同步求和得到,一次同步就可实现全局均值和方差的计算。
关键在于重载 [[replicate]] 方法,原生的该方法只是将模型在每张卡上复制一份,并且没有建立起联系,而我们的 SyncBN 是需要进行同步的,因此需要重载该方法,让各张卡上的SyncBN 通过某种数据结构和同步机制建立起联系。
Sparse Switchable Normalization
Switchable Normalization 的改进版。通过稀疏约束,使用 sparsestmax 替代了 softmax,在保持性能的同时减少了 switchable 的计算量,增加了鲁棒性。
Region Normalization
Region Normalization 在 Batch Normalization 的基础上进一步对 (N, H, W) 中的 (H, W) 进行了划分。Region Normalization 根据输入掩码将空间像素划分为不同的区域,并计算每个区域的均值和方差以进行归一化。实现时只需将输入先进行 mask,再输入 BN 即可。
Local Context Normalization
Local Context Normalization 在 Layer Normalization 的基础上对 (C, H, W) 进行了划分。Local Context Normalization 根据每个特征值所在的局部邻域,计算邻域的均值和方差以进行归一化。实现时作者借助了空洞卷积获取 window 内均值和方差,比较巧妙。
Channel Normalization
在 channel 维度求统计量,对 tensor 进行归一化。
Domain-Specific Batch Normalization
Domain-Specific Batch Normalization 与 Split Batch Normalization 类似,都针对 sample 的不同进行了分组。不同的是,在 Domain adaptation 任务中,Domain-Specific Batch Normalization 选择对 source domain 和 target domain 进行分组。在计算时,干脆创建了两个 Batch Normalization,根据 domain 选择相应的分支输入。
规范化方式的魔改
Filter Response Normalization
对Instance Normalization的改进。在 (H, W) 维度上计算统计量。不同的是,Filter Response Normalization 并不计算平均值,在规范化的过程中,直接使用每个元素除以 (H, W) 维度二范数的平均值。由于缺少减去均值的操作,因此归一化的结果可能会产生偏移(不以 0 为中心),这对于后续的 ReLU 激活是不利的。因此,作者还提出了配套的激活函数 TLU。(其实 TLU 单独使用也挺好用的)
Extended Batch Normalization
Extended Batch Normalization 改进了 Batch Normalization。虽然仍使用均值和方差进行规范化,不同之处在于,Extended Batch Normalization 在 (N, H, W) 的维度上求平均值,在 (N, C, H, W) 的维度上求方差。直观上看,统计元素数量的增多使得方差更为稳定,因而能够在小 batch 上取得较好的效果。
Kalman Normalization
Kalman Normalization 改进了 Batch Normalization,使得不同层之间的统计量得以相互关联。在 Kalman Normalization,当前归一化层的均值和方差是通过和上一个归一化层的均值和方差进行卡尔曼滤波得到的(可以简单理解当前状态为加权历史状态),其在大尺度物体检测和风格任务上都取得了较好的效果。
L1-Norm Batch Normalization
L1-Norm Batch Normalization 将 Batch Normalization 中的求方差换成了 L1 范数。在 GAN 中表现出了较好的效果。
Cosine Normalization
提出使用 cosine similarity 进行归一化。
仿射变换方式的魔改
仿射变换的方式给人的感觉就是将BN也往网络上改。
Conditional Batch/Instance Normalization
Conditional Batch Normalization 在 Batch Normalization 的基础上改进了仿射变换的部分。它使用 LSTM 和多层感知器,将自然语言映射为一组特征向量,作为仿射变换的权重 γ 和偏置 β 引导后续任务。Conditional Instance Normalization 则是对 style 信息进行编码,使用在风格迁移中(正如之前所介绍的,Instance Normalization 在 low-level 任务中更有优势)。笔者认为,二者都可以看作使用 attention 机制引入了外部信息。
Adaptive Instance Normalization
Adaptive Instance Normalization 进一步放宽了权重 γ 和偏置 β 在风格迁移中的估计方法。作者提出的方法不再需要一个单独的 style 特征提取模块,对于给定的风格图像和原始图像都使用相同的 backbone (VGG)提取特征,用风格图像算出权重 γ 和偏置 β ,用于原始图像的仿射变换中。
Adaptive Convolution-based Normalization
Adaptive Convolution-based Normalization 是 Adaptive Instance Normalization 的改良版。不同之处在于,Adaptive Convolution-based Normalization 在仿射变换的过程中,使用一个动态的卷积层完成。这时的仿射变换已经漏出了卷积的獠牙,传统意义上的仿射变换名存实亡。
Spatially-Adaptive Normalization
Spatially-Adaptive Normalization 在使用均值和方差将每个元素标准化到 [0,1] 后,在仿射变换层融入了 mask 引导的 attention 机制。作者首先使用卷积层对 mask 进行变换,在得到的 feature map 上分别使用两个卷积层得到权重 γ 和偏置 β 的估计。最后使用权重 γ 和偏置 β 的估计对归一化的结果进行 element-wise 的乘加操作,完成归一化。
Region-Adaptive Normalization
Region-Adaptive Normalization 在 Spatially-Adaptive Normalization 的基础上进行了 style map 和 segmentation mask 两个 branch 的 fusion。分别使用 style map 和 mask 套用 Spatially-Adaptive Normalization 得到权重 γ 和偏置 β 的估计,再将两个 branch 的估计加权平均,得到最终的估计。而后进行仿射变换完成归一化。
Instance Enhancement Batch Normalization
Instance Enhancement Batch Normalization 对权重 γ 的估计则更为简单粗暴。在使用时,不需要引入 mask 等额外信息做引导,由网络自适应地学习。作者借鉴了 SENet 的思路,通过池化、变换、Sigmoid 得到一组权重 γ 。这使得 Instance Enhancement Batch Normalization 具有很强的通用性,尽管参数量有所提升,但是即插即用无痛涨点。
Attentive Normalization
Attentive Normalization 与 Instance Enhancement Batch Normalization 的方法类似。笔者认为 Attentive Normalization 这个名称似乎更加形象一些。
保存加载模型
保存加载模型基本用法
整个模型
保存整个网络模型(网络结构+权重参数):
1 | torch.save(model, 'net.pkl') |
直接加载整个网络模型(可能比较耗时):
1 | model = torch.load('net.pkl') |
只针对模型参数
只保存模型的权重参数(速度快,占内存少):
1 | torch.save(model.state_dict(), 'net_params.pkl') |
因为我们只保存了模型的参数,所以需要先定义一个网络对象,然后再加载模型参数:
1 |
|
加载部分参数
模型微调操作,能够满足模型的按需加载操作。
1 | pretrained_dict = torch.load('net_params.pkl')#加载模型 |
pkl文件
保存加载的 net.pkl 其实一个字典,通常包含如下内容:
- 网络结构:输入尺寸、输出尺寸以及隐藏层信息,以便能够在加载时重建模型。
- 模型的权重参数:包含各网络层训练后的可学习参数,可以在模型实例上调用
state_dict()
方法来获取,比如前面介绍只保存模型权重参数时用到的model.state_dict()
。 - 优化器参数:有时保存模型的参数需要稍后接着训练,那么就必须保存优化器的状态和所其使用的超参数,也是在优化器实例上调用
state_dict()
方法来获取这些参数。 - 其他信息:有时我们需要保存一些其他的信息,比如
epoch
,batch_size
等超参数。
自定义模型
保存
通过pkl文件格式,我们就可以自定义需要保存的内容,比如:
1 |
|
上面的 checkpoint 是个字典,里面有4个键值对,分别表示网络模型的不同信息。
加载
1 | def load_checkpoint(filepath): |
如果加载模型只是为了进行推理测试,则将每一层的 requires_grad 置为 False,即固定这些权重参数;还需要调用 model.eval() 将模型置为测试模式,主要是将 dropout 和 batch normalization 层进行固定,否则模型的预测结果每次都会不同。
如果希望继续训练,则调用 model.train(),以确保网络模型处于训练模式。
state_dict
state_dict() 也是一个Python字典对象,model.state_dict() 将每一层的可学习参数映射为参数矩阵,其中只包含具有可学习参数的层(卷积层、全连接层等)。
示例
1 |
|
输出:
1 | Model's state_dict: |
可以看到 model.state_dict() 保存了卷积层,BatchNorm层和最大池化层的信息;而 optimizer.state_dict() 则保存的优化器的状态和相关的超参数。
跨设备保存加载模型
Save on GPU, Load on CPU
在 CPU 上加载在 GPU 上训练并保存的模型:
1 | device = torch.device('cpu') |
map_location:一个公式、torch.device、字符串或者一个字典,用来明确如何去重新映射存储位置。
官方示例:
1 | 'tensors.pt') torch.load( |
不需要将模型迁移到device上,因为一开始载入都是载入到内存先的,所以不需要迁移操作。
Save on GPU, Load on GPU
在 GPU 上加载在 GPU 上训练并保存的模型:
1 | device = torch.device("cuda") |
需要将模型和数据都迁移到显存上,才能开始训练。
在这里使用 map_location 参数不起作用,要使用 model.to(torch.device(“cuda”)) 将模型转换为CUDA优化的模型。
还需要对将要输入模型的数据调用 **data = data.to(device)**,即将数据从CPU转移到GPU。请注意,调用 my_tensor.to(device) 会返回一个 my_tensor 在 GPU 上的副本,它不会覆盖 my_tensor。因此需要手动覆盖张量:my_tensor = my_tensor.to(device)。
Save on CPU, Load on GPU
在 GPU 上加载在 CPU 上训练并保存的模型:
1 | device = torch.device("cuda") |
当加载单个包含GPU tensors的模型的设备时,这些tensors 会被默认加载到GPU上,不过是同一个GPU设备。
当有多个GPU设备时,可以通过将 map_location 设定为 cuda:device_id 来指定使用哪一个GPU设备,上面例子是指定编号为0的GPU设备。
CUDA
判断cuda是否可用
1 | print(torch.cuda.is_available()) |
获取gpu数量
1 | print(torch.cuda.device_count()) |
获取gpu名字
1 | torch.cuda.get_device_name(0) |
返回当前gpu设备索引,默认从0开始
1 | torch.cuda.current_device() |
查看tensor或者model在哪块GPU上
1 | torch.tensor([0]).get_device() |
将数据和模型从cpu迁移到gpu上
1 | use_cuda = torch.cuda.is_available() |
no_grad
一般来说,我们在进行模型训练的过程中,因为要监控模型的性能,在跑完若干个epoch训练之后,需要进行一次在验证集[4]上的性能验证。一般来说,在验证或者是测试阶段,因为只是需要跑个前向传播(forward)就足够了,因此不需要保存变量的梯度。
问题:保存梯度是需要额外显存或者内存进行保存的,占用了空间,有时候还会在验证阶段导致OOM(Out Of Memory)错误,因此我们在验证和测试阶段,最好显式地取消掉模型变量的梯度。通过no_grad实现:
1 | model.train() |
在pytorch0.4之前的版本通过volatile=True来进行设置。
model.eval
在模型中有BN层或者dropout层时,在训练阶段和测试阶段必须显式指定train()和eval()。
我们的模型中经常会有一些子模型,其在训练时候和测试时候的参数是不同的,比如dropout[6]中的丢弃率和Batch Normalization[5]中的$\gamma$和$\beta$等,这个时候我们就需要显式地指定不同的阶段(训练或者测试),在pytorch中我们通过model.train()和model.eval()进行显式指定,具体如:
1 | model = CNNNet(params) |
从这里看model.train()和model.eval()函数并不执行模型,只是对其中的BN和Dropout进行设置。
retain_graph
在对一个损失进行反向传播时,在pytorch中调用out.backward()即可实现:
1 | import torch |
w.r.t按这里的翻译就是叶子的梯度。
对loss进行反向传播就可以获得$\partial loss/\partial w_{i,j}$,即是损失对于每个叶子节点的梯度。在.backward这个API文档中,有多个参数:
1 | backward(gradient=None, retain_graph=None, create_graph=False) |
这里我们关注的是retain_graph这个参数,这个参数如果为False或者None则在反向传播完后,就释放掉构建出来的graph,如果为True则不对graph进行释放。
为什么不释放
存在多个loss的时候,很有用。
示例
首先构造graph:
1 | import torch |
如图:
当我们第一次使用$d.backward()$对末节点d进行求梯度,这样在执行完反向传播之后,因为没有显式地要求它保留graph,系统对graph内存进行释放,如果下一步需要对节点e进行求梯度,那么将会因为没有这个graph而报错。因此有例子:
1 | d.backward(retain_graph=True) # fine |
利用这个性质在某些场景是有作用的,比如在对抗生成网络GAN中需要先对某个模块比如生成器进行训练,后对判别器进行训练,这个时候整个网络就会存在两个以上的loss,例子如:
1 | G_loss = ... |
这个时候就可以对网络中多个loss进行分步的训练了。
冻结网络层
freeze
eval
会固定网络中的BN、dropout等在训练和测试的时候行为不一致的模块。
冻结指定的层
1 |
|
named_parameters实现
可以通过named_parameters来找到指定的层进行固定
1 | net = Net() |
只冻结BN
问题
由于bn城的参数不光与其待训练的参数有关,还与runing_mean和runing_var两个参数有关,而这2个参数是通过统计得到的,如果直接使用常规的冻结操作:
1 | for p in self.detection_net: |
是无法有效冻结bn操作的
solution
这里通过apply函数和class属性来实现
class属性
可以使用模型的class属性中的name变量来查看当前层的类型:
1 | l = [conv,bn] |
⚠️ 问题通过以上方式是无法便利出Sequential封装的,所以这个时候就需要使用apply
apply
为当前的元组(或list)的每个元素应用所指定的函数。
最终实现
1 | def fix_bn(m): |
通过以上方式就可以很好的固定BN操作了。
实现二
1 | def fix_bn(self): |
数据
Pytorch的数据读取主要由3个类完成:Dataset、Samper、DataLoader。Dataset用来读取单张样本、Samper对数据集中的样本进行采样、DataLoader根据采样顺序读取一个Batch的数据。为了更加直观,这里反向解释。
dataloader
pytorch/torch/utils/data/dataloader.py
调用了sampler函数进行采样工作
定义了由多少个进程(线程)来处理数据
1 | class DataLoader(object): |
((6258f20b-fc4d-4e0e-8fd4-9cbd2ddfb82b))的作用就是将一个batch的数据进行合并操作。
Sampler
Sampler返回的是样本的index(位置信息)
Pytorch已经实现的Sampler有如下几种:
[[SequentialSampler]]
[[RandomSampler]]
[[WeightedRandomSampler]]
[[SubsetRandomSampler]]
Sampler是对数据集的采样操作,而BatchSampler将Sampler生成的index按照指定的batch size分组,返回的结果是以组为单位的index list。
⚠️ BatchSampler是实现Batch的关键!BatchSampler与其他Sampler的主要区别是它需要将Sampler作为参数进行打包,进而每次迭代返回以batch size为大小的index列表。也就是说在后面的读取数据过程中使用的都是batch sampler。
实现
1 |
|
通常会使用BatchSampler来进行Batch化。
这里以SequentialSampler采样为例,进行BatchSampler:
1 | >>>in : list(BatchSampler(SequentialSampler(range(10)), batch_size=3, drop_last=False)) |
注意
DataLoader的部分初始化参数之间存在互斥关系
- 如果你自定义了batch_sampler,那么这些参数都必须使用默认值:batch_size, shuffle,sampler,drop_last.
- 如果你自定义了sampler,那么shuffle需要设置为False
- 如果sampler和batch_sampler都为None,那么batch_sampler使用Pytorch已经实现好的BatchSampler,而sampler分两种情况:
- 若shuffle=True,则sampler=RandomSampler(dataset)
- 若shuffle=False,则sampler=SequentialSampler(dataset)
DataSet
1 | class Dataset(object): |
getitem的主要作用是能让该类可以像list一样通过索引值对数据进行访问。
扩展
假如你定义好了一个dataset,那么你可以直接通过dataset[0]来访问第一个数据。在此之前我一直没弄清楚__getitem__是什么作用,所以一直不知道该怎么进入到这个函数进行调试。现在如果你想对__getitem__方法进行调试,你可以写一个for循环遍历dataset来进行调试了,而不用构建dataloader等一大堆东西了,建议学会使用ipdb这个库,非常实用!!!以后有时间再写一篇ipdb的使用教程。另外,其实我们通过最前面的Dataloader的__next__函数可以看到DataLoader对数据的读取其实就是用了for循环来遍历数据。
Tensor
其实就是具备GPU加速功能的numpy
Variable
Variable是对Tensor的封装,从Pytorch1.0开始,Variable就已经被取消了,其实就是将Variable的功能集成到了Tensor里。这里介绍的Variable,相当于就是Tensor对numpy的扩充部分。
Variable三元素:
- Tensor: 数据部分
- grad:Tensor的梯度
- grad_fn: 以何种方式得到这种梯度
计算图就是建立在Variable这个数据结构之上的,方便神经网络的构建。 Pytorch默认做完一次自动求导之后就会丢弃计算图,所以在没有使用retain_graph的时候,只能使用一次backward,第二次使用就会报错(因为grad_fn其实就不存在了,没有计算图了怎么还会有如何计算)。
Parameter
这里以最直观的梯度下降更新网络参数为例,展示参数更新过程:
1 | w.data = w.data lr * w.grad.data # lr 是学习率 |
Variable主要是为了解决计算图构建问题,但是它的性质导致其无法作为网络的参数:
- Variable默认是不需要计算梯度的,需要手动设置参数requires_grad=True
- Pytorch默认只计算一次计算图就丢弃,这就导致Variable容易被丢弃,在backward的时候还需要手动设置参数w.backward(retrain_grad=True)来保持计算图。
- 网络中若是有100个参数,都要手写更新代码吗?1000个呢?10000个呢
Pytorch引入nn.Parameter类型和optimizer机制来解决。创建parameter的方式:
- 我们可以直接将**模型的成员变量(**self. )通过nn.Parameter() 创建,会自动注册到parameters中,可以通过model.parameters() 返回,并且这样创建的参数会自动保存到OrderDict中去;
- 通过nn.Parameter()创建普通的Parameter对象,不作为模型的成员变量,然后将Parameter对象通过register_parameter()进行注册,这个时候也会保存到到网络的parameters()对象里。
Parameter是Variable的子类,默认是需要求梯度的。网络net中的所有parameter变量都可以通过net.parameters()来进行访问,这样的好处就是不需要手动为每个参数都写更新代码,只需要使用parameter就行了。 只需将网络中所有需要训练更新的参数定义为Parameter类型,再佐以optimizer,就能够完成所有参数的更新了。
实现
1 | class Net(Module): |
在每一次batch操作中,只需要在每次loss.backward()之后调用optimizer.step()就可以完成参数的更新。
要点
需要注册,通过注册才能被网络跟踪,可以通过state_dict读取,保存到OrderDict里。
named_parameters()
会同时给出网络层的名字和参数的字典,而parameters()只会给出参数(权重)。
buffer
与parameter对应,parameter是网络中需要进行更新的参数,而buffer是不需要更新的参数。
buffer与parameter的异同:
- 都需要通过等级才能通过model.state_dict()查看,parameter通过register_parameter登记,buffer通过register_buffer进行登记
- buffer其实就是requires_grad=False,但是为了更方便总体使用,所以进行不同的封装。
归并运算
与numpy对应,只是参数的名字不一样,在numpy中用axis指定的维度在tensor中使用dim表示。
dim:指定的维度会被聚合
keepdim:默认False。表示不保留聚合的维度,True会保留维度为1。
Name | 功能 |
---|---|
mean/sum/median/mode | 均值/和/中位数/众数 |
norm/dist | 范数/距离 |
std/var | 标准差/方差 |
cumsum/cumprod | 累加/累乘 |
numel | 返回元素数目 |
DataParallel
上图中并没有将opitmizer封装到DataParallel里,这是由于每次前向传播的时候都会分发模型,用不着反向传播时将梯度loss分发到各个GPU单独计算梯度,在进行合并的操作。
可以就在主GPU上根据总loss更新模型的梯度,并且不用同步其他GPU上的模型,因为前向传播的时候会分发模型。
DataParallel是没有完成对GPU的调用的,需要手动cuda上去。
基础示例
在进行多GPU训练的场景,PyTorch通常使用nn.DataParallel来包装网络模型,它会将模型在每张卡上都复制一份,从而实现并行训练。
1 | device_ids = [0,1] |
缺点
DataParallel会导致其中一块卡的显存占用高于其他块。
容易导致负载不均衡,这个时候就需要将loss分配的到多个GPU上进行计算,然后使用loss.mean or loss.sum等方式进行归并。这样主GPU只会多一点占用。
分析
1 | CLASS torch.nn.DataParallel(module, device_ids=None, output_device=None, dim=0) |
module:需要并行计算的模型
device_ids:可用GPU
output_device:当调用nn.DataParallel的时候,只是在input数据是并行的,但是output loss却不是这样的,每次都会在第一块GPU相加计算。这里默认是device_ids[0]
dim:对数据进行分割的维度
由于DataParallel的设计,导致在运行DataParallel模块之前,并行化模块必须在device_ids[0]上具有其参数和缓冲区,如果不想占用多余的卡就需要设置:
1 | os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID" |
使得网络在识别的时候,只识别这2张卡,2卡就会成为第一张卡来存储DataParallel的缓冲区。
DataParallel CODE
1 | def forward(self, *input, **kwargs): |
src_device_obj:第一张卡,默认为device_ids[0]
其中涉及到了4个方法:
scatter:将输入数据及参数均分到每张卡上
replicate:将模型复制一份(注意:卡上必须有scatter分割的数据存在!)
parallel_apply:每张卡并行计算结果,这里会调用被包装的具体模型的前向反馈操作
gather:将每张卡的计算结果统一汇聚到主卡
高阶函数
gather
1 | torch.gather(input, dim, index, out=NOne)->Tensor |
沿着给定的轴dim,对输入索引张量index指定位置的值进行聚合。其实就是只有指定的维度上使用到了index上的值。
对一个三维张量,输出可以定义为:
1 |
|
可以看出index的形状需要和out相同。
同时还要注意:除了指定的维度的长度外,其他维度的长度必须相同才能执行,否则会报错,同时i j k是按照index能够取得的范围来定的。
collate_fn
id:: 6258f20b-fc4d-4e0e-8fd4-9cbd2ddfb82b
默认的collate_fn是将img和label分别合并成imgs和labels,所以如果你的__getitem__方法只是返回 img, label,那么你可以使用默认的collate_fn方法,但是如果你每次读取的数据有img, box, label等等,那么你就需要自定义collate_fn来将对应的数据合并成一个batch数据,这样方便后续的训练步骤。
拼接
1 | batch = self.collate_fn([self.dataset[i] for i in indices]) |
以上式为例:
- indices: 表示每一个iteration,sampler返回的indices,即一个batch size大小的索引列表
- self.dataset[i]: 前面已经介绍了,这里就是对第i个数据进行读取操作,一般来说self.dataset[i]=(img, label)
拼接应该就是通过这个函数完成的,但是indices的获取应该还是BatchSampler的作用,所以它们是不冲突的,BatchSampler获取一个batch的index之后,在这里进行样本对象的拼接。
维度变换
因为在训练时的数据维度一般都是 (batch_size, c, h, w),而在测试时只输入一张图片,所以需要扩展维度。
view
1 | import cv2 |
numpy.newaxis
1 | import cv2 |
unsqueeze
1 | import cv2 |
torchvision.models
内置了多个模型和其权重:
AlexNet
VGG
ResNet
SqueezeNet
DenseNet
查看部分样本信息
torch.index_select()
用于索引给定张量中某一个维度中元素的方法,其API手册如:
1 | torch.index_select(input, dim, index, out=None) → Tensor |
其作用很简单,比如我现在的输入张量为1000 * 10的尺寸大小,其中1000为样本数量,10为特征数目,如果我现在需要指定的某些样本,比如第1-100,300-400等等样本,我可以用一个index进行索引,然后应用torch.index_select()就可以索引了,例子如:
1 | 3, 4) x = torch.randn( |
注意:pytorch似乎在使用GPU的情况下,不检查index是否会越界,因此如果你的index越界了,但是报错的地方可能不在使用index_select()的地方,而是在后续的代码中,这个似乎就需要留意下你的index了。同时,index是一个LongTensor,这个也是要留意的。
torchvision
ops
函数 | 功能 |
---|---|
batched_nms | 根据每个类别进行过滤,只对同一种类别进行计算IOU和阈值过滤 |
nms | 不区分类别对所有bbox进行过滤。如果有不同类别的bbox重叠的话会导致被过滤掉并不会分开计算。 |
ray
基于 Python 的分布式计算框架,采用动态图计算模型。使用起来很方便可通过装饰器的方式,仅需修改极少的的代码,让原本运行在单机的 Python 代码轻松实现分布式计算。目前多用于机器学习方面
1 | import ray |
索引
torch.index_select(input,dim,index)
在维度dim上,按index索引数据
返回值:依index索引数据拼接成张量
torch.masked_select(input,mask)
功能:按mask中的True进行索引
返回值:一维张量
Function
基础函数,不带自动反向求导。可以用来生成Module模型(带自动求导)。
ctx
在function里会封装一个ctx函数,用来传递前向forward的结果到反向backwark进行梯度求导。
以ChannNorm为例
1 | from torch.autograd import Function, Variable |
hook
Hook 是 PyTorch 中一个十分有用的特性。利用它,我们可以不必改变网络输入输出的结构,方便地获取、改变网络中间层变量的值和梯度。
why
pytorch在每一次运算结束后,会将中间变量释放,以节省内存空间,这些会被释放的变量包括非叶子张量的梯度,中间层的特征图等。但有时候,我们想可视化中间层的特征图,又不能改动模型主体代码,该怎么办呢?这时候就要用到hook了。
可视化中间结果,如梯度(反向传播之后中间节点的梯度会被清空)
可视化参数,感觉有了tensorboardx之类的工具了,这个要来干嘛莫。
当前的kook主要分为2类:
- 针对tensor
- 针对nn.Moudle
Hook for Tensors
torch.Tensor.register_hook (Python method, in torch.Tensor)
存在意义
在 PyTorch 的计算图(computation graph)中,只有叶子结点(leaf nodes)的变量会保留梯度。而所有中间变量的梯度只被用于反向传播,一旦完成反向传播,中间变量的梯度就将自动释放,从而节约内存。
但是可以通过retain_grad进行梯度的持久化。保留中间变量。但是这种方法会增加内存占用。所以就有了hook。
对于中间变量z,hook的使用方式如下:
1 | z.register_hook(hook_fn) |
其中hook_fn是一个用户自定义的函数,其签名为:
1 |
|
结果和上文中 z.retain_grad()方法得到的 z 的偏导一致。
hook会改变变量的值,同时一个变量可以绑定多个hook_fn,反向传播时,它们按绑定顺序依次执行。
应用
修改中间结果的梯度
1 | import torch |
原x的梯度为tensor([1., 1., 1., 1.]),经grad_hook操作后,梯度为tensor([2., 2., 2., 2.])。
查看中间节点的梯度
1 | import torch |
可以看到当z.backward()结束后,张量y中的grad为None,因为y是非叶子节点张量,在梯度反传结束之后,被释放。 在对张量y的hook函数(grad_hook)中,将y的梯度保存到了y_grad列表中,因此可以在z.backward()结束后,仍旧可以在y_grad[0]中读到y的梯度为tensor([0.2500, 0.2500, 0.2500, 0.2500])
Hook for Modules
why
但是对于模型而言,对于夹在网络中间的模块,我们不但很难得知它输入/输出的梯度,甚至连它输入输出的数值都无法获得除非设计网络时,在 forward 函数的返回值中包含中间 module 的输出,或者用很麻烦的办法,把网络按照 module 的名称拆分再组合,让中间层提取的 feature 暴露出来。
how
下面剖析一下module是怎么样调用hook函数的呢?
- output = net(fake_img) net是一个module类,对module执行 module(input)是会调用module.call
- module.call 在module.call中执行流程如下:可以看出hook就定义在模型执行内部。
1
2
3
4
5
6
7
8
9
10
11
12
13
14def __call__(self, *input, **kwargs):
for hook in self._forward_pre_hooks.values():
hook(self, input)
if torch._C._get_tracing_state():
result = self._slow_forward(*input, **kwargs)
else:
result = self.forward(*input, **kwargs)
for hook in self._forward_hooks.values(): # 前向hook不能有返回值
hook_result = hook(self, input, result)
if hook_result is not None:
raise RuntimeError(
"forward hooks should never return any values, but '{}'"
"didn't return None".format(hook))
...省略
what
torch.nn.Module.register_forward_hook→None
1 | import torch |
这里再conv1注册了hook,而conv1就是一个卷积模块(依旧是一个Moudle对象),所以执行流程就是
- 主体网络接受输入fake_img进行forward,并按call中的顺序执行(主体Module对象没有hook)
- 执行每个模块单元,并按call执行(这个时候就执行到了conv1,同样按Module call顺序执行,调用注册的hook)
⚠️ 在这里终于要执行我们注册的forward_hook函数了,就在hook_result = hook(self, input, result)这里! 看到这里我们需要注意两点: - hook_result = hook(self, input, result)中的input和result不可以修改! 这里的input对应forward_hook函数中的data_input,result对应forward_hook函数中的data_output,在conv1中,input就是该层的输入数据,result就是经过conv1层操作之后的输出特征图。虽然可以通过hook来对这些数据操作,但是不能修改这些值,否则会破坏模型的计算。
- 注册的hook函数是不能带返回值的,否则抛出异常,这个可以从代码中看到 if hook_result is not None: raise RuntimeError
总结一下调用流程: net(fake_img) –> net.call : result = self.forward(input, *kwargs) –> net.forward: x = self.conv1(x) –> conv1.call:hook_result = hook(self, input, result) hook就是我们注册的forward_hook函数了。
torch.nn.Module.register_forward_pre_hook
虽然也定义在Moudle call中,但是可以看到它的执行是在forward函数之前的
torch.nn.Module.register_backward_hook→Tensor or None
Module反向传播中的hook,每次计算module的梯度后,自动调用hook函数。
注意事项:当module有多个输入或输出时,grad_input和grad_output是一个tuple。
应用场景举例:例如提取特征图的梯度 举例:采用register_backward_hook实现特征图梯度的提取,并结合Grad-CAM(基于类梯度的类激活图可视化)方法对卷积神经网络的学习模式进行可视化。
技巧
调试
终端条件
在执行命令之前添加
CUDA_LAUNCH_BLOCKING=1
由于cuda是异步操作,所以报错的地方不一定是真正有问题的地方,在无法定位错误位置的时候,可以使用这个执行参数使得cuda变为同步的,这样就方便定位错误了。
查看函数功能
可以在python终端下使用:
help(torch.gather)
来实现函数的使用说明。
指定GPU
当服务器中有多个GPU的时候,选择指定的GPU运行程序可在程序运行命令之前使用:
1 | CUDA_VISIBLE_DEVICES=0 |
也可以将这行指令临时添加到终端反复使用:
1 | export CUDA_VISIBLE_DEVICES=1 |
永久设置:
1 | 在~/.bashrc 的最后加上export CUDA_VISIBLE_DEVICES=1,然后source ~/.bashrc |
在代码中指定GPU
指定GPU的命令需要放在和神经网络相关的一系列操作之前。
默认使用某块GPU
一块
设置当前使用的GPU设备仅为0号设备,设备名称为 /gpu:0:
1 | os.environ["CUDA_VISIBLE_DEVICES"] = "0" |
多块
设置当前使用的GPU设备为0,1号两个设备,名称依次为 /gpu:0、/gpu:1:
1 | os.environ["CUDA_VISIBLE_DEVICES"] = "0,1" |
根据顺序表示优先使用0号设备,然后使用1号设备。
查看模型每层输出详情
Keras有一个简洁的API来查看模型的每一层输出尺寸,这在调试网络时非常有用。现在在PyTorch中也可以实现这个功能。
使用很简单,如下用法:
1 | from torchsummary import summary |
梯度裁剪(Gradient Clipping)
梯度裁剪在某些任务上会额外消耗大量的计算时间。
1 | import torch.nn as nn |
nn.utils.clip_grad_norm_的参数:
parameters – 一个基于变量的迭代器,会进行梯度归一化
max_norm – 梯度的最大范数
norm_type – 规定范数的类型,默认为L2
防止验证模型时爆显存
验证模型时不需要求导,即不需要梯度计算,关闭autograd,可以提高速度,节约内存。如果不关闭可能会爆显存。
no_grad
一般来说,我们在进行模型训练的过程中,因为要监控模型的性能,在跑完若干个epoch训练之后,需要进行一次在验证集[4]上的性能验证。一般来说,在验证或者是测试阶段,因为只是需要跑个前向传播(forward)就足够了,因此不需要保存变量的梯度。
问题:保存梯度是需要额外显存或者内存进行保存的,占用了空间,有时候还会在验证阶段导致OOM(Out Of Memory)错误,因此我们在验证和测试阶段,最好显式地取消掉模型变量的梯度。通过no_grad实现:
1 | model.train() |
在pytorch0.4之前的版本通过volatile=True来进行设置。
empty_cache
PyTorch的缓存分配器会事先分配一些固定的显存,即使实际上tensors并没有使用完这些显存,这些显存也不能被其他应用使用。这个分配过程由第一次CUDA内存访问触发的。
而 torch.cuda.empty_cache() 的作用就是释放缓存分配器当前持有的且未占用的缓存显存,以便这些显存可以被其他GPU应用程序中使用,并且通过 nvidia-smi命令可见。注意使用此命令不会释放tensors占用的显存。
显存不够用
retain_graph
进行梯度累积,实现内存紧张情况下的大batch_size训练,在GPU显存紧张的情况下使用可以等价于用更大的batch_size进行训练。
首先我们要明白,当调用.backward()时,其实是对损失到各个节点的梯度进行计算,计算结果将会保存在各个节点上,如果不用opt.zero_grad()对其进行清0,那么只要你一直调用.backward()梯度就会一直累积,没有调用优化器就不会使用这些梯度来更新参数,多次backward之后再调用优化器相当于是在大的batch_size下进行的训练。
示例
1 | import torch |
第一次输出:
1 | tensor([[ 0.0493, -0.0581, -0.0451, 0.0485, 0.1147, 0.1413, -0.0712, -0.1459, |
第二次loss.backward(retain_graph=True),输出为:
1 | tensor([[ 0.0987, -0.1163, -0.0902, 0.0969, 0.2295, 0.2825, -0.1424, -0.2917, |
运行一次opt.zero_grad(),输出为:
1 | tensor([[0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], |
现在明白为什么我们一般在求梯度时要用opt.zero_grad()了吧,那是为了不要这次的梯度结果被上一次给影响,但是在某些情况下这个‘影响’是可以利用的。
扩充Batch
可以通过滞后zero_grad实现batch扩大
问题汇总
Expected object of device type cuda but got device type cpu
这种问题一般是由于to(device)使用不规范导致的。同时to(device)操作也容易导致性能下降。这里建议在模型已经在cuda上的时候,使用like的操作来生成需要的张量数据:
1 | torch.zeros_like() |
AssertionError: nn criterions don’t compute the gradient w.r.t. targets please mark these variables as volatile or not requiring gradients
表示target是需要一个不能被训练的,也就是requires_grad=False的值。
1 |
|
MSELoss,和其他很多loss,比如交叉熵,KL散度等,其target都需要是一个不能被训练的值的,这个和TensorFlow中的tf.nn.softmax_cross_entropy_with_logits_v2不太一样,后者可以使用可训练的target。
必知必会
初始化
合适的初始化很重要,初始化就跟黑科技一样,用对了超参都不用调;没用对,跑出来的结果就跟模型有bug一样不忍直视。
优选xavier初始化或者He初始化。
1xN卷积
适当使用,可能得到更好的泛化效果:
- 卷积可以减少计算量
- 可以在某个方向强调感受野,也就是说假如如果你要对一个长方形形状的目标进行分类,你可以使用的卷积核搭配的卷积核对长边方向设定更大的感受野
ACNet结构
在3x3卷积的基础上加上1x3和3x1的旁路卷积核,最后在推理阶段把三个卷积核都fusion到3x3卷积核上,在许多经典CV任务上都可以获得大概1个点的提升。
BN
加速收敛。
如果有BN了全连接层就没必要加Dropout了。
fpn结构
目标检测不能盲目去掉fpn结构。在针对自己的数据调检测任务如yolov3的时候不能盲目砍掉fpn结构,尽管你分析出某个分支的Anchor基本不可能会对你预测的目标起作用,但如果你直接去掉分支很可能会带来漏检。
激活函数
可以先用ReLU做一版,如果想再提升精度可以将ReLU改成PReLU试试。我更倾向于直接使用ReLU。
batch_size
在不同类型的任务中,batch_size的影响也不同。
loss
用loss的时候往往并不是直接替换loss那么简单,需要仔细思考loss背后的数学原理,要用对地方才可有提升。
为什么 YOLOv3 用了 Focal Loss 后 mAP 反而掉了?
backbone
使用了带backbone的网络,如训练VGG16-SSD建议选择finetune的方式,从头训练不仅费时费力,甚至难以收敛。
upsampling
在做分割实验的时候我发现用upsamling 加1*1卷积代替反卷积做上采样得到的结果更平滑,并且miou差距不大,所以我认为这两者都是都可以使用的。
框过多也是需要优化的
一些Anchor-based目标检测算法为了提高精度,都是疯狂给框,ap值确实上去了,但也导致了fp会很多,并且这部分fp没有回归,在nms阶段也滤不掉。相比于ap提升而言,工程上减少fp更加重要。Gaussian yolov3的fp相比于yolov3会减少40%
epoch or iteration
what
现在主要有4种迭代参数的方式:
- Loss/Epoch:告诉你一个模型要观察同一张图片多少次才能理解它(遍历所有数据才进行更新)
- Loss/Iteration:告诉你需要多少次参数更新。(由于每次Batch的采样已经是独立同分布)(比较优化器时这很有用,可以帮助你加快训练速度或达到更高的精度)
- Loss/Total Image Seen(iteration * batchsize):告诉你算法看到了多少图像时的损失。适合比较两种算法使用数据的效率。并能够消除Batchsize的影响,这允许在不同GPU上训练的具有不同Batch Size的模型之间进行公平地比较。
- Loss/Time(on specific dedicated gpu(s))
seed
需要为Pytorch和numpy分别设置各自的随机生成数。
比如在进行DataLoader的时候,使用worker_init_fn选项专门设置 seed,否则在 PyTorch 同时使用 NumPy 的随机数生成器和多进程数据加载会导致相同的扩充数据。用户没有这样做,因而这个 bug 悄悄地降低了模型的准确率。
如果不针对生成则会使用同一个生成数从而降低模型的准确率。
参考
推荐库
kornia
可以进行Tesnor操作的现代化opencv(针对numpy)。