Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
60220cb
Add tutorial
gabrielguarisa Sep 30, 2024
3142421
Fix requirements
gabrielguarisa Sep 30, 2024
18eda1b
Simplify exceptions handler
gabrielguarisa Sep 30, 2024
dfb7c49
Remove unused schema
gabrielguarisa Sep 30, 2024
eae36a8
Remove metrics endpoint for infrastructure
gabrielguarisa Sep 30, 2024
770c14e
Add optional arg
gabrielguarisa Sep 30, 2024
c3202cc
Set uvicorn back
gabrielguarisa Sep 30, 2024
0c3f43c
Rewrite exceptions
gabrielguarisa Sep 30, 2024
618dc12
test sklearn runner
gabrielguarisa Sep 30, 2024
9b169d9
Update modelib logo image URL and remove extra newline at end of file
gabrielguarisa Sep 30, 2024
71225a9
Change version
gabrielguarisa Sep 30, 2024
17dac22
Remove old schemas
gabrielguarisa Oct 1, 2024
1e44cc0
v0.3.0a2
gabrielguarisa Oct 1, 2024
4c1b2f7
Setting a standard interface for runners
gabrielguarisa Feb 3, 2025
bdaf925
Apply lint and change version to 0.3.0a3
gabrielguarisa Feb 3, 2025
da6bc60
Fix gh action min version
gabrielguarisa Feb 3, 2025
a9bb3d0
Change dependencies spec
gabrielguarisa Feb 5, 2025
aaecc2c
Fix list with python versions
gabrielguarisa Feb 5, 2025
b844260
Bump version to 0.3.0a5 and update numpy dependency specification
gabrielguarisa Feb 6, 2025
21066dc
Bump version to 0.3.0a6 in pyproject.toml
gabrielguarisa Feb 6, 2025
1734347
Update field name handling in pydantic model creation and bump versio…
gabrielguarisa Feb 7, 2025
c847c5f
Add logging to SklearnBaseRunner for better traceability during execu…
gabrielguarisa Feb 7, 2025
a310858
Bump version to 0.3.0a8 in pyproject.toml
gabrielguarisa Feb 7, 2025
d9ba253
Refactor Pydantic schemas and enhance logging in SklearnBaseRunner fo…
gabrielguarisa Feb 7, 2025
37140d1
Bump version to 0.3.0a9 in pyproject.toml
gabrielguarisa Feb 7, 2025
493fa6f
Refactor test cases to simplify model assertions and remove unused pa…
gabrielguarisa Feb 7, 2025
8b27915
Bump version to 0.3.0 in pyproject.toml
gabrielguarisa Feb 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths-ignore:
- '**/*.md'
- '**/*.png'
- '**/*.json'
- "**/*.md"
- "**/*.png"
- "**/*.json"

jobs:
test:
Expand All @@ -16,13 +16,13 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: [3.9, 3.11]
python-version: [3.10.x, 3.11, 3.12, 3.13]
poetry-version: [1.4.2]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Run image
Expand Down Expand Up @@ -52,8 +52,8 @@ jobs:
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Run image
Expand All @@ -63,4 +63,4 @@ jobs:
- name: Install dependencies
run: make init
- name: Run style checks
run: make formatting
run: make formatting
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ jobs:
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Run image
Expand All @@ -27,4 +27,4 @@ jobs:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
run: |
poetry config pypi-token.pypi $PYPI_TOKEN
poetry publish --build
poetry publish --build
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<p align="center">
<a href="https://github.com/pier-digital/modelib"><img src="https://raw.githubusercontent.com/pier-digital/modelib/main/logo.png" alt="modelib"></a>
<a href="https://github.com/pier-digital/modelib"><img src="https://raw.githubusercontent.com/pier-digital/modelib/main/images/logo.png" alt="modelib"></a>
</p>
<p align="center">
<em>A minimalist framework for online deployment of sklearn-like models</em>
Expand All @@ -14,7 +14,6 @@

</div>


## Installation

```bash
Expand Down Expand Up @@ -46,6 +45,7 @@ request_model = [
{"name": "petal width (cm)", "dtype": "float64"},
]
```

Alternatively, you can use a pydantic model to define the request model, where the alias field is used to match the variable names with the column names in the training dataset:

