4、输入张量准备:从图像、文本、音频等原始数据转换为模型所需张量的通用流程与工具
ONNX Runtime 作为跨平台推理引擎,其输入接口只认张量(Tensor),不关心原始数据是 JPEG 图片、UTF-8 文本还是 PCM 音频流。因此,将原始数据转换为模型所需的张量格式,是推理链路中绕不开的“第一公里”。本章聚焦这一转换过程的通用方法论:从数据读取、解码、预处理、张量化到维度调整与类型对齐,并给出图像、文本、音频三大模态的实操案例与工具选型建议。
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 与模型声明一致)。这四步缺一不可,且顺序不能颠倒。
4.2 通用操作流程:五步转换法
无论数据来自哪种模态,都可以遵循以下五步流程,确保转换过程可控、可复现、可调试。
- 读取与解码:将原始字节流转换为内存中的数值数组。图像用
cv2.imread()或PIL.Image.open();文本用tokenizer.encode();音频用soundfile.read()或librosa.load()。 - 模态特定预处理:图像做 resize、crop、颜色空间转换;文本做 padding、truncation、special token 添加;音频做重采样、归一化、分帧加窗。
- 数值类型转换:统一转为模型要求的 dtype。ONNX 模型输入常见为
float32、int64、uint8。图像通常需要从uint8转为float32并除以 255.0 或应用 mean/std 归一化。 - 维度调整与轴顺序变换:图像需从 HWC(Height, Width, Channel)转为 CHW(Channel, Height, Width);文本需增加 batch 维度;音频需从 [samples] 转为 [batch, channels, samples] 或 [batch, mel_bins, time]。
- 封装为 Ort::Value:使用
Ort::Value::CreateTensor<float>()或 Python 的OrtValue.ort_value_from_numpy()将 NumPy 数组封装为 ONNX Runtime 可接受的输入对象。
| 步骤 | 图像 | 文本 | 音频 |
|---|---|---|---|
| 解码 | JPEG/PNG → HWC uint8 | 字符串 → token ids | WAV/MP3 → float32 波形 |
| 预处理 | Resize / CenterCrop / Normalize | Padding / Truncation / Attention Mask | 重采样 / 归一化 / 分帧 / FBank |
| 类型转换 | uint8 → float32 | int64(或 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)
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 uint8 | CLIP tokenizer → input_ids |
| 预处理 | Resize(224) + CenterCrop + BGR→RGB | Padding/Truncation 到 77 tokens |
| 数值转换 | uint8 → float32,除以 255 | int64(无需归一化) |
| 维度调整 | 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 的类型系统对齐。掌握图像、文本、音频三种模态的通用转换流程,并理解动态轴与归一化细节,就能在绝大多数推理场景中避免“第一公里”的坑。