PyTorch 101, part 3:使用PyTorch,讓網絡模型更深

PyTorch 101, part 3:使用PyTorch,讓網絡模型更深

在這篇教程中,我們將深入挖掘PyTorch的函數,並會涉及到高級的任務,比如使用不同的學習率,學習率的策略和不同權重初始化等等。

      讀者們你們好,這是我們PyTorch教程的另外一篇文章。這篇文章適合那些有了PyTorch基礎的、以及想要提高PyTorch水平的讀者。儘管在前面的文章中我們已經涉及到如何實現一個基本的分類器了,但是,在這篇文章中,我們將會討論如何使用PyTorch去實現更爲複雜的深度學習功能。爲了讓你更好理解,下面列出了這篇文章的要達成的目的:

  1. PyTorch類和類之間有哪些不同,比如nn.Module, nn.Functional, nn.Parameter,以及什麼時候該使用哪一個。
  2. 如何定製你自己的訓練選項,比如不用網絡層有不同的學習率,不同學習率的策略。
  3. 設置權重初始化

目錄

PyTorch 101, part 3:使用PyTorch,讓網絡模型更深

1 nn.Module vs nn.Functional

2 理解網絡的狀態

3 nn.Parameter

4 nn.ModuleList和nn.ParameterList

5 權重初始化

6 modules() vs children()

7 打印網絡的信息

8 不同的網絡層,不同的學習率

9 學習率策略

10 保存你的模型

11 總結


1 nn.Module vs nn.Functional

        這些類都是經常會遇到的,特別是當你讀一些開源代碼的時候。在PyTorch中,網絡層經常作爲torch.nn.Module實例對象或者是torch.nn.Funtional函數來實現。該使用哪一個呢?哪一個更好呢?

        我們已經在第二部分講過了,torch.nn.Module是PyTorch基礎。當你第一次定義一個nn.Module實例對象的時候,之後它自己會調用forward()方法去運行。這是一種面向對象的思想。

        另一方面,nn.functional以函數的形式,提供了很多網絡層或者是激活函數,因此它可以直接在輸入上調用,而不需要定義一個對象。舉個例子,如果想要去調整一個圖像tensor的尺寸,你可以調用在圖像tensor上調用torch.nn.functional.interpolate方法。

        所以,當我們使用它們的時候,我們該選擇哪一個呢?什麼時候我們實現的網絡層、激活函數和損失函數含有損失呢?

2 理解網絡的狀態

        一般來說,任何層都可以看做是一個函數。舉例來說,一個卷積操作僅僅是一系列的乘法和加法運算。因此,對於我們來說,僅僅把它理解成是一個函數來實現嗎?但是,網絡層需要保存權重以及當我們訓練的時候,需要進行更新。因此,從一個編程的角度來講,網絡層不僅僅是一個函數。它同樣也需要保存數據,當我們訓練網絡的時候,它會隨之發生變化。

        現在,我希望你去重視這個事實:當我們卷積層變化的時候,數據會被保存。這意味着,當我們進行訓練的時候,網絡層發生了變化。對於我們來說去實現一個可以進行卷積操作的函數,我們還需要去額外定義一個數據結構來單獨保存網絡層的權重。然後讓這個額外的數據結構作爲我們函數的輸入。

        或者,爲了避免麻煩,我們可以僅僅定義一個類去保存數據結構,並且讓卷積運算作爲類的成員函數。這確實會減輕我們的工作量,因爲我們不需要去擔心函數之外的變量狀態。在這些情況下,我們更傾向於使用nn.Module實例對象,我們有權重或者有其他狀態,它或許定義網絡層的某些行爲。具體來說,dropout網絡層和BN網絡層在訓練和推理的時候有着不同的行爲。

        另一方面,沒有權重或者狀態的時候,我們可以使用nn.functional。舉例來說,比如resizing(可以調用nn.functional.interpolate函數),平均池化(可以調用nn.functional.AvgPool2d函數)。

        除了上面的原因,大部分的nn.Module類都有他們對應的nn.functional函數。但是,在實際工作中,應當遵循上面的原理。

