ARK · 理解深度學習CHAPTER 13 / 21

CHAPTER 13 / 21 · PART 3 · 結構的力量:四種主力架構

圖神經網路:在關係網上做學習

Graph neural networks

當資料只剩下「誰跟誰相連」,模型該怎麼設計:訊息傳遞、鄰域聚合,以及層數的代價。

§01學習重點

§02課程內容

一、當資料只剩下「誰跟誰相連」

先把這一章要處理的資料攤開來看。

假設某地出現一波呼吸道傳染病,衛生單位做接觸者追蹤(contact tracing),把每一次「兩個人在同一個場合共處超過一段時間」的回報登記下來。整理完的東西長這樣:一份被匡列的人的名單,加上一份「誰跟誰共處過」的配對清單。每個人身上另外帶幾個數字——距離最後一次共處過了幾天、這幾天回報的體溫偏離值、所在行政區這一週的通報密度、有沒有完成一次快篩。本章從頭到尾用這個設定當例子;所有欄位與後面出現的數字,都是為了把機制講清楚而假想的。

要回答的問題是:接下來該優先安排誰採檢?

這個問題看起來就是個分類任務,把每個人的四個數字餵進前面幾章任何一個網路,好像就能做。可是這樣做等於把最重要的東西丟掉了——答案不在那四個數字裡,在誰跟誰相連裡。 一個各項數值都很普通、但同時跟三個已確診者共處過的人,風險遠高於一個數值稍差、卻只跟一個低風險者接觸過的人。要用上這件事,模型就必須能讀進「連法」本身。

麻煩從這裡開始。這種資料有三個「沒有」,每一個都直接打掉前面某一章的前提。

第一個沒有:每個人沒有座標。 影像裡的每個像素有明確的位置——第幾列、第幾行;一段文字裡的每個字也有位置——第幾個。位置本身帶著意義,所以影像上的運算可以規定「左邊的鄰居用這組權重、上面的鄰居用那組權重」——那是第 10 章卷積網路的地盤。接觸網裡沒有這種東西。你可以說某個人有三個接觸者,但說不出誰是「上面那個」。

第二個沒有:整體沒有固定的先後次序。 名單上誰排第一、誰排第二,是登記的時候順手決定的。把名單重新排一次,關係一條都沒變,你手上還是同一張接觸網。

第三個沒有:每一筆的大小不一樣。 這一波的群聚有四十個人,下一波可能有九百個,再下一波只有六個。前面幾章的網路,輸入向量的長度是設計網路時就釘死的。

把這三件事放在一起,就得到一條硬要求:

任何一層的運算,都不准依賴某個人被編到第幾號。

這條要求聽起來像廢話,實際上非常強,強到足以把「攤平成一條向量再送進全連接層」這個做法整個排除掉。攤平的動作只要一發生,第一個人的資料就落在向量的前幾格、第二個人落在接下來幾格——網路學到的權重從此就綁在編號上。名單重排一次,同一張接觸網會被算成完全不同的東西。

對照一下就更清楚:影像換掉像素的位置,它真的變成另一張影像;文字換掉字的次序,它真的變成另一句話。只有這種關係型資料,換掉編號之後東西完全沒變。 前兩者的「位置有意義」是可以拿來用的資源,這裡的「編號沒意義」則是必須主動繞開的陷阱。

「一堆點,加上連接它們的線」這個數學物件,名字就叫圖(graph)。這裡要先做一次消歧:中文的「圖」有兩個常用意思,本章講的不是圖片、也不是圖表,而是那個由點與線構成的數學物件。 點叫節點(node),線叫邊(edge);本章行文多半直接說「連線」,兩個詞指同一件事。專門處理這類資料的網路,就是圖神經網路(graph neural network),常見的縮寫是 GNN。

本章從頭到尾只談最單純的一種設定:連線沒有方向(甲跟乙共處過,就等於乙跟甲共處過),而且兩個人之間最多只有一條線。真實世界的圖還有很多變化——連線帶方向、節點分好幾種型別、同兩點之間可以有好幾條線——那些變化不改變本章要講的機制,先擱著。

值得順帶一提:能被寫成圖的東西比你想像的多。供應鏈上下游的供貨關係、帳戶之間的轉帳往來、一批人共用同一台機具的排班、水管網裡的閥門與管段,都是「一堆點加上連法」的結構,而且答案往往就藏在連法裡。認出一份資料其實是一張圖,通常是解題的第一步。

二、把一張接觸網交給電腦:兩份東西分開存

一張圖要餵進程式,得拆成兩份東西存:誰跟誰相連,以及每個人身上帶的那串數字。這兩份東西的形狀完全不同,混在一起存反而麻煩。

先看連法。最直接的寫法是開一個以人為列、也以人為行的方陣:第 \(m\) 列第 \(n\) 行如果是 1,就代表第 \(m\) 個人與第 \(n\) 個人共處過;沒共處過就是 0。這個方陣叫鄰接矩陣(adjacency matrix),本章記成 \(\mathbf{A}\);有些中文教材叫它相連方陣,指的是同一個東西。

\(\mathbf{A}\) 有兩個一眼可見的性質。第一,它是對稱的——共處這件事沒有方向,所以第 \(m\) 列第 \(n\) 行與第 \(n\) 列第 \(m\) 行永遠一樣。第二,它非常空。一個人一輩子接觸過的人數,跟名單長度沒什麼關係;名單長到十萬人,某個人的那一列還是只有十幾個 1。所以真實系統幾乎不會真的開一個十萬乘十萬的陣列,而是只把「有連線的那些配對」列成一張清單。本章的例子很小,直接用完整方陣比較好讀。

再看每個人身上的數字。把它排成一個矩陣 \(\mathbf{X}\):一個人一欄,欄數等於人數 \(N\),列數等於每個人帶幾個數字 \(D\)。這個「一個人一欄」的擺法在後面會省下很多力氣,因為它讓「把一群人的向量加起來」剛好變成一次矩陣乘法。

底下這段程式把一個假想的小群聚寫成方陣。八個被匡列的人,編號 0 到 7,十條共處紀錄:

PYTHON
import numpy as np

# 一場群聚事件的接觸網:8 個被匡列的人,編號 0–7。
# 一條連線 = 兩人被回報在同一場合共處過。
N = 8
edges = [(0, 1), (0, 2), (1, 2), (1, 3), (2, 4),
         (3, 4), (3, 5), (4, 6), (5, 6), (6, 7)]

A = np.zeros((N, N), dtype=int)
for m, n in edges:
    A[m, n] = A[n, m] = 1

