Implemente modelos grandes en Amazon SageMaker mediante la inferencia paralela de modelos DJLServing y DeepSpeed

Los últimos años han visto un rápido desarrollo en el campo del procesamiento del lenguaje natural (PNL). Aunque el hardware ha mejorado, como con la última generación de aceleradores de NVIDIA y Amazon, los practicantes de aprendizaje automático (ML) avanzado todavía encuentran problemas regularmente al implementar sus modelos de lenguaje grande. Hoy, anunciamos nuevas capacidades en Amazon SageMaker que pueden ayudar: puede configurar el tamaño máximo del volumen de Amazon EBS y las cuotas de tiempo de espera para facilitar la inferencia de modelos grandes. Junto con las técnicas de inferencia paralela de modelos, ahora puede utilizar las capacidades de administración e implementación de modelos completamente administradas de SageMaker al trabajar con modelos grandes con miles de millones de parámetros.

En esta publicación, demostramos estas nuevas capacidades de SageMaker mediante la implementación de un gran modelo de NLP preentrenado de Hugging Face en varias GPU. En particular, utilizamos las técnicas de paralelismo de tensor y servicio de Deep Java Library (DJL) de DeepSpeed ​​para lograr una latencia inferior a 0,1 segundos en un caso de uso de generación de texto con 6 mil millones de parámetros GPT-J. Ejemplo completo en nuestro repositorio de GitHub próximamente.

Grandes modelos de lenguaje y la creciente necesidad de modelo de inferencia paralela

Los modelos de lenguaje se han disparado recientemente tanto en tamaño como en popularidad. En 2018, BERT-grande entró en escena y, con sus 340 millones de parámetros y su novedosa arquitectura de transformadores, establece el estándar en la precisión de las tareas de NLP. En solo unos pocos años, el tamaño del modelo de PNL de última generación ha crecido más de 500 veces, con modelos como el GPT-3 de 175 mil millones de parámetros de OpenAI y el Bloom 176 B de código abierto de tamaño similar elevando el nivel de precisión de la PNL . Este aumento en el número de parámetros está impulsado por la relación positiva simple y empíricamente demostrada entre el tamaño del modelo y la precisión: más es mejor. Con fácil acceso desde zoológicos de modelos como Hugging Face y precisión mejorada en tareas de PNL como clasificación y generación de texto, los profesionales buscan cada vez más estos grandes modelos. Sin embargo, implementarlos puede ser un desafío.

Los modelos de lenguaje grandes pueden ser difíciles de hospedar para casos de uso de inferencia de baja latencia debido a su tamaño. Por lo general, los profesionales de ML simplemente alojan un modelo (o incluso varios modelos) dentro de la memoria de un solo dispositivo acelerador que maneja la inferencia de extremo a extremo por sí solo. Sin embargo, los modelos de lenguaje grandes pueden ser demasiado grandes para caber en la memoria de un solo acelerador, por lo que este paradigma no puede funcionar. Por ejemplo, código abierto GPT-NeoX con 20 mil millones de parámetros puede requerir más de 80 GB de memoria aceleradora, que es más del triple de lo que está disponible en una NVIDIA A10G, una popular GPU para inferencia. Los profesionales tienen algunas opciones para trabajar contra esta limitación de memoria del acelerador. Un enfoque simple pero lento es usar la memoria de la CPU y transmitir los parámetros del modelo secuencialmente al acelerador. Sin embargo, esto introduce un cuello de botella en la comunicación entre la CPU y la GPU, que puede agregar segundos a la latencia de inferencia y, por lo tanto, no es adecuado para muchos casos de uso que requieren respuestas rápidas. Otro enfoque es optimizar o comprimir el modelo para que pueda caber en un solo dispositivo. Los profesionales deben implementar técnicas complejas como cuantificación, poda, destilación y otras para reducir los requisitos de memoria. Este enfoque requiere mucho tiempo y experiencia y también puede reducir la precisión y la generalización de un modelo, lo que también puede ser un fracaso para muchos casos de uso.

Una tercera opción para usar el paralelismo de modelos. Con el paralelismo de modelos, los parámetros y las capas de un modelo se dividen y luego se distribuyen entre varios aceleradores. Este enfoque permite a los profesionales aprovechar la memoria y el poder de procesamiento de múltiples aceleradores a la vez y puede ofrecer inferencias de baja latencia sin afectar la precisión del modelo. El paralelismo de modelos ya es una técnica popular en el entrenamiento (consulte Introducción al paralelismo de modelos) y se usa cada vez más en la inferencia, ya que los profesionales requieren respuestas de baja latencia de modelos grandes.

