torch库的使用(pytorch框架):
在pytorch中,FloatTensor
是基本的数据格式,等同于ndarray
在numpy中的地位。
另一种常用格式是变量Variable
,常用于计算图。
FloatTensor.view
:与Matlab的reshape
类似,对矩阵进行不同维度间的变换
x.view(2, 12)
把x折成2×12的矩阵
x.view(2, -1)
另一种写法,-1表示此维度可以推断出来。由x共24个元素,会自动生成2×12矩阵
在view的时候,有时会因为一连两个元素相同,python认为这个序列是不连续的,后面会出问题从而报错,所以使用x.contiguous()
它进行连续化,连续化后值不变。
Variable(a)
可以把FloatTensor
转换成变量,而a.data又可以把变量转换为Tensor.
a[a:gt(3)] 返回a中大于3的元素
Variable.backward()
:当计算图建立起来之后,利用backward方法可以反向求梯度。标量可以不带参数,默认为backward(torch.Tensor([1.0]))
,表示步长为1。因为我们求梯度求出来的只是导数,后面要梯度下降的时候并不能直接减去一个导数,而是要减去导数乘以步长的积(),也即走一小步。对向量或矩阵求梯度则需要给出一个步长向量或矩阵,与微分变量tensor同维度()
Variable.grad:在因变量调用了backward()
之后,自变量就会有一个grad属性,此即为的值,可以拿来调整各个权重
在网络的forward前向传播方法定义好之后,backward会自动地生成,不用我们自己再写一遍。
注意在测试阶段, 我们是不需要计算梯度的, 也即不需要保留运算过程中的神经元激活模式, 因此我们要在测试代码的循环前面加上一句with torch.no_grad():
来让pytorch不要计算梯度, 这样可以少占用很多显存.
autograd模块的用法
自定义一个继承于torch.autograd.Function的类,可以做一些别样的 forward操作,同时自定义backward方法,下面是一个例子:
class Render(torch.autograd.Function):
@staticmethod
def forward(ctx, x, y):
z = y.clone()
return z
@staticmethod
def backward(ctx, output_grad):
grad = output_grad + 1
return None, grad
forward的输入可以是tensor也可以是其他奇奇怪怪的东西,比如list,你可以在forward函数里面读取图片,然后返回一个Image tensor。只要forward的输入中有一个是requires_grad=True
的,forward的输出就会是requires_grad=True
的。输出的tensor的grad_fn
就是RenderBackward了,就算forward的输入跟输出没有直接关系,我们也可以通过这种方法给它们架起梯度反传的桥梁。backward的时候,输入自然是z的梯度,而输出个数应与forward的输入参数个数相同且位置上一一对应,不要梯度的要给None,不能直接省略。
autograd.Function文档
单个输出值对上一层的导数是一个m维向量v,上一层参数对上上层参数的Jacobian矩阵是个mxn矩阵J,那么根据链式法则,输出对上上层参数的导数就是 J T ⋅ v J^T·v JT⋅v,依然是一个向量,维度为n,继续往上一层传播。
结合上图,y.backward(v)
中的v其实是表示上层参数对y的导数,这个语句将执行类似 J T ⋅ v J^T·v JT⋅v的操作,把梯度往前传。
设置误差计算的方法:
from torch import nn
criterion = nn.MSELoss() # 采用均方误差
criterion = nn.CrossEntropyLoss() # 采用交叉熵损失
等等
接着计算误差:loss = criterion(output, target)
,loss就可以用来backward了. 每个batch的输出一起进入criterion, 输出只是一个数而不是一个向量, 它应该是该batch中各个样本的loss的平均值.
Pytorch的各种内置损失函数
如果刚好没有满足要求的, 想自定义损失函数也是可以的, 只需这样做:
定义自定义损失函数: l o s s = w 1 ∗ l o s s 1 + w 2 ∗ l o s s 2 loss=w_1∗loss_1+w_2∗loss_2 loss=w1∗loss1+w2∗loss2
class myloss(nn.Module):
def __init__(self, w1, w2):
super(myloss, self).__init__()
self.w1 = w1
self.w2 = w2
def forward(self, output1, target1, output2, target2, mse):#mse:最小平方误差函数
loss = self.w1 * mse(output1, target1) + self.w2 * mse(output2, target2)
return loss
只要是从nn.Module中继承而来, 然后有一个forward方法, 就可以作为自定义损失函数啦.
普通的torch.LongTensor和FloatTensor没有梯度属性,需要先将它们转换为变量才能进行计算loss:如图像的标签预先转换:labels = Variable(labels)
;图像在输入网络前预先转换:inputs = Variable(inputs)
,这样outputs = net(inputs)
输出的结果才会是变量,才能求梯度
算出loss之后就可以进行反向传播loss.backward()
,然后更新参数了:optimizer.step()
。如果想要只训练部分层,可以采用如下方法:
for param in model0.parameters():
param.requires_grad = False
optimizer = optim.SGD(
filter(lambda p: p.requires_grad, model.parameters()), # 记住一定要加上filter(),不然会报错
lr=0.01, weight_decay=1e-5, momentum=0.9, nesterov=True)
另外一种实现训练差异的方法是对不同的层设置不同的学习率,详见: finetune冻结层操作 + 学习率超参数设置
似乎如果loss不执行backward方法,torch计算出的中间变量就不会被删除,而是一直堆在那里,会导致out of memory报错。训练在调试时可以不step,但一定要backward。
optimizer在初始化时传入的params必须是个可迭代对象,对于模型而言直接model.parameters()
就可以了,如果是训练对抗样本,需要对输入进行反向传播的话,要这样写(注意到列表也是可迭代对象来着):
# 需要更新参数的记得加上requires_grad=True
render_inputs_var = torch.autograd.Variable(render_inputs_tensor, requires_grad=True)
optimizer = torch.optim.Adam([{'params': render_inputs_var, 'name': 'new-added'}], lr=...)
试过在外面多套了一层:
render_inputs_var = torch.autograd.Variable(render_inputs_tensor, requires_grad=True)
params = iter([ torch.nn.Parameter(render_inputs_var) ])
optimizer = torch.optim.Adam([{'params': params, 'name': 'new-added'}], lr=...)
结果backward
的时候render_inputs_var有梯度,但是不会更新。
在对图片等张量求梯度时候, 发现了关于requires_grad的用法的一些点:
规则1: 只有叶子结点才能修改requires_grad参数. 原因: 以计算图a->b->c为例, 如果a为true, 为了计算a的梯度, bc必须也为true, 不允许修改为false, 因为pytorch中设定为如果计算图的前面的节点有要计算梯度的, 就不能再去改动其后面的结点的requires_grads属性. 但如果a是false, b是可以在后面用一行语句设置为True的, 不是说一定要是整个运算关系图的起始点.
规则2: 其实这可能不应该算是一个规则, 应该算是一个一开始以为是bug的点. 现象是这样的, 首先对一个变量设为true, 然后对其进行修改: x=x.view(1,2,3,4)
, 并加上一句requires_grad=True
, 最后发现x.grad=None. 究其原因, 其实就是我们被自己用同一个变量名生成的新变量混淆了. 一开始设置x为true, 接着对x进行view操作, 得到了一个新的x, 此时这个x是计算图中的第2个节点, 它只是一个中间变量, 真正的叶子结点是一开始定义的那个x, 然后说一下为什么加了requires还是不行, 根据规则1, 本来是无法修改叶子结点往后的requires属性的, 是因为新的x本就已经是true了(因为它的前一个结点是True, 因此它也自动为True), 而作为中间变量是不会保存backward的时候传过来的梯度的, 因此最后的x.grad一定是None. 而最开始的那个x确实是有梯度的, 只是我们用同一个变量名覆盖掉了它的指针, 现在访问不到它了. 如果把上面的语句改成y=x.view(1,2,3,4)
, backward之后x是有梯度的, y没有, 这点在实验中得到了验证.
nn.PixelShuffle()
函数对矩阵进行重排,将尺寸为(, r 2 C r^2C r2C, H, W)的矩阵变换成为尺寸为(, C, rH, rW)的矩阵. “用于实现高效的子像素卷积,步长为1/r”,也可用于用多个低分辨率(尺度)层变换成一张高分辨率图像.
记torch.nn.functional为F,F.affine_grid函数和F.grid_sample函数常用于空间变换网络STN.
Grid = F.affine_grid(theta, size)
theta是一批映射矩阵(Nx2x3),size是变换后图像的尺度(NxCxHxW),其输出grid是一个(NxHxWx2)的矩阵,grid[n, i, j, 0] (记为p)与grid[n, i, j, 1] (记为q)代表的是目标图片第i行j列的像素取用原图像的第p行第q列的像素值。变换前后的H跟W是可以不同。
利用上面生成的映射矩阵,我们可以通过F.grid_sample(x, grid)
函数,通过双线性插值法得到变换后的图像(因为grid中的值通常不会是整数,所以一个目标图像的像素点的值需要原图像的多个像素点综合起来求解,以及可能会出现一些空白区域,因此需要插值操作)。
需要注意的一点是,theta表示的是从目标图像到原图像的仿射变换矩阵,因为这个矩阵是为了服务于“对目标图像的每一个位置,都找到一个原图像中的像素点与其对应”的需求,所以要反过来的变换矩阵,这点需要注意一下。
关于仿射变换矩阵以及grid跟sample的原理
实现例子
另外, 这里用到的theta的角度跟我们平时说的theta的逆时针方向变化的角度不一样, 测试了一下pytorch是顺时针(clockwise)方向变化的角度, 这点也需要注意一下.
Tensor就是多维的矩阵,可以用torch.FloatTensor()
,torch.IntTensor()
等方法创建。
Tensor.squeeze(dim)
将输入张量形状中的1 去除并返回。 如果输入是形如(A×1×B×1×C×1×D),那么输出形状就为: (A×B×C×D),dim缺省为None,当给定dim时,那么挤压操作只在给定维度上。例如,输入形状为: (A×1×B), squeeze(input, 0)
将会保持张量不变,只有用 squeeze(input, 1)
,形状会变成 (A×B)。
Tensor.unsqueeze(dim)
返回一个新的张量,对输入的指定位置插入维度 1
Tensor.permute(dims)
交换维度顺序,dims是个int向量,指明各维度的摆放位置
维度操作: cat、stack、tranpose、permute、unsqeeze
Tensor.repeat(4,2)
与matlab中的repmat类似
Tensor的乘方也是**
在建立好网络模型model(它的父类是nn.Module)之后,model.train()
将模型设定在训练状态,model.eval()
则将模型切换回测试状态
nn.Module.register_buffer()
函数用于给module添加一个持久的缓冲器buffer,常用于保存模型中的中间变量(不是参数),如批归一化中的’running_mean’. 用法是register_buffer(name, value)
,在buffer声明之后,便可以直接通过该变量名来调用该变量了(不需要加self.
)。
tensor.random(a, b)
会用[a, b)间的随机整数填充整个张量
Tensor.select函数用于选出某个维度里的某个张量矩阵,用法是select(dim, index)
,效果等价于x[:, index, :]
,例如:
x = torch.ones([2, 3, 4])
x.select(2,3)
将返回张量x[:, :, 3]
Tensor.narrow
函数等效于:
切片符,用法是narrow(dimension, start, length)
,例如:
x = torch.ones([2, 3, 4])
x.narrow(2, 1, 2)
将返回张量x[:, :, 1:3], 尺寸为[2, 3, 2]
Tensor.clone()
克隆一个张量
Tensor.fill_(value)
给张量里的元素填充同一个值
Tensor.random(a, b)
会用[a, b)间的随机整数填充整个张量
Tensor.resize_(dim1, dim2,...)
调整张量的结构
Tensor.view(dim1, dim2,...)
输出一个新的张量,是原张量调整结构后得到的,可以用-1代替某个维度(resize_不行)
Tensor.item()
方法可以把一个单元素Tensor转换成python的Float类型. 当我们在累计测试误差以及正确率等时,因为这个一般是直接correct=0,是Float,而torch计算出来的误差等数值都是Tensor,所以需要这一步转换.
Tensor.expand()
函数有点类似np.tile
或是matlab的repmat
函数,用法是a.expand(new_dim1, new_dim2, ...)
,没有发生变换的维度的数据将保持不变,变换了的维度将被复制填充。
b.view_as(a)
方法可以把b按照a的shape进行转换
torch.rsqrt(a)
返回平方根的倒数
torch.mean std prod sum var tanh max min(input)
返回均值 标准差 累乘 求和 方差 双曲正切 最大 最小值
torch.equal(Tensor1,Tensor2)
两个张量进行比较,如果相等返回true,否则返回false
torch.bmm(a, b)
执行两个张量之间的批矩阵间乘积( batch matrix-matrix product),记a.shape=[ B a , H a , W a B_a,\ H_a,\ W_a Ba, Ha, Wa],b.shape=[ B b , H b , W b B_b,\ H_b,\ W_b Bb, Hb, Wb] ,则应满足 B a = B b , W a = H b B_a=B_b,\ W_a=H_b Ba=Bb, Wa=Hb。
torch.topk()
函数用于返回张量中最大/最小的k个值,用法是topk(input, k, dim=None, largest=True, sorted=True, out=None)
。返回一个(values, indices)元组
torch.cat((a, b), dim)
把张量a跟张量b在维度dim上连接起来
torch.save(model, model_out_path)
可以将训练好的模型保存下来,一般保存为.pt文件
torch.load(filename)
方法可以导入之前保存下来的模型
torch.full(size, fill_value)
根据size所给的尺寸,生成一个元素均填充为fill_value的张量
torch.max()
返回最大值,如果不传dim,则只会返回一个最大值标量,而传入dim之后,除了返回一个最大值数组,还会多返回一组最大值对应的索引数组
torch.gather
函数:gather(input, dim, index, out=None)
用于在dim维度,根据index中的下标值,对input进行寻址并输出,输出output, input跟index的尺寸都是相同的。
Tensor
类里面有一些函数,除了a.func
外还有一个很相似的a.func_
,后者称为In-place版本,表示对张量a进行该操作,修改了a的值;前者是利用a进行运算,然后把结果赋给另一个张量,所以它们的调用方式不一样,分别是b = a.func(p)
跟a.func_(p)
。
PyTorch中Tensor的查找和筛选:index_select, where, equal等
torch.manual_seed()
函数可以为CPU设置设定pytorch使用到的随机数种子, 可以控制各种分布(如均匀分布)以及数据集洗牌的顺序是一个固定的序列, 以使多次训练的过程中不发生较大的变化(到同一个epoch时会得到同样的模型). 输入是int
或long
.
如果是在GPU上运行模型, 则需要使用torch.cuda.manual_seed(args.seed)
函数来为当前GPU设置随机种子;如果使用多个GPU,应该使用torch.cuda.manual_seed_all()
为所有的GPU设置种子。
从Tensor
的is_cuda
属性可以看到一个变量是否已被.cuda()
过
在main.py中设置好的种子可以影响到其调用的函数里面的数据抽取过程,如
torch.manual_seed(777)
for j in range(4):
for i, data in enumerate(train_loader_source):
inputs, labels = data
print(i, inputs.sum())
print('')
跟把训练(这里只写了数据抽取)的代码放到另外一个py文件中再去调用:
from test import test
torch.manual_seed(777)
test(train_loader_source)
会得到相同的输出。
基本语句结构:
class Share_convs(nn.Module): # 首先建立一个继承自nn.Module的类
def __init__(self):
super(Share_convs, self).__init__() # 不要忘了初始化
# 建立模型
def forward(self, x):
# 前向传播,获得输出
return out
建立模型有多种方法:
# 一、简单粗暴式,独立写出每一个网络组件,建立模型的代码这么写:
x = self.conv1(x)
x = self.prelu(x)
x = self.conv2(x)
# 前向传播的代码这么写:
out = self.conv1(x)
out = self.prelu(out)
out = self.conv2(out)
out = self.prelu(out)
# 此时生成的网络组件名字就分别叫做conv1, prelu和conv2,训练中若需要调整对应的梯度值,可以通过名字
# 找到对应的参数。Sequential里有4个组件,名字是按计算流的顺序起的,如0,1,2,3。
# 二、Sequential式,一次性列出所有组件,建立模型这么写:
self.model= nn.Sequential(
nn.Conv2d(1,20,5),
nn.PReLU(),
nn.Conv2d(20,64,5),
nn.PReLU()
)
# 前向传播只需一行语句:
out = self.model(x)
# 跟方法一相比,失去了外面的3个独立组件。目前尚不明确是按外面的独立组件名称来寻找还是按Sequential里面的名称来寻找
# 三、命名Sequential式,Sequential里的每个组件都可以有自己的名字,建立模型这么写:
# 需要导入OrderedDict模块
from collections import OrderedDict
self.model = nn.Sequential(OrderedDict([
('conv1', nn.Conv2d(1,20,5)),
('prelu1', nn.PReLU()),
('conv2', nn.Conv2d(20,64,5)),
('prelu2', nn.PReLU())
]))
# 前向传播只需一行语句:
out = self.model(x)
# 四、ModuleList辅助式,一次性建立多个相同结构,其主要作用是节省代码量,建立模型这么写:
self.model = nn.ModuleList([nn.Linear(10, 10) for i in range(10)])
# 前向传播使用一个for循环:
# ModuleList can act as an iterable, or be indexed using ints
for i, l in enumerate(self.linears):
x = self.linears[i // 2](x) + l(x)
# 可以通过print(model.parameters)语句来查看网络参数的情况(名称,位置等)
需要注意的是,如果OrderedDict里面有两个组件名字重复了,那么Sequential的建立结果很可能与我们的预期不同。若第二个conv组件改名为conv1,则在Sequential里,只会有一个卷积层,处于最开头的位置,且为nn.Conv2d(20,64,5),也即是说第三行的conv1设置对第一行的conv1设置做了一个刷新(覆盖),而不是我们所想的还会有两个卷积层。prelu也是一样的道理,重名之后就只剩一个激活层了。而在Sequential外面,由于两个PReLU组件结构完全相同且不可学习,pytorch将他们归为同一个组件,称为prelu;即使Sequential里面卷积层被覆盖了,外面仍然有conv1, conv2两个分别为nn.Conv2d(1,20,5)和nn.Conv2d(20,64,5)的卷积层。
在我们建立好一个网络并实例化之后: net = Net()
网络就有了一些元素, 比如说:
net.state_dict()
类型: Orderdict(有序字典)
作用: 保存模型的参数
比如说, 我们有一个两层的网络, 在Net
类里我们定义的网络模型变量叫model
, 那么state_dict()的内容就会是:
Key: ‘model.0.weight’ Value: 第一层网络的权重张量
Key: ‘model.0.bias’ Value: 第一层网络的偏置张量
Key: ‘model.1.weight’ Value: 第二层网络的权重张量
…
以此类推.
键在有序字典里的排列顺序是按首字母来排的, 不是按网络中的实际先后顺序排的, 这一点需要注意.
通过update方法可以使用已有的参数字典对模型进行更新:
model_dict = net.state_dict()
model_dict.update(pretrained_dict)
net.load_state_dict(model_dict)
注意如果pretrained_dict里存在model_dict没有的键值对, 它也会添加进model_dict里, 最后就会造成model_dict变大了不跟原来的网络匹配了, 在装载回原网络时就会出错, 所以应该先对pretrained_dict进行筛选:
model_dict = net.state_dict()
# 只保留pretrained_dict中与model_dict共有的部分
pretrained_dict_temp = {k: v for k, v in pretrained_dict.items() if k in model_dict}
model_dict.update(pretrained_dict_temp)
net.load_state_dict(model_dict)
如果是想把预训练的特征提取网络,用在自己的分类任务上,则一般需要修改最后的输出单元数:
import torchvision.models as models
model = models.resnet50(pretrained=True)
in_features = model.fc.in_features
model.fc = nn.Linear(in_features, my_out_features)
另一种方法是调用model.children()方法,截掉部分网络(原文地址):
# 去掉最后两层
resnet_layer = nn.Sequential(*list(model.children())[:-2]) # 但是此法对于某些结构的网络(如Inception-v3)似乎不奏效,会把原来的网络连接顺序搞乱,导致维度异常或者结果错误
记变量model为一个网络实例:model = Model(args)
当我们建立的网络中包含有权重共享的部分的时候,为了确保网络建立的正确性,可以查看一下网络里面的各个参数。两个网络各个组件的名字的是一样的,这点不能作为区分,但是它们共享的部分权重应该相同,不共享的部分在进行了随机初始化之后应该不同,为了方便查看可以直接输出参数张量的和而不是整个张量:
for name, param in G1.named_parameters():
print(name, param.sum())
for name, param in G2.named_parameters():
print(name, param.sum())
f = net.parameters()
返回整个网络的参数,为torch.nn.parameter.Parameter类型;
f.data是torch.FloatTensor类型
torch在建立网络后会以默认方式初始化一遍,如果想应用其他初始化方法,可以在后面自己再进行修改。
model.modules()返回网络里面的所有组件。重复的组件(如两个一样尺寸的Linear)将只被返回一个。
model.apply(fn)
方法是个利器,它递归地将某个函数fn
应用到网络的每一个组件上,例:
def init_weights(m): # 一个初始化权重的函数
print(m)
if type(m) == nn.Linear: # 是FC层就初始化权重为1
m.weight.data.fill_(1.0)
print(m.weight)
net = nn.Sequential(nn.Linear(3, 3), nn.Linear(2, 2))
net.apply(init_weights)
网络的结构长这样:
net(Sequential)
│ nn.Linear(3, 3)
│ nn.Linear(2, 2)
apply的输出结果为(注释内容是为了方便理解,不是程序输出的内容):
Linear(in_features=3, out_features=3, bias=True) # init_weights函数往下一钻,先遇到了最底下的nn.Linear(3, 3)
Parameter containing:
tensor([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]], requires_grad=True)
Linear(in_features=2, out_features=2, bias=True) # 接着来到nn.Linear(2, 2),应该是深度优先搜索
Parameter containing:
tensor([[1., 1.],
[1., 1.]], requires_grad=True)
# 最后去访问上层的Sequential,它不是Linear,故不做初始化也不输出weight
Sequential(
(0): Linear(in_features=3, out_features=3, bias=True)
(1): Linear(in_features=2, out_features=2, bias=True)
)
pytorch中获取模型input/output shape
挂钩(直译)大法,挂在那里,每次有数据来就调用一次。其寓意也许是人来回走动的时候,碰到了一下“挂钩”就会启动连锁反应。
下面称组件为module,组件视情况不同,可能为一个conv, fc层,也可能是一个Sequential,还可能是一整个model。
module.register_forward_hook(hook)
给该组件上挂钩,每次前向传播完(也即执行完module.forward())之后会调用一次hook函数,hook的参数表必须是hook(module, input, output),且返回值必须是None(python里面不写return语句默认就是返回None)
input是组件的输入,是一个tuple,如果网络只有一个输入,那就是一个只有1个元素的tuple,保存着组件的输入张量;output是组件的输出,当网络只有一个输出时是个Tensor,有多个输出的情况尚未知晓,待补充。
经人肉实验,得到了一些实验结果:
forward_hook的遍历顺序:先深搜到达最底层最前面的组件,然后如果它位于一个Sequential中,则遍历完Sequential里面的组件,之后访问Sequential,之后访问Sequential的父亲,再下去访问其父亲下面的底层组件,以此类推。(Sequential有不止一个兄弟的情况尚未实验过,不知道其父亲是否会被多次访问,或者是否会在访问完其所有兄弟之后再去访问父亲,还是先访问父亲再去访问各个兄弟,访问兄弟时不再访问父亲。待补充)
backward_hook的遍历顺序:
backward_hook中的hook函数的参数表必须是hook(module, grad_input, grad_output)
,这里解释一下后面两项:grad_input保存的是计算梯度所需的张量,对于一个Conv2d组件来讲,grad_input是一个3元素tuple,其中grad_input[0].size=output.size()(这里的output是指forward_hook的hook函数里面的output,也即该组件的输出张量),包含后一个组件传过来的梯度;grad_input[1].size=weight.size(),包含的是(有点问题,尚未完成)
module还可以挂register_forward_pre_hook(在前向传播之前)、register_backward_hook(在反向传播之后)。
用于module的反向hook详解
参数表:hook(module, grad_input, grad_output)
module
指的是即将被挂上挂钩的组件。这里的"input"和"output"是相对于前向传播而言的,grad_input
不是指梯度的输入,而是指输入张量的梯度。同理,grad_output
是指输出张量的梯度,它来自更靠近Loss的上一个组件。grad_input
是3个元素,而grad_output
是1个元素。grad_output
从Loss的方向传回来,与组件的参数(权重,偏置等)做计算,就得到了grad_input
。grad_output[0]
用于运算之后就被丢弃了,grad_input[1]
, grad_input[2]
是对权重和偏置的梯度,仅在本组件中起作用,grad_input[0]
是对输入图像的梯度,它将继续往前传,也即如果修改了grad_input[0]
,前面所有组件的梯度都会受到影响,而修改grad_input[1]
, grad_input[2]
和grad_output[0]
则不会。grad_input[0]
是个 b a t c h _ s i z e × C o u t batch\_size×C_{out} batch_size×Cout矩阵,逐行求和即得到每个偏置的梯度;grad_input[1]
是准备传给下一层的图像的梯度,grad_input[2]
是权重的梯度。v = torch.tensor([0., 0., 0.], requires_grad=True)
h = v.register_hook(lambda grad: grad * 2) # double the gradient
v.backward(torch.tensor([1., 2., 3.]))
backward时直接使用一个张量为v.grad赋值,赋值完成后hook启动,将新增加的梯度翻倍(不是g,而是 δ g \delta_g δg,因此v.grad最后为[2,4,6]。
挂钩可以叠加,如果重复执行第二个语句,以后翻倍就是翻4倍,再执行一次就翻8倍。
使用h.remove()
语句来移除这个挂钩。如果叠加了多次,那就要多remove几次,remove一次去掉一层。
torch自己下载的预训练网络参数文件一般会保存在家目录的.torch文件夹下
把模型和数据搬上显卡: https://zhuanlan.zhihu.com/p/31936740
在python xx.py
前加上一个CUDA_VISIBLE_DEVICES=1, 可以限制模型只在某张显卡上运行。如果在run.sh里面指定了GPU,而内层在os.system(command)
时不指定,那么内层也会默认只使用外层指定的GPU;如果想要内层能使用多个GPU,则在内层指令里面再次添加CUDA_VISIBLE_DEVICES=x,y,z
即可
requires_grad 与 detach 区别&梯度传递细节
pytorch使用多gpu进行运算只需一行代码:model = nn.DataParallel(Model(args))
指定某几个GPU, 可将CUDA_VISIBLE_DEVICES=0
修改为CUDA_VISIBLE_DEVICES=0,2,3
,或者在代码中设置os.environ["CUDA_VISIBLE_DEVICES"] = '12,13'
Pytorch 多 GPU 并行处理机制
DataParallel 的行为&均衡负载
DataParallel包装的模型在保存时,权值参数前面会带有module字符,因此多GPU训练的模型读到单gpu上时,读取代码需要做点小改动:
# 单GPU训练读取模型
checkpoint = torch.load(save_path)
model.load_state_dict(checkpoint['model_state_dict'])
# 多GPU训练的模型读到单gpu上(多GPU上的模型 直接把网络键名里的'module.'去掉)
state_dict = torch.load(save_path)
from collections import OrderedDict
new_state_dict = OrderedDict()
for k, v in state_dict.items():
namekey = k[7:] # remove `module.`
new_state_dict[namekey] = v
model.load_state_dict(new_state_dict)
在cpu/gpu上加载预先在gpu/cpu上训练好的模型
# 把在GPU上训练好的模型加载到CPU上
model = torch.load(model_loadpath, map_location=torch.device('cpu'))
torchvision包的三个用途:
torchvision.transforms
文档地址
transforms.Resize(400) # Resize函数将图像的短边放缩到400像素
transforms.Resize((400,300)) # 将图像放缩成高400宽300像素
transforms.RandomCrop(200) # 中心点随机的裁剪,切出200x200的小块(HxW)
transforms.RandomCrop((300,200)) # 切出300x200的小块
对torch.utils.data.Dataset和torch.utils.data.DataLoader的介绍(好文)
有趣的是torch.utils.data.DataLoader在刚导入torch包时是不存在的, 而在import torchvision
语句之后, 便多出了一些属性, 包括这个DataLoader. 所以如果没有导入torchvision包, 是没有办法使用DataLoader的, 需要注意一下.
常见的两种数据集形式:
torch.utils.data.DataSet
类就行,见下文)torchvision.datasets.ImageFolder(‘image_dir_root’ )
)第二类数据集的目录结构如下:
root/ants/xxx.png
root/ants/xxy.jpeg
root/ants/xxz.png
.
.
.
root/bees/123.jpg
root/bees/nsdf3.png
root/bees/asd932_.png
上面关于torchivision库的介绍转自:https://blog.csdn.net/Hungryof/article/details/76649006
torchvision数据集(如torchvision.datasets.ImageFolder
或torchvision.datasets.CIFAR10
等)的输出是在[0, 1]范围内的PILImage图片。
torch.utils.data.Dataset
是一个抽象类, 自定义的Dataset需要继承它并且实现三个成员方法.
SR问题的label是一张图片,这时只能靠自定义Dataset了:https://www.cnblogs.com/denny402/p/7512516.html
自定义Dataloader读取数据(待补充)
pytorch中的default loader代码如下:
def default_loader(path):
from torchvision import get_image_backend
if get_image_backend() == 'accimage':
return accimage_loader(path)
else:
return pil_loader(path)
通常我们会对dataloader进行枚举:for i, data in enumerate(dataloader):
data里面就自动包含了数据和对应的标签了(我猜是因为我们告诉ImageFolder每一个文件夹是一个类, 所以在loader的时候它就知道要按图片是从哪个子文件夹来的来给出标签), 而不是说输入数据要一个dataloader, 标签要一个dataloader.
torch.utils.data.DataLoader()中的shuffle=True是指, 在每一个epoch开始时, dataloader会进行一次洗牌, 使得每个epoch里面图片的排列顺序都是不一样的; 而shuffle=False就是在第一个epoch开始时进行一次洗牌, 后面就一直按着这排列顺序来训练. 不管是True还是False, 都会有一次洗牌, 不会说按照文件路径的顺序直接来. 另外一个可能有用的参数是drop_last, 默认是False,即保留最后一个批,这个批的样本数通常不足batch_size,如果设为True则直接丢掉这个批。(我之前是自己写的这个操作,后来看了别人的代码才发现DataLoader自带了这个操作)
dataloader貌似只能返回Tensor
,ndarray
跟int float
的向量,list
跟dict
都没有办法正常地传出来,出来的长度总是1。所以如果真的需要传list
跟dict
,要考虑先转成ndarray
。如果是ndarray
,出来的时候pytorch会自动把它转成Tensor
,这点需要注意一下
dataloader的num_worker
参数控制的是准备数据的线程数,从实验的输出来看,pytorch貌似是有个缓冲区,会放置4*num_workers
的图片等待被读取,而不是每次要数据的时候才开始运行多个线程读取数据。
顺带一提,在跑模型的时候去ps -aux | grep username
,将会看到有不止一个COMMAND
值为我们写的python xx.py
语句的进程,其中一个TIME值很高,其他的都不高且接近。那些不高且接近的进程其实就是dataloader里面分出来的负责读取数据的workers,我们设置的num_workers是多少,就会看到多少个这种进程。因为它们只在读取数据时才工作,所以总的运行时间并不长,TIME值最高的就是主代码了,基本上一直都在运行,TIME值跟真实世界流逝的时间可能比较接近。
查看NVIDIA 显卡 nvidia-smi
查看CUDA版本nvcc -V
选择训练时使用的GPU:
import os os.environ["CUDA_VISIBLE_DEVICES"] = "N" (N=0,1,2,...)
Pytorch的包名是torch,导入时应import torch
而不是pytorch
!
在一个语句前面加上CUDA_VISIBLE_DEVICES=N
(N=0,1,2,…),可以限制这个语句后面的命令在第N个GPU上运行,不会乱跑到其他GPU上干扰别人工作
有时候训练到一半, 命令行窗口突然没有了动静, 很长一段时间内没有输出新东西, 光标也不会闪, 这个时候可能是程序卡死了, 按下Ctrl+C
或许能够起死回生让它继续运行, 否则就只能重新来过了.
自动查找并使用空闲GPU(原文地址):
# 查看GPU memory,并将结果保存在tmp中
os.system('nvidia-smi -q -d Memory |grep -A4 GPU|grep Free >tmp')
# 读取gpu memory
memory_gpu=[int(x.split()[2]) for x in open('tmp','r').readlines()]
# 求剩余memory最多的显卡号,并设置CUDA_VISIBLE_DEVICES为该显卡
os.environ['CUDA_VISIBLE_DEVICES']=str(np.argmax(memory_gpu))
os.system('rm tmp')
Tensorboard 是一个动态可视化数值的工具,同时也能可视化静态的神经网络结构。
Tensorboard 包含两部分功能:
第一部分功能,以python包形式存在。编程者 import tensorboard
从而使用API将动态的数值以protocol buffer格式,不断地写入文件。
第二部分功能,以可执行程序形式存在。这个程序在win下叫 tensorboard.exe
,linux下叫 tensorboard
。该程序是一个web服务器,能够不停地读取本地文件,查询是否有新数值要展示,再应答给网页。(转自:https://blog.csdn.net/u010469993/article/details/80950510 )
安装:
pip install tensorboard
pip install tensorboardX
第一步 创建文件写控制器, 将数值序列写入文件
第二步 在命令行里使用tensorboard读取protocol buffer 格式的数值:
tensorboard --logdir logs --port 8888
分别使用一个终端来完成.
具体的操作方法:
from tensorboardX import SummaryWriter
# 首先创建一个文件写控制器
writer = SummaryWriter(log_dir='logs')
# 注: 下面的代码只是一些零散的示例, 不是一段完整的能运行的代码
# 在写入变量之前应先计算出它的值, 然后再add
# 写入一个标量(注意, 该操作不是一次性的, 而是在每次迭代里面都要写一次, 相当于一次加一笔上去)
writer.add_scalar('data/x', x, epoch) # 展示为一个图里一条线
# 写入一组标量, 展示为一个图里多条线
writer.add_scalars('data/scalar_group', {'x': x,
'y': y,
'loss': loss}, epoch)
# 插入一个直方图, 在HISTOGRAM菜单中显示
writer.add_histogram('zz/x', x, epoch)
# 插入提示文本, 在TEXT菜单中显示
writer.add_text('zz/text', 'zz: this is epoch' + str(epoch), epoch)
# 把标量写入到JSON文件中, 以供外部处理
writer.export_scalars_to_json("./test.json")
# 最后记得关闭控制器
writer.close()
tensorboard 出现locale.Error: unsupported locale setting错误:在~/.bashrc中写入export LC_ALL="en_US.UTF-8"
(原文链接)
torch.autograd
里的Function
模块可以用来自定义对变量Variable的操作, 也可以用来自定义一个层置于模型中.
导入: from torch.autograd import Function as Function
使用:
class layer(Function):
@staticmethod
def forward(ctx, x, alpha):
ctx.alpha = alpha
return x.view_as(x)
@staticmethod
def backward(ctx, grad_output):
output = grad_output.neg() * ctx.alpha
return output, None
注意到静态方法不需要实例, 因此函数的参数表里没有self这个参数. 调用该层时不是直接layer.forward()
, 而是通过一个从Function类继承下来的apply函数, 该函数会自动执行forward和backward的操作. 要注意的是它还会传入自身作为一个参数, 就像是普通类一样. 而forward本不需要这个参数, 为了保持跟普通类的格式相同, 于是又定了一个参数, 叫ctx, 可能是context的意思(ctx不是关键字). 实际调用时只需传入两个参数, 即layer.apply(x, alpha)
一些变更
Variable的.creator属性用于查找变量在计算图中的父节点,在2017年5月被修改为.grad_fn
在0.4.0及以后的版本中,已不需要Variable函数, 如果一个tensor需要求导, 直接设置.requires_grad=True即可.
check_grad
函数时却会报错:AttributeError: ‘NoneType’ object has no attribute ‘abs’__getitem__
里面把变量的requires_grad设为True,stack()
, cat()
等函数不允许把多个需要梯度的变量这样堆叠到一起expand
方法,它相当于浅拷贝出了很多个数据,改一个就会牵连到其他tensor.repeat
,这是深拷贝,各个元素之间不会互相影响loss.backward
的时候报错对同一个变量进行了多次backward,并提示应使用retain_graph=True