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

资讯详情

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

少样本视线估计复现与优化:从数据预处理到微调实验

少样本视线估计复现与优化:从数据预处理到微调实验 简介这套项目源码面向需要完成视线估计方向毕业设计或开展相关研究的开发者完整复现并优化了Seonwook Park提出的few_shot_gaze工作基于MPIIFaceGaze与GazeCapture数据集使用Python实现数据预处理、训练与评估全流程。压缩包共93个文件大小约13.49MB核心由41个Python脚本组成并配有模型权重与配置文件、Shell脚本、说明文档、演示图片、npy特征文件及Jupyter Notebook示例可支撑从环境准备到模型训练的完整链路。项目按功能拆分了元学习、损失函数、骨干网络、数据管理、演示等模块目录结构清晰内置多种视线估计损失与训练测试脚本便于读者对照原论文复现实验、调整超参数或替换数据集扩展。还提供人脸校准、坐标归一化等预处理工具可直接运行演示快速验证效果。目前已有287人学习适合具备一定深度学习基础、希望以few_shot_gaze作为毕设题目的本科或研究生参考使用。1. 复现 few-shot-gaze 之前先看 MPIIFaceGaze 和 GazeCapture 要解决什么问题手里拿到一份 few-shot-gaze 项目的 Python 源码又被 MPIIFaceGaze 和 GazeCapture 两个数据集卡住这大概是视线估计方向做毕设的人最熟悉的第一道坎。这个方向真正要解决的不是把模型在单个数据集上练到多准而是换一个人、换一台设备之后模型能不能靠极少标注快速恢复精度。全量训练谁都会跑答辩时能讲清复现链路、并拿出优化实验才不亏。这篇笔记从数据集差异、环境搭建、训练命令、避坑到评估顺序写目标是用最少调试次数把这条线真正跑通。2. few-shot-gaze 的原理与两个数据集的底细先搞懂为什么是这两个数据集很多同学拿到源码包第一件事是建环境、装依赖、直接开训但 few-shot-gaze 这种项目十个有九个失败在数据没有对齐而不是模型代码。要复现并优化得先接受一个前提它不是把某个大数据集当作“一次性训练全部”而是先让模型在一个通用表示上预训练再用新用户极少量的标注样本做快速适配。这个“先预训练、后少样本微调”的流程决定了你在数据上花的时间不会少于调参。2.1 跨数据集与小样本微调few-shot gaze 真正解决的痛点视线估计是强依赖个体差异的任务。瞳距、眼型、头部转动方式、光照环境和屏幕距离都会影响同一个人的真实注视方向。一个模型只在实验室数据上训练拿到手机前置摄像头场景误差可能从 4 度掉到 10 度以上这种大幅下跌就是跨域差距。要消除差距通常需要重新采集大量目标域数据但现实里不可能让每个新用户都标几百张图尤其考虑到隐私和数据成本。few-shot-gaze 的思路正是预训练加支撑集微调。先用一个大数据集训练出一个通用视线特征提取器面对新用户时只取 5 到 50 张带标注的支撑集support set冻结骨干网络的大部分参数只更新最后的回归头和 BatchNorm 统计量让模型快速适应这个人。这样既避开了昂贵的全量标注又能在新域上把误差压到可用范围。那为什么偏偏是 MPIIFaceGaze 和 GazeCapture 这两个数据集因为它们形态互补。MPIIFaceGaze 是在实验室屏幕前采集的受试者人数不算多但每帧都带较干净的头部姿态和单位视线向量GazeCapture 是手机前置摄像头采集的用户规模大覆盖大量真实世界的眼型、光照和手持姿态缺点是样本嘈杂原始标注只是屏幕注视点坐标。常见做法是用 GazeCapture 训练通用特征再用 MPIIFaceGaze 的受试者作为“新用户”做 few-shot 评测。反过来用也可以但 GazeCapture 的域更广预训练后迁移到实验室场景往往更稳。少样本在这条任务里不是分类里的“每类几张图”而是“每个人几张图”。评测时按受试者划分训练阶段绝不把同一个人的图片塞进预训练集否则模型见过这个人测评就失去意义了。这一点后面评估章节还会再展开也是判断源码包是否可复现的关键。2.2 从输入到标签视线向量、头部姿态和注视点坐标怎么统一两个数据集的标签空间不一致是复现 few-shot-gaze 最容易踩的结构性差异。MPIIFaceGaze 给的是三维视线方向单位向量通常表示为 gaze [gx, gy, gz]GazeCapture 的标注更接近“用户在看屏幕哪个位置”也就是二维屏幕坐标。一个在相机或头部坐标系里一个在屏幕平面里两者不能直接放同一个回归头训练。所以拿到源码包后先检查它如何处理这两个数据集的输出。有些项目干脆做两个输出头一个预测三维视线向量一个预测屏幕坐标共享同一个骨干网络。这种做法预训练省力但 few-shot 微调时只能针对某个输出头跨评测不太直观。另一种常见做法是在预处理阶段把 GazeCapture 的屏幕坐标结合相机内参、头部姿态转换成三维方向向量再统一成同一套标签空间。后者复现成本高但后续训练和评估都干净很多也更容易画误差曲线。输入侧同样要统一。图像通常裁成人脸框有的项目还会进一步取左右眼区域横排拼接后缩放成 224×224 或 160×160。在送入网络前如果有头部姿态标注强烈建议先做一次“归一化正脸”利用头部旋转矩阵把图像 warp 到正对相机的位置。这一操作能显著减少头部姿态变化给回归带来的歧义算是视线估计里最有效的一步预处理但很多源码包并不会默认开启属于你在优化阶段可以动手补的第一块短板。损失函数选择上也别随大流用坐标 MSE。三维视线方向是单位向量MSE 对离群的大角度误差过度惩罚而且单位向量的三个分量存在相关性。更稳妥的是余弦角度损失也就是 1 减去预测和标注向量的余弦相似度评测时换算成角度误差后直接以度为单位呈现。表格里把两套数据集的核心差异整理清楚了动手前先对照看一遍。对比项MPIIFaceGazeGazeCapture采集环境实验室屏幕前摄像头相对固定手机或平板前置摄像头用户手持使用用户规模规模较小按受试者组织上千人级别覆盖真实用户每帧标注头部姿态、三维视线单位向量、人脸框屏幕注视坐标、用户与会话信息标签坐标系头部或相机坐标系屏幕二维坐标系常见用途少样本评测、跨用户验证大规模预训练、学习多样眼型2.3 用脚本把 MPIIFaceGaze 和 GazeCapture 整理成同一份标注表我一般建议不管源码包有没有自带 DataLoader先把两个原始数据各自整理成一个只有 image_path、label_vector、split 三列的 CSV。后面训练要吃哪部分、画曲线、做消融、排查奇怪问题都只跟这一份表打交道。第一步永远是打印原始数据结构别急着写正式加载器。下面这个小脚本适用于 MPIIFaceGaze 这类以 .mat 为载体的数据集直接套用即可。# inspect_data.py复现第一步永远是先看结构别急着套 transform import scipy.io as sio mat sio.loadmat(MPIIFaceGaze/P00/data.mat) for key in mat: if not key.startswith(__): data mat[key] print(key, type(data), data.shape) if data.shape[0] 0: print(data[0])代码逻辑很简单用 scipy 读取 .mat遍历所有非隐藏字段打印类型和形状重点是看第一个维度是样本数还是 cell 数组。很多源码包会在内部做np.squeeze(data)把形状从 1×N 压成 N如果你照着自己写循环极容易把维度多取一层。所以这段脚本的关键用法不是跑完就删而是每换一台新机器、每换一个数据版本都先确认字段名和 shape 没变。第二步处理 GazeCapture 的动态 JSON 结构。不同版本目录布局差异很大我给出一个基于文件名通配的遍历模板。# build_annotations.py把 GazeCapture 整理成 CSV实际字段名按真实包调整 import json import pandas as pd from pathlib import Path def collect_gazecapture(gc_root): rows [] for info_path in Path(gc_root).rglob(*Info.json): meta json.loads(info_path.read_text()) rows.append({ user: meta.get(user, info_path.parent.name), image_root: str(info_path.parent), gaze_on_surface: meta.get(gaze_on_surface), }) return pd.DataFrame(rows) ann collect_gazecapture(GazeCapture) print(ann.shape) ann.to_csv(gazecapture_annotations.csv, indexFalse)说明一下参数与逻辑rglob(*Info.json)是递归匹配所有叫 Info 的 JSON 文件如果匹配结果为空先执行list(Path(gc_root).rglob(*.json))看看包里到底有哪些文件名别硬猜。gaze_on_surface这一列在真实数据里可能是缺失值、可能是字符串、也可能是长度不一的列表生成 CSV 后统一检查这一列的 dtype缺失的直接过滤掉。最后按行写入 CSV后续训练时按 user 列做身份划分比在训练循环里反复读原始 JSON 快得多。3. 搭建 Python 环境并跑通最小复现Python 依赖、预处理和训练命令3.1 环境准备Python 版本、依赖库与目录建议few-shot-gaze 的依赖不算浮夸常见组合是 PyTorch、OpenCV、NumPy、SciPy、Pandas。Python 版本建议用 3.8 或 3.9没必要追新某些老源码用到了已废弃接口新版本 Python 反而会报 deprecation 导致运行失败。先把虚拟环境建好避免把系统 Python 环境搞乱。python -m venv .venv source .venv/bin/activate pip install --upgrade pip pip install numpy scipy pandas opencv-python tqdm tensorboard pip install torch torchvision第一行创建虚拟环境第二行在 Linux 或 macOS 下激活Windows 下把命令换成.venv\Scripts\activate。依赖安装里最需要注意的是最后一行的 torch默认 pip 源通常会装到 CPU 版如果你机器有 NVIDIA GPU先到 PyTorch 官网复制对应 CUDA 的安装命令否则torch.cuda.is_available()会返回 False后面训练速度差好几倍。装完后跑一条检查命令python -c import torch; print(torch.__version__, torch.cuda.is_available())如果不打算上 GPU其实 few-shot 阶段也能跑因为支撑集只有几十张图迭代很快。但预训练 GazeCapture 那个阶段不推荐纯 CPU 硬磕动辄几天起性价比太低。目录建议按data/raw、data/processed、checkpoints、logs分层放不要像我踩过坑那样把原始数据路径写死在脚本里后面换机器全部要重改。3.2 数据预处理把两套图像切成统一输入目录预处理阶段的统一准则是“训练和评估必须走同一套变换”。很多源码包在训练时转灰度、测试时不转或者训练时用 224 分辨率、评估时用 160这些不一致都会让复现少则差两度、多则直接崩。下面给一个最基础的整脸裁剪函数输入一张原图和一个人脸框输出模型要的 224×224 图。# preprocess.py统一裁剪并缩放 import cv2 import numpy as np def normalize_face(img, bbox, size(224, 224)): x, y, w, h [int(v) for v in bbox] face img[y:y h, x:x w] if face.shape[0] 0 or face.shape[1] 0: return np.zeros((size[1], size[0], 3), np.uint8) return cv2.resize(face, size) img cv2.imread(data/raw/MPIIFaceGaze/P00/00001.jpg) bbox [120, 80, 260, 260] # 以源码包自带的人脸框为例 face normalize_face(img, bbox)这段代码的关键坑在 bbox 格式。OpenCV 裁剪用的约定是 x、y、w、h也就是左上角坐标加宽高但如果源码包给的是 x1、y1、x2、y2即左上和右下两个点直接把后两个数当 w、h 用crop 范围就会错得离谱。我建议在预处理入口加一行注释打印出第一个样本的前后 shape确认 bbox 语义后再批量跑。如果项目用的是双眼中部区域并不是整天脸常见做法是在脸部框基础上取中间偏上部分大约占整脸高度三分之二。注意别在做随机旋转时只转图像不转标签视线方向里的 pitch 和 yaw 会被旋转直接影响处理不对等于给标签注入系统误差。3.3 最小复现命令预训练后做 support set 微调few-shot-gaze 的复现至少分两个阶段先在 GazeCapture 上预训练再在 MPIIFaceGaze 的新受试者上做 few-shot 微调。源码包里的入口脚本未必叫 train.py但参数思路一致下面这组命令是常见模板。python train.py --phase pretrain \ --dataset gazecapture \ --input-size 224 \ --batch-size 32 \ --epochs 15 \ --lr 1e-3 \ --out ./checkpoints/pretrain_gc.pth python train.py --phase finetune \ --dataset mpii_face_gaze \ --support-size 10 \ --lr 5e-4 \ --epochs 5 \ --load ./checkpoints/pretrain_gc.pth \ --out ./checkpoints/finetune_s10.pthpretrain 阶段用 GazeCapture 学通用特征batch-size 32 在常见消费级显卡上可以跑显存不够就降到 16 或 8但别把图片分辨率也随意缩小否则预训练特征和微调时用的输入分布不匹配。finetune 阶段里support-size 10 表示每个新用户只取 10 张标注图做微调这是 few-shot 的含义所在如果脚本里没有这个参数多半意味着它还在全量数据集上训练只是名字叫 few-shot需要回去改数据加载逻辑。微调学习率要比预训练小一个数量级5e-4 到 5e-5 之间都出现过有效配置从 5e-4 起步loss 稳不住就降。epochs 在支撑集很小的场景下不要给太多5 到 20 轮足够再多会把十张图背下来测试误差反而上升。训练完保存的 checkpoint 最好同时存一份配置文件记录用了哪些支撑集样本、数据划分版本和随机种子这一步直接影响后续排查。4. 优化 few-shot-gaze 的 3 个实验方向增强、微调策略与评估口径4.1 数据增强与归一化哪些操作对视线回归真正有效视线回归的数据增强有一条铁律图像做了几何变换标签必须跟着变换。水平翻转看起来是最便宜的增强但翻转后左右眼互换视线向量的 x 分量必须取反否则方向就指向屏幕外面了。MPIIFaceGaze 因为是实验室固定头部中位姿态翻转后的样本依然合理所以这个增强收益可观但只做一半的源码也最坑。在 GazeCapture 上我更看好亮度扰动和小范围平移。手机采集过程中光照变化本来就是真实分布的一部分对亮度做 ±30 级别的随机扰动能模拟不同环境随机平移幅度控制在 0.1 以内相当于模拟人脸检测框的小抖动。至于随机擦除也就是把图像里一块区域随机涂黑对分类任务有正则效果但对视线估计这类细粒度回归任务很容易破坏关键眼部线索我只建议在预训练阶段适度加few-shot 微调阶段老老实实关掉。优化前先把所有增强关掉跑通一条原始基线。然后再逐项打开每次只动一个开关记录训练集和测试集的角度误差变化。视线估计这类任务非常容易出现过拟合你自己都会分不清是增强起了作用还是这次随机划分运气好。这也是为什么后面强调固定划分和随机种子。4.2 微调策略与超参数支撑集数量、学习率、冻结层few-shot 微调最怕的是把整个网络一起更新。支撑集只有十张图时反向传播一旦进入骨干网络预训练学到的通用特征很快被破坏测试误差不降反升。常见的做法是冻结骨干网络的大部分参数只更新最后的视线回归头和 BatchNorm 的统计量。BatchNorm 的 gamma 和 beta 对特征分布影响很大放开这些参数去适配新用户相当于只做轻量调整这正是 few-shot 的核心操作。学习率在微调阶段通常设为预训练的五分之一到十分之一。我的经验值是 1e-4 起步如果 loss 震荡就降到 5e-5。支撑集数量从 5、10、20、50 往上加时结果会呈现一条近似幂律下降的曲线5 张能看到效果10 张是性价比拐点20 张基本接近全量微调的表现超过 50 张就不再算 few-shot 了。毕设论文里画这条曲线会比只报一个最终误差更有说服力。超参数建议值说明support-set 数量10205 张起步20 张接近收益拐点微调学习率1e-45e-5从 1e-4 起震荡则下调微调轮数520支撑集小就用较小轮数batch-size832显存不足优先降 batch别降分辨率优化器Adamweight decay 设 1e-4 附近当支撑集只有十几张图时还可以在同一个用户上重复采样几轮让 BN 统计量跑稳再进入评估。少数源码包支持把支撑集写入一个独立文件训练脚本读它而不是读整个数据目录这个设计比较合理复现时优先保留。4.3 评估口径与消融实验角度误差、身份划分和置信区间评估视线估计最常用的指标是平均角度误差单位是度。计算方式很简单预测向量和标注向量做点乘除以两者模长得到余弦相似度再反余弦得到弧度乘 180 除以圆周率。下面这段函数可以直接用在测试集上。import torch def angle_error(pred, target): pred pred / pred.norm(dim-1, keepdimTrue).clamp_min(1e-6) target target / target.norm(dim-1, keepdimTrue).clamp_min(1e-6) cos_sim (pred * target).sum(-1).clamp(-1.0, 1.0) rad torch.acos(cos_sim) return rad * 180 / 3.141592653589793代码里两次归一化保障输入是单位向量clamp(-1.0, 1.0)防止数值误差让余弦相似度越界变成 NaN。这个函数输出的是每个样本的角度误差后面可以算 mean、median、p90。只报 mean 很容易被少数离群样本带偏剖面完整时才可信。评估划分必须按受试者或用户 ID 做不要在全局随机拆分帧。道理很直白模型已经在预训练阶段见过大量普通用户如果测试样本里恰好混入同一个用户的脸误差自然漂亮但这代表不了少样本迁移能力。正确做法是把用户 ID 分成三份预训练阶段只用其中一部分few-shot 阶段从没见过的另一部分用户里采样支撑集测试再用第三部分。同一套划分文件从头用到尾。消融实验建议固定四组不做微调的预训练直接测、support-5 微调、support-10 微调、support-10 加数据增强。每一次都用同一份支撑集和同一随机种子记录 mean 和 p90。这组结果放进论文就是完整的复现与优化证据链。5. 复现 few-shot-gaze 常见问题与避坑清单从数据集翻车到显存爆炸5.1 MPIIFaceGaze .mat 加载后维度不对训练脚本直接崩现象训练脚本进入第一个 epoch 就报维度错误或者看似在跑打印出的 batch shape 多一维loss 一会正常一会爆掉。原因MPIIFaceGaze 的 .mat 文件是嵌套 cell 结构直接用squeeze会把一维容器压平导致索引方式和你脚本里的假设不一致。另一个常见原因是机器上的 scipy 版本差异旧版本读取 struct 数组的字段顺序和新版本不完全一致。解决先跑一段只打印 key 和 shape 的检查脚本确认每个字段的第一维到底代表样本数还是嵌套容器。索引时逐维写清楚例如data[0, 0][face]这类写法如果拿不准就先用shape打印结果对照。不要依赖squeeze之后的“看起来对”加一段断言校验第一帧的标签模长是否约等于 1一次性把结构锁死。5.2 GazeCapture 注视点坐标错位loss 高到像在看随机数现象预训练 loss 一直很高图像可视化发现标注点不在眼睛应该看的位置同一个用户不同 session 的坐标范围都不一致。原因GazeCapture 是移动设备采集屏幕坐标的零点可能在左上角也可能在中心用户手持过程中设备方向还会变。很多第三方整理过的目录结构把注视坐标和原图对应关系弄丢坐标错位成为常态。解决先把坐标范围统一归一化到 -1 到 1并单独按用户和 session 画散点图看屏幕上的注视点分布是否符合常识。对落在屏幕范围外或缺失的帧直接过滤。这一步肉眼检查必须做否则后面所有训练和评估都建立在一堆错位上。5.3 显存不足与 CPU 长时间空转现象预训练阶段 batch-size 调到 16 还是 OOMfew-shot 阶段也报显存爆炸日志里 GPU 利用率很低CPU 却长时间载入。原因显存不够大多是因为输入分辨率过大、batch 过大或者是 dataloader num_workers 设置不合理导致 CPU 瓶颈。少样本微调阶段如果加载器还在遍历整个数据集也会白白占用内存和 CPU。解决预训练分辨率从 224 降到 160batch 降到 8 或 16必要时用梯度累积模拟更大 batch。few-shot 阶段只把支撑集几百张图放进 DataLoader不要走全量目录扫描。验证一下训练循环里是否有torch.cuda.empty_cache()操作它有时反而会拖慢速度该清的是 session 而不是每次迭代。5.4 预训练权重不兼容微调后指标反而更差现象加载预训练 checkpoint 时 key 对不上强制加载后微调 loss 不动甚至测试误差比不微调还高。原因预训练和微调阶段的输入分辨率、归一化方式、输出头维度不一致。比如预训练时用 ImageNet 均值归一化微调时用了数据集自身均值模型看到的输入分布直接变了。又或者 GazeCapture 分支是二维坐标头MPIIFaceGaze 是三维视线头两个 head 尺寸不同强行加载 backbone 时忽略了头部。解决只加载两个模型共用的骨干网络部分从随机初始化开始训练新的回归头。把图像归一化统一成一份全局预处理代码训练和评估都用同一函数。加载后跑几步确认 loss 确实从预训练水平附近起步而不是从完全随机的位置重启。5.5 复现结果不稳定随机种子与数据划分不一致现象同一份代码连续跑三次测试误差差了 2 到 5 度甚至出现 support-20 比 support-10 还差的情况。原因PyTorch、NumPy、Python 内置 random 的种子至少有一处没固定或者数据划分每次都在内存里临时 shuffle导致支撑集选择不同。解决脚本开头一次性固定三种随机种子torch.manual_seed(seed)、np.random.seed(seed)、random.seed(seed)并把划分文件提前生成保存。报告结果时跑三次取均值同时记录每次的角度误差范围。种子和数据划分是复现实验最基本的后悔药省这一步后面所有优化结论都站不住。6. 验证 few-shot-gaze 复现效果的三个进阶技巧累计误差曲线、身份划分与种子存档先算每个测试样本的角度误差然后不要急着看平均值画一条累计误差曲线。横轴是角度误差从 0 到大约 15 度纵轴是小于等于该误差的样本占比。曲线越往左上角靠说明模型在低误差阈值下覆盖的样本越多这比单个 mean 更能反映少样本微调的真实表现。画完这张图很多答辩问题都有了依据。import numpy as np import matplotlib.pyplot as plt errs np.sort(error_list) plt.plot(errs, np.arange(1, len(errs) 1) / len(errs)) plt.axvline(5.0, colorgray, linestyle--) plt.xlabel(angular error (deg)) plt.ylabel(cumulative ratio) plt.title(few-shot gaze evaluation) plt.show()代码里np.sort得到从小到大的误差序列分母用样本总数得到每个点的累积比例灰色虚线标出 5 度阈值这是视线估计里常用来衡量可用的标准线。如果曲线在 5 度附近已经到 0.8 以上说明这个模型对大多数样本都足够准。误差平均值会被少数极端样本抬高曲线则不会骗人。第二个技巧是坚持按身份划分数据并且把划分结果存成文件。读数据时按用户 ID 分组预训练、支撑集、测试三部分各占不重叠的用户集合。支撑集要明确记录取了哪个人、哪个 session、哪几张图甚至把图像文件的哈希写进配置。后面每次实验都要加载同一份划分文件不然你今天查优化项明天差的是数据分布变量被污染工作等于白做。第三个技巧是把随机种子写进 checkpoint 文件名。我现在的习惯是checkpoints/pretrain_gc_seed42.pth微调输出类似finetune_mpi_s10_seed42.pth一眼能看出哪次实验对应哪种配置。很多人只看重最终精度忽略了复现链条的完整结果换个环境重跑时连自己当初改了哪些参数都说不清。视线估计里的误差波动本来就小种子版本对不上解析不到真因。这一套流程跑下来你既能向别人说清楚 few-shot-gaze 在解决什么跨域问题也能在优化项上拿出支撑集数量、增强开关和身份划分的消融证据。我现在复现任何视线估计项目都会第一天就把数据集指纹、划分版本和随机种子写进日志之后任何一次指标变玄学先查划分版本而不是改超参这是我被数据划分坑过多次后养成的习惯。希望这篇笔记能帮你在毕设里少绕两步把 few-shot 这条线真正跑出可信的结果。本文还有配套的精品资源点击获取
返回列表