Eu estava pensando sobre a estrutura do projeto / aprendizado de máquina / ciência de dados / fluxo de trabalho e estava lendo opiniões diferentes sobre o assunto. E quando as pessoas começam a falar sobre fluxo de trabalho, desejam que seus fluxos de trabalho sejam reproduzíveis. Existem muitas postagens por aí que sugerem o uso do
make para manter o fluxo de trabalho reproduzível. Embora
make
seja muito estável e amplamente utilizado, eu pessoalmente gosto de soluções de plataforma cruzada. Afinal, é 2019, não 1977. Pode-se argumentar que se faz multiplataforma, mas, na realidade, você terá problemas e gastará tempo consertando sua ferramenta em vez de fazer o trabalho real. Decidi dar uma olhada e verificar quais outras ferramentas estão disponíveis. Sim, eu decidi gastar algum tempo em ferramentas.
Esta postagem é mais um convite para um diálogo do que para um tutorial. Talvez sua solução seja perfeita. Se for, será interessante ouvir sobre isso.
Neste post, usarei um pequeno projeto Python e executarei as mesmas tarefas de automação com sistemas diferentes:
Haverá uma
tabela de comparação no final da postagem.
A maioria das ferramentas que examinarei são conhecidas como
software de automação de compilação ou
sistemas de compilação . Existem inúmeras em todos os diferentes sabores, tamanhos e complexidades. A idéia é a mesma: o desenvolvedor define regras para produzir alguns resultados de maneira automatizada e consistente. Por exemplo, um resultado pode ser uma imagem com um gráfico. Para criar essa imagem, é necessário fazer o download dos dados, limpá-los e fazer algumas manipulações de dados (exemplo clássico, na verdade). Você pode começar com alguns scripts de shell que farão o trabalho. Depois que você voltar ao projeto, um ano depois, será difícil lembrar de todas as etapas e da ordem que você precisa seguir para criar essa imagem. A solução óbvia é documentar todas as etapas. Boas notícias! Os sistemas de compilação permitem documentar as etapas em uma forma de programa de computador. Alguns sistemas de compilação são como os scripts de shell, mas com sinos e assobios adicionais.
A base desta publicação é uma série de publicações de
Mateusz Bednarski no fluxo de trabalho automatizado para um projeto de aprendizado de máquina. Mateusz explica seus pontos de vista e fornece receitas para o uso do
make
. Convido você a verificar as postagens dele primeiro. Usarei principalmente o código dele, mas com diferentes sistemas de compilação.
Se você quiser saber mais sobre o
make
,
make
seguir algumas referências para algumas postagens.
Brooke Kennedy fornece uma visão geral de alto nível em 5 etapas fáceis para tornar seu projeto de ciência de dados reproduzível.
Zachary Jones fornece mais detalhes sobre a sintaxe e os recursos, além dos links para outras postagens.
David Stevens escreve um post muito hype sobre por que você absolutamente precisa começar a usar o
make
imediatamente. Ele fornece bons exemplos comparando
a maneira antiga e
a nova .
Samuel Lampa , por outro lado, escreve sobre por que usar
make
é uma má idéia.
Minha seleção de sistemas de construção não é abrangente nem imparcial. Se você quiser fazer sua lista, a
Wikipedia pode ser um bom ponto de partida. Conforme mencionado acima,
abordarei CMake ,
PyBuilder ,
pynt ,
Paver ,
doit e
Luigi . A maioria das ferramentas nesta lista é baseada em python e faz sentido, pois o projeto está em Python. Esta postagem não abordará como instalar as ferramentas. Presumo que você seja bastante proficiente em Python.
Estou interessado principalmente em testar esta funcionalidade:
- Especificando alguns destinos com dependências. Eu quero ver como fazê-lo e como é fácil.
- Verificando se compilações incrementais são possíveis. Isso significa que o sistema de compilação não reconstruirá o que não foi alterado desde a última execução, ou seja, você não precisa baixar novamente seus dados brutos. Outra coisa que procurarei é compilações incrementais quando a dependência muda. Imagine que temos um gráfico de dependências
A -> B -> C
O alvo C
será reconstruído se B
mudar? Se um? - Verificando se a reconstrução será acionada se o código-fonte for alterado, ou seja, alteramos o parâmetro do gráfico gerado, da próxima vez que construirmos a imagem, ela deverá ser reconstruída.
- Verificando as maneiras de limpar artefatos de construção, ou seja, remover arquivos que foram criados durante a construção e reverter para o código-fonte limpo.
Não usarei todos os alvos de compilação do post de Mateusz, apenas três deles para ilustrar os princípios.
Todo o código está disponível no
GitHub .
CMake
O CMake é um gerador de script de construção, que gera arquivos de entrada para vários sistemas de construção. E seu nome significa marca de plataforma cruzada. O CMake é uma ferramenta de engenharia de software. Sua principal preocupação é sobre a criação de executáveis e bibliotecas. Portanto, o CMake sabe como criar
destinos a partir do código-fonte nos idiomas suportados. O CMake é executado em duas etapas: configuração e geração. Durante a configuração, é possível configurar a futura compilação de acordo com uma necessidade. Por exemplo, variáveis fornecidas pelo usuário são fornecidas durante esta etapa. A geração é normalmente simples e produz arquivos com os quais os sistemas de construção podem funcionar. Com o CMake, você ainda pode usar o
make
, mas, em vez de escrever diretamente o makefile, você cria um arquivo CMake, que irá gerar o makefile para você.
Outro conceito importante é que o CMake incentiva
compilações fora da fonte . Construções fora da fonte mantêm o código fonte longe de qualquer artefato produzido. Isso faz muito sentido para executáveis nos quais a base de código de fonte única pode ser compilada sob diferentes arquiteturas de CPU e sistemas operacionais. Essa abordagem, no entanto, pode contradizer a maneira como muitos cientistas de dados trabalham. Parece-me que a comunidade de ciência de dados tende a ter um alto acoplamento de dados, código e resultados.
Vamos ver o que precisamos para alcançar nossos objetivos com o CMake. Existem duas possibilidades para definir itens personalizados no CMake: destinos personalizados e comandos personalizados. Infelizmente, precisaremos usar os dois, o que resulta em mais digitação em comparação com o vanila makefile. Um destino personalizado é considerado sempre desatualizado, ou seja, se houver um destino para o download de dados brutos, o CMake sempre o fará novamente. Uma combinação de comando personalizado com destino personalizado permite manter os destinos atualizados.
Para o nosso projeto, criaremos um arquivo chamado
CMakeLists.txt e o colocaremos na raiz do projeto. Vamos conferir o conteúdo:
cmake_minimum_required(VERSION 3.14.0 FATAL_ERROR) project(Cmake_in_ml VERSION 0.1.0 LANGUAGES NONE)
Esta parte é básica. A segunda linha define o nome do seu projeto, versão e especifica que não usaremos nenhum suporte à linguagem incorporada (seno chamaremos de scripts Python).
Nosso primeiro destino fará o download do conjunto de dados IRIS:
SET(IRIS_URL "https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data" CACHE STRING "URL to the IRIS data") set(IRIS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/data/raw) set(IRIS_FILE ${IRIS_DIR}/iris.csv) ADD_CUSTOM_COMMAND(OUTPUT ${IRIS_FILE} COMMAND ${CMAKE_COMMAND} -E echo "Downloading IRIS." COMMAND python src/data/download.py ${IRIS_URL} ${IRIS_FILE} COMMAND ${CMAKE_COMMAND} -E echo "Done. Checkout ${IRIS_FILE}." WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) ADD_CUSTOM_TARGET(rawdata ALL DEPENDS ${IRIS_FILE})
A primeira linha define o parâmetro
IRIS_URL
, que é exposto ao usuário durante a etapa de configuração. Se você usar a GUI do CMake, poderá definir esta variável através da GUI:

