在机器学习项目中构建工具,概述

我想知道机器学习/数据科学项目的结构/工作流程,并在阅读有关该主题的不同意见。 当人们开始谈论工作流时,他们希望其工作流具有可复制性。 有很多建议使用make来保持工作流程可重复的帖子。 尽管make非常稳定且使用广泛,但我个人还是喜欢跨平台解决方案。 毕竟是2019年,而不是1977年。有人可以说使自己是跨平台的,但实际上,您会遇到麻烦,并且会花时间修复工具而不是进行实际工作。 因此,我决定环顾四周,并查看可用的其他工具。 是的,我决定花一些时间在工具上。

图片

这篇文章更多是对话邀请而不是教程。 也许您的解决方案是完美的。 如果是这样,那么听到它将会很有趣。

在本文中,我将使用一个小的Python项目,并在不同的系统上执行相同的自动化任务:


帖子末尾会有一个比较表

我将介绍的大多数工具都称为构建自动化软件构建系统 。 它们有无数种,口味各异,大小和复杂程度各不相同。 想法是一样的:开发人员定义规则以自动且一致的方式产生一些结果。 例如,结果可能是带有图形的图像。 为了制作此图像,需要下载数据,清理数据并进行一些数据操作(确实是经典示例)。 您可以从几个可以完成此工作的shell脚本开始。 一年后返回项目后,将很难记住制作该图像所需的所有步骤及其顺序。 显而易见的解决方案是记录所有步骤。 好消息! 构建系统使您可以以计算机程序的形式记录步骤。 一些构建系统就像您的shell脚本一样,但是带有额外的花哨功能。

这篇文章的基础是Mateusz Bednarski关于机器学习项目的自动化工作流的一系列文章。 Mateusz解释了他的观点,并提供了使用make 。 我鼓励您先检查一下他的帖子。 我将主要使用他的代码,但使用不同的构建系统。

如果您想进一步了解make ,请参考以下几篇文章。 布鲁克·肯尼迪(Brooke Kennedy)在使您的数据科学项目可重现的5个简单步骤中进行了概述。 Zachary Jones提供了有关语法和功能的更多详细信息,以及指向其他文章的链接。 大卫·史蒂文斯David Stevens)写了一篇非常炒作的文章,介绍了为什么您绝对必须立即开始使用make 。 他提供了比较旧方法新方法的很好的例子。 另一方面, 塞缪尔·兰帕Samuel Lampa)写道,为什么使用make是个坏主意。

我对构建系统的选择既不全面也不偏颇。 如果要列出您的清单,则Wikipedia可能是一个不错的起点。 如上所述,我将介绍CMakePyBuilderpyntPaverdoitLuigi 。 此列表中的大多数工具都是基于python的,这很有意义,因为该项目使用的是Python。 这篇文章不会介绍如何安装工具。 我假设您相当精通Python。

我对测试此功能最感兴趣:

  1. 指定具有依赖关系的两个目标。 我想看看它是如何做到的以及它是多么容易。
  2. 检查是否可以进行增量构建。 这意味着构建系统将不会重建自上次运行以来未更改的内容,即您不需要重新下载原始数据。 我还将寻找的另一件事是,依赖关系发生变化时,增量构建。 想象一下,我们有一个依赖图A -> B -> C 如果B发生变化,是否会重建目标C ? 如果一个?
  3. 如果更改了源代码,即更改生成的图形的参数,则检查是否会触发重新构建,下次构建图像​​时必须重新构建。
  4. 检查清理构建构件的方法,即删除在构建过程中创建的文件,并回滚到干净的源代码。

我不会使用Mateusz帖子中的所有构建目标,仅使用其中三个来说明原理。

所有代码都可以在GitHub找到

CMake的


CMake是一个构建脚本生成器,它为各种构建系统生成输入文件。 它的名字代表跨平台的制作。 CMake是一种软件工程工具。 主要关注的是构建可执行文件和库。 因此,CMake知道如何根据支持的语言从源代码构建目标 。 CMake分两个步骤执行:配置和生成。 在配置期间,可以根据一种需求配置将来的版本。 例如,在此步骤中给出了用户提供的变量。 生成通常很简单,并且生成构建系统可以使用的文件。 使用CMake,您仍然可以使用make ,但是您可以直接写一个CMake文件,而不是直接编写makefile,这将为您生成makefile。

另一个重要的概念是CMake鼓励进行源外构建 。 源外构建使源代码远离其产生的任何工件。 这对于可执行文件可能具有很大意义,在可执行文件中,单个源代码库可以在不同的CPU体系结构和操作系统下进行编译。 但是,这种方法可能与许多数据科学家的工作方式相矛盾。 在我看来,数据科学界倾向于将数据,代码和结果高度结合在一起。

