Receptive Field 怎麼算? 從 streaming 的需求談起

本文與 Claude AI 共同編輯

Receptive field (感受野, 以下簡稱 RF) 大概是 CNN 教科書裡最早出現、也最容易被當成「知道就好」的概念之一: 某一個輸出元素, 是由多少個輸入元素決定的?
但如果你試過把一個離線訓練好的模型搬到邊緣裝置上做串流推論, 就會發現 RF 從「知道就好」的常識, 變成整個轉換流程的地基, 它直接決定每個 node 要留多大的 buffer、以及整條 pipeline 會有多少 latency.
這篇筆記想把 RF 的計算講清楚, 並且對照兩種看起來方向完全相反、實際上等價的算法: 一種是正向地把三元組 $[S, a, b]$ 一層一層往下帶, 另一種是 Distill 那篇 Computing Receptive Fields of Convolutional Neural Networks反向遞迴.


為什麼串流需要 RF

PyTorch 訓練時的 graph 是 offline 的: 整段輸入一次餵進去. 以音訊為例, 一次吃 6 秒的音檔, 卷積在整條時間軸上滑過, 該補的 padding 補好, 輸出一次算完. 這在 GPU 上很自然, 但搬到邊緣裝置就有兩個麻煩:

  • 記憶體: 6 秒 16 kHz 的音訊是 96000 個取樣點, 中間每一層的 feature map 都得整條留著
  • 延遲: 要等 6 秒的資料到齊才算得出第一個輸出, 這不是 real time

實務上的做法是把模型設計成 causal 的, 並且把 offline graph 轉成 streaming graph: 每次只餵一小段 (一個 hop), 每個 node 內部保留一小塊 keeping buffer, 把上次算過、這次還用得到的邊界資料留下來. 轉換之後, 記憶體用量從「整段序列」降到「每個 node 只留它真正需要的那幾幀」, 而且每收到一個 hop 就能吐出對應的輸出, 達到 real time 的效果.
要做這個轉換, 不管是編譯器自動做還是人工做, 都必須對每一個 node 回答兩個問題:

  • 這個 node 的輸出幀 $j$, 最早用到哪一幀輸入? → 決定 keeping buffer 要留多少過去的幀
  • 最晚用到哪一幀輸入? → 決定要等多少未來的幀才算得出它, 也就是 lookahead latency

這兩個端點圍出來的區間, 就是這個 node 的 receptive field.
這裡要強調一件事: 串流需要的是理論 RF, 不是 effective RF. Luo et al. 指出輸入對輸出的影響力其實會往視窗邊緣大致呈高斯衰減, 只佔滿理論 RF 的一小部分, 所以理論值高估了「實際上重要」的範圍. 但對 buffer 來說沒得商量, 必須保留每一個可能有影響的幀, 少留一幀, 串流的結果就跟 offline 對不起來了.


先建立直覺

最簡單的例子是一個 $k = 3$、stride 1、padding 1 的 conv: 輸出幀 $j$ 讀輸入幀 $j-1, j, j+1$, 要保留 1 幀過去、等 1 幀未來. 疊兩層, 輸出幀 $j$ 讀第一層的 $j-1, \dots, j+1$, 而它們各自又讀輸入的前後一幀, 合起來就是 $j-2, \dots, j+2$:

1
2
3
4
5
輸入幀 j−2 j−1 j j+1 j+2
\ | /|\ | /
第一層輸出 j−1 j j+1
\ | /
第二層輸出 j

5 幀的視窗、2 幀的未來. 整篇文章要做的, 就是把這個推理寫成一般的公式.


三個數字: [S, a, b]

