PyTorch神經網絡中可學習的參數——CNN權重 | PyTorch系列(十五)

點擊上方AI算法與圖像處理”,選擇加"星標"或“置頂”

重磅乾貨,第一時間送達

文 |AI_study

我們的神經網絡

在本系列的最後幾篇文章中,我們已經開始構建CNN,我們做了一些工作來理解我們在網絡構造函數中定義的層。

class Network(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5)
        self.conv2 = nn.Conv2d(in_channels=6, out_channels=12, kernel_size=5)


        self.fc1 = nn.Linear(in_features=12*4*4, out_features=120)
        self.fc2 = nn.Linear(in_features=120, out_features=60)
        self.out = nn.Linear(in_features=60, out_features=10)


    def forward(self, t):
        # implement the forward pass
        return t

最後,我們在整個過程中的下一步是在網絡的 forward 方法中使用這些層,但是現在,讓我們來看看網絡中的可學習參數

我們已經知道了超參數。我們知道超參數的值是任意選取的。

到目前爲止,我們使用的超參數是我們用來構建網絡架構的參數,儘管我們構建的層被賦值爲類屬性。

Hyperparameter values are chosen arbitrarily.

這些超參數並不是唯一的超參數,當我們開始訓練過程時,我們將看到更多的超參數。我們現在關心的是網絡的可學習參數。

可學習的參數

可學習參數 是指在訓練過程中學習的參數值。

對於可學習的參數,我們通常從一組隨機值開始,然後隨着網絡的學習,以迭代的方式更新這些值。

事實上,當我們說網絡是學習的時候,我們的具體意思是網絡正在學習可學習參數的適當值。這些值是使損失函數最小化的值。

當涉及到我們的網絡時,我們可能會想,這些可學習的參數在哪裏?

Where are the learnable parameters?

我們將可學習的參數是網絡內部的權重,它們存在於每一層中。

獲取網絡的實例

在PyTorch中,我們可以直接檢查權重。讓我們獲取我們的網絡類的一個實例並查看它。

network = Network()

請記住,要獲取我們的網絡類的對象實例,我們需要在類名後面加上括號。當這段代碼執行時,__init__ 類構造函數中的代碼將運行,分配給層返回的對象實例之前的屬性。

名稱爲__init__ 是initialize的縮寫。在對象的情況下,屬性是使用值來初始化的,這些值實際上可以是其他對象。通過這種方式,對象可以嵌套在其他對象中。

我們的網絡類就是這種情況,其網絡類屬性是使用PyTorch 層類的實例初始化的。初始化對象後,我們可以使用網絡變量訪問對象。

在開始使用新創建的網絡對象之前,請查看將網絡傳遞給Python的print() 函數時會發生什麼。

