Leia a Parte II da história aqui.

No Nubank, confiamos fortemente no aprendizado de máquina para tomar decisões escalonáveis e baseadas em dados. Embora existam muitas outras bibliotecas de aprendizado de máquina por aí (usamos muito Xgboost, LGBM e ScikitLearn, por exemplo), sentimos a necessidade de uma abstração de nível mais alto que nos ajudasse a aplicar essas bibliotecas mais facilmente aos problemas que enfrentamos. A fklearn agrupa essas bibliotecas com eficiência em um formato que torna seu uso mais eficaz na produção.

Atualmente, a fklearn capacita um amplo conjunto de modelos de aprendizado de máquina no Nubank, resolvendo problemas que vão desde pontuação de crédito até respostas automatizadas de chat de suporte ao cliente. Nós a criamos com os seguintes objetivos em mente:

  1. A validação deve refletir situações reais
  2. Os modelos de produção devem corresponder a modelos validados
  3. Os modelos devem estar prontos para a produção com poucas etapas extras
  4. A reprodutibilidade e a análise detalhada de resultados do modelo devem ser fáceis de alcançar.

Logo no início, decidimos que a programação funcional seria uma aliada poderosa na tentativa de atingir esses objetivos.

F de Funcional

Aqui no Nubank, somos grandesfãs da programação funcional, e isso não se limita à divisão de Engenharia. Mas como a programação funcional ajuda os cientistas de dados?

O aprendizado de máquina muitas vezes é feito usando código Python orientado a objetos, e é assim que costumávamos fazer no Nubank também. Naquela época, o processo de construção de modelos de aprendizado de máquina e sua colocação em produção era cansativo, e muitas vezes cheio de bugs. Implementávamos um modelo só para descobrir que as previsões feitas na produção não correspondiam ao que era visto durante a validação. Pior ainda, a validação muitas vezes era impossível de reproduzir, sendo frequentemente feita em Jupyter Notebooks com regras stateful.

A programação funcional ajuda a corrigir esses problemas:

  • facilitando a construção de pipelines onde as transformações de dados que acontecem durante o treinamento correspondem aos modelos em produção;
  • permitindo iterações mais seguras em ambientes interativos (por exemplo, Jupyter Notebooks), evitando erros causados por código stateful e tornando a pesquisa mais reproduzível;
  • permitindo-nos escrever códigos muito genéricos de validação, ajuste e seleção de recursos que funcionam em vários tipos de modelos e aplicativos, tornando-nos mais eficientes em geral.

Vejamos um exemplo para ver como a programação funcional faz isso na prática. Digamos que estamos tentando prever quanto alguém gastará no cartão de crédito com base em duas variáveis: renda mensal e valor da fatura anterior. Como o resultado deste modelo será usado para tomadas de decisão sensíveis, gostaríamos de ter certeza de que ele é robusto em relação a valores discrepantes nas variáveis de entrada, e é por isso que decidimos:

  1. Limitar a renda mensal para 50.000, uma vez que a renda é declarada pelo próprio cliente, e às vezes exagerada.
  2. Limitar a faixa de saída do modelo ao intervalo [0, 20.000].

E então usar um modelo de regressão linear simples. O código fica assim:

from fklearn.training.pipeline import build_pipeline
from fklearn.training.regression import linear_regression_learner
from fklearn.training.transformation import capper, floorer, prediction_ranger

def fit(train_data):
    capper_fn = capper(columns_to_cap=["income"], precomputed_caps={"income": 50,000})
    regression_fn = linear_regression_learner(features=["income", "bill_amount"], target="spend")
    ranger_fn = prediction_ranger(prediction_min=0.0,   prediction_max=20000.0)
    
    learner = build_pipeline(capper_fn, regression_fn, ranger_fn)
    predict_fn, training_predictions, logs = learner(train_data)
    
    return predict_fn, logs

Não se assuste! Vamos percorrer o código passo a passo, explicando alguns conceitos importantes da fklearn.

Check our job opportunies

Funções de aprendizado

Enquanto no scikit-learn a abstração primária para um modelo é uma classe com métodos fit e transform; no fklearn usamos o que chamamos de função de aprendizagem (learner). Uma função de aprendizagem recebe alguns dados de treinamento (além de outros parâmetros), aprende algo com eles e retorna três coisas: uma função de previsão, os dados de treinamento transformados e um registro. As três primeiras linhas do nosso exemplo inicializam três funções de aprendizagem: capper, linear_regression_learner e prediction_ranger.

Para ilustrar melhor, aqui está uma definição simplificada de linear_regression_learner:

from typing import Any, Dict, List
from sklearn.linear_model import LinearRegression
from toolz import curry
import pandas as pd
 