print("方陣是否對稱:", np.array_equal(A, A.T))
print("每個人的接觸人數:", A.sum(axis=0))

# 換一套編號:原本的 i 號改叫 perm[i] 號
rng = np.random.default_rng(0)
perm = rng.permutation(N)
Pi = np.zeros((N, N), dtype=int)
Pi[perm, np.arange(N)] = 1              # 排列矩陣
A_new = Pi @ A @ Pi.T

print("新編號:", perm)
print("兩個方陣逐格相同:", np.array_equal(A, A_new))
print("接觸人數(新編號):", A_new.sum(axis=0))
print("兩份接觸人數排序後相同:",
      sorted(A.sum(0).tolist()) == sorted(A_new.sum(0).tolist()))

# 方陣自乘:走 k 步的走法數
W2 = A @ A
print("1 號走兩步到 4 號的走法數:", W2[1, 4])
print("0 號走兩步回到自己的走法數:", W2[0, 0])
print("0 號走兩步到 7 號的走法數:", W2[0, 7])

實跑輸出:方陣對稱為 True;八個人的接觸人數依序是 2 3 3 3 3 2 3 1。換過編號之後(新編號是 2 4 3 6 5 0 1 7),兩個方陣逐格相同是 False,但把接觸人數排序後比對是 True。這兩行是本節的重點——方陣的長相變了,圖沒變。

換編號這件事可以寫得更精確。設 \(\boldsymbol\Pi\) 是一個排列矩陣(permutation matrix):每一列、每一行都恰好只有一個 1,其餘全是 0,它的作用就是把編號重新指派一次。換過編號之後,兩份東西各自變成

$$ \mathbf{A}' = \boldsymbol\Pi\,\mathbf{A}\,\boldsymbol\Pi^{\mathsf{T}}, \qquad \mathbf{X}' = \mathbf{X}\,\boldsymbol\Pi^{\mathsf{T}} $$

逐項拆解:\(\boldsymbol\Pi\) 左乘 \(\mathbf{A}\) 是把列重排,右乘 \(\boldsymbol\Pi^{\mathsf{T}}\) 是把行重排——連法是配對關係,列與行必須同時重排才對得起來。\(\mathbf{X}\) 只在欄的方向上排著人,所以只需要右乘一次。上標 \({\mathsf{T}}\) 是轉置。(有些教材把排列矩陣寫成 \(\mathbf{P}\),本站把這個字母留給別的用途,所以改用大寫希臘字母 \(\boldsymbol\Pi\)。)

這條式子就是第一小節那句「不准依賴編號」的具體形式:一個合格的圖上運算,必須在 \(\mathbf{A}\) 與 \(\mathbf{X}\) 被這樣重排之後,仍然給出對應的答案。

方陣還有一個好玩的用法,順手講一下。把 \(\mathbf{A}\) 自己乘自己,得到的 \(\mathbf{A}^2\) 裡第 \(m\) 列第 \(n\) 行那個數字,是「從第 \(m\) 個人出發、沿著連線走兩步、剛好停在第 \(n\) 個人身上」有幾種走法。程式輸出裡,1 號走兩步到 4 號的走法數是 2(1 號可以先經過 2 號,也可以先經過 3 號),0 號走兩步到 7 號是 0(兩步之內碰不到)。這裡有一個很容易踩的坑:這種「走法」允許來回踩同一個人——0 號走兩步回到自己的走法數是 2,那兩種走法分別是先到 1 號再折回、先到 2 號再折回。所以走法數跟「不重複經過任何人的路徑數」是兩個不同的量,不要混用。

之所以提這件事,是因為下一節那個「一層等於往外走一步」的說法,用得上這個直覺。

三、一層在做什麼:向鄰居要訊息、跟自己合併

這一節是整章的心臟。

一層更新做三個動作,順序固定:

  1. 收集:把跟自己有連線的那些人目前的向量拿過來;
  2. 合併:把收到的那一疊,和自己目前的向量合成一個;
  3. 變換:合成的結果過一次線性變換,再過一次非線性。

寫成式子。設 \(\mathbf{h}^{(k)}_n\) 是第 \(n\) 個人在第 \(k\) 層的向量,\(\mathcal{N}(n)\) 是跟他有連線的那群人(他的鄰居),則

$$ \mathbf{h}^{(k+1)}_n = a\Bigl[\boldsymbol\Omega_k\Bigl(\mathbf{h}^{(k)}_n + \sum_{m\in\mathcal{N}(n)}\mathbf{h}^{(k)}_m\Bigr) + \mathbf{b}_k\Bigr] $$

逐項拆解:括號最裡面那個求和,就是動作一與動作二——把所有鄰居的向量加起來,再加上自己的。\(\boldsymbol\Omega_k\) 是第 \(k\) 層的權重矩陣,\(\mathbf{b}_k\) 是這一層的偏置向量,兩者合起來完成動作三的線性部分(有些教材把權重寫成 \(\mathbf{W}_k\))。\(a[\cdot]\) 是激活函數,本章一律用 ReLU。這一層要學的東西,就只有 \(\boldsymbol\Omega_k\) 與 \(\mathbf{b}_k\)。

因為前一節把節點資料排成「一個人一欄」,這條逐點的式子可以一口氣壓成整網的矩陣形式:

$$ \mathbf{H}_{k+1} = a\bigl[\boldsymbol\Omega_k\,\mathbf{H}_k(\mathbf{A}+\mathbf{I}) + \mathbf{b}_k\mathbf{1}^{\mathsf{T}}\bigr] $$

逐項拆解:\(\mathbf{H}_k\) 是第 \(k\) 層所有人的向量排成的矩陣,第 0 層就是輸入 \(\mathbf{X}\)。\(\mathbf{H}_k\mathbf{A}\) 這個乘法做的事情,正是「每一欄換成它所有鄰居那幾欄的總和」——這是把節點排在欄方向的全部好處。\(\mathbf{I}\) 是單位矩陣(對角線全 1、其餘全 0),加上它就等於把自己那一欄也算進來;請注意這個粗體羅馬的 \(\mathbf{I}\) 跟第 7 章那個黑板粗體的指示函數 \(\mathbb{I}[\cdot]\) 是兩個不同的東西,只是字母長得像。\(\mathbf{1}\) 是一條全部是 1 的向量,\(\mathbf{b}_k\mathbf{1}^{\mathsf{T}}\) 把同一個偏置複製到每一欄上。

這種「每個鄰居送一份自己的近況過來、收到的人把它們併進自己的狀態」的更新方式,領域裡叫訊息傳遞(message passing),把鄰居那一疊併起來的那一步叫鄰域聚合(neighbourhood aggregation)。