print(network)
Network(
    (conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
    (conv2): Conv2d(6, 12, kernel_size=(5, 5), stride=(1, 1))
    (fc1): Linear(in_features=192, out_features=120, bias=True)
    (fc2): Linear(in_features=120, out_features=60, bias=True)
    (out): Linear(in_features=60, out_features=10, bias=True)
)

print() 函數將控制檯的字符串表示形式輸出到控制檯。憑着敏銳的眼光,我們可以注意到這裏的打印輸出詳細說明了我們網絡的結構,列出了我們網絡的層,並顯示了傳遞給層構造函數的值。

網絡的字符串表示

不過,有一個問題。這是怎麼回事?

Where is this string representation coming from?

我們的網絡類將從PyTorch Module基類繼承此功能。觀察如果我們停止擴展神經網絡模塊類會發生什麼。

print(network)
<__main__.Network object at 0x0000017802302FD0>

現在,我們沒有像以前那樣好的描述性輸出。取而代之的是,我們得到了一堆奇怪的內容,如果我們不提供這是默認的Python字符串表示形式。

因此,在面向對象的編程中,我們通常希望在類中提供對象的字符串表示形式,以便在打印對象時獲得有用的信息。這種字符串表示形式來自Python的默認基類object(對象)。

How  Overriding Works

所有Python類都會自動擴展對象類。如果我們想爲我們的對象提供一個自定義的字符串表示形式,我們可以做到,但是我們需要引入另一個面向對象的概念,稱爲overriding(覆蓋)。

當我們擴展一個類時,我們獲得了它的所有功能,作爲補充,我們可以添加其他功能。但是,我們也可以通過將現有功能更改爲不同的行爲來覆蓋現有功能。

我們可以使用__repr__函數覆蓋Python的默認字符串表示。這個名稱是representation(表示)的縮寫。

def __repr__(self):
    return "lizardnet"

這一次,當我們將網絡傳遞給print函數時,我們在類定義中指定的字符串將代替Python的默認字符串打印出來。

> print(network)
lizardnet

當我們在前面討論OOP時,我們瞭解了__nit__方法,以及它是如何作爲一種特殊的Python方法來構造對象的。

我們還會遇到其他特殊的方法,而__repr__就是其中之一。所有特殊的OOP Python方法通常都有雙下劃線的前綴和後綴。

這也是PyTorch模塊基類的工作方式。模塊基類覆蓋了_repr__函數。

字符串表示的是什麼

在大多數情況下,PyTorch提供的字符串表示與我們根據配置網絡層的方式所期望的基本一致。

但是,還有一些額外的信息需要強調。

卷積層

對於卷積層,kernel_size參數是一個Python元組(5,5),儘管我們只在構造函數中傳遞了數字5。

Network(
    (conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
    (conv2): Conv2d(6, 12, kernel_size=(5, 5), stride=(1, 1))
    (fc1): Linear(in_features=192, out_features=120, bias=True)
    (fc2): Linear(in_features=120, out_features=60, bias=True)
    (out): Linear(in_features=60, out_features=10, bias=True)
)

這是因爲我們的濾波器實際上有一個高度和寬度,當我們傳遞一個數字時,該層構造函數中的代碼假設我們需要一個方形濾波器(filter)。

self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5)
self.conv2 = nn.Conv2d(in_channels=6, out_channels=12, kernel_size=5)

stride是一個我們可以設置的額外參數,但是我們把它省略了。當在層構造函數中沒有指定stride時,層會自動設置它。

stride 告訴conv層,在整個卷積中,每個操作之後濾波器應該滑動多遠。這個元組表示當向右移動時滑動一個單元,向下移動時也滑動一個單元。

Linear 層

對於Linear 層,我們有一個額外的參數bias,它的默認參數值爲true。可以通過將它設置爲false來關閉它。

self.fc1 = nn.Linear(in_features=12*4*4, out_features=120)
self.fc2 = nn.Linear(in_features=120, out_features=60)
self.out = nn.Linear(in_features=60, out_features=10)

關於打印對象時顯示的信息,有一點需要注意,那就是它是完全任意的信息。

作爲開發人員,我們可以決定將任何信息放在那裏。然而,Python文檔告訴我們,信息應該足夠完整,以便在需要時可以用來重構對象。

https://docs.python.org/3/reference/datamodel.html#object.__repr__

訪問網絡層

好了,現在我們已經有了一個網絡實例,我們已經檢查了我們的層,讓我們看看如何在代碼中訪問它們。

在Python和許多其他編程語言中,我們使用點符號訪問對象的屬性和方法。

> network.conv1
Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))


> network.conv2
Conv2d(6, 12, kernel_size=(5, 5), stride=(1, 1))


> network.fc1
Linear(in_features=192, out_features=120, bias=True)


> network.fc2                                    
Linear(in_features=120, out_features=60, bias=True)


> network.out
Linear(in_features=60, out_features=10, bias=True)

這就是點表示法。用點表示法,我們用點來表示我們想要打開對象並訪問裏面的東西。我們已經用過很多次了,這裏提到的只是給這個概念一個標籤。

關於這一點需要注意的是與我們剛剛討論的網絡的字符串表示直接相關的是,這些代碼片段中的每一段也爲我們提供了每一層的字符串表示。

在網絡的情況下,網絡類實際上只是將所有這些數據編譯在一起,從而給出一個單獨的輸出。

