別ReID實(shí)戰(zhàn):從工程結(jié)構(gòu)到模型訓(xùn)練全解析)
簡介行人重識(shí)別ReID是計(jì)算機(jī)視覺中的關(guān)鍵任務(wù)目標(biāo)是在不同攝像頭下準(zhǔn)確匹配同一行人。這套Python工程代碼包面向深度學(xué)習(xí)初學(xué)者和視覺研究者提供了從數(shù)據(jù)準(zhǔn)備到模型評(píng)估的完整實(shí)現(xiàn)覆蓋Market-1501等常用數(shù)據(jù)集的處理流程。壓縮包共11個(gè)文件全部為Python腳本大小約18KB包含數(shù)據(jù)加載與增強(qiáng)data_manager.py、dataset_loader.py、骨干網(wǎng)絡(luò)ResNet.py、損失函數(shù)losses.py、優(yōu)化器配置optimizers.py以及評(píng)估指標(biāo)eval_metrics.py等模塊結(jié)構(gòu)清晰適合按模塊逐步研讀便于定位數(shù)據(jù)、模型、優(yōu)化等核心邏輯。目前已有150人學(xué)習(xí)下載。通過分析源碼讀者可以掌握CNN提取行人外觀特征的方法理解triplet loss、center loss在度量學(xué)習(xí)中的作用并學(xué)會(huì)使用mAP、Rank-1等指標(biāo)評(píng)估模型。代碼簡潔可運(yùn)行兼顧理論講解與工程實(shí)踐是課程設(shè)計(jì)、論文復(fù)現(xiàn)或入門ReID研究的高性價(jià)比參考。1. 行人重識(shí)別是什么一個(gè) zip 里裝的是一整套跨鏡檢索系統(tǒng)在商超、園區(qū)、地鐵站跨攝像頭找人靠人眼回看錄像動(dòng)輒一兩個(gè)小時(shí)行人重識(shí)別Person Re-IdentificationReID就是讓深度學(xué)習(xí)模型學(xué)會(huì)“只看一次之后在幾百個(gè)陌生人里把 ta 找回來”。一個(gè)名為“基于深度學(xué)習(xí)的行人重識(shí)別.zip”的工程包解壓開通常不是一個(gè)訓(xùn)練好的模型文件而是一整套從數(shù)據(jù)集組織、訓(xùn)練到檢索評(píng)估的代碼管線這也是它和普通 demo 包最本質(zhì)的區(qū)別。給定一張 query 圖模型輸出一個(gè)能區(qū)分“同一個(gè)人在不同攝像頭下長什么樣”的特征向量再和全庫 gallery 特征算相似度并排序。適合正在做檢測(cè)/分類、想轉(zhuǎn)入檢索方向的工程師或研究生目標(biāo)是能復(fù)現(xiàn)、能改參、能部署。2. 先看懂工程結(jié)構(gòu)再動(dòng)手ReID 源碼包的標(biāo)準(zhǔn)套路與數(shù)據(jù)流拿到一個(gè) ReID 的 zip多數(shù)版本的目錄結(jié)構(gòu)都長得很像這要感謝 Market1501 和后來 BoT、TransReID 這些開源工程養(yǎng)成的社區(qū)習(xí)慣。先別急著跑 train.py花二十分鐘把目錄和數(shù)據(jù)流捋清楚后面能少踩一半坑。這個(gè)環(huán)節(jié)不依賴具體代碼風(fēng)格任何基于 PyTorch 的 ReID 實(shí)戰(zhàn)項(xiàng)目案例基本都跑不出下面這套約定。2.1 一個(gè)典型 ReID 工程有什么從 train.py 到 evaluate.py常見工程解壓后頂層會(huì)有 train.py、evaluate.py 兩個(gè)入口以及 scripts、configs、datasets、models、losses、samplers、utils 這幾個(gè)目錄。scripts 是啟動(dòng)用的 shell 腳本configs 是參數(shù)配置datasets 負(fù)責(zé)數(shù)據(jù)集加載和目錄校驗(yàn)models 是骨干網(wǎng)絡(luò)和脖子模塊losses 是交叉熵、三元組等損失定義samplers 是批次采樣器utils 里是日志和評(píng)估工具。train.py 負(fù)責(zé)訓(xùn)練evaluate.py 負(fù)責(zé)加載權(quán)重、抽取特征并計(jì)算 CMC/mAP。# 解壓并查看頂層結(jié)構(gòu)先確認(rèn)沒有嵌套目錄 unzip 基于深度學(xué)習(xí)的行人重識(shí)別.zip -d ReID cd ReID ls -R . | head -60 # 正常會(huì)看到 configs/ datasets/ models/ losses/ samplers/ utils/ train.py evaluate.py這段命令的作用是把工程解壓到 ReID 目錄并用 ls -R 遞歸列出結(jié)構(gòu)head -60 控制輸出長度。如果解壓出來帶中文目錄名或者多嵌套了一層同名文件夾先用 mv 把它提到頂層否則后面相對(duì)路徑會(huì)全部失效。多數(shù)工程用相對(duì)路徑讀數(shù)據(jù)集目錄層級(jí)錯(cuò)一位報(bào)錯(cuò)信息還不會(huì)直接指到路徑上而是先以“找不到圖片”的形式出現(xiàn)。在 config 或啟動(dòng)腳本里通常能看到這幾個(gè)核心參數(shù)數(shù)據(jù)根目錄 data_root、輸出目錄 save_dir、批次大小 batch_size、學(xué)習(xí)率 lr、訓(xùn)練輪數(shù) epochs、骨干網(wǎng)絡(luò)名稱 arch、損失類型 loss_type、設(shè)備 device。這些參數(shù)大多能從命令行覆蓋意味著不改代碼也能換配置。判斷一個(gè)工程是否成熟一是看 evaluate.py 里有沒有獨(dú)立的評(píng)估協(xié)議二是看 datasets 里有沒有針對(duì) Market1501 的標(biāo)準(zhǔn)目錄處理如果都不具備說明它只是一堆臨時(shí)腳本拼起來的復(fù)現(xiàn)價(jià)值有限別在它身上花太多時(shí)間。2.2 數(shù)據(jù)怎么流動(dòng)一張行人圖到特征向量的完整鏈路在 ReID 里一張行人圖從讀入磁盤到輸出特征向量經(jīng)過五個(gè)環(huán)節(jié)讀取圖片、數(shù)據(jù)增強(qiáng)、骨干網(wǎng)絡(luò)提取特征、降維層壓縮特征、L2 歸一化。訓(xùn)練階段在降維后還要掛分類頭和三元組損失推理階段則直接用歸一化后的特征做余弦相似度。下面這段代碼把五個(gè)環(huán)節(jié)拆開方便對(duì)照工程里每一步在做什么。# 特征提取流水線示意讀圖 - 增強(qiáng) - backbone - neck - L2歸一化 import torch import torchvision.transforms as T from PIL import Image # 1. 讀圖統(tǒng)一轉(zhuǎn) RGB img Image.open(query/0002_c1s1_000451_00.jpg).convert(RGB) # 2. 推理增強(qiáng)縮放、中心裁剪、歸一化訓(xùn)練還會(huì)加隨機(jī)擦除和翻轉(zhuǎn) transform T.Compose([ T.Resize([256, 128]), # ReID 通用輸入尺寸高 256寬 128 T.CenterCrop([256, 128]), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) x transform(img).unsqueeze(0) # [1, 3, 256, 128] # 3. backbone 輸出 2048 維全局池化特征 # 4. neck 降到 512并做 BN推理時(shí)用 BN 后的結(jié)果即可 # 5. 最后做 L2 歸一化后續(xù)算余弦相似度 feat torch.nn.functional.normalize(neck_feat, dim1)這段代碼是工程里推理部分的最小形態(tài)。Resize 到 [256, 128] 是 Market1501 上約定俗成的輸入比例因?yàn)樗ヅ湫腥耸蓍L的外形先驗(yàn)改成正方形會(huì)掉點(diǎn)。CenterCrop 在推理時(shí)是為了對(duì)齊訓(xùn)練時(shí)的隨機(jī)裁剪統(tǒng)計(jì)。歸一化沿用 ImageNet 的 mean 和 std幾乎所有 ReID 工程都直接使用不必自行調(diào)整。代碼里的 neck_feat 是占位寫法實(shí)際是 backbone 輸出經(jīng)過全局平均池化再過 BNNeck 的結(jié)果。實(shí)際工程里這五個(gè)環(huán)節(jié)會(huì)被封裝進(jìn) dataset 的getitem和 model 的 forwardDataLoader 的 num_workers 影響讀圖速度pin_memory 能減少數(shù)據(jù)從 CPU 拷貝到 GPU 的時(shí)間。調(diào)參時(shí)這些是常規(guī)項(xiàng)先確認(rèn)它們沒有問題再懷疑算法本身。2.3 為什么行人重識(shí)別是「分類 度量」的混合問題剛上手的人最容易問訓(xùn)練時(shí)明明用交叉熵把每個(gè)人 ID 當(dāng)成一個(gè)類別來分為什么推理時(shí)輸出的不是類別而是一個(gè)向量這正是 ReID 和圖像分類的關(guān)鍵區(qū)別。分類任務(wù)里一張圖對(duì)應(yīng)一個(gè)固定標(biāo)簽ReID 里訓(xùn)練集和測(cè)評(píng)集的人 ID 幾乎完全不相交模型必須學(xué)習(xí)“一個(gè)人的外觀特征如何跨攝像頭保持穩(wěn)定”這個(gè)能力只能靠度量學(xué)習(xí)來約束。常見做法是同時(shí)掛兩個(gè)頭一個(gè) ID 分類頭用交叉熵逼著特征具有類別判別力一個(gè)三元組損失用難樣本挖掘把同一 ID 的特征拉近、不同 ID 的特征推遠(yuǎn)。兩個(gè)頭共享 backbone 和 neck訓(xùn)練結(jié)束后丟掉分類頭只保留特征提取部分。一句話概括交叉熵教模型“見過誰”三元組教模型“誰和誰像”兩者缺一結(jié)果都有明顯短板——只有交叉熵難樣本區(qū)分不開只有三元組訓(xùn)練不穩(wěn)且收斂慢。在 zip 工程里判斷它做沒做度量學(xué)習(xí)最明顯的標(biāo)志是有沒有 sampler 目錄和 triplet loss 定義。只有分類頭、沒有采樣器的版本通常是從分類項(xiàng)目改過來的半成品。像動(dòng)手深度學(xué)習(xí)這類入門書里的分類代碼拿到 ReID 這里必須要補(bǔ)兩塊一個(gè)是難樣本采樣器一個(gè)是評(píng)估協(xié)議缺了它們訓(xùn)練出來的模型很難在跨攝像頭檢索上拿到可用指標(biāo)。3. 復(fù)現(xiàn)訓(xùn)練全流程從 Market1501 到跑通 train.py 的最小命令這章直奔“照著做能跑通”。深度學(xué)習(xí) PyTorch 生態(tài)里ReID 的復(fù)現(xiàn)難度主要不在網(wǎng)絡(luò)結(jié)構(gòu)而在數(shù)據(jù)劃分和訓(xùn)練參數(shù)對(duì)很多人來說這也是第一次理解“數(shù)據(jù)和指標(biāo)一起決定模型好壞”。這里說的“跑通”不只是 loss 在下降而是訓(xùn)練結(jié)束后 evaluate.py 能算出和論文同量級(jí)的 Rank-1 和 mAP。3.1 數(shù)據(jù)不“改壞”Market1501 的目錄約定Market1501 是 ReID 最常用的公開數(shù)據(jù)集包含 1501 個(gè)行人、32668 個(gè)檢測(cè)框采集自 6 個(gè)攝像頭。它發(fā)布時(shí)已經(jīng)按訓(xùn)練集、查詢集和候選集劃分好目錄名是 bounding_box_train、query 等。復(fù)現(xiàn)時(shí)最容易犯的錯(cuò)是把這些目錄重命名或者在訓(xùn)練集里混入 query。目錄結(jié)構(gòu)一旦改動(dòng)評(píng)估腳本按約定路徑找不到數(shù)據(jù)指標(biāo)直接崩掉。Market1501 的關(guān)鍵目錄及用途如下目錄/文件內(nèi)容本階段是否需要bounding_box_train訓(xùn)練集751 個(gè) ID必需query查詢集3368 張圖必需bounding_box_test候選集 gallery必需gt_queryquery 對(duì)應(yīng)的標(biāo)準(zhǔn)答案標(biāo)注re-ranking 時(shí)需要gt_bbox測(cè)試集行人框標(biāo)注復(fù)現(xiàn)論文對(duì)比時(shí)需要# 假設(shè)工程根目錄是 ReID把數(shù)據(jù)集整理成工程能認(rèn)的格式 cd ReID mkdir -p data/market1501 # 用軟鏈接而不是復(fù)制避免占雙倍硬盤也防止誤改原始文件 ln -s /path/to/Market1501/bounding_box_train data/market1501/bounding_box_train ln -s /path/to/Market1501/query data/market1501/query ln -s /path/to/Market1501/bounding_box_test data/market1501/bounding_box_test ln -s /path/to/Market1501/gt_query data/market1501/gt_query ln -s /path/to/Market1501/gt_bbox data/market1501/gt_bbox軟鏈接的 path 要根據(jù)實(shí)際解壓位置調(diào)整不要照抄。這里的關(guān)鍵是保持目錄名固定很多工程在 dataset 代碼里硬編碼了這些名字你改了它就得跟著改源碼。Market1501 的文件名本身帶標(biāo)簽比如 0002_c1s1_000451_00.jpg前四位是行人 IDc1s1 是攝像頭號(hào)和場(chǎng)景號(hào)。有經(jīng)驗(yàn)的人會(huì)用文件名前四位統(tǒng)計(jì)訓(xùn)練集 ID 數(shù)量如果比 751 少多半是解壓或移動(dòng)過程中丟了文件。3.2 環(huán)境配置與最小訓(xùn)練腳本在跑訓(xùn)練前先把環(huán)境確認(rèn)到位。ReID 對(duì)庫版本不挑剔但 torch 和 torchvision 的版本要配套CUDA 版本對(duì)應(yīng)錯(cuò)import torch 直接報(bào)錯(cuò)。如果你手頭只有普通臺(tái)式機(jī)或筆記本沒法用 GPU也可以把 device 改成 cpubatch_size 調(diào)到 8 試跑一個(gè) epoch能完整走通流程就算環(huán)境通過。# 創(chuàng)建虛擬環(huán)境并安裝依賴這是最省心的組合之一 conda create -n reid python3.8 -y conda activate reid pip install torch1.13.1 torchvision0.14.1 pip install numpy opencv-python pandas tqdm tensorboardtorch 1.13.1 配 torchvision 0.14.1 在多數(shù)顯卡驅(qū)動(dòng)下都能直接裝到對(duì)應(yīng) CUDA 版本不必追新。opencv 用來讀圖和做數(shù)據(jù)增強(qiáng)tqdm 顯示進(jìn)度tensorboard 看訓(xùn)練曲線后面排錯(cuò)會(huì)用到。深度學(xué)習(xí)環(huán)境配置到這里就夠了ReID 本身沒有額外依賴。訓(xùn)練腳本的主體邏輯在多數(shù)工程里一致構(gòu)建數(shù)據(jù)加載器、構(gòu)建模型、定義損失、循環(huán) epoch。下面這段是清洗后的核心流程和真實(shí)工程對(duì)照著看能快速定位自己改壞的地方。# 訓(xùn)練主流程核心片段P×K 采樣 ResNet50 BNNeck import torch from torch import nn from torch.utils.data import DataLoader # 假設(shè) dataset 已按 Market1501 讀入train_set 返回 img, pid, camid # P×K 采樣P 個(gè)身份每人 K 張圖batch P*K sampler RandomIdentitySampler(train_set, num_instances4) loader DataLoader(train_set, batch_size64, samplersampler, num_workers4, pin_memoryTrue) model build_model(archresnet50, num_classes751, last_stride1, neckbnneck) model.cuda() # 雙頭損失交叉熵 三元組Adam 時(shí) lr 建議從 3.5e-4 起步 criterion_id nn.CrossEntropyLoss() criterion_tri TripletLoss(margin0.3) optimizer torch.optim.Adam(model.parameters(), lr3.5e-4, weight_decay5e-4) lr_scheduler WarmupMultiStepLR(optimizer, milestones[40, 90], gamma0.1, warmup_epochs10) for epoch in range(120): model.train() for imgs, pids, _ in loader: imgs imgs.cuda() feats, logits model(imgs) # feats 給三元組logits 給分類頭 loss_id criterion_id(logits, pids) loss_tri criterion_tri(feats, pids) loss loss_id loss_tri optimizer.zero_grad() loss.backward() optimizer.step()這段代碼是 BoT 風(fēng)格的訓(xùn)練設(shè)置很多 zip 里的 train.py 就是它的完整版加命令行解析。batch_size 等于 64 時(shí)RandomIdentitySampler 默認(rèn)是 16 個(gè)身份乘每人 4 張圖保證每個(gè) batch 里同 ID 有足夠樣本供三元組挖掘。Adam 學(xué)習(xí)率 3.5e-4 配合 warmup前 10 個(gè) epoch 線性升溫第 40 和 90 epoch 衰減 0.1 倍。loss_id 和 loss_tri 直接相加是最常見的組合部分工程會(huì)乘 0.5 權(quán)重差別不大。這里有個(gè)容易忽略的細(xì)節(jié)last_stride1。ResNet50 默認(rèn)最后一級(jí)下采樣 stride 是 2ReID 為了保留更多空間細(xì)節(jié)會(huì)把它改成 1特征圖分辨率翻倍rank-1 通常能漲 2 到 3 個(gè)點(diǎn)。如果你的工程里沒這個(gè)參數(shù)訓(xùn)練一次后指標(biāo)偏低先檢查它。別讓 codex 跑深度學(xué)習(xí)模板代碼時(shí)替你悄悄把這類關(guān)鍵參數(shù)“優(yōu)化”掉ResNet50 的 last_stride、BNNeck、P×K 采樣這三個(gè)東西缺一個(gè)都算不上合格的 ReID 工程。3.3 訓(xùn)練中途看什么日志里 loss 和 Acc 的含金量訓(xùn)練日志通常長這樣“epoch 5, loss 3.21, acc 62.4, lr 0.00021”。新手最容易盯著 loss其實(shí)更該看定期在驗(yàn)證集上算出來的 Rank-1 和 mAP。交叉熵的 acc 反映分類頭學(xué)得怎么樣但 acc 高不代表檢索排序好因?yàn)闄z索要看候選集里的整體排序質(zhì)量。epoch 10 loss 1.98 acc 78.5 cmc:63.2/79.1 mAP:52.4 lr 0.00018 epoch 40 loss 0.42 acc 92.3 cmc:85.6/93.4 mAP:72.8 lr 0.00018 epoch 90 loss 0.18 acc 96.1 cmc:89.2/95.7 mAP:78.1 lr 0.000035這份日志是典型的收斂過程第 10 個(gè) epoch 時(shí) mAP 已過半說明骨架和采樣器接對(duì)了第 40 個(gè) epoch 時(shí) acc 超過 92但 CMC 還在漲說明度量頭仍有潛力第 90 個(gè) epoch 后學(xué)習(xí)率衰減兩個(gè)指標(biāo)繼續(xù)爬升。如果在第 40 個(gè) epoch 時(shí) mAP 還不到 50多半是數(shù)據(jù)加載錯(cuò)了或者采樣器沒生效而不是訓(xùn)練不夠。先看數(shù)據(jù)再調(diào)模型這比加訓(xùn)練輪數(shù)更管用。4. 模型選型與關(guān)鍵策略ResNet50 BNNeck 隨機(jī)擦除為什么是標(biāo)配看完訓(xùn)練流程自然會(huì)問這個(gè)工程里的模型結(jié)構(gòu)能不能換先說結(jié)論能換但得不償失的情況很多。在 ReID 這個(gè)方向模型選型和訓(xùn)練策略綁定得很緊深度學(xué)習(xí)模型那塊動(dòng)一個(gè)參數(shù)后面的指標(biāo)和顯存可能全變。4.1 三套主流骨架的取舍ResNet50、Swin/TransReID 與輕量網(wǎng)絡(luò)骨架參數(shù)量復(fù)現(xiàn)難度適用階段典型收益ResNet50約 25M低絕大多數(shù)工程默認(rèn)穩(wěn)定和論文易對(duì)齊ResNet50 IBN約 25M中跨數(shù)據(jù)集泛化域泛化更好Swin-T / ViT-Base約 28M/86M中高追求 SOTA指標(biāo)上線更高訓(xùn)練敏感MobileNetV3/輕量骨干約 7M中邊緣部署速度優(yōu)先精度下降可接受ResNet50 是 ReID 圈事實(shí)上的“工業(yè)標(biāo)準(zhǔn)”論文對(duì)比都在它上面跑復(fù)現(xiàn)時(shí)最容易對(duì)齊指標(biāo)。Swin 和 ViT 骨架在行人重識(shí)別上能到更高精度但對(duì)學(xué)習(xí)率、warmup、數(shù)據(jù)增強(qiáng)更敏感訓(xùn)練不穩(wěn)是常態(tài)新手拿它當(dāng)?shù)谝话嫒菀追嚒BN-Net 結(jié)構(gòu)在跨數(shù)據(jù)集測(cè)試時(shí)優(yōu)勢(shì)明顯如果你后續(xù)要在別的監(jiān)控場(chǎng)景上直接用它比原版 ResNet50 更值得。輕量網(wǎng)絡(luò)適合深度學(xué)習(xí)模型部署的場(chǎng)景。實(shí)時(shí)檢索系統(tǒng)里攝像頭數(shù)量多單卡推理壓力大MobileNetV3 能把單張圖的推理時(shí)間壓到幾毫秒代價(jià)是 Rank-1 降 5 到 8 個(gè)點(diǎn)。對(duì)工程落地來說這個(gè)差距通常能用 re-ranking 或更好的數(shù)據(jù)增強(qiáng)補(bǔ)回來一部分。選型沒有絕對(duì)答案但第一版復(fù)現(xiàn)我一般只會(huì)用 ResNet50因?yàn)樗炎兞靠刂频米钌佟?.2 BNNeck、隨機(jī)擦除和 P×K 采樣各自解決什么問題這三個(gè)名字在 ReID 論文里頻繁出現(xiàn)作用各不相同。BNNeck 解決的是交叉熵和三元組對(duì)特征分布要求不一致的問題交叉熵希望特征的范數(shù)大、類別邊界明顯三元組希望特征落在超球面上、分布緊湊。BNNeck 在 neck 輸出的兩個(gè)分支上分別做處理分類頭分支加 BN度量分支不加損失可以在各自舒適的分布上優(yōu)化。沒有它兩個(gè)損失會(huì)互相拉扯訓(xùn)練曲線震蕩明顯。隨機(jī)擦除Random Erasing是 ReID 里性價(jià)比最高的數(shù)據(jù)增強(qiáng)。行人經(jīng)常被遮擋或者被其他行人擋住隨機(jī)擦除一塊矩形區(qū)域讓模型不要把注意力全壓在某個(gè)局部部件上。它和普通分類任務(wù)里的 CutOut 類似但擦除比例和時(shí)間點(diǎn)對(duì) ReID 更敏感。P×K 采樣則是難樣本挖掘的基礎(chǔ)隨機(jī)采樣 P 個(gè)身份每個(gè)身份取 K 張圖這樣每個(gè) batch 內(nèi)部天然形成正負(fù)樣本對(duì)。下面是一段典型配置# 隨機(jī)擦除與 P×K 采樣的典型參數(shù) from random_erasing import RandomErasing train_transform T.Compose([ T.Resize([256, 128]), T.RandomCrop([256, 128], padding10), # 先隨機(jī)裁剪模擬位移 T.RandomHorizontalFlip(p0.5), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), RandomErasing(probability0.5, sh0.2, mean[0.4914, 0.4822, 0.4465]), ]) # 采樣器參數(shù)每 batch 16 個(gè)身份每個(gè)身份 4 張圖 sampler RandomIdentitySampler(dataset, num_instances4, batch_size64)擦除概率 0.5、遮擋面積比例上限 0.2這兩個(gè)數(shù)字是多數(shù)論文調(diào)試出來的穩(wěn)定區(qū)間。如果你碰到擦除后模型收斂變慢先降低 probability 而不是取消擦除取消后 Rank-1 通常掉 2 個(gè)點(diǎn)左右。RandomCrop 的 padding10 也很關(guān)鍵它給模型提供輕微位移不變性配合擦除能明顯提升跨攝像頭魯棒性。4.3 數(shù)據(jù)并行與 batch size 的關(guān)系batch size 在 ReID 里不是簡單的顯存問題它直接影響采樣質(zhì)量。P×K 采樣下batch size 是 P 和 K 的乘積64 的 batch 對(duì)應(yīng) 16 個(gè)身份、每個(gè)身份 4 張圖。如果把 batch size 降到 32常見操作是改成 8 個(gè)身份乘每人 4 張但 8 個(gè)身份會(huì)減少難樣本挖掘的覆蓋面指標(biāo)在訓(xùn)練中期就能看出差距。顯存不足時(shí)優(yōu)先降 K 而不是降 P比如 16 個(gè)身份乘每人 2 張batch 還是 32身份覆蓋面保住三元組只損失一點(diǎn)。# 單卡顯存不夠時(shí)的啟動(dòng)參數(shù)示例 python train.py --batch-size 32 --num-instances 2 --lr 3.5e-4 # 多卡時(shí)先用 torchrun 指定可用 GPU再按卡數(shù)等比放大 batch torchrun --nproc_per_node2 train.py --batch-size 128 --lr 6e-4多卡訓(xùn)練時(shí) batch size 翻倍學(xué)習(xí)率也要相應(yīng)調(diào)大常見做法是線性縮放。兩張卡從 64 變 128學(xué)習(xí)率從 3.5e-4 調(diào)到 6e-4不是簡單翻倍具體倍數(shù)靠驗(yàn)證集微調(diào)。數(shù)據(jù)并行時(shí)注意每個(gè) GPU 上的 batch 要獨(dú)立做 P×K 采樣不少工程在分布式改造時(shí)把采樣器丟了默認(rèn)隨機(jī)指標(biāo)掉得莫名其妙。5. ReID 復(fù)現(xiàn)避坑指標(biāo)對(duì)不上、顯存不足和數(shù)據(jù)集損壞的 4 個(gè)真實(shí)問題這一章是給“訓(xùn)練能跑但結(jié)果不對(duì)勁”的人準(zhǔn)備的。下面的問題我在不同工程里見過多次每條都按現(xiàn)象、原因、解決的順序?qū)懣梢灾苯诱罩挪椤?.1 坑zip 解壓后路徑亂套訓(xùn)練一啟動(dòng)就報(bào)“找不到文件”現(xiàn)象解壓、整理目錄后運(yùn)行 train.py報(bào)錯(cuò) FileNotFoundError但路徑看起來是對(duì)的或者在 windows 上解壓后訓(xùn)練正常換到 Linux 上同一份代碼報(bào)錯(cuò)。原因這個(gè) zip 工程在壓縮時(shí)可能帶了中文目錄名或者內(nèi)部多嵌套了一層文件夾。Windows 上路徑大小寫不敏感掩蓋了問題Linux 上目錄里文件名大小寫不一致直接失效。還有一部分工程的 config 里寫的是相對(duì)路徑從不同目錄啟動(dòng) train.py 結(jié)果完全不同。解決解壓后第一件事是 cd 進(jìn)工程目錄執(zhí)行 ls確認(rèn)數(shù)據(jù)目錄真實(shí)位置然后用軟鏈接統(tǒng)一指向原始 Market1501不要復(fù)制重命名。啟動(dòng)命令固定從工程根目錄執(zhí)行不要用 python /path/to/train.py 這種跨目錄方式。如果數(shù)據(jù)文件名有中文或空格先用 rename 批量清掉ReID 代碼普遍不做中文路徑處理。5.2 坑訓(xùn)練 loss 正常下降但 Rank-1 和論文差 10 個(gè)點(diǎn)以上現(xiàn)象訓(xùn)練日志里 loss 從 4 降到 0.3acc 到 95 以上但 evaluate.py 算出來的 Rank-1 只有 70 出頭論文里同配置是 89 以上。原因最大的可能性是評(píng)估階段和訓(xùn)練階段數(shù)據(jù)預(yù)處理不一致。訓(xùn)練用隨機(jī)裁剪、隨機(jī)翻轉(zhuǎn)、隨機(jī)擦除評(píng)估應(yīng)該只用 Resize 加 CenterCrop。如果評(píng)估代碼里帶了 RandomCrop 或漏了 CenterCrop特征分布對(duì)不上指標(biāo)直接崩。另一個(gè)常見原因是 query 和 gallery 劃分錯(cuò)了有些工程會(huì)把 query 圖片也塞進(jìn) gallery檢索時(shí)自己檢索自己mAP 虛高但 Rank-1 偏低兩者矛盾時(shí)基本就是數(shù)據(jù)劃分問題。解決先固定隨機(jī)種子然后把 evaluate.py 里的預(yù)處理和訓(xùn)練預(yù)處理逐行對(duì)比。再檢查評(píng)估代碼里是否在初始化 dataset 時(shí)用了 train 模式有些 dataset 類會(huì)在 mode 為 train 時(shí)自動(dòng)加隨機(jī)增強(qiáng)。最后統(tǒng)計(jì) query 和 gallery 是否有重疊文件名有就說明劃分腳本有問題回去看數(shù)據(jù)準(zhǔn)備那步。5.3 坑batch size 調(diào)小后反而不收斂或者收斂極慢現(xiàn)象顯存不夠把 batch_size 從 64 調(diào)到 24 或 16結(jié)果訓(xùn)練 30 個(gè) epoch 后 loss 還在 2 以上mAP 不到 30。原因ReID 的 batch 不適合直接“整體縮放”。batch_size64 對(duì)應(yīng) 16 個(gè)身份乘 4 張圖縮到 24 如果還是隨機(jī)采樣成 24 個(gè)不同身份每個(gè)身份只有 1 張圖三元組完全失效BN 層的統(tǒng)計(jì)量也因?yàn)?batch 太小而漂移。這屬于典型的“參數(shù)改了但沒改配套結(jié)構(gòu)”。解決如果顯存只夠 batch 24改成 8 個(gè)身份乘每人 3 張圖至少保證每個(gè)身份有 3 張正樣本。同時(shí)把 BN 層改為凍結(jié)狀態(tài)或使用較小的 batch norm momentum比如 momentum 從 0.1 改成 0.01。再配合梯度累積每 2 個(gè) batch 更新一次參數(shù)等效 batch 48這樣既保住采樣結(jié)構(gòu)又不至于讓 BN 統(tǒng)計(jì)量亂跳。5.4 坑GPU 顯存直接 OOM連一個(gè) epoch 都跑不完現(xiàn)象啟動(dòng)訓(xùn)練后幾秒鐘報(bào) CUDA out of memory然后進(jìn)程退出。有些人把 batch_size 調(diào)到 4 能跑但訓(xùn)練完全失去意義。原因顯存爆掉不一定全是 batch size 的鍋。輸入圖片尺寸如果從 [256,128] 被改成 [384,192]顯存占用翻 2.25 倍backbone 如果用 ResNet50 的 last_stride2特征圖尺寸比 last_stride1 小一半占用更少但丟細(xì)節(jié)。還有一個(gè)隱藏項(xiàng)是 DataLoader 的 num_workers 開太多每多一個(gè) worker 都會(huì)復(fù)制數(shù)據(jù)到共享內(nèi)存峰值顯存看似不變但內(nèi)存帶寬吃緊。解決先看這個(gè) zip 工程里默認(rèn)的 Resize 是不是被改成了大尺寸統(tǒng)一回到 [256,128]。再確認(rèn) last_stride 是否等于 1它影響顯存和精度的平衡。顯存仍不夠時(shí)用梯度累積替代調(diào)小 batch或者開 torch.cuda.amp 混合精度顯存能降一半ReID 訓(xùn)練對(duì) fp16 的敏感度比檢測(cè)低得多基本可以無痛用上。6. 驗(yàn)證與進(jìn)階用一張 query 手工走一遍檢索再談值不值得做訓(xùn)練完不是終點(diǎn)你得親眼看到檢索結(jié)果才敢把它交給下游。這一章只提供一個(gè)最小驗(yàn)證鏈路以及這條路繼續(xù)往深走的幾個(gè)方向。6.1 手工驗(yàn)證加載權(quán)重、抽特征、算余弦相似度用 evaluate.py 能看到整體指標(biāo)但看單張圖的檢索排序能更直觀地判斷模型學(xué)到的是“外觀”還是“某個(gè)攝像頭下的背景”。手工驗(yàn)證只需要加載模型、抽 query 特征、遍歷 gallery 算相似度。# 手工檢索驗(yàn)證輸出前 5 個(gè)結(jié)果的文件名 import torch import torch.nn.functional as F model.eval() with torch.no_grad(): q_feat F.normalize(model(transform_query(img_query).cuda())[1], dim1) g_feats [] # gallery 特征列表由 evaluate 階段緩存得到 names [] # 對(duì)應(yīng)的文件名 for g_img, name in gallery_loader: f F.normalize(model(g_img.cuda())[1], dim1) g_feats.append(f); names.extend(name) g_feats torch.cat(g_feats, dim0) sims torch.mm(q_feat, g_feats.t())[0] # 余弦相似度矩陣 top5 torch.topk(sims, 5).indices.tolist() print([names[i] for i in top5])這段代碼里 model 的輸出取索引 [1]是因?yàn)榍跋蚍祷亓?(feats, logits) 或 (feats, pooled_feats) 兩個(gè)值具體取哪個(gè)看工程定義。驗(yàn)證時(shí)用 L2 歸一化后的特征做矩陣乘法等價(jià)于余弦相似度。如果 top5 里出現(xiàn)了和 query 同一個(gè) ID 但不同攝像頭的結(jié)果說明模型學(xué)到了跨攝像頭不變性如果 top5 全是同一個(gè)攝像頭下的圖大概率模型偷懶了它學(xué)到的是環(huán)境背景不是行人本身。6.2 進(jìn)階方向re-ranking、多尺度測(cè)試與部署形態(tài)這個(gè)小流程跑通后你可以按預(yù)算選進(jìn)階方向。re-ranking 是性價(jià)比最高的一項(xiàng)它把 gallery 內(nèi)部的結(jié)構(gòu)關(guān)系考慮進(jìn)去通常能再漲 2 到 5 個(gè)點(diǎn) mAP但推理時(shí)要把整個(gè) gallery 特征送入內(nèi)存計(jì)算適合離線檢索場(chǎng)景。多尺度測(cè)試是另一個(gè)穩(wěn)定技巧把同一張圖縮成 192x96、256x128、320x160 三份分別抽特征再拼接漲點(diǎn)不多但穩(wěn)定幾乎不花訓(xùn)練成本。更接近交付形態(tài)的做法是把特征抽取封裝成 ONNX 或 TensorRT 的部署接口前接檢測(cè)模型后接向量數(shù)據(jù)庫做近似最近鄰檢索。ReID 模型本身不復(fù)雜但工程鏈路比訓(xùn)練長得多。我第一次完整跑通這套鏈路時(shí)最深的教訓(xùn)是不要在訓(xùn)練腳本里省評(píng)估代碼訓(xùn)練和評(píng)估的預(yù)處理、數(shù)據(jù)劃分必須同一個(gè)來源否則指標(biāo)永遠(yuǎn)是自欺欺人。先把這套手工驗(yàn)證跑通再談優(yōu)化這條路值得投入它能幫你把零散的檢測(cè)和目標(biāo)跟蹤串成真正能用的跨鏡檢索系統(tǒng)。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取