汇总 Pytorch 踩过的10个坑

极市导读
本文总结了10大Pytorch操作中的“坑”,能够帮助使用者规避不必要的麻烦。>>>极市七夕粉丝福利活动:炼丹师们,七夕这道算法题,你会解吗?
pytorch中的交叉熵
pytorch的交叉熵nn.CrossEntropyLoss在训练阶段,里面是内置了softmax操作的,因此只需要喂入原始的数据结果即可,不需要在之前再添加softmax层。这个和tensorflow的tf.softmax_cross_entropy_with_logits如出一辙.[1][2]pytorch的交叉熵nn.CrossEntropyLoss在训练阶段,里面是内置了softmax操作的,因此只需要喂入原始的数据结果即可,不需要在之前再添加softmax层。这个和tensorflow的tf.softmax_cross_entropy_with_logits如出一辙.[1][2]
pytorch中的MSELoss和KLDivLoss
在深度学习中,MSELoss均方差损失和KLDivLossKL散度是经常使用的两种损失,在pytorch中,也有这两个函数,如:
loss = nn.MSELoss()input = torch.randn(3, 5, requires_grad=True)target = torch.randn(3, 5)output = loss(input, target)output.backward()
这个时候我们要注意到,我们的标签target是需要一个不能被训练的,也就是requires_grad=False的值,否则将会报错,出现如:
AssertionError: nn criterions don’t compute the gradient w.r.t. targets - please mark these variables as volatile or not requiring gradients
我们注意到,其实不只是MSELoss,其他很多loss,比如交叉熵,KL散度等,其target都需要是一个不能被训练的值的,这个和TensorFlow中的tf.nn.softmax_cross_entropy_with_logits_v2不太一样,后者可以使用可训练的target,具体见[3]
在验证和测试阶段取消掉梯度(no_grad)
一般来说,我们在进行模型训练的过程中,因为要监控模型的性能,在跑完若干个epoch训练之后,需要进行一次在验证集[4]上的性能验证。一般来说,在验证或者是测试阶段,因为只是需要跑个前向传播(forward)就足够了,因此不需要保存变量的梯度。保存梯度是需要额外显存或者内存进行保存的,占用了空间,有时候还会在验证阶段导致OOM(Out Of Memory)错误,因此我们在验证和测试阶段,最好显式地取消掉模型变量的梯度。 在pytroch 0.4及其以后的版本中,用torch.no_grad()这个上下文管理器就可以了,例子如下:
model.train()# here train the model, just skip the codesmodel.eval() # here we start to evaluate the modelwith torch.no_grad():for each in eval_data:data, label = eachlogit = model(data)... # here we just skip the codes
如上,我们只需要在加上上下文管理器就可以很方便的取消掉梯度。这个功能在pytorch以前的版本中,通过设置volatile=True生效,不过现在这个用法已经被抛弃了。
显式指定model.train()和model.eval()
dropout[6]中的丢弃率和Batch Normalization[5]中的和等,这个时候我们就需要显式地指定不同的阶段(训练或者测试),在pytorch中我们通过model.train()和model.eval()进行显式指定,具体如:model = CNNNet(params)# here we start the trainingmodel.train()for each in train_data:data, label = eachlogit = model(data)loss = criterion(logit, label)... # just skip# here we start the evaluationmodel.eval()with torch.no_grad(): # we dont need grad in eval phasefor each in eval_data:data, label = eachlogit = model(data)loss = criterion(logit, label)... # just skip
关于retain_graph的使用
pytorch中调用out.backward()即可实现,给个小例子如:import torchimport torch.nn as nnimport numpy as npclass net(nn.Module):def __init__(self):super().__init__()self.fc1 = nn.Linear(10,2)self.act = nn.ReLU()def forward(self,inputv):return self.act(self.fc1(inputv))n = net()opt = torch.optim.Adam(n.parameters(),lr=3e-4)inputv = torch.tensor(np.random.normal(size=(4,10))).float()output = n(inputv)target = torch.tensor(np.ones((4,2))).float()loss = nn.functional.mse_loss(output, target)loss.backward() # here we calculate the gradient w.r.t the leaf
loss进行反向传播就可以求得,即是损失对于每个叶子节点的梯度。我们注意到,在.backward()这个API的文档中,有几个参数,如:backward(gradient=None, retain_graph=None, create_graph=False)
retain_graph这个参数,这个参数如果为False或者None则在反向传播完后,就释放掉构建出来的graph,如果为True则不对graph进行释放[7][8]。import torchfrom torch.autograd import Variablea = Variable(torch.rand(1, 4), requires_grad=True)b = a**2c = b*2d = c.mean()e = c.sum()