Network(
    (conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
    (conv2): Conv2d(6, 12, kernel_size=(5, 5), stride=(1, 1))
    (fc1): Linear(in_features=192, out_features=120, bias=True)
    (fc2): Linear(in_features=120, out_features=60, bias=True)
    (out): Linear(in_features=60, out_features=10, bias=True)
)

關於這些對象的字符串表示的最後一件事是,在本例中,我們實際上並沒有使用print方法。

我們仍然獲取字符串表示的原因是我們使用的是Jupyter notebook,而在後臺notebook正在訪問字符串表示,因此它可以向我們顯示一些內容。這是字符串表示的主要用例的一個很好的例子。

訪問層權重

現在我們已經訪問了每一層,我們可以訪問每一層中的權重。我們來看看第一個卷積層。

> network.conv1.weight
Parameter containing:
tensor([[[[ 0.0692,  0.1029, -0.1793,  0.0495,  0.0619],
            [ 0.1860,  0.0503, -0.1270, -0.1240, -0.0872],
            [-0.1924, -0.0684, -0.0028,  0.1031, -0.1053],
            [-0.0607,  0.1332,  0.0191,  0.1069, -0.0977],
            [ 0.0095, -0.1570,  0.1730,  0.0674, -0.1589]]],




        [[[-0.1392,  0.1141, -0.0658,  0.1015,  0.0060],
            [-0.0519,  0.0341,  0.1161,  0.1492, -0.0370],
            [ 0.1077,  0.1146,  0.0707,  0.0927,  0.0192],
            [-0.0656,  0.0929, -0.1735,  0.1019, -0.0546],
            [ 0.0647, -0.0521, -0.0687,  0.1053, -0.0613]]],




        [[[-0.1066, -0.0885,  0.1483, -0.0563,  0.0517],
            [ 0.0266,  0.0752, -0.1901, -0.0931, -0.0657],
            [ 0.0502, -0.0652,  0.0523, -0.0789, -0.0471],
            [-0.0800,  0.1297, -0.0205,  0.0450, -0.1029],
            [-0.1542,  0.1634, -0.0448,  0.0998, -0.1385]]],




        [[[-0.0943,  0.0256,  0.1632, -0.0361, -0.0557],
            [ 0.1083, -0.1647,  0.0846, -0.0163,  0.0068],
            [-0.1241,  0.1761,  0.1914,  0.1492,  0.1270],
            [ 0.1583,  0.0905,  0.1406,  0.1439,  0.1804],
            [-0.1651,  0.1374,  0.0018,  0.0846, -0.1203]]],




        [[[ 0.1786, -0.0800, -0.0995,  0.1690, -0.0529],
            [ 0.0685,  0.1399,  0.0270,  0.1684,  0.1544],
            [ 0.1581, -0.0099, -0.0796,  0.0823, -0.1598],
            [ 0.1534, -0.1373, -0.0740, -0.0897,  0.1325],
            [ 0.1487, -0.0583, -0.0900,  0.1606,  0.0140]]],




        [[[ 0.0919,  0.0575,  0.0830, -0.1042, -0.1347],
            [-0.1615,  0.0451,  0.1563, -0.0577, -0.1096],
            [-0.0667, -0.1979,  0.0458,  0.1971, -0.1380],
            [-0.1279,  0.1753, -0.1063,  0.1230, -0.0475],
            [-0.0608, -0.0046, -0.0043, -0.1543,  0.1919]]]], 
            requires_grad=True
)

輸出是一個張量,但在我們研究這個張量之前,我們先來討論一下OOP。這是一個很好的例子,展示了對象是如何嵌套的。我們首先訪問位於網絡對象內部的conv層對象。

network.conv1.weight

然後,我們訪問權張量對象,它位於conv層對象內部,所以所有這些對象都鏈接在一起。

關於權張量的輸出有一件事需要注意,它說的是輸出頂部包含的參數。這是因爲這個特殊的張量是一個特殊的張量因爲它的值或者標量分量是我們網絡的可學習參數。

這意味着這個張量裏面的值,就是我們上面看到的那些,實際上是在網絡訓練的過程中習得的。當我們訓練時,這些權值會以使損失函數最小化的方式更新。

PyTorch參數類

跟蹤網絡中所有的張量權重。PyTorch有一個特殊的類,稱爲Parameter。Parameter類擴展了張量類,所以每層中的權張量就是這個Parameter類的一個實例。這就是爲什麼我們會在字符串表示輸出的頂部看到包含文本的參數。

我們可以在Pytorch源代碼中看到,Parameter類通過將包含正則張量類表示輸出的文本參數放在前面,從而覆蓋了__repr__函數。

def __repr__(self):
    return 'Parameter containing:\n' + super(Parameter, self).__repr__()

PyTorch的nn.Module類基本上是在尋找其值是Parameter類的實例的任何屬性,當它找到參數類的實例時,就會對其進行跟蹤。

所有這些實際上都是在幕後進行的PyTorch技術細節,我們將看到其中的一部分。

現在就我們的理解而言,重要的部分是張量權重形狀的解釋。在這裏,我們將開始使用在本系列早期學習的關於張量的知識。

現在讓我們看一下這些形狀,然後對其進行解釋。

張量權重形狀

在上一篇文章中,我們說過傳遞給層的參數值會直接影響網絡的權重。在這裏將看到這種影響。

對於卷積層,權重值位於濾波器內部,而在代碼中,濾波器實際上是權重張量本身。

層內的卷積運算是該層的輸入通道與該層內的濾波器之間的運算。這意味着我們真正擁有的是兩個張量之間的運算。

話雖如此,讓我們解釋這些權重張量,這將使我們更好地瞭解網絡內部的卷積操作。

請記住,張量的形狀實際上編碼了我們需要了解的有關張量的所有信息。

對於第一個conv 層,我們有1個顏色通道,應由6個5x5大小的濾波器進行卷積以產生6個輸出通道。這就是我們解釋層構造函數中的值的方式。

> network.conv1
Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))