先定義記號. 把每個 node 配上一個累積 stride $S$ 與兩個偏移 $a \le b$, 意思是: 該 node 的輸出幀 $j$ 讀取網路輸入的第
$$[\, j \cdot S + a,\ \ j \cdot S + b \,]$$ 幀. 就這樣, 三個數字把一個 node 對輸入的依賴講完了.
錨點 $j \cdot S$. 輸出每走 1 幀, 輸入走 $S$ 幀, 所以輸出幀 $j$ 在時間上對應到輸入幀 $j \cdot S$, 稱為它的錨點. $a$$b$ 是相對錨點的偏移: 視窗從錨點往前 $-a$ 幀開始、到錨點往後 $b$ 幀結束. 前一節的兩層例子就是 $S = 1,\ a = -2,\ b = 2$.
由此得到我們真正想要的兩個量:

  • $\mathrm{rf} = b - a + 1$: 視窗的幀數, 也就是 keeping buffer 的大小
  • $\mathrm{lookahead} = \max(b, 0)$: 要算出輸出 $j$, 輸入必須到達 $j \cdot S + b$, 也就是比錨點多等 $b$

$\mathrm{lookahead}$$\max$ 是因為 $b$ 可以是負的, 輸出只依賴錨點之前的輸入, 那就完全不必等, latency 是 0 而不是負數.

每個 op 都有自己的 [S, a, b]

一個 op 只要能寫成「輸出幀 $j$ 讀輸入幀 $j \cdot s - p,\ \dots,\ j \cdot s - p + k - 1$」, 它就有自己的 $(k, s, p)$, 也就有自己的 $[S, a, b]$ (這裡 $p$ 指的是 padding; 右 padding 只決定序列結尾多吐幾幀, 不改變某個輸出讀的是哪些輸入, 所以完全不會出現在公式裡). 把 op 單獨接在網路輸入上, 從 $[1, 0, 0]$ 出發套一次下一節的規則, 就得到:

op $k,\ s,\ p$ $[S,\ a,\ b]$ $\mathrm{rf}$ / $\mathrm{lookahead}$
ReLU / Sigmoid 等 pointwise $1,\ 1,\ 0$ $[1,\ 0,\ 0]$ 1 / 0
Conv1d(k=3, s=1, p=1) ("same") $3,\ 1,\ 1$ $[1,\ -1,\ 1]$ 3 / 1
Conv1d(k=3, s=2, p=1) $3,\ 2,\ 1$ $[2,\ -1,\ 1]$ 3 / 1
causal conv: F.pad(x,(2,0)) + Conv1d(k=3, p=0) $3,\ 1,\ 2$ $[1,\ -2,\ 0]$ 3 / 0
Conv1d(k=3, d=2, p=2) (dilated) $5,\ 1,\ 2$ $[1,\ -2,\ 2]$ 5 / 2
MaxPool1d(k=2, s=2) $2,\ 2,\ 0$ $[2,\ 0,\ 1]$ 2 / 1
純延遲 F.pad(x,(2,0)) $1,\ 1,\ 2$ $[1,\ -2,\ -2]$ 1 / 0
GRU / LSTM $[1,\ -\infty,\ 0]$ $\infty$ / 0

幾個值得一提的:

  • Dilation 走 $k_{\text{eff}}$. 膨脹率 $d$ 的 kernel 實際涵蓋 $k_{\text{eff}} = d(k-1) + 1$ 個元素, 所以公式裡的 $k$ 一律換成 $k_{\text{eff}}$ 就好, 不需要另外一套規則.
  • Causal conv 的 lookahead 是 0. 把 padding 全部補在左邊 ($p = k - 1$) 會讓 $b = k - 1 - p = 0$. 這正是「causal」的定義, 而它在 $[S, a, b]$ 的語言裡就只是 $b \le 0$.
  • $b$ 可以是負的. 純延遲 F.pad(x, (2,0)) 的輸出幀 $j$ 就是輸入幀 $j - 2$ (前兩幀是補出來的 0), 所以 $a = b = -2$: 輸出比錨點還早確定, 一幀都不用等.
  • RNN 讓 $a = -\infty$. GRU / LSTM 的輸出依賴整段過去, 視窗左端延伸到序列開頭, $\mathrm{rf} = \infty$. 但 $b$ 不受影響, RNN 不會讓你多等未來的幀.
  • Pointwise 的 op 直接沿用輸入的 $[S, a, b]$, 因為它不讀相鄰幀. 這裡也順帶回答一個常見的問題: layer normalization 算不算? 如果它沿 channel 維做, 那對時間軸而言就是 pointwise ($k = 1$); 如果是沿整條時間軸做 global normalization, 那等於 $k = \infty$, 跟 RNN 一樣讓 $a = -\infty$. 重點是只要能定義出 $(k, s, p)$, 就能算 RF, 跟這個 op 在做什麼運算無關.

