Hola, les saluda Miguel y aquà les traigo este artÃculo.
Índice
Cómo acelerar el modelo de PyTorch
en Android usando la inferencia de GPU
y la biblioteca MACE
En los últimos años, ha habido una tendencia hacia el uso de inferencias de GPU en teléfonos móviles. En Tensorflow, la compatibilidad con GPU en dispositivos móviles está integrada en la biblioteca estándar, pero aún no está implementada en el caso de PyTorch, por lo que necesitamos usar bibliotecas de terceros.
En este artÃculo, veremos el paquete PyTorchâž”ONNXâž”libMACE
.
A continuación, veremos los pasos para instalar libMACE
.
Creemos un entorno virtual separado que contendrá la biblioteca y todo lo que necesita para funcionar.
python3 -m virtualenv ./VENV source ./VENV/bin/activate
Luego tenemos que instalar el sistema de compilación bazel
, según el funcionario guÃa de instalación.
sudo apt install g++ unzip zip wget https://github.com/bazelbuild/bazel/releases/download/0.13.0/bazel-0.13.0-installer-linux-x86_64.sh chmod +x ./bazel-0.13.0-installer-linux-x86_64.sh ./bazel-0.13.0-installer-linux-x86_64.sh --prefix=/home/user/VENV/opt/bazel export PATH="${PATH}:/home/user/VENV/opt/bazel/bin/"
La construcción de la biblioteca MACE
requiere Android NDK
. Necesitamos la versión r15c
, que descargaremos en el directorio ~ / VENV / opt / android-ndk-r15c /
.
wget -q https://dl.google.com/android/repository/android-ndk-r15c-linux-x86_64.zip unzip -q android-ndk-r15c-linux-x86_64.zip
A continuación, necesitamos instalar bibliotecas adicionales.
sudo apt install cmake android-tools-adb pip install numpy scipy jinja2 pyyaml sh==1.12.14 pycodestyle==2.4.0 filelock pip install onnx
En la última etapa, crearemos un script
para configurar las variables de entorno android_env.sh
(que se muestra a continuación).
export ANDROID_NDK_VERSION=r15c export ANDROID_NDK=/home/user/VENV/opt/android-ndk-r15c/ export ANDROID_NDK_HOME=${ANDROID_NDK} export PATH=${PATH}:${ANDROID_NDK_HOME} export PATH=${PATH}:/home/user/VENV/opt/bazel/bin/
MACE
usa su propio formato para la representación de redes neuronales, por lo que necesitamos transformar el modelo original. El proceso de conversión consta de varias etapas. Lo veremos usando el ejemplo de ResNet 50
de la biblioteca de torchvision
.
En la primera etapa, convertimos el modelo PyTorch
al formato ONNX
.
antorcha de importación de modelos de importación de torchvision modelo = modelos.resnet50 (preentrenado = verdadero) datos = antorcha.rand (1, 3, 256, 256) input_names = ["input"] output_names = ["salida"] torch.onnx.export (modelo, datos, "model.onnx", verbose = True, input_names = input_names, output_names = output_names, opset_version = 11, export_params = True, keep_initializers_as_inputs = True)
Después de la conversión, el contenido de la carpeta deberÃa verse asÃ.
~/VENV/opt$ ls android_env.sh android-ndk-r15c mace model.onnx resnet.ipynb
En la segunda etapa, necesitamos guardar el modelo en su propio formato libMACE
. Creemos un archivo de configuración de acuerdo con el guÃa.
library_name: resnet_model target_abis: [arm64-v8a] model_graph_format: file model_data_format: file models: resnet_model: platform: onnx model_file_path: /home/user/VENV/opt/model.onnx model_sha256_checksum: 1cafd297ee8ac70dd6e2e5644d4c29e8121f6af0b3f42d652888c18cf73314ee subgraphs: - input_tensors: - input input_shapes: - 1,3,256,256 output_tensors: - output output_shapes: - 1,1000 input_data_formats: - NCHW backend: pytorch runtime: cpu+gpu winograd: 1
El archivo debe especificar la ruta absoluta al archivo ONNX (model_file_path)
, la suma de comprobación SHA256 (model_sha256_checksum)
, la geometrÃa y los nombres de los tensores de entrada y salida (input_tensors, input_shapes, output_tensors y output_shapes)
, y el formato de datos, en nuestro caso NCHW
.
La suma de comprobación se puede calcular utilizando la utilidad sha256sum
.
sha256sum ../model.onnx
Después de crear el archivo de configuración, podemos ejecutar el script de conversión del modelo.
cd ./mace/ source ./android_env.sh python ./tools/converter.py convert --config ../resnet_model.yml
Si la conversión se completó sin errores
, se mostrará el siguiente texto.
******************************************** Model resnet_model converted ******************************************** -------------------------------------------- Library -------------------------------------------- | key | value | ============================================ | MACE Model Path| build/resnet_model/model| --------------------------------------------
El resultado de la conversión serán los archivos reset_model.data y reset_model.pb
, en ~ / VENV / opt / mace / build / resnet_model / model /
.
Para nuestra aplicación en Android Studio, necesitamos especificar el tipo C ++ Native Application
.
A continuación, necesitamos una compilación binaria de la biblioteca MACE
del repositorio.
wget https://github.com/XiaoMi/mace/releases/download/v0.13.0/libmace-v0.13.0.tar.gz
Creemos un directorio / app / lib make
con carpetas arm64-v8 y armeabi-v7a
donde copiamos versiones de libmace.so para cpu_gpu arch de libmace-v0.13.0 / lib /
.
En CMakeLists.txt
necesitamos agregar MACE
incluye dir.
include_directories(/home/user/VENV/opt/libmace-v0.13.0/include/)
A continuación, en el archivo CMakeLists.txt
, necesitamos crear una biblioteca lib_mace
y agregarla a la lista target_link_libraries
.
También necesitamos agregar -ljnigraphics
a esta lista para compatibilidad con JNI Bitmap
.
set(LIBMACE_DIR ${CMAKE_SOURCE_DIR}/../../../libmace/) add_library(lib_mace SHARED IMPORTED) set_target_properties(lib_mace PROPERTIES IMPORTED_LOCATION ${LIBMACE_DIR}/${CMAKE_ANDROID_ARCH_ABI}/libmace.so) target_link_libraries( native-lib lib_mace -ljnigraphics ${log-lib} )
En el archivo app / build.gradle
, necesitamos agregar las subsecciones abiFilters y externalNativeBuild
a defaultConfig
.
defaultConfig { ... ndk { abiFilters 'arm64-v8a', 'armeabi-v7a' } externalNativeBuild { cmake { cppFlags "" arguments "-DANDROID_ARM_NEON=TRUE","-DANDROID_STL=c++_shared" } } }
En la sección de Android, necesitamos agregar la entrada sourceSets
.
android { ... sourceSets { main { jniLibs.srcDirs = ['libmace/'] } } }
A AndroidManifest.xml
agregamos permiso de lectura al sistema de archivos.
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
A continuación, creemos una carpeta de activos (Crear nueva ➔ Carpeta ➔ Carpeta de activos)
y copiemos resnet_model.pb y resnet_model.data
en ella.
Primero, agregaremos la función de carga del modelo a MainActivity.java
.
public native long loadModel(String cache_dir, String model_data, String model_pb);
También agregaremos su implementación a native_lib.cpp
.
extern "C" JNIEXPORT jlong JNICALL Java_com_example_resnetm_MainActivity_loadModel(JNIEnv *env, jobject thiz, jstring cache_dir, jstring model_data, jstring model_pb) {…}
La biblioteca MACE
requiere una configuración de inicio especial, que se muestra a continuación.
#if USE_GPU DeviceType device_type = DeviceType::GPU; MaceStatus status; MaceEngineConfig config(device_type); std::shared_ptr<GPUContext> gpu_context; gpu_context = GPUContextBuilder() .SetStoragePath(storage_path) .Finalize(); config.SetGPUContext(gpu_context); config.SetGPUHints( static_cast<GPUPerfHint>(GPUPerfHint::PERF_NORMAL), static_cast<GPUPriorityHint>(GPUPriorityHint::PRIORITY_LOW)); #else DeviceType device_type = DeviceType::CPU; MaceStatus status; MaceEngineConfig config(device_type); #endif
Veamos el código con más detalle. Primero, necesitamos seleccionar un dispositivo para computación (device_type)
. En el caso de la GPU
, MACE
prepara binarios OpenCL
, por lo que requiere el directorio storage_path
donde la biblioteca puede guardarlos.
A continuación, especificamos la prioridad de la tarea (GPUPerfHint y GPUPriorityHint)
. Si seleccionamos una prioridad alta, la interfaz puede congelarse.
Después de crear la configuración de inicio, cargamos la red neuronal en la memoria y creamos un MaceEngine
.
std::ifstream yoga_classifier_data_in( yoga_classifier_data_, std::ios::binary ); std::vector<unsigned char> yoga_classifier_data_buffer((std::istreambuf_iterator<char>(yoga_classifier_data_in)), (std::istreambuf_iterator<char>( ))); std::ifstream yoga_classifier_pb_in( yoga_classifier_pb_, std::ios::binary ); std::vector<unsigned char> yoga_classifier_pb_buffer((std::istreambuf_iterator<char>(yoga_classifier_pb_in)), (std::istreambuf_iterator<char>( ))); size_t data_size = yoga_classifier_data_buffer.size(); size_t pb_size = yoga_classifier_pb_buffer.size(); std::shared_ptr<mace::MaceEngine> engine; std::vector<std::string> input_names = {"input"}; std::vector<std::string> output_names = {"output"}; MaceStatus create_engine_status = CreateMaceEngineFromProto(&yoga_classifier_pb_buffer[0], pb_size, &yoga_classifier_data_buffer[0], data_size, input_names, output_names, config, &engine); if (create_engine_status != MaceStatus::MACE_SUCCESS) { return 0; }
La red neuronal se devuelve como shared_ptr
, que no podemos pasar directamente a MainActivity
, por lo que presentaremos una clase intermedia ModelData
(que se muestra a continuación).
class ModelData { public: std::shared_ptr<mace::MaceEngine> engine; };
El resultado de loadModel
es un puntero a un objeto de este tipo.
ModelData *result = new ModelData(); result->engine = engine;
Devolviendo el puntero tanto tiempo.
return long(result);
Para la inferencia del modelo, declaramos la función de clasificación en MainActivity
.
public native int classifyImage(long model_ptr, Bitmap bitmap);
También agregamos su definición a native_lib.cpp
.
extern "C" JNIEXPORT jint JNICALL Java_com_example_resnetm_MainActivity_classifyImage(JNIEnv *env, jobject thiz, jlong model_ptr, jobject bitmap) {…}
Más adelante en esta sección, encontrará una descripción paso a paso de esta función.
Primero, restauramos el puntero de los modelos de largo.
ModelData *modelData = (ModelData*)model_ptr;
A continuación, necesitamos preparar los datos en formato NCHW
(a continuación se proporciona un código de muestra).
for (int i = 0; i < height; i++) { for (int j = 0; j < width; j++) { for (int k = 0; k < n_channels; k++) { float c = src_input[i*width*bitmap_step + j*bitmap_step + k]; c = (c / 255 - mean[k]) / std[k]; dst_input[k*width*height + i*width + j] = c; } } }
Después de cargar los datos, necesitamos formar parámetros para una red neuronal como un diccionario de tensores, como este código.
Las formas de los tensores deben coincidir con las especificadas en el archivo resnet_model.yml
.
std::vector<std::string> input_names = {"input"}; std::vector<std::string> output_names = {"output"}; vector<vector<int64_t>> input_shapes; input_shapes.push_back(vector<int64_t> {1, 3, 256, 256}); vector<vector<int64_t>> output_shapes; output_shapes.push_back(vector<int64_t> {1, 1000}); std::map<std::string, mace::MaceTensor> inputs; inputs["input"] = mace::MaceTensor(input_shapes[0], buffer_in, DataFormat::NCHW); auto buffer_out = std::shared_ptr<float>(new float[1*1000], std::default_delete<float[]>()); std::map<std::string, mace::MaceTensor> outputs; outputs["output"] = mace::MaceTensor(output_shapes[0], buffer_out);
Ahora los datos están listos. Iniciemos la inferencia de modelos.
run_status = modelData->engine->Run(inputs, &outputs); auto code = run_status.code(); string inference_info = run_status.information(); float *res = outputs["output"].data().get();
La clase predicha será el número de salida con el valor máximo.
int result = 0; float best_score = 0; for (int i = 0; i < 1000; i++) { float score = res[i]; if (score > best_score) { best_score = score; result = i; } }
Ahora, se crea la parte principal de la aplicación. También necesitamos algo de lógica Java
relacionada con la interacción del usuario.
No se describe en el artÃculo, pero se puede encontrar en el repositorio del proyecto. Después de agregarlo, podemos construir el programa y ejecutarlo en el teléfono móvil.
Espero que le haya sido de utilidad. Gracias por leer este artÃculo.
Añadir comentario