聚合這一步不能亂選,它必須同時滿足兩個條件。 第一,對鄰居的排列不敏感——鄰居沒有次序可言,先加誰後加誰都得到同一個答案。第二,不限定鄰居有幾個——某人可能有一個接觸者,某人可能有四十個,同一個運算得兩種都吃得下。

這兩條同時排除掉一個很多人第一時間會想到的做法:把鄰居的向量接起來成一條長向量。 接起來立刻壞掉,而且壞兩次:接的次序不同會得到不同的長向量(違反第一條),鄰居數不同會得到不同長度的向量(違反第二條)。相加不會有這兩個問題,取平均、逐項取最大值也不會。

還有一件事同樣關鍵:\(\boldsymbol\Omega_k\) 與 \(\mathbf{b}_k\) 是全網共用的,不是每個人各配一組。 用疫調的語言講就是:同一套研判規則套在每一個被匡列的人身上,不管他是這波的第一個個案還是第九百個。這件事帶來兩個後果。一是參數量跟名單長度脫鉤——網路多大都是那一組 \(\boldsymbol\Omega_k\),所以訓練時看的是四十人的群聚、上線後遇到九百人的群聚也照樣跑得動。二是模型被迫用同一把尺看待每個位置,它沒有機會去記「7 號那個人比較特別」,因為根本沒有一組專屬於 7 號的參數可以記。

到這裡就可以把「不准依賴編號」講成兩個更精確的性質。

排列等變(permutation equivariance):把輸入的編號換一套,輸出跟著換同一套。逐點輸出的任務要的就是這個——每個人各自一個答案,名單重排時答案應該跟著人走。寫成式子,若 \(\mathbf{F}[\cdot]\) 代表一整層(或一整個網路主體),則

$$ \mathbf{F}\bigl[\mathbf{H}\boldsymbol\Pi^{\mathsf{T}},\; \boldsymbol\Pi\mathbf{A}\boldsymbol\Pi^{\mathsf{T}}\bigr] = \mathbf{F}\bigl[\mathbf{H},\mathbf{A}\bigr]\,\boldsymbol\Pi^{\mathsf{T}} $$

逐項拆解:等號左邊是「先換編號,再算」,右邊是「先算,再換編號」。兩邊相等,就代表這一層完全不在意你怎麼編號。

排列不變(permutation invariance):把輸入的編號換一套,輸出根本不動。整張網只給一個答案的任務要的是這個——一整條傳染鏈屬於哪一種傳播型態,不會因為你把名單重排就變成另一種。

兩者的關係很簡單:訊息傳遞的每一層都是等變的,把整網的向量匯總成一個(例如取平均)這個動作是不變的,等變的層疊幾次還是等變,最後接上一個不變的匯總,整個網路就變成不變。

底下這段程式把一層訊息傳遞真的跑一次,並且把等變性當成數值來驗證:

PYTHON
import numpy as np

rng = np.random.default_rng(0)

N = 8                                    # 同一張接觸網
edges = [(0, 1), (0, 2), (1, 2), (1, 3), (2, 4),
         (3, 4), (3, 5), (4, 6), (5, 6), (6, 7)]
A = np.zeros((N, N))
for m, n in edges:
    A[m, n] = A[n, m] = 1.0

D_in, D_out = 3, 4
X = np.round(rng.normal(0, 1, (D_in, N)), 2)   # 一個人一欄
Om = rng.normal(0, 0.5, (D_out, D_in))         # 全網共用的一組參數
b = rng.normal(0, 0.5, (D_out, 1))


def layer(H, adj):
    agg = H @ (adj + np.eye(adj.shape[0]))     # 收鄰居 + 自己
    return np.maximum(0.0, Om @ agg + b)       # 線性變換 + ReLU


H1 = layer(X, A)
print("輸入形狀:", X.shape, " 一層之後:", H1.shape)
print("3 號的輸入向量:", X[:, 3])
agg3 = (X @ (A + np.eye(N)))[:, 3]
print("3 號收完鄰居後:", agg3)
print("3 號的預活化(ReLU 之前):", np.round(Om @ agg3 + b.ravel(), 3))
print("3 號更新後的向量:", np.round(H1[:, 3], 3))

# 換編號,再跑同一層
perm = rng.permutation(N)
Pi = np.zeros((N, N))
Pi[perm, np.arange(N)] = 1.0
H1_new = layer(X @ Pi.T, Pi @ A @ Pi.T)
print("新編號:", perm)
print("『先換編號再算』與『先算再換編號』的最大差距:",
      float(np.abs(H1_new - H1 @ Pi.T).max()))

# 整網一個答案:對所有人取平均之後才換編號
print("換編號前的整網平均向量:", np.round(H1.mean(axis=1), 3))
print("換編號後的整網平均向量:", np.round(H1_new.mean(axis=1), 3))

實跑輸出:輸入形狀是 (3, 8),一層之後變成 (4, 8)——人數不變,每個人身上的數字從三個變成四個。3 號的輸入向量是 [0.1 0.04 1.04],收完鄰居(1 號、4 號、5 號)與自己之後變成 [-0.21 -3.78 1.96],過完線性變換得到 [-1.328 1.113 0.737 0.632],再過一次 ReLU 就是 [0. 1.113 0.737 0.632]——第一維本來是負的,被壓成了零。等變性那一行最關鍵:「先換編號再算」與「先算再換編號」的最大差距是 2.220446049250313e-16,也就是浮點運算的捨入誤差量級,等於零。至於整網的平均向量,換編號前後都是 [0.097 0.36 0.284 0.798],一個數字都沒動——這就是不變性。

比喻: 想像疫調人員在一天結束時做一輪回報。每個被匡列的人把自己今天的狀況——體溫、有沒有症狀、快篩結果——送給所有跟他共處過的人;每個人收到一疊回報之後,連同自己今天的狀況一起,重新評估自己的風險等級。這就是一層訊息傳遞:收、併、更新。要注意這個畫面在兩個地方失準。第一,真實的疫調有先後:甲先接到通知,才去通知乙,時間差是真的存在的;而一層訊息傳遞是所有人同時更新,同一輪之內誰都不會用到別人這一輪的新結果。第二,真實接觸網上流動的東西裡有病原體本身,被通知的人真的可能被傳染;而網路裡流動的只有數字,沒有任何東西真的「被傳過去」——訊息傳遞這個名字容易讓人以為模型在模擬傳播過程,它沒有,它只是在算一個函數。

