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) 卷积、逻辑(S 函数)激活、线性整流函数 (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 张量传递到 Android 9(API 级别 28)及更高版本中提供的 ANEURALNETWORKS_PAD 运算时,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(),从而指定是否允许使用较低的范围或精度(像 IEEE 754 16 位浮点格式的范围或精度那么低)对 ANEURALNETWORKS_TENSOR_FLOAT32 进行计算。

  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。