Em seguida, definimos variáveis com o local baixado do conjunto de dados IRIS. Em seguida, adicionamos um comando personalizado, que produzirá
IRIS_FILE
conforme a saída. No final, definimos um dado
rawdata
destino personalizado que depende de
IRIS_FILE
o que significa que, para criar dados
rawdata
IRIS_FILE
deve ser construído. A opção
ALL
do destino personalizado diz que os dados
rawdata
serão um dos destinos padrão a serem construídos. Observe que eu uso
CMAKE_CURRENT_SOURCE_DIR
para manter os dados baixados na pasta de origem e não na pasta de compilação. Isso é apenas para torná-lo o mesmo que Mateusz.
Tudo bem, vamos ver como podemos usá-lo. Atualmente, estou executando-o no Windows com o compilador MinGW instalado. Pode ser necessário ajustar a configuração do gerador para suas necessidades (execute
cmake --help
para ver a lista de geradores disponíveis). Inicie o terminal e vá para a pasta pai do código-fonte e, em seguida:
mkdir overcome-the-chaos-build cd overcome-the-chaos-build cmake -G "MinGW Makefiles" ../overcome-the-chaos
resultado- Configuração concluída
- Gerando feito
- Os arquivos de compilação foram gravados em: C: / home / espaço de trabalho / superar o caos-compilar
Com o CMake moderno, podemos construir o projeto diretamente do CMake. Este comando chamará o comando
build all
:
cmake --build .
resultadoDependências de varredura de dados brutos de destino
[100%] Dados brutos de destino criados
Também podemos visualizar a lista de destinos disponíveis:
cmake --build . --target help
E podemos remover o arquivo baixado por:
cmake --build . --target clean
Veja que não precisamos criar o destino limpo manualmente.
Agora vamos para o próximo alvo - dados pré-processados do IRIS. Mateusz cria dois arquivos a partir de uma única função:
processed.pickle
e
processed.xlsx
Você pode ver como ele se sai limpando esse arquivo do Excel usando
rm
com curinga. Eu acho que essa não é uma abordagem muito boa. No CMake, temos duas opções de como lidar com isso. A primeira opção é usar a propriedade de diretório
ADDITIONAL_MAKE_CLEAN_FILES . O código será:
SET(PROCESSED_FILE ${CMAKE_CURRENT_SOURCE_DIR}/data/processed/processed.pickle) ADD_CUSTOM_COMMAND(OUTPUT ${PROCESSED_FILE} COMMAND python src/data/preprocess.py ${IRIS_FILE} ${PROCESSED_FILE} --excel data/processed/processed.xlsx WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} DEPENDS rawdata ${IRIS_FILE} ) ADD_CUSTOM_TARGET(preprocess DEPENDS ${PROCESSED_FILE})
A segunda opção é especificar uma lista de arquivos como uma saída de comando customizada:
LIST(APPEND PROCESSED_FILE "${CMAKE_CURRENT_SOURCE_DIR}/data/processed/processed.pickle" "${CMAKE_CURRENT_SOURCE_DIR}/data/processed/processed.xlsx" ) ADD_CUSTOM_COMMAND(OUTPUT ${PROCESSED_FILE} COMMAND python src/data/preprocess.py ${IRIS_FILE} data/processed/processed.pickle --excel data/processed/processed.xlsx WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} DEPENDS rawdata ${IRIS_FILE} src/data/preprocess.py ) ADD_CUSTOM_TARGET(preprocess DEPENDS ${PROCESSED_FILE})
Veja que nesse caso eu criei a lista, mas não a usei dentro do comando personalizado. Não conheço uma maneira de referenciar argumentos de saída de comando personalizado dentro dele.
Outra coisa interessante a ser observada é o uso de
depends
neste comando personalizado. Definimos a dependência não apenas de um destino personalizado, mas também da saída e do script python. Se não adicionarmos dependência ao
IRIS_FILE
, a modificação manual do
iris.csv
não resultará na reconstrução do destino de
preprocess
-processo. Bem, você não deve modificar os arquivos no diretório de construção manualmente, em primeiro lugar. Apenas deixando você saber. Mais detalhes no
post de Sam Thursfield . A dependência do script python é necessária para reconstruir o destino se o script python mudar.
E finalmente o terceiro alvo:
SET(EXPLORATORY_IMG ${CMAKE_CURRENT_SOURCE_DIR}/reports/figures/exploratory.png) ADD_CUSTOM_COMMAND(OUTPUT ${EXPLORATORY_IMG} COMMAND python src/visualization/exploratory.py ${PROCESSED_FILE} ${EXPLORATORY_IMG} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} DEPENDS ${PROCESSED_FILE} src/visualization/exploratory.py ) ADD_CUSTOM_TARGET(exploratory DEPENDS ${EXPLORATORY_IMG})
Esse objetivo é basicamente o mesmo que o segundo.
Para encerrar. O CMake parece confuso e mais difícil que o Make. De fato, muitas pessoas criticam o CMake por sua sintaxe. Na minha experiência, o entendimento chegará e é absolutamente possível entender até arquivos CMake muito complicados.
Você ainda se colará bastante, pois precisará passar as variáveis corretas. Não vejo uma maneira fácil de referenciar a saída de um comando personalizado em outro. Parece que é possível fazê-lo através de destinos personalizados.
Pybuilder
A parte do PyBuilder é muito curta. Eu usei o Python 3.7 no meu projeto e a versão atual do PyBuilder 0.11.17 não o suporta. A solução proposta é usar a versão de desenvolvimento. No entanto, essa versão é limitada ao pip v9. Pip é v19.3 até o momento da redação. Que chatice. Depois de brincar um pouco, não funcionou para mim. A avaliação do PyBuilder foi de curta duração.
pynt
Pynt é baseado em python, o que significa que podemos usar funções python diretamente. Não é necessário agrupá-los com
clique e fornecer uma interface de linha de comando. No entanto, o pynt também é capaz de executar comandos do shell. Vou usar funções python.
Os comandos de compilação são fornecidos em um arquivo
build.py
. Os alvos / tarefas são criados com decoradores de funções. As dependências da tarefa são fornecidas pelo mesmo decorador.
Como gostaria de usar funções python, preciso importá-las no script de construção. O Pynt não inclui o diretório atual como script python, portanto, escrever algo assim:
from src.data.download import pydownload_file
não vai funcionar. Temos que fazer:
import os import sys sys.path.append(os.path.join(os.path.dirname(__file__), '.')) from src.data.download import pydownload_file
Meu arquivo
build.py
inicial era assim:
E o destino do
preprocess
não funcionou. Ele estava constantemente reclamando dos argumentos de entrada da função
pypreprocess
. Parece que o Pynt não lida com argumentos de função opcionais muito bem. Eu tive que remover o argumento para criar o arquivo do Excel. Lembre-se disso se seu projeto tiver funções com argumentos opcionais.
Podemos executar o pynt na pasta do projeto e listar todos os destinos disponíveis:
pynt -l
resultado Tasks in build file build.py: clean Clean all build artifacts exploratory Make an image with pairwise distribution preprocess Preprocess IRIS dataset rawdata Download IRIS dataset Powered by pynt 0.8.2 - A Lightweight Python Build Tool.
Vamos fazer a distribuição aos pares:
pynt exploratory
resultado [ build.py - Starting task "rawdata" ] Downloading from https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data to data/raw/iris.csv [ build.py - Completed task "rawdata" ] [ build.py - Starting task "preprocess" ] Preprocessing data [ build.py - Completed task "preprocess" ] [ build.py - Starting task "exploratory" ] Plotting pairwise distribution... [ build.py - Completed task "exploratory" ]
Se agora executarmos o mesmo comando novamente (ou seja,
pynt exploratory
), haverá uma reconstrução completa. Pynt não rastreou que nada mudou.
Pavimentadora
Pavimentadora parece quase exatamente como Pynt. É um pouco diferente de uma maneira que define as dependências entre os alvos (outro decorador
@needs
). A Pavimentadora realiza uma reconstrução completa a cada vez e não funciona bem com funções que possuem argumentos opcionais. As instruções de compilação são encontradas no arquivo
pavement.py .
doit
Doit parece uma tentativa de criar uma ferramenta de automação de compilação verdadeiramente em python. Pode executar código python e comandos de shell. Parece bastante promissor. O que parece faltar (no contexto de nossos objetivos específicos) é a capacidade de lidar com dependências entre alvos. Digamos que queremos criar um pequeno pipeline em que a saída do destino A seja usada como entrada do destino B. E digamos que estamos usando arquivos como saídas, portanto, o destino A crie um arquivo denominado
outA
.