```python
Expand All @@ -66,7 +66,7 @@ import modelib as ml
simple_runner = ml.SklearnRunner(
name="my simple model",
predictor=MODEL,
method_name="predict",
method_names="predict",
request_model=request_model,
)
```
Expand All @@ -75,7 +75,7 @@ Another option is to use the `SklearnPipelineRunner` class which allows you to g

```python
pipeline_runner = ml.SklearnPipelineRunner(
"Pipeline Model",
name="Pipeline Model",
predictor=MODEL,
method_names=["transform", "predict"],
request_model=request_model,
Expand Down Expand Up @@ -129,4 +129,4 @@ The response will be a JSON with the prediction:

## Contributing

If you want to contribute to the project, please read the [CONTRIBUTING.md](CONTRIBUTING.md) file for more information.
If you want to contribute to the project, please read the [CONTRIBUTING.md](CONTRIBUTING.md) file for more information.
237 changes: 237 additions & 0 deletions Tutorial.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
# Disponibilizando modelos de machine learning em APIs REST com modelib

O uso de modelos de Machine Learning em ambientes produtivos é um motivo de atenção pois une conhecimentos das áreas de Ciência de Dados e Engenharia de Software. Conhecimentos esses que estão, muitas vezes, divididos em diferentes áreas das empresas. O Engenheiro de Machine Learning é o profissional que está nessa intersecção de conhecimentos, sendo o responsável por toda a infraestrutura que disponibiliza o trabalho das cientistas de dados (o modelo) para ser consumido pelos sistemas desenvolvidos pelas equipes de desenvolvimento.

Neste artigo, apresentaremos uma solução para implantar modelos de machine learning numa API REST utilizando a biblioteca [modelib](https://github.com/pier-digital/modelib) que, de forma simples, suporta realizar as predições em tempo real e sob demanda, com uma interface que disponibiliza a inteligência dos modelos para outros serviços através de chamadas HTTP.

## Definindo o modelo

> Se você quiser fazer uma torta de maçã a partir do zero, você deve primeiro inventar o Universo. - Carl Sagan

Antes de pensarmos em como disponibilizar um modelo, nós precisaremos (obviamente) de um modelo. Para facilitar o entendimento, utilizaremos o famoso [Iris Dataset](https://scikit-learn.org/stable/auto_examples/datasets/plot_iris_dataset.html).

Abaixo, definimos a função `create_model` como responsável por retornar um modelo treinado com parte do conjunto de dados mencionado.

```python
def create_model():
from sklearn.datasets import load_iris
from sklearn.ensemble import RandomForestClassifier
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split

X, y = load_iris(return_X_y=True, as_frame=True)

X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)

model = Pipeline(
[
("scaler", StandardScaler()),
("clf", RandomForestClassifier(random_state=42)),
]
).set_output(transform="pandas")

model.fit(X_train, y_train)

return model
```

Com esse modelo em mãos, podemos nos preocupar em como disponiblizá-lo.

## Escolhendo como disponibilizar o modelo

> Você é livre para fazer suas escolhas, mas é prisioneiro das consequências. - Pablo Neruda

Com a crescente demanda do uso de inteligência de modelos de ML em contextos empresariais, diversas soluções foram criadas para implementar e disponibilizar as predições de tais modelos.

Dentre as soluções mais comuns, destacam-se as seguintes ferramentas e plataformas open-source:

- [BentoML](https://www.bentoml.com/): Uma plataforma para servir, gerenciar e implantar modelos de machine learning;
- [MLflow](https://mlflow.org/): Uma plataforma para gerenciar o ciclo de vida de modelos de machine learning;
- [Seldon](https://www.seldon.io/): Uma plataforma para implantar e gerenciar modelos de machine learning em escala;
- [Kubeflow](https://www.kubeflow.org/): Uma plataforma para implantar, gerenciar e escalar modelos de machine learning em Kubernetes;
- [FastAPI](https://fastapi.tiangolo.com/): Um framework (de alto desempenho) para construir APIs em Python;

Num mar com tantas escolhas, que ainda incluem soluções privadas e nativas de clouds específicas, decidir qual tecnologia sua empresa adotará pode desencadear numa série de custos e limitações indesejadas. Um outro ponto importante é que, em vários casos, a escolha da tecnologia de deploy de modelos de ML pode entrar em conflito com escolhas de infraestrutura já existentes na empresa.

## Apresentando a biblioteca modelib

> A simplicidade é a sofisticação final. - Leonardo da Vinci

A biblioteca funciona como uma extensão do [FastAPI](https://fastapi.tiangolo.com/), que é um framework (de alto desempenho) para construir APIs em Python.

Um ponto importante é que o deploy de modelos com o FastAPI já foi abordado em diversos outros artigos (deixo [aqui](https://engineering.rappi.com/using-fastapi-to-deploy-machine-learning-models-cd5ed7219ea) um como exemplo). Entretanto, o principal objetivo da biblioteca é oferecer uma forma padronizada para a chamada de modelos através de uma interface simples e comum para validação dos inputs e tratamento dos outputs.

Desta forma, não estamos preocupados sobre as escolhas de serviço de nuvem (AWS, Azure, GCP, etc), ferramenta de ambiente virtual (virtualenv, poetry, pipenv, etc), serviço de containerização (Docker, Podman, etc) e até mesmo sobre o pipeline de deploy dos modelos.

Abaixo é apresentado o código necessário para criar um endpoint de predição numa API do FastAPI.

```python
import modelib as ml
import pydantic

class InputData(pydantic.BaseModel):
sepal_length: float = pydantic.Field(alias="sepal length (cm)")
sepal_width: float = pydantic.Field(alias="sepal width (cm)")
petal_length: float = pydantic.Field(alias="petal length (cm)")
petal_width: float = pydantic.Field(alias="petal width (cm)")

simple_runner = ml.SklearnRunner(
name="my simple model",
predictor=create_model(),
method_names="predict",
request_model=InputData,
)

app = ml.init_app(runners=[simple_runner])
```

Note que é necessário criar um `Runner` a partir do modelo treinado. Além disso, precisamos definir:

- `name`: nome que será utilizado na definição do path do endpoint gerado;
- `method_names`: nome do método do preditor que será utilizado (`predict`, `transform`, etc);
- `request_model`: modelo que define os inputs esperados pelo modelo.

### Definindo o formato dos inputs

Para definir o `request_model` podemos definir uma classe que define o schema esperado pelo modelo para realizar a predição. Um ponto importante é que o nome dos campos deve ser igual ao definido durante o treinamento. Caso o nome da feature contenha espações ou caracteres não suportados para nomes de variáveis no python, utilize o campo alias, conforme demonstrado abaixo:

```python
class InputData(pydantic.BaseModel):
sepal_length: float = pydantic.Field(alias="sepal length (cm)")
sepal_width: float = pydantic.Field(alias="sepal width (cm)")
petal_length: float = pydantic.Field(alias="petal length (cm)")
petal_width: float = pydantic.Field(alias="petal width (cm)")
```

Existe uma segunda forma de definir o schema como uma lista de dicionários. O uso dessa segunda abordagem pode ser interessante para fluxos de deploy onde tais informações são definidas em arquivos de configuração.

```python
features_metadata = [
{"name": "sepal length (cm)", "dtype": "float64"},
{"name": "sepal width (cm)", "dtype": "float64"},
{"name": "petal length (cm)", "dtype": "float64"},
{"name": "petal width (cm)", "dtype": "float64"},
]