不過,在層內部,對於這6個濾波器,我們沒有明確地有6個權重張量。實際上,我們使用單個權重張量表示所有6個濾波器,其形狀反映或說明了6個濾波器。

第一卷積層的權重張量的形狀告訴我們,我們有一個4階權重張量。第一個軸的長度爲6,這說明了6個濾波器。

> network.conv1.weight.shape
torch.Size([6, 1, 5, 5])

第二個軸的長度爲1,代表單個輸入通道,最後兩個軸的長度和寬度代表濾波器。

考慮這一點的方式就像我們將所有濾波器打包到一個張量中一樣。

現在,第二個conv層具有12個濾波器,不是單個輸入通道,而是有6個來自上一層的輸入通道。

> network.conv2.weight.shape
torch.Size([12, 6, 5, 5])

在這裏將此值 6 賦予每個濾波器一定的深度。我們的濾波器具有的深度與通道數匹配,而不是讓濾波器迭代地對所有通道進行卷積。

關於這些卷積層的兩個主要方面是,我們的濾波器使用單個張量表示,並且張量內的每個濾波器也具有一個深度,該深度說明了正在卷積的輸入通道。

  1. 所有濾波器均使用單個張量表示。

  2. 濾波器的深度由輸入通道決定。

我們的張量是4階張量。第一個軸代表濾波器的數量。第二個軸代表每個濾波器的深度,它對應於卷積的輸入通道數。

最後兩個軸代表每個過濾器的高度和寬度。我們可以通過索引權重張量的第一軸來拉出任何單個濾波器。

(Number of filters, Depth, Height, Width)

這給我們提供了一個高度和寬度爲5,深度爲6的濾波器。

權重矩陣

對於線性層或完全連接的層,我們將張量展平一階作爲輸入和輸出。我們將線性層中的in_features轉換爲out_features的方式是使用通常稱爲權重矩陣的2階張量。

這是由於權重張量在高度和寬度軸上均爲2階。

> network.fc1.shape
torch.Size([120, 192])


> network.fc2.shape                                    
torch.Size([60, 120])


> network.out.shape
torch.Size([10, 60])

