4、输入张量准备:从图像、文本、音频等原始数据转换为模型所需张量的通用流程与工具

ONNX Runtime 作为跨平台推理引擎,其输入接口只认张量(Tensor),不关心原始数据是 JPEG 图片、UTF-8 文本还是 PCM 音频流。因此,将原始数据转换为模型所需的张量格式,是推理链路中绕不开的“第一公里”。本章聚焦这一转换过程的通用方法论:从数据读取、解码、预处理、张量化到维度调整与类型对齐,并给出图像、文本、音频三大模态的实操案例与工具选型建议。

原始数据 → 模型输入张量:通用转换链路 原始数据 图像 / 文本 / 音频 解码与读取 PIL / cv2 / tokenizer / soundfile 预处理 Resize / Normalize / Pad / FFT 张量化 numpy → torch / onnx 关键校验:shape / dtype / layout / 归一化参数 必须与 ONNX 模型输入元信息完全一致 图像模态 HWC → CHW / BGR→RGB Resize + Normalize(mean/std) dtype: uint8 → float32 shape: [1,3,H,W] 文本模态 tokenizer → input_ids attention_mask / token_type_ids padding + truncation shape: [1, seq_len] 音频模态 重采样 → 归一化 → 分帧 MFCC / FBank / 波形 shape: [1, feat_dim, T] dtype: float32 核心原则:先解码 → 再预处理 → 后张量化 → 最后校验元信息

4.1 核心原理:张量是模型与数据之间的“契约”

ONNX 模型通过 ModelProto 中的 graph.input 声明输入张量的名称、数据类型(tensor(float)、tensor(int64) 等)和维度信息(可能包含动态轴,如 batch、seq_len)。推理时,ONNX Runtime 的 Ort::Session::Run() 接口要求调用方传入与元信息匹配的 Ort::Value 对象。任何维度不匹配、类型不一致或数据布局错误,都会导致 FAIL 状态或产生错误的推理结果。

从数据到张量的转换,本质上是完成四件事:解码(将压缩或编码格式还原为数值数组)、预处理(几何变换、数值归一化、序列截断等)、张量化(将数组封装为特定 dtype 的 Tensor)以及元信息对齐(shape、layout、name 与模型声明一致)。这四步缺一不可,且顺序不能颠倒。

关键认知:ONNX Runtime 本身不提供图像解码、文本分词或音频重采样功能。它只负责张量运算。因此,输入张量准备必须在调用 Session 之前完成,通常使用 Python 的 NumPy、Pillow、OpenCV、HuggingFace Tokenizers、Librosa 等库,或 C++ 的 OpenCV、SentencePiece 等工具。

4.2 通用操作流程:五步转换法

无论数据来自哪种模态,都可以遵循以下五步流程,确保转换过程可控、可复现、可调试。

  1. 读取与解码:将原始字节流转换为内存中的数值数组。图像用 cv2.imread() 或 PIL.Image.open();文本用 tokenizer.encode();音频用 soundfile.read() 或 librosa.load()。
  2. 模态特定预处理:图像做 resize、crop、颜色空间转换;文本做 padding、truncation、special token 添加;音频做重采样、归一化、分帧加窗。
  3. 数值类型转换:统一转为模型要求的 dtype。ONNX 模型输入常见为 float32、int64、uint8。图像通常需要从 uint8 转为 float32 并除以 255.0 或应用 mean/std 归一化。
  4. 维度调整与轴顺序变换:图像需从 HWC(Height, Width, Channel)转为 CHW(Channel, Height, Width);文本需增加 batch 维度;音频需从 [samples] 转为 [batch, channels, samples] 或 [batch, mel_bins, time]。
  5. 封装为 Ort::Value:使用 Ort::Value::CreateTensor<float>() 或 Python 的 OrtValue.ort_value_from_numpy() 将 NumPy 数组封装为 ONNX Runtime 可接受的输入对象。
步骤图像文本音频
解码JPEG/PNG → HWC uint8字符串 → token idsWAV/MP3 → float32 波形
预处理Resize / CenterCrop / NormalizePadding / Truncation / Attention Mask重采样 / 归一化 / 分帧 / FBank
类型转换uint8 → float32int64(或 int32)float32
维度调整HWC → NCHW[seq] → [1, seq][samples] → [1, 1, samples] 或 [1, feat, T]
封装OrtValue / Ort::Value::CreateTensor

4.3 图像输入张量准备:从像素到 NCHW

