Neural Networks API
NNAPI是一个 Android C API,专门为在移动设备上对机器学习运行计算密集型运算而设计。 NNAPI 旨在为构建和训练神经网络的更高级机器学习框架(例如 (TensorFlow Lite、Caffe2 或其他)提供一个基础的功能层。 API 适用于运行 Android 8.1(API 级别 27)或更高版本的所有设备
NNAPI 支持通过以下方式进行推理:将 Android 设备中的数据应用到先前训练的开发者定义模型。 推理的示例包括分类图像、预测用户行为以及选择对搜索查询的适当响应。
设备上推理具有许多优势:
- 延迟时间:不需要通过网络连接发送请求并等待响应。 这对处理从摄像头传入的连续帧的视频应用至关重要。
- 可用性:应用甚至可以在没有网络覆盖的条件下运行。
- 速度:与单纯的通用 CPU 相比,特定于神经网络处理的新硬件可以提供显著加快的计算速度。
- 隐私:数据不会离开设备。
- 费用:所有计算都在设备上执行,不需要服务器场。 还存在一些开发者应考虑的利弊:
- 系统利用率:评估神经网络涉及许多计算,这会增加电池消耗。 如果应用需要注意耗电量,应当考虑监视电池运行状况,尤其是针对长时间运行的计算进行监视。
- 应用大小:注意模型的大小。 模型可能会占用很多兆字节的空间。 如果在APK中绑定较大的模型会过度地影响用户,则您需要考虑在应用安装后下载模型、使用较小的模型或在云中运行您的计算。 NNAPI 未提供在云中运行模型的功能。
Neural Networks API 运行时
NNAPI 将通过机器学习库、框架和工具调用,这些工具可以让开发者脱离设备训练他们的模型并将其部署在 Android 设备上。 应用一般不会直接使用 NNAPI,但会直接使用更高级的机器学习框架。 这些框架反过来可以使用 NNAPI 在受支持的设备上执行硬件加速的推理运算。
根据应用的要求和设备上的硬件能力,Android 的神经网络运行时可以在可用的设备上处理器(包括专用的神经网络硬件、图形处理单元 (GPU) 和数字信号处理器 (DSP))之间有效地分配计算工作负载。
对于缺少专用的供应商驱动程序的设备,NNAPI 运行时将依赖优化的代码在 CPU 上执行请求。
NNAPI编程模型
要使用 NNAPI 执行计算,首先需要构建一个可以定义要执行的计算的有向图。 此计算图与您的输入数据(例如,从机器学习框架传递过来的权重和偏差)相结合,构成 NNAPI 运行时评估的模型。
NNAPI 使用四种主要抽象:
-
模型:数学运算和通过训练过程学习的常量值的计算图。 这些运算特定于神经网络, 并且包括二维 (2D) 卷积、逻辑 (sigmoid)) 激活和整流线性 (ReLU) 激活等。 创建模型是一个同步操作,但是一旦成功创建,就可以在线程和编译之间重用模型。 在 NNAPI 中,一个模型表示为一个
ANeuralNetworksModel
实例。 -
编译:表示用于将 NNAPI 模型编译到更低级别代码中的配置。 创建编译是一个同步操作,但是一旦成功创建,就可以在线程和执行之间重用编译。 在 NNAPI 中,每个编译表示为一个
ANeuralNetworksCompilation
实例。 -
内存:表示共享内存、内存映射文件和类似的内存缓冲区。 使用内存缓冲区可以让 NNAPI 运行时将数据更高效地传输到驱动程序。 一个应用一般会创建一个共享内存缓冲区,其中包含定义模型所需的每一个张量。 还可以使用内存缓冲区存储执行实例的输入和输出。 在 NNAPI 中,每个内存缓冲区表示为一个
ANeuralNetworksMemory
实例。 -
执行:用于将 NNAPI 模型应用到一组输入并采集结果的接口。 执行是一种异步操作。 多个线程可以在相同的执行上等待。 当执行完成时,所有的线程都将释放。 在 NNAPI 中,每一个执行表示为一个
ANeuralNetworksExecution
实例。
提供训练数据访问权限
训练权重和偏差数据可能存储在一个文件中。 要让 NNAPI 运行时有效地获取此数据,请调用 ANeuralNetworksMemory_createFromFd()
函数并传入已打开数据文件的文件描述符,创建一个 ANeuralNetworksMemory
实例。
也可以在共享内存区域于文件中开始的位置指定内存保护标志和偏移。
// Create a memory buffer from the file that contains the trained data.
ANeuralNetworksMemory* mem1 = NULL;
int fd = open("training_data", O_RDONLY);
ANeuralNetworksMemory_createFromFd(file_size, PROT_READ, fd, 0, &mem1);
尽管在此示例中我们仅为所有权重使用了一个 ANeuralNetworksMemory
实例,但是可以为多个文件使用一个以上的 ANeuralNetworksMemory
实例
模型
模型是 NNAPI 中的基本计算单位。 每个模型都由一个或多个操作数和运算定义。
操作数
操作数是定义计算图时使用的数据对象。 其中包括模型的输入和输出、包含从一个运算流向另一个运算的数据的中间节点,以及传递到这些运算的常量。
可以向 NNAPI 模型中添加两种类型的操作数:标量和张量。
标量表示一个数字。 NNAPI 支持 32 位浮点、32 位整数和无符号 32 位整数格式的标量值。
NNAPI 的大多数运算都涉及张量。 张量是 N 维数组。 NNAPI 支持具有 32 位整数、32 位浮点和 8 位量化值的张量。
上面的模型有七个操作数。 这些操作数按照它们添加到模型中的顺序索引显式标识。 添加的第一个操作数的索引为 0,第二个操作数的索引为 1,依此类推。
添加操作数的顺序不重要。 例如,模型输出操作数可以是添加的第一个操作数。 重要的部分是在引用操作数时使用正确的索引值。
操作数具有类型。 这些类型在添加到模型中时指定。 一个操作数无法同时用作模型的输入和输出
运算
运算指定要执行的计算。 每个运算都包含下面这些元素:
- 运算类型(例如,加法、乘法、卷积),
- 运算用于输入的操作数索引列表,以及
- 运算用于输出的操作数索引列表。
操作数在这些列表中的顺序非常重要;请针对每个运算查阅 NNAPI API 参考,了解预期输入和输出。
在添加运算之前,必须先将运算消耗或生成的操作数添加到模型中。
添加运算的顺序不重要。 NNAPI 依赖操作数和运算的计算图建立的依赖关系来确定运算的执行顺序。
下表汇总了 NNAPI 支持的运算:
已知问题: 将
ANEURALNETWORKS_TENSOR_QUANT8_ASYMM
张量传递到ANEURALNETWORKS_PAD
运算(在 Android 9(API 级别 28)及更高版本中提供)时,NNAPI 的输出可能与较高级别机器学习框架(如 TensorFlow Lite)的输出不匹配。 应只传递ANEURALNETWORKS_TENSOR_FLOAT32
直到问题得到解决。
构建模型
1 .调用 ANeuralNetworksModel_create()
函数来定义一个空模型。
ANeuralNetworksModel* model = NULL;
ANeuralNetworksModel_create(&model);
2 . 调用 ANeuralNetworks_addOperand()
,将操作数添加到您的模型中。 它们的数据类型使用 ANeuralNetworksOperandType
数据结构定义。
// In our example, all our tensors are matrices of dimension [3][4].
ANeuralNetworksOperandType tensor3x4Type;
tensor3x4Type.type = ANEURALNETWORKS_TENSOR_FLOAT32;
tensor3x4Type.scale = 0.f; // These fields are useful for quantized tensors.
tensor3x4Type.zeroPoint = 0; // These fields are useful for quantized tensors.
tensor3x4Type.dimensionCount = 2;
uint32_t dims[2] = {3, 4};
tensor3x4Type.dimensions = dims;
// We also specify operands that are activation function specifiers.
ANeuralNetworksOperandType activationType;
activationType.type = ANEURALNETWORKS_INT32;
activationType.scale = 0.f;
activationType.zeroPoint = 0;
activationType.dimensionCount = 0;
activationType.dimensions = NULL;
// Now we add the seven operands, in the same order defined in the diagram.
ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 0
ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 1
ANeuralNetworksModel_addOperand(model, &activationType); // operand 2
ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 3
ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 4
ANeuralNetworksModel_addOperand(model, &activationType); // operand 5
ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 6
3 . 对于具有常量值的操作数,例如应用从训练过程获取的权重和偏差,请使用 ANeuralNetworksModel_setOperandValue()
和 ANeuralNetworksModel_setOperandValueFromMemory()
函数。
// In our example, operands 1 and 3 are constant tensors whose value was
// established during the training process.
const int sizeOfTensor = 3 * 4 * 4; // The formula for size calculation is dim0 * dim1 * elementSize.
ANeuralNetworksModel_setOperandValueFromMemory(model, 1, mem1, 0, sizeOfTensor);
ANeuralNetworksModel_setOperandValueFromMemory(model, 3, mem1, sizeOfTensor, sizeOfTensor);
// We set the values of the activation operands, in our example operands 2 and 5.
int32_t noneValue = ANEURALNETWORKS_FUSED_NONE;
ANeuralNetworksModel_setOperandValue(model, 2, &noneValue, sizeof(noneValue));
ANeuralNetworksModel_setOperandValue(model, 5, &noneValue, sizeof(noneValue));
4 .对于有向图中您想要计算的每个运算,请调用 ANeuralNetworksModel_addOperation()
函数,将运算添加到您的模型中。
应用必须以此调用的参数形式提供以下各项:
- 运算类型,
- 输入值计数,
- 输入操作数索引的数组,
- 输出值计数,以及
- 输出操作数索引的数组。
请注意,一个操作数无法同时用作同一个运算的输入和输出
// We have two operations in our example.
// The first consumes operands 1, 0, 2, and produces operand 4.
uint32_t addInputIndexes[3] = {1, 0, 2};
uint32_t addOutputIndexes[1] = {4};
ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_ADD, 3, addInputIndexes, 1, addOutputIndexes);
// The second consumes operands 3, 4, 5, and produces operand 6.
uint32_t multInputIndexes[3] = {3, 4, 5};
uint32_t multOutputIndexes[1] = {6};
ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_MUL, 3, multInputIndexes, 1, multOutputIndexes);
5 .调用 ANeuralNetworksModel_identifyInputsAndOutputs()
函数,确定模型应将哪些操作数视为其输入和输出。 此函数可以将模型配置为使用上面的第 4 步中指定的输入和输出操作数子集
// Our model has one input (0) and one output (6).
uint32_t modelInputIndexes[1] = {0};
uint32_t modelOutputIndexes[1] = {6};
ANeuralNetworksModel_identifyInputsAndOutputs(model, 1, modelInputIndexes, 1 modelOutputIndexes);
6 . 可选)通过调用 ANeuralNetworksModel_relaxComputationFloat32toFloat16()
,指定是否允许 ANEURALNETWORKS_TENSOR_FLOAT32
使用低至 IEEE 754 16 位浮点格式的范围或精度计算。
7 .调用 ANeuralNetworksModel_finish()
来最终确定模型的定义。 如果没有错误,此函数将返回 ANEURALNETWORKS_NO_ERROR
的结果代码。
ANeuralNetworksModel_finish(model);
编译
编译步骤确定模型将在哪些处理器上执行,并要求对应的驱动程序准备其执行。 这可能包括生成机器代码,此代码特定于模型将在其上面运行的处理器。
要编译模型,请按以下步骤操作:
- 调用
ANeuralNetworksCompilation_create()
函数来创建一个新的编译实例。
// Compile the model.
ANeuralNetworksCompilation* compilation;
ANeuralNetworksCompilation_create(model, &compilation);
2.可以选择性地影响运行时如何在电池消耗与执行速度之间权衡。 为此,可以调用 ANeuralNetworksCompilation_setPreference()
。
// Ask to optimize for low power consumption.
ANeuralNetworksCompilation_setPreference(compilation, ANEURALNETWORKS_PREFER_LOW_POWER);
可以指定的有效首选项包括:
* [`ANEURALNETWORKS_PREFER_LOW_POWER`](https://developer.android.com/ndk/reference/group/neural-networks#group___neural_networks_1gga034380829226e2d980b2a7e63c992f18a370c42db64448662ad79116556bcec01): 倾向于以最大程度减小电池消耗的方式执行。 此首选项适合将要经常执行的编译。
* [`ANEURALNETWORKS_PREFER_FAST_SINGLE_ANSWER`](https://developer.android.com/ndk/reference/group/neural-networks#group___neural_networks_1gga034380829226e2d980b2a7e63c992f18af7fff807061a3e9358364a502691d887): 倾向于尽快返回单个回答,即使这会导致耗电量增加。
* [`ANEURALNETWORKS_PREFER_SUSTAINED_SPEED`](https://developer.android.com/ndk/reference/group/neural-networks#group___neural_networks_1gga034380829226e2d980b2a7e63c992f18af727c25f1e2d8dcc693c477aef4ea5f5): 倾向于最大化连续帧的吞吐量,例如,在处理来自摄像头的连续帧时。
- 调用
ANeuralNetworksCompilation_finish()
,最终确定编译定义。 如果没有错误,此函数将返回ANEURALNETWORKS_NO_ERROR
的结果代码。
ANeuralNetworksCompilation_finish(compilation);
执行
执行步骤会将模型应用到一组输入,并将计算输出存储到一个或多个用户缓冲区或者应用分配的内存空间中。
要执行编译的模型,请按以下步骤操作:
- 调用
ANeuralNetworksExecution_create()
函数来创建一个新的执行实例。
// Run the compiled model against a set of inputs.
ANeuralNetworksExecution* run1 = NULL;
ANeuralNetworksExecution_create(compilation, &run1);
2 指定应用为计算读取输入值的位置。 通过分别调用 ANeuralNetworksExecution_setInput()
或 ANeuralNetworksExecution_setInputFromMemory()
,应用可以从用户缓冲区或分配的内存空间读取输入值。
// Set the single input to our sample model. Since it is small, we won’t use a memory buffer.
float32 myInput[3][4] = { ..the data.. };
ANeuralNetworksExecution_setInput(run1, 0, NULL, myInput, sizeof(myInput));
3.指定应用写入输出值的位置。 通过分别调用 ANeuralNetworksExecution_setOutput()
或 ANeuralNetworksExecution_setOutputFromMemory()
,应用可以将输出值分别写入用户缓冲区或分配的内存空间。
// Set the output.
float32 myOutput[3][4];
ANeuralNetworksExecution_setOutput(run1, 0, NULL, myOutput, sizeof(myOutput));
- 调用
ANeuralNetworksExecution_startCompute()
函数,计划要开始的执行。 如果没有错误,此函数将返回ANEURALNETWORKS_NO_ERROR
的结果代码。
// Starts the work. The work proceeds asynchronously.
ANeuralNetworksEvent* run1_end = NULL;
ANeuralNetworksExecution_startCompute(run1, &run1_end);
- 调用
ANeuralNetworksEvent_wait()
函数以等待执行完成。 如果执行成功,此函数将返回ANEURALNETWORKS_NO_ERROR
的结果代码。 等待可以在不同于开始执行的线程上完成。
// For our example, we have no other work to do and will just wait for the completion.
ANeuralNetworksEvent_wait(run1_end);
ANeuralNetworksEvent_free(run1_end);
ANeuralNetworksExecution_free(run1);
6.或者,也可以使用同一个编译实例来创建一个新的 ANeuralNetworksExecution
实例,将一组不同的输入应用到编译的模型。
// Apply the compiled model to a different set of inputs.
ANeuralNetworksExecution* run2;
ANeuralNetworksExecution_create(compilation, &run2);
ANeuralNetworksExecution_setInput(run2, ...);
ANeuralNetworksExecution_setOutput(run2, ...);
ANeuralNetworksEvent* run2_end = NULL;
ANeuralNetworksExecution_startCompute(run2, &run2_end);
ANeuralNetworksEvent_wait(run2_end);
ANeuralNetworksEvent_free(run2_end);
ANeuralNetworksExecution_free(run2);
清理
清理步骤可以处理计算所用内部资源的释放。
// Cleanup
ANeuralNetworksCompilation_free(compilation);
ANeuralNetworksModel_free(model);
ANeuralNetworksMemory_free(mem1);
操作数的更多主题
下面一部分介绍了有关使用操作数的高级主题。
量化张量
量化张量是一种表示 N 维浮点值数组的紧凑型方式。
NNAPI 支持 8 位非对称量化张量。 对于这些张量,每个单元格的值都通过一个 8 位整数表示。 与张量关联的是一个比例和一个零点值。 这些用于将 8 位整数转换成要表示的浮点值。
公式为:
(cellValue - zeroPoint) * scale
其中,zeroPoint 值是一个 32 位整数,scale 是一个 32 位浮点值。
与 32 位浮点值的张量相比,8 位量化张量具有两个优势:
- 应用将更小,因为训练的权重占 32 位张量大小的四分之一。
- 计算通常可以更快地执行。 这是因为仅需要从内存提取少量数据,并且 DSP 等处理器进行整数数学运算的效率更高。
尽管可以将浮点值模型转换成量化模型,但我们的经验表明,直接训练量化模型可以取得更好的结果。 事实上,神经网络会通过学习来补偿每个值增大的粒度。 对于量化张量,scale 和 zeroPoint 值在训练过程中确定。
在 NNAPI 中,需要将 ANeuralNetworksOperandType
数据结构的类型字段设置为 ANEURALNETWORKS_TENSOR_QUANT8_ASYMM
,定义量化张量类型。 还需要在该数据结构中指定张量的 scale 和 zeroPoint 值。
可选操作数
一些运算(例如 ANEURALNETWORKS_LSH_PROJECTION
)会采用可选操作数。 要在模型中指示忽略可选操作数,请调用 ANeuralNetworksModel_setOperandValue()
函数,为 buffer 传递 NULL
,为 length 传递 0。
如果是否使用操作数的决定因执行而异,应通过以下方式指示忽略操作数:使用 ANeuralNetworksExecution_setInput()
或 ANeuralNetworksExecution_setOutput()
函数,同时为 buffer 传递 NULL
,为 length 传递 0。