Neural Networks API

Android Neural Networks API (NNAPI) 是一个 Android C API,专门为在移动设备上对机器学习运行计算密集型运算而设计。 NNAPI 旨在为构建和训练神经网络的更高级机器学习框架(例如 TensorFlow Lite、Caffe2 或其他)提供一个基础的功能层。 API 适用于运行 Android 8.1(API 级别 27)或更高版本的所有设备。

NNAPI 支持通过以下方式进行推理:将 Android 设备中的数据应用到先前训练的开发者定义模型。 推理的示例包括分类图像、预测用户行为以及选择对搜索查询的适当响应。

设备上推理具有许多优势:

  • 延迟时间:您不需要通过网络连接发送请求并等待响应。 这对处理从摄像头传入的连续帧的视频应用至关重要。
  • 可用性:应用甚至可以在没有网络覆盖的条件下运行。
  • 速度:与单纯的通用 CPU 相比,特定于神经网络处理的新硬件可以提供显著加快的计算速度。

  • 隐私:数据不会离开设备。

  • 费用:所有计算都在设备上执行,不需要服务器场。

还存在一些开发者应考虑的利弊:

  • 系统利用率:评估神经网络涉及许多计算,这会增加电池消耗。 如果您的应用需要注意耗电量,您应当考虑监视电池运行状况,尤其是针对长时间运行的计算进行监视。

  • 应用大小:注意您的模型的大小。 模型可能会占用很多兆字节的空间。 如果在您的 APK 中绑定较大的模型会过度地影响您的用户,则您需要考虑在应用安装后下载模型、使用较小的模型或在云中运行您的计算。 NNAPI 未提供在云中运行模型的功能。

相关示例

  1. Android Neural Networks API 示例

了解 Neural Networks API 运行时

NNAPI 将通过机器学习库、框架和工具调用,这些工具可以让开发者脱离设备训练他们的模型并将其部署在 Android 设备上。 应用一般不会直接使用 NNAPI,但会直接使用更高级的机器学习框架。 这些框架反过来可以使用 NNAPI 在受支持的设备上执行硬件加速的推理运算。

根据应用的要求和设备上的硬件能力,Android 的神经网络运行时可以在可用的设备上处理器(包括专用的神经网络硬件、图形处理单元 (GPU) 和数字信号处理器 (DSP))之间有效地分配计算工作负载。

对于缺少专用的供应商驱动程序的设备,NNAPI 运行时将依赖优化的代码在 CPU 上执行请求。

图 1 显示了 NNAPI 的高级系统架构。

图 1. Android Neural Networks API 的系统架构

Neural Networks API 编程模型

要使用 NNAPI 执行计算,您首先需要构建一个可以定义要执行的计算的有向图。 此计算图与您的输入数据(例如,从机器学习框架传递过来的权重和偏差)相结合,构成 NNAPI 运行时评估的模型。

NNAPI 使用四种主要抽象:

  • 模型:数学运算和通过训练过程学习的常量值的计算图。 这些运算特定于神经网络。 它们包括二维 (2D) 卷积、逻辑 (sigmoid) 激活和整流线性 (ReLU) 激活等。 创建模型是一个同步操作,但是一旦成功创建,就可以在线程和编译之间重用模型。 在 NNAPI 中,一个模型表示为一个 ANeuralNetworksModel 实例。

  • 编译:表示用于将 NNAPI 模型编译到更低级别代码中的配置。 创建编译是一个同步操作,但是一旦成功创建,就可以在线程和执行之间重用编译。 在 NNAPI 中,每个编译表示为一个 ANeuralNetworksCompilation 实例。

  • 内存:表示共享内存、内存映射文件和类似的内存缓冲区。 使用内存缓冲区可以让 NNAPI 运行时将数据更高效地传输到驱动程序。 一个应用一般会创建一个共享内存缓冲区,其中包含定义模型所需的每一个张量。 您还可以使用内存缓冲区存储执行实例的输入和输出。 在 NNAPI 中,每个内存缓冲区表示为一个 ANeuralNetworksMemory 实例。

  • 执行:用于将 NNAPI 模型应用到一组输入并采集结果的接口。 执行是一种异步操作。 多个线程可以在相同的执行上等待。 当执行完成时,所有的线程都将释放。 在 NNAPI 中,每一个执行表示为一个 ANeuralNetworksExecution 实例。

图 2 显示了基本的编程流。

图 2. Android Neural Networks API 的编程流

本部分的剩余内容将说明设置您的 NNAPI 模型的步骤,以便执行计算、编译模型和执行编译模型。

提供训练数据访问权限

您的训练权重和偏差数据可能存储在一个文件中。 要让 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 位量化值的张量。

例如,图 3 表示一个具有两种运算的模型:先加法后乘法。 模型获取输入张量并生成一个输出张量。

图 3. NNAPI 模型的操作数示例

上面的模型有七个操作数。 这些操作数按照它们添加到模型中的顺序索引显式标识。 添加的第一个操作数的索引为 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() 函数来定义一个空模型。

    在下面的示例中,我们创建了可在图 3 中找到的双运算模型。

    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);
    

在您创建完模型后,您可以将其编译任意次数,并将每个编译执行任意次数。

编译

编译步骤确定您的模型将在哪些处理器上执行,并要求对应的驱动程序准备其执行。 这可能包括生成机器代码,此代码特定于您的模型将在其上面运行的处理器。

要编译模型,请按以下步骤操作:

  1. 调用 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);
    

    您可以指定的有效首选项包括:

  3. 调用 ANeuralNetworksCompilation_finish(),最终确定编译定义。 如果没有错误,此函数将返回 ANEURALNETWORKS_NO_ERROR 的结果代码。

    ANeuralNetworksCompilation_finish(compilation);
    

执行

执行步骤会将模型应用到一组输入,并将计算输出存储到一个或多个用户缓冲区或者您的应用分配的内存空间中。

要执行编译的模型,请按以下步骤操作:

  1. 调用 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));
    
  4. 调用 ANeuralNetworksExecution_startCompute() 函数,计划要开始的执行。 如果没有错误,此函数将返回 ANEURALNETWORKS_NO_ERROR 的结果代码。

    // Starts the work. The work proceeds asynchronously.
    ANeuralNetworksEvent* run1_end = NULL;
    ANeuralNetworksExecution_startCompute(run1, &run1_end);
    
  5. 调用 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。