Docker, deploy de modelos e Ciência dos Dados — parte 1/3: Servindo nosso modelo com Python e Flask

O passo inicial para colocar nosso modelo em produção

Kattson Bastos
10 min readJan 10, 2022

Série de postagens:

0) Docker, deploy de modelos e Ciência dos Dados — parte 0/3: do ciclo de vida de desenvolvimento de software ao de Engenharia de Machine Learning (link)
1) Docker, deploy de modelos e Ciência dos Dados — parte 1/3: Servindo nosso modelo com Python e Flask (você está aqui)
2) Docker, deploy de modelos e Ciência dos Dados — parte 2/3: O que são containers e como ‘dockerizar’ nossa Flask API (você está aqui)
3) Docker, deploy de modelos e Ciência dos Dados — parte 3/3: deploy rápido e fácil do nosso container no Heroku! (ainda a produzir)

Sumário:

0) Introdução
1) APIs e Flask: Um resumo
2) Preparando o ambiente
3) Construindo e salvando um modelo simples de classificação
4) Servindo nosso modelo com a API em Flask
5) Testando requisições
6) Conclusão

0) Introdução e Link do Repositório

Nessa postagem iremos construir um modelo simples para diagnóstico de doenças cardiovasculares e implementaremos uma API com Flask para realizar as requisições e receber o resultado da classificação.

Contudo, antes disso, falaremos, resumidamente na próxima seção, sobre o que são APIs e indicaremos outros materiais que podem ser de muita valia para o entendimento desses conceitos.

Link do Repositório no Github: https://github.com/KattsonBastos/docker-model-deployment

1) APIs e Flask: Um resumo

Quando falamos em APIs, estamos falando de um conjunto de padrões, rotinas e funções que permite uma interação (“conversa”) entre sistemas e usuários. Ou seja, dada uma aplicação X, por intermédio de uma API específica, outras aplicações Y e Z poderão utilizar funcionalidades ou obter respostas de X sem que elas conheçam detalhes desta.

Um exemplo clássico, e descrito neste post, é o seguinte:

Imagine que você está em um restaurante, conferindo o cardápio para fazer seu pedido. A cozinha é a parte do “sistema” responsável por preparar seu pedido. Para seu pedido chegar até a cozinha e, depois, o prato chegar até sua mesa, é necessário contar com um elo de ligação essencial. Esse elo de ligação é o garçom — ou a API. O garçom (a API) é o mensageiro que registra seu pedido e informa à cozinha (o sistema) o que fazer. Depois, ele traz o prato (ou a resposta à sua solicitação) até você.

Nesse caso, poderíamos pensar que há uma maneira correta de nós, enquanto usuários, realizarmos a interação com o garçom e assim iniciar o processo: levantar a mão para chamá-lo, solicitar o cardápio, caso não tenha sido oferecido inicialmente, e verbalizar o pedido, dentre aqueles disponíveis, para que seja registrado e preparado. Da mesma forma, a API segue um padrão de desenvolvimento, com restrições e estilos de arquitetura.

Trazendo para o mundo da Ciência dos Dados, por intermédio de uma API, podemos permitir que um usuário envie dados para a predição de um modelo e receba o retorno sem que seja necessário ele saber o que é um model.predict().

Vamos ver um exemplo prático antes de iniciar a preparação do nosso ambiente de desenvolvimento.

Suponhamos que treinamos um modelo para diagnosticar se um paciente possui ou não doença cardiovascular (aquele mesmo que apresentamos na postagem anterior), implementamos uma API que consome esse modelo e a deixamos executando na máquina local da nossa empresa (vamos pensar simples). Paralelamente, desenvolvemos uma página extra no sistema que a empresa cliente já possui contendo um formulário para que um médico especialista preencha com os dados do paciente e envie para nossa API para que o modelo possa fazer o que ele faz de melhor. A figura a seguir ilustra esse processo:

O processo resumido seria assim:

  • O médico preenche o formulário, clica em “enviar”.
  • A plataforma deles traduz aquele formulário para um Json (é um formato chave-valor semelhante ao dicionário no Python) e envia para um endpoint da API, algo como https://123.123.123.123:1234/classificacao-doenca-cardiovascular
    uma nota: esse Json deve conter os mesmos campos que utilizamos pra treinar o modelo, ou seja, deve corresponder às colunas (features) que utilizamos, veremos isso na prática logo mais.
  • A API pega os dados, faz as mesmas transformações e processos que usamos antes do treinamento, realiza a predição, retorna a mensagem para o sistema que, por fim, mostra para o médico (vamos nos abster aqui daquela parte de salvar no banco de dados e etc.).

Para fazer tudo isso, utilizaremos o micro framework Flask, que é utilizado para a criação de aplicações web de forma fácil. Flask possibilita a criação da aplicação de forma simples e escalável e, embora seja simples, nele nós podemos adicionar extensões para que possamos usar outras funcionalidades.

Agora que já sabemos de forma geral que são APIs e qual intuição de seu uso em Ciência dos Dados, vamos preparar o ambiente e desenvolver a nossa.

2) Preparando o ambiente

