Cómo implementar modelos TensorFlow en producción usando TF Serving

Introducción

Poner en producción los modelos de Machine Learning (ML) se ha convertido en un tema popular y recurrente. Muchas empresas y marcos ofrecen diferentes soluciones que tienen como objetivo abordar este problema.

Para abordar esta preocupación, Google lanzó TensorFlow (TF) Serving con la esperanza de resolver el problema de implementar modelos ML en producción.

Esta pieza ofrece un tutorial práctico sobre el servicio de una red de segmentación semántica convolucional previamente capacitada. Al final de este artículo, podrá usar TF Serving para implementar y realizar solicitudes a un Deep CNN capacitado en TF. Además, presentaré una descripción general de los principales bloques de TF Serving, y discutiré sus API y cómo funciona todo.

Una cosa que notará de inmediato es que requiere muy poco código para servir realmente un modelo TF. Si desea seguir el tutorial y ejecutar el ejemplo en su máquina, sígalo como está. Pero, si solo quiere saber acerca de Servir TensorFlow, puede concentrarse en las dos primeras secciones.

Esta pieza enfatiza parte del trabajo que estamos haciendo aquí en Daitan Group.

Bibliotecas de servicio de TensorFlow: descripción general

Dediquemos un tiempo a comprender cómo TF Serving maneja el ciclo de vida completo de los modelos de ML. Aquí, repasaremos (a un alto nivel) cada uno de los bloques de construcción principales de TF Serving. El objetivo de esta sección es proporcionar una introducción suave a las API de Servir TF. Para obtener una descripción detallada, diríjase a la página de documentación de TF Serving.

TensorFlow Serving se compone de algunas abstracciones. Estas abstracciones implementan API para diferentes tareas. Los más importantes son Servable, Loader, Source y Manager. Veamos cómo interactúan.

En pocas palabras, el ciclo de vida del servicio comienza cuando TF Serving identifica un modelo en el disco. El componente Fuente se encarga de eso. Es responsable de identificar nuevos modelos que deben cargarse. En la práctica, vigila el sistema de archivos para identificar cuándo llega una nueva versión del modelo al disco. Cuando ve una nueva versión, procede creando un cargador para esa versión específica del modelo.

En resumen, el cargador sabe casi todo sobre el modelo. Incluye cómo cargarlo y cómo estimar los recursos necesarios del modelo, como la memoria RAM y GPU solicitada. El cargador tiene un puntero al modelo en el disco junto con todos los metadatos necesarios para cargarlo. Pero hay un problema aquí: el cargador no puede cargar el modelo todavía.

Después de crear el cargador, la fuente lo envía al administrador como una versión deseada.

Al recibir la Versión Aspirada del modelo, el Gerente continúa con el proceso de publicación. Aquí hay dos posibilidades. Una es que la primera versión del modelo se empuja para su implementación. En esta situación, el Administrador se asegurará de que los recursos necesarios estén disponibles. Una vez que lo están, el administrador le da permiso al cargador para cargar el modelo.

El segundo es que estamos impulsando una nueva versión de un modelo existente. En este caso, el Administrador debe consultar el complemento de Política de versiones antes de continuar. La Política de versiones determina cómo se lleva a cabo el proceso de carga de una nueva versión del modelo.

Específicamente, al cargar una nueva versión de un modelo, podemos elegir entre preservar (1) disponibilidad o (2) recursos. En el primer caso, estamos interesados ​​en asegurarnos de que nuestro sistema esté siempre disponible para las solicitudes de los clientes entrantes. Sabemos que el Administrador le permite al Cargador crear instancias del nuevo gráfico con los nuevos pesos.

En este punto, tenemos dos versiones de modelo cargadas al mismo tiempo. Pero el Administrador descarga la versión anterior solo después de que se completa la carga y es seguro cambiar entre modelos.

Por otro lado, si queremos ahorrar recursos al no tener el búfer adicional (para la nueva versión), podemos optar por preservar los recursos. Puede ser útil para modelos muy pesados ​​tener una pequeña brecha en la disponibilidad, a cambio de ahorrar memoria.

