4. 模型训练与导出:WiderFace + RetinaFace → ONNX
好,到了真正动手的环节了。前面我们聊了数据集、模型选型,现在要把这两样东西捏到一起——用 WiderFace 训练 RetinaFace,然后导出成 ONNX,再做一波简化优化。
这一步卡住过不少人。我自己刚开始做部署时,模型在 PyTorch 里跑得飞起,一导出 ONNX 就各种报错。后来发现,很多坑其实是可以提前避开的。
4.1 训练前的准备工作
先说数据集。WiderFace 官方给的是 .mat 标注文件,训练前得转成 RetinaFace 能吃的格式。
我个人习惯转成 VOC 格式的 XML,或者直接转成 TXT(每行:图片路径、人脸框、关键点)。RetinaFace 官方代码里有个 widerface2txt.py,可以直接用。
训练配置方面,我一般用以下参数起步:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 输入尺寸 | 640×640 | 兼顾精度与速度,嵌入式常用 |
| Batch Size | 8~16 | 看显存,我常用 8 |
| 学习率 | 1e-3 → 1e-5 | Warmup + 阶梯下降 |
| Backbone | MobileNet0.25 | 轻量,适合嵌入式 |
| Epochs | 100~150 | WiderFace 数据量大,100 轮基本收敛 |
4.2 训练 RetinaFace
RetinaFace 的核心思想是:多任务学习。它同时输出三样东西:
- 人脸分类:有没有人脸?
- 边界框回归:人脸在哪?
- 关键点回归:眼睛、鼻子、嘴角在哪?
训练时,这三个 loss 会加权求和。我常用的权重是:分类 1.0、框回归 2.0、关键点 1.0。
启动训练的命令大概长这样:
python train.py --network mobile0.25 \
--dataset widerface \
--batch_size 8 \
--lr 1e-3 \
--epochs 100 \
--save_folder ./weights/
嗯,这里要注意:WiderFace 的图片分辨率差异很大,从几百到几千都有。训练时我建议做 随机裁剪 + 缩放,让模型适应不同尺度的人脸。
4.3 导出为 ONNX 格式
训练完成后,我们拿到的是 PyTorch 的 .pth 权重。但嵌入式设备不认识 PyTorch,它只认 ONNX、TFLite、NCNN 这些中间格式。
导出 ONNX 的步骤其实就三步:
- 加载训练好的模型权重
- 构造一个 dummy 输入(比如 1×3×640×640)
- 调用
torch.onnx.export()
核心代码:
import torch
model = RetinaFace(cfg_re50, phase='test', pretained=False)
model.load_state_dict(torch.load('weights/RetinaFace_mobile0.25.pth'))
model.eval()
dummy_input = torch.randn(1, 3, 640, 640)
torch.onnx.export(
model,
dummy_input,
'retinaface_mobile0.25.onnx',
input_names=['input'],
output_names=['output'],
opset_version=11,
dynamic_axes={'input': {0: 'batch_size'},
'output': {0: 'batch_size'}}
)
导出后,建议用 onnxruntime 验证一下:
import onnxruntime as ort
session = ort.InferenceSession('retinaface_mobile0.25.onnx')
input_name = session.get_inputs()[0].name
output = session.run(None, {input_name: dummy_input.numpy()})
print(output[0].shape) # 看看输出对不对
如果这一步报错,多半是模型里有动态 shape 或者某些算子 ONNX 不支持。我遇到过 torchvision.nms 导出失败的情况,后来换成自定义实现才搞定。
4.4 ONNX 简化与优化
导出的 ONNX 模型通常有冗余——比如训练时的 Dropout 层、Identity 层、多余的 reshape。这些在推理时完全没用,但会增加模型体积和推理时间。
我常用的工具是 onnx-simplifier:
pip install onnx-simplifier
python -m onnxsim retinaface_mobile0.25.onnx retinaface_sim.onnx
简化前后对比一下:
| 指标 | 简化前 | 简化后 | 变化 |
|---|---|---|---|
| 模型大小 | 4.2 MB | 3.8 MB | ↓ 约 10% |
| 算子数量 | 287 | 203 | ↓ 约 30% |
| 推理耗时(CPU) | 45 ms | 38 ms | ↓ 约 15% |
除了简化,还可以做 量化。ONNX 支持动态量化和静态量化。对于嵌入式设备,我建议先做 INT8 静态量化,精度损失通常在 1~3% 以内,但推理速度能翻倍。
4.5 本章知识结构
下面这张图把整个流程串起来了:
4.6 常见问题与避坑
- 导出 ONNX 时算子不支持:我遇到过
torch.cat和torch.split在某些 opset 下报错。解决办法是升级 opset 版本,或者手动替换成 ONNX 原生算子。 - 简化后精度下降:onnx-simplifier 99% 的情况是安全的,但偶尔会误删一些必要的节点。简化后一定要跑一遍验证集,对比精度。
- 量化后模型跑飞:这通常是因为校准数据分布和实际数据分布差异太大。我建议校准集尽量覆盖各种光照、角度的人脸。
- 动态 batch 支持:导出时如果设了
dynamic_axes,有些推理引擎(比如 TensorRT)需要额外配置。如果不需要动态 batch,建议固定 batch=1,省心。
好了,模型训练和导出就聊到这儿。下一步,我们会把这个 ONNX 模型部署到具体的嵌入式设备上——那才是真正考验工程能力的地方。