Hay dos tipos generales de paralelismo de modelo: paralelismo de canalización y paralelismo de tensor. El paralelismo de canalización divide un modelo entre capas, de modo que cualquier capa dada está contenida dentro de la memoria de una sola GPU. Por el contrario, el paralelismo tensorial divide las capas de modo que una capa de modelo se distribuye en varias GPU. Ambas técnicas de modelo paralelo se utilizan en el entrenamiento (a menudo juntas), pero el paralelismo de tensores puede ser una mejor opción para la inferencia porque el tamaño del lote suele ser uno con la inferencia. Cuando el tamaño del lote es uno, solo el paralelismo de tensor puede aprovechar varias GPU a la vez al procesar el paso hacia adelante para mejorar la latencia.

En esta publicación, usamos DeepSpeed ​​para particionar el modelo usando técnicas de paralelismo tensorial. DeepSpeed ​​Inference admite grandes modelos basados ​​en transformadores con miles de millones de parámetros. Le permite servir modelos grandes de manera eficiente al adaptarse a las mejores estrategias de paralelismo para la inferencia de múltiples GPU, teniendo en cuenta tanto la latencia como el costo de la inferencia. Para obtener más información, consulte DeepSpeed: aceleración de la inferencia y el entrenamiento de modelos a gran escala a través de optimizaciones y compresión del sistema y esto Inferencia de DeepSpeed: habilitación de la inferencia eficiente de modelos de transformadores a una escala sin precedentes.

Descripción general de la solución

Deep Java Library (DJL) es un marco Java independiente del motor, de alto nivel y de código abierto para el aprendizaje profundo. El DJL está construido con conceptos nativos de Java además de los marcos de aprendizaje profundo existentes. El DJL está diseñado para ser un motor de aprendizaje profundo agonista. Puede cambiar de motor en cualquier momento. El DJL también proporciona elección automática de CPU/GPU basada en la configuración del hardware.

Aunque DJL está diseñado originalmente para que los desarrolladores de Java comiencen con ML, DJLServing es una solución de servicio de modelo universal de alto rendimiento impulsada por DJL que es independiente del lenguaje de programación. Puede servir a los tipos de modelos comúnmente vistos, como el modelo PyTorch TorchScript, el paquete TensorFlow SavedModel, el modelo Apache MXNet, el modelo ONNX, el modelo TensorRT y el modelo de script de Python. DJLServing admite procesamiento por lotes dinámico y escalado automático de trabajadores para aumentar el rendimiento. Puede cargar diferentes versiones de un modelo en un solo punto final. También puede servir modelos de diferentes marcos de ML al mismo tiempo. Además, DJLServing admite de forma nativa múltiples GPU configurando configuraciones MPI y conexiones de socket para inferencia. Esto libera el trabajo pesado de configurar un entorno multi-GPU.

Nuestra solución propuesta utiliza las capacidades de SageMaker recientemente anunciadas, DJLServing y DeepSpeed ​​Inference, para la inferencia de modelos grandes. A partir de este escrito, todos TransformadorSe admiten modelos basados ​​en . Esta solución está pensada para la inferencia de modelos paralelos utilizando un solo modelo en una sola instancia.

DJLServing está construido con múltiples capas. La capa de enrutamiento se construye sobre Neto. Las solicitudes remotas se manejan en la capa de enrutamiento para distribuirlas a los trabajadores, ya sean subprocesos en Java o procesos en Python, para ejecutar la inferencia. El número total de subprocesos de Java se establece en 2 * cpu_core de la máquina para aprovechar al máximo la potencia informática. Los números de trabajadores se pueden configurar por modelo o la detección automática de DJL en el hardware. El siguiente diagrama ilustra nuestra arquitectura.

Inferencia de modelos grandes en SageMaker

Los siguientes pasos demuestran cómo implementar un modelo gpt-j-6B en SageMaker mediante el servicio DJL. Esto es posible gracias a la capacidad de configurar el tamaño del volumen de EBS, el tiempo de espera de descarga del modelo y el tiempo de espera de verificación de estado de inicio. Puede probar esta demostración ejecutando el siguiente cuaderno.