让我们看看使用CMake实现目标所需要的。 在CMake中定义自定义内容有两种可能性:自定义目标和自定义命令。 不幸的是,与vanila makefile相比,我们将需要同时使用两者,这将导致更多的键入。 自定义目标被认为总是过时的,即,如果有用于下载原始数据的目标,CMake将始终重新下载它。 将自定义命令与自定义目标结合使用可使目标保持最新状态。

对于我们的项目,我们将创建一个名为CMakeLists.txt的文件,并将其放在项目的根目录中。 让我们检查一下内容:

 cmake_minimum_required(VERSION 3.14.0 FATAL_ERROR) project(Cmake_in_ml VERSION 0.1.0 LANGUAGES NONE) 

这部分是基本的。 第二行定义项目的名称,版本,并指定我们将不使用任何内置语言支持(正弦,我们将称为Python脚本)。

我们的第一个目标将下载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}) 

第一行定义参数IRIS_URL ,该参数在配置步骤中向用户公开。 如果使用CMake GUI,则可以通过GUI设置此变量:



接下来,我们使用IRIS数据集的下载位置定义变量。 然后,我们添加一个自定义命令,该命令将在输出时生成IRIS_FILE 。 最后,我们定义了一个依赖于IRIS_FILE的自定义目标rawdata ,这意味着要构建rawdata IRIS_FILE必须构建rawdata 。 自定义目标的选项ALL表示rawdata将是要构建的默认目标之一。 请注意,我使用CMAKE_CURRENT_SOURCE_DIR来将下载的数据保留在源文件夹中,而不是构建文件夹中。 这仅仅是为了使其与Mateusz相同。

好吧,让我们看看如何使用它。 我目前正在装有已安装MinGW编译器的Windows上运行它。 您可能需要根据需要调整发电机设置(运行cmake --help以查看可用发电机的列表)。 启动终端并转到源代码的父文件夹,然后:

 mkdir overcome-the-chaos-build cd overcome-the-chaos-build cmake -G "MinGW Makefiles" ../overcome-the-chaos 

结果
-配置完成
-完成生成
-构建文件已写入到:C:/ home / workspace /克服混乱构建

使用现代CMake,我们可以直接从CMake构建项目。 该命令将调用build all命令:

 cmake --build . 

结果
扫描目标原始数据的依存关系
[100%]建立目标原始数据

我们还可以查看可用目标的列表:

 cmake --build . --target help 

我们可以通过以下方式删除下载的文件:

 cmake --build . --target clean 

看到我们不需要手动创建清理目标。

现在,我们移至下一个目标-预处理的IRIS数据。 Mateusz通过一个函数创建两个文件: processed.pickleprocessed.xlsx 。 您可以看到他如何通过将rm与通配符一起使用来清理此Excel文件。 我认为这不是一个很好的方法。 在CMake中,我们有两种方法来处理它。 第一种选择是使用ADDITIONAL_MAKE_CLEAN_FILES目录属性。 代码将是:

 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 ) 

第二个选项是指定文件列表作为自定义命令输出:

 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}) 

看到在这种情况下,我创建了列表,但是没有在自定义命令中使用它。 我不知道在其中引用自定义命令的输出参数的方法。

要注意的另一件有趣的事情是此自定义命令中depends的用法。 我们不仅从自定义目标设置了依赖关系,还设置了它的输出以及python脚本。 如果我们不向IRIS_FILE添加依赖IRIS_FILE ,那么手动修改iris.csv将不会导致重建preprocess目标。 好吧,您不应该首先手动修改构建目录中的文件。 只是让你知道。 有关更多详细信息,请参见Sam Thursfield的文章 。 如果python脚本发生更改,则需要依赖python脚本来重建目标。

最后是第三个目标:

 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}) 

该目标与第二个目标基本相同。

总结一下。 CMake看起来比较混乱,比Make更难。 确实,很多人批评CMake的语法。 以我的经验,理解将会来临,并且完全有可能理解甚至非常复杂的CMake文件。

您仍然会做很多事情,因为您需要传递正确的变量。 我没有看到一种简单的方法来引用另一个自定义命令的输出。 似乎可以通过自定义目标来实现。

Pybuilder


PyBuilder部分很短。 我在项目中使用了Python 3.7,而PyBuilder当前版本0.11.17不支持它。 建议的解决方案是使用开发版本。 但是,该版本仅限于pip v9。 撰写本文时,点数为v19.3。 mm 稍微摆弄一下之后,它根本对我不起作用。 PyBuilder评估是短暂的。

nt


Pynt是基于python的,这意味着我们可以直接使用python函数。 不必通过单击将它们包装起来并提供命令行界面。 但是,pynt也能够执行shell命令。 我将使用python函数。

生成命令在文件build.py中给出。 使用功能装饰器创建目标/任务。 任务依赖项通过同一装饰器提供。

由于我想使用python函数,因此需要在构建脚本中导入它们。 Pynt不包含当前目录作为python脚本,因此编写如下的smth:

 from src.data.download import pydownload_file 

