Android神经​​网络API(NNAPI)

  • 内容
  • 评论
  • 相关

Android神经​​网络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的高级系统体系结构。

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

神经网络API编程模型

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

NNAPI使用四个主要抽象:

  • 模型:数学运算的计算图和通过训练过程学习的常量值。这些操作是特定于神经网络的。它们包括二维(2D)卷积,logistic(sigmoid)激活, 整流线性(ReLU)激活等等。创建一个模型是一个同步操作,但是一旦成功创建,它可以在线程和编译中重用。在NNAPI中,模型被表示为一个ANeuralNetworksModel 实例。
  • 编译:表示将NNAPI模型编译为低级代码的配置。创建一个编译是一个同步操作,但是一旦成功创建,它可以在线程和执行之间重用。在NNAPI中,每个编译都被表示为一个ANeuralNetworksCompilation 实例。
  • 内存:表示共享内存,内存映射文件和类似的内存缓冲区。使用内存缓冲区可让NNAPI运行时更有效地将数据传输到驱动程序。应用程序通常会创建一个共享内存缓冲区,其中包含定义模型所需的每个张量。您还可以使用内存缓冲区来存储执行实例的输入和输出。在NNAPI中,每个内存缓冲区都表示为一个 ANeuralNetworksMemory 实例。
  • 执行:将NNAPI模型应用于一组输入并收集结果的接口。执行是一个异步操作。多个线程可以等待相同的执行。执行完成后,所有线程都将被释放。在NNAPI中,每个执行都表示为一个 ANeuralNetworksExecution 实例。

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

图2. Android Neural Networks API的编程流程

本节的其余部分将介绍设置NNAPI模型以执行计算,编译模型和执行编译模型的步骤。

提供访问培训数据

训练后的权重和偏差数据可能存储在一个文件中。要为NNAPI运行时提供对此数据的有效访问,请ANeuralNetworksMemory 通过调用该ANeuralNetworksMemory_createFromFd() 函数并传入打开的数据文件的文件描述符来创建一个 实例 。

您还可以指定内存保护标志和共享内存区域在文件中开始的偏移量。

// 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. NNAPI模型的操作数示例

上面的模型有七个操作数。这些操作数由它们添加到模型的顺序的索引隐式标识。第一个操作数的索引是0,第二个索引是1,依此类推。

您添加操作数的顺序无关紧要。例如,模型输出操作数可能是第一个添加的操作数。重要的部分是在引用操作数时使用正确的索引值。

操作数有类型。这些在添加到模型中时指定。操作数不能用作模型的输入和输出。

有关使用操作数的其他主题,请参阅 有关操作数的更多信息

操作

操作指定要执行的计算。每个操作都由这些元素组成:

  • 操作类型(例如,加法,乘法,卷积),
  • 操作用于输入的操作数的索引列表,以及
  • 操作用于输出的操作数的索引列表。

这些列表中的顺序很重要; 有关预期输入和输出的每项操作,请参阅 NNAPI API参考

在添加操作之前,您必须将操作消耗或生成的操作数添加到模型中。

您添加操作的顺序无关紧要。NNAPI依赖操作数和操作的计算图形建立的依赖关系来确定操作的执行顺序。

下表汇总了NNAPI支持的操作:

类别 操作
基于元素的数学运算
数组操作
图像操作
查找操作
规范化操作
卷积运算
集合操作
激活操作
其他操作

建立模型

要构建模型,请按照下列步骤操作:

  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. 对于具有固定值的操作数(例如应用程序从训练过程中获得的权重和偏见),请使用 ANeuralNetworks_setOperandValue() 和ANeuralNetworks_setOperandValuesFromMemory() 函数。

    在下面的例子中,我们从上面创建内存缓冲区的训练数据文件中设置了常量值。

    // 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. 对于要计算的有向图中的每个操作,请通过调用该ANeuralNetworks_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_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位整数,并且规模是一个32位浮点值。

与32位浮点值的张量相比,8位量化张量具有两个优点:

  • 您的应用程序将更小,因为经过训练的权重将占用32位张量大小的四分之一。
  • 计算通常可以更快地执行。这是由于需要从内存中获取的数据量较小,以及处理器(如DSP)在整数数学中的效率。

虽然可以将浮点模型转换为量化模型,但我们的经验表明,通过直接训练量化模型可以获得更好的结果。实际上,神经网络学习补偿每个值的增加的粒度。对于每个量化张量,在训练过程中确定尺度和零点值。

在NNAPI中,通过设置ANeuralNetworksOperandType 数据结构 的类型字段来定义量化张量类型 ANEURALNETWORKS_TENSOR_QUANT8_ASYMM。您还可以在该数据结构中指定张量的比例和zeroPoint值。

可选操作数

一些操作,如 ANEURALNETWORKS_LSH_PROJECTION采取可选的操作数。要在模型中指示可选操作数被省略,请调用ANeuralNetworksModel_setOperandValue() 函数,传递NULL缓冲区和0作为长度。

如果决定操作数是否存在会随着每次执行而变化,则表示使用ANeuralNetworksExecution_setInput()or ANeuralNetworksExecution_setOutput() 函数省略操作数 ,传递NULL缓冲区并将长度设为0。