Revolucione Seus Projetos de Machine Learning com o GitHub Actions

Iago Modesto Brandão
13 min readMay 15, 2023

--

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.

Fonte: docs.github.com

Você verá o workflow sendo executado, conforme imagem abaixo

Fonte: Autoria Própria

Clique neste workflow, note que o step Run Hello foi executado com sucesso!

Fonte: Autoria Própria

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 e save_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”.

Fonte: Autoria Própria

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 executione Run 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!

Fonte: Autoria Própria

Clicando na etapa de CD dentro das Actions do Github Actions, temos os status de sucesso em todos steps! 🥹

Fonte: Autoria Própria

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.

Fonte: Autoria Própria

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. 😇

Fonte: Autoria Própria

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!

https://www.linkedin.com/in/iagombrandao

Referências

[1] GitHub (2023). Documentação do GitHub Actions. Disponível em https://docs.github.com/pt/actions

--

--

Iago Modesto Brandão

Passionate by tech and all possibilities, come with us to learn more and develop the next step of the world?