MLOps é o termo da moda.
Nessa mistura entre Machine Learning e Operações, o que encontramos são times responsáveis por criar componentes de implantação de modelos de ciência de dados e de manter em pé aqueles que já estão sendo executados.
E com o perdão da expressão: é tiro, porrada e bomba.
Um time de MLOPs utiliza de conhecimentos que vão de Docker, API, webdev, Cloud, Segurança, Redes até, veja só, machine learning. Isso sem considerar a gigantesca conexão com engenharia de dados.
E nem quero falar de governança: modelos, dados, código, pessoas, processos, mudanças, testes.
Não existem respostas prontas, nem ferramentas que cubram todo o ciclo de vida de um modelo.
Pra colocar mais água no feijão, a primeira conferência mundial do tema foi em (pasmem!) 2019.
Colocar modelos em produção não é algo novo…
Mas, colocar modelos em produção de forma rápida como um serviço escalável é algo que estamos todos aprendendo.
Divaguei e até agora não expliquei o que quero fazer nesse post. Então vamos lá!
Vamos criar uma esteira de MLOps para servir um modelo a partir do zero com uma única condição: O modelo tem que estar em produção em todo momento, inclusive enquanto não temos todos os componentes da esteira.
Essa é a proposta!
Habemus dados
Vamos criar uma aplicação web (nome bonito pra site) em que o usuário se informa da chance dele ter alguma doença cardíaca.
Esse tema é totalmente aleatório.
Busquei alguma base de dados pública e encontrei o Heart Disease Data Set da UCI.
Com os dados na mão, vamos criar um modelo para estimar a chance de doença cardíaca. Vamos utilizar o pacote ranger do R criar um modelo sem qualquer feature engineering, já que o que queremos é um modelo em produção o mais rápido possível agora.
Ahh por quê não utilizar python?
No fim do post, eu explico.
O modelo
Para construir o modelo, usei apenas a variável age (idade) do banco de dados e treinei um random forest mantendo todos os parâmetros default.
Os dados foram armazenados no diretório data/ do projeto e o modelo foi salvo no diretório models/ serializado na extensão .RDS, padrão do R.
# modelling.R
library(ranger)
data <- read.csv(here::here("data/heart.csv"))
model = ranger::ranger(target ~ age, data = data)
saveRDS(model, "models/model.RDS")
Interagindo com o modelo
Em nenhum momento, disse que a aplicação web que iriamos montar não poderia ser um servidor FTP em que armazenaríamos o modelo para o usuário fazer download, abrir o R em sua máquina e executar um predict no modelo.
Mas, isso seria bem terrível! Reduziríamos nossa audiência a uma fração muito especifica. Nem se fala (horrível) da experiência que o usuário teria.
Para atingir um público maior e tornar a informação do modelo atrativa, precisamos de uma interface!
O R possui o pacote shiny
para construção de web apps, o qual tem como grandes atrativos no nosso caso:
-
A abstração de todo código html, css, js envolvido nessas aplicações: tudo é código R
-
O encapsulamento do frontend e backend em um único arquivo: simples execução.
De forma rápida, chegamos no seguinte código:
# app.R
library(shiny)
library(ranger)
ui <- fluidPage(
titlePanel("Heart Disease Predictor"),
sidebarLayout(
sidebarPanel(
numericInput(inputId = "idade",
label = "idade",
value = 25,
min = 0,
max = 100,
step = 1)),
mainPanel(
wellPanel(textOutput("distPlot"))
)
)
)
server <- function(input, output) {
model <- readRDS(here::here("models/model.RDS"))
output$distPlot <- renderText({
prediction = predict(model, data = data.frame(age = input$idade))
prediction$predictions
})
}
options(shiny.port = 4200, shiny.host = "0.0.0.0")
shinyApp(ui = ui, server = server)
Em termos gerais, essa aplicação shiny tem os seguintes elementos:
- Um botão em que o usuário define sua idade;
- Um painel que retorna a chance de doença cardíaca predita pelo modelo;
- A aplicação sobe na porta 4200 da máquina que a executa.
Disponibilizando o modelo
Uma vez que temos o código da aplicação e o modelo que será servido por ela, são inúmeras as formas de disponibilizar essa solução.
- Gist no github
- Aplicação como um pacote R
- Compartilhando esse post com os amiguinhos
- Servidor FTP (de novo)
Em todos os casos, o usuário teria que conhecer sobre programação. E nosso objetivo é alcançar um público maior.
Então, por quê não servir nossa aplicação como um software?
Precisaremos de uma máquina com ip público e todos os softwares e pacotes necessários para executar nossa aplicação Shiny.
Como minha conta da AWS estava aberta, é lá mesmo que vai ser.
O produto primordial da AWS são suas máquinas virtuais (EC2) e eles oferecem instâncias até certo tamanho (memória e cpu) e configuração (sistema operacional) de graça!
Para conhecer mais sobre EC2 e como realizar essas configurações, recomendo o vídeo Amazon EC2 Basics & Instances Tutorial do Stephane Maarek.
O Importante é saber que temos uma máquina com ip público em que podemos executar nossa aplicação.
Mas…
Funciona em minha máquina
A EC2 que criamos não tem os softwares que necessários para executar o R, e para agravar, é uma máquina fedora
que consome bibliotecas diferentes da minha máquina local (Ubuntu).
Pensando em agilidade na entrega, seria penoso debugar a EC2 até conseguir compilar o R e buscar cada biblioteca o equivalente no fedora (g++, libcairo, xvfb).
Um solução prática, é executar um container Docker na EC2! Por pura coincidência, eu já tinha um dockerfile para instalar o R 4.0.2.
FROM ubuntu:18.04
LABEL org.label-schema.license="GPL-3.0" \
org.label-schema.vendor="mlworks" \
maintainer="Adelmo Filho <adelmo.aguiar.filho@gmail.com>"
# Assumir defaults nas instalações
ENV DEBIAN_FRONTEND=noninteractive
# Parâmetros da instalação
ENV R_VERSION_MAJOR=4
ENV R_VERSION_MINOR=0
ENV R_VERSION_PATCH=2
ENV CONFIGURE_OPTIONS="--with-cairo --with-jpeglib --enable-R-shlib --with-blas --with-lapack"
ENV RENV_VERSION 0.12.0
# Instalar dependências
RUN apt-get update && apt-get install -y --no-install-recommends \
apt-utils\
gfortran \
git \
g++ \
libreadline-dev \
libx11-dev \
libxt-dev \
libpng-dev \
libjpeg-dev \
libcairo2-dev \
libcurl4-openssl-dev \
libssl-dev \
libxml2-dev \
libudunits2-dev \
libgdal-dev \
libbz2-dev \
libzstd-dev \
liblzma-dev \
libpcre2-dev \
locales \
openjdk-8-jdk \
screen \
texinfo \
texlive \
vim \
wget \
xvfb \
&& rm -rf /var/lib/apt/lists/*
# Ajuste o locale
RUN locale-gen pt_BR.UTF-8
ENV LANG pt_BR.UTF-8
ENV LANGUAGE pt_BR.UTF-8
ENV LC_ALL pt_BR.UTF-8
# Instalar R
RUN wget https://cran.rstudio.com/src/base/R-${R_VERSION_MAJOR}/R-${R_VERSION_MAJOR}.${R_VERSION_MINOR}.${R_VERSION_PATCH}.tar.gz && \
tar zxvf R-${R_VERSION_MAJOR}.${R_VERSION_MINOR}.${R_VERSION_PATCH}.tar.gz && \
rm R-${R_VERSION_MAJOR}.${R_VERSION_MINOR}.${R_VERSION_PATCH}.tar.gz
WORKDIR /R-${R_VERSION_MAJOR}.${R_VERSION_MINOR}.${R_VERSION_PATCH}
RUN ./configure ${CONFIGURE_OPTIONS} && \
make && \
make install
# Instalar renv
RUN R -e "install.packages('remotes', repos = c(CRAN = 'https://cloud.r-project.org'))"
Para construir a imagem base, basta executar o comando docker build
no diretório do arquivo dockerfile.
docker build -t adelmofilho/r-base:4.0.2 .
Em cima da imagem adelmofilho/r-base:4.0.2
vamos construir a imagem Docker de nossa aplicação.
FROM adelmofilho/r-base:4.0.2
WORKDIR /app
COPY renv.lock renv.lock
RUN R -e 'renv::restore()'
COPY . .
CMD Rscript app/app
Em termos gerais, a imagem realiza os seguintes passos:
- Cria o diretório
app/
- Copia, para a imagem, o arquivo
renv.lock
que contém a lista de pacotes R utilizados para montar a aplicação. - Instala os pacotes R (equivalente ao
pip install -r requeriments.txt
do python) - Copia todos os arquivos do projeto para a imagem
- Executa a aplicação shiny
Executamos o build da imagem com o comando.
docker build -t adelmofilho/hearth:latest .
Finalmente, comprimimos a imagem adelmofilho/hearth:latest
em um arquivo de extensão .tar
.
docker save adelmofilho/hearth:latest > hearth.tar
E, enviamos a imagem Docker para a máquina EC2 via scp
.
scp -i "mlops.pem" hearth.tar ec2-user@ec2-18-230-130-46.sa-east-1.compute.amazonaws.com:~
A transferência levou alguns minutos por conta do tamanho da imagem (~ 2GB).
Configurando o servidor
Usando SSH, acessamos a máquina para realizar configurações adicionais.
ssh -i "mlops.pem" ec2-user@ec2-18-230-130-46.sa-east-1.compute.amazonaws.com
O boas-vindas da máquina confirma que entramos na EC2 que solicitamos.
Last login: Thu Sep 30 03:33:13 2020 from 231-21-286-64.dial-up.teleba.net.br
__| __|_ )
_| ( / Amazon Linux 2 AMI
___|\___|___|
https://aws.amazon.com/amazon-linux-2/
Inicialmente, vamos instalar o docker e criar um container com nossa imagem.
sudo yum install -y docker
sudo service docker stop
sudo service docker start
sudo docker load --input hearth.tar
sudo docker run -p 80:4200 hearth
Da forma que foi executado, o container executa a aplicação na porta 4200 e a máquina host mapeia essa porta para a 80 (lembre-se de abrir o firewall da EC2 para conexões inbound http).
Acesse o endereço http://ec2-18-230-130-46.sa-east-1.compute.amazonaws.com/
para interagir com nossa aplicação!!
E agora?
Agora, não temos MLOPs.
O que fizemos foi uma enorme gambiarra recheada de débitos técnicos.
Débito técnico literalmente quer dizer: custo de retrabalho por ter sido adotada uma solução fácil, leia-se, frágil.
Vamos listar, então, quais seriam os débitos técnicos ou aprimoramentos que está aplicação precisaria solucionar para que entrasse em produção tendo um time envolvido em seu desenvolvimento.
- Não utilizamos qualquer ferramenta para versionamento de dados, códigos, nem modelos;
- Todo processo de implantação de atualizações da aplicação é manual;
- Não existem testes para validar se alterações no código quebram a aplicação;
- Não existe estrategia para colaboração no código do modelo, da aplicação ou da conta da AWS
- Não é realizado nenhum monitoramento do uso do modelo, de seu comportamento e dos recursos por ele consumidos.
- A infraestrutura de deploy não leva em conta aspectos de escalabilidade ou de disponibilidade
- As credenciais da instância EC2 foram entregues ao usuário sem controles de segurança do uso e compartilhamento;
- Não foi realizada nenhuma análise de custos;
- A modelagem não levou em consideração aspectos de cross-validação, feature engineering, nem qualquer outra experimentação.
- Não existe qualquer documentação do que foi realizado (com exceção desse post);
- Toda configuração do código ou aplicação foi realizada hard-coded;
- Não existe qualquer controle de reproduzibilidade do modelo implantado.
E se quiséssemos, poderiamos encontrar muitos outros pontos.
O paper da Google - Hidden Technical Debt in Machine Learning Systems - descreve em detalhes esses e outras dezenas de débitos técnicos importantes de serem visíveis em projetos de ML.
Tá. Mas, e agora?
Agora, vamos encerrar esse post.
Mas, esse trabalho não acaba por aqui. Nos próximos posts vamos atacar cada um desses pontos de melhoria até obtermos uma esteira MLOps com R.