在這裏,我們可以看到每個線性層都有一個2階的權重張量。我們在這裏可以看到的模式是權重張量的高度具有所需輸出特徵的長度和輸入特徵的寬度。

一、矩陣乘法

這個事實是由於矩陣乘法是如何執行的。讓我們通過一個較小的示例來了解這一點。

假設我們有兩個2階張量。第一個形狀爲3x4,第二個形狀爲4x1。現在,由於我們要演示的是矩陣乘法,因此請注意,這兩個2階張量的確是矩陣。

對於輸出中的每個行-列組合,通過獲取第一矩陣的相應行與第二矩陣的相應列的點積來獲得該值。

由於本示例中的第二個矩陣僅具有1列,因此我們將其全部使用了3次,但是這種想法是通用的。

該操作起作用的規則是,第一個矩陣中的列數必須與第二個矩陣中的行數匹配。如果該規則成立,則可以執行這樣的矩陣乘法運算。

點積意味着我們將相應組件的乘積相加。如果您想知道,點積和矩陣乘法都是線性代數概念。

二、使用矩陣表示的線性函數

像這樣的矩陣乘法的重要之處在於它們代表了可以用來構建神經網絡的線性函數。

具體而言,權重矩陣是線性函數,也稱爲線性映射,該線性映射將4維的向量空間映射到3維的向量空間。

當我們更改矩陣內的權重值時,實際上是在更改此函數,而這恰恰是我們在搜索網絡最終逼近的函數時要執行的操作。

讓我們看看如何使用PyTorch執行相同的計算。

三、使用PyTorch進行矩陣乘法

在這裏,我們使用in_features和weight_matrix作爲張量,並使用名爲matmul() 張量方法執行操作。我們現在知道的名稱matmul() 矩陣乘法的縮寫。

> weight_matrix.matmul(in_features)
tensor([30., 40., 50.])

一個迫在眉睫的問題是,我們如何才能一次訪問所有參數?有一個簡單的方法。讓我告訴你。

訪問網絡參數

第一個示例是最常見的方法,我們將在訓練過程中更新權重時使用它來遍歷權重。

for param in network.parameters():
    print(param.shape)


torch.Size([6, 1, 5, 5])
torch.Size([6])
torch.Size([12, 6, 5, 5])
torch.Size([12])
torch.Size([120, 192])
torch.Size([120])
torch.Size([60, 120])
torch.Size([60])
torch.Size([10, 60])
torch.Size([10])

第二種方法只是顯示如何也可以看到該名稱。這揭示了我們將不詳細介紹的內容,偏差也是可學習的參數。默認情況下,每個層都有一個偏差,因此對於每個層,我們都有一個權重張量和一個偏差張量。

for name, param in network.named_parameters():
    print(name, '\t\t', param.shape)


conv1.weight      torch.Size([6, 1, 5, 5])
conv1.bias      torch.Size([6])
conv2.weight      torch.Size([12, 6, 5, 5])
conv2.bias      torch.Size([12])
fc1.weight      torch.Size([120, 192])
fc1.bias      torch.Size([120])
fc2.weight      torch.Size([60, 120])
fc2.bias      torch.Size([60])
out.weight      torch.Size([10, 60])
out.bias      torch.Size([10])

現在,我們應該對可學習的參數,網絡內部的位置以及如何使用PyTorch訪問權重張量有了很好的瞭解。

在下一篇文章中,我們將瞭解如何通過將張量傳遞給層來處理它們。

文章中內容都是經過仔細研究的,本人水平有限,翻譯無法做到完美,但是真的是費了很大功夫,希望小夥伴能動動你性感的小手,分享朋友圈或點個“在看”,支持一下我 ^_^

英文原文鏈接是:

https://deeplizard.com/learn/video/stWU37L91Yc

加羣交流

歡迎小夥伴加羣交流,目前已有交流羣的方向包括:AI學習交流羣,目標檢測,秋招互助,資料下載等等;加羣可掃描並回復感興趣方向即可(註明:地區+學校/企業+研究方向+暱稱)

 謝謝你看到這裏! ????

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