Machine Learning · Insper

MLOps

Um modelo num notebook não gera valor nenhum

Empacotamento, serving, versionamento, monitoramento e retreino, cada um medido

Esta aula começa onde as outras terminaram

  • Até aqui, toda aula terminava quando o escore de teste era impresso.
  • O que gera valor são predições entregues de forma confiável a quem age com base nelas.
  • MLOps (machine-learning operations): os hábitos de engenharia que levam um pipeline ajustado para produção e o mantêm confiável depois que ele chega lá.
O lab acompanha o modelo de telemarketing de um banco, treinado com ligações de 2008–2009, nas ligações de 2009–2010, quando a fração de clientes que disseram sim passou de cerca de 7% para mais de 50%. Drift real, não simulado.

O modelo é a caixa pequena

Sculley et al. (2015), Hidden Technical Debt in Machine Learning Systems: o código que aprende é uma caixinha cercada de infraestrutura e a maioria das falhas acontece na infraestrutura.

212
linhas de código no lab desta aula
4 + 5
chamam fit + definem o modelo

O resto carrega, verifica, empacota, serve, monitora e compara.

O laço e o que o monitor manda de volta

Clique nos botões: o que um alerta de drift manda de volta? E um bug de dados?

Cada seta pode quebrar sem erro nenhum

etapafalha em silêncio quando...
empacotaro arquivo carrega em outra versão da biblioteca e prediz diferente
serviruma requisição malformada ainda recebe uma probabilidade com cara de confiável
treinar de novoninguém sabe dizer que dados, código e semente produziram o modelo em produção
monitoraro mundo muda e o modelo continua respondendo como se não tivesse mudado
servir vs treinaro código de serving calcula um atributo de forma diferente do código de treino

O caminho desta aula

  1. Empacotar o modeloo pipeline inteiro, riscos do pickle, versões fixas, skops
  2. Servir prediçõesbatch ou online, checagem de schema, endpoint, latência
  3. Reproduzir e versionarsementes, impressão digital dos dados, registro de experimentos
  4. Monitorar o driftmedir a mudança, rótulos atrasados, retreino
  5. Mudar com segurançadesalinhamento treino–serving, laços de realimentação, troca segura, maturidade
Cada etapa responde a uma pergunta: o que exatamente vai para produção? → como uma requisição chega ao modelo com segurança? → conseguiríamos reconstruir o modelo que está rodando? → como perceber que o mundo mudou? → o que mais quebra e como trocar um modelo com segurança?
Etapa 1 · o que exatamente vai para produção?

Empacotamento: o artefato é o pipeline inteiro

Crédito alemão (1.000 empréstimos passados): 3 colunas numéricas e 5 categóricas → scaler + one-hot → regressão logística.


joblib.dump(pipe, path)                  # o pipeline INTEIRO, um arquivo
loaded = joblib.load(path)
np.array_equal(proba, loaded.predict_proba(X_test)[:, 1])
  

AUC de teste

0,783
área sob a curva ROC

no disco

4.721
bytes

ida e volta

True
idêntico bit a bit

Por que não distribuir só o modelo?

Preveja. Tire a LogisticRegression ajustada de dentro do pipeline e dê a ela a tabela crua. O que acontece?

ValueError: X has 8 features, but LogisticRegression is expecting 30 features as input.
  
  • O modelo viu as 30 colunas do column transformer: 3 números padronizados + 27 indicadores one-hot.
  • Servi-lo sozinho exige reconstruir essas 30 colunas à mão: mesmas médias, mesma ordem de categorias, mesmos desconhecidos. Qualquer diferença produz predições erradas sem erro nenhum.
Distribua o pipeline: o pré-processamento vira parte do artefato.

Um pickle é um programa


class Innocent:
    def __reduce__(self):                 # diz ao pickle como reconstruir este objeto...
        return (print, ('this line ran while the file was being LOADED',))

obj = pickle.loads(pickle.dumps(Innocent()))   # ...e o carregamento obedece
  

this line ran while the file was being LOADED
  
Troque print por os.system e o carregamento roda um comando de shell. Carregue arquivos de modelo apenas de fontes em que você confiaria para rodar código na sua máquina.

Fixe as versões

O scikit-learn carimba sua versão em todo estimador que salva. Forjamos um arquivo de outra versão trocando um dígito do carimbo:


InconsistentVersionWarning for: ['StandardScaler', 'OneHotEncoder',
  'ColumnTransformer', 'LogisticRegression', 'Pipeline']
  

