3. 模型转换与量化:把PyTorch/TensorFlow模型塞进Eyeriss

好,到了第三章。前面两章我们把Eyeriss的硬件架构和工具链摸了个大概,现在该动真格的了——把训练好的模型,从PyTorch或者TensorFlow那边,搬到Eyeriss能认得的地盘上,顺便给它做个“瘦身”,也就是INT8量化。

这一步,说白了就是“翻译+压缩”。翻译不好,模型跑不起来;压缩不好,精度崩给你看。我当年第一次做这个环节,差点被一个量化后的精度掉点搞到通宵,后来发现是校准集没选对。嗯,这些坑咱们今天一个个填上。

3.1 模型转换:从框架到Eyeriss中间表示

Eyeriss不认PyTorch的.pth,也不认TensorFlow的.pb。它有自己的中间表示格式,通常是基于ONNX或者自定义的IR(Intermediate Representation)。我个人习惯走ONNX这条路,生态好,工具链成熟。

3.4.1 PyTorch → ONNX → Eyeriss IR

流程其实不复杂,但细节多。我给你拆成几步:

  1. 导出ONNX:用torch.onnx.export(),注意设置opset_version,我一般用13或更高。
  2. ONNX简化:用onnx-simplifier去掉一些冗余节点,比如Identity、Cast。
  3. ONNX转Eyeriss IR:调用Eyeriss SDK提供的eyeriss_convert工具。

核心要点:ONNX导出时,dynamic_axes参数要谨慎使用。Eyeriss目前对动态shape支持有限,我建议固定batch size为1,省得后面报错。

import torch
import torchvision.models as models

model = models.resnet18(pretrained=True)
model.eval()

dummy_input = torch.randn(1, 3, 224, 224)

torch.onnx.export(
    model,
    dummy_input,
    "resnet18.onnx",
    opset_version=13,
    input_names=['input'],
    output_names=['output'],
    dynamic_axes={
        'input': {0: 'batch_size'},
        'output': {0: 'batch_size'}
    }
)
print("ONNX导出完成")

然后跑简化:

python -m onnxsim resnet18.onnx resnet18_sim.onnx

最后转成Eyeriss IR:

eyeriss_convert --input resnet18_sim.onnx --output resnet18.eyir --target eyeriss_v2

3.1.2 TensorFlow → ONNX → Eyeriss IR

TensorFlow这边稍微麻烦点,因为TF的算子命名和ONNX不完全对齐。我建议用tf2onnx这个工具。

import tensorflow as tf
import tf2onnx

model = tf.keras.applications.MobileNetV2(weights='imagenet')
spec = (tf.TensorSpec((1, 224, 224, 3), tf.float32, name="input"),)

output_path = model.output_names[0]
onnx_model, _ = tf2onnx.convert.from_keras(
    model, input_signature=spec, opset=13, output_path="mobilenetv2.onnx"
)
print("TF转ONNX完成")

注意:TF的BatchNormalization层在转换时可能会被折叠,导致ONNX里的scale/mean参数精度丢失。我曾经因为这个,量化后精度掉了3个点。解决办法:转换前先冻结BN层,或者用tf2onnx的--fold_const选项。

3.2 INT8量化:精度与速度的博弈

量化,就是把FP32的权重和激活值,用INT8来表示。好处很明显:模型体积缩小4倍,推理速度提升2-4倍,功耗也降下来。但代价是精度损失。

Eyeriss支持两种量化方式:训练后量化(PTQ)和量化感知训练(QAT)。我一般先试PTQ,不行再上QAT。

3.2.1 训练后量化(PTQ)

PTQ不需要重新训练,只需要准备一小部分校准数据,让Eyeriss的量化工具统计出每层激活值的范围,然后算出scale和zero_point。

步骤:

  1. 准备校准集:从训练集里抽200-500张图片,覆盖各类场景。
  2. 运行校准:用eyeriss_quantize工具,指定校准数据路径。
  3. 验证精度:对比量化前后的精度差异。
eyeriss_quantize \
    --model resnet18.eyir \
    --calib_data ./calib_images/ \
    --calib_size 200 \
    --output resnet18_int8.eyir \
    --mode ptq

我的经验:校准集不要只用同一类图片。比如做分类任务,每个类别至少抽几张。否则量化后的模型会对某些类别“偏科”,精度掉得厉害。

3.2.2 量化感知训练(QAT)

如果PTQ后精度掉点超过1%,那就得上QAT了。QAT在训练过程中模拟量化效果,让模型自己学会适应低精度。

Eyeriss SDK提供了QAT的PyTorch插件,用法和PyTorch自带的torch.quantization类似:

import torch
import eyeriss_quant

model = torch.load("resnet18.pth")
model.qconfig = eyeriss_quant.get_default_qconfig('eyeriss_v2')

# 插入量化/反量化节点
eyeriss_quant.prepare_qat(model, inplace=True)

# 继续训练几个epoch
for epoch in range(5):
    for data, target in train_loader:
        output = model(data)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()

# 转换并导出
eyeriss_quant.convert(model, inplace=True)
torch.save(model.state_dict(), "resnet18_qat.pth")

注意:QAT训练时,学习率要调小,一般是原来的1/10。我见过有人直接用原学习率,结果模型直接不收敛了。

3.3 量化参数与精度对比

下面这张表,是我在实际项目中总结的,不同量化方式的效果对比:

量化方式 模型大小 推理速度 精度损失 适用场景
FP32(基准) 100% 1x 0% 精度优先
INT8 PTQ 25% 2-3x 0.5%-1% 快速部署
INT8 QAT 25% 2-4x 0.1%-0.5% 精度敏感

你看,QAT虽然训练时间长一点,但精度损失小很多。如果你的模型要部署在医疗、自动驾驶这类场景,我建议直接上QAT。

3.4 常见问题与避坑指南

  • 算子不支持:Eyeriss的IR不是所有ONNX算子都支持。遇到不支持的,要么替换成等价算子组合,要么走Eyeriss的自定义算子接口。我遇到过Softmax不支持,后来用Exp+ReduceSum+Div替代了。
  • 量化后精度崩了:先检查校准集是否合理,再检查是否有异常值(比如输入数据没归一化)。我曾经因为一张全黑图片混进校准集,导致量化后的模型输出全是0。
  • 转换后模型跑不起来:多半是输入输出的shape对不上。用eyeriss_inspect工具查看IR的输入输出信息,和你的预处理代码对比一下。

3.5 本章知识体系

下面这张图,把模型转换和量化的核心流程串起来了。你一看就明白:

模型转换与量化流程 模型转换与量化核心流程 PyTorch模型 (.pth) ONNX导出 ONNX简化 TF模型 (.pb/.h5) tf2onnx转换 ONNX简化 Eyeriss IR PTQ量化 QAT量化 INT8量化模型 校准集:200-500张,覆盖各类场景 QAT:学习率调为原1/10

嗯,到这里,模型转换和量化的核心内容就讲完了。你手头如果有现成的模型,可以照着上面的步骤跑一遍。遇到问题别慌,翻翻前面的避坑指南,大部分都能解决。