8.3 圖與網路的矩陣——鄰接矩陣、Laplacian 與譜聚類#
本節目標#
- 學會圖的三種矩陣表示:鄰接矩陣 \(A\) 、度矩陣 \(D\) 、Laplacian \(L = D - A\)
- 理解鄰接矩陣的冪 \(A^k\) 的意義:路徑計數,並用程式驗證
- 用 Laplacian 特徵值判斷圖的連通性(algebraic connectivity)
- 概念式理解譜聚類,並對一個「兩個社群」的圖完成小型實作
- 完整分析一個社群網路範例
問題:怎麼讓電腦「看見」一個網路?#
社群、交通、神經元、超連結——世界上充滿了「點與連線」的結構(圖,graph)。 第 8.1 節的 PageRank 只用了一半的工具(轉移矩陣),這一節把整套矩陣武器補齊。
圖的矩陣表示#
一個 \(n\) 個節點的無向圖可以用 \(n \times n\) 的鄰接矩陣 \(A\) 表示:
無向圖的 \(A\) 是對稱矩陣;有向圖(如 8.1 的網頁連結)則不一定。 度矩陣 \(D\) 是對角矩陣,對角線上是各節點的連接數:
而本節的主角 Laplacian 矩陣定義為兩者之差:
一個刻畫圖的幾何性質、又是半正定的對稱矩陣——它的特徵值和特徵向量 幾乎編碼了整個圖的「形狀」。
鄰接矩陣的冪:路徑計數器#
矩陣乘法的定義恰好和圖論的直覺對上。 \(A^2\) 的第 \((i, j)\) 元:
一般化:
\(A^k\) 的 \((i, j)\) 元 \(=\) 從節點 \(i\) 到節點 \(j\) 、**長度恰好為 \(k\) ** 的路徑數。
用一個 4 節點的鏈狀圖驗證:
graph LR
1((1)) --- 2((2))
2 --- 3((3))
3 --- 4((4))import numpy as np
A = np.array([
[0, 1, 0, 0],
[1, 0, 1, 0],
[0, 1, 0, 1],
[0, 0, 1, 0],
])
A2 = A @ A
print("A^2 =\n", A2)
print("從 1 到 3 的 2 步路徑數 =", A2[0, 2]) # 1(1→2→3)
print("從 1 到 4 的 2 步路徑數 =", A2[0, 3]) # 0(不可能兩步走到)
A^2 =
[[1 0 1 0]
[0 2 0 1]
[1 0 2 0]
[0 1 0 1]]
從 1 到 3 的 2 步路徑數 = 1
從 1 到 4 的 2 步路徑數 = 0
再驗證 \(A^3\) ,並用暴力列舉所有路徑做交叉檢查:
from itertools import product
A3 = A @ A @ A
print("A^3[0, 3] =", A3[0, 3]) # 應為 1:1→2→3→4
# 暴力法:列舉所有長度 k 的節點序列,數有幾條是合法路徑
def count_walks(A, s, t, k):
n = len(A)
return sum(
all(A[p[i], p[i + 1]] for i in range(k))
for p in product(range(n), repeat=k + 1)
if p[0] == s and p[-1] == t
)
for k in range(1, 5):
Mk = np.linalg.matrix_power(A, k)
ok = all(Mk[s, t] == count_walks(A, s, t, k)
for s in range(4) for t in range(4))
print(f"k={k}: 矩陣冪與暴力列舉一致? {ok}")
A^3[0, 3] = 1
k=1: 矩陣冪與暴力列舉一致? True
k=2: 矩陣冪與暴力列舉一致? True
k=3: 矩陣冪與暴力列舉一致? True
k=4: 矩陣冪與暴力列舉一致? True
兩種完全不同的方法(代數的矩陣冪 vs 組合的暴力列舉)給出相同答案, 這就是「矩陣乘法 = 路徑組合」的具體證明。 順帶一提:若 \(A^k\) 的對角元 \((A^k)_{ii} > 0\) , 代表節點 \(i\) 有長度 \(k\) 的環回到自己——這是判斷三角形、 社群內部凝聚力的常用技巧。
Laplacian 與連通性#
對任意向量 \(\mathbf{x}\) ,Laplacian 有一個漂亮的二次型:
右邊是「相鄰節點的標籤差」的總和,永遠 \(\ge 0\) ——所以 \(L\) 半正定, 特徵值全部 \(\ge 0\) ,而且:
- **最小特徵值必為 \(0\) **(取 \(\mathbf{x}\) 為常數向量即可, \(\mathbf{x}^T L \mathbf{x} = 0\) )
- 特徵值 \(0\) 的重數 \(=\) 連通分量的個數!
這給了一個驚人的能力:對矩陣做特徵分解,就能知道圖斷成幾塊。 第二大特徵值 \(\lambda_2\) 稱為 algebraic connectivity (代數連通度): \(\lambda_2 > 0\) 代表圖連通,而且 \(\lambda_2\) 越大, 圖被切斷所需的「成本」越高、連接越緊密。
def laplacian(A):
return np.diag(A.sum(axis=1)) - A
# 情境一:兩個孤立的三角形(斷成兩塊)
A_disc = np.zeros((6, 6))
for i, j in [(0,1),(1,2),(2,0),(3,4),(4,5),(5,3)]:
A_disc[i, j] = A_disc[j, i] = 1
L = laplacian(A_disc)
eigvals = np.linalg.eigvalsh(L) # 對稱矩陣用 eigvalsh
print("特徵值 =", eigvals.round(3))
print("接近 0 的特徵值個數 =", int((eigvals < 1e-9).sum()))
特徵值 = [0. 0. 3. 3. 3. 3.]
接近 0 的特徵值個數 = 2
兩個 \(0\) ,正確指出兩個連通分量。若把兩個三角形之間加一條橋邊 (2, 3):
A_conn = A_disc.copy()
A_conn[2, 3] = A_conn[3, 2] = 1
eigvals = np.linalg.eigvalsh(laplacian(A_conn))
print("加橋後特徵值 =", eigvals.round(3))
# 加橋後特徵值 = [0. 0.586 3. 3.414 4. 5. ]
只剩一個 \(0\) (連通了),而且 \(\lambda_2 = 0.586\) —— 橋邊雖然只接了一條,卻讓整個圖從「必然斷裂」變成「勉強連通」, algebraic connectivity 從 \(0\) 跳升到 \(0.586\) 。
譜聚類:讓特徵向量替你分社群#
真實社群網路是連通的,但內部常有「社群內邊很密、社群之間只有稀疏橋」的結構。 譜聚類(spectral clustering)的想法:
- 算 Laplacian \(L\) 的特徵向量
- 第二小特徵值對應的特徵向量 \(\mathbf{v}_2\) (稱 Fiedler 向量) 把節點投影成一條數線——同一社群的節點數值相近
- 直接看 \(\mathbf{v}_2\) 的正負號(或對其做 k-means)即可分群
為什麼有效?回看二次型 \(\mathbf{x}^T L \mathbf{x} = \sum (x_i - x_j)^2\) : \(\mathbf{v}_2\) 是在「與常數向量垂直」的約束下讓這個和最小的向量—— 也就是盡量讓相鄰節點取值接近、同時不退化成常數的標籤。 被橋邊連接的兩個社群,自然分別聚在數線的兩側。
兩社群圖的完整實作#
造一個 10 節點的圖:節點 0–4 是一個緊密社群,5–9 是另一個,中間只有一條橋:
graph TD
subgraph 社群 A
n0((0)) --- n1((1)); n0 --- n2((2)); n1 --- n2
n1 --- n3((3)); n2 --- n4((4)); n3 --- n4
end
subgraph 社群 B
n5((5)) --- n6((6)); n5 --- n7((7)); n6 --- n7
n6 --- n8((8)); n7 --- n9((9)); n8 --- n9
end
n4 --- n5import matplotlib.pyplot as plt
edges = [(0,1),(0,2),(1,2),(1,3),(2,4),(3,4),
(5,6),(5,7),(6,7),(6,8),(7,9),(8,9),(4,5)]
n = 10
A = np.zeros((n, n))
for i, j in edges:
A[i, j] = A[j, i] = 1
L = laplacian(A)
eigvals, eigvecs = np.linalg.eigh(L) # 由小到大排序
print("特徵值 =", eigvals.round(3))
# 特徵值 = [0. 0.268 1.586 2. 2.382 3. 3. 3.382 3.414 4. ]
v2 = eigvecs[:, 1] # Fiedler 向量
print("Fiedler 向量 =", v2.round(2))
labels = (v2 > 0).astype(int)
print("分群結果 =", labels)
# 分群結果 = [1 1 1 1 1 0 0 0 0 0]
正負號完美把 0–4 和 5–9 分成兩群,演算法只看矩陣、完全不知道人為社群的存在。
# 視覺化:用圓形佈局畫圖,節點顏色 = 分群結果
theta = np.linspace(0, 2 * np.pi, n, endpoint=False)
pos = np.c_[np.cos(theta), np.sin(theta)]
plt.figure(figsize=(5, 5))
for i, j in edges:
plt.plot(*pos[[i, j]].T, color="gray", zorder=1)
scatter = plt.scatter(*pos.T, c=labels, cmap="coolwarm",
s=600, zorder=2, edgecolors="k")
for i in range(n):
plt.annotate(str(i), pos[i], ha="center", va="center", fontsize=11)
plt.title(f"Fiedler 向量分群(λ₂ = {eigvals[1]:.3f})")
plt.axis("off"); plt.tight_layout()
plt.savefig("spectral_clustering.png", dpi=120); plt.show()
社群網路範例:更真實的結構#
真實社群不是完美兩團。做一個 12 人網路: 三個小圈圈(室友團、社團團、同事團),各有少量跨團友誼:
rng = np.random.default_rng(42)
groups = [range(0, 4), range(4, 8), range(8, 12)]
A = np.zeros((12, 12))
def add_edge(i, j):
A[i, j] = A[j, i] = 1
# 圈內:每個群補成較密的隨機圖
for g in groups:
g = list(g)
for i in g:
for j in g:
if i < j and rng.random() < 0.85:
add_edge(i, j)
# 跨圈:稀疏的弱連結
for _ in range(4):
i = rng.choice(4); j = rng.integers(4, 12); add_edge(i, j)
for _ in range(3):
i = rng.integers(4, 8); j = rng.integers(8, 12); add_edge(i, j)
L = laplacian(A)
eigvals, eigvecs = np.linalg.eigh(L)
v2 = eigvecs[:, 1]
print("λ2 =", eigvals[1].round(3), "(不大 → 容易切成兩大塊)")
labels = (v2 > 0).astype(int)
print("兩大塊分群 =", labels)
# 想切三群,就多取一個特徵向量 v3,對 (v2, v3) 平面上的點做 k-means
v3 = eigvecs[:, 2]
pts = np.c_[v2, v3]
centers = pts[[0, 4, 8]].copy() # 粗略初始中心
for _ in range(20):
lab = np.argmin(
((pts[:, None, :] - centers[None, :, :]) ** 2).sum(axis=2), axis=1)
centers = np.array([pts[lab == c].mean(axis=0) for c in range(3)])
print("三群分群 =", lab)
(分群輸出會依隨機邊而異,但模式穩定:先切成兩大塊, 再用 Fiedler 平面上的 k-means 恢復三個原始社群。)
解讀:
- \(\lambda_2\) 小,代表整個網路「鬆散相連」,少剪幾條邊就斷
- Fiedler 向量 \(\mathbf{v}_2\) 找的是最脆弱的切面(二分)
- 要找 \(k\) 個社群,就取前 \(k\) 個非零特徵向量當座標,在 \(\mathbb{R}^k\) 裡做普通聚類 ——這正是譜聚類的完整流程:把圖的結構問題變成歐氏空間的幾何問題
小結與練習#
本節重點
- 圖的三件套: \(A\) (誰連誰)、 \(D\) (連幾個)、 \(L = D - A\) (圖的形狀)
- \(A^k\) 是路徑計數器:矩陣乘法就是「走一步」的組合
- \(L\) 半正定;特徵值 \(0\) 的個數 \(=\) 連通分量數; \(\lambda_2\) (algebraic connectivity)衡量「連得有多緊」
- 譜聚類:Fiedler 向量把圖切成最脆弱的兩半; 多個特徵向量 + k-means 可推廣到 \(k\) 個社群
練習
- 對 4 節點鏈狀圖計算 \(A^4\) ,用暴力列舉驗證 \((A^4)_{13} = 3\) ,並在圖上把這三條 4 步路徑畫出來。
- 把兩社群範例中的橋邊從 1 條增加到 3 條,觀察 \(\lambda_2\) 如何上升,並解釋「社群界線變模糊」在特徵值上的體現。
- 用
(A @ A > 0)找出範例社群網路中「朋友的朋友」對(原本不相連的),數量多嗎?這和六度分隔理論有什麼關係? - 挑戰題:證明 \(\mathbf{x}^T L \mathbf{x} = \sum_{(i,j)\in E}(x_i - x_j)^2\) ,並由此說明 \(L\) 半正定、特徵值 \(0\) 對應常數向量。