模型验证与调试
模型导出后,验证其正确性是必不可少的步骤。本章介绍如何系统地验证 ONNX 模型与原始模型的一致性,以及调试常见问题的方法。
为什么需要验证?
模型从 PyTorch/TensorFlow 转换为 ONNX 的过程中,可能出现多种问题:
- 数值差异:浮点运算顺序不同导致的结果偏差
- 算子行为差异:同一算子在不同框架中的实现细节不同
- 形状推断错误:动态形状处理不当
- 类型转换问题:隐式类型转换导致的精度损失
验证的核心目标是确保转换后的模型在相同输入下产生相同(或足够接近)的输出。
基础验证流程
第一步:模型结构检查
使用 ONNX 官方工具检查模型文件的完整性:
import onnx
# 加载模型
model = onnx.load("model.onnx")
# 检查模型结构
try:
onnx.checker.check_model(model)
print("✓ 模型结构检查通过")
except onnx.checker.ValidationError as e:
print(f"✗ 模型结构检查失败: {e}")
# 检查模型 IR 版本和 Opset
print(f"IR 版本: {model.ir_version}")
print("Opset 导入:")
for opset in model.opset_import:
domain = opset.domain if opset.domain else "ai.onnx (默认)"
print(f" {domain}: {opset.version}")
第二步:形状推断验证
确保模型的形状信息完整:
import onnx
from onnx import shape_inference
# 加载模型
model = onnx.load("model.onnx")
# 执行形状推断
inferred_model = shape_inference.infer_shapes(model)
# 检查输入输出形状
print("输入形状:")
for inp in inferred_model.graph.input:
shape = [d.dim_value if d.dim_value else d.dim_param
for d in inp.type.tensor_type.shape.dim]
print(f" {inp.name}: {shape}")
print("输出形状:")
for out in inferred_model.graph.output:
shape = [d.dim_value if d.dim_value else d.dim_param
for d in out.type.tensor_type.shape.dim]
print(f" {out.name}: {shape}")
第三步:推理一致性验证
对比原始模型和 ONNX 模型的输出:
import torch
import onnxruntime as ort
import numpy as np
def validate_model(
pytorch_model,
onnx_path,
input_shape=(1, 3, 224, 224),
num_tests=10,
tolerance=1e-5
):
"""验证 PyTorch 模型和 ONNX 模型输出一致性"""
pytorch_model.eval()
ort_session = ort.InferenceSession(onnx_path)
input_name = ort_session.get_inputs()[0].name
max_diffs = []
for i in range(num_tests):
# 生成随机输入
input_np = np.random.randn(*input_shape).astype(np.float32)
input_torch = torch.from_numpy(input_np)
# PyTorch 推理
with torch.no_grad():
pytorch_output = pytorch_model(input_torch).numpy()
# ONNX Runtime 推理
onnx_output = ort_session.run(None, {input_name: input_np})[0]
# 计算差异
diff = np.abs(pytorch_output - onnx_output).max()
max_diffs.append(diff)
if diff > tolerance:
print(f"测试 {i}: 差异过大 {diff:.6f}")
avg_diff = np.mean(max_diffs)
max_diff = np.max(max_diffs)
print(f"\n验证结果:")
print(f" 测试次数: {num_tests}")
print(f" 平均最大差异: {avg_diff:.6e}")
print(f" 最大差异: {max_diff:.6e}")
print(f" 容差阈值: {tolerance:.6e}")
if max_diff < tolerance:
print(" 状态: ✓ 通过")
return True
else:
print(" 状态: ✗ 失败")
return False
# 使用示例
# validate_model(pytorch_model, "model.onnx")
高级验证技术
批量验证
验证不同 batch size 下的输出一致性:
def validate_dynamic_batch(
pytorch_model,
onnx_path,
input_shape=(3, 224, 224), # 不含 batch 维度
batch_sizes=[1, 2, 4, 8, 16],
tolerance=1e-5
):
"""验证动态 batch size 的正确性"""
pytorch_model.eval()
ort_session = ort.InferenceSession(onnx_path)
input_name = ort_session.get_inputs()[0].name
all_passed = True
for batch_size in batch_sizes:
full_shape = (batch_size,) + input_shape
input_np = np.random.randn(*full_shape).astype(np.float32)
input_torch = torch.from_numpy(input_np)
with torch.no_grad():
pytorch_output = pytorch_model(input_torch).numpy()
onnx_output = ort_session.run(None, {input_name: input_np})[0]
diff = np.abs(pytorch_output - onnx_output).max()
status = "✓" if diff < tolerance else "✗"
print(f"Batch {batch_size}: 差异 {diff:.6e} {status}")
if diff >= tolerance:
all_passed = False
return all_passed
逐层输出对比
当整体输出不一致时,需要逐层对比定位问题:
import torch
import onnx
import onnxruntime as ort
import numpy as np
def compare_layer_outputs(pytorch_model, onnx_path, input_data):
"""逐层对比 PyTorch 和 ONNX 的中间输出"""
pytorch_model.eval()
# 获取 PyTorch 各层输出
layer_outputs = {}
hooks = []
def make_hook(name):
def hook(module, input, output):
layer_outputs[name] = output.detach().numpy()
return hook
# 注册 hook
for name, module in pytorch_model.named_modules():
if len(name) > 0: # 跳过根模块
hooks.append(module.register_forward_hook(make_hook(name)))
# PyTorch 前向传播
with torch.no_grad():
pytorch_model(torch.from_numpy(input_data))
# 移除 hooks
for hook in hooks:
hook.remove()
# 打印各层输出统计
print("PyTorch 各层输出:")
for name, output in layer_outputs.items():
print(f" {name}: shape={output.shape}, mean={output.mean():.4f}, std={output.std():.4f}")
return layer_outputs
# 使用示例
# input_data = np.random.randn(1, 3, 224, 224).astype(np.float32)
# compare_layer_outputs(model, "model.onnx", input_data)
可视化模型结构
使用 Netron 可视化 ONNX 模型:
# 方法一:使用 Python 启动 Netron
import netron
netron.start("model.onnx") # 会在浏览器中打开
# 方法二:使用命令行
# pip install netron
# netron model.onnx
也可以直接访问 https://netron.app/ 上传模型文件。
常见问题调试
问题一:输出差异较大
症状:PyTorch 和 ONNX 输出差异超过 1e-5
排查步骤:
- 检查模型是否在 eval 模式:
model.eval() # 必须调用!
- 检查 BatchNorm 行为:
# 确保 BatchNorm 使用推理模式
for module in model.modules():
if isinstance(module, torch.nn.BatchNorm2d):
assert not module.training, "BatchNorm 应在 eval 模式"
- 检查 Dropout:
# Dropout 在 eval 模式下应被跳过
for module in model.modules():
if isinstance(module, torch.nn.Dropout):
assert not module.training
- 检查数据类型:
# 确保输入是 float32
input_data = input_data.astype(np.float32)
问题二:形状不匹配
症状:
InvalidArgument: Got invalid dimensions for input
排查步骤:
- 查看 ONNX 模型的输入形状:
session = ort.InferenceSession("model.onnx")
for inp in session.get_inputs():
print(f"输入: {inp.name}, 形状: {inp.shape}")
- 检查动态维度配置:
# 导出时是否配置了 dynamic_axes
torch.onnx.export(
model, dummy_input, "model.onnx",
dynamic_axes={
"input": {0: "batch_size"},
"output": {0: "batch_size"}
}
)
问题三:NaN 或 Inf 值
症状:输出包含 NaN 或 Inf
排查步骤:
import numpy as np
output = session.run(None, {"input": input_data})[0]
print(f"包含 NaN: {np.isnan(output).any()}")
print(f"包含 Inf: {np.isinf(output).any()}")
# 如果有问题,检查输入
print(f"输入 NaN: {np.isnan(input_data).any()}")
print(f"输入 Inf: {np.isinf(input_data).any()}")
# 检查模型权重
for name, param in pytorch_model.named_parameters():
if torch.isnan(param).any() or torch.isinf(param).any():
print(f"权重 {name} 包含 NaN/Inf")
问题四:特定输入失败
症状:某些输入正常,某些输入报错或输出异常
排查步骤:
- 边界值测试:
# 测试极端输入
test_inputs = [
np.zeros((1, 3, 224, 224), dtype=np.float32),
np.ones((1, 3, 224, 224), dtype=np.float32),
np.full((1, 3, 224, 224), -1.0, dtype=np.float32),
np.full((1, 3, 224, 224), 1e10, dtype=np.float32),
np.full((1, 3, 224, 224), 1e-10, dtype=np.float32),
]
for i, test_input in enumerate(test_inputs):
try:
output = session.run(None, {"input": test_input})[0]
print(f"测试 {i}: 正常, 输出范围 [{output.min():.4f}, {output.max():.4f}]")
except Exception as e:
print(f"测试 {i}: 失败, 错误: {e}")
- 数值范围检查:
# 检查输入数值范围是否合理
print(f"输入范围: [{input_data.min():.4f}, {input_data.max():.4f}]")
print(f"输入均值: {input_data.mean():.4f}")
print(f"输入标准差: {input_data.std():.4f}")
使用 ONNX Runtime 内置调试
ONNX Runtime 提供了一些调试选项:
import onnxruntime as ort
options = ort.SessionOptions()
# 启用详细日志
options.log_severity_level = 0 # 0=Verbose, 1=Info, 2=Warning, 3=Error
# 启用性能分析
options.enable_profiling = True
options.profile_file_prefix = "profile"
session = ort.InferenceSession("model.onnx", sess_options=options)
# 执行推理
input_data = np.random.randn(1, 3, 224, 224).astype(np.float32)
output = session.run(None, {"input": input_data})
# 获取性能分析结果
profile_file = session.end_profiling()
print(f"性能分析文件: {profile_file}")
模型修改调试
当需要修改模型来解决特定问题时,可以使用 onnx 库直接操作:
import onnx
from onnx import numpy_helper
model = onnx.load("model.onnx")
# 查看所有节点
print("模型节点:")
for i, node in enumerate(model.graph.node):
print(f" {i}: {node.op_type} - {node.name}")
print(f" 输入: {list(node.input)}")
print(f" 输出: {list(node.output)}")
# 查看初始值(权重)
print("\n模型权重:")
for init in model.graph.initializer:
arr = numpy_helper.to_array(init)
print(f" {init.name}: shape={arr.shape}, dtype={arr.dtype}")
# 修改节点属性
for node in model.graph.node:
if node.op_type == "Conv":
# 修改卷积的填充属性
for attr in node.attribute:
if attr.name == "pads":
attr.ints[:] = [1, 1, 1, 1] # 修改填充值
# 保存修改后的模型
onnx.save(model, "model_modified.onnx")
验证工具函数汇总
import torch
import onnx
import onnxruntime as ort
import numpy as np
class OnnxValidator:
"""ONNX 模型验证工具类"""
def __init__(self, pytorch_model, onnx_path):
self.pytorch_model = pytorch_model.eval()
self.onnx_path = onnx_path
self.session = ort.InferenceSession(onnx_path)
self.input_name = self.session.get_inputs()[0].name
def check_structure(self):
"""检查模型结构"""
try:
model = onnx.load(self.onnx_path)
onnx.checker.check_model(model)
return True, "结构检查通过"
except Exception as e:
return False, f"结构检查失败: {e}"
def check_shapes(self):
"""检查输入输出形状"""
model = onnx.load(self.onnx_path)
# 检查输入
tf_input_shape = None
for inp in self.session.get_inputs():
onnx_shape = inp.shape
print(f"ONNX 输入 '{inp.name}': {onnx_shape}")
return True
def compare_outputs(self, input_data, tolerance=1e-5):
"""比较输出"""
input_torch = torch.from_numpy(input_data)
with torch.no_grad():
pytorch_output = self.pytorch_model(input_torch).numpy()
onnx_output = self.session.run(None, {self.input_name: input_data})[0]
diff = np.abs(pytorch_output - onnx_output)
max_diff = diff.max()
mean_diff = diff.mean()
passed = max_diff < tolerance
return passed, {
"max_diff": max_diff,
"mean_diff": mean_diff,
"pytorch_shape": pytorch_output.shape,
"onnx_shape": onnx_output.shape
}
def full_validation(self, input_shape=(1, 3, 224, 224), num_tests=10):
"""完整验证"""
print("=" * 50)
print("开始完整验证")
print("=" * 50)
# 1. 结构检查
passed, msg = self.check_structure()
print(f"\n1. 结构检查: {msg}")
# 2. 形状检查
print("\n2. 形状检查:")
self.check_shapes()
# 3. 输出对比
print(f"\n3. 输出对比 ({num_tests} 次测试):")
all_passed = True
for i in range(num_tests):
input_data = np.random.randn(*input_shape).astype(np.float32)
passed, info = self.compare_outputs(input_data)
status = "✓" if passed else "✗"
print(f" 测试 {i+1}: max_diff={info['max_diff']:.6e} {status}")
if not passed:
all_passed = False
print("\n" + "=" * 50)
if all_passed:
print("验证结果: ✓ 全部通过")
else:
print("验证结果: ✗ 存在问题")
print("=" * 50)
return all_passed
# 使用示例
# validator = OnnxValidator(pytorch_model, "model.onnx")
# validator.full_validation()
总结
模型验证是部署流程中不可跳过的环节。一个完善的验证流程应该包括:
- 结构检查:确保 ONNX 文件完整有效
- 形状推断:验证输入输出形状信息
- 数值对比:多个测试用例下的输出一致性
- 边界测试:极端输入的稳定性
- 性能验证:满足延迟和吞吐要求
当验证失败时,使用逐层对比、可视化工具等方法定位问题根源。