Primeiramente, precisamos ter nosso ambiente virtual criado, seja em conda, virtualenv ou algum outro. Se você não sabe o é um ambiente virtual, recomendo esta leitura. Com isso, temos que instalar as bibliotecas utilizando o requirements.txt que disponibilizo no repositório no Github. Se você é novo nisso, basta abrir o terminal na pasta em que o arquivo estiver (e com seu ambiente virtual iniciado) e executar o seguinte comando:

pip install -r requirements.txt

Os arquivos que utilizaremos a seguir estão na pastapart1-building-api , que por sua vez possui a seguinte estrutura:

├── API
│ ├── app.py
│ ├── config.py
│ ├── resources
│ │ ├── classification.py
│ └── tools
│ ├── file_tools.py
│ ├── prediction_tools.py
├── data
│ ├── cardio_train.csv
│ └── cardio_train_sampled.csv
├── models
│ └── model.pkl
├── parameters
│ ├── age_scaler.pkl
│ ├── ap_hi_scaler.pkl
│ ├── ap_lo_scaler.pkl
│ └── weight_scaler.pkl
├── preparing_data_and_modelling
│ ├── modelling.py
│ └── splitting_dataset.py
└── requirements.txt

No geral, temos cinco pastas principais:

  • API: contem os códigos da API que desenvolveremos
  • data: contem os dados originais e o subset que faremos, ambos em .csv
  • models: contem o modelo treinado em formato .pkl
  • parameters: contém os scalers das features também em .pkl
  • preparing_data_and_modelling: contem um arquivo splitting_dataset.py, que salva um subset dos dados originais; e o arquivo modelling.py, que treina o algoritmo e salva o modelo.

Visto que utilizaremos uma amostra dos dados originais, precisamos realizar o split. Para isso, o seguinte código apresenta esse processo, pegando os dados da pasta data , separando cinco mil observações de cada classe da variável resposta, concatenando e salvando-os de volta na pasta data . Além disso, para fins de simplicidade, filtramos apenas algumas colunas nessa amostragem.

Com isso, podemos iniciar o treinamento do algoritmo.

3) Construindo e salvando um modelo simples de classificação

Com os dados amostrados, podemos usá-los no treinamento a seguir. Esse processo será bem simples, pois precisamos apenas do modelo treinado e dos scalers, logo, não realizaremos nenhum processo de fine tuning ou uma seleção de algoritmos mais apropriada.

O snippet de código a seguir apresenta o script modelling.py . Nele, primeiramente, carregamos aquele dataset amostrado, dividimos a coluna age por 365 para obter os valores em anos e criamos o objeto X com as features e o objeto y com a target. Logo em seguida, separamos uma parcela de teste apenas para termos uma ideia geral do desempenho do modelo, embora isso não seja escopo aqui.

Após isso, instanciamos os scalers para cada feature e transformamos os dados.

Após transformar os dados de teste, treinamos um classificador LightGBM e salvamos o modelo em formato .pkl .

Obs.: Temos que ter em mente que esse processo de divisão da age por 365 e o processo de scaling das variáveis teremos que replicar na API para o dado novo que estivermos recebendo.

Vamos lá, então?

4) Servindo nosso modelo com a API em Flask

Vamos, primeiramente, rever a estrutura de arquivos do diretório da API. Como poderemos ver em seguida, tudo isso poderia estar em um mesmo arquivo, no app.py , porém, para fins de organização (e é o recomendado), resolvi separá-los.

├── app.py
├── config.py
├── resources
│ └── classification.py
└── tools
├── file_tools.py
└── prediction_tools.py

Peço que crie em sua máquina local esses arquivos vazios para irmos desenvolvendo ao longo dessa postagem.

Adentraremos em cada um deles ao mesmo tempo em que apresentamos o processo como um todo. O arquivo central aqui é o app.py apresentado no snippet a seguir. Esse app é o script que ficará executando, sendo a base do processo.

Podemos notar que o arquivo é bem simples (e isso mesmo, o Flask proporciona uma estrutura bem simplificada para usarmos). Na linha 5 criamos a instancia da aplicação Flask passando o __name__ , que, em python, representa o nome do módulo (um modulo em python é qualquer arquivo .py) que estamos executando. Nesse caso, como estamos executando o módulo em si como um programa (ou seja, chamando ele via terminal com um python app.py, por exemplo), ele automaticamente pegará o nome __main__ (para mais sobre essa parte, vide essa postagem).

Criamos uma classe Api, na linha 7, passando o app que acabamos de criar, de forma a criar o principal entry point da aplicação.

A linha 9 adiciona o recurso Classification (veremos sobre ele jajá) e associamos ao endpoint cvd-classification. Ou seja, a partir dai, podemos fazer requisições com o método POST (veja mais sobre os métodos http) para http://localhost:8000/cvd-classification, passando um json contendo dados para aquelas 5 features que treinamos o modelo: age, weight , ap_hi , ap_lo echolesterol .

