Despliegue de Modelos con FastApi y Docker

Por Jose R. Zapata

Ultima actualización: 15/May/2025

Invítame a un Café

Entrenar modelos de Machine Learning es solo una parte del proceso. El siguiente paso radica en llevar esos modelos a producción, donde puedan ser utilizados por aplicaciones reales. En esta sección, exploraremos cómo desplegar modelos de Machine Learning utilizando FastAPI y Docker, dos herramientas que facilitan la creación de APIs eficientes y escalables.

FastAPI es un framework moderno y rápido para construir APIs con Python, que permite crear aplicaciones web de alto rendimiento. Por otro lado, Docker es una plataforma que permite empaquetar aplicaciones y sus dependencias en contenedores, asegurando que se ejecuten de manera consistente en cualquier entorno.

Comenzaremos creando una API de modelo de ML sencilla con FastAPI y, a continuación, empaquetaremos toda la aplicación con Docker para facilitar su implementación. Al finalizar, estarás preparado para implementar tus propios modelos de ML en entornos de producción.

Crear el ambiente de desarrollo con UV

Para comenzar, es recomendable crear un ambiente virtual para tu proyecto. Esto te permitirá gestionar las dependencias de manera aislada y evitar conflictos con otras aplicaciones. se recomienda usar uv para la gestión del proyecto.

Para mas información lo puedes consultar en: https://joserzapata.github.io/courses/ciencia-datos-en-produccion/configuracion-entorno/

Entrenar el Modelo de Machine Learning

Antes de desplegar un modelo, es necesario entrenarlo siguiendo buenas prácticas de desarrollo como:

  • Evitar datos duplicados y realizar una limpieza adecuada de los datos.
  • Realizar un análisis exploratorio de datos (EDA) para entender las características y patrones de los datos.
  • Evitar el Data Leakage
  • Usar Pipelines de Scikit-learn para unir el flujo de procesamiento de datos y el modelo.
  • Evitar el sobreajuste, realizar validación cruzada y ajustar los hiperparámetros.

A continuación, se muestra un ejemplo de cómo entrenar un modelo de clasificación utilizando el conjunto de datos del Titanic. Este ejemplo incluye la preparación de los datos, la creación de un pipeline de procesamiento y la búsqueda de hiperparámetros óptimos. Para ver el código completo, puedes consultar el siguiente link: https://joserzapata.github.io/post/ciencia-datos-proyecto-python/6-model_selection/

import pandas as pd
from joblib import dump
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import RandomForestClassifier
from sklearn.impute import SimpleImputer
from sklearn.metrics import recall_score
from sklearn.model_selection import GridSearchCV, train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder

# 💾 Load data

dataset = pd.read_csv(
    "https://www.openml.org/data/get_csv/16826755/phpMYEkMl",
    low_memory=False,
    na_values="?",
)


dataset_features = dataset[
    [
        "pclass",
        "sex",
        "age",
        "sibsp",
        "parch",
        "fare",
        "embarked",
        "survived",
    ]
]
# Convert data types

# Categorical variables

dataset[["sex", "embarked", "pclass"]] = dataset[["sex", "embarked", "pclass"]].astype("category")

dataset["pclass"] = pd.Categorical(dataset["pclass"], categories=[3, 2, 1], ordered=True)

# Numerical variables

dataset[["age", "fare"]] = dataset[["age", "fare"]].astype("float")
dataset[["sibsp", "parch"]] = dataset[["sibsp", "parch"]].astype("int8")

# ### target variables
dataset["survived"] = dataset["survived"].astype("int8")

dataset = dataset.drop_duplicates()

# 👨‍🏭 Feature Engineering

numeric_pipe = Pipeline(
    steps=[
        ("imputer", SimpleImputer(strategy="median")),
    ]
)
categorical_pipe = Pipeline(
    steps=[
        ("imputer", SimpleImputer(strategy="most_frequent")),
        ("onehot", OneHotEncoder()),
    ]
)
categorical_ord_pipe = Pipeline(
    steps=[
        ("imputer", SimpleImputer(strategy="most_frequent")),
        ("onehot", OrdinalEncoder()),
    ]
)

preprocessor = ColumnTransformer(
    transformers=[
        ("numeric", numeric_pipe, ["age", "fare", "sibsp", "parch"]),
        ("categoric", categorical_pipe, ["sex", "embarked"]),
        ("categoric ordinal", categorical_ord_pipe, ["pclass"]),
    ]
)

# Train / Test split
X_features = dataset.drop("survived", axis="columns")
Y_target = dataset["survived"]

x_train, x_test, y_train, y_test = train_test_split(
    X_features, Y_target, stratify=Y_target, test_size=0.2, random_state=42
)

# Create pipeline
data_model_pipeline = Pipeline(
    steps=[("preprocessor", preprocessor), ("model", RandomForestClassifier())]
)

# Hyperparameter tunning
hyperparameters = {
    "model__max_depth": [4, 5, 7, 9, 10],
    "model__max_features": [2, 3, 4, 5, 6, 7, 8, 9],
    "model__criterion": ["gini", "entropy"],
}

grid_search = GridSearchCV(
    data_model_pipeline,
    hyperparameters,
    cv=5,
    scoring="recall",
    n_jobs=8,
)
grid_search.fit(x_train, y_train)