為什麼有人把這件事叫做圖上的卷積? 因為它跟卷積共用同一個骨架:只從附近取用資訊,而不是一次看全部;同一組參數走遍所有位置,而不是每個位置各配一組。這兩條合起來,正是卷積這個想法的核心(它在影像上的展開,是第 10 章的卷積網路)。

但它不完全是。 差別有兩處,而且都很實在。影像上每個位置的鄰居數量固定(一個 3×3 的窗永遠是八個鄰居),關係網上不固定。更關鍵的是,影像上的鄰居有明確的方位——左邊的鄰居和上面的鄰居可以配不同的權重,而這正是卷積能認出「邊緣往哪個方向」的原因。關係網上沒有方位,你的三個接觸者之間沒有任何一個「在左邊」。所以圖上的一層只能對所有鄰居一視同仁地聚合,表達力天生比影像上的卷積弱一截。理解這一點,後面那些「加權聚合」的變體才會顯得有動機。

既然聚合可以有變體,這裡一次講完取捨。相加與取平均的差別,是本節最值得記住的一組。 相加會把「這個人有幾個接觸者」這個訊息一起帶進結果裡——接觸五十個人的那一欄,數值量級天生就比只接觸一個人的大;取平均則把它抹掉,五十個鄰居和一個鄰居出來的量級一樣。哪個好取決於任務:如果「接觸人數多寡」本身就是風險訊號,抹掉它是損失;如果你希望模型別被大戶帶偏,抹掉它是保護。其餘變體都是這條軸上的微調——把自己那一份乘上一個可學的倍率、按接觸人數多寡把每個鄰居的份量調降、每一維各自取鄰居裡的最大值。

還有一種更有意思的變體:讓每個鄰居的份量由資料自己算出來。 也就是給每一對相連的節點一個權重 \(\alpha_{mn}\),聚合時按這個權重加權:

$$ \mathbf{h}^{(k+1)}_n = a\Bigl[\boldsymbol\Omega_k \sum_{m\in\mathcal{N}(n)\cup\{n\}} \alpha_{mn}\,\mathbf{h}^{(k)}_m + \mathbf{b}_k\Bigr] $$

逐項拆解:\(\alpha_{mn}\) 是「第 \(m\) 個人的向量在更新第 \(n\) 個人時佔多少份量」,通常被壓成非負、且對 \(n\) 的所有鄰居加起來等於 1。前面那個一視同仁的版本,等於把所有 \(\alpha_{mn}\) 都固定成同一個數;現在則讓它由兩端節點目前的向量算出來。這種做法叫圖注意力(graph attention)。

這裡值得點一句它跟第 12 章的 Transformer 的關係,因為兩者共用同一個骨架:加權平均一群向量,權重不是固定的,而是算出來的。 差別在權重受什麼限制。序列上的做法讓每個位置都能看向所有位置——等於在一張「人人相連」的圖上做訊息傳遞;圖上的做法則先被連法擋住,只有真的有連線的那些對子才有權重,其餘一律是零。反過來說,把序列上的權重遮成「只准看鄰居」,得到的就是圖注意力層。權重從哪裡來(連法給的,還是算出來的)與權重能落在哪裡(全部,還是只有鄰居),是這兩章的分界線;至於權重具體怎麼算出來,那是第 12 章的主場。

最後補一句路標:從「訊號在圖上怎麼分解」的角度出發,也能推導出圖上的卷積,那是另一條技術路線,需要不少額外的線性代數工具,本課不走。

四、層數=往外看多遠:感受野與它的代價

一層等於往外走一步。這句話值得停下來確認一次。

第 \(k+1\) 層的向量,是由自己和鄰居的第 \(k\) 層向量算出來的。所以第 1 層的向量吃進了自己與一步之內的人;第 2 層的向量吃進了自己與一步之內的人「他們的第 1 層向量」,而那些向量又各自吃進了他們的鄰居——加起來就是兩步之內的人。以此類推,\(K\) 層網路裡,每個人的最終向量吃進的是他 \(K\) 步之內能碰到的所有人的原始資料。

這一群人有一個名字:一個節點的感受野(receptive field),指的就是「它的最終輸出實際上用到了哪些節點的輸入資料」。它在別的架構上也有對應的意思,遇到時可以套用同一句定義。

用疫調的話說,感受野就是「這個人的判讀,實際上算進了往外幾圈的人」。一層就是直接接觸者,兩層是接觸者的接觸者,三層再往外一圈。

問題在於這個圈長得非常快,而且它長多快由連法決定,不是由你決定。 影像上的感受野是可以算的:每疊一層 3×3 的卷積,方形範圍就往外推一格,規規矩矩。關係網上不是——只要中間有一個接觸人數特別多的人,往外一步就可能把幾十個人一次拉進來。

這件事直接跑數據看最清楚。下面這段程式造一張大一點的接觸網,然後量兩件事:感受野隨層數怎麼長,以及節點向量之間的差異隨層數怎麼掉。

PYTHON
import numpy as np

rng = np.random.default_rng(0)

# 造一張大一點的接觸網:150 個被匡列的人、40 場被回報的聚會,
# 每場 4–8 人到場,同一場的人兩兩相連。
N, n_events = 150, 40
A = np.zeros((N, N))
for _ in range(n_events):
    size = int(rng.integers(4, 9))
    who = rng.choice(N, size=size, replace=False)
    for i in range(size):
        for j in range(i + 1, size):
            A[who[i], who[j]] = A[who[j], who[i]] = 1.0

deg = A.sum(axis=0)
core = np.nonzero(deg > 0)[0]            # 至少有一個接觸者的人
print(f"連線 {int(A.sum() // 2)} 條,平均接觸人數 {deg.mean():.2f},"
      f"最多 {int(deg.max())},完全沒有連線的人 {N - core.size} 個")

# 感受野:往外走 k 步碰得到的人數(含自己),對全體取平均
reach = np.eye(N) + A
for k in range(1, 6):
    size = (reach > 0).sum(axis=1)
    print(f"{k} 層:平均感受野 {size.mean():6.2f} 人,最大 {int(size.max())} 人")
    reach = (reach @ (np.eye(N) + A) > 0).astype(float)

# 過度平滑:把線性變換與非線性拿掉,只留聚合這一步
H = rng.normal(0, 1, (8, N))
M = (A + np.eye(N)) / (deg + 1)          # 鄰居與自己取平均


def spread(V):                           # 兩兩之間的平均歐氏距離
    n = V.shape[1]
    d = np.linalg.norm(V[:, :, None] - V[:, None, :], axis=0)
    return d.sum() / (n * (n - 1))


print(f"0 層:有連線者的平均兩兩距離 {spread(H[:, core]):.4f}")
for k in range(1, 9):
    H = H @ M
    print(f"{k} 層:有連線者的平均兩兩距離 {spread(H[:, core]):.4f}")

