API de Neural Networks

La API de Neural Networks (NNAPI) de Android C está diseñada para ejecutar operaciones con mucha carga de cálculo para aprendizaje automático en dispositivos móviles. La NNAPI está diseñada para proporcionar una capa básica de funcionalidad para marcos de trabajo de aprendizaje automático de nivel superior (como TensorFlow Lite, Caffe2 u otros) que crean y preparan redes neuronales. La API está disponible en todos los dispositivos con Android 8.1 (nivel de API 27) o versiones posteriores.

NNAPI admite la formulación de inferencias mediante la aplicación de datos de dispositivos Android a modelos definidos por el programador y previamente preparados. Dentro de los ejemplos de formulación de inferencias se incluyen la clasificación de imágenes, la predicción del comportamiento del usuario y la selección de respuestas apropiadas para una consulta de búsqueda.

La formulación de inferencias en el dispositivo tiene muchos beneficios:

  • Latencia: No necesitas enviar una solicitud a través de una conexión de red y esperar una respuesta. Esto puede ser crítico en las aplicaciones de video que procesan fotogramas sucesivos provenientes de una cámara.
  • Disponibilidad: La aplicación se ejecuta incluso cuando se encuentra fuera de la cobertura de la red.
  • Velocidad: El nuevo hardware específico para procesamiento de redes neuronales proporciona un cálculo considerablemente más rápido que con la CPU de uso general.
  • Privacidad: Los datos no salen del dispositivo.
  • Costo: No se necesitan granjas de servidores cuando se realizan todos los cálculos en el dispositivo.

También hay compensaciones que un desarrollador debe tener en cuenta:

  • Utilización del sistema: La evaluación de las redes neuronales implica muchos cálculos, lo cual puede aumentar el consumo de batería. Debes considerar controlar el estado de la batería si esto es importante para tu app, en especial para cálculos de ejecución prolongada.
  • Tamaño de aplicación: Presta atención al tamaño de tus modelos. Los modelos pueden ocupar varios megabytes. Si la agrupación de modelos grandes en tu APK tiene un efecto indebido para tus usuarios, tal vez debas considerar descargarlos después de la instalación de la app, usar modelos más pequeños o ejecutar tus cálculos en la nube. La NNAPI no proporciona una funcionalidad para ejecutar modelos en la nube.

Ejemplo relacionado:

  1. Ejemplo de la Neural Networks API de Android

Explicación del tiempo de ejecución de la API de Neural Networks

A la NNAPI deben llamarla bibliotecas, marcos de trabajo y herramientas de aprendizaje automático que permiten a los programadores preparar sus modelos sin el dispositivo e implementarlos en dispositivos Android. Por lo general, las apps no usan la NNAPI directamente; como alternativa, emplean directamente marcos de trabajo de aprendizaje automático de nivel superior. Estos marcos de trabajo a la vez pueden usar la NNAPI para realizar operaciones de inferencia aceleradas por hardware en dispositivos compatibles.

Según los requisitos de la app y las capacidades del hardware de un dispositivo, el tiempo de ejecución de las redes neuronales de Android puede distribuir de manera eficaz la carga de trabajo de cálculo en procesadores disponibles del dispositivo; se incluyen el hardware de red neuronal dedicado, las unidades de procesamiento de gráficos (GPU) y los procesadores de señal digital (DSP).

En el caso de los dispositivos que no tienen un controlador de proveedores especializado, el tiempo de ejecución de la NNAPI emplea código optimizado para ejecutar solicitudes en la CPU.

En la figura 1 se muestra una arquitectura de sistema de nivel superior para la NNAPI.

Figura 1: Arquitectura del sistema para la API de Neural Networks de Android

Modelo de programación de la API de Neural Networks

Para realizar cálculos con la NNAPI, primero debes construir un gráfico dirigido que defina los cálculos que se realizarán. Este gráfico de cálculos, combinado con tus datos de entrada (por ejemplo, las ponderaciones y las compensaciones transmitidas desde un marco de trabajo de aprendizaje automático), forma el modelo para la evaluación del tiempo de ejecución de la NNAPI.

