实验室定期的学习交流会,整理一份我自己主讲的pytorch框架的笔记,主要针对看论文学习过程中遇到的一些问题,因为之前是学的tensorflow
1️⃣PyTorch 的设计遵循tensor
→variable(autograd)
→nn.Module
2️⃣三个由低到高的抽象层次,分别代表高维数组(张量)
、自动求导(变量)
和神经网络(层/模块)
,而且这三个抽象之间联系紧密,可以同时进行修改和操作
3️⃣PyTorch的源码只有TensorFlow的十分之一左右,更少的抽象、更直观的设计使得PyTorch的源码更易于阅读
特点
GPU/CPU
之上、基础的张量操作库;torch
、torch.nn
、torch.optim
等;Pytorch常用工具包
torch
:类似 NumPy 的张量库,强 GPU 支持 ;torch.autograd
:基于 tape 的自动区别库,支持 torch 之中的所有可区分张量运行;torch.nn
:为最大化灵活性未涉及、与 autograd 深度整合的神经网络库;torch.optim
:与 torch.nn 一起使用的优化包,包含 SGD、RMSProp、LBFGS、Adam 等标准优化方式;torch.multiprocessing
: python 多进程并发,进程之间 torch Tensors 的内存共享;torch.utils
:数据载入器。具有训练器和其他便利功能;torch.legacy(.nn/.optim)
:处于向后兼容性考虑,从 Torch 移植来的 legacy 代码;数字代表的是网络的深度,这里的18指定的是带有权重的18层,包括卷积层和全连接层,不包括池化层和BN层
从Resnet论文中的这张图可以看出,Resnet18由17个卷积层+1个全连接层组成
无论哪一种resnet,除了公共部分(conv1)
外,都是由4大块组成,con2x
, con3x
, con4x
, con5x
我笔记里面提到的动态区就是这4大块,静态区就是conv1
了
1️⃣ BasicBlock 两层的残差块 resnet18/34
class BasicBlock(nn.Module):
"""
"""
def __init__(self, inplanes, planes, stride=1):
super(BasicBlock, self).__init__()
self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=3, stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(planes)
self.relu = nn.ReLU(inplace=True)
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(planes)
# 重要的forward函数
def forward(self, x):
residual = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out += residual
out = self.relu(out)
return out
2️⃣ Bottleneck 三层的残差块 resnet50/101/152
class Bottleneck(nn.Module):
def __init__(self, inplanes, planes, stride=1, downsample=None):
super(Bottleneck, self).__init__()
self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, stride=stride, bias=False)
self.bn1 = nn.BatchNorm2d(planes)
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(planes)
self.conv3 = nn.Conv2d(planes, planes*4, kernel_size=1, bias=False)
self.bn3 = nn.BatchNorm2d(planes*4)
self.relu = nn.ReLU(inplace=True)
def forward(self, x):
residual = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
out += residual
out = self.relu(out)
return out
为什么要用add_module()函数
什么是add_module()函数
可以看到,是Module类的成员函数,输入参数为
Module.add_module(name: str, module: Module)
功能为,为Module添加一个子module,对应名字为name。
怎么用add_module()函数
现在回忆一下,一般定义模型时,Module A的子module都是在A.init(self)
中定义的,比如A中一个卷积子模块self.conv1 = torch.nn.Conv2d(…)
。此时,这个卷积模块在A的名字其实是’conv1’。
对比之下,add_module()函数就可以在A.init(self
以外定义A的子模块。如定义同样的卷积子模块,可以通过A.add_module(‘conv1’, torch.nn.Conv2d(…))
。
以上是给A添加一个子模块。那么删除就是del A.conv1
。而替换同样采用add_module()函数,只要name与被替换模块相同即可完成替换。如之前已经定义了A.conv1,但此时希望将其替换为新的自定义模块NewOne(torch.nn.Module),只需要A.add_module(‘conv1’, NewOne())
即可。
注意事项
如果是替换,只要保证前后(如这里的torch.nn.Conv2d与NewOne)的forward输入输出维度一致,就可以不用改写A.forward()。如果是增减,则需要考虑重写A.forward()
。
如果使用了cuda,并且多卡,需要将model放回cpu后进行结构修改。
例子
class ConvLayer(nn.Module):
def __init__(self):
self.add_module('conv',nn.Conv2d(...))
self.add_module('bn',nn.BatchNorm2d(...))
语法:
torch.nn.functional.interpolate(input, size=None, scale_factor=None, mode='nearest', align_corners=None)
input
– 输入张量
size
– 输出大小
scale_factor
– 指定输出为输入的多少倍数。如果输入为tuple,其也要制定为tuple类型
mode
– 可使用的上采样算法,有’nearest’, ‘linear’, ‘bilinear’, ‘bicubic’ , ‘trilinear’和’area’,默认使用’nearest’
align_corners
– 几何上,我们认为输入和输出的像素是正方形,而不是点。如果设置为True,则输入和输出张量由其角像素的中心点对齐,从而保留角像素处的值。如果设置为False,则输入和输出张量由它们的角像素的角点对齐,插值使用边界外值的边值填充;当scale_factor保持不变时,使该操作独立于输入大小。仅当使用的算法为’linear’, ‘bilinear’, 'bilinear’or 'trilinear’时可以使用。默认设置为False
根据给定的 size 或 scale_factor 参数来对输入进行下/上采样
使用的插值算法取决于参数 mode 的设置
一个均匀分布,一个是标准正态分布。
均匀分布
torch.rand(*sizes, out=None) → Tensor
返回一个张量,包含了从区间[0, 1)的均匀分布中抽取的一组随机数。张量的形状由参数sizes定义。
参数:
sizes
(int…) - 整数序列,定义了输出张量的形状out
(Tensor, optinal) - 结果张量例子:
torch.rand(2, 3)
0.0836 0.6151 0.6958
0.6998 0.2560 0.0139
[torch.FloatTensor of size 2x3]
标准正态分布
torch.randn(*sizes, out=None) → Tensor
返回一个张量,包含了从标准正态分布(均值为0,方差为1,即高斯白噪声)中抽取的一组随机数。张量的形状由参数sizes定义。
参数:
sizes
(int…) - 整数序列,定义了输出张量的形状out
(Tensor, optinal) - 结果张量例子:
torch.randn(2, 3)
0.5419 0.1594 -0.0413
-2.7937 0.9534 0.4561
[torch.FloatTensor of size 2x3]
在分类问题中,通常需要使用max()
函数对softmax
函数的输出值进行操作,求出预测值索引。下面讲解一下torch.max()
函数的输入及输出值都是什么。
torch.max() 返回最大值
1️⃣
torch.max(input) → Tensor
返回输入tensor中所有元素的最大值
a = torch.randn(1, 3)
>>0.4729 -0.2266 -0.2085
torch.max(a)
>>0.4729
2️⃣
torch.max(input, dim, keepdim=False, out=None) -> (Tensor, LongTensor)
按维度dim 返回最大值
输入
input
是softmax函数输出的一个tensor
dim
是max函数索引的维度0/1
,0
是每列的最大值,1
是每行的最大值
输出
- 函数会返回两个
tensor
,第一个tensor
是每行的最大值;第二个tensor
是每行最大值的索引(位置)。
例如,一个tensor a 是这样的:
tensor([[ 1, 5, 62, 54],
[ 2, 6, 2, 6],
[ 2, 65, 2, 6]])
索引每行的最大值:
torch.max(a, 1)
输出:
torch.return_types.max(
values=tensor([62, 6, 65]), # 每一行的最大值分别是62 6 65
indices=tensor([2, 3, 1]) # 他们的位置索引分别是2 3 1
)
torch.logsumexp(input, dim, keepdim=False, out=None)
input
([Tensor] – the input tensor.dim
([int]) or tuple of python:ints) – the dimension or dimensions to reduce. 要减小的尺寸keepdim
([bool]) – whether the output tensor has dim
retained or not. 输出张量是否保持dimout
([Tensor], optional) – the output tensor.这是一个计算数学公式的函数
l o g s u m e x p ( x ) i = l o g ∑ j e x p ( x i j ) logsumexp(x)_i=log\sum_{j}exp(x_{ij}) logsumexp(x)i=logj∑exp(xij)
返回给定维度中输入张量的总指数对数
如果keepdim
为True
,则输出张量的大小与input
相同,但尺寸为dim
的大小为 1。否则,压缩dim
(请参见 [torch.squeeze()
],导致输出张量的尺寸减少 1(或len(dim)
)。
>>> a = torch.randn(3, 3)
>>> torch.logsumexp(a, 1)
tensor([ 0.8442, 1.4322, 0.8711])
torch.save(state, dir)
state
是一个字典,保存三个参数:
state = {‘net':model.state_dict(), 'optimizer':optimizer.state_dict(), 'epoch':epoch}
dir
表示保存文件的绝对路径+保存文件名,如'/home/qinying/Desktop/modelpara.pth'
在GPU中使用torch.cuda
进行训练可以大幅提升深度学习运算的速度. 而且 Torch有一套很好的GPU运算体系.可以大大的提升我们的元算速度,特别是当我们进行大数据的运算时,今天我们来讲解以及分析一下pytorch使用CUDA,视频教程可以参考GPU 加速运算
首先你的电脑里必须得有合适的GPU显卡(NVIDIA),且支持CUDA模块. GPU支持请参考NVIDIA官网
必须安装GPU版的Torch,即安装pytorch时指定了cuda,安装教程参考使用Pip/conda/source在Ubuntu/centos/Mac Os中安装Pytorch v0.2教程
说了这么多,到底如何查看当前环境是否支持CUDA呢?只需要:
print torch.cuda.is_available()
# 返回True代表支持,False代表不支持
pytorch中的torch.cuda基础入门在文档中其实已经讲得很详细了,比如我们使用torch.cuda.synchronize()
可以等待当前设备上所有流中的所有内核完成。同时我们还可以使用NVIDIA工具扩展(NVTX),还有很多用法,这里就不一一介绍了,具体查看torch.cuda中文文档
有很多朋友说在使用GPU和CPU进行运算的过程中(比如GAN),发现使用的时间都差不多;是不是GPU并不比CPU快多少呢?
其实不是这样,如果你运行一个很小的数据模型,那么CPU和GPU的运算速度是差不多的,但是如果你运行大型模型,就可以看到加速效果。我们不能单纯说GPU一定比CPU快,决定因素除了除了我们GPU的配置,还有我们的网络,数据的大小以及数据的类型,有时候GPU运算反而不如CPU快速
举例说明:在使用的情况下,在Titan X GPU
中运行VGG16
比在Dual Xeon E5-2630 v3 CPU
中快66倍
深度学习中我们默认使用的是CPU,如果我们要使用GPU,需要使用.cuda
将计算或者数据从CPU移动至GPU,
如果当我们需要在CPU上进行运算时,比如使用plt
可视化绘图, 我们可以使用.cpu
将计算或者数据转移至CPU.
import torch
from torch.autograd import Variable
# 将变量或者数据移到GPU
gpu_info = Variable(torch.randn(3,3)).cuda()
# 将变量或者数据移到CPU
cpu_info = gpu_info.cpu()
PyTorch中,所有神经网络的核心是 autograd
包。先简单介绍一下这个包,然后训练我们的第一个的神经网络。
autograd
包为张量上的所有操作提供了自动求导机制。它是一个在运行时定义(define-by-run)的框架,这意味着反向传播是根据代码如何运行来决定的,并且每次迭代可以是不同的.
torch.Tensor
是这个包的核心类。如果设置它的属性 .requires_grad
为 True
,那么它将会追踪对于该张量的所有操作。当完成计算后可以通过调用 .backward()
,来自动计算所有的梯度。这个张量的所有梯度将会自动累加到.grad
属性.
另外一个Tensor中通常会记录如下图中所示的属性:
data
: 即存储的数据信息
requires_grad
: 设置为True
则表示该Tensor需要求导
grad
: 该Tensor的梯度值,每次在计算backward时都需要将前一时刻的梯度归零,否则梯度值会一直累加,这个会在后面讲到。
grad_fn
: 叶子节点通常为None,只有结果节点的grad_fn才有效,用于指示梯度函数是哪种类型。例如上面示例代码中的
y.grad_fn=<PowBackward0 at 0x213550af048>, z.grad_fn=<AddBackward0 at 0x2135df11be0>
is_leaf
: 用来指示该Tensor是否是叶子节点。还有一个类对于autograd的实现非常重要:Function
。
Tensor
和 Function
互相连接生成了一个无圈图(acyclic graph),它编码了完整的计算历史。每个张量都有一个 .grad_fn
属性,该属性引用了创建 Tensor
自身的Function
(除非这个张量是用户手动创建的,即这个张量的 grad_fn
是 None
)。
如果需要计算导数,可以在 Tensor
上调用 .backward()
。如果 Tensor
是一个标量(即它包含一个元素的数据),则不需要为 backward()
指定任何参数,但是如果它有更多的元素,则需要指定一个 gradient
参数,该参数是形状匹配的张量。
import torch
创建一个张量并设置requires_grad=True
用来追踪其计算历史
x = torch.ones(2, 2, requires_grad=True)
print(x)
输出:
tensor([[1., 1.],
[1., 1.]], requires_grad=True)
transforms.Compose([transforms.ToTensor(),
transforms.Normalize(std=(0.5, 0.5, 0.5),mean=(0.5, 0.5, 0.5))])
则其作用就是先将输入归一化到(0,1),再使用公式”(x-mean)/std”
0.8 变成 (0.8-0.5)/0.5
经过,这一波操作以后,数据分布到 (-1,1之间了)
前面的(0.5,0.5,0.5)是R G B三个通道上的均值,后面(0.5, 0.5, 0.5)是三个通道的标准差
各有3个是因为处理的是RGB三通道的彩色图像是对数据做归一化
这两个tuple数据是用来对RGB 图像做归一化的
# 一般都有这个 F
import torch.nn.functional as F
def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
torch.nn.Module
中实现layer
的都是一个特殊的类,他们都是以class xxx
来定义的,会自动提取可学习的参数
而nn.functional
中的函数,由def function( )
定义
简单来说,就是functional中的函数是一个确定的不变的运算公式,输入数据产生输出就ok
而深度学习中会有很多权重是在不断更新的,不可能每进行一次forward就用新的权重重新来定义一遍函数来进行计算
如果模型有可学习的参数,最好使用nn.Module对应的相关layer,否则二者都可以使用,没有什么区别
F函数通常不含有weight。 比如relu 就没有weight。
机器学习领域有个很重要的假设:独立同分布假设,**即假设训练数据和测试数据是满足相同分布的。**我们知道:神经网络的训练实际上就是在拟合训练数据的分布。如果不满足独立同分布假设,那么训练得到的模型的泛化能力肯定不好。
再来思考一个问题:为什么传统的神经网络要求将数据归一化(训练阶段将训练数据归一化并记录均值和方差,测试阶段利用记录的均值和方差将测试数据也归一化)?
首先:做了归一化之后,可以近似的认为训练数据和测试数据满足相同分布(即均值为0,方差为1的标准正态),这样一来模型的泛化能力会得到提高。其次:如果不做归一化,使用mini-batch梯度下降法训练的时候,每批训练数据的分布不相同,那么网络就要在每次迭代的时候去适应不同的分布,这样会大大降低网络的训练速度。综合以上两点,所以需要对数据做归一化预处理。PS:如果是mini-batch梯度下降法,每个batch都可以计算出一个均值和方差,最终记录的均值和方差是所有batches均值和方差的期望,当然也有其它更复杂的记录方式,如pytorch使用的滑动平均。
Internal Covariate Shift问题:在训练的过程中,即使对输入层做了归一化处理使其变成标准正态,随着网络的加深,函数变换越来越复杂,许多隐含层的分布还是会彻底放飞自我,变成各种奇奇怪怪的正态分布,并且整体分布逐渐往非线性函数(也就是激活函数)的取值区间的上下限两端靠近。对于sigmoid函数来说,就意味着输入值是大的负数或正数,这导致反向传播时底层神经网络的梯度消失,这是训练深层神经网络收敛越来越慢的本质原因。
为了解决上述问题,又想到网络的某个隐含层相对于之后的网络就相当于输入层,所以BN的基本思想就是:把网络的每个隐含层的分布都归一化到标准正态。其实就是把越来越偏的分布强制拉回到比较标准的分布,这样使得激活函数的输入值落在该激活函数对输入比较敏感的区域,这样一来输入的微小变化就会导致损失函数较大的变化。通过这样的方式可以使梯度变大,就避免了梯度消失的问题,而且梯度变大意味着收敛速度快,能大大加快训练速度。
简单说来就是:传统的神经网络只要求第一个输入层归一化,而带BN的神经网络则是把每个输入层(把隐含层也理解成输入层)都归一化。
BN实际上包含两步操作。 x i x_i xi 是BN的输入, y i y_i yi 是BN的输出。
1.归一化到标准正态, ϵ \epsilon ϵ 是一个非常小的数字,是为了防止除以0
μ = 1 m ∑ i = 1 m x i \mu=\frac{1}{m}\sum\limits_{i=1}^{m}x_i μ=m1i=1∑mxi
σ 2 = 1 m ∑ i = 1 m ( x i − μ ) 2 \sigma^2=\frac{1}{m}\sum\limits_{i=1}^{m}(x_i-\mu)^2 σ2=m1i=1∑m(xi−μ)2
x ^ i ← x i − μ σ 2 + ϵ \hat{x}_i\larr\frac{x_i-\mu}{\sqrt{\sigma^2+\epsilon}} x^i←σ2+ϵxi−μ
以sigmoid函数为例,可以将其近似的看成两个部分,中间区域的线性部分以及两侧的非线性部分。Internal Covariate Shift问题就是:隐含层的输出都落在了sigmoid函数的非线性区域,这部分区域对损失函数的影响极小,所以梯度也极小。归一化操作就是把非线性区域的值拉回到线性区域,这样一来虽然增大了梯度,但也降低了数据的非线性表示能力。所以还需要缩放操作来弥补归一化操作降低的非线性表达能力。归一化从形式上看来就是把输入值减去一个数字,再除以一个数字。它的逆操作就是先乘以一个数字,在加上一个数字,这就是缩放。
2.缩放
y i ← γ x i ˆ + β ≡ B N γ , β ( x i ) y i ← γ x i ^ + β ≡ B N γ , β ( x i ) yi←γxiˆ+β≡BNγ,β(xi)y_i\larr\gamma\hat{x_i}+\beta\equiv BN_{\gamma,\beta}(x_i) yi←γxiˆ+β≡BNγ,β(xi)yi←γxi^+β≡BNγ,β(xi)
Pytorch中的BN操作为
nn.BatchNorm2d(self, num_features, eps=1e-5, momentum=0.1, affine=True, track_running_stats=True)
num_features
,输入数据的通道数,归一化时需要的均值和方差是在每个通道中计算的eps
,用来防止归一化时除以0momentum
,滑动平均的参数,用来计算running_mean和running_varaffine
,是否进行仿射变换,即缩放操作track_running_stats
,是否记录训练阶段的均值和方差,即running_mean和running_varBN层的状态包含五个参数:
weight
和bias
这两个参数需要训练,而running_mean、running_val和num_batches_tracked不需要训练,它们只是训练阶段的统计值。
pytorch中的BN继承自:
class _BatchNorm(Module):
这个类的代码:
class _BatchNorm(Module):
def __init__(self, num_features, eps=1e-5, momentum=0.1, affine=True,
track_running_stats=True):
super(_BatchNorm, self).__init__()
self.num_features = num_features
self.eps = eps
self.momentum = momentum
self.affine = affine
self.track_running_stats = track_running_stats
if self.affine:
self.weight = Parameter(torch.Tensor(num_features))
self.bias = Parameter(torch.Tensor(num_features))
else:
self.register_parameter('weight', None)
self.register_parameter('bias', None)
if self.track_running_stats:
self.register_buffer('running_mean', torch.zeros(num_features))
self.register_buffer('running_var', torch.ones(num_features))
else:
self.register_parameter('running_mean', None)
self.register_parameter('running_var', None)
self.reset_parameters()
def reset_parameters(self):
if self.track_running_stats:
self.running_mean.zero_()
self.running_var.fill_(1)
if self.affine:
self.weight.data.uniform_()
self.bias.data.zero_()
def forward(self, input):
self._check_input_dim(input)
return F.batch_norm(
input, self.running_mean, self.running_var, self.weight, self.bias,
self.training or not self.track_running_stats, self.momentum, self.eps)