實跑輸出的第一行是:連線 716 條,平均接觸人數 9.55,最多 36,完全沒有連線的人 29 個。感受野的部分,一到五層的平均人數依序是 10.5562.3597.1197.8097.80

這組數字有三件事要讀出來。第一,兩層就從十個人跳到六十二個人。 這不是線性成長,是滾雪球——每往外一圈,新拉進來的人數大致是上一圈乘上「平均還能再往外連幾個」。第二,第三層之後就不動了。 因為這張網已經被走遍了;一百五十人裡有二十九個人完全沒有回報過任何共處,剩下的一百二十一人連成一整塊,感受野長到一百二十一就到頂。第三,最大值和平均值差很多——同樣是一層,最廣的那個人的感受野是三十七人,而完全沒有回報過共處的那二十九人,感受野永遠只有他自己一個。感受野在這裡不是一個整齊的方形範圍,它的形狀由連法決定,每個人各不相同。

感受野膨脹帶來兩個實務後果,方向相反但都很麻煩。

第一個後果:深不一定好。 資訊愈滾愈廣,每個人的向量吃進的東西愈來愈接近「整張網的平均」,本來的局部差異就被沖淡。程式的第二段把這件事量出來了:把線性變換和非線性拿掉、只留聚合這一步,有連線的那一百二十一人之間的平均兩兩距離,從第 0 層的 3.7698 掉到第 1 層的 1.0386、第 2 層的 0.5560、第 3 層的 0.3432,第 8 層只剩 0.0502。大約每疊一層就對折一次。這個現象有一個標準名字:過度平滑(over-smoothing)。當所有人的向量都長得差不多,逐點分類的任務就沒東西可分了——你不可能用一組幾乎相同的向量去區分誰該優先採檢。

(要說明的是,這段程式刻意拿掉了權重與非線性,為的是把聚合這一步單獨拉出來看。真實的網路有權重可以學,衰減不會這麼乾脆,但方向是同一個。)

所以圖神經網路很少疊得很深。前面幾章「深就是好」的直覺在這裡失效,而且失效的理由很具體:這裡的「深」不只是多做幾次變換,它同時在擴大每個節點看到的範圍,而範圍擴大到某個程度就會把訊號洗掉。

第二個後果:大網路很難湊出一個乾淨的批次。 第 6 章講過的小批次隨機梯度下降有一個隱含前提:一批資料裡的每一筆是各自獨立的,算完梯度就可以丟掉。在圖上這個前提不成立——你想更新某十個人的參數,就得先把他們 \(K\) 步之內的鄰居全部載進來算前向傳遞。而剛剛那組數字說明,光是一個人、兩層,平均感受野就有六十二人(全網的四成),三層是九十七人;十個人的感受野聯集起來,多半就是整塊連得到的那一百二十一人。一批「十個人」,實際要載入的資料量跟整張網差不多。

實務上有兩條壓縮的路。一條是往外每一圈只隨機留幾個鄰居,不要全收——這樣每往外一層,需要載入的人數就從「乘上平均接觸人數」變成「乘上你設定的那個小數字」。這個做法順帶帶來一點正則化的效果,因為每次抽到的鄰居不一樣,等於每一步都把一部分連線臨時遮起來,模型沒辦法死記某一條特定的連線。另一條是先把大網切成幾塊,一塊當一批。切的時候當然會剪斷一些連線,那些被剪斷的關係就在這一批裡消失了,這是為了跑得動而付出的代價。

比喻: 疫調實務裡,「匡列到第幾圈」是要當場決定的。只匡列直接接觸者,範圍小、動得快,但可能漏掉一條已經傳出去兩手的傳染鏈;匡列到第三圈,覆蓋率高了,代價是名單暴增、每個人分到的採檢資源被稀釋。層數要選幾層,面對的是同一個取捨。這個比喻有一處明確失準:真實匡列往外一圈,風險是遞減的——第三圈的人本來就比第一圈的人風險低,實務上會據此分級處理。訊息傳遞不會自動幫你打這個折,它一視同仁地把外圈的資訊混進來;如果你希望模型對遠處的人打折,那件事得由網路自己從資料裡學會,或由你在設計聚合方式時明白寫進去。另一處失準更根本:真實的匡列不會讓被匡列的人彼此變得越來越像,而過度平滑恰恰就是這件事。

五、三種任務、三種收尾,以及把資訊搬到連線上

同一張接觸網可以問三種不同層級的問題。這三種問題共用同一個網路主體,差別只在最後怎麼收尾——這一點值得明講,因為很多人第一眼會以為要學三種不同的模型。

第一種,每個人各一個答案。 「這個被匡列的人接下來會不會驗出陽性?」主體跑完之後,每個人手上有一個向量 \(\mathbf{h}^{(K)}_n\),直接逐點接一個輸出層,把它壓成一個機率。這叫節點分類(node classification);如果答案是連續值(例如預估幾天後會發病),就是節點層級的迴歸。這一類任務要求整個網路是排列等變的。

第二種,判斷兩個人之間該不該有一條連線。 疫調裡有一種很實際的需求:兩個人都說自己沒接觸過,但他們的關係網長得像是「中間漏了一次共處」。這叫連線預測(link prediction)。做法是把兩端的最終向量合成一個數,再壓成機率:

$$ Pr(\text{有連線}\mid m,n) = \mathrm{sig}\bigl[\mathbf{w}^{\mathsf{T}}\bigl(\mathbf{h}^{(K)}_m \odot \mathbf{h}^{(K)}_n\bigr) + b\bigr] $$

逐項拆解:\(\odot\) 是逐元素相乘(兩個等長向量對應位置各自相乘),把兩個人的向量揉成一個同長度的向量;\(\mathbf{w}\) 與 \(b\) 把它壓成一個數;\(\mathrm{sig}[\cdot]\) 是第 5 章的 logistic sigmoid,把任意實數壓進 0 與 1 之間。這裡要防一個常見誤解:這個任務預測的是「該不該有這條線」,不是「這條線有多重」。 前者是二選一,後者是迴歸,兩者的輸出層和損失都不一樣。

第三種,整張網只給一個答案。 「這一整條傳染鏈屬於哪一種傳播型態——單一場所內擴散,還是跨場所串聯?」這時要先把所有人的向量匯總成一個向量,再接輸出層:

$$ \mathbf{h}_{\text{net}} = \frac{1}{N}\sum_{n=1}^{N}\mathbf{h}^{(K)}_n $$