Extraiga la imagen de Docker y envíela a Amazon ECR

La imagen de Docker djl-serving:0.18.0-deepspeed es nuestro contenedor de servicio DJL con DeepSpeed ​​incorporado. Luego, enviamos esta imagen a Amazon Elastic Container Registry (Amazon ECR) para su uso posterior. Ver el siguiente código:

docker pull deepjavalibrary/djl-serving:0.18.0-deepspeed

Crear nuestro archivo modelo

Primero, creamos un archivo llamado serving.properties que contiene sólo una línea de código. Esto le dice al servidor modelo DJL que use el Rubikon motor. Rubikon es un paquete de soporte de modelos grandes desarrollado por AWS. En esta demostración, facilita la configuración de subprocesos MPI y la conexión de socket. También establece el número de GPU (número de corte del modelo) leyendo en el TENSOR_PARALLEL_DEGREE parámetro definido en nuestro model.py archivo en el párrafo siguiente. El archivo contiene el siguiente código:

A continuación, creamos nuestro model.py archivo, que define nuestro modelo como gpt-j-6B. En nuestro código, leemos en el TENSOR_PARALLEL_DEGREE variable de entorno (el valor predeterminado es 1). Establece el número de dispositivos sobre los que se distribuyen los módulos paralelos tensoriales. Tenga en cuenta que DeepSpeed ​​proporciona algunas lógicas de partición integradas y gpt-j-6B es uno de ellos. Lo usamos especificando replace_method y relpace_with_kernel_inject. Si tiene su modelo personalizado y necesita DeepSpeed ​​para particionar de manera efectiva, debe cambiar relpace_with_kernel_inject a falso y agregar injection_policy para hacer que la partición de tiempo de ejecución funcione. Para obtener más información, consulte a Inicializar para inferencia.

from djl_python import Input, Output
import os
import deepspeed
import torch
from transformers import pipeline, AutoModelForCausalLM, AutoTokenizer

predictor = None

def get_model():
    model_name="EleutherAI/gpt-j-6B"
    tensor_parallel = int(os.getenv('TENSOR_PARALLEL_DEGREE', '1'))
    local_rank = int(os.getenv('LOCAL_RANK', '0'))
    model = AutoModelForCausalLM.from_pretrained(model_name, revision="float32", torch_dtype=torch.float32)
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    
    model = deepspeed.init_inference(model,
                                     mp_size=tensor_parallel,
                                     dtype=model.dtype,
                                     replace_method='auto',
                                     replace_with_kernel_inject=True)
    generator = pipeline(task='text-generation', model=model, tokenizer=tokenizer, device=local_rank)
    return generator


def handle(inputs: Input) -> None:
    global predictor
    if not predictor:
        predictor = get_model()

    if inputs.is_empty():
        # Model server makes an empty call to warmup the model on startup
        return None

    data = inputs.get_as_string()
    result = predictor(data, do_sample=True, min_tokens=200, max_new_tokens=256)
    return Output().add(result)

Creamos un directorio llamado gpt-j y copiar model.py y serving.properties a este directorio:

mkdir gpt-j
cp model.py gpt-j
cp serving.properties gpt-j

Por último, creamos el archivo del modelo y lo subimos a Amazon Simple Storage Service (Amazon S3):

tar cvfz gpt-j.tar.gz gpt-j
aws s3 cp gpt-j.tar.gz s3://djl-sm-test/deepspeed/

Crear un modelo de SageMaker

Ahora creamos un modelo de SageMaker. Usamos la imagen de ECR que creamos anteriormente y el artefacto del modelo del paso anterior para crear el modelo de SageMaker. En la configuración del modelo, configuramos TENSOR_PARALLEL_DEGREE=2, lo que significa que el modelo se dividirá en 2 GPU. Ver el siguiente código:

aws sagemaker create-model 
--model-name gpt-j 
--primary-container 
Image=<account_id>.dkr.ecr.us-east-1.amazonaws.com/djl-deepspeed:latest,ModelDataUrl=s3://djl-sm-test/deepspeed/gpt-j.tar.gz,Environment= 
--execution-role-arn <IAM_role_arn>

Después de ejecutar el comando anterior, verá un resultado similar al siguiente:

Crear un punto final de SageMaker

Puede usar cualquier instancia con varias GPU para realizar pruebas. En esta demostración, usamos una instancia p3.16xlarge. En el siguiente código, observe cómo configuramos el ModelDataDownloadTimeoutInSeconds, ContainerStartupHealthCheckTimeoutInSecondsy VolumeSizeInGB parámetros para acomodar el gran tamaño del modelo. los VolumeSizeInGB El parámetro se aplica a las instancias de GPU que admiten el archivo adjunto de volumen de EBS.

aws sagemaker create-endpoint-config 
    --region us-east-1 
    --endpoint-config-name gpt-j-config 
    --production-variants '[
      
    ]'

Por último, creamos un punto final de SageMaker:

aws sagemaker create-endpoint 
--endpoint-name gpt-j 
--endpoint-config-name gpt-j-config

Lo ves impreso en el siguiente código:

Iniciar el punto final puede llevar un tiempo. Puedes intentarlo unas cuantas veces más si te encuentras con el InsufficientInstanceCapacity error.

La optimización del rendimiento

El ajuste y la optimización del rendimiento es un proceso empírico que a menudo implica múltiples iteraciones. El número de parámetros a ajustar es combinatorio y el conjunto de valores de los parámetros de configuración no son independientes entre sí. Varios factores afectan el ajuste óptimo de los parámetros, incluidos el tamaño de la carga útil, el tipo y la cantidad de modelos ML en el gráfico de flujo de solicitud de inferencia, el tipo de almacenamiento, el tipo de instancia informática, la infraestructura de red, el código de la aplicación, el tiempo de ejecución y la configuración del software de servicio de inferencia, y más.

La inferencia en tiempo real de SageMaker es ideal para cargas de trabajo de inferencia en las que tiene requisitos de baja latencia, interactivos y en tiempo real. Hay cuatro métricas más utilizadas para monitorear la latencia de solicitud de inferencia para puntos finales de inferencia de SageMaker:

  • Latencia del contenedor – El tiempo que lleva enviar la solicitud, obtener la respuesta del contenedor del modelo y completar la inferencia en el contenedor. Esta métrica está disponible en Amazon CloudWatch como parte de las métricas de invocación publicadas por SageMaker.
  • latencia del modelo – El tiempo total que tardan todos los contenedores de SageMaker en una canalización de inferencia. Esta métrica está disponible en CloudWatch como parte de las métricas de invocación publicadas por SageMaker.
  • latencia de sobrecarga – Medido desde el momento en que SageMaker recibe la solicitud hasta que devuelve una respuesta al cliente, menos la latencia del modelo. Esta métrica está disponible en CloudWatch como parte de las métricas de invocación publicadas por SageMaker.
  • Latencia de extremo a extremo – Medido desde el momento en que el cliente envía la solicitud de inferencia hasta que recibe una respuesta. Puede publicar esto como una métrica personalizada en CloudWatch.

La latencia del contenedor depende de varios factores; los siguientes son algunos de los más importantes:

  • Protocolo subyacente (HTTP(s)/gRPC) utilizado para comunicarse con el servidor de inferencia
  • Sobrecarga relacionada con la creación de nuevas conexiones TLS
  • Tiempo de deserialización de la carga útil de solicitud/respuesta
  • Solicitud de colas y funciones de procesamiento por lotes proporcionadas por el servidor de inferencia subyacente
  • Capacidades de programación de solicitudes proporcionadas por el servidor de inferencia subyacente
  • Rendimiento de tiempo de ejecución subyacente del servidor de inferencia
  • Rendimiento de las bibliotecas de preprocesamiento y posprocesamiento antes de llamar a la función de predicción del modelo
  • Rendimiento de back-end del marco de ML subyacente
  • Optimizaciones específicas del modelo y del hardware

En esta sección, nos centramos principalmente en la latencia del contenedor y, específicamente, en la optimización de DJLServing que se ejecuta dentro de un contenedor de SageMaker.

Ajuste el motor ML para la inferencia de subprocesos múltiples

Una de las ventajas de DJL es el soporte de inferencia de subprocesos múltiples. Puede ayudar a aumentar el rendimiento de su inferencia en CPU y GPU de varios núcleos y reducir el consumo de memoria en comparación con Python. Referirse a Optimización del rendimiento de la inferencia para obtener más información sobre cómo optimizar la cantidad de subprocesos para diferentes motores.

