Uncategorized

Continuous Machine Learning – Parte I

Reading time: 11 minutes
Image by Taras Tymoshchuck from here.

Esse texto faz parte de uma série de 3 partes sobre Continuous Machine Learning. Você pode checar a parte II aqui e a parte III aqui.

O que é isso?

Continuous Machine Learning (CML) segue o mesmo conceito de Continuous Integration and Continuous Delivery (CI/CD), famosos concenitos da Software Engineering / DevOps, mas aplicado a projetos de Aprendizado de Máquina e Ciência de Dados.

O que vou aprender com esse post?

Eu irei abordar aqui um conjunto de ferramentas que podem fazer sua vida como Cientista de Dados muito mais interessante. Nós utilizaremos o MIIC, um algoritmo de inferência de redes causais, para inferir a rede de um famoso dataset (alarm do pacote bnlearn). A ideia é utilizar (1) o git para versionar nosso código, (2) o DVC para versionar nosso dataset, arquivos de saíde e até o pipeline! (3) Utilizaremos o GitHub como git remote e (4) o Google Drive como DVC remote. Pense nos remotes como tendo uma cópia do seu repositório remotamente, em um outro computador. Eu também escrevi um tutorial sobre gerenciamento de projetos de ciência de dados com o DVC e se você estiver interessado, clique aqui para abrir o tutorial em uma nova aba para você lê-lo depois.

Uma coias que me incomoda um pouco é frequentemente ter que abrir a página do GitHub e utilizar a interface deles. É muito mais prático poder fazer tudo pelo terminar e aqui vou te apresentar o gh, a aplicação de linha de comando oficiial do GitHub. Também iremos utilizar o CML, uma biblioteca open source para implementar continuous integration & delivery (CI/CD) em projetos de aprendizagem de máquina. O CML irá conectar o git, DVC e GitHub Actions (Sistema de CI/CD built-in do GitHub). A ideia é criar um gatilho que será disparado toda vez que você fizer uma determinada ação no seu repositório, como um push. Essa ação pode ser processamento, qualquer tipo de computação, e irá por padrão acontecer em um GitHub Runner (Esse é o nome que o GitHub da a uma máquina virtual na infraestrutura deles. Veja mais sobre isso aqui). Um exemplo seria utilizar branches do git como experimentos no seu projeto de ciência de dados. Imagine executar seu código uma vez, alterar algum parâmetro do algoritmo, executá-lo novamente, mais uma alteração qui e outra ali e assim por diante até você ter várias branches e experimentos. Toda vez que você der um git push, por exemplo, um GitHub Runner poderia ser acionado para comparar as métricas desse experimento com os outros e imprimir na tela (como um comentário na página do Pull Request) um relatório. Parece legal, né?

Hora de arregaçar as mangas

Vamos criar nosso repositório no GitHub e fazer uma cópia local dele. Faremos isso a partir da linha de comando! (Instruções aqui de como instalar o gh).

mkdir $HOME/dev
cd dev
gh repo create dvc-miic-cml -d 'GitHub repo to play with DVC, MIIC and CML' --public

Você será perguntado se deseja criar uma cópia local do repositório. Se você disser não, você terá que clonar o repositório mais tarde, portanto responda com Y e pressione enter. Após isso, entre no diretório.

cd dvc-miic-cml

Vamos criar um arquivo LEIAME, onde iremos colocar uma descrição do nosso repositório. No GitHub isso costuma ser colocado em um arquio chamado README.md, escrito com formatação Markdown.

echo '# DVC-MIIC-CML' > README.md
echo 'This is a sample repository for testing DVC, MIIC and CML in GitHub.' >> README.md
echo 'The analyses will be performed using [MIIC](https://github.com/miicTeam/miic_R_package) to infer the network from the [alarm dataset](https://rdrr.io/cran/bnlearn/man/alarm.html).' >> README.md