取平均是最常見的匯總方式,取總和或逐項取最大值也行。匯總這個動作把等變變成不變,正好對上這類任務的需求。這叫圖分類(graph classification)。

損失函數不是新東西。連續值的答案用第 5 章的平方誤差,是非題用第 5 章的二元交叉熵,全站一律採平均口徑。本章唯一換掉的是損失套在哪個層級上:逐點任務對有標籤的那些節點取平均,連線預測對有標籤的那些配對取平均,圖分類對整批的圖取平均。寫成通式就是

$$ \mathcal{L}[\boldsymbol\phi] = \frac{1}{\lvert\mathcal{S}\rvert}\sum_{s\in\mathcal{S}} \ell_s $$

其中 \(\mathcal{S}\) 是「有答案可以比對的那些東西」的集合——它可能是一群節點、一群配對,也可能是一批圖。\(\boldsymbol\phi\) 是整個網路的參數全集。

底下這段程式把三種收尾接在同一個主體後面,看看它們各自吐出什麼形狀:

PYTHON
import numpy as np

rng = np.random.default_rng(0)

N = 8
edges = [(0, 1), (0, 2), (1, 2), (1, 3), (2, 4),
         (3, 4), (3, 5), (4, 6), (5, 6), (6, 7)]
A = np.zeros((N, N))
for m, n in edges:
    A[m, n] = A[n, m] = 1.0
M = (A + np.eye(N)) / (A.sum(axis=0) + 1)      # 這次改成取平均

D_in, D_h = 3, 4
X = rng.normal(0, 1, (D_in, N))
trunk = [(rng.normal(0, 0.8, (D_h, D_in)), rng.normal(0, 0.3, (D_h, 1))),
         (rng.normal(0, 0.8, (D_h, D_h)), rng.normal(0, 0.3, (D_h, 1)))]

H = X
for Om, b in trunk:                            # 共用的主體:兩層訊息傳遞
    H = np.maximum(0.0, Om @ (H @ M) + b)
print("主體輸出的形狀:", H.shape)


def sigmoid(z):
    return 1.0 / (1.0 + np.exp(-z))


# 收尾一:每個人各一個答案
w_node = rng.normal(0, 1.0, (1, D_h))
p_node = sigmoid(w_node @ H + 0.4).ravel()
print("逐人機率:", " ".join(f"{v:.3f}" for v in p_node))

# 收尾二:兩人之間是否漏掉一次共處
w_link = rng.normal(0, 1.0, D_h)
for m, n in [(0, 7), (2, 5), (1, 3)]:
    s = float(w_link @ (H[:, m] * H[:, n]))
    print(f"({m},{n}) 目前 {'已' if A[m, n] else '未'}連線,"
          f"補線機率 {sigmoid(s):.3f}")

# 收尾三:整張網一個答案
w_graph = rng.normal(0, 1.0, (1, D_h))
p_graph = sigmoid(w_graph @ H.mean(axis=1, keepdims=True) - 0.2).item()
print("整張接觸網屬於跨場所串聯型的機率:", round(p_graph, 3))

# 損失:假設這 8 個人後來的採檢結果如下(0=陰性、1=陽性)
y = np.array([0., 1., 1., 0., 1., 0., 0., 0.])
bce = -np.mean(y * np.log(p_node) + (1 - y) * np.log(1 - p_node))
print("逐人任務的二元交叉熵(平均):", round(float(bce), 4))

實跑輸出:主體輸出的形狀是 (4, 8);逐人機率是 0.237 0.221 0.241 0.206 0.227 0.201 0.223 0.235;三對配對的補線機率分別是 0.4330.4730.484;整張網的機率是 0.096;逐人任務的二元交叉熵是 0.708

這裡的參數全是亂數、完全沒有訓練過,所以每一個機率值本身都沒有意義。它證明的只有一件事:同一個 H 出來之後,三行不同的收尾就得到三種不同形狀的輸出——一個長度 8 的向量、幾個純量、一個純量。順帶一提,八個人的機率全部擠在 0.2 到 0.24 之間,這正是上一節那個現象的小規模版本:兩層取平均的聚合已經把八個人洗得差不多了。

最後補一個構造。前面所有機制都假設資訊長在節點上,但有些任務的資訊長在連線上——共處了多久、在什麼樣的場合、距離多近。這種情況有一個漂亮的處理辦法:換一張圖來看。 把原本的每一條連線改當成新圖的一個節點;原本共用同一個端點的兩條連線,在新圖裡就連起來。這個構造出來的圖叫線圖(line graph);中文也有人叫它邊圖,指的是同一件事。

舉例:原圖裡 3 號跟 1 號、4 號、5 號各有一條連線,這三條線在新圖裡變成三個節點,而且兩兩相連(因為它們都共用 3 號這個端點)。原本掛在連線上的資訊——共處時長、場合類型——現在變成節點身上帶的向量。

這個構造的全部價值就在下一句:換過去之後,前面講的所有機制原封不動可以用。 你不需要為「資訊長在連線上」發明一套新的層,只要把問題翻譯到線圖上,訊息傳遞、聚合、感受野、三種收尾全部照舊。代價是線圖通常比原圖大得多——原圖有幾條連線,線圖就有幾個節點,而且一個接觸人數為 \(d\) 的人,會讓線圖多出 \(d(d-1)/2\) 條連線。

六、歸納式與轉導式:本章最容易混淆的一組

先拆一個中文的陷阱,不拆會一路混淆到底。

「歸納式」的「歸納」,跟第 4、8 章出現過的「歸納偏好(inductive bias)」,是同樣兩個字、完全不同的兩件事。 歸納偏好講的是「模型偏好哪一類解」——當有無限多組參數都能把訓練資料配到零誤差時,是什麼因素讓模型挑了其中一個。本節要講的歸納式講的是另一件事:學到的規則能不能搬到一張沒見過的圖上。 兩者唯一的共同點是中文用字,看到「歸納」時請先確認它接的是「偏好」還是「式」。

拆完之後進正題。歸納式與轉導式的差別,在資料的形狀,不在演算法

歸納式(inductive)的處境:你手上有很多張各自獨立的圖。假想你累積了過去三年的四百多條群聚事件接觸網,每一張大小不同、人也完全不重疊,每一張都標了它最後的傳播型態。目標是學出一套判準,然後套到明年新冒出來的群聚上——那張網在訓練時完全不存在。

轉導式(transductive)的處境:你手上只有一張大圖。這一波疫情全市的匡列關係就是這一張網,其中有一部分人已經採檢過、有結果,其餘的人還沒。目標不是學出一套可以搬走的規則,而是把這張網上還沒有答案的那些人填上答案。(這個詞的中譯不只一種,也有人寫成直推式,指的是同一件事。)