Tune Netty

DJLServing está construido con múltiples capas. La capa de enrutamiento se construye sobre Neto. Netty es un marco de servidor de cliente NIO que permite el desarrollo rápido y fácil de aplicaciones de red, como servidores de protocolo y clientes. en red, Channel es el contenedor principal; contiene un ChannelPipeline y está asociado con un EventLoop (un contenedor para un hilo) de un EventLoopGroup. EventLoop es esencialmente un subproceso de E/S y puede ser compartido por múltiples canales. ChannelHandlers se ejecutan en estos EventLoop hilos. Este modelo de subprocesamiento simple significa que no necesita preocuparse por los problemas de concurrencia en la ejecución de su ChannelHandlers. Siempre tiene garantizadas ejecuciones secuenciales en el mismo subproceso para una sola ejecución a través de su canalización. DJLServing utiliza Netty EpollEventLoopGroup en Linux. El número total de subprocesos Netty de forma predeterminada se establece en 2 * el número de CPU virtuales de la máquina para aprovechar al máximo la potencia informática. Además, debido a que no crea una gran cantidad de subprocesos, su CPU no está sobrecargada por el cambio de contexto. Esta configuración predeterminada funciona bien en la mayoría de los casos; sin embargo, si desea configurar el número de subprocesos de Netty para procesar las solicitudes entrantes, puede hacerlo configurando el SERVING_NUMBER_OF_NETTY_THREADS Variable ambiental.

Sintonice la gestión de la carga de trabajo (WLM) de DJLServing

DJLSirviendo tiene Administrador de carga de trabajo, que es responsable de administrar la carga de trabajo del subproceso de trabajo. Administra los grupos de subprocesos y las colas de trabajos, y aumenta o reduce la cantidad requerida de subprocesos de trabajo por modelo de ML. Tiene escalado automático, que agrega un trabajo de inferencia a la cola de trabajos del siguiente trabajador libre y aumenta el grupo de subprocesos del trabajador para ese modelo específico si es necesario. La escala se basa principalmente en la profundidad de la cola de trabajos del modelo, el tamaño del lote y la cantidad actual de subprocesos de trabajo en el grupo. los job_queue_size controla el número de trabajos de inferencia que se pueden poner en cola en cualquier momento. De forma predeterminada, se establece en 100. Si tiene necesidades de concurrencia más altas por instancia de servicio de modelo, puede aumentar el job_queue_sizeel tamaño del grupo de subprocesos y los trabajadores de subprocesos mínimos o máximos para un modelo en particular configurando las propiedades en serving.propertiescomo se muestra en el siguiente código de ejemplo:

serving.properties
# use minWorkers/maxWorkers for all devices
gpu.minWorkers=2
gpu.maxWorkers=3
cpu.minWorkers=2
cpu.maxWorkers=4

A partir de este escrito, no puede configurar job_queue_size en serving.properties. El valor predeterminado job_queue_size está controlado por una variable de entorno, y solo puede configurar la configuración por modelo con el registerModel API.

Muchos profesionales tienden a ejecutar la inferencia secuencialmente cuando se invoca el servidor con múltiples solicitudes independientes. Aunque es más fácil de configurar, generalmente no es la mejor práctica utilizar la potencia de cómputo de la GPU. Para abordar esto, DJLServing ofrece las optimizaciones integradas de procesamiento por lotes dinámico para combinar estas solicitudes de inferencia independientes en el lado del servidor para formar un lote más grande dinámicamente para aumentar el rendimiento.

Todas las solicitudes llegan primero al dosificador dinámico antes de ingresar a las colas de trabajo reales para esperar la inferencia. Puede establecer sus tamaños de lote preferidos para el procesamiento por lotes dinámico utilizando el batch_size ajustes en serving.properties. También puede configurar max_batch_delay para especificar el tiempo de demora máximo en el procesador por lotes para esperar otras solicitudes para unirse al lote en función de sus requisitos de latencia.

