尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Few-shot视线估计复现全攻略:从环境搭建到模型调优

Few-shot视线估计复现全攻略:从环境搭建到模型调优 简介一套面向毕业设计的视线估计gaze estimationfew-shot 项目源码核心工作为复现并优化 Seonwook Park 的 few_shot_gaze 方法并基于 MPIIFaceGaze 与 GazeCapture 两大公开数据集完成训练与评估。压缩包共93个文件、整体约13.49MB主体为41个 Python 脚本覆盖 HDF5 数据预处理、自编码器训练、元学习阶段与结果合并等完整流程同时附带 DenseNet、DT-ED 模型定义、多种损失函数实现、Caffe 模型与 prototxt 配置以及可用于实时摄像头演示的 demo 脚本、MTCNN 人脸检测组件与相机标定工具。源码目录结构清晰训练、测试、演示模块分离并配有 README 说明、依赖清单与 shell 脚本便于在 Linux 环境下复现实验元学习与嵌入一致性等关键代码也便于逐段阅读对比。目前已有287人浏览学习适合需要快速搭建少样本视线估计基线、理解元学习与嵌入一致性约束的计算机视觉方向毕业生或研究者参考。1. few-shot-gaze是什么两个数据集和一个毕设级复现问题拿到这个标题时我第一反应是——又有同学在视线估计gaze estimation这个方向上选了个硬骨头。few-shot-gaze并不是某个单独的算法包名而是“用少量带标注样本来训练视线估计模型”这一类问题的统称。标题里把MPIIFaceGaze和GazeCapture两个数据集放在一起指向的是一个近年很常见的复现组合前者是实验室环境下的头部姿态大样本数据集后者是手机和平板环境下的大规模众包数据集。两个数据集的域差异domain gap非常大跨数据集做少样本学习正好是检验一个模型泛化能力最狠的试金石。你要做的就是把这样一个zip源码包里的训练流程跑通搞清楚它为什么能在10张、20张带标注样本下拟合出新用户的视线方向然后想办法优化它。这篇笔记我按自己的复现路径来写环境怎么搭、数据怎么解析、few-shot训练循环怎么写、到底哪些坑会让你熬夜最后收在如何验证优化效果上。适合正在做毕设、或者刚接触视线估计想要一个完整落地方案的人。2. 复现前的环境与依赖python版本、PyTorch与CUDA的选型2.1 为什么python版本选择和CUDA绑定会让项目翻车视线估计模型几乎都跑在PyTorch上而PyTorch的版本链对python版本极其敏感。我见过的复现失败案例里有一半是环境问题而不是代码问题。标题里的源码包大概率来自GitHub上某个学术实现这类项目最常见的配置是python 3.7-3.9 PyTorch 1.8-1.13。如果你直接上python 3.10老版本的torchvision可能无法import分布式训练相关的APIs会报AttributeError。这里我给一个保守但兼容性最好的组合python 3.8 CUDA 11.3 PyTorch 1.12.1。这个组合能跑通绝大多数2022-2023年发布的few-shot相关开源项目。如果你显卡驱动只支持CUDA 12.x那就换成PyTorch 2.0但要注意老代码里init_distributed_mode这类函数在新版本下参数变了需要小幅修改。2.2 用conda建立隔离环境一份能直接用的安装脚本我建议直接用conda建环境python版本锁定在3.8然后在conda环境里用pip装torch系列。这样做的好处是protobuf、numpy这些底层库的版本冲突概率会低很多。conda create -n fsgaze python3.8 -y conda activate fsgaze pip install torch1.12.1 torchvision0.13.1 --index-url https://download.pytorch.org/whl/cu113 pip install opencv-python4.6.0.66 opencv-contrib-python4.6.0.66 pip install numpy1.23.5 scipy1.9.3 pandas matplotlib tqdm tensorboard这段脚本里有个细节值得说明opencv-python我把版本卡在4.6。原因是很多视线估计代码里会用cv2.face模块做人脸对齐而新版OpenCV把face模块移到了opencv-contrib-python里你不锁定版本就会遇到AttributeError: module cv2 has no attribute face。scipy1.9.3则是为了兼容读取.mat文件时的一些老API——如果你用了更新的scipy部分低版本.mat文件读取会直接报错。装完之后先import一遍确认torch.cuda.is_available()返回True。如果返回False大概率不是python问题而是PyTorch的CUDA版本和驱动不匹配——Windows下可以用GPU-Z看驱动支持的CUDA版本号。这一步做对了后续的工作基本就是舒舒服服调参数做不对的话后面每一步都像是在踩雷。2.3 源码包的目录结构为什么值得先花5分钟看一遍解压zip后不要急着找train.py去跑。先看一眼目录结构。常见的视线估计项目一般长这样few-shot-gaze/ ├── configs/ # yaml或py格式的配置文件 ├── datasets/ # 数据集解析代码 ├── models/ # 主干网络和head定义 ├── utils/ # 坐标系转换、指标计算、平均脸等 ├── train.py # 训练入口 ├── eval.py # 评估入口 └── requirements.txt我会先在configs目录里看数据集路径字段简写成了什么比如mpiifacegaze_root、gaze_capture_dir。然后顺着它去datasets里找加载代码。这个动作能帮你提前预判数据集解析逻辑里有没有硬编码路径避免后面运行到一半才爆出一个FileNotFoundError。多数源码包不会给你完整数据集只会给你一个示例目录结构。所以提前看清楚它期望的目录形态很重要。3. 吃透两个数据集的加载与预处理MPIIFaceGaze和GazeCapture3.1 MPIIFaceGaze的标注格式与读取方法MPIIFaceGaze已经是视线估计领域的事实基准之一。15个参与者的数据每个参与者采集约3000张人脸图像标注字段是3D视线方向向量和对应的2D注视点屏幕坐标。它的原始标注文件是.mat格式里面的人脸图像已经做了归一化裁剪尺寸是60x60灰度图。这个尺寸非常小直接决定了模型输入分辨率不需要设得太高。读取.mat文件是第一步坑MPIIFaceGaze官网下载的.mat文件是用MATLAB保存的结构体数组需要scipy.io.loadmat来读但要注意h5py和scipy读出来的结构不同——新版的MPIIFaceGaze数据集打包成.h5格式而标题里的源码包如果给你们的是老脚本那多半还是按.mat写的。下面这段代码兼容两种格式import h5py import numpy as np import scipy.io as sio def load_mpiigaze_annotations(path): 读取MPIIFaceGaze标注文件兼容.mat和.h5两种格式 if path.endswith(.mat): data sio.loadmat(path) # 老版本.mat格式data[data]是一个结构体里面有face和gaze字段 face data[data][face][0, 0] # N x 60 x 60 的归一化人脸 gaze data[data][gaze][0, 0] # N x 3 的视线方向向量 head data[data][head][0, 0] # N x 3 的头部姿态向量 else: # .h5 with h5py.File(path, r) as f: person_ids list(f.keys()) face [] gaze [] for pid in person_ids: face.append(np.array(f[pid][face])) gaze.append(np.array(f[pid][gaze])) face np.concatenate(face, axis0) gaze np.concatenate(gaze, axis0) return face, gaze这段代码有两个关键处理第一.mat格式拿到的face数组要检查维度是(N, 60, 60)还是(N, 60, 60, 1)后者需要squeeze掉最后一维否则大多数模型的Conv2d第一层会报维度不匹配第二.h5格式的gaze向量单位是弧度而.mat格式里同样是弧度——千万别当成角度去算损失数值会差出57.3倍这是视线估计领域最经典的翻车点之一。读取完之后你要统一先做一个/ 180 * np.pi之类的检查确认数据的取值范围视线向量各分量应该落在[-1, 1]区间头部姿态向量各分量应该在[-π/2, π/2]左右但通常只有一个分量接近边界。3.2 ????GazeCapture的JSON结构与帧采样策略GazeCapture是移动端视线数据集采样频率约为30fps包含用户在自然使用手机时的人脸视频帧、注视点坐标以及设备传感器数据。它的原始数据下载后是一堆用户目录每个用户目录里有一个frames子目录放的是裁剪好的人脸图片160x120彩色以及一个包含人脸关键点、2D视线目标点、头部姿态角度的JSON文件。这个数据集最大的问题不是格式难读而是数据量太大——完整下载要几十个GB对毕设来说根本没必要。通常源码包里给出的GazeCapture配置会支持按比例采样。比如只取其中5个用户目录每个用户取200帧做few-shot微调那就是非常典型的few-shot场景。解析engine的伪代码大概是这样import json import os import cv2 import numpy as np def load_gazecapture_sample(user_dir, frame_index): 读取GazeCapture单帧数据返回人脸图、视线方向、头姿 json_path os.path.join(user_dir, frames, f{frame_index}.json) img_path os.path.join(user_dir, frames, f{frame_index}.jpg) with open(json_path, r) as f: meta json.load(f) # JSON里存的是2D注视点坐标需要按输入图像尺寸归一化 h, w 160, 120 gaze_x meta[gaze_point_x] / w gaze_y meta[gaze_point_y] / h # 有些版本会直接给出3D视线向量 gaze_3d np.array(meta.get(gaze_3d, [gaze_x, gaze_y, 0.0])) img cv2.imread(img_path) img cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 两个数据集统一成灰度 return img, gaze_3d这段代码需要注意gaze_point_x / w的归一化方式。GazeCapture的标注坐标原点是图像左上角单位是像素方向向右和下。而很多模型训练时使用的视线标签是“从眼睛到头戴设备屏幕中心连线与屏幕法线的夹角”这是两组不一样的东西。如果源码包里的模型是直接回归3D视线向量那么要对GazeCapture的2D注视点做一次反投影把人眼坐标和屏幕注视点连成一条空间射线再换算到相机坐标系下的方向向量。这个转换通常在utils/geometry.py里已经实现你要做的是确认它调用时传入的相机内参到底是设备默认值还是外部标定值。3.3 划分train/val/test时最容易犯的交叉污染错误复现时一个很容易翻车的小动作是直接在所有用户的数据上混合后随机划分训练集和测试集。视线估计的隐私属性很强——同一个人的脸部外观会在训练和测试里同时出现时模型记住的是这个人长得什么样而不是视线往哪看测试分数会虚高得很厉害。正确的做法是按用户划分即MPIIFaceGaze的15个参与者里取11个做训练、2个做验证、2个做测试GazeCapture也是按用户目录划分且训练用户与测试用户完全没有交集。def split_by_person(person_ids, train_ratio0.7, val_ratio0.1, seed42): 按用户划分数据集确保测试用户没参与过训练 rng np.random.RandomState(seed) shuffled rng.permutation(person_ids) n_train int(len(shuffled) * train_ratio) n_val int(len(shuffled) * val_ratio) train_persons shuffled[:n_train] val_persons shuffled[n_train:n_train n_val] test_persons shuffled[n_train n_val:] return train_persons, val_persons, test_persons如果你在源码包里看到的是按“帧索引”随机划分而不是按用户划分那这个源码包大概率是个玩具实现不建议直接拿来做毕设基线。交叉污染会让评估结果失去意义你在论文里写出来的benchmark也经不起评委追问。另外还有一个细节MPIIFaceGaze的60x60图像是经过人脸归一化的头部中心已经被对齐到图像中心而GazeCapture的160x120图像只是简单裁剪并没有做姿态归一化。如果源码包里的模型期望输入是“对齐后的脸”那你在训练GazeCapture数据前要先用人脸关键点做一次similarity transform。不做这一步跨域few-shot的效果会非常难看。4. few-shot训练的主体实现从主干网络到episode采样4.1 主干网络选型ResNet18是底线而非上限视线估计模型的主干网络通常没有做得特别大。MPIIFaceGaze的输入才60x60ResNet18甚至ResNet10已经能提取足够好的特征。使用更大的ResNet50在参数和显存上付出代价精度提升却非常有限——因为输入分辨率本身限制了信息上限。few-shot条件下大模型反而更容易过拟合支持集support set只有10张或20张图片微调阶段模型很快就记住了这些样本的个体特征而非视线语义。所以复现时我建议先保留源码包的默认主干结构只在最后一层全连接层上做调整。常见做法是去掉原始的1000类ImageNet分类头换成输出维度为22D视线角或33D视线方向的小head。这会让预训练权重发挥作用也不会破坏原有特征提取能力。import torch import torch.nn as nn import torchvision.models as models class GazeResNet(nn.Module): 在ResNet18基础上替换分类头为视线回归头 def __init__(self, pretrainedTrue, out_dim2): super().__init__() backbone models.resnet18(pretrainedpretrained) self.features nn.Sequential( backbone.conv1, backbone.bn1, backbone.relu, backbone.maxpool, backbone.layer1, backbone.layer2, backbone.layer3, backbone.layer4, ) self.avgpool nn.AdaptiveAvgPool2d((1, 1)) self.fc nn.Linear(512, out_dim) def forward(self, x): x self.features(x) x self.avgpool(x) x torch.flatten(x, 1) return self.fc(x)这里有几个参数值得说明。out_dim2对应回归两个视线角度偏航角yaw和俯仰角pitchout_dim3对应回归3D单位方向向量。两种输出格式对应的损失函数不太一样角度回归常用nn.L1Loss平均绝对误差3D向量回归常用nn.MSELoss——但要把预测向量做归一化使其模长为1。源码包里如果直接对未归一化的向量做MSE模型会学歪因为它可以用缩小向量模长来投机取巧地把loss压小。另外要注意如果源码包里的模型在torchvision的ResNet基础上改了第一层卷积的stride和kernel_size比如为了适应60x60低分辨率通常会把第一层从kernel7, stride2改成kernel3, stride1并去掉后续的maxpool。如果你不做这种改动60x60输入经过两层下采样后感受野会被压缩得很厉害视线特征根本没有足够大的空间范围来捕捉。我在实验里对比过单是这一项就能让MPIIFaceGaze上的角度误差差出1度左右。4.2 episode采样support set与query set的组织方式few-shot视线估计的实现典型做法是在训练阶段构造episode来模拟测试时的少样本场景。每个episode相当于一个学习任务随机抽一个用户作为目标从他的样本中拿K张作为support set再抽一批其他样本作为query set。模型要从support set里尽快适应这个人的视线模式再在query set上给出预测。class FewShotEpisodeSampler: 构造few-shot episode的核心类稳定随机种子保证可复现 def __init__(self, person_to_indices, k_shot8, n_query16, seed42): self.rng np.random.RandomState(seed) self.person_to_indices person_to_indices self.k_shot k_shot self.n_query n_query def sample_episode(self): 返回一个task的support/query索引列表 target_person self.rng.choice(list(self.person_to_indices.keys())) indices np.array(self.person_to_indices[target_person]) self.rng.shuffle(indices) support_idx indices[:self.k_shot] query_idx indices[self.k_shot:self.k_shot self.n_query] return support_idx, query_idx, target_person参数上k_shot就是每类每个人的标注样本数few-shot视线估计里常见取值是4、8、16。n_query不宜太小否则query上的评估噪声大每个episode的loss波动会很明显。另一个参数是episode数量——一个epoch里构造多少个episode我一般设200-400太少则模型见过的任务数不足太多则训练时间过长。为什么要用episode而不是直接拿所有人数据混合训练因为few-shot的核心假设是你希望在测试阶段能“快速适应新用户”训练时如果不模拟这种场景模型学到的是在数据分布上求平均的通用特征不会主动去学习适应策略。这就是MAML这类元学习算法的思想——但要注意源码包里如果用的是简单的“预训练微调”策略而不是MAMLepisode采样方式会简单得多它只需要在微调阶段用目标用户的K张图做几次梯度更新。预训练阶段还是用常规的数据混合训练。4.3 训练循环预训练、元学习、微调三种模式怎么串起来一个完整的few-shot-gaze项目训练流程通常是三段式的。第一段是在源域数据MPIIFaceGaze全部 GazeCapture大部分上做常规的监督预训练第二段是可选地用MAML或Reptile做元学习训练第三段是评估时在目标用户上做微调。下面是一个兼容三种模式的训练循环骨架def train_one_epoch(model, dataloader, optimizer, device, modemeta): 统一训练循环支持常规监督与episode两种模式 model.train() total_loss 0.0 for batch in dataloader: if mode supervised: # 常规模式batch里直接是图像标签 imgs, gazes batch imgs imgs.to(device) gazes gazes.to(device) pred model(imgs) loss nn.L1Loss()(pred, gazes) optimizer.zero_grad() loss.backward() optimizer.step() elif mode meta: # meta模式batch里是support和query两个分支 support_imgs, support_gazes, query_imgs, query_gazes batch # 先在support上做内循环更新模拟新用户适应 inner_model copy.deepcopy(model) inner_opt torch.optim.SGD(inner_model.parameters(), lr1e-3) for inner_step in range(5): inner_pred inner_model(support_imgs.to(device)) inner_loss nn.L1Loss()(inner_pred, support_gazes.to(device)) inner_opt.zero_grad() inner_loss.backward() inner_opt.step() # 在query上计算一次前向loss更新外层模型 query_pred inner_model(query_imgs.to(device)) loss nn.L1Loss()(query_pred, query_gazes.to(device)) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() * imgs.size(0) return total_loss / len(dataloader.dataset)torch版本比较老时需要记得加with torch.no_grad()来包住support集的这些内循环更新否则梯度图会占光所有显存。这里有个非常关键的实现细节元学习模式下内循环的更新次数inner_step和步长lr1e-3本身就是超参数。内循环步数太多会让support拟合得很干净但在query上泛化差步数太少则没有适应性步长太大同样导致振荡。我跑下来的经验是MPIIFaceGaze上inner_step3、学习率1e-3时微调效果和效率平衡最好。4.4 优化器与学习率调度不要用Adam的默认参数视线估计模型的特征空间维度不高但数据分布相对集中Adam的lr1e-3默认值很容易在预训练后期出现震荡。常见做法是先用1e-4做预热然后线性衰减到1e-6。optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay5e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30, eta_min1e-6)AdamW比Adam多了一个权重衰减策略——它把weight decay从L2正则里解耦出来对特征提取层更友好尤其适合先预训练再微调的场景。T_max30表示学习率在30个epoch内从初始值余弦下降到1e-6。如果你数据集比较大T_max也要相应放大否则学习率衰减太快后期训练会卡在某个局部最优处不动。这属于是从视觉Transformer里带过来的习惯视线估计里同样有效。还有一点batch size不要选太大。视线估计的样本差异性主要在头部姿态与人脸外观上batch size 64时loss曲线还比较稳定超过128后测试精度反而下降原因是大batch降低了梯度噪声让模型更容易陷入support set的局部模式。我把这一点归为“深度学习中少有的小batch效果更好的场景”。5. 复现时最容易翻车的五个坑与排查5.1 坑一标注坐标系不一致导致loss爆炸或NaN现象训练loss先异常大比如1e2级别几个epoch后变为NaNTensorBoard上曲线直接断掉。原因MPIIFaceGaze的视线方向是相机坐标系下的3D单位向量而GazeCapture的JSON里有时给的是屏幕坐标系下的2D注视点像素坐标。模型输出3D方向你却拿2D像素点去做回归两个物理量纲不同loss值自然降不下去。NaN则更可能是loss回传时某个坐标分量出现了反三角函数取值越界。解决写一个检查脚本把两个数据集的标注分布打出来确认范围。如果同时出现2D和3D标签首先要定义一个转换函数统一到同一个坐标系下其次检查模型的最后一层有没有tanh激活——如果预测向量被限制在[-1, 1]而你的标签取值范围是[0, 1080]像素模型先爆炸再收敛多半又是维度问题。5.2 坑二人脸对齐操作被写在训练循环里拖慢速度现象训练一整个epoch要三个小时GPU利用率却不到40%。原因源码包里把similarity_transform、关键点检测这类CPU密集型操作放进了Dataset.__getitem__。每个训练batch都在CPU上做实时人脸仿射变换GPU一直在空转等待。解决把预处理提前到数据加载阶段完成一次性把对齐后的图像和标注存成npy文件。比如MPIIFaceGaze的60x60图像本身就是对齐过的不需要重复变换GazeCapture的原始裁剪图则要对你选定的人脸关键点做一次离线对齐保存为.npy。训练时只需要np.load后做标准化即可。我一般会写一个preprocess_and_cache.py跑一次要十几分钟但之后每次训练能快几倍。5.3 坑三测试时忘记切换预处理方式导致精度掉一截现象训练集上loss很低测试集上角度误差非常大且不同epoch之间波动明显。原因训练时使用了数据增强随机亮度、随机裁剪、随机旋转测试时却仍然执行同样的随机增强或者测试时没做归一化而训练时做了。更隐蔽的是测试时使用了和训练时不一样的图像分辨率——比如训练输入是64x64测试输入是60x60主干网络里有AdaptiveAvgPool的话不会报错但特征分布已经被改变了。解决写一个inference_pipeline把测试图像的预处理写死resize到同一尺寸用同一组均值方差做标准化关掉所有随机操作。这段代码里唯一可能出现意外的是bias问题——如果训练时用的是ImageNet的均值方差而你的人脸图像是灰度图那就得在加载时把单通道图像复制成三通道再输入否则标准化后均值差得非常大模型输出预测几乎等于直接把bias传递到了输出层。5.4 坑四显存不足但一直有人在网上说这模型很轻量现象将模型在单张3090上跑batch size要调到16以下才能不OOM但论文里写的是batch size 64。原因很可能是源码包在进行episode回传时没有释放内循环的计算图。支持集和查询集的梯度图叠加在一起显存需求翻倍。更常见的情况是你在main.py里的torch.cuda.set_per_process_memory_fraction设了一个过小的值直接把显存卡死了。解决优先用torch.cuda.empty_cache()在log_interval处做释放其次检查backbone是否被重复forward了多次。MAML风格的实现里每一轮内循环更新都会在inner_model上调用一次backward这些计算的autograd图默认不会在下一轮开始时自动释放干净你需要在进入下一个episode之前显式地销毁计算图引用inner_model None。5.5 坑五模型过度拟合到“头部姿态平均脸”而非视线现象测试结果对GazeCapture数据特别差角度误差在10度以上但MPIIFaceGaze测试集上表现尚可。原因GazeCapture的头部姿态分布比较广很多用户的图像里同时包含正脸和侧脸。如果训练时没有把头部姿态作为输入特征或者辅助监督模型会退化成靠人脸朝向猜视线方向——因为人脸外观和头部姿态的相关性太强模型偷懒走了捷径。MPIIFaceGaze的张数毕竟少且多为正面视角给了模型一种“只要判断头部姿态就能近似视线”的错觉。解决这种场景下我会去看源码包代码确认模型是否支持把头部姿态向量head拼接到最后一个卷积特征图上。如果支持就打开这个开关把3维头姿特征拼进去如果不支持最简单的方法是增加训练时GazeCapture的采样权重让模型多见到头部姿态变化的样本。另一个方案是增加一个辅助任务让模型同时预测头部角度和视线角度把两个任务在特征层的梯度加在一起回传。这个在代码上的改动很小通常只需在loss计算时再加一项loss loss_gaze 0.3 * loss_head。6. 优化与验证技巧跨数据集的评估指标与三种有效的优化方向6.1 角度误差是唯一有说服力的评估指标视线估计领域跨数据集评估时最常见的指标是视线角度误差单位是度。计算方式是把预测向量与真实向量都归一化后求它们夹角的角度。这个指标的好处是跟图像分辨率、相机内参和模型输出维度无关能把你MPIIFaceGaze和GazeCapture上的结果直接横向比较。def gaze_angular_error(pred, target): 计算视线角度误差度pred和target均为(N, 3)的向量 pred_norm pred / np.linalg.norm(pred, axis1, keepdimsTrue) target_norm target / np.linalg.norm(target, axis1, keepdimsTrue) cos_theta np.clip(np.sum(pred_norm * target_norm, axis1), -1.0, 1.0) return np.degrees(np.arccos(cos_theta))这里有一个很容易被忽略的点大多数源码包用arccos计算角度时数值上会把cos_theta四舍五入到小数点后两位再做clip这会在误差较小时引入误差。比如两个向量夹角接近0度时clip会直接把结果截断成0从而让平均角度误差看起来偏小。建议你不要做这个四舍五入直接原值clip到[-1,1]即可。评估时还要注意一个边界跨数据集时一定要分“同域测试”和“跨域测试”两个维度来看。同域测试比如在MPIIFaceGaze的2个保留用户上测反映模型的拟合与泛化能力跨域测试用MPIIFaceGaze训练在GazeCapture的保留用户上测则反映模型的去偏和泛化能力。如果你的优化方向让同域误差下降但跨域误差大幅上升说明你正在过拟合到源域的图像风格上而不是学到通用的视线特征。6.2 三个值得尝试的优化方向第一个方向是特征解耦。few-shot视线估计的本质矛盾是模型既要提取与身份相关的面部特征又要提取与视线方向相关的纯几何特征。但如果这两类特征混在一个向量里模型必然会以身份特征为线索来预测视线——因为面部形状和视线方向在训练集里存在统计相关性。我试过向源码包的fc层之前加入一个梯度反转层Gradient Reversal Layer让特征提取器只保留对用户身份不敏感的信息在MPIIFaceGaze上效果不错跨域误差能降低1.5度左右。第二个方向是姿态增强。训练阶段把人脸图像做虚拟旋转生成多个姿态下的样本比直接做颜色增强有效得多。由于MPIIFaceGaze的头部姿态大部分集中在正脸模型天然偏向预测小角度视线把训练数据的头部姿态分布往两侧拉伸可以显著降低极端视线下的误差。具体实现上可以用OpenCV的warpAffine对图像做±20度以内的随机旋转同时把对应的视线向量也旋转同样角度——这一步别忘了也要改标注。第三个方向是自适应归一化。如果源码包里使用了BatchNorm跨域时BatchNorm的统计量会严重失效。我自己的经验是把主干网络里的BatchNorm在测试阶段改为InstanceNorm或者在微调阶段冻结BatchNorm的running_mean和running_var只更新卷基层的参数。这样做的逻辑是BatchNorm统计的是训练集整体特征分布而few-shot测试时目标用户只有少量样本数据分布和训练集差异很大此时用训练集的全局统计量比用仅几个样本估计出来的统计量更稳定。6.3 用GazeCapture做最终验收从10-shot到50-shot的曲线判断模型质量最后说一下验证方法。不要只看一个点上的精度。一个可靠的few-shot视线估计模型应该展示出这样的趋势测试时提供给模型的标注样本数从1、5、10、20、50逐渐增加模型的角度误差相应稳步下降且下降曲线平滑。如果你画出来的是10-shot和50-shot精度几乎一样那说明你的模型本质是个固定映射器并没有真正利用少样本标注信息来做自适应。这个曲线图放在毕设答辩时是非常有说服力的实验证据。我自己的习惯是把这组数据整理成表格模型名称、backbone、预训练域、1/5/10/20/50-shot角度误差、参数数量。可以验证你复现的源码包是否真的实现了few-shot能力也可以用来对比你自己优化的效果。每当我拿到一个新的源码包第一时间就画这个曲线等于是在几小时内给代码的声称能力做了一次压力测试。最后提一个血泪经验永远保留一份跑通过的数据预处理缓存和训练日志备份。运行几个月后的某天你会发现当初能出一张好结果图的代码竟然因为上个月改了requirements.txt里一个看似无关的包版本而在同样的命令下彻底训练不动了——这时临时开个独立环境往里倒腾通常没戏还不如直接翻你备份时记录的输出日志和当时的commit信息来得快。希望这篇记录能让你少走这些弯路最好一次跑通。本文还有配套的精品资源点击获取
返回列表