網路加深,增加的是可能的計算路徑,不會自動增加訓練成功的機率。ResNet讓一個區塊學習「在輸入上補多少」,再與輸入相加;DenseNet則把先前特徵保留下來,沿通道串接,交給後面的層使用。兩者都改變跨層連接,但不是同一種合併方式。
上一篇談過VGG的重複block與Inception的多分支。這次要處理的是:即使模型的容量夠,為什麼多堆幾層仍可能學得更差?shortcut提供了什麼,又沒有保證什麼?
本篇先看原始概念圖,再讀一組歷史控制實驗的限制。最後用完整CPU程式檢查殘差梯度、投影與串接;教材曲線屬於來源實驗,本站沒有訓練完整ResNet或DenseNet。
容量增加、找到好解、泛化良好,是三件事
圖1|D2L左圖的函數集合沒有逐級包含,右圖則由內到外巢狀包含;星號f*代表目標函數。這是集合關係的概念圖,面積不是實測容量單位,也不是訓練誤差曲線。來源:D2L,CC BY-SA 4.0;完整SVG轉PNG,手機先保留全圖再分欄放大。
先把「模型可表達哪些函數」和「優化器實際找到哪個函數」分開。假設比較同一份資料、同一個損失,而且較大模型的函數集合確實包含較小模型,那麼較小模型的解也在較大集合裡;理想的最小訓練損失不應更差。
這個推論沒有保證有限訓練時間內能找到該解,也不保證改善是嚴格的,更沒有保證測試資料上的誤差下降。若新增層、激活或尺寸變換連原本的映射都不能原樣保留,「只是多幾層所以必然包含舊模型」的前提本身也需要檢查。
因此,看到模型變深後表現下降,至少要分清:可表達範圍是否改變、訓練有沒有找到好解、以及找到的解能不能泛化。這些問題不能只靠「參數比較多」回答。
原始ResNet實驗,退化指的是什麼?
原論文第5頁Figure 4在ImageNet比較18/34層plain網路與殘差網路。細線是training error,粗線是center-crop validation error;青色18層、紅色34層。虛線背景是格線,不是另一種資料。
這組設定下,plain-34的訓練誤差比plain-18高;換成殘差參數化後,34層相對18層的情況反轉。作者也指出plain使用BN後梯度norm健康,不能把這張圖當成「已證明梯度消失就是原因」。
同頁Table 2另報10-crop的validation top-1 error,單位為百分比:
| 深度 | Plain | ResNet |
|---|---|---|
| 18層 | 27.94 | 27.88 |
| 34層 | 28.54 | 25.03 |
表格不是Figure 4粗線的最後一點,因為評估裁切方式不同;本組shortcut採無額外可訓練參數的option A,也不是後面教材的投影方案。這是特定設定的優化退化證據,不推出所有深層網路都更差。原圖請沿連結閱讀,未在本站重刊。
ResNet:不用每次重新學完整映射
圖2|左欄權重區塊直接擬合f(x),右欄學g(x),再與x相加;原圖的f(x)=g(x)+x位於最上方激活函數之前。右側繞行的線是shortcut,不是把前面層刪掉。來源:D2L,CC BY-SA 4.0;完整圖轉PNG,手機另分欄放大。
若希望區塊在激活前得到H(x),可以把要學的部分改寫為:
F(x) = H(x) - x
H(x) = x + F(x)
這裡的殘差是對區塊輸入的映射差,不是直接把「正確標籤減預測值」塞回每層。F可以由多個卷積、Normalization與非線性組成;它和原本的主路徑一樣會參與前向與反向運算。
若某個區塊只需保留輸入,在沒有後續激活改寫等條件下,F=0便能給出H=x。這讓接近identity的映射有一種直接的參數化方式,但「容易表示」仍不等於任何優化器都一定學得容易。
post-add ReLU會改變identity的條件
圖3|左欄直接傳x,右欄shortcut有1×1卷積。主路徑是Conv→BN→ReLU→Conv→BN,接著相加,最後才ReLU。第二個Conv後沒有先多接一個ReLU再相加。來源:D2L,CC BY-SA 4.0;完整SVG轉PNG,手機另分欄放大。
這個教材基本塊的完整輸出應寫成:
output = ReLU(F(x) + S(x))
同形狀時S(x)=x。即使F=0,輸出也是ReLU(x),不是對任意有正有負的x都等於x;在輸入非負等適用條件下才一致。若F裡還有BN與偏置,單把某個卷積權重設為0,也不自動證明整條分支輸出為0。
後續Identity Mappings論文另研究full pre-activation,把BN/ReLU放到權重層之前,保留相加後的直接映射。這是不同設計,不能反過來把最初的post-activation區塊說成已經沒有相加後的ReLU。
梯度中的I代表什麼?先算一個兩維例子
為了單獨看相加的作用,先不加外層ReLU或BN,定義線性小例:
x = [2, 1]
W = [[0.5, -1],
[1, 0.25]]
y = x + W @ x = [2, 3.25]
L = sum(y)
W@x是[0,2.25],所以兩個輸出是[2,3.25]。這不是影像分類模型,只是一個可以逐項驗算的映射。
輸出對輸入的Jacobian為I+W,其中I來自直接傳遞的x。由於L對兩個輸出的局部梯度都是1:
dL/dx = (I + W).T @ [1, 1]
= [2.5, 0.25]
文末autograd會得到相同結果。shortcut提供了一個不必逐層穿過F的直接貢獻,但總梯度仍是所有路徑相加後的結果。
如果改成W=−I,就有y=x+(-I)@x=0,I與殘差分支的導數抵消,總梯度是0。若改成相加後接ReLU,並讓F=0、x=[-1,-2],輸出與梯度同樣都是0。兩個反例都在程式中實跑。
因此,不能從I出現在算式裡,就推論所有深度、所有權重、所有輸入下梯度都不消失。這個小例也沒有證明兩種參數化的收斂速度;它只讓「存在直接路徑」與「總梯度一定非零」的差別具體可查。
尺寸不同時,shortcut也要做變換
逐元素相加要求兩路形狀對齊。本篇採N×C×H×W;通道或空間大小不同,不能只看到PyTorch某些張量可以broadcast,就把那種廣播當成標準殘差塊的設計。
圖3右欄用1×1卷積改通道,若同時設步長2,也可縮小空間尺寸。主路徑必須用相容設定產生同形狀;不是加上任意一個1×1就自然吻合。
本站小例把輸入設為1×2×4×4:
| 分支 | 設定 | 輸出形狀 |
|---|---|---|
| 主路徑 | 3×3,2→3通道,步長2,padding1 | 1×3×2×2 |
| shortcut | 1×1,2→3通道,步長2 | 1×3×2×2 |
| 相加 | 兩路逐元素相加 | 1×3×2×2 |
為了看清shortcut,我們把主路徑權重設0,投影的三個輸出通道則分別取原通道A、原通道B、A+B;步長2取用空間位置。輸入由1到32排列,左上位置投影後是[1,17,18]。
這是一個新的映射,已經不是原張量的identity:輸入2通道4×4,輸出3通道2×2。原論文也有增加通道時補零、跨解析度取樣的方案,與可學習投影需要分清;「通道補零」也不是卷積在影像四周補邊的同一件事。
ResNet-18:名字裡的18,不是把所有方框都數一次
圖4|D2L原圖由左往右:第一個stage重複兩個同形狀基本塊;後面三個stage各包含起始投影塊與另一基本塊。2×與3×括號的重複單位不同,不能直接讀成總共五個殘差塊。手機先保留全圖,再整圖逆時針旋轉90度,由下往上看。來源:D2L,CC BY-SA 4.0;轉PNG與上述旋轉,連線與數值不改。
教材的四個stage各有2個基本塊,通道依序64、128、256、512。每塊主路徑2個3×3卷積,共16個;加上開頭7×7卷積與末端線性分類層,命名數為18。它不把BN、Pooling、ReLU及shortcut投影另外加進這個數字。
以教材96×96單通道輸入為例,按其PyTorch設定手算:
1×96×96
-> stem Conv7,stride2,pad3: 64×48×48
-> MaxPool3,stride2,pad1: 64×24×24
-> stage1: 64×24×24
-> stage2: 128×12×12
-> stage3: 256×6×6
-> stage4: 512×3×3
-> Global AvgPool: 512
-> Linear: 10
這是形狀推算,本站的小例沒有執行整個ResNet-18。末端仍有線性分類器,Global Average Pooling不表示整個模型沒有全連接層。原始較深ResNet還使用1×1、3×3、1×1的bottleneck,不能只把基本塊重複次數改大,就宣稱完整重製了原版ResNet-50/101/152。
DenseNet:把前面特徵留在通道裡
圖5|左邊的「+」把兩路相加,右邊的「[,]」把先前表示與新表示串接。圖中A的輸出同時進入B與shortcut,最後合併兩路;A、B不是類別標籤。來源:D2L,CC BY-SA 4.0;完整SVG轉PNG,手機保留全圖後分欄放大。封面採同張完整圖等比加白邊。
殘差加法把兩份同形狀數值混合進同一組通道;DenseNet則把舊特徵與新特徵分開保留在串接結果裡,讓下一層取用。
若用h1、h2表示各層新產生的特徵,dense block可寫成:
h1 = H1(x)
h2 = H2(concat[x, h1])
h3 = H3(concat[x, h1, h2])
block_output = concat[x, h1, h2, h3]
圖6|D2L以多條跨層線表示後層能取用前面表示。原圖節點簡寫fi(x),實際各層輸入應按上方公式理解為先前特徵的串接,不是每層都只讀原始x。來源:D2L,CC BY-SA 4.0;完整圖轉PNG,手機另放大左右區域,跨區連線以先列的完整圖為準。
「每層取得所有先前特徵」指同一個dense block內、空間尺寸一致的連接。過了改解析度的transition,不能再假設所有最早的原始特徵圖都跨尺寸原封不動串進來。
Growth rate是每層新增幾個通道
假設block輸入C0個通道,每層新增k個通道,block內做L層,輸出通道數是:
C_out = C0 + L*k
第l層讀取的通道則是C0+(l-1)*k。這裡k是growth rate,不是整個block固定的輸出寬度。
本站用C0=2、k=1、L=3做小例,通道會是2→3→4→5。令輸入的兩張2×2通道為A與B,手設各層1×1權重後:
| 階段 | 新產生的通道 | 保留的累積通道 |
|---|---|---|
| 輸入 | — | A、B |
| 第1層 | A | A、B、A |
| 第2層 | A+B | A、B、A、A+B |
| 第3層 | 3A+2B | A、B、A、A+B、3A+2B |
這份程式只示範串接依賴,用1×1與正值輸入,不含BN,也不是原版DenseNet完整的BN→ReLU→3×3結構。新通道剛好重複A,是手設權重的結果,沒有證明訓練後學到了有效特徵重用。
Transition:控制通道與空間尺寸,兩個任務要分開
通道一直累加,後面的卷積也要讀取更多輸入。DenseNet在dense blocks之間安排transition,典型組合含1×1卷積與2×2平均池化,分別調整通道與空間大小。
若transition接到m個通道,使用壓縮係數theta,輸出可設為floor(theta*m)。theta=0.5代表通道約減半;theta=1仍可下採樣空間,所以transition不等於必須把通道減半。
原始DenseNet論文還區分:B在每個dense layer的3×3前放1×1 bottleneck;C是在transition壓縮通道;兩者一起是BC。transition本來就有1×1,不代表模型因此自動屬於B。
本站transition小例從5通道挑回原始A、B兩個通道,再平均池化2×2,得到1×2×1×1,值為[2.5,6.5]。它刻意丟掉新增的三個通道,只是檢查映射與形狀,沒有把這個選擇說成學得的有效壓縮。
參數少,不代表訓練顯存一定少
Concat本身不學權重,但後層仍要處理更多通道,訓練也可能保留更多中間特徵。權重參數、計算量、activation與反向所需儲存、峰值顯存、實際時間,是不同資源。
同一個block內通道線性增長,不代表整個網路的所有成本也只線性增加。實際記憶體還取決於特徵圖大小、batch、實作與重算策略,不能只因某份DenseNet參數比某份ResNet少,就宣布任意設定都更省顯存或更快。本篇未測GPU記憶體或速度。
兩張教材曲線,不是plain/residual控制實驗
圖7|D2L ResNet-18,Fashion-MNIST resize到96×96、10類、batch128、SGD lr=0.01、10 epochs。藍實線train_loss、橙虛線val_loss、綠點畫線val_acc;原圖可見後段訓練loss遠低於val loss。來源:D2L,CC BY-SA 4.0;完整SVG轉PNG。這張圖沒有plain baseline,不是原論文Figure 4。
圖8|D2L簡化DenseNet,同樣96×96、10類、batch128、SGD lr=0.01、10 epochs。四個dense block各4層、growth rate32;block內是BN→ReLU→單3×3,並非每層都有bottleneck的ImageNet BC完整配置。來源:D2L,CC BY-SA 4.0;完整SVG轉PNG。
兩張圖都來自各章實際PyTorch分頁。共同的helper使用60,000筆訓練資料,將train=False的10,000筆官方test當作val;沒有另外保留一個完全未參與選模的測試集。
藍橙線是mean cross entropy,未加額外weight-decay penalty;綠線是batch accuracy比例,與原論文的error百分比不同。ProgressBoard把一組batch平均值再彙整,尾批較短時並不保證是精確按樣本數加權的整epoch統計。
這些曲線可用來練習看訓練與評估的落差,卻不能隔離出「只加shortcut」或「只改concat」的因果效果。兩個模型的寬度、結構與初始化流程不同,沒有公開統一固定seed;程式至多要求一張GPU,靜態圖沒有記錄實際裝置及耗時。它們也不能證明多加資料必然消除落差,或某個架構在所有任務比較好。
完整小程式:核對數值與連接,再談成效
下方程式不下載資料,也不訓練模型。它核對線性殘差的梯度、兩個零梯度反例、投影兩路形狀、三層小串接及transition。
本次實際環境為Python3.10.12、PyTorch2.3.1+cu118;運算明確使用CPU float64,沒有使用GPU。卷積權重都是為了示範手設,並非經過學習。可在獨立環境安裝torch==2.3.1,存成connection_checks.py後執行python connection_checks.py;不同平台的建置尾碼可能不同。
import torch
from torch import nn
print(f"torch={torch.__version__} device=cpu dtype=float64")
W = torch.tensor([[.5, -1.], [1., .25]], dtype=torch.float64)
x = torch.tensor([2., 1.], dtype=torch.float64, requires_grad=True)
y = x + W @ x
(y.sum()).backward()
expected_gradient = torch.ones(2, dtype=torch.float64) + W.T @ torch.ones(2, dtype=torch.float64)
assert torch.equal(y, torch.tensor([2., 3.25], dtype=torch.float64))
assert torch.equal(x.grad, expected_gradient)
print("linear_residual_output=", y.detach().tolist(), "sum_gradient=", x.grad.tolist())
# Direct identity paths do not imply a nonzero total gradient.
u = torch.tensor([2., 1.], dtype=torch.float64, requires_grad=True)
cancel = u + (-torch.eye(2, dtype=torch.float64)) @ u
cancel.sum().backward()
assert torch.equal(u.grad, torch.zeros(2, dtype=torch.float64))
v = torch.tensor([-1., -2.], dtype=torch.float64, requires_grad=True)
post_relu = torch.relu(v + torch.zeros_like(v))
post_relu.sum().backward()
assert torch.equal(v.grad, torch.zeros_like(v))
print("canceling_residual_sum_gradient=", u.grad.tolist(), "negative_post_ReLU_sum_gradient=", v.grad.tolist())
def fixed_conv(cin, cout, kernel, stride=1, padding=0):
layer = nn.Conv2d(cin, cout, kernel, stride=stride, padding=padding, bias=False).double()
with torch.no_grad():
layer.weight.zero_()
return layer
# A projection is a new mapping, not a shape-preserving identity.
main = fixed_conv(2, 3, 3, stride=2, padding=1)
project = fixed_conv(2, 3, 1, stride=2)
with torch.no_grad():
project.weight.copy_(torch.tensor([[1., 0.], [0., 1.], [1., 1.]], dtype=torch.float64)[:, :, None, None])
a = torch.arange(1., 33., dtype=torch.float64).reshape(1, 2, 4, 4)
with torch.no_grad():
m, p = main(a), project(a)
result = m + p
expected_projection = torch.cat((a[:, 0:1, ::2, ::2], a[:, 1:2, ::2, ::2], a[:, 0:1, ::2, ::2] + a[:, 1:2, ::2, ::2]), dim=1)
assert list(m.shape) == list(p.shape) == [1, 3, 2, 2]
assert torch.equal(result, expected_projection)
assert list(a.shape) != list(result.shape)
print(f"projection_input_shape={list(a.shape)} main_and_shortcut_shape={list(result.shape)} nonidentity_mapping=True")
# A tiny concatenation example, not a complete DenseNet/BN block.
b = torch.arange(1., 9., dtype=torch.float64).reshape(1, 2, 2, 2)
features = [b]
widths = [2]
weight_rows = [[1., 0.], [0., 1., 1.], [1., 1., 1., 1.]]
for row in weight_rows:
joined = torch.cat(features, dim=1)
layer = fixed_conv(joined.shape[1], 1, 1)
with torch.no_grad():
layer.weight.copy_(torch.tensor(row, dtype=torch.float64).reshape(1, -1, 1, 1))
new = torch.relu(layer(joined))
features.append(new)
widths.append(sum(f.shape[1] for f in features))
dense = torch.cat(features, dim=1)
expected_dense = torch.cat((b, b[:, 0:1], b[:, 0:1] + b[:, 1:2], 3*b[:, 0:1] + 2*b[:, 1:2]), dim=1)
assert torch.equal(dense, expected_dense)
assert widths == [2, 3, 4, 5] and dense.shape[1] == 2 + 3*1
print(f"toy_dense_channel_widths={widths} output_shape={list(dense.shape)} growth_rate=1")
# One possible compression + downsampling transition.
transition = fixed_conv(5, 2, 1)
with torch.no_grad():
transition.weight[0, 0, 0, 0] = 1.
transition.weight[1, 1, 0, 0] = 1.
reduced = nn.functional.avg_pool2d(transition(dense), 2, 2)
assert list(reduced.shape) == [1, 2, 1, 1]
assert torch.equal(reduced.flatten(), torch.tensor([2.5, 6.5], dtype=torch.float64))
print(f"toy_transition_output_shape={list(reduced.shape)} values={reduced.flatten().tolist()}")
print("scope=hand-set CPU linear residual Jacobian, cancellation and post-ReLU counterexamples, projected shortcut, tiny 1x1 concatenation and transition; not a complete ResNet/DenseNet, image training, learned feature reuse, gradient-flow guarantee or benchmark")
本次觀察輸出如下。它們只對應這些小例,不是完整ResNet/DenseNet的訓練紀錄:
torch=2.3.1+cu118 device=cpu dtype=float64
linear_residual_output= [2.0, 3.25] sum_gradient= [2.5, 0.25]
canceling_residual_sum_gradient= [0.0, 0.0] negative_post_ReLU_sum_gradient= [0.0, 0.0]
projection_input_shape=[1, 2, 4, 4] main_and_shortcut_shape=[1, 3, 2, 2] nonidentity_mapping=True
toy_dense_channel_widths=[2, 3, 4, 5] output_shape=[1, 5, 2, 2] growth_rate=1
toy_transition_output_shape=[1, 2, 1, 1] values=[2.5, 6.5]
scope=hand-set CPU linear residual Jacobian, cancellation and post-ReLU counterexamples, projected shortcut, tiny 1x1 concatenation and transition; not a complete ResNet/DenseNet, image training, learned feature reuse, gradient-flow guarantee or benchmark
如果改動stride、通道或concat順序,先重新推形狀,再改預期結果;不要為了讓assert通過而硬把兩路reshape成一樣。形狀對上,仍不表示語意上的投影、相加與串接可以互換。
把兩種連接放在一起比較
| 問題 | ResNet基本殘差連接 | DenseNet的block內連接 |
|---|---|---|
| 合併方式 | 同形狀逐元素相加 | 沿通道串接舊特徵與新特徵 |
| 輸出通道 | 相加本身不增加通道;stage可另外改寬度 | 每層新增growth rate指定的通道 |
| 改解析度時 | 兩路要相容取樣/投影 | dense blocks間用transition |
| 需要小心的說法 | 直接路徑不保證總梯度永遠非零 | 可取用舊特徵不等於一定更省顯存 |
這些連接方式讓模型設計不只剩「再加幾層」。真正要檢查的是,哪些資訊可以直接傳遞、在哪裡混合、形狀何時改變,以及比較效果時是否控制了訓練和評估條件。
來源與圖像授權
原始技術與歷史實驗:
- He、Zhang、Ren、Sun:Deep Residual Learning for Image Recognition,CVPR 2016,§3與§4.1、Table 1–2/Figure 4。2015年arXiv初稿與2016年會議版本的年份分開;本文歷史控制實驗按CVF版,表內數字是validation 10-crop,原圖僅連結。
- He等:Identity Mappings in Deep Residual Networks,2016,區分shortcut、相加後函數及full pre-activation。
- Huang、Liu、van der Maaten、Weinberger:DenseNet,CVPR 2017,§3的concat、growth、B/C/BC。原論文圖未在此重刊。
教材原圖與曲線:
- D2L:Residual Networks,函數集合、一般/殘差塊、投影與ResNet-18主幹及PyTorch曲線。
- D2L:DenseNet,相加/串接與跨層圖、簡化DenseNet程式及PyTorch曲線。
- D2L實際版本PyTorch helper,資料切分、交叉熵、accuracy、Trainer與ProgressBoard定義。
- Fashion-MNIST資料集,兩張教材曲線所用資料;本站沒有重跑其分類訓練。
圖1–8與封面出自《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,部分手機版先保留完整原圖再分欄/分區放大,圖4另整圖旋轉;跨區連線以完整圖為準,沒有新增箭頭、節點或修改數值。封面由圖5等比縮放加白邊。上述D2L圖像及衍生版本同樣按CC BY-SA 4.0提供;此授權不延伸至來源章節引述的第三方論文圖。
以下保留兩張教材曲線所用Fashion-MNIST的MIT授權聲明,不把D2L曲線本身改授MIT:
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
登入後就能留言。
登入