@curry
def linear_regression_learner(df: pd.DataFrame,
                              features: List[str],
                              target: str,
                              params: Dict[str, Any] = None) -> LearnerReturnType:
 
   # initialize and fit the linear regression
   reg = LinearRegression(**params) 
   reg.fit(df[features].values, df[target].values)
 
   # define the prediction function
   def p(new_df: pd.DataFrame) -> pd.DataFrame:
       # note that `reg` here refers to the linear regression fit above, via the functions closure.
       return new_df.assign(prediction=reg.predict(new_df[features].values))
 
   # the log can contain arbitrary information that helps inspect or debug the model
   log = {'linear_regression_learner': {
       'features': features,
       'target': target,
       'parameters': params,
       'training_samples': len(df),
       'feature_importance': dict(zip(features, reg.coef_.flatten()))
   }
 
   return p, p(df), log

Observe o uso de variáveis tipadas! Elas ajudam a deixar a programação funcional em Python menos complicada, especialmente em conjunto com a biblioteca toolz, que é extremamente útil.

Como mencionamos, uma função de aprendizagem retorna três coisas (uma function, um DataFrame e um dictionary), conforme descrita pela definição LearnerReturnType:

from typing import Any, Callable, Dict, Tuple
import pandas as pd

LearnerReturnType = Tuple[PredictFnType, pd.DataFrame, LearnerLogType]
PredictFnType = Callable[[pd.DataFrame], pd.DataFrame]
LearnerLogType = Dict[str, Any]
  • A função de previsão sempre tem a mesma assinatura: ela pega um DataFrame e retorna um DataFrame (usamos Pandas). Ela deve ser capaz de receber qualquer novo DataFrame (desde que contenha as colunas necessárias) e transformá-lo (é equivalente ao método transform de um objeto do scikit-learn). Neste caso, a função de previsão simplesmente cria uma nova coluna com as previsões do modelo de regressão linear que foi treinado.
  • Os dados de treinamento transformados geralmente são apenas a função de previsão aplicada aos dados de treinamento. São úteis quando você deseja previsões em seu conjunto de treinamento ou para construir pipelines, como veremos depois.
  • O registro é um dicionário e pode incluir qualquer informação que seja relevante para inspecionar ou depurar a função de aprendizagem (por exemplo, quais recursos foram usados, quantas amostras havia no conjunto de treinamento, importância das características ou coeficientes).

As funções de aprendizagem mostram algumas propriedades comuns de programação funcional:

  • Elas são funções puras, o que significa que sempre retornam o mesmo resultado com a mesma entrada, e não têm efeitos colaterais. Na prática, isso significa que você pode chamar a função de aprendizado quantas vezes quiser sem se preocupar em obter resultados inconsistentes. Este nem sempre é o caso ao chamar fit em um objeto do scikit-learn, por exemplo, pois os objetos podem sofrer mutação.
  • Elas são funções de ordem superior, pois retornam outra função (a função de previsão). Como a função de previsão é definida dentro da própria função de aprendizagem, ela pode acessar variáveis no escopo da função de aprendizagem por meio de suaclausura.
  • Por terem assinaturas consistentes, as funções de aprendizagem (e funções de previsão) podem ser compostas. Isso significa que é fácil criar pipelines inteiros a partir delas, como veremos em breve.
  • Elas são passíveis de currying, o que significa que você pode inicializá-las em etapas, passando apenas alguns argumentos por vez (isso é o que realmente está acontecendo nas três primeiras linhas do nosso exemplo). Isso será útil ao definir pipelines e aplicar um único modelo a diferentes conjuntos de dados, ao mesmo tempo que obtém resultados consistentes.

Pode levar algum tempo para entender tudo isso, mas não se preocupe, pois não precisa ser um especialista em programação funcional para usar a fklearn de maneira eficaz. O principal é compreender que os modelos (e outras transformações de dados) podem ser definidos como funções seguindo a abstração da função de aprendizagem.

Pipelines

No entanto, os modelos de aprendizado de máquina raramente existem isolados. Ao focar apenas no modelo, os cientistas de dados tendem a esquecer quais transformações os dados estão passando antes e depois da parte de aprendizado de máquina. Muitas vezes, essas transformações precisam ser exatamente as mesmas durante o treinamento e a implantação de modelos, e os cientistas de dados podem tentar recriar manualmente suas etapas de pré e pós-processamento de treinamento na produção, o que leva a uma duplicidade de código que é difícil de lidar.

As funções de aprendizado podem ser compostas, o que significa que duas ou mais delas combinadas podem ser vistas como uma função nova e mais complexa. Isso significa que não importa quantas etapas você tenha em seu pipeline, seu modelo final se comportará da mesma forma que um único modelo, e fazer previsões será tão simples quanto chamar a função de previsão final em novos dados. Ter todas as etapas do pipeline de modelagem contidas em uma única função pura também ajuda na validação e no ajuste, pois podemos passá-lo para outras funções sem medo de efeitos colaterais.

Em nosso exemplo, nosso pipeline consiste em três etapas: limitar a variável de renda, executar a regressão e, em seguida, restringir a saída da regressão ao intervalo [0, 20.000]. Depois que cada função de aprendizagem é inicializada, criamos o pipeline e o aplicamos ao conjunto de treinamento usando estas duas linhas de código:

  ...
  learner = build_pipeline(capper_fn, regression_fn, ranger_fn)
  predict_fn, training_predictions, logs = learner(train_data)
  ...

A variável de aprendizagem agora contém o pipeline resultante da composição das três funções de aprendizagem, e é aplicada aos dados de treinamento para produzir a função de previsão final. Essa função aplicará todas as etapas equivalentes do pipeline aos dados de teste, conforme ilustra a imagem abaixo:

Exemplo de como os dados fluem através de um pipeline durante o treinamento e através de uma função de previsão durante a previsão. A própria função de previsão é retornada pelo pipeline; ela é a composição das três funções de previsão geradas por cada função de aprendizagem quando o pipeline foi chamado pela primeira vez nos dados de treinamento. Os registros são uma combinação dos registros provenientes de todas as funções de aprendizado no pipeline.

Qual é o próximo passo?

Vimos como modelos e etapas de transformação de dados podem ser escritos como funções de aprendizagem, e como pipelines funcionais na fklearn nos ajudam a garantir que as transformações feitas durante o treinamento e a validação correspondam às feitas na produção.

Na Parte II dessa postagem, falamos sobre validação e análise de modelo, e as ferramentas que a fklearn fornece para tornar essas etapas mais eficazes.

Enquanto isso, convidamos você a experimentar afklearn você mesmo! Não esperamos que a fklearn substitua os padrões atuais de aprendizado de máquina, mas esperamos que ele inicie conversas interessantes sobre os benefícios da programação funcional para aprendizado de máquina.

Check our job opportunies