Al final, cuando un cliente solicita un identificador para el modelo, el Administrador devuelve un identificador al Servable.

Con esta descripción general, estamos listos para sumergirnos en una aplicación del mundo real. En las siguientes secciones, describimos cómo prestar servicio a una Red neuronal convolucional (CNN) mediante TF Serving.

Exportar un modelo para servir

El primer paso para servir un modelo ML construido en TensorFlow es asegurarse de que esté en el formato correcto. Para hacer eso, TensorFlow proporciona la clase SavedModel.

SavedModel es el formato de serialización universal para los modelos TensorFlow. Si está familiarizado con TF, probablemente haya utilizado el TensorFlow Saver para conservar las variables de su modelo.

TensorFlow Saver proporciona funcionalidades para guardar / restaurar los archivos de puntos de control del modelo en / desde el disco. De hecho, SavedModel envuelve el TensorFlow Saver y está destinado a ser la forma estándar de exportar modelos TF para servir.

El objeto SavedModel tiene algunas características agradables.

Primero, le permite guardar más de un meta-gráfico en un solo objeto SavedModel. En otras palabras, nos permite tener diferentes gráficos para diferentes tareas.

Por ejemplo, suponga que acaba de terminar de entrenar a su modelo. En la mayoría de las situaciones, para realizar la inferencia, su gráfico no necesita algunas operaciones específicas de capacitación. Estas operaciones pueden incluir variables del optimizador, tensores de programación de velocidad de aprendizaje, operaciones de preprocesamiento adicionales, etc.

Además, es posible que desee publicar una versión cuantificada de un gráfico para la implementación móvil.

En este contexto, SavedModel le permite guardar gráficos con diferentes configuraciones. En nuestro ejemplo, tendríamos tres gráficos diferentes con las etiquetas correspondientes, como "entrenamiento", "inferencia" y "móvil". Además, estos tres gráficos compartirían el mismo conjunto de variables, lo que enfatiza la eficiencia de la memoria.

No hace mucho tiempo, cuando queríamos implementar modelos TF en dispositivos móviles, necesitábamos saber los nombres de los tensores de entrada y salida para alimentar y obtener datos desde / hacia el modelo. Esta necesidad obligó a los programadores a buscar el tensor que necesitaban entre todos los tensores del gráfico. Si los tensores no se nombraran correctamente, la tarea podría ser muy tediosa.

Para facilitar las cosas, SavedModel ofrece soporte para SignatureDefs. En resumen, SignatureDefs define la firma de un cálculo compatible con TensorFlow. Determina los tensores de entrada y salida adecuados para un gráfico computacional. En pocas palabras, con estas firmas puede especificar los nodos exactos que se utilizarán para la entrada y la salida.

Para utilizar sus API de servicio integradas, TF Serving requiere que los modelos incluyan uno o más SignatureDefs.

Para crear tales firmas, necesitamos proporcionar definiciones para entradas, salidas y el nombre del método deseado. Las entradas y salidas representan una asignación de cadena a objetos TensorInfo (más sobre esto último). Aquí, definimos los tensores predeterminados para alimentar y recibir datos hacia y desde un gráfico. El parámetro method_name se dirige a una de las API de servicio de alto nivel de TF.

Actualmente, hay tres API de servicio: clasificación, predicción y regresión. Cada definición de firma coincide con una API RPC específica. La Clasificación SegnatureDef se utiliza para la API Classify RPC. Predict SegnatureDef se utiliza para la API Predict RPC y sigue.

Para la firma de Clasificación, debe haber un tensor de entradas (para recibir datos) y al menos uno de los dos posibles tensores de salida: clases y / o puntajes. La regresión SignatureDef requiere exactamente un tensor para la entrada y otro para la salida. Por último, la firma Predict permite un número dinámico de tensores de entrada y salida.

Además, SavedModel admite el almacenamiento de activos para casos en los que la inicialización de operaciones depende de archivos externos. Además, tiene mecanismos para borrar dispositivos antes de crear el modelo guardado.

Ahora, veamos cómo podemos hacerlo en la práctica.

Configurando el medio ambiente

Antes de comenzar, clone esta implementación de TensorFlow DeepLab-v3 de Github.