将无法正常工作。 我们必须做:

 import os import sys sys.path.append(os.path.join(os.path.dirname(__file__), '.')) from src.data.download import pydownload_file 

我最初的build.py文件是这样的:

 #!/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') 

而且preprocess目标无效。 一直在抱怨pypreprocess函数的输入参数。 似乎Pynt不能很好地处理可选函数参数。 我必须删除用于制作excel文件的参数。 如果您的项目具有带有可选参数的函数,请记住这一点。

我们可以从项目的文件夹运行pynt并列出所有可用的目标:

 pynt -l 

结果
 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. 


让我们进行成对分布:

 pynt exploratory 

结果
 [ 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" ] 


如果我们现在再次运行相同的命令(即pynt exploratory ),将进行完全重建。 Pynt没有追踪到没有任何变化。

摊铺机


摊铺机看上去几乎与Pynt一样。 它在定义目标之间的依赖关系(另一种装饰器@needs )的方式上略有不同。 Paver每次都会进行完全重建,因此不能与带有可选参数的函数配合使用。 构建说明位于pavement.py文件中。


Doit似乎是在尝试用python创建真正的构建自动化工具。 它可以执行python代码和shell命令。 看起来很有希望。 在我们特定目标的背景下,似乎遗漏的是处理目标之间依赖关系的能力。 假设我们要建立一个小的管道,其中将目标A的输出用作目标B的输入。并且假设我们将文件用作输出,因此目标A创建了一个名为outA的文件。



为了建立这样的管道,我们将需要在目标A中两次指定文件outA (作为目标的结果,但还要在执行目标时返回其名称)。 然后,需要将其指定为目标B的输入。因此,总共需要提供3个地方来提供有关文件outA信息。 而且即使这样做,修改文件outA也不会导致目标B的自动重建。这意味着,如果我们要求doit构建目标B,则doit将仅检查目标B是否为最新,而不检查任何目标B。的依赖关系。 为了克服这个问题,我们将需要指定4次outA也作为目标B的文件依赖项。我认为这是一个缺点。 Make和CMake都能够正确处理这种情况。

doit中的依赖项基于文件,并表示为字符串。 这意味着依赖项./myfile.txtmyfile.txt被视为不同。 就像我在上面写的,我发现将信息从目标传递到目标的方式(使用python目标时)有些奇怪。 目标有一个将要产生的工件列表,但是另一个目标不能使用它。 相反,构成目标的python函数必须返回一个字典,该字典可以在另一个目标中访问。 让我们看一个例子:

 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, } 

在此,目标preprocess取决于rawdata 。 依赖关系通过getargs属性提供。 它说函数doit_pypreprocess的参数input_file是目标rawdata的输出filename 。 看一下dodo.py文件中的完整示例。

值得一读使用doit 的成功案例 。 它绝对具有不错的功能,例如提供自定义最新目标检查的功能。

路易吉


Luigi与其他工具分开,因为它是一个用于构建复杂管道的系统。 一位同事告诉我他尝试过Make,但从未在Windows / Linux上使用过它,后来搬到了Luigi,它出现在我的雷达上。

Luigi致力于生产就绪系统。 它带有一个服务器,该服务器可用于可视化您的任务或获取任务执行的历史记录。 该服务器称为中央调度程序 。 本地调度程序可用于调试目的。

Luigi在创建任务的方式上也与其他系统不同。 Lugi不会对某些预定义文件(例如dodo.pypavement.py或makefile) dodo.py 。 相反,必须传递一个python模块名称。 因此,如果我们尝试以与其他工具类似的方式使用它(将带有任务的文件放在项目的根目录中),它将无法正常工作。 我们必须安装项目或通过将路径添加到项目来修改环境变量PYTHONPATH

luigi的优点是指定任务之间的依赖关系的方法。 每个任务都是一个类。 方法output告诉Luigi任务的结果将在哪里结束。 结果可以是单个元素或列表。 方法requires指定任务依赖项(其他任务;尽管可以从自身创建依赖项)。 就是这样。 如果任务B依赖任务A,则在任务A中指定为output的任何内容都将作为输入传递给任务B。


Luigi不在乎文件修改。 它关心文件的存在。 因此,当源代码更改时,不可能触发重建。 路易吉(Luigi)没有内置的清理功能。

该项目的Luigi任务可在luigitasks.py文件中找到 。 我从终端运行它们:

 luigi --local-scheduler --module luigitasks Exploratory 

比较方式


下表总结了不同系统在实现我们特定目标方面的工作方式。
用依赖定义目标增量构建如果源代码更改,则增量构建能够确定在clean命令期间要删除的工件
CMake的是的是的是的是的
平特是的没有啦没有啦没有啦
摊铺机是的没有啦没有啦没有啦
是的是的是的是的
路易吉是的没有啦没有啦没有啦

Source: https://habr.com/ru/post/zh-CN451962/


All Articles