模型適配任意關(guān)系類型:異質(zhì)圖的語(yǔ)言模型化與少樣本實(shí)踐)
項(xiàng)目標(biāo)題里的“一個(gè)模型任意關(guān)系類型”第一次看到時(shí)我第一反應(yīng)是營(yíng)銷話術(shù)。做過(guò)異質(zhì)圖異構(gòu)圖的人都知道這行的痛點(diǎn)是 schema 定制換一個(gè)業(yè)務(wù)域節(jié)點(diǎn)類型、邊類型一變模型基本就得推倒重來(lái)元路徑要重新設(shè)計(jì)參數(shù)要重新訓(xùn)連評(píng)估腳本都得改。所以“任意關(guān)系類型”這五個(gè)字如果成立意味著圖模型從“一個(gè)數(shù)據(jù)集一個(gè)模型”走向“一個(gè)底座服務(wù)一堆圖”這是工程上質(zhì)變不是學(xué)術(shù)上刷點(diǎn)。我花了幾天時(shí)間把這套思路從頭捋了一遍也自己搭了個(gè)小規(guī)模的復(fù)現(xiàn)實(shí)驗(yàn)。這篇就把我理解到的核心設(shè)計(jì)、我踩過(guò)的坑、以及可以直接抄的實(shí)操流程全部攤開(kāi)講。適合兩類人看一類是在做推薦、風(fēng)控、內(nèi)容理解手里握著多張異構(gòu)數(shù)據(jù)表、每次上新業(yè)務(wù)都要重做一遍圖模型的工程同學(xué)另一類是剛接觸異質(zhì)圖想知道“圖”和“語(yǔ)言模型”這兩套東西到底怎么接上的同學(xué)。不需要你有很深的圖神經(jīng)網(wǎng)絡(luò)基礎(chǔ)我會(huì)把該補(bǔ)的常識(shí)都補(bǔ)上。1. 一個(gè)模型吃下任意關(guān)系類型難點(diǎn)到底卡在哪1.1 先把“關(guān)系類型”這個(gè)詞說(shuō)清楚很多同學(xué)一上來(lái)就被“異質(zhì)圖”三個(gè)字唬住其實(shí)拆開(kāi)看很簡(jiǎn)單。普通圖里所有節(jié)點(diǎn)是同一種東西所有邊也是同一種關(guān)系比如社交網(wǎng)絡(luò)里“人—關(guān)注—人”。異質(zhì)圖就是節(jié)點(diǎn)和邊都分了類節(jié)點(diǎn)可能是用戶、商品、店鋪、評(píng)論邊可能是購(gòu)買、瀏覽、收藏、屬于、評(píng)價(jià)。所謂關(guān)系類型嚴(yán)格講是一個(gè)三元組源節(jié)點(diǎn)類型、邊語(yǔ)義、目標(biāo)節(jié)點(diǎn)類型比如“用戶—購(gòu)買—商品”和“用戶—瀏覽—商品”雖然兩端類型一樣但是兩種完全不同的關(guān)系類型一個(gè)是交易意圖一個(gè)是興趣信號(hào)。這個(gè)區(qū)分極其重要因?yàn)闆Q定模型能不能“通用”的關(guān)鍵就在這兒。如果你的模型只認(rèn)邊的名字那“購(gòu)買”這個(gè)詞換個(gè)業(yè)務(wù)就叫“下單”模型又得重學(xué)如果模型認(rèn)的是“源類型—語(yǔ)義—目標(biāo)類型”這個(gè)結(jié)構(gòu)位置加上語(yǔ)義描述它就有機(jī)會(huì)把“用戶—購(gòu)買—商品”上學(xué)到的東西遷移到“讀者—訂閱—專欄”上。這就好比一個(gè)人學(xué)會(huì)了“主語(yǔ)—?jiǎng)幼鳌e語(yǔ)”的句法結(jié)構(gòu)換一門語(yǔ)言的詞匯他也能快速上手但如果他只是死記硬背了某幾句話那換個(gè)場(chǎng)景就廢了。所以后面所有設(shè)計(jì)本質(zhì)都在回答一個(gè)問(wèn)題怎么把“關(guān)系類型”表示成一個(gè)可以增刪、可以遷移、語(yǔ)義還能被模型理解的東西。1.2 傳統(tǒng)方案的三道坎每一道都很要命第一道坎是結(jié)構(gòu)綁定。主流異質(zhì)圖神經(jīng)網(wǎng)絡(luò)HGT、HAN 這一類在建模時(shí)關(guān)系類型通常對(duì)應(yīng)一組獨(dú)立的參數(shù)矩陣或者注意力頭。關(guān)系類型有多少種參數(shù)就有多少套。這意味著你新增一種邊類型模型結(jié)構(gòu)就得改改動(dòng)之后舊參數(shù)還不能直接復(fù)用得重訓(xùn)。業(yè)務(wù)上這是什么體驗(yàn)?zāi)闵蟼€(gè)新功能數(shù)據(jù)團(tuán)隊(duì)加了一張關(guān)聯(lián)表算法這邊排期兩周。這在快速迭代的場(chǎng)景里基本不可接受。第二道坎是元路徑靠人肉設(shè)計(jì)。“用戶—購(gòu)買—商品—屬于—店鋪—是同店—店鋪”這種路徑需要領(lǐng)域?qū)<沂止っ杜e。枚舉得好效果起飛枚舉漏了模型就是瞎子。更麻煩的是元路徑數(shù)量隨類型數(shù)組合爆炸你沒(méi)法窮舉只能憑經(jīng)驗(yàn)挑幾條這就把大量結(jié)構(gòu)信息浪費(fèi)了。第三道坎是少樣本場(chǎng)景直接躺平。新業(yè)務(wù)上線時(shí)標(biāo)簽往往只有幾十上百條傳統(tǒng) GNN 參數(shù)多、先驗(yàn)少幾十條標(biāo)簽喂下去基本就是過(guò)擬合。而語(yǔ)言模型這邊預(yù)訓(xùn)練階段已經(jīng)吃進(jìn)了海量文本天然帶著“購(gòu)買意味著交易”“評(píng)論通常針對(duì)商品”這類世界知識(shí)這部分先驗(yàn)如果能接進(jìn)圖任務(wù)里少樣本表現(xiàn)就有救。三道坎疊在一起就解釋了為什么“一個(gè)模型通吃”聽(tīng)起來(lái)這么誘人——它不是把效果提升幾個(gè)點(diǎn)的問(wèn)題而是把交付模式從“項(xiàng)目制”變成“產(chǎn)品制”的問(wèn)題。1.3 換思路別把圖當(dāng)矩陣把圖當(dāng)一段話關(guān)鍵的思路轉(zhuǎn)換在這里。傳統(tǒng) GNN 把圖看成鄰接矩陣加特征矩陣做的是矩陣乘法和消息聚合。而語(yǔ)言模型只認(rèn)一件事token 序列。那能不能把一張異質(zhì)圖的局部結(jié)構(gòu)翻譯成一段模型能讀的“話”完全可以而且翻譯起來(lái)很自然。一個(gè)節(jié)點(diǎn)的鄰域本質(zhì)上就是一堆事實(shí)陳述“這個(gè)用戶買了這個(gè)商品”“這個(gè)商品屬于這個(gè)店鋪”“這條評(píng)論針對(duì)這個(gè)商品”。這些本來(lái)就是自然語(yǔ)言能表達(dá)的東西只不過(guò)我們平時(shí)用鄰接矩陣存沒(méi)用文字寫(xiě)出來(lái)。一旦寫(xiě)成文字語(yǔ)言模型讀起來(lái)毫無(wú)障礙。我在實(shí)驗(yàn)里做過(guò)一個(gè)直觀對(duì)比。把同一個(gè)商品的鄰域用兩種方式喂給模型一種是one-hot 的鄰居 ID 列表模型完全不知道這些 ID 有什么含義另一種是帶類型標(biāo)注的文本描述模型立刻就能說(shuō)出“這個(gè)商品被大量用戶收藏但購(gòu)買少可能是個(gè)高關(guān)注低轉(zhuǎn)化品”。后者的 zero-shot 表現(xiàn)明顯更好因?yàn)樗{(diào)用了預(yù)訓(xùn)練階段積累的常識(shí)。這套“圖轉(zhuǎn)文本”的做法有個(gè)隱藏好處關(guān)系的可擴(kuò)展性變成了詞表的可擴(kuò)展性。語(yǔ)言模型本來(lái)就支持?jǐn)U詞表新增一種關(guān)系類型無(wú)非是新增一個(gè)特殊 token 的事模型主體一行代碼都不用改。這才是“任意關(guān)系類型”能成立的工程基礎(chǔ)。2. 整體設(shè)計(jì)拆解語(yǔ)言模型怎么“看懂”一張異質(zhì)圖2.1 令牌化的三層結(jié)構(gòu)節(jié)點(diǎn)、關(guān)系、結(jié)構(gòu)把圖變成序列不是簡(jiǎn)單地把鄰居名字拼起來(lái)就完事得設(shè)計(jì)好三層信息怎么編碼缺一層模型就殘廢。第一層是節(jié)點(diǎn)令牌。節(jié)點(diǎn)本身有 ID但光有 ID 沒(méi)用必須有內(nèi)容。所以節(jié)點(diǎn)令牌一般由“類型標(biāo)記 屬性文本”構(gòu)成比如node_type:item 無(wú)線耳機(jī)價(jià)格 299月銷 1.2 萬(wàn)退貨率 3%。這里的屬性文本是天然語(yǔ)言模型理解起來(lái)零成本。ID 只用來(lái)做去重和引用比如序列里第二次出現(xiàn)同一個(gè)節(jié)點(diǎn)就寫(xiě)成ref:id_88省 token。第二層是關(guān)系令牌。這是整套設(shè)計(jì)的靈魂。每種關(guān)系類型對(duì)應(yīng)一個(gè)可學(xué)習(xí)的特殊 token命名上強(qiáng)制遵循“源類型__語(yǔ)義__目標(biāo)類型”的規(guī)范比如rel:user__buy__item。這個(gè) token 在詞表里是一個(gè)獨(dú)立條目有自己的 embedding但它承載的語(yǔ)義不是從零學(xué)的——訓(xùn)練時(shí)我會(huì)同時(shí)給它配一句自然語(yǔ)言描述“用戶購(gòu)買了商品”讓文本描述和關(guān)系令牌在表示空間里對(duì)齊。第三層是結(jié)構(gòu)令牌用來(lái)表達(dá)拓?fù)湮恢?。包括跳?shù)標(biāo)記hop:1、hop:2、方向標(biāo)記正向/反向、以及度數(shù)分桶deg:high表示該節(jié)點(diǎn)連接數(shù)處于全網(wǎng)前 10%。別小看這幾個(gè)標(biāo)記異質(zhì)圖里“中心節(jié)點(diǎn)”和“邊緣節(jié)點(diǎn)”的鄰域信息密度差了一個(gè)數(shù)量級(jí)模型不知道這個(gè)背景很容易把采樣偏差當(dāng)成真實(shí)規(guī)律。三層拼起來(lái)一個(gè)節(jié)點(diǎn)的局部鄰域就長(zhǎng)這樣node:u_1024 類型用戶注冊(cè) 3 年近 30 天活躍 12 天 rel:user__buy__item hop:1 node:i_88 類型商品無(wú)線耳機(jī)299 元 rel:user__browse__item hop:1 node:i_203 類型商品機(jī)械鍵盤(pán)499 元 rel:user__follow__shop hop:1 node:s_7 類型店鋪數(shù)碼專營(yíng)這段“話”人讀得懂模型也讀得懂信息密度比鄰接矩陣高得多。我實(shí)測(cè)過(guò) token 消耗1-hop 采 15 個(gè)鄰居平均每個(gè)鄰居 25 到 40 個(gè) token整體控制在 600 token 以內(nèi)絕大多數(shù)開(kāi)源基座的上下文窗口都裝得下。2.2 關(guān)系令牌新增一種關(guān)系只需要加一個(gè)詞這是我覺(jué)得最漂亮的一點(diǎn)值得單獨(dú)展開(kāi)。傳統(tǒng)做法里新增關(guān)系類型等于新增一組參數(shù)矩陣參數(shù)量是hidden_dim × hidden_dim幾百萬(wàn)起步而且和舊參數(shù)不共享必須重訓(xùn)。關(guān)系令牌的做法是新增一個(gè) token參數(shù)量是hidden_dim7B 模型上大概 4096 個(gè)數(shù)四舍五入等于零。具體怎么加三步在 tokenizer 里注冊(cè)新的特殊 token調(diào)用resize_token_embeddings擴(kuò)展 embedding 矩陣訓(xùn)練時(shí)凍結(jié)原有詞表 embedding只訓(xùn)練新增的那幾行再配一個(gè) LoRA 適配器。這里有個(gè)細(xì)節(jié)必須講清楚不然容易翻車。新 token 的 embedding 是隨機(jī)初始化的如果直接開(kāi)訓(xùn)梯度信號(hào)很稀疏一個(gè)關(guān)系類型在序列里可能只出現(xiàn)幾次學(xué)得非常慢。我的做法是給它一個(gè)文本初始化先用關(guān)系類型的自然語(yǔ)言描述“用戶購(gòu)買商品”過(guò)一遍模型取最后一層隱狀態(tài)均值作為這個(gè)關(guān)系令牌的初始向量。這么一搞新關(guān)系類型從第 0 步就帶著語(yǔ)義收斂速度快很多我這邊實(shí)測(cè)能省掉大約三分之一的訓(xùn)練步數(shù)。還有一個(gè)好處是關(guān)系類型的可組合性。因?yàn)殛P(guān)系令牌是按“源類型__語(yǔ)義__目標(biāo)類型”命名的模型在訓(xùn)練中見(jiàn)過(guò)“用戶—購(gòu)買—商品”和“用戶—收藏—商品”當(dāng)遇到全新的“讀者—訂閱—專欄”時(shí)雖然三個(gè)詞都沒(méi)見(jiàn)過(guò)但“施事—?jiǎng)幼鳌苁隆钡慕Y(jié)構(gòu)位置是見(jiàn)過(guò)的模型能借助結(jié)構(gòu)位置做類比推理。這就是我說(shuō)的“學(xué)句法而不是背句子”。我做過(guò)一個(gè)對(duì)照讓模型在 zero-shot 條件下處理一個(gè)訓(xùn)練時(shí)完全沒(méi)出現(xiàn)過(guò)的關(guān)系類型。純隨機(jī)初始化的情況下準(zhǔn)確率基本是瞎猜水平做了文本初始化加結(jié)構(gòu)位置對(duì)齊之后準(zhǔn)確率能到有監(jiān)督版本的六成左右。這個(gè)數(shù)字不算驚艷但考慮到零樣本、零參數(shù)改動(dòng)工程價(jià)值很大。2.3 指令混合讓模型學(xué)“關(guān)系語(yǔ)義”而不是背數(shù)據(jù)集如果只用單一數(shù)據(jù)集做指令微調(diào)模型很容易學(xué)歪——它會(huì)把“用戶買商品”這個(gè)具體場(chǎng)景記死換個(gè)領(lǐng)域就完了。這是所有微調(diào)方案的通病業(yè)內(nèi)叫“指令過(guò)擬合”。解決辦法是指令混合從多個(gè)來(lái)源、多種 schema 的子圖里分別構(gòu)造指令混在一起訓(xùn)。具體來(lái)源可以分三類。第一類是目標(biāo)域自己的無(wú)標(biāo)注數(shù)據(jù)用自監(jiān)督任務(wù)構(gòu)造指令掩碼屬性還原、邊類型預(yù)測(cè)。第二類是結(jié)構(gòu)相似但領(lǐng)域不同的圖比如都是“用戶—物品”二部圖一個(gè)來(lái)自電商一個(gè)來(lái)自內(nèi)容社區(qū)讓模型見(jiàn)識(shí)同一結(jié)構(gòu)在不同語(yǔ)義下的表現(xiàn)。第三類是純文本的關(guān)系描述把關(guān)系類型寫(xiě)成句子做文本蘊(yùn)含或者分類任務(wù)保持模型的語(yǔ)言能力不被圖任務(wù)沖垮。混合比例上我試過(guò)幾組比較舒服的是 5:3:2目標(biāo)域自監(jiān)督占一半跨域結(jié)構(gòu)占三成純文本占兩成。純文本那一份千萬(wàn)別省我第一版為了“專注圖任務(wù)”把它去掉了結(jié)果訓(xùn)完模型的語(yǔ)言能力明顯退化生成的解釋開(kāi)始胡說(shuō)八道下游 zero-shot 也跟著掉。這個(gè)坑我印象很深。指令的模板也要多樣化不能所有任務(wù)都套一個(gè)殼。節(jié)點(diǎn)分類用“該節(jié)點(diǎn)的類別是”鏈接預(yù)測(cè)用“這兩個(gè)節(jié)點(diǎn)之間是否存在某種關(guān)系”鄰域描述用“請(qǐng)概括該節(jié)點(diǎn)的一階鄰域特征”。模板多樣性帶來(lái)的收益在跨域遷移時(shí)特別明顯我觀察到的提升大概有三到五個(gè)點(diǎn)。3. 核心細(xì)節(jié)解析幾個(gè)決定成敗的實(shí)現(xiàn)要點(diǎn)3.1 鄰域采樣與序列預(yù)算別讓 token 爆掉圖節(jié)點(diǎn)的度數(shù)分布是長(zhǎng)尾的熱門商品可能有幾萬(wàn)個(gè)鄰居你不采樣直接塞序列長(zhǎng)度爆炸。所以采樣策略和預(yù)算分配必須先定死。我的配置是這樣的1-hop 最多采 20 個(gè)鄰居2-hop 最多采 30 個(gè)每個(gè) 1-hop 鄰居下掛 1 到 2 個(gè)總 token 預(yù)算 768超了就截?cái)?。采樣的方式不是純隨機(jī)而是分層加權(quán)優(yōu)先保留關(guān)系類型稀缺的鄰居比如某個(gè)用戶只有一條“投訴”邊這條邊信息量遠(yuǎn)大于他的一百條“瀏覽”邊同類型內(nèi)部再按度數(shù)歸一化隨機(jī)采。這么做的道理很直白信息量取決于稀有度和區(qū)分度不取決于數(shù)量。預(yù)算分配上給個(gè)具體算法方便你直接抄先算出每個(gè)關(guān)系類型在序列里的平均 token 消耗再按各類型的重要度分配配額。比如“購(gòu)買”類關(guān)系重要度高給 30% 預(yù)算“瀏覽”類重要度低給 10%。如果某個(gè)類型超配額了就按時(shí)間衰減或權(quán)重排序截?cái)?。這套邏輯寫(xiě)成代碼不到二十行但能顯著降低無(wú)意義的長(zhǎng)尾噪聲。提示序列長(zhǎng)度不是越長(zhǎng)越好。我在 768 和 2048 兩個(gè)預(yù)算下做過(guò)對(duì)比2048 的版本在小樣本任務(wù)上反而略差。原因不復(fù)雜序列越長(zhǎng)關(guān)鍵信息被稀釋得越厲害模型抓不住重點(diǎn)。除非你的任務(wù)真的依賴遠(yuǎn)距離結(jié)構(gòu)否則別盲目堆長(zhǎng)度。還有個(gè)容易忽略的點(diǎn)負(fù)采樣。鏈接預(yù)測(cè)任務(wù)里負(fù)樣本不能純隨機(jī)采否則模型學(xué)會(huì)的是“這兩個(gè)節(jié)點(diǎn)類型不匹配”這種低級(jí)規(guī)律一測(cè)就露餡。正確做法是采類型匹配但實(shí)際無(wú)邊連接的節(jié)點(diǎn)對(duì)難度更高學(xué)出來(lái)的表示才有用。3.2 關(guān)系令牌的初始化與訓(xùn)練策略前面提了文本初始化這里把完整的訓(xùn)練策略講透因?yàn)檫@塊最容易出問(wèn)題。參數(shù)分組是這樣的新增的關(guān)系令牌 embedding 用較大學(xué)習(xí)率我一般設(shè) 1e-3 到 3e-3因?yàn)樗獜牧銓W(xué)LoRA 適配器用常規(guī)學(xué)習(xí)率1e-4 到 2e-4主干權(quán)重全部?jī)鼋Y(jié)。為什么主干要凍兩個(gè)原因。一是算力全參微調(diào) 7B 模型在單機(jī)上基本別想LoRA 能把可訓(xùn)練參數(shù)壓到千分之一量級(jí)二是防止災(zāi)難性遺忘小樣本場(chǎng)景下全參微調(diào)幾乎必然把預(yù)訓(xùn)練知識(shí)沖掉。順便算一筆賬讓你對(duì)參數(shù)量有個(gè)直觀感受。假設(shè)詞表新增 200 個(gè)關(guān)系令牌隱藏維度 4096那新增 embedding 參數(shù)量是 200 × 4096 ≈ 82 萬(wàn)。LoRA 這邊rank 取 8作用在注意力層的 q、k、v、o 四個(gè)投影矩陣上每個(gè)矩陣的低秩分解參數(shù)量是 2 × 8 × 4096 ≈ 6.6 萬(wàn)四個(gè)就是 26 萬(wàn)假設(shè) 32 層總共約 840 萬(wàn)。加起來(lái)不到一千萬(wàn)相對(duì) 70 億的底座是 1.4% 左右。這就是“參數(shù)高效”的實(shí)際含義。訓(xùn)練輪數(shù)不宜多小樣本場(chǎng)景下 3 到 5 輪就夠再多必然過(guò)擬合。我是按驗(yàn)證集損失早停patience 設(shè) 2。另外有個(gè)小技巧訓(xùn)練初期先只訓(xùn)關(guān)系令牌 embedding把 LoRA 關(guān)掉跑個(gè)幾百步等關(guān)系令牌的表示初步成型再打開(kāi) LoRA 聯(lián)合訓(xùn)練。這相當(dāng)于先讓模型學(xué)會(huì)“新詞的意思”再讓它適應(yīng)“新任務(wù)的問(wèn)法”分階段比一鍋燉效果穩(wěn)。3.3 自監(jiān)督目標(biāo)怎么設(shè)計(jì)才有信息量沒(méi)有標(biāo)簽的時(shí)候自監(jiān)督任務(wù)是唯一的監(jiān)督信號(hào)設(shè)計(jì)得好壞直接決定模型學(xué)沒(méi)學(xué)到東西。我用了三個(gè)目標(biāo)各有分工。第一個(gè)是掩碼屬性還原。隨機(jī)遮蔽節(jié)點(diǎn)屬性文本中的一部分比如價(jià)格、類目讓模型根據(jù)鄰域結(jié)構(gòu)預(yù)測(cè)被遮的內(nèi)容。這個(gè)任務(wù)逼著模型把結(jié)構(gòu)信息和屬性信息對(duì)齊比如它要知道“連接了很多高端商品的用戶其購(gòu)買力標(biāo)簽大概率不低”。第二個(gè)是關(guān)系類型預(yù)測(cè)。給定兩個(gè)節(jié)點(diǎn)和它們的上下文讓模型判斷它們之間是什么關(guān)系類型。這個(gè)任務(wù)直接訓(xùn)練關(guān)系令牌的區(qū)分度是“任意關(guān)系類型”能力的核心來(lái)源。這里負(fù)樣本構(gòu)造很關(guān)鍵要從候選關(guān)系類型集合里采混淆項(xiàng)比如“瀏覽”和“收藏”容易混“購(gòu)買”和“退貨”容易混專挑這些難例。第三個(gè)是鄰域?qū)Ρ取M粋€(gè)節(jié)點(diǎn)的兩個(gè)不同采樣鄰域作為正對(duì)不同節(jié)點(diǎn)的鄰域作為負(fù)對(duì)做對(duì)比學(xué)習(xí)。這個(gè)目標(biāo)的作用是讓表示對(duì)采樣噪聲魯棒——畢竟每次采樣出來(lái)的鄰居不一樣模型不能因?yàn)閾Q了幾個(gè)鄰居就給出完全不同的判斷。三個(gè)目標(biāo)的損失權(quán)重我用的是 1 : 1 : 0.5。對(duì)比學(xué)習(xí)權(quán)重調(diào)低是因?yàn)樗菀字鲗?dǎo)梯度把另外兩個(gè)目標(biāo)壓住。這個(gè)比例不是理論最優(yōu)是我在驗(yàn)證集上試出來(lái)的你換數(shù)據(jù)集可能得重新調(diào)。4. 從零跑通一次小樣本實(shí)驗(yàn)完整實(shí)操流程4.1 數(shù)據(jù)與 schema 準(zhǔn)備假設(shè)手上有一份電商場(chǎng)景的異質(zhì)圖節(jié)點(diǎn)類型有用戶、商品、店鋪、評(píng)論關(guān)系類型有購(gòu)買、瀏覽、收藏、屬于、評(píng)價(jià)。第一步不是寫(xiě)模型是寫(xiě)一份schema 描述文件這東西后面所有環(huán)節(jié)都要用。# schema.yaml node_types: user: description: 平臺(tái)注冊(cè)用戶 attributes: [reg_days, active_days_30d, city_level] item: description: 在售商品 attributes: [category, price, monthly_sales, return_rate] shop: description: 商品所屬店鋪 attributes: [shop_level, main_category] review: description: 用戶對(duì)商品的評(píng)價(jià) attributes: [rating, text_len] relation_types: - name: user__buy__item src: user dst: item description: 用戶購(gòu)買了商品 - name: user__browse__item src: user dst: item description: 用戶瀏覽過(guò)商品詳情頁(yè) - name: item__belong_to__shop src: item dst: shop description: 商品歸屬于店鋪 - name: user__review__item src: user dst: item description: 用戶對(duì)商品發(fā)表了評(píng)價(jià)這份文件有三個(gè)作用一是自動(dòng)生成關(guān)系令牌的名字保證命名規(guī)范統(tǒng)一不會(huì)出現(xiàn)“buy”和“purchase”混用二是提供自然語(yǔ)言描述用于關(guān)系令牌的文本初始化三是作為序列化模板的輸入程序化生成指令文本。我強(qiáng)烈建議把 schema 當(dāng)成一等公民來(lái)管理用版本控制管起來(lái)。我踩過(guò)的坑就是早期隨手在代碼里寫(xiě)死關(guān)系名改一次業(yè)務(wù)就要全局搜索替換痛不欲生。4.2 序列化與詞表擴(kuò)展序列化我寫(xiě)成了一個(gè)獨(dú)立模塊輸入是節(jié)點(diǎn) ID 和 schema輸出是文本序列。核心邏輯是 BFS 采樣加模板填充。def serialize_neighborhood(node_id, graph, schema, budget768): 把節(jié)點(diǎn)的局部鄰域序列化成文本控制在 token 預(yù)算內(nèi)。 lines [] # 按關(guān)系類型分組稀有類型優(yōu)先保留 groups graph.group_neighbors(node_id) ordered sorted(groups.items(), keylambda kv: kv[1].rarity, reverseTrue) for rel_name, neighbors in ordered: quota allocate_quota(rel_name, schema) # 按重要度分配 token 配額 used 0 for nbr in neighbors.sample(weightedTrue): line render_line( rel_tokfrel:{rel_name}, hophop:1, node_descgraph.describe(nbr, schema), ) used count_tokens(line) if used quota or count_tokens(lines) used budget: break lines.append(line) return \n.join(lines)渲染出來(lái)的單行大概長(zhǎng)這樣和前面舉的例子一致。這里有個(gè)細(xì)節(jié)值得說(shuō)graph.describe里屬性的順序是固定的必須按 schema 里聲明的順序輸出。為什么不隨機(jī)打亂因?yàn)樵谡Z(yǔ)言模型眼里位置是有信息的屬性順序穩(wěn)定反而幫助它形成“第幾個(gè)位置對(duì)應(yīng)什么字段”的模式實(shí)測(cè)比隨機(jī)打亂收斂快一點(diǎn)。詞表擴(kuò)展和模型加載這部分直接給可運(yùn)行的骨架from transformers import AutoTokenizer, AutoModelForCausalLM from peft import LoraConfig, get_peft_model BASE your-open-base-model # 7B 量級(jí)開(kāi)源基座 tokenizer AutoTokenizer.from_pretrained(BASE) model AutoModelForCausalLM.from_pretrained(BASE, torch_dtypebfloat16) # 1. 從 schema 收集所有關(guān)系類型生成特殊 token rel_tokens [frel:{r[name]} for r in schema[relation_types]] node_tokens [fnode_type:{n} for n in schema[node_types]] struct_tokens [hop:1, hop:2, deg:low, deg:mid, deg:high] special rel_tokens node_tokens struct_tokens tokenizer.add_special_tokens({additional_special_tokens: special}) model.resize_token_embeddings(len(tokenizer)) # 2. 用自然語(yǔ)言描述初始化關(guān)系令牌 init_relation_embeddings(model, tokenizer, schema) # 內(nèi)部取描述文本隱狀態(tài)均值 # 3. 凍結(jié)主干只訓(xùn) LoRA 和新增 token for name, p in model.named_parameters(): p.requires_grad False for tid in tokenizer.convert_tokens_to_ids(special): model.get_input_embeddings().weight.requires_grad True # 配合 mask 只訓(xùn)新增行 lora_cfg LoraConfig( r8, lora_alpha16, lora_dropout0.05, target_modules[q_proj, k_proj, v_proj, o_proj], task_typeCAUSAL_LM, ) model get_peft_model(model, lora_cfg)代碼里“只訓(xùn)新增行”那一步需要額外寫(xiě)一個(gè)梯度 mask不然整個(gè) embedding 矩陣都會(huì)被更新那就等于把預(yù)訓(xùn)練詞表也帶偏了。這個(gè)細(xì)節(jié)很多教程會(huì)漏掉但不做的話效果會(huì)明顯下降尤其是小樣本場(chǎng)景詞表一動(dòng)模型的語(yǔ)言先驗(yàn)就開(kāi)始流失。優(yōu)化器分組也要配套寫(xiě)optimizer torch.optim.AdamW([ {params: new_token_embedding_params, lr: 3e-3}, {params: lora_params, lr: 1.5e-4}, ], weight_decay0.01)學(xué)習(xí)率差二十倍這是有意為之。新令牌要快速建立語(yǔ)義LoRA 只需要輕微調(diào)整兩邊用一個(gè)學(xué)習(xí)率必然有一邊不合適。4.3 參數(shù)高效微調(diào)配置與訓(xùn)練循環(huán)訓(xùn)練配置直接給表格方便對(duì)照。配置項(xiàng)取值說(shuō)明基座規(guī)模7B 量級(jí)再大單卡扛不住再小語(yǔ)義先驗(yàn)不足LoRA rank8試過(guò) 4 和 168 在小樣本上最均衡LoRA alpha16常規(guī)取 rank 的 2 倍LoRA dropout0.05小樣本必須加正則關(guān)系令牌學(xué)習(xí)率3e-3從零學(xué)需要大LoRA 學(xué)習(xí)率1.5e-4輕微調(diào)整即可batch size8配合梯度累積 4等效 32訓(xùn)練輪數(shù)3 到 5早停 patience2序列長(zhǎng)度上限768實(shí)測(cè)優(yōu)于 2048精度bfloat16顯存友好數(shù)值范圍大訓(xùn)練循環(huán)本身沒(méi)什么花活但有兩個(gè)實(shí)操細(xì)節(jié)我要強(qiáng)調(diào)。第一指令要打亂混采不能讓模型連續(xù)看幾十條同類型的指令否則它會(huì)摸到“第幾批次該做什么任務(wù)”的捷徑。第二損失只算答案部分指令和圖描述部分不參與 loss 計(jì)算用 label mask 屏蔽掉。這一點(diǎn)如果做錯(cuò)模型會(huì)花大量精力去學(xué)“怎么復(fù)述輸入”而不是“怎么回答問(wèn)題”這是我早期踩過(guò)的最蠢的坑訓(xùn)了兩天才發(fā)現(xiàn) loss 曲線好看但下游一塌糊涂。評(píng)估環(huán)節(jié)要分三組做缺一不可同域小樣本k1、3、5看節(jié)點(diǎn)分類的 Macro-F1。這是基本盤(pán)。跨域零樣本在訓(xùn)練時(shí)完全沒(méi)出現(xiàn)過(guò)的關(guān)系類型上直接推理看掉多少。這是檢驗(yàn)“任意關(guān)系類型”成色的關(guān)鍵指標(biāo)。消融對(duì)照去掉文本初始化、去掉指令混合、去掉結(jié)構(gòu)令牌各跑一遍確認(rèn)每個(gè)模塊真的在貢獻(xiàn)。我的實(shí)驗(yàn)結(jié)果大致是k5 時(shí)同域 Macro-F1 與調(diào)優(yōu)過(guò)的傳統(tǒng) GNN 基本持平甚至略高k1 時(shí)優(yōu)勢(shì)明顯能高出五到八個(gè)點(diǎn)跨域零樣本大約是同域有監(jiān)督的六成左右。這個(gè)表現(xiàn)我認(rèn)為是“可用”而非“驚艷”寫(xiě)論文可能不夠但做工程底座是夠的。5. 常見(jiàn)問(wèn)題與排查實(shí)錄5.1 效果反而不如傳統(tǒng) GNN八成是這幾個(gè)原因第一關(guān)系令牌根本沒(méi)學(xué)動(dòng)。癥狀是訓(xùn)練 loss 下降很慢或者下游表現(xiàn)和隨機(jī)初始化的模型沒(méi)區(qū)別。排查方法很簡(jiǎn)單把關(guān)系令牌的 embedding 取出來(lái)算一下不同關(guān)系類型之間的余弦相似度。如果所有關(guān)系令牌的向量幾乎一模一樣說(shuō)明模型壓根沒(méi)區(qū)分開(kāi)它們。原因通常是訓(xùn)練樣本里該關(guān)系類型出現(xiàn)次數(shù)太少少于 50 次或者學(xué)習(xí)率給低了。第二序列化把結(jié)構(gòu)信息洗掉了。我見(jiàn)過(guò)一種寫(xiě)法只把鄰居的屬性文本拼起來(lái)關(guān)系令牌和跳數(shù)標(biāo)記全省了。這樣模型看到的就是一堆無(wú)序的文本片段它根本不知道誰(shuí)連誰(shuí)退化成文本分類器。異質(zhì)圖的價(jià)值全在結(jié)構(gòu)上序列化時(shí)寧可少放幾個(gè)鄰居也要保留關(guān)系標(biāo)記和跳數(shù)。第三評(píng)估泄漏。這個(gè)坑最隱蔽。如果你的訓(xùn)練集和測(cè)試集共用同一套關(guān)系類型那模型可能只是記住了“這個(gè)關(guān)系類型對(duì)應(yīng)的答案分布”跨域一測(cè)就現(xiàn)原形。做通用性驗(yàn)證時(shí)測(cè)試集必須包含訓(xùn)練時(shí)完全沒(méi)出現(xiàn)的關(guān)系類型而且這個(gè)隔離要在數(shù)據(jù)劃分階段就做好不能訓(xùn)完再切。第四文本屬性太長(zhǎng)關(guān)鍵字段被淹沒(méi)。商品描述動(dòng)輒幾百字全塞進(jìn)去價(jià)格、類目這些關(guān)鍵信號(hào)就被稀釋了。我的處理是屬性分級(jí)核心字段類目、價(jià)格、評(píng)分必留長(zhǎng)文本字段截?cái)嗟?50 字以內(nèi)并且只保留前面部分因?yàn)闃?biāo)題類文本的信息量集中在前半段。5.2 問(wèn)題速查表現(xiàn)象大概率原因處理方式關(guān)系令牌向量高度相似訓(xùn)練樣本不足或?qū)W習(xí)率過(guò)低提高新令牌學(xué)習(xí)率至 3e-3補(bǔ)充該關(guān)系的自監(jiān)督樣本零樣本跨域表現(xiàn)接近隨機(jī)指令過(guò)擬合單一 schema提高指令混合比例跨域樣本占比提到三成訓(xùn)練 loss 正常但下游極差label mask 沒(méi)做對(duì)模型在學(xué)復(fù)述檢查 loss 是否只算答案段序列長(zhǎng)度頻繁觸頂被截?cái)噜従硬蓸舆^(guò)密或?qū)傩晕谋具^(guò)長(zhǎng)降低 1-hop 采樣數(shù)長(zhǎng)文本字段截?cái)嘈颖?k1 時(shí)嚴(yán)重過(guò)擬合LoRA rank 過(guò)大或 dropout 不足rank 降到 4dropout 提到 0.1減少訓(xùn)練輪數(shù)模型輸出格式不穩(wěn)定答案混在解釋里指令模板不統(tǒng)一固定輸出格式訓(xùn)練時(shí)對(duì)格式做約束顯存溢出序列長(zhǎng)度或 batch 設(shè)置過(guò)大開(kāi)梯度檢查點(diǎn)序列降到 512batch 降到 4還有兩個(gè)我沒(méi)寫(xiě)進(jìn)表格但值得單獨(dú)提的經(jīng)驗(yàn)。一個(gè)是新關(guān)系類型的冷啟動(dòng)如果你的業(yè)務(wù)要上一批全新關(guān)系別指望直接 zero-shot先給每種新關(guān)系構(gòu)造 200 條左右的自監(jiān)督指令不需要人工標(biāo)注用結(jié)構(gòu)自動(dòng)生成訓(xùn)個(gè)幾百步效果提升比調(diào)任何超參都明顯。另一個(gè)是別迷信大基座我對(duì)比過(guò) 7B 和更大規(guī)模小樣本圖任務(wù)上 7B 完全夠用更大反而因?yàn)閮鼋Y(jié)參數(shù)太多、LoRA 容量相對(duì)不足而吃虧。算力省下來(lái)去做數(shù)據(jù)構(gòu)造回報(bào)率更高。6. 這套思路適合誰(shuí)用邊界在哪6.1 什么場(chǎng)景值得上什么場(chǎng)景別折騰先說(shuō)什么場(chǎng)景值得。第一類是多業(yè)務(wù)線共存、schema 頻繁變動(dòng)的團(tuán)隊(duì)。你有五條業(yè)務(wù)線每條線的異質(zhì)圖 schema 都不一樣以前要維護(hù)五個(gè)模型現(xiàn)在一個(gè)底座加五套關(guān)系令牌運(yùn)維成本差一個(gè)量級(jí)。第二類是冷啟動(dòng)嚴(yán)重的新業(yè)務(wù)標(biāo)簽幾十條傳統(tǒng)模型根本訓(xùn)不動(dòng)語(yǔ)言模型的先驗(yàn)這時(shí)候就是救命稻草。第三類是需要可解釋性的場(chǎng)景因?yàn)檎麄€(gè)輸入是文本模型給出的判斷可以順著鄰域描述往回追這在風(fēng)控、內(nèi)容審核這類需要給理由的場(chǎng)景里價(jià)值很高。再說(shuō)什么場(chǎng)景別折騰。單域、數(shù)據(jù)量大、schema 穩(wěn)定的場(chǎng)景傳統(tǒng) GNN 依然是性價(jià)比之王。你手里有幾百萬(wàn)標(biāo)注樣本、關(guān)系類型就那五種、一年也不變那老老實(shí)實(shí)用 GNN訓(xùn)練快、推理便宜、效果穩(wěn)。這套方案的優(yōu)勢(shì)全在“泛化”和“少樣本”上如果你的問(wèn)題根本不缺這兩樣那就是殺雞用牛刀還得為牛刀付 GPU 賬單。還有一類要謹(jǐn)慎對(duì)推理延遲極其敏感的場(chǎng)景。語(yǔ)言模型推理比 GNN 慢一到兩個(gè)數(shù)量級(jí)如果你要在毫秒級(jí)響應(yīng)里做實(shí)時(shí)圖推理這套方案得配合蒸餾或者緩存策略不能直接上。我一般的做法是離線把節(jié)點(diǎn)表示算好緩存起來(lái)線上只做輕量查詢把重活放在異步鏈路里。6.2 后續(xù)可以怎么擴(kuò)展我自己在往兩個(gè)方向試。一個(gè)是關(guān)系類型的層次化現(xiàn)在的關(guān)系令牌是扁平的“購(gòu)買”和“復(fù)購(gòu)”被當(dāng)成兩個(gè)獨(dú)立類型其實(shí)它們有語(yǔ)義包含關(guān)系。如果能把關(guān)系類型組織成樹(shù)或者圖父子關(guān)系共享參數(shù)那新類型的冷啟動(dòng)會(huì)更輕松。另一個(gè)是結(jié)構(gòu)信息的顯式注入。目前結(jié)構(gòu)只靠跳數(shù)和度數(shù)分桶這種粗粒度標(biāo)記信息損失不小。我在試把一些輕量的結(jié)構(gòu)編碼比如節(jié)點(diǎn)在元路徑上的位置編碼拼到關(guān)系令牌上讓小樣本下的拓?fù)涓兄珳?zhǔn)。這個(gè)方向還在跑實(shí)驗(yàn)暫時(shí)沒(méi)有確定的結(jié)論但直覺(jué)上是有空間的。最后分享個(gè)實(shí)操上的小習(xí)慣。我每次做新數(shù)據(jù)集都會(huì)先花半小時(shí)手寫(xiě)十條左右的鄰域序列化樣本自己讀一遍。如果連我自己讀著都覺(jué)得信息不足、抓不住重點(diǎn)那模型肯定也學(xué)不好。這個(gè)“人肉 sanity check”幫我省下的調(diào)試時(shí)間比任何自動(dòng)化工具都多。