5. 张量(Tensor)与数据类型:张量的维度与形状、数据类型(FLOAT32, INT32, QUANT8_ASYMM等)、创建与设置张量(ANeuralNetworksModel_addOperand)
好,我们继续往下走。上一章我们把模型的基础骨架搭起来了,但模型要真正跑起来,还得往里面塞数据。在NNAPI的世界里,所有数据都是以张量(Tensor)的形式存在的。
说白了,张量就是个多维数组。你想想看,一张图片是三维的(高、宽、颜色通道),一段音频是一维的(采样点序列),一个视频是四维的(帧、高、宽、通道)。NNAPI要处理这些数据,就得先告诉它:嘿,我要塞进来的数据长什么样,是什么类型的。
我个人习惯把张量理解成一个“数据容器”,它有固定的形状和数据类型。你定义错了,模型推理就会出幺蛾子。我曾经在一个项目里,因为把输入张量的维度顺序搞反了,结果模型跑出来的结果完全不对,排查了半天才发现是形状定义错了。
5.1 张量的维度与形状
维度(Dimension)和形状(Shape)是张量的核心属性。维度指的是张量有多少个轴(Axis),形状则是一个数组,描述了每个轴上有多少个元素。
举个例子:
- 标量(0维张量):形状是
[],就是一个单独的数字。 - 向量(1维张量):形状是
[n],比如[10]表示有10个元素的一维数组。 - 矩阵(2维张量):形状是
[m, n],比如[224, 224]表示224行224列的二维数组。 - 3维张量:形状是
[d, h, w],比如[3, 224, 224]表示3个通道,每个通道是224x224的矩阵。很多图像模型就用这种格式。 - 4维张量:形状是
[n, c, h, w]或[n, h, w, c],这是批量处理时的常见格式。n是批次大小,c是通道数,h和w是高度和宽度。
这里有个坑,我必须要提醒你:不同框架对维度的排列顺序可能不一样。比如TensorFlow默认是 [batch, height, width, channels](NHWC),而PyTorch默认是 [batch, channels, height, width](NCHW)。NNAPI本身支持多种布局,但你在定义张量时一定要搞清楚模型训练时用的是哪种顺序,否则数据进去就是乱的。
核心要点:张量的形状必须与模型期望的输入输出完全一致。多一个维度、少一个维度,或者顺序不对,都会导致推理失败或结果错误。
5.2 数据类型(FLOAT32, INT32, QUANT8_ASYMM等)
形状说完了,接下来是数据类型。NNAPI支持多种数据类型,每种类型都有它的适用场景。我整理了一张表,方便你对照查看:
| 类型枚举值 | 描述 | 字节数 | 典型应用场景 |
|---|---|---|---|
ANEURALNETWORKS_FLOAT32 |
32位浮点数 | 4 | 高精度模型,训练和推理精度要求高的场景 |
ANEURALNETWORKS_INT32 |
32位有符号整数 | 4 | 索引、形状参数、标量常量 |
ANEURALNETWORKS_UINT32 |
32位无符号整数 | 4 | 某些特定硬件加速器需要的类型 |
ANEURALNETWORKS_TENSOR_FLOAT32 |
32位浮点张量 | 4 * 元素数 | 大多数浮点模型的标准输入输出 |
ANEURALNETWORKS_TENSOR_INT32 |
32位有符号整数张量 | 4 * 元素数 | 量化模型的偏置(bias)通常用这个 |
ANEURALNETWORKS_TENSOR_QUANT8_ASYMM |
8位非对称量化张量 | 1 * 元素数 | 移动端量化模型,最常用的量化类型 |
ANEURALNETWORKS_TENSOR_QUANT8_SYMM |
8位对称量化张量 | 1 * 元素数 | 某些硬件加速器对对称量化支持更好 |
ANEURALNETWORKS_TENSOR_QUANT16_SYMM |
16位对称量化张量 | 2 * 元素数 | 需要更高精度的量化场景 |
ANEURALNETWORKS_TENSOR_FLOAT16 |
16位浮点张量 | 2 * 元素数 | 支持FP16的硬件加速器,兼顾精度和速度 |
ANEURALNETWORKS_BOOL |
布尔类型 | 1 | 控制流、条件判断 |
嗯,这里要注意一下QUANT8_ASYMM。这是移动端量化模型里最常见的类型。为什么叫“非对称”?因为它允许零点(zero point)不在0的位置。比如一个8位量化张量,它的实际值计算公式是:
real_value = (quantized_value - zero_point) * scale
其中scale是缩放因子,zero_point是零点偏移。非对称量化可以更好地覆盖数据的实际分布范围,尤其是当数据不是关于0对称的时候(比如ReLU的输出全是非负数)。
我的经验:在移动端做模型部署时,尽量用QUANT8_ASYMM。它比FLOAT32省3/4的内存,推理速度也快很多。但要注意,量化模型对数值精度敏感,如果模型原本的数值范围很宽,量化后精度可能会下降。我建议你先用少量测试数据跑一遍,对比一下量化前后的输出差异。
5.3 创建与设置张量(ANeuralNetworksModel_addOperand)
好,理论知识差不多了。接下来我们看看怎么在代码里创建和设置张量。核心API就是ANeuralNetworksModel_addOperand。
这个函数的作用是向模型中添加一个操作数(Operand),也就是一个张量。你需要告诉它这个张量的类型和形状。
函数签名长这样:
int ANeuralNetworksModel_addOperand(ANeuralNetworksModel* model,
const ANeuralNetworksOperandType* type);
关键在第二个参数——ANeuralNetworksOperandType。这是一个结构体,你需要先填充它,再传给上面的函数。
我们来看一个完整的例子:创建一个形状为 [1, 224, 224, 3] 的FLOAT32张量,作为模型的输入。
// 1. 定义张量类型
ANeuralNetworksOperandType inputType;
// 设置数据类型为32位浮点张量
inputType.type = ANEURALNETWORKS_TENSOR_FLOAT32;
// 设置维度数量为4
inputType.dimensionCount = 4;
// 设置每个维度的大小:batch=1, height=224, width=224, channels=3
uint32_t inputDimensions[4] = {1, 224, 224, 3};
inputType.dimensions = inputDimensions;
// 量化参数:非量化类型填0和0.0f即可
inputType.scale = 0.0f;
inputType.zeroPoint = 0;
// 2. 添加到模型中
int result = ANeuralNetworksModel_addOperand(model, &inputType);
if (result != ANEURALNETWORKS_NO_ERROR) {
// 处理错误
// 我曾经遇到过因为dimensions指针传了null而导致崩溃的情况,记得检查!
}
再来看一个量化张量的例子:创建一个形状为 [1, 224, 224, 3] 的QUANT8_ASYMM张量。
ANeuralNetworksOperandType quantInputType;
quantInputType.type = ANEURALNETWORKS_TENSOR_QUANT8_ASYMM;
quantInputType.dimensionCount = 4;
uint32_t quantDims[4] = {1, 224, 224, 3};
quantInputType.dimensions = quantDims;
// 量化参数:scale和zeroPoint必须与模型训练时一致
quantInputType.scale = 0.0078125f; // 1/128
quantInputType.zeroPoint = 128; // 零点偏移
int result = ANeuralNetworksModel_addOperand(model, &quantInputType);
重要提醒:量化张量的scale和zeroPoint必须与模型训练时使用的量化参数完全一致。如果填错了,推理结果就是垃圾数据。我曾经接手过一个项目,前任工程师把scale写成了0.01,结果模型输出全是乱码,排查了整整两天才发现是这里的问题。
另外,对于标量常量(比如卷积的padding大小、步长等),你可以用ANEURALNETWORKS_INT32类型来定义。它的维度数量是0(标量),不需要传dimensions指针。
ANeuralNetworksOperandType scalarType;
scalarType.type = ANEURALNETWORKS_INT32;
scalarType.dimensionCount = 0; // 标量
scalarType.dimensions = nullptr;
scalarType.scale = 0.0f;
scalarType.zeroPoint = 0;
ANeuralNetworksModel_addOperand(model, &scalarType);
最后,我用一张图来总结一下张量创建的核心流程:
嗯,到这里张量的创建和设置就讲完了。记住,张量是NNAPI模型的数据基石,形状和类型必须精确匹配。下一节我们会把这些张量连接起来,组成真正的计算图。