Photo by Noémi Macavei-Katócz on Unsplash

Largando os cadernos: de notebooks à scripts .py

Como a Engenharia de Software me auxiliou a dar o próximo passo no desenvolvimento de modelos ML

Letícia Gerola
Published in
5 min readDec 6, 2022

--

Que os notebooks são indispensáveis na vida de um cientista de dados, isso é inegável. Seja localmente em um Jupyter Lab, através de um Google Colab ou mesmo dentro de estruturas Cloud, essa oitava maravilha da ciência é o que possibilita a visualização imediata do resultado da execução de um bloco de código, facilitando e muito a etapa de análise exploratória — e todo o desenvolvimento de um projeto de Machine Learning.

Na hora de produtizar esse modelo, no entanto, é que as coisas ficam complexas. Como o próprio nome já diz, estamos colocando esse modelo em produção, ou seja, tornando-o uma solução-produto que irá alimentar a empresa de previsões e insights. Em resumo, um software — que processa dados e contém modelos de Machine Learning — mas ainda sim, um software. E, quando o assunto é software, notebooks não se sustentam.

Orientação à objetos
Nada mais satisfatório, após desenvolver um modelo eficiente, do que vê-lo em ação sendo útil para seu propósito! Por isso, fui estudar como tornar minhas soluções (quase sempre, notebooks) soluções altamente produtizáveis e prontas para serem deployadas no ambiente que fosse necessário. Acaba sendo divertido: literalmente elevar o código a um próximo nível. Quando o modelo está feito em Python, a primeira coisa a ser feita é transformar os blocos de código desse notebook em algo orientado à objetos — ou seja, utilizando técnicas de construção de classes, métodos e atributos. Veja o exemplo abaixo que faz uma leitura de dados:

# atributing dataset to a dataframe df
df = pd.read_csv('FuelConsumptionCo2.csv')
df.head()

Seguindo a orientação à objetos, o bloco acima se torna algo nessa linha:

class Loader:

def load_data(self, url: str):
""" Carrega o arquivo e retorna um DataFrame.
:url: string com o nome/endereço do file
"""
return pd.read_csv(url)

# Instanciação das Classes
loader = Loader()

# Parâmetros
url_dados = ('FuelConsumptionCo2.csv')

# carga
data = loader.load_data(url_dados)

Fica outra coisa né? Além de muito organizado e elegante, eu consigo reutilizar a classe Loader para qualquer projeto que envolva a leitura de dados neste formato, ou mesmo reutilizar essa classe nesse mesmo projeto na hora de ler dados novos, por exemplo.

Decidi pegar um projeto antigo da Pyrentena, o modelo de previsão de emissão de carbono em veículos (que você pode conferir aqui a primeira versão) e transformá-lo em uma solução produtiva. Aproveitei outros recursos que aprendi desde que fiz esse projeto e adicionei uma parte de AutoML na solução, que automatizou a seleção do modelo testando uma série de opções diferentes pensando em, quem sabe, encontrar um resultado superior ao que eu já tinha.

Revisite seus códigos
Leva tempo, mas vale a pena! Com uma boa dose de estudos, refiz os código do notebook antigo em classes que executassem o que eu precisava, e o resultado foi muito interessante. No processo, ainda atualizei o modelo e melhorei sua performance! Na versão 1, eu cheguei em um modelo de Regressão Linear com um r2-score de 68%. Suficiente, mas nada sensacional.

Aproveitei a biblioteca TPOT, de AutoML, pra testar uma série de modelos automaticamente e verificar se havia algum outro que tivesse um desempenho melhor. E claro, tudo isso dentro das classes:

class MLModel:

def select_best_model(self, cv, X_train, Y_train):
""" Utiliza AutoML para identificar o melhor modelo de regressão.
:cv: define a validação cruzada
:X_train: features da base de treino
:Y_treino: variável target da base de treino
"""

# define busca do melhor modelo de regressão
model = TPOTRegressor(generations=5, population_size=50, scoring='r2', cv=cv, verbosity=2, random_state=1, n_jobs=-1)
model.fit(X_train, Y_train)
model.export('best_model.py')

# display resultados do AutoML
resultado = pd.DataFrame(model.evaluated_individuals_)
resultado.columns = list(map(lambda x: x[0], resultado.columns.str.split('(')))
return print(resultado.T)

def model_trainning(self, X_train, Y_train):
""" Cria pipeline de treinamento do melhor modelo encontrado na etapa de search.
:X_train: features da base de treino
:Y_treino: variável target da base de treino
"""
best_pipeline = ExtraTreesRegressor(bootstrap=False, max_features=0.25, min_samples_leaf=1, min_samples_split=5, n_estimators=100)
best_pipeline = best_pipeline.fit(X_train, Y_train)
return best_pipeline

