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