Revolucione Seus Projetos de Machine Learning com o GitHub Actions
English version available here
Neste artigo, faremos um exemplo de uso do GitHub Actions em um projeto de Machine Learning (ML) 🫰.
Introdução
Confiabilidade é um ponto importante ao fazer deploy de modelos de machine learning em ambiente produtivo. Podemos aumentar a robustez, velocidade, qualidade e reprodutibilidade via esteiras de CI/CD — Continuous Integration (CI) e Continuous Deployment (CD) — permitindo aos desenvolvedores automatizar o processo de testar, construir e implantar código.
O GitHub Actions é uma plataforma poderosa para implementar fluxos de trabalho de CI/CD diretamente dentro de seus repositórios GitHub, faremos um exemplo usando um Projeto de ML simplificado.
Esteiras de CI/CD para Machine Learning?
Esteiras de CI/CD são uma prática de desenvolvimento de software que envolve a integração regular de alterações de código em um repositório central e a entrega automática dessas alterações para produção.
Aplicando ao nosso contexto de ML, as Esteiras de CI/CD desempenham o papel da automação de várias etapas do ciclo de vida do ML, como treinamento de modelo, validação e implementação.
A esteira de Continuous Integration (CI) usualmente tem o objetivo de evitar que código quebrado suba em produção, gerando grande dor de cabeça. Esta esteira pode ser composta por testes unitários e de integração, validando de forma antecipada se o código está quebrado ou com funcionalidade fora de conformidade.
A esteira de Continuous Deployment (CD) comumente tem o objetivo de carregar todo o artefato de código gerado para produção, fazendo todas configurações produtivas necessárias. Esta esteira pode ser composta por upload de código em alguma solução de armazenamento, o agendamento da execução do código ou a subida/atualização de alguma API.
Mas afinal, o que é GitHub Actions?
O GitHub Actions é uma ferramenta que permite automatizar fluxos de trabalho dentro do ambiente GitHub. Você pode configurar eventos específicos para disparar ações, como empurrar para o repositório, abrir um pull request, ou até mesmo em um cronograma específico.
Mãos a Obra — GitHub Actions para ML
Combinando o jogo 😉
Antes de explicar como vamos criar Esteiras de CI/CD usando o GitHub Actions, vamos combinar quais serão as responsabilidades da Esteira de CI e da Esteira de CD:
Responsabilidades da esteira de Continuous Integration (CI)
- Configurar o ambiente e instalar dependências
- Executar testes unitários para evitar implantar código quebrado em produtção
Responsabilidades da esteira de Continuous Deployment (CD)
- Armazenar artefatos em um ambiente de storage
- Publicar o agendamento da execução do modelo
- Disparar uma execução do modelo para validar a correta execução
Como a etapa de CD é fortemente ligada a qual solução de cloud, ou outra infraestrutura de deploy, você está utilizando, substituiremos o código que faria integração com uma cloud específica por algum mockup, combinado?
Passo 1: Criando o Projeto de ML básico para exemplo
Como vamos simular o CI/CD em um projeto, precisamos criar dois arquivos de exemplo na raíz do repositório, o arquivo train_model.py
que vai conter o código de treinamento de um modelo para prever o tipo de vinho, usando o dataset wine do scikit-learn e o arquivo requirements.txt
que vai conter os pacotes necessários para execução do projeto de ML.
Passo 1.1: Criar arquivo de treino do modelo ✍️
Crie na raíz do repositório o arquivotrain_model.py
, com o seguinte conteúdo:
from sklearn.datasets import load_wine
from sklearn.ensemble import RandomForestClassifier
import numpy as np
import pickle
def load_dataset():
"""
Carrega o conjunto de dados Wine do Scikit-Learn.
Retorna:
X (np.array): Dados de entrada.
y (np.array): Rótulos de destino.
"""
X, y = load_wine(return_X_y = True)
return X, y
def train_model(X : np.array, y : np.array) -> RandomForestClassifier:
"""
Treina um modelo de floresta aleatória no conjunto de dados Wine.
Parâmetros:
X (np.array): Dados de entrada.
y (np.array): Rótulos de destino.
Retorna:
model (RandomForestClassifier): O modelo treinado.
"""
# Definir o modelo
model = RandomForestClassifier(n_estimators = 2, max_depth = 1, random_state = 1)
# Treinar o modelo
model = model.fit(X, y)
# Avaliar o treinamento do modelo
print(f"Training Mean Accuracy: {model.score(X, y)}")
return model
def save_model(model, filepath):
"""
Salva o modelo treinado em um arquivo pickle.
Parâmetros:
model (qualquer): O modelo treinado para ser salvo.
filepath (str): O caminho do arquivo onde o modelo treinado será salvo.
"""
with open(filepath, 'wb') as f:
pickle.dump(model, f)
def main():
"""
A função principal que executa as etapas de treinamento do modelo.
Esta função carrega o conjunto de dados Wine usando a função load_dataset(),
treina um modelo de floresta aleatória nos dados carregados usando a função train_model(),
e retorna o modelo treinado.
"""
X, y = load_dataset()
model = train_model(X, y)
save_model(model, 'model.pkl')
return
if __name__ == "__main__":
main()
Passo 1.2: Criar arquivo de pacotes necessários para o modelo ✍️
Crie na raíz do repositório o arquivorequirements.txt
, com o seguinte conteúdo:
pytest==7.3.1
scikit-learn==1.2.2
numpy==1.24.3
Passo 2: Criando um arquivo de workflow do GitHub
Os fluxos de trabalho do GitHub Actions são definidos em arquivos .yml que residem no diretório .github/workflows
na raiz do seu repositório do GitHub.
Vamos criar juntos este arquivo, adicionando parcialmente as partes até completarmos nossas esteiras de CI/CD.
✍️ Crie um novo arquivo chamado ml_workflow.yml
dentro do diretório .github/workflows
do seu repositório, contendo o seguinte conteúdo, vamos começar criando a esteira de Continuous Integration (CI):
# This is a basic workflow to help you get started with Actions
name: ML CI/CD Pipeline
# Controls when the workflow will run
on:
# Triggers the workflow on push events but only for the "dev" branch
push:
branches: [ "dev" ]
# A workflow run is made up of one or more jobs that can run
# sequentially or in parallel
jobs:
# This workflow will contain two jobs, CI and CD.
CI:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE,
# so your job can access it
- uses: actions/checkout@v3
- uses: actions/setup-python@v2
with:
python-version: '3.8.x'
# Runs a single command using the runners shell
- name: Run Hello
run: echo Hello, Medium!
Neste exemplo, o workflow se chama ML CI/CD Pipeline
, ele vai disparar as ações que desejamos sempre que houver um push na branch dev
, sendo executado em uma máquina linux com distribuição ubuntu
e python 3.8
. Ação que queremos é que ele simplesmente nos diga “Hello, Medium!”.
Testando o Passo 2
Faça o commit do arquivo ml_workflow.yml
na branch dev
, depois disso acesse o repositório do GitHub pelo browser e vá até a aba Actions, como no print screen abaixo.
Você verá o workflow sendo executado, conforme imagem abaixo
Clique neste workflow, note que o step Run Hello
foi executado com sucesso!
Passo 3: Configurando o ambiente
É comum que modelos python precisem de pacotes específicos para rodarem, no nosso exemplo vamos usar scikit-learn e numpy.
✍️ Adicionando este trecho dentro do seu arquivo de workflows do github actions.github/workflows/ml_workflow.yml
, você já vai conseguir executar a instalação dos pacotes que desejar para seu projeto Python, estes foram listados no arquivo requirements.txt
que criamos no Passo 1.2!
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
Passo 4: Desenvolvendo testes para o treino do modelo
A etapa de Continuos Integration (CI) envolve a execução de testes para verificar se o código existente não é quebrado por novas alterações. Isso é essencial em projetos de ML, pois ajuda a detectar problemas que podem surgir devido a mudanças nos dados ou no código.
No nosso exemplo, vamos validar se o cientista não pulou alguma etapa que era esperada no código de treino do modelo train_model.py
, validando dois pontos centrais:
- A existência de métodos padrão para o contexto de modelagem supervisionada, os métodos
load_dataset
,train_model
esave_model
. - O correto salvamento do arquivo pickle com o modelo treinado.
✍️ Crie na raíz do repositório um arquivo nomeado unit_test_train_model.py
dentro da pasta tests
, com o seguinte conteúdo:
import pytest
import os
import train_model
def test_load_dataset_exists():
"""
Testa se o método load_dataset existe no módulo train.
Este teste verificará a existência do método load_dataset no módulo train.
Se o método não existir, o teste falhará com uma mensagem indicando que o método load_dataset não existe.
"""
assert hasattr(train_model, 'load_dataset'), "O método load_dataset não existe"
def test_train_model_exists():
"""
Testa se o método train_model existe no módulo train.
Este teste verificará a existência do método train_model no módulo train.
Se o método não existir, o teste falhará com uma mensagem indicando que o método train_model não existe.
"""
assert hasattr(train_model, 'train_model'), "O método train_model não existe"
def test_save_model_exists():
"""
Testa se o método save_model existe no módulo train.
Este teste verificará a existência do método save_model no módulo train.
Se o método não existir, o teste falhará com uma mensagem indicando que o método save_model não existe.
"""
assert hasattr(train_model, 'save_model'), "O método save_model não existe"
@pytest.fixture(scope="module")
def train_and_save_model():
"""
Fixture para treinar e salvar o modelo antes de executar o teste test_model_file_exists.
Esta função chama a função main() do módulo train para treinar e salvar o modelo.
Após o término do teste, remove o arquivo 'model.pkl'.
"""
train_model.main()
yield
os.remove('model.pkl')
def test_model_file_exists(train_and_save_model):
"""
Testa se o arquivo model.pkl foi salvo corretamente.
Este teste, que depende da fixture train_and_save_model, verificará a existência do arquivo model.pkl no diretório atual.
Se o arquivo não existir, o teste falhará com uma mensagem indicando que o arquivo model.pkl não foi salvo corretamente.
"""
assert os.path.isfile('model.pkl'), "O arquivo model.pkl não foi salvo corretamente"
Passo 5: Etapa de CI — Executar testes
Adicione o seguinte trecho dentro do seu arquivo de workflows do github actions.github/workflows/ml_workflow.yml
, você já vai conseguir executar testes variados dentro da sua esteira de CI, criamos testes unitários para validar existência de métodos padrão dentro do arquivo unit_test_train_model.py
que criamos há pouco.
# Install Python packages dependencies from requirements.txt file
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
# Execute unit tests for train_model.py script
- name: Run model training unit tests
run: |
python -m pytest tests/unit_test_train_model.py
Testando o Passo 5
Faça o commit na branch dev
dos arquivos atuais do repositório, depois disso acesse o repositório do GitHub pelo browser e vá até a aba Actions, como feito na etapa “Testando o Passo 2”.
Podemos notar que o código foi executado com sucesso, pois nosso arquivo train_model.py
possui todos os métodos load_dataset
, train_model
e save_model
, além de conseguir salvar com sucesso o arquivo model.pkl
.
Passo 6: Etapa de CD— Fazendo o deploy
Após termos validado que o código está saudável na etapa de CI, vamos gerar o arquivo model.pkl
para ser armazenado em uma solução de armazenamento e reutilizado a qualquer momento, sendo agendado e tendo um treino disparado para fins de validação.
Abaixo, vou providenciar todo arquivo ml_workflow.yml
para aumentar a praticidade ao testar, ✍️ substitua esse arquivo pelo antigo para garantir que está tudo certo.
Nota: Os steps
Upload artifacts
,Schedule execution
eRun deploy tests
foram simplificados para fins didáticos, uma vez que estas etapas são condicionadas a infraestrutura de deploy utilizada e podem envolver credenciais. Se quiser eu eu forneça um exemplo com código usando a Azure, dê um clap neste post
# This is a basic workflow to help you get started with Actions
name: ML CI/CD Pipeline
# Controls when the workflow will run
on:
# Triggers the workflow on push or pull request events but only for the "main" branch
push:
branches: [ "dev" ]
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
CI:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v3
- uses: actions/setup-python@v2
with:
python-version: '3.8.x'
# Runs a single command using the runners shell
- name: Run Hello
run: echo Hello, Medium!
# Install Python packages dependencies from requirements.txt file
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
# Execute unit tests for train_model.py script
- name: Run model training unit tests
run: |
python -m pytest tests/unit_test_train_model.py
CD:
needs: CI
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v3
- uses: actions/setup-python@v2
with:
python-version: '3.8.x'
# Install Python packages dependencies from requirements.txt file
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
# Run train model script
- name: Train model
run: |
python train_model.py
# Upload the model saved
- name: Upload artifacts
run:
echo "You can choose where you want to storage your artifacts"
# Schedule the execution of the model
- name: Schedule execution
run:
echo "You can upload your new Airflow DAG to a specific folder "
# Execute the model once to test if it does run
- name: Run deploy tests
run:
echo "Here, you can execute a pytest to check if artifacts and the DAG were uploaded"
echo "After this, you can ask Airflow to execute a first model run"
Testando o Passo 6
Faça o commit na branch dev
dos arquivos atuais do repositório, incluindo o arquivo ml_workflow.yml
providenciado no Passo 5, depois disso acesse o repositório do GitHub pelo browser e vá até a aba Actions, como feito na etapa “Testando o Passo 2”.
Agora, você vai perceber que o CD depende do CI, pois adicionando o argumento “needs: CI” na etapa de CD, isso garante que o CD só será executado caso o CI termine com sucesso!
Clicando na etapa de CD dentro das Actions do Github Actions, temos os status de sucesso em todos steps! 🥹
Quebrando (propositalmente 😎) o Projeto de ML
Vamos simular como a esteira de CI pode nos ajudar a evitar que código quebrado suba em produção, vamos exemplificar um caso que pode acontecer durante tanto o desenvolvimento do modelo ou até no deploy do projeto de ML, um bugfix seja feita de última hora.
Vamos supor que o cientista desenvolveu uma nova versão do modelo, sendo mais assertivo agora, mas esqueceu de tirar um hiperparâmetro que não pertence ao modelo utilizado.
Simulando a falha no desenvolvimento: vamos inser o parâmetro n_neighbors
ao instancializar a RandomForestClassifier, este parâmetro não existe para esta classe. Além disso, vamos trocar o nome do arquivo .pkl gerado de model.pkl
para my_beautiful_model.pkl
.
Substituia o arquivo train_model.py
pelo conteúdo abaixo:
from sklearn.datasets import load_wine
from sklearn.ensemble import RandomForestClassifier
import numpy as np
import pickle
def load_dataset():
"""
Carrega o conjunto de dados Wine do Scikit-Learn.
Retorna:
X (np.array): Dados de entrada.
y (np.array): Rótulos de destino.
"""
X, y = load_wine(return_X_y = True)
return X, y
def train_model(X : np.array, y : np.array) -> RandomForestClassifier:
"""
Treina um modelo de floresta aleatória no conjunto de dados Wine.
Parâmetros:
X (np.array): Dados de entrada.
y (np.array): Rótulos de destino.
Retorna:
model (RandomForestClassifier): O modelo treinado.
"""
# Definir o modelo
model = RandomForestClassifier(n_estimators = 15,
max_depth = 3,
n_neighbors = 120,
random_state = 1)
# Treinar o modelo
model = model.fit(X, y)
# Avaliar o treinamento do modelo
print(f"Training Mean Accuracy: {model.score(X, y)}")
return model
def save_model(model, filepath):
"""
Salva o modelo treinado em um arquivo pickle.
Parâmetros:
model (qualquer): O modelo treinado para ser salvo.
filepath (str): O caminho do arquivo onde o modelo treinado será salvo.
"""
with open(filepath, 'wb') as f:
pickle.dump(model, f)
def main():
"""
A função principal que executa as etapas de treinamento do modelo.
Esta função carrega o conjunto de dados Wine usando a função load_dataset(),
treina um modelo de floresta aleatória nos dados carregados usando a função train_model(),
e retorna o modelo treinado.
"""
X, y = load_dataset()
model = train_model(X, y)
save_model(model, 'my_beautiful_model.pkl')
return
if __name__ == "__main__":
main()
Faça o commit na branch dev
dos arquivos atuais do repositório, incluindo o arquivo train_model.py
acima, depois disso acesse o repositório do GitHub pelo browser e vá até a aba Actions, como feito na etapa “Testando o Passo 2”.
Podemos notar que o CI quebrou, conforme o esperado, e por este motivo o CD não executou.
Acessando a etapa de CI, somos alertados do TypeError: unexpected keyword argument ‘k_neighbors’, de fato, este parâmetro não existe na classe RandomForestClassifier. 😇
Podemos notar que o CI cumpriu seu propósito e já evidenciou o problema antes mesmo de tentar subir o código em produção 🥰, evitando muita dor de cabeça e esforço em depurar o que houve com o modelo pós deploy, muitas vezes alertado pelo cliente que passa a não receber o resultado de execução do modelo.
Considerações finais
Implementar CI/CD em projetos de Machine Learning com o auxílio do GitHub Actions não apenas otimiza o processo de desenvolvimento, mas também contribui significativamente para a qualidade dos modelos gerados e para a robustez dos sistemas construídos.
Ao automatizar tarefas como treinamento de modelo, validação, implantação e monitoramento, as equipes podem se concentrar no que é mais importante: aprimorar os modelos e estratégias de Machine Learning e fornecer valor contínuo por meio de suas soluções.
Através deste artigo, espero ter fornecido uma compreensão clara de como configurar uma esteira de CI/CD para projetos de Machine Learning com o GitHub Actions. A aplicação de CI/CD no Machine Learning é um grande passo em direção à MLOps, a disciplina de aplicar as melhores práticas de DevOps no ciclo de vida de Machine Learning. 😁
Lembre-se, as configurações apresentadas aqui são um ponto de partida. Cada projeto tem suas próprias necessidades e requerimentos específicos, portanto, sinta-se à vontade para adaptar e expandir esses conceitos conforme sua necessidade. Ao adotar a mentalidade de integração e entrega contínuas, você estará se posicionando na vanguarda do desenvolvimento de soluções de Machine Learning eficientes e sustentáveis.
Obrigado por acompanhar este artigo e esperamos que você esteja agora mais equipado para criar e gerenciar seu próprio pipeline de CI/CD em Machine Learning usando o GitHub Actions. 🤗
Crie conexões:
Gostou do conteúdo? Vamos tomar um café, me adicione no LinkedIn para trocarmos ideias e compartilharmos conhecimentos!
Referências
[1] GitHub (2023). Documentação do GitHub Actions. Disponível em https://docs.github.com/pt/actions