境搭建與數(shù)據(jù)加載實(shí)戰(zhàn):從零跑通3D姿態(tài)估計(jì))
簡(jiǎn)介這份資源面向計(jì)算機(jī)視覺(jué)與機(jī)器學(xué)習(xí)方向的開(kāi)發(fā)者尤其是正在入門或?qū)嵺`3D人體姿態(tài)估計(jì)的研究者與學(xué)生圍繞Human3.6M這一標(biāo)準(zhǔn)數(shù)據(jù)集提供用Python獲取與處理數(shù)據(jù)的完整工程代碼。壓縮包共14個(gè)文件約34KB以4個(gè)Python腳本為核心覆蓋數(shù)據(jù)下載、處理與提取等環(huán)節(jié)另含txt校驗(yàn)與依賴說(shuō)明、Dockerfile與docker-compose.yml容器配置、yml、xml元數(shù)據(jù)、md說(shuō)明文檔及l(fā)icense等便于快速搭建可復(fù)現(xiàn)的運(yùn)行環(huán)境。資源已有3634人學(xué)習(xí)下載熱度較高。讀者可借助腳本完成HDF5數(shù)據(jù)讀取、3D關(guān)節(jié)位置提取、格式轉(zhuǎn)換與標(biāo)準(zhǔn)化并參考說(shuō)明進(jìn)行訓(xùn)練集、驗(yàn)證集、測(cè)試集劃分為CNN、RNN或seq2seq等姿態(tài)估計(jì)模型的構(gòu)建、訓(xùn)練與評(píng)估打好數(shù)據(jù)基礎(chǔ)同時(shí)理解多視角、多動(dòng)作數(shù)據(jù)的組織方式與工程化處理思路。1. 從一次數(shù)據(jù)加載失敗說(shuō)起Human3.6M 環(huán)境搭建到底卡在哪第一次在實(shí)驗(yàn)室服務(wù)器上跑 Human3.6M 的姿態(tài)估計(jì)訓(xùn)練我信心滿滿地裝完 PyTorch結(jié)果卡在了數(shù)據(jù)加載這一步。報(bào)錯(cuò)信息翻來(lái)覆去就那幾句找不到h36m目錄、.npz文件解析失敗、torch.utils.data的collate_fn直接拋異常。折騰了大半天才反應(yīng)過(guò)來(lái)——問(wèn)題根本不在模型代碼而是這套數(shù)據(jù)集的環(huán)境搭建本身就有一堆前置條件目錄結(jié)構(gòu)要對(duì)、標(biāo)注文件要轉(zhuǎn)格式、Python 版本和依賴庫(kù)版本還得對(duì)得上。Human3.6M 是 3D 人體姿態(tài)估計(jì)領(lǐng)域最常用的基準(zhǔn)數(shù)據(jù)集之一做機(jī)器學(xué)習(xí)方向的同學(xué)幾乎繞不開(kāi)它但它的原始數(shù)據(jù)格式和主流框架之間隔著一層不薄的適配工作。這篇筆記記錄的就是我從零搭好這套環(huán)境、跑通第一個(gè)數(shù)據(jù)加載腳本的完整過(guò)程適合剛接觸 3D 姿態(tài)估計(jì)、正準(zhǔn)備在本地或服務(wù)器上部署 Human3.6M 的開(kāi)發(fā)者。2. 環(huán)境準(zhǔn)備Python 版本、依賴庫(kù)與目錄結(jié)構(gòu)2.1 版本選擇與依賴清單Human3.6M 本身是 MATLAB 格式的標(biāo)注文件Python 這邊要讀它核心依賴是h5py和numpy。但真正決定你能不能跑通的是深度學(xué)習(xí)框架的版本。我試過(guò)三種組合最后穩(wěn)定下來(lái)的是 Python 3.8 PyTorch 1.10 CUDA 11.3。Python 3.9 以上在裝h5py時(shí)偶爾會(huì)遇到 wheel 不匹配的問(wèn)題Python 3.7 又太老部分新版的numpy已經(jīng)不支持了。下面是我實(shí)際用的依賴清單直接寫進(jìn)requirements.txt# requirements.txt numpy1.21.6 h5py3.6.0 torch1.10.0 torchvision0.11.1 matplotlib3.5.3 tqdm4.64.1numpy鎖在 1.21.6 是因?yàn)?1.22 之后np.float被徹底移除而 Human3.6M 的一些舊版處理腳本里還在用這個(gè)別名。h5py3.6.0 是我測(cè)下來(lái)讀寫.h5標(biāo)注文件最穩(wěn)的版本再新的版本在讀取某些舊格式的 Human3.6M 標(biāo)注時(shí)會(huì)報(bào)OSError: Unable to open file。torch和torchvision的版本要對(duì)應(yīng)1.10 配 0.11.1 是官方驗(yàn)證過(guò)的組合。安裝命令# 創(chuàng)建虛擬環(huán)境避免污染系統(tǒng) Python python3.8 -m venv h36m_env source h36m_env/bin/activate # 安裝 PyTorchCUDA 11.3 版本 pip install torch1.10.0cu113 torchvision0.11.1cu113 -f https://download.pytorch.org/whl/torch_stable.html # 安裝其余依賴 pip install -r requirements.txt這里有個(gè)細(xì)節(jié)PyTorch 的 CUDA 版本要和服務(wù)器驅(qū)動(dòng)匹配。我用的服務(wù)器驅(qū)動(dòng)是 465.19支持 CUDA 11.3所以選了cu113的 wheel。如果你的驅(qū)動(dòng)版本更低比如 450 系列那就得降到 CUDA 11.0 對(duì)應(yīng)的 PyTorch 1.7.1。查驅(qū)動(dòng)版本用nvidia-smi右上角那行CUDA Version就是上限。2.2 目錄結(jié)構(gòu)規(guī)劃Human3.6M 原始數(shù)據(jù)集下載下來(lái)是一堆壓縮包解壓后目錄很亂。我一般會(huì)先規(guī)劃好一個(gè)固定的目錄結(jié)構(gòu)后面寫 DataLoader 的時(shí)候直接按這個(gè)路徑來(lái)省得每次改代碼# 我用的目錄結(jié)構(gòu) data/ ├── h36m/ │ ├── annotations/ │ │ ├── h36m_train.npz │ │ └── h36m_test.npz │ ├── images/ │ │ ├── s_01_act_02_subact_01_ca_01/ │ │ ├── s_01_act_02_subact_01_ca_02/ │ │ └── ... │ └── metadata.xml └── scripts/ ├── prepare_data.py └── dataset.pyannotations放轉(zhuǎn)換后的.npz文件images放原始圖像序列metadata.xml是數(shù)據(jù)集自帶的相機(jī)參數(shù)和動(dòng)作標(biāo)簽。這個(gè)結(jié)構(gòu)不是官方強(qiáng)制的但社區(qū)里大部分開(kāi)源項(xiàng)目的 DataLoader 都默認(rèn)按這個(gè)來(lái)比如VideoPose3D和MotionBERT的預(yù)處理腳本。提前對(duì)齊能省掉后面改路徑的麻煩。創(chuàng)建目錄的命令mkdir -p data/h36m/annotations data/h36m/images data/scripts提示如果你的數(shù)據(jù)盤空間緊張images目錄可以放到掛載的大容量盤上然后用軟鏈接鏈回來(lái)。Human3.6M 的原始圖像有幾十 GB全量下載前先確認(rèn)磁盤余量。2.3 驗(yàn)證 Python 環(huán)境是否就緒裝完依賴別急著跑數(shù)據(jù)先驗(yàn)證一下核心庫(kù)能不能正常導(dǎo)入# check_env.py import numpy as np import h5py import torch print(fnumpy version: {np.__version__}) print(fh5py version: {h5py.__version__}) print(ftorch version: {torch.__version__}) print(fCUDA available: {torch.cuda.is_available()}) print(fCUDA device: {torch.cuda.get_device_name(0) if torch.cuda.is_available() else CPU only})跑python check_env.py如果CUDA available輸出True說(shuō)明 PyTorch 和顯卡驅(qū)動(dòng)對(duì)上了。如果輸出False先別往下走回去檢查 CUDA 版本和驅(qū)動(dòng)版本是否匹配。這一步偷懶后面訓(xùn)練時(shí)會(huì)在model.cuda()那里直接崩掉。3. 數(shù)據(jù)準(zhǔn)備從原始標(biāo)注到可加載的 npz 文件3.1 原始數(shù)據(jù)下載與解壓Human3.6M 的原始數(shù)據(jù)需要從官方渠道獲取下載下來(lái)是多個(gè)壓縮包按受試者subject和動(dòng)作action分卷。我一般會(huì)先下載標(biāo)注文件和幾個(gè)受試者的圖像數(shù)據(jù)做測(cè)試不用一上來(lái)就拉全量。標(biāo)注文件是h36m_annotations.rar或類似的壓縮包解壓后得到.h5文件和metadata.xml。解壓命令# 假設(shè)下載的壓縮包放在 ~/Downloads 下 cd ~/Downloads unzip h36m_annotations.zip -d ./h36m_raw # 如果壓縮包是分卷的先合并再解壓 cat h36m_images_part_* h36m_images_full.zip unzip h36m_images_full.zip -d ./h36m_raw/images解壓后檢查一下文件完整性標(biāo)注文件應(yīng)該有h36m_train.h5和h36m_test.h5兩個(gè)圖像目錄下每個(gè)動(dòng)作序列對(duì)應(yīng)一個(gè)文件夾里面是.jpg格式的幀圖像。3.2 標(biāo)注文件格式轉(zhuǎn)換原始.h5文件的結(jié)構(gòu)比較繞直接讀也能讀但寫 DataLoader 的時(shí)候不方便。我一般會(huì)先轉(zhuǎn)成.npz格式把關(guān)鍵字段提取出來(lái)# prepare_data.py import h5py import numpy as np import os def convert_h36m_annotations(h5_path, output_path): 將 Human3.6M 的 h5 標(biāo)注文件轉(zhuǎn)換為 npz 格式 提取 3D 關(guān)節(jié)點(diǎn)坐標(biāo)、2D 投影坐標(biāo)和相機(jī)參數(shù) with h5py.File(h5_path, r) as f: # 遍歷所有受試者和動(dòng)作 for subject in f.keys(): for action in f[subject].keys(): # 3D 關(guān)節(jié)點(diǎn)坐標(biāo)形狀為 (幀數(shù), 32, 3) poses_3d f[subject][action][3d_joints][:] # 2D 投影坐標(biāo)形狀為 (幀數(shù), 32, 2) poses_2d f[subject][action][2d_joints][:] # 相機(jī)外參 camera_param f[subject][action][camera_param][:] # 保存為 npz每個(gè)動(dòng)作序列一個(gè)文件 save_name f{subject}_{action}.npz np.savez_compressed( os.path.join(output_path, save_name), poses_3dposes_3d, poses_2dposes_2d, camera_paramcamera_param ) print(fSaved {save_name}, 3D shape: {poses_3d.shape}) if __name__ __main__: convert_h36m_annotations( h5_pathdata/h36m/annotations/h36m_train.h5, output_pathdata/h36m/annotations/ )這段代碼的核心邏輯是遍歷.h5文件里的層級(jí)結(jié)構(gòu)把每個(gè)動(dòng)作序列的 3D 坐標(biāo)、2D 坐標(biāo)和相機(jī)參數(shù)分別提取出來(lái)存成獨(dú)立的.npz文件。poses_3d的形狀是(幀數(shù), 32, 3)32 是關(guān)節(jié)點(diǎn)數(shù)量3 是 XYZ 坐標(biāo)。poses_2d是投影到圖像平面的坐標(biāo)訓(xùn)練 2D 到 3D 的提升網(wǎng)絡(luò)時(shí)會(huì)用到。camera_param包含相機(jī)的內(nèi)外參數(shù)做多視角融合或者坐標(biāo)變換時(shí)要用。注意不同版本的 Human3.6M 標(biāo)注文件里字段名可能不一樣有的用3d_joints有的用S。跑之前先用h5py打開(kāi)文件看一眼結(jié)構(gòu)f[subject][action].keys()打印出來(lái)確認(rèn)字段名。3.3 寫一個(gè)可復(fù)用的 Dataset 類轉(zhuǎn)換完數(shù)據(jù)下一步是寫 PyTorch 的Dataset類。這個(gè)類要能按索引讀取單個(gè)樣本返回 3D 坐標(biāo)和對(duì)應(yīng)的 2D 坐標(biāo)# dataset.py import numpy as np import torch from torch.utils.data import Dataset import os class Human36MDataset(Dataset): def __init__(self, data_dir, splittrain, seq_len1): data_dir: npz 文件所在目錄 split: train 或 test seq_len: 每個(gè)樣本包含的連續(xù)幀數(shù) self.data_dir data_dir self.seq_len seq_len self.file_list sorted([ f for f in os.listdir(data_dir) if f.endswith(.npz) and split in f ]) # 預(yù)加載所有數(shù)據(jù)到內(nèi)存數(shù)據(jù)量不大時(shí)可行 self.data [] for fname in self.file_list: npz np.load(os.path.join(data_dir, fname)) self.data.append({ poses_3d: npz[poses_3d], poses_2d: npz[poses_2d], camera_param: npz[camera_param] }) def __len__(self): return len(self.data) def __getitem__(self, idx): sample self.data[idx] poses_3d torch.from_numpy(sample[poses_3d]).float() poses_2d torch.from_numpy(sample[poses_2d]).float() return { poses_3d: poses_3d, poses_2d: poses_2d }__init__里把每個(gè).npz文件加載成一個(gè)字典存到self.data列表里。__getitem__返回單個(gè)樣本的 3D 和 2D 坐標(biāo)轉(zhuǎn)成torch.float32張量。這里沒(méi)有做序列切分如果要做時(shí)序模型可以在__getitem__里按seq_len截取連續(xù)幀。測(cè)試 Dataset 是否能正常加載from dataset import Human36MDataset ds Human36MDataset(data_dirdata/h36m/annotations/, splittrain) print(fDataset size: {len(ds)}) sample ds[0] print(f3D shape: {sample[poses_3d].shape}) print(f2D shape: {sample[poses_2d].shape})如果輸出3D shape: torch.Size([幀數(shù), 32, 3])說(shuō)明數(shù)據(jù)加載鏈路已經(jīng)通了。4. 避坑記錄Human3.6M 環(huán)境搭建中最容易翻車的五個(gè)點(diǎn)4.1 h5py 讀取報(bào) OSError: Unable to open file現(xiàn)象用h5py.File(h36m_train.h5, r)打開(kāi)標(biāo)注文件時(shí)直接拋OSError文件路徑?jīng)]問(wèn)題文件大小也正常。原因h5py3.7 及以上版本對(duì)某些舊版 HDF5 格式的兼容性變差了Human3.6M 的標(biāo)注文件恰好是用舊版 HDF5 庫(kù)生成的。解決降級(jí)到h5py3.6.0命令是pip install h5py3.6.0。如果已經(jīng)裝了新版先pip uninstall h5py再裝。裝完用python -c import h5py; print(h5py.__version__)確認(rèn)版本。4.2 numpy 版本過(guò)高導(dǎo)致 np.float 報(bào)錯(cuò)現(xiàn)象跑數(shù)據(jù)預(yù)處理腳本時(shí)報(bào)AttributeError: module numpy has no attribute float。原因numpy1.22 開(kāi)始移除了np.float、np.int這些別名但 Human3.6M 的一些社區(qū)處理腳本還在用。解決把numpy鎖在 1.21.6或者手動(dòng)把腳本里的np.float改成np.float64。我一般選前者因?yàn)楦哪_本容易漏。命令是pip install numpy1.21.6。4.3 CUDA 版本和 PyTorch 不匹配現(xiàn)象torch.cuda.is_available()返回False但nvidia-smi能看到顯卡。原因裝的 PyTorch wheel 是 CPU 版本或者 CUDA 版本和驅(qū)動(dòng)不兼容。解決先用nvidia-smi查驅(qū)動(dòng)支持的 CUDA 上限然后去 PyTorch 官網(wǎng)找對(duì)應(yīng)版本的安裝命令。比如驅(qū)動(dòng)支持 CUDA 11.3就裝torch1.10.0cu113。裝完在 Python 里跑import torch; print(torch.version.cuda)確認(rèn)。4.4 圖像路徑和標(biāo)注文件對(duì)不上現(xiàn)象DataLoader 能加載標(biāo)注但讀圖像時(shí)報(bào)FileNotFoundError。原因Human3.6M 的圖像目錄命名規(guī)則和標(biāo)注文件里的動(dòng)作名稱不是一一對(duì)應(yīng)的中間需要做一次映射。解決檢查metadata.xml里的動(dòng)作標(biāo)簽和圖像目錄名做對(duì)照。我一般會(huì)寫一個(gè)映射字典把標(biāo)注里的action名稱轉(zhuǎn)成圖像目錄的實(shí)際名稱。這個(gè)映射關(guān)系在社區(qū)腳本里能找到參考但不同下載渠道的文件命名可能有差異以實(shí)際目錄為準(zhǔn)。4.5 內(nèi)存不夠?qū)е录虞d中斷現(xiàn)象__init__里預(yù)加載所有數(shù)據(jù)時(shí)進(jìn)程被系統(tǒng) kill 掉終端顯示Killed。原因Human3.6M 全量數(shù)據(jù)加載到內(nèi)存后占用幾個(gè) GB如果服務(wù)器內(nèi)存緊張就會(huì)被 OOM killer 干掉。解決改成懶加載__getitem__里再讀文件而不是在__init__里全讀進(jìn)來(lái)?;蛘哂胣p.load的mmap_moder參數(shù)做內(nèi)存映射不實(shí)際加載數(shù)據(jù)到內(nèi)存。我一般會(huì)先估算一下數(shù)據(jù)量超過(guò) 2GB 就用懶加載。5. 裝完怎么驗(yàn)證跑通一個(gè)最小訓(xùn)練循環(huán)環(huán)境搭好、數(shù)據(jù)加載通了最后一步是驗(yàn)證整條鏈路能不能跑起來(lái)。我一般會(huì)寫一個(gè)最小訓(xùn)練循環(huán)用幾行代碼確認(rèn)前向傳播和反向傳播都沒(méi)問(wèn)題# verify_pipeline.py import torch import torch.nn as nn from torch.utils.data import DataLoader from dataset import Human36MDataset # 加載數(shù)據(jù)集 ds Human36MDataset(data_dirdata/h36m/annotations/, splittrain) loader DataLoader(ds, batch_size4, shuffleTrue, num_workers0) # 定義一個(gè)極簡(jiǎn)的 2D 到 3D 提升網(wǎng)絡(luò) class SimpleLiftingNet(nn.Module): def __init__(self): super().__init__() self.fc nn.Sequential( nn.Linear(32 * 2, 256), nn.ReLU(), nn.Linear(256, 32 * 3) ) def forward(self, x): # x: (batch, 幀數(shù), 32, 2) batch, frames, joints, _ x.shape x x.view(batch * frames, joints * 2) out self.fc(x) return out.view(batch, frames, joints, 3) # 初始化模型和優(yōu)化器 device torch.device(cuda if torch.cuda.is_available() else cpu) model SimpleLiftingNet().to(device) optimizer torch.optim.Adam(model.parameters(), lr1e-3) criterion nn.MSELoss() # 跑一個(gè) epoch model.train() for batch_idx, batch in enumerate(loader): poses_2d batch[poses_2d].to(device) poses_3d batch[poses_3d].to(device) optimizer.zero_grad() pred_3d model(poses_2d) loss criterion(pred_3d, poses_3d) loss.backward() optimizer.step() if batch_idx % 5 0: print(fBatch {batch_idx}, Loss: {loss.item():.4f}) # 只跑幾個(gè) batch 驗(yàn)證流程 if batch_idx 20: break print(Pipeline verification passed.)這段代碼的關(guān)鍵驗(yàn)證點(diǎn)有三個(gè)第一DataLoader能正常迭代說(shuō)明數(shù)據(jù)加載鏈路通了第二model(poses_2d)能跑通前向傳播說(shuō)明輸入輸出維度對(duì)上了第三loss.backward()和optimizer.step()不報(bào)錯(cuò)說(shuō)明反向傳播和參數(shù)更新正常。如果這三步都過(guò)了環(huán)境就算搭好了。跑的時(shí)候注意num_workers先設(shè)成 0避免多進(jìn)程加載數(shù)據(jù)時(shí)出一些玄學(xué)問(wèn)題。等確認(rèn)流程通了再改成 4 或 8 加速。另外batch_size設(shè)小一點(diǎn)4 就行驗(yàn)證階段不需要跑全量數(shù)據(jù)。我一般還會(huì)額外檢查一下poses_3d和pred_3d的數(shù)值范圍如果 loss 一開(kāi)始就是nan多半是數(shù)據(jù)里有異常值或者輸入沒(méi)歸一化。Human3.6M 的 3D 坐標(biāo)單位是毫米數(shù)值范圍在幾百到幾千之間如果看到poses_3d里有inf或nan回去檢查標(biāo)注文件轉(zhuǎn)換那一步是不是漏了字段。從那以后我每次搭新環(huán)境都會(huì)先跑一遍這個(gè)最小驗(yàn)證腳本確認(rèn)數(shù)據(jù)加載、模型前向、反向傳播三條鏈路都通了再開(kāi)始寫正式的訓(xùn)練代碼。這個(gè)習(xí)慣幫我省掉了無(wú)數(shù)次「訓(xùn)練跑了一晚上才發(fā)現(xiàn)數(shù)據(jù)沒(méi)加載對(duì)」的后悔藥。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取