域特征與視覺token工程實踐)
簡介這份代碼包面向圖像字幕、視覺問答VQA及顯著區(qū)域特征提取的研究者與工程實踐者提供基于Faster R-CNN、ResNet-101和Visual Genome對象及屬性標(biāo)注的多GPU訓(xùn)練實現(xiàn)可生成對應(yīng)顯著圖像區(qū)域的輸出特征并作為注意力字幕與VQA模型中CNN特征的直接替代。壓縮包共981個文件、約14.27MB內(nèi)含較完整的Caffe源碼與配置包括cpp、hpp、prototxt、cu等訓(xùn)練和網(wǎng)絡(luò)定義文件也含Python腳本、Jupyter Notebook、預(yù)訓(xùn)練pt模型、Shell腳本及Markdown說明便于從數(shù)據(jù)接口、模型配置到實驗示例逐層查閱。已有753人學(xué)習(xí)或下載在相關(guān)研究中具備一定參考價值。讀者可據(jù)此復(fù)現(xiàn)多GPU訓(xùn)練流程理解自下而上注意力特征提取與Faster R-CNN配置方法并借助預(yù)訓(xùn)練權(quán)重開展圖像字幕和VQA實驗結(jié)合說明文檔梳理目錄結(jié)構(gòu)與關(guān)鍵模塊為二次開發(fā)和對照實驗提供基礎(chǔ)。1. 先說結(jié)論VQA 的瓶頸不在語言模型而在“看哪里”圖像問答VQA準(zhǔn)確率卡住的時候很多人的第一反應(yīng)是換更大的語言模型、加厚 LSTM、多訓(xùn)幾十輪。但在公開榜單上性價比最高的改動往往發(fā)生在輸入端把整張圖切成的 7×7 網(wǎng)格特征換成幾十個“物體級”區(qū)域向量每個區(qū)域一個 2048 維向量。語言模型一行不改準(zhǔn)確率就能抬一截。這就是自下而上注意力bottom-up attention解決的問題讓模型看到的不再是均分的格子而是“這張圖里到底有哪些東西”。這條路線最早被系統(tǒng)性驗證的載體是一套基于 Faster R-CNN 與 Visual Genome 預(yù)訓(xùn)練的區(qū)域特征抽取器外加配套的 image-captioning 與 VQA 下游代碼。搞懂它你就搞懂了今天多模態(tài)模型里“視覺 token”這個概念是從哪來的。2. 自下而上與自上而下兩種注意力到底差在哪2.1 網(wǎng)格特征的問題空間對齊不等于語義對齊卷積骨干網(wǎng)最后一層特征圖是固定尺寸的網(wǎng)格。輸入 600×800 的圖stride 16 之后大概得到 38×50 的響應(yīng)圖常見的做法是再池化成 7×7 或者 14×14每個格子對應(yīng)一個 2048 維向量。這套表示的優(yōu)點是規(guī)整所有圖片出來都是同樣的張量形狀接任何下游網(wǎng)絡(luò)都不用改接口。缺點是它和圖像內(nèi)容沒有關(guān)系——網(wǎng)格線是人為劃的物體邊界永遠(yuǎn)不會剛好落在格線上。后果有三個。第一一個格子常常橫跨物體和背景池化之后語義被稀釋第二小目標(biāo)可能整個落在半個格子里激活強(qiáng)度拼不過旁邊大片背景第三同一個格子里塞了兩個物體后續(xù)注意力再聰明也拆不開。語言模型只能在一堆已經(jīng)混了雜質(zhì)的向量里做加權(quán)上限被輸入卡死了。自上而下top-down注意力解決的是“解碼時看哪”它作用在已有的特征上自下而上bottom-up注意力解決的是“特征本身長什么樣”。前者是后者的消費(fèi)者不是替代品。很多實現(xiàn)把兩者疊在一起用效果才好。2.2 Faster R-CNN 在這里的角色不是檢測器是候選區(qū)域生成器這里最容易誤解的一點這套流程里的 Faster R-CNN 并不需要輸出“這是貓、那是狗”。下游的 image-captioning 和 VQA 只關(guān)心“這里有一塊值得看的東西”不關(guān)心它屬于哪一類。所以抽取特征時通常走 class-agnostic 模式把所有類別的框混在一起做非極大值抑制只保留位置和分?jǐn)?shù)。這個設(shè)計動機(jī)很實在。如果按類別篩選你就得為下游任務(wù)預(yù)設(shè)一個類別清單而 Visual Genome 里的問題涉及顏色、材質(zhì)、動作、關(guān)系很多根本不在 80 類檢測清單里。放棄類別等于把檢測頭“降級”成一個區(qū)域打分器換來的是召回率的提升——寧可多給幾個語義模糊的框也不要漏掉關(guān)鍵區(qū)域。結(jié)構(gòu)上它還是標(biāo)準(zhǔn)的兩階段RPN 在特征圖上滑窗產(chǎn)生候選RoI head 對每個候選做分類和回歸精修。區(qū)別只在于最后保留下來的東西不是類別標(biāo)簽而是一組歸一化坐標(biāo)加一個置信度分?jǐn)?shù)。2.3 Visual Genome 為什么必須出現(xiàn)屬性監(jiān)督把“物體”變成“概念”只在 80 類檢測數(shù)據(jù)集上訓(xùn)練的檢測器見過的概念太少。問到“那個人穿的紅衣服是什么材質(zhì)”模型可能連“紅衣服”這個區(qū)域都沒提出來。Visual Genome 的價值在這里體現(xiàn)它的標(biāo)注不只是框還有對象、屬性、關(guān)系三層。訓(xùn)練時把屬性也當(dāng)成可檢測的目標(biāo)模型學(xué)到的就不只是“人”這個類還有“穿紅衣服的人”“木制的桌子”這類組合概念。常見的實現(xiàn)思路是在 Visual Genome 上微調(diào)檢測器把對象類和屬性類一起放進(jìn)分類頭讓每個 proposal 的語義覆蓋更寬。圖片規(guī)模在十萬量級關(guān)系標(biāo)注在百萬量級這個體量足夠把骨干網(wǎng)的語義偏置調(diào)整過來。抽特征階段再把檢測器凍結(jié)只做前向因為下游任務(wù)的數(shù)據(jù)量和它不匹配——微調(diào)容易過擬合而且訓(xùn)練成本高一個數(shù)量級。這三個環(huán)節(jié)串起來才是完整的“自下而上”Visual Genome 提供概念廣度Faster R-CNN 提供區(qū)域定位class-agnostic 的輸出提供任務(wù)無關(guān)性。3. 一個區(qū)域怎么變成 2048 維向量從 ROI 池化到均值池化3.1 骨干網(wǎng) C5 層與 ROI 池化的分辨率選擇特征從哪一層取是一個容易被忽略但影響很大的決定。ResNet-101 的 conv5_3 輸出 stride 16、2048 通道語義最強(qiáng)但空間分辨率低conv4 階段 stride 8、1024 通道位置準(zhǔn)但語義弱。區(qū)域特征抽取一般選前者——因為下游要的是“這塊區(qū)域是什么”語義比像素級位置更重要。選定層之后對每個 proposal 做 ROI 池化把任意大小的框映射成固定尺寸的響應(yīng)塊常見是 7×7。這一步的關(guān)鍵參數(shù)是池化后的空間尺度7×7 保留了一點內(nèi)部布局信息14×14 更細(xì)但特征維度會翻四倍。實際工程里 7×7 是主流因為再往上對下游幾乎沒有增益只是白白占用顯存。抽取腳本的典型調(diào)用長這樣參數(shù)名對著倉庫里的 argparse 核一遍即可python tools/generate_tsv.py \ --cfg experiments/cfgs/faster_rcnn_end2end_resnet.yml \ --def models/vg/ResNet-101/faster_rcnn_end2end_final/test.prototxt \ --net data/faster_rcnn_models/resnet101_faster_rcnn_final.caffemodel \ --out data/genome/features_resnet101_genome \ --split train \ --max_boxes 100 \ # 每張圖最多保留多少個區(qū)域決定下游 K 的上限 --min_boxes 10 \ # 兜底分?jǐn)?shù)再低也要湊夠這么多個避免整圖無框 --conf_thresh 0.2 \ # 初始置信度門檻低于它的 proposal 直接丟 --nms_thresh 0.7 \ # 非極大值抑制的 IoU 閾值class-agnostic 時跨類合并 --batch_size 1 # prototxt 里 batch 維度通常寫死為 1別亂改--max_boxes和--min_boxes是一對不是隨便填的。上限決定下游注意力的候選池大小也決定特征文件體積下限是防御性的遇到純色背景、模糊圖這類檢測器信心普遍很低的場景兜底補(bǔ)幾個框總比返回空數(shù)組好——空數(shù)組會讓下游的 batch 組織直接崩掉。3.2 框的篩選置信度、NMS 與數(shù)量上限的聯(lián)動篩選順序是有講究的先按置信度卡閾值再按分?jǐn)?shù)排序做 NMS最后截斷到上限。順序反過來NMS 的復(fù)雜度會隨著低分框數(shù)量暴漲抽取速度直接掉一個檔次。NMS 閾值取 0.7 偏寬松這是刻意的。檢測任務(wù)里 0.5 左右更常見目的是去掉重復(fù)框但這里的目標(biāo)是給下游提供多樣化的候選區(qū)域兩個 IoU 0.6 的框可能分別覆蓋“人”和“人手里的杯子”壓掉一個就少了一個語義。寬松閾值的代價是框之間有冗余不過下游的注意力機(jī)制本來就擅長在冗余里挑重點。class-agnostic 模式下的 NMS 是跨類合并的這意味著同一個位置不會出現(xiàn)“既是桌子又是書”的兩個框。這個取舍換來的是框的數(shù)量可控——如果按類做 NMS一張圖很容易冒出幾百個框max_boxes截斷時反而會把真正重要的區(qū)域擠掉。3.3 從 flatten 到 mean為什么要做空間平均ROI 池化得到的是(2048, 7, 7)的張量。直接 flatten 是一萬維接全連接層參數(shù)爆炸而且 flatten 隱含了“左上角和右下角位置固定”的假設(shè)同一個物體稍微平移特征就完全變了這對下游沒有好處。主流做法是做空間維度的均值池化得到 2048 維。為什么是均值不是最大值最大池化只保留每個通道的最強(qiáng)響應(yīng)假如某個通道對“紅色”敏感那這塊區(qū)域里只要有一小塊紅整個向量就被打滿區(qū)域整體的信息被丟掉了。均值保留的是平均激活強(qiáng)度對區(qū)域大小和形狀更魯棒。也有實現(xiàn)用求和效果接近區(qū)別只在于數(shù)值尺度——求和會讓大框的向量模長明顯更大如果下游沒做歸一化注意力會天然偏向大物體。提示不少下游代碼會在讀入特征后做一次 L2 歸一化。這一步不是可選項尤其是不同來源的特征文件混用時尺度不一致會讓注意力權(quán)重完全跑偏。最終每個區(qū)域就是一個 2048 維向量加上 4 個歸一化坐標(biāo)外加一個可選的置信度分?jǐn)?shù)。3.4 特征落盤格式與讀取對齊抽取完的特征通常按 split 打包成一個壓縮的 npz鍵名固定下游只認(rèn)這幾個import numpy as np d np.load(train_resnet101_faster_rcnn_genome.npz) image_id d[image_id] # (N,) 原始圖片 id不是文件序號 num_boxes d[num_boxes] # (N,) 每張圖實際有效的框數(shù) boxes d[boxes] # (N, K, 4) 歸一化 xyxyK 是上限 features d[features] # (N, K, 2048) image_h d[image_h] # (N,) image_w d[image_w] # (N,) i 0 k int(num_boxes[i]) box boxes[i, :k] # 后面全是零填充必須按 num_boxes 切 feat features[i, :k] # 需要像素坐標(biāo)時再還原位置編碼、畫圖、裁圖都用得上 xyxy_pixel box * np.array([image_w[i], image_h[i], image_w[i], image_h[i]])三個細(xì)節(jié)決定這份文件好不好用。一是boxes歸一化到[0,1]并采用 xyxy 順序跨數(shù)據(jù)集混用時不用再關(guān)心原圖分辨率二是features按 K 補(bǔ)齊補(bǔ)齊位是零讀的時候必須靠num_boxes切掉否則零向量會參與注意力計算三是image_id必須和下游數(shù)據(jù)集使用的 id 體系一致這一步對不上后面全盤錯位而且錯得很隱蔽——loss 會正常下降只是學(xué)到的是噪聲。4. 同一份特征池怎么喂給字幕和 VQA 兩個任務(wù)4.1 圖像字幕top-down attention 每一步只看幾個區(qū)域image-captioning 的解碼器通常是兩層 LSTM。第一層叫 attention LSTM輸入是上一個詞、上一個隱狀態(tài)、以及上一時刻的上下文向量它的輸出用來對 K 個區(qū)域打分。第二層叫 language LSTM接收注意力加權(quán)后的上下文和第一層的隱狀態(tài)輸出當(dāng)前詞。為什么拆兩層而不是一層搞定生成一個詞要做好兩件不同的事——決定“現(xiàn)在該看圖的哪個部分”和“把看到的東西組織成語法正確的句子”。塞進(jìn)同一個隱狀態(tài)里兩件事互相干擾訓(xùn)練早期尤其明顯。拆開之后注意力層專心做區(qū)域選擇語言層專心做詞序收斂更穩(wěn)。import torch, torch.nn as nn class TopDownAttention(nn.Module): def __init__(self, feat_dim2048, hidden512, att_dim512): super().__init__() self.feat_proj nn.Linear(feat_dim, att_dim) # 區(qū)域側(cè)降維 self.hidden_proj nn.Linear(hidden, att_dim) # 解碼器隱狀態(tài)側(cè)降維 self.score nn.Linear(att_dim, 1) # 每個區(qū)域出一個標(biāo)量分 def forward(self, feats, h_t, mask): # feats: (B, K, 2048) h_t: (B, hidden) mask: (B, K)1 有效 0 填充 a torch.tanh(self.feat_proj(feats) self.hidden_proj(h_t).unsqueeze(1)) s self.score(a).squeeze(-1) # (B, K) s s.masked_fill(mask 0, -1e4) # 填充位必須打掉 alpha torch.softmax(s, dim-1) # (B, K) 注意力權(quán)重 ctx (feats * alpha.unsqueeze(-1)).sum(1) # (B, 2048) 上下文向量 return ctx, alphamask那一行是整段代碼里最不能省的。K 在 batch 內(nèi)是變長的有的圖 12 個框有的 87 個補(bǔ)齊到同一個上限之后如果不把填充位打掉softmax 會把概率分給一堆零向量上下文向量被稀釋而且稀釋程度隨 batch 內(nèi)最大 K 波動——同一張圖換個 batch 組合生成的字幕就不一樣這種玄學(xué) bug 極難排查。4.2 VQA把問題向量當(dāng)查詢做區(qū)域加權(quán)VQA 的融合方式比字幕更自由。常見做法是問題過一遍 GRU 或 LSTM 得到q每個區(qū)域特征v_i與q做一個聯(lián)合表示聯(lián)合表示過一個線性層得到標(biāo)量分?jǐn)?shù)softmax 歸一化后加權(quán)求和得到v_hat最后把[v_hat, q]拼接送進(jìn)分類頭輸出答案。聯(lián)合表示的選擇是有講究的。逐元素相乘element-wise product參數(shù)少、訓(xùn)練快但只能捕捉“同維度上的呼應(yīng)”雙線性池化bilinear pooling能建模二階交互表達(dá)力更強(qiáng)代價是參數(shù)量隨維度平方增長得靠低秩分解壓回來。實際項目里數(shù)據(jù)量小的時候用逐元素相乘更穩(wěn)數(shù)據(jù)量上來了再換雙線性收益才明顯。另一個容易忽略的點是答案空間。VQA 的答案本質(zhì)是一個開放詞表但實際訓(xùn)練會截斷到出現(xiàn)頻率最高的幾千個答案剩下的統(tǒng)統(tǒng)映射成“其他”。這個截斷閾值直接決定準(zhǔn)確率上限——閾值太低很多本該答對的被歸到“其他”太高長尾類別的樣本太少學(xué)不動。4.3 跨框架搬運(yùn)Caffe 抽特征、PyTorch 訓(xùn)下游這套資源的一個現(xiàn)實特征特征抽取用的是 Caffe模型定義是 prototxt權(quán)重是 caffemodel下游訓(xùn)練用的是 PyTorch。中間靠 npz 文件解耦。這個設(shè)計在當(dāng)時是無奈之舉——Visual Genome 上預(yù)訓(xùn)練好的檢測器權(quán)重是 Caffe 格式的重訓(xùn)一遍成本太高但下游研究早就轉(zhuǎn)移到 PyTorch 了。解耦帶來的好處比想象中大抽特征是一次性的離線任務(wù)跑完就固化下游可以反復(fù)改模型結(jié)構(gòu)、調(diào)超參不用每次重新過一遍檢測器。資源里配的 Jupyter Notebook 示例主要就是用來看數(shù)據(jù)、驗證對齊、畫區(qū)域框比直接看訓(xùn)練腳本直觀得多。需要注意的對齊點有三個。一是顏色通道Caffe 側(cè)通常按 BGR 讀圖并減均值PyTorch 側(cè)默認(rèn) RGB如果下游還要自己做圖像增廣得確認(rèn)增廣后的圖不會和特征錯位。二是輸入尺度檢測器對輸入圖的短邊做了固定縮放框坐標(biāo)歸一化之后這件事被隱藏了但如果你想把框映射回原圖裁圖就得自己還原。三是數(shù)值精度特征存成 float32 就夠轉(zhuǎn)成 float16 能省一半空間但注意力打分對精度敏感容易在小分?jǐn)?shù)差上翻車。5. 復(fù)現(xiàn)時的五個坑從坐標(biāo)歸一化到框架版本漂移5.1 現(xiàn)象訓(xùn)練 loss 正常下降但指標(biāo)比預(yù)期低一截原因幾乎總是特征和標(biāo)簽的讀取錯位。features和boxes是按 K 補(bǔ)齊的直接整體喂進(jìn)網(wǎng)絡(luò)零向量會參與注意力計算更隱蔽的情況是num_boxes用了 float 存儲轉(zhuǎn) int 時截斷方向不對少取了一個框。解決方式是寫一個斷言讀完之后立刻檢查features[i, :k].abs().sum() 0且features[i, k:].abs().sum() 0兩條都成立才說明切得對。這個檢查成本極低建議固化到 Dataset 的__init__里。5.2 現(xiàn)象少數(shù)圖片的特征全是零或者只有三五個框原因是篩選鏈條上多個閾值疊加。置信度門檻卡掉大部分 proposalNMS 又壓掉一批最后剩下的不夠min_boxes。如果兜底邏輯寫得不對比如兜底時忘了重置 NMS就會出現(xiàn)空結(jié)果。排查方法是先統(tǒng)計整個 split 的num_boxes分布畫個直方圖。正常情況應(yīng)該集中在幾十的量級如果出現(xiàn)明顯的零值尖峰就是篩選參數(shù)的問題。臨時把conf_thresh降到 0.05 再跑一遍對照能快速定位。5.3 現(xiàn)象評估指標(biāo)斷崖式下跌但訓(xùn)練過程毫無異常經(jīng)典的血淚經(jīng)驗image_id對不上。圖像數(shù)據(jù)集的 id 往往不是文件序號而是從文件名經(jīng)過某種映射得到的整數(shù)訓(xùn)練集和驗證集的文件排列順序也不一樣。如果按行號對齊訓(xùn)練時看著正常因為特征和標(biāo)簽都來自同一份錯位數(shù)據(jù)一到評估就全亂。解決辦法只有一個所有地方都以image_id為鍵建字典禁止用行號索引。寫數(shù)據(jù)加載時先做一次全量校驗確認(rèn)特征文件里的 id 集合和標(biāo)注文件里的 id 集合完全一致差一個都別往下跑。5.4 現(xiàn)象自己前向算出的特征和文件里的數(shù)值差一點點怎么都對不上這類偏差通常來自預(yù)處理。通道順序BGR/RGB、均值減法的方式逐通道減還是整體減、輸入縮放時短邊的目標(biāo)尺寸、以及 prototxt 里可能存在的輸入尺度因子任何一個不同都會讓數(shù)值漂移。排查要逐層來拿一張圖固定輸入把骨干網(wǎng)某一層的輸出打印出來和 Caffe 側(cè)同層輸出做逐元素對比。差在輸入層就是預(yù)處理問題差在中間層就是某次歸一化或激活的配置不同。別指望整體對比能看出原因那是黑匣子。5.5 現(xiàn)象抽取速度慢到不可接受或者顯存直接爆兩個獨立原因。速度慢多半是沒做批內(nèi)共享——同一張圖的所有 proposal 共享同一次全圖卷積如果實現(xiàn)里對每個 proposal 都重跑一遍骨干網(wǎng)耗時會翻幾十倍。顯存爆多半是batch_size調(diào)大了但 prototxt 里的輸入維度是寫死的 1改命令行參數(shù)沒用得改網(wǎng)絡(luò)定義。工程上的常規(guī)做法是batch_size保持 1用多進(jìn)程或多卡按圖片分片并行每個分片寫自己的輸出文件最后合并。這樣既繞開了 prototxt 的限制又能把 GPU 吃滿。6. 離線特征的取舍固定 K 與凍結(jié)骨干到底劃不劃算把特征抽出來存成文件本質(zhì)上是拿靈活性換效率。這筆賬算下來劃不劃算取決于你改模型的頻率和數(shù)據(jù)的規(guī)模。維度離線固定特征在線聯(lián)合訓(xùn)練訓(xùn)練速度快每輪只跑輕量下游網(wǎng)絡(luò)慢每步都要過檢測器顯存占用低特征可放內(nèi)存或本地盤高骨干網(wǎng)激活要留著反傳數(shù)據(jù)增廣幾乎沒法做圖已經(jīng)變成向量可以自由裁剪、翻轉(zhuǎn)、變色端到端優(yōu)化斷開檢測器和下游各自最優(yōu)連通理論上界更高復(fù)現(xiàn)成本低特征文件一次生成反復(fù)用高環(huán)境和隨機(jī)種子都要對齊固定 K 也是同一類取舍。自適應(yīng)的 10 到 100 個框更貼近內(nèi)容但 batch 內(nèi)變長意味著要么補(bǔ)齊浪費(fèi)算力要么按長度分桶增加實現(xiàn)復(fù)雜度。很多下游實現(xiàn)干脆統(tǒng)一取前 36 個按置信度排序換來的是張量形狀固定、代碼干凈、顯存可預(yù)測。代價是信息量被截斷——一張信息密集的圖可能真的需要八十個區(qū)域截到 36 個就丟了細(xì)節(jié)。這個損失在小分辨率圖上不明顯在高分辨率、多物體的場景里會體現(xiàn)出來。凍結(jié)骨干是第三個取舍點。檢測器在 Visual Genome 上學(xué)到的區(qū)域概念是通用的下游數(shù)據(jù)量通常在幾萬到十幾萬的量級微調(diào)一個上億參數(shù)的檢測器幾乎必然過擬合。但反過來說如果你的下游任務(wù)領(lǐng)域偏移很大比如醫(yī)學(xué)影像、衛(wèi)星圖通用檢測器提的框可能根本不落在關(guān)鍵區(qū)域上這時候不微調(diào)就是自縛手腳。判斷標(biāo)準(zhǔn)很樸素先抽一批特征可視化看看框落在哪如果框的位置和人類直覺一致凍結(jié)就行如果框總是飄在背景上那問題不在下游模型在抽取器。我踩過最疼的一次坑是拿到一份特征文件就直接開訓(xùn)訓(xùn)了三天指標(biāo)不動最后發(fā)現(xiàn)那份文件是用另一套 id 映射生成的訓(xùn)練集和驗證集的特征整體錯了位。從那以后我每次接入新的特征文件都強(qiáng)制走一遍三步校驗id 集合比對、num_boxes分布直方圖、隨機(jī)抽三張圖把框畫回原圖肉眼確認(rèn)。這三步加起來不到十分鐘能省掉三天。希望幫到你。本文還有配套的精品資源點擊獲取