best_data_model_pipeline = grid_search.best_estimator_

# evaluation
y_pred = best_data_model_pipeline.predict(x_test)

metric_result = recall_score(y_test, y_pred)
print(f"evaluation metric: {metric_result}")

Luego de tener el modelo entrenado y validado guardarlo en un archivo utilizando joblib o pickle. Por ejemplo:

dump(best_data_model_pipeline,"models/model.joblib",protocol=5)

Creacion de la API con FastAPI

Para desplegar el modelo de Machine Learning, utilizaremos FastAPI para crear una API que permita realizar predicciones. FastAPI es un framework moderno y rápido para construir APIs con Python, que facilita la creación de aplicaciones web de alto rendimiento.

Para ejecutar luego el script se puede utilizar el siguiente comando:

fastapi dev model_deploy.py

Then, you can access the API Documentation at http://127.0.0.1:8000/docs

import joblib
import pandas as pd
from fastapi import FastAPI
from pydantic import BaseModel, Field


class TitanicInput(BaseModel):
    age: int = Field(default=30, ge=0, le=100)
    pclass: int = Field(default=1, ge=1, le=3)
    sex: str = Field(default="male", pattern="^(male|female)$")
    sibsp: int = Field(default=0, ge=0, le=15)
    parch: int = Field(default=0, ge=0, le=15)
    fare: float = Field(default=30, ge=0, le=300)
    embarked: str = Field(default="C", pattern="^(C|Q|S)$")


class TitanicOutput(BaseModel):
    survived: int


app = FastAPI(
    title="Titanic Survival Prediction API",
    description="API for predicting survival on the Titanic",
    version="1.0",
)


@app.get("/")
def read_root() -> str:
    """
    Root endpoint for the Titanic Survival Prediction API.
    Returns:
        str: A simple message indicating the API is running.
    """
    return "Welcome to the Titanic Survival Prediction API! more info at /docs"


@app.post("/predict")
def predict(input: TitanicInput) -> TitanicOutput:
    """
    Predict survival on the Titanic based on input features.
    Args:
        input (TitanicInput): Input features for prediction.
    Returns:
        TitanicOutput: Prediction result indicating survival (1) or not (0).
    """
    input_dict = {
        "age": [input.age],
        "pclass": [input.pclass],
        "sex": [input.sex],
        "sibsp": [input.sibsp],
        "parch": [input.parch],
        "fare": [input.fare],
        "embarked": [input.embarked],
    }

    input_df = pd.DataFrame.from_dict(input_dict)

    # Load the model
    model_name = "model.joblib"
    model = joblib.load("models/" + model_name)

    if model is None:
        raise ValueError("Modelo no encontrado")

    # Make prediction
    prediction = model.predict(input_df)[0]
    return TitanicOutput(survived=prediction)

Empaquetar la API con Docker y UV

Para facilitar el despliegue de la API, utilizaremos Docker para empaquetar la aplicación y sus dependencias en un contenedor. Esto asegura que la aplicación se ejecute de manera consistente en cualquier entorno. Para crear un contenedor Docker, primero necesitamos crear un archivo Dockerfile que defina cómo se construirá la imagen del contenedor. A continuación, se muestra un ejemplo de Dockerfile para nuestra API de predicción del modelo realizado:

# An example using multi-stage image builds to create a final image without uv.

# First, build the application in the `/app` directory.
# See `Dockerfile` for details.
FROM ghcr.io/astral-sh/uv:python3.11-bookworm-slim AS builder
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy

# Disable Python downloads, because we want to use the system interpreter
# across both images. If using a managed Python version, it needs to be
# copied from the build image into the final image; see `standalone.Dockerfile`
# for an example.
ENV UV_PYTHON_DOWNLOADS=0

WORKDIR /app
RUN --mount=type=cache,target=/root/.cache/uv \
    --mount=type=bind,source=uv.lock,target=uv.lock \
    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    uv sync --frozen --no-install-project --no-dev
ADD . /app
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen  --no-dev


# Then, use a final image without uv
FROM python:3.11-slim-bookworm
# It is important to use the image that matches the builder, as the path to the
# Python executable must be the same, e.g., using `python:3.12-slim-bookworm`
# will fail.

# Copy the application from the builder
COPY --from=builder --chown=app:app /app /app

# Place executables in the environment at the front of the path
ENV PATH="/app/.venv/bin:$PATH"

# Set the working directory a la raíz
WORKDIR /app

# Run the FastAPI application by default
CMD ["fastapi", "run", "--host", "0.0.0.0", "--port", "5000", "model_deploy.py"]

Para construir la imagen Docker, ejecuta el siguiente comando en el directorio donde se encuentra el Dockerfile:

docker build -t fastapi-docker:v0.0.1 .

Luego, puedes ejecutar el contenedor con el siguiente comando:

docker run -p 5000:5000 fastapi-docker:v0.0.1

Una vez que el contenedor esté en ejecución, podrás acceder a la API de predicción del Titanic en http://localhost:5000/docs. Desde allí, podrás probar la API y realizar predicciones enviando solicitudes POST al endpoint /predict.

Jose R. Zapata