Su modelo no estará terminado hasta que alguien más pueda llamarlo

Para darle contexto, sentí curiosidad por saber qué implica crear e implementar modelos de aprendizaje automático. Entonces, en lugar de aprender teoría. Como siempre, decidí aprender construyendo, porque sigo creyendo que es la mejor manera de aprender. El primer proyecto que elegí abordar fue una API de predicción de abandono.

Es una pequeña aplicación FastAPI que creé para predecir si un cliente está a punto de abandonar. La forma en que funciona es que, si le proporciona los datos de un cliente, le devuelve una probabilidad, una predicción y un nivel de riesgo. En mi artículo anterior, entrené el modelo, lo envolví en un punto final y lo probé en Swagger UI en mi propia computadora portátil. Funcionó y cada solicitud obtuvo una respuesta real. Puedes leer el artículo aquí. En este punto, pensé que ya había terminado.

Ni siquiera cerca. Resulta que el modelo que construí no sirve de ninguna manera. Es sólo otro juguete con el que sólo puedo jugar. No podría sobrevivir a que mi computadora portátil entrara en suspensión, y mucho menos a que un extraño pudiera acceder a ella escribiendo una URL.

Así que voy a abordar este tema en este artículo. Me ocuparé del poco glamoroso trabajo de hacerlo accesible. Entonces, les explicaré cómo lo puse en contenedores y lo moví a un servidor real. Por cierto, también me encontré con tres fallas distintas que no vi venir, que también señalaré en este artículo.

Resumen rápido de dónde terminó la Parte A

Este proyecto implementa una API de abandono de la aplicación FastAPI. Proporciona un punto final simple/predict que utiliza una canalización de aprendizaje de scikit previamente entrenada que consta de un escalador y un clasificador. Esto se utiliza para predecir la probabilidad de que un cliente determinado abandone. Si es nuevo en el proyecto, le recomiendo comenzar con la Parte A, ya que este artículo continúa desde allí.

Por qué Docker

Entonces aquí está el problema. Seguro que el proyecto funciona perfectamente en mi computadora, el problema es que tiene una versión específica de Python, versiones de paquetes específicas, rutas de archivos específicas que la computadora de otra persona en la que se implementaría o un servidor AWS podrían no tener.

Docker resuelve esto. He aquí cómo: en lugar de entregar solo código que con suerte funciona después de que la máquina esté configurada correctamente (lo cual es un tema para otro día), el entregable ahora es una imagen autosuficiente que se ejecuta en cualquier computadora y en cuyo entorno (versión de Python, varias bibliotecas requeridas, estructura de carpetas) ya no tiene efecto. Esto se debe a que todo esto se envía con la imagen misma, por lo que ahora el receptor no necesita configurar nada.

Escribiendo el Dockerfile y lo que se rompió

Incluso antes de comenzar a escribir Dockerfile, quería asegurarme de saber exactamente qué estaba instalado en mi entorno local. El objetivo era utilizar las mismas versiones de paquetes dentro del contenedor, en lugar de permitir que pip instale la última versión en ese momento.

Al principio, intenté usar la lista conda, pero no me daba la lista limpia de versiones de paquetes que necesitaba. Así que cambié a pip Freeze, lo que me dio las versiones exactas de los paquetes con los que estaba trabajando:

fastapi==0.141.1uvicornio[standard]==0.40.0aprendizaje-scikit==1.9.0pandas==3.0.3numeroso==2.4.6pidántico==2.13.4biblioteca de trabajo==1.5.3

Y hubo un detalle más que resultó importante: estaba ejecutando Python 3.14.6 localmente, que es bastante nuevo. Así que no podía simplemente optar por la imagen base habitual de Python:3.11-slim por costumbre. Si quería que el contenedor coincidiera lo más posible con mi entorno local, el Dockerfile también necesitaba usar Python 3.14.

DE Python: 3.14-slimDIRTRABAJO /códigoCOPIAR requisitos.txt.EJECUTAR instalación de pip –no-cache-dir -r requisitos.txtCOPIAR ./aplicación ./aplicaciónCOPIAR ./modelos ./modelosEXPONER 8000CMD [“uvicorn”, “app.main:app”, “–host”, “0.0.0.0”, “–port”, “8000”]