正向規則

現在把規則推出來. 假設某個 node 已經有了 $[S, a, b]$, 也就是它的輸出幀 $i$ 讀網路輸入幀 $[\, i \cdot S + a,\ i \cdot S + b \,]$. 後面接一個 window 層, 參數是 $(k, s, p)$. 由定義, 新的輸出幀 $j$ 讀這個 node 的第
$$i = j \cdot s - p,\ \ \dots,\ \ j \cdot s - p + k - 1$$ 幀. 把最左的 $i$ 代進 $i \cdot S + a$、最右的 $i$ 代進 $i \cdot S + b$, 就得到它讀的網路輸入幀的兩個端點; 再整理成「$j \cdot$ 新 stride $+$ 偏移」的形式:
$$\begin{aligned} (j s - p) \cdot S + a &= j \cdot (S s) + (a - p S) \\ (j s - p + k - 1) \cdot S + b &= j \cdot (S s) + \big(b + (k - 1 - p) S\big) \end{aligned}$$ 對照定義 $[\, j \cdot S' + a',\ j \cdot S' + b' \,]$, 就直接讀出了正向規則 (右邊的 $S$ 都是更新前的值):
$$S' = S \cdot s, \qquad a' = a - p \cdot S, \qquad b' = b + (k - 1 - p) \cdot S$$ 三條式子在做的是同一件事: 把這一層的量 (以這一層的輸入幀為單位) 乘上 $S$, 換算成網路輸入幀.

  • $S' = S \cdot s$: 這一層每走一步跳 $s$ 幀, 而這一層的每一幀又相當於 $S$ 個網路輸入幀
  • $a' = a - p \cdot S$: 左 padding 讓視窗往錨點左邊多伸 $p$ 幀, 換算成 $p \cdot S$ 個網路輸入幀
  • $b' = b + (k - 1 - p) \cdot S$: 視窗從左端往右寬 $k - 1$ 幀, 扣掉 padding 往左推的 $p$ 幀, 右端比錨點多出 $k - 1 - p$

兩式相減, 得到只關心大小時的版本:
$$\mathrm{rf}' = \mathrm{rf} + (k - 1) \cdot S$$ padding 消失了, 它改變的是讀哪些輸入幀, 不是讀幀. 這跟直覺一致.
起點是網路輸入自己: 它的輸出幀 $j$ 就是輸入幀 $j$, 所以

$$S = 1, \qquad a = b = 0$$

為什麼要正向走

Distill 文章的所有遞迴式都是從輸出往輸入走 ($l \to l-1$), 而這裡是反過來的: 依拓撲順序從網路輸入出發, 把 $[S, a, b]$ 一層一層往下帶.
選正向的理由很實際: 一個 graph 可能有很多個輸出, 但通常只有一個帶時間軸的輸入. 正向走一次, 就同時得到了每一個 node 的答案; 反向的話, 每個輸出都得各走一次. 而串流轉換需要的恰恰是「每個 node 的 buffer 要多大」, 不是只有最後那個輸出的數字.
另一個好處是, $[S, a, b]$大小與位置一起帶著走. 反向那套要算位置得另外再跑一組遞迴 (下一節的 $u, v$$S_l, P_l$).

走一個例子

拿一條 5 層的鏈: 全部 $k = 3$"same" padding ($p = 1$), strides 依序是 $1, 1, 2, 1, 1$.

