跳至主要內容

    AlexNet、VGG、Inception:影像模型解過哪些瓶頸?

    從可訓練的大型CNN,到重複小卷積與控制多分支的計算成本

    sunny 桑尼· · 27 MIN READ · 162 次瀏覽

    AlexNet、VGG、Inception:影像模型解過哪些瓶頸?
    本文目錄

    AlexNet、VGG與Inception值得學的地方,是它們把不同的設計問題攤開來處理:更大的CNN怎麼訓練?網路變深時,能不能用規律的積木搭建?想同時看不同尺度,又怎麼避免通道與計算量失控?

    這幾個轉折主要發生在2012到2014年前後,早於本系列所回顧的大型語言模型普及期。上一篇已拆開卷積、Pooling與感受野,這篇把零件接成模型,重點放在「為什麼這樣接」及「付出什麼代價」。

    下面的架構原圖來自D2L教材,歷史細節另對照原始論文。三張訓練曲線是教材在Fashion-MNIST上的示範,本站沒有重跑這些訓練;我們實際執行的是文末的小型形狀、參數與運算檢查。兩種證據各自回答不同問題。

    AlexNet:擴大模型,也要讓它訓練得起來

    在傳統影像辨識流程中,常見做法是先設計SIFT、HOG等特徵,再交給分類器。CNN讓特徵提取也成為可訓練的部分:分類損失沿反向傳播更新卷積權重,讓前面的表示和後面的任務一起調整。這不表示資料整理、架構設計與工程判斷從此消失。

    AlexNet在2012年的重要性,是把大型資料、較大的卷積網路與GPU計算結合,展示這條路能在當時的大規模影像任務上發揮作用。把功勞只歸到「多幾層」或「換一個激活函數」,都會漏掉其他條件。原論文的§3與§4分別交代架構、GPU配置與過擬合處理。

    圖1|D2L的LeNet/AlexNet教學比較,由下往上閱讀。右欄有5個卷積層,再接4096、4096、1000的全連接輸出;Pooling另列,但不算進這裡的8個有權重層。此圖是簡化示意,沒有畫出原版雙GPU分組連線與LRN。來源:D2L,CC BY-SA 4.0;完整SVG轉PNG,手機保留全圖,再分欄放大。

    右欄的卷積核從11×11、5×5,走到多個3×3;最後才進入全連接部分。不同層的核、步長、通道數各自設計,整個網路還不是一套高度一致的重複積木。

    有三項要一起理解:

    • 局部運算能擴大,但計算與記憶體會成為限制。 原版把模型切到兩張GPU,某些卷積層只接同卡上的前層通道,其他層才跨卡連接。這包含連線結構的選擇,不只是把兩張卡插上去。
    • ReLU改變激活與梯度的行為。 正半軸局部導數為1,能避開sigmoid兩端飽和的部分問題,但不保證深網所有梯度正常,也可能有長期不啟動的單元。
    • 較大容量仍要控制過擬合。 原版在前兩個全連接層使用Dropout,也使用資料增強。這些不是從架構圖的方框數量就能讀出的訓練條件。

    圖中的RGB輸入與1000類輸出,是大型影像分類設定的示意;D2L該章實際的Fashion-MNIST程式則使用單通道輸入、10類輸出,並以普通單流Conv2d省略原版分組及LRN。讀者可以藉此理解骨架,但不能把這段教學程式稱為原始比賽設定的完整復現。

    VGG:先把「一個block怎麼長」規律化

    如果每一層都重新挑核大小、步長與下採樣位置,模型一深,設計與修改就會變得繁瑣。VGG提供一種容易推演的方式:重複幾次3×3卷積與ReLU,保留空間大小,再做一次2×2、步長2的Max Pooling。

    圖2|擷取D2L原圖的中、右欄:左側是VGG block,右側是重複block再接分類頭。資料由下往上;省略號代表重複結構,不是少畫一個固定層數的完整VGG-16。來源:D2L,CC BY-SA 4.0;裁切、轉PNG,手機再把兩欄放大,原框與箭頭保留。

    3×3、步長1、每側padding1會保留高寬,所以可以先多做幾次非線性轉換,再決定何時縮小特徵圖。若每做一次卷積就讓高寬減半,空間格子很快就不夠用了。

    小核堆疊,改的是運算結構,不只是一個尺寸

    假設輸入、中間、輸出通道都固定為C,步長1,先不計偏置。兩層3×3可以讓中央輸出接觸到原始輸入5×5範圍;一層5×5也能有這個理論範圍,但權重數不同:

    兩層3×3:2 × 9 × C²  = 18C²
    一層5×5:    25 × C² = 25C²
    

    C=4時,就是288與400個權重。文末程式會實際建立這兩個小結構,再用正值輸入、全1卷積核檢查兩層3×3中央位置的梯度支援,空間範圍確實是25格。輸入有4個通道,因此若沿通道一起數,非零梯度位置會有100個,不能把兩種數法混在一起。

    兩個結構的輸出形狀與理論感受野相同,並不表示數值或可表達函數相同。兩層之間多一次ReLU,改變了非線性結構;即使拿掉ReLU,也不能推論每個任意5×5核都能由指定中間寬度的兩個3×3核表示。

    上面的參數比較還依賴「各層通道都相同」的前提。實際網路可能改變通道寬度,也要儲存更多中間activation。不能從這個算式直接推論整個VGG一定比AlexNet參數少、記憶體小或跑得快。

    VGG-11、16、19,數的是哪些層?

    原始VGG論文的Table 1列出一組架構家族。下面摘出三個常見配置,數字包含卷積與全連接的有權重層:

    常見名稱/原文配置五個stage的卷積層數卷積+全連接
    VGG-11/A1、1、2、2、28+3=11
    VGG-16/D2、2、3、3、313+3=16
    VGG-19/E2、2、4、4、416+3=19

    Pooling、ReLU不是這個命名中的額外權重層。原文另外還有同為16層、部分使用1×1核的配置C;這裡的VGG-16指常見的D,不能說所有VGG版本都只使用3×3卷積。

    VGG在2014年提出預印本,正式論文刊於ICLR 2015。這裡的重點是可重複設計及深度比較,而不是把後續較深的架構當成同一個模型換名字。

    Inception:同時看多種尺度,分支先各算各的

    一個位置附近的細紋、較大輪廓,可能需要不同空間範圍的計算。Inception把幾條路並排:有的做1×1,有的做3×3,有的做5×5,還有Pooling路徑;最後沿通道方向串接。

    圖3|從底部Input分成四路,在頂部Concatenation合併。中間兩路先做1×1,再做較昂貴的3×3或5×5;最右路先Pooling再做1×1。來源:D2L,CC BY-SA 4.0;完整原圖轉PNG,手機先留全圖,再由左至右放大各路運算框;跨區連線以全圖為準。

    這裡的Pooling路徑採3×3、步長1、padding1,所以不會把空間尺寸減半。其他路徑也用對應padding與步長1,使四路的高寬一致,才能沿通道串接。

    串接不是相加。 若四條路各輸出4、6、2、2個通道,串接後就是14個通道,各路結果都保留下來;元素相加則需要相容形狀,這裡的4與6通道就不能直接逐元素相加。這也和下一篇將談的殘差加法不同。

    用本站的小例,把輸入設為1×8×9×9,不含卷積偏置:

    路徑運算順序輸出形狀N×C×H×W
    11×1,8→41×4×9×9
    21×1,8→3;3×3,3→61×6×9×9
    31×1,8→2;5×5,2→21×2×9×9
    43×3 MaxPool,步長1;1×1,8→21×2×9×9
    串接沿C軸依序接起來1×14×9×9

    卷積後都接ReLU。這些通道數是為了驗算而自訂的小例,不是原始GoogLeNet某一個完整模組的規格。四路合計350個卷積權重;它的用途是把資料流看清楚,不是聲稱350個權重已經能做影像辨識。

    1×1降通道:讓大核不用先吃下所有通道

    多尺度不是免費的。若把8個輸入通道直接交給輸出6通道的3×3核,權重數是:

    8 × 6 × 3 × 3 = 432
    

    先用1×1把8個通道變成3個,再做3×3得到6個通道,則是:

    8 × 3 + 3 × 6 × 3 × 3 = 24 + 162 = 186
    

    這正是上表的第2路。1×1的工作是同座標的通道混合,不直接讀相鄰像素;接在它後面的3×3才做空間運算。若中間通道設得太大,先加1×1也可能增加成本,所以「有瓶頸層就一定更省」不是通則。

    對這個9×9、batch size 1的限定例子,按密集卷積的邏輯乘加次數計算,兩個方案分別是:

    直接3×3:432 × 9 × 9 = 34,992 MAC
    先降通道:186 × 9 × 9 = 15,066 MAC
    

    這裡把一次乘加計作一個MAC,包含同尺寸輸出各位置的密集核計算,沒有計入偏置、ReLU、Pooling比較、串接與記憶體搬移,也不是實測硬體指令數。不同報表可能把一次乘法加一次加法算成兩個FLOPs,不能把MAC與FLOPs數字直接混比。

    先壓窄通道也改變了表示能力,加上中間ReLU後更不是和原本大通道卷積數值等價。這個成本檢查沒有證明準確率不變,MAC較少也不能直接換算成同倍率的實際加速。

    GoogLeNet:Inception是積木,完整模型還有前後段

    Inception是一種模組;GoogLeNet則是使用這些模組的具體網路。原始設計參與2014年ImageNet競賽,論文發表於CVPR 2015,不能把兩個年份當成互相矛盾。

    圖4|D2L的簡化GoogLeNet主幹,原圖由左到右;三組Inception分別重複2、5、2次,共9個,尾端有Global AvgPool與FC。手機先保留原方向,再把全圖逆時針旋轉90度,改成由下往上閱讀;節點文字轉正,重複次數標記隨原圖旋轉,數值不改。來源:D2L,CC BY-SA 4.0;轉PNG與上述編排,未新增節點或連線。

    前段卷積與Pooling先建立特徵,主體反覆使用Inception,中間再安排下採樣;最後以全域平均池化整理每個通道,再接分類頭。這比「全網都在並行做四種核」更精確:並行發生在模組內,模組之間仍有先後關係。

    Global Average Pooling省掉哪一段大矩陣?

    若有14×9×9特徵,直接Flatten會得到1134個數。全域平均池化則對每個通道的9×9位置取平均,只留下14個數。

    若兩者後面都接5類的單層線性分類頭,包含偏置的參數量為:

    Flatten之後:1134 × 5 + 5 = 5675
    Global AvgPool之後:14 × 5 + 5 = 75
    

    省掉的是這種分類頭對每個空間位置分別配一個權重的需求;不是整個模型從此零參數,也不是完全沒有全連接層。全域平均也會抹去部分空間位置差異,因此兩種頭的函數與效果並不等價。

    本站程式實際算了全域平均,並建立兩個Linear層檢查參數數量;這兩個Linear沒有做前向預測或訓練。不能把參數量的差異說成已測得準確率或泛化改善。

    GoogLeNet原論文的§5還有訓練期的兩個輔助分類頭,各以0.3權重加入損失,推論時拿掉;主分類頭另使用Dropout。D2L本篇採用的教學程式省略這些部分及原版LRN,以96×96、10類Fashion-MNIST示範主幹。圖4沒有呈現輔助頭,不能拿它當原版全部訓練分支的完整圖。

    三張教材曲線,應該拿來看什麼?

    架構圖回答資料怎麼流,曲線回答特定訓練流程記錄了什麼。先把這三張圖的共同前提放在前面:它們來自D2L各章的PyTorch分頁,都是Fashion-MNIST、batch size 128、SGD learning rate 0.01、10 epochs。不是原始ImageNet競賽圖,也不是本站重跑。

    原始Fashion-MNIST為28×28、單通道、10類。教材將AlexNet與縮窄版VGG的輸入放大到224×224,GoogLeNet放大到96×96;放大沒有增加新的原始觀測細節。VGG的各stage通道是16、32、64、128、128,不是常見標準寬度64、128、256、512、512。

    圖5|D2L AlexNet教學結果:輸入resize到224×224,兩個4096-unit隱藏全連接層,Dropout p=0.5。藍實線train_loss、橙虛線val_loss、綠點畫線val_acc。來源:D2L,CC BY-SA 4.0;保留完整座標、圖例與三條曲線,SVG轉PNG。不是2012年ImageNet結果。

    圖6|D2L的VGG-11式縮窄版本:卷積層數為1、1、2、2、2,但通道縮成16、32、64、128、128;輸入224×224,兩個4096-unit隱藏全連接層與Dropout p=0.5。來源:D2L,CC BY-SA 4.0;完整SVG轉PNG。它不是標準寬度VGG-16的訓練成績。

    圖7|D2L簡化GoogLeNet教學結果,輸入96×96、10類,使用主分類頭。後段val_loss不必單調下降;圖中波動本身不足以確定原因。來源:D2L,CC BY-SA 4.0;完整SVG轉PNG。不是2014年比賽成績。

    讀這三張圖時,再檢查四件事:

    1. 同一個y軸上畫了不同指標。 Loss是平均交叉熵,accuracy是0到1的正確比例;綠線高於藍線,不表示「accuracy超過loss所以模型較好」。
    2. val在這份教材程式裡的來源是官方test split。 訓練集60,000筆,train=False的10,000筆被當成val使用,沒有另外保留一份未參與選模的測試集。不能拿它當成嚴格隔離的最終泛化證據。
    3. 圖上的點是batch指標的彙整。 Helper會把一組mini-batch平均loss/accuracy再平均;最後一批較短,並不保證是精確按樣本數加權的整個epoch統計。這裡的交叉熵也沒有額外加上weight-decay penalty。
    4. 比較條件沒有控制一致。 輸入解析度、模型寬度、初始化細節及正則化各有差異,沒有公開統一固定seed。程式要求至多一張GPU,但靜態圖未記錄實際執行裝置與耗時。

    所以,可以用它們練習辨識曲線、追查訓練設定;不能從三個終點直接宣布哪一種架構在所有影像任務都最好。訓練與val loss接近,也不能單憑這點就證明Dropout造成了良好泛化;還需要控制其他因素的比較與合適的資料切分。

    小型實跑:先驗形狀與成本,再談訓練成效

    下面的完整程式不下載資料、不訓練完整AlexNet/VGG/GoogLeNet。它做三種檢查:固定通道小卷積的參數與中央感受野、四路小模組的實際輸出及串接、GAP與兩個可能分類頭的參數量。

    本次環境是Python3.10.12、PyTorch2.3.1+cu118。卷積與GAP數值使用CPU float64;卷積核全設為1。最後兩個Linear只建立並計數,保留框架預設初始化與dtype,沒有執行其前向,也沒有把它們稱為全1的float64分類器。

    可在獨立虛擬環境安裝torch==2.3.1,存成architecture_checks.py後執行python architecture_checks.py。不同平台的PyTorch建置尾碼可能不同;此處不比較速度或GPU效率。

    import torch
    from torch import nn
    
    print(f"torch={torch.__version__} device=cpu dtype=float64")
    
    def conv(cin, cout, kernel, padding=0):
        layer = nn.Conv2d(cin, cout, kernel, padding=padding, bias=False).double()
        with torch.no_grad():
            layer.weight.fill_(1.)
        return layer
    
    def count(model):
        return sum(p.numel() for p in model.parameters())
    
    # A controlled same-width comparison; not complete VGG/AlexNet models.
    stack = nn.Sequential(conv(4, 4, 3, 1), nn.ReLU(), conv(4, 4, 3, 1), nn.ReLU())
    large = conv(4, 4, 5, 2)
    x = torch.ones((1, 4, 9, 9), dtype=torch.float64, requires_grad=True)
    z = stack(x)
    z[0, 0, 4, 4].backward()
    mask = x.grad.ne(0).any(dim=1)[0]
    expected = torch.zeros((9, 9), dtype=torch.bool)
    expected[2:7, 2:7] = True
    assert torch.equal(mask, expected)
    assert count(stack) == 2 * 3 * 3 * 4 * 4 == 288
    assert count(large) == 5 * 5 * 4 * 4 == 400
    assert tuple(z.shape) == tuple(large(x).shape) == (1, 4, 9, 9)
    print(f"same_width_no_bias two3x3_weights={count(stack)} single5x5_weights={count(large)} output_shape={list(z.shape)} stacked_center_spatial_support={int(mask.sum())}")
    
    # Four-branch teaching block with our own small channel counts.
    branches = nn.ModuleList([
        nn.Sequential(conv(8, 4, 1), nn.ReLU()),
        nn.Sequential(conv(8, 3, 1), nn.ReLU(), conv(3, 6, 3, 1), nn.ReLU()),
        nn.Sequential(conv(8, 2, 1), nn.ReLU(), conv(2, 2, 5, 2), nn.ReLU()),
        nn.Sequential(nn.MaxPool2d(3, stride=1, padding=1), conv(8, 2, 1), nn.ReLU())
    ])
    u = torch.arange(1., 649., dtype=torch.float64).reshape(1, 8, 9, 9) / 100
    with torch.no_grad():
        parts = [branch(u) for branch in branches]
        joined = torch.cat(parts, dim=1)
    assert [list(p.shape) for p in parts] == [[1, 4, 9, 9], [1, 6, 9, 9], [1, 2, 9, 9], [1, 2, 9, 9]]
    assert list(joined.shape) == [1, 14, 9, 9]
    start = 0
    for part in parts:
        assert torch.equal(joined[:, start:start + part.shape[1]], part)
        start += part.shape[1]
    assert count(branches) == 32 + (24 + 162) + (16 + 100) + 16 == 350
    print("toy_branch_shapes=", [list(p.shape) for p in parts])
    print(f"concat_output_shape={list(joined.shape)} total_convolution_weights_no_bias={count(branches)}")
    plain_weights = 8 * 6 * 3 * 3
    reduced_weights = 8 * 3 + 3 * 6 * 3 * 3
    assert reduced_weights == count(branches[1]) == 186
    print(f"branch2_no_bias plain3x3_weights={plain_weights} reduced1x1_then3x3_weights={reduced_weights} dense_conv_MACs_per_sample={plain_weights * 9 * 9}_vs_{reduced_weights * 9 * 9}")
    with torch.no_grad():
        pooled = nn.AdaptiveAvgPool2d((1, 1))(joined).flatten(1)
    assert torch.allclose(pooled, joined.mean(dim=(2, 3)))
    # Only count these two possible classifier heads; neither is fitted.
    pooled_head = nn.Linear(14, 5)
    flat_head = nn.Linear(14 * 9 * 9, 5)
    assert count(pooled_head) == 75 and count(flat_head) == 5675
    print(f"global_average_shape={list(pooled.shape)} five_class_head_parameters_with_bias={count(pooled_head)} flatten_head_parameters_with_bias={count(flat_head)}")
    print("scope=hand-set CPU shape, weight-count, center-gradient-support, branch-concatenation and pooling checks; dense convolution MAC formulas exclude bias, activations, max pooling and memory movement; no training, dataset, timing, accuracy comparison or claim of equivalent models")
    

    本次執行觀察如下;首列是環境,其他列對應本文的小例,而非任何原始模型的完整參數表:

    torch=2.3.1+cu118 device=cpu dtype=float64
    same_width_no_bias two3x3_weights=288 single5x5_weights=400 output_shape=[1, 4, 9, 9] stacked_center_spatial_support=25
    toy_branch_shapes= [[1, 4, 9, 9], [1, 6, 9, 9], [1, 2, 9, 9], [1, 2, 9, 9]]
    concat_output_shape=[1, 14, 9, 9] total_convolution_weights_no_bias=350
    branch2_no_bias plain3x3_weights=432 reduced1x1_then3x3_weights=186 dense_conv_MACs_per_sample=34992_vs_15066
    global_average_shape=[1, 14] five_class_head_parameters_with_bias=75 flatten_head_parameters_with_bias=5675
    scope=hand-set CPU shape, weight-count, center-gradient-support, branch-concatenation and pooling checks; dense convolution MAC formulas exclude bias, activations, max pooling and memory movement; no training, dataset, timing, accuracy comparison or claim of equivalent models
    

    第一段中央梯度的25格,只對這次正值輸入、全1卷積核的非零支援成立;遇到不啟動的ReLU、零權重或抵消路徑,實際梯度支援可能更小。理論感受野與某次梯度圖的差別,仍沿用上一篇的限制。

    這些檢查沒有回答影像準確率,也沒有證明不同模組等價。它們的價值是:在花時間訓練前,就能先抓出通道接錯、concat誤當加法、瓶頸未必省計算,以及分類頭參數量被忽略的問題。

    讀到一個模型名稱後,先查四個設定

    要查的設定為什麼會改變判斷

    精確版本與連線
    VGG的層數、Inception模組與完整GoogLeNet不是同一層級的名稱;教學簡化也可能省略重要分支
    輸入解析度、通道與輸出類別
    同名模型可能從RGB/1000類改成灰階/10類,運算與參數跟著變
    分類頭與下採樣位置
    大Flatten矩陣、GAP、步長與Pooling影響成本,也改變保留的空間資訊
    訓練與評估流程
    資料切分、增強、優化器、seed及測試方式,不能由架構圖或模型名稱推定

    AlexNet讓大型CNN的訓練條件變得具體,VGG讓深度與block設計更容易推演,Inception讓多尺度分支與計算預算一起被考慮。這些是理解後續模型的工具;當網路繼續加深,還會遇到優化與資訊傳遞問題,下一篇再看ResNet與DenseNet如何改變連接方式。

    來源與圖像授權

    原始歷史依據:

    圖像與教材實驗依據:

    圖1–7及封面原作出自《Dive into Deep Learning》,作者Aston Zhang、Zachary C. Lipton、Mu Li、Alexander J. Smola,按D2L實際版本授權採CC BY-SA 4.0。架構圖來自該版本原SVG;曲線是實際網站PyTorch分頁的靜態輸出,不是pinned repo中的重新產生結果。

    本站將SVG轉白底PNG;圖2擷取原圖中右兩欄,部分手機版保留完整採用區域後再分欄/分區放大,圖4手機版另旋轉全圖;封面將圖3等比縮放並加白邊。沒有改動原圖數值、圖例或另畫連線,以上D2L圖像及其衍生版本同樣按CC BY-SA 4.0提供。來源章節含其他第三方論文插圖,不因本篇採用教材自有圖而一併取得重刊授權。

    以下保留教材曲線所用Fashion-MNIST資料集的MIT授權聲明;這不表示曲線本身改以MIT取代D2L的CC BY-SA授權:

    The MIT License (MIT) Copyright © 2017 Zalando SE, https://tech.zalando.com
    
    Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
    
    The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
    
    THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    

    留言0

    登入後就能留言。

    登入
    sunny 桑尼
    sunny 桑尼

    AI 資料科學 · 產品經理 · 蒙奇 D 桑尼

    專注於 AI 工具應用與資料科學教育,致力於讓每個人都能用 AI 提升工作效率。曾任職於科技業,現為全職教育創作者。

    讀更多文章 →想自己動手試?領免費 Starter Kit →