docs-merge-00
TowardsDataScience 2024 中文翻译(一)
TDS24
来源:{url}
一年的持续 Kaggle 实践:它教会了我什么?
竞赛比其他组件更有价值
·发布于 Towards Data Science ·阅读时长 5 分钟·2024 年 5 月 11 日
--
Kaggle是一个让用户在实践数据科学和机器学习方面获得动手经验的平台。它有四个不同的进阶组件,分别是竞赛、数据集、笔记本和讨论。使用这个平台并开始学习不需要数据科学方面的先前经验。
我的背景:我在 2023 年初在我的本科学位(数学与计算机科学)课程中完成了第一个 Kaggle 项目,这个项目是机器学习课程的一部分。从那时起,我就迷上了这个平台,将其作为最喜欢的消遣。到目前为止,我参加了 20 场竞赛。开始使用 Kaggle 之前,我没有作为数据科学家的工作或实习经验。
在一年时间里,我在 Kaggle 旅程中取得了(我认为)显著的进展,包括赢得了 2 枚金奖竞赛奖牌,其中一项我获得了第一名,并且在竞赛类别中跻身前 116 名,同时几乎没有错过任何一天的活动。


现在,让我们深入探讨我在 Kaggle 旅程中至今得到的三大关键学习。
- 你的团队不能仅仅依赖公开的笔记本在竞赛中获得成功
标准的 Kaggle 竞赛只会颁发金奖给前 10 名+ floor(NumTeams / 500)支队伍!例如,在一个有 2500 支队伍的竞赛中,只有 15 支队伍能够获得金奖。要想晋升到竞赛中的Master等级,这是强制性要求,你需要五枚金奖(其中至少一枚为个人赛金奖)才能晋升到Grandmaster等级。
你的团队不太可能通过简单地修改公共工作(如集成公共笔记本)而获得金奖名次。你的团队将与顶尖的数据科学家和大师们竞争,他们有很多创造性的想法来解决问题。
简单修改公共工作是即使是机器学习初学者也能做到的事情,并且使用这种方法不太可能让你的团队的解决方案脱颖而出。很可能,稍微改进一个公共笔记本会获得铜奖,或者如果运气好,获得银奖。
在我们团队赢得金奖的两场比赛中:
-
1/2048(冠军)PII 检测:我们使用了多种 Deberta 架构和后处理策略,其中大多数没有在公共论坛中分享。我们的最终集成模型中没有使用公共模型。
-
14/4436 Optiver — 临近收盘交易:我们使用了在线训练,以确保模型在做出预测之前已经适配了最新的数据。编写在私有排行榜上有效的在线训练管道并不容易,据我所知,这个想法没有在公共论坛中分享。我们没有使用流行的公共训练方法,因为我们认为它会对训练数据过拟合,尽管它的公共排行榜成绩非常好。
相比之下,这是我们团队获得铜奖的一个比赛:
总结:在我看来,花更多时间分析基准,并进行研究以思考如何改进是更好的做法。可能从一个小模型(例如 deberta-v3-xsmall)开始,以快速评估想法是一个不错的选择。目标是从一开始就建立一个稳健的交叉验证策略。
2. 相比数据集/笔记本/讨论,你从比赛类别中学到的更多。
我所学到的一些真实世界技能。
-
我是大部分比赛的队长,其中包括我们团队赢得金奖的两场比赛。这大大提升了我的沟通和领导能力。
-
与来自不同国家和时区的其他数据科学家/工程师合作,并向他们学习最佳实践。
-
使用Wandb跟踪和记录实验。
-
定制化变换器模型的架构
-
使用 LLM 生成特定用例的合成数据集。
-
从数据科学角度建模真实世界的用例。
-
编写简洁、易于理解的代码。
-
如何利用多 GPU 训练
-
更好的时间管理
-
评估并减轻模型错误
相比之下,在数据集/笔记本/讨论中进展要容易得多,而无需深入学习数据科学。在讨论中,用户可以通过在 Kaggle 论坛发布自己的成就来获得金奖讨论奖牌。我怀疑如果没有参与竞赛,我大多数技能是无法学到的。在我看来,在数据集/笔记本/讨论中的进展不一定说明一个人对数据科学充满热情。
3. 游乐场竞赛是初学者入门的好方式
游乐场系列模拟了特色竞赛,只不过它更加适合初学者,并且不颁发奖牌/奖品。在游乐场中,你需要在表格数据集上做预测,这让你能够学习如何编写机器学习管道的基础。游乐场中有很多共享的笔记本,既有表格数据方法也有神经网络(NN)方法,所以如果你卡住了,那些公共笔记本是一个很好的参考。
每个游乐场系列的竞赛大约持续 1 个月。
根据我的经验,游乐场竞赛教会了我:
-
如何建立一个稳健的交叉验证策略,避免在公共排行榜上过拟合
-
如何选择提交内容进行评估
-
如何进行特征工程和特征选择
-
如何设计 Jupyter 笔记本的样式
-
(更多关于数据工程方面的内容)如何使用Polars。这是一种比 Pandas 更快的 DataFrame 库,更适用于大数据场景
总结来说,我觉得做 Kaggle 最有收获的部分是竞赛中的实战经验,以及与全球数据专业人士合作的机会。我得以解决各种问题,从表格数据到更高级的自然语言处理任务。期待在数据科学领域继续提高自己!
100 年的(可解释)人工智能
反思在不断发展的 LLM 和人工智能治理时代中,深度学习与可解释性方面的进展与挑战
·发表于Towards Data Science ·20 分钟阅读·2024 年 12 月 18 日
--

图片由作者提供
背景
想象一下你正在驾驶一辆自动驾驶汽车,完全依赖它的车载计算机做出瞬间的决策。它能够检测物体、识别行人,甚至能够预测其他车辆的行为。但是,有一个问题:你知道它有效,但你完全不知道如何实现的。如果发生了意外情况,你无法清楚地了解结果背后的推理过程。这就是可解释人工智能(XAI)发挥作用的地方。深度学习模型,常常被视为“黑盒”,如今在各个领域被广泛应用于自动化预测和决策。可解释性就是要打开那个黑盒。我们可以把它看作是一个工具包,它不仅帮助我们理解这些模型做了什么,还帮助我们理解为什么它们做出这些决策,从而确保这些系统按预期工作。
XAI 领域近年来取得了显著进展,提供了关于模型内部工作机制的深入见解。随着人工智能成为关键领域的核心,处理责任问题变得至关重要,这对于保持系统的可靠性和信任至关重要 [Göllner & a Tropmann-Frick, 2023, Baker&Xiang, 2023]。这对于高风险应用尤为重要,如汽车、航空航天和医疗保健等领域,在这些领域中,理解模型决策能够确保系统的稳健性、可靠性以及安全的实时操作 [Sutthithatip et al., 2022, Borys et al., 2023, Bello et al., 2024]。无论是解释为何一份医疗扫描在特定患者中被标记为值得关注,还是识别风力发电风险评估中鸟类检测模型误分类的因素,XAI 方法都能让我们窥见模型的推理过程。
我们经常听到关于与模型和透明度等级相关的“盒子”及其种类,但拥有一个可解释的 AI 系统到底意味着什么呢?这如何应用于深度学习?而且,这不仅仅是满足我们的好奇心。本文将讨论 XAI 以及为什么它在 2024 年及以后会成为一个热点话题。我们将探讨可解释性如何在过去几十年中发展,并如何重塑计算机视觉的格局,反之亦然(第一部分)。我们将讨论从早期研究开始,XAI 是如何发展成为一项涵盖整个行业的实用技术,以及在全球监管框架和负责任的 AI 实践背景下,它的未来可能会如何(第二部分)。在这里,我们还将关注以人为中心的可解释性方法,我们将回顾利益相关者群体、他们的需求以及可能的解决方案,以应对在促进信任和确保安全 AI 部署过程中持续存在的挑战(第 3.1 节)。此外,您将了解常用的 XAI 方法,并检查评估这些解释效果的指标(第 3.2 节)。最后一部分(第四部分)将展示如何将可解释性有效应用于图像分类,以增强对模型决策的理解并验证其正确性。
1. 回归根本
在过去的一个世纪里,深度学习和计算机视觉领域经历了重大的突破,这些突破不仅塑造了现代人工智能,还推动了解释性方法和框架的发展与完善。让我们花一点时间回顾深度学习历史中的关键里程碑,这些里程碑促成了我们今天的成就,并展示了它们对视觉领域 XAI(可解释人工智能)发展的影响(涵盖:1920 年代 — 现在):
-
1924 年: 德国数学家弗朗茨·布赖西格(Franz Breisig)将电子学中四端子网络的明确使用视为“黑箱”——这个概念用于指代仅可见端口的系统,其内部机制被隐藏。
-
1943 年: 沃伦·麦卡洛克(Warren McCulloch)和沃尔特·皮茨(Walter Pitts)在其开创性工作《神经活动中固有思想的逻辑演算》中提出了麦卡洛克-皮茨(MCP)神经元,这是第一个人工神经元的数学模型,为神经网络的基础奠定了基础。
-
1949 年: 唐纳德·O·赫布(Donald O. Hebb)提出了赫布学习的神经心理学概念,解释了突触可塑性的基本机制,认为(大脑中的)神经连接在使用中会变得更强(同时激活的神经元会“联合”),因此能够通过学习重新塑造。
-
1950 年: 艾伦·图灵(Alan Turing)出版了《计算机与智能》,提出了他开创性的想法——图灵测试,用于判断机器是否能够“思考”。
-
1958 年: 美国心理学家弗兰克·罗森布拉特在其著作《感知机:大脑中信息存储和组织的概率模型》中提出了感知机,这是第一个人工神经网络。

图 1. 罗森布拉特感知机示意图(来源: Rosenblatt, 1958)
-
1962 年: 弗兰克·罗森布拉特(Frank Rosenblatt)提出了反向传播误差修正,这一计算机学习的基本概念,启发了后续的深度学习研究。
-
1963 年: 阿根廷-加拿大哲学家兼物理学家马里奥·布恩格(Mario Bunge)出版了《一般黑箱理论》,为黑箱理论的发展做出了贡献,并将其定义为一种抽象,表示“一个具体系统的集合,其中刺激 S 作用于系统并产生反应 R 的输出”。
-
1967 年: 日本工程师兼神经科学家天野俊一(Shunichi Amari)率先提出了第一个使用随机梯度下降训练的多层感知机,用于分类非线性可分模式。
-
1969 年: 日本计算机科学家福岛邦彦(Kunihiko Fukushima)提出了修正线性单元(ReLU),它后来成为深度学习中最广泛采用的激活函数。
-
1970 年: 芬兰数学家兼计算机科学家塞坡·林内马(Seppo Linnainmaa)在他的硕士论文中提出了“反向自动微分模式”,这是反向传播的现代变体。
-
1980 年: 福岛邦彦提出了 Neocognitron,这是早期的深度学习架构之一,适用于卷积神经网络(CNN),但并不使用反向传播进行训练。
-
1989 年: 法美籍计算机科学家 Yann LeCun 提出了 LeNet,这是第一个成功应用反向传播进行手写 ZIP 代码识别的卷积神经网络(CNN)架构。
-
1995 年: Morch 等人提出了显著性图,这为揭示深度神经网络内部工作原理提供了最早的可解释性方法之一。
-
2000 年代: 进一步的进展包括 CUDA 的开发,使得 GPU 并行处理能够支持高性能科学计算,此外还有 ImageNet,这个大规模的手动策划的视觉数据集,推动了基础和应用人工智能研究的进展。
-
2010 年代: 计算机视觉领域持续突破,如 Krizhevsky、Sutskever 和 Hinton 提出的用于 ImageNet 分类的深度卷积网络,推动了人工智能在各行各业的广泛应用。可解释人工智能(XAI)领域蓬勃发展,出现了 CNN 显著性图、LIME、Grad-CAM 和 SHAP 等技术*。

图 2: ImageNet 分类的视觉模型 SOTA 基准,2014–2024 年(来源:PapersWithCode)
-
2020 年代: 人工智能热潮随着 2017 年论文《Attention Is All You Need》的发布而加速,该论文提出了一种名为 Transformer 的编码器-解码器架构,推动了更先进的基于 Transformer 的架构的发展。在 Allen AI 的 ELMo、谷歌的 BERT 和 OpenAI 的 GPT 等早期成功的基础上,Transformer 被应用于多个领域和模式,包括视觉,加速了多模态研究的进展。2021 年,OpenAI 推出了 CLIP,一个能够从自然语言监督中学习视觉概念的模型,为生成型人工智能创新铺平道路,包括 DALL-E、Vision Transformer(ViT)、Stable Diffusion 和 Sora,提升了图像和视频生成能力。
-
2024 年: 欧盟人工智能法案生效,建立了针对欧洲人工智能系统的法律要求,包括透明度、可靠性和公平性的规定。例如,第 27 条将人工智能系统的透明度定义为:“以一种允许适当追溯性和可解释性的方式开发和使用 […] 有助于设计一致、可信和以人为本的人工智能”。
正如我们所见,早期的工作主要集中在基础方法和算法上,随着后期的进展,逐渐聚焦于特定领域,包括计算机视觉。到 20 世纪末,关键概念开始出现,为未来的突破奠定了基础,例如 1980 年代的反向传播训练卷积神经网络(CNNs)。随着时间的推移,可解释人工智能(XAI)领域迅速发展,增强了我们对预测背后推理的理解,并通过增加的研究和行业应用促进了更为明智的决策。随着(X)AI 的兴起,焦点转向了平衡系统效率与可解释性,在大规模模型理解中起到辅助作用,并在整个机器学习生命周期中整合 XAI 解决方案[Bhatt et al., 2019,Decker et al., 2023]。实际上,直到过去二十年,这些技术才足够成熟,得以广泛应用。最近,立法措施和监管框架,如欧盟人工智能法案(2024 年 8 月)和中国 TC260 的人工智能安全治理框架(2024 年 9 月),相继出台, 标志着人工智能开发和部署的更严格监管的开始,包括强制执行“从部署者那里获得对 AI 系统在决策过程中角色的清晰且有意义的解释,以及所作决策的主要要素”(2026 年第 86 条)。在这一点上,XAI 可以发挥其最佳作用。然而,尽管经过多年的严格研究和对可解释性的日益重视,这一话题似乎已经逐渐淡出人们的视野。真的是这样吗?现在,让我们从全局角度重新审视这一切。
2. 人工智能的兴奋与现在
今天是处于科技世界的一个激动人心的时刻。在 1990 年代,Gartner 提出了一个叫做炒作周期的方法论,用来描述新兴技术如何随时间演变——从最初的兴趣激发到社会应用。根据这一方法论,技术通常从创新突破(即“技术触发”)开始,随后迎来激动人心的快速上升,最终达到“过高期望的顶峰”。然而,当技术未能按预期交付时,它会跌入“失望的谷底”,此时热情消退,人们感到沮丧。这个过程可以描述为一条陡峭的上升曲线,最终会下行至低点,然后平稳上升,进入所谓的“生产力平台”。后者意味着,随着时间的推移,某项技术可以变得真正具有生产力,尽管围绕它的炒作已不再那么强烈。

图 3. 2024 年人工智能的炒作周期(来源:Gartner)
看看以前那些被认为能够解决一切的技术——智能代理、云计算、区块链、大脑-计算机接口、大数据,甚至深度学习。它们都在科技界取得了辉煌的地位,但当然,它们都没有成为万能的解决方案。现在,关于可解释性的话题也有类似的情况。我们可以一次又一次地看到历史的重复。如 2024 年 Gartner 人工智能热潮周期所强调的(图 3),负责任的人工智能(RAI)正逐渐获得更多关注(左上角),预计将在未来五年内达到成熟。可解释性为 RAI 实践提供了基础,促进了人工智能系统中的透明度、问责制和公平性。
下图概述了 XAI(可解释人工智能)研究的趋势和应用,来源于 2018 年至 2022 年间发表的科学文献,涵盖了 XAI 领域的各个概念,包括“可解释人工智能”、“可解释性人工智能”和“负责任的人工智能”[Clement et al., 2023]。图 4a 根据元综述结果概述了 XAI 的关键研究领域。最大的一部分(44%)集中在设计可解释性方法,其次是 15%专注于 XAI 在特定应用场景中的应用。依赖领域的研究(如金融)占 12%,而其他小领域——需求分析、数据类型和人机交互——各自占约 5-6%。

图 4.XAI 研究视角(a)和应用领域(b)(来源: Clement et al., 2023)
旁边是常见的应用领域(图 4b),其中医疗保健居领先地位(23%),由建立信任和支持决策的需求推动。工业 4.0(6%)和安全(4%)紧随其后,可解释性被应用于工业优化和欺诈检测。其他领域包括自然科学、法学、机器人技术、自动驾驶、教育和社会科学 [Atakishiyev et al., 2024, Chen et al., 2023, Loh et al., 2022]。随着 XAI 向可持续状态发展,研究和开发越来越集中于解决公平性、透明性和问责制问题 [Arrieta et al., 2020, Responsible AI Institute Standards, Stanford AI Index Report]。这些维度对于确保公平结果、澄清决策过程并为这些决策建立责任至关重要,从而促进用户信任,并与监管框架和行业标准保持一致。反映过去技术进步的轨迹,XAI 的崛起突显了在构建 AI 驱动解决方案方面的挑战和机遇,确立了它作为负责任 AI 实践中一个重要元素的地位,增强了 AI 在现实应用中的长期相关性。
3. 将焦点重新放回到 XAI(可解释的人工智能)
3.1. 为什么以及何时需要模型理解——可解释性基础
这是对人工智能系统的一个普遍看法:你输入数据,然后系统就像一个黑箱一样处理它,产生输出,但我们无法检查系统的内部工作原理。但真的是这样吗?随着人工智能的不断发展,开发可靠、可扩展且透明的系统变得越来越重要。简而言之:可解释人工智能的理念可以描述为采取某些方法来提供更清晰的理解,说明输入和输出之间发生了什么。从广义上讲,可以将其视为一组方法,允许我们构建能够提供理想结果的系统。在实践中,模型理解可以定义为生成用户能够理解的模型行为解释的能力。这种理解在各行各业的各种应用场景中都至关重要,包括:
-
模型调试和质量保证(例如,制造业、机器人技术);
-
确保系统对终端用户的可信度(医学、金融);
-
通过识别模型可能失败的场景来提高系统性能(银行欺诈检测、电子商务);
-
增强系统对抗敌对行为的鲁棒性(网络安全、自动驾驶汽车);
-
解释决策过程(如信用评分中的金融、司法决策中的法律);
-
检测数据标注错误及其他问题(零售中的客户行为分析、医疗健康中的医学影像)。
人工智能的广泛采用使其在各个领域和风险应用中得到了广泛使用。归根结底,人类理解与模型理解并不相同。虽然人工智能模型处理信息的方式对人类来说并不直观,但可解释人工智能的主要目标之一是创建能够有效传达推理过程的系统——换句话说,能够以对最终用户可理解且有意义的方式“发声”。因此,问题就变成了:我们如何弥合模型“知道”的内容与人类如何理解其输出之间的差距?
3.2. 它是为谁服务的——可解释人工智能的利益相关者期望
可解释人工智能不仅仅是对模型的解释,还涉及到通过知识转移让机器有效地支持人类。为了应对这些方面,可以思考如何将可解释性与涉及人工智能生态系统的各种角色和利益相关者的期望联系起来。这些群体通常包括用户、开发者、部署者、受影响方和监管者[Leluschko&Tholen,2023, Sadek 等人,2024]。因此,他们的期望——即他们对人工智能的功能和结果的预期——也会有所不同,这表明可解释性需要满足广泛的需求和挑战。在这项研究中,Langer 等人,2021强调了理解在解决认识论方面的关键作用,指的是利益相关者评估系统是否满足他们期望的能力,比如公平性或透明度。例如,在医学诊断等高风险场景中,可能需要更深层次的解释以建立信任[Saraswat 等人,2022]。图 5 展示了一个概念模型,概述了从可解释性方法到满足利益相关者需求的路径,而这些需求又反过来影响它们是否得到满足。那么,什么构成一个“好的”解释呢?该研究认为,它不仅应该准确、具有代表性,并且与系统及其功能的上下文相关,还应该符合社会伦理和法律考虑,这些因素可能在为某些期望提供正当理由时起决定性作用。

图 5:可解释性与利益相关者期望的关系(来源: Langer 等人,2021)
在这里,我们可以说,XAI(可解释人工智能)作为技术的成功,依赖于它如何通过解释性信息有效地促进人类理解,强调了在利益相关者之间仔细平衡权衡的重要性。例如,对于领域专家和用户(如医生、法官),他们需要解读和审核 AI 系统输出结果以进行决策,因此确保可解释性结果简洁且具有领域针对性非常重要,以便与专家直觉对接,同时避免信息过载,尤其在涉及人类干预的应用中更为重要。在这种情况下,挑战可能出现在输入和输出之间的不确定性和缺乏清晰因果关系上,这可以通过针对特定用例的局部事后解释来解决[Metta 等人,2024]。受影响方(例如,求职者、患者)是受 AI 决策影响的个体,公平性和伦理性是关键问题,特别是在招聘或医疗等领域。在这种情况下,可解释性方法可以帮助识别在决策过程中导致偏见的因素,从而进行缓解或至少承认并消除偏见[Dimanov 等人,2020]。同样,监管机构可能希望确定一个系统是否对某一群体存在偏见,以确保其符合伦理和监管标准,特别关注透明度、可追溯性和高风险应用中的非歧视性[Gasser & Almeida, 2017,Floridi 等人,2018,欧盟 AI 法案 2024]。

图 6. 机器学习生命周期中的可解释性 (来源: Decker 等人,2023)
对于采用人工智能的企业和组织来说,挑战可能在于确保在符合监管和行业标准的前提下负责任地实施 AI,同时保持用户的信任[Ali et al., 2023, Saeed & Omlin, 2021]。在这种情况下,使用全球性解释并将 XAI 融入机器学习生命周期(图 6)中,可能特别有效[Saeed & Omlin, 2021, Microsoft Responsible AI Standard v2 General Requirements, Google Responsible AI Principles]。总体而言,监管机构和部署者都旨在了解整个系统,以尽量减少不合理的极端情况。在从业者(如开发者和研究人员)方面,他们可能对利用 XAI 工具诊断和改善模型性能感兴趣,同时推进现有解决方案,通过可解释性界面提供关于模型推理的详细信息[Bhatt et al., 2020]。然而,这些工具可能伴随高昂的计算成本,使大规模部署变得具有挑战性。在这种情况下,XAI 开发堆栈可以包括开源和专有工具包、框架和库,例如PyTorch Captum、Google Model Card Toolkit、Microsoft Responsible AI Toolbox、IBM AI Fairness 360,以确保从开发到部署以及后续阶段,构建的系统是安全、可靠和值得信赖的。
正如我们所看到的——“一刀切”并不适用。持续的挑战之一是提供既准确又有意义的解释,满足不同利益相关者的需求,同时在实际应用中平衡透明度和可用性[Islam et al., 2022, Tate et al., 2023, Hutsen, 2023]。现在,让我们以更实际的角度来讨论 XAI。
4. 视觉模型的可解释性
4.1. 特征归因方法
随着人工智能系统的进步,现代方法在复杂任务中的表现有了显著改善,例如图像分类(图 2),超越了早期依赖手工算法进行视觉特征提取和检测的图像处理技术[Sobel and Feldman, 1973, Canny, 1987]。虽然现代深度学习架构本身并不具备可解释性,但已经提出了多种解决方案,用于为给定输入提供模型行为的解释,从而弥合人类(理解)与机器(过程)之间的差距。继深度学习突破之后,各种 XAI(可解释人工智能)方法应运而生,旨在增强计算机视觉领域的可解释性方面。下面的图 7 描述了过去几十年中开发的几种常用的 XAI 方法,专注于图像分类和目标检测应用:

图 7. 计算机视觉的可解释性方法(作者提供的图像)
XAI 方法可以根据其方法论大致分为基于反向传播的方法和基于扰动的方法,而解释范围则可以是局部的或全局的。在计算机视觉中,这些方法或它们的组合用于揭示模型预测背后的决策标准。基于反向传播的方法将信号从输出传播到输入,为在前向传播过程中计算的每个中间值分配权重。然后,梯度函数更新模型中的每个参数,以使输出与真实值对齐,这些技术也被称为基于梯度的方法。示例包括显著性图[Simonyan et al., 2013]、集成梯度[Sundararajan et al., 2017]、Grad-CAM[Selvaraju et al., 2017]。相比之下,基于扰动的方法通过遮挡[Zeiler & Fergus, 2014]、LIME[Ribeiro et al., 2016]、RISE[Petsiuk et al., 2018]等技术修改输入,评估这些轻微变化如何影响网络输出。与基于反向传播的方法不同,扰动技术不需要梯度,因为单次前向传播就足以评估输入变化如何影响输出。
对于“黑箱”架构,通常通过模型训练后采用外部事后方法实现可解释性(例如,CNN 的梯度)。相对而言,“白箱”架构本身具有可解释性,在这种架构中,可解释性可以作为模型训练的副产品来实现。例如,在线性回归中,通过求解线性方程组得到的系数可以直接用于为输入特征分配权重。然而,尽管在处理线性回归时,特征重要性较为直观,但更复杂的任务和高级架构会考虑输入与输出之间高度非线性的关系,因此需要外部的可解释性方法来理解和验证哪些特征对预测有最大的影响。也就是说,使用线性回归来进行计算机视觉任务并不是一个可行的方案。
4.2. XAI 的评估指标
评估解释对于确保从模型中提取的见解及其通过可解释性界面展示给最终用户的方式是有意义的、有用的且值得信赖的至关重要 [Ali et al., 2023]。XAI 方法日益多样化,迫使我们进行系统化的评估和比较,摆脱主观的“眼见为实”的方法. 为了解决这一挑战,研究人员设计了多种算法和基于用户的评估技术,并提出了框架和分类法,用以捕捉解释的主观与客观、定量与定性属性 [Doshi-Velez & Kim, 2017, Sokol & Flach, 2020]。可解释性是一个谱系,而非二元特征,其有效性可以通过评估某些属性的实现程度来量化。对 XAI 评估方法进行分类的一种方式是基于所谓的 Co-12 属性 [Nauta et al., 2023],这些属性按内容、展示和用户维度分组,概括如表 1 所示。

表 1. 用于评估的 Co-12 解释质量属性(来源: Nauta et al., 2023)
在更细粒度的层面上,XAI(可解释人工智能)的定量评估方法可以纳入一些指标,如忠实度、稳定性、保真度和明确性[Alvarez-Melis & Jaakkola, 2018、Agarwal et al., 2022、Kadir et al., 2023],从而能够衡量解释的内在质量。例如,忠实度衡量解释与模型行为的一致性,侧重于为目标类别预测选择的特征的重要性。Qi et al., 2020展示了一种通过集成梯度进行特征重要性分析的方法,强调生成忠实的模型行为表示的重要性。稳定性指的是在类似输入之间解释的一致性。Ribeiro et al., 2016对 LIME 的研究强调了稳定性在生成可靠解释中的重要性,稳定的解释不会因为细微的输入变化而发生剧烈波动。保真度反映了解释与模型决策过程的准确匹配程度。Doshi-Velez & Kim, 2017在其可解释机器学习框架中强调了保真度,认为高保真度对可信赖的 AI 系统至关重要。明确性涉及人类理解解释的难易程度。Alvarez-Melis & Jaakkola, 2018通过自解释神经网络(SENN)讨论了解释性中的鲁棒性,SENN 在追求明确性的同时,也注重稳定性和忠实度。
为了联系这些概念,如表 1 所述,正确性属性是指解释与被解释模型之间的忠实度,表示解释反映“真实”黑盒行为的真实性。这一属性不同于模型的预测准确性,而是对 XAI 方法在模型功能方面的描述性[Naute et al., 2023, Sokol & Vogt, 2024]。理想情况下,解释应该是“完全真实的”,因此需要较高的正确性。通过删除得出的忠实度得分可以通过计算曲线下的标准化面积来获得,该曲线表示两种特征重要性函数之间的差异:一种是通过逐渐移除特征(从最不相关的特征开始 — LeRF)并在每一步评估模型性能,另一种则是删除顺序随机(随机顺序 — RaO)[Won et al., 2023]。计算两种曲线的点数是从将完整图像输入模型开始,接着逐渐移除像素,这些像素的特征重要性低于某个阈值,且由归因方法分配。得分越高,意味着模型在删除冗余特征时,仍能更好地保留重要信息(公式 1)。

公式 1. 通过删除评估特征重要性的忠实度度量计算
评估忠实度的另一种方法是通过插入计算特征重要性,类似于上述方法,但通过逐步展示模型由归因方法识别出的最相关的图像区域。这里的关键思想是:包含重要特征并观察结果。在演示中,我们将探讨评估模型解释的定性和定量方法。
5. 精细分类的特征重要性
在细粒度分类任务中,如区分不同类型的车辆或识别鸟类物种,视觉外观的微小变化可能会显著影响模型的预测结果。确定哪些特征对模型决策过程最为重要,有助于揭示误分类问题,从而能够优化模型的任务表现。为了展示如何有效地应用可解释性方法来加深对深度学习视觉模型的理解,我们将考虑鸟类分类的应用案例。鸟类种群是重要的生物多样性指标,因此收集物种及其在不同环境中的相互作用的可靠数据对生态学家来说至关重要[Atanbori et al., 2016]。此外,自动化鸟类监测系统也能为风电场生产商带来好处,因为建设过程中需要在设计阶段进行初步的碰撞风险评估和缓解[Croll et al., 2022]。本部分将展示如何应用 XAI 方法和指标来增强鸟类物种分类模型的可解释性(更多相关内容可以参考文章和教程)。
下图展示了使用在 ImageNet 上预训练并在 Caltech-UCSD Birds-200–2011 数据集上微调的 ResNet-50 进行细粒度图像分类的特征重要性分析结果。对 Guided Grad-CAM 方法进行了定性评估,以评估给定模型的选定特征的重要性。定量 XAI 指标包括通过删除(FTHN)评估的忠实度,值越高表示忠实度越好,同时还有反映非稳健性和不稳定性程度的指标,如最大敏感性(SENS)和不忠实度(INFD),其中较低的值为佳。后者指标基于扰动,依赖于假设解释应随着输入数据或模型本身的微小变化保持一致的前提[Yeh et al., 2019]。

图 8:评估细粒度图像分类的可解释性指标(图像由作者提供)
当在北方红雀的独立测试图像上评估我们的模型时,我们注意到在初始迭代过程中模型得分的轻微变化之后,随着最关键特征的逐步加入,最终迭代的得分急剧增加(图 8)。这些结果表明,对于评估的 XAI 方法,模型的忠实性有两个关键解释。首先,使用引导型 Grad-CAM 的基于归因的可解释性忠实于模型,因为添加被识别为冗余的区域(90%的 LeRF,x 轴)对模型得分的影响很小(预测概率得分低于 0.1)。这意味着模型在做出预测时并没有依赖这些区域,而是依赖于剩余的前 10%最相关的特征。另一类——鲁棒性——指的是模型对小的输入变化的抗干扰能力。在这里,我们可以看到,原始图像约 90%的变化对模型整体性能几乎没有影响,尽管大部分像素发生变化,模型仍能保持目标概率得分,这表明模型在目标类别预测上的稳定性和泛化能力。
为了进一步评估我们模型的鲁棒性,我们计算了额外的指标,如敏感性和失真[Yeh et al., 2019]。结果表明,尽管模型对输入中的轻微扰动不太敏感(SENS=0.21),但对最重要区域的改动可能会对模型决策产生影响,尤其是对于前 10%的区域(图 8)。为了更深入地评估模型解释的敏感性,我们可以进一步扩展可解释性方法列表,例如使用集成梯度和 SHAP[Lundberg & Lee, 2017]。此外,为了评估模型对对抗性攻击的抵抗力,下一步可能包括量化更多的鲁棒性指标[Goodfellow et al., 2015, Dong et al., 2023]。
结论
本文提供了对过去几十年发表的科学文献的全面概述,涵盖了深度学习和计算机视觉领域的关键里程碑,这些里程碑为 XAI(可解释人工智能)领域的研究奠定了基础。回顾这一领域最近的技术进展和前景,我们讨论了在新兴的 AI 监管框架和负责任的 AI 实践背景下,XAI 可能带来的影响,并预见到可解释性在未来将变得更加重要。此外,我们还考察了 XAI 的应用领域,探索了各利益相关方的需求,并提出了如何应对当前挑战、满足需求的实践建议,以创造可靠和值得信赖的 AI 系统。我们还涵盖了与可解释性相关的基本概念和分类法,常用的视觉方法和技术,以及用于评估事后解释的定性和定量指标。最后,为了展示如何利用可解释性加深对深度学习模型的理解,最后一节呈现了一个案例,展示了 XAI 方法和指标如何有效应用于精细分类任务,以识别影响模型决策的相关特征,并对结果进行定性和定量评估,从而验证所得到的解释的质量,特别是相对于模型推理的合理性。
接下来是什么?
在接下来的文章中,我们将进一步探讨可解释性的主题及其实际应用,重点讨论如何在设计中利用 XAI 优化模型性能并减少分类错误。想继续了解吗?请关注更多资料,访问——github.com/slipnitskaya/computer-vision-birds 和 medium.com/@slipnitskaya。
12 个 RAG 痛点及解决方案
解决检索增强生成的核心挑战
· 发表在 Towards Data Science · 18 分钟阅读 · 2024 年 1 月 30 日
--

图片改编自 《工程化检索增强生成系统的七个失败点》
· 痛点 1:缺失内容
· 痛点 2:错过了排名靠前的文档
· 痛点 3:缺乏上下文 — 整合策略的局限性
· 痛点 4:未提取
· 痛点 5:格式错误
· 痛点 6:不正确的特异性
· 痛点 7:不完整
· 痛点 8:数据摄取的可扩展性
· 痛点 9:结构化数据 QA
· 痛点 10:从复杂的 PDF 中提取数据
· 痛点 11:回退模型
· 痛点 12:LLM 安全性
受 Barnett 等人论文 《工程化检索增强生成系统的七个失败点》 启发,本文将探讨该论文中提到的七个失败点以及在开发 RAG 管道过程中遇到的五个常见痛点。更重要的是,我们将深入分析这些 RAG 痛点的解决方案,以便在日常 RAG 开发中更好地应对这些问题。
一种新的方法来检测大型语言模型的“虚构”幻觉
通过使用第二个 LLM 计算语义熵,我们可以更好地标记那些由于缺乏知识而被认为不可靠的答案
LucianoSphere (Luciano Abriata, PhD)
·发布于Towards Data Science ·阅读时间 10 分钟·2024 年 6 月 25 日
--

图片由Jametlene Reskp提供,来自Unsplash
正如你所知道的,人工智能在过去两年中取得了巨大进展,特别是在大型语言模型(LLMs)的开发和大规模部署方面。这些模型似乎具有令人印象深刻的推理和问答能力;然而,一个持久的挑战是它们倾向于“幻觉”,即生成带有虚假或随意内容的输出。这些幻觉可能带来严重后果,因此当前大量 LLM 开发的研究都在致力于尽可能抑制它们。为此,一篇新论文提出了一种名为“语义熵”的方法,能够识别和减轻由知识缺乏引起的特定类型幻觉,即所谓的“虚构”。不用多说,这对在多个应用场景中更可靠地使用 LLM 非常有用,尤其是在那些需要事实知识的应用中。值得注意的是,量化语义熵需要应用第二个 LLM 来评估 LLM 生成的序列之间的相似性。继续阅读以了解更多细节和一些示例。
17 种(高级)RAG 技术,将你的 LLM 应用程序原型转变为可投入生产的解决方案
一系列 RAG 技术,帮助你将 RAG 应用程序开发成一个强大且持久的系统
·发布于Towards Data Science ·阅读时间:24 分钟·2024 年 6 月 26 日
--
现在人们成为生成式 AI 专家的速度令人惊叹。每个人都宣称,生成式 AI 将带来下一次工业革命。
这是一个大承诺,但我同意,我认为这次是真的。AI 将最终彻底改变我们的工作和生活方式。我无法想象滑入下一个AI 冬天。
大型语言模型(LLMs)和多模态模型简直太有用了,而且“相对”容易集成到现有的流程中。一个向量数据库,几行代码来处理原始数据,再加上几次 API 调用。
就是这样。— 至少在理论上是这样。
尽管听起来相当直接,但行业中的真正进展可能更好地通过Matt Turck 最近的一篇 LinkedIn 帖子来描述:
2023 年:“我希望生成式 AI 不会把我们全部杀掉!”
2024 年:“我希望生成式 AI 能在我公司从概念验证实验,发展到小规模生产部署,在接下来的 12 到 18 个月内能读取 PDF!”
- Matt Turck
构建一个原型很容易。将其转变为可投入生产的系统则非常困难。
你应该注意的两个隐秘的 PySpark 错误
在处理大型数据集时,小错误可能会导致严重后果。
·发布于 Towards Data Science ·阅读时间:5 分钟·2024 年 2 月 16 日
--

图片来源:Ernie A. Stephens 通过 Unsplash
在编程中,当我们犯错时,并不总是会报错。代码能够正常工作,不会抛出异常,我们以为一切都没问题。那些不会导致脚本失败的错误,很难被发现和调试。
在数据科学中,发现这些错误更具挑战性,因为我们通常不会得到一个单一的输出结果。
假设我们有一个包含数百万行的数据集。我们在计算销售数量时犯了一个错误。然后,我们基于销售数量创建了聚合特征,如每周总数、过去 14 天的移动平均等。这些特征被用于一个机器学习模型,该模型预测下一周的需求。
我们评估预测结果,发现准确率不够好。然后,我们花了很多时间尝试不同的方法来提高准确率,比如特征工程或超参数调优。这些策略对准确率的影响不大,因为问题出在数据上。
这是我们在处理大型数据集时可能遇到的一个场景。在本文中,我们将讨论两个可能导致意外结果的 PySpark 错误。
2023 年回顾:疾病与健康的一年
分析我自己的数据,以更好地理解我的健康模式
·发表于《数据科学前沿》 ·10 分钟阅读·2024 年 1 月 23 日
--

对我在 2023 年个人健康和疾病日子的概览,由作者创作。
这篇文章最初发表于 Art Fish Intelligence.
每年初,我都会对前一年的数据进行分析,以反思过去发生的一切。
去年,即 2022 年,我对自己的哭泣模式进行了深入调查。
使用我自己收集的数据调查我的哭泣模式
towardsdatascience.com
今年,即 2023 年,我分析了自己的疾病与健康模式。
在这篇文章中,我展示了我在 2023 年收集并分析的个人数据概览,并给出了一些关于如何在 2024 年过上更健康、更好生活的结论。
数据收集概述
我的数据来自以下几个来源:
-
Google Maps 位置历史
-
Apple Health
-
每天结束时我填写的关于日常习惯的调查问卷
在整合来自不同数据源的数据后,我得到了大量的…
2024 年亮点:引起轰动的人工智能与数据科学文章
·发布于 Towards Data Science ·作为 新闻简报 发送 ·6 分钟阅读·2024 年 12 月 19 日
--
想在 2024 年结束前写下你的第一篇 TDS 文章吗?我们始终欢迎新作者的投稿。
就这样,2024 年(几乎)已经接近尾声。这是一个充满激动人心的过渡之年——对TDS 团队而言是如此,对数据科学、机器学习和人工智能社区而言,也以许多重要的方式如此。我们想感谢大家——读者、作者以及追随者们,感谢你们的支持,感谢你们通过精彩的投稿和评论让我们保持忙碌并积极参与其中。
与 2023 年不同,去年一个事件(ChatGPT 的发布,距年初仅几周)让所有人停下脚步,几个月内都在主导讨论。今年,我们经历了一种更具累积性和碎片化的转变感。各行各业的从业者和学者们不断尝试新工具,努力寻找创新的方式,从 LLM 的快速崛起中获益;与此同时,他们还不得不应对充满挑战的就业市场,以及一个人工智能的足迹越来越接近他们日常工作流程的世界。

图片来自 Oskars Sylwan 在 Unsplash
为了帮助你理解这些发展,我们在过去的一年里发布了超过 3,500 篇文章,其中包括数百篇首次投稿的文章。我们的作者具有将独特视角融入任何他们所写话题的惊人能力——从大问题和时事话题到更聚焦的技术挑战——我们为 2024 年发布的每一篇文章感到自豪。
在这大量的创意输出中,有些文章特别能够引起读者的共鸣,我们将这一期的《Variable》专刊献给这些文章:我们最受阅读、讨论和分享的文章。如你所料,它们涉及的主题非常广泛,因此我们决定按照今年我们发现的主要主题来排列它们:从零开始学习和构建,RAG 和 AI 代理,职业成长,以及突破和创新。
我们希望你喜欢探索我们的 2024 年亮点,并祝你度过一个轻松的年终假期——2025 年 1 月见!
从零开始学习和构建
最受欢迎的 TDS 文章类型通常是那些教读者如何独立完成有趣且富有成效的事情,并且要求最低的前提条件。今年也不例外——我们 2024 年最受欢迎的三篇文章都属于这一类别。
-
你可以在这个周末构建的 5 个 AI 项目(使用 Python)从适合初学者到高级的项目创意,Shaw Talebi展示了任何人都可以亲手实践 AI。
-
从零开始使用初中数学理解 LLMsLLM 是如何工作的?Rohit Patel提供了你能找到的最易懂且富有吸引力的解释之一。
-
如何自学 AI(自学指南)对于那些自我驱动的学习者,Thu Vu为学习 AI 的基本构建块提供了一条精简的学习路线图。
-
神经网络背后的数学为了理解神经网络——“现代 AI 的支柱”,Cristian Leo带领我们深入探讨其底层的数学原理。
-
文本嵌入:全面指南嵌入使得 LLM 的魔力成为可能,Mariya Mansurova的详细介绍让我们清楚地了解了它们如何以及为什么变得如此重要。
-
我如何在两周内学习 LLM:一份全面的学习路线图另一份优秀的学习资源来自于Hesam Sheikh,他为我们讲解了一个密集但易于理解的课程,让我们掌握了大型语言模型的基础知识(以及更多)。
RAG 和 AI 代理
当 LLM 初步的兴奋感平息(稍微)之后,数据和机器学习专业人士意识到,这些强大的模型在开箱即用的情况下并不那么有用。检索增强生成(RAG)和代理 AI 在过去一年中成为了两种主流方法,它们架起了模型潜力和实际应用价值之间的桥梁;它们也成为了我们最近几个月覆盖最多的技术话题。
-
LangChain 与 LLM 代理入门:当 RAG 不足以满足需求时早在三月——并且远远走在前沿——Alex Honchar发布了关于使用代理的终极入门指南。
-
使用 LangChain ReAct 代理在 RAG 系统中回答多跳问题在这篇教程中,Dr. Varshita Sher向我们展示了代理和 RAG 如何相互补充,她解决了在内部文档中回答复杂查询的普遍需求。
-
17 种 (高级) RAG 技术,将你的 LLM 应用原型转变为生产就绪的解决方案构建一个基础的 RAG 流水线是一回事;将其优化到可以在商业环境中实际工作的程度又是另一回事。Dominik Polzer整理了一份全面的指南,介绍了你可以利用的各种方法来实现这一宏大目标。
-
12 个 RAG 痛点及其解决方案在类似的故障排除话题中,Wenqi Glantz概述了十二种简化的方法,帮助解决实践者在实施 RAG 时遇到的一些常见挑战。
-
选择 LLM 代理框架在一个每天都有主要和新兴玩家发布新工具的生态系统中,做出明智选择可能很困难。Aparna Dhinakaran提供了关于需要考虑的权衡的深刻见解,帮助我们做出决策。
职业成长
数据科学和机器学习的职业道路不断发展,适应这种变化的环境可能会给许多专业人士带来不小的压力,无论他们是已经深耕职业生涯,还是刚刚起步。我们喜欢发布关于这个话题的个人反思,尤其当这些反思还能为读者提供务实建议时——以下是我们(以及我们的读者)认为特别值得关注的四个反思。
-
在 Uber、Meta 和初创公司工作的 10 年让我学到了关于数据分析的那些事从讲故事的重要性和商业敏锐度到指标的局限性,Torsten Walbaum慷慨地将十年的工作经验总结为可操作的见解。
-
我在自由职业数据科学家岗位上的前三个月所学职业转型总是充满挑战,从公司工作的结构转向自雇世界也伴随着一系列挑战——正如CJ Sullivan所展示的那样,这也为学习和成长提供了巨大的机会。
-
我是如何成为数据科学家的——没有计算机科学学位,没有训练营对于任何刚踏入这个领域的人来说,Egor Howell关于自己进入数据科学的坦诚叙述是一本必读之作。
-
我花费了 96 万美元成为数据科学家。以下是所有初学者必须知道的 5 个关键课程从另一个角度讨论如何进入这个领域,Khouloud El Alami提供了关于如何管理你的数据科学教育的实用建议,帮助你走上正确的道路。
突破与创新
紧跟前沿研究和新工具有时会让人感到不知所措,这也是为什么我们对顶级论文解析以及新兴库和模型的入门教程特别钟情。这里有三篇文章特别引起了我们的读者的共鸣。
-
一种新的相关系数“如果有人告诉你,存在一种新方法可以衡量两个变量之间的关系,类似于相关系数,但可能更好,你会怎么想?”这就是Tim Sumner在其关于 2020 年一篇开创性论文的解释文章中的开头。
-
DSPy 简介:告别提示词,迎接编程!在又一个激动人心的开源工具年份里,DSPy 作为新晋工具之一脱颖而出,旨在为程序员打开大型语言模型(LLM)的应用,并简化构建模块化 AI 解决方案的过程。Leonie Monigatti的动手实践介绍是开始探索其潜力的完美起点。
-
Kolmogorov-Arnold 网络:神经网络的最新进展,简单明了地解释KANs,“作为多层感知器(MLP)的有前景的替代方案”,在去年春天引起了轰动;Theo Wolf通过这篇易懂的入门文章,阐明了它们对机器学习从业者的影响及潜在好处。
感谢您在 2024 年对我们作者工作的支持!如果写作是您 2025 年的目标之一,何不现在就开始呢?不要犹豫,与我们分享您的作品。
直到下一个 Variable,我们将在 1 月的第一周与您见面,
TDS 团队
2024 年回顾:我对 2024 年的预测正确与错误,以及对 2025 年更大胆的预测
我对 2024 年趋势的正确与错误的看法,以及对未来一年更大胆的预测
·发布于Towards Data Science ·8 分钟阅读·2024 年 12 月 17 日
--

AI 流行语和趋势宾果游戏(图片由作者提供)
在 2023 年,构建 AI 驱动的应用充满了希望,但挑战已经开始显现。到了 2024 年,我们开始尝试一些技术,来应对将这些应用投入生产中的艰难现实。
去年,我回顾了 2023 年 AI 领域的最大趋势并对 2024 年做出了预测。今年,我不打算列出时间线,而是专注于关键主题:出现了哪些趋势?我哪里预测错了?我们可以期待 2025 年会发生什么?
2024 年回顾
如果我必须总结 2024 年 AI 领域的情况,那就是“船长,今天是星期三”这个梗。今年的重大发布数量令人震惊。我不怪任何一个在这个领域感到疲惫的人,尤其是到了年底。这真是一次疯狂的旅程,而且很难跟得上。让我们回顾一下 AI 领域的关键主题,看看我去年是否正确预测了这些变化。
评估
让我们从一些已投入生产的生成型 AI 解决方案开始。数量并不多。正如 A16Z 的调查报告所显示,2024 年,许多公司仍然对在面向客户的应用中部署生成型 AI 感到犹豫。相反,它们更愿意将其用于内部任务,如文档搜索或聊天机器人。
那么,为什么目前市面上并没有那么多面向客户的生成式 AI 应用呢?可能是因为我们仍在摸索如何正确评估它们。这是我对 2024 年的预测之一。
很多研究涉及使用另一个 LLM 来评估 LLM 的输出(LLM-as-a-judge)。虽然这种方法可能很巧妙,但由于增加了成本、引入了偏见以及不可靠性,它也并不完美。
回顾过去,我曾预计今年会解决这个问题。然而,今天来看,尽管这一话题是讨论的重点,但我们依然没有找到一种可靠的方式来有效评估生成式 AI 解决方案。虽然我认为 LLM 作为评判工具是我们唯一能够大规模评估生成式 AI 解决方案的方法,但这也表明我们在这个领域仍处于早期阶段。
多模态性
尽管这对许多人来说可能是显而易见的,但我并没有把它纳入我 2024 年的预测中。随着GPT4、Llama 3.2和ColPali的发布,多模态基础模型成为 2024 年的一个重要趋势。尽管我们开发者忙于弄清楚如何在现有管道中使 LLM 发挥作用,研究人员却已经走在了前面,他们已经在构建能够处理多种模态的基础模型。
“绝对不可能在没有让机器从高带宽感官输入(如视觉)中学习的情况下,我们能达到人类水平的 AI。” — Yann LeCun
以 PDF 解析作为多模态模型在文本到图像任务之外的有用示例。ColPali的研究人员通过使用视觉语言模型(VLMs)避免了 OCR 和布局提取的复杂步骤。像 ColPali 和ColQwen2这样的系统将 PDF 作为图像处理,直接提取信息,无需预处理或分块。这提醒我们,简单的解决方案往往来源于改变问题的框架方式。
多模态模型的变革比看起来更为深远。PDF 文档的搜索仅仅是开始。基础模型中的多模态性将为各行各业的应用解锁全新的可能性。随着模态的增多,AI 不再仅仅局限于语言——它关乎于理解世界。
微调开放权重模型与量化
开源模型正在缩小与封闭模型之间的性能差距。对它们进行微调可以提升性能,同时保持轻量化。量化技术使这些模型更小、更高效(另见绿色 AI),能够在任何地方运行,甚至在小型设备上。量化与微调配合得很好,尤其是考虑到微调语言模型本身就具有挑战性(另见QLoRA)。
综合来看,这些趋势表明,未来不仅仅是更大的模型——而是更智能的模型。
由Maxime Labonne做出的精彩视觉总结。如果你对微调 LLM 感兴趣,也可以查看他的博客。
我觉得我并没有明确提到过这一点,而只是在 2024 年第二季度写了一篇关于此的文章。所以,我在这里不会给自己加分。
AI 代理
今年,AI 代理和代理工作流受到了广泛关注,正如 Andrew Ng 年初所预测的那样。我们看到Langchain和LlamaIndex开始整合代理,CrewAI获得了很大动力,OpenAI 推出了Swarm。这是另一个我没有预料到的主题,因为我之前没有深入研究过。
“我认为 AI 代理工作流将在今年推动大规模的 AI 进展——可能甚至超过下一代基础模型。” — Andrew Ng

2024 年“AI 代理”这一术语在 Google Trends 上的截图。
尽管 AI 代理引起了极大的关注,但它们也可能具有争议。首先,关于“AI 代理”及其能力仍然没有明确的定义。AI 代理只是具备工具访问能力的 LLM,还是具备其他特定能力?其次,它们带来了额外的延迟和成本。我读到很多评论说,代理系统由于这些问题不适合用于生产系统。
但我认为我们已经开始在生产环境中看到一些具有轻量化工作流的代理管道,比如将用户查询路由到特定的功能调用。我认为我们将在 2025 年继续看到 AI 代理。希望我们能得到更清晰的定义和图景。
RAG 并未死亡,信息检索正在成为主流
检索增强生成(RAG)在 2023 年获得了显著关注,并在 2024 年依然是一个关键话题,许多新的变体应运而生。然而,这仍然是一个备受争议的话题。一些人认为,随着长上下文模型的出现,RAG 正在变得过时,而另一些人则质疑这是否真的是一个新思想。虽然我认为关于术语的批评是有道理的,但我认为这个概念会持续存在(至少还会有一段时间)。
所有不同的 RAG 变体
每次发布一个新的长上下文模型时,有些人就预测这将是 RAG 管道的终结。我不认为这会发生。这个讨论本应成为一篇单独的博客文章,所以我在这里不会深入探讨,讨论会留到下一篇。让我只是说,我认为它们并不是互相排斥的,它们是互补的。相反,我们可能会将长上下文模型与 RAG 管道一起使用。
此外,在应用程序中拥有数据库并不是一个新概念。‘RAG’这一术语,指的是从知识源中检索信息以增强 LLM 的输出,面临着批评。有些人认为这不过是对其他领域(如软件工程)中长期使用的技术的重新命名。虽然我认为我们可能会在长远来看抛弃这个术语,但这一技术将继续存在。
尽管有预测认为 RAG(检索增强生成)的时代将结束,但检索依然是 AI 流程的基石。虽然我可能会因为自己在检索领域的工作而有所偏见,但我感觉这个话题在今年变得更加主流。最初,许多讨论集中在将关键词搜索(BM25)作为 RAG 流程的基准。随后,这一讨论扩展到了更广泛的密集检索模型,如ColBERT或 ColPali。
知识图谱
我完全错过了这个话题,因为我对此不太熟悉。RAG 系统中的知识图谱(例如,图 RAG)是另一个大话题。由于我目前能说的关于知识图谱的内容只是它们似乎是强大的外部知识源,因此我会简短地处理这一部分。
2024 年的关键话题表明,我们现在已经意识到仅仅构建基础模型应用程序的局限性。围绕 ChatGPT 的热潮可能已经平息,但将基础模型集成到应用中的推动力依然强劲。只不过这比我们预期的要困难得多。
“在投资者失去热情之前,争夺让 AI 更加高效和有用的竞赛已经开始。” — 经济学人
2024 年告诉我们,仅仅扩大基础模型的规模是不够的。我们需要更好的评估、更智能的检索和更高效的工作流,以使 AI 变得有用。今年我们遇到的局限性并不是停滞的标志——它们是关于我们需要在 2025 年解决的问题的线索。
我的 2025 年预测
好的,现在进入有趣的部分,我的 2025 年预测是什么?今年,我想为来年做出一些更大胆的预测,让它更有趣:
-
视频将成为一种重要的模态: 在纯文本 LLM 发展为多模态基础模型(主要是文本和图像)之后,视频自然会成为下一个模态。我可以想象更多支持视频的基础模型将会追随Sora的步伐。
-
从单次到代理式,再到人机协作: 我想象我们将开始将人类融入 AI 驱动的系统中。虽然我们从单次系统开始,但我们尚未达到由 AI 代理协调不同任务以提高结果的阶段。但 AI 代理不会取代人类,它们将赋能于人类。结合人类反馈的系统将在各个行业提供更好的成果。从长远来看,我想象我们将必须拥有在执行下一任务之前等待人类反馈的系统。
-
AI 与加密货币的融合: 坦率地说,我对整个加密货币领域了解不多,但我看到了Brian Armstrong 的这条推文,讨论 AI 代理应配备加密钱包。此外,像DePin(去中心化物理基础设施)这样的概念,在模型训练和推理方面可能值得探索。虽然这听起来像是流行语游戏,但我很想看看早期实验是否能证明这是真实的还是炒作。
-
每个 token 的延迟和成本将下降: 目前,AI 代理面临的一个大问题是延迟和成本的增加。然而,随着摩尔定律的推进和使 AI 模型更高效的研究,如量化和高效的训练技术(不仅是出于成本原因,也出于环保原因),我可以预见每个 token 的延迟和成本将逐渐降低。
我很想听听你对 2025 年 AI 领域的预测!
PS:有趣的是,几天前,我和 ChatGPT 一起研究圣诞饼干食谱,而不是使用 Google,这让我想起两年前 ChatGPT 发布时我曾想过的事情。
[## 我们明年会用 ChatGPT 代替 Google 查找圣诞饼干食谱吗?
ChatGPT 会取代搜索引擎吗?通过查找糖饼干食谱的使用案例来演示
喜欢这个故事吗?
免费订阅 以便在我发布新故事时收到通知。
[## 每当 Leonie Monigatti 发布时,获取电子邮件通知。
每当 Leonie Monigatti 发布新内容时,您将收到电子邮件通知。通过注册,您将创建一个 Medium 账户(如果您还没有的话)…
在 LinkedIn,Twitter* 和* Kaggle上找到我!
2024 年机器学习工程师面试生存指南
面向初级 MLE 的年终面试准备总结
·发表于Towards Data Science ·14 分钟阅读·2024 年 12 月 24 日
--
找工作真不容易!
在今天的市场中,寻求与机器学习相关职位的工作比以往任何时候都更加复杂。尽管公开报告宣称机器学习工程师(MLE)的职位需求正在快速增长,但事实上,市场在过去几年已经转变为雇主市场。在 2020 年、2022 年和 2024 年寻找机器学习工作可能会是完全不同的体验。更重要的是,一些因素会导致在地理位置、领域以及职位级别之间求职难度的差异:
-
地理位置:根据AI 领域人才报告,2024 年北美招聘最多的城市包括湾区、纽约市、西雅图等。如果我们通过专业人士与职位的比例来评估找工作成功的概率,那么湾区的成功率为 3.6%。然而,如果你住在洛杉矶或多伦多,需求量则大幅降低,导致成功率下降至 1.4%,仅为湾区的 40%。如果你住在其他城市,成功率可能还会更低。
-
领域: 不同领域的机器学习工程师所需的技能集差异很大。以深度学习模型为例,计算机视觉(CV)通常使用像 ResNet、Yolo 等模型,而自然语言处理(NLP)则涉及理解 RNN、LSTM、GRU 和 Transformers;欺诈检测使用 SpinalNet;大语言模型(LLM)注重 Llama 和 GPT 的知识;推荐系统则包括对 Word2Vec 和 Item2Vec 的理解。然而,并非所有领域都招聘相同数量的机器学习工程师。如果我们查找来自 Evidently AI 的系统设计案例研究中的标签,CV 对应 30 个案例,欺诈检测对应 29 个,NLP 对应 48 个,LLM 对应 81 个,而推荐系统对应 82 个。推荐系统的用例几乎是计算机视觉的 2.7 倍。这个比例可能存在偏差,并不完全反映就业市场的实际情况,但它显示了在推荐系统领域对于机器学习工程师的更多机会。
-
资历要求。 根据365 DataScience 报告,尽管 72%的职位没有明确说明所需的工作年限(YOE),但具有 2-4 年经验的工程师需求最高。这意味着你可能会面临更大的挑战来获得入门级工作机会。(大多数需求集中在 2-4 年经验的工程师身上,可能是因为五年前 MLE 还不是一个典型职位,正如这篇博客文章中所解释的。)
本文将总结 MLE 面试准备的材料和策略。但请记住,这只是我收集到的经验列表,可能适用于你的背景或即将到来的面试,也可能不适用。希望本文能够为你职业发展的旅程提供一些启示或指导。

图片来源:pxhere.com/en/photo/655321
面试前 — 该期待什么?
面试过程可能会被延长、痛苦且孤独。当你开始申请工作时,有些事情你需要提前思考并做出相应的计划:
-
面试时间表
-
职位类型
-
公司类型
-
领域
-
地点
面试时间表。 每个公司的时间表不同。对于规模较小的公司(<500 人),通常处于种子轮或 A/B 轮阶段,面试时间通常较短,你可以预期在几周内完成申请过程。然而,对于规模较大的公司(> 1 万人或 FAANG 公司),从申请提交到最终录用阶段,可能需要 3 到 6 个月,甚至更长时间。
职位类型。 我建议参考 Chip Huyen 的《机器学习面试》书 以获取有关不同机器学习相关职位的详细讨论。MLE 的职位可能有不同的名称,例如机器学习工程师、机器学习科学家、深度学习工程师、机器学习开发者、应用机器学习科学家、数据科学家等。归根结底,典型的机器学习工程师角色是端到端的,这意味着你将从与产品经理(有时是客户)沟通并定义机器学习问题开始,准备数据集、设计和训练模型、定义评估指标、服务和扩展模型,并持续改进结果。有时公司会混淆职称,例如将 MLE 与 ML Ops 混在一起。重要的是职责,而不是职称。
公司类型。 再次, Chip Huyen 的《机器学习面试》书讨论了应用型公司与工具型公司、大公司与初创公司、B2B 与 B2C 公司的差异。此外,还值得考虑公司是上市公司还是私有公司,是否以销售为驱动或以产品为导向。这些概念不容忽视,特别是如果你正在寻找第一份行业工作,因为它们将构建你“职业的眼镜”,我们将在面试策略部分详细讨论。
领域。 如在引言部分所述,MLE 的工作可能涉及不同的领域,如推荐系统和大型语言模型(LLMs),你需要花时间准备基础知识。你需要确定一个或两个最感兴趣的领域,以最大化你的机会;然而,准备所有不同的领域几乎是不可能的,因为这会分散你的精力和注意力,使你准备不足。
地点。 除了上述所有因素外,地点是一个重要的因素。如果你住在像湾区或纽约这样的高需求地区,寻找 MLE 职位会更容易。如果无法搬迁,你可能需要为获取令人满意的工作邀请计划更长的时间线;然而,如果你保持开放的搬迁选项,申请高需求地区的职位可能是一个好主意。
面试过程中 — 如何准备?
一旦你开始申请并开始获得面试机会,有几件事是你需要搜索和准备的:
-
面试形式
-
推荐、网络
-
LinkedIn 或作品集
-
面试资源和材料
-
策略:规划、跟踪、演变、提示、评估你的水平、戴上“职业的眼镜”、找到面试伙伴、注意警示信号
-
接受 offer
面试形式
面试的形式在不同公司之间有所不同。没有两家公司对 MLE 职位的面试形式完全相同,因此你必须提前做足“功课”,研究面试的形式。例如,即使是 FAANG 公司,苹果也以其创业型的面试形式著称,这种形式在不同团队之间可能会有所不同。另一方面,Meta 往往在公司层面上有一个一致的面试形式,通常包括一到两轮 LeetCode 编程题和 ML 系统设计题。通常情况下,招聘人员会提供关于大公司面试形式的详细信息,这样你就不会感到意外。然而,小公司可能会有更少的结构化流程,且更频繁地变化。有时候,小公司会用其他编程题代替 LeetCode 编程题,且只会轻度触及建模部分,而不是进行完整的 ML 系统设计环节。你应该在像 Prepfully、Glassdoor、Interview Query 或其他付费网站上查找关于面试形式和流程的信息,以便更好地提前准备。最后,不要被面试形式所限制,因为它并不是一个标准化测试——在技术面试中可能会涉及行为元素,而在招聘经理的面试环节中可能会有技术性问题。做好准备,但要灵活,并准备好应对意外。
推荐和网络建设
许多网络文章会夸大推荐信的好处,但拥有推荐信只是让你更快通过招聘人员筛选,直接进入第二轮面试(通常是招聘经理面试)。除了拥有推荐信,面对面的网络建设几乎同样重要,例如,利用黑客马拉松的机会与公司交流,参加现场招聘会,参与由你感兴趣的公司赞助的线下志愿者活动。请不要依赖推荐信或网络建设来找工作,而是将它们作为增加你获得更多招聘人员和招聘经理面试机会的机会,以最大化面试效率。
LinkedIn 或 个人作品集。 LinkedIn 和个人作品集只是广告工具,帮助招聘人员了解你在简历中的文字信息之外的内容。作为一名初级 MLE,将课程项目和 Kaggle 挑战放在你的 GitHub 仓库中,能够展示更多相关经验,这会有所帮助;然而,随着你职位的晋升,玩具项目的意义就不那么大了,但在大型开源项目中的 PR、深刻的文章和分析、SOTA 研究或工具箱的教程,将使你从其他候选人中脱颖而出。
面试资源和材料
一般来说,你需要涵盖五个领域的材料:i) 编程,ii) 行为,iii) 机器学习/深度学习基础,iv) 机器学习系统设计,以及 v) 一本关于 MLE 面试的通用建议书。
i) 编程。如果你不是 LeetCode 专家,那么我建议你从以下资源开始:
[## NeetCode
更好的编程面试准备方法。
neetcode.io [## 破解编程面试:189 个编程问题
阅读来自全球最大读者社区的 23 条评论。我不是招聘人员。我是一个软件工程师。而且……
www.goodreads.com [## 编程面试模式:让你成功通过下一次编程面试
阅读来自全球最大读者社区的评论。未定义
ii) 行为:
阅读来自全球最大读者社区的评论。最快的方式提高你的薪资和补偿……
www.goodreads.com [## 员工工程师之路:个体贡献者的指南
阅读来自全球最大读者社区的 163 条评论。多年来,公司一直奖励他们最……
iii) 机器学习/深度学习基础:
阅读来自全球最大读者社区的 5 条评论。本书提供了一个全面的介绍……
iv) 机器学习系统设计:
阅读来自全球最大读者社区的 7 条评论。机器学习系统设计面试是最……
www.goodreads.com](https://www.goodreads.com/book/show/120532868-machine-learning-system-design-interview?source=post_page-----e74eccef4645--------------------------------) [## 设计机器学习系统:一个迭代过程...
阅读来自全球最大读者社区的 76 条评论。机器学习系统既复杂又独特…
www.goodreads.com](https://www.goodreads.com/book/show/60715378-designing-machine-learning-systems?source=post_page-----e74eccef4645--------------------------------)
v) 一本通用的 MLE 面试建议书:
您可以在此处阅读该书的网页友好版。源代码可以在 GitHub 上找到。讨论可以通过 Discord 进行…
huyenchip.com](https://huyenchip.com/ml-interviews-book/?source=post_page-----e74eccef4645--------------------------------) [## 机器学习面试揭秘:151 个真实问题…
阅读来自全球最大读者社区的评论。让曾任亚马逊工程领导的彭绍(Peng Shao)为您分享经验…
www.goodreads.com](https://www.goodreads.com/book/show/152155500-inside-the-machine-learning-interview?source=post_page-----e74eccef4645--------------------------------)
您还需要有一些面试伙伴——现在,您可以订阅在线面试准备服务(不要选择那些收费高昂、要您花费上千美元的服务;总有更便宜的替代品),并与其他 MLE 候选人进行技能和信息交换。
策略:规划、跟踪、演变、提示、提升面试水平、戴上“职业发展的眼镜”、寻找面试伙伴、警惕红旗
规划、跟踪和进化。理想情况下,正如本文所描述的,你应该至少收到几次招聘人员的电话,并将面试按兴趣级别进行分类。一方面,招聘市场在不断变化,没人能够在第一次面试时就规划出最佳策略。另一方面,你会在面试过程中不断学习和成长,因此你会与几个月前的自己不同,尤其是在求职初期。所以,即使你是市场上最有才华的候选人,也非常有必要将面试分布在几个月的时间内,并且从你最不感兴趣的对话开始,以便熟悉市场并磨练面试技巧,而把最重要的对话留到后期。在面试过程中跟踪你的进展、反馈和思考。设定具体的学习目标,并随着面试不断进化。过去几年你可能从未接触过生成式人工智能(GenAI)知识,但你可以利用面试过程,通过在线课程学习并构建小型副项目。最好的结果是面试后拿到工作,第二好的结果是即使没有获得工作邀请,你也学到了一些有用的东西。如果你在每次面试中都不断学习,最终,它将大大提高你获得下一份工作邀请的机会。
提示。现在是大型语言模型(LLM)的时代,你应该明智地利用它。查找职位描述或职责中的关键词。如果有涉及“软件工程原则”的面试,那么你可以提示你喜欢的 LLM,要求它提供一份关于机器学习的软件工程原则的清单,以备准备之用。再次强调,提示的答案不应是你唯一的知识来源,但它可以弥补你日常阅读资料中的一些盲点。
提升一个职位层级的面试。有时,职位层级之间的界限并不明确。除非你在这个领域是绝对的新手,否则你可以尝试申请比自己当前层级高一层的机会,并为职位邀请阶段准备低一层的机会。如果你正在面试高级职位,准备或申请更高层次的职位并不会有什么坏处。它不总是有效,但有时它能为你打开一些大门。
戴上你的“职业眼镜”。面试时不要只是单纯地想着面试本身。除非你因为某种特殊原因急需这份工作,否则请问问自己,这份工作在你的整体职业规划中扮演着什么角色?这个问题有两个层面的意义:首先,它帮助你选择你想要加入的公司,例如,一家初创公司可能短期内提供更高的薪水,但如果它不重视软件工程的基本原则,那么你将失去在长期内成长为一份有前途的职业的机会;其次,它帮助你诊断面试的结果,例如,你的拒绝大多来自初创公司,但最终你收到了知名上市公司的 offer,那么你就会意识到这些拒绝并不意味着你不合格,而是因为在初创公司面试需要不同的技巧,而那些技巧并不属于你的职业路径。
结伴而行。五年前,还没有所谓的面试伙伴。不过现在,互联网上有各种各样的面试服务,从极其昂贵(我不推荐)到几百美元不等。记住,这是一个不断变化的市场,因此没有人能掌握全貌。获得信息的最佳方式是与非竞争领域的同行合作(例如,你在简历领域,而你的伙伴在推荐系统领域),共同练习并一起提高。比起单纯的合作,你应该主动寻求合作——尽量寻找那些在更高职位的人,趁你还能为他们提供有价值的帮助时。你可能会问,这怎么可能?为什么比我资深的人愿意和我一起练习?记住,没有人是完美的,你总是能为别人提供一些东西。有些资深的软件工程师希望转型成 MLE(机器学习工程师),你可以用你的机器学习知识交换他们的软件工程最佳实践。有些产品经理需要机器学习相关的输入,你可以请求他们提供行为面试练习作为回报。即使是那些没有行业经验的初级人员,你也可以向他们请求编码练习,或者听听他们的生活故事,获得灵感。作为 MLE,特别是在高级/职员级别,你需要展示领导力,而你能展示的最佳领导力就是集结不同层级的专业人士,帮助你实现你追求的目标——你的梦想 offer。
红色警示信号。一些明显的红色警示信号,比如要求你加班过多或面试后“失联”,是显而易见的。然而,有些红色警示信号则更为微妙或故意伪装。例如,招聘经理可能会礼貌地解释他们的情况,并希望你“刚开始时不要抱有过高的期望,几个月后再决定是否离开”——这听起来非常体贴,然而它掩盖了员工流动性高的事实。避免红色警示信号的最佳策略是通过阅读 Glassdoor 评论和在面试过程中了解公司文化。具体来说,“文化”并不是指公司网站上定义的“文化宣言”,而是你与团队之间的实际互动。面试官是否只是在问事先准备好的问题,而没有试图了解你的问题解决能力?当你提出问题时,面试官能否理解并给出一个有助于你更好理解公司价值的回答?最后,始终记得听从你的直觉,决定你是否喜欢未来的团队。毕竟,如果你决定接受这份工作,你将每天与这些人相处八个小时,未来几年都是如此;如果你的直觉告诉你不喜欢他们,那么无论如何你都不会感到快乐。
接受录取通知。 一旦你度过了所有的沮丧、失望和辛苦的工作,是时候讨论录取通知了。很多论坛讨论了谈判薪资的必要性,但我建议在这个雇主市场中要谨慎。如果你想谈判,最好的做法是手里有两个可比的录取通知,并准备好最坏的情况。同时,像 Levels.fyi 和 Glassdoor 这样的站点可以用来研究薪酬范围。
面试后——接下来该做什么?
恭喜!现在你已经接受了录取通知,准备开始你的新旅程,但还有什么其他事情可以做吗?
总结一下你的面试过程。 面试过程漫长且充满挑战,但你也从中获得了许多经验!现在你已经放松并感到开心,这是一个反思的最佳时机,感谢那些在面试过程中帮助过你的人,并与那些还在为面试过程挣扎的人分享一些信息。另外,过去几个月你一定积累了很多笔记和待办事项清单,却从未有时间坐下来整理,现在正是整理它们的最佳时机!
规划你的职业路径。 在过去几个月中,你的自我认知可能发生了变化;现在,你更好地了解了自己在压力下的学习能力和解决问题的能力。你与许多初创公司和大公司进行了交流,开始更清楚地了解自己在未来五年的发展方向。如果你处于中高层级,那么你可能已经与许多资深员工交流,获得了关于下一阶段工作的更清晰的认知。现在正是进行所有这些规划的好时机!
持续学习。 如果你来自学术背景,你可能已经习惯于阅读论文,了解最新的机器学习技术。然而,MLE(机器学习工程师)的角色远不止学术领域。它结合了研究、应用机器学习实践和软件工程,以对业务产生实际影响。现在是时候思考最佳策略,利用多种来源持续学习,以保持自己的知识更新。
为新角色做好准备。 你已经与新公司的其他 MLE 进行了交流,了解了他们使用的模型或技术栈。在某些情况下,你可能已经非常熟悉这些技术栈,但大多数时候,你需要为新角色学习很多新知识。制定一个学习计划,并设定小的里程碑来实现目标。此外,了解你的新公司,浏览其官网并理解其业务目标。这将有助于你在开始新工作时树立一个良好的印象,并与新同事交流。
毕竟,每个人的面试旅程都是不同的。你的经验水平、专注领域、长期职业目标和个性都共同塑造了一个独特的面试旅程。希望本文能为你提供面试准备材料和策略上的一些启发。也希望每个人最终都能拿到心仪的工作邀请!
致谢: 特别感谢Ben Cardoen和Rostam Shirani的校对和富有洞察力的建议,帮助本文达到了最终版本。
参考文献
-
Sophie Magnet,“2024 年最受欢迎的机器学习工程师技能”,链接:
365datascience.com/career-advice/career-guides/machine-learning-engineer-skills -
Sam Jones,“2024 年机器学习工程师的就业市场”,链接:
www.peopleinai.com/blog/2024-job-market-for-machine-learning-engineers -
詹妮弗·罗伯逊,《为什么很难找到资深机器学习工程师》,链接:
blog.qwasar.io/blog/why-its-so-difficult-to-find-senior-machine-learning-engineers -
奇普·许恩,《机器学习面试入门书》,链接:
huyenchip.com/ml-interviews-book/ -
NeetCode 路线图,链接:
neetcode.io/roadmap -
格雷尔·拉克曼·麦克道尔,《破解编程面试:189 个编程问题与解决方案》,2015 年
-
亚历克斯·徐,《编程面试模式:掌握你的下一次编程面试》,2024 年
-
亚历杭德罗·孔帕尼奥尼,《2024 年作为机器学习工程师求职指南》,链接:
acompa.net/mle-job-hunt-2024.html -
彭绍,《机器学习面试揭秘:来自 FAANG 的 151 个真实问题及其解答》,2023 年
-
奇普·许恩,《设计机器学习系统:生产就绪应用的迭代过程》,2022 年
-
阿里·阿米尼安,亚历克斯·徐,《机器学习系统设计面试》,2023 年
-
克里斯托弗·M·毕晓普,休·毕晓普,《深度学习:基础与概念》,2023 年
-
塔尼娅·瑞利,《资深工程师之路:个体贡献者在成长与变革中如何导航》,2022 年
-
梅莉亚·斯特凡诺维奇,《软件工程师行为面试:所有必须知道的问题以及能让你获得工作的证明策略与答案》,2023 年
2024 年:价值驱动的数据人才之年
“以增长为唯一目标”的理念已经被“需要高效运作并以投资回报率为驱动”所取代——数据团队也不例外
·发表于 Towards Data Science ·7 分钟阅读·2024 年 1 月 4 日
--
如果你在过去几年里从事技术工作,那真是经历了一场旋风。
从 2022 年到 2023 年,风险投资资金下降了 72%
新上市的公司从 2021 年到 2022 年下降了 82%
2023 年,美国超过 150,000 名技术工人被裁员

2023 年的现实检验。来源:作者,引用了《Techcrunch》,《The Verge》和 CNN 等出版物
在 2021 年之前的繁荣时期,资金充裕,团队发展速度远远跟不上需求。2022 年,“以增长为唯一目标”的理念被盈利目标所取代。预算不再基于天马行空的目标分配,而是受到首席财务官的严格审查。
数据团队也未能幸免于此。根据dbt 2023 年的调查,28%的数据团队计划裁减人员。
根据对选定规模扩展公司中数据岗位数量的观察,与去年年初相比,减少员工数量的公司比扩展团队的公司要多。

来源:作者根据 2023 年 4 月和 2024 年 1 月的 LinkedIn 数据创建。数据团队 通常占公司员工的 1-5% 。
数据团队的新现实
数据团队现在发现自己正处于一个深渊之中。
一方面,数据团队的投资回报率(ROI)历来难以衡量。另一方面,人工智能正处于风口浪尖(根据《麻省理工科技评论》的一项调查,81%的高管认为人工智能将成为他们业务的重要竞争优势)。AI 和 ML 项目通常有更明确的 ROI 案例,而数据团队正处于这一中心,越来越多的机器学习系统依赖数据仓库来驱动。
那么,2024 年数据人员该做什么呢?
在下文中,我探讨了如果你从事数据相关工作,确保自己处于有利位置并与业务价值对齐的五个步骤。
向你的利益相关者寻求反馈
当人们能够分享关于你的看法时,他们会觉得被倾听,这也给你提供了一个了解自己弱点的机会。你应该抓住这个机会,主动向重要的利益相关者寻求反馈。
虽然你可能不想对公司里的每个人进行调查,但你可以创建一个以数据为依赖的人员小组,例如所有高级职位的员工。让他们对诸如对自助服务的满意度、仪表板的质量以及是否在他们的领域有足够的数据人员等问题提供坦率且匿名的反馈(这也能在你要求增加人手时为你提供一些有力的支持)*。

来源:作者
最后问一个问题:“如果你有一根魔法棒,你会改变什么?”,让他们提出开放式的建议。
调查结果——关于数据团队数据工作的数据。这没有变得更好……

营销数据团队需要一些审视。数据基础设施也没有做得很好。来源:作者
对调查结果保持透明,并将其反馈给利益相关者,同时给出明确的行动计划来改进需要改进的地方。如果你每六个月进行一次调查,并且言行一致,你可以在后续展示出实际的改进。确保收集关于受访者所在业务领域的数据。这将为你提供一些宝贵的洞察,帮助你发现盲点,了解是否存在你未曾察觉的业务领域痛点。
构建一个商业案例,就像你在寻求风险资本投资一样
你可以坐等利益相关者的请求找到你。但如果你像大多数数据人员一样,你会希望对自己参与的项目有所发言权,甚至可能自己有一些想法。
在我担任 Google 数据分析师时,一位业务单元总监曾分享过一句智慧的建议:“如果你希望我支持你的项目,就像你是一个创业者在为你的初创公司筹集资金一样向我展示。” 这听起来可能像硅谷的言辞,但当我深入思考时,他确实有些合理的观点。
-
展示总的$机会是多少,以及你预期能够捕获的百分比
-
展示你已经做出了一个最小可行产品(MVP)以证明它是可行的
-
给我看看替代方案,以及为什么我应该选择你的想法
示例–ML 模型商业案例提案总结

来源:作者
像上面那样的商业案例提案通常会呈现给你所在领域的一些高级利益相关者,以争取他们的支持,让你把时间花在这项工作上,而不是你可以做的其他成百上千项事情上。它为他们提供了一个透明的论坛,让他们从一开始就参与项目,也为那些机会太小或风险太大的项目提供了一个早期否决的机会。
像新的 ML 模型或新项目创造运营效率这样的项目特别适合这种情况。但即使你被要求重建一组数据模型或构建一个全公司的 KPI 仪表盘,应用一些相同的原则也是有意义的。
采取整体的成本减少方法
当你考虑成本时,很容易陷入“只见树木不见森林”的境地。例如,数据分析师通过优化一些最耗时的 dbt 查询每月节省$5,000,听起来似乎很有成效。但尽管这些成就不应被忽视,采取一种更为整体的成本节约方法会更有帮助。
从问自己数据团队的所有成本组成部分及其含义开始。
如果你以一个中型数据团队为例,在规模扩展的公司中,三个最大的成本驱动因素往往是不成比例的分配为:
-
员工人数(15 个 FTE × $100,0000):$1,500,000
-
数据仓库成本(例如,Snowflake,BigQuery):$150,000
-
其他数据工具(例如,Looker,Fivetran):$100,000

来源:作者
这并不是说你应该立即专注于裁减人员,但如果你的成本分布与上述类似,问问自己以下问题:
我们是否应该让 2 倍 FTE 的员工构建这个内部工具,还是可以直接购买?
是否有低价值的项目,导致高成本人员被浪费在其中?
为了节省$5,000 的成本,花费两周的工作时间,这样的投资回报是否合适?
在开发工作流程中,是否有优化空间,比如 CI/CD 检查速度,可以改进以释放更多时间?
平衡速度和质量
我曾见过团队因拥有成千上万个 dbt 测试而陷入困境,这些测试遍布数千个数据模型。很难知道哪些是重要的,而且开发新的数据模型比其他所有事情都需要花费两倍的时间,因为所有东西都必须经过相同的严格审查。
另一方面,那些几乎不对数据管道进行测试,构建不遵循良好数据建模原则的数据模型的团队,往往会发现自己进展缓慢,必须花费两倍的时间去清理和修复事后出现的问题。
值导向的数据人员仔细平衡速度和质量
-
对最重要的数据资产的共同理解
-
根据数据资产的重要性,定义期望的测试级别
-
根据问题的严重性,定义期望的解决时间和服务级别协议(SLA)
他们也知道,为了成功,他们的公司需要更像一艘快艇,少像一艘油轮——通过实验快速学习什么有效,什么无效,快速调整方向,每两周回顾一次进展,给予每个团队自主权来设定他们的方向。
数据团队通常在不确定性中工作(例如,这个 ML 模型是否有效)。你发货越快,越能快速了解到什么有效,什么无效。最优秀的数据人员总是谨慎地记住这一点,并知道自己处于曲线的哪个位置。
例如,如果你是一个 ML 工程师,正在开发一个模型来决定哪些客户可以注册一个价值数十亿的全新银行,你就无法再做快速且草率的工作。但如果你在一个种子阶段的创业公司工作,整个后台系统可能在几个月内会重做,你就会知道有时候需要在速度和质量之间找到平衡。
主动分享你的工作影响
数据角色的人通常不是那些大声宣扬自己成就的人。虽然没有人愿意做无耻的自我宣传者,但还是可以寻找到一个平衡点。
如果你做的工作产生了影响,不要害怕让同事知道。如果你有一些数据来支持这一点,那就更好(谁比你更清楚数据工作的影响)。在做这件事时,很容易被构建过程中的实施细节困扰,比如有多难,使用了什么精妙的算法,或者写了多少行代码。但利益相关者对此并不关心。相反,考虑用以下的方式来表达。
-
聚焦影响
-
不要仅仅说“交付了 X”
-
使用“交付了 X,并且产生了 Y 的影响”
当事情没有按预期进展时,不要害怕提前指出。例如,如果你正在做一个毫无进展或变得越来越复杂的项目,就要及时指出。你可能会担心这样做会让自己陷入困境,但你的利益相关者会认为这是表现出高度的责任感,而不是陷入沉没成本谬论。
如果你对文章有更多想法或问题,欢迎通过LinkedIn 联系我。
如何在不到 12 个月的时间里从分析师转型为数据科学家
没有技术背景,也没有数学博士学位
·发布于 Towards Data Science ·6 分钟阅读·2024 年 8 月 24 日
--

图片来自 fancycrave1 ,来源于 Pixabay
我的数据科学之旅始于一次危机。
在星期一早上 9:01,我的手机响起了 PagerDuty 警报。本能地,我打开了公司仪表盘,看到贷款批准率在一夜之间翻倍了。
我的心开始剧烈跳动。
那时我是一名风险分析师,管理着一个贷款组合。批准高风险细分市场的贷款可能对损失率产生巨大影响。
睁着疲惫的眼睛,我投入了数据分析。
我疯狂地:
-
编写 SQL 查询以提取数据,
-
用 Python 分析数据,试图找出异常,
-
寻找能够解释批准率突然激增的模式。
我发现信用模型中的一个特定特征发生了漂移。
结果证明这只是一个简单的时区错误。
这一事件成为我职业生涯的转折点,标志着我从数据分析师转型为数据科学家的开始。
不要从零开始
发现 Llama 模型中每个神经元的作用
Transluce 的新工具正在改变 AI 透明度的游戏规则——一个测试案例和一些思考材料
·发表于Towards Data Science ·7 分钟阅读·2024 年 10 月 25 日
--

图片由作者提供——拍下了使用新工具的瞬间!
Transluce,一个具有启发性使命的非营利研究实验室,刚刚发布了一个引人入胜的工具,提供了 LLM 中神经元行为的洞察。或者用他们自己的话来说:
当 AI 系统的行为出乎意料时,我们希望了解“思考过程”,以解释为何会发生这种行为。这让我们能够预测并修复 AI 模型中的问题,揭示隐藏的知识,揭露学到的偏见和虚假的关联。
为了实现他们的使命,他们推出了一个可观察性界面,你可以在其中输入自己的提示语,获取回应,并查看哪些神经元被激活。你可以进一步探索这些激活的神经元及其对模型输出的归因,所有这些都得益于他们通过创新方法自动生成高质量的神经元描述。
如果你想测试这个工具,可以点击这里。他们还提供了一些有用的教程。在本文中,我将尝试提供另一个用例,并分享我自己的经验。
你可能需要了解很多内容(具体取决于你的背景),但我将重点介绍两个关键特性:激活值和归因。
激活值衡量的是神经元的(归一化)激活值。Llama 使用门控 MLP,这意味着激活值可以是正数也可以是负数。我们通过神经元在大规模数据集上的 10–5 分位数值进行归一化。
归因 衡量神经元对模型输出的影响程度。归因必须基于特定的输出标记,并等于该输出标记概率相对于神经元激活的梯度,乘以神经元的激活值。归因值没有标准化,以绝对值的形式报告。
利用这两个特征,你可以探索模型的行为、神经元的行为,甚至注意到神经元行为现象的模式(或者他们称之为“聚类”)。
如果模型输出不是你期望的,或者模型得出了错误的结果,该工具允许你通过加强或抑制与概念相关的神经元来引导并“修复”问题(有许多出色的工作讲解如何根据概念进行引导 —— 其中之一是这篇出色的工作)。
所以,出于好奇,我用我自己的提示进行了测试。
我选择了一个大多数今天的模型无法解决的简单逻辑问题。
问题:“𝗔𝗹𝗶𝗰𝗲 有 4 个兄弟和 2 个姐妹。爱丽丝的兄弟有多少个姐妹?”

主页。图片来自monitor.transluce.org
瞧,成功了……

Llama 做错了。图片来自monitor.transluce.org
或者没有。
在左侧,你可以看到提示和输出。在右侧,你可以看到“激活”最多的神经元,并观察这些神经元聚集的主要簇。
如果你将鼠标悬停在左侧的标记上,你可以看到最高的概率。如果你点击其中一个标记,你可以找到哪些神经元参与了预测该标记。

将鼠标悬停在“in”上。我们可以看到最高概率的标记。图片来自monitor.transluce.org
正如你所看到的,逻辑和答案都是错误的。
“因为爱丽丝有 4 个兄弟,所以我们需要找出他们共有多少个姐妹” >>> 哎呀!你已经知道答案了。
当然,如果爱丽丝有两个姐妹(这是输入中给定的),这并不意味着爱丽丝的兄弟有 2 个姐妹 😦
所以,让我们试着修复这个问题。在检查神经元之后,我注意到“多样性”概念过于活跃(也许它对爱丽丝的身份感到困惑?)。因此,我尝试调整这些神经元。

引导窗口。图片来自monitor.transluce.org
我抑制了与此概念相关的神经元,并重新尝试:

调整后的模型在引导后。图片来自monitor.transluce.org
如你所见,它仍然输出了错误的答案。但如果仔细观察输出,逻辑已经有所变化,看起来好多了——它捕捉到我们需要“转变”到“其中一个兄弟的视角”。而且,它也理解了爱丽丝是一个姐妹(终于!)。
最终答案仍然不正确。
我决定加强“性别角色”这一概念,认为这有助于模型更好地理解问题中兄妹的角色,同时保持它对爱丽丝与她兄妹关系的理解。

另一个调整。图片来自monitor.transluce.org
好吧,答案仍然不正确,但似乎推理过程略有改善。模型表示“提到的是爱丽丝的两个姐妹”。句子的前半部分表现出一定的理解(是的,这部分也在输入中。并且不,我并不是在争论模型或任何模型能否真正理解——这是另一个讨论的话题),即爱丽丝有两个姐妹。它也依然认知到爱丽丝自己是一个姐妹(“…这个兄弟有 2 个姐妹——爱丽丝和另一个姐妹…”)。但答案还是错的。真是差一点……
现在我们接近正确答案了,我注意到一个无关的概念(“化学化合物与反应”)影响了“2”这个符号(左侧以橙色高亮显示)。我不确定为什么这个概念会有这么大的影响,但我决定它与问题无关,于是将其抑制了。
结果如何?

最终结果。图片来自monitor.transluce.org
成功了!!(差不多)
如上所示,它终于得到了正确的答案。
但是……推理过程如何呢?
嗯……

最终输出。图片来自monitor.transluce.org
它跟随了一种奇怪的逻辑过程,带有一些角色扮演上的混乱,但最终还是得出了正确的答案(如果你能解释这个过程,请分享)。
所以,在一些试错后,我几乎成功了。在调整了与性别和化学化合物相关的神经元后,模型给出了正确答案,但推理过程还不够完善。我不确定,也许通过更多的微调和调整(或许更好的概念和神经元选择),我能够同时得到正确的答案和正确的逻辑。我挑战你试试看。
这仍然是实验性的,我没有使用任何系统化的方法,但说实话,我很受震撼,认为这非常有前景。为什么?因为能够观察并获取每个神经元的描述,理解(即使是部分理解)它们的影响,并实时地引导行为(无需重新训练或提示)真是令人印象深刻——是的,也有点上瘾,所以要小心!
另一个想法是:如果这些描述是准确的(反映了实际行为),而且如果我们能够手动尝试不同的设置,为什么不尝试基于神经元激活和归因值构建一个模型呢?Transluce 团队,如果你们在看这条信息……你们怎么看?
总的来说,做得很好。我强烈建议深入研究一下。它的易用性和观察神经元行为的能力非常有吸引力,我相信我们会看到更多的工具采用这些技术来帮助我们更好地理解我们的模型。
我现在打算在一些最具挑战性的法律推理用例中测试这个——看看它如何捕捉更复杂的逻辑结构。
这对人工智能意味着什么?我们得拭目以待……但就像 GPT 如此迅速自然地被接受一样,我认为这个版本的发布为大语言模型的可解释性开辟了新篇章。更重要的是,它将我们带得更近,朝着构建更加对齐和负责任的工具迈进。
既然他们的工作是开源的,那么就由社区来挑战它、改进它或在此基础上进行构建。
所以,试试看吧。
与此同时,你怎么看?
一些限制(简要说明):
-
该工具就在昨天发布,我还没有完全有机会审阅整个文档。
-
我成功地尝试了简单的问题,但当我提出带有不同属性的类似问题时,逻辑仍然失败。归纳能力是这里的关键——尝试在可观察性工具中“捕捉”某些归纳性,将使其达到新的高度。
-
即使在低温或零温设置下,也并非总是可以重现。
-
没有单一的路径可以同时获得正确答案和逻辑推理。
-
它涉及到相当多的反复试验。在几次迭代后,我开始“感觉到”它的运作,但这和刚开始使用 GPT 时很像——当它有效时令人兴奋,但常常让你陷入困惑,“这里到底发生了什么?”因此,仍然需要进一步的工作。
提升 RAG 系统的三种高级文档检索技术
查询扩展、跨编码器重排序和嵌入适配器
·发表于Towards Data Science ·10 分钟阅读·2024 年 1 月 15 日
--

图片由作者使用 DALL-E 3 创作
你是否曾观察到,RAG 系统检索到的文档并不总是与用户的查询完全匹配?
这是一个常见的现象,尤其是在现成的 RAG 实现中。检索到的文档可能无法完全回答查询,可能包含冗余信息,或者有不相关的细节。此外,这些文档呈现的顺序也可能无法始终与用户的意图相符。
在这篇文章中,我们将探讨三种有效的技术,以增强基于 RAG 的应用中的文档检索:
-
查询扩展
-
跨编码器重排序
-
嵌入适配器
通过结合这些技术,你可以检索到更相关的文档,这些文档与用户的查询更加匹配,从而提高生成答案的影响力。
让我们来看一下👇。
如果你对机器学习内容感兴趣,想要获取来自行业的详细教程和实用技巧,欢迎关注我的 新闻通讯。它的名字叫做《技术自助餐》。
3 个 AI 应用场景(不是聊天机器人)
特征工程、结构化非结构化数据和潜在客户评分
·发布于Towards Data Science ·阅读时间:7 分钟·2024 年 8 月 21 日
--
我从(AI 咨询)客户那里收到的最常见请求是“帮我构建一个定制聊天机器人。”虽然这对某些问题来说是一个很好的解决方案,但它远不是万能的。在本文中,我将分享三种商业可以在销售环境中使用 AI 创造价值的替代方式。这些方法涵盖了生成性 AI、深度学习和机器学习。

图片来自 Canva。
大型语言模型(LLMs)已经席卷了商业世界,现在每家公司都在尝试使用生成性 AI。虽然像 ChatGPT 这样的工具显然很强大,但目前尚不清楚企业如何可靠地利用这项技术来驱动价值。
对于我接触过的大多数企业来说,“使用 AI”意味着构建一个聊天机器人、协作助手、AI 代理或 AI 助手。然而,随着对这些解决方案的初步兴奋逐渐消退,组织们开始意识到围绕 LLMs 构建系统的关键挑战。
这比我想象的还要难…
一个核心挑战是,LLMs 本质上是不可预测的(比传统的机器学习系统还要不可预测)。因此,让它们以可预测的方式解决特定问题并不容易。
3 种流行的空间插值方法
从头开始,用 Python 实现
·发布于 Towards Data Science ·10 分钟阅读·2024 年 4 月 9 日
--

图片由作者提供。
插值是一个相当常见的数学概念,不仅数据科学家使用,许多其他领域的人也会用到。然而,当处理地理空间数据时,插值变得更为复杂,因为你需要基于多个往往稀疏的观测数据创建一个具有代表性的网格。
在深入探讨地理空间部分之前,让我们简要回顾一下线性插值。
和往常一样,要跟上教程,你可以在这里下载并运行笔记本。
为了演示,我将使用一个常规的多项式函数:
def F(x):
return -2*x**3+x**2+2.1
x = np.arange(-5,5, 0.1)
y = F(x)

图片由作者提供。
现在我们可以随机选择几个点 [-4.2, 0, 2.5] 并将它们连接起来:

图片由作者提供。
这就是线性插值,因为在每个区间内,函数由一条直线近似,而现在,只知道函数在 3 个点的值,我们就可以找到区间[-4.2;2.5]内的值。
还有许多其他方法,它们具有更高的精度,但它们的基本思路是相同的:在至少两个已知点之间找到函数值。
现在是时候进入地理空间部分了。在本教程中,我们的目标是对瑞士各地气象站测得的每日平均气温进行空间插值,这些数据由NOAA提供。预期结果是生成一个温度网格,单元格分辨率为 0.1°。
首先,我们需要获取瑞士的行政边界,并使用 geopandas 将其可视化:
import geopandas as gdp
shape = gpd.read_file('gadm41_CHE_0.shp')
shape.plot()

图片由作者提供。
确实,看起来像是瑞士,哇 =)
现在让我们绘制我们的温度观测数据,并将其与国家边界形状叠加。为此,我们将气象数据加载到常规的 pandas 数据框中,然后将其转换为带有坐标的 geopandas 数据框,这些坐标转化为shapely点:
import pandas as pd
from shapely.geometry import Point
df = pd.read_csv('3639866.csv')
points = list()
for i in range(len(df)):
point = Point(df.loc[i, 'LONGITUDE'], df.loc[i, 'LATITUDE'])
points.append(point)
gdf = gpd.GeoDataFrame(geometry=points).set_crs(shape.crs)
完成后,我们可以很容易地使用matplotlib将这两个数据框叠加在一起。
fig, ax = plt.subplots(figsize=(16,9))
shape.plot(ax=ax, color='black')
gdf.plot(ax=ax, color='r', markersize=85)
plt.show()

图片由作者提供。
为了可视化我们的任务,让我们创建一个用于插值的网格,并将其与上面的地图叠加:
import matplotlib.ticker as mticker
import cartopy.crs as ccrs
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
LAT, LON = np.arange(45.75, 48, 0.1), np.arange(6, 10.81, 0.1)
fig, ax = plt.subplots(subplot_kw=dict(projection=ccrs.PlateCarree()), figsize=(16, 9))
shape.plot(ax=ax, color='grey')
gdf.plot(ax=ax, color='r', markersize=85)
gl = ax.gridlines(draw_labels=True,linewidth=2, color='black', alpha=0.5, linestyle='--')
gl.xlocator = mticker.FixedLocator(LON)
gl.ylocator = mticker.FixedLocator(LAT)
gl.xformatter = LONGITUDE_FORMATTER
gl.yformatter = LATITUDE_FORMATTER
plt.show()

图片由作者提供。
所以目标是对上面插值的常规网格进行插值,包含 8 个温度观测值。
I. 最近邻(NN)
第一个直观且简单的方法叫做最近邻(NN)。正如你从名称中可以猜到的,这个算法将最近观测值的值分配给每个网格节点。
为了实现它,我们只需要两个函数。第一个函数叫做 Euclidean,它使用以下公式计算两点之间的距离:

图片由作者提供。
第二个方法就是 NN 方法本身。在创建一个空数组来存储值之后,我们遍历所有纬度和经度,计算每个点到当前网格节点的距离,并将最近观测值的值分配给该网格节点:
def Euclidean(x1,x2,y1,y2):
return ((x1-x2)**2+(y1-y2)**2)**0.5
def NN(data, LAT, LON):
array = np.empty((LAT.shape[0], LON.shape[0]))
for i, lat in enumerate(LAT):
for j, lon in enumerate(LON):
idx = data.apply(lambda row: Euclidean(row.LONGITUDE, lon, row.LATITUDE, lat), axis = 1).argmin()
array[i,j] = data.loc[idx, 'TAVG']
return array
整个思路在这一行中:
idx = data.apply(lambda row: Euclidean(row.LONGITUDE, lon, row.LATITUDE, lat), axis = 1).argmin()
变量数据是我们包含气象站点的 pandas 数据框(每行表示一个站点)。因此,在for循环中,我们计算距离并找到距离最小的站点的索引。
现在让我们运行算法,并将结果封装到 xarray 数据集中:
t2m = NN(df, LAT, LON)
ds = xr.Dataset(
{'TAVG': (['lat', 'lon'], t2m)},
coords={'lat': LAT, 'lon': LON})
现在我们可以绘制结果了:

图片由作者提供。
看起来不错,但让我们通过使用 regionmask 库创建瑞士的掩模来增强我们的绘图:
shape['new_column'] = 0
sw = shape.dissolve(by='new_column')['geometry']
rg = regionmask.mask_3D_geopandas(sw, lon_or_obj=ds.lon, lat=ds.lat)
fig, ax = plt.subplots(subplot_kw=dict(projection=ccrs.PlateCarree()), figsize=(16, 9))
#shape.plot(ax=ax, color='black')
ds.where(rg).TAVG.plot(ax=ax, alpha=0.6)
gdf.plot(ax=ax, color='r', markersize=85)
ax.gridlines(draw_labels=True,linewidth=2, color='black', alpha=0.5, linestyle='--')
plt.show()

图片由作者提供。
如你所见,这种方法只能应用于分类数据。由于我们处理的是温度数据,温度是连续变量,意味着它可以在某一范围内取任何值,因此这种插值是误导性的。现实生活中总是存在梯度和随机性。
那么,让我们来看看更先进的算法。
II. 反距离加权(IDW)
基本上,反距离加权(IDW)是 NN 的增强版:
def IDW(data, LAT, LON, betta=2):
array = np.empty((LAT.shape[0], LON.shape[0]))
for i, lat in enumerate(LAT):
for j, lon in enumerate(LON):
weights = data.apply(lambda row: Euclidean(row.LONGITUDE, lon, row.LATITUDE, lat)**(-betta), axis = 1)
z = sum(weights*data.TAVG)/weights.sum()
array[i,j] = z
return array
如你所见,我们不是直接赋值给最近已知的点,而是计算权重。为此,前面提到的欧几里得距离也被使用,但这次我们将每个距离的-β次方(β是任意值)。这些权重基本上是每个地面点对某个网格节点的贡献。距离越远,这个点对节点值的影响就越小。
在得到权重后,我们计算加权平均值。

图片来自作者。

图片来自作者。
让我们绘制它:

图片来自作者。
如你所见,现在的结果更真实且平滑!
三、克里金(Kriging)
今天的最后一个方法是克里金(Kriging)。在这三者中,这是最复杂的,我们将仅仅触及它。如果你想有意识且有效地使用它,可以考虑查阅相关文献!
所以这个方法的主要思想是使用变异函数(或半变异函数)。本质上,变异函数量化了某个参数如何随着距离和方向的变化而变化。这正是我们在处理空气温度时所需要的。
为了实现克里金算法,我们将需要两种类型的变异函数:实验变异函数和理论变异函数。
第一个计算非常简单。它被定义为gamma γ:

图片来自作者。
其中 h — 两点之间的地理距离,z — 温度函数。所以简而言之,它是已知点温度差的平均值。
理论变异函数要复杂一些。首先,有很多种:

图片来自作者。
其中 p — 是部分阈值,d — 距离(我们之前用了h),n — 暴露量,r — 范围。
我在CDT Columbia找到了一个非常好的这些参数的可视化解释。我从他们的资料中采用了一张插图,展示了γ与距离之间的关系。如你所见,现在清楚地展示了阈值、部分阈值、暴露量和范围。

图片来自作者。
所以这个算法的整个思路是调整理论变异函数的参数,使其与实验变异函数匹配,然后利用它预测节点的值。
为了实现该方法,我们将需要几个额外的库,并创建一个名为 OrdinaryKriging 的类。
from scipy.linalg import solve
from itertools import product
from sklearn.metrics import mean_squared_error as MSE
class OrdinaryKriging:
def __init__(self, lats, lons, values):
self.lats = lats
self.lons= lons
self.values = values
self.nugget_values = [0, 1, 2, 3, 4]
self.sill_values = [1, 2, 3, 4, 5]
self.range_values = [1, 2, 3, 4, 5]
# Generate all combinations of parameter values to fit
self.parameter_combinations = list(product(self.nugget_values, self.sill_values, self.range_values))
self.optimal_pars = None
def theoretical_variogram(self, h, nugget, sill, r):
return nugget + (sill-nugget) * (1-np.exp(-3*h/r))
def Euclidean(self, X, Y):
all_dists, point_dists = [], []
for x,y in zip(X, Y):
k = 0
for k in range(len(X)):
h = np.linalg.norm(np.array([x, y]) - np.array([X[k], Y[k]]))
point_dists.append(h)
all_dists.append(point_dists)
point_dists = []
return all_dists
def gamma(self):
distances = self.Euclidean(self.lats, self.lons)
differences = np.abs(self.values.reshape(-1,1) - self.values)
variogram_values = []
for h in np.unique(distances):
values_at_h = differences[(distances == h)]
variogram_values.append(np.mean(values_at_h**2))
return variogram_values, np.unique(distances)
def fit(self):
experimental_variogram, distances = self.gamma()
fit_metrics = []
for nugget, sill, range_ in self.parameter_combinations:
theoretical_variogram_values = self.theoretical_variogram(distances, nugget, sill, range_)
fit_metric = MSE(experimental_variogram, theoretical_variogram_values)
fit_metrics.append((nugget, sill, range_, fit_metric))
self.optimal_pars = min(fit_metrics, key=lambda x: x[3])[:3]
def predict(self, point):
points = np.array([(x,y) for x,y in zip(self.lats, self.lons)])
distances = np.linalg.norm(points - point, axis=1)
pars = list(self.optimal_pars)
pars.insert(0, distances)
weights = self.theoretical_variogram(*pars)
weights /= np.sum(weights)
return np.dot(weights, self.values)
kriging = OrdinaryKriging(df.LATITUDE.values, df.LONGITUDE.values, df.TAVG.values)
kriging.fit()
现在让我们分别考虑每个函数。
init 函数除了初始化坐标和值外,还包括三个列表,分别包含 nugget、sill 和 range 的可能值。这三者被混合在一起,形成所有可能的组合,并存储在 parameter_combinations 变量中。稍后我们将用它来搜索最优值。
def __init__(self, lats, lons, values):
self.lats = lats
self.lons= lons
self.values = values
self.nugget_values = [0, 1, 2, 3, 4]
self.sill_values = [1, 2, 3, 4, 5]
self.range_values = [1, 2, 3, 4, 5]
# Generate all combinations of parameter values to fit
self.parameter_combinations = list(product(self.nugget_values, self.sill_values, self.range_values))
self.optimal_pars = None
第二个函数,theoretical_variogram,只是上述公式之一的 Python 实现。在我们的例子中,它是 指数型(但你可以为其他公式编写代码并进行比较):
def theoretical_variogram(self, h, nugget, sill, r):
return nugget + (sill-nugget) * (1-np.exp(-3*h/r))
第三个类方法 欧几里得。这是我们为 NN 和 IDW 创建的函数的修改版。这次我们返回一个矩阵 (n,n),其中每一行表示一个点与所有其他点之间的距离(每行中的一个值为 0,因为一个点与它自身的距离为 0)。
def Euclidean(self, X, Y):
all_dists, point_dists = [], []
for x,y in zip(X, Y):
k = 0
for k in range(len(X)):
h = np.linalg.norm(np.array([x, y]) - np.array([X[k], Y[k]]))
point_dists.append(h)
all_dists.append(point_dists)
point_dists = []
return all_dists
第四个函数执行 拟合。在这里,它获取实验变差值和欧几里得距离。然后,通过迭代我们关于 sill、range 和 nugget 的组合,计算理论变差值,并进行理论值与实验值之间的均方误差(MSE)估计(当然你也可以使用其他度量标准)。然后,我们将最优参数保存到类变量 optimal_pars 中。
def fit(self):
experimental_variogram, distances = self.gamma()
fit_metrics = []
for nugget, sill, range_ in self.parameter_combinations:
theoretical_variogram_values = self.theoretical_variogram(distances, nugget, sill, range_)
fit_metric = MSE(experimental_variogram, theoretical_variogram_values)
fit_metrics.append((nugget, sill, range_, fit_metric))
self.optimal_pars = min(fit_metrics, key=lambda x: x[3])[:3]
最后一个函数是 预测。它接收一个点(lat;lon)作为输入,估算该点与其他已知点之间的距离。接着,它调用 theoretical_variogram 函数,传入我们之前获得的最优参数,并输出权重。然后,计算加权均值并返回。
def predict(self, point):
points = np.array([(x,y) for x,y in zip(self.lats, self.lons)])
distances = np.linalg.norm(points - point, axis=1)
pars = list(self.optimal_pars)
pars.insert(0, distances)
weights = self.theoretical_variogram(*pars)
weights /= np.sum(weights)
return np.dot(weights, self.values)
现在我们可以收集所有预测并可视化地图:
row, grid = [], []
for lat in LAT:
for lon in LON:
row.append(kriging.predict(np.array([lat, lon])))
grid.append(row)
row=[]
ds = xr.Dataset(
{'TAVG': (['lat', 'lon'], grid)},
coords={'lat': LAT, 'lon': LON})
fig, ax = plt.subplots(subplot_kw=dict(projection=ccrs.PlateCarree()), figsize=(16, 9))
ds.where(rg).TAVG.plot(ax=ax, alpha=0.6)
gdf.plot(ax=ax, color='r', markersize=85)
ax.gridlines(draw_labels=True,linewidth=2, color='black', alpha=0.5, linestyle='--')
plt.show()

图片来源:作者。
如你所见,结果与 IDW 得到的结果有很大不同。对于克里金插值法(Kriging),最重要的参数是你选择的理论变差模型,因为它基本上定义了预测值与距离之间的关系。如果你不想修改我提供的代码或你自己的代码,你可以探索 PyKrige 库,它实现了许多变差模型。
希望这篇文章对你有帮助,提供了有价值的见解!
===========================================
我在 Medium 上的所有出版物都是免费的,公开访问的,所以如果你在这里关注我,我将非常感激!
P.s. 我对(地理)数据科学、机器学习/人工智能以及气候变化充满热情。所以如果你想一起合作某个项目,请通过 LinkedIn 联系我。
🛰️关注我以获取更多更新🛰️
弥合工程与分析之间鸿沟的 3 个最佳实践
分配代码所有者、聘请分析工程师并创建飞轮效应
·发表于Towards Data Science ·6 分钟阅读·2024 年 4 月 19 日
--

图片来自Alex Radelich在Unsplash上的分享
作为一名分析工程师,我面临的最具挑战性的问题之一是弥合工程与分析之间的鸿沟。工程和分析往往各自为政,各自形成独立的团队,这使得跨团队协作变得相当困难。
工程团队推动的软件和数据变更往往是分析团队无法预见的。分析团队随后不得不调整工作,以适应这些变化。更糟的是,分析团队可能需要建议一个变更,而工程团队必须将其纳入本已紧张的日程安排中。
如果团队之间的鸿沟过于宽广,会增加大量时间、努力和数据质量问题,使本已堆积的工作更加繁重。这就是为什么从一开始就实施最佳沟通实践至关重要,一旦发现这一问题就应立即采取行动。
在本文中,我将讨论一些我所面临的最常见问题以及你可以应用的最佳实践来解决这些问题。
缺乏代码和测试的所有权
通常,代码中构建的逻辑缺乏所有权,尤其是在分析和工程之间。谁拥有生成代码的权利……
2025 年数据科学职业发展的 3 项必备商业技能
数据科学
包括如何培养这些技能的资源
·发布于 Towards Data Science ·7 分钟阅读·2024 年 12 月 11 日
--

图片来源:作者。使用 Midjourney 创建
如果你已经做了数据科学家一段时间,迟早你会发现,你的日常工作已经从一个喜爱 VSCode、阅读研究论文、提交 git 版本的数据科学家,转变为一个推动协作、界定项目范围、管理利益相关者、制定战略的人。
这一转变将是渐进的,几乎不易察觉,但它将要求你戴上不同的帽子,以确保数据项目的进展和影响力。此时,你将开始意识到除了日常的数据科学技能外,磨炼一些商业技能的必要性。这也将是你准备向更高层次的技术领导职位迈进的一个良好信号,比如首席、负责人或资深数据科学家。
以下是我个人的三大推荐,这些技能在我担任 FTSE 100 公司数据科学领导职务时非常有用,同时在初创企业环境中同样适用。
1. 精通金融
了解一家公司如何赚钱无论公司大小以及你在其中的角色如何,都至关重要。不幸的是,很多数据科学工作常常发生在孤岛中,问题陈述或假设,或者分析流程通常是自上而下的,可能与公司的财务目标缺乏直接的对接。
当你在团队中担任更高级的领导角色时,掌握商业语言至关重要。了解像资本支出(CapEx)与运营支出(OpEx)、EBITDA 利润率、摊销、综合客户获取成本(CAC)、客户流失群体、市场公平份额等术语有助于你与高层沟通结果。这样,你可以根据数据科学驱动的举措如何影响这些领域,定制你的洞察,使你的分析对财务相关人员更加相关和具有说服力。
你知道吗,苹果公司在 2024 年花费了 1100 亿美元进行股票回购?为什么?市场上的流通股减少了 = 每股收益(EPS)提高,从而推动了股价上涨。
了解你的数据能为你和公司带来好处:了解你的数据意味着你知道什么对业务有益,什么无效,能够识别增长的领域,并基于数据做出明智的财务决策。例如,除了仅仅展示模型精度的提升,还可以展示预测结果如何直接影响公司的最终盈利。
同样,通过展示你的工作如何直接贡献于公司的财务成功,你甚至可以为自己争取更好的薪酬!
但它不仅仅是沟通。这些知识为许多数据科学家错失的机会打开了大门。例如,有些方案允许你为公司与研发活动相关的资本支出(如专利相关费用、专业软件许可费等)申请税收返还。
我见过一些团队,通过理解这些财务机制,并将他们的机器学习基础设施投资定位为研发项目,从而成功获得了资金。
同样,根据你所在的行业,也有一些你或你的公司可能符合条件的政府补助。例如,美国农业部(USDA)为农业技术创新项目提供补助和资金。
如何培养这项技能?
-
阅读财务方面的书籍,快速掌握关键术语,并学习来自同行业其他公司的案例研究(最坏的情况——你要么快速失败,要么在最佳情况下——你会了解应该避免的常见陷阱)。
如果你没有时间从头到尾阅读书籍,至少要熟悉它们的关键理念。我使用AcceleratEd来获取书籍摘要,但也有其他选择,你可以从中挑选,我在这篇文章中讨论了这些选项。
-
从 YT 频道如TheFinanceStoryteller和Investopedia获取内容,这些频道将复杂的金融话题拆解成易于理解的小块。
-
留意适用于你业务的奖学金和资助机会。
-
跟随你的 COO、运营经理,或者在某些情况下,甚至是你的 PO(我的 PO 在帮助我理解医疗行业中的价值计算以及提升我对公司财务的理解方面,帮助非常大)。
2. 跟上趋势
无论你喜欢还是讨厌,但你不能否认 AI/ML/生成 AI 领域正在以前所未有的速度发展。我经常看到新闻报道说技术 X 取代了技术 Y,而我不禁想——那技术 Y 到底是什么!
平均每个月,大约有 8000 篇新的研究论文(计算机科学类)在 arXiv 上发表![来源]
要在这一新角色中提供任何形式的思想领导力,你的行业和技术意识需要在两个层面上运作——本地和全球。
跟上本地****趋势意味着要时刻了解最新的工具、技术和趋势。从实际角度来说,这意味着你需要知道(a)哪些模型在你的使用场景中位于排行榜的前列(无论是预测、生成 AI 还是计算机视觉),(b)哪些新兴的突破性框架可能会成为行业的变革者(例如,百度最近推出的iRAG 技术,解决了图像生成中的幻觉问题),以及(c)DevOps/LLMOps/MLOps 方面的进展,这些都能帮助简化工作流程并提高效率。
跟上全球****趋势意味着要了解科技领域的更大格局——理解创新如何塑造行业,以及这些技术对社会和伦理的广泛影响——尤其是当世界各国政府开始采取措施来规范科技领域时。
从实际角度来看,这可能意味着要时刻了解你所在领域(法律、医疗、快速消费品等)的最新法规,并检查是否符合相关的指导方针。
例如,最近生效的欧盟 AI 法案 2024对 AI 的开发、部署和使用提出了详细的指导方针,包括强制要求 AI 生成内容加水印等指导原则。
同样,跟踪大科技公司如 NVIDIA、OpenAI、Anthropic 等的动态,对于预测业务的短期和长期技术变革尤为重要。一个短期的例子是最近OpenAI 与微软的合作关系破裂的新闻,这可能影响任何依赖微软 Azure OpenAI 作为 LLM 供应商的正在进行的项目。
一个长期的例子是近期微软、亚马逊和谷歌等公司对核能项目的投资,以应对大型语言模型(LLM)对高能耗的日益增长需求,这一需求通常被视为 AI 进展的瓶颈。稳定、可预测且零碳的能源来源可能意味着为你的 AI 驱动型业务带来长期的成本节约。
如何培养这项技能?
-
每天通过应用程序(如Curio)或网站(如HackerNews)获取一剂科技新闻。
-
订阅几份每周的 AI 新闻通讯,或者根据你的工作量,尽可能多地订阅一些你能保持跟进的通讯。我是一个高度自觉的人,我唯一依赖的是The Batch。
3. 成为一名沟通高手
对于那些从数据科学家晋升为新领导角色的幸运少数人来说,软性沟通技巧——对于管理团队、数据讲故事和跨团队合作至关重要——对他们来说是自然而然的。然而,对于其他人来说,还是有希望的!只要通过实践,任何技能都是可以实现的。
在你问为什么这一点至关重要之前,想象一下,如果你不知道如何向一群非技术背景的风投和投资者推销你的优秀数据产品,会是怎样的情景。或者如何有效地传达你一周来的 EDA 过程中的洞察。又或者在关键产品发布期间,如何正确地激励你那些聪明但有些不堪重负的数据科学家。
晋升到领导职位意味着要坚定但礼貌,清楚地解释团队需要做什么,并且在与利益相关者沟通时,要明确技术上的限制,阐明他们的要求和实际可行范围之间的差距——同时考虑到成本、延迟等限制因素。
这意味着,当一个利益相关者说“ChatGPT 几秒钟就能做到”时,或者有人要求“100%准确的模型”时,保持冷静。
为了有效地进行沟通,你需要学习不同的动态。你需要比起冲动反应,更加外交和理性,尤其是在有人建议“尝试那些在会议中提出的 20 个想法”,或者在你明显察觉到范围蔓延时,避免使用不恰当的口头和非语言提示。
如何培养这项技能?
-
同样,书籍在这里可以是你的好朋友。这是我的书籍收藏,专注于团队动态管理,包括像情商 2.0、团队的五大功能障碍和关键对话:在重大时刻沟通的工具、令人信服的沟通等书籍。我最近写了一篇文章,分享了这些书籍是如何对帮助我作为技术负责人保持理智有着极大帮助的。
-
(书籍只能带你走这么远,所以要主动)领导工作中的利益相关者会议。没有什么能比得上亲身实践的经验。
-
在会议和研讨会上志愿参加圆桌讨论和炉边谈话。这些形式比你是唯一演讲者而其他人被动听讲时更为轻松,也能减轻压力。用事实、书籍中的证据、最新新闻和有信誉的研究论文支持你的讨论点,以确保你的论点有分量。
最后的思考
从核心数据科学家的角色转变为被人仰望的思想领袖,负责定义、预见并塑造数据团队的战略方向,可能会让你感到害怕。
但你需要记住,作为一个既有技术又有商业技能的人,没有人比你更适合掌控局面。
你理解黑箱模型背后的技术,这让你更容易找到可行的技术解决方案,同时你也拥有商业眼光,能够将这些解决方案与商业目标最好的对接。
希望这篇文章对你有所帮助,如果机会来临,希望你能够毫不犹豫地抓住它。看到你的公司/团队/业务单元取得成功,并知道自己在他们成长中扮演了角色,真的是一种非常有价值的经历。
2024 年成为数据科学家的 3 大挑战
在当前的环境下,数据科学适合你吗?
·发布于Towards Data Science ·阅读时长 6 分钟·2024 年 6 月 27 日
--

图片来源:kaleb tapp来自Unsplash
现在是成为数据科学家的好时机。它是增长最快的职业之一,预计从2022 年到 2032 年的职位空缺将增长 35%。美国的平均薪资大约为 16 万美元(当然,这取决于经验)。该领域正在取得巨大进展,尤其是在人工智能领域,随着大型语言模型(LLMs)的出现,提供了许多新的任务和机会。
但是,所有这些优点背后也有一些独特的挑战,这些挑战是数据科学家目前正在面对的,并且在未来几年将继续面对。在你考虑进入这个领域之前,了解一些这些挑战非常重要,这样你才能评估这是否是适合你的职业道路。
1. 它是一个快速发展的领域
相较于金融和工程等其他领域,数据科学仍然是一个发展中的领域。数据科学的学位课程直到2012 年左右才开始广泛提供。即使在其他更加成熟的领域,随着现代技术和社会的不断发展,也会始终伴随变化。
新的工具、技术和语言每天都在开发中。Python 定期更新,其他相关技术也在不断发展……
构建涉及 API 的增量数据加载脚本时需要解决的 3 个关键问题
·发表于 Towards Data Science ·阅读时长 8 分钟·2024 年 6 月 30 日
--
简短说明
本文解释了使用 dlt(一个 Python 库)从 API 端点到数据库同步数据的概念框架和实际代码实现。
在本教程结束时,你将理解(并知道如何实现)以下示意图中的同步行为,在此示意图中,我们通过增量方式提取数据,并使用合并(去重)策略将数据写入目标表。

作者图片
背景
-
你希望将数据从一个应用程序(例如,广告效果、销售数据等)同步到你的数据库。
-
你的应用程序提供 API 端点来检索其数据。
-
数据需要每天同步到你的数据库。
-
你希望仅将 ❗“新”数据(或更改部分)❗ 加载到数据库中。你不希望每次同步时都重新加载整个数据集。
你如何在 Python 中实现这一点?
我将通过回答以下三个问题,带你了解解决方案。
💥 问题 1:我需要从 API 获取哪些信息以进行增量数据同步?
在开发增量加载脚本之前,我们需要了解我们正在使用的 API 端点的行为。
❗并不是所有的 API 都支持增量加载。
👉 答案:支持增量加载的查询参数
让我们来看一个应用程序的示例(或称为“源”应用程序),它跟踪你的销售业绩。在这个应用程序中,每条记录代表一个产品及其销售量。字段 created_at 和 updated_at 表示记录的创建和更新时间。
销售数据的变化通常有两种主要方式:
-
新产品已被添加到列表中。
-
更新会应用到现有记录的销售数据,这会导致
**updated_at**的新值。这样可以帮助我们追踪新的变化;如果没有它,我们就无法知道哪些记录已被修改。
👁️👁️ 下面是源应用数据库中的示例销售表。
↪️ 昨天的数据:2 条记录

图片由作者提供
↪️ 今天的数据:新增了一条记录,并且修改了现有记录

图片由作者提供
🟢 要点: 如果 API 端点允许基于 updated_at 参数进行查询,你可以通过发起请求来只检索那些 updated_at 值晚于上次同步保存的 updated_at 的记录,从而实现增量加载。在这种情况下,updated_at 被称为增量游标,其值会持续到下次同步,这个值称为状态。
updated_at 字段是增量游标的常见选择。其他查询参数,如 id 或 sales,无法帮助我们增量请求数据,因为它们无法告诉我们自上次同步以来,哪些记录已被添加或更新。
你需要哪个查询参数来增量加载数据?

图片由作者提供
由于我们正在开发一个与 API 配合使用的数据加载脚本,我将介绍两个 API 的其他重要方面,以便代码实现:分页和路径参数。不过,它们与增量加载无关。
🤷 分页机制
API 通常会分批返回结果,以提高性能。例如,API 可能不会一次性返回 10,000 条记录,而是将响应限制为每次最多 100 条记录,要求你迭代处理后续批次。
为了管理这个,你通常(但并非总是)需要使用两个查询参数:limit 和 skip(或 offset)。
这里有一个简单的例子来说明:
对于第一个请求:
-
limit=100 -
skip=0
对于第二个请求,要跳过我们已经同步的前 100 条记录:
-
limit=100 -
skip=100
这一模式将持续进行,每批次后,skip 值会按 limit 增加,直到所有记录都被检索。
🟢 要点: 你需要理解 API 如何返回响应,以免在提取数据时漏掉任何记录。API 有很多管理分页的方法,超出了常用的 skip 和 offset 方法。这是另一个话题,留待以后再说。
🤷 路径参数
路径参数直接包含在 API 的 URL 中,通常用于区分数据的不同部分(分区)。例如,它们可能指定你营销账户中的不同活动或源应用中管理的不同子账户。
在下面的例子中:路径参数是 applicationId 和 campaignId。
[yourbaseurl.myapp/v1/applications/{applicationId}/campaigns/{campaignId}/](https://yourbaseurl.talon.one/v1/applications/%7BapplicationId%7D/campaigns/%7BcampaignId%7D/coupons/no_total)sales
🟢 总结:你需要决定是将来自同一 API 但具有不同路径参数的数据同步到同一张表,还是同步到不同的表(如 sales_campaign_1, sales_campaign_2 等)。
💥 问题 2:我如何将提取的记录写入目标表?
现在,假设你已经通过上述参数提取了一批记录,是时候决定如何将这些记录写入目标表了。
👉 答案:合并/去重模式(推荐)
这个问题涉及到写入方式(Write disposition)或同步模式(Sync mode)的选择。直接的答案是,如果你希望增量加载数据,你可能会选择以追加模式或合并模式(也叫去重模式)将提取的数据写入。
然而,让我们退后一步,更仔细地审视我们的选择,并确定哪种方法最适合增量加载。
以下是常见的写入方式。
-
🟪 覆盖/替换(overwrite/replace):删除目标表中所有现有记录,然后插入提取的记录。
-
🟪 追加(append):简单地将提取的记录追加到目标表中。
-
🟪 合并 / 去重(merge / dedup):插入新(*)记录并更新(**)现有记录。
(*) 我们怎么知道哪些记录是新的?:通常,我们会使用主键来确定。如果使用 dlt,它的合并策略可以比这更复杂,包括区分merge_key和primary_key (一个用于合并,一个用于去重后再合并),或者dedup_sort (在去重过程中,哪些记录会被删除)。这一部分我将留到另一个教程讲解。
(**) 这是一个简单的解释,如果你想了解更多关于 dlt 如何处理这种合并策略的信息,可以点击这里阅读更多。
👁️👁️ 这里有一个例子帮助我们理解不同写入方式的结果。
↪️ 2024 年 6 月 19 日:我们进行了第一次同步。
🅰️ 数据在 **源应用程序**️️

图片由作者提供
🅱️ 数据已加载到我们的 **目标数据库**
无论选择哪种同步策略,目标表中的数据严格来说是源表的副本。

图片由作者提供
已保存的 updated_at 状态 = 2024-06-03,这是我们同步的两条记录中最新的 updated_at。
↪️ 2024 年 6 月 2 日:我们进行了第二次同步。
🅰️ ️️️️️️️数据在 **源应用程序**

图片由作者提供
✍️ 源表中的更改:
-
记录 ID=1 已更新(销售数据)。
-
记录 ID=2 已删除。
-
记录 ID=3 已插入。
在这次同步中,我们仅提取 updated_at > 2024–06–03(上次同步时保存的状态)之后的记录。因此,我们只会提取记录 id=1 和 id=3。由于记录 id=2 已从源数据中删除,我们无法识别此变动。
在第二次同步后,你将能够看到不同的写入策略之间的差异。
🅱️ 数据已加载到我们的 **目标数据库**
❗ 场景 1:覆盖

图片由作者提供
目标表将被这次提取的两条记录覆盖。
❗ 场景 2:追加

图片由作者提供
这两条提取的记录将被追加到目标表中,现有的记录不会受到影响。
❗ 场景 3:合并或去重

图片由作者提供
提取的两条记录,id=1 和 id=3,将替换目标中的现有记录。这一处理过程被称为合并或去重。目标表中的记录 id=2 将保持不变。
🟢 总结: 合并(去重)策略在增量数据加载管道中可能非常有效,但如果你的表非常大,去重过程可能需要相当长的时间。
💥 问题 3:我如何在代码中实现它?
👉 答案:dlt —— 因为它轻量、文档齐全,并且有活跃的社区支持。
dlt 是一个非常不错的选择,因为它提供了适当的抽象层次。事实上,你可以选择你需要的抽象程度。如你在我下面的示例代码中看到的,我主动编写了自己的请求循环,但 dlt 提供了可以用更少代码行完成的辅助函数。这种灵活性使得 dlt 相较于其他解决方案脱颖而出。
你可以参考这个图表来获得高层次的视图,然后再深入到带有详细备注的代码中。
小提示:dlt 在其结构中使用了 源 和 资源 这两个术语。资源通常对应一个 API 端点,并将数据写入目标数据库中的表。源是资源的集合。
在下面的插图中,你可以看到我们讨论的两个问题的答案:
-
问题 1 的答案:使用日期游标向 API 端点发送请求,逐步获取数据(并持久化游标值,也就是状态,以便后续执行)。
-
问题 2 的答案:使用合并策略将数据写入目标表。

现在,你可能想知道如何运行这个 Python 脚本?我建议你访问这个代码库,并亲自尝试一下。这个代码库还为你提供了一个模拟 API,你可以在本地部署进行测试。查看 README 获取详细的执行指南。
💥 这里是我附带备注的 dlt 实现片段 💥
🟣️ 完整代码可以在仓库中查看 点击这里🟣
✅ 本教程到此结束。希望您已经了解了构成增量加载脚本的不同组件,以及代码的实现方法。
如果您有兴趣了解如何使用 dlt 构建增量加载脚本,请查看他们的文档点击这里。
关于我
我是 Daniel Le,居住在柏林,目前从事数据工程和机器学习领域的工作。
我对新技术充满兴趣,尤其是它们如何应用于解决现实世界中的问题。
如果您有任何疑问或希望进一步讨论这些兴趣,欢迎通过LinkedIn与我联系。
参考文献
每个数据科学家必须了解的概率分布的 3 个关键概念
你知道 PMF、PDF 和 CDF 实际上是什么意思吗?它们是所有分布的核心。
·发布于 Towards Data Science ·5 分钟阅读·2024 年 5 月 28 日
--

图片由作者提供
动机
有许多实际事件,我们知道其确切的结果,例如 1+1=2,3x2=6,等等。还有许多不可能发生的事件,例如太阳从西方升起、牛在天空中飞等。在这两种情况下,我们对结果有 100%的把握。但对于不可预测的事件,例如明天早上是否会下雨,我们能有多大的把握呢?在这里,概率的必要性就体现出来了!
***[注意:*** [***如果你想执行包含的分布图,请访问笔记本***](https://deepnote.com/workspace/zubair-6f469541-5170-4b0d-a125-36e6a5c03769/project/Article-code-c3041ae8-e1f6-4485-999e-5738394b76e3/notebook/Probability%20PDF%20and%20CDF-c6c26d65d9e74be38914ecd8f55fcee3)***.]***
目录
-
概率 -
概率分布 -
离散概率分布和 PMF -
连续概率分布、PDF 和 CDF -
总结
概率
概率表示随机事件发生的可能性。 以抛硬币为例,如果你随机抛掷硬币,可能有两种结果——正面或反面。因此,如果抛掷硬币,结果是正面的概率为 50%,而…
机器学习的 3 种关键编码技术:适合初学者的指南,包含优缺点和 Python 代码示例
我们应该如何在标签编码、独热编码和目标编码之间做选择呢?
·发表于 Towards Data Science ·15 分钟阅读·2024 年 2 月 7 日
--
为什么我们需要编码?
在机器学习领域,大多数算法要求输入为数字形式,特别是在许多流行的 Python 框架中。例如,在 scikit-learn 中,线性回归和神经网络要求数字变量。这意味着我们需要将分类变量转换为数值变量,以便这些模型能够理解它们。然而,对于像基于树的模型,通常不需要这一过程。
今天,我很高兴为大家介绍三种基本的编码技术,它们是每个初学数据科学家的必备技能!另外,我在最后还附上了一个实用的小贴士,帮助你更好地理解这些技术的实际应用!除非特别说明,所有代码和图片均由作者创作。
标签编码 / 顺序编码
标签编码和顺序编码都涉及将整数分配给不同的类别。区别在于类别变量是否本身具有顺序。例如,“强烈同意”,“同意”,“中立”,“不同意”和“强烈不同意”是有序的,因为它们遵循特定的顺序。当一个变量没有这样的顺序时,我们使用标签编码。
让我们深入探讨标签编码。
我已经准备了一个合成数据集,包含数学考试成绩和学生最喜欢的科目。这个数据集旨在反映偏爱 STEM 科目的学生获得更高的分数。以下代码展示了数据集的合成过程。
import numpy as np
import pandas as pd
math_score = [60, 70, 80, 90]
favorite_subject = ["History", "English", "Science", "Math"]
std_deviation = 5
num_samples = 30
# Generate 30 samples with a normal distribution
scores = []
subjects = []
for i in range(4):
scores.extend(np.random.normal(math_score[i], std_deviation, num_samples))
subjects.extend([favorite_subject[i]]*num_samples)
data = {'Score': scores, 'Subject': subjects}
df_math = pd.DataFrame(data)
# Print the DataFrame
print(df_math.sample(frac=0.04))import numpy as np
import pandas as pd
import random
math_score = [60, 70, 80, 90]
favorite_subject = ["History", "English", "Science", "Math"]
std_deviation = 5 # Standard deviation in cm
num_samples = 30 # Number of samples
# Generate 30 samples with a normal distribution
scores = []
subjects = []
for i in range(4):
scores.extend(np.random.normal(math_score[i], std_deviation, num_samples))
subjects.extend([favorite_subject[i]]*num_samples)
data = {'Score': scores, 'Subject': subjects}
df_math = pd.DataFrame(data)
# Print the DataFrame
sampled_index = random.sample(range(len(df_math)), 5)
sampled = df_math.iloc[sampled_index]
print(sampled)

你会惊讶于数据编码是多么简单——只需一行代码!你可以传递一个字典,将主题名称和数字映射到 pandas 数据框的默认方法,如下所示。
# Simple way
df_math['Subject_num'] = df_math['Subject'].replace({'History': 0, 'Science': 1, 'English': 2, 'Math': 3})
print(df_math.iloc[sampled_index])

手动编码
但是,如果你处理的是大量的类别,或者你希望找到一种更直接的方法怎么办?这时,scikit-learn 库中的**LabelEncoder**函数就派上了用场。它根据类别的字母顺序自动编码。为了获得最佳体验,我建议使用 1.4.0 版本,它支持我们正在讨论的所有编码器。
# Scikit-learn
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
df_math["Subject_num_scikit"] = le.fit_transform(df_math[['Subject']])
print(df_math.iloc[sampled_index])

使用 scikit-learn 库进行编码
然而,有一个问题。考虑一下:我们的数据集并没有暗示喜欢的科目之间存在有序关系。例如,“历史”被编码为 0,但这并不意味着它比编码为 3 的“数学”更“低级”。类似地,“英语”和“科学”之间的数值差距小于“英语”和“历史”之间的差距,但这不一定反映它们之间的相对相似性。
这种编码方法也会影响某些算法的可解释性。例如,在线性回归中,每个系数表示预测变量变化 1 单位时,结果变量的预期变化。但是,当一个主题被数值编码时,我们该如何解释“单位变化”?让我们通过对我们的数据集进行线性回归来进行理解。
from sklearn.linear_model import LinearRegression
model = LinearRegression()
model.fit(df_math[["Subject_num"]], df_math[["Score"]])
coefficients = model.coef_
print("Coefficients:", coefficients)

我们如何解释这里的系数 8.26 呢?最直观的方式是,当标签变化 1 单位时,测试分数变化 8。但这对于从科学(编码为 1)到历史(编码为 2)并不完全正确,因为我合成的方式使得平均分分别为 80 和 70。所以,当我们标记每个类别的方式没有实际意义时,我们不应解释该系数!
现在,转到有序编码,我们将其应用于另一个合成数据集,这次专注于身高和学校类别。我已经调整这个数据集,以反映不同学校级别的平均身高:幼儿园 110 厘米,小学 140 厘米,依此类推。让我们看看结果如何。
import numpy as np
import pandas as pd
# Set the parameters
mean_height = [110, 140, 160, 175, 180] # Mean height in cm
grade = ["kindergarten", "elementary school", "middle school", "high school", "college"]
std_deviation = 5 # Standard deviation in cm
num_samples = 10 # Number of samples
# Generate 10 samples with a normal distribution
heights = []
grades = []
for i in range(5):
heights.extend(np.random.normal(mean_height[i], std_deviation, num_samples))
grades.extend([grade[i]]*10)
data = {'Grade': grades, 'Height': heights}
df = pd.DataFrame(data)
sampled_index = random.sample(range(len(df)), 5)
sampled = df.iloc[sampled_index]
print(sampled)

一部分合成的学校身高数据
scikit-learn 的预处理工具包中的**OrdinalEncoder**是处理有序变量的一个真正的宝藏。它非常直观,自动确定有序结构并相应地进行编码。如果你查看 encoder.categories_,你可以检查变量是如何被编码的。
from sklearn.preprocessing import OrdinalEncoder
encoder = OrdinalEncoder(categories=[grade])
df['Category'] = encoder.fit_transform(df[['Grade']])
print(encoder.categories_)
print(df.iloc[sampled_index])

编码完成后
对于序数类别变量,解释线性回归模型变得更加简单。编码反映了教育程度的数值顺序——教育程度越高,其对应的数值越大。
from sklearn.linear_model import LinearRegression
model = LinearRegression()
model.fit(df[["Category"]], df[["Height"]])
coefficients = model.coef_
print("Coefficients:", coefficients)
height_diff = [mean_height[i] - mean_height[i-1] for i in range(1, len(mean_height),1)]
print("Average Height Difference:", sum(height_diff)/len(height_diff))

模型揭示了一些直观的内容:学校类型的单位变化对应身高增加 17.5 厘米。考虑到我们的数据集,这完全合情合理!
那么,让我们用一个快速的总结来结束标签/序数编码:
优点:
-
简单性:用户友好,易于实现。
-
效率:这种方法对计算资源和内存的要求较低,仅生成一个新的数值特征。
-
适用于有序类别:在处理具有自然顺序的类别变量时,独热编码表现得尤为出色。
缺点:
-
隐含顺序:一个潜在的缺点是,它可能会引入一种本不存在的顺序,导致误解(比如认为标记为‘3’的类别优于标记为‘2’的类别)。
-
并非总是适用:某些算法,如线性回归或逻辑回归,可能会错误地将编码后的数值解释为具有序数意义。
独热编码
接下来,让我们深入探讨另一种解决可解释性问题的编码技术:独热编码。
标签编码的核心问题是,它为那些本身没有顺序的变量强加了一个序数结构,通过将类别替换为数值。独热编码通过为每个类别创建一个单独的列来解决这个问题。每个列包含二进制值,表示该行是否属于该类别。 这就像是将数据转换为更宽格式,对于熟悉这种概念的人来说。为了更清楚地说明这一点,我们来看一个使用math_score和subject数据的例子。**OneHotEncoder**来自 sklearn.preprocessing,非常适合这个任务。
from sklearn.preprocessing import OneHotEncoder
data = {'Score': scores, 'Subject': subjects}
df_math = pd.DataFrame(data)
y = df_math["Score"] # Target
x = df_math.drop('Score', axis=1)
# Define encoder
encoder = OneHotEncoder()
x_ohe = encoder.fit_transform(x)
print("Type:",type(x_ohe))
# Convert x_ohe to array so that it is more compatible
x_ohe = x_ohe.toarray()
print("Dimension:", x_ohe.shape)
# Convet back to pandas dataframe
x_ohe = pd.DataFrame(x_ohe, columns=encoder.get_feature_names_out())
df_math_ohe = pd.concat([y, x_ohe], axis=1)
sampled_ohe_idx = random.sample(range(len(df_math_ohe)), 5)
print(df_math_ohe.iloc[sampled_ohe_idx])

通过独热编码进行编码
现在,我们的数据集不再只有一个‘学科’列,而是为每个学科单独设置了列。这有效地消除了任何不必要的序数结构!不过,这个过程稍微复杂一些,我来解释一下。
与标签/序数编码一样,你首先需要定义编码器。但独热编码的输出有所不同:标签/序数编码返回一个 numpy 数组,而独热编码通常生成一个scipy.sparse._csr.csr_matrix。为了将其与 pandas 数据框架结合使用,你需要将其转换为数组。然后,创建一个包含这个数组的新数据框,并为其指定列名,这些列名可以通过编码器的get_feature_names_out()方法获取。或者,你也可以通过在定义编码器时设置sparse_output=False来直接获得 numpy 数组。
然而,在实际应用中,你不需要经历所有这些步骤。我将在讨论的最后向你展示使用**make_column_transformer**的更简化方法!
现在,让我们继续对我们的独热编码数据进行线性回归。这应该使得解释变得更容易,对吧?
model = LinearRegression()
model.fit(x_ohe, y)
coefficients = model.coef_
intercept = model.intercept_
print("Coefficients:", coefficients)
print(encoder.get_feature_names_out())
print("Intercept:",intercept)

每列的截距和系数
等等,为什么系数这么小,截距又这么大?到底出了什么问题?这个困惑是线性回归中的一个特定问题,叫做完美多重共线性。完美多重共线性发生在当线性回归模型中的一个变量可以通过其他变量完美预测时,独热编码中也会出现这种情况,因为如果其他类别都为零,一个类别就可以被推断出来。为了避免这个问题,我们可以通过设置OneHotEncoder(drop=”first”)来去除一个类别。让我们看看这一调整的影响。
encoder_with_drop = OneHotEncoder(drop="first")
x_ohe_drop = encoder_with_drop.fit_transform(x)
# if you don't sparse_output = False, you need to run the following to convert type
x_ohe_drop = x_ohe_drop.toarray()
x_ohe_drop = pd.DataFrame(x_ohe_drop, columns=encoder_with_drop.get_feature_names_out())
model = LinearRegression()
model.fit(x_ohe_drop, y)
coefficients = model.coef_
intercept = model.intercept_
print("Coefficients:", coefficients)
print(encoder_with_drop.get_feature_names_out())
print("Intercept:",intercept)

每列的截距和系数(去除一列)
在这里,英语列已经被去除,现在系数看起来更加合理了!而且,它们也更容易解释。当所有的独热编码列都为零(表示英语是最喜欢的科目)时,我们预测考试成绩大约为 71(与我们为英语定义的平均分数一致)。对于历史科目,成绩是 71 减去 11 等于 60,对于数学,成绩是 71 加上 19,以此类推。
然而,独热编码有一个显著的警告:它可能导致高维数据集,特别是当变量具有大量类别时。让我们考虑一个包含 1000 行数据集的情况,每一行代表一个具有各种特征的独特产品,其中一个类别跨越了 100 种不同类型。
# Define 1000 categories (for simplicity, these are just numbered)
categories = [f"Category_{i}" for i in range(1, 200)]
manufacturers = ["Manufacturer_A", "Manufacturer_B", "Manufacturer_C"]
satisfied = ["Satisfied", "Not Satisfied"]
n_rows = 1000
# Generate random data
data = {
"Product_ID": [f"Product_{i}" for i in range(n_rows)],
"Category": [random.choice(categories) for _ in range(n_rows)],
"Price": [round(random.uniform(10, 500), 2) for _ in range(n_rows)],
"Quality": [random.choice(satisfied) for _ in range(n_rows)],
"Manufacturer": [random.choice(manufacturers) for _ in range(n_rows)],
}
df = pd.DataFrame(data)
print("Dimension before one-hot encoding:",df.shape)
print(df.head())

产品的合成数据集
请注意,数据集的维度是 1000 行和 5 列。现在,让我们观察应用独热编码后的变化。
# Now do one-hot encoding
encoder = OneHotEncoder(sparse_output=False)
# Reshape the 'Category' column to a 2D array as required by the OneHotEncoder
category_array = df['Category'].values.reshape(-1, 1)
one_hot_encoded_array = encoder.fit_transform(category_array)
one_hot_encoded_df = pd.DataFrame(one_hot_encoded_array, columns=encoder.get_feature_names_out(['Category']))
encoded_df = pd.concat([df.drop('Category', axis=1), one_hot_encoded_df], axis=1)
print("Dimension after one-hot encoding:", encoded_df.shape)

维度显著增加了!
在应用独热编码后,我们的数据集维度膨胀到了 1000x201——比之前大了整整 40 倍。这一增长是个问题,因为它需要更多的内存。而且,你会注意到新创建的列中的大多数值都是零,这就导致了我们所谓的稀疏数据集。某些模型,特别是基于树的模型,处理稀疏数据时会遇到困难。此外,处理高维数据时会出现其他挑战,通常被称为“维度灾难”。另外,由于独热编码将每个类别当作一个独立的列,我们会失去任何顺序信息。因此,如果你的变量中的类别本身具有层次顺序,独热编码可能不是最佳选择。
我们如何应对这些缺点?一种方法是使用不同的编码方式。或者,你可以限制变量的类别数量。通常,即使类别数量很多,变量的大多数值也集中在少数几个类别中。在这种情况下,将这些少数类别视为“其他”类别可能会很有效。这可以通过设置如**min_frequency**或**max_categories**之类的参数,在 OneHotEncoder 中实现。另一种应对稀疏数据的策略是使用特征哈希技术,本质上是通过哈希函数将多个类别映射到一个低维空间,或者使用 PCA 等降维技术。
这是独热编码的简要总结:
优点:
-
防止误导性解释:避免模型将数据误解为有某种顺序,这是标签/目标编码中常见的问题。
-
适用于非序数特征:非常适合没有序数关系的类别数据。
缺点:
-
维度增加:会显著增加数据集的维度,这在变量类别较多时尤为成问题。
-
稀疏矩阵:会导致许多列填充零,生成稀疏数据。
-
高基数特征效率低:对于类别数较多的变量效果较差。
目标编码
现在,让我们探索目标编码,这是一种在高基数数据和树模型等算法中特别有效的技术。
目标编码的本质是利用因变量的值信息。其实现方式根据任务的不同而有所变化。在回归任务中,我们通过每个类别的因变量均值来编码目标变量。在二分类任务中,编码是通过目标变量所属类别的概率来进行的(该概率通过该类别中结果为 1 的行数与该类别总行数之比计算得到)。在多分类任务中,类别变量是基于所属每个类别的概率进行编码的,结果会产生与因变量中的类别数量相等的新列。为了更清楚地说明这一点,让我们使用与独热编码相同的商品数据集。
让我们从回归任务中的目标编码开始。假设我们想要预测商品价格,并希望对商品类型进行编码。与其他编码方式类似,我们使用来自 sklearn.preprocessing 的TargetEncoder!
from sklearn.preprocessing import TargetEncoder
x = df.drop(["Price"], axis=1)
x_need_encode = df["Category"].to_frame()
y = df["Price"]
# Define encoder
encoder = TargetEncoder()
x_encoded = encoder.fit_transform(x_need_encode, y)
# Encoder with 0 smoothing
encoder_no_smooth = TargetEncoder(smooth=0)
x_encoded_no_smooth = encoder_no_smooth.fit_transform(x_need_encode, y)
x_encoded = pd.DataFrame(x_encoded, columns=["encoded_category"])
data_target = pd.concat([x, x_encoded], axis=1)
print("Dimension before encoding:", df.shape)
print("Dimension after encoding:", data_target.shape)
print("---------")
print("Encoding")
print(encoder.encodings_[0][:5])
print(encoder.categories_[0][:5])
print(" ")
print("Encoding with no smooth")
print(encoder_no_smooth.encodings_[0][:5])
print(encoder_no_smooth.categories_[0][:5])
print("---------")
print("Mean by Category")
print(df.groupby("Category").mean("Price").head())
print("---------")
print("dataset:")
print(data_target.head())

回归任务中的目标编码
编码后,你会注意到,尽管变量有许多类别,数据集的维度保持不变(1000 x 5)。你还可以观察每个类别是如何编码的。尽管我提到每个类别的编码是基于该类别的目标变量的均值,但你会发现,使用默认设置进行编码时,实际均值与编码结果略有不同。这种差异产生的原因是,默认情况下,函数会自动选择一个平滑参数。该参数将本地类别的均值与全局均值相结合,这对于防止样本量较少的类别过拟合非常有用。如果我们设置smooth=0,编码值将与实际均值完全一致。
现在,让我们考虑二分类问题。假设我们的目标是判断产品的质量是否令人满意。在这种情况下,编码值表示该类别是“满意”的概率。
x = df.drop(["Quality"], axis=1)
x_need_encode = df["Category"].to_frame()
y = df["Quality"]
# Define encoder
encoder = TargetEncoder()
x_encoded = encoder.fit_transform(x_need_encode, y)
x_encoded = pd.DataFrame(x_encoded, columns=["encoded_category"])
data_target = pd.concat([x, x_encoded], axis=1)
print("Dimension:", data_target.shape)
print("---------")
print("Encoding")
print(encoder.encodings_[0][:5])
print(encoder.categories_[0][:5])
print("---------")
print(encoder.classes_)
print("---------")
print("dataset:")
print(data_target.head())

二分类的目标编码
你确实可以看到,encoded_category表示的是“满意”的概率(介于 0 和 1 之间的浮动值)。要查看每个类别是如何编码的,你可以查看编码器的classes_属性。对于二分类问题,列表中的第一个值通常会被丢弃,这意味着此列表示的是满意的概率。方便的是,编码器会自动检测任务类型,因此无需指定它是一个二分类任务。
最后,我们来看一个多类分类的例子。假设我们正在预测哪家制造商生产了某个产品。
x = df.drop(["Manufacturer"], axis=1)
x_need_encode = df["Category"].to_frame()
y = df["Manufacturer"]
# Define encoder
encoder = TargetEncoder()
x_encoded = encoder.fit_transform(x_need_encode, y)
x_encoded = pd.DataFrame(x_encoded, columns=encoder.classes_)
data_target = pd.concat([x, x_encoded], axis=1)
print("Dimension:", data_target.shape)
print("---------")
print("Encoding")
print(encoder.encodings_[0][:5])
print(encoder.categories_[0][:5])
print("---------")
print("dataset:")
print(data_target.head())

多类分类的目标编码
编码后,你会看到现在我们有了每个制造商的列。这些列表示一个产品属于某个类别并由该制造商生产的概率。尽管我们的数据集略有扩展,但因变量的类别数通常较少,因此不太可能引起问题。
目标编码对于基于树的模型尤其有利。这些模型根据特征值进行划分,目的是最有效地分隔目标变量。通过直接结合目标变量的均值,目标编码为模型提供了一种清晰且高效的划分方式,通常比其他编码方法更有效。
然而,使用目标编码时需要谨慎。如果某个类别的观测样本很少,而且这些样本不能代表该类别的真实均值,就有可能导致过拟合。
这引出了另一个关键点:在将数据拆分为训练集和测试集之后执行目标编码是至关重要的。如果提前进行编码,可能会导致数据泄漏,因为编码会受到测试数据集中的结果的影响。这可能导致模型在训练数据集上表现得非常好,给您一个虚假的效果评价。因此,为了准确评估模型的性能,确保在训练集和测试集拆分后再进行目标编码。
这里是关于目标编码的简要总结:
优点:
-
控制基数:对于高基数特征非常有效,因为它不会增加特征空间。
-
能够捕获标签中的信息:通过引入目标数据,它通常能够提高预测性能。
-
对树模型有用:特别适用于复杂模型,如随机森林或梯度提升机。
缺点:
-
过拟合风险:特别是当类别的观察数据量有限时,过拟合的风险会增加。
-
目标泄漏:它可能无意中将未来的信息引入模型,即目标变量中的细节,这些细节在实际预测中是无法访问的。
-
可解释性较差:由于转换是基于目标的,与独热编码或标签编码等方法相比,它们可能更难解释。
最后的提示
总结一下,我想提供一些实用的建议。在整个讨论中,我们已经探讨了不同的编码技术,但实际上,您可能希望对数据集中的不同变量应用不同的编码。这时候,**make_column_transformer**来自 sklearn.compose 就非常有用了。例如,假设您正在预测产品价格,并决定对“类别”使用目标编码,因为其基数较高,而对“制造商”和“质量”使用独热编码。为此,您可以定义包含每种编码类型变量名称的数组,并像下面这样应用该函数。这个方法使得您能够无缝地处理转换后的数据,从而得到一个高效编码的数据集,准备好进行分析!
from sklearn.compose import make_column_transformer
ohe_cols = ["Manufacturer"]
te_cols = ["Category", "Quality"]
encoding = make_column_transformer(
(OneHotEncoder(), ohe_cols),
(TargetEncoder(), te_cols)
)
x = df.drop(["Price"], axis=1)
y = df["Price"]
# Fit the transformer
x_encoded = encoding.fit_transform(x, y)
x_encoded = pd.DataFrame(x_encoded, columns=encoding.get_feature_names_out())
x_rest = x.drop(ohe_cols+te_cols, axis=1)
print(pd.concat([x_rest, x_encoded],axis=1).head())

使用 make_column_faster 结合目标编码和独热编码
非常感谢您抽出时间阅读这篇文章!当我第一次开始我的机器学习旅程时,选择正确的编码技术并理解它们的实现对我来说是一件相当复杂的事。我真诚地希望这篇文章能为您提供一些启示,让您的路径变得更加清晰!
来源:
Scikit-learn: Python 中的机器学习,Pedregosa 等人,JMLR 12,页 2825–2830,2011。
Scikit-learn 文档:
使你的 Matplotlib 图表达到出版水平的 3 个关键调整
Matplotlib 图表默认看起来很难看——这是该怎么解决。
·发表于 Towards Data Science ·阅读时间 5 分钟·2024 年 8 月 8 日
--

文章缩略图(作者提供的图片)
数据可视化比直接查看原始数值数据提供了更深刻的见解。
然而,创建吸引人的图表需要时间和精力。Matplotlib 是 Python 数据可视化的事实标准库。它简单,已经使用了几十年,任何你想要的功能只需一次网络搜索就能找到。
但并非一切都那么美好和顺利。Matplotlib 的默认可视化效果非常糟糕,作为数据专业人士,你将需要调整许多参数才能得到可用的效果。今天的文章目标就是帮助你做到这一点。
到最后,你将拥有一段可以粘贴到任何 Jupyter Notebook 的代码片段。
Matplotlib 默认样式的问题是什么?
为了跟随本教程,你无需下载任何数据集。你将创建一个合成的时间序列数据集,具有上升趋势和可重复的季节性模式:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# Single season multiplier factors - for seasonality effect
seasonal_multipliers = [1.1, 1.3, 1.2, 1.5, 1.9, 2.3, 2.1, 2.8, 2.0, 1.7, 1.5, 1.2]…
作为一名初级数据科学家,我犯过的 3 个痛苦的错误
今天从这些教训中学习,加速你的职业发展
·发布于数据科学前沿 ·6 分钟阅读·2024 年 6 月 21 日
--

图片来源:Kenny Eliason 于Unsplash
成为数据科学家不仅仅是处理数字——更是要在一片充满潜在误区的雷区中航行。
在我的旅程中,我犯了一些错误,这些错误不仅痛苦,而且极具启发性。以下是我犯的三个重大错误,希望你们能避免走我曾经走过的弯路。
你好!
我是Mandy Liu,前 Meta 数据科学家 😃
我学习了经济学,开始我的咨询职业,并最终转型进入数据科学和个人创业的迷人世界。
我每周发布通讯,内容涉及数据科学、人工智能、商业和创业。一定要订阅!⬇️
[## 好奇的技术人 | Mandy Liu | Substack
每周由前 FAANG 数据科学家提供的见解,内容涵盖:数据科学、人工智能、商业和个人发展。点击阅读全文…
mandyliu.substack.com](https://mandyliu.substack.com/?source=post_page-----df06b9513b47--------------------------------)
1. 隐藏自己的弱点
我曾经相信,要成为完美的数据科学家,必须在所有领域都表现出色——不仅要精通 A/B 测试,还要了解最新的机器学习模型。
3 个强大的 SQL 查询,帮助你处理日期时间数据
数据科学
使用这些技巧进行有效的趋势分析,并获得推动决策的洞察。
·发布于 Towards Data Science ·阅读时长 9 分钟·2024 年 7 月 12 日
--

尼泊尔喜马拉雅山小屋,巴伐利亚,德国 | 作者拍摄的照片
处理日期时间值 — 数据分析的一个重要部分。
每当你在数据中看到日期时间值时,把它看作是充满信息的宝藏。至少,我是这样看的。
嗯,要获得对你有用的那部分“宝藏”从来都不容易,这也是本文灵感的来源。
是的!我将给你 3 个 SQL 查询,你可以用它们从数据中的日期时间值中获得最大收益。你可以利用它们从数据中获取有价值的洞察,支持有效的决策制定。
如果你使用 Python 进行数据处理和分析,我在下面的两篇文章中已经解释了类似的实用技巧,快来看看吧 —
-
3 个强大的技巧,帮助你处理 Python 中的日期时间数据
-
3 个实用的 Pandas 技巧,帮助你处理日期时间数据
回到我们的主题,接下来让我告诉你如何使用 SQL 从日期时间数据中获得最大洞察。
这里是你将在本文中探索的内容的快速概述。
应对音乐 AI 中数据匮乏的三大实用技巧
从你的音乐数据中获取更多
·发布于Towards Data Science ·阅读时长 11 分钟·2024 年 5 月 22 日
--

标题图像由作者使用 DALL-E 2 生成。
标注音频数据在音乐 AI 中长期匮乏。在这篇文章中,我将分享一些在这种情况下构建强大模型的技巧。
与计算机视觉或自然语言处理(NLP)等其他领域相比,寻找适合音乐 AI 的公共数据集通常是困难的。无论你是想进行情绪识别、噪音检测还是乐器标注,你都可能会在寻找合适的数据时遇到困难。
然而,数据匮乏不仅影响到业余程序员和学生。渴望发展的音乐技术初创公司甚至一些已建立的音乐公司也面临同样的问题。在 AI 时代,许多人急切地尝试收集专有数据资源以用于机器学习。
话虽如此,让我们深入探讨一下我的从音乐数据中获取更多信息的三大技巧。
提示 1:应用自然数据增强

横幅由作者使用 DALL-E 2 生成。
如果你是一名数据科学家,你可能听说过数据增强。基本的想法是从我们现有的数据集中获取现有样本,并稍微改变它们来生成新的合成训练样本。这个概念最容易通过图像来说明。例如,如果我们的数据集中包含一张猫的图片,我们可以通过平移和旋转原始猫的图片,轻松生成新的合成猫图像。

图像数据增强示例。图像灵感来源于 Suki Lau,并由作者使用Alexander London拍摄的猫的照片重新创作。
数据增强特别对较小的数据集有效。如果你的数据集只有 100 张猫的照片,那么所有可能的角度和旋转正确表示的几率就很低。这些数据集中的盲点将自动转化为 AI 感知和判断中的盲点。通过合成创建现有图像的变化,我们可以减轻这种风险。
数据增强在音乐 AI 中的不同之处
虽然数据增强在计算机视觉中是一个游戏规则改变者,但在音乐 AI 中,它不那么直接。音乐 AI 模型的最常见输入是声谱图(了解更多这里)。但你尝试过旋转和移动一个声谱图吗?

无效的音频数据增强示例。图像来自作者。
很容易看出,计算机视觉中的相同技巧不能直接应用于音乐 AI。但为什么这个例子如此荒谬呢?答案是,与猫的例子相比,这种增强对于声谱图来说是不自然的。
当所做的变化代表了模型在实际应用中可能遇到的变化时,数据增强是自然的。虽然旋转一个声谱图显然会改变数据,但在视觉上它毫无意义,且在实际中永远不会发生。相反,我们需要为音乐数据找到特别的自然变化。
使用效果进行自然音频增强
最常见的自然音乐数据增强方法是对音频信号应用效果。每个音乐制作人都知道一堆来自数字音频工作站(DAW)的效果:
-
时间拉伸
-
音高移位
-
压缩器、限制器、失真
-
混响、回声、合唱
-
以及更多…
这些效果可以应用于任何音乐作品,改变数据同时保持其主要的音乐特征。如果你想知道如何在实践中实现这一点,查看我关于这个话题的文章:
## 使用 Spotify 的 Pedalboard 进行自然音频数据增强
提供现成的 Python 代码和预设
towardsdatascience.com
数据增强不仅在许多音乐 AI 研究论文中得到应用,我自己也取得了很好的效果。当数据稀缺时,数据增强可以让你的模型从不可用转变为可接受。即使数据量较大,它仍能增加一些额外的可靠性,这在生产环境中可能至关重要。
在实际实施音乐数据增强时,重要的是要牢记以下三点:
-
保持自然。 增强后听一下你的数据,确保它听起来仍然自然。否则,模型可能会学习到错误的模式。
-
并非每个训练样本都应该被增强。 为了确保你的模型主要从真实的、未被修改的音乐中学习,增强样本应仅占你训练数据的一部分(20%–30%)。你还可以在训练时使用样本加权来调整增强样本对模型的影响。
-
不要增强你的验证和测试数据。 增强可以帮助模型学习通用的模式。你的验证和测试数据应该保持未修改,以便能够在真实示例上进行准确的基准测试。
是时候通过数据增强提升你的模型效果了!
提示 2:使用更小的模型和输入数据

横幅由作者使用 DALL-E 2 生成。
更大 = 更好?
在 AI 中,更大通常意味着更好——前提是有足够的数据来训练这些大型模型。然而,数据有限时,更大的模型更容易发生过拟合。过拟合发生在模型记住了训练数据中的某些模式,但这些模式无法很好地推广到真实世界中的数据示例。但在这个背景下,我发现还有另一种方法值得考虑,这种方法甚至更有说服力。
假设你有一个小型的谱图数据集,并在选择一个小的 CNN 模型(10 万参数)和一个大的 CNN 模型(1000 万参数)之间做决定。记住,每个模型参数实际上都是从训练数据集中得出的最佳猜测值。如果我们这么想,显然让一个模型准确地获得 10 万个参数比准确地获得 1000 万个参数更容易。
最终,两个观点得出的结论是相同的:
如果数据稀缺,考虑构建专注于基本模式的小型模型。
那么,我们如何在实践中实现更小的模型呢?
不要用大锤砸胡桃
我的音乐 AI 学习之旅一直被深度学习主导。直到一年前,我几乎用大型神经网络解决了所有问题。虽然对于复杂的任务,如音乐标签或乐器识别,这样做是有道理的,并不是所有任务都那么复杂。
例如,一个不错的 BPM 估计器或调性检测器可以通过分析音符的开始时间,或者通过将色谱图与调性配置文件进行关联来构建,而不需要任何机器学习。
即使是像音乐标签这样的任务,也不一定非得使用深度学习模型。我通过一个简单的 K-近邻分类器在嵌入空间(例如 CLAP)上取得了很好的情绪标签结果。
尽管音乐 AI 中大多数最先进的方法都是基于深度学习的,在数据稀缺的情况下,应该考虑替代解决方案。
注意数据输入大小
比模型选择更重要的通常是输入数据的选择。在音乐 AI 中,我们很少使用原始波形作为输入,因为它们在数据效率上较低。通过将波形转换成(梅尔)声谱图,我们可以将输入数据的维度减少 100 倍以上。这很重要,因为大规模的数据输入通常需要更大和/或更复杂的模型来处理。
为了最小化模型输入的大小,我们可以采取两条路线
-
使用更小的音乐片段
-
使用更压缩/简化的音乐表示。
使用更小的音乐片段
使用较小的音乐片段尤其有效,如果我们关心的结果是全局性的,即适用于整首歌的每个部分。例如,我们可以假设一首歌曲的类型在整首歌的过程中保持相对稳定。因此,我们可以轻松地使用 10 秒钟的片段,而不是完整的歌曲(或者非常常见的 30 秒片段)来进行音乐类型分类任务。
这有两个优点:
-
较短的片段意味着每个训练示例的数据点更少,这样可以使用更小的模型。
-
通过绘制三个 10 秒的片段,而不是一个 30 秒的片段,我们可以将训练观察次数增加三倍。总的来说,这意味着我们可以构建更少依赖数据的模型,同时给它们提供比之前更多的训练示例。
然而,这里有两个潜在的风险。首先,片段大小必须足够长,才能进行有效的分类。例如,即使是人类,在面对 3 秒钟的片段时,也很难进行类型分类。我们应当仔细选择片段大小,并将这个决定视为我们 AI 解决方案的超参数。
其次,并不是每个音乐属性都是全局性的。例如,如果一首歌包含人声,并不意味着它就没有器乐部分。如果我们将歌曲切割成非常短的片段,我们就会在训练数据集中引入许多错误标签的例子。
使用更高效的音乐表示
如果你十年前学习过音乐 AI(当时这一切还叫做“音乐信息检索”),你会了解到色度图、MFCC 和节拍直方图。这些手工设计的特征旨在让音乐数据能够与传统的机器学习方法配合使用。随着深度学习的崛起,这些特征似乎已经完全被(梅尔)声谱图所取代。
声谱图将音乐压缩成图像,并且几乎没有信息丢失,这使得它们与计算机视觉模型非常适配。我们不再需要为不同任务设计定制的特征,而是可以使用相同的输入数据表示和模型来处理大多数音乐 AI 问题——前提是你有成千上万的训练示例来喂给这些模型。
当数据稀缺时,我们希望尽可能压缩信息,以便模型更容易从数据中提取出相关模式。请考虑以下四种音乐表示方式,并告诉我哪一种能帮助你最快地识别音乐的调性。

四种不同表示同一首歌(Tina Turner 的《Honky Tonk Woman》)的示例。尽管色调图比波形图小约 70 万,但它能更有效地帮助我们识别调性(C# 大调)。图像由作者创建。
尽管梅尔频谱图可以用作调性检测系统的输入(如果你有足够的数据,可能应该使用它),但沿时间维度平均的简单色调图能更快地揭示特定信息。这就是为什么频谱图需要像 CNN 这样的复杂模型,而色调图则可以通过传统模型,如逻辑回归或决策树,轻松分析的原因。
总结,已建立的频谱图 + CNN 组合在许多问题中仍然非常有效,只要你有足够的数据。然而,针对较小的数据集,可能需要重新审视一些来自 MIR 的特征工程技术,或开发自己特定任务的表示。
提示 3:利用预训练模型或嵌入

横幅由作者使用 DALL-E 2 生成。
当数据稀缺时,最有效的策略之一是利用预训练模型或嵌入。这种方法可以让你在现有知识的基础上构建,利用在大规模数据集上训练的模型,从而缓解较小数据集的限制。
为什么使用预训练模型?
预训练模型已经学会了从其训练数据中识别和提取有意义的特征。例如,一个在流派分类任务上训练的模型,很可能在训练过程中学到了多种有意义的音乐模式。如果我们现在想构建自己的情感标签模型,使用预训练的流派模型作为起点可能是有意义的。
如果预训练模型是在类似任务上训练的,你可以将它们学到的表示转移到你的特定任务中。这一过程被称为迁移学习。迁移学习可以大幅减少从零开始训练自己模型所需的数据量和计算资源。
音乐 AI 中的流行预训练模型
几年前,最常见的方法是使用像流派分类器这样的预训练模型,并将其微调以执行特定任务。像 MusiCNN 这样的模型通常被用于这种情况。
然而,现在更常见的是使用那些专门训练过的预训练模型,这些模型可以产生有意义的音乐嵌入,即歌曲的向量表示。以下是三种常用的预训练嵌入模型:
根据我个人的经验,使用微软的 CLAP 进行迁移学习,和使用 LAION CLAP 进行相似性搜索,效果最好。
利用预训练模型的不同方式
预训练模型可以以多种方式使用:
-
完全微调:使用一个预训练的分类模型或嵌入模型,并在一个较小的任务特定数据集上进行微调。如果你能够使用完整的大型模型进行训练和推理,并且知道如何实现它,这种方法通常能获得最佳结果。
-
嵌入作为输入特征:一种更高效的资源使用方法是从预训练模型中提取嵌入,并将它们作为新模型的输入。由于这些嵌入通常是 500 到 1000 维的向量,可以附加一个具有几千个参数的较小神经网络来进行更高效的微调。对于小型数据集,这种方法通常优于完全微调。
-
直接使用嵌入:即使没有任何微调,嵌入也可以直接使用。例如,来自预训练模型的嵌入通常用于音乐相似性搜索。CLAP 模型甚至可以用于文本到音乐的检索,或者(尽管效果还不理想)用于零-shot 分类,即没有训练的分类。
利用预训练模型的嵌入可以显著增强你的音乐 AI 项目。通过在这些模型的学习模式识别基础上构建,你避免了重新发明轮子。当数据稀缺时,预训练模型应该始终被考虑。
结论
不要让数据稀缺成为你的阻碍!许多几年前需要成千上万训练样本的使用案例,现在几乎已经成为商品。
要在小数据集上实现稳健的性能,你的首要任务应该是不要浪费任何宝贵的数据。让我们回顾一下本文的要点:
-
数据增强是一种让模型通过小但有效的变化多次从训练样本中学习的好方法,从而增强其鲁棒性。
-
更小的模型和更高效的数据表示迫使你的模型专注于数据中最重要的、潜在的模式,从而避免过拟合。
-
预训练模型让你通过微调借用一些来自更大 AI 系统的智能。再也不需要从头开始训练了!
当然,使用小数据集时你能实现的效果有其天然的局限性。如果你有 100 个标注样本,而你的目标是建立一个具有 10 个类别和 30 个子类别的多标签流派分类器,那么即使你使用了我所有的技巧,也不会走得太远。
尽管如此,我已经开发出了令人惊讶的能力强大的流派和情绪分类器,仅用 1000 首标注歌曲。仅仅两年前,凭借这么小的数据集实现这一目标几乎是不可能的。我认为,这些去中心化效应是当前 AI 热潮中最令人兴奋的方面之一。
如果你有一个小而高质量的音乐数据集,并且正在考虑用它进行机器学习,现在正是尝试的最佳时机!
对音乐 AI 感兴趣吗?
如果你喜欢这篇文章,或许你会想看看我其他的一些作品:
-
“2024 年值得期待的三大音乐 AI 突破”。Medium 博客
-
“生成式 AI 音乐中的人类元素”。Audiosocket 播客视频采访
-
“谷歌如何利用你的数据来改进其音乐 AI”。Medium 博客
你还可以通过Linkedin关注我,获取有关音乐 AI 的新论文和趋势的最新信息。
感谢阅读本文!
3 种简单的统计方法用于离群值检测
如果有效,就保持简单
·发布于 Towards Data Science ·阅读时长 6 分钟·2024 年 6 月 18 日
--

离群值。图片由作者提供
正如我们所知,数据科学家的一项重要工作是清理和预处理数据。大部分工作都涉及到离群值的检测和去除。大的离群值、异常峰值和错误数据会严重影响机器学习模型的训练,因此处理离群值非常重要。
但数据科学家并不总是使用像孤立森林或局部离群因子这样的机器学习模型来识别离群值。我在数据科学职业生涯中学到的一件事是,如果更简单的解决方案有效,就使用它。
我想给你提供 3 个简单的统计方法,用于检测离群值,这些方法在大多数情况下效果很好。我还会展示如何在 Python 中实现这些方法。
1. Z 分数
Z 分数,也叫标准分数,是最常见的离群值检测方法之一。它本质上表示的是一个数据点距离均值有多少个标准差。
给定数据集中某个数据点的 z 分数计算公式如下:
你从未见过的 3 个 Airflow 中分支的惊人用例

你的数据管道可以像这棵漂亮的树一样有许多分支。照片由 Andrew Svk 拍摄,来源于 Unsplash
分支条件性是许多 DAG 的重要特性
·发表于 Towards Data Science ·4 分钟阅读·2024 年 8 月 2 日
--
介绍
你多久会写一个数据管道,突然希望能够做一些有条件的操作?某些操作仅在满足一组条件时才会发生?
希望不会太频繁!Airflow已经通过 AirflowBranchPython Operator 支持了这种功能。许多其他工作流编排工具也效仿了这一做法。Prefect 有 Conditional Flows,Dagster 有 DyanmicOutput,而在 Orchestra 中,我们基于状态来实现分支。
这引出了最重要的问题:为什么?
那么,为什么要费心去做分支,使得管道变得比实际需要的更复杂呢?我们将看到,实际上有一些非常不可思议的用例,尤其是对于那些希望生活中有更多自动化的人来说。
Airflow 中分支的一个简单示例
在深入探讨用例之前,我们将使用下面的代码作为参考,以便理解分支在实践中的工作原理。
from airflow import DAG
from airflow.operators.dummy import DummyOperator
from…
作为一些基础图表替代方案的三种三角形图表创意
使用 Python 创建数据可视化,作为条形图、饼图和一些 3D 图形的替代方案。
·发表于Towards Data Science ·阅读时间 8 分钟·2024 年 11 月 19 日
--

图片由Igor Karimov 🇺🇦提供,来源于Unsplash
许多图表通常由矩形或圆形组成,例如条形图和饼图。这些图表既常见又实用,因为它们不仅容易制作,而且大多数人都知道如何阅读和理解它们。
虽然它们适用于许多场合,但也有一些场景可能觉得它们过于基础,比如制作信息图或吸引人们的注意力。可以应用不同的方法使图表更具吸引力。其中之一就是改变图表的形状。


本文中的三角形图表示例。图片由作者提供。
本文旨在提供关于如何将三角形图表作为替代方案的创意和指南。这并不意味着它们可以完全替代原始图表。每种图表都有其优缺点,取决于使用的目的。
三角形图表
本文将解释三种三角形图表:
- 三角形条形图
利用 Apple Silicon 的 GPU 进行深度学习的 3 种方法
Apple Silicon 提供了令人印象深刻的性能提升,并且具有极好的功率效率。我们可以将这些芯片用于深度学习吗?
·发布于 Towards Data Science ·阅读时间 4 分钟·2024 年 3 月 1 日
--

作者提供的图片
Apple Silicon 提供了令人印象深刻的性能提升,并且具有出色的功率效率。那么这些芯片也能用于深度学习吗?绝对可以!
在本文中,我们将探索 3 种方法,利用 Apple Silicon 的 GPU 来处理各种深度学习任务。
1. 在 PyTorch 中使用 MPS 后端
使用 GPU 进行深度学习的最简单方法是通过 Metal 性能着色器 (MPS)。MPS 扩展了 PyTorch 框架,以便利用 Mac 上的 GPU。
要使用 MPS 后端,您需要:
-
macOS 12.3 或更高版本
-
Python 3.7 或更高版本
让我们创建一个脚本,检查 MPS 是否设置正确。首先,我们需要创建一个包含虚拟环境的目录,并安装 PyTorch:
mkdir mps_test
cd mps_test
python3 -m venv .venv
source .venv/bin/activate
pip install torch
接下来,我们将创建一个 Python 文件,check.py,并使用以下代码:
3 种让你像(伟大的)数据科学家一样思考的方法
通过更好的思考,让你的职业生涯更上一层楼
·发表于《走向数据科学》 ·7 分钟阅读·2024 年 2 月 22 日
--
如果你已经经历过学习编程的过程,你会明白,这不仅仅是记住语法。它更像是学习一种新的思维方式。
首先,你需要学习工具(语法、数据结构、算法等)。然后,你会遇到一个问题,必须用高效的方式解决它,充分利用这些工具。
数据科学也是一样。在这个领域工作意味着你每天都会遇到问题,我指的不仅仅是代码错误。
数据科学家需要解决的问题示例:
我如何在这个数据集中检测到异常值?
我如何预测明天的能源消耗?
我如何将这张图片分类为面部或物体?

图片来源:Kenny Eliason,来自 Unsplash
数据科学家使用各种工具来解决这些问题:机器学习、统计学、可视化等等。但如果你想找到最佳解决方案,你需要一种方法,牢记某些原则。
1. 优先考虑数据
理解数据是最重要的东西。
我知道,这听起来很显然。让我解释一下。
我的#30DayMapChallenge 2024
30 天,30 张地图:我在数字制图中的 11 月冒险
·发表于Towards Data Science ·14 分钟阅读·2024 年 12 月 7 日
--

由 DALL·E 生成的图像
#30DayMapChallenge 是一个社交媒体活动,地图爱好者在 11 月的 30 天里根据每天的主题制作地图。你可以在这里找到更多细节。每天发布一张地图并不是强制性的——许多参与者选择专注于他们最喜欢的主题。
在见证了多年的#30DayMapChallenge 后,今年是我第一次参与。我最初以为这会非常困难,但我很快发现最难的部分其实只是开始。现在,在经历了整个 11 月之后,我自豪地说我成功完成了挑战,并在 30 天内连续制作了 30 张地图!
在这篇文章中,我将分享我为挑战制作的地图,以及我在过程中的一些想法和我使用的工具。
数据来源
330 周数据可视化:我的旅程与关键收获
数据可视化中持续练习如何提升我的数据科学技能
·发表于Towards Data Science ·阅读时长 7 分钟·2024 年 6 月 22 日
--
自从 2018 年开始我的全职数据科学工作以来,我一直在制作每周一张可视化图。现在,330 多周过去了,我认为这是我真正引以为豪的成就。在咖啡闲聊时,人们经常问我关于这件事的事情,这也激发了我写这篇文章,分享我的旅程和关键收获。
我将介绍我如何开始、如何找到可视化的数据集、如何选择合适的可视化工具、我所学到的知识,以及给有志成为数据可视化专家的人的一些建议。
我是如何开始的
当我开始我的第一份全职工作时,我的导师,一位 Tableau 大师,向我介绍了MakeOverMonday项目。
欢迎来到“改造星期一”!
“改造星期一”是你每周与自己和成百上千个充满激情的数据人的学习与发展时光,完全免费!
每周一加入我们,使用给定的数据集,创建更好、更有效的可视化,并帮助我们让信息更加易于获取。
“改造星期一”社区每周都会聚集来自全球的成员,我们也希望你能成为其中的一员。
这是一个了不起的社区,如果你在寻找 Tableau 数据可视化的灵感,我强烈推荐加入。
3D 图聚类与图论:完整指南
3D Python
用图论进行 3D 点云的欧几里得聚类 Python 教程。无监督分割的基本概念和顺序工作流。
·发表于Towards Data Science ·阅读时间:22 分钟·2024 年 12 月 2 日
--

3D 图聚类与图论。© F. Poux
除了听起来很酷,为什么我要花三周时间编写关于 3D 数据图论的 Python 教程?
简短的答案是,它对于理解 3D 场景极为有用。它可以改变你在决策场景中处理 3D 数据集的效率。
但也有许多挑战需要注意。
如果我们退一步来看,我们的眼睛能够捕捉空间信息,然后通过我们的认知系统进行处理。这就是其中的魔力所在:我们的脑袋帮助我们理解场景及其关系分解。

作者在《机器人自主性功能性物体场景重建》中描述的场景关系示例,2022 年。
通过内部知识表示,你可以立即知道你的场景由地板和墙壁组成,地板上有椅子和桌子,而桌子上又放着杯子、微波炉和笔记本电脑。
相交两条 3D 射线(最近点)
让我们解开 3D 射线背后的数学,展示它其实并不比传统的 2D 情况更具挑战性。
·发表于 Towards Data Science ·7 分钟阅读·2024 年 2 月 12 日
--

摄影:由 Tobias Cornille 拍摄,来源于 Unsplash
1. 介绍
动机
在计算机图形学、自动驾驶导航或增强现实等领域,通过操控 3D 环境来创造沉浸式体验,深刻理解射线相交和三角剖分算法,使数据科学家能够制作栩栩如生的模拟和互动虚拟世界。
它还展示了诸如伪逆矩阵、最小二乘法优化、克莱姆法则或三重标量积等数学原理,不仅仅是理论;它们是解决现实世界问题的实际工具!
射线定义
射线从给定位置开始,并沿着给定方向无限延伸,就像激光束或光线一样。
可以把射线看作是单行道,有一个固定的起点,而直线则像一条双向街道,无限延伸。
从数学上讲,一条射线由其起点o和方向d定义。由于射线是 1 维空间,射线上任意一点可以通过其相对于原点的正距离(或时间)t来参数化。

射线相交
找到两条射线r1和r2的交点意味着找到两个参数t1和t2,分别位于第一条和第二条射线上,使它们在空间中结果为完全相同的点。

一个有趣的点是,我们并没有指定光线所在空间的维度。它可以是二维的,比如纸上的一条直线,三维的,如激光束,甚至可以存在于更高维度的空间中!
由于我们要找的是t1和t2的值,我们将它们连接成一个列向量x。这使我们能够将这个方程向量化成标准的线性系统Ax=b。为了更清晰,每个项下方都有注释,指明了矩阵或向量的形状,其中n是定义光线的空间维度。

注意:矩阵 A 是通过将两个方向作为列向量并排连接构建的,第二个方向是反向的。
解线性方程Ax=b可以得到t1和t2。然而,如果这两个参数中的任何一个是负数,则表示交点位于某个光线的原点之后,意味着两条光线之间没有交点。

图片由Mehdi MeSSrro拍摄,来自Unsplash
2. 2D 光线相交
引言
2D 情况是最简单的。通常,除非它们完全平行,否则两条二维线总是会在一个唯一的公共点相交。
在下图中,r1和r2相交。然而,r3没有与任何光线相交,因为它与r2平行,并且在原点之后与r1相交。

2D 光线相交——图示由作者提供
解线性系统
上面介绍的线性方程Ax=b有n个方程和 2 个未知数。因此,在二维情况下,我们最终得到的是一个有 2 个方程和 2 个未知数的系统,这意味着我们只需要求解矩阵A的逆。

如果光线是平行的,矩阵A将不可逆,因为它的两列,d1和-d2,是共线的。
如果你不关心t1和t2,只想知道是否存在相交,计算矩阵A的行列式并将其与 0 比较就是你所需要的全部。
如果你不关心同时得到t1和t2,例如如果你只关心交点的实际二维位置,使用克拉梅法则是一个聪明的技巧。它通过矩阵A和修改过的矩阵的行列式来表示解,其中A的其中一列被b替换。

克拉梅法则用于识别t1和t2。运算符|.|表示行列式。逗号用于分隔矩阵列。

图片由Vince Fleming拍摄,来自Unsplash
3D 光线相交
引言
与二维情况不同,两个三维射线相交的可能性非常小。例如,试图让两个人手持的激光指示器相交是非常具有挑战性的。
线性系统Ax=b最终有 3 个方程和 2 个未知数,除非方程中有冗余,否则无法求解。
最小二乘法
由于大多数情况下没有解,两个射线之间的最短线段可以视为它们的交点。我们将p1和p2分别表示为位于r1和r2上的这段最优线段的端点。

3D 射线交点 — 作者图示
我们现在的任务转化为一个最小二乘法优化问题。由于Ax-b已经表示了p2和p1之间的向量,所以其公式化非常简单。

这是一个众所周知的问题。在最优解处将梯度归零可以通过伪逆矩阵得到解,这是构建一个方阵并使其可逆的便捷方法。


正交性
尽管在上面的图示中最短线段似乎垂直于两条射线,但我们还未正式证明这一点,尽管这看起来是直观的。
零梯度也意味着由p1和p2定义的线段与两个射线方向d1和d2之间的点积为零,从而证明了正交性。

替代线性系统
最小二乘法方法完全有效,但我们也可以引入第三个未知变量,使得我们的线性系统可以求解。
考虑到向量d1、d2和p2-p1构成了一个正交基,我们可以以o1为原点构建一个正交框架。然后,可以通过表达o2相对于该框架的局部坐标来推导出点p1和p2。
请注意,线段p2-p1的方向由两个射线方向的叉积给出。我们将在该方向上引入带符号的比例δ,并且在正交基中使用该比例。两条射线之间的实际距离可以通过将|δ|与叉积的范数相乘得到。

t1、δ和 t2 可以通过线性方程组求解。

3x3 矩阵的行列式可以通过标量三重积来推导,标量三重积涉及一个列向量与其他两个列向量的叉积的点积。

在我们的例子中,简化行列式方程是直接的,因为第二列已经是叉积。请注意,将叉积插入为最后一列将导致范数的否定。

使用克拉默法则求解可以得到最优的t1和t2,还可以得到带符号的尺度δ。

如果你喜欢简洁的方程式,可以重新排列和交换列,去除负号,使其看起来更整洁!

交点
交点可以定义为最短线段的中点,即0.5(p1+p2)。
射线之间的距离
如前所述,带符号的尺度δ必须乘以两个射线叉积的模长,才能获得射线之间的真实距离。

结论
希望你喜欢阅读这篇文章,并且它能为你提供更多关于如何求交 2D 或 3D 射线的洞见!
使用 Python 和 Meshroom 进行 3D 重建教程
3D 重建
快速学习如何从照片创建 3D 模型,并掌握使用 Python + Meshroom(摄影测量法)生成点云。
·发表于 Towards Data Science ·5 分钟阅读·2024 年 7 月 20 日
--
你可能已经从我之前写的文章中听说过基于图像的 3D 重建。这是一个深度优先的示例(以备后用):
使用 3D 摄影测量法、Reality Capture、Meshroom 和 Blender 的完整实操 3D 重建教程。
towardsdatascience.com
今天,我想重点介绍一个“直奔主题”的阅读,确保你可以拍摄一堆照片,并在你的 Python 脚本中创建 3D 点云或 3D 网格,进而开发出一个很酷的体验。
如果你觉得不错,让我快速回答下面的问题:

用于 3D 点云生成的工具应该选择哪一个?© Florent Poux, Ph.D.
当你准备好时,让我们开始有趣的部分吧。
📦 Florent 的资源:请在这里找到本教程的资源——首先是Meshroom 软件及其文档。然后是Anaconda 发行版,包含 Python 3.9+,以便使用一些库。
Lexcube 的地理空间大数据三维可视化!(Python)
学习如何使用 Lexcube,这个用于时空域数据可视化的 Python 包!
·发表于 Towards Data Science ·阅读时间 9 分钟·2024 年 2 月 12 日
--
目录
-
🌟 介绍
-
🌐 Lexcube
-
📰 数据
-
📂 带有随机数的数据立方体
-
🗂️ 带有气候数据的数据立方体
-
🔄 光栅图层转为 Xarray
-
🌍 Lexcube 的 Xarray 三维可视化
-
📦 我们还能用 Lexcube 做什么
-
📝 结论
-
📚 参考文献
🌟 介绍
三维数据可视化(纬度、经度和时间)非常迷人,对吧?作为一名地理空间数据科学家,我一直想知道如何以最简单的方式绘制一个由数百个光栅图层合并而成的立方体数据集。在我浏览 LinkedIn 信息流时,我发现了一个很棒的 Python 库,叫做 Lexcube,它最近已在 Jupyter Notebook 上发布。关于 Lexcube 的更多信息,请参考这篇文章或查看Lexcube 在 GitHub 上的页面。
首先,我要感谢 Miguel Mahecha 在 LinkedIn 上分享这篇文章,还要感谢 Maximilian Söchting 和他的团队开发了一个对地理空间数据社区非常有价值的工具。其次,这里有一个实际操作练习,帮助你使用这个包…
4 个示例助你提升 PySpark 技能
使用 PySpark 适应大规模数据处理
·发布于 Towards Data Science ·7 分钟阅读·2024 年 1 月 30 日
--

PySpark 是 Spark 的 Python API,Spark 是一个用于大规模数据处理的分析引擎。特别是当我们处理那些使用 Pandas 和 SQL 等工具难以应对的大型数据集时,Spark 已成为数据科学生态系统中占主导地位的工具。
在本文中,我们将学习 PySpark,但与大多数教程的视角不同。我们不会简单地讲解常用的 PySpark 函数并说明如何使用它们,而是通过解决一些具有挑战的数据清洗和处理任务来学习。这样的学习方式不仅帮助我们学习 PySpark 函数,还能帮助我们了解何时使用它们。
在我们开始示例之前,先让我告诉你如何获取示例中使用的数据集。它是我用虚拟数据准备的一个示例数据集。你可以从我的 datasets 仓库下载,文件名是“sample_sales_pyspark.csv”。
让我们从这个数据集创建一个 DataFrame 开始。
from pyspark.sql import SparkSession
from pyspark.sql import Window, functions as F
spark = SparkSession.builder.getOrCreate()
data = spark.read.csv("sample_sales_pyspark.csv", header=True)
data.show(5)
# output…
如果你打算从 Pandas 转到 Polars,必须了解的 4 个函数
本文包含了 Pandas 和 Polars 的代码
·发布于 Towards Data Science ·阅读时长 6 分钟·2024 年 1 月 11 日
--

图片来自 Israel Palacio 在 Unsplash
当数据量很大时,Pandas 有时会变得难以使用。与大数据集相关的两个主要问题是 Pandas 进行内存中分析以及创建中间副本。
另一方面,Pandas 用户友好的 API 和丰富的灵活函数选择,使其成为最受欢迎的数据分析和处理库之一。
Polars 是 Pandas 的一个很好的替代方案,尤其是当数据量太大,Pandas 难以处理时。Polars 的语法介于 Pandas 和 PySpark 之间。
在本文中,我们将介绍四个在使用 Pandas 和 Polars 进行数据清理、处理和分析时必须掌握的函数。
数据
首先,我们当然需要数据来学习这些函数是如何工作的。我准备了示例数据,您可以在我的 datasets 仓库中下载。我们将在本文中使用的数据集名为“data_polars_practicing.csv”。
让我们从将数据集读入 DataFrame 开始,DataFrame 是 Polars 和 Pandas 库中的二维数据结构。
import polars as pl
df_pl =…
2024 年所有分析工程师必须学习的 4 件事
现代数据栈中的基础知识和即将出现的趋势
·发布于 Towards Data Science ·6 分钟阅读·2024 年 1 月 26 日
--

图片来源:Hal Gatewood 来自 Unsplash
我已经将 2024 年定为学习和投资自我成长的一年。虽然每年都应该这样做,但有时我们会把它搁置一旁,而专注于职业发展,或者是我们的业务。
不幸的是,许多人没有意识到学习新事物对职业发展的重要性——尤其是数据领域的职业。由于这个领域不断变化,知识很容易落后。
分析工程师需要每周抽出合适的时间,确保他们扩展对数据栈不同元素、分析工程的关键基础和市场上所有新工具的知识。
将此列表作为分析工程师应该关注的重点。每周或每月选择其中一个主题进行学习,观察自己在角色中的成功飞速增长。
如何在分析工程师和软件工程师之间强制执行数据合同
数据合同在过去几年非常流行,而且有充分的理由。它是分析工程师之间存在的一个长期问题…
我的求职马拉松的四个建议
用工具来应对挑战
·发表于Towards Data Science ·6 分钟阅读·2024 年 3 月 7 日
--

我在 2023 年 8 月重新回到了求职市场。尽管大公司的裁员和当前的挑战状况让我面临很大困难,我还是毅然决定离开公司,全身心投入到寻找下一份机器学习/数据科学职位的过程中。
我天真地估计自己会在几个月内找到梦想中的职位。现实证明,这比预期的时间要长。因此,这个过程对我来说是一次马拉松,而不是短跑。幸运的是,我这个月完成了这场马拉松🎉
现在,我想分享这些宝贵的收获——我的学习经历——来激励求职者,让他们知道在这场充满起伏的马拉松中,他们并不孤单。
以下是我在这场马拉松中遵循并学到的建议:
-
确定你下一个角色的标准
-
拒绝的次数会比成功的回应多
-
不要盲目地撒网祈祷
-
使用基于 AI 的招聘工具
P.S. 最后有一个额外的小贴士😉
免责声明:全职投入求职需要仔细的预算规划和充足的财务资源。这可能是我在马拉松过程中遇到的最重要的考虑因素。不过,我设计这篇博客的目的是专注于如何应对挑战,并提供支持你准备面试、简历和求职信的工具。
确定你下一个角色的标准
随着你积累更多的工作经验,你会更加清楚自己在哪些工作环境中能茁壮成长。除了职位描述外,公司的文化和价值观、工作方式(远程/现场/混合),以及你所创造的影响/结果,都是成功的重要因素。
所以,问问自己这些关键问题:
-
你在远程/混合/办公室设置下更有生产力吗?
-
你适合什么样的领导风格和管理方式?
-
你寻找的文化元素是什么?哪些价值观最能引起共鸣?
-
是什么激励你每天前行?你是否希望自己的工作每天都能产生积极的影响?
这些问题将帮助你明确理想的职位和公司。你可以通过在面试中提出正确的问题,尤其是关于文化价值观的问题,来大致了解这些话题。
在准备我的面试问题时,我借助了keyvalues.com。这是一个很棒的网站,帮助你设计正确的面试问题,以便了解公司的文化价值观、领导风格和团队的日常工作。
拒绝的数量将大于成功的回应
准备好接受更多的拒绝,而不是成功的回应。这个在 LinkedIn 上疯传的图表证明了这一点。它展示了一个市场营销技术求职者的申请统计数据。从我的经验来看,我可以告诉你,这个统计数据对于数据科学/机器学习领域是有效的。

图片来自 Reddit 上的 r/recruitinghell。 图片链接
我从 100 份申请中收到了 23 个成功回应和 5 个工作邀请。其余的要么是没有回复,要么是被拒绝。一开始,我对每一次被拒绝感到沮丧,甚至为一些我感觉和公司文化高度契合、自己技能匹配度很高的拒绝而哭泣🥲。经过几次低谷后,我学会了度过失望的情绪,并不把拒绝和自己的自我价值挂钩。
此外,拒绝仅适用于特定的职位和时间。未来相同的职位可能会重新开放,要求也可能发生变化。在此期间,你可能已经自我提升了。
例如,2023 年 11 月,我与两家公司进行了面试,分别是 MLOps 工程师和数据科学家的职位。第一次面试后,我被淘汰了。快进到 2024 年 2 月,我收到了两家公司的机器学习工程师职位的工作邀请。所以,永远不要放弃,坚持尝试✌🏼
不要盲目投递
“我一天申请多达 50 个职位,涉及两个不同的职业方向。”
…这是上面图片的创作者所说的。
要在一天内定制简历并提交 50 份申请几乎是不可能的。这是撒网式投递方法的一个例子。它是通过公司招聘页面将一份通用简历提交给每个职位发布的传统方式。
在今天竞争激烈的求职市场中,只有高质量的申请才能通过第一次筛选。要制作一份高质量的申请,你需要根据每个申请量身定制简历和求职信。
在你的简历中,按顺序排列你的技能,使与职位描述匹配的技能排在前面,提到职位描述中列出的你的品质,并确保使用积极动词。
一封定制的求职信也是在申请过程中脱颖而出的必要条件。大多数公司都会记录他们的招聘流程、价值观以及他们在候选人身上寻找的品质。同时,在撰写求职信之前,了解他们正在研发的产品和面临的问题。然后将这些学习和价值观融入到求职信中,并确保突出显示职位要求中列出的技术技能。
最重要的是,利用你的网络通过初筛,不要犹豫联系你的网络(前同事、大学朋友等)。询问他们是否愿意成为你的推荐人。你会惊讶地发现,人们会支持你。我相信这种支持来自一个共情的空间,因为我们每个人在人生中都曾是求职者 😌
此外,大多数公司都有员工推荐系统,因此,如果过程成功结束,对你们双方来说都是双赢的局面。
使用基于 AI 的招聘工具
大约在九月,我意识到“撒网式投递”对我并不奏效。那时我了解了基于 AI 的工具,来提升我的简历和求职信准备过程。
我选择了Teal,这是我在这篇 LinkedIn 文章中列出的多个工具中挑选的。我使用了简历构建器功能,为每个申请制作了量身定制的简历。

图片由作者提供
此外,匹配部分帮助我量化了职位发布与我的简历之间的匹配度。它会根据简历和职位发布中的关键词计算一个百分比。在定制简历时,你可以目标是获得一个较高的匹配得分,从而帮助你通过初筛。此外,Teal 还会建议如何改善简历中的措辞和表述你的成就。
基于 AI 的工具仍然是我们的助手,它们尚不能取代人在创意写作中的作用。我认为求职信和简历是一种专业的创意写作形式。所以不要仅仅依赖由 AI 工具生成的求职信或简历。始终评估它们的输出,并将其建议作为反馈。
额外提示: 由于年初的预算调整和人们在年初薪资调整后更倾向于换工作,因此每年第一季度的就业市场更为活跃。这是我个人的观察,也来自一位在亚马逊拥有多年人力资源经验的朋友的建议😉
感谢阅读!
六个月后,我完成了这场马拉松,并获得了一个满足我大部分标准的职位。这对我来说是一次充满挑战的旅程,经历了起伏。
我从上述建议中受益匪浅,因此无法将其保留给自己。我希望我的经验能够照亮你的求职之路,并支持你的求职过程🤗
如有评论或建设性反馈,你可以通过回复、X(Twitter) 或 LinkedIn 联系我!
8 分钟看完 4 年的数据科学
我在这四年多的学习数据科学的旅程中所学到的
·发布于 Towards Data Science ·8 分钟阅读·2024 年 10 月 24 日
--

图片由 Greg Rosenke 提供,来源于 Unsplash
我过去四年一直在学习数据科学,在这期间,我作为一名专业数据科学家工作了超过三年。
我从物理学开始学习,当时并不确定大学毕业后想做什么。然而,现在我有了我热爱的职业,无法想象自己做其他任何事情!
在本文中,我想按年回顾我的成长历程,分享那些帮助我成为数据科学家的经验、学习和失败。
我希望你能从我的经历中找到有价值的内容,帮助你在自己的道路上前进!
第一年
正如你们中的一些人可能知道的,我最初的计划是攻读物理学博士,最终目标是成为一名职业研究员!
在我的硕士课程中,我需要从第三年的下半年到第四年的上半年进行一年的研究。这基本上就是 2020 年。
5 个你可以在这个周末用 Python 构建的 AI 项目
从适合初学者到高级
·发布于 Towards Data Science ·阅读时间 7 分钟·2024 年 10 月 9 日
--

图片来自 Canva。
提升 AI 技能的最佳方式是通过构建项目。然而,如果你刚刚开始,弄清楚该构建什么可能会很困难。在这里,我分享了 5 个你可以快速构建的 AI 项目,分为三个复杂度级别。我将分解实现每个想法所需的步骤和 Python 库。
初学者在考虑项目创意时犯的第一大错误是从“我如何使用这个新技术?”这个问题开始。虽然这可能是学习新工具的好方法,但其实有更好的方法。
好的项目创意应从“我能解决什么问题?”这个问题开始。这不仅在与潜在雇主分享时能讲述一个好故事,而且解决问题正是你将技术技能转化为价值的方式。
以下项目都采用了以问题为导向的方法。你可以直接实现这些创意,或者(更好)将它们作为灵感,用于解决你个人面临的问题。
5 款令人惊艳的插件,让你的 Visual Studio Code 界面更具吸引力
通过将 IDE 转变为一个令人惊叹的编码环境,让你的代码焕发光彩
·发表于 Towards Data Science ·阅读时间:6 分钟·2024 年 10 月 5 日
--

介绍
我已经使用 Visual Studio Code 好几年了,我坚信它是现存最优秀的 IDE 之一。虽然一些人可能更偏爱 JetBrains 的 IDE,这些 IDE 同样非常受欢迎,但 Visual Studio Code 具有显著的优势:它是免费的、轻量级的,且高度可定制。
Visual Studio Code 拥有一个庞大的开发者和贡献者社区。鉴于此,市面上存在大量的免费插件。根据我的经验,插件在提高生产力方面起着至关重要的作用,它们使工程师能够使用自定义快捷键、美化 IDE、自动保存文件,甚至管理 Git 工作流和 Docker 容器。
由于我在日常工作中频繁使用 Visual Studio Code,并且非常欣赏它让编码过程变得更加愉快,因此我决定与读者分享我最喜欢的插件。
插件有不同的用途,而本文将介绍的插件,专注于让 Visual Studio Code 的用户界面更加美观。
# 01. Atom One Dark 主题
5 个正在扼杀你作为数据工程师潜力的坏习惯
还有一些可以立即扭转这些问题的简单解决方案
·发表于 Towards Data Science ·6 分钟阅读·2024 年 2 月 16 日
--

每个数据工程师都希望自己能够不断地作为专业人士进步,并提升自己的技术技能。作为数据工程师,我们喜欢接受挑战,并希望自己朝着最终目标不断取得进展。
这就是我们工作性质的特点——改善系统、实施提升数据质量的流程,并找到创新的方式来减少成本。所有的一切都在于不断向前迈步,而不是后退。
在担任数据工程师三年后,我开始感觉自己的成长停滞不前。我对自己正在做的工作不再感到兴奋,也没有从身边的人那里学到什么新东西。通过我的工作,我没有能够学习到任何新的技术技能。
现在回过头来看,我可以清楚地看到,正是时候换一个新的角色了。然而,我能明确指出有几个特定的习惯,正是这些习惯导致了我在那家公司取得的成功开始停滞。
在这篇文章中,我将分享可能正在扼杀你作为数据工程师潜力的坏习惯,以及你可以如何修正它们。
避免做困难的事情
5 次点击惊艳全场:如何通过更改数据类型快速优化 Power BI 模型
优化 Power BI 语义模型不一定总是一个令人生畏且耗时的任务。你通常可以获得许多快速且轻松的胜利!
·发布于 Towards Data Science ·阅读时间 6 分钟·2024 年 10 月 4 日
--

图片由作者提供
几周前,我被要求优化一个运行缓慢的 Power BI 报告。当然,Power BI 报告运行缓慢的原因可能有很多,但在这篇文章中,我想与大家分享一些“低悬果实”,以及如何通过应用一些非常简单的优化技术,显著提升 Power BI 语义模型的性能。
设定场景
为了演示,我将使用一个事实表,该表包含了虚构公司“Customer First”的客户支持部门进行的聊天数据。这个表大约有 900 万行,在 Power BI 和分析工作负载的上下文中,这并不算是一个大的表。为了简便起见,我们假设我们的模型仅由这一张表组成。最后,语义模型被配置为导入模式模型。如果你想了解 Power BI 中数据是如何存储的,我建议你首先阅读这篇文章。
构建受利益相关者喜爱的商业仪表盘的五个必备技巧
一本关于设计清晰、有效且可操作的决策支持仪表盘的实用指南
·发布于 Towards Data Science ·阅读时间 7 分钟·2024 年 12 月 11 日
--
在数据科学工作中,仪表盘制作常常感觉像是不受欢迎但不可避免的工作。为什么它不受欢迎?仪表盘制作比分析和建模技术含量低(不那么炫酷),而且更加重复。但为什么它也不可避免?它是理解任何产品的第一步和必备步骤,为分析和建模打开大门。它甚至有助于与利益相关者建立信任,因为仪表盘通常是来自利益相关者的首要请求。
与此同时,仪表盘的难度往往被低估。在过去的七年里,我看过许多同事制作的仪表盘。令人惊讶的是,并不是每个人都能做好这个“简单的任务”。你可能会想,仪表盘不就是做一堆图表吗?嗯,答案是有也不是。要构建一个好的仪表盘,你需要有逻辑地组织每个可视化,并简化复杂的数据,以便让受众理解。这更关乎基于数据的决策。此外,一个好的仪表盘总能实现自助分析,减少繁琐的数据拉取请求,反过来为数据团队带来益处。
在本文中,我将分享我设计有效商业仪表盘的五个顶级技巧,这些仪表盘能够提供清晰、可操作的洞察力并带来持久的价值。这些技巧适用于你使用的任何仪表盘工具——无论是 Tableau、Looker 还是 PowerBI 等。
5 个示例,助你掌握 PySpark 窗口操作
数据分析必备工具
·发表于Towards Data Science ·8 分钟阅读·2024 年 1 月 22 日
--

图片来源:Pierre Châtel-Innocenti 于Unsplash
我所使用过的所有数据分析和处理工具都具备窗口操作功能。有些工具更加灵活和强大,但能够在窗口上进行计算是数据分析中必须掌握的技能。
什么是数据分析中的窗口?
窗口是一组在某些方面相关的行。这个关系可以是属于同一组,或者是连续 n 天的记录。一旦我们根据所需的约束生成了窗口,就可以在其上进行计算或聚合操作。
在本文中,我们将通过 5 个详细的示例,全面了解 PySpark 中的窗口操作。我们将学习如何使用分区创建窗口,自定义这些窗口,并进行相关的计算。
PySpark 是 Spark 的 Python API,Spark 是一个用于大规模数据处理的分析引擎。
数据
我为本文准备了一个包含虚拟数据的示例数据集,您可以从我的datasets仓库下载。我们在本文中使用的数据集名为“sample_sales_pyspark.csv”。
让我们启动一个 Spark 会话并从这个数据集中创建一个 DataFrame。
from pyspark.sql import SparkSession…
提高生产力的 5 种高级数据科学家常用习惯
在你的每个数据科学项目步骤中都应用这些实践
·发表于 Towards Data Science ·阅读时间 15 分钟·2024 年 2 月 5 日
--

图片来自作者(同时也是作者本人)
你之所以来到这里,是因为你是一名数据科学家,想将自己的能力提升到新的高度。
这不仅仅是技术或软技能。不,你已经掌握了这些。
你所追求的是一种特别的升级。就像马里奥喝了蘑菇汁后那种疯狂飙升的状态。
那么,猜猜看?我正好有你需要的东西!!
我在 Spotify 开始了我的数据科学职业生涯,像一个迷你马里奥在大数据科学超级路易吉们的行列中玩耍,祈祷上帝保佑你能得到裁判的认可。
与他们密切合作让我意识到一件事:
成为一名成功的数据科学家的关键在于你有能力 创造一个有利于成功的环境。
这是让你快速成长的关键动力,就像变成超级马里奥一样!
今天是你的幸运日,因为我将与您分享我从 Spotify 的高级数据科学家那里学到的五个宝贵实践。
让我成为数据科学家的五个习惯
成为数据科学家的建议和技巧
·发布于 Towards Data Science ·7 分钟阅读·2024 年 6 月 28 日
--

图片来源:Nubelson Fernandes 于 Unsplash
成为一名数据科学家并不容易,但一些关键的习惯在我的旅程中帮助了我,今天我将与大家分享这些习惯!
希望这些习惯是你在学习过程中可以养成的,因为我认为它们在长期内会让你成为一名优秀的数据科学家。
以行动为导向

图片来源:Jakob Owens 于 Unsplash
在学习数据科学的初期,直接投入其中就好。停止思考,开始行动。
不去过度思考,立即开始学习是最好的方法,几乎每个人都这么说。
举个例子,我总是提到没有“最好的课程”这种说法。确实,有些教程比其他的更好,但任何入门级课程通常都会涵盖相同的内容。
5 个关于生成式 AI 的严峻真相,科技领导者必看
产生真正商业价值的 GenAI 需要真正的努力。但这是值得的。
·发表于 Towards Data Science ·8 分钟阅读·2024 年 1 月 4 日
--

原始图片来自 The Everett Collection 在 Shutterstock 上。图片由作者编辑。
GenAI 无处不在,来自各行各业的组织都在施加压力,要求他们的团队加入这场竞争——77%的商业领袖 担心他们已经错失了 GenAI 的好处。
数据团队正在紧急响应这个需求。但要构建一个真正能推动商业价值的生成式 AI 模型是困难的。
从长远来看,快速与 OpenAI API 集成是行不通的。这是 GenAI,但它有什么护城河呢?为什么用户要选择你而不是 ChatGPT?
那种简单的勾选框式集成看似是前进的一步,但如果你还没有考虑如何将大语言模型(LLMs)与你的专有数据和商业背景连接起来,真正驱动差异化价值,那你已经落后了。
这不是夸张。我这一周仅在这个话题上就与半打数据领袖进行了对话。没有一个人没意识到这是一场竞赛。终点线前会有赢家和输家。那些是大片公司和 Netflix。
如果你觉得起跑枪已经响起,但你的团队还在起跑线前拉伸,讨论着“泡沫”和“炒作”,我总结了 5 个严峻的真相,帮助你打破自满情绪。
硬真相 #1:你的生成性人工智能功能没有被很好地采用,你的变现速度很慢。
“Barr,如果生成性人工智能如此重要,为什么我们目前实施的功能接受度如此低?”
好吧,有几个原因。其一,你的人工智能项目并不是对一系列明确的用户问题的回应。对大多数数据团队来说,原因在于你们都在赶时间,而这个过程还处于早期阶段,你们希望先获得一些经验。
然而,不久之后,你的用户会遇到一个最适合通过生成性人工智能解决的问题,而当那时发生——与你的“虎队”集思广益,探讨如何将生成性人工智能与用例结合相比,你会获得更好的采纳率。
而且因为处于早期阶段,已经整合的生成性人工智能功能只是“ChatGPT,但在这里而已”。
让我举个例子。想象一下你每天可能会用到的一个生产力应用,用来分享组织知识。这样的应用可能会提供一些命令执行功能,比如“总结此内容”、“延长内容”或“改变语气”等,用于处理结构化较差的文本块。一个命令等于一个 AI 信用点。
是的,这很有帮助,但它并没有什么差异化。
也许团队决定购买一些 AI 信用点,或者他们只需切换到 另一个标签页并向 ChatGPT 提问。我不想完全忽视或低估不将专有数据暴露给 ChatGPT 的好处,但这也是一种较小的解决方案和愿景,而非那些在全国各地的财报电话会议中描绘的愿景。

从概念到价值的那个恼人的中间步骤。图片由 Joe Reis 在 Substack提供。
那么请考虑一下:你的生成性人工智能的差异化和附加值是什么?让我给你一个提示:高质量的专有数据。
这就是为什么 RAG 模型(或有时是微调过的模型)对于生成性人工智能项目如此重要的原因。它为大语言模型(LLM)提供了对企业专有数据的访问权限。下面我会解释原因。
硬真相 #2:你害怕在生成性人工智能(Gen AI)上做得更多。
事实确实如此:生成性人工智能让人感到畏惧。
当然,你可以将你的 AI 模型更深入地整合到你所在组织的流程中,但这感觉风险很大。让我们面对现实:ChatGPT 会产生幻觉,而且是不可预测的。它存在知识截止的问题,使得用户容易得到过时的输出。数据处理不当和向消费者提供错误信息的法律后果,即使是无意的,也是存在的。

听起来足够真实,对吧?Llama 2 确实这么认为。图片由 Pinecone提供。
你的数据失误会带来后果。这也是为什么必须准确了解你在输入给生成性人工智能的数据,以及确保数据的准确性。
在我们向数据领导者们发送的匿名调查问卷中,我们询问了他们的团队距离实现生成型人工智能(GenAI)应用案例有多远,其中一位回答说:“我认为我们的基础设施并不是阻碍我们的因素。我们在这里非常谨慎——随着形势发展如此迅速,且‘失控’聊天机器人的声誉风险,我们决定按兵不动,等待一阵炒作平息。”
这是我与许多数据领导者交流时普遍感受到的情绪。如果数据团队突然展示了面向客户的安全数据,那么他们就得为此负责。数据治理是一个巨大的考虑因素,而且这是一个高门槛。
这些是真实的风险,需要解决方案,但你不能通过袖手旁观来解决它们。还有一个实际的风险,就是眼睁睁看着你的业务被率先解决这些问题的团队彻底颠覆。
通过微调和 RAG 将大型语言模型(LLMs)与你的专有数据结合,是这个难题中的一大关键部分,但这并不容易……
严峻的事实#3:RAG 很难。
我相信 RAG(检索增强生成)和微调是企业生成型人工智能未来的核心组成部分。但尽管在大多数情况下 RAG 是较为简单的方法,开发 RAG 应用仍然可能很复杂。

我们就不能直接开始使用 RAG 吗?这有何大不了的?图片来源于 Reddit。
RAG 看起来可能是定制大型语言模型(LLM)的显而易见解决方案。但即使对于最优秀的数据工程师来说,RAG 的开发也伴随着学习曲线。他们需要了解提示工程、向量数据库与嵌入向量、数据建模、数据编排、数据管道……这些都是为了 RAG 而需要掌握的。而且,因为它是新的(Meta AI 于 2020 年提出),许多公司尚未积累足够的经验,无法建立最佳实践。

RAG 应用架构。图片来源于 Databricks。
以下是 RAG 应用架构的过度简化版:
-
RAG 架构将信息检索与文本生成模型结合,因此它可以在尝试回答用户问题时访问你的数据库。
-
数据库必须是一个可信的来源,包含专有数据,并允许模型将最新且可靠的信息纳入其回答和推理过程中。
-
在后台,数据管道将各种结构化和非结构化的数据源摄取到数据库中,以保持其准确性和最新性。
-
RAG 链接接受用户查询(文本),从数据库中检索相关数据,然后将这些数据和查询传递给 LLM,以生成高度准确且个性化的响应。
这个架构有很多复杂之处,但它确实带来了一些重要的好处:
-
它将您的 LLM 基于准确的专有数据,从而使其变得更加有价值。
-
它将模型带到数据中,而不是将数据带到模型中,这是一种相对简单且具有成本效益的方法。
我们可以看到这在现代数据堆栈中成为现实。最大的参与者正在以惊人的速度工作,努力通过在其环境中提供 LLM 来简化 RAG,使得企业数据得以存储。
Snowflake Cortex 现在使组织能够直接在 Snowflake 中快速分析数据并构建 AI 应用程序。Databricks 新发布的 Foundation Model APIs 提供了直接在 Databricks 内访问 LLM 的即时能力。微软发布了 Microsoft Azure OpenAI 服务,而亚马逊最近推出了 Amazon Redshift 查询编辑器。

Snowflake 数据云。图片来自 Umesh Patel,来源于 Medium。
我相信所有这些功能都有很大机会推动高采纳率。但它们也加大了对这些数据存储中数据质量的关注。如果为您的 RAG 管道提供数据的数据存在异常、过时或其他不可靠的情况,您的生成式 AI 项目的未来将会如何?
严酷的事实 #4:您的数据反正还没准备好。
仔细审视您的数据基础设施。很有可能,即使您明天就能拥有一个完美的 RAG 管道、经过精调的模型和清晰的使用案例 (而且 那岂不是太好了?),您仍然无法将这些都与干净、良好建模的数据集结合使用。
假设您希望您的聊天机器人与客户进行交互。为了完成任何有用的操作,它需要了解该组织与客户之间的关系。如果您今天是一家企业组织,那么这种关系可能跨越 150 个数据源和 5 个孤岛数据库…其中 3 个仍然是在本地部署。
如果这描述了您的组织,那么可能您距离让数据基础设施具备 GenAI 准备状态还有一年(或两年!)。
这意味着,如果你希望很快能在生成式 AI 上做点什么,你需要在现代数据平台中创建有用、可靠、高度整合且文档完善的数据集……最好是昨天就开始做了。否则,当教练把你叫上场时,你的裤子可能还没穿好。
你的数据工程团队是确保数据健康的支柱。而现代数据架构使数据工程团队能够持续监控数据质量,确保未来的数据健康。
硬真相 #5:你可能在不知情的情况下忽视了关键的生成式 AI 参与者。
生成式 AI 是一项团队运动,尤其是在开发方面。许多数据团队犯了将关键成员排除在生成式 AI 精英团队之外的错误,而这从长远来看会给他们带来损失。
谁应该加入 AI 精英团队?领导层或主要业务利益相关者应率先推动这一举措,并提醒团队关注业务价值。软件工程师负责开发代码、用户端应用和 API 调用。数据科学家则考虑新的使用案例,优化模型,并推动团队朝新的方向发展。那么,谁还缺席呢?
数据工程师。
数据工程师对生成式 AI 项目至关重要。他们能够理解那些提供相对于 ChatGPT 的竞争优势的专有业务数据,并且他们将构建数据管道,使这些数据通过 RAG(检索增强生成)提供给大型语言模型(LLM)。
如果你的数据工程师没有参与其中,那么你的精英团队就没有充分的实力。那些在生成式 AI 领域最具创新性的公司告诉我,他们已经在所有开发小组中嵌入了数据工程师。
赢得生成式 AI 竞赛
如果这些硬真相适用于你,不必担心。生成式 AI 仍处于初期阶段,仍然有时间重新开始,这次要勇敢面对挑战。
后退一步,理解 AI 模型可以解决的客户需求,将数据工程师带入更早的开发阶段,从一开始就确保竞争优势,并花时间构建一个能够提供高质量、可靠数据的 RAG 管道。
而且,投资现代数据架构,将数据质量放在优先位置。因为没有高质量数据的生成式 AI,只是一堆空洞的噱头。
提升你数据科学水平的 5 项关键软技能
数据科学家应高度重视的软技能,以便在人群中脱颖而出
·发表于Towards Data Science ·10 分钟阅读·2024 年 4 月 5 日
--
在这个故事中,我将与大家分享我认为数据科学家应高度重视的五项软技能,以帮助他们在人群中脱颖而出。这些观察基于我过去十年的经验,与数据科学家一起工作并管理他们的经历。

图片来自Gary Butterfield在Unsplash上的分享
1. 好奇心
我不会让你对这个(或下一个)感到措手不及,但好奇心仍然是数据科学家(以及许多其他职业)的一个关键特征。
保持学习
数据科学家应该非常重视好奇心,因为好奇心能够促进在快速发展的数据科学领域中的持续学习和适应。
由于研究论文和会议中不断涌现创新,拥有好奇心的思维方式鼓励数据科学家紧跟新兴趋势,拥抱新工具,并不断提升自己的技能。
在我的职业生涯中,我曾有过几次专注于一个话题数月的经历,低头工作,忘记了其他一切。在某个时刻,我的好奇心把我拉回现实,促使我睁开眼睛,去寻找这段时间领域里发生了什么。如果没有它,我可能会陷入困境,变得过时。
跳出框框思考
此外,好奇心也是解决问题时创意的来源。
数据科学常常面临复杂的挑战,需要创新的解决方法,同时也常常是寻找将旧有并已验证的方法应用到新领域的创新方式,这些方法之前并未尝试过。
因此,好奇的头脑更有可能跳出框框,拓宽视野,尝试新事物。这就是你解决问题所需要的一切!
创造力并不单单来自于好奇心。例如,我认为由具有不同背景的人组成的团队更有可能激发创造力。因此,如果你是经理,考虑招聘与自己或同事截然不同的人,对不寻常的职业道路保持好奇心,你可能会发现一些珍贵的人才。
如何增强你的好奇心?
- 阅读
你应该多读博客和书籍(或任何你喜欢的其他阅读/收听形式)。你应该选择一些自己不熟悉的话题,超出你通常阅读的内容。如果你有科学背景,可以阅读有关商业、沟通、决策等方面的内容。
我喜欢阅读我感兴趣的话题,比如篮球、数据科学或数据工程。然而,我也尝试阅读更多关于软件工程、市场营销、创业、生物学、个人理财等方面的内容。
2. 跳出你的舒适区
抓住任何离开舒适区的机会,学习新事物。如果你采取一种对所有事物都感兴趣、对每个人都值得倾听的态度,你将扩展自己的视野,并开始质疑自己在专业领域之外的事情。

照片由Panos Sakalakis拍摄,来源于Unsplash
2. 沟通
在任何职业中,你可能都遇到过与一个在有效沟通方面存在困难的人合作的挑战。这个问题在数据科学领域尤为突出,因为精确地传达技术概念是一项艰巨的任务。
适应你的听众
数据科学家往往处于多个团队的交叉点,需与各种利益相关者及背景各异的人合作。因此,弥合知识差距,确保每个人对所说的内容有共同的理解非常重要。
没有清晰的沟通,合作变得迅速不可能,甚至可能危及到一个项目、一个团队,或者你的工作。
在我之前的经验中,我一直对那些能够在不使用任何技术术语的情况下表达自己当前工作的人的能力感到印象深刻。对我来说,这能很好地反映出一个人对主题的掌控程度,并且在讨论之前,已经对其有了全面的理解。
高效地传达成果
即使只有 1%的项目时间用于沟通成果,这看似微小的部分,往往包含了 99%的价值。
可惜的是,我注意到许多初级数据科学家会花费数周时间完善他们的技术方法或数据分析,但在传达有影响力的见解时却因为沟通不力而失败。
如何提高你的沟通能力?
- 实践
你应该把每一个与人互动的机会都看作是练习和提升你沟通技巧的机会。
例如,如果你有日常会议,提前准备几分钟,确保能够清晰地传达信息给你的听众。
每当我有一个“重要”的会议时,我会花 5 分钟准备。我的准备方法如下:
-
花 1 分钟进行颧骨热身(这有助于改善发音)->
preply.com/en/blog/english-pronunciation-practice/ -
定义我的目标
-
确定我的听众对听我讲话的兴趣点
-
写下我发言的关键主题或结构化总结
永远假设他们对你所做的事情一无所知,并帮助他们填补知识空白,让他们对你所说的内容感到舒适。
同时,向他们寻求反馈:你记得我今天的主要目标是什么吗?是否足够清晰?
资源:
2. 阅读
阅读关于沟通的书籍!我强烈推荐从 Chip 和 Dan Heath 的《Made To Stick》开始,这本书提供了一种使你的想法更具记忆性和影响力的方法。

图片由 Seema Miah 提供,来源于 Unsplash
你也可以在互联网上找到许多有趣的免费资源,比如 online.hbs.edu/blog/post/communication-techniques
3. 科学思维方式
很容易忘记“数据科学家”这一角色名称中的“科学家”部分,但非常重要的一点是,数据科学家必须运用科学的方法来利用数据解决问题。
科学严谨性构成了数据科学的基础,确保分析结果是稳健、可靠且可重复的。应用科学严谨性主要是指遵循严格的方法论,并对发现进行批判性评估。
你可以成为一名优秀的软件工程师,而不需要应用任何科学的方法论。但如果没有这些方法论,你就不可能成为一名优秀的数据科学家。
在我看来,这不是一个选择。即使你不是研究科学家,运用科学的严谨性对减少错误结论的风险也至关重要。这就是为什么,每当我与同事讨论实验和结果时,我倾向于在初期阶段更看重科学方法而非结果。
拥抱更高科学严谨性的技巧
- 问题定义
说起来容易做起来难。但如果你解决的是错误的问题,那么你将一事无成,浪费时间。不幸的是,大多数人都会选择第一个可行的问题版本,因为他们更喜欢编写代码。因此,解决方案很简单,投入更多时间来定义问题,和相关利益方讨论期望结果,并设置合适的初步假设/约束。
2. 统计学
统计学是数据科学家硬技能的三大支柱之一。而任何科学方法都需要合理使用统计工具。例如,统计检验可以帮助你检查特征相关性或数据分布。因此,如果你不熟悉这些工具,可以考虑提升你的统计武器库。
你可以立即在 Medium 上开始:towardsdatascience.com/ultimate-guide-to-statistics-for-data-science-a3d8f1fd69a7
我还推荐这本全面的书籍:www.oreilly.com/library/view/practical-statistics-for/9781492072935/
3. 工具
人们往往会尝试同时做多件事以节省时间。但逐步进行,每次评估一件事是必须的,这样才能确保你理解正在发生的事情,并得出正确的结论。
使用合适的工具可以显著简化过程。这正是“实验跟踪”工具的功能,随着数据科学领域的不断发展,这些工具的使用越来越广泛。
对于像 Kaggle 竞赛这样的个人项目,我喜欢使用DVC,它几年前引入了实验跟踪功能。然而,市场上也有许多更先进的工具,如MLFlow和Neptune.ai。
你可以在这里找到一个关于实验跟踪工具的全面比较:towardsdatascience.com/a-comprehensive-comparison-of-ml-experiment-tracking-tools-9f0192543feb
但是,你不需要复杂的工具来记录你的想法、问题和实验。所以我建议至少从用记事本写下内容开始。

图片来源:Brett Jordan在Unsplash
4. 诚信
正如任何数据科学家都清楚的那样,数据分析的结果会根据呈现者希望传达给观众的叙事而大不相同。
他是在用数据撒谎吗?
不可否认的是,从数据集中得出的结论本质上会受到呈现者的观点和意图的影响。这个简单的事实凸显了诚信的重要性。
资源:
-
《如何用统计学撒谎》by Darrell Huff
-
towardsdatascience.com/lessons-from-how-to-lie-with-statistics-57060c0d2f19
拥有巨大权力的同时也意味着巨大的责任。但,这并不是我认为诚信对我重要的唯一原因。
挑战模型偏见
模型中的偏见也是每个数据科学从业者应该关心的一个如今广为人知的问题。
解决这个问题是一个艰难的任务,但每个人都应该关注,因为它可能带来的商业影响,更重要的是,一个有偏见的模型可能对社会造成的潜在影响。
相关的有趣资源:
如果你像我一样是 AWS 用户,Sagemaker Clarify提供了多种偏见检测分析工具。
如果你对深度学习的公平性感兴趣,可以仔细查看DEEL 实验室的出版物,以及他们的开源解决方案,如Influencia。
环境影响
数据科学的另一个伦理方面是它的环境影响,这个问题由于测量和理解的复杂性,通常被忽视。
我猜这个领域的研究正在扩展,如果你有相关材料愿意与我分享,我会非常感激。
数据科学家必须积极评估和减少他们的环境影响。例如,他们必须质疑处理大量数据是否对于实现预期的商业目标至关重要。
此外,他们应该探索减少模型环境影响的方法。然后,与利益相关者分享结果和观点,将扩大对数据驱动决策中固有环境影响的认识。
这个问题还有更多的维度,我会在未来的故事中进一步思考。
有趣的资源:
-
像 AWS 这样的编辑可能会提供类似于
aws.amazon.com/aws-cost-management/aws-customer-carbon-footprint-tool/的工具。
如果你想开始思考你的代码碳足迹:
-
了解你的代码在哪些地方低效也是一个很好的起点:
pyinstrument.readthedocs.io/en/latest/
你如何保持你的诚信?
诚信就是对自己诚实,在价值观上保持一致,并根据自己的原则行事。因此,首先要问自己的是:你的核心价值观是什么,以及如何在日常工作中最好地体现它们。
然后,抵制外部压力,保持忠于自己。同时,不要忽视领域中的伦理挑战。这些问题对社会来说日益重要,我们有责任提供解决方案。

图片来源:Afif Ramdhasuma 在 Unsplash
5. 以价值为中心
所有数据科学家都喜欢探索数据和构建模型。这也是 Kaggle 如此受欢迎的原因。作为专业人士,很容易被无尽的数据探索、无限的实验或模型优化所困住。
以价值为中心指的是一种方法或心态,它强调将提供价值作为决策、问题解决和整体战略中的主要目标。
因此,在数据科学的背景下,作为以价值为中心意味着你必须保持专注,并运用你的技能创造价值,而不是将时间浪费在那些你希望通过解决技术问题来达成最佳解决方案的事情上。
再次强调,我合作过的最优秀的数据科学家都是有目的地探索数据,提出并回答那些能帮助他们解决正确问题的问题。然后,他们进行最小限度的实验,以得出解决方案并构建 MVP。他们迅速投入生产,看看会发生什么,并进行迭代。
这条路径由在优化事物和为最终用户增加增量价值之间的无数权衡组成。
如何专注于价值?
作为以价值为中心的一部分,最难的一点是认识到,即使你不是自己构建一个全面的数据产品,你也在构建其中的一部分,因此你必须遵循产品思维,并专注于最终为终端用户创造的价值。
你的决策应该始终评估完成某事所需的时间与它在产品中所提供的价值之间的对比。有些事情很重要,但可以推迟到未来的迭代中做,另一些事情则不值得做。
在构建数据科学模型时,通常可以根据预期的性能以及模型如何影响业务,快速评估一个模型是否能提供足够的价值。
例如,如果你不熟悉如何构建基于非技术指标评估模型的自定义评分函数,可以查看这个:towardsdatascience.com/calculating-the-business-value-of-a-data-science-project-3b282de9be3c
在某些时刻,你也可能面临提高模型性能的机会。这个时候正是理解哪些改进会有价值的时候。相比你需要付出的努力,提高 0.01 的 F1 分数值得吗?这是否需要收集 10 万条新的标注数据?
由于产品负责人和其他软件工程师可能无法理解开发模型的所有技术细节,因此做出这些决策是你的责任。
其他有趣的资源:
- 工作伦理的概念包含了价值和责任的思想:
hbr.org/2022/09/how-to-develop-a-strong-work-ethic
结论
要成为一名更好的数据科学家,你应该专注于培养好奇心、沟通能力、诚信、科学思维和以价值为中心的理念。
大多数时候,我会推荐阅读书籍,但也有许多其他有价值的资料,比如在线课程和博客文章。这些技能中的一些只能通过面对现实来发展。因此,要对你将面临的机会保持警觉并做好准备。
如果你想收到关于数据科学等方面的最新文章通知,请在这里订阅或关注我。
你知道你可以拍手多次吗?
5 个你应该使用的 Snowflake 查询技巧
更少的行数,和更快的执行时间,还有一些 NLP
·发表于Towards Data Science ·阅读时长 5 分钟·2024 年 1 月 8 日
--

Snowflake 是一个云计算数据解决方案,允许用户在其云平台上存储数据并直接运行查询,可以通过网页浏览器直接访问。它常用于数据存储和自动扩展功能,其中集群会自动启动和停止,以管理查询负载。
在本系列的第一部分,我们探讨了 Snowflake 如何具有独特的查询语法,而这些语法在其他数据库系统如 PostgreSQL 或 MySQL 中可能无法找到。当正确使用这些子句时,它们有助于提高语法和可读性,最重要的是,减少计算成本和执行时间。
在下文中,我们将介绍 5 个这样的子句,包括一些字符串处理函数,您可能最初会认为这些需要使用 Python 或 R 来完成。
1. MAX_BY / MIN_BY
min_by和对应的 max_by 子句允许我们找到与给定列的最小/最大值对应的行,并返回该行中另一个列的值。
一个常见的方法是先在子查询中找到最小值,然后将其作为筛选条件。例如…
5 种你必须尝试的 PCA 可视化方法,用于下一个数据科学项目
哪些特征具有最大的权重?原始特征如何贡献于主成分?这五种可视化类型能给出答案。
·发布于 Towards Data Science ·阅读时长 8 分钟·2024 年 8 月 2 日
--

图片来源:Andrew Neel 在 Unsplash
主成分分析(PCA)能告诉你很多关于数据的信息。简而言之,它是一种降维技术,用于将高维数据集转换到一个可以可视化的空间。
但我猜你已经知道这一点了。 如果不知道,可以查看我的从零开始指南。
今天,我们只关注视觉效果。到文章结束时,你将学会如何创建和解读:
-
解释方差图
-
累计解释方差图
-
2D/3D 组件散点图
-
属性双变量图
-
加载得分图
开始 — PCA 可视化的先决条件
我很想立刻深入可视化的内容,但你需要数据才能跟上。 本节将介绍数据加载、预处理、PCA 拟合和一般的 Matplotlib 样式调整。
数据集信息
超优化 AI 工作流的 5 个支柱
创建适用于生产的、可扩展且高度优化的 AI 工作流的方法论简介
·发布于 Towards Data Science ·阅读时间:7 分钟·2024 年 9 月 3 日
--

致谢:Google Gemini,作者提供的提示
引言
在过去十年中,每个我参与的项目中,我心里都怀揣着一个深刻的问题:
我该如何(究竟怎么)构建和开发我的 AI 和机器学习项目?
我想知道——是否有一种优雅的方式,以迭代的方式构建适用于生产的代码?一种可扩展、优化、可维护且可重现的代码库?
那么,这个秘密到底在哪里呢?谁掌握了这门黑暗艺术的知识?
多年来,我一直在全力以赴寻找答案——阅读文章、观看教程,并尝试不同的方法论和框架。但我始终未能找到一个令人满意的答案。每当我觉得自己接近解决方案时,总有某些东西仍然缺失。
经过大约 10 年的试验与错误,尤其是在过去两年专注努力之后,我认为我终于找到了一个令人满意的答案,结束了我长期以来的追寻。这篇文章是我分享所发现内容的旅程的开始。
我的研究让我识别出了5 个关键支柱,它们构成了我所称之为超优化 AI 工作流的基础。在这篇文章中,我将简要介绍每个支柱——为你概述即将到来的内容。
我想强调的是,我将介绍的每个支柱都基于实际的方法和工具,我将在未来的文章中详细阐述。如果你已经迫不及待想看到它们的实际应用,可以查看我在 Hamilton 见面会上的这段视频,现场展示了这些内容:
注意:在本文及系列文章中,我将交替使用人工智能(AI)、机器学习(ML)和数据科学(DS)这几个术语。我们讨论的概念适用于所有这些领域。
现在,让我们探索每个支柱。
1 — 基于指标的优化
每个 AI 项目都有一个我们想要实现的目标,理想情况下——还有一组我们想要优化的指标。
这些指标可以包括:
-
预测质量指标:准确率、F1-Score、召回率、精确率等……
-
成本指标:实际金额、FLOPS、大小(以 MB 为单位)等……
-
性能指标:训练速度、推理速度等……
我们可以选择一个指标作为我们的“北极星”,或者创建一个综合指标。例如:
-
0.7 × F1-Score + 0.3 × (1 / 推理时间(毫秒))
-
0.6 × AUC-ROC + 0.2 × (1 / 训练时间(小时)) + 0.2 × (1 / 云计算成本(美元))
有一个 安德鲁·吴的精彩短视频 ,在视频中他解释了 单一数字评估指标 的话题。
一旦我们确定了一个需要优化的指标并设定了要满足的约束条件,我们的目标是构建一个在满足约束条件的同时最大化该指标的工作流。
2 — 交互式开发者体验
在数据科学和 AI 开发的世界里——交互性是关键。
作为 AI 工程师(或者我们数据科学家现在使用的任何头衔),我们需要编写在不同场景下都能无错运行的代码。
与传统软件工程不同,我们的角色不仅仅是编写“能工作”的代码。我们工作的重要部分是审视数据,检查模型的输出和各个处理步骤的结果。
最常见的交互式探索环境是 Jupyter 笔记本。
在笔记本环境中工作允许我们测试不同的实现,尝试新的 API,检查工作流的中间结果,并基于我们的观察做出决策。 这是第二个支柱的核心。
然而,尽管我们在日常工作中享受这些好处,笔记本有时也会包含臭名昭著的坏代码,这些代码只能按非平凡的顺序执行。
此外,笔记本中的一些探索性部分可能与生产环境无关,因此不清楚这些部分如何有效地交付到生产中。
3 — 生产就绪代码
“生产就绪”在不同的上下文中可能意味着不同的事情。对于一个组织来说,它可能意味着在指定的时间范围内提供结果。对于另一个组织来说,它可能指的是服务的正常运行时间(SLA)。而对于第三个组织,它可能意味着代码、模型或工作流经过充分测试,以确保其可靠性。
这些都是交付可靠产品的重要方面,具体要求可能因地而异。由于我的探索侧重于构建 AI 工作流的“元”层面,我想讨论这些定义之间的共同点:将我们的工作流封装为一个可服务的 API,并将其部署到一个可以被外部应用程序或用户查询的环境中。
这意味着我们需要有一种方法,将代码库的复杂性抽象为一个明确定义的接口,以便在各种用例中使用。让我们考虑一个例子:
想象一下,我们开发了一个复杂的 RAG(检索增强生成)系统,运行在 PDF 文件之上。它可能包含 10 个不同的部分,每个部分由数百行代码组成。
然而,我们仍然可以将它们包装成一个简单的 API,只有两个主要功能:
-
upload_document(file: PDF) -> document_id: str
-
query_document(document_id: str, query: str, output_format: str) -> response: str
这种抽象使用户能够:
-
上传 PDF 文档并接收一个唯一的标识符。
-
使用自然语言提问关于文档的问题。
-
指定所需的响应格式(例如,Markdown、JSON、Pandas DataFrame)。
通过提供这个简洁的接口,我们有效地隐藏了工作流的复杂性和实现细节。
拥有一种系统化的方式,将任意复杂的工作流程转换为可部署的 API,是我们的第三个支柱。
此外,我们理想中希望建立一种方法,确保我们的迭代性日常工作与生产代码保持同步。
这意味着如果我们对工作流进行更改——修复一个 bug、添加新的实现,甚至调整一个配置——我们应该能够通过点击一个按钮,将这些更改部署到生产环境中。
4 — 模块化和可扩展的代码
我们方法论的另一个关键方面是保持模块化和可扩展的代码库。
这意味着我们可以添加新的实现,并与现有的实现进行对比测试,这些实现占据了相同的逻辑步骤,而无需修改现有的代码或覆盖其他配置。
这种方法与开放-封闭原则相一致,其中我们的代码可以扩展,但不能修改。它使我们能够:
-
在现有实现的基础上引入新实现
-
容易比较不同方法的性能
-
维护当前工作解决方案的完整性
-
在不危及整个系统稳定性的情况下扩展我们工作流的功能
让我们看一个简单的例子:

作者提供的图片

作者提供的图片
在这个示例中,我们可以看到一段模块化和可配置的(伪)代码。通过这种方式,我们可以轻松地添加新的配置并测试它们的性能:

作者提供的图片
一旦我们的代码由多个竞争的实现和配置组成,我们进入了一个我称之为“工作流的叠加态”的状态。在这种状态下,我们可以使用特定的配置集来实例化和执行工作流。
5 — 分层和可视化结构
如果我们进一步推进模块化和可扩展性呢?如果我们将这种方法应用于我们工作流的整个部分呢?
所以现在, instead of configuring this LLM or that retriever, 我们可以配置整个预处理、训练或评估步骤。
让我们看一个例子:

作者图片
这里我们看到整个机器学习工作流。现在,让我们添加一个新的数据准备实现并放大查看:

作者图片
当我们以这种分层和可视化的方式工作时,我们可以选择工作流中的某一部分进行改进,并添加一个与现有部分具有相同输入/输出接口的新实现。
然后我们可以“放大”到这个特定部分,专注于它,而不必担心项目的其他部分。一旦我们对实现感到满意——我们就可以开始在工作流中的其他不同配置下进行测试。
这种方法解锁了几个好处:
-
减少心理负担: 每次专注于一个部分,提供清晰度并减少决策时的复杂性。
-
更容易的协作: 模块化结构简化了任务分配给队友或 AI 助手,并且每个组件都有清晰的接口。
-
可重用性: 这些封装的实现可以在不同的项目中使用,可能无需修改其源代码。
-
自我文档化: 可视化整个工作流及其组件,使得理解项目的结构和逻辑变得更加容易,而无需深入到不必要的细节。
总结
这些是我认为构成“超优化 AI 工作流”基础的五大支柱:
-
基于指标的优化: 定义并优化清晰的、项目特定的指标,以指导决策和工作流的改进。
-
互动开发者体验: 使用像 Jupyter Notebooks 这样的工具进行迭代编码和数据检查。
-
生产就绪代码: 将完整的工作流封装成可部署的 API,并同步开发与生产代码。
-
模块化和可扩展代码: 结构化代码,使其能够轻松添加、替换和测试不同的实现。
-
分层和可视化结构: 将项目组织为可视化的分层组件,这些组件可以独立开发,并且在不同的抽象层次上易于理解。
在接下来的博客文章中,我将深入探讨这些支柱,提供更详细的见解、实际示例和工具,帮助你在自己的 AI 项目中实现这些概念。
具体来说,我打算介绍我在DAGWorks Inc* Hamilton 框架之上构建的方法论和工具,以及我自己的软件包:Hypster和HyperNodes(目前仍处于早期阶段)。
敬请期待更多内容!
*我与 DAGWorks Inc 没有任何隶属关系,也没有为其工作。
确保人工智能不会夺走你工作的 5 种强大策略——一名 Spotify 数据科学家的生存指南
为什么我不再担心人工智能会取代我,而是做了这些事情
·发表于 Towards Data Science ·14 分钟阅读·2024 年 4 月 7 日
--
我们都知道,人工智能正在向我们逼近。
这不再是“是否”的问题,而是“何时”。人工智能何时会来夺走我们的工作,把我们挤出舞台?
似乎没有人是安全的。人工智能威胁要取代几乎所有行业的工作。无论是通过机器人技术、任务委派还是自动化,我们都看到过人工智能进行手术、创造电影等。
这仅仅是个开始。真不敢想象几年后它会变得多强大,最好做好准备,对吧?
可是,怎么做呢?你如何为未知做计划?在面临完全被取代的风险时,如何规划职业生涯?
最近,这个问题一直在我的脑海中萦绕,我对此进行了很多反思。

作者图片
解决方案是弄清楚人工智能将占据你工作中的多少部分,哪些部分仍然需要人类的参与
通过这次反思,得到的结论出奇地积极,因为我不再感到害怕。我已经驯服了那些恐惧……
你可以在机器学习背景下使用的 5 个项目管理框架
在这篇文章中,我们将讨论 5 个你可以在数据科学和机器学习背景下使用的著名项目管理框架:敏捷(Agile)、瀑布(Waterfall)、SEMMA、CRISP-DM 和 KDD
·发表于 Towards Data Science ·阅读时间 8 分钟 ·2024 年 4 月 26 日
--

图片来自 joszczepanska @Unplash.com
好吧,我承认,项目管理并不是机器学习和数据科学中最有趣的概念,特别是对于技术用户来说。但当谈到机器学习项目失败的原因时,糟糕的项目管理很可能排在前 5 名,或者至少会影响到其他一些在前 5 名中出现的原因(如利益相关者管理不善、没有为部署留出时间等)。
为什么在机器学习和数据科学的背景下项目管理如此困难?主要原因是,通常情况下,项目的结果充满了不确定性——在其中一些项目中,与研究的交集很大,这个灰色区域需要大量的实验。这可能与大多数项目管理框架要求严格的时间表以确保项目遵循时间和预算的要求直接冲突。
在这篇博客文章中,我们将探索 5 个著名的项目管理框架,这些框架可以应用于…
提升 RAG 表现的 5 种经过验证的查询翻译技术
如何在面对模糊的用户输入时,获得近乎完美的 LLM 表现
·发表于Towards Data Science ·10 分钟阅读·2024 年 8 月 8 日
--

图片来自travelnow.or.crylater于Unsplash
如果你认为用户会向 LLM 提出完美的问题,那你就大错特错了。与其直接执行,不如我们先优化用户的问题。这就是查询翻译。
我们开发了一个应用,允许用户查询公司曾经制作的所有文档。这些文档包括 PPT、项目提案、进度更新、交付物、文档等。这一尝试非常值得注意,因为过去许多类似的尝试都未能成功。多亏了 RAGs,这一次,结果非常有希望。
我们做了一个演示,大家都很兴奋地想要使用它。最初的推出是针对一小批选定的员工。但我们注意到的结果对我们来说并不那么令人兴奋。
这一举措本应彻底改变我们的工作方式。但大多数用户只尝试了几次应用,之后便再也没有使用过。他们退出了应用,就像这是一个为小学生准备的玩具项目一样。
日志显示了令人满意的结果。然而,我们与真正使用该应用的用户进行了交谈,以确定真正的问题。我们从中学到的经验让我们开始思考如何通过查询翻译技术来克服用户输入中的模糊性。
启动数据探索的 5 个 Python 一行代码
如何使用一行代码启动你的 EDA
·发表于 Towards Data Science ·6 分钟阅读 ·2024 年 12 月 6 日
--

图片来自 Lukas Blazek 在 Unsplash 上
探索性数据分析
当谈到机器学习时,探索性数据分析(EDA)是你在将数据收集并加载到 Python 中后需要做的第一件事之一。
EDA 涉及:
-
通过描述性统计总结数据
-
可视化数据
-
识别模式、检测异常和生成假设
通过 EDA,数据科学家能够深入了解数据,从而评估数据质量,并为更复杂的机器学习任务做好准备。
但有时在刚开始时,可能会遇到挑战,不知道从哪里入手。
这里有 5 个简单的 Python 一行代码,能帮助你快速启动 EDA 过程。
1. df.info()
这是每个 EDA 过程中的必备步骤。事实上,这是我加载数据框(df)后运行的第一行代码。
它告诉你:
-
列的名称
-
每一列中有多少个非空值
-
列的数据类型
你需要知道的 5 个 Redshift SQL 函数
包括如何使用这些函数的代码示例
·发表于Towards Data Science ·阅读时间:5 分钟·2024 年 3 月 8 日
--

如果你是 Redshift 的新用户,你可能会发现 SQL 语法与在其他数据仓库中编写的 SQL 有所不同。
每个数据仓库都有自己特定的 SQL 方言,Redshift 也不例外。
一开始,发现你最喜欢的函数不存在时,可能会感到沮丧。然而,Redshift 有很多你可以在代码中利用的优秀函数。
在这篇文章中,我将向你展示我在工作中发现的最有用的 Redshift 函数。每个函数都包括一个定义和代码示例,说明如何使用它。
PIVOT
PIVOT是 Redshift 内置的一个函数,它允许你进行数据透视。我的意思是什么呢?数据透视允许你重新组织数据,使得行中的值变成列,或列中的值变成行。
PIVOT 可以帮助你:
-
统计列中的值
-
聚合行值
-
基于列或行值派生布尔字段
我最近在 Redshift 中使用了 PIVOT 来查找不同页面是否对每个用户有效。为此,我需要对page_type字段进行 PIVOT 操作,并…
用 Python 构建美丽堆叠面积图的 5 个步骤
如何充分利用 Matplotlib 的全部功能讲述一个更具吸引力的故事
·发表于 Towards Data Science ·阅读时间 7 分钟·2024 年 1 月 13 日
--

美国按来源分类的电力生产 — 图片由作者提供
动机
当支撑故事的图表清晰、自解释且视觉上令人愉悦时,用数据讲述一个引人入胜的故事就变得容易得多。
在很多情况下,内容和形式同样重要。
数据呈现不佳的优秀数据无法引起应有的注意,而呈现精美的糟糕数据则容易被质疑。
希望这对许多数据分析师或任何曾经在众人面前展示过图表的人有所共鸣。
Matplotlib 提供了现成的函数来快速绘制数据,但精细调整却需要更多的努力。
我花费了相当多的时间研究使用 Matplotlib 构建吸引人的图表的最佳实践,目的是为你省去这些努力。
在这篇文章中,我聚焦于堆叠面积图,并解释了我如何将零散的知识拼接在一起,从而得出这个结果……
将混乱的函数转换为可生产的代码的 5 个步骤
数据科学家可扩展且可维护功能指南
·发表于 Towards Data Science ·阅读时长 11 分钟·2024 年 1 月 24 日
--

图片来源:作者
动机
函数在数据科学项目中至关重要,因为它们使代码更加模块化、可重用、可读且可测试。然而,编写一个杂乱无章的函数,试图做太多事情,可能会引入维护难题,并降低代码的可读性。
在以下代码中,函数impute_missing_values非常长且混乱,试图完成很多事情。由于有许多硬编码的值,其他人将无法在具有不同列名的 DataFrame 上重用此函数。
def impute_missing_values(df):
# Fill missing values with group statistics
df["MSZoning"] = df.groupby("MSSubClass")["MSZoning"].transform(
lambda x: x.fillna(x.mode()[0])
)
df["LotFrontage"] = df.groupby("Neighborhood")["LotFrontage"].transform(
lambda x: x.fillna(x.median())
)
# Fill missing values with constant
df["Functional"] = df["Functional"].fillna("Typ")
df["Alley"] = df["Alley"].fillna("Missing")
for col in ["GarageType", "GarageFinish", "GarageQual", "GarageCond"]:
df[col] = df[col].fillna("Missing")
for col in ("BsmtQual", "BsmtCond", "BsmtExposure"…
评估 ELT/ETL 工具时需要做的 5 件事
一份让评估 ELT/ETL 工具变得不那么令人生畏的清单
·发表于Towards Data Science ·阅读时间 4 分钟·2024 年 5 月 7 日
--

照片由Volodymyr Hryshchenko提供,来自Unsplash
我们都经历过:你参加了与所有 SaaS 数据集成工具公司销售代表的(许多!)会议,并获得了 14 天的试用期来体验他们的产品。现在你必须决定测试哪些内容,以便明确地判断这个工具是否适合你和你的团队。
我想整理一些关于关键评估问题的笔记,以及几种检查功能的方法,因为我确信这是一个我将会一次又一次遇到的过程,而我喜欢为这些类型的事情准备一个模板。
这些主要是针对基于云的集成平台(例如但不限于 Fivetran、Airbyte 和 Rivery)收集的,但也可以应用于其他情况!
如果你有任何测试新数据工具的好方法,欢迎在评论区分享!
1. 创建一个评分标准
你可以找到无数关于数据集成工具评估标准的文章(我个人很喜欢这篇!),但归根结底,这一切取决于你的数据平台以及你试图解决的问题。
集合团队并确定这些需求。显然,有一些功能是必需的,比如必需的源和目标连接器,这些可能是决定性因素,但也许你还在寻找一种提供数据血统(lineage)的元数据解决方案,或者希望增加监控,或者需要扩展某个自建的系统,但它现在已经无法独立运行了。
当你列出所有这些内容时,它也能帮助你将这些评估的工作分配给团队成员,进行并行处理。
2. 立即启动一个简单的管道
选择一个简单的例子,并在第一天就将其启动。这有助于创建一个关于日志记录、元数据、延迟、CDC 以及管道中其他所有内容的整体画面。
如果幸运的话,你可能在 14 天的测试期间遇到平台错误,并能看到该错误如何在工具公司那边得到处理。如果你使用的是开源选项,这也能帮助你了解自己是否有能力在内部处理这些问题。
关键问题
-
文档和用户界面是否能以直观的方式指导你完成权限和密钥设置、调度、架构设置等操作,还是需要你联系技术代表寻求帮助?
-
如果发生平台错误,是否能通过日志轻松发现,还是很难判断问题是出在你还是平台本身?
-
平台出现故障时,客户通知的速度如何,问题能多快解决?
3. 创建几个端到端的转换
一些工具带有内建的 DBT 集成,一些则允许完全自定义的基于 Python 的转换。将一些转换,甚至是一个相对复杂的转换,从现有解决方案中端到端地迁移,可以帮助你大致了解迁移的难度以及是否可能完成。
关键问题
-
你能否以目前的数据格式进行数据导入,还是会以某种方式发生变化,进而对上游依赖造成重大影响?
-
在数据导入之前,你是否有一些转换操作无法在工具中完成(例如,连接补充数据源、解析杂乱的多层级 JSON 等),这些操作现在是否需要在数据库中处理?
4. 输入一个非本地数据源
尝试处理来自不本地支持的源或格式的数据(可以模拟一些固定宽度的文件,或者选择一个以非常规方式导出数据的内部工具),或者至少与技术销售代表讨论一下如何处理。即便现在这不是一个问题,如果将来出现了,了解如何实现该功能是值得的。
关键问题
-
当遇到不受支持的源时,工具是否提供足够的灵活性,在其框架内构建解决方案?
-
当你开始为框架添加自定义功能时,是否仍然适用相同的日志记录、错误处理、状态管理等机制?
5. 强制产生一个错误
在你构建的测试管道中的某个环节,丢入一个格式错误的文件,向转换中加入错误代码,改变架构,或者以其他创意方式制造混乱,看看会发生什么。
像这样的第三方工具在某些方面可能是黑箱,当管道崩溃时,没有什么比不可理解的错误信息更令人沮丧了。
关键问题
-
错误信息和日志是否清楚地说明了哪里出了问题,出了什么问题?
-
一旦你修复了问题,管道中的数据会发生什么变化?是否会丢失数据,或者数据加载的次数比应该的更多?
-
是否有选项可以重定向错误数据并允许其他部分的管道继续运行?
一些额外的好处
让非技术用户导入 Google 表格
需要集成手动上传的电子表格中的数据,是比数据工程师通常认为的更常见的使用场景。一个工具应该让生产业务团队可以轻松地完成这一操作,而无需数据工程师介入。
阅读 Reddit 上关于该工具的讨论
我发现 Reddit 在查看工具选项时非常有用。人们通常会非常客观地评估工具的正面和负面体验,并且乐于回答问题。毕竟,即使是彻底的试用阶段也会错过一些东西,而这可以是一个轻松的方式,看看你是否有盲点。
5 个让你的简历在 FAANG 应聘中真正脱颖而出的技巧
如何在仅仅 6 秒钟内让招聘者说“WOW!”
·发表于 Towards Data Science ·阅读时长 8 分钟·2024 年 9 月 2 日
--
你是否厌倦了投递无数简历,却只收到寂静的回应?
也许你觉得自己陷入了一个无休止的申请和拒绝循环,并且你开始怀疑自己是否能够找到一份工作。
相信我,我完全理解你的感受,因为就在几年前,我和你处于同样的境地——我投递了数百份申请。我倾注了全部心血在每一份申请上,但最终得到的只有拒绝。
但事情是这样的:我逆转了局面,最终拿到了 Spotify 数据科学家的梦想职位。
怎么做到的?一切的转机开始于一份令人印象深刻的简历,它让我在人群中脱颖而出。

作者图片
在这篇文章中,我将分享改变我求职命运的五个强有力的技巧。此外,我还将揭示一个最终的秘密,这个秘密在我成功获得 Spotify 数据科学家职位时起到了至关重要的作用——这是我当时甚至没有意识到的……
5 种有用的可视化来增强你的分析
使用 Python 的统计可视化库 Seaborn 来提升你的分析。
·发表于 Towards Data Science ·7 分钟阅读·2024 年 3 月 24 日
--

介绍
Seaborn 已经存在很长时间了。
我敢打赌,它是最著名且最常用的数据可视化库之一,因为它对初学者友好,使非统计学家也能构建强大的图形,从而帮助提取有统计支持的见解。
我不是统计学家。我对这个主题的兴趣来源于数据科学。为了更好地完成我的工作,我需要学习统计概念。因此,我喜欢能轻松访问直方图、置信区间和线性回归的方式,并且代码量非常少。
Seaborn 的语法非常基础:sns.type_of_plot(data, x, y)。使用这个简单的模板,我们可以构建许多不同的可视化,例如 barplot、histplot、scatterplot、lineplot、boxplot 等。
但这篇文章并不是要讨论这些。它讨论的是其他几种增强型可视化,它们能够在你的分析中带来不同。
让我们看看它们是什么。
可视化
要创建这些可视化并与本练习一起编写代码,只需使用 import seaborn as sns 导入 seaborn 库。
5 种服务开源 LLM 的方法(带代码示例)
适用于 Llama 2 7B 的每种方法的代码和说明
·发表于Towards Data Science ·阅读时间:10 分钟·2024 年 4 月 9 日
--

在快速发展的大型语言模型(LLMs)领域,服务它们的工具和技术正以前所未有的速度进步,几乎与这些模型本身的速度一样快。与 xgboost 或 MNIST 分类器 CNN 等传统模型不同,LLM 模型在规模和复杂性上都要庞大得多,因此在部署时需要更加细致的关注。
本文的焦点集中在开源 LLM 上,它们因可调性和可操作性而脱颖而出,可能是最具优势的选择,因为它们允许任何人参与并推动这一领域的进步。
我的目标是引导你了解多种服务 LLM 的方法,满足不同的使用场景。我将介绍五种不同的选项,每种选项都附有详细的复制指南,并对其各自的优缺点进行全面分析。
我们将探讨本地部署和使用托管服务的选项。更重要的是,我们将讨论的这些服务提供了丰厚的免费额度,帮助你在不花一分钱的情况下进行实验。
以下是可用的选项:
-
本地服务器:Anaconda + CPU
-
本地服务器:Anaconda + GPU
作为领导者,你如何破坏人工智能的 5 个方式
那些正在偏离人工智能潜力并烧掉投资的关键错误
·发表于Towards Data Science ·阅读时间 7 分钟·2024 年 8 月 15 日
--
在最近几周,随着各种技术公告的发布,我们开始听到关于人工智能泡沫破裂的声音。
在此之前,我们迎来了那个姗姗来迟的词语“AI 洗牌”,用来形容公司为了利用人工智能的热潮,纷纷在财务报告和初创公司推介书中添加“人工智能”这个词。

图片由Francesca Zanette提供,来源于Unsplash
随着越来越多的人对人工智能能否兑现其承诺产生怀疑,公众的审视将转向这项技术、本身的能力以及开发它的人。
我希望把聚光灯照在商业领袖、创始人、CEO 以及决策者们身上,关注他们在人工智能投资成功或失败中的角色,以及过去两年我所见到的错误,这些错误将我们带到了今天的局面。
让我们从一个大问题开始,好吗?
1. 为了人工智能本身而优先考虑人工智能
每当一种新技术兴起时,都会出现这种情况。我称之为“闪亮物体综合症”,它与 FOMO(害怕错过)密切相关。我们渴望下一个大事件、最复杂的技术,以便跟上行业的步伐。而我们却没有放慢脚步,思考它的真正用途。
《50 次初吻》与 MemGPT
有时候,一个好故事(加上一些滑稽的喜剧)有助于我们理解复杂的问题。
·发布于 Towards Data Science ·阅读时间 11 分钟·2024 年 2 月 28 日
--

OpenAI. (2024). ChatGPT [大型语言模型]. /g/g-2fkFE8rbu-dall-e
注:本文最初发布于 pirate.baby,这是我在互联网上的小角落。现在在这里重新发布以扩大影响力。
前言
从电影院开车回家的路上不太顺利。那时我的女朋友坐在副驾驶座上,双臂交叉,交通信号灯的红光与她的心情相符。“你不能让它只是浪漫的。你为什么非得把一切都毁了!?!”我们刚刚看完了《50 次初吻》,这是一部经典的桑德勒/巴里摩尔浪漫电影,讲述了一位因脑部受伤无法形成长期记忆的女性的故事。在电影中,露西(巴里摩尔饰)通过她的日记建立了自己的“外部记忆”;这虽然是一个巧妙的情节设计,但也需要观众的信任,而我却无法接受这种设定。在电影字幕滚动时,我做了一些速算:如果露西每篇日记的写作时间是 20 分钟(这比大多数人写得还快,而且大多数人不需要弥补脑损伤),每篇日记的阅读时间大约是写作时间的一半——大约 10 分钟——所以,阅读一周的日记将需要超过一个小时。而到了 6 个月时,每天的回顾将需要比一天还要多的时间来阅读。浪漫吗?是的。现实吗?不是。所以我们争论了起来。我记得我们的对话大致是这样的:
“她:但是如果每周天她写一张过去一周的备忘单,只读那些呢?那样会节省时间。”**
”我:即便是每周总结,也不到一年就会变得无法阅读。”
“她:好吧,那如果她总结了那些备忘单呢?她可以不断把历史压缩得越来越小。”
“我:是的,但最终她会丧失太多细节,摘要会变得毫无用处。”
“她:但她还是会有她的日记,什么时候需要那些细节时可以查看!”
“我:她怎么能搜索到那个呢?我们又回到了原点。”*
二十年后,“露西问题”成为了帮助我们理解设计大型语言模型代理框架中最重要挑战之一的完美视角。加州大学伯克利分校的研究人员提出的解决方案非常具有创新性,并且充满了令人兴奋的潜力——这也是一个与我在那次回家路上的争论中反对的“露西解决方案”非常相似的方案。看来我欠某人一个迟来的道歉。
露西语言模型:几乎完美的类比
大型语言模型实际上只是函数。你输入至少一个参数(文本),它们就会输出相应的结果。这个输出是模型业务逻辑、组合参数和内部参数的产物——其中一个参数就是用来开发推理模型的训练数据。这些训练数据作为模型的“记忆”;没有它,LLM 几乎不会输出任何有价值的信息,就像试图与一个新生儿进行深入对话一样。大型语言模型中的训练数据“记忆”在推理时是固定的,完全像电影中的露西的记忆。她已经积累了经验并收集了信息,直到某个非常具体的时刻(在她的案例中,是她事故的那一天);从那天起,她根据当时的心理状态和记忆来解读刺激。这正是大型语言模型推理的运作方式——固定在训练完成的那一刻,结果函数被封存。
每次执行 LLM 函数时(这里我们将这种执行和响应的结合称为回合,借用聊天术语),就像露西生活中的一天一样。在模型温度设置为 0(确定性)时,每个回合在相同输入下将完全像露西的早期日常——一遍又一遍地重复同一天(并且做很多一模一样的生日蛋糕)。LLM 不能像露西一样形成新的“记忆”,它只是一个纯粹的推理终端。
为了弥补这一点,使用大语言模型时,自然的下一步是将这些新的“记忆”作为文本的一部分,传递给 LLM 函数,从而有效地增强了语言模型在该回合期间的训练数据(1)。然而,语言模型的上下文窗口——即可以在单次回合中输入和输出的文本量——是有限的。同样,这正是巴里摩尔(Drew Barrymore)在电影中角色体验世界的方式;她的上下文窗口仅限于一天。就像我多年前所说的,露西的记忆最终需要比她一天之内能够记住的时间更长才能被消化一样,必须在回合中包含的新知识很快就会超出可用的上下文窗口。
提示工程的局限性
大部分关于大语言模型(LLM)工程的讨论都集中在提示工程上,即设计我们在每次对话中提交的内容,以产生最理想的结果。围绕提示设计迅速发展了一个完整的生态系统,从提示工程课程到提示交换市场——这一切都源于这样一个观点:通过“完美的提示”,你可以引导出“完美的输出”。
亨利,桑德勒在电影《50 次初恋》中的角色,可能是最早的提示工程师之一。在电影的开头,亨利爱上了露西,并同意不告诉她她的伤势,而是每天重新追求她。他每天的“提示”从一开始就异常拙劣,大多数以拒绝告终。随着时间的推移,他的技巧逐渐演变,直到露西每天都重新爱上他。我们在无数的语言模型演示中也能看到类似的例子,精心设计的提示被用来为数据集可视化分析或生成精准的求职信。
这些例子令人印象深刻,但这种提示设计真的有多有用呢?在电影中,亨利最终解决了无限次初恋中的极大局限性,告诉露西她的病情。对于语言模型而言,单独执行的“完美提示”同样也有其局限性。复杂任务需要多个复杂的步骤,每个步骤都建立在一个修改过的状态上——这无法在单次回合中完成。虽然提示工程无疑是解决问题的重要组成部分,但它远不是我们问题的整体解决方案。
RAG,一家报纸和一盘录像带
对于露西和语言模型来说,一旦我们开始外部化记忆,事情就变得有趣起来。检索增强生成(RAG)在 LLM 相关内容中可能是仅次于提示工程的关注焦点。RAG 可以简单地表述为“将文本存储在某处,然后在每次迭代中搜索该文本并将其片段加入到提示中。”目前最常见的 RAG 实现是盲语义搜索,其中每次用户输入都会根据语义相似性在 RAG 存储中进行搜索,然后将前几个搜索结果与用户输入一起组合成提示。它们看起来像这样:
# prompt with just user input
Question: What is the last thing Todd said before he quit yesterday?
vs
# prompt with vector similarity search results for "What is the last thing Todd said before he quit yesterday?" via embeddings, prepended to prompt
Context:
"Margo: Todd is quitting today!"
"Todd: I am quitting today. I've had enough."
"Clark: I can't believe Todd finally quit, Margo is going to freak."
Question: What is the last thing Todd said before he quit yesterday?
RAG 注入的上下文可能非常有用,也可能几乎没有相关性。更重要的是,问题可能根本不需要上下文,而 RAG 可能只是噪音。
《50 次初恋》再次没有让人失望,通过现实生活的类比做得很好。在电影中,露西的病情通过伪造的情境线索被隐藏起来;她的父亲用重新印刷的报纸替换掉她的报纸,将一场录制过的足球比赛当做现场直播播放,每天晚上重新粉刷墙壁,让她第二天可以再次粉刷,而她却毫无察觉。这些情境的设置增加了提示的有效性,使露西能够度过完整的一天(尽管是同样的一天重复上演)。它比完全依赖当天的自然事件要显著更好地达成预期目标(露西享受她的一天并能在其中正常运作)。随后,亨利尝试通过一段 VHS 录像带来诚实地面对露西。对于电影情节而言,这是一个关键时刻,因为这是露西恢复自主性的第一步。对于语言模型而言,这和报纸与涂料是一样的;每一次迭代在包含 RAG 内容时可能会更好、更有信息,但它依然是一个孤立的迭代,没有真正的外部状态管理。
无论露西消费的是哪一个——假报纸还是亨利的真实 VHS 录像带——露西生活的改善仅限于当天的结果。露西依然没有自主权去过完整的生活,就像我们的语言模型无法向完成复杂任务迈出任何有意义的步伐一样。
就像提示工程一样,RAG 是拼图的一部分,但它本身并不是答案。
一颗有日记的心灵
让我们从车上回顾一下那个理论。如果露西保留了一本日记,然后通过总结、整合并制作备忘单来管理这个“外部记忆”?与她父亲的报纸或亨利的 VHS 录像带不同,这种记忆将完全由她掌控。她决定哪些信息至关重要,哪些记忆可以被遗忘,哪些知识应存放在“冷存储”中,只有在需要时才会被挖掘出来。电影触及了这一理念(尽管是通过我那破坏浪漫的逻辑才真正看到它在细节上的展开)。拥有这样的外部记忆,露西现在不再受限于她的上下文窗口。她可以根据兴趣追求任何事情,无论多长时间,积极参与日常但重要的生活事件,拥有一个家庭,活出自己。她可以在周一制定一周的菜单,在周三去买菜,周五做饭——这些由几本笔记本和待办清单所赋予她的自主性。
这与MemGPT项目背后的理念非常相似(2)。上下文窗口、聊天历史和 RAG 知识库被视为分层的“记忆”;每个记忆可以通过工具调用由 LLM 写入或清除。不同于将每个回合视为孤立的提示 + RAG + 输入 = 响应,“核心”记忆(上下文窗口的内容)被持续处理,像一个守护进程一样,在每次“心跳”中进行新的回合。就像每一天露西都会看到前一天的笔记和待办事项列表一样,每一回合,语言模型都会看到由上一个回合组装的核心记忆。在这一回合中,语言模型可以编辑这个核心记忆,将信息从核心移入归档记忆,或者从归档记忆中搜索/调用并将其添加回核心,而这些变化将在下一回合中应用于核心记忆。

来自MemGPT 研究论文。这是一个极为出色的作品,非常推荐大家亲自去阅读。
这里是一个代理如何工作(带有一些伪代码)来回答用户关于某个公司的问题的示例。代理有一个描述自己和用户的核心记忆,一个包含最后五条信息的历史记录部分,以及代理可以调用的工具列表。这个核心记忆每 10 秒钟作为提示传递一次,或者在外部触发时更早传递。
-
在第一次处理时,代理调用
archive_lines()工具将用户信息的第二段移入冷存储。为什么?因为系统提示告知代理当前和最大内存大小,并指示代理在需要时进行自我调整。 -
在下一轮中,工具调用请求和结果是核心记忆历史中的最新条目(上一轮的 2 个最旧条目已被移到回忆记忆中,不可见)。在这一轮中,智能体调用
pause_heartbeats()以等待外部刺激,因为它检测到没有其他内容需要在自身的核心记忆中更新。 -
下一轮由用户消息触发,这是本轮核心记忆中的最新历史条目。用户消息是“西雅图办公室有多少人?”智能体的第一个动作是调用
get_employee_list()工具,并伴随内部独白“我必须获取员工列表,并且只计算在西雅图办公室的员工。然后,我必须将消息发送回用户。” -
在下一轮中,智能体的工具调用、内部独白和工具调用结果都在核心记忆历史中,但原始的用户消息已被推出。在这一轮中,智能体的内部独白是“西雅图办公室有 65 名员工。我必须获取原始问题,以便能正确回答它”,工具调用是
retrieve_message(user, -1),以获取用户的上一条消息。 -
在下一轮中,智能体调用
send_message("西雅图办公室有 65 名员工"),并回应用户。
节奏继续。
这种“语言模型操作系统”与推理服务如何完成复杂多面的任务形成鲜明对比。它还改变了作为智能体的“进化”范式,使应用程序能够进行调整和自我修正。提示和 RAG(检索增强生成)成为生成“循环”中的可修正元素;如果 RAG 搜索无效或提示偏离目标,它可以在下一轮中重新尝试或进行补偿。与单轮智能体设计最显著的区别在于,这种自我管理的记忆结果是累积的。这是实现真正智能体所必需的。
我对基于这一概念构建的框架可能带来的意义感到非常兴奋;向一个精心设计的智能体(或智能体群体)添加刺激,变成了一个执行层,超越了文本生成,并且其投资回报随着任务复杂度的增加而呈指数增长。以这种方式运行的语言模型仍然是一个语言模型——一个功能,而非有知觉的存在——但它跨越了一个外观的门槛,这是科幻作品中的东西。更重要的是,它为生成方程式添加了一个关键元素:重复。我看不到没有这个元素,自动智能体能够成功。人类不会立即想到每一个必要的想法并一口气说出完美的回答;我们采取步骤,提出问题以揭示新的问题,暂停内心思考,然后得出答案。通过赋予应用程序同样的能力,这种语言模型操作系统可能成为计算领域的新范式。
对于我们这些构建用户交互代理的人来说,这是一个值得关注并投入时间和精力的概念。单轮提示库和切片式嵌入构建,通过 RAG 处理不良响应是我们在六个月前的 AI 黑暗时代里想到的最佳主意,但我认为它们无法带我们走向目标。在《50 次初恋》中,Lucy 嫁给了 Henry,成为了妈妈,并航行到了阿拉斯加,所有这一切都因为她恢复了掌控自己思想和管理记忆的能力。如果我们想解锁语言模型代理,它们也需要在“操作系统”中拥有相同的能力。
下次加入我,我们将探讨《快乐吉尔摩》和 K8S 之间的相似之处(提示:它们之间没有任何相似之处)。
脚注:
- 为提示添加上下文并微调或重新训练模型其实并不完全是同一件事,但为了清晰地展示主题概念,我愿意在技术准确性上做出一些适当的妥协。
2. 请注意,在本文中我指的是 MemGPT 背后研究所提出的概念,而不是其具体实现。这里的术语、伪代码和事件描述并不旨在反映软件项目本身。
MemGPT 引用:
packer 2023 memgpt, MemGPT: 面向 LLMs 作为操作系统的未来
作者:Packer, Charles 和 Wooders, Sarah 和 Lin, Kevin 和 Fang, Vivian 和 Patil, Shishir G. 和 Stoica, Ion 和 Gonzalez, Joseph E.
arXiv 预印本 arXiv:2310.08560
直接湖的 50 种面貌——关于新 Power BI 存储模式你需要知道的一切
使用直接湖模式需要掌握许多新的概念!本文涵盖了你应该知道的所有基本内容
·发表于Towards Data Science ·15 分钟阅读·2024 年 8 月 20 日
--

图片由作者提供
免责声明: 本文的目的是 NOT 提供“哪种模式更好——导入模式还是直接湖模式”的答案,因为这个问题无法回答,因为没有一种“万能”的解决方案…虽然导入模式(Import)在大多数情况下仍然是默认选择,但在某些场景中,您可能会选择直接湖模式(Direct Lake)。本文的主要目标是提供关于直接湖模式如何在幕后工作,并进一步解释各种直接湖的概念。
如果你想了解更多关于导入(Import)模式与直接湖(Direct Lake)模式的比较,以及何时选择其中一个,我强烈建议你阅读Marco Russo 在 SQLBI 上的文章。
现在,我们可以开始了 😃…
我不知道你是否有同样的感受,但当我看电影时,看到一些令人叹为观止的场景时,我总是在想——他们是怎么做到的?!他们用了什么样的技巧让它看起来如此神奇?
56 步,1 个完整的营销项目:从数据到战略
构建一个完整的营销项目,从数据分析和特征工程到假设检验和商业洞察。
·发布于Towards Data Science ·78 分钟阅读·2024 年 8 月 20 日
--

照片由Kalen Emsley提供,来源于Unsplash
项目概述
在这篇文章中,我将展示一个完整的数据探索分析项目,专注于营销领域,这是我已经尝试了几周——如果不是几个月——才结构化完成的。
此外,这份材料将作为个人指南,因为我相当没有条理。相信我,当我为你写这些时,我会尽全力,而这也有助于我整理思路。
在继续之前,我想做一个小小的备注:你有没有试过点击👏按钮多次?它真的很神奇,但只有在你觉得这项工作对你的学习有价值时才会有效,就像在我开发它的过程中一样。
这不会占用你太多时间,而且没有什么能比得上我为你带来的内容。相信我!如果你严格按照这个逐步指南操作,你可以在任何数据科学项目中复制它。
本指南的目标——或者说是手册、教程、演练、电子书,随你怎么称呼——是使用来自行业的真实数据,在特定的背景下进行工作。我将…
初学者数据科学学生常犯的 6 个错误
进入数据科学时要避免这些陷阱
·发表于 Towards Data Science ·8 分钟阅读·2024 年 2 月 5 日
--

图片由作者制作。
在我最近的博客和视频中,我收到了不少寻求数据科学学习建议的消息。虽然我能为他们提供足够的学习路线图,但我认为同样重要的是,指出初学者常犯的一些错误。
在本文中,我想概述六个错误,这些错误是我认为初学者数据科学家在试图进入这一领域时常常会犯的。成功避免其中一个陷阱就算成功,你会比我刚开始时做得更好!
跳过基础数学
许多人认为现在不需要了解数学,因为所有现代的 Python 包都将其需求抽象化了。你几乎永远不会手动进行反向传播或从头开始构建决策树(尽管尝试一下其实很有趣!)。
很容易把这一点视为理所当然,从而避免学习任何相关的背景理论……
微软最新 Phi-3 视觉语言模型的 6 种实际应用
探索 Phi-3-Vision 的可能应用场景,这是一款小巧却强大的 MLLM,可以在本地运行(附带代码示例)
·发表于Towards Data Science ·阅读时间 7 分钟·2024 年 5 月 24 日
--

微软最近发布了 Phi-3,一款强大的语言模型,并推出了一种新的视觉-语言变体——Phi-3-vision-128k-instruct。这款拥有 40 亿参数的模型在公开基准测试中取得了令人印象深刻的成绩,在某些情况下甚至超越了 GPT-4V,并且在除了 MMMU 之外的所有领域都优于 Gemini 1.0 Pro V。
本文将探讨如何将 Phi-3-vision-128k-instruct 作为您数据科学工具包中的强大视觉和文本模型。我们将通过各种应用场景展示其能力,包括:
-
光学字符识别(OCR)
-
图像描述
-
表格解析
-
图像理解
-
扫描文档的阅读理解
-
集合标记提示
我们将首先提供一个简单的代码片段,用于通过 transformers 和 bitsandbytes 在本地运行该模型。然后,我们将展示上述每个应用场景的示例。
在本地运行模型:
管理生成性 AI 产品中声誉与伦理风险的六步框架
一份务实的指南,帮助理解 AI 风险,并通过负责任的 AI 开发与用户建立信任
·发表于 Towards Data Science ·阅读时间:9 分钟·2024 年 2 月 19 日
--

来源:Dalle-3
在快速发展的技术领域,产品团队感受到将创新型 AI 产品迅速推向市场的巨大压力。然而,将速度置于伦理和安全考虑之上可能会带来严重的负面后果。当 AI 系统违反社会规范和价值观时,组织面临的风险包括长时间的声誉损害和失去用户信任。
企业正处于 AI 时代的现实“囚徒困境”之中——虽然集体遵循伦理开发是理想的,但公司往往更倾向于通过妥协这些关键标准来获得先发优势。这一紧张局势通过臭名昭著的Facebook-Cambridge Analytica丑闻得到了体现,在该丑闻中,采取伦理上的捷径导致了严重且持久的声誉损害。随着 AI 在我们日常生活中的进一步渗透,这种伦理赌博的风险只会加剧,行业中的每个参与者都在权衡短期利益与长期信任和责任的价值。
自 2018 年以来,随着各行业 AI 应用的增加,高曝光度的伦理违规事件急剧上升。现在,识别这些风险并制定缓解策略变得至关重要。本文探讨了 AI 风险的主要来源,回顾了几个高曝光度的失败案例,并提出了一个六步框架,用于构建能应对这些威胁的产品。
识别 AI 的风险
1. 隐私侵犯与同意问题
风险: 可以说,最普遍的 AI 陷阱是侵犯隐私和版权法。这涉及两种不同的失败:未能获得数据使用的同意,以及将数据用于超出同意范围的目的。
示例: 艺术家和作家已对OpenAI和MidJourney提起集体诉讼,指控这些公司未经同意使用他们的作品来训练模型。同样,Getty Images也在起诉Stability AI,指控其使用其数据进行模型训练。即便在给予同意的情况下,如果数据被用于未预料的目的,也可能发生隐私泄露。例如,Google DeepMind使用了皇家伦敦 NHS 基金会信托的 160 万患者数据,构建了一个新的医疗应用程序。尽管存在隐性同意,即这些数据可以用于改善患者健康,但信托和 DeepMind 未明确告知患者他们的信息被用于构建应用程序。
[## 艺术家再次起诉 Stability,Midjourney,更新后的版权诉讼
一群视觉艺术家已经对 Stability AI、Midjourney 和其他公司提起了修改后的版权诉讼…
为什么 Google 收购 DeepMind Health 让隐私专家感到恐惧 [## 为什么 Google 收购 DeepMind Health 让隐私专家感到恐惧
DeepMind Health 曾遇到过数据保护问题,其完全并入 Google 后,问题可能更加严重…
2. 算法偏见
风险:此风险涉及 AI 系统做出有偏见的预测,这种预测会系统性地使某些群体处于不利或排除地位,基于如种族、性别或社会经济背景等特征。这类偏见可能对社会产生重大影响,特别是当 AI 被用来做出影响个人生活的关键决策时。
示例: 2019 年,苹果因涉嫌男性在Apple Credit Cards的信用额度高于女性而遭到反对,尽管在某些情况下女性的信用评分更高。其他显著的例子包括 AI 驱动的招聘软件和刑事司法应用程序,例如COMPAS工具,因表现出种族和性别偏见而受到批评。
www.cnn.com/2019/11/12/business/apple-card-gender-bias/index.html?source=post_page-----7ed46783d282-------------------------------- [## 苹果卡被指控存在性别偏见。它是如何发生的 | CNN 商业
一些苹果卡用户表示,该信用卡的发行方高盛银行为女性设置了远低于男性的信用额度,即使…
www.cnn.com [## 观点 | 当算法帮助你被送进监狱时(2017 年发布)
将判刑的责任交给计算机程序并不能消除偏见。
3. 精确度风险与可解释性差距
风险: 当用于做出重大决策的人工智能系统提供不准确的结果,或未能为其输出提供清晰的理由时,可能会产生重大风险。人工智能的“黑箱”性质使得用户难以理解和验证其结果,这模糊了问责制并导致信任丧失。
例子: IBM 沃森肿瘤学旨在为临床医生提供个性化的癌症治疗推荐。然而,有报告显示沃森给出了不安全和不正确的治疗建议,这导致了对技术的信任丧失,并损害了 IBM 的声誉。
## IBM 的沃森超级计算机推荐了“危险且不正确”的癌症治疗方案,内部文件…
去年夏天,IBM 沃森健康部门的高管展示的幻灯片大多将问题归咎于…
4. 安全风险
风险: 使用人工智能生成深度伪造内容会带来显著的社会风险,这涉及到生成高度逼真的图像或视频,让人们看起来像是在说或做他们实际上从未做过的事情。深度伪造已被用于制造欺骗性且逼真的媒体,可用于实施欺诈、传播虚假信息或破坏声誉。此外,人工智能还可能被用于网络攻击和社会工程学,如量身定制钓鱼攻击,这可能引入广泛的安全漏洞。
示例:在政治领域,深度伪造(deepfake)被用来伪造演讲或行为,可能在关键时刻如选举期间左右公众舆论。在俄乌冲突期间,一段伪造的视频显示乌克兰总统似乎告诉他的士兵们放下武器投降,这段视频被广泛传播,这种行为可能使军队士气低落,并为俄罗斯提供了关键的战术优势。在网络安全领域,AI 驱动的声音模仿被用来伪造 CEO 的声音,成功地进行了一笔欺诈性资金转移。
## 专家警告:泽连斯基的深度伪造视频可能是信息战的“冰山一角”
一段伪造的视频显示乌克兰总统宣布失败并在社交媒体上传播。
www.npr.org](https://www.npr.org/2022/03/16/1087062648/deepfake-video-zelenskyy-experts-war-manipulation-ukraine-russia?source=post_page-----7ed46783d282--------------------------------) ## 通过深度伪造音频进行的异常 CEO 欺诈,窃取了英国公司 243,000 美元
一起异常的 CEO 欺诈案件使用了深度伪造音频,这是一种由人工智能(AI)生成的音频,已被报道…
缓解 AI 风险的六步框架
管理 AI 风险需要在整个产品生命周期中采取深思熟虑的方法。下面是一个六步框架,按 AI 开发的不同阶段组织,供组织采纳,以确保其产品中 AI 技术的负责任使用。

来源:作者
1. 开发前:伦理基础和设计原则
在编写任何一行代码之前,产品团队应当为产品打下基础。优先与广泛的利益相关者进行早期接触,包括用户、技术专家、伦理学家、法律专业人士以及可能受到 AI 应用影响的社区成员。目标是识别与产品使用案例相关的明显和微妙的风险。利用这些见解来制定一套伦理准则和产品能力,在产品发布之前嵌入到产品中,以预防性地解决已识别的风险。
2. 开发阶段:数据同意、完整性、多样性
数据是人工智能的基石,同时也是人工智能风险的最重要来源。确保所有用于模型训练的数据都具有道德来源,并且获得了用于其预定目的的同意,至关重要。例如,Adobe 使用专有数据训练其图像生成模型(Firefly),使其能够为用户提供法律保护,避免版权诉讼。
此外,应该从用于训练模型的敏感数据集中删除个人身份信息(PII),以防止潜在伤害。对这些数据集的访问应适当设置门槛并进行跟踪,以保护隐私。同样重要的是,确保数据集能够代表用户群体的多样性和使用场景的广度,从而减少偏见和公平性风险。像Runway 这样的公司已经使用合成数据集训练其文本到图像模型,这些数据集包含了来自不同种族、性别、职业和年龄段的人工智能生成的人的图像,以确保其人工智能模型在生成内容时体现多样性。
人工智能(AI)正在从根本上改变许多行业。人工智能将带来许多好处,但它也…
www.nasdaq.com [## 缓解文本到图像生成系统中的刻板偏见 | Runway 研究
用人工智能重新定义创意。
3. 开发:稳健性测试与实施防护措施
测试阶段在决定人工智能是否准备好公开发布中至关重要。这涉及将人工智能的输出与经过验证的结果集进行比较。有效的测试使用:
-
与用户目标和商业价值对齐的性能指标,
-
代表不同人口统计群体并涵盖各种使用场景(包括边缘情况)的评估数据,
除了性能测试,实施防护措施同样至关重要,以防止人工智能产生有害结果。例如,ImageFX,Google 的图像生成服务,主动阻止用户生成可能被认为不适当或用于传播虚假信息的内容。同样,Anthropic 已主动设定了防护措施,以避免其人工智能服务在 2024 年选举中被滥用。
我们正在推出 Imagen 2,这是我们图像生成技术的重大更新。今天就可以在 Bard、Image FX 中试用它……
blog.google](https://blog.google/technology/ai/google-imagen-2/?source=post_page-----7ed46783d282--------------------------------#:~:text=Our responsible approach to building Imagen 2) [## Anthropic 在 LinkedIn:为 2024 年全球选举做准备
随着全球 2024 年高风险选举的临近,我们正在揭示我们的一些工作……
4. 开发:可解释性与赋能
在需要建立信任的关键行业应用中,AI 应当帮助人类担任辅助角色。这可以通过以下方式实现:
-
提供 AI 洞察来源的引文。
-
强调 AI 预测的 不确定性 或 信心级别。
-
提供让用户选择退出使用 AI 的选项。
-
创建应用程序工作流,确保人工监督并防止某些任务被完全自动化。
5. 部署:渐进式推出与透明度
当你将 AI 系统从开发阶段过渡到实际部署时,采用渐进式的发布策略至关重要,这有助于在受控环境中评估风险并收集反馈。同时,清晰地向用户和利益相关者传达 AI 的预期使用案例、能力和局限性也非常重要。此阶段的透明度有助于管理期望,并减少与 AI 系统意外失败相关的声誉风险。
OpenAI,例如,通过其最新的文本转视频服务Sora展示了这种方法,最初仅向部分红队成员和创意专业人士提供该服务。它已经公开了 Sora 的能力和当前的局限性,比如在生成涉及复杂物理交互的视频时面临的挑战。这种程度的透明度确保了用户了解该技术的优势和可能失败的地方,从而管理期望,赢得用户的信任,并促进 AI 技术的负责任采用。
Sora 是一个能够根据文本指令创建逼真和富有创意场景的 AI 模型。阅读技术报告 All…
6. 部署:监控、反馈与适应
在 AI 系统上线后,工作并未结束。接下来需要密切关注 AI 在实际环境中的表现,并根据观察结果进行调整。创建一个持续的机制,追踪性能漂移,并不断在新数据上测试和训练模型,以避免 AI 性能的下降。使用户能够轻松标记问题,并利用这些反馈调整 AI,持续更新保护措施以符合高伦理标准。这将确保 AI 系统保持可靠、值得信赖,并与其操作的动态世界保持同步。
结论:
随着 AI 在我们生活中的进一步渗透,主动管理伦理风险已不再是可选的——它是必须的。通过在每一个将 AI 产品推向市场的步骤中嵌入伦理考量,公司不仅可以降低风险,还能与用户建立起根本性的信任。当面对比我们之前见过的任何技术都要强大的技术时,快速行动并破坏一切的时代已经不能再适用。在处理可能对社会产生深远影响的风险时,没有捷径可走。
AI 产品构建者有责任更加有意图和有目的地行动,将信任作为他们的真正指南针。AI 的未来成功与持续进展取决于今天是否正确处理伦理问题。
感谢阅读!如果这些见解对你产生共鸣或激发了新的思考,让我们继续交流。请在下方评论区分享你的看法,或者在 LinkedIn* 上与我联系。*
让 AI 为你编写 Python 代码的 6 个步骤
使用 INSPIRe 框架节省时间并获得竞争优势(ChatGPT-4 — Claude 3 — Gemini)
·发表于Towards Data Science ·阅读时间 19 分钟·2024 年 4 月 14 日
--

由作者生成的图像。
AI 不会取代你,但会取代那些写得更好提示的人。
这就是我告诉我的数据科学和软件工程的朋友们的事情。有些人对此不以为然。“AI 不可能生成多步骤的代码,”他们会这么说。其他人则笑了。“得了吧,它只对初学者有用!”
但很快,他们都会开始寻求提示工程的建议。
到 2022 年底,大型语言模型(LLM)已将代码生成从一个最终目标转变为一种可以随时使用的廉价资源。规则很简单:提示输入;代码输出。
那种必须编写每一行代码并检查错误的时代已经过去。如果你学会了如何与 AI 对话,你就可以将大部分代码生成外包出去——这里的“多数”指的是 90%。
我是个糟糕的程序员。语法是我最大的敌人,对我来说,通往地狱的路是铺满缩进的。像 Devin 这样的 AI 解决方案…
7 本关于人工智能的书籍
2024 年被认为是“人工智能之年”,我们将看到比 2023 年更多的突破。
·发布于 Towards Data Science ·15 分钟阅读·2024 年 2 月 27 日
--

作者提供的图片。
在这篇文章中,我将分享一些我最近阅读的关于人工智能的最有趣的书籍,以及我的一些个人想法:
👉 注意:你也可以在我的 Youtube 频道上观看这篇博客文章的视频版本:
不再赘述,让我们直接进入正题!
生命 3.0

作者提供的图片。
来自英特尔机器学习实习的 7 个经验
自动化、机器学习和 LLMs 在芯片行业中的应用
·发布于Towards Data Science ·7 分钟阅读·2024 年 2 月 12 日
--

(来源:chatGPT)
我感觉自己就像是《怪兽公司》里的那些人。你知道的,穿着大黄色防化服的那些人。必要的预防措施!我进入了世界上最复杂的制造环境之一。这个环境需要极高的精确度,甚至你呼吸时微小的颗粒也能干扰它。
在英特尔的六个月实习期间,我学到了很多关于半导体行业的知识。它正经历着动荡的时期,英特尔是如何应对的,以及这为什么意味着机器学习将变得更加重要。我看到从传统的计算机视觉算法到大型语言模型(LLM)如何帮助生产世界上最有价值的商品——芯片。
我想和你分享这个经历。
1)“英特尔就像是一家面包店”
就是我的经理尝试总结的一个过程,我永远无法完全理解。他们:
-
创建配方 → 设计芯片
-
烤面包 → 制造芯片
但他们并不制造做这些事情的机器。
我近距离看到了一些这些。最让我印象深刻的是ASML制造的光刻机。它们利用光波将几原子宽的图案刻入硅片中……
我作为数据科学家第一年的 7 个遗憾
我在开始第一份数据科学工作之前希望自己知道的事情
·发布于Towards Data Science ·阅读时长 8 分钟·2024 年 3 月 22 日
--

图片由@felipepelaquim提供,来源于Unsplash
我在 2021 年 9 月开始了我的第一份数据科学工作。我记得在那第一年结束时,我开始回想那些自己在开始这份工作之前,曾希望知道的事情。在这篇文章中,我想把我当时的所有“遗憾”总结一下。希望你在最终获得第一份数据科学工作时,至少能避免其中的一项。
学习 Git

图片由Pankaj Patel提供,来源于Unsplash
在学习数据科学时,你很可能是独自工作的,所以版本控制的需求似乎相对有限。此外,如果你像我一样,学习 git 这类工具可能会觉得比学习神经网络还要枯燥无味。
然而,我可以向你保证,几乎每个行业中的数据科学家都以某种方式使用 Git 或 GitHub。在大多数公司中…
7 个帮助我作为数据科学家的订阅服务
提升我作为一名在职数据科学家的生产力、知识和专注力的订阅服务
·发表于Towards Data Science ·阅读时长 8 分钟·2024 年 4 月 19 日
--

由Chase Chappell拍摄,图片来自Unsplash
在本文中,我将探讨七个关键的订阅服务,这些服务帮助我在作为一名在职数据科学家的工作中提升生产力、专注力和学习效率。从能够回答我问题的人工智能,到帮助我集中注意力的简短音乐节奏,希望这些工具能对你有所帮助。
不用说,这篇文章中的内容不构成财务建议,而且我并没有得到任何列出产品或服务的赞助。
ChatGPT
这不足为奇,但我推荐给数据科学家的头号订阅服务是 ChatGPT。
立即获取答案,寻找创意灵感,学习新知识。
尽管有免费版本,但每月多花 20 美元完全值得。你将获得……
未来可持续的机器学习项目的 7 个建议
开发更具协作性、可复现性和可重用性的机器学习代码简介
·发表于 Towards Data Science ·8 分钟阅读·2024 年 2 月 24 日
--

7 个建议确保机器学习项目的未来可持续性(作者图片)
在从探索性机器学习项目(通常用于研究和学习)过渡到行业级项目时,可能会出现知识差距。这是因为行业项目通常有三个额外的目标:协作性、可复现性和可重用性,这些目标旨在增强业务连续性、提高效率并降低成本。虽然我离找到完美的解决方案还远,但我希望记录一些建议,将探索性、基于笔记本的机器学习代码转变为一个更具可扩展性和可持续性的行业级项目。
我将这些建议分为三个关键策略:
-
改进 1:模块化 — 将代码拆分成更小的部分
-
改进 2:版本管理 — 数据、代码和模型版本管理
-
改进 3:一致性 — 一致的结构和命名约定
改进 1:模块化 — 将代码拆分成更小的部分
问题陈述
我面临的一个挑战是只有一个笔记本用于...
8 张图表,帮助你向外行解释线性回归
用残差图、权重图、效应图和 SHAP 图向非技术观众解释回归
·发布于Towards Data Science ·阅读时长:13 分钟·2024 年 4 月 16 日
--

“不要使用任何数学公式。”这是我经理的指示。
我还能怎么解释回归是如何工作的!?
我当时并不知道,数据科学家工作的很大一部分是要用非技术性语言来解释模型。我们理解一个线性模型所需的,只是一份回归总结。然而,这些数字表格很快就会让(或吓到)其他观众感到厌烦。为了让你的观点更易理解,你需要使用更易消化的可视化工具。所以,我们将探索 8 种图表,帮助你让线性回归变得更易于理解:
-
残差图
-
相关性热图
-
权重图
-
效应图
-
平均效应图
-
个体效应图
-
趋势效应图
-
线性模型的 SHAP 值
我们将看到,第 4 到第 7 张图特别有用,因为它们有助于从模型对预测的贡献来解释模型。我们以每个人都能理解的数量来“重新框架”结果。例如,我们可以从解释回归参数转向……
8 个实用的提示工程技巧,帮助提升 LLM 应用
·发表于Towards Data Science ·9 分钟阅读·2024 年 8 月 1 日
--
提示工程无疑是开发 LLM 本地应用程序中最关键的技能,因为制定正确的提示可以显著影响应用程序的性能和可靠性。
在过去的两年里,我一直在帮助组织构建和部署数十个用于实际案例的 LLM 应用程序。这段经历让我对有效的提示技巧有了宝贵的洞察。
本文基于 LLM 三角原理介绍了八个实用的提示,帮助提升你的 LLM 提示工程技能。
“LLM 本地应用程序是 10%的复杂模型和 90%的实验性数据驱动工程工作。”

(由 Midjourney 生成)
在本文中,我们将使用“登录页面生成器”示例来演示如何应用每个技巧,以增强实际的 LLM 应用程序。
你也可以查看登录页面生成器示例的完整脚本,以便全面了解。
注意: 这是一个简化的非生产示例,旨在说明这些技巧。代码和提示故意简化,以突出讨论的概念。
1. 定义明确的认知过程边界
从定义每个代理或提示的目标开始。坚持每个代理一个认知过程类型,例如:构思一个登录页面、选择组件或为特定部分生成内容。
拥有明确的边界有助于保持 LLM 交互中的专注和清晰,这与 LLM 三角原则的工程技术顶端保持一致。
“我们流程中的每个步骤都是一个独立的过程,必须完成才能实现我们的任务。”*
例如,避免在同一提示中组合不同的认知过程,这可能会产生不理想的结果。相反,可以将这些过程分解成独立的、专注的代理:
def generate_landing_page_concept(input_data: LandingPageInput) -> LandingPageConcept:
"""
Generate a landing page concept based on the input data.
This function focuses on the creative process of conceptualizing the landing page.
"""
pass
def select_landing_page_components(concept: LandingPageConcept) -> List[LandingPageComponent]:
"""
Select appropriate components for the landing page based on the concept.
This function is responsible only for choosing components,
not for generating their content or layout.
"""
pass
def generate_component_content(component: LandingPageComponent, concept: LandingPageConcept) -> ComponentContent:
"""
Generate content for a specific landing page component.
This function focuses on creating appropriate content based on the component type and overall concept.
"""
pass
通过为每个代理定义明确的边界,我们可以确保工作流中的每个步骤都专门针对特定的心理任务。这将提高输出的质量,并使调试和精炼变得更容易。
2. 明确指定输入/输出
定义明确的输入和输出结构,以反映目标并创建明确的数据模型。这个实践涉及到 LLM 三角原则的工程技术和上下文数据的顶端。
class LandingPageInput(BaseModel):
brand: str
product_desc: str
campaign_desc: str
cta_message: str
target_audience: str
unique_selling_points: List[str]
class LandingPageConcept(BaseModel):
campaign_desc_reflection: str
campaign_motivation: str
campaign_narrative: str
campaign_title_types: List[str]
campaign_title: str
tone_and_style: List[str]
这些Pydantic模型定义了我们输入和输出数据的结构,并为代理定义了明确的边界和期望。
3. 实施防护措施
放置验证以确保 LLM 输出的质量和适度。Pydantic非常适合实现这些防护措施,我们可以利用其原生功能来实现。
class LandingPageConcept(BaseModel):
campaign_narrative: str = Field(..., min_length=50) # native validations
tone_and_style: List[str] = Field(..., min_items=2) # native validations
# ...rest of the fields... #
@field_validator("campaign_narrative")
@classmethod
def validate_campaign_narrative(cls, v):
"""Validate the campaign narrative against the content policy, using another AI model."""
response = client.moderations.create(input=v)
if response.results[0].flagged:
raise ValueError("The provided text violates the content policy.")
return v
在这个例子中,通过定义两种类型的验证器来确保我们应用程序的质量:
-
使用 Pydantic 的
**Field**来定义简单的验证,例如至少包含 2 个语调/风格属性,或叙述至少包含 50 个字符 -
使用自定义的
**field_validator**来确保生成的叙述符合我们的内容审查政策(使用 AI)。
4. 与人类认知过程对齐
通过将复杂的任务分解为遵循逻辑顺序的小步骤,来构建模拟人类认知过程的 LLM 工作流。为此,遵循 LLM 三角原则中的SOP(标准操作程序)指导原则。
“没有标准操作程序(SOP),即使是最强大的 LLM 也无法 consistently 交付一致的高质量结果。”
4.1 捕捉隐藏的隐性认知跳跃
在我们的例子中,我们期望模型返回LandingPageConcept作为结果。通过要求模型输出某些字段,我们指导 LLM,就像人类市场营销人员或设计师在创建着陆页概念时可能采取的方式一样。
class LandingPageConcept(BaseModel):
campaign_desc_reflection: str # Encourages analysis of the campaign description
campaign_motivation: str # Prompts thinking about the 'why' behind the campaign
campaign_narrative: str # Guides creation of a cohesive story for the landing page
campaign_title_types: List[str]# Promotes brainstorming different title approaches
campaign_title: str # The final decision on the title
tone_and_style: List[str] # Defines the overall feel of the landing page
LandingPageConcept 结构鼓励 LLM 跟随类似人类的推理过程,模仿专家在直觉上所做的 微妙心理跃迁(隐性认知“跳跃”),就像我们在标准操作程序(SOP)中所建模的那样。
4.2 将复杂的过程分解为多个步骤/代理
对于复杂任务,将过程分解为多个步骤,每个步骤由单独的 LLM 调用或 "代理" 处理:
async def generate_landing_page(input_data: LandingPageInput) -> LandingPageOutput:
# Step 1: Conceptualize the campaign
concept = await generate_concept(input_data)
# Step 2: Select appropriate components
selected_components = await select_components(concept)
# Step 3: Generate content for each selected component
component_contents = {
component: await generate_component_content(input_data, concept, component)
for component in selected_components
}
# Step 4: Compose the final HTML
html = await compose_html(concept, component_contents)
return LandingPageOutput(concept, selected_components, component_contents, html)

多代理过程代码的示意图。(作者提供的图片)
这种多代理方法与人类解决复杂问题的方式一致——通过将问题分解成更小的部分。
5. 利用结构化数据(YAML)
YAML 是一种 流行的 人类友好的数据序列化格式。它设计上既便于人类阅读,又易于机器解析——这使得它成为 LLM 使用中的经典选择。
我发现 YAML 在 LLM 交互中特别有效,并且在不同模型之间能产生更好的结果。它将令牌处理集中在有价值的内容上,而非语法。
YAML 在不同的 LLM 提供者之间具有更强的可移植性,允许你保持结构化的输出格式。
async def generate_component_content(input_data: LandingPageInput, concept: LandingPageConcept,component: LandingPageComponent) -> ComponentContent:
few_shots = {
LandingPageComponent.HERO: {
"input": LandingPageInput(
brand="Mustacher",
product_desc="Luxurious mustache cream for grooming and styling",
# ... rest of the input data ...
),
"concept": LandingPageConcept(
campaign_title="Celebrate Dad's Dash of Distinction",
tone_and_style=["Warm", "Slightly humorous", "Nostalgic"]
# ... rest of the concept ...
),
"output": ComponentContent(
motivation="The hero section captures attention and communicates the core value proposition.",
content={
"headline": "Honor Dad's Distinction",
"subheadline": "The Art of Mustache Care",
"cta_button": "Shop Now"
}
)
},
# Add more component examples as needed
}
sys = "Craft landing page component content. Respond in YAML with motivation and content structure as shown."
messages = [{"role": "system", "content": sys}]
messages.extend([
message for example in few_shots.values() for message in [
{"role": "user", "content": to_yaml({"input": example["input"], "concept": example["concept"], "component": component.value})},
{"role": "assistant", "content": to_yaml(example["output"])}
]
])
messages.append({"role": "user", "content": to_yaml({"input": input_data, "concept": concept, "component": component.value})})
response = await client.chat.completions.create(model="gpt-4o", messages=messages)
raw_content = yaml.safe_load(sanitize_code_block(response.choices[0].message.content))
return ComponentContent(**raw_content)
请注意我们如何使用少样本示例来 “通过展示,而不是告诉” 预期的 YAML 格式。与提示中明确的输出结构指令相比,这种方法更为有效。
6. 精心设计你的上下文数据
仔细考虑如何将数据建模并呈现给 LLM。这一提示对于 上下文数据 是 LLM 三角原理 顶端的关键。
“即使是最强大的模型,也需要相关且结构化的上下文数据才能发挥作用。”
不要丢弃你所有关于模型的数据,而是将与你定义的目标相关的信息提供给模型。
async def select_components(concept: LandingPageConcept) -> List[LandingPageComponent]:
sys_template = jinja_env.from_string("""
Your task is to select the most appropriate components for a landing page based on the provided concept.
Choose from the following components:
{% for component in components %}
- {{ component.value }}
{% endfor %}
You MUST respond ONLY in a valid YAML list of selected components.
""")
sys = sys_template.render(components=LandingPageComponent)
prompt = jinja_env.from_string("""
Campaign title: "{{ concept.campaign_title }}"
Campaign narrative: "{{ concept.campaign_narrative }}"
Tone and style attributes: {{ concept.tone_and_style | join(', ') }}
""")
messages = [{"role": "system", "content": sys}] + few_shots + [
{"role": "user", "content": prompt.render(concept=concept)}]
response = await client.chat.completions.create(model="gpt-4", messages=messages)
selected_components = yaml.safe_load(response.choices[0].message.content)
return [LandingPageComponent(component) for component in selected_components]
在这个示例中,我们使用 Jinja 模板动态生成提示。这为每次 LLM 交互优雅地创建了聚焦且相关的上下文。
“数据为 LLM 原生应用提供动力。战略性地设计上下文数据能释放它们的真正潜力。”
6.1 利用少样本学习的力量
少样本学习是提示工程中必不可少的技术。向 LLM 提供相关的示例可以显著提高其对任务的理解。
请注意,在我们下面讨论的两种方法中,我们 重用我们的 Pydantic 模型进行少样本学习——这一技巧确保了示例和实际任务之间的一致性!不幸的是,我是通过艰难的方式学到这一点的。
6.1.1 示例 少样本学习
看看第五部分中的 few_shots 字典。在这种方法中:
示例作为单独的用户和助手消息添加到消息列表中,随后是实际的用户输入。
messages.extend([
message for example in few_shots for message in [
{"role": "user", "content": to_yaml(example["input"])},
{"role": "assistant", "content": to_yaml(example["output"])}
]
])
# then we can add the user prompt
messages.append({"role": "user", "content": to_yaml(input_data)})
通过将示例作为消息放置,我们与指令模型的训练方法保持一致。这使得模型在处理用户输入之前,能看到多个“示例交互”——帮助它理解预期的输入输出模式。
随着应用程序的增长,你可以添加更多的少量示例(few-shots)来涵盖更多的使用案例。对于更高级的应用,考虑实现动态少量示例选择,根据当前输入选择最相关的示例。
6.1.2 任务特定的少量示例学习
这种方法使用与当前任务直接相关的示例在 提示 中。例如,这个提示模板用于生成额外的独特卖点:
Generate {{ num_points }} more unique selling points for our {{ brand }} {{ product_desc }}, following this style:
{% for point in existing_points %}
- {{ point }}
{% endfor %}
这通过直接将示例包含在提示中,而不是作为单独的消息,提供了针对特定内容生成任务的有针对性的指导。
7. KISS — 保持简单,傻瓜
尽管像“思维树”(Tree of Thoughts)或“思维图”(Graph of Thoughts)这样的精妙提示工程技术很有趣,特别是在研究中,但我发现它们在实际生产中相当不切实际,并且往往过于复杂。对于真实的应用程序,重点应该放在设计合适的 LLM 架构(即工作流工程)上。
这同样适用于在你的 LLM 应用中使用代理。理解标准代理与自主代理之间的区别至关重要:
代理: “通过做 XYZ 把我从 A 带到 B。”
自主代理: “通过做某件事把我从 A 带到 B,我不在乎怎么做。”
虽然自主代理提供了灵活性和更快的开发速度,但它们也可能带来不可预测性和调试挑战。谨慎使用自主代理——只有在其优点明显超过潜在的控制损失和复杂性增加时才使用。

(由 Midjourney 生成)
8. 迭代,迭代,再迭代!
持续的实验对于改进你的 LLM 原生应用至关重要。不要对实验的想法感到害怕——它们可以像调整一个提示那么小。如在"构建 LLM 应用:清晰的逐步指南"中所述,建立基准并根据它跟踪改进是至关重要的。
像其他所有“AI”领域的技术一样,LLM 原生应用需要研究和实验的思维方式。
另一个好技巧是尝试在比你计划在生产中使用的模型性能更弱的模型上测试你的提示(例如,开源的 8B 模型)——在较小模型上表现“还行”的提示在更大模型上会表现得更好。
结论
这八个技巧为有效的提示工程提供了坚实的基础,适用于 LLM 原生应用。通过在提示中应用这些技巧,你将能够创建更可靠、高效和可扩展的 LLM 原生应用。
记住,目标不是创建最复杂的系统,而是构建一个在现实世界中有效的系统。继续实验、学习和构建——可能性是无限的。
如果你觉得这篇文章对你有帮助,请在 Medium 上给它一些掌声👏并分享给你身边的 AI 爱好者。你的支持对我意义重大!🌍
让我们继续保持对话——随时通过电子邮件或在 LinkedIn 上联系 🤝
特别感谢Liron Izhaki Allerhand和Yam Peleg;这篇文章基于我们之间的对话和见解。
数据科学面试的 8 个收获
·发表于Towards Data Science ·8 分钟阅读·2024 年 1 月 8 日
--

图片来源:Christin Hume在Unsplash上
好久没分享文章了,大家好!
免责声明:本文写于 2021 年 8 月,尽管所述原则依然适用,但其中提到的一些网站和功能可能已发生变化!
我很高兴在 2024 年写更多的文章,并分享我从数据科学转向数据工程的过程!
申请工作可能是一个耗时的过程,在这篇文章中,我总结了在最近求职过程中学到的一些经验。
在这段时间里,我参加了多个岗位的面试,每个岗位的面试流程都不同——面试的结构因岗位而异,但它们都以与内部招聘人员的非正式交谈开始,目的是了解更多关于岗位、公司以及是否适合我的信息。
从那时起,我经历了多种面试形式——包括与我经验相关的面试、带回家测试、展示带回家测试的结果、技术面试(SQL、定量分析、统计学、数据结构、算法)、案例分析问题和行为面试。
就个人而言,我并不喜欢带回家测试,尤其是在同时申请多个岗位时。我确实喜欢数据挑战,但当你因疫情、日常工作和准备其他岗位面试而感到疲惫时,我更希望能够在面试的环境中进行评估!
以下是我最近求职的主要收获!
1. 找一个简历伙伴

摄影师 João Ferrão 在 Unsplash 提供的照片
更新我的简历有时会在申请工作时拖延我,因为它是一个非常令人畏惧的任务,我倾向于推迟。除此之外,我经常在回忆我参与过的项目并突出与我申请职位相关的细节时感到卡壳——我实在是太完美主义了。
然而,这次我找到了一位简历搭档——一个也在更新简历的朋友。在几次视频通话中,我们轮流检查对方的简历,互相提供反馈,边说边做的过程帮助我们理清了重要细节。幸运的是,他们也在寻找数据相关的职位,因此了解哪些技能和经验是适用的,但最有价值的部分是我们在解释自己每个职位的工作内容时,另一方会总结并提取出相关要点!通过讨论我的工作,并受到提问的启发,我想起了更多的细节,并能更简洁地重新措辞。这是一个反复迭代的过程,持续了几周,我们每次都带着笔记回去工作,改进后再次汇合以进一步完善,最终我们都得到了令自己骄傲的简历。
2. 从共享资源开始
在几个申请过程中,招聘人员提供了对准备面试有帮助的资源链接。我简要浏览了这些资源,并花时间在我认为最相关的部分上,但有一次,我把一个 1.5 小时的 YouTube 视频留到后面看,因为它的长度让我觉得有点压倒性,加上其他许多我自己收集的资源。当我最终观看时,内容非常有用,我后悔没有早点看!
3. 研究公司和面试流程
另外,查看 Glassdoor 也是值得的,因为许多人会分享他们的面试经历和在面试中被问到的类型问题。我记下了很多这些问题,以确保自己能回答它们,这些问题在我大声练习回答时也派上了用场。Medium 上也有许多文章,人们分享了他们的数据科学面试准备经验,或者是针对特定公司的准备。我希望工作过的资源清单远比我准备的时间长,所以需要优先排序。如上所述,事后看来,我应该从招聘人员分享的资源开始,但我花更多时间在技术性较强的文章和教程上,以确保我掌握了这些技能,这些技能因为需要细致的理解,所以非常耗时!

摄影作品来自 Jason Goodman 在 Unsplash
4. 拆解它
如果像我一样,你需要并行准备多个面试,按照主题、所需时间和记忆保持来拆分面试内容会很有帮助。例如,如果你有两个面试,都将评估你的编码技能和案例分析能力,并且其中一个还包括行为面试,那么最好将行为面试的准备留到最后,因为你可能希望准备一些实际情况作为例子,可能会被提及。我发现这类问题最好在面试前一两天进行练习,这样它们能新鲜地记在脑海里。
对于编码,可能需要在你已有的技能基础上进行构建,并且可以通过每天练习一些问题或学习教程的某一部分来逐步提高,直到面试。如果你感觉某个领域特别薄弱,那么早些花时间集中攻克这个领域是很有帮助的,之后你可以继续练习相关问题,确保自己有信心并能够应用它 —— 这里再次分享我的经验,我对 SQL 窗口函数的使用经验较少,因此我初期花了相当多时间来学习和练习这些内容,这让我在第一次面试时感到很有信心,但在后续的 SQL 面试中,我没有再花时间练习这些内容,结果在压力下感到慌乱。在最终的 SQL 面试之前,我知道这个问题曾经让我出错过,因此再次花些时间进行练习,但到那时我已经失去了信心,只有一天时间准备,我并不像第一次面试时那样觉得准备充分!
对我来说,案例分析在面试中是一个相对较新的概念,因此需要一些阅读才能理解可能会被问到什么问题以及如何最好地准备。就个人而言,与某人一起大声讨论一个情境,并让对方提出后续问题,是最有帮助的准备方式。
5. 了解更多
在整个招聘过程中,我能够与公司内部的人员进行交流,询问有关为公司工作的相关问题,甚至还被提供了与某人交流面试准备的机会。我不确定这是否是常规做法,因为我以前从未遇到过,但进行模拟面试问题并被告知面试官会关注哪些方面,这对我帮助巨大!经过几轮面试后,每一轮面试都为下一轮面试做了很好的准备,因为问题类型和面试技巧都很相似。
另一个好的建议是利用你的社交网络,无论是与已经在公司工作的联系人取得联系,还是向你认识的最近刚找到新工作的朋友询问他们的面试经验——不同公司在数据职位的面试过程中有许多相似之处,因此了解他们的经历和任何可能的建议都是非常值得的。
6. 面试练习问题
下面是我在准备不同类型的面试时发现的一些有用资源:
统计学
Brilliant 提供了广泛的统计学和数学课程,我非常喜欢它们提问的方式,不过某些课程确实有内容重叠,因此值得选择性地完成某些部分,以免浪费时间。我还报名参加了 7 天的免费试用,这对访问更多内容非常有帮助,但也需要在这 7 天内专注于统计学。
SQL
Mode 提供了一个互动教程,可以访问 SQL 数据库,供你练习查询。

图片由 Justin Morgan 提供,来源于 Unsplash
案例分析
与 SQL 重叠的,Mode 提供了一些值得做的案例分析示例,并且包括了可以与自己答案对比的潜在解决方案。
常见问题
Interview query 的博客包含了一些有用的 SQL、统计学和案例分析问题,可以进行练习。我也尝试完成网站上的一些问题,但即便注册了账户,你也仅能执行 1 个 SQL 查询,并查看少量的解决方案,因此在我看来,博客会更有价值。
Medium 上有很多关于数据科学职位面试准备的文章!许多包含示例问题和主题的文章可能会出现,我觉得这些文章在练习问题时很有用,但它们有时也可能包含太多细节。例如,有一篇文章列出了大量数据科学和机器学习的概念,并附上了资源链接,这些对理解有很大帮助,但你不太可能需要学习所有这些概念来参加面试。
要实际评估准备时间、你已经拥有的知识基础,以及你对下一个职位的期望!
7. 实践
就像写简历时一样,在准备面试时一起合作也很有帮助。如果你的朋友申请的是类似类型的职位,为什么不一起练习一些技术性问题呢——可以选择一个话题,互相提问面试式问题,或者一起做案例分析,准备各自的答案——你会惊讶于你们能从彼此身上学到多少东西!
如果你的朋友不在相似的行业中,仍然可以一起练习行为类问题,或者找一份相关的面试问题清单,让别人问你这些问题,这样你就可以练习大声思考并清晰表达答案(例如,使用 STAR 方法)!
拥有在找工作时的朋友支持也很棒,他们可以在你面试时互相分享经历,彼此加油!我很喜欢听到我的朋友们在面试中大获全胜,并找到可以学习和成长的新职位!
8. 成长心态
无论面试结果如何,你在研究和学习中花费的时间都在帮助你成长!这次你可能没有找到合适的职位,但花时间反思你的面试经历、所学到的知识,并向面试官请求反馈,最终会帮助你顺利找到下一个职位!
反思
尽管面试是一个紧张而忙碌的过程,我仍然享受了花时间复习那些一段时间没用过的概念,我觉得自己因为这个变成了一个更好的数据科学家。我很感激自己被提供了并接受了一个新职位,在整个申请过程中,我对我的目标和下一个职位的期望有着清晰的认识。在这段时间里,我也能够反思自己在当前职位中学到了什么,以及未来希望建立的习惯或项目!
你最近在找工作吗?你有哪些重要的收获?
如果你喜欢这篇文章,你可以订阅我的通讯,以便第一时间将我的最新文章、我使用的资源和其他小贴士送到你的邮箱!
大多数数据科学课程未教的 8 件事(但你应该知道)——第二部分
数据科学 101
麻省理工学院称之为“计算机科学教育中缺失的学期”
·发布于 Towards Data Science ·阅读时间:10 分钟·2024 年 3 月 28 日
--

使用 Midjourney 创建。
数据科学和软件工程的共同点是写代码。但虽然代码是软件工程的主要成果,数据科学项目通常以模型、结果和报告结束。因此,在数据科学中,代码的质量、结构和交付通常充其量是事后的考虑。
数据科学项目的隐性期望是最终报告的结果是值得信赖的。
这意味着如果有人要求你重新运行你或他人的分析,你能够获得相同的结果,无论自你第一次执行分析以来已经过去了多少时间。
类似地,如果你正在为一个产品开发组件,隐含的期望是你开发的组件代表了在产品要求范围内尽可能最好的性能。
这些说法看起来可能显而易见,但满足这两种期望其实是相当困难的。
如果你不信,想想你过去的项目吧。
8 年数据之路:我希望从一开始就知道的事情
以及为有志成为数据专业人士的五个关键经验
·发表于Towards Data Science ·阅读时长 4 分钟·2024 年 6 月 18 日
--

图片由Mike Kononov提供,来源于Unsplash
引言
在回顾我 8 年的数据之路时,我经常被问到,数据工作究竟意味着什么。这里是一个诚实的回顾,去除了“AI”和“ChatGPT”这些流行词。
回到 2015 年。当时,我的任务是优化搜索引擎的在线营销活动,但我并不知道有一些至关重要的东西缺失了。直到有一个商业智能团队加入,他们的目标是“帮助我们生成数据驱动的洞察,以提高绩效。”最初我持怀疑态度——我以为自己已经知道如何做工作——但很快我发现了自己所忽视的深度。
与这个团队的密切合作让我接触到了整个数据分析的领域,打开了我对工作中一些我之前未曾注意到的方面的眼界。这次合作扩展了我的视野,并激发了我深入探索数据世界的兴趣。
在这里,我分享了我希望在开始这段旅程时就知道的五个教训,希望能为今天踏上数据探索之路的人提供一些指导。
1. 精通 SQL
我听说过一些人多年来宣称 SQL 已经“死亡”。然而,我坚信它仍然是几乎所有数据相关工作的基石。我个人一直在...
开始问你的数据“为什么?”——因果关系的温和入门
一份关于超越相关性思维的初学者指南。
·发表于Towards Data Science ·阅读时间:16 分钟·2024 年 9 月 12 日
--
因果关系不仅仅是统计学的一个方面——它是对统计学的补充——Judea Pearl

牛顿摆。图片来源:Pickpik。
相关性并不意味着因果关系。然而,事实证明,通过一些简单巧妙的技巧,我们可以在标准的观察数据中揭示因果关系,而无需依赖昂贵的随机对照试验。
本文面向任何做数据驱动决策的人。主要的核心信息是,因果关系是可能的,通过理解数据背后的故事和数据本身一样重要。
通过介绍辛普森悖论和伯克森悖论——即当一个总体的结果与其子群体的结果相冲突时——我强调了使用因果推理识别这些悖论并避免误解的重要性。具体来说,我介绍了因果图作为一种可视化数据背后故事的方法,并指出,通过将这一方法加入你的工具箱,你将能够进行更好的分析和实验。
我的最终目标是激发你对因果关系的兴趣,因为我相信通过…
9.11 还是 9.9——哪个更大?
评估 LLM 提示中的不确定性和脆弱性
·发表于Towards Data Science ·阅读时间 5 分钟·2024 年 7 月 25 日
--
这个 ChatGPT 提示及其对应的(错误的)回答最近在 LinkedIn 上被分享和转发了无数次。它们被作为证明 AGI 还远未实现的有力证据。进一步的转发还指出,将提示改为:“哪个更大:9.11 还是 9.9?” 可以保证得到正确答案,并进一步强调了 LLM 的脆弱性。
在对一组随机的 ChatGPT 用户进行评估后,我们发现,在两种情况中,答案大约 50%的时间是错误的。正如一些用户正确指出的那样,这个问题存在微妙的歧义,即我们是指两个实数的数学不等式,还是指两个日期(例如 9 月 11 日与 9 月 9 日),或者是指文档中的两个子部分(例如第 9.11 章或第 9.9 章)?
我们决定通过使用 OpenAI API 进行更受控的实验。通过这种方式,我们可以完全控制系统提示和用户提示;我们还可以通过设置低温度等方式尽可能地去除采样不确定性。
最终结果非常有趣!
假设和实验设计
我们的假设可以如下表述:
-
在相同的提示下,如果没有额外的上下文,并且将温度保持接近零,我们应该几乎总是得到相同的输出,且日志概率稳定。虽然人们常将 LLM 称为“随机的”,但对于给定的输入,LLM 应该总是生成相同的输出;所谓的“幻觉”或变异来自 LLM 之外的采样机制,这一点我们可以通过设置非常低的温度值来显著减弱。
-
基于我们与 ChatGPT 进行的随机用户测试,我们预期原始提示和重新措辞后的版本在 50% 的情况下给出错误答案——换句话说,若没有进一步的澄清或上下文,我们不认为某一个提示会比另一个表现得更好。
对于我们的实验设计,我们执行以下步骤:
-
我们进行了一系列实验,从原始提示开始,随后进行一系列的“干预”
-
对于每次实验/干预,我们执行 1 000 次试验
-
我们使用 OpenAI 最先进的 GPT-4o 模型
-
我们将温度设置为 0.1,以基本消除由采样引起的随机性;我们实验了随机种子和固定种子两种情况
-
为了评估答案的“信心”,我们收集了日志概率并计算每次试验中答案的线性概率;我们绘制了每次实验中 1000 次试验的线性概率的核密度估计(KDE)图
我们的实验设计的完整代码可以在这里找到。
实验 (A) — 原始提示
用户提示设置为“9.11 还是 9.9 — 哪个更大?”。
与社交媒体用户的反馈一致,GPT-4o 正确回答的概率为 55% ☹️。该模型的“信心”也并不高——在大量试验中,它对答案的“信心”约为 80%。

图 1 — 在 1000 次试验中,当使用原始用户提示时,信心值(0-100%)的平滑直方图(KDE);图片由作者提供
实验 (B) — 重新措辞的用户提示
在重新措辞的用户提示中,未提供额外的上下文/澄清,但措辞略作更改为:“哪个更大,9.11 还是 9.9?”
令人惊讶的是,与我们的 ChatGPT 用户测试相反,在 1000 次试验中正确答案的达成率为 100%。此外,模型对其答案表现出非常高的信心🤔。

图 2 — 在 1000 次试验中,当原始用户提示被稍微重新措辞时,信心值(0-100%)的平滑直方图(KDE);图片由作者提供
实验 (C) — 带推理的原始用户提示
最近在努力提高大语言模型“推理”能力方面做了大量工作,其中链式推理(CoT)提示法是最受欢迎的。黄等发布了关于 LLM 推理能力的非常全面的调查。
因此,我们修改了原始用户提示,要求大语言模型同时解释其推理。有趣的是,正确答案的概率提高到了 62%,然而答案伴随的是更大的不确定性。

图 3 — 在 1000 次试验中,当原始用户提示被修改为同时“解释其推理”时,信心值(0-100%)的平滑直方图(KDE);图片由作者提供
实验(D)— 原始用户提示与系统提示中的推理
最后的实验与实验“C”相同,但我们改为通过告诉 LLM“解释其推理过程”来引导系统提示。令人难以置信的是,我们现在看到100%正确的答案,并且信心极高。如果我们使用重新表述的用户提示,结果也完全相同。

图 4 — 信心值(0-100%)在 1000 次试验中的平滑直方图(KDE),使用原始用户提示,并且系统提示已修改,增加了“解释其推理过程”的指示;图像由作者提供
结论与总结
最初是一个简单的实验,旨在验证社交媒体上看到的一些说法,结果却得出了一些非常有趣的发现。让我们总结一下关键结论:
-
对于相同的提示,当温度设置非常低(基本消除采样不确定性)并且种子值固定时,我们看到对数概率的方差非常大。轻微的方差可以通过硬件精度来解释,但如此大的方差很难解释。这表明,可能有两种情况:(1)采样机制复杂得多,或者(2)在我们控制范围之外,还有更多的层级/模型在上游。
-
与之前的文献一致,仅仅指示 LLM“解释其推理过程”就能提高其表现。
-
系统提示与用户提示之间显然有明显的差异处理。在系统提示中引导角色,而非在用户提示中引导,似乎会显著提高性能。
-
我们可以清楚地看到提示是多么脆弱。这里的关键结论是,我们应该始终努力在提示中提供消歧义和明确的上下文。
免责声明: 由于社交媒体上的广泛讨论,OpenAI 的工作人员可能已经改进了上述行为,因此结果可能无法直接重现。但关键的结论仍然有效!
提升检索增强生成(RAG)系统的 9 种有效技术
ReRank、混合搜索、查询扩展、利用元数据等……
·发表于Towards Data Science ·阅读时间 7 分钟·2024 年 1 月 2 日
--

图像由 Bing 生成
2023 年是自然语言处理(NLP)历史上最富有成效的一年。这一时期,ChatGPT 以及众多其他大型语言模型的出现,既有开源的也有专有的。
与此同时,微调大型语言模型(LLM)变得更加容易,云服务提供商之间在生成式人工智能(GenAI)产品上的竞争也显著加剧。
有趣的是,个性化且功能完备的 RAG 需求在各行业中激增,每个客户都急于拥有自己量身定制的解决方案。
说到这一点,关于创建功能完善的 RAG,今天我们将讨论一篇 论文 ,该论文回顾了当前构建这些系统的最新技术。
不再浪费时间,让我们一起来看看 🔍
如果你对机器学习内容感兴趣,想要了解来自业界的详细教程和实用技巧,请关注我的新闻通讯。它叫做《The Tech Buffet》。
时间序列分析要点
通过 ChatGPT 详细解释的趋势分析和预测高级技术
·发布于Towards Data Science ·阅读时间 31 分钟·2024 年 8 月 13 日
--

DALL-E 3
分析时间序列绝不是一项简单的任务。我不会说它很难,但它总是复杂且微妙的,尤其是在你需要操作时间序列数据时。
什么是时间序列?
时间序列是指随着时间推移发生的事件。例如,一个公司的每日总销售额。公司销售产品,并在一天结束时计算总销售额:周一、周二、周三等的总销售额是多少。这就是一个时间序列。
时间序列的值——在这个例子中,是周二的总销售额——是否会影响周三(第二天)时间序列的值?这是分析时间序列的人可能想要回答的问题。
为了回答这个问题,我需要分析数据,检查我将采用的技术的假设,考虑时间因素,并经常应用滞后的概念,在某些时刻将数据移动到时间上。换句话说,这绝不是一项简单的任务,而是一项高水平的任务,因为它有助于解决各个领域的许多商业问题。
时间序列分析技巧:
2024 年从“忙碌”的数据科学家到高效数据科学家的 9 个简单技巧
这些技巧可以帮助你在新的一年成为最具生产力的数据科学家版本
·发布于Towards Data Science ·15 分钟阅读·2024 年 2 月 4 日
--

图片来源:Ümit Yıldırım于Unsplash
你真的忙吗,还是你只是效率低下?
每当感觉再多一个任务就会让我无法挽回地沉没时,我就会问自己这个问题。它让我脚踏实地,并让我思考在过去的一周里,我是否真正有效地利用了我的时间,还是只是在“忙碌”。
我们带着“忙碌”徽章,就像是在我们这个狂热追求工作的社会中佩戴某种身份象征。如果你不忙,就意味着你没有足够努力地工作,无法成为一名成功的数据科学家——总会有人比你更努力工作,一秒钟就能取代你的位置。但正如John Spencer 简洁地总结道: “你不会因为把日程安排得满满当当,承担更多的项目、成就和会议而获得奖杯。” 当然,我们下意识地知道这一点,但不知为何,我们仍然像是期待得到奖项、奖金或加薪一样工作——尽管我们都知道,除非我们在数据部门表现出色,否则我们的辛勤工作往往会被忽视。
因此,我认为没有比成为一名高效的数据科学家更好的 2024 年目标了。以下是我从“忙碌”的数据科学家转变为高效数据科学家的 9 个最佳技巧。
1. 制作任务优先级矩阵

这是一个帮助你优先排序任务的任务矩阵示例。我无法在Asana提供的这个示例上做出改进。虽然我们可能并非所有人都处于能够委派任务的位置(我开玩笑地把那些紧急但不重要的任务委派给我的狗),但这依然是一个相关的分类,你可以将任务放入其中(也许根据你的需求,只需要调整标题即可)。
你之前听过这个观点,在这里你还会再次听到:使用任务优先级矩阵将提高你作为数据科学家的生产力。
每当我感到任务堆积如山时,我会把它们都写在纸上,然后根据它们的紧急性和重要性将任务进行分类,使用与上面提到的艾森豪威尔矩阵相同的公式。这会立即提供一个清晰的图景,告诉你什么需要做,什么可以等待,什么是无关紧要的。这些任务可能是你为客户进行的数据分析中的特定任务(所以你会根据项目逐个建立矩阵),也可能只是你一整周的工作任务。
生产力源于专注于立即紧急的任务,并去除那些扰乱我们桌面、无关紧要的任务。在数据科学领域,大多数工作都集中在为截止日期交付成果上,这是一种确保你优先处理正确事项的便捷方式。
然而,同样重要的是,不要过于纠结于确保所有任务都按照我们认为的方式归类。你的老板不可避免地会给你一个你认为既不紧急也不重要的任务(比如把图表中的条形图颜色从红色改为橙色),但对他们来说,这个任务会是重要的。这不是一场需要打的战斗。然而,对于其余的常规任务,你通常可以通过这种方式进行组织,取得非常有效的结果。
2. 为每天、每周、每月以及每个项目设定目标
生产力源于明确的路线图,了解我们想要完成什么,以及我们计划如何实现它。年度决心无法支撑你度过每天、每周、每月和其中的各个项目,这就是为什么你需要相应地设定这些类型的目标。
如果没有为每天、每周、每月和每个项目设定目标,几乎看不到有人能够实现他们的大年度目标。就像人们在二月就失去完成新年决心的动力一样,如果你没有一些每日、每周、每月和项目导向的目标来推动自己,你就无法在数据科学领域拥有一个高效的工作年。
在数据科学工作环境中,大多数目标将由你的项目团队为你设定,这些目标都围绕着项目截止日期展开。然而,在实现你项目部分的目标时,你会有一些灵活性。为了确保你在实现这些整体目标时保持高效,你需要在其中加入自己的一套目标,确保每一天、每一周、每一月都充满高效的工作,直到项目截止日期为止。这些目标可以很小,比如“周五,我将阅读客户提供的文档,并准备好下周一会议的笔记和问题”,也可以很大,比如“在本月底之前,我将完善我的模型,使其能够给出统计显著的结果,p<0.05”。
无论你的目标是什么,你应该有一个填满目标的月度日历,并通过颜色编码标明它们是每日目标、每周目标、月度目标,还是与特定项目截止日期相关的目标。这为你提供了一张清晰的路线图,告诉你每天需要完成哪些任务以保持高效。
还需要注意的一点是,每天只能设定 2 到 3 个目标。要对自己设定的目标所需时间保持现实的认识,确保这些目标能够在合理时间内完成。通过设定可实现的每日目标,你确保每一天结束时都有一定程度的成功——谁不需要这种成就感呢?
3. 跟踪你的时间
你觉得自己每天工作多久?不,真的,像是实际工作的时间?
在许多流行设备上实现的“屏幕时间”功能,对我来说在思考我实际上花费多少时间在工作上产生了革命性的影响。你可能觉得自己在工作上很忙,但你的手机显示今天你在 Instagram 上花费的时间却完全不同。
跟踪你的时间可以成为一种改变游戏规则的策略,帮助你确定自己每天花费多少时间在工作上,每个任务花费了多少时间,以及这些时间是否真的是在进行有效的工作。它还能让你了解是否有些任务比预期的要花更多时间。例如,也许你在制作数据可视化方面花费了太多时间。通过创建样式表并严格遵循它,可以避免浪费时间在字体选择或确保颜色对所有观众可访问上。
有一些时间跟踪应用程序(我最喜欢的之一是Forest),但我发现使用手机上的计时器和便签应用是最简单的方式。不过,嘿,你是数据科学家,为什么不直接写代码来自动化这个任务呢?开始时,只需要在开始工作时启动计时器,工作完成时停止计时器,然后在手机上记录你工作了多长时间以及你正在进行的任务。
看着自己一天的工作时间在眼前,真的能让人眼前一亮。你可能被要求在 9 点到 5 点工作,但你真正工作了多少小时呢?更进一步,你会认为其中有多少小时是高效的?关于我们工作日内有多少小时是高效的,仍然没有定论,目前大多数人生产力的黄金时段大约在2–6个小时之间。从我个人进行数据分析和计算机建模的经验来看,我的高效工作时间似乎介于 5 到 6 小时之间——超过这个时间段,我的大脑就感觉像是被烧坏了。然而,需要注意的是,这取决于我所做的工作类型。深入研究代码或数据的技术工作(尤其是当它没有按预期工作时)比起写报告或制作漂亮的可视化图表更容易让我感到精疲力尽。每个人的情况都不同,因此记录你每天工作的感受,看看是否能开始发现一些任务、生产力和精力水平的模式,也不是一个坏主意。
4. 时间块管理
我不知道你怎么样,但如果我在开会、回复电子邮件,同时还在摸我的狗的时候写代码注释,那些注释根本没有意义。
虽然每个人都有适合自己的工作风格,但我想我们都能在 2024 年达成共识:多任务处理可能是迄今为止最无效的生产力技巧。相反,时间块管理已经开始取而代之,成为提高生产力的真正方法。
时间块管理是指你在一天内为特定任务分配某些时间段。例如,你的工作日的前两个小时可以分配给行政任务,如回复电子邮件和参加每日站会。接下来的一个小时可以集中处理代码缺陷积压。午餐后,你可以用三个小时来清理、准备和分析数据。你一天的最后一个小时用于参加几场会议。
为什么时间块管理能提高你的生产力?它通过避免可能打断你专注状态的干扰,使你能够专心投入到某项任务或特定类型的工作中。例如,如果你在调试代码时,团队成员不断走进办公室问你问题,你是无法取得任何进展的。当我们能够全身心地专注于需要完成的工作时,我们的最佳表现就能完成,而我们的“忙碌”工作则发生在有六个不同的干扰要求我们注意力时,导致我们无法专注于真正需要做的事。
在办公室环境中,时间区块管理可能会很有挑战,因为你容易受到周围人的干扰。然而,在共享日历空间中划定一天中的时间段,并确保在这些深度工作时段内办公室门保持关闭,是开始这一生产力习惯的一个好方法。
我是在重新开始处理数据和代码时发现了时间区块管理,并发现它是我生产力工具箱中最有效的工具。给自己三小时不间断地处理代码或数据,可以让你完全沉浸其中,更深入地理解数据如何相互结合,并且能更清晰地了解代码应该如何工作。换句话说,三小时的专注工作将比六小时的中断工作产生更好的、更高效的结果。
5. 设置一套生产力工具工作流程,即使用 Git 进行版本控制,使用 Trello 或 Notion 来组织项目,使用 Scribe Chrome 扩展来创建可视化的逐步指南,安装网站屏蔽器等。
作为数据科学家,你的生产力潜力与周围使用的工具息息相关。
首先,挑选你工作中需要组织和简化的三个领域。例如,这可能是你的代码版本控制、日常计划与项目规划,以及当代码停止工作时你容易分心并开始陷入“末日滚动”的习惯。从这里开始,决定是以数字化方式还是模拟方式处理这些领域——换句话说,你是需要一款电脑上的工具,还是需要一个物理解决方案?对于代码版本控制,你需要一个数字版本控制库,比如 GitHub 提供的版本库。处理日常计划和项目规划时,你可以选择模拟或数字方式。选择模拟方式可能意味着买一个日程本来安排日程,买一个挂历来管理项目。数字方式的话,一些常用的工具包括 Notion,可以用于日常安排和项目规划,Trello 是一个非常方便的项目规划工具。最后,解决“末日滚动”问题,可以通过在工作设备上安装网站屏蔽器并将手机设置为“请勿打扰”来轻松做到。这样,你就建立了一个生产力工作流程!
生产力工作流是独特的系统,这意味着在过程中会涉及试错。例如,我发现自己喜欢使用 GitHub Desktop 来提交代码,但不想从命令行使用 Git。同样,我用 Notion 来规划我的项目,但仅用手机上的日历来安排我的一天。此外,如果我需要进行深度工作,我需要将手机放在完全不同的房间。通过像 Medium 和 YouTube 这样的网站查找其他人的生产力工作流是一个很好的主意,可以为你提供灵感和关于哪些工具最适合你的想法,但绝不应该将你的工作流建立在别人身上。可能需要几个月的时间来完善你的系统,但当你不再需要思考它时,你就会知道它是适合你的。
6. 提高你的沟通技能,并重新考虑什么才是真正需要开会的
尽管许多在 TowardsDataScience 上与求职相关的文章强调数据科学家沟通技能的重要性,但我们中许多人在这些技能上承认不如自己希望的那样优秀。我们可能擅长向客户传达数据故事,但在能够真正产生影响的地方——与团队沟通时,我们可能做得不够好。
对于数据科学家来说,没有什么比团队成员频繁打断或发 Teams 消息要求重新解释需要完成的任务更能拖慢生产力了。更糟糕的是,在一个对哪些话题或危机需要开会没有明确概念的团队中工作。虽然这两种情况都有例外,比如和实习生一起工作或者真的发生了重大问题,但通常这些小小的生产力不便是可以消除的。一切都始于强大而清晰的沟通。
在个人层面,开始练习你的沟通技巧,尤其是在解释任务、概念和结果时。这可以像和你的 桌面鸭 或妈妈交谈,写博客文章和在线文章,或者制作 YouTube 教程一样简单。无论你选择哪种方法,练习沟通,直到你能够一次性清晰表达你的观点,且没有后续问题。如果你确实收到后续问题,看看你能否在不需要第二次解释的情况下回答。清晰简洁的沟通一次解决每个问题 = 更多的“心流”状态生产力。
在团队层面,设定一些团队标准(更重要的是,执行并遵守这些标准),明确什么情况下需要开会。无论是虚拟会议还是面对面的会议,通常如果没有真正解决问题,会议往往会消耗生产力和精力。这个简单的任务是提高团队合作与沟通的快捷方式,而且可以轻松调整,以适应可能出现的工作流和项目。
同样,今年也要努力成为一个更好的倾听者。因为不理解别人说的内容或不知道如何做某事而请别人重复一遍是完全可以接受的;而因为自己没有听清楚就要求别人重复,就不太合适了。
7. 在工作日的早期处理艰难任务,工作日的后期处理简单任务
无论是早上 6 点还是下午 6 点,你的一天开始时通常是最专注、最坚持和最充满活力的时候。那时正是你需要攻克当天最艰难任务的时候。
无论是清理数据、制作演示文稿还是修复数据管道,你都应该在工作日的早期处理难度较大的任务,把简单的事情留到后面。
我们都熟悉工作中的午后低潮,那时你的眼睛几乎睁不开,感觉自己已经盯着同一个 bug 看了 30 分钟,却始终没有真正解决它。这个时候不应该尝试修复你的计算机模型,因为大多数情况下,你可能会让问题变得更糟,或者更有可能的是,根本没有朝着“修复”方向前进。
确保你的艰难任务在第一时间完成的最佳方法是通过制作当天的任务清单来开始一天的工作。参考你的任务优先级矩阵(参见上面的第一个提示)在这里会非常有用。挑选 1 到 2 个你知道会让你精疲力竭的任务,首先处理这些 — 其余的任务可以稍后再做。把那些不费脑筋或你觉得简单的任务留到工作日的中后段,那个时候你通常会感到比较迟钝和缺乏灵感。你会发现,经过几天这样的实践后,你的一天会开始得更有成效,直到下班时间都保持高效。
8. 成为更好的团队成员,以优化协作和任务分配
我曾在一个团队工作,其中一名成员是首次担任这种规模的团队领导,并且对团队的表现非常关心。这导致这位团队领导不信任团队成员能够完成他们的工作,虽然进行了任务分配,但总是导致团队领导深度介入,常常直到任务截止前才会放手。自然,这导致一切都只能在最后一分钟完成,团队领导总是要求在最后一刻进行修改,直到任务提交的最后一秒。这常常表现为大量“忙碌”的工作,随后是一两天 12 小时的加班,尝试在截止日期前进行更改。这样的工作环境不健康,每个项目都拖到最后一秒,不是吗?
从这个故事中,我希望你能领悟到,优化团队合作是确保你作为数据科学家生产力的最佳方式之一。无论你是一个小型创业公司的独立数据科学家,还是大公司中一个数据专业团队的成员,建立有效的团队间和团队内系统对你的生产力至关重要。
这一切归结于团队协作和任务分配的团队规范——这两者都要求各方信任对方能够按设定标准和截止日期完成工作。虽然我们许多人在工作时可能会感觉像是身处孤岛,但重要的是要记住,我们的结果和交付物往往依赖于其他团队成员完成他们的任务。因此,团队应尽早聚集,制定标准来决定谁需要一起工作,哪些任务需要分配给谁,以及在项目截止日期之前这些任务需要完成的时间,以确保有足够的时间进行修改。
现在这些标准很少能完美执行,因为,坦率地说,这就是现实世界——如果我们不不断走向无序的状态,那我们就有可能在令人担忧的方式上违背了物理定律。数据可能比预期更难处理,团队成员可能在他们负责可视化数据时突然生病,而这些数据只有他们理解,你的预测模型可能返回的结果充其量是可疑的,而你的主管可能在交付物即将发送之前几秒钟给你最后一刻的修正。然而,即使只是让每个团队成员在心中牢记上述建议的标准,也足以让每个人更加意识到如何更高效地协作。每次让这一点更有效的方式是继续阅读,看看如何通过月度反思来评估生产力策略的有效性。
9. 每个月结束时花些时间反思哪些方法有效,哪些无效,哪些地方可以做得更有效
在一个月结束时,坐下来,诚实地对自己说:“究竟发生了什么?”并不是一件坏事。然而,你不应该养成每个月都这样结束的习惯。重要的是你如何回应并继续前进。
压力是生产力的快速敌人,这也是为什么每个月结束时退后一步是个好主意(此时可能已经经历了许多压力大的事情,难免将你拉回“忙碌”状态),看看哪些方法有效,哪些方法无效,以及下个月如何提高生产力。也许这意味着你需要为数据存储开发更好的文件夹系统,或者你需要将更多的解释性任务委派给分析师,让科学家可以专注于更具预测性的任务。也许你只需要坚定决心,把手机放进时间锁安全盒,避免“末日滚动”,而是专心解决当前像意大利面一样堆积的、充满 bug 的代码。
无论你面临什么挫折或干扰,都请在每个月的最后花些时间,诚实地写下是什么提高了你的生产力,是什么削弱了它,以及你将尝试在下个月实施或继续做些什么,来防止自己陷入“忙碌”状态。
订阅,直接将我的故事发送到你的收件箱:故事订阅
请通过我的推荐链接成为会员,获得无限制访问 Medium 的权限(此举不会对你产生额外费用,我将获得少量佣金):Medium 会员
订阅我的新闻通讯,获取更多具有环保主义视角的数据驱动内容:数据驱动环保主义者
构建 AI 助手的 10 分钟模板
以及你今天为什么应该开始构建自己的 AI 助手
·发表于Towards Data Science ·阅读时长 10 分钟·2024 年 5 月 5 日
--

所有图片均由作者通过截图和 Midjourney 提供。
AI 代理是人工智能领域的下一个大趋势。想象一下,完全自动驾驶的汽车,机器人帮你报税,还有 AI 导师在教你的孩子。
“嘿,Jarvis!请遛狗,预定下周末的旅行,顺便把我的收入翻倍。”
但让我们现实一点。代理——我指的是完全的自主代理——离被解决还有很长的路要走。这是因为代理涉及计划、世界模型和资源管理,而这些正是机器学习的弱项。
构建自主代理是如此困难,以至于公司们要么改变“代理”一词的定义,要么试图找到绕过难题的办法。
有些公司将两个笨重的大型语言模型(LLM)拼凑在一起,假装它们已经弄明白了代理(嗨,AutoGPT)。其他公司则展示了半成品的虚假演示,吹嘘其不成熟的功能(你好,Devin)。
然而,这并非全是炒作和空洞的言辞。少数几个参与者正在努力接近目标,给我们展示了代理(agents)可能的样子——而且……
一个 6 个月的详细计划,帮助你打造初级数据科学作品集
创建、打磨和部署一个作品集的逐步指南,帮助你找到第一份工作
·发表于 Towards Data Science ·阅读时间 11 分钟·2024 年 11 月 8 日
--

如果你刚刚完成学位或正在寻找第一份工作,这篇文章就是为你准备的。如果你还在攻读学位或尚未开始你的数据科学之旅,你可能需要先查看这篇文章。
正如你所知道的,数据科学的就业市场比以往任何时候都更具竞争力。仅仅拥有学位或学术项目不足以让你从人群中脱颖而出。你需要实际的、动手的项目,展示你在实践中的技能。
对于那些不认识我的人,我的旅程始于十年前,当时我从一所工程学院获得了应用数学学位。从那时起,我在多个行业工作过,从水务到能源,还曾担任讲师。我也曾招聘过初级数据科学家,现在我在这里教你如何打造完美的作品集,帮助你找到第一份工作。
今天的菜单 🍔
-
🍛 如何规划你的 6 个月之旅,打造你的数据科学作品集。
-
🍔 开始前的准备工作。
-
🥤这 8 个项目将让你的……
量子生成对抗网络基础介绍
一种量子-经典混合方法用于合成数据生成
·发布于 Towards Data Science ·阅读时间 9 分钟 ·2024 年 9 月 21 日
--
随着量子硬件的进步,量子优势在特定数据生成任务中具有潜力,可能超越经典方法。量子生成对抗网络(QGANs)是合成数据生成中的一项有前景的进展,特别适用于表格数据。
量子电路:量子计算的通用语言
正如 Scott Aaronson 所说,一旦你把物理学剥离出来,量子计算就变得非常简单了。我们使用的量子电路就像是量子计算机的食谱或操作手册。它们逐步描述了对量子比特(经典比特的量子版本)进行哪些操作,以执行量子计算。这些电路能够表示和操作复杂的概率分布,而经典神经网络可能在这方面会遇到困难。这可能导致更准确地建模复杂的模式和表格数据中的相关性。通常,量子系统可以有效地表示和处理多维数据。对于具有大量特征的表格数据集,这可能会导致更紧凑和稳健的模型。这些系统具有固有的随机性,这可能在生成多样且真实的合成样本时非常有用,从而提升生成数据的整体质量和多样性。量子系统的概率性质……
面向初学者的 LLM 简介
大型语言模型的第一步
·发表于Towards Data Science ·13 分钟阅读·2024 年 5 月 10 日
--

我一直想写一篇关于大型语言模型(LLM)的教程,之后我一直在思考如何编写一系列适合初学者的文章,帮助大家理解并入门 LLM。在这篇文章中,我将尝试提供一个面向初学者的 LLM 简介,并以简单的方式解释关键概念,而不深入探讨技术细节。我的希望是,阅读完这篇文章后,你能更加自信地阅读关于 LLM 的更高级文档。
不是 Medium 会员?没关系!继续阅读通过这个朋友链接。
目录
· 1. 引言
· 2. LLM 的定义
· 3. 一些 LLM
∘ 3.1. BERT 家族
∘ 3.2. GPT 家族
∘ 3.3. PaLM 家族
∘ 3.4. LLaMA 家族
· 4. LLM 系统
∘ 4.1. 一般架构
∘ 4.2. 训练过程
∘ 4.3. LLM 的输入与输出
· 5. 使用场景
· 6. 如何调整 LLM
∘ 6.1. 微调
∘ 6.2. 提示词设计
· 7. 挑战
· 8. 结论
使用 ChatGPT 建立数据科学作品集网站:完整教程
从零开始使用生成式 AI 创建一个引人入胜的作品集网站(+免费提示工程速成课程)
·发表于 Towards Data Science ·阅读时间:21 分钟·2024 年 5 月 1 日
--

图片来自 Lee Campbell 在 Unsplash
几年前,一位雇主找到了我的个人作品网站,并聘用了我作为数据分析实习生。
我现在已经完成了职业转型,正在全职担任数据科学家。
这个就是让我获得这份工作的作品网站。
我是通过 HTML 和 CSS 这两种前端网页开发框架创建了这个网站的。整个过程花了我几周的时间。
然而,通过 ChatGPT,你可以在不到 30 分钟的时间里,使用HTML 和 CSS建立一个像这样的网页——即使你对这些编程语言一无所知。
而且我将向你展示如何做到这一点。
如果你想看视频版,请点击 这里。
你将学到的内容
从视频构建知识图谱的入门指南
构建一个管道来分析并存储视频中的数据。
·发表于Towards Data Science ·阅读时间:10 分钟·2024 年 1 月 17 日
--
在深入讨论本文的技术内容之前,让我们先设定一下背景,回答一个你可能有的问题:什么是知识图谱?
为了回答这个问题,想象一下,不是将知识存储在文件柜中,而是将它们存储在一张织网中。每一个事实、概念、关于人、地点、事件甚至抽象概念的信息都是一个节点,而将它们连接在一起的线条则是它们之间的关系。这张错综复杂的网络,朋友们,就是知识图谱的精髓。

图片由Shubham Dhage提供,来自Unsplash
可以把它想象成一张繁忙的城市地图,不仅显示街道,还揭示了地标、公园和商店之间的联系。同样,知识图谱不仅仅是存储冷冰冰的事实,它捕捉了事物如何相互联系的丰富画面。例如,你可能知道玛丽·居里发现了镭,然后通过一条线索发现镭被用于医学治疗,进而与医院和癌症研究相关联。你看到了吗?一个事实如何自然而然地引导到另一个事实,勾画出一个更大的图景。
那么,为什么这种像地图一样存储知识的方式如此流行呢?嗯,想象一下在线搜索信息。传统的方法常常让你得到的是零散的片段,就像你只看到地图上的建筑物,却不知道连接它们的街道。而知识图谱则像是带你进行一场旅行,从一个事实引导到另一个事实,就像有一个友好的向导在信息世界的每个角落悄悄地讲述迷人的故事。很有趣,对吧?我知道。
自从我发现了这个魔法,它就吸引了我的注意力,我探索并玩弄了许多潜在的应用。在这篇文章中,我将向你展示如何构建一个管道,从视频中提取音频,然后将音频转录,再从转录的文本中构建一个知识图谱,从而实现对视频中信息的更加细致和互联的表示。
我将使用 Google Drive 来上传视频样本。我还将使用 Google Colab 编写代码,最后,你需要有 GPT Plus API 的访问权限来进行这个项目。我将把步骤分解清楚,确保对初学者来说易于理解:
-
设置一切。
-
从视频中提取音频。
-
将音频转录为文本。
-
构建知识图谱。
到了本文的最后,你将构建一个如下所示的图谱。

作者提供的图片
让我们直接开始吧!
1- 设置一切
如前所述,我们将使用 Google Drive 和 Colab。在第一个单元格中,让我们将 Google Drive 连接到 Colab,并创建我们的目录文件夹(video_files、audio_files、text_files)。下面的代码可以完成这项工作。(如果你想跟着代码一起操作,我已经将这个项目的所有代码上传到 GitHub,你可以从 这里访问它。)
# installing required libraries
!pip install pydub
!pip install git+https://github.com/openai/whisper.git
!sudo apt update && sudo apt install ffmpeg
!pip install networkx matplotlib
!pip install openai
!pip install requests
# connecting google drive to import video samples
from google.colab import drive
import os
drive.mount('/content/drive')
video_files = '/content/drive/My Drive/video_files'
audio_files = '/content/drive/My Drive/audio_files'
text_files = '/content/drive/My Drive/text_files'
folders = [video_files, audio_files, text_files]
for folder in folders:
# Check if the output folder exists
if not os.path.exists(folder):
# If not, create the folder
os.makedirs(folder)
或者,你也可以手动创建文件夹,并将视频样本上传到“video_files”文件夹,随你更方便的方式来做。
现在我们有了三个文件夹,其中“video_files”文件夹中有一个视频样本,用于测试代码。
2- 从视频中提取音频
接下来,我们要做的是导入视频并从中提取音频。我们可以使用 Pydub 库,它是一个高级音频处理库,可以帮助我们完成这项工作。让我们看看代码,然后再在下面解释。
from pydub import AudioSegment
# Extract audio from videos
for video_file in os.listdir(video_files):
if video_file.endswith('.mp4'):
video_path = os.path.join(video_files, video_file)
audio = AudioSegment.from_file(video_path, format="mp4")
# Save audio as WAV
audio.export(os.path.join(audio_files, f"{video_file[:-4]}.wav"), format="wav")
安装完我们的包 pydub 后,我们从 Pydub 库中导入了 AudioSegment 类。接着,我们创建了一个循环,遍历我们之前创建的“video_files”文件夹中的所有视频文件,并通过 AudioSegment.from_file 将每个文件加载为音频。然后,使用 audio.export 将加载的音频导出为 WAV 文件,并保存到指定的“audio_files”文件夹中,文件名与视频文件相同,但扩展名为 .wav。
此时,你可以去 Google Drive 中的“audio_files”文件夹,那里可以看到提取的音频。
3- 音频转录为文本
在第三步,我们将把音频文件转录为文本文件,并将其作为 .txt 文件保存在“text_files”文件夹中。在这里,我使用了 OpenAI 的 Whisper ASR(自动语音识别)系统来完成这项工作。我之所以选择它,是因为它简单且相当准确,并且它有不同的模型可以提供不同的准确度。不过,模型越准确,模型的体积就越大,加载速度也会越慢,因此我将使用中等精度的模型来进行演示。为了使代码更简洁,我们将创建一个函数来转录音频,然后使用循环对目录中的所有音频文件应用这个函数。
import re
import subprocess
# function to transcribe and save the output in txt file
def transcribe_and_save(audio_files, text_files, model='medium.en'):
# Construct the Whisper command
whisper_command = f"whisper '{audio_files}' --model {model}"
# Run the Whisper command
transcription = subprocess.check_output(whisper_command, shell=True, text=True)
# Clean and join the sentences
output_without_time = re.sub(r'\[\d+:\d+\.\d+ --> \d+:\d+\.\d+\] ', '', transcription)
sentences = [line.strip() for line in output_without_time.split('\n') if line.strip()]
joined_text = ' '.join(sentences)
# Create the corresponding text file name
audio_file_name = os.path.basename(audio_files)
text_file_name = os.path.splitext(audio_file_name)[0] + '.txt'
file_path = os.path.join(text_files, text_file_name)
# Save the output as a txt file
with open(file_path, 'w') as file:
file.write(joined_text)
print(f'Text for {audio_file_name} has been saved to: {file_path}')
# Transcribing all the audio files in the directory
for audio_file in os.listdir(audio_files):
if audio_file.endswith('.wav'):
audio_files = os.path.join(audio_files, audio_file)
transcribe_and_save(audio_files, text_files)
使用的库:
-
os: 提供与操作系统交互的方式,用于处理文件路径和文件名。
-
re: 用于模式匹配和替换的正则表达式模块。
-
subprocess: 允许创建额外的进程,这里用来从命令行执行 Whisper ASR 系统。
我们创建了一个 Whisper 命令,并将其保存为变量以便于处理。之后,我们使用 subprocess.check_output 来运行 Whisper 命令,并将生成的转录结果保存在 transcription 变量中。但此时的转录结果并不干净(你可以通过在函数外打印 transcription 变量来检查,它包含时间戳和一些与转录无关的内容),因此我们添加了一个清理代码,使用 re.sub 去除时间戳并将句子合并在一起。之后,我们在“text_files”文件夹中创建了一个与音频文件同名的文本文件,并将清理后的转录结果保存到其中。
现在,如果你去“text_files”文件夹,你可以看到包含转录结果的文本文件。哇,第三步成功完成!恭喜!
4- 构建知识图谱
这是关键部分——可能也是最长的部分。我将采用模块化方法,使用 5 个函数来处理这项任务,但在此之前,让我们先从必要的库和模块开始,包括用于发出 HTTP 请求的 requests,用于处理 JSON 数据的 json,用于处理数据框的 pandas,以及用于创建和可视化图形的 networkx 和 matplotlib。同时,还需要设置全局常量,这些常量是在代码中各个地方使用的变量。API_ENDPOINT 是 OpenAI API 的端点,API_KEY 是存储 OpenAI API 密钥的位置,prompt_text 将存储用作 OpenAI 提示输入的文本。所有这些都在这段代码中完成。
import requests
import json
import pandas as pd
import networkx as nx
import matplotlib.pyplot as plt
# Global Constants API endpoint, API key, prompt text
API_ENDPOINT = "https://api.openai.com/v1/chat/completions"
api_key = "your_openai_api_key_goes_here"
prompt_text = """Given a prompt, extrapolate as many relationships as possible from it and provide a list of updates.
If an update is a relationship, provide [ENTITY 1, RELATIONSHIP, ENTITY 2]. The relationship is directed, so the order matters.
Example:
prompt: Sun is the source of solar energy. It is also the source of Vitamin D.
updates:
[["Sun", "source of", "solar energy"],["Sun","source of", "Vitamin D"]]
prompt: $prompt
updates:"""
接下来,让我们继续分析我们函数的结构:
第一个函数,create_graph(),该函数的任务是使用 networkx 库创建一个图形可视化。它接收一个 DataFrame df 和一个边标签字典 rel_labels(将在下一个函数中创建)作为输入。然后,它使用 DataFrame 创建一个有向图,并利用 matplotlib 进行可视化,经过一些自定义后输出我们需要的漂亮图形。
# Graph Creation Function
def create_graph(df, rel_labels):
G = nx.from_pandas_edgelist(df, "source", "target",
edge_attr=True, create_using=nx.MultiDiGraph())
plt.figure(figsize=(12, 12))
pos = nx.spring_layout(G)
nx.draw(G, with_labels=True, node_color='skyblue', edge_cmap=plt.cm.Blues, pos=pos)
nx.draw_networkx_edge_labels(
G,
pos,
edge_labels=rel_labels,
font_color='red'
)
plt.show()
DataFrame df 和边标签 rel_labels 是下一个函数 preparing_data_for_graph() 的输出。这个函数将作为输入的 OpenAI api_response(将在下一个函数中创建)进行处理,并从中提取实体关系三元组 (source, target, edge)。这里我们使用 json 模块解析响应并获取相关数据,然后过滤掉缺失数据的元素。之后,根据这些三元组构建一个知识库 DataFrame kg_df,最后创建一个字典 (relation_labels),将节点对与对应的边标签进行映射,最终返回 DataFrame 和字典。
# Data Preparation Function
def preparing_data_for_graph(api_response):
#extract response text
response_text = api_response.text
entity_relation_lst = json.loads(json.loads(response_text)["choices"][0]["text"])
entity_relation_lst = [x for x in entity_relation_lst if len(x) == 3]
source = [i[0] for i in entity_relation_lst]
target = [i[2] for i in entity_relation_lst]
relations = [i[1] for i in entity_relation_lst]
kg_df = pd.DataFrame({'source': source, 'target': target, 'edge': relations})
relation_labels = dict(zip(zip(kg_df.source, kg_df.target), kg_df.edge))
return kg_df,relation_labels
第三个函数是 call_gpt_api(),它负责向 OpenAI API 发送 POST 请求并输出 api_response。这里我们构造了一个包含模型信息、提示词和其他参数(如模型(此处为:gpt-3.5-turbo-instruct)、max_tokens、stop 和 temperature)的数据负载。然后通过 requests.post 发送请求并返回响应。我还加入了简单的错误处理,如果发生异常,将打印错误信息。try 块包含可能会在执行过程中抛出异常的请求代码,因此如果在此过程中发生异常(例如网络问题、API 错误等),except 块中的代码将会被执行。
# OpenAI API Call Function
def call_gpt_api(api_key, prompt_text):
global API_ENDPOINT
try:
data = {
"model": "gpt-3.5-turbo",
"prompt": prompt_text,
"max_tokens": 3000,
"stop": "\n",
"temperature": 0
}
headers = {"Content-Type": "application/json", "Authorization": "Bearer " + api_key}
r = requests.post(url=API_ENDPOINT, headers=headers, json=data)
response_data = r.json() # Parse the response as JSON
print("Response content:", response_data)
return response_data
except Exception as e:
print("Error:", e)
然后倒数第二个函数是 main() 函数,它协调脚本的主流程。首先,它读取之前在“text_files”文件夹中的文本文件内容,并将其保存到变量 kb_text 中。接着获取存储我们提示词的全局变量 prompt_text,然后用文本文件内容 kb_text 替换提示模板中的占位符 ($prompt)。接下来调用 call_gpt_api() 函数,传入 api_key 和 prompt_text,以获取 OpenAI API 的响应。然后将响应传递给 preparing_data_for_graph() 函数,以准备数据并获取 DataFrame 和边标签字典,最后将这两个值传递给 create_graph() 函数来构建知识图谱。
# Main function
def main(text_file_path, api_key):
with open(file_path, 'r') as file:
kb_text = file.read()
global prompt_text
prompt_text = prompt_text.replace("$prompt", kb_text)
api_response = call_gpt_api(api_key, prompt_text)
df, rel_labels = preparing_data_for_graph(api_response)
create_graph(df, rel_labels)code
最后是 start() 函数,它会遍历我们“text_files”文件夹中的所有文本文件——如果有多个文件,它会获取文件的名称和路径,并将其与 api_key 一起传递给主函数以完成任务。
# Start Function
def start():
for filename in os.listdir(text_files):
if filename.endswith(".txt"):
# Construct the full path to the text file
text_file_path = os.path.join(text_files, filename)
main(text_file_path, api_key)
如果你正确地按照步骤操作,运行 start() 函数后,你应该能看到类似的可视化效果。

图片来源:作者
你当然可以将这个知识图谱保存到 Neo4j 数据库中,并进一步拓展。
注意:此工作流仅适用于你拥有的视频,或其条款允许这种下载/处理的情况。
摘要:
知识图谱利用语义关系来表示数据,从而实现更细致和具有上下文意识的理解。这种语义丰富性使得查询和分析更加复杂,因为实体之间的关系被明确地定义了。
在本文中,我概述了如何构建一个流程,包括从视频中提取音频,使用 OpenAI 的 Whisper ASR 进行转录,以及构建知识图谱的详细步骤。作为对这一领域感兴趣的人,我希望本文能使初学者更容易理解,展示知识图谱应用的潜力和多样性。
这段代码的完整版本可以在GitHub找到。
初学者探索关键数学概念:应用数据分析简化版
·发布于Towards Data Science ·18 分钟阅读·2024 年 11 月 26 日
--
我从未在数学上处于顶尖水平,但我也不差,总是对它有某种兴趣。同时,我曾经长时间认为:如果你数学不行,就不能从事计算机科学、数据分析、数据科学或 IT 工作。经过几次绕道,我最终进入了 Salesforce 顾问的角色,并成功完成了商业信息学和人工智能学位,专攻软件架构与设计,并取得了优异的成绩。
无论我是在使用 Python 编程、创建机器学习模型还是进行数据分析,我总是会遇到数学概念。在使用数据分析、自动化或 Python 编程时,我认为不必了解每一个数学话题的细节。但数学知识确实经常出现。几个月前,我想刷新我对数学和逻辑的理解,偶然发现了‘Brilliant’这个应用程序(不是合作链接)。该应用以非常简单的方式解释了诸如概率和回归模型等话题。它的酷之处在于,应用的结构与 Duolingo 类似,几乎让人上瘾,得益于其游戏化设计……阅读到文章的最后,看看我已经完成了多少连胜💪
在这篇文章中,我将向你介绍一些对于应用数据分析至关重要的数学话题。如果你是初学者或中级水平,想要深化对这些关键概念的理解……
类别编码器的基准与分类
新的。全面的。可扩展的。
·发布于 Towards Data Science ·12 分钟阅读·2024 年 3 月 29 日
--

图片由作者使用 recraft.ai 创建
许多数据集包含类别特征。例如,在UC Irvine 机器学习库 [1]中的 665 个数据集中,有 42 个是完全类别的,366 个被报告为混合类型。然而,基于距离的机器学习模型和几乎所有scikit-learn 实现都要求特征采用数值格式。类别编码器通过将这些特征中的类别替换为实数来实现这一点。
存在多种类别编码器,但很少有尝试在多个数据集上、使用各种机器学习模型、以及在不同的管道中进行比较。本文介绍了我们近期发表的论文 [2](海报,GitHub 上的代码)中的一个最新的编码器基准。在本文中,我将重点介绍补充出版物的内容,并且这些内容在实际中具有重要意义。特别地,除了我们基准结果的总结外,我还将:
-
提供 55 个类别编码器的列表,并附上大多数编码器的解释和实现链接。
-
解释你也可以使用我们的代码作为对
[Category Encoders](https://contrib.scikit-learn.org/category_encoders/)Python 模块的补充,来处理那些尚未在该模块中实现的编码器。 -
将编码器归类为不同的家族,这样你就不必记住每个单独的编码器,而是能够了解如何构建每个家族中的成员。
-
解释如何利用[2]中的代码和详细的基准数据,将你的编码器、数据集或机器学习模型纳入比较中,而无需重新运行现有的实验。根据你实验的范围和计算资源,这可以节省你几周的计算时间。
为什么要做另一个基准测试?
已经有几项科学研究比较了分类编码器[3–12],并且至少有一个分类编码器的基准测试在 TDS 上[13]。我将在这里展示的研究与其他研究的主要区别在于范围:我们比较了来自不同编码器家族的代表性编码器,并在多种配置下进行了测试。我们实验了5个机器学习模型(决策树、kNN、SVM、逻辑回归、LGBM),4个质量评估指标(AUC、准确率、平衡准确率、F1 分数),3个调优策略(我会稍后介绍),50个数据集,和32个编码器配置。
下表展示了我们基准测试中涵盖的编码器(深绿色列)以及其他实验比较中涉及的编码器(浅绿色列)。蓝色列展示了在另外两个来源中描述的编码器:一篇专门介绍对比编码器的文章[15]和 Medium 上的文章[14]:
总结 54 篇 Medium 故事
最后一列黄色列展示了[Category Encoders](https://contrib.scikit-learn.org/category_encoders/)模块[16]涵盖的编码器。需要注意的是,[2]中的代码实现了一些编码器——来自相似性、分箱和数据约束家族——这些编码器并不属于Category Encoders模块。此外,我们发现用 R 实现的 GLMM 编码器,其速度比Category Encoders中的 GLMM 编码器要快。因此,对于这些实现,你可能会发现我们的代码更有用。

表 1. 编码器家族及其在各种资源中的覆盖情况。作者拥有版权
表 1 已经包含了多个编码器,但这个列表并非详尽无遗。为了更好地理解编码器的全貌,分类编码器是非常有用的,这有助于理解编码器设计的原理,而不是仅仅记住大量的单独编码器。
编码器家族
接下来,我们考虑一个具有n个长度和k个基数的分类特征。从高层次来看,分类编码器可以分为监督式或无监督式。
1. 无监督编码器 在编码过程中不包括目标变量。
1.1. 标识符编码器 使用单射函数转换分类变量,即它们为每个类别分配一个唯一的数值或唯一的数值组合。它们会创建从 1 到 k 个新的特征。例如,独热编码器创建 k 个特征,标签编码器或有序编码器创建一个新特征,Base N 编码器创建 ⌈log(k)⌉ 个新特征,其中对数以 N 为底。
这些编码器对于表示唯一标识符的分类变量非常有用,例如产品代码或邮政编码。通常,除了有序编码器外,标识符编码器并不假设类别之间存在任何内在顺序,也不认为该顺序对分析有意义,因此在编码过程中忽略任何此类顺序。
1.2. 对比编码器 通过基于类别之间的比较分配数值来转换分类变量。通常,一组 k-1 个新特征代表一个具有 k 类别的分类变量。与标识符编码器不同,这些编码器专门设计用于探索回归分析中分类变量不同水平之间的关系。
要创建对比编码器,可以选择不同的 编码方案 [15]。每种编码方式以特定方式将一个类别与其他类别对比,用以检验与数据相关的假设。例如,Helmert 编码将每个水平与随后的水平均值对比,而求和编码将每个水平与总体均值对比。
1.3. 频率编码器 用与数据集中类别出现频率相关的值替换分类变量的类别。与标识符编码器或对比编码器不同,频率编码器只创建一个新的特征,并不一定是单射函数。它们提供了类别出现频率的数值表示,这在机器学习模型需要理解类别值的普遍性时尤为有用。表 1 中的所有三种频率编码器都是频率的单调递增函数。然而,这并不是定义这一组编码器的必要条件。
1.4. 相似性编码器 [5, 8, 18] 通过应用捕捉不同类别之间相似性或距离的度量,将分类数据转换为数值形式。
一类相似度编码器[8, 18]基于将两个类别视为字符串进行形态学比较。相似度度量的示例包括 Levenshtein 比率、Jaro-Winkler 相似度或 N-gram 相似度。然后,类别变量被编码为一个向量,其中每个维度对应于参考类别与所有类别的成对比较,值表示计算出的相似度得分(类似于构造方差-协方差矩阵)。这一组的编码器通常会创建k个新特征。这种编码特别适用于处理“脏”类别数据集,这些数据集可能包含拼写错误和冗余[18]。可以将 One-Hot 编码视为相似度编码的一种特例,其中相似度度量只能取两个值:0 或 1。
另一类相似度编码器[5],包括如嵌入(Embeddings)、最小哈希(Min-Hash)或伽马-泊松矩阵分解(Gamma-Poisson matrix factorization),设计用于高基数类别(k>>1)。它们将类别特征投射到低维空间。因此,这一组与分箱编码器相似,分箱编码器同样在处理大k时特别有用,但不旨在保留类别的形态相似性。
2. 有监督编码器 使用目标变量的信息。对于回归或二分类任务,它们通常为每个类别变量创建一个新的特征。
2.1. 简单目标编码器 捕捉类别特征与目标变量之间的关系。构建简单目标编码器涉及计算每个类别水平上基于目标变量的统计量。常见的统计量包括目标变量的均值、中位数或按每个类别条件的概率比。
该过程如下:
-
对每个类别水平,按类别变量对数据进行分组。
-
在每个组内,计算所需的统计量。
-
对数据中的每个实例,用相应的统计量替换原始类别值。
简单目标编码存在过拟合的风险,尤其是在数据集较小或类别观察值非常少的情况下。诸如平滑(正则化)和交叉验证等技术(我们稍后会介绍)可以帮助缓解这一风险。
2.2. 平滑编码器 是简单目标编码器的推广,它引入了一个平滑参数。该平滑参数的目的是防止过拟合,并提高编码器对新数据的泛化能力,特别是在类别观察值较少时。平滑值的常见公式为:

其中,m是数据集中类别出现的次数。它可能会略有变化,见[13]。通过调整平滑参数,您可以控制类别统计量和整体统计量之间的平衡。较大的平滑参数使得编码对类别特定的目标统计量不那么敏感。将上述公式中的平滑参数设置为零将产生简单的目标编码。
2.3. 数据约束编码器使用数据集子集中的信息。数据约束编码器的一个例子是留一法编码器,它对于给定的分类值,计算类别出现的所有其他实例中的目标变量统计量,但不包括当前实例。这种方法确保给定数据集的编码值不包括其自身的目标值。
数据约束策略有助于创建更能代表未见数据中真实关系的编码,并且不易过拟合。编码列中的唯一值数量可能会超过原始分类列的基数k。还可以在数据约束编码中引入平滑。
3. 分箱编码器可以是有监督的也可以是无监督的。通常,它们的工作可以分为两步。第一步通过将原始类别排序到箱中来创建一个辅助的分类特征。第二步对这个辅助特征应用来自上述其他组中的一个编码器。两步可以独立地是有监督的或无监督的。例如,在第一步中,您可以通过将稀有类别分组在一起形成箱(无监督的,例如,One-Hot MC 编码器);或者,您可以应用简单的目标编码器,将具有相似编码值的类别分组在一起(有监督的,例如,离散化目标编码器)。
一些分箱编码器,例如 Hashing,不具有这种两阶段结构。分箱编码器创建的新特征的数量通常是<k。
调优策略
在继续讨论结果之前,我将简要总结我们基准测试中的调优策略。这对于理解以下图表的范围以及重用我们基准测试中的数据和代码非常重要。三种调优策略如下:
无调优:
-
对训练数据中的分类列进行编码;
-
使用默认超参数训练机器学习模型。
模型调优:
-
对训练数据中的分类列进行编码;
-
将训练数据拆分为折叠;
-
使用交叉验证优化机器学习模型的超参数。
完全调优:
-
将训练数据拆分为折叠;
-
分别对每个训练折叠进行编码(如果数据限制了编码器家族,每个折叠将进一步拆分为嵌套折叠);
-
使用交叉验证优化机器学习模型的超参数。
结果
我将列出我们实验中获胜的编码器,指出哪种调优策略表现最佳并应成为你机器学习流程的一部分,同时展示各个编码器的运行时间。
排名

图 1. 所有实验中编码器的性能。作者拥有版权
在图 1 中,箱线图展示了编码器在所有数据集、质量指标和调优策略下的排名分布。也就是说,每个箱线图包含大约 50×4×3=600 个单独模型的实验,以及大约 50×4×3×5=3000 个所有模型的实验,排除了因超时或其他原因失败的少数实验。我们观察到,四个编码器:独热编码、二进制编码(图中标为‘Bin’)、求和编码和证据权重编码,在所有实验中始终表现最佳。对于逻辑回归,这四个编码器与其他编码器的差异在统计上是显著的。这个结果有些令人惊讶,因为许多先前的研究(见 [13, 14])报告了无监督编码器的缺点,特别是独热编码。在图 1 中,其他编码器缩写的具体含义对接下来的内容并不重要,但你可以在 [2] 中找到这些缩写的定义。
调优策略

图 2. 完全调优与不调优策略的性能提升。作者拥有版权

图 3. 完全调优与模型调优的性能提升。作者拥有版权
图 2 显示了完全调优与不调优策略之间的性能差异(如我上面所描述),针对每个类别编码器。每个图表都涵盖了不同数据集、机器学习模型和性能指标的实验。图 3 是一个类似的图表,比较了完全调优与模型调优策略。请注意,这些图表未包括一些机器学习模型和数据集,因为我们限制了计算复杂度。有关详细信息和更多图表,请参见 [2]。
基于这些结果,我一般建议坚持采用完全调优策略,即在优化机器学习模型的超参数时,分别为每个折叠单独编码数据;对于数据约束编码器来说,这一点尤其重要。
运行时间

图 4. 编码器的运行时间。作者拥有版权
最后,图 4 绘制了数据集编码所需时间的对数坐标图。简单的目标和分箱编码器是最快的,而平滑和数据约束编码器最慢。表现最好的四个编码器——独热编码、二进制编码(图中标为‘Bin’)、求和编码和证据权重——在处理数据时所需时间合理。
重用代码和结果
我们基准测试的一个优点是你可以轻松扩展它来测试你自己的分类编码方法,并将其置于现有实验结果的背景中。例如,你可能想测试表 1 中不在我们基准测试中的编码器,或者你可能想测试自定义的复合编码器,例如 n/k>a 时使用 One-Hot MC,否则使用 One-Hot,其中 k 是特征基数,n 是数据集的大小,像之前一样。我们在GitHub上解释了这个过程,并在Kaggle 笔记本 [17]中展示了如何实现。
为了便于说明,我们将示例限制在未调参的逻辑回归模型中。我们基准测试的相应结果如下图 5 所示。

图 5. 逻辑回归下无调参策略的编码器排名。作者拥有版权。
现在假设Yury Kashnitsky(他友善地对我们的论文提供了反馈)想知道哈希编码器是否与我们评估的编码器具有竞争力。在[17]中,我们展示了如何进行缺失的实验,并发现哈希编码器在我们选择的超参数下表现合理,如图 6 所示。

图 6. 添加哈希技巧编码器后的编码器排名。作者拥有版权。
结论
我总结了我们的分类编码器基准测试,并解释了你如何从我们共享的文档中受益。你可能学到了:
-
分类编码器,作为机器学习模型,可以是监督式的或无监督式的。
-
我介绍了八种编码器类别。某些编码器保持分类特征的基数,其他编码器则能减少基数(通常是分箱的情况,但也适用于其他一些编码器,如朴素目标编码)或增加基数(数据约束)。
-
在我们的实验中,One-Hot、Binary、Sum 和 Weight of Evidence 编码器的平均表现最好,特别是在逻辑回归模型下。
-
请参见Kaggle 笔记本 [17],其中展示了如何将所需的编码器添加到基准测试并绘制结果;必要的代码可以在GitHub上找到。
致谢
如果我的同事和[2]的第一作者,Federico Matteucci,没有编写基准测试和 Kaggle 笔记本[17]的代码,这个故事就不会存在。
资源
[1] 加州大学欧文分校机器学习数据集
[2] Matteucci, F., Arzamasov, V., & Böhm, K. (2024). 分类编码器的基准测试,用于二分类。神经信息处理系统进展,36。(论文, GitHub 上的代码, 海报)
[3] Seca, D., & Mendes-Moreira, J. (2021). 名义特征编码在回归中的基准测试。在世界信息系统与技术大会(第 146–155 页)。Cham:Springer 国际出版社。
[4] Pargent, F., Pfisterer, F., Thomas, J., & Bischl, B. (2022). 正则化目标编码在具有高基数特征的监督机器学习中优于传统方法。计算统计学,37(5),2671–2692。
[5] Cerda, P., & Varoquaux, G. (2020). 高基数字符串分类变量编码。IEEE 知识与数据工程学报,34(3),1164–1176。
[6] Potdar, K., Pardawala, T. S., & Pai, C. D. (2017). 神经网络分类器的分类变量编码技术的比较研究。国际计算机应用期刊,175(4),7–9。
[7] Dahouda, M. K., & Joe, I. (2021). 一种用于分类特征编码的深度学习嵌入技术。IEEE Access,9,114381–114391。
[8] Cerda, P., Varoquaux, G., & Kégl, B. (2018). 用于脏分类变量学习的相似性编码。机器学习,107(8),1477–1494。
[9] Wright, M. N., & König, I. R. (2019). 随机森林中的分类预测变量分裂。PeerJ,7,e6339。
[10] Gnat, S. (2021). 分类变量编码对物业质量估值的影响。Procedia 计算机科学,192,3542–3550。
[11] Johnson, J. M., & Khoshgoftaar, T. M. (2021 年 8 月). 高基数特征和集成学习器的编码技术. 收录于 2021 IEEE 第 22 届国际数据科学信息重用与集成会议 (IRI)(第 355-361 页)。IEEE。
[12] Valdez-Valenzuela, E., Kuri-Morales, A., & Gomez-Adorno, H. (2021 年). 使用合成数据测量类别编码器在机器学习任务中的效果. 收录于 计算智能进展:2021 年墨西哥国际人工智能会议(MICAI 2021),墨西哥城,2021 年 10 月 25–30 日,会议论文集,第 I 部分(第 92-107 页)。Springer 国际出版。
[13] 类别编码器基准测试(TDS 上的故事)
[14] 类别编码:关键见解(我在 Medium 上的故事)
[15] R 语言类别变量对比编码系统
[16] 类别编码器(Python 模块)
[17] 向基准添加自定义编码器(Kaggle 笔记本)
[18] 使用 dirty_cat 进行脏类别的相似度编码(TDS 上的故事)
《线性代数一瞥:矩阵链乘法》
线性代数是人工智能的基础。矩阵乘法是线性代数的基本操作。如果存在一整个矩阵链呢?
·发表于Towards Data Science ·23 分钟阅读·2024 年 11 月 13 日
--

图像由 MidJourney 创建
这是正在进行中的线性代数书籍的第四章。到目前为止的目录:
-
第一章:基础知识
-
第二章:映射的度量——行列式
-
第四章(当前章节):矩阵链乘法
-
第五章:方程组、线性回归与神经网络
-
第六章:秩、虚秩及行秩 == 列秩
我们在第三章中讨论了矩阵乘法,以及它为什么以这种方式定义。我们还从五种不同的方式进行了可视化展示。花一章的篇幅讨论这个非常重要的操作是值得的,因为它在许多领域都至关重要。
当涉及到两个矩阵时,很快就会涉及到更多的矩阵。在矩阵乘法中,其中一个矩阵通常会分裂成两个,以此类推,直到我们得到一整条需要相乘的矩阵链。而与两个矩阵的情况不同,这时就不再只有一个矩阵,而是……
线性代数的鸟瞰图:正交归一矩阵
正交归一矩阵:线性代数中最优雅的矩阵
·发布于 Towards Data Science ·17 分钟阅读·2024 年 12 月 24 日
--

图片由 midjourney 创建
这是正在进行中的线性代数书籍《线性代数的鸟瞰图》的第八章。目前的目录如下:
-
第二章:映射的度量 — 行列式
-
第五章:方程组、线性回归与神经网络
-
第六章:秩与零度,为什么行秩 == 列秩
-
第七章:左右逆 => 单射-满射映射
-
第八章(当前):正交归一矩阵
在本章中,我们将讨论特殊类型的矩阵:正交矩阵和正交规范矩阵。这些类型的矩阵(以及它们所表示的对应线性映射)具有良好的性质,从理论到数值计算,使得它们易于使用。例如,要获取正交规范矩阵的逆,你只需将其翻转(取其转置)。但我们现在有些超前了。让我们先理解什么是正交规范矩阵。
线性代数的鸟瞰图:秩-零度定理及行秩为何等于列秩
为什么所有矩阵的独立行向量的数量必须与独立列向量的数量完全相等?
·发表于 Towards Data Science ·20 分钟阅读·2024 年 1 月 30 日
--

图片来自作者。由 midjourney 制作。
这是《线性代数的鸟瞰图》这本正在编写的书的第五章。到目前为止的目录:
-
第二章:映射的度量——行列式
-
第五章:方程组、线性回归与神经网络
-
第六章:秩与零度及为何行秩 == 列秩
-
第七章:左右逆矩阵 => 单射-满射映射
-
第八章(当前):正交矩阵
在之前的章节中,我们讨论了现代机器学习和人工智能应用是如何通过将图像、文本、音频等各种对象编码到高维向量空间中来工作的。而我们为处理高维向量空间所开发的基本工具是线性代数。
语言模型在文本生成中的演变概述
·发表于Towards Data Science ·阅读时间 8 分钟·2024 年 7 月 10 日
--
在本文中,我想分享一些关于语言模型(LM)在过去几十年发展过程中的笔记。本文可以作为一个温和的入门介绍,帮助理解语言模型历史中的概念要点。值得一提的是,我并没有深入探讨实现细节和背后的数学原理,但本文的描述层次足以让人正确理解语言模型的演变。
什么是语言建模?
一般来说,语言建模是将语言,特别是自然语言,形式化的过程,目的是使其机器可读并能够以各种方式处理。因此,这不仅仅是生成语言的问题,还是关于语言表示的问题。
由于生成式人工智能(GenAI)的普及,“语言建模”这一概念通常与文本生成过程紧密相关。因此,本文从文本生成的角度,探讨语言模型的演变过程。
N-gram 语言模型
尽管 n-gram 语言模型的基础在 20 世纪中期就已经奠定,但这类模型的广泛应用始于 1980 年代和 1990 年代。
n-gram 语言模型利用了马尔科夫假设,该假设在语言模型的背景下指出,下一个词的概率仅依赖于前一个词。因此,利用 n-gram 语言模型对给定上下文的词的概率进行近似,可以形式化为以下方式:

给定一系列前面的词,预测下一个词的概率可以通过给定 N 个前面词的概率来近似(例如,N=2 — 二元语言模型)。(图片来源:作者)
其中t是整个序列中的单词数,N是上下文的大小(单字模型(1),双字模型(2)等)。现在,问题是如何估计这些 n-gram 概率?最简单的方法是使用 n-gram 计数(可以通过“无监督”的方式在大文本语料库中计算):

给定前 N 个单词的下一个单词的概率:分子——结果序列在数据中出现的次数,分母——前一个单词序列在数据中出现的次数(图片来自作者)
显然,以上方程中的概率估计可能显得过于简单。如果分子甚至分母的值为零怎么办?这就是为什么更高级的概率估计方法包括平滑或回退(例如,加法平滑,愚蠢回退,Kneser-Ney 平滑)。不过我们在这里不会深入探讨这些方法,然而从概念上讲,概率估计方法在任何平滑或回退方法下都不会改变。n-gram 语言模型的高级表示如下所示:

n-gram 语言模型的高级表示(图片来自作者)
得到计数之后,我们如何从这样的语言模型中生成文本呢?实际上,这个问题的答案适用于下面要讨论的所有语言模型。从语言模型中根据概率分布选择下一个单词的过程叫做采样。以下是适用于 n-gram 语言模型的几种采样策略:
-
贪婪采样——选择具有最高概率的单词;
-
随机采样——根据概率选择下一个随机单词
分布。
前馈神经网络语言模型
尽管有平滑和回退,n-gram 语言模型的概率估计仍然直观上过于简单,无法建模自然语言。Yoshua Bengio 等人(2000 年)提出的一个具有改变游戏规则的创新方法非常简单,却很有创新性:如果我们用神经网络来估计单词概率,而不是使用 n-gram 计数,会怎么样?尽管论文声称递归神经网络(RNN)也可以用于这个任务,但主要内容集中在前馈神经网络(FFNN)架构上。
Bengio 提出的 FFNN 架构是一个简单的多类分类器(类别数量为词汇表的大小V)。训练过程基于预测上下文单词c中的缺失单词w的任务:P (w|c),其中|c|是上下文窗口大小。Bengio 等人提出的 FFNN 架构如下所示:

FFNN 架构用于下一个单词概率估计(图片来自作者)
这样的基于 FFNN 的语言模型可以在大规模文本语料库上以自监督的方式进行训练(即不需要显式标注的数据集)。
那么采样呢?除了贪婪和随机策略外,还有两种可以应用于基于神经网络的语言模型的策略:
-
top-k 采样 —— 与贪婪相同,但在经过重新归一化的 top-k 集合中进行
词汇(softmax 在 top-k 词汇上重新计算),
-
nucleus 采样 —— 与 top-k 相同,但使用百分比而不是 k 作为数字。
循环神经网络语言模型
到目前为止,我们一直假设下一个词的概率仅依赖于前一个(或多个)词。我们还考虑了固定的上下文或 n-gram 大小来估计概率。如果词与词之间的连接也很重要呢?如果我们想要考虑整个前置词序列来预测下一个词呢?这可以通过 RNN 完美建模!
自然地,RNN 的优势在于它们能够捕捉整个词序列的依赖关系,同时将来自前一步(t-1)的隐藏层输出添加到当前步(t)的输入中:

简单 RNN 在步骤 t 处的隐藏层输出计算(图片来自作者)
其中 h — 隐藏层输出,g(x) — 激活函数,U 和 W — 权重矩阵。
RNN 还在大规模文本语料库上按照自监督的设置进行训练,以预测给定序列的下一个词。然后通过所谓的 自回归生成 过程进行文本生成,这也被称为 因果语言建模生成。下面演示了使用 RNN 进行的自回归生成:

使用 RNN 进行自回归文本生成和其他列出的示例(图片来自作者)
在实践中,经典的 RNN 很少用于语言模型任务。相反,有改进的 RNN 架构,例如堆叠和 双向,长短期记忆(LSTM) 及其变体。
Sutskever 等人(2014) 提出了最著名的 RNN 架构之一 —— 编码器-解码器(或 seq2seq)基于 LSTM 的架构。与简单的自回归生成不同,seq2seq 模型将输入序列编码为中间表示 —— 上下文向量 —— 然后使用自回归生成解码它。
然而,最初的 seq2seq 架构存在一个主要瓶颈——编码器将整个输入序列压缩为一个表示——上下文向量。为了消除这个瓶颈,Bahdanau 等人(2014 年)提出了注意力机制,它(1)为每个解码器隐藏状态生成一个单独的上下文向量(2)基于加权的编码器隐藏状态。因此,注意力机制的直觉是每个输入词都会影响每个输出词,而且这种影响的强度是变化的。
值得一提的是,基于 RNN 的模型被用于学习语言表示。特别是,最著名的模型包括ELMo(2018 年)和ULMFiT(2018 年)。
评估:困惑度
在没有将语言模型应用于特定任务(例如机器翻译)的情况下,有一个通用的衡量标准可以帮助我们了解语言模型的表现。这一标准叫做困惑度。

困惑度公式(图片由作者提供)
其中,p — 词的概率分布,N — 序列中的总词数,wi — 表示第i个词。由于困惑度使用了熵的概念,其直觉是模型对预测序列的不确定程度。困惑度越低,模型的预测不确定性越小,因而模型的预测能力越强。
Transformer 语言模型
现代最先进的语言模型(LMs)利用了前一段提到的注意力机制,特别是自注意力机制,它是Transformer 架构的一个重要组成部分。
Transformer 语言模型(LMs)在计算效率方面相较于 RNN 语言模型(RNN LMs)具有显著优势,因为它们能够并行化计算。在 RNN 中,序列是逐步处理的,这使得 RNN 的速度较慢,尤其是处理长序列时。相比之下,Transformer 模型使用自注意力机制,能够同时处理序列中的所有位置。下面是带有LM 头的 Transformer 模型的高层表示。

简化版的 Transformer 语言模型架构(图片由作者提供)
为了表示输入标记,Transformer 将标记和位置嵌入加在一起。通常,最后一个 Transformer 层的最后隐藏状态用于通过 LM 头生成下一个词的概率。Transformer 语言模型是按照自监督范式进行预训练的。当考虑解码器或编码-解码器模型时,预训练任务是预测序列中的下一个词,类似于之前的语言模型。
值得一提的是,自从 transformer(2017 年)的诞生以来,语言建模的最大进展体现在两个主要方向: (1) 模型规模扩展 和 (2) 指令微调,包括基于人类反馈的强化学习。
评估:指令基准
经过指令微调的语言模型被认为是通用问题解决者。因此,困惑度可能不是最好的质量度量标准,因为它隐含地计算了这类模型的质量。评估经过指令微调的语言模型的显式方法是基于指令基准,
例如大规模多任务语言理解(MMLU)、代码的 HumanEval、数学问题求解(MATH)等。
总结
我们在这里考虑了语言模型在文本生成中的演变,涵盖了至少过去三十年的发展。尽管没有深入探讨细节,但可以清楚地看到自 1990 年代以来,语言模型的发展历程。
n-gram 语言模型通过使用 n-gram 计数和应用平滑方法来近似下一个单词的概率。为了改进这种方法,提出了前馈神经网络架构来近似单词的概率。尽管 n-gram 模型和 FFNN 模型仅考虑了固定数量的上下文,并忽略了输入句子中单词之间的联系,但 RNN 语言模型通过自然地考虑单词之间的联系以及整个输入令牌序列填补了这一空白。最终,transformer 语言模型在计算效率上优于 RNN,并利用自注意力机制生成了更多上下文化的表示。
自 2017 年 transformer 架构的发明以来,语言建模领域的最大进展被认为是模型规模扩展和指令微调,包括基于人类反馈的强化学习(RLHF)。
参考文献
我想感谢 Dan Jurafsky 和 James H. Martin 的《语音与语言处理》一书,这本书是本文的主要灵感来源。
其他参考资料已作为超链接包含在文中。
后记
如果你想了解更多有关在实际工业用例(如 AI 助手、基于代理的系统等)中应用 LLM 的知识,请通过[contact (at) perevalov (dot) com]与我联系或访问我的网站。
《论集成方法——数据科学家的最佳朋友》
在机器学习模型中利用群体智慧。
·发表于 Towards Data Science ·14 分钟阅读·2024 年 12 月 17 日
--

图片来源:Luca Upper 在 Unsplash
近年来,我们理所当然地使用维基百科或 Reddit 等资源——这些资源依赖于个体贡献者的集体知识,为我们提供大多准确的信息,这种现象有时被称为“群体智慧”。其思想是,集体决策可能比任何个人的判断更为准确,因为我们每个人可能有自己的隐性偏见和/或知识缺乏,从而导致判断中的一定误差。集体的这些误差可能会相互抵消——例如,我们可以弥补某人在某一领域知识/专业技能的不足,而他们则在其他领域弥补我们的不足。将这一思想应用到机器学习中就形成了“集成”方法。
从一个非常高的层次来看,我们训练机器学习模型是为了对未来进行预测。换句话说,我们为模型提供训练数据,希望模型能够对未来做出准确的预测。但如果我们能训练多个机器学习模型,然后以某种方式汇总它们对于预测的看法会怎样呢?事实证明,这是一种非常有用的方法,并且在行业中被广泛应用。
近距离观察 AI 痛点,以及如何(有时)解决它们
·发布于 Towards Data Science ·以 新闻通讯 形式发送 ·阅读时间:3 分钟·2024 年 9 月 26 日
--
想要写你在 TDS 上的第一篇文章吗?我们始终欢迎新作者的投稿。
在短短几年内,AI 驱动的工具已经从(相对)小众的产品,面向具有专业技能的受众,发展到广泛且迅速被采纳的产品——有时是被那些并未完全理解其权衡和局限性的组织所采纳。
这种巨大的转变几乎可以确保出现失误、瓶颈和痛点。无论是个人还是团队,目前都在应对一项新兴技术的复杂地形,这项技术伴随着许多尚未解决的难题。
本周,我们将重点介绍几篇出色的文章,这些文章以清晰且务实的方式解决了这个难题。从处理幻觉问题到为特定使用场景做出正确的产品选择,它们正面应对了 AI 的一些最大痛点。它们可能不会为每个可能的场景提供完美的解决方案——在某些情况下,解决方案甚至可能还不存在(或许?)——但它们可以帮助你以正确的心态应对自己的挑战。
-
为何 GenAI 是一个数据删除和隐私噩梦 “尝试删除已经融入大规模语言模型的训练数据,就像试图在蛋糕烤好后去除糖分一样。”Cassie Kozyrkov再次回到 TDS,深入分析了在基于用户数据训练模型时可能出现的隐私问题,以及在事后才引入保护措施时解决这些问题的困难。
-
通过 ARTKIT 暴露 LLM 应用中的越狱漏洞 对 LLM(大规模语言模型)产品固有的安全和隐私风险的理解日益增加,尤其是那些复杂的“越狱”技术,可以通过一些坚持和耐心绕过开发者设置的任何数据保护措施。Kenneth Leung在他最新的文章中展示了这一问题的紧迫性,文章探讨了如何使用开源的 ARTKIT 框架自动评估 LLM 安全漏洞。

图片由Paul Bergmeir提供,来源于Unsplash
-
在 LLM 代理框架之间的选择 AI 代理的兴起为自动化和简化繁琐的工作流提供了新的机会,但也提出了关于如何将合适的工具与合适的任务匹配的紧迫问题。Aparna Dhinakaran的详细概述解决了 ML(机器学习)产品经理在选择代理框架时面临的最大困境之一:“你是选择长期存在的 LangGraph,还是选择新兴的 LlamaIndex Workflows?或者你走传统路线,自己编写整个框架?”
-
我如何处理 AI 初创公司中的幻觉问题 “想象一下,AI 将发票金额误读为 100,000 美元,而不是 1,000 美元,导致超额支付了 100 倍。” 如果基于 LLM 的聊天机器人幻觉出一个错误的饼干食谱,你最终得到的是不可食用的零食。如果它用错误的信息回应商业查询,你可能会犯下非常昂贵的错误。从依赖较小的模型到利用基础方法,Tarik Dzekman提供了避免这种命运的实用见解,这些见解都基于他在文档自动化和信息提取方面的工作。
如果你正在寻找有关其他数据科学和机器学习主题的阅读推荐,别再犹豫了:
-
在他的 TDS 首次亮相中,Iqbal Rahmadhan带来了一个详细且易懂的介绍,帮助你理解、计算、可视化和解读比值比及其置信区间。
-
什么是语义层,如何构建一个有效的语义层?Marina Tosic的实践指南为刚刚入门的你提供了所有答案。
-
如果你对涉及多语言方法的机器学习工作流感兴趣,千万不要错过Milan Tamang的逐步指南,展示了如何构建泰语多语言子词分词器。
-
如果加速你的深度学习模型训练对你的当前项目有帮助,Mengliu Zhao将带领我们深入了解自动混合精度技术的内部工作原理。
-
在最新一期引人入胜的“水冷器闲聊”系列中,Maria Mouschoutzi, PhD通过统计学和一点 Python,揭示了著名的生日悖论。
-
对于刚接触 NumPy 及其众多强大功能的用户,应该探索Lee Vaughan的四部分新手友好系列,介绍了这个重要的 Python 库——第一部分自然是一个很好的起点。
感谢您支持我们作者的工作!正如我们之前提到的,我们非常喜欢发布新作者的文章,因此,如果您最近写了一篇有趣的项目讲解、教程或关于我们核心主题的理论思考,请不要犹豫,与我们分享。
直到下一个变量,
TDS 团队
细看 Scipy 的 Stats 模块 — 第一部分
让我们学习 Python 中 scipy.stats 模块的主要方法。
·发表于 Towards Data Science ·阅读时间 7 分钟 ·2024 年 9 月 19 日
--

图片由 Алекс Арцибашев 提供,来自 Unsplash
引言
在这个领域有足够经验的数据显示科学家和程序员,且选择 Python 作为编程语言的人,肯定已经听说过 Scipy 或科学 Python。
Scipy 是一个用于(如其所示)科学运算和计算的 Python 包。在这篇文章中,我们将更具体地讨论 scipy.stats 模块,这是用于 Python 中统计测试的模块。
Scipy Stats
Scipy.stats 是科学 Python 中功能强大的统计模块。它是一个资源丰富的模块,提供了许多用于创建随机分布、进行统计测试、重抽样、变换、蒙特卡罗模拟等方法。
在这篇文章的第一部分,我们将探索分布、统计数据和假设检验。
接下来,在第二篇文章中,我们将探讨处理多个样本、重抽样和变换的工具。
让我们深入了解一下 Scipy 统计模块的主要方法。
分布
细看 Scipy 的 Stats 模块 — 第二部分
让我们学习 Python 中 scipy.stats 模块的主要方法。
·发表于 Towards Data Science ·阅读时间 6 分钟·2024 年 9 月 19 日
--

图片由 Алекс Арцибашев 提供,来源于 Unsplash
介绍
在上一篇文章中,细看 Scipy Stats—第一部分,我们学习了分布、统计学和单样本假设检验。
现在,我们将开始学习这个强大的模块,并检查该包中一些更复杂的功能。
在本文中,我们将学习比较两个样本的统计检验、引导法(Bootstraping)、蒙特卡洛模拟以及使用 Scipy 进行的一些变换。
让我们开始吧。
长时间从事该领域的、选择 Python 作为编程语言的数据科学家和程序员…
towardsdatascience.com
比较两个样本
比较两个样本是数据科学家常见的任务。在 Scipy 中,当我们想要检查两个不同的样本是否来自相同的分布,从而具有统计上相似的平均值时,可以使用独立样本检验。
# Two samples test: Comparison of means
# Sample 1
samp1 = scs.norm.rvs(loc=2, scale=5…
BERT 完整指南(附代码)
历史、架构、预训练和微调
·发表于 Towards Data Science ·阅读时间 43 分钟·2024 年 5 月 13 日
--
“从零开始构建大语言模型”系列的第四部分——完整指南,帮助你理解和构建大语言模型。如果你对这些模型是如何工作的感兴趣,我鼓励你阅读:
-
第四部分:BERT 完整指南(附代码)
简介
双向编码器表示(BERT)是由谷歌 AI 语言团队开发的大型语言模型(LLM),在自然语言处理(NLP)领域取得了显著的进展。近年来,许多模型受到了 BERT 的启发,或是对 BERT 的直接改进,例如 RoBERTa、ALBERT 和 DistilBERT 等。原始的 BERT 模型是在 OpenAI 的生成式预训练变换器(GPT)之后不久发布的,二者都建立在前一年提出的变换器架构基础上。虽然 GPT 侧重于自然语言生成(NLG),但 BERT 则优先考虑自然语言理解(NLU)。这两项技术的发展重塑了 NLP 领域,成为机器学习进步的重要里程碑。
以下文章将探讨 BERT 的历史,并详细描述其创建时的背景。这将全面展示论文作者所做的架构决策,并帮助理解如何训练和微调 BERT,以便在工业和业余应用中使用。我们将通过详细的架构分析和图示,逐步介绍如何编写代码从零开始微调 BERT 进行情感分析任务。
目录
1 — BERT 的历史与关键特性
2 — 架构与预训练目标
3 — 微调 BERT 进行情感分析
4 — 结论
5 — 深入阅读
1 — BERT 的历史与关键特性
BERT 模型可以通过四个主要特性来定义:
-
仅编码器架构
-
预训练方法
-
模型微调
-
使用双向上下文
这些特性是论文作者做出的设计选择,可以通过考虑模型创建时的背景来理解。以下部分将逐一介绍这些特性,并展示它们是如何受到 BERT 同时代模型(Transformer 和 GPT)的启发,或是作为对它们的改进而提出的。
1.1 — 仅编码器架构
2017 年 Transformer 的问世,开启了一场基于其创新设计生产新模型的竞赛。OpenAI 于 2018 年 6 月率先推出了 GPT:一个仅解码器模型,在自然语言生成(NLG)任务中表现卓越,最终推动了 ChatGPT 在后续版本中的发展。谷歌随后在四个月后发布了 BERT:一个专为自然语言理解(NLU)设计的仅编码器模型。这两种架构都可以生成非常强大的模型,但它们能够执行的任务略有不同。下面是对每种架构的概述。
仅解码器模型:
-
目标: 针对输入序列预测新的输出序列
-
概述: 在 Transformer 中,解码器模块负责根据提供给编码器的输入生成输出序列。仅解码器模型通过完全省略编码器模块,并将多个解码器堆叠在一个模型中来构建。这些模型接受提示作为输入,并通过预测下一个最可能的词(或更具体地说,token)逐个生成响应,这一任务被称为下一个词预测(Next Token Prediction,NTP)。因此,仅解码器模型在自然语言生成(NLG)任务中表现出色,例如:对话式聊天机器人、机器翻译和代码生成。这些模型可能是大众最为熟悉的,因为像 ChatGPT 这样的应用广泛使用了由仅解码器模型(GPT-3.5 和 GPT-4)驱动。
仅编码器模型:
-
目标: 对输入序列中的词进行预测
-
概述: Transformer 中的编码器块负责接收输入序列,并为每个单词(或更具体地说,为每个标记)创建丰富的数值向量表示。仅编码器模型省略了解码器,并堆叠多个 Transformer 编码器来生成一个单一模型。这些模型并不接受提示,而是接受输入序列进行预测(例如,预测序列中的缺失单词)。仅编码器模型缺少用于生成新词的解码器,因此不会像 GPT 那样用于聊天机器人应用。相反,仅编码器模型通常用于自然语言理解(NLU)任务,如命名实体识别(NER)和情感分析。编码器块创建的丰富向量表示赋予了 BERT 对输入文本的深刻理解。BERT 的作者认为,这一架构选择相比 GPT 会提高 BERT 的表现,特别是他们指出,仅解码器架构的模型是:
“在句子级任务中表现 sub-optimal,并且在应用基于微调的方法处理如问答之类的标记级任务时可能非常有害” [1]
注意: 从技术上讲,可以使用 BERT 生成文本,但正如我们将看到的,这并非该架构的初衷,且其结果在任何方面都无法与仅解码器模型相媲美。
Transformer、GPT 和 BERT 的架构图:
以下是我们迄今为止讨论的三种模型的架构图。该图是通过改编原始 Transformer 论文《Attention is All You Need》中的架构图 [2] 创建的。模型中编码器或解码器块的数量由 N 表示。在原始 Transformer 中,N 对编码器和解码器都等于 6,因为它们分别由六个编码器和解码器块堆叠而成。

对 Transformer、GPT 和 BERT 架构的比较。该图由作者根据《Attention is All You Need》论文中的 Transformer 架构图 [2] 改编而来。
1.2 — 预训练方法
GPT 在多个方面影响了 BERT 的发展。这个模型不仅是第一个仅解码器的 Transformer 衍生模型,而且 GPT 还普及了模型的预训练。预训练包括训练一个大型模型,以获得对语言的广泛理解(涵盖词汇使用和语法模式等方面),从而生成一个任务无关的基础模型。在上述图示中,基础模型由线性层下方的组件(显示为紫色)构成。训练完成后,这个基础模型的副本可以被微调以应对特定任务。微调只涉及训练线性层:一个小型的前馈神经网络,通常被称为分类头或简称头。模型其余部分的权重和偏差(即基础部分)保持不变,或称为冻结。
类比:
为了构建一个简短的类比,考虑情感分析任务。在这个任务中,目标是根据文本所表达的情感将其分类为positive(正面)或negative(负面)。例如,在一些电影评论中,像I loved this movie这样的文本会被分类为positive,而像I hated this movie这样的文本会被分类为negative。在传统的语言建模方法中,你可能需要从零开始训练一个新的架构,专门针对这个任务。你可以把这看作是通过展示电影评论来教某人英语,直到他们最终能够分类其中的情感。当然,这样的做法既慢又昂贵,并且需要大量的训练样本。而且,得到的分类器仍然只会精通这个任务。在预训练方法中,你会采用一个通用模型并对其进行微调,专门用于情感分析。你可以把这看作是将已经流利掌握英语的人带入情境,仅展示少量的电影评论,让他们熟悉当前的任务。显然,第二种方法要高效得多。
早期的预训练尝试:
预训练的概念并非由 OpenAI 发明,早在之前的几年,其他研究人员也已探索过这一方向。一个显著的例子是 ELMo 模型(来自语言模型的嵌入),该模型由 Allen Institute 的研究人员开发[3]。尽管有这些早期尝试,但没有其他研究人员能像 OpenAI 在其开创性论文中那样有力地证明预训练的有效性。用他们自己的话说,团队发现他们的
“任务无关模型的表现超越了那些使用专门为每个任务精心设计架构的判别式训练模型,显著提高了当前的技术水平”[4]。
这一发现坚定地确立了预训练范式作为未来语言建模的主导方法。顺应这一趋势,BERT 的作者们也完全采用了预训练方法。
1.3 — 模型微调
微调的好处:
微调已经成为今天的常态,以至于我们容易忽略这种方法崭露头角的时间其实是很短的。2018 年之前,通常每个不同的自然语言处理(NLP)任务都会引入一种新的模型架构。转向预训练不仅大幅减少了开发模型所需的训练时间和计算成本,而且还减少了所需的训练数据量。与其完全重新设计和从头开始重新训练一个语言模型,像 GPT 这样的通用模型只需少量任务特定的数据,就能在短时间内进行微调。根据任务的不同,分类头可以改变,以包含不同数量的输出神经元。这对于情感分析等分类任务非常有用。例如,如果 BERT 模型的目标输出是预测一个评论是positive还是negative,则可以将头部更改为包含两个输出神经元。每个神经元的激活表示该评论为positive或negative的概率。对于一个有 10 个类别的多类分类任务,头部可以改为有 10 个神经元的输出层,依此类推。这使得 BERT 更加多功能,使基础模型能够用于各种下游任务。
BERT 中的微调:
BERT 沿袭了 GPT 的做法,也采用了预训练/微调的方法。谷歌发布了两个版本的 BERT:Base 版和 Large 版,用户可以根据硬件限制选择不同的模型大小。两个版本都需要在多个 TPU(张量处理单元)上预训练约 4 天,BERT Base 在 16 个 TPU 上进行训练,BERT Large 则在 64 个 TPU 上进行训练。对于大多数研究人员、爱好者和行业从业者来说,这种训练规模是不可行的。因此,花费仅几个小时在特定任务上对基础模型进行微调,依然是一个更具吸引力的选择。原始的 BERT 架构已经在多个任务和数据集上经历了数千次微调迭代,其中许多数据集可以通过 Hugging Face 等平台公开下载[5]。
1.4 — 使用双向上下文
作为一种语言模型,BERT 预测在观察到前一个单词的基础上,某个词出现的概率。这一基本特性是所有语言模型所共有的,无论其架构或任务如何。然而,正是这些概率的使用赋予了模型任务特定的行为。例如,GPT 被训练用来预测序列中下一个最可能的词。也就是说,给定已经观察到的前一个词,模型预测下一个词。其他模型可能被训练用于情感分析,预测输入序列的情感,使用诸如positive或negative之类的文本标签,等等。要对文本做出任何有意义的预测,都需要理解其上下文,尤其是在自然语言理解(NLU)任务中。BERT 通过其关键特性之一——双向性——确保了对上下文的良好理解。
双向性可能是 BERT 最重要的特性,也是其在自然语言理解任务中表现出色的关键所在,同时也是推动其编码器架构的原因。虽然 Transformer 编码器的自注意力机制计算双向上下文,但解码器则生成单向上下文,不能做到这一点。BERT 的作者认为,GPT 缺乏双向性使其无法像 BERT 那样达到相同深度的语言表示。
定义双向性:
那么,究竟“双向”上下文是什么意思呢?在这里,双向性意味着输入序列中的每个词都可以从前后两个方向的词(分别称为左侧上下文和右侧上下文)获取上下文。在技术术语中,我们可以说,注意力机制可以关注每个词的前后词汇。为了进一步解释,回顾一下,BERT 仅对输入序列中的词做出预测,而不像 GPT 那样生成新的序列。因此,当 BERT 预测输入序列中的某个词时,它可以结合周围词语的上下文线索。这为 BERT 提供了双向的上下文,帮助它做出更加准确的预测。
与仅使用解码器的模型(如 GPT)进行对比,在这些模型中,目标是逐个预测新词,以生成输出序列。每个预测的词只能利用前面词语提供的上下文(左侧上下文),因为后续的词(右侧上下文)尚未生成。因此,这些模型被称为单向模型。

单向上下文和双向上下文的比较。图片来源:作者。
图片解析:
上图展示了一个典型的 BERT 任务示例,使用双向上下文,而一个典型的 GPT 任务示例则使用单向上下文。对于 BERT,这里的任务是预测被 [MASK] 标记的词。由于该词的两侧都有词语,左右两边的词语都可以用于提供上下文。如果你作为人类,只凭左侧或右侧的上下文来阅读这句话,可能会很难预测被遮掩的词。然而,使用双向上下文时,更有可能猜测出被遮掩的词是 fishing。
对于 GPT,目标是执行经典的自然语言推理(NTP)任务。在这种情况下,目标是基于输入序列提供的上下文以及输出中已生成的词语,生成一个新的序列。假设输入序列指示模型写一首诗,且当前已生成的词语是 Upon a,你可能会预测下一个词是 river,接着是 bank。由于有许多潜在的候选词,GPT(作为一种语言模型)会计算其词汇表中每个词语出现的概率,并根据其训练数据选择最有可能的词语之一。
1.5 — BERT 的局限性
作为一个双向模型,BERT 存在两个主要缺点:
增加的训练时间:
基于 Transformer 的模型提出双向性作为对当时普遍存在的从左到右上下文模型的直接改进。其思想是 GPT 只能以单向的方式获取输入序列的上下文信息,因此缺乏对词语间因果关系的完整理解。然而,双向模型提供了对词语间因果联系的更广泛理解,因此可能在 NLU 任务中表现更好。尽管双向模型在过去曾被探索过,但它们的成功是有限的,正如 1990 年代末期的双向 RNN 所示 [6]。通常,这些模型需要更多的计算资源进行训练,因此,在相同的计算能力下,你可以训练一个更大的单向模型。
语言生成中的差劲表现:
BERT 专门设计用于解决自然语言理解(NLU)任务,选择了使用编码器和处理输入序列以深入理解的能力,而放弃了解码器和生成新序列的能力。因此,BERT 最适合处理一部分 NLP 任务,如命名实体识别(NER)、情感分析等。值得注意的是,BERT 不接受提示,而是处理序列来制定预测。虽然 BERT 在技术上可以生成新的输出序列,但我们需要认识到,在后 ChatGPT 时代,我们对大语言模型(LLMs)的理解与 BERT 设计之间的差异。
2 — 架构与预训练目标
2.1 — BERT 预训练目标概述
训练一个双向模型需要允许使用左侧和右侧上下文进行预测的任务。因此,作者精心构造了两个预训练目标,以建立 BERT 对语言的理解。这两个任务是:掩码语言模型(MLM)任务和下一个句子预测(NSP)任务。每个任务的训练数据都是从当时可用的所有英文维基百科文章(25 亿个单词)以及额外的 11,038 本来自 BookCorpus 数据集的书籍(8 亿个单词)中抓取的[7]。然而,原始数据首先根据具体任务进行预处理,如下所述。
2.2 — 掩码语言建模(MLM)
MLM 概述:
掩码语言建模(Masked Language Modelling, MLM)任务的创建是为了直接解决训练双向模型的需求。为了实现这一点,模型必须被训练成能够利用输入序列的左侧上下文和右侧上下文来进行预测。具体做法是随机掩盖训练数据中 15%的单词,并训练 BERT 预测缺失的单词。在输入序列中,被掩盖的单词将被替换为[MASK]标记。例如,假设原始训练数据中有句子A man was fishing on the river,当将原始文本转换为 MLM 任务的训练数据时,单词fishing可能会被随机掩盖并替换为[MASK]标记,得到训练输入A man was [MASK] on the river,目标是fishing。因此,BERT 的目标是预测缺失的单词fishing,而不是用缺失的单词填补输入序列。掩盖过程可以对所有可能的输入序列(例如句子)重复执行,以建立 MLM 任务的训练数据。这个任务在语言学文献中早已存在,被称为完形填空(Cloze)任务[8]。然而,在机器学习领域,由于 BERT 的流行,它通常被称为 MLM。
缓解预训练与微调之间的不匹配:
然而,作者指出,由于[MASK]标记只会出现在训练数据中,而不会出现在实际数据(推理时),因此会导致预训练与微调之间的不匹配。为了缓解这一问题,并非所有被掩盖的单词都会被替换为[MASK]标记。相反,作者表示:
训练数据生成器随机选择 15%的标记位置进行预测。如果选择了第 i 个标记,我们将第 i 个标记替换为(1)80%的时间替换为
[MASK]标记(2)10%的时间替换为一个随机标记(3)10%的时间保留不变。
计算预测单词与目标单词之间的误差:
BERT 将接受一个最大包含 512 个标记的输入序列,无论是 BERT Base 还是 BERT Large。如果序列中的标记少于最大数量,则会通过使用 [PAD] 标记进行填充,直到达到 512 的最大数量。输出标记的数量也将严格等于输入标记的数量。如果输入序列的第 i 个位置存在掩码标记,BERT 的预测将位于输出序列的第 i 个位置。其他所有标记将在训练过程中被忽略,因此模型权重和偏差的更新将基于预测标记和目标标记之间的误差进行计算。误差通过损失函数进行计算,通常使用交叉熵损失(负对数似然)函数,正如我们稍后将看到的那样。
2.3 — 下一句预测(NSP)
概述:
BERT 的第二个预训练任务是下一句预测(Next Sentence Prediction, NSP),其目标是判断一个片段(通常是句子)是否在逻辑上紧跟另一个片段。选择 NSP 作为预训练任务,特意是为了补充 MLM(掩码语言模型)并增强 BERT 的自然语言理解(NLU)能力,作者表示:
许多重要的下游任务,如问答(QA)和自然语言推理(NLI),都基于理解两个句子之间的关系,而这一点并没有被语言模型直接捕捉到。
通过为 NSP 进行预训练,BERT 能够发展出对散文文本中句子之间流畅连接的理解——这一能力对于广泛的自然语言理解问题非常有用,例如:
-
释义中的句子对
-
逻辑蕴含中的假设-前提对
-
问题-段落对(Question Passage Pairs)在问答任务中的应用
BERT 中实现 NSP:
NSP 的输入由第一个和第二个片段(分别表示为 A 和 B)组成,两个片段之间用 [SEP] 标记分隔,末尾还会有第二个 [SEP] 标记。实际上,BERT 期望每个输入序列中至少有一个 [SEP] 标记,用以标示序列的结束,无论是否执行 NSP。因此,WordPiece 分词器将在 MLM 任务以及任何不包含 [SEP] 标记的非 NSP 任务中,将其中一个标记附加到输入的末尾。NSP 形成一个分类问题,当片段 A 逻辑上紧跟片段 B 时,输出为 IsNext;如果不是,则输出为 NotNext。训练数据可以通过从任何单语语料库中生成:将句子与其后一句配对 50%,其余 50% 的句子则与随机句子配对。
2.4 — BERT 的输入嵌入
BERT 的输入嵌入过程包括三个阶段:位置编码、片段嵌入和标记嵌入(如下图所示)。
位置编码:
就像 Transformer 模型一样,位置相关的信息被注入到每个标记的嵌入中。然而,与 Transformer 不同,BERT 中的位置编码是固定的,并不是通过函数生成的。这意味着 BERT 在输入序列中限制为 512 个标记,无论是 BERT Base 还是 BERT Large。
片段嵌入:
每个标记所属片段的向量也会被加入。对于 MLM 预训练任务或任何其他非 NSP 任务(这些任务只包含一个[SEP]标记),输入中的所有标记都被视为属于片段 A。对于 NSP 任务,第二个[SEP]之后的所有标记都被表示为片段 B。
标记嵌入:
与原始的 Transformer 一样,每个标记学习到的嵌入随后会与其位置和片段向量相加,创建最终的嵌入,这将传递给 BERT 中的自注意力机制,以加入上下文信息。

BERT 嵌入过程的概述。图像来自 BERT 论文[1]。
2.5 — 特殊标记
在上面的图像中,你可能注意到输入序列前面被加上了一个[CLS](分类)标记。这个标记被添加用来封装整个输入序列的语义总结,帮助 BERT 执行分类任务。例如,在情感分析任务中,最终层中的[CLS]标记可以被分析以提取对输入序列情感是正面还是负面的预测。[CLS]和[PAD]等是 BERT 的特殊标记。这里需要注意的是,这是 BERT 特有的功能,因此你不应该期待在像 GPT 这样的模型中看到这些特殊标记。总的来说,BERT 有五个特殊标记。下面提供了一个总结:
-
[PAD](标记 ID:0)——一个填充标记,用于将输入序列中的总标记数填充至 512。 -
[UNK](标记 ID:100)——一个未知标记,用于表示 BERT 词汇表中没有的标记。 -
[CLS](标记 ID:101)——一个分类标记,预期每个序列的开头都会有一个,无论是否使用。这个标记封装了分类任务的类信息,可以被认为是一个聚合的序列表示。 -
[SEP](标记 ID:102)——一个分隔符标记,用于区分单个输入序列中的两个片段(例如,在下一个句子预测任务中)。每个输入序列至少应该有一个[SEP]标记,最多可以有两个。 -
[MASK](标记 ID:103)——一个掩码标记,用于在掩码语言建模任务中训练 BERT,或在掩码序列上执行推理。
2.4 — BERT Base 和 BERT Large 的架构对比
从架构角度来看,BERT Base 和 BERT Large 非常相似,正如你所期望的那样。它们都使用 WordPiece 分词器(因此也期望与前面描述的相同的特殊符号),并且都具有最大 512 个词元的序列长度。BERT 的词汇表大小为 30,522,其中大约 1,000 个词元被标记为“未使用”。这些未使用的词元故意留空,以允许用户添加自定义词元,而无需重新训练整个分词器。这在处理特定领域的词汇时非常有用,比如医学和法律术语。BERT Base 和 BERT Large 都具有比原始 Transformer 更高的嵌入维度(d_model)。这对应于模型词汇中每个词元的学习向量表示的大小。对于 BERT Base,d_model = 768;对于 BERT Large,d_model = 1024(是原始 Transformer 512 的两倍)。
这两个模型主要在四个类别上有所不同:
-
编码器块数量,
**N**: 堆叠在一起的编码器块的数量。 -
每个编码器块的注意力头数: 注意力头计算输入序列的上下文向量嵌入。由于 BERT 使用多头注意力,这个值指的是每个编码器层的头数。
-
前馈网络中隐藏层的大小: 线性层由一个隐藏层组成,该隐藏层具有固定数量的神经元(例如 BERT Base 为 3072),然后传递到输出层,输出层的大小可以有所不同。输出层的大小取决于任务。例如,二分类问题只需要两个输出神经元,而一个有十个类别的多分类问题则需要十个神经元,以此类推。
-
总参数量: 模型中所有权重和偏置的总数。在当时,具有数亿参数的模型被认为非常大。然而,以今天的标准来看,这些值相对较小。
下图展示了 BERT Base 和 BERT Large 在这些类别中的对比。

BERT Base 和 BERT Large 的对比。图片来自作者。
3 — 微调 BERT 进行情感分析
本节涵盖了在 Python 中微调 BERT 的实际示例。代码采用任务无关的微调管道形式,使用 Python 类实现。接着,我们将实例化该类的对象,并用它来微调 BERT 模型以进行情感分析任务。该类还可以重复使用,用于微调 BERT 完成其他任务,如问答、命名实体识别等。第 3.1 节到 3.5 节逐步讲解了微调过程,第 3.6 节展示了完整的管道。
3.1 — 加载并预处理微调数据集
微调的第一步是选择一个适合特定任务的数据集。在这个示例中,我们将使用斯坦福大学提供的情感分析数据集。该数据集包含来自互联网电影数据库(IMDb)的 50,000 条在线电影评论,每条评论标注为正面或负面。您可以直接从斯坦福大学网站下载该数据集,或者您可以在Kaggle上创建一个笔记本,并将您的工作与他人进行比较。
import pandas as pd
df = pd.read_csv('IMDB Dataset.csv')
df.head()

显示在 Pandas DataFrame 中的 IMDb 数据集的前五行。图片来自作者。
与早期的 NLP 模型不同,基于 Transformer 的模型(如 BERT)需要的预处理工作最小化。诸如去除停用词和标点符号等步骤,在某些情况下可能适得其反,因为这些元素为 BERT 提供了理解输入句子的宝贵上下文。尽管如此,仍然需要检查文本以查找任何格式问题或不必要的字符。总体而言,IMDb 数据集相当干净。然而,似乎有一些抓取过程的遗留物,例如 HTML 换行标签(<br />)和不必要的空白,应该去除。
# Remove the break tags (<br />)
df['review_cleaned'] = df['review'].apply(lambda x: x.replace('<br />', ''))
# Remove unnecessary whitespace
df['review_cleaned'] = df['review_cleaned'].replace('\s+', ' ', regex=True)
# Compare 72 characters of the second review before and after cleaning
print('Before cleaning:')
print(df.iloc[1]['review'][0:72])
print('\nAfter cleaning:')
print(df.iloc[1]['review_cleaned'][0:72])
Before cleaning:
A wonderful little production. <br /><br />The filming technique is very
After cleaning:
A wonderful little production. The filming technique is very unassuming-
编码情感:
预处理的最后一步是将每个评论的情感编码为0表示负面或1表示正面。这些标签将用于稍后在微调过程中训练分类头。
df['sentiment_encoded'] = df['sentiment'].\
apply(lambda x: 0 if x == 'negative' else 1)
df.head()

编码情感列后的 IMDb 数据集前五行。图片来自作者。
3.2 — 分词微调数据
一旦预处理完成,微调数据就可以进行分词处理。这个过程:将评论文本拆分成单独的标记,添加[CLS]和[SEP]特殊标记,并处理填充。选择合适的分词器非常重要,因为不同的语言模型需要不同的分词步骤(例如,GPT 不需要[CLS]和[SEP]标记)。我们将使用来自 Hugging Face transformers库的BertTokenizer类,它是专为 BERT 模型设计的。有关分词如何工作的更深入讨论,请参见本系列的第一部分。
transformers库中的分词器类提供了一种简单的方法,可以使用from_pretrained方法创建预训练的分词器模型。使用此功能的方法是:导入并实例化一个分词器类,调用from_pretrained方法,并传入一个包含在 Hugging Face 模型库中托管的分词器模型名称的字符串。或者,你可以传入一个包含分词器所需词汇文件的目录的路径[9]。在我们的示例中,我们将使用来自模型库的预训练分词器。在使用 BERT 时,有四个主要选项,每个选项都使用来自 Google 预训练分词器的词汇表。这些选项是:
-
bert-base-uncased— BERT 的较小版本的词汇表,它对大小写不敏感(例如,Cat和cat会被视为相同的标记) -
bert-base-cased— BERT 的较小版本的词汇表,它对大小写敏感(例如,Cat和cat不会被视为相同的标记) -
bert-large-uncased— BERT 的较大版本的词汇表,它对大小写不敏感(例如,Cat和cat会被视为相同的标记) -
bert-large-cased— BERT 的较大版本的词汇表,它对大小写敏感(例如,Cat和cat不会被视为相同的标记)
BERT Base 和 BERT Large 使用相同的词汇表,因此bert-base-uncased和bert-large-uncased之间实际上没有区别,bert-base-cased和bert-large-cased之间也没有区别。对于其他模型来说,情况可能不同,因此如果不确定,最好使用相同的分词器和模型大小。
何时使用 **cased** 与 **uncased**:
在使用cased和uncased之间的选择取决于数据集的性质。IMDb 数据集包含由互联网用户编写的文本,这些用户在使用大写字母时可能不一致。例如,一些用户可能会忽略应有的大写字母,或者为了强调情感(如表达兴奋、沮丧等)使用大写字母。因此,我们将选择忽略大小写,并使用bert-base-uncased分词器模型。
在其他情况下,通过考虑大小写可能会获得性能上的优势。一个例子可能是在命名实体识别任务中,目标是识别输入文本中的实体,如人名、组织名、地点等。在这种情况下,大写字母的存在可以非常有助于识别一个单词是否为某个人的名字或一个地方的名称,因此在这种情况下,选择bert-base-cased可能更为合适。
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
print(tokenizer)
BertTokenizer(
name_or_path='bert-base-uncased',
vocab_size=30522,
model_max_length=512,
is_fast=False,
padding_side='right',
truncation_side='right',
special_tokens={
'unk_token': '[UNK]',
'sep_token': '[SEP]',
'pad_token': '[PAD]',
'cls_token': '[CLS]',
'mask_token': '[MASK]'},
clean_up_tokenization_spaces=True),
added_tokens_decoder={
0: AddedToken(
"[PAD]",
rstrip=False,
lstrip=False,
single_word=False,
normalized=False,
special=True),
100: AddedToken(
"[UNK]",
rstrip=False,
lstrip=False,
single_word=False,
normalized=False,
special=True),
101: AddedToken(
"[CLS]",
rstrip=False,
lstrip=False,
single_word=False,
normalized=False,
special=True),
102: AddedToken(
"[SEP]",
rstrip=False,
lstrip=False,
single_word=False,
normalized=False,
special=True),
103: AddedToken(
"[MASK]",
rstrip=False,
lstrip=False,
single_word=False,
normalized=False,
special=True),
}
编码过程:将文本转换为标记再转换为标记 ID
接下来,可以使用分词器对清理后的微调数据进行编码。这个过程会将每条评论转换为一个标记 ID 的张量。例如,评论I liked this movie将通过以下步骤进行编码:
- 将评论转换为小写(因为我们使用的是
bert-base-uncased)
2. 根据bert-base-uncased词汇表将评论拆分为单独的 token:['i', 'liked', 'this', 'movie']
2. 添加 BERT 期望的特殊 token:['[CLS]', 'i', 'liked', 'this', 'movie', '[SEP]']
3. 将 token 转换为它们的 token ID,同样根据bert-base-uncased词汇表(例如[CLS] -> 101,i -> 1045,等等)
BertTokenizer类的encode方法使用上述过程对文本进行编码,并可以返回 PyTorch 张量、TensorFlow 张量或 NumPy 数组格式的 token ID 张量。返回张量的数据类型可以通过return_tensors参数来指定,取值为:pt、tf和np,分别对应 PyTorch、TensorFlow 和 NumPy。
注意: 在 Hugging Face 中,token ID 通常被称为
input IDs,因此你可能会看到这两个术语交替使用。
# Encode a sample input sentence
sample_sentence = 'I liked this movie'
token_ids = tokenizer.encode(sample_sentence, return_tensors='np')[0]
print(f'Token IDs: {token_ids}')
# Convert the token IDs back to tokens to reveal the special tokens added
tokens = tokenizer.convert_ids_to_tokens(token_ids)
print(f'Tokens : {tokens}')
Token IDs: [ 101 1045 4669 2023 3185 102]
Tokens : ['[CLS]', 'i', 'liked', 'this', 'movie', '[SEP]']
截断与填充:
BERT Base 和 BERT Large 都设计为处理恰好 512 个 token 的输入序列。但如果输入序列不符合这个限制,怎么办呢?答案是截断和填充!截断通过简单地删除超过特定长度的 token 来减少 token 的数量。在encode方法中,你可以将truncation设置为True,并指定一个max_length参数来强制对所有编码序列施加长度限制。数据集中有几条评论超过了 512 个 token 的限制,因此这里的max_length参数被设置为 512,以从所有评论中提取尽可能多的文本。如果没有评论超过 512 个 token,可以不设置max_length,它将默认使用模型的最大长度。或者,你也可以强制设置一个小于 512 的最大长度,从而在微调过程中减少训练时间,尽管这样会影响模型性能。对于长度小于 512 个 token 的评论(大多数评论是这样),会添加填充 token,以将编码后的评论扩展到 512 个 token。可以通过将填充parameter设置为max_length来实现。更多关于encode方法的细节,请参阅 Hugging Face 文档[10]。
review = df['review_cleaned'].iloc[0]
token_ids = tokenizer.encode(
review,
max_length = 512,
padding = 'max_length',
truncation = True,
return_tensors = 'pt')
print(token_ids)
tensor([[ 101, 2028, 1997, 1996, 2060, 15814, 2038, 3855, 2008, 2044,
3666, 2074, 1015, 11472, 2792, 2017, 1005, 2222, 2022, 13322,
...
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0]])
使用注意力掩码与 **encode_plus**:
上面的示例展示了数据集中第一个评论的编码,其中包含 119 个填充 token。如果直接用于微调,BERT 可能会关注到这些填充 token,从而可能导致性能下降。为了解决这个问题,我们可以应用一个注意力掩码,指示 BERT 忽略输入中的某些 token(在这个例子中是填充 token)。我们可以通过修改上面的代码,使用encode_plus方法,而不是标准的encode方法来生成这个注意力掩码。encode_plus方法返回一个字典(在 Hugging Face 中称为 Batch Encoder),其中包含以下键:
-
input_ids— 与标准encode方法返回的 token ID 相同 -
token_type_ids— 用于区分句子 A(id = 0)和句子 B(id = 1)的段落 ID,常用于下一个句子预测等句子对任务 -
attention_mask— 一个由 0 和 1 组成的列表,其中 0 表示在注意力过程中应该忽略该标记,而 1 表示该标记不应被忽略
review = df['review_cleaned'].iloc[0]
batch_encoder = tokenizer.encode_plus(
review,
max_length = 512,
padding = 'max_length',
truncation = True,
return_tensors = 'pt')
print('Batch encoder keys:')
print(batch_encoder.keys())
print('\nAttention mask:')
print(batch_encoder['attention_mask'])
Batch encoder keys:
dict_keys(['input_ids', 'token_type_ids', 'attention_mask'])
Attention mask:
tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
...
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0]])
编码所有评论:
标记化阶段的最后一步是编码数据集中的所有评论,并将标记 ID 和对应的注意力掩码存储为张量。
import torch
token_ids = []
attention_masks = []
# Encode each review
for review in df['review_cleaned']:
batch_encoder = tokenizer.encode_plus(
review,
max_length = 512,
padding = 'max_length',
truncation = True,
return_tensors = 'pt')
token_ids.append(batch_encoder['input_ids'])
attention_masks.append(batch_encoder['attention_mask'])
# Convert token IDs and attention mask lists to PyTorch tensors
token_ids = torch.cat(token_ids, dim=0)
attention_masks = torch.cat(attention_masks, dim=0)
3.3 — 创建训练和验证 DataLoader
现在每条评论都已编码完成,我们可以将数据分割为训练集和验证集。验证集将用于在微调过程中评估其效果,使我们能够在整个过程中监控模型的性能。我们期望随着模型在epochs中进一步微调,损失下降(因此模型准确性提高)。一个 epoch 指的是对训练数据的完整遍历。BERT 的作者推荐进行 2 到 4 个 epochs 的微调[1],意味着分类头将看到每条评论 2 到 4 次。
为了对数据进行划分,我们可以使用 SciKit-Learn 的model_selection包中的train_test_split函数。此函数需要我们打算划分的数据集、分配给测试集(或在我们的案例中是验证集)的项目百分比,以及一个可选参数,指示数据是否应随机打乱。为了确保可重复性,我们将shuffle参数设置为False。对于test_size,我们选择一个较小的值 0.1(相当于 10%)。在使用足够的数据来验证模型并准确评估其性能与保留足够数据来训练模型并提高其性能之间,需要找到平衡。因此,通常更倾向于选择较小的值,如0.1。在分割完标记 ID、注意力掩码和标签后,我们可以将训练和验证的张量组合在一起,使用 PyTorch 的 TensorDatasets。然后,我们可以通过将这些 TensorDatasets 分批来创建用于训练和验证的 PyTorch DataLoader 类。BERT 论文推荐使用 16 或 32 的批量大小(即在重新计算分类头中的权重和偏置之前,模型需要先看到 16 条评论及其对应的情感标签)。使用 DataLoader 将使我们能够在微调过程中有效地加载数据,通过利用多个 CPU 核心进行并行化[11]。
from sklearn.model_selection import train_test_split
from torch.utils.data import TensorDataset, DataLoader
val_size = 0.1
# Split the token IDs
train_ids, val_ids = train_test_split(
token_ids,
test_size=val_size,
shuffle=False)
# Split the attention masks
train_masks, val_masks = train_test_split(
attention_masks,
test_size=val_size,
shuffle=False)
# Split the labels
labels = torch.tensor(df['sentiment_encoded'].values)
train_labels, val_labels = train_test_split(
labels,
test_size=val_size,
shuffle=False)
# Create the DataLoaders
train_data = TensorDataset(train_ids, train_masks, train_labels)
train_dataloader = DataLoader(train_data, shuffle=True, batch_size=16)
val_data = TensorDataset(val_ids, val_masks, val_labels)
val_dataloader = DataLoader(val_data, batch_size=16)
3.4 — 实例化一个 BERT 模型
下一步是加载一个预训练的 BERT 模型以供我们进行微调。我们可以像导入标记器一样,从 Hugging Face 模型库中导入一个模型。Hugging Face 有许多带有预配置分类头的 BERT 版本,这使得这个过程非常便捷。带有预配置分类头的模型示例包括:
-
BertForMaskedLM -
BertForNextSentencePrediction -
BertForSequenceClassification -
BertForMultipleChoice -
BertForTokenClassification -
BertForQuestionAnswering
当然,你也可以在 PyTorch 或 TensorFlow 中导入一个无头 BERT 模型,并从头开始创建自己的分类头。然而,在我们的案例中,我们可以简单地导入BertForSequenceClassification模型,因为它已经包含了我们需要的线性层。这个线性层是使用随机权重和偏置初始化的,这些将在微调过程中进行训练。由于 BERT Base 使用 768 个嵌入维度,隐藏层包含 768 个神经元,这些神经元与模型的最终编码器块相连。输出神经元的数量由num_labels参数决定,并对应于唯一情感标签的数量。IMDb 数据集只有positive和negative两类情感标签,因此num_labels参数被设置为2。对于更复杂的情感分析,可能包括如neutral或mixed等标签,我们可以简单地增大或减小num_labels的值。
注意: 如果你有兴趣查看预配置的模型如何在源代码中编写,Hugging Face 的 transformers 库中的
modelling_bert.py文件展示了如何加载一个无头 BERT 模型并添加线性层[12]。线性层是在每个类的__init__方法中添加的。
from transformers import BertForSequenceClassification
model = BertForSequenceClassification.from_pretrained(
'bert-base-uncased',
num_labels=2)
3.5 — 实例化优化器、损失函数和调度器
优化器:
在分类头处理完一批训练数据后,它会更新线性层中的权重和偏置,以提高模型在这些输入上的表现。在多个批次和多个训练轮次中,目标是让这些权重和偏置收敛到最优值。优化器用于计算每个权重和偏置所需的变化,可以从 PyTorch 的optim包中导入。Hugging Face 在其示例中使用的是 AdamW 优化器,因此我们在这里也将使用该优化器[13]。
损失函数:
优化器通过确定分类头中权重和偏置的变化如何影响与一个称为损失函数的评分函数的损失来工作。损失函数可以很容易地从 PyTorch 的nn包中导入,如下所示。语言模型通常使用交叉熵损失函数(也称为负对数似然函数),因此这也是我们在此使用的损失函数。
调度器:
一个叫做学习率的参数用于确定分类头中权重和偏置变化的大小。在早期的批次和训练轮次中,较大的变化可能会证明是有利的,因为随机初始化的参数可能需要大量的调整。然而,随着训练的进行,权重和偏置趋于改善,过大的变化可能会适得其反。调度器的设计是随着训练的进行,逐渐减少学习率,从而减少每次优化步骤中对每个权重和偏置的变化大小。
from torch.optim import AdamW
import torch.nn as nn
from transformers import get_linear_schedule_with_warmup
EPOCHS = 2
# Optimizer
optimizer = AdamW(model.parameters())
# Loss function
loss_function = nn.CrossEntropyLoss()
# Scheduler
num_training_steps = EPOCHS * len(train_dataloader)
scheduler = get_linear_schedule_with_warmup(
optimizer,
num_warmup_steps=0,
num_training_steps=num_training_steps)
3.6 — 微调循环
使用带 CUDA 的 GPU:
计算统一设备架构(CUDA)是由 NVIDIA 创建的计算平台,旨在提升各个领域应用的性能,如科学计算和工程应用[14]。PyTorch 的cuda包允许开发者在 Python 中利用 CUDA 平台,并使用图形处理单元(GPU)进行加速计算,从而在训练机器学习模型时提高计算效率。可以使用torch.cuda.is_available命令来检查 GPU 是否可用。如果不可用,代码会默认使用中央处理单元(CPU),但需要注意,这将导致训练时间变长。在随后的代码片段中,我们将使用 PyTorch 的Tensor.to方法将张量(包含模型权重和偏置等)转移到 GPU 上,以加快计算速度。如果设备设置为cpu,则张量将不会被移动,代码也不会受到影响。
# Check if GPU is available for faster training time
if torch.cuda.is_available():
device = torch.device('cuda:0')
else:
device = torch.device('cpu')
训练过程将通过两个 for 循环进行:一个外循环,用于为每个 epoch 重复训练过程(这样模型可以多次看到所有训练数据);一个内循环,用于对每个批次重复进行损失计算和优化步骤。为了解释训练循环,可以参考下面的步骤。训练循环的代码改编自 Chris McCormick 和 Nick Ryan 的这篇精彩博客文章[15],我强烈推荐大家阅读。
对于每个 epoch:
1. 使用模型对象的train方法将模型切换到训练模式。这将导致模型在训练模式下的行为与评估模式时有所不同,特别是在处理批归一化(batchnorm)和丢弃层(dropout layers)时非常有用。如果你之前查看过BertForSequenceClassification类的源代码,可能已经注意到分类头确实包含一个丢弃层,因此,在微调过程中正确区分训练模式和评估模式是非常重要的。这些层只应在训练时启用,而不应在推理时启用,因此能够在训练和推理模式之间切换是一个非常有用的功能。
2. 在 epoch 开始时将训练损失设为 0。这个损失值用于跟踪模型在后续 epoch 中对训练数据的表现。如果训练成功,损失应该会随着每个 epoch 的进行而减少。
对于每个批次:
根据 BERT 作者的建议,每个 epoch 的训练数据会被分成若干个批次。对于每个批次,重复训练过程。
3. 如果 GPU 可用,将 token ID、注意力掩码和标签移动到 GPU 上以加速处理,否则这些数据将保留在 CPU 上。
4. 调用zero_grad方法来重置上一轮迭代计算出的梯度。虽然在 PyTorch 中这不是默认行为,但一些原因解释了为什么像循环神经网络(RNN)这样的模型需要在迭代之间保留梯度,不能重置。
5. 将批次数据传递给模型以计算 logits(基于当前分类器权重和偏置的预测值)以及损失值。
6. 增加该轮次的总损失值。损失值由模型以 PyTorch 张量形式返回,因此使用item方法提取浮动值。
7. 执行模型的反向传播,并通过分类器头传播损失。这将使模型确定需要对权重和偏置进行哪些调整,以提高其在当前批次上的表现。
8. 将梯度裁剪到最大值为 1.0,以避免模型遭遇梯度爆炸问题。
9. 调用优化器,根据反向传播计算的误差面朝向进行一步优化。
每个批次训练后:
10. 计算训练过程中每轮的平均损失值和训练时间。
for epoch in range(0, EPOCHS):
model.train()
training_loss = 0
for batch in train_dataloader:
batch_token_ids = batch[0].to(device)
batch_attention_mask = batch[1].to(device)
batch_labels = batch[2].to(device)
model.zero_grad()
loss, logits = model(
batch_token_ids,
token_type_ids = None,
attention_mask=batch_attention_mask,
labels=batch_labels,
return_dict=False)
training_loss += loss.item()
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
scheduler.step()
average_train_loss = training_loss / len(train_dataloader)
验证步骤发生在外层循环中,因此会为每个 epoch 计算平均验证损失。随着 epoch 数的增加,我们预计验证损失会减少,分类器准确率会提高。验证过程的步骤如下所示。
本轮验证步骤:
11. 使用eval方法将模型切换到评估模式——这将禁用 dropout 层。
12. 将验证损失初始化为 0。这用于追踪模型在后续轮次中在验证数据上的损失。若训练成功,损失应随着每轮的进行而减小。
13. 将验证数据拆分成若干批次。
每个批次:
14. 如果可用,将 token IDs、注意力掩码和标签移动到 GPU 上以加速处理,否则这些数据将保留在 CPU 上。
15. 调用no_grad方法,指示模型不要计算梯度,因为在这里我们不会进行任何优化步骤,只进行推理。
16. 将批次数据传递给模型以计算 logits(基于当前分类器权重和偏置的预测值)以及损失值。
17. 从模型中提取 logits 和标签,并将它们移动到 CPU(如果它们尚未在 CPU 上)。
18. 增加损失值并根据验证数据加载器中的真实标签计算准确率。
19. 计算平均损失和准确率。
model.eval()
val_loss = 0
val_accuracy = 0
for batch in val_dataloader:
batch_token_ids = batch[0].to(device)
batch_attention_mask = batch[1].to(device)
batch_labels = batch[2].to(device)
with torch.no_grad():
(loss, logits) = model(
batch_token_ids,
attention_mask = batch_attention_mask,
labels = batch_labels,
token_type_ids = None,
return_dict=False)
logits = logits.detach().cpu().numpy()
label_ids = batch_labels.to('cpu').numpy()
val_loss += loss.item()
val_accuracy += calculate_accuracy(logits, label_ids)
average_val_accuracy = val_accuracy / len(val_dataloader)
上面代码片段倒数第二行使用了我们尚未定义的函数calculate_accuracy,所以现在让我们来定义它。模型在验证集上的准确度由正确预测的比例给出。因此,我们可以取模型生成的 logits,这些值存储在变量logits中,并使用 NumPy 中的argmax函数。argmax函数将简单地返回数组中最大元素的索引。如果文本I liked this movie的 logits 是[0.08, 0.92],其中0.08表示文本是negative的概率,0.92表示文本是positive的概率,那么argmax函数将返回索引1,因为模型认为该文本更可能是正面的而不是负面的。然后,我们可以将标签1与我们在第 3.3 节(第 19 行)之前编码的labels张量进行比较。由于logits变量将包含批次中每个评论的正负概率值(总共 16 个),因此模型的准确度将从最多 16 个正确预测中计算得出。上面单元格中的代码显示了val_accuracy变量跟踪每个准确度得分,我们在验证结束时对其进行除法运算,以确定模型在验证数据上的平均准确度。
def calculate_accuracy(preds, labels):
""" Calculate the accuracy of model predictions against true labels.
Parameters:
preds (np.array): The predicted label from the model
labels (np.array): The true label
Returns:
accuracy (float): The accuracy as a percentage of the correct
predictions.
"""
pred_flat = np.argmax(preds, axis=1).flatten()
labels_flat = labels.flatten()
accuracy = np.sum(pred_flat == labels_flat) / len(labels_flat)
return accuracy
3.7 — 完整的微调管道
至此,我们已经完成了微调的解释!下面的代码将上述所有内容汇总成一个单一、可重用的类,可以用于 BERT 的任何 NLP 任务。由于数据预处理步骤依赖于任务,因此它已被移出微调类之外。
情感分析的预处理函数(使用 IMDb 数据集):
def preprocess_dataset(path):
""" Remove unnecessary characters and encode the sentiment labels.
The type of preprocessing required changes based on the dataset. For the
IMDb dataset, the review texts contains HTML break tags (<br/>) leftover
from the scraping process, and some unnecessary whitespace, which are
removed. Finally, encode the sentiment labels as 0 for "negative" and 1 for
"positive". This method assumes the dataset file contains the headers
"review" and "sentiment".
Parameters:
path (str): A path to a dataset file containing the sentiment analysis
dataset. The structure of the file should be as follows: one column
called "review" containing the review text, and one column called
"sentiment" containing the ground truth label. The label options
should be "negative" and "positive".
Returns:
df_dataset (pd.DataFrame): A DataFrame containing the raw data
loaded from the self.dataset path. In addition to the expected
"review" and "sentiment" columns, are:
> review_cleaned - a copy of the "review" column with the HTML
break tags and unnecessary whitespace removed
> sentiment_encoded - a copy of the "sentiment" column with the
"negative" values mapped to 0 and "positive" values mapped
to 1
"""
df_dataset = pd.read_csv(path)
df_dataset['review_cleaned'] = df_dataset['review'].\
apply(lambda x: x.replace('<br />', ''))
df_dataset['review_cleaned'] = df_dataset['review_cleaned'].\
replace('\s+', ' ', regex=True)
df_dataset['sentiment_encoded'] = df_dataset['sentiment'].\
apply(lambda x: 0 if x == 'negative' else 1)
return df_dataset
任务无关的微调管道类:
from datetime import datetime
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.optim import AdamW
from torch.utils.data import TensorDataset, DataLoader
from transformers import (
BertForSequenceClassification,
BertTokenizer,
get_linear_schedule_with_warmup)
class FineTuningPipeline:
def __init__(
self,
dataset,
tokenizer,
model,
optimizer,
loss_function = nn.CrossEntropyLoss(),
val_size = 0.1,
epochs = 4,
seed = 42):
self.df_dataset = dataset
self.tokenizer = tokenizer
self.model = model
self.optimizer = optimizer
self.loss_function = loss_function
self.val_size = val_size
self.epochs = epochs
self.seed = seed
# Check if GPU is available for faster training time
if torch.cuda.is_available():
self.device = torch.device('cuda:0')
else:
self.device = torch.device('cpu')
# Perform fine-tuning
self.model.to(self.device)
self.set_seeds()
self.token_ids, self.attention_masks = self.tokenize_dataset()
self.train_dataloader, self.val_dataloader = self.create_dataloaders()
self.scheduler = self.create_scheduler()
self.fine_tune()
def tokenize(self, text):
""" Tokenize input text and return the token IDs and attention mask.
Tokenize an input string, setting a maximum length of 512 tokens.
Sequences with more than 512 tokens will be truncated to this limit,
and sequences with less than 512 tokens will be supplemented with [PAD]
tokens to bring them up to this limit. The datatype of the returned
tensors will be the PyTorch tensor format. These return values are
tensors of size 1 x max_length where max_length is the maximum number
of tokens per input sequence (512 for BERT).
Parameters:
text (str): The text to be tokenized.
Returns:
token_ids (torch.Tensor): A tensor of token IDs for each token in
the input sequence.
attention_mask (torch.Tensor): A tensor of 1s and 0s where a 1
indicates a token can be attended to during the attention
process, and a 0 indicates a token should be ignored. This is
used to prevent BERT from attending to [PAD] tokens during its
training/inference.
"""
batch_encoder = self.tokenizer.encode_plus(
text,
max_length = 512,
padding = 'max_length',
truncation = True,
return_tensors = 'pt')
token_ids = batch_encoder['input_ids']
attention_mask = batch_encoder['attention_mask']
return token_ids, attention_mask
def tokenize_dataset(self):
""" Apply the self.tokenize method to the fine-tuning dataset.
Tokenize and return the input sequence for each row in the fine-tuning
dataset given by self.dataset. The return values are tensors of size
len_dataset x max_length where len_dataset is the number of rows in the
fine-tuning dataset and max_length is the maximum number of tokens per
input sequence (512 for BERT).
Parameters:
None.
Returns:
token_ids (torch.Tensor): A tensor of tensors containing token IDs
for each token in the input sequence.
attention_masks (torch.Tensor): A tensor of tensors containing the
attention masks for each sequence in the fine-tuning dataset.
"""
token_ids = []
attention_masks = []
for review in self.df_dataset['review_cleaned']:
tokens, masks = self.tokenize(review)
token_ids.append(tokens)
attention_masks.append(masks)
token_ids = torch.cat(token_ids, dim=0)
attention_masks = torch.cat(attention_masks, dim=0)
return token_ids, attention_masks
def create_dataloaders(self):
""" Create dataloaders for the train and validation set.
Split the tokenized dataset into train and validation sets according to
the self.val_size value. For example, if self.val_size is set to 0.1,
90% of the data will be used to form the train set, and 10% for the
validation set. Convert the "sentiment_encoded" column (labels for each
row) to PyTorch tensors to be used in the dataloaders.
Parameters:
None.
Returns:
train_dataloader (torch.utils.data.dataloader.DataLoader): A
dataloader of the train data, including the token IDs,
attention masks, and sentiment labels.
val_dataloader (torch.utils.data.dataloader.DataLoader): A
dataloader of the validation data, including the token IDs,
attention masks, and sentiment labels.
"""
train_ids, val_ids = train_test_split(
self.token_ids,
test_size=self.val_size,
shuffle=False)
train_masks, val_masks = train_test_split(
self.attention_masks,
test_size=self.val_size,
shuffle=False)
labels = torch.tensor(self.df_dataset['sentiment_encoded'].values)
train_labels, val_labels = train_test_split(
labels,
test_size=self.val_size,
shuffle=False)
train_data = TensorDataset(train_ids, train_masks, train_labels)
train_dataloader = DataLoader(train_data, shuffle=True, batch_size=16)
val_data = TensorDataset(val_ids, val_masks, val_labels)
val_dataloader = DataLoader(val_data, batch_size=16)
return train_dataloader, val_dataloader
def create_scheduler(self):
""" Create a linear scheduler for the learning rate.
Create a scheduler with a learning rate that increases linearly from 0
to a maximum value (called the warmup period), then decreases linearly
to 0 again. num_warmup_steps is set to 0 here based on an example from
Hugging Face:
https://github.com/huggingface/transformers/blob/5bfcd0485ece086ebcbed2
d008813037968a9e58/examples/run_glue.py#L308
Read more about schedulers here:
https://huggingface.co/docs/transformers/main_classes/optimizer_
schedules#transformers.get_linear_schedule_with_warmup
"""
num_training_steps = self.epochs * len(self.train_dataloader)
scheduler = get_linear_schedule_with_warmup(
self.optimizer,
num_warmup_steps=0,
num_training_steps=num_training_steps)
return scheduler
def set_seeds(self):
""" Set the random seeds so that results are reproduceable.
Parameters:
None.
Returns:
None.
"""
np.random.seed(self.seed)
torch.manual_seed(self.seed)
torch.cuda.manual_seed_all(self.seed)
def fine_tune(self):
"""Train the classification head on the BERT model.
Fine-tune the model by training the classification head (linear layer)
sitting on top of the BERT model. The model trained on the data in the
self.train_dataloader, and validated at the end of each epoch on the
data in the self.val_dataloader. The series of steps are described
below:
Training:
> Create a dictionary to store the average training loss and average
validation loss for each epoch.
> Store the time at the start of training, this is used to calculate
the time taken for the entire training process.
> Begin a loop to train the model for each epoch in self.epochs.
For each epoch:
> Switch the model to train mode. This will cause the model to behave
differently than when in evaluation mode (e.g. the batchnorm and
dropout layers are activated in train mode, but disabled in
evaluation mode).
> Set the training loss to 0 for the start of the epoch. This is used
to track the loss of the model on the training data over subsequent
epochs. The loss should decrease with each epoch if training is
successful.
> Store the time at the start of the epoch, this is used to calculate
the time taken for the epoch to be completed.
> As per the BERT authors' recommendations, the training data for each
epoch is split into batches. Loop through the training process for
each batch.
For each batch:
> Move the token IDs, attention masks, and labels to the GPU if
available for faster processing, otherwise these will be kept on the
CPU.
> Invoke the zero_grad method to reset the calculated gradients from
the previous iteration of this loop.
> Pass the batch to the model to calculate the logits (predictions
based on the current classifier weights and biases) as well as the
loss.
> Increment the total loss for the epoch. The loss is returned from the
model as a PyTorch tensor so extract the float value using the item
method.
> Perform a backward pass of the model and propagate the loss through
the classifier head. This will allow the model to determine what
adjustments to make to the weights and biases to improve its
performance on the batch.
> Clip the gradients to be no larger than 1.0 so the model does not
suffer from the exploding gradients problem.
> Call the optimizer to take a step in the direction of the error
surface as determined by the backward pass.
After training on each batch:
> Calculate the average loss and time taken for training on the epoch.
Validation step for the epoch:
> Switch the model to evaluation mode.
> Set the validation loss to 0\. This is used to track the loss of the
model on the validation data over subsequent epochs. The loss should
decrease with each epoch if training was successful.
> Store the time at the start of the validation, this is used to
calculate the time taken for the validation for this epoch to be
completed.
> Split the validation data into batches.
For each batch:
> Move the token IDs, attention masks, and labels to the GPU if
available for faster processing, otherwise these will be kept on the
CPU.
> Invoke the no_grad method to instruct the model not to calculate the
gradients since we wil not be performing any optimization steps here,
only inference.
> Pass the batch to the model to calculate the logits (predictions
based on the current classifier weights and biases) as well as the
loss.
> Extract the logits and labels from the model and move them to the CPU
(if they are not already there).
> Increment the loss and calculate the accuracy based on the true
labels in the validation dataloader.
> Calculate the average loss and accuracy, and add these to the loss
dictionary.
"""
loss_dict = {
'epoch': [i+1 for i in range(self.epochs)],
'average training loss': [],
'average validation loss': []
}
t0_train = datetime.now()
for epoch in range(0, self.epochs):
# Train step
self.model.train()
training_loss = 0
t0_epoch = datetime.now()
print(f'{"-"*20} Epoch {epoch+1} {"-"*20}')
print('\nTraining:\n---------')
print(f'Start Time: {t0_epoch}')
for batch in self.train_dataloader:
batch_token_ids = batch[0].to(self.device)
batch_attention_mask = batch[1].to(self.device)
batch_labels = batch[2].to(self.device)
self.model.zero_grad()
loss, logits = self.model(
batch_token_ids,
token_type_ids = None,
attention_mask=batch_attention_mask,
labels=batch_labels,
return_dict=False)
training_loss += loss.item()
loss.backward()
torch.nn.utils.clip_grad_norm_(self.model.parameters(), 1.0)
self.optimizer.step()
self.scheduler.step()
average_train_loss = training_loss / len(self.train_dataloader)
time_epoch = datetime.now() - t0_epoch
print(f'Average Loss: {average_train_loss}')
print(f'Time Taken: {time_epoch}')
# Validation step
self.model.eval()
val_loss = 0
val_accuracy = 0
t0_val = datetime.now()
print('\nValidation:\n---------')
print(f'Start Time: {t0_val}')
for batch in self.val_dataloader:
batch_token_ids = batch[0].to(self.device)
batch_attention_mask = batch[1].to(self.device)
batch_labels = batch[2].to(self.device)
with torch.no_grad():
(loss, logits) = self.model(
batch_token_ids,
attention_mask = batch_attention_mask,
labels = batch_labels,
token_type_ids = None,
return_dict=False)
logits = logits.detach().cpu().numpy()
label_ids = batch_labels.to('cpu').numpy()
val_loss += loss.item()
val_accuracy += self.calculate_accuracy(logits, label_ids)
average_val_accuracy = val_accuracy / len(self.val_dataloader)
average_val_loss = val_loss / len(self.val_dataloader)
time_val = datetime.now() - t0_val
print(f'Average Loss: {average_val_loss}')
print(f'Average Accuracy: {average_val_accuracy}')
print(f'Time Taken: {time_val}\n')
loss_dict['average training loss'].append(average_train_loss)
loss_dict['average validation loss'].append(average_val_loss)
print(f'Total training time: {datetime.now()-t0_train}')
def calculate_accuracy(self, preds, labels):
""" Calculate the accuracy of model predictions against true labels.
Parameters:
preds (np.array): The predicted label from the model
labels (np.array): The true label
Returns:
accuracy (float): The accuracy as a percentage of the correct
predictions.
"""
pred_flat = np.argmax(preds, axis=1).flatten()
labels_flat = labels.flatten()
accuracy = np.sum(pred_flat == labels_flat) / len(labels_flat)
return accuracy
def predict(self, dataloader):
"""Return the predicted probabilities of each class for input text.
Parameters:
dataloader (torch.utils.data.DataLoader): A DataLoader containing
the token IDs and attention masks for the text to perform
inference on.
Returns:
probs (PyTorch.Tensor): A tensor containing the probability values
for each class as predicted by the model.
"""
self.model.eval()
all_logits = []
for batch in dataloader:
batch_token_ids, batch_attention_mask = tuple(t.to(self.device) \
for t in batch)[:2]
with torch.no_grad():
logits = self.model(batch_token_ids, batch_attention_mask)
all_logits.append(logits)
all_logits = torch.cat(all_logits, dim=0)
probs = F.softmax(all_logits, dim=1).cpu().numpy()
return probs
使用该类进行情感分析(使用 IMDb 数据集)示例:
# Initialise parameters
dataset = preprocess_dataset('IMDB Dataset Very Small.csv')
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = BertForSequenceClassification.from_pretrained(
'bert-base-uncased',
num_labels=2)
optimizer = AdamW(model.parameters())
# Fine-tune model using class
fine_tuned_model = FineTuningPipeline(
dataset = dataset,
tokenizer = tokenizer,
model = model,
optimizer = optimizer,
val_size = 0.1,
epochs = 2,
seed = 42
)
# Make some predictions using the validation dataset
model.predict(model.val_dataloader)
4 — 结论
在本文中,我们探讨了 BERT 的各个方面,包括其创建时的背景、模型架构的详细解析,以及编写任务无关的微调管道,并通过情感分析进行了演示。尽管 BERT 是最早的 LLM 之一,但即使到今天,它仍然具有相关性,并继续在研究和工业中找到应用。理解 BERT 及其对自然语言处理领域的影响为使用最新的最先进模型奠定了坚实的基础。预训练和微调仍然是 LLM 的主导范式,因此希望本文能为你提供一些宝贵的见解,可以在自己的项目中加以应用!
5 — 进一步阅读
[1] J. Devlin, M. Chang, K. Lee, 和 K. Toutanova,BERT:用于语言理解的深度双向变换器预训练(2019),计算语言学学会北美分会
[2] A. Vaswani, N. Shazeer, N. Parmar, J. Uszkoreit, L. Jones, A. N. Gomez, Ł. Kaiser, 和 I. Polosukhin, 注意力机制:一切你需要的 (2017), 神经信息处理系统进展 30(NIPS 2017)
[3] M. E. Peters, M. Neumann, M. Iyyer, M. Gardner, C. Clark, K. Lee, 和 L. Zettlemoyer, 深度上下文化的词表示 (2018), 2018 年北美计算语言学协会年会:人类语言技术会议论文集,第 1 卷(长篇论文)
[4] A. Radford, K. Narasimhan, T. Salimans, 和 I. Sutskever (2018), 通过生成预训练改进语言理解,
[5] Hugging Face, 微调后的 BERT 模型 (2024), HuggingFace.co
[6] M. Schuster 和 K. K. Paliwal, 双向递归神经网络 (1997), IEEE 信号处理学报 45
[7] Y. Zhu, R. Kiros, R. Zemel, R. Salakhutdinov, R. Urtasun, A. Torralba, 和 S. Fidler, 对齐书籍和电影:通过观看电影和阅读书籍实现类似故事的视觉解释 (2015), 2015 年 IEEE 计算机视觉国际会议(ICCV)
[8] L. W. Taylor, “完形填空程序”:衡量可读性的新工具 (1953), 新闻学季刊,30(4),415–433。
[9] Hugging Face, 预训练分词器 (2024) HuggingFace.co
[10] Hugging Face, 预训练分词器编码方法 (2024) HuggingFace.co
[11] T. Vo, PyTorch DataLoader:特点、优势及如何使用 (2023) SaturnCloud.io
[12] Hugging Face, BERT 建模 (2024) GitHub.com
[13] Hugging Face, 运行 Glue, GitHub.com
[14] NVIDIA, CUDA 专区 (2024), Developer.NVIDIA.com
[15] C. McCormick 和 N. Ryan, BERT 微调 (2019), McCormickML.com
完整指南:在 AWS 上掌握 Step Functions
使工作流编排变得更简单
·发表于Towards Data Science ·阅读时长:9 分钟·2024 年 6 月 27 日
--

动机
构建和维护复杂系统仍然是行业中的一个常见挑战,尤其是在云基础设施的环境下。
幸运的是,像**AWS Step Functions**这样的多种工具已被开发出来,旨在简化这些工作流的编排,无论其复杂性如何。
本文提供了一个逐步的指南,带领你从设置云账户到实现实际场景,使用 Step Functions。
什么是 AWS Step Functions?
AWS Step Functions 是一个无服务器的编排服务,主要目的是创建可视化的工作流。它使得 AWS Lambda 函数和其他 AWS 资源的协调变得无缝。
Step Functions 可以与 Amazon EC2、Amazon ECS、本地服务器、Amazon API Gateway 和 Amazon SQS 队列等集成。AWS Step Functions 的这种多功能性使其适用于广泛的应用场景。
开始使用 AWS Step Functions
本节的目标是提供使用 AWS Step Functions 的步骤,从理解构建模块开始,到…
完整指南:编写你自己的 Transformer
这是一个端到端的 Pytorch Transformer 实现,在这个过程中我们将涵盖诸如自注意力(self-attention)、编码器(encoders)、解码器(decoders)等关键概念,以及更多内容。
·发表于Towards Data Science ·阅读时间 18 分钟·2024 年 2 月 24 日
--

图片由Susan Holt Simpson提供,来源于Unsplash。
我们自己编写
当我决定深入研究 Transformer 架构时,在阅读或观看在线教程时,我常常感到沮丧,因为我觉得它们总是遗漏了一些内容:
-
Tensorflow 或 Pytorch 的官方教程使用了它们自己的 API,因此保持在高层次,迫使我必须进入它们的代码库去查看底层实现。这样非常耗时,而且阅读成千上万行代码并不总是容易的。
-
我找到的其他带有自定义代码的教程(文章末尾有链接)通常过于简化了使用场景,并且没有涉及如变长序列批处理的遮蔽等概念。
所以我决定自己写一个 Transformer,以确保我理解这些概念,并能够在任何数据集上使用它。
因此,在本文中,我们将采取一种系统的方法,逐层逐块地实现一个变换器(Transformer)。
显然,已经有许多不同的实现,以及来自 Pytorch 或 Tensorflow 的高级 API 可以直接使用——我相信它们的性能比我们将要构建的模型更好。
“好吧,那为什么不使用 TF/Pytorch 的实现呢?”
本文的目的是为了教育目的,我并不打算超过 Pytorch 或 Tensorflow 的实现。我确实相信,Transformer 背后的理论和代码并不简单直接,这也是为什么我希望通过这篇一步步的教程,帮助你更好地掌握这些概念,并在以后构建自己代码时更加得心应手。
从头开始构建自己的 Transformer 还有一个原因,那就是它能帮助你完全理解如何使用上述的 API。如果我们查看 Pytorch 中 Transformer 类的 forward() 方法的实现,你会看到很多晦涩的关键词,比如:

来源:Pytorch 文档
如果你已经熟悉这些关键词,那么你可以愉快地跳过本文。
否则,本文将带你逐步了解这些关键词及其背后的概念。
Transformer 简短介绍
如果你已经听说过 ChatGPT 或 Gemini,那么你已经接触过 Transformer。实际上,ChatGPT 中的“T”就代表 Transformer。
该架构首次由 Google 研究人员在 2017 年的《Attention is All you need》论文中提出。它具有革命性,因为以前的模型通常依赖于 RNNs 来进行序列到序列的学习(如机器翻译、语音转文本等),而 RNNs 在计算上是昂贵的,因为它们需要一步步地处理序列,而 Transformer 只需一次性查看整个序列,从而将时间复杂度从 O(n) 降低到 O(1)。

Transformer 在 NLP 领域的应用非常广泛,包括语言翻译、问答系统、文档摘要、文本生成等。
Transformer 的整体架构如下:

多头注意力
我们将实现的第一个模块实际上是 Transformer 中最重要的部分,称为多头注意力机制。让我们来看一下它在整体架构中的位置。

注意力机制其实并不是 Transformer 所特有的,它早在 RNN 的序列到序列模型中就已经被使用过。

Transformer 中的注意力机制(来源:Tensorflow 文档)

Transformer 中的注意力机制(来源:Tensorflow 文档)
import torch
import torch.nn as nn
import math
class MultiHeadAttention(nn.Module):
def __init__(self, hidden_dim=256, num_heads=4):
"""
input_dim: Dimensionality of the input.
num_heads: The number of attention heads to split the input into.
"""
super(MultiHeadAttention, self).__init__()
self.hidden_dim = hidden_dim
self.num_heads = num_heads
assert hidden_dim % num_heads == 0, "Hidden dim must be divisible by num heads"
self.Wv = nn.Linear(hidden_dim, hidden_dim, bias=False) # the Value part
self.Wk = nn.Linear(hidden_dim, hidden_dim, bias=False) # the Key part
self.Wq = nn.Linear(hidden_dim, hidden_dim, bias=False) # the Query part
self.Wo = nn.Linear(hidden_dim, hidden_dim, bias=False) # the output layer
def check_sdpa_inputs(self, x):
assert x.size(1) == self.num_heads, f"Expected size of x to be ({-1, self.num_heads, -1, self.hidden_dim // self.num_heads}), got {x.size()}"
assert x.size(3) == self.hidden_dim // self.num_heads
def scaled_dot_product_attention(
self,
query,
key,
value,
attention_mask=None,
key_padding_mask=None):
"""
query : tensor of shape (batch_size, num_heads, query_sequence_length, hidden_dim//num_heads)
key : tensor of shape (batch_size, num_heads, key_sequence_length, hidden_dim//num_heads)
value : tensor of shape (batch_size, num_heads, key_sequence_length, hidden_dim//num_heads)
attention_mask : tensor of shape (query_sequence_length, key_sequence_length)
key_padding_mask : tensor of shape (sequence_length, key_sequence_length)
"""
self.check_sdpa_inputs(query)
self.check_sdpa_inputs(key)
self.check_sdpa_inputs(value)
d_k = query.size(-1)
tgt_len, src_len = query.size(-2), key.size(-2)
# logits = (B, H, tgt_len, E) * (B, H, E, src_len) = (B, H, tgt_len, src_len)
logits = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
# Attention mask here
if attention_mask is not None:
if attention_mask.dim() == 2:
assert attention_mask.size() == (tgt_len, src_len)
attention_mask = attention_mask.unsqueeze(0)
logits = logits + attention_mask
else:
raise ValueError(f"Attention mask size {attention_mask.size()}")
# Key mask here
if key_padding_mask is not None:
key_padding_mask = key_padding_mask.unsqueeze(1).unsqueeze(2) # Broadcast over batch size, num heads
logits = logits + key_padding_mask
attention = torch.softmax(logits, dim=-1)
output = torch.matmul(attention, value) # (batch_size, num_heads, sequence_length, hidden_dim)
return output, attention
def split_into_heads(self, x, num_heads):
batch_size, seq_length, hidden_dim = x.size()
x = x.view(batch_size, seq_length, num_heads, hidden_dim // num_heads)
return x.transpose(1, 2) # Final dim will be (batch_size, num_heads, seq_length, , hidden_dim // num_heads)
def combine_heads(self, x):
batch_size, num_heads, seq_length, head_hidden_dim = x.size()
return x.transpose(1, 2).contiguous().view(batch_size, seq_length, num_heads * head_hidden_dim)
def forward(
self,
q,
k,
v,
attention_mask=None,
key_padding_mask=None):
"""
q : tensor of shape (batch_size, query_sequence_length, hidden_dim)
k : tensor of shape (batch_size, key_sequence_length, hidden_dim)
v : tensor of shape (batch_size, key_sequence_length, hidden_dim)
attention_mask : tensor of shape (query_sequence_length, key_sequence_length)
key_padding_mask : tensor of shape (sequence_length, key_sequence_length)
"""
q = self.Wq(q)
k = self.Wk(k)
v = self.Wv(v)
q = self.split_into_heads(q, self.num_heads)
k = self.split_into_heads(k, self.num_heads)
v = self.split_into_heads(v, self.num_heads)
# attn_values, attn_weights = self.multihead_attn(q, k, v, attn_mask=attention_mask)
attn_values, attn_weights = self.scaled_dot_product_attention(
query=q,
key=k,
value=v,
attention_mask=attention_mask,
key_padding_mask=key_padding_mask,
)
grouped = self.combine_heads(attn_values)
output = self.Wo(grouped)
self.attention_weigths = attn_weights
return output
我们需要在这里解释几个概念。
1) 查询、键和值。
查询是你试图匹配的信息,
键和 值 是存储的信息。
可以把它想象成使用字典:每当使用 Python 字典时,如果你的查询与字典的键不匹配,你将不会得到任何返回。但如果我们希望字典返回的是一组相似的信息呢?就像我们有:
d = {"panther": 1, "bear": 10, "dog":3}
d["wolf"] = 0.2*d["panther"] + 0.7*d["dog"] + 0.1*d["bear"]
这基本上就是注意力的作用:观察数据的不同部分,并将它们融合成一个答案来回应你的查询。
代码的相关部分是这一部分,我们计算查询与键之间的注意力权重
logits = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k) # we compute the weights of attention
这是我们应用标准化权重到值上的情况:
attention = torch.softmax(logits, dim=-1)
output = torch.matmul(attention, value) # (batch_size, num_heads, sequence_length, hidden_dim)
2) 注意力掩码和填充
在处理顺序输入的各部分时,我们不希望包含无用或禁止的信息。
无用信息例如填充:填充符号用于将批次中的所有序列对齐到相同的序列大小,我们的模型应忽略这些填充。我们将在最后一节回到这一点
禁止信息稍微复杂一些。在训练过程中,模型学习编码输入序列,并将目标与输入对齐。然而,由于推理过程涉及查看先前发出的标记来预测下一个标记(例如 ChatGPT 中的文本生成),我们在训练时也需要应用相同的规则。
这就是我们应用 因果掩码 的原因,以确保每个时间步的目标只能看到来自过去的信息。以下是应用掩码的对应部分(掩码的计算将在最后部分讨论)
if attention_mask is not None:
if attention_mask.dim() == 2:
assert attention_mask.size() == (tgt_len, src_len)
attention_mask = attention_mask.unsqueeze(0)
logits = logits + attention_mask
位置编码
它对应于 Transformer 的以下部分:

当接收和处理输入时,Transformer 没有顺序感知,因为它将序列视为整体,这与 RNN 的做法不同。因此,我们需要加入一些时间顺序的提示,以便 Transformer 能学习依赖关系。
位置编码如何工作的具体细节超出了本文的范围,但可以随时阅读原始论文以了解更多。
# Taken from https://pytorch.org/tutorials/beginner/transformer_tutorial.html#define-the-model
class PositionalEncoding(nn.Module):
def __init__(self, d_model, dropout=0.1, max_len=5000):
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(p=dropout)
pe = torch.zeros(max_len, d_model)
position = torch.arange(max_len, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.unsqueeze(0)
self.register_buffer('pe', pe)
def forward(self, x):
"""
Arguments:
x: Tensor, shape ``[batch_size, seq_len, embedding_dim]``
"""
x = x + self.pe[:, :x.size(1), :]
return x
编码器
我们即将完成一个完整的编码器!编码器是 Transformer 的左侧部分

我们将向代码中添加一个小部分,即前馈部分:
class PositionWiseFeedForward(nn.Module):
def __init__(self, d_model: int, d_ff: int):
super(PositionWiseFeedForward, self).__init__()
self.fc1 = nn.Linear(d_model, d_ff)
self.fc2 = nn.Linear(d_ff, d_model)
self.relu = nn.ReLU()
def forward(self, x):
return self.fc2(self.relu(self.fc1(x)))
将各部分拼接起来,我们得到了一个编码器模块!
class EncoderBlock(nn.Module):
def __init__(self, n_dim: int, dropout: float, n_heads: int):
super(EncoderBlock, self).__init__()
self.mha = MultiHeadAttention(hidden_dim=n_dim, num_heads=n_heads)
self.norm1 = nn.LayerNorm(n_dim)
self.ff = PositionWiseFeedForward(n_dim, n_dim)
self.norm2 = nn.LayerNorm(n_dim)
self.dropout = nn.Dropout(dropout)
def forward(self, x, src_padding_mask=None):
assert x.ndim==3, "Expected input to be 3-dim, got {}".format(x.ndim)
att_output = self.mha(x, x, x, key_padding_mask=src_padding_mask)
x = x + self.dropout(self.norm1(att_output))
ff_output = self.ff(x)
output = x + self.norm2(ff_output)
return output
如图所示,编码器实际上包含 N 个编码器块或层,以及用于输入的嵌入层。因此,我们通过添加嵌入层、位置编码和编码器块来创建一个编码器:
class Encoder(nn.Module):
def __init__(
self,
vocab_size: int,
n_dim: int,
dropout: float,
n_encoder_blocks: int,
n_heads: int):
super(Encoder, self).__init__()
self.n_dim = n_dim
self.embedding = nn.Embedding(
num_embeddings=vocab_size,
embedding_dim=n_dim
)
self.positional_encoding = PositionalEncoding(
d_model=n_dim,
dropout=dropout
)
self.encoder_blocks = nn.ModuleList([
EncoderBlock(n_dim, dropout, n_heads) for _ in range(n_encoder_blocks)
])
def forward(self, x, padding_mask=None):
x = self.embedding(x) * math.sqrt(self.n_dim)
x = self.positional_encoding(x)
for block in self.encoder_blocks:
x = block(x=x, src_padding_mask=padding_mask)
return x
解码器
解码器部分是左侧部分,需要更多的构建工作。

有一种叫做掩蔽多头注意力的机制。还记得我们之前说过的因果掩蔽吗?这里也会用到。我们将使用多头注意力模块的 attention_mask 参数来表示这一点(更多关于如何计算掩蔽的细节在最后):
# Stuff before
self.self_attention = MultiHeadAttention(hidden_dim=n_dim, num_heads=n_heads)
masked_att_output = self.self_attention(
q=tgt,
k=tgt,
v=tgt,
attention_mask=tgt_mask, <-- HERE IS THE CAUSAL MASK
key_padding_mask=tgt_padding_mask)
# Stuff after
第二种注意力机制叫做交叉注意力。它会使用解码器的查询与编码器的键和值进行匹配!请注意:在训练过程中,它们的长度可能不同,因此通常最好明确定义输入的预期形状,如下所示:
def scaled_dot_product_attention(
self,
query,
key,
value,
attention_mask=None,
key_padding_mask=None):
"""
query : tensor of shape (batch_size, num_heads, query_sequence_length, hidden_dim//num_heads)
key : tensor of shape (batch_size, num_heads, key_sequence_length, hidden_dim//num_heads)
value : tensor of shape (batch_size, num_heads, key_sequence_length, hidden_dim//num_heads)
attention_mask : tensor of shape (query_sequence_length, key_sequence_length)
key_padding_mask : tensor of shape (sequence_length, key_sequence_length)
"""
这是我们使用编码器输出的部分,称为记忆,并将其与解码器输入结合的地方:
# Stuff before
self.cross_attention = MultiHeadAttention(hidden_dim=n_dim, num_heads=n_heads)
cross_att_output = self.cross_attention(
q=x1,
k=memory,
v=memory,
attention_mask=None, <-- NO CAUSAL MASK HERE
key_padding_mask=memory_padding_mask) <-- WE NEED TO USE THE PADDING OF THE SOURCE
# Stuff after
将各个部分组合在一起,我们得到了解码器的结构:
class DecoderBlock(nn.Module):
def __init__(self, n_dim: int, dropout: float, n_heads: int):
super(DecoderBlock, self).__init__()
# The first Multi-Head Attention has a mask to avoid looking at the future
self.self_attention = MultiHeadAttention(hidden_dim=n_dim, num_heads=n_heads)
self.norm1 = nn.LayerNorm(n_dim)
# The second Multi-Head Attention will take inputs from the encoder as key/value inputs
self.cross_attention = MultiHeadAttention(hidden_dim=n_dim, num_heads=n_heads)
self.norm2 = nn.LayerNorm(n_dim)
self.ff = PositionWiseFeedForward(n_dim, n_dim)
self.norm3 = nn.LayerNorm(n_dim)
# self.dropout = nn.Dropout(dropout)
def forward(self, tgt, memory, tgt_mask=None, tgt_padding_mask=None, memory_padding_mask=None):
masked_att_output = self.self_attention(
q=tgt, k=tgt, v=tgt, attention_mask=tgt_mask, key_padding_mask=tgt_padding_mask)
x1 = tgt + self.norm1(masked_att_output)
cross_att_output = self.cross_attention(
q=x1, k=memory, v=memory, attention_mask=None, key_padding_mask=memory_padding_mask)
x2 = x1 + self.norm2(cross_att_output)
ff_output = self.ff(x2)
output = x2 + self.norm3(ff_output)
return output
class Decoder(nn.Module):
def __init__(
self,
vocab_size: int,
n_dim: int,
dropout: float,
n_decoder_blocks: int,
n_heads: int):
super(Decoder, self).__init__()
self.embedding = nn.Embedding(
num_embeddings=vocab_size,
embedding_dim=n_dim,
padding_idx=0
)
self.positional_encoding = PositionalEncoding(
d_model=n_dim,
dropout=dropout
)
self.decoder_blocks = nn.ModuleList([
DecoderBlock(n_dim, dropout, n_heads) for _ in range(n_decoder_blocks)
])
def forward(self, tgt, memory, tgt_mask=None, tgt_padding_mask=None, memory_padding_mask=None):
x = self.embedding(tgt)
x = self.positional_encoding(x)
for block in self.decoder_blocks:
x = block(
x,
memory,
tgt_mask=tgt_mask,
tgt_padding_mask=tgt_padding_mask,
memory_padding_mask=memory_padding_mask)
return x
填充与掩蔽
记得在多头注意力部分,我们提到过在做注意力计算时,排除输入的某些部分。
在训练过程中,我们会考虑输入和目标的批次,其中每个实例可能具有不同的长度。考虑以下示例,我们将四个单词批处理在一起:banana, watermelon, pear, blueberry。为了将它们作为一个批次处理,我们需要将所有单词对齐到最长单词的长度(watermelon)。因此,我们将为每个单词添加一个额外的标记,PAD,使它们都达到与 watermelon 相同的长度。
在下面的图中,上表表示原始数据,下表表示编码后的版本:

(图片来源:作者)
在我们的案例中,我们希望排除填充索引对计算注意力权重的影响。因此,我们可以如下计算掩蔽,分别用于源数据和目标数据:
padding_mask = (x == PAD_IDX)
那么因果掩蔽怎么办?如果我们希望模型在每个时间步只关注过去的步骤,这意味着对于每个时间步 T,模型只能关注 t 在 1…T 范围内的每个步骤。这是一个双重 for 循环,因此我们可以使用矩阵来计算这一点:

(图片来源:作者)
def generate_square_subsequent_mask(size: int):
"""Generate a triangular (size, size) mask. From PyTorch docs."""
mask = (1 - torch.triu(torch.ones(size, size), diagonal=1)).bool()
mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))
return mask
案例研究:一个单词反转 Transformer
现在让我们将各个部分整合起来,构建我们的 Transformer!
在我们的使用案例中,我们将使用一个非常简单的数据集来展示 Transformer 是如何学习的。
“但是为什么要使用 Transformer 来反转单词?我已经知道如何在 Python 中使用 word[::-1]来做了!”
这里的目标是观察 Transformer 的注意力机制是否有效。我们期望看到的是,当给定一个输入序列时,注意力权重会从右向左移动。如果是这样,这意味着我们的 Transformer 已经学会了一种非常简单的语法,即从右到左阅读,并且在进行实际的语言翻译时,能够推广到更复杂的语法。
让我们首先从我们自定义的 Transformer 类开始:
import torch
import torch.nn as nn
import math
from .encoder import Encoder
from .decoder import Decoder
class Transformer(nn.Module):
def __init__(self, **kwargs):
super(Transformer, self).__init__()
for k, v in kwargs.items():
print(f" * {k}={v}")
self.vocab_size = kwargs.get('vocab_size')
self.model_dim = kwargs.get('model_dim')
self.dropout = kwargs.get('dropout')
self.n_encoder_layers = kwargs.get('n_encoder_layers')
self.n_decoder_layers = kwargs.get('n_decoder_layers')
self.n_heads = kwargs.get('n_heads')
self.batch_size = kwargs.get('batch_size')
self.PAD_IDX = kwargs.get('pad_idx', 0)
self.encoder = Encoder(
self.vocab_size, self.model_dim, self.dropout, self.n_encoder_layers, self.n_heads)
self.decoder = Decoder(
self.vocab_size, self.model_dim, self.dropout, self.n_decoder_layers, self.n_heads)
self.fc = nn.Linear(self.model_dim, self.vocab_size)
@staticmethod
def generate_square_subsequent_mask(size: int):
"""Generate a triangular (size, size) mask. From PyTorch docs."""
mask = (1 - torch.triu(torch.ones(size, size), diagonal=1)).bool()
mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))
return mask
def encode(
self,
x: torch.Tensor,
) -> torch.Tensor:
"""
Input
x: (B, S) with elements in (0, C) where C is num_classes
Output
(B, S, E) embedding
"""
mask = (x == self.PAD_IDX).float()
encoder_padding_mask = mask.masked_fill(mask == 1, float('-inf'))
# (B, S, E)
encoder_output = self.encoder(
x,
padding_mask=encoder_padding_mask
)
return encoder_output, encoder_padding_mask
def decode(
self,
tgt: torch.Tensor,
memory: torch.Tensor,
memory_padding_mask=None
) -> torch.Tensor:
"""
B = Batch size
S = Source sequence length
L = Target sequence length
E = Model dimension
Input
encoded_x: (B, S, E)
y: (B, L) with elements in (0, C) where C is num_classes
Output
(B, L, C) logits
"""
mask = (tgt == self.PAD_IDX).float()
tgt_padding_mask = mask.masked_fill(mask == 1, float('-inf'))
decoder_output = self.decoder(
tgt=tgt,
memory=memory,
tgt_mask=self.generate_square_subsequent_mask(tgt.size(1)),
tgt_padding_mask=tgt_padding_mask,
memory_padding_mask=memory_padding_mask,
)
output = self.fc(decoder_output) # shape (B, L, C)
return output
def forward(
self,
x: torch.Tensor,
y: torch.Tensor,
) -> torch.Tensor:
"""
Input
x: (B, Sx) with elements in (0, C) where C is num_classes
y: (B, Sy) with elements in (0, C) where C is num_classes
Output
(B, L, C) logits
"""
# Encoder output shape (B, S, E)
encoder_output, encoder_padding_mask = self.encode(x)
# Decoder output shape (B, L, C)
decoder_output = self.decode(
tgt=y,
memory=encoder_output,
memory_padding_mask=encoder_padding_mask
)
return decoder_output
使用贪心解码进行推理
我们需要添加一个方法,它将充当 scikit.learn 中著名的model.predict。目标是让模型根据输入动态输出预测。在推理过程中,没有目标:模型首先通过关注输出生成一个 token,并使用自身的预测继续生成 token。这就是为什么这些模型通常被称为自回归模型,因为它们使用过去的预测来预测下一个。
贪心解码的问题在于它每一步都选择概率最高的 token。如果第一个 token 完全错误,这可能会导致非常糟糕的预测。还有其他解码方法,如 Beam search,它考虑一个候选序列的简短列表(可以理解为在每个时间步骤保持 top-k token 而不是 argmax),并返回具有最高总概率的序列。
现在,让我们实现贪心解码并将其添加到我们的 Transformer 模型中:
def predict(
self,
x: torch.Tensor,
sos_idx: int=1,
eos_idx: int=2,
max_length: int=None
) -> torch.Tensor:
"""
Method to use at inference time. Predict y from x one token at a time. This method is greedy
decoding. Beam search can be used instead for a potential accuracy boost.
Input
x: str
Output
(B, L, C) logits
"""
# Pad the tokens with beginning and end of sentence tokens
x = torch.cat([
torch.tensor([sos_idx]),
x,
torch.tensor([eos_idx])]
).unsqueeze(0)
encoder_output, mask = self.transformer.encode(x) # (B, S, E)
if not max_length:
max_length = x.size(1)
outputs = torch.ones((x.size()[0], max_length)).type_as(x).long() * sos_idx
for step in range(1, max_length):
y = outputs[:, :step]
probs = self.transformer.decode(y, encoder_output)
output = torch.argmax(probs, dim=-1)
# Uncomment if you want to see step by step predicitons
# print(f"Knowing {y} we output {output[:, -1]}")
if output[:, -1].detach().numpy() in (eos_idx, sos_idx):
break
outputs[:, step] = output[:, -1]
return outputs
创建示例数据
我们定义了一个小数据集,它会反转单词,这意味着“helloworld”将返回“dlrowolleh”:
import numpy as np
import torch
from torch.utils.data import Dataset
np.random.seed(0)
def generate_random_string():
len = np.random.randint(10, 20)
return "".join([chr(x) for x in np.random.randint(97, 97+26, len)])
class ReverseDataset(Dataset):
def __init__(self, n_samples, pad_idx, sos_idx, eos_idx):
super(ReverseDataset, self).__init__()
self.pad_idx = pad_idx
self.sos_idx = sos_idx
self.eos_idx = eos_idx
self.values = [generate_random_string() for _ in range(n_samples)]
self.labels = [x[::-1] for x in self.values]
def __len__(self):
return len(self.values) # number of samples in the dataset
def __getitem__(self, index):
return self.text_transform(self.values[index].rstrip("\n")), \
self.text_transform(self.labels[index].rstrip("\n"))
def text_transform(self, x):
return torch.tensor([self.sos_idx] + [ord(z)-97+3 for z in x] + [self.eos_idx])
我们现在将定义训练和评估步骤:
PAD_IDX = 0
SOS_IDX = 1
EOS_IDX = 2
def train(model, optimizer, loader, loss_fn, epoch):
model.train()
losses = 0
acc = 0
history_loss = []
history_acc = []
with tqdm(loader, position=0, leave=True) as tepoch:
for x, y in tepoch:
tepoch.set_description(f"Epoch {epoch}")
optimizer.zero_grad()
logits = model(x, y[:, :-1])
loss = loss_fn(logits.contiguous().view(-1, model.vocab_size), y[:, 1:].contiguous().view(-1))
loss.backward()
optimizer.step()
losses += loss.item()
preds = logits.argmax(dim=-1)
masked_pred = preds * (y[:, 1:]!=PAD_IDX)
accuracy = (masked_pred == y[:, 1:]).float().mean()
acc += accuracy.item()
history_loss.append(loss.item())
history_acc.append(accuracy.item())
tepoch.set_postfix(loss=loss.item(), accuracy=100\. * accuracy.item())
return losses / len(list(loader)), acc / len(list(loader)), history_loss, history_acc
def evaluate(model, loader, loss_fn):
model.eval()
losses = 0
acc = 0
history_loss = []
history_acc = []
for x, y in tqdm(loader, position=0, leave=True):
logits = model(x, y[:, :-1])
loss = loss_fn(logits.contiguous().view(-1, model.vocab_size), y[:, 1:].contiguous().view(-1))
losses += loss.item()
preds = logits.argmax(dim=-1)
masked_pred = preds * (y[:, 1:]!=PAD_IDX)
accuracy = (masked_pred == y[:, 1:]).float().mean()
acc += accuracy.item()
history_loss.append(loss.item())
history_acc.append(accuracy.item())
return losses / len(list(loader)), acc / len(list(loader)), history_loss, history_acc
然后训练模型若干轮:
import torch
import time
import torch.nn as nn
import numpy as np
import matplotlib.pyplot as plt
from tqdm import tqdm
from torch.utils.data import DataLoader
from torch.nn.utils.rnn import pad_sequence
from mpl_toolkits.axes_grid1 import ImageGrid
def collate_fn(batch):
"""
This function pads inputs with PAD_IDX to have batches of equal length
"""
src_batch, tgt_batch = [], []
for src_sample, tgt_sample in batch:
src_batch.append(src_sample)
tgt_batch.append(tgt_sample)
src_batch = pad_sequence(src_batch, padding_value=PAD_IDX, batch_first=True)
tgt_batch = pad_sequence(tgt_batch, padding_value=PAD_IDX, batch_first=True)
return src_batch, tgt_batch
# Model hyperparameters
args = {
'vocab_size': 128,
'model_dim': 128,
'dropout': 0.1,
'n_encoder_layers': 1,
'n_decoder_layers': 1,
'n_heads': 4
}
# Define model here
model = Transformer(**args)
# Instantiate datasets
train_iter = ReverseDataset(50000, pad_idx=PAD_IDX, sos_idx=SOS_IDX, eos_idx=EOS_IDX)
eval_iter = ReverseDataset(10000, pad_idx=PAD_IDX, sos_idx=SOS_IDX, eos_idx=EOS_IDX)
dataloader_train = DataLoader(train_iter, batch_size=256, collate_fn=collate_fn)
dataloader_val = DataLoader(eval_iter, batch_size=256, collate_fn=collate_fn)
# During debugging, we ensure sources and targets are indeed reversed
# s, t = next(iter(dataloader_train))
# print(s[:4, ...])
# print(t[:4, ...])
# print(s.size())
# Initialize model parameters
for p in model.parameters():
if p.dim() > 1:
nn.init.xavier_uniform_(p)
# Define loss function : we ignore logits which are padding tokens
loss_fn = torch.nn.CrossEntropyLoss(ignore_index=PAD_IDX)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001, betas=(0.9, 0.98), eps=1e-9)
# Save history to dictionnary
history = {
'train_loss': [],
'eval_loss': [],
'train_acc': [],
'eval_acc': []
}
# Main loop
for epoch in range(1, 4):
start_time = time.time()
train_loss, train_acc, hist_loss, hist_acc = train(model, optimizer, dataloader_train, loss_fn, epoch)
history['train_loss'] += hist_loss
history['train_acc'] += hist_acc
end_time = time.time()
val_loss, val_acc, hist_loss, hist_acc = evaluate(model, dataloader_val, loss_fn)
history['eval_loss'] += hist_loss
history['eval_acc'] += hist_acc
print((f"Epoch: {epoch}, Train loss: {train_loss:.3f}, Train acc: {train_acc:.3f}, Val loss: {val_loss:.3f}, Val acc: {val_acc:.3f} "f"Epoch time = {(end_time - start_time):.3f}s"))

可视化注意力
我们定义了一个小函数来访问注意力头的权重:
fig = plt.figure(figsize=(10., 10.))
images = model.decoder.decoder_blocks[0].cross_attention.attention_weigths[0,...].detach().numpy()
grid = ImageGrid(fig, 111, # similar to subplot(111)
nrows_ncols=(2, 2), # creates 2x2 grid of axes
axes_pad=0.1, # pad between axes in inch.
)
for ax, im in zip(grid, images):
# Iterating over the grid returns the Axes.
ax.imshow(im)

作者提供的图片
我们可以看到一个漂亮的从右到左的模式,当从上方读取权重时。y 轴底部的垂直部分可能表示由于填充掩蔽导致的掩蔽权重。
测试我们的模型!
为了用新数据测试我们的模型,我们将定义一个小的 Translator 类来帮助我们进行解码:
class Translator(nn.Module):
def __init__(self, transformer):
super(Translator, self).__init__()
self.transformer = transformer
@staticmethod
def str_to_tokens(s):
return [ord(z)-97+3 for z in s]
@staticmethod
def tokens_to_str(tokens):
return "".join([chr(x+94) for x in tokens])
def __call__(self, sentence, max_length=None, pad=False):
x = torch.tensor(self.str_to_tokens(sentence))
x = torch.cat([torch.tensor([SOS_IDX]), x, torch.tensor([EOS_IDX])]).unsqueeze(0)
encoder_output, mask = self.transformer.encode(x) # (B, S, E)
if not max_length:
max_length = x.size(1)
outputs = torch.ones((x.size()[0], max_length)).type_as(x).long() * SOS_IDX
for step in range(1, max_length):
y = outputs[:, :step]
probs = self.transformer.decode(y, encoder_output)
output = torch.argmax(probs, dim=-1)
print(f"Knowing {y} we output {output[:, -1]}")
if output[:, -1].detach().numpy() in (EOS_IDX, SOS_IDX):
break
outputs[:, step] = output[:, -1]
return self.tokens_to_str(outputs[0])
translator = Translator(model)
你应该能看到以下内容:

如果我们打印注意力头,我们将观察到以下内容:
fig = plt.figure()
images = model.decoder.decoder_blocks[0].cross_attention.attention_weigths[0,...].detach().numpy().mean(axis=0)
fig, ax = plt.subplots(1,1, figsize=(10., 10.))
# Iterating over the grid returs the Axes.
ax.set_yticks(range(len(out)))
ax.set_xticks(range(len(sentence)))
ax.xaxis.set_label_position('top')
ax.set_xticklabels(iter(sentence))
ax.set_yticklabels([f"step {i}" for i in range(len(out))])
ax.imshow(images)

作者提供的图片
我们可以清楚地看到,当我们倒置句子“reversethis”时,模型是从右到左进行关注的!(步骤 0 实际上接收的是句子的开始 token)。
结论
就这样,你现在能够编写 Transformer 并将其应用于更大的数据集,进行机器翻译或创建你自己的 BERT 例如!
我希望这个教程能向你展示在编写 Transformer 时的注意事项:填充和掩蔽可能是最需要关注的部分(无心的双关),因为它们将在推理过程中决定模型的良好性能。
在接下来的文章中,我们将看看如何创建你自己的 BERT 模型,以及如何使用 Equinox,一个基于 JAX 的高性能库。
敬请期待!
有用的链接
(+) “从零开始学习 Transformers”
(+) “使用 Transformer 和 Keras 的神经机器翻译”
(+) “图解 Transformer”
(+) 阿姆斯特丹大学深度学习教程
(+) Pytorch 关于 Transformer 的教程
《实践中的协作 AI 代理综合指南》
定义概述,以及构建一个 AI 代理团队,优化你的简历和求职信,帮助你申请工作
·发表于面向数据科学 ·14 分钟阅读·2024 年 7 月 3 日
--

图片由 DALLE3 生成。
还不是会员?免费阅读!
代理型 AI 是 2024 年 AI 社区最热门的话题之一,背后有充分的理由。基础模型在推理和规划方面变得越来越复杂。凭借这些能力,我们可以利用大型语言模型(LLMs)将给定任务分解为更小的部分,逐步执行这些部分,并反思其结果,从而创建AI 代理。
如果你对 AI 充满热情,或者喜欢玩转语言模型,或者从事机器学习工作,那么全面理解 AI 代理以及它们如何在实际中运作,是跟上最新 AI 成就的必要条件。
如果你像我一样正在找工作,你一定知道,为每个申请的职位写一封求职信,或者调整简历以契合职位要求,是一项繁琐的工作。对于我的 AI 代理实现,我将开发一个 AI 代理团队,这些代理合作完成以下任务:
-
提取职位描述中的关键信息
-
修改你的简历和求职信以符合职位要求
-
评估从招聘者的角度来看最终结果,并给你打分,从 0 到 100 分

图 1. 我们将创建一个 AI 代理团队来修改我们的简历和求职信,并给我们打分,从 0 到 100 分。(作者提供)
在本文中,我们将深入探讨什么是协作 AI 智能体,智能体与 LLM 有什么不同,并通过一个实际的实现来展示如何创建 AI 智能体比你预期的要简单得多。
· AI 智能体简化版
∘ LLM 驱动的 AI 智能体
· 多智能体协作
∘ 什么构成一个智能体
∘ 多智能体需要什么
· 代码实现:帮助你申请工作的智能体
AI 智能体简化版
AI 智能体是一种以目标为导向的工具,旨在完成一组特定的任务。虽然 AI 语言模型的设计目的是为了响应特定的提示,AI 智能体则在更广泛的范围内运作,通过决策和任务执行来解决复杂问题。它们围绕一个任务进行推理,并通过使用可用工具,执行小任务来完成大目标。
AI 聊天机器人会说话,AI 智能体会行动。
无论是在客户服务、金融还是软件开发领域,AI 智能体的目标都是一样的:自动化。希望 AI 智能体能够帮助个人和组织处理日常琐事,以更低的成本解决那些创意性较低的任务。

图 2. 基于 LLM 的智能体模型结构概览。(来源:Wikimedia)
LLM 驱动的 AI 智能体
尽管 AI 智能体与大型语言模型(LLM)有很大不同,LLM 是我们的 AI 智能体的大脑。智能体需要 LLM 来执行智能的操作,进行推理,并规划下一步。也就是说,使用哪个 LLM 会完全改变智能体的行为。这就是为什么 AI 智能体话题的兴起得益于生成型 AI 的快速发展。
注意: 我们今天所听到的 AI 智能体,与传统文献和教科书中的定义略有不同。如果你仅仅是在搜索 AI 智能体,你会得到各种不同的定义,这可能会让你感到困惑。我们今天使用的 AI 智能体,也是当前 AI 社区的热门话题,是基于 LLM 的智能体,正是本文讨论的内容。

图 3. (作者提供)
从本质上讲,自主 AI 智能体依赖于三个组成部分:
-
规划: 这是智能体的核心功能,使其能够将一个目标拆分成更小的步骤,并逐一完成。它们规划的另一个方面是自我反思,从而从中学习。智能体如何进行自我反思很大程度上取决于实现方式,但一般框架可以参考图 4 所示。
-
记忆: 要从过去的错误中学习,你必须记住它们。记忆是智能体存储并随后检索信息的组件,用来改进其行为。
-
工具使用:简单的 LLM 和 AI 代理之间的一个关键区别是它们使用工具的能力。使用工具可以像调用 API 或使用 Python 函数读取或写入文件一样简单。

图 4. 自我反思是代理思考该做什么、执行并观察结果的有限循环,可能会采用新的方法来解决问题。(作者)
多代理协作
单一代理有什么不如多个代理的地方?多个代理!
拥有一个 AI 代理是一回事,但拥有多个相互协作的 AI 代理来分解任务并执行它们又是另外一回事。
但是,为什么我们需要多个代理?
当将目标分解成更小的部分时,你会得到需要不同技能集的子目标。这时你就需要多个代理。一个代理团队,每个代理都有特定的角色和技能,确保每个子目标都由相应的代理来处理。
你可能需要为每个代理提供不同的 LLM(大语言模型),这个 LLM 需要对该代理所承担的任务更加精细化。一个为编程能力创建的代理可能需要与一个负责写文章的代理完全不同的 LLM。
代理的构成
如何定义一个代理在很大程度上取决于你使用的实现或库。通常,代理归结为三个主要元素:

图 5.(作者)
-
目标:代理旨在实现的具体目标。这决定了其决策框架。例如,“编写易于理解的面向对象 Python 代码。”
-
角色:代理的功能。它是什么?调试器、数据科学家、销售市场人员等。
-
背景故事:代理的背景。解释目标、角色以及代理擅长的领域。一个背景故事的例子可能是:“你是一位资深的 Python 程序员,专门编写优化的、文档齐全的代码以及其测试用例。”
多代理的需求
多代理的实现细节有一些需要考虑的细微差别。想象一下一个团队朝着同一个目标努力,举例来说,可以是餐厅厨房里的厨师团队。

由 DALLE3 创建的图像。
你需要一个领头大厨来领导团队。团队成员需要相互沟通。你需要他们能够将完成的工作传给其他厨师,为下一步的食物准备做好准备。这只是一个例子,说明了要让多个代理协作完成共享任务需要的许多要素。
通常,多个代理需要:
-
信息共享:代理需要将自己的结果传递给其他代理,并分享他们的发现。一个代理完成的工作可能是另一个代理开始其任务的输入。
-
协作:代理应该能够互相帮助,并在需要时委派工作。这在简单的场景中可能不是必须的,但在复杂的流程中是非常必要的。
-
经理代理:控制代理之间任务的流动,保持任务的可控性。
代码实现:帮助你申请工作的代理
现在,让我们创建我们的代理团队。有多个库可以帮助你开发 AI 代理,例如 LlamaIndex 或 LangChain。我将使用 CrewAI,因为它的工作流程易于使用且抽象程度高。它是免费的,虽然可以充分控制代理,但在简单项目中能避免不必要的复杂性。
我将创建一个代理团队,帮助我根据给定的职位描述修改我的简历和求职信,以评估最终结果并看看我有多大机会获得面试机会。为此,我将创建四个代理:
-
职位爬虫:该代理将接收职位发布的 URL,爬取网页,并提取关于职位要求、资格等的关键信息。
-
简历修改器:基于职位信息,由职位爬虫提供,该代理会读取我的简历并对其进行增强,以便更好地符合职位描述。
-
求职信修改器:这个代理的工作方式与简历修改器相同,但作用于我的求职信。
-
招聘者:充当职位招聘者的角色,分析我修改后的简历和求职信。它将给出反馈,并提供一个[0–100]范围内的评分。

图 6. 代理工作流概述。(作者)
对于 LLM,我们将使用gpt-4-turbo。然而,使用 Ollama,你可以在本地100%并且免费运行 LLM。我在本文中不会详细讨论,但如果你想了解如何在本地运行LLama-3、Mistral、Phi-3等多个模型,请阅读这篇文章:
[## 🚀 在 5 个简单步骤中构建您的第一个 AI 代理(100%本地化)
使用 CrewAI 创建 AI 代理,并通过 Ollama 在本地 100%运行它们,五个非常简单的步骤!
pub.towardsai.net](https://pub.towardsai.net/build-your-first-ai-agent-in-5-easy-steps-100-local-2fb771438a8f?source=post_page-----1f4048947d9c--------------------------------)
接下来,我将加载我的OPENAI_API_KEY并初始化我的model。这个模型是我们代理推理的核心:
from dotenv import load_dotenv
from crewai import Agent, Task, Crew, Process
from langchain_openai import ChatOpenAI
from langchain.tools import tool
import re
# Load your OPENAI_API_KEY from your .env file
load_dotenv()
# The model for the agents
model = ChatOpenAI(model_name="gpt-4-turbo", temperature=0.8)
我们还需要定义两个工具,供代理使用。工具是代理用来执行特定任务的工具。为代理编写自定义工具就像编写一个 Python 函数一样简单。你只需要使用 Langchain 的工具装饰器将其包装起来。我们为这个项目所需的工具包括一个通过给定 URL 获取页面内容的工具和一个读取CV.pdf和Cover Letter.pdf内容的工具。
from langchain_community.document_loaders import PyMuPDFLoader
import requests
# Tool for loading and reading a PDF locally
@tool
def fetch_pdf_content(pdf_path: str):
"""
Reads a local PDF and returns the content
"""
loader = PyMuPDFLoader(pdf_path)
data = loader.load()[0]
return data.page_content
# Tool for loading a webpage
@tool
def get_webpage_contents(url: str):
"""
Reads the webpage with a given URL and returns the page content
"""
try:
response = requests.get(url)
response.raise_for_status() # Check for HTTP errors
return response.text
except requests.exceptions.RequestException as e:
return str(e)
准备好我的模型和所需的工具后,接下来是定义我的代理。CrewAI 中的代理需要三个主要属性,角色、目标和背景故事。对于每个代理,我们将指定一组所需的工具。代理还可以选择将部分任务委派给其他代理,但由于这个项目并不复杂,我们将allow_delegation设置为 False,适用于所有代理。
job_crawler = Agent(
role='Job Description Crawler',
goal='Extract the relevant job description, requirements and qualificiations',
backstory='Specialized in parsing HTML and retrieving important information from it',
verbose=True,
tools=[get_webpage_contents],
allow_delegation=False,
llm=model
)
cv_modifier = Agent(
role='CV/Resume Writer',
goal='Write a top-notch CV that increases the chance of landing an interview',
backstory='Expert in writing CV that is best recieved by the recruiting team and HR',
verbose=True,
tools=[fetch_pdf_content],
allow_delegation=False,
llm=model
)
cover_letter_modifier = Agent(
role='Cover Letter Writer',
goal='Write an intriguing cover letter that boosts the chance of landing an interview',
backstory='Expert in writing Cover Letter that is best recieved by the recruiting team and HR',
verbose=True,
tools=[fetch_pdf_content],
allow_delegation=False,
llm=model
)
recruiter = Agent(
role='Hiring Manager',
goal='Analyze how well a candidate is suited for a job description, given their CV and Cover Letter',
backstory='Experienced hiring manager with an especialization of giving feedback to job seekers',
verbose=True,
allow_delegation=False,
llm=model
)
现在我们需要列出任务。任务是你希望特定代理执行的具体工作。我将为每个代理分配一个任务。我将解释任务的描述,并具体说明我希望任务的输出是什么样的。
def extract_job_information(page_url):
return Task(
description=f"Given this url: {page_url}, extract the job description, and relative information about the job",
agent=job_crawler,
expected_output="Key points of the job description, requirements, and qualifications needed for the job",
)
def cv_modifying(cv_path):
return Task(
description=f"Read the CV at this local path: {cv_path}, then\
modify the keypoints and the order of the skills, to make it emphasize what is needded by the job.\
Do NOT add any extra skill or new information, keep it honest.",
agent=cv_modifier,
expected_output="A modified version of CV, tailor-made for the job description",
)
def cover_letter_modifying(cv_path):
return Task(
description=f"Read the cover letter at this local path: {cv_path},\
then baseed on the provided job description by 'job_crawler' agent, \
modify it to make it target the provided job description. Fill in the empty brackets with the company name.\
Do NOT add any extra skill or new information, keep it honest.",
agent=cover_letter_modifier,
expected_output="A modified version of cover letter, tailor-made for the job description",
)
evaluate = Task(
description=f"Provided the modified CV and Cover Letter, and the key points of the job description,\
give a score to the candidate from 0-100, based on how well suited they are for this job",
agent=recruiter,
expected_output="Score in the range [0-100]",
)
请注意,其中一些任务依赖于另一个任务的输出。例如,我需要修改后的简历和求职信来评估它们。而且,除非Job Crawler代理已经成功从网页上提取了职位信息,否则我也无法修改我的简历和求职信。
然而,你必须意识到,最终你是在从外部网站内容中提取关键信息。最佳实践是,在将这些工具用于个人或商业用途时,记得考虑网站的版权/权利。
这一切由 CrewAI 自动处理,确保代理之间共享所需的信息。
剩下的就是创建我的代理团队,并启动它们!
# USER INPUTS
cover_letter_path = r'Cover Letter.pdf'
cv_path = r'CV.pdf'
job_url = [www.job.com]
extract_job_information_task = extract_job_information(job_url)
cv_modifying_task = cv_modifying(cv_path)
cover_letter_modifying_task = cover_letter_modifying(cover_letter_path)
# make the crew
crew = Crew(
agents=[job_crawler, cv_modifier, cover_letter_modifier, recruiter],
tasks=[
extract_job_information_task,
cv_modifying_task,
cover_letter_modifying_task,
evaluate
],
verbose=2
)
# Let's start!
result = crew.kickoff()
这段代码接收用户输入的文件路径和网页地址。然后,我将使用之前定义的代理和任务来创建我的团队,并启动它们!
结果是所有代理的思维过程和结果的长输出。为了简洁起见,我将展示我的Cover Letter Modifier代理的思维过程示例:
> Entering new CrewAgentExecutor chain...
I need to read the content of the cover letter from the provided PDF file to understand its structure and content. This will help modify it according to the job description.
Action: fetch_pdf_content
Action Input: {"pdf_path": "Cover Letter.pdf"}
Dear Hiring Manager,
[Original cover letter, excluded for brevity]
Sincerely,
Thought:
Now I have the content of the original cover letter. I need to tailor this cover letter to specifically address the job opening at Huawei's AI4Sec Research Team. I will adjust the content to highlight relevant experiences and skills that align with the job description provided.
Final Answer:
Dear Hiring Manager,
[Modified cover letter, excluded for brevity]
Sincerely,
你可以看到代理与自己进行思考,识别需要完成的任务,并使用手头的工具执行。
在每个代理完成任务后,我们可以看到它们的输出以及它们的表现如何。

图 7. 模板求职信与代理修改后的求职信对比。(作者提供)
你可以看到,Cover Letter Modifier 代理成功地将我的求职信调整为符合职位描述的要求,甚至添加了一些职位描述中强调的要点。然而,CV Modifier 代理的输出稍显不足,需要对其代理和任务定义进行一些调整。
招聘代理还审阅了我修改后的简历和求职信,并给出了92/100的评分:
Based on the detailed review of Hesam Sheikh Hassani’s CV and
Cover Letter in relation to the key points of the job description
for the AI Researcher position at [Excluded Intentionally], I would score the candidate
a 92 out of 100\. Hesam exhibits a strong academic and professional
background in Artificial Intelligence, particularly in machine learning
and deep learning, which aligns well with the requirements of the
[Excluded Intentionally] Research Team at [Excluded Intentionally].
His experience with Large Language Models (LLMs) and his ability to
communicate complex AI concepts effectively, as demonstrated through
his engagement on social media and articles, are particularly impressive
and directly relevant to the position. His technical proficiency with tools
critical for the role, such as Git, Linux, Python, and advanced
machine learning algorithms, further strengthens his candidacy.
Hesam's previous roles and projects, such as the Agricultural Sorting Machine
at PFK and his work on AI-on-Demand platforms, showcase his leadership skills
and his capability to manage and execute AI projects, which is essential for
the role at [Excluded Intentionally]. His expressed eagerness to contribute to and learn from
[Excluded Intentionally]’s research initiatives, along with his alignment with [Excluded Intentionally]’s core
values of continuous improvement, AI integrity, and inclusiveness, make
him a strong fit culturally and professionally for the position.
His immediate availability and willingness to relocate also add to his
suitability for the role, ensuring a smooth transition and immediate
contribution to the team. Thus, given Hesam Sheikh Hassani’s strong
alignment with both the technical and cultural aspects of the AI
Researcher position at [Excluded Intentionally],
he is highly recommended for this role with a score of 92.
AI 代理修改简历和求职信的示例用例,主要是为了强调我们如何使用 AI 来自动化重复任务的范围,并作为教育示例。如果你想在实践中使用此类工具,最好记住一些限制:
-
AI 生成的简历或求职信可能会被招聘人员视为红旗。招聘经理可能会使用 AI 检测工具来排除此类候选人的申请。
-
像 ChatGPT 这样的 AI 工具仍然陷入了简历语言的陷阱。AI 在简历中的一些写作风格和用词很容易被识别。像“delve”或“spearhead”这样的词可能会让人觉得是 AI 生成的文本。这种写作风格可能会使你的简历显得不真实,缺乏亲和力。
[## 为什么 ChatGPT 这么常用“Delve”?谜底揭晓。
ChatGPT 在回应提示时过度使用“Delve”这个词。这与 ChatGPT 的构建方式有关。
pub.towardsai.net](https://pub.towardsai.net/why-does-chatgpt-use-delve-400621728787?source=post_page-----1f4048947d9c--------------------------------)
- AI 聊天机器人和 LLM 会受到幻觉的影响。在这个例子中,幻觉可能意味着在你的求职信和简历中添加你实际上并不具备的经验和技能,而这些内容可能是受到职位描述的启发。在修改简历/求职信时,请仔细检查是否有任何不现实的改动。
对大规模语言模型(LLMs)能做什么有明确的把握,让公司和专家处于有利地位。一个令人着迷的能力是 AI 代理。在本文中,我们探讨了 AI 代理是什么,它与 LLM 的不同之处,以及如何通过几行代码实现它们以自动化我们的重复任务。
🌟 加入超过 1000 人,学习 Python、机器学习/MLOps/人工智能、数据科学和 LLM。关注我,并查看我的X/Twitter,我每天更新**。
感谢阅读,
— Hesam
《Matplotlib 插入坐标轴的全面指南》
包括 4 种创建方式和 2 种样式设置方式
·发表于Towards Data Science ·8 分钟阅读·2024 年 1 月 30 日
--

带有四个插入坐标轴的示例图
插入坐标轴是一种强大的数据可视化技术,可以突出特定的图形区域或添加详细的子图。它们是有效利用本来空白的图形空间的绝佳方式。
本教程展示了4 种在matplotlib中创建插入坐标轴的方法,这些方法可以让你根据坐标轴、整体图形、绝对单位(如英寸)或使用网格系统定位插入坐标轴——后者在处理多个插入坐标轴时特别有用。
我们还将介绍2 种设置缩放插入图样式的方法:使用经典的引导线和使用颜色编码覆盖层。

带有颜色编码覆盖层的插入坐标轴
在本教程结束时,你将能够确定哪种方法最符合你的需求,并且拥有一些可以复制/粘贴的代码来实现这一目标。
下面是我们将要覆盖的内容。
创建插入坐标轴:
-
预定义的坐标轴级位置代码:
mpl_toolkits.axes_grid1.inset_axes, -
使用坐标轴级坐标手动定位:
Axes.inset_axes,并 -
使用图形级坐标手动定位:
Figure.add_axes; -
多插入自动布局:
Axes.inset_axes与outset.layout_corner_insets。
添加缩放指示器:
5. 引导线:Axes.indicate_inset_zoom 和
6. 颜色编码的覆盖层:OutsetGrid.marqueeplot。
第四部分和第六部分使用了我最近发布的开源Outset库中的新工具,专门用于多尺度数据可视化,我很高兴能与社区分享。
方法 1:使用*mpl_toolkits.axes_grid1.inset_axes`*
这个函数简化了插入的添加。以下是如何使用它的说明,包括loc参数用于定位:
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
fig, ax = plt.subplots(); ax.set_box_aspect(0.5) # main figure and axes
ax.plot([0, 9], [0, 9]) # example plot
# create inset axes & plot on them
inset_ax = inset_axes(ax, width="30%", height=1.2, loc="upper left")
inset_ax.plot([9, 0], [0, 9], color="r")
plt.xticks([]); plt.yticks([]) # strip ticks, which collide w/ main ax
请注意,坐标轴的大小可以相对于父坐标轴指定,或者如这里所示,可以以英寸为单位指定宽度和高度。

loc的支持位置代码如下:
"upper right" | "upper left" | "lower left" | "lower right"
"right" | "center left" | "center right" | "lower center"
"upper center" | "center"
方法 2:使用Axes.inset_axes
Matplotlib 的Axes类提供了inset_axes成员函数,这是一个简单的方法来创建相对于父坐标轴的插入坐标轴:
import matplotlib.pyplot as plt
fig, ax = plt.subplots(); ax.set_box_aspect(0.5) # main figure and axes
ax.plot([0, 9], [0, 9]) # example plot
# create inset axes & plot on them
ins_ax = ax.inset_axes([.6, .15, .3, .3]) # [x, y, width, height] w.r.t. ax
ins_ax.plot([9, 0], [0, 9], color="r")

坐标是相对于父坐标轴指定的,因此——例如——(0, 0, 0.5, 0.2)将会在左下角创建一个坐标轴,宽度占父坐标轴的一半,高度占父坐标轴的 0.2。
要相对于父坐标轴ax以英寸为单位定位插入坐标轴,我们必须首先计算父坐标轴的尺寸(以英寸为单位)。
w_inch, h_inch = ax.figure.get_size_inches() * ax.get_position().size
然后,将你的x、y、w和h(单位为英寸)传递给Axes.inset_axes,如下所示
ax.inset_axes([x/w_inch, y/h_inch, w/w_inch, h/h_inch])
方法 3:使用Figure.add_axes
Matplotlib 的Figure类提供了类似的add_axes成员函数,它允许你相对于整体图形定位插入坐标轴。
import matplotlib.pyplot as plt
fig, ax = plt.subplots(); ax.set_box_aspect(0.5) # main figure and axes
ax.plot([0, 9], [0, 9]) # example plot
# create inset axes & plot on them
ins_ax = fig.add_axes([.2, .5, .2, .2]) # [x, y, width, height] w.r.t. fig
ins_ax.plot([9, 0], [0, 9], color="r")

与之前类似,坐标是相对于父坐标轴指定的,因此——例如——(0.5, 0.5, 0.3, 0.2)将创建一个坐标轴,其高度为整体图形的 2/10,宽度为 3/10,左下角在图形内居中。
方法 4:`Axes.inset_axes ```py with outset.layout_corner_insets ``
For this next example, we will use the outset library, which provides specialized tools for working with inset axes in matplotlib. It can be installed as python3 -m pip install outset.
The outset library provides the flexible outset.util.layout_corner_insets utility to position multiple inset axes within a specified corner of a main axes. Here’s how to use it to pick positions for calls to Axes.inset_axes.
import matplotlib.pyplot as plt
import outset
fig, ax = plt.subplots(); ax.set_box_aspect(0.5) # 主图形和坐标轴
ax.plot([0, 9], [0, 9]) # 示例图形
# ------ 选择插入坐标轴位置:左上角放 3 个,右下角放一个
inset_positions = outset.util.layout_corner_insets( # 左上角位置
3, "NW", # 插入的数量和位置的角落
# 可选的布局调整...
inset_pad_ratio=(.2,.35), inset_grid_size=(.6,.65), inset_margin_size=.05)
inset_positions.append( # 生成右下角位置并添加到列表中
outset.util.layout_corner_insets(1, "SE", inset_grid_size=.4))
# ----- 创建插入坐标轴并在其上绘图
inset_axes = [*map(ax.inset_axes, inset_positions)] # 创建插入坐标轴
对于 inset_axes 中的 iax: # 示例绘图
iax.plot([9, 0], [0, 9], color="r")
```py
Note the optional customizations to inset positioning made through keyword arguments to `outset.util.layout_corner_insets`. Here, “pad” refers to spacing between insets, “margin” refers to space between the insets and the main axes, and “grid size” refers to the overall fraction of axes space that insets are stacked into.

That covers it for techniques to place inset axes!
A common use case for inset axes is to provide magnification of an area of interest within the main plot. Next, we’ll look at two ways to visually correspond magnifying insets with a region of the main plot.
# Method 5: Zoom Indicator Leaders
A classic approach depicting zoom relationships draws connecting lines between the corners of the inset axes and the region it is magnifying. This works especially well when plotting small numbers of insets.
Matplotlib’s `Axes` includes the `indicate_inset_zoom` member function for this purpose. Here’s how to use it.
from math import isclose; import matplotlib.pyplot as plt
设置主图/坐标轴
fig, main_ax = plt.subplots(); main_ax.set_box_aspect(0.5)
inset_ax = main_ax.inset_axes(
[0.05, 0.65, 0.3, 0.3], # 相对于坐标轴的[x, y, 宽度, 高度]
xlim=[4, 5], ylim=[4, 5], # 设置视口并指示与主坐标轴的关系
xticklabels=[], yticklabels=[]
)
添加绘图内容
对于 ax 在 main_ax 和 inset_ax 中:
ax.plot([0, 9], [0, 9]) # 第一个示例线
ax.plot([0, 9], [1, 8]) # 第二个示例线
添加缩放指示器
main_ax.indicate_inset_zoom(inset_ax, edgecolor="blue")
小心!如果插入坐标轴的纵横比与主坐标轴不匹配,请发出警告
if not isclose(inset_ax._get_aspect_ratio(), main_ax._get_aspect_ratio()):
print("选择的嵌套框 x/ylim & 宽度/高度与主坐标轴的比例倾斜!")

Note that to use `Axes.indicate_inset_zoom`, inset axes must be created using `Axes.inset_axes`.
# Method 6: Zoom Indicator Overlays
When working with larger numbers of insets, it may work better to correspond regions with numbered, color-coded highlights instead of leader lines.
The `outset` library’s `OutsetGrid` provides a `marqueeplot` member function for this purpose. Under this scheme, inset positioning is handled by `outset.inset_outsets`. Here’s how to create a zoom plot with color-coded position overlays.
from matplotlib import pyplot as plt
import numpy as np
import outset
示例改编自 matplotlib.org/stable/gallery/
i, a, b, c, d = np.arange(0.0, 2 * np.pi, 0.01), 1, 7, 3, 11
3 个坐标轴网格:源图和两个缩放框
grid = outset.OutsetGrid([ # 缩放部分的数据坐标...
(-10, 8, -8, 9), (-1.6, 5, -0.5, 3)]) # ...如 (x0, y0, x1, y1)
grid.broadcast(plt.plot, # 在所有坐标轴上运行绘图器
# subsequent kwargs go to the plotter
np.sin(i * a) * np.cos(i * b) * 20, # 线条坐标
np.sin(i * c) * np.cos(i * d) * 20, # 线条坐标
c="mediumblue", zorder=-1, # 线条样式
)
将嵌套框放置在源图的左下角("SW")
outset.inset_outsets(grid, insets="SW")
grid.marqueeplot() # 渲染叠加注释

Note that inset positioning can be more finely controlled via `outset.util.layout_corner_insets`, as used for Method 4 above:
... # 如前所示
customized_placements = outset.util.layout_corner_insets(
2, "SW", # 在左下角放置 2 个嵌套框
inset_margin_size=0.05, inset_grid_size=(0.8, 0.55) # 布局参数
)
outset.inset_outsets(grid, insets=customized_placements)
grid.marqueeplot() # 渲染叠加注释

Inset placements can also be manually specified to `outset.inset_outsets`using axes-relative coordinates, too:
... # 如前所示
outset.inset_outsets(grid, insets=[
(0.05, 0.05, 0.25, 0.25), # 两个嵌套框,使用相对于坐标轴的坐标...
(0.7, 0.7, 0.3, 0.3), # ...指定的 (x0, y0, 宽度, 高度)
])
grid.marqueeplot() # 渲染叠加注释

And, finally, to use bigger, side-by-side magnification panels instead of insets, just omit the call to `outset.inset_outsets`.

# Conclusion
Inset axes are a great way to take your *matplotlib* visualizations to the next level, whether you’re looking to highlight specific data regions or add detailed subplots.
Here, we’ve covered the variety of approaches at your disposal, both built in to *matplotlib* and using the *outset* library. Each method offers unique advantages, ensuring a good fit for nearly any inset plotting scenario.
Now go make something informative and beautiful! Happy plotting :)
# Further Information
*matplotlib* has some excellent inset example materials that are well worth checking out. In particular,
* [Zoom region inset axes](https://matplotlib.org/stable/gallery/subplots_axes_and_figures/zoom_inset_axes.html)
* [Inset locator demo](https://matplotlib.org/stable/gallery/axes_grid1/inset_locator_demo.html)
*outset* also provides some good example materials, notably one on [zoom plots over rain guage time series data](https://mmore500.com/outset/example-rain.html). *outset* also provides a useful [quickstart guide](https://mmore500.com/outset/quickstart.html) and [gallery page](https://mmore500.com/outset/gallery.html).

images from the outset gallery
Formal argument-by-argument listings for all code covered here can be found in the API documentation pages for [*outset*](https://mmore500.com/outset/_autosummary/outset.html) and [*matplotlib*](https://matplotlib.org/stable/api/index.html).
Both projects are open source on GitHub, *m*a*tplotlib* under a PSF license at [matplotlib/matplotlib](https://github.com/matplotlib/matplotlib) and *outset* under the MIT License at [mmore500/outset](https://github.com/mmore500/outset) — *outset* is a new project, consider leaving a ⭐️!
Joseph Early also has an excellent medium article on inset axes in matplotlib, which you can read here.
# Authorship
This tutorial is contributed by me, [Matthew Andres Moreno](https://mmore500.com/).

I currently serve as a postdoctoral scholar at the [University of Michigan](https://umich.edu), where my work is supported by the Eric and Wendy Schmidt AI in Science Postdoctoral Fellowship, a Schmidt Futures program.
My appointment is split between the university’s Ecology and Evolutionary Biology Department, the Center for the Study of Complexity, and the Michigan Institute for Data Science.
Find me on Twitter as [@MorenoMatthewA](https://twitter.com/morenomatthewa) and on GitHub as [@mmore500](https://github.com/mmore500/).
*disclosure:* I am the author of the *outset* library.
# Citations
*J. D. Hunter, “Matplotlib: A 2D Graphics Environment”, Computing in Science & Engineering, vol. 9, no. 3, pp. 90–95, 2007\.* [*https://doi.org/10.1109/MCSE.2007.55*](https://doi.org/10.1109/MCSE.2007.55)
*Matthew Andres Moreno. (2023). mmore500/outset. Zenodo.* [*https://doi.org/10.5281/zenodo.10426106*](https://doi.org/10.5281/zenodo.10426106)
# Appendix
To install dependencies for code snippets in this article,
python3 -m pip install \
matplotlib `# ==3.8.2`\
numpy `# ==1.26.2` \
outset `# ==0.1.8`
*所有图像均为作者作品。*
# 混合整数线性规划建模技术的全面指南
> 原文:[`towardsdatascience.com/a-comprehensive-guide-to-modeling-techniques-in-mixed-integer-linear-programming-3e96cc1bc03d?source=collection_archive---------0-----------------------#2024-03-11`](https://towardsdatascience.com/a-comprehensive-guide-to-modeling-techniques-in-mixed-integer-linear-programming-3e96cc1bc03d?source=collection_archive---------0-----------------------#2024-03-11)
## 将想法转化为数学表达式,以解决运筹学问题。
[](https://medium.com/@bruscalia12?source=post_page---byline--3e96cc1bc03d--------------------------------)[](https://towardsdatascience.com/?source=post_page---byline--3e96cc1bc03d--------------------------------) [Bruno Scalia C. F. Leite](https://medium.com/@bruscalia12?source=post_page---byline--3e96cc1bc03d--------------------------------)
·发布于[Towards Data Science](https://towardsdatascience.com/?source=post_page---byline--3e96cc1bc03d--------------------------------) ·13 分钟阅读·2024 年 3 月 11 日
--

摄影:来自[Aaron Lefler](https://unsplash.com/@alefler?utm_source=medium&utm_medium=referral)的照片,来源于[Unsplash](https://unsplash.com/?utm_source=medium&utm_medium=referral)
数值优化是决策科学中定量技术的核心。它包含一系列框架,用于在一些规定的情境下优化一个或多个目标。
优化问题主要分为*线性*和*非线性规划*两大类。这些类别的区分依据是决策变量在目标函数和约束条件中的数学关系如何建立。管理科学和运筹学广泛使用线性模型,而非线性规划问题则往往自然出现在物理科学和工程领域中(Nocedal & Wright,2006)。
数学模型通常需要将离散决策和整数值纳入其中。这些问题特别在线性模型的背景下进行了探索,因为多年来提出了高效的算法。当*线性规划*(LP)包含整数约束时,它被归类为*整数规划*或*混合整数线性规划*(MILP)。幸运的是,我们有多种求解器可以解决这些问题,包括开源的替代方案,如[HiGHS](https://highs.dev/#top)和[SCIP](https://www.scipopt.org/)。
尽管数学模型是对现实的近似表示,但它们在指导决策方面仍然有用。因此,本文旨在阐述如何用数学术语描述在 MILP 中常见的公式和表达式。
接下来的部分将涵盖:
+ 逻辑语句
+ 设立成本和批量大小
+ 条件表达式
+ 离散规划时域
+ 线性化技术
+ 进一步阅读
+ 结论
+ 参考文献
本文将是一个更抽象的数学模型文本,而不是一个代码应用指南。然而,你可能会在我的[GitHub 仓库](https://github.com/bruscalia/optimization-demo-files)中找到一些关于如何解决数值优化模型的实际例子。
在开始之前,请记住,如果你已经熟悉*线性规划*,你可能会更好地理解这篇文章。你可以在我之前的文章中找到该主题的有趣概述。
[](/linear-programming-theory-and-applications-c67600591612?source=post_page-----3e96cc1bc03d--------------------------------) ## 线性规划:理论与应用
### 线性优化的主要概念及其在 Python 中的实现
towardsdatascience.com
现在让我们深入了解!
# 逻辑语句
在整数规划(IP)模型中,逻辑语句可以转化为线性约束,以捕捉决策变量之间的特定条件或关系。让我们首先考虑涉及二进制决策变量的逻辑语句。
## 蕴涵
让我们从“如果*x*则*y*”这一语句开始。它必须确保如果*x*为真(值为 1),则*y*也必须为真。否则,*y*可以是 0 或 1。可以通过一个简单的不等式来描述,即*y*大于或等于*x*。

在整数规划中的蕴涵约束。(图片来源:作者)
## 逻辑非
“not *y*”的表达式必须返回其相反值。所以,当*y*为 1 时,它必须返回 0。否则,它应该返回 1。

在整数规划中的逻辑非。(图片来源:作者)
“not”语句可以与蕴涵约束结合,例如,“如果*x*则 not *y*”。

在整数规划中结合逻辑非的蕴涵约束。(图片来源:作者)
## 逻辑与
为了计算逻辑“与”,我们引入一个变量 *z*,当 *x* 和 *y* 都为真时,*z* 为真。这些规则与描述两个二进制变量之间乘积的规则相同,因为 *x* 乘以 *y* 仅在两者都等于 1 时才等于 1。这在计算连接两个节点的活动边时出现在设施分配问题中。

整数规划中的逻辑与约束。(图片来自作者)。
## 逻辑或
现在,要计算逻辑“或”时,*z* 必须为真,如果 *x* 或 *y* 其中之一为真。我们必须确保它大于或等于它们中的每一个,并且小于它们的和。

整数规划中的逻辑或约束。(图片来自作者)。
# 设置成本和批量大小
接下来,我们引入 *设置成本* 和 *批量大小*,它们类似于隐含约束,但在具有实数值和整数/二进制变量的上下文中。考虑一个非负变量 *x*,并且在 *x* 大于零时应用设置成本。为了数学表示这一点,我们声明 *y* 乘以一个大值 *M* 应该大于或等于 *x*。

混合整数线性规划中的设置成本隐含约束。(图片来自作者)。
如果 *x* 大于零,则不等式仅在 *y* 等于 1 时满足。例如,在[动态批量问题](https://medium.com/towards-data-science/the-dynamic-lot-size-model-a-mixed-integer-programming-approach-4a9440ba124e)中,我们在平衡设置成本和库存持有成本时会遇到这种情况。

为生产一个或多个项目而产生的机器设置成本。(图片来自作者)。
*M* 项 — 通常表示为“大 M” — 应该是 *x* 的自然上界。重要的是要使用最小的可能值来定义它,以确保当 *y* 等于 1 时,约束不会生效。避免使用过大的值可以改善线性松弛,从而对求解器性能产生积极影响,同时避免数值误差。
对于涉及多个整数批次且每个批次有有限容量的场景(例如,向客户提供供应的运输任务),我们可以使用类似的表达式。现在假设 *y* 是一个整数变量,表示执行的批次数量,*Q* 是每个批次的容量。决策变量 *x* 仍然表示一个非负的实数值。我们可以写出相同的方程式来捕捉我们的决策规则。

混合整数线性规划中的批量大小约束。(图片来自作者)。
# 条件表达式 — “如果-那么”规则
现在假设我们希望当二进制变量 *y* 为 1 时,不等式成立,否则不成立。我们可以再次引入一个大值 *M* 来捕捉这种条件。

混合整数线性规划中的条件表达式。(图片来源:作者)
当*y*等于 1 时,乘以*M*的项变为零,原始约束是紧约束。否则,*M*乘以 1,因此它假定一个任意大的值,使得在考虑问题背景下,原始约束对于任何可行的*x*值都是非紧约束。

当*y*等于 1 时必须满足的条件表达式(不等式)。(图片来源:作者)
这些表达式在混合整数规划模型中会经常出现,以下是一些最常见的应用。
## 分支规则 — “如果-那么-否则”
分支约束是一系列连接的“如果-那么”约束。额外的规则确保要么一个满足,要么另一个满足。

混合整数线性规划中的分支表达式。(图片来源:作者)
## 优先级和序列
现在考虑一个决策,确定哪个操作,*i* 还是 *j*,先执行。在一些调度模型中,我们可以为每个操作的开始设置一个实数决策变量*x*,另一个变量或固定参数表示它的持续时间*d*,以及一个二进制决策变量(优先级)*y*,当*i*在*j*之前发生时,*y*为 1。

混合整数线性规划中的优先约束。(图片来源:作者)
这些表达式将在[分支作业车间调度模型](https://medium.com/towards-data-science/the-job-shop-scheduling-problem-mixed-integer-programming-models-4bbee83d16ab)和[旅行商问题](https://doi.org/10.1145/321043.321046)中使用 MTZ 公式(Miller 等,1960)。

10 个作业和 10 台机器的作业车间调度问题的最优解的甘特图。(图片来源:作者)
## 最小-最大和最大-最小目标
这些目标包括最小化一组表达式的最大值或最大化它们的最小值。简单来说,我们的目标是尽可能保持最坏情况的结果良好。
在最小-最大表达式的情况下,我们可以创建一个标量决策变量*z*,并确保当选择相应的项目*i*时,它始终大于或等于一组选项*I*中的属性*d*。

混合整数线性规划中的最小-最大表达式。(图片来源:作者)
在处理最大最小表达式时,现在考虑*z*为小于或等于所有选定项属性的标量。在这两种情况下,我们引入大*M*来确保,如果某个项没有被选择,约束就不再起作用。一个好的*M*选择是* d*(来自整个项集合的最大值)与项*i*的属性之间的差值。

混合整数线性规划中的最大最小表达式。(图像由作者提供)
这些表达式例如出现在考虑使用二进制变量来表示给定弧或边是否激活的[设施分散问题](https://medium.com/towards-data-science/the-facility-dispersion-problem-mixed-integer-programming-models-98ffdb56cd26)中。

在欧几里得实例中最大化任何五个选定点之间的最小距离,属于*p-分散问题*。(图像由作者提供)
# 离散规划时段
一些运筹学问题包括在离散时段内进行规划。在这种情况下,时间线中某一时刻所采取的行动将影响未来决策的执行方式。例如,生产和库存规划通常涉及在持有库存成本、设置成本、库存覆盖率和需求预测等方面之间进行平衡。某一时刻生产的某一产品数量通常是一个需要做出的决策,它应当不仅在生产时影响产品的可用性,而且在未来考虑库存商品的可能性时也会产生影响。
## 库存平衡
为了计算库存平衡,首先定义一个离散规划时段*T*,包含时刻*t*。给定时刻的库存将是一个决策变量*I*,其索引由集合*T*的元素组成。如果我们将某一时刻的输入(例如生产的物品数量)和输出(相应的需求或运输的物品)分别定义为*x*和*d*,那么我们可以通过以下公式计算给定时刻的最终库存:上一个时刻的最终库存加上输入减去输出。

离散规划时段中库存平衡的图形表示。(图像由作者提供)
请记住,输入和输出可能是问题的固定参数,也可能是决策变量。
我们可以将其表示为以下方程。

简单的库存平衡方程。(图像由作者提供)
一个很好的例子来说明这一点是[动态批量大小](https://medium.com/towards-data-science/the-dynamic-lot-size-model-a-mixed-integer-programming-approach-4a9440ba124e)模型。然而,过程可能涉及更复杂的决策,并且此时应在库存平衡方程中包含新的元素。接下来将展示一个示例。
## 带有积压的库存
现在假设我们有每个时刻 *t* 的确定性需求 *d*,但我们可能决定推迟其中一些需求以减少成本(例如设置成本或运输系统中的固定费用)。这些可能会成为库存平衡方程中的*积压*,通常在目标函数中带有一些相关的成本。再一次,假设我们的输入由 *x* 表示,但现在我们还将包含一个非负的决策变量来表示积压 *b*,它也由 *t* 索引。新的库存平衡方程变为以下形式。

带有积压的库存平衡。(图片由作者提供)。
然后由优化求解器来定义何时应该推迟需求以减少总体成本。
## 开始和结束
一些规划过程包括在某个时刻开始的活动,这些活动可能会持续超过一个时刻在离散规划范围内。当将这些决策插入到优化模型时,通常是模型来定义活动何时开始,何时结束,以及可能的持续时间。因此,我们必须包含一些二元决策变量来表示这些决策。
为了更好地理解建模表达式,我们可以直观地表示一个活动,它在某个时刻开始,在规划范围内的若干时刻处于活动状态,然后结束。

在离散规划范围内调度的活动。(图片由作者提供)。
注意,活动开始于其处于活动状态的时刻,但它必须在前一个时刻处于非活动状态。相反,活动结束时,前一个时刻没有规则要求,但它必须在接下来的时刻处于非活动状态。
为了将其写成数学表达式,我们使用三组决策变量——它们都由规划范围 *T* 中的元素索引。变量 *x* 用于表示活动处于活动状态的时刻,*y* 用于表示活动的开始,*z* 用于表示活动的结束。
为了获得紧凑的线性松弛并帮助求解器在分支定界过程中,三组约束将被创建以建立 *x* 和 *y* 之间的关系,以及 *x* 和 *z* 之间的关系。
为了识别开始:

一组约束,用于识别活动在离散规划范围内的开始。(图片由作者提供)。
并且为了识别结束:

一组约束,用于识别活动在离散规划范围内的结束。(图片由作者提供)。
为了确保在这些时刻中,*x*、*y* 和 *z* 之间的期望关系,可能会在第一和最后时刻加入额外的隐含约束。
# 线性化技术
一些问题包括无法直接纳入 MILP 模型的非线性表达式。幸运的是,有一些策略利用二进制变量使我们能够线性化某些非线性表达式。接下来将介绍两个常见的应用。
## 实值与二进制变量的乘积
现在,假设我们有一个连续的决策变量*x*,其值在可行范围*L*和*U*之间。假设还有一个二进制决策变量*y*和一个支持决策变量*z*,它应该计算*x*和*y*之间的乘积。当*y*为 1 时,*z*应该取*x*的值,否则为 0。

如果*y*为 1,*z*必须大于或等于*x*(及其自然下限),并且小于或等于*x*(及其自然上限)。因此,满足这些不等式的唯一方法是,*z*等于*x*。
现在,如果*y*为 0,最后两个不等式确保*z*等于 0,而前两个不等式确保任何在自然界限内的*x*值都是可行的。
因此,利用这些不等式,可以将二进制和实值决策变量之间的乘积线性化。
## 分段线性近似
现在假设我们要计算在分段线性区间中定义的函数值。这可以是一般非线性表达式的近似,或者仅仅是计算分段线性表达式的一种策略,例如在具有不同单位成本水平的合同中,成本水平根据交易量的变化而变化。下面的图示通过 5 个线性区间近似 Sigmoid 函数来展示这个思想。

Sigmoid 函数及其通过 5 个区间的分段线性近似。(图片来自作者)
考虑一个通用函数*f*(*x*),定义在一个实值变量*x*的给定区间[l, u]上。现在,假设该函数在*N*个任意大小的区间上进行了离散化。每个索引为*i*的区间位于插值点*x*的索引*i*和*i*+1 之间,且总共有*N*+1 个插值点。我们现在加入参数来定义每个区间的线性方程:
+ *aᵢ*:第*i*个线性区间的斜率
+ *bᵢ*:第*i*个线性区间的截距
我们的目标是让决策变量*y*取值为*f*(*x*)。为此,我们还引入了辅助决策变量,索引为*i*,其中*i*属于{1, 2, …, *N*}:
+ *zᵢ*:一个二进制决策变量,用于指示*x*是否位于第*i*个线性区间内(闭区间)。
+ *sᵢ*:一个实值决策变量,如果*x*位于第*i*个线性区间内(闭区间),则它取*x*的值。
变量*x*应该位于所创建的一个区间内,因此必须有一个约束确保恰好有一个*z*变量是活动的。

约束确保在分段线性函数中有一个分段处于激活状态。(图片由作者提供)
现在我们必须确保当 *zᵢ* 等于 1 时,*sᵢ* 等于 *x*,并且基于 *sᵢ* 的值和每个分段对应的参数计算 *y*。

分段线性函数约束。(图片由作者提供)
因此,使用这些表达式,可以计算通用分段线性函数 *f*(*x*) 对应于实值决策变量 *y* 的值,以便混合整数线性规划求解器能够理解。
# 进一步阅读
大多数 MILP 求解器核心算法被称为*分支定界*。它通过高效地探索离散决策的根搜索树,避免了枚举所有可能性。你可以在我之前的文章中找到关于这个主题的详细介绍。
[](/a-gentle-introduction-to-branch-bound-d00a4ee1cad?source=post_page-----3e96cc1bc03d--------------------------------) ## 分支定界的简明介绍
### 解释最基本的整数与混合整数规划算法,使用 Python 实现
[towardsdatascience.com
一些模型可能会采用其他技术,例如*延迟列生成*,这种方法旨在解决决策变量过多,无法一开始就显式枚举的模型。关于这一主题的介绍和代码示例可以在之前的文章中找到。
[](/column-generation-in-linear-programming-and-the-cutting-stock-problem-3c697caf4e2b?source=post_page-----3e96cc1bc03d--------------------------------) ## 线性规划中的列生成与切割库存问题
### 如何使用 Python 示例解决具有大量决策变量的线性问题
[towardsdatascience.com
现在,参考传统书籍,感兴趣的读者可以参考*《数值优化》*(Nocedal & Wright, 2006)和*《线性与非线性规划》*(Luenberger & Ye, 2008),以深入理解连续域上的优化算法——这些算法通常是整数规划中求解的子模型的组成部分。*《整数规划》*(Wolsey, 2020)一书常被称为该领域的“圣经”。这是一本极好的参考书,能够帮助更好地理解如何设计离散搜索空间上的精确算法,深入探讨*分支定界*、割平面等内容。*《运筹学:应用与算法》*(Winston & Goldberg, 2004)是另一本很好的替代书籍,对于那些渴望看到更多实际应用示例的读者,它也提供了很好的理论概述。
# 结论
混合整数线性规划(MILP)是数值优化中的一个相关领域,具有重要的应用,特别是在管理科学和运筹学中。本文介绍了 MILP 的关键表达式,包括逻辑语句、设置成本、条件表达式、离散规划时间范围和线性化技术。通过结合这些元素,读者应该能够将现实世界中的问题转化为可以通过 MILP 求解器解决的优化模型。
# 参考文献
Luenberger, D. G. & Ye, Y., 2008. *线性与非线性规划.* 第 3 版. 斯坦福:施普林格。
Nocedal, J. & Wright, S. J., 2006. *数值优化.* 第 2 版. 纽约:施普林格·纽约。
Winston, W. L. & Goldberg, J. B., 2004. *运筹学:应用与算法.* 第 4 版. 加利福尼亚州贝尔蒙特:汤姆森·布鲁克斯/科尔贝尔蒙特。
Wolsey, L. A., 2020. *整数规划.* 第 2 版. 约翰·威利与 sons 出版公司。
# A/B 测试的全面引导项目(带有笔记本)
> 原文:[`towardsdatascience.com/a-comprehensive-guided-project-to-a-b-testing-notebook-91e5a300966c?source=collection_archive---------5-----------------------#2024-12-19`](https://towardsdatascience.com/a-comprehensive-guided-project-to-a-b-testing-notebook-91e5a300966c?source=collection_archive---------5-----------------------#2024-12-19)
## 从大学时期统计学的失败到对假设检验的热情
[](https://medium.com/@panData?source=post_page---byline--91e5a300966c--------------------------------)[](https://towardsdatascience.com/?source=post_page---byline--91e5a300966c--------------------------------) [Leo Anello 💡](https://medium.com/@panData?source=post_page---byline--91e5a300966c--------------------------------)
·发布于 [Towards Data Science](https://towardsdatascience.com/?source=post_page---byline--91e5a300966c--------------------------------) ·23 分钟阅读·2024 年 12 月 19 日
--

使用**Dall-E**进行 A 与 B 的比较
# 概述
本项目聚焦于**地理空间营销分析中的 A/B 测试**,这是一种非常有价值的方法,尤其适用于在线销售数字产品的公司。
让我们设想以下情景:你的公司推出了一项面向两个不同地理区域的营销活动,比如**加利福尼亚**和**纽约**。
同一营销活动已发送至两个地区,关键问题是:**转换率是否因地区而异?**
为了回答这个问题,我们将应用**A/B 测试**,这是一种统计方法。通常,**参数检验**更为常见,因为它们具有强大和精确的特性。
说到个人经历,我在大学时两次没通过**推断统计学**,这——**具有讽刺意味的是**——激发了我对其优雅和强大功能的兴趣。
对于细节的描述,我深感抱歉,尽管这些细节不完全是数学性的。当涉及到统计学时,我只会按照规则和框架来操作。
# 市场数据工程的全面介绍
> 原文:[`towardsdatascience.com/a-comprehensive-introduction-to-marketing-data-engineering-f292a4a6568b?source=collection_archive---------12-----------------------#2024-08-30`](https://towardsdatascience.com/a-comprehensive-introduction-to-marketing-data-engineering-f292a4a6568b?source=collection_archive---------12-----------------------#2024-08-30)
## 数据工程
## 基础知识、职责和挑战
[](https://tamimi-naser.medium.com/?source=post_page---byline--f292a4a6568b--------------------------------)[](https://towardsdatascience.com/?source=post_page---byline--f292a4a6568b--------------------------------) [Naser Tamimi](https://tamimi-naser.medium.com/?source=post_page---byline--f292a4a6568b--------------------------------)
·发布于 [Towards Data Science](https://towardsdatascience.com/?source=post_page---byline--f292a4a6568b--------------------------------) ·16 分钟阅读·2024 年 8 月 30 日
--

图片由作者生成。
在今天的数字化环境中,市场营销越来越依赖于数据。与传统市场营销(例如:电视、广播、广告牌、标志和印刷品)不同,传统营销渠道、策略和广告的影响通常难以衡量,而数字营销使我们能够精确地衡量这些影响。事实上,数字营销之所以成为一种更有效的方式,不仅是因为在线的庞大受众,还因为它使我们能够更准确地评估市场营销效果,并更有效地调整目标定位。
因此,市场营销团队需要强大且可扩展的数据基础设施,以便从多个来源或渠道获取、处理和分析数据。当然,这就是市场数据工程成为一个重要话题的原因。市场数据工程是现代数字营销的支柱。在这一特定领域,数据工程产品的质量使得基于数据的决策成为可能,这些决策能够显著影响公司目标的实现。
虽然市场数据工程是一个庞大的领域,无法在一篇文章中完全涵盖,但我旨在提供一个简洁的介绍,涵盖一些重要的内容…
# 自动驾驶感知工程师规划速成课程
> 原文:[`towardsdatascience.com/a-crash-course-of-planning-for-perception-engineers-in-autonomous-driving-ede324d78717?source=collection_archive---------1-----------------------#2024-06-30`](https://towardsdatascience.com/a-crash-course-of-planning-for-perception-engineers-in-autonomous-driving-ede324d78717?source=collection_archive---------1-----------------------#2024-06-30)
## 规划与决策的基础
[](https://medium.com/@patrickllgc?source=post_page---byline--ede324d78717--------------------------------)[](https://towardsdatascience.com/?source=post_page---byline--ede324d78717--------------------------------) [Patrick Langechuan Liu](https://medium.com/@patrickllgc?source=post_page---byline--ede324d78717--------------------------------)
·发表于 [Towards Data Science](https://towardsdatascience.com/?source=post_page---byline--ede324d78717--------------------------------) ·44 分钟阅读·2024 年 6 月 30 日
--

AlphaGo、ChatGPT 和 FSD(图片来源:[Elena Popova](https://unsplash.com/@elenapopova?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash)、[Karthik Sridasyam](https://unsplash.com/@karthik1324?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash) 和 [Jonathan Kemper](https://unsplash.com/@jupp?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash) 于[Unsplash](https://unsplash.com/photos/a-computer-screen-with-a-text-description-on-it-5yuRImxKOcU?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash))
经典的模块化自动驾驶系统通常由感知、预测、规划和控制组成。直到大约 2023 年,AI(人工智能)或 ML(机器学习)主要增强了大多数量产自动驾驶系统中的**感知**功能,其在下游组件中的影响逐渐减弱。与规划堆栈中 AI 集成较低形成鲜明对比的是,端到端感知系统(例如[BEV 或鸟瞰图感知管道](https://medium.com/towards-data-science/monocular-bev-perception-with-transformers-in-autonomous-driving-c41e4a893944?sk=b7498a375cf92533e8242b1837f29af4))已经在[量产汽车](https://medium.com/towards-data-science/bev-perception-in-mass-production-autonomous-driving-c6e3f1e46ae0?sk=8963783161435815fa1b0957fd325d39)中部署。

2023 年及之前的经典模块化自动驾驶堆栈设计(图表由作者制作)
这是有多个原因的。基于人工构建框架的经典栈比基于机器学习的特征更具可解释性,并且能够更快地进行迭代,以解决现场测试中的问题(几个小时内解决),而基于机器学习的特征可能需要几天或几周的时间才能完成。然而,让现成的人工驾驶数据闲置是没有意义的。此外,增加计算能力比扩展工程团队更具可扩展性。
幸运的是,学术界和工业界都有一个强烈的趋势,正在改变这种情况。首先,下游模块正变得越来越数据驱动,并且可能通过不同的接口进行集成,比如在[CVPR 2023 最佳论文 UniAD](https://arxiv.org/abs/2212.10156)中提出的接口。此外,受到日益增长的生成式 AI 浪潮的推动,一个单一的统一视觉-语言-动作(VLA)模型在处理复杂的机器人任务中显示出巨大的潜力(学术界的[RT-2](https://robotics-transformer2.github.io/assets/rt2.pdf),工业界的 TeslaBot 和 1X)以及自动驾驶(学术界的[GAIA-1](https://arxiv.org/abs/2309.17080)、[DriveVLM](https://arxiv.org/abs/2402.12289),工业界的 Wayve AI driver 和 Tesla FSD)。这将 AI 和数据驱动开发的工具集从感知层带入了规划层。
这篇博客文章旨在介绍规划栈的问题设置、现有方法以及挑战,以感知工程师的速成课程形式进行呈现。作为一名感知工程师,过去几周我终于有时间系统地学习经典的规划栈,并且我想分享我所学到的内容。我还会从 AI 实践者的角度分享我的思考,探讨 AI 如何提供帮助。
本文的目标读者是从事自动驾驶领域的 AI 实践者,特别是感知工程师。
这篇文章有点长(11100 字),下面的目录将最有可能帮助那些想要快速使用关键词进行 ctrl+F 搜索的人。
中文版本可以在[知乎知乎](https://zhuanlan.zhihu.com/p/706193528)上找到。
```py
**Table of Contents (ToC)**
Why learn planning?
What is planning?
The problem formulation
The Glossary of Planning
Behavior Planning
Frenet vs Cartesian systems
Classical tools-the troika of planning
Searching
Sampling
Optimization
Industry practices of planning
Path-speed decoupled planning
Joint spatiotemporal planning
Decision making
What and why?
MDP and POMDP
Value iteration and Policy iteration
AlphaGo and MCTS-when nets meet trees
MPDM (and successors) in autonomous driving
Industry practices of decision making
Trees
No trees
Self-Reflections
Why NN in planning?
What about e2e NN planners?
Can we do without prediction?
Can we do with just nets but no trees?
Can we use LLMs to make decisions?
The trend of evolution
为什么要学习规划?
这引出了一个有趣的问题:为什么在 AI 时代,尤其是经典的规划栈值得学习?
从问题解决的角度来看,更好地理解客户的挑战,将使你作为一名感知工程师能够更有效地为下游客户服务,即使你的主要工作仍然集中在感知领域。
机器学习是一个工具,而不是一个解决方案。解决问题的最有效方法是将新工具与领域知识结合起来,特别是那些具有扎实数学公式的领域知识。受领域知识启发的学习方法可能会更具数据效率。随着规划从基于规则的系统向基于机器学习的系统过渡,即使端到端系统的早期原型和产品已经上路,仍然需要能够深入理解规划和机器学习基础的工程师。尽管发生了这些变化,传统方法和学习方法可能会在相当长的一段时间内继续共存,比例可能会从 8:2 转变为 2:8。对于在这一领域工作的工程师来说,理解这两个世界几乎是必不可少的。
从价值驱动的开发角度来看,理解传统方法的局限性至关重要。这个洞察力使你能够有效地利用新的机器学习工具来设计一个解决当前问题并带来即时影响的系统。
此外,规划是所有自主智能体中的关键部分,不仅仅是自动驾驶。理解什么是规划以及它如何运作,将使更多的机器学习人才参与到这一激动人心的话题中,推动真正自主智能体的发展,无论是汽车还是其他形式的自动化。
什么是规划?
问题表述
作为自动驾驶车辆的“大脑”,规划系统对于车辆的安全高效驾驶至关重要。规划者的目标是生成既安全、舒适,又能高效推进到目标的轨迹。换句话说,安全、舒适和效率是规划的三个关键目标。
作为规划系统的输入,所有感知输出都是必需的,包括静态路面结构、动态道路代理、占据网络生成的空闲空间和交通等待情况。规划系统还必须通过监控加速度和加速度变化来确保车辆的舒适性,以实现平滑的轨迹,同时考虑互动和交通礼让。
规划系统生成的轨迹是以一系列航路点的形式表示的,供自车的低层控制器跟踪。具体来说,这些航路点代表了自车在一系列固定时间戳下的未来位置。例如,每个航路点可能相隔 0.4 秒,覆盖 8 秒的规划范围,总共生成 20 个航路点。
一个经典的规划堆栈大致由全局路线规划、局部行为规划和局部轨迹规划组成。全局路线规划提供从起点到终点的路面级路径,该路径基于全局地图。局部行为规划决定接下来几秒钟内的语义驾驶动作类型(例如,跟车、轻推、侧方超车、让行和超车)。基于行为规划模块决定的行为类型,局部轨迹规划生成短期轨迹。全局路线规划通常由地图服务提供,一旦设置了导航,它就不在本文讨论的范围内。接下来,我们将重点讨论行为规划和轨迹规划。
行为规划和轨迹生成可以明确地协同工作,也可以合并成一个单一过程。在显式方法中,行为规划和轨迹生成是两个不同的过程,它们在一个层次结构框架内运行,工作频率不同,其中行为规划的频率为 1–5 Hz,轨迹规划的频率为 10–20 Hz。尽管大多数时候效率很高,但适应不同场景可能需要进行重大修改和微调。更高级的规划系统将两者合并成一个单一的优化问题。这种方法确保了可行性和最优性,不会做出任何妥协。

规划设计方法的分类(来源:Fluid Dynamics Planner)
规划术语表
你可能已经注意到,上述部分中使用的术语与图像并不完全匹配。没有统一的标准术语被广泛采用。在学术界和工业界,工程师使用不同的名称来指代同一概念,或用相同的名称指代不同的概念,这并不罕见。这表明自动驾驶中的规划仍在积极发展中,并未完全统一。
在这里,我列出了本文中使用的符号,并简要解释了文献中出现的其他概念。
-
规划:一个顶级概念,与控制平行,用于生成轨迹的关键点。规划与控制一起被统称为 PnC(规划与控制)。
-
控制:一个顶级概念,接收轨迹关键点并生成高频率的转向、油门和刹车命令,供执行器执行。与其他领域相比,控制相对成熟,尽管 PnC 的常见观点存在,但它超出了本文的范围。
-
预测:一个顶级概念,用于预测除自车外的交通参与者的未来轨迹。预测可以看作是其他交通参与者的轻量级规划器,也称为运动预测。
-
行为规划:一个生成高级语义动作(如车道变换、超车)的模块,通常会生成粗略的轨迹。它也被称为任务规划或决策制定,特别是在交互的上下文中。
-
运动规划:一个模块,接收语义动作,并生成平滑、可行的轨迹航点,供控制在规划时域内执行。它也被称为轨迹规划。
-
轨迹规划:运动规划的另一种说法。
-
决策制定:专注于交互的行为规划。如果没有自车与其他主体的交互,则仅称为行为规划。它也被称为战术决策制定。
-
路径规划:在道路网络上找到首选路线,也称为任务规划。
-
基于模型的方法:在规划中,指的是手工构建的框架,这些框架用于经典的规划栈,与神经网络模型相对。基于模型的方法与基于学习的方法形成对比。
-
多模态:在规划的上下文中,通常指的是多个意图。这与感知中的多模态传感器输入或多模态大型语言模型(如 VLM 或 VLA)中的多模态有所不同。
-
参考线:基于全局路线信息和自车当前状态的局部(几百米范围内)且粗略的路径。
-
弗涅坐标:基于参考线的坐标系统。弗涅坐标将笛卡尔坐标系中的曲线路径简化为一条直线隧道模型。以下会有更详细的介绍。
-
轨迹:三维时空曲线,表示为笛卡尔坐标中的(x, y, t)或弗涅坐标中的(s, l, t)。轨迹由路径和速度组成。
-
路径:二维空间曲线,表示为笛卡尔坐标中的(x, y)或弗涅坐标中的(s, l)。
-
语义动作:一种高级抽象的动作(如跟车、轻推、侧超车、让行、超车),具有明确的人类意图。也被称为意图、策略、机动或原始运动。
-
动作:一个没有固定含义的术语。它可以指代控制的输出(执行器所执行的高频率转向、油门和刹车指令),也可以指代规划的输出(轨迹的航点)。语义动作指的是行为预测的输出。
不同的文献可能使用不同的符号和概念。以下是一些示例:
-
决策系统:有时也包括规划和控制。(来源:自动驾驶城市车辆运动规划与控制技术综述,以及BEVGPT)
-
运动规划:有时是顶层的规划概念,包含行为规划和轨迹规划。(来源:基于流体动力学的通用运动规划方法应用于自动驾驶车辆)
-
规划:有时包括行为规划、运动规划以及路线规划。
这些变化展示了术语的多样性以及该领域不断发展的特点。
行为规划
作为一名机器学习工程师,你可能会注意到行为规划模块是一个 heavily 依赖人工设计的中间模块。对于其输出的具体形式和内容并没有共识。具体来说,行为规划的输出可以是参考路径或在自我机动过程中对目标物体的标注(例如,从左侧或右侧超车,超车或让行)。术语“语义动作”并没有严格的定义,也没有固定的方法。
行为规划与运动规划的解耦提高了在解决自动驾驶车辆极高维度动作空间时的效率。自动驾驶车辆的行为需要在通常为 10Hz 或更高的频率下推理(时间分辨率以路径点为单位),且大多数动作相对简单,如直行。解耦后,行为规划层只需以相对粗糙的分辨率推理未来场景,而运动规划层则基于行为规划做出的决策,在局部解空间中进行操作。行为规划的另一个好处是将非凸优化问题转化为凸优化问题,下面我们将进一步讨论这一点。
弗雷内坐标系与笛卡尔坐标系的对比
弗雷内坐标系是一种广泛采用的坐标系统,值得单独设立一个介绍部分。弗雷内框架通过独立地管理相对于参考路径的横向和纵向运动,简化了轨迹规划。sss 坐标表示纵向位移(沿道路的距离),而 lll(或 ddd)坐标表示横向位移(相对于参考路径的侧向位置)。
弗雷内坐标系将笛卡尔坐标系中的曲线路径简化为一个直线隧道模型。这种转换将曲线路段上的非线性道路边界约束转化为线性约束,从而显著简化后续的优化问题。此外,人类对纵向和横向运动的感知不同,弗雷内坐标系允许对这些运动进行独立且更灵活的优化。

从笛卡尔坐标系到弗雷内坐标系的转换示意图(来源:笛卡尔规划器)
弗雷内坐标系需要清晰、结构化的道路图,且道路曲率较小。实际应用中,更适合用于曲率较小的结构化道路,如高速公路或城市快速路。然而,当参考线的曲率增加时,弗雷内坐标系的问题会被放大,因此在曲率较大的结构化道路上,如带导向线的城市交叉口,使用时应谨慎。
对于没有结构的道路,如港口、矿区、停车场或没有指引线的交叉口,推荐使用更灵活的笛卡尔坐标系统。笛卡尔系统更适合这些环境,因为它能更有效地处理更高的曲率和更少结构化的场景。
经典工具——规划的三驾马车
自动驾驶中的规划涉及从初始的高维状态(包括位置、时间、速度、加速度和震动)计算一条轨迹到目标子空间,确保满足所有约束条件。搜索、采样和优化是规划中最广泛使用的三种工具。
搜索
经典的图搜索方法在规划中很受欢迎,并且广泛应用于结构化道路上的路线/任务规划,或直接用于无结构环境(如停车场或城市交叉口,尤其是无地图场景)的运动规划,寻找最佳路径。其发展路径明确,从 Dijkstra 算法到 A(A-star),再到混合 A。
Dijkstra 算法通过探索所有可能的路径来找到最短路径,使其成为一种盲目(无信息)的搜索算法。它是一种系统的方法,保证最优路径,但部署时效率较低。如下面的图表所示,它几乎会探索所有方向。本质上,Dijkstra 算法是加权的广度优先搜索(BFS)。为了提高效率,我们可以利用目标位置的信息来缩小搜索空间。

Dijkstra 算法和 A* 搜索的可视化(来源:PathFinding.js,示例灵感来自于 RedBlobGames)
A* 算法使用启发式方法优先考虑看似更接近目标的路径,从而提高效率。它将当前的成本(Dijkstra)与到达目标的成本(启发式,本质上是贪婪的最佳优先搜索)结合起来。只有当启发式方法是可接受且一致的时,A* 才能保证最短路径。如果启发式方法不佳,A* 的性能可能会比 Dijkstra 基准差,甚至可能退化为贪婪最佳优先搜索。
在自动驾驶的具体应用中,混合 A* 算法通过考虑车辆运动学进一步改进了 A。A 可能无法满足运动学约束,也无法准确跟踪路径(例如,转向角度通常在 40 度以内)。尽管 A* 在网格空间中对状态和动作都进行操作,混合 A* 将它们分开,保持状态在网格中,但根据运动学允许连续的动作。
解析扩展(射击到目标)是混合 A* 提出的另一个关键创新。A* 的一种自然增强方法是将最近探索的节点与目标连接,使用不碰撞的直线。如果可能的话,我们就找到了这个解。在混合 A* 中,这条直线被替换为符合车辆动力学的杜宾曲线和里德-谢普(RS)曲线。这种提前停止方法通过更多关注远端的可行性,平衡了最优性和可行性。
混合 A* 在停车场场景和无地图的城市交叉口中被广泛使用。这里有一个非常好的视频展示了它如何在停车场场景中工作。

带有解析扩展的混合 A* 算法(来源:2010 IJRR 混合 A* 论文 和 2012 Udacity 课程)
采样
另一个流行的规划方法是采样。著名的蒙特卡罗方法是一种随机采样方法。本质上,采样涉及随机或根据先验选择许多候选项,然后根据定义的成本选择最优的那个。对于基于采样的方法,快速评估许多选项至关重要,因为它直接影响自动驾驶系统的实时性能。
大型语言模型(LLMs)本质上提供样本,并且需要一个评估器,其定义的成本与人类偏好对齐。这个评估过程确保所选择的输出符合期望的标准和质量。这一过程基本上就是所谓的对齐。
如果我们已经知道给定问题或子问题的解析解,则可以在参数化的解空间中进行采样。例如,通常我们希望最小化冲击的平方的时间积分(位置 p(t) 的三阶导数,表示为 p 上的三点符号,其中一个点代表相对于时间的一个阶导数),这是在其他标准下进行的。

最小化平方冲击以提高驾驶舒适性(来源:Werling 等人,ICRA 2010)
可以通过数学证明,五次(5 阶)多项式在位置-速度-加速度空间中提供了两种状态之间的冲击最优连接,即使在考虑额外的成本项时也是如此。通过在这个五次多项式的参数空间中进行采样,我们可以找到一个具有最小成本的解,从而获得近似解。成本考虑了诸如速度、加速度、冲击限制和碰撞检查等因素。这种方法本质上通过采样来解决优化问题。

横向运动时间配置文件的采样(来源:Werling 等人,ICRA 2010)
基于采样的方法启发了众多机器学习论文,包括 CoverNet、Lift-Splat-Shoot、NMP 和 MP3。这些方法用大量数据库中的人类驾驶行为取代了数学上合理的五次多项式。轨迹的评估可以轻松并行化,这进一步支持了基于采样的方法。该方法有效地利用了大量专家示范,模仿了类似人类的驾驶行为,同时避免了对加速度和转向配置文件的随机采样。

从人类驾驶数据中进行采样以支持数据驱动的规划方法(来源:NMP,CoverNet 和 Lift-splat-shoot)
优化
优化通过在给定约束下最大化或最小化特定的目标函数来找到问题的最佳解决方案。在神经网络训练中,采用类似的原理,使用梯度下降和反向传播调整网络的权重。然而,在神经网络之外的优化任务中,模型通常较为简单,且常常采用比梯度下降更有效的方法。例如,虽然梯度下降可以应用于二次规划,但它通常不是最有效的方法。
在自动驾驶中,优化规划成本通常会考虑动态物体以避免障碍物,静态道路结构以跟随车道,导航信息以确保正确路线,以及自车状态以评估平顺性。
优化可以分为凸优化和非凸优化两类。其主要区别在于,在凸优化场景中,只有一个全局最优解,而这个解也是局部最优解。这一特性使得优化问题不受初始解的影响。对于非凸优化,初始解非常重要,如下图所示。

凸优化与非凸优化(来源:斯坦福课程资料)
由于规划涉及高度非凸优化,且存在多个局部最优解,因此它严重依赖于初始解。此外,凸优化通常运行得更快,因此更适用于诸如自动驾驶等车载实时应用。一个典型的方法是将凸优化与其他方法结合使用,首先勾画出一个凸的解空间。这就是将行为规划与运动规划分开的数学基础,在这个过程中,找到一个良好的初始解是行为规划的职责。
以避障为例,它通常会引入非凸问题。如果我们知道推动方向,那么这就变成了一个凸优化问题,障碍物的位置作为约束条件成为优化问题的下界或上界。如果我们不知道推动方向,我们需要先决定哪个方向来推动,这使得问题变成了一个由运动规划来解决的凸问题。这个推动方向的决定属于行为规划范畴。
当然,我们也可以通过直接优化非凸优化问题来解决,比如使用投影梯度下降、交替最小化、粒子群优化(PSO)和遗传算法等工具。然而,这超出了本文的讨论范围。

凸路径规划问题与非凸路径规划问题(作者制作的图表)

凸与非凸路径规划问题的解决过程(作者制作的图表)
我们如何做出这样的决策呢?我们可以使用前述的搜索或采样方法来解决非凸问题。基于采样的方法通过在参数空间中分散多个选项,类似于搜索,有效地处理非凸问题。
你也许会质疑,为什么仅仅决定从哪个方向推动就足够保证问题空间是凸的。为了说明这一点,我们需要讨论拓扑学。在路径空间中,相似的可行路径可以在没有障碍物干扰的情况下连续地互相转化。这些相似路径在拓扑学的正式语言中被归为“同伦类”,它们都可以通过一个初始解来探索,这个初始解与这些路径是同伦的。所有这些路径共同形成了一个驾驶通道,如上图中的红色或绿色阴影区域所示。对于三维时空情况,请参考QCraft 技术博客。
我们可以利用广义 Voronoi 图来列举所有同伦类,这大致对应于我们可用的不同决策路径。然而,这个话题涉及到高级数学概念,超出了本文的范围。
高效解决优化问题的关键在于优化求解器的能力。通常,求解器需要大约 10 毫秒来规划一条轨迹。如果我们能够将这一效率提高十倍,将对算法设计产生重大影响。这个改进在 2022 年特斯拉 AI 日中得到了突出展示。类似的改进也出现在感知系统中,随着可用计算能力提升十倍,从 2D 感知转向了鸟瞰图(BEV)。通过更高效的优化器,可以计算和评估更多选项,从而减少决策过程的重要性。然而,设计一个高效的优化求解器需要大量的工程资源。
每次计算能力提升 10 倍时,算法将进化到下一代。
— — 未验证的算法进化定律
规划的行业实践
各种规划系统的关键区别在于它们是否是时空解耦的。具体来说,时空解耦方法首先在空间维度上进行规划以生成路径,然后在该路径上规划速度轮廓。这种方法也被称为路径-速度解耦。
路径-速度解耦通常被称为横向-纵向(lat-long)解耦,其中横向(lat)规划对应路径规划,纵向(long)规划对应速度规划。这一术语似乎源自前面解释的弗雷内坐标系。
解耦方案更易于实现,能够解决大约 95%的问题。相比之下,联合方案具有更高的理论性能上限,但实现起来更具挑战性。它们涉及更多的参数调节,并且需要更有原则的参数调节方法。

解耦与联合规划的比较(来源:由作者制作,灵感来源于Qcraft)

解耦与联合时空规划的优缺点(图表由作者制作)
路径-速度解耦规划
我们可以以Baidu Apollo EM 规划器为例,展示使用路径-速度解耦规划的系统。
EM 规划器通过将三维的站点-横向-速度问题转化为两个二维问题:站点-横向和站点-速度,从而显著降低了计算复杂度。Apollo 的 EM 规划器的核心是一个迭代的期望最大化(EM)步骤,由路径优化和速度优化组成。每个步骤被分为 E 步骤(在二维状态空间中的投影和公式化)和 M 步骤(在二维状态空间中的优化)。E 步骤涉及将三维问题投影到弗雷内 SL 框架或 ST 速度跟踪框架中。

Apollo EM 规划器中的 EM 迭代(来源:百度 Apollo EM 规划器)
在路径和速度优化中的 M 步(最大化步骤)涉及解决非凸优化问题。对于路径优化,这意味着决定是否将物体推向左侧或右侧;而对于速度优化,则涉及决定是超车还是对动态物体让行。Apollo EM 规划器通过两步法解决这些非凸优化挑战:动态规划(DP)和二次规划(QP)。
动态规划(DP)使用采样或搜索算法生成粗略的初步解,有效地将非凸空间修剪成凸空间。然后,二次规划(QP)将粗略的 DP 结果作为输入,在 DP 提供的凸空间内进行优化。本质上,DP 关注可行性,而 QP 则在凸约束下对解进行精炼,以实现最优解。
在我们定义的术语中,路径 DP 对应于横向基本规划(BP),路径 QP 对应于横向主规划(MP),速度 DP 对应于纵向基本规划(BP),速度 QP 对应于纵向主规划(MP)。因此,该过程涉及在路径和速度步骤中先进行基本规划(BP),然后进行主规划(MP)。

一个完整的自动驾驶系统,采用路径-速度解耦规划(图表由作者制作)
联合时空规划
虽然解耦规划可以解决自动驾驶中 95%的情况,但剩余的 5%涉及具有挑战性的动态交互,在这些情况下,解耦解往往导致次优的轨迹。在这些复杂场景中,展现智能是至关重要的,这也使其成为该领域的一个热点话题。
例如,在狭窄空间通过时,最优行为可能是减速让行或加速超车。这样的行为在解耦的解空间内无法实现,需要联合优化。联合优化允许更综合的方法,能够同时考虑路径和速度,从而有效处理复杂的动态交互。

一个完整的自动驾驶系统,采用联合时空规划(图表由作者制作)
然而,联合时空规划面临重大挑战。首先,在更高维的状态空间中直接解决非凸问题比使用解耦方案更加困难且耗时。其次,在时空联合规划中考虑交互性则更加复杂。我们将在后续讨论决策时更详细地探讨这一主题。
在这里,我们介绍了两种求解方法:暴力搜索和构建时空走廊进行优化。
粗暴搜索直接发生在三维时空空间中(空间为二维,时间为一维),可以在 XYT(笛卡尔坐标)或 SLT(弗雷内坐标)坐标系中执行。我们以 SLT 为例。SLT 空间长而平坦,类似于一个能量条。它在 L 维度上拉长,在 ST 面上平坦。对于粗暴搜索,我们可以使用混合 A 星算法,代价是进度代价与前进代价的组合。在优化过程中,我们必须遵守搜索约束,防止在 s 和 t 维度上发生反向移动。

空间时间晶格中的变道超车(来源:基于 A*的时空优化)
另一种方法是构建时空走廊,本质上是一个曲线,表示汽车的足迹在三维时空状态空间中盘旋(例如 SLT)。SSC(时空语义走廊,RAL 2019)将语义元素提供的需求编码到语义走廊中,从而生成安全轨迹。语义走廊由一系列互相连接的无碰撞立方体组成,这些立方体的动态约束由时空域中的语义元素施加。在每个立方体内,它变成一个凸优化问题,可以使用二次规划(QP)来求解。
SSC 仍然需要一个 BP(行为规划)模块来提供粗略的驾驶轨迹。环境中的复杂语义元素相对于参考车道投影到时空域中。EPSILON(TRO 2021)展示了一个系统,其中 SSC 作为与行为规划器协同工作的运动规划器。在下一节中,我们将讨论行为规划,特别是专注于交互。在这种情况下,行为规划通常被称为决策制定。

时空走廊的示意图(来源:SSC)
决策制定
什么和为什么?
自动驾驶中的决策制定本质上是行为规划,但更侧重于与其他交通主体的交互。假设其他主体大多数是理性的,并且会以可预测的方式响应我们的行为,这种情况可以描述为“有噪声的理性”。
当先进的规划工具可用时,人们可能会质疑决策制定的必要性。然而,有两个关键因素——不确定性和交互——使得环境具有了概率性质,主要是由于动态物体的存在。交互是自动驾驶中最具挑战性的部分,它将自动驾驶与一般机器人技术区分开来。自动驾驶汽车不仅要导航,还必须预测并应对其他主体的行为,因此,稳健的决策制定对安全性和效率至关重要。
在没有互动的确定性(纯几何)世界中,决策将是多余的,通过搜索、采样和优化进行规划就足够了。在三维 XYT 空间中进行暴力搜索可以作为一种通用解决方案。
在大多数经典自动驾驶系统中,采用预测-然后规划的方法,假设自车与其他车辆之间是零阶互动。该方法将预测结果视为确定性的,要求自车做出相应的反应。这会导致过于保守的行为,典型例子就是“冻结机器人”问题。在这种情况下,预测填充了整个时空空间,阻止了像在拥挤条件下变道这样的动作——这是人类能更有效管理的事情。
为了处理随机策略,马尔可夫决策过程(MDP)或部分可观测马尔可夫决策过程(POMDP)框架是必不可少的。这些方法将重点从几何转向概率,解决混乱的不确定性。通过假设交通参与者理性行为或至少是嘈杂理性,决策可以帮助在本应混乱的时空空间中创建安全的行驶通道。
在规划的三个主要目标——安全、舒适性和效率——中,决策主要提升效率。保守的行为可以最大化安全性和舒适性,但通过决策有效地与其他道路参与者协商,对于实现最优效率至关重要。有效的决策也展现了智能。
MDP 和 POMDP
我们将首先介绍马尔可夫决策过程(MDP)和部分可观测马尔可夫决策过程(POMDP),接着介绍它们的系统化解法,如价值迭代和策略迭代。
马尔可夫过程(MP)是一种处理动态随机现象的随机过程,不同于静态概率。在马尔可夫过程中,未来状态仅依赖于当前状态,因此仅凭当前状态就足够进行预测。对于自动驾驶而言,相关的状态可能只包括最后一秒的数据,我们可以扩展状态空间,允许使用更短的历史窗口(例如,取代更长的位置信息历史,可以使用更短的位置信息、速度、加速度等历史窗口)。
马尔可夫决策过程(MDP)是通过引入行动扩展了马尔可夫过程,涵盖了决策过程。MDP 模型中的决策过程,其结果既有部分随机性,也有部分由决策者或智能体控制。一个 MDP 可以通过五个因素来建模:
-
状态(S):环境的状态。
-
行动(A):智能体可以采取的、影响环境的动作。
-
奖励(R):环境因行动而给予智能体的奖励。
-
转移概率(P):智能体采取行动后,从旧状态转移到新状态的概率。
-
折扣因子(γ):未来奖励的折扣因子。
这也是强化学习(RL)使用的常见框架,RL 本质上是一个 MDP。MDP 或 RL 的目标是在长期内最大化累积回报。这要求智能体根据策略,在给定的环境状态下做出正确的决策。
策略π是一个映射,将每个状态 s ∈ S 和动作 a ∈ A(s)映射到在状态 s 下采取动作 a 的概率π(a|s)。MDP 或强化学习研究如何推导出最优策略的问题。

MDP 和 RL 中的智能体-环境接口(来源:强化学习:导论)
部分可观察的马尔可夫决策过程(POMDP)通过认识到状态不能直接观察到,而是通过观察来推测,增加了额外的复杂性。在 POMDP 中,智能体保持一种信念——对可能状态的概率分布——来估计环境的状态。由于环境的不确定性和部分可观察性,自动驾驶场景通常由 POMDP 更好地表示。MDP 可以看作是 POMDP 的一个特例,其中观察能够完美地揭示状态。

MDP 与 POMDP(来源:POMDPs 作为随机应急规划)
POMDPs 可以主动收集信息,从而采取收集必要数据的行动,展现了这些模型的智能行为。这一能力在一些场景中尤为重要,比如在交叉口等待时,收集其他车辆意图和交通信号灯状态的信息对做出安全和高效的决策至关重要。
值迭代与策略迭代
值迭代和策略迭代是解决 MDP 或 POMDP 问题的系统方法。尽管由于其复杂性,这些方法在实际应用中不常用,但理解它们能够帮助我们洞察精确解的获取方式,以及如何在实践中简化这些方法,比如在 AlphaGo 中使用 MCTS,或者在自动驾驶中使用 MPDM。
在 MDP 中找到最佳策略,我们必须评估一个状态的潜在或期望回报,或者更具体地说,评估在该状态下采取某个动作的回报。这个期望回报不仅包括即时回报,还包括所有未来的回报,正式称为回报或累积折扣回报。(若要深入了解,请参考“强化学习:导论”,这本书常被视为该领域的权威指南。)
价值函数(V)通过求和期望回报来描述状态的质量。动作价值函数(Q)则评估在特定状态下执行某一动作的质量。这两个函数都是根据给定策略来定义的。贝尔曼最优性方程表明,最优策略将选择能够最大化即时奖励以及从结果状态中获得的期望未来奖励的动作。简单来说,贝尔曼最优性方程建议考虑动作的即时奖励以及未来的后果。例如,在换工作时,不仅要考虑即时的加薪(R),还要考虑新职位带来的未来价值(S’)。

贝尔曼最优性方程(图表由作者制作)
一旦获得最优值函数,从贝尔曼最优性方程中提取最优策略是相对简单的。那么我们如何找到这个最优值函数呢?这时,价值迭代就派上用场了。

从最优值中提取最佳策略(图表由作者制作)
价值迭代通过反复更新每个状态的值,直到其稳定,从而找到最佳策略。这个过程是通过将贝尔曼最优性方程转化为更新规则来实现的。从本质上讲,我们使用最优的未来图景来引导迭代朝着它前进。通俗来说,就是“装作成功直到你成功!”

在贝尔曼方程的指导下更新价值函数(图表由作者制作)
价值迭代在有限状态空间内保证收敛,无论初始状态值如何(详细证明请参见强化学习圣经)。如果折扣因子 gamma 设置为 0,意味着我们只考虑即时奖励,价值迭代将在一次迭代后收敛。较小的 gamma 会导致更快的收敛,因为考虑的视野更短,尽管这不一定总是解决具体问题的最佳选择。平衡折扣因子是工程实践中的关键方面。
有人可能会问,如果所有状态都初始化为零,这个方法如何有效。贝尔曼方程中的即时奖励对于引入额外信息并打破初始对称性至关重要。想一想那些能直接到达目标状态的状态,它们的价值像病毒一样传播在状态空间中。通俗来说,这就是不断地取得小胜利。

值函数和策略函数相互作用,直到它们一起收敛到最优值(来源:强化学习:导论)
然而,值迭代也存在低效的问题。它需要在每次迭代中通过考虑所有可能的行动来选择最优行为,类似于 Dijkstra 算法。尽管作为一种基本方法它是可行的,但通常不适用于现实世界的应用。

贝尔曼方程与贝尔曼最优方程的对比(图表由作者制作)
策略迭代在此基础上进行改进,依据当前策略采取行动,并根据贝尔曼方程(而非贝尔曼最优方程)进行更新。策略迭代将策略评估与策略改进解耦,使其成为一种更快速的解决方案。每一步都是根据给定的策略来执行,而不是探索所有可能的行动以找到最大化目标的行动。尽管策略迭代的每次迭代可能因策略评估步骤而计算量较大,但总体上它能更快地收敛。
简单来说,如果你只能完全评估某个行为的结果,最好还是依赖自己的判断,尽力利用当前可用的信息。
AlphaGo 与蒙特卡洛树搜索(MCTS)——当网络遇见树
我们都听过 AlphaGo 在 2016 年击败世界顶级围棋选手的惊人故事。AlphaGo 将围棋的游戏规则视为马尔可夫决策过程(MDP),并通过蒙特卡洛树搜索(MCTS)进行求解。那么,为什么不使用值迭代或策略迭代呢?
值迭代和策略迭代是解决 MDP 问题的系统性迭代方法。然而,即使是改进后的策略迭代,仍然需要执行耗时的操作来更新每个状态的值。标准的 19x19 围棋棋盘大约有2e170 种可能状态。如此庞大的状态空间使得传统的值迭代或策略迭代方法无法解决。
AlphaGo 及其后续版本使用蒙特卡洛树搜索(MCTS)算法来寻找最佳走法,并通过值网络和策略网络进行指导,这些网络通过人类和计算机对局的数据进行训练。我们先来了解一下基础的 MCTS。

AlphaGo 的 MCTS 四个步骤,结合了值网络和策略网络(来源:AlphaGo,《自然》2016)
蒙特卡洛树搜索(MCTS)是一种用于策略估计的方法,侧重于从当前状态进行决策。一次迭代涉及四个步骤:选择、扩展、模拟(或评估)和备份。
-
选择:算法根据之前的模拟结果,遵循最有前景的路径,直到到达一个叶节点,即尚未完全探索的位置。
-
扩展:添加一个或多个子节点,表示从叶节点出发的可能下一步。
-
模拟(评估):算法从新节点开始,随机进行一局游戏,直到结束,这称为“回放”。这通过模拟随机动作直到达到终止状态,评估从扩展节点的潜在结果。
-
回传:算法根据游戏结果更新路径上节点的值。如果结果是胜利,节点的值增加;如果是失败,节点的值减少。这个过程将回放的结果向上传播,基于模拟的结果细化策略。
在一定次数的迭代后,MCTS 提供了在模拟过程中从根节点选择即时动作的百分比频率。在推理时,会选择访问次数最多的动作。这里是一个简单的MTCS 互动示意图展示了井字游戏。
AlphaGo 中的 MCTS 通过两个神经网络得到增强。值网络评估给定状态(棋盘配置)的胜率。策略网络评估所有可能动作的分布。这些神经网络通过减少搜索树的有效深度和广度来改进 MCTS。策略网络有助于采样动作,集中搜索有前景的动作,而值网络提供了对位置的更准确评估,减少了广泛回放的需求。这种组合使得 AlphaGo 能够在围棋的广阔状态空间中进行高效且有效的搜索。

AlphaGo 的策略网络和值网络(来源:AlphaGo,Nature 2016)
在扩展步骤中,策略网络对最可能的位置进行采样,有效地修剪了搜索空间的广度。在评估步骤中,值网络对位置进行本能的评分,而更快速、轻量级的策略网络则执行回放,直到游戏结束并收集奖励。然后,MCTS 使用两个网络评估的加权总和来做出最终评估。
注意,单次对值网络的评估接近使用 RL 策略网络的蒙特卡罗回放的准确度,但计算量少了 15,000 倍。这类似于快-慢系统的设计,类似于诺贝尔奖得主丹尼尔·卡尼曼所描述的系统 1 与系统 2,也可见于最近的工作中,如DriveVLM。
准确地说,AlphaGo 在不同的层次上结合了两种慢速-快速系统。在宏观层面,策略网络选择棋步,而更快的回滚策略网络评估这些棋步。在微观层面,更快的回滚策略网络可以通过一个价值网络来近似,该网络直接预测棋盘位置的胜率。
我们可以从 AlphaGo 中为自动驾驶学到什么?AlphaGo 展示了使用强大的世界模型(仿真)提取优秀策略的重要性。同样,自动驾驶也需要一个高精度的仿真,以有效地利用类似于 AlphaGo 的算法。这一方法强调了将强大的策略网络与详细、精确的仿真相结合,以增强决策和优化复杂动态环境中的表现的价值。
MPDM(及其后继研究)在自动驾驶中的应用
在围棋游戏中,所有状态对两位玩家都是立即可用的,这使得它成为一个完备信息游戏,其中观察等于状态。这使得游戏可以通过 MDP 过程来表征。相比之下,自动驾驶是一个 POMDP 过程,因为状态只能通过观察来估计。
POMDP 通过一种原理性的方法将感知和规划联系起来。POMDP 的典型解决方案类似于 MDP 的解决方案,通过有限的前瞻来实现。然而,主要的挑战在于维度灾难(状态空间爆炸)和与其他代理的复杂交互。为了使实时进展可行,通常会做出特定领域的假设来简化 POMDP 问题。
MPDM(以及两个 后续研究,和白皮书)是这一方向上的一项开创性研究。MPDM 将 POMDP 简化为对有限、离散的语义级策略集合进行闭环前向仿真,而不是为每辆车评估所有可能的控制输入。这一方法通过关注可管理的、有意义的策略,解决了维度灾难问题,从而实现了在自动驾驶场景中的有效实时决策。

语义动作有助于控制维度灾难(来源:EPSILON)
MPDM 的假设有两个方面。首先,许多人类驾驶员的决策涉及离散的高级语义动作(例如减速、加速、变道、停车)。在此背景下,这些动作被称为策略。第二个隐含假设是关于其他代理的:其他车辆将做出合理安全的决策。一旦车辆的策略确定,其行动(轨迹)也随之确定。

MPDM 框架(图表由作者创建)
MPDM 首先从多个选项中为自车选择一个策略(因此其名称中有“多策略”一词),并根据各自的预测为每个附近的代理选择一个策略。接着进行前向仿真(类似于 MCTS 中的快速展开)。经过评估后,最好的交互场景会传递给运动规划,例如在联合时空规划会议中提到的时空语义走廊(SCC)。
MPDM 使得智能和类人行为成为可能,例如,即使没有足够的空隙,依然能够主动切入密集的交通流中。这是基于预测-再规划管道无法实现的,因为它并未明确考虑交互。MPDM 中的预测模块通过前向仿真与行为规划模型紧密集成。
MPDM 假设在整个决策时域(10 秒)内使用单一策略。实质上,MPDM 采用了一种 MCTS 方法,具有一层深度和超宽的结构,考虑了所有可能的代理预测。这为改进留下了空间,启发了许多后续工作,如 EUDM、EPSILON 和 MARC。例如,EUDM 考虑了更灵活的自我策略,并分配了一个深度为四的策略树,每个策略涵盖 2 秒的时间段,整个决策时域为 8 秒。为了弥补因树深增加带来的额外计算量,EUDM 通过引导分支执行更高效的宽度修剪,识别关键场景和重要车辆。这种方法探索了一个更平衡的策略树。
MPDM 和 EUDM 中的前向仿真使用了非常简单的驾驶员模型(纵向仿真使用智能驾驶员模型 IDM,横向仿真使用纯追踪算法 PP)。MPDM 指出,高保真度的真实感不如闭环特性本身重要,只要策略层决策不受低级动作执行不准确性的影响。

决策制定的概念图,其中预测、行为规划(BP)和运动规划(MP)紧密集成(图表由作者创建)
自动驾驶中的应急规划涉及生成多种潜在轨迹,以应对各种可能的未来场景。一个关键的激励性例子是,经验丰富的驾驶员会预见到多种未来情景,并始终为安全备选方案做计划。这种预见性方法能够带来更平稳的驾驶体验,即使在车辆突然切入自车道时也能保持平稳。
应急规划的一个关键方面是推迟决策分岔点。这意味着延迟不同潜在轨迹分歧的点,为自车提供更多时间来收集信息并应对不同的结果。通过这样做,车辆可以做出更明智的决策,从而实现更平稳、更自信的驾驶行为,类似于经验丰富的驾驶员。

风险意识应急规划(来源:MARC,RAL 2023)
MARC 也将行为规划和运动规划结合在一起。这扩展了前向仿真的概念和实用性。换句话说,MPDM 和 EUDM 仍然使用策略树进行高层次行为规划,并依赖其他运动规划管道,如语义时空通道(SSC),这是因为在策略树中的自车运动仍然由高度量化的行为桶来表征。MARC 通过保持除自车外其他代理的量化行为,但在前向展开中直接使用更精细的运动规划来扩展这一点。从某种意义上说,这是一种混合方法,其中“混合”与混合 A*中的含义相似,结合了离散和连续。
MPDM 及其所有后续工作可能的一个缺点是,它们依赖于为类似高速公路结构的环境设计的简单策略,例如车道保持和变道。这种依赖可能限制了前向仿真在处理复杂交互时的能力。为了解决这个问题,跟随 MPDM 的例子,使 POMDPs 更有效的关键是通过高层次策略树的扩展来简化动作和状态空间。通过列举所有相对物体的时空相对位置标签,然后执行引导分支,可能能够创建一个更灵活的策略树。
决策制定的行业实践
决策制定仍然是当前研究的热门话题。即使是经典的优化方法也未被完全探索。机器学习方法有可能大放异彩,产生颠覆性影响,尤其是在大型语言模型(LLM)问世后,借助如思维链(CoT)或蒙特卡洛树搜索(MCTS)等技术。
树
树是进行决策的系统化方法。特斯拉 2021 年和 2022 年的 AI 日展示了它们的决策能力,深受 AlphaGo 及其后继版本 MuZero 的影响,用以处理高度复杂的交互。
从高层次来看,特斯拉的方法遵循行为规划(决策制定)后接运动规划。它首先寻找一个凸形通道,然后将其输入到连续优化中,采用时空联合规划。这种方法有效地解决了狭窄通过等场景,这是路径-速度解耦规划的典型瓶颈。

神经网络启发式引导的蒙特卡罗树搜索(MCTS)(来源:Tesla AI Day 2021)
特斯拉还采用了一种混合系统,结合了数据驱动和基于物理的检查。从定义的目标开始,特斯拉的系统生成种子轨迹并评估关键场景。然后,它会分支创建更多的场景变体,如对交通代理进行让步或请求。这样的政策树交互搜索在2021 年和2022 年的展示中有所展示。
特斯拉在机器学习应用中的一个亮点是通过轨迹优化加速树搜索。对于每个节点,特斯拉使用基于物理的优化和神经网络规划器,实现了 10 毫秒与 100 微秒的时间差——带来了 10 倍到 100 倍的性能提升。神经网络通过专家演示和离线优化器进行训练。
轨迹评分通过将经典物理基础检查(如碰撞检查和舒适度分析)与预测干预可能性和人类相似度的神经网络评估器相结合来完成。这种评分有助于修剪搜索空间,将计算集中在最有前景的结果上。
虽然许多人认为机器学习应该应用于高层决策,但特斯拉基本上使用机器学习来加速优化,从而加速树搜索。
蒙特卡罗树搜索(MCTS)方法似乎是决策的终极工具。有趣的是,研究大规模语言模型(LLM)的人们正在尝试将 MCTS 融入 LLM,而从事自动驾驶的人们则在尝试用 LLM 取代 MCTS。
大约两年前,特斯拉的技术采用了这种方法。然而,从 2024 年 3 月开始,特斯拉的全自动驾驶(FSD)切换到了更加端到端的方法,明显不同于他们之前的做法。
无树
我们仍然可以考虑不隐式生成树的交互。可以实现临时逻辑,以在预测与规划之间进行一级交互。即使是一级交互,也能产生良好的行为,正如 TuSimple 所展示的那样。MPDM 的原始形式本质上就是一级交互,但它以更有原则和可扩展的方式执行。

预测与规划之间的多级交互(来源:TuSimple AI Day,中文,由作者翻译)
TuSimple 也展示了执行应急规划的能力,类似于MARC中提出的方法(尽管 MARC 也能适应定制的风险偏好)。

应急规划(来源:TuSImple AI day,中文,作者翻译)
自我反思
在学习了经典规划系统的基本构建块之后,包括行为规划、运动规划和通过决策来处理交互的有原则方法,我一直在反思系统中潜在的瓶颈以及机器学习(ML)和神经网络(NN)如何提供帮助。我在此记录下我的思考过程,以便将来参考,也为其他可能有类似问题的人提供帮助。请注意,本节中的信息可能包含个人偏见和推测。
为什么在规划中使用神经网络(NN)?
让我们从三个不同的角度来看待这个问题:在现有的模块化管道中,作为端到端(e2e)神经网络(NN)规划器,或者作为端到端的自动驾驶系统。
回到最初的设计,我们来回顾一下自动驾驶中规划系统的问题定义。目标是获得一个能够确保安全、舒适和高效的轨迹,同时在一个高度不确定且交互密集的环境中运行,并遵循车载的实时工程约束。以下图表总结了这些因素:目标、环境和约束。

神经网络在规划中的潜力(图表由作者制作)
自动驾驶中的不确定性可以指感知(观察)中的不确定性以及预测未来长期代理行为的不确定性。规划系统还必须处理其他代理未来轨迹预测中的不确定性。正如前面讨论的,采用有原则的决策系统是有效管理这些不确定性的方法。
另外,一个通常被忽视的方面是,规划必须容忍不确定的、不完美的,有时是不完整的感知结果,特别是在当前以视觉为中心且没有高清地图的驾驶时代。车载标准定义(SD)地图作为先验有助于缓解这种不确定性,但它仍然对一个高度手工制作的规划系统构成重大挑战。这个感知不确定性曾被 Level 4(L4)自动驾驶公司认为是一个已解决的问题,尤其是通过大量使用激光雷达(Lidar)和高清地图。然而,随着行业朝着没有这两项“拐杖”的量产自动驾驶解决方案发展,这个问题再次浮出水面。神经网络规划器更为强健,能够处理大部分不完美和不完整的感知结果,这对于以视觉为中心且没有高清地图的量产高级驾驶辅助系统(ADAS)至关重要。
交互应该通过一个有原则的决策系统来处理,例如蒙特卡洛树搜索(MCTS)或简化版的 MPDM。主要的挑战是通过利用自动驾驶领域的知识进行智能剪枝,成长一个平衡的策略树,以应对维度灾难(组合爆炸)问题。MPDM 及其变种在学术界和工业界(如特斯拉)提供了如何以平衡的方式成长这棵树的良好示例。
神经网络(NN)还可以通过加速运动规划优化,增强规划器的实时性能。这可以将计算负载从 CPU 转移到 GPU,实现数量级的加速。优化速度的十倍提升可以从根本上影响高层次算法设计,例如 MCTS。
轨迹也需要更具人类特征。可以利用大量的人类驾驶数据训练人类相似度和接管预测模型。相比维持一个不断壮大的工程团队,增加计算池的规模更具可扩展性。

基于神经网络(NN)的规划堆栈可以更有效地利用人类驾驶数据(图表由作者制作)
那么,端到端(e2e)神经网络规划器呢?
端到端(e2e)神经网络(NN)规划器仍然构成一个模块化的自动驾驶(AD)设计,接受结构化的感知结果(以及可能的潜在特征)作为输入。该方法将预测、决策和规划融合到一个单一网络中。诸如DeepRoute(2022)和华为(2024)等公司声称采用此方法。请注意,这里省略了相关的原始传感器输入,如导航信息和自车信息。

配备端到端规划器的完整自动驾驶堆栈(图表由作者制作)
这个端到端(e2e)规划器可以进一步发展成一个结合感知与规划的端到端自动驾驶系统。这正是Wayve 的 LINGO-2(2024)和特斯拉的 FSDv12(2024)声称实现的目标。
这种方法的好处有两个方面。首先,它解决了感知问题。驾驶中有许多方面我们无法通过常用的感知接口轻松地显式建模。例如,手工打造一个驾驶系统来绕过水坑或减速通过坑洼或裂缝是非常具有挑战性的。虽然传递中间的感知特征可能有所帮助,但它可能无法从根本上解决问题。
此外,涌现行为可能帮助更系统地解决极端情况。大模型的涌现行为可能导致智能处理边缘情况,例如上述的例子。

一个完整的自动驾驶堆栈包括一个一体化模型(e2e)驾驶员(图表由作者制作)
我的猜测是,在其最终形态下,端到端(e2e)驾驶员将是一个大规模视觉与行动原生的多模态模型,并通过蒙特卡罗树搜索(MCTS)增强,假设没有计算限制。
到 2024 年共识为止,自动驾驶中的世界模型通常是一个多模态模型,至少覆盖视觉和行动模式(或 VA 模型)。虽然语言可以加速训练、增强可控性并提供可解释性,但它不是必需的。在其完全发展形态下,世界模型将是一个 VLA(视觉-语言-行动)模型。
开发世界模型至少有两种方法:
-
视频原生模型:训练一个模型预测未来的视频帧,基于或输出相应的行动,正如 GAIA-1 等模型所展示的。
-
多模态适配器:从一个预训练的大型语言模型(LLM)开始,并添加多模态适配器,如 Lingo-2、RT2 或 ApolloFM 等模型所示。这些多模态 LLM 并非天生适用于视觉或行动,但需要的训练资源明显较少。
一个世界模型可以通过行动输出自身生成策略,从而直接控制车辆。或者,MCTS 可以查询世界模型并利用其策略输出引导搜索。虽然这种世界模型-MCTS 方法计算密集度更高,但由于其明确的推理逻辑,它可能在处理极端情况时具有更高的上限。
我们能做到不进行预测吗?
当前大多数运动预测模块将自车之外的代理未来轨迹表示为一个或多个离散轨迹。是否这种预测-规划接口足够或必要,仍然是一个问题。
在经典的模块化管道中,预测仍然是必需的。然而,预测-再规划的管道确实限制了自动驾驶系统的上限,如在决策制定部分讨论的那样。一个更关键的问题是如何将这一预测模块更有效地整合进整个自动驾驶堆栈中。预测应该辅助决策制定,并且在整体决策框架中使用可查询的预测模块,如 MPDM 及其变种,是优选方案。只要正确整合,例如通过策略树展开,具体的轨迹预测没有严重问题。
预测的另一个问题是,开放式关键绩效指标(KPIs),例如平均位移误差(ADE)和最终位移误差(FDE),并不是有效的指标,因为它们无法反映对规划的影响。相反,应该考虑像意图层次的召回率和精确度等指标。
在端到端系统中,可能不需要显式预测模块,但隐式监督——以及来自经典堆栈的其他领域知识——绝对可以帮助,或者至少能提高学习系统的数据效率。评估预测行为,无论是显式的还是隐式的,也有助于调试这样的端到端系统。
我们能仅依靠神经网络而没有树形结构吗?
结论优先。对于助手,神经网络(nets)能够实现非常高的,甚至是超人类的表现。对于智能体,我认为使用树形结构仍然是有益的(尽管不一定是必须的)。
首先,树形结构可以增强神经网络。树形结构能够提升给定网络的性能,无论其是否基于神经网络。在 AlphaGo 中,即使使用通过监督学习和强化学习训练的策略网络,整体表现仍然不如基于蒙特卡罗树搜索的 AlphaGo,后者将策略网络作为一个组件进行集成。
其次,神经网络可以提炼树形结构。在 AlphaGo 中,蒙特卡罗树搜索(MCTS)使用了价值网络和来自快速回滚策略网络的奖励来评估树中的节点(状态或棋盘位置)。AlphaGo 论文还提到,尽管单独使用价值函数也可以,但结合两者的结果会得到最佳效果。价值网络本质上是通过直接学习状态-价值对来提炼策略回滚的知识。这类似于人类如何将缓慢的系统 2 的逻辑思维提炼成快速、直觉性的系统 1 反应。丹尼尔·卡尼曼在他的书《思考,快与慢》中描述了一个国际象棋大师如何在多年练习后快速识别模式并做出迅速决策,而新手则需要付出相当大的努力才能达到类似的结果。类似地,AlphaGo 中的价值网络被训练来提供对给定棋盘位置的快速评估。

无需搜索的国际象棋大师级水平(来源:DeepMind, 2024)
最近的论文探讨了神经网络在这一快速系统中的上限。《无搜索的国际象棋》一文证明,在有足够数据的情况下(这些数据通过传统算法的树搜索准备),可以达到大师级水平。数据量和模型规模与系统熟练度之间存在明显的“扩展法则”,表明随着数据量和模型复杂度的增加,系统的熟练度也会提高。
所以我们这里有一对强力组合:树提升网络,网络提炼树。这种正反馈循环本质上就是 AlphaZero 用来自我启动,达到在多种游戏中超越人类表现的机制。
相同的原则也适用于大规模语言模型(LLMs)的开发。对于游戏来说,由于我们有明确的奖励——胜利或失败,我们可以使用前向展开来确定某个动作或状态的价值。对于 LLMs,奖励不像围棋那样明确,因此我们依赖人类偏好,通过人类反馈的强化学习(RLHF)来评估模型。然而,像 ChatGPT 这样的模型已经经过训练,我们可以使用监督微调(SFT),这本质上是模仿学习,用以提炼出更小但仍然强大的模型,而无需 RLHF。
回到最初的问题,网络可以通过大量高质量的数据实现极高的性能。这对于一个助手可能已经足够,具体取决于对错误的容忍度,但对于一个自主智能体来说,可能还不够。对于面向驾驶辅助(ADAS)的系统,通过模仿学习的网络可能是足够的。
树可以通过显式的推理循环显著提升网络的性能,这使得它们可能更适合完全自主的智能体。树或推理循环的程度取决于工程资源的投入回报。例如,哪怕只有一个层次的交互,也能带来显著的好处,正如在TuSimple AI Day中所展示的那样。
我们可以使用 LLMs 做决策吗?
从下面关于当前最热 AI 系统代表的总结中,我们可以看到,LLMs 并不是为了执行决策制定而设计的。本质上,LLMs 是被训练来完成文档的,甚至是经过 SFT 对齐的 LLM 助手也将对话视为一种特殊类型的文档(完成一个对话记录)。

2024 年代表性的 AI 产品(图表由作者制作)
我不完全同意最近关于 LLMs 是慢速系统(系统 2)的说法。由于硬件限制,它们在推理时确实不必要地慢,但在它们的原始形式中,LLMs 是快速系统,因为它们无法进行反事实检查。诸如“思维链”(CoT)或“思维树”(ToT)之类的提示技术,实际上是简化版的蒙特卡洛树搜索(MCTS),使得 LLMs 的功能更像是较慢的系统。
有大量研究尝试将完整的 MCTS 与大规模语言模型(LLMs)结合。具体来说,LLM-MCTS(NeurIPS 2023)将 LLM 视为常识性的“世界模型”,并使用 LLM 引导的策略动作作为启发式方法来指导搜索。在复杂的新任务中,LLM-MCTS 在性能上远远超过了单独的 MCTS 和 LLM 引导的策略。来自 OpenAI 的高度推测 Q-star 看起来似乎采用了类似的方式,通过 MCTS 增强 LLM,就如其名称所示。
演变趋势
以下是自动驾驶规划栈的粗略演变。之所以说粗略,是因为列出的解决方案不一定比上面的更加先进,而且它们的首次出现可能并不按照严格的时间顺序排列。尽管如此,我们仍然可以观察到一些总体趋势。请注意,行业中列出的代表性解决方案基于我对各种新闻稿的解读,可能存在误差。
一种趋势是朝着更端到端的设计发展,将更多的模块整合成一个系统。我们看到规划栈从路径-速度解耦规划发展到联合时空规划,从预测-然后-规划系统发展到联合预测与规划系统。另一个趋势是越来越多地将基于机器学习的组件纳入其中,特别是在最后三个阶段。这两种趋势汇聚成一个端到端的神经网络规划器(没有感知)甚至是一个端到端的神经网络驾驶员(有感知)。

规划演变的粗略历史(图表由作者制作)
重点
-
机器学习作为工具:机器学习是一种工具,而不是独立的解决方案。即使在当前的模块化设计中,它也能帮助进行规划。
-
完整的公式化:从完整的问题公式化开始,然后做出合理的假设,以平衡性能和资源。这有助于为未来的系统设计创造清晰的方向,并随着资源的增加进行改进。回想一下从 POMDP 的公式化到像 AlphaGo 的 MCTS 和 MPDM 这样的工程解决方案的过渡。
-
算法的适应性:理论上优美的算法(例如,Dijkstra 和价值迭代)在理解概念上非常有用,但在实际工程中需要适应(将价值迭代转化为 MCTS,就像将 Dijkstra 算法转化为混合 A-star 一样)。
-
确定性与随机性:规划在解决确定性(不一定是静态)场景方面表现出色。随机场景中的决策是朝着完全自主迈进的最具挑战性的任务。
-
应急规划:这有助于将多个未来情景合并为一个共同的行动。采取积极进取的态度是有益的,这样你总能依靠备份计划。
-
端到端模型:端到端模型是否能解决完全自动化问题尚不明确。它可能仍然需要经典方法,如 MCTS。神经网络可以处理辅助任务,而树结构则能管理代理。
致谢
-
感谢Naiyan Wang和 Jingwei Zhao 的深入和启发性讨论。感谢论文推土机、Invictus、XXF、PYZ 和 JCL 在初稿中提供的关键反馈。感谢与伯克利大学的赵伟教授就学术界趋势进行的深刻讨论。
参考文献
-
UniAD: 面向规划的自动驾驶,CVPR 2023 最佳论文
-
自动驾驶行业与学术界端到端规划回顾:2022–2023,Arxiv 2024
-
BEVGPT: 用于自动驾驶预测、决策和规划的生成预训练大模型,AAAI 2024
-
基于流体动力学的自动驾驶通用运动规划,Arxiv 2024
-
Tusimple AI Day,中文带英文字幕在哔哩哔哩,2023 年 07 月
-
Qcraft 关于联合时空规划的技术博客,中文在知乎,2022 年 08 月
-
自动驾驶全栈回顾,中文在知乎,2018 年 12 月
-
Tesla AI Day 规划,中文在知乎,2022 年 10 月
-
关于 ApolloFM 的技术博客,中文清华 AIR 发布,2024 年
-
在 Frenet 坐标系中为动态街道场景生成最优轨迹,ICRA 2010
-
MP3: 一个统一的模型来映射、感知、预测和规划,CVPR 2021
-
NMP: 端到端可解释的神经运动规划器,CVPR 2019 口头报告
-
CoverNet: 使用轨迹集进行多模态行为预测,CVPR 2020
-
百度 Apollo EM 动作规划器,百度,2018 年
-
AlphaGo: 利用深度神经网络和树搜索掌握围棋,《自然》2016 年
-
AlphaZero: 一种通用强化学习算法,通过自我对弈掌握国际象棋、将棋和围棋,Science 2017
-
MuZero: 通过规划与学习模型掌握 Atari、围棋、国际象棋和将棋,Nature 2020
-
ToT: 思维树:使用大语言模型进行深思熟虑的问题解决,NeurIPS 2023 Oral
-
CoT: 思维链提示引发大语言模型中的推理,NeurIPS 2022
-
LLM-MCTS: 大语言模型作为大规模任务规划中的常识知识,NeurIPS 2023
-
MPDM: 动态不确定环境下的多策略决策制定用于自动驾驶,ICRA 2015
-
MPDM2: 基于变点行为预测的自动驾驶多策略决策制定,RSS 2015
-
MPDM3: 基于变点行为预测的自动驾驶多策略决策制定:理论与实验,RSS 2017
-
EUDM: 使用引导分支的高效不确定性意识决策制定用于自动驾驶,ICRA 2020
-
MARC: 自动驾驶的多策略和风险意识应急规划,RAL 2023
-
EPSILON: 高度互动环境下自动驾驶车辆的高效规划系统,TRO 2021
对 AI 图像生成的批判性审视
图像生成型 AI 究竟能告诉我们关于这个世界什么?
·发表于Towards Data Science ·阅读时间 9 分钟·2024 年 10 月 17 日
--

我最近有机会对一个有趣的项目进行分析,并且我有更多的想法,超出了那篇文章所能包含的内容,所以今天我将进一步讨论我对这个项目的看法。
研究人员在这个项目中的方法是向不同的生成型 AI 图像生成工具提供一系列提示词:Stable Diffusion、Midjourney、YandexART 和 ERNIE-ViLG(由百度开发)。这些提示词特别围绕不同的代际群体——婴儿潮一代、X 世代、千禧一代和 Z 世代,要求生成这些群体在不同场景下的图像,如“与家人在一起”,“度假”或“在工作中”。
尽管这些结果非常有趣,或许能揭示一些关于视觉表现的见解,但我认为我们也应该注意到这些图像无法告诉我们什么,或者它们的局限性在哪里。我将把我的讨论分为审美(图像的外观)和表现(图像中实际呈现的内容)两部分,并稍微偏离一下,谈谈这些图像是如何生成的,因为这一点对这两个话题都非常重要。
引言
不过在开始之前,先快速回顾一下这些图像生成模型。它们的创建方式是通过大量图像数据集(包括照片、艺术作品等)与简短的文本描述配对,目标是让模型学习词语和图像外观之间的关系,以便当给定一个词时,模型能够生成与之大致匹配的图像。模型的内部机制更为复杂,而且这些模型(像其他生成式 AI)内建了随机性,这样可以产生变化和惊喜。
当你使用这些托管模型时,你输入一个文本提示,然后模型会返回一张图像。然而,重要的是要注意,提示并不是模型接收到的唯一信息。模型还有一些内建的指令,有时我称之为预提示指令,这些指令会对输出结果产生影响。举例来说,可能会告诉模型拒绝创建某些类型的冒犯性图像,或者拒绝使用冒犯性语言的提示。
训练数据
这里一个重要的框架是,训练数据——那些与文本描述配对的大型图像集——是模型试图复制的对象。所以,我们应该对训练数据及其来源提出更多问题。为了训练这些模型,所需的图像数据量是巨大的。Midjourney 就是在 laion.ai/ 上进行训练的,该平台的更大数据集包含了跨多种语言的 50 亿对图像-文本配对,我们可以假设其他模型的内容量也相似。这意味着工程师不能对用于训练的图像挑剔太多,因为他们基本上需要尽可能多的内容。
好的,那么我们从哪里获取图像呢?它们是如何生成的?嗯,我们自己制作图像并通过社交媒体大量发布,所以这无疑会占据其中一部分。(从这些平台上获取也很容易。)媒体和广告也创造了大量的图像,从电影到广告,再到杂志等等。许多其他图像永远无法被这些模型访问,比如你奶奶的照片专辑没人数字化过,但可以用于训练的图像主要来自这两个来源:独立/个人创作者和媒体/广告。
那么,当你使用这些模型时,实际上会得到什么呢?
美学
如果你尝试这些不同的图像生成器,你会注意到它们之间的风格区别,以及风格内部的一致性。我认为这非常有趣,因为它们几乎像是有自己的个性!Midjourney 显得阴暗且情绪化,带有阴影元素,而 Stable Diffusion 则明亮且过度饱和,对比度极高。ERNIE-ViLG 似乎倾向于卡通风格,同样具有极高的对比度,且质感呈现出橡胶感或高度滤镜化的效果。YandexART 的颜色偏褪色,背景通常没有特征或非常模糊,并且有聚光灯效果(在某些情况下让我想起了在百货商店拍的家庭照)。多种不同的因素可能导致每个模型的独特风格。
正如我之前提到的,预先提示指令是在用户提供输入的基础上应用的。这些指令可能会指定输出应该始终具有的特定美学成分,例如色调、亮度和对比度等风格选择,或者指示模型避免遵循不当的指令等。这为模型提供者实现工具的某些限制和保护措施,防止滥用,也能创造出美学上的连贯性。
通过强化学习进行微调的过程也可能影响风格,其中人工观察员对返回模型的输出做出判断。人工观察员会接受培训,并被指示哪些图像特征应该被批准/接受,哪些应该被拒绝或降分,这可能涉及对某些类型的视觉效果给予更高的评分。
训练数据的类型也会产生影响。我们知道一些用于训练模型的大型数据集,但可能还有我们不知道的内容,因此我们必须从模型输出中推断。如果模型生成的是高对比度、明亮的图像,那么训练数据中很可能包含了大量具有这些特征的图像。
然而,在我们分析不同模型的输出时,重要的是要记住,这些风格可能是预先提示指令、训练数据和人工微调的结合。
除了图像的视觉吸引力/风格之外,图像中实际包含了什么内容?
表现形式
局限性
模型的能力将受限于它们的训练现实。这些模型是基于过去的图像进行训练的——一些是最近的图像,但有些则是更久远的图像。例如,考虑到:随着时间的推移,年轻一代的整个生活都将在线上有图像,但对于年长的群体来说,他们年轻或青少年时期的图像并没有大量数字化(或者是高质量的)可用于训练数据,因此我们可能永远不会看到这些模型将他们展现为年轻人。在这个项目中,这一点非常明显:对于 Z 世代和千禧一代,在这些数据中我们看到模型在输出中很难适当地“衰老”这些人像,以符合当今这些群体的实际年龄范围。大多数情况下,这两代人看起来年龄相仿,Z 世代有时在(比如和学校相关的提示)中被展示为孩子。相比之下,婴儿潮一代和 X 世代主要显示为中年或老年,因为现有的训练数据不太可能有他们年轻时的照片扫描副本,这些照片大多来自 1960 年代到 1990 年代。如果你从训练数据的背景来看,这完全是合理的。
[随着时间的推移,年轻一代的整个生活将在线上有图像,但对于年长的群体来说,他们年轻或青少年时期的图像并没有数字化用于训练数据,因此我们可能永远不会看到这些模型将他们展现为年轻人。]
身份
有鉴于此,我认为如果我们调查这些图像,能够从中得到的印象是 A.不同年龄群体如何在图像中展示自己,尤其是年轻人自拍照的表现,B.这些群体在媒体中的表现如何。(有时很难将这两者分开,因为媒体和青少年文化是相互对立的。)
训练数据并非凭空产生——人类选择了创建、分享、标注和策划这些图像,因此这些人的选择影响了图像的所有内容。模型获取的是这些世代中有人选择展示的图像,且在所有情况下,这些展示背后都有原因和意图。
一个青少年或二十多岁的人自拍并将其发布到网上(以便它能够成为这些模型的训练数据)可能在选择发布到 Instagram 之前,已经拍了十张、二十张或者五十张。与此同时,一个专业摄影师在为广告活动挑选模特时,考虑的因素很多,包括产品、受众、品牌形象等等。因为专业广告并不免于种族主义、性别歧视、年龄歧视或其他任何“主义”,所以这些图像也不会例外,结果是这些模型生成的图像也带有这些社会负担。从这些图像中,你可以看到在一些模型(尤其是 Midjourney 和 Yandex)中,千禧一代和 Z 世代中更多的种族特征,而在同样的模型中,X 世代和婴儿潮一代几乎看不到这些种族特征。这可能至少部分是因为广告商在针对某些群体时,选择那些他们认为会吸引并让他们产生共鸣的种族、民族(以及年龄)代表,且他们预设认为,婴儿潮一代和 X 世代如果看到较老且白人的模特,更可能产生购买欲望。这些图像被创建出来,随后进入训练数据,因此模型学会了生成这些图像。
我想表达的观点是,这些数据并非不受文化和社会的影响——无论这种影响是好是坏。训练数据来源于人类创造,因此模型带着那些人类的社会负担。
我想表达的观点是,这些数据并非不受文化和社会的影响——无论这种影响是好是坏。训练数据来源于人类创造,因此模型带着那些人类的社会负担。
因为这个现实,我认为问我们是否能从模型生成的图像中了解不同代际是一个错误的问题,或者至少是一个误导性的前提。我们可能偶然学到一些关于训练集中人类创造的东西,可能包括自拍,但我们更有可能学到关于更广泛社会的东西,形式上包括人们拍摄他人和自己的照片、媒体以及商业化。我们得到的一些(甚至是很多)内容,尤其是对于那些较少在线发布自创视觉媒体的老年群体,充其量是广告和媒体中的对该群体的刻板印象,而我们知道这些刻板印象本身就有固有缺陷。
从这些图像中是否可以获得关于代际理解的任何东西?或许可以。我认为这个项目可能有助于我们看到代际身份是如何通过媒体进行过滤的,尽管我怀疑这是否是进行这种分析的最方便或最简单的方式。毕竟,我们可以去直接了解源头——尽管这些模型进行的聚合可能在学术上很有趣。它对于年轻一代来说可能更有用,因为更多的训练数据是自我生成的,但即便如此,我依然认为我们应该记住,我们在发布的关于自己的图像中也会注入自己的偏见和议程。
顺便提一下,一些评论员有一种本能的冲动,要求对像这样的模型所生成的内容进行某种程度的美化——这就是我们得到会生成各种种族和民族外貌的纳粹士兵图像的模型的原因。正如我之前写的,这在很大程度上是为了避免面对这些模型反馈给我们的社会现实。我们不喜欢镜子中的样子,所以我们涂抹镜面,而不是去审视我们自己的面容。
当然,这也并不完全正确——我们所有的规范和文化并不会在模型的输出中得到体现,只有那些我们付诸于图像并输入到训练数据中的内容才会被表示出来。我们只看到社会的一部分,而不是以完全真实、无所隐瞒的方式呈现整个社会。因此,我们必须根据这些模型的性质和它们的创建方式,现实地设定我们的期望。我们无法在这些模型中得到我们生活的纯粹图像,因为我们拍摄的照片(以及我们未拍摄的照片,或者未分享的照片),以及媒体创造和传播的图像,都不是没有偏见或客观性的。这就像我们不应该拿自己和生活与朋友们在 Instagram 上发布的图像做比较——那也不是他们生活的完整和准确的写照。除非我们进行一场大规模的摄影和图像标注活动,追求准确性和平等的代表性,并将这些用于训练数据,否则我们无法改变这个系统的运作方式。
结论
花时间思考这些想法对我来说非常有趣,我希望这个分析对那些经常使用这些类型模型的人有所帮助。使用生成型 AI 图像生成模型有许多问题,从环境到经济,但我认为理解这些模型是什么(以及不是)以及它们实际做的事情,对于你选择是否在日常生活中使用这些模型是至关重要的。
阅读更多我的文章,请访问 www.stephaniekirmer.com。
深入阅读
当 LLMs 给我们呈现出揭示人类社会缺陷的结果时,我们能否选择倾听它们所告诉我们的?
[towardsdatascience.com
www.theverge.com/2024/2/21/24079371/google-ai-gemini-generative-inaccurate-historical
使用 Taipy 在纯 Python 中创建数据仪表盘
数据可视化
Taipy 是一个创新框架,旨在简化交互式和视觉吸引力强的数据科学应用程序的创建。
·发表于 Towards Data Science ·13 分钟阅读·2024 年 6 月 25 日
--

我笔记本上的 Taipy 仪表盘
Taipy 框架面向数据科学和数据可视化社区,因此与 Streamlit 处于同一领域竞争。尽管如此,正如我们将看到的,它是一个完全不同的工具。
在本教程中,我们将介绍 Taipy 的基本概念,并指导您创建一个 CO2 排放可视化应用程序。这个示例将展示如何使用 Taipy 构建一个交互式应用程序,通过分级色块地图及配套的图表和表格来可视化全球 CO2 排放数据。
最终的应用程序将呈现如下截图所示的样式。

仪表盘截图
如您所见,屏幕顶部有一个横幅标题和两个图表:左侧是分级色块地图,右侧是柱状图。分级色块地图显示了每个国家在某一年份的总 CO2 排放量,而柱状图则显示了所选国家的 CO2 排放量…
数据网格实现:加速从 ERP/CRM 系统中提取价值
从大型操作系统中启用快速数据开发
·发布于Towards Data Science ·阅读时间 7 分钟·2024 年 2 月 1 日
--

图片由Benjamin Zanatta提供,来自Unsplash
面对“怪物”时的挑战
对于从事务性系统(如 ERP(企业资源规划)和 CRM(客户关系管理))构建分析的数据显示工程师来说,主要的挑战在于如何弥合原始操作数据与领域知识之间的鸿沟。ERP 和 CRM 系统的设计和构建是为了满足广泛的业务流程和功能需求。这种通用性使得它们的数据模型复杂且晦涩,需要领域专家的参与。
更难以管理的是,在大型组织中,常见的设置是拥有多个这些系统的实例,并且某些底层过程负责在它们之间传输数据,这可能导致数据重复、不一致以及不透明。
操作团队沉浸在日常职能中,而从操作过程中生成的数据中提取业务价值的团队之间的脱节,仍然是一个显著的摩擦点。
搜索数据
想象一下,作为一名数据工程师/分析师,你的任务是识别公司内部的畅销产品。你的第一步可能是找到订单。然后你开始研究数据库对象,发现几个视图,但它们之间存在一些不一致之处,你不知道该使用哪个。此外,识别数据所有者也非常困难,其中一个数据所有者甚至最近已经离开公司。因为你不想在不确定的情况下开始开发,你决定直接处理操作原始数据。这听起来是不是很熟悉?
访问操作数据
我曾通过连接事务数据库中的视图或操作系统提供的 API 来请求原始数据。

订单快照存储在我自己的开发区域(图像由作者提供)
为了防止我的数据提取影响操作端的性能,我定期查询这些数据并将其存储在数据仓库中的持久化暂存区(PSA)。这让我能够使用这些快照执行复杂的查询和数据管道,而不会消耗操作系统的任何资源,但如果我没有意识到其他团队也在进行相同的提取,可能会导致数据的重复。
理解操作数据
一旦原始操作数据可用,我就面临下一个挑战:解读所有复杂的对象和属性,并处理它们之间数十种关系的迷宫(即 SAP 中的通用物料数据,leanx.eu/en/sap/table/mara.html)
尽管 ERP 或 CRM 系统中的标准对象有很好的文档记录,但我需要处理许多需要领域专业知识的自定义对象和属性,因为这些对象无法在标准数据模型中找到。大多数时候,我发现自己在不断进行‘试错’查询,试图对齐操作对象之间的键,根据属性的值来解释属性的含义,并通过操作界面截图验证我的假设。
数据网格中的操作数据管理
数据网格的实施改善了我在以下方面的体验:
-
知识:我可以快速识别暴露数据的所有者。所有者与生成数据的领域之间的距离是加速进一步分析开发的关键。
-
可发现性:共享数据平台提供了一个操作数据集的目录,形式为源对齐的数据产品,帮助我理解暴露数据的状态和性质。
-
可访问性:我可以轻松地请求访问这些数据产品。由于这些数据存储在共享数据平台中,而非操作系统中,我不需要与操作团队对接,争取在特定窗口期执行我的数据提取,从而避免影响操作性能。
源对齐数据产品
根据数据网格(Data Mesh)分类法,基于操作源构建的数据产品被称为源对齐数据产品:
源领域数据集密切代表了原始数据在创建时的状态,并未根据特定消费者进行调整或建模 —— Zhamak Dehghani
源对齐数据产品旨在在共享数据平台中以一对一关系表示操作源与操作实体,并且不应包含可能更改其任何属性的业务逻辑。
所有权
在数据网格实现中,这些数据产品应该
严格 由生成原始数据的业务领域拥有。该领域的负责人负责数据的质量、可靠性和可访问性,并将数据视为可以由同一团队及组织中其他数据团队使用的产品。
这种所有权确保了领域知识接近暴露的数据。这对于快速开发分析数据产品至关重要,因为其他数据团队所需的任何澄清都可以迅速且有效地处理。
实现
按照这种方法,销售领域负责发布“sales_orders”数据产品并将其提供到共享数据目录中。

销售订单数据产品暴露销售订单数据集(图片来自作者)
负责维护数据产品的数据管道可以这样定义:

数据管道步骤(图片来自作者)
数据提取
构建源对齐数据产品的第一步是从操作源中提取我们要暴露的数据。有很多数据集成工具提供了简化数据摄取的 UI,数据团队可以在这里创建作业,使用 JDBC 连接或 API 从操作源提取原始数据。为了避免浪费计算工作,并且在可能的情况下,只应增量地添加自上次提取以来更新的原始数据到数据产品中。
数据清洗
现在我们已经获得了所需的数据,下一步是进行一些整理,以便消费者不必处理真实源中的现有不一致性。尽管在构建源对齐数据产品时不应实现任何业务逻辑,但基本的清洗和标准化是允许的。
-- Example of property standardisation in a sql query used to extract data
case
when lower(SalesDocumentCategory) = 'invoice' then 'Invoice'
when lower(SalesDocumentCategory) = 'invoicing' then 'Invoice'
else SalesDocumentCategory
end as SALES_DOCUMENT_CATEGORY
数据更新
一旦提取的操作数据准备好供消费,数据产品的内部数据集将增量更新为最新快照。
数据产品的一个要求是可互操作。这意味着我们需要暴露全局标识符,以便我们的数据产品可以在其他领域中普遍使用。
元数据更新
数据产品需要是可理解的。生产者需要为所包含的实体和属性加入有意义的元数据。这些元数据应涵盖每个属性的以下方面:
-
业务描述:每个属性对业务的意义。例如,“销售订单的业务类别”。
-
源系统:与运营领域中的原始属性建立映射。例如,“原始来源:ERP | MARA-MTART 表 BIC/MARACAT 属性”。
-
数据特征:数据的特定特征,如枚举和选项。例如,“它是一个包含以下选项的枚举:发票、付款、投诉”。
数据产品还需要是可发现的。生产者需要将它们发布到共享的数据目录中,并通过定义输出端口资产来指明数据的消费方式,这些端口资产充当数据暴露的接口。
而且,数据产品必须是可观察的。生产者需要部署一组监控工具,可以在目录中显示。当潜在消费者在目录中发现数据产品时,他们可以快速了解数据的健康状况。
消费者体验
现在,再次想象你是一名数据工程师,负责识别公司内的畅销产品。但这次,假设你可以访问一个数据目录,其中提供代表每个领域实际情况的数据产品,这些领域塑造了业务。你只需在数据产品目录中输入“订单”,就可以找到销售数据团队发布的条目。只需一眼,你就能评估数据的质量和新鲜度,并阅读详细的内容描述。

数据目录中销售订单数据产品的条目示例(图片由作者提供)
这个升级的体验消除了传统发现方法中的不确定性,使你能够立即开始处理数据。更重要的是,你知道在需要进一步信息时,谁对数据负责。并且,每当销售订单数据产品出现问题时,你将收到通知,以便你提前采取行动。
结论
我们已经识别出通过来源对齐的数据产品启用运营数据的若干好处,特别是当它们由数据生产者拥有时:
-
精心策划的运营数据可访问性:在大型组织中,与来源对齐的数据产品代表了运营和分析领域之间的桥梁。
-
减少与运营工作的冲突:运营系统的访问被隔离在与来源对齐的数据产品管道中。
-
事实来源:一个公共的数据目录,列出了精心策划的运营业务对象,减少了组织内部的重复和不一致。
-
明确的数据所有权:与来源对齐的数据产品应由生成运营数据的领域拥有,以确保领域知识紧密接近暴露的数据。
根据我自己的经验,这种方法在大型组织面临不同领域之间的数据不一致性和在运营数据之上构建分析时遇到阻力的情况下非常有效。数据网格鼓励每个领域建立它们生成的核心实体的“真实来源”,并将其提供在共享目录中,允许其他团队访问这些数据并在整个组织中创建一致的度量标准。这使得分析数据团队能够加速工作,生成能够推动真实商业价值的分析。
参考文献
集中式数据湖存在问题。未来的数据网格需要领域、自助服务平台以及……
martinfowler.com](https://martinfowler.com/articles/data-monolith-to-mesh.html?source=post_page-----66ac65644fe2--------------------------------)
www.oreilly.com/library/view/data-mesh/9781492092384/
感谢我的 Thoughtworks 同事 Arne(两次!)、Pablo、Ayush 和 Samvardhan,感谢他们花时间审阅本文的早期版本
数据人员应对分析瘫痪的指南
一个简单且有效的框架,帮助你建立数据展示的信心
·发布于 Towards Data Science ·阅读时间 9 分钟·2024 年 2 月 14 日
--
“如果你不能简单地解释它,那说明你还没有完全理解它。” — 阿尔伯特·爱因斯坦
如果你是一个数据人员,你很可能在职业生涯中经历过在做数据展示时信心不足的各种阶段。我们在大学、研究生院和训练营中都接受过广泛的训练,学习如何进行数据挖掘并从复杂的数据集中提取洞察。按照书本上的标准,我们知道首先是数据清洗和准备,然后是数据探索,最后是挖掘和发现模式。但在现实世界中,当我们面临真正的商业问题时,常常会花费数小时怀疑自己的方法论,有时甚至怀疑整个前提。更糟糕的是,我们可能会陷入长时间的分析瘫痪中。为了帮助应对这种心态,我在此分享一个简单的框架。
注意: 在整篇文章中,我将使用“数据人员”这一术语,而不是交替使用数据分析师、数据科学家、数据专家、数据主题专家等术语。不论我们的职称如何,毕竟我们都是充满好奇心的人,拥有丰富的信息资源,试图利用手头的数据资源创造影响。

图片由作者提供
- 用你自己的术语框定商业问题:
作为一名数据人员,你将在职业生涯中学习许多数据处理和脚本语言。但理解业务问题并能够用简单的英语进行解释是一项更为重要的技能。数据人员拥有独特的能力,能够解构问题陈述并将其映射到可用的数据点,建立联系并找到答案。以下是一个例子:
来自利益相关者的问题:“我不知道为什么自上个季度以来,我的客户没有更多地转向订阅计划了。”
数据人员的解读:“我在寻找过去三个月选择订阅计划的客户数量相比单次购买数量的下降趋势。”
数据人员的分析计划:“我可以查看过去几个月新客户与回访客户的比例,并看看他们来自哪些获取渠道。我还应该检查我们的订阅流程中是否有任何页面的流失情况。我还需要检查在哪些地区出现了较高的流失率。”
2. 确定利益相关者
你的主要利益相关者是提出业务问题的人,但根据数据揭示的内容,可能还有其他一些利益相关者群体需要被包括在你的演示听众中。你的听众的数据素养水平也决定了你在演示中使用的语言。更重要的是,你需要确保这个群体愿意听取你的建议,这样你的数据工作才能在组织层面产生应有的影响。
3. 在深入数据之前准备好你的幻灯片
这看起来有些违背直觉且不合逻辑,但在你开始寻找组织数据时,准备好带有占位符的幻灯片将确保你在寻找所需数据点之前,已经有了一个初步的列表。这也会帮助你专注于问题陈述,而不会被你发现的其他有趣数据点所分心。如果出现了一些与问题陈述不完全对齐的有趣内容,你可以将它们保存在附录中。就我个人而言,我发现这种思维方式的转变非常具有变革性。在数据探索之前进行这一步骤,确保了:
-
你已经有了一个与问题陈述一致的数据故事,并且这个故事可以根据你从分析中得到的发现稍后进行修改。
-
这样可以避免你陷入过多的偏题和被新的、引人注目的数据事物分散注意力。(是的,丰富和清晰的数据集是一种特权,但当它让我们无法专注于目标时,也可能导致浪费时间的数据迷宫。)
这一步的更深入分析将在文章的后半部分进行说明。
4. 抵制将所有分析和方法论细节都纳入的冲动
在我看来,这是最困难的一步。不管问题多么简单,数据人员在缩小答案到一组特定数据点之前,都会花费大量时间进行数据准备和探索。可能花了你好几天或几周时间来准备一个干净的数据集,你的探索步骤可能涉及几个失败的假设和假定,才最终得出答案。新的、更有趣的关联可能已经出现,但如果这些信息对你呈现的数据故事没有任何有用的补充,请抑制将其包括在演示中的冲动。
5. 填充幻灯片
如果你已经完成了所有前面的步骤,插入数据和发现到幻灯片中应该会变得非常简单。
我喜欢按问题、问题的影响、发现、建议和下一步的顺序安排它们。这能确保你的数据故事容易跟随,即使你不在场讲解和浏览整个幻灯片,它也能自我说明。
在其他应用场景中,如例行性能更新或回顾性总结,顺序可能会有所不同。但对于需要数据洞察来辅助决策的典型请求,这个顺序是一个很好的起点。确保这一步的关键是拥有准确且相关的数据点,能够清晰地支持你的故事。除此之外,我还有一些小建议,帮助你将分析解决方案整洁地收尾。这些小细节在演示过程中能起到重要作用,帮助观众记住关键洞察。
-
清晰地标明共享的关键数据点是好兆头还是坏兆头,可以通过使用箭头和颜色来表示。(例如:低跳出率是好事,而低转化率是坏兆头。)
-
始终为幻灯片中共享的任何数字(数据点)添加背景信息,包括基准细节或趋势分析。(例如:本月的转化率为 12%,这一数据与同一产品线的其他 SKU 一致,且高于过去三年同月的平均转化率。)
-
将洞察与每个幻灯片中原始业务问题的某个部分联系起来。
-
在脚注中包括诸如样本大小、分析时间框架和重要注释等细节将有助于建立信任和信誉。
除了这些,找到创造你自己数据讲故事风格的方法将帮助你的工作在所有其他模板化的分析报告中脱颖而出。
你不能总是为你制作的演示文稿加水印,但你通过上述技巧建立的讲故事风格和信誉将帮助人们将你与数据卓越联系起来。
6. 记录学习
提升的简单有效方式就是通过失败并记录下所学的经验。随着你获得更多展示数据工作的机会,别忘了记录下哪些做法有效,哪些无效。很快,你将会根据你的观众和利益相关者群体,拥有一份关于可视化、分析和建议的类型清单。这个清单越大,你的数据演示就会变得越有影响力。
在我作为分析实习生的最初几周,我感到非常不自在,因为我在迎新期间结识的大多数同伴都选择了与数据科学完全无关的轮岗方向。这是 2012 年,所以我当时并不懂得将自己的感受标记为“冒名顶替综合症”,但我在 One Note 上创建了一个小部分,记录了所有我的分析被好评的情况,以及那些我需要花大量时间通过电子邮件反复解释幻灯片内容的情况。这项练习帮助我意识到:
- 如果你与组织的优先事项没有对齐,那么你(作为数据人员)所兴奋的内容,很可能与观众所兴奋的内容不同。
(差不多十年前,我花了几周时间追踪一个数据源,最终让我能够连接两个不同产品线的记录,并进行相似度建模。当时我非常兴奋,花了好几页幻灯片讲述这项技术的伟大,并投入时间进行分析。但最终,由于它与公司当季的优先事项没有直接关联,听众无法共享我那种兴奋感。现在回想起来,我真想给当时的初级分析师自己盖上一条毯子,告诉他要放远视角,关注业务简报。)
- 你的分析演示的观众不仅仅是会议中的人,还包括那些将会在组织内转发你演示的人。一份以数据为支撑、清晰简洁的观察和建议的演示本身就能说服他人,并且有潜力触及更多决策者。
我在初级分析师阶段记录的这两项经验,已经陪伴我超过十年。从一个简单的习惯开始,它如今已经变成了一个针对我在组织中不同观众的个人规则清单,帮助我做出更有影响力的演示。
让我们通过一个示例场景来实践这个框架
让我们看一个假设的业务问题,以及使用或不使用这些策略时分析解决过程的不同。
业务问题: 一个电子商务办公用品商店的桌面和移动网页版体验存在差异。(假设这是 2000 年代末,那时响应式网页设计还不是主流。)你部门的领导在考虑应该优先重设计哪一部分,并且这个决定是否会影响收入指标。
新手数据人员版:

-
数据人员接手问题。
-
搜索公司数据仓库,找出包含每次网站访问详细信息的数据源。他们找到了一个丰富的数据集,包含大约 30 个属性。
-
他们花时间探索数据集,发现了几个新的亮眼数据属性。
亮眼的事物 1:移动设备品牌——他们产生了兴趣,并寻找按移动设备品牌的购买模式。偷偷地对这些品牌作出评判。(不需要翻白眼,我们都做过。)
亮眼的事物 2:屏幕分辨率——他们对屏幕分辨率是否会影响用户互动指标产生了兴趣。
亮眼的事物 3:移动设备品牌——他们对是否某些设备品牌的“购买时间”较短产生了兴趣。
亮眼的事物 4:人口统计数据——他们对通过七个不同的用户属性来查看购买模式产生了兴趣。
-
准备演示文稿:继续在幻灯片中包含所有的学习成果和分析方法。
-
由于未提供明确的建议,业务部门有更多后续问题。对于每个后续问题,从第 1 步重新开始。
资深数据人员的版本:

-
数据人员接手问题。
-
设置演示文稿的顺序如下:
幻灯片 1:业务问题,决策受影响的因素,相关利益相关者
幻灯片 2:数据可视化展示了桌面与移动设备访问占比,以及桌面与移动设备转化占比。
幻灯片 3:数据可视化展示了新客户与回访客户的占比,按每种设备类型进行展示。
幻灯片 4:数据可视化展示了同一用户在后续访问中使用的设备。
幻灯片 5:学习与建议的占位符
-
确定数据源和填充幻灯片所需的属性。
-
填充幻灯片:基于幻灯片 1-4 中的观察,数据人员说明回访用户占客户基数的主要部分。超过 70% 的回访订单是在桌面设备上下单的。虽然首单通常是在移动设备上下单,但再订购请求通常是通过桌面处理的。因此,保持过时的桌面体验会影响大部分忠实客户。这帮助领导层得出结论,必须优先考虑桌面重设计工作。
结论:
总结:有效的数据讲故事就是让复杂的事情看起来简单,这样你的数据技能就能够在组织中推动决策制定,尽管不同的成员数据素养水平不同。
关于作者:
Nithhyaa Ramamoorthy 是一位数据领域专家,拥有超过 12 年的分析和大数据经验,尤其专注于医疗健康与消费者行为的交集。她拥有信息科学的硕士学位,并且最近获得了 CSPO 证书以及其他多个认证。她热衷于运用自己的分析技能推动商业决策,创建根植于同理心的包容性和公平的数字产品。她目前在梅奥诊所——数字健康中心担任分析主管。
数据科学领导者确保每个项目推动业务价值的指南
来自管理一个 8 人团队的人的经验
·发布于 Towards Data Science ·阅读时间:8 分钟·2024 年 8 月 22 日
--

图片由 OpenAI ChatGPT 生成
大多数数据科学家自然倾向于数据科学中有趣的部分——开发一个技术先进、复杂的机器学习模型。然而,许多数据科学经理在模型的技术设计上投入了过多时间,却没有投入足够的时间去深入理解模型所要解决的业务问题。因此,技术上成功的项目被部署到生产环境中,却未能实现预期的业务价值,并最终进入我所称之为数据科学墓地的地方。作为一个大团队的数据科学经理,我已经将更多的项目埋葬在那个墓地里,甚至不愿承认。然而,这些经历教会了我宝贵的经验,让我明白了如何确保数据科学项目能够创造真正的业务价值。在本文中,我将分享四个重要的经验教训,帮助数据科学经理确保项目产生明确且有意义的业务影响。
#1: 将项目与公司业务目标对齐
大多数公司,甚至可以说所有公司,都会在一定周期内共享战略目标和计划。一个例子目标可能是……
一项面向日全食观察者的数据科学项目
快速成功的数据科学
记录太阳光曲线
·发布于Towards Data Science ·6 分钟阅读·2024 年 3 月 26 日
--

2017 年日全食期间,在全食前的钻石戒指效应(作者拍摄)
我只需开车几个小时就能到达今年日全食的路径,当人们说他们要呆在家里,因为“95%的太阳会被遮挡,这已经足够了”时,这让我非常抓狂。
我可以向你保证,这绝对不够。远远不够。我在 2017 年看过日全食,虽然我不会称其为“改变人生”,但它无疑是我一生中的伟大时刻之一。
事情是这样的:太阳比满月亮度要亮 480,000 倍。当 95%的太阳被遮挡——即使考虑到边缘变暗——它仍然比月亮亮超过 7,000 倍!这与阴天的亮度相当。

“边缘变暗”指的是太阳的边缘比中心更暗(来源于国家太阳天文台(CC BY 4.0))
而且由于全食大约需要 75 分钟才能到达,眼睛的瞳孔会自然放大以适应逐渐变暗的光线。在日全食的大部分时间里,你几乎无法察觉到它的发生。
当太阳完全被遮挡时,全食的黑暗程度是阴天的 200 倍以上。气温骤降。人们惊叹不已。鸟儿停止歌唱。你的脖毛竖立……
数据科学家指南:相关方
数据科学家如何与非数据科学人员有效沟通
·发表于 Towards Data Science ·阅读时长 7 分钟 ·2024 年 9 月 26 日
--

图片来源:Campaign Creators 在 Unsplash
数据科学家与相关方的关系
一个相关方是指任何对项目或业务的决策或活动有直接利益关系的人。可以是公司内部的同事,也可以是外部客户。
每当数据科学家在公司进行项目时,他们都不是孤立工作的。这个项目总是有一个商业目的。
例如,如果你作为数据科学家正在开发一个可以预测特定商业建筑能耗的模型,那么相关方可能包括:
-
建筑物的业主/经理
-
在现场工作并参与项目的工程师
-
你组织中的同事,他们将向客户交付结果(例如,负责创建仪表盘的软件工程师或 BI 分析师)
大多数相关方并没有数据科学背景。他们可能是商业分析师、销售人员或工程师,对机器学习、人工智能和数据科学的理解程度各不相同。
尽管如此,每当需要数据科学解决方案来支持一个项目时,这些“非技术性”(缺乏更好的描述词)相关方就发挥了重要作用……
数据科学家的一天
我到底整天都在做些什么呢?
·发表于Towards Data Science ·阅读时长 7 分钟·2024 年 6 月 8 日
--

图片由ThisisEngineering提供,来自Unsplash
尽管数据科学是近年来增长最快的职业之一,但在我日常生活中,很多人仍然不太明白我做什么(从而也不理解数据科学家通常都做些什么)。
甚至我在自己的工作场所也有人多次问我,“你到底是做什么的?”
对于那些不完全理解数据科学家做什么(但有兴趣了解)的人,或者那些对这个职业感兴趣但不确定是否适合的人,我想提供一些关于数据科学家日常工作内容的见解。
检查我的模型

图片由Luke Chesser提供,来自Unsplash
通常我早晨做的第一件事之一(除了查看 Slack 消息和电子邮件)就是查看我的建模仪表板。
仪表板通常会显示一段历史数据,并将预测值一起绘制出来,以便您查看预测结果的表现。此外,还会显示下一时间段的预测结果。
2024 年数据分析顾问的一天
你想了解的关于数据专业人士的一天的一切
·发布于 Towards Data Science ·阅读时长:6 分钟·2024 年 2 月 6 日
--

由 Christin Hume 摄影,图片来自 Unsplash
嗨!我是 Rashi,一名来自芝加哥的数据分析顾问,自 2021 年以来一直在健康保险行业工作。
我加入公司时是数据分析师,逐步晋升为数据分析顾问。我在研究生期间(2020–2021)实习了一年,但这是我第一次做 9 到 5 的工作,在这个充满多才多艺的专业人士的环境中,找到自己的位置让我非常紧张。
工作三年左右,我已经为自己开辟了一个良好的位置,带来了独特的优势和技能,也形成了一套随着时间打磨的完美 9 到 5 工作日常。每一天的工作内容都很不同,这也是我喜欢这份工作的原因——没有两天是相同的,这让我远离了无聊和厌倦。
说到我的工作日看起来不一样,我每天做的事情、使用的工具和进行的对话有很多共同点,因此,我可以写下这篇关于作为数据分析顾问的一天的博客。
让我们窥探一下我生活中的星期四(通常是我一周中最忙的一天)
[07:00 AM]:起床 & 闪亮登场
深入探讨赔率比
赔率比系列深度解析第一部分
了解、计算、可视化和解释赔率比及其置信区间,并通过 Python 中的实际示例进行演示。
·发布于Towards Data Science ·阅读时间:17 分钟·2024 年 9 月 24 日
--

图片来源:Christian Stahl在Unsplash
你可能听过类似这样的一句话:“吸烟者患肺癌的可能性是非吸烟者的五倍。”尽管这个例子并非来自真实的研究,仅用于说明,但它为我们讨论赔率比提供了一个很好的出发点。
还不是 Medium 会员?继续使用此免费版!
赔率比是一个相对简单的概念,实际上也很容易解释。在本文中,我们将讨论赔率和赔率比,提供清晰的逐步计算解释,并展示如何使用 Python 中的 SciPy 库进行快速计算。最后,我们将探讨如何有效地传达和解释赔率比。
什么是赔率比?
我使用上述例子是因为赔率比在临床研究中有着重要应用,用于量化暴露与结果之间的关系。顾名思义,赔率比比较了事件(或疾病)发生的赔率……
深入探讨 LIME 的局部解释
本地可解释模型无关解释(LIME)的直觉、理论和代码
·发表于Towards Data Science ·13 分钟阅读·2024 年 6 月 26 日
--

(来源:DALL.E)
LIME 是 XAI 方法的始祖。它使我们能够理解机器学习模型是如何工作的。具体来说,它可以帮助我们理解个别预测是如何做出的(即局部解释)。
尽管近年来的进展意味着 LIME 的受欢迎程度有所下降,但它仍然值得理解。这是因为它是一种相对简单的方法,并且对于许多可解释性问题来说“足够好”。它也是一种更近期局部可解释性方法——SHAP 的灵感来源。
所以我们将:
-
讨论 LIME 为获得局部解释所采取的步骤。
-
详细讨论与你选择的步骤相关的一些决策,包括如何为样本加权以及使用哪种代理模型。
在此过程中,我们将把该方法与 SHAP 进行比较。这是为了更好地理解它的弱点。我们还将看到,尽管 LIME 是一个局部方法,我们仍然可以聚合 LIME 的权重来获得全局解释。这样做将帮助我们理解该包所做的一些默认选择。
深入探讨使用逻辑回归的优势比
《深入探讨优势比系列》第二部分
本文是关于如何使用 Python 和 Statsmodels 从逻辑回归模型中提取和探索优势比(odds ratios)的全面指南,并附带示例。
·发表于 Towards Data Science ·阅读时间:18 分钟·2024 年 10 月 8 日
--

当我们构建一个统计模型时,我们通常更关注它的预测价值。然而,我们也可以利用它揭示数据背后的故事。
还不是 Medium 会员?继续使用这个 免费版本****!
逻辑回归是最简单也是最有效的二分类模型之一。除了预测外,我们还可以从拟合的逻辑回归模型中获取每个变量的优势比,这对于我们理解数据至关重要。
在本文中,作为《深入探讨优势比系列》第一篇文章的延续,我们将探讨如何从逻辑回归中提取优势比。我们将首先推导模型与优势比之间的关系。然后,我们将研究一些使用逻辑回归方法的案例,这些案例展示了它在计算优势比方面比基本方法的优势,特别是在:对分类变量和数值变量的计算、处理多个变量时的优势等。
人机交互(HCI)与设计研究论文发表指南
将“发表或消亡”转变为“学习、写作和分享”
·发表于Towards Data Science ·阅读时长 9 分钟·2024 年 12 月 15 日
--
当我三年前开始我的博士学位时,我对学术界以及在期刊和会议上发表论文的过程都非常陌生。由于我来自计算机工程领域,我的博士研究涉及设计研究,我感觉在人机交互、设计、设计工程、设计研究以及其他相关领域的发表过程,比计算机科学或其他纯粹的 STEM 学科更加模糊。
尽管我当时还不懂很多,但有一件事很快变得清晰,那就是许多人将发表论文与作为研究者的成功联系在一起。发表似乎很重要,甚至是关键,不仅对博士学位至关重要,对我作为学术研究者或研究人员的职业生涯也是如此(即使是一些行业岗位也如此)。
现在,三年过去了,拥有七篇第一作者的发表论文的我,来分享我在第一年时希望自己知道的所有事情。

2022 年,我在荷兰埃因霍温的 ACM 对话式用户界面(CUI)会议上展示了我的研究成果。
发表到底是什么?
我这里所说的“发表”是指将论文在期刊或会议上被接受。这通常涉及到同行评审过程,也就是说两位或更多的评审人员会阅读并批准你的论文。这些评审人员通常是你所写论文主题的‘专家’,这可能意味着他们是学者、研究人员、行业专家等。这个过程通常是盲审的,也就是说你的名字和机构会对评审人员保密,以避免任何偏见。
论文只能提交到一个会议或期刊,如果被拒绝或撤回,才能重新提交到第二个地方。有时,描述研究的期刊论文可以作为会议论文重新使用,反之亦然。这通常是在同一研究/数据提出显著不同的角度或扩展的情况下,否则通常一个研究 = 一篇论文。
我可以发布什么?
通常,在人机交互和设计研究领域,论文将属于两种类型之一:
综述论文: 这通常是一个系统的文献综述,你只是分析已经存在的某一主题的文献。这涉及到系统性(即,遵循一个已知的过程,如PRISMA,以确保你已经收集了该主题下所有的文献)和进行一些分析(即,不仅仅是说“这些是现有的文献”,还要从这些文献的整体组合中寻找可以学习的趋势或模式)。
你可以在这里查看一个示例,那里我分析了所有与不同利益相关者共同设计对话式人工智能的文献。我不仅收集并展示这些文献,还分析了不同的因素,例如涉及了哪些利益相关者以及他们使用了哪些协作设计活动。
这种类型的论文通常只会在期刊上接受,较少在会议上接受,后者通常更侧重于下面提到的第二种类型。

图片由Kelly Sikkema提供,来源于Unsplash
实证论文: 这是一篇你进行了新研究并展示结果的论文。在我提到的领域中,这通常涉及人类参与者,包括调查、访谈、研讨会、民族志等方法。你会先介绍背景和概述文献综述,为你的研究提供背景和上下文,然后总结你使用的方法,展示结果并进行讨论。
这种类型的论文示例可以在这里找到。我采访了构建对话式人工智能的专业人士,以了解他们的经验和困难,然后总结并分析这些内容,与其他类似的论文进行对比,这些论文也采访了其他人工智能专业人士。对于更定量的工作,你可以查看我的指南,如何进行设计研究的统计分析。
这种类型的论文通常在会议和期刊上都会被接受。

由Vanilla Bear Films拍摄,图片来自Unsplash
如何发布论文?
1. 做好研究工作!
记住,你做博士研究或从事研究工作并不是为了发表论文。你的目标是做有影响力的高质量研究,出版只是这一过程的结果,并且是与你的广泛社区分享你工作的方式。 花时间明确你想要研究的内容,并制定一套严谨的方法论。
对于博士学位,我建议将其分解为一系列能够回答小问题的研究,这些研究最终会帮助解答你的主要研究问题。从这个角度来看,每项研究都可以作为独立的论文发表(这正是我所做的)。同时,尽早写下每个研究的论文非常有帮助,无论是研究进行中还是刚完成时写,这样一切都能及时记录清楚,等到写论文时会节省大量时间。
另一种获得出版物的方式是与其他研究人员合作参与他们的项目和研究。这可能不会让你获得第一作者的身份,因为第一作者通常是做了大部分工作的那个人,但它可以增加你的整体出版数量,并展示你与他人的合作能力。根据我的经验,你的第一作者文章是最具影响力的,其他作者通常按贡献大小排序,最后一个作者通常是研究的导师或最资深的人员。

由Jo Szczepanska拍摄,图片来自Unsplash
2. 确定出版场所
下一步是选择你希望发表工作的地方。以下是一些有影响力的设计期刊和会议,但一个好的经验法则是查看你在论文中引用的文献来自哪里。如果你发现自己多次引用了某个期刊或会议,这通常是你研究可能适合在那里发表的一个良好指标。否则,检查不同出版场所的影响因子也非常重要。这个数字表示在该出版场所发表的论文有多么有影响力(这是引用次数和其他指标的综合)。一般来说,设计和人机交互领域的影响因子低于医学领域,后者的论文通常影响力更大。根据我的经验,设计领域影响因子 3 算是相当不错了。
与此同时,如果你有参会的目标,必须查看会议的截止日期。虽然期刊通常全年接受投稿,但会议会有投稿截止日期,这可能与你的计划相距太远。
同样值得记住的是,影响力分数越高或者一个会议的声望越大,想被接受的竞争也会越激烈!
优秀的设计/人机交互期刊
《设计研究》, 《ACM 人机交互学报》, 《共同设计》, 《国际设计期刊》。
优秀的设计/人机交互会议
ACM 计算机系统人因会议(CHI)、交互系统设计会议(DIS)、设计研究学会(DRS)会议、DESIGN、国际人机交互理论与应用会议(HUCAPP)、ACM 社会公益信息技术会议。
根据你的研究领域,还可以查看更多特定领域的期刊和会议。例如,《AI 伦理学》和《AI 与社会》是很好的 AI 专注期刊,而在医学/医学研究领域也有其他优秀的期刊。

3. 撰写论文
下一步是撰写论文本身。通常,如我之前提到的,这将包括引言、文献综述或背景部分、方法论部分、结果、讨论和结论。查看你所选择的期刊或会议中的论文,并遵循他们使用的格式也非常重要。
尽可能多地从同事和导师那里获取反馈,不要害怕删掉某些部分并重新开始!写一篇好的论文需要时间,你不应该急于完成。

摄影:由Annie Spratt拍摄,来源:Unsplash
4. 提交并等待
按照会议的指南提交你的工作,并等待决定。记住,撰写论文并提交本身就是一个你应该庆祝的里程碑!审稿过程很长,可能需要几个月的时间。在规划出版物时,尤其是如果你希望在完成博士学位前发表它们,必须牢记这一点。审稿人对你论文的不同决定会因会议而异,但通常会有以下几种:
✅ 接受 — 无需进一步修改即可接受。
⏳ 接受并进行小幅修改 — 如果你根据审稿人给出的反馈进行一些修改,通常会给你一个月左右的时间来完成。
⚠️ 接受并大幅修改/修改后重新提交 —— 这意味着评审认为你的论文有潜力和价值,但需要进行重大修改。这可能意味着需要重做某部分研究或收集更多数据,或者可能意味着需要对论文本身进行大幅重写。通常你会有大约 3 个月的时间来完成这些修改。根据我的经验,这个决定是件好事。这意味着论文并没有致命的缺陷,不需要被拒绝。通过一些努力和一封好的回复信,详细解释你如何根据评审反馈进行修改,你就能成功升级为接受或接受并作小修。
🛑 拒绝 —— 这意味着论文存在致命缺陷,或与所选的会议完全不相关。我也曾经历过完全被拒的情况,这种情况确实让人沮丧。一个好的起点是查看评审的详细反馈,了解如何改进研究或论文。我曾经遇到过被拒绝但最终被接受的情况,所以不要因为拒绝而轻易放弃工作!
5. 不要惊慌!
拒绝是正常的,迭代是正常的,这都是过程的一部分!我大部分的论文都是在博士第二年末和第三年期间完成的。如果你刚刚开始,不妨把出版作为一个长期目标,首先专注于合理安排学位结构,做好各项研究任务。提交不同研究的论文也是一个获取反馈和及早发现问题的好方法,而不是等到答辩/口试时才遭遇麻烦,或者当已经无法重复某项研究时才发现问题。将其视为一个学习和获取反馈的机会,你会发现提交论文总是有益的,即使结果是暂时的拒绝。

要想写出一篇被接受的论文,需要经过大量的草稿和迭代!
我的定位
我是帝国理工学院的博士生,已进入最后一年。我的博士项目让我开发了一个框架和工具包,用于协作设计更符合人类价值观的对话式人工智能。
你可以查看我项目的官方页面,访问帝国理工学院官网。你也可以阅读我写的另一篇文章,解释我的博士项目的细节。
我创建了这个 Medium 账号,用于发布我在攻读博士项目过程中学到的有趣经验和研究摘录。我的目标是以一种任何人都能理解的方式传播有关我的工作的信息。
Claude Sonnet 3.5 系统提示的法医分析
Claude 3.5 Sonnet 伪造物对于结构化输出(如代码生成)就像向量检索对于 RAG 一样。
·发表于Towards Data Science ·7 分钟阅读·2024 年 6 月 24 日
--

引入伪造物
结构化输出生成的一大进步。
这篇文章分析了Claude 3.5 Sonnet系统提示的生成。本文末尾提供了该分析代码的链接,文章与源代码一同发布。该分析的主要部分涉及伪造物的引入,以及这一过程如何作为 Anthropic 目前在代码生成和其他任务中超越所有 LLM 基准的分类与检索系统的一部分。
“伪造物用于包含大量、可独立使用且用户可能修改或重用的内容。”
伪造物是一种范式转变,因为它正式化了一个新概念:持久数据的概念。伪造物是 LLM 将要完成的每项任务的基础对象或示例。通过持久化数据,我们能够进行迭代并记住变化。
通过提供固定的参考,我们解锁了逐步改进和精炼输出的能力。这是朝着控制冗长 LLM 输出的短暂性质并生成标准化输出的一步。
深度学习中的非线性全新视角
我们需要非线性激活函数的传统理由只是这个故事的一个维度。
·发布于Towards Data Science ·阅读时间 8 分钟·2024 年 8 月 15 日
--
softmax、ReLU、sigmoid 和 tanh 函数有什么共同点?它们都是激活函数——而且它们都是非线性的。但我们为什么一开始就需要激活函数,特别是非线性激活函数呢?有一种传统的解释,也有一种新的看法。
传统的理由是:如果没有非线性激活函数,一个深度神经网络仅仅是矩阵乘法和加偏置的组合。这些是线性变换,你可以通过线性代数证明,线性变换的组合仍然是线性变换。
所以无论我们堆叠多少线性层,没有激活函数,我们的整个模型也不过比线性回归更差。它将完全无法捕捉到非线性关系,甚至是像 XOR 这样简单的关系。
引入激活函数:通过允许模型学习一个非线性函数,我们获得了建模各种复杂真实世界关系的能力。
这个你可能已经很熟悉的故事完全正确。但任何话题的研究都能从不同的角度受益,尤其是深度学习,其具备许多可解释性挑战。今天,我想和你分享一种看待激活函数需求的全新方式,以及它揭示的深度学习模型内部工作原理。
简而言之,我想和你分享的是:我们通常构建深度学习分类器的方式会在模型中引入 归纳偏差。具体来说,使用线性层作为输出意味着模型的其余部分必须找到输入的 线性可分 转换。这背后的直觉非常有用,所以我将分享一些例子,希望能澄清这些术语。
传统解释
让我们通过一个例子来重新审视传统的非线性激活函数的理由。我们将看一个简单的案例:XOR。

一个带有彩色真实值的 XOR 函数图。背景颜色代表线性回归的预测值。图片由作者提供。
在这里,我训练了一个线性回归模型来拟合 XOR 函数,输入为两个二进制值(真实值以点的形式绘制)。我将回归的输出作为背景颜色绘制。回归模型什么也没学到:它在所有情况下都预测为 0.5。
现在,我将训练一个非常基础的深度学习模型,使用 MSE 损失函数。只有一个具有两个神经元的线性层,接着是 ReLU 激活函数,最后是输出神经元。为了简化起见,我只使用权重,不使用偏置。

我们基本神经网络的示意图。由作者使用 draw.io 制作。
现在会发生什么?

另一个 XOR 函数的图,这次是用一个简单的深度学习模型的预测结果绘制的。图片由作者提供。
哇,现在完美了!权重看起来怎么样?
Layer 1 weight: [[ 1.1485, -1.1486],
[-1.0205, 1.0189]]
(ReLU)
Layer 2 weight: [[0.8707, 0.9815]]
所以对于两个输入 x 和 y,我们的输出是:

这真的很像

你可以验证,这是对于输入 x 和 y 在 {0, 1} 中的 XOR 函数。
如果没有 ReLU,我们可以将模型简化为 0.001y - 0.13x,这是一个完全不起作用的线性函数。所以就是这样,传统的解释:由于 XOR 是一个本质上非线性的函数,它不能被任何线性函数精确建模。即使是线性函数的组合也不行,因为那仍然是另一个线性函数。引入非线性 ReLU 函数 让我们能够捕捉到非线性关系。
深入挖掘:归纳偏差
现在我们将继续研究相同的 XOR 模型,但我们将通过不同的视角来观察它,以更好地理解该模型的 归纳偏差。
什么是归纳偏差?面对任何问题,都有许多解决方法。本质上,归纳偏差是模型架构中内建的一种特性,使得模型倾向于选择某种特定的方法来解决问题,而不是其他方法。
在这个深度学习模型中,我们的最终层是一个简单的线性层。这意味着,除非模型在最终层之前的输出能够通过线性回归求解,否则模型完全无法工作。换句话说,输出前的最后一个隐藏状态必须是线性可分的,模型才能正常工作。这种归纳偏差是我们模型架构的特性,而不是 XOR 函数的特性。
幸运的是,在这个模型中,我们的隐藏状态只有两个神经元。因此,我们可以在二维空间中进行可视化。它看起来是什么样子?

XOR 函数的输入表示通过深度学习转化为隐藏表示(经过一层线性层和 ReLU 激活)。背景颜色表示线性回归模型的预测。图片来源:作者。
如我们之前所见,单独的线性回归模型对 XOR 输入并不有效。但是,一旦我们将输入通过神经网络的第一层和 ReLU 激活函数,输出类别就可以被一条线整齐地分开(线性可分)。这意味着线性回归现在可以起作用,实际上我们最终的层只是在执行这种线性回归。
那么,这告诉我们关于归纳偏差什么信息呢?由于我们最后一层是一个线性层,因此该层之前的表示必须至少大致是线性可分的。否则,作为线性回归功能的最后一层将会失败。
线性分类器探针
对于 XOR 模型,这看起来可能只是我们之前看到的传统观点的一个微不足道的扩展。但是,如何处理更复杂的模型呢?随着模型的加深,我们可以通过这种方式观察非线性,从而获得更多的洞察。这篇论文由 Guillaume Alain 和 Yoshua Bengio 撰写,探讨了这个思想,并使用线性分类器探针进行研究。[1]

“左侧的十六进制转储包含的信息内容比右侧的图像多。只有其中之一可以被人脑处理,以便及时挽救他们的生命。计算的便利性很重要。不仅仅是熵。” 图示和说明来自 Alain & Bengio, 2018(链接)。[1]
对于像 MNIST 手写数字这样的许多情况,做出预测所需的所有信息已经存在于输入中:只是需要处理这些信息。Alain 和 Bengio 观察到,随着模型的加深,我们实际上在每一层的可用信息减少,而不是增加。但好处是,在每一层,已有的信息变得“更容易使用”。我们所指的是,每一层之后,信息变得越来越线性可分。
我们如何找出每一层之后模型的表示有多线性可分?Alain 和 Bengio 建议使用他们所称的线性分类器探针。其想法是在每一层之后,我们训练一个线性回归模型,使用该层的隐状态作为输入来预测最终输出。
这实际上就是我们在最后的 XOR 图中所做的:我们在倒数第二层的隐状态上训练了一个线性回归模型,发现这个回归成功地预测了最终输出(1 或 0)。在数据不线性可分时,我们无法使用原始输入来完成这一任务。记住,最后一层本质上是线性回归,因此在某种意义上,这种方法就像是在模型中提前创建了一个新的最终层。
Alain 和 Bengio 将这一方法应用于一个在 MNIST 手写数字数据集上训练的卷积神经网络:在每次卷积、ReLU 激活和池化之后,他们都加入了一个线性探针。他们发现,测试误差几乎总是从一个探针到下一个探针时减少,表明线性可分性在增加。
为什么数据会变得线性可分,而不是“多项式可分”或其他形式的可分?因为最后一层是线性的,我们使用的损失函数会迫使模型中的其他层一起工作,并为最终层创造一个线性可分的表示,以便进行预测。
这个想法是否同样适用于大型语言模型(LLM)?事实上,是的。Jin 等人 (2024)使用线性分类器探针展示了 LLM 如何学习不同的概念。他们发现,简单的概念,例如某个城市是否是某个国家的首都,在模型的早期阶段就变得线性可分:仅需少数几个非线性激活就能建模这些关系。相比之下,许多推理技能直到模型的后期才变得线性可分,或者对于较小的模型来说完全无法线性可分。[2]
结论
当我们使用激活函数时,我们将非线性引入到深度学习模型中。这当然是很重要的知识,但通过从多种角度解读线性和非线性的后果,我们能获得更多的价值。
上述解释是从整体上看待模型,但一个有用的思维模型集中在深度学习模型的最终线性层上。由于这是一个线性层,之前的所有内容必须是线性可分的;否则,模型无法正常工作。因此,在训练过程中,模型的其他层会共同协作,找到一个线性表示,供最终层用于预测。
对于同一事物,拥有多种直觉总是有益的。在深度学习中尤为如此,因为模型可能是如此的黑箱化,任何能够提升可解释性的技巧都很有帮助。许多论文都应用了这一直觉,取得了令人着迷的结果:Alain 和 Bengio(2018)利用这一直觉提出了线性分类器探测的概念,而 Jin 等人(2024)在此基础上,逐层观察语言模型中越来越复杂的概念发展。
希望这个新的非线性思维模型对你有所帮助,并且你现在能够为黑箱深度神经网络带来更多的启示!

图片来源:Nashad Abdu via Unsplash
参考文献
[1] G. Alain 和 Y. Bengio,使用线性分类器探测理解中间层(2018),arXiv
[2] M. Jin 等人,探索概念深度:大规模语言模型如何在不同层次上获取知识?(2024),arXiv
分布式推理的DCIN简介

图片来源:Shubham Dhage于Unsplash
分布式协作智能网络如何运作
·发表于 Towards Data Science ·阅读时间 25 分钟 ·2024 年 10 月 29 日
--
引言
我们正站在人工智能和 Web3 领域新时代的门槛上。
传统上,人工智能推理一直是集中式数据中心和高性能计算集群的领域,只有少数人能够访问。即便在许多去中心化推理网络中,节点运营商仍然必须依赖高端硬件,才能赚取奖励,实际上只是为了弥补开支。
这不是民主化人工智能的访问:大多数用户无法积极参与推理阶段,因为 GPU 的成本过高,而那些希望拥有适度去中心化或隐私保护的客户则被困在非常慢或昂贵的解决方案中。
在过去的几个月里,我和我的团队一直在努力开发一项希望能够在人工智能和 Web3 的交集领域树立标杆的技术:DCI 网络。
但在讨论这个问题之前,让我们先回顾一下我们为何会陷入这种境地。
一些背景
神经网络正如其名,是由人工神经元组成的网络,按层次组织…
《走得太远:图形 RAG 并不需要每个图形工具》
不要用图形数据库(DBs)、图形查询语言(QLs)或图形分析来让事情变得复杂。
·发布于Towards Data Science ·12 分钟阅读·2024 年 10 月 18 日
--

知识图谱的冒险:迷失在无尽的文档中。由 Brian Godsey 使用 DALL-E 生成。
当 RAG 开发者决定尝试图形 RAG——即构建一个知识图谱并将其集成到他们的 RAG(检索增强生成)系统中时,根据互联网的信息,他们面临着大量的选择和决策。有很多文章、指南和操作手册介绍了如何使用图形 RAG 以及图形的其他工具。所以,一些开发者直接跳入其中,认为他们需要集成和配置一长串图形工具和技术,才能正确实现图形 RAG。在寻找如何入门时,通常会找到一些文章,建议你需要以下的一些或所有内容:
-
知识图谱——连接语义搜索无法捕获的关键术语和概念
-
关键词和实体提取工具——用于构建知识图谱
-
图形遍历算法——用于探索图中的连接
-
属性图实现——用于丰富图形结构和遍历方法
-
图形数据库(DBs)——用于存储和交互图形,以及高级图形分析
-
图形查询语言(QLs)——用于对图形节点和边进行复杂查询
-
图形节点嵌入算法——用于将图形对象嵌入可搜索的向量空间
-
向量存储——用于存储和搜索嵌入语义向量空间中的文档
当然,可以说这些工具和实现对特定的图形使用案例非常有帮助。但对于任何开始典型图形 RAG 使用案例的开发者来说,简单的事实仍然是:大多数“图形”工具在生成式 AI 革命之前就已经设计和构建完成了。 生成式 AI 使用案例与传统的图形使用案例在本质上是不同的,需要一种不同的方法,即使某些工具可以在两者之间共享。
上述建议的图形 RAG 工具清单中包含一些对于典型生成式 AI(GenAI)使用案例来说通常不必要的工具。而且,除了不必要之外,加入这些工具可能会使事情变得过于复杂——导致开发时间增加、成本提高,并且产生额外的维护开销,这些本可以避免。通过专注于基本要素来保持技术栈的简洁,可以提高效率,并让你在不增加负担的情况下充分发挥图形 RAG 的优势。
一个常见的误解是,进行图形 RAG 需要图形数据库(Graph DB)。 图形数据库和图形查询语言(Graph QL)是强大的图形分析工具和深度图形算法,但图形 RAG 和生成式 AI 应用通常并不从这些传统的图形分析中受益。图形数据库可以支持图形 RAG,但它们也会给技术栈带来不必要的复杂性。我们将在下面深入探讨这个话题。
在本文中,我们讨论了涉及图形的各种使用案例的软件需求,重点讨论了生成式 AI 使用案例和应用,并在从普通 RAG 转向图形 RAG 时尽量减少额外的努力和复杂性。在大多数情况下,我们不需要一个庞大的工具清单;采用与我们目标一致的几个关键技术,不仅能简化我们的工作,还能通常取得更好的结果。
生成式 AI 图形使用案例
语义向量搜索在查找与查询上下文相似的文档时非常强大。然而,仍然存在一些情况,这种方法表现不佳,尤其是当所需信息是非语义的,或者需要更深入的数据洞察时。图形 RAG 技术可以通过利用非语义信息来补充向量搜索的能力——例如在以下常见的使用场景中:
利用文档中的非语义信息
虽然语义搜索在根据上下文相似性识别文档方面表现出色,但它通常忽略了非语义线索,而这些线索对于全面的数据分析至关重要。图形可以结合和利用非语义信息,如元数据,这些信息可以包括链接、专有术语和定义、交叉引用、词汇表以及文档结构,如标题、章节标题和子部分内容。此外,图形还可以连接从文本中提取或推断出的实体、关键词和概念。
社区总结
当目标是总结来自某个社区或特定群体的内容时,基于图形的方法可以发挥不可或缺的作用。图形可以识别数据中的簇或社区,总结跨多个文档或贡献者的流行主题或讨论。
邻域探索
探索图形中某个特定节点或查询的“邻域”或直接连接可以揭示通过语义搜索无法显现的关系和洞察。上下文探索允许从起始节点出发,探索相邻节点(文档、术语或概念),以发现相关信息,从而为初始查询增加深度。

知识图谱中的冒险:玩具世界中的图形。由 Brian Godsey 使用 DALL-E 生成。
为什么生成性人工智能与传统图形使用案例不同
在生成性人工智能出现之前,有知识图谱和图数据库。这些图形工具早于生成性人工智能很多年,部分相关技术是为非常不同的使用案例设计的。这些技术主要用于结构化数据的探索,而不是生成性人工智能擅长的非结构化文本处理和语义理解。
从传统图形使用案例到生成性人工智能的转变,是数据处理技术中的一个重要变化。传统图形非常适合清晰、明确的关系,但它们通常缺乏应对生成性人工智能细微需求所需的灵活性。
传统图形工具是为处理庞大、复杂的图形而构建的
知识图谱通常是来自多个来源的大量数据的聚合,连接了跨多个数据点的复杂且相互依赖的关系。节点和边的巨大数量,加上它们连接的复杂性,会使得数据处理和分析任务在计算上变得密集且耗时。
这就是图数据库(graph DBs)最初创建的原因。它们提供了优化的存储解决方案和处理能力,旨在高效地管理庞大的节点和边的网络。与图数据库一起,图查询语言(graph QLs)被设计用来便捷地对这些大型图形及其子图执行复杂的查询操作。这些工具擅长执行涉及深度遍历、模式匹配和动态数据聚合的操作,这些操作在图形分析中很常见。图数据库和图形分析的常见使用案例包括社交网络分析、推荐系统、欺诈检测和复杂网络管理。在这些场景中,快速高效地分析大数据集中的复杂关系至关重要。
图数据库(graph DBs)和图查询语言(graph QLs)的某些经典使用案例:
-
中心性分析 — 识别社交网络中最具影响力的人物。涉及中心性度量,如度数中心性、介数中心性和特征向量中心性
-
社区检测 — 将网络划分为社区或簇,其中成员之间的内部连接比与网络其他部分的连接更为紧密。涉及图聚类算法和边缘介入度社区检测。
-
路径寻找 — 找到两个节点之间的最短路径,以了解个体之间的分离度。涉及诸如 Dijkstra 算法或 A*(A-star)算法等最短路径计算算法。
当然,还有许多其他复杂的图查询和图分析用例,这是传统图工具所设计并擅长的。但这里给出的例子,以及许多其他例子,与我们今天在生成式 AI 应用中看到的图用例有很大的不同。
知道了这些,为什么我们要使用图数据库来构建图 RAG 系统,而将向量存储和搜索作为附加功能,当现代向量存储完全能够支持我们需要的所有图操作时呢?我们不应该这么做,接下来我们将深入探讨向量存储如何与图操作配合工作。
图 RAG 和向量搜索都在本地操作
之前,我将“邻域探索”列为生成式 AI 用例中图的一种应用,但从概念上讲,它可以被视为一个广义的术语,几乎可以涵盖生成式 AI 中所有的图用例。换句话说,当我们在生成式 AI 中使用图时,我们几乎肯定只是在探索邻域——很少会探索整个图或图的大部分。最多,我们会探索相对于整个图而言相当小的子图。
在图论中,“邻域”指的是一个图中与给定节点相邻的一组节点,这些节点通过直接链接或边缘相连。因此,从知识图中检索节点的邻居应该返回一组与起始节点直接相关的项目或概念。类似地,在向量搜索中,标准实现返回“近似最近邻”(ANN),即在语义向量空间中与查询最相关的文档。 (ANN 之所以是“近似的”,是因为要使其精确会更慢且更昂贵。)
因此,向量搜索和从起始节点开始的图遍历都在寻找“最近邻”,其中“最近”在两种情况下的含义不同。向量搜索寻找最近的语义邻居,而图遍历则寻找图的邻居——如果集成得当,可以将语义上和多种非语义方式相关的文档汇聚在一起,这些方式的限制仅受你如何构建知识图的影响。
这里需要注意的重点是,图形 RAG 完全关注于探索局部邻域,无论是图形还是向量 —— 就像 RAG 始终专注于纯向量侧面一样。其含义是,我们的图形 RAG 软件栈应建立在擅长局部邻域搜索和检索的基础上,因为我们在生成型人工智能(GenAI)应用中的所有查询都聚焦于特定领域的知识,这些领域不需要对整个知识图谱进行全面的探索或分析。

知识图谱的冒险:现实中的图谱。由 Brian Godsey 使用 DALL-E 生成。
单点选择图形工具:只采纳你所需要的
回到本文开头提到的“图形工具清单”,让我们更仔细地看看,何时你可能希望将它们作为图形 RAG 堆栈的一部分,或者选择不使用它们。
知识图谱
-
何时采用 — 永远以某种形式采用。知识图谱是图形 RAG 的核心部分。
-
何时避免 — 永远不要,除非是为了替换图形 RAG,改用普通的 RAG。
实体和关键词提取工具
-
何时采用 — 当直接从文本内容构建知识图谱时,通过自动化提取能够高效地将相关实体和关键词填充到你的图谱中。
-
何时避免 — 如果你的数据不适合自动化提取,或者当像文档链接、手动策划或专门的解析器等替代方法更适合你的数据和用例时。
图形遍历算法
-
何时采用 — 永远。一个简单的图形遍历算法是图形 RAG 必需的,例如通常从起始节点进行深度为 1–3 的简单遍历。
-
何时避免 — 尽管基本的遍历是必要的,但除非你的应用场景特别需要高级的图形导航功能,否则应避免使用过于复杂的算法。
属性图实现
-
何时采用 — 当你的项目需要对边缘内复杂关系和属性进行精密建模,而这些内容远超基本的连接时。
-
何时避免 — 对于大多数标准的图形 RAG 实现,在不需要进行复杂关系建模的情况下,通常简单的图形模型就足够了。
图形数据库
-
何时采用 — 当需要处理广泛复杂的查询,并且需要执行超出标准系统能力的高级图形分析和遍历时。
-
何时避免 — 如果你的图形 RAG 系统不涉及复杂、广泛的图形特定操作。在这种情况下采用图形数据库可能会导致不必要的系统复杂性和资源分配。
图形查询语言(Graph QLs)
-
何时采用 — 如果采用图形数据库。当图形数据的复杂查询对你的应用至关重要时,这将允许你对互联数据进行复杂的操作和检索。
-
何时避免 — 对于简单的图 RAG 设置,当基本的检索方法足够时,加入图 QL 可能会使架构过于复杂。
图节点嵌入算法
-
何时采用 — 当你有一个图,并且想要将图节点转换为向量时。这是一个具有优缺点的专业用例。请参见流行算法 node2vec。
-
何时避免 — 如果你的系统不需要将图节点作为向量进行搜索。
向量存储
-
何时采用:始终需要。它们是必需的,因为它们作为存储和搜索对 RAG 系统至关重要的高维向量表示的基础。
-
何时避免 — 永远不要。
每个组件的引入应与图 RAG 系统的具体需求和复杂性相匹配,确保每项采用的技术都能增加价值并提高系统性能,而不会增加不必要的复杂性。

知识图谱中的冒险:波普艺术遍历。由 Brian Godsey 使用 DALL-E 生成。
最小化图 RAG 系统的要求
考虑到上述关于图工具和技术的说明,以下是任何图 RAG 系统所需的核心组件:
-
向量存储 — 对任何 RAG 框架至关重要,在图 RAG 中,向量存储对维持文档检索的可扩展性和效率更为关键。向量存储提供了存储和搜索嵌入语义向量空间中文档的基础设施,这是 RAG 系统检索过程的基础。
-
知识图谱 — 图 RAG 与普通 RAG 的定义性概念,知识图谱连接了语义向量搜索可能遗漏的关键术语和概念。这个图对于扩展上下文和增强 RAG 系统可用的关联数据至关重要,从而证明了它在图 RAG 中的核心作用。
-
图遍历 — 一个简单的图遍历算法对于导航知识图谱是必要的。这个组件不需要过于复杂,因为图 RAG 主要需要探索与查询直接相关的局部邻域或小子图,而不是深度或广泛的图遍历。
对于专业化的用例,或者如果最小化的实现性能不足时,可以添加更多的图工具和功能 — 一些重要的考虑因素将在下一节中概述。
从向量开始,根据需要添加“图” — 而不是反过来
在处理 GenAI 用例时,知识的基础在于向量空间。我们使用像向量存储这样的向量优化工具,因为它们直接与 LLM 和其他 GenAI 模型的语言——向量——进行操作。我们的 GenAI 应用实现应当以向量为先,因为最重要的向量操作(例如近似最近邻搜索)既耗时又昂贵,因此我们应优化这些操作的性能和效率。将图功能添加到 GenAI 应用中,应该仅仅是将图能力添加到现有的向量优化基础设施中。在某些特定的使用案例中,可能需要从向量优化的基础设施迁移到图本地的基础设施,但在绝大多数情况下,这会使技术栈变得复杂,增加部署难度。
在开始实施典型的图 RAG,并考虑增加更复杂的图工具和功能时,重要的是要仔细评估具体的挑战和使用案例的需求,而不是简单地认为更复杂或精密的图工具对任何图用例都是更好的。
以下是一些关键考虑因素:
-
图操作的局部性 — 在图 RAG 中,图操作主要是局部的,只涉及在直接邻域和小子图中的简单遍历。这种方法通常不会从复杂的图算法中获益,因为这些算法可能会使检索过程过于复杂。
-
向量存储在图操作中的能力 — 现代向量存储在执行必要的图操作方面非常强大,尤其是当操作不太复杂时。这使得向量技术和图技术能够无缝集成,彼此互补,无需单独的图数据库。
-
现代向量存储的可扩展性和效率 — 向量存储被设计用于高效处理大规模文档数据集,使其成为 RAG 系统的核心,特别是在快速检索至关重要的情况下。直接在向量存储中使用图能力也能满足必要的图操作而不牺牲性能。
-
图数据库、查询语言和分析的复杂性 — 在技术栈中引入图数据库可能会不必要地复杂化软件架构。鉴于图 RAG 中的图需求通常不需要复杂的大规模图操作,利用现有的向量存储能力来处理这些需求可能更高效,并且能保持系统架构的简洁。
每个新增功能都应谨慎考虑,确保它能够直接解决特定需求,而不引入不必要的复杂性或开销。这种战略方法确保增强功能能通过功能或性能的切实改进得到合理的支持。
开始进行图 RAG 的简单方法
如果你想要一个简单明了的示例,了解如何在没有任何专业图形工具的情况下,仅通过 LangChain 中开源的图形向量存储实现图形 RAG,请查看我在《Towards Data Science》上的上一篇文章。或者,如果你想了解如何入门的更广泛视角,请参阅这篇图形 RAG 指南。
布莱恩·戈茨博士(LinkedIn) — 数学家、数据科学家和工程师 // AI 和 ML 产品在 DataStax // 编写了《 像数据科学家一样思考》

知识图谱中的冒险:探索印象主义。由布莱恩·戈茨使用 DALL-E 生成。

知识图谱中的冒险:探索表现主义。由布莱恩·戈茨使用 DALL-E 生成。
A/B 测试中估算长期效应的指南
解决在在线实验中识别和衡量长期效应的复杂性
·发布于Towards Data Science ·阅读时间 9 分钟 ·2024 年 2 月 24 日
--

图片由Isaac Smith提供,来源于Unsplash
想象一下,你是一个在线商店的分析师。你和你的团队的目标是了解提供免费送货如何影响平台上的订单数量,于是你决定进行 A/B 测试。测试组享受免费送货,而对照组则坚持正常的运费。在实验的初期,你会看到更多人在将商品加入购物车后完成订单。但真正的影响是长期的——测试组的用户更有可能再次回到平台购物,因为他们知道你提供免费送货。
本例的核心结论是什么?免费送货对订单的影响往往是逐渐增加的。仅在短期内进行测试可能意味着你错过了整个故事,而这是我们在本文中旨在解决的挑战。
理解为什么长期效应和短期效应可能不同
总体而言,实验的短期效应与长期效应可能存在差异的原因有很多[1]:
异质性处理效应
- 实验的影响可能因产品的常用用户和偶尔使用者而有所不同。在短期内,常用用户可能会对实验结果产生不成比例的影响,从而引入对平均处理效应的偏差。
用户学习
-
新颖效应 —— 想象一下:你向产品中引入了新的游戏化机制。最初,用户会感到好奇,但这一效应通常会随着时间的推移而减少。
-
初始效应 —— 想象一下,当 Facebook 将排名算法从按时间顺序显示改为推荐时。最初,用户可能会因为无法找到预期内容而导致信息流中的时间减少,产生挫败感。然而,随着时间的推移,随着用户逐渐适应新算法并发现有趣的帖子,互动可能会恢复。用户最初可能反应消极,但最终会适应,从而导致互动增加。
在本文中,我们的重点将放在回答两个问题:
如何识别和测试实验的长期影响是否与短期有所不同?
当实验不能进行足够长的时间时,如何估计长期效应?
识别长期效应趋势的方法
可视化
初始步骤是观察测试组与控制组之间的差异如何随时间变化。如果你注意到类似的模式,就需要深入细节,理解长期效应。

我来自 Sadeghi 等人(2021)的插图[2]
你可能也会觉得有诱惑去绘制实验效果,不仅基于实验的日期,还基于自第一次接触以来的天数。

我来自 Sadeghi 等人(2021)的插图[2]
然而,当你查看自第一次接触以来的天数时,有几个陷阱需要注意:
-
参与用户偏差:图表的右侧可能显示更多的活跃用户。观察到的模式可能并非由于用户学习,而是因为不同的处理效应。对高度活跃用户的影响可能与对偶尔用户的影响不同。
-
选择性抽样问题:我们可以决定仅关注高度活跃的用户,观察他们的效应如何随时间演变。然而,这一子集可能无法准确代表整个用户群体。
-
用户数量下降:从第一次接触开始已经有较长时间的用户可能很少(图表的右侧部分)。这会扩大置信区间,使得得出可靠结论变得更加困难。
在实验中识别长期效应的可视化方法非常直观,通常观察效果随时间的变化是一个很好的起点。然而,这种方法缺乏严谨性;你也可以考虑正式测试长期效应的存在。我们将在下一部分深入探讨这一点。
阶梯实验任务[2]
这种方法背后的概念如下:在开始实验之前,我们将用户分为k个队列,并逐步将他们引入实验。例如,如果我们将用户分为 4 个队列,k_1为对照组,k_2从第 1 周开始接受处理,k_3从第 2 周开始接受处理,k_4从第 3 周开始接受处理。

我摘自 Sadeghi 等人(2021 年)²
用户学习率可以通过比较不同时间段的处理效果来估计。

我摘自 Sadeghi 等人(2021 年)[2]
例如,如果你想估计第 4 周的用户学习情况,你可以比较值T4_5和T4_2。
这种方法的挑战非常明显。首先,它为实验设计引入了额外的操作复杂性。其次,需要大量用户才能有效地将其分成不同的队列,并获得合理的统计显著性水平。第三,应该提前预期到不同的长期效应,并准备在这种复杂环境中进行实验。
差分中的差分 [2]
这种方法是前一种方法的简化版。我们将实验分成两个(或更一般的,分成k)时间段,并比较第一个时间段和第k个时间段的处理效果。

我摘自 Sadeghi 等人(2021 年)[2]
在这种方法中,一个关键问题是如何估计估计值的方差,以便得出关于统计显著性的结论。作者建议使用以下公式(详细信息请参阅文章):

我摘自 Sadeghi 等人(2021 年)[2]
σ2 — 每个实验单元在每个时间窗口内的方差
ρ — 每个实验单元在两个时间窗口中的度量相关性
随机与恒定处理分配 [3]
这是阶梯实验分配的另一种扩展方法。在这种方法中,用户池被分为三组:C — 对照组,E — 在整个实验中都接受处理的组,E1 — 每天以概率p将用户分配到处理组的组。因此,E1组中的每个用户只会在几天内接受处理,从而防止了用户的学习。那么,如何估计用户的学习情况呢?我们引入E1_d — E1组中在第d天接受处理的用户比例。然后,用户学习率由E组和E1_d组之间的差异来确定。
用户“遗忘”[3]
这种方法使我们能够评估用户学习的存在性以及这种学习的持续时间。这个概念相当优雅:它假设用户的学习速率与他们“遗忘”的速率相同。其思路如下:关闭实验并观察测试组和对照组随着时间的推移如何趋同。由于两组在实验结束后将接受相同的处理,任何行为的变化都将是因为实验期间不同的处理方式。
这种方法帮助我们衡量用户“遗忘”实验所需的时间,我们假设这个遗忘期将等同于用户在功能推出期间所需的学习时间。
这种方法有两个显著的缺点:首先,它需要大量时间来分析用户学习。最初,你需要运行一个长期的实验以让用户“学习”,然后你必须停用实验并等待他们“遗忘”。这个过程可能非常耗时。其次,你需要停用实验功能,这可能会让企业犹豫不决。
评估长期效果的方法 [4]
你已经成功确认了实验中用户学习的存在性,并且很明显,长期结果可能与短期观察的结果不同。那么,现在的问题是如何在不进行数周甚至数月实验的情况下预测这些长期结果。
一种方法是尝试使用短期数据预测长期结果 Y。最简单的方法是使用 Y 的滞后值,这被称为“自动替代”模型。假设你想预测实验结果在两个月后的表现,但目前只有两周的数据。在这种情况下,你可以训练一个线性回归(或其他)模型:

我来自张等人(2023)的插图 [5]
m 是用户 i 在两个月期间的平均每日结果
Yi_t 是用户 i 在第 t 天的度量值(在我们的例子中,T 范围是从 1 到 14)
在这种情况下,长期治疗效果是通过使用替代模型,测试组和对照组的度量预测值之间的差异来决定的。

我来自张等人(2023)的插图 [5]
其中,N_a 表示实验组的用户数量,N_0 表示对照组的用户数量。
这里似乎有些不一致:我们目标是预测μ(实验的长期效果),但训练模型时却需要这个μ。那么,我们该如何获得模型呢?有两种方法:
-
使用实验前数据: 我们可以使用同一用户的两个月实验前数据来训练一个模型。
-
类似实验: 我们可以从相同产品领域选择一个持续了两个月的“黄金标准”实验,并用它来训练模型。
在他们的文章中,Netflix 使用 200 次实验验证了这种方法,并得出结论:在 95% 的实验中,代理指数模型与长期测量结果一致[5]。
结论
我们学到了很多,现在让我们来总结一下。短期实验结果通常与长期结果不同,原因可能包括异质性处理效应或用户学习等因素。有几种方法可以检测这种差异,其中最直接的方法是:
-
视觉方法: 简单地观察测试组和对照组随时间的差异。然而,这种方法缺乏严谨性。
-
差异中的差异: 比较实验开始时和一段时间后测试组与对照组之间的差异。
如果你怀疑实验中存在用户学习的现象,理想的方法是延长实验直到处理效应稳定。然而,由于技术(例如短效 cookie)或商业限制,这可能并不总是可行。在这种情况下,你可以使用自动代理模型来预测长期效果,通过Y的滞后值来预测实验的长期结果。
感谢您花时间阅读这篇文章。我很想听听您的想法,请随时分享任何评论或问题。
参考文献
-
N. Larsen, J. Stallrich, S. Sengupta, A. Deng, R. Kohavi, N. T. Stevens, 在线控制实验中的统计挑战:A/B 测试方法综述(2023),
arxiv.org/pdf/2212.11366.pdf -
S. Sadeghi, S. Gupta, S. Gramatovici, J. Lu, H. Ai, R. Zhang, 新颖性与首因效应:一种长期在线实验估算方法(2021),
arxiv.org/pdf/2102.12893.pdf -
H. Hohnhold, D. O’Brien, D. Tang, 聚焦长期:这对用户和商业都有好处(2015),
static.googleusercontent.com/media/research.google.com/en//pubs/archive/43887.pdf -
S. Athey, R. Chetty, G. W. Imbens, H. Kang, 代理指数:结合短期代理快速而精确地估算长期处理效应(2019),
www.nber.org/system/files/working_papers/w26463/w26463.pdf -
V. Zhang, M. Zhao, A. Le, M. Dimakopoulou, N. Kallus, 使用 Netflix 上的 200 次 A/B 测试评估代理指数作为决策工具(2023),
arxiv.org/pdf/2311.11922.pdf
从零开始建立数据部门指南
数据战略
一些实用的建议,来自曾经走过这条路的人
·发表于Towards Data Science ·阅读时间:7 分钟·2024 年 3 月 12 日
--
在寻找有关如何在中型公司建立数据部门的建议和最佳实践时,我遇到的大多数资源要么是关于创建数据驱动的组织,要么是关于建立数据团队。然而,我两年多前所处的位置则需要一种更务实的方法:
作为组织中的第一位(也是唯一的)数据人员,你如何具体开始引入数据最佳实践并建立数据部门?
最近有被要求在某公司建立数据部门吗?你计划在不久的将来承担这个角色吗?或者,出于好奇,你是否一直在想,一个组织是如何从几乎不使用数据到拥有一个多人的数据团队的? 如果是这样,本文就是为你准备的。

从零开始建立数据部门的挑战(图片由作者提供,基于Sigmund和Dorian Mongel在 Unsplash 上的图片)
设置背景
当我在 2021 年 11 月被聘为我当前公司第一位“数据人员”时,我面临的是一片空白。高层管理已经为一名数据人员解锁了预算,但公司距离数据驱动还很远。我的经理曾与技术和数据角色合作过,但他的背景是……
聚类算法指南
·发表于 Towards Data Science ·阅读时间:6 分钟·2024 年 9 月 6 日
--
聚类是任何数据科学家必备的技能,因为它在解决实际问题时的实用性和灵活性。本文概述了聚类及不同类型的聚类算法。
什么是聚类?
聚类是一种流行的无监督学习技术,旨在根据对象或观测值之间的相似性将它们分组。聚类有许多有用的应用,如市场细分、推荐系统、探索性分析等。

图片由作者提供
虽然聚类是数据科学领域广为人知且被广泛使用的技术,但一些人可能不了解不同类型的聚类算法。尽管聚类算法种类不多,但理解这些算法及其工作原理对于根据你的使用场景获得最佳结果至关重要。
基于中心点的聚类
基于中心点的聚类是大多数人提到聚类时的典型方式。它是通过使用预定义数量的中心点(中心)来根据数据点与每个中心点的距离将数据分组的“传统”聚类方法。最终,中心点成为其分配数据点的均值。尽管基于中心点的聚类非常强大,但它对离群值的鲁棒性较差,因为离群值仍需要被分配到某个聚类中。
K-Means
K-Means 是最广泛使用的聚类算法,它可能是你作为数据科学家学习的第一个算法。如上所述,目标是最小化数据点与聚类中心之间的距离之和,从而确定每个数据点应该属于的正确组别。其工作原理如下:
-
一个预定义数量的中心点被随机放置到无标签数据的向量空间中(初始化)。
-
每个数据点会与每个质心进行自我比较(通常使用欧几里得距离),并将自己分配给最近的一个质心。
-
质心会移动到它们所分配的数据点的均值位置。
-
步骤 2-3 会反复执行,直到生成“最佳”聚类。

图片由作者提供
from sklearn.cluster import KMeans
import numpy as np
#sample data
X = np.array([[1, 2], [1, 4], [1, 0],
[10, 2], [10, 4], [10, 0]])
#create k-means model
kmeans = KMeans(n_clusters = 2, random_state = 0, n_init = "auto").fit(X)
#print the results, use to predict, and print centers
kmeans.labels_
kmeans.predict([[0, 0], [12, 3]])
kmeans.cluster_centers_
K-Means ++
K-Means ++是 K-Means 初始化步骤的改进版。由于质心是随机选择的,因此可能会有多个质心初始化到同一个聚类中,导致结果不理想。
然而,K-Means ++通过随机分配第一个质心来解决这个问题,最终找到最大的聚类。然后,其他质心会被放置在与初始聚类一定距离的位置。K-Means ++的目标是使质心之间的距离尽可能远。这种方式可以得到高质量的聚类,且这些聚类是独立且明确定义的。
from sklearn.cluster import KMeans
import numpy as np
#sample data
X = np.array([[1, 2], [1, 4], [1, 0],
[10, 2], [10, 4], [10, 0]])
#create k-means model
kmeans = KMeans(n_clusters = 2, random_state = 0, n_init = "k-means++").fit(X)
#print the results, use to predict, and print centers
kmeans.labels_
kmeans.predict([[0, 0], [12, 3]])
kmeans.cluster_centers_
基于密度的聚类
基于密度的算法也是一种流行的聚类形式。然而,它们并不是通过从随机位置的质心进行度量,而是通过识别数据中的高密度区域来创建聚类。基于密度的算法不需要预先定义聚类的数量,因此优化起来工作量较小。
虽然基于质心的算法在处理球形聚类时表现更好,但基于密度的算法可以处理任意形状的聚类,并且更具灵活性。它们还不会将离群点包括在聚类中,因此具有较强的鲁棒性。然而,这些算法在面对具有不同密度和高维度的数据时可能会遇到困难。

图片由作者提供
DBSCAN
DBSCAN 是最流行的基于密度的算法。DBSCAN 的工作原理如下:
-
DBSCAN 随机选择一个数据点,并检查它是否在指定的半径内有足够的邻居。
-
如果某个点有足够的邻居,它将被标记为聚类的一部分。
-
DBSCAN 会递归检查邻居是否也在半径内有足够的邻居,直到聚类中的所有点都被访问过。
-
重复步骤 1-3,直到剩余的数据点在半径内没有足够的邻居。
-
剩余的数据点被标记为离群点。
from sklearn.cluster import DBSCAN
import numpy as np
#sample data
X = np.array([[1, 2], [2, 2], [2, 3],
[8, 7], [8, 8], [25, 80]])
#create model
clustering = DBSCAN(eps=3, min_samples=2).fit(X)
#print results
clustering.labels_
层次聚类
接下来,我们来讲解层次聚类。这种方法首先通过从原始数据计算一个距离矩阵开始。这个距离矩阵最好且通常通过树状图(见下文)进行可视化。数据点通过找到最近的邻居,一一连接,最终形成一个巨大的聚类。因此,可以通过设置一个截断点,停止所有数据点的连接,从而识别出不同的聚类。

图片由作者提供
通过使用这种方法,数据科学家可以通过定义离群点并将其排除在其他聚类之外,构建一个鲁棒的模型。这种方法在处理层次结构数据(如分类法)时效果极佳。聚类的数量取决于深度参数,范围可以是 1 到 n。
from scipy.cluster.hierarchy import dendrogram, linkage
from sklearn.cluster import AgglomerativeClustering
from scipy.cluster.hierarchy import fcluster
#create distance matrix
linkage_data = linkage(data, method = 'ward', metric = 'euclidean', optimal_ordering = True)
#view dendrogram
dendrogram(linkage_data)
plt.title('Hierarchical Clustering Dendrogram')
plt.xlabel('Data point')
plt.ylabel('Distance')
plt.show()
#assign depth and clusters
clusters = fcluster(linkage_data, 2.5, criterion = 'inconsistent', depth = 5)
基于分布的聚类
最后,基于分布的聚类考虑了除距离和密度之外的度量标准,即概率。基于分布的聚类假设数据由概率分布组成,例如正态分布。该算法创建了“带状”区域,表示置信区间。数据点距离聚类中心越远,我们就越不确定该数据点是否属于该聚类。

作者提供的图片
基于分布的聚类由于其假设条件,实施起来非常困难。通常不推荐使用,除非经过严格分析确认其结果。例如,可以使用它来识别市场数据集中的客户群体,并确认这些群体符合某种分布。这个方法也非常适合探索性分析,不仅能了解聚类中心的组成,还能分析聚类的边缘和离群点。
结论
聚类是一种无监督机器学习技术,在许多领域的应用日益增多。它可以用于支持数据分析、细分项目、推荐系统等多个场景。上述内容中,我们探讨了它们的工作原理、优缺点、代码示例,甚至一些使用案例。我认为,数据科学家必须具备使用聚类算法的经验,因为它们具有广泛的实用性和灵活性。
希望你喜欢我的文章!请随时发表评论、提问或请求其他话题。
数据工程基础设施指南
使用现代工具自动化资源配置
·发布于 Towards Data Science ·12 分钟阅读·2024 年 1 月 20 日
--

图片来源:Ehud Neuhaus 由 Unsplash 提供
现代数据栈由各种工具和框架组成,用于处理数据。通常,它是一个庞大的云资源集合,旨在转换数据并将其带到可以生成数据洞察的状态。管理这些数据处理资源的庞大数量并非易事,可能会让人感到不知所措。幸运的是,数据工程师发明了一个叫做基础设施即代码的解决方案。它本质上是帮助我们部署、配置和管理在数据管道中可能需要的所有资源的代码。在这个故事中,我想讨论一些流行的技术和现有框架,旨在简化资源配置和数据管道部署。我还记得在数据职业生涯的最初,我是通过网页用户界面部署数据资源的,比如存储桶、安全角色等。那些日子早已过去,但我仍然记得,当我了解到可以通过模板和代码编程方式完成时,那份喜悦和兴奋。
现代数据栈
什么是现代数据栈(MDS)?它是专门用于组织、存储和操作数据的技术,这些技术组成了现代数据栈[1]。这就是……
使用 Python 进行数据管道测试指南
面向初学者的单元测试、模拟和打补丁的温和介绍
·发布于 Towards Data Science ·9 分钟阅读 ·2024 年 3 月 9 日
--

使用 Kandinsky 生成的 AI 图像
在这篇文章中,我想讨论一下数据工程中的单元测试。尽管互联网上有很多关于 Python 单元测试的文章,但这个话题看起来有些模糊且未被完全覆盖。我们将讨论数据管道、它们所包含的部分,以及如何测试它们以确保持续交付。数据管道的每个步骤都可以视为一个函数或过程,理想情况下,它不仅应该作为单元进行测试,还应该作为一个整体进行测试,集成到一个单一的数据流过程中。我将尝试总结我常用的技术,包括模拟、打补丁和测试数据管道的技术,涵盖集成测试和自动化测试。
在数据世界中,什么是单元测试?
测试是任何软件开发生命周期中的关键部分,有助于开发人员确保代码是可靠的,并且可以在未来轻松维护。将我们的数据管道视为一组处理步骤或函数。在这种情况下,单元测试可以视为一种编写测试的技术,旨在确保我们代码中的每个单元,或者我们数据管道中的每个步骤不会产生意外结果,并且适合其目的。
简而言之,每个步骤……
机器学习中的线性与非线性指南
…以及它们在决策边界、嵌入、动力学系统和下一代大语言模型中的作用
·发布于Towards Data Science ·阅读时间:21 分钟·2024 年 10 月 28 日
--
“以牙还牙,以眼还眼。”
— 报应法则,《汉谟拉比法典》
著名的报应法则是一种比例法则。你夺走我的眼睛,我夺走你的。你夺走我的牙齿,我夺走你的(当时做巴比伦的牙医一定很辛苦)。
该法律并非为了促进暴力,而是旨在限制暴力。报应法则设想了一个可以用线性方程来描述的一切的法律世界:每个罪行都会产生与其输入成比例的输出。由于对犯罪的惩罚与罪行成比例,它避免了过度的报复和暴力爆发,这些暴力将一切摧毁殆尽。
在报应的世界之外,线性在我们理解世界的过程中扮演着重要角色:在线性系统中,一切都是可以理解的。没有混乱,没有复杂的数学。所有科学家整天要做的就是解决这些类型的方程:
每一个动作都有一个相等且相反的反应。
— 牛顿第三定律
不幸的是,我们所处的现实远非这种线性理想世界。历史上充斥着世界以高度不成比例的方式回应小事的例子:布拉格窗户事件引发了三十年战争……
强大的 Python 枚举指南
PYTHON 编程
通过简单、易读且高效的枚举来改进数据科学中的 Python 代码
·发表于 Towards Data Science ·阅读时间 23 分钟·2024 年 5 月 20 日
--

Python 枚举提供了有用的数据类型。图片来源:Waldemar via Unsplash
枚举类型在多种编程语言中都有应用,例如 C、C#、C++、Java、Go、Rust——当然也包括 Python。不知道出于什么原因,Python 枚举在数据科学领域常常被低估和少用。我自己虽然相对频繁地使用它们,并且也看到过它们的使用——但绝对是远远不够的。我希望这篇文章能够在改变这一情况上迈出小小的一步。
Python 枚举提供了一个非常强大的工具,不容忽视。如果一个人不知道枚举是如何工作的,可能会认为它过于复杂——这也是为什么最好先学习它们的工作原理,再尝试使用它们。我希望在阅读完这篇文章后,你能同意我的看法:枚举不仅不会使代码复杂,反而能够让代码变得更加简洁、短小、减少错误——甚至提升性能。
我们将通过数据科学中的一个具体示例来讨论枚举类型。为此,我们将定义并比较四个类,其中两个基于字符串和 typing.Literal 类型的组合,另外两个则基于 enum.Enum,这是构成标准库的类…
使用 weakref 模块深入了解 Python 的弱引用
了解 Python 中弱引用的所有内容:引用计数、垃圾回收以及 weakref 模块的实际应用
·发表于 Towards Data Science ·阅读时间:6 分钟·2024 年 6 月 25 日
--

图片来源:Dan Cristian Pădureț 在 Unsplash
你可能从未接触过,甚至可能没听说过 Python 的 weakref 模块。尽管它在你的代码中可能不常用,但它对许多库、框架甚至 Python 本身的内部工作至关重要。因此,在这篇文章中,我们将探索它是什么,它如何有用,以及你如何将它应用到你的代码中。
基础知识
要理解 weakref 模块和弱引用,我们首先需要简单了解 Python 中的垃圾回收机制。
Python 使用 引用计数 作为垃圾回收的机制——简单来说——Python 为我们创建的每个对象保持一个引用计数,当代码中引用该对象时,引用计数会增加;当对象被取消引用时(例如变量设置为 None),引用计数会减少。如果引用计数降到零,内存将被回收(垃圾回收)。
让我们通过一些代码来更好地理解它:
import sys
class SomeObject:
def __del__(self):
print(f"(Deleting {self=})")
obj = SomeObject()
print(sys.getrefcount(obj)) # 2
obj2 = obj
print(sys.getrefcount(obj)) # 3
obj = None
obj2 = None
# (Deleting self=<__main__.SomeObject object at 0x7d303fee7e80>)
在这里,我们定义了一个只实现 __del__ 方法的类,该方法在对象被垃圾回收(GC)时调用——我们这样做是为了看到垃圾回收发生的时刻。
在创建了这个类的实例后,我们使用sys.getrefcount来获取当前对该对象的引用数。我们本来会期望得到1,但getrefcount返回的计数通常比预期的要高 1,因为当我们调用getrefcount时,引用会按值复制到函数的参数中,从而暂时增加对象的引用计数。
接下来,如果我们声明obj2 = obj并再次调用getrefcount,我们会得到3,因为它现在被obj和obj2两个变量引用。相反,如果我们将None赋值给这些变量,引用计数将减少到零,最终我们将通过__del__方法收到对象被垃圾回收的消息。
那么,弱引用如何适应这一点呢?如果对象的唯一剩余引用是弱引用,那么 Python 解释器就可以自由地回收这个对象。换句话说——对一个对象的弱引用不足以保持该对象的存活:
import weakref
obj = SomeObject()
reference = weakref.ref(obj)
print(reference) # <weakref at 0x734b0a514590; to 'SomeObject' at 0x734b0a4e7700>
print(reference()) # <__main__.SomeObject object at 0x707038c0b700>
print(obj.__weakref__) # <weakref at 0x734b0a514590; to 'SomeObject' at 0x734b0a4e7700>
print(sys.getrefcount(obj)) # 2
obj = None
# (Deleting self=<__main__.SomeObject object at 0x70744d42b700>)
print(reference) # <weakref at 0x7988e2d70590; dead>
print(reference()) # None
这里我们再次声明了一个obj变量,作为我们类的实例,但这次我们并没有为这个对象创建第二个强引用,而是在reference变量中创建了弱引用。
如果我们检查引用计数,我们可以看到它没有增加,并且如果我们将obj变量设置为None,我们可以看到它会立即被垃圾回收,尽管弱引用仍然存在。
最后,如果尝试访问已经被垃圾回收的对象的弱引用,我们将得到一个“死”引用,返回值为None。
同时请注意,当我们使用弱引用访问对象时,必须将其作为函数调用(reference())来检索对象。因此,通常使用代理会更加方便,特别是当你需要访问对象属性时:
obj = SomeObject()
reference = weakref.proxy(obj)
print(reference) # <__main__.SomeObject object at 0x78a420e6b700>
obj.attr = 1
print(reference.attr) # 1
何时使用弱引用
既然我们已经了解了弱引用是如何工作的,接下来我们来看看它们如何能派上用场。
弱引用的一个常见用例是树状数据结构:
class Node:
def __init__(self, value):
self.value = value
self._parent = None
self.children = []
def __repr__(self):
return "Node({!r:})".format(self.value)
@property
def parent(self):
return self._parent if self._parent is None else self._parent()
@parent.setter
def parent(self, node):
self._parent = weakref.ref(node)
def add_child(self, child):
self.children.append(child)
child.parent = self
root = Node("parent")
n = Node("child")
root.add_child(n)
print(n.parent) # Node('parent')
del root
print(n.parent) # None
在这里,我们使用Node类实现了一个树,其中子节点对父节点有弱引用。在这种关系中,子Node可以在没有父Node的情况下存在,这允许父节点在没有任何警告的情况下被删除/垃圾回收。
或者,我们可以反过来做:
class Node:
def __init__(self, value):
self.value = value
self._children = weakref.WeakValueDictionary()
@property
def children(self):
return list(self._children.items())
def add_child(self, key, child):
self._children[key] = child
root = Node("parent")
n1 = Node("child one")
n2 = Node("child two")
root.add_child("n1", n1)
root.add_child("n2", n2)
print(root.children) # [('n1', Node('child one')), ('n2', Node('child two'))]
del n1
print(root.children) # [('n2', Node('child two'))]
在这里,相反地,父节点维护了一个子节点的弱引用字典。这使用了WeakValueDictionary——每当字典中引用的元素(弱引用)在程序的其他地方被取消引用时,它会自动从字典中移除,因此我们不需要管理字典项的生命周期。
weakref的另一个用途是在观察者设计模式中:
class Observable:
def __init__(self):
self._observers = weakref.WeakSet()
def register_observer(self, obs):
self._observers.add(obs)
def notify_observers(self, *args, **kwargs):
for obs in self._observers:
obs.notify(self, *args, **kwargs)
class Observer:
def __init__(self, observable):
observable.register_observer(self)
def notify(self, observable, *args, **kwargs):
print("Got", args, kwargs, "From", observable)
subject = Observable()
observer = Observer(subject)
subject.notify_observers("test", kw="python")
# Got ('test',) {'kw': 'python'} From <__main__.Observable object at 0x757957b892d0>
Observable类保持对其观察者的弱引用,因为它不关心它们是否被移除。和之前的例子一样,这避免了管理依赖对象生命周期的需要。正如你可能注意到的,在这个例子中我们使用了WeakSet,它是weakref模块中的另一个类,行为与WeakValueDictionary类似,但它是通过Set实现的。
本部分的最后一个例子来自于[weakref](https://docs.python.org/3/library/weakref.html#comparing-finalizers-with-del-methods)文档:docs:
import tempfile, shutil
from pathlib import Path
class TempDir:
def __init__(self):
self.name = tempfile.mkdtemp()
self._finalizer = weakref.finalize(self, shutil.rmtree, self.name)
def __repr__(self):
return "TempDir({!r:})".format(self.name)
def remove(self):
self._finalizer()
@property
def removed(self):
return not self._finalizer.alive
tmp = TempDir()
print(tmp) # TempDir('/tmp/tmp8o0aecl3')
print(tmp.removed) # False
print(Path(tmp.name).is_dir()) # True
这展示了weakref模块的另一个特性,即weakref.finalize。顾名思义,它允许在相关对象被垃圾回收时执行一个终结函数/回调。在这个例子中,我们实现了一个TempDir类,用于创建临时目录——理想情况下,我们总是会记得在不需要TempDir时清理它,但如果忘记了,终结器会在TempDir对象被垃圾回收时自动执行rmtree,这包括程序完全退出时。
现实世界中的例子
前一部分展示了几个weakref的实际用途,但我们也来看看一些现实世界中的例子——其中之一是创建缓存实例:
import logging
a = logging.getLogger("first")
b = logging.getLogger("second")
print(a is b) # False
c = logging.getLogger("first")
print(a is c) # True
上面展示的是 Python 内建logging模块的基本用法——我们可以看到,它允许只将一个日志实例与给定名称关联——这意味着当我们多次获取相同的日志器时,它始终返回相同的缓存日志实例。
如果我们想实现这一点,可能会是这样的:
class Logger:
def __init__(self, name):
self.name = name
_logger_cache = weakref.WeakValueDictionary()
def get_logger(name):
if name not in _logger_cache:
l = Logger(name)
_logger_cache[name] = l
else:
l = _logger_cache[name]
return l
a = get_logger("first")
b = get_logger("second")
print(a is b) # False
c = get_logger("first")
print(a is c) # True
最后,Python 本身也使用了弱引用,例如在OrderedDict的实现中:
from _weakref import proxy as _proxy
class OrderedDict(dict):
def __new__(cls, /, *args, **kwds):
self = dict.__new__(cls)
self.__hardroot = _Link()
self.__root = root = _proxy(self.__hardroot)
root.prev = root.next = root
self.__map = {}
return self
上面是来自CPython的[collections](https://github.com/python/cpython/blob/0c0348adbfca991f78b3aaa6790e5c26606a1c0f/Lib/collections/__init__.py#L103)模块的代码片段。在这里,weakref.proxy用于防止循环引用(更多细节请参见文档字符串)。
结论
weakref是一个相当晦涩但在某些时候非常有用的工具,你应该将其保留在你的工具箱中。当实现缓存或包含引用循环的数据结构时,它非常有帮助,比如双向链表。
话虽如此,应该注意到weakref的支持——这里以及文档中提到的内容是针对 CPython 的,其他 Python 实现会有不同的weakref行为。此外,许多内建类型不支持弱引用,例如list、tuple或int。
本文最初发布于 martinheinz.dev
你可能还会喜欢……
## 你可以用 Python 的 textwrap 模块做什么
了解你可以使用 Python 的 textwrap 模块完成的所有操作,包括格式化、文本换行、修剪等…
towardsdatascience.com
LLM 评估指标的高级指南
发展对各种 LLM 基准和评分的理解,包括何时它们可能对你的目的有价值的直觉
·发表于Towards Data Science ·阅读时间 17 分钟·2024 年 2 月 27 日
--

标题卡由作者创建
几乎每周都会有一个新的大型语言模型(LLM)向公众发布。每次发布 LLM 时,这些提供商都会宣扬一些看起来相当令人印象深刻的性能数据。我发现的挑战是,这些新闻稿中引用的性能指标种类繁多。虽然有一些指标比其他指标出现得更频繁,但遗憾的是,并没有简单的“一两个”标准指标。如果你想看到一个实际的例子,查看 GPT-4 性能的页面。它引用了许多不同的基准和评分!
第一个自然的问题是:“为什么我们不能简单地达成共识,使用一个单一的指标?”简而言之,没有一种简单的方法可以评估 LLM 的性能,因此每个性能指标都试图为某个特定领域提供定量评估。此外,许多这些性能指标还有“子指标”,这些子指标计算方式与原始指标略有不同。当我最初开始为这篇博客文章进行研究时,我的初衷是覆盖所有这些基准……
人道主义危机情况报告 AI 助手:探索 LLMOps 与 Prompt Flow
·发布于Towards Data Science ·17 分钟阅读·2024 年 4 月 1 日
--
TL;DR
关于如何使用强大的大型语言模型(LLMs)进行知识检索,有许多教程。然而,如果考虑到这些技术在现实世界中的应用,就需要应用工程最佳实践,并且这些实践应该扩展以减轻与 LLMs 相关的一些新风险,如幻觉现象。在本文中,我们探讨了如何实现一些关键领域,来实现 LLMs 的操作化——如安全性、提示工程、基础扎实性和评估——并开发了一个简单的提示流,以创建一个简单的 演示 AI 助手 ,用以回答关于人道主义灾难的问题,信息来源于 ReliefWeb 平台上的情况报告。提示流包括一套很棒的工具,用于编排 LLM 工作流,并且像 deep eval 这样的包提供了在运行时测试 LLM 输出的方法(尽管有一些注意事项)。
操作化大型语言模型应用
在之前的博客文章“一些关于 LLM 应用程序操作化的思考”中,我们讨论了在推出 LLM 应用程序时,除了生成型 AI 的炫目新技术外,还有许多因素需要考虑。许多工程要求适用于任何软件开发,如 DevOps 以及拥有坚实的框架来监控和评估性能,但其他领域,如减轻幻觉风险,则相对较新。任何在推出炫酷的生成型 AI 应用时忽视这些问题的组织,尤其是在高风险的环境中,都会冒很大风险,因为偏见、不正确和缺失的信息可能会导致非常严重的后果。

启动使用大型语言模型(LLM)应用程序之前需要考虑的一些关键领域。来源:一些关于 LLM 操作化的思考
许多组织目前正在经历这一操作化过程,并试图弄清楚如何准确地使用新的生成型 AI。好消息是,我们正处于一个阶段,支持性产品和服务开始让应用安全、成本效益和准确性原则的应用变得更加容易。AWS Bedrock,Azure 机器学习与 Studio,Azure AI Studio(预览版),以及一系列其他供应商和开源产品,都使得开发 LLM 解决方案变得更加容易。
Prompt Flow
在本文中,我们将重点讨论使用由微软开发的开源项目 Prompt Flow……
Prompt Flow 是一套开发工具,旨在简化基于 LLM 的 AI 应用程序的端到端开发周期,从构思、原型制作、测试、评估到生产部署和监控。它使得提示工程变得更加容易,并使你能够构建具有生产质量的 LLM 应用程序。
为什么选择 Prompt Flow?
经过大量的个人研究,Prompt Flow在某些情况下已成为开发 LLM 应用程序的一个极好的选择,原因如下……
-
直观的用户界面。正如我们下面所见,即使是简单的大型语言模型应用程序也需要复杂的工作流。Prompt Flow 提供了一个良好的开发用户界面,使得可视化流程更加容易,并且内置了评估功能,支持与Visual Studio Code 的强大集成,并且有完善的支持文档。
-
开源。在将应用程序部署到具有不同基础设施需求的组织时,这非常有用。正如我们下面所见,Prompt Flow 并不依赖于任何特定的云供应商(尽管它是由微软开发的),并且可以以多种方式进行部署。
-
Azure 中的企业支持。虽然是开源的,但如果你在 Azure 上,Prompt Flow 得到了本地支持,并提供广泛的企业级功能。作为 Azure 机器学习工作室和预览版 Azure AI 工作室的一部分,它提供了现成的集成功能,涵盖了安全性、可观察性和部署,帮助节省时间专注于业务用例。
-
轻松部署。如上所述,Azure 上的部署只需几次点击。但即使你在本地或使用其他云供应商,Prompt Flow 也支持通过 Docker 部署。
当然,这可能并不适用于所有情况,但如果你想要兼具开源和 Azure 企业支持的最佳体验,那么 Prompt Flow 可能适合你。
一个回答有关当前人道主义灾难问题的 AI 助手
在本文中,我们将使用 Prompt Flow 开发一个 AI 助手,该助手可以使用在令人惊叹的ReliefWeb 平台上包含的人道主义报告中的信息回答问题。ReliefWeb 包含人道主义组织提交的内容,提供全球灾难发生现场的信息,常见的格式是“情况报告”。内容可能很多,因此能够快速提取所需的关键信息比逐一阅读每份报告要省力得多。
请注意: 本文的代码可以在 这里找到,但应该提到的是,它是一个基础示例,仅用于展示一些将大型语言模型(LLMs)投入生产的关键概念。要在生产环境中使用它,还需要更多的工作来整合和查询 ReliefWeb,以及分析 PDF 文档而不仅仅是它们的 HTML 摘要,但希望代码能提供一些大家可能会觉得有用的示例。

本文中使用的流程 — 一个演示的 AI 代理,用于回答有关人道主义灾难的问题,使用的信息来自 ReliefWeb 上的情况报告。完整代码可以在这里找到。
演示应用程序已经设置好,用于展示以下内容……
-
内容安全监控
-
编排 LLM 任务
-
自动化的事实准确性和覆盖度自我检查
-
基于事实性的批量测试
-
使用 Prompt Flow 在 GitHub Actions 中进行自我测试
-
部署
演示版 Prompt Flow 应用程序的设置
本文的演示应用程序包含一个 requirements.txt 文件,并且可以在 Python 3.11.4 环境下运行,如果您希望将其安装到现有环境中,请参考下面的设置步骤。
如果您尚未安装这些,请安装……
然后按照以下步骤操作……
您将需要来自 OpenAI 或 Azure OpenAI 的 LLM API 密钥,以及您想要使用的模型的部署名称
查看 应用程序仓库,其中包含本文中的 Prompt Flow 应用程序
在您的仓库顶层文件夹中,复制 .env.example 到 .env 并设置该文件中的 API 密钥
在命令行中设置环境,打开终端,在仓库的顶层目录运行:conda env create -f environment.yml。这将构建一个名为 pf-rweb-demo 的 conda 环境
打开 VS Code
通过“文件” > “打开文件夹”打开仓库,并选择仓库的顶层目录
在 VS Code 中,点击左侧栏的 Prompt Flow 图标 —— 它看起来像一个‘P’

第一次点击时,您应该在左上角看到如下消息,点击“安装依赖项”链接

点击“选择 Python 解释器”,选择您在步骤 7 中创建的 conda Python 环境 pf-rweb-demo。完成此操作后,库部分应该会……

现在,您应该能看到左侧导航栏中名为“Flows”的部分,点击“relief web_chat”并选择“打开”

这应该会打开 Prompt Flow 用户界面……

本文演示代码的 Prompt Flow 用户界面。该流程展示了如何编排 LLM 应用程序中的各个阶段
点击左侧垂直栏中的‘P’(Prompt Flow),您应该能看到一个连接部分

-
点击“+”按钮,选择您正在使用的服务(Azure OpenAI 或 OpenAI)。
-
在连接编辑窗口中,将名称设置为合理的内容,如果使用的是 Azure,则将字段
api_base设置为您的基础 URL。不要填充api_key,因为系统会提示您输入此信息。

-
点击小的“创建连接”按钮,系统提示时输入您的 API 密钥,您的连接现在已创建
-
如果您使用的是 Azure,并且将连接命名为 azure_openai,且模型部署为“gpt-4-turbo”和“got-35-turbo-16k”,您应该已经配置完成,否则,请点击 Prompt Flow 用户界面中的任何 LLM 节点,正确设置连接和部署名称。请参见下面用于“extract_entities”LLM 节点的设置。

运行演示版 Prompt Flow 应用程序
现在您已完成设置,任何时候您想要运行流程时…
-
按照上面第 9–11 步描述的方式打开流程
-
点击流程顶部的小双播放图标

这应该运行完整的流程。要查看输出,您可以点击任何节点并查看输入/输出,甚至作为调试的一部分运行单独的节点。
现在,让我们来看看应用程序的几个主要组件……
内容安全
任何使用 LLM 的聊天应用程序都应进行一些测试,以确保用户输入和 LLM 输出是安全的。安全检查应涵盖以下领域:
-
偏见
-
仇恨言论 / 有毒内容
-
自残
-
暴力
-
提示注入(通过黑客手段获取不同的提示进入 LLM)
-
知识产权侵权
该列表并不详尽,且并非所有项都适用,具体取决于应用程序的上下文,但应该始终进行审查,并确定适当的安全性测试。
Prompt Flow 集成了Azure 内容安全,该功能涵盖了上述一些内容,并且通过在创建新节点时选择“内容安全”非常容易实现。我最初配置了演示应用程序来使用此功能,但意识到并非每个人都有 Azure,因此该流程包括了两个 Python 占位符节点 content_safety_in 和 content_safety_out,以说明可以应用内容安全检查的位置。这些节点在演示应用程序中并未实现实际的安全验证,但像Guardrails AI和deep eval等库提供了一系列可以在这些脚本中使用的测试。
content_safety_in节点控制下游流程,如果内容被认为不安全,将不会调用相关任务。
由于 LLM 的输出严重依赖于提供的数据并实时评估,对于此应用程序来说,可能没有必要在输出中添加安全检查,但它展示了在 LLM 应用中可以执行安全性强制的两个方面。
还应注意,如果使用 Azure 模型库,Azure 也提供 LLM 级别的安全过滤器。这是一种方便实现内容安全的方法,无需开发代码或指定流程中的节点,点击按钮并支付一些额外费用来获取安全服务,有时可能是更好的选择。
实体提取
为了查询 ReliefWeb API,提取用户问题中的实体并使用这些实体进行搜索,而不是直接使用原始输入是很有用的。根据远程 API 的不同,这可以产生更合适的情况报告,以便找到答案。
演示应用程序中的一个示例如下……
用户输入:“有多少儿童受到苏丹危机的影响?”
LLM 提取的实体:
[
{
"entity_type": "disaster_type",
"entity": "sudan crises"
}
]
ReliefWeb API 查询字符串:“苏丹危机”
这是一个非常基础的实体提取,因为我们仅对一个简单的搜索查询感兴趣,该查询会返回 ReliefWeb API 中的结果。该 API 支持更复杂的过滤功能,实体提取可以相应扩展。像GLiNER这样的其他命名实体识别技术可能会提高性能。
从 ReliefWeb API 获取数据
一旦生成查询字符串,就可以调用 ReliefWeb API。对于演示应用程序,我们将结果限制为最新的 5 个情况报告,其中 Python 代码创建以下 API 请求……
{
"appname": “<YOUR APP NAME>”,
"query": {
"value": "Sudan crises",
"operator": "AND"
},
"filter": {
"conditions": [
{
"field": "format.name",
"value": "Situation Report"
}
]
},
"limit": 5,
"offset": 0,
"fields": {
"include": [
"title",
"body",
"url",
"source",
"date",
"format",
"status",
"primary_country",
"id"
]
},
"preset": "latest",
"profile": "list"
}
[上述内容对应于这个网站查询]
关于调用 API 有一点需要注意的是,如果 API 结果直接由 LLM 处理,它们可能会产生费用。我在这里写了一些相关内容,但对于少量数据,上述方法应该足够。
摘要
尽管该演示应用程序的重点是回答特定问题,但在流程中已包含了一个摘要节点,以展示 LLM 执行多项任务的可能性。这就是 Prompt Flow 表现良好的地方,能够协调复杂的多任务流程。
LLM 摘要是一个活跃的研究领域,并且带来了一些有趣的挑战。任何摘要都会丢失原始文档中的信息,这是预期的。然而,控制哪些信息被排除是很重要的,并且会根据需求有所不同。在总结一个 ReliefWeb 的情况报告时,某些场景下可能需要确保与难民迁移相关的所有指标都被准确表示。其他场景可能需要将与基础设施相关的信息作为重点。关键点在于,摘要提示可能需要根据观众的需求进行定制。如果不是这种情况,也有一些有用的通用摘要提示,比如Chain of Density (CoD),旨在捕捉相关信息。
演示应用程序有两个摘要提示,其中一个非常基础……
system:
You are a humanitarian researcher who needs produces accurate and consise summaries of latest news
========= TEXT BEGIN =========
{{text}}
========= TEXT END =========
Using the output from reliefweb above, write a summary of the article.
Be sure to capture any numerical data, and the main points of the article.
Be sure to capture any organizations or people mentioned in the article.
以及一个使用 CoD 的变体……
system:
Article:
{{text}}
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
You are an expert in writing rich and dense summaries in broad domains.
You will generate increasingly concise, entity-dense summaries of the above JSON list of data extracted.
Repeat the following 2 steps 5 times.
- Step 1: Identify 1-3 informative Entities from the Article
which are missing from the previously generated summary and are the most
relevant.
- Step 2: Write a new, denser summary of identical length which covers
every entity and detail from the previous summary plus the missing entities
A Missing Entity is:
- Relevant: to the main story
- Specific: descriptive yet concise (5 words or fewer)
- Novel: not in the previous summary
- Faithful: present in the Article
- Anywhere: located anywhere in the Article
Guidelines:
- The first summary should be long (5 paragraphs) yet
highly non-specific, containing little information beyond the entities
marked as missing.
- Use overly verbose language and fillers (e.g. "this article discusses") to
reach approx.
- Make every word count: re-write the previous summary to improve flow and
make space for additional entities.
- Make space with fusion, compression, and removal of uninformative phrases
like "the article discusses"
- The summaries should become highly dense and concise yet self-contained,
e.g., easily understood without the Article.
- Missing entities can appear anywhere in the new summary.
- Never drop entities from the previous summary. If space cannot be made,
add fewer new entities.
> Remember to use the exact same number of words for each summary.
Answer in JSON.
> The JSON in `summaries_per_step` should be a list (length 5) of
dictionaries whose keys are "missing_entities" and "denser_summary".
问题回答
演示应用程序包含一个节点来回答用户的原始问题。为此,我们使用了如下提示:
system:
You are a helpful assistant. Using the output from a query to reliefweb,
anser the user's question.
You always provide your sources when answering a question, providing the
report name, link and quote the relevant information.
{{reliefweb_data}}
{% for item in chat_history %}
user:
{{item.inputs.question}}
assistant:
{{item.outputs.answer}}
{% endfor %}
user:
{{question}}
这是一个基本的提示,其中包括请求在任何答案中包含参考和链接。
信息来源的归属
即使对 LLM 输出进行验证和自动事实检查,提供数据来源的归属链接依然非常重要,以便人工可以自行检查。在某些情况下,提供一个不确定的答案——明确告知用户不确定性——仍然可能是有用的,只要提供了信息来源的链路,供进一步的人工验证。
在我们的示例中,这意味着链接到用于回答用户问题的情况报告。这允许提问者跳转到源头,自己核对事实,并阅读更多的背景信息。在演示应用程序中,我们包含了两种归属方法。第一种是在提示中包含请求,如上所示。与任何 LLM 输出一样,这当然可能导致幻觉,但正如我们下面所看到的,这些可以进行验证。
第二种方法是简单地整理在 API 调用中返回的文档列表,即所有被审阅的源,即使某些源未在答案中使用。能够查看完整列表有助于识别因 API 查询方式而可能错过的关键报告。
两种归属方法对用户理解答案的来源都很有帮助。
自动事实检查
LLM 信息提取虽然令人惊叹,但并不完美。在对内容提出问题时,幻觉和信息遗漏是可能发生的。因此,验证答案以确保其不提供错误或不完整的信息是至关重要的。由于我们本质上是在将一篇文本(从 API 返回的原始数据)与 LLM 生成的文本(问题回答)进行比较,我们也可以使用 LLM 来进行验证。通过良好的提示设计,这些方法可以很好地工作,尽管不能做到绝对完美。
在演示应用中,我们包含了两种方法来说明这一点……
实时事实检查
一个用于检查 LLM 生成的输出与源数据一致性的有用库是deep eval,它提供了广泛的 LLM 生成输出测试。在我们的示例中,我们实现了一个忠实度检查,比较答案与原始 API 输出,并给出 0 到 1 之间的分数及其原因,其中 1 是最佳得分。在演示应用中,即使得分低于 1,我们也选择允许将答案展示给用户,并附上警告和得分低于完美的原因。
这是一个示例答案,显示了对提问“苏丹危机中互联网供应商的当前状态是什么?”的警告,我们故意调整答案,表示影响是轻微的,而不是源数据中提到的重大通讯中断……
根据世界粮食计划署(WFP)领导的紧急电信集群(ETC)在“苏丹,冲突 — ETC 情况报告#15,报告期:2024 年 2 月 1 日至 2024 年 2 月 29 日”中报告,苏丹互联网服务提供商的当前状态显示,由于苏丹武装部队(SAF)与快速支持部队(RSF)之间的冲突,电信基础设施仅发生了轻微中断。以下是报告中的相关摘录:
“根据最新报告,苏丹三大主要互联网运营商在 2 月 7 日停机,属于轻微的网络通讯中断,影响了数百万人的通讯。”
这些信息表明,截至报告期,苏丹发生了轻微的网络中断,苏丹的主要互联网运营商处于离线状态,这可能对该国的通讯和响应工作产生轻微影响。
若想了解更多细节,可以访问 ReliefWeb 上的完整报告:Sudan, conflict — ETC Situation Report #15。
警告! 事实检查员评估返回的得分为 0.88/1.0
原因:
得分为 0.88,因为实际输出错误地轻描淡写了苏丹电信基础设施损毁的程度,仅提到轻微的中断,而检索上下文则表明,电信基础设施和国家电网遭遇了广泛的损坏。
请注意末尾的警告部分及相关原因。
然而需要注意的是,尽管深度评估提供了一种评估 LLM(大语言模型)的方法,但由于它使用 LLM,因此它有时也可能遭遇幻觉问题。在演示应用程序中,重新运行同一问题 20 次的性能是可以接受的,但在生产环境中,包含自测以评估评估结果(!)并确保行为符合预期是有意义的。
批量一致性测试
Prompt Flow 支持的另一种方法是能够创建一个test 文件,其中包含输入和上下文信息,可以在 Prompt Flow 批量运行中执行。这类似于软件自检,唯一的不同是,在评估 LLM 时,由于每次响应可能会略有不同,因此在测试中使用 LLM 也非常有用。在示例应用中,有一个“合理性”测试正是为批量运行设计的,在该测试中,所有测试的输出会被整理和总结,从而可以跟踪性能变化。
我们在示例应用中包含了批量测试节点以供演示,但在实际应用中,这些节点不需要,并且可以删除以提高性能。
最后,值得注意的是,尽管我们可以实施策略来减轻与 LLM 相关的问题,但任何软件都可能存在 bug。如果从 API 返回的数据本身就不包含所需的信息,那么再多的 LLM 魔力也无法找到答案。例如,从 ReliefWeb 返回的数据受到搜索引擎的强烈影响,因此如果没有使用最佳的搜索词,重要报告可能不会包含在原始数据中。LLM 的事实核查无法解决这个问题,因此,切勿忘记传统的自检和集成测试。
LLMOps
现在,我们在 Prompt Flow 中有了批量测试,可以将这些测试作为我们 DevOps 或 LLMOps 流程的一部分。示例应用仓库包含一组GitHub Actions,它会自动运行测试,并检查聚合结果,以自动确认应用是否按预期运行。这种确认可以用来控制应用是否部署。

部署
这将引导我们进入部署环节。Prompt Flow 提供了简便的部署方式,这是一个非常棒的功能,可以节省时间,从而将更多精力投入到解决用户需求上。
“构建”选项会建议两个选项:“作为本地应用构建”和“作为 Docker 构建”。

第一个选项非常有用,它会启动一个聊天界面,但仅用于测试,不能用于生产环境。第二个选项将构建一个 Docker 容器,以呈现一个运行流程的 API 应用。这个容器可以部署在支持 Docker 的平台上,并与前端聊天界面(如 Streamline、Chainlit、Copilot Studio 等)配合使用。如果使用 Docker 进行部署,那么如何观察应用的使用情况——确保 AI 安全的必备功能——需要在托管 Docker 容器的服务上进行配置。
对于使用 Azure 的用户,流程可以导入到 Azure 机器学习中,在那里可以像在 VS Code 中一样进行管理。这里的一个额外功能是,可以通过点击按钮将其部署为 API。这是一个很好的选择,因为部署可以配置以包含详细的可观察性和安全监控,尽管需要一些成本,但付出的努力很少。
最终思考
我们快速探索了在操作化大语言模型(LLM)时需要实现的一些重要概念:内容安全、事实检查(实时和批量)、事实归属、提示工程和 DevOps。这些概念是通过 Prompt Flow 实现的,这是一个用于开发 LLM 应用程序的强大框架。
我们使用的演示应用程序只是一个展示,但它展示了在考虑到将 LLM 应用程序安全生产化的各个方面时,简单任务如何迅速变得复杂。
注意事项和权衡
和所有事情一样,实施上述一些项目时会有权衡。增加安全性测试和实时评估会减慢应用响应时间,并带来一些额外的成本。对我而言,这是一个可以接受的权衡,因为它确保了解决方案的安全性和准确性。
此外,虽然 LLM 评估技术在使应用程序更值得信赖和安全方面迈出了重要步伐,但使用 LLM 进行评估并不是万无一失的,有时会失败。这可以通过更多地工程化演示应用程序中的 LLM 输出,以及 LLM 能力的进展来解决——毕竟这是一个相对较新的领域——但值得在这里提到的是,应用程序设计应包括对评估技术的评估。例如,可以创建一组自我测试,定义上下文和问题答案,并将其通过评估工作流,以确保它在动态环境中按预期工作。
希望你喜欢这篇文章!
参考文献
-
从稀疏到密集:使用密度提示链进行 GPT-4 摘要,Adams 等,2023
-
GLiNER:使用双向 Transformer 的命名实体识别通用模型,Zaratiana 等,2023
-
本文的代码可以在这里找到
如果你喜欢这篇文章,请点赞,如果你关注我,我将非常高兴!你可以在 这里**找到更多文章。
一种机器学习莫比乌斯环:模型能够相互学习吗?
合成数据和学习范式中的新颖转折
·发布于 Towards Data Science ·14 分钟阅读·2024 年 8 月 27 日
--

一颗金属之心 — 图片来源:Leonardo.ai
人工智能的炒作对那些关注其基本原理的人有实际的好处。这个领域发展迅速,阅读初步研究令人出乎意料的愉快。然而,这种节奏的负面影响是伴随而来的庞杂(有时是混乱且不连贯的)术语。因此,我常常翻阅我十年前的讲义或书籍,来弥补知识上的空白。结合我对尝试新发现的渴望,这让我对人机学习的交叉点重新产生了兴趣。
神经网络,现代人工智能的基础,灵感来源于人脑的结构。重新审视这一事实让我提出了一个看似简单的问题:机器能否像人类学习一样相互学习?
尽管这个话题并不新颖——事实上,它是神经网络的基础——但它的广泛意义,从反乌托邦的情景到先进的人工智能演示带来的兴奋,令人着迷。除了这种潜在的人工智能自催化的感觉外,我的问题还带有一些直接的相关性。出现了两个交织的问题。首先,许多数据科学家承认,数据的挑战日益增长……
一种混合方法的新闻推荐系统离线评估
结合读者调查反馈和行为点击数据,以优化内容个性化。
·发表于Towards Data Science ·阅读时间:8 分钟·2024 年 10 月 11 日
--

在合适的时间为读者提供相关内容。图片由作者提供。
在数字新闻中,点击文章的决定受到多种因素的影响。从标题和热门话题到文章的布局,甚至读者的心情,新闻消费背后的复杂性既令人着迷又具有挑战性。考虑到这些不同的影响因素,我们提出了一个关键问题:读者过去的行为在多大程度上影响他们当前的选择?
在DER SPIEGEL(德国之声)上,我们正在解决这个问题,开发一个新闻推荐系统。我们的目标是为读者在合适的时间提供相关内容。然而,这个目标也带来了一个挑战——在系统上线之前,我们如何有效地评估和优化系统?我们的解决方案是采用混合方法的离线评估。通过将历史点击数据与通过调查收集的新闻项目偏好结合起来,我们开发了一种旨在改善我们理解和预测读者行为的方法。在描述这种方法的细节之前,理解为什么传统的新闻推荐系统离线评估方法可能存在局限性是非常重要的。
评估新闻推荐系统的挑战
离线评估是推荐系统开发中的一个关键步骤。它们帮助在上线前选择最有前景的算法和参数。通过使用用户的历史点击数据,我们可以评估我们的推荐系统如何预测读者实际选择的项目。[1] 但是,评估新闻推荐系统具有挑战性。大多数新闻文章的生命周期较短,用户偏好会迅速变化,尤其是基于当前事件的变化。平衡用户兴趣、编辑优先级和伦理考虑也十分困难。
传统的离线评估通常仅依赖历史点击数据,这可能无法全面捕捉这些因素。它们无法告诉我们用户是否真正喜欢他们点击的文章,或者他们是否可能更喜欢那些他们没有点击的文章,因为他们可能根本没看到它。[1] 此外,经典的方法通常偏向于非个性化的、基于流行度的算法。[2]
然而,离线实验在研究和开发阶段似乎特别具有吸引力。学术研究通常仅依赖离线实验,主要是因为研究人员很少能够访问在线测试的生产系统。[3] 离线方法可以在不需要实时用户交互的情况下,以成本效益高的方式比较各种算法。[4] 但也广泛认同,在线实验提供了系统性能的最强证据,因为它们涉及真实用户执行真实任务。我们的方法旨在填补这一空白,提供稳健的离线洞察,指导随后的在线测试。
我们的方法:结合用户调查与行为数据
为了克服传统离线评估的局限性,我们开发了一种混合方法,将用户调查与行为数据分析相结合。正如论文《主题偏好胜过其他特征在新闻推荐中的应用》[5]中所示,研究人员通过调查收集用户关于其主题偏好的反馈,以了解他们对特定新闻文章的参与情况。受到该方法的启发,我们正在使用点击历史与调查反馈结合的方式,而不是直接询问用户的偏好。以下是具体方法:
-
文章选择:我们开发了一种基于发布日期和最近流量来选择调查文章的方法。这种方法确保了新文章和仍然相关的旧文章的混合。
-
用户调查:我们对约 1500 名SPIEGEL.de读者进行了调查。每位参与者对 15 篇文章预览进行了评分,评分范围从 0(低兴趣)到 1000(高兴趣),并且可以标注出曾经阅读过的文章。
-
行为数据分析:对于每个参与者,我们分析了他们在调查之前的历史点击数据。我们将文章转换为数值嵌入,以计算平均用户嵌入,代表读者的全球口味。然后,我们计算了用户偏好向量与调查中评分文章的嵌入之间的余弦距离。[6]

用户调查截图。
在整个过程中,我们识别了几个显著影响模型效果的参数。这些包括:点击历史中要包含的文章类型(是否有付费墙)、每篇文章的最低阅读时长阈值、用户点击历史的回溯期、嵌入模型的选择、内容的嵌入方式、以及使用每篇文章的总体访问量进行重新排序。为了评估我们的方法并优化这些参数,我们使用了两个主要指标:Spearman 相关系数,它衡量文章评分与用户偏好向量之间的关系;以及Precision@K,它衡量我们的模型将最高评分的文章放入前 K 个推荐中的效果。

比较调查中的前 5 篇文章与不同排序方法的效果。图像来源:作者。
为了说明我们的评估方法,我们可以为每个用户想象出四个相同文章的列表,每个列表的排序方式不同:
-
调查评分:这个列表代表我们的真实情况,显示了用户在调查中给出的实际评分。我们的建模方法旨在尽可能准确地预测这个列表。
-
随机排序:这作为我们的基准,模拟了一个没有用户信息的场景,我们随机猜测用户的新闻偏好。
-
总体覆盖率:这个列表是根据每篇文章在所有用户中的总体受欢迎程度进行排序的。
-
用户嵌入:这个列表是根据每篇已评分文章与用户平均嵌入之间的余弦距离进行排序的。这个方法的参数通过网格搜索进行优化,以实现最佳性能。
通过比较这些列表,我们可以评估我们的用户嵌入方法与真实情况以及简单方法(如随机选择或基于受欢迎度的排序)之间的表现差异。这个比较让我们能够量化个性化推荐方法的效果,并确定最佳的参数设置。
结果与关键发现
我们的混合方法离线评估显示出有前景的结果,证明了我们的推荐系统的有效性。随机基准,如预期的那样,表现最差,precision@1 为 0.086。基于覆盖度的方法,通过按文章的整体受欢迎程度排序,略有改善,precision@1 为 0.091。然而,我们的个性化模型相较于随机基准和基于覆盖度的方法,展示了显著的改进。该模型达到了 precision@1 为 0.147,比随机基准提高了 70.7%。这种性能提升在不同的 k 值下持续存在。

另一个例子:如果我们从展示的 15 篇文章预览中随机选择 5 篇,并将其与用户评分最高的 5 篇文章进行比较,我们的平均 precision 为 5/15 = 33%。由于并非每个用户都实际评分了 15 篇文章(有些将文章标记为已读),因此我们数据中的实际 Precision@5 为 38%(见上图)。个性化模型的平均 Precision@5 为 45%。与随机模型相比,这提升了 17%(见下图)。注意:随着 K 值的增加,随机相关元素被纳入推荐集的概率也会增加。完美精度的收敛:如果 K 达到或超过 15(相关元素的总数),每种方法(包括随机方法)都将包含所有相关元素,并达到 1.0 的精度。

除了 Precision@K,Spearman 相关系数也突显了我们个性化方法的优势。我们的模型达到了 0.17 的相关性,且 p 值小于 0.05。这表明我们的模型预测与实际用户偏好之间存在一致性。
所描述的结果表明,项目评分与用户偏好向量之间的距离存在相关性。尽管所有模型的精度都相对较低,但提升幅度相当高,尤其是在低 K 值时。由于在生产环境中,我们每个用户的候选池中将有显著超过 15 篇文章,因此低 K 值的提升尤为重要。
结论
尽管我们的混合方法离线评估提供了坚实的基础,我们认识到,真正的考验将在上线时到来。我们将离线评估中的洞察和优化参数作为在线 A/B 测试的起点。此方法使我们能够弥合离线评估与在线表现之间的差距,为更有效的转向实时测试和迭代奠定基础。
随着我们不断优化方法,我们始终致力于在技术创新和新闻诚信之间找到平衡。我们的目标是开发一个新闻推荐系统,确保个性化推荐不仅准确,而且对多样性进行排名。这确保了在优化个体偏好的同时,我们也能保持广泛的视角和话题,维护DER SPIEGEL以全面和公正的新闻标准著称。
感谢阅读 🙏
希望你喜欢,如果喜欢的话,请给我鼓掌。若有任何问题或进一步讨论,欢迎随时通过 LinkedIn 与我联系。
作为DER SPIEGEL的数据科学家,我有权访问专有的用户数据和点击历史,这些数据构成了本研究的基础。这些数据不对外公开。所有展示的结果都是聚合和匿名处理的,以保护用户隐私,同时展示我们在新闻推荐中的方法论。
参考文献
[1] Garcin, Florent & Faltings, Boi & Donatsch, Olivier & Alazzawi, Ayar & Bruttin, Christophe & Huber, Amr. (2014). 在 swissinfo.ch 上进行新闻推荐系统的离线和在线评估。RecSys 2014 —— 第八届 ACM 推荐系统会议论文集。
[2] Cremonesi, Paolo & Koren, Yehuda & Turrin, Roberto. (2010). 推荐算法在 Top-N 推荐任务中的表现。RecSys’10 —— 第四届 ACM 推荐系统会议论文集。
[3] Castells, Pablo & Moffat, Alistair. (2022). 离线推荐系统评估:挑战与新方向。AI Magazine。
[4] Janzen, Noah & Gedikli, Fatih. (2023). NewsRecs:一个用于进行和评估新闻推荐系统在线实验的移动应用框架。
[5] Knudsen, Starke, Trattner. (2023). 主题偏好在新闻推荐中优于其他特征:基于来自挪威代表性样本的联合分析。计算机协会(ACM)RecSys ’23。
[6] Rosnes, Daniel & Starke, Alain & Trattner, Christoph. (2024). 塑造基于内容的新闻推荐系统的未来:评估特征特定相似性度量的洞察。
一种新的 AI 安全方法:层增强分类(LEC)
LEC 通过将机器学习分类器的高效性与 LLM 的语言理解结合,超越了像 GPT-4o 这样的顶级模型
·发表于 Towards Data Science ·8 分钟阅读·2024 年 12 月 20 日
--
想象一下,你正坐在会议室里,讨论着我们这个时代最具变革性的技术——人工智能,并意识到我们正乘坐一艘没有可靠安全带的火箭。由英国政府主办并得到 29 个国家支持的 AI 安全峰会上发布的《布莱奇利声明》完美地捕捉了这一情感[1]:
“这些 AI 模型最强大的能力可能会带来严重甚至灾难性的伤害,无论是故意的还是无意的。”

来源: Dalle3
然而,现有的 AI 安全方法迫使组织在成本、速度和准确性之间做出一个无法取胜的权衡。传统的机器学习分类器难以捕捉自然语言和 LLM 的微妙差异,而强大的 LLM 引入了巨大的计算开销——需要额外的模型调用,这导致每个 AI 安全检查的成本上升。
我们的团队(Mason Sawtell、Sandi Besen、Tula Masterman、Jim Brown)提出了一种名为 LEC(层增强分类)的新方法。

图片来自:Sandi Besen, Tula Masterman, Mason Sawtell, Jim Brown
我们证明 LEC 结合了机器学习分类器的计算效率和 LLM 的复杂语言理解——因此你不需要在成本、速度和准确性之间做出选择。LEC 超越了像 GPT-4o 这样的顶尖模型以及专门训练用于识别不安全内容和提示注入的模型。更棒的是,我们相信 LEC 可以被修改以应对非 AI 安全相关的文本分类任务,如情感分析、意图分类、产品分类等。
这些影响是深远的。无论你是一个在 AI 安全复杂领域中航行的技术领导者,一个在缓解潜在风险的产品经理,还是一个在制定负责任创新战略的高管,我们的方法都提供了一个可扩展且适应性强的解决方案。

图 1:一个调整后的模型推理管道示例,包含 LEC 分类器。图片来源:Sandi Besen, Tula Masterman, Mason Sawtell, Jim Brown
进一步的细节可以在 完整论文的预印本中找到,或者在 Tula Masterman 的总结文章 中了解更多有关论文的信息。
将 LEC 应用于负责任的 AI 使用案例
负责任的 AI 已成为整个技术生态系统中领导者的关键优先事项——从模型开发者如 Anthropic、OpenAI、Meta、Google 和 IBM,到企业咨询公司和 AI 服务提供商。随着 AI 采纳的加速,其重要性变得更加突出。
我们的研究特别针对 AI 安全中的两个关键挑战——内容安全和提示注入检测。内容安全指的是识别和防止生成有害、不当或可能对用户造成危险的内容,这些内容可能会带来风险或违反伦理指南。提示注入涉及检测通过构造输入提示来操控 AI 系统的尝试,目的是绕过安全机制或迫使模型生成不道德的输出。
为了推动伦理 AI 领域的发展,我们将 LEC 的能力应用于现实世界中的负责任 AI 使用案例。我们的希望是,这种方法能够广泛采用,帮助让每个 AI 系统都不那么容易受到剥削。
使用 LEC 进行内容安全任务
我们策划了一个包含 5,000 个示例的内容安全数据集,用于在二分类(2 类)和多分类(>2 类)任务中测试 LEC。我们使用了 OpenSafetyLab 的 SALAD Data 数据集[3]来代表不安全内容,并使用了 LMSYS 的“LMSYS-Chat-1M”数据集来代表安全内容[4]。
对于二分类任务,内容要么是“安全的”,要么是“不安全的”。对于多分类任务,内容要么被分类为“安全的”,要么被分配到一个特定的“不安全”类别。
我们将使用 LEC 训练的模型与 GPT-4o(广泛被认为是行业领先者)、Llama Guard 3 1B 和 Llama Guard 3 8B(专门训练来处理内容安全任务的特殊目的模型)进行了比较。我们发现,使用 LEC 的模型在训练样本仅为 20 个的二分类任务和 50 个的多分类任务时,超越了我们比较的所有模型。
表现最佳的 LEC 模型在二分类任务中达到了加权 F1 分数 0.96(满分 1),相比之下,GPT-4o 的分数为 0.82,LlamaGuard 8B 的分数为 0.71。
这意味着,仅需 15 个示例,使用 LEC 你就能训练出一个模型,在识别安全或不安全内容时,超越行业领导者,而且计算成本极低。

内容安全结果总结。图片来源:Sandi Besen、Tula Masterman、Mason Sawtell、Jim Brown
使用 LEC 识别提示注入
我们使用 SPML Chatbot 提示注入数据集策划了一个提示注入数据集。我们选择 SPML 数据集是因为它在展示真实世界聊天机器人场景时具有多样性和复杂性。该数据集包含了一对系统和用户的提示,用于识别那些试图违抗或操控系统提示的用户提示。这对那些部署面向公众的聊天机器人、仅用于回答特定领域问题的企业尤其相关。
我们将使用 LEC 训练的模型与 GPT-4o(行业领先者)以及 deBERTa v3 Prompt Injection v2(专门训练用以识别提示注入的模型)进行了比较。我们发现,使用 LEC 的模型在训练样本为 55 个的 GPT-4o 和训练样本仅为 5 个的特殊目的模型面前表现更优。
表现最佳的 LEC 模型在多分类任务中达到了加权 F1 分数 0.98(满分 1),相比之下,GPT-4o 的分数为 0.92,deBERTa v2 Prompt Injection v2 的分数为 0.73。
这意味着,仅需 5 个示例,使用 LEC 你就能训练出一个模型,在识别提示注入攻击时超越行业领导者。

提示注入结果总结。图片来源:Sandi Besen、Tula Masterman、Mason Sawtell、Jim Brown
完整的结果和实验实现细节可以在 Arxiv 预印本中找到。
你的企业如何从使用 LEC 中受益
随着组织越来越多地将 AI 融入其业务运营,确保 AI 驱动交互的安全性和完整性已经成为关键任务。LEC 提供了一种强大且灵活的方式来确保潜在的不安全信息被检测到——从而减少操作风险并提高最终用户的信任度。LEC 模型可以通过多种方式融入到您的 AI 安全工具包中,以防止在使用 AI 工具时出现不必要的漏洞,包括在语言模型推理过程中、推理前/后,甚至在多代理场景中。
语言模型推理过程中
如果您正在使用开源模型或能够访问闭源模型的内部工作原理,可以将 LEC 作为推理流水线的一部分,实时保障 AI 安全。这意味着,如果在信息通过语言模型时出现任何安全问题,可以立即停止输出的生成。图 1 展示了这一过程的示例。
语言模型推理前/后
如果您无法访问语言模型的内部工作原理,或者希望将安全检查作为单独的任务执行,您可以在调用语言模型之前或之后使用 LEC 模型。这使得 LEC 能够与像 Claude 和 GPT 这样的闭源模型兼容。
将 LEC 分类器集成到您的部署流水线中,可以避免将潜在有害的内容传递到您的语言模型中,并/或在输出返回给用户之前检查有害内容。
使用 LEC 分类器与代理
代理型 AI 系统可能会放大任何现有的非预期行为,导致不良后果的累积效应。LEC 分类器可以在代理场景的不同阶段使用,以保护代理免受接收或生成有害输出的风险。例如,通过将 LEC 模型集成到您的代理架构中,您可以:
-
检查请求是否可以开始处理
-
确保调用的工具不会违反任何 AI 安全准则(例如,为关键字搜索生成不适当的搜索主题)
-
确保返回给代理的信息无害(例如,从 RAG 搜索或谷歌搜索返回的结果是“安全的”)
-
在将代理的最终响应返回给用户之前进行验证
如何基于语言模型访问实现 LEC
拥有访问模型内部工作的企业可以直接在推理流水线中集成 LEC,从而在 AI 内容生成过程中实现持续的安全监控。当通过 API 使用闭源模型(如 GPT-4)时,企业无法直接访问训练 LEC 模型所需的底层信息。在这种情况下,可以在模型调用之前和/或之后应用 LEC。例如,在 API 调用之前,可以筛查输入是否包含不安全内容;调用后,可以验证输出是否符合业务安全协议。
无论你选择以何种方式实现 LEC,使用其强大能力都能在比现有技术更短的时间和更低的成本下,为你提供更优的内容安全性和提示注入防护。
结论
层级增强分类(LEC)是我们所乘坐的人工智能火箭船的安全带。
价值主张非常明确:LEC 的人工智能安全模型可以减轻监管风险,帮助确保品牌保护,并增强用户对人工智能驱动互动的信任。它标志着人工智能发展的新时代,在这个时代,准确性、速度和成本不再是相互竞争的优先事项,人工智能安全措施可以在推理时、推理前或推理后进行处理。
在我们的内容安全实验中,表现最好的LEC 模型在二分类任务中达到了 0.96 的加权 F1 分数,远超 GPT-4o 的 0.82和 LlamaGuard 8B 的 0.71 分,而这一成绩是在仅使用 15 个训练样本的情况下取得的。同样,在提示注入检测中,我们最优秀的 LEC 模型达到了 0.98 的加权 F1 分数,而 GPT-4o 为 0.92,deBERTa v2 提示注入 v2 为 0.73,且该成绩仅用了 55 个训练样本。这些结果不仅展示了卓越的性能,还突显了 LEC 在极少训练数据下实现高准确度的非凡能力。
尽管我们的工作集中在使用 LEC 模型处理人工智能安全用例上,但我们预期这种方法可以应用于更广泛的文本分类任务。我们鼓励研究界将我们的工作作为探索更多可能性的跳板——进一步开辟更智能、更安全、更值得信赖的人工智能系统的新道路。
注意:本文和论文中表达的意见仅代表作者本人,不一定反映其各自雇主的观点或政策。
有兴趣联系吗?可以在Linkedin上私信我!我总是乐于参与思想碰撞,并在我的工作中进行迭代。
参考文献:
[3] huggingface.co/datasets/OpenSafetyLab/Salad-Data
[4] huggingface.co/datasets/lmsys/lmsys-chat-1m
新的相关系数

作者图片
·发表于Towards Data Science ·阅读时间:10 分钟·2024 年 3 月 31 日
--
如果有人告诉你,存在一种新的方法来衡量两个变量之间的关系,就像相关性一样,但可能更好呢?更具体来说,2020 年发表了一篇名为A New Coefficient of Correlation[1]的论文,介绍了一种新的衡量方法,当且仅当两个变量独立时其值为 0,当且仅当一个变量是另一个变量的函数时其值为 1,最后还具有一些良好的理论性质,允许进行假设检验,同时在实际应用中对数据几乎不做任何假设。在深入讨论之前,让我们简要了解一下传统的相关性衡量方法是如何工作的。
相关性复习
很少有工具能像流行的相关系数那样,帮助理解数据,并且被广泛使用(也常常被误用)。这个工具正式被称为皮尔逊的r,是几乎每一门入门统计学或商业课程中教授的样本相关系数,可以说是每个数据专业人士必须熟悉的第一工具。这其中的原因有很多,其中之一是它既易于使用又容易解释。作为提醒,样本相关系数衡量的是两个变量之间的线性关系,并且可以通过以下公式计算。

最后提醒一下,这个值的范围是从-1 到+1,负值表示两个变量之间存在反向的线性关系,正值则表示相反的情况。请注意,至此我们一直强调的是衡量线性关系。线性关系可以理解为关系的形态大致可以用一条直线来表示。

对大多数人来说,现实世界中线性关系的出现是罕见的,这一点应该不足为奇。这就是为什么几十年来,像斯皮尔曼的ρ(rho)和肯达尔的τ(tau)等其他度量方法应运而生的原因。这些新的度量方法在识别单调关系方面要比线性关系更为有效,而这也使得它们更具鲁棒性,因为线性关系只是单调关系的一种特殊形式。单调关系基本可以理解为始终增加或始终减少的关系。

大多数时候,相关性被用来识别的并不一定是两个变量之间的线性或单调关系,而是试图识别是否存在任何关系。这就带来了问题,因为如果关系既不是线性的也不是单调的,这些现有的度量方法就无法很好地工作。请注意,下面的图形都清楚地展示了两个变量之间存在强关系,但常用的相关性技术仅擅长识别单调关系。

尽管这些相关性方法存在明显的局限性,但它们仍被广泛用于得出有关各种数据的结论。那么,是否有可能识别出比上述示例中更为复杂的关系呢?这时,引入了新的系数ξ(发音为“ksaai”)。
在继续之前,最后提一下,2022 年发布了一篇名为《关于线性和单调关联的神话》[2]的论文,讨论了哪种常见的相关性度量方法适用于哪种类型的数据。早些时候,我曾建议,皮尔逊的r最适合用于线性度量,而斯皮尔曼的ρ和肯达尔的τ则更适用于单调数据。尽管这一规则在许多数据专业人士中广为流传并被普遍应用,但这篇论文指出,这不一定是事实,实际上在某些情况下,情况可能正好相反。
公式
在介绍公式之前,首先需要完成一些准备工作。正如我们之前所说,相关性可以被看作是衡量两个变量之间关系的一种方式。假设我们正在衡量 X 和 Y 之间当前的相关性。如果确实存在线性关系,那么它可以被认为是互相共享的,这意味着 X 和 Y 之间的相关性总是等于 Y 和 X 之间的相关性。然而,在这种新方法中,我们不再衡量 X 和 Y 之间的线性关系,而是我们的目标是衡量 Y 在多大程度上是 X 的函数。理解这种细微但重要的区别将使得理解公式变得更容易,因为通常来说,ξ(X,Y) 不再等于 ξ(Y,X)。
继续沿着这个思路,假设我们仍然想衡量 Y 在多大程度上是 X 的函数。请注意,每个数据点都是 X 和 Y 的有序对。首先,我们必须将数据按 (X₍₁₎,Y₍₁₎), … ,(X₍ₙ₎,Y₍ₙ₎) 的顺序排序,使得 X₍₁₎ ≤ X₍₂₎ ≤ ⋯ ≤ X₍ₙ₎。换句话说,我们必须根据 X 排序数据。然后我们就能创建变量 r₁, r₂, … ,rₙ,其中 rᵢ 等于 Y₍ᵢ₎ 的排名。现在这些排名已经确定,我们就可以开始计算了。
根据你所使用的数据类型,有两种公式。如果数据中不可能有平局(或极不可能有平局),则我们有

如果允许平局,则有

其中 lᵢ 定义为满足 Y₍ⱼ₎ ≥ Y₍ᵢ₎ 的 j 的数量。最后一个重要的说明是当允许平局时。除了使用第二个公式外,为了获得最佳的估计,重要的是随机排序观察到的平局,以便选择一个值被排名为高于/低于另一个值,这样 (rᵢ₊₁ — rᵢ) 就不会像之前那样等于零。然后,变量 lᵢ 就是 Y₍ᵢ₎ 实际大于或等于的观察数量。
为了不深入探讨理论,还值得简要指出,这种新的相关性背后有一些很好的渐近理论,使得在不对基础分布做任何假设的情况下,进行假设检验变得非常容易。这是因为这种方法依赖于数据的排名,而不是数据本身的值,因此它是一种无参数统计量。如果 X 和 Y 是独立的,并且 Y 是连续的,那么

这意味着如果你有足够大的样本量,那么这个相关性统计量大致遵循正态分布。如果你想测试两个变量之间的独立性程度,这会非常有用。
代码
随着这一新方法的发布,R 包XICOR也已发布,其中包含一些相关的函数,包括一个名为 xicor() 的函数,当提供 X 和 Y 向量或矩阵时,可以轻松计算统计量 ξ,并提供假设检验的 p 值等附加信息。与我撰写的所有文章一样,我将提供 R、Python 和 Julia 的原始函数,您可以选择复制并保留它们。在这里,X 和 Y 必须是向量或数组,对于 Python 和 Julia,您可能需要分别安装 NumPy 和 Random 包。
我需要说明的是,这三个函数写得非常简单,仅用于介绍性目的。话虽如此,对于任何专业用途,我鼓励使用由专业库或更高效的贡献者提供的更高效的函数。例如,这里是由Itamar Faran编写的一个 Python 函数,其执行速度远快于下面提供的函数。
## R Function ##
xicor <- function(X, Y, ties = TRUE){
n <- length(X)
r <- rank(Y[order(X)], ties.method = "random")
set.seed(42)
if(ties){
l <- rank(Y[order(X)], ties.method = "max")
return( 1 - n*sum( abs(r[-1] - r[-n]) ) / (2*sum(l*(n - l))) )
} else {
return( 1 - 3 * sum( abs(r[-1] - r[-n]) ) / (n² - 1) )
}
}
## Python Function ##
from numpy import array, random, arange
def xicor(X, Y, ties=True):
random.seed(42)
n = len(X)
order = array([i[0] for i in sorted(enumerate(X), key=lambda x: x[1])])
if ties:
l = array([sum(y >= Y[order]) for y in Y[order]])
r = l.copy()
for j in range(n):
if sum([r[j] == r[i] for i in range(n)]) > 1:
tie_index = array([r[j] == r[i] for i in range(n)])
r[tie_index] = random.choice(r[tie_index] - arange(0, sum([r[j] == r[i] for i in range(n)])), sum(tie_index), replace=False)
return 1 - n*sum( abs(r[1:] - r[:n-1]) ) / (2*sum(l*(n - l)))
else:
r = array([sum(y >= Y[order]) for y in Y[order]])
return 1 - 3 * sum( abs(r[1:] - r[:n-1]) ) / (n**2 - 1)
## Julia Function ##
import Random
function xicor(X::AbstractVector, Y::AbstractVector, ties::Bool=true)
Random.seed!(42)
n = length(X)
if ties
l = [sum(y .>= Y[sortperm(X)]) for y ∈ Y[sortperm(X)]]
r = copy(l)
for j ∈ 1:n
if sum([r[j] == r[i] for i ∈ 1:n]) > 1
tie_index = [r[j] == r[i] for i ∈ 1:n]
r[tie_index] = Random.shuffle(r[tie_index] .- (0:sum([r[j] == r[i] for i ∈ 1:n])-1))
end
end
return 1 - n*sum( abs.(r[2:end] - r[1:n-1]) ) / (2*sum(l.*(n .- l)))
else
r = [sum(y .>= Y[sortperm(X)]) for y ∈ Y[sortperm(X)]]
return 1 - 3 * sum( abs.(r[2:end] - r[1:end-1]) ) / (n² - 1)
end
end
示例
为了初步了解使用这个新公式可能带来的好处,让我们比较几个模拟示例的计算相关系数值,这些示例突出了每种相关性方法之间的关键差异。



从顶部开始,我们可以看到使用这种新方法的相关性不再告诉你关系的方向,因为值不再是负数。然而,正如预期的那样,当关系变强时,这个值接近 1,而当关系变弱时,它接近 0,就像上述方法一样。
接下来是令人兴奋的部分。从下方的四个图表可以明显看出,这种新方法在识别一般显著关系方面,比传统计算方法更为有效。像上面展示的这些案例正是推动这一新公式研究的主要动力,因为第二个示例表明,皮尔逊的 r 结论是 X 和 Y 之间没有显著关系,而实际上它们之间明显存在一个抛物线关系;第三个示例表明,同样的方法得出结论认为两者之间有轻微的正相关,而实际上,这一关系似乎没有上升趋势。
到目前为止,我们只看了模拟数据。现在让我们用一个真实世界的例子,展示使用这种新相关性方法的一些可视化结果。假设我们想要衡量大脑信号和时间之间的独立性。
以下数据是通过流行的 R 包astsa提供的功能性磁共振成像(fMRI)测量的大脑活动记录,数据以 BOLD 信号的形式呈现。为了提供更多背景信息,该数据集包含了在五个受试者中,皮层、丘脑和小脑八个不同大脑位置的平均反应。每个受试者暴露于手部周期性刷洗 32 秒,然后暂停 32 秒,形成 64 秒的信号周期。数据每 2 秒记录一次,总共记录了 256 秒(n = 128)。

假设我们希望测量大脑的这三部分中哪一部分最可能与时间相关,这意味着它们在执行规定的刺激时参与度最高。从上图来看,皮层信号的噪声最小,而丘脑信号之一的噪声最大,但让我们通过我们新的相关性统计量来量化这一点。下表显示了使用流行的皮尔逊r和新相关性ξ的八个测量值的相关性值。

上表揭示了常用的相关性方法始终显示这些关系为负值或接近零,这意味着几乎没有可观察到的关系,如果有关系的话,它呈现出下降趋势。显然情况并非如此,因为其中一些波长与时间之间存在明显的强关系,而且它们看起来都没有呈现出趋势。
此外,新的相关性值在识别哪些位置最不嘈杂方面表现得更好,这也是本分析的主要目的。结果显示,皮层的部分区域在刺激期间最为显著地被使用,因为这些区域的相关性值平均最高,但丘脑的某些部分也似乎有参与,使用标准方法并不容易检测到这一结果。
结论
还有更多可以做的工作,以继续我们开始的分析,例如使用前面介绍的渐近理论进行正式的独立性假设检验,但本报告的目的是简单地介绍这个新度量,并展示这些计算有多简单以及如何利用这些结果。如果您有兴趣了解更多关于这种方法的优缺点,我鼓励您阅读下文参考资料中介绍该方法的正式出版物。
这种新方法远非完美,但它是为了解决当前普遍接受的方法中一些最显著的问题而创造的。自从发现它以来,我一直在为自己的个人研究使用它,并且它已被证明非常有帮助。
除非另有说明,所有的图像、图表和表格均为作者提供。
[1] S. Chatterjee, 一种新的相关系数(2020),《美国统计学会期刊》。
[2] E. Heuvel 和 Z. Zhan, 线性和单调关联的误区(2022),《美国统计学家》。
中心极限定理的新视角

公有领域/公有领域/图片来源/ 公有领域/公有领域/CC BY-SA 4.0
它的定义、众多应用、与逆概率的深刻关联,以及它的历史一瞥
·发表于 Towards Data Science ·11 分钟阅读·2024 年 1 月 8 日
--
尽管中心极限定理内容丰富,但它有一个简明的定义。简单来说,它阐述了以下内容:该 标准化 的样本和或均值 i.i.d. 随机变量 在分布上收敛 到 N(0,1)。围绕这一核心概念构建了一个适度大小的变体和特例的网络,但定理的主题仍然保持不变。
中心极限定理定义的简洁明了掩盖了其多种不同的用途,这些用途只有在你仔细解构其定义中的词汇并加以应用时才会显现出来。
中心极限定理的定义背后还隐藏着一条长达三百多年的发现之路。在众多数学家的贡献下,中心极限定理在 17、18、19 和 20 世纪间掀起了一场研究人员的淘金热。
在数学思想的发展历史中,很少有那么多的研究人员在如此多的世纪中对单一思想作出如此重大的贡献。但中心极限定理(CLT)并非普通的思想。它是统计学的金标准。
中心极限定理的应用
看到中心极限定理(CLT)的作用时,可能会感到十分吸引人。在接下来的这个视频中,你将看到 CLT 如何作用于从一个指数分布的总体中抽取的大小为 n 的随机样本。这个模拟生成了 1000 个不同的大小为 10 的随机样本。它计算了每个样本的均值,并绘制了这 1000 个均值的频率分布图。这种分布看起来完全不像正态分布。但一旦样本大小从 10 增加到 20、30、40、50 等,你将看到样本均值是如何迫不及待地排列成一个正态分布的。
中心极限定理的应用(作者视频)
中心极限定理到底是什么?
中心极限定理有着不断变化的定义,通常会根据具体背景进行调整。让我们拆解它的定义。
我们将从一个熟悉的随机样本开始。让我们使用符号(X_1, X_2, …, X_n)表示从一个基础总体中随机抽取(有放回)大小为 n 的样本。由于每个样本元素X_i 是独立且随机地(有放回)从总体中选择的,(X_1, X_2, …, X_n)是一组 n 个独立同分布的(i.i.d.)随机变量。我们进一步假设总体的均值为μ,且方差σ²是有限且正的。
设X_bar_n 为样本均值或样本和。它的定义如下:

n 个 i.i.d.随机变量的均值或和(作者图片)
一些统计学教材用X_bar_n 表示样本均值,用S_n 表示样本和。
由于X_bar_n 是 n 个随机变量的函数,X_bar_n 本身是一个随机变量,具有自己的均值和方差,分别用符号 E(X_bar_n)和 Var(X_bar_n)表示。
假设X_bar_n 是均值。由于(X_1, X_2, …, X_n)是 i.i.d.变量,可以证明:
E(X_bar_n) = μ
Var(X_bar_n) = σ²/n
现在让我们定义一个新的随机变量Z_n,如下所示:

X的标准化版本(作者图片)
Z_n,在此定义下,称为标准化样本均值。它是样本均值与总体均值之间的距离,以(可能是分数)标准差的数量表示。你还可以通过另一种方法计算Z_n。如果你使用公式(X_i — μ)/(σ/√n)对原始样本的每个数据点X_i 进行变换,并取变换后样本的简单均值,你将得到标准化样本均值Z_n。
根据上述装置,中心极限定理对于Z_n做出了以下所有等价的陈述:
随着样本大小的增加,Z_n 的累积分布函数(CDF)开始越来越像标准正态随机变量 N(0,1)的 CDF,并且当样本大小趋近于∞时,它与 N(0,1)的 CDF 完全一致。
随着样本大小无限增大,Z_n 的 CDF 趋近于 N(0,1)的 CDF。
对于大样本量,Z_n 大致是一个标准正态 N(0,1)随机变量。
以方程形式表示,三个陈述如下:

中心极限定理以几种不同的等价方式表达(图源:作者)
在(a)中,左边的积分是Z_n 的 CDF,而右边的积分是 N(0, 1)的 CDF。
在(b)和(c)中,P(Z_n ≤ z)只是表示Z_n 的 CDF 的另一种方式。Φ(z)是标准正态随机变量 N(0, 1)的 CDF 常用的表示法。
在(c)中,波浪等号(或半等号‘ ≃’)表示渐近相等。
在(d)中,箭头上的‘d’表示分布收敛。
请注意,中心极限定理并不假设X_1、X_2、……、X_n 的概率分布。它们不需要服从正态分布,而这一点大大扩展了中心极限定理的适用性。X_1、X_2、……、X_n 只需是独立同分布的,甚至在某些特殊版本的中心极限定理中,这两个限制条件也有所放宽。
你可以用中心极限定理做什么?
中心极限定理无处不在。在简单的频率派推断问题中,在最小二乘估计法中,在最大似然估计法中,在贝叶斯推断中,在时间序列模型分析中——无论你在哪个统计学领域研究,你很快会发现某种形式的中心极限定理在发挥作用。每当你遇到它时,都可以利用它为自己带来好处。
让我们来看一下这些好处。
计算观察到特定样本均值的概率
这是一个中心极限定理的简单应用。我将通过一个例子来说明。假设你的宽带连接平均速度为 400 Mbps,方差为 100 Mbps。即 μ=400 Mbps 和 σ²=100 Mbps。如果你在一天的 25 个随机时刻测量宽带速度,样本均值落在 395 Mbps 和 405 Mbps 之间的概率是多少?即,以下这个概率是多少:

样本带宽均值落在 395 和 405 Mbps 之间的概率是多少?(图片来自作者)
在上面的概率中,竖线 ‘|’ 表示‘条件在’。
解决方案在于这样的洞察:根据中心极限定理(CLT),标准化的样本均值大致服从标准正态分布。

标准化均值在分布上收敛于 N(0,1)(图片来自作者)
你可以利用这个事实,通过以下步骤来计算解决方案:

(图片来自作者)
我们看到,在所有 25 次测量的随机样本中,98.76% 的样本带宽均值会落在 395 Mbps 和 405 Mbps 之间。
这是中心极限定理(CLT)的经典应用。为了理解为什么,回想一下我如何用一个天真的假设开始这个例子,假设你会知道你互联网连接的真实带宽 μ 和方差 σ²。嗯,你真的知道吗?你看,μ 和 σ² 是总体值,而在实际中,你不太可能知道任何参数的总体值。
围绕未知总体均值构建置信区间
更常见和实际的中心极限定理应用是围绕未知总体均值 μ 或方差 σ² 构建一个 (1 —α)100% 置信区间。继续以带宽为例,假设这次正确的是你不知道 μ 和 σ²。假设你在一天的随机时刻进行了 25 次测量,得到了一个大小为 25 的随机样本,样本均值为 398.5 Mbps,样本方差为 85 Mbps。给定这个观察到的样本均值和方差,什么样的区间 [μ_low, μ_high] 可以使得真实的未知均值 μ 落在这个区间内的概率为 95%?
这里 n = 25, X_bar_n = 398.5, S = 85。未知数是 μ 和 σ²。你需要找到一个区间 [μ_low, μ_high],使得:

总体均值的 95% 置信区间(图片来自作者)
注意到这个概率是与之前情况中的概率相反的。在之前的情况中,你在寻找 P(X_bar_n|n,μ,σ²),即给定 n, μ 和 σ² 下 X_bar_n 的概率。而这里你(实际上)在寻找 P(μ|n,X_bar_n,σ²),即给定 n, X_bar_n 和 σ² 下 μ 的概率。
P(μ|n,X_bar_n, σ²) 或简称 P(μ|X_bar_n),被称为逆概率。顺便提一下,因为你的连接速度是一个实数,P(μ|X_bar_n) 是实数均值 μ 在观察到的均值 X_bar_n 下的概率密度函数。
那么我们究竟在说什么呢?我们是在说存在无限多个可能的真实均值,每一个均值都有一个概率密度,这个概率密度位于一个固定形状的概率密度函数上,形状在你观察到样本均值的那一刻就已经确定吗?
是的,这正是我们所说的。如果这个问题开始让你联想到量子力学的多世界解释,你并不孤单!
如果你对逆概率感到困惑,不要太自责。这确实是一个深奥的概念。数学家们在逆概率问题上钻研了一个多世纪,直到法国数学家皮埃尔-西蒙·拉普拉斯和英国长老会牧师托马斯·贝叶斯在 18 世纪末独立地解开了这个难题(显然是指问题,而不是他们的脑袋)。在这篇文章的续集中,我将详细介绍拉普拉斯对逆概率的精彩解决方法,他在量子力学的多世界解释首次出现在科学期刊上之前,就已经提出了这一方法,足足有两个世纪之久。拉普拉斯被称为“法国牛顿”绝非偶然。

皮埃尔-西蒙·拉普拉斯(1749–1827),和托马斯·贝叶斯(1701–1761)(公有领域图片)
顺便提一下,提到头部受伤,拉普拉斯的大部分工作发生在 18 世纪中叶到 19 世纪初的法国社会剧变的背景下。尽管拉普拉斯巧妙地保持了自己免受动荡和法国1789–1799 年革命后随之而来的不断变化的权力格局的影响,但许多他的同行科学家却不自觉地被解除了一切世俗责任。

1793 年 11 月 12 日:让·西尔万·巴耶——法国天文学家、数学家、自由石匠、政治领袖——凝视着他眼前的未来(公有领域图片)。
拉普拉斯对逆概率的解答提供了一个非常自然且令人满意的理由,用以围绕未知的总体均值构建置信区间——你猜对了——就是中心极限定理。
现在我可以简单地告诉你如何使用中心极限定理来构建围绕未知均值的置信区间。只需几分钟。我将展示大多数教科书采用的方法步骤,完成它。但请听我说:如果你跟随拉普拉斯的路径来构建置信区间,这将会更加有趣,因为它将会穿越他计算逆概率的方法。这正是我们将要构建置信区间的方式,接下来的文章会讲解这一过程。
与此同时,这里是中心极限定理的第三个应用场景。
误差的正态性和最小二乘估计的合理性
在 19 世纪初,拉普拉斯利用中心极限定理来论证测量误差服从正态分布。回顾起来,他的论证异常简单。可以把测量误差看作是无数原因的效果之和。拉普拉斯假设这些原因是独立的、均匀分布的随机变量。如果某一测量的误差是若干原因的随机样本之和,那么根据中心极限定理,它们的标准化和必须在分布上收敛于标准正态分布。这就是基本误差假设。
与此同时,在 1809 年,约翰·卡尔·弗里德里希·高斯(1777–1855)在现今的德国提出了最小二乘估计法。他的整个方法依赖于一个假设,即测量误差服从正态分布;一个高斯自己甚至都不信的假设,但他为此提供了一个非常复杂的论证。随后,拉普拉斯及时伸出援手,提出了他的“基本误差假设”,以此来证明高斯的假设,即误差服从正态分布,从而使高斯的最小二乘估计法得到了合法性。
在时间序列模型中的应用
最终,某些高级形式的中心极限定理,例如马尔可夫中心极限定理和戈尔丁中心极限定理,被用来构建时间序列模型预测的置信区间。如果不使用这些中心极限定理的特殊形式,就无法轻松分析这些模型预测结果的准确性。
更深入地探讨中心极限定理(CLT)的起源
互联网速度测试是一个有趣的例子。如果你想提高样本平均值的准确性,你的直觉可能是增加一天中测量的频率。但多频繁才算足够呢?每小时一次?每秒一次?每飞秒一次?问题是,样本空间是由一个实数值——时间——来索引的,这使得样本空间是不可数的无限集合。
在这种情况下,均值的实际值——如果它确实存在——是无法测量的。没有人——至少在地球上的任何人——能够知道真实的值是什么。但是,我们是否有办法让大自然揭示出真实的值呢?只要我们有某种方式来衡量该估计值的准确性,我们甚至会乐于知道真实值的估计。
1687 年,一位非常著名的科学家思考了这样的问题。他的思考使他发现了(弱)大数法则。这也引发了一系列研究,持续了一个多世纪,最终导致了德莫伊夫定理、正态曲线、拉普拉斯的逆概率方法,以及最终的中心极限定理。这位科学家是雅各布·伯努利。
下周加入我,我将讨论雅各布·伯努利发现的弱大数法则。WLLN 是中心极限定理的基石。拔掉 WLLN,CLT 的庞大建筑物就会崩塌成一堆废墟。我们将看看是如何发生的。敬请期待。
参考文献和版权
书籍和论文
Bernoulli, Jakob (2005) [1713],大数法则,第四部分《猜想的艺术》(英文翻译),Oscar Sheynin 翻译,柏林:NG Verlag,ISBN 978–3–938417–14–0 PDF 下载
Seneta, Eugene. 大数法则的三百年历史。Bernoulli 19 (4) 1088–1121,2013 年 9 月。doi.org/10.3150/12-BEJSP12 PDF 下载
Fischer, H., 中心极限定理的历史:从经典到现代概率论,Springer 科学与商业媒体,2010 年 10 月
Hald, A., 从伯努利到费舍尔的参数统计推断历史,1713–1935,Springer,2006
Stigler, S. M., 统计学的历史:1900 年前不确定性的测量,哈佛大学出版社,1986 年 9 月
图像和视频
本文中的所有图像和视频版权归Sachin Date所有,采用CC-BY-NC-SA许可协议,除非在图像或视频下方提到不同的来源和版权。
感谢阅读!如果你喜欢这篇文章,请 关注我 获取更多关于统计学和统计建模的内容。
高绩效数据科学团队新经理指南
从管理八人团队的人那里学到的经验教训
·发表在Towards Data Science ·阅读时间 14 分钟·2024 年 4 月 16 日
--

照片由John Schnobrich拍摄,来源于Unsplash
对于新的数据科学经理来说,管理团队可能会很困难。领导责任往往伴随着陡峭的学习曲线。然而,通过专注于建立一个支持、雄心勃勃和团结的团队环境,你可以将团队的表现提升到新的高度。在这篇文章中,我将分享我在领导一个由八人组成的数据科学团队时学到的六个最重要的经验教训,强调这些见解如何帮助将任何团队转变为高绩效团队。
成长思维
每个加入你团队的人都有自己的职业目标和愿望。他们加入你的团队是为了学习新技能,接触新工具和技术,并了解业务的新部分。最终,可能会出现新的机会,你希望你的团队为这些机会做好准备。表明你致力于职业成长和发展会激励员工,并帮助团队随着时间的推移成熟起来。为了支持这一承诺,考虑实施以下策略:
- 制定个人职业发展计划: 发展计划应该针对每个个人量身定制。它们应该突出个人的优势,并突出显示他们的弱点。是否有数据...
一种利用聚类检测协调攻击的新方法
揭示隐藏的模式:将恶意行为分组
·发表于Towards Data Science ·14 分钟阅读·2024 年 10 月 16 日
--
聚类是无监督机器学习中的一种强大技术,它根据数据的固有相似性将数据进行分组。与依赖预先标注数据来指导学习过程的监督学习方法(如分类)不同,聚类是在没有标签的数据上进行操作。这意味着没有预定义的类别或标签,而是算法在没有事先了解分组应如何进行的情况下,发现数据的潜在结构。
聚类的主要目标是将数据点组织成簇,其中同一簇内的数据点相较于不同簇的数据点具有更高的相似性。这一区别使得聚类算法能够形成反映数据自然模式的群体。本质上,聚类旨在最大化簇内相似性,同时最小化簇间相似性。这项技术在需要发现数据中隐藏的关系或结构的场景中特别有用,因此在欺诈检测和异常识别等领域具有重要价值。
通过应用聚类,可以揭示通过其他方法可能不明显的模式和洞察力,它的简单性和灵活性使其能够适应各种数据类型和应用场景。
利用聚类检测在线系统中的欺诈行为
聚类的一个实际应用是在线系统中的欺诈检测。考虑一个例子,多个用户正在向一个网站发起请求,每个请求都包括像 IP 地址、请求时间和交易金额等详细信息。
以下是聚类如何帮助检测欺诈行为:
-
假设大多数用户是从唯一的 IP 地址发起请求,并且他们的交易模式自然会有所不同。
-
然而,如果多个请求来自同一 IP 地址,并显示出类似的交易模式(如频繁的高价值交易),这可能表明有欺诈者从同一来源发起了多个虚假交易。
通过基于 IP 地址和交易行为对所有用户请求进行聚类,我们可以检测到来自单一 IP 地址的可疑请求聚类。这可以标记潜在的欺诈活动,并帮助采取预防措施。
下面的图示展示了聚类概念的一个示例。
假设你有代表交易请求的数据点,绘制在一个图表上,其中:
-
X 轴:来自同一 IP 地址的请求数量。
-
Y 轴:平均交易金额。
左侧是原始数据。没有标签时,我们可能已经能看到一些模式的形成。右侧是应用聚类后的数据,数据点被分为不同的聚类,每个聚类代表不同的用户行为。

欺诈用户行为的聚类示例。图片来源 (CC BY 4.0)
定义相似性度量以实现有效的聚类
为了有效地分组数据,我们必须定义一个相似性度量或指标,用来量化数据点之间的接近程度。这个相似性可以通过多种方式进行度量,具体取决于数据的结构和我们希望发现的洞察。有两种主要的相似性度量方法——手动相似性度量和嵌入式相似性度量。
手动相似性度量涉及明确地定义一个数学公式,以根据数据点的原始特征进行比较。这种方法直观,我们可以使用欧几里得距离、余弦相似度或 Jaccard 相似度等距离度量来评估两个数据点的相似性。例如,在欺诈检测中,我们可以手动计算交易属性(如交易金额、请求频率)之间的欧几里得距离,以检测可疑行为的聚类。尽管这种方法相对容易设置,但需要仔细选择相关特征,且可能错过数据中的更深层次模式。
另一方面,嵌入相似度度量利用机器学习模型的力量来创建数据的学习表示或嵌入。嵌入是捕捉数据中复杂关系的向量,可以通过像 Word2Vec 这样的文本模型或用于图像的神经网络生成。一旦计算出这些嵌入,就可以使用传统的度量标准(如余弦相似度)来衡量相似性,但此时比较是在一个转换后的低维空间中进行的,这个空间捕捉了更有意义的信息。嵌入相似度对于复杂数据尤为有用,例如网站上的用户行为或自然语言处理中的文本数据。例如,在电影或广告推荐系统中,用户行为可以被嵌入成向量,并且在这个嵌入空间中的相似性可以用来推荐给相似的用户内容。
虽然手动相似度度量提供了透明性并且能更好地控制特征选择和设置,但嵌入相似度度量能够捕捉到数据中更深层次和更抽象的关系。两者之间的选择取决于数据的复杂性和聚类任务的具体目标。如果你有充分理解的结构化数据,手动度量可能足够了。但如果数据丰富且多维,比如文本或图像分析,基于嵌入的方法可能会给出更有意义的聚类。理解这些权衡对于选择适合的聚类方法至关重要。
在像欺诈检测这样的场景中,数据通常是丰富的,并且基于用户活动的行为,基于嵌入的方法通常更有效,因为它能捕捉到那些可能表明风险活动的细微模式。
引入行为分析与聚类分类系统(BACCS)以检测协调的欺诈攻击
协调欺诈攻击行为通常表现出特定的模式或特征。例如,欺诈活动可能来自一组相似的 IP 地址,或依赖于一致的、重复的战术。检测这些模式对于维持系统的完整性至关重要,而聚类是一种有效的技术,通过共享特征将实体分组。这有助于通过检查聚类中的集体行为来识别潜在的威胁。
然而,仅仅依靠聚类可能不足以准确检测欺诈,因为它也可能将良性活动与有害活动混合在一起。例如,在社交媒体环境中,用户发布像“你今天怎么样?”这样的无害信息,可能会与那些进行网络钓鱼攻击的用户被归为同一类。因此,需要额外的标准来区分有害行为与良性行为。
为了应对这一点,我们引入了行为分析与聚类分类系统(BACCS),作为一个旨在检测和管理恶意行为的框架。BACCS 通过生成和分类实体的聚类(如个人账户、组织档案和交易节点)来工作,并且可以广泛应用于社交媒体、银行和电子商务等多个领域。重要的是,BACCS 侧重于对行为进行分类,而非内容,使其更适合识别复杂的欺诈活动。
该系统通过分析聚类中实体的整体属性来评估聚类。这些属性通常是布尔值(真/假),系统评估表现出特定特征的实体比例来确定聚类的整体性质。例如,聚类中大量新创建账户的存在可能表明有欺诈活动。根据预定义的策略,BACCS 识别出指示滥用行为的属性比率组合,并确定相应的应对措施以缓解威胁。
BACCS 框架提供了几个优势:
-
它通过基于行为相似性对实体进行分组,从而实现对协调攻击的检测。
-
它通过定义聚类成员的相关属性并应用自定义策略来对聚类进行分类,从而识别潜在的滥用行为。
-
它支持对被标记为有害的聚类采取自动行动,从而确保系统完整性,并增强防范恶意活动的能力。
这种灵活和自适应的方法使得 BACCS 能够持续演化,确保它能够有效应对跨不同平台和行业的新兴协调攻击形式。
让我们通过一个类比更好地理解:假设你有一辆满载苹果的马车,准备出售。这些苹果在被多名工人装载到马车之前都会被放入袋子中。一些工人不喜欢你,试图在他们的袋子里装入酸苹果来捣乱。你需要识别出任何可能含有酸苹果的袋子。要识别酸苹果,你需要检查它是否软,但问题是,一些苹果天生比其他苹果软。你通过打开每个袋子并挑出五个苹果,检查它们是否软来解决了这些恶意工人的问题。如果几乎所有苹果都很软,那么这个袋子很可能包含酸苹果,你将其放到一边以便稍后进一步检查。一旦你识别出所有可能含有可疑软苹果的袋子,你就将它们的内容倒出,挑出健康的硬苹果,扔掉所有软苹果。你现在已经最小化了顾客咬到酸苹果的风险。
BACCS 的操作方式类似;你有的是实体(例如,用户账户),而不是苹果。你有的是恶意用户,而不是坏工人;你有的是按共同特征(例如,相似的账户创建时间)分组的实体,而不是一袋苹果。BACCS 对每组实体进行抽样,检查是否有恶意行为的迹象(例如,政策违规率较高)。如果某个组显示出这些迹象的高频率,则会被标记为进一步调查的对象。
就像在教室里检查材料一样,BACCS 使用预定义的信号(也称为属性)来评估聚类中实体的质量。如果发现某个聚类存在问题,可以采取进一步的措施来隔离或移除恶意实体。该系统具有灵活性,可以通过调整标记聚类的标准或根据新出现的滥用模式创建新的聚类类型来适应新的恶意行为。
这个类比说明了 BACCS 如何通过主动识别和缓解潜在问题来帮助维护环境的完整性,为所有合法用户提供一个更安全、更可靠的空间。
该系统提供了众多优势:
-
更好的精确度:通过对实体进行聚类,BACCS 提供了强有力的协同证据,从而能够制定那些如果应用于孤立的个体实体时会过于模糊的政策。
-
可解释性:与某些机器学习技术不同,BACCS 所做的分类是透明且易于理解的。追溯和理解某个特定决策是如何做出的非常简单。
-
快速响应时间:由于 BACCS 采用基于规则的系统,而非依赖机器学习,因此无需进行大量的模型训练。这使得响应时间更快,对于及时解决问题至关重要。
如果你:
-
专注于分类行为而非内容:尽管 BACCS 中的许多聚类可能围绕内容(例如,图像、电子邮件内容、用户电话号码)形成,但系统本身并不会直接对内容进行分类。
-
处理发生频率较高的问题:BACCS 采用统计方法,当聚类中包含大量滥用实体时最为有效。对于稀疏发生的有害事件,可能效果不佳,但对于像垃圾邮件这样的高频问题更为适用。
-
处理协调或相似行为:聚类信号主要表示协调或相似的行为,这使得 BACCS 在解决这些类型的问题时尤为有用。
以下是将 BACCS 框架融入实际生产系统的方式:

设置 BACCS 在生产环境中的部署。图像来源:作者
-
当实体在平台上进行活动时,您需要建立一个观察层来捕捉这些活动并将其转化为事件。这些事件随后可以由专门用于聚类分析和执行的系统进行监控。
-
基于这些事件,系统需要根据各种属性将实体分组到聚类中——例如,所有从同一 IP 地址发布的用户将被分到一个聚类中。这些聚类随后应转发进行进一步分类。
-
在分类过程中,系统需要为聚类成员的样本计算一组专业的布尔信号。此类信号的一个例子可能是账户年龄是否小于一天。然后,系统将这些信号计数汇总到聚类中,例如确定在 100 个用户的样本中,有 80 个用户的账户年龄小于一天。
-
这些汇总的信号计数应与政策进行比较,以确定聚类是否看起来异常,并在出现异常时应采取哪些行动。例如,某项政策可能规定,如果某个 IP 聚类中超过 60%的成员账户年龄小于一天,则这些成员应接受进一步验证。
-
如果某项政策将一个聚类标识为异常,系统应识别出该聚类中所有展示触发该政策的信号的成员(例如,所有账户年龄小于一天的成员)。
-
系统应将所有此类用户引导到适当的行动框架中,执行政策所指定的行动(例如,进一步验证或封锁其账户)。
通常,从实体活动到采取行动的整个过程是在几分钟内完成的。还必须认识到,尽管该系统为聚类分类提供了框架和基础设施,但客户端/组织需要提供自己的聚类定义、属性和针对特定领域的政策。
示例:通过基于 IP 的聚类和封锁来减轻垃圾邮件问题
我们来看一个例子,我们试图通过按 IP 对发送电子邮件的用户进行聚类来减轻垃圾邮件问题,如果聚类中超过 60%的成员账户年龄小于一天,则对其进行封锁。

聚类和封锁操作。图片由作者提供
成员可能已经存在于聚类中。当聚类达到某一大小或自上次分类以来发生了足够的变化时,可以触发重新分类。
在选择聚类标准和定义用户属性时,目标是识别与您试图检测的特定风险或活动相符合的模式或行为。例如,如果您在进行欺诈行为或协调攻击的检测工作,标准应当捕捉到通常由恶意行为者共享的特征。以下是选择聚类标准和定义用户属性时需要考虑的一些因素:
选择聚类标准
你选择的聚类标准应围绕代表可能表明风险的行为特征。这些特征可能包括:
-
基于时间的模式: 例如,通过用户账户创建时间或在给定时间段内的行为频率将用户分组,可以帮助检测到活动的激增,这可能表明存在协调行为。
-
地理位置或 IP 地址: 按照 IP 地址或地理位置对用户进行聚类,尤其有助于检测协调行为,例如来自同一地区的多个欺诈性登录或内容提交。
-
内容相似度: 在误信息或垃圾邮件检测等情况下,通过内容的相似性(例如,帖子/邮件中相似的文本)进行聚类可以识别出可疑的协调行为。
-
行为指标: 特征如交易数量、平均会话时间或与平台的交互类型(例如,点赞、评论或点击)在聚类后可以指示异常模式。
关键是选择那些不仅与正常用户行为相关联,而且足够独特以隔离风险模式的标准,这将导致更有效的聚类。
定义用户属性
一旦选择了聚类标准,为每个聚类中的用户定义有意义的属性至关重要。这些属性应为可衡量的信号,帮助评估有害行为的可能性。常见的属性包括:
-
账户年龄: 新创建的账户通常涉及恶意活动的风险较高,因此像“账户年龄 < 1 天”这样的属性可以标记可疑行为。
-
连接密度: 对于社交媒体平台,像聚类中账户之间的连接或交互数量这样的属性可以指示异常行为。
-
交易金额: 在金融欺诈的情况下,平均交易额或高价值交易的频率可能是标记高风险聚类的关键属性。
每个属性应清楚地与可能表明合法使用或潜在滥用的行为相关联。重要的是,属性应该是布尔值或数值,便于聚合和比较。
一种巧妙的技巧:通过降低机器学习阈值 + 聚类增加召回率来增强检测能力
另一种高级策略是使用机器学习分类器的输出作为属性,但调整阈值。通常,你会为分类有害行为设置较高的阈值,以避免假阳性。然而,当与聚类结合时,你可以降低这个阈值,因为聚类本身作为附加信号来加强该属性。
假设有一个模型 X,能够识别诈骗并禁用模型 X 得分 > 0.95 的电子邮件账户。假设这个模型已经在生产中上线,并且以 100%的精度在阈值 0.95 下禁用不良电子邮件账户。我们需要提高这个模型的召回率,同时不影响精度。
-
首先,我们需要定义能够将协调活动归为一组的簇。假设我们知道存在一个协调活动,其中不法分子使用相同的主题行,但不同的电子邮件 ID 来发送欺诈邮件。通过使用 BACCS,我们将形成一组组电子邮件账户,这些账户的发送邮件都有相同的主题名称。
-
接下来,我们需要降低原始模型阈值,并定义一个 BACCS 属性。我们将把模型 X 集成到我们的生产检测基础设施中,并使用降低的模型阈值(例如 0.75)创建一个属性。对于那些模型 X 得分 >= 0.75 的电子邮件账户,该属性值为“True”。
-
然后我们将定义异常阈值,并说,如果活动名称簇中 50%的实体具有该属性,则将这些簇标记为不良簇,并禁用那些该属性为 True 的广告账户。
所以我们本质上是降低了模型的阈值,并开始在一个显著低于当前模型执行阈值的特定簇中禁用实体,但仍然可以确保执行的精度没有下降,并且召回率得到了提高。让我们来理解一下如何实现的 -
假设我们有 6 个实体,它们有相同的主题行,且它们的模型 X 得分如下:

通过机器学习模型执行的实体。图像来源:作者
如果我们使用原始模型得分(0.95),我们将只禁用 2/6 个电子邮件账户。
如果我们按主题行文本对实体进行聚类,并定义策略,找出那些模型 X 得分 >= 0.75 且包含超过 50%不良实体的簇,我们将禁用以下这些账户:

通过聚类处理的实体,使用机器学习得分作为属性。图像来源:作者
所以我们将执行召回率从 33%提高到了 83%。本质上,即使个体行为看起来风险较低,但它们是可疑簇的一部分,这提升了它们的重要性。这种组合为检测有害活动提供了强信号,同时最小化了误报的可能性。
通过降低阈值,你可以让聚类过程显现出一些模式,这些模式如果仅依赖分类可能会被忽视。这种方法结合了机器学习模型提供的细致洞察力和聚类能够识别的更广泛的行为模式。它们共同构建了一个更强大的系统,用于检测和减轻风险,并能捕捉到更多实体,同时保持较低的误报率。
结论
聚类技术仍然是检测协调攻击和确保系统安全的重要方法,特别是在那些更容易受到欺诈、滥用或其他恶意活动的平台上。通过将相似行为分组到不同的簇中,并应用政策将恶意实体从这些簇中剔除,我们可以检测并减轻有害活动,从而为所有用户确保一个更安全的数字生态系统。选择更先进的基于嵌入的方法有助于比手动相似性检测方法更好地表示复杂的用户行为模式。
随着我们不断推进安全协议,像 BACCS 这样的框架在打击大规模协调攻击方面发挥着至关重要的作用。将聚类与基于行为的策略相结合,能够实现动态适应,使我们能够迅速响应新型滥用行为,同时加强平台的信任和安全性。
在未来,进一步研究和探索补充性技术以增强聚类效果将是一个重要的机会。诸如基于图的分析方法,用于映射实体之间复杂关系的技术,可以与聚类技术结合,从而在威胁检测中提供更高的精度。此外,将聚类与机器学习分类结合的混合方法,可以在更高召回率和更低假阳性率下有效检测恶意活动。探索这些方法,并不断优化当前方法,将确保我们能够应对数字威胁不断变化的环境。
参考文献
一个 Patch 大小超过了 16*16 像素
关于像素变换器和超长序列分布式变换器
·发表于 Towards Data Science ·阅读时间 7 分钟·2024 年 6 月 17 日
--
你是否曾经想过,为什么 Vision Transformer(ViT)使用 16*16 大小的 patch 作为输入 token?
这一切可以追溯到变换器模型的早期。原始的变换器模型在 2017 年提出,且仅适用于自然语言数据。当 BERT 模型在 2018 年发布时,它只能处理最大长度为 512 的 token 序列。到了 2020 年,当 GPT-3 发布时,它能够处理长度为 2048 和 4096 的序列(3.5 版本)。所有这些模型在处理序列到序列和文本生成任务时都展示了惊人的表现。

图像来源:pxhere.com/en/photo/141620
然而,当 tokens 按像素级别提取时,这些序列对于图像来说过于简短。例如,在 Cifar-100 中,图像大小是 3232 = 1024 像素。在 ImageNet 中,图像大小是 224224 = 50176 像素。如果直接将变换器应用于像素级别,序列长度将立即成为一个障碍。
ViT 论文于 2020 年发布,提出使用 patch 而非像素作为输入 token。对于一个 224224 大小的图像,使用 1616 的 patch 大小,序列长度将大幅减少至 196,这完美解决了这个问题。
然而,这个问题仅部分解决了。对于需要更精细特征的任务,必须采用不同的方法以恢复像素级的精度。Segformer 提出通过融合来自不同分辨率的层次化变换器编码器的特征来解决这个问题。SwinIR 则结合了卷积神经网络(CNN)与变换器模块周围的多层跳跃连接进行细粒度特征提取。Swin Transformer 是一个用于通用计算机视觉任务的模型,它从每个局部窗口的 44 补丁大小开始,然后逐渐构建到全球 1616 补丁大小,以同时获得全局性和细粒度性。从本质上讲,这些努力指向一个事实——仅使用 16*16 大小的补丁是不够的。
自然的问题是,“像素”是否可以作为变换器的直接标记?这个问题进一步分为两个:1. 是否可以将超长序列(例如,50k)输入变换器?2. 将像素作为标记输入是否比输入补丁提供更多的信息?
在本文中,我将总结两篇最近的论文:1. 像素变换器,这是 Meta AI 上周发布的技术报告,从减少局部归纳偏置的角度,比较了像素级标记和补丁级标记在图像分类、预训练和生成等三项不同任务中的变换器模型表现。2. 超长序列分布式变换器:通过分布查询向量,作者展示了在 3k 个 GPU 上扩展 50k 长度输入序列的可能性。
像素变换器(PiT)——从归纳偏置的角度
Meta AI 上周发布了技术报告,标题为:“一张图比 16*16 的补丁更有价值”。这份技术报告并没有提出一种新方法,而是回答了一个长期存在的问题:使用像素而不是补丁作为输入标记是否有意义?如果有,为什么?
论文从局部归纳偏置的角度进行分析。根据 K. Murphy 的著名《机器学习书籍》,归纳偏置是“关于数据分布性质的假设”。在早期的“非深度学习”时代,归纳偏置更多是“特征相关的”,源自为特定任务人工设计的特征。这个归纳偏置并不是坏事,尤其是在那些通过人类专家获得非常好先验知识的特定任务中,使得设计的特征非常有用。然而,从泛化的角度来看,设计的特征很难泛化到通用任务,比如通用的图像分类和分割。
除了特征偏差之外,架构本身也包含了归纳偏差。ViT 就是一个很好的例子,展示了在架构层次结构、传播均匀性、表示规模和注意力局部性方面,比 CNN 模型具有更少的归纳偏差。详细讨论请参见我之前的 Medium 博客。不过,ViT 仍然保留了一种特殊类型的归纳偏差——局部性。当 ViT 处理一系列 patch tokens 时,同一 patch 内的像素会自然地与来自不同 patch 的像素不同地被模型处理。这就是局部性的来源。
那么,是否有可能进一步去除局部性的归纳偏差?答案是肯定的。PiT 提出了使用“像素集”作为输入,并采用不同的位置编码(PE)策略:sin-cos、学习式和无编码。它在有监督、自监督和生成任务中展现了比 ViT 更优越的性能。下图展示了所提出的流水线。

Pixel Transformer 流水线。图片来源:arxiv.org/abs/2406.09415
这个想法看似简单明了,作者声称他们“并没有引入一种新方法”。但是,PiT 仍展现出了巨大的潜力。在 CIFAR-100 和 ImageNet(输入大小缩小到 28*28)的有监督分类任务中,分类准确率比 ViT 提高了超过 2%。请参见下表。

有监督学习分类。图片来源:arxiv.org/pdf/2406.09415
在自监督学习任务和图像生成任务中也观察到类似的提升。更重要的是,作者还展示了当将 patch 大小从 88 降到 11(单像素)时,性能提高的趋势,如下所示:

从 ViT(88 patch)到 PiT(11 patch)的性能提升。图片来自:arxiv.org/pdf/2406.09415
关于位置编码。
正如这篇研究论文中指出的那样,位置编码是基于 Transformer 的模型中,用于输入 token 序列排序和提高准确性的前提。然而,PiT 表明,即使去掉了位置编码,模型性能下降也很小:

使用三种不同位置编码(PE)的 PiT 性能:1. 固定的 sin-cos PE;2. 可学习的 PE;3. 无 PE。图片来源:arxiv.org/pdf/2406.09415
为什么要丢弃位置编码?不仅仅是因为丢弃位置编码意味着很好地减少了局部性偏差。如果我们以分布式方式思考自注意力计算,它将大大减少跨设备通信的工作量,关于这一点我们将在下一节详细讨论。
超长序列变换器——分布式查询向量解决方案
局部性归纳偏差只是讲了一部分故事。如果仔细查看 PiT 论文中的结果,我们会发现实验限于 2828 大小的调整图像,这是由于计算限制。但现实世界很少使用如此小尺寸的图像。所以,自然的问题是,尽管 PiT 可能有效并超越 ViT,它能否在标准分辨率的自然图像上工作,例如 244244?
2023 年发布的论文《超长序列分布式变换器》回答了这个问题。论文提出了一种解决方案,将 50k 长序列的变换器计算扩展到 3k 个 GPU 上。

原始变换器、基线并行化和所提的超长序列变换器。图片来源:arxiv.org/pdf/2311.02382
这个想法很简单:变换器的瓶颈是自注意力计算。为了确保全局注意力计算,所提出的方法只将查询向量分配到不同设备上,同时在所有设备上保持相同的键值对副本。

自注意力计算分配的方程式。核心思想是分配 Q,并保持 K 和 V 的相同副本。图片来源:arxiv.org/pdf/2311.02382
位置编码感知的双重梯度平均。对于具有可学习位置编码参数的架构,梯度反向传播与位置编码分布相关。因此,作者提出了双重梯度平均技术:当对同一序列的两个不同段执行梯度平均时,不涉及位置编码,但当需要对两个序列的对应段进行梯度平均时,位置编码参数会同步。

位置编码感知的双重梯度平均。图片来源:arxiv.org/pdf/2311.02382

关于所使用 GPU 数量的最大序列长度。图片来源:arxiv.org/pdf/2311.02382
当我们将这两篇论文结合起来时,事情变得有趣了。减少归纳偏差不仅有助于模型性能,还在分布式计算中起着至关重要的作用。
-
局部性和设备通信的归纳偏差。PiT 论文展示了通过去除局部性偏差来构建无位置编码模型的潜力。此外,从分布式计算的角度来看,减少对 PE 的需求可以进一步减少跨设备的通信负担。
-
分布式序列计算中的局部性归纳偏差。在多 GPU 上分布式自注意力意味着查询向量是基于段的。如果段依赖于连续的标记,则会有自然的局部性偏差。PiT 在“标记集”上进行计算,这意味着不需要连续的段,它将使查询向量没有偏差。
参考文献:
-
Nguyen 等人,《一张图片胜过 16x16 块:在单个像素上探索 Transformers》。arXiv 预印本 2024。
-
Wang 等人,《超长序列分布式 Transformer》。arXiv 预印本 2023。
-
Keles 等人,《自注意力的计算复杂度》。ALT 2023。
-
Jiang 等人,《视觉 Transformer 中位置嵌入的编码方法》。《视觉通信与图像表示杂志》。2022。
-
Xie 等人,《SegFormer:一种简洁高效的语义分割设计,基于 Transformers》。NeurIPS 2021。Github:
github.com/NVlabs/SegFormer -
Liang 等人,Swinir:使用 Swin Transformer 进行图像恢复。ICCV 2021。Github:
github.com/JingyunLiang/SwinIR -
Liu 等人,《Swin Transformer:使用平移窗口的分层视觉 Transformer》。ICCV 2021。Github:
github.com/microsoft/Swin-Transformer -
Dosovitsckiy 等人,《一张图像胜过 16x16 个词:用于大规模图像识别的 Transformers》。arXiv 预印本,2020。
-
Brown 等人,《语言模型是少量样本学习者》。NeurIPS 2020。Github:
github.com/openai/gpt-3 -
Devlin 等人,《BERT:用于语言理解的深度双向 Transformer 预训练》。arXiv 预印本 2018。HuggingFace 官方:
huggingface.co/docs/transformers/en/model_doc/bert -
Vaswani 等人,《Attention is all You need》。NeurIPS 2017。
-
Murphy,《机器学习:一种概率视角》。MIT 出版社 2012。
一种强大的 EDA 工具:分组聚合

照片由Mourizal Zativa提供,来自Unsplash
学习如何使用分组聚合从数据中发现洞察
·发表于Towards Data Science ·阅读时间:7 分钟·2024 年 7 月 4 日
--
探索性数据分析(EDA)是数据分析师的核心能力。每天,数据分析师的任务是看到“未见之事”,或者从浩瀚的数据海洋中提取有用的洞察。
在这方面,我想分享一种我认为有助于从数据中提取相关洞察的技巧:分组聚合。
为此,本文的其余部分将按以下方式安排:
-
在 Pandas 中进行分组聚合的解释
-
数据集:地铁州际交通
-
地铁交通探索性数据分析(EDA)
分组聚合
分组聚合是一种数据操作技巧,包含两个步骤。首先,我们根据特定列的值对数据进行分组。其次,我们在分组数据上执行一些聚合操作(例如,求和、平均、求中位数、计数唯一值等)。
分组聚合在数据粒度较细时尤其有用,典型的如事实表(交易数据)和间隔较窄的时间序列数据。通过在比原始数据粒度更高的层次进行聚合,我们可以以更紧凑的方式表示数据——并且可能在此过程中提炼出有用的洞察。
在 pandas 中,我们可以使用以下通用语法形式进行分组聚合。
df.groupby(['base_col']).agg(
agg_col=('ori_col','agg_func')
)
其中,base_col是作为分组基础的列,agg_col是通过对ori_col列进行agg_func聚合后定义的新列。
例如,考虑著名的泰坦尼克号数据集,以下显示了其五行数据。
import pandas as pd
import seaborn as sns
# import titanic dataset
titanic = sns.load_dataset("titanic")
titanic.head()

泰坦尼克号数据的前 5 行(作者提供的图片)
我们可以通过survived列对这些数据进行分组,然后通过计算fare列的中位数来聚合,得到以下结果。

按生存状态划分的泰坦尼克号乘客票价中位数(作者提供的图片)
突然间,我们看到一个有趣的见解:生还的乘客有更高的票价中位数,且几乎翻倍。这可能与优先为高级舱乘客(即票价较高的乘客)提供救生艇有关。
希望这个简单的例子能够展示通过分组聚合从数据中提取见解的潜力。那么,现在让我们在一个更有趣的数据集上尝试一下分组聚合!
数据集
我们将使用地铁州际交通量数据集。这是一个公开可用的数据集,具有创意共享 4.0 许可证(允许以任何目的共享和修改数据集)。
该数据集包含 2012-2018 年间明尼阿波利斯-圣保罗,明尼苏达州 I-94 西行的每小时交通量数据,同时也包含天气详情。数据字典信息可以在其UCI 机器学习库页面找到。
import pandas as pd
# load dataset
df = pd.read_csv("dir/to/Metro_Interstate_Traffic_Volume.csv")
# convert date_time column from object to proper datetime format
df['date_time'] = pd.to_datetime(df['date_time'])
# head
df.head()

交通数据(df)头部(作者提供的图片)
对于这个博客示例,我们将仅使用 2016 年及之后的数据,因为早期的交通数据缺失(自己尝试检查一下,作为练习!)。
此外,我们将添加一个新的列is_congested,如果traffic_volume超过 5000,则值为 1,否则为 0。
# only consider 2016 onwards data
df = df.loc[df['date_time']>="2016-01-01",:]
# feature engineering is_congested column
df['is_congested'] = df['traffic_volume'].apply(lambda x: 1 if x > 5000 else 0)
地铁交通 EDA
以分组聚合作为主要方法,我们将尝试回答以下分析问题。
-
交通量的月度变化如何?
-
一周内每天的交通概况如何(星期一、星期二等)?
-
典型的 24 小时交通量是如何变化的,按工作日与周末进行区分?
-
哪些天气条件与更高的拥堵率相关?
交通量的月度变化
这个问题要求我们按月聚合(求和)交通量数据。由于我们没有month列,我们需要基于date_time列派生出该列。
有了month列,我们可以基于该列进行分组,并计算traffic_volume的总和。代码如下所示。
# create month column based on date_time
# sample values: 2016-01, 2026-02
df['month'] = df['date_time'].dt.to_period("M")
# get sum of traffic_volume by month
monthly_traffic = df.groupby('month', as_index=False).agg(
total_traffic = ('traffic_volume', 'sum')
)
# convert month column to string for viz
monthly_traffic['month'] = monthly_traffic['month'].astype(str)
monthly_traffic.head()

月度交通头部(作者提供的图片)
我们可以从这个数据框中绘制折线图!
# draw time series plot
plt.figure(figsize=(12,5))
sns.lineplot(data=monthly_traffic, x ="month", y="total_traffic")
plt.xticks(rotation=90)
plt.title("Monthly Traffic Volume")
plt.show()

月度交通量(作者提供的图片)
上面的可视化显示,在所考虑的数据期间内,交通量普遍增加。
每日交通概况
为了分析这一点,我们需要创建两个额外的列:date和dayname。前者用作主要的分组依据,而后者用于在展示数据时进行细分。
在接下来的代码中,我们定义了date和dayname列。之后,我们基于这两列进行分组,以获取traffic_volume的总和。请注意,由于dayname比date更粗略(聚合层级较高),实际上意味着我们是基于date值进行聚合的。
# create column date from date_time
# sample values: 2016-01-01, 2016-01-02
df['date'] = df['date_time'].dt.to_period('D')
# create dayname column
# sample values: Monday, Tuesday
df['dayname'] = df['date_time'].dt.day_name()
# get sum of traffic, at date level
daily_traffic = df.groupby(['dayname','date'], as_index=False).agg(
total_traffic = ('traffic_volume', 'sum')
)
# map dayname to number for viz later
dayname_map = {
'Monday': 1,
'Tuesday': 2,
'Wednesday': 3,
'Thursday': 4,
'Friday': 5,
'Saturday': 6,
'Sunday': 7
}
daily_traffic['dayname_index'] = daily_traffic['dayname'].map(dayname_map)
daily_traffic = daily_traffic.sort_values(by='dayname_index')
daily_traffic.head()

daily_traffic 头部数据(图像来源:作者)
上表包含了按星期名称划分的每日总交通量的不同表现形式。箱形图可有效展示这些交通量的变化,让我们能够理解星期一、星期二等的交通量差异。
# draw boxplot per day name
plt.figure(figsize=(12,5))
sns.boxplot(data=daily_traffic, x="dayname", y="total_traffic")
plt.xticks(rotation=90)
plt.title("Daily Traffic Volume")
plt.show()

上图显示,所有工作日(周一至周五)的交通密度大致相同。周末(周六和周日)的交通较少,其中周日的交通最少。
按周末状态细分的小时交通模式
与之前的问题类似,我们需要创建两个新列来回答这个问题,即hour和is_weekend。
使用相同的技巧,我们将按is_weekend和hour列进行分组,以获取traffic_volume的平均值。
# extract hour digit from date_time
# sample values: 1,2,3
df['hour'] = df['date_time'].dt.hour
# create is_weekend flag based on dayname
df['is_weekend'] = df['dayname'].apply(lambda x: 1 if x in ['Saturday', 'Sunday'] else 0)
# get average traffic at hour level, broken down by is_weekend flag
hourly_traffic = df.groupby(['is_weekend','hour'], as_index=False).agg(
avg_traffic = ('traffic_volume', 'mean')
)
hourly_traffic.head()

hourly_traffic 头部数据(图像来源:作者)
对于可视化,我们可以使用条形图,并按照is_weekend标志进行细分。
# draw as barplot with hue = is_weekend
plt.figure(figsize=(20,6))
sns.barplot(data=hourly_traffic, x='hour', y='avg_traffic', hue='is_weekend')
plt.title("Average Hourly Traffic Volume: Weekdays (blue) vs Weekend (orange)", fontsize=14)
plt.show()

按周末状态划分的小时交通模式(图像来源:作者)
非常有趣且丰富的可视化!观察结果:
-
工作日的交通呈双峰分布模式。它在早上 6 点到 8 点和下午 4 点到 5 点之间达到交通量的峰值。这是直观的,因为这些时间段代表着人们上下班的时间。
-
周末的交通遵循完全不同的模式。它呈单峰形状,且有一个较大的高峰区间(12–17 点)。尽管总体上(交通量较少)低于工作日的同一时段,但值得注意的是,周末的深夜时段(22–2 点)交通实际上更高。这可能是因为人们在周末晚上待得更晚。
与拥堵相关的天气类型
为了回答这个问题,我们需要计算数据集中每种天气条件下的拥堵率(利用is_congested列)。我们能通过分组聚合来计算吗?当然可以!
需要注意的关键点是,is_congested列是二进制的。因此,拥堵率可以通过简单地对该列求平均值来计算!二进制列的平均值等于count(rows with value = 1)/count(all rows) —— 这就是拥堵率的定义。
基于这一精妙的观察,我们所要做的就是按weather_description分组,对is_congested求平均值(即均值)。接下来,我们按congested_rate降序排列结果。
# rate of congestion (is_congested) , grouped by weather description
congested_weather = df.groupby('weather_description', as_index=False).agg(
congested_rate = ('is_congested', 'mean')
).sort_values(by='congested_rate', ascending=False, ignore_index=True)
congested_weather.head()

拥堵天气头(图片由作者提供)
# draw as barplot
plt.figure(figsize=(20,6))
sns.barplot(data=congested_weather, x='weather_description', y='congested_rate')
plt.xticks(rotation=90)
plt.title('Top Weather with High Congestion Rates')
plt.show()

基于拥堵率的天气排行(图片由作者提供)
从图表来看:
-
拥堵率最高的三种天气情况是冰雹、小阵雪和暴雨。
-
与此同时,小雨和雪、带毛毛雨的雷暴、冰冻雨和阵风并未引起任何拥堵。在如此极端的天气下,人们一定都待在室内了!
结尾
在这篇博客文章中,我们介绍了如何在 EDA 练习中使用分组聚合。正如我们所看到的,这项技术在从数据中揭示有趣且有用的见解时非常有效,尤其是在处理粒度数据时。
希望你能在下一个 EDA 项目中练习使用分组聚合!总的来说,感谢你的阅读,欢迎在LinkedIn上与我联系!👋
提升 Python 代码效率并简化复杂工作流的强大功能
你需要了解的 Python 循环
·发表于 Towards Data Science ·9 分钟阅读·2024 年 9 月 15 日
--

图片来源:Xiaole Tao 在 Unsplash
Python 处理重复任务、自动化流程以及通过其强大的循环实现复杂算法的能力是相当显著的。为了帮助 Python 爱好者全面理解循环并掌握其在各种场景中的应用,本文将介绍我认为重要的 Python 循环的关键特性、用户常犯的错误以及如何避免这些错误。我还将分享一些实用的示例,展示 Python 循环如何通过简化流程和提高代码效率,增强典型的预测建模项目。
Python 循环的类型
在深入探讨 Python 循环的关键特性之前,了解各种类型的 Python 循环非常重要,因为它们构成了今天讨论的基础。Python 提供了两种主要类型的循环:for 循环和while 循环。
For 循环
for 循环会遍历一个集合中的项目,例如列表或字典,并对集合中的每个元素执行一段代码。以下是 for 循环的语法:
d = {"a": 1, "b": 2, "c": 3}
for key in d:
print(key)
一种实用的算法效率方法

图片由Julian Hochgesang拍摄,来源于Unsplash
探索大 O 符号、运行时效率和算法可扩展性
·发表于Towards Data Science ·8 分钟阅读·2024 年 2 月 16 日
--
在我的文章中,我常常强调一个事实,那就是即使在从事人工智能工作时,软件工程技能仍然至关重要,因为归根结底我们生产的是代码。同样,算法理论也不例外,因为机器学习算法依然是算法,理解如何评估它们至关重要。例如,你知道 transformers 中的注意力机制是如何扩展的吗?
我使用Deepnote运行脚本:这是一个基于云的笔记本,适合协作数据科学项目,也非常适合原型开发。
基本算法分析
当我们研究或开发一个算法时,理解它所需要的时间和空间资源的数量是至关重要的。只有这样,我们才能理解它的扩展性。一个排序算法可能在一个长度为 3 的数组的玩具案例中看起来最优,但当我们有一个长度为 10M 的数组时,它可能会非常低效。
因此,比较两种不同算法的性能并非易事。某些情况下,一种算法可能看起来更快,而在其他情况下,另一种算法可能更快;我们需要一种数学的、正式的方法来正确评估…
数据分析的实用框架:六大核心原则

图片来源:Cytonn Photography via Unsplash
如何像专业人士一样从数据中发现洞察
·发布于Towards Data Science ·阅读时间 11 分钟·2024 年 11 月 14 日
--
作为一名在消费科技行业工作了六年的数据科学家,我已经进行了无数次的探索性数据分析(EDA),以从数据中发现洞察,最终目的是回答业务问题并验证假设。
基于我的这些经验,我将我的关键洞察提炼为六个数据分析原则。这些原则在我的日常工作中一再证明了其价值,我很高兴与大家分享。
在本文的其余部分,我们将逐一讨论这六个原则。
-
建立基准线
-
标准化指标
-
MECE 分组
-
聚合粒度数据
-
移除无关数据
-
应用帕累托原则
建立基准线
假设你在一家电子商务公司工作,管理层希望识别出拥有优质客户的地区(“优质”可以通过多种指标来定义,比如总支出、平均订单价值或购买频率)。
为了简化起见,假设公司在印度尼西亚的三个最大城市运营:雅加达、万隆和泗水。
一位经验不足的分析师可能会急于计算每个城市中优质客户的数量。假设他们发现如下数据。

优质用户分布(图由作者提供)
注意到 60%的优质客户位于雅加达。基于这一发现,他们建议管理层在雅加达增加市场营销投入。
然而,我们可以做得更好!
这种方法的问题在于,它只告诉我们哪个城市有最多的优秀客户数量。它没有考虑到,优秀客户最多的城市可能只是用户基数最大的城市。
鉴于此,我们需要将优秀客户分布与基准:所有用户的分布进行比较。这个基准帮助我们进行理智的检查,验证雅加达是否真的存在一个令人感兴趣的优秀客户数量。因为也许雅加达的优秀客户数量高,仅仅是因为它的总用户数最高——因此,拥有最多的优秀客户是可以预期的。
我们继续检索总的用户分布并获得以下结果。

所有用户分布作为基准(图源:作者)
结果显示,雅加达占所有用户的 60%。请注意,这验证了我们之前的担忧:雅加达拥有 60%的高价值客户,完全是与其用户基数成比例的;因此,雅加达并没有发生什么特别的事情。
当我们将两个数据结合起来,按城市获得优秀客户比例时,请考虑以下数据。

按城市的优秀用户比例(图源:作者)
观察泗水:它拥有 30 个优秀用户,而总用户数只有 150 人,导致优秀用户比例为 20%——是所有城市中最高的。
这是值得采取行动的洞察。它表明,泗水在高价值客户方面的倾向高于平均水平——换句话说,泗水的用户比雅加达的用户更有可能成为优秀客户。
规范化指标
考虑以下场景:业务团队刚刚进行了两个不同的主题产品活动,我们的任务是评估和比较它们的表现。
为此,我们计算了两个活动的总销售额并进行比较。假设我们获得了以下数据。

活动总销售额(图源:作者)
从这个结果中,我们得出结论,活动 A 优于活动 B,因为 450 百万大于 360 百万。
然而,我们忽略了一个重要因素:活动持续时间。如果最终发现两个活动的持续时间不同呢?如果是这样,我们需要规范化比较指标。否则,我们无法公平比较,因为活动 A 的销售额可能更高,仅仅是因为它持续的时间更长。
指标规范化确保我们能够公平地比较指标,使比较变得更加公正。在这种情况下,我们可以通过将销售指标除以活动持续天数来规范化销售指标,从而得出每日报销额。
假设我们得到了以下结果。

规范化销售数据的活动数据(图源:作者)
结论发生了变化!在规范化销售指标后,实际上是 B 活动表现更好。它每天的销售额为 1200 万,比 A 活动的 1000 万高出 20%。
MECE 分组
MECE 是咨询顾问最喜爱的框架。MECE 是他们将复杂问题拆解成更小、更易管理的部分或分区的首选方法。
MECE 代表互斥、完全穷尽。这里有两个概念。我们一个一个地来解决它们。为了演示这个概念,假设我们希望研究某个特定消费者应用服务的用户获取渠道归因。为了获得更多的洞察,我们根据用户的归因渠道将其分开。
假设在第一次尝试时,我们将归因渠道细分如下:
-
付费社交媒体
-
Facebook 广告
-
自然流量

上述分组的集合图:非 MECE(图由作者提供)
互斥(ME)意味着分解集之间不能重叠。换句话说,没有分析单元属于多个分解组。上面的分解不是互斥的,因为 Facebook 广告是付费社交媒体的一个子集。因此,Facebook 广告组中的所有用户也是付费社交媒体组的成员。
完全穷尽(CE)意味着分解的组必须包括所有可能的情况/子集,即没有分析单元会与任何分解组无关。上面的分解不是完全穷尽的,因为它没有包括通过其他渠道(如搜索引擎广告和联盟网络)获得的用户。
上述案例的 MECE 分解版本可以如下:
-
付费社交媒体
-
搜索引擎广告
-
联盟网络
-
自然流量

更新后分组的集合图:MECE!(图由作者提供)
MECE 分组使我们能够将大型异质数据集分解成更小、更同质的部分。这种方法有助于特定数据子集的优化、根本原因分析和其他分析任务。
然而,当存在多个子集时,创建 MECE 分解可能会具有挑战性,即当要分解的因子变量包含许多独特值时。考虑一个电子商务应用漏斗分析,用于了解用户的产品发现行为。在电子商务应用中,用户可以通过多种路径发现产品,这使得标准的 MECE 分组变得复杂(如搜索、分类、横幅广告,更不用说它们的组合)。
在这种情况下,假设我们主要关注了解用户的搜索行为。那么,创建二元分组是切实可行的:is_search 用户,其中如果用户曾经使用过应用的搜索功能,值为 1。这简化了 MECE 分解,同时仍然支持主要的分析目标。
如我们所见,二元标志提供了一种简单的 MECE 分解方法,在这种方法中,我们将最相关的类别作为正值(如 is_search、is_paid_channel 或 is_jakarta_user)。
聚合粒度数据
许多行业中的数据集都是粒度化的,这意味着它们以原始详细的级别呈现。例子包括交易数据、支付状态日志、应用内活动日志等。这些粒度数据是低级的,包含丰富的信息,但代价是高冗余。
我们在处理粒度数据时需要小心,因为它可能会妨碍我们获得有用的洞察。考虑以下简化的交易数据示例。

示例粒度交易数据(图片由作者提供)
乍一看,表格似乎没有包含任何有趣的发现。共有 20 笔交易涉及不同的手机,每笔交易的数量都是 1。因此,我们可能得出结论,认为没有有趣的模式,比如哪款手机比其他手机更受欢迎,因为它们的表现完全相同:它们的销量都是一样的。
然而,我们可以通过在手机品牌级别进行聚合,并计算每个品牌销量的百分比份额来改进分析。

交易数据的聚合过程(图片由作者提供)
突然之间,我们得到了不容忽视的发现。三星手机是最流行的,占总销量的 45%。其次是苹果手机,占总销量的 30%。接下来是小米,占 15%。而 Realme 和 Oppo 则是购买最少的,每个品牌的份额为 5%。
如我们所见,聚合是处理粒度数据的有效工具。它有助于将粒度数据的低级表示转换为更高级的表示,从而增加从数据中获得非平凡发现的可能性。
对于想了解更多关于聚合如何帮助发现有趣洞察的读者,请查看我下面的 Medium 文章。
学习如何使用分组聚合从数据中发现洞察
[towardsdatascience.com
移除无关数据
现实世界的数据既凌乱又脏乱。除了技术性问题,如缺失值和重复条目外,还存在数据完整性方面的问题。
这在消费类应用行业尤其如此。按设计,消费类应用的使用者数量庞大。消费类应用的一个常见特点是它们极度依赖促销策略。然而,存在一类特定的用户,他们非常 opportunistic(机会主义)。如果他们认为某项促销策略具有价值,他们可能会下很多订单以最大化其利益。这种异常行为可能会对我们的分析造成干扰。
举个例子,假设我们是一个电子商务平台的数据分析师。我们被分配了一个有趣的项目:分析每个产品类别的自然重新订购间隔。换句话说,我们想了解:用户通常需要多少天重新订购蔬菜?用户通常多久重新订购洗衣液?零食呢?牛奶呢?等等。这些信息将由 CRM 团队用来发送及时的订单提醒。
为了回答这个问题,我们检查了过去 6 个月的交易数据,目的是获得每个产品类别的中位数重新订购间隔。假设我们得到了以下结果。

每个产品类别的中位数重新订购间隔(图源:作者)
从数据来看,结果有些出乎意料。表格显示,米的中位数重新订购间隔为 3 天,食用油为 2 天。洗衣液和洗洁精的中位数重新订购间隔为 5 天。另一方面,蔬菜、牛奶和零食的订单频率大致符合我们的预期:蔬菜每周购买一次,牛奶和零食每月购买两次。
我们应该将这些发现报告给 CRM 团队吗?别急!
真的吗?人们每 3 天就购买一次米,或者每 2 天就购买一次食用油?这种消费者究竟是怎样的呢?
在重新审视数据时,我们发现了一群用户进行交易的频率极高——甚至是每天都在交易。这些过度购买主要集中在受欢迎的非易腐商品上,且对应的产品类别在我们的研究中显示出令人惊讶的低中位数重新订购间隔。
我们认为这些超级频繁的用户并不代表我们的典型目标客户。因此,我们将他们从分析中排除,并生成了更新后的研究结果。

更新后的中位数重新订购数据(图源:作者)
现在一切都说得通了。米、食用油、洗衣液和洗洁精的真实重新订购节奏,已被这些异常的超级频繁用户所扭曲,这些用户与我们的分析无关。在剔除这些异常值之后,我们发现人们通常每 14 天(每两周)重新订购一次米和食用油,而洗衣液和洗洁精则是按月购买的。
现在我们有信心与 CRM 团队分享这些洞察!
在行业中,从分析中去除无关数据的做法既常见又至关重要。在实际数据中,异常现象很常见,我们需要排除它们,以防止我们的结果受到它们极端行为的扭曲,而这些行为并不代表我们典型用户的行为。
应用帕累托原则
我想分享的最后一个原则是如何在分析数据时获得最大效益。为此,我们将应用帕累托原则。
帕累托原则指出,对于许多结果,大约 80%的后果来自 20%的原因。
根据我的行业经验,我观察到帕累托原则在许多场景中都有体现:只有少数产品贡献了大部分销售额,只有少数城市拥有大部分客户群,等等。我们可以在数据分析中运用这一原则,以节省时间和精力,创造出有效的洞察。
假设我们正在为一个跨越印度尼西亚所有一线和二线城市的电子商务平台工作(这些城市有几十个)。我们的任务是基于城市分析用户交易概况,涉及的指标包括购物车大小、交易频率、购买的产品、运输 SLA 以及用户地址距离等。
在初步查看数据后,我们发现 85%的销售额来自仅仅三个城市:雅加达、万隆和泗水。考虑到这一点,将我们的分析重点放在这三个城市上比试图分析所有城市更有意义(后者就像是在“煮海洋”,收效递减)。
采用这一策略,我们最大程度地减少了工作量,同时仍然实现了关键分析目标。所获得的洞察将保持有意义和相关,因为它们来源于大多数人群。此外,基于这些洞察得出的商业建议,按定义将对整个群体产生重大影响,从而依然具有强大的影响力。
应用帕累托原则的另一个优点与建立 MECE 分组有关。在我们的例子中,我们可以将城市分为四组:雅加达、万隆、泗水和“其他”(将所有剩余的城市合并为一组)。通过这种方式,帕累托原则有助于简化我们的 MECE 分组:每个主要贡献的城市独立存在,而其余的城市(超过帕累托阈值的)被合并为一个单独的组。
结语
感谢你坚持阅读完本文的最后部分!
在这篇文章中,我们讨论了六个数据分析原则,这些原则可以帮助我们更有效地发现洞察。这些原则来源于我多年的行业经验,并且在我的 EDA 实践中极为有用。希望你在未来的 EDA 项目中也能发现这些原则的价值。
再次感谢阅读,让我们在LinkedIn上联系吧!👋
搜索评估的实用框架
基于数据的方法提升用户体验和业务表现
·发表于Towards Data Science ·阅读时间 13 分钟·2024 年 7 月 12 日
--

搜索评估 — 图片来源:inspiring.team
搜索功能在当今几乎所有数字资产中的用户体验中都起着至关重要的作用。无论是电商平台、内容丰富的网站,还是内部知识库,搜索结果的质量都能决定用户的失望与满足之间的差异。
那么,你如何知道你的搜索算法是否返回了相关的结果呢?你如何确定它满足了用户需求并推动了业务目标的实现呢?虽然这是一个非常重要的子应用,但实际上我们缺乏一个结构化的方法来评估搜索算法。
这正是该搜索算法评估框架所提供的内容。通过建立一个系统的搜索质量评估程序,企业将能够获得关于算法表现的有意义的洞见,了解在哪些方面应投入更多努力以推动改进,并学会如何衡量长期的进展。
在这篇文章中,我们将探讨一个完整的搜索算法评估框架,包括通过用户行为定义相关性、量化性能指标的测量方法,以及如何将这些方法调整以满足特定的业务需求。
搜索评估的商业案例
搜索评估不仅仅是一个纯技术性工作,它是一个战略性的商业决策,且在每个环节中都有广泛的影响。要理解这一点,首先需要考虑搜索在当今数字环境中的地位。
对于许多企业来说,搜索功能将是用户与其数字产品互动的首要方式。这可以是客户在电商网站上寻找产品、员工在内部知识库中查找信息,或者读者在内容平台上探索——通常,搜索是首先发生的。然而,当这一关键功能表现不佳时,可能会导致严重的后果。
搜索表现不佳会导致用户满意度和参与度下降。当用户找不到自己想要的内容时,他们会很快感到沮丧。这种沮丧情绪迅速对跳出率施加压力,最终减少在网站上的停留时间,最终导致错失机会。
另一方面,一个经过精细调整的搜索功能可以成为推动企业成功的最大驱动力之一。它可以提高转化率,改善用户参与度,有时甚至能开辟全新的收入渠道。对于内容网站来说,改进的搜索可能推动广告展示和订阅,而对于内部系统,它可能显著缩短员工寻找信息时浪费的时间。
在这个极度个性化的时代,良好的搜索功能将处于所有个性化体验的核心。搜索表现评估有助于理解和了解用户的偏好和行为,从而不仅为搜索改进提供指导,也为更广泛的战略决策提供依据。
通过全面投资于搜索评估,你所做的并不仅仅是改善一个技术功能。这实际上是在为你的企业在数字时代蓬勃发展的韧性进行投资。
评估搜索相关性的常用方法
衡量企业搜索功能表现的基本问题并非技术性问题。具体来说,是定义任何用户进行的某一特定搜索所构成的相关结果。简而言之,问题是:“对于任何特定的搜索,什么算是好的搜索结果?”
这一过程高度主观,因为不同的用户可能对同一个查询有不同的意图和期望。质量的定义也因行业细分而异。每种类型的业务都需要根据自身的目标和用户群体,采取不同的方式来完成这一评估。
尽管这一问题复杂且主观,但它推动了搜索社区开发出几种广泛采用的度量标准和方法,用以满足对搜索算法评估的需求。这些方法将相关性和用户满意度进行操作化,从而尝试量化这些指标。因此,它们提供了评估和改善搜索表现的一种方式。单一方法无法捕捉搜索相关性的全部复杂性,但它们的结合能为搜索算法服务用户的效果提供有价值的洞见。在接下来的部分,我们将探讨一些常见的评估方法,包括点击流分析和以人为本的方法。
点击流分析
获取洞察的最常见指标之一是通过用户与网站互动时获得的指标。第一个是点击率(CTR),即在看到结果后点击该结果的用户比例。

点击率并不一定衡量搜索结果的相关性,更多的是衡量其吸引力。然而,大多数企业仍然倾向于优先考虑吸引人的结果,而非用户倾向忽视的结果。
其次是停留时间,即用户点击某个页面后停留在该页面上的时间。如果停留时间较短,表示用户没有足够参与内容。这可能意味着该搜索结果对他们来说不相关。
我们还有跳出率(BR)。跳出率是指在没有点击任何结果的情况下离开搜索的用户比例。

通常,较高的跳出率表明没有任何搜索结果对用户相关,因此一个好的搜索引擎应该尽量减少跳出率。
最后,另一个可以分析的指标(如果适用)是任务完成率(TCR)。任务完成率是指完成了一个期望任务(例如购买产品)的用户占所有浏览该任务的用户的比例。

这个指标在行业和使用场景上具有高度的特定性。例如,这是一个电子商务企业非常重视的指标,而学术期刊通常不会。较高的任务完成率表明产品或服务对客户有吸引力,因此在搜索算法中优先考虑它是相关的。
以人为本的评估方法
虽然点击流分析提供了一些有用的定量数据,但以人为本的评估方法为搜索相关性提供了关键的定性洞察。这些方法基于直接的人类判断,获取对搜索结果质量和相关性的反馈。
可能衡量搜索效果最直接的方式就是直接询问用户。这可以通过在每个搜索结果旁放置一个简单的点赞/点踩按钮来实现,让用户表明一个结果是否有用。更详细的问卷进一步可以检查用户满意度和搜索体验的具体情况,从非常基础到相当复杂,提供关于用户感知和需求的第一手宝贵数据。
更正式地说,许多组织可以使用评审小组、搜索分析师或工程师。通过生成各种测试查询,并根据预定义的标准或量表(例如,1–10 的相关性评分)对结果进行评估。尽管这一过程可能非常耗时且成本高昂,但它提供了自动化系统无法比拟的细致评估。评审人员可以评估上下文相关性、内容质量以及最重要的,与业务目标的相关性。
基于任务的用户测试提供了用户在使用搜索引擎执行特定任务时的表现信息。它不仅揭示了结果的相关性,还能深入分析搜索体验的其他方面,包括易用性和满意度等参数。这些方法能够揭示出可用性问题和用户行为,这些往往是单独依赖定量数据时所忽视的。
这些以人为中心的方法,虽然比自动化分析方法资源消耗更大,但却能提供对搜索相关性深刻的洞察。通过将这些方法与定量方法结合使用,组织可以深入了解其搜索表现及需要改进的领域。
搜索算法的定量评估指标
在定义了什么构成良好搜索结果的系统后,接下来就是衡量我们的搜索算法如何检索这些结果。在机器学习领域,这些参考评估被称为真实标签。以下指标适用于信息检索系统的评估,其中大多数在推荐系统中也有相应的对照。在接下来的部分中,我们将介绍一些相关的定量指标,从简单的如精确率和召回率,到更复杂的衡量指标,如标准化折扣累积增益(NDCG)。
混淆矩阵
尽管混淆矩阵通常是机器学习分类问题中的一种工具,但它也可以有效地用于搜索算法的评估。由于搜索结果通常被简单地分类为相关或不相关,混淆矩阵提供了一种直观的方式来衡量搜索性能。此外,可以从中计算出一些重要的指标,使其在保持简洁易用的同时,更具实用性。下方展示了应用于信息检索的混淆矩阵。

检索系统的混淆矩阵
在这里,对于给定的搜索查询,结果搜索可以被归入以下四个类别:它被正确检索,尽管是错误检索但不相关,或者它本可以正确忽略,或者结果是相关的,但无论如何被忽略了。
我们在这里需要考虑的主要是第一页,因为大多数用户很少会翻到之后的页面。我们引入了一个截断点,通常这个截断点大约是每页结果的数量。
让我们运行一个示例。假设我们有一个电商网站,每页列出 10 个产品。库中共有 50 个产品,其中 8 个是实际相关的。搜索算法成功地将其中 7 个相关产品显示在第一页。在这种情况下:
-
RR = 7(正确返回的相关产品)
-
IR = 3(页面总数 10 — 7 个相关 = 显示的 3 个不相关结果)
-
RI = 1(8 个总相关项目 — 显示的 7 个 = 丢失的 1 个相关产品)
-
II = 39(50 个总产品 — 显示的 10 个 — 丢失的 1 个相关 = 正确忽略的 39 个)
从混淆矩阵中可以得出的关键指标包括精确度和召回率。精确度是指检索到的项目中相关的比例。在给定的示例中,精确度为 7/10。这也被称为Precision @ K,其中 K 是排名前几的项目的截断点。

召回率是指检索到的相关项目的比例。在给定的示例中,召回率为 7/8。

这些都是需要跟踪的重要指标,因为低精确度表示用户看到很多不相关的结果,而低召回率表示许多相关的结果未能显示给用户。这两个指标通过一个单一的度量综合并平衡,即 F1 分数,它取了这两个指标的调和平均数。在上述示例中,F1 分数为 7/9。

我们可以归结出这一简单搜索性能度量的两个重要局限性。第一个是它没有考虑结果的顺序,仅仅关注是否成功检索到相关项。这可以通过扩展混淆矩阵中得出的指标,提供更先进的度量方法来缓解,例如均值平均精度(MAP)。第二个局限性是(从我们的示例中可以看出)如果我们每页的相关结果少于实际结果数量,即使算法检索到了所有相关项,它也永远无法获得完美的得分。
总体而言,混淆矩阵提供了一种简单的方式来检查搜索算法的性能,通过将搜索结果分类为相关或不相关。这是一个相当简化的度量方法,但它与大多数搜索结果评估方法(特别是类似用户需要对特定结果提供点赞/点踩反馈的情况)兼容并且有效。
经典误差度量
存储搜索索引的大多数数据库,例如OpenSearch,通常会为搜索结果分配分数,并检索分数最高的文档。如果提供了这些分数,则可以利用实际数据分数推导出更多关键度量指标。
一种非常常见的度量指标是平均绝对误差(MAE),它比较了被认为是正确或理想的分数与算法为给定搜索结果分配的分数之间的差异。然后计算所有这些偏差的平均值,使用以下公式,其中帽子符号表示估计值,y 是给定搜索结果的实际分数。

较高的 MAE 表示搜索结果表现不佳,MAE 为零意味着搜索结果完全符合实际数据。
一个类似但更加常见的度量指标是均方误差(MSE),它与平均绝对误差相似,但每个偏差都会被平方。

使用均方误差(MSE)相对于平均绝对误差(MAE)的主要优点是,MSE 会对极端值进行惩罚,因此几条评分极差的结果会导致 MSE 远高于 MAE。
总体而言,通过为结果分配分数,我们可以使用更多经典的方法来量化搜索算法所感知的相关性与我们通过经验数据发现的相关性之间的差异。
高级信息检索度量
许多组织会使用高级度量指标,如归一化折扣累积增益(NDCG)和平均倒数排名(MRR),来洞察其搜索系统的表现。这些指标提供了超越简单搜索质量的精度和召回的深入分析。
归一化折扣累积增益(NDCG)是衡量搜索结果排名质量的度量指标。特别是在有等级相关性评分的情况下,它考虑了结果的相关性,并将其按顺序排列在搜索输出中。NDCG 的核心思想是将最相关的结果展示在搜索结果列表的顶部。首先,需要计算 DCG 以进行 NDCG 的计算。在这种情况下,DCG 是仅从搜索索引获得的相关性分数的总和,通过其位置的对数进行折扣,然后与理想排名进行归一化,最终得到一个介于 0 和 1 之间的分数。DCG 计算的表示如下所示:

这里,p 是搜索结果在排名中的位置,rel 是位置 i 的结果相关性分数。这个计算对于真实分数和实际数据的分数都要进行,两个值的商就是 NDCG。

在上述公式中,IDCG 指的是理想或真实相关性评分的 DCG 计算。NDCG 之所以特别有用,是因为它能够处理多级相关性判断。它可以区分那些有些相关的结果和那些高度相关的结果。此外,NDCG 通过位置调节这一点,使用一个递减函数来反映用户通常不会查看列表下方的结果。NDCG 评分为 1 意味着算法返回的结果是按最相关的顺序排列的。
相对而言,平均倒数排名(MRR)聚焦于第一个正确或相关结果的排名。MRR 通过计算某一查询集合中,首个相关文档的倒数排名的平均值来评估。

这里,Q 表示查询的数量,rank 表示给定查询中第一个相关结果的位置。MRR 值介于 0 和 1 之间,值越高越好。MRR 为 1 意味着对于任何查询,最相关的结果始终出现在最顶部位置。这个指标特别适用于评估搜索性能,特别是在用户通常寻找单一信息的应用中,如问答系统或电商平台上的产品搜索。
这些度量标准,当应用于系统时,会为搜索算法的表现提供一个视角。
实施综合评估系统
在每个搜索算法中,都需要一个综合评估系统,结合上述方法和定量度量。
尽管自动化度量在提供定量数据方面具有强大的作用,但我们不应忽视人类判断在真正评估搜索相关性中的作用。在评估过程中,通过定期的专家评审和用户反馈评审来增加上下文。专家和用户反馈的定性特征有助于赋予有时模糊的定量结果更多的意义,进而揭示自动化度量可能忽视的系统问题。人类因素将反馈放入上下文中并为其增加维度,确保我们不仅优化数字指标,还优化真实的用户满意度。
最后,需要将度量标准调整为业务需求。适用于电商网站的度量标准可能完全不适用于内容平台或内部知识库。一个相关的评估框架视角应当根据上下文量身定制——基于与业务目标的相关性以及被评估算法的期望。定期的评审和调整评估标准将确保与不断变化的业务目标和最终用户需求保持一致。
除非另有说明,图片由作者创建。
贝叶斯数据科学:什么,为什么,以及如何
选择频率派和贝叶斯方法之间的争论是上世纪的大辩论之一,近期贝叶斯方法在科学领域的应用有了激增。
·发表于 Towards Data Science ·阅读时长:5 分钟·2024 年 4 月 18 日
--

在 sciencedirect.com 上提到贝叶斯统计的文章数量(2024 年 4 月)——图表由作者提供
这有什么不同?
哲学上的差异其实非常微妙,有人认为伟大的贝叶斯批评家费舍尔在某种程度上本身也是一位贝叶斯学派的人。虽然有无数文章深入探讨公式上的差异,但实际的好处是什么呢?贝叶斯分析能为普通的数据科学家提供什么,是频率派方法已经涵盖不了的?本文旨在为读者提供一个关于贝叶斯方法的动机、构思和应用的实用介绍。让我们深入了解。
先验信念
频率派统计学家专注于描述数据的精确分布,而贝叶斯观点则更主观。主观性和统计学?没错,它们实际上是兼容的。
让我们从简单的例子开始,比如掷硬币。假设你掷硬币 10 次,得到正面 7 次。那么,得到正面的概率是多少?
P(正面) = 7/10 (0.7)?
显然,这里我们遇到了低样本量的问题。然而,从贝叶斯的角度来看,我们可以直接编码我们的信念,假设如果硬币是公平的,那么正面和反面的概率必须相等,即 1/2。虽然在这个例子中选择似乎非常明显,但当我们面对更复杂、不太显而易见的现象时,讨论会更加微妙。
然而,这个简单的例子是一个强有力的起点,突出了贝叶斯分析的最大优点和缺点:
优点:处理数据不足。假设你正在建模一个国家的传染病传播,而该国的数据收集稀缺。你会使用这些少量的数据来得出所有的结论吗?还是希望将类似国家中常见的模式(即有依据的先验信念)纳入模型中?虽然选择显而易见,但这直接导致了缺点的出现。
缺点:先验信念难以构建。例如,如果硬币实际上不是公平的,那么假设 P (正面朝上) = 0.5 就是错误的,并且几乎不可能在没有长期实验的情况下找到真正的 P (正面朝上)。在这种情况下,假设 P (正面朝上) = 0.5 反而会对寻找真相带来不利影响。然而,每一个统计模型(无论是频率派的还是贝叶斯派的)都必须在某种层面上做出假设,而人类大脑中的“统计推理”实际上与贝叶斯推理非常相似,即构建先验信念系统,在每个新情境中都影响我们的决策。此外,从建模角度来看,构建错误的先验信念通常并不意味着“死刑”,如果我们能从足够的数据中学习(这一点将在后续文章中进一步讨论)。
贝叶斯定理
那么这一切在数学上看起来是什么样的呢?贝叶斯定理为此奠定了基础。假设我们有一个参数 θ,它定义了某个模型,能够描述我们的数据(例如,θ 可能代表均值、方差、相对于协变量的斜率等)。贝叶斯定理表明

托马斯·贝叶斯在 1700 年代提出了贝叶斯定理,并在去世后发布。[图片 通过维基共享资源,依据 知识共享 署名-相同方式共享 4.0 国际,未经改动]
P (θ = t|data) ∝ P (data|θ = t) * P (θ=t)
用更简单的话说,
-
P (θ = t|data) 表示在给定数据的情况下,θ 等于 t 的条件概率(也就是后验)。
-
反过来,P (data|θ) 表示在 θ = t 时,观察到我们数据的概率(也就是‘似然’)。
-
最后,P (θ=t) 只是 θ 取值 t 的概率(即臭名昭著的‘先验’)。
那么,这个神秘的 t 是什么呢?它可以取多个可能的值,这取决于 θ 的含义。实际上,你需要尝试很多值,并检查每个值下数据的似然。这是一个关键步骤,你真的非常希望自己检查了θ的最佳可能值,即那些覆盖数据的最大似然区域的值(对那些在意的人来说,就是全局最小值)。
这就是贝叶斯推理的核心内容!
-
为可能的 θ 值构建一个先验信念,
-
用每个 θ 值下的似然,结合观察到的数据进行缩放,且
-
返回计算结果,即后验,它告诉你每个测试的θ值的概率。
从图形上看,结果大致如下:

先验(左)与似然(中)的结合形成了后验(右)(图像改编自 Andrew Gelman 的书籍)。这里,θ 编码了飞机的东西向位置坐标。先验信念认为飞机偏向东部而不是西部。数据挑战了这一先验,因此后验位于两者之间。[图片使用作者生成的数据]
这也突显了贝叶斯统计的下一个重大优势——
-
我们对θ分布的整体形状有了一个大致的了解(例如,峰值有多宽,尾部有多重,等等),这可以使我们进行更为稳健的推断。为什么?因为我们不仅能更好地理解它,还能够量化不确定性(与传统的点估计和标准差相比)。
-
由于这个过程是迭代的,我们可以随着更多数据流入模型,不断更新我们的信念(估计),这使得构建完全在线的模型变得更加容易。
听起来很简单!但其实并非如此…
这个过程涉及大量计算,你需要为每个可能的θ值计算似然。好吧,如果假设θ的范围很小,比如 [0,1],也许这会很简单。我们可以使用暴力网格方法,在离散的区间(例如 10,0.1 区间,或者 100,0.01 区间,或更多……你懂的)测试值,以所需的分辨率映射整个空间。
但如果空间非常庞大,而且上帝保佑,如果涉及到额外的参数,像任何现实生活中的建模场景一样怎么办?
现在,我们不仅需要测试可能的参数值,还需要测试所有可能的组合,即解空间呈指数级扩展,这使得网格搜索在计算上变得不可行。幸运的是,物理学家已经在高效采样问题上做了大量工作,今天已经有了先进的算法(例如,Metropolis-Hastings MCMC、变分推断),它们能够快速探索高维参数空间并找到凸点。你也不必自己编写这些复杂的算法,像 PyMC 或 STAN 这样的概率计算语言使得这个过程变得高度简化且直观。
STAN
STAN 是我最喜欢的工具,因为它支持与更多常见的数据科学语言(如 Python、R、Julia、MATLAB 等)接口,从而有助于推广。STAN 依赖于最先进的哈密尔顿蒙特卡洛采样技术,几乎可以确保在规定时间内对良好指定的模型进行合理的收敛。在我下篇文章中,我将介绍如何开始使用 STAN,涵盖简单和复杂的回归模型,并附带完整的 Python 代码讲解。我还会介绍完整的贝叶斯建模工作流,包括模型规范、拟合、可视化、比较和解释。
关注并保持关注!
对比学习实用指南
如何使用 FashionMNIST 构建你的第一个 SimSiam 模型
·发表于 Towards Data Science ·阅读时长 10 分钟·2024 年 7 月 30 日
--
对比学习在当今有很多应用场景。从自然语言处理(NLP)和计算机视觉到推荐系统,对比学习可以在没有任何显式标签的情况下学习数据的潜在表示,然后可以用于下游的分类、检测、相似度搜索等任务。
网上有很多资源可以帮助读者理解对比学习的基本概念,因此我不会再写一篇重复这些信息的博客文章。相反,在本文中,我将展示如何将你的监督学习问题转化为对比学习问题。具体来说,我将从一个基本的分类模型开始,使用 FashionMNIST 数据集(MIT 许可证)。接着,我将处理一个有有限训练标签的高级问题(例如,将完整的 60,000 个标签的训练集缩减为 1,000 个标签)。我将介绍 SimSiam 这一最先进的对比学习方法,并提供逐步的指导,说明如何按 SimSiam 风格修改原始的线性层。最终,我会展示结果 —— SimSiam 在一个非常基础的配置下,能够提高 15%的 F1 分数。

图片来源:pxhere.com/en/photo/395408
现在,让我们开始吧。首先,我们将加载 FashionMNIST 数据集。我们使用一个自定义的 FashionMNIST 类来获取训练集的一个子集,命名为 finetune_dataset。自定义 FashionMNIST 类的源代码将在本文结尾处提供。
import matplotlib.pyplot as plt
import torchvision.transforms as transforms
from FashionMNIST import FashionMNIST
train_dataset = FashionMNIST("./FashionMNIST",
train=True,
transform=transforms.ToTensor(),
download=True,
)
test_dataset = FashionMNIST("./FashionMNIST",
train=False,
transform=transforms.ToTensor(),
download=True,
)
finetune_dataset = FashionMNIST("./FashionMNIST",
train=True,
transform=transforms.ToTensor(),
download=True,
first_k=1000,
)
# Create a subplot with 4x4 grid
fig, axs = plt.subplots(4, 4, figsize=(8, 8))
# Loop through each subplot and plot an image
for i in range(4):
for j in range(4):
image, label = train_dataset[i * 4 + j] # Get image and label
image_numpy = image.numpy().squeeze() # Convert image tensor to numpy array
axs[i, j].imshow(image_numpy, cmap='gray') # Plot the image
axs[i, j].axis('off') # Turn off axis
axs[i, j].set_title(f"Label: {label}") # Set title with label
plt.tight_layout() # Adjust layout
plt.show() # Show plot
代码将展示来自 train_dataset 的图像网格。

来自 FashionMNIST 训练集的前 16 张图像。图片来自作者。
接下来,我们将定义监督分类模型。该架构包含一个卷积层的主干网络和一个由两层线性层组成的 MLP 头部。这将为接下来的实验设置一个一致的基准,因为 SimSiam 仅会替换 MLP 头部以进行对比学习。
import torch.nn as nn
class supervised_classification(nn.Module):
def __init__(self):
super(supervised_classification, self).__init__()
self.backbone = nn.Sequential(
nn.Conv2d(1, 32, kernel_size=3, stride=2, padding=1),
nn.ReLU(),
nn.BatchNorm2d(32),
nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1),
nn.ReLU(),
nn.BatchNorm2d(64),
nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1),
nn.ReLU(),
nn.BatchNorm2d(128),
)
self.fc = nn.Sequential(
nn.Linear(128*4*4, 32),
nn.ReLU(),
nn.Linear(32, 10),
)
def forward(self, x):
x = self.backbone(x).view(-1, 128 * 4 * 4)
return self.fc(x)
我们将训练模型 10 个 epoch:
import tqdm
import torch
import torch.optim as optim
from torch.utils.data import DataLoader
import wandb
wandb_config = {
"learning_rate": 0.001,
"architecture": "fashion mnist classification full training",
"dataset": "FashionMNIST",
"epochs": 10,
"batch_size": 64,
}
wandb.init(
# set the wandb project where this run will be logged
project="supervised_classification",
# track hyperparameters and run metadata
config=wandb_config,
)
# Initialize model and optimizer
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
supervised = supervised_classification()
optimizer = optim.SGD(supervised.parameters(),
lr=wandb_config["learning_rate"],
momentum=0.9,
weight_decay=1e-5,
)
train_dataloader = DataLoader(train_dataset,
batch_size=wandb_config["batch_size"],
shuffle=True,
)
# Training loop
loss_fun = nn.CrossEntropyLoss()
for epoch in range(wandb_config["epochs"]):
supervised.train()
train_loss = 0
for batch_idx, (image, target) in enumerate(tqdm.tqdm(train_dataloader, total=len(train_dataloader))):
optimizer.zero_grad()
prediction = supervised(image)
loss = loss_fun(prediction, target)
loss.backward()
optimizer.step()
wandb.log({"training loss": loss})
torch.save(supervised.state_dict(), "weights/fully_supervised.pt")
使用 scikit-learn 包中的 classification_report,我们将得到以下结果:
from sklearn.metrics import classification_report
supervised = supervised_classification()
supervised.load_state_dict(torch.load("weights/fully_supervised.pt"))
supervised.eval()
supervised.to(device)
target_list = []
prediction_list = []
for batch_idx, (image, target) in enumerate(tqdm.tqdm(test_dataloader, total=len(test_dataloader))):
with torch.no_grad():
prediction = supervised(image.to(device))
prediction_list.extend(torch.argmax(prediction, dim=1).detach().cpu().numpy())
target_list.extend(target.detach().cpu().numpy())
print(classification_report(target_list, prediction_list))
# Create a subplot with 4x4 grid
fig, axs = plt.subplots(4, 4, figsize=(8, 8))
# Loop through each subplot and plot an image
for i in range(4):
for j in range(4):
image, label = test_dataset[i * 4 + j] # Get image and label
image_numpy = image.numpy().squeeze() # Convert image tensor to numpy array
prediction = supervised(torch.unsqueeze(image, dim=0).to(device))
prediction = torch.argmax(prediction, dim=1).detach().cpu().numpy()
axs[i, j].imshow(image_numpy, cmap='gray') # Plot the image
axs[i, j].axis('off') # Turn off axis
axs[i, j].set_title(f"Label: {label}, Pred: {prediction}") # Set title with label
plt.tight_layout() # Adjust layout
plt.show() # Show plot

完全监督模型的分类结果。图片来自作者。
现在,让我们思考一个新问题。如果我们只获得训练集标签的有限子集,例如,仅有 60,000 张图像中的 1000 张有标签,我们该怎么办?自然的想法是简单地在有限的标注数据集上训练模型。因此,在不改变主干网络的情况下,我们让模型在有限的子集上训练 100 个 epoch(我们增加训练 epoch 数,以便与 SimSiam 的训练做公平比较):
import tqdm
import torch
import torch.optim as optim
from torch.utils.data import DataLoader
import wandb
wandb_config = {
"learning_rate": 0.001,
"architecture": "fashion mnist classification full training on finetune set",
"dataset": "FashionMNIST",
"epochs": 100,
"batch_size": 64,
}
wandb.init(
# set the wandb project where this run will be logged
project="supervised_classification",
# track hyperparameters and run metadata
config=wandb_config,
)
# Initialize model and optimizer
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
supervised = supervised_classification()
optimizer = optim.SGD(supervised.parameters(),
lr=wandb_config["learning_rate"],
momentum=0.9,
weight_decay=1e-5,
)
finetune_dataloader = DataLoader(finetune_dataset,
batch_size=wandb_config["batch_size"],
shuffle=True,
)
# Training loop
loss_fun = nn.CrossEntropyLoss()
for epoch in range(wandb_config["epochs"]):
supervised.train()
train_loss = 0
for batch_idx, (image, target) in enumerate(tqdm.tqdm(finetune_dataloader, total=len(finetune_dataloader))):
optimizer.zero_grad()
prediction = supervised(image)
loss = loss_fun(prediction, target)
loss.backward()
optimizer.step()
wandb.log({"training loss": loss})
torch.save(supervised.state_dict(), "weights/fully_supervised_finetunedataset.pt")

在有限训练集上的完全监督训练损失。图片来自作者。

在测试集上的定量评估结果。注意,通过减少训练集大小,性能下降超过 25%。图片来自作者。
现在是进行对比学习的时候了。为了缓解标注标签不足的问题,并充分利用大量的未标注数据,可以使用对比学习来有效地帮助主干网络学习数据表示,而无需特定任务。主干网络可以在给定的下游任务中冻结,并仅在有限的标注数据集上训练一个浅层网络,从而获得令人满意的结果。
最常用的对比学习方法包括 SimCLR、SimSiam 和 MOCO(请参见我之前的关于 MOCO 的文章)。在这里,我们对 SimCLR 和 SimSiam 进行了比较。
SimCLR在数据批次内计算正样本对和负样本对,这需要硬负样本挖掘、NT-Xent 损失(它扩展了批次上的余弦相似度损失)以及较大的批量大小。SimCLR 还需要 LARS 优化器来适应较大的批量大小。
SimSiam使用的是一种孪生网络架构,它避免了使用负样本对,进而避免了对大批量数据的需求。SimSiam 与 SimCLR 的区别如下表所示。

SimCLR 和 SimSiam 的比较。图片来自作者。

SimSiam 架构。图片来源:arxiv.org/pdf/2011.10566
从上图可以看出,SimSiam 架构仅包含两个部分:编码器/主干网络和预测器。在训练过程中,Siamese 部分的梯度传播被停止,并计算预测器和主干网络输出之间的余弦相似度。
那么,我们如何在实际中实现这个架构呢?继续基于监督分类设计,我们保持主干网络不变,仅修改 MLP 层。在监督学习架构中,MLP 输出一个包含 10 个元素的向量,表示 10 个类别的概率。但对于 SimSiam 来说,目标不是进行“分类”,而是学习“表示”,因此我们需要输出的维度与主干网络的输出维度相同,以便进行损失计算。负余弦相似度公式如下:
import torch.nn as nn
import matplotlib.pyplot as plt
class SimSiam(nn.Module):
def __init__(self):
super(SimSiam, self).__init__()
self.backbone = nn.Sequential(
nn.Conv2d(1, 32, kernel_size=3, stride=2, padding=1),
nn.ReLU(),
nn.BatchNorm2d(32),
nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1),
nn.ReLU(),
nn.BatchNorm2d(64),
nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1),
nn.ReLU(),
nn.BatchNorm2d(128),
)
self.prediction_mlp = nn.Sequential(nn.Linear(128*4*4, 64),
nn.BatchNorm1d(64),
nn.ReLU(),
nn.Linear(64, 128*4*4),
)
def forward(self, x):
x = self.backbone(x)
x = x.view(-1, 128 * 4 * 4)
pred_output = self.prediction_mlp(x)
return x, pred_output
cos = nn.CosineSimilarity(dim=1, eps=1e-6)
def negative_cosine_similarity_stopgradient(pred, proj):
return -cos(pred, proj.detach()).mean()
训练 SimSiam 的伪代码在原始论文中如下所示:

SimSiam 的训练伪代码。来源:arxiv.org/pdf/2011.10566
我们将其转化为真实的训练代码:
import tqdm
import torch
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision.transforms import RandAugment
import wandb
wandb_config = {
"learning_rate": 0.0001,
"architecture": "simsiam",
"dataset": "FashionMNIST",
"epochs": 100,
"batch_size": 256,
}
wandb.init(
# set the wandb project where this run will be logged
project="simsiam",
# track hyperparameters and run metadata
config=wandb_config,
)
# Initialize model and optimizer
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
simsiam = SimSiam()
random_augmenter = RandAugment(num_ops=5)
optimizer = optim.SGD(simsiam.parameters(),
lr=wandb_config["learning_rate"],
momentum=0.9,
weight_decay=1e-5,
)
train_dataloader = DataLoader(train_dataset, batch_size=wandb_config["batch_size"], shuffle=True)
# Training loop
for epoch in range(wandb_config["epochs"]):
simsiam.train()
print(f"Epoch {epoch}")
train_loss = 0
for batch_idx, (image, _) in enumerate(tqdm.tqdm(train_dataloader, total=len(train_dataloader))):
optimizer.zero_grad()
aug1, aug2 = random_augmenter((image*255).to(dtype=torch.uint8)).to(dtype=torch.float32) / 255.0, \
random_augmenter((image*255).to(dtype=torch.uint8)).to(dtype=torch.float32) / 255.0
proj1, pred1 = simsiam(aug1)
proj2, pred2 = simsiam(aug2)
loss = negative_cosine_similarity_stopgradient(pred1, proj2) / 2 + negative_cosine_similarity_stopgradient(pred2, proj1) / 2
loss.backward()
optimizer.step()
wandb.log({"training loss": loss})
if (epoch+1) % 10 == 0:
torch.save(simsiam.state_dict(), f"weights/simsiam_epoch{epoch+1}.pt")
我们训练了 100 个 epoch,以便与有限的监督训练进行公平比较;训练损失如下所示。注意:由于其 Siamese 设计,SimSiam 可能对超参数,如学习率和 MLP 隐藏层,非常敏感。原始的 SimSiam 论文提供了 ResNet50 主干网络的详细配置。对于基于 ViT 的主干网络,我们建议阅读MOCO v3 论文,该论文采用了 SimSiam 模型,并使用动量更新方案。

SimSiam 的训练损失。图片由作者提供。
然后,我们在测试集上运行训练好的 SimSiam,并使用 UMAP 降维可视化表示:
import tqdm
import numpy as np
import torch
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
simsiam = SimSiam()
test_dataloader = DataLoader(test_dataset, batch_size=32, shuffle=False)
simsiam.load_state_dict(torch.load("weights/simsiam_epoch100.pt"))
simsiam.eval()
simsiam.to(device)
features = []
labels = []
for batch_idx, (image, target) in enumerate(tqdm.tqdm(test_dataloader, total=len(test_dataloader))):
with torch.no_grad():
proj, pred = simsiam(image.to(device))
features.extend(np.squeeze(pred.detach().cpu().numpy()).tolist())
labels.extend(target.detach().cpu().numpy().tolist())
import plotly.express as px
import umap.umap_ as umap
reducer = umap.UMAP(n_components=3, n_neighbors=10, metric="cosine")
projections = reducer.fit_transform(np.array(features))
px.scatter(projections, x=0, y=1,
color=labels, labels={'color': 'Fashion MNIST Labels'}
)

SimSiam 表示在测试集上的 UMAP 降维。图片由作者提供。
有趣的是,在上面的降维图中,出现了两个小岛:类别 5、7、8 以及部分 9。如果我们查看 FashionMNIST 类别列表,就知道这些类别对应的是鞋类,如“凉鞋”、“运动鞋”、“包”和“短靴”。而大的紫色簇对应的是衣物类,如“T 恤/上衣”、“裤子”、“套头衫”、“连衣裙”、“外套”和“衬衫”。SimSiam 展示了在视觉领域中学习有意义的表示。
既然我们已经得到了正确的表示,那么它们如何帮助我们的分类问题呢?我们只需将训练好的 SimSiam 主干网络加载到我们的分类模型中。然而,我们并不是在有限的训练集上微调整个架构,而是微调线性层并冻结主干网络,因为我们不想破坏已经学到的表示。
import tqdm
import torch
import torch.optim as optim
from torch.utils.data import DataLoader
import wandb
wandb_config = {
"learning_rate": 0.001,
"architecture": "supervised learning with simsiam backbone",
"dataset": "FashionMNIST",
"epochs": 100,
"batch_size": 64,
}
wandb.init(
# set the wandb project where this run will be logged
project="simsiam-finetune",
# track hyperparameters and run metadata
config=wandb_config,
)
# Initialize model and optimizer
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
supervised = supervised_classification()
model_dict = supervised.state_dict()
simsiam_dict = {k: v for k, v in model_dict.items() if k in torch.load("simsiam.pt")}
supervised.load_state_dict(simsiam_dict, strict=False)
finetune_dataloader = DataLoader(finetune_dataset, batch_size=32, shuffle=True)
for param in supervised.backbone.parameters():
param.requires_grad = False
parameters = [para for para in supervised.parameters() if para.requires_grad]
optimizer = optim.SGD(parameters,
lr=wandb_config["learning_rate"],
momentum=0.9,
weight_decay=1e-5,
)
# Training loop
for epoch in range(wandb_config["epochs"]):
supervised.train()
train_loss = 0
for batch_idx, (image, target) in enumerate(tqdm.tqdm(finetune_dataloader)):
optimizer.zero_grad()
prediction = supervised(image)
loss = nn.CrossEntropyLoss()(prediction, target)
loss.backward()
optimizer.step()
wandb.log({"training loss": loss})
torch.save(supervised.state_dict(), "weights/supervised_with_simsiam.pt")
这是 SimSiam 预训练分类模型的评估结果。与仅使用监督学习的方法相比,平均 F1 分数提高了 15%。

SimSiam 模型在有限数据集上微调后的分类分数。图像来自作者。
总结。我们展示了一个简单但直观的例子,使用 FashionMNIST 进行对比学习。通过使用 SimSiam 进行骨干网络预训练,仅在有限的训练集(仅包含完整训练集的 2%标签)上微调线性层,我们将平均 F1 分数提高了 15%,超过了完全监督学习方法。训练好的权重、笔记本和自定义的 FashionMNIST 数据集类都包含在这个GitHub 仓库中。
试试看!
参考文献:
-
Chen 等,探索简单的 Siamese 表示学习。CVPR 2021。
-
Chen 等,视觉表示的对比学习简单框架。ICML 2020。
-
Chen 等,训练自监督视觉 Transformer 的经验研究。ICCV 2021。
-
Xiao 等,Fashion-MNIST:用于基准测试机器学习算法的新型图像数据集。arXiv 预印本 2017。Github:
github.com/zalandoresearch/fashion-mnist
《检索增强生成(RAG)实用指南》
基本技术如何用于构建强大的 LLM 应用程序…
·发布于 Towards Data Science ·27 分钟阅读·2024 年 3 月 26 日
--

(照片由 Matthew Dockery 提供,来自 Unsplash)
最近对生成式 AI 的兴趣激增,导致了许多可以用于解决各种任务的 AI 助手的出现,这些任务包括从 购物产品 到 搜索相关信息。所有这些有趣的应用都依赖于现代大语言模型(LLM)的进展,这些模型通过海量文本信息的训练,积累了庞大的知识库。然而,LLM 在检索和处理它们所拥有的知识方面有着臭名昭著的不足,这导致了像幻觉(即生成错误信息)、知识截止以及对专业领域理解较差等问题。我们能否有一种方法来提高 LLM 访问和利用高质量信息的能力?
“如果 AI 助手要在日常生活中发挥更有用的作用,它们不仅需要能够访问大量信息,更重要的是,能够访问正确的信息。” —— 来源
上述问题的答案是明确的“是”。在本概述中,我们将探讨将知识注入大语言模型(LLM)的一种最流行的技术——检索增强生成(RAG)…
亚马逊 SageMaker 训练作业的优先级调度器
优化有限的 AI 训练加速器的使用——第二部分
·发布于Towards Data Science ·阅读时间:12 分钟·2024 年 3 月 8 日
--

由Adrien Aletti拍摄,图片来源于Unsplash
本文与Max Rabin合作创作。
这是关于最大化稀缺 AI 资源利用的系列文章的第二部分。在第一篇文章中,我们提到,随着 AI 资源规模化能力的限制日益增加,AI 开发团队为保证 AI 计算能力,正逐渐倾向于通过构建内部 AI 服务器农场和/或在云中预留专用实例等方式来解决这一问题。AI 计算资源的稀缺性促使我们设计专门的调度解决方案,以最大程度地减少空闲时间,并优先处理关键工作负载。请参阅我们之前的文章,其中我们提出了此类解决方案的详细需求清单。我们采用的方法是利用现有的基于优先级的调度器,该调度器与Kubernetes一起提供,并使我们的训练开发工作流与其使用保持一致。在本文中,我们探讨了保持现有框架进行 AI 模型训练并通过自定义实现优先级调度器来增强该框架的选项。重要的是,这种类型的解决方案的需求通常不仅由 AI 资源的稀缺性驱动,还受到希望增加对训练工作负载的编排和优先级管理控制的推动,从而降低开发成本。例如,即使在容量充足的情况下,你也可能选择将训练实例的数量限制在一个固定范围内,以便控制训练开销。
本文假设我们选择的训练框架是 AWS 的 AI 模型训练托管服务Amazon SageMaker。我们将提出的解决方案将使用其他 AWS 服务,如Amazon DynamoDB和AWS Lambda。选择使用 AWS 服务来展示我们的解决方案不应被视为对其的推荐。市面上有许多基于云的服务可供选择,最适合您的解决方案将取决于您项目的具体细节。我们所描述的类似解决方案可以在其他云环境中设计,或使用其他云服务来实现。
启动 SageMaker 训练任务的传统方法
传统上,我们会使用Amazon SageMaker Python SDK启动 SageMaker 训练任务。在下面的代码块中,我们使用 SageMaker SDK(版本 2.208)在单个类型为p5.48xlarge的实例上运行 PyTorch 训练工作负载。
from sagemaker.pytorch import PyTorch
# define job
estimator = PyTorch(
role='<sagemaker role>',
entry_point='train.py',
instance_type='ml.p5.48xlarge',
instance_count=1,
framework_version='2.0.1',
py_version='py310',
tags=[{'Key': 'priority', 'Value': '100'}
)
# start job
estimator.fit()
当调用 estimator.fit() 函数时,SageMaker 库会将我们的代码上传到 Amazon S3,然后将请求转换为一个 boto3 SageMaker 客户端的create_training_job请求(请参见此处)。
启动训练作业的这种方法依赖于所请求资源的可用性。对于我们所面临的稀缺 AI 资源场景,成功的可能性往往较低。尽管通过为连续工作负载保留已配置的计算实例可以在一定程度上缓解这个问题,但 API 不提供适当的工具来最大化它们的效用。假设我们希望使用恰好两台p5.48xlarge实例。为了简化讨论,我们假设每个训练工作负载都在单个实例上运行。通常,在 AI 模型开发周期中,会有一些时间段,训练工作负载的数量超过了两台实例的处理能力。现有的 API 会尝试启动第三台p5.48xlarge实例,并且很可能因为其可用性有限而失败。即使存在实例可用性,我们也可能希望将训练限制在我们指定的两台实例上,以便更好地控制训练的成本。
我们需要一个新的 API 来提交训练作业,它不会立即启动新的p5.48xlarge实例,而是将作业放入优先级队列中。我们还需要一个关联的作业调度器来管理这两台资源的使用,同时优先处理关键工作负载。
需要特别注意的是,截至本文撰写时,Amazon SageMaker 不支持在预留 Amazon EC2 实例上进行训练。尽管Amazon SageMaker Savings Plans具有与实例预留相似的特性,但它不保证实例的容量。在上一篇文章中,我们讨论了这一限制,并建议使用由 SageMaker 管理的热池作为保留已配置实例访问权限的替代方法。在本文的其余部分,我们将假设无论是通过此方法还是其他方式,我们能够获得我们选择的两台实例。
基于优先级的调度对于 Amazon SageMaker
在本节中,我们将描述我们提出的解决方案的组件。我们将使用AWS 无服务器应用程序模型 (SAM) 规范。更具体地说,我们将创建一个AWS SAM 模板 YAML 文件,并逐步添加我们需要的 AWS 资源。有关如何使用 AWS SAM 定义和部署无服务器解决方案的详细信息,请参见文档。

AWS 架构图(作者提供)
提交训练作业的私有 API
我们首先使用Amazon API Gateway来定义一个私有 REST API,用于提交训练作业请求。我们将 API 命名为training-job-queue。稍后,我们将添加一个名为add-job的 POST 方法,并修改我们的训练作业创建代码,以使用该方法,而不是使用 SageMaker 客户端的create_training_job API。下面的代码块包含了在 SAM 中定义的私有API 资源。实际上,您可能希望为该 API 指定访问限制和/或授权方法。
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
InternalAPI:
Type: AWS::Serverless::Api
# Auth: # Add access control to API
EndpointConfiguration:
Type: PRIVATE
# VPCEndpointIds: # Specify VPC Endpoint(s)
Name: training-job-queue
StageName: prod
定义一个用于存储训练作业请求的 AWS DynamoDB 表
我们将使用一个名为sagemaker-queue的Amazon DynamoDB表来存储提交的训练工作负载。每个条目将包含以下字段:
-
jobName: 存储训练作业的唯一名称。
-
entryTime: 存储作业添加的日期和时间。
-
jobState: 存储训练作业的当前状态。有效值为“pending”、“running”和“preempted”。
-
priority: 存储表示作业相对优先级的整数值。
-
jobDetails: 存储作业请求的详细信息。
我们在 SAM 模板 YAML 文件中使用AWS::Serverless::SimpleTable资源定义我们的 DynamoDB 表。
DynamoSMQueue:
Type: AWS::Serverless::SimpleTable
Properties:
PrimaryKey:
Name: jobName
Type: String
TableName: sagemaker-queue
我们定义了一个函数,从给定的训练作业请求创建表项。我们假设请求包含与create_training_job API 的输入内容相同的 JSON 格式数据。我们进一步假设工作负载的优先级作为一个键值标签输入到训练作业定义中。
import json, boto3, datetime
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('sagemaker-queue')
def add_job_entry(job_json):
job_details = json.loads(job_json)
# extract job_name
job_name = job_details['TrainingJobName']
print(f'add entry {job_name}')
# get current time
entry_time = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
# default priority is 0
priority = 0
# update priority based on tags
tags = job_details['Tags']
for tag in tags:
if tag['Key'] == 'priority':
priority = int(tag['Value'])
break
# create entry
entry = {
'jobName': job_name,
'entryTime': entry_time,
'jobState': 'pending',
'priority': priority,
'jobDetails': job_json
}
table.put_item(Item=entry) #TODO handle errors
print(f'Added job {job_name} to queue')
我们即将定义的 REST API add-job 方法将被编程为调用 add_job_entry 函数。
我们定义了第二个函数,从数据库中提取待处理作业,并按优先级返回它们。如果多个作业具有相同的优先级,它们将根据在队列中等待的时间长短进行排序。
from boto3.dynamodb.conditions import Attr
# Get a list of all pending jobs sorted by priority
def get_pending_jobs():
response = table.scan(
ProjectionExpression='jobName, priority, entryTime',
FilterExpression=Attr('jobState').ne('running')
)
jobs = response.get('Items', [])
# sort jobs, first by priority (descending) and then by entryTime
sorted_jobs = sorted(jobs,
key=lambda x: (-x['priority'], x['entryTime']))
return sorted_jobs
以下实用功能将在接下来的章节中派上用场。
# Get a jobName -> priority mapping of all running jobs
def get_running_jobs_dict():
# Get all running jobs
response = table.scan(
ProjectionExpression="jobName, priority",
FilterExpression=Attr('jobState').eq('running')
)
jobs = response.get('Items', [])
running_jobs = {job['jobName']: job['priority'] for job in jobs}
return running_jobs
# Print the queue state
def print_queue_state():
response = table.scan(
ProjectionExpression='jobName, jobState, priority'
)
jobs = response.get('Items', [])
print_table = []
for job in jobs:
print_table.append([job['jobName'], job['jobState'], job['priority']])
# sort by priority
sorted_table = sorted(print_table,
key=lambda x: -x[2])
# Print the table
from tabulate import tabulate
print(tabulate(sorted_table, headers=['Job Name', 'State', 'Priority']))
# get job details
def get_job_details(job_name):
response = table.get_item(
Key={'jobName': job_name},
ProjectionExpression='jobDetails'
)
return json.loads(response.get('Item').get('jobDetails'))
# get job state or None if the job does not exist
def get_job_state(job_name):
response = table.get_item(
Key={'jobName': job_name},
ProjectionExpression='jobState'
)
job = response.get('Item')
return job.get('jobState') if job else None
# update the job state
def update_job_state(job_name, new_state):
table.update_item(
Key={'jobName': job_name},
UpdateExpression="SET jobState = :new_state",
ExpressionAttributeValues={":new_state": new_state}
)
print(f'Update job {job_name} to {new_state}')
# remove a job entry
def remove_job(job_name):
table.delete_item(
Key={'jobName': job_name}
)
print(f'Removed job {job_name} from queue')
我们选择 DynamoDB 及其使用(例如,使用Scan API 而不是Query API)假设队列中的作业总数最多为几十个。对于更大规模的解决方案,您可能需要选择一个更强大的数据库(例如,能够为您执行排序操作的数据库)或更复杂的 DynamoDB 使用方式(例如,参见这里)。
定义训练作业队列管理器
我们解决方案的主要组件是训练作业调度器。在这里,我们实现了一个相对简单的管理器,执行以下步骤:
-
提取按优先级排序的队列中作业列表。如果没有作业,返回。
-
发现未使用的实例容量。对于每个空闲实例,在 SageMaker 上启动一个待处理作业。如果没有剩余作业,返回。
-
计算处于停止状态的 SageMaker 作业数量。如果超过待处理作业的数量,返回。
-
通过将运行中的 SageMaker 作业的优先级与待处理作业的优先级进行比较,评估是否需要抢占正在运行的作业。
# set the limit on total number of instances/jobs
MAX_CAPACITY = 2
sagemaker = boto3.client('sagemaker')
# apply a queue stamp to identify that the job came from the queue
def apply_qstamp(job_name):
return f'{job_name}-qstamp-{datetime.now().strftime("%d%H%M")}'
# strip the queue stamp
def strip_qstamp(job_name):
return job_name.split('-qstamp-')[0]
# start a SageMaker job and update job entry in queue
def start_job(job_name):
print(f'start job {job_name}')
job_details = get_job_details(job_name)
job_details['TrainingJobName'] = apply_qstamp(job_name)
if(job_details):
# start job with detail from queue
# (you may optinally overwrite fields such as the iam role)
response = sagemaker.create_training_job(**job_details)
if response['ResponseMetadata']['HTTPStatusCode'] == 200:
print(f'started job {job_name}')
update_job_state(job_name, 'running')
# preempt a SageMaker job and update job entry in queue
def preempt_job(job_name):
print(f'preempt job {job_name}')
response = sagemaker.stop_training_job(TrainingJobName=job_name)
if response['ResponseMetadata']['HTTPStatusCode'] == 200:
print(f'preempted job {job_name}')
update_job_state(strip_qstamp(job_name), 'preempted')
# get SageMaker jobs
def get_sagemaker_jobs(status):
running = sagemaker.list_training_jobs(StatusEquals=status)
return running.get('TrainingJobSummaries', [])
# queue manager
def manage_queue():
# extract pending jobs to run
pending = get_pending_jobs()
if not pending:
return
if len(pending) > MAX_CAPACITY:
pending = pending[:MAX_CAPACITY]
# get running sagemaker jobs
running = get_sagemaker_jobs('InProgress')
total_running = len(running)
# get stopping sagemaker jobs
stopping = get_sagemaker_jobs('Stopping')
total_stopping = len(stopping)
# calculate the number of free instances
free_slots = MAX_CAPACITY - total_running - total_stopping
jobs_to_start = min(len(pending), free_slots)
# for each free instance, start a job
for i in range(jobs_to_start):
start_job(pending[i].get('jobName'))
still_pending = pending[jobs_to_start:]
if not still_pending:
return
# assume that 'total_stopping' number of jobs will start soon
test_for_preemption = len(still_pending) - total_stopping
if test_for_preemption <= 0:
return
# check if preemption is required
test_priority = still_pending[total_stopping:]
running_jobs = get_running_jobs_dict()
priority_dict = {}
for job in running:
job_name = job['TrainingJobName']
priority_dict[job_name] = running_jobs[strip_qstamp(job_name)]
# sort running jobs from lowest to highest priority
sorted_running = sorted(priority_dict.items(), key=lambda item: item[1])
index = 0
while index < test_for_preemption and \
test_priority[index].get('priority') > sorted_running[index][1]:
preempt_job(sorted_running[index][0])
index = index + 1
重要提示:
-
我们的实现非常乐观,假设所有插入的作业都是有效的,并且我们可以在 SageMaker 上顺利启动它们。实际上,应该添加适当的错误处理(例如,使用适当的日志记录从队列中移除有问题的作业)。
-
在生产环境中,我们需要考虑当多个并发事件触发我们的竞争条件时,可能会发生的情况。解决此问题的方法有多种(例如,参见这里),包括强制原子性(例如,通过将我们的Lambda 函数并发设置为 1)、使用某种形式的锁机制(例如,如这里所做的),或使我们的函数具有幂等性。在这里,我们采用了我们所称的“乐观幂等性”方法,依赖于 API 的适当使用以及底层调用 SageMaker API 的幂等性。
-
我们强调,我们的实现是初级的。实际上,我们建议使用更复杂的算法,1)考虑使用不同类型的实例和需要多个实例的任务,2)考虑所有边界情况,3)根据项目的具体需求量身定制。
定义 AWS Lambda 函数
解决方案的下一个组件是 Lambda 函数。以下代码块包括我们无服务器函数的SAM定义。我们编写该函数以响应两种不同类型的事件:对我们私有 API 网关上的add-job的任何调用,以及SageMaker 训练任务状态的变化。
ManagedTrainingJobQueue:
Type: AWS::Serverless::Function
Properties:
CodeUri: job-queue/ # the directory containing our index.py file
Handler: index.lambda_handler
Runtime: python3.12
Architectures:
- arm64 # use graviton
Policies: # allow access to SageMaker and DynamoDB
- !Sub "arn:${AWS::Partition}:iam::aws:policy/AmazonSageMakerFullAccess"
- DynamoDBCrudPolicy:
TableName: !Ref DynamoSMQueue
Events:
CreateTraining:
Type: Api
Properties:
Path: /add-job
Method: post
RestApiId: !Ref InternalAPI
SageMakerEvent:
Type: EventBridgeRule
Properties:
Pattern:
source:
- aws.sagemaker
detail-type:
- SageMaker Training Job State Change
detail:
TrainingJobStatus:
- "Completed"
- "Failed"
- "Stopped"
lambda_handler函数实现如下:
def lambda_handler(event, context):
# identify source of event and take appropriate action
if 'requestContext' in event and 'apiId' in event['requestContext']:
print('Lambda triggerred by API Gateway')
job_details = json.loads(event.get('body'))
add_job_entry(job_details)
elif 'source' in event and event['source'] == 'aws.sagemaker':
print('Lambda triggerred by SageMaker job state change')
job_name = event['detail']['TrainingJobName']
job_status = event['detail']['TrainingJobStatus']
print(f'{job_name} status changed to {job_status}')
# strip qstamp from job_name
job_name = strip_qstamp(job_name)
if job_status in ['Completed' , 'Failed']:
remove_job(job_name)
elif job_status == 'Stopped':
# check if it was manually stopped or preempted by queue manager
if get_job_state(job_name) == 'preempted':
print(f'job {job_name} preemption completed')
else:
print(f'job {job_name} {job_status}, remove from queue')
remove_job(job_name)
# in all cases invoke queue manager
manage_queue()
拦截创建训练任务请求
为了完成我们的解决方案,最后需要修改的部分是拦截对 SageMaker 的create_training_job API 的调用,并将其重新路由到我们的add-job方法。我们通过重写SageMaker 会话类的_intercept_create_request函数来实现这一点:
from sagemaker.pytorch import PyTorch
from sagemaker.session import Session
import requests, logging
logger = logging.getLogger('sagemaker')
def submit_to_training_queue(job):
logger.info(f'Adding training-job {job['TrainingJobName']} to queue')
logger.debug('train request: {json.dumps(job, indent=4)}')
vpce='<vpc endpoint>' # insert id of vpc endpoint
region='us-east-1' # specify region
url=f'https://{vpce}.execute-api.{region}.vpce.amazonaws.com/prod/add-job'
headers = {'x-apigw-api-id': '<api-id>'} # insert api gateway id
# submit job
response = requests.post(url, headers=headers, json=job)
class QueueTrainingJobSession(Session):
def _intercept_create_request(self, request, create, func_name = None):
"""This function intercepts the create job request
Args:
request (dict): the create job request
create (functor): a functor calls the sagemaker client create method
func_name (str): the name of the function needed intercepting
"""
if func_name == 'train':
submit_to_training_queue(request)
else:
super()._intercept_create_request(request,create,func_name)
# define job
estimator = PyTorch(
role='<sagemaker role>',
entry_point='train.py',
instance_type='ml.p5.48xlarge',
instance_count=1,
framework_version='2.0.1',
py_version='py310',
tags=[{'Key': 'priority', 'Value': '100'},
keep_alive_period_in_seconds=60, # keep warm for 1 minute
# use our custom Session class
sagemaker_session=QueueTrainingJobSession()
)
estimator.fit(wait=False)
用例示例
为了测试我们的解决方案,我们提交以下任务序列。每次调用后,我们打印队列状态(使用print_queue_state函数),并暂停 20 秒。
-
使用优先级 1 启动 job1。
-
使用优先级 2 启动 job2。
-
使用优先级 1 启动 job3。
-
使用优先级 3 启动 job4。
前两个作业会立即提交到 SageMaker,并更新为运行中状态。由于第三个作业的优先级较低,并且我们恰好有两个训练实例,它会保持在待处理状态,等待轮到它。提交前三个作业后,队列状态如下所示:
Job Name State Priority
---------- ------- ----------
job2 running 2
job1 running 1
job3 pending 1
我们提交的第四个作业优先级高于队列中所有作业。因此,优先级最低的正在运行作业job1会被抢占。相应的 SageMaker 作业被停止,一旦实例释放,队列状态变为:
Job Name State Priority
---------- --------- ----------
job4 running 3
job2 running 2
job1 preempted 1
job3 pending 1
正在运行的 SageMaker 作业job2是第一个完成的,job2从队列中移除,我们被抢占的作业恢复执行:
Job Name State Priority
---------- ------- ----------
job4 running 3
job1 running 1
job3 pending 1
一旦job4完成,它也会从队列中移除,为job3腾出位置。剩余的作业也会运行到完成,最终使我们的队列为空。
摘要
获取 AI 计算能力的难度不断增加,迫使 AI 开发团队重新评估他们用于训练 AI 模型的流程。我们在这篇文章中展示的方法是通过自定义优先级队列和关联的作业调度器,增强传统的模型训练 API。重要的是,我们提出的方案应该被视为一个通用方案,而不是一个生产级解决方案。为了满足您项目的具体需求,需要进行适当的修改和增强。
中心极限定理的证明

图片由作者提供
如果你喜欢巧克力,这个证明将像是一块 Mars 巧克力棒的多层魔力。
·发表于Towards Data Science ·阅读时间 24 分钟 ·2024 年 4 月 2 日
--
本质上,证明中心极限定理有两种方式。第一种是经验方法,第二种则运用了数学思维那种静谧而完美的精准性。我将在本文中介绍这两种方法。
在经验方法中,你将亲眼看到中心极限定理的运作,或者可以说是部分运作,甚至在你实验所创造的雪花球宇宙中完全无法奏效。经验方法并不是在证明中心极限定理,而是在验证它在给定数据上的有效性。
我将进行这个实验,但不会使用合成模拟数据。我将使用实际的物理物体——那些你可以用手指捡起,拿在眼前,甚至放进嘴里的物体。我将测试这个实验的结果是否符合正态分布。
理论方法是对中心极限定理的完整数学证明,它涉及到五个概念的堆叠:
-
泰勒定理(从中衍生出泰勒级数)
-
矩生成函数
-
泰勒级数
-
生成函数
-
无限序列和级数
提议的 Python 项目完美包原型
如何构建 Python 包项目的结构,以确保效率、效果和未来的可扩展性
·发布于 Towards Data Science ·14 分钟阅读·2024 年 6 月 18 日
--

图片由 Luke Heibert 提供,来源于 Unsplash
介绍
在设计 Python 包的结构时,有许多选项需要考虑,也有许多决策需要做出,但一旦这些设计选择确定下来,就很难做出根本性的结构变化。
此外,如果用户已经开始在他们的项目中嵌入命名空间,那么你所做的任何更改都意味着这些用户需要更改他们的源代码。
因此,在一开始就做出良好的设计和布局决策对于你的包来说至关重要。
问题
组织 Python 包有很多选项,因此找到合适的设计(至少是不会导致后续问题的设计)可能是一个挑战。
机遇
如果能够建立一个标准的包布局,解决所有挑战,并在一个骨架项目中进行原型设计,那么创建未来的包将变得非常快捷简单,可以直接复制粘贴。
前进的道路
《Python 工程师的 3D 高斯溅射入门(第一部分)》
从 Python 工程师的角度理解和编码高斯溅射
·发表于Towards Data Science ·阅读时长 7 分钟·2024 年 6 月 11 日
--

2023 年初,来自法国蔚蓝海岸大学和德国马克斯·普朗克计算机研究所的作者们发表了一篇题为《用于实时场景渲染的 3D 高斯溅射》的论文¹。该论文提出了一项在实时神经渲染领域的重要进展,超越了像 NeRF 这样的先前方法的实用性²。高斯溅射不仅减少了延迟,还在渲染质量上与 NeRF 相当,甚至超越了它,迅速在神经渲染领域引起了轰动。
高斯喷洒虽然有效,但对于那些不熟悉相机矩阵和图形渲染的人员来说,可能很难理解。此外,我发现 Python 中实现高斯喷洒的资源稀缺,因为即使是作者的源代码也是用 CUDA 编写的!本教程旨在弥补这一空白,为精通 Python 和机器学习但对图形渲染经验较少的工程师提供基于 Python 的高斯喷洒入门教程。随附的 GitHub 代码演示了如何初始化并渲染 COLMAP 扫描中的点,最终生成一个类似于喷洒应用中的前向传播图像(对于有兴趣的人,还包括一些 CUDA 代码)。本教程还提供了一个伴随的 Jupyter Notebook(part_1.ipynb 在 GitHub 上),其中包含了所有跟随教程所需的代码。虽然我们不会构建一个完整的高斯喷洒场景,但如果按步骤操作,本教程应能为读者提供深入了解喷洒技术的基础知识。
首先,我们使用 COLMAP,这是一款通过结构光从运动(SfM)提取在多张图像中一致出现的点的软件。SfM 本质上是通过匹配不同图像中的点(例如门框右上角的边缘),来确定每个点在 3D 空间中的深度。这与人类立体视觉的工作原理非常相似,通过比较每只眼睛稍有不同的视角来感知深度。因此,SfM 从多张图像中找到的共同点生成了一组具有 x、y 和 z 坐标的 3D 点,进而为我们提供了场景的“结构”。
在本教程中,我们将使用一个预构建的 COLMAP 扫描,该扫描可在 此处下载(Apache 2.0 许可证)。具体来说,我们将使用下载数据集中的 Treehill 文件夹。

图像以及从所有输入到 COLMAP 的图像中提取的所有点。请参阅下面的示例代码或 part_1.ipynb 文件以了解该过程。Apache 2.0 许可证。
该文件夹包含三个文件,分别对应相机参数、图像参数和实际的 3D 点。我们将从 3D 点开始。
点文件包含成千上万个 3D 点及其相关颜色。这些点围绕所谓的世界原点进行定位,基本上它们的 x、y 或 z 坐标是基于它们相对于这个世界原点被观察到的位置。世界原点的确切位置对于我们的目的并不重要,因此我们不会专注于它,因为它可以是空间中的任何一个任意点。相反,了解自己在世界中相对于该原点的位置才是至关重要的。这就是图像文件发挥作用的地方!
广义而言,图像文件告诉我们图像的拍摄位置以及相机的朝向,都是相对于世界原点的。因此,我们关心的关键参数是四元数向量和平移向量。四元数向量使用 4 个不同的浮动值描述相机在空间中的旋转,这些值可以用来形成一个旋转矩阵(3Blue1Brown 有一个很好的视频解释四元数的概念,观看视频)。然后,平移向量告诉我们相机相对于原点的位置。这些参数共同构成外参矩阵,其中四元数值用于计算一个 3x3 的旋转矩阵(公式),平移向量则附加到该矩阵上。

典型的“外参”矩阵。通过结合一个 3x3 的旋转矩阵和一个 3x1 的平移向量,我们能够将坐标从世界坐标系转换到相机坐标系。图片来源:作者。
外参矩阵将点从世界空间(点文件中的坐标)转换到相机空间,使得相机成为世界的新中心。例如,如果相机在 y 方向上移动了 2 个单位而没有任何旋转,我们只需从所有点的 y 坐标中减去 2 个单位,以便获得在新坐标系中的点。
当我们将坐标从世界空间转换到相机空间时,依然得到一个三维向量,其中 z 坐标表示相机视图中的深度。这个深度信息对于确定光点的顺序至关重要,后续渲染时需要用到。
我们通过解释相机参数文件来结束对 COLMAP 的讨论。相机文件提供了如高度、宽度、焦距(x 和 y 方向)以及偏移量(x 和 y)等参数。通过这些参数,我们可以构建内参矩阵,它表示 x 和 y 方向的焦距以及主点坐标。
如果你对相机矩阵完全陌生,我推荐你参考 Shree Nayar 讲授的计算机视觉基础课程,特别是关于针孔和透视投影的讲座,然后是关于内外参数矩阵的讲座。

典型的内参矩阵。表示 x 和 y 方向的焦距以及主点坐标。图片来源:作者。
内参矩阵用于将点从相机坐标(通过外参矩阵获得)转换到二维图像平面,也就是你所看到的“图像”。单独的相机坐标不能表明其在图像中的外观,因为必须反映深度信息才能准确评估相机看到的内容。
为了将 COLMAP 点转换为 2D 图像,我们首先使用外参矩阵将它们投影到相机坐标,然后再使用内参矩阵将它们投影到 2D。然而,一个重要的细节是,在此过程中我们使用了齐次坐标。外参矩阵是 4x4 的,而输入点是 3x1 的,因此我们需要在输入点上加一个 1,将它们变为 4x1。
这是逐步的过程:
-
将点转换到相机坐标:将 4x4 的外参矩阵与 4x1 的点向量相乘。
-
转换为图像坐标:将 3x4 的内参矩阵与得到的 4x1 向量相乘。
这将得到一个 3x1 的矩阵。为了获得最终的 2D 坐标,我们将这个 3x1 矩阵的第三个坐标除以它,得到图像中的 x 和 y 坐标!你可以看到图像编号 100 应该是什么样子,下面是复制结果的代码。
def get_intrinsic_matrix(
f_x: float, f_y: float, c_x: float, c_y: float
) -> torch.Tensor:
"""
Get the homogenous intrinsic matrix for the camera
"""
return torch.Tensor(
[
[f_x, 0, c_x, 0],
[0, f_y, c_y, 0],
[0, 0, 1, 0],
]
)
def get_extrinsic_matrix(R: torch.Tensor, t: torch.Tensor) -> torch.Tensor:
"""
Get the homogenous extrinsic matrix for the camera
"""
Rt = torch.zeros((4, 4))
Rt[:3, :3] = R
Rt[:3, 3] = t
Rt[3, 3] = 1.0
return Rt
def project_points(
points: torch.Tensor, intrinsic_matrix: torch.Tensor, extrinsic_matrix: torch.Tensor
) -> torch.Tensor:
"""
Project the points to the image plane
Args:
points: Nx3 tensor
intrinsic_matrix: 3x4 tensor
extrinsic_matrix: 4x4 tensor
"""
homogeneous = torch.ones((4, points.shape[0]), device=points.device)
homogeneous[:3, :] = points.T
projected_to_camera_perspective = extrinsic_matrix @ homogeneous
projected_to_image_plane = (intrinsic_matrix @ projected_to_camera_perspective).T # Nx4
x = projected_to_image_plane[:, 0] / projected_to_image_plane[:, 2]
y = projected_to_image_plane[:, 1] / projected_to_image_plane[:, 2]
return x, y
colmap_path = "treehill/sparse/0"
reconstruction = pycolmap.Reconstruction(colmap_path)
points3d = reconstruction.points3D
images = read_images_binary(f"{colmap_path}/images.bin")
cameras = reconstruction.cameras
all_points3d = []
all_point_colors = []
for idx, point in enumerate(points3d.values()):
if point.track.length() >= 2:
all_points3d.append(point.xyz)
all_point_colors.append(point.color)
gaussians = Gaussians(
torch.Tensor(all_points3d),
torch.Tensor(all_point_colors),
model_path="point_clouds"
)
# we will examine the 100th image
image_num = 100
image_dict = read_image_file(colmap_path)
camera_dict = read_camera_file(colmap_path)
# convert quaternion to rotation matrix
rotation_matrix = build_rotation(torch.Tensor(image_dict[image_num].qvec).unsqueeze(0))
translation = torch.Tensor(image_dict[image_num].tvec).unsqueeze(0)
extrinsic_matrix = get_extrinsic_matrix(
rotation_matrix, translation
)
focal_x, focal_y = camera_dict[image_dict[image_num].camera_id].params[:2]
c_x, c_y = camera_dict[image_dict[image_num].camera_id].params[2:4]
intrinsic_matrix = get_intrinsic_matrix(focal_x, focal_y, c_x, c_y)
points = project_points(gaussians.points, intrinsic_matrix, extrinsic_matrix)
复习一下,现在我们可以通过任何一组 3D 点,利用我们需要的各种位置和相机参数,计算它们在 2D 图像平面上出现的位置!有了这些,我们可以继续理解第二部分中高斯溅射的“高斯”部分。
-
Kerbl, Bernhard, 等. “实时辐射场渲染的 3D 高斯溅射。” ACM 图形学学报 42.4(2023):1–14。
-
Mildenhall, Ben, 等. “Nerf:将场景表示为神经辐射场进行视图合成。” ACM 通讯 65.1(2021):99–106。
-
Snavely, Noah, Steven M. Seitz, 和 Richard Szeliski. “照片旅游:在 3D 中探索照片集合。” ACM siggraph 2006 论文集,2006,835–846。
-
Barron, Jonathan T., 等. “Mip-nerf 360: 无边界抗锯齿神经辐射场。” IEEE/CVF 计算机视觉与模式识别会议论文集,2022。
Python 工程师的 3D 高斯点洒射入门(第二部分)
理解并编码高斯在 3D 高斯点洒射中的应用
·发布于 Towards Data Science ·8 分钟阅读·2024 年 6 月 13 日
--
现在来讲讲高斯分布!每个人最喜欢的分布。如果你是刚加入我们的读者,我们在第一部分中讨论了如何根据相机位置将三维点转换为二维。本文将继续探讨高斯点洒射中的高斯部分。我们将在我们的GitHub中使用 part_2.ipynb 文件。
我们在这里做的一个小改动是使用不同于上一篇文章中所示的内部矩阵的透视投影矩阵。然而,这两者在将点投影到二维时是等效的,我个人认为第一部分介绍的方法更容易理解,不过我们更改了方法,以便在 Python 中尽可能地复制作者的代码。具体来说,我们的“内部”矩阵将由这里展示的 OpenGL 投影矩阵给出,乘法的顺序将变为 points @ external.transpose() @ internal。

内部透视投影矩阵。参数将在下面的段落中进行解释。
对于那些对这个新内部矩阵感到好奇的人(否则可以跳过这一段),r 和 l 是左右裁剪平面,本质上是指根据照片的宽度,哪些点可能在视图中,t 和 b 是上下裁剪平面。N 是近裁剪平面(即点将被投影到的位置),而 f 是远裁剪平面。想要了解更多信息的人,我发现 scratchapixel 上的章节内容非常有参考价值(www.scratchapixel.com/lessons/3d-basic-rendering/perspective-and-orthographic-projection-matrix/opengl-perspective-projection-matrix.html)。这也返回了标准化设备坐标(在-1 和 1 之间),然后我们将其投影到像素坐标。题外话暂且不提,任务依旧是相同的,将 3D 点投影到 2D 图像平面。然而,在本教程的这一部分,我们现在使用的是高斯点而不是点。
def getIntinsicMatrix(
focal_x: torch.Tensor,
focal_y: torch.Tensor,
height: torch.Tensor,
width: torch.Tensor,
znear: torch.Tensor = torch.Tensor([100.0]),
zfar: torch.Tensor = torch.Tensor([0.001]),,
) -> torch.Tensor:
"""
Gets the internal perspective projection matrix
znear: near plane set by user
zfar: far plane set by user
fovX: field of view in x, calculated from the focal length
fovY: field of view in y, calculated from the focal length
"""
fovX = torch.Tensor([2 * math.atan(width / (2 * focal_x))])
fovY = torch.Tensor([2 * math.atan(height / (2 * focal_y))])
tanHalfFovY = math.tan((fovY / 2))
tanHalfFovX = math.tan((fovX / 2))
top = tanHalfFovY * znear
bottom = -top
right = tanHalfFovX * znear
left = -right
P = torch.zeros(4, 4)
z_sign = 1.0
P[0, 0] = 2.0 * znear / (right - left)
P[1, 1] = 2.0 * znear / (top - bottom)
P[0, 2] = (right + left) / (right - left)
P[1, 2] = (top + bottom) / (top - bottom)
P[3, 2] = z_sign
P[2, 2] = z_sign * zfar / (zfar - znear)
P[2, 3] = -(zfar * znear) / (zfar - znear)
return P
一个 3D 高斯点由 x、y 和 z 坐标以及相关的协方差矩阵组成。正如作者所指出的:“一种显而易见的方法是直接优化协方差矩阵Σ,以获得表示辐射场的 3D 高斯。但协方差矩阵只有在它们是正半定时才具有物理意义。由于我们对所有参数的优化使用的是梯度下降方法,而梯度下降方法无法轻松约束以生成这样的有效矩阵,因此更新步骤和梯度非常容易产生无效的协方差矩阵。”¹
因此,作者使用了协方差矩阵的分解方法,该方法始终产生正半定的协方差矩阵。特别地,他们使用了 3 个“尺度”参数和 4 个四元数,这些四元数被转换为 3x3 的旋转矩阵(R)。然后,协方差矩阵由下式给出:

协方差矩阵的方程,其中 R 表示从四元数导出的 3x3 旋转矩阵,S 是 3 个尺度参数。图片由作者提供。
注意,在将四元数转换为旋转矩阵之前,必须对四元数向量进行归一化,以获得有效的旋转矩阵。因此,在我们的实现中,一个高斯点由以下参数组成:坐标(3x1 向量)、四元数(4x1 向量)、尺度(3x1 向量)和一个最终的浮动值,表示不透明度(即该高斯点的透明度)。现在,我们只需要优化这 11 个参数来得到我们的场景——听起来很简单,对吧!
事实证明,事情比这更复杂。如果你记得高中数学,某一点上高斯函数的强度由以下方程给出:

某一点 x 处的高斯强度由均值(mu)和协方差矩阵的逆给出。图片由作者提供。
然而,我们关心的是 3D 高斯在 2D 中的强度,也就是在图像平面上的强度。但你可能会说,我们知道如何将点投影到 2D!尽管如此,我们还没有讨论如何将协方差矩阵投影到 2D,因此如果我们还没有找到 2D 协方差矩阵,显然无法求得 2D 协方差矩阵的逆矩阵。
现在是有趣的部分(取决于你怎么看)。《EWA Splatting》这篇由 3D 高斯喷溅的作者引用的论文,准确展示了如何将 3D 协方差矩阵投影到 2D²。然而,这假设了你已经掌握了雅可比仿射变换矩阵的知识,下面我们将进行计算。我发现代码在理解复杂概念时非常有帮助,因此我提供了一些代码示例,以说明如何从 3D 协方差矩阵转到 2D。
def compute_2d_covariance(
points: torch.Tensor,
external_matrix: torch.Tensor,
covariance_3d: torch.Tensor,
tan_fovY: torch.Tensor,
tan_fovX: torch.Tensor,
focal_x: torch.Tensor,
focal_y: torch.Tensor,
) -> torch.Tensor:
"""
Compute the 2D covariance matrix for each gaussian
"""
points = torch.cat(
[points, torch.ones(points.shape[0], 1, device=points.device)], dim=1
)
points_transformed = (points @ external_matrix)[:, :3]
limx = 1.3 * tan_fovX
limy = 1.3 * tan_fovY
x = points_transformed[:, 0] / points_transformed[:, 2]
y = points_transformed[:, 1] / points_transformed[:, 2]
z = points_transformed[:, 2]
x = torch.clamp(x, -limx, limx) * z
y = torch.clamp(y, -limy, limy) * z
J = torch.zeros((points_transformed.shape[0], 3, 3), device=covariance_3d.device)
J[:, 0, 0] = focal_x / z
J[:, 0, 2] = -(focal_x * x) / (z**2)
J[:, 1, 1] = focal_y / z
J[:, 1, 2] = -(focal_y * y) / (z**2)
# transpose as originally set up for perspective projection
# so we now transform back
W = external_matrix[:3, :3].T
return (J @ W @ covariance_3d @ W.T @ J.transpose(1, 2))[:, :2, :2]
首先,tan_fovY 和 tan_fovX 是视场角一半的正切值。我们使用这些值来限制我们的投影,防止任何过度的、屏幕外的投影影响我们的渲染。可以从 3D 到 2D 的变换中推导出雅可比矩阵,这个变换在第一部分中介绍过,但我已经为你省去了麻烦,展示了预期的推导过程。最后,如果你记得我们在上面转置了旋转矩阵,以便重排项,因此在倒数第二行进行转置后,我们会返回最终的协方差计算。正如 EWA 喷溅论文中所指出的,由于我们只关心 2D 图像平面,可以忽略第三行和第三列。你可能会问,为什么一开始不能这么做?嗯,协方差矩阵的参数会根据你观察的角度而变化,因为在大多数情况下它不会是完美的球体!现在我们已经转换到正确的视角,协方差矩阵中的 z 轴信息就变得无用,可以丢弃。
鉴于我们已经有了 2D 协方差矩阵,我们距离能够计算每个高斯对图像中任意像素的影响已经很接近,我们只需要找到反转的协方差矩阵。再回顾一下线性代数,要找到 2x2 矩阵的逆矩阵,你只需要找到行列式,然后进行一些项的重排。这里有一些代码,可以帮助你引导这一过程。
def compute_inverted_covariance(covariance_2d: torch.Tensor) -> torch.Tensor:
"""
Compute the inverse covariance matrix
For a 2x2 matrix
given as
[[a, b],
[c, d]]
the determinant is ad - bc
To get the inverse matrix reshuffle the terms like so
and multiply by 1/determinant
[[d, -b],
[-c, a]] * (1 / determinant)
"""
determinant = (
covariance_2d[:, 0, 0] * covariance_2d[:, 1, 1]
- covariance_2d[:, 0, 1] * covariance_2d[:, 1, 0]
)
determinant = torch.clamp(determinant, min=1e-3)
inverse_covariance = torch.zeros_like(covariance_2d)
inverse_covariance[:, 0, 0] = covariance_2d[:, 1, 1] / determinant
inverse_covariance[:, 1, 1] = covariance_2d[:, 0, 0] / determinant
inverse_covariance[:, 0, 1] = -covariance_2d[:, 0, 1] / determinant
inverse_covariance[:, 1, 0] = -covariance_2d[:, 1, 0] / determinant
return inverse_covariance
哇,现在我们可以计算图像中每一个像素的强度了。然而,这样做非常慢,而且不必要。例如,我们真的不需要浪费计算能力去搞清楚一个位于(0,0)的溅射如何影响一个位于(1000, 1000)的像素,除非协方差矩阵非常庞大。因此,作者选择计算他们所称的“半径”来表示每个溅射的影响。如下代码所示,我们计算沿每个轴的特征值(记住,特征值表示变动)。然后,我们取最大特征值的平方根,得到一个标准差度量,并将其乘以 3.0,这样可以覆盖 99.7%的分布范围,位于 3 个标准差内。这个半径帮助我们确定溅射所涉及的最小和最大 x、y 值。在渲染时,我们仅计算位于这些范围内的像素的溅射强度,节省了大量不必要的计算。聪明吧?
def compute_extent_and_radius(covariance_2d: torch.Tensor):
mid = 0.5 * (covariance_2d[:, 0, 0] + covariance_2d[:, 1, 1])
det = covariance_2d[:, 0, 0] * covariance_2d[:, 1, 1] - covariance_2d[:, 0, 1] ** 2
intermediate_matrix = (mid * mid - det).view(-1, 1)
intermediate_matrix = torch.cat(
[intermediate_matrix, torch.ones_like(intermediate_matrix) * 0.1], dim=1
)
max_values = torch.max(intermediate_matrix, dim=1).values
lambda1 = mid + torch.sqrt(max_values)
lambda2 = mid - torch.sqrt(max_values)
# now we have the eigenvalues, we can calculate the max radius
max_radius = torch.ceil(3.0 * torch.sqrt(torch.max(lambda1, lambda2)))
return max_radius
上述所有步骤为我们提供了预处理过的场景,可以在渲染步骤中使用。回顾一下,我们现在拥有了二维中的点、与这些点相关的颜色、二维中的协方差、二维中的逆协方差、排序后的深度顺序、每个溅射的最小 x、最小 y、最大 x、最大 y 值,以及相关的不透明度。有了这些组件,我们终于可以开始渲染图像了!
-
Kerbl, Bernhard, 等人. “用于实时辐射场渲染的 3D 高斯溅射.” ACM 图形学学报 42.4 (2023): 1–14.
-
Zwicker, Matthias, 等人. “EWA 溅射.” IEEE 计算机视觉与图形学学报 8.3 (2002): 223–238.
一位 Python 工程师的 3D 高斯喷溅入门(第三部分)
高斯喷溅教程的第三部分,展示如何将喷溅渲染到 2D 图像上
·发表于 Towards Data Science ·8 分钟阅读·2024 年 7 月 18 日
--
最后,我们进入了高斯喷溅过程中的最有趣的阶段:渲染!这一步可以说是最关键的,因为它决定了我们模型的真实感。然而,它也可能是最简单的。在我们系列的第一部分和第二部分中,我们展示了如何将原始的喷溅数据转换为可以渲染的格式,但现在我们实际上要做的是渲染工作,将其绘制到固定的像素集上。作者们使用 CUDA 开发了一个快速渲染引擎,虽然这个引擎有些难以跟随。因此,我认为首先通过 Python 代码讲解,使用简单的for循环来保持清晰,是非常有益的。对于那些渴望深入了解的人,所有必要的代码都可以在我们的GitHub上找到。
让我们讨论如何渲染每一个独立的像素。从我们之前的文章中,我们已经具备了所有必要的组件:2D 点、关联的颜色、协方差、排序后的深度顺序、2D 逆协方差、每个溅射的最小和最大 x 和 y 值,以及关联的不透明度。借助这些组件,我们可以渲染任何像素。给定特定的像素坐标,我们会遍历所有的溅射,直到达到饱和度阈值,按照溅射的深度顺序相对于相机平面(先投影到相机平面,然后按深度排序)。对于每个溅射,我们首先检查像素坐标是否在由最小和最大 x 和 y 值定义的范围内。这个检查决定了我们是继续渲染还是忽略这些坐标的溅射。接下来,我们使用溅射的均值、溅射的协方差和像素坐标来计算该像素位置的高斯溅射强度。
def compute_gaussian_weight(
pixel_coord: torch.Tensor, # (1, 2) tensor
point_mean: torch.Tensor,
inverse_covariance: torch.Tensor,
) -> torch.Tensor:
difference = point_mean - pixel_coord
power = -0.5 * difference @ inverse_covariance @ difference.T
return torch.exp(power).item()
我们将这个权重与溅射的不透明度相乘,以获得一个名为 alpha 的参数。在将这个新值添加到像素之前,我们需要检查是否超过了我们的饱和度阈值。如果像素已经饱和,我们不希望其他位于后面的溅射影响像素的着色并浪费计算资源。因此,我们使用一个阈值来确保一旦超过这个阈值,我们就停止渲染。实际上,我们将饱和度阈值从 1 开始,然后将其与 min(0.99, (1 — alpha)) 相乘得到一个新值。如果该值小于我们的阈值(0.0001),我们就停止渲染这个像素,并认为它已经完成。如果不是,我们将按饱和度 * (1 — alpha) 的值加权颜色,并更新饱和度为 new_saturation = old_saturation * (1 — alpha)。最后,我们遍历每一个像素(或者实际中是每个 16x16 的瓦片)进行渲染。完整的代码如下所示。
def render_pixel(
self,
pixel_coords: torch.Tensor,
points_in_tile_mean: torch.Tensor,
colors: torch.Tensor,
opacities: torch.Tensor,
inverse_covariance: torch.Tensor,
min_weight: float = 0.000001,
) -> torch.Tensor:
total_weight = torch.ones(1).to(points_in_tile_mean.device)
pixel_color = torch.zeros((1, 1, 3)).to(points_in_tile_mean.device)
for point_idx in range(points_in_tile_mean.shape[0]):
point = points_in_tile_mean[point_idx, :].view(1, 2)
weight = compute_gaussian_weight(
pixel_coord=pixel_coords,
point_mean=point,
inverse_covariance=inverse_covariance[point_idx],
)
alpha = weight * torch.sigmoid(opacities[point_idx])
test_weight = total_weight * (1 - alpha)
if test_weight < min_weight:
return pixel_color
pixel_color += total_weight * alpha * colors[point_idx]
total_weight = test_weight
# in case we never reach saturation
return pixel_color
现在我们可以渲染一个像素,那么我们也可以渲染一张图像的一部分,或者说是作者所称的一个瓦片!
def render_tile(
self,
x_min: int,
y_min: int,
points_in_tile_mean: torch.Tensor,
colors: torch.Tensor,
opacities: torch.Tensor,
inverse_covariance: torch.Tensor,
tile_size: int = 16,
) -> torch.Tensor:
"""Points in tile should be arranged in order of depth"""
tile = torch.zeros((tile_size, tile_size, 3))
# iterate by tiles for more efficient processing
for pixel_x in range(x_min, x_min + tile_size):
for pixel_y in range(y_min, y_min + tile_size):
tile[pixel_x % tile_size, pixel_y % tile_size] = self.render_pixel(
pixel_coords=torch.Tensor([pixel_x, pixel_y])
.view(1, 2)
.to(points_in_tile_mean.device),
points_in_tile_mean=points_in_tile_mean,
colors=colors,
opacities=opacities,
inverse_covariance=inverse_covariance,
)
return tile
最后,我们可以使用所有这些瓦片来渲染整个图像。注意我们如何检查确保溅射实际上会影响当前瓦片(x_in_tile 和 y_in_tile 代码)。
def render_image(self, image_idx: int, tile_size: int = 16) -> torch.Tensor:
"""For each tile have to check if the point is in the tile"""
preprocessed_scene = self.preprocess(image_idx)
height = self.images[image_idx].height
width = self.images[image_idx].width
image = torch.zeros((width, height, 3))
for x_min in tqdm(range(0, width, tile_size)):
x_in_tile = (x_min >= preprocessed_scene.min_x) & (
x_min + tile_size <= preprocessed_scene.max_x
)
if x_in_tile.sum() == 0:
continue
for y_min in range(0, height, tile_size):
y_in_tile = (y_min >= preprocessed_scene.min_y) & (
y_min + tile_size <= preprocessed_scene.max_y
)
points_in_tile = x_in_tile & y_in_tile
if points_in_tile.sum() == 0:
continue
points_in_tile_mean = preprocessed_scene.points[points_in_tile]
colors_in_tile = preprocessed_scene.colors[points_in_tile]
opacities_in_tile = preprocessed_scene.sigmoid_opacity[points_in_tile]
inverse_covariance_in_tile = preprocessed_scene.inverse_covariance_2d[
points_in_tile
]
image[x_min : x_min + tile_size, y_min : y_min + tile_size] = (
self.render_tile(
x_min=x_min,
y_min=y_min,
points_in_tile_mean=points_in_tile_mean,
colors=colors_in_tile,
opacities=opacities_in_tile,
inverse_covariance=inverse_covariance_in_tile,
tile_size=tile_size,
)
)
return image
最终,既然我们已经具备了所有必要的组件,我们就可以渲染图像了。我们从 treehill 数据集中获取所有的 3D 点,并将它们初始化为高斯溅射。为了避免代价高昂的最近邻搜索,我们将所有的尺度变量初始化为 0.01(注意,使用如此小的方差时,我们需要在一个位置上有较强的溅射集中才能使其可见。较大的方差会使得过程变得相当缓慢)。然后我们所需要做的就是调用 render_image,传入我们尝试模拟的图像编号,正如你所看到的,我们得到了一个稀疏的点云集,看起来像我们的图像!(查看底部的附加部分,了解使用 pyTorch 的便捷工具编译 CUDA 代码的等效 CUDA 内核!)

实际图像,CPU 实现,CUDA 实现。图片来自作者。
虽然反向传递不在本教程的内容中,但有一点需要提到的是,尽管我们一开始只有这些点,但在大多数场景中,我们很快会有数十万个 splats。这是由于将大 splats(由较大的轴方差定义)分解为更小的 splats,并移除具有极低透明度的 splats。例如,如果我们真的将缩放初始化为三个最邻近点的均值,我们将覆盖大部分空间。为了获得精细的细节,我们需要将这些 splats 分解成更小的 splats,以便捕捉精细的细节。它们还需要填充那些高斯分布稀少的区域。它们将这两种情况称为过度重建和欠重建,并通过各种 splats 的大梯度值来定义这两种情况。然后根据 splats 的大小拆分或克隆它们(见下图),并继续优化过程。
尽管反向传递不在本教程的范围内,但需要注意的是,虽然我们一开始只有几个点,但很快在大多数场景中会有数十万个 splats。这一增加是由于将大 splats(在轴上具有较大方差)拆分成更小的 splats,并移除透明度非常低的 splats。例如,如果我们最初将缩放设置为三个最近邻点的均值,大部分空间会被覆盖。为了实现更细致的细节,我们需要将这些大 splats 分解成更小的 splats。此外,少数高斯分布的区域也需要填充。这些场景被称为过度重建和欠重建,特征是各种 splats 的大梯度值。根据它们的大小,splats 会被拆分或克隆(见下图),优化过程继续进行。

来自作者原始论文,讨论在训练过程中如何对高斯分布进行拆分或克隆。来源:arxiv.org/abs/2308.04079
这就是高斯 Splatting 的简单介绍!你现在应该对高斯场景渲染的前向传递过程有了很好的直觉。虽然它有点复杂,并不完全是神经网络,但只需一点线性代数,我们就能在 2D 中渲染 3D 几何!
如果你对某些内容有疑问,或者我哪里弄错了,随时可以在评论区留言,你也可以通过 LinkedIn 或 Twitter 与我联系!
奖金 — CUDA 代码
使用 PyTorch 的 CUDA 编译器编写自定义 CUDA 核心!
def load_cuda(cuda_src, cpp_src, funcs, opt=True, verbose=False):
return load_inline(
name="inline_ext",
cpp_sources=[cpp_src],
cuda_sources=[cuda_src],
functions=funcs,
extra_cuda_cflags=["-O1"] if opt else [],
verbose=verbose,
)
class GaussianScene(nn.Module):
# OTHER CODE NOT SHOWN
def compile_cuda_ext(
self,
) -> torch.jit.ScriptModule:
cpp_src = """
torch::Tensor render_image(
int image_height,
int image_width,
int tile_size,
torch::Tensor point_means,
torch::Tensor point_colors,
torch::Tensor inverse_covariance_2d,
torch::Tensor min_x,
torch::Tensor max_x,
torch::Tensor min_y,
torch::Tensor max_y,
torch::Tensor opacity);
"""
cuda_src = Path("splat/c/render.cu").read_text()
return load_cuda(cuda_src, cpp_src, ["render_image"], opt=True, verbose=True)
def render_image_cuda(self, image_idx: int, tile_size: int = 16) -> torch.Tensor:
preprocessed_scene = self.preprocess(image_idx)
height = self.images[image_idx].height
width = self.images[image_idx].width
ext = self.compile_cuda_ext()
now = time.time()
image = ext.render_image(
height,
width,
tile_size,
preprocessed_scene.points.contiguous(),
preprocessed_scene.colors.contiguous(),
preprocessed_scene.inverse_covariance_2d.contiguous(),
preprocessed_scene.min_x.contiguous(),
preprocessed_scene.max_x.contiguous(),
preprocessed_scene.min_y.contiguous(),
preprocessed_scene.max_y.contiguous(),
preprocessed_scene.sigmoid_opacity.contiguous(),
)
torch.cuda.synchronize()
print("Operation took seconds: ", time.time() - now)
return image
#include <cstdio>
#include <cmath> // Include this header for expf function
#include <torch/extension.h>
__device__ float compute_pixel_strength(
int pixel_x,
int pixel_y,
int point_x,
int point_y,
float inverse_covariance_a,
float inverse_covariance_b,
float inverse_covariance_c)
{
// Compute the distance between the pixel and the point
float dx = pixel_x - point_x;
float dy = pixel_y - point_y;
float power = dx * inverse_covariance_a * dx + 2 * dx * dy * inverse_covariance_b + dy * dy * inverse_covariance_c;
return expf(-0.5f * power);
}
__global__ void render_tile(
int image_height,
int image_width,
int tile_size,
int num_points,
float *point_means,
float *point_colors,
float *image,
float *inverse_covariance_2d,
float *min_x,
float *max_x,
float *min_y,
float *max_y,
float *opacity)
{
// Calculate the pixel's position in the image
int pixel_x = blockIdx.x * tile_size + threadIdx.x;
int pixel_y = blockIdx.y * tile_size + threadIdx.y;
// Ensure the pixel is within the image bounds
if (pixel_x >= image_width || pixel_y >= image_height)
{
return;
}
float total_weight = 1.0f;
float3 color = {0.0f, 0.0f, 0.0f};
for (int i = 0; i < num_points; i++)
{
float point_x = point_means[i * 2];
float point_y = point_means[i * 2 + 1];
// checks to make sure we are within the bounding box
bool x_check = pixel_x >= min_x[i] && pixel_x <= max_x[i];
bool y_check = pixel_y >= min_y[i] && pixel_y <= max_y[i];
if (!x_check || !y_check)
{
continue;
}
float strength = compute_pixel_strength(
pixel_x,
pixel_y,
point_x,
point_y,
inverse_covariance_2d[i * 4],
inverse_covariance_2d[i * 4 + 1],
inverse_covariance_2d[i * 4 + 3]);
float initial_alpha = opacity[i] * strength;
float alpha = min(.99f, initial_alpha);
float test_weight = total_weight * (1 - alpha);
if (test_weight < 0.001f)
{
break;
}
color.x += total_weight * alpha * point_colors[i * 3];
color.y += total_weight * alpha * point_colors[i * 3 + 1];
color.z += total_weight * alpha * point_colors[i * 3 + 2];
total_weight = test_weight;
}
image[(pixel_y * image_width + pixel_x) * 3] = color.x;
image[(pixel_y * image_width + pixel_x) * 3 + 1] = color.y;
image[(pixel_y * image_width + pixel_x) * 3 + 2] = color.z;
}
torch::Tensor render_image(
int image_height,
int image_width,
int tile_size,
torch::Tensor point_means,
torch::Tensor point_colors,
torch::Tensor inverse_covariance_2d,
torch::Tensor min_x,
torch::Tensor max_x,
torch::Tensor min_y,
torch::Tensor max_y,
torch::Tensor opacity)
{
// Ensure the input tensors are on the same device
torch::TensorArg point_means_t{point_means, "point_means", 1},
point_colors_t{point_colors, "point_colors", 2},
inverse_covariance_2d_t{inverse_covariance_2d, "inverse_covariance_2d", 3},
min_x_t{min_x, "min_x", 4},
max_x_t{max_x, "max_x", 5},
min_y_t{min_y, "min_y", 6},
max_y_t{max_y, "max_y", 7},
opacity_t{opacity, "opacity", 8};
torch::checkAllSameGPU("render_image", {point_means_t, point_colors_t, inverse_covariance_2d_t, min_x_t, max_x_t, min_y_t, max_y_t, opacity_t});
// Create an output tensor for the image
torch::Tensor image = torch::zeros({image_height, image_width, 3}, point_means.options());
// Calculate the number of tiles in the image
int num_tiles_x = (image_width + tile_size - 1) / tile_size;
int num_tiles_y = (image_height + tile_size - 1) / tile_size;
// Launch a CUDA kernel to render the image
dim3 block(tile_size, tile_size);
dim3 grid(num_tiles_x, num_tiles_y);
render_tile<<<grid, block>>>(
image_height,
image_width,
tile_size,
point_means.size(0),
point_means.data_ptr<float>(),
point_colors.data_ptr<float>(),
image.data_ptr<float>(),
inverse_covariance_2d.data_ptr<float>(),
min_x.data_ptr<float>(),
max_x.data_ptr<float>(),
min_y.data_ptr<float>(),
max_y.data_ptr<float>(),
opacity.data_ptr<float>());
return image;
}
定量方法在产品市场契合度中的应用(Python 代码)
使用 Python 在你的用户数据中寻找意义
·发表于Towards Data Science ·12 分钟阅读·2024 年 4 月 8 日
--

图片来源:Jason Coudriet来自Unsplash
曾经,我在一家种子风险投资公司从事数据工作。我的部分工作内容是与创业者讨论他们商业中的产品市场契合度指标。我通常会使用Andrew Chen 的定量方法来分析产品市场契合度。我们首先与创业者讨论产品市场契合度的指标,然后请他们提供用户的原始数据。接着,我会处理这些数据,以寻找不同用户及其行为随时间变化的趋势。
在这种定量方法中,主要有两种表示方式:增长核算和用户群分析。在初创公司中,原始数据通常很混乱,因此我需要一遍又一遍地编写代码来调整数据。然而,几周前,我翻开了我的旧笔记本。随着时间和视角的变化,我决定将框架进行概括,并创建更易使用的 Python 对象。这样,你只需要清理和准备数据集,数据可视化部分已经完成!
我编写了两个对象:一个用于增长核算,另一个用于用户群分析。它们是以机器学习风格构建的,因此你可以设置参数,用你的数据拟合对象,然后请求结果和数据可视化。分布情况……
网络科学速成指南

图片来自作者。
适合那些想了解复杂连接的人——从理论到 Python 实践
·发表于 Towards Data Science ·阅读时间:7 分钟·2024 年 11 月 28 日
--
在本文中,我将简要但全面地列出一些学习资料推荐,帮助你开启网络科学的学习之旅。
具体来说,在简要介绍网络科学领域之后,我将列出五本书,这些书籍包括了该领域的基础知识、技术内容,甚至还有实践编程部分。然后,我将介绍五个在网络科学中经常使用的 Python 工具,为你的网络科学之旅奠定实践基础,最后以五个我强烈推荐的教程来结束本次推荐。
什么是网络科学?
网络科学可以称为连接科学。任何复杂系统背后都有隐藏的网络,从社会到城市,再到我们的神经系统。网络科学提供了一个通用的定量框架,用来分析和理解这些系统中的各种模式。如今,网络科学已经在从医学到人力资源,从多媒体到城市规划,再到政策制定和欺诈检测等多个领域找到了应用。
Mojo 快速介绍 —— 一个超快速的 Python 超集
为人工智能学习 Python 的新兄弟语言
·发表于 Towards Data Science ·阅读时长 8 分钟·2024 年 1 月 22 日
--

图片来源:51581,Pixabay
Mojo 是一种全新的编程语言,2023 年出现并迅速引起了广泛关注。它由 Swift 编程语言的原始架构师 Chris Lattner 创造。Mojo 设计为 Python 的超集,语法与 Python 非常相似,因此对 Python 开发者来说非常友好。
Mojo 是一种编译语言,比 Python 快几个数量级。它专门针对机器学习/人工智能相关的计算进行了优化,因此对于普通 Python 开发者来说,学习 Mojo 可能会非常有趣且有帮助。
在这篇文章中,我们将介绍 Mojo 的基础知识,并将其与 Python 中的对应概念进行比较。我相信,当你看到 Mojo 的语法和效率时,定会对比 Python 感到惊讶。
为什么选择 Mojo
Mojo 是目前由 AI 公司 Modular 开发的专有编程语言,其目标是构建一个可扩展和统一的 AI 平台,消除复杂性并为 AI 开发增加灵活性和速度。而 Mojo 是为这个平台开发的全新语言,用以解决 AI 基础设施的扩展和加速问题。它是第一个使用 MLIR 编译器基础设施构建的编程语言……
一种真实世界中的新颖方法,用于增强推荐系统中的多样性
推荐系统
解决长尾问题并提升 Headspace 应用中的用户推荐体验
·发表于Towards Data Science ·阅读时间:6 分钟·2024 年 3 月 1 日
--

作者生成的 DALLE 图像
两种推荐系统的选择
在经过数周的辛勤工作后,你终于有一个值得的夜晚来享受一次愉快的餐厅体验。两个朋友都给出了餐厅推荐。一个朋友倾向于选择那些经过验证的地方,极少涉足未曾尝试的领域。一旦一个笑话奏效,他们会不断地用稍微变动的方式重复。虽然他们的品味可以依赖,但他们的推荐从未让你彻底印象深刻。第一个朋友就像一个受到长尾问题困扰的推荐系统;他们推荐的餐厅是那些大众熟知的、可靠且对大多数人适口的,但不一定是量身定制的。
相反,还有一个朋友总是充满冒险精神,随意地说出各种想法。你总是对他们的口味偏好感到困惑。尽管他们推荐了一些不错的餐厅,但也推荐了一些糟糕的地方,且每次推荐时他们的热情都一样高涨。第二个朋友……
《大型语言模型中‘突现特性’的理性检查》
LLM 通常被认为具有“突现特性”。但我们究竟指的是什么,且我们有哪些证据呢?
·发布于Towards Data Science ·12 分钟阅读·2024 年 7 月 15 日
--

关于大型语言模型(LLM)的一个常被提到的观点,在我们ICML’24 立场文件中有讨论,认为它们具有“突现特性”。不幸的是,在大多数情况下,发言人/作者没有明确说明他们所指的“突现”是什么意思。但在这个问题上的误解可能对研究议程以及公共政策产生重大影响。
从我在学术论文中看到的情况来看,NLP 研究人员至少在四种意义上使用这个术语:
1. 模型表现出的特性,即使它没有针对该特性进行明确的训练。例如,Bommasani et al. (2021, p. 5) 提到 GPT-3 的少样本表现 (Brown et al., 2020) 被称为“一个突现特性,既没有专门训练,也没有预期会出现”。
2. (与定义 1 相对):模型从训练数据中学到的特性。例如,Deshpande et al. (2023, p. 8) 讨论突现作为“预训练优势”的证据。
3. 如果某一特性在较小的模型中不存在,但在较大的模型中存在,那么这个特性就是“突现的”(Wei et al., 2022, p. 2)。
定义(3)的一个版本,其中使得涌现特性“引人注目”的因素是“它们的尖锐性,从不存在到存在似乎瞬间过渡,以及它们的不可预测性,似乎在不可预见的模型尺度上出现”(Schaeffer, Miranda, & Koyejo, 2023, p. 1)
对于一个技术术语来说,这种模糊性是令人遗憾的。如果许多人在没有澄清其含义的情况下重复“LLM 具有涌现特性”这一说法,读者可能会推断出,根据读者自己的定义,科学界对此说法的真实性存在广泛的共识。
我写这篇文章是在全球各地的自然语言处理(NLP)研究小组中进行了许多关于这一主题的讲座后写的——包括美国的阿姆赫斯特和乔治城大学、英国的剑桥、卡迪夫和伦敦、丹麦的哥本哈根、瑞典的哥德堡、意大利的米兰、以及新加坡的 Genbench 研讨会(EMNLP'23)(感谢所有在场的观众!)。这给了我一个机会,向 220 位 NLP 研究员和博士生进行调查,了解他们对涌现的看法。根据他们的回答,到目前为止,最受欢迎的定义是(1),其次是(4)。

定义(1)中表达的观点在公众话语中也经常被引用。例如,你可以在关于谷歌的 PaLM 模型“知道”它没有接受过训练的语言的说法中看到这一点(这几乎肯定是错误的)。相同的观点也引发了美国参议员与梅拉尼·米切尔(Melanie Mitchell,一位著名的人工智能研究员,圣塔菲研究所的教授)之间的以下公开对话:
这段对话表明,定义(1)中的 LLM“涌现特性”这一观点在研究领域之外有影响。它助长了对超级人工智能(AGI)即将接管的焦虑,以及暂停研究的呼声。它可能会推动政策制定者走错方向,比如禁止开源研究——这将进一步巩固大科技实验室的资源,并确保它们几乎没有竞争对手。它还创造了 LLM 作为独立于开发者和部署者选择的实体的印象——这对谁对这些模型造成的任何伤害负责具有重大意义。对于研究界和社会来说,既然赌注如此之高,我们是否应该至少确保科学是可靠的?
这些关于“涌现”的概念在多大程度上有助于我们对 LLM 的科学理解?
上述关于 LLM 中‘涌现’的版本仍然存在很多争议:它们在多大程度上推动了科学讨论,与已经使用的其他术语和已知原理相比?我想强调的是,这一讨论与 LLM 是否有用或有价值的问题完全无关。无数模型在没有“涌现”声明的情况下已经是并将继续是实际有用的。
让我们从定义 2 开始:模型从训练数据中学到的东西。既然这正是机器学习模型应该做的事情,那么这种版本的“涌现”是否为“学习”增加了什么呢?
对于定义(3)(只有大型模型才能做到的事情),考虑到基本的机器学习原理,大型模型的更好性能是可以预期的:大型模型有更多的容量来学习训练数据中的模式。因此,这种“涌现”的版本也没有增加太多。除非我们期望更大的模型,而不是小模型,做一些它们未经过训练的事情——但这个定义又依赖于定义(1)。
对于定义(4),性能急剧变化的现象被证明是由于非连续的评估指标(例如,多项选择题回答类任务),而不是 LLM 本身的原因(Schaeffer, Miranda, & Koyejo, 2023)。此外,J. Wei 本人也承认,当前关于急剧变化的说法是基于只有少量尺寸(1B,7B,13B,70B,150B 等)的模型的结果,如果我们有更多中间模型尺寸的结果,性能的提高可能会是平滑的(Wei, 2023)。
定义(4)中的不可预测性部分被J. Wei (2023)再次强调如下:“如果预测性存在较大差异,‘涌现’现象仍然很有趣:对于某些问题,大型模型的表现可以很容易地从比其小 1000 倍的模型的表现中推断出来,而对于其他问题,即使是从小 2 倍的模型也无法推断出来。”
然而,所引用的在计算量减少 1,000 倍时的可预测性,指的是 GPT-4 报告(OpenAI, 2023),在该报告中,开发者事先知道目标评估,并为此进行了专门优化。鉴于此,从理论上讲,可预测的扩展性并不令人惊讶(尽管从工程的角度来看仍然令人印象深刻)。这与在计算量减少 2 倍时,对于未计划的 BIG-Bench 评估中的不可预测性(Wei et al., 2022)形成对比。这种不可预测性是预期中的,原因仅仅是(a)训练数据与测试数据的相似性,和(b)足够的模型容量能够学习一些特定模式之间的未知交互。
因此,我们回到了定义(1):突现属性是指模型没有专门训练的属性。这可以从两个角度来解释:
5. 如果模型没有接触到该属性的训练数据,则该属性是突现的。
6. 即使模型已暴露于相关的训练数据中,某个属性仍然是突现的——只要模型开发者没有意识到这一点。
根据定义 6,研究问题实际上是“网络上有哪些数据?”(或生成型 AI 公司的专有训练数据集中的数据),而我们正在将大规模语言模型(LLM)作为一种非常昂贵的方法来回答这个问题。例如,ChatGPT 可以生成看起来合理的国际象棋走法(但通常是非法的)。如果我们把 ChatGPT 看作是一个语言模型,这就很令人惊讶,但如果我们知道它是一个在网络语料库上训练的模型,这就不奇怪了,因为这样的语料库可能不仅包含自然语言的文本,还包括如国际象棋记录、ASCII 艺术、MIDI 音乐、编程代码等材料。术语“语言模型”实际上是一个误称——它们更像是语料库模型(Veres, 2022)。
根据定义 5,我们只能通过证明模型未曾接触到可能成为训练数据中模型输出基础的证据来证明某个属性是突现的。而且,这不能是由于在连续表示的潜在空间中幸运抽样的结果。如果我们可以生成任意多的样本并精心挑选,最终我们甚至能从一个随机初始化的模型中得到一些流畅的文本——但从定义(5)来看,这不应该算作是“突现属性”。
对于像 ChatGPT 这样没有公开训练数据的商业模型,这种证明是不可能的。但是即便是对于“开放”型 LLM,这也仅仅是一个假设(如果不是一厢情愿的话),因为到目前为止,我们缺乏详细的研究(甚至是方法论)来考虑特定模型输出与训练文本数据中的证据量和种类之间的确切关系。在定义 5 中,涌现特性是机器学习中的“炼金术”——因此提出这一假设的标准应该相当高。
尤其是在面对相反证据时。
对 LLM 中“涌现特性”的反证
以下是一些实证结果,这些结果使得 LLM 具有“涌现特性”的定义(5)(模型未接触到该特性相关的训练数据)变得值得怀疑:
-
提示敏感性现象 (Lu, Bartolo, Moore, Riedel, & Stenetorp, 2022; Zhao, Wallace, Feng, Klein, & Singh, 2021): 大型语言模型(LLMs)对语义上应该等价的提示做出不同的响应。如果我们认为模型具备回答问题的涌现特性,那么提问的方式稍有不同,特别是少量示例的顺序不同,也不应该产生影响。提示敏感性的最可能解释是,模型更倾向于对那些在某些方面与训练数据更相似的提示做出更好的响应,这种相似性有助于模型的表现。
-
Liang 等人评估了 30 个 LLM,并得出结论:“再现(受版权保护的材料)风险与模型准确性显著相关”(2022, p. 12)。这表明,越是“记住”更多训练数据的模型,表现越好。
-
McCoy, Yao, Friedman, Hardy, & Griffiths (2023) 表明,LLM 的性能依赖于网页文本中输出单词序列的概率。
-
Lu, Bigoulaeva, Sachdeva, Madabushi, & Gurevych (2024) 表明,18 个 LLM 的“涌现”能力大多可以归因于上下文学习。指令调优促进了上下文学习,但似乎没有独立的影响。
-
至于上下文学习本身(首次在 GPT-3 中展示(Brown et al., 2020),并被 Bommasani 等人用作“涌现”的例子(2021, p. 5)),Chen, Santoro 等人(2022)的研究结果表明,这种现象只发生在经过序列训练的 Transformer 模型中,这些序列在结构上类似于上下文学习将被测试的序列。
-
Liu et al. (2023)报告称,ChatGPT 和 GPT-4 在较旧的基准测试上表现优于新的基准测试,这表明许多评估结果可能因数据污染而被夸大。OpenAI 在 GPT-3 的论文中(Brown et al., 2020)费尽心思展示了缓解这个问题的困难。由于我们对最新模型的训练数据一无所知,外部评估结果可能没有意义,而以商业服务出售其模型的公司所提供的内部报告则存在明显的利益冲突。
一个著名的努力是提出一种方法论,至少可以避免数据污染问题,那就是“AGI 的火花”研究(Bubeck et al., 2023)。该研究通过新构建的测试案例,结合公共网页数据及其扰动进行检查,作者显著地得出结论,认为 GPT-4 具备“非常先进的心智理论”。至少有两项研究得出了相反的结论(Sap, Le Bras, Fried, & Choi, 2022; Shapira et al., 2024)。这种方法论失败的最可能原因是,尽管我们可以检查网页上的直接匹配,我们仍然可能错过一些高度相似的案例(例如,文中提到的著名的由 tikz 绘制的独角兽可能基于 stackoverflow 社区绘制的其他动物图像)。此外,像 GPT-4 这样的商业 LLM 也可能在并非公开的数据上进行训练。在 OpenAI 的案例中,在 OpenAI 更改其服务条款,默认不再使用通过 API 提交的数据进行训练之前,成百上千的研究人员和 GPT-3 的其他用户通过 API 提交了大量数据。
这并不是说大型语言模型(LLMs)绝对不可能在其训练分布之外表现良好。某种程度的泛化确实发生了,最好的情况是,这种泛化是由于对训练数据中各个单独观察到的模式进行插值,而这些模式在训练时并没有一起出现。但我们何时可以说结果是某种质的新的东西,什么样的与训练数据的相似性是重要的,以及我们如何识别这些相似性——这些仍然是未解决的研究问题。
自然语言处理(NLP)研究人员实际上并不相信 LLM 的“突现属性”
正如我提到的,我有机会在几个 NLP 研究小组中做关于此话题的讲座。在这些讲座的开始,在我展示上述讨论之前,我问了观众几个问题,包括他们是否个人相信 LLM 具有涌现特性(根据他们偏好的定义,如上所示,主要是(1))。我还询问了他们对领域内共识的看法——他们认为大多数其他 NLP 研究者对此持什么观点?对于第一个问题,我得到了 259 位研究人员和博士生的回答,对于第二个问题,我得到了 360 个回复(提醒自己:给大家更多时间连接到调查)。
结果令人震惊:尽管大多数受访者对 LLM 的涌现特性持怀疑或不确定态度(只有 39%的人同意该说法),但 70%的人认为大多数其他研究者相信这一点。

这与其他一些错误的社会学观念相符:例如,许多 NLP 研究者并不认为 NLP 排行榜特别有意义,也不认为规模化可以解决一切问题,但他们确实认为其他的 NLP 研究者认为(Michael et al., 2023)。在我的样本中,认为 LLM 具有涌现特性的观点在少数研究者中占有一定地位,但却被误解为是大多数人的看法。即便在这少数人中,这一信念也并不十分坚定。在我的四场演讲中,在呈现上述讨论之后,我还询问了观众们现在的看法。在这 70 个回复的样本中,最初同意“LLM 具有涌现特性”这一说法的 83%的人,改为不同意(13.9%)或不确定(69.4%)。
回顾起来,“同意/不同意/不确定”并不是这项调查的最佳选项。作为科学家,我们几乎不可能做到百分之百的确定:正如 Yann LeCun 在Munk 辩论中所说的,我们甚至无法证明现在没有茶壶在绕木星运行。我们的工作不是陷入这些分心的“兔子洞”,而是制定和验证假设,推动我们对所研究现象的理解。对于 LLM 中的“涌现”,我认为我们仍然处于“假设制定”阶段——因为即使在完成了所有上述关于“涌现”的澄清工作后,我们仍然没有明确的研究问题,也没有清晰的方法来获得实证证据。
关键的未解问题是,现有模式的哪种插值才能被视为足够新颖,足以在自然语言数据领域被归类为‘涌现现象’。这个领域尤其困难,因为它融合了不同类型的信息(语言、社会、事实、常识),而且这些信息可能以不同的方式呈现(在上下文中是显式的、隐式的,或需要在长上下文中进行推理)。参见Rogers, Gardner, & Augenstein (2023, 第 8.2 节),讨论了仅在问答任务中涉及的不同技能。
📢 如果你(或你认识的人)希望解决 LLM 输出与其训练数据之间的关系问题——在美丽的哥本哈根,有资助的博士后 / 博士学位职位,可以从事相关研究!(申请截止日期:2024 年 11 月 15 日/22 日)
如果你想引用这篇文章:上述大多数观点,除了投票结果,基于这篇ICML 2024 立场论文的一部分,该论文还探讨了‘涌现’的科学哲学意义及其在大语言模型(LLMs)中的适用性。我们还讨论了我们到底是什么意思为‘大语言模型’(与‘基础模型’和‘前沿模型’相对),以及几个其他常被重复的说法,这些说法伴随着大量的脚注:大语言模型是强健的,大语言模型是最先进的,(大语言模型)规模就是一切,大语言模型是通用技术。
致谢:
-
我的杰出合著者萨莎·卢奇奥尼
-
上述论文的所有匿名审稿人
-
Rob van der Goot, Christian Hardmeier, Yacine Jernite, Margaret Mitchell, Dennis Ulmer,他们阅读了论文的早期版本并提供了反馈
-
Ryan Cotterell, Ishita Dasgupta, Laura Gwilliams, Julia Haas, Anna Ivanova, Tal Linzen, Ben Lipkin, Asad Sayeed 感谢他们的见解和讨论
-
所有参与我投票的剑桥 LTL、卡迪夫 NLP、哥本哈根大学语言技术中心、CLASP、乔治城大学 CL@Georgetown、EMNLP23 的 Genbench、米兰 NLP、QMUL和马萨诸塞大学阿姆赫斯特分校的参与者。
最初发表于 https://hackingsemantics.xyz 于 2024 年 7 月 15 日。
故事中的所有图片均由作者创作。头图包括 Edward von Lõngus 的涂鸦“裸帝”作品,采用 CC-BY-SA 4.0 许可协议.
3D 网格邻域的清晰和扎实的轮廓
如何将 2D 网格算法引入 3D 世界
·发表于 Towards Data Science ·阅读时间:20 分钟·2024 年 3 月 8 日
--

三角形 50 邻域的特写。(图片来源:作者)
在我之前的两篇文章中,《与帕斯卡三角形的简短直接漫步》 和 《基于网格的可视性的快速清晰分析》,我们看到使用基于网格的算法生成看起来不错的旅行路径并计算可视区域是多么容易。我在这些文章中分享的技巧可以应用于视频游戏、移动机器人和建筑设计,尽管我们的例子仅限于二维。在本系列的第三篇也是最后一篇中,我们将把我们对 2D 网格算法的理解扩展到第三维度。继续阅读,了解五种可以用于解决 3D 导航和可视性等 AI 问题的 3D 网格邻域。
3D 导航和可视性问题
由于世界是三维的,视频游戏、移动机器人挑战和建筑设计工具通常需要路径寻找和可视性算法的 3D 变种也就不足为奇了。例如,下面的图片展示了一个人在城市的 3D 模型中从某个点看到的景象。建筑师可能会使用这种 3D 可视性分析来设计一座大型建筑,同时让附近的行人尽可能多地看到天空。

Kean Walmsley在《Through the Interface》上的 3D 可视性示例。(经许可使用)
下一张图片为我们展示了一个人可能在大楼不同楼层之间走动的路线的 X 光视图。

Kean Walmsley在Through the Interface上的 3D 路径寻找示例。(已获许可使用)
上面的例子是典型的 3D 路径寻找,路径被限制在可行走的表面上,例如楼梯和地板。另一种类型的 3D 导航问题出现在为空中机器人(如四旋翼无人机)生成飞行路径时。在这种情况下,路径可能会直接穿越空中,而不是依附于表面。
与之前的文章一样,我们的目的是使用基于网格的算法来解决导航和可视性问题。这意味着每次访问一个网格点时,信息只能流向相邻的网格点。被认为是“邻居”的网格点集合由网格邻域决定。网格邻域有很多种可能性,但下图所示的是五种最小的标准二维网格邻域[1]。请注意,随着邻域从 4 个到 16 个邻居的增加,它们在矩形网格和三角形网格之间交替。一般来说,使用更大邻域的算法运行时间较长,但结果更为精确。

矩形和三角形二维网格邻域。(图片由Autodesk Research [1]提供,已获许可使用)
现在我们关心的问题是:
这些二维邻域在 3D 中是什么样的?
4 邻域和 8 邻域的 3D 等效形式在期刊论文《基于网格的导航路径计数》以及文献中的其他地方有描述,但我在查找其他三种邻域的 3D 版本时遇到了困难。最后我决定自己推导出来,这样就可以呈现完整的集合。在逐一讲解之前,这里先给大家看一下五种最小的标准 3D 网格邻域。

3D 中五种最小的标准网格邻域。(图片由作者提供)
为了展示这五种邻域,我们将使用它们来解决 3D 可视性问题,并比较五种解法的准确性。我们之所以专注于基于网格的可视性,是因为它是最简单的基于网格的算法之一——足够简单,我们可以仔细查看代码。一旦你了解了如何在 3D 中实现基于网格的可视性,你可以使用自己选择的 3D 网格邻域来解决 3D 路径寻找问题以及其他在 3D 世界中出现的 AI 挑战。
矩形 6 邻域
我们将从定义在3D 矩形网格上的邻域开始,简单来说,3D 矩形网格是由一组点[x, y, z]组成,其中 x、y 和 z 是整数。这些网格被广泛使用,可以使用标准的 3D 数组在计算机上表示。

3D 矩形网格的 3x3x3 切片。(图片由作者制作)
第一个 3D 网格邻域几乎是无处不在的,但我们还是将其展示出来以完整性考虑。当二维的矩形 4 邻域扩展到 3D 时,我们得到下图所示的矩形 6 邻域。要解读这张图,请想象两个垂直箭头分别指向上和下,其余箭头分别指向北、东、南和西。

用向量表示的矩形 6 邻域。(图片由作者制作)
现在我们将应用矩形 6 邻域来使用 Python 解决 3D 可视化问题。在下面的代码中,grid_visibility函数输入一个名为grid0的 3D 数组,表示环境。初始网格中值为 1 的单元表示空旷区域,值为 0 的单元表示障碍物。该函数将在另一个 3D 数组grid中计算可视化结果。输出网格中值为 1 的单元被认为从视点[0, 0, 0]处可见,值为 0 的单元则被认为是被遮挡的。
import numpy as np
# Solve the 3D visibility problem using a simple grid-based method
def grid_visibility(grid0):
grid = grid0.copy()
for x in range(grid.shape[0]):
for y in range(grid.shape[1]):
for z in range(int(x==0 and y==0), grid.shape[2]):
vx = grid[x-1,y,z]
vy = grid[x,y-1,z]
vz = grid[x,y,z-1]
grid[x,y,z] *= (x*vx + y*vy + z*vz) / (x + y + z)
return grid >= 0.5
视点固定在[0, 0, 0]的原因只是为了简化代码。如果你希望视点位于其他地方,比如网格的中心,上一篇文章通过数组索引技巧解决了这个问题,并且该技巧也适用于 3D 情况。
为了测试我们的 3D 网格可视化解算器,我们将使用下面显示的场景。输入网格为 40x40x40,具有一个球形障碍物,中心位于[10, 20, 16],半径为 8。

测试场景。(图片由作者制作)
这个问题足够简单,可以通过解析的方法解决,从而让我们测试基于网格的解决方案的准确性。下面动画中的红点表示我们使用 6 邻域网格方法错误分类的网格点。请注意,40x40x40 网格中的绝大多数点没有红点,这意味着它们被正确分类。错误发生在障碍物“阴影”的边界附近,那里网格点要么几乎不可见,要么几乎被遮挡。我发现此类错误通常是可以容忍的,尽管这取决于具体应用。我将在文章的最后提供测试和可视化代码。

使用 6 邻域的网格可视化分类错误。(动画由作者制作)
现在,我们将重新编写基于网格的可见性算法,以适应更大的 3D 网格邻域。关键是解决在由一组向量界定的圆锥内的可见性问题。在上一篇文章中,我们定义了一个 2D 的 visibility_within_cone 函数,该函数需要两个向量来指定一个三角形圆锥。在 3D 中,该函数需要三个向量来定义一个四面体圆锥。
# Solve the 3D visibility problem by modifying a grid within a cone
def visibility_within_cone(grid, u_vector, v_vector, w_vector):
u = np.asarray(u_vector, dtype=int)
v = np.asarray(v_vector, dtype=int)
w = np.asarray(w_vector, dtype=int)
origin = np.array([0,0,0], dtype=int)
dims = np.asarray(grid.shape, dtype=int)
m = 0
k = 0
q = 0
pos = np.array([0,0,0], dtype=int)
while np.all(pos < dims):
while np.all(pos < dims):
while np.all(pos < dims):
if not np.all(pos == 0):
p = tuple(pos)
if grid[p] == 1:
pu = tuple(np.maximum(origin, pos - u))
pv = tuple(np.maximum(origin, pos - v))
pw = tuple(np.maximum(origin, pos - w))
grid[p] = (m*grid[pu] +
k*grid[pv] +
q*grid[pw]) / (m + k + q)
q += 1
pos += w
k += 1
q = 0
pos = m*u + k*v
m += 1
k = 0
q = 0
pos = m*u
以下是 6-邻域的另一种表示方式,展示了与每个圆锥相关的三角面。以这种方式表示,6-邻域看起来像一个八面体。

矩形 6-邻域,表示为三角面。(图片由作者提供)
如果我们将八面体切成两半,就可以看到矩形 6-邻域的 2D 对应物:4-邻域。

将 3D 6-邻域切成两半,揭示出 2D 4-邻域。(图片由作者提供)
让我们再次查看完整的八面体,并将其中一个三角形从原点投影出去,以帮助我们可视化一个四面体圆锥。6-邻域总共有 8 个这样的圆锥,每个圆锥对应于该区域的一个三维八分之一。请注意,每个圆锥延伸到无穷远,覆盖其整个八分之一。

6-邻域中圆锥的可视化。(图片由作者提供)
这是仅包含 6-邻域一个八分之一及其单个圆锥的图。该图使我们可以轻松读取界定向量的坐标,后续我们将需要这些坐标来重新实现基于网格的算法。在这种情况下,界定向量是 [1,0,0]、[0,1,0]、[0,0,1],即三角形的三个角。

在矩形 6-邻域的一个八分之一中绘制了一个圆锥图形。(图片由作者提供)
以下是我们新的 6-邻域 3D 基于网格的可见性实现。
# Solve the 3D visibility problem using the 6-neighborhood
def grid6_visibility(grid0):
grid = grid0.copy()
visibility_within_cone(grid, [1,0,0], [0,1,0], [0,0,1])
return grid >= 0.5
新的 grid6-visibility 函数产生的结果与我们之前看到的 grid-visibility 函数完全相同,但我们的重构工作将帮助我们处理更大的三维邻域,这些邻域包含更多的圆锥。
矩形 26-邻域
当矩形 8-邻域在 2D 中被扩展到 3D 时,我们得到下图所示的矩形 26-邻域。该邻域呈现为一个 2x2x2 的立方体,每一面都被三角形镶嵌,代表圆锥。

矩形 26-邻域。(图片由作者提供)
和之前一样,我们可以将邻域切成两半,以查看其 2D 对应物:8-邻域。

将 3D 26-邻域切成两半,揭示出 2D 8-邻域。(图片由作者提供)
矩形 26-邻域是众所周知的,尽管它很少以能识别其 48 个四面体圆锥的方式展示。下面的插图突出了其中一个圆锥。

矩形 26 邻域中一个锥体的可视化。(图片来源:作者)
下图帮助我们读取一个八分体内 6 个锥体的坐标。

矩形 26 邻域中一个八分体内 6 个锥体的图。(图片来源:作者)
这是我们基于 26 邻域的 3D 网格可视性实现。请注意,我们对上图中的每个三角形调用了一次visibility_within_cone。
# Solve the 3D visibility problem using the 26-neighborhood
def grid26_visibility(grid0):
grid = grid0.copy()
visibility_within_cone(grid, [1,0,0], [1,1,0], [1,1,1])
visibility_within_cone(grid, [1,0,0], [1,0,1], [1,1,1])
visibility_within_cone(grid, [0,1,0], [1,1,0], [1,1,1])
visibility_within_cone(grid, [0,1,0], [0,1,1], [1,1,1])
visibility_within_cone(grid, [0,0,1], [1,0,1], [1,1,1])
visibility_within_cone(grid, [0,0,1], [0,1,1], [1,1,1])
return grid >= 0.5
使用 26 邻域得到的可视性结果比使用 6 邻域的错误更少。你可以看到,下面红点的分布更加稀疏。

使用 6 邻域(左)和 26 邻域(右)的分类错误。(图片来源:作者)
26 邻域是常见的,尽管通常未标明 48 个四面体锥体。理论上,这些锥体对于路径寻找或可视性并非必需,但它们使得我们能够采用更快的算法。例如,计算机科学家普遍认为,可以通过在矩形网格上应用 26 个邻域的 Dijkstra 算法来找到 3D 中的最短网格路径。Dijkstra 算法不要求我们知道这些邻域如何组合成锥体。然而,如果我们已经标识出锥体,就可以采用一种更快速的路径寻找方法,称为3D 跳点搜索 [2]。如果你正在寻找挑战,不妨尝试使用你选择的 3D 网格邻域实现跳点搜索。
矩形 74 邻域
前两个 3D 网格邻域已经相对成熟,但现在我们必须进入未知领域。当二维的矩形 16 邻域扩展到三维时,我们得到了矩形 74 邻域。我不确定该如何描述 74 邻域的形状,但它大致长这个样子。

矩形 74 邻域。(图片来源:作者)
这是它的再次展示,这次被切成两半以展示 16 邻域。

3D 的 74 邻域被切成两半,展示了 2D 的 16 邻域。(图片来源:作者)
矩形 74 邻域共有 144 个锥体。以下是表示一个八分体内 18 个锥体的图。

矩形 74 邻域中一个八分体内 18 个锥体的图。(图片来源:作者)
阅读图中每个三角形的坐标后,我们现在可以实现 74 邻域的 3D 网格可视性。
# Solve the 3D visibility problem using the 74-neighborhood
def grid74_visibility(grid0):
grid = grid0.copy()
visibility_within_cone(grid, [1,0,0], [2,1,0], [2,1,1])
visibility_within_cone(grid, [1,1,0], [2,1,0], [2,1,1])
visibility_within_cone(grid, [1,1,0], [1,1,1], [2,1,1])
visibility_within_cone(grid, [1,0,0], [2,0,1], [2,1,1])
visibility_within_cone(grid, [1,0,1], [2,0,1], [2,1,1])
visibility_within_cone(grid, [1,0,1], [1,1,1], [2,1,1])
visibility_within_cone(grid, [0,1,0], [1,2,0], [1,2,1])
visibility_within_cone(grid, [1,1,0], [1,2,0], [1,2,1])
visibility_within_cone(grid, [1,1,0], [1,1,1], [1,2,1])
visibility_within_cone(grid, [0,1,0], [0,2,1], [1,2,1])
visibility_within_cone(grid, [0,1,1], [0,2,1], [1,2,1])
visibility_within_cone(grid, [0,1,1], [1,1,1], [1,2,1])
visibility_within_cone(grid, [0,0,1], [1,0,2], [1,1,2])
visibility_within_cone(grid, [1,0,1], [1,0,2], [1,1,2])
visibility_within_cone(grid, [1,0,1], [1,1,1], [1,1,2])
visibility_within_cone(grid, [0,0,1], [0,1,2], [1,1,2])
visibility_within_cone(grid, [0,1,1], [0,1,2], [1,1,2])
visibility_within_cone(grid, [0,1,1], [1,1,1], [1,1,2])
return grid >= 0.5
以下是我们在测试场景中应用的三种 3D 矩形网格邻域的错误。74 邻域解决方案包含最少的分类错误点。

使用 6 邻域(左)、26 邻域(中)、和 74 邻域(右)的分类错误。(图片来源:作者)
三角形 18 邻域
在处理完 3D 矩形邻域后,接下来就该看看 3D 中的三角形邻域是什么样的了。它们出奇地难以可视化!一个好的开始方法是提出以下问题:
哪些固体物体的面是等边三角形,并且可以用于填充 3D 空间?
亚里士多德在 2000 多年前试图回答这个问题。他著名地提出,规则的四面体可以填充空间[3]。但他错了。如果你有一大堆规则四面体并尝试将它们组合起来,你必然会遇到空隙。规则八面体也是如此:它们也无法填充空间。但如下所示,你可以使用四面体和八面体填充空间。

八面体(蓝色)和四面体(红色)填充空间,由 TED-43 在 维基百科 上提供
在上面的空间填充排列中,请注意四面体和八面体的顶点位于规则间隔的点上。这些点构成了一个面心立方晶格,我们将其称为3D 三角网格。如果其中一个点位于 [0, 0, 0],我们可以缩放并定向 3D 三角网格,使得其点与 3D 矩形网格上的每个交替点重合。下图显示了具有此配置的 3D 三角网格。

一个 3x3x3 的 3D 三角网格切片。(图片由作者提供)
为了在计算机上表示这些网格,我们将采用与 3D 矩形网格相同的数组类型。然而,在 3D 三角网格的情况下,只有一半的数组元素会被使用。只有当 (x + y + z) 为偶数时,数组元素 [x, y, z] 才会被使用。如果 (x + y + z) 是奇数,则该元素将初始化为 0,并始终保持为 0。
我们现在知道如何排列 3D 三角网格中的点,但在 3D 中,三角网格单元长什么样呢?当我使用“网格单元”这个术语时,我指的是一个以网格点为中心的填充空间的形状。在 2D 中,三角网格单元不是三角形,而是六边形。红博客游戏上关于六边形网格的教程使这一点变得容易理解。结果证明,在 3D 中,三角网格单元被称为菱形十二面体。菱形十二面体填充 3D 空间。

多面体的对偶是你将每个面替换为一个顶点,每个顶点替换为一个面的形状。菱形十二面体的对偶被称为立方八面体。

如果我们将一个立方八面体置于 3D 三角网格点的中心,可以对其进行缩放和定向,使其 12 个顶点与最近的邻网格点重合。换句话说,立方八面体是一个可行的 3D 网格邻域。然而,我不会认为这个 12-邻域是一个标准的 3D 网格邻域,原因很简单:它的一些面是正方形而非三角形。城市设计领域有一个基于网格的可视性算法,可以调整以适应 12-邻域的正方形面[4],但我们将坚持使用当前需要三角形面的算法。
满足我们标准的最小 3D 三角形邻域是三角形 18-邻域。它呈现为一个八面体,每一面都被镶嵌成三角形。

三角形 18-邻域。(图像来源:作者)
如果我们斜切 18-邻域,可以看到它扩展了 2D 三角形 6-邻域。

3D 18-邻域被切成两半,揭示出 2D 6-邻域。(图像来源:作者)
三角形 18-邻域有 32 个锥体,每个八分之一有 4 个锥体。

4 个锥体在三角形 18-邻域的一个八分之一中的图示。(图像来源:作者)
这是我们基于网格的 18-邻域可视性实现。
# Solve the 3D visibility problem using the 18-neighborhood
def grid18_visibility(grid0):
grid = grid0.copy()
visibility_within_cone(grid, [2,0,0], [1,1,0], [1,0,1])
visibility_within_cone(grid, [0,2,0], [1,1,0], [0,1,1])
visibility_within_cone(grid, [0,0,2], [1,0,1], [0,1,1])
visibility_within_cone(grid, [1,1,0], [1,0,1], [0,1,1])
return grid >= 0.5
这是结果。

使用 18-邻域的分类错误。(图像来源:作者)
初看之下,18-邻域似乎比三个矩形邻域(即使它们的邻居和锥体更多)更精确。然而,红点比之前的图中更稀疏的主要原因是,对于 3D 三角网格,我们只评估每隔一个点的[x, y, z]。
三角形 50-邻域
我们集合中的第五个也是最后一个邻域是三角形 50-邻域。它的整体形状被称为星形八面体,基本上是一个在每个面上粘贴一个四面体的八面体。在 50-邻域的情况下,星形八面体的每个面都被镶嵌成 4 个三角形,如下所示。

三角形 50-邻域。(图像来源:作者)
50-邻域扩展了 2D 三角形 12-邻域。

3D 50-邻域被切成两半,揭示出 2D 12-邻域。(图像来源:作者)
它有 96 个锥体,每个八分之一区域 12 个。

在一个三角形 50 邻域的八分之一区域中绘制的 12 个锥体。(图片来源:作者)
以下是 50 邻域网格基础的可见性。
# Solve the 3D visibility problem using the 50-neighborhood
def grid50_visibility(grid0):
grid = grid0.copy()
visibility_within_cone(grid, [2,0,0], [1,1,0], [2,1,1])
visibility_within_cone(grid, [2,0,0], [1,0,1], [2,1,1])
visibility_within_cone(grid, [1,1,0], [2,1,1], [2,2,2])
visibility_within_cone(grid, [1,0,1], [2,1,1], [2,2,2])
visibility_within_cone(grid, [0,2,0], [1,1,0], [1,2,1])
visibility_within_cone(grid, [0,2,0], [0,1,1], [1,2,1])
visibility_within_cone(grid, [1,1,0], [1,2,1], [2,2,2])
visibility_within_cone(grid, [0,1,1], [1,2,1], [2,2,2])
visibility_within_cone(grid, [0,0,2], [1,0,1], [1,1,2])
visibility_within_cone(grid, [0,0,2], [0,1,1], [1,1,2])
visibility_within_cone(grid, [1,0,1], [1,1,2], [2,2,2])
visibility_within_cone(grid, [0,1,1], [1,1,2], [2,2,2])
return grid >= 0.5
最后,以下是我们两个 3D 三角网格邻域的结果。可能一眼看不出区别,但 50 邻域的结果错误较少。

使用 18 邻域(左)和 50 邻域(右)的分类错误。(图片来源:作者)
邻域比较
下表列出了五种展示的 3D 网格邻域、它们的属性,以及将每个邻域应用于我们的测试问题时获得的准确性。准确性值是通过计算正确分类为可见或不可见的网格点数量,并除以评估的网格点总数来得出的。正如我们所预期的,准确性分数随着邻居数量的增加而提高。

3D 网格邻域、其属性和准确性结果的列表。(图片来源:作者)
该分析主要用于说明性目的。如果我们的目标是对这五种 3D 网格邻域进行严格比较,我们不会满足于单一的测试场景。相反,我们会希望将每个邻域应用于大量的测试场景,并对结果进行平均。
我还应该指出,在本文和上一篇文章中,我在实现大邻域的网格基础可见性时,字面和比喻上都采取了捷径。正确的公式,你可以在期刊论文“基于网格的路径计数”[1]中找到,要求对每一对相邻网格点进行视距测试。为了说明这一点,请考虑以下 2D 场景。

在 2D 矩形网格上的两个标记单元格。(图片来源:作者)
如果我们使用 4 邻域或 8 邻域,那么上面示例中的单元格A和B不是邻居。但是,如果我们使用 16 邻域,那么这两个点是邻居,因此我们应该在它们之间执行视距测试。本文系列中的算法减轻了对远距离网格点之间视距检查的需求,尽管在邻居之间的短距离上最好预先计算这些检查。如果我们在单元格A和B的中心之间画一条线,这条线将穿过一个被阻塞的单元格。这表明,视距算法可能不应该直接从A传播信息到B。
我采取的字面和比喻上的快捷方式是,假设两个相邻的单元格只要它们都是空的,就可以互相可见。这个方法在二维的 4 邻域和三维的 6 邻域中效果很好,但对于更大的邻域来说并不完全正确。在上面的示例中,我的 Python 代码的 16 邻域版本会将单元格A和B视为互相可见。它会愉快地将信息从一个单元格传播到另一个单元格,实际上是通过障碍物“走捷径”。
如果我们的障碍物与网格间距相比足够宽,这种我描述的“捷径”并不那么重要。在我们的测试结果中,尽管存在这个缺陷,但较大的 3D 邻域比较小的邻域取得了更高的准确度。但如果你计划在自己的工作中使用大型的二维或三维网格邻域,我建议你仔细考虑哪些相邻的网格点应该被视为直接的信息通道,哪些不应该。
测试和可视化代码
如果你不感兴趣运行本文中展示的 Python 代码,请跳过此部分并继续阅读结论。
如果你有兴趣运行代码,请按照以下步骤操作:
-
确保你已安装 Python,并且安装了 NumPy 和 Matplotlib 库。
-
创建一个空文本文件,命名为
grid_visibility_3D.py。从顶部开始,将本文到目前为止出现的所有代码块复制到这个文本文件中。 -
创建另一个文本文件,命名为
test_grid_visibility_3D.py,并将下面这些指令后的长代码块复制进去。 -
在命令提示符下,运行
python test_grid_visibility_3D.py。你应该看到与邻域比较表中报告的相同准确性分数。你还应该看到测试场景的 3D 可视化。 -
关闭可视化窗口并运行命令
python test_grid_visibility_3D.py 6。你应该看到相同的输出,但在 3D 可视化中会出现红点。你可以拖动光标旋转图形以获得更好的视角。这些红点是与 6 邻域可见性算法相关的错误。再次运行代码,将命令行参数6更改为18、26、50或74,查看与其他 3D 网格邻域相关的错误。
from grid_visibility_3D import *
import matplotlib.pyplot as plt
import sys
# Set dimensions for the test scenario
nx = 40
ny = 40
nz = 40
# Set spherical obstacle parameters for the test scenario
x_sphere = 10
y_sphere = 20
z_sphere = 16
r_sphere = 8
# Initialize the 3D visibility problem for the test scenario
def initial_grid():
grid = np.ones((nx,ny,nz))
p_sphere = np.array([x_sphere, y_sphere, z_sphere])
for x in range(nx):
for y in range(ny):
for z in range(nz):
p = np.array([x,y,z])
r = np.sqrt(np.sum((p - p_sphere)**2))
if r < r_sphere:
grid[x,y,z] = 0
return grid
# Solve the 3D visibility problem analytically for the test scenario
def analytic_solution():
grid = initial_grid()
p_sphere = np.array([x_sphere, y_sphere, z_sphere])
d_sphere = np.sqrt(np.sum(p_sphere**2))
u = p_sphere/d_sphere
for x in range(nx):
for y in range(ny):
for z in range(nz):
if grid[x,y,z]:
p = np.array([x,y,z])
d = np.sum(p*u)
if d > d_sphere:
h = np.sqrt(np.sum((p - d*u)**2))
grid[x,y,z] = h*d_sphere >= d*r_sphere
return grid
# Compare the 3D grid-based results to the analytic solution
def evaluate_grid(test_name, grid, solution, triangular=False):
error_grid = np.abs(grid - solution)
total_count = nx*ny*nz
if triangular:
for x in range(nx):
for y in range(ny):
for z in range(nz):
if (x + y + z)%2 == 1:
error_grid[x,y,z] = 0
total_count -= 1
error_count = int(np.sum(error_grid))
accuracy = 100*(1 - error_count/total_count)
print(test_name + " accuracy: %.3f" % accuracy)
return error_grid
# Plot the 3D visibility problem with or without resulting errors
def plot_test_scenario(error_grid=None, obstacle=True, pretty=True):
elevation = 19
azimuth = 33
ax = plt.figure().add_subplot(projection='3d')
ax.view_init(elev=elevation, azim=azimuth, roll=0)
ax.set_aspect('equal')
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
ax.scatter(0, 0, 0, color='#6A22C2', s=64) # Render viewpoint
if pretty:
# Choose limits that avoid padding
ax.set_xlim(0.9, nx - 0.9)
ax.set_ylim(0.9, ny - 0.9)
ax.set_zlim(0.9, nz - 0.9)
# Ensure axes are prominently displayed
ax.plot([0,nx], [0,0], [0,0], color='gray', linewidth=2)
ax.plot([0,nx], [ny,ny], [0,0], color='black', linewidth=1)
ax.plot([0,nx], [0, 0], [nz,nz], color='black', linewidth=1)
ax.plot([0,0], [0,ny], [0,0], color='gray', linewidth=2)
ax.plot([nx,nx], [0,ny], [0,0], color='black', linewidth=1)
ax.plot([0,0], [0,ny], [nz,nz], color='black', linewidth=1)
ax.plot([0,0], [0,0], [0,nz], color='gray', linewidth=2)
ax.plot([0,0], [ny,ny], [0,nz], color='black', linewidth=1)
ax.plot([nx,nx], [0,0], [0,nz], color='black', linewidth=1)
else:
ax.set_xlim(0, nx)
ax.set_ylim(0, ny)
ax.set_zlim(0, nz)
if obstacle:
n = 100
us = np.linspace(0, 2*np.pi, n)
vs = np.linspace(0, np.pi, n)
xs = r_sphere*np.outer(np.cos(us), np.sin(vs)) + x_sphere
ys = r_sphere*np.outer(np.sin(us), np.sin(vs)) + y_sphere
zs = r_sphere*np.outer(np.ones(n), np.cos(vs)) + z_sphere
ax.plot_surface(xs, ys, zs, color='lightgray')
if np.all(error_grid) != None:
error_count = int(np.sum(error_grid))
xs = np.zeros(error_count)
ys = np.zeros(error_count)
zs = np.zeros(error_count)
i = 0
for x in range(nx):
for y in range(ny):
for z in range(nz):
if error_grid[x,y,z]:
xs[i] = x
ys[i] = y
zs[i] = z
i += 1
ax.scatter(xs, ys, zs, color='red')
plt.show()
if __name__ == "__main__":
# Compute the grid-based solutions
grid0 = initial_grid()
grid = grid_visibility(grid0)
grid6 = grid6_visibility(grid0)
grid18 = grid18_visibility(grid0)
grid26 = grid26_visibility(grid0)
grid50 = grid50_visibility(grid0)
grid74 = grid74_visibility(grid0)
# Ensure that 6-neighbor solutions are identical
if np.any(grid != grid6):
print("Warning: Alternative 6-neighbor solutions differ")
# Compute the analytic solution
solution = analytic_solution()
# Compute the errors and report accuracy
error_grid6 = evaluate_grid(' 6-neighbor', grid6, solution)
error_grid18 = evaluate_grid('18-neighbor', grid18, solution, True)
error_grid26 = evaluate_grid('26-neighbor', grid26, solution)
error_grid50 = evaluate_grid('50-neighbor', grid50, solution, True)
error_grid74 = evaluate_grid('74-neighbor', grid74, solution)
# Plot the results
error_grid = None
if len(sys.argv) >= 2:
if sys.argv[1] == "6":
error_grid = error_grid6
elif sys.argv[1] == "18":
error_grid = error_grid18
elif sys.argv[1] == "26":
error_grid = error_grid26
elif sys.argv[1] == "50":
error_grid = error_grid50
elif sys.argv[1] == "74":
error_grid = error_grid74
plot_test_scenario(error_grid)
结论
感谢您阅读我关于二维和三维路径寻找与可视性的文章。我希望这系列文章扩展了您对于使用简单网格算法能够实现的功能的视野。通过路径计数(参见第一部分)、线性插值(参见第二部分)、选择更大的网格邻域(如本文所述——第三部分)或仅仅选择更精细的网格分辨率,我们可以克服网格的感知局限,取得非常满意的结果。下次当您遇到通常通过暴力射线投射或繁琐的解析计算解决的 AI 问题时,请记住您可以通过基于网格的方法和您选择的邻域完成的工作。
参考文献
[1] R. Goldstein, K. Walmsley, J. Bibliowicz, A. Tessier, S. Breslav, A. Khan, 基于网格的导航路径计数(2022),《人工智能研究杂志》,第 74 卷,917–955 页
[2] T. K. Nobes, D. D. Harabor, M. Wybrow, S. D. C. Walsh, 3D 中的跳跃点搜索路径寻找系统(2022),《组合搜索国际研讨会论文集》(SoCS)
[3] C. L. Jeffrey, C. Zong, 规律四面体堆积的奥秘(2012),《美国数学学会通知》,第 59 卷,第 11 期,1540–1549 页
[4] D. Fisher-Gewirtzman, A. Shashkov, Y. Doytsher, 基于体素的城市环境可视性分析(2013),《测量回顾》,第 45 卷,第 333 期,451–461 页
使用 PCA 进行异常值检测的简单示例
通过在进行异常值检测之前进行 PCA 变换,提高准确性、速度和内存使用效率
·发表于Towards Data Science ·19 分钟阅读·2024 年 11 月 2 日
--
本文是 PCA(主成分分析)在异常值检测中应用系列文章的延续,紧接着是使用 PCA 进行异常值检测的简介。那篇文章介绍了 PCA 本身,并介绍了我们可以使用 PCA 进行异常值检测的两种主要方式:评估重构误差,和在 PCA 转换后的空间上运行标准的异常值检测器。它还提供了使用重构误差的第一种方法的示例,该方法可以通过PyOD提供的 PCA 和 KPCA 检测器轻松实现。
本文介绍了第二种方法,我们首先使用 PCA 转换数据空间,然后在此空间上运行标准的异常值检测。如前一篇文章所述,这在某些情况下可能会降低可解释性,但在准确性、执行时间和内存使用方面确实有一些令人惊讶的好处。
这篇文章也是关于异常值检测的大系列的一部分,目前已经涵盖了 FPOF、计数异常值检测器、距离度量学习、共享最近邻以及掺杂。这篇文章还包括了我书中的一段摘录,《Python 中的异常值检测》。
如果你对 PCA 本身(它用于降维或可视化)已经相当熟悉,你可以跳过前一篇文章,直接阅读这篇。我会快速回顾一下主要思想。
PCA 是一种将数据(将数据记录视为高维空间中的点)从一组坐标系转换到另一组坐标系的方法。如果我们从一个数据集开始(如左侧面板所示),包含 100 条记录和两个特征,那么我们可以将数据视为二维空间中的 100 个点。对于更现实的数据,我们可能有更多的记录和更多的维度,但同样的概念适用。使用 PCA,我们将数据移动到一组新的坐标系,从而有效地创建描述每条记录的新特征集。如前一篇文章所述,这是通过识别穿过数据的正交线(在左侧面板中以蓝色和橙色线表示)来实现的,这些线很好地拟合数据。
所以,如果我们从一个数据集开始,如下左侧面板所示,我们可以应用 PCA 变换将数据转化为右侧面板所示的样子。在右侧面板中,我们展示了数据被映射到的两个 PCA 组件。组件简单地命名为 0 和 1。

左侧面板:一个包含两个特征的数据集中的 100 个数据点。蓝色和橙色的线表示可以穿过数据点并很好地捕捉到点位的正交线。这些线用于确定 PCA 变换。右侧面板:PCA 变换后的相同数据。我们有相同的 100 个数据点,但有两个新的坐标,称为组件 0 和组件 1。
关于 PCA 组件,有一点需要注意,那就是它们是完全不相关的。这是由于它们的构建方式;它们基于穿过原始数据的直线、平面或超平面,这些平面彼此之间是严格正交的。我们可以在右侧面板中看到,组件 0 和组件 1 之间没有任何关系。
这对异常值检测具有重要意义;特别是它意味着异常值往往会转化为一个或多个组件中的极端值,因此更容易被检测到。这也意味着不需要更复杂的异常值测试(测试特征之间的异常关联),可以使用更简单的测试。
单变量和多变量异常值检测器
在深入了解 PCA 在异常值检测中的优势之前,我会简要介绍两种类型的异常值检测器。有很多方法可以对异常值检测算法进行分类,但一种有用的方式是区分单变量测试和多变量测试。
单变量测试
单变量一词指的是只检查一个特征的测试——这些测试识别该特征中罕见或极端的值。示例包括基于 z-score 的测试、四分位距(IQR)、十分位距(IDR)、中位数绝对偏差(MAD)、直方图测试、KDE 测试等。
由PyOD提供的基于直方图的测试之一是 HBOS(基于直方图的异常值得分——在我关于计数异常值检测器的 Medium 文章中有描述,并在《Python 中的异常值检测》中有详细介绍)。
正如在《引入 PCA 进行异常值检测》中所讲到的,PyOD 提供的另一种单变量测试是ECOD。
为了描述单变量测试,我们来看一个针对特定现实世界数据集的异常值检测示例。下表是来自 OpenML 的棒球数据集的子集(该数据集具有公开许可证),这里只显示了三行五列(完整数据集还有更多特征)。每一行代表一个球员,包含该球员的统计数据,包括他们参加的赛季数、比赛场次等。

棒球数据集的子集
为了识别异常球员,我们可以查找那些具有异常单一值的记录(例如,参加了异常多赛季的球员,或者有异常多击球次数的球员,等等)。这些都可以通过单变量测试找到。
例如,使用 z-score 测试来查找异常记录时,我们实际上会对每一列进行 z-score 测试,一次一列。我们首先检查“赛季数量”列(评估该列中每个值相对于该列的异常程度),然后是“比赛场数”列,依此类推。
例如,在检查“Number seasons”列时,使用 z-score 检验,我们首先需要确定该列的均值和标准差。(其他检验可能会确定该列的中位数和四分位距,直方图的箱子数量等。)
然后,我们会计算“Number seasons”列中每个值的绝对 z-score:即每个值与均值的标准差差距。z-score 越大,值越异常。任何绝对 z-score 超过约 4.0 或 5.0 的值,通常可以认为是异常的,尽管这取决于数据的大小和分布。
然后,我们会对每一列重复这个过程。完成后,对于每一行,我们会得到一个评分,表示该行中每个值相对于其所在列的异常程度。因此,每一行会有一组分数:每个值一个分数。
然后,我们需要为每条记录确定一个整体的离群值分数。有不同的方法可以做到这一点,并且每种方法都有一些细微的差异,但两种简单的方法是取每行值的平均 z-score,或者取每行的最大 z-score。
多变量检验
多变量检验一次考虑多个特征。实际上,几乎所有的多变量离群值检测器都是同时考虑所有特征的。
大多数离群值检测器(包括 Isolation Forest、Local Outlier Factor(LOF)、KNN 等)都是基于多变量检验的。
这些检测器的优势在于,我们可以寻找值的异常组合。例如,一些球员可能有典型的跑垒次数和击球次数,但可能在跑垒次数上表现异常多(或异常少),而这些在击球次数的情况下是不常见的。这些异常可以通过多变量检验来发现。
在上面的散点图中(考虑左侧窗格中的原始数据),A 点在两个维度上都极端,因此可以通过单变量检验检测到。实际上,对特征 A 进行单变量检验可能会标记 A 点,对特征 B 进行单变量检验也可能会标记它,因此 A 点在这两个特征中都是异常的,会在单变量检验中得到较高分数。
然而,B 点在两个维度上都是典型的。只有值的组合是异常的,要检测这种异常,我们需要使用多变量检验。
通常,在对表格数据进行离群值检测时,我们关注的是异常的行,而不是单个异常值。而且,异常行不仅包括那些有异常单个值的行,还包括异常的值组合。因此,单变量和多变量检验通常都会有用。然而,多变量检验能够捕捉到单变量和多变量的离群值(在散点图中,像 Isolation Forest、LOF 或 KNN 这样的多变量检验通常会同时检测到 A 点和 B 点),因此在实际应用中,多变量检验更常被使用。
然而,在异常值检测中,我们经常将分析限制为单变量检验。单变量检验更快——通常快得多(这在实时环境或需要评估大量数据的环境中非常重要)。单变量检验还更容易解释。
而且它们不受维度灾难的影响。这个问题在计数异常值检测器、共享最近邻和Python 中的异常值检测中有所涉及,但总体思路是,多变量检验在处理过多特征时会失效。这有很多原因,但一个重要的原因是,距离计算(包括 LOF 和 KNN 在内的许多异常值检测器依赖的计算方法)在维度增多时可能变得毫无意义。通常在仅使用 20 个特征时,或者更常见的是使用 50 个以上特征时,异常值得分可能会变得不可靠。
单变量检验比多变量检验更适应高维度数据,因为它们不依赖于行之间的距离计算。
因此,使用单变量检验有一些重大优势。但也有一些重大劣势:它们无法捕捉到与异常值组合相关的异常值,因此只能检测出部分相关的异常值。
主成分分析(PCA)组件的单变量检验
因此,在大多数情况下,运行多变量检验是有用的(而且更常见)。但它们速度较慢,解释性差,并且更容易受到维度灾难的影响。
PCA 变换的一个有趣效果是,单变量检验变得更加实用。一旦完成 PCA 变换,特征之间就没有关联,因此也就不存在异常值组合的概念。
在上面的散点图(右侧窗格——PCA 变换后)中,我们可以看到,点 A 和点 B 都可以简单地被识别为极端值。点 A 在组件 0 中极端;点 B 在组件 1 中极端。
这意味着,我们可以使用简单的统计检验(如 z-score、IQR、IDR 或 MAD 检验),或使用简单的工具(如 HBOS 和 ECOD)来有效地进行异常值检测。
话虽如此,在通过 PCA 变换数据空间之后,仍然可以使用标准的多变量检验方法,如隔离森林(Isolation Forest)、LOF 或任何其他标准工具。如果这些是我们最常用的工具,那么继续使用它们是有便利的,并且可以在预处理步骤中先使用 PCA 变换数据。
相比于统计方法(如 z-score 等),它们的一个优势在于,它们会自动为每一条记录提供一个单一的异常值得分。如果我们对每一条记录使用 z-score 测试,且数据有 20 个特征,我们将其转换为 10 个主成分(正如下面所述,可以不使用所有主成分),那么每条记录将有 10 个异常值得分——每个得分与其在 10 个主成分中的异常程度相关。然后需要将这些得分合并成一个单一的异常值得分。如上所述,有一些简单的方法可以做到这一点(包括对每行的每个值取均值、中位数或最大 z-score),但执行此操作时存在一些复杂性(详见Outlier Detection in Python)。这虽然是可以处理的,但让检测器提供一个单一得分还是非常方便的。
使用 PCA 进行异常值检测的示例
现在我们来看一个使用 PCA 帮助更好地识别数据集中的异常值的示例。为了更容易看到 PCA 如何进行异常值检测,我们将在这个示例中创建两个相对简单的合成数据集。我们将两个数据集都创建为 10 万个行和 10 个特征,并添加一些已知的异常值,类似于上面散点图中的 A 点和 B 点。
为了简化起见,我们将数据集限制为十个特征,但正如上面和上一篇文章中所建议的,使用 PCA 在高维空间中可能会有很大的优势,因此(尽管在这个示例中没有覆盖),在成百上千的特征下使用 PCA 会比仅使用十个特征更有优势。然而,这里使用的数据集相对容易处理和理解。
第一个数据集 data_corr 是为了让特征之间有强关联(相关性)而创建的。我们更新了最后一行,使其包含一些较大的(但不是特别大的)值。关键是这行数据偏离了特征之间的正常模式。
我们创建了另一个测试数据集,名为 data_extreme,它的特征之间没有关联。该数据集的最后一行被修改为包含某些特征中的极端值。
这使我们可以在两个广为人知的数据分布和异常值类型上进行测试(我们在 data_corr 中有一个忽略了特征之间正常相关性的异常值;我们在 data_extreme 中有一个在某些特征上有极端值的异常值)。
本示例使用了多个 PyOD 检测器,首先需要执行以下代码:
pip install pyod
代码从创建第一个测试数据集开始:
import numpy as np
import pandas as pd
from sklearn.decomposition import PCA
from pyod.models.ecod import ECOD
from pyod.models.iforest import IForest
from pyod.models.lof import LOF
from pyod.models.hbos import HBOS
from pyod.models.gmm import GMM
from pyod.models.abod import ABOD
import time
np.random.seed(0)
num_rows = 100_000
num_cols = 10
data_corr = pd.DataFrame({0: np.random.random(num_rows)})
for i in range(1, num_cols):
data_corr[i] = data_corr[i-1] + (np.random.random(num_rows) / 10.0)
copy_row = data_corr[0].argmax()
data_corr.loc[num_rows-1, 2] = data_corr.loc[copy_row, 2]
data_corr.loc[num_rows-1, 4] = data_corr.loc[copy_row, 4]
data_corr.loc[num_rows-1, 6] = data_corr.loc[copy_row, 6]
data_corr.loc[num_rows-1, 8] = data_corr.loc[copy_row, 8]
start_time = time.process_time()
pca = PCA(n_components=num_cols)
pca.fit(data_corr)
data_corr_pca = pd.DataFrame(pca.transform(data_corr),
columns=[x for x in range(num_cols)])
print("Time for PCA tranformation:", (time.process_time() - start_time))
我们现在有了第一个测试数据集 data_corr。在创建这个数据集时,我们将每个特征设置为前面特征的总和加上一些随机性,因此所有特征之间有很好的相关性。最后一行故意设置为异常值。虽然这些值较大,但并没有超出现有数据的范围。然而,已知异常值中的值并未遵循特征之间的正常模式。
我们接着计算其 PCA 变换。
接下来我们对另一个测试数据集执行此操作:
np.random.seed(0)
data_extreme = pd.DataFrame()
for i in range(num_cols):
data_extreme[i] = np.random.random(num_rows)
copy_row = data_extreme[0].argmax()
data_extreme.loc[num_rows-1, 2] = data_extreme[2].max() * 1.5
data_extreme.loc[num_rows-1, 4] = data_extreme[4].max() * 1.5
data_extreme.loc[num_rows-1, 6] = data_extreme[6].max() * 1.5
data_extreme.loc[num_rows-1, 8] = data_extreme[8].max() * 1.5
start_time = time.process_time()
pca = PCA(n_components=num_cols)
pca.fit(data_corr)
data_extreme_pca = pd.DataFrame(pca.transform(data_corr),
columns=[x for x in range(num_cols)])
print("Time for PCA tranformation:", (time.process_time() - start_time))
在这里,每个特征是独立创建的,因此特征之间没有关联。每个特征简单地遵循均匀分布。最后一行被设为离群值,在第 2、4、6 和 8 个特征中具有极端值,因此在十个特征中的四个特征存在离群值。
现在我们有了两个测试数据集。接下来我们定义一个函数,给定一个数据集和一个探测器,该函数会在完整数据集上训练探测器并在同一数据上进行预测(因此可以识别单一数据集中的离群值),并对两个操作进行计时。对于 ECOD(经验累积分布)探测器,我们添加了特殊处理来创建一个新的实例,以避免保持先前执行的记忆(对于其他探测器,这一点并不必要):
def evaluate_detector(df, clf, model_type):
"""
params:
df: data to be assessed, in a pandas dataframe
clf: outlier detector
model_type: string indicating the type of the outlier detector
"""
global scores_df
if "ECOD" in model_type:
clf = ECOD()
start_time = time.process_time()
clf.fit(df)
time_for_fit = (time.process_time() - start_time)
start_time = time.process_time()
pred = clf.decision_function(df)
time_for_predict = (time.process_time() - start_time)
scores_df[f'{model_type} Scores'] = pred
scores_df[f'{model_type} Rank'] =\
scores_df[f'{model_type} Scores'].rank(ascending=False)
print(f"{model_type:<20} Fit Time: {time_for_fit:.2f}")
print(f"{model_type:<20} Predict Time: {time_for_predict:.2f}")
下一个定义的函数会对每个数据集执行,调用先前的方法对每个数据集进行处理。这里我们测试了四种情况:使用原始数据、使用 PCA 变换后的数据、使用 PCA 变换后数据的前 3 个成分、以及使用最后 3 个成分。这将帮助我们了解这四种情况在时间和准确性上的比较。
def evaluate_dataset_variations(df, df_pca, clf, model_name):
evaluate_detector(df, clf, model_name)
evaluate_detector(df_pca, clf, f'{model_name} (PCA)')
evaluate_detector(df_pca[[0, 1, 2]], clf, f'{model_name} (PCA - 1st 3)')
evaluate_detector(df_pca[[7, 8, 9]], clf, f'{model_name} (PCA - last 3)')
如下所述,仅使用最后三个成分在准确性方面表现良好,但在其他情况下,使用早期成分(或中间成分)也可以表现得很好。这里将其作为一个示例,但本文的其余部分将专注于仅使用最后三个成分的选项。
最后定义的函数会对每个数据集调用。它会对每个测试的探测器执行先前的函数。在这个例子中,我们使用了六个来自 PyOD 的探测器(Isolation Forest、LOF、ECOD、HBOS、Gaussian Mixture Models(GMM)和 Angle-based Outlier Detector(ABOD)):
def evaluate_dataset(df, df_pca):
clf = IForest()
evaluate_dataset_variations(df, df_pca, clf, 'IF')
clf = LOF(novelty=True)
evaluate_dataset_variations(df, df_pca, clf, 'LOF')
clf = ECOD()
evaluate_dataset_variations(df, df_pca, clf, 'ECOD')
clf = HBOS()
evaluate_dataset_variations(df, df_pca, clf, 'HBOS')
clf = GMM()
evaluate_dataset_variations(df, df_pca, clf, 'GMM')
clf = ABOD()
evaluate_dataset_variations(df, df_pca, clf, 'ABOD')
最后,我们对两个测试数据集调用 evaluate_dataset()方法,并打印出前几个离群值(已知的离群值在两个测试数据集的最后几行中)。
# Test the first dataset
# scores_df stores the outlier scores given to each record by each detector
scores_df = data_corr.copy()
evaluate_dataset(data_corr, data_corr_pca)
rank_columns = [x for x in scores_df.columns if type(x) == str and 'Rank' in x]
print(scores_df[rank_columns].tail())
# Test the second dataset
scores_df = data_extreme.copy()
evaluate_dataset(data_extreme, data_extreme_pca)
rank_columns = [x for x in scores_df.columns if type(x) == str and 'Rank' in x]
print(scores_df[rank_columns].tail())
有几个有趣的结果。我们首先查看 data_corr 数据集的拟合时间,见下表(其他测试集的拟合和预测时间相似,因此这里不再显示)。测试是在 Google Colab 上进行的,时间以秒为单位显示。我们可以看到,不同的探测器有着显著不同的时间表现。ABOD 比其他探测器显著慢,而 HBOS 则显著更快。这里包含的另一个单变量探测器,ECOD,也非常快。
拟合 PCA 变换数据的时间与原始数据的时间差不多,这是有道理的,因为这两种数据的大小相同:我们将 10 个特征转换为 10 个成分,处理这些成分的时间与原始数据等效。
我们还测试了仅使用最后三个 PCA 组件(第 7、8、9 组件),并且在某些情况下,拟合时间显著减少,特别是在局部异常因子(LOF)方面。与使用所有 10 个原始特征(19.4 秒)或使用所有 10 个 PCA 组件(16.9 秒)相比,使用 3 个组件仅需要 1.4 秒。在所有其他情况下,除了孤立森林外,拟合时间都有明显下降。

第一测试数据集 data_corr 的 6 个 PyOD 检测器的拟合时间。
在下表中,我们看到 data_corr 数据集的预测时间(其他测试集的时间也类似)。同样,我们看到在只使用三个组件时,预测时间显著下降,尤其是在 LOF 情况下。我们再次看到,两个单变量检测器 HBOS 和 ECOD 是最快的,尽管在预测时,GMM 的速度与它们一样快或更快(尽管在拟合时间上稍微慢一些)。
对于孤立森林(IF),由于我们无论特征数量如何都训练相同数量的树,并且将所有待评估的记录都通过相同的树集,因此时间不受特征数量的影响。然而,对于此处显示的所有其他检测器,特征数量非常相关:在使用 3 个组件时,所有其他检测器相比于使用 10 个原始特征或所有 10 个组件时,预测时间显著减少。

第一数据集 data_corr 的 6 个 PyOD 检测器的预测时间
就准确性而言,在大多数情况下,所有五个检测器在两个数据集上的表现良好,尤其是在将最高异常值分数分配给最后一行时,而在这两个测试数据集中,最后一行正是已知的异常值。结果显示在下表中。共有两行,每行对应一个数据集。对于每个数据集,我们展示了每个检测器为唯一已知异常值分配的排名。理想情况下,所有检测器应将此排名设为 1(最高异常值分数)。
在大多数情况下,最后一行实际上被分配了最高或接近最高的排名,除了在第一个数据集上,IF、ECOD 和 HBOS 例外。这是一个很好的例子,说明即使是像 IF 这样的强大检测器,有时也会在明确的异常值情况下表现不佳。

使用 6 个 PyOD 检测器在原始数据上执行时,在两个测试数据集中为唯一已知异常值分配的排名。
对于第一个数据集,ECOD 和 HBOS 完全错过了异常值,但这是可以预料的,因为它是基于多个值的组合(它忽略了特征之间的正常线性关系),而单变量测试无法检测到。第二个数据集的异常值基于极端值,通常单变量和多变量测试都能可靠地检测到,并且在这里也能做到。
当我们为这些数据集和这些检测器使用 PCA 时,我们看到了精度的显著提高,如下表所示。虽然这并非总是如此,但在这里确实适用。当检测器在 PCA 转换后的数据上执行时,所有 6 个检测器都将已知离群值在两个数据集中的排名排在首位。当数据经过 PCA 转换时,各个组件之间是相互独立的;离群值是极端值,更容易被识别。

使用 6 个 PyOD 检测器对 PCA 转换后的数据进行测试时,两个测试数据集中已知离群值的排名,使用了所有 10 个组件。
另一个有趣的地方是,仅使用最后三个组件就足以将已知离群值排在顶端,见下表。

与前一张表格类似,但只使用了 3 个 PCA 组件。
如上所述,在这些情况下,拟合和预测的时间显著缩短。这就是我们可以利用 PCA 实现显著性能提升的地方:通常只需要使用少量的组件。
仅使用少量组件还可以减少内存需求。这并不总是一个问题,但在处理大型数据集时,这通常是一个重要的考虑因素。
本实验涵盖了数据中两种主要的离群值类型:极端值和偏离线性模式的值,这两者都可以在后期的组件中被识别。在这些情况下,使用最后三个组件效果良好。
使用多少个组件以及哪些组件最合适可能会有所不同,因此需要进行一些实验(最好使用掺假数据来发现最佳方案)。在某些情况下,为了执行时间、可靠地检测相关离群值和减少噪音,使用较早的组件可能更为合适;在其他情况下,使用中间的组件或后面的组件可能更合适。正如我们在本文开始时看到的散点图所示,不同的组件可能会突出不同类型的离群值。
随着时间的推移,改进离群值检测系统
使用主成分分析(PCA)组件的另一个有用好处是,它可以让你更容易地随着时间的推移调整离群值检测系统。通常,在进行离群值检测时,系统不仅仅是对单一数据集进行一次性运行,而是会持续进行,因此要不断评估新的数据(例如,新的金融交易、传感器读数、网站日志、网络日志等)。随着时间的推移,我们能更好地理解哪些离群值对我们最相关,以及哪些离群值被低估或高估。
由于在处理 PCA 转换后的数据时报告的所有异常值都与单一组件相关联,我们可以看到与每个组件相关的相关异常值和无关异常值的数量。这在使用简单的单变量测试(如 z-score、IQR、IDR、基于 MAD 的测试等)时特别容易做到。
随着时间的推移,我们可以学会对与某些组件相关的异常值赋予更高的权重,而对其他组件赋予较低的权重(这取决于我们对假阳性和假阴性的容忍度)。
可视化
降维也有一些优势,因为它有助于可视化异常值,特别是当我们将数据降至二维或三维时。尽管如此,和原始特征一样,即使有超过三维,我们仍然可以通过直方图逐一查看 PCA 组件,或通过散点图两两查看。
例如,检查第一个测试数据集的最后两个组件 data_corr(其中包含不寻常的值组合),我们可以清楚地看到已知的异常值,如下所示。然而,这种做法的有效性有些值得怀疑,因为这些组件本身很难理解。

PCA 转换后的第一个数据集的最后两个组件(组件 8 和 9)的散点图,数据集包含不寻常的值组合。在这里,我们看到空间右上方的一个点。显然,这个点是一个强异常值,尽管组件 8 和 9 表示的内容并不清晰。
结论
本文介绍了 PCA,但也有其他可以类似使用的降维工具,包括 t-SNE(与 PCA 一样,这是 scikit-learn 提供的)、UMAP和自编码器(也在《Python 中的异常值检测》一书中讨论)。
此外,使用 PCA 时,基于重建误差的方法(衡量如何仅使用组件的子集来近似记录的值)可以非常有效,并且通常值得调查,正如在本系列的上一篇文章中所讨论的那样。
本文介绍了使用标准异常值检测器(尽管如示范所示,这更容易包括简单的单变量异常值检测器,而通常不太可能做到这一点)进行异常值检测,展示了先通过 PCA 转换数据的好处。
这一过程的效果如何取决于数据(例如,PCA 依赖于特征之间存在强线性关系,如果数据存在严重聚类,则可能会失效)以及你希望找到的异常值类型。通常需要使用doping或其他测试方法来确定其效果,并调整过程——特别是确定使用哪些成分。不过,当执行时间或内存限制不成问题时,简单地使用所有成分并平等加权它们,通常可以作为一个良好的起点。
同样,在异常值检测中,通常没有单一的异常值检测过程能够可靠地识别你关心的所有类型的异常值(尤其是当你希望找到所有可以合理认为在某种方式上统计上不寻常的记录时),因此通常需要使用多种异常值检测方法。将基于 PCA 的异常值检测与其他方法结合,可以覆盖比仅使用基于 PCA 的方法或仅使用没有 PCA 变换的方法所能检测到的更广泛的异常值范围。
但是,在基于 PCA 的方法有效时,它们通常可以提供更准确的检测,因为异常值往往能够被更好地分离并且更容易被检测到。
基于 PCA 的方法也可以更快地执行(特别是在它们足够有效且不需要与其他方法结合时),因为:1)可以使用更简单(且更快速)的检测器,如 z-score、IQR、HBOS 和 ECOD;2)可能只需使用较少的成分。PCA 变换本身通常非常快速,与拟合或执行异常值检测相比,时间几乎可以忽略不计。
使用 PCA,至少在只需要子集成分时,也可以减少内存需求,这在处理特别大的数据集时可能是一个问题。
所有图片均由作者提供
一个简单的 RAG 增强视觉问答框架
为 Phi-3.5-vision 注入维基百科知识以增强视觉问答能力。
·发表于Towards Data Science ·17 分钟阅读·2024 年 8 月 30 日
--

图片来源:Christian Lue 于Unsplash
引言
检索增强生成(RAG)是一种强大的技术,可以提高大型语言模型(LLMs)生成答案的准确性和可靠性。它还提供了检查模型在特定生成过程中使用的来源的可能性,从而使人工用户更容易进行事实核查。此外,RAG 使得模型的知识保持最新,并能够在不进行微调的情况下融入特定领域的信息。总的来说,RAG 提供了许多好处且几乎没有缺点,并且其工作流程容易实现。因此,它已成为许多需要最新和/或专业知识的 LLM 使用案例的首选解决方案。
在生成式人工智能领域,一些最新的进展集中在扩展流行的 Transformer 架构,以应对多种输入和/或输出模态,尝试复制大语言模型(LLMs)的巨大成功。目前,已经有几个模型,无论是开源还是闭源,展示了处理多种模态的卓越能力。一个流行的多模态场景,且是首批被解决的场景之一,是视觉语言模型(VLMs),这一领域通过发布一些小而强大的模型,如 LLaVA、Idefics 和 Phi-vision,取得了有趣的开源贡献。如果你想入门 VLMs 并了解如何使用 LLaVA 构建一个视觉语言聊天助手,可以查看我之前的文章使用 LLaVA 创建你的视觉聊天助手。
为多模态模型设计 RAG 系统比纯文本模型更具挑战性。事实上,RAG 系统在 LLM 中的设计已经成熟,并且在一般工作流程上已有共识,因为最近的许多发展都集中在提高准确性、可靠性和可扩展性,而不是从根本上改变 RAG 架构。另一方面,多模态性开辟了多种检索相关信息的方式,因此可以做出几种不同的架构选择,每种选择都有其优缺点。例如,可以使用多模态嵌入模型为不同的模态创建一个共享的向量空间,或者选择仅在一种模态中扎根信息。
在这篇博客文章中,我将讨论一个简单的框架,如何将 RAG 扩展到视觉语言模型(VLMs),并重点介绍视觉问答任务。该方法的核心思想是利用 VLM 的能力,理解文本和图像,以生成适当的搜索查询,用于在回答用户提示之前检索外部信息。
我还将提供一个实践教程,介绍如何实现该框架,使 Phi-3.5-vision 能够访问维基百科信息,讨论实现的关键点并展示一些示例。我会把详细内容留给我在以下Git Hub 仓库中分享的完整代码。
视觉问答的 RAG
在本节中,我将描述介绍中提到的框架的一般工作流程。为了便于说明,我将讨论仅有一个用户提示和一张图像的情况。这种情况,例如,适用于简单的视觉问答(VQA)任务。该方法可以直接推广到多个提示和图像,但流程将变得更加复杂,并引入更多的挑战。此外,我将仅考虑外部数据仅由文本文档组成的情况。使用多模态嵌入模型进行检索,或者更一般地,使用多模态搜索引擎,也可以将图像包含在外部数据中。
至于常见的 RAG 工作流程,该框架的工作流程可以分为两部分:检索相关的外部信息和基于提供的外部数据进行生成。
在检索阶段,目标是从外部文本文档中检索一些段落,这些段落可以提供有用的信息来回答用户的提示。为了有效地做到这一点,我们必须确保检索到的段落与提供的图像、提示以及更重要的两者之间的关系相关。实际上,即使检索到的文档包含有关图像的信息,它们也可能没有包含提供答案所需的特定信息。另一方面,提示只有在与它所指的图像配对时才可能被正确理解。为了解决这些挑战,本文讨论的框架利用多模态模型生成一个适当的搜索查询,旨在捕捉在提供的图像背景下回答用户提示所需的信息。搜索引擎将使用生成的查询从外部数据中检索相关信息。
更详细地说,多模态模型将用户的提示和图像作为输入,并负责创建一个与两者整体相关的搜索查询。这个过程可以看作是查询转换的一种特例,旨在考虑问题的多模态性质。事实上,模型将用户的提示转化为一个搜索查询,同时考虑到它所指的图像。
相较于将每种输入模态单独处理的其他方法,例如使用多模态嵌入模型进行检索或使用生成的图像标题/描述来进行语义相似性分析,这种方法的优势在于,它能够更有效地捕捉提示与图像之间的关系。
检索阶段的流程图如下所示。

在检索过程中,模型的任务是创建一个查询,该查询将被搜索引擎用来检索相关的段落。图片由作者提供。
生成阶段与标准的文本-only RAG 工作流程非常相似,唯一的区别是模型在推理时除了接收提示和检索到的段落外,还接收到图像。这一过程如下所示。

在生成过程中,模型将检索到的段落、提示和图像作为输入。图像由作者提供。
为 Phi-3.5-vision 赋能,加入维基百科
在本节中,我将提供一份实用指南,讲解如何应用讨论过的框架,通过让多模态模型访问维基百科来增强其能力。我选择了模型Phi-3.5-vision,因为它是一个非常强大且轻量的开源视觉语言模型。
在本节中,我将仅讨论实现的总体方面,具体细节请参考提供的GitHub 仓库中的代码。
检索
检索阶段的目标是从维基百科收集一些段落,这些段落可以为回答用户关于图像的问题提供有用的信息。在代码实现中,我使用了 Python 包wikipedia来搜索和检索维基百科的内容。
下面是实现检索相关段落的步骤:
-
使用多模态模型生成捕捉问题与图像含义的关键词。
-
使用生成的关键词在维基百科上搜索相关页面。
-
将每个检索到的页面的内容分割成若干块。
-
选择与问题和关键词在语义文本相似度上最相关的内容块。
第一步利用 Phi-3.5-vision 生成一个适当的搜索查询,用于检索相关的维基百科页面。为此,我要求 Phi-3.5-vision 生成与用户问题和图像相关的关键词。然后,我使用维基百科包的内置搜索功能来检索与生成的关键词相关的页面。
Phi-vision-3.5 的通用单轮单图聊天模板具有以下结构:
<|user|>\n
<|image_1|>\n
{prompt}<|end|>\n
<|assistant|>\n
为了生成关键词,我使用了以下提示:
Your task is to write a few search keywords to find Wikipedia pages containing
the relevant information to answer the question about the provided image. The
keywords must be as specific as possible and must represent the information
that is needed to answer the question in relation to the provided image. Don't
write more than 3 search keywords.
Question: {question}
标签{question}在推理前会被用户的问题替代。
在生成关键词后,使用 Wikipedia 包的内置搜索功能来检索与生成的关键词相关的页面。最后,将选定的页面拆分为段落,然后使用嵌入模型和 LangChain 实现的 FAISS 向量存储来选择最相关的段落。我使用了嵌入模型snowflake-arctic-embed-l来嵌入问题和关键词的拼接,以及检索到的页面的片段。实际上,检索阶段本质上是一种“混合搜索”形式,包含两个顺序步骤:使用 Wikipedia 包的内置搜索功能进行关键词搜索,以及使用嵌入模型进行相似度检索。通过这种方式,检索在通过关键词搜索选定的最相关页面的较小段落空间上进行,避免了需要建立一个包含整个 Wikipedia 内容的庞大向量存储。在不同的设置中,检索阶段可以重新设计为在整个外部语料库上进行相似度检索,或使用不同的检索方法组合。
从多个页面检索段落有助于减少选择错误页面的概率,并且当需要从多个页面获取信息来生成答案时也很有用。
生成
在生成阶段,用户的问题、检索到的段落和原始图像作为输入传递给 Phi-3.5-vision 来生成答案。
我在 Phi-3.5-vision 的通用聊天模板中使用了以下提示:
You are a helpful assistant tasked with answering questions about the provided
image.
Answer the following question: {question}
You can use the following passages retrieved from Wikipedia to provide your
answer:
{passages}
在生成时,标签 {question} 会像之前一样被用户问题替代,而标签 {passages} 会被检索到的段落和相应页面名称替代,格式如下:
From Wikipedia page {page_name} : "{passage1}"\n\n
From Wikipedia page {page_name} : "{passage2}"\n\n
From Wikipedia page {page_name} : "{passage3}"\n\n
...
提供提取段落的页面名称有助于解决歧义,特别是当段落内容不足以唯一确定其所指的主题或话题时。
示例
在本节中,我将展示上一节中讨论的实现获得的一些答案示例,比较带有 RAG 功能的视觉语言模型和基础版本的输出。
对于下面的每个示例,我将展示提供给模型的图像、包含问题及 RAG 增强和基础 VLM 答案的块、模型生成的搜索查询块,以及从 Wikipedia 检索到的段落块。
示例 1

图片来自engin akyurt于Unsplash
Question: How tall are the plants that produce this fruit?
Base VLM: Tomatoes are typically grown on plants that can reach heights of 2 to
4 feet, depending on the variety and growing conditions.
RAG VLM: The tomato plants that produce this fruit typically grow to 1-3 meters
(3-10 feet) in height.
VLM 生成了以下搜索关键词:
Search keywords: tomato plant, tomato plant height
并检索到以下段落:
From page Tomato (https://en.wikipedia.org/wiki/Tomato):
"throughout all seasons of the year. Tomato plants typically grow to 1–3
meters (3–10 ft) in height. They are vines that have a weak stem that sprawls
and typically needs support. Indeterminate tomato plants are perennials in
their native habitat, but are cultivated as annuals. (Determinate, or".
From page Tomato (https://en.wikipedia.org/wiki/Tomato):
"Numerous varieties of the tomato plant are widely grown in temperate climates
across the world, with greenhouses allowing for the production of tomatoes
throughout all seasons of the year. Tomato plants typically grow to 1–3 meters
(3–10 ft) in height. They are vines that have a weak stem that".
From page Early Girl (https://en.wikipedia.org/wiki/Early_Girl):
"== See also ==
List of tomato cultivars
== References ==".
From page Black Krim (https://en.wikipedia.org/wiki/Black_Krim):
"== References ==
== See also ==
List of tomato cultivars
Heirloom tomato".
From page Celebrity tomato (https://en.wikipedia.org/wiki/Celebrity_tomato):
"== See also ==
List of tomato cultivars
Tomato production in Florida
== References ==".
From page Fourth of July tomato
(https://en.wikipedia.org/wiki/Fourth_of_July_tomato):
"== See also ==
List of tomato cultivars
== References ==
== External links ==
Picture Gallery from Google Images".
From page Mortgage Lifter (https://en.wikipedia.org/wiki/Mortgage_Lifter):
"== See also ==
List of tomato cultivars
== References ==
== External links ==
Picture Gallery from Google Images".
增强了 RAG 的模型正确报告了 Wikipedia 页面‘番茄’上列出的高度范围,而基础模型的回答则是一个较低的范围。
示例 2

图片来源:Hans-Jurgen Mager via Unsplash
Query: How much does this species weight?
Base VLM: Polar bears can weigh between 900 to 1,600 pounds
(408 to 727 kilograms).
RAG VLM: The weight of this species, the polar bear, ranges from 300-800 kg
(660-1,760 lb) for males and 150-300 kg (330-660 lb) for females.
Search keywords: polar bear, weight
From page Polar bear (https://en.wikipedia.org/wiki/Polar_bear):
"Males are generally 200–250 cm (6.6–8.2 ft) long with a weight of 300–800 kg
(660–1,760 lb). Females are smaller at 180–200 cm (5.9–6.6 ft) with a weight
of 150–300 kg (330–660 lb). Sexual dimorphism in the species is particularly
high compared with most other mammals. Male polar bears also have".
From page Polar bear (https://en.wikipedia.org/wiki/Polar_bear):
"== Notes ==
== References ==
== Bibliography ==
== External links ==
Polar Bears International website
ARKive—images and movies of the polar bear (Ursus maritimus)".
From page Polar bear (https://en.wikipedia.org/wiki/Polar_bear):
"weight of 150–300 kg (330–660 lb). Sexual dimorphism in the species is
particularly high compared with most other mammals. Male polar bears also have
proportionally larger heads than females. The weight of polar bears fluctuates
during the year, as they can bulk up on fat and increase their mass by".
From page List of ursids (https://en.wikipedia.org/wiki/List_of_ursids):
"long, plus a 3–20 cm (1–8 in) tail, though the polar bear is 2.2–2.44 m
(7–8 ft) long, and some subspecies of brown bear can be up to 2.8 m (9 ft).
Weights range greatly from the sun bear, which can be as low as 35 kg (77 lb),
to the polar bear, which can be as high as 726 kg (1,600 lb). Population".
From page Knut (polar bear) (https://en.wikipedia.org/wiki/Knut_(polar_bear)):
"== See also ==
Binky (polar bear)
List of individual bears
== References ==
== External links ==".
From page Polar bear (https://en.wikipedia.org/wiki/Polar_bear):
"The polar bear is the largest living species of bear and land carnivore,
though some brown bear subspecies like the Kodiak bear can rival it in size.
Males are generally 200–250 cm (6.6–8.2 ft) long with a weight of 300–800 kg
(660–1,760 lb). Females are smaller at 180–200 cm (5.9–6.6 ft) with a".
From page Subspecies of brown bear
(https://en.wikipedia.org/wiki/Subspecies_of_brown_bear):
"males, potentially tripling their average weight within three years' time,
and can expect to average between 360 and 545 kg (794 and 1,202 lb).
The reported mean adult body masses for both sexes of the polar bear are very
similar to the peninsular giant and Kodiak bears. Due to their roughly".
再次,RAG 模型使用 Wikipedia 页面‘北极熊’中报告的权重正确回答,而基础模型则使用稍有不同范围的权重作答。
示例 3
在我之前的博客文章 使用 LLaVA 创建视觉聊天助手,我展示了当用户的提示询问图像相关的误导性问题时,LLaVA-1.5 模型如何被诱导产生幻觉。让我们看看使用 Phi-3.5-vision 模型在有无 RAG 增强情况下会发生什么。在这个例子中,特别是当检索到任何误导性段落时,模型现在可能更容易产生幻觉。

图片来源:Wolfgang Hasselmann via Unsplash
Question: Tell me the species of the fish in this image
Base VLM: The image does not contain any fish or aquatic environments, so
I cannot provide a species of fish.
RAG VLM: The image does not contain any fish or aquatic environments. It is
a photograph of a desert landscape with sand dunes.
Therefore, I cannot provide the species of fish based on this image.
基础模型和 RAG 模型都没有被误导性提示诱导产生幻觉,表明 Phi-3.5-vision 相比 LLaVA-1.5 可能具有更好的鲁棒性。特别是,RAG VLM 没有从 Wikipedia 检索到任何页面。事实上,当被要求生成关键词时,模型回答了
There are no fish visible in the image provided. The image shows a desert
landscape with sand dunes. Therefore, the search keywords related to the image
content would be more appropriate for a desert or dune-related topic.
该搜索查询未通过 Wikipedia 包的搜索功能生成任何结果。
注意:在后面的例子中,在与 Idefics 2 进行的早期实验中,基础 VLM 模型和 RAG VLM 模型都受到了幻觉的影响,回答了图像中不存在的鱼类种类名称。特别是,RAG 模型检索到了与各种鱼类相关的误导性段落。幻觉现象是模型依赖的,RAG 框架可能无法在所有情况下完全消除它。
限制
该框架的主要限制是它要求模型至少具备图像和提供问题的基本知识(具体细节将通过 RAG 检索),以生成有效的搜索查询。在某些特定领域的设置中,可能需要对模型进行微调,以生成搜索查询。
本文中讨论的框架也存在传统文本-only RAG 系统常见的局限性。生成中的一个常见错误来源是外部信息检索不正确。在某些情况下,模型无法正确识别检索阶段的失败,从而生成基于错误、不相关或误导性信息的答案。特别是,不相关段落的存在有时会引发模型本不会出现的幻觉。
另一个常见的错误来源是忽视检索到的段落或错误引用它们。这有时会导致检索到的段落和生成的答案之间存在微妙的差异,可能一开始不容易察觉。
示例
在这个示例中,我将展示一个模型错误引用检索到的段落的实例。

搜索关键词:雄狮,体重,平均体重 图片由Luke Tanis提供,来自Unsplash
Query: What's the average weight of this animal?
Base VLM: The average male lion can weigh between 160 to 310 kilograms, while
females typically weigh between 120 to 180 kilograms.
RAG VLM: Based on the information provided in the passages, the average weight
of a male lion is 187.5 kg (386 lb) in Southern Africa and 174.9 kg (386 lb) in
East Africa.
Search keywords: lion, weight, average weight
From page American lion (https://en.wikipedia.org/wiki/American_lion):
"(518lbs-1153lbs) in males and 175kg to 365 kg (385lbs-805lbs) for females.
In 2008, the American lion was estimated to weigh up to 420 kg (930 lb). A
study in 2009 showed an average weight of 256 kg (564 lb) for males and 351 kg
(774 lb) for the largest specimen analyzed.".
From page Lion (https://en.wikipedia.org/wiki/Lion):
"=== Size ===
Among felids, the lion is second only to the tiger in size. The size and weight
of adult lions vary across its range and habitats. Accounts of a few
individuals that were larger than average exist from Africa and India.
=== Mane ===".
From page Panthera leo leo (https://en.wikipedia.org/wiki/Panthera_leo_leo):
"The lion's fur varies in colour from light buff to dark brown. It has rounded
ears and a black tail tuft. Average head-to-body length of male lions is
2.47–2.84 m (8 ft 1 in – 9 ft 4 in) with a weight of 148.2–190.9 kg
(327–421 lb). Females are smaller and less heavy. Zoological lion specimens".
From page Panthera leo melanochaita
(https://en.wikipedia.org/wiki/Panthera_leo_melanochaita):
"Average head-to-body length of male lions is 2.47–2.84 m (8 ft 1 in – 9 ft
4 in) with a weight ranging from 150–225 kg (331–496 lb) averaging 187.5 kg
(413 lb) in Southern Africa and 145.4–204.7 kg (321–451 lb) averaging 174.9 kg
(386 lb) in East Africa. Females average 83–165 kg (183–364 lb) in".
From page Asiatic lion (https://en.wikipedia.org/wiki/Asiatic_lion):
"An adult male Asiatic lion weighs 160.1 kg (353 lb) on average with the
limit being 190 kg (420 lb); a wild female weighs 100 to 130 kg (220 to 285 lb)
.[1]".
From page List of largest mammals
(https://en.wikipedia.org/wiki/List_of_largest_mammals):
"== See also ==
List of largest land carnivorans
Largest organisms
Largest prehistoric animals
List of largest birds
List of largest cats
List of largest fish
List of largest plants
List of largest reptiles
List of largest insects
List of heaviest land mammals
Smallest organisms
== Notes ==".
From page Ancient Mesopotamian units of measurement
(https://en.wikipedia.org/wiki/Ancient_Mesopotamian_units_of_measurement):
"== See also ==
Assyrian lion weights
Babylonian mathematics
Historical weights and measures
Weights and measures
== References ==
=== Citations ===".
尽管答案中以千克为单位的体重是正确的,但模型在转换为南非雄狮的平均体重时给出了错误的磅数,尽管从维基百科提取的相关段落报告了正确的数值。
结论
在这篇文章中,我展示了一个可以用来增强视觉问答系统(Visual Question Answering,VQA)并具备检索增强生成(RAG)能力的简单框架。该方法的核心思想是利用视觉语言模型生成查询,然后通过标准的 RAG 管道来检索外部语料库中的信息。我还展示了该框架的实现,它使 Phi-3.5-vision 能够访问维基百科。该实现的完整代码可以在GitHub 仓库中找到。
尽管所讨论的方法简单且有效,但它仍然无法避免所有 RAG 系统常见的局限性,也无法应对多模态设置所带来的新挑战。一方面,检索某些特定问题的相关信息可能会很困难。由于搜索查询是通过视觉语言模型(Vision Language Model,VLM)创建的,因此检索准确性进一步受限于 VLM 识别图像和理解问题所指的细节的能力。另一方面,即使正确的信息已被检索出来,也不能保证模型在生成答案时不会出现幻觉。在多模态设置中,这个问题可能因模型需要同时关联文本和图像的正确含义,并且还需要理解它们之间的互动而变得更加严重。
我在这篇文章中讨论的框架是对基础 RAG 流水线的直接扩展,适用于视觉问答任务。可以轻松加入标准的高级 RAG 技术,如查询转换、重新排序检索到的段落,以及假设文档嵌入(HyDE)来提高性能。此外,使用多模态嵌入模型(如 CLIP)可以带来新的机会:在进行相似性搜索时,可以利用图像嵌入来查找相关的文本文档,同时也可以检索与原始图像和问题相似和/或相关的图像。后者在某些情况下可能会很有用,比如当需要从不同角度查看图像以回答提示时。另一个改进方向是进行微调,以获得更专业和更有效的模型。考虑到多模态模型在检索和生成过程中的作用,可以执行两种不同的微调过程:一种是专门针对生成搜索查询的模型,另一种是提高模型在基于事实生成任务中的表现。最后,框架可以集成到一个专门的代理系统中,进一步提升其性能和鲁棒性。比如,一个代理系统可以通过对检索到的段落提供反馈、提出后续问题,或仅在必要时专注于搜索图像中特定细节的信息,来迭代地完善生成的查询。它还可以处理更复杂问题的多跳问答任务,并决定何时需要检索更多外部信息来回答用户的查询。
我很乐意在评论区讨论更多改进和/或多模态 RAG 的不同方法!
简单的 LLM 代理部署教程
简单、可扩展且快速的 LLM 代理部署模板
·发布在Towards Data Science ·阅读时间:8 分钟·2024 年 11 月 5 日
--

图片由Jeremy Bishop提供,来自Unsplash
许多教程展示了如何实现 LLM 代理。然而,关于将这些代理部署到 API 或用户友好的 UI 后面的资源非常有限。本文通过一步步的指南来填补这一空白,介绍如何实现和部署一个简单但功能齐全的 LLM 代理。这为您的 LLM 代理概念验证提供了一个起点,无论是用于个人使用还是与他人共享。
我们的实现分为几个部分:
-
代理实现: 使用 LangGraph 作为代理框架,Fireworks AI 作为 LLM 服务。
-
用户界面: 通过使用 FastAPI 和 NiceGUI 构建的 UI 来暴露代理。
-
容器化: 将应用打包为 Docker 镜像。
-
部署: 将 Docker 镜像部署到 Google Cloud Run。
完整的代码和演示应用将在文章末尾提供链接。

组件列表 — 图片来自作者
构建代理
该代理需要两个核心组件:
- LLM 服务: 我们将使用Fireworks AI。
提升 MLLMs 在自定义使用场景中性能的简单方法
一份使用最新口袋大小 Mini-InternVL 模型的 MLLM QLoRA 微调教程
·发表于 Towards Data Science ·阅读时长 6 分钟·2024 年 6 月 11 日
--

图片由 Maarten van den Heuvel 提供,来源于 Unsplash
大型语言模型(LLMs)的世界在不断发展,新的技术进步层出不穷。一个令人兴奋的领域是多模态 LLM(MLLM)的发展,它能够理解并与文本和图像进行交互。这为文档理解、视觉问答等任务带来了无限可能。
我最近写了一篇关于这种模型的一般性文章,您可以在这里查看:
## 微软最新的 Phi-3 视觉语言模型的 6 个现实世界应用
探索 Phi-3-Vision 的可能应用场景,这是一个小巧而强大的 MLLM,可以本地运行(附代码示例)
towardsdatascience.com
但在这篇文章中,我们将探索一个强大的组合:InternVL 模型和 QLoRA 微调技术。我们将重点讨论如何轻松定制这些模型以适应任何特定的使用场景。我们将使用这些工具创建一个收据理解流水线,能够高精度地提取诸如公司名称、地址和总购金额等关键信息。
理解任务和数据集
一个简单的 GAN 正则化方法
如何使用 GANs 有效捕捉数据分布
·发布于 Towards Data Science ·14 分钟阅读·2024 年 7 月 29 日
--
2018 年,我有幸在 AAAI 会议上做口头报告。一个常见的反馈是,报告中的见解比论文中更加清晰。尽管从那时起已经过去了一段时间,我认为分享这些核心见解和直觉仍然很有价值。
这篇论文解决了使用生成对抗网络(GANs)可靠捕捉数据集中的模式的一个重要问题。本文是围绕我对 GANs 的直觉进行阐述的,并从这些直觉中推导出提出的方法。最后,我为那些想尝试的人提供了一个现成的解决方案。如果你已经熟悉 GANs,可以直接跳到下一节。
论文:Sharma, S. 和 Namboodiri, V., 2018 年 4 月. 不遗漏任何模式:使用 GANs 有效捕捉数据分布。在人工智能学会会议论文集, github)
生成对抗网络简介
GANs 用于学习给定分布的生成器。这意味着,如果我们得到一组鸟类图像的数据集,我们需要学习一个函数来生成看起来像鸟的图像。生成器函数通常是确定性的,因此它依赖于一个随机数作为输入,通过随机性生成各种图像。因此,该函数接受一个n维的数字作为输入,并输出一张图像。输入数字z通常是低维的,并从均匀分布或正态分布中随机抽取。这个分布被称为潜在分布Pz。
我们将“所有可能”图像的空间称为数据空间 X,将鸟类图像的集合称为真实 R,其分布为 Pr。在最优状态下,生成器将每个 z 的值映射到某个高概率属于 R 的图像。
GAN 通过两个学习的函数来解决这个问题:生成器(G)和鉴别器(D)。G 以数字 z 为输入,从数据空间中生成一个样本,x = G(z)。在任何时候,我们将 G 生成的所有图像集合称为假样本 F,它们的分布为 Pg。鉴别器从数据空间中取样一个样本 x,并输出一个标量 D(x),预测它属于真实分布还是假分布的概率。
最初,G 和 D 都没有经过充分的训练。在每个训练步骤中,我们随机抽取一些数字并将其传递给 G,以获得一些假样本。同样,我们从真实子集里取出相等数量的随机样本。D 被训练通过交叉熵损失输出 0 表示假样本,输出 1 表示真实样本。G 被训练来欺骗 D,使得 D(G(z)) 的输出变为 1。换句话说,增加生成高评分样本的概率(生成更多样本),减少低评分样本的概率。梯度从损失函数流经 D,然后流经 G。有关损失方程,请参阅原始的 GAN 论文。

[图 1.] 图片取自 2014 年蒙特利尔 NIPS 研讨会《生成对抗网络》上的演示文稿《Generative Adversarial Networks》。[注:在本文中,我们将 Pd 称为 Pr]
上图展示了 GAN 如何在一维空间 X 中进行学习。黑色虚线代表真实分布,我们称之为 Pr。绿色线条代表假样本的分布 Pg。蓝色虚线代表鉴别器输出 D(x) 对数据样本的判断。开始时,D 和 G 都没有正确地执行任务。首先,D 被更新,以正确分类真实样本和假样本。接着,G 被更新,以跟随鉴别器值对生成样本 D(G(z)) 的局部梯度,使得 Pg 更接近 Pr。换句话说,G 根据 D 的反馈轻微改善每一个样本。最后的插图展示了最终的平衡状态。
这可以被看作是一种频率主义的方法。如果 G 从一个模式生成的样本超过了 Pr 中该模式的出现频率,尽管这些样本可能看起来无瑕疵,D 会开始将它们分类为假样本,从而阻止 G 生成这样的样本。相反,当 G 生成的样本较少时,D 开始将它们分类为真实样本,鼓励 G 生成更多此类样本。这个过程一直持续,直到某个元素的生成频率与其在 Pr 中的出现频率匹配。或者,该元素在 Pg 和 Pr 中的出现概率相等。当分布完全匹配时,D 在所有点的输出为 0.5,表示它无法区分真实和假样本。此时损失达到最小值,G 和 D 都无法进一步改进;这种状态被称为纳什均衡。
后来的 Wasserstein GAN 对这个目标进行了一些修改。D 被训练为对真实样本增加值,对假样本则无限减少。它们称之为 Critic。与其计算基于频率的损失,它们将 G 的目标修改为直接调整 Pg 的方向,以提升 D(G(z)) 的值。有关平衡保证和该方法其他细节,请参阅原始论文。
根据我在 GAN 方面的经验,我发现将 GAN 看作是 G 和 D 之间的合作互动,而非竞争,能带来更好的效果。鉴别器的目标是建立 Pg 和 Pr 之间的“真实性”梯度,就像一个软边界。然后,G 使用这个反馈将 Pg 移向 Pr。这个边界越平滑,G 改进的难度就越小。将 GAN 设定为竞争模式是不利的,因为无论是 D 还是 G 网络的损失,都意味着最终目标的失败。然而,联合目标的视角直接与所期望的行为对齐。
模式丢失问题
在 GAN 中,一个常见的问题是生成器丢失了次要模式。G 只能通过 D 对其生成的样本获得反馈。如果 G 因为最初专注于较大的模式而错过了一个模式,它就无法对此进行改进。G 只有在生成接近该模式的样本时,才会在该模式上进行改进。技术上讲,生成器跟随鉴别器的局部梯度,将 Pg 的模式移向与 Pr 相匹配的方向。一旦 G 失去了次要模式的局部梯度,它就永远不会因没有从该模式生成样本而受到惩罚。这个问题在现实世界的数据集中尤为突出,因为这些数据集通常是稀疏的,并且包含许多次要模式。

[图 2.] 在插图中,数字表示 D(x) 等高线的值,虚线边界表示假分布 F。箭头表示 G 所经历的梯度(与等高线垂直)。有两个模式,主模式 M1 和次要模式 M2。尽管鉴别器已将 M2 标记为真实,但由于生成器分布没有接收到导致 M2 的梯度,因此它错过了该模式。
这可以从用于计算梯度的微分方程中看出。给定损失函数,学习G的梯度计算如下:

中间项依赖于在生成样本的数据样本 G(z) 上,D(G(z)) 相对于数据样本 G(z) 的改进。
我们的方法
在我们的论文中,我们提出了解决这个问题的可靠方法。我们通过生成的玩具数据集和一个具有大单峰的真实世界图像数据集进行测试。我们还通过评估 CIFAR 分数和使用 CelebA 面部数据集的定性分析来测试学习表示的质量。
以下部分解释了我们方法背后的基本直觉。
反向生成器或编码器
让我们探讨相反的问题;给定一个图像数据集,我们需要学习从图像到潜在分布的映射。假设潜在分布是一个 10 维的均匀分布 [0, 1]。因此,我们构建一个 GAN,其中 G 是一个将图像作为输入并输出一个 10 维的数值,数值范围在 [0, 1] 之间的函数。D 从这个空间中取数值并输出它们的“真实度”,表示它来自均匀分布的可能性。
在这种情况下,生成器被称为编码器 (E)。这是因为它学习压缩信息。但这有用吗?
我们可以将编码器的任务视为为每个图像分配 10 个浮动数字,范围在 [0, 1] 之间。这实际上将所有给定的图像沿着长度为 1 的直线排布,并为 10 条不同的直线重复此过程。由于我们将真实分布指定为均匀分布,因此在平衡时,编码器将与该分布匹配。或者,所有图像将均匀分布在这 10 条直线之上。
假设E具有有限的容量,这意味着它无法记住特征中的所有模式,并且它被正则化,以确保输入输出之间的连续性。也就是说,权重是有限的,并且对于输入的微小变化,输出不能突然改变。这将导致E将具有相似特征的图像归为有意义的组,这将帮助它在这些约束下完成任务。因此,将语义上更接近的图像放在特征空间中一起。尽管这些特征可能是交织在一起的,但它们仍然能够产生有意义的表示。
现在从这个角度来看模式丢失问题。我们选择均匀分布作为Pr。由于它是单峰分布,因此没有更弱的模式可供丢失。如果E遗漏了模式中的某个区域,它会在Pg的边缘体验到指向该区域的梯度。如果判别器被正则化,它的输出将在遗漏区域的边界逐渐发生变化。从技术上讲,D 在该区域的边界对X是可微的。然后,E 将跟随增加的 D 值进行改进。E遗漏的任何区域最终都会被捕捉到。因此,在这种情况下不会出现模式丢失问题!
由于整个区域是连通的,编码器将在Pg和Pr之间的任何差异中体验到修正梯度。只有全局最优解,网络不会陷入局部最优解。因此,给定足够的容量,编码器可以完美地将任何数据分布编码为单峰分布。我们通过插图展示了这一点,以均匀分布为例。
从这里开始,我们将图像的分布称为Pr,潜在分布称为Pz。图像样本将表示为x,潜在样本表示为z。生成器以z为输入生成图像G(z),编码器以x为输入生成潜在表示E(x)。


[图 3.] 编码器在均匀潜在分布与具有不连通模态的分布中的模式损失。
BIGAN(编码器与生成器联合训练)
BIGAN 由 Donahue 等人于 2017 年提出。它同时训练一个生成器(G)和一个编码器(E),并共享一个判别器(D)。虽然编码器和生成器的操作与之前相同,但判别器将x和z作为输入并产生一个标量输出。
D的目标是将元组(x, E(x))标记为 1,将(G(z), z)标记为 0。因此,它试图在(x, E(x))和(G(z), z)的分布之间建立一个边界。生成器沿着这个边界梯度向上移动,生成更多被判别器标记为 1 的样本,编码器则类似地沿着这个边界向下传播。这里的D的目标是帮助(x, E(x))和(G(z), z)的分布融合。
那么,这些分布融合的意义何在?只有当G(z)的分布与数据分布Pr匹配,且E(x)的分布与潜在分布Pz匹配时,这种情况才会发生。因此,每个潜在变量都映射到一张图像,每张图像也都映射到一个潜在变量。另一个固有的重要特性是,这种映射是可逆的,即G(E(x))=x且E(G(z))=z。更多细节请参考原论文。
让我们来可视化一下它是什么样子——判别器在x和z的联合空间中工作。下面的插图展示了G和E的起始状态和均衡状态,对于1维的X和1维的Z。Pz是均匀分布,Pr是一个具有 5 个点模式的稀疏分布。因此,Pr的模式({x1, x2, x3, x4, x5})表现为“点”,而潜在变量的分布表现为连续分布。绿色点表示(G(z), z)元组,黄色点表示(x, E(x))元组。修改E会沿着Z 轴移动黄色点,而修改G会沿着X 轴移动绿色点。因此,为了使分布匹配,E必须沿着Z 轴扩展黄色点,以逼近均匀分布。而G必须水平地移动绿色点,以使其类似于给定数据的分布Pr。


[图 4.] 一开始,G 将所有的 z 值映射到一个随机的 x,而 E 将所有的 x 值{ x1,x2,x3,x4,x5 }映射到一个随机的 z。在均衡状态下,黄色点沿着 Z 轴均匀分布。绿色点则对齐到Pr中的可能模式。真实样本(x, E(x))显示为“集中”状态,因为编码器的有限容量无法扩展点模式。生成的样本(G(z), z)显示为“线状”,因为 z 是从连续分布中采样的。

[图 5.] 请注意,如果生成器和编码器分别使用不同的判别器单独训练,这也是一个有效的配置。这符合匹配分布的标准,但不允许 G 和 E 的可逆性。这不是 BIGAN 的目标。

[图 6.] 如果数据模式不是点而是略微分散的,编码器可以在有限容量下将其扩展为均匀分布。具有有限容量的生成器是连续的;因此,对于某些 z 值,G 可以输出数据的跨模式值。
需要注意的是,G和E并不直接相互作用,而是通过D进行交互。因此,它们的目标或损失函数是相互独立的。例如,编码器的目标是使E(x)的分布匹配Pz,而不考虑G的表现如何。这是因为在将元组(x, E(x))与(G(z), z)匹配时,编码器只能控制E(x),而E(x)必须与Pz匹配,无论G(z)是否匹配Pr。对生成器来说也是同样的道理。因此,编码器对于单峰分布仍然能够完美地执行。
在 BIGAN 中,模式丧失的问题是什么样的?
如果生成器失去对较弱模式的梯度,它们仍然可能会丢失,即使这些梯度已经被很好地编码。

[图 7.] 一个塌陷的生成器,对于所有 z 的值输出 x3。
在上面的插图中,G已经塌陷到模式x3。G经历了沿X 轴指向邻近模式x2和x4的梯度,蓝色箭头表示。但远离的模式x1和x5可能会被忽视并被落下。
最后,我们的解决方案!
Gulrajani 等人在论文《改进的 Wasserstein GAN 训练》中提出了通过一种方法来稳定 Wasserstein GAN。由于 WGAN 中的鉴别器是无界的,如果没有正则化,损失可能会突然增加。这可以通过链式法则扩展后的损失方程中看到。

这里,术语∂D/∂G应始终是有限的,或者说D(x)应在相对于x的每个位置都是可微的。原始方法通过对权重施加边界来实现这一点。然而,Gulrajani 等人建议通过额外的损失直接对梯度施加惩罚。为此,在当前批次中,点被随机采样于真实样本和生成样本之间。并且,在这些点处,梯度∂D/∂x的幅度被强制为 1,通过均方误差来实现。
要传达的信息是,直接建模鉴别器的景观也是一种可行的解决方案。受到直接建模鉴别器景观技术的启发,我们可以使用类似的方法。让我们再看一眼图 7。

[图 8.] 在这里,生成的点 {g1, g2, g3, g4, g5} 应该到达标记的模式,但由于缺少梯度,它们未能成功到达。
点{g1, g2, g3, g4, g5}是数据点{x1, x2, x3, x4, x5}的编码E(x)的生成G(z),或者gi = G(E(xi))。这些是点xi的重建。我们需要建模梯度∂D/∂x,使得点gi开始向它们各自的目标点xi移动。
为了实现这一点,我们沿着连接xi与其重建gi的线段均匀地采样一些点。然后我们强制所有这些点的梯度∂D/∂x为单位值,并且指向xi,通过均方误差来实现。我们称之为成对梯度惩罚,并将其作为额外的损失加到鉴别器中。

损失中的第一项是指向正确方向的单位向量,第二项是鉴别器在采样点处相对于 x 的梯度。[注:此处 gi 被称为 x-hat。]
可能有人会考虑将xi与其重建gi之间的均方误差作为生成器的附加损失项,以期达到类似的效果。然而,我们发现很难平衡生成器的重建损失和对抗损失。这是因为对抗损失和重建损失在行为和规模上完全不同,难以找到一个常数权重来有效地平衡它们,适用于玩具数据集和真实数据集。相比之下,梯度惩罚并不直接约束D(x),而是约束∂D/∂x;因此,它不是对抗损失的直接竞争目标,而仅起到正则化作用。我们发现一个常数(λ=1)在所有情况下都有效。
它有效吗?
我们使用不同的损失函数训练简单的网络,如 DCGAN 和 MLP。我们使用玩具数据集来更好地可视化解决方案,并使用具有重中心模式的图像数据集来检查模式丢失。
A. 玩具数据集 我们使用正态分布的混合生成具有多个稀疏模式的(二维X和一维Z)数据集。这些模式排列成圆形和网格状。可以看出,默认的 BIGAN 很容易错过模式,但我们的方法在所有情况下都能捕捉到所有模式。

[图 9.] 使用原始方法和我们提出的方法在一个具有稀疏模式的玩具数据集上训练 BIGAN 网络的结果。第一列展示了原始 GAN 的结果,第二列展示了我们提议方法的结果。
B. 重中心模式 我们从交通交叉口的视频中定期提取快照(参考文献 [5])。背景保持静止,在某些时间和某些位置,画面中的活动非常少。该数据集具有一个巨大的模式,仅包含背景,没有车辆。虽然原始的 GAN 和 WGAN 在这个任务上始终失败,但我们的方法展示了显著的学习效果。

[图 10.] 来自原始 GAN 的生成和重建。注意它会收敛到最频繁的样本。

[图 11.] 我们方法的生成和重建。生成器能够捕捉到较小的模式。
C. 潜在插值 我们还使用 CelebA 人脸数据集测试了我们的方法,发现模型学习到了一些只在部分帧中出现的细节特征,如帽子、眼镜、极端的面部角度等。完整结果请参见论文。

[图 12.] 潜在空间插值的生成结果。
试试吧
对于使用 BIGAN 或其他任何方法,其中E和G是可逆的,尽管尝试。只需将以下函数的输出添加到鉴别器损失中。该方法应该适用于所有网络架构。至于其他使用传统 GAN 的用户,BIGAN 可能是一个有价值的考虑。
def gradient_penalty(x, z, x_hat, discriminator):
"""
Computes the pair-wise gradient penalty loss for a BIGAN.
Args:
x: Samples from the real data.
z: Samples from encoded latent distribution (= Enc(x)).
x_hat: The reconstruction of the real samples (= G(E(x)))
discriminator: The discriminator model with signature (x,z).
Returns:
gp_loss: Computed per example loss.
"""
# Assuming only 1st dimension is the batch dimension.
num_batch_dims = 1
epsilon = tf.reshape(tf.random.uniform(shape=x.shape[:num_batch_dims]), x.shape[:num_batch_dims] + [1] * (len(x.shape) - num_batch_dims))
# Compute interpolations.
x_inter = (epsilon * x) + ((1\. - epsilon) * x_hat)
x_inter = tf.stop_gradient(x_inter)
z = tf.stop_gradient(z)
with tf.GradientTape(watch_accessed_variables=False) as tape:
tape.watch(x_inter)
# Compute discriminator values for the interpolations.
d_inter = discriminator(x_inter, z)
# Compute gradients at the interpolations.
d_inter_grads = tape.gradient(d_inter, x_inter)
# Compute the unit vector in the direction (x - x_hat).
delta = x - x_hat
unit_delta = delta / tf.norm(delta, axis=-1, keepdims=True)
# Compute loss as the mse between gradients and the unit vector.
return tf.reduce_mean((d_inter_grads - unit_delta)**2, -1)
结论
如果编码器和鉴别器具有足够的容量,编码器可以准确地将任何分布映射到单峰潜在分布。当实现这一点时(生成器和编码器是可逆的),生成器也可以通过成对梯度惩罚完美地学习真实分布。惩罚有效地对鉴别器进行正则化,消除了平衡三个网络的需要。该方法通过独立增加任何一个网络的容量而受益。
希望这能帮助人们深入了解 GAN,并可能有助于模式损失 😃
参考文献
[注:除非另有说明,所有图片均为作者所拍摄]
[1] Goodfellow, I., Pouget-Abadie, J., Mirza, M., Xu, B., Warde-Farley, D., Ozair, S., Courville, A. and Bengio, Y., 2014. 生成对抗网络。神经信息处理系统进展, 27。
[2] Arjovsky, M., Chintala, S. and Bottou, L., 2017 年 7 月。Wasserstein 生成对抗网络。在机器学习国际会议上(第 214-223 页)。PMLR。
[3] Gulrajani, I., Ahmed, F., Arjovsky, M., Dumoulin, V. and Courville, A.C., 2017. 改进的 Wasserstein GAN 训练。神经信息处理系统进展, 30。
[4] Donahue, J., Krähenbühl, P. and Darrell, T., 2017. 对抗特征学习。在:第 5 届国际学习表示会议(ICLR),法国图伦,2017 年 4 月 24-26 日。
[5](交通数据集):Varadarajan, J. and Odobez, J.M., 2009 年 9 月。用于场景分析和异常检测的主题模型。在2009 年 IEEE 第 12 届国际计算机视觉研讨会,ICCV 研讨会(第 1338-1345 页)。IEEE。
提高 LLM 查询生成的简单策略
动态少量提示(Dynamic Few-Shot Prompting)
·发布于Towards Data Science ·22 分钟阅读·2024 年 8 月 6 日
--
在 2024 年 3 月,我在Real Python上写了一篇教程,详细介绍了使用LangChain构建一个检索增强生成(RAG)聊天机器人的步骤。这个聊天机器人正式命名为医院系统聊天机器人,使用Neo4j从一个合成的医院系统数据集中检索数据,数据中包括有关患者、患者评论、医院位置、就诊记录、保险支付者和医生的信息。
医院系统聊天机器人代理通过 FastAPI 托管,并可以通过Streamlit应用程序访问 — 所有内容都通过 Docker Compose 打包:

医院系统聊天机器人的演示。图片由作者提供。
所有代码都可以在GitHub上找到,任何人都可以在Real Python上详细了解这个项目。
[## GitHub - hfhoffman1144/langchain_neo4j_rag_app: 使用 LangChain 和 Neo4j 构建的知识图谱 RAG 应用程序…
使用 LangChain 和 Neo4j 构建的知识图谱 RAG 应用程序 - hfhoffman1144/langchain_neo4j_rag_app
自从编写教程以来,我一直在 GitHub 上维护该项目,以提高医院系统聊天机器人的功能,使其更加适合生产环境。例如,我添加了单元测试,重构了代码以修复已废弃的特性,利用 GitHub Actions 创建了部署流程,最近还集成了动态少量示例提示,以改进 Cypher 查询生成。
一种在几秒钟内完成数据分析的简单技巧

使用 ydata-profiling 发现隐藏的洞察
·发表于Towards Data Science ·阅读时长 7 分钟·2024 年 1 月 10 日
--
探索性数据分析(EDA)在数据科学中发挥着至关重要的作用,它使我们能够洞察数据并理解数据集中的模式。在我之前的文章中,我介绍了一个名为“Pandas GUI”的 Python 库,它是一个即用型的 Python EDA 工具。
PandasGUI 数据分析的实用概述
towardsdatascience.com
现在,让我们将注意力转向“ydata-profiling”,它是流行的“pandas-profiling”库的继任者。“ydata-profiling”提供了先进的 EDA 能力,解决了其前身的局限性,使其成为数据科学家和分析师不可或缺的资源。
快速入门

和往常一样,在我们开始使用这个库之前,我们需要通过pip安装它。
pip install ydata-profiling


浙公网安备 33010602011771号