1
2
3
4
5
6
7
LAYERS = [(3, 1, 1), (3, 1, 1), (3, 2, 1), (3, 1, 1), (3, 1, 1)] # (k, s, p)
S, a, b = 1, 0, 0
print(f"{'x':6s} S={S} a={a:3d} b={b:3d} rf={b-a+1:3d} lookahead={max(b,0):3d}")
for i, (k, s, p) in enumerate(LAYERS, 1):
S, a, b = S * s, a - p * S, b + (k - 1 - p) * S # 右邊都是更新前的 S
print(f"L{i:<5d} S={S} a={a:3d} b={b:3d} rf={b-a+1:3d} lookahead={max(b,0):3d}")
1
2
3
4
5
6
x S=1 a= 0 b= 0 rf= 1 lookahead= 0
L1 S=1 a= -1 b= 1 rf= 3 lookahead= 1
L2 S=1 a= -2 b= 2 rf= 5 lookahead= 2
L3 S=2 a= -3 b= 3 rf= 7 lookahead= 3
L4 S=2 a= -5 b= 5 rf= 11 lookahead= 5
L5 S=2 a= -7 b= 7 rf= 15 lookahead= 7

逐層手算對一下:
$$\begin{array}{llll} \texttt{L1}: & S = 1 \cdot 1 = 1, & a = 0 - 1 \cdot 1 = -1, & b = 0 + (3 - 1 - 1) \cdot 1 = 1 \\ \texttt{L2}: & S = 1 \cdot 1 = 1, & a = -1 - 1 \cdot 1 = -2, & b = 1 + 1 \cdot 1 = 2 \\ \texttt{L3}: & S = 1 \cdot 2 = 2, & a = -2 - 1 \cdot 1 = -3, & b = 2 + 1 \cdot 1 = 3 \\ \texttt{L4}: & S = 2 \cdot 1 = 2, & a = -3 - 1 \cdot 2 = -5, & b = 3 + 1 \cdot 2 = 5 \\ \texttt{L5}: & S = 2 \cdot 1 = 2, & a = -5 - 1 \cdot 2 = -7, & b = 5 + 1 \cdot 2 = 7 \end{array}$$ 注意 L4$k = 3$$b$ 的貢獻是 $1 \cdot 2 = 2$ 幀而不是 1 幀: L3 已經把 stride 墊到 2, L4 的一幀等於輸入的兩幀. RF 的成長速度取決於它前面累積了多少 stride, 這是整套公式最關鍵的直覺.

因為是對稱 padding, 這裡剛好 $a = -b$, 所以 RF 序列是 $3, 5, 7, 11, 15$: 每層往兩邊各長一樣多.


反向的視角: Distill 文章的公式

記號與遞迴

Distill 那篇把網路看成 $L$ 層的鏈, 第 $l$ 層有 kernel $k_l$、stride $s_l$、左 padding $p_l$, 輸入 feature map 為 $f_{l-1}$、輸出為 $f_l$ ($f_0$ 是網路輸入、$f_L$ 是輸出).
定義 $r_l$ 為「$f_L$一個輸出元素, 需要 $f_l$ 的多少個元素」. 若我們關心 $f_l$ 上的 $r_l$ 個連續元素 $u, \dots, u + r_l - 1$, 它們在 $f_{l-1}$ 裡從 $u s_l - p_l$ 讀到 $(u + r_l - 1)s_l - p_l + k_l - 1$, 共 $s_l(r_l - 1) + k_l$ 個. 於是從 $r_L = 1$ 出發反向走:
$$r_{l-1} = s_l \cdot r_l + (k_l - s_l) \tag{1}$$ 展開得到封閉式:
$$r_0 = \sum_{l=1}^{L} (k_l - 1) \prod_{i=1}^{l-1} s_i + 1 \tag{2}$$ 位置也有一組類似的遞迴. 設輸出上關心的元素索引為 $u_L, \dots, v_L$, 則

$$u_{l-1} = -p_l + u_l \cdot s_l \tag{3}$$ $$v_{l-1} = -p_l + v_l \cdot s_l + k_l - 1 \tag{4}$$ $$u_0 = u_L \prod_{i=1}^{L} s_i - \sum_{l=1}^{L} p_l \prod_{i=1}^{l-1} s_i \tag{5}$$

