in Uncategorized

RESTful API para servicing de modelos de Machine Learning com R, H2O.ai 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 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.

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:

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:

if (!require('logger')) install.packages('logger'); library('logger')
start_time_pipeline <- Sys.time()
log_debug('Training pipeline start time – {start_time_pipeline}')
# Local directories
ROOT_DIR <- getwd()
PROJECT_DIR <-
'r-api-data-hackers'
DATA_DIR <- 'data'
MODELS_DIR <- 'models'
API_DIR <- 'api'
LOGS_DIR <- 'logs'
get_artifact_path <- function(file_name,
artifact_dir,
root_dir=ROOT_DIR,
project_dir=PROJECT_DIR){
artifact_path <-
file.path(root_dir,
project_dir,
artifact_dir,
file_name)
return (artifact_path)
}
logging_file_path <-
get_artifact_path("training_pipeline_auto_ml.log", LOGS_DIR)
log_appender(appender_file(logging_file_path))
log_layout(layout_glue_colors)
log_threshold(DEBUG)
log_info('Start logging')

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

install_dependencies <- function(){
package_url_logger <- 'https://cran.rstudio.com/bin/macosx/el-capitan/contrib/3.6/logger_0.1.tgz'
package_url_h2o <- 'https://cran.rstudio.com/bin/macosx/el-capitan/contrib/3.6/h2o_3.30.0.1.tgz'
package_url_cluster <- 'https://cran.rstudio.com/bin/macosx/el-capitan/contrib/3.6/cluster_2.1.0.tgz'
package_url_dplyr <- 'https://cran.rstudio.com/bin/macosx/el-capitan/contrib/3.6/dplyr_0.8.5.tgz'
package_url_tidyverse <- 'https://cran.rstudio.com/bin/macosx/el-capitan/contrib/3.6/tidyverse_1.3.0.tgz'
log_debug('logger CRAN URL: {package_url_logger}')
log_debug('h2o CRAN URL: {package_url_h2o}')
log_debug('cluster CRAN URL: {package_url_cluster}')
log_debug('dplyr CRAN URL: {package_url_dplyr}')
log_debug('tidyverse CRAN URL: {package_url_tidyverse}')
packages_urls <- c(
package_url_logger,
package_url_dplyr,
package_url_cluster,
package_url_tidyverse,
package_url_h2o
)
for(url in packages_urls)
{for(package_url in url)
log_info('Installing {} package')
{install.packages(package_url, repos=NULL, type='source')}
log_info('Package {package_url} installation finished')
}
}
log_info('Start installing dependencies')
install_dependencies()
log_info('Dependencies installed')
packageVersion_logger <- packageVersion('logger')[1]
packageVersion_h2o <- packageVersion('h2o')[1]
packageVersion_cluster <- packageVersion('cluster')[1]
packageVersion_dplyr <- packageVersion('dplyr')[1]
packageVersion_tidyverse <- packageVersion('tidyverse')[1]
log_debug('logger Version: {packageVersion_logger}')
log_debug('h2o Version: {packageVersion_h2o}')
log_debug('cluster Version: {packageVersion_cluster}')
log_debug('dplyr Version: {packageVersion_dplyr}')
log_debug('tidyverse Version: {packageVersion_tidyverse}')
log_info('Loading packages')
packages <- c(
"logger",
"h2o",
"cluster",
"dplyr",
"tidyverse")
invisible(lapply(packages, library, character.only = TRUE))
log_info('Packages loaded')
session_info_base_packages <- sessionInfo()$basePkgs
log_info('Session Info Base Packages: {session_info_base_packages}')
session_info_loaded_packages <- sessionInfo()$loadedOnly
log_debug('Session Info Loaded Packages: {session_info_loaded_packages}')

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:

log_info('Initializing H2O')
host = "localhost"
host_port = 54321
cpus = 1
memory_size = "7g"
log_debug('H2O Cluster host: {host}')
log_debug('H2O Cluster host port: {host_port}')
log_debug('H2O Cluster Number CPUs: {cpus}')
log_debug('H2O Cluster Memory Size allocated: {memory_size}')
h2o.init(
ip = host,
port = host_port,
nthreads = cpus,
max_mem_size = memory_size
)
cluster_status <- h2o.clusterStatus()
log_debug('H2O Cluster Status Info: {cluster_status}')

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:

log_debug('Load data')
layman_brothers_url =
"https://raw.githubusercontent.com/fclesio/learning-space/master/Datasets/02%20-%20Classification/default_credit_card.csv"
layman_brothers.hex = h2o.importFile(path = layman_brothers_url,
destination_frame = "layman_brothers.hex")
log_debug('Data loaded')
log_debug('Transform default variable to factor')
layman_brothers.hex$DEFAULT = as.factor(layman_brothers.hex$DEFAULT)
log_debug('Construct test and train sets using sampling')
layman_brothers.split <- h2o.splitFrame(data = layman_brothers.hex,
ratios = 0.90, seed =42)
layman_brothers.train <- layman_brothers.split[[1]]
layman_brothers.test <- layman_brothers.split[[2]]
qty_samples_train <- nrow(layman_brothers.train)
qty_samples_test <- nrow(layman_brothers.test)
log_debug('Training set with {qty_samples_train} records')
log_debug('Test set with {qty_samples_test} records')
log_debug('Set predictor and response variables')
y = "DEFAULT"
x = c(
"LIMIT_BAL"
,"SEX"
,"EDUCATION"
,"MARRIAGE"
,"AGE"
,"PAY_0"
,"PAY_2"
,"PAY_3"
,"PAY_4"
,"PAY_5"
,"PAY_6"
,"BILL_AMT1"
,"BILL_AMT2"
,"BILL_AMT3"
,"BILL_AMT4"
,"BILL_AMT5"
,"BILL_AMT6"
,"PAY_AMT1"
,"PAY_AMT2"
,"PAY_AMT3"
,"PAY_AMT4"
,"PAY_AMT5"
,"PAY_AMT6")

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

log_debug('Run AutoML for model training')
start_time <- Sys.time()
aml <-
h2o.automl(x=x,
y=y,
training_frame = layman_brothers.train,
validation_frame = layman_brothers.test,
max_models = 20,
nfolds = 5,
stopping_metric = c("AUC"),
project_name = "data-hackers-auto-ml",
sort_metric = c("AUC"),
verbosity = "warn",
seed = 42
)
end_time <- Sys.time()
log_debug('AutoML training ended')
time_elapsed <- end_time start_time
log_debug('Time elapsed – {time_elapsed}')

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:

lb <- aml@leaderboard
for (model_auto_ml in 1:nrow(lb)){
auto_ml_model_id <-
as.list(lb$model_id)[model_auto_ml][1]
auto_ml_auc <-
as.list(lb$auc)[model_auto_ml][1]
auto_ml_logloss <-
as.list(lb$logloss)[model_auto_ml][1]
auto_ml_aucpr <-
as.list(lb$aucpr)[model_auto_ml][1]
auto_ml_mean_per_class_error <-
as.list(lb$mean_per_class_error)[model_auto_ml][1]
auto_ml_rmse <-
as.list(lb$rmse)[model_auto_ml][1]
auto_ml_mse <-
as.list(lb$mse)[model_auto_ml][1]
log_info("AutoML – model_id: {auto_ml_model_id} – auc: {auto_ml_auc} – logloss: {auto_ml_logloss} – aucpr: {auto_ml_aucpr} – mean_per_class_error: {auto_ml_mean_per_class_error} – rmse: {auto_ml_rmse} – mse: {auto_ml_mse}")
}
log_info("AutoML Winning Model – model_id: {[email protected]@model_id} – algorithm: {[email protected]@algorithm} – seed: {[email protected]@parameters$seed} – metalearner_nfolds: {[email protected]@parameters$metalearner_nfolds} – training_frame: {[email protected]@parameters$training_frame} – validation_frame: {[email protected]@parameters$validation_frame}")
model_file_path <-
get_artifact_path("", MODELS_DIR)
log_info("Model destination path: {model_file_path}")
model_path <- h2o.saveModel(object=aml@leader,
path=model_file_path,
force=TRUE)
log_info("Model artifact path: {model_path}")
end_time_pipeline <- Sys.time()
log_debug('Training pipeline end time – {end_time_pipeline}')
time_elapsed_pipeline <- end_time_pipeline start_time_pipeline
log_debug('Training pipeline time elapsed – {time_elapsed_pipeline[1]} mins')
log_debug('Training pipeline finished')

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.

library(plumber)
library('logger')
ROOT_DIR <- getwd()
PROJECT_DIR <-
'r-api-data-hackers'
API_DIR <- 'api'
LOGS_DIR <- 'logs'
api_path_object <-
file.path(ROOT_DIR,
PROJECT_DIR,
API_DIR,
"api.R")
logging_file_path <-
file.path(ROOT_DIR,
PROJECT_DIR,
LOGS_DIR,
"automl_predictions.log")
log_appender(appender_file(logging_file_path))
log_layout(layout_glue_colors)
log_threshold(DEBUG)
convert_empty <- function(string) {
if (string == "") {
""
} else {
string
}
}
r <- plumb(api_path_object)
r$registerHooks(
list(
preroute = function() {
# Start timer for log info
tictoc::tic()
},
postroute = function(req, res) {
end <- tictoc::toc(quiet = TRUE)
log_info('REMOTE_ADDR: {convert_empty(req$REMOTE_ADDR)}, HTTP_USER_AGENT: "{convert_empty(req$HTTP_USER_AGENT)}", HTTP_HOST: {convert_empty(req$HTTP_HOST)}, REQUEST_METHOD: {convert_empty(req$REQUEST_METHOD)}, PATH_INFO: {convert_empty(req$PATH_INFO)}, request_status: {convert_empty(res$status)}, RESPONSE_TIME: {round(end$toc – end$tic, digits = getOption("digits", 5))}')
}
)
)
r
r$run(host="127.0.0.1", port=8000, swagger=TRUE)
view raw endpoint.r hosted with ❤ by GitHub

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.

