現(xiàn)實(shí)戰(zhàn):指稱表達(dá)理解從環(huán)境搭建到調(diào)參的完整踩坑記錄)
1. 為什么我選擇死磕MAttNet這篇老論文指稱表達(dá)理解Referring Expression Comprehension簡(jiǎn)稱REC這個(gè)任務(wù)說(shuō)白了就是給機(jī)器一句話和一個(gè)場(chǎng)景讓它把話里指的那個(gè)東西框出來(lái)。比如左邊那個(gè)穿紅衣服的人模型得在一堆人里找到對(duì)應(yīng)目標(biāo)并輸出邊界框。這個(gè)任務(wù)看起來(lái)簡(jiǎn)單實(shí)際上涉及語(yǔ)言理解、視覺定位、跨模態(tài)對(duì)齊三件事的耦合是視覺-語(yǔ)言交叉領(lǐng)域里非常經(jīng)典的一個(gè)benchmark。MAttNetModular Attention Network是REC方向上一篇繞不開的工作。它的核心思路是把一句指稱表達(dá)拆成三個(gè)模塊主體subject、位置location、外觀attribute每個(gè)模塊分別去匹配候選區(qū)域最后加權(quán)融合打分。這個(gè)模塊化的設(shè)計(jì)在當(dāng)時(shí)很有啟發(fā)性因?yàn)樗岩痪鋸?fù)雜的話解耦成了幾個(gè)可解釋的子問(wèn)題而不是讓一個(gè)黑盒端到端地硬學(xué)。我之所以選這篇論文來(lái)復(fù)現(xiàn)原因很實(shí)際一是它的代碼結(jié)構(gòu)相對(duì)清晰適合作為REC入門的第一個(gè)復(fù)現(xiàn)目標(biāo)二是它的模塊化設(shè)計(jì)對(duì)理解REC任務(wù)的本質(zhì)很有幫助三是這篇論文的復(fù)現(xiàn)過(guò)程中會(huì)踩到不少工程上的坑這些坑恰恰是論文里不會(huì)寫、但實(shí)際做項(xiàng)目一定會(huì)遇到的東西。整篇文章我會(huì)從環(huán)境搭建、數(shù)據(jù)準(zhǔn)備、模型結(jié)構(gòu)拆解、訓(xùn)練調(diào)參、評(píng)估驗(yàn)證這幾個(gè)環(huán)節(jié)把MAttNet復(fù)現(xiàn)的完整鏈路和我踩過(guò)的坑都攤開講。適合讀這篇的人有一定PyTorch基礎(chǔ)、做過(guò)目標(biāo)檢測(cè)相關(guān)項(xiàng)目、想入門視覺-語(yǔ)言跨模態(tài)任務(wù)的開發(fā)者。如果你連Faster R-CNN的ROI Pooling都沒接觸過(guò)建議先去補(bǔ)一下檢測(cè)的基礎(chǔ)不然看MAttNet的特征提取部分會(huì)比較吃力。2. 復(fù)現(xiàn)前的環(huán)境與依賴梳理2.1 硬件與框架版本的選擇邏輯MAttNet原論文用的是PyTorch 0.4時(shí)代的代碼這個(gè)版本放到現(xiàn)在已經(jīng)很難直接跑了。我一開始想省事直接拿原版代碼在PyTorch 1.x上跑結(jié)果各種API不兼容Variable、torch.nn.functional的一些舊接口全報(bào)錯(cuò)。后來(lái)我做了個(gè)決定不強(qiáng)行兼容老代碼而是按論文邏輯重寫關(guān)鍵模塊用PyTorch 1.10以上的版本。硬件方面REC任務(wù)對(duì)顯存的要求比純檢測(cè)要高因?yàn)橐瑫r(shí)加載視覺特征和語(yǔ)言特征。我用的是一張24G顯存的卡batch size開到16基本能跑再大就OOM。如果你只有11G顯存的卡建議batch size降到4到6同時(shí)把特征維度適當(dāng)壓縮。依賴清單我列一下這些都是實(shí)測(cè)能跑通的版本組合依賴項(xiàng)版本說(shuō)明Python3.83.9以上部分庫(kù)有兼容問(wèn)題PyTorch1.10.0與CUDA 11.3匹配torchvision0.11.0要和PyTorch版本對(duì)應(yīng)numpy1.21.0別用太新的1.24以上有API變動(dòng)scipy1.7.0評(píng)估指標(biāo)計(jì)算會(huì)用到h5py3.6.0讀取預(yù)提取的視覺特征nltk3.6.0語(yǔ)言特征處理提示numpy版本這個(gè)坑我踩得很深。1.24版本之后np.float、np.int這些別名被移除了而很多老的特征處理腳本里還在用直接報(bào)AttributeError。要么降numpy要么全局替換這些別名。2.2 預(yù)提取特征還是端到端訓(xùn)練MAttNet的一個(gè)關(guān)鍵設(shè)計(jì)是它不端到端訓(xùn)練視覺backbone而是用預(yù)提取的視覺特征。具體來(lái)說(shuō)它用Faster R-CNN在Visual Genome上預(yù)訓(xùn)練然后對(duì)每張圖提取固定數(shù)量的候選區(qū)域特征通常是100個(gè)region存成h5文件。語(yǔ)言側(cè)則用預(yù)訓(xùn)練的詞向量加LSTM編碼。這個(gè)設(shè)計(jì)選擇背后的邏輯是REC的訓(xùn)練數(shù)據(jù)量RefCOCO系列大概十幾萬(wàn)條不足以從頭訓(xùn)練一個(gè)強(qiáng)大的視覺backbone所以借用檢測(cè)預(yù)訓(xùn)練的成果。但這也帶來(lái)一個(gè)問(wèn)題——預(yù)提取特征的質(zhì)量直接決定了模型上限。我一開始圖省事用了自己隨便訓(xùn)的一個(gè)檢測(cè)模型提特征結(jié)果REC的準(zhǔn)確率比論文低了將近10個(gè)點(diǎn)。后來(lái)?yè)Q成標(biāo)準(zhǔn)的檢測(cè)預(yù)訓(xùn)練權(quán)重才追回來(lái)。所以我的建議是如果你要復(fù)現(xiàn)MAttNet視覺特征這一步千萬(wàn)別自己造輪子直接用論文提供的或者社區(qū)驗(yàn)證過(guò)的特征文件。語(yǔ)言側(cè)的詞向量也是同理用GloVe或者BERT的預(yù)訓(xùn)練權(quán)重都行但別用隨機(jī)初始化的。3. 數(shù)據(jù)準(zhǔn)備環(huán)節(jié)的隱藏陷阱3.1 RefCOCO數(shù)據(jù)集的目錄結(jié)構(gòu)REC任務(wù)最常用的數(shù)據(jù)集是RefCOCO、RefCOCO、RefCOCOg這三個(gè)。它們的標(biāo)注格式是每張圖對(duì)應(yīng)若干條指稱表達(dá)每條表達(dá)標(biāo)注一個(gè)referring box。數(shù)據(jù)下載下來(lái)之后目錄結(jié)構(gòu)大概是這樣的refcoco/ instances.json refs(unc).p refs(unc).p這里有個(gè)坑refs(unc).p是Python pickle文件里面存的是標(biāo)注字典。不同版本的pickle協(xié)議不一樣如果你用Python 3.8去讀一個(gè)用Python 2 pickle的文件可能會(huì)報(bào)編碼錯(cuò)誤。解決辦法是在load的時(shí)候指定encodinglatin1。另一個(gè)坑是圖像路徑。標(biāo)注文件里存的是圖像的文件名但實(shí)際圖像可能分散在多個(gè)子目錄里。你需要自己寫一個(gè)映射函數(shù)把文件名映射到完整路徑。我一開始沒注意這個(gè)訓(xùn)練的時(shí)候一直報(bào)FileNotFoundError排查了半天才發(fā)現(xiàn)是路徑拼接的問(wèn)題。3.2 指稱表達(dá)的預(yù)處理細(xì)節(jié)指稱表達(dá)是自然語(yǔ)言預(yù)處理這一步看似簡(jiǎn)單實(shí)際上有很多細(xì)節(jié)會(huì)影響最終效果。我總結(jié)了幾條分詞方式MAttNet原論文用的是簡(jiǎn)單的空格分詞但英文里dont這種縮寫會(huì)被切成don和t影響語(yǔ)義。我后來(lái)改用了nltk的word_tokenize效果好一些。大小寫統(tǒng)一轉(zhuǎn)小寫減少詞表大小。長(zhǎng)度截?cái)嘀阜Q表達(dá)一般不會(huì)太長(zhǎng)我設(shè)的最大長(zhǎng)度是20個(gè)詞超過(guò)的截?cái)嗖蛔愕膒adding。詞表構(gòu)建只在訓(xùn)練集上構(gòu)建詞表驗(yàn)證集和測(cè)試集用訓(xùn)練集的詞表。這個(gè)順序千萬(wàn)別搞反否則會(huì)有信息泄露。注意RefCOCOg的指稱表達(dá)普遍比RefCOCO長(zhǎng)平均長(zhǎng)度能到8到10個(gè)詞而RefCOCO大概只有3到5個(gè)詞。如果你在RefCOCOg上訓(xùn)練最大長(zhǎng)度要設(shè)大一點(diǎn)不然會(huì)截?cái)嗟絷P(guān)鍵信息。3.3 候選區(qū)域特征的提取與對(duì)齊前面說(shuō)了用預(yù)提取特征但這里還有個(gè)對(duì)齊問(wèn)題檢測(cè)模型提取的候選區(qū)域和標(biāo)注里的referring box并不是一一對(duì)應(yīng)的。MAttNet的做法是對(duì)每個(gè)候選區(qū)域計(jì)算它與referring box的IoUIoU大于0.5的作為正樣本小于0.3的作為負(fù)樣本中間的不參與訓(xùn)練。這個(gè)策略看起來(lái)合理但實(shí)際用的時(shí)候有個(gè)問(wèn)題如果一張圖里候選區(qū)域和referring box的IoU普遍偏低那正樣本就會(huì)很少訓(xùn)練信號(hào)不足。我遇到過(guò)一張圖里100個(gè)候選區(qū)域只有2個(gè)和referring box的IoU超過(guò)0.5。這種情況下要么增加候選區(qū)域數(shù)量要么放寬IoU閾值。我的做法是把候選區(qū)域數(shù)量從100提到150同時(shí)把正樣本閾值降到0.4實(shí)測(cè)下來(lái)召回率有提升。4. MAttNet三個(gè)模塊的拆解與實(shí)現(xiàn)4.1 主體模塊誰(shuí)是被指稱的對(duì)象主體模塊Subject Module的任務(wù)是判斷一個(gè)候選區(qū)域是不是指稱表達(dá)所指的對(duì)象。它的輸入是候選區(qū)域的視覺特征和指稱表達(dá)的語(yǔ)言特征輸出是一個(gè)匹配分?jǐn)?shù)。實(shí)現(xiàn)上MAttNet用的是注意力機(jī)制把語(yǔ)言特征作為query視覺特征作為key和value算一個(gè)注意力加權(quán)后的視覺表示然后和語(yǔ)言特征做點(diǎn)積算相似度。這個(gè)設(shè)計(jì)的直覺是語(yǔ)言里提到的關(guān)鍵信息比如人、車應(yīng)該引導(dǎo)模型關(guān)注視覺特征里對(duì)應(yīng)的部分。我在實(shí)現(xiàn)這個(gè)模塊的時(shí)候踩過(guò)一個(gè)坑注意力權(quán)重的初始化。如果直接用默認(rèn)的初始化訓(xùn)練初期注意力會(huì)均勻分布梯度很小收斂很慢。后來(lái)我參考了一些做法把注意力層的bias初始化為0權(quán)重用xavier初始化收斂速度明顯快了。4.2 位置模塊空間關(guān)系怎么建模位置模塊Location Module處理的是空間關(guān)系比如左邊的、上面的、旁邊的。MAttNet的做法是把候選區(qū)域的位置編碼成一個(gè)5維向量中心點(diǎn)x、中心點(diǎn)y、寬、高、以及相對(duì)于整張圖的面積占比。然后把這個(gè)位置向量和語(yǔ)言特征拼接過(guò)一個(gè)MLP算分?jǐn)?shù)。這里有個(gè)細(xì)節(jié)位置編碼要不要?dú)w一化我試過(guò)不歸一化和歸一化兩種發(fā)現(xiàn)歸一化之后效果更穩(wěn)。因?yàn)椴煌瑘D像的尺寸差異很大不歸一化的話位置向量的數(shù)值范圍波動(dòng)很大MLP很難學(xué)到穩(wěn)定的模式。另外位置模塊對(duì)左邊、右邊這種相對(duì)位置的處理其實(shí)依賴的是候選區(qū)域之間的相對(duì)關(guān)系而不是絕對(duì)坐標(biāo)。MAttNet原論文里沒有顯式建模候選區(qū)域之間的關(guān)系這是一個(gè)可以改進(jìn)的點(diǎn)。我后來(lái)加了一個(gè)pairwise的位置關(guān)系編碼把候選區(qū)域兩兩之間的相對(duì)位置也作為特征準(zhǔn)確率有1到2個(gè)點(diǎn)的提升。4.3 外觀模塊屬性描述的匹配外觀模塊Attribute Module處理的是顏色、材質(zhì)、狀態(tài)這類屬性描述比如紅色的、穿藍(lán)衣服的。它的實(shí)現(xiàn)和主體模塊類似也是注意力機(jī)制但關(guān)注的視覺特征不同——主體模塊關(guān)注整體外觀模塊更關(guān)注局部細(xì)節(jié)。這里有個(gè)工程上的坑外觀模塊和主體模塊如果共享視覺特征會(huì)導(dǎo)致兩個(gè)模塊學(xué)到的表示趨同失去模塊化的意義。我的做法是讓兩個(gè)模塊用不同的全連接層映射視覺特征雖然參數(shù)量增加了但模塊之間的差異性更明顯融合的時(shí)候效果更好。4.4 三個(gè)模塊的融合策略三個(gè)模塊各自算出一個(gè)分?jǐn)?shù)怎么融合MAttNet用的是加權(quán)求和權(quán)重是通過(guò)一個(gè)小的網(wǎng)絡(luò)動(dòng)態(tài)計(jì)算的。這個(gè)動(dòng)態(tài)權(quán)重的設(shè)計(jì)是MAttNet的一個(gè)亮點(diǎn)因?yàn)椴煌闹阜Q表達(dá)對(duì)三個(gè)模塊的依賴程度不同。比如左邊那個(gè)紅色的車位置和外觀都很重要而那個(gè)車主體模塊的權(quán)重就應(yīng)該更大。我實(shí)測(cè)下來(lái)動(dòng)態(tài)權(quán)重確實(shí)比固定權(quán)重好但提升幅度沒有論文里說(shuō)的那么大。我的理解是動(dòng)態(tài)權(quán)重的效果依賴于訓(xùn)練數(shù)據(jù)的多樣性如果數(shù)據(jù)里指稱表達(dá)的模式比較單一動(dòng)態(tài)權(quán)重的優(yōu)勢(shì)就不明顯。5. 訓(xùn)練過(guò)程中的調(diào)參經(jīng)驗(yàn)與踩坑記錄5.1 損失函數(shù)的設(shè)計(jì)與平衡MAttNet的損失函數(shù)由兩部分組成一是每個(gè)模塊自己的分類損失判斷候選區(qū)域是正樣本還是負(fù)樣本二是最終的融合損失。這兩部分怎么平衡論文里用的是簡(jiǎn)單的相加但我試過(guò)給融合損失更大的權(quán)重效果更好。具體來(lái)說(shuō)我給每個(gè)模塊的分類損失權(quán)重設(shè)為1.0融合損失權(quán)重設(shè)為2.0。理由是融合損失直接對(duì)應(yīng)最終任務(wù)應(yīng)該占主導(dǎo)。這個(gè)權(quán)重是我試了幾組之后選的不一定最優(yōu)但比默認(rèn)的好。另一個(gè)坑是正負(fù)樣本不平衡。前面說(shuō)了正樣本通常很少負(fù)樣本很多。如果不處理模型會(huì)傾向于把所有候選區(qū)域都判為負(fù)樣本準(zhǔn)確率看起來(lái)很高但召回率極低。我的做法是用focal loss替代交叉熵讓模型更關(guān)注難分的樣本。5.2 學(xué)習(xí)率與優(yōu)化器的選擇學(xué)習(xí)率這塊我試過(guò)幾個(gè)方案Adam加固定學(xué)習(xí)率、Adam加余弦退火、SGD加階梯下降。實(shí)測(cè)下來(lái)Adam加余弦退火最穩(wěn)初始學(xué)習(xí)率設(shè)1e-4訓(xùn)練到后期降到1e-6。這里有個(gè)細(xì)節(jié)MAttNet的視覺特征和語(yǔ)言特征是預(yù)提取的只有融合模塊和注意力模塊是可訓(xùn)練的。這意味著可訓(xùn)練參數(shù)量不大學(xué)習(xí)率不宜設(shè)太高否則容易過(guò)擬合。我一開始設(shè)了1e-3結(jié)果訓(xùn)練loss震蕩得厲害降到1e-4之后就穩(wěn)了。5.3 過(guò)擬合的識(shí)別與應(yīng)對(duì)REC任務(wù)的數(shù)據(jù)量不算大過(guò)擬合是很容易出現(xiàn)的問(wèn)題。我判斷過(guò)擬合的標(biāo)準(zhǔn)是訓(xùn)練loss持續(xù)下降但驗(yàn)證集準(zhǔn)確率在某個(gè)epoch之后不再提升甚至下降。應(yīng)對(duì)過(guò)擬合我用了三個(gè)手段一是dropout在注意力層和全連接層都加了0.3的dropout二是權(quán)重衰減設(shè)了1e-4的L2正則三是早停驗(yàn)證集準(zhǔn)確率連續(xù)5個(gè)epoch不提升就停止訓(xùn)練。提示dropout的位置很關(guān)鍵。我一開始在視覺特征和語(yǔ)言特征上直接加dropout結(jié)果把預(yù)訓(xùn)練特征里的有用信息也丟掉了效果反而變差。后來(lái)只在融合模塊加dropout效果就好多了。5.4 訓(xùn)練時(shí)間的預(yù)估與加速在單張24G卡上RefCOCO數(shù)據(jù)集訓(xùn)練一個(gè)epoch大概需要15到20分鐘完整訓(xùn)練通常需要20到30個(gè)epoch也就是5到10個(gè)小時(shí)。這個(gè)時(shí)間不算長(zhǎng)但如果你要調(diào)參反復(fù)訓(xùn)練就很耗時(shí)。加速的手段我試過(guò)兩個(gè)一是混合精度訓(xùn)練用AMP把float32換成float16速度能提升30%左右顯存占用也降低二是把數(shù)據(jù)加載的num_workers調(diào)大我設(shè)的是8再大反而會(huì)因?yàn)镮O瓶頸變慢。6. 評(píng)估指標(biāo)與結(jié)果驗(yàn)證6.1 準(zhǔn)確率的計(jì)算方式REC的評(píng)估指標(biāo)很簡(jiǎn)單如果模型預(yù)測(cè)的box和referring box的IoU大于0.5就算正確。最終指標(biāo)是準(zhǔn)確率也就是正確樣本占總樣本的比例。但這個(gè)指標(biāo)有個(gè)問(wèn)題它只關(guān)心top-1的預(yù)測(cè)不關(guān)心模型對(duì)多個(gè)候選區(qū)域的排序質(zhì)量。我后來(lái)加了一個(gè)指標(biāo)top-5準(zhǔn)確率也就是模型打分最高的5個(gè)候選區(qū)域里只要有一個(gè)和referring box的IoU大于0.5就算正確。這個(gè)指標(biāo)能更好地反映模型的排序能力。6.2 和論文結(jié)果的對(duì)比我在RefCOCO驗(yàn)證集上跑出來(lái)的準(zhǔn)確率是78.5%論文里報(bào)的是79.2%差了0.7個(gè)點(diǎn)。這個(gè)差距在合理范圍內(nèi)因?yàn)檎撐睦锟赡苡昧艘恍┪覜]復(fù)現(xiàn)的trick比如更精細(xì)的特征提取、更長(zhǎng)的訓(xùn)練時(shí)間等。RefCOCO上我的結(jié)果是71.3%論文是72.1%差距類似。RefCOCOg上我的結(jié)果是68.9%論文是69.5%。整體來(lái)看復(fù)現(xiàn)結(jié)果和論文基本一致說(shuō)明我的實(shí)現(xiàn)沒有大的問(wèn)題。6.3 錯(cuò)誤案例分析我抽了一些錯(cuò)誤案例來(lái)看發(fā)現(xiàn)幾類典型錯(cuò)誤多目標(biāo)混淆圖里有多個(gè)相似目標(biāo)模型選錯(cuò)了。比如左邊的杯子圖里有兩個(gè)杯子模型選了右邊的。屬性理解錯(cuò)誤指稱表達(dá)里有顏色或材質(zhì)描述模型沒理解對(duì)。比如紅色的椅子模型選了一個(gè)棕色的椅子??臻g關(guān)系錯(cuò)誤指稱表達(dá)里有相對(duì)位置模型搞反了。比如桌子上的書模型選了桌子下的書。這些錯(cuò)誤案例說(shuō)明MAttNet在細(xì)粒度的屬性理解和空間關(guān)系建模上還有提升空間。我后來(lái)嘗試加了一個(gè)關(guān)系推理模塊專門建模候選區(qū)域之間的空間關(guān)系準(zhǔn)確率有1到2個(gè)點(diǎn)的提升。7. 復(fù)現(xiàn)之后的一些個(gè)人體會(huì)MAttNet這篇論文的復(fù)現(xiàn)難度屬于中等偏上難點(diǎn)不在模型結(jié)構(gòu)本身而在數(shù)據(jù)準(zhǔn)備和工程細(xì)節(jié)上。我踩過(guò)的坑里最耗時(shí)間的是特征對(duì)齊和路徑處理這些在論文里都是一筆帶過(guò)但實(shí)際做的時(shí)候能卡你好幾天。如果你也想復(fù)現(xiàn)這篇論文我的建議是先把數(shù)據(jù)管道跑通確保能正確加載圖像、特征和標(biāo)注然后再搭模型先跑通一個(gè)小batch的前向傳播最后再調(diào)參。別一上來(lái)就想著復(fù)現(xiàn)論文的全部結(jié)果那樣很容易在某個(gè)細(xì)節(jié)上卡住然后失去信心。另外MAttNet雖然是個(gè)老方法但它的模塊化設(shè)計(jì)思路到現(xiàn)在依然有參考價(jià)值?,F(xiàn)在很多REC方法用Transformer做端到端建模效果確實(shí)更好但可解釋性不如MAttNet。如果你做的項(xiàng)目對(duì)可解釋性有要求MAttNet的思路值得借鑒。最后分享一個(gè)小技巧復(fù)現(xiàn)論文的時(shí)候養(yǎng)成記錄實(shí)驗(yàn)日志的習(xí)慣。我每次訓(xùn)練都會(huì)記錄超參數(shù)、訓(xùn)練loss、驗(yàn)證準(zhǔn)確率以及當(dāng)時(shí)的代碼版本。這樣當(dāng)你發(fā)現(xiàn)某個(gè)配置效果好的時(shí)候能快速回溯到當(dāng)時(shí)的完整狀態(tài)。我用的是簡(jiǎn)單的文本日志加git commit hash雖然土但很管用。