La compilación en sí fue exitosa y no obtuve errores. Aunque tomó alrededor de 166 segundos, probablemente porque algunos paquetes aún no tenían ruedas prediseñadas para esta nueva versión de Python y tuvieron que compilarse desde el código fuente.

Luego lo ejecuté y obtuve esto:

ModuleNotFoundError: ningún módulo llamado ‘esquemas’

Este fue uno de esos problemas que sólo me di cuenta de que existía después de comenzar a ejecutar el contenedor. Durante la construcción nada había sugerido que pudiera surgir tal error.

Cuando ejecutaba la aplicación localmente, siempre me encontraba en la carpeta app/, por lo que ejecutar uvicorn main:app –reload nunca fue un problema: esquemas.py estaba cerca, al lado de main.py, que Python podía encontrar fácilmente.

Pero el Dockerfile era sutilmente diferente. Allí, mi CMD estaba ejecutando uvicorn app.main:app en/code, lo que llevó a Python a creer que la aplicación era un paquete y que debería tratarse como tal. En otras palabras, tenía un directorio de trabajo diferente y eso fue suficiente para romperlo.

La solución fue sencilla. En lugar de modificar la estructura del proyecto, es mucho más sencillo decirle a Uvicorn que use y cargue la aplicación usando la carpeta de la aplicación como raíz. Esto imitaría cómo lo había usado anteriormente.

CMD [“uvicorn”, “main:app”, “–app-dir”, “app”, “–host”, “0.0.0.0”, “–port”, “8000”]

Después de ejecutar esto, reconstruí la imagen y abarqué el contenedor, y esta vez no tuve problemas.

Armado con esto; Al igual que en la Parte A, luego envié una solicitud ficticia a mi FastAPI con el único propósito de comparar mis números de la Parte A. Esta prueba arrojó exactamente lo que esperaba: la misma probabilidad de abandono, la misma predicción, el mismo nivel de riesgo.

Por lo tanto, sabía que mi contenedor no había cambiado ni alterado el comportamiento de la aplicación. Funcionó de la misma manera que en mi máquina. Entonces, primer punto de control: hecho.

Elegir AWS y defender EC2

Elegí EC2 nuevamente, al igual que con la implementación de rss-pipeline. Esto es útil para mantener coherencia en mis ideas sobre la arquitectura de proyectos; este proyecto es una instancia t3.micro que usa Ubuntu; Este tipo de instancia es elegible para el nivel gratuito y es ideal para implementar una aplicación FastAPI que se ejecuta en un solo modelo.

​El proceso de configuración implicó principalmente navegar por las opciones en la consola de AWS. Lancé una instancia y la configuré con Ubuntu; Elegí el tipo de instancia t3.micro y generé un par de claves para la conexión SSH. La configuración del grupo de seguridad debería hacerse con más cuidado: elegí dos reglas de entrada. El primero solicitó SSH en el puerto 22 y funcionó para el tráfico desde mi IP, mientras que el segundo implicó una regla TCP personalizada en el puerto 8000. Se aplicó a todos, ya que se deseaba que se permitiera el acceso API en este puerto.

​Una última cosa: seguí el hábito de utilizar un usuario de IAM, no la cuenta raíz de AWS, cuando escribí esto. Cuando se realiza una operación desde la cuenta raíz de AWS, el acceso a esta cuenta no está restringido por ningún límite. Ejecutar trabajos como configurar una instancia adicional utiliza esta cuenta. Sin embargo, esto conlleva un mayor riesgo porque incluso la más mínima posibilidad de que se produzca una situación imprevista causará una gran cantidad de destrucción. El uso del usuario de IAM con permisos funcionará como lo haría cuando la cuenta raíz realice el trabajo y lo hará con un radio menor si sucede algo, como no debería.

Colocar Docker en EC2 y enviar la imagen

Después de realizar SSH en la instancia, instalé Docker en el servidor:

actualización sudo aptasudo apt instalar -y docker.iosudo systemctl iniciar ventana acoplablesudo systemctl habilitar ventana acoplablesudo usermod -aG ventana acoplable $USUARIO

Decidí reconstruir la imagen de Docker en la instancia directamente con Dockerfile, en lugar de agregarla a Amazon ECR. Con un proyecto a pequeña escala como este, copiar datos de prueba locales parecía más fácil, lo que me ahorró algunos aprendizajes de AWS.