La NNAPI usa cuatro abstracciones principales:

  • Modelo: Gráfico de cómputos de operaciones matemáticas y de los valores constantes aprendidos a través de un proceso de capacitación. Estas operaciones son específicas de las redes neuronales. Entre otras, incluyen la convolución bidimensional (2D), la activación logística (sigmoid) y la activación lineal rectificada (ReLU). La creación de un modelo es una operación sincrónica, pero una vez que se realiza con éxito, el modelo puede reutilizarse en subprocesos y compilaciones. En la NNAPI, un modelo se representa como una instancia de ANeuralNetworksModel.
  • Compilación: Representa una configuración para la compilación de un modelo de la NNAPI en un código de nivel inferior. La creación de una compilación es una operación sincrónica, pero una vez que se realiza con éxito, la compilación puede reutilizarse en subprocesos y ejecuciones. En la NNAPI, cada compilación se representa como una instancia de ANeuralNetworksCompilation.
  • Memoria: Representa la memoria compartida, archivos asignados a la memoria y búferes de memoria similares. El uso de un búfer de memoria permite que el tiempo de ejecución de la NNAPI transfiera datos a los controladores de un modo más eficaz. Por lo general, una app crea un búfer de memoria compartido que contiene todos los tensores que se necesitan para definir un modelo. También puedes usar los búferes de memoria a fin de almacenar las entradas y las salidas para una instancia de ejecución. En la NNAPI, cada búfer de memoria se representa como una instancia de ANeuralNetworksMemory.
  • Ejecución: Interfaz para aplicar un modelo de la NNAPI en un conjunto de entradas y recopilar resultados. La ejecución es una operación asíncrona. Varios subprocesos pueden esperar en la misma ejecución. Cuando la ejecución se complete, se liberarán todos los subprocesos. En la NNAPI, cada ejecución se representa como una instancia de ANeuralNetworksExecution.

En la figura 2 se muestra el flujo de programación básico.

Figura 2: Flujo de programación para la API de Neural Networks de Android

En el resto de esta sección se describen los pasos para configurar tu modelo de la NNAPI a fin de realizar cómputos, compilar el modelo y ejecutar el modelo compilado.

Otorgamiento de acceso a los datos de preparación

Es probable que tus datos de ponderaciones preparadas y sesgos se almacenen en un archivo. Para que el tiempo de ejecución de la NNAPI tenga acceso eficaz a estos datos, crea una instancia de ANeuralNetworksMemory llamando a la función ANeuralNetworksMemory_createFromFd() y pasando el descriptor de archivos del archivo de datos abierto.

También puedes especificar indicadores de protección de memoria y un desplazamiento donde comienza la región de memoria compartida en el archivo.

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

Aunque en este ejemplo usamos únicamente una instancia de ANeuralNetworksMemory para todas las ponderaciones, es posible usar más de una instancia de ANeuralNetworksMemory para varios archivos.

Modelos

Un modelo es la unidad fundamental de cálculos de la NNAPI. Cada modelo se define mediante uno o más operandos y operaciones.

Operandos

Los operandos son objetos de datos usados en la definición del gráfico. Entre estos se incluyen las entradas y salidas del modelo, los nodos intermedios que contienen los datos que van de una operación a otra y las constantes que se pasan a estas operaciones.

Hay dos tipos de operandos que se pueden agregar a los modelos de la NNAPI: escalares y tensores.

Un escalar representa un solo número. La NNAPI admite valores escalares en un punto flotante de 32 bits, un entero de 32 bits y un formato entero de 32 bits sin firma.

En la mayoría de las operaciones con la NNAPI se incluyen tensores. Los tensores son matrices de dimensión n. La NNAPI admite tensores con un entero de 32 bits, un punto flotante de 32 bits y valores de 8 bits cuantificados.

Por ejemplo, en la figura 3 se representa un modelo con dos operaciones: una suma seguida de una multiplicación. El modelo toma un tensor de entrada y produce uno de salida.

Figura 3: Ejemplo de operandos para un modelo de la NNAPI

El modelo anterior cuenta con siete operandos. Estos operandos se identifican de forma implícita con el índice del orden en el que se agregan al modelo. Para el primer operando agregado el índice es 0, para el segundo 1 y así sucesivamente.

No importa el orden en que se agreguen los operandos. Por ejemplo, el operando de salida del modelo puede ser el primero en agregarse. Lo importante es usar el valor de índice correcto al hacer referencia a un operando.

Los operandos se clasifican por tipos. Estos se especifican cuando se agregan al modelo. No se puede usar un operando como entrada y salida de un modelo.

Para conocer temas adicionales sobre el uso de los operandos, consulta Más información sobre los operandos.

Operaciones

Una operación especifica los cómputos que se realizarán. Cada operación consiste en estos elementos:

  • un tipo de operación (por ejemplo, suma, multiplicación y convolución);
  • una lista de índices de los operandos que usa la operación para la entrada; y
  • una lista de índices de los operandos que usa la operación para la salida.

El orden en estas listas es importante; consulta la referencia de la API de la NNAPI, en la que hallarás cada operación para las entradas y las salidas esperadas.

Debes agregar al modelo los operandos que consume o produce una operación antes de agregar la operación.

No importa el orden en que se agreguen las operaciones. La NNAPI emplea las dependencias establecidas por el gráfico de cálculos de operandos y operaciones para determinar el orden en que se ejecutan las operaciones.