綜上所述,當我們需要保存網絡層的權重和狀態的時候,就需要使用nn.Module類;如果我們僅僅是進行一些變換的話,我們使用nn.functional類就可以了。

3 nn.Parameter

        PyTorch中一個重要的類是nn.Parameter類,出乎我們意料,在PyTorch介紹的文章中卻很少涉及。考慮一下下面的情況:

class net(nn.Module):
  def __init__(self):
    super().__init__()
    self.conv = nn.Linear(10,5)
    
  def forward(self, x):
    return self.linear(x)


myNet = net()

#prints the weights and bias of Linear Layer
print(list(myNet.parameters()))     

        每個nn.Module都有一個parameters()的函數,它返回的是可訓練的參數。我們需要顯示地定義參數。在定義nn.Conv2d的時候,PyTorch的創造者定義了權重和偏置爲該網絡層的參數。但是,需要注意一件事,當我們定義net的時候,我們並不需要將nn.Conv2d的parameters添加到net的parameters中。它天生就有將nn.Conv2d實例對象設置爲net實例對象成員變量的優勢(也就是說PyTorch自己會將nn.Conv2d的參數加載到net的參數中,並不需要程序員添加)。

        這是nn.Parameter類內部的實現,nn.Parameter類是Tensor的子類。當我們調用nn.Module對象的parameters()函數的時候,它會返回nn.Parameter對象的成員變量。

        事實上,所有的nn.Module類的訓練權重都是作爲nn.Parameter實例對象實現的。無論何時,一個nn.Module(在我們的例子中是nn.Conv2d)被分配爲另一個nn.Module的成員變量,分配的實例對象的 "parameters"(也就是nn.Conv2d的權重)也要添加到被分配的那個實例對象的”parameters"中(也就是nn.Conv2d的參數要添加到net網絡中)。這叫做是nn.Module的註冊的"parameters"。

        如果你試圖去給nn.Module實例對象分配一個張量的,它不會在parameters()中顯示,除非你把它定義爲nn.Parameter的實例對象。你可以在你需要去保存一個不可微的張量的時候去這樣做,比如在RNNs中保存上一層的輸出。

class net1(nn.Module):
  def __init__(self):
    super().__init__()
    self.conv = nn.Linear(10,5)
    self.tens = torch.ones(3,4)                       # This won't show up in a parameter list 
    
  def forward(self, x):
    return self.linear(x)

myNet = net1()
print(list(myNet.parameters()))

##########################################################

class net2(nn.Module):
  def __init__(self):
    super().__init__()
    self.conv = nn.Linear(10,5) 
    self.tens = nn.Parameter(torch.ones(3,4))                       # This will show up in a parameter list 
    
  def forward(self, x):
    return self.linear(x)

myNet = net2()
print(list(myNet.parameters()))

##########################################################

class net3(nn.Module):
  def __init__(self):
    super().__init__()
    self.conv = nn.Linear(10,5) 
    self.net  = net2()                      # Parameters of net2 will show up in list of parameters of net3
    
  def forward(self, x):
    return self.linear(x)


myNet = net3()
print(list(myNet.parameters()))

4 nn.ModuleList和nn.ParameterList

        我記得當我使用PyTorch實現YOLOV3的教程中,我已經使用了nn.ModuleList這個類。我通過解析包含YOLOV3網絡架構的文本文件去構建了這個網絡。我在PyTorch列表中保存所有的nn.Module實例對象,然後讓這個一系列的nn.Module模塊表示這個網絡。

        簡化一下,就像這樣:

layer_list = [nn.Conv2d(5,5,3), nn.BatchNorm2d(5), nn.Linear(5,2)]

class myNet(nn.Module):
  def __init__(self):
    super().__init__()
    self.layers = layer_list
  
  def forward(x):
    for layer in self.layers:
      x = layer(x)

net = myNet()

print(list(net.parameters()))  # Parameters of modules in the layer_list don't show up.
# 輸出結果就是: []

        如你所見,這和我們單獨註冊模塊的結果不一樣,PyTorch並不會註冊Python列表中的Modules參數(也就是說,PyTorch不會保存list中的Modules的parameters)。爲了解決這個問題,我們用nn.ModuleList類包裹我們的列表,然後就可以將其變爲網絡類的成員。