Também iremos fazer o download de um arquivo de licença (GNU GPLv3) a partir do site do projeto GNU e salvá-lo como um arquivo chamado LICENSE, como costuma ser feito em repositórios no GitHub.

wget -c https://www.gnu.org/licenses/gpl-3.0.txt -O LICENSE

Após isso, iremos adicionar esses dois novos arquivos para o índice (index) do nosso repositório git através do comando git add e após isso fazer um commit do estado atual do repositório (uma espécie de snapshot, caso você não tenha o workflow / linguajar do git na ponta da língua). Com o commit realizado, iremos enviar o estado atual do repositório para o remote no GitHub através do comando git push. Desse modo, qualquer pessoa que solicite uma cópia atual do repositório para o GitHub, receberá também as nossas modificações. Como essa é a primeira vez que estamos dando um git push, precisamos informar ao git quem é a branch principal, padrão, do nosso repositório. Faremos isso através do parâmetro --set-upstream. No futuro, quando quisermos dar um novo git push, não precisaremos desse parâmetro. Apenas git push será suficiente.

# The dot when provided to git add means everything in the current folder
git add .
git commit -m 'Initial commit'
git push --set-upstream origin master

Você pode checar o seu repositório no GitHub agora. Ele deve estar atualizado! O meu está! O git não foi feito para versionar datasets e arquivos grandes, nem muito menos pipelines. É nessa lacuna que o DVC cai como uma luva. Vamos começar a utilizá-lo para versionar o dataset que iremos analisar. Você pode baixar o dataset alarm através do site oficial do MIIC clicando aqui. Na linha de comando, você deve digitar:

wget -c https://miic.curie.fr/datasets/alarm1000samples.txt -O alarm.tsv

Eu particularmente não gosto quando os repositórios git são u amontoado de arquivos jogados na base do diretório. Prefiro organizar em sub diretórios. Vamos fazer isso.

mkdir data
mv alarm.tsv data/

O DVC entra em cena

Se você digitar git status, você irá ver que a pasta está como untracked, não versionada, o que de certo modo é um pouco irritante porque (a) git não deveria estar preocupado com arquivos de dados e (b) ainda que algumas pessoas forcem a barra e façam-no versionar datasets, você não quer isso. Afinal, temos o DVC, certo? Uma das coisas que o DVC faz automaticamente, após você informá-lo que você quer que ele versione alguns arquivos, é dizer para o git ignorar esses arquivos já que o DVC irá tomar conta deles. Assim como antes de começar a dizer o git o que fazer, com o DVC é o mesmo: Você precisa inicializar o DVC no repositório. Com o git, digitamos git init, e com o DVC não será diferente. Instruções sobre como instalar o DVC podem ser encontradas aqui.

dvc init
dvc add data/alarm.tsv

Após ser inicializado, o DVC irá criar arquivos arquivos. Esses meta-dados são arquivos que precisam ser versionados pelo git. Acho útil pensar no DVC como algo que complementa o git, portanto o git segue sendo o ator principal já que ele versiona os arquivos do DVC. Vamos adicionar esses arquivos ao índice do git, index, e realizar um commit. Nosso dataset não será adicionado nem comitado poqrue o DVC já o adicionou ao .gitignore, um arquivo oculto que o git uso para esse propósito: saber que arquivos deve ignorar.

git add .
git commit -m 'Initiates DVC and asks it to track alarm dataset'

Dependendo do quanto você está familiarizado com o git, isso pode ser uma surpresa ou não mas você não precisa dar git push (enviar a versão mais atual do repositório para um git remote, como o GitHub). Nós não iremos fazer isso agora, por exemplo (ainda que pudéssemos). E do mesmo modo que o GittHub é um remote do git, você tambám pode ter um remote do DVC. O Dropbox, um bucket do Amazon S3, Google Drive ou até uma pasta no seu computador ou em um disco externo, qualquer uma dessas opções pode ser um DVC remote. Por questões de simplicidade, iremos aqui utilizar o Google Drive.