Las operaciones que admite la NNAPI se resumen en la siguiente tabla:

Categoría Operaciones
Operaciones matemáticas en elementos
Operaciones de matrices
Operaciones de imágenes
Operaciones de búsqueda
Operaciones de normalización
Operaciones de convolución
Operaciones de agrupación
Operaciones de activación
Otras operaciones

Problema conocido: Al pasar tensores ANEURALNETWORKS_TENSOR_QUANT8_ASYMM a la operación ANEURALNETWORKS_PAD, disponible en Android 9 (nivel 28 de API) y versiones posteriores, es posible que el resultado de la NNAPI no coincida con el resultado de los marcos de trabajo de aprendizaje automático de nivel superior, como TensorFlow Lite. En su lugar, solo se debe pasar ANEURALNETWORKS_TENSOR_FLOAT32 hasta que se resuelva el problema.

Creación de modelos

Para crear un modelo, sigue estos pasos:

  1. Llama a la función ANeuralNetworksModel_create() para definir un modelo vacío.

    En el siguiente ejemplo, creamos el modelo de dos operaciones que se muestra en la figura 3.

    ANeuralNetworksModel* model = NULL;
        ANeuralNetworksModel_create(&model);
        
  2. Agrega los operandos a tu modelo llamando a ANeuralNetworks_addOperand(). Sus tipos de datos se definen usando la estructura de datos de 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. Para los operandos que tienen valores constantes, como ponderaciones y compensaciones que tu app obtiene de un proceso de preparación, usa las funciones ANeuralNetworksModel_setOperandValue() y ANeuralNetworksModel_setOperandValueFromMemory().

    En el siguiente ejemplo, establecemos valores constantes desde el archivo de datos de preparación para el cual creamos el búfer de memoria anterior.

    // 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. Para cada operación del gráfico dirigido que desees calcular, agrega la operación a tu modelo llamando a la función ANeuralNetworksModel_addOperation().

    Como parámetros para este llamado, tu app debe proporcionar:

    • El tipo de operación
    • el conteo de valores de entrada;
    • la matriz de los índices para operandos de entrada;
    • el conteo de valores de salida; y
    • la matriz de los índices para operandos de salida.

    Ten en cuenta que un operando no se puede usar para la entrada y salida de la misma operación.

    // 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. Identifica qué operandos debe tratar el modelo como entradas y salidas llamando a la función ANeuralNetworksModel_identifyInputsAndOutputs(). Esta función te permite configurar el modelo para usar un subconjunto de los operandos de entrada y salida que especificaste anteriormente en el paso 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. También puedes especificar si ANEURALNETWORKS_TENSOR_FLOAT32 se puede calcular con un rango o precisión tan bajos como la del formato de punto flotante IEEE 754 de 16 bits llamando a ANeuralNetworksModel_relaxComputationFloat32toFloat16().

  7. Llama a ANeuralNetworksModel_finish() para finalizar la definición de tu modelo. Si no hay errores, esta función muestra un código de resultado ANEURALNETWORKS_NO_ERROR.

    ANeuralNetworksModel_finish(model);
        

Una vez que creas un modelo, puedes compilarlo y ejecutar cada compilación la cantidad de veces que quieras.

Compilación

El paso de compilación determina los procesadores en los que se ejecutará tu modelo y solicita a los controladores correspondientes que se prepararen para su ejecución. Esto puede incluir la generación de código máquina específico para los procesadores en que se ejecutará tu modelo.

Para compilar un modelo, sigue estos pasos:

  1. Llama a la función ANeuralNetworksCompilation_create() para crear una instancia nueva de compilación.

    // Compile the model.
        ANeuralNetworksCompilation* compilation;
        ANeuralNetworksCompilation_create(model, &compilation);
        
  2. Opcionalmente, puedes condicionar el modo en que el tiempo de ejecución realiza intercambios entre el uso de la batería y la velocidad de ejecución. Para ello, llama a ANeuralNetworksCompilation_setPreference().

    // Ask to optimize for low power consumption.
        ANeuralNetworksCompilation_setPreference(compilation, ANEURALNETWORKS_PREFER_LOW_POWER);
        

    Dentro de las preferencias válidas que puedes especificar, se incluyen las siguientes:

  3. Llama a ANeuralNetworksCompilation_finish() para finalizar la definición de la compilación. Si no hay errores, esta función muestra un código de resultado ANEURALNETWORKS_NO_ERROR.

    ANeuralNetworksCompilation_finish(compilation);
        

Ejecución

El paso de ejecución aplica el modelo a un conjunto de entradas y almacena las salidas de cálculo en uno o más búferes de usuario o espacios de memoria asignados por tu app.