DeepLab es la mejor segmentación semántica de Google ConvNet. Básicamente, la red toma una imagen como entrada y genera una imagen similar a una máscara que separa ciertos objetos del fondo.

Esta versión fue entrenada en el conjunto de datos de segmentación de VOC Pascal. Por lo tanto, puede segmentar y reconocer hasta 20 clases. Si desea saber más sobre la segmentación semántica y DeepLab-v3, eche un vistazo a Inmersión en redes de segmentación semántica convolucional profunda y Deeplab_V3.

Todos los archivos relacionados con el servicio residen en: ./deeplab_v3/serving/. Allí encontrará dos archivos importantes: deeplab_saved_model.py y deeplab_client.ipynb

Antes de continuar, asegúrese de descargar el modelo previamente entrenado Deeplab-v3. Dirígete al repositorio de GitHub arriba, haz clic en el enlace de los puntos de control y descarga la carpeta llamada 16645 /.

Al final, debe tener una carpeta llamada tboard_logs / con la carpeta 16645 / ubicada dentro de ella.

Ahora, necesitamos crear dos entornos virtuales de Python. Uno para Python 3 y otro para Python 2. Para cada env, asegúrese de instalar las dependencias necesarias. Puede encontrarlos en los archivos serving_requirements.txt y client_requirements.txt.

Necesitamos dos Python env porque nuestro modelo, DeepLab-v3, fue desarrollado bajo Python 3. Sin embargo, la API de Python de TensorFlow Serving solo se publica para Python 2. Por lo tanto, para exportar el modelo y ejecutar TF que sirve, usamos Python 3 env . Para ejecutar el código del cliente utilizando la API de Python TF Serving, utilizamos el paquete PIP (solo disponible para Python 2).

Tenga en cuenta que puede renunciar al entorno de Python 2 utilizando las API de publicación de Bazel. Consulte la Instalación de servicio TF para obtener más detalles.

Con ese paso completo, comencemos con lo que realmente importa.

Cómo hacerlo

Para usar SavedModel, TensorFlow proporciona una clase de utilidad de alto nivel fácil de usar llamada SavedModelBuilder. La clase SavedModelBuilder proporciona funcionalidades para guardar múltiples metagramas, variables asociadas y activos.

Veamos un ejemplo en ejecución de cómo exportar un modelo CNN de segmentación profunda para servir.

Como se mencionó anteriormente, para exportar el modelo, usamos la clase SavedModelBuilder. Generará un archivo de búfer de protocolo SavedModel junto con las variables y los activos del modelo (si es necesario).

Vamos a diseccionar el código.

SavedModelBuilder recibe (como entrada) el directorio donde guardar los datos del modelo. Aquí, la variable export_path es la concatenación de export_path_base y model_version. Como resultado, se guardarán diferentes versiones del modelo en directorios separados dentro de la carpeta export_path_base.

Supongamos que tenemos una versión de referencia de nuestro modelo en producción, pero queremos implementar una nueva versión del mismo. Hemos mejorado la precisión de nuestro modelo y queremos ofrecer esta nueva versión a nuestros clientes.

Para exportar una versión diferente del mismo gráfico, solo podemos establecer FLAGS.model_version en un valor entero más alto. Luego, se creará una carpeta diferente (que contiene la nueva versión de nuestro modelo) dentro de la carpeta export_path_base.

Ahora, necesitamos especificar los tensores de entrada y salida de nuestro modelo. Para hacer eso, usamos SignatureDefs. Las firmas definen qué tipo de modelo queremos exportar. Proporciona una asignación de cadenas (nombres de Tensor lógicos) a objetos TensorInfo. La idea es que, en lugar de hacer referencia a los nombres reales de tensor para entrada / salida, los clientes pueden hacer referencia a los nombres lógicos definidos por las firmas.

Para servir una CNN de segmentación semántica, vamos a crear una firma de predicción. Tenga en cuenta que la función build_signature_def () toma la asignación de los tensores de entrada y salida, así como la API deseada.

