Lesson 8. 卷積神經網絡 (CNN) 課程筆記 by Claude

這是以 Claude 輔助撰寫筆記的再次嘗試,這次要求它只能就所提供的資料來撰寫筆記。

還沒看內容正確性,先記錄下來,再找時間觀看。

我心目中理想的 Claude 輔助撰寫筆記是這樣:Claude 扮演大部分打字和整理的工作,我則接手修飾(因為 GAI 大多廢話贅詞很多)和撰寫真正的感想,而不是我之前形容的,心得分享變成了課程英翻中。

提醒:筆記由 Claude 輔助撰寫,內容有可能不完全正確。


1. 認識卷積

卷積是深度學習中的一個強大工具,特別是在處理圖像數據時。在機器學習中,一個重要的技術是特徵工程(feature engineering)。特徵是對數據的轉換,目的是使數據更容易被模型處理。例如,在 fast.ai 的表格數據預處理中,add_datepart 函數為 Bulldozers 數據集添加了日期特徵。那麼,我們可以從圖像中創建什麼樣的特徵呢?

在圖像中,特徵是視覺上的顯著屬性。例如,數字「7」的特徵是在數字頂部附近的水平邊緣,以及下面的從右上到左下的對角線邊緣。而數字「3」的特徵則包括左上和右下的對角線邊緣,左下和右上的對角線,以及中間、頂部和底部的水平邊緣。如果我們能夠提取圖像中邊緣出現位置的信息,並使用這些信息作為特徵,而不是原始像素,會怎麼樣?

事實證明,在計算機視覺中,尋找圖像中的邊緣是一個非常常見的任務,而且實現起來相當簡單。我們可以使用稱為卷積的操作來完成這個任務。卷積只需要乘法和加法——兩種操作,它們構成了本書中我們將看到的每一個深度學習模型的絕大部分工作!

卷積在圖像上應用一個(kernel)。核是一個小矩陣,如下圖右上角的 3×3 矩陣。

左側的 7×7 網格是我們要應用核的圖像。卷積操作將核的每個元素與圖像的 3×3 塊的每個元素相乘。這些乘法的結果然後被加在一起。下圖展示了將核應用於圖像中單個位置的例子,即圍繞單元格 18 的 3×3 塊。

讓我們用代碼實現。首先,我們創建一個小的 3×3 矩陣:

top_edge = tensor([[-1,-1,-1],
                   [ 0, 0, 0],
                   [ 1, 1, 1]]).float()

我們稱這個為核(因為這是計算機視覺研究者對它的稱呼)。然後我們需要一個圖像:

im3 = Image.open(path/'train'/'3'/'12.png')

現在我們要取圖像的左上 3×3 像素方塊,並將這些值中的每一個乘以核中的每一項。然後我們將它們加起來:

im3_t = tensor(im3)
im3_t[0:3,0:3] * top_edge

結果為:

tensor([[-0., -0., -0.],
        [0., 0., 0.],
        [0., 0., 0.]])
(im3_t[0:3,0:3] * top_edge).sum()

結果為:

tensor(0.)

到目前為止不是很有趣—左上角的所有像素都是白色。但讓我們選擇一些更有趣的點:

在圖像中的不同位置應用我們的核,我們可以得到不同的結果。例如,在某些位置,計算結果是 762.0,而在其他位置,結果可能是 -29.0。

讓我們看一下數學原理。該過濾器將取圖像中任何大小為 3×3 的窗口,如果我們將像素值命名為:

$$\begin{matrix} a1 & a2 & a3 \ a4 & a5 & a6 \ a7 & a8 & a9 \end{matrix}$$

它將返回 -a1-a2-a3+a7+a8+a9。如果我們在圖像的一部分,其中 a1a2a3 的總和與 a7a8a9 的總和相同,那麼這些項將相互抵消,我們將得到 0。然而,如果 a7 大於 a1a8 大於 a2a9 大於 a3,我們將得到更大的數值結果。因此,這個過濾器會檢測水平邊緣——更準確地說,它會檢測從上到下由暗變亮的邊緣。

更改我們的過濾器,使頂部行為 1 而底部行為 -1,將檢測從亮到暗的水平邊緣。將 1-1 放在列而不是行中,將給我們檢測垂直邊緣的過濾器。每組權重將產生不同類型的輸出。

在 PyTorch 中的卷積

卷積是如此重要且被廣泛使用的操作,以至於 PyTorch 內置了它。它被稱為 F.conv2d。PyTorch 文檔告訴我們它包括這些參數:

  • input:: 形狀為 (minibatch, in_channels, iH, iW) 的輸入張量
  • weight:: 形狀為 (out_channels, in_channels, kH, kW) 的過濾器

這裡 iH,iW 是圖像的高度和寬度(即 28,28),kH,kW 是我們核的高度和寬度(3,3)。但顯然 PyTorch 期望這兩個參數都是 4 階張量,而目前我們只有 2 階張量(即矩陣,或者說有兩個軸的數組)。

PyTorch 之所以需要這些額外的軸,是因為它有一些技巧。第一個技巧是 PyTorch 可以同時對多個圖像應用卷積。這意味著我們可以一次調用它處理一個批次中的所有項目!

第二個技巧是 PyTorch 可以同時應用多個核。所以讓我們也創建對角邊緣核,然後將我們的四個邊緣核堆疊成一個單一張量:

diag1_edge = tensor([[ 0,-1, 1],
                     [-1, 1, 0],
                     [ 1, 0, 0]]).float()
diag2_edge = tensor([[ 1,-1, 0],
                     [ 0, 1,-1],
                     [ 0, 0, 1]]).float()

edge_kernels = torch.stack([left_edge, top_edge, diag1_edge, diag2_edge])

PyTorch 最重要的技巧是它可以使用 GPU 並行完成所有這些工作——也就是說,同時應用多個核,到多個圖像,跨多個通道。並行完成大量工作對於高效地使用 GPU 至關重要;如果我們一次只執行這些操作中的一個,我們通常會運行得慢數百倍(如果我們使用前一節中的手動卷積循環,我們將慢數百萬倍!)。

通常我們希望不丟失那兩個像素。實現這一點的方法是添加填充(padding),這只是在我們圖像外部添加的額外像素。最常見的是,添加的像素值為零。

步幅和填充

通過適當的填充,我們可以確保輸出激活圖與原始圖像大小相同,這在構建架構時可以使事情簡單很多。

到目前為止,當我們將核應用於網格時,我們每次移動一個像素。但我們可以跳得更遠;例如,我們可以在每次核應用後移動兩個像素。這被稱為步幅 2 卷積(stride-2 convolution)。實踐中最常見的核大小是 3×3,最常見的填充是 1。正如您將看到的,步幅 2 卷積對於減小輸出大小很有用,而步幅 1 卷積則對於在不更改輸出大小的情況下添加層很有用。

理解卷積方程

為了解釋卷積背後的數學原理,fast.ai 學生 Matt Kleinsmith 想出了一個非常聰明的想法,展示從不同視角看 CNN。事實上,這是如此聰明,如此有幫助,我們在這裡也要展示它!

以下是我們的 3×3 像素圖像,每個像素用一個字母標記:

以下是我們的核,每個權重用一個希臘字母標記:

由於過濾器在圖像中適合四次,我們有四個結果。

這顯示了我們如何將核應用於圖像的每個部分以產生每個結果。

方程視圖如下。

注意,偏置項 b 對於圖像的每個部分都是相同的。您可以將偏置視為過濾器的一部分,就像權重(α,β,γ,δ)是過濾器的一部分一樣。

卷積可以表示為一種特殊的矩陣乘法,如下圖所示。權重矩陣就像來自傳統神經網絡的矩陣。然而,這個權重矩陣有兩個特殊屬性:

  1. 灰色顯示的零是不可訓練的。這意味著它們在優化過程中將保持為零。
  2. 一些權重是相等的,雖然它們是可訓練的(即可更改的),但它們必須保持相等。這些被稱為共享權重

零對應於過濾器無法觸及的像素。權重矩陣的每一行對應於過濾器的一次應用。

現在我們了解了什麼是卷積,讓我們用它來構建一個神經網絡。

2. 我們的第一個卷積神經網絡