图像模型(如 ResNet、YOLO、EfficientNet)通常要求输入为 [batch, 3, H, W] 的 float32 张量,数值范围在 [0,1] 或经过 ImageNet 的 mean/std 归一化。实际转换时,最容易出错的是颜色通道顺序和布局转换。

import cv2
import numpy as np
from onnxruntime import InferenceSession

# 1. 读取图像(BGR 格式,HWC 布局)
img = cv2.imread("cat.jpg")  # shape: [H, W, 3], dtype: uint8, 通道顺序 BGR

# 2. 缩放至模型输入尺寸
img = cv2.resize(img, (224, 224))  # 假设模型输入为 224x224

# 3. BGR → RGB(大多数 ONNX 视觉模型基于 RGB 训练)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

# 4. HWC → CHW,并增加 batch 维度
img = img.transpose(2, 0, 1)  # [3, 224, 224]
img = np.expand_dims(img, axis=0)  # [1, 3, 224, 224]

# 5. uint8 → float32,并归一化到 [0, 1]
img = img.astype(np.float32) / 255.0

# 6. 使用 mean/std 归一化(ImageNet 标准)
mean = np.array([0.485, 0.456, 0.406], dtype=np.float32).reshape(1, 3, 1, 1)
std = np.array([0.229, 0.224, 0.225], dtype=np.float32).reshape(1, 3, 1, 1)
img = (img - mean) / std

# 7. 封装为 ONNX Runtime 输入
session = InferenceSession("resnet50.onnx")
input_name = session.get_inputs()[0].name
ort_input = {input_name: img}
result = session.run(None, ort_input)
常见陷阱:OpenCV 默认读取为 BGR,而 PyTorch 预训练模型通常使用 RGB。如果忘记转换,模型精度会大幅下降(例如 Top-1 准确率从 75% 跌至 30% 以下)。另外,某些 ONNX 模型要求输入为 uint8(如量化模型),此时不要除以 255,而是直接传入 0-255 的整数张量。

4.4 文本输入张量准备:Tokenization 与动态轴处理

文本模型(BERT、GPT、T5 等)的输入通常是 input_ids、attention_mask 和可选的 token_type_ids。这些张量的 shape 为 [batch, seq_len],其中 seq_len 是动态轴,取决于输入文本的长度。ONNX Runtime 支持动态轴,但调用方必须确保 batch 维度和 seq_len 维度在运行时正确设置。

from transformers import AutoTokenizer
import numpy as np
from onnxruntime import InferenceSession

tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
session = InferenceSession("bert.onnx")

# 1. 原始文本 → token ids + attention mask
text = "ONNX Runtime makes inference fast!"
encoded = tokenizer(
    text,
    max_length=128,
    padding="max_length",
    truncation=True,
    return_tensors="np"  # 直接返回 NumPy 数组
)

# 2. 提取输入张量
input_ids = encoded["input_ids"].astype(np.int64)       # [1, 128]
attention_mask = encoded["attention_mask"].astype(np.int64)  # [1, 128]

# 3. 构造输入字典(注意名称必须与 ONNX 模型输入名一致)
ort_inputs = {
    "input_ids": input_ids,
    "attention_mask": attention_mask
}

# 4. 推理
outputs = session.run(None, ort_inputs)

对于 ONNX 模型,seq_len 通常被声明为动态轴(symbolic_dim),因此你可以传入任意长度(只要不超过模型最大限制)。但需要注意:padding 到固定长度(如 128)会浪费计算资源;不 padding 则可能导致 batch 内长度不一致而报错。推荐的做法是:在预处理阶段将同一 batch 内的文本 padding 到该 batch 的最大长度,而不是全局固定长度。

性能优化:如果模型支持 attention_mask,建议将 padding 长度设为 batch 内最大长度,而不是固定 128 或 512。这样可减少约 30%-50% 的无效计算。ONNX Runtime 的 Transformer 优化(如 Attention Fusion)会利用 mask 跳过 padding 部分。

4.5 音频输入张量准备:波形与频谱特征

音频模型有两种常见输入形式:原始波形(如 WaveNet、MELGAN)和频谱特征(如 FBank、MFCC,用于 ASR 或声纹识别)。波形输入相对简单,但频谱特征需要额外的 DSP 处理。

import librosa
import numpy as np
from onnxruntime import InferenceSession

# 1. 加载音频并重采样到 16kHz
audio, sr = librosa.load("speech.wav", sr=16000, mono=True)
# audio shape: [N_samples], float32, 范围 [-1, 1]