轉導式付出的代價很明確:新的人加進來時,你得重跑一次。 因為你根本沒有產出一套可以獨立帶走的規則,你產出的是「這張網上這些人的答案」。而它換來的好處也很明確:沒有答案的那些人也在幫忙做決定。 他們的連線和身上的向量照樣參與訊息傳遞,照樣影響鄰居的最終向量。在標籤很貴、未標記的節點卻很多的場合——採檢量能有限、但匡列名單很長——這個好處是實打實的。

有一個問題值得你自己先推一遍再往下看:三種任務裡,哪一種只可能出現在其中一種處境?

答案是圖分類。圖分類要求你手上有很多張圖,每一張是一筆資料;只有一張圖的時候,這個任務連「一批資料」都湊不出來。所以圖分類天生是歸納式的。節點分類與連線預測則兩種處境都可能出現:既可以在四百張獨立的接觸網上學一套逐點判準(歸納式),也可以在單獨一張大網上把未知的節點填滿(轉導式)。

現在講本章最值得帶走的一句提醒,它跟第 8 章直接接上。

第 8 章立了一條紀律:測試集只能用一次。那條紀律建立在一個前提上——測試資料在訓練期間完全不參與。轉導式的設定會削弱這個前提,而且是設定本身就削弱的,不是有人偷懶。想想看:被拿來當測試的那些節點,他們的連線與身上的向量,在訓練時已經參與過訊息傳遞了。訓練那些有標籤的節點時,梯度會流經測試節點的向量;測試節點的鄰居關係,實實在在地形塑了訓練節點的最終表示。

被藏起來的只有標籤,不是資料。

這不是作弊——問題設定本身就長這樣,你要在一張網上填答案,就不可能把待填的那些節點從網上挖掉。但它有一個實際後果:同一個模型在轉導式設定下量出來的表現,跟它在歸納式設定下量出來的表現,不是同一個數字,也不該被拿來直接比較。 所以報告表現時,你必須把切分是怎麼做的講清楚:哪些東西被藏起來了(只有標籤,還是連標籤帶連線?),未標記節點的特徵在訓練時有沒有參與,測試節點的連線有沒有被拿掉。這幾句話寫不寫,決定了別人能不能正確理解你那個數字。

比喻: 兩種處境對應到兩種很不一樣的疫調工作。第一種:你在寫一份判讀手冊,希望明年、後年遇到新的群聚時,第一線人員照著它就能判斷這條傳染鏈屬於哪一型——手冊必須能離開這一波疫情獨立存在。第二種:你面對的就是眼前這一張匡列名單,採檢量能只夠做三分之一的人,你要做的只是把剩下三分之二排出優先順序——你不需要一本能傳給後人的手冊,你需要今天下午的名單。這個比喻在一處失準:它讓人覺得第二種比較「省事」,好像是第一種的簡化版。事實不是——第二種每次有新的個案加進來,整個排序都得重算一次,長期成本反而可能更高。兩者是不同的問題設定,不是難易之分。

§03原書對照

原書第 13 章的篇幅比它的骨架大得多,多出來的部分集中在三個方向:圖有哪些種類、矩陣層面的推導,以及在超大關係網上訓練的工程細節。想往下挖的人,可以按下面的位置翻。

先看圖本身。原書在 pp.240–242 一口氣鋪開好幾類真實世界裡天生就是圖的東西,並用兩組圖示區分連線沒有方向的圖、連線有方向的圖,以及節點型別與連線型別都不只一種的知識圖譜;同一組圖裡還收了由三維點集連成的幾何圖,以及把數張小圖各自當成一個節點、再組成一張大圖的階層圖。想知道「什麼東西可以被寫成圖」,那三頁的清單比任何摘要都齊全。

第二處是相連方陣的代數性質。原書在 pp.244–245 用一整張圖示範:把方陣自乘幾次,裡面的數字就變成兩點之間走幾步的走法數,並提醒走法允許重複經過同一個點,因此與不重複的路徑不是同一回事。緊接著還把「換一套節點編號」寫成兩條含排列矩陣的乘法式,指出點的資料矩陣與相連方陣各自要怎麼被作用。想看這條性質怎麼從線性代數長出來,那兩頁最直接。

第三處是任務與損失。原書 pp.246–248 把整圖層級、節點層級與連線預測三類任務並排,各自寫出輸出層要怎麼接、機率怎麼算——整圖層級先把所有節點的向量匯總成一個再往下接,節點層級則逐點各接一個——並用一張三聯圖標出三者在同一個網路後端的差別。緊接著 pp.249–251 是層的推導:從「參數為什麼要在所有節點之間共用」談起,一路寫到把整層壓成一條矩陣式,中間附了等變性的定義式與對應的習題編號。

第四處是工程面。原書 p.251 給出一個判定分子是否有毒的完整網路方程組,並在 pp.251–252 說明一批大小不一的圖要怎麼併成一次前向傳遞。pp.252–253 用一節文字與一張對照圖處理歸納式與轉導式的差別;pp.254–256 接著談節點的感受野會如何往外膨脹,以及鄰域取樣與圖切分兩種對策,各配一張示意圖。

第五處是變體。原書 pp.256–258 列出好幾種聚合與合併的寫法,包含依鄰居數量做正規化的版本,也包含把自己那一份加重、或改成逐項取最大值的做法;pp.258–260 則把圖上的注意力層與序列模型的自注意力並排比較,明確指出兩者差在哪三點。pp.260–261 轉向連線上的資訊,示範怎麼把原圖換成一張「連線變成節點」的新圖,再照樣套用前面所有機制。

最後是註記與習題。pp.262–266 的註記區密度很高:圖神經網路的早期形式、頻域方法與空間方法的分家、各種取樣與正則化手法,以及層數加深為什麼長期沒有帶來好處的兩種解釋,都在那幾頁被逐一點名並附上文獻出處。pp.266–268 的習題偏重手算,適合拿來自我檢查——寫出相連方陣、數走法數、驗證一層的等變性、畫出把連線換成節點之後的新圖,都能在紙上完成。

原書第 13 章對應印刷頁 pp.240–268。

§04作業和解答

作業一:把接觸網寫成方陣,並數清楚走法

某次群聚事件匡列了六個人,記作甲、乙、丙、丁、戊、己,依序編號 0 到 5。回報的共處配對有七組:甲–乙、甲–丙、乙–丙、乙–丁、丙–戊、丁–戊、戊–己。

(a)寫出鄰接矩陣 \(\mathbf{A}\),並列出六個人各自的接觸人數。

