2.3 数据的阶段

大多数团队会依靠他们所使用的平台来提供他们需要的大部分功能,其中包括数据存储和处理平台。YarnIt不是一个大型组织,但我们仍然会让负责业务管理、数据工程和运营的员工参与进来,帮助我们理解并满足此处的要求。我们很幸运地拥有站点可靠性工程师,他们将解决与数据的存储和处理有关的可靠性问题。

从根本上来说,数据管理阶段是关于将我们所拥有的数据转化为适合该过程之后阶段的格式和存储模式的。在这个过程中,我们也可能会应用一系列特定于模型(或至少是特定于模型领域)的数据转换,以便为训练准备数据。我们对数据的下一步操作是将其用于训练机器学习模型,对某些敏感的数据内容进行匿名化处理,并在我们不再需要或被要求时删除这些数据。为了准备对数据进行上述操作,我们将继续从业务领导那里获得他们的意见输入,以回答关于数据的主要使用场景的问题,以及未来可能的探索领域。

与本书的大部分章节一样,设计一个能够运行且可靠的机器学习系统,深入了解机器学习并不是必需的,有时甚至是不值得的。然而,对模型训练的基本理解确实直接告知我们在准备数据时要做什么。现代机器学习环境中的数据管理在将数据送入模型训练管道之前包括多个阶段,如图2-2所示:

● 创建

● 提取

● 处理(包括验证、清洗和丰富)

● 后期处理(包括数据管理、存储和分析)

图2-2:机器学习数据管理阶段

2.3.1 创建

这可能是看起来要么很奇怪,要么很明显的陈述,但机器学习训练数据来自某处。也许你的数据集来自很多地方,比如另一个部门的同事或一个学术项目,并且是在那里创建的。但数据集都是在某个时间点通过某种过程创建的。这里的数据创建是指在一些数据存储系统而不是我们的数据存储系统中生成或捕获数据的过程。例如,来自服务系统的日志、从一个事件中捕获的大量图像集合、来自医疗程序的诊断数据等。在这个过程中隐含的是,我们将希望设计新系统并调整现有系统以生成更多的数据,这样我们的机器学习系统就有数据可用了。

有些数据集在静态(或至少变化不频繁)时效果很好,而其他数据集只有在频繁更新时才有用。例如,一个照片识别数据集可能可以使用很多个月,只要它能准确地代表我们的模型想要识别的照片类型。另外,如果户外的照片只代表温带气候的冬季环境,那么照片集的分布将与我们需要识别的期望图像集有很大的不同。因此,当这些地方的环境在春季变暖时,它将没有用处。同样,如果试图自动识别yarnit.ai的交易中的欺诈行为,我们会希望在最近的交易中不断训练自己的模型,同时提供关于这些交易是否具有欺诈性的信息。否则,有人可能会想出难以检测的欺诈方法偷走我们的所有编织用品,而我们可能永远无法让模型学会如何发现它。

我们收集的数据种类和创建的数据集可以是非结构化的、半结构化的,或者是结构化的(如图2-3所示)。

结构化数据使用预定义的数据模型/模式进行量化,是高度组织化的,并且以表格格式(如电子表格或关系数据库)存储。姓名、地址、地理位置、日期和支付信息都是结构化数据的常见示例。由于结构化数据具有良好的格式,可以通过启发法用相对简单的代码轻松处理。

另一方面,非结构化数据是定性的,没有标准的数据模型或模式,所以它不能用传统的数据方法和工具来处理和分析。非结构化数据的示例包括电子邮件正文、产品描述、网络文本以及视频和音频文件。半结构化数据没有特定的数据模型/模式,但包括标签和语义标记,因此是介于结构化数据和非结构化数据之间的一种结构化数据类型。半结构化数据的示例包括电子邮件和社交媒体内容,前者可以按发件人、收件人、收件箱、已发送、草稿等进行检索,而后者则可以被归类为公共、私人和朋友,也可以从用户维护的角度分类,如散列标签。数据内部结构的特点对我们处理、存储和使用数据的方式影响很大。

图2-3:机器学习训练数据的分类