O primeiro passo é ir ao site do Google Drive, autenticar-se na sua conta, e criar uma pasta. Eu criei uma com o nome dvc-miic-cml example. A URL na barra de endereço do meu navegador ficou assim: https://drive.google.com/drive/u/1/folders/188CmpQIYqKOgvcgaLZOxz1GqlwTasv8c

O próximo passo é extrair a última parte dessa URL, após o folders/, ou seja, 188CmpQIYqKOgvcgaLZOxz1GqlwTasv8c. Vamos agora informar o DVC que queremos que essa pasta do Google Drive seja nosso DVC remote.

dvc remote add -d myremote gdrive://188CmpQIYqKOgvcgaLZOxz1GqlwTasv8c

O parâmetro -d é importante pois ele diz ao DVC que esse é nosso remote padrão. Assim como é possível ter mais de um remote no git, também é possível ter mais de um no DVC. Sem informar qual o remote padrão, toda vez que déssemos um git push, o DVC perguntaria qual o remote desejado. O próximo passo é enviar as atualizações do nosso repositório local para o GitHub, nosso git remote, com o git push. Consegue adivinhar qual o comando para enviarmos as atualizações do repositório local para o DVC remote? Tenho certeza que você adivinhou corretamente!

dvc push

Para ter certeza que está tudo claro pra você: Os códigos fonte e meta dados do DVC irão para o GitHub remote. Os dados, pipelines e saída de algoritmos irão para o DVC remote.

Se você for conferir a sua pasta no Google Drive agora, você verá que a pasta não está mais vazia. O nome das pastas e seus conteúdos lá não fazem muito sentido, mas acredite em mim: O DVC sabe como interpretar isso :P. Por força do hábito, você digita git status e percebe que algo mudou no seu repositório. Como assim?! Ao adicionar um remote, um arquivo de configuração do DVC foi alterado. Você poderia executar um git add para adicionar o arquivo ao índice e um git commit para salvar um novo snapshot do seu repositório, mas por uma questão didática, eu irei fazer algo diferente: Eu irei emendar o último commit, e ao fazer isso atualizar a mensagem de commit. Amending, ou emendamento, é útil quando você realiza um commit e pouco tempo depois percebe que esqueceu de algo, ou decide melhorar a mensagem do seu commit. A ideia é alterar o seu último commit, em vez de fazer um novo.

git add .
git commit --amend -m 'Initiates DVC, sets the default remote and asks it to track alarm dataset'

Ok, vamos dar um git push agora.

git push

A página do GitHub do seu repositório deve estar parecida com essa aqui. Talvez você esteja se perguntando por que existem dois arquivos na sua pasta data/ do GitHub se eu te disse que o git não irá versionar datasets. Um desses arquivos é o .gitignore, responsável por informar o git para ignorar arquivos nesse diretório. O arquivo com a extensão .dvc é utilizado pelo DVC e contém uma hash construída a partir do conteúdo do dataset. É assim que o DVC sabe quando o dataset mudou, porque a hash irá mudar também.

Nota sobre o DVC

Se alguém estiver interessado neste repositório (talvez você esteja), a ideia é fazer como faria com qualquer outro repositório do GitHub. É só cloná-lo!

git clone https://github.com/mribeirodantas/dvc-miic-cml.git
cd dvc-miic-cml

Ao listar os arquivos dentro do repositório com o comando ls data, você perceberá que o dataset não está lá. Bem, claro que ele não está, certo? Você apenas clonou o repositório git, com código. Vamos executar o comando dvc pull para baixar os dados versionados pelo DVC.

dvc pull

Agora sim 🙂 . Hora de começar a escrever nosso script de inferência de rede. Utilizaremos o MIIC (Multivariate information-based inductive causation) para isso. Crie um arquivo chamado infer_network.R com o conteúdo abaixo:

library(miic)
alarm_dataset <- read.table('data/alarm.tsv', header = TRUE)
res <- miic(input_data = alarm_dataset)
total_edges <- nrow(res$all.edges.summary)
retained_edges <- nrow(res$all.edges.summary[res$all.edges.summary$type == 'P', ])
ratio_edges <- paste0('Ratio of retained edges: ', retained_edges/total_edges)
write.table(ratio_edges, file = 'metrics.txt', col.names = FALSE, row.names = FALSE)

Esse código carrega o pacote do R miic, lê o dataset, trazendo-o para dentro do ambiente R, chama o algoritmo miic para inferir a rede a partir desse dataset e calcula a razão de arestas mantidas com relação ao total possível. Após isso, essa razão é salva em um arquivo chamado metrics.txt.

GitHub Actions

Agora é hora de começar a brincar com o GitHub Actions e deixar o CML fazer sua mágica. Toda vez que fizermos um novo commit para esse repositório e fazemos um push, o modelo será construído novamente e a métrica será recalculada.

Para utilizar o GitHub Actions, precisamos criar um arquivo especial em uma pasta especial. O caminho dentro de nosso repositório git é: .github/workflows

Dentro da pasta, você precisar criar o arquivo que o GitHub Action irá buscar por instruções. Você pode escolher qualquer nome, mas precisa estar no formato YAML format. Vamos começar criando o caminho para nosso arquivo do GitHub Action.

mkdir -p .github/workflows
cd .github/workflows

Após isso, criaremos um arquivo chamado cml.yaml e escreveremos o código abaixo. Essas instruções basicamente pedem por uma máquina rodando a última versão do Ubuntu, com o R devidamente instalado, clona o nosso repositório git, instala o MIIC, DVC, as suas dependências, executa o comando dvc pull para baixar nosso dataset, e chama o script infer_network.R que irá inferir a rede e salvar a métrica para um arquivo que, no final de tudo, será impresso na tela.

name: dvc-cml-miic
on: [push]
jobs:
  run:
    runs-on: [ubuntu-latest]
    steps:
      - uses: r-lib/actions/setup-r@master
        with:
          version: '3.6.1'
      - uses: actions/checkout@v2
      - name: cml_run
        env:
          repo_token: ${{ secrets.GITHUB_TOKEN }}
          GDRIVE_CREDENTIALS_DATA: ${{ secrets.GDRIVE_CREDENTIALS_DATA }}
        run: |
          # Install miic and dependencies
          wget -c https://github.com/miicTeam/miic_R_package/archive/v1.4.2.tar.gz
          tar -xvzf v1.4.2.tar.gz
          cd miic_R_package-1.4.2
          R --silent -e "install.packages(c(\"igraph\", \"ppcor\", \"scales\", \"Rcpp\"))"
          R CMD INSTALL . --preclean
          cd ..
          # Install Python packages
          pip install --upgrade pip
          pip install wheel
          pip install PyDrive2==1.6.0 --use-feature=2020-resolver
          # Install DVC
          wget -c https://github.com/iterative/dvc/releases/download/1.4.0/dvc_1.4.0_amd64.deb
          sudo apt install ./dvc_1.4.0_amd64.deb
          # Run DVC
          dvc pull
          Rscript infer_network.R
          # Write your CML report
          echo "MODEL METRICS"
          cat metrics.txt

Em vez de fazer um commit com essas novas alterações na branch principal,m a master, nós iremos criar uma banch de experimento. É assim que você deve usar o git com o DVC! Nós iremos analisar a versão raw, bruta, do datase alarm, sem pré-processamento, portanto eu irei chamar essa branch de raw_alarm_dataset.

Você já usou o dvc pull, portanto sua máquina já está autentiada com o Google Drive. Crie um GitHub secret com o conteúdo do arquivo .dvc/tmp/gdrive-user-credentials.json e nomeie esse GitHub secret como GDRIVE_CREDENTIALS_DATA.