Todo estimador reclama e ainda assim o arquivo carrega. Ele pode predizer diferente.

  • guarde as versões (Python, scikit-learn, NumPy) ao lado do modelo, num arquivo de metadados;
  • fixe-as no ambiente de serving (requirements.txt, uma imagem de contêiner);
  • ao atualizar a biblioteca, retreine e revalide.

Um formato mais seguro: skops

Ao carregar, o skops só reconstrói tipos de uma lista permitida; qualquer outro tipo precisa ser declarado confiável explicitamente.

pipelinetipos não confiáveiscarregando sem confiar
scaler + one-hot + logística[]carrega, probabilidades idênticas
com uma função própria['__main__.cap_amount']UntrustedTypesFoundException

Fora do Python, o ONNX (Open Neural Network Exchange, via skl2onnx) converte um pipeline ajustado num grafo portátil para runtimes em C++, Java ou JavaScript.

Etapa 2 · como uma requisição chega ao modelo com segurança?

Serving: como o modelo é chamado

padrãolatênciaexemplo
batchhorasescores de churn gravados num banco de dados toda noite
online (API)milissegundosdecisão de crédito durante o checkout
streamingsegundosalertas de fraude num fluxo de transações
edgeno dispositivoprevisão de teclado no celular
Prefira batch sempre que a decisão puder esperar: mais barato, mais fácil de monitorar e mais fácil de rodar de novo.

O pipeline aceita lixo

Preveja. Uma requisição boa e quatro quebradas. Quantas das quebradas levantam erro?
requisiçãoresposta
requisição boaP(bad) = 0,626
idade = −3P(bad) = 0,784
valor em milharesP(bad) = 0,574
propósito desconhecido 'crypto'P(bad) = 0,519
housing ausenteValueError: columns are missing

Só uma. As outras três recebem uma probabilidade que parece tão confiável quanto a da boa.

Um schema e uma função que o verifica

Registre o que o modelo viu no treino (uma faixa por número, uma lista por categoria) e confira cada requisição antes de predizer:

requisiçãostatusmensagem
requisição boaok0,626
idade = −3rejeitadaage: −3 outside training range [19, 75]
valor em milharesrejeitadacredit_amount: 4 outside training range [250, 15945]
propósito desconhecidorejeitadapurpose: unknown category 'crypto'
housing ausenterejeitadahousing: missing
Responda com a versão do modelo, para que toda resposta registrada possa ser rastreada. Rejeite valores impossíveis; avise (prediga, mas registre) para valores que são apenas novos.

A mesma função atrás de um endpoint HTTP


# app.py  (ilustrativo: rode com uvicorn, não num notebook)
from fastapi import FastAPI
from service import predict_one           # a função testada

app = FastAPI()

@app.post('/predict')
def predict(payload: dict):
    return predict_one(payload)
  
A camada web não tem nenhuma decisão. Tudo o que pode estar errado está em predict_one, uma função comum que você testa num notebook.

Latência: paga-se por chamada, não por linha

Preveja. Pontuar uma linha por chamada ou 10.000 linhas numa só chamada: quão diferente é o custo por linha?
O tempo por chamada fica plano em cerca de 1,7 ms de 1 até cerca de 1.000 linhas e depois cresce; o custo por linha cai de 1,7 ms para 0,0006 ms

Na máquina que gerou a página: 1,724 ms/linha uma de cada vez, 0,0006 ms/linha num só lote, cerca de 2.700×. Uma chamada custa cerca de 1,7 ms quer carregue uma linha, quer mil: trabalho fixo, não aritmética.

Etapa 3 · conseguiríamos reconstruir o modelo que está rodando?

Reprodutibilidade: cada ingrediente tem uma versão

ingredienteo que registrarferramenta típica
códigoo commit que o treinougit
dadosuma impressão digital e um jeito de recuperar as linhasDVC, lakeFS, snapshots
ambienteversões das bibliotecaslock files, um contêiner
aleatoriedadecada sementerandom_state
configuraçãohiperparâmetros, atributos, limiaresum arquivo de configuração no git
resultadosmétricas, o artefatorastreador, registro de modelos

Sementes: mesmo escore, decisões diferentes

Duas random forests que diferem só na semente:

AUC

0,763 · 0,766
parecem o mesmo modelo

probabilidades

96%
diferem

decisões

18 de 250
mudam
"Por que fui recusado ontem e aprovado hoje?" merece uma resposta melhor do que "rodamos o script de novo". Fixe a semente; nunca teste sementes até o escore de teste ficar o melhor possível.

A impressão digital dos dados


