机器学习可重复性危机下,创建复杂数据系统的挑战
导语
论文题目:
Navigating the development challenges in creating complex data systems
论文地址:
https://www.nature.com/articles/s42256-023-00665-x
一、引言
二、核心问题
三、为什么复杂数据科学系统具有挑战性
四、将软件开发方法论用于开发复杂系统
五、数据科学系统开发的新建议
六、结语
一、引言
一、引言
机器学习(machine learning,ML)正面临可重复性危机[1-3]。我们认为这主要源于代码质量低下,其根本原因有两个:产出优质代码的激励机制不足,以及广泛的软件工程技能匮乏。这一危机还暗示,如果在数据科学系统(data science systems,DSS)的开发过程中没有持续进行验证和测试的基础工具,系统可能会悄然崩溃[4-6]。
现代数据科学系统是极度复杂的系统,含有许多组件,其中许多组件对数据或底层代码的微小变化极其敏感。成功开发数据科学系统涉及三个关键的群体:研究人员、专业软件工程师以及工程师的激励者。所谓激励者,是以各种方式推动数据科学系统开发人员产出有价值结果的人,比如研究指导者、雇主、资金提供方和期刊编辑。这些群体可以各自工作或联合工作,但他们接受的培训和获得的回报各不相同。
尽管在学术界和工业界设计和实施数据科学系统的研究人员会意识到自己领域中数据的特殊复杂性,但他们往往缺乏在软件开发方法论方面的正规训练。因此,数据科学系统通常只是为了满足研究人员的目标和激励而被设计构建的。然而,软件工程师则专门接受复杂系统开发的训练,并被激励去创造出灵活、组织良好且可持续维护的系统。
盖尔定律(Gall’s law,从系统论角度认为从头开始设计复杂系统不可行,应当从简单系统开始逐渐演变升级)[7]指出,复杂系统无法被建造,只能生长出来,敏捷等软件开发方法也隐含了我们无法预先设计复杂系统的观点。相反,我们需要按照盖尔定律去演化和孕育复杂系统。在软件开发中,开发一个最小可行产品是一项关键要求。对于数据管道(data pipelines,指一组数据处理步骤所组成的过程,一般包括收集、清理、转化、储存、机器学习分析等,即数据流程或数据流水线)来说,最小可行产品通常被称为“钢线”(steel thread)[8],即构建稳定路径的初始阶段,之后可以逐步扩展以构建更完整的流程。通过有效利用反馈回路和重复测试,我们可以在不偏离已有工作代码库的前提下评估代码的正确性。
当前机器学习可重复性危机,实际上源于数据科学家在没有系统地开发或对其代码的正确性进行细致、持续评估的情况下,构建了复杂的数据科学系统。尽管错误的代码在计算上能够重现——即重新运行代码会得到相同的结果——但对于可复制性(replicability,强调在完全相同条件下重做实验,得到一样的结果)和一般的可重复性(reproducibility,强调在数据或方法变化的条件下,仍然可以得到一致或类似的结果)来说,使用独立实现验证其正确性至关重要。更为关键的是,可重用性(reusability,强调一个系统、组件或资源在不同环境或应用中,能够被多次使用)——也就是站在巨人肩膀上——要求高度的准确性。对于数据科学系统来说,一个具有良好可重用性的模型或算法意味着它可以在不同的项目、任务或数据集上使用,并且能够产生一致和准确的结果。事实上,如果缺乏正确性,每个后续任务都将承受其缺乏正确性和可重复性带来的问题。
由于机器学习的普及,开发数据科学系统的群体现已庞大且多元化。因此,在这篇文章中,我们针对那些数据科学系统的开发者及其激励者提出了具体的挑战,并推荐了提高数据科学系统可重用性和可复现性的具体措施。我们还提出了一种新的开发理念,将软件开发方法形式化,以适应数据科学研究人员的需求。
二、核心问题
二、核心问题
(一)数据×代码=复杂性2
近年以来,统计学家已经在数据分析领域占据了主导地位。他们接受高度训练,精于理解数据中的复杂关系和偏见,并运用相对简单(正面意义上)的方法来分析数据并拟合模型。数据收集往往在他们的指导下进行,以确保理解、记录并减轻偏见。如今,数据已无处不在,被称为“新石油”。然而,现实世界的数据集往往更像一场石油泄漏,充满了诸多未知(甚至不可知)的偏见[9]。
数据科学系统的开发者必须对数据和代码中的复杂性,以及交互所产生的复杂性,保持容忍。虽然软件工程能够驾驭代码的复杂性,但将代码与数据结合则是在复杂性之上叠加复杂性。因此,构建一个数据科学系统可能类似于在一根棍子上平衡另一根棍子。结果就是,如果缺乏足够的统计学和软件工程技巧,开发数据科学系统往往会导致以下影响:
大数据=>混乱数据=>大代码=>混乱代码=>错误结论
(二)数据和代码库的寒武纪爆发
数据规模的激增和机器学习工具的应用范围扩大,也引发了对它们使用方式的彻底转变。从数据和代码库的视角,这种范式转变就像寒武纪大爆发,其数量和内生复杂性(而非由于处理或描述方式造成的复杂性)均大幅度增加。因此,数据科学系统研究者所运用的软件工具,比传统统计学家要多得多。
作为众多工具的使用者,我们不得不将注意力集中在如何与这些工具交互上(而非深入理解它们的内部运作)。因此,底层软件必须是值得信赖的。我们必须假设它几乎无漏洞,其它任何残留漏洞都微不足道。
这种转变使得在代码中表达和构建分析计划成为所有数据科学项目的基石。然而,软件工程是一门充满挑战的学科,在庞大且陌生的代码库上构建项目,往往会带来意料之外的后果。
三、为什么复杂数据科学系统具有挑战性
三、为什么复杂数据科学系统具有挑战性
本节将探讨数据科学家在开发准确、高效的数据科学系统时所面临的一些重大挑战,这些挑战既包括技术性的,也包括由人为驱动的。
(一)挑战一:数据科学系统研究者缺乏软件工程和软件开发技能
大多数数据科学家只熟悉编写小型代码库,然而软件开发重在构建互相连接的模块和组件,每个部分都是更大代码库的独立组成部分。代码是众多数据科学工具的接口,软件工程则是有条不紊地组织这些接口的学科。在本文中,我们将软件工程定义为管理代码和数据复杂性的学问,接口是其主要手段之一[10]。尽管许多软件开发实践主要关注企业级软件,并不适用于所有数据科学系统的组件,但我们坚信,在数据科学项目中,软件开发方法论应当扮演更重要的角色。
(二)挑战二:正确性和有效性
数据科学系统必须能正确运作,也就是按照设想的方式执行。同时,它也必须具有效,也就是能产生相关且可用的预测结果。如前所述,缺乏软件工程的引导,往往会引发以下几个问题:
多个实验 => 混乱代码 => 不正确的结论
那么,为何我们需要确保一个可信度高、性能高的模型要具备正确性和有效性呢?首先,虽然已发布且可执行的代码库能提供计算的可复现性,但要达到真正的可重复性,则需要其正确无误。其次,虽然一个错误的数据科学系统可能因为某个幸运的漏洞而侥幸运行良好,但它是难以解读和修改的。如果失去了正确性,我们就无法理解、解读或信任数据科学系统产生的结果或基于结果形成的结论(见图1)。
图1 代码正确或不正确以及有效或无效的后果。
(三)挑战三:对研究人员的逆向激励
软件工程师因为打造高性能、文档齐全且可重用的代码库而得到奖励,工业数据科学家因为数据科学系统对业务的实用性而得到奖励。然而,研究型数据科学家所处的激励体系却截然不同。他们被鼓励利用数据科学系统的输出来撰写新颖论文,以推动自身领域的发展、申请资金支持,并提升自身声望及职业前景。这对研究者来说,造成时间上的冲突:短期来看,快速发表论文并对代码库的可重用性投入较少精力是有所回报的;然而长远看,可重用的数据科学系统能增加相关论文影响力和被引次数的可能性。这种逆向激励(perverse incentivization,与长期目标相悖的短期激励机制)可能阻碍了研究型数据科学家生成和发布易于大众理解的代码,以避免被竞争者“抢先”。
如果某个领域的激励机制与期望目标不符,那么最省力的道路往往会胜出。激励者必须认识到他们的激励是否与长远目标相左,并在必要时纠偏。尤其是期刊,必须认识到对论文新颖性的需求可能导致研究者在数据科学系统中人为地增加复杂性,以提高论文被接受的概率。
(四)挑战四:走捷径
强大的数据分析和机器学习工具的广泛应用,使得热衷于此的业余爱好者能迅速开发复杂的数据科学系统。这并不意味着使用强大而公开可用的工具或走此类捷径在本质上就是错误的。而是说,如果每个从业者都编写个性化的常用工具包,这将成为错误频发的主要来源。虽然强大的工具减少了偶然复杂性,却没有减少数据科学系统的内生复杂性。因此,它们使得建立具有高度内生复杂性的系统变得更为容易。管理这种内生复杂性极其困难,尤其是当它往往隐藏在细微之处时。
(五)挑战五:团队协作与个人工作
在同一个代码库上进行团队协作可以显著提高效率。然而,如果缺乏适当的培训或组织结构,这也可能引发巨大的低效和错误——毕竟团队本身就是复杂系统。软件工程师通常接受过有效团队协作方法论的高级培训,如SCRUM敏捷开发框架[11,12]。他们也知道如何充分利用基础设施的优势,比如版本控制、持续集成流水线和配对编程。与软件工程师相比,研究人员只经历过提升团队协作工具的非正式培训。并且有时为了确保个人贡献的明确性,他们甚至会被限制团队协作。
(六)挑战六:弥合学术界与工业界之间的鸿沟
工业界和学术界的数据科学项目有很多共同点。然而,除了前文讨论过的激励差异之外,数据科学系统的开发环境也存在关键差异。在工业界,由于深厚的软件工程文化,人们会普遍认为高质量代码是维护数据科学系统所必需的;但学术研究人员通常不能获得这种激励,因为其项目往往期限很短。在学术界中,尽管有许多优势,尤其是探索全新想法的自由,但其激励机制却倾向于强化对代码一次性使用、即用即弃的心态,并且学术界几乎没有针对代码质量的反馈机制。高质量的代码既不是大部分学术出版物所必需的,也并未被用来评价工作表现。
(七)挑战七:训练一个数据科学系统的成本很高
数据科学系统的变动可能需要昂贵且漫长的重新培训,以检查它是否以及如何改变结果。因此,看似微小的修复、改进和代码清理可能根本不会发生。
(八)挑战八:长期维护
即使是一个小型的数据科学系统,由于其复杂性,所依赖的软件包数量也很容易达到几十个。由于复杂系统本质上是脆弱的,软件依赖关系的微小改变可能导致整个数据科学系统(可能悄无声息地)出现故障。这是长期维护代码成本高昂或不可能长期维护的诸多原因之一。尽管有许多措施可以促进计算结果的可重复性,例如发布Python/Anaconda环境和测试套件,但它们并不能确保在更大规模的数据科学系统中实现未来的可重用性。
(九)挑战的总结
许多研究人员系统性地忽视了软件工程在现代数据科学中所占据的核心地位。这种情况源于正规培训匮乏以及激励机制扭曲,二者共同造成了巨大的价值创造机会损失。数据科学系统必须既具正确性又具有效性。现代数据科学工具的易用性释放了巨大潜力,推动了实质进展。然而,这也导致了众多看似有效但实际错误的系统的诞生。工业界在开发高质量代码方面具有天然优势,因为它需要与基础设施、团队和部署平台进行整合。而学术界缺乏这样的指导原则,代码开发可能是短视的。
四、将软件开发方法论用于开发复杂系统
四、将软件开发方法论用于开发复杂系统
每个程序员都可以编写小型代码库,但要正确执行和维护更大的代码库则需要软件开发方法论[13]。那么,为何通常很难从零开始构建复杂系统呢?
复杂系统往往由许多高度相互连接的组件构成,这些组件对微小扰动极为敏感。就代码库而言,这些扰动可能是简单的拼写错误——仅语法错误尚且是幸运的。然而,一个简单的拼写错误也可能以未知方式微妙地改变结果,导致戏剧性、意料之外的后果。
反之,我们应该采用小步渐进的方式,永远不要偏离一个有效运行的系统太远。盖尔定律表明,复杂系统必须是逐渐生长而非一步到位的。这对数据科学家来说具有重大价值,因为我们主张,通过逐步发展一个含n个组件的系统,可以将构建复杂性从O(n2)降(最高也是最坏的情况)降至O(n)。
尽管人们常能将复杂系统分解为主要简单的组件,但它们的数量庞大且相互作用迅速形成了一个复杂整体。若想构建一个含n个组件的系统,则最多有O(n2)次交互作用,因此存在O(n2)个可能的故障点(假设每个组件都正常运行)。针对“O(n2)问题”,软件工程领域已经发展出两种主要解决方案:软件架构和敏捷开发方法[13,14]。
(一)软件架构
良好的代码开发原则是软件开发的关键组成部分。其中一个基本原则是关注点分离,即将软件划分为处理各自独立任务的不同组件,每个组件都具有简单、隐藏复杂性的接口。这些组件由连接较低层次独立组件形成。以此方式设计软件架构,可以将不同组件之间可能存在的密集连接图(具有O(n²)个连接)减少为稀疏图,大大减少了潜在故障点。同时,在代码和图中保持一种局部性也是有益的,使得各个组件尽可能地形成局部连接。图2提供了此情况的可视化展示。
(二)敏捷开发
现代软件开发倾向于遵循敏捷方法论,即通过逐步增加或更改一个组件来持续构建软件,以确保系统始终可运行。我们只需考虑新组件如何与现有的n个组件交互。这将潜在的O(n²)个故障点,减少为每个步骤的O(n)个可能故障点。最终,当复杂系统增长时,这种方式将构建复杂性从O(n²)降低到O(n)。图3提供了可视化展示。
图3 敏捷开发的可视化。一些软件工程实践需要牺牲短期进展以换取长期进展。
五、数据科学系统开发的新建议
五、数据科学系统开发的新建议
现在,我们将讨论如何把这些原则结合并概括成可操作的建议,供数据科学家参考。遵循这些建议,他们能在更短时间内编写出正确且高效的代码。
(一)不是构建复杂系统,而是让其自然生长
我们已经讨论过如何通过逐步发展数据科学系统来将构建复杂性从O(n²)降低到O(n)。对于复杂系统,一次性规划整个系统然后全部构建是行不通的。我们必须逐步发展数据科学系统,以保持每个增量阶段的复杂性最高为O(n)。理想情况下,通过良好的软件架构甚至能实现低至O(1)的复杂性。
规划仍然是必要的,它能确保我们持续朝着期望的目标发展系统,但它应该是高度迭代的,并与增量实施步骤交替进行。规划不仅引领着迭代过程,还有助于在数据科学系统演化过程中避免陷入局部最优。值得注意的是,复杂系统的未来演变会越来越模糊。规划应采用多尺度方法[15-17],并对未来细节进行适当折现。一个很好的例子是将SpaceX火箭开发过程与传统方法相比较。火箭的设计是通过对许多迭代适应实例的测试而逐步完善的,每一次都比上一次更接近成功。在开发数据科学系统及任何复杂系统时[18],我们必须深刻理解并接受一个事实:这种迭代过程不可避免地会出现错误。
(二)反馈回路的性质和必要性
反馈回路(feedback loop)的力量使得逐步、迭代式的开发极其有效。在建立反馈回路时,有两个重要属性值得考虑:对齐性(alignment)和周期时长(cycle time),见图4。
对于对齐性,我们要考虑反馈回路测量了关于特定代码组件的多少假设?将反馈回路与我们对代码的目标进行对齐极为关键。如果对齐性不佳,反馈回路无法给我们提供对组件可信度的保证。
在讨论反馈周期时长时,我们要思考获取反馈需要花费多少时间(或成本)?如果运行耗时过长,100%对齐的反馈回路也将变得无用。理想情况下,我们希望周期更短以便实现高频反馈。
编写测试套件对于建立代码反馈机制极为有效。每个测试的执行都会针对代码的某一特定方面提供反馈。以高对齐性和短周期时间(即快速并频繁运行)来响应测试套件的信号,能建立起强大的反馈回路。阅读代码也会产生反馈信号,其强度由代码的可读性决定。
如前所述,数据科学家应同时关注模型的效力和代码的正确性。因此,我们需要能衡量这两方面的反馈回路:通过在测试集上评估模型来衡量数据科学系统的效力;而通过测试套件以及使代码尽可能易读,来衡量其正确性(或可信度)。
(三)数据科学系统的软件架构
良好的软件架构能减少与系统新增部分相连的组件数量,从而降低系统的构建复杂性。良好的架构还能显著提高代码的可读性,并使代码库在未来更具灵活性[19]。我们主张,对于数据科学系统而言,关键的架构概念是水平分层结构(horizontal layers),如图5所示。
水平软件层指的是分析流程中的不同组成部分,比如数据加载、预处理、模型训练和评估。我们可以将每一层看作是软件中的高级组件。因此,与其说是与其他层之间的连接,不如说是内部连接应该更为主导。每一层都像是一个复杂性的小宇宙,在系统的其他组件面前隐藏了其自身的复杂性。
特征工程和模型工程是机器学习中至关重要的两个任务,我们可以理解为它们都在对数据提出问题。我们经常问的最终问题是“在给定特征、特定预处理和具体模型的情况下,如何能很好地预测一些标签?”回答这个问题需要编写完整的流程以建立有效反馈。这个漫长的等待过程与敏捷方法的理念相违背。
因此,我们建议在水平软件层的尺度上来组织数据处理流程,并以简约而不完全的方式构建每一层,这样可以在项目早期就建立起各层之间的基本连接(如图5所示)。我们认为这一点对于快速建立紧密、高度对齐的反馈回路非常关键。没有这个反馈回路,即使是特征预处理也可能变成一个无目标、无准备的“钓鱼式探险”,因为你无法知道它是否改善了结果。
当进行预测模型拟合时,首先使用像线性回归这样的简单方法是个好思路。人们通常声称这样做是为了避免过拟合。然而,关键的好处在于线性回归易于实现且运行速度快,使我们能够快速建立第一个反馈回路,并保持较短的反馈周期。这条钢线(软件工程中的最小可行产品)允许在不偏离工作代码库太远的情况下进行迭代和构建复杂性。
(四)数据科学系统的测试
在开发数据科学系统的同时,为代码库和数据集开发一套测试(例如单元、集成以及端到端测试),可以提供对代码正确性的全面的反馈回路。
代码测试:
优质的测试套件能够缓解数据科学系统(作为复杂系统)所不可避免的脆弱性。比如,一个拼写错误可能就会破坏一切,而且可能从未被察觉。然而,软件工程师常常基于示例输入-输出对进行测试,但对于非平凡的数值代码来说,要了解这些可能是无法实现的。
此时,基于属性的测试就能提供帮助。尽管数值函数的正确输出可能难以预知,但我们通常知道函数应当满足哪些数学属性。基于属性的测试利用这些知识,例如通过创建随机输入并检查是否满足相应属性。如pytest[20]和hypothesis[21]等库可以分别用于通用测试和基于属性的测试。
一个关键的问题是“为了对系统的正确性有信心,需要编写哪些测试?”我们认为,关注系统在部署时必须具备的功能是关键。这样可以递归地思考需要对哪些系统组件进行测试,以及测试的深度,从而在系统在外部部署时对其功能实现有信心。
数据科学系统不仅应该处理数据,还应该向开发者揭示出现的问题和受挑战的假设。通常情况下,机器学习代码,尤其是深度学习代码,会以许多意想不到的方式失效。例如,如果卷积神经网络的早期层的架构存在错误,并且在训练过程中它们的权重没有更新,人们可能不会注意到这点。
数据测试:
我们无法知道数据所代表的一切信息。这也是为什么数据科学系统可能会悄然出错乃至失败的另一个原因。因此,实施针对代码和数据的测试至关重要。尽可能频繁地绘制数据也同样重要。查看图表是一种高度对齐的反馈回路。然而,查看图表也是需要时间的,即具有较长的反馈周期。我们建议开发者从图表内容中提取相关信息,并基于此编写测试[22]。
在编码过程中,我们建议尽可能多地检验关于数据的假设。虽然应该反复进行此操作,但可能最关键的一点是,在数据输入模型之前就立即进行检验。检验对数据的假设是困难的,因为我们常常对其一无所知,可能会忘记做出了哪些假设。所以在注意到它们时,将其硬编码为测试,显得非常必要。人们也可以用专门的Python库如pandera[23]来实现这一步骤。如果我们期望一个变量以特定格式存在,那就应该编写一个检验程序,在数据变量格式错误时能够生成错误或警告。数据测试通过将假设转化为确定性的,来降系统的不确定性。
我们通过进行实验来提出数据问题。就如同在深入的交谈中,你必须仔细听取答案,并据此调整自己后续的回应和提问。这并不意味着你的问题生成算法需要贪婪,但它必须是持续迭代的。一方面,迭代工作释放了反馈回路的潜力,这在处理复杂或真实数据时尤为重要。另一方面,这要求你在与数据交互时具备灵活性。
六、结语
六、结语
反馈回路是特征工程、模型开发等各环节的基础设施。反馈回路让我们能以更快、更远、更自信的步伐前进。逐步发展的数据科学系统能够充分利用反馈回路的力量。
正确性和有效性是两个不同的概念,它们需要不同的反馈回路。对于正确性而言,最核心的反馈回路是编写和执行测试套件,并尽可能编写清晰易懂的代码。对于打造有效反馈回路来说,最重要的一点是尽早建立起整个数据流水线,并使其尽可能细致。
我们注意到,(几乎)没有反馈回路能完全对齐;但是,对齐却是不可或缺的。然而,我们要指出,在迭代不对齐的反馈回路时可能会出现微妙的问题。过拟合,也被称为古德哈特定律,指出一旦某个度量成为目标,它就不再是一个好的度量标准。过拟合主要是有效反馈回路的问题。
此前研究认为[26],人和流程通过不协调的反馈来优化不当的激励,可能导致他们(有意或无意地)“玩弄系统”。这种过拟合,即在验证集上的过拟合,可能影响整个数据科学系统,而非仅限于模型。尽管研究者在训练模型时通常意识到这个问题,但他们往往未能察觉到整个数据科学系统存在同样问题。机器学习和数据科学项目中,通常把数据集分为训练集、验证集和测试集,其中训练集用于训练模型,验证集用于在模型训练过程中调整参数和选取最佳模型,而测试集则在所有模型开发完成后,用于评估模型表现。针对模型过拟合的策略,也同样适用于数据科学系统,例如,在开发过程中不使用保留的测试集,而是在开发最后才用测试集评估模型。
此外,我们再次强调这是一个社会技术问题。对学生和早期职业研究人员进行这些特定问题的培训至关重要,可以参考《图灵之道》[7]。此外,尽管有Zenodo或SoftwareX等项目的努力,但学术界仍常将激励机制引向远离创造和发布高质量数据科学系统的方向。因此,我们必须改进学术界的激励机制,使之与期望的科研目标相匹配。
参考文献
(参考文献可上下滑动查看)
计算社会科学读书会第二季
【计算社会科学读书会】第二季由清华大学罗家德教授领衔,卡内基梅隆大学、密歇根大学、清华大学、匹兹堡大学的多位博士生联合发起,进行了12周的分享和讨论,一次闭门茶话会,两次圆桌讨论。本季读书聚焦讨论Graph、Embedding、NLP、Modeling、Data collection等方法及其与社会科学问题的结合,并针对性讨论预测性与解释性、人类移动、新冠疫情、科学学研究等课题。欢迎从事相关研究或对计算社会科学感兴趣的朋友参与学习。
详情请见:
推荐阅读