# 2. 提取 FBank 特征(80 维滤波器组)
fbank = librosa.feature.melspectrogram(
    y=audio, sr=16000, n_mels=80,
    n_fft=400, hop_length=160, power=2.0
)
fbank = librosa.power_to_db(fbank, ref=np.max)  # 转为 dB 单位
# fbank shape: [80, T](T 为时间帧数)

# 3. 归一化(常用 mean-std 或 min-max)
fbank = (fbank - fbank.mean()) / (fbank.std() + 1e-8)

# 4. 增加 batch 和 channel 维度
fbank = np.expand_dims(fbank, axis=0)  # [1, 80, T]
fbank = np.expand_dims(fbank, axis=0)  # [1, 1, 80, T](若模型要求 4D)

# 5. 封装为 ONNX 输入
session = InferenceSession("asr_model.onnx")
ort_inputs = {session.get_inputs()[0].name: fbank.astype(np.float32)}
result = session.run(None, ort_inputs)

音频处理中,采样率一致性是最大的坑。如果模型在 16kHz 下训练,而你传入 44.1kHz 的音频,特征分布会完全错位。务必使用 librosa.load(..., sr=16000) 或 soundfile.read(..., samplerate=16000) 强制重采样。

4.6 案例拆解:多模态模型的输入组装

假设我们需要部署一个视觉-文本多模态模型(如 CLIP),它同时接受图像和文本输入。该 ONNX 模型有两个输入:image([1,3,224,224] float32)和 text([1,77] int64)。以下是完整的组装流程:

步骤图像分支文本分支
原始数据photo.jpg (1920x1080)"A dog playing with a ball"
解码cv2.imread → BGR uint8CLIP tokenizer → input_ids
预处理Resize(224) + CenterCrop + BGR→RGBPadding/Truncation 到 77 tokens
数值转换uint8 → float32,除以 255int64(无需归一化)
维度调整HWC → CHW → [1,3,224,224][77] → [1,77]
封装构造 dict:{"image": img_tensor, "text": text_tensor}

实际推理时,我们遇到一个问题:图像经过 cv2.resize 后,长宽比失真导致模型输出相似度分数偏低。分析后发现,CLIP 模型在训练时使用 Resize(224) + CenterCrop,而不是直接拉伸。修正方案:先按短边缩放到 224,再中心裁剪 224x224 区域。

def clip_image_preprocess(img_path):
    img = cv2.imread(img_path)
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    h, w = img.shape[:2]
    # 按短边缩放
    if h < w:
        new_h, new_w = 224, int(w * 224 / h)
    else:
        new_w, new_h = 224, int(h * 224 / w)
    img = cv2.resize(img, (new_w, new_h))
    # 中心裁剪
    start_x = (new_w - 224) // 2
    start_y = (new_h - 224) // 2
    img = img[start_y:start_y+224, start_x:start_x+224]
    img = img.astype(np.float32) / 255.0
    img = img.transpose(2, 0, 1)[None, ...]  # [1,3,224,224]
    return img

修正后,图像与文本的余弦相似度从 0.62 提升到 0.87,验证了预处理细节对多模态模型的重要性。

4.7 常见误区与总结复盘

  • 误区一:忽略 dtype 匹配。ONNX Runtime 对 dtype 敏感,float32 与 float64 不兼容。使用 astype(np.float32) 显式转换。
  • 误区二:layout 错误。图像模型要求 NCHW,而 OpenCV 默认 HWC。使用 transpose(2,0,1) 调整。
  • 误区三:动态轴处理不当。对于文本和音频,seq_len 或 time 轴是动态的。不要硬编码固定值,除非模型明确要求。
  • 误区四:归一化参数不一致。训练时用 mean=[0.485,0.456,0.406],推理时却忘记减均值或除标准差。
  • 误区五:忽略 batch 维度。ONNX 模型输入通常要求 4D(图像)或 2D(文本),单样本也需要增加 batch 维。
最佳实践:在编写预处理代码前,先打印 session.get_inputs() 的 name、shape、type 信息,并对照模型文档确认预处理要求。建议将预处理封装为独立函数,并编写单元测试验证输出张量的 shape 和 dtype。

总结而言,输入张量准备的核心是“对齐”二字:与模型元信息对齐、与训练时的预处理对齐、与 ONNX Runtime 的类型系统对齐。掌握图像、文本、音频三种模态的通用转换流程,并理解动态轴与归一化细节,就能在绝大多数推理场景中避免“第一公里”的坑。