樣本引導(dǎo)重排序:讓圖像描述模型不再“胡說八道”)
做圖像描述看圖說話這幾年我一直有個(gè)很深的體會(huì)模型能“說”并不難難的是讓它“說到點(diǎn)子上”。前兩天看到一篇來自某歐洲高校計(jì)算機(jī)視覺實(shí)驗(yàn)室的新工作團(tuán)隊(duì)把方法命名為RNS全稱是 Reranking with Negative Sampling翻譯過來就是“負(fù)樣本引導(dǎo)的重排序”。如果你也在做圖像理解、多模態(tài)生成或者正在被模型“一本正經(jīng)胡說八道”的問題折磨這篇內(nèi)容值得認(rèn)真讀完。RNS解決的問題非常具體在模型已經(jīng)能生成多個(gè)候選描述的情況下怎么從這些候選中挑出那個(gè)真正貼合圖片內(nèi)容的句子而不是只挑一個(gè)“讀起來順口但其實(shí)看圖不靠譜”的結(jié)果。它把一個(gè)常被忽略的環(huán)節(jié)——句子選擇從“按生成概率直接取最高”升級成了“用專門的排序器綜合判斷”。概念不復(fù)雜工程上也容易落地但效果提升卻非常明顯。下面我把它的來龍去脈、核心原理和復(fù)現(xiàn)要點(diǎn)完整拆一遍。1. 看圖說話的問題到底卡在哪1.1 一個(gè)看似簡單卻一直被低估的任務(wù)圖像描述image captioning的任務(wù)一句話就能說清楚給模型一張圖讓它輸出一句通順自然、且內(nèi)容與圖像高度一致的自然語言描述。但真做起來的難度遠(yuǎn)超預(yù)期。它不像圖像分類那樣只輸出一個(gè)標(biāo)簽也不像機(jī)器翻譯那樣輸入輸出都是文本。圖像描述要求模型同時(shí)具備三件事看懂圖上有哪些物體和關(guān)系、把這些視覺信息組織成符合語法的句子、保證句子里的每個(gè)關(guān)鍵語義都能在圖像里找到對應(yīng)點(diǎn)。這三件事任何一環(huán)掉鏈子輸出就會(huì)出問題。我在實(shí)際項(xiàng)目中見過太多“看起來還行”的模型語法完全正確句子也流暢但描述的對象和圖像內(nèi)容對不上號。比如一張“白色轎車停在超市門口”的圖模型寫成了“一輛公交車行駛在公路上”。這種錯(cuò)誤不是模型不會(huì)說英文不是詞匯量不夠而是它在“選擇該說什么”這個(gè)環(huán)節(jié)出了問題。1.2 三個(gè)典型錯(cuò)誤幻覺對象、重復(fù)片段、語義漂移如果長期看模型生成的描述文本會(huì)發(fā)現(xiàn)錯(cuò)誤類型其實(shí)高度集中。最常見的是幻覺對象圖片里根本沒有出現(xiàn)的東西被模型大言不慚地寫進(jìn)了句子。模型在訓(xùn)練時(shí)看到“貓”和“沙發(fā)”經(jīng)常一起出現(xiàn)于是只要視覺特征里有類似沙發(fā)紋理的信號它就可能憑空補(bǔ)出一個(gè)“貓”來。第二個(gè)高頻問題是重復(fù)片段。自回歸解碼器逐字生成時(shí)如果某個(gè) token 的概率分布不夠尖銳很容易陷入局部循環(huán)輸出“a dog and a dog and a dog…”這種毫無信息量的重復(fù)句子長度越長越明顯。很多入門項(xiàng)目里都能看到這類輸出根源在于解碼策略和訓(xùn)練目標(biāo)都沒有對“全局連貫性”做足夠的約束。第三類問題叫語義漂移。句子前半段還在描述正確的對象生成到后半段話題突然跳走。比如“一個(gè)穿紅色衣服的女孩在公園里跑步手里拿著一個(gè)手機(jī)”后半截的“手機(jī)”就是憑空跳出來的。這類問題最隱蔽因?yàn)槊總€(gè)局部片段單獨(dú)看都正常合在一起就是錯(cuò)。1.3 為什么數(shù)據(jù)更多、模型更大也不一定能解決很多人第一反應(yīng)是模型不行那就加數(shù)據(jù)、加參數(shù)??涩F(xiàn)實(shí)是即便模型規(guī)模翻倍、訓(xùn)練數(shù)據(jù)增加上述三類錯(cuò)誤依然會(huì)存在只是頻率降低不會(huì)根除。原因在于這些錯(cuò)誤的本質(zhì)是“解碼和選擇機(jī)制”不夠好而不是視覺表征不夠強(qiáng)。你可以把生成描述理解成兩步第一步是從圖像里提取足夠好的信息第二步是把這些信息組織成一句最優(yōu)的句子?,F(xiàn)有的大規(guī)模預(yù)訓(xùn)練模型第一步通常已經(jīng)做得相當(dāng)好但第二步里的“最優(yōu)句子選擇”傳統(tǒng)做法往往只是按生成概率做個(gè)貪心解碼或 beam search本質(zhì)上是局部最優(yōu)不是全局最優(yōu)。也就是說模型可能早就“想”出了正確描述但在最終選擇路徑時(shí)把它丟掉了。這正是 RNS 選擇切入的地方不動(dòng)表征不改主干模型專門去優(yōu)化“從多個(gè)候選里挑出最好那個(gè)”的選擇環(huán)節(jié)。2. RNS的整體設(shè)計(jì)與核心思路2.1 一句話理解RNS先多生成再按可靠性挑RNS 的思路用一個(gè)比方能講明白現(xiàn)在公司要招人你不再讓候選人只交一份簡歷就定結(jié)果而是先讓所有人做一輪開放式筆試產(chǎn)出一批候選方案再由資歷更深的評審團(tuán)從“實(shí)際做事能力、專業(yè)匹配度、表達(dá)是否靠譜”等多個(gè)維度綜合打分選出最終錄用的人。放到圖像描述場景里筆試環(huán)節(jié)就是讓訓(xùn)練好的自回歸解碼器生成 N 個(gè)候選句子評審環(huán)節(jié)就是用一個(gè)專門的重排序器給每個(gè)候選句子打一個(gè)“可靠性分?jǐn)?shù)”最后選分?jǐn)?shù)最高的句子作為最終輸出。這個(gè)兩階段框架并不算新思想信息檢索領(lǐng)域的 re-ranking 早就在用。RNS 的獨(dú)特之處在于它對重排序器的訓(xùn)練方式做了針對性設(shè)計(jì)核心是引入了高質(zhì)量的負(fù)樣本。傳統(tǒng)的重排序器只會(huì)見過“候選句子與參考句子像不像”這樣的正樣本比較信號RNS 會(huì)額外構(gòu)造大量“與圖片描述部分相關(guān)、但關(guān)鍵信息出錯(cuò)”的負(fù)樣本逼著重排序器學(xué)會(huì)辨認(rèn)那些看起來順眼、實(shí)際上與圖像不一致的句子。2.2 負(fù)樣本不是隨便抄幾句壞話負(fù)樣本的質(zhì)量直接決定排序器的上限。如果只在海里隨便抽幾句完全無關(guān)的話當(dāng)負(fù)樣本模型很快就能學(xué)會(huì)——那也太簡單了句子和圖像完全不相關(guān)一眼就能看出來。這種負(fù)樣本訓(xùn)練出來的排序器拿到真實(shí)場景里沒有任何實(shí)戰(zhàn)能力因?yàn)檎鎸?shí)候選句都是“有點(diǎn)相關(guān)、但細(xì)節(jié)出錯(cuò)”的類型。RNS 構(gòu)造負(fù)樣本的核心原則是“難負(fù)樣本”也就是和正樣本高度相似、但存在關(guān)鍵語義錯(cuò)誤的句子。常用做法有三種。第一種是對象級替換把參考描述中的核心名詞替換成同類別或視覺特征相近的另一個(gè)詞。比如“一只貓趴在窗臺(tái)上”改成“一只狗趴在窗臺(tái)上”。第二種是局部語序打亂把正常的句法結(jié)構(gòu)破壞讓模型看到“語序正確性”也是一種重要的判斷信號。第三種是跨圖錯(cuò)配把另一張圖片的、語法完全正確的參考描述拿過來當(dāng)作當(dāng)前圖片的負(fù)樣本。這類負(fù)樣本在訓(xùn)練中非常有效能迫使排序器學(xué)會(huì)真正去理解“圖像內(nèi)容”與“句子語義”之間的對應(yīng)關(guān)系而不只是依賴句子本身的質(zhì)量。2.3 可靠性分?jǐn)?shù)如何計(jì)算重排序器給候選句子打分不能只看一個(gè)信號至少要綜合三路信息。第一路是生成似然分?jǐn)?shù)。解碼器給這個(gè)候選句子分配的概率能反映出模型自己覺得它順不順但這項(xiàng)分?jǐn)?shù)單獨(dú)用會(huì)偏向高頻套話所以只能作為基礎(chǔ)分。第二路是視覺-語言匹配分?jǐn)?shù)。用一個(gè)跨模態(tài)編碼器把圖像特征和候選句子的文本特征做交叉注意力判斷句子描述的內(nèi)容在圖像里有沒有對應(yīng)區(qū)域。第三路是負(fù)樣本辨別分?jǐn)?shù)。排序器在訓(xùn)練時(shí)見過大量難負(fù)樣本會(huì)學(xué)會(huì)一套“哪些細(xì)節(jié)最容易出錯(cuò)”的敏感度這部分信號能讓排序器主動(dòng)懲罰那些“看起來合理、實(shí)際是幻覺”的候選句。最終分?jǐn)?shù)是這三路信號的加權(quán)融合。融合權(quán)重不需要太復(fù)雜固定比例或者加一個(gè)簡單的門控網(wǎng)絡(luò)都可以。我在復(fù)現(xiàn)時(shí)先用固定比例比如 0.3、0.4、0.3跑通流程再根據(jù)驗(yàn)證集效果做小幅調(diào)整效果就足夠好。2.4 為什么比端到端微調(diào)更穩(wěn)看圖說話領(lǐng)域有一個(gè)很常見的優(yōu)化路線是用強(qiáng)化學(xué)習(xí)直接優(yōu)化 CIDEr 之類的指標(biāo)例如 self-critical 方法。它的問題在于訓(xùn)練方差大、超參數(shù)敏感經(jīng)常出現(xiàn)指標(biāo)在訓(xùn)練集上漲、驗(yàn)證集開始震蕩或者生成的句子為了押中 n-gram 導(dǎo)致嚴(yán)重重復(fù)。這種“直接拿指標(biāo)當(dāng)獎(jiǎng)勵(lì)”的方式相當(dāng)于讓一個(gè)新手去考滿分卷過程很容易走偏。RNS 把生成和選擇解耦生成器繼續(xù)用穩(wěn)定成熟的交叉熵訓(xùn)練排序器單獨(dú)用排序損失訓(xùn)練。每個(gè)模塊的任務(wù)都更簡單訓(xùn)練也就更穩(wěn)定。工程上這還有個(gè)額外好處排序器和生成器可以分別迭代。今天換一個(gè)更強(qiáng)的生成器排序器不用重頭訓(xùn)練排序器想加一個(gè)新打分信號生成器也不用動(dòng)。模塊化帶來的是實(shí)打?qū)嵉牡侍嵘.?dāng)然RNS 也不是沒有代價(jià)。它需要額外維護(hù)一個(gè)重排序模型推理時(shí)也要先把 N 個(gè)候選句子都生成出來再打分計(jì)算量比單次貪婪解碼要高。但在精度優(yōu)先的業(yè)務(wù)場景里這一點(diǎn)點(diǎn)成本完全劃算。3. 關(guān)鍵細(xì)節(jié)與實(shí)操要點(diǎn)3.1 候選生成階段的參數(shù)控制RNS 的效果上限很大程度取決于候選句子質(zhì)量。如果 beam search 出來的 16 個(gè)候選本身就全是爛句子那排序器再強(qiáng)也挑不出金子。所以第一步不是調(diào)排序器而是先把生成階段調(diào)好。beam size 我建議設(shè)成 8 到 16。太小候選多樣性不夠太大排序器的計(jì)算壓力會(huì)明顯增加。長度懲罰length penalty也值得專門調(diào)我常用 alpha 在 0.8 到 1.2 之間重點(diǎn)觀察候選句里有沒有因?yàn)殚L度懲罰過低導(dǎo)致的長句崩壞。另外一定要開啟重復(fù)懲罰。如果你用的解碼器支持 repetition_penalty 之類的參數(shù)建議設(shè)到 1.2 以上。否則生成階段就會(huì)把“重復(fù)片段”這類錯(cuò)誤大量帶進(jìn)候選池排序器需要花很大力氣去糾正等于白白浪費(fèi)擬合能力。3.2 重排序器的結(jié)構(gòu)與輸入重排序器本身不需要很復(fù)雜。我用的是一個(gè)輕量級的雙向上下文編碼器輸入由三個(gè)部分組成候選句子的 token 序列、圖像的區(qū)域特征或者圖像級特征視任務(wù)規(guī)模而定、以及一個(gè)表示“候選句子來自第幾個(gè) beam”的序號特征。序號特征這個(gè)細(xì)節(jié)容易忽略但實(shí)測對結(jié)果有正貢獻(xiàn)因?yàn)?beam 里靠前的候選句通常整體質(zhì)量更穩(wěn)排序器可以學(xué)習(xí)到這個(gè)先驗(yàn)。訓(xùn)練損失用成對排序損失pairwise ranking loss。目標(biāo)很簡單正樣本的分?jǐn)?shù)比負(fù)樣本的分?jǐn)?shù)至少高出 margin。代碼里可以寫成 max(0, margin - score_pos score_neg)。margin 我推薦 0.2 到 0.5太小會(huì)讓排序器“無所謂”太大則可能導(dǎo)致訓(xùn)練早期梯度太強(qiáng)、模型不穩(wěn)定。訓(xùn)練數(shù)據(jù)構(gòu)造環(huán)節(jié)要特別提醒正樣本和負(fù)樣本必須來自同一幅圖。如果你把 A 圖的正樣本和 B 圖的負(fù)樣本配對模型學(xué)到的是“哪句話更順”而不是“哪句話更貼合這張圖”會(huì)嚴(yán)重影響最終效果。3.3 與其他優(yōu)化路線的對比為了讓你對 RNS 的定位更清楚我用一張表橫向?qū)Ρ葞讞l常見的優(yōu)化路線。方法路線核心思想優(yōu)點(diǎn)缺點(diǎn)生成概率重排直接用解碼器的生成概率選 top-1實(shí)現(xiàn)簡單、零額外成本偏向局部高頻詞忽視全局語義一致性強(qiáng)化學(xué)習(xí)微調(diào)用評估指標(biāo)當(dāng)獎(jiǎng)勵(lì)直接優(yōu)化生成器能讓模型對齊指標(biāo)方差大、容易重復(fù)、超參數(shù)敏感兩階段人工篩選生成大量候選后人工挑最好的上限高不可自動(dòng)化無法落地RNS生成候選 負(fù)樣本引導(dǎo)排序器自動(dòng)挑穩(wěn)定、可解釋、模塊化多一個(gè)排序器訓(xùn)練和推理成本從這張表可以看出來RNS 并不是要在生成模型上和別人硬拼參數(shù)它贏在“選擇”這件事做得系統(tǒng)化。這也是我在業(yè)務(wù)項(xiàng)目里最認(rèn)可它的地方。4. 一個(gè)可落地的最小復(fù)現(xiàn)流程4.1 準(zhǔn)備一套小規(guī)模數(shù)據(jù)先說數(shù)據(jù)。圖像描述模型需要圖文對數(shù)據(jù)。如果你沒有現(xiàn)成的內(nèi)部數(shù)據(jù)建議直接從某個(gè)公開的圖像描述數(shù)據(jù)集中切一個(gè)子集出來先跑通流程再逐步擴(kuò)大規(guī)模。我在這里用一個(gè)虛構(gòu)代稱 U 指代那個(gè)數(shù)據(jù)集具體是誰不重要關(guān)鍵是它的格式要包含圖像文件路徑、每張圖對應(yīng)多條人工描述、以及劃分好的訓(xùn)練集/驗(yàn)證集/測試集。小規(guī)模驗(yàn)證項(xiàng)目M 我采用的是 2 萬張圖、每張圖 5 條參考描述。這個(gè)體量足夠看到一個(gè)趨勢又不至于讓你卡在算力上。數(shù)據(jù)準(zhǔn)備好后記得做三件事把圖片統(tǒng)一縮放到相同尺寸把描述文本全部切成 token過濾掉出現(xiàn)頻率過低的詞。詞匯表太大會(huì)讓模型學(xué)不扎實(shí)建議控制在 5000 到 10000 個(gè)詞的規(guī)模。4.2 訓(xùn)練生成模型第一階段的生成模型可以用一個(gè)輕量結(jié)構(gòu)視覺編碼器負(fù)責(zé)把圖片轉(zhuǎn)成特征序列解碼器負(fù)責(zé)把特征序列轉(zhuǎn)化為描述文本。訓(xùn)練目標(biāo)就是標(biāo)準(zhǔn)的交叉熵。import torch import torch.nn as nn # 示意image_feats.shape [batch, num_regions, feat_dim] # target_ids.shape [batch, seq_len] def train_generator(model, image_feats, target_ids, vocab_size): # 解碼器基于圖像特征和已生成的前綴 token 預(yù)測下一個(gè) token logits model(image_feats, target_ids[:, :-1]) # [batch, seq_len-1, vocab_size] loss nn.CrossEntropyLoss(ignore_index0)( logits.reshape(-1, vocab_size), target_ids[:, 1:].reshape(-1) ) return loss這個(gè)階段不需要太長驗(yàn)證集上 CIDEr 曲線開始走平就可以停。之后我建議再用一個(gè)輕量級的強(qiáng)化信號做幾個(gè) epoch 的微調(diào)但它不是必須的。如果你的硬性評價(jià)指標(biāo)壓力不大跳過這一步完全可行。4.3 訓(xùn)練RNS排序器生成模型訓(xùn)練好后固定權(quán)重開始構(gòu)造排序器訓(xùn)練數(shù)據(jù)。每一張訓(xùn)練圖片走一遍 beam search得到 N 個(gè)候選句子再從參考描述里隨機(jī)抽一個(gè)作為正樣本用前面提到的三類策略構(gòu)造負(fù)樣本。def train_ranker(ranker, image_feats, pos_ids, neg_ids, margin0.3): pos_score ranker(image_feats, pos_ids) # 正樣本可靠性分?jǐn)?shù) neg_score ranker(image_feats, neg_ids) # 負(fù)樣本可靠性分?jǐn)?shù) loss torch.relu(margin - pos_score neg_score).mean() return loss訓(xùn)練排序器時(shí)有一個(gè)操作細(xì)節(jié)值得強(qiáng)調(diào)候選句子要從生成模型實(shí)際采樣出來而不是從參考描述里隨機(jī)切斷或復(fù)制。原因很簡單推理階段排序器面對的一定是生成模型自己吐出來的句子如果訓(xùn)練時(shí)用的是“干凈的人工描述”分布就錯(cuò)位了。這一點(diǎn)很多人第一次實(shí)現(xiàn)時(shí)會(huì)踩坑。4.4 推理階段接入重排序推理流程比訓(xùn)練更簡單。原來你可能直接用 beam search 的 top-1 作為結(jié)果現(xiàn)在改成先生成 N 個(gè)候選句子逐句過一遍排序器最后取排序器分?jǐn)?shù)最高的那句。def infer_with_ranker(generator, ranker, image_feats, beam_size16): candidates, scores generator.beam_search(image_feats, beam_sizebeam_size) # 對每個(gè)候選句子計(jì)算 RNS 綜合分?jǐn)?shù) final_scores [ 0.3 * scores[i] 0.4 * ranker.visual_match(image_feats, candidates[i]) 0.3 * ranker.discriminative_score(image_feats, candidates[i]) for i in range(len(candidates)) ] best_idx torch.tensor(final_scores).argmax() return candidates[best_idx]這一步整體替換成本很低不需要改動(dòng)生成模型內(nèi)部結(jié)構(gòu)也不需要重新訓(xùn)練主模型。我在實(shí)際項(xiàng)目中從決定接入到跑通線上效果只用了不到一周絕大多數(shù)時(shí)間都花在調(diào)負(fù)樣本構(gòu)造策略上。5. 常見問題與排查技巧實(shí)錄5.1 候選都是爛句子重排救不回來這是最先要排除的情況。如果你把 beam search 的結(jié)果完整打印出來發(fā)現(xiàn) 16 個(gè)句子里有 14 個(gè)都是重復(fù)或明顯偏題的那說明問題在生成階段而不是排序器。先回去加大 repetition_penalty適當(dāng)調(diào)低溫度或者檢查生成模型是不是訓(xùn)練沒收斂。一個(gè)實(shí)用的判斷標(biāo)準(zhǔn)是候選池里至少有三分之一句子人眼看著是“有潛力但有小錯(cuò)”的。如果達(dá)不到就別先折騰排序器。5.2 排序器在驗(yàn)證集上反而變差典型反應(yīng)是生成器沒動(dòng)加了排序器后指標(biāo)反而掉了。這時(shí)候先別懷疑 RNS 的思路大概率是負(fù)樣本構(gòu)造出了問題。負(fù)樣本太簡單排序器學(xué)到的判別邊界特別粗糙真實(shí)候選句在它眼里全都差不多負(fù)樣本太難排序器又會(huì)被一些極端case帶偏出現(xiàn)分?jǐn)?shù)震蕩。我的經(jīng)驗(yàn)是先做一個(gè)最簡單的版本只用“跨圖錯(cuò)配”這一種負(fù)樣本跑通流程保證排序器確實(shí)有效然后再逐步加入對象替換和語序打亂。這樣一旦效果變差你立刻知道是哪種負(fù)樣本引入的問題。5.3 指標(biāo)漲了人眼看著別扭CIDEr 這類指標(biāo)本質(zhì)上是 n-gram 匹配度它天生偏好高頻出現(xiàn)的套話。排序器在訓(xùn)練時(shí)如果過度擬合 CIDEr就會(huì)傾向于給那些“常見但平庸”的句子打高分。這會(huì)導(dǎo)致一個(gè)尷尬局面客觀指標(biāo)漲了 2 個(gè)點(diǎn)但人眼評測時(shí)感覺模型變得更套路化。我的解決辦法是訓(xùn)練排序器時(shí)不要把參考句子當(dāng)唯一天花板而是在 loss 里加入一個(gè)正則項(xiàng)如果候選句子包含的圖像級對象與視覺檢測模塊給出的高置信度對象列表完全不匹配就額外懲罰。這個(gè)信號不用很強(qiáng)但能有效拉住模型讓它別為了指標(biāo)好看而丟掉真實(shí)視覺信息。5.4 常見問題速查表現(xiàn)象可能原因排查方向候選句子反復(fù)出現(xiàn)同一詞組解碼階段重復(fù)懲罰不夠調(diào)高 repetition_penalty增大 beam size排序器分?jǐn)?shù)分布太集中幾乎分不出高低負(fù)樣本太簡單替換成難負(fù)樣本提高對象級替換比例訓(xùn)練排序器時(shí) loss 遲遲不降margin 設(shè)置過大或數(shù)據(jù)配對錯(cuò)誤降低 margin檢查是否同圖配對推理速度不符合要求候選數(shù)量多排序器計(jì)算量大減小 beam size給排序器做量化或蒸餾指標(biāo)漲但人工評測差過度擬合 n-gram 指標(biāo)加入視覺對象一致性正則我在自己業(yè)務(wù)場景里復(fù)現(xiàn)這套思路時(shí)最值錢的不是模型結(jié)構(gòu)而是把“選出好句子”這一件小事真正當(dāng)成獨(dú)立工程問題來對待。RNS 給我的最大啟發(fā)是當(dāng)模型輸出不穩(wěn)定時(shí)先別急著加參數(shù)、加數(shù)據(jù)試著把生成和選擇拆開看看是不是選擇環(huán)節(jié)太隨意了。這個(gè)思路不僅能用在圖像描述上機(jī)器翻譯、文本摘要、對話系統(tǒng)其實(shí)都有同樣的問題。最后再分享一個(gè)小技巧負(fù)樣本的質(zhì)量直接決定排序器上限這個(gè)環(huán)節(jié)非常值得你花最多的時(shí)間去調(diào)試。每調(diào)整一次負(fù)樣本構(gòu)造策略都先在驗(yàn)證集上用一個(gè)小樣本池跑一輪對比穩(wěn)定了再去全量訓(xùn)練效率會(huì)高很多。