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