$$v_0 = v_L \prod_{i=1}^{L} s_i - \sum_{l=1}^{L} (1 + p_l - k_l) \prod_{i=1}^{l-1} s_i \tag{6}$$ 文章還給 Eq.(5) 裡的兩個量取了名字, effective stride $S_l$effective padding $P_l$:
$$S_l = \prod_{i=l+1}^{L} s_i, \qquad P_l = \sum_{m=l+1}^{L} p_m \prod_{i=l+1}^{m-1} s_i$$ 它們同樣滿足反向遞迴 (起點 $S_L = 1$$P_L = 0$):

$$S_{l-1} = s_l \cdot S_l \tag{7}$$

$$P_{l-1} = s_l \cdot P_l + p_l \tag{8}$$ 於是單一輸出元素的 Eq.(5) 簡化成 $u_0 = -P_0 + u_L \cdot S_0$, 而 RF 的中心是

$$c_0 = -P_0 + u_L \cdot S_0 + \frac{r_0 - 1}{2} \tag{9}$$

注意: 兩邊的「RF」定義不一樣

這是最容易搞混的地方, 值得特別標出來.
正向那套問的是:

要跑出 $f_l$ 的一幀, 需要多少 $f_{\mathbf{0}}$ 的幀? 輸入端固定在 $f_0$, 變動的是輸出端

Distill 文章的 $r_l$ 問的則是:

要跑出 $f_{\mathbf{L}}$ 的一幀, 需要多少 $f_l$ 的幀? 輸出端固定在 $f_L$, 變動的是輸入端

所以同樣寫成「第 $l$ 層的 RF」, 兩者是不同的東西. $r_3$$\mathrm{rf}_3$ 沒有任何理由要相等, 實際上也不相等. 兩者只在端點重合:
$$r_0 \;=\; \mathrm{rf}_L$$ 都是「整個網路的輸出一幀, 要看幾幀輸入」. 接下來要證的等價性, 指的就是這件事.
還有一個記號上的小坑: Distill 的 $S_l$從第 $l$ 層算到輸出的 stride 乘積, 而正向規則裡的 $S$從網路輸入算到目前這層的乘積. 兩者在端點也對得上 ($S_0 = \prod_{i=1}^{L} s_i$ 就是走完全部層之後的 $S$), 但中途的值同樣不能直接對照.

同一條鏈的反向遞迴

把前面〈走一個例子〉那條 5 層鏈拿來反向跑一次:

1
2
3
4
5
r, S, P = 1, 1, 0
print(f"r_{len(LAYERS)}={r:3d} S={S} P={P}")
for i, (k, s, p) in reversed(list(enumerate(LAYERS, 1))):
r, S, P = s * r + (k - s), s * S, s * P + p
print(f"r_{i-1}={r:3d} S={S} P={P}")
1
2
3
4
5
6
r_5= 1 S=1 P=0
r_4= 3 S=1 P=1
r_3= 5 S=1 P=2
r_2= 11 S=2 P=5
r_1= 13 S=2 P=6
r_0= 15 S=2 P=7

$r_0 = 15$$S_0 = 2$$P_0 = 7$. 對照正向的最後一列: $\mathrm{rf} = 15$$S = 2$$-a = 7$, 完全相同.
同時也看到了中途的值確實對不上: 正向的 $\mathrm{rf}_3 = 7$, 反向的 $r_3 = 5$. 兩個都對, 只是在回答不同的問題.


等價性

大小