Para ejecutar un modelo compilado, sigue estos pasos:

  1. Llama a la función ANeuralNetworksExecution_create() para crear una instancia nueva de ejecución.

    // Run the compiled model against a set of inputs.
        ANeuralNetworksExecution* run1 = NULL;
        ANeuralNetworksExecution_create(compilation, &run1);
        
  2. Especifica el lugar en que tu app lee los valores de entrada para el cálculo. Tu app puede leer valores de entrada desde un búfer de usuario o un espacio de memoria asignado llamando a ANeuralNetworksExecution_setInput() o ANeuralNetworksExecution_setInputFromMemory() respectivamente.

    // 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. Especifica el punto en el que tu app escribe los valores de salida. Tu app puede escribir valores de salida en un búfer de usuario o un espacio de memoria asignado por medio de una llamada a ANeuralNetworksExecution_setOutput() o ANeuralNetworksExecution_setOutputFromMemory() respectivamente.

    // Set the output.
        float32 myOutput[3][4];
        ANeuralNetworksExecution_setOutput(run1, 0, NULL, myOutput, sizeof(myOutput));
        
  4. Programa el comienzo de la ejecución llamando a la función ANeuralNetworksExecution_startCompute(). Si no hay errores, esta función muestra un código de resultado ANEURALNETWORKS_NO_ERROR.

    // Starts the work. The work proceeds asynchronously.
        ANeuralNetworksEvent* run1_end = NULL;
        ANeuralNetworksExecution_startCompute(run1, &run1_end);
        
  5. Llama a la función ANeuralNetworksEvent_wait() para esperar a que se complete la ejecución. Si no hay errores, esta función muestra un código de resultado ANEURALNETWORKS_NO_ERROR. La espera puede producirse en un subproceso diferente del que inició la ejecución.

    // 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. También puedes aplicar un conjunto diferente de entradas al modelo compilado usando la misma instancia de compilación para crear una instancia ANeuralNetworksExecution nueva.

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

Limpieza

El paso de limpieza maneja la liberación de recursos internos usados para tu cálculo.

// Cleanup
    ANeuralNetworksCompilation_free(compilation);
    ANeuralNetworksModel_free(model);
    ANeuralNetworksMemory_free(mem1);
    

Más información sobre los operandos

En la siguiente sección, se describen temas avanzados vinculados al uso de operandos.

Tensores cuantificados

Un tensor cuantificado es un modo compacto de representar una matriz de dimensión n de los valores de punto flotante.

La NNAPI admite tensores cuantificados asimétricos de 8 bits. Para estos tensores, el valor de cada celda está representado por un entero de 8 bits. Una escala y un valor de punto cero se asocian con el tensor. Estos se usan para convertir los enteros de 8 bits en los valores de punto flotante que se representan.

La fórmula es:

(cellValue - zeroPoint) * scale
    

El valor "zeroPoint" es un entero de 32 bits y la escala un valor de punto flotante de 32 bits.

En comparación con los tensores de valores de punto flotante de 32 bits, los tensores cuantificados de 8 bits tienen dos ventajas:

  • Tu aplicación será más pequeña, ya que las ponderaciones preparadas tomarán un cuarto del tamaño de los tensores de 32 bits.
  • A menudo, los cálculos se pueden ejecutar más rápido. Esto se debe a la menor cantidad de datos que deben obtenerse de la memoria y a la eficacia de los procesadores, como DSP, para realizar cálculos con enteros.

Aunque es posible convertir un modelo de punto flotante a uno cuantificado, nuestra experiencia demuestra que se logran mejores resultados con la preparación directa de un modelo cuantificado. Por lo tanto, la red neuronal aprende a compensar el aumento de granularidad de cada valor. Para cada tensor cuantificado, la escala y los valores zeroPoint se determinan durante el proceso de preparación.

En NNAPI, se definen tipos de tensores cuantificados fijando el campo de tipos de la estructura de datos de ANeuralNetworksOperandType en ANEURALNETWORKS_TENSOR_QUANT8_ASYMM.También se especifican la escala y el valor zeroPoint del tensor en esa estructura de datos.

Operandos opcionales

En algunas operaciones, como ANEURALNETWORKS_LSH_PROJECTION, se toman operandos opcionales. Para indicar en el modelo que omitió el operando opcional, llama a la función ANeuralNetworksModel_setOperandValue() y pasa NULL por el búfer y 0 por la longitud.

Si la decisión respecto de que el operando esté presente o no varía para cada ejecución, se indica que este se omite mediante la función ANeuralNetworksExecution_setInput() o ANeuralNetworksExecution_setOutput(), y se pasan NULL por el búfer y 0 por la longitud.