simple_runner = ml.SklearnRunner(
...,
request_model=features_metadata,
)
```

Onde é possível definir os campos:

- `name`: nome do campo;
- `dtype`: tipo do campo, onde são aceitos os valores `float64`, `int64`, `object`, `bool` e `datetime64`;
- `optional`: booleano indicando se o campo é opcional ou não;
- `default`: valor padrão do campo;

### Usando diferentes runners

Por padrão, existem dois tipos de runners já implementados na biblioteca, a saber:

- `SklearnRunner`: Executa qualquer modelo que segue a interface de um [BaseEstimator do sklearn](https://scikit-learn.org/stable/modules/generated/sklearn.base.BaseEstimator.html);
- `SklearnPipelineRunner`: Similar ao anterior, mas específico para a execução de [Pipelines](https://scikit-learn.org/stable/modules/generated/sklearn.pipeline.Pipeline.html);

No exemplo acima, utilizamos um runner do tipo `SklearnRunner` que, além do `request_model` definido, também informamos os valores dos parâmetros a seguir:

- `name`: Nome do runner;
- `predictor`: Modelo treinado;
- `method_name`: Nome do método do modelo que será utilizado para realizar a predição;

Caso o modelo seja um pipeline, podemos utilizar o runner `SklearnPipelineRunner` que ao invés de receber apenas um valor no campo `method_name`, recebe uma lista de strings com os nomes dos métodos que serão executados em sequência no campo `method_names`.

```python
pipeline_runner = ml.SklearnPipelineRunner(
"Pipeline Model",
predictor=create_model(),
method_names=["transform", "predict"],
request_model=request_model,
)
```

A vantagem de utilizar um pipeline `SklearnPipelineRunner` é que recebemos a predição do modelo juntamente com o resultado das transformações realizadas no input em cada etapa do pipeline.

Além disso, é possível definir runners customizados, bastando para isso criar uma classe que herda de `modelib.BaseRunner` e implementar o método `get_runner_func` que deve retornar uma função que recebe um input e retorna um output.

```python
class CustomRunner(ml.BaseRunner):
def get_runner_func(self):
def runner_func(input_data):
# Implementação do runner
return output_data
return runner_func
```

### Inicializando a aplicação

Por fim, para inicializar a aplicação, basta chamar a função `init_app` passando uma lista de runners como argumento.

```python
app = ml.init_app(runners=[simple_runner, pipeline_runner])
```

Caso você queira utilizar uma aplicação já existente, basta chamar a função `init_app` passando a aplicação como argumento.

```python
import fastapi

app = fastapi.FastAPI()

app = ml.init_app(app=app, runners=[simple_runner, pipeline_runner])
```

Após definir os runners e inicializar a aplicação, basta subir a aplicação utilizando o comando `uvicorn` ou `gunicorn` e a aplicação estará pronta para receber requisições.

```bash
uvicorn <filename>:app --reload
```

### Realizando predições

Após subir a aplicação, a mesma estará pronta para receber requisições. Para realizar uma predição, basta enviar uma requisição do tipo POST para o endpoint do runner desejado com um payload contendo os inputs esperados pelo modelo.

```json
{
"sepal length (cm)": 5.1,
"sepal width (cm)": 3.5,
"petal length (cm)": 1.4,
"petal width (cm)": 0.2
}
```

Que para o exemplo acima, o endpoint gerado será `/my-simple-model` e a resposta será algo como:

```json
{
"result": 0
}
```

Já para o runner do tipo `SklearnPipelineRunner`, a resposta será algo como:

```json
{
"result": 0,
"steps": {
"scaler": [
{
"sepal length (cm)": -7.081194586015879,
"sepal width (cm)": -6.845571885453045,
"petal length (cm)": -2.135591504400147,
"petal width (cm)": -1.5795728805764124
}
],
"clf": [0]
}
}
```

## Conclusão

Neste artigo, apresentamos a biblioteca modelib que oferece uma forma padronizada para a chamada de modelos de machine learning através de uma interface simples e comum para validação dos inputs e tratamento dos outputs. A biblioteca é uma extensão do [FastAPI](https://fastapi.tiangolo.com/), que é um framework (de alto desempenho) para construir APIs em Python.

O principal objetivo da biblioteca é fornecer uma forma simples que se integre tanto em infraestruturas já existentes como em novos projetos, permitindo que cientistas de dados e engenheiros de machine learning possam disponibilizar seus modelos de forma rápida e padronizada.
4 changes: 2 additions & 2 deletions example.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@ class InputData(pydantic.BaseModel):
simple_runner = ml.SklearnRunner(
name="my simple model",
predictor=MODEL,
method_name="predict",
method_names="predict",
request_model=InputData, # OR request_model=features_metadata
)

pipeline_runner = ml.SklearnPipelineRunner(
"Pipeline Model",
name="Pipeline Model",
predictor=MODEL,
method_names=["transform", "predict"],
request_model=InputData,
Expand Down
File renamed without changes
11 changes: 10 additions & 1 deletion modelib/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
from modelib.server.app import init_app
from modelib.runners.base import BaseRunner
from modelib.runners.sklearn import SklearnRunner, SklearnPipelineRunner
from modelib.core import exceptions, schemas, endpoint_factory

__all__ = ["init_app", "BaseRunner", "SklearnRunner", "SklearnPipelineRunner"]
__all__ = [
"init_app",
"BaseRunner",
"SklearnRunner",
"SklearnPipelineRunner",
"exceptions",
"schemas",
"endpoint_factory",
]
Loading