Puede ajustar los siguientes parámetros para aumentar el rendimiento por modelo:

  • tamaño del lote – El tamaño del lote de inferencia. El valor predeterminado es 1.
  • max_batch_delay – El retraso máximo para la agregación por lotes. El valor predeterminado es 100 milisegundos.
  • Max Idle Time – El tiempo de inactividad máximo antes de que se reduzca el subproceso de trabajo.
  • min_worker – El número mínimo de procesos de trabajo. Para el motor DeepSpeed ​​de DJL, min_worker se establece en el número de GPU/TENSOR_PARALLEL_DEGREE.
  • max_worker – El número máximo de procesos de trabajo. Para el motor DeepSpeed ​​de DJL, max_worker se establece en el número de GPU/TENSOR_PARALLEL_DEGREE.

Ajustar el grado de paralelismo tensorial

Para la compatibilidad con modelos grandes que no caben en la memoria del dispositivo acelerador único, la cantidad de procesos de Python está determinada por la cantidad total de dispositivos aceleradores en el host. los tensor_parallel_degree se crea para cortar el modelo y distribuirlo a múltiples dispositivos aceleradores. En este caso, incluso si un modelo es demasiado grande para alojarlo en un solo acelerador, DJLServing aún puede manejarlo y puede ejecutarse en múltiples dispositivos aceleradores mediante la partición del modelo. Internamente, DJLServing crea múltiples procesos MPI (igual a tensor_parallel_degree) para administrar el segmento de cada modelo en cada dispositivo acelerador.

Puede establecer el número de particiones para su modelo configurando el TENSOR_PARALLEL_DEGREE Variable ambiental. Tenga en cuenta que esta configuración es una configuración global y se aplica a todos los modelos del host. Si el TENSOR_PARALLEL_DEGREE es menor que el número total de dispositivos aceleradores (GPU), DJLServing lanza múltiples grupos de procesos de Python equivalentes al número total de GPU/TENSOR_PARALLEL_DEGREE. Cada grupo de procesos de Python consta de procesos de Python equivalentes a TENSOR_PARALLEL_DEGREE. Cada grupo de procesos de Python contiene la copia completa del modelo.

Resumen

En esta publicación, mostramos la capacidad de SageMaker recientemente lanzada que le permite configurar volúmenes de EBS de instancias de inferencia, tiempo de espera de descarga de modelos y tiempo de espera de inicio de contenedor. Demostramos esta nueva capacidad en un ejemplo de implementación de un modelo grande en SageMaker. También cubrimos las opciones disponibles para ajustar el rendimiento del DJL. Para obtener más detalles sobre SageMaker y la nueva función lanzada, consulte [!Link] y [!Link].


Sobre los autores

franco liu es ingeniero de software para AWS Deep Learning. Se centra en la creación de herramientas innovadoras de aprendizaje profundo para ingenieros de software y científicos. En su tiempo libre, disfruta de caminatas con amigos y familiares.

qing-lan es ingeniero de desarrollo de software en AWS. Ha estado trabajando en varios productos desafiantes en Amazon, incluidas soluciones de inferencia ML de alto rendimiento y un sistema de registro de alto rendimiento. El equipo de Qing lanzó con éxito el primer modelo de mil millones de parámetros en Amazon Advertising con una latencia muy baja requerida. Qing tiene un conocimiento profundo sobre la optimización de la infraestructura y la aceleración del aprendizaje profundo.

qingweili es especialista en aprendizaje automático en Amazon Web Services. Recibió su Ph.D. en Investigación de Operaciones después de que rompió la cuenta de beca de investigación de su asesor y no entregó el Premio Nobel que prometió. Actualmente, ayuda a los clientes de la industria de seguros y servicios financieros a crear soluciones de aprendizaje automático en AWS. En su tiempo libre le gusta leer y enseñar.

Patel Dhawal es Arquitecto Principal de Aprendizaje Automático en AWS. Ha trabajado con organizaciones que van desde grandes empresas hasta empresas emergentes medianas en problemas relacionados con la computación distribuida y la inteligencia artificial. Se enfoca en el aprendizaje profundo, incluidos los dominios de NLP y Computer Vision. Ayuda a los clientes a lograr una inferencia de modelos de alto rendimiento en SageMaker.

Roberto Van Dusen es gerente sénior de productos en AWS.

alan bronceado es gerente sénior de productos en SageMaker y lidera los esfuerzos en la inferencia de modelos grandes. Le apasiona aplicar Machine Learning al área de Analytics. Fuera del trabajo, disfruta del aire libre.

Fuente del artículo

¿Que te ha parecido?

Deja un comentario