Android 神经网络API

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 运行时评估的模型。

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

编译

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

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

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

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

*   [`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): 倾向于最大化连续帧的吞吐量,例如,在处理来自摄像头的连续帧时。
  1. 调用 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));

  1. 调用 ANeuralNetworksExecution_startCompute() 函数,计划要开始的执行。 如果没有错误,此函数将返回 ANEURALNETWORKS_NO_ERROR 的结果代码。
// Starts the work. The work proceeds asynchronously.
ANeuralNetworksEvent* run1_end = NULL;
ANeuralNetworksExecution_startCompute(run1, &run1_end);
  1. 调用 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。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,081评论 1 32
  • 一、温故而知新 1. 内存不够怎么办 内存简单分配策略的问题地址空间不隔离内存使用效率低程序运行的地址不确定 关于...
    SeanCST阅读 7,772评论 0 27
  • ORA-00001: 违反唯一约束条件 (.) 错误说明:当在唯一索引所对应的列上键入重复值时,会触发此异常。 O...
    我想起个好名字阅读 5,130评论 0 9
  • 所有知识点已整理成app app下载地址 J2EE 部分: 1.Switch能否用string做参数? 在 Jav...
    侯蛋蛋_阅读 2,407评论 1 4
  • 女儿高考后开始搬家,从来不知道家里有如此多的东西,理解了嫂子告诉我的话,破家值万贯的道理。 这是最后一车要拉的东西...
    悠然_3c09阅读 440评论 1 4