Un SignatureDef requiere la especificación de: entradas, salidas y nombre del método. Tenga en cuenta que esperamos tres valores para las entradas: una imagen y dos tensores más que especifiquen sus dimensiones (alto y ancho). Para las salidas, definimos solo un resultado: la máscara de salida de segmentación.

Tenga en cuenta que las cadenas "imagen", "altura", "ancho" y "mapa de segmentación" no son tensores. En cambio, son nombres lógicos que se refieren a los tensores reales input_tensor, image_height_tensor e image_width_tensor. Por lo tanto, pueden ser cualquier cadena única que desee.

Además, las asignaciones en SignatureDefs se relacionan con objetos de protofoto TensorInfo, no con tensores reales. Para crear objetos TensorInfo, utilizamos la función de utilidad: tf.saved_model.utils.build_tensor_info (tensor).

Eso es. Ahora llamamos a la función add_meta_graph_and_variables () para construir el objeto de búfer del protocolo SavedModel. Luego ejecutamos el método save () y persistirá una instantánea de nuestro modelo en el disco que contiene las variables y los activos del modelo.

Ahora podemos ejecutar deeplab_saved_model.py para exportar nuestro modelo.

Si todo salió bien, verá la carpeta ./serving/versions/1. Tenga en cuenta que el "1" representa la versión actual del modelo. Dentro de cada subdirectorio de la versión, verá los siguientes archivos:

  • saved_model.pb o saved_model.pbtxt. Este es el archivo serializado de SavedModel. Incluye una o más definiciones gráficas del modelo, así como las definiciones de firma.
  • Variables Esta carpeta contiene las variables serializadas de los gráficos.

Ahora, estamos listos para lanzar nuestro servidor modelo. Para hacer eso, ejecuta:

$ tensorflow_model_server --port = 9000 --model_name = deeplab --model_base_path = 

Model_base_path se refiere a dónde se guardó el modelo exportado. Además, no especificamos la carpeta de versión en la ruta. El control de versiones del modelo es manejado por TF Serving.

Generando solicitudes de clientes

El código del cliente es muy sencillo. Míralo en: deeplab_client.ipynb.

Primero, leemos la imagen que queremos enviar al servidor y la convertimos al formato correcto.

A continuación, creamos un trozo de gRPC. El código auxiliar nos permite llamar a los métodos del servidor remoto. Para hacer eso, instanciamos la clase beta_create_PredictionService_stub del módulo prediction_service_pb2. En este punto, el código auxiliar contiene la lógica necesaria para llamar a procedimientos remotos (desde el servidor) como si fueran locales.

Ahora, necesitamos crear y establecer el objeto de solicitud. Dado que nuestro servidor implementa la API de predicción de TensorFlow, debemos analizar una solicitud de predicción. Para emitir una solicitud de Predict, primero, instanciamos la clase PredictRequest del módulo predict_pb2. También necesitamos especificar los parámetros model_spec.name y model_spec.signature_name. El nombre param es el argumento "nombre_modelo" que definimos cuando lanzamos el servidor. Y el signature_name se refiere al nombre lógico asignado al parámetro signature_def_map () de la rutina add_meta_graph ().

A continuación, debemos proporcionar los datos de entrada tal como se definen en la firma del servidor. Recuerde que, en el servidor, definimos una API de predicción para esperar una imagen y dos escalares (la altura y el ancho de la imagen). Para alimentar los datos de entrada al objeto de solicitud, TensorFlow proporciona la utilidad tf.make_tensor_proto (). Este método crea un objeto TensorProto a partir de un objeto numpy / Python. Podemos usarlo para alimentar la imagen y sus dimensiones al objeto de solicitud.

Parece que estamos listos para llamar al servidor. Para hacer eso, llamamos al método Predict () (usando el código auxiliar) y pasamos el objeto de solicitud como argumento.

Para las solicitudes que devuelven una sola respuesta, gRPC admite llamadas síncronas y asíncronas. Por lo tanto, si desea realizar algún trabajo mientras se procesa la solicitud, podríamos llamar a Predict.future () en lugar de Predict ().

Ahora podemos buscar y disfrutar los resultados.

Espero que les haya gustado este artículo. ¡Gracias por leer!

Si quieres más, mira: