Skip to content

Commit 7e04c64

Browse files
Merge pull request #5 from pier-digital/f/fix-requirements
Project refactor and fix requirements
2 parents 6f0c15e + 8b27915 commit 7e04c64

20 files changed

+1093
-1168
lines changed

.github/workflows/checks.yml

+9-9
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ on:
33
pull_request:
44
types: [opened, synchronize, reopened, ready_for_review]
55
paths-ignore:
6-
- '**/*.md'
7-
- '**/*.png'
8-
- '**/*.json'
6+
- "**/*.md"
7+
- "**/*.png"
8+
- "**/*.json"
99

1010
jobs:
1111
test:
@@ -16,13 +16,13 @@ jobs:
1616
strategy:
1717
fail-fast: false
1818
matrix:
19-
python-version: [3.9, 3.11]
19+
python-version: [3.10.x, 3.11, 3.12, 3.13]
2020
poetry-version: [1.4.2]
2121
os: [ubuntu-latest]
2222
runs-on: ${{ matrix.os }}
2323
steps:
24-
- uses: actions/checkout@v2
25-
- uses: actions/setup-python@v2
24+
- uses: actions/checkout@v4
25+
- uses: actions/setup-python@v5
2626
with:
2727
python-version: ${{ matrix.python-version }}
2828
- name: Run image
@@ -52,8 +52,8 @@ jobs:
5252
os: [ubuntu-latest]
5353
runs-on: ${{ matrix.os }}
5454
steps:
55-
- uses: actions/checkout@v2
56-
- uses: actions/setup-python@v2
55+
- uses: actions/checkout@v4
56+
- uses: actions/setup-python@v5
5757
with:
5858
python-version: ${{ matrix.python-version }}
5959
- name: Run image
@@ -63,4 +63,4 @@ jobs:
6363
- name: Install dependencies
6464
run: make init
6565
- name: Run style checks
66-
run: make formatting
66+
run: make formatting

.github/workflows/release.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ jobs:
1414
os: [ubuntu-latest]
1515
runs-on: ${{ matrix.os }}
1616
steps:
17-
- uses: actions/checkout@v2
18-
- uses: actions/setup-python@v2
17+
- uses: actions/checkout@v4
18+
- uses: actions/setup-python@v5
1919
with:
2020
python-version: ${{ matrix.python-version }}
2121
- name: Run image
@@ -27,4 +27,4 @@ jobs:
2727
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
2828
run: |
2929
poetry config pypi-token.pypi $PYPI_TOKEN
30-
poetry publish --build
30+
poetry publish --build

README.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<p align="center">
2-
<a href="https://github.com/pier-digital/modelib"><img src="https://raw.githubusercontent.com/pier-digital/modelib/main/logo.png" alt="modelib"></a>
2+
<a href="https://github.com/pier-digital/modelib"><img src="https://raw.githubusercontent.com/pier-digital/modelib/main/images/logo.png" alt="modelib"></a>
33
</p>
44
<p align="center">
55
<em>A minimalist framework for online deployment of sklearn-like models</em>
@@ -14,7 +14,6 @@
1414

1515
</div>
1616

17-
1817
## Installation
1918

2019
```bash
@@ -46,6 +45,7 @@ request_model = [
4645
{"name": "petal width (cm)", "dtype": "float64"},
4746
]
4847
```
48+
4949
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:
5050

5151
```python
@@ -66,7 +66,7 @@ import modelib as ml
6666
simple_runner = ml.SklearnRunner(
6767
name="my simple model",
6868
predictor=MODEL,
69-
method_name="predict",
69+
method_names="predict",
7070
request_model=request_model,
7171
)
7272
```
@@ -75,7 +75,7 @@ Another option is to use the `SklearnPipelineRunner` class which allows you to g
7575