沒有理由相信某些特定的邊緣過濾器是圖像識別最有用的核。此外,我們已經看到,在後面的層中,卷積核變成了對來自較低層次的特徵的複雜變換,但我們對如何手動構建這些沒有好的想法。

相反,最好是學習核的值。我們已經知道如何做到這一點——SGD!實際上,模型將學習對分類有用的特徵。

當我們使用卷積代替(或者除了)常規的線性層,我們創建了一個卷積神經網絡(CNN)。

創建 CNN

讓我們回到我們在先前章節中的基本神經網絡。它是這樣定義的:

simple_net = nn.Sequential(
    nn.Linear(28*28,30),
    nn.ReLU(),
    nn.Linear(30,1)
)

我們可以查看模型的定義:

Sequential(
  (0): Linear(in_features=784, out_features=30, bias=True)
  (1): ReLU()
  (2): Linear(in_features=30, out_features=1, bias=True)
)

現在我們想要創建一個類似於這個線性模型的架構,但使用卷積層代替線性層。nn.Conv2dF.conv2d 的模塊等價物。當創建架構時,它比 F.conv2d 更方便,因為它在我們實例化它時自動為我們創建權重矩陣。

以下是一個可能的架構:

def conv(ni, nf, ks=3, act=True):
    res = nn.Conv2d(ni, nf, stride=2, kernel_size=ks, padding=ks//2)
    if act: res = nn.Sequential(res, nn.ReLU())
    return res

simple_cnn = sequential(
    conv(1 ,4, ks=5),        #14x14
    conv(4 ,8),              #7x7
    conv(8 ,16),             #4x4
    conv(16,32),             #2x2
    conv(32,2, act=False),   #1x1
    Flatten(),
)

這裡需要注意的一點是,我們不需要指定 28×28 作為輸入大小。這是因為線性層需要權重矩陣中為每個像素都有一個權重,所以它需要知道有多少像素,但卷積會自動應用於每個像素,權重只取決於輸入和輸出通道的數量以及核大小,就像我們在前一節中看到的那樣。

當我們使用步幅 2 卷積時,我們通常會同時增加特徵數量。這是因為我們正在將激活圖中的激活數量減少 4 倍;我們不希望一次將層的容量減少太多。

理解卷積算術

感受野是用於計算一層中某一區域的圖像區域。在 Excel 電子表格"conv-example.xlsx"中,當我們點擊 conv2 部分中的一個單元格(顯示第二個卷積層的輸出),並點擊跟踪引用項,我們可以看到其直接引用項——它們是來自輸入層(左側)的相應 3×3 單元格區域和來自過濾器(右側)的單元格。如果我們再次點擊跟踪引用項,我們可以看到計算這些輸入所使用的單元格。

在這個例子中,我們只有兩個卷積層,每個都是步幅 2,所以現在我們正在追踪回到輸入圖像。我們可以看到,輸入層中的 7×7 單元格區域被用來計算 Conv2 層中的單個綠色單元格。這個 7×7 區域是該單元格在 Conv2 中的感受野

正如我們從這個例子中看到的,我們在網絡中的位置越深(特別是,在一個層之前有多少步幅 2 卷積),該層中一個激活的感受野就越大。一個大的感受野意味著用於計算該層中每個激活的輸入圖像量也很大。我們現在知道,在網絡的更深層次中,我們有語義豐富的特徵,對應於更大的感受野。因此,我們會預期,對於我們的每個特徵,我們將需要更多的權重來處理這種不斷增加的複雜性。這是另一種說法,就是當我們在網絡中引入步幅 2 卷積時,我們也應該增加通道數。

彩色圖像的處理

到目前為止,我們只展示了黑白圖像的例子,每個像素只有一個值。在實踐中,大多數彩色圖像每個像素有三個值來定義它們的顏色。

彩色圖像是一個 3 階張量:

im = image2tensor(Image.open(image_bear()))
im.shape

結果為:

torch.Size([3, 1000, 846])

第一個軸包含通道:紅色、綠色和藍色。

在卷積層中,我們會像之前一樣對每個通道執行相同的操作。但由於我們有多個通道,我們需要將所有這些結果相加以獲得一個數字。換句話說,對於滑動窗口的每個位置,我們將有:

(紅色通道的卷積結果) + (綠色通道的卷積結果) + (藍色通道的卷積結果)

這使我們可以處理彩色圖像,而不需要對我們已經開發的基於卷積的架構做任何特殊修改。只需確保您的第一層有三個輸入。

處理彩色圖像有很多方法。例如,您可以將它們轉換為黑白,從 RGB 轉換為 HSV(色調、飽和度和值)色彩空間等。一般來說,從實驗上看,只要您在轉換中不丟失信息,改變顏色的編碼方式不會對您的模型結果產生任何影響。

3. 提高訓練穩定性

讓我們來看看如何讓我們的 CNN 訓練得更好。

增加批次大小

使訓練更穩定的一種方法是增加批次大小。更大的批次具有更準確的梯度,因為它們是從更多數據計算的。但不利的一面是,更大的批次大小意味著每個 epoch 的批次更少,這意味著模型更新權重的機會更少。

1cycle 訓練

我們的初始權重不適合我們嘗試解決的任務。因此,一開始就使用高學習率進行訓練是危險的:我們可能會使訓練立即發散。我們可能也不希望以高學習率結束訓練,這樣我們就不會跳過最小值。但我們希望在訓練期間的其餘時間使用高學習率,因為這樣我們能夠更快地訓練。因此,我們應該在訓練期間改變學習率,從低到高,然後再回到低。

Leslie Smith(是的,就是發明學習率查找器的那個人!)在他的文章"超級收斂:使用大學習率非常快速地訓練神經網絡"中發展了這個想法。他設計了一個學習率時間表,分為兩個階段:一個階段學習率從最小值增長到最大值(預熱),另一個階段學習率回到最小值(退火)。Smith 將這些方法的組合稱為 1cycle 訓練

1cycle 訓練允許我們使用比其他類型的訓練更高的最大學習率,這提供了兩個好處:

  • 通過使用更高的學習率,我們訓練得更快——Smith 將這種現象稱為超級收斂
  • 通過使用更高的學習率,我們過擬合較少,因為我們跳過了尖銳的局部最小值,最終進入損失的更平滑(因此更具普遍性)部分。

第二點是一個有趣而微妙的點;它基於這樣的觀察:一個很好地泛化的模型是一個,如果你稍微改變輸入,其損失不會變化很多的模型。如果一個模型在較大的學習率下訓練很長時間,並且能夠找到一個好的損失,那麼它必須找到一個也能很好地泛化的區域,因為它在從批次到批次(這基本上是高學習率的定義)大幅跳躍。問題是,正如我們所討論的,直接跳到高學習率更可能導致損失發散,而不是看到損失改善。所以我們不直接跳到高學習率。相反,我們從一個不會使損失發散的低學習率開始,然後允許優化器通過逐漸到更高更高的學習率來逐漸找到越來越平滑的參數區域。

然後,一旦我們為我們的參數找到了一個很好的平滑區域,我們想要找到該區域的最佳部分,這意味著我們必須再次降低我們的學習率。這就是為什麼 1cycle 訓練有一個逐漸的學習率預熱和一個逐漸的學習率冷卻。許多研究人員發現,在實踐中,這種方法導致更精確的模型,並且訓練得更快。這就是為什麼它是 fastai 中 fine_tune 默認使用的方法。

Smith 在"關於神經網絡超參數的規範方法:第 1 部分"中引入了循環動量的想法。它建議動量在與學習率相反的方向上變化:當我們處於高學習率時,我們使用較少的動量,當我們在退火階段時,我們再次使用更多的動量。

我們可以在 fastai 中通過調用 fit_one_cycle 來使用 1cycle 訓練:

def fit(epochs=1, lr=0.06):
    learn = Learner(dls, simple_cnn(), loss_func=F.cross_entropy,
                    metrics=accuracy, cbs=ActivationStats(with_hist=True))
    learn.fit_one_cycle(epochs, lr)
    return learn

批量標準化

為了解決訓練速度慢和最終結果不佳的問題,我們需要修復初始大比例的接近零的激活,然後在整個訓練過程中維持良好的激活分佈。

Sergey Ioffe 和 Christian Szegedy 在 2015 年的論文"批量標準化:通過減少內部協變量偏移加速深度網絡訓練"中提出了解決這個問題的方案。在摘要中,他們描述了我們看到的問題:

“訓練深度神經網絡的複雜性在於,每層的輸入分佈在訓練期間會發生變化,因為前一層的參數會變化。這會減慢訓練速度,需要更低的學習率和仔細的參數初始化…我們將這種現象稱為內部協變量偏移,並通過標準化層輸入來解決這個問題。”

他們的解決方案,他們說:

“將標準化作為模型架構的一部分,並為每個訓練小批量執行標準化。批量標準化允許我們使用更高的學習率,對初始化不那麼小心。”

批量標準化(通常簡稱為 batchnorm)的工作原理是通過取一層激活的均值和標準偏差的平均值,然後用這些來標準化激活。然而,這可能會導致問題,因為網絡可能希望某些激活非常高,以便做出準確的預測。所以他們還添加了兩個可學習的參數(意味著它們將在 SGD 步驟中更新),通常稱為 gammabeta。在標準化激活以獲得新的激活向量 y 后,batchnorm 層返回 gamma*y + beta

這就是為什麼我們的激活可以有任何均值或方差,獨立於前一層結果的均值和標準偏差。這些統計數據是單獨學習的,使我們的模型更容易訓練。訓練和驗證期間的行為不同:在訓練期間,我們使用批次的均值和標準偏差來標準化數據,而在驗證期間,我們使用在訓練期間計算的統計數據的運行均值。

讓我們將 batchnorm 層添加到 conv 中:

def conv(ni, nf, ks=3, act=True):
    layers = [nn.Conv2d(ni, nf, stride=2, kernel_size=ks, padding=ks//2)]
    if act: layers.append(nn.ReLU())
    layers.append(nn.BatchNorm2d(nf))
    return nn.Sequential(*layers)

包含批量標準化層的模型往往比不包含它們的模型泛化得更好。雖然我們還沒有看到對這裡發生的情況的嚴格分析,但大多數研究人員認為,原因是批量標準化為訓練過程增加了一些額外的隨機性。每個小批量的均值和標準偏差會與其他小批量略有不同。因此,激活將被不同的值標準化。為了使模型做出準確的預測,它將不得不學會對這些變化保持魯棒性。通常,向訓練過程添加額外的隨機化通常有幫助。

Dropout

Jeremy 使用 Excel 電子表格中的 “conv-example (dropout)” 頁面來展示 Dropout 的工作原理:

  1. 創建一個介於 0 和 1 之間的隨機數數組
  2. 創建一個基於這些數字的"dropout 掩碼",如果隨機數大於 dropout 因子(例如 0.5),則為 1,否則為 0
  3. 將此掩碼乘以過濾後的圖像,產生一個"損壞"的圖像
  4. 使用這個損壞的圖像作為下一層的輸入

目標是迫使網絡學習更魯棒的表示。通過隨機"關閉"或丟棄不同的激活,網絡不能依賴於任何單一特徵,而必須學會使用多種線索來識別對象。

Dropout 是由 Geoffrey Hinton 的研究組開發的,在神經網絡訓練中已成為標準的正則化技術。您可以通過調整 dropout 因子來控制多少激活被丟棄,從而在良好的泛化和良好的訓練性能之間取得平衡。

4. 池化(Pooling)

池化是 CNN 架構中的另一個常見組件。它的目的是減少特徵圖的大小,同時保留最重要的信息。

最大池化(Max Pooling)

最大池化在一個滑動窗口(例如 2×2)中取最大值。例如,在步長為 2 的 2×2 最大池化中,輸出將是輸入高度和寬度的一半。

最大池化適合在只關心特徵是否存在而不關心確切位置的情況。例如,如果您想找到圖像角落的一個小熊,最大池化可能比平均池化更有效,因為它只會保留最強的激活。

平均池化(Average Pooling)

平均池化取滑動窗口中所有值的平均值。它通常在網絡的最後階段使用,例如對 7×7 特徵圖應用全局平均池化。

平均池化適合當您關心特徵在整個區域的整體存在時。例如,如果您想確定一張照片主要是否包含一隻熊,平均池化可能更合適,因為它考慮了整個 7×7 網格中的所有激活。

如 Jeremy 所提及,許多現代架構不再使用單獨的池化層,而是使用步幅為 2 的卷積來同時執行特徵提取和下採樣。fastai 使用一種稱為 “Concat Pooling” 的技術,它同時執行最大池化和平均池化,然後將結果連接在一起,這樣就可以同時獲得兩種方法的優點。

池化與步幅卷積

在早期的 CNN 架構中,通常的做法是使用卷積後跟隨最大池化,再卷積,再最大池化等。每個 2x2 的最大池化會將激活數量減少到原來的四分之一。這種方式會讓我們最終得到很少的激活,這就是我們想要的。

現代的做法略有不同。現在我們通常不使用獨立的最大池化層,而是使用步幅為 2 的卷積。當我們應用滑動窗口時,我們不是從列 “G” 移動到 “H”,再到 “I”,而是會跳過一列,從 “G” 直接到 “I”,再到 “K”。這被稱為步幅 2 卷積,這樣做會讓每次卷積操作將特徵圖的大小在每個維度上減半,總體減少到四分之一。

現代架構中的另一個變化是,我們通常不在最後使用單一的全連接層,而是繼續使用步幅 2 卷積直到得到約 7x7 的網格,然後在最後進行一次全局池化。現在我們通常不使用最大池化,而是使用平均池化,即對每個 7x7 特徵的激活取平均值。

這種做法對於理解模型的行為很重要。例如,一個 ImageNet 式的圖像檢測器會最終得到一個 7x7 網格 - 假設它在嘗試判斷圖像中是否有熊 - 在 7x7 網格的每個部分,它基本上在問:“圖像的這部分有熊嗎?”,然後取這 49 個預測的平均值來決定圖像中是否有熊。

如果是一張主要是熊的照片,這種方法效果很好,因為 7x7 網格中的大部分區域都包含熊的部分。但如果是一隻在角落的小熊,那麼 49 個方格中可能只有一個包含熊,更糟的是,如果照片中有很多不同的物體,只有一個是熊,這可能不是一個好的熊檢測器。

正如 Jeremy 所說,這就是為什麼模型構建的細節很重要。如果你想找到照片中只有一小部分是熊的情況,你可能會選擇使用最大池化而不是平均池化,因為最大池化會在 49 個網格部分中的任何一個看起來像熊的情況下認為這是一張熊的照片。

5. 激活函數

激活函數為神經網絡引入非線性,這使得網絡能夠學習複雜的模式。在卷積神經網絡中最常用的激活函數是 ReLU(Rectified Linear Unit):

ReLU(x) = max(0, x)

我們在 Excel 示例中也看到了這一點,在計算卷積後,我們應用了 ReLU 函數 —— 如果結果小於零,則將其設為零。

除了 ReLU 外,還有其他幾種激活函數如 sigmoid、tanh 和 Leaky ReLU 等。但如 Jeremy 所言:“基本上任何非線性都可以工作得很好,所以我們不會花太多時間討論激活函數。”

如果不使用任何激活函數(或使用線性激活函數),那麼無論網絡有多深,它始終等同於一個單層線性模型,因為多個線性變換的組合仍然是線性的。

6. 總結

我們已經看到,卷積只是一種矩陣乘法,對權重矩陣有兩個約束:一些元素總是零,一些元素是綁定的(強制始終具有相同的值)。這些約束允許我們在模型中使用更少的參數,而不犧牲表示複雜視覺特徵的能力。這意味著我們可以更快地訓練更深的模型,減少過擬合。

雖然萬能近似定理表明在一個隱藏層的全連接網絡中可能表示任何東西,但我們現在已經看到,在實踐中,通過對網絡架構進行思考,我們可以訓練出更好的模型。

在本筆記中,我們學習了 CNN 的關鍵組件:

  • 卷積操作如何工作
  • 卷積層的參數:核大小、步幅和填充
  • 使用批量標準化和 dropout 來改善訓練穩定性和泛化能力
  • 不同類型的池化操作及其使用場景
  • 1cycle 訓練方法及其優點

所有這些技術共同構成了現代 CNN 的基礎,這些網絡已經在各種視覺任務中取得了驚人的成功。