training data      68ad024c9661
one value changed  7eb64a1901f0
rows shuffled      a662547f0332
same data again    68ad024c9661
  
  • Um único valor alterado entre 6.000 dá uma impressão digital completamente diferente; outra ordem das linhas também.
  • "Foi treinado com a extração deste mês?" vira uma comparação de strings.

Um hash apenas identifica os dados; reconstruir exige as próprias linhas (DVC, lakeFS, snapshots somente leitura).

Um registro de experimentos: uma linha por execução, nunca editada

dadosCAUC na VCdp na VC
68ad024c96610,010,74950,0292
68ad024c96610,100,75990,0224
68ad024c96611,000,75220,0244
68ad024c966110,000,74420,0230

VC: validação cruzada; dp: desvio padrão entre folds.

A vantagem de C = 0.1 sobre C = 1 (0,0077) fica muito abaixo dos desvios padrão entre folds (cerca de 0,02): trate-as como empatadas. Guarde as execuções perdedoras: elas registram quantas coisas foram tentadas.
Etapa 4 · como perceber que o mundo mudou?

Monitoramento: modelos se degradam em silêncio

O serviço continua respondendo 200 OK enquanto suas predições pioram. Três camadas: sistema, qualidade dos dados, drift estatístico.

mudançao que mudao que fica fixoexemplo
covariate shift\(P(x)\)\(P(y \mid x)\)o banco vende para clientes mais jovens
concept drift\(P(y \mid x)\)\(P(x)\)uma crise muda quem paga
prior shift\(P(y)\)\(P(x \mid y)\)uma epidemia: mais pacientes doentes

Medindo uma mudança: o PSI, à mão

PSI: índice de estabilidade populacional (population stability index).

\[ \text{PSI} = \sum_{i=1}^{B} (a_i - e_i)\,\ln\frac{a_i}{e_i} \]
faixa\(e_i\)\(a_i\)\(\ln(a_i/e_i)\)termo
10,250,10−0,9160,1374
20,250,20−0,2230,0112
30,250,30+0,1820,0091
40,250,40+0,4700,0705
PSI0,228

Faixas cortadas nos quartis do treino. Todo termo é positivo.

< 0,1 nenhuma mudança relevante · 0,1–0,25 merece atenção · > 0,25 significativa (Siddiqi, 2006)

O que quer dizer PSI = 0,25?

Preveja. Um atributo normal cuja média se desloca. Quanto ela precisa se mover para o PSI chegar ao nível de alarme 0,25?
média deslocada em00,1 dp0,25 dp0,5 dp1 dp
PSI (10 faixas)0,0050,0150,0640,2500,929

O nível de alarme é um deslocamento de meio desvio padrão (dp). Sem deslocamento nenhum, o PSI não é exatamente zero: duas amostras nunca têm histogramas idênticos.

Simulador: PSI faixa a faixa

Preveja. Ligue o concept drift sem mexer nas entradas. O que acontece com o PSI e com a acurácia?

Medindo uma mudança: o teste KS

À esquerda: frações da referência e dos dados atuais em 10 faixas de decis, PSI 0,25; à direita: as duas distribuições acumuladas com o maior salto D = 0,21 marcado

KS: Kolmogorov–Smirnov.

\[ D = \max_x \left| F_{\text{ref}}(x) - F_{\text{cur}}(x) \right| \]

À mão, 1–5 contra 3–7: \(D = 0{,}4\), \(p = 0{,}873\): nada surpreendente com cinco pontos de cada lado.

deslocamento de 0,02 dp, n =1.00010.000100.0001.000.000
p-valor do KS0,0970,140,000491,6e-41
PSI0,03440,00180,00040,0005
Com lotes grandes, o p-valor sempre diz "diferente": dispare alertas por magnitude (PSI ou \(D\)). Com muitos atributos, corrija para testes múltiplos.

As três mudanças, medidas

Preveja. Uma das três mudanças é invisível para todo monitor de entrada. Qual?
PSI(x)p do KSacuráciaprecisãotaxa realmédia prevista
sem mudança0,0040,6950,8160,7690,4620,465
covariate shift3,5050,0000,7280,7290,7200,780
concept drift0,0030,3400,6600,3290,1670,459
prior shift0,1520,0000,7940,4910,2000,339
Quatro painéis: sem mudança, covariate shift, concept drift e prior shift, cada um com a distribuição de x, a curva de probabilidade verdadeira e a curva do modelo

Os rótulos chegam atrasados: monitorar em duas velocidades

imediatamente

