4. 模型构建与操作(Operations)
好,咱们接着往下聊。上一章我们把模型的基础结构搭起来了,但一个神经网络模型光有结构可不行,它得能「干活」。怎么干活?就是靠操作(Operations)。
说白了,操作就是告诉硬件:「嘿,你把这堆数据做个卷积,再把结果做个ReLU激活,最后给我吐出来。」NNAPI里,这些操作被抽象成一个个的ANeuralNetworksModel_addOperation调用。
我个人习惯把模型构建比作搭积木。张量是积木块,操作就是胶水。没有胶水,积木堆得再高也是一碰就倒。
4.1 支持的操作类型列表
NNAPI支持的操作类型,其实一直在扩充。从Android 8.1(API 27)的几十个,到Android 14(API 34)已经超过100个。我列一下最常用的几类,你心里有个谱就行。
| 类别 | 典型操作 | 说明 |
|---|---|---|
| 激活函数 | ANEURALNETWORKS_RELU, ANEURALNETWORKS_SIGMOID, ANEURALNETWORKS_TANH | 给线性变换加点非线性,不然堆再多层也是线性 |
| 卷积 | ANEURALNETWORKS_CONV_2D, ANEURALNETWORKS_DEPTHWISE_CONV_2D | 图像处理的核心,我项目中90%的模型都用到了 |
| 池化 | ANEURALNETWORKS_AVERAGE_POOL_2D, ANEURALNETWORKS_MAX_POOL_2D | 降采样,减少计算量 |
| 全连接 | ANEURALNETWORKS_FULLY_CONNECTED | 分类器最后一层常用 |
| 归一化 | ANEURALNETWORKS_SOFTMAX, ANEURALNETWORKS_BATCH_NORMALIZATION | 让数据分布更稳定 |
| 张量操作 | ANEURALNETWORKS_RESHAPE, ANEURALNETWORKS_CONCATENATION, ANEURALNETWORKS_SLICE | 调整数据形状,拼接拆分 |
| 算术运算 | ANEURALNETWORKS_ADD, ANEURALNETWORKS_MUL, ANEURALNETWORKS_SUB | 逐元素加减乘除 |
| 逻辑运算 | ANEURALNETWORKS_EQUAL, ANEURALNETWORKS_GREATER, ANEURALNETWORKS_LOGICAL_AND | 比较、与或非 |
ANeuralNetworksCompilation_getSupportedOperations查一下。我曾经在某个低端手机上栽过跟头——模型里用了ANEURALNETWORKS_BATCH_NORMALIZATION,结果设备不支持,编译直接失败。
4.2 添加操作:ANeuralNetworksModel_addOperation
这个函数是模型构建的核心。它的签名长这样:
int ANeuralNetworksModel_addOperation(
ANeuralNetworksModel* model,
ANeuralNetworksOperationType type,
uint32_t inputCount,
const uint32_t* inputs,
uint32_t outputCount,
const uint32_t* outputs
);
参数看着多,其实逻辑很简单:
- model:你正在构建的模型句柄
- type:操作类型,比如
ANEURALNETWORKS_CONV_2D - inputCount / inputs:输入张量的数量及其索引数组
- outputCount / outputs:输出张量的数量及其索引数组
嗯,这里要注意:输入输出的顺序是固定的。比如卷积操作,输入张量必须排在第0位,权重排在第1位,偏置排在第2位。顺序搞反了,模型跑出来的结果就是一团浆糊。
来看一个完整的例子:添加一个ReLU激活操作。
// 假设我们已经有了一个输入张量,索引为0
uint32_t inputIndex = 0;
uint32_t outputIndex = 1;
// 创建输出张量(形状与输入相同)
ANeuralNetworksModel_addOperand(model, &operandType);
// 设置输出张量的形状(略)
// 添加ReLU操作
uint32_t reluInputs[] = {inputIndex};
uint32_t reluOutputs[] = {outputIndex};
int result = ANeuralNetworksModel_addOperation(
model,
ANEURALNETWORKS_RELU, // 操作类型
1, // 输入数量
reluInputs, // 输入索引数组
1, // 输出数量
reluOutputs // 输出索引数组
);
if (result != ANEURALNETWORKS_NO_ERROR) {
// 处理错误
__android_log_print(ANDROID_LOG_ERROR, "NNAPI",
"Failed to add ReLU operation: %d", result);
}
你看,代码本身并不复杂。复杂的是你要清楚每个操作需要几个输入、几个输出,以及它们的顺序。
4.3 操作数与张量
操作数(Operand)和张量(Tensor)这两个概念,我刚开始学的时候也容易搞混。简单说:
- 张量是数据容器,它存着具体的数值
- 操作数是张量的「引用」或「索引」,它告诉操作去哪里找数据
打个比方:张量就像图书馆里的书,操作数就是书的索书号。你告诉管理员「我要借索书号A-001的书」,管理员就知道去哪个书架拿。
在NNAPI里,每个张量在创建时都会得到一个索引(从0开始递增)。这个索引就是操作数。当你调用ANeuralNetworksModel_addOperation时,传入的就是这些索引。
ANeuralNetworksModel_setOperandValue来填充。
来看一个更完整的例子:构建一个简单的「输入→卷积→ReLU→输出」模型。
// 伪代码,展示操作数与张量的关系
// 步骤1:创建张量
uint32_t inputTensor, weightTensor, biasTensor, convOutputTensor, reluOutputTensor;
// 创建输入张量(索引0)
ANeuralNetworksModel_addOperand(model, &inputOperandType);
inputTensor = 0;
// 创建权重张量(索引1)
ANeuralNetworksModel_addOperand(model, &weightOperandType);
weightTensor = 1;
// 创建偏置张量(索引2)
ANeuralNetworksModel_addOperand(model, &biasOperandType);
biasTensor = 2;
// 创建卷积输出张量(索引3)
ANeuralNetworksModel_addOperand(model, &convOutputOperandType);
convOutputTensor = 3;
// 创建ReLU输出张量(索引4)
ANeuralNetworksModel_addOperand(model, &reluOutputOperandType);
reluOutputTensor = 4;
// 步骤2:设置张量数据(权重、偏置等)
ANeuralNetworksModel_setOperandValue(model, weightTensor, weightData, weightSize);
ANeuralNetworksModel_setOperandValue(model, biasTensor, biasData, biasSize);
// 步骤3:添加卷积操作
// 输入顺序:input, weight, bias
// 输出顺序:convOutput
uint32_t convInputs[] = {inputTensor, weightTensor, biasTensor};
uint32_t convOutputs[] = {convOutputTensor};
ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_CONV_2D,
3, convInputs, 1, convOutputs);
// 步骤4:添加ReLU操作
// 输入顺序:convOutput
// 输出顺序:reluOutput
uint32_t reluInputs[] = {convOutputTensor};
uint32_t reluOutputs[] = {reluOutputTensor};
ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_RELU,
1, reluInputs, 1, reluOutputs);
你想想看,这个流程其实很直观。先准备好所有「积木块」(张量),然后用「胶水」(操作)把它们粘起来。每个操作都知道自己该从哪个积木块读数据,该往哪个积木块写结果。
4.4 知识体系总览
为了让你更直观地理解这一章的内容,我画了一张图。它展示了操作、操作数、张量三者之间的关系。
从这张图你可以看到,整个流程是单向的:张量负责存数据,操作数负责「指路」,操作负责「干活」。各司其职,清晰明了。
4.5 一些实战经验
最后,分享几个我在项目中踩过的坑:
- 操作数索引不要重复使用:每个张量只能有一个索引。我曾经图省事,把同一个张量既当输入又当输出,结果模型编译直接崩溃。
- 注意操作的生命周期:操作添加的顺序就是执行顺序。NNAPI不会帮你重排序,你添加的顺序决定了计算图的结构。
- 善用
ANEURALNETWORKS_ADD做残差连接:在构建ResNet这类模型时,残差连接其实就是把两个张量加起来。用ANEURALNETWORKS_ADD操作就能实现,不需要自己写复杂的逻辑。
ANeuralNetworksModel_getSupportedOperations检查一下。这个函数会返回一个布尔数组,告诉你每个操作是否被当前设备支持。我每次写完模型都会跑一遍这个检查,能省下不少调试时间。
好了,这一章的内容就到这里。操作是NNAPI模型构建的灵魂,理解了操作、操作数、张量三者的关系,你就能搭建出任意复杂的神经网络了。
公众号:蓝海资料掘金营,微信deep3321