Crie ferramentas em projetos de aprendizado de máquina, uma visão geral

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.

imagem

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:

  1. Especificando alguns destinos com dependências. Eu quero ver como fazê-lo e como é fácil.
  2. 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?
  3. 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.
  4. 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 . 

resultado
Dependê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}) # Additional files to clean set_property(DIRECTORY PROPERTY ADDITIONAL_MAKE_CLEAN_FILES ${CMAKE_CURRENT_SOURCE_DIR}/data/processed/processed.xlsx ) 

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:

 #!/usr/bin/python import os import sys sys.path.append(os.path.join(os.path.dirname(__file__), '.')) from pynt import task from path import Path import glob from src.data.download import pydownload_file from src.data.preprocess import pypreprocess iris_file = 'data/raw/iris.csv' processed_file = 'data/processed/processed.pickle' @task() def rawdata(): '''Download IRIS dataset''' pydownload_file('https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data', iris_file) @task() def clean(): '''Clean all build artifacts''' patterns = ['data/raw/*.csv', 'data/processed/*.pickle', 'data/processed/*.xlsx', 'reports/figures/*.png'] for pat in patterns: for fl in glob.glob(pat): Path(fl).remove() @task(rawdata) def preprocess(): '''Preprocess IRIS dataset''' pypreprocess(iris_file, processed_file, 'data/processed/processed.xlsx') 

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.
Definir destino com dependênciaConstruções incrementaisConstruções incrementais se o código fonte for alteradoCapacidade de descobrir quais artefatos remover durante o comando clean
CMakesimsimsimsim
Pyntsimnãonãonão
Pavimentadorasimnãonãonão
doitUm pouco simsimsimsim
Luigisimnãonãonão

Source: https://habr.com/ru/post/pt451962/


All Articles