Pytorch參數更新實驗

概述

構建一個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')
實驗觀察結論
  1. 初始化各模塊如self.conv3後,其_grad值爲None
  2. self.conv3只有在forward中運行了,纔會被納入graph中,纔會在loss.backward時計算_grad
  3. 若只有第一次forward時self.conv3進行了運算,其他都跳過,則後續self.conv3的_grad爲0.0
    ——> 原因:會被zero_grad;注意梯度爲0,而非爲None,self.conv3的參數還是會被更新,這是由於實際優化時有動量的存在,梯度爲0時也會使參數值變化
  4. 若第一次forward時跳過了self.conv3,後面又加入?
    ——> 第一次爲None,後面正常計算梯度
  5. 若_grad一直爲None,是否會更新參數呢?–> 不會更新
  6. 參與forward計算,但在model外部,一開始就設置self.conv3的requires_grad爲False,參數是否更新?
    ——> 不會計算_grad,即一直爲None,故不更新參數
  7. 參與forward計算,但在model外部,在backward和step之間才設置self.conv3的requires_grad爲False,參數是否更新?
    ——> 會更新!因爲backward後已經有了_grad生成,則只要參數收錄於optimizer,就會被更新;
    ——> 同樣,下一次迭代時,由於zero_grad,_grad會爲[0.0,…],然後由於requires_grad爲False,所以不會計算新的_grad
    ——> 參數也是基於動量而改變的
  8. 參與forward計算,但forward內部,設置self.conv3的requires_grad爲False,參數是否更新?
    ——> 經測試,竟然可以直接在forward內部賦值;經分析,可轉爲case 6
  9. 參與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中所述。
  10. 分析測試是什麼造成PolyFaceNet中的forward內部不能設置requires_grad ?
    ——> 經測試,各種情況下,forward內部都能直接設置requires_grad
  11. 測試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將"懵逼",所以邏輯上會禁止在前向中修改,這麼一想,合情合理。
  12. 測試os.environ[‘CUDA_VISIBLE_DEVICES’] = '0,1’實現多GPU訓練
    ——> .cuda()和.to(device)均失敗!模型和數據只會被移到默認的gpu0上(10873/11178M, 353M/11178M)
  13. DataParallel是單機多卡,對於多機多卡:https://zhuanlan.zhihu.com/p/68717029
總結

正常情況下,不考慮上述測試中的各種特殊情形

  1. requires_grad管梯度計算,參數的該屬性爲True時就會計算該參數的梯度
  2. optimizer.param_groups管參數更新,若參數w被收錄於param_groups中,參數w就會參與“更新過程”,否則不參與。(注意,這裏參數w參與更新過程是指optimizer.step()中會遍歷到w,但w數值可能不改變,比如requires_grad=False導致grad=None,將會跳過w: ‘ if p.grad is None: continue ’)
  3. 對於平時採用預訓練模型,比如要固定backbone訓練head,則有兩種方案實現:a. 定義optimizer時不收錄backbone的params,無所謂requires_grad的取值;b. 設置backbone的params的requires_grad=False,無所謂optimizer中收錄不收錄。(通過上述分析,自然是採用方案b爲佳,因爲backward時不用再計算backbone參數的梯度!大大加快訓練速度且節省大量顯存)
  4. 結合pytorch的動態圖機制理解(點這裏),前向負責構圖反向負責求梯度(對所有requires_grad=True的變量算梯度),反向完畢則釋放圖,因此若需要二次反向,需要保證圖沒有被釋放掉:retain_graph=True
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章