Servicing de Modelos com R e Plumber

Em algum momento todo cientista de dados ou engenheiro de machine learning já se deparou com enquetes e blog posts com a seguinte pergunta: “Para ambientes de produção, qual é melhor R ou Python?”.

Na maioria das vezes a linguagem Python sempre leva uma vantagem neste quesito; seja por conta da sua facilidade de aprendizado, ou (especulo eu) o fato que muitos usuários não entendem a diferença entre uma linguagem de uso geral e para uma linguagem de programação com objetivo de computação científica em script.

Existem inúmeros recursos que detonam o uso da linguagem R em produção por inúmeros fatores, alguns destes muito justos como:

  • Grande parte dos usuários não têm background em desenvolvimento de software;
  • Devido ao ponto anterior, não existe na comunidade uma cultura de práticas como gestão de dependências, testes, tratamento de erros e logging (mesmo com boas ferramentas para fazer tudo isso);
  • Argumentos ocultos na linguagem, como o inacreditável [stringAsFactors = TRUE](https://stat.ethz.ch/pipermail/r-announce/2020/000653.html) que só foi corrigido agora na versão 4.0.0 (i.e. não tem retrocompatibilidade!). Em outras palavras, um bug virou feature e um update de major version é necessário para corrigir um comportamento na linguagem por um erro de design da linguagem (uma boa explicação para isso está aqui neste post);
  • A falta de familiaridade dos usuários do R com pacotes/software que poderiam garantir uma maior robustez em termos de produtos de dados/inferência em produção como packrat para checkpointing e Docker para setup de front-end.

Contudo, em termos práticos nem sempre é possível que todos os cientistas de dados, analistas e demais usuários migrem para o Python devido à inúmeros motivos (e.g. custo de migração, custo de treino de pessoal, riscos de negócios para de retirar algo de produção, etc.).

Com isso, parte dos usuários em R acabam sem ter acesso a formas de colocar os seus modelos em produção e o mais importante: realizar o servicing desses modelos (i.e. receber requisições e dar respostas com as predições em uma plataforma que vai ser, a grosso modo, como uma espécie de serviço web em que a API vai servir para comunicar duas aplicações).

A ideia desse post é ajudar estas pessoas a terem o poder de subir uma RESTful APIs em produção para fazer o servicing desses modelos, e com uma ajuda de um time de infraestrutura esses códigos podem ser colocados em um servidor ou em uma imagem Docker, e assim estar disponível para outras aplicações.

Mas para dar um pouco mais de realismo no nosso exemplo, vamos usar um exemplo de um Banco chamado de Layman Brothers [N1] que tem como objetivo entregar um serviço de Machine Learning que informa se um cliente vai entrar em uma situação de atraso de pagamento ou não. E para isso vamos usar (apenas para fins de simplicidade) o AutoML para realizar o treinamento deste modelo.

Fonte: Steel factory by Gadjo_Niglo

AutoML (Automatic Machine Learning)

Para quem não sabe o conceito de AutoML (Automatic Machine Learning, ou treinamento automático de Machine Learning) é o processo de automação de todo o pipeline de treinamento de modelos de machine learning através do treinamento de inúmeros modelos dentro de um limite de tempo ou condição de parada (e.g. AUC, RMSE, Recall, Precision, etc).

Isto permite que mesmo pessoas não especialistas em Data Science e Machine Learning apenas passem os dados para o AutoML, e este realiza inúmeros treinamentos com varias combinações de algoritmos dentro de um determinado limite de tempo.

A ideia aqui é simplificar o processo de treinamento de ponta a ponta, fazendo o treino de inúmeros modelos simples ou a combinação de vários algoritmos (XGBoost, Deep Learning, GLM, GBM, etc.) com varias combinações de hiperparâmetros.

Em outras palavras, ao invés de haver um ser humano testando manualmente diversas combinações, o AutoML já faz tudo isso.

Em alguns casos, os modelos do AutoML chegam até mesmo bater cientistas de dados em leaderboards do Kaggle, como podemos ver no exemplo abaixo em que a Erin LeDell com apenas 100 minutos de treinamento no AutoML conseguiu ficar em 8º lugar em um Hackathon no Kaggle:

Fonte: Erin LeDell Twitter

AutoML no R com H2O.ai

No treinamento do nosso modelo de Default Prediction (ou previsão de calotes para os mais simplistas) do Layman Brothers, nós vamos usar a linguagem R e a implementação do AutoML no H2O (eu já postei alguns tutoriais e considerações sobre essa ferramenta aqui no blog anteriormente, vale a pena conferir).

No nosso caso vamos usar o AutoML to H2O devido ao fato de que além da ferramenta usar os algoritmos mais comuns, a implementação do AutoML no H2O tem também a opção de Stacked Ensembles de todos os modelos previamente treinados, e de quebra nos dá o leaderboard dos melhores modelos.

Para o treinamento do nosso modelo vamos usar os dados do nosso Layman Brothers no AutoML.

A estrutura do projeto terá 5 pastas com nomes autoexplicativos: (1) api, (2) data, (3) logs, (4) models e (5) src (onde ficará o código fonte). O caminho pode deve ser alterado (recomendável) e como não estamos usando um conjunto de dados que está no projeto mas sim em um endereço do GitHub, a pasta data é dispensável.

Antes de mais nada, vamos carregar da biblioteca de logging e usar os caminhos padrão como constantes para armazenar os nossos objetos:

https://gist.github.com/fclesio/5e2d2f8ea1b99082237863795cb54dbf

Com o log criado, vamos agora instalar o H2O direto do CRAN.

https://gist.github.com/fclesio/deb1cb6dbdafea9d8360a2389d886a8d

Um ponto que tem que ser levado em consideração aqui, é que eu estou realizando a instalação dos pacotes direto do CRAN devido ao fato de que, ao menos pra mim, as ferramentas de gestão de dependências do R não tem uma usabilidade boa em comparação com o Homebrew, npm e até mesmo o pip.

Dependências instaladas, vamos iniciar o nosso H2O cluster:

https://gist.github.com/fclesio/f8b4dae0873e7145a5f318b15931807f

No nosso caso vamos usar todos os CPUs das máquinas que por ventura estiverem no cluster (cpus=-1). Como eu estou rodando em uma máquina apenas, eu vou limitar o tamanho da memória em 7Gb.

Cluster iniciado, vamos agora fazer a carga dos nossos dados no H2O, separar os datasets de treinamento e teste, e determinar as variáveis [N2] que vamos usar no treinamento dos modelos:

https://gist.github.com/fclesio/d06e46613efd73139b002f299cfaacc0

Agora que os nossos dados estão carregados, vamos realizar o treinamento usando o AutoML:

https://gist.github.com/fclesio/9bc4484fa4cf9c10f4c33fce934d3c5a

No nosso caso, vamos usar no máximo 20 modelos (max_models = 20), com o AutoML fazendo o Cross Validation com 5 partições (nfolds = 5), travando semente randômica em 42 (seed = 42) e com o AUC como métrica que vai ser a referência no treinamento para determinar qual modelo é melhor (sort_metric = c("AUC")).

Existem inúmeras outras opções que podem ser configuradas, mas vamos usar estas para fins de simplicidade.

Após o treinamento, podemos armazenar as informações do leaderboard no log; ou verificar no console:

https://gist.github.com/fclesio/bf247cc482f2781f2f73128ca07ed8da

Se tudo ocorreu bem aqui, no final teremos o modelo vencedor serializado na pasta models pronto para ser usado pela nossa RESTful API [N4].

Para ler as informações do log durante o treinamento do modelo, basta apenas abrir o arquivo training_pipeline_auto_ml.log no sistema operacional, ou executar o comando $ tail -F training_pipeline_auto_ml.log durante a execução.

Isso pode ajudar, por exemplo, a ter o registro de quanto cada fase esta levando para acontecer. Caso a pessoa responsável pelo script queira, podem ser aplicados tratamentos de erros no código e posterior logging desses erros para facilitar a depuração de qualquer problema que venha acontecer.

Com o nosso modelo treinado e serializado, vamos agora subir o nosso endpoint [N3].

Configuração do endpoint da RESTful API no Plumber

Para o servicing dos nossos modelos, vamos usar o Plumber que é uma ferramenta que converte código em R em uma web API [N4]. No nosso caso, vamos usar o Plumber como ferramenta para subir a nossa API e fazer o servicing do modelo [N3].

Primeiramente vamos configurar o nosso endpoint. Resumidamente, um endpoint é um caminho de URL que vai comunicar-se com uma API [N4]. Este arquivo será chamado de endpoint.r.

Este endpoint vai ser responsável por fazer a ligação do nosso arquivo em que estará a nossa função de predição (falaremos sobre ele mais tarde) e as requisições HTTP que a nossa API vai receber.

Vamos colocar aqui também um arquivo de log, neste caso chamado de automl_predictions.log em que vamos registrar todas as chamadas neste endpoint.

https://gist.github.com/fclesio/2880ead6d1919215dab49a9dd564ff9f

Os mais atentos repararam que existem 3 funções neste endpoint. A primeira é a convert_empty que vai somente colocar um traço caso alguma parte das informações da requisição estiverem vazias.

A segunda é a função r$registerHooks que é oriunda de um objeto do Plumber e vai registrar todas as informações da requisição HTTP como o IP que está chamando a API, o usuário, e o tempo de resposta da requisição.

A terceira e ultima função é função r$run que vai determinar o IP em que a API vai receber as chamadas (host="127.0.0.1") a porta (port=8000) e se a API vai ter o Swagger ativo ou não (swagger=TRUE). No nosso caso vamos usar o Swagger para fazer os testes com a nossa API e ver se o serviço está funcionando ou não.

Esta vai ser o ultimo script que será executado, e mais tarde vamos ver como ele pode ser executado sem precisarmos entrar no RStudio ou demais IDEs.

Contudo, vamos agora configurar a nossa função de predição dentro do Plumber.

Configuração da função de predição dentro no Plumber

No nosso caso, vamos criar o arquivo chamado api.R. Este arquivo vai ser usado para (a) pegar os dados da requisição, (b) fazer um leve processamento nestes dados, (c) passar os mesmos para o modelo, (d) pegar o resultado e devolver para a o endpoint.

Esse arquivo vai ser referenciado no nosso exemplo, na linha 16 do arquivo endpoint.r.

Contudo, vamos agora entender cada uma das partes do arquivo api.r.

Aos moldes do que foi feito anteriormente, vamos iniciar o nosso arquivo buscando o caminho em que o nosso modelo esta salvo para posteriormente fazer a carga do mesmo em memória (linha 27 - “StackedEnsemble_AllModels_AutoML_20200428_181354") e posteriormente vamos iniciar o nosso logging (linha 27 "api_predictions.log").

No neste exemplo, o modelo serializado é o “StackedEnsemble_AllModels_AutoML_20200428_181354" que foi o melhor do leaderboard do AutoML.

Na linha 34, fazemos a carga do modelo em memória e a contar deste ponto o mesmo está pronto para receber dados para realizar as suas predições.

https://gist.github.com/fclesio/64eebb038b6ad2a62c0370ab08d791ff

Logging e modelo carregado, agora entra a parte em que vamos configurar as variáveis que serão recebidas pelo modelo. No nosso caso, temos o seguinte código:

https://gist.github.com/fclesio/3a5444dedb22c11e71f731f1dcb9ff8c

Os caracteres #* significam que estamos informado os parâmetros que serão passados para a função.

Abaixo temos o comando #* @post /prediction que, a grosso modo, vai ser o nome da pagina que vai receber o método POST. [N4]

Agora que temos as variáveis que o modelo vai receber devidamente declaradas para o Plumber (ou seja, a nossa API é capaz de receber os dados através das requisições), vamos criar a função que vai receber os dados e vai realizar a predição:

https://gist.github.com/fclesio/fbecb5d6754ccb5606f511a879a02de9

Essa é uma função simples em R que vai receber como argumentos, as variáveis que foram declaradas anteriormente para o Plumber.

Entre as linhas 8-30 eu fiz a conversão de todas as variáveis para numérico por um motivo simples: No momento em que eu passo a função direto (sem as conversões) o Plumber não faz a verificação de tipagem das variáveis antes de passar para o modelo.

Por causa desse problema eu perdi algumas horas tentando ver se havia alguma forma de fazer isso direto no Plumber, e até tem; mas no meu caso eu preferi deixar dentro da função e ter o controle da conversão lá. Na minha cabeça, eu posso deixar o tratamento de erro dentro da própria função e ao menos tentar algumas conversões, se for o caso. Mas aí vai da escolha de cada um.

Entre a linhas 34 e 59 eu construo o data.table, para posteriormente nas linhas 62 e 63 converter como objeto do H2O.ai.

Essa conversão torna-se necessária, pois os modelos do H2O.ai, até a presente versão, só aceitam objetos de dados dentro do seu próprio formato.

Finalmente entre as linhas 62 e 70 realizamos a predição propriamente dita, e retornamos a nossa predição na função.

Em seguida tem uma segunda função que pega o corpo da requisição (body) e mostra os valores no console (esses valores podem ser gravados no log também).

https://gist.github.com/fclesio/704941b057e9fb1c306935cbc9d7c6ee

E assim temos os nossos arquivos endpoint.r e api.r criados em que, em termos simples os arquivos tem os seguintes objetivos:

  • api.R: _Eu ten_ho o modelo carregado em memória, eu recebo os dados, eu faço o tratamento desses inputs, jogo no modelo e devolvo uma predição. E de quebra, eu sou responsável por falar quais parâmetros o modelo vai receber.
  • endpoint.r: Eu subo a API, recebo as informações de quem está fazendo a requisição como IP e usuário, e faço a referência ao api.R que vai fazer a parte difícil da predição.

No seu caso, se voce já tiver o seu modelo, basta apenas trabalhar nos arquivos api.R e endpoint.r, e adaptar os inputs com os seus dados e colocar o seu modelo de machine learning em memória.

Agora que temos os nossos arquivos, vamos subir a nossa API.

Inicializando a RESTful API

Com os nossos arquivos da API e do nosso endpoint devidamente configurados, para inicializar a nossa API podemos executar o arquivo endpoint.R dentro do R Studio.

Entretanto, como estamos falando em um ambiente de produção, fazer isso manualmente não é prático, principalmente me um ambiente em que mudanças estão sendo feitas de forma constante.

Dessa forma, podemos inicializar essa API executando o seguinte comando na linha de comando (terminal para os usuários de Linux/MacOS):

$ R < /<<YOUR-PATH>>/r-api-data-hackers/api/endpoint.R --no-save

Na execução desse comando, teremos no terminal a seguinte imagem:

Com esse comando precisamos apenas dos arquivos nos diretórios para inicializar a nossa RESTful API que ser inicializada no endereço http://127.0.0.1:8000.

Contudo, acessando essa URL no browser não vai aparecer nada, e para isso vamos usar o Swagger para realizar os testes. Para isso, vamos acessar no nosso browser o endereço: http://127.0.0.1:8000/__swagger__/

No browser teremos uma tela semelhante a esta:

Para realizar a predição via a interface do Swagger, vamos clicar no ícone verde escrito POST. Veremos uma tela semelhante a esta:

Em seguida, vamos clicar no botão escrito “Try it out” e preencha as informações dos campos que declaramos como parâmetros lá no arquivo endpoint.r:

No final, após todas as informações estarem preenchidas, clique no botão azul que contem a palavra execute:

Clicando neste botão, podemos ver o resultado da nossa predição no response body:

O corpo dessa resposta de requisição que mandamos para a URL tem as seguintes informações:

[
  {
    "predict": "1",
    "p0": 0.5791,
    "p1": 0.4209
  }
]

Ou seja, dentro desses valores passados na requisição, o modelo do banco Layman Brothers previu que o cliente vai entrar na situação de default (ou dar o calote). Se quisermos trabalhar com as probabilidades, o modelo da essas informações na resposta, sendo que o cliente tem a probabilidade de 58% de dar o calote, contra 42% de probabilidade de não dar o calote.

Mas para os leitores que não morreram ate aqui, alguns deles podem perguntar: “Poxa Flavio, mas os clientes não vão entrar na nossa pagina swagger e fazer a requisição. Como uma aplicação em produção vai fazer o uso desse modelo?

Lembram que eu falei que essa RESTful API seria, a grosso modo, uma espécie de serviço web? Então o ponto aqui é que a aplicação principal, i.e. Plataforma do nosso banco Layman Brothers que vai receber as informações de credito, vai passar essas informações para a nossa RESTful API que está fazendo o servicing dos modelos via requisições HTTP e a nossa API vai devolver os valores da mesma maneira que vimos no corpo da mensagem anterior.

Trazendo para termos mais concretos: No momento em que a sua RESTful API está rodando, o seu modelo está pronto para ser requisitado pela aplicação principal.

Essa chamada HTTP pode ser feita copiando o comando curl que está sendo informado pelo Swagger, como podemos ver na imagem abaixo:

Neste caso, para simular a chamada que a aplicação principal do Layman Brothers tem que fazer, vamos copiar o seguinte comando curl:

curl -X POST "http://127.0.0.1:8000/prediction?PAY_AMT6=1000&PAY_AMT5=2000&PAY_AMT4=300&PAY_AMT3=200&PAY_AMT2=450&PAY_AMT1=10000&BILL_AMT6=300&BILL_AMT5=23000&BILL_AMT4=24000&BILL_AMT3=1000&BILL_AMT2=1000&BILL_AMT1=1000&PAY_6=200&PAY_5=200&PAY_4=200&PAY_3=200&PAY_2=200&PAY_0=2000&AGE=35&MARRIAGE=1&EDUCATION=1&SEX=1&LIMIT_BAL=1000000" -H "accept: application/json"

Após copiarmos esse comando, vamos colar no terminal, e executar apertando a tecla enter. Teremos o seguinte resultado:

Ou seja, recebemos o mesmo resultado que executamos no Swagger. Sucesso.

Para ler os nossos logs posteriormente, basta executarmos o comando tail -F api_predictions.log dentro da pasta logs como abaixo, temos o seguinte resultado:

Aqui temos todas as informações que colocamos para serem registradas no arquivo de logs. Dessa forma, caso esse processo seja automatizado, pode ser feito uma depuração ou auditoria dos resultados, casa seja necessário.

Existem duas versões desse código no GitHub. Essa versão light está no repositório r-api-data-hackers e a versão mais completa, está no repositório r-h2o-prediction-rest-api.

CONSIDERAÇÕES FINAIS

O objetivo aqui neste foi mostrar de um passo a passo como cientistas de dados, estatísticos, e demais interessados podem subir uma RESTful API inteiramente usando código R.

O projeto em si, dentro da perspectiva de codificação em produção tem muitas limitações como tratamento de erro, segurança, logging, tratamento das requisições e das respostas no log, e subir tudo isso em um ambiente mais isolado, como por exemplo no Docker.

Entretanto, acho que depois desse tutorial muitos problemas relativos à parte prática de colocar modelos de machine learning em produção no R podem, no mínimo ser endereçados e com isso dar mais poder aos cientistas de dados que desenvolvem em R e demais interessados.

NOTAS

  • [N1] - Nome sem nenhuma ligação com a realidade.
  • [N2] - As variáveis SEX (gênero), MARRIAGE (se o/a cliente é casado(a) ou não) e AGE (idade) estão apenas para fins de demonstração como qualquer outra variável. No mundo real, idealmente essas variáveis seriam totalmente eliminadas para não trazer vieses discriminatórios nos modelos e demais problemas éticos.
  • [N3] - Existem inúmeras opções para subir a API em produção através de hosting que nada mais são do que serviços pagos que garantem parte da infraestrutura e cuidam de algumas questões de segurança e autenticação como a Digital Ocean, o RStudio Connect, e existem alguns recursos para fazer o hosting do Plumber em imagens Docker. No nosso caso, vamos assumir que essa API vai ser colocada em produção em uma maquina em rede na qual um analista de infraestrutura ou um cientista de dados possa fazer o deployment dessa API.
  • [N4] - Embora o objetivo deste post seja “fazer funcionar primeiro, para depois entender” é de extrema importância o entendimento dos aspectos ligados as nomenclaturas o que faz cada parte da arquitetura REST. Existem ótimos recursos para isso como aqui, aqui, aqui, e aqui

REFERÊNCIAS