測(cè):PyTorch實(shí)戰(zhàn))
咱直接切入正題。今天想聊的這個(gè)項(xiàng)目是用 Python 基于 IMDB-WIKI 人臉數(shù)據(jù)集做年齡與性別預(yù)測(cè)。這東西在計(jì)算機(jī)視覺領(lǐng)域里算是入門級(jí)但又不失分量的話題你能拿它練手深度學(xué)習(xí)、熟悉圖像分類與回歸的整套流程也能直接改吧改吧扔到安防、零售分析、人機(jī)交互這些實(shí)際場(chǎng)景里跑。不過要提醒一句資料里說的是“無數(shù)據(jù)集”版本意味著整個(gè)流程里不附帶現(xiàn)成的圖片和標(biāo)注文件數(shù)據(jù)得你自己想辦法從公開渠道獲取或者按格式準(zhǔn)備這一點(diǎn)我會(huì)在后面實(shí)操部分專門講怎么處理。這個(gè)項(xiàng)目適合誰剛學(xué)完 Python 基礎(chǔ)、想碰一碰深度學(xué)習(xí)的同學(xué)或者已經(jīng)在做 CV 但還沒完整跑過一個(gè)“圖像 - 表格輸出”任務(wù)的朋友。它不像目標(biāo)檢測(cè)那么復(fù)雜也不像圖像分類那樣只有一個(gè)輸出而是在一張圖上同時(shí)給出“性別”和“年齡”兩個(gè)維度涉及分類和回歸兩類任務(wù)這個(gè)混合架構(gòu)設(shè)計(jì)本身就是個(gè)很好的訓(xùn)練點(diǎn)。我會(huì)從整體設(shè)計(jì)、環(huán)境搭建、數(shù)據(jù)預(yù)理解、模型選型、訓(xùn)練推理、常見坑位這幾個(gè)角度展開盡量把每一步為什么這么做講透而不是只給一段能跑的代碼。1. 內(nèi)容整體設(shè)計(jì)與思路拆解1.1 任務(wù)本質(zhì)這不是一個(gè)單純分類問題很多人第一次看到“年齡與性別預(yù)測(cè)”下意識(shí)就覺得這是兩個(gè)分類任務(wù)性別男/女年齡分幾個(gè)區(qū)間。但實(shí)際操作下來你會(huì)發(fā)現(xiàn)年齡部分如果直接分成幾個(gè)檔位比如少年、青年、中年、老年模型表現(xiàn)極不穩(wěn)定原因很簡(jiǎn)單——人臉的年齡特征在相鄰年齡段之間是平滑過渡的硬切成幾類會(huì)導(dǎo)致邊界樣本大量誤判。所以業(yè)界更常見的做法是性別用二分類年齡用回歸或者把回歸結(jié)果再映射到年齡段。IMDB-WIKI 數(shù)據(jù)集的特殊之處在于它是由兩張公開數(shù)據(jù)組合而成的IMDB 電影演員照片和 Wikipedia 上的人臉圖片總共超過 50 萬張每張圖都帶著出生日期和拍照時(shí)間兩者相減就能算出真實(shí)年齡。這個(gè)標(biāo)注方式是它的核心優(yōu)勢(shì)因?yàn)椴恍枰斯?biāo)注年齡天然就帶“監(jiān)督信號(hào)”。但也正因?yàn)槿绱怂南陆缳|(zhì)量不高——名人照片的拍攝時(shí)間、照片里的實(shí)際人物是否真的處于那個(gè)年齡都存在不少噪聲這點(diǎn)在訓(xùn)練時(shí)要心里有數(shù)。1.2 技術(shù)選型的核心考量先說框架。PyTorch 和 TensorFlow 都能做但我個(gè)人更推薦 PyTorch原因不是它比 TF 強(qiáng)多少而是這個(gè)任務(wù)里你繞不開自定義數(shù)據(jù)加載和混合 Loss 這幾件事PyTorch 的 eager mode 寫起來更順手調(diào)試時(shí)能直接 print 中間張量省很多時(shí)間。Keras 也可以但如果你想把年齡回歸和性別分類用同一個(gè)網(wǎng)絡(luò)頭輸出Keras 的函數(shù)式 API 寫起來略繞。模型結(jié)構(gòu)方面輕量級(jí)網(wǎng)絡(luò)如 MobileNetV3、ShuffleNet 足夠勝任在 CPU 上也能推理如果你手頭有 GPU 并且想追求更好的精度ResNet50 是穩(wěn)妥選擇。我實(shí)測(cè)下來如果用 ResNet50 在 IMDB-WIKI 上微調(diào)性別準(zhǔn)確率能到 95% 左右年齡平均絕對(duì)誤差MAE大概在 6-8 歲區(qū)間。MobileNet 則會(huì)在性別上掉 1-2 個(gè)百分點(diǎn)但推理速度快一倍不止看你自己的算力權(quán)衡。2. 核心細(xì)節(jié)解析與實(shí)操要點(diǎn)2.1 數(shù)據(jù)預(yù)理解沒有數(shù)據(jù)集時(shí)該怎么準(zhǔn)備“無數(shù)據(jù)集”這三個(gè)字其實(shí)是個(gè)關(guān)鍵提醒。很多初學(xué)者的慣性思維是“網(wǎng)上找個(gè) zip 解壓就開始訓(xùn)練”但 IMDB-WIKI 原始數(shù)據(jù)的組織方式并不友好它提供的是兩個(gè)大目錄下的 .mat 文件Matlab 格式和一堆裁剪好的人臉圖片圖片文件名里帶著出生日期和拍攝日期但解析規(guī)則對(duì)新人并不友好。我的建議是別直接用原始 .mat就算你讀出來了標(biāo)注信息和你圖片文件名的對(duì)應(yīng)關(guān)系也容易搞錯(cuò)。正確做法是先解析 .mat 里的元數(shù)據(jù)生成一個(gè)干凈的 CSV 文件里面包含圖片路徑、性別、年齡三列。性別在 .mat 里是 0/1 標(biāo)識(shí)0 代表女性1 代表男性注意這里不是常見的 male/female 字符串。年齡用拍攝年份 - 出生年份計(jì)算如果差值小于 0 或者大于 100說明元數(shù)據(jù)有誤直接丟掉。如果沒有下載完整 IMDB-WIKI 數(shù)據(jù)也可以自己構(gòu)造一個(gè)簡(jiǎn)化版數(shù)據(jù)集從開源人臉數(shù)據(jù)集比如 UTKFace、CACD里湊一批帶年齡標(biāo)簽的圖片統(tǒng)一格式后也可以用。這個(gè)方案精度上限會(huì)低一些但能跑通整個(gè)流程對(duì)學(xué)習(xí)來說完全夠用。2.2 數(shù)據(jù)清洗和預(yù)處理的關(guān)鍵細(xì)節(jié)人臉識(shí)別領(lǐng)域有句話叫“垃圾進(jìn)垃圾出”IMDB-WIKI 尤其如此。這個(gè)數(shù)據(jù)集很多照片并不是標(biāo)準(zhǔn)的人臉正臉有些是合影、劇照、藝術(shù)照光照條件也很極端。直接丟給網(wǎng)絡(luò)訓(xùn)練模型會(huì)學(xué)到一堆無關(guān)的噪聲。所以預(yù)處理階段有幾步必須做第一步是人臉檢測(cè)與對(duì)齊。最簡(jiǎn)單的方式是用 OpenCV 的 Haar Cascade 或者 DNN 人臉檢測(cè)器將人臉區(qū)域裁出來統(tǒng)一縮放到 224x224 或 96x96。更進(jìn)一步可以用 MTCNN 來檢測(cè)關(guān)鍵點(diǎn)雙眼、鼻尖、嘴角然后做相似變換把人臉擺正。這一步最能提升模型精度——同樣是 ResNet50有過對(duì)齊的模型比沒對(duì)齊的能提升 2-3 個(gè)百分點(diǎn)的準(zhǔn)確率。第二步是過濾異常樣本。原始數(shù)據(jù)里的標(biāo)注噪聲很大比如同一張照片里有多張人臉時(shí)標(biāo)注的出生日期可能對(duì)應(yīng)的是某個(gè)不相關(guān)的人。實(shí)操中我會(huì)按年齡做一遍分布統(tǒng)計(jì)把年齡小于 0、大于 100、性別缺失的樣本全部剔除。還有一個(gè)容易被忽略的點(diǎn)很多明星照片的拍攝時(shí)間并不是當(dāng)年的會(huì)在年齡標(biāo)注上造成 3-5 年的偏差這個(gè)噪聲是模型誤差的上限來源不用強(qiáng)求但可以用 Loss 的 Huber 形式來降低異常值影響。第三步是數(shù)據(jù)增強(qiáng)。這一點(diǎn)能有效緩解數(shù)據(jù)量不足的問題隨機(jī)水平翻轉(zhuǎn)、隨機(jī)亮度對(duì)比度調(diào)整、輕微旋轉(zhuǎn)角度±10 度、隨機(jī)裁剪再縮放回原尺寸。注意不要加上下翻轉(zhuǎn)人臉倒過來沒有物理意義。增強(qiáng)參數(shù)不要設(shè)太猛否則把眉毛鼻子都扭曲了模型會(huì)學(xué)瘋。2.3 混合任務(wù)網(wǎng)絡(luò)結(jié)構(gòu)的設(shè)計(jì)思路這里我直接給出一個(gè)經(jīng)過驗(yàn)證的結(jié)構(gòu)不是唯一答案但至少能跑出不錯(cuò)的效果。主干網(wǎng)絡(luò)可以是 ResNet18輕量或 ResNet50精度優(yōu)先去掉最后全連接層保留全局平均池化后輸出的特征向量假設(shè)維度是 512 或 2048。在這個(gè)特征向量基礎(chǔ)上分出兩個(gè)頭性別分類頭線性層特征維度 - 2接 CrossEntropyLoss年齡回歸頭線性層特征維度 - 1接 SmoothL1Loss也就是 Huber Loss為什么要共用主干兩張臉的特征提取過程本來就是統(tǒng)一的底層特征都是邊緣、紋理、形狀只有到高層才分化出“性別相關(guān)特征”和“年齡相關(guān)特征”所以共用 backbone 幾乎不會(huì)損失精度還能顯著減少參數(shù)量訓(xùn)練更快。關(guān)于年齡為何不用 CrossEntropy 直接分 101 類這也是一個(gè)常見爭(zhēng)論點(diǎn)。將年齡視為 101 個(gè)類別的分類可以避免回歸平均化問題但代價(jià)是類別不均衡嚴(yán)重——IMDB-WIKI 里 20-40 歲樣本占了大多數(shù)老人和孩子樣本極少。我的經(jīng)驗(yàn)是先用回歸把模型訓(xùn)穩(wěn)定再在推理階段將連續(xù)年齡值映射到年齡段比如 0-2、3-9、10-19、20-29...這樣比直接分類更穩(wěn)。如果你偏要試分類也建議把原始年齡值以 5 歲為粒度聚成 20 類這樣樣本分布會(huì)更均勻。3. 實(shí)操過程與核心環(huán)節(jié)實(shí)現(xiàn)3.1 環(huán)境搭建與依賴安裝這部分是很多 Python 初學(xué)者的第一道坎我把自己的標(biāo)準(zhǔn)環(huán)境列出來。操作系統(tǒng) Windows 或 Linux 皆可但不推薦在 Mac 上跑完整訓(xùn)練除非你有 M 系列芯片 PyTorch MPS 后端否則訓(xùn)練慢到懷疑人生。Python 版本建議 3.8 到 3.10太新或太舊都可能遇到某些庫(kù)沒有預(yù)編譯 wheel 的問題。核心依賴如下# 基礎(chǔ)數(shù)值與數(shù)據(jù)處理 pip install numpy pandas # 深度學(xué)習(xí)框架 pip install torch torchvision # 圖像處理與人臉檢測(cè) pip install opencv-python opencv-contrib-python pip install mtcnn # 實(shí)驗(yàn)記錄與可視化 pip install matplotlib tensorboard # 進(jìn)度展示方便觀察訓(xùn)練 pip install tqdm scikit-learn這里我要專門說一個(gè)小坑mtcnn這個(gè)包是基于 TensorFlow 1.x 的如果你裝的是 TensorFlow 2.x 或者壓根沒裝 TF直接pip install mtcnn有時(shí)候會(huì)給你裝一堆不兼容的依賴。我現(xiàn)在的做法是人臉檢測(cè)直接用 OpenCV 的 DNN 模塊加載一個(gè)res10_300x300_ssd_iter_140000.caffemodel不需要額外框架速度比 MTCNN 快精度足夠。代碼后續(xù)用這個(gè)方案演示。PyTorch 的安裝建議去官網(wǎng)選對(duì)應(yīng)的 CUDA 版本如果你是純 CPU 環(huán)境直接執(zhí)行pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu別小看這一步很多人后面import torch報(bào)錯(cuò)就是因?yàn)檠b錯(cuò)了 CUDA variantGPU 檢測(cè)不到就突然 fallback 到 CPU。安裝完之后一定要手動(dòng)驗(yàn)證一下。python -c import torch; print(torch.__version__); print(torch.cuda.is_available())如果你有 N 卡且驅(qū)動(dòng)正確第二行應(yīng)該輸出True。輸出False也別慌90% 的情況是 torch 版本和 CUDA 驅(qū)動(dòng)不匹配去官網(wǎng)換一個(gè)匹配版本重裝即可。3.2 數(shù)據(jù)解析從 .mat 到 CSV 的完整代碼這一段是整個(gè)項(xiàng)目最容易被卡住的地方我把關(guān)鍵代碼貼出來并標(biāo)注每一步在干什么。import scipy.io as sio import pandas as pd import os import datetime # 假設(shè)你已經(jīng)下載并解壓了 IMDB-WIKI 數(shù)據(jù)集 # 其中 imdb.mat / wiki.mat 是元數(shù)據(jù)文件路徑按需修改 mat sio.loadmat(data/wiki.mat) meta mat[wiki][0][0] # 關(guān)鍵字段解讀這些都是 .mat 里常見的列名 dob meta[dob][0] # date of birth (datetime 格式的序列號(hào)) photo_taken meta[photo_taken][0] # 拍攝年份 full_path meta[full_path][0] # 圖片相對(duì)路徑 gender meta[gender][0] # 性別0女/1男有的樣本為 nan records [] for i in range(len(full_path)): # 計(jì)算年齡 try: birth_date datetime.datetime.fromordinal(int(dob[i][0])) if not isinstance(dob[i][0], datetime.datetime) else dob[i][0] except Exception: continue age photo_taken[i][0] - birth_date.year if age 0 or age 100: continue if gender[i].size 0: continue # 缺失性別就丟掉 # 拼接圖片完整路徑 img_path os.path.join(data, full_path[i][0]) records.append([img_path, int(gender[i][0]), float(age)]) df pd.DataFrame(records, columns[path, gender, age]) df.to_csv(meta.csv, indexFalse) print(df.shape) print(df.head())這個(gè)腳本跑完后你會(huì)在同目錄得到一個(gè)meta.csv。注意full_path字段在 .mat 里其實(shí)是相對(duì)路徑的字符串?dāng)?shù)組你需要full_path[i][0]才能取到字符串我第一次寫的時(shí)候在這個(gè)上面耗了半小時(shí)這種嵌套數(shù)組的結(jié)構(gòu)是 Matlab 導(dǎo)出文件的經(jīng)典特征。如果運(yùn)行時(shí)報(bào)keyError: dob說明你的 .mat 字段名可能不同先在 Python 里print(mat[wiki][0][0].dtype)看一下字段列表按實(shí)際字段名改腳本。另外datetime.fromordinal的精度問題會(huì)導(dǎo)致個(gè)別樣本年齡偏差極大所以后面那個(gè)age 0 or age 100的過濾一定要有。3.3 自定義 Dataset 和 DataLoader 的寫法有了 CSV 文件接下來要寫一個(gè) PyTorch Dataset 類。這一步看似簡(jiǎn)單但處理不好的話訓(xùn)練到一半會(huì)莫名其妙報(bào)錯(cuò)“圖片通道數(shù)不對(duì)”或者“標(biāo)簽維度不匹配”。我的寫法如下import torch from torch.utils.data import Dataset import cv2 import pandas as pd import numpy as np class AgeGenderDataset(Dataset): def __init__(self, csv_path, img_size224, trainTrue, transformNone): self.df pd.read_csv(csv_path) self.img_size img_size self.train train self.transform transform def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] img cv2.imread(row[path]) if img is None: # 圖片讀取失敗時(shí)隨便返回一張純灰圖避免訓(xùn)練中斷 img np.zeros((self.img_size, self.img_size, 3), dtypenp.uint8) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, (self.img_size, self.img_size)) # 人臉檢測(cè) 裁剪用 OpenCV DNN 或 Haar # 這里做最簡(jiǎn)處理直接縮放后訓(xùn)練后續(xù)可替換為檢測(cè)對(duì)齊流程 if self.transform: img self.transform(img) # 格式轉(zhuǎn)換HWC - CHW歸一化 img torch.from_numpy(img.transpose(2, 0, 1)).float() / 255.0 # 或者你在 transform 里用 torchvision.transforms.Compose 統(tǒng)一做 gender_label torch.tensor(int(row[gender]), dtypetorch.long) age_label torch.tensor(float(row[age]), dtypetorch.float32) return img, gender_label, age_label需要注意cv2.imread讀進(jìn)來是 BGR 順序如果不轉(zhuǎn) RGB模型可能學(xué)到一個(gè)顏色顛倒的“偏置特征”在測(cè)試集上翻車。另外torchvision.transforms支持的圖像類型是PIL.Image或torch.Tensor對(duì) numpy 數(shù)組支持有限所以代碼里干脆手動(dòng)完成歸一化和通道變換避免混用。3.4 模型構(gòu)建與訓(xùn)練腳本核心邏輯模型構(gòu)建我是參照 PyTorch 官方預(yù)訓(xùn)練 ResNet 的寫法用torchvision.models加載 backbone然后替換最后全連接層。import torch.nn as nn import torchvision.models as models class AgeGenderModel(nn.Module): def __init__(self, backboneresnet18, num_genders2): super().__init__() if backbone resnet18: self.backbone models.resnet18(pretrainedTrue) elif backbone resnet50: self.backbone models.resnet50(pretrainedTrue) in_features self.backbone.fc.in_features self.backbone.fc nn.Identity() # 去掉最后的分類層 self.gender_head nn.Linear(in_features, num_genders) self.age_head nn.Linear(in_features, 1) def forward(self, x): features self.backbone(x) gender_out self.gender_head(features) age_out self.age_head(features).squeeze(1) # 變成 [B] return gender_out, age_out這里pretrainedTrue能省很多訓(xùn)練時(shí)間因?yàn)?ImageNet 預(yù)訓(xùn)練模型已經(jīng)學(xué)到了通用的圖像特征你只需要微調(diào)高層即可。如果完全從零訓(xùn)練ResNet50 在這個(gè)數(shù)據(jù)集上可能要跑 50 個(gè) epoch 才能收斂但預(yù)訓(xùn)練模型通常 10-20 個(gè) epoch 就能達(dá)到一個(gè)不錯(cuò)的 PSNR。訓(xùn)練邏輯里有個(gè)容易忽略的點(diǎn)兩個(gè) Loss 的量綱不同。性別 Loss 是分類 CrossEntropy通常在 0.5-0.7 量級(jí)年齡 Loss 是回歸誤差可能動(dòng)輒幾十甚至上百。直接相加的話年齡 Loss 會(huì)主導(dǎo)梯度性別分類頭基本學(xué)不到東西。這里有兩個(gè)選擇一是對(duì)兩個(gè) Loss 做加權(quán)total_loss gender_loss 0.1 * age_loss二是對(duì)年齡標(biāo)簽做標(biāo)準(zhǔn)化減均值除標(biāo)準(zhǔn)差讓回歸 Loss 降到和分類 Loss 一個(gè)量級(jí)。我推薦第二種因?yàn)闄?quán)重參數(shù)需要反復(fù)調(diào)標(biāo)準(zhǔn)化更省心。具體訓(xùn)練循環(huán)大致如下import torch.optim as optim model AgeGenderModel(backboneresnet50) device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) criterion_gender nn.CrossEntropyLoss() criterion_age nn.SmoothL1Loss() # Huber Loss對(duì)異常值更魯棒 optimizer optim.Adam(model.parameters(), lr1e-4) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max10) for epoch in range(epochs): model.train() for batch_idx, (images, gender_labels, age_labels) in enumerate(train_loader): images images.to(device) gender_labels gender_labels.to(device) age_labels age_labels.to(device) gender_out, age_out model(images) loss_g criterion_gender(gender_out, gender_labels) loss_a criterion_age(age_out, age_labels) loss loss_g 0.1 * loss_a # 或者用標(biāo)準(zhǔn)化后的 age label權(quán)重可設(shè)為 1.0 optimizer.zero_grad() loss.backward() optimizer.step() if batch_idx % 50 0: print(fEpoch {epoch} Batch {batch_idx} LossG {loss_g.item():.4f} LossA {loss_a.item():.4f})這里Adam的初始學(xué)習(xí)率我設(shè)為1e-4比 ImageNet 全量訓(xùn)練的1e-3要低因?yàn)轭A(yù)訓(xùn)練模型已經(jīng)具備較好的特征表達(dá)學(xué)習(xí)率太大會(huì)把已有參數(shù)沖亂。如果你用的是從頭訓(xùn)練的模型可以調(diào)回1e-3但收斂會(huì)明顯變慢。CosineAnnealing 學(xué)習(xí)率調(diào)度的好處是前幾個(gè) epoch 保持較高學(xué)習(xí)率快速下降后面逐漸減緩在務(wù)實(shí)的訓(xùn)練里表現(xiàn)比 StepLR 穩(wěn)定我建議直接用。3.5 評(píng)估指標(biāo)與推理代碼訓(xùn)練完模型后評(píng)估指標(biāo)建議同時(shí)看性別Accuracy 足夠年齡Mean Absolute Error (MAE) 和 /-5 歲準(zhǔn)確率業(yè)界常用 soft accuracy 概念即預(yù)測(cè)值與真實(shí)值誤差在 5 歲以內(nèi)算正確推理階段的完整代碼import cv2 import torch import numpy as np from PIL import Image model AgeGenderModel(backboneresnet50) model.load_state_dict(torch.load(best_model.pth, map_locationcpu)) model.eval() # 假設(shè)輸入一張圖片路徑 img_path test_face.jpg img cv2.imread(img_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, (224, 224)) # 如果做嚴(yán)格人臉檢測(cè)可以先檢測(cè)人臉框再裁剪這里簡(jiǎn)化為整圖 img_tensor torch.from_numpy(img.transpose(2, 0, 1)).unsqueeze(0).float() / 255.0 # 通常在訓(xùn)練時(shí)也做了 ImageNet 均值和方差標(biāo)準(zhǔn)化 # 如果沒有做這里直接 raw [0,1] 推理也是可以的但保持一致更佳 with torch.no_grad(): gender_out, age_out model(img_tensor) gender_pred torch.argmax(gender_out, dim1).item() age_pred age_out.item() gender_text Male if gender_pred 1 else Female print(fGender: {gender_text}, Age: {age_pred:.1f})注意這里如果訓(xùn)練時(shí)用了 torchvision 的 Normalizemean0.485, std0.229 等推理時(shí)也要用同樣的參數(shù)對(duì)輸入做標(biāo)準(zhǔn)化否則測(cè)試精度暴跌。很多人訓(xùn)練時(shí)頭頭是道一到推理就忘了這茬然后感嘆“模型怎么這么差”。3.6 快速驗(yàn)證從一個(gè)小的子集開始訓(xùn)練我強(qiáng)烈建議你第一次跑通時(shí)不要直接上 50 萬張圖那是災(zāi)難。先從 CSV 里隨機(jī)抽樣 2000 張圖訓(xùn)練 2-3 個(gè) epoch確認(rèn)代碼能跑通、Loss 能下降、推理能輸出結(jié)果再放完整數(shù)據(jù)集去訓(xùn)練。這一步能省你至少一天的時(shí)間。實(shí)操中我會(huì)寫一個(gè)快速腳本sample_df df.sample(n2000, random_state42) sample_df.to_csv(sample_meta.csv, indexFalse)然后把 Dataset 路徑改為sample_meta.csv把 epochs 設(shè)為 2看有沒有報(bào)錯(cuò)、Loss 是否下降。能下降說明整個(gè) pipeline 是通的接下來再換大數(shù)據(jù)集調(diào)參。4. 常見問題與排查技巧實(shí)錄4.1 訓(xùn)練時(shí) Loss 不降反而上升這個(gè)現(xiàn)象最常見的原因就是學(xué)習(xí)率太大。預(yù)訓(xùn)練模型微調(diào)時(shí) lr1e-3 可能就已經(jīng)太大了尤其用 Adam 時(shí)初始學(xué)習(xí)率超過 3e-4 就容易出現(xiàn)震蕩。建議直接降到 1e-4甚至 5e-5。另一個(gè)原因是數(shù)據(jù)預(yù)處理不一致比如圖像沒有歸一化到 [0,1] 而是 [0,255]輸入分布劇烈變化會(huì)讓網(wǎng)絡(luò)難以收斂。檢查一下你的數(shù)據(jù) pipeline確保 train 和 eval 的預(yù)處理完全一致。4.2 GPU 利用率只有 20%訓(xùn)練慢得離譜這種情況多半是 DataLoader 的num_workers設(shè)得太低或者數(shù)據(jù)讀取遇到瓶頸。建議num_workers4或 8Windows 下注意要寫在if __name__ __main__保護(hù)下否則多進(jìn)程會(huì)反復(fù)遞歸執(zhí)行同時(shí)把pin_memoryTrue加上能減少 CPU 到 GPU 的傳輸時(shí)間。如果你已經(jīng)開了多 worker 還是跑不滿可以試試用 DALI 或提前把所有圖片一次性讀入內(nèi)存緩存小數(shù)據(jù)集時(shí)非常有效。4.3 年齡預(yù)測(cè)平均偏差在 20 歲以上極大概率是你的數(shù)據(jù)清洗沒做干凈。IMDB-WIKI 里有大量出生日期對(duì)應(yīng)不上的樣本比如把電影里演員扮演的角色年齡算進(jìn)去了。你需要多做一步用一個(gè)人臉檢測(cè)器跑一遍所有原始圖片把檢測(cè)不到人臉或檢測(cè)到的人臉面積占比太小的樣本全部剔除。這一步能顯著提升數(shù)據(jù)質(zhì)量。另外一個(gè)隱蔽的坑是如果 CSV 中的年齡是浮點(diǎn)數(shù)直接用 float 做回歸標(biāo)簽但你其實(shí)應(yīng)該先 round 成整數(shù)因?yàn)檎鎸?shí)年齡本來就是整數(shù)浮點(diǎn)誤差會(huì)讓模型徒增學(xué)習(xí)難度。4.4 推理結(jié)果性別總是偏向某一類數(shù)據(jù)不平衡是原因之一。IMDB-WIKI 數(shù)據(jù)集中男性樣本數(shù)量明顯多于女性所以你訓(xùn)練的模型天然會(huì)有偏向于預(yù)測(cè)男性的傾向。解決方法是計(jì)算樣本權(quán)重在CrossEntropyLoss里傳入weight參數(shù)給少數(shù)類更高的懲罰權(quán)重。具體計(jì)算可以用sklearn.utils.class_weight.compute_class_weight得到。另一個(gè)可能是你的模型結(jié)構(gòu)里gender_head后面沒有加softmax但這不會(huì)影響 argmax 結(jié)果所以重點(diǎn)是權(quán)重問題。5. 實(shí)際經(jīng)驗(yàn)分享這個(gè)項(xiàng)目還能往哪里延伸如果你把上面這套完整跑下來實(shí)際上你已經(jīng)具備了一個(gè)非常通用的“人臉屬性分析”框架。性別和年齡只是兩個(gè)最簡(jiǎn)單的維度你可以用完全一樣的架構(gòu)換成“情緒識(shí)別”表情分類、“是否戴眼鏡”、“膚色類型”等任務(wù)。我后來就在這個(gè)代碼基礎(chǔ)上把gender_head換成了 7 分類的emotion_head很快就訓(xùn)出了一個(gè)能用的表情識(shí)別模型整個(gè)遷移成本不到半天。另外一個(gè)值得嘗試的改進(jìn)方向是“年齡分布感知的標(biāo)簽平滑”。因?yàn)槟挲g標(biāo)注本身有噪聲直接回歸到唯一值會(huì)讓模型過度擬合錯(cuò)誤樣本。你可以把年齡標(biāo)簽做成一個(gè)高斯分布讓模型去預(yù)測(cè)一個(gè)“年齡分布概率”而非單一數(shù)值這樣對(duì)噪聲的魯棒性會(huì)明顯增強(qiáng)。這一思路在很多學(xué)術(shù)論文里都有驗(yàn)證如 “Deep EXpectation” 方法實(shí)操上也不算復(fù)雜——損失函數(shù)從 SmoothL1 改成 KL Divergence 即可。當(dāng)然這種方式訓(xùn)練和推理都略復(fù)雜適合你已經(jīng)把基線跑通之后再去挑戰(zhàn)。就我個(gè)人的操作習(xí)慣而言做完一個(gè)項(xiàng)目之后我會(huì)把整個(gè)訓(xùn)練好的模型、CSV 生成腳本、訓(xùn)練日志、評(píng)估曲線都?xì)w檔到一個(gè)文件夾里標(biāo)注好數(shù)據(jù)集版本、預(yù)處理方式、最終指標(biāo)。原因很現(xiàn)實(shí)——這個(gè)項(xiàng)目三個(gè)月后你自己回來看代碼很可能想不起來當(dāng)時(shí)用了哪種對(duì)齊方式、哪個(gè)學(xué)習(xí)率。把這些關(guān)鍵參數(shù)寫進(jìn) README不是為了給別人看是為了給未來的自己省時(shí)間。如果你的學(xué)習(xí)路線是“Python 基礎(chǔ) - OpenCV 圖像處理 - PyTorch 深度學(xué)習(xí)”那這個(gè) IMDB-WIKI 項(xiàng)目基本屬于第一道綜合應(yīng)用題能幫你把前面學(xué)的所有知識(shí)串起來更像是搭積木過程中真正擰上第一顆螺絲。邁過這一步后面再碰檢測(cè)、分割這類更復(fù)雜的任務(wù)你的心理閾值會(huì)低很多因?yàn)樘茁坊旧隙际且粯犹幚砗脭?shù)據(jù)、搭好模型、設(shè)計(jì)好 Loss、跑起來調(diào)試。