d进行求梯度,我们有:d.backward()
e进行求梯度,那么将会因为没有这个graph而报错。因此有例子:d.backward(retain_graph=True) # finee.backward(retain_graph=True) # fined.backward() # also finee.backward() # error will occur!
loss,例子如:G_loss = ...D_loss = ...opt.zero_grad() # 对所有梯度清0D_loss.backward(retain_graph=True) # 保存graph结构,后续还要用opt.step() # 更新梯度,只更新D的,因为只有D的不为0opt.zero_grad() # 对所有梯度清0G_loss.backward(retain_graph=False) # 不保存graph结构了,可以释放graph,# 下一个迭代中通过forward还可以build出来的opt.step() # 更新梯度,只更新G的,因为只有G的不为0
loss进行分步的训练了。进行梯度累积,实现内存紧张情况下的大batch_size训练
retain_graph参数中,还可以用于累积梯度,在GPU显存紧张的情况下使用可以等价于用更大的batch_size进行训练。首先我们要明白,当调用.backward()时,其实是对损失到各个节点的梯度进行计算,计算结果将会保存在各个节点上,如果不用opt.zero_grad()对其进行清0,那么只要你一直调用.backward()梯度就会一直累积,相当于是在大的batch_size下进行的训练。我们给出几个例子阐述我们的观点。import torchimport torch.nn as nnimport numpy as npclass net(nn.Module):def __init__(self):super().__init__()self.fc1 = nn.Linear(10,2)self.act = nn.ReLU()def forward(self,inputv):return self.act(self.fc1(inputv))n = net()inputv = torch.tensor(np.random.normal(size=(4,10))).float()output = n(inputv)target = torch.tensor(np.ones((4,2))).float()loss = nn.functional.mse_loss(output, target)loss.backward(retain_graph=True)opt = torch.optim.Adam(n.parameters(),lr=0.01)for each in n.parameters():print(each.grad)
tensor([[ 0.0493, -0.0581, -0.0451, 0.0485, 0.1147, 0.1413, -0.0712, -0.1459,0.1090, -0.0896],[ 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,0.0000, 0.0000]])tensor([-0.1192, 0.0000])
loss.backward(retain_graph=True),输出为:tensor([[ 0.0987, -0.1163, -0.0902, 0.0969, 0.2295, 0.2825, -0.1424, -0.2917,0.2180, -0.1792],[ 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,0.0000, 0.0000]])tensor([-0.2383, 0.0000])
tensor([[ 0.1480, -0.1744, -0.1353, 0.1454, 0.3442, 0.4238, -0.2136, -0.4376,0.3271, -0.2688],[ 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,0.0000, 0.0000]])tensor([-0.3575, 0.0000])
opt.zero_grad(),输出为:tensor([[0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],[0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]])tensor([0., 0.])
opt.zero_grad()了吧,那是为什么不要这次的梯度结果被上一次给影响,但是在某些情况下这个‘影响’是可以利用的。调皮的dropout
torch.nn.functional.dropout的时候,其参数为:torch.nn.functional.dropout(input, p=0.5, training=True, inplace=False)
training指明了是否是在训练阶段,是否需要对神经元输出进行随机丢弃,这个是需要自行指定的,即便是用了model.train()或者model.eval()都是如此,这个和torch.nn.dropout不同,因为后者是一个层(Layer),而前者只是一个函数,不能纪录状态[9]。嘿,检查自己,说你呢, index_select
torch.index_select()是一个用于索引给定张量中某一个维度中元素的方法,其API手册如:torch.index_select(input, dim, index, out=None) → TensorParameters:input (Tensor) – 输入张量,需要被索引的张量dim (int) – 在某个维度被索引index (LongTensor) – 一维张量,用于提供索引信息out (Tensor, optional) – 输出张量,可以不填
1000 * 10的尺寸大小,其中1000为样本数量,10为特征数目,如果我现在需要指定的某些样本,比如第1-100,300-400等等样本,我可以用一个index进行索引,然后应用torch.index_select()就可以索引了,例子如:>>> x = torch.randn(3, 4)>>> xtensor([[ 0.1427, 0.0231, -0.5414, -1.0009],[-0.4664, 0.2647, -0.1228, -1.1068],[-1.1734, -0.6571, 0.7230, -0.6004]])>>> indices = torch.tensor([0, 2])>>> torch.index_select(x, 0, indices) # 按行索引tensor([[ 0.1427, 0.0231, -0.5414, -1.0009],[-1.1734, -0.6571, 0.7230, -0.6004]])>>> torch.index_select(x, 1, indices) # 按列索引tensor([[ 0.1427, -0.5414],[-0.4664, -0.1228],[-1.1734, 0.7230]])
pytorch似乎在使用GPU的情况下,不检查index是否会越界,因此如果你的index越界了,但是报错的地方可能不在使用index_select()的地方,而是在后续的代码中,这个似乎就需要留意下你的index了。同时,index是一个LongTensor,这个也是要留意的。悄悄地更新,BN层就是个小可爱
running_mean和running_var是在调用forward()后就更新的,这个和一般的参数不同,容易造成疑惑,考虑到篇幅较长,请移步到[11]。F.interpolate的问题
pytorch的确也是提供对以tensor形式表示的图像进行插值的功能,那就是函数torch.nn.functional.interpolate[12],但是我们注意到这个插值函数有点特别,它是对以batch为单位的图像进行插值的,如果你想要用以下的代码去插值:image = torch.rand(3,112,112) # H = 112, W = 112, C = 3的图像image = torch.nn.functional.interpolate(image, size=(224,224))
size只接受一个整数,其对W这个维度进行缩放,这里,interpolate会认为3是batch_size,因此如果需要对图像的H和W进行插值,那么我们应该如下操作:image = torch.rand(3,112,112) # H = 112, W = 112, C = 3的图像image = image.unsqueeze(0) # shape become (1,3,112,112)image = torch.nn.functional.interpolate(image, size=(224,224))
Reference
推荐阅读

