TowardsDataScience-2023-博客中文翻译-三十-
TowardsDataScience 2023 博客中文翻译(三十)
Lucene 透视 — 处理整数编码和压缩
原文:
towardsdatascience.com/lucene-inside-out-dealing-with-integer-encoding-and-compression-fe28f9dd265d
深入探讨 PackedInts、VInt、FixedBitSet 和 RoaringDocIdSet(Roaring Bitmaps)
·发布于 Towards Data Science ·13 分钟阅读·2023 年 6 月 28 日
--
图片由 Gerd Altmann 提供,来自 Pixabay
早些时候,我们学习了使用 产品量化 进行相似性搜索的向量压缩。
如何在内存中压缩和适配一个巨大的向量集,以便在不对称距离计算下进行相似性搜索……
towardsdatascience.com
在这篇文章中,我们将探讨并深入了解整数在 Lucene 中的编码和压缩方式,那里倒排索引是核心。
Lucene — 简介
Lucene 是一个用 Java 编写的开源搜索引擎库。由 Doug Cutting 于 1999 年创建,以全文搜索和索引著称。
这个开源软件项目在 Apache 软件基金会 旗下,经过二十多年仍在积极开发中。多年来,它不断发展壮大,成为一个强大、功能齐全的高性能搜索引擎库。
毫无疑问,Lucene 的成功在很大程度上归功于其强大的社区以及贡献者们的卓越工作。他们的参与和合作使得 Lucene 达到了今天的水平。许多流行的企业搜索平台和解决方案,如 Solr 和 Elasticsearch,都是建立在 Lucene 之上的。
“对于一个开源项目来说,20 年是很长的时间。毫无疑问,Lucene 的长期存在证明了其社区的力量和多样性” — 庆祝 Apache Lucene 20 年
反向索引
反向索引是 Lucene 的核心。反向索引包括两部分 —— 左边是术语字典,右边是每个术语的 postings。
图 1: 术语字典和 postings 列表形成 Lucene 中的反向索引。所有图片均由作者提供,除非另有说明。
Postings 是关于术语在文档中出现的信息。Postings 列表包含术语出现的文档的 Doc ID。
如果定义了,它也可能包括诸如术语在文档中的频率,甚至位置、字符偏移量和有效负载等信息。
是的,这些都是整数,确实有大量的整数需要在 Lucene 中处理。正如 Apache 软件基金会的 Lucene 贡献者 Adrien Grand [1] 所引述的:
“搜索引擎最重要的构建块之一是能够高效地压缩和快速解码排序的整数列表”
在接下来的部分中,我们将深入探讨 Lucene 用于编码和压缩整数的技术,特别是来自 postings 列表的整数 —— Doc ID 和术语频率。
Delta 编码
让我们首先看看 Lucene 如何在磁盘上编码和存储 postings 数据。包含每个术语的文档列表保存在 .doc 文件中。跳过数据也保存在同一个文件中,但在本文中不作讨论。
首先,正如 图 1 所示,每个术语所指向的 Doc ID 基本上是一个排序好的整数列表。对于每个术语,我们首先将排序好的 Doc ID 列表转换为 Doc Deltas。
通过 delta 编码,Doc Deltas 是通过计算每个 Doc ID 和前一个 Doc ID 之间的差异得到的,除了第一个 Doc ID。
接下来,将 Doc Deltas 切分为固定的 128 个整数块。这些块被称为 PackedDocDeltaBlock。每个块随后使用 PackedInts 进行编码,这是一种 Lucene 实现的位打包方式。剩余的 Doc Deltas 则用 VInt 编码。
以下图是一个简化的示意图,其中 PackedDocDeltaBlock 的块大小为 4,而不是实际的 128。
图 2: Doc ID 的编码过程
你是否注意到,大多数整数在经过 delta 编码后变得更小了?
较小的整数需要更少的位来表示,这对于使用 PackedInts 和 VInt 进行编码的下一步至关重要。
PackedInts
**Integer data types:**
Byte = 8 bits
Short integer (*short*) = 16 bits (2 bytes)
Integer (*int*) = 32 bits (4 bytes)
Long integer (*long*) = 64 bits (8 bytes)
通常,位打包将多个值在位级别组合成一个或多个字节(或一个或多个长整型*)。例如,四个 2 位的值可以打包成一个字节,八个 16 位的值可以打包成两个长整型。
通过位打包,存储整数所需的空间可以显著减少。
这是可能的,因为典型的 32 位 *int*
(最常用的整数类型)几乎总是包含位级别的前导零,位打包会丢弃这些零。
图 3: 将 4 个整数打包到 1 个字节中的示例,每个值占用 2 位。存储空间从 16 字节减少到 1 字节。
在 PackedInts 中,数据以每个值消耗固定数量位数的方式存储,这个位数在 1 到 64 之间,被称为bitsPerValue
。
在 Doc IDs 的编码过程之后,如果一个数据字段定义为包含术语频率(即该字段的索引选项设置为 IndexOptions.DOCS_AND_FREQS
),那么每个 PackedDocDeltaBlock 都会紧随其后一个 PackedFreqBlock。
图 4: PackedDocDeltaBlock 和 PackedFreqBlock
正如其名,PackedFreqBlock 包含在前一个 PackedDocDeltaBlock 中出现的术语的对应频率。与文档 ID 不同,术语频率没有经过 delta 编码。
每个 PackedDocDeltaBlock 和 PackedFreqBlock 都使用 PackedInts 独立编码。bitsPerValue
是根据块中表示最大整数所需的位数得出的。尽管如此,每个值所消耗的位数可能会比预期的更多。这是为什么,如何发生的?
实际上,Lucene 可能会调整 bitsPerValue
以基于一个称为 acceptableOverheadRatio
的参数来获得最佳的读写性能。这个参数是为了在内存效率与快速随机读取之间进行权衡所愿意接受的开销。
图 5: PackedInts 中的压缩模式
在 Lucene 中,bitsPerValue
的 调整 是以这样一种方式进行的:当 bitsPerValue
增加到 8、16、32 或 64 时,结果开销不会超过 acceptableOverheadRatio
。但为什么是 8、16、32、64,而不是其他数字?
大多数情况下,当表示一个值的位数是字节对齐或是 8 的倍数(即 8、16、32、64)时,读写性能最佳。因为没有值在一个字节内共同存在,读写操作得以简化。
换句话说,1 字节、2 字节、4 字节或 8 字节的空间完全用于表示一个值。
在内存效率最差的情况下,bitsPerValue
从 1 调整到 8。对于每一个存在的有效位,消耗 7 个其他未使用的位。这导致了 700% 的内存开销。实际上,即使只有 1 位在使用,每个值也会消耗 8 位。
acceptableOverheadRatio
为 7 时,随机读取访问速度往往最快。这是当bitsPerValue
从 1 到 7 调整为 8,bitsPerValue
从 9 到 15 调整为 16,以此类推时的结果。实现的内存开销有不同程度,最高达到 700%。
图 6:当每个值的位数调整为 8 时的内存开销
另一方面,当acceptableOverheadRatio
为 0 时,bitsPerValue
保持不变,不进行调整。数据被紧密打包以实现最佳内存效率,但随机读取可能较慢。可能会有多个值占据一个字节的空间。因此,表示一个值的位可能会溢出到下一个字节。
图 7:具有每值 6 位的紧凑数据示例。字节数从 4 减少到 3。
综上所述,Lucene 默认使用的PackedInts压缩模式的acceptableOverheadRatio
为 0.25。此设置确保任何产生的内存开销永远不会超过 25%。
VInt
VInt 是一种基础 128 压缩类型,生成可变长度整数。每个整数单独编码为 1 到 5 字节。
图 8:将 17000 转换为VInt的示例
生成VInt时,位被分成 7 位一组,从右侧的低位开始。
对于每个从右到左的 7 位块,另一个位被添加以形成一个字节。这个位作为续接标志,构成字节的最高有效位。如果后面还有更多字节,这个位的值为1
,否则为0
。
使用这种表示方式,1 字节足以表示从 0 到 127 的小整数。大多数整数需要 3 字节或更少,因为 3 字节的VInt能够表示 16,384 到 2,097,151 之间的值。
之前,参见图 2,我们提到剩余的文档增量是用VInt编码的。当术语频率被索引时会发生什么呢?
在这种情况下,文档增量现在定义了文档编号和频率。表示文档增量的位将向左移动一步,这样最不重要的位现在用于标记频率是否为 1。如果频率为 1,则最不重要的位为1
,否则为0
。
如在Lucene90PostingsFormat中所记录,当文档增量为奇数时,频率为 1。当文档增量为偶数时,频率被读取为另一个VInt。
下图显示了如何在VInt中对文档增量
7, 10
(其中词语分别出现一次和三次)进行编码,序列为15, 20, 3
,当词频被索引时。
图 9: VInt 编码与不编码词频的对比
FixedBitSet
在 Lucene 中,FixedBitSet是一个固定长度的位图实现,用于在内存中存储文档 ID。
位图是一组映射到整数列表的位。一个被设置为1
的位表示一个整数,其值是该位的索引。
FixedBitSet在 Lucene 中内部实现为*long[]*
整数数组,因此每个整数占 64 个位。该数组的长度(或数组中的整数数量)基于位图所需的位数来确定。
例如,要编码一个最大值为 190 的文档 ID 列表,需要至少 191 个位来表示从 0 到 190 的位图索引。因此,将分配一个长度为 3 的数组,该数组能够容纳3*64=192
位。
图 10: FixedBitSet 示例 — bitset[]
数组包含 3 个 64 位的长整数
在上述示例中,FixedBitSet使用 24 个字节来编码一个最大值为 190 的 6 个整数的列表。这是稀疏数据的一个例子,其中在位集中的 192 个位中仅有 6 个位被设置为1
。
在这里,如果这些整数使用*int*
类型存储,则内存没有节省,所用的字节数相同。
这表明,当其表示的数据是稀疏时,FixedBitSet或一般位图的效率较低。
RoaringDocIdSet(Roaring 位图)
图片由Glen Carrie拍摄,来源于Unsplash
在 Lucene 中,查询通过 LRU 缓存,LRU 是一种缓存方案,当缓存满时会驱逐最少使用的项以为新的项腾出空间。缓存允许快速访问经常查询的数据。
并非所有查询在 Lucene 中都被缓存。但是,对于那些被缓存的内容,缓存的内容包含文档 ID 结果集。
Lucene 中的LRU 查询缓存对密度小于 1% 的集合使用RoaringDocIdSet。否则,使用FixedBitSet。
RoaringDocIdSet 是受Roaring Bitmaps的思想和设计结构启发的实现。那么Roaring Bitmaps是什么呢?正如 roaringbitmap.org所描述的那样,
Roaring 位图是压缩位图,其性能通常优于传统的压缩位图,如 WAH、EWAH 或 Concise。在某些情况下,它们的速度可以快几百倍,而且通常提供显著更好的压缩效果。
Roaring Bitmaps通过将数据分区并存储到不同的容器中来工作。Roaring Bitmaps中的稀疏和密集数据容器根据容器的基数以不同的方式存储。在 Lucene 的文献中,这些容器被称为块。
在RoaringDocIdSet中,块号由 16 个最重要的位标识。剩余的 16 个最低有效位是将存储在块中的值。
图 11:文档 ID 的二进制格式示例
从上述示例可以看出,前四个文档 ID 会被存储在Block 0中。接下来的两个文档 ID 将存储在Block 1中,而最后三个文档 ID 则存储在Block 4中。
图 12:RoaringDocIdSet中数据的块分区
这样,RoaringDocIdSet 可以容纳最多2¹⁶ = 65536
个块,每个块可以存储最多 65536 条记录。
那么这些块中的数据究竟是如何存储的呢?
每条记录 16 位(或 2 字节),一个*short[]*
整型数组占用 128 kB 存储 65536 条记录。数组所需的空间随着记录数量线性增长。
相反,一个可以容纳 65536 位的位图仅占用 8 kB。与 128 kB 相比,这是一种巨大的差异,空间减少了 16 倍。因此,人们倾向于认为使用位图更为高效。
图 13:使用short[]
整型数组与位图
但稍等,我们来做些分析,仔细查看图表。可以观察到,当记录总数低于 4096 时,使用*short[]*
整型数组实际上占用的空间不到 8 kB。
这就是决定每个块存储方法的原因。
使用混合数据结构,包含少于 4096 条记录的稀疏块使用
*short[]*
整型数组存储,而包含 4096 条或更多记录的密集块则使用位图存储。Lucene 进一步改进了这一点,通过使用
*short[]*
整型数组存储超密集块的集合的反向数据。
这意味着当记录数超过 61440 时,存储的是具有不到 4096 个值的集合的逆。这是多么聪明的做法!
图 14:对于超密集块,Lucene 使用*short[]*
整数数组存储集合的逆。
有趣的是,RoaringDocIdSet在与FixedBitSet进行基准测试时的表现。根据这个补丁,从图表可以观察到,当 Doc ID 集的密度低于 1%时,
RoaringDocIdSet的内存占用可比FixedBitSet小超过 128 倍。
RoaringDocIdSet的构建时间可快约 64 倍于FixedBitSet。
RoaringDocIdSet的迭代性能和跳过性能(使用
*nextDoc()*
和*advance()*
)可快约 90 倍于FixedBitSet。相反,当 Doc ID 集的密度高于 1%时,FixedBitSet的表现优于RoaringDocIdSet。
关键要点
-
在压缩方面,没有一种通用的方法。为了实现最佳结果,Lucene 使用了多种技术和策略来处理整数压缩。
-
Delta 编码在有效减少整数大小后,再进行PackedInts或VInt的压缩中起着重要作用。
-
如果存在大值会怎样?数据压缩质量会受到影响,因为块中的最大整数决定了用于PackedInts的每个值的位数。将 Doc Deltas 和术语频率拆分成固定大小的块是缓解此问题的明智方法。其影响仅限于块内的数据,而其他数据保持不变。
-
尽管位图非常适合于密集整数集合,但看到RoaringDocIdSet(Roaring Bitmaps的一种变体)以巧妙的方式处理密集和稀疏集合,确实令人着迷。
结论
Lucene 的大部分工作涉及整数。因此,整数压缩在减少存储和内存占用,以及缩短从磁盘或内存读取或写入数据的传输时间方面至关重要。
如 Lucene 所示,采用正确的策略来匹配用例,并通过创新方式优化高效访问,是促成搜索引擎领域持续增长和发展的成功因素之一。
这些实现可以在存储、内存和网络带宽方面带来显著的成本节约,同时提升性能。
参考
[1] A. Grand, 参考框架与 Roaring 位图(2015)
[3] 咆哮位图:更好的压缩位集
[4] S. Chambi, D. Lemire, O. Kaser 和 R. Godin, 使用咆哮位图提高位图性能 (2016)
[5] D. Lemire, G. Ssi-Yan-Kai 和 O. Kaser, 使用咆哮位图实现一致更快且更小的压缩位图 (2018)
[6] D. Lemire, O. Kaser, N. Kurz, L. Deri, C. O’Hara, F. Saint-Jacques 和 G. Ssi-Yan-Kai, 咆哮位图:优化软件库的实现 (2022)
[7] V. Oberoi, 咆哮位图简介:它们是什么以及如何工作 (2022)
[8] D. Lemire 和 L. Boytsov, 通过向量化每秒解码数十亿个整数 (2021)
在你离开之前…
🙏 感谢你阅读这篇帖子,希望你喜欢了解 Lucene 中的整数编码和压缩。
👉 如果你喜欢我的帖子,不要忘记点击 关注 和 订阅,以便在我发布新内容时通过电子邮件收到通知。
😃 可选地,你也可以 注册 成为 Medium 会员,以获得对 Medium 上每个故事的完全访问权限。
📑 访问这个 GitHub 仓库,获取我在帖子中分享的所有代码和笔记本。
© 2023 保留所有权利。
Ludwig — 一个“更友好”的深度学习框架
原文:
towardsdatascience.com/ludwig-a-friendlier-deep-learning-framework-946ee3d3b24
使用这个低代码、声明式框架让深度学习变得简单
·发表于 Towards Data Science ·阅读时间 11 分钟·2023 年 6 月 26 日
--
图片来源:作者:使用 Midjourney 生成
背景 — 深度学习,是否过于复杂?
我一直倾向于避免将深度学习应用于行业用例。这并不是因为缺乏兴趣,而是因为我觉得流行的深度学习框架很繁琐。我欣赏 Pytorch 和 TensorFlow 是用于研究目的的绝佳工具,但它们的 API 并不是最用户友好的。在需要为客户快速交付概念验证的情况下,我最不希望做的就是捣鼓 Pytorch 张量。
在伦敦参加 AI 峰会时,我偶然发现一个团队声称他们有解决我的深度学习问题的方案。他们采用了一种不同的方法,他们将其描述为“介于 TensorFlow 和 AutoML 之间的中点”,这是一个名为 Ludwig 的框架。
什么是 Ludwig?
Ludwig 是由 Uber 开发的,Ludwig 是一个用于构建深度学习模型的开源框架。它是声明式的,这意味着你无需像在 TensorFlow 中那样一层层构建复杂的模型,而是只需通过配置文件声明模型的结构。这听起来好得令人难以置信,所以我决定亲自体验一下。在这篇文章的其余部分,我将通过一个我从 Kaggle 上获得的示例项目详细描述我对 Ludwig 的体验。在此过程中,我将讨论它的一些优点、痛点,并给出是否值得使用的结论。
注意 — 尽管最初由 Uber 开发,Ludwig 是一个 开源 库,采用 Apache 2.0 许可证。该项目由 Linux Foundation AI & Data主办。我与 Uber 或 Ludwig 的开发者没有商业关系。
项目 — 需求预测
项目简介:预测零售商 WOMart 各个门店的最后 30 天订单。
你的客户 WOMart 是领先的营养和补充品零售连锁店,提供全面的产品以满足你的健康和健身需求。
WOMart 遵循多渠道分销战略,在 100 多个城市拥有 350 多个零售店。
数据
数据集共有 22,265 个观察值,每个观察值对应于一个特定门店的一天销售数据。为了简洁起见,我不会详细介绍数据集的所有细节,但你可以在这里查看一些描述性统计数据。
注意:数据在 Open Data Commons 许可证下可以用于任何目的。
数据字典:
方法论概述
我不会在这里详细介绍方法论,因为这不是本文的主要目的。我将高层次地介绍我如何框定问题,以便为你提供一些背景。
我将预测问题框定为一个“伪”序列到序列深度学习问题。这种方法涉及利用 360 天的时间序列数据点来预测接下来的 30 天的客户订单。我引入了一些分类变量,并且需要为每一天的订单生成单独的预测,这导致了一个略显非传统的设置——因此使用了“伪”序列到序列来描述这个问题。我将在本文后面详细讨论特征工程的具体细节。
除此之外,我遵循的方法论对于模型开发来说是标准的。我将数据分为训练数据集和保留数据集,并对特征和标签进行了重新缩放。模型训练在训练数据上进行,测试在保留数据上进行。
注意:Ludwig 确实提供了在 API 中本地拆分数据的功能。然而,为了保持严谨性,我建立了一个单独的保留数据集。训练数据集随后被进一步划分为训练、验证和测试子集。保留数据集完全被排除,仅用于分析模型生成的预测。
特征工程
在撰写本文时,我认为在 Ludwig 中进行时间序列预测的序列到序列建模是很棘手的。这是因为特征工程。Ludwig API 在处理序列作为输入方面表现出色,但它们尚未(还未)开发出对时间序列作为输出的连贯方法。你可以通过声明多个输出来开发一个“伪”序列到序列模型,但整体特征工程体验感觉相当“黑客”。
序列特征:除了那些随时间变化的特征外,我将所有预测特征工程为“Ludwig 格式”的序列。每个输入序列是每个“时间序列”特征在预定义时间范围内的水平堆叠。每个特征序列在商店级别确定,并封装在数据框的一个单元格中(看起来就像听起来那么乱)。
序列标签:对于序列标签,你必须将序列中的每一点声明为模型的单独标签。结果是我为每个商店声明了 30 个标签,每天一个标签,用于预测订单。
下面是特征工程过程的示例:
数据示例:粗体值将用于构造序列标签,常规值将用于构造序列特征。
特征工程示例:Order_sequence 是一个“Ludwig 格式”的序列。标签会被单独返回,以便后续声明为模型输出(标签)。
设计你的模型
Ludwig API 允许你通过声明方式构建相当复杂和可定制的模型。Ludwig 通过 .yaml 文件来实现这一点。现在,我知道许多数据科学家可能没有使用过.yaml 文件,但在软件开发中,这些文件通常用于配置。文件乍一看可能显得吓人,但实际上非常友好。让我们逐步了解一下我创建模型时使用的文件的主要部分。
作者提供的图像:模型架构
在深入配置之前,值得简要介绍一下 Ludwig 深度学习框架的核心架构:架构:编码器、组合器和解码器。你在 Ludwig 中配置的大多数模型将主要遵循这一架构。理解这一点可以简化堆叠组件的过程,从而快速构建你的深度学习模型。
声明你的模型
在文件的最上方,你声明所使用的模型类型。Ludwig 提供了两种选择:基于树的模型和深度神经网络,我选择了后者。
model_type: ecd
声明数据拆分
你可以通过声明拆分百分比、拆分类型以及你要拆分的列或变量来本地拆分数据集。出于我的目的,我希望确保一个商店只能出现在一个数据集中,哈希拆分正好适合这一点。
最佳实践是,我建议在 Ludwig API 之外构建一个保留集,尤其是在进行初步特征工程(如独热编码或归一化)时。这有助于防止数据泄漏。
model_type: ecd
split:
type: hash
column: Store_id
probabilities:
- 0.7
- 0.15
- 0.15
#...omitted sections...
声明模型输入
你通过名称、类型和编码器来声明输入。根据模型输入的类型,你有多种编码器选项。编码器本质上是一种将输入转换为模型可以解读的方式。编码器的选择实际上取决于数据和建模任务。
model_type: ecd
split:
type: hash
column: Store_id
probabilities:
- 0.7
- 0.15
- 0.15
input_features:
- name: Sales
type: sequence
encoder: stacked_cnn
reduce_output: null
- name: Order
type: sequence
encoder: stacked_cnn
reduce_output: null
- name: Discount
type: sequence
encoder: stacked_cnn
reduce_output: null
- name: DayOfWeek
type: sequence
encoder: stacked_cnn
reduce_output: null
- name: MonthOfYear
type: sequence
encoder: stacked_cnn
reduce_output: null
- name: Holiday
type: sequence
encoder: stacked_cnn
reduce_output: null
- name: Store_Type
type: category
encoder: dense
- name: Location_Type
type: category
encoder: dense
- name: Region_Code
type: category
encoder: dense
#...omitted sections...
声明组合器
组合器,顾名思义,用于合并你的编码器的输出。Ludwig API 提供了多种不同的组合器,每种都有其特定的使用场景。组合器的选择可能取决于模型的结构和特征之间的关系。例如,如果你想简单地将编码器的输出进行连接,可以使用“concat”组合器;如果你的特征有顺序关系,可以使用“sequence”组合器。
model_type: ecd
split:
type: hash
column: Store_id
probabilities:
- 0.7
- 0.15
- 0.15
input_features:
- name: Sales
type: sequence
encoder: stacked_cnn
reduce_output: null
- name: Order
type: sequence
encoder: stacked_cnn
reduce_output: null
# ... omitted sections ...
- name: Location_Type
type: category
encoder: dense
- name: Region_Code
type: category
encoder: dense
combiner:
type: sequence
main_sequence_feature: Order
reduce_output: null
encoder:
# ... omitted sections ...
与深度学习的许多方面一样,最佳的组合器选择通常取决于你的数据集和问题的具体情况,并可能需要一些实验。
声明模型输出
完成你的网络就像声明输出一样简单,输出就是你的标签。我对 Ludwig 的时间序列处理的一个小抱怨是,当前你无法(还)声明时间序列输出。正如我之前提到的,你必须通过单独声明时间序列中的每个点来“破解”它。这让我有了三十个单独的声明,说实话看起来非常杂乱。对于每个输出,你还可以指定损失函数,增加额外的可配置性。Ludwig 为不同的输出类型预设了大量选项,但我不确定你是否能够像在 Pytorch 中那样实现自定义损失函数。
model_type: ecd
split:
type: hash
column: Store_id
probabilities:
- 0.7
- 0.15
- 0.15
input_features:
- name: Sales
type: sequence
encoder: stacked_cnn
reduce_output: null
- name: Order
type: sequence
encoder: stacked_cnn
reduce_output: null
# ...omitted sections...
- name: Location_Type
type: category
encoder: dense
- name: Region_Code
type: category
encoder: dense
combiner:
type: sequence
main_sequence_feature: Order
reduce_output: null
encoder:
type: parallel_cnn
output_features:
- name: Order_sequence_label_2019-05-02
type: number
loss:
type: mean_absolute_error
- name: Order_sequence_label_2019-05-03
type: number
loss:
type: mean_absolute_error
#...omitted sections...
type: mean_absolute_error
- name: Order_sequence_label_2019-05-30
type: number
loss:
type: mean_absolute_error
- name: Order_sequence_label_2019-05-31
type: number
loss:
type: mean_absolute_error
#...omitted sections...
声明训练器
Ludwig 的训练器配置虽然是可选的(因为 Ludwig 提供了合理的默认设置),但允许高度的自定义。这让你能够控制模型训练的具体细节。这包括指定所用优化器的类型、训练轮数、学习率以及早停标准等参数。
model_type: ecd
split:
type: hash
column: Store_id
probabilities:
- 0.7
- 0.15
- 0.15
input_features:
- name: Sales
type: sequence
encoder: stacked_cnn
reduce_output: null
- name: Order
type: sequence
encoder: stacked_cnn
reduce_output: null
# ...omitted sections...
- name: Location_Type
type: category
encoder: dense
- name: Region_Code
type: category
encoder: dense
combiner:
type: sequence
main_sequence_feature: Order
reduce_output: null
encoder:
type: parallel_cnn
output_features:
- name: Order_sequence_label_2019-05-02
type: number
loss:
type: mean_absolute_error
- name: Order_sequence_label_2019-05-03
type: number
loss:
type: mean_absolute_error
#...omitted sections...
type: mean_absolute_error
- name: Order_sequence_label_2019-05-30
type: number
loss:
type: mean_absolute_error
- name: Order_sequence_label_2019-05-31
type: number
loss:
type: mean_absolute_error
trainer:
epochs: 200
learning_rate: 0.0001
early_stop: 20
evaluate_training_set: true
validation_metric: mean_absolute_error
validation_field: Order_sequence_label_2019-05-31
对于你的特定用例,你可能会发现自己定义这些参数会更有益。例如,你可能希望根据模型的复杂性和数据集的大小调整学习率或训练轮数。同样,早停可以成为防止过拟合的有用工具,通过在模型在验证集上的表现不再改善时停止训练过程。
训练你的模型
训练你的模型可以通过 Ludwig 的 Python 实验 API 轻松完成。请参见下面的脚本示例:
其他配置
除了我提到的,Ludwig 还有大量可能的配置。它们都记录得很好且结构清晰。我建议阅读他们的 文档 来熟悉它们。
模型性能分析——简要视图
本文旨在通过一个实际的示例项目来探讨 Ludwig 框架的一些功能。虽然展示模型性能是其中的一部分,但无需深入探讨指标的细节。我将讨论限制在展示一些从模型分析中生成的图表。请注意,全面的端到端脚本在我的 GitHub 上可以找到,链接在文章结尾。
作者提供的图片:过去 30 天的数据中,模型预测(红色)与实际订单(蓝色)的对比。这些例子来自保留集。
作者提供的图片:误差分布,其中误差为实际订单减去预测订单。
作者提供的图片:模型训练的损失曲线。损失指标为均方绝对误差
我的判决
我将首先承认,最初我对 Ludwig 持怀疑态度。然而,在自己实验之后,我相信它的能力,并认为它如承诺般有效。我认为有几个真正令人印象深刻的功能值得突出。
编码体验:编码体验更像是在构建一个精致的乐高模型。你可以通过玩弄组件和不同的架构来找到你的完美模型,确实非常有趣。
文档:文档清晰且结构良好。很容易搞清楚如何实现不同的架构和更改模型配置。大部分文档似乎也很及时更新,这是一大优势。
后端:后端体验非常出色。库的开发者在抽象掉训练深度神经网络所需的大部分常规配置方面做得很好。我在 Google Collab 上训练了我的模型,Ludwig 自动将工作负载转移到 GPU 上。
Ludwig 还有一个很棒的特点,就是后端高度可配置。例如,如果你在运行大规模工作负载并需要一个 GPU 集群,你也可以进行配置!
实验追踪:Ludwig 提供了一个实验 API,可用于在实验运行之间跟踪模型工件。我相信它也与 MLflow 集成,这对于商业规模的 MLOps 来说非常棒。
个人偏好
有一些领域可以进一步增强这个框架,让我们一起来探讨一下。
可视化:Ludwig 确实提供了一个可视化 API 来跟踪数据集中的训练损失。然而,在撰写本文时,它的功能并不特别好,其使用也不够直观。我尝试在 Google Collab 中运行,但没有成功。最终,我通过编写一个 Python 函数来解析 Ludwig 在每次实验运行后保存的 training_statistics.json 文件,创建了自己的损失曲线可视化。
支持:虽然有一定的支持可用,但 Ludwig 的社区似乎还没有 TensorFlow 或 Pytorch 那么广泛。在 GitHub 上提出了一些问题,有些线程可能提供帮助,但大部分情况下,感觉你只能依靠自己。至于 ChatGPT,它提供了截至 2021 年的一定程度的支持。
透明度:Ludwig 在消除构建深度学习模型中更具挑战性的方面表现出色。然而,这也以牺牲透明度为代价,偶尔使日志显得有些难以理解和调试。
结论
在我看来,Ludwig 是一个出色的工具,适合那些希望开始使用深度学习的人,无论是在商业环境中还是仅仅为了学习。虽然它可能对前沿研究目的来说过于高层次,但它非常适合快速解决明确定义的问题。尽管仍然需要对深度学习有一定的理解,但一旦掌握了概念,Ludwig 的入门门槛比 TensorFlow 或 Pytorch 低得多。
端到端的笔记本可以在我的 GitHub 仓库中找到,请随意进行实验。
关注我在 LinkedIn
订阅 Medium 以获取更多我的见解:
[## 通过我的推荐链接加入 Medium — John Adeojo
我分享数据科学项目、经验和专业知识,以助你一臂之力。你可以通过 Medium 注册…
johnadeojo.medium.com](https://johnadeojo.medium.com/membership?source=post_page-----946ee3d3b24--------------------------------)
如果你有兴趣将 AI 或数据科学集成到业务运营中,我们邀请你预约一次免费的初步咨询:
通过免费的咨询发现我们在帮助企业实现雄心勃勃目标方面的专业知识。我们的数据科学家和…
www.data-centric-solutions.com](https://www.data-centric-solutions.com/book-online?source=post_page-----946ee3d3b24--------------------------------)
机器学习算法第一部分:线性回归
使用线性回归预测钻石价格
·
关注 发表在 Towards Data Science · 12 分钟阅读 · 2023 年 1 月 6 日
--
图片由 Bas van den Eijkhof 提供,发布在 Unsplash
线性回归是一种强大但相对简单的工具,可以用来理解变量之间的关系。本教程将以初学者友好的方式探索线性回归的基础知识。在本教程结束时,你将对线性回归有一个扎实的理解,并知道如何使用实际数据实现它。
什么是线性回归?
线性回归,作为一种统计方法,首次用于 1877 年,用于预测因变量的值。实质上,它通过对提供给模型的多个点进行“拟合”来最准确地匹配因变量和自变量之间的关系,这类似于散点图。通过图表最容易观察到这一点:
来源:维基百科。
线性回归通过创建一条线性直线(形式为y=mx+b
)来最准确地预测因变量的值,通过求解值m
(斜率)和b
(y 截距)。
最小二乘法
为此,模型使用了一种称为最小二乘法的方法,以最准确地找到最佳拟合线。该方法的目标是尽可能减少特定数据点到最拟合线的偏差平方总和。最拟合的直线将具有最小的最小二乘函数结果值。我们可以使用以下方程计算从每个提供的点到最拟合线的偏差:
图像由作者提供。
实质上,线性函数的输出因变量(y 值)从给定数据点的因变量中减去。这个值可以是正数也可以是负数,取决于函数的值是否大于或小于数据点的值。然而,偏差是正还是负并不重要——无论如何,数值都会被平方。
更简单地说,我们可以用一个总和来找到所有偏差平方的总值:
图像由作者提供。
最小二乘法声称,最准确/拟合的数据线将具有最小的总和(S
)。
示例
使用点:(1,2), (3,5), (5,2)
。
图像由作者提供。
现在是提到为什么最小二乘法中的值被平方的好时机。首先,如前所述,它确保所有偏差都是正值。然而,更重要的是,它确保较大的偏差被赋予更多的权重。这使得拟合线能够更多地关注异常值。
以最左边的图为例。我们可以看到,从直线到前两个数据点的偏差要么是 0,要么可以忽略不计。相比之下,其他两个图的直线在平均上更接近数据点。比较原始(未平方的)偏差值时,我们可以看到它们彼此相当接近。比较平方偏差值时,我们可以看到最左边的图的偏差比右边的图大 600%以上。这是因为较大的偏差受到的惩罚更大,这意味着异常值对最终直线的影响更大。
使用最小二乘法
最小二乘法可以通过两种方式实现。虽然使用矩阵运算是计算效率最高、最广泛使用的方法,我们将探讨使用梯度下降来寻找最佳直线。梯度下降是一种优化算法,我们将在其中计算和的导数,然后根据导数指示的方向调整系数值。这个过程会重复进行,直到找到最优解。这只是梯度下降的简要概述;请关注将为不懂微积分的人解释梯度下降的文章。
MSE 与 SSE
我们将使用均方误差(Mean of Squared Errors)作为我们的成本函数。基本上,我们想要最小化这个成本函数的值,以输出最拟合的直线。之前,我们使用 SSE(平方误差和)来确定哪条直线最适合其数据点。MSE 相当直观——它与 SSE 相同,但我们将最终的和除以数据点的数量:
作者提供的图片。
MSE 比 SSE 更受欢迎,有很多原因。
首先,均方误差(MSE)比平方误差和(SSE)对离群点的敏感度更低。由于 MSE 通过数据点的数量来归一化误差,离群点的影响较小。假设数据集中的误差为1, 4, 1, 25
。离群点(25)只占通过 MSE 计算误差的 25%。因此,MSE 将是7.75
。SSE 将是31
。
其次,使用 MSE 也允许比较不同直线的拟合度,即使它们使用的数据点数量不同。例如,考虑使用不同数量数据点的两个模型,模型 A 和模型 B。如果模型 A 使用 100 个数据点,模型 B 使用 50 个数据点,大多数情况下模型 A 将有更高的 SSE。然而,如果通过使用 MSE 来归一化误差,无论模型使用多少数据点,这些模型都可以直接进行比较。
上述因素的结合意味着 MSE 比 SSE 更易于解释。数据集中的离群点可能会使一个模型看起来比另一个模型显著更好,如果它们使用 SSE 进行比较,即使该模型可能更好地拟合大多数数据点。
代码时间!
有了我们对线性回归的所有知识,我们现在可以使用 Python 自行实现它!
开始使用
对于本教程,你需要:
- Python(版本 3.7 或更高)— 推荐有基础经验
(安装教程:www.tutorialspoint.com/how-to-install-python-in-windows
)
然后,在你的终端中使用pip
安装三个包:
-
pip install notebook
-
pip install numpy
-
pip install matplotlib
在终端中运行jupyter notebook
。你的默认网页浏览器会打开一个选项卡,在其中你会看到文件资源管理器。简单地进入你希望创建程序的目录,然后创建一个 Python 3 Notebook(在右上角选择new
)。你现在应该会看到以下界面:
图片由作者提供。
你可以通过点击“Untitled”来重命名文件。
初始数据
我们将开始导入 Python 库 NumPy,它在对数字数组进行数学操作时非常有用。然后,我们将定义点的 NumPy 数组,并将斜率和截距变量初始化为 0。看这些点,很容易推断出这些点的最适合的线是y=1x+0
。我们使用这样的可预测值,以便对模型进行基准测试。
# Importing numpy for number processing
import numpy as np
# Define the data points as a matrix, where each row represents a data point
# and each column represents a variable
points = np.array([(1, 1), (2, 2), (3, 3)])
# Defining initial values for the slope and y-intercept of the line
slope = 0
intercept = 0
按alt
+ enter
来创建一个新单元格。
线性函数
由于我们在执行线性回归,拥有一个可以评估线性函数的函数是有用的:
def result_of_function(independent_variable, slope, intercept):
"""
Function to model y=mx+b
:param slope: the slope of the linear function (m)
:param intercept: the y-intercept of the linear function (b)
:param independent_variable: the independent variable (x value) being inputted into the linear function (x)
:returns: the value of the dependent variable of the function (y)
"""
return independent_variable * slope + intercept
按alt
+ enter
来创建一个新单元格。
成本函数
包含一个成本函数来衡量我们回归的有效性也会有帮助:
def cost_function(x, y, slope, intercept):
"""
Calculate the mean squared error of a linear function with given parameters.
:param x: The independent variable (x-values) of the data points.
:param y: The dependent variable (y-values) of the data points.
:param slope: The slope of the linear function.
:param intercept: The y-intercept of the linear function.
:returns: The mean squared error of the linear function.
"""
# Predict the y-values using the given slope and intercept
y_preds = result_of_function(x, slope, intercept)
# Calculate the squared errors between the predicted and actual y-values
squared_errors = (y_preds - y)**2
# Return the mean of the squared errors
return squared_errors.mean()
按alt
+ enter
来创建一个新单元格。
梯度下降
我们将开始定义我们的输入和输出值(x 坐标和 y 坐标):
# Define the input and output data
X = np.array(points[:, 0])
Y = np.array(points[:, 1])
最后,我们将实现梯度下降:
alpha = 0.01
# Iterate for a 1000 of epochs
for i in range(1000):
# Calculate the gradients of J with respect to the slope and intercept
grad_slope = -2 * ((Y - result_of_function(X, slope, intercept)) * X).mean()
grad_intercept = -2 * ((Y - result_of_function(X, slope, intercept))).mean()
# Update m and b using the gradients and the learning rate
slope -= alpha * grad_slope
intercept -= alpha * grad_intercept
print(cost_function(X, Y, slope, intercept))
# Print the final values of m and b
print(f'Final values: slope = {slope}, intercept = {intercept}')
如果这不太有意义,别担心——梯度下降过于复杂,无法在本文中深入解释,所以请留意一篇解释梯度下降的文章,特别是对于不懂微积分的读者!
但你理解周期是很重要的。也就是说,这一行:
for i in range(1000):
# code
一个周期基本上是梯度下降程序的一次迭代。在每次迭代中,线性函数的斜率和截距会使用梯度下降计算中的数学公式进行调整。运行的周期越多,值调整和微调得越多。
运行此程序后,你应该得到以下斜率和截距值:
Final values: slope = 0.98539125301466, intercept = 0.033209115678908344
如果我们运行更多周期,这些值会更接近 1 和 0。然而,这些值已经非常接近预期结果。
从图形上看,结果如下:
图片由作者提供。
结果分析
最终的均方误差(MSE)是0.00015821003618271694
——这是一个极低的值。然而,如果我们将 MSE 图形化显示每个周期(或迭代),我们将得到以下图表:
图片由作者提供。
这些似乎是非常、非常小的收益。实际上,在第 25 个周期左右,MSE 似乎完全没有变化!让我们从不同的角度看这个图,省略前 50 个周期:
图片由作者提供。
看似直线的并非直线——从第 50 个 epoch 到第 1000 个 epoch,MSE 几乎减小了 100 倍。你可能会问——MSE 约 0.015 不是已经够低了吗?让我们尝试再次运行梯度下降,但这次只用 50 个 epochs:
Final values: slope = 0.8539016923117737, intercept = 0.32575579906831564
接近,但还不够接近:
作者提供的图片。
相反,让我们用 100,000 个 epochs 运行梯度下降:
作者提供的图片。
完美!似乎运行 100,000 个 epochs 的模型给出了几乎完美的结果。虽然用更多的 epochs 运行线性回归可以提高模型的准确性,但重要的是要考虑准确性和时间之间的平衡。一般来说,您应该使用足够的 epochs 来拟合模型的数据,但不要使用过多,以至于模型训练的时间不必要地延长。通常在各种模型中使用一种叫做早期停止的技术,当模型达到一定的准确性时会自动停止。这允许模型尽可能快地训练,但仍确保一定的准确性。
应用线性回归
最后但同样重要的是,是时候将我们的线性回归知识应用到实际数据上了!
查找数据集
让我们从寻找数据集开始。Kaggle是一个很好的资源,可以找到高质量且在结构或主题上各异的免费数据集。对于这个小项目,我选择使用钻石数据分析数据集,以便开发钻石克拉(自变量)与其价格(因变量)之间的线性关系。
选择数据集
通常,在选择或构建用于线性回归的数据集时,需要考虑以下因素:
-
自变量和因变量之间的强线性相关性——如果这些变量似乎没有相关性,或其相关性是非线性的,可能需要选择不同的回归方法。
-
异常值——一个好的数据集应该相对没有异常值,因为它们会严重影响回归的性能。
-
适用性——数据集必须与您试图解决的问题相关。例如,如果您想根据房屋的平方英尺预测纽约市的房价,那么用蒙大拿乡村农场的数据来训练模型就不合适。
下载数据集
在 Kaggle 上下载数据集非常直观。点击屏幕右上角的黑色下载按钮,将.zip
文件保存到计算机上。然后解压缩文件,将其中的.csv
文件移动到与 Jupyter Notebook 文件相同的目录中。
实施
现在,剩下的就是使用数据进行线性回归模型。再次地,在这个实例中,我们将根据钻石的克拉数预测价格。注释掉以下行:
X = np.array(points[:, 0])
Y = np.array(points[:, 1])
用以下内容替换它们:
diamond_data = np.genfromtxt('diamonds.csv', delimiter=',')
Y = diamond_data[1:][:, 7] # Costs of the diamonds
X = diamond_data[1:][:, 1] # Carats of the diamonds
运行程序(1000 个周期)后的输出结果是:
Final values: slope = 7756.425617968576, intercept = -2256.3605800455275
最终的 MSE 是:
2397955.0500126793
哇!这是一个极其庞大的数字。诚然,我们使用的钻石数据集存在缺陷。图上的线性回归是:
图片由作者提供。
以 1 克拉钻石的不同价格为例,它的价格范围可以从 ~$1000 到近$20,000!这是一个典型的例子,说明这个数据集在两个变量之间的线性关系不足。在这种情况下,钻石的切工、颜色和清晰度也都对价格产生了重大影响。同时,需要考虑的是,现实世界的数据中,MSE 为 0 几乎是不可能的。现实世界现象受到众多因素的影响,捕捉所有这些因素并在回归模型中反映出来几乎是不可能的。留给读者作为练习的是探索 Kaggle 上更强的两个给定变量之间的线性相关性数据集。
测试
让我们来测试一下我们的模型。根据CreditDonkey,1 克拉钻石的最佳价值在$4500 到$6000 之间。使用以下code
:
carat = 1
function_result = result_of_function(carat, slope, intercept)
print(f"A {carat}-carat diamond will cost: ${round(function_result, 2)}")
模型输出结果:
A 1-carat diamond will cost: $5488.47
成功!
结论
总结一下——线性回归是一种统计方法,用于理解两个线性相关变量之间的关系。这是通过将一条形式为y=mx+b
的直线拟合到提供的自变量和因变量上来完成的。通过使用称为最小二乘法的方法,可以找到最适合的直线,该方法最小化每个点到其对应直线上的点的平方偏差之和。最小二乘法可以通过矩阵运算和梯度下降来实现,本文重点介绍了梯度下降的应用。使用 MSE 成本函数(即平方误差的均值)来确定模型的准确性。通过最小化 MSE,我们可以优化模型并提高其准确性。
我给你留下了一个令人满意的 GIF,展示了模型逐渐收敛到最适合的直线:
图片由作者提供。
谢谢你的阅读!
机器学习不仅仅预测未来,它还积极地创造未来
关于位置偏差的入门(以及它为何重要)
·发表于Towards Data Science ·4 min read·2023 年 1 月 11 日
--
图片由 Stable Diffusion 生成
标准的机器学习课程教导我们,机器学习模型从过去存在的模式中学习,以预测未来。
这是一个很好的简化,但一旦这些模型的预测被用于生产环境中,情况会发生戏剧性的变化,因为它们会产生反馈循环:现在,模型的预测本身正在影响模型试图从中学习的世界。我们的模型不再仅仅是预测未来,它们实际上是在创造未来。
其中一个反馈循环是位置偏差,这一现象已经在排名模型中被观察到,这些模型支持搜索引擎、推荐系统、社交媒体信息流和广告排名器等。
什么是位置偏差?
位置偏差意味着排名最高的项目(Netflix 上的视频、Google 上的页面、Amazon 上的产品、Facebook 上的帖子或 Twitter 上的推文)之所以创造了最多的互动,不是因为它们实际上是用户最需要的内容,而仅仅是因为它们的排名最高。
这种偏差的表现形式是因为排名模型非常好,以至于用户开始盲目相信排名最高的项目,而不再进一步查看(“盲目信任偏差”),或者用户根本没有考虑其他可能更好的项目,因为它们的排名太低,用户甚至没有注意到(“展示偏差”)。
为什么这是一个问题?
让我们回到基础。排名模型的目标是展示最相关的内容,按参与概率的顺序排序。这些模型是基于隐式用户数据进行训练的:每次用户点击搜索结果页面上的一个项目或参与界面时,我们将该点击作为下一个模型训练迭代中的正标签。
如果用户只是因为内容的排名而非其相关性而开始与内容互动,我们的训练数据就会被污染:模型不仅仅是从用户真正想要的东西中学习,而是从自身过去的预测中学习。随着时间的推移,预测会变得静态,缺乏多样性。结果,用户可能会感到厌倦或烦恼,并最终转向其他地方。
另一个位置偏差的问题是离线测试变得不可靠。根据定义,位置偏倚的用户参与数据总是会偏向现有的生产模型,因为这是生成用户看到的排名的模型。一个实际上更好的新模型在离线测试中可能看起来更差,可能会被过早地丢弃。只有在线测试才能揭示真相。
我们如何减轻位置偏差?
模型从数据中学习,因此为了去偏模型,我们需要去偏训练数据。正如Joachims et al(2016)所示,这可以通过根据位置偏差的倒数加权每个训练样本来实现,为低偏差的样本赋予更多权重,为高偏差的样本赋予较少的权重。直观地,这很有意义:点击排名第一的项目(具有高位置偏差)可能比点击第十个项目(具有低位置偏差)信息量少。
因此,减轻位置偏差的问题归结为测量它。我们如何做到这一点?
一种方法是结果随机化:对于服务人群中的一个小子集,简单地随机重新排序前 N 项,然后测量在该人群中排名变化所引起的参与度变化。这种方法有效,但成本较高:随机搜索结果或推荐,尤其是对于较大的 N,会导致用户体验较差,从而影响用户留存率和商业收入。
因此,更好的替代方法可能是干预采集,由Argawal et al(2018)在全文档搜索的背景下提出,同时由Aslanyan et al(2019)在电子商务搜索的背景下提出。关键思想是,成熟排名系统中记录的用户参与数据已经包含了来自多个不同排名模型的排名,例如来自历史 A/B 测试或仅仅来自时间上推出的不同版本的生产模型。这种历史多样性在排名中创造了固有的随机性,我们可以“采集”这些数据来估计位置偏差,而无需任何昂贵的干预。
最后,还有一个更简单的想法,即谷歌的“规则 36”。他们建议在训练模型时将排名本身作为另一个特征添加,然后在推断时将该特征设置为默认值(例如 -1)。直觉是,通过提前将所有信息提供给模型,它会在后台隐式地学习参与模型和位置偏见模型。无需额外步骤。
最终思考
让我们回顾一下。位置偏见是一个在整个行业中都被观察到的真实问题。它之所以成问题,是因为它可能会限制排名模型的多样性。但我们可以通过使用偏见估计对训练数据进行去偏差来减轻它,这些偏见估计可以通过结果随机化或干预采集获得。另一种减轻策略是直接将排名作为模型特征,并让模型隐式地学习偏见,无需额外步骤。
从整体上考虑,位置偏见的存在确实有些讽刺。如果我们不断改进我们的排名模型,这些改进可能会导致越来越多的用户盲目相信排名最高的结果,从而增强位置偏见,并最终降低我们的模型效果。除非我们采取有意识的步骤来监测和减轻位置偏见,否则任何模型改进最终可能会变得适得其反。
这本书是什么?机器学习最奇特的事情之一是学术 ML 研究的二分法…
samflender.gumroad.com](https://samflender.gumroad.com/l/mlontheground?source=post_page-----1615895c80a9--------------------------------)
机器学习工程师——他们实际上做什么?
“机器学习工程师”对我们领域来说意味着什么新事物吗?如果是,那么是什么呢?
·
关注 发表在 Towards Data Science ·4 分钟阅读·2023 年 8 月 9 日
--
图片来源:Letizia Bordoni,来自 Unsplash
这个标题当然是一个圈套问题。就像之前的数据科学家一样,机器学习工程师这个头衔正在成为我们职业市场的一个趋势,但对这个头衔的含义或它应该包含的职能和技能没有共识。我想新进入数据科学/机器学习领域的从业者会发现这很难解读。(即便是有经验的人也会如此!)所以,让我们谈谈根据谁在说话,它可能意味着什么。
当我前几天与朋友讨论这个问题时,我用“MACHINE LEARNING 工程师”或机器学习 ENGINEER 来描述它。基本上,根据我看到的情况,这些头衔下的角色和期望要么是:
-
A. 期望具备广泛的软件工程技能,并且对机器学习有一定的经验或至少熟悉,或者
-
B. 对机器学习经验有较高期望,通常包括深度学习或生成式 AI,并且他们希望你能够在需要时编写一个函数。
以前这一类人可能只是“软件工程师”,而后一类人则舒适地归入“数据科学家”之下,回到我刚开始职业生涯的时候(尽管当时生成式 AI 确实不是游戏的一部分)。
这反映了我们职业领域更广泛的发展中的一个有趣模式。我们一直没有很好地将我们领域的角色划分为明确的子类别,以清晰界定角色的技能集(或职责)。这是一个快速发展、不断变化的年轻领域,所以这并不令人惊讶!这一直以来都是数据科学家这个头衔的特点,它本质上是“比数据分析师更具技术技能”的一个标识。曾经有些人把数据科学家称为能够处理非结构化或无序数据的人,而从我看来,这个定义因素已经不再存在。
我强烈怀疑 MLE 的增长是因为招聘 SWE 类型的人才时,雇主们对找不到懂得机器学习模型的人感到不满,而招聘数据科学家时,他们得到的是分析专家而不是具有机器学习技能的建模师。他们从两个方向交汇,形成了一个新头衔,在这个头衔下,对于每项技能的重视程度存在内部分歧。因此,现在我们有了一个新的领域需要思考。
虽然这一领域的细分可能非常自然,作为对这种困难的回应,我想指出这对候选人和领域的意义。每当发生新的分化,职业路径有了新的可能分支时,这两个方向会被赋予不同的地位和特权,最常通过每个方向的薪资差异来体现。现在,随着数据科学领域的正规化及更多教育机会的出现,人们进入这一职业的途径变得更加容易。这包括在更广泛社会中处于劣势或边缘化的人。我相信我们面临着数据科学家“粉红领”效应的风险。
(简而言之,粉色领带效应是指当女性在某一领域中的工作比例增加时,那些以她们为主的角色的薪资和社会地位系统性地降低。兽医学是一个常见的例子。情况也会相反,例如 1960 年代和 1970 年代初,女性在计算机编程领域占主导地位,当男性在该领域的代表性增加时,他们的薪资和声望也随之上升。)
这真的发生了吗?我不完全确定。我仅从像 Harnham 和 Burtch Works 这样的行业报告以及浏览 LinkedIn 等地方的招聘信息中看到一些轶事证据,这些证据表明数据科学家和机器学习工程师之间的薪资差距似乎正在出现。我确实比五年前遇到更多年轻女性、有色人种及不同性别认同和性取向的人在数据科学家角色中。
我非常希望研究人员能够发现这一薪资变化是否在统计上显著,如果显著的话,是否与我怀疑的员工人口统计变化相对应。
无论如何,对招聘领域的挑战是,不让更具声望的、更“技术性的”职位(例如现在的机器学习工程师)被男性和具有社会优势的人主导,同时相应地确保数据科学家职位不会成为一个较低地位的变种,导致其他人无论能力如何都被排挤。给这些职位支付符合你业务价值的薪资,但不要让这影响你考虑或设想每个角色中的人员组成。这是我们在不断发展的游戏阶段中能做到的最低限度。
你可以在 www.stephaniekirmer.com上找到更多我的作品。
使用机器学习进行柔术
原文:
towardsdatascience.com/machine-learning-for-jiu-jitsu-94a0b44f57ab
照片由 Kampus Production 提供,来自 Pexels: https://www.pexels.com/photo/a-judoka-throwing-an-opponent-to-the-ground-6765024/
使用 mediapipe 的姿态估计来跟踪柔术动作
·发表于Towards Data Science ·阅读时间 18 分钟·2023 年 3 月 13 日
--
姿态跟踪以提升柔术水平
巴西柔术是一种最近因其在实际战斗中的有效性和适用性而变得非常受欢迎的武术。
我已经练习巴西柔术超过 10 年,并决定将我对武术和机器学习的兴趣结合起来,提出一个位于这两个非常有趣领域交汇处的项目。
因此,我转向了姿态估计,作为一种有前景的技术,用于作为辅助工具帮助我在柔术中的发展。
在本文中,我想与大家分享如何使用姿态跟踪来增强战斗动作中的反馈纠正。
如果你更喜欢视频,可以在这里查看我关于此主题的 YouTube 视频:
什么是姿态跟踪?
姿态跟踪是利用计算机视觉技术实时检测和跟踪人体运动的过程。它涉及使用算法捕捉和解释各种身体部位(如手臂、腿部和躯干)的运动。
这一技术对于分析运动中的身体动作具有相关性,因为它允许教练和运动员识别和纠正可能对表现产生负面影响或导致受伤的运动模式。
通过提供实时反馈,运动员可以调整他们的技术,从而提高表现并减少受伤风险。此外,这项技术还可以用于将动作与顶级运动员的动作进行比较,例如,帮助初学者识别需要改进的领域,并相应地完善他们的技术。
什么是 Jiu Jitsu?
Jiu Jitsu 是一种以通过组合使用固定技和提交保持(如关节锁和窒息)来制服对手的武术。
Jiu Jitsu 专注于抓取和地面战斗技巧。它最初在日本开发,后来在巴西进行了修改和推广。但现在,由于其在美国特别是普及率的增加,它已经传播到全球。
基本原则是,较小、较弱的人可以通过使用杠杆和技巧来防御较大、较强的对手。练习者的目标是控制对手的身体,并将自己置于一个主导位置,以便执行诸如窒息、关节锁和投掷等技巧。
图片由 Timoth Eberly 提供,链接: *https://unsplash.com/photos/7MRajrPiTqw**
Jiu Jitsu 现在是一项在全球范围内流行的运动和自我防卫系统。它要求身体和心理上的纪律,以及学习和适应的意愿。
研究还发现,它具有许多好处,包括 改善身体健康和心理敏锐性、增加自信和自尊、以及缓解压力。
为什么选择 Jiu Jitsu 的姿势追踪
对技巧的高度重视使得这门武术相当独特,在 Jiu Jitsu 俱乐部的环境中,通常是黑带教练的职责来给学生反馈,评价他们对不同技巧的执行是否得当。
然而,人们常常希望学习,但要么无法接触到专家,要么班级人数过多,导致授课者难以提供具体和个人化的反馈,无法确定学生是否正确执行了动作。
在这种反馈的空白中,我认为像姿势追踪这样的工具可以极大地惠及武术世界,尤其是 Jiu Jitsu(尽管可以对柔道、摔跤和以打击为基础的武术做同样的论证),因为它们可以无缝集成到智能手机中,只需运动员在尝试改进的动作时拍摄自己。
这种反馈的形式需要进行开发,本文旨在提供有关这种基于机器学习的反馈系统如何帮助学生提高运动基础动作的指导。
我为什么要这样做?
好的,故事是这样的。
通常,当你发展你的柔术技能时,你最终会落入两个类别之一:下位选手或上位选手。这意味着你是否倾向于从下方使用“防守”(指使用双腿对对手进行攻击)进行比赛,还是从上方先将对手摔倒,然后继续穿过对方的双腿(通常)达到如骑乘对手或抓住对手背部等占据主导地位的目标。
图片由诺兰·肯特提供,来源于 unsplash.com/photos/x_V62hOwnDk?utm_source=unsplash&utm_medium=referral&utm_content=creditShareLink
这种二元性显然是人为的,通常,大多数经验丰富的选手能很好地掌握两种位置。
然而,许多人在柔术旅程的开始时倾向于偏好某些技巧,这可能会严重影响他们在其他领域的进步,如果他们不断重复同样的策略的话。
在某种程度上,这就是我所经历的,我曾经作为防守选手打斗很多,这主要是由于巴西的主流文化,鼓励从膝盖开始摔跤以避免受伤,或者因为比赛垫的空间不像美国大中学体育馆中的摔跤垫那么大。
作者提供的图片。比赛中我进行防守的照片。
这种主动坐下来并从背后打斗的习惯,未能让对手参与站立战斗,对我的武术发展产生了负面影响,因为随着我在柔术中变得越来越好,我意识到阻碍我的一个因素是我缺乏高水平的摔倒对手的知识。
这激发了我从站立位置开始更多地进行训练,于是我在获得棕带的几年后,开始学习和练习摔跤和柔道。
在过去的 2 年里,我主要是一个上位选手,确实在将对手摔倒在地的能力上有了很大提升。
作者提供的图片。我的摔跤之旅。
不过,例如在柔道中有一些基础动作非常难以掌握,因为我不认识任何柔道专家,也不住在任何高水平的柔道或摔跤馆附近,我意识到我需要另一种方式来提高某些基础动作,特别是像“内腿技”和其他基于髋部的摔法的髋部灵活性。
照片由 Kampus Production 提供,来源于: https://www.pexels.com/photo/a-judoka-throwing-an-opponent-to-the-ground-6765024/
机器学习的作用
好的,为了提高我执行像内股这样的柔道投掷技术的能力,我制定了一个“书呆子”的计划:我要使用机器学习(我知道,这个计划真是太具体了)。
我决定要调查是否可以使用姿态追踪来获取关于如何纠正脚的速度和方向以及执行这些动作的其他方面的见解。
那么让我们来看看我是怎么做的。
使用姿态追踪生成柔术见解的步骤
整体计划是这样的:
1. 找到一个包含我想要模仿动作的视频参考
2. 录制自己多次执行该动作
3. 使用姿态追踪和 Python 可视化生成见解。
为了做到这一点,我需要一个顶级练习者执行我试图学习的动作的参考视频。对于内股,我找到了一段奥林匹克级别选手在墙边进行热身的技术视频,这直接与我想要学习的内容相关:
然后我开始录制自己执行这些动作的视频,至少是我正在积极学习的某些动作。
图片由作者提供。
拥有了参考视频,并且录制了一些自己的镜头之后,我准备尝试一些有趣的机器学习内容。
姿态追踪来追踪身体关节
对于姿态追踪,我使用了一个叫做mediapipe的工具,这是谷歌的开源项目,旨在促进机器学习在实时和流媒体中的应用。
## GitHub - google/mediapipe: 跨平台、可定制的机器学习解决方案,适用于实时和流媒体。
MediaPipe 提供跨平台、可定制的机器学习解决方案,适用于实时和流媒体。端到端加速……
这个选项的易用性让我很兴奋,迫不及待地想要尝试。
本质上,我做了以下工作:
1. 首先,我创建了一些叠加姿态估计的视频
2. 创建了实时绘图,展示脚的 x、y 和 z 坐标,以说明动作的主要方面
3. 创建了表示某个动作在特定时间执行的轨迹
4. 将我的尝试所产生的轨迹与专家视频生成的参考轨迹进行比较
初步结果
1. 姿态估计叠加
我写了这段代码来创建模型估计身体关节位置的视频
并将其叠加在实际镜头中,以展示模型的鲁棒性。
图片由作者提供
是的,是的,我知道,我看起来并不完全像顶级选手。但给我一点时间,我的柔道技能还在建设中!
我用来做这个的代码是:
from base64 import b64encode
import cv2
import mediapipe as mp
import matplotlib.pyplot as plt
import seaborn as sns
sns.set()
import numpy as np
from natsort import natsorted
from mpl_toolkits.mplot3d import Axes3D
from matplotlib.animation import FuncAnimation
from IPython.display import clear_output
%matplotlib inline
import pandas as pd
import plotly.graph_objects as go
import plotly.express as px
from IPython.display import HTML, display
import ipywidgets as widgets
from typing import List # I don't think I need this!
# Custom imports
from pose_tracking_utils import *
mp_drawing = mp.solutions.drawing_utils
mp_drawing_styles = mp.solutions.drawing_styles
mp_pose = mp.solutions.pose
def create_pose_tracking_video(video_path):
# For webcam input:
cap = cv2.VideoCapture(video_path)
frame_width = int(cap.get(3))
frame_height = int(cap.get(4))
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
output_path = pathlib.Path(video_path).stem + "_pose.mp4"
out = cv2.VideoWriter(output_path, fourcc, 30.0, (frame_width, frame_height))
with mp_pose.Pose(min_detection_confidence=0.5,
min_tracking_confidence=0.5) as pose:
while cap.isOpened():
success, image = cap.read()
if not success:
print("Ignoring empty camera frame.")
break
# To improve performance, optinally mark the iamge as
# not writeable to pass by reference.
image.flags.writeable = False
image= cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
results = pose.process(image)
# Draw the annotation on the image.
image.flags.writeable = True
image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
mp_drawing.draw_landmarks(image, results.pose_landmarks,
mp_pose.POSE_CONNECTIONS,
landmark_drawing_spec=mp_drawing_styles.get_default_pose_landmarks_style())
# Flip the image horizontally for a self-view display.
out.write(cv2.flip(image, 1))
if cv2.waitKey(5) & 0xFF == 27:
break
cap.release()
out.release()
print("Pose video created!")
return output_path
这基本上利用了 mediapipe 包来生成一个可视化,它检测关键点并将其覆盖在视频画面上。
2. 脚部的 X、Y 和 Z 坐标的实时图表
VIDEO_PATH = "./videos/clip_training_session_1.mp4"
# Initialize MediaPipe Pose model
body_part_index = 32
pose = mp_pose.Pose(static_image_mode=False, min_detection_confidence=0.5, min_tracking_confidence=0.5)
# Initialize OpenCV VideoCapture object to capture video from the camera
cap = cv2.VideoCapture(VIDEO_PATH)
# Create an empty list to store the trace of the right elbow
trace = []
# Create empty lists to store the x, y, z coordinates of the right elbow
x_vals = []
y_vals = []
z_vals = []
# Create a Matplotlib figure and subplot for the real-time updating plot
# fig, ax = plt.subplots()
# plt.title('Time Lapse of the X Coordinate')
# plt.xlabel('Frames')
# plt.ylabel('Coordinate Value')
# plt.xlim(0,1)
# plt.ylim(0,1)
# plt.ion()
# plt.show()
frame_num = 0
while True:
# Read a frame from the video capture
success, image = cap.read()
if not success:
break
# Convert the frame to RGB format
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# Process the frame with MediaPipe Pose model
results = pose.process(image)
# Check if any body parts are detected
if results.pose_landmarks:
# Get the x,y,z coordinates of the right elbow
x, y, z = results.pose_landmarks.landmark[body_part_index].x, results.pose_landmarks.landmark[body_part_index].y, results.pose_landmarks.landmark[body_part_index].z
# Append the x, y, z values to the corresponding lists
x_vals.append(x)
y_vals.append(y)
z_vals.append(z)
# # Add the (x, y) coordinates to the trace list
trace.append((int(x * image.shape[1]), int(y * image.shape[0])))
# Draw the trace on the image
for i in range(len(trace)-1):
cv2.line(image, trace[i], trace[i+1], (255, 0, 0), thickness=2)
plt.title('Time Lapse of the Y Coordinate')
plt.xlabel('Frames')
plt.ylabel('Coordinate Value')
plt.xlim(0,len(pose_coords))
plt.ylim(0,1)
plt.plot(y_vals);
# Clear the plot and update with the new x, y, z coordinate values
#ax.clear()
# ax.plot(range(0, frame_num + 1), x_vals, 'r.', label='x')
# ax.plot(range(0, frame_num + 1), y_vals, 'g.', label='y')
# ax.plot(range(0, frame_num + 1), z_vals, 'b.', label='z')
# ax.legend(loc='upper left')
# plt.draw()
plt.pause(0.00000000001)
clear_output(wait=True)
frame_num += 1
# Convert the image back to BGR format for display
image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
# Display the image
cv2.imshow('Pose Tracking', image)
# Wait for user input to exit
if cv2.waitKey(1) & 0xFF == ord('q'):
break
# Release the video capture, close all windows, and clear the plot
cap.release()
cv2.destroyAllWindows()
plt.close()
然后,我生成了一个包含 x、y、z 坐标时间线的图表:
plt.figure(figsize=(15,7))
plt.subplot(3,1,1)
plt.title('Time Lapse of the x Coordinate')
plt.xlabel('Frames')
plt.ylabel('Coordinate Value')
plt.xlim(0,len(pose_coords))
plt.ylim(0,1)
plt.plot(x_vals)
plt.subplot(3,1,2)
plt.title('Time Lapse of the y Coordinate')
plt.xlabel('Frames')
plt.ylabel('Coordinate Value')
plt.xlim(0,len(pose_coords))
plt.ylim(0,1.1)
plt.plot(y_vals)
plt.subplot(3,1,3)
plt.title('Time Lapse of the z Coordinate')
plt.xlabel('Frames')
plt.ylabel('Coordinate Value')
plt.xlim(0,len(pose_coords))
plt.ylim(-1,1)
plt.plot(z_vals)
plt.tight_layout();
这个想法是为了对诸如执行动作时脚的位置方向等细节进行细致控制。
现在我对模型能够正确捕捉我的身体姿势感到自信,我创建了一些相关身体关节的轨迹可视化,比如脚部(在执行摔跤技术时非常重要)。
3. 创建运动轨迹
为了了解动作的执行情况,我制作了一个可视化,表示从身体部位的角度(在这种情况下是脚)执行该动作的过程:
作者提供的图片
我为我的训练课程和包含我试图模仿的动作的参考视频做了这个。
这里需要注意的是,进行此操作存在许多问题,包括相机的分辨率、动作执行的距离以及每个视频的帧率。然而,我只是绕过了这些问题,创建了一个花哨的图表(哈哈)。
这是这种方法的代码:
def create_joint_trace_video(video_path,body_part_index=32, color_rgb=(255,0,0)):
"""
This function creates a trace of the body part being tracked.
body_part_index: The index of the body part being tracked.
video_path: The path to the video being analysed.
"""
# Initialize MediaPipe Pose modelpose = mp_pose.Pose(static_image_mode=False, min_detection_confidence=0.5, min_tracking_confidence=0.5)
# Initialize OpenCV VideoCapture object to capture video from the camera
cap = cv2.VideoCapture(video_path)
frame_width = int(cap.get(3))
frame_height = int(cap.get(4))
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
output_path = pathlib.Path(video_path).stem + "_trace.mp4"
out = cv2.VideoWriter(output_path, fourcc, 30.0, (frame_width, frame_height))
# Create an empty list to store the trace of the body part being tracked
trace = []
with mp_pose.Pose(min_detection_confidence=0.5,
min_tracking_confidence=0.5) as pose:
while cap.isOpened():
success, image = cap.read()
if not success:
print("Ignoring empty camera frame.")
break
# Convert the frame to RGB format
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# Process the frame with MediaPipe Pose model
results = pose.process(image)
# Check if any body parts are detected
if results.pose_landmarks:
# Get the x,y coordinates of the body part being tracked (in this case, the right elbow)
x, y = int(results.pose_landmarks.landmark[body_part_index].x * image.shape[1]), int(results.pose_landmarks.landmark[body_part_index].y * image.shape[0])
# Add the coordinates to the trace list
trace.append((x, y))
# Draw the trace on the image
for i in range(len(trace)-1):
cv2.line(image, trace[i], trace[i+1], color_rgb, thickness=2)
# Convert the image back to BGR format for display
image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
# Display the image
out.write(image)
if cv2.waitKey(5) & 0xFF == 27:
break
cap.release()
out.release()
print("Joint Trace video created!")
在这里,我简单地处理每一帧,就像之前制作姿势视频一样,然而,我还将特定身体部位的 x 和 y 坐标追加到一个我称之为trace
的列表中,这个列表用于生成伴随身体部位的轨迹线。
4. 比较轨迹
拥有这些能力后,我终于可以进入从这种方法中获取见解的阶段。
为了做到这一点,我需要一种比较这些轨迹的方法,以生成一些视觉丰富的反馈,这可以帮助我理解自己动作执行的不足与顶级运动员的表现相比如何。
现在,没有背景视频的实际轨迹已被绘制成图表。
def get_joint_trace_data(video_path, body_part_index,xmin=300,xmax=1000,
ymin=200,ymax=800):
"""
Creates a graph with the tracing of a particular body part,
while executing a certain movement.
"""
cap = cv2.VideoCapture(video_path)
frame_width = int(cap.get(3))
frame_height = int(cap.get(4))
# Create an empty list to store the trace of the body part being tracked
trace = []
i = 0
with mp_pose.Pose(min_detection_confidence=0.5,
min_tracking_confidence=0.5) as pose:
while cap.isOpened():
success, image = cap.read()
if not success:
print("Ignoring empty camera frame.")
break
# Convert the frame to RGB format
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# Process the frame with MediaPipe Pose model
results = pose.process(image)
# Check if any body parts are detected
if results.pose_landmarks:
# Get the x,y coordinates of the body part being tracked (in this case, the right elbow)
x, y = int(results.pose_landmarks.landmark[body_part_index].x * image.shape[1]), int(results.pose_landmarks.landmark[body_part_index].y * image.shape[0])
# Add the coordinates to the trace list
trace.append((x, y))
# Plot the trace on the graph
fig, ax = plt.subplots()
#ax.imshow(image)
ax.set_xlim(xmin,xmax)
ax.set_ylim(ymin,ymax)
ax.invert_yaxis()
ax.plot(np.array(trace)[:, 0], np.array(trace)[:, 1], color='r')
# plt.savefig(f'joint_trace{i}.png')
# plt.close()
i+=1
plt.pause(0.00000000001)
clear_output(wait=True)
# Display the graph
#plt.show()
if cv2.waitKey(5) & 0xFF == 27:
break
cap.release()
return trace
video_path = "./videos/clip_training_session_2.mp4"
body_part_index = 31
foot_trace = get_joint_trace_data(video_path, body_part_index)
video_path = "./videos/uchimata_wall.mp4"
body_part_index = 31
foot_trace_reference = get_joint_trace_data(video_path, body_part_index,xmin=0,ymin=0,xmax=1300)
foot_trace_clip = foot_trace[:len(foot_trace_reference)]
plt.subplot(1,2,1)
plt.plot(np.array(foot_trace_clip)[:, 0], np.array(foot_trace_clip)[:, 1], color='r')
plt.gca().invert_yaxis();
plt.subplot(1,2,2)
plt.plot(np.array(foot_trace_reference)[:, 0], np.array(foot_trace_reference)[:, 1], color='g')
plt.gca().invert_yaxis();
好的,通过这些,我们开始更清楚地看到在不同背景下脚部移动的特征形状之间的差异。
首先,我们看到,虽然顶级运动员在转弯时做了一个比较直的步骤,用脚生成了一个几乎完整的半圆,而我则在初步步骤内有一个弯曲的外观,并且在将腿抬到空中时也没有生成半圆。
此外,虽然顶级运动员在抬起腿时会生成一个宽大的圆圈,而我则会生成一个浅圆圈,几乎像是一个椭圆。
作者提供的图片,比较动作执行的轨迹
我发现这些初步结果相当不错,因为它们表明,尽管比较存在局限性,但通过观察这些轨迹,可以评估动作执行的特征形状之间的差异。
除此之外,我还想看看是否可以比较动作执行的速度,为此我可视化了身体关节的实时运动,将我和专家的图放在一起,看看我的时机偏差有多大。
这项分析的挑战在于,由于视频的速度各不相同且未对齐,我首先需要以有意义的方式对齐它们。
我不确定该使用哪种技术,但与我的朋友 Aaron(里斯本 Champalimaud 神经科学研究所的神经科学家)的对话让我有了一个选择:动态时间规整。
使用动态时间规整比较速度和时机
动态时间规整(DTW)是一种用于测量两个具有不同速度的时间序列之间相似度的技术。
基本思想是你有两个不同的时间序列,它们可能包含一些你希望分析的模式,因此你试图通过应用一些规则将它们对齐,从而计算两个序列之间的最佳匹配。
两次步态序列,尽管速度各异,我们可以观察到四肢的轨迹非常相似;取自维基百科,参考(Olsen et al, 2017)。
我在这篇文章中找到了对这个话题的很好的介绍:
解释与代码实现
towardsdatascience.com
作者:Jeremy Zhang。
为了使用动态时间规整,我做了以下工作:
1. 将值归一化到相同范围
2. 使用了 DTW 算法的 Python 实现。
from fastdtw import fastdtw
from scipy.spatial.distance import euclidean
max_x = max(max(foot_trace_clip, key=lambda x: x[0])[0], max(foot_trace_reference, key=lambda x: x[0])[0])
max_y = max(max(foot_trace_clip, key=lambda x: x[1])[1], max(foot_trace_reference, key=lambda x: x[1])[1])
foot_trace_clip_norm = [(x/max_x, y/max_y) for (x, y) in foot_trace_clip]
foot_trace_reference_norm = [(x/max_x, y/max_y) for (x, y) in foot_trace_reference]
distance, path = fastdtw(foot_trace_clip_norm, foot_trace_reference_norm, dist=euclidean)
我得到的输出是:
1. distance
:两个时间序列向量之间的欧几里得距离。
2. path
:两个时间序列之间的映射,以嵌套的元组列表形式存在
现在,我可以使用存储在path
变量中的输出,创建一个对齐了两个序列的图:
foot_trace_reference_norm_mapped = [foot_trace_reference_norm[path[i][1]] for i in range(len(path))]
foot_trace_clip_norm_mapped = [foot_trace_clip_norm[path[i][1]] for i in range(len(path))]
plt.subplot(1,2,1)
plt.plot(np.array(foot_trace_reference_norm_mapped)[:, 0], np.array(foot_trace_reference_norm_mapped)[:, 1], color='g')
plt.gca().invert_yaxis();
plt.subplot(1,2,2)
plt.plot(np.array(foot_trace_clip_norm_mapped)[:, 0], np.array(foot_trace_clip_norm_mapped)[:, 1], color='r')
plt.gca().invert_yaxis();
plt.show()
图片由作者提供,使用 DTW 算法对齐的时间序列
现在,由于参考轨迹的数据不足,我不能说这个图比之前讨论的元素给了我更多的见解,然而,它确实有助于突出我之前提到的运动形状。
然而,作为未来的一个备注,我的想法是,如果可以满足某些条件来帮助使两个视频更一致,我希望有一个参考轨迹,我可以用来比较我的尝试轨迹,以便用于即时反馈。
我将使用 DTW 算法输出的欧几里得距离作为我的反馈指标,并且会有一个应用程序可以突出显示我是否接近或远离我尝试模仿的签名形状。
为了说明这一点,让我给你展示一个例子。
def find_individual_traces(trace,window_size=60, color_plot="r"):
"""
Function that takes in a liste of tuples containing x,y coordinates
and plots them as different clips with varying sizes to allow the user to find
the point where a full repetition has been completed
"""
clip_size = 0
for i in range(len(trace)//window_size):
plt.plot(np.array(trace[clip_size:clip_size+window_size])[:, 0], np.array(trace[clip_size:clip_size+window_size])[:, 1], color=color_plot)
plt.gca().invert_yaxis()
plt.title(f"Trace, clip size = {clip_size}")
plt.show()
clip_size+=window_size
def get_individual_traces(trace, clip_size):
num_clips = len(trace)//clip_size
trace_clips = []
i = 0
for clip in range(num_clips):
trace_clips.append(trace[i:i+clip_size])
i+=clip_size
return trace_clips
find_individual_traces(foot_trace_clip_norm)
图片由作者提供。由我执行的脚部动作的轨迹。
这里我展示了视频中的剪辑,我在每个单独的动作中执行。每个这些轨迹可以与类似获得的参考轨迹进行比较:
find_individual_traces(foot_trace_reference_norm, window_size=45,color_plot="g")
图片由作者提供。由精英玩家执行的脚部动作的轨迹。
当我获得参考轨迹时,也会得到一些噪声信号,但我将使用第三个作为我的参考:
图片由作者提供
现在我可以循环遍历代表我实际动作的轨迹,并查看它们如何与在几次训练课程中得到的参考轨迹进行比较。
video_path = "./videos/clip_training_session_3.mp4"
body_part_index = 31
foot_trace_clip = get_joint_trace_data(video_path, body_part_index)
video_path = "./videos/uchimata_wall.mp4"
body_part_index = 31
foot_trace_reference = get_joint_trace_data(video_path, body_part_index,xmin=0,ymin=0,xmax=1300)
# Showing a plot with the tracings from the training session
plt.plot(np.array(foot_trace_clip)[:, 0], np.array(foot_trace_clip)[:, 1], color='r')
plt.gca().invert_yaxis();
图片由作者提供。几次执行动作中,脚的 x,y 坐标的轨迹。
现在我从两个轨迹中获取标准化值。
max_x = max(max(foot_trace_clip, key=lambda x: x[0])[0], max(foot_trace_reference, key=lambda x: x[0])[0])
max_y = max(max(foot_trace_clip, key=lambda x: x[1])[1], max(foot_trace_reference, key=lambda x: x[1])[1])
foot_trace_clip_norm = [(x/max_x, y/max_y) for (x, y) in foot_trace_clip]
foot_trace_reference_norm = [(x/max_x, y/max_y) for (x, y) in foot_trace_reference]
我从训练剪辑和参考轨迹中获取轨迹,以帮助我设定目标。
剪辑大小是手动设置的。
traces = get_individual_traces(foot_trace_clip_norm, clip_size=67)
traces_ref = get_individual_traces(foot_trace_reference_norm, clip_size=60)
我展示了在经过经验观察手动分类为噪声后去除了一些轨迹的例子。
# Here I show an example trace from the new clip
index = 0
color_plot = "black"
plt.plot(np.array(traces[index])[:, 0], np.array(traces[index])[:, 1], color=color_plot)
plt.gca().invert_yaxis()
plt.title(f"Trace {index}")
plt.show()
图片由作者提供
然后,我循环遍历轨迹,并将它们的得分与从精英玩家视频中获得的参考轨迹进行比较:
trace_ref = traces[2]
trace_scores = []
for trace in traces:
distance, path = fastdtw(trace, trace_ref, dist=euclidean)
trace_scores.append(distance)
plt.plot(trace_scores, color="black")
plt.title("Trace Scores with DTW")
plt.xlabel("Trace Index")
plt.ylabel("Euclidean Distance Score")
plt.show()
图片由作者提供
现在,我注意到的第一个奇怪的现象是指标的上下波动,这只能通过一些获得的轨迹指向脚下落而非上升来解释。
然而,这个图表的有趣之处在于,轨迹的得分似乎甚至略有改善,并且至少保持在 20(在这种情况下是两序列之间的欧几里得距离的度量)。
尽管此时无法明确解释这些数字,但我发现像这样的处理方法可以转换为一个可衡量的指标,用于比较一个动作相对于另一个动作的质量,这一点相当有见地。
最终备注
未来,我希望研究如何更好地提取训练片段,以获得每次动作执行的完美对齐段,以便产生更一致的结果。
总的来说,我认为做这些实验相当有趣,因为它突出了这种技术在提供动作的详细评估方面的力量,尽管它仍需大量工作才能成为一个有用的洞察工具。
如果你喜欢这篇文章, 加入 Medium,并订阅我的 Youtube 频道 和 我的新闻通讯。谢谢,下次见! 😃
参考文献
使用不平衡数据进行回归的机器学习
原文:
towardsdatascience.com/machine-learning-for-regression-with-imbalanced-data-62629d7ad330
为什么在数据集中预测异常值如此困难,以及你可以采取什么措施来应对
·发表于 Towards Data Science ·阅读时间 6 分钟·2023 年 8 月 10 日
--
什么是不平衡数据?
许多现实世界的数据集存在不平衡的问题,其中某些类型的样本在数据集中被过度代表,而其他类型则出现较少。一些例子包括:
-
在将信用卡交易分类为欺诈性或合法交易时,大多数交易将属于后者类别
-
强降雨发生的频率低于中等降雨,但可能对人类和基础设施造成更大的损害
-
在尝试识别土地用途时,代表森林和农业的像素比城市定居点的像素更多
在这篇文章中,我们旨在提供对机器学习算法为何在不平衡数据上表现不佳的直观解释,展示如何使用分位数评估来量化算法的性能,并展示三种不同的策略来提高算法的性能。
由 Elena Mozhvilo 在 Unsplash 上的照片
回归示例数据集:加州住房
数据集不平衡通常在分类问题中表现为一个多数类掩盖了一个少数类。在这里,我们关注的是回归问题,其中目标是一个连续的数值。我们将使用scikit-learn 提供的加利福尼亚住房数据集。该数据集包含超过 20,000 个房屋样本,特征包括位置、房间和卧室数量、房龄、面积和邻里中位收入。目标变量是中位房价,以百万美元计。为了查看数据集是否不平衡,我们绘制了目标变量的直方图。
加利福尼亚房价数据集中目标变量的直方图。红线表示均值为 1.9 M$。
显然,并不是所有的中位房价都被同等地表示。目标变量的均值为 1.9 M\(,标准差为 0.98 M\),但这些值并不遵循正态分布——分布偏向于中位值超过 4.0 M$ 的昂贵房屋。
均方误差损失函数
我们在 Keras 中实现了一个小型神经网络,并使用它来预测中位房价。由于这是一个回归问题,均方误差函数是一个合适的损失函数。对于给定的一批样本,均方误差(MSE)计算如下:
在这个公式中,预测值和实际值的距离是损失的决定性因素。
模型训练很快,损失曲线看起来合理。训练集的最终损失为 0.2562,验证集为 0.2584,因此我们似乎达到了偏差-方差权衡的最佳点。
使用目标变量的分位数进行评估。
总体而言,我们的机器学习算法在保留的测试集上产生了 0.27 的均方误差。但这与不同房价相比如何呢?我们将目标变量分成每个 1 M$ 价格区间的分箱,并分别计算每个分箱中样本的均方误差。
加利福尼亚房价数据集中目标变量的均方误差。
正如我们所见,我们的机器学习算法在接近目标变量均值的样本上表现最佳。在最高的分箱中,房价超过 4 M$,误差几乎高出 10 倍!
分位数评估是探索机器学习算法在数据集不同区域表现的好方法。这种快速分析可以直接指出实验设置中的问题,你应始终在仅报告整个数据集的平均性能指标之前考虑它。
为什么模型难以预测高房价?
简单规则学习
我们甚至不能责怪机器学习算法,因为它正好做了我们要求的事情:对于大多数样本,它具有良好的预测能力。只是价格超过 400 万美元的房屋在数据集中不足代表——只有 4%的训练数据落在这个范围内——因此算法没有足够的激励来优先考虑这些样本。
我们应该始终记住,机器学习算法在学习我们提出的任务。它们容易简单规则学习,这由 Geirhos 等(2021)定义:
简单规则是在标准基准测试中表现良好,但在更具挑战性的测试条件下(如真实世界场景)失败的决策规则。相关问题在比较心理学、教育学和语言学中已知,表明简单规则学习可能是学习系统(无论是生物的还是人工的)共同的特征。
提出正确的问题
比如说,我们正在为一个主要关注估算高端房屋中位数价值的房地产经纪人开发算法。对于这个客户,目前的算法无法提供期望的预测能力,因为它在他们感兴趣的房屋类型上的表现不佳。
专注于最昂贵的房屋,而不是在平均样本上表现出色。照片由 Daniel Barnes 提供,来源于 Unsplash
处理不平衡数据
策略 1:增加批量大小
增加批量大小时,每个训练批次包含来自不足代表组的样本的可能性更高。我们将批量大小设置为 512,并重复模型训练。
策略 2:在损失函数中引入权重
在这里,我们要求机器学习算法关注在训练中不足代表的样本。这些样本具有更高的权重。权重可以直接计算并传递给 Keras 中的函数 model.fit(..., sample_weights=...)
。
用语言解释权重的计算:
-
计算每个区间的样本出现次数
-
除以样本总数——你将获得给定区间的样本频率
-
逆数是相关的权重
以及代码:
策略 3:将目标值转换为正态分布
在这种情况下,我们将目标变量转换为符合正态分布。正态分布最适合用于均方误差损失,并减少异常值的特征。重要——评估之前不要忘记重新缩放预测值!
评估
哪种策略表现最好?
现在是时候比较三种不同的策略了。我们跟踪了每种策略下每个目标变量区间的均方误差,如下图所示。仅关注最高区间时,带权重的损失表现最佳。虽然它在平均房价的算法表现有所降低,但在我们最感兴趣的区域,它增加了预测能力。其他两种策略,即增加批量大小和缩放目标变量,甚至增加了最高区间的均方误差。因此,它们没有被证明对解决我们的问题有帮助。
各策略下目标变量每个区间的均方误差。
因此,我们建议我们的客户——高端房地产经纪人——使用一种机器学习算法,该算法使用权重来强调数据集中表现不足的样本。然而,请注意,机器学习是一门经验科学,对于其他数据集,您可能会找到解决数据集不平衡问题的不同方案。
《机器学习插图:分类的评估指标》
原文:
towardsdatascience.com/machine-learning-illustrated-classification-evaluation-metrics-dfc33b373c43
一个全面(且丰富多彩)的指南,介绍你需要了解的关于评估分类模型的一切
·发表于 Towards Data Science ·12 分钟阅读·2023 年 4 月 20 日
--
我在学习过程中意识到自己是一个非常视觉化的学习者,我喜欢使用颜色和有趣的插图来学习新概念,特别是那些通常像这样解释的科学概念:
从我之前的文章中,通过大量可爱的评论和消息(感谢所有的支持!),我发现有很多人对这种情感产生了共鸣。因此,我决定开始一个新的系列,我将尝试用插图来讲解机器学习和计算机科学的概念,希望能使学习变得有趣。所以,请系好安全带,享受这段旅程吧!
让我们通过探索机器学习中的一个基本问题来开始这个系列:我们如何评估分类模型的性能?
在之前的文章中,如 决策树分类 和 逻辑回归,我们讨论了如何构建分类模型。然而,量化这些模型的表现至关重要,这就提出了一个问题:我们应该使用什么指标来做到这一点?
为了说明这个概念,让我们构建一个贷款还款分类模型。
我们的目标是预测一个人是否可能还清贷款,基于他们的信用评分。虽然年龄、薪水、贷款金额、贷款类型、职业和信用历史等其他变量也可能影响这样的分类器,但为了简便起见,我们只考虑信用评分作为我们模型的主要决定因素。
根据逻辑回归文章中列出的步骤,我们构建了一个分类器,该分类器根据信用评分预测某人是否会还款。
从中我们可以看到,信用评分越低,越可能该人不会还款,反之亦然。
目前,该模型的输出是一个人会还款的概率。然而,如果我们想将贷款分类为会还款或不会还款,我们需要找到将这些概率转换为分类的方法。
一种方法是设定 0.5 作为阈值,将低于该阈值的人分类为不会还款,高于该阈值的人分类为会还款。
从中我们推断出,这个模型会将信用评分低于 600 的人分类为不会还款(粉色),高于 600 的人分类为会还款(蓝色)。
使用 0.5 作为分界线,我们将一个信用评分为 420 的人分类为…
…不会还款。而这个信用评分为 700 的人则为…
…会还款。
现在为了测试我们的模型效果,我们需要远远超过 2 个人的数据。因此,让我们深入挖掘过去的记录,收集 10000 人的信用评分以及他们是否还款的信息。
注意:在我们的记录中,有 9500 人还款,只有 500 人未还款。
然后我们对每个人运行我们的分类器,根据他们的信用评分预测他们是否会还款。
混淆矩阵
为了更好地可视化我们的预测与实际情况的比较,我们创建了一个称为混淆矩阵的东西。
在这个特定的混淆矩阵中,我们将还款的个体视为正例标签,将未还款的个体视为负例标签。
-
真阳性(TP):实际还款的人,并且被模型正确地分类为会还款
-
假阴性(FP):实际还款的人,但被模型错误地分类为不会还款
-
真阴性(TN):实际上未还款的人,并且被模型正确地分类为不会还款
-
假阳性(FP):实际上未还款的人,但被模型错误地分类为会还款
现在假设,我们将 10000 人的信息输入到我们的模型中。我们得到的混淆矩阵如下:
从中我们可以推导出——
-
在 9500 名还款的人中——9000 人被正确分类(TP),500 人被错误分类(FN)
-
在 500 名没有还款的人中——200 人(TN)被正确分类,300 人(FP)被错误分类。
准确率
直观上,我们首先要问自己的是:我们的模型有多准确?
在我们的案例中,准确率是:
92%的准确率确实令人印象深刻,但需要注意的是,准确率通常是评估模型性能的一个简化指标。
如果我们更仔细地查看混淆矩阵,我们可以看到,虽然许多还款的个体被正确分类,但在 500 名未还款的个体中,只有 200 人被模型正确标记,其余 300 人被错误分类。
那么,让我们深入探讨一些其他常用的指标,以评估我们模型的表现。
精准度
我们可以问的另一个问题是:被预测为会还款的个体中,实际还款的百分比是多少?
计算精准度时,我们可以将真正例数除以预测为正例的总数(即,分类为会还款的个体)。
因此,当我们的分类器预测一个人会还款时,我们的分类器在 96.8%的情况下是正确的。
敏感性(也叫召回率)
接下来,我们可以问自己:我们模型正确识别的实际还款个体的百分比是多少?
计算敏感性时,我们可以取真正例数,并将其除以实际还款的总人数。
分类器正确标记了 94.7%实际还款的人,而其余的则错误标记为不会还款。
注意:精准度和敏感性公式中的术语有时可能会令人困惑。一个简单的记忆法是记住两个公式都使用 TP(真正例),但分母不同。精准度的分母是(TP + FP),而敏感性的分母是(TP + FN)。
为了记住这个区别,可以将 FP 中的“P”与精准度中的“P”联系起来:
这就剩下 FN,我们在敏感性的分母中找到它:
F1 Score
另一个结合了敏感性和精准度的有用指标是 F1 分数,它计算了精准度和敏感性的调和平均值。
在我们的案例中,F1 分数是:
通常,F1 分数提供了对模型性能更全面的评估。因此,F1 分数通常比准确率在实际中更有用。
特异性
另一个需要考虑的关键问题是特异性,它提出了这样一个问题:未偿还贷款的个体中有多少百分比被正确识别为不会偿还?
要计算特异性,我们将真正负例除以未偿还贷款的个体总数。
我们可以看到,我们的分类器仅正确识别了 40%的未偿还贷款的个体。
特异性与其他评估指标之间的明显差异强调了选择适当指标评估模型性能的重要性。考虑所有评估指标并进行适当解释是至关重要的,因为每个指标可能提供对模型有效性的不同视角。
注意:我经常发现结合各种指标或根据问题制定自己的指标是有帮助的
在我们的场景中,准确识别不会偿还贷款的个体更为关键,因为向这些个体提供贷款可能会带来相较于拒绝那些会偿还的个体更高的成本。因此,我们需要考虑改善性能的方法。
实现这一点的一种方法是调整分类的阈值。
虽然这样做可能看起来违反直觉,但对我们来说,重要的是正确识别那些不会偿还贷款的个体。因此,错误标记实际上会偿还贷款的人对我们来说并不是那么重要。
通过调整阈值,我们可以让模型对负类(不会偿还的人)更敏感,代价是对正类(会偿还的人)的敏感度下降。这可能会增加假阴性(将偿还的人分类为不会偿还),但可能减少假阳性(未能正确识别未偿还的人)。
直到现在,我们使用了 0.5 的阈值,但让我们尝试调整一下,看看我们的模型是否能表现得更好。
让我们从将阈值设置为 0 开始。
这意味着每个人都会被分类为将偿还(由蓝色表示):
这将导致以下混淆矩阵:
每个人都被分类为将偿还
…准确率为:
…敏感性和精确度:
…以及特异性:
当阈值=0 时,我们的分类器无法正确分类任何没有偿还贷款的个人,即使准确性和敏感度看起来可能令人印象深刻,它也是无效的。
让我们尝试 0.1 的阈值:
因此,任何信用分数低于 420 的人将被分类为不会偿还。这将导致如下的混淆矩阵和指标:
我们再次看到,除了特异性外,所有指标都非常出色。
接下来,让我们去到另一个极端,将阈值设置为 0.9:
因此,任何信用分数低于 760 的个人都将被标记为不会偿还。这将导致如下的混淆矩阵和指标:
在这里,我们看到指标几乎是翻转的。特异性和精准度很好,但准确性和敏感度很差。
你明白了。我们可以为更多的阈值(0.004, 0.3, 0.6, 0.875…)进行类似操作。但是这样会导致大量的混淆矩阵和指标,从而造成很多混淆。这绝对是有意为之。
ROC 曲线
这就是接收者操作特征(ROC)曲线的作用,用以消除这种混淆。
ROC 曲线总结并允许我们可视化分类器在所有可能阈值下的表现。
曲线的 y 轴是真正例率,即敏感度。x 轴是假正例率,即 1-特异性。
假阳性率告诉我们那些没有偿还却被错误分类为 将要偿还 (FP)的人的比例。
所以当阈值=0 时,从之前我们看到的混淆矩阵和指标是:
我们知道真正例率 = 敏感度 = 1
和 假正例率 = 1 — 特异性 = 1 — 0 = 1。
现在让我们将这些信息绘制在 ROC 曲线上:
这条虚线蓝色线显示了真正例率=假正例率的位置:
这条线上的任何点都意味着正确分类为偿还的人的比例与错误分类为未偿还的人的比例相同。
关键在于我们希望我们的阈值点尽可能远离左侧的线,并且我们不希望有任何点低于这条线。
现在当阈值=0.1 时:
在 ROC 曲线上绘制这个阈值:
由于新点 (0.84, 0.989) 位于蓝色虚线的左侧,我们知道偿还的正确分类人群比例大于未偿还的错误分类人群比例。
换句话说,新阈值比蓝色虚线上的第一个阈值更好。
现在让我们将阈值提高到 0.2。我们计算该阈值的真正例率和假阳性率,并绘制图表:
新点 (0.75, 0.98) 更远离蓝色虚线,显示新阈值比之前的更好。
现在我们继续使用其他几个阈值(=0.35, 0.5, 0.65, 0.7, 0.8, 1)重复相同的过程,直到阈值=1。
在阈值=1 时,我们处于点 (0, 0),其中真正例率 = 假负例率 = 0,因为分类器将所有点分类为不会还款。
现在无需排序所有混乱的矩阵和指标,我可以看到:
因为在紫色点处,当 TPR = 0.8 且 FPR = 0,
换句话说,这个阈值没有产生假阳性。而在蓝色点处,尽管 80%还款的人被正确分类,但未还款的人的正确分类率只有 80%(相比于之前阈值的 100%)。
现在如果我们连接所有这些点……
…我们最终得到 ROC 曲线。
AUC
现在假设我们想比较我们构建的两个不同的分类器。例如,第一个分类器是我们迄今为止看到的逻辑回归分类器,它产生了这个 ROC 曲线:
我们决定建立另一个决策树分类器,结果得到了这个 ROC 曲线:
比较两个分类器的一种方法是计算它们各自曲线下的面积或 AUC。
由于逻辑回归曲线的 AUC 值更大,我们得出结论它是一个更好的分类器。
总结一下,我们讨论了评估分类模型的常用指标。然而,选择指标是主观的,取决于对问题和业务需求的理解。使用这些指标的组合或创建更适合特定模型需求的新指标也可能是有用的。
向 StatQuest 致以巨大的感谢,我最喜欢的统计学和机器学习资源。欢迎在LinkedIn上与我联系,或发邮件至shreya.statistics@gmail.com。
机器学习图解:增量学习
原文:
towardsdatascience.com/machine-learning-illustrated-incremental-machine-learning-4d73747dc60c
模型如何随着时间学习新信息,同时保持和建立在之前的知识基础上
·发布于数据科学前沿 ·阅读时间 7 分钟·2023 年 9 月 15 日
--
欢迎回到《图解机器学习》系列。如果你阅读了系列中的其他文章,你会知道套路。我们将一个(听起来很枯燥)的机器学习概念通过图示化的方式变得有趣!这篇文章将介绍一个名为增量学习的概念,在这个概念中,机器学习模型会随着时间的推移学习新信息,同时保持和建立在之前的知识基础上。但在深入之前,让我们先讨论一下今天的模型构建过程是什么样的。
在构建模型时,我们通常遵循一种称为静态学习的过程。在这个过程中,我们使用最新可用的数据来训练模型。在训练过程中,我们会调整和优化模型。一旦对其性能感到满意,我们就会部署它。这个模型会在生产中使用一段时间。然后我们会注意到模型的性能随着时间的推移变得越来越差。这时,我们会丢弃现有的模型,并使用最新的数据重新构建一个新的模型。我们反复执行这个过程。
让我们用一个具体的例子来说明这一点。考虑以下假设场景。我们在 2023 年 1 月底开始构建一个欺诈检测模型。这个模型用于检测信用卡交易是否欺诈。我们使用过去一年(2022 年 1 月到 2022 年 12 月)的所有信用卡交易数据来训练我们的模型,并用本月(2023 年 1 月)的交易数据来测试模型。
到下个月底,我们发现模型在面对新数据时表现不佳。因此我们构建了另一个模型,但这次使用过去一年的数据(2022 年 2 月到 2023 年 1 月)进行训练,然后用当前月份的数据(2023 年 2 月)进行测试。所有超出这些训练和测试周期的数据都被丢弃。
下个月,我们再次注意到模型性能在面对新数据时表现不佳。于是,我们再次使用过去一年的数据构建了一个新模型。
每当我们发现模型性能下降时,我们会重复这个过程。这不一定是在 1 个月后。也可能是在 3 个月、6 个月甚至一年后。
那我们为什么要这样批量处理数据呢?
3 个主要原因。
-
概念漂移:随着时间的推移,我们会看到一种叫做概念漂移的现象,这意味着我们试图预测的内容会随时间变化,使用较旧的数据有时可能会适得其反。
-
内存限制:我们的训练集越大,占用的内存就越多。因此,我们尝试限制输入到模型中的数据量。
-
时间限制:与第二个原因类似,我们的训练数据越大,模型训练所需的时间就越长。(尽管这通常对我们构建的大多数模型来说不是一个大问题。可能会出现问题的地方是自然语言处理模型。)
但如果我们不想丢弃所有旧模型和数据呢? 丢弃旧模型意味着浪费了旧模型迄今为止积累的所有知识。理想情况下,我们希望找到一种方法来保留之前的知识,同时逐步添加来自新数据的信息。我们希望保留这种“制度性知识”,因为它对适应缓慢变化或重复出现的模式至关重要。
这正是增量学习所做的事情。
在增量学习中,模型逐步学习和增强其知识,而不会忘记以前获得的信息。它随着数据的增长而变得更加完善。
为了说明这一点,我们回到我们的欺诈模型示例。我们以与静态学习相同的方式开始。我们使用去年数据构建模型,但当下个月获得新数据时,我们不是从头开始构建新模型,而是将本月的新数据添加到现有模型中。然后我们在 2 个月、3 个月等时重复这个过程。所以这里我们实际上不是在构建新模型,而是在构建相同模型的新版本。
这可能是个好事,因为 a) 这是一种高效利用资源的方式,因为更少的数据存储 = 节省更多内存。每次迭代使用的内存更少,从而降低成本。 b) 这对动态数据很有用,现实世界中的大多数数据都是动态的,因为我们可以在获取新数据时持续更新预测,而不是每次都重新构建一个新模型。 c) 训练过程在更小的数据部分上进行,因此速度更快。
欺诈检测实际上是增量学习如何有益的一个很好的例子,以万事达卡的实时欺诈检测系统为例。每次交易时,万事达卡的系统会检查 100 多个变量(如交易大小、位置和商户类型)以评估欺诈的可能性。(来源:DataCamp)该系统使用增量学习来适应欺诈活动模式的变化。在像金融欺诈这样的动态环境中,欺诈者不断调整他们的方法,概念漂移的挑战非常显著。因此,我们的模型必须迅速适应,以保持其性能并有效打击欺诈者及其不断变化的策略。
好消息是——增量学习已经被内置到我们一些最喜欢的模型中,比如 XGBoost 和 CatBoost。实现起来非常简单!
我在之前的文章中解释了 XGBoost 和 CatBoost 背后的数学细节。
让我们在真实数据上测试静态学习和增量学习,以比较性能。
我们将使用这个信用卡欺诈数据集(CC0)来构建模型。在进行一些特征清理和选择后,我们得到了如下数据集:
其中is_fraud是我们的目标(y)列。
让我们从静态学习开始。我们构建了相同的 12 个 XGBoost 模型……
model = xgb.XGBClassifier(scale_pos_weight=10).fit(X_train, y_train)
…在接下来的 12 个一年期训练周期中:
在每次迭代或训练周期结束时,我们将训练日期提前一个月。然后,我们在接下来的一个月测试周期中测试每个模型,这些测试周期从其对应的训练周期结束时开始:
然后我们记录了 12 个 AUC 分数:
roc_auc_score(y_test, model.predict_proba(X_test)[:,1])
我们记录这些分数,以便与我们从增量模型中获得的分数进行比较。记住,我们的 AUC 分数越高,模型的表现越好。
现在我们进入增量学习模型。我们从在与静态学习相同的一年期间训练第一个模型开始。这是我们的基准模型。 但对于接下来的 11 个模型,我们逐步将新月份的数据输入到已有的模型中。
因此,我们每次都从上次结束的地方继续训练模型。
这个模型看起来基本与之前相同,只是有一个小变化。
model = xgb.XGBClassifier(scale_pos_weight=10).fit(X_train, y_train, xgb_model=model)
在这里,我们使用参数xgb_model
在当前构建的 xgboost 模型中声明之前的模型。通过保留之前训练的模型,训练过程变得更快、更高效,因为模型不需要每次都从头开始学习。
然后我们使用与静态模型相同的 12 个月测试期来测试 12 个模型…
…并记录 AUC 分数:
roc_auc_score(y_test, model.predict_proba(X_test)[:,1])
现在是有趣的部分,我们来比较这两种过程的性能。在记录的 11 个 AUC 分数中(因为第一个分数是相同的,因为我们使用了相同的训练和测试数据),增量学习在 11 次迭代中的 7 次中取得了更好的 AUC 分数!
尽管如此,仍然有一些警告:
-
增量学习面临过拟合的风险,因为它依赖于连续的数据流。风险在于,它可能会根据最近的数据过度调整其参数,这些数据可能无法准确代表整体分布。相比之下,状态学习可以考虑整个分布。
-
尽管增量学习可以处理不断变化的数据,但数据趋势的突变可能会带来挑战。因此,对于变化过于剧烈的数据,增量学习可能不适用。
-
增量学习面临一种现象,称为灾难性遗忘,即在学习新数据时旧知识会丧失,并且很难确定具体丢失了什么信息。
虽然有一些需要考虑的因素,但通过优化模型的每个版本,将这种方法集成到模型中是有益的。我们可以通过调整参数或改进特征选择来进一步提升结果。
以上就是关于增量学习的所有内容!一如既往,如果你有任何评论/问题/顾虑,请告诉我,也可以通过LinkedIn与我联系,或通过shreya.statistics@gmail.com给我发邮件。
除非另有说明,所有图片均由作者提供。
机器学习的可视化:用 SHAP 揭开黑箱模型的面纱
原文:
towardsdatascience.com/machine-learning-illustrated-opening-black-box-models-with-shap-9e92d0400680
如何使用 SHAP 解释和解读任何机器学习模型
·发表于 Towards Data Science ·10 分钟阅读·2023 年 5 月 8 日
--
Shapley Values 是经济学中合作博弈论的一个概念,它根据每个参与者对博弈的贡献为每个玩家分配一个价值。在机器学习领域,这一概念被适配成了 SHAP(SHapley Additive exPlanations)框架,这是一种有效的模型解释技术。
如果你有兴趣深入了解 Shapley Values,我强烈推荐阅读我之前的文章以了解 Shapley Values 背后的数学和直觉。尽管它已被修改以适应机器学习的需求,但理解其基本原理仍然是有帮助的。
Shapley Values 是博弈论中广泛使用的概念,它提供了一种公平的方式来分配总收益。
SHAP 框架类似于 Shapley 值,因为它计算了特征在一个游戏(即机器学习模型)中的单独影响。然而,机器学习模型是非合作性的游戏,这意味着特征之间不一定像在合作游戏中那样相互作用。相反,每个特征独立地贡献于模型的输出。虽然 Shapley 值公式可以使用,但由于参与者和联盟的数量众多,它可能在计算上非常昂贵且不准确。为了解决这个问题,研究人员开发了替代方法,如蒙特卡洛方法和基于核的方法。在本文中,我们将深入探讨蒙特卡洛方法。
让我们用一个例子来设置。假设我们有一个数据集,包含 20,640 栋加州房屋的价格。
from sklearn.datasets import fetch_california_housing
import pandas as pd
# load dataset
housing = fetch_california_housing()
X, y = housing.data, housing.target
X = pd.DataFrame(X, columns=housing.feature_names)
# feature dataset
X = X.drop(['Population', 'AveBedrms', 'AveOccup'], axis=1)
我们将使用特征MedInc, HouseAge, AveRooms, Latitude, 和 Longitude(X)……
…预测房价(y)。注意:房价以$100,000 为单位表示。
我们将建立一个模型。这可以是任何模型,但我们将使用 XGBoost(阅读我的上一篇文章以了解 XGBoost 背后的数学)。按照标准步骤——将数据分为训练集和测试集,并用训练集训练模型。
# split data into train and test
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0)
# train an XGBoost model
from xgboost import XGBRegressor
model = xgb.XGBRegressor(objective='reg:squarederror', random_state=4)
model.fit(X_train, y_train)
然后使用这个模型对测试集进行预测。
y_pred = model.predict(X_test)
让我们进一步分解。我们想要测试集中第一个样本(或房子)的预测房价。所以对于第一个样本,这些特征值是……
X_test.iloc[0,]
MedInc 4.151800
HouseAge 22.000000
AveRooms 5.663073
Latitude 32.580000
Longitude -117.050000
…预测的房价是:
y_pred[0]
> 1.596
XGBoost 做出的预测显得神秘,因为它是一个黑箱模型,所以我们看不到箱子内部发生了什么。为了更深入地了解预测值是如何得出的,我们需要理解两点:
-
特征对预测房价的影响是正面还是负面?(例如,更高的MedInc值是否导致预测房价的增加?)
-
这些影响的程度是多少?(例如,MedInc对预测房价的影响是否比AveRooms更大?)
为了回答这些问题并确定每个特征对 1.596 这一房价预测的贡献,我们可以依赖特征的 SHAP 值。
让我们通过以下步骤来计算对 1.596 这一预测的MedInc 的 SHAP 值:
第 0 步:计算预期的预测房价
预期的预测房价不过是所有预测值的平均值。即:
y_pred.mean()
> 2.07
所以,2.07 作为预期的预测房价。我们现在尝试弄清楚为什么预测值 1.596 与预期的 2.07 有偏差。0.476(2.07–1.596)的差异来自哪里?
特征的 SHAP 值衡量了该特征对模型预测值偏离/接近期望预测值的贡献程度。
步骤 1:获取特征的随机排列
我们从这个特征的排列开始…
…在随机重排后,我们得到这个:
我们在这里关注MedInc,所以让我们突出显示MedInc以及其右边的任何特征。
步骤 2:从数据集中选择一个随机样本
我们回到我们的测试数据集,从中选择一个随机样本:
步骤 3:形成两个新样本
这些样本将部分来源于原始样本…
…以及我们在步骤 2 中刚刚提取的新样本:
我们创建的第一个样本,称为 x1,将拥有原始样本中的所有相同值,除了MedInc右边的特征。对于MedInc右边的特征,我们从新样本中获取。
我们创建的第二个样本,称为 x2,将拥有原始样本中的所有值,除了MedInc右边的特征和 MedInc。
从中我们可以看到,这些样本完全一样,除了一个关键的地方——它们在MedInc值上有所不同,这是我们关注的特征。
步骤 4:使用新样本进行预测,并找出预测值的差异
现在我们将这些样本输入到模型中,并获取它们的预测值。
我们找到这些值之间的差异:
由于 x1 和 x2 之间的唯一区别是MedInc的值,这个区别表示当MedInc的值发生变化时预测的变化。
步骤 5:重复步骤 1–4 几次,然后计算差异的平均值
我们现在要做的就是重复上述过程,并计算步骤 4 中找到的所有差异值的平均值。
假设我们将这个过程重复 1000 次(这个数字可能会根据模型和数据集的复杂性而有所不同)。在对结果取平均后,我们得到了一个值为0.22。
这个 0.22 表示MedInc在第一个样本中的 SHAP 值,这意味着MedInc对将期望预测值 2.07 调整到我们预测值 1.596 的贡献为+0.22。
我们可以对其他特征——HouseAge, AveRooms, Latitude, 和 Longitude 应用相同的过程,以找到它们的 SHAP 值。
现在我们了解了 SHAP 的基本计算,我们可以通过可视化来应用它。为了可视化它们,我们将使用 Python 的shap库中的Explainer并输入我们的模型。
import shap
explainer = shap.Explainer(model)
shap_values = explainer(X_test)
这将为测试集中每个样本的所有特征(MedInc、房龄、平均房间数、纬度和经度)提供 SHAP 值。利用这些 SHAP 值,我们开始绘图。
1. 瀑布图
这个图帮助我们逐个可视化数据中每个样本的 SHAP 值。让我们可视化第一个样本的 SHAP 值。
# visualize the SHAP values of the first sample
shap.plots.waterfall(shap_values[0])
请注意,预期的预测值 E[f(X)] = 2.07,这是第 0 步计算的值,而第一个房子的预测房价 f(X) = 1.596 是我们对第一个样本的预测。
注意到MedInc的 SHAP 值为+0.22(我们在第 5 步中看到),经度的 SHAP 值为-2.35,等等。如果我们将所有上述 SHAP 值加到 2.07 中,并从中减去,我们得到第一个房子的预测值 1.596。
忽略符号,经度的 SHAP 值的大小为 2.35,大于其他特征的值。这意味着经度对这一特定预测的影响最大。
就像我们可视化第一个样本的 SHAP 值一样,我们也可以可视化第二个样本的 SHAP 值。
# visualize the SHAP values of the second sample
shap.plots.waterfall(shap_values[0])
比较我们测试数据中第一个和第二个房子的 SHAP 值,我们观察到显著差异。在第一个房子中,经度对预测价格的影响最大,而在第二个房子中,MedInc的影响最为显著。
注意:这些 SHAP 值的差异突出了每个特征对模型输出的独特贡献。理解这些贡献对于建立对模型决策过程的信任至关重要,并确保模型不会存在偏见或歧视。
2. 力图
另一种可视化上述内容的方法是力图。
shap.plots.force(shap_values[0])
3. 平均 SHAP 图
要确定哪些特征通常对我们模型的预测最重要,我们可以使用所有观测值的平均 SHAP 值的条形图。取绝对值的平均值可以确保正负值不会相互抵消。
shap.plots.bar(shap_values)
每个特征都有一个对应的条形,高度表示平均 SHAP 值。例如,在我们的图中,平均 SHAP 值最大的特征是纬度,这表明它对我们模型的预测影响最大。这些信息可以帮助我们了解哪些特征对模型的决策过程至关重要。
4. 蜂群图
蜜蜂图是一种有用的可视化工具,用于检查每个特征的所有 SHAP 值。y 轴按特征将 SHAP 值分组,点的颜色表示相应的特征值。通常,较红的点代表较高的特征值。
蜜蜂图可以帮助识别特征与模型预测之间的重要关系。在这个图中,特征按其平均 SHAP 值排序。
shap.plots.beeswarm(shap_values)
通过检查蜜蜂图中的 SHAP 值,我们可以开始理解特征与预测房价之间关系的性质。例如,对于MedInc,我们观察到 SHAP 值随着特征值的增加而增加。这表明较高的MedInc值有助于提高预测的房价。
相比之下,对于Latitude和Longitude,我们注意到相反的趋势,即更高的特征值会导致较低的 SHAP 值。这一观察表明较高的Latitude和Longitude值与较低的预测房价相关联。
5. 依赖图
为了更深入地理解各个特征与其相应的 SHAP 值之间的关系,我们可以创建依赖图。依赖图是一个散点图,展示了单个特征的 SHAP 值与特征值之间的关系。
shap.plots.scatter(shap_values[:,"MedInc"])
shap.plots.scatter(shap_values[:,"Latitude"])
通过分析依赖图,我们可以确认在蜜蜂图中所做的观察。例如,当我们为MedInc创建一个依赖图时,我们观察到MedInc值与 SHAP 值之间存在正相关关系。换句话说,更高的MedInc值会导致预测的房价更高。
总体而言,依赖图提供了对单个特征与预测房价之间复杂关系的更详细理解。
总之,SHAP 值让我们窥探了每个特征如何贡献于模型输出。这些见解可以通过可视化进一步增强。我们可以利用 SHAP 做出更明智的特征选择、模型改进决策,并最终在机器学习应用中实现更好的性能。
一如既往,感谢你让我参与到你的机器学习旅程中。你可以通过邮件联系我,邮箱是shreya.statistics@gmail.com,或者在LinkedIn上与我联系,提出你的意见、问题和建议!
非欧几里得空间中的机器学习
原文:
towardsdatascience.com/machine-learning-in-a-non-euclidean-space-99b0a776e92e
图片来源:Greg Rosenke 在 Unsplash
第一章:为什么你应该学习非欧几里得机器学习
·发表于 Towards Data Science ·阅读时间 10 分钟·2023 年 6 月 16 日
--
“我们的舒适和熟悉的欧几里得空间及其线性结构是否总是适合机器学习?近期研究对此提出了不同的看法:并非总是必要,有时甚至有害,这一点在一系列令人兴奋的工作中得到了证明。自从两年前提出双曲表示法用于层次数据以来,取得了重大进展,产生了非欧几里得空间表示的新思想、新算法和模型,以及对非欧几里得机器学习基本功能的新视角。” 作者 Fred Sala、 Ines Chami、 Adva Wolf、 Albert Gu、 Beliz Gunel 和 Chris Ré, 2019
你将在本文中学到什么。
-
扭曲度量了在将数据表示为另一个空间时距离保留的效果。
-
对于某些数据,欧几里得空间会产生较高的扭曲,因此使用像球面或双曲空间这样的非欧几里得空间。
-
黎曼几何工具如流形和黎曼度量被用于非欧几里得机器学习。
-
流形是局部上欧几里得的曲面。
-
指数映射和对数映射用于从流形到其切空间的转换。
-
黎曼度量允许在流形上计算最短距离。
在继续阅读本系列关于应用于机器学习(ML)的非欧几里得几何之前,我必须回答一个重要的问题。是否值得深入了解非欧几里得 ML?
为了回答这个问题,我开始研究非欧几里得 ML。我很快找到了一些资源,第一个是来自斯坦福的,以上引用就是摘自其中。作者认为,机器学习的设计采用了某种几何,即欧几里得几何,这更多是传统或便利的选择,而非理性思考的结果。
到目前为止,选择欧几里得几何似乎并不是一个大问题。但作者通过引用 Bronstein 等人在其开创性几何深度学习描述中引起了我们的注意。
“[m]许多科学领域研究具有非欧几里得空间基础结构的数据。” Bronstein et al.
当我继续阅读文章时,我遇到了一个我不熟悉的方面:空间平坦性的概念。
“我们选择了使用欧几里得空间,其固有的属性中最关键的之一就是平坦性。” Fred Sala, Ines Chami, Adva Wolf, Albert Gu, Beliz Gunel 和 Chris Ré, 2019
斯坦福文章的作者提到了平坦性的影响。以下是提出的三点,你应该阅读我们的系列文章以获得更多直观的理解:
-
更好的表示 — 他们认为欧几里得空间不适合某些数据集,例如可以用树描述的层次数据集。
-
释放模型的全部潜力 — 他们认为,为了推动模型性能的界限,我们可以通过将数据所在的空间从欧几里得几何转变为非欧几里得几何来改进。
-
更灵活的操作 — 他们认为非欧几里得空间中的操作更加灵活,所需维度更少。作者在文章中后面解释了这一点。我们将尽量在我们的 Medium 系列中简化这一点。
将非平坦实体表示到平面空间中
选择合适的几何非常重要,依赖于输入数据。下面,我们展示了一个非欧几里得数据的例子,这些数据被“强迫”适应于二维欧几里得空间。这是我们众所周知的球形地球被压缩成平面。然而,这种转换伴随着不可忽视的扭曲。所谓扭曲是指从原始空间[地球 — 球体]到数据表示的空间[地图 — 平面],距离没有被保留。
例如,下图中的墨西哥在实际情况中几乎与格林兰(右)具有相同的表面,但在实际投影中(左)看起来要小得多。
资源: 作者提供。请注意,世界地图(左)使用的是我们球面星球的墨卡托投影。墨卡托地图由公式 (x, y) = λ, log tan(π/4 + φ/2) 定义。改编自 维基百科.
表示地球的方式有很多种,这些方式都涉及一定程度的扭曲。
资源: 全球投影,带有扭曲。来自作者,改编自维基百科。
例如,在著名的墨卡托投影中,自然观察到扭曲。格林兰问题展示了从球面表示转到平面表示时信息的丢失。这种投影不是面积保持的,这是在这种情况下期望的核心属性。实际上,格林兰的面积约为 220 万平方公里,比南美洲(面积约 1780 万平方公里)看起来要大。这种墨卡托投影保持角度但不保持面积,因此使其不完美。
现在,其他数据集也被迫位于欧几里得空间中,我们观察到扭曲。这是图形的情况:在欧几里得空间中,我们不能在低扭曲或信息丢失的情况下嵌入大类图形。
扭曲有几个更严格的数学定义。从本质上讲,我们希望扭曲能够通过评估距离保持的好坏来衡量嵌入的质量。这里,我们定义如下:
扭曲 ~ AVG
例子。
在下图中,我们可以通过庞加莱型不等式证明,我们不能将两个循环(方形,圆形)嵌入到欧几里得空间中而不扭曲。注意,扭曲为 1 是完美的扭曲——图形距离完全匹配嵌入空间距离。任何与 1 不同的扭曲意味着我们没有保持图形距离。
资源: 作者提供。长度为 4 的循环的最佳嵌入[左],以及 3-star K(1,3)的最佳嵌入[右]。改编自 Octavian Ganea 在苏黎世联邦理工学院的讲座。
在上面的方形中,对角线上的两个对立节点在图形距离上有 2 的距离。然而,欧几里得嵌入中的最短路径距离为√2。
这个扭曲的概念非常重要,因为欧几里得几何不允许对图形数据进行理想的“投影”。特别地,对于层次图形数据,为了最小化扭曲,一种解决方案是使用双曲空间。
注意。我们将在下一章中深入了解这个非欧几里得空间的例子。
在非欧几里得空间中表示数据
很难理解我们如何用除了Rn以外的方式表示数据。而且,我们如何摆脱我们非常熟悉的欧几里得距离来比较两个向量表示?
一种解决方案由黎曼几何中的流形描述。流形是看起来像Rn 但仅在局部。 这意味着我们可以在局部使用向量来表示我们的数据点。但仅在局部!
资源:在流形 M [深灰色] 上的一个点 x 处的切空间 [浅灰色,TxM] 和它的切向量 v。流形中的向量 x 可以在欧几里得切空间中局部表示。来自 维基百科
相似性或距离的概念在机器学习中至关重要。如果我们在构建一个 NLP 模型,我们希望在表示文本输入的嵌入空间中保留语义上的相似性。换句话说,我们希望语义相似的两个词在欧几里得空间中也相似,即欧几里得距离低。同样,语义不相似的两个词在欧几里得空间中应当距离较远,即欧几里得距离高。
因此,当逃避欧几里得几何时,需要有一个等效的方法。这种方法由黎曼度量来描述。黎曼度量使我们能够在非欧几里得空间中比较两个实体,并保留这种直观的距离概念。
👀 我记得了。
现在,我们需要记住,在这个非欧几里得框架中,我们可以对数据表示进行局部操作,并且我们有一个度量来测量距离。因此,我们具备了在非欧几里得空间中进行机器学习的能力。
🙌🏻 为什么我要在非欧几里得空间中学习更多关于机器学习的知识?
到目前为止,我们知道没有天才欧几里得的机器学习实际上是有意义的。确实存在一些项目,它们用不同的几何框架来解决我们传统的机器学习问题。
现在,一个自然的问题出现了:值得花时间了解这个领域的存在吗?
这是一个相当令人畏惧的空间,涉及复杂的数学。但我的朋友 Aniss Medbouhi,KTH 的 ML 博士研究员,将帮助我们克服这个空间固有的复杂性。
我之所以对这个空间不太信服的另一个原因是,我读到它主要适用于可以用树描述的层次数据。乍一看,这似乎与我每天处理的数据无关。
然而,下面的摘要给了我们一些相关数据集的概念:
“然而,最近的研究表明,嵌入复杂网络的适当等距空间不是平坦的欧几里得空间,而是负曲率的双曲空间。我们提出了一个新概念,利用这些最新的见解,建议在双曲空间中学习图的神经嵌入。我们提供了实验证据,表明在其自然几何中嵌入图显著改善了在多个真实世界公共数据集上的下游任务表现。” Chamberlain 等
“然而,虽然复杂的符号数据集通常表现出潜在的层级结构,最先进的方法通常在欧几里得向量空间中学习嵌入,而这并未考虑到这一特性。为此,我们引入了一种新的方法,通过将其嵌入双曲空间——或更准确地说,嵌入到 n 维庞加莱球体中——来学习符号数据的层级表示。” Nickel 和 Kiela
上述数据集列举如下,由Chamberlain 等提供:
(1) Karate: 扎卡里(Zachary)的空手道俱乐部包含 34 个顶点,分为两个派系。[4]
(2) Polbooks: 关于美国政治的书籍网络,这些书籍是在 2004 年总统选举前后出版,并由在线书商 Amazon.com 出售。书籍之间的边表示相同买家的频繁共同购买。
(3) Football: 2000 年秋季常规赛期间,Division IA 大学之间的美国橄榄球比赛网络。[2]
(4) Adjnoun: 查尔斯·狄更斯(Charles Dickens)小说《大卫·科波菲尔(David Copperfield)》中常见形容词和名词的邻接网络。[3]
(5) Polblogs: 记录于 2005 年的美国政治博客之间的超链接网络。[1]
此外,在生物学中,我们找到以下参考数据集:
- 生物学:诸如蛋白质等进化数据。[5]
资源:扎卡里的空手道俱乐部中 34 个人的社会关系网络表示。根据事件[4],该人群被分为两个部分。改编自维基百科。
最后,NLP 数据,即文本数据,是另一种类型的层级数据。因此,许多领域可能从理解非欧几里得机器学习的进展中受益。
现在我们知道如何更好地表示某些数据集,将其与机器学习联系起来是关键。任何下游机器学习任务都需要首先摄取数据。大量时间花在清理基础数据和准确表示数据上。数据表示的质量至关重要,因为它直接影响模型的性能。例如,在自然语言处理(NLP)中,我建议我的学生专注于提供良好嵌入的架构,例如上下文嵌入。关于改进嵌入的研究非常广泛,从浅层神经网络(fasttext, word2vec)到深层神经网络和变换器(sentence-transformers, BERT, RoBERTa, XLM)。然而,也值得注意的是,数据表示与手头的任务紧密相关,研究显示对于某些任务,某些浅层神经网络比深层神经网络提供更好的结果。
结论
在本文中,我们看到可以利用非欧几里得几何来解决特定于球面数据和层次数据集(如图)的现有问题。将这些数据集嵌入到欧几里得空间中,代价是扭曲,这种扭曲无法保留从原始空间到嵌入空间的距离。这种扭曲在我们地球的表示中很直观,我们有多种方式表示我们的地球,其中一些方式无法保留期望的核心属性,例如面积保持。对于图来说,核心属性需要被保留,扭曲基础空间可能导致下游机器学习任务的性能较差。
在下一章中,我们将学习更多关于球面几何和双曲几何的知识。我们将更多关注后者,并提供如何在这样的空间中更好地嵌入层次数据的直观理解。
与贡献者联系。
KTH 皇家技术学院的机器学习博士研究员。
Linkedin. www.linkedin.com/in/aniss-medbouhi/
微软的数据科学家和 EPITA 巴黎的讲师。
Linkedin. www.linkedin.com/in/mastafa-foufa/
[1] Lada A. Adamic 和 Natalie Glance. 政治博客圈和 2004 年美国选举。《第三届国际链接发现研讨会会议录》——LinkKDD ’05, 页 36–43, 2005。
[2] Michelle Girvan 和 Mark E. J. Newman. 社会和生物网络中的社区结构。在《国家科学院学报》会议录中,99:7821–7826, 2002。
[3] Mark E. J. Newman. 使用矩阵特征向量发现网络中的社区结构。《物理评论 E》——统计学、非线性和软物质物理,74(3):1–19, 2006。
[4] Wayne W. Zachary. 小组中的冲突和分裂的信息流模型。《人类学研究杂志》,33:452–473, 1977。
[5] AlQuraishi, Mohammed. “ProteinNet: 一个标准化的蛋白质结构机器学习数据集。” BMC 生物信息学 20.1 (2019): 1–10.
三步掌握机器学习:如何高效学习
原文:
towardsdatascience.com/machine-learning-in-three-steps-how-to-efficiently-learn-it-aefcf423a9e1
优先考虑预测建模的核心要点,避免让自己不堪重负
·发表于 Towards Data Science ·阅读时间 21 分钟·2023 年 3 月 3 日
--
我观察到两种极端的方法,数据科学家在学习机器学习算法时通常会采用其中一种。第一种方法涉及学习所有的细节,并从头开始实现算法,以获得真正的掌握。另一方面,第二种方法则认为计算机会“自动学习”,从而使个人学习算法变得不必要。这使得一些人仅仅依赖于如 lazypredict 这样的工具。
在学习机器学习算法时,采取两种极端方法之间的折中方式是现实的。然而,问题依然是,从哪里开始呢?在这篇文章中,我将把机器学习算法分为三类,并提供我个人的意见,告诉你应该从什么开始,以及哪些可以跳过。
机器学习算法的复杂性
初学机器学习可能会因为众多可用的算法而感到不知所措。线性回归、支持向量机(SVM)、梯度下降、梯度提升、决策树、LASSO、岭回归、网格搜索等等,这些都是在提问时会想到的算法。
在监督学习领域,这些算法服务于不同的目的,并具有不同的目标。本文将仅讨论监督学习。
为了更好地理解各种技术,按其目标和复杂性级别进行分类可能会有所帮助。通过将这些算法组织成不同的类别和复杂性级别,可以简化这些概念,使其更易于理解。这种方法可以大大提升对机器学习的理解,并帮助识别适合特定任务或目标的最佳技术。
当学生深入机器学习领域时,可能会因其复杂性而感到沮丧。然而,在实践之前并不需要学习或熟悉所有的算法。机器学习领域的不同职位可能需要不同水平的熟练度,缺乏某些方面的知识也是可以接受的。例如,数据科学家、数据分析师、数据工程师和机器学习研究人员对其工作角色的要求各不相同。
对整体过程有广泛的理解可以使机器学习从业者在时间紧迫的情况下跳过某些技术细节,同时仍能全面理解过程。
照片由 Julio Wolf 提供,见Unsplash
1. 机器学习算法的细分
1.1 模型、训练和调优
“机器学习算法”的范围相当广泛,可以分为三种主要类型的算法:
-
(1) 机器学习模型,旨在接收输入数据并随后生成预测,如线性回归、SVM、决策树、KNN 等。在 scikit-learn 库中,这些也被称为“估计器”。
-
(2) 模型训练/拟合算法,用于创建或优化模型,也即为特定数据集寻找模型的参数。不同的机器学习模型有其特定的训练算法。虽然梯度下降是训练基于数学函数模型的最著名方法,但其他机器学习模型可以使用不同的技术进行训练,我们将在本文后续部分深入探讨这些技术。
-
(3) 超参数调优,包括寻找机器学习模型的最佳超参数。与训练过程不同,超参数调优过程通常不依赖于机器学习模型。网格搜索是这个任务中一种流行且常用的方法,尽管还有其他替代方法,我们将在本文后续部分深入探讨。
1.2 机器学习模型的三种类别
第一个方面涉及能够接收数据并根据这些数据生成预测的模型。这些模型可以分为三类:
-
基于距离的模型,包括 K 近邻、线性判别分析和二次判别分析。
-
基于决策树的模型,如单棵决策树(用于分类或回归)、随机森林和梯度提升决策树。
-
基于数学函数的模型,也称为参数模型,是假设输入和输出之间存在特定函数形式的模型。它们可以进一步分为线性模型,如 OLS 回归、SVM(具有线性内核)、岭回归和 LASSO,以及非线性模型,如具有非线性内核的 SVM 和神经网络。
1.3 元模型和集成方法
在机器学习中,元模型是一个结合多个单独模型预测以进行更准确预测的模型。它也被称为“堆叠模型”或“超级学习器”。组成元模型的单独模型可以是不同类型的或使用不同算法的,它们的预测通过加权平均或其他技术进行组合。
元模型的目标是通过减少单个模型中可能存在的方差和偏差,提高预测的整体准确性和鲁棒性。它还可以通过捕捉数据中的复杂模式来帮助克服单个模型的局限性。
创建元模型的一个常见方法是使用集成方法,如自助采样、提升或堆叠。
-
自助采样(Bagging),或称自助聚合,是一种在机器学习中用于通过组合在数据集的不同样本上训练的多个模型来减少模型方差的技术。自助采样的想法是生成多个模型,每个模型使用数据的一个子集,然后将它们组合起来,创建一个更强健且不易过拟合的模型。
-
提升:提升是另一种集成方法,它结合多个弱模型来创建一个强模型。与自助采样不同,提升依次训练模型。每个新模型在由先前模型错误分类的数据上进行训练,最终预测通过聚合所有模型的预测来得出。
-
堆叠:堆叠,或称堆叠泛化,是一种元模型集成方法,涉及训练多个基础模型,并使用它们的预测作为更高级模型的输入。高级模型学习如何组合基础模型的预测以进行最终预测。
-
随机森林:它是对自助采样(bagging)的扩展,增加了一层额外的随机性。除了随机抽样数据外,随机森林还会随机选择每次分裂时的特征子集。这有助于减少过拟合,并增加集成中模型的多样性。
集成方法最常应用于决策树,而不是像线性回归这样的线性模型。这是因为决策树比线性模型更容易过拟合,而集成方法通过结合多个模型来帮助减少过拟合。
决策树具有高方差和低偏差,这意味着它们容易对训练数据进行过拟合,从而导致在新数据上的表现较差。集成方法通过聚合多个决策树的预测来解决这个问题,从而得到一个更强健和准确的模型。
另一方面,线性模型如线性回归具有低方差和高偏差,这意味着它们不容易过拟合,但可能会欠拟合训练数据。由于这些模型已经具有低方差,因此集成方法对线性模型的效果不如对决策树的效果明显,因为集成方法在这方面的收益不大。
然而,仍然有一些情况可以将集成方法应用于线性模型。例如,袋装技术中使用的自助聚合技术可以应用于任何类型的模型,包括线性回归。在这种情况下,袋装算法会对训练数据进行采样,并在自助样本上拟合多个线性回归模型,从而得到一个更稳定且更强健的模型。然而,值得注意的是,得到的模型仍然是线性回归模型,而不是一个元模型。
总体而言,尽管集成方法最常用于决策树,但在某些情况下它们也可以与线性模型一起使用。然而,重要的是要记住每种模型的优缺点,并选择适合当前问题的方法。
1.4 机器学习算法概述
下图提供了分类为 3 类的各种机器学习算法的总结。本文的后续部分将更深入地探讨每一类。
机器学习算法概述 — 图片由作者提供
2. 机器学习模型
在这一部分,我们将更详细地观察三种机器学习模型的家族。以下是有关的更详细的计划。
(1) 基于距离的模型
-
基于实例的模型:KNN
-
贝叶斯分类器:LDA,QDA
(2) 基于决策树的模型
(3) 基于数学函数的模型
-
线性模型
-
核方法模型,如核 SVM 或核岭回归
-
神经网络
-
深度学习模型
2.1 基于实例的模型
第一类机器学习模型是基于距离的模型。这些模型使用数据点之间的距离来进行预测。
最简单且最具代表性的模型是 K-最近邻(KNN)。它计算新数据点与数据集中所有现有数据点之间的距离。然后选择 K 个最近邻,并将新数据点分配给 K 个邻居中最常见的类别。
在考察 k-最近邻(KNN)算法时,可以注意到在训练阶段没有显式构建模型。在 KNN 中,新的观察值的预测是通过在训练集中找到 k 个最近邻居,并计算它们目标值的平均值或多数投票来完成的。
与其他在训练过程中拟合模型的算法不同,KNN 存储整个训练数据集,只计算新观察值与现有数据集之间的距离来进行预测。因此,KNN 可以被视为一种“懒惰学习”算法,因为它在训练阶段不会主动构建模型,而是将决策过程推迟到推理时。
因此,推理/测试阶段可能会很慢。需要注意的是,可以使用更高效的算法,如 k-d 树。
2.2 贝叶斯分类器
线性判别分析(LDA)和二次判别分析(QDA)是基于距离的模型,它们使用马氏距离进行预测。马氏距离是一种测量点与分布之间距离的方法,考虑了变量之间的相关性。
线性判别分析(LDA)假设不同类别的方差相同,而二次判别分析(QDA)假设每个类别的方差不同。这意味着 LDA 假设所有类别的协方差矩阵相同,而 QDA 允许每个类别拥有自己的协方差矩阵。
2.3 基于决策树的模型
第二类机器学习模型是基于决策树的模型。它们也可以称为基于规则的模型,这意味着模型生成一组规则,用于解释它如何得出决策或预测。
决策树的每个分支代表一个规则或条件,用于确定接下来跟随的数据子集。这些规则通常以简单的 if-then 语句的形式存在,例如“如果变量 X 的值大于 5,则跟随左分支,否则跟随右分支。”
决策树的最终叶节点表示基于输入变量的值和导致该预测的规则,目标变量的预测类别或值。
决策树的一个优点是它们易于解释和理解,因为规则可以以清晰直观的方式进行可视化和解释。这使得它们在向非技术利益相关者解释预测或决策背后的原因时非常有用。
然而,决策树也容易过拟合,这发生在模型变得过于复杂并过于紧密地拟合训练数据,从而对新数据的泛化能力差。为了解决这个问题,通常将集成方法应用于决策树。
2.4 基于数学函数的模型
第三类机器学习模型是基于数学函数的模型。这些模型使用数学函数来建模输入变量和目标变量之间的关系。线性模型,例如普通最小二乘(OLS)回归和具有线性核的支持向量机(SVM)、岭回归和 LASSO,假设输入变量和目标变量之间的关系是线性的。非线性模型,例如具有非线性核的 SVM 和神经网络,可以建模输入变量和目标变量之间更复杂的关系。
对于基于数学函数的模型,例如线性回归或逻辑回归,我们必须定义损失函数。损失函数衡量模型的预测与实际数据的匹配程度。目标是通过调整模型的参数来最小化损失函数。
相对而言,对于基于非数学函数的模型,例如 KNN 或决策树,我们不需要定义损失函数。相反,我们使用不同的方法,比如在 KNN 的情况下寻找最近邻,或者在决策树的情况下根据特征值递归地划分数据。
在基于数学函数的模型中,定义适当的损失函数至关重要,因为它决定了模型解决的优化问题。根据具体问题的不同,可以使用不同的损失函数,例如回归问题的均方误差或二分类问题的交叉熵。
值得注意的是,所有线性模型,例如普通最小二乘(OLS)、LASSO、岭回归和具有线性核的支持向量机(SVM),都可以写成线性方程 y = wX + b 的形式。然而,这些模型之间的区别在于用于估计模型参数 w 和 b 的最优值的成本函数。
因此,虽然所有这些模型可以写成相同数学函数的形式,但重要的是要注意成本函数的选择决定了模型的行为和性能。因此,更准确地说,这些模型应该被视为具有不同成本函数的不同模型,而不是相同模型的不同成本函数。
非线性模型是解决复杂机器学习问题的强大工具,而这些问题无法通过线性模型得到充分解决。在实践中,主要有两种方法:核技巧和神经网络。
核技巧是一种有效地实现特征映射的方法,而无需显式计算变换后的特征。相反,它涉及定义一个核函数,用于计算变换特征空间中输入样本对之间的相似性。通过使用核函数,我们可以隐式地将输入数据映射到高维空间,在那里数据可以更容易地被分离和建模。
从这个意义上说,卷积部分可以被看作是一种特征工程,其中模型能够创建出更适合当前任务的新特征。这与传统的特征工程形成对比,后者是人工专家基于领域知识和直觉手动创建新特征。
创建非线性模型的另一种方法是通过使用神经网络。它们由一层层互联的节点或“神经元”组成,每个节点对其输入执行简单的数学操作,并将结果传递给下一层。
神经网络强大的关键在于它们学习输入和输出之间复杂的非线性关系的能力。这是通过在训练过程中调整神经元之间连接的权重来实现的,基于预测输出和实际输出之间的误差。
2.5 深度学习模型
深度学习专注于通过多层次的层次结构学习数据表示。由于在计算机视觉、自然语言处理和语音识别等广泛应用中的成功,它近年来变得越来越受欢迎。虽然深度学习模型由于其大量的参数和层次结构可以被认为是复杂的,但确实深度学习的一部分也涉及特征工程。
深度学习模型的一个例子是卷积神经网络(CNN)。在其核心,CNN 将一系列滤波器应用于输入图像,每个滤波器寻找特定的特征,如边缘或角落。网络的后续层则利用这些提取的特征来对输入图像进行分类。
从这个角度来看,像 CNN 这样的深度学习模型可以被认为是特征工程和可训练模型的结合。模型的特征工程方面涉及设计网络的架构以从输入数据中提取有用的特征,而可训练模型则涉及优化网络的参数以适应数据并进行准确的预测。
3. 模型训练/拟合
训练机器学习模型的过程是通过向模型展示一组标记示例来教会模型进行预测或决策。这些标记示例,也称为训练数据,由输入特征和输出标签的对组成。
在训练过程中,机器学习模型学习识别输入特征及其对应输出标签中的模式。模型使用特定的算法从训练数据中学习,并调整其内部参数,以提高预测或分类新数据的能力。
一旦模型在标记示例上训练完成,它就可以用于对新的、未见过的数据进行预测或决策。这个过程称为推断或测试。
不同的机器学习模型有不同的训练算法。以下是不同机器学习模型使用的一些训练算法的例子。
3.1 基于距离的模型训练
K-最近邻(KNN):KNN 是一种非参数算法,不需要显式的训练。相反,它存储整个训练数据集,并通过基于某种距离度量找到训练数据集中 K 个最接近的实例来预测新实例的标签。然后,预测基于 K 个最近邻的多数投票。
线性判别分析(LDA)是一种用于分类任务的监督学习算法。LDA 建模每个类别的输入特征分布,并利用这些信息找到一个线性组合的输入特征,最大化类别之间的分离。生成的线性判别函数随后可以用来对新实例进行分类。
LDA 的训练过程涉及估计每个类别的输入特征的均值和协方差矩阵。这些估计值然后用来计算类内散布矩阵和类间散布矩阵,这些矩阵用于推导线性判别函数。线性判别函数的数量等于类别数减一。
3.2 基于决策树的模型训练
至于决策树,它们使用一种称为递归划分的不同方法进行训练。
递归划分是一种自顶向下的过程,从整个数据集开始,根据一系列规则或条件将其划分为子集。这个划分过程在每个子集上递归重复,直到满足停止准则,通常是当子集变得过小或进一步划分无法改善模型性能时。
划分规则基于数据集的特征或属性,算法在每一步选择提供模型性能最显著提升的特征。划分过程生成树状结构,其中内部节点代表划分条件,叶子节点代表最终预测结果。
在训练过程中,决策树可以使用各种度量标准进行评估,如信息增益或基尼不纯度,以确定最佳的划分标准。一旦树训练完成,可以通过根据输入特征从根节点到适当叶节点的路径来对新的、未见过的数据进行预测。
3.3 基于数学函数的模型训练
基于数学函数的模型,也称为参数模型,是假设输入与输出之间存在特定函数形式的模型。
用于优化数学函数模型参数的最基本算法是梯度下降。梯度下降是一种迭代优化算法,从参数值的初始猜测开始,然后根据损失函数对参数的梯度更新参数。这个过程持续进行,直到算法收敛到损失函数的最小值。
对于非凸函数,通常使用随机梯度下降(SGD)代替梯度下降。SGD 在每次迭代中随机抽样数据的一个子集来计算梯度,这使得它比梯度下降更快、更高效。
在神经网络中,反向传播用于计算损失函数相对于参数的梯度。反向传播本质上是将微积分的链式法则应用于神经网络表示的复合函数。它允许对网络的每一层有效地计算梯度,这是训练深度神经网络所必需的。
对于深度学习模型,通常使用更高级的优化技术来提高性能。这些技术包括如动量,它帮助算法避免陷入局部最小值,以及自适应学习率方法,它在训练过程中自动调整学习率,以提高收敛速度和稳定性。
总的来说,梯度下降是优化基于数学函数的模型参数的基本算法。对于非凸函数,通常使用随机梯度下降。反向传播用于计算神经网络中的梯度,而更高级的技术常用于深度学习模型。
4. 模型调优
机器学习的第三个方面涉及通过使用网格搜索来优化模型的超参数。超参数是模型的设置或配置,这些设置在训练过程中不会被学习,而必须手动指定。
超参数的例子包括学习率、神经网络中的隐藏层数量和正则化强度。通过使用网格搜索,评估多个超参数组合,以确定模型的最佳配置。
网格搜索是一种用于优化机器学习模型超参数的流行技术。然而,它并不是唯一的方法,还有其他几种替代方案可以用来微调模型的参数。一些最受欢迎的网格搜索替代方法包括:
-
随机化网格搜索:与网格搜索不同,随机搜索涉及从预定义范围中随机抽样超参数,从而更高效地探索参数空间。
-
贝叶斯优化:贝叶斯优化使用概率模型通过迭代评估模型的性能和更新超参数的概率分布来寻找最佳的超参数组合。
-
遗传算法:遗传算法模拟自然选择的过程,通过生成潜在解决方案的种群,评估它们的性能,并选择最适合的个体进行繁殖,从而找到最佳的超参数组合。
-
基于梯度的优化:基于梯度的优化涉及使用梯度迭代调整超参数,旨在最大化模型的性能。
-
基于集成的优化:基于集成的优化涉及将多个具有不同超参数的模型结合起来,以创建一个更稳健、更准确的最终模型。
每种替代方案都有其优缺点,最佳方法可能取决于所解决的具体问题、参数空间的大小以及可用的计算资源。
5. 帮助你有效学习机器学习的技巧
现在我们对不同类别的机器学习算法有了大致了解,让我们探索一下创建有效预测模型所需学习的内容。
5.1 算法是否太难以学习?
让我们从一些初看起来可能显得复杂的算法开始,这可能会让你认为机器学习是一个具有挑战性的领域。然而,通过将过程分解为三个阶段(模型、拟合和调优),你将能够获得更清晰的理解。
例如,学习支持向量机(SVM)对有志于数据科学的学生来说可能令人畏惧,因为存在大量技术术语,如最优超平面、无约束最小化、对偶性(原始和对偶形式)、拉格朗日乘数、Karush-Kuhn-Tucker 条件、二次规划等。然而,重要的是要理解 SVM 只是一个线性模型,就像 OLS 回归一样,其方程为 y = wX+b。
虽然上述各种技术用于通过不同方法优化 SVM,但至关重要的是不要被技术细节所困扰,而要专注于 SVM 作为线性模型的基本概念。
如果你有兴趣进一步探讨这个观点,我将会撰写一篇相关的文章。请在评论中告知我。
5.2 理解模型
我们讨论了三种类型的机器学习算法——模型、拟合算法和调优算法。在我看来,有志于数据科学的学生应优先理解模型,而不是其他两个步骤。
从这个角度来看,机器学习模型被分类为三种主要类型,有助于理解它们的功能:
-
基于距离的模型:在这种类型中,KNN 不被视为一个适当的模型,因为新观察的距离是直接计算的。在 LDA 或 QDA 中,距离是计算到一个分布的。
-
基于决策树的模型:决策树遵循 if-else 规则,并形成一组用于决策的规则。
-
基于数学函数的模型:它们可能不容易理解。然而,这些函数通常很简单。
一旦你对模型的工作原理有了深入理解,模型的拟合和调优可以通过现有的包来完成:对于拟合,流行的 scikit-learn 库提供了model.fit
方法,而对于调优,像 Optuna 这样的工具提供了有效的研究优化技术,使用study.optimize
。通过专注于理解模型本身,未来的数据科学家可以更好地为成功做好准备。
对于某些单独的模型,如果你采用这种方法,你可以提高对它们的理解。我会写单独的文章,但这里有一些示例:
-
多项式回归是线性回归,在将特征提高到不同的幂之后。
-
线性回归、岭回归、LASSO 和 SVR 是相同的模型,只是基础的成本函数不同。
-
线性回归、逻辑回归和支持向量机(SVM)是相同的模型,只是基础的成本函数不同。在这里你可能会注意到,线性回归是回归器,而逻辑回归和 SVM 是分类器。嗯,查看 SGDClassifier 的文档或看看这篇关于SGDClassifier的文章吧。
这些十大最常见却令人困惑的机器学习模型名称说明了理解模型并不总是直观的。
5.3 可视化模型
可视化可以是理解模型的一个极其有用的工具。在处理机器学习模型时,使用简单的数据集创建可视化可以帮助说明模型是如何创建的以及它们是如何工作的。
这里是我写的一些文章,你可以在下面找到链接。这些文章涵盖了线性回归的可视化,这也适用于岭回归、LASSO 和 SVM,以及神经网络。此外,还有一篇关于在 Excel 中实现 KNN 的文章。
另一种方法是在 Excel 中实现这些模型,因为它可以提供一种直观的方式来查看数据和模型的输出。
-
线性回归的可视化
-
决策树回归器的可视化
-
敬请关注,我会发布更多内容!
线性回归的可视化 — 图片由作者提供
不同特征尺度下的 K 最近邻回归器——图片来源于作者
5.4 使用 Excel 理解拟合过程
理解拟合过程起初可能会感到困难。然而,如果你想学习,就需要从对模型如何工作的扎实理解开始。在这方面,Microsoft Excel 是一个特别有帮助的工具。
Excel 是一个广泛使用的电子表格程序,可以用来可视化和操作数据。在机器学习的背景下,它可以用来演示简单模型如线性回归的拟合过程。通过使用 Excel,你可以逐步看到算法的实际实现。
需要记住的一点是,Excel 不是进行机器学习的最有效工具。尽管它可以用来理解简单数据集的拟合过程。
使用 Excel 理解拟合过程对机器学习初学者来说是一个有用的工具。它提供了一种简单且可访问的方式来可视化算法并查看其工作原理。
我已经写了几篇关于线性回归、逻辑回归和神经网络的梯度下降文章。你可以通过以下链接访问这些文章。
-
Excel 中的K-最近邻
-
在 Excel 中进行梯度下降的线性回归
-
在 Excel 中进行梯度下降的逻辑回归
-
从零开始在 Excel 中创建神经网络分类器
-
Excel 中的决策树回归器
-
在 Excel 中实现 KNN
-
从零开始在 Excel 中实现 K 均值算法
-
敬请关注,更多文章即将发布!
此外,如果你希望获得相应的 Excel/Google 表格文件,请通过以下链接支持我:ko-fi.com/s/4ddca6dff1
。
使用 Excel 进行机器学习算法——图片来源于作者
5.5 使用简单数据集进行测试
为了全面理解机器学习算法,从零开始实现它们可以是一个有效的方法。然而,这种方法可能会非常耗时,并且需要较高的技术水平。另一种方法是使用现有的包或库来创建和可视化模型的输出,特别是使用简单的数据集。
利用这些包,你可以轻松地实验不同的参数并测试各种机器学习算法。这种方法有助于你理解算法的内部工作原理,同时也能快速评估它们在特定数据集上的有效性。
通过使用这样的数据集,可以轻松地可视化模型的输入和输出。这反过来允许我们更深入地了解模型如何进行预测。此外,通过更改超参数和模型的其他方面,我们还可以可视化它们对模型预测的影响。
这种方法可以帮助初学者入门机器学习,并对不同算法的工作原理有更好的理解。这是获得实际经验和实验不同模型的绝佳方式,而无需在实现上花费太多时间。
6. 结论
总之,机器学习可以是一个复杂的领域,特别是对于有志成为数据科学家的人员。然而,理解三种主要的机器学习算法——模型、拟合算法和调优算法——并根据它们的目标和复杂性对它们进行分类,可以帮助提供它们工作原理的整体理解。通过优先理解模型、可视化它们,并在像 Excel 这样的工具中实现它们,我们可以揭示拟合和调优过程的神秘面纱。
持续学习机器学习的不同方面,例如分类与回归、处理缺失值和变量重要性,对于不断加深我们对这一领域的理解至关重要。如果你想了解更多,请查看这篇文章:监督机器学习算法概述。
我写关于机器学习和数据科学的文章,并尝试以清晰的方式简化复杂概念。请通过下面的链接关注我,获取我的文章的全部访问权限:medium.com/@kezhanshi/membership
机器学习并非你所需的一切:关于签名检测的案例研究
机器学习不应成为你处理所有任务的首选解决方案。像我在签名检测中一样考虑 KISS 原则。
·
关注 发表在 Towards Data Science ·6 min read·2023 年 12 月 21 日
--
图片由作者提供
在这篇文章中,我展示了一个案例研究,说明机器学习不应成为你处理所有任务的首选解决方案。更简单的技术也可能取得良好的结果,而且更容易实施。
案例研究:签名检测
想象一下我们有一堆合同,需要知道它们是否已签名。这个场景涉及签名检测——可靠地识别特定位置是否出现签名——假设你已经知道签名应该大致位于哪里(例如,东南方向)。在古代,这个任务是通过二值化图像并计算区域内的黑色像素来完成的。如果存在签名,黑色像素计数会超过阈值。但在 2023 年,我们如何以不同的方式完成这个任务呢?
机器学习方法
我们将使用 GroundingDino,这是一个最先进的零-shot 目标检测模型。模型的输入是图像与提示的结合,而输出则是表示潜在位置的矩形及其相关的置信度分数。虽然这乍一看似乎是一个理想的解决方案,但仍然存在一些值得考虑的限制。让我们尝试用三个不同的提示:‘signature’,‘handwriting’ 和 ‘scribble’。
提示结果分别为‘signature’,‘handwriting’ 和 ‘scribble’。图像由作者提供。
你可以看到结果严重依赖于提示,更不用说在 CPU 上结果出现前大约需要 30 秒。这是因为这是一个基础模型,经过大量类别的训练,不仅仅是签名。我们可以做些什么来使其更准确和快速呢?我们可以使用 Autodistill (教程),它使用 Grounding DINO 训练 YOLOv8 模型。有效地使用基础模型来训练一个更轻量的监督模型。工作流程是收集大量签名文档数据集,然后找到一个好的提示以获取标记数据,最终在其上训练一个 YOLOv8 模型。
你可以想象这需要大量的时间和精力。但是还有其他办法吗?
替代方法:OpenCV
OpenCV 是一个开源计算机视觉库,提供了广泛的功能用于实时图像和视频处理、分析和理解,使用优化算法。
OpenCV 中的 connectedComponentsWithStats
函数用于标记和分析图像区域(连通组件),基于其像素连接性,并额外计算每个标记区域的各种统计信息,如面积和边界框尺寸。
为了更易于理解,我创建了这张图。它是签名区域的裁剪图像。每个连通像素的岛屿都有一种颜色,代表一个单独的连通组件(或:标签)。
图像由作者提供。
了解上述内容后,让我们深入探讨这种计算机视觉方法背后的直觉。这里的关键思想是:我们能否识别出构成签名的标签?
在普通的、一般的文档上运行这个函数会产生数百甚至数千个唯一的标签,适用于:
-
每个字母(因为它没有连接到其他字母)
-
更大的东西,比如签名和徽标
-
更小的东西,比如微小的噪点和点
为了筛选掉不相关的标签,我们可以取所有标签的中位数区域,这将是单个字符的大小(假设图像中包含的字母多于噪声),作为最小阈值。任何低于此阈值的区域都可以被过滤掉。我们还可以设置一个最大阈值,假设签名不会占据超过字母 x 倍的区域。剩下的就是实际的签名候选项。但徽标呢?它们可能与签名大小相同,但签名通常在字母之间有很多空白。通过黑色像素比例过滤器,我可以将这些徽标筛选掉。剩下的标签应该是实际的签名。
将上述内容转化为代码结果如下:
def find_signature_bounding_boxes(image):
# Start measuring time
start_time = time.time()
if image is None:
raise ValueError("Could not open or find the image")
# Binarize the image using Otsu's thresholding method
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# Threshold the image using Otsu's method
_, binary_image = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
# Find connected components
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(binary_image, connectivity=8, ltype=cv2.CV_32S)
# Calculate median area of components
areas = stats[1:, cv2.CC_STAT_AREA] # Exclude background
median_area = np.median(areas)
print('median_area: ' + str(median_area))
median_character_width = int(math.sqrt(median_area))
print('median_character_width: ' + str(median_character_width))
# Define area thresholds
min_area_threshold = median_area * 4
max_area_threshold = median_area * 50
# Filter components based on area thresholds
possible_signatures = []
for i in range(1, num_labels): # Exclude background
area = stats[i, cv2.CC_STAT_AREA]
if min_area_threshold < area < max_area_threshold:
left = stats[i, cv2.CC_STAT_LEFT]
top = stats[i, cv2.CC_STAT_TOP]
width = stats[i, cv2.CC_STAT_WIDTH]
height = stats[i, cv2.CC_STAT_HEIGHT]
print('Found candidate with area: ' + str(area))
#filter horizontal lines
if height < median_character_width * 5 and width > median_character_width*30:
print(' -> candidate is horizontal line with width, height: ' + str(width) + ',' + str(height))
continue
#filter vertical lines
if width < median_character_width * 5 and height > median_character_width*30:
print(' -> candidate is vertical line with width, height: ' + str(width) + ',' + str(height))
continue
#filter on a ratio of black pixels (logos for example have a higher ratio)for now guestimate is 0.3
roi = binary_image[top:top+height, left:left+width]
num_black_pixels = cv2.countNonZero(roi) # Calculate the number of black pixels in the ROI
total_pixels = width * height # Calculate the total number of pixels in the ROI
ratio = num_black_pixels / total_pixels # Calculate and return the ratio of black pixels
print(' -> candidate has black pixel ratio: ' + str(ratio))
if ratio > 0.30:
print(' -> candidate has too high black pixel ratio: ' )
continue
possible_signatures.append((left, top, width, height))
print('Nr of signatures found before merging: ' + str(len(possible_signatures)))
possible_signatures = merge_nearby_rectangles(possible_signatures, nearness=median_character_width*4)
# End measuring time
end_time = time.time()
print(f"Function took {end_time - start_time:.2f} seconds to process the image.")
return possible_signatures
def merge_nearby_rectangles(rectangles, nearness):
def is_near(rect1, rect2):
left1, top1, width1, height1 = rect1
left2, top2, width2, height2 = rect2
right1, bottom1 = left1 + width1, top1 + height1
right2, bottom2 = left2 + width2, top2 + height2
return not (right1 < left2 - nearness or left1 > right2 + nearness or
bottom1 < top2 - nearness or top1 > bottom2 + nearness)
def merge(rect1, rect2):
left1, top1, width1, height1 = rect1
left2, top2, width2, height2 = rect2
right1, bottom1 = left1 + width1, top1 + height1
right2, bottom2 = left2 + width2, top2 + height2
min_left = min(left1, left2)
min_top = min(top1, top2)
max_right = max(right1, right2)
max_bottom = max(bottom1, bottom2)
return (min_left, min_top, max_right - min_left, max_bottom - min_top)
merged = []
while rectangles:
current = rectangles.pop(0)
has_merged = False
for i, other in enumerate(merged):
if is_near(current, other):
merged[i] = merge(current, other)
has_merged = True
break
if not has_merged:
for i in range(len(rectangles) - 1, -1, -1):
if is_near(current, rectangles[i]):
current = merge(current, rectangles.pop(i))
if not has_merged:
merged.append(current)
return merged
我只花费了原本需要的一小部分时间来实现机器学习方法。除了节省时间之外,它的效果非常好。它能处理高分辨率和低分辨率扫描。该方法的其他优点是它能很容易地集成到现有的 C++或 Python 代码中,并且运行速度极快。当然,参数可以进一步调整,为此我邀请你打开我的共享 colab 笔记本自行尝试。如果你愿意在线试用,可以尝试我的Huggingface 演示。
图片由作者提供。
结论
面对技术挑战时,不要立刻进入“机器学习模式”,要对其他更简单的技术保持开放。虽然机器学习令人兴奋并开辟了许多新可能,但并不是每个任务都需要它。在选择适合你挑战的方法时,考虑开发时间、部署难易程度、准确性权衡和处理速度等因素是很重要的。
机器学习变得直观
ML:你需要知道的所有内容,没有任何复杂的数学
·
关注 发表在 Towards Data Science ·9 分钟阅读·2023 年 7 月 5 日
--
你可能认为的 ML…(照片由 Justin Cheigh 在丹麦比隆拍摄)
什么是机器学习?
当然,像 ChatGPT 这样的模型背后的实际理论确实非常复杂,但机器学习(ML)背后的基本直觉是,嗯,很直观!那么,什么是 ML?
机器学习允许计算机通过数据进行学习。
但这意味着什么呢?计算机如何使用数据?计算机学习意味着什么?首先,谁在乎?让我们从最后一个问题开始。
现在,数据无处不在。因此,使用像机器学习这样的工具变得越来越重要,因为它可以在没有明确编程的情况下帮助发现数据中的有意义模式!换句话说,通过利用机器学习,我们能够将通用算法成功地应用于各种问题。
机器学习有几个主要类别,其中主要类型包括有监督学习(SL)、无监督学习(UL)和强化学习(RL)。今天我将只描述有监督学习,但在后续的帖子中,我希望能更详细地讲解无监督学习和强化学习。
1 分钟 SL 速跑
看,我明白你可能不想读完整篇文章。在这一部分,我将教你最基础的知识(对很多人来说,这就是你需要了解的全部内容!),然后再深入后面的部分。
有监督学习涉及如何使用不同的特征来预测某个标签。
设想一下,你试图找出一种方法来预测钻石的价格,使用的特征包括克拉、切工、净度等。这里的目标是学习一个函数,该函数以特定钻石的特征作为输入,并输出相应的价格。
就像人类通过例子学习一样,在这种情况下,计算机也会如此。为了能够学习预测规则,这个机器学习代理需要“标记示例”,包括钻石的特征和价格。监督的存在是因为你得到了标签(价格)。实际上,需要考虑的是你的标记示例是否真实,因为有监督学习的一个假设是标记示例是“真实情况”。
好的,现在我们已经了解了最基本的内容,我们可以更深入地探讨整个数据科学/机器学习流程。
问题设置
让我们用一个非常贴近的例子,这个例子来源于这本教科书。假设你被困在一个只有一种稀有水果“贾斯廷蜜瓜”的岛上。尽管你从未吃过贾斯廷蜜瓜,但你吃过很多其他水果,并且你知道你不想吃已经坏掉的水果。你还知道通常通过查看水果的颜色和坚实度可以判断水果是否变坏,因此你推测这对于贾斯廷蜜瓜也适用。
在机器学习术语中,你使用了之前的行业知识来确定两个特征(颜色、坚实度),你认为这将准确预测标签(贾斯廷蜜瓜是否变坏)。
但你怎么知道什么颜色和什么坚实度对应于水果变坏呢?谁知道呢?你只需要尝试。在机器学习术语中,我们需要数据。更具体地说,我们需要一个包含真实贾斯廷蜜瓜及其相关标签的标记数据集。
数据收集/处理
所以你花了接下来几天吃蜜瓜,并记录了颜色、硬度以及蜜瓜是否变坏。经过几天痛苦的吃坏掉的蜜瓜后,你得到了以下标注的数据集:
每一行代表一个特定的蜜瓜,每一列代表相应蜜瓜的特征/标签值。但请注意,我们有的是文字,因为特征是分类的而不是数值的。
实际上,我们需要数字以便计算机处理。有多种技术可以将分类特征转换为数值特征,从独热编码到嵌入以及其他方法。
我们可以做的最简单的事情是将“Label”列转换为“Good”列,如果蜜瓜好则为 1,如果坏则为 0。现在,假设有某种方法将颜色和硬度转换为一个从-10 到 10 的尺度,使其合理。作为额外的挑战,考虑将像颜色这样的分类特征放到这样一个尺度上的假设。经过这种预处理后,我们的数据集可能看起来像这样:
我们现在有了一个标注的数据集,这意味着我们可以使用监督学习算法。我们的算法需要是分类算法,因为我们预测的是一个类别,即好(1)或坏(0)。分类算法与回归算法相对,后者预测的是连续值,比如钻石的价格。
探索性数据分析
但使用什么算法呢?有多种监督分类算法,从基本的逻辑回归到一些复杂的深度学习算法。首先,让我们通过进行一些探索性数据分析(EDA)来查看我们的数据:
上面的图像是特征空间的图示;我们有两个特征,我们只是将每个例子放到一个坐标图上,两个轴分别是这两个特征。此外,如果相关的蜜瓜是好的,我们将点标记为紫色,如果是坏的,则标记为黄色。显然,通过稍微进行一些 EDA,就可以找到一个明显的答案!
我们可能应该将红圈内的所有点分类为优质瓜,而红圈外的点分类为劣质瓜。直观上,这样做是合理的!例如,你不想要一个坚硬如石的瓜,但也不希望它过于软绵绵。你想要的是介于两者之间的瓜,颜色的情况也可能类似。
我们确定我们希望的决策边界是一个圆,但这只是基于初步的数据可视化。我们如何系统地确定这一点?这在更大的问题中尤为相关,答案也不那么简单。想象一下数百个特征。在任何合理的方式中,都无法可视化 100 维特征空间。
我们学到了什么?
第一步是定义你的模型。分类模型有很多种。由于每种模型都有自己的假设,因此做出一个好的选择非常重要。为了强调这一点,我将从一个非常糟糕的选择开始。
一个直观的想法是通过权衡每个因素来进行预测:
Justin Cheigh 使用 Embed Fun 提供的公式
例如,假设我们的参数 w1 和 w2 分别为 2 和 1。还假设我们的输入 Justin Melon 是 Color = 4,Firmness = 6 的瓜。那么我们的预测 Good = (2 x 4) + (1 x 6) = 14。
我们的分类(14)甚至不是有效选项(0 或 1)之一。这是因为这实际上是一个回归算法。事实上,它是最简单的回归算法的简单案例:线性回归。
那么,让我们将其转化为分类算法。一个简单的方法是:使用线性回归,并在输出高于偏置项 b 时分类为 1。实际上,我们可以通过在模型中添加一个常数项来简化,使得当输出高于 0 时分类为 1。
在数学中,设 PRED = w1 * Color + w2 * Firmness + b。然后我们得到:
Justin Cheigh 使用 Embed Fun 提供的公式
这当然更好,因为我们至少在执行分类,但让我们绘制 PRED 在 x 轴上的图和分类在 y 轴上的图:
这有点极端。PRED 的轻微变化可能会完全改变分类。一种解决方案是让我们模型的输出表示 Justin-Melon 是好瓜的概率,我们可以通过平滑曲线来实现:
这是一条 sigmoid 曲线(或 logistic 曲线)。所以,与其使用 PRED 并应用这段分段激活(PRED ≥ 0 时为好),我们可以应用这个 sigmoid 激活函数来获得如上所示的平滑曲线。总体来说,我们的 logistic 模型看起来是这样的:
Justin Cheigh 使用 Embed Fun 提供的公式
在这里,sigma 代表的是 sigmoid 激活函数。很好,所以我们有了我们的模型,我们只需要找出哪些权重和偏差是最好的!这个过程被称为训练。
模型训练
很好,所以我们需要做的就是找出哪些权重和偏差是最好的!但这说起来容易做起来难。有无数种可能性,而“最好”到底意味着什么呢?
我们从后一个问题开始:什么是最好的?这里有一种简单而强大的方法:最优的权重是那些在我们的训练集上获得最高准确率的权重。
因此,我们只需要找出一个能够最大化准确率的算法。然而,从数学上来说,最小化某些东西更容易。换句话说,我们更倾向于定义一个损失函数,而不是一个值函数,其中较低的损失更好。虽然人们通常使用类似二元交叉熵的东西来计算(分类)损失,但我们将使用一个简单的例子:最小化错误分类的点数。
为此,我们使用一个被称为梯度下降的算法。从一个非常高的层次来看,梯度下降就像是一个视力不佳的滑雪者试图下山。一个好的损失函数的重要特性(而我们的粗糙损失函数实际上缺乏这种特性)是平滑性。如果你绘制我们的参数空间(参数值和相应的损失在同一图上),图像将看起来像一座山。
所以,我们首先从随机参数开始,因此我们很可能从一个糟糕的损失开始。就像一个滑雪者试图尽快下山一样,算法在各个方向上查找,试图找到最陡的路径(即如何调整参数以最大程度地降低损失)。但是,滑雪者的视力不佳,所以他们只能在每个方向上看一点。我们迭代这个过程,直到我们到达山底(敏锐的观察者可能会注意到我们实际上可能会到达局部最小值)。此时,我们得到的参数就是我们的训练参数。
一旦你训练了你的逻辑回归模型,你会发现你的性能仍然很差,准确率只有大约 60%(勉强比猜测好一点!)。这是因为我们违反了模型的一个假设。逻辑回归在数学上只能输出一个线性决策边界,但从我们的 EDA 中我们知道,决策边界应该是圆形的!
有了这个理念,你尝试了不同的、更复杂的模型,结果得到一个准确率达到 95%的模型!现在你有了一个完全训练好的分类器,能够区分好的 Justin-Melons 和不好的 Justin-Melons,你终于可以吃到你想要的所有美味水果了!
结论
让我们退后一步。大约在 10 分钟内,你学到了很多关于机器学习的知识,包括基本的监督学习流程。那么,接下来是什么呢?
好吧,那得由你来决定!对于一些人来说,这篇文章足以让他们对机器学习有一个高层次的了解。而对其他人来说,这篇文章可能会留下很多未解的问题。这很好!也许这种好奇心会促使你进一步探讨这个话题。
例如,在数据收集步骤中,我们假设你会连续几天吃大量的瓜果,而没有真正考虑任何特定的特征。这是没有意义的。如果你吃了一种绿色糊状的Justin-Melon并且让你感到剧烈不适,你可能会避开这种瓜果。在现实中,你会通过经验来学习,不断更新你的信念。这个框架更类似于强化学习。
那么如果你知道一种不好的Justin-Melon可能会立即致命,并且没有确定性就尝试它太过风险呢?没有这些标签,你无法进行监督学习。但也许还有一种方法可以在没有标签的情况下获得洞察。这个框架更类似于无监督学习。
在接下来的博客文章中,我希望能类比地扩展强化学习和无监督学习。
感谢阅读!
机器学习必读:秋季专题
·
跟随 发表在 Towards Data Science ·作为 Newsletter 发送 ·阅读时间 3 分钟 ·Oct 5, 2023
--
理解当前机器学习的现状并不容易:一方面,即使在该领域工作了一段时间,掌握基础概念和方法也需要时间。另一方面,新的工具和模型不断涌现,更新速度迅猛。作为机器学习学习者应该怎么办?
我们倾向于采取一种平衡、渐进的方法——这种方法认识到没有一个人可以掌握所有的知识,但是持续、稳定地消化信息的方法可以帮助你在该领域站稳脚跟。
我们本周的亮点选择反映了这一信念:我们挑选了一些精心制作的文章,涵盖了基础话题和前沿话题,初学者和经验丰富的专业人士都能从中受益。让我们深入了解一下。
-
SHAP 与 ALE 在特征交互中的对比:理解冲突结果理解模型预测是数据专业人员工作的核心,但这一过程很少是直接的。Valerie Carey的最新文章集中讨论了一个特别棘手的情况,即两个可解释性工具——SHAP 和 ALE——产生冲突结果,并探讨了如何超越这些困惑时刻。
-
AI 的奥林匹克:机器学习系统的基准测试借鉴首位突破 4 分钟英里障碍的运动员,Matthew Stewart, PhD提供了机器学习基准测试的全景概述,并探讨了这些基准如何促进创新和提高性能:“一个设计良好的基准可以引导整个社区向突破性的进展迈进,重新定义一个领域。”
-
DINO——计算机视觉的基础模型如果你喜欢深入探讨某个话题,不要错过Sascha Kirch的系列文章,这些文章逐一解析和背景化有影响力的机器学习论文。在最近的一篇文章中,Sascha 带我们深入了解了 DINO,这是一种基于视觉变换器(ViT)开创性能力的基础模型。
-
探索 GEMBA:一种基于 LLM 的新型翻译质量评估指标机器翻译技术并不新颖,但 LLM 的兴起为提升现有工具和工作流程带来了新可能。Dr. Varshita Sher的最新文章向我们介绍了 GEMBA,这是一种利用 GPT 模型评估机器翻译文本质量的新指标。
-
机器学习插图:增量学习
对于视觉学习者,特别是那些刚踏入这一领域的人,Shreya Rao的面向初学者的增量学习指南解答了一个关键问题:模型如何维护和构建现有知识?
本周想要拓展其他话题吗?希望如此——以下是一些最近的亮点:
-
如果你觉得在繁忙的日程中抽时间去探索新话题和扩展技能很困难,不妨看看Zijing Zhu为数据科学家制定的形成健康持续学习习惯的指南。
-
弥合现有对话 AI 工具与现实世界用户界面系统之间的差距是一个真正的挑战;Janna Lipenkova的深度解析提供了一份详细的路线图,帮助你实现目标。
-
一个功能性、实用的 AI 伦理工具包会是什么样子?Malak Sadek 分享了她最近主持的一次以设计为导向的研讨会中的有益见解。
-
对于以市场营销和业务为重点的数据科学家,Damian Gil概述了几种高级客户细分技术(包括一种依赖 LLMs 力量的技术),这些技术可以帮助你生成有价值的见解。
-
各国政府正在尝试对 AI 工具进行监管。Viggy Balagopalakrishnan 反思了这些尝试的不足之处,并倡导一种更为务实、基于机制的方法。
感谢你支持我们作者的工作!如果你喜欢在 TDS 上阅读的文章,考虑成为 Medium 会员——这将解锁我们整个档案(以及 Medium 上的其他每一篇文章)。
多输出数据集上的机器学习:快速指南
如何在多输出数据集上以最小编码工作量训练和验证机器学习模型
·
关注 发布于 Towards Data Science ·6 min read·2023 年 3 月 10 日
--
图片由 Victor Barrios 提供,来源于 Unsplash
介绍
大家熟悉的标准机器学习任务包括分类(二分类和多分类)和回归。在这些情况下,我们尝试预测一个目标列。在多输出情况下,有多个目标列,我们希望训练一个能够同时预测所有目标列的模型。我们识别出三种多输出任务:
-
多标签:多标签是一个分类任务,将每个样本标记为m个标签,标签来自n_classes个可能的类别,其中m的范围是 0 到n_classes。这可以被视为预测样本的非互斥属性。例如,对文本文件相关话题的预测。该文件可能涉及宗教、政治、金融或教育中的一个、多个或所有话题类别。
-
多类-多输出:多类-多输出(也称为多任务分类)是一个分类任务,其中每个样本都有一组非二元属性标签。属性的数量和每个属性的类别数量都大于 2。这不仅是对多标签分类任务的泛化(多标签分类任务只考虑二元属性),也是对多类分类任务的泛化(多类分类任务只考虑一个属性)。例如,对一组水果图像的“水果类型”和“颜色”属性进行分类。属性“水果类型”可能的类别有:苹果、梨和橙子。属性“颜色”可能的类别有:绿色、红色、黄色和橙色。每个样本是一张水果的图像,对两个属性都会输出一个标签,每个标签都是对应属性的可能类别之一。
-
多输出回归:多输出回归预测每个样本的多个数值属性。每个属性是一个数值变量,每个样本需要预测的属性数量>=2。例如,使用在某个位置获得的数据预测风速和风向(以度为单位)。每个样本是从一个位置获得的数据,对每个样本会输出风速和风向。
在这个故事中,我们将解释ATOM库如何帮助你加快多输出数据集的管道。从数据预处理到模型训练、验证和结果分析。ATOM 是一个开源 Python 包,旨在帮助数据科学家探索机器学习管道。
注意:这个故事专注于使用 ATOM 处理多输出数据集。库的基础知识教学不在本故事范围之内。如果你想了解库的简单介绍,请阅读这篇文章。
数据预处理
在atom中初始化多输出数据集的过程与其他任务非常相似,但有一点需要注意:你必须使用关键字参数y
指定目标列。
atom = ATOMClassifier(X, y=y, verbose=2, random_state=1)
如果不提供y=
,atom 会认为第二个参数是测试集,就像你用atom = ATOMClassifier(train, test)
进行初始化一样,这会导致列不匹配异常。
你还可以提供一系列列名或位置来指定 X
中的目标列。例如,要指定最后 3 列作为目标,请使用:
atom = ATOMClassifier(X, y=(-3, -2, -1), verbose=2, random_state=1)
在所有情况下,打印 self.y
现在返回的是 DataFrame
类型的目标,而不是 Series
类型。
对于多标签任务,目标列可能看起来像这样。
0 [politics]
1 [religion, finance]
2 [politics, finance, education]
3 []
4 [finance]
5 [finance, religion]
6 [finance]
7 [religion, finance]
8 [education]
9 [finance, religion, politics]
Name: target, dtype: object
模型不能直接处理变量数量的目标类。使用
使用 clean 方法为每个样本的每个类分配一个二进制输出。正类用 1 表示,负类用 0 表示。因此,它可与运行 n_classes 个二元分类任务相比较。
atom.clean()
在我们的示例中,目标(atom.y
)被转换为:
education finance politics religion
0 0 0 1 0
1 0 1 0 1
2 1 1 1 0
3 0 0 0 0
4 0 1 0 0
5 0 1 0 1
6 0 1 0 0
7 0 1 0 1
8 1 0 0 0
9 0 1 1 1
模型训练和验证
一些模型对多输出任务有原生支持。这意味着
原始估计器用于直接对所有
目标列。
然而,大多数模型没有对多输出任务的集成支持。ATOM 仍然使使用它们成为可能,通过将估计器包装在一个能够处理多个目标列的元估计器中。这是自动完成的,无需额外的代码或用户的先验知识。
对于多标签任务,默认使用的元估计器是:
对于多类多输出和多输出回归任务,
默认的元估计器分别是:
multioutput
属性包含了元估计器对象。更改该属性:
属性的值使用自定义对象。无论是类还是实例,只要
原估计器是第一个参数的情况。比如,要更改回归模型的元估计器,使用:
from sklearn.multioutput import RegressorChain
atom.multioutput = RegressorChain
要检查哪些模型对多输出数据集有原生支持,哪些没有,使用:
atom.available_model()[["acronym", "model", "native_multioutput"]]
现在,你可以正常训练模型。
atom.run(models=["LDA", "RF"], metric="f1")
并检查估计器。
一些模型,如 MultiLayer Perceptron,对多标签任务有原生支持,但对多类多输出任务没有。因此,它们的 native_multioutput
标记为 False,但如果你有一个多标签任务,这些模型不一定需要一个多输出元估计器。在这种情况下,使用 atom 的 multioutput
属性告诉 atom 不使用任何多输出包装器。
atom.multioutput = None
# MLP won't use a meta-estimator wrapper now
atom.run(models=["MLP"])
注意: sklearn 度量不支持多类-多输出分类任务。ATOM 计算这种任务的度量方法是对每个目标列的得分取平均值。
结果分析
具有多输出估算器的模型可以正常调用分析方法和绘图。在绘图中使用target
参数以指定使用哪个目标列。
atom.plot_roc(target=2)
当target
参数还指定了类别时,使用格式(列,类别)。
atom.plot_probabilities(models="MLP", target=(2, 1))
with atom.canvas(figsize=(900, 600)):
atom.plot_calibration(target=0)
atom.plot_calibration(target=1)
结论
我们已经展示了如何轻松使用 ATOM 包,以便快速探索多输出数据集上的机器学习管道。点击这里查看多输出回归任务的完整示例,点击这里查看多标签分类示例。
关于 ATOM 的更多信息,请查看该包的文档。如有错误或功能请求,请随时在GitHub上提交问题或发送电子邮件给我。
相关故事:
-
towardsdatascience.com/atom-a-python-package-for-fast-exploration-of-machine-learning-pipelines-653956a16e7b
-
towardsdatascience.com/how-to-test-multiple-machine-learning-pipelines-with-just-a-few-lines-of-python-1a16cb4686d
-
towardsdatascience.com/from-raw-data-to-web-app-deployment-with-atom-and-streamlit-d8df381aa19f
-
towardsdatascience.com/exploration-of-deep-learning-pipelines-made-easy-e1cf649892bc
-
towardsdatascience.com/deep-feature-synthesis-vs-genetic-feature-generation-6ba4d05a6ca5
-
towardsdatascience.com/from-raw-text-to-model-prediction-in-under-30-lines-of-python-32133d853407
-
towardsdatascience.com/how-to-make-40-interactive-plots-to-analyze-your-machine-learning-pipeline-ee718afd7bc2
参考文献:
- 本故事中的所有绘图均由作者创建。
机器学习编排与 MLOps
·
关注 发布于 数据科学前沿 · 5 分钟阅读 · 2023 年 6 月 5 日
--
图片由 Mark Williams 提供,来源于 Unsplash
我听到过我合作过的 ML 工程师说“机器学习操作(MLOps)的主要部分只是数据工程”。有一篇博客文章将实际百分比定为98%。这显然是夸张的说法,但我认为这个观点是正确的。然而,MLOps 的含义仍在变化。虽然有很多组件和变动的部分可以被认为是 MLOps 的一部分,Cristiano Breuel 的这个定义足够好,适合我在这篇文章中讨论的内容:
ML Ops 是一组结合了机器学习、DevOps 和数据工程的实践,旨在可靠且高效地部署和维护生产中的 ML 系统。
作者改编自原始图像由 CM Beuel 提供
我想补充的是,一个“好”的 ML 系统是:
解决业务需求并有效交付的系统。MLOps 团队的关注点应该始终放在解决业务需求上。
像大多数基于工作流(或管道)的系统一样,MLOps 系统需要一个协调器。在这个上下文中,它可以被称为机器学习协调器(或现在的 MLOx)。MLOx 的工作就是一个协调器的工作,即一个可以在非常具体的时间表上管理和协调复杂工作流和过程的机制。在 MLOps 和许多其他 ML 相关系统中,常用的一个协调器是Apache Airflow,其他包括 Dagster、Prefect、Flyte、Mage等。本文将重点关注 Airflow。
我喜欢把 MLOx 想象成类似于电影导演(或管弦乐队的指挥,但那可能会让人困惑,因为我们会讨论指挥管弦乐队)。导演有一个剧本,他们依据这个剧本来指导各种过程,以交付最终产品——电影。在 MLOx 的上下文中,工作流就相当于剧本。协调器的角色是确保工作流中的各种过程按计划、按正确的顺序执行,并妥善处理故障。
然而,Airflow 给这个类比带来了复杂性。Airflow 还具有计算能力,因为它可以利用运行环境来执行任何 Python 代码(类似于 Dagster、Prefect 等)。作为一个可扩展的开源工具,它更像是一个演员兼导演,其中一个 Airflow 任务可以像导演在电影中扮演角色一样成为工作流的一个重要部分。使用 Airflow,你可以将数据加载到内存中,进行一些处理,然后将数据传递到下一个任务。通过这种方式,Airflow 可以作为 MLOps 工具,也可以作为协调器。它可以直接执行所需的机器学习操作,或者仅仅作为协调器,实例化 TensorFlow 集群上的进程或启动 Spark 作业等。
简而言之,MLOx 是一个利用机器学习工具进行操作的协调器。
ZenML 定义的 MLOx 如下:
协调器是任何 MLOps 堆栈中的关键组件,因为它负责运行你的机器学习管道。为此,协调器提供了一个环境,以便执行管道的各个步骤。它还确保只有在所有输入(即管道之前步骤的输出)都可用时,管道的步骤才会被执行。
由作者改编自 原始图像 CM Beuel
使 Airflow 特别适合作为 MLOx 的特点包括:
-
DAGs(有向无环图):DAGs 是机器学习工作流的可视化表示。DAGs 使得查看任务之间的依赖关系以及跟踪工作流进度变得更加容易。
-
调度:Airflow 可以用来定期调度机器学习工作流。这有助于确保机器学习模型始终保持最新,并及时用于预测。
-
监控:Airflow 提供了多种工具来监控机器学习工作流。这些工具可以用于跟踪机器学习模型的性能,并识别潜在的问题。
想想“Airflow 和”,而不是“Airflow 或”
我发现当人们决定选择 ML 工具时,一个复杂的问题是寻找一个能够完成所有工作的工具,包括协调。某些一体化工具包括一个基本的工作流调度器以满足最低要求,但它的能力可能远不如 Airflow。一旦你的 MLOx 需求超出了包含的协调器的能力,你就需要引入一个更强大的协调器,重新进行调度工作,并且可能还需要重写大量代码。
另一个问题是我看到有人将 Airflow 与做着截然不同事情的 ML 工具进行比较,有些工具的名称恰好以“flow”结尾,比如 MLFlow 或 Kubeflow。MLFlow 主要用于实验跟踪,与 Airflow 的操作方式完全不同。Airflow 只是广泛 MLOps 工具空间中的另一个组件,从模型注册到实验跟踪到专门的模型训练平台。MLOps 涵盖了有效 ML 工作流管理所需的许多组件。
一些进入 MLOps 的人来自更具实验性的 数据科学 环境,并且没有 Dev Ops 和 数据工程 带来的更严格要求的经验。数据科学家以更随意的方式工作,而 MLOps 则需要结构化的方法。要实现端到端的 MLOps 管道,需要一种系统化、可重复的方法来处理数据、提取特征、训练模型和部署模型。Airflow 经常用于编排像这样的结构化过程,可能需要对那些习惯于更灵活的数据科学方法的人进行一些学习。然而,如果你计划扩展你的 MLOps 能力,应该从一开始就使用正确的工具。
使用 Airflow 作为 MLOx 的最终考虑是许多组织已经拥有一个正在进行某种数据编排工作的 Airflow 实现。如果已经有人知道如何管理 Airflow 基础设施,并且能够帮助创建和运行 DAG,你就拥有了启动和运行 MLOx 所需的一切。加上像 gusty 和 Astro SDK 这样的自动 DAG 生成工具,或者来自 ZenML 和 Metaflow 的机器学习专用 DAG 生成器,让你可以在不需要深入了解 Airflow 的情况下获得一个可用的 MLOx。
结论
机器学习编排,也称为 MLOx,是 MLOps 的一个重要组成部分。它需要一个全面且适应性强的解决方案。Apache Airflow 是一个强大的编排工具,能够实现无缝的工作流管理和执行。通过涵盖编排者和 MLOps 工具的角色,Airflow 使组织能够高效地部署和维护机器学习模型。随着 MLOps 领域的不断发展,采用像 Airflow 这样的工具对于最大化生产力和释放机器学习的真正潜力变得至关重要。
机器学习技巧、与 ChatGPT 的学习以及其他近期必读书单
·
关注 发表在 Towards Data Science · 以 新闻通讯 发送 · 3 分钟阅读 · 2023 年 8 月 31 日
--
随着八月的结束,我们许多读者即将重返学校(无论是大学、培训班还是在线学习),而其他人则在摆脱夏季的悠闲节奏。现在正是为你下一阶段的数据科学学习之旅做好准备的最佳时机。
如果你重新关注数据和机器学习相关的话题,你将大有收获:我们许多作者在过去一个月里显然并没有减缓步伐,实际上,他们正全力以赴地撰写一些迄今为止最强的文章。为了迎接新的一月(并向上一个月作出适当的告别),我们收集了八月份最受欢迎的文章,供你浏览、收藏——我们希望——深入阅读。享受吧!
我们最受欢迎的文章
群体智慧可能并不总是无懈可击,但你不会错过本月 TDS 的任何伟大作品——在我们发布的数百篇文章中,这些是与我们(极其)精明的读者产生共鸣的文章。
-
130 个精心策划的机器学习技巧和资源(3 年收录)这是一个巨大的实用小知识汇编,由 Bex T. 精心编排。
-
如果我可以重新开始,我会如何通过 ChatGPT 学习数据科学来自 Natassha Selvaraj,这是一个借助某个聊天机器人的实用数据科学学习路线图。
-
如何使用大型语言模型与 PDF 和图像文件对话——附代码
对于任何希望获得 LLM 实践经验的人,Zoumana Keita的教程是一个顶级选择。
-
图卷积网络:GNNs 介绍
如果你想阅读一篇理论与实践完美结合的文章,不妨看看 Maxime Labonne 关于 GNNs 的易读入门。
-
掌握蒙特卡洛方法:如何通过模拟改进你的机器学习模型证明了即便是长篇精心制作的文章也永远不会显得过长,Sydney Nye对蒙特卡洛模拟的深入探讨——她的 TDS 首篇文章!—在我们的读者中引起了极大的轰动。
图片由 Mike Petrucci 提供,来源于 Unsplash
话题引发者
我们喜欢那些能引发热烈讨论的文章——或者为现有对话增添有趣的角度。以下是最近三篇在这方面表现突出的帖子。
-
决策科学是否悄然成为了新的数据科学?如果你一直在好奇决策科学家在做什么以及这一角色如何与传统数据分析师有所不同,不要错过Matt Chapman的见解(以及它引发的许多热烈评论)。
-
机器学习工程师——他们到底做什么?感谢Stephanie Kirmer关于 MLEs 的帖子,我们有机会看到关于职位名称和描述演变的另一场精彩讨论——以及在这个案例中,职位描述如何有时反映出我们需要直接面对的偏见和紧张。
-
你的数据(终于)在云端了。现在,别再表现得像在本地部署近年来数据基础设施的迅速演变使得许多团队在云端和本地部署之间的某个地方停滞不前。幸运的是,Barr Moses提供了关于如何迈向更高效未来的深刻见解。
我们最新的一批新作者
每个月,我们都很高兴看到一群新的作者加入 TDS,他们将自己独特的声音、知识和经验分享给我们的社区。如果你在寻找新的作家进行探索和关注,可以浏览我们最新加入的作者的作品,包括Jonathan Apple、Jarom Hulet、Sergey Vilov、David Rubio、Sydney Nye、Rüdiger Buchkremer, PhD、Michal Szudejko、Wanming Huang、John Lenehan、François Porcher、Tingsong Ou、Le Nguyen、Suyang Li、Ida Johnsson, PhD、Michael Segner、Radmila M.、Chen Margalit、Pratik Aher、Gabriel L. Sena、Stan Pugsley、Caroline Arnold、Jeff Braun、Gianpi Colonna、Jaroslaw Drapala、Ahmad Albarqawi、Mateusz Praski、Julie Zhang、Joseph George Lewis、Thao Vu、Stefan Berkner、Ryan Shrott、Mary M、Eric Zhù、Stefan Suwelack、Maham Haroon、Jeff Chou、Berkan Zorlubas、Mariano Kamp、Douglas Blank, PhD、Gamze Zorlubas、Fatih Demirci、Jerry Qu,以及Daniel Frees。
感谢您支持我们作者的工作!如果您喜欢在 TDS 上阅读的文章,考虑成为 Medium 会员——这将解锁我们整个档案库(以及 Medium 上的其他所有帖子)。
直到下一个 Variable,
TDS 编辑团队
专家模型的机器学习:入门指南
原文:
towardsdatascience.com/machine-learning-with-expert-models-a-primer-6c74585f223f
数十年前的理念如何使得今天训练极其庞大的神经网络成为可能
·发表于 Towards Data Science ·9 分钟阅读·2023 年 9 月 5 日
--
(Pexels)
专家模型是机器学习中最有用的发明之一,但它们往往未能获得应有的关注。事实上,专家建模不仅使我们能够训练出“极其庞大”的神经网络(稍后会详细讲述),它们还使我们能够构建出更像人脑的模型,即不同区域专门处理不同类型的输入。
在这篇文章中,我们将探讨专家建模的关键创新,这些创新最终导致了如 Switch Transformer 和 Expert Choice Routing 算法这样的最新突破。但首先让我们回顾一下所有这一切的起点: “专家混合模型”。
专家混合模型(1991)
1991 年的原始 MoE 模型。图片来源:Jabocs et al 1991, Adaptive Mixtures of Local Experts。
专家混合模型(MoE)的理念可以追溯到三十多年前,源于 1991 年由人工智能奠基人Geoffrey Hinton共同撰写的论文。MoE 的核心思想是通过结合多个“专家”E 来建模输出“y”,每个专家的权重由“门控网络”G 控制:
在这个上下文中,专家可以是任何类型的模型,但通常选择的是多层神经网络,门控网络是
其中 W 是一个可学习的矩阵,用于将训练样本分配给专家。因此,训练 MoE 模型的学习目标有两个方面:
-
专家将学习处理他们所得到的输出,生成最佳输出(即预测),并且
-
门控网络将学习“路由”正确的训练样本到正确的专家,通过联合学习路由矩阵 W。
为什么要这样做?为什么有效?从高层次来看,使用这种方法有三个主要动机:
首先,MoE 由于模型的稀疏性允许将神经网络扩展到非常大的规模,即使整体模型很大,由于存在高度专业化的专家,对于任何给定的训练样本,只需执行少量计算。这种方法与标准的“密集型”神经网络形成对比,在标准模型中,每个输入样本都需要每一个神经元。
其次,MoE 允许更好的模型泛化,降低过拟合风险,因为每个单独的专家可以是一个相对较小的神经网络,但通过添加更多专家,我们仍然能实现强大的整体模型性能。类似于提升方法,这是一种将大量相对较弱的学习器结合成一个强大的学习器的方法。
第三,MoE 更接近我们大脑的工作方式:任何给定的输入只会激活我们大脑中的某些区域,不同区域处理触觉、视觉、听觉、嗅觉、味觉、方向感等。这些区域都可以看作是“专家”。
简而言之,MoE 使我们不再需要对每个输入激活每一个神经元。MoE 模型稀疏、高度灵活且极其强大。
极其庞大的神经网络(2017)
快进 26 年到影响深远的论文 “极其庞大的神经网络”,同样来自 Hinton 的团队(这次在 Google Brain)。在这项工作中,作者将 MoE 推向极限,展示了一个具有千名专家的 60 亿参数 MoE 模型。为了构建如此庞大的 MoE 模型,作者引入了几种建模技巧,包括:
噪声 top-k 门控。 Top-k 门控意味着对于每个训练样本,我们只计算 top k 专家的输出(由门控确定),忽略其他所有专家。主要动机是节省计算成本:例如,如果我们有 20 个专家并应用 k=5 的 top-k 门控,我们可以将模型训练的总计算成本减少一个数量级的 4 倍!
“噪声”意味着我们在门控值中添加可调的高斯随机噪声。作者发现这有助于负载均衡,即确保整批数据不会全部发送到单一专家。
辅助损失。 作者将两个辅助(正则化)损失项添加到模型的损失函数中,即“负载均衡损失”和“专家多样性损失”,每个都有其自身可调的正则化参数:
-
负载均衡损失与每个专家接收的训练样本数量的变异系数成正比。增加这个损失可以提高计算性能,因为它防止了“专家瓶颈”的出现,即所有训练样本必须通过一个专家。(一个细节是每个专家的训练样本数量不可微分——因此作者使用了这个数量的平滑近似值。)
-
专家多样性损失与专家重要性的变异系数成正比,其中专家重要性定义为该专家的门值之和。增加这个损失可以促使模型平等地利用所有专家,而不是简单地将所有训练样本发送给一个学习所有内容的专家,这在训练 MoE 模型中是一个常见问题,也是一个局部最小值。
尽管这两个损失项相似,作者发现当同时添加负载均衡损失和专家多样性损失,并且两个损失项的正则化参数为 0.1 时,整体性能最佳。
定制化并行性。 作者展示了大型 MoE 模型如何从数据并行和模型并行的定制组合中受益:特别是,我们可以允许专家分布在不同的机器上,因为我们不需要它们之间的通信,同时使用数据并行性来增加批处理大小。这种并行形式使我们能够大幅扩展 MoE 模型:在他们的实验中,作者将模型扩展到 60 亿参数,配备数千个专家。
使用他们的大规模 MoE 模型,作者在 Billion-words 语言建模基准上建立了新的 SOTA(最先进技术)。
Switch Transformers (2022)
使用 Switch Transformer 进行 7 倍更快的训练。图片来源:Fedus et al 2022
虽然“极大规模神经网络”展示了 top-k 门控在 MoE 模型中的有效性,Switch Transformers同样由 Google 提出,通过构建 k=1 的 MoE Transformer 模型将其推向了极限,即每个训练样本仅被发送到一个专家。作者称之为“硬路由”,与标准 MoE 模型中的“软路由”形成对比,在标准 MoE 模型中,来自多个专家的输出被汇聚。
实际上,硬路由(k=1)意味着我们可以拥有多个专家,数量 N 可以为任何数值,并且计算复杂度保持不变:模型容量按 O(1)扩展!唯一的折衷是我们需要大量的内存来存储所有专家。然而,由于我们不要求专家之间的通信,这些内存可以分布在一个大型机器集群中。
此外,作者还介绍了许多实用的建模技巧,包括:
-
精确投射:这意味着我们在机器之间传送权重时使用 BF16(16 位brain float),但在计算门控值时将其转换为 FP32(32 位浮点精度)。这一技巧减少了通信开销,因为我们只需传输 16 位而非 32 位,同时确保了 softmax 计算的稳定性(16 位下不稳定)。
-
激进的专家丢弃:作者发现,通过在专家模块中应用 0.4 的激进丢弃,同时在模型架构的其他部分保持 0.1 的丢弃率,可以提高模型性能。原因是专家容易过拟合,因为它们仅看到一部分数据,因此需要更强的正则化。
-
缩放的专家初始化:作者发现,当将专家层的初始化缩小 10 倍时,训练稳定性显著提高。
最终,作者基于 Google 的 T5 语言模型构建了一个 Switch Transformer,并在相同计算资源下实现了 7 倍的预训练速度提升,这强有力地展示了通过将基于硬路由的 MoE 与 Transformer 模型结合所能带来的建模改进。
专家选择路由(2022)
标准的令牌选择路由(左)与新的专家选择路由算法(右)相比。请注意,在专家选择路由中,一个令牌可以同时路由到多个专家。图像来源:Zhou et al 2022
近期在专家建模领域的突破之一是“专家选择路由”,这是 Google 的又一创新。
训练 MoE 模型的问题在于专家通常由于“赢家通吃”效应而未被充分利用,其中一个随机接收到前几个训练样本的专家迅速变得比其他专家更优秀并主导门控。到目前为止,标准做法是添加辅助损失,以促使模型平等利用所有专家(如 Hinton 工作中的专家多样性损失)。然而,添加辅助损失带来了调整其正则化参数相对于实际损失的难题:太小,则模型行为无变化;太大,则风险使所有专家相同(另一个局部最小值)。
“专家选择路由”的关键思想简单却强大:让专家在批次中选择其最优的 k 个训练样本,而不是让训练样本选择其最优的 k 个专家。这有几个优点:
-
它保证了负载均衡和专家利用率的完美一致(每个专家总是在每个批次中接收相同数量的训练样本),
-
并非每个训练样本都有相同数量的专家,有些可能被分配给 1 位专家,有些则分配给所有专家。这是一种期望的特性:不同的训练样本对模型而言难度不同,因此可能需要不同数量的专家。
-
不需要调整的额外辅助损失。
从数学上讲,专家选择路由替代了标准的门控函数。
与
也就是说,与 e 维向量不同,门控 G 现在是一个维度为 e(专家数量)乘以 n(令牌数量)的矩阵。为什么是令牌?作者将专家选择路由考虑在 NLP 问题的背景下,因此每个训练样本只是 n 个可能令牌的序列。
到目前为止,这些理论(实际上就是将 G 从向量转换为矩阵),但它在实践中表现如何?作者展示了他们可以在一半的训练时间内达到与Switch Transformer相同的性能,并且在相同的计算成本下,在 GLUE 和 SuperGLUE 基准测试的 11 项任务上超越了Switch Transformer。
专家选择路由击败了Switch Transformer,这证明了至少对于 NLP 问题而言,“专家选择”优于“令牌选择”。
摘要
简要回顾:
-
MoE 模型中的关键思想是将输出 y 建模为专家的加权和,其中每个专家本身就是一个小型神经网络,权重由 G(x) = softmax(Wx)决定,其中 W 是一个可学习的矩阵。
-
top-k 门控意味着我们忽略所有专家,只关注前 k 个。这节省了大量计算成本,并且是专家建模中的一个重要突破。
-
MoE 模型经常陷入局部最小值,即所有训练样本都被送往单个专家。解决方法是向模型的损失函数中添加“专家多样性损失”。
-
Switch Transformers将 MoE 的概念与Transformer模型结合,并展示了这种组合可以将 T5 语言模型的训练速度提高 7 倍。这里的一个关键创新是“硬路由”,即 k=1 的 top-k 路由。
-
专家选择路由用专家选择训练样本的概念取代了训练样本选择专家的概念。这允许更好的训练稳定性而无需引入额外的辅助损失。
这仅仅是冰山一角。事实上,受到Hinton“极大规模”MoE 模型、Switch Transformer和新专家选择路由算法成功的启发,专家建模领域涌现了大量新的论文。
关于 MoE 模型的研究论文集,其中“专家选择”论文用红色突出显示,来源:ConnectedPapers。
专家建模是一个令人兴奋的领域,经过数十年的发展,我们才刚刚开始看到它对现代机器学习应用的影响。请关注这个领域——新的突破无疑在即!
机器学习的公众认知问题
为什么公众的机器学习素养需要成为数据科学的优先事项,以及我们能为此做些什么。
·
关注 发表在 Towards Data Science ·10 分钟阅读·2023 年 9 月 2 日
--
最近我在听一个播客,里面有一些聪明、深思熟虑的普通人(我不会透露他们的名字,以示礼貌),讨论 AI 如何在医疗保健中使用。我已经有些担忧,因为他们使用了“AI”这个术语,我发现这常常意味着同时包括了所有和没有任何意义。但我继续听下去,他们讨论了如何将 AI 工具(实际上只是机器学习)融入医疗实践。这些工具包括根据症状建议诊断,以及根据患者的生命体征和病情调整药物剂量,这些看起来很有前景且实际。
然而,在接下来的瞬间,我有些震惊,因为一位发言者(一位医学博士)说(我概括一下)“似乎 AI 在数学方面变差了”。这一点不仅在整个播客中萦绕于我,整个周末都未曾离开。
当受过教育、聪明的普通人对机器学习感到如此困惑和误解时,我们就有了问题。(我会避免使用“AI”这个术语,因为我真的相信它比解释更多地混淆了我们的意义。在这个背景下,这些人讨论的是机器学习和使用它的产品,即使他们对此并不知晓。)
在这位医生的例子中,他可能是在谈到大型语言模型(LLMs)时提到数学。他不知为何认为一个被训练以复杂方式排列单词以响应提示的模型也应该能够进行数学计算。它在这方面并不擅长(因为它并没有被训练来做这些!),他的所有机器学习的印象都被这一现实所玷污。
与这种误解相反,数据科学家知道 LLMs 只是更广泛机器学习领域的一小部分。许多其他算法和模型在数学计算方面表现出色,因为那是它们的具体目的。(正如一个朋友说的,当我告诉他这个故事时,“机器学习模型本质上就是数学!”)然而,这段话的开头是问题所在——数据科学家知道这一点,但公众普遍并不知晓。
…数据科学家明白 LLMs 只是更广泛机器学习领域的一小部分。
我可以花整篇文章讨论语言模型与其他形式的机器学习之间的区别,但这并不是我今天真正感兴趣的。相反,我想探讨一下为什么我们需要关注普通人缺乏这些信息,以及可能的影响。
为什么我们应该关心普通人是否了解机器学习?
作为一名转行的数据科学家,我非常关注人们如何与数据科学和机器学习互动。我对此有个人的哲学:如果你的机器学习在某种程度上没有惠及人们或我们周围的世界,那它真的不重要。我认为人类努力的目的需要是改善他人的生活,这同样适用于机器学习。
然而,即使你不认同这种观点,我认为你仍然应该关心大众是否理解机器学习的基本概念。如果人们缺乏这种理解,宝贵的、值得信赖的工具的采纳可能会停滞不前。
我的论点大致如下:
-
人们天生不准备理解和互动机器学习。
-
没有理解这些工具,一些人可能会避免或不信任它们。
-
更糟糕的是,一些人可能因为误导信息而滥用这些工具,从而导致不利的结果。
-
经历了滥用的负面后果后,人们可能会变得不愿意采纳未来可能改善他们生活和社区的机器学习工具。
机器学习的效果依赖于使用者能否最大限度地发挥其功能。我经常看到和听到像我开头提到的轶事一样的例子,人们对机器学习的理解充满了极端的误解,并在这个错误的基础上建立了思维框架。这导致了他们对机器学习的整个认知图谱都是不正确的。
这对数据科学领域的意义在于,我们在构建越来越先进的机器学习中的所有工作,其可能性并不受限于我们能获得多少 GPU,而是受限于我们解释我们所构建的内容和教育公众有关其意义及如何使用的能力。
…我们在建设更先进机器学习服务中的工作,其可能性并不受我们能获得多少 GPU 的限制,而是受限于我们解释所构建内容的能力。
人们天生并不准备理解机器学习。
我最近读了一篇文章,题为 “Why Johnny Can’t Prompt”(Zamfirescu-Pereira, Wong, Hartmann, 和 Yang, 2023 年 4 月)。这让我对非数据科学家如何看待和处理生成式 AI,特别是广义上的机器学习,有了很多思考。
我可能会另写一篇文章来详细讨论这个话题,但对于这个论点来说,有价值的一点是:人们倾向于将他们与其他人互动的既有框架应用于与机器学习系统的互动中,从而导致效果不佳和用户挫败感。
人们倾向于将他们与其他人互动的既有框架应用于与机器学习系统的互动中,从而导致效果不佳和用户挫败感。
现在,我不认为这是不可修复的。我实际上认为人类总是需要学习如何使用新工具,我们绝对可以做到。想想我们如何逐渐学会使用电脑和智能手机。最开始并不明显如何操作或如何让自己被面前的设备“理解”。
这主要是通过时间的推移、设备设计的改进使其更直观(例如,技术迎合我们现有的需求)和教育来解决的。当我年轻时,年长或技术水平较低的人可以在当地社区学院接受免费或低费用的计算机课程。目标不是学习编程,而是有效地使用计算机,因为它们是极其有用的工具。
我认为这个过程同样适用于机器学习,但有一些不同之处。首先,很多机器学习对我们来说是抽象的,或者它被包装在一个拟人化的界面中(例如,LLM 聊天机器人)。许多机器学习模型的结果进入我们的生活中而我们没有意识到,例如搜索结果个性化,或者基于对我们需求预测的应用程序提醒,仅举几例。在生成性 AI 的情况下,很多机器学习隐藏在对话型聊天机器人背后,我们自然倾向于像与任何人类对话伙伴一样与其互动。然而,正如我之前提到的文章中的作者所描述的,这是一种错误。在目前,LLM 的最佳结果不是通过“像对人一样”与其对话来实现的。
有些人不会使用他们不理解的东西。
这种现实产生了一些我们需要注意的条件。首先,许多人不会接受机器学习完全有益且简单的说法。许多人对新一代生成性 AI 感到惊恐而非兴奋。对于许多人来说,这是一种合理的反应。一方面,我们有很多文化参考和曝光教会我们,“过于聪明”的计算机是危险的,我们应该对此保持警惕。
人们对个人电脑也有这种感觉。一些人担心它们可能具有的能力和力量,或对自己理解和使用它们的实际能力感到紧张。社区学院的计算机课程让犹豫不决的人对计算机的概念有了舒适的接受。不幸的是,我没有看到数据科学领域对公众的不确定成员采取同样的关怀。
采用新技术总是充满挑战的,这不是因为人们不聪明或不好奇,而是出于对潜在风险的真实担忧。承认这些担忧并展示防止负面结果的承诺可以提高公众对机器学习的信任度。
其他人会误用和滥用他们不理解的东西。
另一方面,很多人已经全身心投入与机器学习,特别是 LLMs 的互动中。人们在各种行业和娱乐中使用它。炒作和媒体报道提高了对 LLM 技术及其潜力的认识,几乎每个有电脑的公司都在尝试将 AI 纳入他们的业务战略中。
然而,这种兴奋也有负面的一面。当人们开始使用机器学习,如 LLMs 时,他们开始注意到问题以及技术未能达到过高期望的方式。也许聊天机器人没有理解你的问题,或模型的预测并不总是准确,但最终用户期待机器不会犯错。他们为什么会有这种期望?因为他们对机器学习的了解来自流行文化和炒作。我们数据科学家没有花时间解释哪些期望是合理的,哪些仍然是科幻材料。
在误用他们不了解的工具之后,人们将害怕将来使用新的工具。
那么,当我们在机器学习解决方案中对普通用户过度承诺而未能兑现时,会发生什么呢?在很多情况下,我们将会失望和幻灭的用户,他们本可能成为新技术的伟大倡导者。他们会更不愿意尝试下一个版本,或将来使用机器学习,因为他们已经尝试过并受到了伤害。
想想这个例子:使用 ChatGPT 获取简报引文的律师。 当这个故事出来时,数据科学界对这位律师进行了抨击。“谁会这么做?难道他们不知道不能依赖 ChatGPT 的准确性吗?”
我实际上对这位律师感到相当遗憾,即使这些后果是由于相当大的马虎造成的。ChatGPT 的承诺对很多公众而言似乎几乎是魔法般的,媒体对其近乎奇迹般能力的描述进一步助长了这种观念。很多人首次了解到 ChatGPT 会“撒谎”是通过阅读这个案例。
这些误解源于对大型语言模型(LLMs)的拟人化,假设它们具有类似人类的推理和真伪判断能力。实际上,ChatGPT 是一个非常复杂的模型,它根据你给出的提示将单词排列在一起。它经过训练,能够生成非常易于理解的语言。但 ChatGPT 没有“真相”或“谎言”的概念。它没有内部嵌入来表示某事是否准确。因此,当新闻谈论 ChatGPT 撒谎或“产生幻觉”时,这有些误导。
然而,重要的是,我们现在有一群人看到了这个消息,更不用说涉及的律师,他们对从 ChatGPT 中获得的任何信息是否可靠感到焦虑。这个情境并没有帮助他们理解 LLM 的概念,也确实没有帮助实现将机器学习应用于有益的更广泛目标。有人因为缺乏对模型工作原理的了解而受到了伤害,其他人对此嗤之以鼻,而现在我们制造了更多的怀疑者,他们将来可能会避免使用至少一些形式的机器学习。
所有这些都指向相同的问题——当技术缺乏适当的公众教育时,我们就把公众教育的任务留给了不可靠和有偏见的来源,这些来源的优先级与公众利益不一致。只需问问任何公共卫生专业人员,看看他们如何努力提高疫苗接种率。机器学习如果我们不能在公众教育方面走在前面,可能会沿着相同的不幸道路发展。
我们可以做些什么来解决这个问题?
作为数据科学的从业者,我们如何弥合技术专长与公众意识之间的差距?作为一名前教育工作者,我对此非常关注。公众是否真正理解机器学习能为我们做些什么是很重要的,因为我们有机会用它做很多有益的事情。
我认为我们可以做的一件事是将更多的时间和精力投入到公众教育中。现在,我并不是说街上的每个人都需要学习反向传播或编码器架构的教程。(这就像说人们需要研究微芯片才能成为有效的计算机用户一样。)但我确实认为,人们需要了解一些机器学习的基本要素,以便成为信息技术的知情用户,包括目前技术的伦理、风险和局限性。作为一个领域,数据科学需要了解一个人需要掌握哪些信息才能成为成功且有效的机器学习用户,并且我们如何能够分享这些信息。
如果我们没有看到如此戏剧性的转变,LLM(大型语言模型)正快速普及到公众手中,我们或许可以对此稍作等待。基本的预测模型结果通常由数据科学专业人员进行中介,即模型的输入经过精心设计,结果也以深思熟虑的方式呈现。然而,对于 LLM 聊天机器人来说,这种情况并不成立。人们可以输入任何他们想要的内容,没有人控制返回的结果。用户需要更多的知识来负责任地生成和消费这些信息。
其次,我认为数据科学作为一个领域,需要对机器学习的实际能力以及它能够做什么进行更多的发声和坚持反对过度炒作和夸大其词。我发现这种情况大多数存在于吸引眼球的媒体中,甚至一些理论上更可信的新闻报道中。不要误解我,机器学习确实令人惊叹,并且它可以做出令人难以置信的事情!然而,它并不完美,我们不应该让任何人假装它是完美的,而不进行反对。
忽视这个问题,我们可能会阻碍机器学习的进步——不仅仅是技术上的进步(尽管国会未能理解机器学习可能会产生这样的效果),还包括其在实际生活中的应用进步。我不希望看到这项技术的巨大潜力因为我们没有帮助公众做好准备而被边缘化或缩小。
查看更多我的作品请访问 www.stephaniekirmer.com。
魔法:聚会竞技场:用概率获胜
原文:
towardsdatascience.com/magic-the-gathering-arena-winning-with-probability-b71f363e0ce2
如何利用概率和 Excel 赢得锦标赛
·发表在Towards Data Science ·7 分钟阅读·2023 年 5 月 9 日
--
作者: Edward Krueger 数据科学家、讲师及 Erin Oefelein 数据科学家,Taxwell 创始人。
作为数据科学家,我们被这个问题吸引,因为我们发现这是一个迷人的例子,展示了看似简单的规则如何导致相当复杂的情况,且需要仔细分析。
虽然在数据科学中已经有大量关于常见分布(例如正态分布)的研究,但现实生活中的问题通常需要不那么常见的分布,有时甚至是没有名称的分布:经验法则和天真的事件概率计算,而没有深入了解分布。在本文中,我们将探讨一个需要使用负二项分布的问题,但实际上会做一些调整以适应我们问题的特殊性。
许多游戏和锦标赛有复杂的规则。为什么?它们利用我们的心理学使游戏更有趣、上瘾,并且通过让我们赢的机会看起来比实际情况高,从而使组织者获得更多收益。当粉丝、玩家或赌徒相信他们能赢时,这是最吸引人的。如果你认为自己的机会比实际情况更好,这种效果最为显著。
在本文中,我们展示了一个来自 MTG Arena 游戏的锦标赛并计算获胜的几率。当讨论这个锦标赛时,通常认为在对战平衡的情况下,玩家平均会赢得 3 到 4 场比赛,而粗略的计算会严重高估获胜的几率。我们将进行数学计算,并展示如何使用 Excel 计算几率——将结果适配到其他统计软件应该是直接的。
摄影作品来自 Ryan Quintal 在 Unsplash
《魔法风暴竞技场》(MTGA) 十项全能由十个不同的赛事组成,持续一个月。每个赛事的入场费为 2,000 金币或 400 宝石。赛事可以是单局对决(Best-Of-One,单场比赛)或三局两胜(Best-Of-Three,三场比赛)。在本文中,我们探讨与单局对决(BO1)MTGA 赛事相关的预期结果。为了获得代币,BO1 游戏要求玩家在遭遇 3 次失败之前赢得 7 场 BO1 比赛。根据这些参数:
-
参加赛事时获得代币的概率是多少?
-
玩家需要参加多少场赛事才能赢得代币? 换句话说,平均而言,玩家需要参加多少场赛事才能赢得 7 次比赛,在遭遇 3 次失败之前,这样才能获得代币?
MTG 是一个运气和技能并存的游戏。尽管运气会引入变化,高技能的玩家通常会赢得更多的比赛。在数学领域,运气通过概率概念来量化。
概率描述了随机过程中的不确定性,其范围从 0 到 1。值为 0 表示事件 不会发生,而值为 1 表示事件 会发生。我们使用随机变量的概念来描述重复随机过程,随机变量表示在指定范围内的重复随机过程的输出。随机变量的概率分布是一个统计函数,描述了这些输出在随机变量上的分布。
摄影作品来自 Giorgio Trovato 在 Unsplash
概率分布将离散或连续随机变量从函数输入映射到实数输出。根据我们 MTG 问题的参数,我们知道负二项分布建模了我们 MTG 问题中固有的随机过程。负二项分布建模了具有以下属性的重复随机过程:
-
该过程包括重复的试验,每次试验都是独立的,即每次(x)试验的结果不影响其他试验的结果。
-
实验将持续进行,直到观察到某个预定事件(r)。这个预定事件通常被称为“成功”,然而,值得注意的是,它不一定是积极的结果!
-
每次试验只有两种可能的结果,成功(r)或失败(f),其中成功的概率(p)在每次试验中相同。
负二项分布描述了一个离散随机变量,因此由称为概率质量函数或 PMF 的概率分布类型建模,PMF 专门用于建模离散概率分布。负二项分布的 PMF,其中随机变量 X 表示第 r 次成功发生的试验(r-1),定义为:
由作者给出的方程
每个变量定义如下:
-
x:在负二项实验中产生 r 次成功的试验次数
-
r:负二项实验中的“成功”次数
-
p:任何给定个体试验中的成功概率
-
q:任何给定个体试验中的失败概率(等于 1-p)
或者,负二项分布可以用随机变量 Y 来定义,Y 代表在第 r 次成功之前发生的失败次数(y)。注意,这种替代形式在统计上是显著的,因为 Y = X - r。转换得 Y + r = X。
由作者给出的方程
那么 PMF 为什么重要?它重要的原因在于,它输出了离散随机变量在某个特定值下的概率。为了理解离散随机变量在某个特定值或更小值下的概率,我们使用累积分布函数或 CDF,这就是随机变量 X 小于或等于 x 的概率。
负二项分布的 PMF 相关变量所取的值决定了负二项分布的确切形状。这个形状由两个(2)参数特征,即停机参数 r和成功概率 p。因此,我们说负二项分布具有 (r, p) 分布。
记住,我们的目标是建模一个问题,其中玩家必须赢得 7 场 BO1 比赛,才能承受 3 次失败。实际上,玩家不能赢得超过 7 次,但为了用负二项分布来建模,我们将允许 8、9、10 次胜利。这不会影响我们的结果,因为我们将计算在事件中有 7 次胜利的概率,作为在模型中有超过 6 次胜利的概率。
重要的是玩家失败的次数。这是一个重要的区分,因为这决定了我们的停止参数 r。 当玩家遭遇 3 次失败时,尝试结束。换句话说,我们要计算的是玩家在遭遇 3 次失败之前赢得至少 7 次的概率。因为任何超过 7 次的胜利都是可以接受的,只要失败次数少于 3 次,我们将使用累计分布函数来建模这个问题。
幸运的是,像这样的建模问题使用计算机软件更为简便。Excel 提供了 NEGBINOM.DIST 函数,它需要以下输入:
-
Number_f: “失败”的次数
-
Number_s: “成功”的次数 -> 停止参数
-
Probability_s: 成功的概率
-
Cumulative: 取值为 TRUE,表示 CDF 问题,或 FALSE,表示 PMF 问题
这些参数按以下顺序设置:
- NEGBINOM.DIST(Number_f, Number_s, Probability_s, Cumulative)
我们可以定义获得代币的概率以及在不同游戏胜率水平下获得代币的预期尝试次数(在我们的负二项方程中称为 p):
作者绘图
要获得代币,玩家必须在输掉 3 场比赛之前至少赢得 7 场比赛。因此,每个事件包含的游戏次数可多可少,直到输掉 3 场比赛或赢得 7 场比赛。
那么我们是如何计算的呢?我们通过将以下内容插入 Excel 中的 NEGBINOM.DIST 方程来计算获得代币的概率以及预期事件次数:
1 — NEGBINOM.DIST(Number_f=6, Number_s=3, Probability_s=1-game win rate, Cumulative=TRUE)
-
Number_f: 请记住,CDF 表示随机变量 Y 小于或等于指定值 y 的概率,数学上表示为 P(Y <= y)。要找到赢得 7 场比赛的概率,我们可以先找到其补集的概率:未 赢得 7 场比赛的概率。数学上,这就是 P(Y < 7) = P(Y <= 6),这是在 6 处计算的 CDF。然后,我们可以将赢得 7 场比赛的概率计算为 P(Y=7) = 1 — P(Y<7) = 1 — P(Y<=6)。
Number_s: 停止参数, 等于 3 次失败
我们将“成功”定义为失败。因此,我们将定义“成功次数或停止参数”的第二个参数设为 3。
- Probability_s: 1 — 游戏胜率
因为我们定义了“成功”为玩家失败的概率,我们将使用游戏胜率的逆作为成功的概率的参数,等于 1 — 游戏胜率。
- Cumulative: 设为 TRUE,因为这个问题被定义为 CDF
因此,要在 50% 的尝试中获得代币,玩家必须拥有至少 71.36% 的游戏胜率。
结论
对于任何涉及随机性的游戏,扎实的概率知识都会派上用场。在这里,我们展示了如何应用概率概念来正确计算在不同条件下获胜的几率。了解所有可能的结果及每个结果发生的概率,有助于做出更明智的决策,并希望能帮助你获得胜利。
来源:
维护你的特征库的质量
图片由作者提供
特征库的基本概念以及如何和为什么你应该监控它们的一些提示
·
关注 发表在 Towards Data Science ·5 分钟阅读·2023 年 3 月 30 日
--
自 2017 年 Uber 首次介绍这一概念以来,特征库作为一种支持数据科学家和机器学习工程师定义、发现和访问高质量数据的工具,已经稳步获得了越来越多的关注。
图表由作者提供
从特征工程到特征库
在机器学习项目中,原始数据被收集、清理、格式化并以数学方式转换为称为“特征”的数据。特征在模型生命周期的许多不同阶段是必需的,包括实验、模型训练和模型服务,以从部署在生产管道中的模型中获取预测。
一旦特征被计算出来,我们可以开始通过尝试不同的建模技术和特征集来开发我们的模型。
当模型被训练时,它们会自动发现特征数据中的模式,将这些模式以数学方式编码,然后使用这些信息做出明智的预测。
当模型最终确定后,它会被部署到生产环境中,消耗特征数据以重新训练和生成预测。模型预测通常是批量生成的,或在某些情况下是实时生成的。
特征存储基础
特征存储可以被认为是一个预计算特征的中央存储。这个数据存储服务于机器学习项目中的每一个步骤。
作者提供的图表
作者提供的图表
组织利用特征存储来简化数据和机器学习生命周期中的一些事情。
集中数据
-
特征存储提供了一个一站式商店,存储了从不同来源收集并存储在中央位置的数据。
-
如果没有特征存储,机器学习项目的原始数据通常需要从公司内多个不同的数据源收集,甚至来自供应商或第三方。这意味着数据科学家必须识别并访问多个数据源。
清理数据
-
在将个人可识别信息(PII)或机器学习工作流中不需要的敏感数据存储到特征存储之前,可以将其移除。
-
如果没有特征存储,数据科学家将不得不在不需要敏感数据的情况下访问敏感数据或创建自己的方法来去除敏感数据。
在模型之间共享特征
-
相同的特征通常可以在不同的用例中的多个模型中使用。特征存储计算这些特征一次,并使其在所有机器学习项目中可用。数据科学家可以随着时间的推移向这个特征库中添加更多特征,以建立一个供其他团队重复使用的特征库。
-
如果没有特征存储,许多相同的特征计算将被重新编写到不同的模型和管道中。这迫使数据科学家执行浪费的重复工作,以重新计算可能已经存在于类似管道中的特征,并且使得维护特征计算的一致性变得困难。
提供统一的特征接口
-
特征存储的一部分是对数据本身的标准化推理和共享的特征编码器,帮助在在线和离线应用中强制执行一致的结果。
-
如果没有特征存储,不同的代码或转换可能导致模型中出现稍微意外或错误的结果。这通常表现为在线和离线特征偏差。
减少特征延迟
-
在线特征存储提供预计算的特征,以支持以低延迟提供实时预测。
-
如果没有特征存储,特征将在推断请求时需要计算,这会导致在请求时需要额外的计算——最终影响应用延迟。
导航特征版本控制
-
特征存储对数据应用版本控制。时间旅行数据快照允许对模型进行时间点分析,或找出数据错误的根本原因。
-
如果没有特征存储,追踪某一时刻特征的确切状态或值可能会很困难。这会使得调试和实验变得具有挑战性,甚至不可能。
特征存储的类型
一般来说,特征存储要么作为独立的第三方工具提供,要么作为更广泛的云服务的一部分提供。大多数使用特征存储的 Arize 客户使用像 Tecton 这样的专用工具,但希望在开源解决方案上构建的团队有几个选项(即 Feast、Feathr)。此外,云服务也可以作为现有堆栈的简便附加功能提供。
第三方工具示例:
-
Feast(开源)
-
Feathr(开源)
-
Tecton
-
Hopsworks
云工具示例
-
Databricks 特征存储
-
SageMaker 特征存储
维护特征存储的质量
特征存储可能会静默失败。当模型崩溃或产生较差结果时,根本原因通常追溯到数据本身。许多常见的机器学习问题可以通过对数据应用正确的监控和质量检查来解决。
如果数据集中在一个特征存储中,维护起来可能更容易。通过对特征存储应用数据质量监控,实践者可以在数据问题影响模型性能之前自动捕捉到这些问题。
有几种常见的数据问题,监控可以带来重大差异(完全披露:我在 Arize 工作,Arize 提供监控工具,但这些最佳实践来源于我的实际经验,同样适用于内部或其他地方构建的监控平台)。
数据质量监控可以捕捉到如缺失值、数据格式变化或意外值(数据基数变化)等问题。ML 可观察性平台可以用来自动检测并警报这些类型的数据质量问题,这些问题在特征数据中很常见。
此示例显示了针对模型特征的%-empty 度量的触发数据质量监控(图像作者提供)
数据漂移监控可以捕捉到由于时间自然变化引起的统计分布偏移。漂移可以通过 PSI、KL 散度等指标来测量。机器学习可观测性平台可以用来自动检测和警报特征数据中常见的统计漂移类型。
此示例展示了生产和训练数据分布之间的预测漂移(图像由作者提供)
此外,通过排查离线和在线特征计算及代码的数据一致性,还可以识别训练-服务偏差。
结论
随着特征存储逐渐进入主流,团队正在制定最佳实践,以增强其在机器学习工作流中的集成。一个关键关注点是管理上游数据质量问题。通过实施数据质量监控和数据漂移检测,团队可以有效地维护其特征存储,同时主动预防模型性能退化。
使用 Seaborn 制作嵌套条形图
原文:
towardsdatascience.com/make-a-nested-bar-chart-with-seaborn-9a9988e30dca
我 快速成功的数据科学
大学足球投票的准确性基准测试
·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 10 月 19 日
--
嵌套条形图示例(作者提供)
嵌套条形图是一种可视化方法,用于比较类别中的多个测量值。其中一个测量值代表次要或背景测量,例如目标或之前的值。主要测量值代表实际或当前值。
次要测量通常以减少的容量呈现,从而为主要测量提供背景。将较宽且较暗的主要条形图放在较窄且较浅的次要条形图上面,能够生成一个美观且紧凑的图表。这也解释了为什么这种图形有时被称为口红条形图。
口红图片由 Onder Ortel 在 Unsplash 上提供
当然,为了使其正常工作,主要的条形图应该永远不要比次要的条形图更长。因此,你将需要使用嵌套条形图来绘制每个类别中递减值的示例,例如房价下降或由于新疫苗导致的疾病率下降。
在这个快速成功的数据科学项目中,我们将查看著名的美联社大学足球前 25 名投票在每个赛季开始时挑选 25 支最佳美国大学足球队的表现。由于赛季前挑选的球队数量不能超过排名中的最终球队数量,这是一种很好的嵌套条形图应用。
AP 大学足球前 25 名投票
AP 每年 8 月或 9 月发布他们的赛季前投票,并在整个赛季中每周更新一次。全国范围内超过 60 位知名体育作家和广播员会为最佳球队投票。
他们首先创建了一个列表,列出了他们认为的 25 支最佳球队(从 133 支球队中选出),并给每支球队分配了投票数,给予最佳球队最多 25 分。然后,AP 将这些投票结合起来,按降序对球队进行排名。在碗赛季和大学橄榄球季后赛结束后,它会发布一份最终排名。
你可以在 AP 网站以及Sports Illustrated、NCAA和Sports Reference等体育网站上找到这些投票结果。为了方便起见,我已经将过去 20 年(2002–2022)的季前赛和最终排名整理成 CSV 文件,并存储在这个Gist中。
代码
以下代码的灵感来自 Oscar Leo 最近关于口红图的文章:
一种数据可视化,当数值越低越好
towardsdatascience.com
在这篇文章中,我们将基于 Oscar 的 Python 代码,设置颜色调色板,一个吸引人的 seaborn 样式,以及一个绘制不同宽度和透明度的水平条形图的函数,这些都是制作嵌套条形图所必需的。我们将调整一些代码,并添加更多代码以加载和准备数据,并进行最终展示。
导入库并设置样式
对于这个项目,我们需要使用 matplotlib 和 seaborn 进行绘图,使用 pandas 进行数据加载和准备。你可以通过搜索install
对于颜色调色板,我选择了 Oscar Leo 文章中推荐的Color Hunt site上的“足球”绿色和棕色。
Seaborn 提供了一个设置运行配置样式参数的方法,这些参数会自动应用于每个图形。如果你希望制作多个具有相同参数的图形,或者想通过将这些细节抽象到另一个单元格或位置来“简化”你的绘图代码,这个功能会很有帮助。显然,如果你使用的是 seaborn 的默认绘图参数,你不需要这些代码。
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
# Set up a color palette of grass greens and pigskin browns:
BACKGROUND_COLOR = '#B5CB99'
TEXT_COLOR = '#FCE09B'
BAR_COLOR = '#B2533E'
LEGEND_COLOR = '#186F65'
# Create a dictionary of display parameters favorable for nested bars:
sns.set(style='white', rc={
'axes.facecolor': BACKGROUND_COLOR,
'figure.facecolor': BACKGROUND_COLOR,
'text.color': TEXT_COLOR,
'axes.spines.left': False,
'axes.spines.bottom': False,
'axes.spines.right': False,
'axes.spines.top': False
})
加载数据
以下代码使用 pandas 的read_csv()
方法从 gist 中加载 CSV 格式的投票数据,然后显示前三行以进行质量控制。
# Load AP Top 25 College poll data:
df = pd.read_csv('https://bit.ly/45yEPtI')
df.head(3)
注:由于我们将比较球队名称,因此最好检查它们是否在各个排名中一致。在这种情况下,一些排名在球队名称后面附上了球队的战绩。这是一个小问题,我已经清理过了,但如果你想在未来扩展这个项目,你应该注意这一点。
准备绘制前 25 结果的数据
我们将从查看季前排名中进入赛季结束时的最终排名的球队数量开始。为此,我们将依赖 Python 的 set
数据类型。
就像在经典数学中一样,set
只能包含唯一值(无重复),并且你可以使用内置函数找到两个集合的交集。这意味着我们可以提取季前排名和最终排名之间共享的项(球队)。
# Initialize a list to store the intersection results:
top_25 = []
# Get unique years from the DataFrame:
unique_years = df['Year'].unique()
# Loop through each year and find the intersection of Final and Preseason Teams:
for year in unique_years:
year_data = df[df['Year'] == year]
# Make sets of the final and preseason teams:
final_teams = set(year_data[year_data['Poll'] == 'Final']['Team'])
preseason_teams = set(year_data[year_data['Poll'] == 'Preseason']['Team'])
# Find the set intersections for each year and append to the top_25 list:
intersection = final_teams.intersection(preseason_teams)
num_right = len(intersection)
top_25.append({'Year': year, 'Finishers': num_right})
# Create a new DataFrame from the list:
df_25 = pd.DataFrame(top_25)
# Add columns for the number of ranked teams and percent predicted correctly:
df_25['Top 25'] = 25
df_25['Pct Right'] = df_25['Finishers'] / df_25['Top 25']
df_25['Pct Right'] = df_25['Pct Right'].apply(lambda x: f'{x:.0%}')
print(df_25)
定义绘制条形图的函数
以下代码定义了一个函数,该函数调用 seaborn 的 barplot()
方法。其参数让你可以控制生成嵌套条形图时使用的参数。例如,你需要坐标轴对象(ax_obj
)以便在同一图中叠加条形,width
使主条比副条宽,以及 alpha
调整每个条形的透明度,使主条更暗。
def add_bars(ax_obj, x, y, width, alpha, label):
"""Plot a seaborn horizontal bar chart (credit Oscar Leo)."""
sns.barplot(ax=ax_obj, x=x, y=y,
label=label,
width=width,
alpha=alpha,
color=BAR_COLOR,
edgecolor=TEXT_COLOR,
orient="h")
绘制前 25 的嵌套条形图
在接下来的代码中,我们设置了一个图形,然后调用 add_bars()
函数两次,调整参数,来生成主条和副条。label
参数用于图例中。
为了使显示更加信息化,我们将使用 bar_label()
方法添加关于季前预测准确率的文本。我们将填充文本到左侧,以确保文本在视觉上与正确的条形相关联。
# Make the display, calling add_bars() twice to nest the bars:
fig, ax = plt.subplots(figsize=(8, 9))
ax.set_title('Number of Teams Starting AND Finishing in \
AP Top 25 College Football Poll', color='k', fontsize=13, weight='bold')
# Plot bars for total number of teams (secondary measure):
add_bars(ax_obj=ax,
x=df_25['Top 25'],
y=df_25['Year'],
width=0.55,
alpha=0.6,
label='Teams in Preseason Poll')
# Plot bars for teams that started AND finished in the Top 25 (primary measure):
add_bars(ax_obj=ax,
x=df_25['Finishers'],
y=df_25['Year'],
width=0.7,
alpha=1,
label='Teams in Preseason AND Final Polls')
# Add informative text stating percent correct:
ax.bar_label(ax.containers[1],
labels=df_25['Pct Right'] + ' correct',
padding=-70)
# Assign a custom x-axis label and legend:
ax.set_xlabel('Number of Teams')
ax.legend(bbox_to_anchor=(1.0, -0.085), facecolor=LEGEND_COLOR);
显示进入最终排名的季前球队数量的嵌套条形图(作者)
在过去 20 年中排名前 500 的球队中,有 313 支季前被选中的球队进入了最终名单,成功率约为 63%。
这算是一个好结果吗?我不太确定。美国大学橄榄球被多个强大程序主宰,这些程序通常出现在最终排名中,因此挑选这些球队相对容易且可靠。然而,一个有趣的观察是,自 2011 年以来,成功率呈下降趋势。
自 2011 年以来预测结果的一般下降趋势(作者)
这个趋势可能是巧合,也可能是多个因素的结果,包括游戏规则的变化、会议调整、NIL(名称、图像和肖像)支付的引入以及转会门户的开放。
准备绘制前 4 结果的数据
从 2014 年开始,全国大学体育协会(NCAA)采用了四队淘汰赛来决定第一级橄榄球碗分区的国家冠军。让我们重新审视之前的代码,选择前 4 名球队,以查看排名对最终冠军的选择效果如何。这里有个剧透:季前赛排名在过去 20 年中只正确选择了 2 次最终冠军,成功率仅为 10%!
# Filter the original DataFrame to teams ranked 4 or better:
df_4 = df[(df['Rank'] <= 4)].copy()
# Initialize a list to store the intersection results:
top_4 = []
# Loop through each year and find the intersection of Final and Preseason Teams:
for year in unique_years:
year_data = df_4[df_4['Year'] == year]
# Make sets of the final and preseason teams:
final_teams = set(year_data[year_data['Poll'] == 'Final']['Team'])
preseason_teams = set(year_data[year_data['Poll'] == 'Preseason']['Team'])
# Find the set intersections for each year and append to the top_4 list:
intersection = final_teams.intersection(preseason_teams)
num_right = len(intersection)
top_4.append({'Year': year, 'Finishers': num_right})
# Create a new DataFrame from the list:
df_final_4 = pd.DataFrame(top_4)
# Add columns for the number of ranked teams and percent predicted correctly:
df_final_4['Top 4'] = 4
df_final_4['Pct Right'] = (df_final_4['Finishers'] / df_final_4['Top 4'])
df_final_4['Pct Right'] = df_final_4['Pct Right'].apply('{:.0%}'.format)
print(df_final_4)
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(10, 10))
ax.set_title('Number of Teams Starting AND Finishing in \
Top 4 of AP College Football Poll', color='k', fontsize=14, weight='bold')
add_bars(ax_obj=ax,
x=df_final_4['Top 4'],
y=df_final_4['Year'],
width=0.55,
alpha=0.6,
label='Top 4')
add_bars(ax_obj=ax,
x=df_final_4['Finishers'],
y=df_final_4['Year'],
width=0.7,
alpha=1,
label='Finishers')
ax.bar_label(ax.containers[1],
labels=df_final_4['Pct Right'] + ' correct',
padding=3)
ax.set_xticks(range(5))
ax.set_xlabel('Number of Correct Final Four Predictions',
fontdict={'size': 16});
这里需要注意的一点是,这次我们必须将条形标签填充到右侧。问题在于 2010 年和 2013 年没有任何季前赛球队进入最终排名。如果我们像之前的显示那样将标签填充到左侧,注释将“掉出边缘”,并覆盖 y 轴值。正如你可以想象的那样,短条和长条的组合不适合注释嵌套条形图。
团队在 AP 大学橄榄球排名中开始和结束于前 4 名的嵌套条形图(作者提供)
在过去 20 年中,80 支进入前 4 名的球队中,季前赛排名大约识别了 42%。在 2011 年和 2020 年中,它准确地预测了 4 中的 3 个。在 2010 年和 2013 年中,它的预测全都错误。
总结
嵌套条形图是一种干净且紧凑的方式,用于比较一个测量值始终低于另一个测量值的分类数据。通过包含条形标签来提供额外信息,你可以轻松将这些图表转化为有吸引力的信息图,传达意义和信息。
谢谢!
感谢阅读,未来请关注我更多的快速成功数据科学项目。
使用 Seaborn 制作打卡图
原文:
towardsdatascience.com/make-a-punchcard-plot-with-seaborn-ee8097bee4e1
快速识别周期性趋势
·发布在 Towards Data Science ·阅读时间 6 分钟·2023 年 9 月 17 日
--
带有时间卡的打卡钟(图像由 Hennie Stander 提供,来源于 UnSplash)
打卡图,也称为表格气泡图,是一种用于突出数据中周期性趋势的可视化类型。它以一个固定的矩阵或网格格式显示数据,通常由一周的天数与一天的小时构成。圆圈代表行和列交点的数据点,其大小传达数据值。颜色可以用来包含额外的信息。
表格气泡图(图像由作者提供)
“打卡”这个名字暗指过去工人用来记录上下班时间的“时间卡”。
要构建打卡图,你需要时间戳数据。在这个快速成功数据科学项目中,我们将使用 Kaggle 数据集来跟踪在华盛顿特区租借自行车的时间。
数据集
Kaggle 上的华盛顿特区自行车共享数据集包含了 2011 年和 2012 年在华盛顿特区Capital bikeshare system租借自行车的每小时和每日数据[1]。这些数据在 CC0 1.0 许可证下发布。有关数据集内容的详细信息,请访问readme 文件。
为了方便,我已经将这些数据下载到一个公共的 Gist。
安装库
除了 Python,你还需要 pandas 数据分析库和 seaborn 绘图库。你可以通过以下命令安装它们:
conda install pandas seaborn
或
pip install pandas seaborn
代码
以下注释代码是在 JupyterLab 中编写的,并由单元描述。
导入库和加载数据
在导入 matplotlib 和 seaborn 以进行绘图以及 pandas 以进行数据分析后,我们将把租赁数据的 CSV 文件读入 pandas DataFrame,只保留季节、工作日、小时和数量(租赁次数)这几列。
工作日以数字形式存储(以 0 代表星期日)。为了便于阅读,我们将这些数字映射到星期的名称,并创建一个新的列,称为“Day”来保存这些名称。此外,为了提高可读性,我们将“hrs”和“cnt”列重命名,将“Hours”大写,以便用于图例标签。
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
# Load the data:
df = pd.read_csv('https://bit.ly/3raML6h',
usecols=['season', 'weekday', 'hr', 'cnt'])
# Define a dictionary to map numerical values to day names
day_mapping = {0: 'Sunday', 1: 'Monday',
2: 'Tuesday', 3: 'Wednesday',
4: 'Thursday', 5: 'Friday',
6: 'Saturday'}
# Map weekday numbers to names and create a new column:
df['Day'] = df['weekday'].map(day_mapping)
df = df.rename(columns={'hr': 'Hour', 'cnt': 'count'})
df.head(3)
创建夏季月份的数据框
对于这个分析,我们将重点关注夏季,这将提供关于自行车娱乐和商务使用的良好示例。数据集中季节用数字标记,夏季标记为 2。
首先,我们将创建一个仅包含夏季数据的新 DataFrame,然后使用 pandas 的 groupby()
和 sum()
方法按天和小时汇总租赁数据。由于一天有 7 天,每天有 24 小时,因此这个新 DataFrame 将包含 168 行。
# Create a new DataFrame for the summer season:
df_summer = (df[(df['season'] == 2)].copy()
.groupby(['Day', 'Hour'])['count']
.sum().reset_index())
制作打卡图
为了制作打卡图,我们将使用 seaborn 的 scatterplot()
方法。圆形标记的大小由“count”列控制。你可以调整 sizes
和 figsize
参数,以找到视觉上令人满意的组合。
虽然我们可以将小时转换为 datetime,以便“10”变为“10:00”,但我觉得这样会使 x 轴显得拥挤,而没有太大价值。
此外,星期几是按字母顺序绘制的。我更喜欢这样,因为它将周末的天数分组在一起,而不是在图的顶部和底部分开显示。它还将星期五和星期一相邻绘制,这有助于轻松比较工作周开始和结束时的行为。
fig, ax = plt.subplots(figsize=(8, 5))
ax = sns.scatterplot(data=df_summer,
x='Hour',
y='Day',
size='count',
color='k',
alpha=0.7,
sizes=(1, 300))
ax.legend(loc='right',
shadow=True,
bbox_to_anchor=(1.2, 0.8),
ncol=1)
ax.set_xticks(range(24))
ax.set_xticklabels(list(range(24)))
ax.set_title('Washington D.C. Summer Bike Share Rentals (2011-12)');
# Optional code to add a grid:
# sns.set_style('whitegrid')
# ax.grid(True)
# Optional code to save figure as an image:
# plt.savefig('file_name.png', bbox_inches='tight', dpi=600)
初始打卡图(作者)
这是一个看起来很酷的图,有几个容易区分的模式。首先,周末的租赁行为与工作周明显不同。其次,工作日的变化很小。星期五和星期一,这两个相邻的日子,显示出非常相似的趋势。第三,工作周期间的早晚高峰时段非常明显。
我们可以通过突出显示如高峰时段和周末等事件,使这个图更容易阅读。
突出显示高峰时段
根据互联网的说法,华盛顿特区的高峰时段是早上 6:00 到 9:00 和晚上 4:00 到 7:00。为了突出这些时段,在之前的代码中,将 scatterplot()
方法的 alpha
参数提高到 1
,然后添加以下代码到底部并重新运行。
# Add shading for rush hour:
ax.text(x=6, y=0.6, s='Rush Hour', c='firebrick')
ax.axvspan(xmin=6, xmax=9, facecolor='red', alpha=0.3)
ax.text(x=16, y=0.6, s='Rush Hour', c='firebrick')
ax.axvspan(xmin=16, xmax=19, facecolor='red', alpha=0.3);
突出显示高峰时段的打卡图(作者)
突出显示周末
为了突出显示周末的天数,添加下面的代码并重新绘制图形。
# Add shading for weekend:
ax.axhspan(ymin='Sunday', ymax='Saturday', fc='grey', alpha=0.3)
ax.text(x=1, y=2.6, s='Weekend', c='k');
高亮显示高峰时段和周末的打卡图(作者)
尽管这个图表很有用,但很难判断哪些天的租赁率最高和最低。为了检查这一点,我们可以为夏季制作一个新的数据框,只聚合日期。
# Create a new dataframe for summer season:
df_summer_days = (df[(df['season'] == 2)].copy()
.groupby(['Day'])['count'].sum().reset_index())
df_summer_days = df_summer_days.sort_values('count')
print(df_summer_days)
现在我们可以定量判断不同日期之间的差异。
sns.barplot(data=df_summer_days, x='Day', y='count', color='grey');
夏季自行车租赁总数与星期几(作者)
解释和使用数据
在打卡图中一个有趣的观察是,下午高峰时段的自行车租赁量多于早晨高峰时段。这些额外的骑行者可能代表了外出办事或休闲骑行的人群。
在工作周内,进行自行车维护的最佳时间是上午 9:00 到下午 4:00,这时通勤者都在工作。为了增加收入,你需要分析在非高峰时段降低租金,以激励自行车使用。
从“夏季日”数据框中,我们可以看到自行车在星期六的租赁量最多,而星期天则有所下降。这提供了一个机会,通过促销或降价来增加星期天的自行车使用量。同样,工作周内的自行车租赁量稳步上升,再次提示我们需要在周初激励租赁。
总结
打卡图是一种有趣的数据可视化方式。就像传统的时间卡可以让你监控员工的工作习惯一样,打卡图帮助你一眼识别周期性趋势。
在这个项目中,我们使用了 seaborn 的scatterplot()
方法来制作打卡图。seaborn 的一个优点是它建立在 matplotlib 之上,可以利用 matplotlib 的高级自定义选项。在这种情况下,我们通过使用阴影和文本注释来强调重要的时间段,如周末和高峰时段,从而协助数据分析过程。
引用
- Fanaee-T, Hadi, 和 Gama, Joao, “事件标记结合集成检测器和背景知识”,《人工智能进展》(2013): 页码 1–15,Springer Berlin Heidelberg,doi:10.1007/s13748–013–0040–3。
谢谢!
感谢阅读,请关注我,未来将有更多快速成功数据科学项目发布。
使用 GPT-3 创建文本总结器
原文:
towardsdatascience.com/make-a-text-summarizer-with-gpt-3-f0917a07189e
使用 Python、OpenAI 的 GPT-3 和 Streamlit 的快速教程
·发表于 Towards Data Science ·阅读时间 11 分钟·2023 年 1 月 23 日
--
照片由 Ed Robertson 提供,来源于 Unsplash
我们很可能还处于语言模型的早期阶段,未来几年将会有很多有用的集成方案开发出来。我最近对文本总结很感兴趣,并且发现 OpenAI 提供的解决方案在这方面表现出色。
本教程的目标是向读者介绍 OpenAI 生态系统,并给出一个从构思到部署的实际构建示例。我们将通过构建一个使用 OpenAI 的 GPT-3 模型来总结文本的 Streamlit 应用,并将其部署到 Streamlit Cloud 来实现这一目标。
如果你有兴趣了解如何在 Python 中使用语言模型,这是一个很好的第一步。如果你有兴趣构建自己的集成方案,可以将其作为模板来开发自己的想法。
尽管一些 Python、Git/Github 和命令行的知识将是有用的,但本教程可以作为一个指南,帮助新手在学习这些内容的过程中了解一些基本知识。
步骤 1:在 Github 上设置 git 代码库,并在本地克隆它
如果你还没有 Github 账户,你需要注册一个。登录后,创建一个新的代码库,命名为 text-summarizer
(你可以取任何你想要的名字,但我会用这个名字)。
一旦代码库创建完成,你可以在本地克隆它并导航到项目:
git clone <git clone url>
cd text-summarizer
我们可以创建一个新的分支来进行工作:
git branch initial_build
git checkout initial_build
步骤 2:设置 .gitignore 文件
我们需要一个 API 密钥来使用 OpenAI API,并且需要确保不会不小心将其提交到我们的代码库中。你可以像这样创建文件:
echo ".streamlit/" >> .gitignore
这确保了 .streamlit
目录中的任何内容都不会被添加到你的代码库中。我们还没有创建这个目录,但我喜欢先设置好。
步骤 3:设置虚拟环境并安装软件包
这是创建虚拟环境的命令:
python -m venv venv
现在你可以激活环境了:
source venv/bin/activate
这个应用需要两个软件包:openai
和 streamlit
。首先,让我们升级 pip:
pip install --upgrade pip
现在我们可以安装软件包了:
pip install openai streamlit
步骤 4:设置 secrets.toml
对于本地开发,我们将 API 密钥存储在名为 secrets.toml
的文件中,并将其放在 .streamlit
目录中。假设你在项目根目录,可以输入以下命令:
mkdir .streamlit
cd .streamlit
touch secrets.toml
一旦我们在下一步中生成了 API 密钥,你可以将其存储在这个文件中。
步骤 5:获取 OpenAI API 密钥
如果你还没有 OpenAI 账户,可以在这里创建一个账户。创建账户并登录后,你可以点击右上角的名字图标,选择查看 API 密钥
。
你可以按下显示为 Create new secret key
的按钮。复制弹出窗口中的密钥,并将其粘贴到 secrets.toml
文件中,如下所示(不要忘记将 API 密钥放在引号中):
OPENAI_KEY="<paste-your-key-here-with-quotes>"
如果你已经用完了免费的 OpenAI 额度并转为付费账户,设置使用限制是个好主意。由于我们将这个应用公开发布,你需要确保不会意外花费超出预期的费用。
步骤 6:构建前端
这个应用将非常简单。它将包含一个文本区域,用户可以在其中输入文本,下方的第二个文本框将显示总结后的文本。我们还将添加一个提交按钮。
要开始,我们将在项目根目录下创建一个名为 app.py
的新文件:
touch app.py
在文本编辑器中打开 app.py
,并输入以下内容:
import streamlit as st
st.title("Text Summarizer")
input_text = st.text_area(label='Enter full text:', value="", height=250)
st.button("submit")
output_text = st.text_area(label='Summarized text:', value='', height=250)
这是一个简单的代码片段,用于创建我们应用的骨架。即使你对 Python 或 Streamlit 一无所知,这段代码也应该很容易理解。我们只是创建了一个标题,两个文本框和一个提交按钮。
要运行这个应用,从你的项目根目录输入以下命令:
streamlit run app.py
运行此命令后,浏览器窗口应打开并运行应用。如果没有,你可以在终端中找到 URL,并将其粘贴到浏览器中。这是你应该看到的内容:
来源:作者提供的图片
这展示了 Streamlit 的强大,因为只需几行代码就能做出一些很酷的东西。
步骤 7:开始使用 OpenAI 和 GPT-3
现在我们有了应用的骨架,我们需要让它执行一些操作。以下是我们可以用来让 GPT-3 从提示生成文本的基本语法。
import openai
import os
openai.api_key = os.getenv('API_KEY')
prompt = "ENTER TEXT HERE"
openai.Completion.create(
model="text-davinci-003",
prompt=prompt,
temperature=1,
max_tokens=1000,
)
第一行从你的环境变量中读取 API 密钥。当在 Streamlit 应用的上下文中运行时,os.getenv
将能够访问我们存储在 .streamlit/secrets.toml
顶层的密钥。
然后,我们调用 OpenAI API 的 Completion
端点,并传递以下参数的参数:
-
模型: 我们将使用
text-davinci-003
,这是最先进的模型。 -
prompt: 这可以是任何任意文本。对于文本总结来说,这将是完整的文本。
-
temperature: 这是一个介于 0 和 1 之间的数字,用来定义模型在生成输出时会承担多大的风险。数字越高,模型承担的风险越大。对于文本总结来说,似乎较低的数字会得到更好的结果。你可以对此进行实验。
-
max_tokens: 这定义了输出的最大长度(以 tokens 为单位)。一个 token 大约等于 4 个字符。
你可以在OpenAI 文档中查看所有参数。
如果你想尝试这个代码片段,可以将其保存为.py
文件并运行。请注意,我们存储在.streamlit/secrets.toml
中的 OpenAI API 密钥在 Streamlit 应用之外不可访问,因此你需要将密钥添加为环境变量,或者直接将其粘贴到os.getenv('API_KEY')
中。请务必不要将你的 API 密钥推送到 Github。
这个代码片段最终会被重构成一个由 UI 中的submit
按钮触发的函数。
第 8 步:准备 app.py 以处理来自 OpenAI 的输出
如上所述,submit
按钮将触发总结函数。我们需要将模型的输出存储在一个变量中,以便填充下方的文本框。我们将通过使用 Streamlit 中的session_state
对象来解决这个问题。
首先,我们需要初始化一个状态变量,这可以像这样完成:
if "summary" not in st.session_state:
st.session_state["summary"] = ""
st.session_state
对象是一个 Python dict
,它在应用程序重新运行时保存数据。上面的代码片段是在测试是否已经有summary
的条目。如果summary
不在st.session_state
中,它将被添加,值设置为空的str
。
现在我们需要用summary
的当前状态填充底部文本区域。由于st.session_state
只是一个 Python dict
,所以我们只需将st.session_state["summary"]
传递给第二个st.text_area
实例的value
参数即可。
你的app.py
文件现在应该是这样的:
import streamlit as st
st.title("Text Summarizer")
# initialize state variable
if "summary" not in st.session_state:
st.session_state["summary"] = ""
input_text = st.text_area(label='Enter full text:', value="", height=250)
st.button("submit")
# configure text area to populate with current state of summary
output_text = st.text_area(label='Summarized text:', value=st.session_state["summary"], height=250)
你可以通过检查 UI 来确保代码正常工作。界面看起来应该没有变化,但不应该有任何错误。
第 9 步:构建总结文本的函数
首先,我们要创建一个模块来存储函数,以保持 Streamlit 应用的清洁,你可以从仓库的根目录开始按照这些步骤操作:
mkdir text_summarizer
cd text_summarizer
touch functions.py
现在我们可以将其添加到functions.py
中:
import openai
import streamlit as st
def summarize(prompt):
augmented_prompt = f"summarize this text: {prompt}"
st.session_state["summary"] = openai.Completion.create(
model="text-davinci-003",
prompt=augmented_prompt,
temperature=.5,
max_tokens=1000,
)["choices"][0]["text"]
这与我们在第 7 步中使用的内容变化不大。我们通过在prompt
前添加"summarize this text: "
来告诉模型我们希望它总结文本。然后我们运行openai.Completion.create
并将输出保存到summary
状态变量中。最后部分只是解析响应以仅抓取我们感兴趣的文本。我已经硬编码了参数,但你可以随意实验。
第 10 步:完成 app.py
现在我们已经创建了函数,我们可以完成应用程序。首先,我们需要在顶部导入这个函数:
from text_summarizer.functions import summarize
现在我们需要配置 submit
按钮,以便在点击时触发 summarize
。下面是方法:
st.button(
"Submit",
on_click=summarize,
kwargs={"prompt": input_text},
)
on_click
参数指定了回调中将使用的可调用函数,在这种情况下我们将使用 summarize
。如果你不熟悉 kwargs
,它只是指关键字参数。我们将传递一个包含单个条目的 dict
(因为函数只有一个参数),并将用户在第一个文本区域中输入的 str
传递进去。
我们还需要指定 API 密钥:
openai.api_key = os.getenv('OPENAI_KEY')
app.py
文件现在应该如下所示:
import streamlit as st
import openai
import os
from text_summarizer.functions import summarize
openai.api_key = os.getenv('OPENAI_KEY')
if "summary" not in st.session_state:
st.session_state["summary"] = ""
st.title("Text Summarizer")
input_text = st.text_area(label="Enter full text:", value="", height=250)
st.button(
"Submit",
on_click=summarize,
kwargs={"prompt": input_text},
)
output_text = st.text_area(label="Summarized text:", value=st.session_state["summary"], height=250)
现在你应该能够运行应用程序并开始总结文本。
步骤 11:处理错误
在当前状态下,应用程序对错误的处理不是很好。如果发生错误,用户将会收到暴露了用户不应看到的信息的错误消息。
如果你想模拟错误,可以导航到 .streamlit/secrets.toml
并删除你的 API 密钥的最后一个字母。重新运行你的应用程序,然后尝试总结一些文本。你应该会看到一个错误,显示有关你的 API 的 Traceback 和密钥的一部分。
为了将此隐藏于用户之外,我们可以在两个地方添加类似 try-except
的模式:
app.py
import streamlit as st
import openai
import os
from text_summarizer.functions import summarize
try:
openai.api_key = os.getenv('OPENAI_KEY')
if "summary" not in st.session_state:
st.session_state["summary"] = ""
st.title("Text Summarizer")
input_text = st.text_area(label="Enter full text:", value="", height=250)
st.button(
"Submit",
on_click=summarize,
kwargs={"prompt": input_text},
)
output_text = st.text_area(label="Summarized text:", value=st.session_state["summary"], height=250)
except:
st.write('There was an error =(')
text_summarizer/functions.py
import openai
import streamlit as st
def summarize(prompt):
augmented_prompt = f"summarize this text: {prompt}"
try:
st.session_state["summary"] = openai.Completion.create(
model="text-davinci-003",
prompt=augmented_prompt,
temperature=.5,
max_tokens=1000,
)["choices"][0]["text"]
except:
st.write('There was an error =(')
步骤 12:制作 requirements.txt
由于我们最终将把它部署到 Streamlit Cloud,因此我们需要记录应用程序所需的 Python 包。一个简单的方法是创建一个 requirements.txt
文件。假设你已经激活了虚拟环境并安装了所有必要的包,你可以在仓库的根目录下运行这个命令:
pip freeze >> requirements.txt
步骤 12:提交、推送、合并到主分支
现在代码已经可以工作了,我们可以将文件添加到仓库并提交:
git add app.py requirements.txt text_summarizer/functions.py
然后提交更改:
git commit -m "initial commit"
然后将你的更改推送到远程 Github 仓库:
git push --set-upstream origin initial_build
现在你可以回到 Github 并进入 Pull request 部分。点击 New pull request
并将比较分支设置为 initial_build
。现在你可以简单地创建 Pull request 并合并它。你可以在这里找到有关 PR 的更多详细文档。
步骤 12:部署到 Streamlit Cloud
Streamlit Cloud 是一个分享你的 Streamlit 应用程序的超级简单方法,而且是免费的。首先你需要在这里注册,并连接你的 Github 账户。点击 New app
按钮,并选择 From existing repo
。
当你点击 Repository
文本框时,你应该会看到来自你 Github 账户的仓库列表。选择 text-summarizer
。确保选择了正确的分支,并且 Main file path
为 app.py
。
最后,由于我们小心翼翼地没有将 API 密钥推送到 Github,我们需要以安全的方式存储该密钥。幸运的是,Streamlit Cloud 具有内置的秘密管理器。点击 Advanced settings
,然后你可以在此处粘贴 .streamlit/secrets.toml
的内容。现在你可以按 Deploy
,你的应用将在几分钟内上线。
Streamlit Cloud 将监视你的仓库,并自动部署所有合并的新提交。
你可以在 这里 找到有关 Streamlit Cloud 的更详细文档。
结果:
这是使用本教程介绍的内容作为输入的应用测试:
来源:作者提供的图片
正如你所看到的,模型在总结提示方面做得相当不错。
额外内容:如何使用自定义 URL
Streamlit 和 Streamlit Cloud 具有许多出色的功能,但目前不支持自定义 URL。一种解决方法是托管一个静态网站,将你的应用嵌入为 iframe
。
如果你想尝试,可以在仓库根目录下创建一个名为 index.html
的新文件,并使用此代码:
<!DOCTYPE html>
<html>
<head>
<title>Text Summarizer</title>
</head>
<body>
<div style="text-align: center;">
<iframe src="<URL-TO-YOUR-APP>/?embed=true" width="1000", height="1000"></iframe>
</div>
</body>
</html>
你只需要从你的 Streamlit 应用中复制 URL,并将其粘贴到 iframe
的 src
参数中。确保在末尾添加 ?embed=true
[1]。
现在你可以将此文件存储在云存储桶中,如 AWS S3 或 DigitialOcean Spaces,并将自定义 URL 指向暴露的存储桶。
限制和可能的改进:
这个应用旨在作为一个入门项目,故意功能简 sparse。因此,有一些明显的限制可以改进。
-
赋予用户更多控制权: 你可以添加 Streamlit 小部件,以允许用户为 OpenAI 函数的每个参数指定参数。例如,用户可能希望选择摘要的长度。
-
使处理较长文本成为可能: 单次请求能够处理的文本量有限。为了允许用户总结更长的文本,你可以将输入文本拆分成更小的提示,分别处理每个提示,并将各个响应解析成更长的摘要。我还没有尝试过这方法,这可能需要一些工作才能获得令人满意的结果。
-
引导用户解锁未知功能: GPT-3 非常灵活,用户可以在不显式构建这些功能的情况下添加许多功能。例如,你可以通过稍微修改提示来获得西班牙语响应:
来源:作者提供的图片
根据 Google 翻译(我不会说西班牙语),响应实际上不是西班牙语的总结,而是西班牙语的翻译。虽然这不是预期的行为,但我相信通过稍微调整提示(或在翻译后通过文本总结器传递翻译),你可以获得你想要的结果。
如果你能找出如何使其运作,你可以简单地添加一个 Streamlit 小部件,允许用户选择语言。然后,你需要修改代码,以将用户输入增强到提示中。
结论
你刚刚学会了如何使用 GPT-3 和 Streamlit 制作文本摘要应用程序。这个应用程序显然很简单,但你可以将其作为模板,构建你想尝试的任何新集成。感谢阅读。
资源
[1] discuss.streamlit.io/t/embeding-streamlit-cloud-url-with-iframe/27511
使用 Python 制作美观(且实用)的意大利面图
原文:
towardsdatascience.com/make-beautiful-and-useful-spaghetti-plots-with-python-ec4269d7e8c9
堆叠折线图现在非常热门!
·发布于 Towards Data Science ·9 分钟阅读·2023 年 8 月 16 日
--
照片由 Hunter Harritt 提供,发布在 Unsplash
最近出现了很多关于气候变化的文章,其中许多使用了独特的堆叠折线图来总结几十年的数据。这里是一个来自 气候再分析器 的示例,展示了过去一年半海温远高于平均水平[1]:
全球(60S-60N)海面温度(1981–2023)[1]
这里是来自 Dr. Zachary Labe 的 网站 的类似图表,展示了过去 40 多年南极海冰的范围[2]:
南极海冰范围(1978–2023)[2]
这些图表已经成为信息图的热门选择,如 这篇文章 中所示,但这种受欢迎程度有些令人惊讶[3]。由于很难跟踪这些密集、缠绕的显示中的单条线,它们通常被回避并被贬低为“意大利面”图。
但要成功使用意大利面图有一个秘诀。你必须强调一两条线,并将它们置于所有其他线条的减弱背景中。这种策略让你能将选定的线条放置在整体背景下。它们代表正常结果还是异常值?结果是非常好还是非常差?通过将它们叠加在背景趋势上,故事可以自然而然地展开。
在这个快速成功的数据科学项目中,我们将使用 Plotly Express 绘图库生成前 Antarctic Sea Ice 图表的副本。通过这个代码示例,你应该能够为自己的数据集生成类似的图表。
国家雪冰数据中心
对于数据,我们将使用由国家雪冰数据中心编制的全面公共数据集,该中心是科罗拉多大学博尔德分校环境科学合作研究所(CIRES)的一个部分[4]。该数据集利用卫星图像追踪和监测极地海冰的变化,例如南极洲周围的“光晕”。
2023 年 8 月 10 日海冰范围基于卫星图像[4]
数据以每月和每天的增量提供。为了获得尽可能高的分辨率,我们将查看每日数据。为了方便起见,我已将 CSV 文件下载到这个 gist。此外,用户指南可以在这里找到。
安装库
Plotly Express 是 Plotly 绘图库的高级版本,可以制作美丽且高度互动的可视化。你可以通过 conda 或 pip 安装它。
这是 conda 安装:
conda install -c plotly plotly_express
这是 pip 版本:
pip install plotly
我们还需要 pandas 数据分析包。可以通过以下方式安装:
conda install pandas
或:
pip install pandas
你可能还需要nbformat
用于 Plotly Express。可以通过以下方式安装:
conda install -c conda-forge nbformat
或:
pip install nbformat
代码
以下代码是在 Jupyter Lab 中编写的,并按单元呈现。
导入库
这里是导入。我们使用别名以便于输入:
import pandas as pd
import plotly.graph_objects as go
import plotly.io as plt_io
import plotly.express as px
通常,导入 Plotly Express 就足够了。然而,包括 Plotly 的graph_objects
模块则提供了更多的自定义选项(想想 matplotlib 与 seaborn 的区别)。plotly.io
模块将让我们导入 Plotly 的现成设计模板,从而节省工作量。
加载和准备数据
以下注释的代码使用 pandas 库从 Gist 加载数据并为绘图做准备。这部分包括为一年中的每一天创建一个新的 DataFrame 列(1 月 1 日= 1,12 月 31 日= 365(非闰年),366(闰年))。我们将使用这个Day of Year
列作为折线图中的 x 轴。
# Read sea ice extent file:
URL = 'https://bit.ly/3OtPnnh'
df = pd.read_csv(URL, skiprows=[1])
df.columns = df.columns.str.strip() # Strip any leading white spaces.
df.drop(columns=['Missing', 'Source Data'], inplace=True)
# Combine date columns into a single datetime column:
df['Date'] = pd.to_datetime(df[['Year', 'Month', 'Day']])
# Extract the day of the year from the 'Date' column:
df['Day of Year'] = df['Date'].dt.dayofyear
# Move Date column to the far left:
column_to_move = df.pop("Date")
df.insert(0, "Date", column_to_move)
df.head(3)
绘制堆叠折线图
以下注释的代码绘制了海冰范围的堆叠折线图。每年的线条首先以浅灰色绘制。然后将 2022 年和 2023 年的线条分别以黑色和红色绘制,并加粗线条。目的是展示冰层在过去两年中退缩的程度。
# Plot each year's extent data in a stacked line chart:
fig = px.line(df,
x='Day of Year',
y='Extent',
line_group='Year',
color='Year',
labels={'x': 'Month', 'y': 'Extent'},
title='Antarctic Sea Ice Extent January to December (1978-2023)',
template='plotly_white')
# Customize layout; tickvals represent starting 'day of year' of each month:
fig.update_layout(width=800,
height=650,
legend={'orientation': 'h'},
xaxis_title='',
yaxis_title='Sea Ice Extent (million sq km)',
xaxis={'tickmode':'array',
'tickvals': [1, 32, 60, 91, 121, 152,
182, 213, 244, 274, 305, 336],
'ticktext': ['Jan', 'Feb', 'Mar', 'Apr',
'May', 'Jun', 'Jul', 'Aug',
'Sep', 'Oct', 'Nov', 'Dec']})
# Draw border around the plot:
fig.update_xaxes(showline=True, linewidth=1, linecolor='black', mirror=True)
fig.update_yaxes(showline=True, linewidth=1, linecolor='black', mirror=True)
# Update trace styles to make all lines light gray:
fig.update_traces(line={'color': "lightgray", 'width': 0.75})
# Highlight selected years:
fig.update_traces(patch={'line': {'color': 'black', 'width': 2}},
selector={'legendgroup': '2022'})
fig.update_traces(patch={'line': {'color': 'red', 'width': 2}},
selector={'legendgroup': '2023'})
# Add annotation:
fig.add_annotation(dict(font=dict(color='darkgray',size=15),
x=85,
y=16,
showarrow=False,
text='All years 1978-2023----',
textangle=0,
xanchor='left'))
fig.show()
使用 Plotly Express 构建的南极海冰面积图(图像由作者提供)
哇,真是个美丽的图表!使用 Plotly Express,图例是“动态”的。这意味着你可以点击一个年份,它会从图表中消失。双击一个年份,所有其他线条都会消失。真是解开意大利面图的简便方法!
如果你想使用暗色主题,只需将调用px.line()
方法时的template
参数替换为plotly_dark
。你还需要在fig.update_xaxes()
和fig.update_yaxes()
方法中将边框的线条颜色更改为white
,并在第一次调用fig.update_traces()
时为 2022 年更改线条颜色。结果如下:
使用“plotly_dark”主题的南极海冰面积图(图像由作者提供)
要将这些图表转化为持久的静态图像,只需点击 Plotly 工具栏上的“相机”图标。这将把图表保存为PNG 文件。
使用填充颜色
显示大多数线条为浅色的另一种替代方法是使用实心填充颜色来表示最大和最小的范围。以下是 Zack Labe 的一个吸引人的例子 [2]:
南极海冰异常(1979–2022) [2]
让我们用之前的图表尝试这种方法。第一步是计算按年份中的天(Day of Year
)分组的Extent
列的统计数据。对于每个按天分组的结果,pandas 的agg()
(聚合)方法将帮助我们找到最小值和最大值。我们会将结果保存在一个名为bounds
的新 DataFrame 中。
# Calculate minimum and maximum bounds of "Extent" for each day of the year:
bounds = df.groupby('Day of Year')['Extent'].agg(['min', 'max']).reset_index()
bounds.rename(columns={'min': 'Min Extent', 'max': 'Max Extent'}, inplace=True)
bounds.head(3)
如果你查看原始 CSV 数据文件,你会发现数据是每隔一天收集一次。多年来,每一天都会被采样,但不是每个特定的年份。
CSV 数据文件的开头(图像由作者提供)
因为相邻天的范围值可能来自不同的年份,所以我们可能会得到“颤动”的曲线。这似乎在最小范围值上是一个更大的问题:
不规则采样导致了锯齿状的最小范围曲线(图像由作者提供)
平滑最小范围曲线
为了平滑这条锯齿状的曲线,我们只需要对数据进行 2 天的移动平均。这涉及到在列上调用 pandas 的 rolling()
方法,传入2
,然后调用 mean()
方法。由于第一行之前没有数据,它将被分配一个 NaN
值,所以我们将其从 DataFrame 中删除。
# Smooth the "Min Extent" using a 2-day simple moving average (SMA):
bounds['Min SMA2'] = bounds['Min Extent'].rolling(2).mean()
bounds = bounds.iloc[1:] # Remove first row with NaN for SMA2.
bounds.head(3)
突出显示最近两年
为了在新的图表中轻松突出显示 2022 年和 2023 年的海冰范围,我们将过滤原始 DataFrame(df
),以创建两个新的 DataFrame。
# Filter data for plotting specific years:
df_2022 = df[(df['Year'] >= 2022) & (df['Year'] < 2023)].copy().reset_index()
df_2023 = df[df['Year'] >= 2023].copy().reset_index()
绘制填充图表
以下注释代码生成了填充线图。由于 Plotly 的plotly_dark
模板并不是真正的黑色,前几步创建了一个自定义模板,其中所有元素都是黑色的。这样的控制使用完整的 Plotly会更容易,而不是使用更高层次的 Plotly Express 包。
接下来,我们将使用 Plotly 的go.Scatter()
方法,并传递填充曲线下方区域的参数。对于上面的Max Extent
曲线,我们将使用tonexty
参数填充曲线下方的区域为深灰色。然后,对于平滑的Min SMA2
曲线,我们将使用tozeroy
参数用黑色填充其下方,覆盖之前的深灰色。
# Load the dark template:
plt_io.templates["custom_dark"] = plt_io.templates["plotly_dark"]
# Customize the template using all black background colors:
plt_io.templates["custom_dark"]['layout']['paper_bgcolor'] = '#000000'
plt_io.templates["custom_dark"]['layout']['plot_bgcolor'] = '#000000'
# Customize gridline colors:
plt_io.templates['custom_dark']['layout']['yaxis']['gridcolor'] = '#000000'
plt_io.templates['custom_dark']['layout']['xaxis']['gridcolor'] = '#000000'
# Create a figure:
fig = go.Figure()
# Add filled area traces for max and min extents:
fig.add_trace(go.Scatter(x=bounds['Day of Year'], y=bounds['Max Extent'],
fill='tonexty', fillcolor='darkgray',
line=dict(color='lightgrey', width=0.75)))
fig.add_trace(go.Scatter(x=bounds['Day of Year'], y=bounds['Min SMA2'],
fill='tozeroy', fillcolor='black',
line=dict(color='lightgrey', width=0.75)))
# Add traces for 2022 and 2023
fig.add_trace(go.Scatter(x=df_2022['Day of Year'], y=df_2022['Extent'],
mode='lines',
marker=dict(color='white', size=4),
name='2022'))
fig.add_trace(go.Scatter(x=df_2023['Day of Year'], y=df_2023['Extent'],
mode='lines',
marker=dict(color='red', size=4),
name='2023'))
# Update layout
fig.update_layout(
width=800,
height=650,
template='custom_dark',
title=dict(text='Antarctic Sea Ice Extent (1978-2023)',
font=dict(size=30)),
showlegend=False,
xaxis_title='Month',
yaxis_title='Sea Ice Extent (million sq km)',
xaxis=dict(tickmode='array',
tickvals=[1, 32, 60, 91, 121, 152, 182, 213, 244, 274, 305, 336],
ticktext=['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug',
'Sep', 'Oct', 'Nov', 'Dec']))
# Update x and y axes properties:
fig.update_xaxes(showgrid=False,
ticks="outside",
tickson="boundaries",
ticklen=5)
fig.update_yaxes(showgrid=False,
ticks="outside",
tickson="boundaries",
ticklen=20)
# Add annotations for 2022 and 2023:
fig.add_annotation(dict(font=dict(color='white', size=15),
x=368, y=5.0,
showarrow=False,
text="2022",
textangle=0,
xanchor='left'))
fig.add_annotation(dict(font=dict(color='red', size=15),
x=220, y=15,
showarrow=False,
text="2023",
textangle=0,
xanchor='left'))
fig.show()
使用自定义暗色主题绘制的填充线图(作者提供的图片)
注意灰色填充是如何被“拉下”以连接到最终的 2023 数据点。这展示了 2023 年对海冰记录的巨大影响。
绘制标准差与平均值
许多已发布的海冰图表包含了 1981–2010 年 20 年期间的平均值和 2 倍标准差。使用 pandas 的agg()
方法,轻松可以回到这些年份的原始 DataFrame,并重新生成bounds
DataFrame 以包括平均值和 2 倍标准差的列。结果如下:
显示 1981–2010 年平均值和 2 倍标准差的填充线图(作者提供的图片)
对于高斯分布,两个标准差涵盖了 95%以上的样本。这真正强调了过去两年的极端性质。
总结
在这篇文章中,我们重现了气候数据可视化中流行的堆叠线图技术。Plotly 库不仅使这变得简单,而且生成了可以保存为静态图像的交互式数字图表。
尽管意大利面图常因其复杂性而被贬低,但如果使用得当,它们可以讲述一个有力的故事。在这种情况下,我们使用了强调技术来突出显示几条线,同时削弱其他所有线条。
关于理解意大利面图的其他策略,请访问数据讲述网站。要使用 matplotlib 库制作类似的图表,请查看IceVarFigs/README.md at master · zmlabe/IceVarFigs (github.com)
引用
-
Birkel, S.D.,“每日海表温度,”气候再分析器 (https://ClimateReanalyzer.org),缅因大学气候变化研究所,美国。访问日期:2023 年 8 月 13 日。(气候再分析器内容在创意共享署名 4.0 国际许可证下授权。)
-
Labe, Zachary, 2023 年,“南极海冰,”气候可视化,(
zacklabe.com/
),普林斯顿大学和 NOAA GFDL。访问日期:2023 年 8 月 13 日。 (内容受创意共享署名 4.0 国际许可协议许可)。 -
Readfern, Graham, 2023 年 7 月 28 日,“‘一些奇怪的事情正在发生’:在南极海冰保持历史低位时寻求答案,” (
theguardian.com
)。 -
Fetterer, F., K. Knowles, W. N. Meier, M. Savoie, 和 A. K. Windnagel. 海冰指数,第 3 版。2017 年,由国家雪冰数据中心分发。
doi.org/10.7265/N5K072F8
。访问日期:2023 年 8 月 9 日。
谢谢!
感谢阅读,请关注我,以便未来获取更多快速成功的数据科学项目。
用数据科学让每一分钱的营销投资都发挥作用
当今经济要求我们在广告支出上更加谨慎。幸运的是,找到盈利营销的可行路径可以通过数据来实现。
·
关注 发表在 Towards Data Science ·7 min read·Jun 13, 2023
--
今天的经济与几年前截然不同。如今,我们都被要求以更少的资源来运作。在营销方面,这意味着我们需要在所有活动中做到更高的精准度。幸运的是,盈利营销的可行路径可以通过数据找到。
自从我们开始帮助电子商务品牌通过数据驱动的方法优化他们的营销,我们在第一个月内反复发现了显著的优化机会。我们在电子商务领域不断看到相同的优化模式。在这篇文章中,我将分享我们所学到的,并讨论如何通过数据科学的视角在您的组织中找到营销优化机会。
作者提供的图像
广泛定位对你的初创企业无效,原因如下
大多数电子商务初创企业都有非常明确的目标受众——否则,这个领域将会被大型竞争者的竞争所挤满。然而,当谈到广告时,行业中存在一种神话,即品牌可以依赖广告平台的广泛定位方法来实现最佳结果。
很明显,这种策略并不适用于所有人,因为许多电子商务初创企业告诉我们,他们在让广泛定位发挥作用方面已经苦苦挣扎了很长时间。让我们探讨一下为什么广泛定位可能对大型品牌或几年前开始广告的品牌有效,但对你的初创企业无效。
广告平台利用人工智能、机器学习和数据科学来改进其算法。然而,这些算法在很大程度上依赖大量的训练数据。一方面,有消费者的人口统计信息、行为和兴趣等输入;另一方面,有像购买这样的转化事件,它们作为输出。向广告平台提供更多的转化事件可以帮助它们更好地识别适合你的理想客户画像。
作者提供的图像
在 iOS 14 的隐私变更和 cookies 之前,广告平台可以追踪到更多的消费者数据,例如跨平台的浏览和购买行为。这为它们的算法提供了更多的输入和输出,即使数据较少也能正常运行。
然而,这些隐私变更显著影响了广告平台的跟踪能力,导致了其定位准确性的降低。在现实世界的案例研究中,我们发现,严重依赖消费者行为跟踪的广告平台比其他平台受到的影响更大。因此,这些平台在市场细分上的预算分配可能与品牌的商业案例不一致。
作者提供的图像
对于初创企业来说,由于转化数据有限,这种不匹配现象更为明显。广告平台将超过 50%的广告支出分配给不属于我们客户目标受众的市场细分并不罕见。
聚焦你的广告活动
由于这些行业变化,告知广告平台你的目标受众是实现令人满意广告结果的关键。通过指定目标受众,你可以缩小广告平台算法的搜索和测试范围,缩短学习阶段并提高广告表现。
实际操作中,这些规格可能包括客户的地点、年龄、性别、收入水平、兴趣、喜爱的产品及其他相关因素。你可以评估各个市场细分,确定是否有某些细分市场表现优于其他。
作者提供的图片
我们的客户通过在广告中指定目标受众,迅速获得了盈利结果。即使他们稍后选择扩大目标,他们的广告表现仍保持在高水平,因为广告平台已经为他们的业务案例进行了训练。
如果你使用广泛的定位且无法获得令人满意的结果,尝试通过市场细分分析广告表现。你可能会从这些细分中找到洞察。
通过缩减不盈利的项目来抓住低垂果实
如之前讨论过的,品牌可以通过简单地减少对无效项目的预算来获得巨大的增长机会。在我们的客户案例中,我们看到许多这样的机会被忽视,尤其是当品牌大量投资于营销并有众多广告活动时。
下面展示了一个电子商务品牌的广告活动表现示例。
作者提供的图片
如果你正在经历不盈利的广告,并且无法通过分析高层广告表现数据确定原因,尝试检查单个广告活动,以评估低效广告是否影响了你的利润。
作为经验法则,如果一个广告活动运行很长时间但仍远未盈亏平衡,那么减少对该活动的投资可能是明智的。
加大对高效广告的投入,以利用有利趋势
低垂果实的另一面是高效广告往往被忽视。在许多客户的案例中,我们发现了值得更多投资的盈利广告。这些有前景的信号之前被低效广告活动产生的噪音掩盖了。
为了突出表现优秀的广告,避免错过任何机会,你首先需要整合你的广告活动和广告。
作者提供的图片
例如,如果你有十个广告活动,其中两个持续盈利,三个表现一般,五个远未盈利,你应该集中精力在两个盈利的广告活动上,减少对五个亏损广告活动的投入,并调查三个表现一般的广告活动,看看是否有表现优秀的广告。
不是所有的优化见解从一开始就显而易见。
尽管优化营销的路径有很多,但并不是所有的路径都是立刻明显的。如果超过 50%的广告活动表现不佳,你可能无法从市场细分中获得可操作的见解。
在这种情况下,你应该首先淘汰那些明显表现不佳的营销举措,并根据步骤 1 优先考虑表现良好的举措,缩减无利可图的举措,以及步骤 2,加大对高表现广告的投入。一旦你达到广告活动的一半表现良好,你可以开始分析市场细分,以评估盈利广告的可行路径。
图片来源:作者
在进行更改之前,总是要确认趋势的一致性。
需要注意的是,广告表现数据通常会因为异常事件而失真。例如,某个特定客户的高价值购买可能使该市场细分的表现看起来很有利。然而,这些事件可能不可持续。因此,在基于市场细分中的好或坏信号进行任何更改之前,你应该检查该细分的时间序列,以确保这些信号是一致的。
图片来源:作者
一致性的定义取决于广告支出的规模和一年中的时间。例如,如果你的支出较少,确保趋势在一两周内是一致的,因为性能可能会每天波动。相反,如果你的广告支出较高,几天的广告表现就足以说明问题。
此外,在评估趋势时,你应该对促销期间保持警惕。许多广告在促销期间表现良好,但在正常时期回报可能不高。在非促销期间分析广告表现将提供每个市场细分的准确图景。
如果你发现广告回报低且感觉自己已经测试了所有可能的方案,不要灰心。相反,尝试从数据科学的角度来评估情况。我们曾与许多处于相同情况的品牌合作,通过数据快速识别出可盈利的广告路径。
在我的下一篇文章中,我将深入探讨每种方法,并分享来自实际案例研究的更多收获。敬请期待!
如果你需要数据科学方面的帮助或想讨论上述任何方法,请随时通过LinkedIn或 info@ivyliu.io 与我们联系。下次再见。
通过缓存函数提升 Python 速度:记忆化
原文:
towardsdatascience.com/make-python-faster-by-caching-functions-memoization-4fca250ab5f6
PYTHON 编程
这篇文章讨论了使用 Python 标准库进行记忆化。functools.lru_cache 装饰器让这一切变得如此简单!
·发表于 Towards Data Science ·11 分钟阅读·2023 年 11 月 11 日
--
你可以让 Python 记住函数已经返回的结果,并加以利用。照片由 Kelly Sikkema 在 Unsplash 提供
我们都知道 Python 可能会很慢:
我经常听到 Python 太慢了。真的如此吗?
在 Python 中,通常最耗时间的操作是调用执行复杂过程的函数和类方法。设想一下,如果你需要对相同的参数运行这样的函数两次,它将需要两倍的时间,即使这两个调用的输出完全相同。是否可以仅仅记住这个输出,并在需要时再次使用它?
是的,你可以!这叫做 记忆化,这是编程中的一个常见术语。你可以实现自己的记忆化技术,但实际上你并不需要这样做。Python 提供了一个强大的记忆化工具,而且它在标准库中:functools.lru_cache
装饰器。
尽管记忆化通常非常高效,但在 Python 教科书中经常被忽略,即使是那些描述代码分析和内存节省的书籍也不例外。提到 Python 中记忆化的书籍包括 Julien Danjou 的 Serious Python,Luciano Ramalho 的 流畅的 Python, 第 2 版,以及 Steven F. Lott 的 函数式 Python 编程, 第 3 版。
本文展示了两件事:如何在 Python 标准库中简单地使用记忆化(即使用functools.lru_cache
),以及这种技术的强大之处。然而,这并非全是美好的一面。因此,我们也会讨论使用functools.lru_cache
缓存工具时可能遇到的问题。
使用 functools 模块进行缓存
functools.lru_cache
Python 提供了各种记忆化工具,但今天,我们讨论的是 Python 标准库的一部分:functools.lru_cache
。
LRU 缓存策略代表最近最少使用。在这种策略中,当缓存的大小超过限制时,最少使用的项会被从缓存中移除。
要将其用于函数,请使用@functools.lru_cache
进行装饰:
from functools import lru_cache
@lru_cache
def foo():
print("I am running the foo() function")
return 10
注意foo()
不接受任何参数,这基本上意味着它在一个会话期间只会运行一次:
>>> x = foo()
I am running the foo() function
>>> x
10
>>> y = foo()
>>> y
10
>>> z = foo()
>>> z
10
实际上,在被调用三次之后,函数只运行了一次——但我们得到了相同的输出(10
)三次。
当然,函数记忆化的真正威力体现在接受参数的函数上。这样的函数会对每一组新参数进行运行,但每当之前已经使用过相同的参数时,函数不会重新运行,而是使用记住的输出作为函数的输出。考虑以下函数:
from collections.abc import Sequence
@lru_cache
def sum_of_powers(x: Sequence[float], pow: float) -> float:
output = sum(xi**pow for xi in x)
print(f"Call: sum_of_powers({x=}, {pow=})")
return output
注意sum_of_powers
的类型:
>>> sum_of_powers
<functools._lru_cache_wrapper object at 0x7f...>
所以它的类型不是function
而是lru_cache_wrapper
。让我们看看函数的实际效果:
>>> x = (1, 1, 2)
>>> sum_of_powers(x, 2)
Call: sum_of_powers(x=(1, 1, 2), pow=2)
6
>>> sum_of_powers(x, 3)
Call: sum_of_powers(x=(1, 1, 2), pow=3)
10
>>> sum_of_powers(x, 2)
6
>>> sum_of_powers(x, 3)
10
所以,函数的最后两次调用实际上并没有运行它——但由于记忆化,函数仍然返回了输出。
你可以使用lru_cache
的maxsize
参数来指示缓存中保持的项的数量。默认值为128
。正如 Luciano Ramalho 在他的 流畅的 Python. 第 2 版. 书中解释的那样,为了获得最佳性能,你应该使用maxsize
值为2
的幂。当maxsize
为None
时,缓存可以保存任何数量的对象,更准确地说,是内存允许的数量。请谨慎,因为这可能会导致内存耗尽。
你可以使用一个额外的参数,即typed
,它是一个布尔对象,默认值为False
。当它为True
时,不同类型的相同值会被分别保存。因此,整数值1
和浮点值1.0
会作为不同的项保存。当typed=False
(默认值)时,它们会被作为一个项保存。
有一种方法可以了解装饰了functools.lru_cache
的函数是如何工作的,感谢函数中添加的cache_info
属性。更准确地说,这是一个方法:
>>> sum_of_powers.cache_info
<built-in method cache_info of functools._lru_cache_wrapper object at 0x7f...>
这就是cache_info()
的工作原理:
>>> sum_of_powers.cache_info()
CacheInfo(hits=2, misses=2, maxsize=128, currsize=2)
这意味着以下几点:
-
hits=2
→ 缓存被使用了两次;实际上,我们对x=(1, 1, 2)
和pow=2
使用了函数两次,对x=(1, 1, 2)
和pow=3
也使用了两次,但缓存对于每个x
的值只使用了一次。 -
misses=2
→ 这个输出表示两个具有新参数值的函数调用,因此缓存未被使用。未命中的次数与命中的次数比率越高,缓存的效果越差,因为函数经常使用新的参数值,而很少使用已经使用过的参数值。 -
maxsize=128
→ 缓存的大小;我们将在下文中讨论。 -
currsize=2
→ 缓存中已保留的元素数量。
在一个实际项目中,如果你想使用缓存,最好先进行一些实验,而cache_info()
方法对此非常有帮助。记住,缓存不应该自动用于任何函数;这可能导致时间损失而不是收益,因为缓存本身也需要一些时间。
functools.cache
从 Python 3.9 开始,functools
模块还提供了the [cache](https://docs.python.org/3.9/library/functools.html#functools.cache)
decorator。这只是一个包装器,围绕着functools.lru_cache
,其中maxsize=None
。因此,使用functools.cache
装饰器等同于使用functools.lru_cache(maxsize=None)
。说实话,我认为这不是一个必要的修正——在我看来,标准库不需要这样过于简化的包装器。使用functools.lru_cache(maxsize=None)
有什么问题吗?
注释
用于类方法
函数缓存被认为是一种函数式编程工具,但它也可以用于类方法。与 Python 函数不同,Python 类可以有状态,这就是为什么在类中实现单独的缓存很简单:
>>> class JustOneLetterCachedInside:
... def __init__(self, letter):
... self.letter = letter
... self.cache = {}
... def make_dict(self, n):
... if n not in self.cache:
... print(f"The output for n of {n} is being cached.")
... self.cache[n] = [self.letter] * n
... return self.cache[n]
>>> instance = JustOneLetterCachedInside("a")
>>> _ = instance.make_dict(10)
The output for n of 10 is being cached.
>>> _ = instance.make_dict(10)
>>> _ = instance.make_dict(10)
>>> _ = instance.make_dict(20)
The output for n of 20 is being cached.
>>> _ = instance.make_dict(20)
>>> instance.cache.keys()
dict_keys([10, 20])
这是一种非常简化的缓存方法,而且在更改类实例中的self.letter
属性后将无法工作。不过,这个问题很容易解决,例如,可以通过将self.cache
设为一个以字母为键的字典的字典来解决。我将实现留给你作为练习。
这是一个手动实现的示例,但我们也可以使用functools.lru_cache
装饰器。与函数的用法一样简单:只需在类定义中装饰方法即可:
>>> class JustOneLetter:
... def __init__(self, letter):
... self.letter = letter
... @lru_cache
... def make_dict(self, n):
... return [self.letter] * n
>>> instance = JustOneLetter("a")
>>> _ = instance.make_dict(10)
>>> _ = instance.make_dict(10)
>>> _ = instance.make_dict(10)
>>> _ = instance.make_dict(20)
>>> _ = instance.make_dict(20)
>>> instance.make_dict.cache_info()
CacheInfo(hits=3, misses=2, maxsize=128, currsize=2)
正如你所看到的,这与函数的使用方式完全相同,所以当你看到这样的需求时,不要犹豫,也为类方法使用functools.lru_cache
装饰器。
仅支持可哈希参数
标准缓存使用哈希表进行映射。这意味着对于具有不可哈希参数的函数,它将不起作用。因此,例如,你不能对以下类型的对象使用缓存:列表、集合和字典。
让我们深入分析上述函数sum_of_powers()
。它的x
参数的通用类型是Sequence[float]
。collections.Sequence
并未说明其是否可哈希;实际上,遵循该协议的一些类型是可哈希的,例如tuple
,但其他一些则不是,例如list
。注意:
>>> sum_of_powers((1, 2, 3), 2)
Call: sum_of_powers(x=(1, 2, 3), pow=2)
14
>>> sum_of_powers([1, 1, 2], 2)
Traceback (most recent call last):
...
TypeError: unhashable type: 'list'
>>> from typing import Dict
>>> class Dicti(Dict[str, int]): ...
>>> @ lru_cache
... def foo(x: Dicti) -> int: return len(x)
>>> foo(Dict({"a": 1}))
Traceback (most recent call last):
...
TypeError: Type Dict cannot be instantiated; use dict() instead
仅供参考,静态类型检查器可以指出使用不可哈希参数调用的缓存装饰函数。这种调用是正常的:
来自 Visual Studio Code 的截图。函数调用时使用了一个可哈希的元组,没有静态错误。图像由作者提供
但这个例外:
来自 Visual Studio Code 的截图。Mypy 指出静态错误,表明尝试缓存一个使用列表(不可哈希)的函数。图像由作者提供
纯函数和非纯函数
你可能听说过,记忆化应该仅用于所谓的纯函数,即没有副作用的函数。副作用是指在后台进行的操作;例如,改变全局变量,从数据库中读取数据,记录信息。
通常,非纯函数确实不应缓存,但也有一些例外。例如,想象一个生成报告并将其保存到文件中的函数,基于提供的函数参数值。缓存该函数意味着只生成一次这些参数值的报告,而不是每次函数调用时都生成一次。一般来说,通过缓存可以节省 I/O 操作的成本。然而,在这种情况下,我们必须确保该函数的副作用在后续调用中不会发生变化。例如,如果一个函数会用完全相同内容的新文件覆盖旧文件,但操作时间很重要。
实际上,我们已经观察到具有副作用的函数的缓存。我们在上面做过,当我们缓存打印消息到控制台的函数时。正如你可以回忆的,这些函数只在第一次调用时打印消息,而在使用缓存时不再打印。这正是我们所说的:当副作用不重要时,你可以使用缓存;当副作用重要时,你应该避免,因为你可能会面临副作用只在函数第一次调用时被观察到,而在后续调用中没有观察到的风险。
性能
我不会展示通常用于缓存的示例,即计算阶乘的函数。相反,我将使用一个非常简单的函数,它使用 dict comprehension 创建一个简单的字典。
我将使用以下代码:
import perftester
from functools import lru_cache
from rounder import signif
def make_dict(n):
return {str(i): i**2 for i in range(n)}
@lru_cache
def make_dict_cached(n):
return {str(i): i**2 for i in range(n)}
n = 100
perftester.config.set_defaults(
which="time",
Repeat=1,
Number=int(1_000_000 / n)
)
t1 = perftester.time_benchmark(make_dict, n=n)
t2 = perftester.time_benchmark(make_dict_cached, n=n)
print(
"Benchmark for {n}",
f"Regular: {signif(t1['min'], 4)}",
f"Cached: {signif(t2['min'], 4)}",
f"Ratio: {signif(t1['min'] / t2['min'], 4)}"
)
你可以对不同的n
值运行这个脚本,n
作为两个基准函数的参数。很容易猜到实验背后的假设:n
值越大,即两个函数创建的字典越大,缓存效果应该越明显。
我们很快就会看到是否如此。我们将对以下n
值运行脚本:1
、10
、100
、1000
、10_000
和 100_000
。运行次数需要调整,以避免测试时间过长——但你可以自己用更多的运行次数进行实验;为此,请更改以下片段:Number=int(1_000_000 / n)
。
你可能会想知道为什么我没有创建一个循环来在脚本的一次运行中运行所有基准测试。这是因为每个实验应该是独立的;否则,我们可能会冒险使后续实验的结果受到偏见。最好是单独运行基准测试,不要在脚本的同一次运行中将它们结合起来。
脚本打印每个函数的最佳结果以及一个额外的指标:缓存函数比未缓存函数快了多少次。下面,我将只展示这个比率:
1: 4.6
10: 26.6
100: 222.7
1000: 551.7
10000: 100.0
100000: 9.2
如你所见,缓存的性能非常出色,但在某个点上(这里是10_000
的n
值)性能显著下降;对于更大的n
,性能则急剧下降。我们能解决这个问题吗?
不幸的是,我们不能。我们可以在这个函数中做的唯一更改是选择maxsize
,但在这里没有用,因为缓存只保留一个元素:给定n
值的字典。
因此,我担心在处理如此大项的缓存时,我们无法提高functools.lru_cache
的性能。如你所见,这不是缓存技术的问题,而是缓存本身的问题。
我们的性能实验教会了我们两件重要的事情:
-
使用
functools.lru_cache
进行缓存可以显著提高性能。 -
缓存非常大的对象可能会降低性能,而相比之下缓存较小的对象性能更佳。
让我们实现一个简单的自定义缓存,看看它是否会出现相同的情况。这将是一个过于简化的缓存工具,是我在实际项目中不会决定使用的。
这就是代码:
import perftester
from rounder import signif
MEMORY: dict = {}
def make_dict(n):
return {str(i): i**2 for i in range(n)}
def make_dict_cached(n):
if n not in MEMORY:
MEMORY[n] = {str(i): i**2 for i in range(n)}
return MEMORY[n]
n = 1000000
perftester.config.set_defaults(
which="time",
Repeat=1,
Number=int(1_000_000 / n)
)
t1 = perftester.time_benchmark(make_dict, n=n)
t2 = perftester.time_benchmark(make_dict_cached, n=n)
print(
f"Benchmark for {n}",
f"Regular: {signif(t1['min'], 4)}",
f"Cached: {signif(t2['min'], 4)}",
f"Ratio: {signif(t1['min'] / t2['min'], 4)}"
)
结果如下:
1: 3.4
10: 18.3
100: 184.2
1000: 726.0
10000: 82.4
100000: 6.7
它们与之前的结果有些不同,但趋势是一样的。这表明,functools.lru_cache
使用的策略并不是导致性能显著下降的原因。最可能的原因是输出的大小(这里是字典)。
结论
缓存是一个极好的工具,可以显著提高性能。好的一点是,它学习使用的时间不长,而且使用起来也很简单。
Python 标准库通过 functools.lru_cache
装饰器提供缓存功能。它通常能完全满足你的需求,但有时其局限性可能使其无法使用。最显著的限制似乎是无法使用不可哈希的参数。另一个限制是缓存应仅用于纯函数;然而,正如我们上面讨论的,这并不总是如此。
我们还没有完成缓存的讨论。在这篇文章中,我们探讨了 Python 标准库工具,但在未来的文章中,我们将讨论 PyPi 缓存工具,如 cache3 和 cachetools。我们将比较它们与 functools.lru_cache
的性能,并考虑它们的优缺点。
感谢阅读。如果你喜欢这篇文章,你也可能会喜欢我写的其他文章;你可以在 这里 查看。如果你想加入 Medium,请使用下面的推荐链接:
[## 使用我的推荐链接加入 Medium - Marcin Kozak
作为 Medium 会员,你的一部分会费将分配给你阅读的作者,你可以完全访问每个故事……
medium.com](https://medium.com/@nyggus/membership?source=post_page-----4fca250ab5f6--------------------------------)
用 UTF-8 让你的图表更出色
原文:
towardsdatascience.com/make-your-charts-great-with-utf-8-f1ec9dcc97d0
在 Plotly Express 中使用自定义图标
·发表于 Towards Data Science ·阅读时长 5 分钟·2023 年 6 月 14 日
--
Python 的主要图形库可以开箱即用地生成美观的图表,但它们主要是为探索性数据分析、专业报告和科学文章设计的。它们对于普通公众和其他非技术人员可能显得有些枯燥。
在这个快速成功的数据科学项目中,我们将探索一种使用现成图标来为你的图形增添趣味的方法,这些图标可以被视为文本。具体来说,我们将使用流行的Plotly Express库制作一个条形图,显示每个国际象棋棋子在一次移动中可以控制的理论最大方格数(假设棋子在一个空棋盘的中心)。
为了使这个信息图表更具吸引力,我们将用 UTF-8 字符集中可用的符号装饰条形图。
装饰条形图的示例(图片由作者提供)
什么是 UTF-8?
根据维基百科的说法,“UTF-8 是一种用于电子通信的可变长度字符编码标准。由 Unicode 标准定义,其名称源自 Unicode(或通用编码字符集)转换格式—8 位。UTF-8 是万维网(及互联网技术)的主要编码方式。”
使用 UTF-8 编码,每个你可能想用的字符,例如π符号或字母“A”,都被分配了一个独特的代码。除了常见的文本字符,UTF-8 还包括各种类型的图标,从笑脸到喷气式飞机再到蜗牛。这些图标在注释 Plotly Express 图表时可以被视为文本。
查找 UTF-8 图标
你可以在这个 网站 上搜索 UTF-8 图标。与其搜索每个单独的棋子,不如搜索“chess”。这将返回如下页面,列出兼容的图标。
搜索“chess” 的 Unicode 符号结果(作者提供的图片)
如果你点击白骑士图标,你将看到如下屏幕。
点击白骑士图标的结果(作者提供的图片)
这里是有趣的部分。要使用这个图标,只需高亮并复制大图像。然后,你可以直接将此图像粘贴到你的 Python 代码中,正如我稍后将演示的那样。无需输入实际的 UTF-8 代码。
Plotly Express 库
为了制作图表,我们将使用 Plotly Express,这是 Plotly 图形库的高级版本。这个库抽象了制作图表中的许多繁琐工作,让你轻松生成具有丰富内置功能的漂亮图形。
Plotly Express 需要 Plotly 作为依赖项。你可以使用 conda 或 pip 安装它。
这里是 conda 安装:
conda install -c plotly plotly_express
这里是 pip 版本:
pip install plotly
代码
以下代码在 JupyterLab 中运行。它以 单元格 的形式展示和描述。
导入库
Plotly Express 旨在与 pandas DataFrame 格式的数据良好配合,因此我们需要同时导入 Plotly Express 和 pandas。Pandas 可以通过 conda install pandas
或 pip install pandas
安装。
import pandas as pd
import plotly.express as px
输入数据
我们将数据输入到两个字典中,这些字典使用棋子的名称作为键。其中一个保存每个棋子控制的方格数量,另一个保存 UTF-8 符号。我们将在 棋子名称 上合并这些字典,因此这两个字典中的名称应该是相同的。
注意你可以直接从之前提到的 UTF-8 搜索页面粘贴图标。这不是很酷吗?你只需将其括在单引号或双引号中,因为它被视为字符串。
squares = {'King': 8,
'Queen': 27,
'Rook': 14,
'Bishop': 13,
'Knight': 8,
'Pawn': 2}
symbols = {'King': '♔',
'Queen': '♕',
'Rook': '♖',
'Bishop': '♗',
'Knight': '♘',
'Pawn': '♙'}
df_squares = pd.DataFrame(squares.items(),
columns=['Piece', 'Max Squares'])
df_squares = df_squares.sort_values(by='Max Squares')
df_symbols = pd.DataFrame(symbols.items(),
columns=['Piece', 'Symbol'])
df_merged = df_squares.merge(df_symbols)
df_merged.head(6)
合并后的 DataFrame(作者提供的图片)
创建图表
Plotly Express 使生成标准图表变得简单,例如条形图、散点图、热图等。在这里,我们将使用 bar()
方法制作条形图。一旦你将 DataFrame 的 名称 传递给方法,你只需提供 列名 以在后续参数中访问数据。这使得代码非常易读。
text
参数捕捉 UTF-8 符号,该符号会自动放置在每个条形的顶部附近。要控制符号的大小,请使用 textfont_size
参数和 update_traces()
方法。marker_color
参数指的是 条形 颜色,不是 符号颜色。
fig = px.bar(df_merged,
x='Piece',
y='Max Squares',
height=550,
text='Symbol')
fig.update_traces(textfont_size=70, marker_color='black')
fig.show() # optional
每个棋子在任何时刻可以控制的理论最大方块数(图片由作者提供)
正如你所见,在 Plotly Express 中制作图表时,UTF-8 图标可以被视为常规文本。这意味着你可以将它们用作文本注释,并将它们放置在你想要的任何位置。在接下来的示例中,我们将一个红色笑脸放置在骑士条上方以演示这个过程。
# Add annotation:
fig.add_annotation(dict(font=dict(color='red',
size=50),
x=0.39,
y=0.45,
showarrow=False,
text="☺",
textangle=0,
xanchor='left',
xref="paper",
yref="paper"))
fig.show() # optional
带有红色笑脸贴在骑士条上的条形图(图片由作者提供)
结果
UTF-8 图标可以为你的图表增添一些趣味,赋予它们一种“信息图表”的感觉。虽然这对于分析工作或科学文章并非必要或甚至不被期望,但在准备新闻通讯、教程、年度报告和其他面向非技术观众的文档时,以这种方式装饰图表是有益的。
谢谢!
感谢阅读,未来请关注我获取更多快速成功数据科学项目。
让你的图表看起来辉煌
一些简单的格式化技巧,使 matplotlib 图表准备就绪
·
关注 发表在Towards Data Science · 12 分钟阅读 · 2023 年 1 月 31 日
--
图片由Luke Chesser拍摄,发布在Unsplash。
图表无疑是向观众传达信息的最佳方式。没有任何疑问——图片胜于文字。根据你询问的人不同,一张图的价值大约等于一千字。
这并不意味着创建一个好的可视化很简单!创建一个强大且外观良好的展览以传达信息是困难的。如果展览是在现场演示中首次亮相,这就更加具有挑战性:观众不仅会专注于所说的内容(希望如此),还会试图理解图表背后的信息,同时形成问题。
我可能会给你展示很多我自己的图表,这些图表证明了制作一个好的图表是多么困难,但我会省略这些创伤。相反,在这篇文章中,我们将看到如何:
-
创建“基础”折线图和条形图。
-
更改标题和标识,以改善图表的视觉效果和信息传递能力。
-
去除杂乱以提高图表的可读性。
-
改变图表的外观,以真正强调一个信息。
我们将借用一些我们上次讨论的技巧,那时我们探讨了如何通过格式化pandas
DataFrames 来改善信息传递和讲故事。我(显然)建议你阅读一下,如果你对展示一些华丽的表格以及优秀的图表感兴趣:
全面指南:格式化 pandas DataFrames | Towards Data Science
让我们开始吧——首先检查如何制作美丽的折线图,然后再尝试绘制条形图。在这两种情况下,我们都将接受我们内心的新年决心,并使用(虚构的)关于运动和训练的数据。
附注:这里的技巧与 matplotlib
有关,这是我在 Python 中绘图的首选包。这并不是说在其他包中,如 seaborn
,不能做到相同的技巧和窍门,只是你可能需要调整方法。
折线图
我们将从折线图开始——简单的可视化,当试图传达时间上的趋势或模式时非常有效。
数据
我已经捏造了一些数据,捕捉了一个虚构调查的受访者比例,该调查询问 18-60 岁男性他们偏好的运动形式。受访者有 4 个选项可供选择:跑步、骑自行车、游泳和混合方案。
附注:通过“捏造一些数据”,我真的指的是“在 Excel 中创建示例数据”。
让我们准备好,查看数据:
# functionality
import pandas as pd
import numpy as np
import os
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import matplotlib.ticker as mtick
# data import
file_location = r'C:\Users\...\Charts'
file_name = r'data.csv'
df = pd.read_csv(os.path.join(file_location,file_name))
# convert date column to datetime
df['Date'] = pd.to_datetime(df['Date'],format = '%d/%m/%Y')
# set date column to index
df.set_index('Date',inplace = True)
df
作者提供的图片
数据集相当简单。让我们从某个地方开始,创建我们的“基础”图表。
基础折线图
这里没有什么华丽的——只是一些matplotlib
:
# plot size and configuration
fig,ax = plt.subplots(figsize = (20,7.5))
# lines
for activity in df.columns:
plt.plot(df.index,df[activity],marker = '^',label = f'{activity}')
# format y-axis
plt.gca().yaxis.set_major_formatter(mtick.PercentFormatter(1,decimals = 0))
# add title and axis labels
plt.title('Exercise types (survey respondents, men aged 18 - 60)')
plt.ylabel('Proportion')
plt.xlabel('Survey date')
# misc - grid and legend
plt.grid(axis = 'both',alpha = 0.45)
plt.legend(loc = 'best',ncol = 1)
# results
plt.show()
……这就给出了:
作者提供的图片
现在,我内心的书呆子可能认为这个可视化展示得完全可以。他可能会说一些类似的话:
-
标题、轴标签和图例条目结合在一起,告诉读者该图表与调查受访者(男性,年龄 18-60 岁)参与某种类型的运动的比例有关。
-
数据随着时间的推移被绘制,所以可能存在某种趋势或模式。
-
事实上,当我们查看图表时,我们看到跑步和骑自行车的受欢迎程度随着时间的推移而下降,而混合模式在这个群体中变得越来越受欢迎。
奇怪的是,这正是我们想传达的信息!我们需要找到一种方法来传达这个信息,而不需要观众做那么多繁重的工作。
通常,最好的方法就是直接告诉观众你想说什么。我们通过使标题更有用来做到这一点,仅在副标题增加可视化价值时才使用副标题。
使用描述性标题
我们将使用text
命令¹给图表添加描述性标题。
我们将尽可能清晰地捕捉信息:
-
标题(即标题)的核心是混合模式训练随时间变得越来越受欢迎
-
副标题(即副标题)是混合模式受欢迎程度的提升源于跑步和骑自行车的受欢迎程度下降。
从代码的角度来看,这看起来像是(为方便阅读格式化):
# plot
fig,ax = plt.subplots(figsize = (20,7.5))
# informative title + subtitle
title = 'Mixed training is gaining in popularity over time'
subtitle = 'Men aged 18-60 are ditching running and
cycling in favour of a mixed training regime'
# add title + subtitle to plot
plt.text(
x = 0.125,y = 0.90,s = title,fontname = 'Arial',
fontsize = 20,ha='left',transform = fig.transFigure
)
plt.text(
x = 0.125,y = 0.86,s = subtitle,fontname = 'Arial',
fontsize = 16,ha = 'left',transform = fig.transFigure
)
# lines
for activity in df.columns:
plt.plot(df.index,df[activity],marker = '^',label = f'{activity}')
# format y-axis
plt.gca().yaxis.set_major_formatter(mtick.PercentFormatter(1,decimals = 0))
# axis labels
plt.ylabel('Proportion')
plt.xlabel('Survey date')
# misc - grid and legend
plt.grid(axis = 'both',alpha = 0.45)
plt.legend(loc = 'best',ncol = 1)
# fiddle with space above chart
plt.subplots_adjust(top=0.8, wspace=0.3)
plt.show()
… 这给出了:
图片由作者提供
我认为这是一个很好的改进,因为信息直接呈现在读者面前。图表本身仍然看起来很忙,因此我们将接下来处理这个问题。
顺便提一下:如果你非常注重细节,你可能会觉得标题和图表之间的间距有点大。现在看来,你可能没错,但继续阅读——我已经为此计划了一些东西。
去除杂乱
杂乱的图表难以阅读。实际上,任何使人分心的东西都会让图表更难以阅读。
在这种情况下,“杂乱”实际上可以指任何东西——坐标轴标签、标记、位置不当的标题,甚至是网格线。我们将查看所有这些元素,但我们将从最大的罪魁祸首——图例开始。
图表的图例可能会非常有争议。一方面,传统观点认为一个好的图表包含一个好的图例:一个可以让读者轻松区分不同数量的图例。
另一方面,如果读者在图表和图例之间来回查看(可能多次),那么图例显然是一种干扰。
我们将通过将图例从几乎独立的图表元素变为集成到可视化中的元素来找到折衷方案。我们将通过注释³每一条单独的线条来做到这一点。
所以我们可以得到这样的效果:
图片由作者提供
不错——我们去掉了图例而实际上没有丧失其任何有用性。
一旦你掌握了要领,注释线条是非常简单的:
# annotate
plt.annotate(
text = 'Running',
xy = (pd.to_datetime('01-01-2022'),df['Run'][-1]),
textcoords = 'offset points',
xytext = (5,-4),fontname = 'Arial',fontsize = 13,color = 'tab:blue'
)
我仍然认为网格线和绘图边框(或“脊柱”)是一种干扰。去除它们很简单:
# grid lines
# keep only toned down vertical lines
plt.grid(axis = 'y',alpha = 0.3)
# turn off spines
plt.gca().spines[['left','right', 'top']].set_visible(False)
图片由作者提供
我们现在真的开始接近目标了!看了一下,我觉得我们需要在标题和图表本身之间增加更多的间隔。因此,我们将添加一条分隔线。
我们还需要为 y 轴添加一些描述。我们可能在删除“杂乱”时过于严厉,因此我们会在这里添加一些内容。引用信息来源也是一种良好的做法,所以我们将添加一个脚注来实现这一点。
图片由作者提供
从代码角度看,这相当简单:
# line between titles and chart
plt.gca().plot(
[0.1, .9], # x co-ords
[.87, .87], # y co-ords
transform = fig.transFigure,
clip_on = False,
color = 'k',
linewidth = 1.5
)
# axis description
description = 'Proportion of survey respondents (%)'
plt.text(
x = 0.1,
y = 0.8,
s = description,
fontname = 'Arial',
fontsize = 14,
ha='left',
transform = fig.transFigure
)
# foot note
footnote = "Source: Brad's imagination, January 2023"
plt.text(
x = 0.1,
y = 0.05,
s = footnote,
fontname = 'Arial',
fontstyle = 'italic',
fontsize = 12,
ha = 'left',
transform = fig.transFigure
)
现在我们可以做最后的一步,以真正强调信息——更改颜色。
尽管图表非常漂亮,但彩色线条可能会分散整体信息。因此,为了强调混合训练随着时间的推移越来越受欢迎,我们将通过以下方式使混合训练线条更突出:
-
让它变得非常粗体、加粗,并且是红色的。
-
将其他线条设置为灰色。
图片由作者提供
看,这就是一个出色的图表,信息丰富,易于理解(如果我这么说的话)。
条形图
条形图和柱状图通常用于比较不同的定量或定性数据。当你做少量比较时,它们非常有用,但我认为如果你有许多比较要做,或者你试图可视化时间趋势,它们并不适用。
现在,如果你在谷歌上搜索“条形图与柱状图”,你会发现大量文章解释和概述这两种可视化之间的确切区别。
我不会这样做,因为我诚实地说,我不知道区别。对像我这样的“实践者”来说,这些语义是一种干扰。重要的是知道在某些情况下,水平条形图比垂直条形图更有用。我们稍后会看到一个很好的例子。
数据
让我们创建一些数据来可视化。再次,这将与健身相关,这次捕捉到按时间段访问健身房的比例。为了简单起见,我们的“时间段”变量将大致分为 5 个类别。
# data
df = pd.DataFrame(
{
'Time':['Early morning','Morning','Midday','Afternoon','Evening'],
'Athletes':[0.17,0.075,0.23,0.125,0.4]
}
)
基础条形图
再次,我们将创建一个“基础”图表,以便我们可以改进它。一段简单的代码给我们提供了一个相当标准的图表——没有特别之处,但能够完成任务。
# plot
fig,ax = plt.subplots(figsize = (20,7.5))
# bars
plt.bar(df['Time'],df['Athletes'])
# format y-axis
plt.gca().yaxis.set_major_formatter(mtick.PercentFormatter(1,decimals = 0))
# grid line
plt.grid(axis = 'y',alpha = 0.45)
# labels and title
plt.title('Proportion of athletes training, by time of day')
plt.ylabel('Proportion')
plt.xlabel('Time of day')
plt.show()
图片由作者提供
我们来看看这个图表并修改一些内容。
目前很明显,晚上时段是去健身房的最受欢迎时间。然而,下一次最受欢迎(或最不受欢迎)的时间并不立刻显现出来。因此,我们将更改条形图的绘制顺序。
虽然有些人喜欢按升序排列值(即ascending = True
),我认为我们应该更改图表方向,从上到下绘制最受欢迎的时间。
外观很重要,因此我们将去除图表杂乱并进行样式调整。我们还将增加一些设计元素来提升美感。
最后,我们会通过改变一些颜色来重申信息。
让我们开始吧。
改变方向
…以及一些排序。
没有特别需要强调的内容。DataFrame 已经使用 sort_values
重新排序,我们使用 barh
而不是 bar
来实现水平排列。
这给我们带来了:
图片由作者提供
这已经好多了——看看现在多么容易识别出最繁忙的时间段,并在每个时间段之间进行比较。
读取 x 轴上的精确值有点挑战,因此在改善杂乱和样式时,我们会考虑到这一点。
杂乱与样式
现在进行一些外观上的修整:轴标签、网格线和坐标轴将被移除,我们将让标题发挥作用。我们还将添加条形标签,以便在去掉 x 轴时不会丢失任何信息。
当然,我们总是引用和注明我们的数据来源,因此我们会添加脚注。
除此之外,我们还会进行一些美学上的改动,使图表更吸引眼球。由于我们的条形图没有理由做得那么长,部分样式调整将是一些通用的尺寸调整,但最有效的变化将来自于强调信息的改动。
我们已经看到如何做大部分变化,因此接下来的部分代码量会比较少,展示的新概念。
准备好迎接新的改进版图表了吗?我准备好了!
图片由作者提供
这是一个很好的进展。注意使用条形标签如何让我们完全去掉 x 轴。这是一个小的代码变化:
# plot
fig,ax = plt.subplots(figsize = (20,7.5))
# add bars
bars = plt.barh(df['Time'],df['Athletes'],color = 'k')
# add labels
plt.bar_label(
bars,
labels = [f'{x:.0%}' for x in bars.datavalues],
padding = 10,
fontsize = 14
)
红色图形是矩形和直线的组合。拼凑起来有点麻烦,但我认为它给图表增添了一点风格——相当像《经济学人》杂志的风格。
# add a little graphic flair
# rectangle first
plt.gca().add_patch(
plt.Rectangle(
(-0.05,.95), # location
0.0125, # width
-0.13, # height
facecolor = 'tab:red',
transform = fig.transFigure,
clip_on = False,
linewidth = 0
)
)
# now the line
plt.gca().plot(
[-0.049, .95], # length of line
[.82, .82], # height
transform = fig.transFigure,
clip_on = False,
color = 'tab:red',
linewidth = 3
)
最后但绝对重要的是,我们将改变条形图的颜色,以强调大多数人是在晚上锻炼的消息。
图片由作者提供
这是可能最大的图表变化,它由最小的代码变化创建:我们将颜色列表传递给 barh
的 color
参数,而不是使用单一字符串。
太棒了!
总结和闲聊
我有一个越来越不好的习惯,就是同时总结和喋喋不休。
通过一些示例线图和条形图,我们已经看到描述性标题和视觉技巧如何改善图表的信息传达。我们还看到,去除图表杂乱的元素可以让读者更专注于我们想要传达的信息。
现在,我喜欢多彩的图表,我不羞于承认(显然)。但我确实得承认,调整颜色和策略性使用颜色可以真正提升图表向观众传达信息的能力。
就像演示文稿中的幻灯片一样,你需要真正思考图表想要传达什么,然后给它最好的机会去做到这一点。这可能意味着更改图表类型、更换颜色调色板、添加描述性标题,甚至移除一些图表元素。就像出色的室内设计一样,不要害怕做出大胆的决定。如果情况变得更糟,你总可以重新编写图表代码!
如果你缺乏设计灵感,我推荐查看《经济学人》和《金融时报》这类出版物——它们通常在用优美的图表传达信息方面表现出色。我通过阅读matplotlib
文档和浏览各种 StackOverflow 帖子,学到了很多代码技巧和窍门。
我最后一个——也是可能最重要的——建议是多加练习,然后回顾你的图表。更好的是,请其他人审阅你的图表,看看他们是否“理解”你试图传达的信息。我知道这听起来很无聊,但这确实有帮助。
如果你能读到这里,谢谢你。我希望你阅读这篇文章的体验和我写作时一样愉快(制作图表意外地具有宣泄作用)。我仍在学习和练习如何制作更好的图表,所以任何技巧或窍门都非常感激!
参考资料和资源
一些让我的工作更轻松的官方文档:
让你的 sklearn 模型速度提高最多 100 倍
如何仅通过更改 1 行代码显著减少训练时间
·
关注 发表在Towards Data Science · 4 分钟阅读·2023 年 3 月 16 日
--
照片由Markus Winkler拍摄,发布在Unsplash上。
介绍
使用Intel® Extension for Scikit-learn包(简称sklearnex),你可以加速 sklearn 模型和变换器,同时保持与 sklearn API 的完全兼容。Sklearnex 是一个免费的 AI 加速器,可以让你的 sklearn 代码速度提升 10 到 100 倍。
软件加速是通过使用向量指令、IA 硬件特定的内存优化、线程处理以及对即将推出的所有 Intel 平台的优化来实现的。
在这个故事中,我们将解释如何使用ATOM库来利用 sklearnex 的速度。ATOM 是一个开源的 Python 包,旨在帮助数据科学家探索机器学习管道。如果你想要温和地了解这个库,可以阅读另一个故事。
硬件要求
需要考虑的 sklearnex 附加硬件要求:
-
处理器必须具有 x86 架构。
-
处理器必须至少支持 SSE2、AVX、AVX2、AVX512 中的一种指令集。
-
ARM*架构不支持。
-
Intel®处理器提供的性能优于其他 CPU。
注意: sklearnex 和 ATOM 也可以通过 GPU 加速,但在这个故事中我们不会讨论这一选项。目前,让我们集中讨论 CPU 加速。
示例
让我们通过一个例子来了解如何开始。我们以通常的方式初始化atom,并指定engine
参数。引擎参数规定了使用哪个库来处理模型。选项包括:
-
sklearn(默认)
-
sklearnex(我们在这个故事中的选择)
-
cuml(用于 GPU 加速)
from atom import ATOMClassifier
from sklearn.datasets import make_classification
# Create a dummy dataset
X, y = make_classification(n_samples=100000, n_features=40)
atom = ATOMClassifier(X, y, engine="sklearnex", n_jobs=1, verbose=2)
接下来,调用[run](https://tvdboom.github.io/ATOM/latest/API/ATOM/atomclassifier/#atomclassifier-run)
方法来训练模型。请查看这里以获取支持 sklearnex 加速的模型列表。
atom.run(models="RF")
print(f"\nThe estimator used is {atom.rf.estimator}")
print(f"The module of the estimator is {atom.rf.estimator.__module__}")
训练和验证模型花费了 1.7 秒。请注意,该模型来自于daal4py。这个库是 sklearnex 的后台引擎。
为了比较,我们还训练了另一个随机森林模型,但这次是在 sklearn 而不是 sklearnex 上。我们也可以直接在run
方法中指定engine
参数。
atom.run(models="LR_sklearn", engine="sklearn")
print(f"\nThe estimator used is {atom.rf.estimator}")
print(f"The module of the estimator is {atom.rf.estimator.__module__}")
这次花费了 1.5 分钟,而不是仅仅几秒钟!前者的模型几乎快了 60 倍,而且在测试集上的表现略好。
让我们可视化结果。
atom.plot_results(metric="time")
需要注意的是,这些模型之间没有显著的差异,无论是在性能方面还是在模型做出预测时使用的逻辑方面。后者的情况可以通过特征重要性图(特征重要性相似)和SHAP 决策图(决策模式匹配)来可视化。
atom.plot_feature_importance(show=10)
atom.rf.plot_shap_decision(show=10, title="sklearnex")
atom.rf_sklearn.plot_shap_decision(show=10, title="sklearn")
结论
我们已经学习了如何利用 ATOM 库轻松加速 sklearn 模型的训练。此加速在 CPU 上实现。如何在 GPU 上实现这一点将是未来故事的重点,敬请期待。
欲了解更多关于 ATOM 的信息,请查看该包的文档。如有 bug 或功能请求,请随时在GitHub上提交问题或发送电子邮件给我。
相关故事:
-
Atom:快速探索机器学习管道的 Python 包
-
如何用几行 Python 代码测试多个机器学习管道
-
从原始数据到 Web 应用部署:使用 Atom 和 Streamlit
-
深度学习管道探索简化
-
深度特征合成 vs 遗传特征生成
-
在不到 30 行 Python 代码中从原始文本到模型预测
-
如何制作 40 个交互式图表来分析你的机器学习管道
-
多输出数据集上的机器学习:快速指南
参考文献:
- 所有图表和图像(除特色图像外)均由作者创建。
让你的表格看起来更华丽
简单的格式化技巧,让你的 pandas DataFrames 准备好展示
·
关注 发布于 Towards Data Science ·13 分钟阅读·2023 年 1 月 10 日
--
图片由 Pierre Bamin 提供,来源于 Unsplash。
2023 年带来了我们所有人希望和梦想的一切,新的一年也带来了其他东西:年终报告和演示文稿。
不管我们喜欢与否,视觉印象都很重要。虽然图形通常比一系列数字更易于理解,更能传达信息,但有时我们仍不得不使用数据表。但这并不意味着表格不能漂亮!
现在,公平地说——Jupyter notebooks 确实能生成看起来不错的表格,但仅使用pandas
,我们可以进行相当多的自定义,从而真正使表格属于我们,并且——更重要的是——传达我们的信息。
在这篇文章中,我们将看到如何:
-
格式化日期
-
格式化绝对数值
-
格式化货币
-
格式化比率
-
导出格式化的 DataFrames
让我们开始吧,假设我们在 Widget 公司,向内部销售团队展示两种类型的部件的销售数据。
附注:当然,你不会仅在一月份报告结果、预测和总结,因此我在这里总结的技巧希望能更具长远性。
数据
我们将从创建一个虚拟数据集开始。这里没有什么花哨的——只是使用pandas
和numpy
进行一点模拟。
import pandas as pd
import numpy as np
# simulated data for widget A
df_a = pd.DataFrame(
{
'Month':pd.date_range(
start = '01-01-2012',
end = '31-12-2022',
freq = 'MS'
),
'Quotes':np.random.randint(
low = 1_000_000,
high = 2_500_000,
size = 132
),
'Numbers':np.random.randint(
low = 300_000,
high = 500_000,
size = 132
),
'Amounts':np.random.randint(
low = 750_000,
high = 1_250_000,
size = 132
)
}
)
df_a['Product'] = 'A'
# simulated data for widget B
df_b = pd.DataFrame(
{
'Month':pd.date_range(
start = '01-01-2012',
end = '31-12-2022',
freq = 'MS'
),
'Quotes':np.random.randint(
low = 100_000,
high = 800_000,
size = 132
),
'Numbers':np.random.randint(
low = 10_000,
high = 95_000,
size = 132
),
'Amounts':np.random.randint(
low = 450_000,
high = 750_000,
size = 132
)
}
)
df_b['Product'] = 'B'
# put it together & sort
df = pd.concat([df_a,df_b],axis = 0)
df.sort_values(by = 'Month',inplace = True)
df.reset_index(drop = True,inplace = True)
到目前为止,都很简单。让我们计算一些“有趣”的统计数据——平均销售金额和产品转化率:
# average sale
df['Average sale'] = df['Amounts'] / df['Numbers']
# conversion
df['Product conversion'] = df['Numbers'] / df['Quotes']
……这给我们带来了以下(缩写的)DataFrame:
作者提供的图片
这里有一个相当典型的总结表格,我们可能希望向利益相关者展示:数字、货币金额和一些比率都表示在时间跨度中。
让我们逐步处理数据集,逐个格式化元素。
为了简洁,接下来的 DataFrame 图片将仅显示前六行数据,但任何代码片段都适用于整个 DataFrame。
格式化日期
首先是日期列。
格式化方面可以说没有什么错误,但可以更好。例如,由于所有的月度数据都是以每月第一天为基准的,因此保留每个Month
条目的日期元素意义不大,因为它对读者提供的信息很少。
敏锐的读者会注意到我没有使用美国常见的日期格式;当然,我建议使用在你所在地区普遍接受的格式。
让我们只显示年份和月份:
# remove day of month from month column
df.style.format({'Month':'{:%Y-%m}'})
作者提供的图片
小改动,但已经好多了!
现在,我们可以通过使用每个月的名称而不是月份数字来进一步提高可读性,而且我们可以不必更改基础数据。
# use full name of month
df.style.format({'Month':'{:%B %Y}'})
作者提供的图片
现在可能有点啰嗦——让我们改用缩写(例如“Jan”代替“January”)。
# use abbreviated month name
df.style.format({'Month':'{:%b %Y}'})
作者提供的图片
简短而简洁。我想重新处理年份和月份数字,目标是比YYYY-MM
格式更具可读性,但又不使用名称(无论是缩写还是完整名称)。所以,让我们重新格式化数据,但不使用YYYY-MM
格式,而是使用年份和月份数字,由字母“M”分隔。
# year and month number, separated by letter 'M'
df.style.format({'Month':'{:%Y M%m}'})
作者提供的图片
还不错,但我更喜欢简写的名称,因此我们将使用它。
附注:如果你对使用上面提到的 YYYY M-MM
格式感兴趣,但不喜欢前导零的外观,可以使用格式字符串 {:%Y M%#m}
来去掉那个讨厌的零。
用千位分隔符格式化数字
在这里,我们通过用逗号分隔成千上万的 Quotes
和 Numbers
,体验了相当直接的格式化过程。
然而需要注意的是,如果我们还希望保留应用于 Month
列的格式(我们希望保留),那么我们需要扩展格式化字典。
# thousands separator for absolute numbers
df.style.format(
{
'Month':'{:%b %Y}',
'Quotes':'{:,.0f}',
'Numbers':'{:,.0f}'
}
)
图片来源:作者
这看起来还不错——更容易感知这些绝对数字的规模。
接下来是货币金额,在这里重要的是要反映出数字的大小和货币的面值。
格式化货币
小工具公司刚好在一个使用 £ 符号的国家生产和销售它的产品(希望比我赚取 £ 的国家更温暖、更阳光明媚)。
让我们在表格中体现这一点,并提醒自己:
-
从整体上看,使用小数点可能有点过多
-
在更低的层次上——比如说,平均销售值——使用小数点可能会很有用。
因此,我们将货币格式化添加到 Amounts
和 Average sale
的格式化字典中:
# currency formatting
df.style.format(
{
'Month':'{:%b %Y}',
'Quotes':'{:,.0f}',
'Numbers':'{:,.0f}',
'Amounts':'£{:,.0f}',
'Average sale':'£{:,.2f}'
}
)
图片来源:作者
有多种显示货币的方式,改变格式也很简单——例如,如果我们想将 Average sale
显示为数字后跟货币符号,我们可以这样做:
# different currency representation
df.style.format(
{
'Month':'{:%b %Y}',
'Quotes':'{:,.0f}',
'Numbers':'{:,.0f}',
'Amounts':'£{:,.0f}',
'Average sale':'{:,.2f} (£)'
}
)
图片来源:作者
就个人而言,我更喜欢 货币符号 -- 数字
格式,但当然我们也可以应用数字格式化,并将货币符号包括在列名中——例如 Average sale (£)
。
格式化百分比
另一个相当直接的格式化步骤,当以百分比形式表示比率时,比用浮点数表示要更容易查看。
我再次建议根据使用场景调整确切的格式。如果不需要很高的精度,在百分比中使用较少(或没有)小数可以使表格看起来更整洁。
无论如何,我们的格式化代码现在变成了:
# percentage formatting
df.style.format(
{
'Month':'{:%b %Y}',
'Quotes':'{:,.0f}',
'Numbers':'{:,.0f}',
'Amounts':'£{:,.0f}',
'Average sale':'£{:,.2f}',
'Product conversion':'{:.2%}'
}
)
图片来源:作者
我认为我们的数据现在已经格式化得相当不错了。让我们进入影响表格整体外观和感觉的更改。
隐藏索引
我觉得默认的 DataFrame 索引很丑。好了,我说出来了。
这些索引显然很重要,但它们可能会显得很碍眼,甚至更糟,成为干扰。设身处地考虑一下利益相关者的感受:你看到的表格不仅有多余的行号,而且编号从零开始!如果你不习惯使用 Python,这就显得相当奇怪了。
当然,我们可以用多种方法来解决这个问题。我们可以将 Month
设为 DataFrame 的索引(更好的是,设置为 Month x Product
多级索引)。或者,为了展示的美观,我们可以将索引设置为一个空字符串数组。
或者,我们 可以 只是隐藏显示中的索引。这样更干净,并且不干扰 DataFrame 的索引。
# suppress the index
df.style.format(
{
'Month':'{:%b %Y}',
'Quotes':'{:,.0f}',
'Numbers':'{:,.0f}',
'Amounts':'£{:,.0f}',
'Average sale':'£{:,.2f}',
'Product conversion':'{:.2%}'
}
).hide_index()
图片作者提供
现在我们在接近目标。
条件格式化
高亮显示数据中的某些元素是传达信息或引起观众对数据某一方面关注的好方法。
我们将从高亮显示行开始,如果该行的某个元素满足给定条件——在这种情况下,高亮显示所有包含与产品 A 相关的信息的行。
我们分两步完成这个:
-
定义函数
highlight_product
,当给定条件满足时返回一个字符串(即,如果该行与指定产品相关)。该字符串包含一个格式命令,我们将传递给Styler
。 -
生成的格式命令通过
apply
命令传递。
# function to conditionally highlight rows based on product
def highlight_product(s,product,colour = 'yellow'):
r = pd.Series(data = False,index = s.index)
r['Product'] = s.loc['Product'] == product
return [f'background-color: {colour}' if r.any() else '' for v in r]
# apply the formatting
df.style\
.apply(highlight_product,product = 'A',colour = '#DDEBF7', axis = 1)\
.format(
{
'Month':'{:%b %Y}',
'Quotes':'{:,.0f}',
'Numbers':'{:,.0f}',
'Amounts':'£{:,.0f}',
'Average sale':'£{:,.2f}',
'Product conversion':'{:.2%}'
}
).hide_index()
图片作者提供
因此,与产品 A 相关的行得到了高亮显示。非常简单。
实际上,这也使得表格更具可读性,因为现在更容易区分这两种产品类型。
专业提示:我们可以向 pandas
* 提供颜色十六进制代码,使得定制格式化更为便捷——这个特定的蓝色其实是我最喜欢的 Microsoft Excel 颜色之一。将高亮颜色定制为匹配公司的颜色方案是一个非常棒的细节。*
我们当然可以使用不同的条件测试。这是对 Average sale
应用条件阈值的示例:
# function to highlight rows based on average sale
def highlight_average_sale(s,sale_threshold = 5):
r = pd.Series(data = False,index = s.index)
r['Product'] = s.loc['Average sale'] > sale_threshold
return ['background-color: yellow' if r.any() else '' for v in r]
# apply the formatting
df.iloc[:6,:].style\
.apply(highlight_average_sale,sale_threshold = 20, axis = 1)\
.format(
{
'Month':'{:%b %Y}',
'Quotes':'{:,.0f}',
'Numbers':'{:,.0f}',
'Amounts':'£{:,.0f}',
'Average sale':'£{:,.2f}',
'Product conversion':'{:.2%}'
}
).hide_index()
图片作者提供
……我们可以看到 Average sale > £20
的行被非常明显地高亮显示为黄色。
我们可以将格式化更改限制为 DataFrame 的一个子集,而不是对整行应用高亮。例如,我们会对 Product conversion
应用两个阈值测试,如果条件满足则更改字体颜色和粗细。
如常,我们需要首先指定一些格式化函数:
# functions to change font colour based on a threshold
def colour_threshold_lessthan(value,threshold,colour = 'red'):
if value < threshold:
return f'color: {colour}'
else:
return ''
def colour_threshold_morethan(value,threshold,colour = 'green'):
if value > threshold:
return f'color: {colour}'
else:
return ''
# functions to change font weight based on a threshold
def weight_threshold_lessthan(value,threshold):
if value < threshold:
return f'font-weight: bold'
else:
return ''
def weight_threshold_morethan(value,threshold):
if value > threshold:
return f'font-weight: bold'
else:
return ''
# apply the formatting
df.style\
.apply(highlight_product,product = 'A',colour = '#DDEBF7', axis = 1)\
.applymap(colour_threshold_lessthan,threshold = 0.05,subset = ['Product conversion'])\
.applymap(weight_threshold_lessthan,threshold = 0.05,subset = ['Product conversion'])\
.applymap(colour_threshold_morethan,threshold = 0.2,subset = ['Product conversion'])\
.applymap(weight_threshold_morethan,threshold = 0.2,subset = ['Product conversion'])\
.format(
{
'Month':'{:%b %Y}',
'Quotes':'{:,.0f}',
'Numbers':'{:,.0f}',
'Amounts':'£{:,.0f}',
'Average sale':'£{:,.2f}',
'Product conversion':'{:.2%}'
}
)\
.hide_index()
图片作者提供
不错!
注意我们在这里使用 applymap
而不是 apply
,并使用 subset
参数将格式化限制为 DataFrame 的 子集。
我想在这里做两个更改:
-
在代码方面,我会使用 lambda 函数,而不是定义如此多类似的辅助函数。那样代码会更简洁。
-
我会犹豫不决地展示一个表格,其中某些单元格具有不同的字体格式,除非格式应用于行或列的总计。
表格级别的更改:文本对齐和标题
只是一个简单的整体改进——我们会对齐文本,并为我们的 DataFrame 添加标题(明确的标识总是好的!)。
# align the text
df.style\
.set_properties(**{’text-align’:’center’})\
.apply(highlight_product,product = 'A’,colour = '#DDEBF7’, axis = 1)\
.applymap(lambda u: 'color: red' if u < 0.15 else '’,subset = [’Product conversion’])\
.applymap(lambda u: 'font-weight: bold' if u < 0.15 else '’,subset = [’Product conversion’])\
.applymap(lambda u: 'color: green' if u > 0.2 else '’,subset = [’Product conversion’])\
.applymap(lambda u: 'font-weight: bold' if u > 0.2 else '’,subset = [’Product conversion’])\
.format(
{
'Month’:’{:%b %Y}’,
'Quotes’:’{:,.0f}’,
'Numbers’:’{:,.0f}’,
'Amounts’:’£{:,.0f}’,
'Average sale’:’£{:,.2f}’,
'Product conversion’:’{:.2%}'
}
)\
.set_caption(’Sales data <br> Produced by Team X’)\
.hide_index()
……这产生了:
作者提供的图像
专业提示:在指定多行标题时,换行符应使用 <br>
而不是 \n
。
将所有内容整合在一起
现在让我们把这些技巧和窍门结合起来,包括在 DataFrame 底部添加一列“总计”。
列总计需要一些额外的工作:
-
我们通过取和得到原始总计。
-
平均值和转换需要使用新的总计重新计算。
-
对于
Product
列没有有意义的“总计”计算,因此我们将该元素替换为空字符串。 -
我们将
Month
条目替换为缺失值,以便可以覆盖它而不使事情变得复杂(狡猾!)。
# create a total "row" - i.e. column total
total = df.sum()
total['Month'] = pd.NaT
total['Product'] = ''
total['Average sale'] = total['Amounts'] / total['Numbers']
total['Product conversion'] = total['Numbers'] / total['Quotes']
total = total.to_frame().transpose()
作者提供的图像
然后,通过使用 pd.concat
将总计添加到 DataFrame 中非常简单。我们还编写了一个快速函数来使总计行中的文本加粗。
# function to highlight the total row
def highlight_total(s):
r = pd.Series(data = False,index = s.index)
r['Month'] = pd.isnull(s.loc['Month'])
return ['font-weight: bold' if r.any() else '' for v in r]
把所有内容放在一起,使用我们新连接的 DataFrame d
:
# stack and reset index
d = pd.concat([df,total],axis = 0)
d.reset_index(drop = True,inplace = True)
# apply formatting
d.style\
.set_properties(**{'text-align':'center'})\
.apply(highlight_product,product = 'A',colour = '#DDEBF7',axis = 1)\
.apply(highlight_total,axis = 1)\
.format(
{
'Month':'{:%b %Y}',
'Quotes':'{:,.0f}',
'Numbers':'{:,.0f}',
'Amounts':'£{:,.0f}',
'Average sale':'£{:,.2f}',
'Product conversion':'{:.2%}'
},
na_rep = 'Total'
)\
.set_caption('Sales data <br> Produced by Team X')\
.hide_index()
… 完成!
作者提供的图像
不错!
专业提示:注意我们如何在 Month
中使用了缺失值,以及 na_rep
参数来用字符串填充缺失值。这是一种在日期时间列中使用字符串的简单方法。
导出魔法
如果你仍然在为工作演示使用 PowerPoint,那么你可能会熟悉将 Jupyter 笔记本的屏幕截图粘贴到幻灯片中的痛苦。
这显然非常低效(更不用说无聊了),但有一个好消息——你可以使用代码将表格导出为图像。更好的消息是,有一个方便的 Python 包可以做到这一点——介绍 dataframe_image
²:
import dataframe_image as dfi
# style the table
d_styled = d.style\
.set_properties(**{'text-align':'center'})\
.apply(highlight_product,product = 'A',colour = '#DDEBF7',axis = 1)\
.apply(highlight_total,axis = 1)\
.format(
{
'Month':'{:%b %Y}',
'Quotes':'{:,.0f}',
'Numbers':'{:,.0f}',
'Amounts':'£{:,.0f}',
'Average sale':'£{:,.2f}',
'Product conversion':'{:.2%}'
},
na_rep = 'Total'
)\
.set_caption('Sales data <br> Produced by Team X')\
.hide_index()
# export the table to PNG
export_destination = r'C:\Users\...\Presentations'
dfi.export(
d_styled,
os.path.join(
export_destination,
'styled_dataframe.png'
)
)
作者提供的图像
结果的图像与我们在笔记本中看到的略有不同,但它看起来非常好(如果我这么说的话!)。
总结
让我们总结一下。
这篇文章有点偏重于图像,但这对于演示如何使用 pandas
格式化日期、绝对数字、货币和比率是必要的。我们还使用了 dataframe_image
将我们花哨的表格导出为图像格式。
我们在这里已经覆盖了相当多的内容,但这绝不是详尽的讨论。使用一些更高级的功能和一点 HTML 可以制作出非常酷的表格;不幸的是,我对 HTML 有点生疏,所以很快就达到了我的能力极限。我建议查看官方文档³,以了解实际可能性;你还会在那里找到对 Styler
的更好解释,以及 apply
和 applymap
之间的区别。
最后,尽管我对 dataframe_image
是新手,但我发现它真的很简单使用,并且它确实如宣传所述功能实现。nbconvert
用户可能永远不需要使用它,但它是一个非常有用的工具。
希望这篇文章能为你的一年精彩的数据表格奠定基础。告诉我你是如何传达(表格)信息的——我总是很乐意了解更好的沟通方式!
参考资料和资源
日期格式的有用总结:datetime — 基本日期和时间类型 — Python 3.11.1 文档。
通过这些技巧和窍门使你的表格数据在 CLI 中脱颖而出
提高可读性:在 CLI 中展示数据集的技巧
·发表于 Towards Data Science ·6 分钟阅读·2023 年 4 月 12 日
--
几天前,我想帮助我的父亲解决一个问题。他需要尽快聚合、筛选和展示一些数据。事实上,他每次都打印数据(大约 10 页!!)并手动搜索数据!我看到了他的困难,决定立即帮助他。
对于像我这样能够分析数据的人来说,这并不难:数据已经是 Excel 格式,所以 Jupyter Notebook 和 Pandas 是完美的选择。
问题是我不为我的父亲工作。而且,我们住在不同的城市,每隔几周才见一次面。所以,我需要给他一个可以使用的工具,其特点如下:
-
简单使用。
-
如果他输入错误,不要让电脑爆炸。
这就是我决定创建一个可以通过 CLI 管理的小程序的原因。我想创建的是简单的:用户通过命令行输入,然后程序在终端中显示与 Pandas 相同的所有数据。
在这篇文章中,我将展示如何通过 CLI(命令行界面,即终端)展示表格数据。如果你不知道的话,终端就是我们要使用的工具。我们将创建一个简单的项目,让你立刻上手 Python,并讨论我使用的库。
创建表格数据
首先,让我们创建一些表格数据以便练习:
import pandas as pd
# Create data
data = {"fruit":["banana", "apple", "pear", "orange"],
"color":["yellow", "red", "green", "orange"],
"weight(kg)":[0.3, 0.1, 0.1, 0.2]
}
# Transform data to data frame
df = pd.DataFrame(data)
这些是我们的数据:
我们创建的表格数据。图片由作者提供。
我们创建了一些包含水果信息的表格数据,特别是:水果名称、颜色和重量(以千克为单位)。
现在,为了使其“更真实”,我们可以将其保存到 Excel 文件中,如下所示:
# Save data frame to xlsx file
df.to_excel("fruit.xlsx")
**NOTE:** This methodology of saving files that Pandas gives us is very useful.
For example, we can use it to convert CSV files into XLSX; We did it
in this article here.
问题及其解决方案
现在,我们将创建一个简单的过滤器,如果我们稍微使用一下 Excel,就不需要 Python。我面临的问题更复杂,但这里我们故意简单地创建它:我们的目标不是展示这种方法是否比其他方法更好。这里我们展示的是如何通过 CLI 显示表格数据,一个简单的示例就能完成任务。
所以,假设这是我们的问题:我们希望用户输入一个水果的名称,我们的程序返回所选水果的所有特征。我们还希望过滤器在某种程度上是“智能”的,以便如果用户输入“pea”,它将显示与“pear”相关的特征。
为此,在 Pandas 中我们可以使用方法str.contains()
。让我们在 Jupyter Notebook 中尝试一下:
import pandas as pd
# Import data
df = pd.read_excel("fruit.xlsx")
# Filter for pear
data_frame = df[df["fruit"].str.contains("pea")]
# Show filtered data
data_frame.head()
我们得到:
过滤后的数据。图片由作者提供。
仔细阅读:我们故意将“pea”写成了一个错别字,以确保 Pandas 无论如何都会返回数据。它确实如预期般返回了数据。
现在,我们必须面对另一个问题:用户通过 CLI 的干预。据我所知,在这些情况下,我们可以使用两种不同的方法:我们可以使用内置函数input
,或者可以使用库argparse
。
如果你错过了,我写了一篇关于如何在数据科学中使用argparse
的文章。可以在这里查看:
我知道,这可能很难承认,但……它真的可以节省你的时间!
现在,在这种情况下,我决定使用内置函数input
,因为我认为它更容易使用,在像这样的简单情况下是一个很好的选择。实际上,如果我们只需要通过 CLI 传递一个字符串作为参数,这是完美的选择(你可以在这里阅读文档)。
我们可以这样使用input
函数:
# User input
fruit = input("filter the data for the kind of fruit: ")
现在,让我们看看这如何工作以及如何返回数据。我们可以使用的代码如下:
import pandas as pd
# User input
fruit = input("filter the data for the kind of fruit: ")
# Import data
df = pd.read_excel("fruit.xlsx")
# Filter for user input
data_frame = df[df["fruit"].str.contains(fruit)]
# Print results
print(data_frame)
**NOTE:**
look at the difference of how we've pasted the arguments in the method
str.contains(). Aboved we've passed "pea" with quotes because we were
searching directly for a string.
In this case, insetead, we have passed "fruit" without quotes because
we have used "fruit" as a variable to invoke the input() function so it
has to be passed as is (with no quotes).
现在,让我们将其保存为fruit.py
,移动到fruit.xlsx
所在的文件夹,并通过终端运行它:
我们通过 CLI 的代码。GIF 由作者提供。
好吧,正如我们所看到的,一切都正常工作。但有一点:我们可以改进可视化吗?如果我们想要更好地展示数据,就像在 Pandas 中一样,会怎样呢?
嗯,我找到的解决方案是使用库 tabulate
(这是文档)。
所以,让我们将 tabulate
添加到我们的代码中,看看会发生什么:
import pandas as pd
from tabulate import tabulate
# User input
fruit = input("filter the data for the kind of fruit: ")
# Import data
df = pd.read_excel("fruit.xlsx")
# Filter for user input
data_frame = df[df["fruit"].str.contains(fruit)]
# Print results
print(tabulate(data_frame, headers='keys', tablefmt='psql'))
然后我们得到了:
我们的 CLI 代码。图片由作者提供。
如我们所见,数据以“表格方式”显示,这样更清晰。此外,正如我们所见,代码可以正确处理拼写错误,如果我们搜索“pea”就像之前在 Jupyter 中那样。
结论
我希望这对你在通过 CLI 显示表格数据时有所帮助。如果你有其他建议,请在评论中告诉我:我总是乐于改进和学习新知识。
-
订阅 我的新闻通讯 以获取更多关于 Python 和数据科学的信息。
-
喜欢这篇文章吗?通过 我的推荐链接加入 Medium:以 5$/月(无需额外费用)解锁 Medium 上的所有内容。
-
在这里找到/联系我: https://bio.link/federicotrotta
-
觉得有用吗? 支持我一下 Ko-fi。
让语言模型更像人脑
原文:
towardsdatascience.com/making-language-models-similar-to-human-brain-b6ea8270be08
神经科学 | 人工智能 | 自然语言处理
在自然语言处理领域,语言模型(LMs)与人脑之间仍存在差距,这激励人工智能(AI)去弥补这一差距。
·发表于 Towards Data Science ·14 分钟阅读·2023 年 3 月 23 日
--
图片由作者使用 OpenAI DALL-E 生成
地球上有数千种脊椎动物物种,但只有一种能够通过语言传达无限的概念。语言传递对人类至关重要,并使他们能够塑造我们所知的故事。
语言模型是否无法匹配这种人类能力?为什么?
一项新的研究尝试基于语言模型和神经科学来回答这个问题。
## 人脑在听语音时的预测编码层级证据 - Nature Human…
最近,在自然语言处理领域取得了显著进展:深度学习算法越来越…
人工大脑和自然大脑如何进行交流?
最近,我们看到 语言模型(LMs)在文本生成、翻译和补全等任务上取得了重大进展。这一切都归功于一个简单却有效的想法:我们可以通过上下文来预测一个词。
尽管这个想法看起来如此简单,但它却是从 BERT 到 ChatGPT 的所有语言模型的基础。每个变换器都基于 嵌入 和 自注意力(这使我们能够将句子中的词关联起来)。
现状、最新消息、影响因素以及正在发生的变化。所有这些都在一篇文章中。
多年来,作者们尝试检测这些模式的激活与人脑对语言和文本的反应之间是否存在映射关系。多项研究表明,这种映射是线性的,取决于模型预测序列中未来单词的能力。
最近,一项研究显示,甚至可以利用人工智能可视化一个人观察到的图像。表明可以从脑活动记录中重建一个人所见的内容。
研究人员能够使用 fMRI 数据重建图像。
levelup.gitconnected.com](https://levelup.gitconnected.com/stable-diffusion-and-the-brain-how-ai-can-read-our-minds-45398b395ea9?source=post_page-----b6ea8270be08--------------------------------)
人工智能算法是否已经赶上了人类的能力?
不,而且我们对人类语言或相关脑过程的理解也不充分。
然而,人类与这些算法之间仍然存在差距:尽管有大量的训练数据,当前的语言模型在长篇故事生成、总结和连贯对话以及信息检索方面仍然面临挑战;它们未能捕捉到几种句法结构和语义属性,其语言理解仍然肤浅。(来源)
“即使在大量人类上下文和强大的 GPT-2 大语言模型下,Beam Search(大小 32)会导致退化重复(蓝色高亮),而纯采样则导致不连贯的胡言乱语(红色高亮)”来源(这里)
示例展示了即使是最新的语言模型在识别嵌套短语中的主语及其依赖关系时仍然存在问题。无论如何,作者指出,单纯优化下一词预测往往会导致生成不一致且平淡的序列(有时还会出现重复循环)。
预测编码理论 可能为为什么语言模型仍然落后于人类语言提供了解释。这到底是什么?
根据预测编码假说,皮层的结构实现了一种自上而下的预测算法,不断地预测即将到来的感觉刺激。每个皮层区域都拥有一个环境的内部模型,该模型通过汇总支配过去输入的统计规律生成。 (source)
换句话说,大脑保持(并不断更新)对周围环境的心理模型。大脑有一个空间层次(或概念的层次)表示。实际上,皮层是从最简单到最复杂的层次结构组织的。
因此,人脑并不是预测序列中的下一个词,而是在多个时间尺度和不同的表征层次上进行预测,向上层级递进。
“深度语言算法通常训练以从其紧密上下文中预测单词。与这些算法不同,大脑根据预测编码理论进行(1)长程和(2)层次预测。” 图片来源:here,许可证:here
在语言的背景下,这一理论包含可测试的假设,例如大脑应不断预测一个从音素(什么声音可能出现)到单词甚至短语(可能传达什么意义)的语言表征层次。 (source)
例如,以前的研究表明,当参与者听到短语“从前有一个……”时,可以从脑部记录中追踪到单词“时间”(即使在其被发音之前)。
虽然我们对基本原理有一个大致的了解,但实际过程仍不明确。事实上,我们不知道在语言表达过程中,大脑如何实现多层次的预测。
我们为什么关心这个问题?
更好地理解这一过程是修改大型语言模型的第一步。使大型语言模型更类似于人脑,可以帮助我们减少人类与语言模型之间的差距。第三,正如之前所示,映射大脑与模型之间的关系还可以帮助我们更好地理解模型本身。
如何将模型映射到大脑
自然故事听觉范式和数据来源的示意图。来源:here,许可证:here
第一步是,我们能否将神经网络的激活映射到大脑上?它们之间有关系吗?
作者们从叙事数据集开始。该数据集由 345 名受试者在约 4.6 小时内(超过 40,000 个独特单词)听取 27 个不同故事组成。
作者们定义:
-
w 作为一系列 M 词(受试者听到的故事中的词汇)
-
Y 作为由 w 诱发的 fMRI 记录
-
X 作为用 w (从 GPT-2 第 12 层提取)输入的深度语言模型的激活
首先,作者决定量化 fMRI 听觉(Y)与深度语言算法(X)激活之间的相似性,当模型输入相同的故事时。为此,他们创建了一种所谓的“脑评分”。
作者们取用了患者听到的故事中的相同词序列(数据集中包含了转录,并与 fMRI 记录对齐),并将其用作模型的输入。因此,通过输入网络计算一个词的向量(模型根据一个序列预测下一个词)。
一旦获得这些数据,作者们尝试将 Y 激活与音频故事的反应和 X 模式激活进行映射:
为此,我们在训练集上拟合了线性岭回归 W,以预测 fMRI 扫描结果。然后,我们通过计算预测的 fMRI 扫描和实际 fMRI 扫描之间的 Pearson 相关性来评估这种映射 (source)
换句话说,他们在获得 X 的线性投影后,将 X 和 Y 相关联。
结果与之前的研究一致,表明GPT-2的激活准确映射到分布在大脑两个半球的区域。脑评分在听觉皮层以及前颞叶和上颞叶区域达到了顶峰。
此外,这不仅限于 GPT-2,还适用于分析过的其他变换器模型。换句话说,这种映射可以推广到其他最先进的语言模型。
总体而言,这些结果确认深度语言模型线性映射到大脑对口语故事的反应上。 (source)
大脑如何预测长距离的词?
照片 Jessica Yap 来自 Unsplash
变换器 替代了 递归神经网络(RNNs),因为它能够建模长期依赖(输出依赖于过去的输入)。这全因 自注意力 使其能够使用更长的序列作为输入。
正如我们所说,尽管语言模型取得了巨大的进展,但它们与人类之间仍存在很大差距。
通常,在阅读文本、听演讲或对话中,会有许多长期依赖。大脑往往能够轻松地从上下文中预测下一个单词或短语。但大脑是如何处理这些的呢?
接下来,我们测试了增强语言模型的激活与长期预测是否会导致更高的大脑得分。 (source)
换句话说,如果我们添加预测表示,它能否提高我们预测大脑的能力?
作者定义了一个预测窗口,其中包含未来一定数量的单词信息。模型保持不变,只是在这种情况下,输入也附加了预测表示(预测窗口)。对于距离 d(单词数量),预测窗口是当前单词的七个连续单词的网络激活的串联。
我们不将未来的单词串联在序列中,而是将模型的激活进行串联,因此我们并不将未来的单词提供给模型,而是它们的表示。
“预测得分”简单来说就是“将预测窗口与当前 GPT-2 激活拼接时,大脑得分的增益。” 或者用简单的话说,就是对下一个单词的表示在多大程度上有助于我们预测大脑活动。
“c,测试是否添加未来单词的表示能改善这种相关性。d,上图中,一个平坦的预测得分在不同距离下表明预测表示并没有使算法更类似于大脑。下图中,相反,预测得分在d > 1 处达到峰值则表明模型缺乏类脑预测。Fd 的峰值指示了算法需要预测表示的未来距离,以便与大脑最相似。” 图像来源:这里,许可证:这里
我们的结果表明,F 在 d = 8 个词的距离处达到最大值,并在通常与语言处理相关的区域达到峰值(图 2b–d)。作为比较,刺激中每秒平均有 2.54 个词。因此,8 个词对应于 3.15 秒的音频(两个连续 fMRI 扫描的时间)。 (来源)
简而言之,作者注意到几个有趣的事项:
-
从零到 10 的每个词都有助于这种预测效果。
-
最佳窗口大小大约是八个词。
-
随机预测表示未能帮助预测脑部活动。
-
你甚至可以使用 GPT-2 生成的词代替序列中的真实词(未来词)。这显示了类似的结果,但效果较小。
综合来看,这些结果揭示了大脑中的长程预测表示,代表了大脑评分的 23%(±9% 跨个体)的改善。 (来源)
作者换句话说,这些数据确认了过去和当前的词表示占据了语言理解中涉及的大脑信号的很大一部分。确实,这些信号映射到以前定义的 语言网络。
大脑解剖学研究表明,皮层是按层级组织的。输入和信息以层级方式处理,因此低级声学、音素和语义由大脑中的不同结构按照精确的层级编码。
这是不同区域如何在自然语言中编码不同任务的示例。来源 (这里)
因此,作者提出了一个问题:“这一皮层层级的不同层次是否预测相同的时间窗口?”
换句话说,他们测试了预测范围在皮层层级中的变化。他们随后研究了不同区域如何影响预测分数及其在词距离 d 上的变化。
结果表明,前额叶区域的预测平均而言,比颞叶区域更偏向未来。 (来源)
已经展示了区域之间存在差异,仍然有几个问题。这种时间差异与上下文有什么关系?在句法和语义内容方面是否存在差异?
如所示,变换器编码语言表示的方式具有层次结构。例如,BERT的各层捕获了不同的表示。较低的层捕获短语级别的信息,然后这些信息会被稀释。各种语言信息是以层次化的方式学习的,较低层学习表面特征,中间层学习句法特征,高层学习语义特征。
为此,作者计算了预测得分,但使用了不同级别的 GPT-2。然后,他们将这些预测得分按级别映射到脑区。
总的来说,这些结果表明,额顶皮层的长期预测比低级脑区的短期预测更具上下文关联性和更高层次。 (source)
换句话说,模型各层的不同表示(正如我们上面所说的,模型层的复杂性随着选择的层的深度而增加)在大脑中的映射方式不同。预测的深度与皮层层级之间存在对应关系。
低级预测(低级信息)在处理高层预测(中颞叶、顶叶和额叶区域,这些区域处理预测和复杂信息整合)的脑区之外的不同脑区(具体来说是上颞沟和脑回)中进行预测和分析。
作者然后提取了句法和语义预测表示,对于每个词及其上下文,他们生成了 10 个可能的未来,这些未来具有与原始句子相同的句法。实际上,给定一个句子开头,他们生成了 10 种不同单词的续写,但具有相同的句法属性(词性和依赖树)。然而,这些句子具有不同的语义(即不同的含义)。
然后,他们提取了 GPT-2 的激活(第 8 层),并对这十种可能的未来进行了平均,以提取各个未来的共同句法组件。进一步地,他们将这种平均(句法表示)从实际词序列的激活中减去(以获得纯粹的语义表示)。然后,他们构建了一个语义和语义分开的预测窗口:
我们通过分别连接七个连续未来词的句法和语义组件,构建了句法和语义预测窗口。 (source)
这种方法允许将激活分解为两个组件,一个是语义的,另一个是句法的。一旦完成这些操作,作者计算了预测评分,如之前所见,显示出大脑活动的差异。
结果显示,语义预测是长距离的(d** = 8),并涉及一个分布广泛的网络,主要集中在额叶和顶叶。相比之下,句法预测(图 4b)相对短距离(d** = 5),并局限于上颞叶和左额叶区域(图 4c,d)。 (source)
正如作者所指出的,这些结果表明大脑进行多层次的预测,不同区域有不同的任务:
上颞叶皮层主要预测短期、浅层和句法的表示,而下额叶和顶叶区域主要预测长期、上下文、较高层次和语义的表示。 (source)
我们可以在语言模型中实现预测编码吗?
他们从之前的结果中注意到了他:
这些结果显示,将 GPT-2 当前和未来词的表示进行串联,可以更好地模拟大脑活动,特别是在额顶区。 (source)
这些原则可以转化为 GPT-2 训练吗?
换句话说,使用微调是否可以让模型学习预测更长范围、更有上下文且更高层次的表示?如果可以,这是否改善了模型的脑图谱?
为了测试这一点,作者决定对 Wikipedia 上的 GPT-2 进行微调。他们决定改变目标训练方式,而不是使用语言建模方法(即给定前一个词序列预测下一个词),他们决定在经典目标的基础上增加高层次和长距离目标,模型还必须预测序列中远离的词的高层次表示。
具体而言,模型不仅需要预测下一个词,还需要预测后续词的表示。模型还必须预测序列中距离为 d=8 的单词的未微调 GPT-2(第 8 层)模型的隐藏状态。然后,模型还会学习序列中下一个词的长期、高层次和更具上下文的表示。
结果表明,GPT-2 在高层次和长范围建模下的微调最能解释前额顶叶反应(图 5,在 IFG 和角回/上缘回平均增益>2%,所有P < 0.001)。这些结果进一步强化了前额顶叶区域在预测长距离、上下文和高层次语言表示中的作用。 (source)
简单来说,提供这种上下文表示使模型更能预测大脑活动。
结束语
图片由 Anete Lūsiņa 在 Unsplash 提供
这项研究在更好地理解大脑如何理解语言和作出反应方面提出了有趣的观点,并且探讨了神经科学与机器学习之间的联系。
通过更好地理解这些机制,我们可以设计出与人脑更为相似的人工智能模型。当前的语言模型在给定前文的情况下预测下一个词,但人脑考虑的是上下文和未来的可能性。实际上,大脑预测传感器输入,然后将其预测与现实进行比较,然后更新自身的内部模型表示。
因此,未来的模型在训练过程中可能需要考虑未来单词的远程和抽象表示。在这项研究中,他们展示了甚至不需要改变模型的架构,但未来的模型可以重新调整其结构以提高效果。
此外,正如在其他背景中所示,未来的观察(例如,未来待分类的图像)确实保持不确定,其潜在表示更加稳定。这就是为什么像对比学习这样的办法已被证明更有效。所以,如果这已被证明有效,为何不在具有更多上下文信息和更多关于未来潜在表示的信息的语言模型架构和训练中实施它呢?
作者指出,然而,这项研究仍处于初步阶段:
最后,目前测试的预测编码架构仍然很初步。需要对这一方法在自然语言处理基准上的系统性推广、扩展和评估,以证明使模型更接近大脑的有效实用性。 (source)
无论如何,自从 Transformer 发布以来,模型变得更大了,但结构几乎保持不变。为了克服当前语言模型的局限性,我们需要对架构和训练进行修改。还有什么比人脑更好的灵感来源呢?
如果你觉得这很有趣:
你可以查看我的其他文章,你也可以订阅以便在我发布文章时获得通知,还可以通过LinkedIn与我联系。
这是我的 GitHub 仓库链接,我计划在这里收集与机器学习、人工智能等相关的代码和资源。
[## GitHub - SalvatoreRa/tutorial:关于机器学习、人工智能、数据科学的教程……
关于机器学习、人工智能、数据科学的教程,包含数学解释和可重用的代码(用 Python 编写……
或许你会对我最近的一篇文章感兴趣:
Google 的新模型经过训练可以回答医学问题。如何做到这一点?
META 的 LLaMA:小型语言模型战胜巨头 [## META 的 LLaMA:小型语言模型战胜巨头
META 开源模型将帮助我们理解语言模型偏见的产生
META 的 LLaMA:小型语言模型战胜巨头 [## 稳定扩散技术填补医学图像数据的空白
一项新的研究表明,稳定扩散技术可能有助于医学图像分析和稀有疾病研究。如何做到这一点?
为什么我们有巨大的语言模型和小型视觉变换器? [## 为什么我们有巨大的语言模型和小型视觉变换器?
Google ViT-22 为新的大型变换器铺平道路,革新计算机视觉
作为分析师的错误——以及应对策略
原文:
towardsdatascience.com/making-mistakes-as-an-analyst-and-strategies-to-deal-with-them-b4d486d7c4dd
曾经因为分析中的错误被指出而感到焦虑吗?你并不孤单。
·发表于 Towards Data Science ·6 min read·2023 年 10 月 17 日
--
来源:Unsplash
制作错误分析并根据我的结果将组织引导到错误方向,一直是我开始作为分析师以来最大的恐惧之一。老实说,被暴露和被指出我的错误甚至更让我感到焦虑。
我的噩梦场景:根据我的建议,我的团队花费数月时间开发的一个主要功能在测试中未能提供价值。人们开始提出问题,经过调查后,发现我的假设是基于错误的分析结果。
自那时以来,与许多分析师交谈和指导后,我了解到我并不是唯一有这种感觉的人。相反,这是许多分析师共有的非常普遍的恐惧。
我们为何对我们的分析感到焦虑
对于分析感到焦虑的原因非常明显:发布和推广错误的结果和建议不仅影响整个组织,同时也对我们个人带来风险。
尽管存在这些风险,避免分享分析或回避有争议的或挑战假设的结果并不是正确的方法。事实上,这些分析往往是最重要的。相反,关键是制定有助于减少产生错误输出风险的策略。 这从欣赏以下几点开始:
作为分析师犯错误是非常容易的。
分析师在一个高度复杂的环境中工作,拥有很高的自由度,因此有许多机会犯错或得出错误结论。
进行分析涉及几个不同的步骤:
-
定义研究问题和方法
-
收集和清理数据
-
数据的分析和解释
-
结果的可视化和沟通
在每个阶段,犯错都是相当容易的,而这些错误最终会反映在我们的建议和组织采取的行动中。但同时,我们可以使用不同的策略来最小化这种风险。
在开始分析之前设置一个合适的基础
在开始编写第一行代码并深入数字之前,至关重要的是对分析的目标和达成目标的方法达成一致。我在职业生涯初期学到,分析特定问题对不同的人可能意味着非常不同的事情:例如,分析新功能的性能是否意味着调查哪些用户群体使用该功能、该功能驱动了哪些动作(以及可能的收入)或对生态系统其他部分的副作用是什么(或者这些都包括在内,这种情况下我们需要讨论时间线)?
每当我开始进行新的分析时,我通常会制定一个简短的分析计划并与我的同事和利益相关者讨论,以获得早期反馈。这些早期反馈让我可以提前验证我的分析计划,并在必要时进行调整。同时,这也使我能够将分析集中在真正需要的方面,而不是生成一个非常广泛、工作量大的文档,试图涵盖特定问题的每一个角度。
下一步是研究过去是否进行过类似的分析。你可能不是第一个研究特定主题的人,特别是如果你在一个有多个分析师的团队中。确保检查是否有人在你之前已经研究过相同的问题以及他们使用的方法。这可以再次帮助验证你的方法,为如何结构和处理你的分析提供更多灵感,并作为与利益相关者达成交付物一致的模板。
来源:Unsplash
利用以往的工作来验证你的方法和结果
一旦分析的目标和方法达成一致,最终就可以开始运行第一个查询并深入数据。在分析结果和所采用的方法上,利益相关者不应再有任何惊讶,但在处理数据时仍然有很多可能出错的地方:使用错误的数据集、编码错误或逻辑错误,几乎有无尽的潜在错误空间。
以前的分析和查询可以是增强你对结果信心的绝佳方式。检查和比较其他人如何使用特定的数据表和字段(即使他们调查了不同的问题):是否有特定的人群你不应包括在分析中?是否有需要警惕的例外或边缘情况?
如果有以前的分析涉及相同或类似的问题,确保将这些结果与你当前的结果进行比较。是否在特定数字上存在较大差异?如果有,是否可以通过业务变化或不同的基本假设来解释,还是数据问题可能导致了差距?
持续分享进行中的工作
一旦分析有了些许成果,持续与利益相关者和同事分享你的进行中的工作以寻求早期反馈是一个好主意。虽然最初呈现不成熟或不完整的结果可能有些不安,但这对于早期发现差距和潜在的后续问题至关重要。
在开始分享我的工作时,我还会尝试发现差距并预测人们在阅读我的结果和建议时可能会有的问题:在漏斗中是否有特别的下降点,需要更多的清晰度来解释为什么会发生这种情况?我的结果是否与之前的假设一致,如果不一致,是否需要额外的背景信息?
同时,呈现不成熟的结果并不意味着呈现一个难以消化的粗糙作品。你的(早期)分析的外观不仅有助于传达当前状态和下一步,还能建立对你工作质量的信任。根据我的经验,即使是仍在进行中的工作,也值得花额外的精力使分析从一开始就看起来专业,以促进持续的讨论。这也有助于你自己构建你希望通过数据呈现的叙事。
构建一个最小化错误空间的环境
最终,拥有正确的分析文化对于促进健康的讨论和建立有助于最小化错误的流程至关重要。这包括但不限于创建一个开放的环境,在这里可以公开分享和讨论进行中的工作,重点在于共同提升彼此的工作。
采用适当的工具和机制来利用彼此的工作,可以极大地提升你和你的团队所产出的工作的质量。首要任务是,分享用于生成分析结果的查询和代码应当成为组织中的最佳实践。直接将来源链接到分析结果,对于重现结果和学习彼此的方法是非常有帮助的。
此外,将管道工作作为每次分析的核心部分有助于降低复杂性。我见过分析师从头开始构建各种复杂的查询,以得出相同的指标或见解(我自己也犯过这种错误)。然而,通过将特定的指标或数据字段集成到管道中,使这些数据对其他人可用,不仅有助于减少复杂性和工作量,还能标准化某些定义和方法,使来自不同分析的结果更容易进行比较。
将错误转化为成功
在某些时候,我们必须接受无论我们做什么,总会有人为错误的可能性。上述策略通常有助于降低这种风险,但在某些情况下,错误可能仍然会出现。如果发生这种情况,重要的是从错误中学习并对其保持开放态度。
归根结底,尽管采取了所有预防措施,错误是人类的特性,难免会发生。犯错是学习和成长的自然部分。
在这些情况下,我们必须前进并从错误中学习。然而,在许多情况下,我们的团队也可以从我们的错误中学习。公开讨论错误和不良结果可以帮助下一位避免完全相同的错误,并总体上提高团队工作的质量。
所以下次你犯错时(尽管采取了上述所有预防措施),我强烈建议你考虑将其作为经验教训公开分享给你的团队,并将这些讨论作为你们文化的一部分。
通过源分离实现音乐标记 AI 的可解释性
让我们打开黑匣子
·
关注 发表于Towards Data Science ·10 min read·Apr 4, 2023
--
图片由Jadson Thomas,Clem Onojeghuo和Dmitry Demidov提供。
介绍
为什么我们需要这个?
音乐标记的 AI 系统已经存在了一段时间。自 2010 年代中期以来,音乐流媒体服务一直在竞争最具创新性的音乐推荐系统,后台使用复杂的标记 AI。逐渐地,制作音乐库和音乐标签公司开始关注标记 AI,用于对其庞大的音乐数据库进行分类、过滤和查询。今天,甚至艺术家们也在使用自动标记系统,以获得对自己音乐的客观见解,找到合适的受众。
尽管自动标签系统广泛存在,但其内部工作原理鲜为人知。由于音频数据复杂且高维,深度学习模型在音乐标记任务中始终优于传统的机器学习方法。深度学习的问题是显而易见的:通过构建越来越复杂和高效的模型,我们牺牲了可解释性。深度学习模型实际上是黑箱,我们无法确切了解内部发生了什么。
本项目的目标
本项目旨在通过将音轨拆分成其乐器来源(stem)并观察分类如何变化,来阐释音乐标记 AI 系统的行为。这种方法受到了一个著名的可解释 AI(XAI)概念的启发,称为Shapley 值。
Shapley 值来源于合作博弈论。假设我们有不同的玩家,他们可以组成“联盟”来实现共同目标。目标是为每个玩家分配一个值,表示将他们加入联盟通常如何改善结果,即确定他们的公平份额。那么,这个概念与音乐标记和乐器源分离有什么关系呢?
图 1 帮助你理解博弈论与音乐分类器之间的类比。如果一首曲目被 AI 标记系统指定为“爵士”风格,我们可能会想知道乐队中的每种乐器(鼓、吉他、声乐)对这一分类的贡献有多大。换句话说:我们想找出每种乐器对使曲目听起来像“爵士”音乐有多重要。
图 1:本项目目标的插图。图像由作者提供。
方法
示例音乐
所有用于此分析的示例曲目均取自Free Music Archive (FMA),这是一个包含成千上万的免版权音乐片段的数据集。第一个示例是一首包含多个失真的吉他、重鼓和开头较柔和男声的曲目。这首曲目将作为我们的“摇滚”示例。
摘自 Aviv Mark 的《Wonder 的面貌》(CC BY-NC-ND 3.0 许可证)。
下一段音乐包含步伐感强的贝斯、扫帚鼓、轻松的吉他和一个主导的即兴钢琴。这将作为我们的“爵士”示例。
摘自 Quantum Jazz 的《Jingle Jazz》(CC BY-SA 3.0 许可证)。
第三条也是最后一条轨道是“爵士”和“摇滚”流派的混合,结合了布鲁斯风格的钢琴、带有轻微摆动的鼓、失真的电吉他和更像摇滚的声音。这一片段将作为我们的“混合流派”示例。
摘自 Sakee Sed 的《Vermouth 和 Baby》(CC BY-NC-ND 3.0 许可证)。
源分离与重组
对于这个项目,我使用了LALAL.AI 的源分离工具,这是一家专门从事此技术的初创公司。虽然还有许多其他竞争解决方案,如 Spleeter 或 Vocalremover.org,但在我对他们的在线演示印象深刻之后,我选择了 LALAL.AI。此外,他们还非常友好地提供了进行本文所需源分离工作的资源。
在将三条轨道拆分成其乐器源后,我将这些孤立的源组合起来,形成每条轨道的所有可能源组合。对于具有 4 个源(“摇滚”和“爵士”)的轨道,这导致了 15 个新的音频文件,从单一的鼓轨道到完整的乐队设置。而对于有 5 种乐器的“混合流派”轨道,总共有 31 种组合可能。每种组合都类似于 Shapley 值博弈理论中的“联盟”概念。
流派与心情标记
至于自动标记 AI,我使用了另一个名为“Cyanite AI”的服务。Cyanite 是一个自动标记工具,提供来自不同类别的标签,如流派、心情或乐器。虽然还有其他自动标记提供商,如 AudioRanger 和 Songtradr 的 musicube,但我对 Cyanite 的产品非常熟悉,因为我曾作为工作学生为其做过贡献。Cyanite 还慷慨地为我提供了分析本项目示例轨道的资源。
自动标记器为每个标签输出 0 到 1 之间的分数,表示标签与轨道的匹配程度(1 表示非常匹配,0 表示不匹配)。这个输出值可以解释为轨道是“摇滚”、“金属”还是“悲伤”或“愤怒”的程度。
计算 Shapley 值
对于每条轨道,我分析了所有源联盟的两个最显著的流派和心情。为了获得乐器的 Shapley 值,我计算了每个乐器缺席的联盟与该乐器存在的联盟之间的流派和心情评分差异。这些差异的平均值,即将该乐器添加到混音中的平均贡献,就是 Shapley 值。
结果
摇滚轨道
摘自 Aviv Mark 的《Wonder 的外观》(CC BY-NC-ND 3.0 许可证)。
“摇滚”轨道被自动标记工具标记为“摇滚”和“金属”流派,以及“充满活力”和“攻击性”心情。
图 2:基于 Stem 的“摇滚”轨道 Shapley 值。图像来源:作者。
我们能看到什么?
图 2 显示了每个乐器来源对各自标签的贡献。例如,我们可以看到,电吉他平均增加了“金属”评分超过 50 个百分点。另一方面,“摇滚”评分仅增加了大约 5 个百分点。从中我们可以看出,电吉他的声音更像是一把“金属”吉他,而不是“摇滚”吉他。对于人声,则正好相反:人声显著提高了“摇滚”评分,同时降低了“金属”评分。这告诉我们,这首歌中的人声和电吉他在风格上相互对比。这不是很酷吗?
从情绪的角度来看,我们可以看到电吉他在表达这首曲目的“充满活力”和“攻击性”情绪方面特别重要。与风格图不同,所有乐器似乎都传达了相同的情绪,只是程度不同。有趣的是,贝斯乐器似乎对风格或情绪评分没有贡献。这可能是因为贝斯的表现不够鲜明,要么是因为它比较安静,要么是因为它的演奏方式不具备某种特定风格或情绪的特点。
我们能用它做什么?
Shapley 值方法已经帮助我们通过分析哪些乐器以何种方式对我们感兴趣的特定风格或情绪做出贡献(或相反),来理解自动标记系统的输出。例如,如果我们希望这首曲目获得更高的“金属”评分,我们应该考虑用不同的歌手或唱腔重新录制人声。为了使其更“摇滚”而不那么“金属”,我们可以尝试降低电吉他的失真度。
爵士曲目
摘自 Quantum Jazz 的《Jingle Jazz》(CC BY-SA 3.0 许可证)。
对于“爵士”曲目,唯一的音乐风格输出为“爵士”,情绪输出为“愉快”和“放松”。
图 3:基于 Stem 的 Shapley 值用于“爵士”曲目。图像由作者提供。
我们能看到什么?
图 3 显示了钢琴和鼓使这首歌听起来特别“爵士”。虽然从音乐的角度来看,贝斯和电吉他在录音中也表现得非常“爵士”,但突出钢琴和鼓是有道理的,因为它们在混音中更为突出,似乎承载了歌曲的“风格重量”,在我听来。单凭鼓和钢琴就能做出一首听起来很棒的爵士曲目,而如果只有吉他和贝斯,没有其他乐器的话则会显得奇怪。
情绪分析揭示了所有乐器对相同情绪的积极贡献。然而,钢琴和鼓比对“轻松”情绪的贡献更多地提高了“快乐”分数。电吉他和贝斯则相反,它们对曲子的“轻松”分数贡献更多。这一点很有趣,因为尽管钢琴和鼓也是情绪标签中最重要的乐器,但如果没有电吉他和贝斯或它们的演奏方式不同,这首曲子会显著地少一些“轻松”感。
我们可以用它做什么?
通过我们的分析,我们再次获得了哪些乐器对曲目标签最重要的见解。此外,我们还可以看到,通过改变一些乐器的演奏、录制或混音方式,我们现在可以操控歌曲的情绪特征。
混音曲目
摘自 Sakee Sed 的《Vermouth and Baby》(CC BY-NC-ND 3.0 许可证)。
正如预期,这首曲子被标记为“摇滚”和“爵士”。至于情绪,“快乐”和“轻松”也被标记了。
图 4: “爵士”曲目的基于干音的 Shapley 值。作者提供的图像。
我们可以看到什么?
图 4中展示的结果令我非常惊讶!首先,我们的方法识别出钢琴非常“爵士”,并且不太适合“摇滚”风格。另一方面,声乐非常不典型于“爵士”,更倾向于“摇滚”分类。其他乐器也是对比性的,即它们指向两个风格方向中的任何一个,但它们的整体贡献分数确实很低。
我们首次看到了一些对比性的情绪分数贡献,尽管数值较小。图表暗示,声乐和鼓使得这首曲子更“快乐”而更少“轻松”。然而,大部分的权重还是由钢琴承担的,这使得曲子更“快乐”且更“轻松”。
我们可以用它做什么?
这项分析为我们提供了一些关于哪些乐器对自动标记 AI 的风格和情绪输出有贡献的宝贵建议。我们还可以看到,去掉或改变钢琴部分会使这首曲子少一些“爵士”风格,但同时也可能减少其“快乐”和“轻松”感。
讨论
什么效果良好?
本文的整体目标是找到一种算法解决方案,通过源分离为黑箱自动标记系统提供局部解释,这一目标已经达成。生成的解释提供了每种乐器如何对自动标记系统的风格和情绪输出做出贡献的见解。尽管我们只查看了几个选定的风格和情绪,但这种方法可以扩展到其他标签,如年代、品牌价值、地点等,而无需额外的努力。
我可以看到这种方法可以在两个用例中对音乐家的工作有所帮助。
-
音乐家可以使用它来检查他们的音乐是否以他们认为合适的方式被 AI 解读。如果不是这样,Shapley 值会给出关于哪些乐器可以更改以实现这一目标的建议。这可以帮助音乐家在音乐目录或流媒体服务中获得更好的可见性。
-
最后,这种方法对创作和制作过程本身可能是有用的。音乐家可以分析他们的演示,并获得一些关于歌曲中每个乐器的特征,如哪些流派、情绪等的 AI 反馈。这可以帮助作曲家进一步完善他们的作品或尝试新的风格方法。
有哪些方面表现不佳?
提议的算法主要问题是资源效率低。输入的音乐轨道必须被拆分成所有存在的乐器/乐器组。这本身就需要很长时间,因为音乐源分离系统复杂且需要大量计算能力。然后,所有乐器源的组合被生成并分别输入到 AI 自动标签器中。如果我们在轨道中有 6 个乐器源,这意味着自动标签器需要处理 31 条轨道而不是仅仅一条。一旦完成,Shapley 值至少可以非常高效地计算。总的来说,对少量轨道进行这种处理是可行的,但在当前技术下,规模化应用此算法可能超出了范围。
这种方法的另一个弱点是,尽管源分离工具在过去几年中取得了显著进步,但结果仍然不完美。如果我们自动化算法而不逐个检查每个分离的源,我们将无法发现分离效果不佳的情况。这可能会严重扭曲分析结果。
下一步可能是什么?
提议的方法具有较高的算法复杂性,因此在轨道中乐器数量增加时不易扩展。此外,目前我们是否可以充分依赖源分离系统的质量尚不清楚。随着计算资源变得更便宜以及源分离工具的质量进一步提高,这两个挑战将不再是问题。然而,算法复杂性仍将是一个问题,除非找到并开发出一种高效的近似算法。
为这篇博客文章编写的代码远非高效,仅仅是为了文章目的而创建的。虽然我会在GitHub上发布代码,但我建议不要将其作为实际应用该算法的基础。我认为,最好从头开始构建一个高效的多阶段处理流程,并注重细节。基于云的实现看起来很有前途,因为大多数处理过程可以通过利用并行计算来大幅加速。
从宏观角度来看,这种方法将 Shapley 值的概念应用于音乐作品的抽象特征,而不是 AI 所看到的“真实”数据(例如,光谱图或波形的值)。我相信这种特征抽象的方法在其他使用非表格数据训练 AI 模型的领域也有其他应用(例如图像、文本、语音)。例如,我们可以将一段文本拆分成单独的句子,并尝试类似于我们在这里做的事情。通过这种方法,我们可能能够识别出邮件中哪个句子导致邮件被分类为垃圾邮件。一定还有更多类似的应用等待发现和实施!
非常感谢你的阅读!
我写了很多关于 AI 和音乐的文章。以下是一些相关的帖子,你可能会感兴趣:
-
使用 Spotify 的 Pedalboard 进行自然音频数据增强
-
使用分治法 CRNN 进行音乐分类
了解 A/B 测试的意义:通过困难问题更好地理解
通过具有挑战性的问题揭示 A/B 测试中反直觉的方面,提升你的理解,并避免错误
·
关注 发表在 Towards Data Science · 6 分钟阅读 · 2023 年 7 月 4 日
--
照片由 ALAN DE LA CRUZ 提供,来源于 Unsplash
这篇文章强调了实验中常见的统计错误。它以五个问题和答案的形式展开,这些答案常常让人感到直觉上不对劲。它专为那些已经熟悉 A/B 测试但希望扩展理解的人群量身定制。这可以帮助你在日常工作中避免常见错误或在面试中表现出色。
问题 1:你进行了一个 A/B 测试(α = 0.05, β = 0.2),结果具有统计学意义。在这种情况下,它是真阳性的可能性有多大?
想象一下,如果你只测量有效的假设。那么,100%的成功 A/B 测试将是真阳性。当你的所有假设都无效时,100%的成功 A/B 测试将是假阳性。
这两个极端的例子旨在说明,没有额外的步骤——对假设分布的假设——是不可能回答这个问题的。
再试一次,假设我们测试的 10%的假设是有效的。那么,从 A/B 测试中观察到统计显著的结果意味着有 64%(根据贝叶斯定理,(1–0.2)0.1 / ((1–0.2)0.1 + 0.05*(1–0.1)))的机会它是真阳性。
作者提供的图像
问题 2:假设原假设成立。在这种情况下,更高还是更低的 p 值更可能出现?
很多人认为是前者。这似乎是直观的:当没有效应时,结果更可能远离统计显著性,因此 p 值更高。
然而,答案是否定的。当原假设成立时,p 值是均匀分布的。
混淆的原因在于,人们通常通过 z 分数、样本均值或样本均值的差异来可视化这些概念。所有这些都是正态分布的。因此,可能很难理解 p 值的一致性。
让我们通过一个模拟来说明这一点。假设治疗组和对照组均来自同一个正态分布(μ = 0, σ = 1),这意味着原假设成立。我们将比较它们的均值,计算 p 值,并多次重复这个过程。为了简化起见,我们只关注治疗组均值较大的情况。然后,我们再看看 p 值在 0.9 到 0.8 和 0.2 到 0.1 之间的情况。
当我们将这些 p 值区间映射到我们模拟的分布上时,图像变得更加清晰。尽管接近零的分布峰值较高,但此处区间的宽度较窄。相反,当我们远离零时,峰值缩小,但区间的宽度增加。这是因为 p 值的计算方式使得等长区间包含曲线下相同的面积。
作者提供的图像
问题 3:由于一些技术或业务限制,你进行了一个样本量较小的 A/B 测试。结果勉强显著。然而,效应值很大,比你在类似 A/B 测试中通常看到的更大。效应值的增大是否能增强你对结果的信心?
不一定。为了将效应归类为显著,它必须与零的距离为正负 2 个标准误差(当α = 0.05)。随着样本量的缩小,标准误差通常会增加。这意味着在小样本中观察到的统计显著效应往往更大。
以下模拟演示了:这些是当两个组(N=1000)都从相同的正态分布(μ = 0,σ = 1)中抽样时的显著 A/B 测试的绝对效应值。
作者提供的图像
问题 4:让我们基于之前问题中获得的理解继续探讨。是否可以检测到小于 2 个标准误差的真实效应?
是的,尽管这里的语义比较模糊。 真实效应值可能显著小于 2 个标准误差。即便如此,你仍然会预期一定比例的 A/B 测试显示统计显著性。
然而,在这些条件下,你检测到的效应值总是被夸大的。假设真正的效应是 0.4,但你检测到的效应是 0.5,且 p 值为 0.05。你会认为这是真阳性吗?如果真正的效应值只有 0.1,但你又检测到效应为 0.5 呢?如果真正的效应仅为 0.01,那这仍然是一个真阳性吗?
让我们可视化这个场景。对照组(N=100)从正态分布(μ = 0,σ = 2)中抽样,而处理组(N=100)则从相同的分布中抽样,但μ从 0.1 到 1 变化。无论真实效应大小如何,成功的 A/B 测试生成的估计效应值至少为 0.5。当真实效应小于这个值时,结果估计显然被夸大了。
作者提供的图像
这就是为什么一些统计学家避免将结果划分为“真阳性”或“假阳性”这样的二元类别。他们更倾向于以更连续的方式处理这些结果[1]。
问题 5:你进行了 A/B 测试,结果显著,p 值为 0.04。然而,你的老板仍然不相信,并要求再做一次测试。这个后续测试没有产生显著结果,p 值为 0.25。这是否意味着原始效应不真实,初始结果是一个假阳性?
将 p 值解释为二元的、词典式的决策规则总是存在风险。让我们回顾一下 p 值实际上是什么。它是惊讶的度量。它是随机的,是连续的。它只是证据中的一部分。
设想第一个实验(p=0.04)是在 1,000 人中进行的。第二个实验(p=0.25)则是在 10,000 人中进行的。除了质量上的明显差异外,正如我们在问题 3 和 4 中讨论的,第二个 A/B 测试可能估计的效应大小要小得多,可能在实际中不再具有显著意义。
让我们倒过来看这个场景:第一个(p=0.04)是在 10,000 人中进行的,第二个(p=0.25)则是在 1,000 人中进行的。在这种情况下,我们对效果‘存在’的信心更大。
现在,假设两个 A/B 测试是相同的。在这种情况下,你观察到了两个相当相似的、稍微令人惊讶的结果,它们都与零假设不太一致。它们落在 0.05 的两侧并不是特别重要。重要的是,在零假设为真时,连续观察到两个小 p 值是不太可能的。
我们可能需要考虑的一个问题是,这个差异本身是否具有统计显著性。以二元方式分类 p 值会扭曲我们的直觉,让我们相信不同侧的 p 值之间存在巨大甚至本体上的差异。然而,p 值是一个相当连续的函数,尽管两个 A/B 测试的 p 值不同,但它们可能对零假设提供了非常相似的证据[2]。
另一种看待这个问题的方法是结合证据。假设两个测试的零假设都为真,根据Fisher 方法,组合后的 p 值为 0.05。还有其他方法可以结合 p 值,但基本逻辑相同:在大多数情况下,尖锐的零假设并不现实。因此,即使这些结果在统计上没有单独显著,足够多的‘令人惊讶’的结果也可能足以拒绝零假设。
通过使用 Fisher 方法融合两个 p 值。图片来源:Chen-Pan Liao,来自维基百科
结论
我们常用的零假设检验框架,在分析 A/B 测试时并不是特别直观。没有经常的心理训练,我们常常会退回到‘直观’的理解,这可能会误导我们。我们也可能会形成一些例行程序以减轻这种认知负担。不幸的是,这些例行程序往往变得有些仪式化,遵守正式程序的过程掩盖了实际推断的目标。
参考文献
-
McShane, B. B., Gal, D., Gelman, A., Robert, C., & Tackett, J. L. (2019). 放弃统计显著性。美国统计学家, 73(sup1), 235–245.
-
Gelman, A., & Stern, H. (2006). “显著”和“非显著”之间的差异本身并不具有统计显著性。美国统计学家, 60(4), 328–331.
理解大型语言模型的承诺(及风险)
·
关注 发表在 Towards Data Science ·发送至 通讯 ·4 分钟阅读·2023 年 4 月 27 日
--
尽管 ChatGPT 和类似工具在最近几个月中占据了我们大量的关注,但大型语言模型(LLMs)——这些支撑着我们无休止分享、截图,偶尔摇头的聊天输出的基础设施——却一直处于(相对)默默无闻的状态。
从某种程度上说,这很有道理:当你在笔记本电脑上观看节目时,你专注于情节和角色,而不是使那些移动像素成为可能的电网。如果你现在正在吃饼干(就像…一些 TDS 编辑,可能如此),你可能在思考你正在享受的风味和口感,而不是,比如说,关于小麦农民或可可豆的历史。
然而,作为数据专业人员,我们只有通过深入和细致地理解 LLM 才能受益。当然,这有一个职业角度,但也有一种更模糊的享受,即了解某些复杂而神秘的事物在幕后实际是如何运作的。为了给你提供帮助,我们汇集了一系列优秀的文章,探讨了 LLM 的过去、现在和未来——并讨论了它们的惊人能力和非同小可的局限性。请享用!
-
后 ChatGPT 时代的机器学习研究。一些最普遍的 LLM(如 OpenAI 的 GPT 模型)的内部运作仍被企业严格保密。这对那些未来项目很可能会遇到专有壁垒的 NLP 研究人员意味着什么?安娜·罗杰斯提出了一个有力的观点:“那些不开放且不合理可重复的东西不能被视为必要的基线。”
-
评估最近在对话 AI 方面的进展的影响。LLM 的规模和力量使得人机互动在短短几个月内取得了巨大的飞跃。加迪·辛格反思了我们如何走到这里——以及在机器开始以类人方式交流之前,还缺少什么。
-
欢迎来到一个新的伦理(和实际)困境。LLM 需要大量的训练数据,这已成为一个主要的风险因素。这些模型目前面临着对可能存在的偏见、私人或有害文本以及未经授权的版权材料的严密审查。在此期间,用户该如何做?凌娟·吕的首篇 TDS 文章对我们在负责任地生产 AI 生成内容时应了解的风险和危险进行了深思熟虑的概述。
-
LLM 驱动工具的广阔视野。仅几个月前,LangChain 库还未引起大多数从业者的注意。现在不同了:它已经成为许多想利用 LLMs 构建新应用程序的人的首选资源。Varshita Sher 博士的最新深度剖析是该库核心构建模块的有益、实用介绍。
-
使用 LLMs 识别漂移和检测异常。随着使用 ChatGPT 生成生硬诗歌的新奇感逐渐消退,新的用例继续出现 —— 其中许多可能会简化数据科学工作流程。举例来说,Aparna Dhinakaran,Jason Lopatecki 和 Christopher Brown 的最新文章,概述了使用 LLM 嵌入进行异常和漂移检测的有前景方法。
-
额外阅读:深入一层。如果语言模型(LLMs)使用户界面应用如 ChatGPT 成为可能,那么转换器神经网络是首先使 LLMs 成为可能的架构。要理解这个至关重要(且常常复杂)的主题,探索Soran Ghaderi详细的“地图”,回顾过去和现在的转换器研究。
准备继续你用饼干激励的阅读狂欢?以下是几篇你应该看看的杰出文章:
-
我们很高兴分享Michael Bronstein和 Emanuele Rossi 的最新研究,探索机器学习和博弈论的交集。
-
三年后,她的病毒式帖子宣布了仪表板的消亡,泰勒·布朗洛带来了对这些常受诟病但不可避免的工具更新和更加微妙的视角。
-
当前的商业格局如何影响数据团队?对于Barr Moses,数据科学家缩小他们的工作与核心业务需求之间的差距至关重要[/the-next-big-crisis-for-data-teams-58ac2bd856e8]。
-
深度学习专家们,这篇文章适合你们:Shashank Prasanna的最新作品是对推动 PyTorch 2.0 的编译器技术的便捷、耐心的入门指南。
感谢你本周花时间与我们相伴!如果你喜欢在 TDS 阅读的文章,可以考虑 成为 Medium 会员 —— 如果你是符合条件国家的学生,不要错过 享受会员大幅折扣的机会。
直到下一个变量,
TDS 编辑部
2023 年从数据分析师转型为数据科学家
原文:
towardsdatascience.com/making-the-jump-from-data-analyst-to-data-scientist-in-2023-74e2cf7fc139
从数据分析师转型为数据科学家所需的技能和资源。
·发表于Towards Data Science ·阅读时间 12 分钟·2023 年 1 月 2 日
--
图片由作者使用Canva创建。
冒名顶替综合症、挫折、怀疑。这些只是我在 2018 年从数据分析师转型为数据科学家时经历的一些问题。在过去的五年里,人工智能的进步带来了改变世界的技术,例如大型语言模型和变换器模型、扩散模型以及改进的计算机视觉模型。尽管过去五年里自学数据科学的资源数量和质量急剧增长,但从数据分析师过渡到数据科学家所需的技能依然不变。
从数据分析师到数据科学家的转型让我感到不堪重负。我对自己资质的了解有限,不知道自己是否申请了合适的职位,也没有一个可以请教的行业人士。这篇文章包含了我在自己的转型过程中学到的一切,以及我希望从一开始就知道的内容,包括如何利用现有技能、自学数据科学、需要掌握的基本技能以及一系列特定的(免费的)资源,以帮助你转型成为数据科学家。
数据分析师和数据科学家的区别是什么?
虽然分析师有从过去的数据中获取、分析并做出商业建议的经验,但科学家本质上是利用这些数据创建预测未来的产品。在这方面,“未来”并不总是意味着预测尚未发生的事件,但很多时候意味着预测现有“未见”数据的结果。尽管数据科学角色需要比分析师角色更高级的技能,但你需要掌握的技能并不一定难以学习。
数据分析师与数据科学家的技能
数据分析师和数据科学家之间的具体技能有所不同。下表概述了数据分析师和数据科学家常用技能和工具之间的差异。需要特别注意的一点是,这两个类别并不是互斥的。例如,一些数据分析师可能已经在当前职位中使用 R 或 Python 进行基本任务。同样,数据科学家可能会选择使用 Excel 和描述性统计方法来处理某些不需要预测模型的项目。
作者创建的图表。
利用并理解你当前的分析技能
你当前的职位名称可能不包括“数据分析师”这几个字,但这并不意味着你不是数据分析师。你可能是业务分析师、商业智能分析师、质量保证分析师、客户成功分析师、金融分析师、项目分析师或供应链分析师。关键是:你是否利用分析数据获得的洞察帮助他人做出商业决策?如果答案是肯定的,那么你就是数据分析师!
为了更好地理解你可以带到数据科学职位的技能,问自己关于当前和过去工作的以及教育背景的问题。
你分析了哪些数据类型?
-
定性数据还是定量数据?
-
文本还是数字?
-
名义型、序数型还是类别型?
-
离散数据还是连续数据?
-
实时(流式)还是历史数据?
-
结构化、半结构化还是非结构化数据?
-
时间序列数据还是地理位置数据?
-
用户数据还是操作数据?
你如何与数据进行互动?使用了哪些工具?
-
你如何与数据进行互动?使用了哪些工具?
-
从各种来源获取数据(数据库、Google 表格、Excel 文件)
-
清理数据(标准化、去除异常值、通过 vlookups 合并数据、特征工程)
-
分析数据(数据透视表、图表、SQL 查询、R/Python、SPSS)
-
沟通发现(与内部、外部利益相关者)
-
提出商业建议并做出数据驱动的决策
你是如何沟通你的发现的?
-
PowerPoint 演示文稿还是书面报告?
-
研讨会还是电话会议?
你在什么业务领域或学术专长方面有经验?
-
金融服务——保险、投资、会计、审计
-
医疗保健——公共卫生、流行病学、病毒学、临床研究
-
学术领域——物理、化学、生物、遗传学、统计学
在审阅上述信息时,尽可能具体 提供细节。
你与多大的数据集互动过?你使用过哪些类型的 SQL 查询?你建立过哪些类型的统计模型,并使用了哪些指标来评估它们?你有哪些 Python 包的经验?你在分享数据见解时运用了哪些类型的沟通技能?你向哪些利益相关者提供了数据驱动的建议?你的数据驱动决策带来了多少收入或节省?
在从数据分析师转型为数据科学家时,不要低估你的非技术技能和经验的重要性。作为分析师,你带来了一个重要的资产:你已经知道如何以批判性和创新性的方式思考数据。提升你的技术技能时,拥抱这种思维方式。
学习数据科学基础的方法(免费)
CHUTTERSNAP 拍摄的照片,来源于 Unsplash
在线学习平台
如果你完全是编程新手,我建议投资加入一个在线学习平台,如 DataCamp 或 Codeacademy。这是立即开始学习的最快方式,无需下载 Python 到你的电脑上并设置编程环境。开始时只学习你希望的编程语言 Python 或 R,然后再尝试实际的数据科学工作。以下是一些推荐的课程:
-
DataCamp — Python 入门
-
DataCamp — 学习 Python 3
YouTube
如果你的预算有限(或即使没有),YouTube 上有一些极好的资源用于学习技术数据科学技能。
-
3blue1brown — 广泛的数学和统计视频(很棒的神经网络视频)
-
Mosh 编程 — 为有志的软件工程师提供完整课程和较小的专题视频
-
Sentdex — 学习 Python、数据可视化、机器学习和深度学习的视频
免费的在线大学课程
成为数据科学家的道路上,你不一定需要完成(并支付费用)证书课程。在我的经验中,证书在简历上看起来不错,但雇主更关注你完成的实际项目和你建立的模型。
尽管如此,战略性地选择单一在线课程以学习特定技能或技术,可以让你比在线学习平台更深入地探讨这些主题。这些课程围绕特定课程大纲组织,并且通常由全球顶尖大学的教授授课。以下是一些优秀的例子:
-
监督学习:回归与分类 — 我推荐从 Andrew Ng 教授的这个 Coursera 课程开始。理解监督学习为学习更高级的概念,如深度学习,提供了最好的基础。
-
哈佛大学 CS50 — 根据freeCodeCamp的说法,这是世界上最受欢迎的计算机科学入门课程之一,由世界顶级计算机科学家之一教授。
-
斯坦福大学 CS101 — 一门自定进度的课程,教授计算机科学的基本知识,不需要任何经验。
-
斯坦福大学 CS329S — 由 MLOps 大师Chip Huyen教授,这门课程教你如何创建可部署、可靠和可扩展的机器学习系统。
现实世界的数据科学项目
根据美国劳工统计局的数据,预计数据科学家的需求在 2021 到 2031 年间将增长 36%。由于高需求、高薪资、极大的灵活性以及几乎无限的在线学习机会,数据科学职位从未像现在这样受到求职者的青睐。五年前,当我告诉人们我是一名数据科学家时,我通常需要解释我的工作。现在,当我说我是数据科学家时,人们会问我对 ChatGPT 或 AI 艺术的看法。这意味着初级数据科学岗位的竞争非常激烈。
因此,现实世界的数据科学经验比以往任何时候都更为重要。通过原创代码(而非泰坦尼克号分类器的复制品)展示你的编程技能将使你脱颖而出。这体现了你的编码、分析和解决问题的能力以及创造力。有很多方法可以获得这些经验,我无法一一列举,但这些是我最喜欢的一些方法。
通过数据为善(志愿者)学习数据科学
数据为善运动的核心在于利用数据科学和工程技能来促进社会公益。如果你通过像DataKind或CorrelAid这样的专门组织进行志愿服务,你可以申请参与那些已经经过审核、规划和组织的项目。通过志愿服务,你将学习并应用新技能,与其他数据科学家和工程师合作和建立网络,同时在一个有趣且有意义的项目中积累在异步环境下工作的经验。
在你当前的工作中启动一个数据科学项目
如果你已经在担任数据分析师的职位,你可能已经与公司中的数据科学家有所接触。如果没有,主动联系数据科学团队负责人或一位高级数据科学家,或请求你的团队负责人进行介绍。提出问题以了解团队的工作内容,并提出一个满足特定需求的小项目。通过这个过程,你不仅可以获得有价值的技术技能,还能更深入地了解数据科学解决方案如何针对特定的商业问题进行定制。
注意:在要求与其他团队合作时考虑职场政治。
创建一个小型数据科学作品集
在面试之前向潜在雇主展示你的技能。 创建有效数据科学作品集的关键是从小处开始,专注于那些对你有趣的项目!以下是如何入门:
网站
暂时不用担心自定义域名和托管。要快速且免费地上线,可以使用GitHub Pages或datascienceportfol.io。我选择了后者作为我的作品集,因为它是最快的选择,并且不需要我进行编码。
项目
创建小范围项目并逐步更新。可以从数据清洗或可视化项目开始,逐渐过渡到建立模型。以下是一些入门建议:
-
网络爬虫
-
情感分析
-
时间序列分析
-
二分类器
-
回归模型
-
主题建模
-
文本摘要
参与数据科学(和开源)社区
我获取(并保持)灵感的最爱方式是参加数据科学聚会,比如PyLadies Berlin和PyData Berlin。聚会由志愿者组织,通常由一家目前招聘数据相关职位的本地公司赞助。社区成员展示他们最新的个人和工作相关项目(类似迷你会议),并在聚会前后留出时间进行网络交流。
如果你还不是数据科学家,这不应成为你对这些聚会感到畏惧的理由。尽管我在这些活动中遇到了许多经验丰富的、杰出的数据科学家和工程师,但我遇到的有志成为数据科学家和工程师的人更多。
参与开源项目是另一种参与实际项目并提升编程和工程技能的方式。考虑贡献一些你最常使用的包,如Pandas、NumPy或scikit-learn。请记住,这些包的开发和维护几乎完全由志愿者完成,这让我们有工作可做,因此考虑回馈一下!
你是否应该获得数据科学硕士学位?
简短的回答是:不必要。因为数据科学的本科和研究生课程仍然很新,它们尚未被大多数数据科学职位所要求。此外,新学位项目通常需要几年时间才能在学术界建立声誉。
根据我的经验,雇主对你构建了哪些统计模型和数据管道以解决实际问题更感兴趣,而不是你上过哪些大学课程。在数据科学领域,“做”比“知道”更重要。 在考虑高级学位项目或付费证书之前,尝试完成几个从头到尾的实际数据科学项目。
你需要的最低限度数据科学技能
图片由Sarah Dorweiler提供,来源于Unsplash
我遇到了太多试图在申请数据科学职位之前学习所有数据科学知识(这几乎是不可能的)的有志数据科学家。但鉴于现在的职位描述写作方式,我几乎不能责怪他们。当你申请第一个数据科学职位时,记住,所列出的所需技能和技术只是一个愿望清单。
来自 FastAPI 创作者 Sebastián Ramírez 的热门推文。
此外,如果你符合某个职位的所有技能要求,你可能会被认为是过于优秀的候选人。 在申请我的第一个数据科学职位时,我制定了自己的经验法则,这一法则立刻提高了我的面试与申请比例:如果你认为你可以在工作中学会做某事,就把它当作一种技能来声明。
以下是一些示例:
-
如果你懂 R 语言,假设你可以学习 Python。
-
如果你已经构建了回归模型,假设你可以构建 XGBoost 模型。
-
如果你有 MySQL 经验,假设你可以使用 SQLite。
以下是我推荐在申请第一个数据科学职位之前,应该熟练掌握的最低限度技能列表。
编程
Git
-
创建新仓库
-
克隆现有的仓库
-
查看新分支
-
提交并推送更改
环境管理(Python)
-
设置虚拟环境(使用
virtualenv
、conda
) -
安装新包
-
使用
requirements.txt
文件
SQL
-
选择和过滤数据,合并来自多个表的数据
-
(其他操作请在 Pandas 中完成!)
Python
-
本地读写数据
-
列表推导式
-
数据类型
-
编写自定义函数
-
解析字符串
-
scikit-learn
-
将数据分成训练集和测试集
-
训练,评估模型
-
进行预测
-
-
Pandas
-
数据聚合
-
合并,连接
-
删除重复项
-
子集数据
-
按列排序
-
使用 apply 和 lambda 函数创建新列
-
-
Matplotlib 或 Plotly
-
直方图,密度图
-
散点图
-
条形图
-
热图
-
-
基本自然语言处理
-
分词
-
词干提取,词形还原
-
TF-IDF
-
统计分析和可视化工具
理解数据
-
探索性数据分析(EDA)
-
相关性图(用于确定特征重要性和识别混淆变量)
-
分析数据分布并识别偏斜
-
数据聚合
-
数据清理(见“Python 技能”)
-
处理缺失值和异常值
-
数据标准化(在构建模型之前)
-
了解多少数据足以构建模型
统计学
-
集中趋势度量(均值、中位数、众数、标准差)
-
统计显著性的 T 检验
可视化
-
图表(直方图、密度图、散点图、热图)
-
最佳拟合线(用于回归)
-
ROC 曲线
机器学习
模型构建
-
确定是否真的需要 ML 模型?!
-
特征工程
-
监督学习
-
线性回归
-
逻辑回归
-
CART/决策树模型(对代码测试非常有帮助)
-
-
无监督学习
-
聚类,主题模型
-
处理多标签模型的类别不平衡
-
模型评估
-
识别模型过拟合的情况
-
将模型保存为 pickle 文件,加载和推断它们
-
使用混淆矩阵指标(AUC、精确度、召回率、准确率等)进行模型评估
-
分析预测概率的分布
结论
找到第一份数据科学工作的过程需要信心、耐心、自律和毅力。作为一名拥有两年相关工作经验的定量数据分析师,我花了超过八个月的全职求职才收到数据科学职位的录用通知。 在这段时间里我学到了很多,成为数据科学家后我学到的更多。
如果你想做出转变,以下是我的一些顶级建议总结:
-
通过问自己有关你有经验的数据类型的问题,更好地了解你当前的技能。
-
在线学习平台(即 DataCamp、Codecademy)是如果你没有编码经验时学习 R 或 Python 的最快方式。
-
在考虑攻读数据科学高级学位之前,先利用好在线课程、YouTube 视频、GitHub 仓库和博客等免费资源。
-
参与动手项目是同时提高技术技能和软技能的最有效方式(而且是免费的)。
-
考虑与数据公益组织志愿服务或参与开源项目,以获得实际经验。
-
如果你符合特定职位的所有要求,你已经超出了资格要求。
无论你在成为数据科学家的旅程中处于何处,我都祝你好运!查看我的数据科学资源 备忘单 和下面的链接以获取相关内容。
图片和内容由作者生成。点击 这里 下载。
如果你希望保持对最新数据科学趋势、技术和包的了解,可以考虑成为 Medium 会员。你将获得对如 Towards Data Science 等文章和博客的无限访问权,并且支持我的写作。(每个会员我会赚取一小部分佣金)。
[## 使用我的推荐链接加入 Medium - Mary Newhauser
阅读 Mary Newhauser 的所有故事(以及 Medium 上成千上万其他作家的故事)。你的会员费直接支持…
medium.com](https://medium.com/@mary.newhauser/membership?source=post_page-----74e2cf7fc139--------------------------------)
想要联系吗?
我还写了:
清理文本数据的干净方法
towardsdatascience.com [## 对参议员推文进行 DistilBERT 微调
关于使用 snscrape、SQLite 和 Transformers(PyTorch)对美国参议员的推文进行 DistilBERT 微调的指南…
参考资料
(1) J. Alammar,《插图版 Transformer》 (2018)。
(2) S. Rangwala,计算机视觉的进展推动交通自主 (2022)。
(3) R. O’Connor,机器学习的扩散模型介绍(2022 年)。
(4) A. Bridgwater,13 种数据类型(2018 年)。
(5) K. Pannetta,利用数据实现社会公益(2018 年)。
(6) 劳工统计局,职业展望手册,数据科学家(2022 年)。
做出正确决策:AI 建议、决策辅助工具以及大语言模型的前景
探索大语言模型带来的决策新时代
·
关注 发表在Towards Data Science · 10 分钟阅读 · 2023 年 9 月 28 日
--
介绍
人工智能的民主化导致了 AI 系统在各种领域的采用。最近一波生成模型,例如预训练的大型语言模型(LLMs),已广泛应用于我们日常生活中的不同活动——从通过帮助撰写邮件提高生产力,到帮助解决令新手和专家作家都感到困扰的“空白页”难题。由于对 LLMs 在决策中的依赖日益增加,本文提供了人类决策和人类与 AI 决策演变的综合分析。最后,文章反思了 LLMs 在辅助决策任务中提供的机会以及对 LLMs 决策依赖的相关威胁。
人类决策
在一个几乎每个日常决策(例如,购买食品或穿着衣物、阅读书籍、听音乐或观看电影,从生活方式选择到旅行目的地)的选择范围不断扩大的世界中,决策质量受到了重新关注。在他有影响力的作品中揭示“选择悖论”的巴里·施瓦茨(Barry Schwartz)阐述了这种随着千年技术进步而逐渐增加的决策困难。施瓦茨通过一个例子来解释这一点:医生向患者提供一系列治疗方案,传达潜在风险并将其与每种治疗的收益进行权衡。在这种情况下,高风险决策的负担从专家医生转移到非专家患者。除了其他因素外,选择的过多往往会阻碍有效的人类决策。
不同的研究社区,从进化心理学到认知科学和神经科学,已经探索了人类决策的性质以及塑造人类决策过程的各种因素。人类决策受认知偏差困扰,充满了非理性,这一点并不鲜见。这一点在诺贝尔奖获奖行为经济学家丹尼尔·卡尼曼的《思考,快与慢》一书中得到了最著名的记录。
由 Robynne Hu 提供的照片,来源于 Unsplash
人类与人工智能的决策
技术的出现带来了决策支持系统的增长,这些系统可以帮助人类克服决策过程中遇到的障碍。决策支持系统在更广泛的社会技术背景下有各种形态和形式——从驱动用户交互的算法到帮助用户进行预测和预报的复杂机器学习模型。例如,推荐系统可以通过向用户展示可能最能满足其需求的内容或产品来提供帮助。其他算法系统则可以挖掘大量数据,以在各种决策任务中向用户提供建议。
所有的人类与 AI 决策制定背景中一个共同的核心目标是通过将人类智能与算法系统的计算能力相结合,提升决策效果。然而,这与许多协作的人类与 AI 决策过程在现实世界中的展开方式相去甚远。人类在决策任务中未能适当地依赖 AI 系统,从而导致团队表现次优。适当的依赖被概念化为在人类依赖 AI 建议时 AI 正确时的依赖,以及 AI 不正确时的人类自我依赖[11]。有许多因素在塑造这些结果中发挥作用——人类因素(例如,领域知识、对技术交互的亲和力、先前经验);系统因素(例如,AI 系统的准确性或信心);任务因素(例如,任务复杂性、任务不确定性、风险)。
对人类与 AI 在各种背景下的决策制定的实证探索,包括贷款申请决策和医学诊断,已揭示出人类要么对 AI 建议的依赖不足,错失了改善决策结果的机会,要么对 AI 建议的依赖过度,从而导致次优结果。为了应对过度依赖和不足依赖的问题,并促进对 AI 建议的适当依赖,先前的研究提出了使用解释[13]、认知强制功能(即,在决策过程中强制进行关键考虑和反思的干预措施)[4]、传达 AI 系统优缺点的教程或培训课程,以及提高人群一般 AI 素养的倡议。最近的研究提出了一种名为“评估性 AI”的替代框架,以促进对 AI 建议的适当依赖。该框架建议决策支持工具应提供支持和反对人们所做决策的证据,而不是提供接受或拒绝的推荐[7]。
认知偏差也影响了人机决策[1, 3]。Rastogi 等人[9]认为,我们对决策任务的整体感知和理解可能会受到认知偏差的扭曲,例如确认偏差、锚定偏差和可得性偏差。他们探讨了锚定偏差的作用,并提出了减少其对协作决策表现负面影响的方法。He 等人[22]展示了厄尔斯效应(一种元认知偏差)如何影响人们对 AI 系统建议的依赖。他们揭示了那些高估自己能力或表现的用户倾向于对 AI 系统表现出过少的依赖,从而阻碍了决策任务中的最佳团队表现。其他因素,如算法厌恶和欣赏,也被证明对人机决策的成果有影响[17]。
尽管在人机协作的广泛领域中正在进行持续的工作,但在决策任务中如何适当地依赖 AI 系统仍然是一个未解的问题。人工智能、机器学习和人机交互交汇的不同研究社区正积极推进我们对这一领域的理解,并开发方法、工具和框架,帮助我们从人机协作的潜力中受益。
目前,大型语言模型(LLMs)在各个领域得到了广泛的应用和采纳。在本文的余下部分,我们将深入探讨 LLMs 在辅助人类决策中提供的机会以及潜在的好处。
LLMs 用于决策任务
尽管大型语言模型(LLMs)表现出偏见和可能造成伤害的潜在风险,它们在各种社会技术系统中的使用仍在不断增加。话虽如此,它们也显示出了在大规模上产生积极影响的潜力——例如,通过支持审计过程,如 Rostagi 等人[8]所展示的,通过一个由生成型 LLM 驱动的审计工具。作者提议在商业语言模型的协同审计中利用人类和生成模型的互补优势。Wu 等人[14]提出了AutoGen,一个通过多代理对话支持复杂 LLM-based 工作流程的框架。AutoGen 可以支持在线决策任务,如游戏或网络互动。
一方面,有证据表明,像 GPT-3 这样的 LLMs 表现出与人类直觉惊人相似的行为,以及随之而来的认知误差[16]。最近的研究强调了使用 ChatGPT 进行放射学决策的可行性,潜在地改善临床工作流程和放射学服务的负责任使用[18]。为了增强决策过程中的 AI 安全性,Jin 等人[15]旨在复制并赋予 LLMs 在新颖或异常情况下决定是否打破规则的能力。另一方面,LLMs 可能无意中加剧对边缘化群体的刻板印象[20],并表现出涉及种族、性别、宗教和政治取向的偏见。与决策支持系统中培养适当信任和依赖的挑战类似,如果人类依赖 LLMs 做出决策,我们将需要更好地理解这种互动的利与弊。在 LLM 驱动的互动中,尤其突出的风险是通过使用解释在决策任务中创造的解释深度的假象,导致对 AI 系统的过度依赖。如果人类与决策支持系统的互动变得更加“无缝”(例如通过交互式或对话界面),我们可以预期会发现更多不适当依赖的实例。
通过它们的架构和超参数的视角研究 LLMs 变得越来越困难。此时有足够的证据表明,生成式 AI 可以产生高质量的书面和视觉内容,这些内容可以为社会福祉服务,也可能被误用以造成伤害。Potsdam Mann 等人[19]认为,为 LLMs 输出分配责任存在信誉-责备不对称性,并且伦理和政策影响集中在 LLMs 上。
接下来需要做什么?
显然,在决策任务中安全且健壮地使用 LLMs 需要更多的研究和实证工作。特别是考虑到目前在多模态和多语言 LLMs 方面的限制。以下是一些仍然至关重要的问题汇编,用于确定我们可以从将 LLMs 嫁接到日常决策中中持续获益的程度:
-
如何促进对 LLMs 或 LLM 注入系统进行有效决策支持的适当依赖?
-
我们如何提高 LLM 注入的决策支持系统的稳健性、可靠性和信任度?
-
如何在多模态和多语言决策环境中培养对 LLMs 的适当信任和依赖?
-
如何支持不同能力、个人特征、先验知识、教育和资格以及其他人口统计学特征的人在 LLMs 决策任务中得到平等的支持?
因此,如果你手边有一个大语言模型,还是不要急于把它当作黑箱决策辅助工具来依赖!
Dr. ir. Ujwal Gadiraju 是代尔夫特理工大学的终身副教授。他共同指导了Delft “Design@Scale” AI 实验室并领导了一个以人为本的 AI 与众包计算研究方向。他是 ACM 的杰出演讲者,也是CHI Netherlands的董事会成员。Ujwal 在Toloka AI的 AI、数据和研究团队中花费部分时间工作,同时也是Deeploy的顾问委员会成员,Deeploy 是一家正在成长的 MLOps 公司。
参考文献
-
Bertrand, A., Belloum, R., Eagan, J. R., & Maxwell, W. (2022 年 7 月). 认知偏差如何影响 XAI 辅助决策:系统评估. 见于 2022 AAAI/ACM AI、伦理与社会会议论文集(第 78–91 页)。
-
Bossaerts, P., & Murawski, C. (2017). 计算复杂性与人类决策. 认知科学趋势, 21(12), 917–929。
-
Boonprakong, N., He, G., Gadiraju, U., van Berkel, N., Wang, D., Chen, S., Liu, J., Tag, B., Goncalves, J. 和 Dingler, T., 2023. 关于理解和缓解人类与 AI 协作中的认知偏差的研讨会。
-
Buçinca, Z., Malaya, M. B., & Gajos, K. Z. (2021). 信任还是思考:认知强制函数可以减少对 AI 的过度依赖. ACM 人机交互学报, 5(CSCW1), 1–21。
-
Haupt, C. E., & Marks, M. (2023). AI 生成的医学建议——GPT 及其超越。 Jama, 329(16), 1349–1350。
-
Kahneman, D. (2011). 思考,快与慢。 麦克米伦。
-
Miller, T. (2023 年 6 月). 可解释 AI 已死,万岁可解释 AI!使用评估性 AI 的假设驱动决策支持. 见于 2023 ACM 公平性、问责性与透明度会议论文集(第 333–342 页)。
-
Rastogi, C., Tulio Ribeiro, M., King, N., Nori, H., & Amershi, S. (2023 年 8 月). 支持人类与 AI 协作的审计 LLMs. 见于 2023 AAAI/ACM AI、伦理与社会会议论文集(第 913–926 页)。
-
Rastogi, C., Zhang, Y., Wei, D., Varshney, K. R., Dhurandhar, A., & Tomsett, R. (2022). 快速与缓慢决策:认知偏差在 AI 辅助决策中的作用. ACM 人机交互学报, 6(CSCW1), 1–22。
-
Santos, L. R., & Rosati, A. G. (2015). 人类决策的进化根源. 心理学年鉴, 66, 321–347。
-
Schemmer, M., Hemmer, P., Kühl, N., Benz, C., & Satzger, G. (2022). 我是否应该听从基于 AI 的建议?衡量人类与 AI 决策中的适当依赖性. arXiv 预印本 arXiv:2204.06916。
-
Schwartz, B. (2004). 选择的悖论:为何更多更少。 纽约。
-
Vasconcelos, H., Jörke, M., Grunde-McLaughlin, M., Gerstenberg, T., Bernstein, M. S., & Krishna, R. (2023). 解释可以减少在决策过程中对 AI 系统的过度依赖。ACM 人机交互学报,7(CSCW1),1–38。
-
Wu, Qingyun, Gagan Bansal, Jieyu Zhang, Yiran Wu, Shaokun Zhang, Erkang Zhu, Beibin Li, Li Jiang, Xiaoyun Zhang, 和 Chi Wang. AutoGen:通过多代理对话框架启用下一代 LLM 应用。arXiv 预印本 arXiv:2308.08155 (2023)。
-
Jin, Z., Levine, S., Gonzalez Adauto, F., Kamal, O., Sap, M., Sachan, M., Mihalcea, R., Tenenbaum, J. 和 Schölkopf, B., 2022. 何时例外:将语言模型作为人类道德判断的解释。神经信息处理系统进展,35, 第 28458–28473 页。
-
Hagendorff, T., Fabi, S., & Kosinski, M. (2022). 机器直觉:揭示 GPT-3.5 中的类人直观决策。arXiv 预印本 arXiv:2212.05206。
-
Erlei, A., Das, R., Meub, L., Anand, A., & Gadiraju, U. (2022 年 4 月). 值得关注的是:人类会忽略其经济自利以避免与 AI 系统讨价还价。见于 2022 年 CHI 计算机系统人因会议论文集(第 1–18 页)。
-
Rao, A., Kim, J., Kamineni, M., Pang, M., Lie, W., & Succi, M. D. (2023). 评估 ChatGPT 作为放射科决策的辅助工具。medRxiv, 2023–02。
-
Porsdam Mann, S., Earp, B. D., Nyholm, S., Danaher, J., Møller, N., Bowman-Smart, H., … & Savulescu, J. (2023). 生成式 AI 涉及信贷–责备不对称。自然机器智能,1–4。
-
Dhingra, H., Jayashanker, P., Moghe, S., & Strubell, E. (2023). 酷儿首先是人:解构大语言模型中的性别身份刻板印象。arXiv 预印本 arXiv:2307.00101。
-
He, G., Kuiper, L., & Gadiraju, U. (2023 年 4 月). 了解认知:人类能力的幻觉可能阻碍对 AI 系统的适当依赖。见于2023 年 CHI 计算机系统人因会议论文集(第 1–18 页)。
使用 PyTorch、ONNX 和 TensorRT 将视觉变换器的预测速度提高 9 倍
如何使用 16 位浮点数、TensorRT、网络重写和多线程来显著加速深度学习模型预测
·发表于Towards Data Science ·阅读时间 11 分钟·2023 年 6 月 4 日
--
Sanjeevan SatheesKumar拍摄于Unsplash
诸如UNET、SwinUNETR这样的视觉变换器在计算机视觉任务中,如语义分割,都是最先进的。然而,这些模型进行预测需要大量时间。本文展示了如何将这种模型的预测速度提高 9 倍。这一改进为许多实时或接近实时的应用铺平了道路。
肿瘤分割任务
为了设置场景,我使用 SwinUNETR 模型从胸部 CT 扫描图像中分割肺肿瘤,这些图像是单通道灰度 3D 图像。以下是一个示例:
-
左侧列展示了来自 3D CT 扫描图像的几幅 2D 切片,处于轴向平面。两个弯月形的黑色区域是肺部。
-
右侧列显示了肺肿瘤的手动标注。
胸部 CT 扫描通常尺寸为 512×512×300,存储在磁盘上大约需要 60 到 90 兆字节。它们不是小图像。
我使用 PyTorch 训练了一个 SwinUNETR 模型来分割肺肿瘤。经过训练的模型对胸部 CT 扫描进行预测大约需要 10 秒。因此,每张图像 10 秒是我的起点。
在我们讨论速度优化之前,先来看一下模型的输入和输出,以及它是如何进行预测的。
模型输入和输出形状
模型输入和输出,由作者提供
-
输入是一个表示胸部 CT 扫描的 3D numpy 数组。
-
SwinUNETR 模型无法处理整个图像,因为它太大了。一个解决方案是将图像切割成较小的块,称为感兴趣区域(ROIs)。在我的设置中,感兴趣区域的大小为 96×96×96。
-
SwinUNETR 模型一次只看到一个感兴趣区域,输出两个二值分割掩码,一个用于肿瘤类别,另一个用于背景类别。这两个掩码的大小都是感兴趣区域的大小,即 96×96×96。更准确地说,SwinUNETR 输出两个未归一化的类别概率掩码。在后续步骤中,这些未归一化的掩码通过softmax归一化为 0 到 1 之间的概率,然后通过argmax转化为二值掩码。
-
这些掩码根据对应感兴趣区域的切割方式进行合并,以生成两个全尺寸的分割掩码——肿瘤掩码和背景掩码——每个掩码的大小与整个胸部 CT 扫描相同。请注意,尽管模型返回了两个分割掩码,但我们只关注肿瘤掩码,会忽略背景掩码。
-
输入和输出数组,以及模型,使用的是 32 位浮点数。
滑动窗口推断
以下伪代码实现了上述预测思路。
滑动窗口推断,作者提供的图像
注意本文中的代码片段是伪代码,以保持简洁。根据相同的逻辑,像split_image这样的实现显而易见的方法留给读者自行实现。
-
sliding_window_inference方法接受完整的 CT 扫描image和一个 PyTorch model。它还接受一个batch_size,因为一个感兴趣区域很小,GPU 可以同时处理多个区域进行预测。batch_size指定了发送到 GPU 的感兴趣区域的数量。sliding_window_inference返回二值肿瘤分割和背景掩码。
-
该方法首先将整个图像划分为感兴趣区域,然后将这些区域分组为batch,每组包含batch_size个感兴趣区域。在这里,我假设感兴趣区域的数量可以被batch_size整除,以简化代码。
-
每个batch会被发送到model以生成一批预测结果。每个预测结果对应一个单独的感兴趣区域。
-
最终,所有感兴趣区域的预测结果被合并形成两个全尺寸的分割掩码。合并过程还包括softmax和argmax。
对图像进行预测的代码片段
以下代码片段调用了sliding_window_inference方法,以对加载到第一个 GPU “cuda:0” 的图像文件进行预测,图像作为 PyTorch 张量:
调用模型预测的代码片段,由作者提供
在上述设置下,我现在介绍一组策略,以加快模型预测速度。
策略 1:在 16 位浮点数中进行预测
默认情况下,训练好的 PyTorch 模型使用 32 位浮点数。但通常情况下,16 位浮点数精度足以提供非常相似的分割结果。使用单个 PyTorch API half 就可以轻松将 32 位模型转换为 16 位模型:
16 位浮点数精度的预测,作者图片
这个策略将预测时间从 10 秒减少到 7.7 秒。
策略 2:将模型转换为 TensorRT
TensorRT 是 Nvidia 提供的一款软件,旨在为深度学习模型提供快速推理。它通过将通用模型(如 PyTorch 模型或 TensorFlow 模型,能够在多种硬件上运行)转换为仅在特定硬件上运行的 TensorRT 模型来实现这一目标——即你进行模型转换的硬件。在转换过程中,TensorRT 还进行许多速度优化。
来自 TensorRT 安装的 trtexec 可执行文件执行转换。问题是,有时将 PyTorch 模型转换为 TensorRT 模型会失败。在 PyTorch SwinUNETR 模型上失败。特定的失败消息并不重要,你会遇到自己的错误。
重要的是要知道有一个解决方法。解决方法是首先将 PyTorch 模型转换为中间格式,ONNX,然后再将 ONNX 模型转换为 TensorRT 模型。
ONNX 是一种开源格式,用于表示机器学习模型。ONNX 定义了一组通用的操作符——机器学习和深度学习模型的构建块——以及一个通用的文件格式,使 AI 开发者能够在各种框架、工具、运行时和编译器中使用模型。
好消息是,将 ONNX 模型转换为 TensorRT 模型的支持优于将 PyTorch 模型转换为 TensorRT 模型的支持。
将 PyTorch 模型转换为 ONNX 模型
以下代码片段将 PyTorch 模型转换为 ONNX 模型:
将 PyTorch 模型转换为 ONNX 模型,作者提供
它首先为单个兴趣区域创建随机输入。然后使用已安装的onnx Python 包中的 export 方法来执行转换。此转换会生成一个名为 swinunetr.onnx 的文件。参数 dynamic_axes 指定 TensorRT 模型应支持输入的第 0 维的动态大小,即批量维度。
将 ONNX 模型转换为 TensorRT 模型
现在我们可以调用 trtexec 命令行工具将 ONNX 模型转换为 TensorRT 模型:
将 ONNX 模型转换为 TensorRT 模型的 trtexec 命令行,作者提供
-
onnx=swinunetr.onnx 命令行选项指定了 onnx 模型的位置。
-
saveEngine=swinunetr_1_8_16.plan 选项指定生成的 TensorRT 模型的文件名,称为计划。
-
fp16 选项要求转换后的模型以 16 位浮点精度运行。
-
minShapes=modelInput:1×1×96×96×96 指定生成的 TensorRT 模型的最小输入大小。
-
maxshapes=modelInput:16×1×96×96×96 指定生成的 TensorRT 模型的最大输入大小。由于在 PyTorch 转换为 ONNX 的过程中,我们只允许第 0 维,即批量维度,支持动态大小,因此在 minShapes 和 maxShapes 中,只有第一个数字可以改变。它们一起告诉 trtexec 工具输出一个可以用于批量大小在 1 到 16 之间的输入的模型。
-
optShapes=modelInput:8×1×96×96×96 指定生成的 TensorRT 模型在批量大小为 8 时运行最快。
-
workspace=10240 选项为 trtexec 提供 10G 的 GPU 内存来进行模型转换。
trtexec 将运行 10 到 20 分钟,并输出生成的 TensorRT 计划文件。
使用 TensorRT 模型进行预测
以下代码片段加载 TensorRT 模型计划文件,并使用从stackoverflow中改编的 TrtModel:
使用 TensorRT 模型进行预测,由作者提供
请注意,即使在 trtexec 命令行中,我们指定了 fp16 选项,在加载计划时,我们仍然需要指定 32 位浮点数。很奇怪。
对从 stackoverflow 获得的 TrtModel 需要进行一些小调整,但你会解决的。这并不难。
使用这个战术,预测时间为 2.89 秒!
战术 3:包装模型以返回一个掩码
我们的 SwinUNETR 模型返回两个分割掩码,一个用于肿瘤,一个用于背景,以未归一化概率的形式。这两个掩码首先从 GPU 转移到 CPU。然后在 CPU 中,这些未归一化的概率会被 softmax 转换为 0 和 1 之间的适当概率,最后通过 argmax 生成二值掩码。
由于我们只使用肿瘤掩码,因此模型无需返回背景掩码。GPU 和 CPU 之间的数据传输需要时间,而 softmax 等计算也需要时间。
要获得只返回单一掩码的模型,我们可以创建一个新的类来包装 SwinUNETR 模型:
SwinUNETR 包装器以返回单一掩码,由作者提供
下图展示了新的模型输入输出:
SwinWrapper 输入和输出,由作者提供
forward 方法通过神经网络的前向传递处理一批感兴趣区域以进行预测。在这个方法中:
-
原始模型首先在传入的感兴趣区域上调用,以获取两个分割类别的预测。输出形状为 Batch×2×Width×Height×Depth,因为在当前的肿瘤分割任务中,有两个类别——肿瘤和背景。结果存储在out变量中。
-
然后对两个未归一化的分割掩码应用softmax,将它们转换为介于 0 和 1 之间的归一化概率。
-
然后仅选择肿瘤类别,即类别 1,返回给调用者。
实际上,这个封装器实现了两个优化:
-
仅返回一个分割掩码,而不是两个。
-
将softmax操作从 CPU 移到 GPU 中。
那么argmax操作呢?由于只返回一个分割掩码,所以不需要argmax。相反,为了创建原始的二进制分割掩码,我们将进行tumour_segmentation_probability ≥ 0.5,其中tumour_segmentation_probability是 SwinWrapper 中的forward方法的结果。
由于 SwinWrapper 是一个 PyTorch 模型,我们需要再次进行 PyTorch 到 ONNX 和 ONNX 到 TensorRT 的转换步骤。
在将 SwinWapper 模型转换为 ONNX 模型时,唯一需要更改的是使用封装模型:
将封装的 SwinUNETR 模型转换为 ONNX,由作者提供
将 ONNX 模型转换为 TensorRT 计划的trtexec命令行保持不变,所以我不会在这里重复。
这个策略将预测时间从 2.89 秒减少到2.42 秒。
策略 4:将感兴趣区域分配到多个 GPU
以上所有策略仅使用一个 GPU,但有时我们希望使用更昂贵的多 GPU 机器以实现更快的预测。
其思路是将相同的 TensorRT 模型加载到n个 GPU 中,在sliding_window_inference中,我们进一步将一批 ROIs 分割为n个部分,并将每部分发送到不同的 GPU。这种方式下,SwinWrapper 网络的耗时前向传递可以并行处理不同部分。
我们需要将sliding_window_inference方法改为以下的sliding_window_inference_multi_gpu:
多 GPU 滑动窗口推断,由作者提供
-
和之前一样,我们将感兴趣区域分成不同的批次。
-
我们根据提供的 GPU 数量将每个批次拆分成多个部分。
-
对于每个部分batch_per_gpu,我们将任务提交到 ThreadPoolExecutor 中。该任务对传入的部分执行模型推断。
-
submit方法立即返回一个 future 对象,表示任务完成时的结果。submit方法在任务完成前立即返回是至关重要的,这样我们可以在不等待的情况下将其他任务提交到不同的线程,实现并行处理。
-
在内层for 循环中提交所有任务后,等待所有 future 对象完成。
-
在任务完成后,从期望中读取结果并合并结果。
要调用这个新版本的sliding_window_inference_multi_gpu,请使用以下代码片段:
使用多个 GPU 进行模型预测,作者
-
我使用了两个 GPU,因此我创建了两个 TensorRT 模型,每个模型分别放在不同的 GPU 上,“cuda:0”和“cuda:1”。
-
然后我创建了一个包含两个线程的 ThreadPoolExecutor。
-
我将模型和执行器传递给sliding_window_inference_multi_gpu方法,类似于单 GPU 的情况,以获得肿瘤类别分割掩码。
这个策略将预测时间从 2.42 秒减少到1.38 秒!
现在我们有四种策略,将 SwinUNETR 模型的预测速度提高了 9 倍。还不错。但我们是否为了速度牺牲了预测精度?
我们是否为了速度牺牲了预测精度?
在这里“精度”一词指的是最终模型对肿瘤的分割效果,而不是浮点预测精度,例如 16 位、32 位精度。
为了回答这个问题,我们需要查看 DICE 指标,这个指标用于衡量分割模型的性能。
DICE 分数是通过预测的肿瘤与实际肿瘤重叠的比例来计算的。DICE 分数介于 0 和 1 之间;分数越高,模型预测越好:
-
DICE 1 表示完美预测,
-
DICE 0 表示完全错误的预测,或者根本没有预测。
让我们看看测试图像的 DICE 分数:
不同策略下的 Dice 分数,作者
我们可以看到,只有当我们在策略 1 中将 32 位 PyTorch 模型转换为 16 位模型时,DICE 分数从 0.93 略微下降到 0.91。其他策略不会降低 DICE 分数。这表明这些策略可以在仅有微小精度损失的情况下实现更快的预测速度。
结论
本文介绍了四种策略,通过使用 ONNX、TensorRT 和多线程等工具,使视觉变换器预测速度大大提升。
致谢
我要感谢我的朋友 Chunyu Jin。他向我展示了快速深度学习模型推断的可能性。他为我制作了第一个运行的 TensorRT SwinUNETR 模型,并建议了我在这里尝试的许多策略。
[## 通过我的推荐链接加入 Medium - Wei Yi
阅读 Wei Yi(以及 Medium 上的成千上万其他作者)的每一个故事。如果你喜欢我的故事,请考虑…
medium.com](https://medium.com/@jasonweiyi/membership?source=post_page-----dc1f09b6814--------------------------------)
使用 ipywidgets 让你的数据分析变得生动
原文:
towardsdatascience.com/making-your-data-analytics-come-to-life-using-ipywidgets-cfa9538279f7
了解如何使用小部件动态更新数据分析
·发表于Towards Data Science ·8 分钟阅读·2023 年 2 月 21 日
--
图片由John Schnobrich拍摄,来源于Unsplash。
对于我日常的数据分析任务,我最喜欢的开发环境绝对是 Jupyter Notebook。Jupyter Notebook 允许我快速修改代码并重新运行单元格以查看更新。然而,这一功能对使用我的 Jupyter Notebook 查看数据分析结果的用户并不友好。如果用户可以在不修改代码的情况下与我的程序进行交互,那将非常有用。这就是ipywidgets包发挥作用的地方。
ipywidgets是一个包含 Jupyter Notebook 交互式 HTML 小部件的包。通过使用 ipywidgets 中的小部件,你的 Jupyter 笔记本将变得生动,用户可以直接控制他们的数据并可视化数据的变化。在本文中,我将带你深入了解如何将 ipywidgets 与数据集一起使用。
安装 ipywidgets
在你的 Jupyter 笔记本中,按如下方式安装ipywidgets和widgetsnbextension包:
!pip install ipywidgets widgetsnbextension
然后,如下启用widgetsnbextension:
!jupyter nbextension enable --py widgetsnbextension --sys-prefix
使用interact
函数
对于我们的示例,我将使用位于www.kaggle.com/datasets/teertha/ushealthinsurancedataset?resource=download.
的保险数据集。
许可证😗 CC0: 公共领域. 描述 — 该数据集包含 1338 行被保险数据,其中保险费用是针对被保险人的以下属性给出的:年龄、性别、BMI、子女数量、是否吸烟和区域。这些属性包括数字变量和分类变量。
让我们首先将 CSV 文件加载到 Pandas DataFrame 中:
import pandas as pd
df = pd.read_csv('insurance.csv')
df
所有图片由作者提供
滑块控件
假设我想检索所有包含 3 个子项的行。我可以这样做:
df.query(f'children == 3')
如果我想检索具有 4 个子项的行,那么我必须修改我的语句并重新运行单元格。显然,这不是很高效。
这时,ipywidgets
包中的interact
函数就非常有用了:
from ipywidgets import interact
def f(children):
display(df.query(f'children == {children}'))
在上述代码片段中,我定义了一个名为f
的函数,该函数接受一个参数——children
。该函数将查询数据框并显示结果。接下来的语句就是魔法发生的地方:
interact(f, children = 5)
interact()
函数(ipywidgets.interact
)会自动创建用户界面(UI)控件(称为小部件),并将它们绑定到你指定的函数上。上述语句将生成一个包含IntSlider控件以及数据框的输出:
你传入
children
参数的值将决定生成什么类型的用户界面控件。在这里,你传入了一个整数,因此生成了一个滑块控件。如果你传入n
,它将生成一个取值范围为[-n,+3*n]的整数值滑块控件。
去拖动滑块吧。当你拖动时,数据框将动态更新。
我们可以对年龄字段做同样的操作,但这次我们要设置可选择的年龄范围:
def f(age):
display(df.query(f'age > {age}'))
interact(f, age = (df['age'].min(), df['age'].max()))
最小年龄为 18 岁,最大年龄为 64 岁。当前滑块的值将显示在中间位置——41(即(18+64)/2)。
如果你传入一个浮点数,比如 BMI:
def f(bmi):
display(df.query(f'bmi > {bmi}'))
interact(f, bmi = (df['bmi'].min(), df['bmi'].max()))
然后会显示一个FloatSlider控件:
下拉控件
那么选择区域呢?我们可以将区域作为列表传入:
def f(region):
display(df.query(f'region == "{region}"'))
interact(f, region = df['region'].unique())
现在你可以使用下拉控制选择四个区域中的一个:
如果我想选择所有区域呢?为此,你需要发挥创造力。首先,我会创建一个包含四个区域的元组列表:
regions = [(i.capitalize(),i) for i in df['region'].unique()]
上面的代码片段生成了以下列表:
[('Southwest', 'southwest'), ('Southeast', 'southeast'), ('Northwest', 'northwest'), ('Northeast', 'northeast')]
每个元组中的第一个元素是显示给用户的内容。第二个元素是所选项的值。
然后,将一个元组—(‘All Regions’,’@*’)
,添加到列表中:
regions.append(('All Regions','@*'))
现在regions
变量看起来是这样的:
[('Southwest', 'southwest'),
('Southeast', 'southeast'),
('Northwest', 'northwest'),
('Northeast', 'northeast'),
('All Regions', '@*')]
现在你可以将regions
变量传递给interact()
函数:
def f(region):
display(df.query(f'region.str.contains("{region}")'))
interact(f, region = regions)
请注意,我需要更改查询语句以使用 contains()
函数。你现在可以选择所有区域项,以从所有区域中选择行:
复选框小部件
当你向 interact()
函数传入布尔值时,它会显示一个 CheckBox
小部件。考虑以下示例:
YES, NO = 'yes', 'no'
def f(smoker):
display(df.query(f'smoker == "{YES if smoker == True else NO}"'))
interact(f, smoker = True)
上面的代码片段显示了一个复选框。取消选中它,所有非吸烟者将会显示出来:
文本小部件
假设你希望用户在区域中输入而不是从下拉控制中选择。很简单,只需将字符串传递给 interact()
函数,如下所示:
def f(region):
display(df.query(f'region.str.contains("{region}")'))
interact(f, region = "")
现在你将看到一个文本框,你可以在其中输入区域:
有一个问题。观察到当你输入时,数据框会自动更新。这个功能被称为连续更新。对于大型数据框,这将是一个问题,因为每次你更改文本框的内容时,它都会重新查询数据框,这可能会耗费时间。
你可以通过显式指定 Text
小部件(控制)然后将 continuous_update
参数设置为 False
来禁用连续更新:
from ipywidgets import widgets
def f(region):
display(df.query(f'region.str.contains("{region}")'))
interact(f, region = widgets.Text('', continuous_update = False));
数据框现在会在你输入查询并按下 Return/Enter 键后更新:
单选按钮小部件
单选按钮是另一种让用户从预定的值列表中进行选择的方式。对于我们的数据集,你可以使用它来让用户在吸烟者和非吸烟者之间进行选择:
def f(smoker):
display(df.query(f'smoker == "{smoker}"'))
interact(f, smoker = widgets.RadioButtons(options=["yes", "no"], value="no"));
对于单选按钮,你需要手动指定
widgets.RadioButtons()
类。
上面的代码片段显示了一组单选按钮,用于选择吸烟者和非吸烟者:
使用 Interactive 函数
到目前为止,一切顺利。你已经能够创建和使用各个小部件来过滤数据框中的不同字段。但是如果你想将它们全部组合成一个大过滤器怎么办?这时 interactive()
函数就派上用场了。
interactive()
函数类似于 interact()
函数,但它允许你传入多个值以创建多个小部件。以下 f()
函数接收六个参数,这些参数通过 interactive()
函数传入:
from ipywidgets import interactive
import ipywidgets as widgets
report_output = widgets.Output()
display(report_output)
def f(age, bmi, children, region, sex, smoker):
filtered = df.query(f'age >= {age} and bmi >= {bmi} and smoker == "{smoker}" and region.str.contains("{region}") and sex=="{sex}" and children=={children}')
with report_output:
report_output.clear_output()
display(filtered)
regions = [(i.capitalize(),i) for i in df['region'].unique()]
regions.append(('All Regions','@*'))
interactive(f,
age = (df['age'].min(), df['age'].max()),
bmi = (df['bmi'].min(), df['bmi'].max()),
children = (df['children'].min(), df['children'].max()),
region = regions,
sex = widgets.RadioButtons(options=["female", "male"], value="female"),
smoker = widgets.RadioButtons(options=["yes", "no"], value="no"),
)
在运行代码片段的地方,你将看到以下内容:
当你与小部件互动时,数据框会自动更新。如果你有一个大型数据框,你可能会发现每次控制中的值发生变化时更新数据框是比较麻烦的(你的屏幕会因为这些小变化而刷新),因此,最好避免在用户希望查看更新的数据框之前刷新数据框。为此,将第二个参数 ({‘manual’: True}
) 插入到 interactive()
函数中:
interactive(f,
{'manual': True},
age = (df['age'].min(), df['age'].max()),
bmi = (df['bmi'].min(), df['bmi'].max()),
children = (df['children'].min(), df['children'].max()),
region = regions,
sex = widgets.RadioButtons(options=["female", "male"], value="female"),
smoker = widgets.RadioButtons(options=["yes", "no"], value="no"),
)
现在将出现一个标有运行互动的按钮。你可以在小部件中更改值,当你准备好查看更改时,点击运行互动按钮,数据框将会更新。
如果你喜欢阅读我的文章并且这些文章对你的职业/学习有帮助,请考虑注册成为 Medium 会员。每月$5,它可以让你无限访问 Medium 上的所有文章(包括我的)。如果你通过以下链接注册,我将获得少量佣金(对你没有额外费用)。你的支持意味着我将能够投入更多时间撰写类似的文章。
## 通过我的推荐链接加入 Medium - Wei-Meng Lee
阅读 Wei-Meng Lee 的每一个故事(以及 Medium 上的其他数千名作者的故事)。你的会员费直接支持…
总结
本文简要介绍了使用interact()
和interactive()
函数生成小部件并绑定到函数的过程。以下是根据你提供的数据类型创建的小部件类型的快速总结:
-
IntSlider或FloatSlider — 当你传入一个数字值或一对数字值的元组时
-
下拉菜单 — 当你传入一个值的列表(或元组的列表)时
-
文本 — 当你传入一个字符串值时
-
复选框 — 当你传入一个布尔值(
True
或False
)时
还有其他类型的小部件需要你在interact()
和interactive()
函数中手动指定,例如单选按钮及其他很多。在我的下一篇文章中,我将详细讲述在这篇文章中没有机会讨论的其他小部件。在此之前,祝你玩得开心!
管理一个联邦数据产品生态系统
原文:
towardsdatascience.com/managing-a-federated-data-product-ecosystem-3c6bff94c728
随着数据网格的成熟,企业在管理不断增长的联邦数据产品生态系统时遇到了困难。如何管理这个快速发展的生态系统?
·发布于 Towards Data Science ·阅读时间 9 分钟·2023 年 1 月 11 日
--
企业数据管理未兑现的承诺
我们都知道数据的体量、种类和复杂性正在指数增长。然而,我们当前的方式——集中式数据管理——正在失败。企业被提供了更大控制的幻想,但却看到缓慢、缺乏灵活性和官僚化的过程,这些过程阻碍了创新。
因此,企业在不同的数据管理方法中进行实验也就不足为奇了。数据网格就是这些“实验”之一。早期迹象表明,情况在向好的方向发展。
但数据网格仍在发展和成熟中。数据联邦——数据网格所基于的——在其路径上仍然存在一些障碍。
本文描述了数据网格如何拥抱数据的联邦——以及数据管理的联邦——以显著提高速度、促进敏捷性并实现本地自治。但成功的障碍以及如何克服这些障碍也将被讨论。在这方面,将讨论几个主题,包括:
-
数据网格作为联邦数据和联邦数据管理的关键推动者
-
扩展数据网格的潜在挑战
-
在数据网格中联邦数据的新原则
-
使用数据网格扩展数据联邦的新原则
数据网格的承诺
数据网格是一种相对较新的方法,提供了一组新的数据管理原则。简单来说,这些原则将数据视为具有明确所有权和责任的一级产品,支持自助服务平台和联邦治理。关于这方面的资料已经有很多这里、这里和这里讨论过,所以我不会专门深入探讨数据网格。
我建议还有一些更为根本的关于数据网格的内容值得讨论。简单来说,数据网格的承诺在于它实现了数据和数据管理的联邦化,促进了地方自治,从而推动了在今天快速变化市场中所需的速度、灵活性和创新。
联邦数据的演变概念
但我们从头开始:什么是“联邦数据”和“联邦数据管理”?是什么让它变得更好?
让我们从基础开始:数据是现代企业的“乐高积木”。就像一个单独的乐高积木,单个数据元素的价值有限。但当它们结合在一起时,积木可以组装成更大的组件,称为“数据产品”。
图 1,数据网格——企业数据的乐高积木
数据产品结合了数据网格的原则,包括明确的边界、授权的所有者、自助服务功能,从而实现了企业内部数据产品的“联邦化”。数据产品分布在整个企业中,没有中央流程或团队将它们绑定在一起。
大规模联邦数据的观察
今天,许多企业在充满活力但规模较小的生态系统中运行多个数据产品。但随着这些生态系统的发展,我们发现数据产品变成了:
-
难以找到,因为没有一个“注册表”作为数据产品的可搜索目录;而且一旦找到,它们的文档很少或不一致,使得数据产品难以理解,尤其是当它们的使用超出原始创建者组时。
-
难以访问和获取,因为获取数据访问权限的方法简单或一致的方式非常少;有一种名称解析服务将数据产品标识转换为端点,就像互联网的 DNS 一样;而且一旦获得访问权限,数据产品很难使用,因为访问各种数据产品的一致机制非常少。
-
难以操作、观察和安全,因为每个数据产品有不同的安全需求,甚至有更为多样化的实现。观察性工具不一致,安全需求的多样化导致复杂性过高,操作性推迟到生产问题需要解决时才会考虑。
-
难以信任,因为数据的来源、其转换过程以及数据供应链中不可避免的错误会使人怀疑数据产品的质量和可靠性;对于需要深入了解数据的受监管行业而言,数据产品不幸的是难以管理,因为数据产品提供的统计数据和指标不一致,导致手动处理增加。
图 2,数据难以找到、消费、信任和共享
这些问题是所谓的“规模问题”的症状。成功——在这种情况下,即组织中涌现出的大量数据产品——显然有一些负面影响。但是,我们如何驯服联邦的混乱演变,克服这些规模问题呢?
根据我在发展大型数据生态系统方面的经验,我认为出现了一套新的原则,这将使联邦数据管理能够以实际和高效的方式扩展。这些原则分为两个类别:一组适用于数据产品及其负责人,另一组则促进快速增长。
联邦数据产品的实用原则
联邦数据产品依赖于许多因素:愿景的清晰性、实际权衡的考量以及对实施卓越的持续关注。但增长你的联邦数据产品生态系统的关键成功因素是实践性地实施和制度化一个单一的概念:数据产品负责人至高无上。
那么,实际上,这意味着什么?我想这意味着字面上的意思:数据产品负责人对其数据产品的所有元素拥有最终决策权——以及否决权。是的,所有决定。并且,他们需要在良好的企业行为的正常范围内工作——他们必须遵守高级管理政策、监管限制,在某些情况下,还要承担盈亏责任。但他们有权决定如何实施这些政策,如何适应监管限制,如何达到他们的收入(或成本)目标。他们决定!
所以,毫无疑问,在数据产品负责人被赋权并拥有真正的决策权的地方,你会发现一个成功的数据产品生态系统以及不断增长的数据产品集合。
图 3,数据产品负责人原则
所以,为了实现“数据产品负责人至高无上”,我提供以下新的数据产品负责人原则:
-
数据产品负责人“拥有”他们的技术决策;数据产品负责人可以使用他们认为最有效的任何技术——即使这些技术与当前的企业标准相悖——来构建他们的数据产品。例如,数据产品负责人的目标是优先考虑灵活性、速度和数据产品负责人的自主权,这些优先于传统的成本关注和企业标准采纳。现在,要明确的是,他们显然应优先考虑现有企业标准产品,但他们并不依赖于这些标准。是的,他们可能需要承担引入新技术所需的额外开销。但他们自己做决定。
-
数据产品负责人“管理”他们的数据供应链:数据产品负责人保证他们的数据供应链的完整性。他们掌握数据摄取的所有杠杆:数据产品团队,而非中央管道团队,负责他们的数据摄取管道的规格;数据产品负责人还具备设计和构建摄取管道的技能。从实际角度来看,数据产品负责人推动管理实践、工具和仪器的投资,以主动识别、诊断数据供应链中的质量、稳定性或可用性问题。
-
数据产品负责人“认证”他们的数据需求链:数据产品负责人证明——或认证——数据的安全性、可信度、质量以及对服务水平的遵守。数据产品负责人制定消费合同(显然是与消费者合作)。他们还决定这些合同如何实施,以及如何满足服务水平期望。他们还主动衡量并公开报告质量指标,以确保数据期望得到满足。
-
数据产品负责人“销售”他们的数据产品:数据产品,像企业提供给消费者的其他产品一样,拥有生命周期。虽然生命周期类似,但在企业中的重点几乎总是截然不同。虽然传统产品的负责人(即向消费者提供并由消费者支付的产品)预计要达到收入和成本目标,但内部产品几乎从不如此。同样,数据产品负责人需要像传统产品负责人一样行动——他们需要积极推进,他们需要销售,他们需要获得尽可能多的组织支持和动力,以便——至少——成为自给自足、被认可并获得资助。
促进联合数据产品成长的实用原则
尽管拥有了授权的负责人,但事情仍然可能偏离轨道。发生问题时,会发现理解上的差距以及限制数据产品生态系统成长的不必要约束。
因此,让我提供一些对我来说效果良好的原则。对一些人来说,这些原则可能显得相悖,对另一些人来说可能不切实际,对其他人来说可能与企业指令不一致。但它们有效!
这些原则优先考虑灵活性和速度,而不是成本控制。它们选择测试和学习而非追求完美。它们提供创新和加速市场时间,而不是不必要的一致性。
图 4,促进联邦数据产品增长的原则
所以,这些就是新的原则,以及它们如何促进联邦数据产品的增长。
-
“让百花齐放”:我们必须鼓励数据产品的增长和多样性,允许各种想法和方法繁荣,而不是被压制或限制。为了让“百花齐放”变得更容易,企业应该使创建安全、可靠、可观察和可操作的数据产品变得简单。企业应该强烈倡导一种“测试和学习”的方法,容忍实验。
-
“杂草控制是次要优先事项”。继续我们的类比……在一片花海中,难免会有杂草。修剪杂草比为花朵提供食物和光照(是的,这个类比有些牵强)要不那么重要。关键是培养最有前景和最有价值的数据产品,同时允许自然选择过程的发生——并让学习哪些有效、哪些无效的过程得以进行。是的,构造不良的数据产品不会被使用,会失去资金,并最终应该被淘汰。但更重要的是,有价值的数据产品将取而代之,并希望能够蓬勃发展。
-
让数据产品易于查找、理解、使用和信任:企业必须建立一个一致的数据产品“注册表”,其中展示必要的数据词汇表、知识图谱、治理信息和反馈。然而,企业还必须确保数据生产者能够轻松创建、认证和管理注册表中的数据产品。为了使数据值得信赖,数据产品的消费者(和生产者)必须能够向数据产品所有者提供反馈。用现代的说法,他们应该能够“点赞”、“投票”或“收藏”数据产品。这个“众包”模式在软件(GitHub “stars”)和社交媒体(Facebook “likes”)中效果极佳,为数据产品所有者提供了宏观的数据产品质量和可信度视图,同时为数据产品所有者提供了宝贵的信息。不言而喻,“企业”的角色是提供工具和设施,使反馈提供变得简单。
-
简化和优化数据产品治理:企业应定义每个数据产品所有者必须提供的绝对最小属性、指标和服务水平。但企业还必须提供支持工具和设施,以便数据产品所有者能够轻松建立和发布这些信息。
结论
本文描述了数据网格如何拥抱数据联邦——以及数据管理——以显著提高速度,促进敏捷性,并实现地方自主。我们展示了数据网格作为数据联邦和数据管理的关键推动因素,并指出了在扩展数据网格时的潜在挑战。但我们也提出了在数据网格中联邦数据的新原则,同时提供了与数据网格一起扩展数据联邦的新原则。
正因如此,我希望不仅能够助力于您的企业数据网格之旅,更重要的是,加速其成长!
除非另有说明,本文档中的所有图片均由 Eric Broda(本文作者)创建。所有图片中使用的图标均为库存 PowerPoint 图标和/或不受版权保护。
本文中表达的观点仅代表我个人,不一定反映我的客户的观点。
使用 TOML 配置轻松管理深度学习模型
你可能永远不需要那些长 CLI 参数来运行你的 train.py
·
关注 发表在 Towards Data Science ·4 分钟阅读·2023 年 6 月 15 日
--
照片由Scott Graham提供,来源于Unsplash
管理深度学习模型可能很困难,因为需要为所有模块配置大量参数和设置。训练模块可能需要诸如batch_size
或num_epochs
的参数,或用于学习率调度器的参数。同样,数据预处理模块可能需要train_test_split
或用于图像增强的参数。
管理或引入这些参数到管道中的一种天真的方法是将它们作为 CLI 参数传递给脚本。命令行参数可能很难输入,并且在单个文件中管理所有参数可能不切实际。TOML 文件提供了一种更清洁的配置管理方式,脚本可以以 Python dict
的形式加载配置的必要部分,而无需读取/解析命令行参数的样板代码。
在这篇博客中,我们将深入探讨 TOML 在配置文件中的使用,以及如何在训练/部署脚本中高效地使用它们。
什么是 TOML 文件?
TOML,代表Tom’s Obvious Minimal Language,是一种专门为配置文件设计的文件格式。TOML 文件的概念与YAML/YML 文件非常相似,这些文件能够以树状层次结构存储键值对。TOML 相对于 YAML 的一个优点是其可读性,当存在多个嵌套层级时,这一点尤为重要。
图 1. 同一模型配置以 TOML(左)和 YAML(右)编写。TOML 允许我们在相同的缩进级别下编写键值对,无论层次结构如何。
就个人而言,除了增强的可读性,我找不到任何实用的理由来优先选择 TOML 而非 YAML。使用 YAML 完全没问题,这里有一个Python 包用于解析 YAML。
为什么我们需要在 TOML 中配置?
使用 TOML 存储模型/数据/部署配置有两个优势:
在单个文件中管理所有配置:通过 TOML 文件,我们可以为不同模块创建多个设置组。例如,在图 1 中,与模型训练过程相关的设置嵌套在[train]
属性下,类似地,模型部署所需的port
和host
存储在deploy
下。我们不需要在train.py
或deploy.py
之间跳转以更改参数,而是可以从单个 TOML 配置文件中全局化所有设置。
如果我们在虚拟机上训练模型,而没有代码编辑器或 IDE 可用来编辑文件,这可能会非常有帮助。单个配置文件可以通过大多数虚拟机上可用的
vim
或nano
轻松编辑。
我们如何从 TOML 中读取配置?
要从 TOML 文件中读取配置,可以使用两个 Python 包,[toml](https://github.com/uiri/toml)
和 [munch](https://github.com/uiri/toml)
。toml
将帮助我们读取 TOML 文件并返回文件内容作为 Python dict
。munch
将转换dict
的内容以实现属性样式的元素访问。例如,不需要写config[ "training" ][ "num_epochs" ]
,我们可以直接写config.training.num_epochs
,这增强了可读性。
请考虑以下文件结构,
- config.py
- train.py
- project_config.toml
project_config.toml
包含我们的 ML 项目的配置,如,
[data]
vocab_size = 5589
seq_length = 10
test_split = 0.3
data_path = "dataset/"
data_tensors_path = "data_tensors/"
[model]
embedding_dim = 256
num_blocks = 5
num_heads_in_block = 3
[train]
num_epochs = 10
batch_size = 32
learning_rate = 0.001
checkpoint_path = "auto"
在 config.py
中,我们创建了一个函数,该函数返回使用 toml
和 munch
的 munchified 版本的配置,
$> pip install toml munch
import toml
import munch
def load_global_config( filepath : str = "project_config.toml" ):
return munch.munchify( toml.load( filepath ) )
def save_global_config( new_config , filepath : str = "project_config.toml" ):
with open( filepath , "w" ) as file:
toml.dump( new_config , file )
现在,在我们项目的任何文件中,比如 train.py
或 predict.py
,我们都可以加载这个配置,
from config import load_global_config
config = load_global_config()
batch_size = config.train.batch_size
lr = config.train.learning_rate
if config.train.checkpoint_path == "auto":
# Make a directory with name as current timestamp
pass
print( toml.load( filepath ) )
的输出是,
{'data': {'data_path': 'dataset/',
'data_tensors_path': 'data_tensors/',
'seq_length': 10,
'test_split': 0.3,
'vocab_size': 5589},
'model': {'embedding_dim': 256, 'num_blocks': 5, 'num_heads_in_block': 3},
'train': {'batch_size': 32,
'checkpoint_path': 'auto',
'learning_rate': 0.001,
'num_epochs': 10}}
如果您正在使用像 W&B Tracking 或 MLFlow 这样的 MLOps 工具,将配置保持为 dict
可能会很有帮助,因为我们可以直接将其作为参数传递。
结束
希望您考虑在您的下一个 ML 项目中使用 TOML 配置!这是管理全局或局部于您的训练/部署或推理脚本的设置的一种清晰方式。
而不是编写长长的命令行参数,脚本可以直接从 TOML 文件中加载配置。如果我们希望用不同的超参数训练两个版本的模型,我们只需在 config.py
中更改 TOML 文件。我最近的项目中开始使用 TOML 文件,实验变得更快了。MLOps 工具也可以管理模型的版本以及它们的配置,但以上讨论的方法的简单性是独特的,并且需要最小的现有项目更改。
希望您喜欢阅读。祝您有个愉快的一天!
在单台机器上管理多个 CUDA 版本:全面指南
如何在开发环境中处理不同的 CUDA 版本
· 发表在Towards Data Science ·阅读时间 6 分钟·2023 年 10 月 27 日
--
照片由Nikola Majksner提供,发布在Unsplash
在我以前的 AI 顾问角色中,我负责利用虚拟环境作为管理和隔离 Python 环境的工具。鉴于该项目依赖 GPU 加速,我遇到了已安装的 CUDA 版本与项目所需版本不匹配的情况。为了解决这个问题,我不得不安装必要的 CUDA 版本并配置我的环境以使用它,而不影响系统的 CUDA 设置。据我所知,针对这种特定需求的全面教程非常稀缺。因此,本教程为那些寻求了解如何在项目中安全管理多个 CUDA 工具包版本的人提供了宝贵的资源。
目录:
· 1. 介绍
· 2. CUDA 可用版本
· 3. 下载和提取二进制文件
· 4. 安装 CUDA 工具包
· 5. 项目设置
· 6. 结论
1. 介绍
在系统上安装多个版本的 CUDA 工具包可能会产生多种影响和后果,其中一些可能会影响你的系统:
-
这可能会导致系统
PATH
和环境变量中的冲突。如果管理不当,这些冲突可能会影响默认使用的 CUDA 版本。 -
这可能需要特定的 GPU 驱动程序版本以实现最佳性能和兼容性。安装新版本可能需要更新你的 GPU 驱动程序。
-
一些库和软件可能依赖于特定的 CUDA 版本。安装新版本可能会破坏与这些依赖项的兼容性。
-
依赖于 CUDA 的应用程序可能需要进行调整,以便与新版本兼容。不兼容可能会导致错误或意外行为。
-
不正确地管理多个 CUDA 版本可能导致系统不稳定或 GPU 加速应用程序出现错误。
因此,为了安全地管理项目的多个 CUDA Toolkit 版本,请按照以下步骤操作:
-
检查系统当前的 CUDA 版本。
-
下载并解压所需版本的二进制文件。
-
执行安装程序,仅安装工具包。
在本教程中,我将提供一个详细的逐步示例,说明如何完成此操作。此外,我将指导您在成功安装二进制文件后设置虚拟环境。
2. 可用的 CUDA 版本
通过运行命令 nvidia-smi
来查看系统当前使用的 CUDA 版本:
如您所见,CUDA 版本为 12.1。
现在让我们显示我机器上可用的 CUDA 版本:
$ ls /usr/local/ | grep cuda
cuda
cuda-11.7
cuda-12
cuda-12.1
我的机器上有三个不同的版本。
3. 下载并解压二进制文件
假设我将要处理的项目需要 CUDA Toolkit 版本 11.8。要获取它,我们首先访问 NVIDIA CUDA Toolkit Archive 网站:这里。我们找到项目所需的特定版本的 CUDA Toolkit。重要的是确保我们选择与操作系统兼容的版本。在我的例子中,我选择了目标平台:
我的目标平台:Linux — x86_64 — Ubuntu — 22.04
选择与您的操作系统对应的 CUDA Toolkit 的runfile (local)
版本。此文件通常带有 .run
扩展名。选择 runfile (local)
时,网站会提供安装说明。在我的情况下,提供的说明如下:
wget https://developer.download.nvidia.com/compute/cuda/11.8.0/local_installers/cuda_11.8.0_520.61.05_linux.run
sudo sh cuda_11.8.0_520.61.05_linux.run
然而,必须牢记,我们的目标不是安装此版本,因为已经有一个更新版本。因此,我们只需按照第一条说明下载文件:
wget https://developer.download.nvidia.com/compute/cuda/11.8.0/local_installers/cuda_11.8.0_520.61.05_linux.run
可以通过将 MD5 校验和与下载文件的校验和进行比较来验证下载。请访问 此链接 进行验证。
“本地安装程序是自包含的。它是一个大型文件,只需从互联网下载一次即可,在多个系统上安装。本地安装程序是推荐的安装类型,适用于带宽较低的互联网连接,或者在无法使用网络安装程序的情况下(例如由于防火墙限制)。” [1]
此时,打开终端,进入您传输 CUDA runfile 的目录,并使 CUDA runfile 可执行:
chmod +x cuda_11.8.0_520.61.05_linux.run
4. 安装 CUDA 工具包
现在,我们使用--silent
和--toolkit
标志运行 CUDA runfile,以进行 CUDA Toolkit 的静默安装:
sudo ./cuda_11.8.0_520.61.05_linux.run --silent --toolkit
其中:
-
--silent
: 进行无进一步用户输入和最小命令行输出的安装。 -
--toolkit
: 仅安装 CUDA Toolkit 并保留当前驱动程序。
如果系统要求你接受协议,请接受以继续安装。
到此为止,CUDA 工具包二进制文件已被提取。我们可以通过再次运行以下命令来确认:
$ ls /usr/local/ | grep cuda
cuda
cuda-11.7
cuda-11.8
cuda-12
cuda-12.1
如你所见,cuda-11.8
现在在我的机器上可用,系统当前版本保持不变(你可以通过运行nvidia-smi
确认)。
这些步骤允许你安装 CUDA 版本的二进制文件。在下一节中,我将展示如何设置项目以使用所需的 CUDA 版本。
5. 项目设置
在处理多个项目时,建议使用虚拟环境。我们从创建一个虚拟环境开始。在我的情况下,需要python3.8
。要创建虚拟环境,我们可以使用以下命令。我在venv
文件夹中创建了一个名为my_venv
的环境,这是我放置虚拟环境的文件夹:
python3.8 -m venv venv/my_env
source venv/my_env/bin/activate
让我们看看当前使用的 CUDA 版本:
$ nvcc --version
nvcc: NVIDIA (R) Cuda compiler driver
Copyright (c) 2005-2021 NVIDIA Corporation
Built on Thu_Nov_18_09:45:30_PST_2021
Cuda compilation tools, release 11.5, V11.5.119
Build cuda_11.5.r11.5/compiler.30672275_0
如你所见,创建的环境没有使用所需的 CUDA 版本,因此我们需要通过更新 activate 文件并添加以下行来手动设置:
export PATH=/usr/local/cuda-11.8/bin:$PATH
export LD_LIBRARY_PATH=/usr/local/cuda-11.8/lib64:$LD_LIBRARY_PATH
你可以使用你喜欢的编辑器更新 activate 文件,或者你可以简单地运行以下命令将文本追加到文件末尾:
echo "export PATH=/usr/local/cuda-11.8/bin:$PATH" >> venv/my_env/bin/activate
echo "LD_LIBRARY_PATH=/usr/local/cuda-11.8/lib64:$LD_LIBRARY_PATH" >> venv/my_env/bin/activate
最后,我们需要重新激活环境并再次运行nvcc
命令:
$ source venv/nerfstudio/bin/activate
$ nvcc --version
nvcc: NVIDIA (R) Cuda compiler driver
Copyright (c) 2005-2022 NVIDIA Corporation
Built on Wed_Sep_21_10:33:58_PDT_2022
Cuda compilation tools, release 11.8, V11.8.89
Build cuda_11.8.r11.8/compiler.31833905_0
就这些!现在项目已经配置为运行所需的 CUDA 版本,并且没有冲突!
6. 结论
按照本教程中的步骤,你可以成功地在系统上维护多个 CUDA 版本,而不会遇到安装之间的冲突。这种灵活性使每个项目能够使用其所需的确切 CUDA 版本,通过配置环境变量来实现。
感谢阅读。希望你喜欢本教程。如果你喜欢我的教程,请通过关注和订阅来支持我。这样,你将收到有关我新文章的通知。如果你有任何问题或建议,请随时在下面留言。
参考文献
图片来源
本文中所有未在标题中提及来源的图像和图形均由作者提供。
管理大数据应用程序的云存储成本
降低使用基于云的存储成本的技巧
·
关注 发表在 Towards Data Science · 11 分钟阅读 · 2023 年 6 月 26 日
--
图片由 JOSHUA COLEMAN 提供,来源于 Unsplash
随着对不断增长的数据量的依赖,现代公司比以往任何时候都更加依赖高容量和高度可扩展的数据存储解决方案。对于许多公司来说,这种解决方案通常表现为云存储服务,如Amazon S3、Google Cloud Storage和Azure Blob Storage,这些服务都提供了丰富的 API 和功能(例如,多层存储),支持各种数据存储设计。当然,云存储服务也有相关的成本。这些成本通常包括多个组成部分,包括你使用的存储空间的总体大小,以及如将数据传入、传出或在云存储中传输等活动。例如,Amazon S3 的价格(截至本文撰写时)包括六个成本组成部分,每个部分都需要考虑。可以看出,管理云存储成本可能变得复杂,因此开发了指定的计算器(例如,这里)来协助处理这一问题。
在一篇近期文章中,我们扩展了设计数据和数据使用的重要性,以降低与数据存储相关的成本。我们在那里关注的是使用数据压缩作为减少数据总体大小的一种方法。在这篇文章中,我们关注的是一个有时被忽视的云存储成本组成部分 —— 对你的云存储桶和数据对象进行的 API 请求成本。我们将通过示例展示为什么这个组成部分常常被低估,以及如果管理不当,它如何可能成为你大数据应用成本的重要组成部分。然后我们将讨论几种简单的方法来控制这一成本。
免责声明
尽管我们的演示将使用 Amazon S3,但本文的内容同样适用于任何其他云存储服务。请不要将我们选择 Amazon S3 或我们提到的任何其他工具、服务或库解读为对其使用的支持。最适合你的选项将取决于你自己项目的具体细节。此外,请记住,关于如何存储和使用数据的任何设计选择都会有其利弊,这些利弊应根据你自己项目的细节权衡。
本文将包括在 Amazon EC2 c5.4xlarge 实例(具有 16 个 vCPUs 和 “高达 10 Gbps” 的网络带宽)上运行的一系列实验。我们将分享这些实验的输出,作为你可能看到的比较结果的示例。请注意,输出结果可能会因实验运行的环境而有很大不同。请不要将这里呈现的结果作为你自己设计决策的依据。我们强烈建议你在决定自己项目的最佳方案之前,运行这些以及其他额外的实验。
一个简单的思考实验
假设你有一个数据转换应用程序,处理来自 S3 的 1 MB 数据样本,并生成 1 MB 数据输出,然后将其上传到 S3。假设你的任务是通过在合适的 Amazon EC2 实例 上运行你的应用程序来转换 10 亿个数据样本(与 S3 存储桶位于同一地区,以避免数据传输成本)。现在假设 Amazon S3 收费 每 1000 次 GET 操作收费 $0.0004,每 1000 次 PUT 操作收费 $0.005(截至本文撰写时)。乍一看,这些成本似乎如此低,以至于与数据转换相关的其他成本相比,几乎可以忽略不计。然而,简单的计算显示,单是我们的 Amazon S3 API 调用就会产生$5,400的费用!! 这可能是你项目中最主要的成本因素,甚至比计算实例的成本还要高。我们将在本文最后回到这个思考实验。
将数据批量归为大文件
降低 API 调用成本的显而易见方法是将样本归为大文件,并在样本批次上进行转换。将我们的批量大小记作N,这种策略可能将我们的成本降低N倍(假设没有使用多部分文件传输——见下文)。这种技术不仅在 PUT 和 GET 调用上节省了费用,还能在所有与对象文件数量相关的 Amazon S3 成本组成部分上节省开支,而不是数据的整体大小(例如,生命周期转换请求)。
将样本组合在一起有一些缺点。例如,当你单独存储样本时,你可以随时访问其中的任何一个。当样本组合在一起时,这就变得更加具有挑战性。(有关将样本批处理成大文件的优缺点,请参见这篇文章)。如果你选择将样本组合在一起,那么最大的疑问是如何选择大小 N。较大的 N 可能会降低存储成本,但可能会引入延迟,增加计算时间,并由此增加计算成本。找到最佳数量可能需要一些实验,考虑到这些以及其他额外的因素。
但我们不要自欺欺人。进行这种更改并不容易。你的数据可能有很多消费者(包括人类和人工智能),每个人都有自己特定的需求和限制。将样本存储在单独的文件中可以更容易地让每个人满意。找到一个能够满足所有人需求的批处理策略将会很困难。
可能的折衷方案:批量 PUT,单独 GET
你可以考虑的一种折衷方案是上传包含分组样本的大文件,同时允许访问单个样本。实现这一点的一种方法是维护一个索引文件,其中包含每个样本的位置(其所在的文件、起始偏移量和结束偏移量),并向每个消费者提供一个轻量的 API 层,以便他们可以自由下载单个样本。该 API 将使用索引文件和一个允许从对象文件中提取特定范围的 S3 API(例如,Boto3 的 get_object 函数)来实现。虽然这种解决方案不会节省 GET 调用的费用(因为我们仍然要提取相同数量的单个样本),但由于我们上传的是较少的较大文件,因此更昂贵的 PUT 调用将会减少。请注意,这种解决方案对我们与 S3 交互所使用的库有一些限制,因为它依赖于一个允许从大型文件对象中提取部分块的 API。在之前的帖子中(例如,这里),我们讨论了与 S3 接口的不同方式,其中许多方式不支持此功能。
下面的代码块演示了如何实现一个简单的 PyTorch 数据集(使用 PyTorch 版本 1.13),该数据集使用 Boto3 get_object API 从包含分组样本的大文件中提取单个 1 MB 样本。我们将这种方式迭代数据的速度与迭代存储在单独文件中的样本的速度进行比较。
import os, boto3, time, numpy as np
import torch
from torch.utils.data import Dataset
from statistics import mean, variance
KB = 1024
MB = KB * KB
GB = KB ** 3
sample_size = MB
num_samples = 100000
# modify to vary the size of the files
samples_per_file = 2000 # for 2GB files
num_files = num_samples//samples_per_file
bucket = '<s3 bucket>'
single_sample_path = '<path in s3>'
large_file_path = '<path in s3>'
class SingleSampleDataset(Dataset):
def __init__(self):
super().__init__()
self.bucket = bucket
self.path = single_sample_path
self.client = boto3.client("s3")
def __len__(self):
return num_samples
def get_bytes(self, key):
response = self.client.get_object(
Bucket=self.bucket,
Key=key
)
return response['Body'].read()
def __getitem__(self, index: int):
key = f'{self.path}/{index}.image'
image = np.frombuffer(self.get_bytes(key),np.uint8)
return {"image": image}
class LargeFileDataset(Dataset):
def __init__(self):
super().__init__()
self.bucket = bucket
self.path = large_file_path
self.client = boto3.client("s3")
def __len__(self):
return num_samples
def get_bytes(self, file_index, sample_index):
response = self.client.get_object(
Bucket=self.bucket,
Key=f'{self.path}/{file_index}.bin',
Range=f'bytes={sample_index*MB}-{(sample_index+1)*MB-1}'
)
return response['Body'].read()
def __getitem__(self, index: int):
file_index = index // num_files
sample_index = index % samples_per_file
image = np.frombuffer(self.get_bytes(file_index, sample_index),
np.uint8)
return {"image": image}
# toggle between single sample files and large files
use_grouped_samples = True
if use_grouped_samples:
dataset = LargeFileDataset()
else:
dataset = SingleSampleDataset()
# set the number of parallel workers according to the number of vCPUs
dl = torch.utils.data.DataLoader(dataset, shuffle=True,
batch_size=4, num_workers=16)
stats_lst = []
t0 = time.perf_counter()
for batch_idx, batch in enumerate(dl, start=1):
if batch_idx % 100 == 0:
t = time.perf_counter() - t0
stats_lst.append(t)
t0 = time.perf_counter()
mean_calc = mean(stats_lst)
var_calc = variance(stats_lst)
print(f'mean {mean_calc} variance {var_calc}')
下表总结了不同样本分组大小 N 的数据遍历速度。
不同分组策略对数据遍历时间的影响(作者)
注意,尽管这些结果强烈暗示将样本分组到大文件中对单独提取它们的性能影响相对较小,但我们发现比较结果会根据样本大小、文件大小、文件偏移量的值、对同一文件的并发读取次数等有所变化。虽然我们无法了解 Amazon S3 服务的内部工作原理,但考虑到内存大小、内存对齐和限制等因素对性能的影响也不足为奇。找到适合你数据的最佳配置可能需要一些实验。
一个可能干扰我们在这里描述的节省成本的分组策略的重要因素是多部分下载和上传的使用,我们将在下一节中讨论。
使用可以控制多部分数据传输的工具
许多云存储服务提供商支持对象文件的多部分上传和下载选项。在多部分数据传输中,大于某个阈值的文件被划分为多个部分并同时传输。如果你想加速大文件的数据传输,这是一项关键功能。AWS 建议对大于 100 MB 的文件使用多部分上传。在以下简单示例中,我们比较了将 2 GB 文件的多部分 阈值 和 分块大小 设置为不同值时的下载时间:
import boto3, time
KB = 1024
MB = KB * KB
GB = KB ** 3
s3 = boto3.client('s3')
bucket = '<bucket name>'
key = '<key of 2 GB file>'
local_path = '/tmp/2GB.bin'
num_trials = 10
for size in [8*MB, 100*MB, 500*MB, 2*GB]:
print(f'multi-part size: {size}')
stats = []
for i in range(num_trials):
config = boto3.s3.transfer.TransferConfig(multipart_threshold=size,
multipart_chunksize=size)
t0 = time.time()
s3.download_file(bucket, key, local_path, Config=config)
stats.append(time.time()-t0)
print(f'multi-part size {size} mean {mean(stats)}')
此实验的结果总结在下面的表格中:
多部分分块大小对下载时间的影响(作者)
请注意,相对比较将大大依赖于测试环境,特别是实例与 S3 存储桶之间的通信速度和带宽。我们的实验是在与存储桶位于同一区域的实例上运行的。然而,随着距离的增加,使用多部分下载的影响也会增加。
关于我们讨论的话题,重要的是要注意多部分数据传输的成本影响。具体来说,当你使用多部分数据传输时,你会为每个文件部分的 API 操作收费。因此,使用多部分上传/下载将限制将数据样本批量处理成大文件的成本节省潜力。
许多 API默认使用多部分下载。如果你的主要关注点是减少与 S3 的交互延迟,这种做法是很好的。但是,如果你的关心是限制成本,那么这种默认行为对你来说并不有利。例如,Boto3 是一个流行的 Python API,用于从 S3 上传和下载文件。如果未指定,boto3 的 S3 API 如 upload_file 和 download_file 将使用默认的 TransferConfig,该配置将对任何大于 8 MB 的文件应用 8 MB 的块大小的多部分上传/下载。如果你负责控制组织中的云成本,你可能会惊讶地发现这些 API 被广泛使用其默认设置。在许多情况下,你可能会发现这些设置是不必要的,增加多部分阈值和块大小值,或完全禁用多部分数据传输,对应用程序的性能影响很小。
示例 — 多部分文件传输大小对速度和成本的影响
在下面的代码块中,我们创建一个简单的多进程转换函数,并测量多部分块大小对其性能和成本的影响:
import os, boto3, time, math
from multiprocessing import Pool
from statistics import mean, variance
KB = 1024
MB = KB * KB
sample_size = MB
num_files = 64
samples_per_file = 500
file_size = sample_size*samples_per_file
num_processes = 16
bucket = '<s3 bucket>'
large_file_path = '<path in s3>'
local_path = '/tmp'
num_trials = 5
cost_per_get = 4e-7
cost_per_put = 5e-6
for multipart_chunksize in [1*MB, 8*MB, 100*MB, 200*MB, 500*MB]:
def empty_transform(file_index):
s3 = boto3.client('s3')
config = boto3.s3.transfer.TransferConfig(
multipart_threshold=multipart_chunksize,
multipart_chunksize=multipart_chunksize
)
s3.download_file(bucket,
f'{large_file_path}/{file_index}.bin',
f'{local_path}/{file_index}.bin',
Config=config)
s3.upload_file(f'{local_path}/{file_index}.bin',
bucket,
f'{large_file_path}/{file_index}.out.bin',
Config=config)
stats = []
for i in range(num_trials):
with Pool(processes=num_processes) as pool:
t0 = time.perf_counter()
pool.map(empty_transform, range(num_files))
transform_time = time.perf_counter() - t0
stats.append(transform_time)
num_chunks = math.ceil(file_size/multipart_chunksize)
num_operations = num_files*num_chunks
transform_cost = num_operations * (cost_per_get + cost_per_put)
if num_chunks > 1:
# if multi-part is used add cost of
# CreateMultipartUpload and CompleteMultipartUpload API calls
transform_cost += 2 * num_files * cost_per_put
print(f'chunk size {multipart_chunksize}')
print(f'transform time {mean(stats)} variance {variance(stats)}
print(f'cost of API calls {transform_cost}')
在这个例子中,我们将文件大小固定为 500 MB,并对下载和上传应用了相同的多部分设置。一个更完整的分析将会变化数据文件的大小和多部分设置。
在下表中,我们总结了实验的结果。
多部分块大小对数据转换速度和成本的影响(作者)
结果表明,至多达到 500 MB 的多部分块大小(我们文件的大小),数据转换时间的影响是最小的。另一方面,与使用 Boto3 的默认块大小(8MB)相比,云存储 API 成本的潜在节省是显著的,最多可达 98.4%。这个例子不仅展示了将样本分组的成本效益,而且还暗示了通过适当配置多部分数据传输设置获得额外节省的机会。
结论
让我们将上一个示例的结果应用到我们在本文开头介绍的思维实验中。我们展示了如果数据样本存储在单独的文件中,对 1 亿个数据样本应用简单转换的费用将是$5,400。如果我们将样本分组为 200 万个文件,每个文件有 500 个样本,并且在不使用多部分数据传输的情况下(如上述示例的最后一次试验),API 调用的成本将减少到$10.8!!同时,假设测试环境相同,我们预期的整体运行时间的影响(基于我们的实验)将相对较低。我认为这是一个相当不错的交易。你觉得呢?
摘要
在开发基于云的大数据应用时,了解我们活动的所有成本细节至关重要。在这篇文章中,我们专注于 Amazon S3 定价策略中的“请求与数据检索”组件。我们演示了这个组件如何成为大数据应用总体成本的重要组成部分。我们讨论了可能影响这一成本的两个因素:数据样本的分组方式和多部分数据传输的使用方式。
自然,仅优化一个成本组件可能会以某种方式增加其他组件,从而提高整体成本。数据存储的适当设计需要考虑所有潜在成本因素并且将大大依赖于你的具体数据需求和使用模式。
一如既往,请随时提出评论和修正。
机器学习系统的技术债务管理
探索用于可持续减轻快速交付成本的实践(设计模式、版本控制和监控系统)——包括实现代码
·
关注 发表在 Towards Data Science ·10 分钟阅读·2023 年 9 月 26 日
--
随着机器学习(ML)社区的进步,开发 ML 项目的资源非常丰富。例如,我们可以依赖通用的 Python 包 scikit-learn,该包建立在 NumPy、SciPy 和 matplotlib 之上,用于数据预处理和基本预测任务。或者,我们可以利用来自 Hugging Face 的预训练模型的开源集合来分析各种类型的数据集。这些工具使得当前的数据科学家能够迅速而轻松地处理标准 ML 任务,同时取得适度良好的模型性能。
然而,ML 工具的丰富性往往使业务利益相关者甚至从业者低估了构建企业级 ML 系统所需的努力。尤其是在面对紧迫的项目截止日期时,团队可能会加快将系统部署到生产环境的速度,而没有充分考虑技术因素。因此,ML 系统往往不能以技术上可持续和可维护的方式满足业务需求。
随着系统的发展和部署,技术债务会不断累积——隐含的成本拖延不解决,修复的成本会越来越高。
在机器学习(ML)系统中存在多种技术债务来源。以下是一些例子。
#1 硬性代码设计以应对不可预见的需求
为了验证 ML 是否能解决当前的企业挑战,许多 ML 项目开始时会进行 概念验证(PoC)。我们最初创建了一个 Jupyter Notebook 或 Google Colab 环境来探索数据,然后开发了几个临时函数,给利益相关者制造了项目即将完成的假象。直接从 PoC 构建的系统最终可能大部分由 胶水代码 构成——这是一种连接特定不兼容组件的支持代码,但本身并不具备数据分析的功能。它们可能像意大利面一样,难以维护且容易出错。
Jakob Owens 拍摄于 Unsplash
业务利益相关者经常提出不同的要求或希望扩大项目,例如尝试新的数据源或新的算法。因此,我们经常发现自己频繁地重新访问涵盖当前预处理管道和模型开发过程的代码库。灵活性差的代码设计可能导致对新变化的反应困难,甚至需要重写大部分代码以进行小的调整。
#2 ML 系统配置中的混乱
软件工程编程通过定义计算机执行的规则来自动化任务,确保相同输入的精确输出。软件工程师还关注每个角落情况的正确性。另一方面,ML 系统通过收集和输入特征数据到模型中来自动化任务,以实现期望的目标结果。这个实验过程接受不确定性和可变性。随着 ML 系统的成熟,它们通常包含多个版本的配置选项,如具有不同特征组合的数据集和特定算法的学习设置。
照片来自 Ricardo Viana 在 Unsplash
ML 系统中的输入特征本质上是相互关联的。考虑一种情况,如果特征 A 在生产环境中不再可用,你需要重新评估剩余特征的权重。然而,2 个月后,特征 A 又变得可用。如果你没有系统地保存或甚至错误地修改了原始配置,恢复性能下降将需要额外的计算资源和时间精力。
#3 适应不断变化的外部世界的能力有限
ML 系统通常依赖于外部世界,各种隐藏因素不断演变,但未得到适当考虑和监控。
导致模型性能潜在下降的外部因素(图像来源:作者)
-
来自上游生产者的不稳定数据输出:我们的 ML 系统的输入信号可能来自一个随着时间更新的其他机器学习模型。此外,系统可能依赖于来自物联网(IoT)设备的信号、网页抓取数据或音频到文本转换器的输出数据。如果上游生产者的这些工具的维护没有得到适当声明或实施了有缺陷的修补,它可能会意外地降低 ML 系统的性能。
-
输入数据的漂移:以零售行业的需求预测为例。输入数据可以周期性地(例如购买行为的季节性循环)、渐进性地(如供应商商品的通货膨胀成本)和突然地(新竞争者的进入)表现出新的分布。
在接下来的部分中,我们将深入探讨构建机器学习系统的一些最佳实践,并提供示例 Python 代码以演示其实现。
想象一下你希望为城市建立一个强大的交通系统,准备应对交通高峰,因此你收集了过去两年的传感器交通数据。你的目标是预测未来六个月的交通模式(即车辆数量)。
-
训练数据集:ID、日期时间以及车辆数量
-
测试数据集:ID 和日期时间
图片来源:Joey Kyber 在 Unsplash
#1 对于机器学习代码库使用设计模式
为了使代码设计在未来需求中更灵活和可重用,我们可以利用设计模式。这些模式作为解决各种情况中常见问题的模板,使我们能够解耦代码库的不同部分。因此,这有助于提高对代码库的理解,并建立一种共同的语言,以快速沟通解决方案。
机器学习项目中的两个主要组成部分是数据和算法,这些部分可以从设计模式中受益。
- 工厂模式
这种创建模式为在运行时生成对象提供了一层抽象。在机器学习系统中,我们可以通过创建一个数据加载类(例如CSVDataLoader
)来实现这种模式,该类处理训练/测试数据的加载、保存和返回,并保持一致的数据类型。然后我们可以声明DataProcessor
接口,而不指定具体实现。
# CSV Data loader class
class CSVDataLoader:
def __init__(self, file_path):
self.file_path = file_path
def get_data(self):
return pd.read_csv(self.file_path)
def save_data(self, df):
df.to_csv(f'data/transformed_{self.file_path}', index=False)
# Interface
class DataProcessor:
def __init__(self, train_data_loader, test_data_loader):
self.train_data_loader = train_data_loader
self.test_data_loader = test_data_loader
def run(self):
# Load training and test data using data loaders
train_df = self.train_data_loader.get_data()
test_df = self.test_data_loader.get_data()
# Create a data processor instance
process = DataProcessor(
train_data_loader=CSVDataLoader(file_path='train.csv'),
test_data_loader=CSVDataLoader(file_path='test.csv')
)
# Run the data processing pipeline
process.run()
这种方法允许你扩展代码,而不必重新实现DataProcessor
。例如,如果你想从 JSON 文件加载数据集,你可以简单地创建一个新的类JSONDataLoader
作为数据加载器的实例进行声明。
- 策略模式
由于没有适用于所有机器学习问题的通用算法,我们经常在原型设计或项目增强过程中切换和试验不同的算法。我们可以应用策略模式,通过创建一个新的类DataTransformer
进行特征工程,以及另一个类LGBMModel
来封装使用 LightGBM 模型进行拟合和预测的策略。
class DataTransformer:
def transform_data(self, train_df, test_df):
for idx, df in enumerate([train_df, test_df]):
df[‘DateTime’] = pd.to_datetime(df['DateTime'])
# Build ‘Time’ column
df['Time'] = [date.hour * 3600 + date.minute * 60 + date.second for date in df['DateTime']]
# Convert DateTime to Unix timestamp
unixtime = [time.mktime(date.timetuple()) for date in df['DateTime']]
df['DateTime'] = unixtime
# Perform one-hot encoding on the DataFrame
df = pd.get_dummies(df)
if idx == 0:
# Split training DataFrame into features (X_train) and target (y_train)
X_train = df.drop(['Vehicles'], axis=1)
y_train = df[['Vehicles']]
elif idx == 1:
# Store test DataFrame
X_test = df
return X_train, y_train, X_test
class LGBMModel:
def __init__(self, num_leaves, n_estimators):
self.model = lgb.LGBMRegressor(
num_leaves=num_leaves,
n_estimators=n_estimators
)
def fit(self, X, y):
self.model.fit(X, y)
self.model.booster_.save_model('model/lgbm_model.txt')
return self
def predict(self, X):
predictions = self.model.predict(X)
return predictions
接口DataProcessor
的实现和声明如下。这是一个端到端的过程,包括分别使用train_data_loader
和test_data_loader
加载训练和测试数据,使用data_transformer
转换数据,并使用model
将模型拟合到转换后的数据。因此,我们可以预测测试数据集中每条记录中的车辆数量。
# Interface
class DataProcessor:
def __init__(self, train_data_loader, test_data_loader, data_transformer, model):
self.train_data_loader = train_data_loader
self.test_data_loader = test_data_loader
self.data_transformer = data_transformer
self.model = model
def run(self):
# Load train and test data using data loaders
train_df = self.train_data_loader.get_data()
test_df = self.test_data_loader.get_data()
# Transform the data using the data transformer
X_train, y_train, X_test = self.data_transformer.transform_data(train_df, test_df)
# Fit the model and make prediction
self.model.fit(X_train, y_train)
test_df['Vehicles'] = self.model.predict(X_test)
# Save the transformed training data and test data
self.train_data_loader.save_data(pd.concat([X_train, y_train], axis=1))
self.test_data_loader.save_data(test_df)
# Create a data processor instance
process = DataProcessor(
train_data_loader=CSVDataLoader(file_path='train.csv'),
test_data_loader=CSVDataLoader(file_path='test.csv'),
data_transformer=DataTransformer(),
model=LGBMModel(num_leaves=16, n_estimators=80)
)
# Run the data processing pipeline
process.run()
你可以轻松地添加新的代码块来实现其他数据转换想法或算法。类似于工厂模式,这些更改不需要你修改接口DataProcessor
。这种设计使得即使有很长的算法列表,也更容易维护代码。ML 系统的行为可以根据选择的策略动态变化。
当然,上述代码实现仅是开发的初步模板。例如,我们可以进一步通过涵盖数据验证、实施超参数调优机制以及评估模型来增强代码。
#2 ML 系统的版本控制
在复杂的模型开发和管理过程中,我们需要适当的版本控制。这使我们能够维护自己或团队成员所做的修改历史,并跟踪本地环境中相对于 ML 系统组件的数据、训练模型和超参数的版本。因此,我们可以解决一些常见问题,包括:
-
是什么更改导致了模型的失败?
-
哪些修改导致了模型性能的提升?
-
最近发布的是哪个版本的模型?
在这里,我们展示了如何利用DVC中的版本控制功能,它在Git库中效果最佳,用于跟踪我们的原始交通数据、转换后的交通数据和 LGBM 模型。
# Initialise a Git and DVC project in the current working directory
git init
dvc init
# Capture the current state of transformed data in folder 'data' and latest LGBM model in folder 'model'
dvc add data model
# Commit the current state of 1st version
git add data.dvc model.dvc .gitignore
git commit -m “First LGBM model, with Time feature”
git tag -a “v1.0” -m “model v1.0, Time feature”
让我们考虑一个场景,我们在第二个版本的数据处理过程中进行了以下更改:
- 在
DataTransformer
类中添加 Weekday 特性
df[‘Weekday’] = [datetime.weekday(date) for date in df.DateTime]
- 在
DataProcessor
接口中设置 LGBM 模型的新配置参数
model=LGBMModel(num_leaves=20, n_estimators=90)
使用以下命令,我们可以在 DVC 中跟踪数据和模型的第二个版本,并用 Git 提交指向它们的 .dvc 文件。
git add data.dvc model.dvc
git commit -m “Second LGBM model, with Time and Weekday features”
git tag -a “v2.0” -m “model v2.0, Time and Weakday features”
尽管工作区当前定位于数据和模型的第二个版本,但我们可以在必要时轻松地切换回并恢复到第一个快照。
git checkout v1.0
dvc checkout
上述命令涵盖了基本操作。我们可以进一步利用该工具进行项目组织和协作。使用案例的例子包括理解数据最初是如何构建的,并比较实验中的模型指标。
#3 持续测试和监控 ML 系统
一旦增强的模型能够生成预测,必须在将其投入生产之前执行 合理性检查。这可以通过将一组随机的在线数据集拟合到最新模型中进行离线检查,并从不同角度审视结果来实现。
-
确保正确的访问权限:模型结果可以存储在目标路径中(例如,将它们写入 Hive 表)。
-
消除 语义错误:可视化模型拟合的变换特征的分布,以识别任何异常行为。
-
评估模型性能:使用最新模型重新评分,并使用适当的性能指标(例如,对于不平衡类问题,F1-score 是比准确度更好的衡量指标)比较结果。
即使最新版本的机器学习系统发布后,持续监控仍然是必要的,以应对不断变化的外部环境。
-
监控数据漂移和模型漂移:通过模型性能指标、统计测试和自适应窗口技术来检测漂移条件。
-
跟踪上游生产者:了解上游流程中的变化,并定期监控它们,以确保它们符合服务水平目标。
总结
我们已经探索了几种有效的实践,这些实践可以用来解决在开发和部署机器学习系统时出现的技术债务。
-
使用设计模式,创建一个模块化且灵活的数据处理管道,以适应不可预见的需求。
-
利用版本控制,跟踪和管理机器学习工件,如数据和模型,确保工作流程更少混乱。
-
测试和监控机器学习系统,以便及时顺利地处理动态外部世界中的变化。
离开之前
如果你喜欢这篇阅读,我邀请你关注我的 Medium 页面 和 Linkedin 页面。这样,你可以及时获得有关数据科学侧项目、机器学习运营(MLOps)演示以及项目管理方法的精彩内容。
我们的模型在不断变化的世界中如何受到影响?一个集中于漂移示例的分析,并实现基于 Python 的…
[towardsdatascience.com ## 利用超越 A/B 测试的方法优化策略
对经典 A/B 测试的深入解释:Epsilon-greedy、Thompson Sampling、Contextual…
[towardsdatascience.com
使用 Rclone 管理你的云数据存储
如何优化多个对象存储系统之间的数据传输
·
关注 发表于 Towards Data Science ·7 分钟阅读·2023 年 11 月 22 日
--
图片由 Tom Podmore 提供,来源于 Unsplash
随着公司越来越依赖基于云的存储解决方案,拥有合适的工具和技术以有效管理其大数据变得至关重要。在之前的文章中(例如,这里和这里),我们探讨了几种从云存储中检索数据的方法,并展示了它们在不同任务中的有效性。我们发现,最优的工具可能会根据具体任务(例如文件格式、数据文件的大小、数据访问模式)以及我们希望优化的指标(例如延迟、速度或成本)而有所不同。在这篇文章中,我们探讨了另一个流行的云存储管理工具——有时被称为“云存储的瑞士军刀”——rclone命令行工具。rclone 支持超过70 种存储服务提供商,具备类似于供应商特定存储管理应用程序(如 AWS CLI(用于 Amazon S3)和gsutil(用于 Google Storage))的功能。但它是否足够出色以构成一个可行的替代方案?在什么情况下 rclone 会是首选工具?在接下来的章节中,我们将展示 rclone 的使用,评估其性能,并突出其在特定用例中的价值——在不同对象存储系统之间转移数据。
免责声明
本文绝不是为了取代官方的rclone 文档。也不意图为使用 rclone 或我们提到的其他工具提供认可。您在进行云数据管理时的最佳选择将大大依赖于项目的详细信息,并应根据详细的、特定用例的测试来做出。请确保在阅读时重新评估我们所做的陈述,并对照您手头的最新工具。
从云存储中检索数据与 Rclone
以下命令行使用rclone sync来同步云对象存储路径与本地目录的内容。这个例子演示了Amazon S3存储服务的使用,但同样可以使用其他云存储服务。
rclone sync -P \
--transfers 4 \
--multi-thread-streams 4 \
S3store:my-bucket/my_files ./my_files
rclone 命令有数十个 标志 用于编程其行为。* -P * 标志输出数据传输的进度,包括传输速率和总体时间。在上述命令中,我们包含了两个(众多)可能影响 rclone 运行时性能的控制:transfers 标志确定要并发下载的最大文件数,而 multi-thread-streams 确定用于传输单个文件的最大线程数。这里我们将两者都保留在默认值(4)。
rclone 的功能依赖于对 rclone 配置文件 的适当定义。下面我们演示了上面命令行中使用的远程 S3store 对象存储位置的定义。
[S3store]
type = s3
provider = AWS
access_key_id = <id>
secret_access_key = <key>
region = us-east-1
现在我们已经看到 rclone 的实际操作,接下来的问题是它是否相较于其他云存储管理工具(例如流行的 AWS CLI)提供了任何额外的价值。在接下来的两个部分中,我们将评估 rclone 相比于其一些替代品在我们之前帖子中详细探讨的两个场景中的性能:1) 下载一个 2 GB 文件和 2) 下载数百个 1 MB 文件。
用例 1:下载大文件
以下命令行使用 AWS CLI 从 Amazon S3 下载一个 2 GB 文件。这只是我们在 之前的帖子 中评估的众多方法之一。我们使用 linux 的 time 命令来测量性能。
time aws s3 cp s3://my-bucket/2GB.bin .
报告的下载时间约为 26 秒(即 ~79 MB/s)。请注意,该值是在我们自己本地 PC 上计算的,可能会因运行时环境的不同而有很大差异。等效的 rclone copy 命令如下:
rclone sync -P S3store:my-bucket/2GB.bin .
在我们的设置中,我们发现 rclone 的下载时间比标准的 AWS CLI 慢两倍多。通过适当调整 rclone 控制标志,这一性能有可能得到显著改善。
用例 2:下载大量小文件
在这个用例中,我们评估了下载 800 个相对较小的 1 MB 文件的运行时性能。在 之前的博客文章 中,我们讨论了在将数据样本流式传输到深度学习训练工作负载的背景下的这个用例,并展示了 s5cmd beast 模式的优越性能。在 beast 模式下,我们创建一个包含对象文件操作列表的文件,s5cmd 使用 多个并行工作线程(默认值为 256)来执行这些操作。下面展示了 s5cmd beast 模式选项:
time s5cmd --run cmds.txt
cmds.txt 文件包含 800 行,格式如下:
cp s3://my-bucket/small_files/<i>.jpg <local_path>/<i>.jpg
s5cmd 命令平均耗时 9.3 秒(十次试验的平均值)。
Rclone 支持类似于 s5cmd 的 beast mode 的功能,通过 files-from 命令行选项实现。下面我们使用 transfers 值设置为 256 在我们的 800 个文件上运行 rclone copy,以匹配 s5cmd 的默认 并发设置。
rclone -P --transfers 256 --files-from files.txt S3store:my-bucket /my-local
files.txt 文件包含 800 行,格式如下:
small_files/<i>.jpg
我们的 800 个文件的 rclone copy 平均耗时 8.5 秒,比 s5cmd 略少(十次试验的平均值)。
我们承认目前展示的结果可能不足以说服您选择 rclone 而非现有工具。在下一节中,我们将描述一个用例,突出 rclone 的潜在优势。
对象存储系统之间的数据传输
现如今,开发团队维护多个对象存储并不罕见。这可能是为了防范存储故障的风险,或是决定使用多个云服务提供商的数据处理服务。例如,您可能会在 AWS 中使用存储在 Amazon S3 中的数据进行 AI 模型训练,同时在 Microsoft Azure 中运行数据分析,分析的数据存储在 Azure Storage 中。此外,您可能还希望在 FlashBlade、Cloudian 或 VAST 等本地存储基础设施中保持数据的备份。这些情况需要在安全、可靠和及时的方式下,在多个对象存储之间传输和同步数据的能力。
一些云服务提供商提供专门用于此类目的的服务。然而,这些服务并不总是满足您项目的具体需求,或者可能无法提供您所期望的控制级别。例如,Google Storage Transfer 在指定存储文件夹内的 所有数据 迁移方面表现出色,但(截至本文撰写时)不支持从其中传输特定子集的文件。
我们可以考虑的另一个选项是将现有的数据管理方案应用于这个目的。问题在于,像 AWS CLI 和 s5cmd 这样的工具(截至目前)不支持为源存储系统和目标存储系统指定不同的访问设置和安全凭证。因此,在存储位置之间迁移数据需要将数据转移到一个中间(临时)位置。下面的命令中,我们结合使用了 s5cmd 和 AWS CLI,通过系统内存和 Linux 管道将文件从 Amazon S3 复制到 Google Storage:
s5cmd cat s3://my-bucket/file \
| aws s3 cp --endpoint-url https://storage.googleapis.com
--profile gcp - s3://gs-bucket/file
尽管这是一种合法的、尽管笨拙的传输单个文件的方式,但在实际操作中,我们可能需要能够传输数百万个文件。为了支持这一点,我们需要添加一个额外的层来启动和管理多个并行的工作进程/处理器。事情可能会迅速变得棘手。
使用 Rclone 进行数据传输
与 AWS CLI 和 s5cmd 等工具不同,rclone 使我们能够为源和目标指定不同的访问设置。在以下的 rclone 配置文件中,我们添加了 Google Cloud Storage (GCS) 访问设置。(在这里,我们将 GCS 视为 S3 提供商。请查看这里了解其他配置选项。)
[S3store]
type = s3
provider = AWS
access_key_id = <id>
secret_access_key = <key>
[GSstore]
type = s3
provider = GCS
access_key_id = <id>
secret_access_key = <key>
endpoint = https://storage.googleapis.com
在存储系统之间传输单个文件的格式与复制到本地目录相同:
rclone copy -P S3store:my-bucket/file GSstore:gs-bucket/file
然而,rclone 的真正力量来自于将上述的files-from选项与这个功能结合。我们无需为数据迁移的并行化设计自定义解决方案,只需用一个命令就能传输一长串文件:
rclone copy -P --transfers 256 --files-from files.txt \
S3store:my-bucket/file GSstore:gs-bucket/file
实际上,我们可以通过将对象文件列表解析为较小的列表(例如,每个列表包含 10,000 个文件)并在单独的计算资源上运行每个列表,从而进一步加速数据迁移。虽然这种解决方案的具体影响因项目而异,但它可以显著提高开发的速度和效率。
总结
在这篇文章中,我们探讨了使用 rclone 进行基于云的存储管理,并展示了它在维护和同步多个存储系统数据方面的应用。数据传输确实有许多替代解决方案,但 rclone 基于的方法的便利性和优雅性无可置疑。
这只是我们在最大化基于云的存储解决方案效率方面撰写的众多文章之一。请务必查看一些我们的其他文章以了解这一重要话题。
在编写 Apache Beam 管道时使用示例进行 Map、Filter 和 CombinePerKey 转换
让我们用一些真实数据进行练习
·发表于 Towards Data Science ·8 分钟阅读·2023 年 7 月 12 日
--
Apache Beam 作为统一的编程模型,在高效和可移植的大数据处理管道中越来越受欢迎。它可以处理批量数据和流数据。这也是名字的由来。Beam 是 Batch 和 Stream 两个词的组合:
B(来自Batch)+ eam(来自 stream)= Beam
便携性也是一个很棒的特性。你只需专注于运行管道,它可以在任何地方运行,例如 Spark、Flink、Apex 或 Cloud Dataflow。你不需要更改逻辑或语法。
在这篇文章中,我们将专注于学习如何通过示例编写一些 ETL 管道。我们将尝试使用一个好的数据集进行一些转换操作,希望你会发现这些转换操作在工作中也非常有用。
请随意下载这个公共数据集并跟随练习:
这个练习使用了 Google Colab notebook。因此,安装非常简单。只需使用这一行代码:
!pip install --quiet apache_beam
安装完成后,我为这个练习创建了一个名为‘data’的目录:
mkdir -p data
让我们深入探讨今天的话题——转换操作。首先,我们将处理一个最简单的管道,即读取 CSV 文件并将其写入文本文件。
这不像 Padas 的 read_csv() 方法那么简单。它需要一个 coder() 操作。首先,在这里定义了一个 CustomCoder() 类,该类首先将对象编码为字节字符串,然后将字节解码为其对应的对象,并最终指定这个 coder 是否保证对值进行确定性编码。请查看 文档这里。
如果这是你的第一个管道,请注意管道的语法。在 CustomCoder() 类之后是最简单的管道。我们首先将空管道初始化为‘p1’。然后我们编写了‘sales’管道,其中首先从我们之前创建的数据文件夹中读取 CSV 文件。在 Apache beam 中,管道中的每个转换操作都以 | 符号开始。读取 CSV 文件中的数据后,我们只是将其写入文本文件。最后,通过 run() 方法我们运行了管道。这是 Apache beam 中标准和常用的管道语法。
import apache_beam as beam
from apache_beam.coders.coders import Coder
class CustomCoder(Coder):
"""A custom coder used for reading and writing strings as UTF-8."""
def encode(self, value):
return value.encode("utf-8", "replace")
def decode(self, value):
return value.decode("utf-8", "ignore")
def is_deterministic(self):
return True
p1 = beam.Pipeline()
sales = (p1
|beam.io.ReadFromText('data/sales_data_sample.csv', coder=CustomCoder(), skip_header_lines=1)
|beam.io.WriteToText('data/output'))
p1.run()
如果你现在检查你的‘data’文件夹,你会看到一个‘output-00000-of-00001’文件。从这个文件中打印前 5 行以检查数据:
!head -n 5 data/output-00000-of-00001
输出:
10107,30,95.7,2,2871,2/24/2003 0:00,Shipped,1,2,2003,Motorcycles,95,S10_1678,Land of Toys Inc.,2125557818,897 Long Airport Avenue,,NYC,NY,10022,USA,NA,Yu,Kwai,Small
10121,34,81.35,5,2765.9,5/7/2003 0:00,Shipped,2,5,2003,Motorcycles,95,S10_1678,Reims Collectables,26.47.1555,59 rue de l'Abbaye,,Reims,,51100,France,EMEA,Henriot,Paul,Small
10134,41,94.74,2,3884.34,7/1/2003 0:00,Shipped,3,7,2003,Motorcycles,95,S10_1678,Lyon Souveniers,+33 1 46 62 7555,27 rue du Colonel Pierre Avia,,Paris,,75508,France,EMEA,Da Cunha,Daniel,Medium
10145,45,83.26,6,3746.7,8/25/2003 0:00,Shipped,3,8,2003,Motorcycles,95,S10_1678,Toys4GrownUps.com,6265557265,78934 Hillside Dr.,,Pasadena,CA,90003,USA,NA,Young,Julie,Medium
10159,49,100,14,5205.27,10/10/2003 0:00,Shipped,4,10,2003,Motorcycles,95,S10_1678,Corporate Gift Ideas Co.,6505551386,7734 Strong St.,,San Francisco,CA,,USA,NA,Brown,Julie,Medium
Map
让我们来看一下如何在上述管道中使用 Map 转换。这是最常见的转换操作。你在 Map 中指定的转换将应用于 PCollection 中的每一个元素。
例如,我想添加一个 split 方法以从 PCollection 中的每个元素创建列表。在这里,我们将使用 lambda 进行 Map 转换。如果你不熟悉 lambda,请查看这里的 lambda 代码。lambda 后我们提到‘row’,任何其他变量名也可以。我们对‘row’应用的任何函数或方法将应用于 PCollection 中的每个元素。
p2 = beam.Pipeline()
sales = (p2
|beam.io.ReadFromText('data/sales_data_sample.csv', coder=CustomCoder(), skip_header_lines=1)
|beam.Map(lambda row: row.split(','))
|beam.io.WriteToText('data/output2'))
p2.run()
看,它是完全相同的语法。只是我在读取和写入操作之间多了一行代码。再次打印输出的前 5 行进行检查:
!head -n 5 data/output2-00000-of-00001
输出:
['10107', '30', '95.7', '2', '2871', '2/24/2003 0:00', 'Shipped', '1', '2', '2003', 'Motorcycles', '95', 'S10_1678', 'Land of Toys Inc.', '2125557818', '897 Long Airport Avenue', '', 'NYC', 'NY', '10022', 'USA', 'NA', 'Yu', 'Kwai', 'Small']
['10121', '34', '81.35', '5', '2765.9', '5/7/2003 0:00', 'Shipped', '2', '5', '2003', 'Motorcycles', '95', 'S10_1678', 'Reims Collectables', '26.47.1555', "59 rue de l'Abbaye", '', 'Reims', '', '51100', 'France', 'EMEA', 'Henriot', 'Paul', 'Small']
['10134', '41', '94.74', '2', '3884.34', '7/1/2003 0:00', 'Shipped', '3', '7', '2003', 'Motorcycles', '95', 'S10_1678', 'Lyon Souveniers', '+33 1 46 62 7555', '27 rue du Colonel Pierre Avia', '', 'Paris', '', '75508', 'France', 'EMEA', 'Da Cunha', 'Daniel', 'Medium']
['10145', '45', '83.26', '6', '3746.7', '8/25/2003 0:00', 'Shipped', '3', '8', '2003', 'Motorcycles', '95', 'S10_1678', 'Toys4GrownUps.com', '6265557265', '78934 Hillside Dr.', '', 'Pasadena', 'CA', '90003', 'USA', 'NA', 'Young', 'Julie', 'Medium']
['10159', '49', '100', '14', '5205.27', '10/10/2003 0:00', 'Shipped', '4', '10', '2003', 'Motorcycles', '95', 'S10_1678', 'Corporate Gift Ideas Co.', '6505551386', '7734 Strong St.', '', 'San Francisco', 'CA', '', 'USA', 'NA', 'Brown', 'Julie', 'Medium']
看,每个元素都变成了一个列表。
Filter
接下来,我将把 Filter 转换也添加到上述代码块中。这里的 lambda 也可以用于过滤。我们将过滤掉所有数据,只保留 Produc line 中的‘经典汽车’数据。数据集的第 11 列是产品线。正如你所知,Python 是零索引的。所以,列号的计数也从零开始。
p3 = beam.Pipeline()
sales = (p3
|beam.io.ReadFromText('data/sales_data_sample.csv', coder=CustomCoder(), skip_header_lines=1)
|beam.Map(lambda row: row.split(','))
|beam.Filter(lambda row:row[10] == "Classic Cars")
|beam.io.WriteToText('data/output3'))
p3.run()
如之前所示,打印前 5 行进行检查:
!head -n 5 data/output3-00000-of-00001
输出:
['10103', '26', '100', '11', '5404.62', '1/29/2003 0:00', 'Shipped', '1', '1', '2003', 'Classic Cars', '214', 'S10_1949', 'Baane Mini Imports', '07-98 9555', 'Erling Skakkes gate 78', '', 'Stavern', '', '4110', 'Norway', 'EMEA', 'Bergulfsen', 'Jonas', 'Medium']
['10112', '29', '100', '1', '7209.11', '3/24/2003 0:00', 'Shipped', '1', '3', '2003', 'Classic Cars', '214', 'S10_1949', '"Volvo Model Replicas', ' Co"', '0921-12 3555', 'Berguvsvgen 8', '', 'Lule', '', 'S-958 22', 'Sweden', 'EMEA', 'Berglund', 'Christina', 'Large']
['10126', '38', '100', '11', '7329.06', '5/28/2003 0:00', 'Shipped', '2', '5', '2003', 'Classic Cars', '214', 'S10_1949', '"Corrida Auto Replicas', ' Ltd"', '(91) 555 22 82', '"C/ Araquil', ' 67"', '', 'Madrid', '', '28023', 'Spain', 'EMEA', 'Sommer', 'Martn', 'Large']
['10140', '37', '100', '11', '7374.1', '7/24/2003 0:00', 'Shipped', '3', '7', '2003', 'Classic Cars', '214', 'S10_1949', 'Technics Stores Inc.', '6505556809', '9408 Furth Circle', '', 'Burlingame', 'CA', '94217', 'USA', 'NA', 'Hirano', 'Juri', 'Large']
['10150', '45', '100', '8', '10993.5', '9/19/2003 0:00', 'Shipped', '3', '9', '2003', 'Classic Cars', '214', 'S10_1949', '"Dragon Souveniers', ' Ltd."', '+65 221 7555', '"Bronz Sok.', ' Bronz Apt. 3/6 Tesvikiye"', '', 'Singapore', '', '79903', 'Singapore', 'Japan', 'Natividad', 'Eric', 'Large']
查看上面输出中每个列表的第 11 个元素。它是‘经典汽车’。
回答一些问题
每种类型的汽车订购了多少数量?
为了找出这一点,我们首先将创建元组,其中第一个元素或键将来自数据集的第 11 个元素,第二个元素即值将是数据集的第二个元素,即‘订购数量’。在下一步中,我们将使用 CombinePerKey() 方法。正如名字所示,它将为每个键结合具有聚合函数的值。
当你看到代码时会更清楚。这里是代码。
p3a = beam.Pipeline()
sales = (p3a
|beam.io.ReadFromText('data/sales_data_sample.csv', coder=CustomCoder(), skip_header_lines=1)
|beam.Map(lambda row: row.split(','))
#|beam.Filter(lambda row:row[10] == "Classic Cars")
|beam.Map(lambda row: (row[10], int(row[1])))
|beam.io.WriteToText('data/output3a'))
p3a.run()
!head -n 10 data/output3a-00000-of-00001
如你所见,我们在这里使用了两次 Map 函数。第一次是分割并像之前一样生成列表,然后从每行数据中提取第 10 列的产品线和第二列的数量。
这里是输出:
('Motorcycles', 30)
('Motorcycles', 34)
('Motorcycles', 41)
('Motorcycles', 45)
('Motorcycles', 49)
('Motorcycles', 36)
('Motorcycles', 29)
('Motorcycles', 48)
('Motorcycles', 22)
('Motorcycles', 41)
只是打印了输出的前 10 行。如你所见,这里每行数据的订单数量都列出了。回答上述问题的下一步也是最后一步是将每项的所有值结合起来。Apache Beam 中有 CombinePerKey 方法可以实现。顾名思义,它会为每个键使用聚合函数来结合值。在这种情况下,我们需要的是“总和”。
p4 = beam.Pipeline()
sales = (p4
|beam.io.ReadFromText('data/sales_data_sample.csv', coder=CustomCoder(), skip_header_lines=1)
|beam.Map(lambda row: row.split(','))
#|beam.Filter(lambda row:row[10] == "Classic Cars")
|beam.Map(lambda row: (row[10], int(row[1])))
|beam.CombinePerKey(sum)
|beam.io.WriteToText('data/output4'))
p4.run()
!head -n 10 data/output4-00000-of-00001
输出:
('Motorcycles', 11663)
('Classic Cars', 33992)
('Trucks and Buses', 10777)
('Vintage Cars', 21069)
('Planes', 10727)
('Ships', 8127)
('Trains', 2712)
所以,我们得到了每个产品的总订单数量。
哪些州的订单数量超过了 2000 个?
这是一个有趣的问题,我们需要每一个之前做过的变换加上另一个过滤变换。我们需要计算每个州的总订单数量,就像在前面的例子中计算每个产品的总订单数量一样。然后,将数量超过 2000 的订单进行过滤。
在所有之前的例子中,lambda 函数被用于 Map 和 Filter 变换。这里我们将看到如何定义一个函数并在 Map 或 Filter 函数中使用它。这里定义了一个函数 quantity_filter(),它返回值数量大于 2000 的项。
def quantity_filter(row):
name, count = row
if count > 2000:
return row
p7 = beam.Pipeline()
sales = (p7
|beam.io.ReadFromText('data/sales_data_sample.csv', coder=CustomCoder(), skip_header_lines=1)
|beam.Map(lambda row: row.split(','))
|beam.Map(lambda row: (row[17], int(row[1])))
|beam.CombinePerKey(sum)
|beam.Map(quantity_filter)
|beam.io.WriteToText('data/output7'))
p7.run()
!head -n 10 data/output7-00000-of-00001
输出:
('NYC', 5294)
None
None
None
('San Francisco', 2139)
None
('', 33574)
None
None
None
这是输出,其中如果数量不超过 2000,则返回‘None’。我不喜欢看到所有这些‘None’值。我将添加另一个过滤变换来过滤掉这些‘None’值。
p8 = beam.Pipeline()
sales = (p8
|beam.io.ReadFromText('data/sales_data_sample.csv', coder=CustomCoder(), skip_header_lines=1)
|beam.Map(lambda row: row.split(','))
|beam.Map(lambda row: (row[17], int(row[1])))
|beam.CombinePerKey(sum)
|beam.Map(quantity_filter)
|beam.Filter(lambda row: row != None)
|beam.io.WriteToText('data/output8'))
p8.run()
!head -n 10 data/output8-00000-of-00001
输出:
('NYC', 5294)
('San Francisco', 2139)
('', 33574)
('New Bedford', 2043)
('San Rafael', 6366)
所以,我们总共有 5 个返回的值,其中订单数量大于 2000。
结论
在本教程中,我想展示如何在 Apache Beam 中使用 Map、Filter 和 CombinePerKey 变换来编写 ETL 管道。希望它们足够清晰,能够在你的项目中使用。我将在下一篇文章中解释如何使用 ParDo。
随意关注我 Twitter 并点赞我的 Facebook 页面。
相关阅读
关于 Python 中多项式回归的详细教程、概述、实现和过拟合 | 作者:Rashida Nasrin Sucky | 2023 年 6 月 | Towards AI
迷你 VGG 网络图像识别的完整实现 | 作者:Rashida Nasrin Sucky | Towards Data Science
如何在 TensorFlow 中定义自定义层、激活函数和损失函数 | 作者:Rashida Nasrin Sucky | Towards Data Science
在 TensorFlow 中开发多输出模型的逐步教程 | 作者:拉希达·纳斯林·苏基 | 数据科学前沿
OpenCV Python 中的简单边缘检测方法 | 作者:拉希达·纳斯林·苏基 | 数据科学前沿
轨迹预测中的地图匹配
你要去哪里?你是否应该朝那个方向前进?
·
关注 发布于 Towards Data Science ·16 min read·2023 年 7 月 20 日
--
照片由Mateusz Wacławek拍摄,来源于Unsplash
本文提出了一种方法,利用从嘈杂的 GPS 传感器采样的历史旅行数据库来预测数字道路网络上的车辆轨迹。除了预测未来方向外,该方法还为任意位置序列分配一个概率。
这一理念的核心是使用数字地图,我们将所有采样位置投影到地图上,通过将它们聚合到单独的轨迹中并与地图匹配。这一匹配过程将连续的 GPS 空间离散化为预定位置和序列。将这些位置编码为唯一的地理空间标记后,我们可以更容易地预测序列,评估当前观察的概率,并估计未来的方向。这是本文的要旨。
问题
我在这里尝试解决哪些问题?如果你需要分析车辆路径数据,可能需要回答文章小标题中的问题。
你要去哪里?你应该那样走吗?
如何评估观察到的路径是否遵循经常行驶的方向的概率?这是一个重要的问题,因为通过回答它,你可以编程一个自动化系统来根据观察到的频率对行程进行分类。新轨迹的低分将引起关注并促使立即标记。
如何预测车辆接下来会进行哪些操作?它会继续直行,还是在下一个交叉口右转?你期望在接下来的十分钟或十英里内看到车辆在哪里?对这些问题的快速回答将帮助在线跟踪软件解决方案向配送规划者、在线路线优化器,甚至机会充电系统提供答案和见解。
解决方案
我在这里提出的解决方案使用了一个历史轨迹数据库,每个轨迹由特定车辆运动生成的时间序列位置组成。每个位置记录必须包含时间、位置信息、车辆标识符的参考以及轨迹标识符。一辆车有很多轨迹,每个轨迹有很多位置记录。我们输入数据的样本如图 1所示。
图 1 — 上表显示了来自扩展车辆能量数据集的一个轨迹的小样本。虽然每一行包含比展示的更多属性,但我们只需要隐含顺序和位置。注意,由于采样策略,有很多重复的位置。我们稍后将处理这个问题。(图片来源:作者)
我从扩展车辆能量数据集(EVED)[1] 文章中提取了上述数据。你可以通过参考我之前文章中的代码来构建相应的数据库。
本文解释了如何使用通过 Quadkeys 索引的已知速度向量来估计旅行时间。
towardsdatascience.com
我们的第一项工作是将这些轨迹匹配到支持的数字地图上。这个步骤的目的不仅是为了消除 GPS 数据采样误差,更重要的是,将获取的旅行数据强制转换为已知和固定的现有道路网络,其中每个节点和边都已知。每条记录的轨迹因此被转换为与现有数字地图节点相符的一系列数值标记。在这里,我们将使用开源数据和软件,地图数据来源于OpenStreetMap(由Geofabrik编译)、Valhalla地图匹配包和H3作为地理空间分词器。
边缘与节点匹配
地图匹配比乍看起来要复杂。为了说明这个概念的含义,我们来看一下下面的图 2。
图 2 — 上图展示了从 EVED 中获得的噪声轨迹(蓝色)。如你所见,它与最近的道路不匹配,需要与地图进行匹配。一旦我们将蓝线的顶点投影到地图上,就可以得到原始点到推断地图边缘的一系列投影,你可以看到结果轨迹为红色。然而,这条路径在某些地方仍然未能覆盖底层地图,特别是在图像的中心,红线在道路之间跳跃。我们的目标是重建地图上的旅行路径,如绿色线所示,遵循底层地图节点。(图片来源:作者使用 Folium 和 OpenStreetMap 图像)
上述图 2显示,我们可以从原始 GPS 序列中推导出两条轨迹。我们通过将原始 GPS 位置投影到最近(且最可能的)道路网络段来获得第一条轨迹。如你所见,结果折线有时只会沿着道路,因为地图使用图形节点来定义其基本形状。通过将原始位置投影到地图边缘,我们获得了属于地图的新点,但在通过直线连接到下一个点时,可能会偏离地图的几何形状。
通过将 GPS 轨迹投影到地图上的节点,我们得到了一条完美覆盖地图的路径,如图 2中的绿色线所示。虽然这条路径更好地表示了最初驱动的轨迹,但它不一定与原始位置一一对应。幸运的是,这对我们来说没有问题,因为我们将始终将任何轨迹与地图节点进行匹配,因此我们将始终获得一致的数据,只有一个例外。Valhalla 地图匹配代码始终将初始和最终轨迹点进行边缘投影,因此我们会系统性地丢弃它们,因为它们与地图节点不对应。
H3 分词
不幸的是,Valhalla 不报告唯一的道路网络节点标识符,因此我们必须将节点坐标转换为唯一的整数令牌,以便后续的序列频率计算。这就是 H3 介入的地方,它允许我们将节点坐标编码为一个独特的 64 位整数。我们选择 Valhalla 生成的多段线,去掉初始和最终点(这些点不是节点,而是边的投影),并将所有剩余的坐标映射到 level 15 H3 indices。
对偶图
使用上述过程,我们将每个历史轨迹转换为一系列 H3 令牌。下一步是将每个轨迹转换为令牌三元组序列。序列中的三个值表示预测图的两个连续边,我们希望知道这些的频率,因为它们将是预测和概率评估的核心数据。图 3 下面直观地描绘了这一过程。
图 3 — 左侧的地理空间令牌列表扩展为另一列表的三元组,表示隐含图的对偶视图。每个令牌是地理空间图上的一个节点,其序列表示边。转换后的列表将每个边视为对偶图中的一个节点,而中间的令牌是新的边,如右列所示。(图片来源:作者)
上述转换计算了道路图的对偶图,颠倒了原始节点和边的角色。
现在我们可以开始回答提出的问题。
你确定要走那条路吗?
要回答这个问题,我们需要知道车辆轨迹直到某个时刻。我们使用上述相同的过程进行映射匹配和令牌化轨迹,然后使用已知的历史频率计算每个轨迹三元组的频率。最终结果是所有个体频率的乘积。如果输入轨迹中有未知的三元组,它的频率将是零,作为最终路径概率。
三元组概率是特定序列 (A, B, C) 的计数与所有 *(A, B, ) 三元组计数的比率,如下图 图 4 所示。
图 4 — 三元组概率是其频率与所有具有相同两个初始令牌的三元组频率的比率。(图片来源:作者)
旅行概率只是各个旅行三元组的乘积,如下图图 5所示。
图 5 — 旅行概率是所有匹配三元组的简单乘积。(图片来源:作者)
你要去哪里?
我们使用相同的原则来回答这个问题,但仅从最后一个已知的三元组开始。我们可以使用这个三元组作为输入,通过列举所有以输入的最后两个令牌作为前两个令牌的三元组,预测最可能的 k 个后继者。下方的图 6展示了三元组序列生成和评估的过程。
图 6 — 在这个虚拟的案例中,最可能的下一个三元组是观察到的频率最高的三元组(B, C, D)。(图片来源:作者)
我们可以提取前 k 个后继三元组并重复该过程,以预测最可能的旅行。
实现
我们准备讨论实现细节,从地图匹配和一些相关概念开始。接下来,我们将学习如何从 Python 使用 Valhalla 工具集,提取匹配的路径并生成令牌序列。一旦我们将结果存储在数据库中,数据预处理步骤就完成了。
最后,我展示了一个使用 Streamlit 的简单用户界面,该界面计算任何手绘轨迹的概率,并将其投射到未来。
地图匹配
地图匹配 将从移动物体路径中采样的 GPS 坐标转换为现有的道路图。道路图是一个离散的模型,表示物理道路网络,由 节点 和连接的 边 组成。每个节点对应于沿道路已知的地理位置,编码为纬度、经度和高度元组。每个 有向边 连接沿着基础道路的相邻节点,并包含许多属性,如航向、最高速度、道路类型等。下方的图 7用一个简单的例子说明了这个概念。
图 7 — 上图展示了一个小型数字道路网络,突出了一个交叉口。每个红点代表现有道路上的已知地理位置。蓝色线条表示节点之间的连接边。请注意,这些边通常是有向的,并且可能是多个的。(图片来源:作者)
成功时,地图匹配过程会生成有关采样轨迹的相关和有价值的信息。一方面,该过程将采样的 GPS 点投射到最可能的道路图 边 上。地图匹配过程通过将观察到的点准确地放置到推断的道路图 边 上来“纠正”观察到的点。另一方面,该方法还通过提供最可能的路径,通过道路图重建图 节点 的序列,以对应于采样的 GPS 位置。请注意,如前所述,这些输出是不同的。第一个输出包含沿着最可能路径的 边 的坐标,而第二个输出由重建的图 节点 序列组成。下方的图 8展示了这个过程。
图 8 — 上图展示了地图匹配过程,其中绿色点代表观察到的 GPS 坐标,橙色钻石代表沿已知边的投影位置。请注意,对于上述简化的示例,我们只能安全地重建节点 2 和 3 之间的路径。这种困境并不像看起来那么严重,因为在实际地图中,轨迹匹配的边缘远远超过一个,因此信息丢失最小。(图片来源:作者)
地图匹配过程的一个副产品是使用共享道路网络表示法对输入位置进行标准化,特别是在考虑第二种输出类型时:最可能的节点序列。在将采样的 GPS 轨迹转换为一系列节点时,我们通过将推断路径减少为一系列节点标识符,使其可比拟。我们可以将这些节点序列视为已知语言的短语,其中每个推断的节点标识符是一个词,其排列传达了行为信息。
这是第五篇文章,我在其中探讨了扩展车辆能源数据集¹ (EVED) [1]。该数据集是对先前工作的扩展和评审,并提供了原始 GPS 采样位置的地图匹配版本(上方图 8中的橙色钻石)。
不幸的是,EVED 仅包含投影的 GPS 位置,而缺少重建的道路网络节点序列。在我之前的两篇文章中,我解决了从转换后的 GPS 位置重建道路段序列而无需地图匹配的问题。我发现结果有些令人失望,因为我预期的缺陷重建率低于观察到的 16%。您可以从以下文章中跟踪这一讨论。
三角形在地理空间查询中具有强大的特性
towardsdatascience.com ## 更多关于道路网络匹配
道路网络匹配的趣事
towardsdatascience.com
现在,我正在查看源地图匹配工具,以了解它在纠正缺陷重建方面的能力。因此,让我们对Valhalla进行测试。以下是我用来在 Docker 容器中运行 Valhalla 的步骤、参考文献和代码。
Valhalla 设置
在这里,我紧跟桑迪普·潘迪 [2]在其博客上提供的说明。
首先,确保你的机器上已安装 Docker。要安装 Docker 引擎,请参阅 在线说明。如果你使用的是 Mac,另一个很好的选择是 Colima。
安装完成后,你必须从 GitHub 拉取一个 Valhalla 镜像,方法是按照下面的 Figure 9 所示在命令行中发出以下命令。
Figure 9 — 从命令行拉取 Valhalla 的 Docker 镜像。(图片来源:作者)
在执行上述命令时,你可能需要输入你的 GitHub 凭据。此外,确保你已经克隆了本文的 GitHub 仓库,因为一些文件和文件夹结构会引用它。
完成后,你应该打开一个新的终端窗口,并发出以下命令以启动 Valhalla API 服务器(MacOS、Linux、WSL):
Figure 10 — 上述命令在 Docker 容器中运行拉取的 Valhalla 镜像。首次执行时,该命令还会在启动之前下载和准备最新的 Geofabrik Michigan 数据文件。(图片来源:作者)
上述命令行明确指定了要从 Geofabrik 服务下载哪个 OSM 文件,即最新的 Michigan 文件。这一指定意味着第一次执行时,服务器将下载并处理该文件并生成优化后的数据库。在后续调用中,服务器将省略这些步骤。需要时,删除目标目录下的所有内容以刷新下载的数据,并重新启动 Docker。
我们现在可以使用专用客户端调用 Valhalla API。
输入 PyValhalla
这个衍生项目简单地提供了对精彩的 Valhalla 项目 的打包 Python 绑定。
使用 PyValhalla Python 包非常简单。我们从使用以下命令行进行的简洁安装过程开始。
Figure 11 — 你可以使用 PIP 安装 PyValhalla。(图片来源:作者)
在你的 Python 代码中,你必须导入所需的引用,从处理后的 GeoFabrik 文件中实例化配置,最后创建一个 Actor 对象,这是你访问 Valhalla API 的入口。
Figure 12 — 上述代码展示了如何在 Python 应用程序或笔记本上轻松设置 PyValhalla。(图片来源:作者)
在我们调用 Meili 地图匹配服务之前,我们必须使用 Figure 13 中列出的函数获取轨迹 GPS 位置。
Figure 13 — 上述函数加载车辆轨迹的唯一位置,返回一个包含纬度、经度和时间戳的 Pandas DataFrame。(图片来源:作者)
我们现在可以设置参数字典以传递给 PyValhalla 调用以追踪路线。有关这些参数的更多细节,请参见 Valhalla 文档。下面的函数调用了 Valhalla(Meili)中的地图匹配功能,并包含在 数据准备脚本 中。它展示了如何从包含观测 GPS 位置的 Pandas 数据框中推断路线,这些位置编码为纬度、经度和时间元组。
图 14 — 上述函数接受一个 PyValhalla Actor 对象和一个包含源路径的 Pandas DataFrame,并返回一个地图匹配的字符串编码折线。这个字符串随后会解码为与数字地图网络节点对应的地理空间位置列表,极端位置除外,这些位置被投影到边缘上。(图片来源:作者)
上述函数返回的匹配路径是字符串编码的折线。如下面的数据准备代码所示,我们可以使用 PyValhalla 库调用轻松解码返回的字符串。请注意,这个函数返回的是一条折线,其第一个和最后一个位置被投影到边缘,而不是图节点。您将看到这些极端位置在本文后面的代码中被去除。
现在让我们来看看数据准备阶段,我们将 EVED 数据库中的所有轨迹转换为一组地图边缘序列,从中我们可以导出模式频率。
数据准备
数据准备的目的是将噪声较大的 GPS 获取轨迹转换为对应于已知地图位置的地理空间标记序列。主要代码遍历现有的行程,一次处理一个。
我在本文中使用 SQLite 数据库来存储所有数据处理结果。我们从填充匹配的轨迹路径开始。您可以参考下面的 图 15 中的代码描述。
图 15 — 上述代码包含了预处理数据的循环。这个循环遍历已知的轨迹,计算它们的地图匹配路径(如果有的话),将节点分词,并将其扩展为三元组。代码将所有中间结果和最终结果存储在数据库中。(图片来源:作者)
对于每条轨迹,我们实例化一个Actor类型的对象(第 9 行)。这是一个未明确说明的要求,因为每次调用地图匹配服务都需要一个新实例。接下来,我们加载(第 13 行)车辆 GPS 接收器获取的轨迹点,这些点带有原始 VED 文章中提到的添加噪声。在第 14 行,我们调用 Valhalla 进行地图匹配,检索编码后的匹配路径并将其保存到数据库。接着,我们将编码后的字符串解码成一组地理空间坐标列表,去除两端极值(第 17 行),然后将其转换为计算在 15 级别 H3 网格上的 H3 索引列表(第 19 行)。在第 23 行,我们将转换后的 H3 索引和原始坐标保存到数据库,以便后续的反向映射。最后,在第 25至27行,我们基于 H3 索引列表生成一系列 3 元组,并保存它们以便后续推断计算。
让我们逐步分析每一个步骤,并详细解释它们。
轨迹加载
我们已经看到如何从数据库中加载每条轨迹(参见图 13)。一条轨迹是一个按时间顺序排列的采样 GPS 位置序列,编码为纬度和经度对。请注意,我们没有使用 EVED 数据提供的匹配版本的这些位置。在这里,我们使用最初 VED 数据库中存在的带有噪声和原始坐标。
地图匹配
调用地图匹配服务的代码已经在上文的图 14中介绍过。其核心问题在于配置设置;除此之外,这是一个非常直接的调用。将结果编码后的字符串保存到数据库中也很简单。
图 16 — 上述代码将编码后的折线字符串保存到新的数据库中。(图片来源:作者)
在主循环的第 15 图中的第 17 行,我们将几何字符串解码为纬度和经度元组列表。请注意,这是我们剥离初始和最终位置的地方,因为它们没有投影到节点上。接下来,在第 19 行,我们将此列表转换为相应的 H3 标记列表。我们使用最大详细级别来尝试避免重叠,并确保 H3 标记与地图图形节点之间的一对一关系。在接下来的两行中,我们将这些标记插入数据库中。首先,我们保存整个标记列表,并将其与轨迹关联。
图 17 — 上述函数将轨迹的 H3 标记列表插入数据库中。(图片来源:作者)
接下来,我们插入节点坐标到 H3 标记的映射,以便从给定的标记列表绘制折线。这一功能在推断未来行程方向时将会很有帮助。
图 18 — 我们插入 H3 标记和节点坐标之间的映射,以便从给定的推断标记重构轨迹。(图片来源:作者)
我们现在可以生成并保存相应的令牌三元组。下面的函数使用新生成的 H3 令牌列表并将其扩展为另一个三元组列表,如图 3中详细说明的那样。扩展代码在图 19中展示。
图 19 — 上述代码将 H3 令牌列表转换为相应三元组的列表。(图片来源:作者)
在三重扩展之后,我们可以将最终产品保存到数据库中,如下方的图 20所示。通过巧妙地查询这个表,我们将推断当前的三重概率和未来最可能的轨迹。
图 20 — 上述函数将 H3 三元组保存到数据库中。这是数据准备阶段的最后一步。我们现在可以开始探索我们收集的信息。(图片来源:作者)
我们已经完成了一轮数据准备循环。一旦外循环完成,我们将拥有一个新的数据库,所有轨迹都转换为令牌序列,我们可以随意探索。
你可以在 GitHub 仓库中找到完整的数据准备代码。
概率和预测
我们现在转向估计现有行程概率和预测未来方向的问题。让我们先定义一下“现有行程概率”的含义。
行程概率
我们从通过地图匹配投影到道路网络节点的任意路径开始。因此,我们有一个来自地图的节点序列,并希望评估该序列的概率,使用已知的行程数据库作为频率参考。我们使用图 5中的公式。简而言之,我们计算所有单独的三重组概率的乘积。
为了说明这一功能,我实现了一个简单的Streamlit 应用程序,允许用户在覆盖的安娜堡区域绘制任意行程并立即计算其概率。
一旦用户在地图上绘制表示行程或假设 GPS 样本的点,代码会将它们进行地图匹配以检索底层的 H3 令牌。从那时起,只需计算单个三重组的频率并将其相乘即可计算总概率。图 21中的函数计算任意行程的概率。
图 21 — 上述函数从三重频率数据库中计算任意路径的概率。(图片来源:作者)
该代码得到另一个函数的支持,该函数检索任何现有的 H3 令牌对的后继。下列函数在图 22中列出,查询频率数据库并返回一个 Python Counter 对象,包含输入令牌对所有后继的计数。当查询未找到后继时,函数返回None 常量。注意该函数如何使用缓存以提高数据库访问性能(此处未列出代码)。
图 22 — 上述函数查询频率数据库以获取任何一对 H3 令牌的已知后继,并返回一个包含所有后继计数的 Counter 对象。(图片来源:作者)
我设计了这两个函数,使得在任何给定节点没有已知后继时,计算的概率为零。
让我们看看如何预测轨迹的最可能未来路径。
预测方向
我们只需要从给定的运行轨迹中获取最后两个令牌,以预测其最可能的未来方向。这个想法涉及扩展该令牌对的所有后继,并选择最频繁的那些。下面的代码展示了作为方向预测服务入口点的函数。
图 23 — 上述函数从 Folium 中填充一个 FeatureGroup 对象,包含现有用户提供的轨迹的预测路径。(图片来源:作者)
上述函数首先通过检索用户绘制的轨迹作为一组与地图匹配的 H3 令牌,并提取最后一对。我们将这一对令牌称为种子,并将在代码中进一步扩展它。在第 9 行,我们调用种子扩展函数,该函数返回一个与输入扩展标准对应的折线列表:每次迭代的最大分支数和总迭代次数。
让我们通过查看下列代码了解种子扩展函数的工作原理,如图 24所示。
图 24 — 种子扩展函数使用 PredictedPath 类来管理每次迭代。有关该类的更多详细信息,请见下文。(图片来源:作者)
通过调用生成最佳后继路径的路径扩展函数,种子扩展函数迭代地扩展路径,从初始路径开始。路径扩展通过选择一条路径并生成最可能的扩展,如图 25所示。
图 25 — 上述路径扩展函数迭代当前路径的最频繁后继。它为每个最频繁的后继创建一条新路径,使用一个专门的函数(见下文)。(图片来源:作者)
该代码通过将后继节点附加到源路径上来生成新路径,如下图 26所示。
图 26 — 要生成“子”路径,我们只需将后继节点附加到现有路径上,如下所示。注意,代码在附加新节点之前创建了原始路径的副本。(图片来源:作者)
该代码使用一个专门的类来实现预测路径,如图 27所示。
图 27 — 上述类实现了一个具有概率排序支持的预测路径,基于种子令牌对进行创建,并生成地图折线。(图片来源:作者)
应用
现在可以在下面的图 28中查看结果 Streamlit 应用程序。
图 28 — Streamlit 应用程序展示了两个描述的功能。输入轨迹为蓝色,可以使用地图左侧的工具菜单绘制。一旦绘制完成,代码将计算其概率并在底部显示。三条红色轨迹是源轨迹可能演变的三个最可能的五十边预测。点击每条轨迹可以弹出计算出的概率。(图片来源:作者)
结论
在这篇文章中,我介绍了一种预测车辆在数字地图道路网络中未来轨迹的方法。利用历史轨迹数据库,该方法为任何行程分配一个概率,并预测近期最可能的方向。因此,这种方法可以检测到不太可能的或甚至是前所未见的新轨迹。
我们从感兴趣区域的大量车辆轨迹数据库开始。每条路径都是地理坐标(纬度和经度)的时间顺序序列,以及其他相关属性,如速度。我们通常从车载 GPS 接收器收集这些轨迹,并将其集中编入数据库。
GPS 样本由于信号测量过程中不可避免的误差而产生噪声。自然和人工障碍物,如城市峡谷,可能显著降低信号的接收精度并增加地理位置误差。幸运的是,实用的解决方案通过概率匹配将 GPS 样本与数字地图对齐来解决这个问题。这就是地图匹配的全部内容。
通过将嘈杂的 GPS 样本与已知数字地图进行匹配,我们不仅通过将每个实例投影到地图上最可能的道路段来纠正精度问题,还获得了一系列车辆最可能经过的现有地图定义的位置。这个最后的结果对我们的轨迹预测至关重要,因为它本质上将一组嘈杂的 GPS 坐标转换为数字地图中干净且已知的点集。这些数字标记是固定的,不会改变,通过将 GPS 样本序列投影到这些标记中,我们得到一串已知的令牌,稍后可以用于预测。
我们使用已知令牌序列频率来计算所有概率,这些序列代表任意轨迹及其未来演变。结果是几个 Python 脚本,一个用于数据准备,另一个用于使用 Streamlit 平台进行数据输入和可视化。
备注
参考文献
[1] 张三、Fatih、Abdulqadir、Schwarz、和马晓(2022)。扩展车辆能源数据集(eVED):一个用于深度学习车辆行程能源消耗的增强型大规模数据集。arXiv。 doi.org/10.48550/arXiv.2203.08630
[2] Valhalla 的高效快速地图匹配 — Sandeep Pandey(ikespand.github.io)
[3] 使用 Valhalla 的 Meili 正确完成地图匹配 | by Serge Zotov | Towards Data Science
João Paulo Figueira 是 tb.lx by Daimler Truck 在葡萄牙里斯本的数据科学家。
使用 R 绘制南美洲地图:深入探讨地理可视化
导航数据集、地缘政治细节和编码挑战,描绘大陆的全貌
·
关注 发表在 Towards Data Science · 8 分钟阅读 · 2023 年 8 月 30 日
--
图片由 Alexander Schimmeck 提供,来源于 Unsplash
所以你是那种从小就喜欢地图和地理的数据科学家和业余 Medium 作者。你正在寻找一个适合你后续图表工作,特别是地图的好主题,当你意识到你所在的国家巴西的官方统计局发布了最新的普查数据时。为什么不呢?为什么不对比一下巴西与其南美洲邻国的情况呢?这可能是使用 R 和所有优秀包的简单任务。让我们来做吧。
在做出这个决定的瞬间,意识到这个简单的任务实际上是一个英雄的旅程,包含了发现最合适的数据集与形状文件、信息缺乏、形状文件互操作性、纬度和经度数学、地理概念中的文化差异,甚至地缘政治问题,如如何将法国海外领土的地图和数据正确地放在南美洲中。
接下来的段落解释了在世界地图的限定区域绘制人口信息的一些可能路径。下面描述的逐步过程可能对所有那些对国际比较有兴趣的地理可视化方法者有用,即使他们的目的只是比较非洲国家的水资源获取情况或北美的肥胖率。
帕查玛玛
让我们从整体图像开始:R 版的世界地图。请见下图和代码。
世界地图:图片由作者提供
library(readxl)
library(geobr)
library(tidyverse)
library(sf)
library(spData)
library(ggrepel)
library(colorspace)
data("world")
#mapa mundi
world %>%
ggplot() +
geom_sf(aes(fill=pop/10⁶)) +
scale_fill_continuous_sequential(palette= "Heat 2" )+
theme_void() +
theme(
panel.background = element_rect(fill="#0077be")
) +
labs(
fill= str_wrap("População em milhões de habitantes", 10)
)
我使用{spData}包作为具有全球领土形状文件几何信息的数据框的参考。aes 函数使用人口信息填充形状。众所周知,中国和印度是世界上人口最多的国家,每个国家都有超过 10 亿人。热度颜色显示了与其他国家的对比。大多数顺序颜色较弱。我们几乎无法理解图片中的颜色渐变。如果你想要更好的颜色分布,对数是最佳选择。见下文。
对数刻度的世界地图。图片由作者提供
world %>%
ggplot() +
geom_sf(aes(fill=pop)) +
scale_fill_continuous_sequential(palette= "Heat 2", trans= "log2" )+
theme_void() +
theme(
panel.background = element_rect(fill="#0077be"),
legend.position = "none"
)
在代码中,你可以看到 scale_fill_continuous_sequential
函数中的对数变换。
在世界数据框结构中,有一个“Continent”列。因此,使用该列筛选数据以获取南美洲地图是显而易见的。请查看代码,紧接着是地图。
南美洲地图:第一版。图片由作者提供
world %>%
filter(continent == "South America") %>%
ggplot() +
geom_sf(aes(fill=pop/10⁶)) +
scale_fill_continuous_sequential(palette= "Heat 2" )+
theme_void() +
theme(
panel.background = element_rect(fill="#0077be")
) +
labs(
fill= str_wrap("População em milhões de habitantes", 10)
)
如你所见,dplyr 过滤函数运作良好;这正是我们想要看到的地图。但这真的正确吗?
气候变化是一个巨大的问题,但海平面尚未上升到淹没曾经出现在南美洲北部的明显区域的程度。这到底发生了什么?让我们现在借助坐标绘制另一张地图,并命名多边形。
南美洲地图:第二版。作者图片
southamerica<-
world %>%
filter(continent=="South America")
southamerica$lon<- sf::st_coordinates(sf::st_centroid(southamerica$geom))[,1]
southamerica$lat<- sf::st_coordinates(sf::st_centroid(southamerica$geom))[,2]
southamerica %>%
ggplot() +
geom_sf(aes(fill=pop/10⁶)) +
scale_fill_continuous_sequential(palette= "Heat 2" )+
theme_light() +
theme(
panel.background = element_rect(fill="#0077be")
) +
labs(
fill= str_wrap("População em milhões de habitantes", 10)
)+
geom_text_repel(aes(x=lon, y=lat, label= str_wrap(name_long,20)),
color = "black",
fontface = "bold",
size = 2.8)
theme_light
代替 theme_void
足以显示坐标。多边形命名花费了更多工作。我们必须计算每个多边形的质心,然后将这些信息用作 geom_text_repel
函数中的 x 和 y 坐标。
使用这个新地图版本和一些先前的知识,我们发现缺失的领土是法属圭亚那,它位于北纬 0º 和 10º 之间,西经 53º 和 55º 之间。我们的下一个任务是了解如何获取法属圭亚那的信息:多边形、人口以及一些坐标来填补我们的地图。
La Mer
我必须将法国从世界其他地方隔离开来,以理解 {spData} 包如何处理这个国家地图的数据。见下文结果。
法国地图。作者图片
france<-
world %>%
filter(iso_a2 == "FR")
france %>%
ggplot() +
geom_sf(aes(fill=pop)) +
scale_fill_continuous_sequential(palette= "Heat 2", trans= "log2" )+
theme_light() +
theme(
#panel.background = element_rect(fill="#0077be")
) +
labs(
fill= str_wrap("População", 30)
)
法国有许多所谓的海外领土。{spData} 包的方法是仅表示主要领土,加上科西嘉岛(地中海中的一个岛屿)和法属圭亚那,它位于精确的坐标范围内,这个范围正好填补了我们南美洲最后一张地图中的空白。
我的下一个尝试是将包含法国几何数据的数据框添加到我的南美洲过滤器中,但我知道我还需要更多。见下文
南美洲 + 法国。作者图片
southamerica %>%
bind_rows(france) %>%
ggplot() +
geom_sf(aes(fill=pop/10⁶)) +
scale_fill_continuous_sequential(palette= "Heat 2" )+
theme_light() +
theme(
panel.background = element_rect(fill="#0077be")
) +
labs(
fill= str_wrap("População em milhões de habitantes", 10)
)+
geom_text_repel(aes(x=lon, y=lat, label= str_wrap(name_long,20)),
color = "black",
fontface = "bold",
size = 2.8)
正如你在代码中看到的,我使用 bind_row
将南美洲领土与法国的 shapefile 结合起来。这样我们现在就有了良好定位的法属圭亚那。另一方面,地图上没有人口信息,而法国在殖民历史的反面作为南美洲的一部分。
换句话说,我想要的是这张地图。
法属圭亚那在南美洲地图上。作者图片
data_guiana<-
insee::get_idbank_list('TCRED-ESTIMATIONS-POPULATION') %>%
filter(str_detect(REF_AREA_label_fr,"Guyane")) %>%
filter(AGE == "00-") %>% #all ages
filter(SEXE == 0) %>% #men and women
pull(idbank) %>%
insee::get_insee_idbank() %>%
filter(TIME_PERIOD == "2023") %>%
select(TITLE_EN,OBS_VALUE) %>%
mutate(iso_a2 = "FR")
data_guiana <- janitor::clean_names(data_guiana)
southamerica %>%
bind_rows(france) %>%
left_join(data_guiana) %>%
mutate(pop=ifelse(iso_a2=="FR",obs_value,pop))%>%
mutate(lon= ifelse(iso_a2=="FR", france[[11]][[1]][[1]][[1]][1,1], lon),
lat= ifelse(iso_a2=="FR",france[[11]][[1]][[1]][[1]][1,2], lat)) %>%
ggplot() +
geom_sf(aes(fill=pop/10⁶)) +
scale_fill_continuous_sequential(palette= "Heat 2" )+
geom_text_repel(aes(x=lon, y=lat, label= str_wrap(name_long,20)),
color = "black",
fontface = "bold",
size = 2.8)+
coord_sf(xlim = c(-82,-35), ylim=c(-60,15))+
theme_light() +
theme(
panel.background = element_rect(fill="#0077be")
) +
labs(
fill= str_wrap("População em milhões de habitantes", 10)
)
正如你所看到的,我使用了由法国官方统计办公室制作的 R 包 来获取圭亚那的人口。此外,我将地图限制在适当的坐标范围内,以查看南美洲。
Emolduram e aquarelam o meu Brasil
现在地图英雄终于解决了南美洲的问题,并与法国演奏了 pipes of peace,是时候回到巴西的数据和地图了。记住,我想将一些巴西的人口普查细节与巴拿马以南其他国家和地区进行比较。
数据普查可以在 R 包 或 API 地址上找到。我选择了使用 API 的更具挑战性的选项。另一次使用其他选项可能是个好主意。查看下面的代码和地图,我展示了巴西各州的人口与其他南美洲领土的对比。
南美洲 + 巴西各州。图片由作者提供
central_america<-
world %>%
filter(subregion == "Central America")
brasil<- geobr::read_country()
estados<- geobr::read_state()
#dados de população
ibge2022<-
get_municipalies_data()
estados<-
estados %>%
inner_join(
ibge2022 %>%
rename(abbrev_state = uf) %>%
summarise(.by=abbrev_state,
pop = sum(populacao_residente)
)
)
southamerica %>%
filter(iso_a2!="BR") %>%
bind_rows(france) %>%
left_join(data_guiana) %>%
mutate(pop=ifelse(iso_a2=="FR",obs_value,pop))%>%
mutate(lon= ifelse(iso_a2=="FR", france[[11]][[1]][[1]][[1]][1,1], lon),
lat= ifelse(iso_a2=="FR",france[[11]][[1]][[1]][[1]][1,2], lat)) %>%
ggplot() +
geom_sf(aes(fill=pop/10⁶)) +
geom_sf(data=estados, aes(fill=pop/10⁶)) +
geom_sf(data=brasil,fill=NA, color="#00A859", lwd=1.2)+
geom_sf(data= central_america,fill= "#808080")+
scale_fill_continuous_sequential(palette= "Heat 2" )+
geom_text_repel(aes(x=lon, y=lat,
label= str_wrap(name_long,20)),
color = "black",
fontface = "bold",
size = 2.8)+
coord_sf(xlim = c(-82,-35), ylim=c(-60,15))+
theme_void() +
theme(
panel.background = element_rect(fill="#0077be")
) +
labs(
fill= str_wrap("População em milhões", 10)
)
我使用上述 API 编写了函数 get_municipalites_data。代码可以在我的 gist 上找到。还请注意提供用于绘制巴西及其子区域边界的两个函数:read_country 和 read_states。这些函数在 {geobr} 包中。
我使用了世界数据框中的另一个筛选器。在这种情况下,目的是显示中美洲次大陆的起始部分,并用灰色阴影绘制其地图。在这里,我们面临了一种文化差异,因为我们在巴西了解到美洲有三个次大陆:北美、中美洲和南美洲。对于数据集的作者来说,中美洲是北美的一个子区域。
现在是时候结束我的工作了。我想在地图上显示八个最人口稠密的领土的名称。即使在最后的冲刺阶段,也有一些代码技巧。
最人口密集的领土。图片由作者提供
estados$lon<- sf::st_coordinates(sf::st_centroid(estados$geom))[,1]
estados$lat<- sf::st_coordinates(sf::st_centroid(estados$geom))[,2]
most_populated<-
southamerica %>%
filter(iso_a2 !="BR") %>%
rename(name= name_long) %>%
as_tibble() %>%
select(name, pop, lat, lon) %>%
bind_rows(
estados %>%
rename(name= name_state) %>%
as_tibble() %>%
select(name, pop, lat, lon)
) %>%
slice_max(order_by = pop, n=8)
southamerica %>%
filter(iso_a2!="BR") %>%
bind_rows(france) %>%
left_join(data_guiana) %>%
mutate(pop=ifelse(iso_a2=="FR",obs_value,pop))%>%
mutate(lon= ifelse(iso_a2=="FR", france[[11]][[1]][[1]][[1]][1,1], lon),
lat= ifelse(iso_a2=="FR",france[[11]][[1]][[1]][[1]][1,2], lat)) %>%
ggplot() +
geom_sf(aes(fill=pop/10⁶)) +
geom_sf(data=estados, aes(fill=pop/10⁶)) +
geom_sf(data=brasil,fill=NA, color="#00A859", lwd=1.2)+
geom_sf(data= central_america,fill= "#808080")+
scale_fill_continuous_sequential(palette= "Heat 2" )+
geom_text_repel(data= most_populated,
aes(x=lon, y=lat,
label= str_c(str_wrap(name,10),": ",round(pop/10⁶,1))),
color = "black",
fontface = "bold",
size = 2.9)+
coord_sf(xlim = c(-82,-35), ylim=c(-60,15))+
theme_void() +
theme(
panel.background = element_rect(fill="#0077be")
) +
labs(
fill= str_wrap("População em milhões", 10)
)
三个巴西州位于南美洲最人口稠密的八个领土中。实际上,圣保罗是地图上第二个最有人口的地区,仅次于哥伦比亚。
现在,专注于代码,你可以看到我创建了一个新的数据框,通过结合两个不同的 sf 对象来建立这个排名。我选择了一部分列,并将类型从 sf 更改为 tibble,以便进行行绑定。
就这样。英雄完成了一个可能的路径,并留下了下一次旅程的足迹。现在轮到你了。记住所有可能通过地图表示得到显著改善的项目。根据上述操作步骤,收集有关人口、社会经济问题等的所有数据,只需选择一个变量来填充多边形。
代码和数据
完整代码可以在 gist 上找到。
所有巴西数据集被认为是公共领域的数据,因为这些数据是由联邦政府机构生产的,作为主动透明度在互联网上提供,并且受到巴西信息公开法的约束。
IBGE: 巴西人口普查数据
IPEA: 巴西 shapefiles
法国的数据可以从开放数据门户获取,并标注为开放许可,允许将信息用于商业目的。
映射全球自然再造林项目的潜力
原文:
towardsdatascience.com/mapping-the-global-potential-of-natural-reforestation-projects-c425d2998fa5
使用地面观测、遥感和机器学习
·发布于Towards Data Science ·阅读时长 11 分钟·2023 年 8 月 21 日
--
作者:Stephen Klosterman 和 Earthshot 科学团队。内容最初在2022 年 12 月美国地球物理联盟秋季会议上展示。
介绍
生态恢复项目通常需要投资以启动活动。为了为森林增长和保护项目创造碳融资机会,必须能够预测木质生物量中的碳积累,或在防止森林砍伐的情况下避免的排放。此外,还需要尝试理解广泛的其他生态系统属性的可能变化,例如植物和动物物种组成及水质。为了创建碳积累预测,常见的方法是对特定地点的项目给予个别关注和研究努力,这些项目可能分布在全球各地。因此,拥有一张局部准确且全球适用的生长率或其他感兴趣参数值的地图,将便于快速“勘探”生态系统恢复机会。在这里,我们描述了创建这样的地图的方法,该地图源于基于先前发布的文献综述数据训练的机器学习模型。随后,我们展示了如何在 Google Earth Engine 应用中实施该地图。
数据与方法
我们使用了一份最近发布的数据集,该数据集包含森林群体生物量测量值、年龄和地理位置(Cook-Patton et al. 2020),用于训练一个机器学习模型,以预测常用的查普曼-理查兹(CR)生长函数的一个参数。
在清理了异常值和不现实观测数据后,我们剩下了大约 2000 个观测值,如下图所示,符号大小与每个地点的观测数量成正比:
基于地点的全球数据分布;符号大小与每个地点的测量数量成正比。作者提供的图片。
观测数据分布在 390 个地点。大多数地点(64%)只有一个测量点,而有一个地点有 274 个测量点。
Cook-Patton 等(2020)将这些数据与来自美国和瑞典的其他库存数据结合,创建了全球碳积累速率地图。然而,该工作假设了一个线性碳积累模型,而有更多生物学上更为现实的替代模型。这里我们展示了如何为曲线拟合方程(CR 函数)创建全球参数层。我们的方法类似于 Chazdon 等(2016)的工作,创建了拉丁美洲热带地区 Michaelis-Menten 方程的参数值地图。然而,这里我们没有将曲线拟合参数模型限制为环境协变量的线性组合,而是将曲线拟合参数作为统计学习方法(XGBoost)的响应变量,以捕捉特征之间的任何非线性或交互行为。我们选择用 CR 方程表示生长,因为它是一个灵活的曲线,可以呈现 S 形或对数形状,是林业行业中一种简单而生物学上现实的树木生长模型的标准方法(Bukoski 等,2022):
Chapman-Richards(CR)方程。作者提供的图片。
其中y是时间t的生物量,y_max是森林成熟时的生物量上限,b控制时间 0 时的生物量,m是形状参数,k是我们估计的生长参数。在这项工作中,我们假设b = 1,将生物量限制为零,m = ⅔,与类似工作中的做法一致(Bukoski 等,2022)。这就只剩下y、y_max、t和k作为未知量。以下是几个示例 CR 曲线,m = ⅔,b = 1,y_max = 1,和不同的k值:
作者提供的图片
参数k明显影响达到接近最大潜在生物量水平所需的时间:
作者提供的图片
我们从 Walker 等(2022)的潜在生物量图中提取了相关地点的y_max值。与这篇出版物一起提供的地图包括了当前和最大潜在生物量的预测,涵盖了各种假设和条件。特别是,我们使用了这里提供的 Base_Pot_AGB_MgCha_500m.tif 地图。通过为每个测量点估算最大生物量,我们将 Cook-Patton 等(2020)中的生物量和森林立地年龄的配对值分别代入y和t,并计算了该数据集中每个测量值的k值。以下是我们获得的k值分布,显示出一个广泛的范围和较长的右尾:
由作者提供的图片
在解读这些数据时,重要的是要注意,由于我们使用了树木生长数据源,我们的模型将适用于自然再生森林。最近已经完成了对单一物种种植园的文献回顾(Bukoski 等,2022),并且正在进行对农林系统(Cook-Patton等,正在准备中)和多样化树种种植项目(Werden等,正在准备中——请联系 Leland Werden(leland@crowtherlab.com)参与公民科学文献回顾,特别是如果你会讲除英语外的其他语言)。一个类似的相关工作涉及对自然再生和单一栽培在缓解气候变化方面相对有效性的经济计量比较(Busch等,正在准备中)。
我们为我们的模型使用了 61 个空间显式特征,包括生物群落(在进行一次性编码后 12 个特征)、来自 SoilGrids 项目的土壤属性(11 个特征)、Terraclimate提供的月度气候数据(1960–1990 年期间的 14 个特征)以匹配Bioclim数据(19 个特征),以及高程、坡度、方位和阴影的地形特征(4 个特征)。这些特征类似于其他研究中应用机器学习来绘制碳积累速率(Cook-Patton 等,2020)或其他相对时间不变的生态系统属性(如某地点的最大潜在生物量)(Walker 等,2022)所使用的特征。
我们使用了XGBoost来构建多个回归模型以预测k并探索响应变量与潜在特征列表之间的关系。为了帮助解释,我们的目标之一是选择一个特征尽可能少但性能良好的模型。我们使用SHAP(SHapley Additive exPlanation)值来确定特征的重要性,并发现模型性能和选定特征的显著差异,取决于用于训练与测试的数据。因此,我们对所有模型开发使用了 10 折交叉验证,而不是留出单独的测试集。我们使用GroupKFold以确保来自同一地点的测量不会在折叠之间被分割;换句话说,没有任何一个折叠在训练数据和测试数据中都有来自同一地点的数据,从而减少模型评估中的空间自相关效应。
-
修剪相关预测变量: 为了开始选择特征,我们最初根据平均绝对 SHAP 值对所有特征进行排序。为此,我们为每个 10 个训练折叠训练了一个模型,然后计算相应验证折叠的平均绝对 SHAP 值,以查看特征在训练样本之外进行预测时的重要性。我们对所有 10 个折叠中每个特征的平均绝对 SHAP 值进行了汇总,并按降序排序以进行“排名选择投票”特征选择程序。从列表顶部的特征开始,按顺序进行,我们丢弃了所有 Pearson's r > 0.8 且排名较低的预测变量,以减少多重共线性,并开始修剪特征集的过程。此步骤对最终模型性能的影响微乎其微,并将特征集减少到 41 个特征。
-
每折训练单独模型并结合见解: 使用这个较小的特征集,我们对 10 个折叠中的每一个分别进行了反向选择:在每次迭代中,我们根据验证集上的平均绝对 SHAP 值对特征进行排序。根据这个排序,我们确定了特征最少的模型,其生物量估计的 RMSE(均方根误差)在 1 Mg AGB/ha(每公顷地上生物量)内,约为 1%的差异——换句话说,最简单的模型几乎“与最佳模型一样好”。由于这个步骤,不同折叠的最佳模型具有不同的特征和特征数量。为了结合各折叠的见解,我们执行了类似于前一步的排名选择投票,以确定哪些特征应该考虑用于最终模型及其排序(27 个特征)。
-
使用每次折叠的共同特征集进行最终特征选择: 再次使用向后选择程序,我们检查了 10 个折叠的验证集上的均值 RMSE 和 R²,但对于每个折叠使用相同的特征集。我们发现模型验证性能在折叠间通常很嘈杂,这突显了需要额外的数据收集以开发更稳健的模型。我们还发现,相对较少特征的模型在验证得分上几乎与更多特征的模型一样好。
作者提供的图像
我们选择了一个具有 5 个特征的模型(最高验证 R²,RMSE 略低于第二低)。最终的向后选择(步骤 3)的结果如上所示,而最终模型的 SHAP 值(在验证集上收集)如下所示。像这里展示的 SHAP 汇总图,也称为“蜜蜂群”图,有一行代表每个特征,在每行中有一个数据集(此处为验证数据)中的每个预测点。点的颜色表示特征值,垂直偏移表示该 SHAP 值的数据密度,x 轴坐标表示 SHAP 值,或该样本对模型预测的有符号影响(Lundberg 和 Lee,2017)。SHAP 汇总图显示了特征的高值或低值是否对模型预测产生了正面或负面的影响及其大小。
作者提供的图像
为了实现这一模型以创建预测的* k *值的全图,我们利用了我们的开源库earthshot_model_utils来导出空间平铺预测因子的 CSV 文件,执行模型推断,然后将结果导出并合并成一个单一的 geotiff(见下一节)。
讨论与结论
与相关研究工作的比较,我们的模型表现似乎相似。交叉验证 R²的平均值(±标准误差)为 0.485 ± 0.04,而使用这些及其他数据进行训练的线性碳积累率模型的 20%保留测试集的 R²为 0.445(Cook-Patton 等,2020)。
在模型的五个特征中,最重要的特征是等温性,它表示某个区域昼夜温差相对于夏冬温差的大小。其值范围从 0 到 100,其中昼夜温差表示为年温差的百分比。这种昼夜温差与年温差的关系被认为影响物种分布,并且对“热带、岛屿和海洋环境”有用(O’Donnell 和 Ignizio,2012)。它可能是环境支持树木生长的一个指标,较大的特征值(相对于年温差较大的昼夜温差)会导致较高的k预测(相对于最大值的较快增长,如上图的 SHAP 值图所示)。等温性在赤道附近相对较大,年温差最小,因此也一般代表较暖的温度。从剩余的特征来看,SHAP 值清楚地表明,树木在风速较小的地方以及土壤湿度较高的地方(无论是物理上还是生物上)预测生长速度较快,这些都是模型行为上合理的。与 Palmer 干旱严重性指数和土壤碳的关系则更加复杂和互动,我们在这里不再详细讨论。
使用 earthshot_model_utils,我们使用训练好的模型在 1 公里空间分辨率下对非洲进行了推断,生成了* k* 值的大陆地图。将这些值代入 CR 公式,并结合潜在的生物量y_max,我们可以生成森林再生开始后的任何时间点的 AGB 地图。我们的 Google Earth Engine 应用 显示了生长开始后 10 年、20 年和 30 年的生物量;30 年的生物量如下图所示。这个数据产品可以用于几乎即时的自然森林再生预测,具有高空间分辨率和大范围地理尺度,能够快速评估碳项目。
这是对自然再生开始 30 年后的生物量的估算。图片由作者提供。
虽然我们选择使用地面数据来创建这项工作的生物量积累模型,但我们团队也在投入研究其他可能的方法。这些方法包括更多依赖遥感观测而非地面观测的统计建模方法,以及基于过程的模型。遥感指标如冠层高度和生物量使用 LiDAR(激光雷达)或 SAR(合成孔径雷达)技术进行测量,这些技术通常没有多时相组件,因此无法跟踪生物量的增长轨迹。学术研究人员和公司正在利用多时相遥感数据(如 Landsat)作为特征构建机器学习模型,以有效地在空间(例如 Walker 等人 2020 年)和时间上(如CTREEs、Chloris Geospatial 和Sylvera的努力)外推 LiDAR 测量。这开辟了广阔的可能性,但需要注意从太空评估树木大小的局限性和必要的地面验证。基于过程的建模将提供另一种有价值的视角,特别是其在建模生物多样性和其他生态系统服务细节方面的能力(参见例如 Fisher 和 Koven, 2020),并努力与遥感数据结合以提供基准数据(Ma 等人 2022)。
为了继续这里描述的工作,我们计划创建模型预测的不确定性地图,使用多种基于自助抽样的模型,以及蒙特卡洛方法来考虑最大潜在生物量的不确定性。我们计划在未来的工作中探索和比较多种建模技术,利用多个基于地面数据集的方法。
参考文献
Bukoski, J.J., Cook-Patton, S.C., Melikov, C. 等人. 全球单一栽培森林中地上碳积累的速率及驱动因素. 《自然通讯》13, 4206 (2022). doi.org/10.1038/s41467-022-31380-7
Chazdon, R.L., Broadbent, E.N., Rozendaal, D.M.A. 等人. 拉丁美洲热带地区二次生长森林再生的碳封存潜力. 《科学进展》2, 5 (2016). doi.org/10.1126/sciadv.1501639
Cook-Patton, S.C., Leavitt, S.M., Gibbs, D. 等人. 全球自然森林再生的碳积累潜力地图. 《自然》585, 545–550 (2020). doi.org/10.1038/s41586-020-2686-x
Fisher, R.A., 和 Koven, C.D. 关于陆地表面模型未来发展及代表复杂陆地系统的挑战的观点. 《先进地球系统模型杂志》12, 4 (2020). doi.org/10.1029/2018MS001453
Lundberg, S.M. 和 Lee, Su-In。《统一模型预测解释方法》。Advances in Neural Information Processing Systems 30 (2017)。 proceedings.neurips.cc/paper_files/paper/2017/file/8a20a8621978632d76c43dfd28b67767-Paper.pdf
Ma, L., Hurtt, G., Ott, L. 等。《生态系统人口模型 (ED v3.0) 的全球评估》。Geosci Model Dev 15, 1971–1994 (2022)。 doi.org/10.5194/gmd-15-1971-2022
O’Donnell, M.S. 和 Ignizio, D.A. 《支持美国大陆生态应用的生物气候预测因子:美国地质调查局数据系列 691(2012)》。 pubs.usgs.gov/ds/691/ds691.pdf
Walker, W.S., Gorelik, S.R., Cook-Patton, S.C. 等。《陆地上碳存储增加的全球潜力》。Proc Natl Acad Sci USA 119, 23 (2022)。 doi.org/10.1073/pnas.2111312119
最初发布于 https://www.earthshot.eco。
《交通拥堵分析:使用图论》
学习如何使用图论找到城市基础设施中的潜在关键点
·
关注 发布于 Towards Data Science ·10 分钟阅读·2023 年 8 月 19 日
--
图论在现实问题中有很多应用,例如社交网络、分子生物学或地理空间数据。今天,我将展示最后一种应用,即分析城市的道路布局,以预测关键街道、交叉口,以及基础设施的变化如何影响这些因素。但首先,让我们从基础知识开始。
图及其中心性度量
图是顶点及其边的集合:
集合 E 是无序元组 (x, y) 的子集,其中 x 和 y 是图的顶点,且 x 不等于 y。[图像由作者提供]
边表示节点之间的连接。如果边没有方向,我们称之为无向图。无向图的一个现实例子可以是化学分子,其中顶点是原子,化学键表示为边。
血清素分子是一个简单无向图的例子。 [来源]
然而,有时我们需要了解边是从 u 到 v,从 v 到 u,还是双向的。例如,如果马克喜欢爱丽丝,并不一定意味着这是双向的( ☹ )。在这些情况下,我们可以将边定义为有序元组而不是无序元组。
方括号表示公式中的无序元组,而圆括号表示有序元组。[图片由作者提供]
人际互动可以使用有向图来描述。[图片由作者提供]
使用图结构,我们可以定义一个中心性度量。这是一个用于回答以下问题的指标:
这个顶点/边在图中的重要性有多大?
而且有许多方法可以回答这个问题。
评估图组件重要性的不同方法
根据任务的不同,我们可以从不同的角度来评估中心性。最常见的度量指标有:度、紧密性和中介性。我们将使用扎卡里·卡拉泰俱乐部图来讨论这些指标 [更多信息]。它展示了不同空手道俱乐部成员之间的联系。你可以在这里找到用于生成图片的代码。
度中心性
最基本的中心性。它仅定义在顶点上,等于顶点的度数(即邻接顶点的数量)。例如,我们可以回想一下人际关系图,在人际友谊的情况下,这个指标将回答以下问题:
“这个人有多受欢迎?”
空手道俱乐部图的节点度中心性。中心性度量标准按图的最大度数(即节点数减一)进行了归一化。[图片由作者提供]
图中的路径
对于接下来的两个中心性,我们需要将一些概念引入到图论知识中。所有这些都是非常直观的,从边的权重开始。我们可以给边添加权重,以标记它们之间的差异。例如,在交通图的情况下,这可以是道路长度。
在图中,我们可以定义路径,即从 A 到 B 需要遍历的顶点列表。路径中的连续顶点是邻居,第一个顶点是 A,最后一个是 B。路径距离是沿途边的权重之和。A 和 B 之间的最短路径是距离最小的路径。
A 和 F 之间的最短路径是 [A, C, E, D, F],距离为 20。 [source]
近似中心性
拥有所有这些新知识后,我们可以回到我们的指标。下一个是近似中心性,它告诉我们一个节点离图中其余部分的距离。它对特定顶点的定义是图中所有其他顶点的最短路径的平均值的倒数。这样,较短的平均路径转化为更高的近似中心性。
Karate Club 图中的节点近似中心性。 [图片由作者提供]
介数中心性
介数中心性提供了图中哪些节点对交通流量至关重要的信息。想象一个拥有广泛道路网络的城市,其中每个交汇点都是一个节点。这些节点中的一些在日常通勤中作为关键连接点,而其他的可能是交通流量几乎没有影响的死胡同。前者的介数中心性得分较高,计算方法是通过交汇点的最短路径的比例。
Karate Club 图中的节点介数中心性。 [图片由作者提供]
城市规划作为图
现在,我们拥有了描述和分析图的工具,可以开始将城市规划提取为图形形式。为此,我们可以使用 Open Street Maps (OSM),通过 osmnx 库将其导入 Python 作为 NX 图。我们将从一个较小的示例开始,讨论为了提高工作效率和时间的处理过程。
Grzegórzki 是克拉科夫市的十八个区之一,有两个复杂的环形交叉口——Mogilskie 和 Grzegórzeckie,以及许多交汇点。因此,我们将能够看到数据工程中的大多数潜在陷阱。
Grzegórzki 的行政边界。 [©Google]
让我们从将 OSM 仓库中的数据导入到 Python 图中,并绘制结果开始:
原始 OSM 数据导入。白点是节点,代表道路交汇点。 [图片由作者提供]
这个图表有些问题——你能发现是什么吗?
我们为单一道路部分获取了多个边缘,结果图形中有近 3000 个“交汇点”。这并没有提供正确的表示(我们不能在路中间掉头,并且每个节点都会使计算变得更慢)。为了解决这种情况,我们将通过删除两个交汇点之间道路上的所有节点来进行图拓扑简化。在 OSMnx 中,我们有一个名为ox.simplify_graph()的函数来实现这一点。
拓扑简化后的道路布局。现在每个节点表示道路交叉点。[作者提供的图片]
还有一个问题——如你所见,我们大多数道路都有两个边缘,每个方向一个。因此,每个交汇点都有多个节点,这是一种不希望出现的行为。想象一下我们在一个交汇点上,我们要左转,而没有专用的左转车道(或者已经满了)。只要我们不能完成转弯,其他车辆就会被阻挡。在我们当前的图形中,这不是真实的。左转由两个独立的节点组成,一个是左转节点,另一个是跨越对面车道的节点。这会表示这些是两个独立的操作,但实际上并非如此。
这就是为什么我们要合并交汇点,这意味着我们将把相互靠近的多个节点合并为一个。我们会选择一个足够大的合并半径,以将交汇点的多个部分合并为一个,但另一方面保持环形交汇点为多个节点结构,因为它们只能部分被阻塞。为此,我们将使用 osmnx 函数ox.consolidate_intersections()。
交汇点合并后的道路布局。[作者提供的图片]
交汇点的比较。之前和之后。[作者提供的图片]
在这些操作之后,我们几乎准备好进行分析了。最后一个问题是克拉科夫的市政边界——由于许多人从邻近城镇旅行,而图形分析仅包括图形中的数据,我们需要包含这些区域。我将在下一章中介绍不这样做的影响。这里是我们的图形:
颜色表示最大速度。颜色越亮,值越高。我们可以看到 A4 高速公路用黄色标记。大多数道路,用蓝色标记,为 50 km/h。[作者提供的图片]
你可以在这个jupyter notebook中找到用于生成此地图的源代码以及下一章中使用的所有图形。
道路布局的介数中心性
在这个案例研究中,我们将专注于使用介数中心性测量来估计道路交通。未来,这可能会扩展到图论中的其他技术,包括GNN(图神经网络)使用。
我们将从计算道路布局表示中所有节点和边的介数中心性开始。为此,我们将使用NetworkX库。
克拉科夫每个道路段的介数中心性。 [作者提供的图片]
由于图中道路数量众多,很难看出哪些组件在交通中最关键。让我们查看图的中心性度量分布。
克拉科夫道路布局中街道和交叉口的中心性度量分布。 [作者提供的图片]
我们可以使用这些分布来过滤掉不太重要的交叉口和街道。我们将选择每个前 2% 的部分,其阈值如下:
-
节点的中心性为 0.047,
-
边的中心性为 0.021。
中心性度量在阈值处理后的图。 [作者提供的图片]
我们可以看到,介数中心性最高的道路段是:
-
A4 高速公路和 S7 作为克拉科夫的环城高速(注意克拉科夫没有北部环城道路),
-
第二环路的西部以及其与 A4 的连接,
-
第三环路的北部(替代缺失的北部环城道路),
-
Nowohucka 街道连接第二环路和城市的东北部,
-
Wielicka 道路从市中心通向东南部高速公路部分。
让我们将这些信息与 Google Maps 上克拉科夫的实际交通地图进行比较:
克拉科夫周一通勤的典型交通 [©2023 Google, source]
我们可以看到我们的见解与交通雷达的结果相符。其背后的机制很简单——那些在图中具有高介数中心性的组件是最常用来通行最短路径的。如果驾驶员选择最佳路线,那么交通量最高的街道和交叉口将具有最高的介数中心性。
让我们回到图形工程的最后部分——扩展图形边界。我们可以检查如果仅将城市边界纳入分析会发生什么:
克拉科夫道路的介数中心性,未考虑邻近城镇。 [作者提供的图片]
A4 高速公路,由于其环城性质,是最重要的组件之一,但在整个图中的中心性度量却是最低的!这是因为 A4 位于城市的边缘,大部分交通来自外部,我们不能将这一因素纳入介数中心性。
如何使用介数中心性分析布局变化对交通的影响,
让我们来看看图分析的不同场景。假设我们想预测道路封闭(例如由于事故)如何影响交通。我们可以使用中心性测量来比较两个图之间的差异,从而检查中心性的变化。
在本研究中,我们将模拟 A4–7 高速公路段上的汽车事故,这是一个常见的情况。事故将导致该段完全封闭。
我们将通过从图中去除 A4–7 段并重新计算中心性测量来创建一个新的道路网络。
新布局的中心性测量。红色的 A4 部分代表缺失部分。[作者提供的图片]
让我们来看看中心性分布:
去除 A4–7 高速公路段后的克拉科夫道路布局中街道和交叉口的中心性测量分布。[作者提供的图片]
我们可以看到它仍然与原始情况非常相似。为了检查中心性测量的变化,我们将计算残差图,其中中心性测量是原始道路布局与事故后之间的差异。正值将表示事故后的更高中心性。在其中一个图中缺失的节点和交叉口(如 A4–7)将不会被包含在残差图中。以下是残差的测量分布:
去除 A4–7 高速公路段后的中心性变化分布。[作者提供的图片]
再次,我们将筛选出受影响的前 2%的街道和节点。这次的阈值是:
-
节点的测量值为 0.018,
-
边的测量值为 0.017。
去除 A4–7 高速公路段后,介于中介中心性增加最大的街道和交叉口。[作者提供的图片]
我们可以看到,连接环路分段到市中心的道路有所增加,其中第二环路位于城市的西侧,包含两座横跨维斯瓦河的桥梁之一的变化最大。
图的中心性分析无法在道路网络上实现的内容
在图分析中,有一些我们无法考虑的因素。在本分析中我们可以看到的两个最重要的因素是:
- 图的中心性分析假设节点之间的流量分布是均匀的。
这在大多数情况下是错误的,因为乡村和城市的人口密度不同。然而,还有其他因素可以减少这种影响,例如,相比于生活在城市中心的人,居住在邻近乡村的人更倾向于选择汽车作为通勤方式。
- 图分析仅考虑图中存在的事物。
在提供的示例中,这一点不易察觉,尤其是对克拉科夫以外的人来说。我们来看一下Zakopianka。它是连接市中心和克拉科夫南部大多数市镇的主要交通干道,也是贯穿全国的 DK7(国家公路 7 号)的组成部分。
DK7 公路 — 绿色部分表示快速路。 [source]
如果我们将克拉科夫的 DK7 典型交通情况与我们的中心性测量进行比较,它们完全不同。平均介数中心性约为 0.01,这比前 2%的阈值小两倍。然而在现实中,它是最拥堵的路段之一。
Zakopianka 的平均拥堵情况与介数中心性进行比较。 [©2023 Google, source]
总结
图论及其分析在多个场景中都有应用,例如本研究中展示的交通分析。通过对图进行基本操作和度量,我们可以在比建立整个模拟模型更短的时间内获得有价值的见解。
这个完整的分析可以通过几十行 Python 代码来完成,并且不限于一种道路布局。我们也可以很容易地过渡到图论的其他分析工具。
像所有事物一样,这种方法也有其缺点。主要缺点是对均匀交通分布的假设以及范围仅限于图结构。
包含本研究中使用的代码的 Github 仓库可以在这里找到。
使用 MapReduce 进行大规模数据处理
深入探讨 MapReduce 和并行化
·发布于 Towards Data Science ·4 分钟阅读·2023 年 7 月 19 日
--
图片由 Luca Nicoletti 提供,发布在 Unsplash
在当前的市场环境中,组织必须进行数据驱动的决策,以保持竞争力并促进创新。因此,每天都会收集大量数据。
尽管数据持久性问题在很大程度上已得到解决,这要归功于云存储的广泛可用性和价格实惠,现代组织仍然面临着高效有效处理大量数据的挑战。
在过去几十年中,出现了许多编程模型来解决大规模处理大数据的挑战。毫无疑问,MapReduce 是最受欢迎和有效的方法之一。
什么是 MapReduce
MapReduce 是一个分布式编程框架,最初由 Jeffrey Dean 和 Sanjay Ghemawat 于 2004 年在 Google 开发,并受到函数式编程基本概念的启发。他们的提案涉及一个包含两个步骤的并行数据处理模型;map 和 reduce。
简单来说,map 步骤涉及将原始数据分割成小块,以便对每个数据块应用转换逻辑。因此,可以在创建的块上并行处理数据,最后,reduce 步骤将汇总/整合处理过的块,并将最终结果返回给调用者。
MapReduce 算法如何工作
尽管 MapReduce 算法通常被认为是一个两步过程,但它实际上包含三个不同的阶段。
1. Map: 在这个第一步骤中,数据被拆分成更小的块,并分布到通常属于处理单元集群的多个节点上。每个创建的块被分配给一个 mapper。Mapper 的输入是一组 <key, value>
对。数据处理执行后(依然是 <key, value>
形式),Mapper 会将生成的输出写入临时存储。
例如,我们可以考虑以下例子,其中输入文本首先被拆分到三个 Mapper 上,输入以键值对的形式提供。
MapReduce 算法的映射步骤 — 来源:作者
2. Shuffling: 在这个步骤中,算法将数据洗牌,以便具有相同键的记录被分配到相同的工作节点。这通常是整个 MapReduce 过程生命周期中最昂贵的操作。
MapReduce 中的洗牌步骤 — 来源:作者
3. Reduce: 在这最后一步中,每个 Reducer 将接受对应 Mapper 输出的 <key, value>
对作为输入。所有具有相同键的 Mapper 输出将分配给相同的 Reducer,Reducer 将汇总这些值,并将汇总结果以 <key, value>
对的形式返回。
Reduce 步骤中的 MapReduce — 来源:作者
MapReduce 和 Hadoop
MapReduce 是 Apache Hadoop 框架的一部分,用于访问存储在 Hadoop 分布式文件系统(HDFS)中的数据。Hadoop 由四个基本模块组成:
-
Hadoop Distributed File System (HDFS):这是一个分布式文件系统,可以以容错的方式存储大型数据集
-
Yet Another Resource Negotiation (YARN):这是一个节点管理器,监控集群和资源,同时也作为作业调度器。
-
MapReduce
-
Hadoop Common:这是一个提供常用 Java 库的模块
之前我们提到过,Mapper 和 Reducer 在计算机集群的独立节点上运行。实际上,这些工作节点是 Hadoop 框架的一部分,该框架决定了每种情况下所需的 Mapper 数量,这取决于输入大小的体积。
Hadoop 设计时提供了容错功能。在节点发生故障时,Hadoop 会在另一个映射节点上重新运行任务并生成所需的输出。
最后的思考
MapReduce 在分布式计算中是一个突破性的概念,使许多组织能够处理大量数据并提取有价值的洞察。
熟悉这个概念至关重要,特别是在利用如 Spark 等依赖于 MapReduce 框架的技术时。
👉 成为会员 ,在 Medium 上阅读所有故事。您的会员费直接支持我和您阅读的其他作者。您还将获得对 Medium 上每个故事的全面访问权限。
[## 通过我的推荐链接加入 Medium — Giorgos Myrianthous
成为 Medium 会员后,您的会员费的一部分将分配给您阅读的作者,并且您可以全面访问每个故事…
gmyrianthous.medium.com](https://gmyrianthous.medium.com/membership?source=post_page-----f0d8776d0fcf--------------------------------)
3 月版:数据与因果关系
每月版
数据科学家如何处理因果推断
·
关注 发表在 Towards Data Science ·4 分钟阅读·2023 年 3 月 2 日
--
照片由 Joey Genovese 提供,刊登在 Unsplash
在最近的作者焦点问答中,Matteo Courthoud 反映了在工业界或学术界工作时,做出可靠预测的重要性日益增加:
我认为在未来,因果推断将变得越来越重要,我们将看到社会科学的理论方法与计算机科学的数据驱动方法之间的趋同。
我们希望你能阅读我们生动对话的其余部分;与此同时,Matteo 的观察激励我们深入档案,寻找关于因果推断及更广泛的因果主题的其他有见地的文章。我们在本月版中分享的选集内容从入门到更高级别,展示了数据科学和机器学习从业者在工作中每天使用的一些不同方法。
我们希望你喜欢探索这些推荐阅读!一如既往,我们很感激你将 TDS 作为你学习旅程的一部分;如果你希望以其他方式支持我们的工作(并在此过程中获得我们整个档案的访问权限),请考虑成为 Medium 会员。
[TDS 编辑](https://medium.com/u/7e12c71dfa81?source=post_page-----396b2881aea9--------------------------------)
TDS 编辑亮点
-
因果关系的科学与艺术(第一部分)(2023 年 1 月,11 分钟)
Quentin Gallea, PhD对因果推断基础知识的易懂介绍旨在回答两个关键问题:理解因果关系为何如此重要,因果关系为何如此难以评估?
-
使用合成控制的因果推断(2022 年 11 月,9 分钟)
在 A/B 测试不是建立因果关系的好选择的情况下,准实验技术可能是解决方案。diksha tiwari提出合成控制作为一种替代方案。
-
通过回归的因果效应(2023 年 1 月,8 分钟)
Shawhin Talebi已覆盖因果推断的理论与实践超过一年。你可以回到这个系列的起点,或直接跳转到这篇关于回归技术及如何利用它们建立变量之间关系的最新文章。
-
因果推断的事件研究:应该做与不应该做的(2022 年 12 月,17 分钟)
事件研究在准实验背景中是另一种有帮助的方法;正如Nazlı Alagöz在这篇深入说明的文章中指出的,有许多陷阱需要避免,以便我们不从数据中得出错误的见解。
-
使用 SciPy 进行两组独立样本均值的统计显著性检验(2022 年 11 月,8 分钟)
对于那些热衷于动手实验数据的读者,Zolzaya Luvsandorj的教程是理想的起点:它提供了进入假设检验的实用入口,并包括所有所需的 Python 代码。
-
识别:可信因果推断的关键(2023 年 2 月,8 分钟)
正如Murat Unal在一篇深刻的新文章中解释的那样,“没有明确的识别,没有任何复杂的建模或估计能够帮助我们从数据中确定因果关系。” 阅读 Murat 的概述,以更好地理解识别及其重要性。
原创特点
探索我们最新的问答和阅读推荐。
-
“我写作的主要驱动力一直是学习****” 我们与Matteo Courthoud的问答,他反思了离开学术界、对因果推断的兴趣以及公开写作的价值。
-
面对数据偏差依然困难且必要 我们分享了在机器学习和人工智能领域中持续主导对话的话题的核心阅读资料。
-
如何作为数据科学家培养良好习惯 在最近的一次总结中,我们汇总了经验丰富的数据从业者提供的技巧和策略,以便更流畅和可靠的工作流程。
热门文章
如果你错过了,这里是上个月 TDS 中最受欢迎的一些文章。
-
ChatGPT 的工作原理:模型背后的技术 由Molly Ruby撰写
-
你可能在不知不觉中成为高级 Pythonista 的 5 个迹象 由Bex T.撰写
-
使用 OpenAI 和 Python 提升你的简历:一步一步的指南 由Piero Paialunga撰写
-
如何用 Python 构建 ELT 作者:玛丽·陈
-
如何创建有效的自学计划,以成功自学数据科学 作者:马迪逊·亨特
-
你还在使用肘部法则吗? 作者:萨穆埃尔·马赞提
-
如何为你的数据找到最佳理论分布 作者:厄尔多安·塔斯克森
我们很高兴在二月迎来了全新的 TDS 作者团队——他们包括 萨曼莎·霍德、阿尔瓦罗·佩尼亚、泰米托普·索博杜、弗雷德里克·霍特尔、吉尔·肖姆龙、拉斐尔·比绍夫、肖恩·史密斯、布鲁诺·阿尔维西奥、乔里斯·盖林、德米特里·埃柳塞耶夫、科里·贝克、波尔·马林、皮奥特·拉赫特、布鲁诺·波尼 和 诺布尔·阿克森 等。如果你有有趣的项目或想法要与我们分享,我们非常乐意听取你的意见!
下个月见。
以规模化方式掌握语义搜索:使用 FAISS 和 Sentence Transformers 在闪电般的推理时间内索引数百万份文档
深入了解一个高性能的语义搜索引擎的端到端演示,利用 GPU 加速、高效的索引技术和强大的句子编码器处理多达 100 万份文档的数据集,实现 50 毫秒的推理时间
·发表于 Towards Data Science ·阅读时间 15 分钟·2023 年 3 月 31 日
--
介绍
在搜索和信息检索领域,语义搜索已经成为一场变革。它使我们能够根据文档的意义或概念而不仅仅是关键字匹配来进行搜索和检索。与传统的基于关键字的搜索方法相比,语义搜索能够提供更复杂、更相关的结果。然而,挑战在于将语义搜索扩展到处理大量文档的语料库,而不会因分析每个文档的语义内容的计算复杂性而不堪重负。
在这篇文章中,我们迎接了通过利用两种前沿技术来实现可扩展语义搜索的挑战:FAISS 用于高效的语义向量索引,Sentence Transformers 用于将句子编码为这些向量。FAISS 是一个出色的库,旨在快速检索高维空间中的最近邻,使得即使在大规模下也能迅速进行语义最近邻搜索。Sentence Transformers,一个深度学习模型,生成句子的密集向量表示,有效捕捉其语义含义。
本文展示了如何利用 FAISS 和句子变换器的协同作用,构建一个具有卓越性能的可扩展语义搜索引擎。通过将 FAISS 和句子变换器集成,我们可以对来自大量文档的语义向量进行索引,从而实现快速准确的语义搜索体验。我们的方法可以实现新的应用,如上下文化问答和先进的推荐系统,当搜索 1M 文档的语料库时推理时间低至 50 毫秒。我们将指导你实现这一最先进的端到端解决方案,并在基准数据集上展示其性能。
图 1:搜索是你在这个世界上导航所需的一切(来源)
本文属于“大语言模型纪实:导航 NLP 前沿”系列,这是一系列每周更新的文章,将探讨如何利用大模型的力量来完成各种 NLP 任务。通过深入这些前沿技术,我们旨在赋能开发者、研究人员和爱好者,充分利用 NLP 的潜力,开启新的可能性。
迄今已发布的文章:
如往常一样,代码可在我的Github上找到。
句子变换器用于语义编码
深度学习带来了句子变换器的力量,它们制作密集的向量表示,捕捉句子意义的本质。这些模型在大量数据上进行训练,生成上下文化的词嵌入,旨在准确重建输入句子,并将语义相似的句子对拉近。
为了利用句子变换器在语义编码中的潜力,你需要首先选择一个合适的模型架构,如 BERT、RoBERTa 或 XLNet。确定模型后,我们将把文档语料库输入其中,为每个句子生成固定长度的语义向量。这些向量是句子核心主题和话题的紧凑数值表示。
以两个句子为例:“狗追逐猫”和“猫追逐狗”。通过句子变换器处理后,它们的语义向量将紧密相关,即使词序不同,因为基本意义相似。另一方面,像“天空是蓝色的”这样的句子会产生更远的向量,因为其含义不同。
使用句子变换器对整个语料库进行编码,我们得到一组语义向量,这些向量概括了文档的总体含义。为了使这种变换后的表示准备好进行高效检索,我们使用 FAISS 对其进行索引。敬请关注,我们将在下一节中深入探讨这个话题。
FAISS 用于高效索引
FAISS 支持多种索引结构,以优化不同的使用场景。它是一个设计用于在大量向量中快速找到与给定查询向量最接近的匹配项的库。
-
倒排文件(IVF):对相似向量的簇进行索引。适用于中维向量。
-
产品量化(PQ):将向量编码到量化子空间中。适用于高维向量。
-
基于簇的策略:将向量组织成分层的簇集以进行多级搜索。适用于非常大的数据集。
要使用 FAISS 进行语义搜索,我们首先加载我们的向量数据集(来自句子变换器编码的语义向量)并构建 FAISS 索引。我们选择的具体索引结构取决于语义向量的维度和期望的效率等因素。然后,我们通过将语义向量传递到 FAISS 索引中对其进行索引,FAISS 将高效地组织它们以实现快速检索。
对于搜索,我们将新的句子编码成一个语义向量查询并将其传递给 FAISS 索引。FAISS 将检索最接近的语义向量并返回最相似的句子。与线性搜索相比,后者会将查询向量与每个索引向量进行比对,FAISS 能够提供更快的检索时间,通常与索引向量的数量呈对数级缩放。此外,这些索引具有高度的内存效率,因为它们压缩了原始密集向量。
倒排文件索引
FAISS 中的倒排文件(IVF)索引将相似的向量聚集到“倒排文件”中,适用于中维向量(例如,100–1000 维)。每个倒排文件包含相互接近的向量子集。在搜索时,FAISS 仅搜索与查询向量最接近的倒排文件,而不是搜索所有向量,即使有许多向量,也能实现高效搜索。
要构建 IVF 索引,我们需要指定倒排文件(簇)的数量以及每个倒排文件的最大向量数量。然后,FAISS 将每个向量分配给最近的倒排文件,直到没有倒排文件超过最大数量。倒排文件包含代表性点,这些点总结了它们内部的向量。在查询时,FAISS 计算查询向量与每个倒排文件代表性点之间的距离,并仅搜索与查询向量最接近的倒排文件以找到最匹配的向量。
例如,如果我们有 1024 维的图像特征向量并且想要在 100 万个向量中进行快速搜索,我们可以创建一个包含 1024 个倒排文件(簇)的 IVF 索引,每个倒排文件最多包含 1000 个向量。在这种方法中,FAISS 只会搜索与查询最接近的倒排文件,从而比线性搜索更快。
汇总所有内容
在本节中,我们将使用 FAISS 和 Sentence Transformers 构建一个可扩展的语义搜索引擎。我们将展示如何评估这种方法的性能基准,并讨论进一步的改进和应用。
可扩展的语义搜索引擎
为了构建一个可扩展的语义搜索引擎,我们首先初始化ScalableSemanticSearch
类。该类负责使用 Sentence Transformers 对句子进行编码,并使用 FAISS 对其进行索引,以实现高效搜索。它还提供了保存和加载索引、测量时间和内存使用的实用方法。
semantic_search = ScalableSemanticSearch(device="cuda")
接下来,我们使用编码方法对大量文档进行编码,该方法返回一个语义向量的 numpy 数组。该方法还创建了一个索引和句子之间的映射,这在检索前几个结果时会很有用。
embeddings = semantic_search.encode(corpus)
现在,我们使用 build_index 方法构建 FAISS 索引,该方法以嵌入向量作为输入。该方法根据嵌入中的数据点数量创建 IndexIVFPQ 或 IndexFlatL2 索引。
semantic_search.build_index(embeddings)
基于数据集大小选择索引方法
我们定义了两种索引方法:L2 距离的精确搜索和产品量化与 L2 距离的近似搜索。我们还将讨论选择第一种方法用于较小数据集(少于 1500 个文档)和第二种方法用于较大数据集的原因。
1. L2 距离的精确搜索
L2 距离的精确搜索是一种精确搜索方法,它计算查询向量与数据集中每个向量之间的 L2(欧几里得)距离。该方法可以保证找到精确的最近邻,但对于大型数据集可能比较慢,因为它执行线性扫描。
使用案例: 这种方法适用于需要精确最近邻的小型数据集,并且计算成本不是问题。
2. 产品量化与 L2 距离的近似搜索
近似搜索与产品量化和 L2 距离是一种近似最近邻搜索方法,它结合了倒排文件结构、产品量化和 L2 距离,以高效地在大数据集中搜索相似向量。该方法首先使用 k-means(faiss.IndexFlatL2 作为量化器)对数据集进行聚类,然后应用产品量化来压缩残差向量。这种方法比起蛮力方法使用更少的内存,从而实现更快的搜索速度。
使用案例: 这种方法适用于不严格要求精确最近邻的大型数据集,主要关注搜索速度和内存效率。
选择不同方法的原因依据数据集大小
对于包含少于 1500 个文档的数据集,我们设置了 L2 距离的精确搜索方法,因为在这种情况下计算成本不是一个重大问题。此外,这种方法可以保证找到最近邻,这对于较小的数据集是可取的。
对于较大的数据集,我们倾向于使用近似搜索与产品量化和 L2 距离方法,因为它比精确搜索方法更高效,消耗的内存也更少。当优先考虑搜索速度和内存效率而非找到精确的最近邻时,近似搜索方法更适合大数据集。
搜索过程
在构建索引后,我们可以通过提供输入查询和返回的结果数量来执行语义搜索。search
方法计算输入句子与已索引嵌入之间的余弦相似度,并返回最匹配句子的索引和分数。
query = "What is the meaning of life?"
top = 5
top_indices, top_scores = semantic_search.search(query, top)
最后,我们可以使用 get_top_sentences
方法检索最顶级的句子,该方法接受索引到句子的映射和顶级索引作为输入,并返回顶级句子的列表。
top_sentences = ScalableSemanticSearch.get_top_sentences(semantic_search.hashmap_index_sentence, top_indices)
完整模型
我们模型的完整类如下:
class ScalableSemanticSearch:
"""Vector similarity using product quantization with sentence transformers embeddings and cosine similarity."""
def __init__(self, device="cpu"):
self.device = device
self.model = SentenceTransformer(
"sentence-transformers/all-mpnet-base-v2", device=self.device
)
self.dimension = self.model.get_sentence_embedding_dimension()
self.quantizer = None
self.index = None
self.hashmap_index_sentence = None
log_directory = "log"
if not os.path.exists(log_directory):
os.makedirs(log_directory)
log_file_path = os.path.join(log_directory, "scalable_semantic_search.log")
logging.basicConfig(
filename=log_file_path,
level=logging.INFO,
format="%(asctime)s %(levelname)s: %(message)s",
)
logging.info("ScalableSemanticSearch initialized with device: %s", self.device)
@staticmethod
def calculate_clusters(n_data_points: int) -> int:
return max(2, min(n_data_points, int(np.sqrt(n_data_points))))
def encode(self, data: List[str]) -> np.ndarray:
"""Encode input data using sentence transformer model.
Args:
data: List of input sentences.
Returns:
Numpy array of encoded sentences.
"""
embeddings = self.model.encode(data)
self.hashmap_index_sentence = self.index_to_sentence_map(data)
return embeddings.astype("float32")
def build_index(self, embeddings: np.ndarray) -> None:
"""Build the index for FAISS search.
Args:
embeddings: Numpy array of encoded sentences.
"""
n_data_points = len(embeddings)
if (
n_data_points >= 1500
): # Adjust this value based on the minimum number of data points required for IndexIVFPQ
self.quantizer = faiss.IndexFlatL2(self.dimension)
n_clusters = self.calculate_clusters(n_data_points)
self.index = faiss.IndexIVFPQ(
self.quantizer, self.dimension, n_clusters, 8, 4
)
logging.info("IndexIVFPQ created with %d clusters", n_clusters)
else:
self.index = faiss.IndexFlatL2(self.dimension)
logging.info("IndexFlatL2 created")
if isinstance(self.index, faiss.IndexIVFPQ):
self.index.train(embeddings)
self.index.add(embeddings)
logging.info("Index built on device: %s", self.device)
@staticmethod
def index_to_sentence_map(data: List[str]) -> Dict[int, str]:
"""Create a mapping between index and sentence.
Args:
data: List of sentences.
Returns:
Dictionary mapping index to the corresponding sentence.
"""
return {index: sentence for index, sentence in enumerate(data)}
@staticmethod
def get_top_sentences(
index_map: Dict[int, str], top_indices: np.ndarray
) -> List[str]:
"""Get the top sentences based on the indices.
Args:
index_map: Dictionary mapping index to the corresponding sentence.
top_indices: Numpy array of top indices.
Returns:
List of top sentences.
"""
return [index_map[i] for i in top_indices]
def search(self, input_sentence: str, top: int) -> Tuple[np.ndarray, np.ndarray]:
"""Compute cosine similarity between an input sentence and a collection of sentence embeddings.
Args:
input_sentence: The input sentence to compute similarity against.
top: The number of results to return.
Returns:
A tuple containing two numpy arrays. The first array contains the cosine similarities between the input
sentence and the embeddings, ordered in descending order. The second array contains the indices of the
corresponding embeddings in the original array, also ordered by descending similarity.
"""
vectorized_input = self.model.encode(
[input_sentence], device=self.device
).astype("float32")
D, I = self.index.search(vectorized_input, top)
return I[0], 1 - D[0]
def save_index(self, file_path: str) -> None:
"""Save the FAISS index to disk.
Args:
file_path: The path where the index will be saved.
"""
if hasattr(self, "index"):
faiss.write_index(self.index, file_path)
else:
raise AttributeError(
"The index has not been built yet. Build the index using `build_index` method first."
)
def load_index(self, file_path: str) -> None:
"""Load a previously saved FAISS index from disk.
Args:
file_path: The path where the index is stored.
"""
if os.path.exists(file_path):
self.index = faiss.read_index(file_path)
else:
raise FileNotFoundError(f"The specified file '{file_path}' does not exist.")
@staticmethod
def measure_time(func: Callable, *args, **kwargs) -> Tuple[float, Any]:
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
elapsed_time = end_time - start_time
return elapsed_time, result
@staticmethod
def measure_memory_usage() -> float:
process = psutil.Process(os.getpid())
ram = process.memory_info().rss
return ram / (1024**2)
def timed_train(self, data: List[str]) -> Tuple[float, float]:
start_time = time.time()
embeddings = self.encode(data)
self.build_index(embeddings)
end_time = time.time()
elapsed_time = end_time - start_time
memory_usage = self.measure_memory_usage()
logging.info(
"Training time: %.2f seconds on device: %s", elapsed_time, self.device
)
logging.info("Training memory usage: %.2f MB", memory_usage)
return elapsed_time, memory_usage
def timed_infer(self, query: str, top: int) -> Tuple[float, float]:
start_time = time.time()
_, _ = self.search(query, top)
end_time = time.time()
elapsed_time = end_time - start_time
memory_usage = self.measure_memory_usage()
logging.info(
"Inference time: %.2f seconds on device: %s", elapsed_time, self.device
)
logging.info("Inference memory usage: %.2f MB", memory_usage)
return elapsed_time, memory_usage
def timed_load_index(self, file_path: str) -> float:
start_time = time.time()
self.load_index(file_path)
end_time = time.time()
elapsed_time = end_time - start_time
logging.info(
"Index loading time: %.2f seconds on device: %s", elapsed_time, self.device
)
return elapsed_time
端到端演示
本节将提供使用 SemanticSearchDemo 类的可扩展语义搜索引擎的端到端演示。
上述代码中的主函数。目标是理解不同概念和组件如何结合创建一个实用的应用程序。
初始化 SemanticSearchDemo 类:要初始化 SemanticSearchDemo 类,需要提供数据集路径、ScalableSemanticSearch 模型、可选的索引路径和可选的子集大小。这种灵活性使得可以使用不同的数据集、模型和子集大小。
demo = SemanticSearchDemo(
dataset_path, model, index_path=index_path, subset_size=subset_size
)
加载数据:load_data
函数主动读取和处理文件中的数据,然后返回一个句子列表。系统使用这些数据来训练语义搜索模型。
sentences = demo.load_data(file_name)
subset_sentences = sentences[:subset_size]
训练模型:train
函数在数据集上训练语义搜索模型,并返回训练过程的消耗时间和内存使用情况。
training_time, training_memory_usage = demo.train(subset_sentences)
进行推理:infer
函数接受一个查询、一组待搜索的句子和返回的结果数量。它对模型进行推理,并返回最匹配的句子、推理过程的消耗时间和内存使用情况。
top_sentences, inference_time, inference_memory_usage = demo.infer(
query, subset_sentences, top=3
)
演示的完整类如下:
class SemanticSearchDemo:
"""A demo class for semantic search using the ScalableSemanticSearch model."""
def __init__(
self,
dataset_path: str,
model: ScalableSemanticSearch,
index_path: Optional[str] = None,
subset_size: Optional[int] = None,
):
self.dataset_path = dataset_path
self.model = model
self.index_path = index_path
self.subset_size = subset_size
if self.index_path is not None and os.path.exists(self.index_path):
self.loading_time = self.model.timed_load_index(self.index_path)
else:
self.train()
def load_data(self, file_name: str) -> List[str]:
"""Load data from a file.
Args:
file_name: The name of the file containing the data.
Returns:
A list of sentences loaded from the file.
"""
with open(f"{self.dataset_path}/{file_name}", "r") as f:
reader = csv.reader(f, delimiter="\t")
next(reader) # Skip the header
sentences = [row[3] for row in reader] # Extract the sentences
return sentences
def train(self, data: Optional[List[str]] = None) -> Tuple[float, float]:
"""Train the semantic search model and measure time and memory usage.
Args:
data: A list of sentences to train the model on. If not provided, the data is loaded from file.
Returns:
A tuple containing the elapsed time in seconds and the memory usage in megabytes.
"""
if data is None:
file_name = "GenericsKB-Best.tsv"
data = self.load_data(file_name)
if self.subset_size is not None:
data = data[: self.subset_size]
elapsed_time, memory_usage = self.model.timed_train(data)
if self.index_path is not None:
self.model.save_index(self.index_path)
return elapsed_time, memory_usage
def infer(
self, query: str, data: List[str], top: int
) -> Tuple[List[str], float, float]:
"""Perform inference on the semantic search model and measure time and memory usage.
Args:
query: The input query to search for.
data: A list of sentences to search in.
top: The number of top results to return.
Returns:
A tuple containing the list of top sentences that match the input query, elapsed time in seconds, and memory usage in megabytes.
"""
elapsed_time, memory_usage = self.model.timed_infer(query, top)
top_indices, _ = self.model.search(query, top)
index_map = self.model.index_to_sentence_map(data)
top_sentences = self.model.get_top_sentences(index_map, top_indices)
return top_sentences, elapsed_time, memory_usage
我们可扩展语义搜索引擎的性能评估
为了评估我们可扩展的语义搜索引擎的性能,我们可以测量各种操作的时间和内存使用情况,如训练、推理和加载索引。ScalableSemanticSearch
类提供 timed_train
、timed_infer
和 timed_load_index
方法来测量这些基准。
train_time, train_memory = semantic_search.timed_train(corpus)
infer_time, infer_memory = semantic_search.timed_infer(query, top)
关于执行时间和内存使用情况的训练和推理性能图表如下。我们将讨论和解释这些结果,基于我们使用的语料库大小选择的算法。
图 2:不同数据集大小的训练时间和内存使用情况
图 3:不同数据集大小的推断时间和内存使用情况
使用 L2 距离的精确搜索
使用 L2 距离的精确搜索是一种穷举搜索方法,它执行线性扫描以找到最近邻。
理论复杂度
-
时间复杂度:O(n) — 因为它需要将查询向量与数据集中每个向量进行比较。
-
内存复杂度:O(n) — 它存储数据集中的所有向量。
观察到的复杂度
- 从上述图表中可以观察到,对于少于 1500 个文档,训练时间和内存使用量都随着文档数量线性增加,这与预期的理论复杂度相符。
使用产品量化和 L2 距离的近似搜索
使用产品量化和 L2 距离的近似搜索方法是一种近似最近邻搜索方法,它采用产品量化和倒排文件结构来提高效率。聚类数量(k)是此方法中的一个重要因素,计算公式为:max(2, min(n_data_points, int(np.sqrt(n_data_points))))。
简单来说,这个公式确保了:
-
至少有 2 个聚类,提供了最低水平的分区。
-
聚类的数量不会超过数据点的数量。
-
作为启发式方法,使用数据点数量的平方根来平衡搜索精度和计算效率。
理论复杂度
-
时间复杂度(训练):O(n * k) — k-means 聚类算法在训练阶段的复杂度。
-
内存复杂度(训练):O(n + k) — 它存储聚类的质心和残差码。
-
时间复杂度(推断):O(k + m) — 其中 m 是要搜索的最近聚类的数量。由于层次结构和近似,它比线性搜索更快。
-
内存复杂度(推断):O(n + k) — 需要存储倒排文件和质心。
观察到的复杂度
从上述图表中可以观察到,对于超过 1500 个文档:
-
训练时间复杂度:增长速度快于线性,这与预期的理论复杂度 O(n * k) 相符,因为 k 随着数据点(n)的增加而增长。
-
训练内存复杂度:内存使用量随着文档数量的增加呈非线性增长,这与预期的理论复杂度 O(n + k) 相符。
-
推断时间复杂度:执行时间几乎保持不变,与期望的理论复杂度 O(k + m) 一致,因为 m 通常远小于 n。
-
推断内存复杂度:内存使用量随着文档数量线性增加,这与预期的理论复杂度 O(n + k) 相符。
我们还可以通过将搜索引擎的前几个结果与手动整理的真实结果集进行比较,来评估搜索引擎的准确性和召回率。我们可以通过迭代各种查询并比较结果来计算整个数据集的平均准确性和召回率。
结论
展示的方法突显了使用 FAISS 和 Sentence Transformers 进行语义搜索的可扩展性,同时揭示了改进机会。例如,集成先进的 Transformer 模型来对句子进行编码,或测试替代的 FAISS 配置,可能会加快搜索过程。此外,研究最先进的模型,如 GPT-4 或 BERT 变体,可能会提高语义搜索任务的性能和准确性。
可扩展的语义搜索引擎的几个潜在应用包括:
-
在广泛的知识库中检索文档
-
在自动化系统中回答问题
-
提供个性化推荐
-
生成聊天机器人回应
利用 FAISS 和 Sentence Transformers,我们开发了一种可扩展的语义搜索引擎,能够高效处理数十亿份文档并提供准确的搜索结果。这种创新的方法可能会显著影响语义搜索的未来以及其在各个行业和应用中的影响。
随着数字数据的增长,对高效且准确的语义搜索引擎的需求变得越来越重要。基于 FAISS 和 Sentence Transformers,可扩展的语义搜索引擎为克服这些挑战奠定了坚实的基础,并革新了我们搜索和获取相关信息的方式。
未来的发展涉及整合更先进的自然语言处理和机器学习技术,以增强搜索引擎的能力。这些改进可能包括用于更好理解上下文、意图以及查询词和短语之间关系的无监督学习方法,以及处理语言使用中的歧义和变体的技术。
关于我
连续创业者和 AI 领域的领军人物。我为企业开发 AI 产品,并投资于 AI 相关的初创公司。
创始人 @ ZAAI | LinkedIn | X/Twitter
使用 Versatile Data Kit (VDK) 掌握批量数据处理
数据管理
使用 VDK 进行批量数据处理的教程
·
关注 发布于 Towards Data Science ·5 分钟阅读·2023 年 11 月 17 日
--
图片由 Mika Baumeister 提供,来源于 Unsplash
Versatile Data Kit (VDK) 是一个开源的数据摄取和处理框架,旨在简化数据管理的复杂性。虽然 VDK 能够处理各种数据集成任务,包括实时流处理,本文将重点介绍如何在批量数据处理中的应用。
本文涵盖:
-
介绍批量数据处理
-
在 VDK 中创建和管理批量处理管道
-
监控 VDK 中的批量数据处理
1 介绍批量数据处理
批量数据处理是一种在指定时间间隔处理大量数据的方法。批量数据必须是:
-
时间独立:数据不需要立即处理,通常不受实时要求的限制。与需要即时处理的流数据不同,批量数据可以在计划的时间间隔或资源可用时处理。
-
可分块处理:批量数据可以分成较小、更易于管理的段,而不是在单次资源密集型操作中处理整个数据集。这些段可以根据数据处理系统的能力,顺序或并行处理。
此外,批量数据可以离线处理,这意味着它不需要持续连接到数据源或外部服务。当数据源可能是间歇性的或暂时不可用时,这一特性尤为宝贵。
ELT(提取、加载、转换)是批量数据处理的典型用例。ELT 包括三个主要阶段:
-
提取 (E):数据从多个不同格式的来源中提取,包括结构化和非结构化的数据。
-
加载 (L):数据被加载到目标位置,例如数据仓库。
-
转换 (T):提取的数据通常需要初步处理,例如清理、协调和转换为统一格式。
现在你已经了解了什么是批量数据处理,让我们进入下一步:在 VDK 中创建和管理批量处理管道。
2 在 VDK 中创建和管理批量处理管道
VDK 采用组件化的方法,允许你快速构建数据处理管道。有关 VDK 的介绍,请参见我之前的文章 多功能数据工具概述。本文假设你已经在计算机上安装了 VDK。
为了说明 VDK 中的批量处理管道是如何工作的,我们考虑一个需要执行 ELT 任务的场景。
想象一下,你想在 VDK 中导入和处理 欧洲数字图书馆 上提供的文森特·梵高的画作,欧洲数字图书馆是一个著名的欧洲文化遗产聚合器。欧洲数字图书馆通过其公共 REST API 提供所有文化遗产对象。关于文森特·梵高,欧洲数字图书馆提供了超过 700 件作品。
下图展示了在此场景下批量数据处理的步骤。
作者提供的图片
让我们逐点探讨。你可以在 VDK GitHub 仓库 中找到实现此场景的完整代码。
2.1 提取和加载
该阶段包括 VDK 作业调用 Europeana REST API 提取原始数据。具体而言,它定义了三个作业:
-
job1 — 删除现有的表(如果有的话)
-
job2 — 创建一个新表
-
job3 — 直接从 REST API 导入表格值。
此示例需要一个有效的互联网连接才能正确工作,以访问 Europeana REST API。此操作是一个批处理过程,因为它仅下载一次数据,不需要流式处理。
我们将把提取的数据存储在一个表中。这个任务的难点在于构建 REST API 之间的映射,这在 job3 中完成。
编写 job3 仅涉及编写 Python 代码来执行此映射,但不是将提取的文件保存到本地文件中,而是调用 VDK 函数(job_input.send_tabular_data_for_ingestion
)将文件保存到 VDK,如下代码片段所示:
import inspect
import logging
import os
import pandas as pd
import requests
from vdk.api.job_input import IJobInput
def run(job_input: IJobInput):
"""
Download datasets required by the scenario and put them in the data lake.
"""
log.info(f"Starting job step {__name__}")
api_key = job_input.get_property("api_key")
start = 1
rows = 100
basic_url = f"https://api.europeana.eu/record/v2/search.json?wskey={api_key}&query=who:%22Vincent%20Van%20Gogh%22"
url = f"{basic_url}&rows={rows}&start={start}"
response = requests.get(url)
response.raise_for_status()
payload = response.json()
n_items = int(payload["totalResults"])
while start < n_items:
if start > n_items - rows:
rows = n_items - start + 1
url = f"{basic_url}&rows={rows}&start={start}"
response = requests.get(url)
response.raise_for_status()
payload = response.json()["items"]
df = pd.DataFrame(payload)
job_input.send_tabular_data_for_ingestion(
df.itertuples(index=False),
destination_table="assets",
column_names=df.columns.tolist(),
)
start = start + rows
有关完整代码,请参见 GitHub 示例。请注意,你需要一个免费的 API 密钥来从 Europeana 下载数据。
在提取阶段产生的输出是一个包含原始值的表格。
2.2 转换
该阶段涉及清理数据并提取相关信息。我们可以通过两个作业在 VDK 中实现相关工作:
-
job4 — 删除现有的表(如果有的话)
-
job5 — 创建清理后的表。
Job5 仅涉及编写 SQL 查询,如下代码片段所示:
CREATE TABLE cleaned_assets AS (
SELECT
SUBSTRING(country, 3, LENGTH(country)-4) AS country,
SUBSTRING(edmPreview, 3, LENGTH(edmPreview)-4) AS edmPreview,
SUBSTRING(provider, 3, LENGTH(provider)-4) AS provider,
SUBSTRING(title, 3, LENGTH(title)-4) AS title,
SUBSTRING(rights, 3, LENGTH(rights)-4) AS rights
FROM assets
)
在 VDK 中运行此作业将生成另一个名为 cleaned_asset
的表,包含处理后的值。最后,我们准备在某个地方使用清理后的数据。在我们的例子中,我们可以构建一个显示提取画作的 Web 应用程序。你可以在 VDK GitHub 仓库 中找到执行此任务的完整代码。
3 在 VDK 中监控批量数据处理
VDK 提供了 VDK UI,一个用于监控数据作业的图形用户界面。要安装 VDK UI,请按照 此链接 中的官方 VDK 视频操作。下图显示了 VDK UI 的快照。
作者提供的图片
主要有两个页面:
-
探索:此页面使你能够探索数据作业,例如作业执行成功率、过去 24 小时内失败的作业以及过去 24 小时内失败次数最多的作业。
-
管理:此页面提供更多工作细节。你可以按列排序工作,搜索多个参数,通过某些列进行过滤,查看特定工作的来源,添加其他列等等。
观看以下官方 VDK 视频,了解如何使用 VDK UI。
总结
恭喜!你刚刚学会了如何在 VDK 中实现批量数据处理!这只需要接收原始数据,进行处理,然后最终将其用于你的目的!你可以在VDK GitHub 仓库中找到许多其他示例。
关注VDK的最新数据处理进展和最佳实践。不断探索和完善你的专业技能!
其他你可能感兴趣的文章……
开始使用多功能数据工具包,这是一个让数据工程师工作更高效的框架
如何使用 VDK 构建数据管道以处理缺失值的教程
在多功能数据工具包中处理缺失值 [## 从原始数据到清洗数据库:深入探索多功能数据工具包
使用多功能数据工具包(VMware 最近发布的框架)和 Trino DB 的完整示例
使用 Hydra 精通机器学习中的配置管理
原文:
towardsdatascience.com/mastering-configuration-ml-with-hydra-ef138f1c1852
精通机器学习
深入实际案例以改进你的 ML 应用中的配置管理
·发表于 Towards Data Science ·18 分钟阅读·2023 年 6 月 15 日
--
概述
欢迎来到“使用 Hydra 精通机器学习中的配置管理”! 本教程旨在从 Hydra 的基础知识带你深入了解如何在 ML 项目中管理配置的高级技术。我们还将探讨 Hydra 与高性能计算环境和流行机器学习框架的集成。无论你是机器学习新手还是经验丰富的从业者,本教程将为你提供超充机器学习工作流所需的知识和技能。
由作者创建的图。
目录
· I. 介绍
· II. Hydra 基础
∘ Hydra 的安装
∘ Hydra 应用程序的结构
∘ 理解 Hydra 的主要组件
· III. 层次配置
∘ 定义和理解层次配置文件
· IV. 配置组
∘ 理解配置组的概念
∘ 定义不同的设置:开发、预发布、生产
∘ 展示对可重复性和调试的影响
· V. 动态配置
∘ 动态配置的解释
∘ 为超参数动态调整创建规则
∘ 在机器学习环境中实现动态配置
· VI. 环境变量
∘ Hydra 中环境变量的必要性
∘ 处理敏感或频繁变化的数据
∘ 在 Hydra 中使用环境变量:逐步指南
· VII. 配置日志
∘ 机器学习实验中日志记录的重要性
∘ 使用 Hydra 配置 Python 的日志框架
∘ 如何为不同模块创建具有不同详细级别的日志文件
· VIII. 多次运行和搜索
∘ Hydra 的多次运行功能介绍
∘ 设计和配置超参数搜索
∘ 将多次运行和搜索应用于机器学习项目
· IX. 错误处理
∘ 配置管理中错误处理的重要性
∘ 使用 Hydra 进行高级错误处理
∘ 为缺失或不正确的配置自定义行为
· X. 命令行覆盖
∘ 理解 Hydra 中的命令行覆盖
∘ 使用命令行参数在运行时修改配置
∘ 在机器学习实验中使用命令行覆盖的实用示例
· XI. 在基于 Slurm 的 HPC 集群上使用 Hydra
∘ Hydra 与 SLURM:简要概述
∘ 安装
∘ 配置
∘ 运行你的应用程序
∘ 高级主题:使用 Slurm 进行并行运行
· XII. Hydra 与容器化(Docker/Kubernetes)
∘ Hydra 与 Docker
∘ Hydra 与 Kubernetes
· XIII. 与 ML 框架的集成
∘ Hydra 与 PyTorch
· XIV. 结论
· XV. 附录:有用的 Hydra 命令和技巧
∘ 常用的 Hydra 命令
∘ 提示和技巧
I. 介绍
配置管理可能很复杂,从模型超参数到实验设置。跟踪所有这些细节可能很快变得令人不知所措。这就是 Facebook 的 Hydra 配置库派上用场的地方。Hydra 是一个开源的 Python 框架,简化了应用程序中的配置管理,确保更好的可重复性和模块化。
Hydra 提供了一种强大而灵活的机制来管理复杂应用程序的配置。这使得开发人员和研究人员更容易维护和优化机器学习项目。
在本教程中,我们介绍 Hydra 的基础知识,并引导你深入了解其高级功能。在本教程结束时,你将能够有效且高效地管理你的项目配置。
II. Hydra 基础
Hydra 的安装
Hydra 是一个 Python 库,可以通过 pip 轻松安装:
pip install hydra-core
Hydra 应用程序的结构
一个 Hydra 应用程序有一个脚本和一个或多个配置文件。配置文件使用 YAML 编写,并存储在目录结构中。这创建了一个层次结构的配置。
# my_app.py
import hydra
@hydra.main(config_name="config")
def my_app(cfg):
print(cfg.pretty())
if __name__ == "__main__":
my_app()
附带的 YAML 文件可能如下所示:
# config.yaml
db:
driver: mysql
user: test
password: test
Python 脚本 my_app.py
使用 @hydra.main()
装饰器来表示它是一个 Hydra 应用程序。config_name
参数指定要使用的配置文件。请注意,它假定文件类型是 YAML,因此无需选择扩展名。
理解 Hydra 的主要组件
Hydra 包含 配置、插值 和 覆盖。
配置是您应用程序的设置,这些设置在一个或多个 YAML 文件中指定。
插值是对配置其他部分的引用。例如,在下面的 YAML 文件中,full
的值插值了 name
和 surname
。
name: John
surname: Doe
full: ${name} ${surname}
db:
user: ${surname}.${name}
覆盖 允许您在运行时修改配置,而无需更改 YAML 文件。您可以在运行应用程序时在命令行中指定覆盖,如下所示:
python my_app.py db.user=root
在上述命令中,我们正在覆盖配置中 db
下的 user
值。
比较使用 Hydra 管理配置与不使用 Hydra 管理配置的情况。表格由作者创建。
在接下来的部分中,我们将探讨高级功能以及如何在您的 ML 项目中使用它们。
III. 层次结构配置
Hydra 提供了一种直观的方式来分层组织配置文件,反映您项目的目录结构。层次结构配置在管理复杂项目时非常重要,使得创建、维护、扩展和重用您的配置变得更加容易。
定义和理解层次结构配置文件
配置的层级结构由配置文件的目录结构定义。
例如,一个项目的布局可以如下结构:
config.yaml
preprocessing/
- standard.yaml
- minmax.yaml
model/
- linear.yaml
- svm.yaml
因此,standard.yaml
和 minmax.yaml
文件可能包含不同的数据预处理设置;linear.yaml
和 svm.yaml
文件可能包含各种模型类型的配置。
在 config.yaml
中,您可以指定默认使用哪些预处理和模型配置:
defaults:
- preprocessing: standard
- model: linear
Hydra 自动合并指定的配置,因此在启动应用程序时,您仍然可以覆盖默认选择,如以下代码片段所示:
python my_app.py preprocessing=minmax model=svm
上述命令以 minmax
预处理和 svm
模型配置运行应用程序。
IV. 配置组
Hydra 中的配置组提供了一种管理可轻松交换的配置集的方式。此功能对于维护各种设置、环境和配置(如开发、测试、预发布和生产)非常有用。
理解配置组的概念
配置组是一个包含备选配置的目录。在定义配置组时,在您的主要配置文件 (config.yaml
) 中指定默认配置,但在运行应用程序时,您可以轻松覆盖它。
定义不同的设置:开发、测试、生产
考虑一个机器学习项目,其中您有不同的开发、测试和生产环境设置。您可以为每个环境创建一个配置组:
config.yaml
env/
- development.yaml
- staging.yaml
- production.yaml
env
目录中的每个 YAML 文件都包含特定环境的设置。例如,development.yaml
文件可能定义了详细的日志记录和调试设置,而 production.yaml
文件可能包含优化的性能和错误日志记录设置。
在 config.yaml
中,你可以指定默认环境:
defaults:
- env: development
使用此配置,Hydra 在运行应用程序时将自动应用 development.yaml
中的设置。
展示对可重复性和调试的影响
配置组是增强项目可重复性的强大工具。通过定义特定的开发、预发布和生产设置,你可以确保应用程序在不同环境中的行为一致。
此外,配置组可以显著简化调试。你可以通过为项目的不同阶段使用不同的配置组来快速重现和隔离问题。例如,如果在预发布环境中出现问题,你可以切换到 staging
配置以重现问题,而不会影响开发或生产设置。
在启动应用程序时,切换环境就像指定不同的配置组一样简单:
python my_app.py env=production
此命令使用production.yaml
中定义的设置运行应用程序。
使用配置组的好处。表格由作者创建。
V. 动态配置
除了静态配置管理外,Hydra 还允许动态配置。动态配置在一些参数依赖于其他参数或必须在运行时计算的场景中非常有价值。
动态配置的解释
Hydra 中的动态配置通过两个主要特性实现:插值 和 OmegaConf 库。
插值是对配置中其他部分的引用,允许动态设置值。在配置文件中用 ${}
表示。例如:
name: Alice
greeting: Hello, ${name}!
在这个示例中,greeting
的值将动态包含 name
的值。
OmegaConf 是一个灵活的配置库,Hydra 使用它。它不仅支持插值,还支持变量替换甚至复杂表达式:
dimensions:
width: 10
height: 20
area: ${dimensions.width} * ${dimensions.height}
在上述示例中,area
是基于 dimensions
下的 width
和 height
动态计算的。
为超参数的动态调整创建规则
在机器学习中,动态配置对调整超参数非常有用。例如,我们希望学习率依赖于批量大小。我们可以在配置文件中为此定义一个规则:
training:
batch_size: 32
learning_rate: 0.001 * ${training.batch_size}
其中 learning_rate
根据 batch_size
动态调整,如果你提高了批量大小,学习率将自动按比例增加。
在机器学习环境中实现动态配置
让我们考虑一个更复杂的机器学习场景,其中神经网络的第一层的大小取决于数据的输入大小。
data:
input_size: 100
model:
layer1: ${data.input_size} * 2
layer2: 50
在这里,第一层(layer1
)的大小被动态设置为 input_size
的两倍。如果我们更改 input_size
,layer1
将自动调整。
动态配置为应用程序提供了更高的灵活性和适应性。
动态配置的优势。表格由作者创建。
VI. 环境变量
Hydra 支持在配置文件中使用环境变量,提供额外的灵活性和安全性。这一功能对于处理敏感数据或经常变化的数据尤为有用。
Hydra 中对环境变量的需求
环境变量是将配置信息传递给应用程序的常用方式。在以下情况下它们非常有用:
-
敏感数据:密码、密钥和访问令牌不应硬编码到应用程序或配置文件中。这些可以安全地作为环境变量进行存储。
-
经常变化的数据:如果特定参数经常变化或依赖于系统环境(例如,开发和生产环境之间不同的文件路径),将其管理为环境变量更为方便。
-
可移植性和可扩展性:环境变量可以使你的应用程序更容易在不同环境之间移动(例如,从本地开发环境到基于云的生产环境)。
处理敏感或经常变化的数据
诸如数据库凭据之类的敏感信息不应直接存储在配置文件中。相反,你可以将这些信息保存在环境变量中,并通过插值在 Hydra 配置中引用它们。这种做法通过防止敏感数据在代码或版本控制系统中暴露来增强安全性。
同样,经常变化的数据,例如在不同环境之间变化的文件或目录路径,也可以作为环境变量进行管理。这种方法减少了在不同环境之间移动时的手动修改需求。
使用环境变量:逐步指南
要在 Hydra 中使用环境变量,请按照以下步骤操作:
- 在你的 shell 中定义一个环境变量。例如,在基于 Unix 的系统中,你可以使用
export
命令:
export DATABASE_URL=mysql://user:password@localhost/db
2. 使用 ${env:VARIABLE}
语法在你的 Hydra 配置文件中引用环境变量:
database:
url: ${env:DATABASE_URL}
在这个例子中,database
配置中的 url
字段将设置为 DATABASE_URL
环境变量的值。
记住,切勿将敏感信息直接存储在配置文件或代码中。始终使用环境变量或其他安全方法来处理敏感数据。
使用环境变量的好处。表格由作者创建。
VII. 配置日志记录
日志记录是机器学习实验的重要组成部分。它提供了对模型和算法性能及行为随时间变化的可视化。配置适当的日志记录机制有助于模型调试、优化和理解学习过程。
Hydra 内置了对 Python 日志模块的支持,使得控制日志的详细程度、设置不同的处理程序以及格式化日志消息变得容易。
机器学习实验中日志记录的重要性
机器学习的日志记录可以服务于多种目的:
-
模型调试:日志可以包含有关模型行为的宝贵信息,这有助于诊断和修复问题。
-
性能跟踪:记录随时间变化的指标有助于观察模型的学习过程,检测过拟合或欠拟合,并相应调整超参数。
-
审计和可重现性:日志记录了训练过程的详细信息,使得重现结果和了解过去的工作变得更容易。
使用 Hydra 配置 Python 的日志框架
Python 的内置日志模块功能强大且高度可配置,Hydra 可以帮助管理这一复杂性。
要使用 Hydra 配置日志记录,请在配置目录中创建一个hydra.yaml
文件,并在hydra.job_logging
键下定义你的日志设置:
hydra:
job_logging:
root:
level: INFO
handlers:
console:
level: INFO
formatter: basic
file:
level: DEBUG
formatter: basic
filename: ./logs/${hydra:job.name}.log
在此配置中:
-
根日志记录器设置为
INFO
级别,捕获INFO
、WARNING
、ERROR
和CRITICAL
消息 -
有两个处理程序:一个用于控制台输出,一个用于写入文件。控制台处理程序仅记录
INFO
及更高级别的消息,而文件处理程序记录DEBUG
及更高级别的消息。 -
文件处理程序的
filename
使用插值动态创建每个作业的日志文件,基于作业的名称。
如何为不同模块创建具有不同详细程度的日志文件
你可以为应用程序中的不同模块设置不同的日志级别。假设你有moduleA
和moduleB
模块,并且希望moduleA
记录DEBUG
及更高级别的消息,而moduleB
只记录ERROR
及更高级别的消息。以下是配置方法:
hydra:
job_logging:
root:
level: INFO
loggers:
moduleA:
level: DEBUG
moduleB:
level: ERROR
handlers:
console:
level: INFO
formatter: basic
file:
level: DEBUG
formatter: basic
filename: ./logs/${hydra:job.name}.log
这样,你可以控制来自不同应用程序部分的日志输出量。
使用 Hydra 配置日志记录的关键好处。表格由作者创建。
VIII. 多次运行和扫描
机器学习通常涉及使用不同的超参数集合运行实验以找到最佳解决方案。欢迎使用 Hydra 的multirun
功能。它允许你使用不同的配置多次运行应用程序,这对超参数调整非常有帮助。
Hydra 的多次运行功能简介
要使用 multirun
,在运行应用程序时传递 -m
或 --multirun
标志。然后,使用 key=value
语法指定你想在运行中变化的参数:
python my_app.py --multirun training.batch_size=32,64,128
这将运行你的应用程序三次:一次 training.batch_size=32
,一次 training.batch_size=64
,以及一次 training.batch_size=128
。
设计和配置超参数测试
超参数测试是一系列不同超参数的运行。
Hydra 支持不同类型的测试:
-
范围测试:指定参数的值范围。例如,
learning_rate=0.01,0.001,0.0001
-
区间测试:定义一个区间和步长。例如,
epoch=1:10:1
(start:end:step
) -
选择测试:定义一个值列表供选择。例如,
optimizer=adam,sgd,rmsprop
-
网格测试:定义多个参数进行测试。这将对所有参数组合运行你的应用程序。
这些测试类型可以组合使用,以复杂的方式全面探索你模型的超参数空间。
将 Multirun 和测试应用于机器学习项目
让我们考虑一个简单的机器学习项目,你想调整学习率和批量大小。你可以使用multirun
功能轻松配置和运行这些超参数的测试:
python my_app.py --multirun training.batch_size=32,64,128 training.learning_rate=0.01,0.001,0.0001
这个命令将为每个批量大小和学习率组合运行你的应用程序,总共九次运行(3 个批量大小 * 3 个学习率)。
Hydra 的 multirun
功能可以显著简化运行超参数测试的过程,帮助你找到最佳的机器学习模型配置。
使用 Hydra 的 Multirun 功能的好处。作者创建了表格。
IX. 错误处理
适当的错误处理是配置管理中的一个关键方面。它在出现问题时提供宝贵的信息,帮助防止或迅速诊断可能影响你机器学习项目成功的问题。Hydra 可以用于促进高级错误处理。
错误处理在配置管理中的重要性
配置管理中的错误处理有多种用途:
-
错误预防:通过在使用之前验证配置,你可以早期捕捉和纠正错误,防止它们引发更大的问题。
-
快速调试:当错误发生时,详细的错误信息可以帮助你快速识别原因并解决问题。
-
健壮性:全面的错误处理使你的代码更加健壮和可靠,提高其处理意外情况的能力。
使用 Hydra 进行高级错误处理
Hydra 提供了多种高级错误处理功能:
- 严格验证:Hydra 默认会对你的配置进行严格验证。如果你尝试访问一个在配置中未定义的字段,Hydra 会抛出错误。这有助于及早发现拼写错误或遗漏字段。
from omegaconf import OmegaConf
import hydra
@hydra.main(config_path="conf", config_name="config")
def my_app(cfg):
print(cfg.field_that_does_not_exist) # Raises an error
if __name__ == "__main__":
my_app()
- 错误信息:详细的错误信息,当错误发生时。这些信息通常包括配置中错误的确切位置,使得诊断和修复问题更加容易。
自定义缺失或不正确配置的行为
虽然 Hydra 的默认行为是对于缺失或不正确的配置抛出错误,你可以根据需要自定义此行为。例如:
- 可选字段:你可以使用
OmegaConf.select
方法以一种不会在字段缺失时抛出错误的方式访问字段:
value = OmegaConf.select(cfg, "field_that_may_or_may_not_exist", default="default_value")
- 忽略无效类型:如果你从文件中加载配置,并且希望 Hydra 忽略无效类型的字段,可以在调用
OmegaConf.load
时设置ignore_invalid_types
标志:
cfg = OmegaConf.load("config.yaml", ignore_invalid_types=True)
通过利用 Hydra 的错误处理能力,你可以使配置管理过程更加稳健和易于调试。
X. 命令行覆盖
命令行覆盖是一个强大的功能,允许你修改运行时配置。这在机器学习实验中尤其有用,你通常需要调整超参数、在不同模型之间切换或更改数据集。
理解命令行覆盖
你可以从命令行覆盖配置的任何部分。为此,在运行应用程序时传递一个key=value
对:
python my_app.py db.driver=postgresql db.user=my_user
通过这种方式,你的应用程序将db.driver
设置为postgresq
,并将db.user
设置为my_user
,覆盖配置文件或默认值中定义的任何值。
使用命令行参数在运行时修改配置
命令行覆盖可以以各种方式修改配置:
-
更改单一值:如前面的示例所示,你可以更改配置中单一字段的值。
-
更改嵌套值:你也可以使用点表示法更改嵌套字段的值:
python my_app.py training.optimizer.lr=0.01
-
添加新字段:如果你指定一个在配置中不存在的字段,Hydra 将添加它:
python my_app.py new_field=new_value
-
移除字段:你可以通过将字段设置为
null
来从配置中移除该字段:python my_app.py field_to_remove=null
-
更改列表:你可以更改列表字段的值:
python my_app.py data.transforms=[transform1,transform2]
机器学习实验中使用命令行覆盖的实际示例
命令行覆盖在机器学习中尤其有用,因为你通常需要为不同的实验调整配置:
-
超参数调整:轻松调整不同运行的超参数:
python train.py model.lr=0.01 model.batch_size=64
-
模型选择:在不同模型之间切换:
python train.py model.type=resnet50
-
数据选择:更改用于训练的数据集或拆分:
python train.py data.dataset=cifar10 data.split=train
使用命令行覆盖可以大大提高你的机器学习实验的灵活性和便利性。
XI. 在基于 Slurm 的 HPC 集群上使用 Hydra
高性能计算(HPC)集群通常用于处理大规模的机器学习任务。这些集群通常使用简单的 Linux 资源管理工具(Slurm)来管理作业调度。让我们看看如何在基于 Slurm 的 HPC 集群上使用 Hydra。
Hydra 与 SLURM:简要概述
Hydra 包含一个名为 hydra-submitit-launcher
的插件,这使得与 Slurm 作业调度的无缝集成成为可能。通过这个插件,你可以将 Hydra 应用程序作为 Slurm 作业提交,从而利用 HPC 集群的强大计算能力进行机器学习实验。
安装
要将 Submitit 启动器与 Hydra 一起使用,你首先需要安装它:
pip install hydra-submitit-launcher
配置
一旦安装了启动器,你可以在 Hydra 配置文件中进行配置。这里是一个示例配置:
defaults:
- hydra/launcher: submitit_slurm
hydra:
launcher:
_target_: hydra_plugins.hydra_submitit_launcher.config.SubmitterConf
slurm:
time: 60
nodes: 1
gpus_per_node: 2
tasks_per_node: 1
mem_per_node: 10GB
cpus_per_task: 10
submitit_folder: /path/to/your/log/folder
上述中,我们将作业的时间限制设置为 60 分钟,使用一个节点,配备 2 个 GPU,每个任务分配 10GB 的内存和 10 个 CPU。根据集群中可用的资源调整这些设置。
运行你的应用程序
你现在可以像往常一样运行你的 Hydra 应用程序:
python my_app.py
配置了 Submitit 启动器后,Hydra 可以提交 Slurm 作业。
高级主题:与 Slurm 的并行运行
Hydra 的多运行特性和 Submitit 启动器允许你并行运行多个作业。例如,你可以在多个 Slurm 节点上执行超参数搜索:
python my_app.py --multirun model.lr=0.01,0.001,0.0001
这将提交三个 Slurm 作业,每个作业具有不同的学习率。
进一步阅读:
[## Submitit 启动器插件 | Hydra
PyPI
关于使用 Slurm 的一般信息:
[## Slurm 作业管理器
注意:本文档适用于 Slurm 版本 23.02。旧版本的 Slurm 文档会随…
slurm.schedmd.com](https://slurm.schedmd.com/documentation.html?source=post_page-----ef138f1c1852--------------------------------)
XII. 使用容器化的 Hydra(Docker/Kubernetes)
使用 Docker 和 Kubernetes 等工具进行容器化在机器学习中广泛使用,因为它具有一致性、可重复性和可扩展性。这一部分将指导你如何将 Hydra 与 Docker 或 Kubernetes 配合使用,展示如何根据配置动态生成 Dockerfile 或 Kubernetes 清单。
Hydra 与 Docker
使用 Docker 时,你通常需要创建具有不同配置的 Dockerfile。Hydra 可以简化这个过程:
1. Dockerfile
创建一个带有配置选项占位符的 Dockerfile。这里是一个简化的示例:
FROM python:3.8
WORKDIR /appCOPY . .RUN pip install -r requirements.txtCMD ["python", "my_app.py", "${CMD_ARGS}"]
在这个 Dockerfile 中,${CMD_ARGS}
是一个占位符,Hydra 将提供命令行参数。
2. Hydra 配置
在你的 Hydra 配置文件中,定义要传递给 Docker 的配置选项。例如:
docker:
image: python:3.8
cmd_args: db.driver=postgresql db.user=my_user
3. Docker 运行脚本
最后,创建一个使用 Hydra 生成 Docker 运行命令的脚本:
@hydra.main(config_path="config.yaml")
def main(cfg):
cmd = f'docker run -it {cfg.docker.image} python my_app.py {cfg.docker.cmd_args}'
os.system(cmd)
if __name__ == "__main__":
main()
运行此脚本,Hydra 将启动一个具有你指定配置选项的 Docker 容器。
Hydra 与 Kubernetes
使用 Hydra 与 Kubernetes 集成要复杂一些,但基本思想类似。首先,你需要创建一个包含配置选项占位符的 Kubernetes 清单,然后使用 Hydra 生成 Kubernetes 应用命令。
考虑使用 Hydra-KubeExecutor 插件将 Hydra 和 Kubernetes 直接集成。
进一步阅读:
Docker 文档是官方 Docker 资源、教程和指南的库,帮助你构建、共享和…
docs.docker.com](https://docs.docker.com/?source=post_page-----ef138f1c1852--------------------------------) [## Kubernetes 文档
Kubernetes 是一个开源的容器编排引擎,用于自动化部署、扩展和管理…
kubernetes.io](https://kubernetes.io/docs/home/?source=post_page-----ef138f1c1852--------------------------------)
XIII. 与 ML 框架的集成
Hydra 可以显著简化机器学习项目中配置管理的过程。本节将展示如何将 Hydra 与流行的机器学习框架(如 PyTorch、TensorFlow 或 scikit-learn)集成。你将学习如何使用配置文件来管理机器学习管道的不同阶段,从数据预处理到模型训练和评估。
Hydra 与 PyTorch
使用 PyTorch(或任何其他 ML 框架)时,你可以使用 Hydra 来管理模型、数据集、优化器和其他组件的配置。这里是一个简化的示例:
@hydra.main(config_path="config.yaml")
def main(cfg):
# Load dataset
dataset = load_dataset(cfg.data)
# Initialize model
model = MyModel(cfg.model) # Initialize optimizer
optimizer = torch.optim.SGD(model.parameters(), lr=cfg.optim.lr) # Train and evaluate model
train(model, dataset, optimizer, cfg.train)
evaluate(model, dataset, cfg.eval)if __name__ == "__main__":
main()
在这个例子中,config.yaml
将包含 data
、model
、optim
、train
和 eval
的单独部分。这种结构保持了配置的有序性和模块化,使你能够轻松调整机器学习管道中不同组件的配置。
例如,你可以在不同的配置文件中定义不同的模型架构、数据集或训练方案,然后通过命令行覆盖来选择你要使用的选项。
这是 PyTorch 的示例配置组:
defaults:
- model: resnet50
- dataset: imagenet
- optimizer: sgd
model:
resnet50:
num_layers: 50
alexnet:
num_layers: 8dataset:
imagenet:
root: /path/to/imagenet
cifar10:
root: /path/to/cifar10optimizer:
sgd:
lr: 0.01
momentum: 0.9
adam:
lr: 0.001
通过这些配置,你可以轻松地在 ResNet-50 和 AlexNet 之间切换,或者在 ImageNet 和 CIFAR-10 之间切换,只需在运行应用程序时更改命令行参数。
进一步阅读:
[## PyTorch 文档 - PyTorch 2.0 文档
稳定:这些功能将长期维护,通常不会有重大性能限制或…
pytorch.org](https://pytorch.org/docs/stable/index.html?source=post_page-----ef138f1c1852--------------------------------)
XIV. 结论
在本教程中,我们深入探讨了 Hydra,这是一个用于 Python 应用程序(包括机器学习项目)的配置管理强大工具。我们涵盖了基础知识、层次配置、配置组和动态配置。此外,我们学习了如何处理环境变量,使用 Hydra 进行日志记录、错误处理和命令行覆盖。
我们还探讨了 Hydra 的一些高级功能,例如多重运行(multirun)和遍历(sweeps),这些功能对于管理机器学习实验特别有用。最后,我们看到 Hydra 如何在 HPC 上使用,与 Docker 和 Kubernetes 结合使用,并与 Facebook 的另一个开源包(即 PyTorch)集成进行深度学习。在本教程中,我们看到 Hydra 可以极大简化配置管理,使你的代码更具灵活性、鲁棒性和可维护性。
掌握像 Hydra 这样的工具需要实践。因此,继续进行实验,尝试新事物,并推动你在配置管理方面的边界。
XV. 附录:有用的 Hydra 命令和技巧
以下是一些常用的 Hydra 命令、提示和技巧,用于有效地在机器学习项目中使用 Hydra。
常用的 Hydra 命令
-
使用 Hydra 运行应用程序:
python my_app.py
-
使用命令行覆盖:
python my_app.py db.driver=postgresql
-
使用多重运行运行应用程序:
python my_app.py — multirun training.batch_size=32,64,128
提示和技巧
1. 利用层次配置:层次配置可以帮助你管理复杂的配置并避免重复。使用它们来定义可以在应用程序的不同部分之间共享的标准设置。
2. 使用命令行覆盖:命令行覆盖是调整运行时配置的强大工具。使用它们来更改超参数、切换模型或更改数据集以进行不同的实验。
3. 实现错误处理:Hydra 提供了高级错误处理功能。利用这些功能使你的代码更加健壮,易于调试。
4. 使用多重运行进行超参数遍历:Hydra 的多重运行(multirun)功能可以显著简化超参数遍历的过程。使用它来探索模型的超参数空间。
5. 继续探索:Hydra 还有许多功能等待发现。查看 Hydra 文档和 GitHub 以获取更多想法和示例。
介绍
hydra.cc](https://hydra.cc/docs/intro/?source=post_page-----ef138f1c1852--------------------------------) [## GitHub - facebookresearch/hydra: Hydra 是一个优雅配置复杂...
Hydra 是一个优雅配置复杂应用程序的框架 - GitHub - facebookresearch/hydra: Hydra 是一个…
github.com](https://github.com/facebookresearch/hydra?source=post_page-----ef138f1c1852--------------------------------)
请通过下面的评论区分享您的想法、用例和问题。
联系方式
想要联系?请关注 Dr. Robinson 在LinkedIn、Twitter、Facebook和Instagram。访问我的主页获取论文、博客、邮件注册等更多信息!
[## AI 研究工程师与企业家 | Joseph P. Robinson
研究员与企业家问候!作为研究员,Dr. Robinson 提出并应用了先进的 AI 来理解…
www.jrobs-vision.com.](https://www.jrobs-vision.com/?source=post_page-----ef138f1c1852--------------------------------)
精通容器化:创建类似 Docker 环境的指南
解锁容器化的力量:构建类似容器环境的逐步教程。
·发布于 Towards Data Science ·6 分钟阅读·2023 年 2 月 4 日
--
图片由 Timelab Pro 提供,来源于 Unsplash
容器彻底改变了我们部署和管理应用程序的方式,提供了无与伦比的便携性、可扩展性和一致性。
然而,你不应被 Docker 光鲜的外表吓到——是时候深入探讨使容器化成为可能的机制了。通过了解 Docker 的内部工作原理,你将更深刻地欣赏这项技术,并对你的操作系统有更广泛的理解。
本系列的最后三篇文章铺平了道路。我们讨论了名称空间、控制组(cgroups)和覆盖文件系统。这些是我们今天用来创建自己容器环境的基础组件。
## 容器:它们如何在幕后工作,以及为何它们正在占领数据科学世界
初学者理解 Docker 魔力的指南
## Linux Cgroups 的力量:容器如何控制它们的资源
使用 Linux 控制组优化容器资源分配
towardsdatascience.com](/the-power-of-linux-cgroups-how-containers-take-control-of-their-resources-ba564fef13b0?source=post_page-----121b3f444d2f--------------------------------) ## 探索 Linux 容器中 Overlay 文件系统的力量
利用独特而简单的分层概念解锁容器化的潜力
towardsdatascience.com
这篇教育性博客文章将指导你在不依赖 Docker 的情况下构建轻量级的隔离环境。你还不能完全取代 Docker!这仅仅是为了教育目的。Docker 提供的功能远不止创建容器。然而,过程才是最重要的。
你准备好揭开操作系统的秘密,并将你对容器化的理解提升到一个新的水平吗?系好安全带,拿上一杯咖啡,深入探索无 Docker 的容器化世界吧!
学习速率是一个针对那些对 MLOps 世界感兴趣的人的新闻通讯。MLOps 是一个广泛的领域,旨在以高效和可重复的方式将 ML 模型投入生产。容器在这一流程中起着至关重要的作用。如果你想了解更多类似的话题,可以在这里订阅。你将会在每个月的最后一个星期六收到我的更新和关于最新 MLOps 新闻和文章的见解!
快速回顾
在这篇文章中,我们将深入探索容器的激动人心的世界。五分钟内,你应该能够在自己构建的类似容器的环境中运行你自己的 Linux 发行版。
正如我们在前言中所说,我们已经研究了今天要使用的基本概念。为了完整性,这里有一个快速回顾:
-
命名空间:命名空间是内核特性,允许你在单个 Linux 系统中创建隔离的环境。每个命名空间都有自己对系统的视图,这意味着在一个命名空间中的进程对其他命名空间中的进程毫无察觉。
-
控制组:Linux 控制组或 cgroups,是一种内核特性,允许管理员将 CPU、内存和 I/O 带宽等资源分配给进程组。
-
Overlay 文件系统:Overlay 文件系统允许将多个底层叠加在一起,创建数据的统一视图。在 Linux 容器的上下文中,Overlay 文件系统用于将容器所做的更改层叠在基础镜像之上,同时保持原始镜像完好无损。
现在我们知道了我们将使用的每个工具的功能,让我们开始使用它们。
头脑风暴
BusyBox 的磁盘占用大小在 1 到 5MB 之间(取决于变体),它是制作空间高效分发版的非常好的一种成分。
要创建一个 BusyBox 容器,你通常会运行以下命令:
docker run -it --rm busybox
这个命令会在一个 BusyBox 容器内给你一个 shell。今天我们将尝试在不运行docker run
命令的情况下实现类似的效果。
所以,首先,我们需要下载镜像。为此,我们将使用一个叫做Skopeo的工具。这一系列文章都涉及到容器,因此让我们利用它们的功能来下载我们想要的镜像,而无需安装 Skopeo。
创建一个名为busybox-image
的目录,位置随你喜欢,然后运行以下命令:
docker run -v /home/vagrant/projects/container-example/busybox-image:/busybox-image quay.io/skopeo/stable copy docker://docker.io/library/busybox:latest dir:/busybox-image
这是你应该看到的输出:
图片由作者提供
这个命令会将你创建的目录作为卷挂载到 Skopeo 容器中。然后,它会指示容器在该目录中下载 BusyBox 镜像。因此,如果你现在运行ls
命令,你将能够在你创建的目录中看到下载的镜像:
ls -la busybox-image/
图片由作者提供
你会看到一堆文件。我们关心的是最大文件的大小。让我们在一个新目录中解压它。首先,创建一个新目录并进入它:
mkdir busybox && cd busybox
现在在busybox
目录中解压镜像:
tar xf ../busybox-image/205dae5015e78dd8c4d302e3db4eb31576fac715b46d099fe09680ba28093a7a
再次运行ls
命令;你将看到 BusyBox 镜像的根文件系统:
图片由作者提供
返回上级目录,创建三个新目录:upper
目录、workdir
和root
。我们在关于叠加文件系统的帖子中已经讲解了这些内容:
mkdir upper workdir root
现在,让我们创建我们的叠加文件系统:
sudo mount -t overlay -o lowerdir=busybox,upperdir=upper,workdir=workdir none root
太好了!现在,如果你运行ls -la root
,你应该能够看到在你创建的root
目录中 BusyBox 镜像的内容。正如我们在叠加文件系统教程中看到的,root
目录提供了lower
和upper
目录的统一视图。然而,lower
目录仍然是只读的,你所做的任何更改都将记录在upper
目录中。这将保持我们的busybox
基础镜像完整。
最后,让我们使用以下命令创建一个类似容器的环境:
unshare -mipunUrf chroot ROOTFS /bin/bash
就这样!你完成了!运行uname -r
来验证我们是否在 BusyBox 容器中:
图片由作者提供
你甚至可以运行一个熟悉的命令如ping
,观察到你实际上调用的是 BusyBox 版本的命令:
图片由作者提供
目前,仍然有很多东西缺失。例如,你没有访问互联网的权限。运行 ping
或 wget
将不会有任何结果。但这需要等到另一个时间来处理。正如我们所说,这将不是一个生产就绪的环境,而是为了揭示 Docker 做的一些事情。
如果你想限制你的容器玩具的资源消耗,可以查看介绍中链接的控制组文章。这比你想象的要简单。
结论
总结一下,创建一个类似容器的环境而不使用 Docker 对任何开发者来说都是一种有价值的技能。无论你是想探索替代技术、解决兼容性问题,还是仅仅扩展对容器化的理解,这篇文章中涵盖的技术和工具都将帮助你实现目标。
通过遵循这个逐步教程,你可以自信且轻松地构建自己的轻量级、隔离的环境。会成功吗?还不能!请耐心等待下一篇文章!
记住,容器化是一个不断发展的领域,总有更多需要学习的东西。所以,继续探索、实验,并推动可能性极限吧!
关于作者
我的名字是 Dimitris Poulopoulos,我是 Arrikto 的机器学习工程师。我为欧洲委员会、欧洲统计局、国际货币基金组织、欧洲中央银行、经济合作与发展组织和宜家等主要客户设计和实施了人工智能和软件解决方案。
如果你有兴趣阅读更多关于机器学习、深度学习、数据科学和数据操作的文章,可以在 Medium、LinkedIn 或 Twitter 上关注 @james2pl。
所表达的观点仅代表我个人,并不代表我雇主的观点或意见。
使用信用卡交易数据掌握客户细分
使用 RFM 评分建立客户细分
·发表于 Towards Data Science ·阅读时间 7 分钟 ·2023 年 7 月 6 日
--
图片由 Andrea Piacquadio 提供,来源于 Pexels
客户细分是根据历史购买模式识别客户群体的过程。例如,它可以包括识别重复/忠实客户、高消费客户、一次性或不频繁购买的客户等。细分可以使用购买频率、交易金额、购买日期等信息来创建。所有这些特征都可以用来生成具有易于解释特征的明确集群。
这些细分的特征可以为公司提供大量信息和商业价值。例如,公司可以利用这些细分群体通过有针对性的促销信息、客户保留活动、忠诚度计划、产品交叉销售等来增加收入。公司可以对客户细分群体进行有针对性的消息传递,以适应每个细分群体的需求。此外,这些细分群体还可以提供有关客户最敏感的渠道的信息,无论是电子邮件、社交媒体还是外部应用程序。公司还可以利用消费者细分进行升级销售或交叉销售。例如,他们可以为经常购买的商品提供高价选项,或为之前购买的商品提供补充产品。所有这些策略都可以用于增加收入和客户保留。
有多种技术可以用于客户细分。其中一种流行的技术是近期性、频率和货币价值(RFM)评分。
近期性、频率和货币价值
-
最近一次购买 是指客户最后一次购买与参考日期(通常是当前日期或数据中可用的最大日期)之间的天数。
-
购买频率 是指从最后一次购买到当前或数据中最大日期之间的购买次数。
-
货币金额 是指从第一次购买到最后一次购买之间的总花费金额。
你可以使用这些数值来构建 RFM 分数,这些分数可以用来对客户进行分段,并识别高价值和低价值客户。这些分数可以用于多种商业用例,包括定制营销、流失分析、价格优化等。
在这里,我们将学习如何使用信用卡交易数据集计算 RFM 分数。为了我们的目的,我们将使用 Synthetic Credit Card Transaction 数据,这些数据可以在 DataFabrica 上获得。数据包含合成的信用卡交易金额、信用卡信息、交易 ID 等。 免费版 可以免费下载、修改和分享,使用 Apache 2.0 许可证。
在这项工作中,我将使用 Deepnote 编写代码,这是一款协作的数据科学笔记本,可以非常方便地进行可重复实验。
数据探索
首先,让我们导航到 Deepnote 并创建一个新项目(如果你还没有账户,可以免费注册)。
让我们安装必要的包:
作者创建的嵌入
并导入我们将使用的包:
作者创建的嵌入
接下来,让我们将数据读入 pandas 数据框,并显示前五行数据:
作者创建的嵌入
接下来,我们将过滤数据框,只包含购买了 Chick-fil-A 的客户:
作者创建的嵌入
接下来,我们可以查看各州客户的数量。为此,我们需要将 merchant_state 映射到州缩写,这样我们就可以在 Plotly 中绘制每个州的客户数量:
作者创建的嵌入
接下来,我们将映射州缩写,并定义我们的 state_count 表。我们通过对每个州的每个持卡人执行 groupby nunique()
操作来完成:
df['merchant_state_abbr'] = df['merchant_state'].map(state_abbreviations)
state_counts = df.groupby('merchant_state_abbr')['cardholder_name'].nunique().reset_index()
state_counts.columns = ['State', 'Customer_Count']
接下来,我们可以使用 Plotly Express 的 chloropleth
方法生成每个州客户数量的地理图:
fig = px.choropleth(state_counts, locations='State', locationmode='USA-states',
color='Customer_Count', scope='usa',
color_continuous_scale='Blues',
title='Number of Customers by State')
fig.show()
完整的逻辑是:
作者创建的嵌入
生成 RFM 分数
现在让我们定义创建 RFM 分数的逻辑。首先将我们的 transaction_date
转换为 pandas datetime 对象,并定义一个 NOW
变量,它是最大的 transaction_date
:
df['transaction_date'] = pd.to_datetime(df['transaction_date'])
NOW = df['transaction_date'].max()
接下来,让我们执行一个 groupby 聚合操作,以计算最近一次购买、购买频率和货币金额。
-
最近一次购买 — 总最大日期减去客户最大日期:
df.groupby('cardholder_name').agg({'transaction_date': lambda x: (NOW — x.max()).days})
-
频率 — 每个客户的交易 ID 数量:
df.groupby('cardholder_name').agg({transaction_id': lambda x: len(x)})
-
货币价值 — 每个客户的交易金额总和:
df.groupby('cardholder_name').agg({transaction_amount': lambda x: x.sum()})
我们还将 transaction_date
转换为整数:
rfmTable = df.groupby('cardholder_name').agg({'transaction_date': lambda x: (NOW - x.max()).days, 'transaction_id': lambda x: len(x), 'transaction_amount': lambda x: x.sum()})
rfmTable['transaction_date'] = rfmTable['transaction_date'].astype(int)
接下来,让我们适当地重命名我们的列。
-
transaction_date
变为recency
-
transaction_id
变为frequency
-
transaction_amount
变为monetary_value
rfmTable.rename(columns={'transaction_date': 'recency',
'transaction_id': 'frequency',
'transaction_amount': 'monetary_value'}, inplace=True)
rfmTable = rfmTable.reset_index()
完整逻辑是:
嵌入由作者创建
我们可以查看最近性的分布:
嵌入由作者创建
频率:
嵌入由作者创建
以及货币价值:
嵌入由作者创建
接下来,我们可以使用 Pandas qcut
方法计算最近性、频率和货币价值的四分位数:
rfmTable['r_quartile'] = pd.qcut(rfmTable['recency'], q=4, labels=range(1,5), duplicates='raise')
rfmTable['f_quartile'] = pd.qcut(rfmTable['frequency'], q=4, labels=range(1,5), duplicates='drop')
rfmTable['m_quartile'] = pd.qcut(rfmTable['monetary_value'], q=4, labels=range(1,5), duplicates='drop')
rfm_data = rfmTable.reset_index()
接下来,我们可以可视化最近性/频率热图,其中每个单元格显示具有相应最近性和频率值的客户百分比。首先,让我们计算百分比:
heatmap_data = rfm_data.groupby(['r_quartile', 'f_quartile']).size().reset_index(name='Percentage')
heatmap_data['Percentage'] = heatmap_data['Percentage'] / heatmap_data['Percentage'].sum() * 100
接下来,让我们生成我们的热图矩阵:
heatmap_matrix = heatmap_data.pivot('r_quartile', 'f_quartile', 'Percentage')
使用 Seaborn 和 Matplotlib 生成地图并标记/标题我们的热图:
sns.set()
sns.heatmap(heatmap_matrix, annot=True, fmt=".2f", cmap="YlGnBu")
plt.title("Customer Segmentation Heatmap")
plt.xlabel("Frequency Quartile")
plt.ylabel("Recency Quartile")
plt.show()
完整逻辑是:
嵌入由作者创建
我们可以从热图中看到以下洞察:
-
16.21% 的客户最近购买但购买不频繁。
-
3.45% 的客户是频繁且最近的客户。
-
10% 的客户频繁购买但时间不长。
-
5.86% 的客户最近没有购买,也没有频繁购买。
我们仅考虑了单一商户 Chick-fil-A,但我鼓励你对其他一些休闲和高档餐饮商户重复此分析。
接下来,我们可以通过连接最近性、频率和货币价值的四分位数来生成我们的 RFM 分数:
嵌入由作者创建
我们可以可视化我们的 RFM 分数的分布:
嵌入由作者创建
在这里我们看到,最常见的 RFM 分数是‘411’,这对应于最近的客户,花费不频繁且金额很少。
使用 RFM 分数生成和可视化客户分段
接下来,我们可以生成我们的客户分段。这一步有点主观,但我们定义的分段如下:
-
高价值客户:r、f 和 m 全部 ≥ 3
-
重复客户:f ≥ 3 且 r 或 m ≥ 3
-
顶级花费者:m ≥ 3 且 f 或 r ≥ 3
-
高风险客户:r、f 和 m 中两个或更多 ≤ 2
-
非活跃客户:两个或更多 = 1
-
其他:其他任何东西
嵌入由作者创建
接下来,我们可以查看各个分段的分布:
嵌入由作者创建
在这里我们看到,最大的客户分段是非活跃客户,其次是高风险客户、顶级花费者、重复客户和高价值客户。
还可以可视化分布:
嵌入由作者创建
我们还可以查看每个客户分段的平均货币价值:
嵌入由作者创建
我们看到高价值客户、重复客户和顶级花费者的平均货币价值最高。此外,非活跃和高风险客户的货币价值较低。
各客户细分的平均频率值:
嵌入由作者创建
我们看到优质客户、重复客户和高消费客户的频率最高,而风险客户和非活跃客户的频率较低。
最后,各客户细分的平均最近性值:
嵌入由作者创建
我们看到“其他”类别的购买最近性最高。这可能是一个适合“新”客户的细分,这些客户的购买模式尚不明确。
使用客户细分进行定制营销
你可以利用这些客户细分生成与每个细分市场相关的定制/个性化营销信息。例如,你可以通过特别促销和忠诚度优惠奖励优质客户。还可以根据他们的购买历史推广他们可能感兴趣的其他产品。对于重复/忠诚客户,你可以开发自动化电子邮件活动以保持他们的忠诚度。风险客户通常处于脱离状态。我们可以基于这些客户开发活动,以重新吸引他们并促使他们重新购买。对于非活跃客户,可以开发类似的活动。最后,对于高消费客户,我们可以提供特别促销和针对他们可能再次购买的高价产品的优惠。
利用这些数据,我们可以进一步分析,并使用items
和prices
字段为这些细分市场制定定制的营销活动。
我鼓励你从GitHub下载代码,并在其他餐厅商户上重复此分析。
结论
在这里,我们讨论了如何对合成信用卡交易数据进行客户细分。我们首先进行了一些简单的数据探索,查看了商户 Chick-fil-A 每个州的客户数量。接着,我们计算了生成 RFM 得分所需的列,包括最近性、频率和货币价值。然后,我们展示了如何为每个客户生成最近性、频率和货币价值的四分位数,并利用这些数据构建 RFM 得分。接下来,我们使用 RFM 得分生成了客户细分“优质客户”、“重复客户”、“高消费客户”、“风险客户”和“非活跃客户”。我们还生成了一些可视化图表,以便分析客户细分在数据中的分布情况。
使用 RFM 得分生成有洞察力的客户细分是任何希望创造商业价值的数据科学家的宝贵技能。构建可解释的细分并从这些细分中提取洞察力可以帮助企业设计营销活动策略,从而增加收入和客户留存。了解客户的购买行为使企业能够针对适当的客户群体量身定制促销优惠。本文提供了开始所需的基本知识!
免费版的合成信用卡数据可以在这里找到。完整的数据集可以在这里找到。
掌握客户细分的终极技巧
利用 LLM 解锁高级客户细分技术,并通过先进技术改进你的聚类模型
·
关注 发布于 Towards Data Science ·24 分钟阅读·2023 年 9 月 26 日
--
内容目录
· 简介 · 数据 · 方法 1: Kmeans · 方法 2: K-Prototype · 方法 3: LLM + Kmeans · 结论
简介
客户细分项目可以通过多种方式进行。在这篇文章中,我将教你高级技术,不仅是定义簇,还包括分析结果。此帖面向那些希望掌握多种工具以解决聚类问题并向成为资深数据科学家迈进的数据科学家。
我们在这篇文章中会看到什么?
让我们来看看三种方法来处理这种类型的项目:
-
Kmeans
-
K-Prototype
-
LLM + Kmeans
作为一个小预览,我将展示以下不同模型创建的二维表示(PCA)的比较:
三种方法的图形比较(图片由作者提供)。
你还将学习诸如以下的降维技术:
-
PCA
-
t-SNE
-
MCA
结果如下:
三种降维方法的图形比较(图片由作者提供)。
你可以在这里找到带有笔记本的项目。你也可以查看我的 github:
[## damiangilgonzalez1995 - 概述
对数据充满热情,我从物理学转行到了数据科学。曾在 Telefonica、HP 工作,现在担任 CTO…
github.com](https://github.com/damiangilgonzalez1995?source=post_page-----3d9008235f41--------------------------------)
一个非常重要的说明是,这不是一个完整的项目。这是因为我们跳过了这种类型项目中最重要的部分之一:探索性数据分析(EDA)阶段或变量选择。
数据
本项目使用的原始数据来自公开的 Kaggle:银行数据集 — 市场营销目标。此数据集中的每一行包含有关公司客户的信息。一些字段是数值型的,其他的是分类的,我们将看到这扩展了处理问题的可能方法。
我们只会保留前 8 列。我们的数据集如下所示:
让我们简要了解一下数据集中的列:
-
年龄(数值型)
-
工作:工作类型(分类: “行政”,“未知”,“失业”,“管理”,“家政”,“企业家”,“学生”,“蓝领”,“自雇”,“退休”,“技术人员”,“服务”)
-
婚姻状况:婚姻状态(分类: “已婚”,“离婚”,“单身”;注:“离婚”指离婚或丧偶)
-
教育(分类: “未知”,“中等”,“小学”,“高等”)
-
违约:是否有信用违约?(二元: “是”,“否”)
-
余额:年平均余额,单位欧元(数值型)
-
住房:是否有住房贷款?(二元: “是”,“否”)
-
贷款:是否有个人贷款?(二元: “是”,“否”)
对于这个项目,我利用了 Kaggle 提供的训练数据集。在项目仓库中,你可以找到“data”文件夹,其中存储了用于项目的压缩数据集文件。此外,你还会在压缩文件内找到两个 CSV 文件,一个是 Kaggle 提供的训练数据集(train.csv),另一个是经过嵌入处理后的数据集(embedding_train.csv),我们将在稍后进一步解释。
为了进一步澄清项目的结构,展示了项目树:
clustering_llm
├─ data
│ ├─ data.rar
├─ img
├─ embedding.ipynb
├─ embedding_creation.py
├─ kmeans.ipynb
├─ kprototypes.ipynb
├─ README.md
└─ requirements.txt
方法 1:Kmeans
这是最常见的方法,也是你一定熟悉的方法。无论如何,我们将研究它,因为我将在这些情况下展示高级分析技巧。你可以在名为kmeans.ipynb的 Jupyter 笔记本中找到完整的过程。
预处理
变量的预处理如下:
-
这包括将分类变量转换为数值变量。
我们将对名义变量应用 Onehot 编码器,对序数特征(教育)应用 OrdinalEncoder。
-
我们尝试确保数值变量具有高斯分布。为此,我们将应用 PowerTransformer。
让我们看看它在代码中是如何表现的。
import pandas as pd # dataframe manipulation
import numpy as np # linear algebra
# data visualization
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import plotly.express as px
import plotly.graph_objects as go
import seaborn as sns
import shap
# sklearn
from sklearn.cluster import KMeans
from sklearn.preprocessing import PowerTransformer, OrdinalEncoder
from sklearn.pipeline import Pipeline
from sklearn.manifold import TSNE
from sklearn.metrics import silhouette_score, silhouette_samples, accuracy_score, classification_report
from pyod.models.ecod import ECOD
from yellowbrick.cluster import KElbowVisualizer
import lightgbm as lgb
import prince
# Read file
df = pd.read_csv("train.csv", sep = ";")
df = df.iloc[:, 0:8]
# Preprocessing part
categorical_transformer_onehot = Pipeline(
steps=[
("encoder", OneHotEncoder(handle_unknown="ignore", drop="first", sparse=False))
])
categorical_transformer_ordinal = Pipeline(
steps=[
("encoder", OrdinalEncoder())
])
num = Pipeline(
steps=[
("encoder", PowerTransformer())
])
preprocessor = ColumnTransformer(transformers = [
('cat_onehot', categorical_transformer_onehot, ["default", "housing", "loan", "job", "marital"]),
('cat_ordinal', categorical_transformer_ordinal, ["education"]),
('num', num, ["age", "balance"])
])
pipeline = Pipeline(
steps=[("preprocessor", preprocessor)]
)
pipe_fit = pipeline.fit(df)
data = pd.DataFrame(pipe_fit.transform(df), columns = pipe_fit.get_feature_names_out().tolist())
print(data.columns.tolist())
# OUTPUT
['cat_onehot__default_yes',
'cat_onehot__housing_yes',
'cat_onehot__loan_yes',
'cat_onehot__job_blue-collar',
'cat_onehot__job_entrepreneur',
'cat_onehot__job_housemaid',
'cat_onehot__job_management',
'cat_onehot__job_retired',
'cat_onehot__job_self-employed',
'cat_onehot__job_services',
'cat_onehot__job_student',
'cat_onehot__job_technician',
'cat_onehot__job_unemployed',
'cat_onehot__job_unknown',
'cat_onehot__marital_married',
'cat_onehot__marital_single',
'cat_ordinal__education',
'num__age',
'num__balance']
输出:
异常值
关键在于我们的数据中异常值要尽可能少,因为 Kmeans 对异常值非常敏感。我们可以使用 z 分数的典型方法来选择异常值,但在这篇文章中我将展示一种更高级且酷的方法。
那么,这种方法是什么呢?我们将使用Python 异常检测(PyOD)库。这个库专注于检测不同情况下的异常值。更具体来说,我们将使用ECOD方法(“用于异常检测的经验累积分布函数”)。
该方法旨在获取数据的分布,从而了解哪些值的概率密度较低(异常值)。如果你想的话,可以看看Github。
from pyod.models.ecod import ECOD
clf = ECOD()
clf.fit(data)
outliers = clf.predict(data)
data["outliers"] = outliers
# Data without outliers
data_no_outliers = data[data["outliers"] == 0]
data_no_outliers = data_no_outliers.drop(["outliers"], axis = 1)
# Data with Outliers
data_with_outliers = data.copy()
data_with_outliers = data_with_outliers.drop(["outliers"], axis = 1)
print(data_no_outliers.shape) -> (40690, 19)
print(data_with_outliers.shape) -> (45211, 19)
建模
使用 Kmeans 算法的一个缺点是你必须选择要使用的簇数。在这种情况下,为了获取这些数据,我们将使用肘部法则。它的原理是计算簇内点与其质心之间的失真。目标很明确,尽量减少失真。在这种情况下,我们使用以下代码:
from yellowbrick.cluster import KElbowVisualizer
# Instantiate the clustering model and visualizer
km = KMeans(init="k-means++", random_state=0, n_init="auto")
visualizer = KElbowVisualizer(km, k=(2,10))
visualizer.fit(data_no_outliers) # Fit the data to the visualizer
visualizer.show()
输出:
不同簇数的肘部评分(作者提供的图片)。
我们看到从k=5开始,失真没有明显变化。理想的情况是,从 k=5 开始,行为几乎保持平坦。这种情况很少发生,可以应用其他方法来确定最优的集群数量。为了确保,我们可以进行Silhouette 可视化。代码如下:
from sklearn.metrics import davies_bouldin_score, silhouette_score, silhouette_samples
import matplotlib.cm as cm
def make_Silhouette_plot(X, n_clusters):
plt.xlim([-0.1, 1])
plt.ylim([0, len(X) + (n_clusters + 1) * 10])
clusterer = KMeans(n_clusters=n_clusters, max_iter = 1000, n_init = 10, init = 'k-means++', random_state=10)
cluster_labels = clusterer.fit_predict(X)
silhouette_avg = silhouette_score(X, cluster_labels)
print(
"For n_clusters =", n_clusters,
"The average silhouette_score is :", silhouette_avg,
)
# Compute the silhouette scores for each sample
sample_silhouette_values = silhouette_samples(X, cluster_labels)
y_lower = 10
for i in range(n_clusters):
ith_cluster_silhouette_values = sample_silhouette_values[cluster_labels == i]
ith_cluster_silhouette_values.sort()
size_cluster_i = ith_cluster_silhouette_values.shape[0]
y_upper = y_lower + size_cluster_i
color = cm.nipy_spectral(float(i) / n_clusters)
plt.fill_betweenx(
np.arange(y_lower, y_upper),
0,
ith_cluster_silhouette_values,
facecolor=color,
edgecolor=color,
alpha=0.7,
)
plt.text(-0.05, y_lower + 0.5 * size_cluster_i, str(i))
y_lower = y_upper + 10
plt.title(f"The Silhouette Plot for n_cluster = {n_clusters}", fontsize=26)
plt.xlabel("The silhouette coefficient values", fontsize=24)
plt.ylabel("Cluster label", fontsize=24)
plt.axvline(x=silhouette_avg, color="red", linestyle="--")
plt.yticks([])
plt.xticks([-0.1, 0, 0.2, 0.4, 0.6, 0.8, 1])
range_n_clusters = list(range(2,10))
for n_clusters in range_n_clusters:
print(f"N cluster: {n_clusters}")
make_Silhouette_plot(data_no_outliers, n_clusters)
plt.savefig('Silhouette_plot_{}.png'.format(n_clusters))
plt.close()
OUTPUT:
"""
N cluster: 2
For n_clusters = 2 The average silhouette_score is : 0.18111287366156115
N cluster: 3
For n_clusters = 3 The average silhouette_score is : 0.16787543108034586
N cluster: 4
For n_clusters = 4 The average silhouette_score is : 0.1583411958880734
N cluster: 5
For n_clusters = 5 The average silhouette_score is : 0.1672987260052535
N cluster: 6
For n_clusters = 6 The average silhouette_score is : 0.15485098506258177
N cluster: 7
For n_clusters = 7 The average silhouette_score is : 0.1495307642182009
N cluster: 8
For n_clusters = 8 The average silhouette_score is : 0.15098396457075294
N cluster: 9
For n_clusters = 9 The average silhouette_score is : 0.14842917303536465
"""
可以看出,最高的轮廓得分是在 n_cluster=9 时获得的,但如果与其他得分比较,得分的变化确实很小。目前,之前的结果并没有提供太多信息。另一方面,之前的代码创建了轮廓可视化,这为我们提供了更多信息:
不同集群数量的轮廓方法的图形表示(作者提供的图片)。
由于深入理解这些表示并不是本文的目标,我将总结为似乎没有非常明确的决定哪个数字最好。在查看了之前的表示后,我们可以选择K=5 或 K=6。这是因为对于不同的集群,它们的轮廓得分都高于平均值,并且集群大小没有不平衡。此外,在某些情况下,市场部门可能会希望拥有最少的集群/客户类型(这可能会或不会发生)。
最后,我们可以创建 Kmeans 模型,K=5。
km = KMeans(n_clusters=5,
init='k-means++',
n_init=10,
max_iter=100,
random_state=42)
clusters_predict = km.fit_predict(data_no_outliers)
"""
clusters_predict -> array([4, 2, 0, ..., 3, 4, 3])
np.unique(clusters_predict) -> array([0, 1, 2, 3, 4])
"""
评估
评估 kmeans 模型的方法比其他模型稍微开放一些。我们可以使用
-
指标
-
可视化
-
解释(对公司来说非常重要)。
关于模型评估指标,我们可以使用以下代码:
from sklearn.metrics import silhouette_score
from sklearn.metrics import calinski_harabasz_score
from sklearn.metrics import davies_bouldin_score
"""
The Davies Bouldin index is defined as the average similarity measure
of each cluster with its most similar cluster, where similarity
is the ratio of within-cluster distances to between-cluster distances.
The minimum value of the DB Index is 0, whereas a smaller
value (closer to 0) represents a better model that produces better clusters.
"""
print(f"Davies bouldin score: {davies_bouldin_score(data_no_outliers,clusters_predict)}")
"""
Calinski Harabaz Index -> Variance Ratio Criterion.
Calinski Harabaz Index is defined as the ratio of the
sum of between-cluster dispersion and of within-cluster dispersion.
The higher the index the more separable the clusters.
"""
print(f"Calinski Score: {calinski_harabasz_score(data_no_outliers,clusters_predict)}")
"""
The silhouette score is a metric used to calculate the goodness of
fit of a clustering algorithm, but can also be used as
a method for determining an optimal value of k (see here for more).
Its value ranges from -1 to 1.
A value of 0 indicates clusters are overlapping and either
the data or the value of k is incorrect.
1 is the ideal value and indicates that clusters are very
dense and nicely separated.
"""
print(f"Silhouette Score: {silhouette_score(data_no_outliers,clusters_predict)}")
OUTPUT:
"""Davies bouldin score: 1.676769775662788
Calinski Score: 6914.705500337112
Silhouette Score: 0.16729335453305272
"""
据显示,我们没有一个特别好的模型。Davies 得分告诉我们集群之间的距离相当小。
这可能由几个因素造成,但请记住,模型的能量就是数据;如果数据没有足够的预测能力,你不能期望得到卓越的结果。
对于可视化,我们可以使用降维方法,PCA。我们将使用Prince库,专注于探索性分析和降维。如果你愿意,也可以使用 Sklearn 的 PCA,它们是相同的。
首先,我们将计算 3D 中的主成分,然后进行表示。这是前面步骤执行的两个功能:
import prince
import plotly.express as px
def get_pca_2d(df, predict):
pca_2d_object = prince.PCA(
n_components=2,
n_iter=3,
rescale_with_mean=True,
rescale_with_std=True,
copy=True,
check_input=True,
engine='sklearn',
random_state=42
)
pca_2d_object.fit(df)
df_pca_2d = pca_2d_object.transform(df)
df_pca_2d.columns = ["comp1", "comp2"]
df_pca_2d["cluster"] = predict
return pca_2d_object, df_pca_2d
def get_pca_3d(df, predict):
pca_3d_object = prince.PCA(
n_components=3,
n_iter=3,
rescale_with_mean=True,
rescale_with_std=True,
copy=True,
check_input=True,
engine='sklearn',
random_state=42
)
pca_3d_object.fit(df)
df_pca_3d = pca_3d_object.transform(df)
df_pca_3d.columns = ["comp1", "comp2", "comp3"]
df_pca_3d["cluster"] = predict
return pca_3d_object, df_pca_3d
def plot_pca_3d(df, title = "PCA Space", opacity=0.8, width_line = 0.1):
df = df.astype({"cluster": "object"})
df = df.sort_values("cluster")
fig = px.scatter_3d(
df,
x='comp1',
y='comp2',
z='comp3',
color='cluster',
template="plotly",
# symbol = "cluster",
color_discrete_sequence=px.colors.qualitative.Vivid,
title=title).update_traces(
# mode = 'markers',
marker={
"size": 4,
"opacity": opacity,
# "symbol" : "diamond",
"line": {
"width": width_line,
"color": "black",
}
}
).update_layout(
width = 800,
height = 800,
autosize = True,
showlegend = True,
legend=dict(title_font_family="Times New Roman",
font=dict(size= 20)),
scene = dict(xaxis=dict(title = 'comp1', titlefont_color = 'black'),
yaxis=dict(title = 'comp2', titlefont_color = 'black'),
zaxis=dict(title = 'comp3', titlefont_color = 'black')),
font = dict(family = "Gilroy", color = 'black', size = 15))
fig.show()
不用过于担心这些功能,按如下方式使用它们:
pca_3d_object, df_pca_3d = pca_plot_3d(data_no_outliers, clusters_predict)
plot_pca_3d(df_pca_3d, title = "PCA Space", opacity=1, width_line = 0.1)
print("The variability is :", pca_3d_object.eigenvalues_summary)
输出:
PCA 空间及模型创建的集群(作者提供的图片)。
可以看出,集群之间几乎没有分隔,也没有明确的划分。这与指标提供的信息一致。
需要牢记的一点是 PCA 和 特征向量的变异性,这是很少有人关注的。
假设每个领域包含一定量的信息,这会增加它的信息量。如果前 3 个主要组件的累积和约为 80% 的变异性,我们可以说这是可以接受的,并且在表示中获得了良好的结果。如果这个值较低,我们必须对可视化结果持保留态度,因为我们遗漏了其他特征向量中包含的大量信息。
接下来的问题很明显:PCA 执行的变异性是多少?
答案如下:
如图所示,我们用前三个组件的变异性为 27.98%,这不足以得出有根据的结论。
当我们应用 PCA 方法时,由于它是线性算法,它无法捕捉更复杂的关系。幸运的是,有一种称为 t-SNE 的方法,能够捕捉到 这些复杂的多项式关系。这可以帮助我们进行可视化,因为我们之前的方法效果不佳。
如果你在自己的计算机上尝试,请记住这会有较高的计算成本。因此,我对原始数据集进行了抽样,但仍然花了大约 5 分钟才得到结果。代码如下:
from sklearn.manifold import TSNE
sampling_data = data_no_outliers.sample(frac=0.5, replace=True, random_state=1)
sampling_clusters = pd.DataFrame(clusters_predict).sample(frac=0.5, replace=True, random_state=1)[0].values
df_tsne_3d = TSNE(
n_components=3,
learning_rate=500,
init='random',
perplexity=200,
n_iter = 5000).fit_transform(sampling_data)
df_tsne_3d = pd.DataFrame(df_tsne_3d, columns=["comp1", "comp2",'comp3'])
df_tsne_3d["cluster"] = sampling_clusters
plot_pca_3d(df_tsne_3d, title = "PCA Space", opacity=1, width_line = 0.1)
结果,我得到了以下图像。它显示了聚类之间更清晰的分离,但不幸的是,我们仍然没有得到很好的结果。
t-SNE 空间和模型创建的聚类(作者提供的图像)。
实际上,我们可以比较 PCA 和 t-SNE 在 2 维中的降维效果。使用第二种方法的改进是显而易见的。
不同降维方法和模型定义的聚类的不同结果(作者提供的图像)。
最后,让我们稍微探讨一下模型的工作原理,哪些特征最重要,以及聚类的主要特征是什么。
为了查看每个变量的重要性,我们将使用这种情况中的典型“技巧”。我们将创建一个分类模型,其中“X”是 Kmeans 模型的输入,而“y”是 Kmeans 模型预测的聚类。
选择的模型是 LGBMClassifier。这个模型非常强大,并且能很好地处理分类和数值变量。在训练好新模型后,使用 SHAP 库,我们可以获取每个特征在预测中的重要性。代码如下:
import lightgbm as lgb
import shap
# We create the LGBMClassifier model and train it
clf_km = lgb.LGBMClassifier(colsample_by_tree=0.8)
clf_km.fit(X=data_no_outliers, y=clusters_predict)
#SHAP values
explainer_km = shap.TreeExplainer(clf_km)
shap_values_km = explainer_km.shap_values(data_no_outliers)
shap.summary_plot(shap_values_km, data_no_outliers, plot_type="bar", plot_size=(15, 10))
输出:
模型中变量的重要性(作者提供的图像)。
可以看出特征age具有最大的预测能力。还可以看出,簇编号 3(绿色)主要通过balance变量区分。
最终,我们必须分析簇的特征。这部分研究对业务至关重要。我们将为每个簇获取数据集中每个特征的均值(对于数值变量)和最频繁的值(对于分类变量):
df_no_outliers = df[df.outliers == 0]
df_no_outliers["cluster"] = clusters_predict
df_no_outliers.groupby('cluster').agg(
{
'job': lambda x: x.value_counts().index[0],
'marital': lambda x: x.value_counts().index[0],
'education': lambda x: x.value_counts().index[0],
'housing': lambda x: x.value_counts().index[0],
'loan': lambda x: x.value_counts().index[0],
'contact': lambda x: x.value_counts().index[0],
'age':'mean',
'balance': 'mean',
'default': lambda x: x.value_counts().index[0],
}
).reset_index()
输出:
我们看到job=blue-collar的簇在特征之间没有很大的差异,除了年龄特征。这是不理想的,因为很难区分每个簇的客户。在job=management的情况下,我们得到更好的差异化。
经过不同方式的分析,它们得出了相同的结论:“我们需要改进结果”。
方法 2:K-Prototype
如果我们记住原始数据集,我们会看到我们有分类和数值变量。不幸的是,Skelearn 提供的 Kmeans 算法不接受分类变量,迫使原始数据集被修改并严重改变。
幸运的是,你与我和我的帖子一起前进。但最重要的是,感谢ZHEXUE HUANG及其文章Extensions to the k-Means Algorithm for Clustering Large Data Sets with Categorical Values,存在一种接受分类变量的聚类算法。这个算法叫做K-Prototype。提供它的书店是Prince。
程序与之前的情况相同。为了不让这篇文章变得冗长,我们直接进入最有趣的部分。但请记住,您可以通过Jupyter notebook 这里访问。
预处理
由于我们有数值变量,我们必须对其进行某些修改。通常建议所有数值变量处于类似的尺度上,并且分布尽可能接近高斯分布。我们将用于创建模型的数据集是这样创建的:
pipe = Pipeline([('scaler', PowerTransformer())])
df_aux = pd.DataFrame(pipe_fit.fit_transform(df_no_outliers[["age", "balance"]] ), columns = ["age", "balance"])
df_no_outliers_norm = df_no_outliers.copy()
# Replace age and balance columns by preprocessed values
df_no_outliers_norm = df_no_outliers_norm.drop(["age", "balance"], axis = 1)
df_no_outliers_norm["age"] = df_aux["age"].values
df_no_outliers_norm["balance"] = df_aux["balance"].values
df_no_outliers_norm
异常值
由于我提出的异常值检测方法(ECOD)仅接受数值变量,因此必须进行与 kmeans 方法相同的转换。我们应用异常值检测模型,这将告诉我们要删除哪些行,最后留下我们将作为 K-Prototype 模型输入的数据集:
建模
我们创建模型,为此我们首先需要获得最佳的 k。为此,我们使用Elbow Method和这段代码:
# Choose optimal K using Elbow method
from kmodes.kprototypes import KPrototypes
from plotnine import *
import plotnine
cost = []
range_ = range(2, 15)
for cluster in range_:
kprototype = KPrototypes(n_jobs = -1, n_clusters = cluster, init = 'Huang', random_state = 0)
kprototype.fit_predict(df_no_outliers, categorical = categorical_columns_index)
cost.append(kprototype.cost_)
print('Cluster initiation: {}'.format(cluster))
# Converting the results into a dataframe and plotting them
df_cost = pd.DataFrame({'Cluster':range_, 'Cost':cost})
# Data viz
plotnine.options.figure_size = (8, 4.8)
(
ggplot(data = df_cost)+
geom_line(aes(x = 'Cluster',
y = 'Cost'))+
geom_point(aes(x = 'Cluster',
y = 'Cost'))+
geom_label(aes(x = 'Cluster',
y = 'Cost',
label = 'Cluster'),
size = 10,
nudge_y = 1000) +
labs(title = 'Optimal number of cluster with Elbow Method')+
xlab('Number of Clusters k')+
ylab('Cost')+
theme_minimal()
)
输出:
不同聚类数量的肘部评分(图像由作者提供)。
我们可以看到,最佳选项是K=5。
小心,因为这个算法比通常使用的算法要慢一点。对于之前的图表,需要 86 分钟,这一点需要注意。
好的,我们现在已经明确了聚类数量,只需要创建模型:
# We get the index of categorical columns
numerics = ['int16', 'int32', 'int64', 'float16', 'float32', 'float64']
categorical_columns = df_no_outliers_norm.select_dtypes(exclude=numerics).columns
print(categorical_columns)
categorical_columns_index = [df_no_outliers_norm.columns.get_loc(col) for col in categorical_columns]
# Create the model
cluster_num = 5
kprototype = KPrototypes(n_jobs = -1, n_clusters = cluster_num, init = 'Huang', random_state = 0)
kprototype.fit(df_no_outliers_norm, categorical = categorical_columns_index)
clusters = kprototype.predict(df_no_outliers , categorical = categorical_columns_index)
print(clusters) " -> array([3, 1, 1, ..., 1, 1, 2], dtype=uint16)"
我们已经有了我们的模型及其预测,现在只需要对其进行评估。
评估
正如我们之前所见,我们可以应用多种可视化方法来直观地了解模型的好坏。不幸的是,PCA 方法和 t-SNE 不接受分类变量。但不用担心,因为Prince库包含了MCA(多重对应分析)方法,并且可以接受混合数据集。事实上,我鼓励你访问这个库的Github,它有许多非常有用的方法适用于不同情况,见下图:
不同类型案例的降维方法(图像由作者和 Prince 文档提供)。
好吧,计划是应用 MCA 来减少维度并进行图形表示。为此我们使用以下代码:
from prince import MCA
def get_MCA_3d(df, predict):
mca = MCA(n_components =3, n_iter = 100, random_state = 101)
mca_3d_df = mca.fit_transform(df)
mca_3d_df.columns = ["comp1", "comp2", "comp3"]
mca_3d_df["cluster"] = predict
return mca, mca_3d_df
def get_MCA_2d(df, predict):
mca = MCA(n_components =2, n_iter = 100, random_state = 101)
mca_2d_df = mca.fit_transform(df)
mca_2d_df.columns = ["comp1", "comp2"]
mca_2d_df["cluster"] = predict
return mca, mca_2d_df
"-------------------------------------------------------------------"
mca_3d, mca_3d_df = get_MCA_3d(df_no_outliers_norm, clusters)
记住,如果你想完全按照每一步操作,可以查看 Jupyter notebook.
名为mca_3d_df的数据集包含了这些信息:
我们来绘制一个使用 MCA 方法提供的降维结果的图:
MCA 空间和模型创建的聚类(图像由作者提供)
哇,这看起来不太好……无法区分各个聚类。那么我们可以说模型不够好,对吗?
我希望你说了类似的话:
“嘿,Damian,别这么快!!你看过 MCA 提供的 3 个组件的变异性吗?”
实际上,我们必须查看前三个组件的变异性是否足够以得出结论。MCA 方法允许我们以非常简单的方式获得这些值:
mca_3d.eigenvalues_summary
啊哈,这里有一些有趣的东西。由于我们的数据,基本上获得了零变异性。
换句话说,我们不能仅凭 MCA 提供的降维信息得出明确的结论。
通过展示这些结果,我尝试举例说明实际数据项目中会发生什么。好结果并不总是能获得,但一个好的数据科学家知道如何识别原因。
我们还有一个最后的选项来直观地确定 K-Prototype 方法创建的模型是否合适。这个路径很简单:
-
这就是对经过预处理的数据集应用 PCA,将分类变量转换为数值变量。
-
获取 PCA 的组件
-
使用 PCA 组件制作表示图,例如轴线和点的颜色,以预测 K-Prototype 模型。
请注意,PCA 提供的组件将与方法 1 中的 Kmeans 相同,因为它使用的是相同的数据框。
让我们看看我们得到什么…
PCA 空间和模型创建的簇(图片由作者提供)。
看起来不错,实际上与 Kmeans 得到的结果有一定的相似性。
最后,我们获得簇的平均值以及每个变量的重要性:
模型中变量的重要性。表格表示每个簇中最常见的值(图片由作者提供)。
权重大的是数值变量,特别是看到这两个特征的限制几乎足以区分每个簇。
简而言之,可以说得到的结果与 Kmeans 类似。
方法 3:LLM + Kmeans
这种组合可能非常强大,并且能提高获得的结果。直接进入正题!
LLMs无法直接理解书面文本,我们需要对这种类型模型的输入进行转换。为此,句子 嵌入被执行。它包括将文本转换为数值向量。以下图像可以阐明这个概念:
嵌入和相似性的概念(图片由作者提供)。
这种编码是智能完成的,即具有相似含义的短语将具有更相似的向量。请参见下图:
嵌入和相似性的概念(图片由作者提供)。
句子嵌入是通过所谓的变换进行的,这些算法专门用于这种编码。通常可以选择来自这种编码的数值向量的大小。这里是一个关键点:
由于嵌入创建的向量维度较大,可以更精确地观察数据中的小变化。
因此,如果我们向信息丰富的 Kmeans 模型提供输入,它将返回更好的预测。 这是我们追求的目标,以下是步骤:
-
通过句子嵌入转换我们的原始数据集
-
创建一个 Kmeans 模型
-
评估一下
首先一步是通过句子嵌入(Sentence embedding)来编码信息。目的是将每个客户的信息整合成包含其所有特征的文本。这部分需要大量计算时间。因此,我创建了一个脚本来完成这项工作,脚本名为embedding_creation.py。这个脚本收集训练数据集中包含的值,并创建一个由嵌入提供的新数据集。这是脚本的代码:
import pandas as pd # dataframe manipulation
import numpy as np # linear algebra
from sentence_transformers import SentenceTransformer
df = pd.read_csv("data/train.csv", sep = ";")
# -------------------- First Step --------------------
def compile_text(x):
text = f"""Age: {x['age']},
housing load: {x['housing']},
Job: {x['job']},
Marital: {x['marital']},
Education: {x['education']},
Default: {x['default']},
Balance: {x['balance']},
Personal loan: {x['loan']},
contact: {x['contact']}
"""
return text
sentences = df.apply(lambda x: compile_text(x), axis=1).tolist()
# -------------------- Second Step --------------------
model = SentenceTransformer(r"sentence-transformers/paraphrase-MiniLM-L6-v2")
output = model.encode(sentences=sentences,
show_progress_bar=True,
normalize_embeddings=True)
df_embedding = pd.DataFrame(output)
df_embedding
由于这一步骤的理解非常重要。我们逐点来看:
- 第 1 步:为每一行创建文本,其中包含完整的客户/行信息。我们还将其存储在一个 Python 列表中以备后用。请参见下面的图片进行示例。
第一步的图示(图片作者提供)。
- 第 2 步:这是调用转换器的阶段。为此,我们将使用存储在HuggingFace上的模型。这个模型专门用于句子级别的嵌入,而不是Bert 模型,后者专注于令牌和单词级别的编码。要调用模型,你只需提供存储库地址,这里的地址是 “sentence-transformers/paraphrase-MiniLM-L6-v2”。返回给我们的每个文本的数值向量将被归一化,因为 Kmeans 模型对输入的尺度敏感。创建的向量长度为384。我们用这些向量创建一个具有相同列数的数据框。请参见下面的图片:
第二步的图示(图片作者提供),
最终我们从嵌入中获得数据框,这将作为我们 Kmeans 模型的输入。
这一步是最有趣和重要的步骤之一,因为我们已经为即将创建的 Kmeans 模型创建了输入。
创建和评估过程类似于上面所示。为了不使帖子过于冗长,只展示每个点的结果。别担心,所有代码都包含在名为 embedding 的 jupyter notebook中,所以你可以自己复现结果。
此外,应用句子嵌入后得到的数据集已保存为 csv 文件。该 csv 文件名为 embedding_train.csv。在 Jupyter notebook 中,你将看到我们如何访问该数据集并基于此创建我们的模型。
# Normal Dataset
df = pd.read_csv("data/train.csv", sep = ";")
df = df.iloc[:, 0:8]
# Embedding Dataset
df_embedding = pd.read_csv("data/embedding_train.csv", sep = ",")
预处理
我们可以将嵌入视为预处理。
离群点
我们应用了之前介绍的方法来检测离群点,即ECOD。我们创建了一个不包含这些类型点的数据集。
df_embedding_no_out.shape -> (40690, 384)
df_embedding_with_out.shape -> (45211, 384)
建模
首先我们必须找出最佳的簇数。为此我们使用肘部法则。
不同簇数量的肘部得分(图片来源:作者)。
在查看了图表后,我们选择k=5作为簇的数量。
n_clusters = 5
clusters = KMeans(n_clusters=n_clusters, init = "k-means++").fit(df_embedding_no_out)
print(clusters.inertia_)
clusters_predict = clusters.predict(df_embedding_no_out)
评估
下一步是创建我们的 Kmeans 模型,k=5。接下来我们可以获得一些类似于这些的指标:
Davies bouldin score: 1.8095386826791042
Calinski Score: 6419.447089002081
Silhouette Score: 0.20360442824114108
从而看到这些值与之前获得的值非常相似。我们来看一下通过 PCA 分析获得的表示:
PCA 空间和模型创建的簇(图片来源:作者)。
可以看出,簇的区分比传统方法好得多。这是好消息。我们要记住,考虑到 PCA 分析中前三个组件的变异性是很重要的。根据经验,当变异性在 50%左右(3D PCA)时,可以得出比较明确的结论。
PCA 空间和模型创建的簇。还显示了 PCA 前三个组件的变异性(图片来源:作者)。
我们看到 3 个组件的累积变异性为 40.44%,这虽然可以接受,但不理想。
我可以通过修改 3D 表示中点的透明度来直观地查看簇的紧凑程度。这意味着当点在某个空间中聚集时,可以观察到黑点。为了理解我的意思,我展示以下 gif:
plot_pca_3d(df_pca_3d, title = "PCA Space", opacity=0.2, width_line = 0.1)
PCA 空间和模型创建的簇(图片来源:作者)。
从图中可以看出,空间中存在几个点,其中同一簇的点聚集在一起。这表明它们与其他点区分良好,且模型能相当准确地识别它们。
即便如此,可以看出有些簇无法很好地区分(例如:簇 1 和簇 3)。因此,我们进行t-SNE分析,记住这是一种能够考虑复杂多项式关系的降维方法。
t-SNE 空间和模型创建的簇(图片来源:作者)。
显著的改进可以观察到。簇之间没有重叠,点之间的区分很清晰。使用第二种降维方法获得的改进是显著的。我们来看一下 2D 对比:
不同降维方法和模型定义的簇的不同结果(图片来源:作者)。
再次可以看出,t-SNE 中的集群比 PCA 中的集群更为分离和区分。此外,两个方法在质量上的差异比传统 Kmeans 方法时要小。
为了理解我们的 Kmeans 模型依赖于哪些变量,我们进行与之前相同的操作:我们创建一个分类模型(LGBMClassifier)并分析特征的重要性。
模型中变量的重要性(图像来源:作者)。
我们看到这个模型主要基于“婚姻状态”和“职业”变量。另一方面,我们看到有些变量提供的信息不多。在实际情况中,应该创建一个不包含这些信息较少的变量的新版本模型。
Kmeans + 嵌入模型更为优化,因为它需要的变量更少,就能提供良好的预测。好消息!
我们结束于最具揭示性和重要的部分。
管理者和业务方对 PCA、t-SNE 或嵌入并不感兴趣。他们想要的是能够了解主要特征,这里指的是他们客户的主要特征。
为此,我们创建了一张表格,列出我们在每个集群中可以找到的主要特征信息:
发生了一个非常有趣的现象:集群中最频繁的职位是“管理”的有 3 个。在这些集群中,我们发现一个非常特殊的行为,单身的管理者较年轻,已婚者较年长,而离婚者则较老。另一方面,余额的行为则不同,单身人士的平均余额高于离婚人士,已婚人士的平均余额更高。上述情况可以在以下图像中总结:
模型定义的不同客户档案(图像来源:作者)。
这一揭示与现实和社会方面一致。它还揭示了非常具体的客户档案。这就是数据科学的魅力。
结论
结论很明确:
(图像来源:作者)
你必须拥有不同的工具,因为在实际项目中,并非所有策略都有效,你必须有资源来增值。显然,利用 LLMs 创建的模型表现突出。
感谢您的阅读!
如果你觉得我的工作有用,可以订阅 每次发布新文章时接收邮件。
如果你愿意, 在 Linkedin 上关注我 !
掌握 Elasticsearch:强大搜索与精确性的初学者指南 — 第一部分
解锁 Elasticsearch 的力量:深入理解 Elasticsearch,掌握基本的搜索查询,并探索词汇搜索
·
关注 发表于 Towards Data Science ·19 分钟阅读·2023 年 11 月 21 日
--
目录
· 介绍
· 从我们离开的地方开始,Elasticsearch
∘ 示例数据集
∘ 理解 ElasticSearch 查询
∘ 理解响应
∘ 一个基本搜索查询
· 词汇搜索
· 我们当前搜索查询中的问题
∘ 相似词返回不同结果
∘ 对用户需求理解不足
∘ 相似词未被返回
∘ 忽略打字错误
∘ 不同的词组合有不同的含义
· 改进我们的搜索
∘ 提升更相关的字段
∘ 基于函数的提升
∘ 模糊查询
· 结论
介绍
是否曾想过你如何在网上轻松找到完美的鞋子,或在广阔的社交媒体领域中偶然发现朋友的帖子?这都要归功于数字体验中的无名英雄:搜索系统。
回想一下你最近的一次在线购物——无论是时尚的鞋子还是为朋友挑选的贴心书籍。你是如何准确找到你想要的东西的?你很可能是通过搜索栏在大量选项中导航!这就是搜索系统的魔力,它在默默塑造我们的在线体验,并使我们在数字通道中轻松发现完美的商品。在充满选择的世界中,快速而轻松地找到我们想要的东西,证明了强大而直观的搜索系统对于我们喜爱的产品的重要性。
在我最近的 Elasticsearch 探索中(查看我关于其架构和术语的入门介绍),我们揭示了驱动这些发现的引擎。本文深入探讨搜索——导航 ElasticSearch 查询,理解响应,并创建一个基本的查询以设定基础。
我们的目标:建立一个简单的搜索查询,发现问题,并通过实际例子加以改进。加入我们,一起认识到当前搜索系统中的挑战,并在这个数字世界的通道中探索改进的路径。”
从我们停下的地方继续,Elasticsearch
示例数据集
为了演示我们如何改进搜索,让我们设置 Elasticsearch 并加载一些数据。对于这篇文章,我将使用 我在 Kaggle 上找到的新闻数据集。这个数据集非常简单,包含大约 210,000 篇新闻文章,包括它们的标题、简短描述、作者,以及一些我们不太关心的其他字段。我们不需要所有 210,000 个文档,所以我会在 ES 中加载大约 10,000 个文档并开始搜索。
这些是数据集中几个文档的示例 —
[
{
"link": "https://www.huffpost.com/entry/new-york-city-board-of-elections-mess_n_60de223ee4b094dd26898361",
"headline": "Why New York City’s Board Of Elections Is A Mess",
"short_description": "“There’s a fundamental problem having partisan boards of elections,” said a New York elections attorney.",
"category": "POLITICS",
"authors": "Daniel Marans",
"country": "IN",
"timestamp": 1689878099
},
....
]
每个文档代表一篇新闻文章。每篇文章包含一个link
、headline
、一个short_description
、一个category
、authors
、country
(随机值,由我添加)和timestamp
(同样是随机值,由我添加)。
我添加了country
和timestamp
字段,以使接下来的示例更加有趣,所以让我们开始吧!
理解 ElasticSearch 查询
Elasticsearch 查询是用 JSON 编写的。我们不深入探讨所有不同的语法来创建搜索查询,而是从简单的开始,并在此基础上构建。
最简单的全文查询是 match
查询。其思想很简单,您编写一个查询,Elasticsearch 对特定字段执行全文搜索。例如,
GET news/_search
{
"query": {
"match": {
"headline": "robbery"
}
}
}
上述查询找到了所有标题中出现单词“抢劫”的文章。这些是我收到的结果 -
{
"_index" : "news",
"_id" : "RzrouIsBC1dvdsZHf2cP",
"_score" : 7.4066687,
"_source" : {
"link" : "https://www.huffpost.com/entry/guard-cat-hailed-as-hero_n_62e9a515e4b00f4cf2352a6f",
"headline" : "Bandit The 'Guard Cat' Hailed As Hero After Thwarting Would-Be Robbery",
"short_description" : "When at least two people tried to break into a Tupelo, Mississippi, home last week, the cat did everything she could to alert its owner.",
"category" : "WEIRD NEWS",
"authors" : "",
"country" : "US",
"timestamp" : 1693070640
}
},
{
"_index" : "news",
"_id" : "WTrouIsBC1dvdsZHp2wd",
"_score" : 7.4066687,
"_source" : {
"link" : "https://www.huffpost.com/entry/san-francisco-news-crew-security-guard-shot-killed_n_61a2a9d8e4b0ae9a42af278a",
"headline" : "News Crew Security Guard Dies After Being Shot While Helping Robbery Coverage",
"short_description" : "Kevin Nishita was shot in the abdomen while doing his job amid an uptick in organized retail crime.",
"category" : "CRIME",
"authors" : "Daisy Nguyen, AP",
"country" : "US",
"timestamp" : 1692480894
}
}
但是,如果您希望在多个字段上执行全文搜索,该怎么办?您可以通过 multi_match
查询来实现,
GET news/_search
{
"query": {
"multi_match": {
"query": "robbery",
"fields": ["headline", "short_description"]
}
}
}
这执行了类似的操作,但不再仅限于单个字段,而是查看所有文档的 headine
和 short_description
,并对它们进行全文搜索。
理解响应
这是我们上次查询的一个示例响应 -
{
"took" : 5,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : 29.626675,
"hits" : [
{
"_index" : "news",
"_id" : "RzrouIsBC1dvdsZHf2cP",
"_score" : 29.626675,
"_source" : {
"link" : "https://www.huffpost.com/entry/guard-cat-hailed-as-hero_n_62e9a515e4b00f4cf2352a6f",
"headline" : "Bandit The 'Guard Cat' Hailed As Hero After Thwarting Would-Be Robbery",
"short_description" : "When at least two people tried to break into a Tupelo, Mississippi, home last week, the cat did everything she could to alert its owner.",
"category" : "WEIRD NEWS",
"authors" : "",
"country" : "US",
"timestamp" : 1693070640
}
},
.....
]
}
}
took
字段和 timed_out
字段很容易理解,它们简单地表示 Elasticsearch 返回响应所花费的毫秒数,以及查询是否超时。
_shards
字段告诉我们在此搜索操作中涉及了多少分片,其中多少返回成功,多少失败,以及多少被跳过。
hits
字段包含从搜索返回的文档。每个文档根据其与搜索相关性而得分。hits 字段还包含一个 total
字段,提到返回的文档总数,以及文档的最大分数。
最后,在嵌套字段 hits
中,我们获取所有相关文档,包括它们的 _id
和它们的 score
。文档按它们的分数排序。
一个基本的搜索查询
让我们开始构建我们的搜索查询。我们可以从一个简单的查询开始,并剖析其中的问题 -
GET news/_search
{
"query": {
"multi_match": {
"query": "robbery",
"fields": ["headline", "short_description"]
}
}
}
这是一个非常简单的查询,它只是找到所有包含单词“抢劫”的文档,无论其出现在哪些字段中,即 headline
或 short_description
。
它返回了一些结果,我们可以看到它们中都有单词“抢劫”。
{
.....
"hits" : {
"total" : {
"value" : 3,
"relation" : "eq"
},
"max_score" : 8.164355,
"hits" : [
{
"_index" : "news",
"_id" : "hjrouIsBC1dvdsZHgWdm",
"_score" : 8.164355,
"_source" : {
"link" : "https://www.huffpost.com/entry/lady-gaga-dog-walker-reward_n_62d82efee4b000da23fafad7",
"headline" : "$5K Reward For Suspect In Shooting Of Lady Gaga’s Dog Walker",
"short_description" : "One of the men involved in the violent robbery was mistakenly released from custody in April and remains missing.",
"category" : "U.S. NEWS",
"authors" : "STEFANIE DAZIO, AP",
"country" : "IN",
"timestamp" : 1694863246
}
},
{
"_index" : "news",
"_id" : "RzrouIsBC1dvdsZHf2cP",
"_score" : 7.4066687,
"_source" : {
"link" : "https://www.huffpost.com/entry/guard-cat-hailed-as-hero_n_62e9a515e4b00f4cf2352a6f",
"headline" : "Bandit The 'Guard Cat' Hailed As Hero After Thwarting Would-Be Robbery",
"short_description" : "When at least two people tried to break into a Tupelo, Mississippi, home last week, the cat did everything she could to alert its owner.",
"category" : "WEIRD NEWS",
"authors" : "",
"country" : "US",
"timestamp" : 1693070640
}
},
{
"_index" : "news",
"_id" : "WTrouIsBC1dvdsZHp2wd",
"_score" : 7.4066687,
"_source" : {
"link" : "https://www.huffpost.com/entry/san-francisco-news-crew-security-guard-shot-killed_n_61a2a9d8e4b0ae9a42af278a",
"headline" : "News Crew Security Guard Dies After Being Shot While Helping Robbery Coverage",
"short_description" : "Kevin Nishita was shot in the abdomen while doing his job amid an uptick in organized retail crime.",
"category" : "CRIME",
"authors" : "Daisy Nguyen, AP",
"country" : "US",
"timestamp" : 1692480894
}
}
]
}
}
词法搜索
到目前为止,我们所涉及的被称为“词法搜索”。在这种类型的搜索中,系统寻找文档中给定单词或短语的精确匹配。实质上,当用户输入“抢劫”时,我们的搜索查询识别出所有包含确切术语“抢劫”的文档。尽管这种方法最初可能看起来直观,但它的局限性很快就会显现出来,正如我们马上就会发现的那样。
我们当前搜索查询中的问题
相似的词语会返回不同的结果
让我们举几个例子,看看当用户搜索“被抢劫”时我们会得到什么 -
GET news/_search
{
"query": {
"multi_match": {
"query": "robbed",
"fields": ["headline", "short_description"]
}
}
}
这些是我收到的结果 -
{
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : 7.9044275,
"hits" : [
{
"_index" : "news",
"_id" : "YTrouIsBC1dvdsZHh2jf",
"_score" : 7.9044275,
"_source" : {
"link" : "https://www.huffpost.com/entry/multiple-guns-robbery-wellston-market-missouri_n_62994cbee4b05fe694f296ad",
"headline" : "Man Robbed Of Assault Rifle At Gunpoint Opens Fire With Second Gun",
"short_description" : "The accused robber was struck multiple times, and two bystanders were wounded in the St. Louis shootout.",
"category" : "CRIME",
"authors" : "Mary Papenfuss",
"country" : "IN",
"timestamp" : 1691458552
}
},
{
"_index" : "news",
"_id" : "YDrouIsBC1dvdsZH73UQ",
"_score" : 7.8303137,
"_source" : {
"link" : "https://www.huffpost.com/entry/michigan-militia-training-video-gretchen-whitmer_n_5f8b6e26c5b6dc2d17f78e0a",
"headline" : "Chilling Training Videos Released Of Militia Men Charged In Michigan Gov. Kidnap Plot",
"short_description" : "\"I’m sick of being robbed and enslaved by the state ... they are the enemy. Period,\" says one suspect in a video.",
"category" : "POLITICS",
"authors" : "Mary Papenfuss",
"country" : "IN",
"timestamp" : 1692613291
}
}
]
}
为了简单起见,这些是我收到的文档的标题 -
1\. "Man Robbed Of Assault Rifle At Gunpoint Opens Fire With Second Gun"
2\. "Chilling Training Videos Released Of Militia Men Charged In Michigan Gov. Kidnap Plot"
这两篇文档中都包含“被抢劫”一词,无论是在标题还是描述中。但是如果用户搜索的是“抢劫”,那么我们将会在结果中看到完全不同的文档 -
[
{
"_index" : "news",
"_id" : "hjrouIsBC1dvdsZHgWdm",
"_score" : 8.164355,
"_source" : {
"link" : "https://www.huffpost.com/entry/lady-gaga-dog-walker-reward_n_62d82efee4b000da23fafad7",
"headline" : "$5K Reward For Suspect In Shooting Of Lady Gaga’s Dog Walker",
"short_description" : "One of the men involved in the violent robbery was mistakenly released from custody in April and remains missing.",
"category" : "U.S. NEWS",
"authors" : "STEFANIE DAZIO, AP",
"country" : "IN",
"timestamp" : 1694863246
}
},
{
"_index" : "news",
"_id" : "YTrouIsBC1dvdsZHh2jf",
"_score" : 8.079888,
"_source" : {
"link" : "https://www.huffpost.com/entry/multiple-guns-robbery-wellston-market-missouri_n_62994cbee4b05fe694f296ad",
"headline" : "Man Robbed Of Assault Rifle At Gunpoint Opens Fire With Second Gun",
"short_description" : "The accused robber was struck multiple times, and two bystanders were wounded in the St. Louis shootout.",
"category" : "CRIME",
"authors" : "Mary Papenfuss",
"country" : "IN",
"timestamp" : 1691458552
}
},
{
"_index" : "news",
"_id" : "RzrouIsBC1dvdsZHf2cP",
"_score" : 7.4066687,
"_source" : {
"link" : "https://www.huffpost.com/entry/guard-cat-hailed-as-hero_n_62e9a515e4b00f4cf2352a6f",
"headline" : "Bandit The 'Guard Cat' Hailed As Hero After Thwarting Would-Be Robbery",
"short_description" : "When at least two people tried to break into a Tupelo, Mississippi, home last week, the cat did everything she could to alert its owner.",
"category" : "WEIRD NEWS",
"authors" : "",
"country" : "US",
"timestamp" : 1693070640
}
},
{
"_index" : "news",
"_id" : "WTrouIsBC1dvdsZHp2wd",
"_score" : 7.4066687,
"_source" : {
"link" : "https://www.huffpost.com/entry/san-francisco-news-crew-security-guard-shot-killed_n_61a2a9d8e4b0ae9a42af278a",
"headline" : "News Crew Security Guard Dies After Being Shot While Helping Robbery Coverage",
"short_description" : "Kevin Nishita was shot in the abdomen while doing his job amid an uptick in organized retail crime.",
"category" : "CRIME",
"authors" : "Daisy Nguyen, AP",
"country" : "US",
"timestamp" : 1692480894
}
}
]
1\. "$5K Reward For Suspect In Shooting Of Lady Gaga's Dog Walker"
2\. "Man Robbed Of Assault Rifle At Gunpoint Opens Fire With Second Gun"
3\. "Bandit The 'Guard Cat' Hailed As Hero After Thwarting Would-Be Robbery"
4\. "News Crew Security Guard Dies After Being Shot While Helping Robbery Coverage"
简而言之,如果用户搜索“抢劫”和“被抢劫”,我们会得到不同的结果。如果用户搜索了这些(或任何与“抢劫”相关的内容),我们应该显示所有包含“抢劫”不同形式的文档(称为“屈折”形式)的文档。
对用户需求的理解不足
“设计师的目标是倾听、观察、理解、同情、共鸣、综合,并汲取洞察力,使他或她能够使不可见的变得可见。” — 希尔曼·柯蒂斯
我们正在尝试检索与用户查询相符的文档,这一任务超出了用户输入的范围。通过深入探讨附加参数,我们可以获得对用户偏好和需求的更丰富理解。
例如,当用户搜索新闻时,他们的兴趣可能不仅仅在于相关性;新闻文章的时效性往往也很重要。为了提高我们的搜索精度,我们可以微调评分机制。
此外,我们的文章中还有一个location
字段。该字段表示新闻的地理来源,提供了进一步优化结果的机会。我们可以利用这一点来提高来自用户所在国家的文章的排名。
相似词未被返回
由于我们仅返回与用户查询完全匹配的文章,我们很可能会错过包含类似词的相关文档。例如,如果我搜索“盗窃”,我会得到以下文章,
[
{
"_index" : "news",
"_id" : "dzrouIsBC1dvdsZHiGh1",
"_score" : 8.079888,
"_source" : {
"link" : "https://www.huffpost.com/entry/ap-us-church-theft-beheaded-statue_n_629504abe4b0933e7376f2fa",
"headline" : "Angel Statue Beheaded At Church, $2 Million Relic Stolen",
"short_description" : "The New York City church says its stolen 18-carat gold relic was guarded by its own security system and is irreplaceable due to its historical and artistic value.",
"category" : "CRIME",
"authors" : "Michael R. Sisak, AP",
"country" : "IN",
"timestamp" : 1699477455
}
},
{
"_index" : "news",
"_id" : "ATrouIsBC1dvdsZHrG2n",
"_score" : 7.4066687,
"_source" : {
"link" : "https://www.huffpost.com/entry/joseph-sobolewski-charge-dropped-mountain-dew-felony_n_617a15e0e4b0657357447ee2",
"headline" : "Prosecutors Drop Felony Charge Against Man Accused Of 43 Cent Soda Theft",
"short_description" : "Joseph Sobolewski faced up to seven years in prison after paying $2 for a Mountain Dew that cost $2.29 plus tax.",
"category" : "U.S. NEWS",
"authors" : "Nick Visser",
"country" : "IN",
"timestamp" : 1698883200
}
},
{
"_index" : "news",
"_id" : "ZDrouIsBC1dvdsZH73Uq",
"_score" : 7.153779,
"_source" : {
"link" : "https://www.huffpost.com/entry/missing-lemur-found_n_5f8b2c33c5b6dc2d17f76bdb",
"headline" : "'There's A Lemur!' 5-Year-Old Helps Crack San Francisco Zoo Theft Case",
"short_description" : """The arthritic, 21-year-old lemur is "agitated" but safe, zoo staff say.""",
"category" : "U.S. NEWS",
"authors" : "",
"country" : "IN",
"timestamp" : 1698560597
}
}
]
“抢劫”一词可能与“盗窃”一词意义不同,但它仍然是一个相关的词,用户可能也对包含“抢劫”一词的文章感兴趣(虽然相关性评分低于包含用户搜索的确切词的文档)。
盗窃的类似词可能有很多,每个词的相似度不同。例如,“盗窃”可能与“商店盗窃”更相似,而与“入室盗窃”的相似度较低。但在某些上下文中,它们可能是同义词,并且具有一定的相关性,尽管不如查询中的确切词汇“盗窃”相关。
我们当前的搜索没有考虑文档和查询中的词语相似性。如果用户搜索“盗窃”,只会返回包含“盗窃”一词的文章,而我们还应该返回包含与“盗窃”类似词汇(如“入室盗窃”或“抢劫”)的文章。
错别字被忽略
“如果用户不能使用它,那它就不起作用。” — 苏珊·德雷
另一个问题是,用户的任何错别字会导致返回空结果。我们知道用户可能会不小心输入错别字,我们不希望返回空结果。例如,在 Google 新闻上搜索“robbey”仍然会返回与“抢劫”相关的结果。
不同的词语组合有不同的含义
让我们来看一个例子,假设用户提出了这个查询 —
GET news/_search
{
"query": {
"multi_match": {
"query": "new jersey covid virus"
}
}
}
对于你和我来说,显然用户想要搜索与“covid”或“virus”相关的新闻,“New Jersey”。但对于我们的搜索引擎来说,每个词的含义是相同的,它无法理解词语的顺序是否重要(例如,“New”和“Jersey”在“New Jersey”中的顺序)。
让我们看一下前三个结果,
{
"_index" : "news",
"_id" : "0jrouIsBC1dvdsZH03FH",
"_score" : 15.199991,
"_source" : {
"link" : "https://www.huffpost.com/entry/covid-new-york-new-jersey-trend_n_60611769c5b6531eed0621da",
"headline" : "Virus Fight Stalls In Early Hot Spots New York, New Jersey",
"short_description" : "New Jersey has been reporting about 647 new cases for every 100,000 residents over the past 14 days. New York has averaged 548.",
"category" : "U.S. NEWS",
"authors" : "Marina Villeneuve and Mike Catalini, AP",
"country" : "US",
"timestamp" : 1697056489
}
},
{
"_index" : "news",
"_id" : "zzrouIsBC1dvdsZH23Ig",
"_score" : 12.708103,
"_source" : {
"link" : "https://www.huffpost.com/entry/new-variants-raise-worry-about-covid-19-virus-reinfections_n_602193e6c5b689330e31dcc4",
"headline" : "New Variants Raise Worry About COVID-19 Virus Reinfections",
"short_description" : "Scientists discovered a new version of the virus in South Africa that’s more contagious and less susceptible to certain treatments.",
"category" : "WORLD NEWS",
"authors" : "Marilynn Marchione, AP`",
"country" : "IN",
"timestamp" : 1693063095
}
},
{
"_index" : "news",
"_id" : "fTrouIsBC1dvdsZH6HQF",
"_score" : 11.707885,
"_source" : {
"link" : "https://www.huffpost.com/entry/new-york-covid-19-religious-gatherings_n_5fbf42b8c5b66bb88c6430ac",
"headline" : "Supreme Court Blocks New York COVID-19 Restrictions On Religious Gatherings",
"short_description" : "It was the first major decision since Justice Amy Coney Barrett joined the nation's highest court.",
"category" : "POLITICS",
"authors" : "Lawrence Hurley, Reuters",
"country" : "IN",
"timestamp" : 1693362371
}
},
如果你仔细看上面的结果,你会注意到第二个结果,“新变种引发对 COVID-19 病毒再感染的担忧”,与新泽西毫无关系。事实上,在阅读描述后,它似乎与南非的 COVID-19 感染更相关!
这是因为“COVID”、“virus”和“New”这些词是文档的一部分,因此文档得分较高。然而,这与用户查询完全不相关。我们的搜索系统无法理解“New”和“Jersey”应被视为一个单独的词组。
改进我们的搜索。
提升更相关的字段。
“言辞有重量,因此如果你要说重要的事情,请确保选择正确的词。” — Lang Leav
我们可以决定提升某些字段或某些值,这些可能更有助于理解文章的内容。例如,文章的标题可能比文章的描述更有意义。
让我们来看一个示例查询。假设用户试图搜索选举,这将是我们的 Elasticsearch 查询 —
GET news/_search
{
"query": {
"multi_match": {
"query": "elections",
"type": "most_fields",
"fields": ["short_description", "headline"]
}
}
}
这些是我们得到的结果 -
{
"_index" : "news",
"_id" : "qDrouIsBC1dvdsZHwW-a",
"_score" : 15.736175,
"_source" : {
"link" : "https://www.huffpost.com/entry/new-york-city-board-of-elections-mess_n_60de223ee4b094dd26898361",
"headline" : "Why New York City’s Board Of Elections Is A Mess",
"short_description" : "“There’s a fundamental problem having partisan boards of elections,” said a New York elections attorney.",
"category" : "POLITICS",
"authors" : "Daniel Marans",
"country" : "IN",
"timestamp" : 1689878099
}
},
{
"_index" : "news",
"_id" : "8zrouIsBC1dvdsZH63Si",
"_score" : 7.729385,
"_source" : {
"link" : "https://www.huffpost.com/entry/20-funniest-tweets-from-women-oct-31-nov-6_n_5fa209fac5b686950033b3e7",
"headline" : "The 20 Funniest Tweets From Women This Week (Oct. 31-Nov. 6)",
"short_description" : "\"Hear me out: epidurals, but for elections.\"",
"category" : "WOMEN",
"authors" : "Caroline Bologna",
"country" : "IN",
"timestamp" : 1694723723
}
},
{
"_index" : "news",
"_id" : "zzrouIsBC1dvdsZH8nVe",
"_score" : 7.353842,
"_source" : {
"link" : "https://www.huffpost.com/entry/childrens-books-elections-voting_l_5f728844c5b6f622a0c368a1",
"headline" : "25 Children's Books That Teach Kids About Elections And Voting",
"short_description" : "Parents can use these stories to educate their little ones about the American political process.",
"category" : "PARENTING",
"authors" : "Caroline Bologna",
"country" : "IN",
"timestamp" : 1697290393
}
},
如果你看第二篇文章“本周 20 条最有趣的女性推文(10 月 31 日至 11 月 6 日)”,你会发现它似乎与选举无关。然而,由于描述中出现了“election”一词,Elasticsearch 认为这是一个相关的结果。也许我们还有改进的空间。直觉上来说,与用户查询匹配的标题相符的文章更相关。为了实现这一点,我们可以指示 Elasticsearch 提升heading
字段,从而在评分计算中赋予它更大的重要性。
在我们的查询中,这很简单 -
GET news/_search
{
"query": {
"multi_match": {
"query": "elections",
"type": "most_fields",
"fields": ["headline⁴", "short_description"]
}
}
}
注意我在fields
中放置的heading⁴
。这意味着“heading”字段被提升了 4 倍。现在让我们看看结果,
{
"_index" : "news",
"_id" : "qDrouIsBC1dvdsZHwW-a",
"_score" : 37.7977,
"_source" : {
"link" : "https://www.huffpost.com/entry/new-york-city-board-of-elections-mess_n_60de223ee4b094dd26898361",
"headline" : "Why New York City’s Board Of Elections Is A Mess",
"short_description" : "“There’s a fundamental problem having partisan boards of elections,” said a New York elections attorney.",
"category" : "POLITICS",
"authors" : "Daniel Marans",
"country" : "IN",
"timestamp" : 1689878099
}
},
{
"_index" : "news",
"_id" : "zzrouIsBC1dvdsZH8nVe",
"_score" : 29.415367,
"_source" : {
"link" : "https://www.huffpost.com/entry/childrens-books-elections-voting_l_5f728844c5b6f622a0c368a1",
"headline" : "25 Children's Books That Teach Kids About Elections And Voting",
"short_description" : "Parents can use these stories to educate their little ones about the American political process.",
"category" : "PARENTING",
"authors" : "Caroline Bologna",
"country" : "IN",
"timestamp" : 1697290393
}
},
{
"_index" : "news",
"_id" : "_jrouIsBC1dvdsZH3HKZ",
"_score" : 29.415367,
"_source" : {
"link" : "https://www.huffpost.com/entry/shirley-weber-first-black-california-secretary-of-state_n_6014651ec5b6aa4bad33e87b",
"headline" : "Shirley Weber Sworn In As California's First Black Elections Chief",
"short_description" : "She vacates her Assembly seat to be the new secretary of state, replacing Alex Padilla, who last week became the first Latino U.S. senator for California.",
"category" : "POLITICS",
"authors" : "Sarah Ruiz-Grossman",
"country" : "IN",
"timestamp" : 1697300728
}
},
{
"_index" : "news",
"_id" : "NzrouIsBC1dvdsZHnWvd",
"_score" : 26.402336,
"_source" : {
"link" : "https://www.huffpost.com/entry/josh-hawley-democrats-dont-accept-elections_n_61ea1949e4b01440a689bedc",
"headline" : "Sen. Josh Hawley Says, Without Irony, That Democrats Don't Accept Elections They Lose",
"short_description" : "The Missouri Republican led the charge on Jan. 6 to object to Joe Biden's win -- right after he saluted pro-Trump protesters gathering at the U.S. Capitol.",
"category" : "POLITICS",
"authors" : "Josephine Harvey",
"country" : "IN",
"timestamp" : 1692046727
}
},
现在我们可以看到所有前几个结果的标题中都包含“election”一词,因此返回的文章更相关。
基于函数的提升
虽然我们已经提升了某些字段,但我们也希望根据用户在搜索新闻时想要的内容引入两种新的提升类型。
-
我们希望提升用户所在国家的文章。我们不仅仅想基于国家进行过滤,因为这可能导致无关紧要的结果出现在顶部,但我们也不希望完全忽略它。简而言之,我们希望给用户所在国家的文章更多的权重。
-
我们希望提升最新新闻的排名。我们不仅仅是希望根据时效性进行排序,因为这也可能导致无关的结果出现在顶部。相反,我们希望将时效性与相关性进行平衡。
让我们看看如何实现这一点。
在 Elasticsearch 中,我们可以使用function_score
查询来应用自定义评分函数,包括提升评分。function_score
查询允许你根据各种函数修改文档的分数。简而言之,我们可以根据条件提升某些文档的评分。
让我们从提升用户所在国家的权重开始。假设用户的国家是“US”,然后将其插入到发送给 Elasticsearch 的查询中。为此,我们需要添加一个function_score
块,它允许将自定义评分函数应用于查询结果。我们可以为给定的查询定义多个functions
,指定匹配文档的条件和提升值。
我们可以定义一个函数来提升用户所在国家的权重 —
{
"filter": {
"term": {
"country.keyword": "US"
}
},
"weight": 2
}
这将提升国家为“US”的文章的分数 2 倍。
接下来,让我们尝试将最新的新闻置顶。我们可以通过使用field_value_factor
来实现这一点。field_value_factor
函数允许我们使用文档中的一个字段来影响其分数,这正是我们所需要的。让我们看看效果如何 —
{
"field_value_factor": {
"field": "timestamp",
"factor": 2
}
}
factor
术语指定了指定字段的值应如何影响分数的乘数或因子。使用这个函数,具有较新时间戳的文档将被给予更高的分数。
我们的完整查询变成 —
GET news/_search
{
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "covid",
"fields": ["headline⁴", "short_description"]
}
},
{
"function_score": {
"query": {
"multi_match": {
"query": "covid",
"fields": ["headline⁴", "short_description"]
}
},
"functions": [
{
"filter": {
"term": {
"country.keyword": "US"
}
},
"weight": 2
}, {
"field_value_factor": {
"field": "timestamp",
"factor": 2
}
}
]
}
}
]
}
}
}
现在,最新的文档和来自用户所在国家的文档将获得更高的分数。我们可以通过配置weight
和factor
字段的值来调整这种平衡。
模糊查询
接下来,让我们修正搜索查询中的拼写错误。
在 Elasticsearch 中,我们可以执行模糊搜索,以检索即使存在拼写或字符的轻微变化也匹配指定术语的文档。为此,我们只需在查询中添加一个fuzziness
字段。我们的最终查询变成 —
GET news/_search
{
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "covi",
"fields": ["headline⁴", "short_description"],
"fuzziness": 1
}
},
{
"function_score": {
"query": {
"multi_match": {
"query": "covi",
"fields": ["headline⁴", "short_description"],
"fuzziness": 1
}
},
"functions": [
{
"filter": {
"term": {
"country.keyword": "US"
}
},
"weight": 2
}, {
"field_value_factor": {
"field": "timestamp",
"factor": 2
}
}
]
}
}
]
}
}
}
拼写纠正不仅仅是简单地添加fuzziness
。如果你想了解更多,请查看这篇博客文章。
结论
在这篇博客文章中,我们深入探讨了 Elasticsearch 的细节,从实际操作一个样本数据集和构建搜索查询的基础知识开始。我们揭开了 Elasticsearch 响应的神秘面纱,并通过一个基本的搜索查询,为有效探索奠定了基础。
在我们探索词汇搜索的过程中,我们认识到当前搜索方法的一些怪癖。为了解决这些挑战,我们引入了提升和模糊度——这些都是微调搜索和处理现实世界数据复杂性的有用工具。
在我们结束这里的讨论时,视此为我们追求搜索卓越之旅中的一次短暂停留。在下一部分,我们将深入探讨解决当前搜索方法中特定问题的高级策略。准备好迎接语义搜索的迷人世界吧,在这里,重点不再仅仅是匹配关键词,而是理解其背后的含义,从而为更直观、更具上下文意识的搜索体验铺平道路。准备好将你的 Elasticsearch 冒险提升到一个新的水平吧!
喜欢这次 Elasticsearch 的旅程吗?在 Medium 上关注我,获取更多文章。想要更快的知识点(例如我正在阅读的内容、速查表等),在 LinkedIn 上关注我,获取定期的短内容(例如,在阅读 Elasticsearch 时,我简要讨论了一个特定的评分函数 tf-idf 的工作原理,在这篇5 分钟的帖子中)。让我们在技术和数据的探索中保持联系吧!
掌握 Apache Airflow 中的ExternalTaskSensor:如何计算执行增量
外部任务传感器阻止坏数据在数据管道中流入下游。利用它们来创建可靠的数据基础设施。
·发表于Towards Data Science ·15 分钟阅读·2023 年 5 月 8 日
--
外部任务传感器就像守门员——它们阻止坏数据流入下游。图片由 Freepik 提供。
协调数据管道是一项微妙的工作。在数据管道中,我们可能会有数千个任务同时运行,并且这些任务通常相互依赖。如果不小心,单点故障可能会产生多米诺骨牌效应,流入下游并搞乱整个管道。
Apache Airflow 引入了外部任务传感器,以解决这些问题。虽然这是一个极其强大的功能,但也伴随着一定的复杂性。
在这篇介绍性文章中,我希望解开一些关于外部任务传感器的困惑,并展示我们如何使用它来增强数据管道的可靠性——让传感器变得有意义!
-
我们为什么需要外部任务传感器?
-
外部任务传感器有什么作用?
-
我们如何创建外部任务传感器?
-
什么是执行增量和执行日期函数?
– 如何计算执行增量?
– 如何计算执行日期函数?
-
我们如何将外部任务传感器融入到我们的 DAG 中?
-
附加信息:Airflow 中的日期概念
我们为什么需要外部任务传感器?
认识 Jamie,她是 Airflow Bakery 的一个新手厨师。她刚刚加入。她唯一的责任是每小时制作一批新的饼干面团。
杰米的职责以“DAG”格式展示。 Chef (F) 图标由 Freepik 提供。
然后我们有戈登·丹姆斯,饼干大师。戈登从杰米那里拿到面团,并将其制作成获奖的饼干。
戈登的职责以“DAG”格式展示。 Chef (M) 图标由 Freepik 提供。
一天,戈登急忙去找最新鲜的面团来烘焙饼干。但当他咬一口时,哎呀!“坏”只是个轻描淡写的说法。戈登很快发现根本原因是面团陈旧,是一周前剩下的。
戈登显得很沮丧,把饼干扔进了垃圾桶。调整好情绪后,他慢慢转向杰米,问道:“为什么面团不新鲜?”
“我不得不停止制作它们,厨师。原料有问题,”杰米回答道,试图在戈登的愤怒面前保持冷静。不幸的是,坏饼干已经送给了客户,他们不再信任面包店的食品质量。
这次小小的偏离是关于验证数据源新鲜度重要性的警示故事。在故事中,戈登的成功依赖于杰米,但他们独立工作而没有互相沟通。他们“相信”对方会完美地完成自己的工作。但正如任何数据从业者所知道的那样,数据管道中一切可能出错的事情都会出错。
理想情况下,戈登应该检查杰米是否最近制作了面团。一旦确认,这意味着面团是新鲜的,他可以继续烘焙饼干。否则,停止烘焙并找出问题所在。
你看,戈登需要的是一个外部任务传感器。
外部任务传感器的作用是什么?
外部任务传感器检查其他人是否完成了分配的任务。它感知到外部任务的完成,因此得名。
在 Airflow 的背景下,杰米和戈登是 DAG。他们有具体的任务需要完成。
当我们添加一个外部任务传感器时,它会成为协调两个独立 DAG 之间的中介。传感器会在特定时间检查杰米是否完成了她的任务。
如果杰米成功完成了她的任务,传感器将通知戈登,以便他可以继续进行下游任务。
外部任务传感器 — check_dough() 在验证 make_dough() 成功运行后返回成功。 Chef (F) 和 Chef (M) 图标由 Freepik 提供。
如果杰米未能完成她的任务,传感器会阻止戈登执行任何依赖于失败任务的任务。
外部任务传感器 — check_dough() 在验证 make_dough() 未成功运行后返回失败。厨师 (F) 和 厨师 (M) 图标由 Freepik 提供。
拥有这层额外的验证本质上可以防止陈旧数据进一步传递到下游,并污染我们管道中的其他部分,导致数据脏乱且不准确。
我们如何创建外部任务传感器?
Airflow 使创建外部任务传感器变得非常简单——只需导入它们即可。语法大致如下:
from airflow.sensors.external_task import ExternalTaskSensor
ext_task_sensor = ExternalTaskSensor(
dag=gordon_tasks,
task_id='check_dough_freshness',
external_dag_id='jamie_tasks',
external_task_id='make_new_dough',
email=['gordon.damnsie@gmail.com', 'jamie@gmail.com'],
execution_delta=timedelta(minutes=30),
# execution_date_fn=my_function,
timeout=1800,
poke_interval=300,
mode='reschedule'
)
它们的含义如下:
-
**dag**
是当前的 DAG 对象。由于 Gordon 是想检查 Jamie 是否做了面团的人,因此这应该指向 Gordon 的 DAG。 -
**task_id**
是这个外部任务传感器的唯一名称。 -
**external_dag_id**
是你要检查的 DAG 的名称。在这种情况下,是 Jamie 的 DAG。 -
**external_task_id**
是你要检查的具体任务的名称。理想情况下,我们应始终指定这一点。否则,传感器将检查 整个 DAG 的完成情况,而不仅仅是一个特定任务。换句话说,Gordon 不会做任何事,直到 Jamie 完成切洋葱、洗碗和补充食品储备,即使我们只想知道她是否做了面团。更糟的是,如果这些无关任务中的任何一个失败,传感器将不必要地暂停整个管道。 -
**email**
是你希望 Airflow 在外部任务传感器失败时通知的人员名单。请记住,要使其正常工作,你需要在 Airflow 配置文件中正确配置 SMTP 设置。 -
**execution_delta**
可以说是外部任务传感器中最令人困惑但也是最重要的部分。因此,我为此专门设置了一个部分 如下。继续滚动! -
**execution_date_fn**
和执行增量非常相似。我们一次只能使用其中一个。有时候,使用这个比执行增量更方便。我也将其单独列出 如下。 -
**timeout**
限制传感器能够存在的时间。当我们创建一个传感器时,它会占用一个工作槽,从而消耗资源。如果目标任务永远无法完成,这些传感器将无限期地继续检查,同时占用工作槽。随着时间的推移,我们可能会遇到一个 传感器死锁,所有工作槽都被无用的传感器占用,任务无法再运行。因此,设置检查的最大时间限制是最佳实践。 -
**poke_interval**
是在传感器再次检查之前的持续时间,如果之前的检查失败。其理由是我们不希望传感器像疯子一样过度检查,因为这会给服务器增加不必要的负担。另一方面,检查过于频繁意味着传感器会比必要时等待更长时间,从而延迟管道。诀窍是根据外部任务的预期运行时间找到最佳点。 -
**mode**
是我们希望传感器的行为方式。它可以设置为“poke”或“reschedule”。当设置为“poke”时,传感器在失败时进入休眠状态,并在下一个 poke 间隔时重新唤醒尝试。这就像处于待机模式。传感器将更具反应性,但由于它处于待机状态,工人插槽在整个过程中都会被占用。
当设置为“reschedule”时,传感器会检查一次。如果检查失败,传感器会安排在稍后的时间进行另一次检查,但现在会终止自身,释放工人插槽。如果 poke 间隔大于 60 秒,Airflow 推荐使用“reschedule”。
好了,这就是我们需要了解的外部任务传感器的所有参数。虽然这个列表并不详尽,但了解这 10 个参数对于我们在几乎所有用例中正确设置外部任务传感器已经足够了。
为了完整性,我将包括 Airflow 官方文档 供那些渴望更详细了解的人参考。
执行时间差和执行日期函数是什么?
在上面的部分中,我略过了这两个参数,因为它们可以说是外部任务传感器中最臭名昭著、最烦人且最令人困惑的部分。但我认为现在是时候解决它们了。
那么 execution_delta
和 execution_date_fn
是什么呢?
基于我们的类比,external_task_id
告诉传感器检查 Jamie 是否完成了 make_dough()
任务。但她做面团的频率很高——每小时一次。我们是在检查她在过去一小时、昨天还是上周是否做过面团?
这种模糊性使得外部任务传感器感到困惑,这也是 Airflow 提供了两种方式来传达这些信息的原因。execution_delta
和 execution_date_fn
都旨在告知传感器任务的具体时间。
-
execution_delta
以 相对 时间为基础,举例来说:“Jamie 是否在 30 分钟前烘烤过?” 它接受一个datetime.timedelta
对象作为参数,例如:datetime.timedelta(minutes=30)
。 -
execution_date_fn
以 绝对 时间为基础,举例来说:“Jamie 是否在 2023 年 5 月 3 日下午 4:30 烘烤过?” 它接受一个可调用的 Python 函数作为参数。这个函数应该返回我们想要检查的任务的 执行日期,例如:datetime.datetime(year=2023,month=5,day=3,hour=4,minute=30)
。
由于这两者传达了相同的信息,Airflow 仅允许我们使用其中一个,而不能同时使用两个。
我通常使用execution_delta
作为默认选择。但在某些情况下,计算execution_delta
可能过于复杂。在这种情况下,我会使用execution_date_fn
。
如何计算 execution_delta?
词语execution_delta
是delta(即差异)和execution dates(即任务的上次运行时间)的缩写。
execution_delta 的公式。图片由作者提供。
我想强调这里的关键词——“previous”。
有些人可能会想… 为什么 Airflow 需要之前运行的时间差,而不是当前运行的时间差?这在我刚开始使用 Airflow 时曾让我非常困惑。
原来有一个完全合理的原因。然而,我不想偏离当前话题,因此我会在后面的部分(这里)中包含它。现在,我们就按原样接受公式,并看看如何应用它。
假设 Jamie 每小时做一次面团(例如:13:00 pm, 14:00 pm, 15:00 pm,…)。Gordon 也每小时做一次饼干,但他在每小时的第 30 分钟做(例如:13:30 pm, 14:30 pm, 15:30 pm,…)。
在 14:30 pm 整点,Gordon 准备好烘烤饼干。在开始之前,他需要检查 Jamie 是否最近做了新鲜面团。make_dough()
的最新运行时间将是 14:00 pm。
这条时间序列展示了 Jamie 和 Gordon 之间的任务依赖关系。Gordon 总是检查 Jamie 是否在半小时前完成了任务。Chef (F) 和 Chef (M)图标由 Freepik 提供。
由于 Gordon 和 Jamie 的任务是按小时安排的,他们在 14:30 pm 运行时的执行日期(即之前的运行)将是…
-
Gordon 的执行时间 = 14:30 pm — 1 小时 = 13:30 pm
-
Jamie 的执行时间 = 14:00 pm — 1 小时 = 13:00 pm
我们可以将这些值代入公式中,瞧!
execution_delta
的结果是datetime.timedelta(minute=30)对于一次特定运行
。图片由作者提供。
你可以对任务的不同运行进行相同的计算,以获取各自的execution_delta
。
在计算 execution delta 时,将其以这种格式排列是有帮助的。我们想要计算多个运行的执行 delta,而不仅仅是一个,以确保它们都相同!图片由作者提供。
在这个(挑选的)示例中,所有的execution_delta
都恰好相同。我们可以将其传递给我们的 External Task Sensor,一切都会正常工作。
from airflow.sensors.external_task import ExternalTaskSensor
ext_task_sensor = ExternalTaskSensor(
dag=gordon_tasks,
task_id='check_dough_freshness',
external_dag_id='jamie_tasks',
external_task_id='make_new_dough',
email=['gordon.damnsie@gmail.com', 'jamie@gmail.com'],
execution_delta=timedelta(minutes=30), # Pass the execution delta here
timeout=1800,
poke_interval=300,
mode='reschedule'
)
但是-!
execution_delta
有时可能不同。这通常发生在两个 dags 的调度间隔不同(例如:每日 vs 每周,每日 vs 每月,…)时。
例如,假设 Jamie 每周 一次 在星期天 14:00 制作面团,而 Gordon 每天 一次 在 14:30 制作饼干。
Jamie 的任务和 Gordon 的传感器之间的箭头表示执行增量。执行增量在一周内变得更长,直到在星期天再次重置。Chef (F) 和 Chef (M) 图标由 Freepik 提供。
如果我们进行相同的计算,你会看到每次运行的执行增量都不同。
请注意,不同运行的执行增量可能会有所不同。图片来源:作者。
这成为一个问题,因为 execution_delta
只接受单一的 datetime
对象作为其参数。我们不能为每次运行输入不同的 execution_delta
值。
在这种情况下,我们需要 execution_date_fn
。
如何计算执行日期函数?
execution_date_fn
只是一个普通的 Python 函数。像所有 Python 函数一样,它接收一些参数并返回一些输出。但使用函数的优点在于能够根据函数的输入和逻辑返回不同的输出。
对于 execution_date_fn
,Airflow 将 当前任务的执行日期 作为参数传递,并期望函数返回 外部任务的 执行日期。请注意,这些执行日期需要 以 UTC 时间表示。
def my_exec_date_fn(gordon_exec_date):
# Add your logic here.
return jamie_exec_date
ext_task_sensor = ExternalTaskSensor(
dag=gordon_tasks,
task_id='check_dough_freshness',
external_dag_id='jamie_tasks',
external_task_id='make_new_dough',
email=['gordon.damnsie@gmail.com', 'jamie@gmail.com'],
execution_date_fn=my_exec_date_fn, # Pass the function here.
timeout=1800,
poke_interval=300,
mode='reschedule'
)
根据我们之前的案例研究,我们的 execution_date_fn
需要执行以下操作…
我的 Airflow 配置为本地时间(GMT+8),所以我需要减去 8 小时来获得 UTC 时间。图片来源:作者。
一种简单的方法是硬编码每一个运行,直到时间的尽头。
# The naive way (This is a bad practice. Don't do this.)
def my_exec_date_fn(gordon_exec_date):
if gordon_exec_date == datetime(year=2023,month=3,day=14,hour=6,minute=30):
jamie_exec_date = datetime(year=2023,month=3,day=5,hour=6,minute=0)
elif gordon_exec_date == datetime(year=2023,month=3,day=15,hour=6,minute=30):
jamie_exec_date = datetime(year=2023,month=3,day=5,hour=6,minute=0)
elif gordon_exec_date == datetime(year=2023,month=3,day=16,hour=6,minute=30):
jamie_exec_date = datetime(year=2023,month=3,day=5,hour=6,minute=0)
elif gordon_exec_date == datetime(year=2023,month=3,day=17,hour=6,minute=30):
jamie_exec_date = datetime(year=2023,month=3,day=5,hour=6,minute=0)
...
return jamie_exec_date
这有效,但显然不是最有效的方法。
更好的方法是寻找一致的模式,并利用这些模式程序性地推导输出。通常,寻找模式的好地方是 execution_delta
,因为它包含执行日期之间的关系(我们在这里讨论过这个问题)。
此外,我们还可以查看 datetime
属性,如星期几。如果我们仔细考虑一下,我们的 External Task Sensor 将总是指向星期天,因为 Jamie 只在星期天制作面团。随着我们度过一周,Gordon 的任务日期将越来越远离这个星期天,直到下一个星期天再次重置。然后,它会重复。
这显示了当前运行之间的时间差,为了简单起见。Execution_date_fn 查看之前的运行,但我们也会看到相同的模式。Chef (F) 和 Chef (M) 图标由 Freepik 提供。
这表明 星期几 也可以帮助我们制定 execution_date_fn
。所以我们将星期几添加到我们的表格中。我将星期一标记为 1,星期天标记为 7,按ISO 8601 标准。
括号中的数字表示星期几,其中星期一为 1,星期天为 7。图片由作者提供。
通过标记它们,立即可以清楚地看到……
-
execution_delta
从星期六的 6 开始。 -
execution_delta
每天增加 1,每周五最多增加到 12。 -
execution_delta
然后在星期六重置回 6。
我们可以在 Python 函数中重新创建这种关系,并将 execution_date_fn
分配给我们的外部任务传感器。
def my_exec_date_fn(gordon_exec_date):
day_of_week = gordon_exec_date.isoweekday()
if day_of_week in (6, 7):
time_diff = timedelta(days=day_of_week, minute=30)
jamie_exec_date = gordon_exec_date - time_diff
elif day_of_week in (1, 2, 3, 4, 5):
time_diff = timedelta(days=day_of_week+7, minute=30)
jamie_exec_date = gordon_exec_date - time_diff
return jamie_exec_date
ext_task_sensor = ExternalTaskSensor(
dag=gordon_tasks,
task_id='check_dough_freshness',
external_dag_id='jamie_tasks',
external_task_id='make_new_dough',
email=['gordon.damnsie@gmail.com', 'jamie@gmail.com'],
execution_date_fn=my_exec_date_fn,
timeout=1800,
poke_interval=300,
mode='reschedule'
)
这就是我们自己的 execution_date_fn
。凭借一点创造力,execution_date_fn
可以满足 任何 场景。
我们如何将外部任务传感器融入到我们的 DAG 中?
到目前为止,我们已经涵盖了开始使用外部任务传感器所需了解的所有内容。在这一部分,我认为整理我们所学的内容,看看它们在数据管道中如何结合在一起,会是很好的。
首先,我们将在一个名为 jamie_dag.py
的文件中创建 Jamie DAG。
from airflow.operators.dummy_operator import DummyOperator
from airflow.operators.python_operator import PythonOperator
from airflow.sensors.external_task import ExternalTaskSensor
# Define task 1
def make_dough():
# include your secret recipe here!
return cookies
# Create DAG
jamie_tasks = DAG(
dag_id='jamie_tasks',
description='Jamie to do list. (a.k.a making dough only)',
schedule_interval='5 3 * * *',
...
)
# Include task 0 in DAG (as a starting point)
start = DummyOperator(
dag=jamie_tasks,
task_id='start'
)
# Include task 1 in DAG
make_dough = PythonOperator(
dag=jamie_tasks,
task_id='make_dough',
python_callable=make_dough,
...
)
# Create dependencies (deciding the sequence of task to run)
start >> make_dough
然后,我们将在另一个名为 gordon_dag.py
的文件中创建 Gordon DAG。
from airflow.operators.dummy_operator import DummyOperator
from airflow.operators.python_operator import PythonOperator
from airflow.sensors.external_task import ExternalTaskSensor
# Define task 1
def bake_cookies():
# include your secret recipe here!
return cookies
# Define task 2
def make_money():
# include your money making technique step-by-step here.
return money
# Define execution_date_fn for sensor 1
def my_exec_date_fn(gordon_exec_date):
day_of_week = gordon_exec_date.isoweekday()
if day_of_week in (6, 7):
time_diff = timedelta(days=day_of_week, minute=30)
jamie_exec_date = gordon_exec_date - time_diff
elif day_of_week in (1, 2, 3, 4, 5):
time_diff = timedelta(days=day_of_week+7, minute=30)
jamie_exec_date = gordon_exec_date - time_diff
return jamie_exec_date
# Create DAG
gordon_tasks = DAG(
dag_id='gordon_tasks',
description='List of things that Gordon needs to do.',
schedule_interval='5 3 * * *',
...
)
# Include task 0 in DAG (as a starting point)
start = DummyOperator(
dag=gordon_tasks,
task_id='start'
)
# Include task 1 in DAG
bake_cookies = PythonOperator(
dag=gordon_tasks,
task_id='bake_cookies',
python_callable=bake_cookies,
...
)
# Include task 2 in DAG
make_money = PythonOperator(
dag=gordon_tasks,
task_id='make_money',
python_callable=make_money,
...
)
# Create sensor 1
check_dough_freshness = ExternalTaskSensor(
dag=gordon_tasks,
task_id='check_dough_freshness',
external_dag_id='jamie_tasks',
external_task_id='make_new_dough',
email=['gordon.damnsie@gmail.com', 'jamie@gmail.com'],
execution_date_fn=my_exec_date_fn,
timeout=1800,
poke_interval=300,
mode='reschedule'
)
# Create dependencies (deciding the sequence of task to run)
(start
>> check_dough_freshness
>> bake_cookies
>> make_money)
请注意,外部任务传感器在 gordon_dag.py
中,而不是 jamie_dag.py
中,因为我们希望 Gordon 来检查 Jamie,而不是反过来。Gordon 的 DAG 将是当前 DAG,而 Jamie 是外部 DAG。
然后……我们就完成了!
我们创建了第一个外部任务传感器 check_dough_fresness
。这个传感器会检测 Jamie 的 make_new_dough()
是否返回成功或失败。如果失败,bake_cookies()
和 make_money()
将不会运行。
附加内容:Airflow 中的日期概念
在 Apache Airflow 中,日期令人困惑,因为有很多与日期相关的术语,如 start_date
、end_date
、schedule_interval
、execution_date
等。真的很混乱。但让我们通过一个故事来尝试弄清楚。
假设我们的老板想了解他公司销售业绩。他希望这些数据在接下来的 6 个月 中 每天 的 午夜 12 点 刷新。
首先,我们编写一个复杂的 SQL 查询来生成销售业绩数据。运行查询需要 6 小时。
-
task_start
是任务的开始时间。 -
task_end
是任务的结束时间。 -
task_duration
是运行任务所需的时间。
单个任务。图片由作者提供。
每天,我们需要在午夜 12 点运行这个任务。
一个单独的任务,安排在凌晨 12 点并运行 6 小时。图片由作者提供。
为了自动化此查询,我们创建一个 Airflow DAG 并指定 start_date
和 end_date
。只要今天的日期在这个时间段内,Airflow 就会执行 DAG。
一个 Airflow DAG。图片由作者提供。
然后,我们将任务放入 Airflow DAG 中。
我们需要这个数据每天凌晨 12 点刷新一次。因此,我们将 schedule_interval
设置为 "0 0 * * *"
,这相当于 CRON 的每天午夜 12 点。
schedule_interval
实质上在每个连续调度之间添加了延迟,告知 Airflow 只在特定时间运行任务,因为我们不希望任务在完成后立即重新运行。
-
interval_start
指的是特定调度间隔的开始时间。 -
interval_end
指的是特定调度间隔的结束时间。
请注意,interval_start
和 interval_end
可能会重叠。前一个调度间隔的 interval_end
将与下一个调度间隔的 interval_start
相同。图片由作者提供。
这里有最让人惊讶的部分——尽管看似直觉相悖,Airflow 调度器在调度间隔的结束时触发 DAG 运行,而不是在其开始时。
这意味着 Airflow 在第一次调度间隔内不会做任何事情。我们的查询将在 2023 年 1 月 2 日 12 点首次运行。
这些彩色条像数据一样。所有“黄色”数据只有在 1 月 2 日才会被汇总。图片由作者提供。
这是因为 Airflow 最初是作为一个 ETL 工具创建的。它建立在一个理念上,即一段时间的数据在间隔的结束时进行汇总。
例如,如果我们想了解 1 月 1 日的饼干销售情况,我们不会在 1 月 1 日下午 1 点创建销售报告,因为一天还没有结束,销售数字会不完整。相反,我们只会在午夜 12 点处理数据。今天,我们将处理昨天的数据。
这为什么重要?
由于我们在总结上一个运行的数据,我们在 1 月 2 日生成的销售报告描述的是 1 月 1 日的销售情况,而不是 1 月 2 日的销售情况。
因此,尽管任务在 2 日执行,Airflow 仍将其称为 1 月 1 日的运行。为了更好地区分日期,Airflow 给调度间隔的开始时间赋予了一个特别的名称——execution_date
。
虽然我们在 1 月 2 日运行了“黄色”任务,但其执行日期实际上是 1 月 1 日。图片由作者提供。
这就是为什么我们在计算 execution_delta
时总是取“上一个”运行的差值,因为它是 execution_dates
的增量,本质上是“上一个”运行。
结论
外部任务传感器就像门卫。它们通过确保任务按照特定顺序执行,并且在继续执行后续任务之前满足必要的依赖关系,来阻止不良数据流入下游。
对于那些从未使用过外部任务传感器的人,我希望这篇文章能传达它的重要性并说服你开始使用它们。对于那些已经在使用它们的人,我希望这里的一些见解能够帮助加深你的理解。
感谢你的时间,祝你有美好的一天。
喜欢这篇文章?考虑成为一个 Medium 会员 以获得对每篇文章的完全访问权限,并支持像我这样的内容创作者。
[## 使用我的推荐链接加入 Medium - Casey Cheng
阅读每一篇来自 Casey Cheng 的故事(以及 Medium 上成千上万的其他作者的文章)。你的会员费直接支持…
medium.com](https://medium.com/@casey-cheng/membership?source=post_page-----425093323758--------------------------------)
掌握 Python 中的迭代器和生成器
原文:
towardsdatascience.com/mastering-iterators-and-generators-in-python-ca30939d962
Python 为 AI 工程师和数据科学家
创建自定义迭代器和生成器以实现高效的数据处理
·发表于 Towards Data Science ·11 分钟阅读·2023 年 1 月 17 日
--
视觉效果由作者创建。
本博客文章深入探讨了 Python 中迭代器和生成器的世界,提供了如何使用它们来优化代码并提高程序性能和效率的详细指南。文章涵盖了迭代器和生成器的概念,包括它们的工作原理以及如何为特定用例创建自定义迭代器和生成器。博客还探讨了结合迭代器和生成器的高级技术,以实现特定功能并创建复杂的数据处理管道。最后,我们回顾了在 Keras 中用于训练深度网络的示例数据加载器。通过本指南,你将学习如何掌握 Python 中迭代器和生成器编程的艺术,并将数据处理能力提升到一个新的水平。
目录
· 介绍
· 理解迭代器
· 创建自定义迭代器
· 理解生成器
· 创建自定义生成器
· 组合迭代器和生成器
· 深度学习中的迭代器和生成器
· 结论
· 附加资源
· 联系
介绍
Python 是一种多功能且强大的编程语言,提供了多种开发者可以使用的特性。其中之一就是创建和使用迭代器和生成器的能力。理解并掌握 Python 中迭代器和生成器的概念可以显著提升你的编码技能,并改善程序的性能。
迭代器是可以被迭代(循环)的对象。它们代表一个数据流,并实现了迭代器协议,该协议包含 iter()
和 next()
方法。另一方面,生成器是一种迭代器,允许你声明一个像迭代器一样工作的函数。它们使用 yield 语句返回数据,并且是一种处理大型数据集时更节省内存的方式。
本博客文章将深入探讨 Python 的迭代器和生成器世界。我们将探讨迭代器和生成器的概念以及它们在底层的工作原理。我们还将介绍如何创建自定义迭代器和生成器,并查看如何将它们结合起来以在程序中实现特定功能的高级技巧。阅读完本文后,你将对如何在 Python 中使用迭代器和生成器编写高效且有效的代码有一个深入的理解。
理解迭代器
在 Python 中,迭代器是一个可以被迭代(循环)的对象。它是一个返回数据的对象,一次一个元素。它们代表一个数据流,并实现了包含 iter()
和 next()
方法的迭代器协议。iter()
方法返回迭代器对象本身,next()
方法返回迭代器中的下一个项。
要在 Python 中创建迭代器,我们需要在类中实现迭代器协议,即 iter()
和 next()
方法。一旦完成,类的实例可以用作迭代器。
了解每个实现了 iter()
方法的 Python 对象都可以被视为可迭代对象,但并非所有对象都是迭代器。一个 Iterator
还有另一个方法 next()
,用于获取下一个项。
对象要被视为迭代器必须遵循迭代协议。该协议要求对象实现两个方法:iter()
和 next()
。iter()
方法应返回迭代器对象,而 next()
方法应返回迭代器中的下一个项。如果没有更多项可返回,next()
方法应引发 StopIteration
异常。
总结来说,迭代器是可以被迭代的对象,它们实现了包含 iter()
和 next()
方法的迭代器协议。它们是用于遍历容器元素的对象。
创建自定义迭代器
在 Python 中创建自定义迭代器允许你为特定的使用案例定义迭代器的行为。要创建自定义迭代器,你需要定义一个实现了迭代器协议的类,该类包含 iter()
和 next()
方法。
iter()
方法应返回迭代器对象本身,通常是返回 self
。next()
方法应返回迭代器中的下一个项,并在没有更多项可返回时引发 StopIteration 异常。
这是一个简单的自定义迭代器类的示例,它迭代一个数字范围:
class MyIterator:
def __init__(self, start, end):
self.start = start
self.end = end
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current > self.end:
raise StopIteration
else:
self.current += 1
return self.current - 1
这个迭代器类接受起始值和结束值作为参数,并在迭代时返回范围[start
, end
]内的数字。
实现上述类并创建其实例后,我们可以将其用作迭代器,例如:
for i in MyIterator(1,5):
print(i)
这将打印从 1 到 5 的数字。
为大型数据集创建自定义迭代器是一种高效的处理方式。通过创建自定义迭代器,你可以控制数据的访问方式,并限制任何给定时间加载到内存中的数据量。
总结起来,创建自定义迭代器在 Python 中允许你通过实现迭代器协议、iter()
和next()
方法来定义迭代器的行为,以适应特定的使用案例。这是一种高效处理大型数据集的方法,通过控制数据的访问方式并限制任何给定时间加载到内存中的数据量。
理解生成器
生成器是 Python 中的一种迭代器类型,它允许你声明一个像迭代器一样行为的函数。它们使用yield
语句返回数据,并且是处理大型数据集时更节省内存的方式。
生成器函数的定义类似于常规函数,但它使用yield
代替return
语句。当生成器函数被调用时,它返回一个生成器对象,但不会立即执行函数体。函数体只有在调用其next()
方法时才会被执行。
下面是一个简单的生成器函数示例,它生成一个范围内的数字:
def my_gen(start, end):
current = start
while current < end:
yield current
current += 1
这个生成器接受起始值和结束值作为参数,并生成该范围内的数字以进行迭代。
你可以将这个生成器函数用作迭代器,例如:
for i in my_gen(1,5):
print(i)
这将打印从 1 到 5 的数字。
生成器是一种更节省内存的处理大数据的方式,因为整个数据集不会存储在内存中。而是每次调用next()
时生成一个数据项。
总结起来,生成器是 Python 中的一种迭代器类型,它允许你声明一个像迭代器一样行为的函数。它们使用yield
语句返回数据,并且是处理大型数据集时更节省内存的方式。它们的定义类似于常规函数,返回一个生成器对象,一个迭代器,当生成器的next()
方法被调用时执行函数体。
创建自定义生成器
在 Python 中创建自定义生成器允许你为特定的使用案例定义生成器的行为。要创建自定义生成器,你需要定义一个使用yield
语句返回数据的函数。
下面是一个简单的自定义生成器函数示例,它生成斐波那契数列:
def fibonacci(n):
a, b = 0, 1
for i in range(n):
yield a
a, b = b, a + b
这个生成器函数接受一个参数n
,并在迭代时生成斐波那契数列的前n个数字。
你可以将这个生成器函数用作迭代器,例如:
for i in fibonacci(5):
print(i)
这将打印斐波那契数列的前五个数字。
基于生成器的协程是另一种在 Python 中使用生成器的方法,它允许你在没有显式锁、线程或多进程的情况下同步编写并发代码。协程是可以被暂停和恢复的函数,它们使用yield
语句返回数据。
为大型数据集创建自定义生成器是一种高效的处理方法。然后,你可以控制数据的访问方式,并限制在任何给定时间内加载到内存中的数据量。
总之,在 Python 中创建自定义生成器允许你通过定义一个使用yield
语句的函数来为特定用例定义生成器的行为。基于生成器的协程是另一种使用生成器的方法。它们允许你同步编写并发代码,而不需要显式的锁、线程或多进程。
组合迭代器和生成器
在 Python 中,迭代器和生成器可以以各种方式组合,以实现程序中的特定功能。通过链式操作迭代器和生成器,复杂的数据处理管道变得易于理解和维护。
迭代器链式操作是一种将多个迭代器链在一起以创建一个新的迭代器,该迭代器遍历原始迭代器的元素的技术。这可以通过内置函数itertools.chain()
完成。
from itertools import chain
list1 = [1, 2, 3]
list2 = [4, 5, 6]
list3 = [7, 8, 9]
for i in chain(list1, list2, list3):
print(i)
这将打印 1 到 9 之间的数字。
管道迭代器是一种将多个迭代器链在一起的技术,每个迭代器对数据执行特定操作,并将结果传递给下一个迭代器。这可以通过生成器表达式、列表推导式或像map()
和filter()
这样的函数工具完成。
squared_numbers = (x**2 for x in range(10))
even_squared_numbers = (x for x in squared_numbers if x%2==0)
for i in even_squared_numbers:
print(i)
这将打印 0 到 9 之间数字的偶数平方。
高级迭代器技术也可以实现特定功能,例如实现装饰器以为迭代器添加额外功能,或使用zip()
和enumerate()
同时迭代多个序列。
总之,在 Python 中组合迭代器和生成器可以创建复杂的数据处理管道,这些管道易于理解和维护。通过将迭代器和生成器链在一起,你可以创建新的迭代器,这些迭代器遍历所有原始迭代器的元素。迭代器链式操作和管道迭代器是组合迭代器和生成器的两种最常见方法。高级迭代器技术如装饰器、zip()
和enumerate()
也可以用来实现特定功能。
用于深度学习的迭代器和生成器
在机器学习中使用迭代器和生成器的一个例子是处理无法全部加载到内存中的大型数据集。与其将整个数据集加载到内存中,不如使用自定义生成器一次加载一批数据。这节省了内存,并允许增量处理数据,这对在线学习算法非常有帮助。
这是一个使用生成器加载深度学习模型的大型数据集(包括图像和标签)的示例:
import os
from PIL import Image
def image_generator(data_dir, batch_size):
images = os.listdir(data_dir)
for i in range(0, len(images), batch_size):
batch = images[i:i+batch_size]
X, y = [], []
for img in batch:
image = Image.open(os.path.join(data_dir, img))
X.append(np.array(image))
label = img.split(".")[0]
y.append(int(label))
yield np.array(X), np.array(y)
data_dir
在机器学习中,迭代器和生成器可以提高模型的性能和效率。以下是一些示例:
-
数据加载:在处理大型数据集时,将所有数据加载到内存中可能会成为问题。通过使用生成器加载数据,你可以一次只加载一个小批次,并将其提供给模型。这可以通过创建一个生成器函数来完成,该函数从磁盘读取数据,处理它,并以小批次提供数据。
-
数据预处理:在处理图像或视频数据时,可能需要对数据进行广泛的预处理,然后才能输入模型。通过使用生成器,你可以实时预处理数据,并将预处理后的数据提供给模型。
-
数据增强:数据增强是一种通过对数据应用随机变换来增加数据集大小的技术。使用生成器,你可以实时应用数据增强,并将增强后的数据提供给模型。
-
模型训练:在训练模型时,通常需要多次迭代数据。通过使用迭代器,你可以重复迭代。
这是一个使用生成器函数加载和预处理图像数据以用于机器学习模型的示例:
import os
from keras.preprocessing.image import ImageDataGenerator
# create a generator function that loads and preprocesses images
def image_generator(directory, target_size, batch_size):
data_gen = ImageDataGenerator(rescale=1./255) #rescale images
generator = data_gen.flow_from_directory(directory,
target_size=target_size,
batch_size=batch_size,
class_mode='categorical')
for images, labels in generator:
yield images, labels
# specify the directory containing the images
directory = './data/train'
# specify the target size and batch size
target_size = (200, 200)
batch_size = 32
#create an iterator
image_iter = image_generator(directory, target_size, batch_size)
# use the iterator to train the model
model.fit_generator(image_iter, steps_per_epoch=len(image_iter), epochs=10)
本示例使用了 Keras 的 ImageDataGenerator
类来加载和预处理图像。image_generator
函数接收包含图像的目录、目标尺寸和批次大小作为参数,并返回一个生成预处理图片和标签的迭代器。然后使用 model.fit_generator
方法通过传递迭代器和每个 epoch 的步数来训练模型。
这样,生成器函数从磁盘加载图像,预处理它们,并一次提供一个批次。模型可以对其进行迭代,减少内存使用,并允许处理大数据集。
结论
在这篇博客文章中,我们已经深入探讨了 Python 的迭代器和生成器世界。我们探讨了迭代器和生成器的概念及其内部工作原理。我们还介绍了如何创建自定义迭代器和生成器,并查看了将它们结合起来以在程序中实现特定功能的高级技术。
我们已经看到,迭代器是可以被迭代的对象,它们实现了具有 iter()
和 next()
方法的迭代器协议。另一方面,生成器是一种允许你声明一个行为类似于迭代器的函数的迭代器类型。它们使用 yield 语句来返回数据,并且是处理大型数据集的更节省内存的方式。
创建自定义迭代器和生成器可以让你为特定的用例定义迭代器或生成器的行为。此外,将迭代器和生成器串联起来可以创建复杂的数据处理管道,这些管道易于理解和维护。
在这篇文章结束时,你应该对如何在 Python 中使用迭代器和生成器以编写高效而有效的代码有一个扎实的理解。我们鼓励你尝试本篇文章中讨论的概念,并继续学习 Python 的其他功能和能力。
在线可以找到官方文档、书籍、教程和视频等额外资源,以继续学习和掌握 Python 中的迭代器和生成器。
额外资源
Python 中的迭代器和生成器文档:
类提供了一种将数据和功能捆绑在一起的方法。创建一个新类会创建一个新的对象类型…
"Python 迭代器:一步步指南" 由 Corey Schafer 提供:
"如何在 Python 中使用生成器和 yield" 由 Real Python 提供:
## 如何在 Python 中使用生成器和 yield - Real Python
生成器函数由 PEP 255 引入,是一种特殊类型的函数,返回一个惰性迭代器。这些是…
"Python 迭代器和生成器" 由 Trey Hunner 提供:
我之前写过一篇关于驱动 Python for
循环的迭代器协议的文章。那篇文章中有一件事我没有提及…
"什么是 Python 生成器?" 由 Dan Bader 提供:
## 什么是 Python 生成器? - dbader.org
生成器是 Python 中一个棘手的主题。通过本教程,你将从基于类的迭代器跃迁到使用…
"Python 迭代器与生成器" 作者:Kurtis Pykes
理解关键差异
"Python 中的异步迭代器" 作者:Dinesh Kumar K B
如何在 Python 中编写异步 for 循环
"迭代器函数" 作者:Thijmen Dam
使用 Python 内置函数高效迭代
"揭开 Python 中迭代器和生成器的神秘面纱****" 作者:Lynn Kwong
学习高效处理大型数据集的方法
towardsdatascience.com
联系我们
想要联系?关注罗宾逊博士的 LinkedIn、Twitter、Facebook 和 Instagram。访问我的主页获取论文、博客、邮箱注册等更多信息!
研究员与企业家问候!作为研究员,罗宾逊博士提出并使用了先进的 AI 来理解…
www.jrobs-vision.com.](https://www.jrobs-vision.com/?source=post_page-----ca30939d962--------------------------------)
掌握语言模型
通过温度、top-p、top-k 等来平衡质量与多样性
·
关注 发表于 Towards Data Science · 12 min read · 2023 年 10 月 3 日
--
如果你曾经通过操控台或 API 使用过语言模型,你可能被要求选择一些输入参数。对于我们许多人来说,这些参数的意义(以及正确的使用方法)可能并不十分清楚。
一张显示 SillyTavern 界面中参数选择的截图。图片由作者提供。
这篇文章将教你如何使用这些参数来控制幻觉,注入创造力到模型输出中,并进行其他细微的调整以优化行为。就像提示工程一样,输入参数调整可以让你的模型达到 110%的性能。
在本文结束时,你将成为五个重要输入参数的专家——温度、top-p、top-k、频率惩罚和存在惩罚。你还将了解这些参数如何帮助我们在质量和多样性之间进行权衡。
所以,拿一杯咖啡,我们开始吧!
目录
· 背景
· 质量、多样性与温度
· Top-k 和 Top-p
· 频率和存在惩罚
· 参数调整备忘单
· 总结
背景
在我们开始选择输入参数之前,我们需要了解一些背景信息。让我们来谈谈这些模型是如何选择生成的单词的。
要读取文档,语言模型将其分解为一系列标记。标记只是模型可以轻松理解的小块文本:它可以是一个词、一个音节或一个字符。例如,“Megaputer Intelligence Inc.”可能会被分解为五个标记:[“Mega”,“puter”,“Intelligence”,“Inc”,“.”]。
我们熟悉的大多数语言模型通过反复生成序列中的下一个标记来操作。每当模型想要生成另一个标记时,它会重新读取整个序列,并预测下一个应该出现的标记。这种策略被称为自回归生成。
语言模型的自回归标记生成。GIF 由Echo Lu提供,包含由Annie Surla修改的NVIDIA的图像。经版权所有者许可修改。
这解释了为什么 ChatGPT 一次输出一个单词:它在写字的时候把单词流式传输给你。
要选择序列中的下一个标记,语言模型首先为其词汇表中的每个标记分配一个可能性分数。如果标记是文本的良好延续,它会获得一个高可能性分数;如果标记是文本的较差延续,它会获得一个低可能性分数,模型会进行评估。
语言模型分配可能性分数以预测序列中的下一个标记。原始图像由Annie Surla提供,来自NVIDIA,经Echo Lu修改,经过版权所有者许可。
在分配了可能性分数之后,通过考虑可能性分数的标记采样方案来选择标记。标记采样方案可能会包含一些随机性,以便语言模型不会每次以相同的方式回答相同的问题。这种随机性在聊天机器人或其他应用中可能是一个很好的特性。
TLDR: 语言模型将文本拆分为标记,预测序列中的下一个标记,并混入一些随机性。根据需要重复以生成语言。
质量、多样性和温度
那么,为什么我们要选择第二好的标记、第三好的标记,或除了最佳标记之外的其他任何标记呢?难道我们不应该每次都选择最佳标记(即具有最高可能性得分的标记)吗?通常,我们确实如此。但是,如果我们每次都选择最佳答案,我们会每次得到相同的答案。如果我们想要多样化的答案,我们可能需要牺牲一些质量以获得它。这种为多样性而牺牲质量的权衡被称为质量-多样性权衡。
鉴于此,temperature 告诉机器如何在质量-多样性权衡中导航。低温意味着更高的质量,而高温则意味着更多的多样性。当温度设置为零时,模型总是采样具有最高可能性得分的标记,这导致查询之间的多样性为零,但确保我们总是选择模型评估的最高质量的续集。
很多时候,我们会想要将温度设置为零。作为一项规则,对于你只会传递给模型一次的任何提示,你应该始终选择温度零,因为这最有可能得到一个好的答案。在我的数据分析工作中,我将温度设置为零用于实体提取、事实提取、情感分析以及大多数其他标准任务。
在高温下,我们会看到更多的垃圾和幻觉,连贯性较差,响应质量也较低,但也会有更多的创造力和答案的多样性。我们建议你仅在想要对同一个问题得到两个不同答案时使用非零温度。
高温带来了多样性、创造力和多种答案,但也增加了垃圾、无序和幻觉。图片由Echo Lu提供。
那么,为什么我们要对同一个提示得到两个不同的答案呢?在某些情况下,对一个提示有多个答案可能是有用的。例如,有一种技术是生成多个答案并只保留最佳答案,这通常比温度为零的单次查询产生更好的结果。另一个用例是合成数据生成:我们想要许多多样的合成数据点,而不仅仅是一个非常好的数据点。我们可能会在以后的文章中讨论这些用例(以及其他用例),但更多时候,我们每个提示只想要一个答案。有疑问时,选择温度零!
需要注意的是,虽然理论上温度为零应该每次产生相同的答案,但实际情况可能并非如此!这是因为模型运行的 GPU 可能会出现小的计算误差,比如四舍五入错误。这些错误会在计算中引入微小的随机性,即使在温度为零时也是如此。由于在文本中改变一个词可能会显著改变其含义,因此一个错误可能会引起后续文本中不同词汇的连锁反应,从而导致几乎完全不同的输出。但请放心,这通常对质量的影响微乎其微。我们提到这一点是为了让你在温度为零时遇到一些随机性时不会感到惊讶。
除了温度,还有很多其他方法可以在质量和多样性之间取得平衡。在下一节中,我们将讨论一些对温度采样技术的修改。但如果你对使用温度为零感到满意,可以暂时跳过这部分。你可以放心,你选择的这些参数在温度为零时不会影响你的答案。
总结:温度增加了多样性,但通过将随机性添加到模型输出中而降低了质量。
Top-k 和 Top-p
调整我们的词汇采样公式的一种常见方法叫做 top-k 采样。Top-k 采样与普通的温度采样类似,只是排除了最低概率的词汇:只有“前 k”个最佳选择会被考虑,这也是名字的由来。这个方法的优势在于它防止我们选择真正糟糕的词汇。
比如说,我们试图为“太阳在……升起”生成一个续写。如果不使用 top-k 采样,模型会考虑词汇表中的每一个词作为序列的可能延续。这样可能会有非零的几率生成诸如“太阳在冰箱里升起”这样荒谬的内容。使用 top-k 采样,模型会过滤掉这些真正糟糕的选择,只考虑前 k 个最佳选项。通过剪掉长尾,我们会失去一点多样性,但质量会大幅提升。
Top-k 采样通过仅保留 k 个最佳候选词并丢弃其余词来提高质量。图片来源于Echo Lu。
Top-k 采样是一种“既要蛋糕又要吃蛋糕”的方法:它以比单独使用温度更小的成本获得所需的多样性。由于这一技术效果显著,它激发了许多变体。
Top-k 采样的一种常见变体叫做 top-p 采样,也称为核采样。Top-p 采样与 top-k 非常相似,只是它使用概率得分而不是词汇排名来确定剪切尾部的标准。更具体地说,它只考虑那些排名前列的、其组合概率超过阈值 p 的词汇,丢弃其余词汇。
当存在许多质量低劣或平庸的延续时,与 top-k 抽样相比,top-p 抽样的优势变得明显。例如,假设下一个标记只有几个好的选择,而有数十个模糊合理的选择。如果我们使用 k=25 的 top-k 抽样,我们将考虑许多质量低劣的延续。相反,如果我们使用 top-p 抽样来过滤掉概率分布的底部 10%,我们可能只会考虑那些好的标记,同时过滤掉其余的标记。
在实践中,与 top-k 抽样相比,top-p 抽样通常能够产生更好的结果。通过关注累积概率,它能够适应输入的上下文并提供更灵活的截断。因此,总之,top-p 和 top-k 抽样都可以在非零温度下使用,以在较低的质量成本下捕捉多样性,但通常 top-p 抽样做得更好。
提示:对于这两个设置,较低的值=更多过滤。当值为零时,它们将过滤掉除排名第一的标记以外的所有标记,这与将温度设置为零具有相同效果。因此,请使用这些参数时,请注意不要将它们设置得太低,否则会损失所有的多样性。
TLDR:Top-k 和 top-p 在只付出较小代价以增加质量的情况下提高质量。它们通过在随机抽样之前移除最差的标记选择来实现这一点。
频率和存在惩罚
在我们开始总结之前,我们只需讨论另外两个参数:频率和存在惩罚。这些参数——大惊小怪——是导航质量多样性权衡的又一种方式。虽然温度参数通过在标记抽样过程中添加随机性来实现多样性,频率和存在惩罚则通过对已经在文本中出现过的标记施加惩罚来增加多样性。这使得旧的和过度使用的标记的抽样变得不太可能,影响模型进行更新颖的标记选择。
频率惩罚为每次标记在文本中出现都添加一定的惩罚。这样做可以减少重复使用相同的标记/词语/短语,同时也会导致模型讨论更多样化的主题并更频繁地更换话题。另一方面,存在惩罚是一种固定的惩罚,如果一个标记已经在文本中出现过,则会应用该惩罚。这使得模型引入更多新的标记/词语/短语,导致它讨论更多样化的主题并更频繁地更换话题,而且不会显著地抑制经常使用的词语的重复出现。
类似于温度,频率和存在惩罚使我们远离“最佳”答案,趋向于更具创意的答案。但不同的是,它们不是通过随机性实现的,而是通过精心计算的有针对性的惩罚来注入多样性。在一些少见的需要非零温度的任务中(当你需要对同一提示获得多个答案时),你也可以考虑加入小幅的频率或存在惩罚,以提升创意。但对于那些只希望找到唯一正确答案的提示,在一次尝试中设置所有这些参数为零时,你成功的机会最高。
通常,当只有一个正确答案,并且你只问一次时,应该将频率和存在惩罚设置为零。但如果有多个正确答案,比如在文本摘要中呢?在这种情况下,你有一点自由裁量权。如果你发现模型的输出无聊、缺乏创意、重复或范围有限,合理应用频率或存在惩罚可能是个不错的方法来增加趣味。但对于这些参数,我们的最终建议和温度参数一样:当有疑问时,选择零!
我们应该注意,虽然温度和频率/存在惩罚都能增加模型响应的多样性,但它们增加的多样性种类并不相同。频率/存在惩罚增加了单次响应内的多样性。这意味着,一个响应将具有比没有这些惩罚时更多的不同词汇、短语、主题和学科。但当你两次输入相同的提示时,不会更容易得到两个不同的答案。这与温度形成对比,温度增加了响应之间的多样性:在较高的温度下,当多次将相同提示输入模型时,你会得到更为多样化的回答。
我喜欢将这种区分称为响应内多样性与响应间多样性。温度参数增加了响应内和响应间的多样性,而频率/存在惩罚仅增加响应内的多样性。因此,当我们需要多样性时,我们对参数的选择应取决于我们需要的多样性种类。
总结:频率和存在惩罚增加了模型讨论的主题多样性,并使其更频繁地更换话题。频率惩罚还通过减少词汇和短语的重复来增加词汇选择的多样性。
参数调整备忘单
本节旨在作为选择模型输入参数的实用指南。我们首先提供一些明确的规则来决定哪些值应设置为零。然后,我们给出一些建议,帮助你找到适合非零参数的正确值。
我强烈建议你在选择输入参数时使用这份备忘单。现在就去收藏此页面,以免丢失!
将参数设置为零的规则:
温度:
-
每个提示一个答案:零。
-
每个提示多个答案:非零。
频率和存在惩罚:
-
当有一个正确答案时:零。
-
当有多个正确答案时:可选。
Top-p/Top-k:
-
在零温度下:输出不受影响。
-
在非零温度下:非零。
如果你的语言模型有其他未列出的参数,保持其默认值也是可以的。
调整非零参数的提示:
列出应该具有非零值的那些参数,然后去试验场中尝试一些测试提示以查看效果。但如果上述规则要求将参数保持在零,则保持在零!
调整温度/top-p/top-k:
-
为了更多的多样性/随机性,增加温度。
-
在非零温度下,从 top-p 约 0.95(或 top-k 约 250)开始,根据需要逐渐降低。
故障排除:
-
如果出现过多的废话、垃圾或幻觉,降低温度和/或减少 top-p/top-k。
-
如果温度高而多样性低,增加 top-p/top-k。
提示:虽然一些界面允许你同时使用 top-p 和 top-k,但我们倾向于选择其中之一以保持简单。Top-k 更易于使用和理解,但 top-p 通常更有效。
调整频率惩罚和存在惩罚:
-
为了更多的多样化主题和内容,增加存在惩罚。
-
为了更丰富且不重复的语言,增加频率惩罚。
故障排除:
-
如果输出显得零散且主题变化过快,减少存在惩罚。
-
如果有太多新词和不寻常的词,或者存在惩罚设置为零但仍然有太多主题变化,减少频率惩罚。
TLDR:你可以将此部分作为调整语言模型的备忘单。你 肯定 会**忘记这些规则,因此请收藏此页面,并在以后作为参考使用。
总结
虽然定义令牌采样策略的方法无穷无尽,但我们讨论过的参数——温度、top-k、top-p、频率惩罚和存在惩罚——是最常用的参数。这些参数是你可以在 Claude、Llama 和 GPT 系列等模型中找到的。本文展示了所有这些参数实际上只是帮助我们导航质量与多样性权衡的工具。
在我们结束之前,还有一个最后的输入参数需要提及:最大令牌长度。最大令牌长度就是模型停止打印答案的截止点,即使答案未完成。在复杂的讨论之后,我们希望这个参数是不言自明的。🙂
随着我们在这一系列中的深入,我们将更深入地探讨诸如提示工程、为你的使用案例选择合适的语言模型等主题!我还会展示一些来自我在 Megaputer Intelligence 担任数据分析顾问时的实际应用案例。敬请期待更多见解,祝建模愉快!
TLDR: 如果有疑问,将温度、频率惩罚和存在惩罚设置为零。如果这样做对你不起作用,请参考上面的备忘单。
精通线性回归:有志数据科学家的终极指南
你需要知道的关于线性回归的所有信息都在这里(包括 Python 中的应用)
·发表于Towards Data Science ·22 分钟阅读·2023 年 4 月 17 日
--
图片由Dariusz Sankowski提供,来自Pixabay
如果你正在接触机器学习,你可能会遇到的第一个模型就是线性回归。它可能是最容易理解的模型,但不要低估它:有很多东西需要理解和掌握。
如果你是数据科学领域的初学者或有志成为数据科学家的人员,你可能会遇到一些困难,因为市面上有很多资源,但都比较零散。我了解你的感受,这也是我创建这本完整指南的原因:我想给你所有需要的知识,而不必再去寻找其他资源。
所以,如果你想对线性回归有全面的了解,这篇文章就是为你准备的。你可以深入学习并在需要时随时重读。此外,请注意,为了覆盖这一主题,我们需要一些通常与回归分析相关的知识:我们会深入探讨这些内容。
而且……请原谅我如果我要链接一个你需要的资源:过去我曾创建了一篇关于线性回归相关主题的文章,为了全面了解,我建议你阅读它(当我们需要时我会在稍后提供链接)。
**Table of Contents:**
What do we mean by "regression analysis"?
Understanding correlation
The difference between correlation and regression
The Linear Regression model
Assumptions for the Linear Regression model
Finding the line that best fits the data
Graphical methods to validate your model
An example in Python
我们所说的“回归分析”是什么意思?
在这里我们研究线性回归,但我们所说的“回归分析”是什么意思?根据维基百科的解释:
回归分析是一种数学技术,用于找到因变量与一个或多个自变量之间的功能关系。
换句话说,我们知道在数学中我们可以这样定义一个函数:y=f(x)
。一般来说,y
被称为因变量,而x
是自变量。因此,我们用某个函数f
来表示y
与x
的关系。回归分析的目标就是找到函数f
。
现在,这看起来简单,但其实不然。我知道你也知道。之所以不容易,是因为:
-
我们知道
x
和y
。例如,如果我们使用表格数据(例如使用Pandas
),x
是特征,而y
是标签。 -
不幸的是,数据很少遵循非常明确的路径。因此,我们的任务是找到一个最能近似
x
和y
之间关系的函数f
。
总结一下:回归分析旨在找到一个(好的)估计关系,在因变量和自变量之间。
现在,让我们可视化一下为什么这个过程可能很困难。考虑以下代码及其结果:
import numpy as np
import matplotlib.pyplot as plt
# Create random linear data
a = 130
x = 6*np.random.rand(a,1)-3
y = 0.5*x+5+np.random.rand(a,1)
# Labels
plt.xlabel('x')
plt.ylabel('y')
# Plot a scatterplot
plt.scatter(x,y)
上述代码的结果。图片由 Federico Trotta 提供。
现在,告诉我:x
和y
之间的关系能是直线吗?那么……这些数据可以被直线近似吗?例如如下:
一条接近给定数据的直线。图片由 Federico Trotta 提供。
停下来想一想。
好吧,可能的。那么接下来呢?
一条曲线接近给定数据。图片由 Federico Trotta 提供。
好吧,即便如此也可能!那么,哪一个最好?为什么不是另一个呢?
这就是回归的目的:找到最佳的估计函数来近似给定数据。它通过一些方法来实现这一点:我们稍后会在本文中讨论它们。我们将它们应用于线性回归模型,但其中一些可以用于任何其他回归技术。别担心:我会非常具体,以免你感到困惑。
理解相关性
引用自维基百科:
在统计学中,相关性是指两个随机变量之间的任何统计关系,无论是否有因果关系。虽然在最广泛的意义上,“相关性”可能指任何类型的关联,但在统计学中,它通常指的是一对变量之间线性相关的程度。
换句话说,相关性是一个统计测量,表示变量之间的线性关系。
我们可以说两个变量是相关的,如果第一个变量的每个值对应第二个变量的一个值,遵循某条路径。如果两个变量高度相关,路径将是线性的,因为相关性描述了变量之间的线性关系。
相关性的数学基础
这是一个全面的指南,如承诺的那样。所以,我想讲解相关性的数学背景,但不要担心:我们会让它简单易懂,即使你不擅长数学也能理解。
我们通常提到相关系数,也称为 皮尔逊相关系数。这给出了两个变量之间相关性的估计。假设我们有两个变量a
和b
,它们可以达到n
个值。我们可以按如下方式计算相关系数:
皮尔逊系数的定义,由作者通过 embed-dot-fun 提供。
在哪里:
a
的均值(但它适用于两个变量a
和b
):
均值的定义,由作者通过 embed-dot-fun 提供。
- 标准差:
标准差和方差的定义,由作者通过 embed-dot-fun 提供。
所以,总结一下:
皮尔逊系数的定义,由作者通过 embed-dot-fun 提供。
正如你所知道的:
- 均值是变量所有值的总和除以值的数量。因此,例如,如果我们的变量
a
具有值 1, 3, 7, 13, 25,则a
的均值将是:
对 5 个值的均值计算,由作者通过 embed-dot-fun 提供。
- 标准差是统计离散度的一个指标,是变量(或我们在统计学中所说的人群)的变异性的估计。它是表达数据围绕一个指标的离散度的方式之一;对于相关系数,计算离散度的指标是均值(参见上述公式)。标准差越高,均值周围的离散度越高:大多数数据点距离均值较远。
从数值上讲,我们必须记住相关系数的值被限制在 1 和 -1 之间;这意味着:
-
如果 r=1:变量高度正相关;这意味着如果一个变量的值增加,另一个变量也会增加,遵循线性路径。
-
如果 r=-1:变量高度负相关;这意味着如果一个变量的值增加,另一个变量的值会减少,遵循线性路径。
-
如果 r=0:变量之间没有相关性。
最后,如果r>0.75
,通常认为两个变量具有很高的相关性。
相关性不等于因果性
我们需要非常清楚“相关性不等于因果性”这一事实;我们想举一个可能有用的例子来记住它。
现在是炎热的夏天;我们不喜欢我们城市的高温,所以我们去山上。幸运的是,我们到达山顶,测量了温度,发现它比我们城市的低。我们有点怀疑,决定去更高的山,发现温度比前一座山还低。
我们尝试不同高度的山,测量温度,并绘制图表;我们发现随着山的高度增加,温度下降,我们可以看到线性趋势。
这是什么意思?这意味着温度与山的高度相关,呈线性关系:因此温度的下降与高度(山的高度)之间存在相关性。这并不意味着山的高度导致了温度的下降;实际上,如果我们到达相同的高度,在相同的纬度,用热气球测量温度,我们会测得相同的温度。
相关性矩阵
那么,我们如何在 Python 中计算相关系数?通常,我们计算相关性矩阵。假设我们有两个变量,X
和y
; 我们将它们存储在一个名为df
的数据框中,然后可以使用seaborn
来绘制相关性矩阵:
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
# Create data
x = np.array([1, 1, 2, 3, 4, 4, 5, 6, 7, 7, 8, 9])
y = np.array([13, 14, 17, 12, 23, 24, 25, 25, 24, 28, 32, 33])
# Create the dataframe
df = pd.DataFrame({'x':x, 'y':y})
# Plot heat map for correlation coefficient
sns.heatmap(df.corr(), annot=True, fmt="0.2")
上述代码的相关性矩阵。图片由 Federico Trotta 提供。
相关性与回归的区别
如果我们有 0 的相关系数,这意味着数据点不倾向于沿着线性路径增加或减少,因为我们没有相关性。
让我们看一下不同值的相关系数图(图片来自 维基百科):
不同相关值的数据分布。图片版权分发 在这里。
正如我们所见,当相关系数等于 1 或-1 时,数据点的趋势显然沿着一条线。但随着相关系数偏离这两个极值,数据点的分布会偏离线性路径。最后,对于相关系数为 0 的情况,数据的分布可以是任何形式。
所以,当我们得到 0 的相关系数时,我们不能对数据的分布做出任何结论,但我们可以进行回归分析(如果需要)来调查。
所以,相关性和回归是相关但不同的:
-
相关性分析变量的线性分布趋势。
-
回归是研究变量之间关系的学科。
线性回归模型
我们有两种线性回归模型:简单线性回归和多重线性回归。让我们来看一下它们。
简单线性回归模型
简单线性回归的目标是建模单个特征与连续标签之间的关系。这是描述该机器学习模型的数学方程:
y = wx + b
参数b
(也称为“偏置”)表示 y 轴截距(即X=0
时y
的值),而w
是权重系数。我们的目标是学习描述x
和y
之间关系的权重w
。这个权重将用于预测x
的新值的响应。
让我们考虑一个实际的例子:
import numpy as np
import matplotlib.pyplot as plt
# Create data
x = np.array([1, 1, 2, 3, 4, 4, 5, 6, 7, 7, 8, 9])
y = np.array([13, 14, 17, 12, 23, 24, 25, 25, 24, 28, 32, 33])
# Show scatterplot
plt.scatter(x, y)
上述代码的输出。图片由 Federico Trotta 提供。
问题是:这个数据分布可以用直线来近似吗?好吧,我们可以创建类似的东西:
import numpy as np
import matplotlib.pyplot as plt
# Create data
x = np.array([1, 1, 2, 3, 4, 4, 5, 6, 7, 7, 8, 9])
y = np.array([13, 14, 17, 12, 23, 24, 25, 25, 24, 28, 32, 33])
# Create basic scatterplot
plt.plot(x, y, 'o')
# Obtain m (slope) and b (intercept) of a line
m, b = np.polyfit(x, y, 1)
# Add linear regression line to scatterplot
plt.plot(x, m*x+b)
# Labels
plt.xlabel('x variable')
plt.ylabel('y variable')
上述代码的输出。图片由 Federico Trotta 提供。
像我们上面看到的例子一样,这可能是一条直线,但也可能是一般曲线。
稍后我们将看到如何判断数据分布是否可以更好地用直线或一般曲线来描述。
多元线性回归模型
由于现实是复杂的,我们通常面临的情况与多元线性回归模型相关。我们指的是特征x
不仅仅是一个:我们会有多个特征。例如,如果我们处理的是表格数据,一个具有 9 列的数据框有 8 个特征和 1 个标签:这意味着我们的问题是八维的。
如我们所理解的,这种情况非常复杂,不易可视化,直线的方程必须用向量和矩阵来表示,变成:
多元线性回归模型的方程,由 Author 提供的 embed-dot-fun 支持。
所以,直线的方程变成所有权重(w
)与自变量(x
)相乘的和,它甚至可以写成两个矩阵的乘积。
线性回归模型的假设
现在,为了应用线性回归模型,我们的数据应该符合一些假设。这些是:
-
线性:因变量和自变量之间的关系应该是线性的。这意味着自变量的变化应该导致因变量的成比例变化,沿着一条线性路径。
-
独立性:数据集中的观察值应该彼此独立。这意味着一个观察值的值不应依赖于另一个观察值的值。
-
同方差性:残差的方差在所有自变量的水平上应该是恒定的。换句话说,残差的分布在所有自变量的水平上应该大致相同。
-
正态性:残差应该是正态分布的。换句话说,残差的分布应该是一个正态(或钟形)曲线。
-
无多重共线性:自变量之间不应高度相关。如果两个或多个自变量高度相关,那么很难区分每个变量对因变量的个别影响。
不幸的是,测试所有这些假设并不总是可能的,尤其是在多元线性回归模型的情况下。无论如何,有一种方法可以测试所有的假设。这被称为p-value
检验,也许你以前听说过这个。无论如何,我们在这里不会涵盖这个测试,原因有两个:
-
这是一种通用测试,与线性回归模型没有特别关联。因此,它需要在专门的文章中进行特定处理。
-
我是那些(也许是少数)认为在分析数据时计算
p-value
并不总是必要的。我将来会为这个有争议的话题撰写一篇专门的文章。但出于好奇,因为我是工程师,我有非常实用的方法,喜欢应用数学。我在这里写了一篇关于这个话题的文章:
towardsdatascience.com ## 请:不要再在数据科学中抛硬币了
为什么数据科学中的统计学应该被工程化。
[towardsdatascience.com
找到最适合数据的直线
所以,上面我们在考虑以下哪一个可能是最佳拟合:
模型比较。图像由 Federico Trotta 提供。
为了理解最佳模型是左侧的(直线)还是右侧的(一般曲线),我们按如下步骤进行:
-
我们将数据分为训练集和测试集。
-
我们在两个数据集上验证这两种模型,测试我们模型的学习泛化能力。
我们这里不会涉及多项式模型(适用于一般曲线),但可以考虑验证机器学习模型的两种方法:
-
其中一种是分析方法。
-
另一种方法是图形方法。
一般来说,我们将使用两者以更好地理解模型的性能。无论如何,泛化意味着我们的机器学习模型从训练集中学习并将其学习正确应用于测试集。如果没有,我们会尝试其他机器学习模型。以下是过程:
训练和验证机器学习模型的工作流程。图像由 Federico Trotta 提供。
这意味着一个机器学习模型当它在训练集和测试集上表现良好时,泛化能力强。
我在以下文章中讨论了线性回归情况下的机器学习模型的分析验证方法:
towardsdatascience.com ## 精通回归分析的艺术:每个数据科学家都应该知道的 5 个关键指标
关于回归分析中使用的指标的终极指南
[towardsdatascience.com
我建议你阅读它,因为我们将在这篇文章的最后一个例子中使用其中讨论的一些指标。
当然,上述讨论的指标可以应用于任何回归问题的机器学习模型。不过,你很幸运:我使用了线性模型作为示例。
机器学习模型在回归问题中的图形验证方法将在下一段讨论。
验证机器学习模型的图形方法
让我们看看三种验证机器学习模型的图形方式。
1. 残差分析图
该方法特定于线性回归模型,旨在可视化残差的分布。我们期望如下:
残差分析图。图片来源:Federico Trotta。
为了绘制这个图,我们可以使用Seaborn
中的内置函数sns.residplot()
(这是文档)。
这样的图表很好,因为我们希望在横轴上看到随机分布的数据点。事实上,线性回归模型的一个假设是残差必须符合正态分布(上述假设第 4 项)。如果残差符合正态分布,则表示观察值与预测值之间的误差是随机分布在零周围,没有明确的模式或趋势;这正是我们图中的情况。因此,在这些情况下,我们的机器学习模型可能是一个好的模型。
相反,如果我们的残差图中存在特定的模式,那么我们的模型可能不适合我们的机器学习问题。例如,请考虑以下情况:
抛物线形残差分析图。图片来源:Federico Trotta。
在这种情况下,我们可以看到存在抛物线趋势:这意味着我们的模型(线性模型)不适合解决我们的机器学习问题。
2. 实际值与预测值图
另一种我们可以用来验证机器学习模型的图表是实际值与预测值图。在这种情况下,我们绘制一个图,将实际值放在横轴上,将预测值放在纵轴上。目标是在线性回归的情况下尽可能找到数据点沿线分布。我们甚至可以在多项式回归的情况下使用这种方法:在这种情况下,我们会期望数据尽可能分布在一个通用曲线附近。
假设我们得到如下结果:
线性回归中的实际值与预测值图。图片来源:Federico Trotta。
上面的图表显示预测数据点沿一条线分布。虽然不是完美的线性分布,但线性模型可能并不理想。
如果针对我们的特定问题,我们有y_train
(训练集上的标签),并且我们计算了y_train_pred
(训练集上的预测值),我们可以绘制如下图表:
import matplotlib.pyplot as plt
# Scatterplot of y_train and y_train_pred
plt.scatter(y_train, y_train_pred)
plt.plot(y_test, y_test, color='r') # Plot the line
# Labels
plt.title('ACTUAL VS PREDICTED VALUES')
plt.xlabel('ACTUAL VALUES')
plt.ylabel('PREDICTED VALUES')
3. 核密度估计(KDE)图
我们要讨论的最后一个图表用于验证我们的机器学习模型的是核密度估计(KDE)图。这是一种通用方法,可用于验证回归和分类模型。
KDE 是一种用于概率密度估计的核平滑器的应用。核平滑器是一种统计方法,用于将函数估计为邻近观察数据的加权平均值。核定义了权重,赋予离得更近的数据点更高的权重。
要了解平滑函数的有用性,请参见下图:
KDE 背后的想法。图片来源于 Federico Trotta。
如果我们想比较两个量,使用平滑函数来逼近数据点是有帮助的。实际上,在机器学习问题中,我们通常希望查看实际标签与模型预测标签之间的比较,因此我们使用 KDE 来比较两个平滑函数。
假设我们使用线性回归模型预测了标签。我们希望比较训练集的实际标签和预测标签的 KDE。我们可以使用 Seaborn
调用方法 sns.kdeplot()
(这是文档) 来完成。
假设我们有以下结果:
KDE 图。图片来源于 Federico Trotta。
如我们所见,实际标签和预测标签之间的比较很容易进行,因为我们是在比较两个平滑函数;在这种情况下,我们的模型是好的,因为曲线非常相似。
实际上,我们对一个“优秀”机器学习模型的期望包括:
-
曲线应尽可能类似于钟形曲线。
-
两条曲线之间尽可能相似。
Python 示例
现在,让我们将到目前为止学到的所有知识应用到这里。我们将使用著名的“Ames Housing”数据集,这对我们的目的非常合适。
该数据集有 80 个特征,但为了简便,我们将仅使用其中一个子集,即:
-
Overall Qual
:这是对房屋整体材料和装修的评分,范围从 1(差)到 10(优秀)。 -
Overall Cond
:这是对房屋整体状况的评分,范围从 1(差)到 10(优秀)。 -
Gr Liv Area
:这是地面以上的居住面积,以平方英尺为单位。 -
Total Bsmt SF
:这是总地下室面积,以平方英尺为单位。 -
SalePrice
:这是销售价格,单位为美元 $。
我们将把 SalePrice
列视为目标(标签)变量,其他列视为特征。
探索性数据分析 EDA
让我们导入数据,创建一个包含上述特征的子集,并显示一些统计信息:
import pandas as pd
# Define the columns
columns = ['Overall Qual', 'Overall Cond', 'Gr Liv Area',
'Total Bsmt SF', 'SalePrice']
# Create dataframe
df = pd.read_csv('http://jse.amstat.org/v19n3/decock/AmesHousing.txt',
sep='\t', usecols=columns)
# Show statistics
df.describe()
数据集的统计信息。图片来源于 Federico Trotta。
这里一个重要的观察是,所有标签的均值范围不同(Overall Qual
的均值为 6.09
,而 Gr Liv Area
的均值为 1499.69
)。这告诉我们一个重要的事实:我们必须对特征进行缩放。
数据准备
“特征缩放”是什么意思?
缩放一个特征意味着特征范围被缩放到 0 和 1 之间或 1 和 -1 之间。有两种典型的方法来缩放特征:
- 均值归一化: 均值归一化是一种缩放数值数据的方法,使其最小值为零,最大值为一,所有值都围绕均值进行归一化。假设 c 是我们特征的值;在归一化过程后,c′ 是 c 的新值:
均值归一化的公式,由 Author 提供的 embed-dot-fun 提供。
让我们看一个 Python 示例:
import numpy as np
# Create a list of numbers
data = [1, 2, 3, 4, 5]
# Find min and max values
data_min = min(data)
data_max = max(data)
# Normalize the data
data_normalized = [(x - data_min) / (data_max - data_min) for x in data]
# Print the normalized data
print(f'normalized data: {data_normalized}')
>>>
normalized data: [0.0, 0.25, 0.5, 0.75, 1.0]
- 标准化(或 z-score 归一化):该方法将变量转换为均值为零、标准差为一的状态。公式如下(c′c’c′ 是 ccc 在归一化过程后的新值):
标准化的公式,由 Author 提供的 embed-dot-fun 提供。
让我们看一个 Python 示例:
import numpy as np
# Original data
data = [1, 2, 3, 4, 5]
# Calculate mean and standard deviation
mean = np.mean(data)
std = np.std(data)
# Standardize the data
data_standardized = [(x - mean) / std for x in data]
# Print the standardized data
print(f'standardized values: {data_standardized}')
print(f'mean of standardized values: {np.mean(data_standardized)}')
print(f'std. dev. of standardized values: {np.std(data_standardized): .2f}')
>>>
standardized values: [-1.414213562373095, -0.7071067811865475, 0.0, 0.7071067811865475, 1.414213562373095]
mean of standardized values: 0.0
std. dev. of standardized values: 1.00
如我们所见,归一化的数据均值为 0,标准差为 1,正如我们所期望的。好消息是我们可以使用 scikit-learn
库来标准化特征,我们马上就会做到这一点。
特征缩放是在处理 ML 问题时需要做的一项重要工作,原因很简单:
- 如果我们用未缩放的特征进行探索性数据分析,在计算均值时(例如,在计算相关系数时),我们会得到非常不同的数字。如果我们查看之前通过
df.describe()
方法得到的统计数据,我们可以看到每一列的均值差异很大。如果我们缩放或归一化特征,我们会得到 0、1 和 -1,这将从数学上帮助我们。
现在,这个数据集有一些 NaN
值。为了简洁我们不展示这些值(可以自行尝试),但我们会去除它们。同时,我们会计算相关矩阵:
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np
# Drop NaNs from dataframe
df = df.dropna(axis=0)
# Apply mask
mask = np.triu(np.ones_like(df.corr()))
# Heat map for correlation coefficient
sns.heatmap(df.corr(), annot=True, fmt="0.1", mask=mask)
我们数据框的相关矩阵。图片由 Federico Trotta 提供。
因此,使用 np.triu(np.ones_like(df.corr()))
我们创建了一个掩码,这对显示三角相关矩阵非常有用,这样更易读(特别是当我们有比这更多的特征时)。
因此,Total Bsmt SF
和 SalePrice
之间有中等的 0.6
相关性,Gr Liv Area
和 SalePrice
之间有较高的 0.7
相关性,Overall Qual
和 SalePrice
之间有很高的相关性 0.8
;此外,Overall Qual
和 Gr Liv Area
之间有中等的相关性 0.6
,Overall Qual
和 Total Bsmt SF
之间有 0.5
的相关性。
这里没有多重共线性,因此没有特征彼此高度相关(所以我们的特征满足上面列出的第 5 个假设)。如果我们发现一些高度相关的特征,我们可以删除它们,因为两个高度相关的特征对标签的影响是一样的(这适用于每个通用 ML 模型:如果两个特征高度相关,我们可以删除其中一个)。
最后,我们将数据框df
细分为X
(特征)和y
(标签),并对特征进行缩放:
from sklearn.preprocessing import StandardScaler
# Define the features
X = df.iloc[:,:-1]
# Define the label
y = df.iloc[:,-1]
# Scale the features
scaler = StandardScaler() # Call the scaler
X = scaler.fit_transform(X) # Fit the features to scale them
拟合线性回归模型
现在我们需要将特征X
拆分为训练集和测试集,并用线性回归模型拟合它们。然后,我们计算两个数据集的 R²:
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn import metrics
# Split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
# Fit the LR model
reg = LinearRegression().fit(X_train, y_train)
# Calculate R²
coeff_det_train = reg.score(X_train, y_train)
coeff_det_test = reg.score(X_test, y_test)
# Print metrics
print(f" R² for training set: {coeff_det_train}")
print(f" R² for test set: {coeff_det_test}")
>>>
R² for training set: 0.77
R² for test set: 0.73
**Notes:**
1) your results can be slightly different due to the stocastical
nature of the ML models.
2) here we can see generalization on action:
we fitted the Linear Regression model to the train set with
*reg = LinearRegression().fit(X_train, y_train)*.
The, we've calculated R² on the training and test sets with:
*coeff_det_train = reg.score(X_train, y_train)
coeff_det_test = reg.score(X_test, y_test*
In other words: we don't fit the data to the test set.
We fit the data to the training set and we calculate the scores
and predictions (see next snippet of code with KDE) on both sets
to see the generalization of our modelon new unseen data
(the data of the test set).
所以我们在训练测试集上得到 R²为 0.77,在测试集上为 0.73,这些结果相当不错,表明线性模型在解决这个 ML 问题上表现良好。
让我们来看一下两个数据集的 KDE 图:
# Calculate predictions
y_train_pred = reg.predict(X_train) # train set
y_test_pred = reg.predict(X_test) # test set
# KDE train set
ax = sns.kdeplot(y_train, color='r', label='Actual Values') #actual values
sns.kdeplot(y_train_pred, color='b', label='Predicted Values', ax=ax) #predicted values
# Show title
plt.title('Actual vs Predicted values')
# Show legend
plt.legend()
训练集的 KDE。图片由 Federico Trotta 提供。
# KDE test set
ax = sns.kdeplot(y_test, color='r', label='Actual Values') #actual values
sns.kdeplot(y_test_pred, color='b', label='Predicted Values', ax=ax) #predicted values
# Show title
plt.title('Actual vs Predicted values')
# Show legend
plt.legend()
测试集的 KDE。图片由 Federico Trotta 提供。
尽管我们在测试集上获得了 0.73 的 R²,这个结果已经很好(但记住:越高越好),这个图表显示了线性模型确实是解决这个 ML 问题的一个好模型。这就是我喜欢 KDE 图的原因:它是一个非常强大的工具,正如我们所看到的。
同时,这也说明了为什么不应该仅依赖一种方法来验证我们的 ML 模型:将一种分析方法与一种图形方法结合使用,通常能为我们提供正确的见解,以决定是否需要更改我们的 ML 模型。在这种情况下,线性回归模型非常适合进行预测。
结论
希望你能觉得这篇文章有用。我知道它很长,但我想给你提供所有你需要的知识,以便你在最需要的时候可以回顾它。
我们在这里讨论的一些内容是通用话题,而其他一些则特定于线性回归模型。让我们总结一下:
-
回归的定义当然是一个通用定义。
-
相关性通常被称为线性模型。 实际上,正如我们之前所说,相关性是两个变量线性依赖的趋势。然而, 也有定义非线性相关性的方法,但我们将其留给其他文章(不过,作为知识补充:请考虑它们的存在)。
-
我们已经讨论了简单线性回归和多重线性回归模型及其假设(这些假设适用于这两种模型)。
-
当谈到如何找到最适合数据的回归线时,我们参考了文章“掌握回归分析艺术:每个数据科学家都应该知道的 5 个关键指标”。在这里,我们找到了解决回归分析所需了解的所有指标。因此,这是一个通用话题,适用于任何回归模型,包括线性模型。
-
我们展示了验证 ML 模型的三种方法:1) 残差分析图:适用于线性回归模型,2) 实际值与预测值图:适用于线性和多项式模型,3) KDE 图:适用于任何 ML 模型,即使是分类问题。
最后,我想提醒你,我们花了几行文字强调我们可以避免使用 p-values
来检验 ML 模型的假设。我很快会写一篇关于这个话题的文章,但正如你所看到的,KDE 已经向我们展示了我们的线性模型能够解决这个 ML 问题,并且我们没有用 p-values
来验证我们的假设。
到目前为止,在本文中,我们使用了一些图表。你可以 克隆这个仓库 我创建的,以便你可以导入代码并轻松绘制图表。如果你遇到困难,可以在我的 GitHub 项目中找到用法示例。如果你有其他困难,你可以 联系我 我会帮助你。
免费 Python 电子书:
刚开始学习 Python 数据科学但感到困难? 订阅我的新闻通讯并获取我的免费电子书:这将为你提供正确的学习路径,通过实际操作学习 Python 数据科学。
觉得这个故事有趣吗?通过我的推荐链接以 5$/月 成为 Medium 会员 点击这里:我会赚取少量佣金,你无需额外费用。
[## 通过我的推荐链接加入 Medium - Federico Trotta
阅读 Federico Trotta 的每一个故事(以及 Medium 上成千上万其他作家的故事)。您的会员费直接支持…
federicotrotta.medium.com](https://federicotrotta.medium.com/membership?source=post_page-----7abd37fcb9ed--------------------------------)
精通逻辑回归
原文:
towardsdatascience.com/mastering-logistic-regression-3e502686f0ae
从理论到 Python 实现
·发表于 Towards Data Science ·17 分钟阅读·2023 年 5 月 20 日
--
图片来自 Gerd Altmann 的 Pixabay
逻辑回归是最常见的机器学习算法之一。它可以用来预测事件发生的概率,例如预测来邮件是否为垃圾邮件,或者肿瘤是否为恶性肿瘤,基于给定的标记数据集。
由于其简单性,逻辑回归通常被用作评估其他更复杂模型的基准。
该模型的名称中包含“逻辑”一词,因为它使用逻辑函数(Sigmoid)将输入特征的线性组合转换为概率。
它的名称中也包含“回归”一词,因为它的输出是一个介于 0 和 1 之间的连续值,尽管它通常作为二分类器使用,通过选择一个阈值(通常为 0.5),并将概率大于阈值的输入分类为正类,而将低于阈值的输入分类为负类。
在本文中,我们将深入讨论逻辑回归模型,从头开始在 Python 中实现它,然后展示其在 Scikit-Learn 中的实现。
背景:二分类问题
回顾一下在 监督机器学习 问题中,我们会得到一个包含 n 个标记样本的训练集:D = {(x₁, y₁), (x₂, y₂), … , (xₙ, yₙ)},其中 xᵢ 是一个 m 维向量,包含样本 i 的特征,yᵢ 代表该样本的标签。我们的目标是构建一个预测尽可能接近真实标签的模型。
在分类问题中,标签 yᵢ 可以取 k 个值之一,表示样本所属的 k 个类别。更具体地说,在二分类问题中,标签 yᵢ 只能取两个值:0(表示负类)和 1(表示正类)。
此外,我们区分两种类型的分类器:
-
确定性分类器为每个样本输出一个硬标签,而不提供类别的概率估计。这类分类器的例子包括感知机、K-近邻和支持向量机(SVM)。
-
概率分类器输出类别的概率估计,然后根据这些概率为给定样本分配标签(通常是具有最高概率的类别标签)。这类分类器的例子包括逻辑回归、朴素贝叶斯分类器和使用 sigmoid 或 softmax 作为输出层的神经网络。
逻辑回归模型
逻辑回归是一种处理二分类问题的概率分类器。给定一个样本 (x, y),它输出样本属于正类的概率 p:
如果这个概率高于某个阈值(通常选择为 0.5),则样本被分类为 1,否则被分类为 0。
模型如何估计概率p?
逻辑回归的基本假设是样本属于正类的事件的对数赔率是其特征的线性组合。
对数赔率(也称为logit)是赔率比的对数,赔率比是样本属于正类的概率与样本属于负类的概率之间的比率:
对数赔率(logit)函数
我们在这里假设对数的底数是 e(即自然对数),尽管也可以使用其他底数。
logit 函数的图像如下所示:
logit 函数
如图所示,logit 函数将 (0, 1) 区间的概率值映射到 (-∞, +∞) 区间的实数值。
在逻辑回归中,我们假设对数赔率是特征的线性组合,即:
其中 w = (w₀, …, wₘ) 是模型的参数(或权重)。参数 w₀ 通常被称为截距(或偏置)。
对于 p = 0.5(即对数赔率等于 0)的点定义了两个类别之间的分隔超平面,其方程为:
分隔超平面的方程
权重向量 w 与此超平面正交。超平面上方的每个样本 (wᵗx > 0) 被分类为正样本,而超平面下方的每个样本 (wᵗx < 0) 被分类为负样本:
这使得逻辑回归成为一种线性分类器,因为它假设类别之间的边界是一个线性表面。其他线性分类器包括感知器和支持向量机(SVM)。
我们可以通过对对数几率方程两边取指数,找到 p 和参数 w 之间的直接关联:
其中 σ 是Sigmoid 函数(也称为逻辑函数):
Sigmoid 函数用于将对数几率 (wᵗx) 转换为概率。它具有一个特征“ S”形曲线:
Sigmoid 函数
如可以看出,该函数将实数范围 (-∞, +∞) 映射为概率值范围 (0, 1)。
Sigmoid 函数具有一些优良的数学性质,这些性质在后面会很有用:
下图总结了从输入到最终预测的逻辑回归计算过程:
逻辑回归模型
对数损失
我们的目标是找到参数w,使模型的预测 p = σ(wᵗx) 尽可能接近真实标签 y。为此,我们需要定义一个损失函数,用来衡量模型预测与真实标签之间的差距。这个函数需要是可微分的,以便可以使用如梯度下降等技术进行优化(有关机器学习中的损失函数的更多信息,请参见这篇文章)。
逻辑回归使用的损失函数称为对数损失(或逻辑损失)。其定义如下:
对数损失函数
我们是如何得到这个函数的?这个函数的推导基于最大似然原理。更具体地说,我们可以证明对数损失是在标签具有伯努利分布(即一种二元随机变量的概率分布,其中 1 的概率为 p,0 的概率为 1 − p)假设下的负对数似然。
从数学上讲,我们将证明:
其中 P(y|p) 是在模型预测 p 给定的情况下获得标签 y 的概率(即,数据在我们的模型下的似然)。
证明:
给定一个具有参数 p 的贝努利分布模型(标签),样本属于正类的概率就是 p,即,
类似地,样本属于负类的概率是:
我们可以将这两个方程更紧凑地写成如下形式:
解释:当 y = 1 时,pʸ = p 和 (1 − p)¹⁻ʸ = 1,因此 P(y|p) = p。类似地,当 y = 0 时,pʸ = 1 和 (1 − p)¹⁻ʸ = 1 − p,因此 P(y|p) = 1 − p。
因此,给定模型的数据的对数似然是:
对数损失恰好是该函数的负值。因此,最大化对数似然等同于最小化对数损失。
以下图展示了当 y = 1 时的对数损失:
对数损失仅在预测完全准确时(p = 1 且 y = 1,或 p = 0 且 y = 0)为 0,并且当预测变差时(即,当 y = 1 且 p → 0 或 y = 0 且 p → 1)接近无穷大。
成本函数 计算整个数据集上的平均损失:
这个函数可以用向量化的形式表示如下:
其中 y = (y₁, …, yₙ) 是一个包含所有训练样本标签的向量,而 p = (p₁, …, pₙ) 是一个包含模型对所有训练样本的预测概率的向量。
这个成本函数是凸的,即,它有一个全局最小值。然而,由于对数函数引入的非线性,没有封闭形式的解决方案来找到最佳 w*。因此,我们需要使用迭代优化方法如梯度下降来找到最小值。
梯度下降
梯度下降是一种迭代方法,用于寻找函数的最小值,其中我们沿着梯度的相反方向采取小步,以接近最小值:
梯度下降
为了使用梯度下降法找到成本 J(w)* 的最小值,我们需要计算其相对于每一个权重的偏导数。J(w)* 对于给定权重 wⱼ 的偏导数为:
证明:
因此,梯度向量可以用向量化的形式表示如下:
梯度下降更新规则为:
其中α是一个学习率,控制步长(0 < α < 1)。
请注意,每当你使用梯度下降时,你必须确保数据集已经标准化(否则梯度下降可能会在不同方向上采取不同大小的步伐,这会导致不稳定)。
Python 实现
现在我们将从头实现逻辑回归模型,包括成本函数和梯度计算,使用梯度下降优化模型,模型评估以及绘制最终的决策边界。
为了演示,我们将使用Iris 数据集(BSD 许可证)。原始数据集包含 150 个属于三种花卉之一的鸢尾花样本(山鸢尾、变色鸢尾和维吉尼亚鸢尾)。我们将其转化为一个二分类问题,只使用前两种花卉(山鸢尾和变色鸢尾)。此外,我们只使用每朵花的前两个特征(花萼宽度和花萼长度)。
加载数据集
首先,我们导入所需的库并固定随机种子,以获得可重复的结果:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
np.random.seed(0)
接下来,我们加载数据集:
from sklearn.datasets import load_iris
iris = load_iris()
X = iris.data[:, :2] # Take only the first two features
y = iris.target
# Take only the setosa and versicolor flowers
X = X[(y == 0) | (y == 1)]
y = y[(y == 0) | (y == 1)]
让我们绘制数据:
def plot_data(X, y):
sns.scatterplot(x=X[:, 0], y=X[:, 1], hue=iris.target_names[y], style=iris.target_names[y],
palette=['r','b'], markers=('s','o'), edgecolor='k')
plt.xlabel(iris.feature_names[0])
plt.ylabel(iris.feature_names[1])
plt.legend()
plot_data(X, y)
鸢尾花数据集
如可以看到,数据集是线性可分的,因此逻辑回归应该能够找到两个类别之间的边界。
接下来,我们需要向特征矩阵X中添加一列 1,以表示偏置项(w₀):
# Add a column for the bias
n = X.shape[0]
X_with_bias = np.hstack((np.ones((n, 1)), X))
现在我们将数据集分成训练集和测试集:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X_with_bias, y, random_state=0)
模型实现
我们现在准备实现逻辑回归模型。我们从定义一个辅助函数来计算 sigmoid 函数开始:
def sigmoid(z):
""" Compute the sigmoid of z (z can be a scalar or a vector). """
z = np.array(z)
return 1 / (1 + np.exp(-z))
接下来,我们实现成本函数,该函数返回给定数据集(X,y)上具有参数w的逻辑回归模型的成本,以及相对于w的梯度。
def cost_function(X, y, w):
""" J, grad = cost_function(X, y, w) computes the cost of a logistic regression model
with parameters w and the gradient of the cost w.r.t. to the parameters. """
# Compute the cost
p = sigmoid(X @ w)
J = -(1/n) * (y @ np.log(p) + (1-y) @ np.log(1-p))
# Compute the gradient
grad = (1/n) * X.T @ (p - y)
return J, grad
请注意,我们正在使用之前展示的成本函数和梯度函数的向量化形式。
为了对这个函数进行合理性检查,我们来计算模型在某个随机权重向量上的成本和梯度:
w = np.random.rand(X_train.shape[1])
cost, grad = cost_function(X_train, y_train, w)
print('w:', w)
print('Cost at w:', cost)
print('Gradient at w:', grad)
我们得到的输出是:
w: [0.5488135 0.71518937 0.60276338]
Cost at w: 2.314505839067951
Gradient at w: [0.36855061 1.86634895 1.27264487]
梯度下降实现
我们现在将实现梯度下降,以找到最优的w,使成本函数在给定训练集上最小化。该算法最多会对训练集进行max_iter次迭代(默认为 5000),除非成本在上一次迭代后没有至少减少tol(默认为 0.0001),在这种情况下训练将立即停止。
def optimize_model(X, y, alpha=0.01, max_iter=5000, tol=0.0001):
""" Optimize the model using gradient descent.
X, y: The training set
alpha: The learning rate
max_iter: The maximum number of passes over the training set (epochs)
tol: The stopping criterion. Training will stop when (new_cost > cost - tol)
"""
w = np.random.rand(X.shape[1])
cost, grad = cost_function(X, y, w)
for i in range(max_iter):
w = w - alpha * grad
new_cost, grad = cost_function(X, y, w)
if new_cost > cost - tol:
print(f'Converged after {i} iterations')
return w, new_cost
cost = new_cost
print('Maximum number of iterations reached')
return w, cost
通常在这一点上你需要对数据集进行标准化,因为梯度下降对于具有不同尺度的特征效果不好。在我们的特定数据集中,由于两个特征的范围相似,因此标准化不是必需的。
现在让我们调用这个函数来优化我们的模型:
opt_w, cost = optimize_model(X_train, y_train)
print('opt_w:', opt_w)
print('Cost at opt_w:', cost)
算法在 1,413 次迭代后收敛,我们得到的w*是:
Converged after 1413 iterations
opt_w: [ 0.28014029 0.80541854 -1.48367938]
Cost at opt_w: 0.28389717767222555
还有其他优化器可以使用,这些优化器通常比梯度下降更快,例如共轭梯度(CG)和截断牛顿(TNC)。有关如何使用这些优化器的更多细节,请参见scipy.optimize.minimize。
使用模型进行预测
现在我们已经找到了模型的最佳参数,可以使用它进行预测。
首先,我们编写一个函数,它接受新样本的矩阵X并返回它们属于正类的概率:
def predict_prob(X, w):
""" Return the probability that samples in X belong to the positive class
X: the feature matrix (every row in X represents one sample)
w: the learned logistic regression parameters
"""
p = sigmoid(X @ w)
return p
该函数通过简单地计算Xᵗw的 sigmoid 值来计算模型的预测(即对矩阵中每一行x计算σ(wᵗx))。
例如,让我们找出位于(6, 2)的样本属于 versicolor 类的概率:
predict_prob([[1, 6, 2]], opt_w)
array([0.89522808])
这个样本有 89.52%的概率属于 versicolor 花。这是合理的,因为这个样本位于 versicolor 花的区域内,远离类别之间的边界。
另一方面,位于(5.5, 3)的样本属于 versicolor 类的概率是:
predict_prob([[1, 5.5, 3]], opt_w)
array([0.56436688])
这次概率要低得多(仅 56.44%),因为这个样本接近类别之间的边界。
让我们编写另一个函数,它返回预测的类别标签而不是概率:
def predict(X, w):
""" Predict whether the label is 0 or 1 for the samples in X using a threshold of 0.5
(i.e., if sigmoid(X @ theta) >= 0.5, predict 1)
"""
p = sigmoid(X @ w)
y_pred = (p >= 0.5).astype(int)
return y_pred
该函数简单地在正类的概率至少为 0.5 时预测 1,否则预测 0。
让我们用上面的样本测试这个函数:
predict([[1, 6, 2], [1, 5.5, 3]], opt_w)
array([1, 1])
如预期的那样,这两个样本都被分类为 1。
评估模型
接下来,让我们编写一个函数来计算模型在给定数据集上的准确性:
def evaluate_model(X, y, w):
y_pred = predict(X, w)
accuracy = np.mean(y == y_pred)
return accuracy
该函数首先找到模型在给定数据集X上的预测标签,并将其与真实标签y进行比较。然后计算准确性,作为正确分类的平均数量:
让我们使用这个函数来找出模型在训练集和测试集上的准确性:
train_accuracy = evaluate_model(X_train, y_train, opt_w)
print(f'Train accuracy: {train_accuracy * 100:.3f}%')
Train accuracy: 98.667%
test_accuracy = evaluate_model(X_test, y_test, opt_w)
print(f'Test accuracy: {test_accuracy * 100:.3f}%')
Test accuracy: 100.000%
如预期的那样,由于数据集是线性可分的,得分非常高。
除了准确性,还有其他重要指标用于评估分类模型,如精确度、召回率和 F1 分数。这些指标将在未来的文章中讨论。
绘制决策边界
最后,由于我们的数据集是二维的,我们可以绘制模型找到的类别之间的边界线。为此,我们首先需要找到这条线的方程。
边界线由模型预测值恰好为 0.5 的点定义,即:
当 sigmoid 函数的输入等于 0 时,其值为 0.5,因此我们可以写成:
重新排列项后我们得到:
即,边界线的斜率为 -w₁/w₂,截距为 -w₀/w₂。我们现在可以编写一个绘制这条线的函数:
def plot_decision_boundary(X, y, w):
""" Plot the decision boundary between the classes """
plot_data(X, y)
line_x = np.array(plt.gca().get_xlim())
line_y = -1 / w[2] * (w[1] * line_x + w[0])
plt.plot(line_x, line_y, c='k', ls='--')
plot_decision_boundary(X, y, opt_w)
类别之间的决策边界
我们可以看到,只有一个样本被模型误分类。训练模型更多的迭代(约 200,000 次)会找到一个完美分离两类的分界线。使用固定步长时,梯度下降的最优收敛速度非常慢。可以通过使用自适应学习率(例如,使用更激进的步长来补偿快速消失的梯度)来改进这一点。
Scikit-Learn 中的 LogisticRegression 类
尽管从头实现逻辑回归有其自身的教育意义,但更实际的选择是使用 Scikit-Learn 提供的现成的 LogisticRegression 类。该类使用比普通梯度下降更高效的解算器,并且还提供了额外的选项,如正则化和提前停止。
该类的重要超参数有:
-
penalty — 指定要应用的正则化类型。可以是以下选项之一:None, ‘l2’(默认值), ‘l1’ 和 ‘elasticnet’。有关正则化的更多信息,请参见 这篇文章。
-
tol — 停止准则的容忍度(默认为 0.0001)。
-
C — 正则化系数的倒数(默认为 1.0)。较小的值表示更强的正则化。
-
solver — 用于优化的算法。可以选择以下选项之一:‘lbfgs’(默认值),‘liblinear’,‘newton-cg’,‘newton-cholesky’,‘sag’,‘saga’。有关这些优化器的更多信息,请阅读文档。
-
max_iter — 解算器收敛的最大迭代次数(默认为 100)
-
multi_class — 处理多分类问题的方法。可以选择以下选项之一:‘ovr’(一对其余,即为每个类别构建一个二分类器与其他类别对抗)、‘multinomial’(使用多项式逻辑回归)或 ‘auto’(默认)。
使用 LogisticRegression 类时,您无需手动将一列全是 1 的列添加到设计矩阵 X 中,因为这会由 Scikit-Learn 自动完成。因此,在构建模型之前,我们将原始数据(没有额外的全是 1 的列)分割成训练集和测试集:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
我们将使用默认设置创建一个 LogisticRegression 实例,并将其拟合到训练集上:
from sklearn.linear_model import LogisticRegression
clf = LogisticRegression()
clf.fit(X_train, y_train)
接下来,我们将在训练集和测试集上评估模型:
train_accuracy = clf.score(X_train, y_train)
print(f'Train accuracy: {train_accuracy * 100:.3f}%')
test_accuracy = clf.score(X_test, y_test)
print(f'Test accuracy: {test_accuracy * 100:.3f}%')
Train accuracy: 100.000%
Test accuracy: 100.000%
这次我们在训练集和测试集上都获得了完美的分数。我们还可以通过查询 n_iter_ 属性来检查收敛所需的迭代次数:
print(clf.n_iter_)
[15]
仅需 15 次迭代即可收敛!显然,LogisticRegression 使用的求解器(默认使用L-BFGS)比我们实现的梯度下降更高效。
我们可以像之前一样绘制模型找到的决策边界。然而,这次最佳系数存储在模型的两个不同属性中:
-
coef_ 是一个数组,包含所有权重,除了截距项
-
intercept_ 是截距项 (w₀)
因此,我们需要在调用plot_decision_boundary()函数之前,将这两个属性连接成一个数组:
opt_w = np.insert(clf.coef_, 0, [clf.intercept_])
plot_decision_boundary(X, y, opt_w)
LogisticRegression 找到的决策边界
正如预期的那样,LogisticRegression 找到的线完美地分隔了两个类别。
总结
让我们总结一下逻辑回归与其他分类模型的优缺点。
优点:
-
当数据是线性可分的时,算法保证能找到一个类间的分离超平面。
-
提供类概率估计
-
不容易过拟合(但通常对数据存在欠拟合)
-
高度可解释(与每个特征相关的权重表示其重要性)
-
高度可扩展(需要的参数数量与特征数量线性相关)
-
可以处理冗余特征(通过赋予它们接近 0 的权重)
-
超参数数量较少
缺点:
-
只能找到类别之间的线性决策边界
-
通常被更复杂的模型超越
-
仅支持二分类,但可以扩展到多分类。将逻辑回归扩展到多分类问题(称为多项式逻辑回归或softmax 回归)在这篇文章中有介绍。
-
无法处理缺失值
最终备注
除非另有说明,所有图像均由作者提供。
本文的代码示例可以在我的 github 上找到:github.com/roiyeho/medium/tree/main/logistic_regression
感谢阅读!
精通 Python 中的长短期记忆:释放 LSTM 在 NLP 中的力量
一本关于理解和实现 LSTM 层用于 Python 自然语言处理的全面指南
·发布于Towards Data Science ·17 分钟阅读·2023 年 11 月 28 日
--
Sven Brandsma拍摄的照片,来自Unsplash。
这项工作是我关于 RNN 和 Python NLP 的文章的延续。一个简单的递归层的深度学习网络的自然发展是带有长短期记忆(LSTM)层的深度学习网络。
与 RNN 和 NLP 一样,我将尝试详细解释 LSTM 层,并从头编写该层的前向传播代码。
所有代码可以在这里查看: github.com/Eligijus112/NLP-python
我们将使用与上一篇文章相同的¹数据集:
# Data wrangling
import pandas as pd
# Reading the data
d = pd.read_csv('input/Tweets.csv', header=None)
# Adding the columns
d.columns = ['INDEX', 'GAME', "SENTIMENT", 'TEXT']
# Leaving only the positive and the negative sentiments
d = d[d['SENTIMENT'].isin(['Positive', 'Negative'])]
# Encoding the sentiments that the negative will be 1 and the positive 0
d['SENTIMENT'] = d['SENTIMENT'].apply(lambda x: 0 if x == 'Positive' else 1)
# Dropping missing values
d = d.dropna()
数据集中的随机行;作者拍摄的图片
请记住,SENTIMENT=1 表示负面情感,SENTIMENT=0 表示正面情感。
我们需要将文本数据转换为整数序列。不过,与上一篇文章不同的是,我们现在将创建一个字符序列,而不是单词序列。
例如,文本“Nice Game”可以转换为以下示例向量:
[1, 2, 3, 4, 5, 6, 7, 8, 3]
每个字符,包括空格和标点符号,都会有一个索引。
def create_word_index(
x: str,
shift_for_padding: bool = False,
char_level: bool = False) -> Tuple[dict, dict]:
"""
Function that scans a given text and creates two dictionaries:
- word2idx: dictionary mapping words to integers
- idx2word: dictionary mapping integers to words
Args:
x (str): text to scan
shift_for_padding (bool, optional): If True, the function will add 1 to all the indexes.
This is done to reserve the 0 index for padding. Defaults to False.
char_level (bool, optional): If True, the function will create a character level dictionary.
Returns:
Tuple[dict, dict]: word2idx and idx2word dictionaries
"""
# Ensuring that the text is a string
if not isinstance(x, str):
try:
x = str(x)
except:
raise Exception('The text must be a string or a string convertible object')
# Spliting the text into words
words = []
if char_level:
# The list() function of a string will return a list of characters
words = list(x)
else:
# Spliting the text into words by spaces
words = x.split(' ')
# Creating the word2idx dictionary
word2idx = {}
for word in words:
if word not in word2idx:
# The len(word2idx) will always ensure that the
# new index is 1 + the length of the dictionary so far
word2idx[word] = len(word2idx)
# Adding the <UNK> token to the dictionary; This token will be used
# on new texts that were not seen during training.
# It will have the last index.
word2idx['<UNK>'] = len(word2idx)
if shift_for_padding:
# Adding 1 to all the indexes;
# The 0 index will be reserved for padding
word2idx = {k: v + 1 for k, v in word2idx.items()}
# Reversing the above dictionary and creating the idx2word dictionary
idx2word = {idx: word for word, idx in word2idx.items()}
# Returns the dictionaries
return word2idx, idx2word
让我们将数据拆分为训练集和测试集,并应用我们创建的函数:
# Spliting to train test
train, test = train_test_split(d, test_size=0.2, random_state=42)
# Reseting the indexes
train.reset_index(drop=True, inplace=True)
test.reset_index(drop=True, inplace=True)
print(f'Train shape: {train.shape}')
print(f'Test shape: {test.shape}')
Train shape: (34410, 4)
Test shape: (8603, 4)
# Joining all the texts into one string
text = ' '.join(train['TEXT'].values)
# Creating the word2idx and idx2word dictionaries
word2idx, idx2word = create_word_index(text, shift_for_padding=True, char_level=True)
# Printing the size of the vocabulary
print(f'The size of the vocabulary is: {len(word2idx)}')
The size of the vocabulary is: 274
我们的数据中有 274 个独特的字符。让我们打印word2idx字典中的前 10 项:
{'I': 1,
' ': 2,
'd': 3,
'o': 4,
'w': 5,
'n': 6,
'l': 7,
'a': 8,
'e': 9,
'G': 10
}
让我们将文本转换为序列:
# For each row in the train and test set, we will create a list of integers
# that will represent the words in the text
train['text_int'] = train['TEXT'].apply(lambda x: [word2idx.get(word, word2idx['<UNK>']) for word in list(x)])
test['text_int'] = test['TEXT'].apply(lambda x: [word2idx.get(word, word2idx['<UNK>']) for word in list(x)])
# Calculating the length of sequences in the train set
train['seq_len'] = train['text_int'].apply(lambda x: len(x))
# Describing the length of the sequences
train['seq_len'].describe()
count 34410.000000
mean 103.600262
std 79.972798
min 1.000000
25% 41.000000
50% 83.000000
75% 148.000000
max 727.000000
回顾一下,按词级别拆分文本导致序列的平均长度为~22 个令牌。现在,我们有长度为~103 个令牌的序列。标准差非常高,因此我们将使用最大序列长度为200 进行填充。
def pad_sequences(x: list, pad_length: int) -> list:
"""
Function that pads a given list of integers to a given length
Args:
x (list): list of integers to pad
pad_length (int): length to pad
Returns:
list: padded list of integers
"""
# Getting the length of the list
len_x = len(x)
# Checking if the length of the list is less than the pad_length
if len_x < pad_length:
# Padding the list with 0s
x = x + [0] * (pad_length - len_x)
else:
# Truncating the list to the desired length
x = x[:pad_length]
# Returning the padded list
return x
# Padding the train and test sequences
train['text_int'] = train['text_int'].apply(lambda x: pad_sequences(x, 200))
test['text_int'] = test['text_int'].apply(lambda x: pad_sequences(x, 200))
到目前为止,训练集和验证集的数据集如下:
一段数据;作者拍摄的照片
为什么我们应该从普通的 RNN 切换到 LSTM 网络?问题有两个方面:
-
一个简单的 RNN 有所谓的消失梯度问题²或爆炸梯度问题,与网络中for 循环使用的权重相关。
-
网络往往会“忘记”长序列数据的初始步骤输入。
为了说明遗忘,请考虑以下示例:
在我们的数据中,平均而言,有 103 个时间步(文本中从左到右的令牌数量)。回顾 RNN 文章中的图表:
展开 n 步 RNN;作者拍摄的照片
我们有相同的权重W,用来乘以 ReLU 层的输出。然后,我们将该信号添加到下一个时间步,依此类推。如果我们为W选择一个相对较小的值(例如 0.5),并且我们有 103 步时间序列数据,从第一次时间步输入到最终输出的影响大致为0.5¹⁰³ * input1,这大致等于零。
第二次输入的信号将是0.5¹⁰² * input2,以此类推。
可以看出,随着时间步的增加,初始时间步的信息在最终输出中的占比越来越少。
为了应对遗忘过去的问题,伟大的思想家们提出了用于时间序列问题的 LSTM 层³。
从内部来看,LSTM 层使用两个激活函数:
-
Sigmoid 函数
-
Tanh 函数
关于这些函数要记住的关键事实是:
-
sigmoid 激活函数接受实数平面上的任何值,并输出一个在 0 和 1 之间的值。
-
tanh 函数接受实数平面上的任何值,并输出一个在 -1 和 1 之间的值。
def sigmoid(x: float) -> float:
"""
Function that calculates the sigmoid of a given value
Args:
x (float): value to calculate the sigmoid
Returns:
float: sigmoid of the given value in (0, 1)
"""
return 1 / (1 + np.exp(-x))
def tanh(x: float) -> float:
"""
Function that calculates the tanh of a given value
Args:
x (float): value to calculate the tanh
Returns:
float: tanh of the given value in (-1, 1)
"""
return (np.exp(x) - np.exp(-x)) / (np.exp(x) + np.exp(-x))
既然我们已经了解了 sigmoid 和 tanh 激活函数,让我们回到 LSTM 层。
LSTM 层由 2 部分组成(因此得名):
-
长期记忆块
-
短期记忆块
在每个时间步(或令牌步),LSTM 层输出两个预测:长期预测和短期预测。LSTM 单元的高层次图示可以这样可视化:
展开的简单 LSTM 网络;作者拍摄的图表
在每个时间步骤,LSTM 层输出一个数字,这就是我们所称的短期记忆输出。它通常只是一个标量。此外,长期记忆标量也在 LSTM 层中计算,但它不被输出并传递到序列的第二步。值得注意的是,在每个时间步骤中,短期和长期记忆都会被更新。
现在让我们深入探讨 LSTM 层。LSTM 层的第一部分是所谓的忘记门操作:
忘记门;图表由作者提供
忘记门得名于我们计算希望保留的长期记忆百分比。这是因为 sigmoid 激活函数会输出一个介于 0 和 1 之间的数字,我们将这个数字乘以长期记忆并传递到网络中。
我们可以开始看到在训练时会更新的权重:w1, w2 和 b1。这些权重直接影响保持的长期记忆量。
请注意,在这个步骤中,短期记忆没有调整,而是传递到网络的第二步。
class ForgetGate:
"""
Class that implements the forget gate of an LSTM cell
"""
def __init__(
self,
w1: float = np.random.normal(),
w2: float = np.random.normal(),
b1: float = np.random.normal(),
long_term_memory: float = np.random.normal(),
short_term_memory: float = np.random.normal(),
):
"""
Constructor of the class
Args:
long_term_memory (float): long term memory
short_term_memory (float): short term memory
w1 (float): weight 1
w2 (float): weight 2
b1 (float): bias term 1
"""
# Saving the input
self.long_term_memory = long_term_memory
self.short_term_memory = short_term_memory
self.w1 = w1
self.w2 = w2
self.b1 = b1
def forward(self, x: float) -> float:
"""
Function that calculates the output of the forget gate
Args:
x (float): input to the forget gate
Returns:
float: output of the forget gate
"""
# Calculates the percentage of the long term memory that will be kept
percentage_to_keep = sigmoid((self.w1 * x + self.w2 * self.short_term_memory) + self.b1)
# Updating the long term memory
self.long_term_memory = self.long_term_memory * percentage_to_keep
# The output of the forget gate is the new long term memory and the short term memory
return self.long_term_memory, self.short_term_memory
# Initiating
forget_gate = ForgetGate()
print(f'Initial long term memory: {forget_gate.long_term_memory}')
print(f'Initial short term memory: {forget_gate.short_term_memory}')
# Calculating the output of the forget gate
lt, st = forget_gate.forward(0.5)
print(f'Long term memory: {lt}')
print(f'Short term memory: {st}')
Initial long term memory: -0.8221542907288696
Initial short term memory: -0.5617438418718841
Long term memory: -0.37335827895028
Short term memory: -0.5617438418718841
接下来在 LSTM 层的是输入门:
输入门;图表由作者提供
输入门仅调整 LSTM 网络的长期记忆部分,但为此,它使用当前输入和当前短期记忆值。
从图表来看,在乘法步骤之前,我们有两个输出:一个来自 sigmoid 激活函数,另一个来自 tanh 激活层。宽泛地说,sigmoid 层输出要记住的记忆百分比(0,1),而 tanh 输出记住的潜在记忆(-1,1)。
然后我们将当前长期记忆(在忘记门中稍作调整)与输入门输出相加。
class InputGate:
def __init__(
self,
w3: float = np.random.normal(),
w4: float = np.random.normal(),
w5: float = np.random.normal(),
w6: float = np.random.normal(),
b2: float = np.random.normal(),
b3: float = np.random.normal(),
long_term_memory: float = np.random.normal(),
short_term_memory: float = np.random.normal(),
):
"""
Constructor of the class
Args:
long_term_memory (float): long term memory
short_term_memory (float): short term memory
w3 (float): weight 3
w4 (float): weight 4
w5 (float): weight 5
w6 (float): weight 6
b2 (float): bias 2
b3 (float): bias 3
"""
# Saving the input
self.long_term_memory = long_term_memory
self.short_term_memory = short_term_memory
self.w3 = w3
self.w4 = w4
self.w5 = w5
self.w6 = w6
self.b2 = b2
self.b3 = b3
def forward(self, x: float) -> float:
"""
Function that calculates the output of the input gate
Args:
x (float): input to the input gate
Returns:
float: output of the input gate
"""
# Calculating the memory signal
memory_signal = tanh((self.w3 * x + self.w4 * self.short_term_memory) + self.b2)
# Calculating the percentage of memory to keep
percentage_to_keep = sigmoid((self.w5 * x + self.w6 * self.short_term_memory) + self.b3)
# Multiplying the memory signal by the percentage to keep
memory_signal = memory_signal * percentage_to_keep
# Updating the long term memory
self.long_term_memory = self.long_term_memory + memory_signal
# The output of the input gate is the new long term memory and the short term memory
return self.long_term_memory, self.short_term_memory
# Creating the input gate object with the forget gates' output
input_gate = InputGate(long_term_memory=lt, short_term_memory=st)
# Forward propagating
lt, st = input_gate.forward(0.5)
print(f'Long term memory: {lt}')
print(f'Short term memory: {st}')
Long term memory: -1.028998511766425
Short term memory: -0.5617438418718841
从上面的代码片段可以看出,唯一变化的是长期记忆。
LSTM 层的最后一部分是输出门。输出门是我们将调整短期记忆的步骤:
输出门;图表由作者提供
逻辑与之前门的逻辑非常相似:sigmoid 激活函数计算要保留的记忆百分比,而 tanh 函数计算总体信号。
class OutputGate:
def __init__(
self,
w7: float = np.random.normal(),
w8: float = np.random.normal(),
b4: float = np.random.normal(),
long_term_memory: float = np.random.normal(),
short_term_memory: float = np.random.normal(),
):
"""
Constructor of the class
Args:
long_term_memory (float): long term memory
short_term_memory (float): short term memory
w7 (float): weight 7
w8 (float): weight 8
w9 (float): weight 9
w10 (float): weight 10
b4 (float): bias 4
b5 (float): bias 5
"""
# Saving the input
self.long_term_memory = long_term_memory
self.short_term_memory = short_term_memory
self.w7 = w7
self.w8 = w8
self.b4 = b4
def forward(self, x: float) -> float:
"""
Function that calculates the output of the output gate
Args:
x (float): input to the output gate
Returns:
float: output of the output gate
"""
# Calculating the short term memory signal
short_term_memory_signal = tanh(self.long_term_memory)
# Calculating the percentage of short term memory to keep
percentage_to_keep = sigmoid((self.w7 * x + self.w8 * self.short_term_memory) + self.b4)
# Multiplying the short term memory signal by the percentage to keep
short_term_memory_signal = short_term_memory_signal * percentage_to_keep
# Updating the short term memory
self.short_term_memory = short_term_memory_signal
# The output of the output gate is the new long term memory and the short term memory
return self.long_term_memory, self.short_term_memory
# Creating the output gate object
output_gate = OutputGate(long_term_memory=lt, short_term_memory=st)
# Forward propagating
lt, st = output_gate.forward(0.5)
print(f'Long term memory: {lt}')
print(f'Short term memory: {st}')
Long term memory: -1.028998511766425
Short term memory: -0.7233077589896045
如我们所见,输出门仅调整了短期记忆标量。
LSTM 层;图表由作者提供
上图显示了忘记门、输入门和输出门的合成图⁴。
当我们有一个 x 变量的输入序列时,使用 LSTM 层的内部循环是这样的:
-
随机初始化短期和长期记忆。
-
对于每个x1到xn:
2.1 通过 LSTM 层向前传播。
2.2 输出短期记忆
将长期和短期记忆保存到层中。
让我们将每个门封装到一个类中,并创建一个 Python 示例。
# Redefining the forget, input and output gates as functions
def forget_gate(x: float, w1: float, w2: float, b1: float, long_term_memory: float, short_term_memory: float) -> Tuple[float, float]:
"""
Function that calculates the output of the forget gate
Args:
x (float): input to the forget gate
w1 (float): weight 1
w2 (float): weight 2
b1 (float): bias 1
long_term_memory (float): long term memory
short_term_memory (float): short term memory
Returns:
Tuple[float, float]: output of the forget gate
"""
# Calculates the percentage of the long term memory that will be kept
percentage_to_keep = sigmoid((w1 * x + w2 * short_term_memory) + b1)
# Updating the long term memory
long_term_memory = long_term_memory * percentage_to_keep
# The output of the forget gate is the new long term memory and the short term memory
return long_term_memory, short_term_memory
def input_gate(x: float, w3: float, w4: float, w5: float, w6: float, b2: float, b3: float, long_term_memory: float, short_term_memory: float) -> Tuple[float, float]:
"""
Function that calculates the output of the input gate
Args:
x (float): input to the input gate
w3 (float): weight 3
w4 (float): weight 4
w5 (float): weight 5
w6 (float): weight 6
b2 (float): bias 2
b3 (float): bias 3
long_term_memory (float): long term memory
short_term_memory (float): short term memory
Returns:
Tuple[float, float]: output of the input gate
"""
# Calculating the memory signal
memory_signal = tanh((w3 * x + w4 * short_term_memory) + b2)
# Calculating the percentage of memory to keep
percentage_to_keep = sigmoid((w5 * x + w6 * short_term_memory) + b3)
# Multiplying the memory signal by the percentage to keep
memory_signal = memory_signal * percentage_to_keep
# Updating the long term memory
long_term_memory = long_term_memory + memory_signal
# The output of the input gate is the new long term memory and the short term memory
return long_term_memory, short_term_memory
def output_gate(x: float, w7: float, w8: float, b4: float, long_term_memory: float, short_term_memory: float) -> Tuple[float, float]:
"""
Function that calculates the output of the output gate
Args:
x (float): input to the output gate
w7 (float): weight 7
w8 (float): weight 8
b4 (float): bias 4
long_term_memory (float): long term memory
short_term_memory (float): short term memory
Returns:
Tuple[float, float]: output of the output gate
"""
# Calculating the short term memory signal
short_term_memory_signal = tanh(long_term_memory)
# Calculating the percentage of short term memory to keep
percentage_to_keep = sigmoid((w7 * x + w8 * short_term_memory) + b4)
# Multiplying the short term memory signal by the percentage to keep
short_term_memory_signal = short_term_memory_signal * percentage_to_keep
# Updating the short term memory
short_term_memory = short_term_memory_signal
# The output of the output gate is the new long term memory and the short term memory
return long_term_memory, short_term_memory
class simpleLSTM:
def __init__(
self,
w1: float = np.random.normal(),
w2: float = np.random.normal(),
w3: float = np.random.normal(),
w4: float = np.random.normal(),
w5: float = np.random.normal(),
w6: float = np.random.normal(),
w7: float = np.random.normal(),
w8: float = np.random.normal(),
b1: float = np.random.normal(),
b2: float = np.random.normal(),
b3: float = np.random.normal(),
b4: float = np.random.normal(),
long_term_memory: float = np.random.normal(),
short_term_memory: float = np.random.normal(),
):
"""
Constructor of the class
Args:
long_term_memory (float): long term memory
short_term_memory (float): short term memory
w1 (float): weight 1
w2 (float): weight 2
w3 (float): weight 3
w4 (float): weight 4
w5 (float): weight 5
w6 (float): weight 6
w7 (float): weight 7
w8 (float): weight 8
b1 (float): bias 1
b2 (float): bias 2
b3 (float): bias 3
b4 (float): bias 4
"""
# Saving the input
self.long_term_memory = long_term_memory
self.short_term_memory = short_term_memory
self.w1 = w1
self.w2 = w2
self.w3 = w3
self.w4 = w4
self.w5 = w5
self.w6 = w6
self.w7 = w7
self.w8 = w8
self.b1 = b1
self.b2 = b2
self.b3 = b3
self.b4 = b4
def forward(self, x: float) -> float:
"""
Function that calculates the output of the simple LSTM cell
Args:
x (float): input to the simple LSTM cell
Returns:
float: output of the simple LSTM cell
"""
# Calculating the output of the forget gate
lt, st = forget_gate(x, self.w1, self.w2, self.b1, self.long_term_memory, self.short_term_memory)
# Updating the long term memory
self.long_term_memory = lt
# Calculating the output of the input gate
lt, st = input_gate(x, self.w3, self.w4, self.w5, self.w6, self.b2, self.b3, self.long_term_memory, self.short_term_memory)
# Updating the long term memory
self.long_term_memory = lt
# Calculating the output of the output gate
lt, st = output_gate(x, self.w7, self.w8, self.b4, self.long_term_memory, self.short_term_memory)
# Updating the short term memory
self.short_term_memory = st
# The output of the simple LSTM cell is the new long term memory and the short term memory
return self.long_term_memory, self.short_term_memory
def forward_sequence(self, x: list) -> list:
"""
Function that forward propagates a sequence of inputs through the simple LSTM cell
Args:
x (list): sequence of inputs to the simple LSTM cell
Returns:
list: sequence of outputs of the simple LSTM cell
"""
# Creating a list to store the outputs
outputs = []
# Forward propagating each input
for input in x:
# Forward propagating the input
_, st = self.forward(input)
# Appending the output to the list
outputs.append(st)
# Returning the list of outputs
return outputs
# Creating the simple LSTM cell object
simple_lstm = simpleLSTM()
# Creating a sequence of x
x = [0.5, 0.6, 0.7, 0.8, 0.9]
# Forward propagating the sequence
outputs = simple_lstm.forward_sequence(x)
# Rounding
outputs = [round(output, 2) for output in outputs]
# Printing the outputs
print(f'The outputs of the simple LSTM cell are: {outputs}')
The outputs of the simple LSTM cell are: [0.63, 0.41, 0.33, 0.28, 0.25]
现在我们将所有内容包裹在一个漂亮的 pytorch 示例中,使用 LSTM 层。语法与基本的 RNN 模型非常相似:
# Defining the torch model for sentiment classification
class SentimentClassifier(torch.nn.Module):
"""
Class that defines the sentiment classifier model
"""
def __init__(self, vocab_size, embedding_dim):
super(SentimentClassifier, self).__init__()
self.embedding = nn.Embedding(vocab_size + 1, embedding_dim)
self.lstm = nn.LSTM(input_size=embedding_dim, hidden_size=1, batch_first=True)
self.fc = nn.Linear(1, 1) # Output with a single neuron for binary classification
self.sigmoid = nn.Sigmoid() # Sigmoid activation
def forward(self, x):
x = self.embedding(x) # Embedding layer
output, _ = self.lstm(x) # RNN layer
# Use the short term memory from the last time step as the representation of the sequence
x = output[:, -1, :]
# Fully connected layer with a single neuron
x = self.fc(x)
# Converting to probabilities
x = self.sigmoid(x)
# Flattening the output
x = x.squeeze()
return x
# Initiating the model
model = SentimentClassifier(vocab_size=len(word2idx), embedding_dim=16)
# Initiating the criterion and the optimizer
criterion = nn.BCELoss() # Binary cross entropy loss
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
# Defining the data loader
from torch.utils.data import Dataset, DataLoader
class TextClassificationDataset(Dataset):
def __init__(self, data):
self.data = data
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
# The x is named as text_int and the y as airline_sentiment
x = self.data.iloc[idx]['text_int']
y = self.data.iloc[idx]['SENTIMENT']
# Converting the x and y to torch tensors
x = torch.tensor(x)
y = torch.tensor(y)
# Converting the y variable to float
y = y.float()
# Returning the x and y
return x, y
# Creating the train and test loaders
train_loader = DataLoader(TextClassificationDataset(train), batch_size=32, shuffle=True)
test_loader = DataLoader(TextClassificationDataset(test), batch_size=32, shuffle=True)
# Defining the number of epochs
epochs = 100
# Setting the model to train mode
model.train()
# Saving of the loss values
losses = []
# Iterating through epochs
for epoch in range(epochs):
# Initiating the total loss
total_loss = 0
for batch_idx, (inputs, labels) in enumerate(train_loader):
# Zero the gradients
optimizer.zero_grad() # Zero the gradients
outputs = model(inputs) # Forward pass
loss = criterion(outputs, labels) # Compute the loss
loss.backward() # Backpropagation
optimizer.step() # Update the model's parameters
# Adding the loss to the total loss
total_loss += loss.item()
# Calculating the average loss
avg_loss = total_loss / len(train_loader)
# Appending the loss to the list containing the losses
losses.append(avg_loss)
# Printing the loss every n epochs
if epoch % 20 == 0:
print(f'Epoch: {epoch}, Loss: {avg_loss}')
Epoch: 0, Loss: 0.6951859079329055
Epoch: 20, Loss: 0.6478807757224292
Epoch: 40, Loss: 0.6398377026877882
Epoch: 60, Loss: 0.6353290403144067
Epoch: 80, Loss: 0.6312290856884758
# Setting the model to eval model
model.eval()
# List to track the test acc
total_correct = 0
total_obs = 0
# Iterating over the test set
for batch_idx, (inputs, labels) in enumerate(test_loader):
outputs = model(inputs) # Forward pass
# Getting the number of correct predictions
correct = ((outputs > 0.5).float() == labels).float().sum()
# Getting the total number of predictions
total = labels.size(0)
# Updating the total correct and total observations
total_correct += correct
total_obs += total
print(f'The test accuracy is: {total_correct / total_obs}')
The test accuracy is: 0.6447750926017761
这篇文章深入探讨了 LSTM 单元的内部工作细节。某些 LSTM 层的实现可能与这里展示的有所不同,但长期和短期记忆的整体部分在绝大多数实现中都存在。
我希望读者现在对 LSTM 层有了更好的理解,并希望他们能够立即将其实施到他们的工作流程中!
特别感谢 StatQuest 提供的精彩讲解视频⁵。
[1]
名称: 推特情感分析
网址: www.kaggle.com/datasets/jp797498e/twitter-entity-sentiment-analysis
数据集许可证: creativecommons.org/publicdomain/zero/1.0/
[2]
名称: 梯度消失问题
[3]
名称: 长短期记忆
网址: www.bioinf.jku.at/publications/older/2604.pdf
年份: 1997
[4]
名称: 理解 LSTM 网络
网址: colah.github.io/posts/2015-08-Understanding-LSTMs/
年份: 2015
[5]
名称: 长短期记忆(LSTM),清晰解释
网址: www.youtube.com/watch?v=YCzL96nL7j0
年份: 2022
掌握模型可解释性:对部分依赖图的全面分析
开始你在可解释 AI 世界中的旅程。
·
关注 发表在 Towards Data Science ·7 分钟阅读·2023 年 7 月 7 日
--
图片由 David Pupăză 提供,来源于 Unsplash
了解如何解释你的模型对于确定其是否表现异常至关重要。你对模型了解得越多,当它投入生产时,你就越不容易被其行为所惊讶。
此外,你对模型的领域知识越多,你就越能更好地向业务部门推销它。最糟糕的情况是他们意识到你实际上并不确定你在向他们销售什么。
我从未开发过一个不需要解释如何根据输入变量进行预测的模型。至少,向业务说明哪些特征有正面或负面影响是必不可少的。
你可以使用的一个工具来理解你的模型如何工作的就是部分依赖图(PDP),我们将在这篇文章中探讨它。
PDP 是什么
PDP 是一种全局可解释性方法,专注于向你展示模型的特征值如何与模型的输出相关。
这不是理解数据的方法,它只为你的模型生成见解,因此无法从中推断出目标与特征之间的因果关系。然而,它可以让你对模型进行因果推断。
这是因为该方法探测你的模型,因此你可以准确看到当特征变量改变时模型的表现。
它是如何工作的
首先,PDP 允许我们一次只研究一个或两个特征。在这篇文章中,我们将专注于单个特征分析的情况。
在模型训练后,我们生成一个探测数据集。该数据集按照以下算法创建:
-
我们选择我们感兴趣的特征的每个唯一值
-
对于每个唯一值,我们制作一份整个数据集的副本,将特征值设置为该唯一值
-
然后,我们使用模型对这个新数据集进行预测
-
最后,我们对每个唯一值的模型预测进行平均
让我们举个例子。假设我们有以下数据集:
现在,如果我们想将 PDP 应用于特征 0,我们将为特征的每个唯一值重复数据集,例如:
然后,在应用我们的模型后,我们将得到类似这样的结果:
然后,我们计算每个值的平均输出,最终得到以下数据集:
然后只是将这些数据用折线图绘制出来。
对于回归问题,计算每个特征值的平均输出是简单的。对于分类方法,我们可以使用每个类别的预测概率,然后对这些值进行平均。在这种情况下,我们将为数据集中的每个特征和类别对生成一个 PDP。
数学解释
PDP 的解释是我们在边际化一个或两个特征,以评估它们对模型预测输出的边际效应。这由以下公式给出:
其中 \(f\) 是机器学习模型,\(x_S\) 是我们感兴趣分析的特征集合,\(x_C\) 是我们将要平均的其他特征集合。上述函数可以使用以下近似计算:
PDP 的问题
PDP 有一些我们必须注意的限制。首先,由于我们在每个特征值上平均输出,我们最终会得到一个覆盖数据集中每个值的图,即使该值仅出现一次。
因此,你可能会看到数据集中一些非常少见区域的行为,这可能不代表如果这些值更频繁时会发生的情况。因此,在查看 PDP 时,始终查看特征的分布是有帮助的,以了解哪些值更可能发生。
另一个问题发生在特征值相互抵消的情况下。例如,如果你的特征具有以下分布:
计算该特征的 PDP 时,我们将得到类似于以下的结果:
注意特征的影响绝非零,但其平均值为零。这可能会误导你认为特征是无用的,而实际上并非如此。
这种方法的另一个问题是,当我们分析的特征与我们正在平均的特征相关时。如果特征之间存在相关性,如果我们强制数据集中的每个值都具有感兴趣特征的每个值,我们将创建不现实的点。
想象一个包含降雨量和天空中云量的数据集。当我们对降雨量进行平均时,我们会得到一些说天空中有降雨却没有云的点,这是一个不可行的点。
解读 PDP
让我们看看如何分析部分依赖图。请查看下面的图像:
在 x 轴上,我们有特征 0 的值,在 y 轴上,我们有模型对每个特征值的平均输出。注意,对于小于-0.10 的值,模型输出的目标预测非常低,此后预测值上升,然后在特征值超过 0.09 之前,预测值围绕 150 波动,在特征值超过 0.09 时,预测值开始急剧上升。
因此,我们可以说特征与目标预测之间存在正相关关系,但这种相关性不是线性的。
ICE 图
ICE 图尝试解决特征值相互抵消的问题。基本上,在 ICE 图中,我们绘制模型对每个值的每个单独预测,而不仅仅是其平均值。
在 Python 中实现 PDP
让我们在 Python 中实现 PDP。为此,我们首先要导入所需的库:
import numpy as np
import matplotlib.pyplot as plt
from tqdm import tqdm
from sklearn.datasets import load_diabetes
from sklearn.ensemble import RandomForestRegressor
我们将使用来自 sklearn 的糖尿病数据集。tqdm 库将用于为我们的循环生成进度条。
现在,我们将加载数据集并拟合一个随机森林回归器:
X, y = load_diabetes(return_X_y=True)
rf = RandomForestRegressor().fit(X, y)
现在,对于我们数据集中的每个特征,我们将计算在该特征固定值的情况下模型对数据集的平均预测:
features = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
features_averages = {}
for feature in tqdm(features):
features_averages[feature] = ([], [])
# For each unique value in the feature
for feature_val in np.unique(X[:, feature]):
features_averages[feature][0].append(feature_val)
# We remove the feature from the dataset
aux_X = np.delete(X, feature, axis=1)
# We add the feature value for every row of the dataset
aux_X = np.hstack((aux_X, np.array([feature_val for i in range(aux_X.shape[0])])[:, None]))
# We calculate the average prediction
features_averages[feature][1].append(np.mean(rf.predict(aux_X)))
现在,我们为每个特征绘制 PDP 图:
for feature in features_averages:
plt.figure(figsize=(5,5))
values = features_averages[feature][0]
predictions = features_averages[feature][1]
plt.plot(values, predictions)
plt.xlabel(f'Feature: {feature}')
plt.ylabel('Target')
例如,特征 3 的图示为:
结论
现在你有了另一个工具,可以用来提升你的工作效果,并帮助业务部门理解你展示的那个黑箱模型发生了什么。
但不要让理论消失。拿一个你正在开发的模型,将 PDP 可视化应用于它。了解模型在做什么,并在假设上更为精准。
另外,这不是唯一的解释性方法。实际上,我们还有其他方法在处理相关特征时效果更佳。请关注我的下一篇文章,其中会涵盖这些方法。
参考文献
ethen8181.github.io/machine-learning/model_selection/partial_dependence/partial_dependence.html