7676
```python
7777
pipeline_runner = ml.SklearnPipelineRunner(
78-
"Pipeline Model",
78+
name="Pipeline Model",
7979
predictor=MODEL,
8080
method_names=["transform", "predict"],
8181
request_model=request_model,
@@ -129,4 +129,4 @@ The response will be a JSON with the prediction:
129129

130130
## Contributing
131131

132-
If you want to contribute to the project, please read the [CONTRIBUTING.md](CONTRIBUTING.md) file for more information.
132+
If you want to contribute to the project, please read the [CONTRIBUTING.md](CONTRIBUTING.md) file for more information.

Tutorial.md

+237
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
# Disponibilizando modelos de machine learning em APIs REST com modelib
2+
3+
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.
4+
5+
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.
6+
7+
## Definindo o modelo
8+
9+
> Se você quiser fazer uma torta de maçã a partir do zero, você deve primeiro inventar o Universo. - Carl Sagan
10+
11+
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).
12+
13+
Abaixo, definimos a função `create_model` como responsável por retornar um modelo treinado com parte do conjunto de dados mencionado.
14+
15+
```python
16+
def create_model():
17+
from sklearn.datasets import load_iris
18+
from sklearn.ensemble import RandomForestClassifier
19+
from sklearn.pipeline import Pipeline
20+
from sklearn.preprocessing import StandardScaler
21+
from sklearn.model_selection import train_test_split
22+
23+
X, y = load_iris(return_X_y=True, as_frame=True)
24+
25+
X_train, X_test, y_train, y_test = train_test_split(
26+
X, y, test_size=0.2, random_state=42
27+
)
28+
29+
model = Pipeline(
30+
[
31+
("scaler", StandardScaler()),
32+
("clf", RandomForestClassifier(random_state=42)),
33+
]
34+
).set_output(transform="pandas")
35+
36+
model.fit(X_train, y_train)
37+
38+
return model
39+
```
40+
41+
Com esse modelo em mãos, podemos nos preocupar em como disponiblizá-lo.
42+
43+
## Escolhendo como disponibilizar o modelo
44+
45+
> Você é livre para fazer suas escolhas, mas é prisioneiro das consequências. - Pablo Neruda
46+
47+
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.
48+
49+
Dentre as soluções mais comuns, destacam-se as seguintes ferramentas e plataformas open-source:
50+
51+
- [BentoML](https://www.bentoml.com/): Uma plataforma para servir, gerenciar e implantar modelos de machine learning;
52+
- [MLflow](https://mlflow.org/): Uma plataforma para gerenciar o ciclo de vida de modelos de machine learning;
53+
- [Seldon](https://www.seldon.io/): Uma plataforma para implantar e gerenciar modelos de machine learning em escala;
54+
- [Kubeflow](https://www.kubeflow.org/): Uma plataforma para implantar, gerenciar e escalar modelos de machine learning em Kubernetes;
55+
- [FastAPI](https://fastapi.tiangolo.com/): Um framework (de alto desempenho) para construir APIs em Python;
56+
57+
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.
58+
59+
## Apresentando a biblioteca modelib
60+
61+
> A simplicidade é a sofisticação final. - Leonardo da Vinci
62+
63+
A biblioteca funciona como uma extensão do [FastAPI](https://fastapi.tiangolo.com/), que é um framework (de alto desempenho) para construir APIs em Python.
64+
65+
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.
66+
67+
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.
68+
69+
Abaixo é apresentado o código necessário para criar um endpoint de predição numa API do FastAPI.
70+
71+
```python
72+
import modelib as ml
73+
import pydantic
74+
75+
class InputData(pydantic.BaseModel):
76+
sepal_length: float = pydantic.Field(alias="sepal length (cm)")
77+
sepal_width: float = pydantic.Field(alias="sepal width (cm)")
78+
petal_length: float = pydantic.Field(alias="petal length (cm)")
79+
petal_width: float = pydantic.Field(alias="petal width (cm)")
80+
81+
simple_runner = ml.SklearnRunner(
82+
name="my simple model",
83+
predictor=create_model(),
84+
method_names="predict",
85+
request_model=InputData,
86+
)
87+
88+
app = ml.init_app(runners=[simple_runner])
89+
```
90+
91+
Note que é necessário criar um `Runner` a partir do modelo treinado. Além disso, precisamos definir:
92+
93+
- `name`: nome que será utilizado na definição do path do endpoint gerado;
94+
- `method_names`: nome do método do preditor que será utilizado (`predict`, `transform`, etc);
95+
- `request_model`: modelo que define os inputs esperados pelo modelo.
96+
97+
### Definindo o formato dos inputs
98+
99+
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:
100+
101+
```python
102+
class InputData(pydantic.BaseModel):
103+
sepal_length: float = pydantic.Field(alias="sepal length (cm)")
104+
sepal_width: float = pydantic.Field(alias="sepal width (cm)")
105+
petal_length: float = pydantic.Field(alias="petal length (cm)")
106+
petal_width: float = pydantic.Field(alias="petal width (cm)")
107+
```
108+
109+
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.
110+
111+
```python
112+
features_metadata = [
113+
{"name": "sepal length (cm)", "dtype": "float64"},
114+
{"name": "sepal width (cm)", "dtype": "float64"},
115+
{"name": "petal length (cm)", "dtype": "float64"},
116+
{"name": "petal width (cm)", "dtype": "float64"},
117+
]
118+
119+
simple_runner = ml.SklearnRunner(
120+
...,
121+
request_model=features_metadata,
122+
)
123+
```
124+
125+
Onde é possível definir os campos:
126+
127+
- `name`: nome do campo;
128+
- `dtype`: tipo do campo, onde são aceitos os valores `float64`, `int64`, `object`, `bool` e `datetime64`;
129+
- `optional`: booleano indicando se o campo é opcional ou não;
130+
- `default`: valor padrão do campo;
131+
132+
### Usando diferentes runners
133+
134+
Por padrão, existem dois tipos de runners já implementados na biblioteca, a saber:
135+
136+
- `SklearnRunner`: Executa qualquer modelo que segue a interface de um [BaseEstimator do sklearn](https://scikit-learn.org/stable/modules/generated/sklearn.base.BaseEstimator.html);
137+
- `SklearnPipelineRunner`: Similar ao anterior, mas específico para a execução de [Pipelines](https://scikit-learn.org/stable/modules/generated/sklearn.pipeline.Pipeline.html);
138+
139+
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:
140+
141+
- `name`: Nome do runner;
142+
- `predictor`: Modelo treinado;
143+
- `method_name`: Nome do método do modelo que será utilizado para realizar a predição;
144+
145+
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`.
146+
147+
```python
148+
pipeline_runner = ml.SklearnPipelineRunner(
149+
"Pipeline Model",
150+
predictor=create_model(),
151+
method_names=["transform", "predict"],
152+
request_model=request_model,
153+
)
154+
```
155+
156+
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.
157+
158+
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.
159+
160+
```python
161+
class CustomRunner(ml.BaseRunner):
162+
def get_runner_func(self):
163+
def runner_func(input_data):
164+
# Implementação do runner
165+
return output_data
166+
return runner_func
167+
```
168+
169+
### Inicializando a aplicação
170+
171+
Por fim, para inicializar a aplicação, basta chamar a função `init_app` passando uma lista de runners como argumento.
172+
173+
```python
174+
app = ml.init_app(runners=[simple_runner, pipeline_runner])
175+
```
176+
177+
Caso você queira utilizar uma aplicação já existente, basta chamar a função `init_app` passando a aplicação como argumento.
178+
179+
```python
180+
import fastapi
181+
182+
app = fastapi.FastAPI()
183+
184+
app = ml.init_app(app=app, runners=[simple_runner, pipeline_runner])
185+
```
186+
187+
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.
188+
189+
```bash
190+
uvicorn <filename>:app --reload
191+
```
192+
193+
### Realizando predições
194+
195+
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.
196+
197+
```json
198+
{
199+
"sepal length (cm)": 5.1,
200+
"sepal width (cm)": 3.5,
201+
"petal length (cm)": 1.4,
202+
"petal width (cm)": 0.2
203+
}
204+
```
205+
206+
Que para o exemplo acima, o endpoint gerado será `/my-simple-model` e a resposta será algo como:
207+
208+
```json
209+
{
210+
"result": 0
211+
}
212+
```
213+
214+
Já para o runner do tipo `SklearnPipelineRunner`, a resposta será algo como:
215+
216+
```json
217+
{
218+
"result": 0,
219+
"steps": {
220+
"scaler": [
221+
{
222+
"sepal length (cm)": -7.081194586015879,
223+
"sepal width (cm)": -6.845571885453045,
224+
"petal length (cm)": -2.135591504400147,
225+
"petal width (cm)": -1.5795728805764124
226+
}
227+
],
228+
"clf": [0]
229+
}
230+
}
231+
```
232+
233+
## Conclusão
234+
235+
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.
236+
237+
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.

example.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,12 @@ class InputData(pydantic.BaseModel):
4646
simple_runner = ml.SklearnRunner(
4747
name="my simple model",
4848
predictor=MODEL,
49-
method_name="predict",
49+
method_names="predict",
5050
request_model=InputData, # OR request_model=features_metadata
5151
)
5252

5353
pipeline_runner = ml.SklearnPipelineRunner(
54-
"Pipeline Model",
54+
name="Pipeline Model",
5555
predictor=MODEL,
5656
method_names=["transform", "predict"],
5757
request_model=InputData,

logo.png images/logo.png

File renamed without changes.

modelib/__init__.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
from modelib.server.app import init_app
22
from modelib.runners.base import BaseRunner
33
from modelib.runners.sklearn import SklearnRunner, SklearnPipelineRunner
4+
from modelib.core import exceptions, schemas, endpoint_factory
45

5-
__all__ = ["init_app", "BaseRunner", "SklearnRunner", "SklearnPipelineRunner"]
6+
__all__ = [
7+
"init_app",
8+
"BaseRunner",
9+
"SklearnRunner",
10+
"SklearnPipelineRunner",
11+
"exceptions",
12+
"schemas",
13+
"endpoint_factory",
14+
]

0 commit comments

Comments
 (0)