先解反向遞迴 Eq.(1). 宣稱
$$r_l = \sum_{m=l+1}^{L}(k_m - 1) \prod_{i=l+1}^{m-1} s_i \;+\; 1$$
$l = L$ 時右邊是空和加 1, 等於 $r_L = 1$, 成立. 假設對 $l$ 成立, 代進 Eq.(1):
$$\begin{aligned} r_{l-1} &= s_l \left[ \sum_{m=l+1}^{L}(k_m - 1)\prod_{i=l+1}^{m-1} s_i + 1 \right] + k_l - s_l \\ &= \sum_{m=l+1}^{L}(k_m - 1)\prod_{i=l}^{m-1} s_i \;+\; (k_l - 1) + 1 \\ &= \sum_{m=l}^{L}(k_m - 1)\prod_{i=l}^{m-1} s_i \;+\; 1 \end{aligned}$$ 最後一步是把 $k_l - 1$ 寫成 $(k_l - 1)\prod_{i=l}^{l-1} s_i$ (空乘積為 1) 併回和式. 這正是宣稱把 $l$ 換成 $l-1$ 的樣子, 所以歸納成立. 取 $l = 0$ 就得到 Eq.(2).
接著是關鍵的一步, 把封閉式的最後一項拆出來:
$$r_0 \;=\; \underbrace{\left[ \sum_{l=1}^{L-1}(k_l - 1)\prod_{i=1}^{l-1} s_i + 1 \right]}_{\text{把第 } L \text{ 層拿掉之後的封閉式}} \;+\; (k_L - 1)\prod_{i=1}^{L-1} s_i$$ 中括號裡那一坨, 恰好就是「把最後一層拿掉的那個網路」套用 Eq.(2) 的結果, 也就是正向的 $\mathrm{rf}_{L-1}$. 所以
$$\mathrm{rf}_L = \mathrm{rf}_{L-1} + (k_L - 1)\prod_{i=1}^{L-1} s_i$$ 而正向規則給的是 $\mathrm{rf}' = \mathrm{rf} + (k-1) \cdot S$, 其中正向走到第 $L$ 層時, 累積的 $S$ 正好是 $\prod_{i=1}^{L-1} s_i$. 兩式一模一樣.
換句話說: 正向遞迴就是反向遞迴的封閉解按層拆開來看. 反向那條 Eq.(1) 每一步都在做「乘 $s_l$ 再加 $k_l - s_l$」, 看不出每一層各自貢獻多少; 解開之後才看得出來, 第 $l$ 層的貢獻是乾淨的 $(k_l - 1)\prod_{i<l} s_i$, 而且跟後面的層無關, 這就是正向能夠一層一層往下累加的原因.

位置

大小對上了, 但正向的 $[S, a, b]$ 帶的資訊更多, 它還給了位置. 這部分同樣對得上.
正向走到第 $l$ 層時, 手上的 $S$$\prod_{i=1}^{l-1} s_i$, 所以走完整條鏈之後:

  • $S$ 每層乘上 $s_l$, 最後是 $\prod_{i=1}^{L} s_i = S_0$
  • $-a$ 每層加上 $p_l \prod_{i<l} s_i$, 最後是 $\sum_{l=1}^{L} p_l \prod_{i=1}^{l-1} s_i = P_0$
  • $b$ 每層加上 $(k_l - 1 - p_l)\prod_{i<l} s_i$, 最後是 $-\sum_{l=1}^{L}(1 + p_l - k_l)\prod_{i=1}^{l-1} s_i$, 也就是 $v_L = 0$ 時的 $v_0$

第二條特別值得看一眼. Eq.(8) 的 $P_{l-1} = s_l P_l + p_l$ 從輸出端往前收, 展開是
$$P_0 = p_1 + s_1\big(p_2 + s_2(p_3 + \cdots)\big) = \sum_{l=1}^{L} p_l \prod_{i=1}^{l-1} s_i$$ 跟正向由左往右直接累加的是同一個和, 只是結合順序相反, 就像用 Horner 法算多項式跟直接展開算, 結果一樣.

對照表

正向 $[S, a, b]$ Distill
$S$ effective stride $S_0$ (Eq. 7)
$-a$ effective padding $P_0$ (Eq. 8)
$\mathrm{rf} = b - a + 1$ $r_0$ (Eq. 2)
$j \cdot S + a$$j \cdot S + b$ $u_L = v_L = j$ 時的 $u_0$$v_0$ (Eq. 5–6)
$(a + b)/2$ $j = 0$ 時的中心 $c_0$ (Eq. 9)
$\mathrm{lookahead} = \max(b, 0)$ 沒有對應