Essa é a classe que seleciona o melhor modelo (select_best_model) com o TPOT e já faz o treinamento dele na função abaixo (model_trainning), considerando a métrica que eu escolhi. Depois de criada a classe, instanciei ela e apliquei nos meus dados:

# Instanciação das Classes
model = MLModel()

# Parâmetros
cv = RepeatedKFold(n_splits=10, n_repeats=3, random_state=1) # indicado pelo tpot

# Busca e seleção do melhor modelo com Auto ML
best_model_report = model.select_best_model(cv, X_train, Y_train)

# Treinamento do melhor modelo
best_pipeline = model.model_trainning(X_train, Y_train)

O sentimento é de paz terrível quando a solução vai tomando esse formato. Organização &a possibilidade de reutilização são maravilhas da orientação à objetos e das boas práticas de desenvolvimento de software que não podemos ignorar na hora de desenvolver soluções realmente efetivas. E pra fechar com chave de ouro: a bilbioteca de AutoML encontrou com o modelo ExtraTreesRegressor um desempenho ainda melhor do que a solução tinha: um r2-score de 87%. Nada mal para uma atualização!

Aqui tem o link completo para o notebook atualizado com orientação à objetos. O ideal, após feita essa parte, é separar cada classe em seu próprio script .py e importá-las dentro de um script executor:

from loader import Loader
from pre_processor import PreProcessor
from model_trainning import MLModel
from model_evaluator import MLEvaluator
from model_export import ModelExport

No meu caso, criei um script executor chamado de run_pipeline.py, que importa as classes que eu criei e executa na ordem correta cada uma das etapas:

# Instanciação das Classes
loader = Loader()
pre_processor = PreProcessor()
model = MLModel()
performance_evaluator = MLEvaluator()
export_model = ModelExport()

# Parâmetros
url_dados = ('FuelConsumptionCo2.csv')
redundant_cols = ['MODELYEAR','MAKE','MODEL','VEHICLECLASS','TRANSMISSION','FUELTYPE']
percentual_teste = 0.2
cv = RepeatedKFold(n_splits=10, n_repeats=3, random_state=1)

def main():
# Execução do pipeline de treinamento

# carga
data = loader.load_data(url_dados)

X_train, X_test, Y_train, Y_test = pre_processor.pre_process_data(data, percentual_teste,redundant_cols)

# Busca e seleção do melhor modelo com Auto ML
best_model_report = model.select_best_model(cv, X_train, Y_train)

# Treinamento do melhor modelo
best_pipeline = model.model_trainning(X_train, Y_train)

# Resultados de performance considerando r2 score
performance_evaluator.avaliar_r2_score(best_pipeline, X_test, Y_test)

# Export do modelo treinado
loaded_pkl_model = export_model.export_best_model(best_pipeline)

if __name__ == '__main__':
main()

A estrurua completa do projeto você pode conferir aqui. Cada classe tem seu próprio script: load de dados, pré processamento dos dados, modelo (seleção e treinamento) e avaliação dos resultados — muito mais organizado e fácil de ser deployado do que um notebook! Aproveitei e criei um pipeline bem simples de produção, simulando como seria a utilização contínua do modelo. Pra isso, salvei o modelo em pickle file e criei uma classe que aplica esse modelo na leitura de novos dados (reutilizando a classe Loader).

# Instanciação das Classes
loader = Loader()
preprocess_deploy = PreProcessorDeploy()
generate_output = Output()
load_model = LoadModel()

# Parâmetros
redundant_cols = ['MODELYEAR','MAKE','MODEL','VEHICLECLASS','TRANSMISSION','FUELTYPE']
new_file = "brand_new_data.csv"
pkl_model_file = 'model.pkl'

def main():
# Execução do pipeline de produção

# carga
new_data = loader.load_data(new_file)

# processamento dos novos dados
X = preprocess_deploy.pre_process_new_data(new_data, redundant_cols)

# load do modelo treinado
loaded_pkl_model = load_model.load_trained_model(pkl_model_file)

# aplicação do modelo já treinado
predicoes = loaded_pkl_model.predict(X)

# geração e export do arquivo de predições em csv
generate_output.create_output_dataframe(new_data, predicoes)

if __name__ == '__main__':
main()

Vale lembrar que cada ambiente ou empresa tem sua própria forma de produtizar as soluções, com especificidades bastante particulares. Mas te garanto que utilizar a orientação à objetos e organizar sua solução em scripts .py, da mesma forma que um software é 80% do caminho andado na hora de qualquer deploy!

Os scripts da versão 2 do modelo de previsão de emissão de carbono estão disponíveis aqui. Vale comparar com a versão 1, também disonível para consulta.

--

--

Letícia Gerola
Joguei os Dados

Cientista de dados e jornalista. Autora do blog de Data Science ‘Joguei os Dados’.