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: {aml@leader@model_id} – algorithm: {aml@leader@algorithm} – seed: {aml@leader@parameters$seed} – metalearner_nfolds: {aml@leader@parameters$metalearner_nfolds} – training_frame: {aml@leader@parameters$training_frame} – validation_frame: {aml@leader@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

RESTful API para servicing de modelos de Machine Learning com R, H2O.ai e Plumber

Machine Learning e o modelo de queijo suíço: falhas ativas e condições latentes

TL;DR: Problemas sempre vão existir. Uma postura reflexiva, sistemática, e com um plano de ação sempre foi e sempre será o caminho para resolução destes mesmos problemas.

Eu estava escrevendo um post sobre a importância dos Post Mortems em machine learning e vi que esta parte em específico estava ficando maior do que o ponto principal do outro post. Dessa forma eu resolvi quebrar esse post em um assunto específico com um pouco mais de foco e detalhes.

Aplicações de Machine Learning (ML) e Inteligência Artificial (IA) estão avançando em domínios cada vez mais críticos como medicina, aviação, setor bancário, investimentos entre outros. 

Estas aplicações estão diariamente tomando decisões de forma automatizada e em alta escala; não somente moldando a forma na qual indústrias estão operando, mas também como pessoas estão interagindo com plataformas que utilizam estas tecnologias.

Dito isso, é de fundamental importância que a cultura de engenharia em ML/AI incorpore e adapte cada vez mais conceitos como confiabilidade e robustez que são óbvios em outros campos da engenharia.

E um dos caminhos para essa adaptação é o entendimento de aspectos causais que possam elevar o risco de indisponibilidade destes sistemas.

Antes de prosseguir no texto eu recomendo a leitura do post Accountability, Core Machine Learning e Machine Learning Operations que fala um pouco de aplicações de ML em produção e da importância da engenharia na construção desses sistemas complexos.

A ideia aqui é falar sobre falhas ativas e condições latentes utilizando de forma simples o Modelo de Queijo Suíço. O objetivo é mostrar como estes dois fatores estão ligados na cadeia de eventos de indisponibilidades e/ou catastróficos em sistemas de ML.

Mas antes disso vamos entender um pouco do porque o entendimento das falhas pode ser um caminho alternativo para a melhoria da confiabilidade, e também sobre os “cases de sucesso” que vemos todos os dias na internet.

Viés de sobrevivência e aprendizado pela falha

Hoje na internet há uma miríade de informações sobre praticamente qualquer área técnica. Com todo o hype em ML e com a sua crescente adoção, estas informações materializam-se na forma de tutoriais, blog posts, fóruns de discussão, MOOCs, Twitter, entre outras fontes.

Porém, um leitor mais atento pode notar um determinado padrão em parte dessas histórias: Na maioria das vezes são cases de algo que (a) deu extremamente certo, (b) ou que gerou receita para a empresa, (c) ou como a solução o salvou X% em termos de eficiência, e/ou (d) como a nova solução de tecnologia foi uma das maiores maravilhas técnicas que já foram construídas.

Isso rende claps no Medium, posts no Hacker News, artigos em grandes portais de tecnologia, technical blog posts que que viram referências técnicas, papers e mais papers no Arxiv, palestras em conferências, etc.

Logo de antemão eu quero adiantar que eu sou um grande entusiasta da ideia de que “pessoas inteligentes aprendem com os próprios erros, e pessoas sábias aprendem com os erros dos outros”. Estes recursos, especialmente os technical blog posts e as conferências, reúnem um nível altíssimo de informações extremamente valiosas de pessoas que estão nas trincheiras técnicas.

Este bazar de ideias é extremamente saudável para a comunidade como um todo. Além do mais, este bazar está enterrando o antigo modelo de gatekeeping em que algumas consultorias de conferências surfaram por anos às custas de desinformação fazendo inúmeras empresas desperdiçarem rios de dinheiro. Ademais, este bazar de ideias está ajudando a acabar com o nefasto culto à personalidades de tecnologia em que qualquer pessoa pode ter uma voz.

Contudo, o que muitos destes posts, conference talks, papers e demais artigos não citam geralmente, são as coisas que dão/deram muito erradas durante o desenvolvimento dessas soluções; e isso essencialmente é um problema dado que estamos apenas vendo o resultado final e não o como esse resultado foi gerado e os falhas/erros cometidos no caminho.

Realizando um simples exercício de reflexão, é até compreensível que pouquíssimas pessoas socializem os erros cometidos e lições aprendidas; dado que nos dias de hoje, especialmente com as mídias sociais, a mensagem fica muito mais amplificada e distorcida.

Admitir erros não é algo fácil. Dependendo do grau de maturidade psicológica da pessoa que errou, junto com o erro pode vir uma montanha de sentimentos como constrangimento, inadequação, raiva, vergonha, negação etc. Isso pode levar a problemas de ordem psicológica em que um profissional de saúde mental tenha que acompanhar a pessoa que cometeu o erro. 

Do ponto de vista das empresas, a imagem que pode ficar em relação à relações públicas, é de desorganização corporativa, times de engenharia ruins, líderes técnicos que não sabem o que estão fazendo, etc. Isso pode afetar, por exemplo, ações de recrutamento.

Devido a estes pontos acima, isso implica que (1) talvez grande parte destes problemas podem estar acontecendo neste exato momento e estão sendo simplesmente suprimidos e (2) talvez exista um grande viés de sobrevivência nestes posts/talks/papers.

Não existe nada de errado com a forma na qual as empresas colocam os seus relatos, entretanto, um pouco de ceticismo e pragmatismo sempre é bom; pois, para cada caso de sucesso, sempre existirá uma infinidade de times que falharam miseramente, empresas que quebraram, pessoas que foram demitidas, etc.

Mas afinal, o que isso tudo tem a ver com a falhas que acontecem e porque entender os seus fatores contribuintes?

A resposta é: Porque primeiramente o seu time/solução tem que ser capaz de sobreviver à situações catastróficas para que o caso de sucesso exista. E ter a sobrevivência como aspecto motivador para aumentar a confiabilidade de times/sistemas, torna o entendimento dos erros em uma forma atrativa de aprendizado.

E quando existem cenários pequenas violações, supressão de erros, ausência de procedimentos, imperícia, imprudência ou negligência, as coisas dão espetacularmente muito errado, como nos exemplos abaixo:

Claro que nestas linhas mal escritas não haverá um ode à catástrofe ou disaster porn.

Porém, eu quero colocar um outro ponto de vista no sentido de que sempre existe uma lição a ser aprendida diante do que dá errado, e que empresas/times que mantém uma atitude introspectiva em relação aos problemas que acontecem ou analisam os fatores que possam a vir contribuir para um incidente, reforçam não somente uma cultura saudável de aprendizado como promovem uma cultura de engenharia mais orientada para aspectos de confiabilidade.

Partindo para o ponto prático, eu vou comentar um pouco sobre uma ferramenta (modelo mental) de gerenciamento de riscos que é o Modelo do Queijo Suíço que auxilia no entendimento de fatores causais que contribuem para a desastres em sistemas complexos.

O Modelo do Queijo Suíço

Se eu tivesse que dar um exemplo de indústria em que a confiabilidade pode ser considerada referência, com certeza seria a indústria da aviação [N2]. 

Em cada evento catastrófico que ocorre, há uma investigação minuciosa para entender o que aconteceu, e posteriormente endereçar os fatores contribuintes e fatores determinantes para um novo evento catastrófico nunca mais venha a acontecer.

Dessa forma, a aviação garante que aplicando o que foi aprendido devido ao evento catastrófico, todo o sistema fica mais confiável. Não é por acaso que mesmo com o aumento no número de voos (39 milhões de voos no último ano, 2019) o número de fatalidades vem caindo a cada ano que passa.

Uma das ferramentas mais utilizadas em investigação de acidentes aéreos para análise de riscos e aspectos causais é o Modelo de Queijo Suíço

Este modelo foi criado por James Reason através do artigo “The contribution of latent human failures to the breakdown of complex systems” em que houve a construção do seu framework (mas sem referência direta do termo). Entretanto, somente no paper “Human error: models and management o modelo aparece de forma mais direta.

A justificativa do modelo por parte do autor, é feita considerando um cenário de um sistema complexo e dinâmico da seguinte forma:

Defesas, barreiras e salvaguardas ocupam uma posição-chave na abordagem do sistema. Os sistemas de alta tecnologia têm muitas camadas defensivas: algumas são projetadas (alarmes, barreiras físicas, desligamentos automáticos etc.), outras contam com pessoas (cirurgiões, anestesistas, pilotos, operadores de salas de controle, etc.) e outras dependem de procedimentos e controles administrativos. Sua função é proteger possíveis vítimas e ativos contra riscos locais. Muitas das vezes essas camadas fazem isso de maneira muito eficaz, mas sempre há fraquezas.

Em um mundo ideal, cada camada defensiva estaria intacta. Na realidade, porém, são mais como fatias de queijo suíço, com muitos buracos – embora, diferentemente do queijo, esses buracos estejam continuamente abrindo, fechando e mudando de local. A presença de orifícios em qualquer “fatia” normalmente não causa um resultado ruim. Geralmente, isso pode acontecer apenas quando os orifícios em várias camadas se alinham momentaneamente para permitir uma trajetória de oportunidade de acidente – trazendo riscos para o contato prejudicial com as vítimas.

Human error: models and management

Uma forma de visualização deste alinhamento pode ser vista no gráfico abaixo:

Ou seja, neste caso cada fatia do queijo suíço seria uma linha de defesa com camadas projetadas (ex: monitoramento, alarmes, travas de push de código em produção, etc.) e/ou as camadas procedurais que envolvem pessoas (ex: aspectos culturais, treinamento e qualificação de commiters no repositório, mecanismos de rollback, testes unitários e de integração, etc.).

Ainda dentro do que o autor colocou, cada furo em alguma das fatias do queijo acontecem por dois fatores: falhas ativas e condições latentes, em que:

  • Condições latentes são como uma espécie de situações intrinsecamente residentes dentro do sistema; que são consequências de decisões de design, engenharia, de quem escreveu as normas ou procedimentos e até mesmo dos níveis hierárquicos mais altos de uma organização. Essas condições latentes podem levar a dois tipos de efeitos adversos que são situações que provocam ao erro e a criação de vulnerabilidades. Isto é, a solução possui um design que eleva a probabilidade de eventos de alto impacto negativo que pode ser equivalente a um fator causal ou fator contribuinte.  
  • Falhas Ativas são atos inseguros ou pequenas transgressões cometidos pelas pessoas que estão em contato direto com o sistema; atos estes que podem ser deslizes, lapsos, distorções, omissões, erros e violações processuais.

Se as condições latentes estão ligadas à aspectos ligados a engenharia e produto; as falhas ativas estão muito mais relacionadas com fatores humanos. Um ótimo framework para análise de fatores humanos é o Human Factors Analysis and Classification System (HFACS).

O HFACS coloca que as falhas humanas em sistema tecnológico-sociais complexos acontecem em quatro diferentes níveis como pode ser visto na imagem abaixo:

A ideia aqui no post não é discutir esses conceitos, e sim realizar um paralelo com machine learning em que alguns destes aspectos serão tratados. Para quem quiser saber mais eu recomendo a leitura do HFACS para uma leitura aprofundada do framework.

Já que temos alguns dos conceitos bem claros do que são as falhas ativas e condições latentes, vamos realizar um exercício de reflexão usando alguns exemplos com ML.

Gerenciamento de falhas ativas e condições latentes em Machine Learning

Para fazer a transposição destes fatores para a arena de ML de uma forma mais concreta, eu vou usar alguns exemplos do que eu já vi acontecer, do que já aconteceu comigo, e mais alguns dos pontos do excelente artigo de Sculley, David, et al. chamado “Hidden technical debt in machine learning systems.” apenas para efeitos didáticos. 

De maneira geral esses conjuntos de fatores (não-exaustivos) estariam representados da seguinte maneira:

Condições Latentes

  • Cultura de arranjos técnicos improvisados (workarounds): O uso de arranjos técnicos improvisados gambiarra em algumas situações é extremamente necessário. Contudo, uma cultura de voltada a workarounds [N3] em um campo que tem complexidades intrínsecas como ML tende a incluir potenciais fragilidades em sistemas de ML e tornar o processo de identificação e correção de erros muito mais lento.
  • Ausência de monitoramento e alarmística: Em plataformas de ML alguns fatores que precisam de monitoramento específico como data drift (i.e. mudança na distribuição dos dados que servem de input para o treinamento) model drift (i.e. degradação do modelo em relação aos dados que são previstos) e adversarial monitoring que é o monitoramento para assegurar que o modelo está sendo testado para coleta de informações ou ataques adversariais.
  • Resumé-Driven Development ou RDD, é quando engenheiros ou times implementam uma ferramenta em produção apenas para ter no CV que trabalharam com a mesma, potencialmente prospectando um futuro empregador. O RDD tem como principal característica de criar uma dificuldade desnecessária para vender uma facilidade inexistente se a coisa certa tivesse sido feita. 
  • Decisões de tipo democracia com pessoas menos informadas ao invés do consenso entre especialistas e tomadores de risco: O ponto aqui é simples: Decisões chave só podem ser tomadas por (a) quem estiver envolvido diretamente na construção e na operacionalização dos sistemas, (b) quem estiver financiando e/ou tomando o risco, e (c) quem tem o nível de habilidades técnicas para saber os prós e contras de cada aspecto da decisão. A razão é que essas pessoas têm ao menos a própria pele em jogo ou sabem os pontos fracos e fortes do que está sendo tratado. O Fabio Akita já fez um argumento bem interessante nesta linha que mostra o quão ruim pode ser quando pessoas sem a pele em jogo e mal informadas estão tomando decisões. Democracia em profissões de prática não existe. Essa neo-democracia corporativa coletivista não tem rosto, e logo não tem accountability caso algo dê errado. Democracia em aspectos técnicos nos termos colocados acima é uma condição latente. Algo errado nunca será correto apenas porque uma maioria decidiu.

Falhas Ativas

  • Código não revisado indo para produção: Diferentemente da boa engenharia de software tradicional em que existe um camada de revisão de código para assegurar se tudo está dentro dos padrões de qualidade, em ML isso é um tema que ainda tem muito a amadurecer, dado que grande parte dos Data Scientists não têm um background programação e versionamento de código fonte. Outro ponto que dificulta bastante é que no fluxo de trabalho de cientistas de dados muitas das ferramentas usadas, tornam a revisão de código que impossível (e.g. Knit para o R) e Jupyter Notebook para Python.
  • Glue code: Nesta categoria eu coloco os códigos que fazemos no momento da prototipação e do MVP que vai para produção da mesma forma que foram criados. Uma coisa que já vi acontecer bastante neste sentido foi ter aplicações com dependências de inúmeros pacotes e que para ter uma “integração” mínima necessitavam de muito glue code. O código ficava tão frágil que uma mudança na dependência (ex: uma simples atualização do código fonte) quebrava praticamente toda a API em produção.

Um cenário de indisponibilidade em um sistema de ML

Vamos imaginar que a uma empresa financeira fictícia chamada “Leyman Brothers” teve uma indisponibilidade na qual a sua plataforma de trading de ações ficou indisponível por 6 horas causando perdas massivas em alguns investidores.

Após a construção de um devido Post-Mortem o time chegou à seguinte narrativa em relação aos fatores determinantes e contribuintes na indisponibilidade:

O motivo da indisponibilidade foi devido a um erro do tipo falta de memória devido a um bug na biblioteca de ML.

Este erro é conhecido pelos desenvolvedores da biblioteca e existe um ticket aberto sobre o problema desde 2017, mas que até o presente momento não teve solução (Condição Latente).

Outro aspecto verificado foi que o tempo de resposta e solução foi demasiadamente longo devido ao fato de que não haviam mecanismos de alarmística, heartbeating ou monitoramento na plataforma de ML. Dessa forma, sem as informações de diagnóstico, o problema levou mais tempo do que o necessário para ser corrigido (Condição Latente).

No momento do debugging foi verificado que o desenvolvedor responsável pela implementação do trecho de código em que aconteceu a origem do erro, tinha conhecimento das alternativas de correção, mas não o fez devido ao fato de que a correção levaria a implementação de outra biblioteca em uma linguagem de programação a qual ele não têm domínio; mesmo com esta linguagem já sendo utilizada em outras partes do stack de tecnologia (Falha Ativa). 

Por fim, foi visto também que o código entrou diretamente em produção sem nenhum tipo revisão. O projeto no Github não possui nenhuma “trava” para impedir que códigos não revisados entrem em produção. (Falha Ativa devido à Condição Latente).

Transpondo o evento da narrativa para o modelo de Queijo Suíço, visualmente teríamos a seguinte imagem:

No nosso Queijo Suíço cada uma das fatias seriam camadas ou linhas de defesa em que temos aspectos como a arquitetura e engenharia dos sistemas, o stack de tecnologia, os procedimentos específicos de desenvolvimento, a cultura de engenharia da empresa e por fim as pessoas como última salvaguarda.

Os furos por sua vez seriam os elementos falhos em cada uma destas camadas de defesa que podem ser falhas ativas (ex: dar commit direto na master pelo fato de hão haver Code Review) ou condições latentes (e.g. biblioteca de ML, falta de monitoramento e alarmística).

Em uma situação ideal, após um evento de indisponibilidade, todas as condições latentes e as falhas ativas seriam endereçadas e haveria um plano de ação para a solução dos problemas para que o mesmo evento nunca mais acontecesse no futuro

Apesar da narrativa de alto nível, o ponto principal é que indisponibilidades em sistemas complexos e dinâmicos nunca acontecem devido a um fator isolado, mas sim devido à conjunção e sincronização de condições latentes e falhas ativas.

CONSIDERAÇÕES FINAIS

Claro que não existe panaceia em relação ao que pode ser feito em termos de gestão de riscos: alguns riscos e problemas podem ser tolerados e muitas das vezes não existe o tempo e os recursos necessários para aplicação dos devidos ajustes.

Entretanto, quando falamos de sistemas de missão crítica que usam ML fica claro que existem uma miríade de problemas específicos que podem acontecer além dos naturais problemas de engenharia.

O modelo do Queijo Suíço é um modelo de gerenciamento de riscos que é muito utilizado na aviação e oferece uma maneira simples de elencar condições latentes e falhas ativas em eventos que possam levar a falhas catastróficas.

O entendimento dos fatores contribuintes e determinantes em eventos de falha, pode ajudar a eliminar ou minimizar potenciais riscos e consequentemente reduzir o impacto na cadeia de consequências estes eventos.

NOTAS

[N1] – O objetivo deste post é única e exclusivamente comunicar com times de Machine Learning Engineering, Data Science, Data Product Management e demais áreas que tenham realmente a cultura de melhoria e feedback contínuo. Se você e/ou a sua empresa entende que conceitos de qualidade, robustez, confiabilidade e aprendizado são importantes, este post é dedicado especialmente a vocês.

[N2] No momento em que esse artigo estava sendo revisado apareceu essa matéria do novo avião Boeing 787 que devido ao fato de que o sistema core não consegue eliminar dados obsoletos (flush de dados) de algumas informações de sistemas críticos do avião que afetam a aeronavegabilidade, e que por isso a cada 51 dias todos os aviões deste modelo devem ser desligados. Isso mesmo, um avião Boeing precisa do mesmo tipo de reboot ao estilo “já tentou desligar a sua máquina e religar novamente?” para que um evento catastrófico não ocorra. Mas isto mostra que mesmo com uma condição latente é possível operar um sistema complexo de maneira segura.

[N3] Cultura de Gambiarras + eXtreme Go Horse (XGH) + Jenga-Oriented Architecture = Usina de indisponibilidades

[N4] – Agradecimentos especiais ao Comandante Ronald Van Der Put do canal Teaching for Free pela gentileza em me ceder alguns materiais relacionados à segurança e prevenção de acidentes.

REFERÊNCIAS

Reason, James. “The contribution of latent human failures to the breakdown of complex systems.” Philosophical Transactions of the Royal Society of London. B, Biological Sciences 327.1241 (1990): 475-484.

Reason, J. “Human error: models and management.” BMJ (Clinical research ed.) vol. 320,7237 (2000): 768-70. doi:10.1136/bmj.320.7237.768

Morgenthaler, J. David, et al. “Searching for build debt: Experiences managing technical debt at Google.” 2012 Third International Workshop on Managing Technical Debt (MTD). IEEE, 2012.

Alahdab, Mohannad, and Gül Çalıklı. “Empirical Analysis of Hidden Technical Debt Patterns in Machine Learning Software.” International Conference on Product-Focused Software Process Improvement. Springer, Cham, 2019.

Perneger, Thomas V. “The Swiss cheese model of safety incidents: are there holes in the metaphor?.” BMC health services research vol. 5 71. 9 Nov. 2005, doi:10.1186/1472-6963-5-71

“Hot cheese: a processed Swiss cheese model.” JR Coll Physicians Edinb 44 (2014): 116-21.

Breck, Eric, et al. “What’s your ML Test Score? A rubric for ML production systems.” (2016).

SEC Charges Knight Capital With Violations of Market Access Rule

Blog da Qualidade – Modelo Queijo Suíço para analisar riscos e falhas.

Machine Learning Goes Production! Engineering, Maintenance Cost, Technical Debt, Applied Data Analysis Lab Seminar

Nassim Taleb – Lectures on Fat Tails, (Anti)Fragility, Precaution, and Asymmetric Exposures

Skybrary – Human Factors Analysis and Classification System (HFACS)

CEFA Aviation – Swiss Cheese Model

A List of Post-mortems

Richard Cook – How Complex Systems Fail

Airbus – Hull Losses

Number of flights performed by the global airline industry from 2004 to 2020

Machine Learning e o modelo de queijo suíço: falhas ativas e condições latentes

Funções com Multiprocessing para processamento de textos

Quem acompanhou o post A small journey in the valley of Natural Language Processing and Text Pre-Processing for German language acompanhou um pouco dos desafios de modelar um classificador de textos em alemão.

No entanto uma coisa que me salvou na parte de pre-processing foi que eu praticamente usei o multiprocessing para paralelizar o pré-processamento na coluna de texto e isso me salvou um tempo incrível (relembrando: eu tinha 1+ milhão de registros de texto, com 250 palavras média por registro (com um desvio padrão de 700, tudo isso usando biblioteca interna).

import time
import numpy as np
import pandas as pd
import nlp_pre_processing
# An internal NLP lib to process text
nlp = nlp_pre_processing_library.NLPPreprocessor()
# Multiprocessing library that uses pool
# to distribute the task for all processors
from multiprocessing import Pool
print(f'Start processing…: {(time.time() start_time)}')
# Tracking the time
start_time = time.time()
# Number of partitions that
# the Pandas Dataframe will be
# splited to parallel processing
num_partitions = 20
# Number of cores that will be used
# more it's better
num_cores = 16
print(f'Partition Number: {num_partitions} – Number of Cores: {num_cores}…')
def main_process_pipeline(df, func):
"""
Function that will split the dataframe
and process all those parts in a n number
of processors
Args:
df (Pandas dataframe): Dataframe that will be splited
func (function): Python function that will be executed in parallel
Returns:
df: Dataframe with all parts concatenated after the function be applied
"""
df_split = np.array_split(df, num_partitions)
pool = Pool(num_cores)
df = pd.concat(pool.map(func, df_split))
pool.close()
pool.join()
return df
def pre_process_wrapper(df):
""" Will take the Dataframe and apply a function using lambda"""
df['text'] = df['text'].apply(lambda text: nlp.pre_processing_pipeline(text))
return df
# Unite the Dataframe and the Wrapper
processed_df = main_process_pipeline(df, pre_process_wrapper)
print(f'Processing finished in seconds: {(time.time() start_time)}')
view raw multiprocessing.py hosted with ❤ by GitHub

É isso: Simples e tranquilo.

Funções com Multiprocessing para processamento de textos