(b)不查表、直接由圖數出來:甲走兩步到戊有幾種走法?乙走兩步到戊有幾種走法?

(c)甲走兩步回到自己有幾種走法?這個數字為什麼不等於零?

(d)甲走兩步到己有幾種走法?走三步呢?

解答 SOLUTION

(a)依編號 0–5 排列,鄰接矩陣是

$$ \mathbf{A} = \begin{bmatrix} 0&1&1&0&0&0\\ 1&0&1&1&0&0\\ 1&1&0&0&1&0\\ 0&1&0&0&1&0\\ 0&0&1&1&0&1\\ 0&0&0&0&1&0 \end{bmatrix} $$

每一欄(或每一列)加起來就是接觸人數:甲 2、乙 3、丙 3、丁 2、戊 3、己 1。注意對角線全是 0——沒有人跟自己共處。

(b)甲到戊走兩步只有一種走法:甲–丙–戊。甲的鄰居只有乙與丙,乙沒有連到戊,丙有。乙到戊走兩步有兩種:乙–丙–戊、乙–丁–戊。這兩個答案就是 \(\mathbf{A}^2\) 的第 0 列第 4 行(值為 1)與第 1 列第 4 行(值為 2)。

(c)有兩種:甲–乙–甲、甲–丙–甲。這個數字不等於零,正好說明第二小節那個易錯點——這裡數的是「走法」,允許來回踩同一個人。如果數的是「不重複經過任何人的路徑」,那甲走兩步回到自己就是零種。任何一個節點走兩步回到自己的走法數,其實就等於它的接觸人數,因為每個鄰居都提供一條「去了再折回」的走法。

(d)走兩步是零種:甲的鄰居是乙與丙,兩人都沒有連到己(己只連到戊)。走三步有一種:甲–丙–戊–己。(本題的三個矩陣以 numpy 重算核對過,\(\mathbf{A}^2\) 的第 0 列是 2 1 1 1 1 0,\(\mathbf{A}^3\) 的第 0 列是 2 4 4 2 2 1。)

作業二:感受野長多快,由誰決定

本題請你改動第四小節那段程式,量出「連法」對感受野的影響。

(a)不跑程式先想:如果每場聚會的到場人數從 4–8 人減成 3–4 人,其他條件不變,前三層的平均感受野會變大還是變小?為什麼?

(b)把 rng.integers(4, 9) 改成 rng.integers(3, 5),實跑並記下前三層的平均感受野。

(c)再改成 rng.integers(8, 13),記下前兩層的平均感受野,並說明這個結果對「該疊幾層」有什麼實務意涵。

解答 SOLUTION

(a)會變小。同一場聚會裡的人是兩兩相連的,所以一場 \(s\) 人的聚會會產生 \(s(s-1)/2\) 條連線——人數減半,連線數大約減成四分之一。平均接觸人數一掉,每往外一圈能新拉進來的人就少,感受野的滾雪球速度跟著慢下來。

(b)實跑結果:連線只剩 195 條,平均接觸人數 2.60。前三層的平均感受野是 3.608.9118.17 人。對照原設定的 10.5562.3597.11,差距非常大——第三層才碰到十八個人,還不到原設定第二層的三分之一。

(c)實跑結果:連線暴增到 1785 條,平均接觸人數 23.80;第一層的平均感受野是 24.80 人,第二層是 127.16 人——一百五十人的網,兩層就吃掉了八成五。

實務意涵有兩層。第一,「該疊幾層」不是一個可以脫離資料回答的問題。同樣是三層,在稀疏的網上剛剛好,在稠密的網上早就把整張網洗成一片。第二,決定層數之前應該先做這個量測——它只需要幾行矩陣運算,成本遠低於訓練完才發現模型在過度平滑。一個實用的做法是:先量出平均感受野佔全網的比例,選一個讓它落在合理範圍(例如不超過一兩成)的層數,再去調別的東西。

作業三:這個問題該用哪一種設定,以及該怎麼報告表現

下面三個都是假想的情境(數字為出題所需,不是真實統計)。各自判斷它屬於歸納式還是轉導式,並回答附帶的問題。

(a)某研究單位收集了過去三年四百二十條群聚事件的接觸網,每一張都標了傳播型態,要訓練一個模型,用來判讀明年新出現的群聚。

(b)本波疫情全市的匡列名單構成一張大網,共十二萬人,其中一萬八千人已有採檢結果。目標是替其餘十萬兩千人排出採檢優先順序。

(c)承(b),有人提議:「把那一萬八千人裡的三千人留下來當測試集,就能量出模型的表現。」請指出這個做法在報告時必須額外交代什麼,以及為什麼第 8 章那條「測試集只能用一次」的紀律在這裡需要補充說明。

解答 SOLUTION

(a)歸納式。判準是資料的形狀:手上是很多張各自獨立的圖,而且要套用的對象(明年的群聚)在訓練時根本不存在。這個任務是圖分類,而圖分類只可能是歸納式的——單獨一張圖湊不出一批資料。

(b)轉導式。只有一張圖,其中一部分節點有標籤,目標是把其餘節點填上答案。這裡不需要一套能搬到別的城市去用的規則,需要的是這張網上的答案。順帶一提,這正是轉導式划算的典型場合:標籤(採檢結果)很貴,未標記的節點很多,而那十萬兩千人的連線與特徵在訓練時照樣能出力。

(c)必須額外交代的是切分究竟藏起了什麼。第 8 章那條紀律建立在「測試資料在訓練期間完全不參與」這個前提上,而這裡的前提只成立了一半:那三千人的標籤被藏起來了,但他們的連線與特徵沒有——他們照樣參與訊息傳遞,照樣影響了訓練節點的最終向量。

所以報告時至少要寫清楚三件事:第一,被藏起來的是標籤還是連帶連線一起藏;第二,未標記節點的特徵在訓練時有沒有參與前向傳遞;第三,這個數字量的是「在這張網上填空的能力」,而不是「換一張網還能不能用的能力」。少了這些說明,讀者會拿它去跟一個歸納式設定下量出來的數字比較,而那兩個數字量的根本不是同一件事。

要補充的是,這不代表轉導式的評估沒有價值,也不代表它是作弊。它量的是一個真實存在、而且很常見的能力:在一張已知的網上,用少量標籤把其餘節點填滿。問題只出在把它和另一種能力混為一談。第 8 章那條紀律本身仍然成立——那三千人的標籤還是只能用一次,你不能拿它來反覆挑超參數。變的不是紀律,是紀律所依賴的那個前提需要被明確描述。

§05參考資料