網(wǎng)絡(luò)的圖像建圖:像素、超像素與kNN)
把一張彩色圖片塞進圖神經(jīng)網(wǎng)絡(luò)最勸退的不是模型寫不出來而是數(shù)據(jù)準(zhǔn)備那一步一張 1024×1024 的圖如果老老實實按像素建圖節(jié)點數(shù)直接沖到一百萬量級隨手寫一個torch.zeros(N, N)就能把內(nèi)存吃干凈換個思路用超像素建圖又會在“邊權(quán)按什么算”“孤立節(jié)點怎么處理”“拉普拉斯矩陣歸一化除零”這些地方反復(fù)卡殼。把 img 構(gòu)建為 graph說到底就是把規(guī)則的二維像素陣列翻譯成一張帶節(jié)點特征和邊權(quán)的稀疏圖這個翻譯過程里的每個選擇都會在后面模型的精度和顯存占用上留下痕跡。這篇內(nèi)容面向的是已經(jīng)會用 Python 處理圖像、想進一步接觸圖表示與圖學(xué)習(xí)的開發(fā)者也適合做圖像分割、顯著性檢測、遙感或醫(yī)學(xué)影像分析、點云與區(qū)域建模方向的同學(xué)參考。下面我把從像素網(wǎng)格圖、超像素區(qū)域鄰接圖、特征空間 kNN 圖到圖割先驗圖的幾種建圖范式拆開講配上能直接跑的構(gòu)造代碼、內(nèi)存賬目和踩坑記錄盡量讓你看完就能在自己機器上把一張圖變成一張圖。1. 圖像為什么值得被翻譯成圖結(jié)構(gòu)1.1 像素陣列表達不了的那部分信息卷積網(wǎng)絡(luò)在規(guī)則網(wǎng)格上工作得很好因為圖像本身就是等間距采樣的二維信號局部相關(guān)性、平移不變性這些先驗是天然成立的。但一旦你的任務(wù)不再是“給整張圖打一個標(biāo)簽”而是“判斷任意兩塊區(qū)域之間是什么關(guān)系”規(guī)則網(wǎng)格就開始別扭了。比如你希望模型理解“左上角那塊天空和右下角那片水面是同一種材質(zhì)”或者“這兩個相鄰區(qū)域?qū)儆谕粋€器官但中間被一條血管切開”這類關(guān)系本質(zhì)上是區(qū)域與區(qū)域之間的而不是像素與像素之間的。這時候圖結(jié)構(gòu)就派上用場了。圖的節(jié)點可以是像素、可以是超像素、也可以是人工劃定的區(qū)域邊表示它們之間的相似性或空間鄰接邊權(quán)承載“有多像”或者“挨得多緊”。一旦轉(zhuǎn)成圖很多原本不好寫進卷積的操作就有了現(xiàn)成工具圖割把分割寫成最小割、譜聚類把聚類寫成拉普拉斯矩陣的特征分解、圖神經(jīng)網(wǎng)絡(luò)把特征傳播寫成鄰居聚合。同一張圖像你可以用三種完全不同的圖表示去做三件完全不同的事這是像素陣列本身給不了的靈活性。所以判斷“要不要建圖”我的經(jīng)驗是看兩件事節(jié)點之間是否存在非局部的強關(guān)聯(lián)以及你需要的輸出是不是節(jié)點級別或邊級別的。兩個條件有一個成立建圖就有價值。1.2 三類適合建圖的任務(wù)與一個反例按我實際接觸過的場景這三類任務(wù)建圖收益最明顯。第一類是區(qū)域級分割與合并。圖像先過超像素節(jié)點數(shù)從百萬級降到幾百到幾千再用區(qū)域鄰接圖做合并速度快、結(jié)果帶明顯的區(qū)域邊界后續(xù)人工修正也方便。第二類是跨區(qū)域關(guān)系推理比如同一場景里“桌面上的杯子和桌面”是什么關(guān)系把檢測框或超像素當(dāng)節(jié)點邊用空間重疊或語義相似度定義圖網(wǎng)絡(luò)比純卷積更容易建模這種配對關(guān)系。第三類是把組合優(yōu)化問題寫成圖圖割是最典型的例子源點接前景先驗、匯點接背景先驗最小割就是最優(yōu)分割這類問題的解有理論保證不是拍腦袋調(diào)參。反例也要說清楚如果你只是做常規(guī)的圖像分類或者目標(biāo)檢測且目標(biāo)尺度相對固定那建圖大概率是給自己找麻煩。像素級圖的節(jié)點數(shù)是百萬級消息傳遞一輪的開銷比同尺寸卷積大一個量級而卷積的歸納偏置又恰好適配規(guī)則網(wǎng)格得不償失。我見過有同學(xué)在分類任務(wù)上硬套圖網(wǎng)絡(luò)最后精度沒漲、訓(xùn)練時間翻了三倍回頭一查圖結(jié)構(gòu)本身沒提供任何卷積拿不到的信息。2. 四種建圖路線從像素網(wǎng)格到區(qū)域鄰接與 kNN2.1 像素級網(wǎng)格圖最直白也最貴的一種像素級網(wǎng)格圖是最容易理解的每個像素是一個節(jié)點相鄰像素之間連邊。四鄰域就是上下左右八鄰域再加上四條對角。節(jié)點數(shù) N H×W邊數(shù)可以精確算出來四鄰域無向邊數(shù) H(W?1) (H?1)W八鄰域無向邊數(shù) H(W?1) (H?1)W 2(H?1)(W?1)拿 1024×1024 舉例四鄰域約 209.5 萬條邊八鄰域約 418.8 萬條邊。這個規(guī)模用稀疏存儲完全扛得住但如果誰寫成稠密矩陣float32 下需要 (1.048×10?)2 × 4 字節(jié)換算下來是 4.4 TB機器直接趴下。所以像素級建圖的第一條鐵律從第一行代碼開始就用 COO/CSR 稀疏格式永遠不要出現(xiàn)np.zeros((N, N))。像素級圖的優(yōu)點是信息無損沒有超像素那種“先合并再處理”的精度損失適合需要像素級輸出的任務(wù)比如精細(xì)摳圖、邊緣細(xì)化。缺點是節(jié)點太多圖網(wǎng)絡(luò)的感受野要靠堆層數(shù)來擴而每層都要做一次稀疏矩陣乘實際訓(xùn)練速度往往不如同參數(shù)量的卷積網(wǎng)絡(luò)。我的用法是把它當(dāng)后處理階段的圖前面用卷積出粗結(jié)果后面用像素級圖割做邊界精修這樣既有卷積的效率又有圖優(yōu)化的邊界質(zhì)量。2.2 超像素圖與區(qū)域鄰接圖節(jié)點數(shù)直接砍兩個數(shù)量級超像素的思路是先按顏色和空間距離把像素聚成小塊每塊內(nèi)部顏色盡量一致、形狀盡量規(guī)整然后把每塊當(dāng)成一個節(jié)點。常用的做法是用 SLIC 類的算法在 Lab 顏色空間加二維坐標(biāo)組成的五維空間里做局部聚類得到一個標(biāo)簽圖。一張 1024×1024 的圖切成 400 到 2000 個超像素是常見區(qū)間節(jié)點數(shù)瞬間從百萬降到千級。有了超像素邊怎么連有兩種主流做法。一種是基于共享邊界兩個超像素如果有像素直接相鄰就連一條邊邊權(quán)可以用共享邊界的像素數(shù)量邊界越長越“親密”也可以在這個基礎(chǔ)上乘一個顏色相似度。另一種是基于特征 kNN在超像素特征空間里找最近的 k 個鄰居不管它們空間上挨不挨著。前者得到的是區(qū)域鄰接圖物理意義明確、連通性好后者能捕捉遠距離相似區(qū)域但容易引入噪聲邊。我一般先用共享邊界建 RAG再在其上疊加少量 kNN 邊做補充比例大概 9:1。這樣既保住了空間結(jié)構(gòu)的合理性又給了模型一點跨區(qū)域連接的通道。實測下來純 RAG 在區(qū)域合并類任務(wù)上更穩(wěn)純 kNN 在材質(zhì)分類這類任務(wù)上更靈活混著用是個折中方案。2.3 特征空間 kNN 圖與半徑圖相似度即連接kNN 圖的邏輯很直接把每個節(jié)點映射成一個特征向量在特征空間里找最近的 k 個點連邊。它完全不管空間位置只認(rèn)特征相似。半徑圖是它的變體把“最近 k 個”換成“距離小于 r 的所有點”好處是鄰居數(shù)量自適應(yīng)稠密區(qū)域鄰居多、稀疏區(qū)域鄰居少。這里有個非常容易忽略的點特征向量的量綱必須統(tǒng)一。如果你直接把 RGB 三通道和像素坐標(biāo) (x, y) 拼在一起RGB 的取值范圍是 0–255坐標(biāo)可能也是 0–1023看起來差不多但實際上顏色差異 20 和坐標(biāo)差異 20 的語義完全不同。我的做法是先把顏色歸一化到 [0,1]坐標(biāo)也歸一化到 [0,1]然后給坐標(biāo)乘一個權(quán)重系數(shù)常見取值 0.1 到 0.5。這個系數(shù)控制“空間鄰近”相對“顏色相似”的重要程度系數(shù)越大越傾向于連空間上近的點。k 的取值也有講究。太小圖容易碎成多個不連通分量消息傳不遠太大圖變成近似全連接稀疏性的優(yōu)勢就沒了。我的經(jīng)驗區(qū)間是 8 到 16除非節(jié)點數(shù)很少幾百個否則不要超過 20。2.4 圖割先驗圖把分割寫成最小割這一類的思路和前三種不太一樣它不是為了讓圖網(wǎng)絡(luò)跑而是為了讓一個組合優(yōu)化問題有解。典型構(gòu)造是這樣每個像素或超像素是節(jié)點相鄰節(jié)點之間連無向邊邊權(quán)是相似度越像權(quán)重越大再加兩個特殊節(jié)點源點代表“前景”匯點代表“背景”源點到每個節(jié)點的邊權(quán)表示這個節(jié)點屬于前景的先驗強度節(jié)點到匯點的邊權(quán)表示屬于背景的強度。然后求這個圖的最小割割斷的邊權(quán)和最小就意味著“切斷的相似邊最少 違背先驗最少”。數(shù)學(xué)上這就是一個二值標(biāo)號的最優(yōu)解可以用最大流算法在多項式時間內(nèi)求出來。這套構(gòu)造的好處是結(jié)果有全局最優(yōu)性不是局部貪心代價是它是二值的多類語義分割要反復(fù)調(diào)用或者擴展成多標(biāo)簽的最小割。實際用的時候先驗項才是決定成敗的地方。空間先驗離種子點越近越可能是前景好寫但粗糙顏色先驗用種子區(qū)域的顏色分布建模更準(zhǔn)但要小心過擬合到局部顏色。我通常兩者加權(quán)組合空間項權(quán)重壓在 0.2 到 0.3剩下給顏色項效果比單獨用任一項穩(wěn)定。建圖方式節(jié)點規(guī)模10242 圖邊數(shù)級典型用途主要代價像素八鄰域網(wǎng)格約 105 萬約 419 萬像素級精修、圖割節(jié)點多訓(xùn)練慢超像素 RAG400–2000約 2–5 倍節(jié)點數(shù)區(qū)域分割合并依賴超像素質(zhì)量特征 kNN 圖任意k×N/2跨區(qū)域相似性需要量綱統(tǒng)一圖割先驗圖與上面一致鄰接邊 2N二值分割最優(yōu)解只能二值需先驗3. 節(jié)點特征與邊權(quán)設(shè)計真正決定上限的那一層3.1 節(jié)點特征怎么拼才不浪費信息很多人建圖的功夫都花在“邊怎么連”節(jié)點特征反而隨手寫個 RGB 就完事這是本末倒置。節(jié)點特征決定了圖網(wǎng)絡(luò)每一層聚合的信息質(zhì)量拼得好等于白送模型一部分先驗。按信息類型分常用的節(jié)點特征有這幾組。顏色特征是基礎(chǔ)RGB 三個通道能用但 Lab 或 HSV 在感知一致性上更好做相似度計算時 Lab 幾乎是默認(rèn)選擇。空間坐標(biāo)一定要加因為它讓圖網(wǎng)絡(luò)能區(qū)分“顏色相同但位置很遠”的兩個節(jié)點我一般用歸一化的 (y, x)有的場景會追加一個歸一化的徑向距離。紋理特征在材質(zhì)分類、遙感場景里很有用用局部梯度直方圖或者幾個方向的 Gabor 響應(yīng)均值就夠不必上很重的描述子。幾何統(tǒng)計量在超像素圖上特別方便比如區(qū)域面積、周長、緊湊度面積比上周長的平方這幾個量對區(qū)分“細(xì)長條”和“團塊”非常有效而顏色特征完全看不出來。拼接時注意兩件事一是每組特征單獨做歸一化再拼不要拼完統(tǒng)一歸一化否則量綱大的那組會淹沒其他組二是節(jié)點特征維度不要無腦堆超過 32 維而節(jié)點數(shù)只有幾百的時候模型很容易過擬合到節(jié)點特征上泛化反而差。3.2 邊權(quán)函數(shù)的幾種寫法與選擇依據(jù)邊權(quán)本質(zhì)上是一個相似度度量取值越大表示兩個節(jié)點越應(yīng)該被視為一體。常見寫法有三種。第一種是高斯核w exp(?‖xi ? xj‖2 / (2σ2))。這里的 σ 是帶寬參數(shù)控制相似度的衰減速度。σ 太小絕大多數(shù)邊權(quán)接近 0圖變得像一堆孤島σ 太大所有邊權(quán)都接近 1圖變得像全連接區(qū)分度沒了。我習(xí)慣用一個中位數(shù)啟發(fā)式先算出所有相鄰節(jié)點對的距離平方取中位數(shù)再開方作為 σ。這樣能自適應(yīng)圖像的對比度亮圖和暗圖都不用改參數(shù)。想要更極端一點可以把中位數(shù)乘 0.5 或 2前者更強調(diào)差異、后者更強調(diào)連通。第二種是指數(shù)衰減或者倒數(shù)形式w 1 / (1 ‖xi ? xj‖)好處是沒有 σ 要調(diào)缺點是衰減太慢遠距離節(jié)點的邊權(quán)也不會掉到很小。它適合差異本身很大的特征空間比如深度圖。第三種是直接拿幾何量當(dāng)權(quán)重在超像素 RAG 里非常常見比如邊權(quán)就是兩個超像素共享邊界的像素數(shù)。這種權(quán)重的物理意義清晰但會偏向大區(qū)域因為大區(qū)域天然有更長的邊界。修正辦法是除以兩個區(qū)域周長的幾何平均得到一個歸一化的邊界親和度。還有個進階做法是讓邊權(quán)可學(xué)習(xí)把邊權(quán)寫成兩個端節(jié)點特征的某個小網(wǎng)絡(luò)的輸出訓(xùn)練時一起優(yōu)化。聽起來很美但要注意對稱性約束w(i,j) 必須等于 w(j,i)否則拉普拉斯矩陣就不是對稱的譜性質(zhì)的推導(dǎo)全部失效。通常的做法是讓小網(wǎng)絡(luò)輸入 |xi ? xj| 這種對稱量天然保證對稱。3.3 稀疏化與鄰接矩陣的內(nèi)存賬前面提過稠密鄰接矩陣會炸這里把賬算清楚。稀疏 COO 格式每個非零元要存行索引、列索引、值三部分行和列用 int32 各占 4 字節(jié)值用 float32 占 4 字節(jié)也就是每條邊 12 字節(jié)。八鄰域無向圖在 COO 里通常存成有向的對稱形式邊數(shù)翻倍10242 圖就是約 838 萬條記錄838 萬 × 12 字節(jié)約 100 MB可以接受。但如果換成“每個節(jié)點連 64 個鄰居”的 kNN 圖節(jié)點 100 萬邊數(shù) 3200 萬對稱化后 6400 萬條約 768 MB這就開始緊張了。再往上堆顯存直接不夠。所以大規(guī)模節(jié)點場景下稀疏化不是可選項而是必需項要么用超像素把節(jié)點數(shù)降下來要么用半徑圖并限制最大度數(shù)要么在 kNN 之后按權(quán)重裁剪掉最弱的一部分邊比如保留每節(jié)點前 8 個最相似鄰居。提示在 PyTorch 里做稀疏聚合時edge_index的方向約定是源節(jié)點在前、目標(biāo)節(jié)點在后。如果你給自己的圖做了對稱化一定要在拼接后檢查edge_index[0].max()和節(jié)點數(shù)是否對得上越界索引報的錯往往很隱蔽表現(xiàn)為一個莫名其妙的段錯誤而不是明確的異常。4. 從數(shù)組到圖對象把構(gòu)造過程寫成可復(fù)用的代碼4.1 網(wǎng)格圖必須向量化構(gòu)造我見過最多的寫法是雙循環(huán)遍歷每個像素、再遍歷四個方向判斷鄰居Python 里這么寫10242 的圖要跑好幾分鐘。實際上用數(shù)組切片一行就能生成所有邊速度差出三個數(shù)量級。import numpy as np def grid_edge_index(H, W, connectivity8): idx np.arange(H * W).reshape(H, W) src, dst [], [] # 水平方向 src.append(idx[:, :-1].ravel()); dst.append(idx[:, 1:].ravel()) # 垂直方向 src.append(idx[:-1, :].ravel()); dst.append(idx[1:, :].ravel()) if connectivity 8: src.append(idx[:-1, :-1].ravel()); dst.append(idx[1:, 1:].ravel()) src.append(idx[:-1, 1:].ravel()); dst.append(idx[1:, :-1].ravel()) src np.concatenate(src) dst np.concatenate(dst) # 無向圖存成對稱形式 edge_index np.stack([ np.concatenate([src, dst]), np.concatenate([dst, src]) ], axis0) return edge_index這段代碼的核心思想是“先列所有方向的偏移再統(tǒng)一展開”。idx[:, :-1]和idx[:, 1:]的對應(yīng)位置天然就是一對左右鄰居不需要任何循環(huán)。八鄰域里的兩條對角線同理只是切片起點不同。跑完之后你會得到形狀 (2, E) 的數(shù)組E 是雙向邊數(shù)正好是 PyG 那類框架期待的輸入格式。邊權(quán)再用一行算出來def gaussian_edge_weight(feat, edge_index, sigma_scale1.0): diff feat[edge_index[0]] - feat[edge_index[1]] d2 (diff ** 2).sum(axis1) sigma np.sqrt(np.median(d2)) * sigma_scale 1e-8 w np.exp(-d2 / (2 * sigma ** 2)) return w.astype(np.float32)注意sigma用中位數(shù)估算時如果圖里存在大量完全相同的相鄰像素大面積平坦區(qū)域中位數(shù)可能接近 0這時加一個1e-8兜底否則整張圖的邊權(quán)全變成 1等于沒建圖。4.2 超像素加區(qū)域鄰接的完整骨架超像素這條路的完整流程是轉(zhuǎn)顏色空間、聚類、統(tǒng)計節(jié)點特征、統(tǒng)計共享邊界、組裝稀疏矩陣。下面的骨架可以直接改成你需要的版本。import numpy as np from scipy.sparse import coo_matrix from skimage import color from skimage.segmentation import slic def build_rag(img_rgb, n_segments600, compactness10.0): lab color.rgb2lab(img_rgb) sp slic(lab, n_segmentsn_segments, compactnesscompactness, channel_axis-1, start_label0) K int(sp.max()) 1 labels sp.ravel() flat lab.reshape(-1, 3) H, W sp.shape ys, xs np.mgrid[0:H, 0:W] pos np.stack([ys.ravel() / H, xs.ravel() / W], axis1) cnt np.bincount(labels, minlengthK).astype(np.float32) 1e-8 mean_lab np.stack([ np.bincount(labels, weightsflat[:, c], minlengthK) for c in range(3) ], axis1) / cnt[:, None] mean_pos np.stack([ np.bincount(labels, weightspos[:, c], minlengthK) for c in range(2) ], axis1) / cnt[:, None] # 收集相鄰像素對映射到超像素對 pairs [] a, b sp[:, :-1].ravel(), sp[:, 1:].ravel() pairs.append(np.stack([a, b], axis1)) a, b sp[:-1, :].ravel(), sp[1:, :].ravel() pairs.append(np.stack([a, b], axis1)) pairs np.concatenate(pairs, axis0) pairs pairs[pairs[:, 0] ! pairs[:, 1]] pairs np.sort(pairs, axis1) uniq, counts np.unique(pairs, axis0, return_countsTrue) i, j uniq[:, 0], uniq[:, 1] # 邊界長度歸一化 顏色親和度 dil np.bincount(i, weightscounts, minlengthK) djl np.bincount(j, weightscounts, minlengthK) border counts / np.sqrt(dil[i] * djl[j] 1e-8) cdist np.linalg.norm(mean_lab[i] - mean_lab[j], axis1) sigma np.median(cdist) 1e-8 w border * np.exp(-cdist ** 2 / (2 * sigma ** 2)) A coo_matrix((w, (i, j)), shape(K, K)) A (A A.T).tocsr() node_feat np.concatenate([mean_lab, mean_pos], axis1).astype(np.float32) return node_feat, A幾個細(xì)節(jié)值得單獨說。slic的start_label0保證標(biāo)簽從 0 開始連續(xù)否則后面bincount的對齊會出錯。compactness控制超像素的規(guī)整程度值越大形狀越接近方塊、對顏色變化的敏感度越低常規(guī)圖像 10 左右紋理復(fù)雜的圖可以降到 5。顏色距離用 Lab 空間算比 RGB 更接近人眼感知這一點在做相似度時差別很明顯。邊界歸一化除以兩個區(qū)域邊界長度的幾何平均是為了壓掉大區(qū)域的天然優(yōu)勢不然大區(qū)域會把周圍所有小區(qū)域都吸過去。4.3 轉(zhuǎn)成圖學(xué)習(xí)框架能吃的格式有了節(jié)點特征和稀疏鄰接矩陣轉(zhuǎn)成主流圖框架的數(shù)據(jù)對象就是幾行的事關(guān)鍵在于別把對稱性和數(shù)據(jù)類型搞錯。import numpy as np, torch from torch_geometric.data import Data def to_pyg(node_feat, A): A A.tocoo() mask A.row ! A.col # 去掉自環(huán)框架會自己加 row, col, val A.row[mask], A.col[mask], A.data[mask] edge_index torch.as_tensor(np.stack([row, col]), dtypetorch.long) edge_attr torch.as_tensor(val, dtypetorch.float32) x torch.as_tensor(node_feat, dtypetorch.float32) return Data(xx, edge_indexedge_index, edge_attredge_attr)這里有個高頻錯誤如果你的A只是上三角或者只存了一個方向轉(zhuǎn)出來的圖就是有向的消息只能單向流動訓(xùn)練出來的結(jié)果會明顯偏。檢查辦法很簡單統(tǒng)計edge_index里 (i,j) 和 (j,i) 是否成對出現(xiàn)或者直接看A是否滿足(A - A.T).nnz 0。另外自環(huán)要不要留取決于你的模型GCN 類的實現(xiàn)一般內(nèi)部會加自環(huán)手動加會重復(fù)計算節(jié)點自身信息的權(quán)重就偏大了。5. 實測里最容易翻車的三個地方5.1 孤立節(jié)點讓消息傳遞靜默失效閾值設(shè)得太狠或者超像素分割出來的小碎塊沒有共享邊界就會產(chǎn)生度數(shù)為 0 的孤立節(jié)點。這類節(jié)點在消息傳遞時聚合不到任何鄰居輸出要么是零向量要么完全等于自身的線性變換等于這個節(jié)點從頭到尾沒參與過圖上的信息交互。更麻煩的是它不報錯你只會看到某些區(qū)域的預(yù)測結(jié)果莫名其妙地差。排查辦法是建完圖先算一遍度分布deg np.asarray(A.sum(axis1)).ravel()然后看(deg 0).sum()。如果非零要么把孤立節(jié)點合并到最近的鄰居要么給它補一條到 kNN 最近節(jié)點的邊要么干脆把這類節(jié)點從圖里刪掉再重建索引。我一般選第二種補邊的成本最低也不會破壞節(jié)點編號的連續(xù)性。同樣值得看一眼的是連通分量數(shù)。用scipy.sparse.csgraph.connected_components跑一下如果分量數(shù)遠大于 1說明圖的連通性有問題可能是邊權(quán)閾值太嚴(yán)也可能是特征空間太分散。對于需要全局傳播的模型比如圖上的注意力多個連通分量意味著信息永遠跨不過去這時候必須調(diào)整建圖參數(shù)而不是調(diào)模型。5.2 歸一化拉普拉斯的除零與數(shù)值范圍譜方法繞不開歸一化拉普拉斯L I ? D^(?1/2) A D^(?1/2)。推導(dǎo)上要求 D 可逆但實際圖里只要有一個孤立節(jié)點D 的對角元就是 0取倒數(shù)直接變 inf整個矩陣被污染。哪怕只有一個 inf后面所有特征分解、矩陣乘法全廢。固定寫法是先給度加一個小量再取倒數(shù)import numpy as np, scipy.sparse as sp def normalized_laplacian(A): d np.asarray(A.sum(axis1)).ravel() dinv 1.0 / np.sqrt(np.maximum(d, 1e-12)) D sp.diags(dinv) n A.shape[0] return sp.eye(n, formatcsr) - D A D加了1e-12之后孤立節(jié)點對應(yīng)的那一行會變成單位矩陣的一行特征值為 1不會污染其他部分。另外提醒一句歸一化拉普拉斯的特征值理論上落在 [0, 2] 區(qū)間如果你算出來有超過 2 的值八成是 A 不對稱或者有負(fù)權(quán)。負(fù)權(quán)在拉普拉斯框架下是不允許的邊權(quán)設(shè)計時一定保證非負(fù)。5.3 邊權(quán)飽和導(dǎo)致的“假全連接”高斯核的 σ 給大了所有邊權(quán)都擠在 0.9 到 1.0 之間圖在數(shù)值上稀疏、在效果上稠密。這時候圖網(wǎng)絡(luò)的表現(xiàn)會退化成對所有鄰居做平均跟直接對全圖做全局池化差不多節(jié)點之間的差異信息被抹平了。判斷辦法是看邊權(quán)的分布算一下分位數(shù)如果 5% 分位和 95% 分位相差不到 0.1說明飽和了。健康的分布應(yīng)該跨度大一些理想情況是 5% 分位在 0.1 以下、95% 分位在 0.8 以上。調(diào)整手段就是把 σ 調(diào)小或者干脆放棄高斯核改用w 1 - d / d_max這種線性歸一化分布會均勻很多。我在處理低對比度圖像比如某些醫(yī)學(xué)影像時更傾向線性歸一化因為這類圖的像素差異本來就小高斯核很容易把所有邊壓到高位?,F(xiàn)象可能原因快速驗證處理方式部分區(qū)域預(yù)測恒定存在孤立節(jié)點(deg 0).sum()補 kNN 邊或合并節(jié)點訓(xùn)練 loss 不下降圖不連通連通分量數(shù)降閾值、加 kNN 邊輸出異常值/inf度為零做歸一化檢查 D 對角元度加 1e-12 兜底效果等同全局平均邊權(quán)飽和看邊權(quán)分位數(shù)跨度調(diào)小 σ 或改線性核顯存爆掉稠密矩陣或邊數(shù)過多打印A.shape與A.nnz換稀疏格式、裁剪邊6. 建完圖之后的驗證與參數(shù)手感6.1 三分鐘體檢清單圖建完不要急著往模型里灌先跑一遍體檢能省掉后面大量排查時間。我固定會看這幾項。第一節(jié)點數(shù)和邊數(shù)量級對不對。超像素圖的邊數(shù)一般在節(jié)點數(shù)的 2 到 5 倍之間如果只有 1.1 倍說明鄰接關(guān)系太稀可能有大量節(jié)點沒連上如果超過 10 倍說明邊冗余嚴(yán)重稀疏化沒做夠。第二度分布是否合理。網(wǎng)格圖的度應(yīng)該幾乎是常數(shù)四鄰域全是 4八鄰域全是 8如果出現(xiàn)大量差異說明構(gòu)造時有索引錯位。第三邊權(quán)是否非負(fù)且不飽和方法如上。第四節(jié)點特征的每一維標(biāo)準(zhǔn)差是否接近某一維標(biāo)準(zhǔn)差特別大就要單獨歸一化。第五隨機抽 20 條邊把兩個端節(jié)點的坐標(biāo)打印出來人眼確認(rèn)它們確實相鄰或者確實相似。這一步很土但極其有效我靠它抓到過好幾次切片寫反的錯誤——行和列搞反了圖看起來還是連通的只是連的方向全錯。6.2 把圖畫出來看比看數(shù)字快得多統(tǒng)計量能告訴你“有沒有問題”但告訴不了你“問題長什么樣”。所以我建完圖一定會畫兩張圖。第一張是把邊畫在原圖上。用原圖當(dāng)背景把每條邊的兩個端點坐標(biāo)連成線段用半透明的細(xì)線畫上去。超像素圖會看起來像一張蛛網(wǎng)你能直接看出哪些區(qū)域連得密、哪些區(qū)域是孤島。第二張是鄰接矩陣的稀疏結(jié)構(gòu)圖用plt.spy(A)或者把非零位置畫成散點。網(wǎng)格圖的稀疏結(jié)構(gòu)應(yīng)該是非常規(guī)整的帶狀如果出現(xiàn)大塊空白或者雜亂散點說明索引映射出了偏差。這兩張圖加起來花不到五分鐘但能提前發(fā)現(xiàn)八成以上的構(gòu)造錯誤。需要注意的是邊多的時候直接畫會糊成一團我的做法是隨機采樣 500 到 1000 條邊來畫或者只畫權(quán)重最高的那一部分視覺上反而更清楚。6.3 參數(shù)手感的經(jīng)驗值最后說幾個我反復(fù)用過、基本不用大改的經(jīng)驗區(qū)間供你起步時參考具體還得按你的數(shù)據(jù)微調(diào)。超像素數(shù)量按圖像邊長的平方根量級來估1024×1024 取 600 到 1200512×512 取 300 到 600區(qū)域平均尺寸控制在 20 到 35 像素見方比較舒服。compactness常規(guī)圖像取 10紋理密集或邊緣復(fù)雜的圖降到 5 到 8。kNN 的 k 取 8 到 16節(jié)點數(shù)少于 500 時降到 5 到 8。高斯核的 σ 用相鄰距離中位數(shù)想要更強調(diào)差異乘 0.5強調(diào)連通乘 2。邊權(quán)裁剪一般保留每節(jié)點前 8 到 12 個最強鄰居既能壓住內(nèi)存又能保住結(jié)構(gòu)。我個人在實際操作中的體會是建圖這件事的收益曲線很陡參數(shù)大致對的時候效果提升非常明顯但繼續(xù)精調(diào)的邊際收益衰減得很快花在調(diào) σ 上的時間遠不如花在節(jié)點特征設(shè)計上。真正拉開差距的往往是“你有沒有把區(qū)域面積、緊湊度這類幾何量放進節(jié)點特征”而不是“σ 是 0.7 還是 0.8”。所以我的建議是先用一套能跑通的默認(rèn)參數(shù)把整條鏈路走完驗證端到端效果再回頭針對性地改最可疑的那一環(huán)別一上來就在建圖參數(shù)上死磕。