Luego usé scp para colocar los archivos del proyecto en la instancia,

scp -i “churn-api-key.pem” -r . ubuntu@:/home/ubuntu/churn-api

Luego choqué contra una pared:

ssh: conectarse al puerto del host 22: se agotó el tiempo de conexión

Mi primer pensamiento, inevitablemente, es que he hecho algo mal en la instancia. Sin embargo, en realidad esto no es lo que salió mal. En cambio, la culpa fue de la regla de firewall de mi grupo de seguridad, que había restringido el acceso SSH a mi propia dirección IP. Como resultado, la próxima vez que probé SCP, mi IP se había movido. Después de la actualización, volví a ingresar y los archivos se transfirieron exitosamente.

Quizás sea un pequeño punto, pero es un problema común que vale la pena señalar.

Una vez que se ordenó, la copia se realizó y la creación de la imagen en la instancia misma fue el mismo comando que local:

docker build -t churn-api.

Funcionó mucho más lento de lo que normalmente funcionaba en mi computadora portátil, y eso se debe a que t3.micro tiene menos CPU para compilar paquetes. Pero la compilación se ejecutó correctamente y eso es todo lo que me importa.

Ejecutar el contenedor en producción.

ventana acoplable ejecutar -d -p 8000:8000 abandono-api

El -d aquí importa aquí. Lo que hace es mantener el contenedor ejecutándose en segundo plano y desconectado, por lo que sigue ejecutándose incluso después de que cierro la sesión SSH. Sin él, el contenedor muere en el momento en que lo desconecto, lo que anula todo el punto.

Luego, desde mi propia computadora portátil, no desde el servidor, abrí un navegador y fui a:

http://:8000/docs

Se cargó la misma interfaz de usuario de Swagger. Luego envié al mismo cliente de prueba a través de /predict una vez más y obtuve los mismos valores que obtuve localmente y en el contenedor local. Ahora, otra persona, en cualquier lugar, sin nada más que esa dirección IP, podría llamar a mi modelo y obtener una respuesta real.

Una dirección estable: la IP elástica

Al principio quería ignorar esta fase, pero sentí que valía la pena mantenerla para que la producción de este proyecto esté lista. Verá, no se garantiza que la IP pública predeterminada de EC2 permanezca igual, especialmente si la instancia se detiene y se inicia. Adjuntar una IP elástica soluciona este problema al garantizar que la dirección IP permanezca igual y no cambie cada vez que recargo mi servidor. No tomó mucho tiempo configurarlo.

¿Qué es todavía frágil?

​Esto funciona, pero no es de nivel de producción y prefiero ser sincero sobre los problemas restantes que pasarlos por alto:

No hay HTTPS. Actualmente, todo se comunica a través de HTTP simple en el puerto 8000, lo cual es perfectamente aceptable para un proyecto de aprendizaje, pero insostenible cuando se trata de datos reales de clientes.

El punto final carece de autenticación. Cualquiera con la IP puede invocar/predecir. No existe ninguna clave API, ni limitación de velocidad, ni nada que impida el uso indebido.

Si la instancia se reinicia, el contenedor no se reiniciará automáticamente. Si AWS reinicia el hardware subyacente, mi API se desactivará silenciosamente hasta que me dé cuenta y la reinicie manualmente.

Estos problemas no son difíciles de abordar; sin embargo, aún no se han resuelto y me gustaría señalarlos para que el artículo cree una impresión engañosa de finalización.

¿Qué sigue?

El modelo ha evolucionado desde una computadora portátil, luego a una API local, luego a una API en contenedores y finalmente a un estado en el que cualquiera puede acceder a ella en Internet. Esta progresión resume el objetivo principal de esta serie y se destaca como el diferenciador más significativo de la escritura de código convencional; de hecho, la mayoría de los problemas encontrados no se debieron a errores lógicos dentro de mi código, sino que fueron resultado de desajustes ambientales o alteraciones sutiles en los recursos de infraestructura.

La siguiente etapa del viaje es la construcción e implementación de una aplicación de aprendizaje automático completamente funcional en la nube. Esto implicará implementar los mismos conocimientos adquiridos al trabajar con diferentes entornos y cambiar la infraestructura y aplicarlos a un modelo más grande que un simple servicio de un solo extremo.