library('logger')
library(h2o)
library('data.table')
h2o.init()
ROOT_DIR <- getwd()
PROJECT_DIR <-
'r-api-data-hackers'
MODELS_DIR <- 'models'
API_DIR <- 'api'
LOGS_DIR <- 'logs'
model_path_object <-
file.path(ROOT_DIR,
PROJECT_DIR,
MODELS_DIR,
"StackedEnsemble_AllModels_AutoML_20200428_181354")
logging_file_path <-
file.path(ROOT_DIR,
PROJECT_DIR,
LOGS_DIR,
"api_predictions.log")
log_appender(appender_file(logging_file_path))
log_layout(layout_glue_colors)
log_threshold(DEBUG)
log_info('Load saved model')
saved_model <-
h2o.loadModel(model_path_object)
log_info('Model loaded')

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:

#* Return the prediction from Laymans Brothers Bank Model
#* @param LIMIT_BAL
#* @param SEX
#* @param EDUCATION
#* @param MARRIAGE
#* @param AGE
#* @param PAY_0
#* @param PAY_2
#* @param PAY_3
#* @param PAY_4
#* @param PAY_5
#* @param PAY_6
#* @param BILL_AMT1
#* @param BILL_AMT2
#* @param BILL_AMT3
#* @param BILL_AMT4
#* @param BILL_AMT5
#* @param BILL_AMT6
#* @param PAY_AMT1
#* @param PAY_AMT2
#* @param PAY_AMT3
#* @param PAY_AMT4
#* @param PAY_AMT5
#* @param PAY_AMT6
#* @post /prediction

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:

function(LIMIT_BAL, SEX, EDUCATION, MARRIAGE,
AGE, PAY_0, PAY_2, PAY_3, PAY_4, PAY_5,
PAY_6, BILL_AMT1, BILL_AMT2, BILL_AMT3,
BILL_AMT4, BILL_AMT5, BILL_AMT6, PAY_AMT1,
PAY_AMT2, PAY_AMT3, PAY_AMT4, PAY_AMT5, PAY_AMT6) {
LIMIT_BAL <- as.numeric(LIMIT_BAL)
SEX <- as.numeric(SEX)
EDUCATION <- as.numeric(EDUCATION)
MARRIAGE <- as.numeric(MARRIAGE)
AGE <- as.numeric(AGE)
PAY_0 <- as.numeric(PAY_0)
PAY_2 <- as.numeric(PAY_2)
PAY_3 <- as.numeric(PAY_3)
PAY_4 <- as.numeric(PAY_4)
PAY_5 <- as.numeric(PAY_5)
PAY_6 <- as.numeric(PAY_6)
BILL_AMT1 <- as.numeric(BILL_AMT1)
BILL_AMT2 <- as.numeric(BILL_AMT2)
BILL_AMT3 <- as.numeric(BILL_AMT3)
BILL_AMT4 <- as.numeric(BILL_AMT4)
BILL_AMT5 <- as.numeric(BILL_AMT5)
BILL_AMT6 <- as.numeric(BILL_AMT6)
PAY_AMT1 <- as.numeric(PAY_AMT1)
PAY_AMT2 <- as.numeric(PAY_AMT2)
PAY_AMT3 <- as.numeric(PAY_AMT3)
PAY_AMT4 <- as.numeric(PAY_AMT4)
PAY_AMT5 <- as.numeric(PAY_AMT5)
PAY_AMT6 <- as.numeric(PAY_AMT6)
log_debug('Generate data.table with converted variables')
predict_objects <- data.frame(
LIMIT_BAL = c(LIMIT_BAL),
SEX = c(SEX),
EDUCATION = c(EDUCATION),
MARRIAGE = c(MARRIAGE),
AGE = c(AGE),
PAY_0 = c(PAY_0),
PAY_2 = c(PAY_2),
PAY_3 = c(PAY_3),
PAY_4 = c(PAY_4),
PAY_5 = c(PAY_5),
PAY_6 = c(PAY_6),
BILL_AMT1 = c(BILL_AMT1),
BILL_AMT2 = c(BILL_AMT2),
BILL_AMT3 = c(BILL_AMT3),
BILL_AMT4 = c(BILL_AMT4),
BILL_AMT5 = c(BILL_AMT5),
BILL_AMT6 = c(BILL_AMT6),
PAY_AMT1 = c(PAY_AMT1),
PAY_AMT2 = c(PAY_AMT2),
PAY_AMT3 = c(PAY_AMT3),
PAY_AMT4 = c(PAY_AMT4),
PAY_AMT5 = c(PAY_AMT5),
PAY_AMT6 = c(PAY_AMT6),
stringsAsFactors = FALSE
)
log_debug('Convert to H20.ai Object…')
predict_objects <-
as.h2o(predict_objects)
log_debug('Make prediction…')
prediction <-
h2o.predict(object = saved_model,
newdata = predict_objects)
prediction <- as.data.table(prediction)
log_debug('Default: {prediction}')
return(prediction)
}

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).

function(req) {
raw_body = req$postBody
print(raw_body)
}
view raw api-request-values.r hosted with ❤ by GitHub

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 tenho 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

Write a Comment

Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.