verificações de qualidade dos dados, PSI ou KS de cada entrada e a distribuição dos próprios escores do modelo: um dos detectores de mudança mais eficazes, por uma linha (Rabanser et al., 2019)

depois, com os desfechos

as métricas de verdade (AUC, precisão, calibração) por lote, contra a estimativa de validação

O churn se sabe depois de 60 dias; um calote, depois de meses. Um empréstimo recusado nunca ganha histórico de pagamento: uma pequena amostra auditada e rotulada é a única proteção contra concept drift.

Degradar, detectar, retreinar

A população se move 0,3 dp por mês. Modelo estático vs um modelo retreinado sempre que o PSI da sua entrada passa de 0,25.

mêsestáticomonitoradomelhor possível
00,8010,8010,802
2 (alerta)0,7800,7800,785
60,7590,8140,830
80,6540,8220,816
120,2730,8490,850
O PSI da entrada do modelo estático passa de 4; a acurácia estática cai de 0,80 para 0,27, enquanto o modelo retreinado acompanha a melhor acurácia possível
O primeiro alerta dispara quando o modelo estático ainda vai bem: drift não é dano. E retreinar exige rótulos: com 60 dias de atraso, o modelo monitorado fica dois meses atrás.
Etapa 5 · o que mais quebra e como trocar um modelo com segurança?

Desalinhamento treino–serving, medido

Um novo aplicativo cliente manda um campo em outra unidade. O conjunto de teste, de três jeitos:

AUCsinalizadosdecisões mudadasrejeitados pelo schema
como no treino0,78320,0%0,0%1%
valor em milhares0,77514,8%5,2%100%
duração em anos0,7512,8%17,2%93%
O AUC quase não se mexe; as decisões, sim. O schema pega os dois bugs. Outras fontes: pré-processamento reimplementado em SQL ou Java, janelas de tempo diferentes, atributos que conhecem o futuro.

Colocando um modelo novo no ar com segurança

Primeiro um portão de avaliação: vencer o modelo atual em dados recentes e rotulados que nenhum dos dois viu, por mais do que o ruído. Depois, liberar em etapas:

estratégiacomoprotege de
shadowpontua o tráfego real, respostas só registradasquedas, latência, escores estranhos
canary1–5% do tráfegoproblemas que só aparecem com usuários reais
teste A/Bdivisão aleatória, uma métrica de negócio"AUC melhor" que não é resultado melhor
rollbacko artefato anterior fica prontotodo o resto, em minutos
Laços de realimentação: o modelo de crédito só vê o pagamento dos empréstimos que aprovou. Mantenha um tráfego de holdout decidido sem o modelo.

Testes, documentação e maturidade

O ML Test Score

28 testes em 4 grupos: dados, desenvolvimento do modelo, infraestrutura, monitoramento (Breck et al., 2017). A nota é o mínimo entre os grupos.

Model card

uso pretendido, fatores, métricas, dados de treino e de avaliação, considerações éticas, ressalvas (Mitchell et al., 2019)

Uma escada sensata

  1. script + artefatos versionados
  2. CI/CD (integração e entrega contínuas): testes antes de qualquer modelo ir para produção
  3. logs, painéis, alertas de drift
  4. retreino com portões e rollback

Suba quando a dor justificar.

Lab · poucos segundos de computação

Produção e monitoramento de um modelo de marketing bancário

45.211 ligações de telemarketing, de maio de 2008 a novembro de 2010, na ordem em que foram feitas. Implantado em 1º de maio de 2009, acompanhado por 19 meses.

Construir e validar (1–3), empacotar e proteger (4–6), monitorar (7–9), retreinar (10), escrever o model card (11).

O PSI do escore salta para acima de 0,8 a partir do segundo trimestre: o monitor isolado mais barato e mais útil.

Notebook do lab (Jupyter ou Colab, Arquivo → Fazer upload de notebook).

Por trimestre: a taxa real de contratação sobe para acima de 50% enquanto o modelo estático prevê cerca de 16% e o modelo retreinado acompanha a taxa

Resumo

  1. Distribua o pipeline inteiro, confira a ida e volta, fixe as versões; um pickle é um programa.
  2. Ponha um schema na frente do modelo e responda com a versão dele.
  3. Versione código, dados (um hash), ambiente, sementes e resultados.
  4. Monitore entradas e escores na hora, desfechos depois; PSI para magnitudes.
  5. Drift não é dano; concept drift exige rótulos.
  6. Modelos novos passam por um portão e são liberados em etapas, com rollback pronto.