A linha 11 faz a verificação se estamos executando esse script por si só (e não importado em outro arquivo .py). Se assim o for, permitirá a execução da linha 12 em que a aplicação Flask é executada. Essa linha em específico:

  • debug: nos permite localizar e identificar possíveis erros. Com ele configurado para True , os erros serão mostrados no console. Em produção, não é recomendado o uso;
  • host: para indicar o servidor da aplicação. Nesse caso, usaremos o ambiente local (localhost);
  • port: porta pelo qual nossa aplicação poderá ser acessada. Por intermédio dela, por exemplo, poderíamos acessar nossa API por uma outra máquina, caso essa porta estiver liberada para acesso externo em nosso roteador.

Há outras opções para se usar no .run() , mas vamos nos ater a essas acima por enquanto.

Vamos agora partir para o arquivo resources/classification.py . Nele, usamos aquele PredictionTools , que falaremos em seguida.

Basicamente, criamos uma classe que recebe o objeto Resource. Um Resource é um objeto que contém um conjunto de métodos e, ai, podemos utilizar aqueles verbos HTTP que citei há pouco (GET, POST, …). Para isso, iniciamos na linha 6 a criação de um método POST que receberá os dados da requisição como um json e instancia o tools da classe PredictionTools . Essa classe, como veremos, possui dois métodos: um de preparação dos dados e outro que realiza a classificação.

Antes de vermos esse tal de prediction_tools.py ai, vou apresentar rapidamente o file_tools.py .

Costumo, nos projetos, deixar em file_tools.py todas funções referentes à criação de diretórios temporários durante uma execução, download e carregamento de arquivos e etc. Nesse nosso caso, temos apenas uma função que lê arquivos no formato .pkl , que será utilizada no carregamento do modelo e dos scalers.

No prediction_tools.py é onde a mágica acontece. No __init__ já puxamos o modelo e os scalers. Adicionalmente, temos o results que é basicamente um dicionário (como veremos no arquivo config.py ) contendo a descrição da classificação.

No primeiro método, data_preparation, aplicamos aquelas etapas que tivemos que fazer no pré treinamento do algoritmo: a divisão por 365 da feature age e o scaling. Nesse processo, recebemos os dados como um dicionário, convertemos para um pandas DataFrame, e retornamos esse objeto com os dados transformados.

Como vimos no classification.py , imediatamente passamos esse DataFrame para o método de classificação. Usamos aquele result para formatar o retorno do nosso método, sendo uma string, como podemos ver no arquivo config.py a seguir.

Geralmente, nesse arquivo nós deixamos variáveis que ou serão usadas pela API, como variáveis de configuração da API, ou então alguma outra que usaremos ao longo do código, principalmente, variáveis que se repetirão. Assim, mantê-las em um arquivo facilita no caso de querermos alterá-las sem precisar procurar todos lugares aonde estão sendo usadas no código. Na última parte dessa série aprofundaremos na configuração da Flask API e esse config nos será muito útil.

E pronto!! Com isso, podemos executar nossa API. Para isso, basta navegar para a pasta raiz da aplicação (na pasta API) e executar o seguinte comando no terminal:

python app.py

Como isso, o output deve ser algo mais ou menos assim:

Isso significa que nossa API está sendo servida localmente e podemos testar requisições.

5) Testando requisições

Para o teste, preparei um script, make_request.py mostrado a seguir, para facilitar nossa vida. Basta abrirmos um outro terminal, ativar o ambiente virtual e executá-lo. Recomendaria você testar outras maneiras de enviar requisições para nossa API local (ou qualquer uma outra). Uma boa maneira é utilizar o Postman. Se você for um usuário de linux, também é possível utilizar o Curl:

curl -X POST -H "Content-Type: application/json" -d '{"age": [20], "weight": [80], "ap_hi":[125], "ap_lo":[80], "cholesterol": [1]}'  http://localhost:8000/cvd-classification

Nele, passamos o tipo de conteúdo que enviaremos, um Json, e em seguida passamos um formato chave-valor. Por fim, especificamos a URL que receberá a requisição.

Em seguida é apresentado o script de requisição com o Python utilizando a biblioteca requests .

Na linha 3 nós salvamos em uma variável o endpoint e em seguida criamos outra variável data contendo os dados que passaremos para a predição. A biblioteca requests nos possibilita fazer a requisição por um método POST passando os dados no formato json. Salvamos o retorno na variável r e fazemos o display do seu conteúdo. Usando o r.text podemos acessar o resultado que a API nos retornou.

Se executarmos python make_request.py no terminal, obteremos algo semelhante a isso:

Isso significa que deu certo 😍😎

Temos agora nossa aplicação executando e nos retornando o diagnóstico se um paciente tem ou não alguma doença cardiovascular. Não é legal??

6) Conclusão

Bom, nessa postagem vimos um pouco o que é uma API e desenvolvemos a nossa própria utilizando o Flask. Caminho longo, mas espero que tenha te ajudado.

Eternamente grato pelo seu tempo e já sabe, qualquer dúvida/sugestão, vamos conversar ai no chat.

Ah, seria um prazer me conectar com você no LinkedIn e no GitHub.

Te vejo no próximo post: colocaremos essa API em um container usando o Docker e veremos o benefício disso.

--

--