最後一列是唯一一個 Distill 文章裡沒有的量, 而原因也不難理解: 影像的 RF 分析在意的是大小對齊, 沒有時間軸, 自然沒有「要等多久」這回事. 串流才需要把視窗的右端單獨拿出來看.


用 autograd 驗證

公式對不對, 其實可以直接問 autograd: 對每個輸出幀求它對輸入的梯度, 梯度非零的輸入幀, 就是它真正依賴的幀.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import torch, torch.nn as nn
LAYERS = [(3, 1, 1), (3, 1, 1), (3, 2, 1), (3, 1, 1), (3, 1, 1)] # (k, s, p)
S, a, b = 2, -7, 7 # 正向規則算出來的結果
torch.manual_seed(0)
m = nn.Sequential(*[nn.Conv1d(1 if i == 0 else 4, 4, k, stride=s, padding=p)
for i, (k, s, p) in enumerate(LAYERS)]).eval()
x = torch.randn(1, 1, 64, requires_grad=True)
y = m(x)
for j in (0, 1, 8, 15):
g, = torch.autograd.grad(y[..., j].sum(), x, retain_graph=True)
idx = torch.nonzero(g.abs().sum(1).flatten()).flatten()
print(f"j={j:2d}: autograd [{idx.min().item():3d}, {idx.max().item():3d}]"
f" 公式 [{j*S+a:4d}, {j*S+b:4d}]")
1
2
3
4
j= 0: autograd [ 0, 7] 公式 [ -7, 7]
j= 1: autograd [ 0, 9] 公式 [ -5, 9]
j= 8: autograd [ 9, 23] 公式 [ 9, 23]
j=15: autograd [ 23, 37] 公式 [ 23, 37]

在序列內部 ($j = 8, 15$) 兩者完全相同, 公式是 tight 的, 不多不少. 在邊界上公式會伸出去 ($j = 0$ 的左端 $-7$$j = 1$ 的左端 $-5$), 那些位置在 offline 推論時是補出來的 0, 不是真的輸入幀. 這正是 effective padding 這個名字的意思.


小結, 以及還沒講的部分

有了每個 node 的 $[S, a, b]$, 我們能馬上感受到兩件事:

  • $\mathrm{rf} = b - a + 1$ → 這個 node 被多少輸入幀所影響
  • $\mathrm{lookahead} = \max(b, 0)$ → 這個 node 貢獻多少 latency

而且任何 op 只要能定義出 $(k, s, p)$ 的概念, 就能算它的 RF, 跟它在做什麼運算無關. 這也是為什麼這套東西可以寫成一個對 graph 做分析的 pass, 而不是針對每種 layer 各寫一套.
這篇只處理了 single path. 真實的模型有 residual add、有 U-Net 的 skip connection, 兩條 $[S, a, b]$ 不同的路徑在 add 相遇時會發生什麼事? Distill 文章給的對齊條件是, 對所有的路徑配對 $(i, j)$ 與所有的 $l$:
$$S_l^{(i)} = S_l^{(j)}, \qquad -P_l^{(i)} + \frac{r_l^{(i)} - 1}{2} \;=\; -P_l^{(j)} + \frac{r_l^{(j)} - 1}{2}$$ 翻成 $[S, a, b]$ 的語言會漂亮很多. 第二條的左右兩邊分別就是 $a + \frac{b-a}{2} = \frac{a+b}{2}$, 所以整個條件是:
$$S^{(i)} = S^{(j)}, \qquad \frac{a^{(i)} + b^{(i)}}{2} = \frac{a^{(j)} + b^{(j)}}{2}$$ 相同的 $S$、相同的中心. 講白話就是: 兩條路徑的取樣率要一樣, 而且每個輸出位置看的視窗要對準同一個中心點. $S$ 不同表示兩邊的時間軸根本沒對齊; 中心不同則表示視窗一邊偏過去了, 相加之後每個輸出幀的依賴範圍會變得不一致, 這時候就得靠插入 delay 之類的手段把它們橋回來. 這部分留待之後再寫.


參考資料