layer_list = [nn.Conv2d(5,5,3), nn.BatchNorm2d(5), nn.Linear(5,2)]

class myNet(nn.Module):
  def __init__(self):
    super().__init__()
    # 我們需要將list對象轉換成ModuleList對象類型
    # 這是因爲我們PyTorch不會保存list對象的parameters
    self.layers = nn.ModuleList(layer_list)
  
  def forward(x):
    for layer in self.layers:
      x = layer(x)

net = myNet()

print(list(net.parameters()))  # Parameters of modules in layer_list show up.
# 注意:如果輸出的時候沒有list,那麼就不會將網絡的參數打印出來,只會打印內存地址
# <generator object Module.parameters at 0x0000026581AA1830>

        相似的,一系列的張量數據也可以通過nn.ParameterList類包裹進行註冊。

補充一點:nn.ModuleList和nn.Sequential還是有區別的:

  • nn.ModuleList類僅僅是保存module的容器,module在其中存放是沒有順序的,而且它並沒有實現forward()函數。這意味着如果我們不在網絡的forward()函數中指定數據在module中傳遞的順序的話,將不會正確執行的。我們可以使用append()函數,將模塊添加到nn.ModuleList容器中
  • nn.Sequential類則是按照順序存放的。我們只需要將數據輸入到它的實例對象中,PyTorch會自動給定數據在Sequential容器中傳遞的先後順序的。並且,我們可以使用add.module(name, module)函數來將模塊添加到nn.Sequential容器中。

5 權重初始化

        初始化權重可以影響訓練的結果。再者,你可能要爲不同類型的網絡層分配不同的初始化策略。這可以通過modules和apply函數實現。modules是nn.Module類的成員函數,它返回一個包含nn.Module函數的所有的nn.Module成員變量的迭代器。之後可以在每個nn.Module上調用去使用apply函數,來進行初始化。

import matplotlib.pyplot as plt
%matplotlib inline

class myNet(nn.Module):
 
  def __init__(self):
    super().__init__()
    self.conv = nn.Conv2d(10,10,3)
    self.bn = nn.BatchNorm2d(10)
  
  def weights_init(self):
    for module in self.modules():
      if isinstance(module, nn.Conv2d):
        nn.init.normal_(module.weight, mean = 0, std = 1)
        nn.init.constant_(module.bias, 0)

Net = myNet()
Net.weights_init()

for module in Net.modules():
  if isinstance(module, nn.Conv2d):
    weights = module.weight
    # reshape(-1)是將數據變成行向量
    weights = weights.reshape(-1).detach().cpu().numpy()
    print(module.bias)                                       # Bias to zero
    plt.hist(weights)
    plt.show()

 

        可以在torch.nn.init模塊中找到很多的初始化函數。

6 modules() vs children()

        modules和children函數非常相似。雖然它們之前的差異很小,但是很重要。衆所周知,一個nn.Module實例對象可以包含其他的nn.Module實例對象作爲它的成員。

        當children調用的時候,children()只會返回一系列的nn.Module實例對象,它們只是實例對象的數據成員。

         另一方面,nn.Modules遞歸地的進入每個nn.Module實例對象中,創建每個實例對象的列表,直到不存在nn.module實例對象爲止。注意,modules()也會返回已經是列表一部分的nn.Module。

        注意,上面的情況使用與所有繼承nn.Module類的實例對象或者類。

class myNet(nn.Module):
  def __init__(self):
    super().__init__()
    self.convBN =  nn.Sequential(nn.Conv2d(10,10,3), nn.BatchNorm2d(10))
    self.linear =  nn.Linear(10,2)
    
  def forward(self, x):
    pass
  

Net = myNet()

