|
| 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. |
0 commit comments