尽管模型中的偏见来自模型的结构和数据,但数据创建的环境对正确性、公平性和道德也有深远的影响。尽管我们会在第5章和第6章中对此进行更详细的论述,但我们在此可以提出的主要建议是,你要有某种程序来确定你的模型是否有偏见。我们可以用许多方法来做这件事,最简单的方法可能是模型卡(https://oreil.ly/h7E8h),然而无论用何种程序来做此事并且让它在组织上被接受,都要比没有这样的程序好得多[4]。当我们开始在自己的机器学习系统中处理道德和公平性的问题时,这绝对是第一件要做的事。例如,将检测偏见的工作结合到数据起源或数据生命周期会议或跟踪过程中是相对容易的。但每个做机器学习的组织都应该建立这种程序,并将其作为持续改进的一部分来审查。

回顾一下,偏见来自许多方面,并且会在整个过程的很多阶段显现出来。没有能够完全保证数据公平性的方法。在这方面,成功的先决条件是一种有包容性的公司文化且公司内充满了来自各种背景的人,他们有不同的和创造性的观点。有充分的证据表明,具有非常不同的背景和观点的人,在信任和尊重的环境中工作时,会产生比背景全都相似的团队更好和更有用的想法。这可以成为针对经前述检查后漏掉的各种偏见的有力防御的一部分。然而,在没有任何流程和工具的帮助下阻止所有不好的结果,就像在没有系统的帮助下,人类的努力不能阻止所有的坏结果一样。

关于数据集的创建,或者说数据集的增加,还有最后一点需要注意:如果我们有少量的训练数据,但不足以训练出高质量的模型,那么可能需要增加数据。有些工具可以做到这一点。例如,Snorkel(https://www.snorkel.org/features)提供了一个程序化的接口,可以将少量的数据移植到更多的、变化更丰富的数据中,从本质上组成想象中的但统计上有效的训练数据。这是一个很好的开始,因为它允许我们轻松地将一个小的数据集扩展成一个大的。尽管看起来这些通过程序创建的数据集在某种程度上不那么有价值或不那么有用,但存在十分有力的证据表明,这种方法可以以较低的成本产生十分出色的结果,即使这种方法确实需要谨慎使用[5]

2.3.2 提取

数据需要被接收到系统中并写入存储,以便进一步处理。在这个阶段,必定会对数据进行过滤和选择。不是所有创建的数据都被提取或收集,因为可能其中某些数据我们不想要或不需要。

在这个阶段,我们可以按不同类型过滤数据(我们认为对模型没用的数据的字段或元素)。如果我们有太多的数据以至于我们没有信心能负担得起所有的数据处理,也可以在这个阶段简单地抽样,因为机器学习训练和其他数据处理的计算通常是非常昂贵的。对数据进行抽样可以有效地节省中间处理和训练的成本,但其中重要的是,要在抽样导致的质量下降与所节省的成本之间进行权衡。数据的抽样也应该与每个时间段或我们关心的数据中的每个其他切片的数量/速率成正比。这将避免在突发期错过细节。然而,抽样偶尔会丢失一些事件的细节,这是不可避免的。

通常来说,数据越多,机器学习训练系统表现越好。虽然学究们会立即想到许多例外情况,但这是一个有用的出发点。因此,在降低成本的同时,任何数据的减少都可能对其质量产生影响。

根据数据量和服务的复杂性,提取阶段可能是简单的“在不同目录中转存一些文件”,或者是复杂的远程过程调用(RPC)端点,接收特定格式的文件,并确认已收到相应的数据包,以便可以通过系统跟踪其进展。在大多数情况下,我们希望至少有一个简单的API可以用于数据提取,因为这提供了一个明显的位置来确认数据的接收与存储,记录提取情况,并使用关于数据的任何治理策略。

提取阶段的可靠性问题通常集中在数据的正确性和吞吐量上。正确性是一个通用属性,指数据被正确地读取和写入正确的位置而不会被跳过或放错位置。虽然数据放错位置的想法听起来很有趣,但它确实会发生,而且很容易看到它是如何发生的。存储中以日期或时间为导向的分桶系统,再加上摄取过程中的偏差,可能会导致每天的数据都存储在前一天的目录中。在提取前和提取过程中监控数据是否存在及其存储状况是数据管道中最困难的部分。

2.3.3 处理

一旦我们成功地将数据加载(或提取)到一个有效的特征存储系统中,大多数数据科学家或建模师会通过一系列常见的操作,使数据转换为能够进行训练的数据。这些操作(验证、清洗和确保数据一致性,以及丰富和扩展)将在接下来的内容中详述。

验证

无论我们的机器学习模型有多高效强大,它们都无法根据糟糕的数据完成预测。在生产中,数据出错的一个常见原因是最开始收集数据的代码存在错误。即使每个数据源都有一个定义好的模式,从外部提取的数据仍可能会有很多错误(例如,整型字段有一个浮点值)。因此,验证传入的数据是非常重要的,特别是当有一个确定模式或有能力与最后已知的有效数据进行比较时。

验证是根据字段的通用定义来进行的——它是否是我们期望的那样?为了进行这种验证,我们需要预先存储并能够引用这些标准的定义。使用一个全面的元数据系统来管理字段的一致性并跟踪定义,对于保持数据的准确性至关重要。这一话题将在第4章进行详细介绍。

清洗和确保数据一致性

即使有一个全面的验证框架,大多数数据仍然是混乱的。可能有缺失的字段、重复的数据以及错误的分类,甚至是编码错误。我们拥有的数据越多,数据清洗与保持数据一致性就越有可能成为处理工作的重点。

这可能看起来很令人沮丧且没有必要:建立整个系统只是为了检查数据,对许多第一次这样做的人来说,听起来有点夸张了。但现实情况是,我们的机器学习管道肯定会有用于清洗数据的代码。我们可以把这些代码与其他代码分隔开来,在那里它可以被审查并加以改进,或者把它的各个方面放在整个训练管道中。第二种策略使管道变得非常脆弱,因为随着对数据正确性的假设越来越多,我们确保满足这些假设的能力却没有提升。此外,当我们改进一些用于验证和改正数据的代码时,我们可能忽略了在其他正在进行这项工作的地方实施这些改进。或者更糟的是,我们可能会适得其反。例如,我们可能多次“改正”相同的数据,导致数据中的原始信息被消除。也可能存在潜在的竞态条件,即流程的不同部分会以不同的方式清洗数据或保证数据一致性。

在这一环节,另一组数据一致性的任务是数据的归一化。归一化一般指的是一组用于将输入数据转换为同样尺度的技术,这对于像深度学习这样依赖梯度下降或类似数值优化方法进行训练的算法是非常有用的。关于归一化的一些标准技术如图2-4所示,主要包括:

缩放到一个范围

将数据的所有X值映射到一个固定范围,通常是0到1,但有时(对于像身高或年龄这样的东西)会有其他值来表示常见的最大、最小值。

裁剪

除去数据的极端值。当数据集有少量的极端值时,这种方法很有效。

对数缩放

x'=log(x)。当数据符合幂次分布时,会有少量非常大的数值和大量非常小的数值,这种方法很有用。

Z值标准化

将变量映射为平均值的标准差的个数。

值得注意的是,如果这些技术中的任何取值范围、分布或平均值是在一组测试数据上计算的,而这些数据的属性与后来应用的数据集不同,那么这些方法会造成风险。

最后一个常见的相关技术是将数据放入储存桶中:我们将一定范围内的数据映射到一个更小的代表相同范围的组中。例如,我们可以用年来衡量年龄,但在训练时,我们可以把数据放在按10年分开的桶里,这样,所有30到39岁的人都被放入“30岁”的桶里。但分桶可能是许多难以察觉的错误的来源。例如,想象一下,一个系统以10年为界限对年龄进行分类,另一个系统以5年为界限进行分类。当我们对数据进行分桶时,必须认真考虑将现有的数据保存,并将每条数据转换为一个新的、格式正确的数据。如果(什么时候?)我们改变了分桶策略,这样做会很棒;否则,就不能转换。

丰富和扩展

在这个阶段,我们会将自己的数据与其他来源的数据相结合。扩展数据的最常见和最基本的方式是打标签。这个过程通过引入外部数据源(有时是人工参与)的信息来识别一个特定的事件或记录。带标签的数据是所有监督式机器学习的关键驱动力,而且往往是整个机器学习过程中最具挑战性和最昂贵的部分之一。如果没有足够数量的高质量标签数据,监督学习就不会成功。(标签和标签系统将在第4章详细介绍。)

图2-4:“Google Developers Data Prep”课程中介绍的归一化技术(https://oreil.ly/0cgBm

但打标签只是扩展数据的一种方式。我们可能会使用许多外部数据源来扩展我们的训练数据。比方说,出于某种原因我们相信根据人们所在地的温度能预测他们将买什么[6]。我们可以利用yarnit.ai网站上的搜索日志,添加用户在访问网页时的大致地理位置的温度。这可以通过创建或找一个记录历史温度的服务或数据集来达成。这会使我们能够将搜索所在地的温度作为一个特征来训练一个模型,看看我们能用它做出什么样的预测。这有可能是一个糟糕的想法,但并不是完全不切实际的。

2.3.4 存储

最后,我们需要将数据存储在某个地方。我们如何以及在哪里存储数据主要是由我们如何使用它来决定的,这实际上是一组关于训练和服务系统的问题。我们在第4章对这个问题有更多的探讨,而这里有两个主要的问题:存储的效率和元数据。

存储系统的效率是由访问模式决定的,而访问模式受到模型结构、团队结构和训练过程的影响。为了对我们的存储系统做出明智的决定,这里有一些我们需要回答的基本问题:

● 模型是对这些数据进行一次训练还是多次训练?

● 每个模型都将读取所有数据还是只读取部分数据?如果只读取部分数据,被读取的子集是由数据类型(哪些字段)选择的,还是由随机抽样的数据(所有记录的30%)选择的?

● 特别是,相关团队是否读取了数据中略有不同的字段子集?

● 我们是否需要以任何特定的顺序来读取数据?

关于数据的重复使用问题,事实证明,几乎所有的数据都会被多次读取,存储系统应该支持这种功能,即使模型所有者断言他们只会对数据进行一次模型训练。为什么呢?模型开发本质上是一个迭代的过程。一个机器学习工程师构建模型(读取必要的数据),测量模型在其设计的任务中表现如何,然后部署它。接着他们会有另一个想法:一个关于如何改进模型的想法。很快,他们又重新读取同样的数据来尝试他们的新想法。我们建立的每一个系统,从数据到训练一直到服务,都应该假定模型开发者为了改进模型都会半持续地重新训练相同的模型。事实上,当他们这样做的时候,可能每次都会读取不同的数据子集。

鉴于此,每个特征一列的面向列的存储方案(每个特征占一列)是一种常见的设计架构,特别适用于对结构化数据进行训练的模型[7]。大多数读者都熟悉面向行的存储模式,在这种存储中,每次从数据库中获取数据都会检索并匹配行中的所有字段。对于那些使用大部分或全部数据的应用程序集合,或者一些类似的程序集合来说,这是一个合适的架构。面向列的数据有利于只检索字段的一个子集。这对每个使用特定数据子集的应用程序(本例中的机器学习训练管道)的集合更有用。换句话说,面向列的数据存储允许不同模型有效地读取不同的特征子集,并且不用每次都读入整行数据。我们在一个地方收集的数据越多,就越可能拥有使用不同数据子集的模型。

然而,这种方法对于某些训练系统来说太复杂了。举个例子,如果在大量未经预处理的图像上进行训练,我们其实并不需要有一个列式存储系统,这意味着我们可以直接从一个目录或一个桶中读取图像文件。然而,假设我们正在读取一些更结构化的东西,比如交易数据日志,其中有时间戳、来源、推荐人、金额、项目、运输方式和支付机制等字段。那么,假设一些模型会使用其中的一些特征,而另一些会使用其他的特征,这将促使我们使用列式存储结构。

元数据帮助人类与存储的数据进行互动。当多人在同一数据上建立模型时(或者当同一个人长期从事这项工作时),存储特征的元数据会为他们提供巨大的帮助。它是理解之前的模型是如何构造出来,以及我们应当如何构造模型的路线图。存储系统的元数据是机器学习系统中常常被低估的部分之一。

本节会说明我们的数据管理系统主要是由两个因素促成的:

我们打算将数据用于何种商业目的

我们要解决的是什么问题?这个问题对我们整个组织或客户的价值是什么?

模型结构和策略

我们计划构建什么模型?它们是如何组合的?它们多久更新一次?它们有多少个?它们之间的相似度如何?

我们对数据管理系统的每一个决定都受到这两个因素的制约,同时也制约着这两个因素。如果数据管理是关于我们如何以及为什么写入数据,那么机器学习训练管道是关于我们如何以及为什么读取数据。

2.3.5 管理

通常,数据存储系统实施基于凭证的访问控制,用以限制未经授权的用户访问数据。这种简单的技术只能为基本的机器学习实现服务。在更高级的场景中,特别是当数据包含机密信息时,我们需要有更细粒度的数据访问管理方法。例如,我们可能希望模型开发人员只能访问与他们工作直接相关的特征,或以其他方式限制他们对数据子集的访问(也许只有最近的数据)。或者,我们可以在静止状态下或在访问时对数据进行匿名处理或假名处理。最后,我们有时会允许生产工程师访问所有的数据,但只有在证明他们在某次事件中需要这样做,且由一个单独的团队仔细记录和监控他们的访问时才可以。(关于这方面的一些有趣话题将在第11章讨论。)

站点可靠性工程师可以在生产中的存储系统上配置数据访问限制,允许数据科学家通过虚拟专用网络(VPN)等授权网络安全地读取数据,并实施审计记录以跟踪哪些用户和训练任务在访问哪些数据,生成报告,并监控使用模式。企业主或产品经理可以根据用例来定义用户权限。我们可能需要为这些分布并不均匀的维度生成和使用不同种类的元数据,以最大限度地提高后续阶段访问和修改数据的能力。

2.3.6 分析与可视化

数据分析与可视化是使用统计或图形技术以及工具将大量数据转化为易于导航的展示形式的过程[8]。使数据不那么混乱且更容易获取是机器学习架构和知识发现技术的一项基本任务。仅显示一个饼图或柱状图是不够的。我们需要为读者提供解释,说明数据集中的每条记录是什么意思,它与其他数据集中的记录是如何联系的,以及它是否干净,是否可以安全地用于训练模型。如果没有定义明确且性能高的数据可视化工具和流程,数据科学家实际上是不可能直接查看大型数据集的。