概述
構建一個toy net,測試不參與運算的變量是否會更新&如何更新,加深對pytorch框架參數更新邏輯的理解。
起因
實現隨機深度策略時,在block內部進行requires_grad=True/False操作會報錯
(後面測試知道其實是DataParallel的鍋)ref: 1, 2
測試代碼
結論見後
# 以下代碼中,需要設置或取消對應的代碼屏蔽,完成不同的測試內容
class ConvBlock(nn.Module):
def __init__(self):
super(ConvBlock,self).__init__()
self.conv = nn.Conv2d(20,20,3,1,1)
self.bn = nn.BatchNorm2d(20)
self.relu = nn.ReLU()
def forward(self, x):
x = self.conv(x)
x = self.bn(x)
x = self.relu(x)
return x
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
self.conv3 = nn.Conv2d(20,20,3,1,1)
self.conv4 = nn.Sequential(OrderedDict([
('conv4_1', nn.Conv2d(20, 20, 3, 1, 1)),
('conv4_2', nn.Conv2d(20, 20, 3, 1, 1))
])) # 測試不同的block構建是否有影響
self.conv5 = ConvBlock()
self.conv6 = nn.Sequential(
ConvBlock(),
ConvBlock()
) # 測試不同的block構建是否有影響
self.drop = nn.Dropout2d()
self.fc1 = nn.Linear(320, 50)
self.fc2 = nn.Linear(50, 10)
self.m = torch.distributions.bernoulli.Bernoulli(torch.Tensor([0.5]))
self.run = 0
def forward(self, x):
x = F.relu(F.max_pool2d(self.conv1(x), 2))
x = F.relu(F.max_pool2d(self.drop(self.conv2(x)), 2))
# if self.run: # torch.equal(self.m.sample(), torch.ones(1)):
# print('run conv3')
# x = self.conv3(x)
# # self.run = 0 # 設置僅第一次運行時用
# else:
# print('skip conv3')
# self.run += 1 # 設置第一次後運行時用
x = self.conv3(x)
x = self.conv4(x)
x = self.conv5(x)
# self.conv6._modules['0']._modules['conv'].weight.detach_() # 設置後變爲is_leaf=True,requires_grad=False,運行conv6(x)後不會對conv6 block1中的conv造成影響(後者仍爲is_leaf=False,requires_grad=True
x = self.conv6(x)
# self.conv3.weight.requires_grad = False # 可以直接在forward中設置requires_grad
# self.conv4._modules['conv4_1'].weight.requires_grad = False # 可以直接在forward中設置requires_grad
# self.conv5._modules['conv'].weight.requires_grad = False # 可以直接在forward中設置requires_grad
# self.conv6._modules['0']._modules['conv'].weight.requires_grad = False # 可以直接在forward中設置requires_grad
# for item in self.conv6.modules(): # 可以直接在forward中設置requires_grad
# if isinstance(item, nn.Conv2d):
# item_no_grad = item.weight.detach()
# item.weight.requires_grad = False
# self.conv6._modules['0']._modules['conv'].weight.detach_()
# weight_no_grad = self.conv6._modules['0']._modules['conv'].weight.detach()
x = x.view(-1, 320)
x = F.relu(self.fc1(x))
x = F.dropout(x, training=self.training)
x = self.fc2(x)
return x # F.log_softmax(x, dim=1)
gpu_id = [3]
device = torch.device("cuda:%d"%gpu_id[0] if torch.cuda.is_available() else "cpu") #
model = Net()
if len(gpu_id)>1: # multi-GPU setting
model = nn.DataParallel(model,device_ids=gpu_id)
model = model.to(device)
elif len(gpu_id)==1:
model = model.to(device)
# os.environ['CUDA_VISIBLE_DEVICES'] = '3'
# device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# model = polynet_stodepth()
# model = model.cuda() #to(device)
LOSS = nn.CrossEntropyLoss()
OPTIMIZER = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9 )
# del OPTIMIZER.param_groups[0]['params'][4] # 移除優化器中的參數
# OPTIMIZER.param_groups[0]['params'].append(model._modules['conv3'].weight) # 將參數加入優化器參數列表
epoch = 10
batch = 50
bs = 2
model.train()
# model.conv3.weight.requires_grad = False
for i in range(epoch):
print('===== epoch %d'%i)
for j in range(batch):
print('epoch-%d, batch-%d'%(i,j))
inputs = torch.randn(bs,1,28,28).to(device)
labels = torch.randint(low=0,high=9,size=(bs,)).to(device)
# inputs = torch.randn(bs, 3, 235, 235).cuda() #.to(device)
# labels = torch.randint(low=0, high=9, size=(bs,)).cuda() #.to(device)
pred = model(inputs)
loss = LOSS(pred, labels)
# for m in model.modules():
# print(str(m.__class__)) # 打印的是各操作的類型,如Conv2d,類型是有重複的
# for pname, p in model.named_parameters():
# print(pname) # 打印的是各操作的名字,名字是唯一標識的
OPTIMIZER.zero_grad()
loss.backward()
# model.conv3.weight.requires_grad = False
OPTIMIZER.step()
print('done')
實驗觀察結論
- 初始化各模塊如self.conv3後,其_grad值爲None
- self.conv3只有在forward中運行了,纔會被納入graph中,纔會在loss.backward時計算_grad
- 若只有第一次forward時self.conv3進行了運算,其他都跳過,則後續self.conv3的_grad爲0.0
——> 原因:會被zero_grad;注意梯度爲0,而非爲None,self.conv3的參數還是會被更新,這是由於實際優化時有動量的存在,梯度爲0時也會使參數值變化 - 若第一次forward時跳過了self.conv3,後面又加入?
——> 第一次爲None,後面正常計算梯度 - 若_grad一直爲None,是否會更新參數呢?–> 不會更新
- 參與forward計算,但在model外部,一開始就設置self.conv3的requires_grad爲False,參數是否更新?
——> 不會計算_grad,即一直爲None,故不更新參數 - 參與forward計算,但在model外部,在backward和step之間才設置self.conv3的requires_grad爲False,參數是否更新?
——> 會更新!因爲backward後已經有了_grad生成,則只要參數收錄於optimizer,就會被更新;
——> 同樣,下一次迭代時,由於zero_grad,_grad會爲[0.0,…],然後由於requires_grad爲False,所以不會計算新的_grad
——> 參數也是基於動量而改變的 - 參與forward計算,但forward內部,設置self.conv3的requires_grad爲False,參數是否更新?
——> 經測試,竟然可以直接在forward內部賦值;經分析,可轉爲case 6 - 參與forward計算,設置requires_grad=True,但是在optimizer中去掉self.conv3,參數是否更新?
——> 不會更新,但是_grad依然會計算,但不會被置零,(看來zero_grad是按照optimizer中的參數列表進行操作,想來也合理,本來就是optimizer.zero_grad())
——> 補充:不參與forward計算,從optimizer中刪除參數,保留state中參數-動量dict,則參數不更新,梯度和動量不計算且保持原值不變;sgd.py的step中:buf.mul_(momentum).add_(1 - dampening, d_p), d_p是當次的梯度,buf是動量,buf乘以係數momentum再加上(1-dampending)*d_p作爲新的梯度用於更新參數
——> 補充:參數更新對應到optimizer中就是的param_groups中的參數值改變,state的dict{參數i:動量i}的參數i自動相應改變,動量更新如9中所述。 - 分析測試是什麼造成PolyFaceNet中的forward內部不能設置requires_grad ?
——> 經測試,各種情況下,forward內部都能直接設置requires_grad - 測試DataParallel
——> 經測試,就是這個導致的RuntimeError [issue-3]
實際上進入model(x)之前,model的參數都是leaf變量(requires_grad=True,可手動設置),進入model.forward()後,就變成非leaf變量了(requires_grad=True,不能手動設置修改),退出forward()後,恢復成leaf變量(requires_grad=True,可手動設置修改)
——> 在forward內部,在conv6(x)的前或後,使用self.conv6…weight0.detach_(),可以將weight0設置爲leaf變量,且grad_fn=None, requires_grad=False,均不會對self.conv6中後續的其他weight1有影響,但是退出forward後,weight0還是會還原requires_grad=True…
——> 經測試,在PolyFaceNet中設置self.keep_block,若不對model用DataParallel,則可正常改變model的keep_block屬性。若對model用DataParallel,則model保留__init__中初始化的keep_block不變,或者未初始化時,無此屬性(即不會因爲forward添加)
——> 被DataParallel包裝過的model,會多一個前綴’modules’,如PolyFaceNet.py中: model.module.blockA_multi.0 (單gpu時是model.blockA_multi.0)
——> 聯想保存多gpu訓練的模型時,要調用model.module.state_dict(),單gpu時是model.state_dict()
——> 更多:DataParallel也是nn.Module, 傳入的model保存在DataParallel的self.module變量裏面,所以爲什麼多了一個前綴
——> 回過頭看,因爲DataParallel是將模型複製到各gpu上分別進行前向傳播,若允許在前向中修改屬性,會引起歧義:比如gpu1上將blockA保留了,但是gpu2上又將其刪除,則最終綜合結果時,主gpu將"懵逼",所以邏輯上會禁止在前向中修改,這麼一想,合情合理。 - 測試os.environ[‘CUDA_VISIBLE_DEVICES’] = '0,1’實現多GPU訓練
——> .cuda()和.to(device)均失敗!模型和數據只會被移到默認的gpu0上(10873/11178M, 353M/11178M) - DataParallel是單機多卡,對於多機多卡:https://zhuanlan.zhihu.com/p/68717029
總結
正常情況下,不考慮上述測試中的各種特殊情形
- requires_grad管梯度計算,參數的該屬性爲True時就會計算該參數的梯度
- optimizer.param_groups管參數更新,若參數w被收錄於param_groups中,參數w就會參與“更新過程”,否則不參與。(注意,這裏參數w參與更新過程是指optimizer.step()中會遍歷到w,但w數值可能不改變,比如requires_grad=False導致grad=None,將會跳過w: ‘ if p.grad is None: continue ’)
- 對於平時採用預訓練模型,比如要固定backbone訓練head,則有兩種方案實現:a. 定義optimizer時不收錄backbone的params,無所謂requires_grad的取值;b. 設置backbone的params的requires_grad=False,無所謂optimizer中收錄不收錄。(通過上述分析,自然是採用方案b爲佳,因爲backward時不用再計算backbone參數的梯度!大大加快訓練速度且節省大量顯存)
- 結合pytorch的動態圖機制理解(點這裏),前向負責構圖,反向負責求梯度(對所有requires_grad=True的變量算梯度),反向完畢則釋放圖,因此若需要二次反向,需要保證圖沒有被釋放掉:
retain_graph=True
。