Para criar esse pipeline, precisamos especificar o arquivo
outA
duas vezes no destino A (como resultado de um destino, mas também retornar seu nome como parte da execução do destino). Em seguida, precisaremos especificá-lo como entrada para o destino B. Portanto, existem 3 locais no total onde precisamos fornecer informações sobre a
outA
. E mesmo depois de fazer isso, a modificação do arquivo
outA
não levará à reconstrução automática do destino B. Isso significa que, se pedirmos ao doit para construir o destino B, ele verificará apenas se o destino B está atualizado sem verificar nenhuma das dependências. Para superar isso, precisaremos especificar
outA
4 vezes - também como dependência de arquivo do destino B. Vejo isso como uma desvantagem. Make e CMake são capazes de lidar com essas situações corretamente.
Dependências no doit são baseadas em arquivo e expressas como seqüências de caracteres. Isso significa que as dependências
./myfile.txt
e
myfile.txt
são vistas como diferentes. Como escrevi acima, acho a maneira de passar informações de destino para destino (ao usar destinos em python) um pouco estranha. O destino possui uma lista de artefatos que será produzida, mas outro destino não pode usá-lo. Em vez disso, a função python, que constitui o destino, deve retornar um dicionário, que pode ser acessado em outro destino. Vamos ver em um exemplo:
def task_preprocess(): """Preprocess IRIS dataset""" pickle_file = 'data/processed/processed.pickle' excel_file = 'data/processed/processed.xlsx' return { 'file_dep': ['src/data/preprocess.py'], 'targets': [pickle_file, excel_file], 'actions': [doit_pypreprocess], 'getargs': {'input_file': ('rawdata', 'filename')}, 'clean': True, }
Aqui, o
preprocess
destino depende dos dados
rawdata
. A dependência é fornecida através da propriedade
getargs
. Ele diz que o argumento
input_file
da função
doit_pypreprocess
é o
filename
do
filename
de saída dos dados
rawdata
destino. Veja o exemplo completo no arquivo
dodo.py.Pode valer
a pena ler
as histórias de
sucesso do uso do doit. Definitivamente, possui recursos interessantes, como a capacidade de fornecer uma verificação de destino personalizada e atualizada.
Luigi
O Luigi se mantém afastado de outras ferramentas, pois é um sistema para construir dutos complexos. Apareceu no meu radar depois que um colega me disse que tentou o Make, nunca foi capaz de usá-lo no Windows / Linux e se mudou para Luigi.
Luigi visa sistemas prontos para produção. Ele vem com um servidor, que pode ser usado para visualizar suas tarefas ou para obter um histórico de execuções de tarefas. O servidor é chamado de
agendador central . Um agendador local está disponível para fins de depuração.
Luigi também é diferente de outros sistemas de uma maneira como as tarefas são criadas. O Lugi não age em arquivos pré-definidos (como
dodo.py
,
pavement.py
ou makefile). Em vez disso, é preciso passar um nome de módulo python. Portanto, se tentarmos usá-lo da mesma maneira que outras ferramentas (coloque um arquivo com tarefas na raiz do projeto), ele não funcionará. Temos que instalar nosso projeto ou modificar a variável ambiental
PYTHONPATH
adicionando o caminho ao projeto.
O que é ótimo no luigi é a maneira de especificar dependências entre tarefas. Cada tarefa é uma classe. A
output
método informa ao Luigi onde os resultados da tarefa serão finalizados. Os resultados podem ser um único elemento ou uma lista. O método
requires
especifica as dependências da tarefa (outras tarefas; embora seja possível fazer uma dependência por si só). E é isso. O que for especificado como
output
na tarefa A será passado como entrada para a tarefa B se a tarefa B se basear na tarefa A.

Luigi não se importa com modificações de arquivo. Ele se preocupa com a existência do arquivo. Portanto, não é possível disparar recriações quando o código fonte é alterado. Luigi não tem uma funcionalidade
limpa embutida.
As tarefas do Luigi para este projeto estão disponíveis no arquivo
luigitasks.py . Eu os corro do terminal:
luigi --local-scheduler --module luigitasks Exploratory
Comparação
A tabela abaixo resume como os diferentes sistemas funcionam em relação aos nossos objetivos específicos.