print("Printing children\n------------------------------")
print(list(Net.children()))
print("\n\nPrinting Modules\n------------------------------")
print(list(Net.modules()))
# 上面的輸出結果是:
Printing children
------------------------------
[Sequential(
  (0): Conv2d(10, 10, kernel_size=(3, 3), stride=(1, 1))
  (1): BatchNorm2d(10, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
), Linear(in_features=10, out_features=2, bias=True)]


Printing Modules
------------------------------
[myNet(
  (convBN): Sequential(
    (0): Conv2d(10, 10, kernel_size=(3, 3), stride=(1, 1))
    (1): BatchNorm2d(10, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  )
  (linear): Linear(in_features=10, out_features=2, bias=True)
), Sequential(
  (0): Conv2d(10, 10, kernel_size=(3, 3), stride=(1, 1))
  (1): BatchNorm2d(10, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
), Conv2d(10, 10, kernel_size=(3, 3), stride=(1, 1)), BatchNorm2d(10, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True), Linear(in_features=10, out_features=2, bias=True)]

從上面的輸出結果我們可以看到:

  • Net.children()函數只會返回這個網絡架構的第一層成員變量。
  • Net.modules()函數會遞歸進入到每層成員變量然後進行輸出。第一層:它會先打印Sequential(convBN, linear);第二層:它會打印Sequential(Conv2d,BN);第三層:它最後會打印Conv2d這個網絡層。

因此,當我們初始化權重的時候,我們或許會使用modules()函數,因爲我們無法進入到nn.Sequential實例對象內容,以及爲它的成員進行初始化權重。

 

7 打印網絡的信息

        我們可能需要去打印網絡的信息,無論是給使用者或者是出於調試的目的。通過使用它的named_*函數,PyTorch給我們提供了非常簡潔的方式去打印這個網絡的信息,這裏有四個函數:

  1. named_parameters 返回一個迭代器,他給出了包含參數名稱的元組(如果一個卷積層分配爲self.conv1,那麼他的參數就是conv1.weight和conv1.bias)和nn.Parameter的__repr__函數返回的值。
  2. named_modules 和上面類似,但是迭代器返回的模型和modules()函數返回的一樣。
  3. named_children 和上面類似,但是迭代器返回的模型和children()函數返回的一樣。
  4. named_buffers 返回緩存張量,比如批歸一化層的運行平均值。
# 等價於:for x in Net.modules():
for x in Net.named_modules():
  print(x[0], x[1], "\n-------------------------------")


# 輸出結果
myNet(
  (convBN): Sequential(
    (0): Conv2d(10, 10, kernel_size=(3, 3), stride=(1, 1))
    (1): BatchNorm2d(10, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  )
  (linear): Linear(in_features=10, out_features=2, bias=True)
)
convBN Sequential(
  (0): Conv2d(10, 10, kernel_size=(3, 3), stride=(1, 1))
  (1): BatchNorm2d(10, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
convBN.0 Conv2d(10, 10, kernel_size=(3, 3), stride=(1, 1))
convBN.1 BatchNorm2d(10, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
linear Linear(in_features=10, out_features=2, bias=True)

 8 不同的網絡層,不同的學習率

        在這個部分,我們將會學習如何給不同的網絡層使用不同的學習率。通常來說,我們將會涉及如何讓不同組的參數有着不同的超參數,不管是不同網絡層的不同學習率,異或是偏置和權重有着不同的學習率。

        實現這個相符是非常容易的一件事。在我們之前的文章中,我們實現了CIFAR分類器,我們將網絡的所有參數作爲一個整體,傳給了優化器實例對象。

class myNet(nn.Module):
  def __init__(self):
    super().__init__()
    self.fc1 = nn.Linear(10,5)
    self.fc2 = nn.Linear(5,2)
    
  def forward(self, x):
    return self.fc2(self.fc1(x))

Net = myNet()
optimiser = torch.optim.SGD(Net.parameters(), lr = 0.5)

        然而,torch.optim類允許我們用詞典的形式提供不同學習率的不同參數集合。

optimiser = torch.optim.SGD([{"params": Net.fc1.parameters(), 'lr' : 0.001, "momentum" : 0.99},
                             {"params": Net.fc2.parameters()}], lr = 0.01, momentum = 0.9)

        在上面的場景中,fc1的參數使用的學習率爲0.01,momentum爲0.99。如果沒有爲一組參數初始化超參數(比如fc2),那麼它就會使用默認超參數的值,作爲優化器函數的輸入參數(此時因爲沒有爲fc2初始化參數,那麼它的就使用默認的數值)。你也可以創建不同網絡層的偏置參數列表,或者是使用上面我們講過的named_parameters()函數創建權重參數或者是偏置參數。

 9 學習率策略

        設計你學習率是你需要調整的一個重要的參數。PyTorch使用它的torch.optim.lr_scheduler模塊來支持規劃你的學習率,這個模塊有各種不同的學習策略。下面的例子顯示了這樣的一個例子:

scheduler = torch.optim.lr_scheduler.MultiStepLR(optimiser, milestones = [10,20], gamma = 0.1)

        上面的規劃器中,當我們到達milestones列表中的epochs的時候,學習率每次都會乘以gamma。在我們的案例中,在第10個epoch和第20個epoch的時候,學習率將會乘以0.1,。在代碼中,你還要在每個epochs的循環中寫上secheduler.step這行代碼,好讓學習率可以更新。

       通常來說,訓練循環由兩個嵌套的循環構成,一個循環遍歷epochs,另個在epoch中遍歷批次。要確保在epoch循環的開始調用scheduler.step函數,好讓你的學習率可以更新。小心不要把代碼寫在批次循環中,否則你的學習率可能會在第10個批次而不是第10個epoch中進行更新,你可以參考這樣的佈局:

for epoch,(data,label) in enumerate(trainSet):
    Net.train()
    # step1:
    scheduler.step()
    for batch in batches:
        ...
        loss.backward()
        # step2:
        optimiser.step()

        還需要注意,scheduler.step函數不能代替optim.step,並且你需要都需要在反向傳播中調用optim.step函數(這個在批次循環中)。

10 保存你的模型

        爲了後面推理,你或許想要保存你的模型,或者僅僅想創建訓練檢查點。要在PyTorch重保存模型涉及到兩個選項。

        第一個是使用torch.save。這個等價於使用Pickle去序列化整個nn.Module實例對象。它將整個模型保存在磁盤中。你稍後可以使用torch.load去從內存中加載這個模型。

torch.save(Net, "net.pth")

Net = torch.load("net.pth")

print(Net)

        上面的代碼會保存整個模型的權重和架構。如果你僅僅需要保存權重而不是保存整個模型,你可以只使用模型的state_dict函數。這個static_dict是一歌詞典,它將nn.Parameter一個網絡的實例對象映射成它的值。

        如上所述,可以將存在的state_dict加載到一個nn.Module實例對象中。注意,這不會保存整個模型而只會保存參數。在你加載這個state_dict之前,你需要創建這個網絡模型。如果這個網絡架構和你保存的state_dict的不同,PyTorch將會拋出異常。

torch.save(Net.state_dict(), "net_state_dict.pth")

Net.load_state_dict(torch.load("net_state_dict.pth"))

        torch.optim的優化器也有一個state_dict實例對象,它用來保存優化算法的超參數。它也可以用上面我們在優化器實例對象調用的load_state_dict那樣保存和加載。

11 總結

在這篇文章中,我們將重點放在瞭如何構建一個網絡層數更深、更加複雜的網絡架構。

  • 在構建網絡層的時候:你可以使用nn.Module或者使用nn.functional兩這個類。
  • 在保存網絡層的時候:你可以使用nn.ModuleList或者nn.Sequential這兩個容器。
  • 網絡層不同,學習率不同:你可以設定整個網絡層的學習率是一樣的,也可以設定不同的網絡層擁有不同的學習率。    
  • epoch不同,學習率不同:你可以設定整個epochs的學習率是一樣的,當然不同的epoch可以使用不同的學習率。    
  • 保存和加載:你可以將整個模型以及參數保存下來,當然你也可以值保存參數,然後構建網絡模型實例對象,然後將參數加載到這個實例對象中。

我們完成了PyTorch的更高級的特徵的討論。我希望這篇文章能夠幫助你實現你思考出的複雜的深度學習架構。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章