git checkout -b raw_alarm_dataset
# infer_network.R is not in this folder, therefore `git add .` wouldn't
# add it to the index of your git repository. -A adds everything.
git add -A
git commit -m 'Infers alarm network with MIIC and default parameters'
git push origin raw_alarm_dataset
gh pr create --title 'Network inference of alarm dataset'

Agora, vá para o GitHub e veja o que está acontecendo. Se tudo ocorrer de acordo com o plano, você irá ver algo parecido com a imagem abaixo quando a checagem estiver concluída.

Ok… Você imprimiu sua métrica no log. Massa mas vamos lá, seria mais interessante ter algo ao menos um pouquinho mais interessante, né? `^^

Vamos adicionar mais algumas linhas no script infer_network.R para fazê-lo plotar a rede inferida para uma imagem, e alterar a parte final para fazer uso de algumas funcionalidades do CML. O código do novo infer_network.R deve estar assim:

library(miic)
alarm_dataset <- read.table('data/alarm.tsv', header = TRUE)
res <- miic(input_data = alarm_dataset)
total_edges <- nrow(res$all.edges.summary)
retained_edges <- nrow(res$all.edges.summary[res$all.edges.summary$type == 'P', ])
ratio_edges <- paste0('Ratio of retained edges: ', retained_edges/total_edges)
write.table(ratio_edges, file = 'metrics.txt', col.names = FALSE, row.names = FALSE)
# Plot network
png(file='network_diagram.png')
miic.plot(res)
dev.off()

E o novo arquivo cml.yaml deve estar como no código abaixo. A alteração é que agora iremos instalar o CML para poder utilizá-lo.

name: dvc-cml-miic
on: [push]
jobs:
  run:
    runs-on: [ubuntu-latest]
    steps:
      - uses: r-lib/actions/setup-r@master
        with:
          version: '3.6.1'
      - uses: actions/checkout@v2
      - name: cml_run
        env:
          repo_token: ${{ secrets.GITHUB_TOKEN }}
          GDRIVE_CREDENTIALS_DATA: ${{ secrets.GDRIVE_CREDENTIALS_DATA }}
        run: |

          # Install miic and dependencies
          wget -c https://github.com/miicTeam/miic_R_package/archive/v1.4.2.tar.gz
          tar -xvzf v1.4.2.tar.gz
          cd miic_R_package-1.4.2
          R --silent -e "install.packages(c(\"igraph\", \"ppcor\", \"scales\", \"Rcpp\"))"
          R CMD INSTALL . --preclean
          cd ..
          # Install Python packages
          pip install --upgrade pip
          pip install wheel
          pip install PyDrive2==1.6.0 --use-feature=2020-resolver
          # Install DVC
          wget -c https://github.com/iterative/dvc/releases/download/1.4.0/dvc_1.4.0_amd64.deb
          sudo apt install ./dvc_1.4.0_amd64.deb
          # Run DVC
          dvc pull
          Rscript infer_network.R

          # Install CML
          npm init --yes
          npm i @dvcorg/cml@latest
          # Write your CML report
          echo "## Model Metrics" > report.md
          cat metrics.txt >> report.md
          echo "## Data visualization" >> report.md
          npx cml-publish network_diagram.png --md >> report.md
          npx cml-send-comment report.md

Hora de fazer um commit.

git add .
git commit -m 'Uses CML to improve PR feedback'
git push origin raw_alarm_dataset

Ok, agora após as checagens serem realizadas, você deve ver um comentário automático no seu Pull Request com a figura da rede, como abaixo.

Digamos que eu ache que muitas arestas foram removidas e talvez a rede não seja consistente. Irei alterar o código do script infer_network.R para fazer o MIIC buscar por uma rede consistente. A terceira linha deve ser agora como abaixo:

res <- miic(input_data = alarm_dataset, consistent='orientation')

Vamos salvar o estado atual do repositório local com um commit.

git add .
git commit -m 'Makes network consistent'
git push origin raw_alarm_dataset

Ok, agora está como eu queria e já posso aprovar o pull request 🙂 . Eu poderia fazer isso clicando no botão verde “Merge pull request” direto no site do GitHub ou eu poderia usar o gh novamente.

gh pr merge 1

Ele irá te fazer duas perguntas. Eu escolhi criar um commit de merge (mesclagem) e não remover a branch, nem localmente nem no GitHub. Para retornar a branch principal, a master, execute o seguinte comando:

git checkout master

Usando containers do Docker

Você provavelmente reparou que o GitHub Runner demora um certo tempo para fazer as checagens e dependendo de quantos pacotes você quer instalar, essa checagem pode demorar bastante. Um modo de evitar essa demora toda vez que você fizer um push é utilizar um container do Docker que já tem tudo instalado, softwares, pacotes, dependências, tudo. O modo como temos feito até agora está pronto para você adicionar o seu container, já que eu estou instalando o CML manualmente. Se você não quiser usar um container próprio, mas não quer ter que baixar e instalar o CML em todo push, podemos utilizar o container oficial do projeto CML.

Se você se lembra bem, há pouco nós aceitamos o pull request, o que significa que mesclamos aquela branch com a principal, a master. Embora o remote do GitHub esteja atualizado, a nossa cópia local não. Vamos atualizá-la com um git pull, e após isso criar uma nova branch.

git pull
git checkout -b cml_container

Altere o arquivo cml.yaml para ficar como no código abaixo.

scripts

Vamos adicionar esse arquivo ao índice do git, realizar um commit, push e criar um Pull Request (PR).

git add .
git commit -m 'Makes use of CML container'
git push origin cml_container
gh pr create --title 'Use CML container'

Após isso, tudo deve correr tranquilamente, como aconteceu aqui. Você pode mesclar o pull request e executar um git pull novamente para atualizar seu repositório local.

gh pr merge 2
git pull

O que mais?

O DVC não se limita a versionar datasets. Nós também poderíamos criar pipelines (sequências de operações, do download de nossos dados, passando por pré-processamento, inferência, pós-processamento, geração de relatórios, etc) e versioná-los, assim como os arquivos de saíde desses scripts como as imagens que o script infer_network.R gerou. Poderíamos ter vários scripts para etapas anterior e posterior a de inferência e tudo pode ser orquestrado pelo DVC através de um simples comando: dvc repro. Etapas que não precisam ser executadas (apenas datasets que são utilizados em uma etapa maios tardia foram modificados) não são executados. Em vez de ter vários scripts sendo chamados no seu GitHub Action file, teria apenas um dvc repro.

Além disso, poderíamos ter um container do Docker com tudo que não muda entre os commits já instalado. R, DVC, CML, MIIC, dependências do MIIC, entre outras coisas. Isso definitivamente nos pouparia de uma espera nada prazerosa, dependendo do que você for fazer. No nosso caso aqui, baixar, compilar e instalar o MIIC leva alguns minutos que poderiam ser poupados. Para o nosso simples exemplos, acho que não é o fim do mundo esperar alguns poucos minutos, mas se começar a ficar mais complexo, e com datasets maiores, o tempo pode simplesmente se tornar inviável, o que tornaria indispensável criar o seu container do Docker (Parte II dessa série) e utilizar sua própria infraestrutura computacional sincronizada com o GItHub Actions (Parte III).

Por hoje é só pessoal! 😉

Você provavelmente não estaria lendo esse texto se não fosse pela Elle O’Brien, que me mostrou tantas coisas sobre o CML, apresentações e exemplos, e o David Ortega que me ajudou a preparar o ambiente R dentro do container docker oficial do projeto CML.