docs-merge-19
TowardsDataScience 2024 中文翻译(二十)
工具使用、代理和 Voyager 论文
对《Voyager 论文》及其在工具使用方面的发现进行了详细探讨
·发布于Towards Data Science ·阅读时间 8 分钟·2024 年 5 月 1 日
--

图像来源:作者。由 DALL-E 2 生成
随着 LLM 推理能力的不断增强,它们的计划和执行能力也在提高。这导致了出现了这样一种提示模板:用户给 LLM 一个想要的结果,LLM 会找出如何实现它——即使这需要多次行动才能完成。这种提示通常被称为“代理”,并且已经引起了广泛的关注。
举个例子,用户可以要求代理赢得一场游戏,然后观察它制定出一个好的策略来实现这一目标。通常情况下,我们会使用强化学习等框架训练模型赢得像《超级马里奥兄弟》这样的游戏,但当我们考虑像《Minecraft》这样的目标开放的游戏时,LLM 的推理能力就显得尤为重要。
《Voyager 论文》重点讨论了如何提示 LLM,使其能够完成开放性和挑战性任务,如玩《Minecraft》。让我们深入探讨一下!
Voyager 的高级讨论

图 2 来自论文
这里的 Voyager 系统由三个主要部分组成:自动化课程、迭代提示机制和技能库。你可以把课程想象成系统的指南针,它能帮助代理在特定情境下判断应该做什么。当新的情境出现时,我们通过迭代提示机制为新情境创造新的技能。由于 LLM(大规模语言模型)有有限的上下文,而课程可能会创建对无限技能的需求,因此我们还拥有技能库,用来存储这些技能以备后续使用。
自动化课程

论文中的图 3
自动课程本身就是提示工程,其中涉及关于 AI 的即时环境和长期目标的相关信息传递给 LLM。作者非常贴心地在论文中提供了完整的系统提示,因此我将在下面突出其中有趣的部分。

论文附录 A.3.4 的部分内容
首先,提示解释了将传递的基本架构。尽管这里没有填写具体信息,但似乎这是通过引导 LLM 以这种架构接收信息来提供帮助。这类似于少样本推理,因为与此聊天机器人的后续对话将使用这种格式。

论文附录 A.3.4 的部分内容
接下来,提示列出了 LLM 应该如何进行推理的相当精确的方式。请注意,这仍然是用第二人称(你)给出的,而且这些指令高度具体化到 Minecraft 本身。在我看来,上面列出的改进措施似乎会带来最大的回报。此外,请注意,这些步骤本身并不总是像传统编程中那样精确,而是与探索这一更模糊的目标保持一致。这正是代理程序的潜力所在。
在上面的部分中,第一步对我特别有趣,因为它可以被解读为一种“人格化”提示:告诉 LLM 它是一个导师,从而让它在回答时表现出更多的自信。我们过去已经看到,人格化提示可以促使 LLM 采取更果断的行动,因此这可能是一种确保代理能够行动而不是陷入分析瘫痪的方式。

论文附录 A.3.4 的部分内容
最后,提示再次通过提供一个少样本推理的示例来说明最佳回应方式。
从历史上看,我们使用强化学习模型和特定的输入来发现最大化明确指标的最佳策略(例如,在街机游戏中获得最高分)。如今,LLM 被赋予了一个更模糊的长期目标,并采取行动来实现这一目标。我们认为 LLM 能够近似这种类型的目标,这标志着对机器学习代理期望的重大变化。
迭代提示机制

论文中的图 5,展示了环境和执行反馈
在这里,LLM 将创建执行 Minecraft 中某些操作的代码。由于这些操作往往是更复杂的系列动作,我们称之为“技能”。
在创建将进入技能库的技能时,作者让他们的 LLM 在开发过程中接收了三种不同类型的反馈:(1)执行错误,(2)环境反馈,以及(3)来自另一 LLM 的同行评审。
执行错误可能发生在 LLM 在代码语法、Mineflayer 库或其他被编译器或运行时捕捉到的项目上出错时。环境反馈来自 Minecraft 游戏本身。作者使用 Mineflayer 中的bot.chat()功能获取反馈信息,例如“我不能制作 stone_shovel,因为我还需要:2 根棍子”。然后,这些信息被传递给 LLM。
虽然执行和环境反馈看起来很自然,但同行评审的反馈可能会显得有些奇怪。毕竟,运行两个 LLM 比仅运行一个要更昂贵。然而,由于 LLM 可以创建的技能集合庞大,编写代码来验证这些技能是否按预期执行会非常困难。为了避免这个问题,作者让一个独立的 LLM 审查代码并提供反馈,评估任务是否完成。尽管这不像程序化地验证工作是否完成那样完美,但它作为代理已经足够好。

来自论文的图 6
按时间顺序,LLM 将在不断改进的过程中持续尝试创建技能代码,同时根据执行错误、环境反馈和同行评审不断改进。一旦所有反馈都表明该技能已通过,它将被添加到技能库中以供未来使用。
技能库

来自论文的图 4
技能库保存了 LLM 之前生成的技能,这些技能已经通过了迭代提示步骤中的审批过程。每个技能通过将其描述转换为嵌入向量的方式添加到库中。作者随后使用任务的描述查询技能库,以找到与该描述相似的技能嵌入。
由于技能库是一个独立的数据存储,它可以随着时间的推移自由扩展。论文中没有详细讨论如何更新库中已有的技能,因此看起来一旦技能被学习,它就会保持原样。这引发了关于如何在经验积累过程中更新技能的有趣问题。
与其他 Agent 提示的比较
Voyager 被认为是代理空间的一部分——我们期望 LLM 作为一个独立的实体与环境互动并改变事物。

来自REACT: 协同推理与行动的图 1d
语言模型论文](https://arxiv.org/pdf/2210.03629)
为此,有几种不同的提示方法被用来实现这一目标。首先,AutoGPT 是一个 Github 库,人们已用它来自动化许多不同的任务,从文件系统操作到简单的软件开发。接下来,我们有 Reflexion,它给 LLM 提供了一个刚刚发生的事件示例,然后让它反思在类似情况下下次应该做什么。我们使用反思后的建议告诉 Minecraft 玩家接下来该做什么。最后,我们有 ReAct,它让 LLM 通过公式化的思维方式将任务分解成更简单的步骤。从上图中,你可以看到它使用的格式。
每种方法论都已被应用于游戏,下面的表格展示了结果。只有 AutoGPT 和 Voyager 方法成功地达到了木工具阶段。这可能是 LLM 训练数据的结果。对于 ReAct 和 Reflexion,似乎需要相当多的关于当前任务的知识,才能使提示有效。从下表中可以看出,没有技能库的 Voyager 方法做得比 AutoGPT 更好,但仍未能进入最终的钻石工具类别。因此,我们可以清楚地看到,技能库在这里扮演了举足轻重的角色。未来,LLM 的技能库可能成为公司的一种护城河。

论文中的表格 1
技术进步只是看待《Minecraft》游戏的一种方式。下图清晰地概述了每个 LLM 探索过的游戏地图部分。看看 Voyager 在地图上探索的范围远超其他 LLM。这是由于稍有不同的提示词导致的偶然结果,还是 Voyager 架构固有的特性,还有待观察。随着这种方法论应用于其他情境,我们将能更好地理解这一点。

论文中的图 7
结论
本文突出了一个关于工具使用的有趣方法。随着我们推动 LLM 具备更强的推理能力,我们将越来越多地期望它们根据这种推理能力做出决策。尽管一个能自我改进的 LLM 比静态的 LLM 更有价值,但它也带来了一个问题:如何确保它不会偏离正轨?
从某个角度来看,这局限于其行为的质量。复杂环境中的改进并不总是像最大化可微分奖励函数那么简单。因此,未来的一个重要工作领域将集中在验证 LLM 的技能是否在提升,而不是仅仅在变化。
然而,从更广阔的角度来看,我们可以合理地想知道,是否存在一些领域或技能,在某些情况下,如果让大型语言模型(LLM)自行决定,可能会变得过于危险。比如那些直接影响人类生命的领域。现在,像这样的领域仍然存在 LLM 可以解决的问题,因此,解决方案不能是冻结此处的进展,允许那些本应从进展中受益的人遭受损失。相反,我们可能会看到一个人类和机器智能相结合的世界,在这个世界中,LLM 执行人类设计的技能。
现在正是构建的激动人心时刻。
[1] Wang, G., 等人. [“VOYAGER: 一个开放式的具身智能体
与大型语言模型”](https://arxiv.org/pdf/2305.16291.pdf)(2023 年),arXiv
[2] Significant-gravitas/auto-gpt: 一个实验性的开源项目,旨在使 gpt-4 完全自主,2024 年,Github
[3] Yao, S., 等人. “REAC T: 语言模型中推理与行动的协同作用”(2023 年),arXiv
2025 年数据与 AI 十大趋势
Agentic AI、小数据和在非结构化数据堆栈时代寻找价值。
·发表于Towards Data Science ·12 分钟阅读·2024 年 12 月 16 日
--

图片来源:蒙特卡洛
根据行业专家的说法,2024 年注定将成为生成式 AI 的辉煌之年。操作性应用案例不断浮现,技术正在降低进入门槛,而通用人工智能显然就在不远处。
那么……这些事情发生了吗?
嗯,差不多。到了 2024 年底,部分预测已经应验,热乎乎的已经出现。其余的则需要在“烤箱”里再待一段时间(我在看你,通用人工智能)。
这是前瞻性思想家和投资者 Tomasz Tunguz 对 2024 年底数据与 AI 现状的看法——以及我自己的一些预测。
2025 年数据工程趋势来袭。
1. 我们生活在一个没有理由的世界里(Tomasz)
在我们的 AI 反乌托邦刚刚过去三年时,开始看到一些企业在我们预期的领域创造价值——但并非所有领域。根据 Tomasz 的说法,当前的 AI 状态可以总结为三个类别。
1. 预测:AI 副驾驶能够完成句子、修正代码错误等。
2. 搜索:利用数据语料库来回答问题的工具
3. 推理:一个多步骤的工作流程,能够完成复杂的任务
虽然 AI 副驾驶和搜索在企业组织中已经取得了一些成功(特别是前者),但推理模型似乎仍然滞后。根据 Tomasz 的说法,这背后有一个明显的原因。
模型准确性。
正如 Tomasz 所解释的那样,当前的模型在没有看到特定模式很多次之前,很难有效地将任务拆分为多个步骤。而对于这些模型可能被要求执行的大多数工作,情况正是如此。
“今天……如果要求一个大型模型生成一个财务规划与分析图表,它可以做到。但如果存在某些有意义的变化——例如,我们从软件计费转向基于使用量的计费——它就会迷失方向。”
所以目前来看,它的 AI 驾驶员和部分准确的搜索结果似乎是获胜的关键。
2. 过程 > 工具(Barr)
一项新工具的有效性取决于支持它的过程。
随着“现代数据架构”多年来的不断发展,数据团队有时发现自己处于一种不断尝试的状态。他们过于关注平台的什么,而没有足够关注(可能更重要的)如何。
但随着企业领域越来越接近生产就绪的 AI——如何将这些新工具投入运营变得更加紧迫。
让我们暂时考虑一下数据质量的例子。随着为 AI 提供数据的过程在 2024 年成为焦点,数据质量也随之步入了聚光灯下。面对生产就绪 AI 的现实可能性,企业数据领导者没有时间从数据质量菜单中挑选——这里做几个 dbt 测试,那里搞几个点解决方案。他们需要立刻交付价值,而且他们需要能够有效上手和部署的可信解决方案,今天。
当企业数据领导者在面对即将到来的生产就绪 AI 时,他们没有时间从数据质量菜单中挑选——这里做几个 dbt 测试,那里搞几个点解决方案。他们已经有责任交付业务价值,而且他们需要能够有效上手和部署的可信解决方案,今天。
现实是,你可能拥有市场上最先进的数据质量平台——最先进的自动化,最好的驾驶员,最闪亮的集成——但如果你无法快速让组织上线运行,那你其实不过是预算清单中的一项和桌面上的一个新标签。
在未来 12 个月内,我预计数据团队将更多倾向于选择经过验证的端到端解决方案,而非拼凑式的工具包,以优先解决更为关键的挑战,如数据质量的所有权、事件管理和长期领域赋能。
而能够实现这些优先事项的解决方案,将是 AI 领域的胜者。
3. AI 正在推动投资回报率——但不是收入(Tomasz)
像任何数据产品一样,生成型 AI 的价值表现为两种形式之一;降低成本或创造收入。
在收入方面,你可能会看到像 AI 销售开发代表、数据增强机器或推荐系统之类的工具。根据 Tomasz 的说法,这些工具可以生成大量销售线索……但它不会是一个健康的销售管道。所以,如果 AI 没有创造收入,它需要在削减成本方面发挥作用——在这方面,这项新兴技术显然已经找到了立足点。
“并不是很多公司通过这项技术获得了业务。大多数是通过降低成本。Klarna 裁掉了三分之二的员工。微软和 ServiceNow 的工程生产力提升了 50%-75%。”
根据 Tomasz 的说法,当满足以下三条标准之一时,AI 的应用场景就会带来成本降低的机会:
-
重复性工作
-
挑战性的劳动市场
-
紧急的招聘需求
Tomasz 提到的一个有效推动新收入的组织案例是 EvenUp——一家自动化需求函的交易法律公司。像 EvenUp 这样支持模板化但高度专业化服务的组织,可能在 AI 现有形式下处于一个独特的位置,能够看到更大的影响。
4. AI 的采用速度比预期慢——但领导者正在耐心等待时机(Tomasz)
与一年前被广泛接受的“AI 战略”潮流相比,今天的领导者似乎已一致地从这项技术中退后一步。
“去年有一波人尝试各种软件,只是为了看看效果。它们的董事会在询问 AI 战略。但现在,这波早期潮流已经经历了大量的波动。”
虽然一些组织从早期的实验中并没有看到价值,但其他组织却在其基础技术的快速发展中遇到了困难。根据 Tomasz 的说法,这是投资 AI 公司面临的最大挑战之一。问题不在于技术在理论上没有价值——而是组织尚未弄清楚如何在实践中有效地利用它。
Tomasz 认为,下一波采用将与第一波不同,因为领导者将更加清楚自己需要什么——以及在哪里可以找到它。
就像大秀前的彩排一样,团队知道他们在寻找什么,他们已经解决了一些与法律和采购相关的问题——尤其是数据丢失和防护——并且他们已准备好在合适的机会出现时采取行动。
明天的最大挑战是什么?“如何更快地发现和销售价值?”
5. 小数据是 AI 的未来(Tomasz)
开源与托管的争论由来已久……嗯,确实是很久以前的事了。但是,当涉及到 AI 时,这个问题变得更加复杂。
在企业层面,这不仅仅是控制或互操作性的问题——尽管这确实可以发挥一定作用——更是一个运营成本的问题。
尽管 Tomasz 认为最大的 B2C 公司将使用现成的模型,但他预计 B2B 将趋向于使用他们自己的专有和开源模型。
“在 B2B 领域,你会看到整体上更多的小型模型,整体上更多的开源。这是因为运行一个小型开源模型要便宜得多。”
但这并非全是利益。小型模型也能提高性能。像谷歌一样,大型模型旨在服务于多种用例。用户可以向大型模型询问几乎任何问题,因此该模型需要在足够大的数据集上进行训练,以便提供相关的响应。水球。中国历史。法式吐司。
不幸的是,模型训练的主题越多,它就越容易混淆多个概念——而且随着时间的推移,输出的错误也会越多。
“你可以拿像 Llama 2 这种具有 80 亿参数的模型,使用 10,000 个支持票进行微调,它将表现得更好,”Tomasz 说。
更重要的是,ChatGPT 和其他托管解决方案经常因其创建者没有合法权利来使用训练这些模型的数据而在法庭上受到挑战。
在许多情况下,这可能并非错误。
除了成本和性能之外,这也可能对专有模型的长期采用产生影响——特别是在高度监管的行业中——但这种影响的严重性仍然不确定。
当然,专有模型也没有坐视不管。如果 Sam Altman 有话要说的话。 (如果推特教给我们什么,那就是 Sam Altman 肯定有很多话要说。)
专有模型已经在积极降价以促进需求。像 ChatGPT 这样的模型已经将价格降低了大约 50%,并预计在接下来的 6 个月内再降低 50%。这种降价可能是 B2C 公司在人工智能军备竞赛中竞争所急需的福音。
6. 对于分析师和数据工程师来说,界限越来越模糊(巴尔)
当谈到扩大管道生产时,数据团队通常会遇到两个挑战:没有足够技术经验的分析师和没有足够时间的数据工程师。
听起来像是人工智能的问题。
在我们展望数据团队可能如何发展的同时,我相信有两个主要发展趋势可能会推动 2025 年工程与分析职责的整合:
-
增加的需求——随着企业领导者对数据和人工智能产品的需求增长,数据团队将被要求以更少的资源做更多的事情。为了尽量减少瓶颈,领导者将自然赋予之前专门化的团队更多的责任,负责他们的管道和相关方。
-
自动化的改进——新需求总是推动新创新。(在这种情况下,这意味着由人工智能驱动的管道。)随着技术自然变得更加自动化,工程师将能够以更少的资源做更多的事情,而分析师将能够独立做更多的事情。
论点很简单——随着需求的增加,管道自动化将自然发展以满足需求。随着管道自动化的发展,创建和管理这些管道的障碍将降低。技能差距将减少,增加新价值的能力将提高。
向自助式人工智能驱动的管道管理转变意味着每个人工作中最痛苦的部分将被自动化——而他们创造和展示新价值的能力在这个过程中得到了扩展。听起来像是一个美好的未来。
7. 合成数据很重要——但它是有代价的(Tomasz)
你可能见过一条蛇吞噬自己尾巴的图片。如果你仔细看,会发现它和当代人工智能有着惊人的相似之处。
目前互联网上大约有 21 到 25 万亿个词汇(tokens)。目前投入生产的人工智能模型已经使用了其中的所有数据。为了让数据继续发展,需要一个更庞大的数据集来进行训练。数据越多,模型所能获取的上下文信息就越多——从而输出的结果也会更加准确。
那么,当人工智能研究人员没有足够的训练数据时,他们该怎么办?
它们是自己制造的。
随着训练数据变得越来越稀缺,像 OpenAI 这样的公司认为合成数据将成为未来训练模型的重要组成部分。在过去的 24 个月里,一个完整的产业已经发展起来,以服务于这一愿景——包括像 Tonic 这样的公司,生成合成结构化数据,以及像 Gretel 这样的公司,专门为金融和医疗等受监管行业创建合规数据。
那么,合成数据是长期的解决方案吗?可能不是。
合成数据通过利用模型创建反映某人可能自然找到的数据集(在某个数据更多的替代现实中),然后用这些新数据来训练他们自己的模型。小规模来看,这实际上是很有道理的。正如他们所说,过犹不及……
你可以把它看作是情境性营养不良。就像食物一样,如果一个新鲜的有机数据源是模型训练中最有营养的数据,那么从现有数据集中提炼出来的数据,按其本质来说,必定比原始数据更少营养。
少量的人工调味料是可以接受的——但如果这种合成训练数据的饮食继续下去,而没有新的草饲数据被引入,那么模型最终会失败(或者至少,指甲床会明显不再吸引人)。
这不再是“是否”问题,而是“何时”问题。
根据 Tomasz 的说法,我们离模型崩溃还有很长一段路。然而,随着人工智能研究不断推动模型达到其功能极限,不难想象有一天人工智能将达到其功能的瓶颈——可能会比预期更早到来。
8. 非结构化数据堆栈将会出现(Barr)
在生产中利用非结构化数据的想法并不是什么新鲜事——但在人工智能的时代,非结构化数据已经扮演了一个全新的角色。
根据 IDC 的一份报告,目前只有大约一半的组织非结构化数据正在被分析。
这一切即将发生改变。
在生成性 AI 方面,企业成功在很大程度上依赖于用于训练、微调和增强 AI 的大量非结构化数据。随着越来越多的组织寻求将 AI 实际应用于企业用例,非结构化数据的热情——以及正在兴起的“非结构化数据堆栈”——也将持续增长。
一些团队甚至在探索如何使用额外的 LLM 来为非结构化数据添加结构,以便在更多的训练和分析用例中扩大其作用。
识别你组织中存在的非结构化一方数据——以及如何为你的利益相关者激活这些数据——是数据领导者展示其数据平台商业价值的一个蓝海机会(并希望能为优先事项争取一些额外预算)。
如果说 2024 年是探索非结构化数据潜力的一年,那么 2025 年将是实现其价值的一年。问题是……哪些工具将会脱颖而出?
9. 代理型 AI非常适合对话——但不适合部署(Tomasz)
如果你最近在风险投资圈子里活动,你很可能会听到几个术语频繁被提及:“副驾驶”是指用来完成单一步骤的 AI(“纠正我糟糕的代码”),而“代理”则是指多步骤工作流,它可以收集信息并利用这些信息执行任务(“写一篇关于我糟糕代码的博客,并发布到我的 WordPress”)。
毫无疑问,2024 年我们已经看到了 AI 副驾驶的很多成功案例(只要问问 Github、Snowflake、微软回形针等公司),但 AI 代理呢?
虽然“代理型 AI”在客户支持团队中引发了一些混乱,但看起来它短期内就只能停留在这个阶段。尽管这些早期的 AI 代理是一个重要的进步,但这些工作流的准确度仍然较低。
为了提供背景信息,75%-90%的准确度是 AI 的技术前沿。大多数 AI 相当于一名高中生。但如果你有三步分别为 75%-90%的准确度,最终的准确度大约为 50%。
我们已经训练大象用更高的准确度画画了。
远非为组织带来收入驱动,当前表现的绝大多数 AI 代理如果被投放到生产环境中,反而可能会带来负面影响。根据 Tomasz 的说法,我们首先需要解决这个问题。
能够谈论它们很重要,但没有人能在演示之外取得任何成功。因为无论硅谷的人们多么喜欢讨论 AI 代理,这些讨论并不会转化为实际的表现。
10. 管道正在扩展——但质量覆盖并没有跟上(Tomasz)
“在一次与多个 AI 负责人共进晚餐时,我问有多少人对输出结果的质量感到满意,结果没有人举手。在确保输出结果一致性方面,确实存在着质量挑战。”
每年,Monte Carlo 调查 真实数据专家关于数据质量状况的调查。今年,我们将目光投向了 AI 的阴影,结果非常明确。
数据质量风险在不断发展——但数据质量管理并未跟上。
“我们看到团队们正在大规模构建向量数据库或嵌入模型。大规模的 SQLLite。这些 1 亿个小数据库。它们开始在 CDN 层进行架构,以运行所有这些小型模型。iPhone 将拥有机器学习模型。我们将看到总数据管道数量的爆炸式增长,但每个管道的数据量会小得多。”
微调模式将在组织内部创造大量的数据管道。但管道越扩展,数据质量的问题就越难解决。
数据质量与管道的数量和复杂性成正比。你拥有的管道越多(而且越复杂),出现故障的机会就越多——而及时发现问题的可能性则越小。
+++
你怎么看?请联系 Barr,邮箱是 barr@montecarlodata.com**。我在听。
2025 年数据科学家成功所需的 12 项技能
这并不是全部关于 LLMs 和 AI 工具
·发表于 Towards Data Science ·22 分钟阅读·4 天前
--

来源:图像由作者制作,Claude 协助。
AI 领域正在以 2025 年火箭般的速度发展,紧跟其后变得越来越难!
你会保住目前的职位、被聘用、晋升,还是被解雇?这取决于你,以及你能多快适应变化。
这并不是说如果你不适应——你就会灭亡。
许多事情正在发生变化,但也有一些没有变化。了解哪些变化需要你关注是成功的关键。
是的,新的 AI 革命正在经济的各个领域迅速蔓延,涌现出大量新工具来提高生产力并自动化许多任务。如果你去年感到不知所措,那么最好系好安全带迎接另一场疯狂之旅。
那么,你应该如何应对这个总是加速的 AI 热潮和工具?
通过专注于重要的事情。
尽管 AI 工具非常闪亮且强大,但实际上会帮助你在职业生涯中成功的许多技能过去几十年甚至几百年几乎没有太大变化…
搜索数据的三大策略
从传统索引寻求到基于 AI 的语义搜索,每个软件工程师都应该知道的策略!
·发表于 Towards Data Science ·阅读时长 7 分钟·2024 年 12 月 21 日
--
背景 — 一切都是数据驱动的
你同意这个说法吗?技术已经深深融入我们日常生活的方方面面。我当然同意!
当你在亚马逊上购物时,你从数百万个商品中搜索,找到符合你期望的商品!当你在 Facebook、TikTok 或其他社交媒体应用上浏览时,你看到的内容非常有趣,难以停下来滚动。你是否曾经想过 Tinder 是如何找到匹配对象并将其推荐给数百万用户的?我最近读了一些关于推荐引擎的文章,来自Tinder 技术博客,从软件的角度来看,寻找朋友推荐的工作在大规模上是相当令人着迷的。
幕后,这些应用都依赖于高效地检索相关数据,以提供这些无缝的用户体验。
随着数据量的指数增长,快速、准确且智能地搜索数据的能力,成为现代软件工程中的一个关键组成部分。对于我们这些软件工程师和机器学习工程师来说,我们有工具可以利用,这样我们就可以应用正确的搜索策略,满足用户对速度和相关性的期望。无论是通过电子邮件查找用户资料,还是搜索文本标签,或其他…
2024 年初的 30 个 GitHub Python 项目

图片由Med Ahabchane提供,来自Pixabay
星标最多的代码库!
·发布于Towards Data Science ·14 分钟阅读·2024 年 1 月 4 日
--
2024 年新年快乐!作为新年里的第一篇文章,就像我以前做的那样,我非常好奇到目前为止最受欢迎的 Python 项目是什么。GitHub 无疑是最适合获取这些统计数据的地方。虽然并不是所有开源项目都会在这里维护,但没有其他地方能比这里更适合了。
这个排名设计得很简单,因为我将分享我的代码。现在,让我们来看一下如何用几行代码从 GitHub API 中获取排名列表。之后,我会用我的术语对这些项目进行分类,并添加一些简短的介绍。
前 30 名 GitHub 项目的分类如下:
-
2 个代码库:机器学习框架
-
3 个代码库:人工智能驱动的应用
-
8 个代码库:软件开发框架
-
2 个代码库:开发生产力工具
-
3 个代码库:有用的信息目录
-
8 个代码库:教育内容
-
4 个代码库:实际应用
GitHub 搜索 API
前 5 大地理空间数据 API 用于高级分析
探索 Overpass、Geoapify、Distancematrix.ai、Amadeus 和 Mapillary,用于高级映射和位置数据
·发表于 Towards Data Science ·阅读时长 18 分钟 ·2024 年 10 月 6 日
--

Kyle Glenn 在 Unsplash(来源:unsplash.com/es/@kylejglenn)
地理数据在许多分析中至关重要,使我们能够根据位置和空间模式做出决策。地理数据在一些项目中的应用示例包括预测房价、交通路线优化,或为企业制定市场营销策略。
然而,作为数据科学家,你将经常面临如何获取这些数据的挑战。在许多情况下,有公共来源提供地理数据的信息;然而,在许多情况下,他们提供的信息需要进行修正,以适应我们要进行的分析。
本文将评估五个最有用的 API,用于获取大规模地理数据。我们将评估它们的使用方式、优缺点以及它们提供的信息的主要应用。可以将本文视为这些 API 使用和应用的基础,便于你之后深入探索它们提供的所有工具。
1. Overpass
Overpass API 允许访问 OpenStreetMap 网站上可用的信息。OpenStreetMap 是一个开放的地理数据网站……
构建用户友好数据表的五大原则
设计直观可靠的数据表,让你的数据团队喜爱
·发表在Towards Data Science ·阅读 7 分钟·2024 年 10 月 13 日
--
在数据科学和分析领域工作七年,我创建并查询了许多数据表。有很多次我会想,“这一列是什么意思?”“为什么在表 A 和表 B 中有两列同名?我应该使用哪一个?”“这个表的粒度是多少?”等等。
如果你也面临同样的挫折,这篇文章就是为你而写的!
在本文中,我将分享五个原则,这些原则将帮助你创建让同事们欣赏的数据表。请注意,这是从数据科学家的角度撰写的。因此,它不会涵盖传统数据库设计最佳实践,而是专注于制作用户友好表格的策略。

由 DALL·E 创建的图像
I. 单一真相来源
为报告和分析维护每个关键数据点或指标的单一真相来源非常重要。不应该在多个表中重复逻辑。
为了方便起见,有时我们会在多个表中计算相同的指标。例如,总交易价值(GMV)的计算可能存在于客户表、月度财务报表、商家表中…
数据工程师的顶级职业网站
如何找到绝妙的远程工作并获得录用
·发布于 Towards Data Science ·8 分钟阅读·2024 年 8 月 11 日
--

图片来源:Arvind Menon via Unsplash
数据工程目前非常火热,这意味着有大量的就业机会。在本文中,我想探讨专为数据工程师设计的职业网站。我们将深入分析当前可用的数据工程职位的主要方面,以及你需要做什么才能在 2025 年之前进入这个领域。这对于初级 IT 人员和希望转行的中级软件工程师将会有所帮助。
为什么选择数据工程?
数据工程是一个充满活力且高度回报的领域,提供了接触数据各个方面的机会,从 API 和数据连接器到数据平台、商业智能以及市场上各种工具。
这个角色非常稳定,因为企业不断生成数据,确保了对数据工程师的持续需求。这个角色与机器学习(ML)紧密相连,因为你将负责创建和部署各种数据和机器学习管道。所以这项工作绝不会无聊!
这个领域远非平凡,并提供丰厚的报酬。高薪反映了构建强大数据平台的复杂性。它从收集需求和设计系统开始,要求…
顶级数据科学职业问题,答案揭晓
我已经做了三年多的数据科学家。这是大多数人想知道的关于这个领域的事情。
·发表于Towards Data Science ·阅读时间:6 分钟·2024 年 11 月 9 日
--

图片由Clay Banks提供,发布于Unsplash
数据科学家做什么工作?
当我告诉别人我是数据科学家时,大多数人既感到印象深刻,又感到困惑。
给人的印象是,这个职位如今被认为是如此高大上和有声望(尽管有些人仍然称我们为能够编程的统计学家)。
困惑的是……数据科学到底意味着什么?我们究竟在做什么?
好吧,这取决于情况。
关于领域、公司以及团队本身。
但通常来说,数据科学涵盖了以下几个工作类别:
-
数据库与数据工程 — 许多数据科学家与数据库紧密合作,无论是加载和查询大量数据、构建数据管道,还是清理和准备数据以供分析。在我之前的公司,我经常使用 SQL 访问我们的数据库,查询构建机器学习模型所需的数据。我还发现自己在创建和修改表格,以便存储模型和其他分析的结果。
-
数据分析与可视化 — 数据可视化不仅仅是分析数据,还包括…
RAG 失败的顶级评估指标

图像由作者使用 Dall-E 3 创建
使用检索和响应指标排查 LLM 和检索增强生成问题
·发表于Towards Data Science ·7 分钟阅读·2024 年 2 月 2 日
--

图 1:LLM RAG 应用的根本原因工作流程(图表由作者创建)
如果你一直在使用大语言模型(LLM)进行搜索和检索任务,你很可能遇到过检索增强生成(RAG)这一技术,用于向 LLM 生成的响应中添加相关的上下文信息。通过将 LLM 与私有数据连接,RAG 可以通过在上下文窗口中提供相关数据来增强响应效果。
RAG 已被证明在复杂查询解答、知识密集型任务以及提高 AI 模型响应的精度和相关性方面非常有效,特别是在单独的训练数据可能不足的情况下。
然而,只有在持续监控 LLM 系统的常见故障点——特别是响应和检索评估指标时,才能获得 RAG 的这些好处。在本文中,我们将介绍用于排查检索和响应指标不佳的最佳工作流程。
排查检索和响应问题
值得记住的是,RAG 在所需信息易于获取时效果最佳。相关文档的可用性将 RAG 系统评估集中在两个关键方面:
-
检索评估: 评估检索到的文档的准确性和相关性
-
响应评估: 衡量在提供上下文时,系统生成的响应的适当性

图 2:LLM 应用中的响应评估与检索评估(图像由作者提供)
表 1:响应评估指标

表 1 由作者提供
表 2:检索评估指标

表 2 由作者提供
排除 RAG 工作流程中的问题
让我们回顾三种潜在场景,以根据流程图排除 LLM 性能不佳的问题。
场景 1:良好响应,良好检索

图示 由作者提供
在这种情况下,LLM 应用中的一切都按预期工作,我们得到了一个良好的响应和有效的检索。我们发现我们的响应评估是“正确的”,并且我们的“命中 = 真”。命中是一个二元度量,“真”意味着相关文档已被检索,而“假”则意味着未检索到相关文档。请注意,命中的汇总统计量是命中率(具有相关上下文的查询百分比)。
对于我们的响应评估,正确性是一个评估标准,可以通过简单地结合输入(查询)、输出(响应)和上下文来完成,具体如表 1所示。多个评估标准不需要用户标注的真实标签,因为大型语言模型(LLM)也可以使用像OpenAI 函数调用这样的工具来生成标签、分数和解释,下面是一个示例提示模板。

图像 由作者提供
这些LLM 评估可以格式化为数值型、类别型(二元和多类)和多输出型(多个分数或标签)——其中类别型-二元是最常用的,数值型则是最不常用的。
场景 2:错误响应,错误检索

图示 由作者提供
在这种情况下,我们发现响应是错误的,且相关内容未被接收。根据查询,我们看到内容没有接收,因为查询没有解决方案。无论提供什么文档,LLM 都无法预测未来的购买。然而,LLM 可以生成一个比幻觉答案更好的响应。在这里,我们可以通过简单地在 LLM 提示模板中添加一行“如果未提供相关内容且未找到结论性解决方案,请回答答案未知”来进行实验。在某些情况下,正确的答案就是答案不存在。

图示 由作者提供
场景 3:错误响应,混合检索指标
在第三种场景中,我们看到一个错误的响应和混合检索指标(相关文档已被检索,但 LLM 由于接收到过多信息而产生了幻觉)。

图表由作者提供
要评估 LLM RAG 系统,您需要既能获取正确的上下文,又能生成合适的答案。通常,开发人员会嵌入用户查询并用它来搜索向量数据库中的相关片段(见图 3)。检索性能不仅依赖于返回的片段与查询在语义上的相似性,还依赖于这些片段是否提供足够的相关信息来生成正确的查询响应。现在,您需要配置 RAG 系统的参数(检索类型、片段大小和 K 值)。

图 3:RAG 框架(作者提供)
类似于我们上一个场景,我们可以尝试编辑提示模板或更换用于生成响应的 LLM。由于在文档检索过程中已检索到相关内容,但 LLM 没有将其显示出来,这可能是一个快速的解决方案。下面是运行经过修订的提示模板(在调整提示变量、LLM 参数和提示模板本身后)生成的正确响应示例。

图表由作者提供
在排查混合性能指标的错误响应时,我们首先需要弄清楚哪些检索指标表现不佳。实现这一点最简单的方法是设置阈值和监控器。一旦您收到某个性能不佳的指标提醒,就可以通过特定的工作流程来解决问题。以 nDCG 为例,nDCG 用于衡量排名靠前文档的有效性,并考虑相关文档的位置。因此,如果您检索到相关文档(命中 = ‘True’),则需要考虑实现重新排序技术,将相关文档靠近排名靠前的搜索结果。
在我们当前的场景中,我们检索到了一份相关文档(命中 = ‘True’),且该文档位于第一位,因此让我们尝试提升精确度(相关文档的百分比),直到检索到‘K’份文档。目前我们的 Precision@4 是 25%,但如果只使用前两份相关文档,则 Precision@2 = 50%,因为一半的文档是相关的。这个变化导致了 LLM 的正确响应,因为它获取到的信息较少,但比例上却更相关。

图表由作者提供
本质上,我们在这里看到的是 RAG 中一个常见的问题,称为中途迷失,当你的 LLM 被过多不总是相关的信息压倒时,便无法给出最优的答案。从我们的图示中可以看出,调整分块大小是许多团队在改善 RAG 应用时首先做的事情,但这并不总是直观的。面对上下文溢出和中途迷失的问题,更多的文档并不总是更好,重排序也不一定能提高性能。为了评估哪种分块大小最有效,你需要定义一个评估基准,并对不同的分块大小和 top-k 值进行一遍扫查。除了实验不同的分块策略外,测试不同的文本提取技术和嵌入方法也会提升整体 RAG 性能。
响应和检索评估指标总结
这篇文章中的响应和检索评估指标与方法提供了一种全面的方式来查看 LLM RAG 系统的表现,指导开发者和用户理解其优点和局限性。通过不断根据这些指标评估这些系统,可以改进 RAG 的能力,提供准确、相关和及时的信息。
改进 RAG 的其他高级方法包括重排序、元数据附件、测试不同的嵌入模型、测试不同的索引方法、实施HyDE、实施关键词搜索方法,或实施 Cohere 文档模式(类似于 HyDE)。请注意,虽然这些更高级的方法——如分块、文本提取、嵌入模型实验——可能会产生更具上下文一致性的块,但这些方法会更消耗资源。将 RAG 与高级方法结合使用可以提升 LLM 系统的性能,并且只要检索和响应指标得到适当监控和维护,性能提升将会持续。
有问题吗?请通过这里或者在 LinkedIn、 X,或 Slack与我联系!
自然语言处理推荐系统中的主题对齐
利用主题建模将用户查询与文档主题对齐,从而提高基于自然语言处理(NLP)系统中推荐的相关性和上下文准确性。
·发表于 Towards Data Science ·阅读时间 16 分钟·2024 年 10 月 11 日
--

图片由 埃马纽埃尔·伊克韦布 提供,来源:Unsplash
介绍
随着大型语言模型(LLM)的能力不断提升,例如 ChatGPT 和 Llama,越来越多的研究开始集中在将语义推理适应这些系统上。尽管这些模型在提供基于人类先验知识预测的回应时表现出色,但仍然常见一些问题,如幻觉现象、泛化答案,以及不能完全满足用户需求的回答。推荐系统与大型语言模型类似,通过用户输入提供建议。今天,我们将探讨在查询中加入额外的主题元数据时,如何提升推荐系统的响应,尤其是这些主题与生成响应所使用的数据之间的对齐情况。
这项研究非常重要,因为它最终可能通过结合类似人类的推理能力来推断信息中的整体主题,从而增强大型语言模型(LLM)的语义深度。
主题建模…
使用 OpenAlex API 进行开源研究的主题建模
开源情报(OSINT)是能够为组织带来巨大价值的一项技术。从分析社交媒体数据、网络数据或全球研究中获得的洞察,可以在支持各种分析时发挥重要作用。本文将概述如何使用主题建模来帮助我们理解成千上万的开源研究资料。
·发表于Towards Data Science ·阅读时长 9 分钟·2024 年 7 月 15 日
--

“使用纸张…… #research# #proposal”由catherinecronin授权,采用 CC BY-SA 2.0 许可。要查看此许可证的副本,请访问creativecommons.org/licenses/by-sa/2.0/?ref=openverse.
什么是主题建模?
主题建模是一种无监督的机器学习技术,用于分析文档并通过语义相似性识别“主题”。这类似于聚类,但并非每个文档都独立属于一个主题。它更多的是将语料库中的内容进行分组。主题建模有许多不同的应用,但主要用于更好地理解大量文本数据。

作者提供的图片
例如,一家零售连锁可以对客户调查和评论进行建模,以识别负面评论并深入分析客户提到的关键问题。在这个案例中,我们将导入大量的文章和摘要,以理解数据集中的关键主题。
注意:在大规模应用中,主题建模可能会消耗大量计算资源。在这个示例中,我使用了 Amazon Sagemaker 环境,以利用其 CPU 的优势。
OpenAlex
OpenAlex 是一个免费使用的全球研究目录系统。他们已经索引了超过 2.5 亿篇新闻、文章、摘要等。
[## OpenAlex
编辑描述
幸运的是,他们提供了一个免费的(但有限制)灵活 API,可以让我们快速获取成千上万篇文章,并且应用过滤条件,如年份、媒体类型、关键词等。
创建数据管道
在我们从 API 获取数据时,我们会应用一些标准。首先,我们只会获取年份在 2016 到 2022 年之间的文档。我们希望使用相对较新的语言,因为某些主题的术语和分类法会随着时间的推移发生变化。
我们还将添加关键术语并进行多次搜索。虽然通常我们可能会随机获取不同的主题领域,但我们将使用关键术语来缩小搜索范围。通过这种方式,我们可以了解有多少个高级主题,并将其与模型的输出进行比较。下面,我们创建一个可以添加关键术语并通过 API 进行搜索的函数。
import pandas as pd
import requests
def import_data(pages, start_year, end_year, search_terms):
"""
This function is used to use the OpenAlex API, conduct a search on works, a return a dataframe with associated works.
Inputs:
- pages: int, number of pages to loop through
- search_terms: str, keywords to search for (must be formatted according to OpenAlex standards)
- start_year and end_year: int, years to set as a range for filtering works
"""
#create an empty dataframe
search_results = pd.DataFrame()
for page in range(1, pages):
#use paramters to conduct request and format to a dataframe
response = requests.get(f'https://api.openalex.org/works?page={page}&per-page=200&filter=publication_year:{start_year}-{end_year},type:article&search={search_terms}')
data = pd.DataFrame(response.json()['results'])
#append to empty dataframe
search_results = pd.concat([search_results, data])
#subset to relevant features
search_results = search_results[["id", "title", "display_name", "publication_year", "publication_date",
"type", "countries_distinct_count","institutions_distinct_count",
"has_fulltext", "cited_by_count", "keywords", "referenced_works_count", "abstract_inverted_index"]]
return(search_results)
我们进行 5 次不同的搜索,每次搜索涉及不同的技术领域。这些技术领域受到国防部“关键技术领域”的启发。详情请见:
[## USD(R&E)战略愿景与关键技术领域 - 国防部研究与工程,OUSD(R&E)
OUSD(R&E)与军事服务部门、战区司令部、工业界、学术界以及其他利益相关方紧密合作……
这是一个使用所需 OpenAlex 语法进行搜索的示例:
#search for Trusted AI and Autonomy
ai_search = import_data(35, 2016, 2024, "'artificial intelligence' OR 'deep learn' OR 'neural net' OR 'autonomous' OR drone")
在整理我们的搜索结果并去除重复文档之后,我们必须清理数据,为主题模型做准备。当前输出存在两个主要问题。
-
摘要以倒排索引的形式返回(出于法律原因)。然而,我们可以利用这些索引返回原始文本。
-
一旦我们获得原始文本,它将是未经处理的原始数据,产生噪声并影响我们的模型表现。我们将进行传统的 NLP 预处理,以便为模型做好准备。
以下是一个从倒排索引中返回原始文本的函数。
def undo_inverted_index(inverted_index):
"""
The purpose of the function is to 'undo' and inverted index. It inputs an inverted index and
returns the original string.
"""
#create empty lists to store uninverted index
word_index = []
words_unindexed = []
#loop through index and return key-value pairs
for k,v in inverted_index.items():
for index in v: word_index.append([k,index])
#sort by the index
word_index = sorted(word_index, key = lambda x : x[1])
#join only the values and flatten
for pair in word_index:
words_unindexed.append(pair[0])
words_unindexed = ' '.join(words_unindexed)
return(words_unindexed)
现在我们已经得到了原始文本,我们可以进行传统的预处理步骤,如标准化、去除停用词、词形还原等。下面是可以映射到一系列文档的函数。
def preprocess(text):
"""
This function takes in a string, coverts it to lowercase, cleans
it (remove special character and numbers), and tokenizes it.
"""
#convert to lowercase
text = text.lower()
#remove special character and digits
text = re.sub(r'\d+', '', text)
text = re.sub(r'[^\w\s]', '', text)
#tokenize
tokens = nltk.word_tokenize(text)
return(tokens)
def remove_stopwords(tokens):
"""
This function takes in a list of tokens (from the 'preprocess' function) and
removes a list of stopwords. Custom stopwords can be added to the 'custom_stopwords' list.
"""
#set default and custom stopwords
stop_words = nltk.corpus.stopwords.words('english')
custom_stopwords = []
stop_words.extend(custom_stopwords)
#filter out stopwords
filtered_tokens = [word for word in tokens if word not in stop_words]
return(filtered_tokens)
def lemmatize(tokens):
"""
This function conducts lemmatization on a list of tokens (from the 'remove_stopwords' function).
This shortens each word down to its root form to improve modeling results.
"""
#initalize lemmatizer and lemmatize
lemmatizer = nltk.WordNetLemmatizer()
lemmatized_tokens = [lemmatizer.lemmatize(token) for token in tokens]
return(lemmatized_tokens)
def clean_text(text):
"""
This function uses the previously defined functions to take a string and\
run it through the entire data preprocessing process.
"""
#clean, tokenize, and lemmatize a string
tokens = preprocess(text)
filtered_tokens = remove_stopwords(tokens)
lemmatized_tokens = lemmatize(filtered_tokens)
clean_text = ' '.join(lemmatized_tokens)
return(clean_text)
现在我们已经有了一系列经过预处理的文档,我们可以创建我们的第一个主题模型!
创建主题模型
对于我们的主题模型,我们将使用 gensim 创建一个潜在狄利克雷分配(LDA)模型。LDA 是最常见的主题建模模型,因为它在识别语料库中的高层次主题方面非常有效。下面是用于创建模型的包。
import gensim.corpora as corpora
from gensim.corpora import Dictionary
from gensim.models.coherencemodel import CoherenceModel
from gensim.models.ldamodel import LdaModel
在创建模型之前,我们必须准备我们的语料库和 ID 映射。这只需要几行代码就能完成。
#convert the preprocessed text to a list
documents = list(data["clean_text"])
#seperate by ' ' to tokenize each article
texts = [x.split(' ') for x in documents]
#construct word ID mappings
id2word = Dictionary(texts)
#use word ID mappings to build corpus
corpus = [id2word.doc2bow(text) for text in texts]
现在我们可以创建一个主题模型。如你所见,下面有许多不同的参数会影响模型的表现。你可以在 gensim 的文档中阅读关于这些参数的更多信息。
#build LDA model
lda_model = LdaModel(corpus = corpus, id2word = id2word, num_topics = 10, decay = 0.5,
random_state = 0, chunksize = 100, alpha = 'auto', per_word_topics = True)
最重要的参数是主题的数量。在这里,我们设置了一个任意的 10。由于我们不知道应该有多少个主题,这个参数肯定需要优化。但我们如何衡量模型的质量呢?
这时,连贯性得分就显得尤为重要。连贯性得分是一个介于 0 和 1 之间的量度。连贯性得分通过确保主题是合理且独特的来衡量我们主题的质量。我们希望在定义明确的主题之间有清晰的边界。虽然这在最终上是有些主观的,但它能让我们对结果的质量有一个很好的了解。
#compute coherence score
coherence_model_lda = CoherenceModel(model = lda_model, texts = texts, dictionary = id2word, coherence = 'c_v')
coherence_score = coherence_model_lda.get_coherence()
print(coherence_score)
这里,我们得到了大约 0.48 的连贯性得分,虽然不是太差!但还不能投入生产使用。
可视化我们的主题模型
主题模型可能很难进行可视化。幸运的是,我们有一个很棒的模块‘pyLDAvis’,它能够自动生成一个交互式可视化,允许我们在向量空间中查看主题,并深入探讨每个主题。
import pyLDAvis
#create Topic Distance Visualization
pyLDAvis.enable_notebook()
lda_viz = pyLDAvis.gensim.prepare(lda_model, corpus, id2word)
lda_viz
如下所示,这生成了一个很好的可视化图,我们可以快速了解模型的表现。通过查看向量空间,我们可以看到一些主题是明确且定义良好的。然而,也有一些主题是重叠的。

图片来源:作者
我们可以点击一个主题查看最相关的词汇。当我们调整相关性度量(lambda)时,可以通过将其向左滑动来查看特定于主题的词汇,而通过将其向右滑动则可以看到相关但不那么特定于主题的词汇。
当点击每个主题时,我可以隐约看到我最初搜索的主题。例如,主题 5 似乎与我的‘人机界面’搜索一致。也有一簇看起来与生物技术相关的主题,但有些更为明确,有些则不太清楚。
优化主题模型
从 pyLDAvis 界面和我们的连贯性得分 0.48 来看,肯定还有提升的空间。在我们的最后一步,先编写一个函数,让我们可以循环通过不同参数的值并尝试优化我们的连贯性得分。下面是一个测试不同主题数量和衰减率值的函数。该函数为每一组参数组合计算连贯性得分并将其保存在数据框中。
def lda_model_evaluation():
"""
This function loops through a number of parameters for an LDA model, creates the model,
computes the coherenece score, and saves the results in a pandas dataframe. The outputed dataframe
contains the values of the parameters tested and the resulting coherence score.
"""
#define empty lists to save results
topic_number, decay_rate_list, score = [], [], []
#loop through a number of parameters
for topics in range(5,12):
for decay_rate in [0.5, 0.6, 0.7]:
#build LDA model
lda_model = LdaModel(corpus = corpus, id2word = id2word, num_topics = topics, decay = decay_rate,
random_state = 0, chunksize = 100, alpha = 'auto', per_word_topics = True)
#compute coherence score
coherence_model_lda = CoherenceModel(model = lda_model, texts = texts, dictionary = id2word, coherence = 'c_v')
coherence_score = coherence_model_lda.get_coherence()
#append parameters to lists
topic_number.append(topics)
decay_rate_list.append(decay_rate)
score.append(coherence_score)
print("Model Saved")
#gather result into a dataframe
results = {"Number of Topics": topic_number,
"Decay Rate": decay_rate_list,
"Score": score}
results = pd.DataFrame(results)
return(results)
通过将几个小范围的值传递给两个参数,我们找出了能够将我们的连贯性得分从 0.48 提高到 0.55 的参数,取得了显著的改善。
下一步
为了继续构建一个生产级别的模型,我们需要在参数上进行大量实验。由于 LDA 计算开销较大,我将上述实验限定在比较大约 20 个不同的模型。但如果有更多的时间和计算能力,我们可以比较数百个模型。
此外,我们的数据管道也有待改进。我注意到有几个词可能需要添加到停用词列表中。像“use”和“department”这样的词并没有提供任何语义价值,特别是对于不同技术领域的文档来说。还有一些技术术语未能正确处理,结果变成了单个字母或一组字母。我们可以花一些时间做一个词袋分析,以识别这些停用词机会。这将有助于减少数据集中的噪音。
结论
在本文中,我们:
-
了解了主题建模和 OpenAlex 数据源的基本概念
-
构建了一个数据管道,用于从 API 中获取数据并为 NLP 模型做准备
-
构建了一个 LDA 模型,并使用 pyLDAvis 可视化结果
-
编写了代码来帮助我们找到最优参数
-
讨论了模型改进的下一步
这是我在 Medium 上的第一篇文章,希望你喜欢。请随时留下反馈、提问或提出其他话题的请求!
使用 BERTtopic 进行 Python 中的主题建模
使用最先进的基于 Transformer 的主题模型进行政治言论建模的实践教程
·发布于Towards Data Science ·5 分钟阅读·2024 年 4 月 1 日
--

主题建模(即在文本数据语料库中进行主题识别)自隐含狄利克雷分配(LDA)模型发布以来,发展迅速。然而,这一经典的主题模型并未很好地捕捉词语之间的关系,因为它基于词袋模型的统计概念。近年来,基于嵌入的Top2Vec和BERTopic模型通过利用预训练语言模型生成主题,从而解决了这一缺点。
在本文中,我们将使用Maarten Grootendorst(2022)的BERTopic来识别政治演讲稿中的代表性术语。它在各种语料库中的主题建模指标上优于大多数传统和现代主题模型,并已被公司、学术界(Chagnon,2024)以及公共部门广泛使用。我们将在 Python 代码中进行探讨:
-
如何有效地预处理数据
-
如何创建一个二元组主题模型
-
如何探索随时间变化的最频繁术语。
1. 示例数据
作为一个示例数据集,我们将使用Empoliticon: 政治演讲-语境与情感数据集,该数据集发布于…
话题建模你的个人数据
使用传统模型和变换器模型探索由数据经纪人存储的个人数据
·发表于 Towards Data Science ·阅读时长 24 分钟·2024 年 9 月 21 日
--

图像由作者使用 ChatGPT 4o 和 DALL-E-3 创建
在之前的文章中,我描述了如何访问存储并被我们每天接触的前线消费者公司使用的个人数据。这些公司包括零售商、社交媒体、电信提供商、金融服务公司等。我探讨了如何利用各种机器学习模型和可视化工具,发现这些公司如何看待我们。
在撰写那篇文章的过程中,我发现前线公司经常将我们的个人数据与另一类公司共享,这些公司通常被称为数据经纪人或数据聚合商(以下简称聚合商)。聚合商通过其他类型的数据增强我们的数据,这些数据来源于公共记录、其他聚合商以及类似来源,从而创建我们的个人档案。然后,他们将这些档案出售给面向消费者的公司和其他组织,用于营销或其他目的。
我的好奇心被激发了:这些聚合商到底存储了哪些类型的数据?他们存储了多少特征?是否有一些主要类型的数据是个别聚合商重点关注的?如果有的话,这能告诉我他们的最终客户是什么样的?这些最终客户属于哪些行业,他们需要哪些个人数据…
Torch 编译:以低努力实现 2 倍更快的 Llama 3.2
但这将取决于你的 GPU。
·发表于 Towards Data Science ·阅读时间 5 分钟·2024 年 11 月 13 日
--

图像由 ChatGPT 生成。
Torch 编译(torch.compile)首次在 PyTorch 2.0 中引入,但在经历了几次更新和优化之后,才可以稳定地支持大多数大型语言模型(LLM)。
在推理时,torch.compile 可以通过仅小幅增加内存使用量来显著加速解码过程。
在本文中,我们将探讨 torch.compile 的工作原理,并衡量它对 LLM 推理性能的影响。要在代码中使用 torch.compile,你只需添加一行代码。对于本文,我用 Llama 3.2 进行了测试,并尝试了 bitsandbytes 量化,使用了两种不同的 GPU:Google Colab 的 L4 和 A100。
我还创建了一个笔记本,演示如何使用torch.compile并对其性能进行基准测试,点击这里查看:
Torch 编译:它如何让模型更快?
torch.compile 提供了一种通过将标准 PyTorch 代码转换为优化过的机器码来加速模型的方法。这种方法称为 JIT(即时)编译,使代码能够在特定硬件上更高效地运行,即比普通 Python 代码更快。它对于复杂模型尤其有效,即便是微小的速度提升也能产生显著效果……
图谱的泛化探索:从不变性到因果性
这篇博客分享了关于图结构数据的分布外泛化的最新论文
·发布于数据科学前沿 ·阅读时间:14 分钟·2024 年 7 月 18 日
--

图像由 GPT-4 生成
这篇博客介绍了图上的分布外泛化的最新进展,这是机器学习中一个重要但尚未深入探索的问题。我们将首先介绍问题的表述以及涉及图上分布变化的典型场景。然后,我们将概述三篇最近发布的论文(我是作者之一):
处理图上的分布变化:一种不变性视角,ICLR2022。
图谱的分布外泛化通过因果干预,WWW2024。
学习分歧场以获得抗偏移的图表示,ICML2024。
这些工作聚焦于通过不变性原理和因果干预的视角来研究图上的泛化问题。此外,我们将比较这些方法并讨论该领域未来可能的发展方向。
图机器学习仍然是一个热门的研究方向,尤其是在 AI4Science 浪潮的推动下,图数据的应用领域越来越广泛。与普通的图像和文本数据不同,图是一种数学抽象,描述了实体的属性以及它们在系统中的相互作用。在这方面,图不仅可以表示不同规模的现实世界物理系统(如分子、蛋白质相互作用、社交网络等),还可以描述某些抽象的拓扑关系(如场景图、工业过程、思维链等)。
如何构建通用的图数据基础模型是一个最近受到广泛关注的研究问题。尽管现有方法如图神经网络(GNNs)和图变换器在表示能力上表现强大,但图结构数据上机器学习模型的泛化能力仍然是一个尚未深入探索的开放问题[1, 2, 3]。一方面,图数据中涉及的非欧几里得空间和几何结构显著增加了建模的难度,使得现有的旨在增强模型泛化能力的方法难以成功[4, 5, 6]。另一方面,图数据中的分布转移,即训练数据和测试数据之间的分布差异,源于更为复杂的引导因素(如拓扑结构)和外部环境,使得这个问题更加难以研究[7, 8]。

泛化挑战旨在处理从训练到测试的分布转移。
问题与动机
开放世界中的分布转移
泛化问题至关重要,因为在现实场景中,模型通常需要与一个开放、动态且复杂的环境进行交互。在实际情况下,由于观察和资源的限制,训练数据无法涵盖所有可能的环境,模型也无法在训练过程中预见到所有潜在的未来情形。然而,在测试阶段,模型很可能会遇到与训练分布不一致的样本。分布外泛化(OOD)问题的关键焦点是如何使机器学习模型在测试数据上表现良好,即使这些测试数据超出了训练分布。

涉及图数据分布转移的典型场景要求机器学习模型从有限的训练数据泛化到新的测试分布。来自 Medium 博客的图片:时序图网络 和 对流扩散变换器
在这种情况下,由于测试数据/分布在训练过程中是严格未见/未知的,因此关于数据生成的结构假设是必要的前提。相反,如果没有任何数据假设,分布外泛化将是不可能的(无免费午餐定理)。因此,必须事先明确指出,OOD 问题的研究目标不是消除所有假设,而是 1)在合理的假设下最大化模型的泛化能力,2)适当增加/减少假设,以确保模型能够处理某些分布转移。
图上的分布外泛化
一般的分布外(OOD)问题可以简单地描述为:
当 p(x,y|train)≠p(x,y|test) 时,如何设计有效的机器学习方法?
在这里,我们遵循文献中常用的设定,假设数据分布由潜在的环境控制。因此,在给定环境 e 下,数据生成可以写作 (x,y)∼p(x,y|e)。对于 OOD 问题,训练和测试数据可以假定来自不同的环境。因此,这个问题可以进一步阐述为
如何学习一个预测模型 f,使其在所有环境 e∈E 中表现(同样)良好?
具体来说,对于图结构数据,输入数据还包含结构信息。在这方面,根据图结构存在的形式,问题可以进一步分为两类:节点级任务和图级任务。下图展示了在这两种任务类型下 OOD 问题的公式化。

图上的 OOD 泛化形式,其中我们进一步区分了图级任务和节点级任务,这些任务在图结构的形式上有所不同。具体来说,对于节点级任务,由于图结构引入了节点实例之间的相互依赖关系,[5] 提出将整个图分割为以节点为中心的自我图(ego-graphs),这些自我图可以被视为独立的输入。
如前所述,OOD 问题需要关于数据生成的某些假设,这些假设为构建具有泛化能力的机器学习方法铺平了道路。接下来,我们将具体介绍两类方法,分别利用不变性原理和因果干预来实现图上的分布外泛化。
通过不变性原理的泛化
基于不变性原理的学习方法,通常被称为不变学习 [9, 10, 11],旨在设计新的学习算法,引导机器学习模型利用数据中的不变关系。不变关系特别指的是从输入 x 和标签 y 中获得的预测关系,这些关系在所有环境中普遍适用。因此,当预测模型 f(例如神经网络)成功学习到这些不变关系时,它能够跨不同环境中的数据进行泛化。相反,如果模型学习到的是虚假的相关性,即仅在某些环境中成立的从 x 和 y 获得的预测关系,那么过度提高训练准确性将误导预测模型过拟合数据。
根据上述说明,我们注意到不变学习依赖于数据生成中的不变假设,即在不同环境中,x 和 y 之间存在一个保持不变的预测关系。数学上,这可以被公式化为:
存在一个映射 c,使得 z=c(x) 满足 p(y|z,e)=p(y|z),∀e∈E。
在这方面,我们自然有两个后续问题:i) 如何在图上定义不变假设?ii) 对于常见的图数据,这个假设是否合理?
接下来,我们介绍最近的论文[5],吴等人,"处理图上的分布变化:一种不变性视角”(ICLR2022)。该论文提出将不变性原则应用于图上的分布外泛化,并提出了图数据的不变假设。
图上的不变假设
受图同构测试中的 Weisfeiler-Lehman 算法启发,[5]考虑了以每个节点为中心的自我图,并描述了所有节点特征在自我图中对中心节点标签的贡献。后者被具体分解为不变特征和虚假特征。这一定义兼顾了拓扑结构,同时也允许足够的灵活性。下图展示了[5]中定义的不变假设,并提供了一个引用网络的示例。

图上的不变假设(左)和引用网络的示例(右)。在引用网络中,每个节点代表一篇论文,待预测的标签 y 是论文的研究领域。节点特征 x 包括论文的发表场所(x1)和引用指数(x2),环境(e)是发表时间。在这个例子中,x1 是一个不变特征,因为它与 y 的关系与环境无关。相反,x2 是一个虚假特征;尽管它与 y 有很强的相关性,但这种相关性会随时间变化。因此,在这种情况下,一个理想的预测器应该利用 x1 中的信息,以实现跨不同环境的泛化。图像来自论文。
提出的方法:探索-外推风险最小化
在不变假设下,一种自然的方法是对不同环境下的损失差异进行正则化,以促进学习不变关系。然而,现实世界中的数据通常缺乏环境标签,即每个实例与其环境之间的对应关系未知,这使得无法直接计算不同环境间的损失差异。为了解决这个挑战,[5]提出了探索-外推风险最小化(EERM),该方法引入了 K 个上下文生成器来扩展和多样化输入数据,从而模拟来自不同环境的输入数据。通过理论分析,[5]证明了新的学习目标可以保证在所提出的分布外泛化问题上得到最优解。

探索-外推风险最小化(EERM)由[5]提出,其中内在目标是最大化由 K 个上下文生成器生成的数据的“多样性”,外在目标则涉及使用来自 K 个生成的(虚拟)环境的数据来计算损失的均值和方差,用于训练预测模型。
除了生成(虚拟)环境外,最近的另一项研究[12]提出从观察数据推断潜在环境,并引入了一个额外的环境推断模型,在训练过程中与预测器一起迭代优化。同时,[13]通过数据增强处理分布外的泛化,使用不变性原理来指导数据增强过程,从而保留不变的特征。
因果干预下的泛化
不变学习要求假设数据中存在可以学习的不变关系。这在一定程度上限制了此类方法的适用性,因为模型只能在与训练数据共享某些不变性的测试数据上可靠地泛化。对于违反此条件的分布外测试数据,模型的泛化性能仍然未知。
接下来,我们介绍最近的工作[14]提出的另一种方法,Wu 等人,“通过因果干预实现图的分布外泛化” (WWW2024)。本文旨在通过因果干预的视角解决分布外泛化问题。与不变学习不同,该方法不依赖于数据生成中的不变假设。相反,它通过学习算法引导模型从 x 到 y 学习因果关系。
图学习的因果视角
首先,让我们考虑由机器学习模型(如图神经网络)通常引起的变量之间的因果依赖关系。我们有输入 G(例如,图中每个节点中心的自我图),标签 Y,以及影响数据分布的环境 E。在使用标准监督学习目标(例如,经验风险最小化或等效的最大似然估计)进行训练后,它们之间的依赖关系在下图中展示。

在因果图中,有三条依赖路径:i) 由预测器引起的从 G 到 Y 的路径;ii) 由数据生成定义给出的从 E 到 G 的路径;iii) 由模型训练引导的从 E 到 Y 的路径。
上面的因果图揭示了传统训练方法的局限性,特别是它们无法实现分布外的泛化。在这里,输入 G 和标签 Y 是环境 E 的结果,暗示它们由于这个混杂因素而相关。在训练过程中,模型不断拟合训练数据,导致预测器 f 学习到输入和标签之间的虚假关联,这些关联是特定于某一环境的。
[14]通过一个社交网络示例来说明这一学习过程。假设我们需要预测社交网络中用户(节点)的兴趣,其中注意到用户兴趣受到年龄和社交圈等因素的显著影响。因此,如果一个预测模型在大学社交网络的数据上训练,它可能很容易预测用户对“篮球”的兴趣,因为在大学环境中,由于环境本身的影响,用户对篮球的兴趣比例较高。然而,当模型转移到 LinkedIn 社交网络时,这种预测关系可能不再成立,因为 LinkedIn 的用户年龄和兴趣更加多样化。这个例子突显了一个理想的模型需要学习输入和标签之间的因果关系,从而能够在不同的环境中进行泛化。
为此,一种常见的方法是因果干预,即切断因果图中 E 与 G 之间的依赖路径。这是通过干扰环境如何影响输入和标签,从而引导模型学习因果关系来实现的。下图展示了这种方法。在因果推断术语[15]中,旨在移除到特定变量的依赖路径的干预,可以使用 do-算子表示。因此,如果我们在训练过程中旨在强制切断 E 与 G 之间的依赖路径,这实际上意味着将传统的优化目标 p(Y|G)(观察数据的似然)替换为 p(Y|do(G))。

基于因果干预的学习目标。进一步地,利用因果推断中的反向调整[15],我们可以从因果图中推导出目标的显式形式。
然而,计算这个学习目标需要数据中观察到的环境信息,具体来说是每个样本 G 与其环境 E 之间的对应关系。然而,在实际中,环境往往是不可观察的。
提出的方法:变分上下文调整
为了使上述方法可行,[14]推导了因果干预目标的变分下界,采用了一种数据驱动的方法,通过从数据中推断潜在环境来解决不可观察环境的问题。特别地,[14]引入了变分分布 q(E|G),从而得出了下图所示的替代学习目标。

原始因果干预目标的变分下界以及[14]中提出的最终学习目标中三个项的具体实例。图像来自论文。
新的学习目标由三个部分组成。[14]将其具体化为一个环境推断模型,一个 GNN 预测器,和一个(非参数的)环境先验分布。前两个模型包含可训练参数,并在训练过程中共同优化。
为了验证所提方法的有效性,[14]将该模型应用于多个具有分布偏移的真实世界图数据集。具体而言,由于所提方法 CaNet 不依赖于特定的主干模型,[14]分别使用 GCN 和 GAT 作为主干,并与最先进的 OOD 方法(包括先前介绍的 EERM 方法)进行比较。下表显示了部分实验结果。

在 Arxiv(或 Twitch)上测试准确率(或 ROC-AUC)的实验结果,其中分布偏移通过根据发表年份(或子图)对数据进行划分引入。
因果干预中的隐性假设
到目前为止,我们已经介绍了因果干预方法,它在图结构的分布外推理中展现了竞争力。正如之前在本博客中提到的,确保泛化能力需要关于数据如何生成的必要假设。这引发了一个自然的问题:因果干预在泛化中需要什么假设? 与不变学习不同,因果干预并不是从明确假设开始,而是依赖于建模和分析过程中的隐性假设:
在输入和标签之间仅存在一个混杂因素(环境)。
这个假设在一定程度上简化了实际系统的分析,但也引入了近似误差。对于更复杂的场景,未来仍有大量的探索空间。
使用隐式图结构进行泛化
在之前的讨论中,我们假设输入数据的结构信息是可观察和完整的。对于更一般的图数据,结构信息可能是部分可观察的,甚至完全未知。此类数据称为隐式图结构。此外,图上的分布偏移可能涉及影响数据分布的潜在结构,从而带来了在表征几何对数据分布影响时未解决的挑战。
为了解决这个问题,最近的研究[16],吴等,“为抗偏移鲁棒图表示学习发散场”(ICML2024),利用了连续扩散方程与消息传递机制之间的固有联系,整合了前述的因果干预方法。该设计旨在开发一种适用于显式和隐式图结构的学习方法,在这些结构中,分布偏移会构成泛化挑战。
从消息传递到扩散方程
消息传递机制作为现代图神经网络和图 Transformer 的基础设计,通过每一层将信息从其他节点传播到中心节点,从而更新其表示。本质上,如果我们将神经网络的层视为连续时间的离散化近似,那么消息传递可以被看作图上的扩散过程的离散形式[17, 18]。下图说明了它们的类比。(有兴趣深入了解此方面的读者可以参考Prof. Michael Bronstein 等人的最新博客)。

消息传递(GNNs 和 Transformers 中的层间更新)可以通过以下类比看作是连续扩散方程的离散迭代:图中的节点被映射到流形上的位置,节点嵌入由热信号表示,嵌入的层次更新对应于热信号随时间的变化,每层中节点之间的交互反映了流形上位置之间的交互。
特别地,扩散方程中的扩散率(记作 d_u)控制了扩散过程中节点之间的交互。当采用局部或全局扩散形式时,扩散方程的离散迭代分别导致了图神经网络[18]和 Transformer[19]的层更新公式。
然而,确定性扩散率无法模拟实例之间交互中的多方面效应和不确定性。因此,[16] 提出了将扩散率定义为概率分布中的随机样本。相应的扩散方程将产生一个随机轨迹(如下图所示)。

在将扩散率 d_u 定义为随机变量之后,扩散方程在每个时间点的散度场(即当前层中节点嵌入的变化)将变为随机的。这使得能够对节点之间的交互中的不确定性进行建模。
尽管如此,如果直接应用传统的监督学习目标进行训练,上述模型在分布变化的情况下不能很好地泛化。这个问题与之前讨论的图学习的因果视角相呼应。具体而言,在此处考虑的扩散模型中,输入 x(例如图)和输出 y(例如图中的节点标签)通过扩散率关联。扩散率可以看作是特定数据集环境的体现,决定了实例之间的相互依赖关系。因此,训练数据有限的模型往往会学习到特定于训练集的相互依赖模式,从而无法在新的测试数据上泛化。
因果引导的散度场学习
为了解决这一挑战,我们再次采用因果干预,在训练过程中消除扩散性 d 与输入 x 之间的依赖关系。与之前的工作[14]不同,其中输入到输出的映射是由预测器给出的,这里从 x 到 y 的依赖路径涉及一个多步骤的扩散过程(对应于 GNNs/Transformers 中的多层更新)。因此,每一步的扩散过程中都需要因果干预。然而,由于扩散性是一个抽象的建模概念,无法直接观察(类似于前面讨论的环境),[16]扩展了[14]中使用的变分方法,推导出扩散过程学习目标的变分下界。这作为每一步扩散过程中的因果干预的近似目标。

[16]中提出的学习方法估计了扩散模型每一步的扩散性,并应用因果干预。该方法引导模型学习从输入到输出的稳定因果关系,从而增强其在分布偏移下的泛化能力。图片来自论文。
作为上述方法的实现,[16]引入了三种特定的模型设计:
-
GLIND-GCN:将扩散性视为通过标准化图邻接矩阵实例化的常量矩阵;
-
GLIND-GAT:将扩散性视为通过图注意力网络实现的时间依赖矩阵;
-
GLIND-Trans:将扩散性视为通过全局所有对注意力网络实现的时间依赖矩阵。
特别地,对于GLIND-Trans,为了解决全局注意力计算中的二次复杂度问题,[16]进一步采用了 DIFFormer [19]中的线性注意力函数设计。(我们还建议对如何实现所有对注意力的线性复杂度感兴趣的读者参阅此博客)。
下表展示了涉及隐式结构场景中的部分实验结果。

在 CIFAR 和 STL 上的测试准确率实验结果,其中原始数据集不包含结构信息,我们使用 k 近邻方法构建图。此外,对于 CIFAR 和 STL,我们通过添加旋转角度(改变 k 近邻的相似度函数)和使用不同的 k,分别引入了分布偏移。
总结与讨论
本文简要介绍了分布外(OOD)泛化的最新进展,主要聚焦于三篇已发表的论文[5, 14, 16]。这些工作从不变学习和因果干预的角度处理该问题,提出了适用于显式和隐式图结构的方法。如前所述,我们指出,OOD 问题需要在数据生成假设的前提下才能有效解决。基于此,未来的研究可以集中于完善现有方法或分析在已建立的假设条件下的泛化极限,也可以探索在其他假设条件下如何实现泛化。
另一个与 OOD 泛化密切相关的挑战是分布外检测 [20, 21, 22]。与 OOD 泛化不同,OOD 检测的目的是研究如何在训练过程中使模型具备识别测试阶段出现的分布外样本的能力。未来的研究还可以集中在将博客中提到的方法扩展到 OOD 检测,或者探索这两个问题的交集。
参考文献
[1] Garg 等人,图神经网络的泛化与表示限制,ICLR 2020。
[2] Koh 等人,WILDS:野外分布变化基准,ICML 2021。
[3] Morris 等人,Position:图机器学习理论的未来方向,ICML 2024。
[4] Zhu 等人,抗变化的 GNN:克服局部图训练数据的局限性,NeurIPS 2021。
[5] Wu 等人,在图上处理分布变化:一种不变性视角,ICLR 2022。
[6] Li 等人,OOD-GNN:分布外广义图神经网络,TKDE 2022。
[7] Yehudai 等人,从局部结构到图神经网络中的规模泛化,ICML 2021。
[8] Li 等人,生物数据上的图神经网络规模泛化:
来自谱学视角的洞察与实践,Arxiv 2024。
[9] Arjovsky 等人,不变风险最小化,Arxiv 2019。
[10] Rojas-Carulla 等人,因果迁移学习的不变模型,JMLR 2018。
[11] Krueger 等人,通过风险外推进行分布外泛化,ICML 2021。
[12] Yang 等人,学习分布外分子表示的子结构不变性,NeurIPS 2022。
[13] Sui 等人,释放图数据增强在协变量分布变化中的力量,NeurIPS 2023。
[14] Wu 等人,通过因果干预进行图的分布外泛化,WWW 2024。
[15] Pearl 等人,统计学中的因果推断:入门,2016。
[16] Wu 等人,学习用于应对变化的图表示的发散场,ICML 2024。
[17] Freidlin 等人,图上的扩散过程与平均化原理,概率年鉴 1993。
[18] Chamberlain 等人,GRAND:图神经扩散,ICML 2021。
[19] Wu 等人,DIFFormer:由能量约束扩散生成的可扩展(图)变换器,ICLR 2023。
能量约束扩散,ICLR 2023。
[20] Wu 等人,基于能量的图神经网络图外分布检测,ICLR 2023。
[21] Liu 等人,GOOD-D:无监督图外分布检测,WSDM 2023。
[22] Bao 等人,图神经网络的图外分布检测通过邻域塑形,ICML 2024。
朝着提高 LLM 应用中的真实性前进
当前研究中的面向应用的方法
·发布于Towards Data Science ·阅读时间:10 分钟·2024 年 3 月 1 日
--
摘要
本文探讨了增强检索增强生成(RAG)应用输出真实性的方法,重点解决诸如幻觉现象和依赖预训练知识等问题。我分析了不真实结果的原因,评估了评估真实性的方法,并提出了解决方案以提高准确性。本研究强调了 RAG 输出中基础性和完整性的重要性,建议对大型语言模型(LLM)进行微调,并采用元素感知摘要方法以确保事实准确性。此外,文章还讨论了可扩展的评估指标的使用,如可学习评估指标(LENS)和基于思维链(CoT)的评估,用于实时输出验证。文章强调了在增加真实性的好处与可能带来的成本和性能影响之间保持平衡的必要性,并建议根据应用需求选择性地实施这些方法。
1. 引言
一种广泛使用的、大型语言模型(LLM)架构是检索增强生成(RAG)。RAG 是一种通过将参数化记忆(即 LLM 预训练模型)与非参数化记忆(即检索到的文档)结合来扩展 LLM 记忆的方法。为此,从向量数据库中检索最相关的文档,并将它们与用户问题和定制的提示一起传递给 LLM,LLM 根据这些信息生成回答(见图 1)。更多详细信息,请参阅 Lewis 等人(2021 年)。

图 1 — 简化的 RAG 架构
一个现实世界的应用实例可以将 LLM 连接到医疗指南文档的数据库中。医疗从业人员可以通过向 RAG 询问自然语言问题,取代手动查找,作为一种“搜索引擎”来使用。该应用将回答用户的问题,并引用源指南。如果答案基于参数化记忆,例如回答的是包含在预训练中而不是连接数据库中的指南,或者如果 LLM 产生幻觉,这可能会产生严重影响。
首先,如果医疗从业人员核实了引用的指南,他们可能会对应用的回答失去信任,导致使用量下降。其次,更令人担忧的是,如果不是每个答案都经过核实,可能会错误地假设答案是基于查询的医疗指南,这将直接影响患者的治疗。这突显了 RAG 应用中输出真实性的重要性。
在本文中,评估 RAG 时,真实性被定义为在检索文档的事实知识中扎实根植。为了解决这个问题,衍生出了一个总体研究问题(GRQ)和三个具体研究问题(SRQ)。
GRQ: 如何提高 RAG 输出的真实性?
SRQ 1: 什么原因导致 RAG 应用产生不真实的结果?
SRQ 2: 如何评估真实性?
SRQ 3: 可以使用哪些方法来提高真实性?
为了回答 GRQ,SRQ 将根据文献研究按顺序进行分析。目的是识别可以实施的解决方法,适用于医学领域中上述例子的使用场景。最终将推荐两类解决方案方法,以便进一步分析和定制。
2. 不真实的 RAG 输出
如前所定义,真实的答案应当扎实地基于检索文档中的事实知识。衡量这一点的一个指标是事实一致性,衡量摘要中是否包含未经源文献支持的不真实或误导性事实(Liu 等,2023)。这一指标被作为多个基准中的关键评估指标(Kim 等,2023;Fabbri 等,2021;Deutsch & Roth,2022;Wang 等,2023;Wu 等,2023)。在 RAG 领域,这通常被称为“扎实性”(Levonian 等,2023)。此外,为了考虑真实答案的实用性,其完整性也具有重要意义。接下来的段落将深入探讨 RAG 结果不真实的原因。这指的是图 1 中的生成步骤,该步骤根据用户问题总结检索到的文档。
首先,RAG 应用的基础性(groundedness)受到 LLM 的回答是基于参数记忆而非检索文档中的事实知识的影响。例如,如果回答来源于预训练知识或是由于幻觉现象引起的,这种情况可能会发生。幻觉仍然是 LLM 的一个根本问题(Bang 等, 2023;Ji 等, 2023;Zhang & Gao, 2023),即使是强大的 LLM 也会受到其影响(Liu 等, 2023)。根据定义,低基础性会导致不真实的 RAG 结果。
其次,完整性描述了 LLM 的回答是否缺乏文档中的事实知识。这可能是由于 LLM 的总结能力较弱,或缺乏理解事实知识所需的领域知识(T. Zhang 等, 2023)。输出可能依然高度基于事实。然而,回答可能相对于文档而言是不完整的,从而导致用户对数据库内容的错误认知。此外,如果文档中的事实知识缺失,LLM 可能会通过利用其自身的参数记忆来弥补这一点,进而引发上述问题。
在确定了不真实输出的关键原因之后,有必要首先测量并量化这些错误,然后才能寻找解决方案。因此,以下部分将介绍针对上述不真实 RAG 输出来源的测量方法。
3. 评估真实性
在阐述了基础性和完整性及其来源后,本节旨在指导它们的测量方法。我将从广为人知的通用方法开始,并继续突出最近的趋势。TruLens 的反馈功能图在可扩展性和意义性方面提供了宝贵的参考(见图 2)。
在讨论自然语言生成评估时,传统的评估指标如 ROUGE(Lin, 2004)和 BLEU(Papineni 等, 2002)被广泛使用,但往往与人工评估存在差异(Liu 等, 2023)。此外,中型语言模型(MLM)在传统评估指标上展示了优异的表现,但在许多领域中可以被大型语言模型(LLM)替代(X. Zhang & Gao, 2023)。最后,另一种著名的评估方法是对生成文本的人工评估,但这种方法在规模和成本上存在明显的缺点(Fabbri 等, 2021)。由于这些方法的缺点(见图 2),它们在本文中不再进一步讨论。

图 2 — 反馈功能(反馈功能 — TruLens,o. J.)
关于近期趋势,评估指标随着大语言模型(LLMs)普及的增加而发展。其中一种发展是 LLM 评估,允许另一个 LLM 通过思维链(CoT)推理来评估生成的文本(Liu et al., 2023)。通过定制化的提示策略,可以强调并用数值打分聚焦于诸如真实性和完整性等方面(Kim et al., 2023)。对于这种方法,已有研究表明,较大的模型规模有利于摘要评估(Liu et al., 2023)。此外,这种评估还可以基于参考或收集的真实数据,比较生成的文本和参考文本(Wu et al., 2023)。对于没有唯一正确答案的开放性任务,基于 LLM 的评估在与人工质量判断的相关性方面优于基于参考的指标。此外,收集真实数据的成本可能很高。因此,基于参考或真实数据的指标不在本评估范围之内(Liu et al., 2023;反馈函数 — TruLens,o. J.)。
最后,介绍一个值得注意的近期进展,即L可学习评估指标用于文本简化(LENS),Maddela 等人(2023)称其为“首个监督自动化的文本简化评估指标”,在最近的基准测试中展现了良好的结果。它以其在识别幻觉(Kew et al., 2023)方面的有效性而著称。在可扩展性和有效性方面,预计该方法由于成本较低,具有较高的可扩展性,但在意义性上略逊色于 LLM 评估,因此 LENS 接近于图 2 右上角的 LLM 评估。然而,仍需进一步评估以验证这些说法。至此,评估方法的讨论已结束,下一节将聚焦于这些方法的应用。
4. 朝向更高的真实性
在第一节中已确立了 RAG 应用中真实性的重要性,并通过 SRQ1 探讨了不真实输出的原因,通过 SRQ2 进行了评估,本节将聚焦于 SRQ3。因此,将详细描述改善真实性和完整性的具体推荐方法,以提高真实的回应。这些方法可分为两类:输出生成的改进和输出验证。
为了改进 RAG 应用的生成步骤,本文将重点介绍两种方法。这些方法在图 3 中得到了可视化,并且左侧参考了简化的 RAG 架构。第一种方法是微调生成 LLM。模型大小上的指令调整对 LLM 的零-shot 摘要能力至关重要。因此,最先进的 LLM 能够与自由职业者编写的摘要相媲美(T. Zhang 等,2023)。第二种方法专注于元素感知摘要。通过 CoT 提示,如 SumCoT 中所展示的,LLM 可以逐步生成摘要,强调源文本中的事实实体(Wang 等,2023)。具体来说,在额外的一步中,相关文档中的事实元素被提取,并与摘要的上下文一起提供给 LLM,见图 3。两种方法在提高 LLM 生成摘要的基础性和完整性方面已显示出良好的效果。

图 3 — 改进的生成步骤
在验证 RAG 输出时,LLM 生成的摘要会根据其基础性和完整性进行评估。这可以通过 CoT 提示将 LLM 的基础性和完整性得分进行聚合来完成。图 4 中展示了一个 CoT 提示示例,您可以将其转发给更大模型的 LLM 以进行完成。此外,这一步可以通过使用监督指标(如 LENS)来替代或推进。最后,生成的评估结果将与阈值进行比较。如果输出没有基础性或不完整,则可以对其进行修改、提升给用户,或可能被拒绝。

图 4 — 输出验证
在将这些方法应用于 RAG 时,应考虑到运行时评估和生成模型的微调会导致额外的成本。此外,评估步骤会影响应用的回答速度。最后,由于输出被拒绝或提升了真实性问题而没有答案,可能会使应用用户感到困惑。因此,评估这些方法时需要根据应用领域、应用功能和用户期望进行综合考虑。这将导致一种定制化的方法,提升 RAG 应用输出的真实性。
除非另有注明,所有图片均由作者提供。
参考文献列表
Bang, Y., Cahyawijaya, S., Lee, N., Dai, W., Su, D., Wilie, B., Lovenia, H., Ji, Z., Yu, T., Chung, W., Do, Q. V., Xu, Y., & Fung, P. (2023). A Multitask, Multilingual, Multimodal Evaluation of ChatGPT on Reasoning, Hallucination, and Interactivity (arXiv:2302.04023). arXiv. doi.org/10.48550/arXiv.2302.04023
Deutsch, D., & Roth, D. (2022). 基准测试基于问答的摘要评估度量的答案验证方法 (arXiv:2204.10206). arXiv. doi.org/10.48550/arXiv.2204.10206
Fabbri, A. R., Kryściński, W., McCann, B., Xiong, C., Socher, R., & Radev, D. (2021). SummEval: 重新评估摘要评估方法 (arXiv:2007.12626). arXiv. doi.org/10.48550/arXiv.2007.12626
反馈函数 — TruLens。(o. J.)。于 2024 年 2 月 11 日访问,来自www.trulens.org/trulens_eval/core_concepts_feedback_functions/#feedback-functions
Ji, Z., Lee, N., Frieske, R., Yu, T., Su, D., Xu, Y., Ishii, E., Bang, Y., Dai, W., Madotto, A., & Fung, P. (2023). 关于自然语言生成中的幻觉问题的调查. ACM 计算机评论, 55(12), 1–38. doi.org/10.1145/3571730
Kew, T., Chi, A., Vásquez-Rodríguez, L., Agrawal, S., Aumiller, D., Alva-Manchego, F., & Shardlow, M. (2023). BLESS: 基准测试大规模语言模型在句子简化上的表现 (arXiv:2310.15773). arXiv. doi.org/10.48550/arXiv.2310.15773
Kim, J., Park, S., Jeong, K., Lee, S., Han, S. H., Lee, J., & Kang, P. (2023). 哪种更好?探索基于 LLM 度量的提示策略 (arXiv:2311.03754). arXiv. doi.org/10.48550/arXiv.2311.03754
Levonian, Z., Li, C., Zhu, W., Gade, A., Henkel, O., Postle, M.-E., & Xing, W. (2023). 增强检索生成以提高数学问答的表现:基础性与人类偏好之间的权衡 (arXiv:2310.03184). arXiv. doi.org/10.48550/arXiv.2310.03184
Lewis, P., Perez, E., Piktus, A., Petroni, F., Karpukhin, V., Goyal, N., Küttler, H., Lewis, M., Yih, W., Rocktäschel, T., Riedel, S., & Kiela, D. (2021). 基于检索增强生成的知识密集型 NLP 任务 (arXiv:2005.11401). arXiv. doi.org/10.48550/arXiv.2005.11401
Lin, C.-Y. (2004). ROUGE: 一种自动化摘要评估工具包. 文本摘要的扩展应用, 74–81. aclanthology.org/W04-1013
Liu, Y., Iter, D., Xu, Y., Wang, S., Xu, R., & Zhu, C. (2023). G-Eval: 使用 GPT-4 的 NLG 评估与更好的人工对齐 (arXiv:2303.16634). arXiv. doi.org/10.48550/arXiv.2303.16634
Maddela, M., Dou, Y., Heineman, D., & Xu, W. (2023). LENS: 一种可学习的文本简化评估度量 (arXiv:2212.09739). arXiv. doi.org/10.48550/arXiv.2212.09739
Papineni, K., Roukos, S., Ward, T., & Zhu, W.-J. (2002). Bleu:一种自动评估机器翻译的方法. 载于 P. Isabelle, E. Charniak, & D. Lin (编辑), 第 40 届计算语言学会年会论文集 (第 311–318 页). 计算语言学会. doi.org/10.3115/1073083.1073135
Wang, Y., Zhang, Z., & Wang, R. (2023). 具有元素感知的大型语言模型摘要:专家对齐评估与思维链方法 (arXiv:2305.13412). arXiv. doi.org/10.48550/arXiv.2305.13412
Wu, N., Gong, M., Shou, L., Liang, S., & Jiang, D. (2023). 大型语言模型在摘要评估中的多样化角色 (arXiv:2303.15078). arXiv. doi.org/10.48550/arXiv.2303.15078
Zhang, T., Ladhak, F., Durmus, E., Liang, P., McKeown, K., & Hashimoto, T. B. (2023). 基准测试大型语言模型在新闻摘要中的表现 (arXiv:2301.13848). arXiv. doi.org/10.48550/arXiv.2301.13848
Zhang, X., & Gao, W. (2023). 基于大型语言模型的新闻声明事实验证:一种分层逐步提示方法 (arXiv:2310.00305). arXiv. doi.org/10.48550/arXiv.2310.00305
朝向无限 LLM 上下文窗口
一切始于 GPT 具有 512 个标记的输入上下文窗口。仅仅五年后,最新的 LLM 已经能够处理超过 100 万个上下文输入。那么,极限在哪里呢?
·发布于Towards Data Science ·阅读时间 9 分钟·2024 年 4 月 28 日
--

我喜欢将LLMs(具体来说,是模型的参数,即神经网络层的权重和注意力机制的权重)看作是一个庞大的记忆库,里面包含了关于世界的各种信息,并且混合了如何处理文本的语言知识。这就像是模型在预训练过程中学习到的静态知识,(可选地)经过微调后,形成了一个可以在所有与模型的对话中重用的公共核心,无论是谁提出问题。另一方面,输入上下文则是魔法发生的地方。它就像是向模型传递一条带有特殊指令或信息的便条。这可以是我们希望模型回答的问题,也可以是我们拥有的独特信息,甚至是我们与模型之前对话的历史。这是一种给模型提供个人记忆的方式。
上下文大小的演变
一个 LLM 的关键特性之一是它的输入上下文大小。它可以定义为语言模型在生成时考虑的单词数量(或者更准确地说,是标记)。
朝着 Mamba 状态空间模型迈进:图像、视频和时间序列
🐍 朝着 Mamba 状态空间模型迈进:图像、视频和时间序列
第一部分
·发表于 Towards Data Science ·16 分钟阅读·2024 年 8 月 14 日
--

图片由 Sascha Kirch 提供
这是我新系列的第一部分,🐍 朝着 Mamba 状态空间模型迈进:图像、视频和时间序列。
Mamba 是你所需的一切吗?当然,长期以来人们一直认为 A. Vaswani 等人在 2017 年提出的 Attention is all you need 中的 Transformer 架构就是如此。毫无疑问,Transformer 一次又一次地彻底改变了深度学习领域。它的通用架构可以轻松适应多种数据模式,如文本、图像、视频和时间序列,而且似乎你向 Transformer 投入的计算资源和数据越多,它的性能就越强。
然而,Transformer 的注意力机制有一个主要的缺点:它的复杂度是 O(N²),这意味着它随着序列长度的增加而呈二次方增长。也就是说,输入序列越大,你需要的计算资源就越多,这使得处理大序列往往变得不可行。
❓ 问题是:我们能做得更好吗?是否有办法在保持性能的同时减少 O(N²) 复杂度?那么这种新架构会是什么样子的呢?
走向单一语义性:理解大型语言模型的一步
理解机械解释性研究问题并逆向工程这些大型语言模型
·发表于 Towards Data Science ·10 分钟阅读·2024 年 7 月 11 日
--
背景
人工智能研究人员的一个主要迫切问题是理解这些大型语言模型如何工作。从数学角度来看,我们已经有了一个很好的答案,了解不同神经网络权重如何相互作用并产生最终答案。但直观地理解这些模型是人工智能研究人员要回答的核心问题之一。这非常重要,因为除非我们理解这些大型语言模型的工作原理,否则很难解决诸如 LLM 对齐和人工智能安全性等问题,或将 LLM 建模以解决特定问题。理解大型语言模型如何工作的这一问题被定义为一种机械解释性研究问题,核心思想是我们如何逆向工程这些大型语言模型。
Anthropic 是在理解这些大型模型方面取得重大进展的公司之一。主要问题是,除了数学角度,如何理解这些模型的工作原理。2023 年 10 月,他们发布了这篇论文:走向单一语义性:通过字典学习分解语言模型(链接)。这篇论文旨在解决这一问题,并建立对这些模型工作原理的基本理解。
以下文章旨在捕捉高层次的基本概念,并为理解 “走向单一语义性:通过字典学习分解语言模型” 论文打下坚实的基础。
论文开篇提出了一个复杂的术语,“走向单一语义性”。让我们直接深入探讨,理解这意味着什么。
什么是单一语义性与多重语义性?
大型语言模型的基本单元是由神经元组成的神经网络。因此,神经元是整个大型语言模型(LLM)的基本单元。然而,经过检查,我们发现神经元在神经网络中会对不相关的概念发生反应。例如:在视觉模型中,一个神经元同时响应“猫的脸”和“汽车的前脸”。这个概念被称为“多义性”。这意味着神经元可以对不相关的输入的混合产生反应。这使得问题变得非常复杂,因为神经元本身不能用来分析模型的行为。如果一个神经元仅响应猫的脸,而另一个神经元响应汽车的前脸,那就好了。如果一个神经元只对一个特征发生反应,这种特性将被称为“单义性”。
因此,论文的第一部分,“朝向单义性”,意味着如果我们能从多义性转向单义性,这将有助于我们更深入地理解神经网络。
如何超越神经元进一步发展?
现在,关键问题是,如果神经元对不相关的概念发生反应,这意味着网络需要学习一个更基础的数据表示。我们来看一个例子:“猫”和“汽车”。猫可以表示为“动物、毛发、眼睛、四肢、移动”的组合,而汽车则可以表示为“轮子、座椅、矩形、前灯”的组合。这是第一层表示。这些表示可以进一步细分为抽象概念。我们来看“眼睛”和“前灯”。眼睛可以表示为“圆形、黑色、白色”,而前灯可以表示为“圆形、白色、光”。正如你所看到的,我们可以进一步构建这个抽象表示,并注意到两个非常不相关的事物(猫和汽车)开始共享一些表示。这只有两层深,如果我们假设表示深度为 8x、16x 或 256x 层,很多事物将用非常基础的抽象概念表示(这些概念对人类来说难以解释),但这些概念将在不同的实体之间共享。
作者使用了术语“特征”来表示这个概念。根据论文,每个神经元可以存储许多不相关的特征,因此会对完全不相关的输入产生反应。
如果一个神经元存储许多特征,如何获得特征级别的表示?
答案总是要扩展规模。如果我们认为一个神经元存储了 5 个不同的特征,那么我们能否将这个神经元拆分成 5 个独立的神经元,每个子神经元代表一个特征?这就是论文的核心思想。
以下图像展示了论文核心思想的表示。“观察到的模型”是实际存储多个特征信息的模型。它被称为某个假设的大型网络的低维投影。大型网络是一个假设的解耦模型,表示每个神经元映射到一个特征并显示出“单义性”行为。
有了这个,我们可以说,无论我们训练的是哪个模型,总会有一个更大的模型可以实现数据和特征之间的 1:1 映射,因此我们需要学习这个更大的模型,以便向单一语义性迈进。

图片来自transformer-circuits.pub/2023/monosemantic-features
现在,在进入技术实现之前,让我们回顾一下至今为止的信息。神经元是神经网络中的基本单元,但它包含多个数据特征。当数据(令牌)被分解成更小的抽象概念时,这些被称为特征。如果一个神经元存储多个特征,我们需要一种方式将每个特征与其神经元关联,以确保每个特征只有一个神经元激活。这种方法将帮助我们向“单一语义性”迈进。数学上,这意味着我们需要更多的扩展,因为我们需要更多的神经元将数据转化为特征。
在我们掌握了基本的核心思想后,接下来让我们探讨一下如何构建这些技术实现。
技术设置
由于我们已经确定需要更多的扩展,目标是在多层感知机(MLP)之后扩展输出。在讨论如何扩展之前,让我们快速回顾一下 LLM 模型如何与 Transformer 和 MLP 模块一起工作。
下面的图片展示了 LLM 模型如何与 Transformer 和 MLP 模块协同工作。核心思想是每个令牌通过嵌入(向量)表示,并传递到注意力模块,该模块计算不同令牌之间的注意力。注意力模块的输出维度与每个令牌的输入维度相同。然后,来自注意力模块的每个令牌输出经过多层感知机(MLP)进行扩展,再缩小到与输入令牌相同的大小。这个过程会重复多次,直到产生最终输出。在 chat-GPT-3 的情况下,有 96 层执行这个操作。这与 Transformer 架构的工作原理相同。详情请参阅“Attention is all you need”论文。链接

图片由作者提供
现在,在基本架构已经搭建完成后,让我们更深入地了解什么是稀疏自编码器。作者使用了“稀疏自编码器”来进行放大和缩小,因此这些成为了理解的基础模块。
稀疏自编码器
稀疏自动编码器本身就是神经网络,但包含三个阶段(编码器、居中的稀疏激活和解码器)。其思想是,自动编码器接收一个 512 维的输入,将其缩放到一个 4096 的中间层,然后再压缩为 512 维的输出。当一个 512 维的输入到达时,它会通过编码器,编码器的任务是从数据中提取特征。接下来,它被映射到一个高维空间(稀疏自动编码器激活层),在这个空间中,只允许少量非零值,因此被认为是稀疏的。这里的想法是强迫模型在高维空间中学习少量特征。最后,矩阵被强制映射回解码器(512 维),以重构与编码器输入相同的大小和值。
下图表示了稀疏自动编码器(SAE)架构。

图片来自作者
在基本的变换器架构和稀疏自动编码器解释完毕后,让我们尝试理解稀疏自动编码器如何与变换器块集成以实现可解释性。
稀疏自动编码器(SAE)是如何与大语言模型(LLM)集成的?
大语言模型(LLM)基于一个变换器块,这是一个注意力机制,后面接着一个 MLP(多层感知机)块。其思路是从 MLP 获取输出并将其输入稀疏自动编码器块。举个例子:“Golden Gate was built in 1937”。“Golden”是第一个词元,它通过注意力块解析,然后进入 MLP 块。经过 MLP 块后的输出与输入的维度相同,但由于注意力机制,它将包含句子中其他词的上下文。现在,来自 MLP 块的相同输出向量成为稀疏自动编码器的输入。每个词元都有自己的 MLP 输出维度,也可以输入到 SAE 中。下图传达了这一信息,以及它如何与变换器块集成。

图片来自作者
旁注:下面的图片在论文中非常著名,传达的信息与上述部分相同。它接收来自 MLP 层的激活向量,并将其输入稀疏自动编码器进行特征缩放。希望通过上面的解释,下面的图片会更容易理解。

图片来源于 transformer-circuits.pub/2023/monosemantic-features
现在,我们已经理解了稀疏自动编码器与大语言模型(LLM)集成的架构,基本问题是这些稀疏自动编码器是如何训练的?由于这些也是神经网络,这些模型也需要进行训练。
自动编码器是如何训练的?
自编码器的数据集来自于 LLM 本身。当使用一个词汇训练 LLM 模型时,每个词汇在每个 MLP 层后的输出,即激活向量,会被存储。因此,我们有一个来自词汇的输入(512 大小)和来自 MLP 激活层的输出(512 大小)。我们可以在不同上下文中收集相同词汇的不同激活。在论文中,作者为相同的词汇收集了 256 个不同上下文的激活。这为相同的词汇在不同上下文设置中的表示提供了良好的基础。
一旦选择了输入,SAE 就会针对输入和输出进行训练(输入与来自 MLP 激活层(512 大小)的输出相同,输出与输入相同)。由于输入等于输出,SAE 的任务是通过稀疏激活(1–2 个非零值)将 512 大小的信息提升至 4096 大小,然后再转换回 512 大小。由于这是一个上升过程,但在重构信息时需要对 1–2 个非零值进行惩罚,这就是学习发生的地方,模型被迫为特定数据/词汇学习 1–2 个特征。
直观上,这是一个非常简单的问题,模型很容易学习。输入与输出相同,中间层比输入和输出层更大。模型可以学习相同的映射,但我们对中间层中的少数非零值引入了惩罚。现在,它变成了一个困难的问题,因为输入与输出相同,但中间层只有 1–2 个非零值。因此,模型必须显式地在中间层学习数据代表了什么。这就是数据被分解为特征,并且特征在中间层中被学习的地方。
通过以上理解,我们现在准备好处理特征可解释性的问题了。
特征可解释性
训练完成后,我们现在进入推理阶段。这就是解释开始的地方。LLM 模型的 MLP 层输出被输入到 SAE 中。在 SAE 中,只有少数(1–2)块被激活。这是 SAE 的中间层。在这里,需要人工检查以查看中间层中哪个神经元被激活。
示例:假设有 2 种上下文被输入到 LLM 中,我们的任务是确定何时触发“Golden”这一词汇。上下文 1:“Golden Gate 是 1937 年建成的”,上下文 2:“Golden Gate 位于旧金山”。当两个上下文同时输入到 LLM 中,并且提取“Golden”词汇在上下文 1 和上下文 2 中的输出,并将其输入到 SAE 时,SAE 的中间层应该只有 1–2 个特征被激活。假设这个特征编号是 1345(从 4096 中随机分配的数字)。这表示当在输入列表中提到 Golden Gate 时,特征 1345 被触发。这意味着特征 1345 代表了“Golden Gate”的上下文。
因此,这是解释 SAE 特征的一种方式。
当前方法的局限性
测量:主要瓶颈在于特征的解释。在上述例子中,需要人工判断是否 1345 属于金门大桥,并通过多个上下文进行测试。没有任何数学损失函数公式能够定量地回答这个问题。这是机械可解释性领域在确定如何衡量机器进展是否可解释时面临的主要瓶颈之一。
扩展性:另一个方面是扩展性,因为在每一层训练稀疏自编码器(SAE)时,参数是原来的四倍,这对内存和计算资源要求极高。随着主模型参数的增加,扩展稀疏自编码器变得更加困难,因此也存在关于使用稀疏自编码器的扩展性问题。
总体来说,这是一次令人着迷的旅程。我们从一个模型出发,理解了其可解释性方面的细微差别,并探讨了为什么神经元尽管是一个基本单元,但仍不是理解的根本单元。我们进一步深入探讨了数据是如何由特征构成的,以及是否有方法学习这些特征。我们了解了稀疏自编码器如何帮助学习特征的稀疏表示,并且它可以成为特征表示的构建块。最后,我们学习了如何训练稀疏自编码器,并在训练后,如何在推理阶段使用稀疏自编码器来解释特征。
结论
机械可解释性领域仍有很长的路要走。然而,Anthropic 在引入稀疏自编码器方面的当前研究,已是朝着可解释性迈出的重要一步。该领域仍然面临测量和扩展性挑战,但迄今为止,已是机械可解释性领域中最优秀、最先进的研究之一。
参考文献
朝着命名实体消歧(NED)与图嵌入的方向发展
NED-SERIES
如何结合预训练语言模型与图机器学习从生物医学文本中提取知识
·发表于 Towards Data Science ·10 分钟阅读·2024 年 9 月 25 日
--
本文总结了在 IEEE 信息与通信技术应用(AICT 2024)会议上接受的一篇论文,该论文荣获了 最佳论文奖。
除了签名人外,论文的合著者包括 Felice Paolo Colliani(第一作者)、Giovanni Garifo、Antonio Vetrò 和 Juan Carlos De Martin。
完整文章可以在此处访问: https://ieeexplore.ieee.org/abstract/document/10740424.
引言
由于科学研究的增长、技术的进步以及全球对医疗保健和医学研究的重视,生物医学领域的出版率多年来稳步增长。
自然语言处理(NLP)技术在生物医学领域的应用代表着在分析和解读庞大的生物医学知识库方面的一次转变,增强了我们从文本数据中提取有意义洞察的能力。
命名实体消歧(NED)是一个关键的 NLP 任务,涉及通过将实体提及链接到知识库中的正确条目来解决歧义。为了理解此类任务的重要性和复杂性...
朝着可靠的合成控制
使估计的处理效应接近真实值
·发表于Towards Data Science ·阅读时间 7 分钟·2024 年 4 月 16 日
--

图片由Jørgen Håland提供,来源于Unsplash
介绍
近年来,合成控制(SC)方法在工业界得到了越来越广泛的应用,特别是在无法进行随机对照试验(RCTs)时,用于衡量干预的平均处理效应(ATE)。一个例子是衡量户外广告牌的财务影响,而在实践中我们无法进行随机处理分配。
SC 的基本思路是通过将处理组与预测的反事实进行比较来估计 ATE。然而,在实际应用 SC 时,通常会面临其有效性知识有限的问题,因为在现实世界中缺乏真实的反事实。为了解决这一问题,本文将讨论一些可行的最佳实践,以帮助最大限度地提高 SC 估计的可靠性。
这些见解和结论是通过基于多样化合成数据的实验得出的。用于数据生成、因果推断建模和分析的代码可以在托管于Github的 Jupyter 笔记本中找到。
简明的合成控制
测量这种事件的 ATE 的关键是识别处理组的反事实,即在没有处理的情况下的处理组,并量化两者之间的处理后差异。对于随机对照试验(RCTs)而言,这是简单的,因为随机化对照在统计上接近反事实。然而,在其他情况下,由于处理组和对照组在实验前的统计量不均等,问题变得更具挑战性。
作为一种因果推断技术,合成控制(SC)通过基于一些未处理的对照单元创建的合成对照组来表示反事实。这个合成对照组在处理前在统计上等同于处理组,并且预计能够接近处理组在处理后的未处理行为。数学表达如下,它是通过最小化处理组与由f合成的对照组之间的处理前差异来创建的,参数通过此方法获得[1]:

在实验中,存在 J 组,其中第 1 组是处理组,其他是对照组。每组在时间 t 的观察结果用 Yjt 表示。f 是模型,Y1t^N 表示反事实。图像来源:作者。
在实践中,f函数的常用选项包括但不限于加权和[1]、贝叶斯结构时间序列(BSTS)[2]等。
迈向可靠的合成控制的行动
尽管有坚实的理论基础,实际上应用合成控制(SC)通常面临一个挑战,那就是我们无法确定估计的平均处理效应(ATE)有多准确,因为现实中没有后处理反事实来验证合成的反事实。然而,我们可以采取一些措施来优化建模过程,并最大化可靠性。接下来,我将描述这些措施,并通过一系列基于合成时间序列数据的实验,展示它们如何影响估计的 ATE,这些数据具有不同的时间特征。
实验设置
本文中展示的所有实验都基于合成时间序列数据。这些数据是使用timeseries-generator包生成的,该包产生的时间序列捕捉了包括 GDP、假期、周末等在内的现实世界因素。
[## GitHub — Nike-Inc/timeseries-generator: 一个通过…
一个通过易于使用的因子和生成器生成合成时间序列数据的库 — Nike-Inc/timeseries-generator
数据生成旨在模拟新西兰商店在 2019 年 01 月 01 日至 2019 年 12 月 31 日期间的活动表现。为了使潜在的结论在统计上具有显著性,生成了 500 个时间序列来代表商店。每个时间序列都具有统计随机化的线性趋势、白噪声、商店因素、假日因素、工作日因素和季节性。下方展示了 10 家商店的随机样本。

新西兰 10 家商店的随机抽样合成时间序列。图片来自作者。
选择 store1 作为治疗组,而其他商店作为控制组。接下来,从 2019 年 09 月 01 日起将 store1 的结果提升 20% 来模拟处理后的行为,而其原始结果则作为真实反事实。这个 20% 的提升建立了实际的 ATE,随后用于验证后续操作。
cutoff_date_sc = '2019-09-01'
df_sc.loc[cutoff_date_sc:] = df_sc.loc[cutoff_date_sc:]*1.2
下图可视化了模拟的治疗效应和治疗组的真实反事实。

store1 的模拟 ATE 为 +20%,以及其真实反事实。图片来自作者。
给定合成数据,采用 Causalimpact 中的 BSTS 来估算合成的 ATE。然后,通过使用平均绝对百分比误差(MAPE)将估算值与实际 ATE 进行比较,以评估相应的操作。
[## GitHub — jamalsenouci/causalimpact:CausalImpact R 库的 Python 移植版
CausalImpact R 库的 Python 移植版。通过在…上创建帐户,贡献 jamalsenouci/causalimpact 开发。
接下来,让我们逐步介绍相关实验的操作,看看如何生成可靠的 ATE 估算。
治疗-控制相关性
实现可靠 ATE 估算的第一个步骤是选择与治疗组表现出高预处理相关性的控制组。其理由是,高度相关的控制组可能会随着时间的推移持续地与未处理的治疗组相似。
为了验证这个假设,让我们评估使用每个单独的控制组及其自 2019 年 01 月 01 日以来的完整数据所产生的 ATE 估计,以了解相关性的影响。首先,计算治疗组(store1)与控制组(store2 到 499)之间的相关系数 [3]。
def correlation(x, y):
shortest = min(x.shape[0], y.shape[0])
return np.corrcoef(x.iloc[:shortest].values, y.iloc[:shortest].values)[0, 1]
如下图所示,相关性的分布范围从 -0.1 到 0.9,这为跨各种场景的影响提供了全面的理解。

预处理相关性的分布。图片来自作者。
然后,使用每个单独的对照组来预测反事实,估计 ATE,并报告 MAPE。在下图中,ATE 的平均 MAPE 及其 95%置信区间与相应的前期相关性一起绘制。这里,相关系数四舍五入到小数点后一位,以便汇总并提高分析中的统计显著性。从结果来看,明显可以看出,当对照组与治疗组的相关性增加时,估计显示出更高的可靠性。

不同相关性水平下 ATE 的 MAPE。图片来源:作者。
现在让我们来看一些展示前期相关性影响的例子:store88 的相关性为 0.88,MAPE 为 0.12,优于 store3 相关性为 0.43 时的 0.62。除了令人鼓舞的准确性外,相应的概率区间也很窄,这意味着预测的确定性较高。

示例展示相关性的影响。图片来源:作者。
模型拟合窗口
接下来,拟合窗口,即用于拟合模型的前期时间段,需要正确配置。这是因为过多的上下文可能导致失去时效性,而不足的上下文可能导致过拟合。
为了理解拟合窗口如何影响 ATE 估计的准确性,实验了从治疗日期前 1 个月到 8 个月之间的多种值。对于每个拟合窗口,单独评估 499 个对照组中的每一个单位,然后汇总计算带有 95%置信区间的平均 MAPE。如下面的图所示,存在一个接近 2 到 3 个月的最佳区间,可以优化可靠性。识别最优点超出了本讨论的范围,但值得注意的是,训练窗口需要精心选择。

不同训练窗口下 ATE 的 MAPE。图片来源:作者。
该图展示了两个例子:当对照组 199 的拟合窗口从 1 个月增加到 3 个月时,其 MAPE 从 0.89 降至 0.68,因为短窗口包含的知识不足以生成反事实。

示例展示训练窗口的影响。图片来源:作者。
对照组数量
最后,所选对照组的数量也很重要。
通过调查不同控制数量(从 1 到 10)的估计准确性来验证这一假设。具体而言,对于每个控制数,计算基于 50 个随机控制集产生的估计值的平均 MAPE,每个控制集包含相应数量的控制组。此操作避免了不必要地枚举每一种可能的控制组合,同时在统计上控制了相关性。此外,每个估计的拟合窗口设置为 3 个月。
从下面的结果来看,增加控制数量总体上有助于更可靠的 ATE 估计。

不同控制数量下 ATE 的 MAPE。图片由作者提供。
以下示例展示了该效应。第一个估计使用 store311 生成,而第二个估计进一步加入了 store301 和 store312。

下面的示例展示了控制变量数量的影响。图片由作者提供。
结论
在本文中,我讨论了使 SC 估计更可靠的可能方法。基于各种合成数据的实验,前期处理的相关性、拟合窗口和控制单元数量被认为是优化估计的关键方向。寻找每个方法的最优值不在本讨论的范围之内。然而,如果你感兴趣,可以使用隔离的空白期进行参数搜索进行验证 [4],这是一种可能的解决方案。
除非另有说明,所有图片均由作者制作。讨论内容的灵感来源于伟大的著作《合成控制法的应用》[1]。
参考文献
[1] Abadie, Alberto, 和 Jaume Vives-i-Bastida. “合成控制法的应用。” arXiv 预印本 arXiv:2203.06279 (2022)。
[2] Brodersen, Kay H. 等. “使用贝叶斯结构时间序列模型推断因果影响。” (2015): 247–274。
[4] Abadie, Alberto, 和 Jinglong Zhao. “实验设计中的合成控制法。” arXiv 预印本 arXiv:2108.02196 (2021)。
数据工程模式

可持续技术 — 图像由作者生成,使用 DALLE-3
工程师、科学家和分析师有潜力通过引入可持续、高效和稳健的开发方法,显著减少碳排放。
·发表于Towards Data Science ·13 分钟阅读·2024 年 5 月 14 日
--

图片由Isaac Garcia提供,来源于Unsplash
开发人员和数据实践者应如何在开发端到端数据解决方案时,开始将环境因素纳入考虑?
在本文中,我们从数据实践者的角度探讨可持续性,提升对数据处理活动对环境影响程度的认知。我们识别出导致能源消耗增加的低效模式,并深入探讨解决这些问题的策略。重点将放在代码、设计策略、计算优化以及其他可操作的可持续模式上,这些都将有助于实现可持续性的最终目标。
目录
1. 能源是每个应用程序的核心(原因是什么…)
2. 显著的能源消耗是不可持续工程模式的结果(问题是什么…)
- 数据摄取
TPU 不出售,但为什么?
观点
Google 在 AI 硬件方面的独特做法分析
·发表于 Towards Data Science ·10 分钟阅读·2024 年 4 月 30 日
--

图片来源:Dollar Gill via Unsplash
Nvidia 的股价飙升,因为其 GPU 在 AI 硬件市场的主导地位。然而,与此同时,Google 的 TPU 作为知名的 AI 硬件,并不出售。你只能在 Google Cloud 上租用虚拟机来使用它们。那么,为什么 Google 不加入销售 AI 硬件的游戏呢?
免责声明:本文所表达的观点仅代表作者个人意见,并不一定反映 Google 或其附属公司的意见或观点。本文中所提供的所有信息完全来源于公开材料。
一个流行的理论
我听到的一个流行理论是,Google 希望吸引更多客户使用其云服务。如果他们将 TPU 卖给其他云服务提供商,他们在云服务市场中的竞争力将减弱。
根据云服务客户的说法,这一理论并不十分合理。没有企业级客户愿意被锁定在某一特定的云服务提供商。他们希望具有足够的灵活性,能够在需要时迁移到其他提供商。否则,如果服务提供商提高价格,他们将无能为力。
如果 TPU 只能在 Google Cloud 上使用,许多企业客户宁愿不使用它。这也是为什么很多客户不愿意使用 TPU 的原因。直到最近,当 OpenXLA(一个中介软件)支持更多框架,如 PyTorch 时,他们才开始感觉到不那么受限。
所以,使用 TPU 吸引客户到 Google Cloud 并不是不出售它们的合理理由。那么,真正的原因是什么呢?为了更好地回答这个问题,我们必须探讨 Google 是如何启动 TPU 项目的。
为什么 Google 启动了 TPU 项目?
简短的回答是为了专有用途。曾经,GPU 无法满足 AI 硬件的计算需求。
让我们试着估算 TPU 项目大约什么时候开始的。考虑到它在2016 年首次对外公布,我们可以合理推测它大约在 2011 年左右开始。如果这个推测正确,那么他们的项目启动得相当早,因为直到 2012 年 AlexNet 的出现,计算机视觉才取得了显著的进展。
通过这个时间线,我们知道在项目启动时,GPU 的性能不如今天。Google 很早就看到了 AI 革命,并希望为大规模计算提供更快的硬件。他们唯一的选择是为此构建一个新的解决方案。
这就是 Google 启动该项目的原因,但还有更多的问题。为什么当时 GPU 不够好?Google 看到了哪些潜在的改进,足够重要以至于启动了他们的新硬件项目?
答案在于 GPU 和 TPU 的微架构。让我们来看看 GPU 和 TPU 核心的设计。
GPU 的设计理念
首先,让我们快速回顾一下 CPU 的背景知识。当一个指令到来时,它会被指令解码器解码,并与寄存器中的数据一起送入算术逻辑单元(ALU)。ALU 负责所有计算并将结果返回到其中一个寄存器。如果 CPU 有多个核心,它们可以并行工作。
什么是 GPU?它是图形处理单元(Graphics Processing Unit)的缩写。它最初是为图形计算设计的,后来发现它适用于机器学习。GPU 中的大多数操作是矩阵运算,可以并行执行。这也意味着,与 CPU 相比,GPU 需要支持的操作较少。
芯片针对特定任务越专业,完成任务的速度就越快。
GPU 最初设计的核心理念是将 CPU 的功能进行简化,采用更小但更多的核心,以实现更快的并行计算。GPU 上支持的指令数量远少于 CPU,这使得单个核心在芯片上所占的面积更小。这样,它们可以将更多的核心集成到芯片上,进行大规模并行计算。
为什么特性越少意味着芯片上占用的面积越小?在软件中,更多的特性意味着更多的代码;在硬件中,所有特性都是通过逻辑电路实现的,而不是代码。更多特性意味着电路更复杂。例如,CPU 必须在芯片上实现更多的指令。
更小也意味着更快。更简单的逻辑门设计导致了更短的周期时间。
TPU 的设计理念
TPU 进一步发展了这种专门用于深度学习的芯片理念。TPU 的定义性特征是其矩阵乘法单元(MXU)。由于矩阵乘法是深度学习中最常见的操作,TPU 为此构建了一个专门的核心,即 MXU。
这比 GPU 核心更为专门化,能够执行多种矩阵操作,而 MXU 只做一件事:矩阵乘法。
它的工作方式与传统的 CPU/GPU 核心非常不同。所有的动态性和通用性都被去除了。它有一个网格状的节点,每个节点仅按预定义的方式进行乘法和加法。结果会直接传送到下一个节点,进行下一个乘法和加法。因此,一切都是预定义和固定的。
通过这种方式,我们节省了时间,因为不需要进行指令解码,它只会进行接收到的乘法和加法。没有用于读写的寄存器,因为我们已经知道结果应该去哪里,也无需为随后的任意操作存储结果。
除了 MXU,TPU 还被设计为更具可扩展性。它具有用于高带宽芯片间互联(ICI)的专用端口。它被设计为能够安装在 Google 数据中心的机架上并用于集群。由于它仅供专有使用,因此不需要担心销售单个芯片或安装芯片到机架上的复杂性。
TPUs 今天仍然更快吗?
其他人没有想到构建专用的张量操作核心(矩阵乘法)的简单想法是没有道理的。即使他们没有想到这一点,后来也不应该不去模仿。
从时间线来看,似乎 Nvidia 在大约相同的时间提出了相同的想法。Nvidia 的类似产品——张量核心(Tensor Cores)——首次公开宣布是在 2017 年,也就是 Google 发布 TPU 的一年后。
目前尚不清楚 TPU 是否仍然比 GPU 更快。我无法找到最新一代 TPU 和 GPU 的公开基准测试,而且我不清楚应该使用哪个代数和度量标准来进行基准测试。
然而,我们可以使用一个通用的面向应用的度量标准:每轮的花费(dollars per epoch)。我发现一个有趣的基准测试来自 Google Cloud,它将不同的硬件对齐到相同的轴心:金钱。如果你使用相同的模型、数据和轮次,在 Google Cloud 上 TPU 看起来更便宜。
大型模型,如 Midjourney、Claude 和 Gemini,都对训练成本非常敏感,因为它们消耗大量计算资源。因此,它们中的许多都在 Google Cloud 上使用 TPU。
为什么 TPU 更便宜?
一个重要的原因是软件堆栈。你不仅使用硬件,还使用与之相关的软件堆栈。Google 在其软件堆栈和 AI 硬件的垂直集成上,比 GPU 做得更好。
Google 拥有专门的工程团队来为其构建完整的软件堆栈,并实现强大的垂直集成,从模型实现(Vertex Model Garden)到深度学习框架(Keras、JAX 和 TensorFlow),再到为 TPU 优化的编译器(XLA)。
GPUs 的软件堆栈非常不同。PyTorch 是与 Nvidia GPU 一起使用的最流行的深度学习框架,主要由 Meta 开发。与 PyTorch 一起使用的最广泛应用的模型库是 HuggingFace 开发的transformers和diffusers。在这些公司之间实现完美的垂直集成的软件堆栈要困难得多。
一个警告是,使用 JAX 和 TensorFlow 实现的模型较少。有时,你可能需要自己实现模型,或者在 TPU 上使用 PyTorch。根据实现的不同,在 TPU 上使用 PyTorch 时,可能会遇到一些摩擦。因此,除了硬件成本外,可能还会有额外的工程成本。
为什么不开始直接销售 TPU 呢?
我们了解到,该项目最初是为专有用途启动的,并由于其较低的价格,在 Google Cloud 上获得了相当不错的用户基础。为什么 Google 不直接开始将其卖给客户,就像 Nvidia 的 GPU 一样?
简短的回答是保持专注。Google 与 OpenAI 在生成式 AI 领域的竞争非常激烈。同时,Google 正处于多轮科技裁员中,以降低成本。当前的明智策略是将有限的资源集中在最重要的项目上。
如果 Google 想要开始销售 TPU,它将同时与两个强大的对手 Nvidia 和 OpenAI 竞争,这在目前可能不是一个明智的举动。
销售硬件的巨大开销
直接向客户销售硬件为公司带来巨大的开销。相反,租赁云服务上的 TPU 要更容易管理得多。
当 TPU 仅在云端提供时,他们可以集中安装所有 TPU 和相关软件。无需处理各种安装环境或部署 TPU 集群的难题。
他们准确知道需要生产多少 TPU。需求完全是内部需求,因此没有不确定性。因此,供应链管理要容易得多。
销售也变得更加简便,因为这仅仅是在销售云服务。无需组建一个有经验销售硬件的新团队。
TPU 方法的优势
在没有直接向客户销售硬件的所有额外开销下,Google 获得了几项优势。
首先,他们可以采用更具攻击性的 TPU 架构设计。TPU 具有一种独特的芯片连接方式。与多个 GPU 连接到同一主板不同,TPU 被组织成立方体的形式。他们将 64 个 TPU 排列成一个 4x4x4 的立方体,将它们彼此连接,以实现更快的芯片间通信。单个 v5p Pod 中有 8960 个芯片。它们可以轻松地一起使用。这是完全控制硬件安装环境的优势。
第二,他们可以更快地进行迭代,推出新一代硬件。由于他们只需支持一小部分专有用途的应用场景,这大大缩短了每一代芯片的研发周期。我想知道英伟达是否早于谷歌想到了 TensorCore 的概念,但由于向外部客户销售硬件的额外开销,它们只能比谷歌晚一年才发布。
从竞争 GenAI 这个最重要目标的角度来看,这些优势使谷歌处于非常有利的地位。最重要的是,通过这种内部硬件解决方案,谷歌避免了以垄断价格购买英伟达 GPU,从而节省了大量资金。
TPU 方法的缺点
到目前为止,我们讨论了谷歌 AI 硬件方法的许多优点,但它是否也有缺点?确实有一个大缺点。谷歌变成了一个技术孤岛。
每一个技术先锋都将在一段时间内变成一个与世界其他地方隔离的孤岛。这是因为他们在相应的基础设施尚未就绪时就开始了。他们需要从头开始建立一切。由于迁移成本,他们会坚持自己的解决方案,即使其他人都在使用其他方案。
这正是谷歌目前所面临的情况。外界正在通过 HuggingFace 和 PyTorch 的模型进行创新,大家迅速互相调整模型,以开发出更好的模型。然而,由于谷歌的基础设施主要围绕 TensorFlow 和 JAX 构建,它不能轻松地加入这个过程。当将外部模型投入生产时,必须重新用谷歌的框架进行实现。
这种“技术孤岛”问题使得谷歌在从外部世界采纳优秀解决方案时变得缓慢,进一步将其与他人隔离开来。谷歌要么开始引入更多的外部解决方案,如 HuggingFace、PyTorch 和 GPU,要么始终确保其内部解决方案在全球范围内是最优秀的。
未来的 AI 硬件会是什么样子?
最后,让我们展望一下 AI 硬件的未来。未来的 AI 硬件会是什么样子?简短的回答是:随着硬件变得更加专业化,出现模式崩溃现象。
硬件将进一步与应用耦合。例如,支持更多精度格式以更好地为语言模型服务。像bfloat16、TF32一样,它们可能会更好地支持int8和int4。Nvidia 宣布了其第二代变压器引擎,该引擎与 Blackwell GPU 兼容。这使得在不改变用户代码的情况下,更容易优化硬件以适应变压器模型。很多共同设计工作正在进行中。
另一方面,软件也难以跳出变压器领域。如果它们做到了,由于缺乏硬件支持,它们的速度将会很慢。相反,它们是在考虑硬件的前提下实施自己的模型。例如,FlashAttention算法就是为了利用 GPU 的内存层次结构来提高性能。
我们预计很快会出现一次大的模式崩溃。当前的硬件和软件在现有模型中彼此优化得非常好。它们都无法轻易脱离现有的设计或算法。如果有一种全新、完全不同于变压器模型的模型,它需要比现有模型好 10 倍才能广泛采用。它必须激励人们像对变压器那样快速且廉价地制造新硬件。
总结
总结来说,TPU 项目最初是为专有用途而启动的,当时 GPU 的计算能力不足。谷歌希望专注于 GenAI,而不是在 AI 硬件市场上竞争,以避免减缓迭代速度并牺牲其创新设计。更快的计算和更低的成本帮助谷歌在 AI 研究和应用开发上取得了显著进展。然而,这也使谷歌成为了一个技术孤岛。
展望未来,AI 硬件将会更加优化,以适应特定的应用,比如变压器模型。无论是硬件还是模型,都难以轻易跳出这种模式崩溃的局面。
追踪图中的变压器
具体要输入什么,输出什么,并且你是如何生成文本的?
·发表于Towards Data Science ·15 分钟阅读 ·2024 年 11 月 7 日
--
上周,我在听一集关于 Nvidia 的 Acquired播客。这集播客讲到了变压器:GPT 中的T,以及 21 世纪可能最大的发明之一。
在走下 Beacon 街的时候,我一边听着,一边想着,我理解变压器了,对吧?在训练过程中,你会屏蔽掉一些 token,你有这些学习如何连接文本中概念的注意力头,然后你预测下一个单词的概率。我从 Hugging Face 下载了 LLM 并进行过一些尝试。在早期,我使用过 GPT-3,还没出现“聊天”功能时。在 Klaviyo,我们甚至开发了首批基于 GPT 的生成性 AI 功能之一——我们的主题行助手。很久以前,我也曾参与过一个基于旧式语言模型的语法检查器的开发。所以,也许吧。
变压器是由谷歌的一个团队发明的,他们在做自动化翻译,比如从英语到德语。它在 2017 年通过那篇现在广为人知的论文《Attention Is All You Need》向世界介绍。我调出了这篇论文,并看了图 1:

来自《Attention Is All You Need》的图 1
嗯……如果我理解的话,那只是最初的粗略了解。当我越来越仔细地看着图表,读着论文时,我才意识到我并没有完全理解细节。以下是我写下的一些问题:
-
在训练过程中,输入是英语的标记化句子,输出是德语的标记化句子吗?
-
在一个训练批次中,具体每一项是什么?
-
为什么要将输出输入到模型中?以及“掩码多头注意力”是如何足以防止通过学习输出结果来作弊的?
-
多头注意力究竟是什么?
-
损失是如何计算的?不可能是先输入源语言句子,翻译整个句子,再计算损失,这样不合理。
-
训练后,究竟该输入什么来生成翻译?
-
为什么有三条箭头指向多头注意力模块?
我敢肯定,对于两类人来说,这些问题既简单又显得幼稚。第一类是那些已经在使用类似模型(例如 RNN、编码器-解码器)做类似事情的人。当他们读到这篇论文时,肯定立刻就理解了 Google 团队的成就以及他们是如何做到的。第二类是过去七年里意识到 Transformer 重要性的更多人,并且花时间学习其中的细节。
好吧,我想学习这个,于是我觉得最好的方法是从头开始构建模型。我很快就迷失了方向,决定追踪别人写的代码。我找到了一份很棒的笔记,它解释了这篇论文,并在 PyTorch 中实现了模型。我复制了代码并训练了模型。我将所有内容(输入、批次、词汇、维度)都做得非常小,这样我就可以追踪每一步发生了什么。我发现,在图表上标注维度和张量帮助我理清了思路。到我完成时,我已经对上面所有的问题有了相当不错的答案,接下来我会在讲解图表之后回答它们。
这是我整理过的笔记版本。本部分的内容是为了训练一个单一的、非常小的批次,这意味着不同图表中的所有张量都是一组的。
为了让内容更易于跟随,并借鉴了笔记中的一个想法,我们将训练模型来复制标记。例如,一旦训练完成,“dog run” 应该翻译成 “dog run”。

换句话说:

下面试图用文字解释一下图表中张量维度(以紫色显示)到目前为止的含义:

其中一个超参数是 d-model,在论文中的基础模型中,它是 512。这个例子中我设定为 8。这意味着我们的嵌入向量长度为 8。这里再次展示主图,标记了多个地方的维度:

让我们放大看看编码器的输入:


图中显示的大部分模块(加法与归一化、前馈神经网络、最终线性变换)只作用于最后一个维度(8)。如果仅仅是这样的话,那么模型只能利用序列中单一位置的信息来预测单一位置。某个地方必须“混合”位置之间的信息,这个魔法发生在多头注意力模块中。
让我们放大查看编码器中的多头注意力模块。在接下来的图示中,请记住,在我的例子中,我将超参数h(头数)设置为2。(在论文中的基础模型中,它是 8。)

来自《Attention Is All You Need》的图 2,带有作者注释
(2,3,8)是如何变成(2,2,3,4)的?我们进行了线性变换,然后将结果拆分成头数(8 / 2 = 4),并重新排列张量的维度,使得我们的第二个维度就是头。让我们来看一些实际的张量:

我们仍然没有做任何会在位置之间混合信息的操作。那将发生在接下来的缩放点积注意力模块中。维度“4”和维度“3”最终会接触在一起。

来自《Attention Is All You Need》的图 2,带有作者注释
让我们看看这些张量,但为了更容易理解,我们只关注批次中的第一个项目和第一个头。换句话说,就是 Q[0,0],K[0,0],等等。其他三个头也会进行同样的操作。

让我们看一下软最大输出与 V 之间的最终矩阵乘法:

从最开始开始回溯,我们可以看到,在这次乘法之前,V 中的三个位置一直都是独立操作的,一直到我们原始句子“
回到多头注意力的示意图,我们可以看到,concat 操作将每个头的输出重新组合在一起,因此每个位置现在由长度为 8 的向量表示。注意,concat 后但在线性变换前的张量中的1.8和-1.1与上面显示的经过缩放点积注意力后,批次中第一个项目、第一头第一个位置的向量中的1.8和-1.1相匹配。(接下来的两个数字也匹配,只不过它们被省略号隐藏了。)

现在,让我们回到整个编码器的视图:

起初我以为我需要详细追踪前馈网络块。论文中称它为“位置-wise 前馈网络”,我以为这意味着它可能会将信息从一个位置传递到右侧的其他位置。然而,事实并非如此。“位置-wise”意味着它在每个位置上独立运算。它对每个位置进行线性变换,从 8 个元素变换到 32 个元素,然后进行 ReLU(取 0 和数字中的最大值),接着再做一次线性变换回到 8 个元素。(这是在我们的小例子中。在论文中的基础模型中,它从 512 变到 2048,再回到 512。这里有很多参数,可能是学习发生的主要地方!)前馈网络的输出回到(2,3,8)。
先暂时离开我们的简化模型,看看论文中基础模型里的编码器是怎样的。输入和输出的维度匹配真是太好了!

现在让我们拉远视角,看看解码器。

我们不需要追踪解码器的绝大部分内容,因为它和我们刚刚在编码器端看到的非常相似。然而,我标记为A和B的部分是不同的。A不同是因为我们做了掩蔽的多头注意力。这应该是避免在训练时“作弊”的关键。B稍后我们会再回到。首先,让我们隐藏内部细节,保持对解码器输出结果的大致图景。

为了更好地强调这一点,假设我们的英文句子是“she pet the dog”,而翻译成 Pig Latin 后的句子是“eshay etpay ethay ogday”。如果模型已经有了“eshay etpay ethay”,并且正在试图推测下一个词,“ogday”和“atcay”都是高概率选择。考虑到完整英文句子“she pet the dog”的上下文,模型应该能够选择“ogday”。然而,如果模型在训练期间能够看到“ogday”,它就不需要通过上下文来预测,它只需要学会复制。
让我们看看掩码是如何做到这一点的。我们可以跳过一些步骤,因为A的第一部分和之前一样,都是应用线性变换并将东西分割成头部。唯一不同的是进入缩放点积注意力部分的维度是(2,2,2,4),而不是(2,2,3,4),因为我们原始的输入序列长度是 2。这里是缩放点积注意力部分。正如我们在编码器端所做的那样,我们只看批次中的第一个项目和第一个头部。

这次我们有一个掩码。让我们看看 softmax 的输出和 V 之间的最终矩阵乘法:

现在我们准备好看看B,解码器中的第二个多头注意力块。与其他两个多头注意力块不同,我们并没有输入三个相同的张量,因此我们需要思考 V、K 和 Q 分别代表什么。我用红色标出了输入。可以看到,V 和 K 来自编码器的输出,并且维度是(2,3,8)。Q 的维度是(2,2,8)。

和之前一样,我们跳到缩放点积注意力部分。V 和 K 的维度是(2,2,3,4)——批量中有两个项目,两个头,三个位置,长度为四的向量,而 Q 的维度是(2,2,2,4)。这很合理,但也有些令人困惑。

即使我们是在“读取”编码器输出,其中“序列”长度为三,所有的矩阵计算也能顺利进行,我们最终得到了所需的维度(2,2,2,4)。让我们来看一下最终的矩阵乘法:

每个多头注意力块的输出会被加在一起。让我们跳过到解码器的输出部分,并将其转换为预测:

线性变换将我们从(2,2,8)转换为(2,2,5)。可以把它看作是反向嵌入,除了我们不是从长度为 8 的向量转到单个标记的整数标识符,而是转到一个包含 5 个标记的词汇表上的概率分布。在我们这个小示例中,数字看起来有点奇怪。在论文中,这更像是从大小为 512 的向量转到包含 37,000 个词汇的词汇表,当时他们做的是英语到德语的翻译。
稍后我们将计算损失。不过,即使是匆匆一瞥,你也可以大致感知模型的表现。

它正确预测了一个标记。毫不奇怪,因为这是我们的第一个训练批次,而且一切都是随机的。这个图的一个优点是,它清晰地表明这是一个多类分类问题。类别是词汇表(在这个例子中有 5 个类别),这正是我之前感到困惑的地方,我们对翻译句子中的每个标记做出(并评分)一个预测,而不是对每个句子做一个预测。让我们进行实际的损失计算。

比如说,如果-3.2 变成了-2.2,那么我们的损失将减少到 5.7,朝着我们希望的方向移动,因为我们希望模型学习到,第一个标记的正确预测是 4。
上面的图省略了标签平滑。在实际论文中,损失计算会平滑标签并使用 KL 散度损失。我认为当没有平滑时,这样的损失计算结果与交叉熵相同或相似。下面是与上图相同的图,但添加了标签平滑。

让我们也快速看一下在编码器和解码器中学习的参数数量:

作为一个检查,我们的玩具模型中的前馈块有一个从 8 到 32 再回到 8 的线性变换(如上所述),因此是 8 * 32(权重)+ 32(偏置)+ 32 * 8(权重)+ 8(偏置)= 52。请记住,在论文中的基础模型中,d-model是 512,d-ff是 2048,并且有 6 个编码器和 6 个解码器,因此会有更多的参数。
使用训练好的模型
现在让我们看看如何将源语言文本输入并得到翻译后的文本。我这里仍然使用一个玩具模型,通过复制 token 来“翻译”,但与上面的例子不同,这里使用的是大小为 11 的词汇表,并且d-model为 512。(上面我们有一个大小为 5 的词汇表,d-model是 8。)
首先让我们做一次翻译。然后我们再来看它是如何工作的。

第一步是将源句子输入编码器,并保留其输出,在本例中是一个维度为(1, 10, 512)的张量。

第二步是将输出的第一个 token 输入解码器,并预测第二个 token。我们知道第一个 token,因为它总是

在本文中,他们使用了束搜索(beam search),束大小为 4,这意味着我们将考虑此时概率最高的 4 个 token。为了简化,我将改用贪心搜索。你可以把它看作是束大小为 1 的束搜索。因此,从图表的顶部读取,概率最高的 token 是编号5。(上面的输出是概率的对数。概率最高的仍然是最大数字。在这个例子中,是-0.0,实际上是-0.004,但我只显示到一位小数。模型非常确定 5 是正确的!exp(-0.004) = 99.6%)
现在我们将[1,5]输入解码器。(如果我们在进行束搜索并且束大小为 2,我们可以将包含[1,5]和[1,4](下一个最可能的 token)的批次输入,这样可以得到下一步的结果。)

现在我们将[1,5,4]输入:

并输出3。一直这样进行,直到我们得到一个表示句子结束的 token(在我们的示例词汇表中不存在)或者达到最大长度。
回到上面的问题
现在我大致可以回答我最初的问题了。
在训练过程中,输入是用英语分词的句子,输出是用德语分词的句子吗?
是的,大致如此。
训练批次中的每个项目究竟是什么?
每个项目对应一个翻译后的句子对。
-
项目的“x”有两部分。第一部分是源句子的所有 token。第二部分是目标句子的所有 token,除了最后一个。
-
项目的“y”(标签)是目标句子的所有 token,除了第一个。由于源句子和目标句子的第一个 token 总是
,所以我们并没有浪费或丢失任何训练数据。
有一点比较微妙,如果这是一个分类任务,例如模型需要接收一张图像并输出一个类别(比如房子、汽车、兔子等),我们会认为批次中的每个项目都会对损失计算贡献一个“分类”。然而,在这里,批次中的每个项目将会对损失计算贡献(目标句子的 tokens 数量 — 1)个“分类”。
为什么要将输出数据输入模型,且“掩码多头注意力”又如何足以防止模型通过学习输出直接生成输出结果?
你将输出数据馈送给模型,以便模型能够基于源句子的意思和目前已翻译的单词预测翻译。虽然模型中有很多事情在进行,但信息在各个位置之间传递的唯一时机是在注意力步骤中。尽管我们确实将翻译后的句子输入解码器,但第一次注意力计算使用掩码将所有超出当前预测位置的信息清零。
什么是多头注意力?
我可能应该先问一下什么是注意力机制,因为它是更核心的概念。多头注意力意味着将向量切分成若干组,对每组进行注意力计算,然后再将这些组合并起来。例如,如果向量的大小是 512,且有 8 个头,那么注意力将独立地在 8 个组上进行,每组包含一个完整批次的所有位置,每个位置的向量大小为 64。如果你稍微思考一下,你会发现每个头可以学习集中注意力于某些相关的概念,正如那些著名的可视化展示所示,头部会学习代词指代的是哪个词。
损失究竟是如何计算的?不可能是将源语言句子翻译完整个句子后再计算损失,这样不合理吧。
对的。我们不是一次性翻译整个句子然后计算整个句子的相似度或类似的东西。损失的计算方式与其他多分类问题类似。类别就是我们词汇表中的 token。诀窍在于,我们独立地预测目标句子中每个 token 的类别,且仅使用此时应当拥有的信息。标签是我们目标句子中的实际 token。通过使用预测和标签,我们利用交叉熵计算损失。(实际上,我们对标签进行了“平滑”,以考虑到它们不是绝对的,有时同义词也能起到同样的作用。)
训练完成后,生成翻译时究竟输入什么呢?
你不能直接输入某些内容并让模型在一次评估中输出翻译结果。你需要多次使用模型。首先将源句子输入到模型的编码器部分,得到表示句子含义的编码版本,这种表示是以某种抽象、深层次的方式进行的。然后将该编码信息和起始标记
为什么有三条箭头指向多头注意力块?
我猜有三个原因。1)展示解码器中第二个多头注意力块的输入部分来自编码器和解码器前一个块的输入。2)暗示注意力算法是如何工作的。3)暗示每个输入都在实际进行注意力计算之前经历独立的线性变换。
结论
这太美妙了!如果它不这么有用,我可能不会这么想。我现在能理解人们第一次看到这个工作原理时的感受。这个优雅且可训练的模型,用极少的代码就能表达,学会了如何翻译人类语言,并打败了那些花费几十年构建的复杂机器翻译系统。它令人惊叹、聪明且难以置信。你可以看到,下一步就是抛开翻译句对,开始将这种技术应用到互联网上的每一段文字——大型语言模型(LLM)由此诞生!
(我猜上面有一些错误,请告诉我。)
除非另有说明,所有图片均由作者提供,或为作者在 Attention Is All You Need中对图示的注释。
使用 MLflow 追踪计算机视觉实验
发现如何设置高效的 MLflow 环境,以跟踪你的实验,比较并选择最适合部署的模型
·发表于Towards Data Science ·9 分钟阅读·2024 年 12 月 26 日
--
训练和微调各种模型是每个计算机视觉研究人员的基础任务。即使是简单的模型,我们也会进行超参数搜索,以找到在我们自定义数据集上训练模型的最佳方式。数据增强技术(已包括许多不同的选项)、优化器的选择、学习率以及模型本身。这个架构对我来说是最合适的吗?我是否应该添加更多的层,改变架构,很多问题都等待着被提问和探索?
在寻找所有这些问题的答案时,我曾将模型训练过程的日志文件和输出检查点保存在本地的不同文件夹中,每次运行训练时都更改输出目录名称,并手动一项一项比较最终的指标。以如此手动的方式处理实验跟踪过程有许多缺点:它是老派的,耗时耗力,且容易出错。
在这篇博客中,我将向你展示如何使用 MLflow,这是一个追踪实验的最佳工具之一,能够记录你需要的任何信息,直观地比较你完成的不同训练实验,并在一个用户友好(也符合眼睛友好)环境中,帮助你决定哪个训练是最佳选择!
跟踪你的机器学习实验
使用 Neptune.ai 跟踪 Python 中的实验指南
·发布在Towards Data Science ·阅读时间:7 分钟·2024 年 3 月 29 日
--

每位数据科学家都对实验有所了解。
你知道流程的。你获取一个数据集,将其加载到 Jupyter 笔记本中,进行探索,预处理数据,拟合一个或两个基准模型,然后训练一个初步的最终模型,例如 XGBoost。第一次可能没有调整超参数并且包含了 20 个特征。然后,你检查你的误差指标。
它们看起来还不错,但也许你的模型有些过拟合。所以你决定调整一些正则化参数(例如最大深度),以降低模型的复杂性,并重新运行它。
你从上次运行中看到了一些改进,但也许你还想:
-
添加更多特征
-
执行特征选择并去除一些特征
-
尝试为你的特征使用不同的标准化方法
-
调整不同/更多的超参数
随着你想要运行的不同种类的测试增加,记住哪些“实验”组合实际上产生了最佳结果变得越来越困难。你只能运行一个笔记本那么多次,打印出结果,然后复制/粘贴到 Google 文档中,直到你感到沮丧。
使用 AI 气象模型追踪飓风
数据科学家的风暴追踪:飓风米尔顿案例研究
·发布于数据科学前沿 ·6 分钟阅读·2024 年 10 月 11 日
--

图片由Zoshua Colah拍摄,来源于Unsplash
2024 年 10 月 9 日星期三,飓风米尔顿登陆。风暴在傍晚时分到达佛罗里达西海岸。这是 2024 年迄今为止最强的热带气旋。
在这里,我探索了 AI 气象模型PanguWeather是如何预测飓风米尔顿的。本文包括了代码片段,帮助你复制并扩展我的分析。
AI 气象模型使用历史天气数据来预测今天的天气。
要生成你自己的 AI 天气预报,你需要一个预训练的天气模型和初始化数据。这些都可以通过Github从欧洲中期天气预报中心(ECMWF)获得。
我使用了欧洲中期天气预报中心提供的预训练模型和初始化数据来创建全球天气预报。
按照安装说明进行操作,开始进行预报:
ai-models --assets panguweather --input ecmwf-open-data panguweather
我的预测的开始日期是当地时间(EDT)10 月 8 日,星期二,上午 08:00。 在一台笔记本电脑的 CPU 上完成此任务花费了 1.4 小时。 使用 GPU 的话,PanguWeather 可以在几分钟内提供全球预报。
实践中的跟踪:代码、数据与机器学习模型
MLOps 中的跟踪指南
· 发布于 Towards Data Science · 11 分钟阅读·2024 年 7 月 9 日
--

跟踪!无论你是研究人员还是工程师,不论你是从事机器学习、数据科学、软件开发,还是甚至是分析工具(别介意,我最近迷上了惊悚小说)!我想说的是,跟踪既重要又不可避免。在 MLOps 中,我们跟踪其所有组件:代码、数据和机器学习模型!在这篇文章中,我们通过一个实际示例解释了跟踪的重要性,并在机器学习工作流的不同步骤中应用了测试。本文的完整代码库可以在关联的仓库中获取。
还不是 Medium 会员?没关系!继续阅读,使用此好友链接。
目录:
· 1. 介绍
· 2. 项目设置
· 3. 代码跟踪
· 4. 数据跟踪
· 5. 机器学习模型跟踪
· 6. 结论
1. 介绍

MLOps 原则
使用卫星图像跟踪大盐湖的缩小(Python)
分析 Landsat-8 影像(2014–2023)并使用 Python 在 Google Colab 中创建大盐湖表面积的时间序列
·发表于 Towards Data Science ·17 分钟阅读·2024 年 2 月 28 日
--

大盐湖地区的时间流逝影像,基于 2014 年至 2023 年的无云 Landsat-8 图像,由作者可视化
目录
-
🌅 大盐湖缩小问题简介
-
💾 下载 Landsat-8 图像
-
📈 来自统计文件的大盐湖地区时间序列
-
⚙️ 处理 Landsat-8 图像
-
🗺️ 大盐湖图像的可视化
-
🎥 大盐湖缩小的时间流逝影像
-
📉 基于分类图像的大盐湖地区时间序列
-
⚖️ 比较统计文件和图像中的时间序列
-
📄 结论
-
📚 参考文献
🌅 大盐湖缩小问题简介
美国犹他州的大盐湖正在经历长期缩小。根据多项报告,与 1986 年最大水面相比,湖泊已缩小超过 30%。关于湖泊水位下降,有几个原因被提及……
用 60 行代码训练/微调 Segment Anything 2 (SAM 2)
一步步的教程,教你如何微调 SAM2 以完成自定义分割任务
·发表于 Towards Data Science ·13 分钟阅读·2024 年 8 月 3 日
--
SAM2 (Segment Anything 2) 是 Meta 推出的一款新模型,旨在对图像中的任何内容进行分割,而不局限于特定类别或领域。该模型的独特之处在于它的训练数据规模:1100 万张图片和 110 亿个掩码。这种广泛的训练使得 SAM2 成为一个强大的起点,可以用于新的图像分割任务的训练。
你可能会问,如果 SAM 可以分割任何东西,为什么还需要重新训练它?答案是,SAM 在处理常见物体时表现优异,但在处理稀有或特定领域的任务时,效果可能较差。
然而,即使在 SAM 给出的结果不足的情况下,仍然可以通过在新数据上对模型进行微调,显著提高模型的能力。在许多情况下,这需要的训练数据更少,且效果比从头开始训练一个模型更好。
本教程展示了如何用仅 60 行代码(不包括注释和导入)在新数据上微调 SAM2。
完整的训练脚本可以在以下位置找到:
[## fine-tune-train_segment_anything_2_in_60_lines_of_code/TRAIN.py at main ·…
该代码库提供了训练/微调 Meta Segment Anything Model 2 (SAM 2) 的代码……
SAM2 网络结构图,来自SAM2 GIT 页面
Segment Anything 的工作原理
SAM 的主要工作原理是通过获取图像和图像中的一个点,预测包含该点的分割区域的掩膜。该方法能够在没有人工干预的情况下进行完整的图像分割,并且不受类别或分割类型的限制(如在上一篇文章中所讨论的)。
使用 SAM 进行完整图像分割的步骤:
-
在图像中选择一组点
-
使用 SAM 预测包含每个点的分割区域
-
将生成的分割区域合并成一个单一的地图
虽然 SAM 也可以利用其他输入,如掩膜或边界框,但这些主要适用于涉及人工输入的交互式分割。对于本教程,我们将专注于完全自动化的分割,并仅考虑单点输入。
有关该模型的更多细节,请访问项目网站
下载 SAM2 并设置环境
SAM2 可以从以下链接下载:
[## GitHub - facebookresearch/segment-anything-2: 该仓库提供了用于运行推理的代码…
该仓库提供了用于运行 Meta Segment Anything Model 2(SAM 2)推理的代码,链接如下…
如果你不想复制训练代码,你也可以下载我已经包含 TRAIN.py 脚本的 fork 版本。
[## GitHub - sagieppel/fine-tune-train_segment_anything_2_in_60_lines_of_code: 该仓库提供了…
该仓库提供了用于训练/微调 Meta Segment Anything Model 2(SAM 2)的代码…
请按照 GitHub 仓库中的安装说明进行操作。
通常,你需要 Python >=3.11 和PyTorch
此外,我们将使用 OpenCV,可以通过以下命令进行安装:
pip install opencv-python
下载预训练模型
你还需要从以下链接下载预训练模型:
https://github.com/facebookresearch/segment-anything-2?tab=readme-ov-file#download-checkpoints
有几个模型你可以选择,这些模型都与本教程兼容。我建议使用小型模型,它是训练速度最快的。
下载训练数据
对于本教程,我们将使用LabPics1 数据集来分割材料和液体。你可以从这个网址下载数据集:
zenodo.org/records/3697452/files/LabPicsV1.zip?download=1
准备数据读取器
我们需要编写的第一部分是数据读取器。它将读取并准备数据以供网络使用。
数据读取器需要生成:
-
一张图像
-
图像中所有分割区域的掩膜。
-
以及一个每个掩膜内的随机点
让我们从加载依赖项开始:
import numpy as np
import torch
import cv2
import os
from sam2.build_sam import build_sam2
from sam2.sam2_image_predictor import SAM2ImagePredictor
接下来列出数据集中所有的图像:
data_dir=r"LabPicsV1//" # Path to LabPics1 dataset folder
data=[] # list of files in dataset
for ff, name in enumerate(os.listdir(data_dir+"Simple/Train/Image/")): # go over all folder annotation
data.append({"image":data_dir+"Simple/Train/Image/"+name,"annotation":data_dir+"Simple/Train/Instance/"+name[:-4]+".png"})
现在是加载训练批次的主要函数。训练批次包括:一张随机图像,属于此图像的所有分割掩膜,以及每个掩膜中的一个随机点:
def read_batch(data): # read random image and its annotaion from the dataset (LabPics)
# select image
ent = data[np.random.randint(len(data))] # choose random entry
Img = cv2.imread(ent["image"])[...,::-1] # read image
ann_map = cv2.imread(ent["annotation"]) # read annotation
# resize image
r = np.min([1024 / Img.shape[1], 1024 / Img.shape[0]]) # scalling factor
Img = cv2.resize(Img, (int(Img.shape[1] * r), int(Img.shape[0] * r)))
ann_map = cv2.resize(ann_map, (int(ann_map.shape[1] * r), int(ann_map.shape[0] * r)),interpolation=cv2.INTER_NEAREST)
# merge vessels and materials annotations
mat_map = ann_map[:,:,0] # material annotation map
ves_map = ann_map[:,:,2] # vessel annotaion map
mat_map[mat_map==0] = ves_map[mat_map==0]*(mat_map.max()+1) # merged map
# Get binary masks and points
inds = np.unique(mat_map)[1:] # load all indices
points= []
masks = []
for ind in inds:
mask=(mat_map == ind).astype(np.uint8) # make binary mask
masks.append(mask)
coords = np.argwhere(mask > 0) # get all coordinates in mask
yx = np.array(coords[np.random.randint(len(coords))]) # choose random point/coordinate
points.append([[yx[1], yx[0]]])
return Img,np.array(masks),np.array(points), np.ones([len(masks),1])
这个函数的第一部分是选择一张随机图像并加载它:
ent = data[np.random.randint(len(data))] # choose random entry
Img = cv2.imread(ent["image"])[...,::-1] # read image
ann_map = cv2.imread(ent["annotation"]) # read annotation
请注意,OpenCV 读取图像时是 BGR 格式,而 SAM 期望的是 RGB 格式。通过使用[…,::-1],我们将图像从 BGR 转换为 RGB。
SAM 期望图像大小不超过 1024,因此我们将把图像和注释图调整为此大小。
r = np.min([1024 / Img.shape[1], 1024 / Img.shape[0]]) # scalling factor
Img = cv2.resize(Img, (int(Img.shape[1] * r), int(Img.shape[0] * r)))
ann_map = cv2.resize(ann_map, (int(ann_map.shape[1] * r), int(ann_map.shape[0] * r)),interpolation=cv2.INTER_NEAREST)
这里一个重要的点是,在调整注释图(ann_map)大小时,我们使用INTER_NEAREST模式(最近邻)。在注释图中,每个像素值是其所属分割区域的索引。因此,使用不会引入新值的调整方法非常重要。
下一个代码块特定于 LabPics1 数据集的格式。注释图(ann_map)包含图像中血管的分割图(一个通道),另一个通道则用于材料注释图。我们将把它们合并为一张单一的图。
mat_map = ann_map[:,:,0] # material annotation map
ves_map = ann_map[:,:,2] # vessel annotaion map
mat_map[mat_map==0] = ves_map[mat_map==0]*(mat_map.max()+1) # merged map
这将为我们提供一个地图(mat_map),其中每个像素的值是它所属的分割区域的索引(例如:所有值为 3 的像素属于分割区域 3)。我们希望将其转换为一组二进制掩膜(0/1),每个掩膜对应一个不同的分割区域。此外,我们还希望从每个掩膜中提取一个单一的点。
inds = np.unique(mat_map)[1:] # list of all indices in map
points= [] # list of all points (one for each mask)
masks = [] # list of all masks
for ind in inds:
mask = (mat_map == ind).astype(np.uint8) # make binary mask for index ind
masks.append(mask)
coords = np.argwhere(mask > 0) # get all coordinates in mask
yx = np.array(coords[np.random.randint(len(coords))]) # choose random point/coordinate
points.append([[yx[1], yx[0]]])
return Img,np.array(masks),np.array(points), np.ones([len(masks),1])
我们得到了图像(Img),一个与图像中各个分割区域对应的二进制掩膜列表(masks),以及每个掩膜内部的一个点的坐标(points)。

一批训练数据的示例:1) 一张图像。2) 分割掩膜列表。3) 对于每个掩膜,掩膜内的一个单一点(标记为红色)。数据来自 LabPics 数据集。
加载 SAM 模型
现在让我们加载网络:
sam2_checkpoint = "sam2_hiera_small.pt" # path to model weight
model_cfg = "sam2_hiera_s.yaml" # model config
sam2_model = build_sam2(model_cfg, sam2_checkpoint, device="cuda") # load model
predictor = SAM2ImagePredictor(sam2_model) # load net
首先,我们设置模型权重的路径:sam2_checkpoint参数。我们之前从这里下载了权重。“sam2_hiera_small.pt”指的是小模型,但是代码对任何模型都有效。无论你选择哪个模型,都需要在model_cfg参数中设置相应的配置文件。这些配置文件位于主存储库中的子文件夹“sam2_configs/”。
Segment Anything 的总体结构
在开始训练之前,我们需要了解模型的结构。
SAM 由三个部分组成:
1)图像编码器,2)提示编码器,3)掩码解码器。
图像编码器负责处理图像并创建图像嵌入。这是最大的组件,训练它将需要强大的 GPU。
提示编码器处理输入的提示,在我们的例子中就是输入的点。
掩码解码器接受图像编码器和提示编码器的输出,并生成最终的分割掩码。
设置训练参数:
我们可以通过设置来启用掩码解码器和提示编码器的训练:
predictor.model.sam_mask_decoder.train(True) # enable training of mask decoder
predictor.model.sam_prompt_encoder.train(True) # enable training of prompt encoder
你可以通过使用“predictor.model.image_encoder.train(True)”来启用图像编码器的训练。
这需要更强大的 GPU,但会给网络提供更多的改进空间。如果你选择训练图像编码器,必须扫描 SAM2 代码中的“no_grad”命令并将其删除。(no_grad会阻止梯度收集,虽然可以节省内存,但会阻止训练)。
接下来,我们定义标准的 adamW 优化器:
optimizer=torch.optim.AdamW(params=predictor.model.parameters(),lr=1e-5,weight_decay=4e-5)
我们还将使用混合精度训练,这是一种更节省内存的训练策略:
scaler = torch.cuda.amp.GradScaler() # set mixed precision
主要训练循环
现在让我们构建主要的训练循环。第一部分是读取和准备数据:
for itr in range(100000):
with torch.cuda.amp.autocast(): # cast to mix precision
image,mask,input_point, input_label = read_batch(data) # load data batch
if mask.shape[0]==0: continue # ignore empty batches
predictor.set_image(image) # apply SAM image encoder to the image
首先,我们将数据转换为混合精度,以便高效训练:
with torch.cuda.amp.autocast():
接下来,我们使用之前创建的读取器函数来读取训练数据:
image,mask,input_point, input_label = read_batch(data)
我们加载的图像通过图像编码器(网络的第一部分)进行处理:
predictor.set_image(image)
接着,我们使用网络提示编码器处理输入点:
mask_input, unnorm_coords, labels, unnorm_box = predictor._prep_prompts(input_point, input_label, box=None, mask_logits=None, normalize_coords=True)
sparse_embeddings, dense_embeddings = predictor.model.sam_prompt_encoder(points=(unnorm_coords, labels),boxes=None,masks=None,)
请注意,在这一部分,我们也可以输入框或掩码,但我们不会使用这些选项。
现在我们已经编码了提示(点)和图像,我们可以最终预测分割掩码:
batched_mode = unnorm_coords.shape[0] > 1 # multi mask prediction
high_res_features = [feat_level[-1].unsqueeze(0) for feat_level in predictor._features["high_res_feats"]]
low_res_masks, prd_scores, _, _ = predictor.model.sam_mask_decoder(image_embeddings=predictor._features["image_embed"][-1].unsqueeze(0),image_pe=predictor.model.sam_prompt_encoder.get_dense_pe(),sparse_prompt_embeddings=sparse_embeddings,dense_prompt_embeddings=dense_embeddings,multimask_output=True,repeat_image=batched_mode,high_res_features=high_res_features,)
prd_masks = predictor._transforms.postprocess_masks(low_res_masks, predictor._orig_hw[-1])# Upscale the masks to the original image resolution
这段代码的主要部分是model.sam_mask_decoder,它运行网络的 mask_decoder 部分并生成分割掩码(low_res_masks)及其分数(prd_scores)。
这些掩码的分辨率低于原始输入图像,并且会在postprocess_masks函数中被调整到原始输入大小。
这将给我们网络的最终预测:每个输入点对应的 3 个分割掩码(prd_masks)以及掩码分数(prd_scores)。prd_masks 包含每个输入点的 3 个预测掩码,但我们只会使用每个点的第一个掩码。prd_scores 包含网络认为每个掩码的质量(或预测的置信度)的分数。
损失函数
分割损失
现在我们有了网络预测,可以计算损失。首先,我们计算分割损失,即预测掩码与地面真值掩码的匹配程度。为此,我们使用标准的交叉熵损失。
首先,我们需要使用 sigmoid 函数将预测掩码(prd_mask)从 logits 转换为概率:
prd_mask = torch.sigmoid(prd_masks[:, 0])# Turn logit map to probability map
接下来,我们将地面真值掩码转换为 PyTorch 张量:
prd_mask = torch.sigmoid(prd_masks[:, 0])# Turn logit map to probability map
最后,我们使用地面真值(gt_mask)和预测概率图(prd_mask)手动计算交叉熵损失(seg_loss):
seg_loss = (-gt_mask * torch.log(prd_mask + 0.00001) - (1 - gt_mask) * torch.log((1 - prd_mask) + 0.00001)).mean() # cross entropy loss
(我们加上 0.0001 来防止对零值使用对数函数时出现爆炸)。
分数损失(可选)
除了掩码,网络还预测每个预测掩码的分数,表示掩码的质量。训练这一部分的作用较小,但也有其用处。为了训练这一部分,我们首先需要知道每个预测掩码的真实分数。也就是说,我们需要知道预测掩码的质量。我们将通过使用交并比(IOU)度量,将 GT 掩码与相应的预测掩码进行比较,来实现这一点。IOU 只是两个掩码的重叠部分,除以两个掩码的联合面积。首先,我们计算预测掩码和 GT 掩码之间的交集(它们重叠的区域):
inter = (gt_mask * (prd_mask > 0.5)).sum(1).sum(1)
我们使用阈值 (prd_mask > 0.5) 将预测掩码从概率转化为二进制掩码。
接下来,我们通过将交集除以预测掩码和 GT 掩码的联合面积(并集)来获得 IOU:
iou = inter / (gt_mask.sum(1).sum(1) + (prd_mask > 0.5).sum(1).sum(1) - inter)
我们将使用 IOU 作为每个掩码的真实分数,并通过计算预测分数与我们刚刚计算的 IOU 之间的绝对差来获得分数损失。
score_loss = torch.abs(prd_scores[:, 0] - iou).mean()
最后,我们将分割损失和分数损失合并(对第一个赋予更高的权重):
loss = seg_loss+score_loss*0.05 # mix losses
最后一步:反向传播和保存模型
一旦我们得到损失,后续步骤就完全标准了。我们使用之前创建的优化器计算反向传播并更新权重:
predictor.model.zero_grad() # empty gradient
scaler.scale(loss).backward() # Backpropogate
scaler.step(optimizer)
scaler.update() # Mix precision
我们还希望每 1000 步保存一次训练的模型:
if itr%1000==0: torch.save(predictor.model.state_dict(), "model.torch") # save model
由于我们已经计算了 IOU,我们可以将其作为移动平均显示,以查看模型预测随时间的改进情况:
if itr==0: mean_iou=0
mean_iou = mean_iou * 0.99 + 0.01 * np.mean(iou.cpu().detach().numpy())
print("step)",itr, "Accuracy(IOU)=",mean_iou)
就这样,我们在不到 60 行代码(不包括注释和导入)内完成了 Segment-Anything 2 的训练/微调。在约 25,000 步之后,您应该会看到显著的改进。
模型将被保存为“model.torch”。
您可以在以下位置找到完整的训练代码:
[## fine-tune-train_segment_anything_2_in_60_lines_of_code/TRAIN.py at main ·…
该仓库提供了用于训练/微调 Meta Segment Anything Model 2 (SAM 2)的代码……
本教程使用每批次一张图像,更高效的方法是每批次使用多张不同的图像,相关代码可以在以下位置找到:
推理:加载并使用训练好的模型:
现在模型已经完成微调,我们来使用它对一张图像进行分割。
我们将通过以下步骤来实现:
-
加载我们刚刚训练好的模型。
-
给模型提供一张图像和一堆随机点。对于每个点,网络将预测包含该点的分割掩膜以及一个分数。
-
将这些掩膜拼接成一个完整的分割图。
完整的代码可以在以下位置找到:
[## fine-tune-train_segment_anything_2_in_60_lines_of_code/TEST_Net.py at main ·…
该仓库提供了用于训练/微调 Meta Segment Anything Model 2 (SAM 2)的代码……
首先,我们加载依赖项并将权重转换为 float16 格式,这使得模型运行速度更快(仅在推理时可行)。
import numpy as np
import torch
import cv2
from sam2.build_sam import build_sam2
from sam2.sam2_image_predictor import SAM2ImagePredictor
# use bfloat16 for the entire script (memory efficient)
torch.autocast(device_type="cuda", dtype=torch.bfloat16).__enter__()
接下来,我们加载一个示例图像和我们想要分割的图像区域的掩膜(下载图像/掩膜):
image_path = r"sample_image.jpg" # path to image
mask_path = r"sample_mask.png" # path to mask, the mask will define the image region to segment
def read_image(image_path, mask_path): # read and resize image and mask
img = cv2.imread(image_path)[...,::-1] # read image as rgb
mask = cv2.imread(mask_path,0) # mask of the region we want to segment
# Resize image to maximum size of 1024
r = np.min([1024 / img.shape[1], 1024 / img.shape[0]])
img = cv2.resize(img, (int(img.shape[1] * r), int(img.shape[0] * r)))
mask = cv2.resize(mask, (int(mask.shape[1] * r), int(mask.shape[0] * r)),interpolation=cv2.INTER_NEAREST)
return img, mask
image,mask = read_image(image_path, mask_path)
在我们想要分割的区域内随机采样 30 个点:
num_samples = 30 # number of points/segment to sample
def get_points(mask,num_points): # Sample points inside the input mask
points=[]
for i in range(num_points):
coords = np.argwhere(mask > 0)
yx = np.array(coords[np.random.randint(len(coords))])
points.append([[yx[1], yx[0]]])
return np.array(points)
input_points = get_points(mask,num_samples)
加载标准的 SAM 模型(与训练时相同)
# Load model you need to have pretrained model already made
sam2_checkpoint = "sam2_hiera_small.pt"
model_cfg = "sam2_hiera_s.yaml"
sam2_model = build_sam2(model_cfg, sam2_checkpoint, device="cuda")
predictor = SAM2ImagePredictor(sam2_model)
接下来,加载我们刚刚训练的模型的权重(model.torch):
predictor.model.load_state_dict(torch.load("model.torch"))
运行微调后的模型来预测我们之前选择的每个点的分割掩膜:
with torch.no_grad(): # prevent the net from caclulate gradient (more efficient inference)
predictor.set_image(image) # image encoder
masks, scores, logits = predictor.predict( # prompt encoder + mask decoder
point_coords=input_points,
point_labels=np.ones([input_points.shape[0],1])
)
现在我们得到了一个预测的掩膜及其分数的列表。我们希望将它们以某种方式拼接成一个一致的分割图。然而,许多掩膜是重叠的,可能彼此不一致。
拼接方法很简单:
首先,我们将根据预测分数对预测的掩模进行排序:
masks=masks[:,0].astype(bool)
shorted_masks = masks[np.argsort(scores[:,0])][::-1].astype(bool)
现在让我们创建一个空的分割图和占用图:
seg_map = np.zeros_like(shorted_masks[0],dtype=np.uint8)
occupancy_mask = np.zeros_like(shorted_masks[0],dtype=bool)
接下来,我们将按顺序(从高到低分数)将掩模添加到分割图中。只有当掩模与之前添加的掩模一致时,我们才会添加它,这意味着只有当我们想要添加的掩模与已占用区域的重叠部分小于 15%时,才会添加。
for i in range(shorted_masks.shape[0]):
mask = shorted_masks[i]
if (mask*occupancy_mask).sum()/mask.sum()>0.15: continue
mask[occupancy_mask]=0
seg_map[mask]=i+1
occupancy_mask[mask]=1
就这样。
seg_mask现在包含了预测的分割图,每个分割区域有不同的值,背景为 0。
我们可以使用以下方法将其转换为颜色图:
rgb_image = np.zeros((seg_map.shape[0], seg_map.shape[1], 3), dtype=np.uint8)
for id_class in range(1,seg_map.max()+1):
rgb_image[seg_map == id_class] = [np.random.randint(255), np.random.randint(255), np.random.randint(255)]
并显示:
cv2.imshow("annotation",rgb_image)
cv2.imshow("mix",(rgb_image/2+image/2).astype(np.uint8))
cv2.imshow("image",image)
cv2.waitKey()

使用微调后的 SAM2 进行分割结果的示例。图像来自 LabPics 数据集。
完整的推理代码可以在以下位置找到:
[## fine-tune-train_segment_anything_2_in_60_lines_of_code/TEST_Net.py at main ·…
该代码库提供了用于训练/微调 Meta Segment Anything Model 2(SAM 2)的代码……
结论:
就是这样,我们已经在自定义数据集上训练和测试了 SAM2。除了更改数据读取器外,这应该适用于任何数据集。在许多情况下,这应该足以显著提高性能。
最后,SAM2 还可以在视频中对物体进行分割和跟踪,但微调这一部分内容将在另一个时刻讨论。
版权声明: 本文中的所有图像均来自SAM2 GIT代码库(采用 Apache 许可证),以及LabPics数据集(采用 MIT 许可证)。本教程的代码和网络模型可在 Apache 许可证下使用。
快速训练朴素贝叶斯模型

图片由Marc Sendra Martorell提供,来源于Unsplash
在 Julia 中的性能调优
·发表于Towards Data Science ·12 分钟阅读·2024 年 5 月 31 日
--
在最近的一堂课上,我向学生展示了如何使用多项式朴素贝叶斯(MNB)模型进行文档分类。作为示例,我使用了Enron 电子邮件数据集,并基于该模型创建了一个垃圾邮件过滤器。使用的数据集版本包含 33,716 封电子邮件,分类为“垃圾邮件”或“正常邮件”(即非垃圾邮件)。
我们选择了来自 Julia 的MultinomialNBClassifier,并使用同一包中的CountTransformer进行数据准备。我感到相当惊讶的是,使用整个数据集训练这个分类器(在一台 Apple M3,16 GB RAM 的机器上)竟然花费了超过 30 分钟。
通常,仅使用数据集的一部分进行训练,因为其余部分需要用于测试。即便仅使用数据集的 70%进行训练,仍然花费了超过 10 分钟。33,716 封电子邮件无疑超过了简单的教科书示例,但另一方面,朴素贝叶斯模型以低训练成本而著称。
因此,我开始调查为什么训练需要这么长时间,以及是否有方法可以加快速度。在接下来的部分,我将介绍我所应用的性能调优措施及所能实现的加速效果。这些措施并非特别针对这个问题,因此也应该适用于其他情况。
注意:所有实现和基准测试均使用 Julia 1.10.3 版本在 16 GB RAM 的 M3 MacBook Pro 上完成。所使用的 Julia 包包括 MLJ 0.20.0、TextAnalysis 0.7.5、Metal 1.1.0、CategoricalArrays 0.10.8 和 BenchmarkTools 1.5.0。
训练多项式朴素贝叶斯模型
但首先让我介绍一下训练 MNB 所必需的主要步骤,以便理解需要优化的算法。具体包括:
-
一个数据准备步骤,它将文档(在我们的案例中是电子邮件)转换为合适的数据结构(即所谓的文档词项矩阵;DTM)并
-
实际训练步骤,其中将文档词项矩阵(DTM)聚合为每个类别(垃圾邮件或非垃圾邮件)的向量
数据准备
用于 MNB 的文档被表示为“词袋”。也就是说,文档中单词的顺序被认为是无关的,只存储每个单词出现的次数。因此,“the cow eats grass”这个句子在这种表示下等同于“eats the cow grass”或“grass eats the cow”。
为了使用内存高效的表示形式将所有文档转换为这种形式,我们创建了一个字典,其中包含文档中出现的所有单词(它本质上是一个包含所有单词的数组)。假设我们有以下文档 D1、D2 和 D3:
-
D1: “灰色的猫躺在草地上”
-
D2: “牛吃草”
-
D3: “猫是灰色的”
然后字典如下:[“the”, “grey”, “cat”, “lies”, “on”, “grass”, “cow”, “eats”, “is”],因为这三篇文档中有九个不同的单词。
每个文档随后表示为一个与字典长度相同的数组,数组中的每个元素是字典中对应单词出现的次数。因此,D1、D2 和 D3 将具有以下形式:
-
D1: [2, 1, 1, 1, 1, 1, 0, 0, 0] 例如,字典中的第一个词(“the”)出现了两次,第二个词(“grey”)出现了一次,依此类推
-
D2: [1, 0, 0, 0, 0, 1, 1, 1, 0]
-
D3: [1, 1, 1, 0, 0, 0, 0, 0, 1]
如果我们将这些数组合并为一个矩阵——每个文档一行,那么我们就得到了上面提到的文档词项矩阵(DTM)。在我们的案例中,它是一个 3 x 9 的矩阵,因为我们有三篇文档和一个包含九个不同单词的词典。
训练
MNB 的训练基本上是通过将所有文档向量按类别分开进行加总。也就是说,在我们的垃圾邮件示例中,我们必须将所有“垃圾邮件”类别的文档向量加起来,所有“非垃圾邮件”类别的文档向量加起来,最终得到两个向量,每个向量包含该类别的单词频率汇总。
如果我们假设文档 D1 和 D3 是“非垃圾邮件”,D2 是“垃圾邮件”,我们将得到以下结果:
-
“非垃圾邮件”单词频率: [3, 2, 2, 1, 1, 1, 0, 0, 1]
-
“垃圾邮件”单词频率: [1, 0, 0, 0, 0, 1, 1, 1, 0]
在 MNB 的完整训练步骤中,除了对这些数字进行一些后处理外,真正“耗时”的部分是这里展示的 DTM 的聚合过程,这是我们需要优化的部分。
从 Enron 数据集开始
数据准备
我使用 CountTransformer 创建了 Enron 数据集的 DTM,该方法是 MLJ 中的一部分,具体函数如下:
function transform_docs(doc_list)
CountTransformer = @load CountTransformer pkg=MLJText
trans_machine = machine(CountTransformer(), doc_list)
fit!(trans_machine)
return(MLJ.transform(trans_machine, doc_list))
end
此函数的输入 doc_list 是一个分词后的电子邮件数组。也就是说,每封邮件中的每个词被分离成一个单独的字符串(使用 TextAnalysis.tokenize())。
结果是一个 33,716 x 159,093 的矩阵,因为有 33,716 封邮件,字典中包含 159,093 个不同的单词。这是一个包含超过 53 亿个元素的矩阵。令人惊讶的是,创建 DTM 的时间不到一分钟。因此,性能调优的重点将完全放在训练步骤上。
由于 DTM 中大多数元素为 0,因此使用了所谓的 稀疏矩阵 来以内存高效的方式存储它们(在 Julia 中,这种类型是 [SparseMatrixCSC](https://docs.julialang.org/en/v1/stdlib/SparseArrays/))。
更准确地说,CountTransformer 生成的 数据结构是 LinearAlgebra.Adjoint{Int64,SparseMatrixCSC{Int64, Int64}} 类型。稍后我们将讨论这个特殊结构。
训练
然后,训练 MultinomialNBClassifier 的方法如下,X 包含 DTM,y 是垃圾邮件/非垃圾邮件标签的数组(作为一个 [CategoricalArray](https://github.com/JuliaData/CategoricalArrays.jl?tab=readme-ov-file),因为所有 MLJ 模型都期望这种类型):
MultinomialNBClassifier = @load MultinomialNBClassifier pkg=NaiveBayes
nb_classifier = MultinomialNBClassifier()
nb_machine = machine(nb_classifier, X, y)
fit!(nb_machine, verbosity=0)
对 fit! 的调用进行实际训练,处理所有 Enron 邮件时花费了超过 30 分钟,对于一个 70% 子集则花费了超过 10 分钟。
为了专注于训练步骤的分析和优化,我从自己实现的一个函数开始,该函数执行上述所有文档向量聚合,将其转换为包含“垃圾邮件”和“非垃圾邮件”总结词频的两个向量。MultinomialNBCClassifier 的相关代码有太多依赖,这使得演示以下优化步骤变得不可行。
该函数(名为 count_words)的第一个基线方法如下所示:
function count_words_base(X::AbstractMatrix{Int64},y)
ndocs = size(X,1) # number of documents
nwords = size(X,2) # number of words in dictionary
ncats = length(levels(y)) # number of categories in `y`
wcounts = ones(Int64, ncats, nwords) # matrix for storing the word counts by category
for col in 1:nwords
for doc in 1:ndocs
if y[doc] == “ham”
wcounts[1,col] += X[doc, col]
else
wcounts[2,col] += X[doc, col]
end
end
end
return(wcounts)
end
应用于 X 和 y 时,完成需要 241.076 秒。
为了减少测试运行的时间,并避免内存成为影响运行时间的决定性因素,我将所有进一步的测试(除非另有说明)都限制在 DTM 的一部分(称为 Xpart),只包含前 10,000 列(即一个 33,716 x 10,000 的矩阵)。
对于这个减少后的 DTM,count_words_base 完成需要 20.363 秒。
OPT1:以正确的方式使用正确的数据结构
性能调优的一个重要方面是所使用的数据结构,以及它们是否以最有效的方式使用。
列优先存储
从这个意义上来说,count_words_base 已经使用了一种优化。在 Julia 中,矩阵是按列优先顺序存储的。也就是说,每列的元素在内存中是紧密存储的。因此,遍历 一列 的所有元素比遍历 一行 中的元素更快。因此,count_words_base 中的内层循环是遍历 X 中的一列。
在 Julia 中,列优先顺序存储是常见的做法。这对于SparseMatrixCSC或DataFrame等也适用。但检查数据结构使用的是哪种存储顺序始终是一个好主意。
CategoricalArrays
count_words_base中的 if 语句会对 DTM 的每个元素执行。所以优化这部分函数肯定会有帮助。
参数y并不是一个“普通的”数组,它不会将单词"ham"或“spam”存储 33,716 次。它是一个[CategoricalArray](https://github.com/JuliaData/CategoricalArrays.jl?tab=readme-ov-file),它只存储这两个单词一次,并且内部使用一个整数数组来存储 33,716 个不同的"ham"和“spam”值(这两个值由数字 1 和 2 表示)。我们可以使用levelcode函数访问这种数值表示。所以y[1]返回的是"ham",而levelcode(y[1])则返回 1。
因此,我们可以通过以下单行代码替换整个 if 语句(从而得到第一个优化版本count_words_01):
wcounts[levelcode(y[doc]),col] += X[doc, col]
这为我们提供了18.006 s的运行时间,约提升了 10%。
更高效的矩阵
通常内存高效的数据结构在访问其元素时效率较低。因此,我怀疑(稠密)矩阵(即二维Array)可能比用于 DTM 的稀疏矩阵更具性能。
作为参考点,我创建了一个与Xpart相同大小的稠密矩阵Xref(填充随机数字):Xref = rand(0:9, 33716, 10000)。
该矩阵具有以下运行时间:
-
count_words_base: 2.378 s -
count_words_01: 0.942 s
所以CountTransformer生成的 DTM 肯定存在实际问题。即使是基线实现也能让我们获得超过 8 倍的加速,而count_words_01中使用的优化在这种情况下更为有效,将运行时间减少到基线数值的一半以下!
如前所述,CountTransformer并没有生成实际的SparseMatrixCSC,而是生成了一个LinearAlgebra.Adjoint{Int64,SparseMatrixCSC{Int64, Int64}}。也就是说,稀疏矩阵被包装在其他结构中。这可能会成为一个问题。因此,我尝试提取实际的稀疏矩阵……但证明这是困难且昂贵的:提取过程需要将近 17 秒!
但是最终得到的“纯”稀疏矩阵要高效得多:
-
count_words_base: 3.22 s -
count_words_01: 1.435 s
由于我们必须为提取这些数字额外增加将近 17 秒,这并没有从整体上提升处理速度。因此,我在寻找替代方案时,找到了[TextAnalysis](https://github.com/JuliaText/TextAnalysis.jl?tab=readme-ov-file)包,其中也有一个创建 DTM 的函数。创建过程与CountTransformer一样高效,但它直接生成“纯”稀疏矩阵。
因此,我们可以获得稀疏矩阵的运行时数字,而无需额外增加 17 秒。这在这一点上为我们带来了加速,即 20.363/1.435 = 14.2。
OPT2: 多线程
使用 Julia 相对容易实现多线程,尤其是在我们的案例中,我们迭代一个数据结构并在每次迭代中访问该数据结构的不同部分。因此,每次迭代都可以在另一个线程中执行,而不必担心数据访问冲突。
在这种设置下,我们只需要在for语句前面加上宏@threads,然后 Julia 会为我们做其余的工作。也就是说,它会将不同的迭代分配到特定机器上可用的线程中。由于 M3 芯片有八个内核,我将JULIA_NUM_THREADS环境变量设置为 8,并将count_words函数中的 for 循环部分更改如下(得到下一个优化版本count_words_02):
@threads for col in 1:nwords
for doc in 1:ndocs
wcounts[levelcode(y[doc]),col] += X[doc, col]
end
end
这给我们带来了231 ms的运行时间,加速为 20.363/0.231 = 88.2。
OPT3:GPU 与矩阵运算
获得更高的性能通常通过使用 GPU 来实现。但这只有在算法能够适应 GPU 这种非常特殊的计算结构时才能做到。理想情况下,您的算法应该由向量和矩阵运算组成。那么我们来探讨一下,是否可以将我们的count_words函数以这种方式进行适配。
过滤行
我们上面的例子仅包含三份文档 D1、D2 和 D3,可能是一个不错的起点,帮助我们更好地理解。对于这个简单的例子,X和y如下所示:
X = [2 1 1 1 1 1 0 0 0; y = ["ham", "spam", "ham"]
1 0 0 0 0 1 1 1 0;
1 1 1 0 0 0 0 0 1]
函数count_words会对列中的数字求和,但只针对特定的行。在这个例子中,首先加总第 1 行和第 3 行的数据,然后查看第 2 行。也就是说,我们需要某种过滤器来筛选行,然后我们可以直接对列进行求和。
在 Julia 中,可以使用BitArray对数组进行索引。也就是说,X[[1,0,1],:]会返回X的第 1 行和第 3 行,X[[0,1,0],:]则返回第 2 行。如果我们用 1 和 0 替换y中的“ham”和“spam”,并将其转换为以下矩阵,我们就可以得到这些“过滤器”:
yb = [1 0;
0 1;
1 0]
因此,yb[:,1]将是第一个过滤器,yb[:,2]将是第二个过滤器。
对于垃圾邮件模型,我们可以使用以下函数将CategoricalArray类型的y转换为这样的二进制矩阵(y.refs是仅使用整数的内部表示):
function y_as_bitmatrix(y)
spam = y.refs .== 2
ham = y.refs .== 1
return([ham spam]) # Bit-Matrix (one column per category)
end
使用这种表示法的y,我们可以这样实现count_words:
function count_words_03(X::AbstractMatrix{Int64},y::BitMatrix)
nwords = size(X,2). # number of words in dictionary
ncats = size(y,2) # number of categories in `y`
wcounts = ones(Int64, ncats, nwords) # matrix for storing the word counts by category
for cat in 1:ncats
@threads for col in 1:nwords
wcounts[cat,col] = sum(X[y[:,cat],col])
end
end
return(wcounts)
end
该变体的运行时间为652 ms(在 CPU 上)。所以并没有比上面的版本更快,但我们仍在进行探索。
点积
让我们再次回到简单的三文档示例:
X = [2 1 1 1 1 1 0 0 0; yb = [1 0;
1 0 0 0 0 1 1 1 0; 0 1;
1 1 1 0 0 0 0 0 1] 1 0]
我们也可以通过计算X中每一列与yb的第一列的点积,然后再与yb的第二列做同样的操作来实现我们的目标。这将得到count_words_04:
function count_words_04(X::AbstractMatrix{Int64},y::BitMatrix)
nwords = size(X,2) # number of words in dictionary
ncats = size(y,2) # number of categories in `y`
wcounts = ones(Int64, ncats, nwords) # matrix for storing the word counts by category
for cat in 1:ncats
@threads for col in 1:nwords
wcounts[cat,col] = dot(X[:,col], y[:,cat])
end
end
return(wcounts)
end
这导致运行时间为4.96 ms(在 CPU 上),现在的加速为 20.363/0.00496 = 4,105.4!
这种剧烈的改进可能需要一些解释。这里有两个因素是相辅相成的:
-
像点积这样的向量运算在 Julia 中被超级优化,依赖于像BLAS这样的经过验证的库。
-
稀疏矩阵类型在此上下文中非常高效。在这种情况下,我们的密集参考矩阵
Xref的运行时间仅为 455.7 毫秒。
矩阵乘法
将上述的思路稍作扩展,我们可以以其转置形式表示yb,如下所示:
ybt = [1 0 1; X = [2 1 1 1 1 1 0 0 0;
0 1 0] 1 0 0 0 0 1 1 1 0;
1 1 1 0 0 0 0 0 1]
这种描述使得count_words的最简洁、也许是最优雅的版本几乎显而易见。它只是一个矩阵乘法:
function count_words_05(X::AbstractMatrix{Int64},y::BitMatrix)
transpose(Y) * X
end
它也是最快的版本,1.105 毫秒,带来 20.363/0.00105 = 19,393倍的加速!
多线程在这里是隐式的,因为底层的 BLAS 库默认是多线程的。可以通过BLAS.get_num_threads()获取使用的线程数。
此外,这个解决方案具有良好的扩展性。应用于完整数据集,包含 33,716 x 159,093 个元素的矩阵X,需要 13.57 毫秒才能完成。这是 241.076/0.01357 = 17,765倍的加速。
OPT4: GPU
最后,将最后一个变体应用到 GPU 上可以通过Metal.jl包完成。为此,所使用的矩阵只需通过mtl函数转换为相应的金属数组类型:
const mtl_Xpart = mtl(Xpart)
const mtl_yb = mtl(yb)
用于 GPU 的count_words变体,除了数据类型外,和上面的一样:
function count_words_06(X::MtlMatrix,y::MtlMatrix)
transpose(y) * X
end
它的运行时间仅为0.306 毫秒。但是将数据复制到 GPU(使用mtl)所需的时间要远远超过在 GPU 上运行算法所节省的时间。因此,它并不是真的更快。
除此之外,适用于 Apple Silicon GPU 的Metal包还不如例如CUDA.jl那样成熟。这一点在尝试将大型矩阵X转换为金属数组时变得明显:转换会停止并显示错误信息。
结论
当然,并非每个算法都可以像count_words_05那样转换为如此简洁的变体。但即便是更“经典”的实现count_words_04也比我们最初的版本快了超过 4,000 倍。本文中展示的许多性能优化措施也可以应用于其他函数。此外,我还建议任何想要从 Julia 程序中获得更多性能的人,遵循 Julia 文档中的“性能优化提示”。
在 CPU 上训练 AI 模型
在 GPU 稀缺时代重新审视 CPU 在机器学习中的作用
·发表于 Towards Data Science ·13 分钟阅读·2024 年 9 月 1 日
--

最近,人工智能的成功往往归功于 GPU 的出现和发展。GPU 的架构通常包括数千个多处理器、高速内存、专用张量核心等,非常适合满足人工智能和机器学习工作负载的高强度需求。不幸的是,人工智能发展的迅速增长导致了 GPU 需求的激增,使其变得难以获得。因此,机器学习开发者正越来越多地探索替代硬件选项来训练和运行他们的模型。在之前的文章中,我们讨论了使用专用人工智能 ASIC 的可能性,例如 Google Cloud TPU、Haban Gaudi 和 AWS Trainium。虽然这些选项提供了显著的成本节约机会,但它们并不适合所有的机器学习模型,且与 GPU 一样,也可能面临有限的可用性问题。在本文中,我们回归到传统的 CPU,并重新审视它在机器学习应用中的相关性。尽管与 GPU 相比,CPU 通常不太适合处理机器学习工作负载,但它们更容易获取。能够在 CPU 上运行(至少部分)工作负载可能对开发生产力产生重大影响。
在之前的文章中(例如,这里),我们强调了分析和优化 AI/ML 工作负载的运行时性能的重要性,这对于加速开发和降低成本至关重要。尽管无论使用何种计算引擎,这都是至关重要的,但不同平台上的性能分析工具和优化技术差异可能很大。在本篇文章中,我们将讨论一些与 CPU 相关的性能优化选项。我们的重点将放在 Intel® Xeon® CPU 处理器(搭载 Intel® AVX-512)以及 PyTorch(版本 2.4)框架上(尽管类似的技术也可以应用于其他 CPU 和框架)。更具体来说,我们将在一个 Amazon EC2 c7i 实例上进行实验,该实例搭载 AWS Deep Learning AMI。请不要将我们选择的云平台、CPU 版本、ML 框架或我们提到的任何其他工具或库视为对其替代品的推荐。
我们的目标是展示,尽管在 CPU 上进行机器学习开发可能不是我们的首选,但仍然有一些方法可以“缓解影响”,并且——在某些情况下——甚至可能使其成为一个可行的替代方案。
免责声明
本文的目的是展示在 CPU 上可以利用的少数机器学习优化机会。与大多数在线关于 CPU 上机器学习优化的教程相反,我们将重点关注训练工作负载,而不是推理工作负载。有许多专门针对推理的优化工具我们将不做介绍(例如,参见 这里 和 这里)。
请不要将本文视为我们提到的任何工具或技术的官方文档替代品。请记住,鉴于 AI/ML 开发的快速发展,我们提到的一些内容、库和/或指令可能会在您阅读本文时已经过时。请务必参考最新的官方文档。
重要的是,我们将在讨论的优化对运行时性能的影响可能会根据模型和环境的细节(例如,参见官方 PyTorch TouchInductor CPU 推理性能仪表盘 中模型之间的高度差异)而有很大的不同。我们将分享的比较性能数据是针对我们将使用的玩具模型和运行时环境的。请务必在您自己的模型和运行时环境中重新评估所有提议的优化。
最后,我们的重点将仅仅放在吞吐量性能上(以每秒样本数衡量)——而不是训练收敛性。然而,需要注意的是,一些优化技术(例如批量大小调整、混合精度等)可能会对某些模型的收敛性产生负面影响。在某些情况下,可以通过适当的超参数调整来克服这一问题。
示例 — ResNet-50
我们将在一个简单的图像分类模型上进行实验,该模型具有ResNet-50骨干(来自深度残差学习用于图像识别)。我们将在一个虚拟数据集上训练该模型。完整的训练脚本如下所示(大致基于这个示例):
import torch
import torchvision
from torch.utils.data import Dataset, DataLoader
import time
# A dataset with random images and labels
class FakeDataset(Dataset):
def __len__(self):
return 1000000
def __getitem__(self, index):
rand_image = torch.randn([3, 224, 224], dtype=torch.float32)
label = torch.tensor(data=index % 10, dtype=torch.uint8)
return rand_image, label
train_set = FakeDataset()
batch_size=128
num_workers=0
train_loader = DataLoader(
dataset=train_set,
batch_size=batch_size,
num_workers=num_workers
)
model = torchvision.models.resnet50()
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters())
model.train()
t0 = time.perf_counter()
summ = 0
count = 0
for idx, (data, target) in enumerate(train_loader):
optimizer.zero_grad()
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
batch_time = time.perf_counter() - t0
if idx > 10: # skip first steps
summ += batch_time
count += 1
t0 = time.perf_counter()
if idx > 100:
break
print(f'average step time: {summ/count}')
print(f'throughput: {count*batch_size/summ}')
在一台 c7i.2xlarge(8 个 vCPU)实例上运行此脚本,并使用CPU版本的 PyTorch 2.4,得到的吞吐量为每秒 9.12 个样本。为了比较,我们注意到相同(未优化的脚本)在Amazon EC2 g5.2xlarge实例(1 个 GPU 和 8 个 vCPU)上的吞吐量为每秒 340 个样本。考虑到这两种实例类型的比较成本(c7i.2xlarge 每小时$0.357,g5.2xlarge 每小时$1.212,截止至本文写作时),我们发现使用 GPU 实例进行训练的价格性能大约好十一倍(!!)。基于这些结果,使用 GPU 训练机器学习模型的偏好是非常有根据的。让我们评估一些减少这一差距的可能性。
PyTorch 性能优化
本节将探讨一些增加我们训练工作负载运行时性能的基本方法。虽然你可能会在我们的文章中认出其中的一些内容,值得强调的是,CPU 和 GPU 平台上的训练优化有一个显著的区别。在 GPU 平台上,我们的大部分工作致力于最大化(CPU 上的训练数据预处理与)GPU 上的模型训练之间的并行化。而在 CPU 平台上,所有处理都发生在 CPU 上,我们的目标是最有效地分配其资源。
批量大小
增加训练批量大小可能通过减少模型参数更新的频率来提升性能。(在 GPU 上,这还可以减少 CPU 与 GPU 之间事务的开销,比如内核加载)。然而,在 GPU 上,我们的目标是找到最大化 GPU 内存利用率的批量大小,但在 CPU 上,这种策略可能会影响性能。由于 CPU 内存更为复杂,发现最优批量大小的最佳方法可能是通过反复试验。请记住,改变批量大小可能会影响训练收敛性。
下表总结了我们训练工作负载在几种(任意)批量大小选择下的吞吐量:

训练吞吐量与批量大小的关系(作者提供)
与我们在 GPU 上的发现相反,在 c7i.2xlarge 实例类型上,我们的模型似乎更喜欢较小的批量大小。
多进程数据加载
在 GPU 上常用的一种技术是将多个进程分配给数据加载器,以减少 GPU 饥饿的可能性。在 GPU 平台上,通常的经验法则是根据 CPU 核心数量设置工作进程的数量。然而,在 CPU 平台上,由于模型训练和数据加载器使用相同的资源,这种方法可能适得其反。再次强调,选择最佳工作进程数的方法可能是通过反复试验。下表展示了不同num_workers选择下的平均吞吐量:

训练吞吐量与数据加载工作进程数的关系(作者提供)
混合精度
另一种流行的技术是使用低精度浮点数据类型,如torch.float16或torch.bfloat16,其中torch.bfloat16的动态范围通常被认为对 ML 训练更为友好。当然,减少数据类型的精度可能对收敛性产生不利影响,因此应谨慎操作。PyTorch 提供了torch.amp自动混合精度包,旨在优化这些数据类型的使用。英特尔® AVX-512 支持bfloat16数据类型。修改后的训练步骤如下:
for idx, (data, target) in enumerate(train_loader):
optimizer.zero_grad()
with torch.amp.autocast('cpu',dtype=torch.bfloat16):
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
经过此优化后的吞吐量为每秒 24.34 个样本,增加了 86%!!
Channels Last 内存格式
Channels last 内存格式是一种处于测试阶段的优化(在本文写作时),主要涉及视觉模型,支持将四维(NCHW)张量存储在内存中,使得通道成为最后一个维度。这导致每个像素的所有数据被存储在一起。此优化主要针对视觉模型。被认为对英特尔平台更“友好”,这种内存格式报告能够提升在英特尔® Xeon® CPU上的 ResNet-50 的性能。调整后的训练步骤如下:
for idx, (data, target) in enumerate(train_loader):
data = data.to(memory_format=torch.channels_last)
optimizer.zero_grad()
with torch.amp.autocast('cpu',dtype=torch.bfloat16):
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
结果吞吐量为每秒 37.93 个样本——相比我们的基准实验,提升了 56%,总共提升了 415%!我们正在取得巨大的进展!!
Torch 编译
在之前的文章中,我们介绍了 PyTorch 支持图编译的优点及其对运行时性能的潜在影响。与默认的急切执行模式(即每个操作独立运行,也就是“急切”模式)相反,编译API 将模型转换为中间计算图,然后通过 JIT 编译成低级机器代码,以最优方式适应底层训练引擎。该 API 支持通过不同的后端库进行编译,并提供多个配置选项。此处,我们将评估限制在默认(TorchInductor)后端和来自英特尔® PyTorch 扩展的ipex后端,后者是一个专为英特尔硬件优化的库。请参阅文档了解适当的安装和使用说明。更新后的模型定义如下:
import intel_extension_for_pytorch as ipex
model = torchvision.models.resnet50()
backend='inductor' # optionally change to 'ipex'
model = torch.compile(model, backend=backend)
在我们的示例模型中,只有当禁用“channels last”优化时,Torch 编译的影响才会显现(每个后端性能提高约 27%)。当应用“channels last”时,性能反而下降。因此,我们在后续实验中去除了此优化。
内存和线程优化
有许多机会可以优化底层 CPU 资源的使用。这些包括优化内存管理和线程分配,以适应底层 CPU 硬件的结构。内存管理可以通过使用高级内存分配器(如Jemalloc和TCMalloc)和/或减少慢速内存访问(即,跨NUMA 节点)来改进。线程分配可以通过适当的OpenMP 线程库配置和/或使用英特尔的 Open MP 库来改进。
一般来说,这类优化需要深入了解 CPU 架构及其支持的 SW 堆栈的特性。为了简化操作,PyTorch 提供了torch.backends.xeon.run_cpu脚本,用于自动配置内存和线程库,从而优化运行时性能。以下命令将启用专用的内存和线程库。我们将在讨论分布式训练选项时回到 NUMA 节点的话题。
我们验证了TCMalloc(conda install conda-forge::gperftools)和英特尔的 Open MP 库(pip install intel-openmp)的正确安装,并运行了以下命令。
python -m torch.backends.xeon.run_cpu train.py
使用run_cpu脚本进一步提高了我们的运行时性能,达到了每秒 39.05 个样本。请注意,run_cpu脚本包括许多控制项,用于进一步调整性能。务必查阅文档,以便最大化其使用效果。
Intel 扩展包用于 PyTorch
Intel® 扩展包用于 PyTorch包括通过其ipex.optimize函数进行训练优化的更多机会。在这里,我们展示了其默认使用方式。请参阅文档了解其全部功能。
model = torchvision.models.resnet50()
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters())
model.train()
model, optimizer = ipex.optimize(
model,
optimizer=optimizer,
dtype=torch.bfloat16
)
结合上述讨论的内存和线程优化,最终的吞吐量为每秒 40.73 个样本。(请注意,禁用“通道最后”配置时也能得到类似的结果。)
CPU 上的分布式训练
Intel® Xeon®处理器设计采用非统一内存访问(NUMA)架构,其中 CPU 内存被划分为多个组,也就是 NUMA 节点,并且每个 CPU 核心都分配给一个节点。尽管任何 CPU 核心都可以访问任何 NUMA 节点的内存,但访问其自身节点(即本地内存)要快得多。这就产生了跨 NUMA 节点分布式训练的概念,其中分配给每个 NUMA 节点的 CPU 核心作为分布式进程组中的一个进程进行操作,而节点之间的数据分布由Intel® oneCCL,英特尔专用的集体通信库进行管理。
我们可以使用ipexrun工具轻松地在 NUMA 节点之间运行数据分布式训练。在以下代码块中(大致基于这个示例),我们调整脚本以运行数据分布式训练(具体用法详见此处):
import os, time
import torch
from torch.utils.data import Dataset, DataLoader
from torch.utils.data.distributed import DistributedSampler
import torch.distributed as dist
import torchvision
import oneccl_bindings_for_pytorch as torch_ccl
import intel_extension_for_pytorch as ipex
os.environ["MASTER_ADDR"] = "127.0.0.1"
os.environ["MASTER_PORT"] = "29500"
os.environ["RANK"] = os.environ.get("PMI_RANK", "0")
os.environ["WORLD_SIZE"] = os.environ.get("PMI_SIZE", "1")
dist.init_process_group(backend="ccl", init_method="env://")
rank = os.environ["RANK"]
world_size = os.environ["WORLD_SIZE"]
batch_size = 128
num_workers = 0
# define dataset and dataloader
class FakeDataset(Dataset):
def __len__(self):
return 1000000
def __getitem__(self, index):
rand_image = torch.randn([3, 224, 224], dtype=torch.float32)
label = torch.tensor(data=index % 10, dtype=torch.uint8)
return rand_image, label
train_dataset = FakeDataset()
dist_sampler = DistributedSampler(train_dataset)
train_loader = DataLoader(
dataset=train_dataset,
batch_size=batch_size,
num_workers=num_workers,
sampler=dist_sampler
)
# define model artifacts
model = torchvision.models.resnet50()
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters())
model.train()
model, optimizer = ipex.optimize(
model,
optimizer=optimizer,
dtype=torch.bfloat16
)
# configure DDP
model = torch.nn.parallel.DistributedDataParallel(model)
# run training loop
# destroy the process group
dist.destroy_process_group()
不幸的是,截至本文撰写时,Amazon EC2 c7i实例家族并不包含多 NUMA 实例类型。为了测试我们的分布式训练脚本,我们回退到一个拥有 64 个 vCPU 和 2 个 NUMA 节点的Amazon EC2 c6i.32xlarge实例。我们验证了安装了Intel® oneCCL Bindings for PyTorch,并运行以下命令(如此处文档所示):
source $(python -c "import oneccl_bindings_for_pytorch as torch_ccl;print(torch_ccl.cwd)")/env/setvars.sh
# This example command would utilize all the numa sockets of the processor, taking each socket as a rank.
ipexrun cpu --nnodes 1 --omp_runtime intel train.py
下表比较了在c6i.32xlarge实例上启用和未启用分布式训练时的性能结果:

跨 NUMA 节点的分布式训练(作者)
在我们的实验中,数据分布并没有提升运行时性能。有关额外的性能调优选项,请参阅ipexrun 文档。
使用 Torch/XLA 的 CPU 训练
在之前的文章中(例如,这里),我们讨论了PyTorch/XLA库及其利用XLA 编译来支持基于 PyTorch 的训练,在XLA 设备上运行,如 TPU、GPU,以及CPU。类似于 torch 编译,XLA 使用图编译生成针对目标设备优化的机器代码。随着OpenXLA 项目的建立,其中一个目标是支持所有硬件后端的高性能,包括 CPU(请参见 CPU RFC 这里)。下面的代码块演示了我们原始(未优化)脚本的调整,旨在使用PyTorch/XLA进行训练:
import torch
import torchvision
import timeimport torch_xla
import torch_xla.core.xla_model as xm
device = xm.xla_device()
model = torchvision.models.resnet50().to(device)
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters())
model.train()
for idx, (data, target) in enumerate(train_loader):
data = data.to(device)
target = target.to(device)
optimizer.zero_grad()
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
xm.mark_step()
不幸的是(截至本文撰写时),我们在玩具模型上的 XLA 结果似乎远逊于我们之前看到的(未优化的)结果(差距高达 7 倍)。我们预计随着 PyTorch/XLA 的 CPU 支持逐渐成熟,这一情况会有所改善。
结果
我们在下表中总结了部分实验的结果。为了便于对比,我们加入了在Amazon EC2 g5.2xlarge GPU 实例上训练我们模型的吞吐量,并采用了在这篇文章中讨论的优化步骤。每美元样本数是基于Amazon EC2 按需定价页面计算的(截至本文撰写时,c7i.2xlarge 为每小时$0.357,g5.2xlarge 为每小时$1.212)。

性能优化结果(作者提供)
尽管我们成功地将玩具模型在 CPU 实例上的训练性能大幅提升了(446%),但其性能仍然逊色于 GPU 实例上的(优化后)性能。根据我们的结果,GPU 训练将便宜约 6.7 倍。很可能通过进一步的性能调优和/或应用额外的优化策略,我们能够进一步缩小这一差距。再次强调,我们得到的比较性能结果仅适用于该模型和运行时环境。
Amazon EC2 Spot 实例折扣
与 GPU 实例类型相比,基于云的 CPU 实例类型的可用性增加,可能意味着以折扣价获得计算能力的机会更大,例如,通过 Spot 实例的利用。Amazon EC2 Spot 实例是来自云服务冗余容量的实例,提供高达 90%的折扣,远低于按需定价。作为折扣价格的交换,AWS 保留随时中断实例的权利,且通常不提供警告。鉴于 GPU 需求旺盛,你可能会发现 CPU Spot 实例比 GPU 实例更容易获取。在撰写本文时,c7i.2xlarge Spot 实例价格为$0.1291,这将使我们每美元的样本数提高到 1135.76,并进一步缩小优化后的 GPU 和 CPU 价格性能差距(缩小至 2.43 倍)。
尽管我们的小型模型(以及我们选择的环境)在优化后的 CPU 训练中的运行时性能低于 GPU 结果,但如果将相同的优化步骤应用于其他模型架构(例如,包含 GPU 不支持的组件的模型),可能会导致 CPU 性能达到或超越 GPU 性能。即便在性能差距未能弥合的情况下,仍然可能存在 GPU 计算能力短缺的情况,这种情况下将部分机器学习工作负载迁移到 CPU 上可能是合理的。
摘要
鉴于 CPU 的普遍性,有效地利用它们进行训练和/或运行机器学习工作负载,可能会对开发生产力和最终产品部署策略产生重大影响。虽然与 GPU 相比,CPU 架构在许多机器学习应用中的适应性较差,但仍有许多工具和技术可以提高其性能——我们在这篇文章中讨论并展示了一些精选的技术。
在这篇文章中,我们专注于优化 CPU 上的训练。请务必查看我们在Medium 上的其他文章,这些文章涵盖了关于机器学习工作负载性能分析和优化的广泛话题。
训练 CausalLM 模型 第一部分:CausalLM 到底是什么?
使用 HuggingFace 的 CausalLM 类的实用指南第一部分
·发表于 Towards Data Science ·6 分钟阅读·2024 年 3 月 4 日
--

因果语言模型将每个新词建模为所有先前词的函数。来源:Pexels
如果你曾在 HuggingFace 上玩过最近的模型,那么你很可能遇到了因果语言模型。当你打开一个 模型 系列的文档时,你会看到一个页面,其中包含诸如 LlamaForCausalLM 或 LlamaForSequenceClassification 等“任务”。
如果你像我一样,从文档阅读到实际微调模型,可能会有些困惑。我们将专注于 CausalLM,从解释 CausalLM 是什么开始,并在接下来的文章中给出如何微调 CausalLM 模型的实际示例。
背景:编码器与解码器
目前许多最优秀的模型,如 LLAMA-2、GPT-2 或 Falcon,都是“仅解码器”模型。仅解码器模型:
-
接受一系列先前的标记(即提示)
-
将这些标记通过模型(通常是从标记生成嵌入并通过变换器块运行它们)
-
输出单一结果(通常是下一个标记的概率)。
这与“仅编码器”或混合“编码器-解码器”架构的模型形成对比,这些模型将输入整个序列,而不仅仅是前面的标记。这一差异使得两种架构适用于不同的任务。解码器模型设计用于生成新的文本,而编码器模型设计用于需要查看整个序列的任务,如翻译或序列分类。事情变得复杂,因为你可以将一个仅解码器模型用于翻译,或者使用一个仅编码器模型生成新文本。如果你想深入了解编码器与解码器的区别,Sebastian Raschka 写了一篇很好的 指南,另外还有一篇 medium 文章,它更深入地探讨了掩码语言建模和因果语言建模之间的区别。
对于我们的目的,你只需要知道的是:
-
CausalLM 模型通常是仅解码器模型
-
仅解码器模型查看过去的标记来预测下一个标记
对于仅解码器的语言模型,我们可以将下一个标记的预测过程视为“因果语言建模”,因为前面的标记“导致”了每一个额外的标记。
HuggingFace CausalLM
在 HuggingFace 的世界里,CausalLM(LM 代表语言建模)是一类模型,它接受一个提示并预测新的标记。实际上,我们是一次预测一个标记,但这个类抽象掉了需要一次一个标记遍历序列的繁琐过程。在推理过程中,CausalLM 会不断预测单个标记,直到满足某些停止条件,此时模型返回最终连接的标记。
在训练过程中,会发生类似的情况,我们给模型输入一串我们希望学习的标记序列。我们从给定第一个标记预测第二个标记开始,然后给定前两个标记预测第三个标记,依此类推。
因此,如果你想学习如何预测句子“the dog likes food”,假设每个单词是一个标记,你将进行 3 次预测:
-
“the” → dog,
-
“the dog” → likes
-
“the dog likes” → food
在训练过程中,您可以将句子的三个快照视为训练数据集中的三条观察数据。手动将长序列拆分为每个标记的单独行会很繁琐,因此 HuggingFace 会为您处理这件事。
只要你给它一个标记序列,它会在后台将该序列分解成单个标记的预测。
你可以通过将一个常规字符串传递给模型的分词器来创建这个“标记序列”。分词器会输出一个类似字典的对象,包含 input_ids 和 attention_mask 作为键,就像任何普通的 HuggingFace 模型一样。
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bigscience/bloom-560m")
tokenizer("the dog likes food")
>>> {'input_ids': [5984, 35433, 114022, 17304], 'attention_mask': [1, 1, 1, 1]}
对于 CausalLM 模型,还多了一步,模型会期望有一个labels键。在训练过程中,我们使用“前一个”的input_ids来预测“当前”的labels token。然而,你不要把 labels 当作一个问答模型来看待,其中labels的第一个索引对应于input_ids的答案(也就是说,labels应该连接到input_ids的末尾)。相反,你希望labels和input_ids具有相同的形状,互为镜像。在代数符号中,为了预测索引 k 的labels token,我们会使用所有直到 k-1 的input_ids。
如果这让你感到困惑,实际上,你通常可以将labels直接复制为input_ids,就这么做即可。如果你想理解发生了什么,我们将通过一个例子来讲解。
一个快速的实例
让我们回到“the dog likes food”。为了简单起见,我们暂时将单词保留为单词,而不是将它们分配给 token 编号,但实际上这些应该是数字,你可以使用分词器将它们映射回它们的真实字符串表示。
我们的单元素批次输入如下所示:
{
"input_ids": [["the", "dog", "likes", "food"]],
"attention_mask": [[1, 1, 1, 1]],
"labels": [["the", "dog", "likes", "food"]],
}
双括号表示每个键的数组的形状技术上是 batch_size x sequence_size。为了简化,我们可以忽略批处理,直接将它们视为一维向量。
在后台,如果模型正在预测序列中的第 k 个 token,它将像这样进行:
pred_token_k = model(input_ids[:k]*attention_mask[:k]^T)
请注意,这是伪代码。
为了我们的目的,我们可以忽略注意力掩码。对于 CausalLM 模型,我们通常希望注意力掩码为全 1,因为我们希望关注所有前面的 tokens。同时注意,[:k]实际上意味着我们使用从第 0 索引到第 k-1 索引的元素,因为切片中的结束索引是排除的。
考虑到这一点,我们得到:
pred_token_k = model(input_ids[:k])
损失值通过将labels[k]与pred_token_k进行比较来计算。
实际上,二者都表示为 1xv 的向量,其中 v 是词汇表的大小。每个元素表示该 token 的概率。对于预测结果(pred_token_k),这些是真实的概率,由模型预测得出。对于真实标签(labels[k]),我们可以通过将实际的真实 token 设为 1,词汇表中所有其他 token 设为 0,来人为地使其具有正确的形状。
假设我们正在预测样本句子的第二个单词,也就是说 k=1(我们使用的是零索引 k)。第一个要点是我们用来生成预测的上下文,第二个要点是我们要预测的真实标签 token。
k=1:
-
Input_ids[:1] == [the]
-
Labels[1] == dog
k=2:
-
Input_ids[:2] == [the, dog]
-
Labels[2] == likes
k=3:
-
Input_ids[:3] == [the, dog, likes]
-
Labels[3] == food
假设 k=3,我们向模型输入“[the, dog, likes]”。模型的输出是:
[P(dog)=10%, P(food)=60%,P(likes)=0%, P(the)=30%]
换句话说,模型认为下一个 token 是“dog”的概率是 10%,下一个 token 是“food”的概率是 60%,下一个 token 是“the”的概率是 30%。
真实标签可以表示为:
[P(dog)=0%, P(food)=100%, P(likes)=0%, P(the)=0%]
在实际训练中,我们会使用像交叉熵这样的损失函数。为了尽量直观,让我们简单使用绝对差异来大致感知损失。所谓绝对差异,我指的是预测概率与我们的“真实”概率之间的绝对差值:例如,absolute_diff_dog = |0.10–0.00| = 0.10。
即使使用这个粗略的损失函数,你也能看出,为了最小化损失,我们希望对实际标签(例如 food)预测出较高的概率,并对词汇表中的其他所有标记预测较低的概率。
举个例子,假设在训练后,当我们让模型根据[the, dog, likes]预测下一个标记时,输出结果如下:
现在,我们的损失已经变小,因为在给定这些输入后,我们学会了以较高的概率预测“food”。
训练过程就是不断重复这一过程:尝试将预测的概率与训练序列中所有标记的真实下一个标记对齐。
结论
希望你能对如何使用 HuggingFace 训练 CausalLM 模型的过程有一个直观的了解。你可能会有一些问题,比如:“为什么我们需要将 labels 作为一个单独的数组,而不是直接在每一步使用 input_ids 的第 k 个索引?有没有可能 labels 与 input_ids 不同?”
我会让你思考这些问题,暂时先停在这里。我们将在下一个帖子中继续解答这些问题并展示实际代码!
在 Google Colab 上训练语言模型
迭代微调与序列化指南
·发表于Towards Data Science ·阅读时间 5 分钟·2024 年 12 月 4 日
--

所以,你最近发现了Hugging Face和一系列开源模型,如 BERT、Llama、BART,以及由Mistral AI、Facebook、Salesforce等公司推出的众多生成性语言模型。现在你想为自己的副项目尝试微调一些大型语言模型。一开始一切都很顺利,但接着你发现这些模型计算资源消耗巨大,而你手头并没有 GPU 处理器。
Google Colab慷慨地为你提供了免费计算资源,帮助你解决这个问题。缺点是,你需要在一个过渡性的基于浏览器的环境中完成所有操作。更糟的是,整个环境是有时间限制的,所以似乎不管你做什么,最终都会在内核关闭并且环境被清除时失去你珍贵的微调模型和所有结果。
别担心。解决这个问题的方法是:利用Google Drive保存你的中间结果或模型参数。这样你就可以在稍后的阶段继续实验,或者将训练好的模型拿去其他地方进行推理。
为了做到这一点,您需要一个 Google 账户,且该账户必须有足够的 Google Drive 空间来存储您的训练数据和模型检查点。我假设您已经在 Google Drive 中创建了一个名为data的文件夹,里面包含您的数据集。然后另创建一个名为checkpoints的空文件夹。
在您的 Google Colab Notebook 中,您可以使用以下命令挂载您的 Google Drive:
from google.colab import drive
drive.mount('/content/drive')
现在,您可以在一个新的单元格中使用以下两个命令列出数据和检查点目录的内容:
!ls /content/drive/MyDrive/data
!ls /content/drive/MyDrive/checkpoint
如果这些命令有效,您现在可以在笔记本中访问这些目录。如果命令无效,可能是您错过了授权步骤。上述的drive.mount命令应该会弹出一个窗口,要求您点击并授权访问。您可能错过了弹出窗口,或者没有选择所有必要的访问权限。请尝试重新运行单元格并检查。
一旦解决了访问权限问题,您可以编写脚本,使得模型和结果被序列化到 Google Drive 目录中,从而在会话间得以保留。在理想情况下,您会编写训练作业的代码,使得任何运行时间过长的脚本可以从上一次会话加载部分训练好的模型,并从那个点继续训练。
实现这一点的一种简单方法是创建一个保存和加载函数,供训练脚本使用。训练过程应始终检查是否存在部分训练的模型,然后再初始化一个新的模型。以下是一个保存函数的示例:
def save_checkpoint(epoch, model, optimizer, scheduler, loss, model_name, overwrite=True):
checkpoint = {
'epoch': epoch,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'scheduler_state_dict': scheduler.state_dict(),
'loss': loss
}
direc = get_checkpoint_dir(model_name)
if overwrite:
file_path = direc + '/checkpoint.pth'
else:
file_path = direc + '/epoch_'+str(epoch) + '_checkpoint.pth'
if not os.path.isdir(direc):
try:
os.mkdir(direc)
except:
print("Error: directory does not exist and cannot be created")
file_path = direc +'_epoch_'+str(epoch) + '_checkpoint.pth'
torch.save(checkpoint, file_path)
print(f"Checkpoint saved at epoch {epoch}")
在此情况下,我们将模型状态以及一些元数据(如epochs和loss)保存在一个字典结构中。我们提供了一个选项来覆盖单个检查点文件,或为每个 epoch 创建一个新的文件。我们使用的是 torch 的保存函数,但原则上,您可以使用其他序列化方法。关键思想是,您的程序打开文件并确定现有文件的训练 epoch 数量。这允许程序决定是否继续训练或跳过。
类似地,在加载函数中,我们传入一个我们希望使用的模型引用。如果已经存在一个序列化的模型,我们将参数加载到模型中,并返回训练的 epoch 数量。这个 epoch 值将决定还需要多少额外的训练轮次。如果没有模型,则返回默认的零个 epoch,表明模型仍然是初始化时的状态。
def load_checkpoint(model_name, model, optimizer, scheduler):
direc = get_checkpoint_dir(model_name)
if os.path.exists(direc):
file_path = get_path_with_max_epochs(direc)
checkpoint = torch.load(file_path, map_location=torch.device('cpu'))
model.load_state_dict(checkpoint['model_state_dict'])
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
scheduler.load_state_dict(checkpoint['scheduler_state_dict'])
epoch = checkpoint['epoch']
loss = checkpoint['loss']
print(f"Checkpoint loaded from {epoch} epoch")
return epoch, loss
else:
print(f"No checkpoint found, starting from epoch 1.")
return 0, None
这两个函数需要在您的训练循环中调用,并且您需要确保返回的epochs值用于更新训练迭代中的epochs值。这样,您就拥有了一个可以在内核崩溃时重新启动的训练过程,它会从中断的地方继续。
核心训练循环可能看起来像下面这样:
EPOCHS = 10
for exp in experiments:
model, optimizer, scheduler = initialise_model_components(exp)
train_loader, val_loader = generate_data_loaders(exp)
start_epoch, prev_loss = load_checkpoint(exp, model, optimizer, scheduler)
for epoch in range(start_epoch, EPOCHS):
print(f'Epoch {epoch + 1}/{EPOCHS}')
# ALL YOUR TRAINING CODE HERE
save_checkpoint(epoch + 1, model, optimizer, scheduler, train_loss, exp)
注:在这个例子中,我正在实验训练多个不同的模型配置(在一个名为experiments的列表中),可能会使用不同的训练数据集。辅助函数initialise_model_components和generate_data_loaders负责确保我为每个实验获取正确的模型和数据。
上面的核心训练循环允许我们重用整体代码结构,以训练并序列化这些模型,确保每个模型都能完成所需的训练轮次。如果我们重新启动该过程,它会再次遍历实验列表,但会放弃那些已经达到最大训练轮次的实验。
希望你能使用这个样板代码,在 Google Colab 中设置自己的深度学习语言模型训练实验流程。请发表评论并告诉我你正在构建什么以及如何使用这段代码。
非常感谢Aditya Pramar提供的初始脚本,正是这些脚本促成了这篇作品的诞生。
在 Rust 中从零开始训练 LLM
在这篇配套文章中,我将展示我在 Rust 中从零开始训练一个类似 GPT 的模型的实现。没有 GPU,只有 CPU,且性能比本地 C 代码提高了 30 倍。
·发表于Towards Data Science ·阅读时长 14 分钟·2024 年 12 月 26 日
--

图片来源:GoogleDeepMind via Unsplash
在我上一篇文章中,我介绍了矩阵乘法的问题,注意力算法如何使用矩阵乘法来执行平均处理,以及如何在 Rust 中高效实现——或者至少对我而言——矩阵乘法函数,并使用Blas。
在这篇新文章中,我想展示我在用 Rust 实现llm.c的第一个构建块,即从零开始使用 Rust 训练一个类似 GPT 的模型。这是我了解 Rust 生态系统并理解它与 C 语言的可比性的方式。特别是,我希望我的代码能够在仅使用 CPU 的情况下,从 GPT 权重开始训练一个类似 GPT 的模型——也就是不使用 GPU 或 TPU。我的目标是理解我们在简单笔记本电脑上能够推多大这些模型,以及 Rust 生态系统在这方面能发挥多大作用。最终,这段代码也可能对基于给定输入语料库微调 GPT 模型有所帮助。
所有相关的代码片段可以在这里找到。
使用 CrewAI 和 Streamlit 将客户反馈转化为可操作的洞察。
AI 用于商业智能
构建一个 AI 驱动的应用程序,分析非结构化反馈,生成有洞察力的报告,并创建交互式可视化。
·发表于Towards Data Science ·13 分钟阅读·2024 年 12 月 19 日
--

新的 AI 代理工具使得自动化数据分析变得非常简单。当与像 Streamlit 这样的可视化平台结合时,创建一个具有视觉冲击力的商业报告应用程序变得轻而易举。
在本教程中,我们将重点分析客户反馈。这种分析以及对客户情感的良好理解对于确保产品符合预期并解决质量问题至关重要。被认为质量差的产品是不会畅销的。
然而,分析客户反馈可能涉及处理大量的非结构化数据——这是一项可能令人生畏的任务。这正是大型语言模型的强项,它能够从这些数据中提取有意义的洞察。
我们的第一步是明确目标。
假设我们有一个来自在线零售商的客户反馈信息列表。我们的目标是生成一份高层报告,突出表现最好的产品,识别表现不佳的产品……
使用双曲正弦转换数据
为什么处理负值应该是轻松的事
·发表于 Towards Data Science ·阅读时间:8 分钟·2024 年 4 月 29 日
--

图片由 Osman Rana 提供,来源于 Unsplash
许多模型对异常值非常敏感,例如 线性回归、k 近邻算法 和 ARIMA。机器学习算法在异常值的存在下容易过拟合,可能无法很好地推广。¹ 然而,适当的转换可以缩小这些极端值,从而提高模型的性能。
处理负值数据的转换包括:
-
平移对数
-
平移 Box-Cox
-
逆双曲正弦
-
双曲正弦-逆双曲正弦
对数和 Box-Cox 是处理正值数据时的有效工具,但逆双曲正弦(arcsinh)在处理负值时要有效得多。
双曲正弦-逆双曲正弦更为强大。它有两个参数可以调整数据的偏度和峰度,使其更接近正态分布。这些参数可以通过梯度下降法推导出来。请参见本文末尾的 Python 实现。
平移对数
对数转换可以通过添加平移项α来适应负值的处理。

在整篇文章中,我使用“log”表示自然对数。
从视觉上看,这是将对数的垂直渐近线从 0 移动到α。

这是平移后的对数变换图,偏移值为-5,使用Desmos制作,遵循CC BY-SA 4.0协议。图中添加了方程式文本。
股票价格预测
假设你正在构建一个预测股市的模型。Hosenzade 和 Haratizadeh 通过卷积神经网络解决这个问题,使用了我从UCI 欧文机器学习库²提取的大量特征变量。下面是成交量变化特征的分布——这是股市预测中的一个重要技术指标。

使用 Matplotlib 制作
分位数-分位数(QQ)图显示了右尾和左尾的重尾现象。我们变换的目标是将尾部拉近正态分布(红线),使其不再有离群值。
使用-250 的平移值,我得到了这个对数分布。

右尾看起来稍微好一些,但左尾仍然偏离红线。对数变换通过对数据应用一个凹函数,压缩高值并拉伸低值,从而使数据左偏。
对数变换仅使右尾变轻。
虽然这种方法对正偏态数据有效,但对于带有负离群值的数据效果较差。

使用 Desmos 制作,遵循CC BY-SA 4.0协议。图中添加了文本和箭头。
在股票数据中,偏态不是问题。极值出现在左右两侧。峰度较高,意味着两侧的尾部都较重。一个简单的凹函数无法应对这种情况。

移动的 Box-Cox 变换
Box-Cox 是对数变换的广义版本,它也可以通过平移来处理负值,写作:

λ参数控制变换的凹度,使其能够呈现多种形式。当λ = 2 时,Box-Cox 变换为二次型;当λ = 1 时,它是线性的;当λ接近 0 时,它趋近于对数变换。这可以通过使用 L'Hôpital 法则验证。


这是平移后的 Box-Cox 变换图,平移值为-5,并使用了五个不同的λ值,使用Desmos制作,遵循CC BY-SA 4.0协议。图中添加了文本。
为了在我们的股价数据上应用这种变换,我使用平移值-250,并使用 scipy 的boxcox函数来确定λ。
from scipy.stats import boxcox
y, lambda_ = boxcox(x - (-250))
变换后的数据如下所示:

尽管这种变换具有灵活性,但它未能缩小股票价格数据的尾部。λ的低值使数据向左偏斜,缩小了右尾。λ的高值使数据向右偏斜,缩小了左尾,但没有任何值能同时缩小两者。
反双曲正弦
双曲正弦函数(sinh)的定义为

其反函数为

在这种情况下,反函数更为有用,因为它对于较大的x(无论是正数还是负数)大致是对数函数,而对于较小的x则是线性函数。实际上,这种变换缩小了极端值,同时保持了中心值基本不变。
Arcsinh 减小了正负尾部。
对于正值,arcsinh 是凹的,而对于负值,它是凸的。这种曲率的变化是其能够同时处理正负极端值的秘诀。

与对数函数相比,反双曲正弦(arcsinh)的图像,由 Desmos 制作,并在 CC BY-SA 4.0 下可用。图像中添加了文本、箭头和框形。
使用这种变换后的股票数据结果显示,尾部几乎符合正态分布。新的数据没有异常值!

尺度很重要
在将数据输入 arcsinh 之前,请考虑数据的尺度。
对于对数函数,单位的选择无关紧要。美元还是分,克还是千克,英里还是英尺——对对数函数来说都是一样的。输入值的尺度只会将变换后的值平移一个常数值。

对于反双曲正弦(arcsinh),情况并非如此。-1 到 1 之间的值几乎保持不变,而较大的数值则由对数主导。在将数据输入 arcsinh 之前,您可能需要调整不同的尺度和偏移量,以获得您满意的结果。
在文章的最后,我在 python 中实现了一个梯度下降算法,以更精确地估计这些变换参数。

Sinh-arcsinh
由 Jones 和 Pewsey³提出,sinh-arcsinh 变换为

Jones 和 Pewsey 没有在前面包含常数项 1/δ。不过,我在这里包含它,因为它使得展示 arcsinh 作为极限情况更加容易。
参数ε调整数据的偏斜度,δ调整峰度³,使得变换可以呈现多种形式。例如,恒等变换 f(x) = x 是当ε = 0 且δ = 1 时的 sinh-arcsinh 的特例。arcsinh 是当ε = 0 且δ趋近于零时的极限情况,使用 L'Hôpital 法则可以验证这一点。


sinh-arcsinh 变换在不同 ε 和 δ 值下的图。左侧 ε 固定为零,右侧 δ 固定为 0.5,使用* Desmos 制作,采用 CC BY-SA 4.0 许可。图中加入了文字。
规模仍然重要
就像 arcsinh 一样,sinh-arcsinh 变换的结果会受到输入数据平移或缩放的显著影响,这意味着可以选择的不仅仅是两个,而是四个参数。

参数估计
我们已经看到数据变换如何使数据的尾部更加符合高斯分布。现在,让我们通过选择最大化正态对数似然的参数,将其提升到一个新的层次。
设 T(x) 为我的变换,N(x | μ, σ) 为正态分布的概率密度函数,其中均值为 μ,标准差为 σ。假设独立,整个数据集 X 的似然是

这里我利用了变换的雅可比矩阵。对数似然是

我可以去掉绝对值符号,因为变换的导数始终是正的。
梯度下降
我使用梯度下降估计我的参数,将损失函数设置为负的平均对数似然。Tensorflow 的 GradientTape 会自动计算关于 sinh-arcsinh 四个参数以及正态分布的 μ 和 σ 的偏导数。为了确保它们保持正值,参数 β、δ 和 σ 以对数形式表示。你可能想尝试几个变量初始化,以防算法陷入局部最小值。我还建议在运行脚本前,将输入数据标准化为均值为零、标准差为一,以获得最佳性能。
import tensorflow as tf
import numpy as np
@tf.function
def log_prob(x, params):
# extract parameters
alpha, log_beta = params[0], params[1] # rescaling params
beta = tf.math.exp(log_beta)
epsilon, log_delta = params[2], params[3] # transformation params
delta = tf.math.exp(log_delta)
mu, log_sigma = params[4], params[5] # normal dist params
sigma = tf.math.exp(log_sigma)
# rescale
x_scaled = (x - alpha)/beta
# transformation
sinh_arg = epsilon + delta * tf.math.asinh(x_scaled)
x_transformed = (1/delta) * tf.math.sinh(sinh_arg)
# log jacobian of transformation
d_sinh = tf.math.log(tf.math.cosh(sinh_arg))
d_arcsinh = - 0.5*tf.math.log(x_scaled**2 + 1)
d_rescaling = - log_beta
jacobian = d_sinh + d_arcsinh + d_rescaling # chain rule
# normal likelihood
z = (x_transformed - mu)/sigma # standardized
normal_prob = -0.5*tf.math.log(2*np.pi) - log_sigma -0.5*z**2
return normal_prob + jacobian
# Learning rate and number of epochs
learning_rate = 0.1
epochs = 1000
# Initialize variables
tf.random.set_seed(892)
params = tf.Variable(tf.random.normal(shape=(6,), mean=0.0, stddev=1.0), dtype=tf.float32)
# Use the Adam optimizer
optimizer = tf.optimizers.Adam(learning_rate=learning_rate)
# Perform gradient descent
for epoch in range(epochs):
with tf.GradientTape() as tape:
loss = - tf.reduce_mean(log_prob(x_tf, params))
# Compute gradients
gradients = tape.gradient(loss, [params])
# Apply gradients to variables
optimizer.apply_gradients(zip(gradients, [params]))
if (epoch % 100) == 0:
print(-loss.numpy())
print(f"Optimal vals: {params}")
这种优化方法使得分布非常接近高斯分布——不仅是尾部,连中间部分也如此!

结论
对于处理正值数据时,Log 和 Box-Cox 是强大的变换,但仅仅将这些变换平移以包括负值是有严重局限的。arcsinh 变换在同时处理极端正值和负值时要好得多。
如果你愿意增加复杂度,sinh-arcsinh 变换是一个更强大的函数,它是 arcsinh 的广义形式。当正态性非常重要时,其参数也可以通过梯度下降推导出来,以匹配高斯分布。
Arcsinh 变换不常被关注,但它是一个至关重要的变换,应该是每个数据工程师工具箱的一部分。
如果你发现这些转换技巧有用,或者对如何将它们应用到你自己的数据集有任何问题,请在下面的评论中分享你的想法和经验。
除非另有说明,所有图片均由作者提供。
来自《Towards Data Science》编辑的提醒: 虽然我们允许独立作者根据我们的 规则和指南 发布文章,但我们并不认同每位作者的贡献。你不应在没有寻求专业建议的情况下依赖作者的作品。详情请参见我们的 读者条款 。
[1] Jabbar, H. K., & Khan, R. Z. (2014). 在监督机器学习中避免过拟合和欠拟合的方法(比较研究)。
[2] CNNpred: 基于 CNN 的股票市场预测,使用多样化的变量集。(2019)。 UCI 机器学习数据集。
[3] Jones, M & Pewsey, Arthur. (2009). Sinh-arcsinh 分布:一种广泛的家族,能够产生强有力的正态性和对称性检验。Biometrika。
将点云转换为 3D 网格:Python 教程
3D Python
学习如何 使用 Python 从点云数据生成 3D 网格。本教程的最后,我们将实现一个带有Marching Cubes 算法的 3D 建模应用程序。
·发表于 Towards Data Science ·15 分钟阅读·2024 年 11 月 1 日
--

如何将任何点云转换为一个可靠的 3D 网格?© Florent Poux
3D 数据的世界通常是一个支离破碎的景观。
存在点云,它们细节丰富,但缺乏表面信息。
存在3D 网格,它们明确定义了表面,但通常创建起来比较复杂。
将点云转换为网格弥补了这一差距,并解锁了许多可能性,从逼真的模拟到 3D 数字环境设计。
即使你不在渲染部分使用网格,拥有它也能确保我们可以高效地模拟碰撞效果、计算 3D 建筑中的可步行空间,或处理大型场景的遮挡剔除。
那么我们该如何实现呢?如何将任何点云(来自摄影测量、3D 高斯点云渲染或激光扫描方法)转化为一个可靠的 3D 网格?
让我们使用 Python 编写一个强大的点云网格化技术,并将其打造成一个具有图形用户界面的微型 SaaS 应用程序。
变换器?扩散?输血!
对最新的多模态输血模型的温和介绍
·发表于Towards Data Science ·阅读时间 6 分钟·2024 年 9 月 12 日
--
最近,Meta 和 Waymo 发布了他们的最新论文——Transfusion: 用一个多模态模型预测下一个令牌并扩散图像,该论文将流行的变换器模型与扩散模型结合,用于多模态训练和预测。
与 Meta 的先前工作类似,Transfusion 模型基于Llama 架构,采用早期融合,既处理文本令牌序列,也处理图像令牌序列,并使用单一的变换器模型来生成预测。但与以往的工作不同,Transfusion 模型对图像令牌的处理方式有所不同:
-
图像令牌序列由预训练的变分自编码器部分生成。
-
图像序列的变换器注意力是双向的,而非因果的。

Transfusion 模型架构与预训练任务。文本预训练是下一个词预测任务。图像预训练是去噪扩散任务。图像来源:www.arxiv.org/pdf/2408.11039
让我们详细讨论以下内容。我们将首先回顾基础知识,例如自回归和扩散模型,然后深入研究 Transfusion 架构。
自回归模型
目前,大型语言模型(LLMs)主要基于变换器架构,该架构在 2017 年的《Attention is All You Need》论文中提出。变换器架构包含两部分:编码器和解码器。

Transformer 架构。左—编码器;右—解码器。图像来源:arxiv.org/pdf/1706.03762
像 BERT 这样的掩码语言模型使用预训练的编码器部分,通过随机双向掩码令牌预测任务(以及下一句预测)进行训练。而像最新的 LLM 这样的自回归模型,解码器部分通常通过下一令牌预测任务进行训练,其中语言模型的损失被最小化:

方程来源:www.arxiv.org/pdf/2408.11039
在上面的方程中,θ是模型参数集,y_i 是长度为 n 的序列中索引为 i 的令牌,y<i 是所有位于 y_i 之前的令牌。
扩散模型
扩散模型是什么?它是一系列常用于计算机视觉(尤其是医学图像分析)中的深度学习模型,主要用于图像生成/去噪等任务。最著名的扩散模型之一是 DDPM,它来自于 2020 年发表的去噪扩散概率模型论文。该模型是一个参数化的马尔可夫链,包含了前向和反向转移,如下所示。

扩散模型是一个双向马尔可夫链。图像来源:arxiv.org/pdf/2006.11239
马尔可夫链是什么?它是一种统计过程,其中当前步骤仅依赖于前一步,反之亦然。通过假设一个马尔可夫过程,模型可以在前向过程中(图中从右到左)通过迭代添加高斯噪声来开始于一个干净的图像,并通过在反向过程中(图中从左到右)使用基于 UNet 的架构迭代“学习”噪声。这就是为什么我们有时可以将扩散模型看作生成模型(从左到右使用时),有时又可以看作去噪模型(从右到左使用时)。下面给出了 DDPM 损失,其中θ是模型参数集,\epsilon 是已知的噪声,而\epsilon_theta 是由深度学习模型(通常是 UNet)估计的噪声:

方程来源:www.arxiv.org/pdf/2408.11039
潜在空间中的扩散模型
扩散的思想进一步扩展到潜在空间中,在CVPR’22 论文中,图像首先通过使用预训练变分自编码器(VAE)的编码器部分“压缩”到潜在空间。然后,扩散和反向过程在潜在空间中进行,并通过 VAE 的解码器部分将其映射回像素空间。这可以大大提高学习速度和效率,因为大部分计算是在较低维度的空间中进行的。

潜在扩散模型架构。 \Epsilon 和 D 分别是编码器和解码器。图像来源:arxiv.org/pdf/2112.10752
基于 VAE 的图像输送
Transfusion 模型的核心部分是扩散模型与变压器模型在输入图像上的融合。首先,图像被划分为一个 8*8 的补丁序列;每个补丁被送入一个预训练的 VAE 编码器进行“压缩”,以得到一个 8 元素的潜在向量表示。然后,噪声被添加到潜在表示中,并通过一个线性层/U-Net 编码器进一步处理,生成“带噪声”的 x_t。接着,变压器模型处理这一序列的带噪声潜在表示。最后,输出通过另一个线性/U-Net 解码器反向处理,然后使用 VAE 解码器生成“真实”的 x_0 图像。

图像输入的扩散模块部分。噪声被添加到 VAE 编码的嵌入中。图像来源:www.arxiv.org/pdf/2408.11039
在实际实现中,图像的开始(BOI)令牌和结束(EOI)令牌会被填充到图像表示序列的两侧,然后再与文本令牌进行拼接。图像训练的自注意力是双向注意力,而文本令牌的自注意力是因果的。在训练阶段,图像序列的损失是 DDPM 损失,而其余的文本令牌使用的是语言模型(LM)损失。
那为什么要费劲呢?为什么我们需要如此复杂的处理程序来处理图像补丁令牌?论文解释说,文本和图像的令牌空间是不同的。虽然文本令牌是离散的,但图像令牌/补丁自然是连续的。在以往的技术中,图像令牌需要在融合到变压器模型之前进行“离散化”,而直接整合扩散模型可以解决这一问题。
与最先进技术比较
论文比较的主要多模态模型是Chameleon 模型,这是 Meta 今年早些时候提出的。在这里,我们比较了 Chameleon-7B 和 Transfusion-7B 在架构和训练集大小上的差异。

Chameleon 7B 和 Transfusion 7B 之间的架构与训练差异。图片由作者提供。
论文列出了在 Llama2 预训练套件精度、COCO 零样本 Frechet Inception 距离(FID)和 GenEval 基准测试上的性能比较。我们可以看到,Transfusion 在与图像相关的基准(COCO 和 Gen)上表现远好于 Chameleon,而在与 Chameleon 相比时,仅略微有所失分,且参数量相同。

图像来源:www.arxiv.org/pdf/2408.11039
进一步的评论。
尽管论文的思路非常有趣,但“Transfusion”中的“扩散”部分实际上并不是真正的扩散,因为在马尔可夫过程中只有两个时间戳。此外,预训练的 VAE 使得模型不再是严格的端到端。并且,VAE + 线性/UNet + Transformer 编码器 + 线性/UNet + VAE 设计看起来非常复杂,这让观众不禁问,是否有更优雅的方式来实现这个想法?此外,我之前写过关于苹果公司最新出版的论文,探讨了在图像上使用自回归建模的泛化效益,因此或许值得再次思考“MIM + 自回归”方法。
如果你觉得这篇文章有趣并且想讨论,欢迎留言,我很乐意在那儿进一步展开讨论 😃
参考文献
-
Zhou 等人,《Transfusion:用一个多模态模型预测下一个令牌并扩散图像》,arXiv 2024。
-
C. Team,《Chameleon:混合模态早期融合基础模型》,arXiv 预印本 2024。
-
Touvron 等人,《Llama:开放且高效的基础语言模型》,arXiv 2023。
-
Rombach 等人,《使用潜在扩散模型进行高分辨率图像合成》,CVPR 2022。
-
Ho 等人,《去噪扩散概率模型》,NeurIPS 2020。
-
Vaswani,《Attention is all you need》,NeurIPS 2017。
-
Kingma,《自编码变分贝叶斯》,arXiv 预印本 2013。
Transformers:从自然语言处理到计算机视觉
Transformer 架构是如何适应计算机视觉任务的
·发布于Towards Data Science ·阅读时间:7 分钟·2024 年 5 月 5 日
--

由kyler trautner拍摄,图片来自Unsplash
引言
2017 年,论文《Attention is all you need》[1]引起了 NLP 研究界的轰动。至今已被引用超过 10 万次,它的 Transformer 成为了如今大多数主流 NLP 架构的基石。要了解 Transformer 在 NLP 领域的重要成果,可以查看我之前的文章。
它们的结构有何不同,这些差异如何影响模型的能力?
towardsdatascience.com
与此同时,由于计算机视觉(CV)长期以来由 CNN 主导,Transformer 在该领域的应用直到最近才有所突破。本文将讨论将 Transformer 应用于计算机视觉所面临的挑战,以及计算机视觉研究人员如何调整这些模型。
挑战
分词
分词文本序列已经被长期研究,采用各种优化方法来概括和适应未见过的文本。然而,所有这些努力都依赖于将字符和术语作为单位来处理的思想。
Transformer:它们如何转化你的数据?
深入探索 Transformer 架构及其在语言任务中无敌的原因
·发表于Towards Data Science ·阅读时间 11 分钟·2024 年 3 月 28 日
--

作者提供的图片
在人工智能和机器学习飞速发展的今天,有一种创新脱颖而出,对我们处理、理解和生成数据的方式产生了深远影响:Transformer。Transformer 彻底改变了自然语言处理(NLP)及其他领域,为今天一些最先进的 AI 应用提供了动力。但究竟什么是 Transformer,它们又是如何以如此突破性的方式转换数据的呢?本文将揭秘 Transformer 模型的内部工作原理,重点讲解编码器架构。我们将从 Python 中 Transformer 编码器的实现入手,逐步解析其主要组件。然后,我们将可视化 Transformer 如何在训练过程中处理并适应输入数据。
虽然本博客并未涵盖所有架构细节,但它提供了一个实现并帮助你全面理解 Transformer 的变革性力量。想深入了解 Transformer 的工作原理,我建议你参考斯坦福大学的优秀 CS224-n 课程。
我还建议关注与本文相关的GitHub 仓库以获取更多细节。😊
什么是 Transformer 编码器架构?

来自Attention Is All You Need的 Transformer 模型
这张图片展示了原始 Transformer 架构,将编码器和解码器结合用于序列到序列的语言任务。
在本文中,我们将重点介绍编码器架构(图中的红色块)。这正是流行的 BERT 模型在后台使用的架构:其主要关注的是理解和表示数据,而不是生成序列。它可以用于多种应用:文本分类、命名实体识别(NER)、抽取式问答等。
那么,这些数据究竟是如何通过该架构进行转换的呢?我们将详细解释每个组件,但这里是过程的概述。
-
输入文本被标记化:Python 字符串被转换成标记(数字)列表。
-
每个标记都通过一个嵌入层,该层输出每个标记的向量表示。
-
然后,这些嵌入会通过位置编码层进一步编码,添加每个标记在序列中的位置信息。
-
这些新的嵌入通过一系列编码器层进行转换,使用自注意力机制。
-
可以添加一个任务特定的头。例如,我们稍后将使用一个分类头,将电影评论分类为正面或负面。
重要的是要理解,Transformer 架构通过将嵌入向量从高维空间中的一个表示映射到同一空间中的另一个表示,应用一系列复杂的变换来转换这些嵌入。
在 Python 中实现编码器架构
位置编码器层
与 RNN 模型不同,自注意力机制不利用输入序列的顺序。PositionalEncoder 类通过使用两种数学函数:余弦和正弦,向输入嵌入添加位置编码。

位置编码矩阵定义来自Attention Is All You Need
注意,位置编码不包含可训练的参数:它们是确定性计算的结果,这使得该方法非常可处理。此外,正弦和余弦函数的值介于-1 和 1 之间,并具有有助于模型学习单词相对位置的有用周期性特性。
class PositionalEncoder(nn.Module):
def __init__(self, d_model, max_length):
super(PositionalEncoder, self).__init__()
self.d_model = d_model
self.max_length = max_length
# Initialize the positional encoding matrix
pe = torch.zeros(max_length, d_model)
position = torch.arange(0, max_length, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2, dtype=torch.float) * -(math.log(10000.0) / d_model))
# Calculate and assign position encodings to the matrix
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
self.pe = pe.unsqueeze(0)
def forward(self, x):
x = x + self.pe[:, :x.size(1)] # update embeddings
return x
多头自注意力
自注意力机制是编码器架构的关键组件。我们暂时忽略“多头”部分。注意力是一种方法,用来确定每个标记(即每个嵌入)与所有其他嵌入与该标记的相关性,从而获得更精细和与上下文相关的编码。

“它”是如何关注序列中其他单词的?(The Illustrated Transformer)
自注意力机制有三个步骤。
-
使用矩阵 Q、K 和 V 分别转换输入的“查询”、“键”和“值”。请注意,对于自注意力机制,查询、键和值都是等于我们的输入嵌入。
-
通过余弦相似度(点积)计算查询和键之间的注意力得分。得分会通过嵌入维度的平方根进行缩放,以稳定训练过程中的梯度。
-
使用 softmax 层将这些得分转化为概率。
-
输出是值的加权平均,使用注意力得分作为权重。
从数学角度来看,这对应于以下公式。

注意力机制来自《Attention Is All You Need》
“多头”是什么意思?基本上,我们可以并行多次应用上述自注意力机制过程,并将输出进行拼接和投影。这使得每个头可以专注于句子的不同语义方面。
我们首先定义头的数量、嵌入的维度(d_model)以及每个头的维度(head_dim)。我们还初始化了 Q、K 和 V 矩阵(线性层),以及最终的投影层。
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, num_heads):
super(MultiHeadAttention, self).__init__()
self.num_heads = num_heads
self.d_model = d_model
self.head_dim = d_model // num_heads
self.query_linear = nn.Linear(d_model, d_model)
self.key_linear = nn.Linear(d_model, d_model)
self.value_linear = nn.Linear(d_model, d_model)
self.output_linear = nn.Linear(d_model, d_model)
使用多头注意力时,我们对每个注意力头应用一个减少维度的处理(使用 head_dim 而非 d_model),就像原论文中所述,这使得总的计算成本类似于一个全维度的单头注意力层。请注意,这只是一个逻辑上的拆分。多头注意力之所以强大,是因为它仍然可以通过单一的矩阵操作来表示,从而使得 GPU 上的计算非常高效。
def split_heads(self, x, batch_size):
# Split the sequence embeddings in x across the attention heads
x = x.view(batch_size, -1, self.num_heads, self.head_dim)
return x.permute(0, 2, 1, 3).contiguous().view(batch_size * self.num_heads, -1, self.head_dim)
我们计算注意力得分,并使用掩码避免在填充的标记上使用注意力。我们应用 softmax 激活函数将这些得分转化为概率。
def compute_attention(self, query, key, mask=None):
# Compute dot-product attention scores
# dimensions of query and key are (batch_size * num_heads, seq_length, head_dim)
scores = query @ key.transpose(-2, -1) / math.sqrt(self.head_dim)
# Now, dimensions of scores is (batch_size * num_heads, seq_length, seq_length)
if mask is not None:
scores = scores.view(-1, scores.shape[0] // self.num_heads, mask.shape[1], mask.shape[2]) # for compatibility
scores = scores.masked_fill(mask == 0, float('-1e20')) # mask to avoid attention on padding tokens
scores = scores.view(-1, mask.shape[1], mask.shape[2]) # reshape back to original shape
# Normalize attention scores into attention weights
attention_weights = F.softmax(scores, dim=-1)
return attention_weights
forward 属性执行多头逻辑拆分并计算注意力权重。然后,我们通过将这些权重与值相乘来获得输出。最后,我们重新调整输出的形状并通过线性层进行投影。
def forward(self, query, key, value, mask=None):
batch_size = query.size(0)
query = self.split_heads(self.query_linear(query), batch_size)
key = self.split_heads(self.key_linear(key), batch_size)
value = self.split_heads(self.value_linear(value), batch_size)
attention_weights = self.compute_attention(query, key, mask)
# Multiply attention weights by values, concatenate and linearly project outputs
output = torch.matmul(attention_weights, value)
output = output.view(batch_size, self.num_heads, -1, self.head_dim).permute(0, 2, 1, 3).contiguous().view(batch_size, -1, self.d_model)
return self.output_linear(output)
编码器层
这是该架构的主要组件,它利用了多头自注意力。我们首先实现一个简单的类,通过 2 个全连接层执行前向操作。
class FeedForwardSubLayer(nn.Module):
def __init__(self, d_model, d_ff):
super(FeedForwardSubLayer, 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 EncoderLayer(nn.Module):
def __init__(self, d_model, num_heads, d_ff, dropout):
super(EncoderLayer, self).__init__()
self.self_attn = MultiHeadAttention(d_model, num_heads)
self.feed_forward = FeedForwardSubLayer(d_model, d_ff)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x, mask):
attn_output = self.self_attn(x, x, x, mask)
x = self.norm1(x + self.dropout(attn_output)) # skip connection and normalization
ff_output = self.feed_forward(x)
return self.norm2(x + self.dropout(ff_output)) # skip connection and normalization
将一切整合在一起
现在是时候创建我们的最终模型了。我们通过嵌入层将数据传递进去。这将原始标记(整数)转换为数值向量。然后我们应用位置编码器和若干(num_layers)编码器层。
class TransformerEncoder(nn.Module):
def __init__(self, vocab_size, d_model, num_layers, num_heads, d_ff, dropout, max_sequence_length):
super(TransformerEncoder, self).__init__()
self.embedding = nn.Embedding(vocab_size, d_model)
self.positional_encoding = PositionalEncoder(d_model, max_sequence_length)
self.layers = nn.ModuleList([EncoderLayer(d_model, num_heads, d_ff, dropout) for _ in range(num_layers)])
def forward(self, x, mask):
x = self.embedding(x)
x = self.positional_encoding(x)
for layer in self.layers:
x = layer(x, mask)
return x
我们还创建了一个 ClassifierHead 类,用于将最终的嵌入转换为分类任务的类别概率。
class ClassifierHead(nn.Module):
def __init__(self, d_model, num_classes):
super(ClassifierHead, self).__init__()
self.fc = nn.Linear(d_model, num_classes)
def forward(self, x):
logits = self.fc(x[:, 0, :]) # first token corresponds to the classification token
return F.softmax(logits, dim=-1)
注意,稠密层和 softmax 层只应用于第一个嵌入(对应于输入序列的第一个标记)。这是因为在分词文本时,第一个标记是[CLS]标记,代表“分类”。[CLS]标记的设计目的是将整个序列的信息聚合成一个单一的嵌入向量,作为可以用于分类任务的摘要表示。
注意:包含[CLS]标记的概念源自 BERT,BERT 最初是在类似下一句预测任务上进行训练的。[CLS]标记被插入以预测句子 B 是否跟随句子 A,而[SEP]标记则分隔这两个句子。对于我们的模型,[SEP]标记只是标记输入句子的结束,如下所示。

BERT 架构中的[CLS]标记 (All About AI)
当你仔细想想,真是令人震惊,这个单一的[CLS]嵌入能够捕获如此多关于整个序列的信息,这要归功于自注意力机制能够权衡和综合每个文本片段之间相互关系的重要性。
训练与可视化
希望上一节能帮助你更好地理解我们的 Transformer 模型是如何转换输入数据的。接下来,我们将编写我们的训练流程,用于处理 IMDB 数据集(电影评论)的二分类任务。然后,我们将可视化训练过程中[CLS]标记的嵌入,看看我们的模型是如何转换它的。
我们首先定义超参数,并且使用 BERT 分词器。在 GitHub 仓库中,你可以看到我还编写了一个函数,来选择数据集的一个子集,其中包含 1200 个训练样本和 200 个测试样本。
num_classes = 2 # binary classification
d_model = 256 # dimension of the embedding vectors
num_heads = 4 # number of heads for self-attention
num_layers = 4 # number of encoder layers
d_ff = 512\. # dimension of the dense layers in the encoder layers
sequence_length = 256 # maximum sequence length
dropout = 0.4 # dropout to avoid overfitting
num_epochs = 20
batch_size = 32
loss_function = torch.nn.CrossEntropyLoss()
dataset = load_dataset("imdb")
dataset = balance_and_create_dataset(dataset, 1200, 200) # check GitHub repo
tokenizer = AutoTokenizer.from_pretrained('bert-base-uncased', model_max_length=sequence_length)
你可以尝试在其中一个句子上使用 BERT 分词器:
print(tokenized_datasets['train']['input_ids'][0])
每个序列应以 101 号标记开始,对应[CLS],接着是一些非零整数,如果序列长度小于 256,则用零填充。注意,这些零在使用我们的“掩码”进行自注意力计算时会被忽略。
tokenized_datasets = dataset.map(encode_examples, batched=True)
tokenized_datasets.set_format(type='torch', columns=['input_ids', 'attention_mask', 'label'])
train_dataloader = DataLoader(tokenized_datasets['train'], batch_size=batch_size, shuffle=True)
test_dataloader = DataLoader(tokenized_datasets['test'], batch_size=batch_size, shuffle=True)
vocab_size = tokenizer.vocab_size
encoder = TransformerEncoder(vocab_size, d_model, num_layers, num_heads, d_ff, dropout, max_sequence_length=sequence_length)
classifier = ClassifierHead(d_model, num_classes)
optimizer = torch.optim.Adam(list(encoder.parameters()) + list(classifier.parameters()), lr=1e-4)
现在我们可以编写训练函数:
def train(dataloader, encoder, classifier, optimizer, loss_function, num_epochs):
for epoch in range(num_epochs):
# Collect and store embeddings before each epoch starts for visualization purposes (check repo)
all_embeddings, all_labels = collect_embeddings(encoder, dataloader)
reduced_embeddings = visualize_embeddings(all_embeddings, all_labels, epoch, show=False)
dic_embeddings[epoch] = [reduced_embeddings, all_labels]
encoder.train()
classifier.train()
correct_predictions = 0
total_predictions = 0
for batch in tqdm(dataloader, desc="Training"):
input_ids = batch['input_ids']
attention_mask = batch['attention_mask'] # indicate where padded tokens are
# These 2 lines make the attention_mask a matrix instead of a vector
attention_mask = attention_mask.unsqueeze(-1)
attention_mask = attention_mask & attention_mask.transpose(1, 2)
labels = batch['label']
optimizer.zero_grad()
output = encoder(input_ids, attention_mask)
classification = classifier(output)
loss = loss_function(classification, labels)
loss.backward()
optimizer.step()
preds = torch.argmax(classification, dim=1)
correct_predictions += torch.sum(preds == labels).item()
total_predictions += labels.size(0)
epoch_accuracy = correct_predictions / total_predictions
print(f'Epoch {epoch} Training Accuracy: {epoch_accuracy:.4f}')
你可以在 GitHub 仓库中找到 collect_embeddings 和 visualize_embeddings 函数。它们存储了训练集每个句子的[CLS]标记嵌入,应用一种叫做 t-SNE 的降维技术将其转化为二维向量(而不是 256 维向量),并保存动画图表。
让我们来可视化结果。

每个训练点的投影[CLS]嵌入(蓝色代表正向句子,红色代表负向句子)
通过观察每个训练点的[CLS]嵌入的投影图,我们可以看到经过若干轮训练后,正向(蓝色)和负向(红色)句子之间的明显区别。这一可视化展示了 Transformer 架构随着时间推移调整嵌入的显著能力,突出了自注意力机制的强大功能。数据以一种方式进行转化,使得每个类别的嵌入得到了良好的分离,从而大大简化了分类器头的任务。
结论
随着我们对 Transformer 架构的探索的结束,显然这些模型擅长将数据定制化以适应特定任务。通过使用位置编码和多头自注意力机制,Transformer 不仅仅是处理数据:它们以一种前所未见的复杂程度来解释和理解信息。能够动态地权衡输入数据不同部分的相关性,使得对输入文本的理解和表示更加细致。这提升了在各种下游任务中的表现,包括文本分类、问答、命名实体识别等。
现在你已经更好地理解了编码器架构,你可以深入探讨解码器和编码器-解码器模型,这些模型与我们刚刚探讨的非常相似。解码器在生成任务中起着至关重要的作用,是流行的 GPT 模型的核心部分。
-
随时在LinkedIn上与我联系
-
请在GitHub上关注我,获取更多内容
-
访问我的网站: maximewolf.com
参考文献
[1] Vaswani, Ashish, 等人. “Attention Is All You Need.” 第 31 届神经信息处理系统会议(NIPS 2017), 美国加利福尼亚州长滩.
[2] “The Illustrated Transformer.” Jay Alammar 的博客, 2018 年 6 月, jalammar.github.io/illustrated-transformer/
[3] Transformer 架构的官方 PyTorch 实现. GitHub 代码库, PyTorch, github.com/pytorch/pytorch/blob/master/torch/nn/modules/transformer.py
[4] Manning, Christopher, 等人. “CS224n: 使用深度学习进行自然语言处理.” 斯坦福大学, 斯坦福 CS224N NLP 课程, web.stanford.edu/class/cs224n/
Transformer 键值(KV)缓存解释
LLMOps
加速你的 LLM 推理
·发布于 Towards Data Science ·14 分钟阅读·2024 年 12 月 11 日
--

Transformer 架构可以说是现代深度学习中最具影响力的创新之一。它在著名的 2017 年论文《Attention Is All You Need》中提出,已经成为大多数与语言相关的建模方法,包括所有大型语言模型(LLMs),如 GPT 系列,以及许多计算机视觉任务。
随着这些模型的复杂性和规模的增长,它们推理速度优化的需求也在增加,尤其是在聊天应用中,用户期望即时回复。键值(KV)缓存是实现这一目标的一个巧妙技巧——让我们看看它是如何工作的,以及何时使用它。
Transformer 架构概述
在深入了解 KV 缓存之前,我们需要稍作绕道,了解一下 transformers 中的注意力机制。理解其工作原理是发现和理解 KV 缓存如何优化 transformer 推理的关键。
我们将重点关注用于生成文本的自回归模型。这些所谓的解码器模型包括 GPT 系列、Gemini、Claude 或 GitHub Copilot。它们的训练任务很简单:预测序列中的下一个标记。在推理过程中,模型会收到一些文本,其任务是…
Transformers 流程:NLP 任务的综合指南
深入探讨那行代码,它可以将成千上万的现成 AI 解决方案带入你的脚本,并充分利用 🤗 Transformers 库的强大功能。
·发表于Towards Data Science ·19 分钟阅读·2024 年 2 月 16 日
--

摄影:由Simon Kadula提供,图片来自Unsplash
介绍
不同形式和方式下使用的人类语言可以产生大量信息,但这种信息往往是非结构化的。人类天生就有交流和表达自己意见与观点的需求,尤其在当今这个拥有多种渠道进行表达的时代。这导致了大量非结构化数据的产生,而这些数据迄今为止在商业上仍未得到充分或有效的利用。
然而,近年来,发生了显著的变化。
人工智能(AI)领域的快速发展,特别是在自然语言处理(NLP)领域,使我们能够以编程方式理解并与这些信息进行互动,促使许多企业重新审视这一知识来源,并将其作为新产品的动力。
这一变化的催化剂是 ChatGPT 的发布,它向世界展示了变换器模型的有效性,并且普及了大规模语言模型(LLM)这一领域。
数据质量转型:自动化 SQL 测试以实现更快速、更智能的分析
如何测试 SQL 和结果数据集的质量,以回答业务问题并增加客户信任
·发表于Towards Data Science ·阅读时长 11 分钟·2024 年 10 月 26 日
--

图片由Caspar Camille Rubin提供,来自Unsplash
在软件开发中,有许多自动化测试工具和框架可以依赖。但是对于分析团队来说,手动测试和数据质量保证(QA)仍然是常态。许多时候,是客户或业务团队首先发现数据质量或完整性的问题,而不是分析团队。
这就是自动化能够产生巨大影响的地方。通过建立一个自动化系统,使用脚本在大规模上运行数据质量测试,你可以保持工作高效而不牺牲数据的准确性或完整性。
当然,当业务问题模糊或开放性时,情况会变得更加复杂。在这种情况下,规则基础逻辑与大语言模型(LLM)的结合可以提供极大帮助——让你生成场景并运行自动化检查。在本教程中,我们将展示如何构建一个自动化测试系统,用来评估和评分数据和 SQL 查询的质量,即使业务问题是以简单的英文表达的。
在我们开始之前,你需要具备的知识
要跟随本教程,请确保你具备以下内容:
-
对数据库和 SQL 有扎实的理解
-
有使用 Python 进行 API 调用和数据处理的经验
-
访问 GPT-4 API 令牌
-
用于测试的业务问题数据集
设计系统架构
为了构建一个自动化 QA 系统来评估 SQL 查询,架构必须集成基于规则的逻辑、LLM 验证和自动评分。这个设置非常适合处理那些开放式的业务问题,帮助您将测试从手动过程扩展到自动化。
关键组件包括:
-
查询引擎:接收并执行 SQL 查询的地方。
-
评估模块:结合静态规则与基于 LLM 的验证结果。
-
评分系统:根据不同用户角色(如数据科学家、商业领袖和最终用户)对结果进行评分。
该架构包括一个反馈回路,记录问题类型——例如缺失数据、错误粒度或性能缓慢等。这些信息将存储在一个集中式数据库中,以便您可以随时间持续优化系统。我们将使用 Python 进行脚本编写,使用 SQL 跟踪后端问题,使用 OpenAI 的 LLM 解读自然语言输入。通过定期安排这些测试的运行,您将保持一致的数据质量和可扩展性,同时微调查询性能,以与商业目标对齐。
下图展示了数据如何流经系统——从 SQL 摄取到自动化测试、评分和问题跟踪——以便在大规模下保持高数据质量。
最终,这个系统不仅仅是发现错误——它推动持续改进,并使您的技术执行与业务目标保持一致。

作者提供的图像:用来说明技术架构的图示
教程
第 1 步:准备测试问题与答案数据集
要开始,收集内部团队或客户经常向分析团队提出的真实业务问题。许多问题可能是临时的数据请求,因此通过准备多种问题,您可以确保测试具有相关性。以下是一些例子,帮助您入手:
-
问题 #1:“我们的 Pro Plan 用户中有多少是从试用版转化的?”
-
问题 #2:“2024 年 6 月我们吸引了多少新用户?”
-
问题 #3:“目前哪些产品正在流行?”
-
问题 #4:“我们最畅销的产品目前的销售量是多少?”
第 2 步:构建您的评估与评分标准
2a:定义您的评分员
为了进行彻底的测试,从不同角度设置评分员,以确保覆盖所有方面:
-
最终用户:关注可用性和清晰度。结果是否易于解读?是否直接解决了原始的商业问题?
-
数据科学家:评估技术准确性和完整性。所有必要的数据集是否已包含?分析是否详细且可重复?
-
商业领袖:关注与战略目标的对齐。输出是否支持与业务目标一致的决策?
2b:定义评分标准
每个评分员应根据特定因素来评估查询:
-
准确性:查询是否提供正确的答案?是否有数据点缺失或误解?
-
相关性:输出是否包含所有必要的数据,并排除了无关信息?
-
逻辑:查询是否结构良好?连接、过滤和聚合是否正确应用?
-
效率:查询是否在没有额外复杂性或延迟的情况下进行了性能优化?
2c:跟踪和记录问题类型
为了涵盖所有方面,记录查询执行过程中常见的问题非常重要。这样可以更容易地标记并运行自动化评估。
-
错误粒度:数据返回的细节级别不正确。
-
过多列:结果中包含不必要的字段,造成杂乱。
-
缺失数据:输出中缺少关键数据。
-
错误值:计算或数值错误。
-
性能问题:查询运行效率低,执行时间过长。
import openai
import json
# Set your OpenAI API key here
openai.api_key = 'your-openai-api-key'
def evaluate_sql_query(question, query, results):
# Define the prompt with placeholders for question, query, and results
prompt = f"""
As an external observer, evaluate the SQL query and results against the client's question. Provide an assessment from three perspectives:
1\. End User
2\. Data Scientist
3\. Business Leader
For each role, provide:
1\. **Overall Score** (0-10)
2\. **Criteria Scores** (0-10):
- Accuracy: How well does it meet the question?
- Relevance: Is all needed data included, and is irrelevant data excluded?
- Logic: Does the query make sense?
- Efficiency: Is it concise and free of unnecessary complexity?
3\. **Issue Tags** (2D array: ['tag', 'details']):
- Examples: Wrong Granularity, Excessive Columns, Missing Data, Incorrect Values, Wrong Filters, Performance Issues.
4\. **Other Observations** (2D array: ['tag', 'details'])
Client Question:
{question}
SQL Query:
{query}
SQL Results:
{results}
Respond ONLY in this format:
```json
{{
"endUser": {{"overallScore": "", "criteriaScores": {{"accuracy": "", "relevance": "", "logic": "", "efficiency": ""}}, "issueTags": [], "otherObservations": []}},
"dataScientist": {{"overallScore": "", "criteriaScores": {{"accuracy": "", "relevance": "", "logic": "", "efficiency": ""}}, "issueTags": [], "otherObservations": []}},
"businessLeader": {{"overallScore": "", "criteriaScores": {{"accuracy": "", "relevance": "", "logic": "", "efficiency": ""}}, "issueTags": [], "otherObservations": []}}
}}
```py
"""
# Call the OpenAI API with the prompt
response = openai.Completion.create(
engine="gpt-4", # or whichever model you're using
prompt=prompt,
max_tokens=500, # Adjust token size based on expected response length
temperature=0 # Set temperature to 0 for more deterministic results
)
# Parse and return the result
return json.loads(response['choices'][0]['text'])
# Example usage
question = "How many Pro Plan users converted from trial?"
query = "SELECT COUNT(*) FROM users WHERE plan = 'Pro' AND status = 'Converted' AND source = 'Trial';"
results = "250"
evaluation = evaluate_sql_query(question, query, results)
print(json.dumps(evaluation, indent=4))
第 3 步:自动化测试
3a:遍历问题
一旦收集了业务问题,设置一个循环,将每个问题、相关的 SQL 查询以及结果传递到评估函数中。这让你能够自动化整个评估过程,确保每个查询都得到一致的评分。
3b:安排定期运行
通过定期调度脚本运行来自动化测试过程——理想情况下,在每次数据刷新或查询更新后运行。这保持测试与数据同步,能够在问题出现时及时捕捉。
3c:在数据库中记录得分、标签和观察结果
对于每次测试运行,在结构化数据库中记录所有得分、问题标签和观察结果。使用 Python 脚本将相关数据填充到表格(例如,issue_catalog)中。这为你提供了评估历史记录,以便跟踪趋势、找出常见问题,并优化未来的测试。
第 4 步:报告测试结果
4a:按得分进行透视和分组
利用 SQL 查询或 BI 工具创建透视表,根据总体得分和具体标准(如准确性、相关性、逻辑性和效率)对结果进行分组。这有助于你发现性能趋势,找出哪些方面需要更多关注。
为了计算每个查询在所有评分员中的总体得分,可以使用加权公式:
总得分 = w1×准确性 + w2×相关性 + w3×逻辑性 + w4×效率
其中 w1、w2、w3、w4 是分配给每个评分标准的权重。这些权重的总和应为 1,以便进行归一化。
例如,你可以根据数据科学家的优先级赋予准确性更高的权重,而根据业务领导的优先级赋予相关性更高的权重。
4b: 突出显示主要问题
确定最频繁和最关键的问题——比如缺失数据、错误的粒度或性能低效。提供一份详细报告,列出这些问题发生的频率以及哪些类型的查询最受影响。
专注于如果不加以解决可能会导致更大错误的模式。例如,突出显示可能因为数据质量问题而扭曲决策或减缓业务流程的情况。
优先处理需要立即采取行动的问题,例如那些影响查询性能或关键数据集准确性的问题,并列出明确的后续步骤以解决它们。

作者提供的图像:使用示例测试数据创建的图表
4c: 分析评分者的方差
仔细查看不同评分者(终端用户、数据科学家、业务领导)评分之间的差异。较大的差异可能揭示出技术执行和业务目标之间的潜在错位。
例如,如果某个查询在技术准确性上得分较高,但在与业务问题的相关性上得分较低,这表明在将数据洞察转化为可操作结果时存在差距。类似地,如果终端用户认为结果难以理解,而数据科学家认为它们在技术上是正确的,这可能指向沟通或呈现方面的问题。
通过追踪这些差异,你可以更好地将分析过程与技术精度和商业价值对齐,从而保持所有利益相关者的满意。
为了量化这种差异,你可以计算评分者评分的方差。首先,定义各个评分为:
-
S-EndUser: 终端用户的总体评分。
-
S-DataScientist: 数据科学家的总体评分。
-
S-BusinessLeader:来自业务领导的总体评分。
三个评分者的平均分 μ 可以按如下公式计算:
μ = (S-EndUser + S-DataScientist + S-BusinessLeader) / 3
接下来,计算方差 σ²,这是每个评分者的评分与平均评分之间的平方差的平均值。方差的公式是:
σ² = (S-EndUser − μ)² + (S-DataScientist − μ)² + (S-BusinessLeader − μ)² / 3
通过计算这种方差,你可以客观地衡量评分者的评分差异。
大的差异表明一个或多个评分者对查询质量或相关性的看法不同,这可能意味着技术输出与业务需求之间需要更好的对齐。
步骤 5:创建反馈循环
5a: 确定关键问题
在整个测试过程中,你很可能会发现一些问题反复出现。它们可能包括缺失的数据、错误的值、错误的粒度或性能低效等问题。重要的是不仅要记录这些问题,还要对它们进行分类和优先级排序。
例如,如果缺少关键数据,应该立即解决,而性能优化可以视为长期的优化任务。通过专注于最有影响力和重复出现的问题,你将能够有效改善数据质量并更好地解决根本原因。
5b:优化你的 SQL 查询
现在,你已经识别出重复出现的问题,是时候更新 SQL 查询来解决它们了。这涉及到优化查询逻辑,以实现准确的连接、筛选和聚合。例如:
-
如果遇到错误的粒度,调整查询,以适当的级别聚合数据。
-
对于缺失的数据,确保所有相关的表正确连接。
-
如果出现性能问题,简化查询、添加索引或使用更高效的 SQL 函数。
目标是在此过程中,将你记录的反馈转化为 SQL 代码的实际改进,使你未来的查询更加精准、相关且高效。
5c:重新测试以验证结果
一旦你的查询得到优化,重新运行测试以验证改进效果。自动化这个步骤确保你更新后的查询始终与新的数据或业务问题进行评估。再次运行测试可以确认你的修改已解决问题并提高了整体数据质量。它还可以帮助确认你的 SQL 查询完全符合业务需求,从而能够更快速和准确地获得洞察。如果出现新的问题,只需将其反馈到循环中,持续改进。
自动化反馈循环的示例代码
为了自动化这个反馈循环,这里有一个 Python 脚本,可以处理多个测试用例(包括业务问题、SQL 查询和结果),通过 OpenAI 的 API 评估这些用例,并将结果存储到数据库中:
for question, query, results in test_cases:
# Call the OpenAI API to evaluate the SQL query and results
response = openai.Completion.create(
engine="text-davinci-003", # Replace with GPT-4 or relevant engine
prompt=prompt.format(question=question, query=query, results=results),
max_tokens=1000
)
# Process and store the response
process_response(response)
def store_results_in_db(test_run_id, question, role, scores, issue_tags, observations):
# SQL insert query to store evaluation results in the issue catalog
insert_query = """
INSERT INTO issue_catalog
(test_run_id, question, role, overall_score, accuracy_score, relevance_score, logic_score, efficiency_score, issue_tags, other_observations)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s);
"""
db_cursor.execute(insert_query, (
test_run_id, question, role, scores['overall'], scores['accuracy'], scores['relevance'],
scores['logic'], scores['efficiency'], json.dumps(issue_tags), json.dumps(observations)
))
db_conn.commit()
设置问题目录表
issue_catalog 表作为存储详细测试结果的主要库,帮助你清晰地跟踪查询性能并标记长期存在的问题。通过使用 JSONB 格式存储问题标签和观察结果,你可以获得更大的灵活性,允许你记录复杂信息,而无需频繁更新数据库架构。以下是设置的 SQL 代码:
CREATE TABLE issue_catalog (
id SERIAL PRIMARY KEY,
test_run_id INT NOT NULL,
question TEXT NOT NULL,
role TEXT NOT NULL, -- e.g., endUser, dataScientist, businessLeader
overall_score INT NOT NULL,
accuracy_score INT NOT NULL,
relevance_score INT NOT NULL,
logic_score INT NOT NULL,
efficiency_score INT NOT NULL,
issue_tags JSONB, -- Storing issue tags as JSON for flexibility
other_observations JSONB, -- Storing other observations as JSON
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
这个反馈循环的目的
-
持续改进:通过跟踪问题,随着时间的推移,你将能够细化 SQL 查询,并稳步提高其质量。每次测试运行都会提供可操作的洞察,通过针对最常见的问题,你的系统会随着每次运行变得更高效和更具弹性。
-
数据质量保证:定期对更新的 SQL 查询进行测试,帮助你验证它们是否正确处理新数据和测试用例。这个持续的过程展示了你的调整是否真正提高了数据质量,并保持与业务需求的一致性,从而降低未来问题的风险。
-
与业务需求的一致性:根据提出问题的人进行排序——无论是终端用户、数据科学家还是业务领导者——都能让你专注于对技术准确性和业务相关性都有重要意义的改进。随着时间的推移,这将建立一个技术努力直接支持有意义的业务洞察的系统。
-
可扩展的测试和优化:这种方法在增加更多测试用例时能够平滑扩展。随着问题目录的扩展,模式逐渐显现,使得微调影响广泛业务问题的查询变得更加容易。随着每次迭代,你的测试框架变得更强大,推动数据质量在大规模上的持续改善。
总结
自动化 SQL 测试是分析团队的游戏规则改变者,它帮助他们早期发现数据问题并精确地解决它们。通过建立一个结合规则逻辑和大语言模型(LLM)的结构化反馈循环,你可以扩展测试,处理即使是最复杂的业务问题。
这种方法不仅提高了数据的准确性,而且使你的洞察与业务目标保持一致。分析的未来依赖于自动化与洞察之间的平衡——你准备好迈出这一步吗?
将下一个标记预测转化为 LLM 分类任务
从标记到标签:使用大型语言模型进行分类
·发布于Towards Data Science ·阅读时间 6 分钟·2024 年 6 月 20 日
--

图片来源:Myles Bloomfield 在Unsplash
免费链接! 感谢,祝您阅读愉快。😃
大型语言模型(LLMs)通过在大量互联网数据上进行训练,展现出多功能性,可以执行各种自然语言任务。其中一个常见应用是分类,这是一个将主题分为预定义标签的监督学习任务。零-shot和少-shot 分类已经成为流行技术,使得 LLMs 能够在没有训练数据或仅有少量示例的情况下执行分类任务。然而,研究表明,为了提高准确性,指令微调能够通过使用精心挑选的数据集对 LLMs 进行调优,从而提高其性能。
指令微调大型语言模型(LLMs)
常见的指令微调方法是构建一个包含问答对的数据集。然后,使用这些数据对预训练的 LLM 进行监督式的进一步微调。
你可以查看我之前的文章了解这种方法。
翻译回忆录:一段技术旅程
利用 GPT-3.5 和非结构化 API 进行翻译
·发表于《Towards Data Science》 ·阅读时长:6 分钟·2024 年 12 月 11 日
--
这篇博客文章详细介绍了我如何利用 GPT 翻译我家族朋友的个人回忆录,使其能够被更广泛的读者群体阅读。 具体来说,我使用了 GPT-3.5 进行翻译,并通过 Unstructured 的 API 实现高效的内容提取和格式化。
这本回忆录由我的家族朋友卡门·罗莎(Carmen Rosa)撰写,真挚地讲述了她在玻利维亚的成长经历以及在充满活力的 1970 年代,与一位伊朗男子在巴黎的浪漫旅程。原文为西班牙语,我们的目标是通过应用 LLM 技术,在保留她叙事精髓的同时,扩大其在英语读者中的影响力。

《Un Destino Sorprendente》封面图,作者卡门·罗莎·维赫滕达尔(Carmen Rosa Wichtendahl)授权使用。
以下是您可以详细阅读翻译过程的内容,或者您可以点击此处访问 Colab Notebook。
翻译文档
我在翻译这本书时遵循了以下步骤:
-
导入图书数据: 我通过 Unstructured API 将书籍从 Docx 文档中导入,并将其分为章节和段落。
-
翻译技术: 我使用 GPT-3.5 翻译了每一章。在每个段落中,我提供了该章最新的三句翻译(如果有的话)。这种方法有两个目的:
-
风格一致性: 通过提供先前翻译的上下文,保持整个翻译过程中的风格一致性。
-
令牌限制: 限制一次处理的令牌数,以避免超出模型的上下文限制。
3. 将翻译导出为 Docx: 我再次使用 Unstructured 的 API 将翻译内容保存为 Docx 格式。
技术实现
1. 库
我们将从安装和导入必要的库开始。
pip install --upgrade openai
pip install python-dotenv
pip install unstructured
pip install python-docx
import openai
# Unstructured
from unstructured.partition.docx import partition_docx
from unstructured.cleaners.core import group_broken_paragraphs
# Data and other libraries
import pandas as pd
import re
from typing import List, Dict
import os
from dotenv import load_dotenv
2. 连接到 OpenAI 的 API
以下代码设置了用于 Python 项目的 OpenAI API 密钥。你需要将 API 密钥保存在 .env 文件中。
import openai
# Specify the path to the .env file
dotenv_path = '/content/.env'
_ = load_dotenv(dotenv_path) # read local .env file
openai.api_key = os.environ['OPENAI_API_KEY']
3. 加载书籍
代码允许我们导入 Docx 格式的书籍,并将其分成单独的段落。
elements = partition_docx(
filename="/content/libro.docx",
paragraph_grouper=group_broken_paragraphs
)
以下代码返回 elements 中第 10 个索引的段落。
print(elements[10])
# Returns: Destino sorprendente, es el título que la autora le puso ...
4. 将书籍分组为标题和章节
下一步是创建章节列表。每个章节将作为一个字典,其中包含标题和段落列表。这个结构简化了逐章逐段翻译的过程。以下是这种格式的示例:
[
{"title": title 1, "content": [paragraph 1, paragraph 2, ..., paragraph n]},
{"title": title 2, "content": [paragraph 1, paragraph 2, ..., paragraph n]},
...
{"title": title n, "content": [paragraph 1, paragraph 2, ..., paragraph n]},
]
为了实现这一点,我们将创建一个名为 group_by_chapter 的函数。以下是涉及的关键步骤:
-
提取相关信息: 我们可以通过调用
element.category. 来获取每个叙述文本和标题。这些是我们目前只关注翻译的类别。 -
识别叙述性标题: 我们识别出某些标题应作为叙述文本的一部分。为此,我们假设斜体标题属于叙述段落的一部分。
def group_by_chapter(elements: List) -> List[Dict]:
chapters = []
current_title = None
for element in elements:
text_style = element.metadata.emphasized_text_tags # checks if it is 'b' or 'i' and returns list
unique_text_style = list(set(text_style)) if text_style is not None else None
# we consider an element a title if it is a title category and the style is bold
is_title = (element.category == "Title") & (unique_text_style == ['b'])
# we consider an element a narrative content if it is a narrative text category or
# if it is a title category, but it is italic or italic and bold
is_narrative = (element.category == "NarrativeText") | (
((element.category == "Title") & (unique_text_style is None)) |
((element.category == "Title") & (unique_text_style == ['i'])) |
((element.category == "Title") & (unique_text_style == ['b', 'i']))
)
# for new titles
if is_title:
print(f"Adding title {element.text}")
# Add previous chapter when a new one comes in, unless current title is None
if current_title is not None:
chapters.append(current_chapter)
current_title = element.text
current_chapter = {"title": current_title, "content": []}
elif is_narrative:
print(f"Adding Narrative {element.text}")
current_chapter["content"].append(element.text)
else:
print(f'### No need to convert. Element type: {element.category}')
return chapters
在下面的示例中,我们可以看到一个示例:
book_chapters[2]
# Returns
{'title': 'Proemio',
'content': [
'La autobiografía es considerada ...',
'Dentro de las artes literarias, ...',
'Se encuentra más próxima a los, ...',
]
}
5. 书籍翻译
为了翻译这本书,我们遵循以下步骤:
-
翻译章节标题: 我们翻译每个章节的标题。
-
翻译段落: 我们翻译每个段落,给模型提供最新的三个已翻译的句子作为上下文。
-
保存翻译: 我们保存翻译后的标题和内容。
以下函数自动化了这个过程。
def translate_book(book_chapters: List[Dict]) -> Dict:
translated_book = []
for chapter in book_chapters:
print(f"Translating following chapter: {chapter['title']}.")
translated_title = translate_title(chapter['title'])
translated_chapter_content = translate_chapter(chapter['content'])
translated_book.append({
"title": translated_title,
"content": translated_chapter_content
})
return translated_book
对于标题,我们要求 GPT 做一个简单的翻译,如下所示:
def translate_title(title: str) -> str:
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages= [{
"role": "system",
"content": f"Translate the following book title into English:\n{title}"
}]
)
return response.choices[0].message.content
为了翻译单个章节,我们提供相应的段落给模型。我们指导模型如下操作:
-
识别角色: 我们告诉模型它是一本书的有用翻译者。
-
提供上下文: 我们分享该章节中最新的三个已翻译句子。
-
请求翻译: 我们要求模型翻译下一个段落。
在此过程中,函数将所有已翻译的段落合并为一个字符串。
# Function to translate a chapter using OpenAI API
def translate_chapter(chapter_paragraphs: List[str]) -> str:
translated_content = ""
for i, paragraph in enumerate(chapter_paragraphs):
print(f"Translating paragraph {i + 1} out of {len(chapter_paragraphs)}")
# Builds the message dynamically based on whether there is previous translated content
messages = [{
"role": "system",
"content": "You are a helpful translator for a book."
}]
if translated_content:
latest_content = get_last_three_sentences(translated_content)
messages.append(
{
"role": "system",
"content": f"This is the latest text from the book that you've translated from Spanish into English:\n{latest_content}"
}
)
# Adds the user message for the current paragraph
messages.append(
{
"role": "user",
"content": f"Translate the following text from the book into English:\n{paragraph}"
}
)
# Calls the API
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=messages
)
# Extracts the translated content and appends it
paragraph_translation = response.choices[0].message.content
translated_content += paragraph_translation + '\n\n'
return translated_content
最后,下面是获取最新三个句子的辅助函数。
def get_last_three_sentences(paragraph: str) -> str:
# Use regex to split the text into sentences
sentences = re.split(r'(?<!\w\.\w.)(?<![A-Z][a-z]\.)(?<=\.|\?)\s', paragraph)
# Get the last three sentences (or fewer if the paragraph has less than 3 sentences)
last_three = sentences[-3:]
# Join the sentences into a single string
return ' '.join(last_three)
6. 书籍导出
最后,我们将章节的字典传递给一个函数,该函数将每个标题添加为标题,并将每个内容添加为段落。在每个段落后,插入分页符以分隔章节。最终文档将本地保存为 Docx 文件。
from docx import Document
def create_docx_from_chapters(chapters: Dict, output_filename: str) -> None:
doc = Document()
for chapter in chapters:
# Add chapter title as Heading 1
doc.add_heading(chapter['title'], level=1)
# Add chapter content as normal text
doc.add_paragraph(chapter['content'])
# Add a page break after each chapter
doc.add_page_break()
# Save the document
doc.save(output_filename)
限制
尽管使用 GPT 和 API 进行翻译快速高效,但与人工翻译相比,仍有一些关键限制:
-
代词和指代错误: GPT 确实在少数情况下误解了代词或指代,可能将某些行为或陈述错误地归因于叙事中的错误人物。人类翻译者可以更好地解决这些模糊性问题。
-
文化背景: GPT 未能准确捕捉一些微妙的文化参考和习语,这些是人类翻译者能够更精确地解读的。在这种情况下,几个独特于玻利维亚圣克鲁斯的俚语被保留在原语言中,未做额外的解释或上下文说明。
结合人工智能与人工审核可以平衡速度和质量,确保翻译既准确又真实。
结论
本项目展示了通过结合 GPT-3 和 Unstructured APIs 来翻译书籍的一种方法。通过自动化翻译过程,我们显著减少了所需的人工努力。尽管初始翻译输出可能需要进行一些小的人工修订,以精细化细微差别并确保最高质量,但这种方法为高效且有效的书籍翻译提供了坚实的基础。
如果您对如何改进此过程或翻译质量有任何反馈或建议,请随时在下面的评论中分享。
附录
-
链接到Colab 笔记本
-
链接到原语言书籍(西班牙语)
-
链接到翻译后的书籍(英文)
被困在网络中:图形的基础模型在哪里?
|LLM|变换器|基础模型|图|网络|
与其他模态断开连接的图形,等待它们的人工智能革命:它会到来吗?
·发表于 Towards Data Science ·阅读时间:11 分钟·2024 年 11 月 25 日
--

图片由作者使用 DALL-E 创建
“如果基础牢固,其他一切都会随之而来。” – 未知
“建筑越高,基础必须越深。” – 托马斯·阿·肯皮斯
基础模型近年来改变了人工智能。 基础模型是通过大量数据(通常是无监督学习)训练出来的模型,可以适应不同的任务。像BERT或GPT这样的模型引发了一场革命,其中一个模型可以适应一个领域中的所有任务,从而简化了人工智能的使用,并减少了单一任务所需的数据。我们已经有了用于文本和其他模态的基础模型,但对于图形和表格数据这样的模态,目前还没有。在本文中,我们将讨论为什么我们没有图形的基础模型,以及我们如何获得一个,具体来说,我们将回答以下问题:
- 为什么我们需要图形的基础模型?为什么我们没有图形的基础模型?
2024 年将塑造现代数据架构的趋势
生成性 AI、数据网格、监管和可观察性。2024 年将是充满乐趣的一年!
·发表于Towards Data Science ·阅读时间 6 分钟·2024 年 1 月 10 日
--

来源:www.istockphoto.com/fr Andrey Suslov
2023 年将永远被铭记为生成性人工智能的年份。在这个数字时代,你几乎找不到一个拥有互联网接入的人没有听说过 ChatGPT。如果你足够长时间地见证了几次技术周期,你就知道我们正进入一个相当具有变革性的周期。根据麦肯锡的调查,自 2017 年以来,人工智能的应用已经翻了一番。尽管最近的进展无疑会加速这种应用,但我倾向于相信我们在 2024 年看到的成果仍然相对较小,因为大多数公司仍在努力弄清楚如何将数据战略与业务目标对接,同时应对日益增加的监管审查。就数据行业而言,人工智能的应用将通过让数据和数据基础设施更易于组织内的广泛用户访问,推动数据的进一步采用,从而为更多的人工智能项目提供支持。数据的安全民主化将是一个重要话题;我们将看到数据网格的更多实际应用,以及更多对安全、隐私和可观察性的投资。
本文的目的不是做出关于 AI 如何改变我们所知的数据行业的大胆声明,而是希望揭示一些我们可能会看到企业持续投资的领域,以及围绕数据与 AI 成为自我实现的预言所激发的热情。
AI 将被投入工作,并且会颠覆我们所知的现代数据栈。
当然,我们从这里开始。不可否认的是,LLMs 已经完全改变了我们对技术的思考方式,数据与分析领域也不例外。就现代数据栈而言,以下是 LLMs 将会改变游戏规则的一些领域:
数据 分析:将 AI 引入分析工作流将提高自动化、效率和可访问性。
-
自动化:AI 可以用于自动化繁琐的任务,如数据收集、准备和清理,并减少手动错误的可能性。
-
效率:使用更复杂的预测模型将使公司能够预测未来趋势,提高预测的准确性。AI 算法可以用于识别和研究客户行为,从而提供高度个性化的产品推荐和更具针对性的营销活动。
-
可访问性:AI 将帮助 AI 的普及。自然语言处理(NLP)可以被用来使 AI 驱动的数据分析更加易于访问,允许即使是最不懂技术的用户也能以对话的方式与数据进行互动。
向量数据库的崛起:大型语言模型(LLMs)需要支持快速查询和处理大量结构化和非结构化(无模式)数据的基础设施。这正是向量和向量搜索数据库的数学概念发挥作用的地方。与传统关系型数据库的行和列不同,数据在多维空间中表示,典型的数学向量表示方式。在生成 AI 应用的背景下,向量数据库允许对向量化数据进行快速处理和查询。更多信息请见这里和这里。
“[把向量数据库想象成一个庞大的仓库,人工智能则是熟练的仓库经理。在这个仓库里,每个物品(数据)都被存储在一个箱子(向量)里,整齐地组织在多维空间的货架上,]”正如Mark Hinkle 在《The New Stack》中所述。
“机器学习管道”
在传统的数据工程中,数据管道是将数据从源头传输到目标的过程,通常是为了通过商业智能(BI)使其对企业可访问,以便进行报告和分析。机器学习管道与传统数据管道类似,都是数据传输的过程;然而,它的主要目的是支持机器学习模型的开发和部署,与数据管道不同,机器学习管道并不是一条“直线”——关于数据管道和机器学习管道的差异,请参阅这里和这里。
成功的机器学习(ML)、人工智能(AI)和数据科学项目需要强大的基础设施,以便构建、测试、训练、优化和维护模型的准确性。这个过程始于结构良好的机器学习管道。

来源: https://www.forepaas.com/en/blog/data-pipelines-ai-pipelines-2/
隐私,请。
不可否认的是,数据使用量以及公司对数据和平台民主化的需求将在 2024 年继续大幅增长。也就是说,随着数据和人工智能的监管越来越严格,个人数据保护政策的审查将加剧。关于未来 12 个月人工智能监管的预期总结请见这里。
BYODM: 自带数据网格
自从 2019 年由其创始人 Zhamak 首次提出以来,数据网格经历了无数次辩论和相当多的怀疑。四年后,出现了几种实现方式和变体,其中公司采纳了这一概念的原则,并将其应用于自己的架构。去中心化、面向领域的设计、IaaS、数据即产品以及端到端的联合治理都是组织应当采纳的原则,以创建和促进一个无孤岛的、民主化的数据环境。然而,从传统的单体结构转向完整的数据网格并不容易,且需要进行重大的文化和组织变革。这就是为什么渐进式采纳,允许慢慢引入这一概念并证明其价值,同时对齐现有和未来的技术与商业考量,是过去几年中最有效的方式。
最终,需要记住的是,数据网格是一种架构和组织上的转变,而不是技术解决方案。我认为 BYODM 方法将在 2024 年占据主导地位。
数据与 AI 可观察性
我在这里可能有偏见。但话说回来,在每个组织都在思考大语言模型(LLM)潜力的世界里,支持数据与 AI 可观察性的观点是很难反驳的。
“没有数据战略,就没有 AI 战略。我们所追求的智能最终都体现在数据上。” ——Frank Slootman
在过去几年里,数据可观察性已成为每个现代组织数据战略的关键组成部分。如果你对这一概念不熟悉,我建议你从这里或这里开始了解。不可否认,AI 也将重塑数据可观察性领域。采用 AI 代理并使用自然语言处理(NLP)将提高平台解决方案的自动化和包容性,从而推动其采纳。我们所知的数据可观察性概念将发展,以捕捉 AI 在可观察性中的潜力,并覆盖更多的 AI 应用场景。
市场上大多数现有解决方案已经涵盖了将成为数据与 AI 可观察性的部分内容。如果你将数据科学视为数据消费的应用场景,监控进入模型训练的数据在大多数框架下已经有所涵盖。数据与 AI 可观察性的未来将发展为包括对机器学习(ML)模型行为、输出和性能的洞察。就像今天的数据管道一样,数据可观察性平台将包括关于 ML 管道的可操作性洞察,以实现有效的异常检测、根本原因分析和事件管理,并为 ML 产品部署带来可靠性和效率。
结论
2024 年是闰年,这意味着我们有 366 次机会去做更多的事情并创造数据创新。尽管 2023 年将永远被铭记为生成 AI 的元年,但 2024 年将是我们开始看到组织朝着数据与 AI 成熟度迈进的时刻。但要做好 AI,一个深思熟虑的数据战略至关重要。现代数据堆栈是一个不断发展的领域,2024 年我们将看到更多由 AI 日益普及所带来的创新。随着企业在 2024 年更多地试验 AI,治理和可观察性将成为核心,以确保顺利高效的部署。
TSMixer:Google 创新的深度学习预测模型
结合轻量设计与高预测精度
·发布于Towards Data Science ·阅读时长 8 分钟·2024 年 12 月 12 日
--

由 DALLE*3 创建
许多深度学习预测模型倾向于跟随 AI 趋势。
但 TSMixer 不同。它是 Google 在近一年前发布的,凭借这些优势,它在传统机器学习模型之外获得了显著的关注,并被广泛使用:
-
轻量级: 它基于 MLP 的架构能够高效地捕捉时间维度和特征维度中的模式。
-
多功能: TSMixer-Ext变体可以处理历史数据、未来已知输入和静态外生变量。
-
多通道建模: 它利用跨变量信息——其特征混合 MLP 可以联合学习协变量之间的相互依赖关系。
-
优越的长期预测能力: 能够处理更长的上下文,能够在预测期达到 720 个数据点,基准测试结果表明其表现优异。
这些特点使得 TSMixer 成为需求规划、零售和金融市场等领域的多功能选择。
让我们开始吧!
✅ 在我的AI 项目文件夹中找到适用于 TSMixer 的实操项目,和其他酷炫的项目!随时欢迎订阅!
Pandas 中的 TSV:操作指南
使用 Pandas 正确加载和写入 TSV 文件的方法
·发表于Towards Data Science ·阅读时长 5 分钟·2024 年 10 月 1 日
--

图片由Mika Baumeister提供,来源于Unsplash
介绍
TSV 是一种广泛使用的存储表格数据的格式,但在处理文本数据和使用 Pandas 库时,可能会引起混淆。混淆的原因有两个:
-
TSV 与 CSV(广泛使用的存储数据格式)非常相似,但并不完全相同。
-
Pandas 的默认设置与 TSV 格式不兼容。
在本文中,我简要讨论了产生混淆的原因,并展示了使用 Pandas 库处理 TSV 格式的最佳方法。
TSV 格式
TSV[1]是一种简单的文件格式,类似于 CSV。然而,它有几个重要的区别:
-
它使用制表符来分隔字段。
-
它不允许某些字符,即换行符(
\n)、制表符(\t)和回车符(\r)出现在字段内。 -
字段没有引用,也没有特殊字符的转义[2](至少对于原始格式而言)。
第 2 点在处理文本字段时是一个问题,因为它们可能包含禁用字符。建议的处理禁用字符的方法是将它们替换为任意文本,例如……
调整:使用 scikit-learn 的 TunedThresholdClassifierCV 进行决策阈值优化
使用案例和代码来探索新类,帮助调整 scikit-learn 中的决策阈值
·发表于 Towards Data Science ·阅读时间 10 分钟·2024 年 5 月 27 日
--
scikit-learn 1.5 版本引入了一个新类,TunedThresholdClassifierCV,使得优化来自 scikit-learn 分类器的决策阈值变得更加容易。决策阈值是一个临界点,它将机器学习模型输出的预测概率转换为离散类别。对于二分类任务,scikit-learn 分类器的 .predict() 方法的默认决策阈值是 0.5。虽然这是一个合理的默认值,但它很少是分类任务的最佳选择。
本文介绍了 TunedThresholdClassifierCV 类,并展示了它如何优化各种二分类任务的决策阈值。这个新类将帮助填补构建模型的数据科学家和根据模型输出做出决策的业务利益相关者之间的鸿沟。通过微调决策阈值,数据科学家可以提高模型性能,并更好地与业务目标对齐。
本文将涵盖以下几种调整决策阈值有益的情况:
-
最大化指标:当选择一个阈值以最大化评分指标时,使用此方法,例如 F1 分数。
-
成本敏感学习:当误分类的假阳性成本与误分类的假阴性成本不相等,并且你有成本估计时,调整阈值。
-
在约束下调优:在 ROC 或精确度-召回曲线中优化操作点,以满足特定的性能约束。
本文中使用的代码及数据集链接可以在GitHub上找到。
让我们开始吧!首先,导入必要的库,读取数据,并划分训练集和测试集。
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.compose import make_column_selector as selector
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (
RocCurveDisplay,
f1_score,
make_scorer,
recall_score,
roc_curve,
confusion_matrix,
)
from sklearn.model_selection import TunedThresholdClassifierCV, train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
RANDOM_STATE = 26120
最大化一个指标
在任何机器学习项目中开始构建模型之前,与利益相关者一起确定需要优化的指标非常重要。提前做出这个决策能确保项目与其预期目标一致。
在欺诈检测应用中使用准确率作为评估模型性能的指标并不理想,因为数据通常是不平衡的,大多数交易是非欺诈性的。F1 得分是精确度和召回率的调和平均值,对于像欺诈检测这样的不平衡数据集来说,是一个更好的指标。让我们使用TunedThresholdClassifierCV类来优化逻辑回归模型的决策阈值,以最大化 F1 得分。
我们将使用Kaggle 信用卡欺诈检测数据集来介绍我们需要调优决策阈值的第一个场景。首先,将数据划分为训练集和测试集,然后创建一个 scikit-learn 管道来缩放数据并训练一个逻辑回归模型。将管道拟合在训练数据上,以便我们可以将原始模型的表现与调优后的模型表现进行比较。
creditcard = pd.read_csv("data/creditcard.csv")
y = creditcard["Class"]
X = creditcard.drop(columns=["Class"])
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=RANDOM_STATE, stratify=y
)
# Only Time and Amount need to be scaled
original_fraud_model = make_pipeline(
ColumnTransformer(
[("scaler", StandardScaler(), ["Time", "Amount"])],
remainder="passthrough",
force_int_remainder_cols=False,
),
LogisticRegression(),
)
original_fraud_model.fit(X_train, y_train)
目前尚未进行调优,但将在下一个代码块中进行。TunedThresholdClassifierCV的参数与 scikit-learn 中的其他CV类类似,例如GridSearchCV。至少,用户只需传递原始估计器,TunedThresholdClassifierCV将使用 5 折分层 K 折交叉验证(默认)存储最大化平衡准确率的决策阈值(默认)。在调用.predict()时,它也会使用此阈值。然而,任何 scikit-learn 度量(或可调用对象)都可以作为scoring度量。此外,用户可以传递熟悉的cv参数来自定义交叉验证策略。
创建TunedThresholdClassifierCV实例并在训练数据上拟合模型。传递原始模型,并将评分设置为“f1”。我们还需要设置store_cv_results=True,以便在交叉验证期间访问评估的阈值,以便可视化。
tuned_fraud_model = TunedThresholdClassifierCV(
original_fraud_model,
scoring="f1",
store_cv_results=True,
)
tuned_fraud_model.fit(X_train, y_train)
# average F1 across folds
avg_f1_train = tuned_fraud_model.best_score_
# Compare F1 in the test set for the tuned model and the original model
f1_test = f1_score(y_test, tuned_fraud_model.predict(X_test))
f1_test_original = f1_score(y_test, original_fraud_model.predict(X_test))
print(f"Average F1 on the training set: {avg_f1_train:.3f}")
print(f"F1 on the test set: {f1_test:.3f}")
print(f"F1 on the test set (original model): {f1_test_original:.3f}")
print(f"Threshold: {tuned_fraud_model.best_threshold_: .3f}")
Average F1 on the training set: 0.784
F1 on the test set: 0.796
F1 on the test set (original model): 0.733
Threshold: 0.071
现在我们已经找到了最大化 F1 得分的阈值,可以检查tuned_fraud_model.best_score_以了解交叉验证中折叠的最佳平均 F1 得分。我们还可以使用tuned_fraud_model.best_threshold_查看生成这些结果的阈值。您可以使用objective_scores_和decision_thresholds_属性可视化交叉验证过程中不同决策阈值下的度量得分:
fig, ax = plt.subplots(figsize=(5, 5))
ax.plot(
tuned_fraud_model.cv_results_["thresholds"],
tuned_fraud_model.cv_results_["scores"],
marker="o",
linewidth=1e-3,
markersize=4,
color="#c0c0c0",
)
ax.plot(
tuned_fraud_model.best_threshold_,
tuned_fraud_model.best_score_,
"^",
markersize=10,
color="#ff6700",
label=f"Optimal cut-off point = {tuned_fraud_model.best_threshold_:.2f}",
)
ax.plot(
0.5,
f1_test_original,
label="Default threshold: 0.5",
color="#004e98",
linestyle="--",
marker="X",
markersize=10,
)
ax.legend(fontsize=8, loc="lower center")
ax.set_xlabel("Decision threshold", fontsize=10)
ax.set_ylabel("F1 score", fontsize=10)
ax.set_title("F1 score vs. Decision threshold -- Cross-validation", fontsize=12)

图片由作者创建。
# Check that the coefficients from the original model and the tuned model are the same
assert (tuned_fraud_model.estimator_[-1].coef_ ==
original_fraud_model[-1].coef_).all()
我们使用了相同的基础逻辑回归模型来评估两个不同的决策阈值。基础模型是相同的,上面的断言语句证明了系数的相等性。TunedThresholdClassifierCV中的优化是通过后处理技术实现的,这些技术直接应用于模型输出的预测概率。然而,值得注意的是,TunedThresholdClassifierCV默认使用交叉验证来确定决策阈值,以避免过拟合训练数据。
成本敏感学习
成本敏感学习是一种机器学习方法,它为每种类型的错误分类分配成本。这将模型性能转化为利益相关者能够理解的单位,比如节省的美元。
我们将使用TELCO 客户流失数据集,这是一个二分类数据集,来展示成本敏感学习的价值。目标是根据客户的基本信息、合同详情及账户的其他技术信息预测客户是否会流失。使用这个数据集(以及部分代码)的动机来源于Dan Becker 的决策阈值优化课程。
data = pd.read_excel("data/Telco_customer_churn.xlsx")
drop_cols = [
"Count", "Country", "State", "Lat Long", "Latitude", "Longitude",
"Zip Code", "Churn Value", "Churn Score", "CLTV", "Churn Reason"
]
data.drop(columns=drop_cols, inplace=True)
# Preprocess the data
data["Churn Label"] = data["Churn Label"].map({"Yes": 1, "No": 0})
data.drop(columns=["Total Charges"], inplace=True)
X_train, X_test, y_train, y_test = train_test_split(
data.drop(columns=["Churn Label"]),
data["Churn Label"],
test_size=0.2,
random_state=RANDOM_STATE,
stratify=data["Churn Label"],
)
设置一个基本的处理管道,使用随机森林模型生成预测概率。这将作为与TunedThresholdClassifierCV进行比较的基准。
preprocessor = ColumnTransformer(
transformers=[("one_hot", OneHotEncoder(),
selector(dtype_include="object"))],
remainder="passthrough",
)
original_churn_model = make_pipeline(
preprocessor, RandomForestClassifier(random_state=RANDOM_STATE)
)
original_churn_model.fit(X_train.drop(columns=["customerID"]), y_train);
对于本教程来说,预处理和模型类型的选择并不重要。公司希望向预测会流失的客户提供折扣。在与利益相关者的合作中,你了解到,给一个不会流失的客户(假阳性)提供折扣会花费 80 美元。你还了解到,向一个本来会流失的客户提供折扣价值 200 美元。你可以通过一个成本矩阵来表示这种关系:
def cost_function(y, y_pred, neg_label, pos_label):
cm = confusion_matrix(y, y_pred, labels=[neg_label, pos_label])
cost_matrix = np.array([[0, -80], [0, 200]])
return np.sum(cm * cost_matrix)
cost_scorer = make_scorer(cost_function, neg_label=0, pos_label=1)
我们还将成本函数包装在一个 scikit-learn 自定义评分器中。这个评分器将在 TunedThresholdClassifierCV 中作为scoring参数使用,并用于在测试集上评估利润。
tuned_churn_model = TunedThresholdClassifierCV(
original_churn_model,
scoring=cost_scorer,
store_cv_results=True,
)
tuned_churn_model.fit(X_train.drop(columns=["CustomerID"]), y_train)
# Calculate the profit on the test set
original_model_profit = cost_scorer(
original_churn_model, X_test.drop(columns=["CustomerID"]), y_test
)
tuned_model_profit = cost_scorer(
tuned_churn_model, X_test.drop(columns=["CustomerID"]), y_test
)
print(f"Original model profit: {original_model_profit}")
print(f"Tuned model profit: {tuned_model_profit}")
Original model profit: 29640
Tuned model profit: 35600
与原始模型相比,调整后的模型的利润更高。同样,我们可以将目标指标与决策阈值进行绘图,以可视化在交叉验证过程中对训练数据的决策阈值选择:
fig, ax = plt.subplots(figsize=(5, 5))
ax.plot(
tuned_churn_model.cv_results_["thresholds"],
tuned_churn_model.cv_results_["scores"],
marker="o",
markersize=3,
linewidth=1e-3,
color="#c0c0c0",
label="Objective score (using cost-matrix)",
)
ax.plot(
tuned_churn_model.best_threshold_,
tuned_churn_model.best_score_,
"^",
markersize=10,
color="#ff6700",
label="Optimal cut-off point for the business metric",
)
ax.legend()
ax.set_xlabel("Decision threshold (probability)")
ax.set_ylabel("Objective score (using cost-matrix)")
ax.set_title("Objective score as a function of the decision threshold")

图片由作者创建。
实际上,从业务角度来看,给所有误分类实例分配一个固定的成本是不现实的。通过为数据集中的每个实例分配权重来调整阈值是更先进的方法。这一点在 scikit-learn 的成本敏感学习示例中有介绍。
约束下调优
目前这个方法在 scikit-learn 文档中没有涉及,但它是二分类应用场景中常见的业务案例。约束下调优方法通过在 ROC 曲线或精确率-召回率曲线中找到一个点来确定决策阈值。曲线上的这个点是一个轴的最大值,同时约束另一个轴。在本次教程中,我们将使用 Pima 印第安人糖尿病数据集。这是一个二分类任务,用于预测一个人是否患有糖尿病。
假设您的模型将用于对一个平均风险群体进行筛查,且适用于数百万人。在美国,大约有 3800 万人患有糖尿病,这大约占总人口的 11.6%。因此,模型的特异性应该很高,以避免误诊数百万人患有糖尿病,并将其转诊进行不必要的确认性测试。假设您的虚拟 CEO 已经表明,他们不会容忍超过 2% 的假阳性率。让我们构建一个使用 TunedThresholdClassifierCV 达到这一目标的模型。
在本部分教程中,我们将定义一个约束函数,用于在 2% 假阳性率下找到最大真正阳性率。
def max_tpr_at_tnr_constraint_score(y_true, y_pred, max_tnr=0.5):
fpr, tpr, thresholds = roc_curve(y_true, y_pred, drop_intermediate=False)
tnr = 1 - fpr
tpr_at_tnr_constraint = tpr[tnr >= max_tnr].max()
return tpr_at_tnr_constraint
max_tpr_at_tnr_scorer = make_scorer(
max_tpr_at_tnr_constraint_score, max_tnr=0.98)
data = pd.read_csv("data/diabetes.csv")
X_train, X_test, y_train, y_test = train_test_split(
data.drop(columns=["Outcome"]),
data["Outcome"],
stratify=data["Outcome"],
test_size=0.2,
random_state=RANDOM_STATE,
)
构建两个模型,一个是逻辑回归模型作为基线模型,另一个是 TunedThresholdClassifierCV,它将包装基线逻辑回归模型,以实现 CEO 所设定的目标。在调优模型中,设置 scoring=max_tpr_at_tnr_scorer。同样,本教程中的模型选择和预处理步骤并不重要。
# A baseline model
original_model = make_pipeline(
StandardScaler(), LogisticRegression(random_state=RANDOM_STATE)
)
original_model.fit(X_train, y_train)
# A tuned model
tuned_model = TunedThresholdClassifierCV(
original_model,
thresholds=np.linspace(0, 1, 150),
scoring=max_tpr_at_tnr_scorer,
store_cv_results=True,
cv=8,
random_state=RANDOM_STATE,
)
tuned_model.fit(X_train, y_train)
比较 scikit-learn 估计器默认决策阈值 0.5 和使用约束下调优方法在 ROC 曲线中找到的阈值之间的差异。
# Get the fpr and tpr of the original model
original_model_proba = original_model.predict_proba(X_test)[:, 1]
fpr, tpr, thresholds = roc_curve(y_test, original_model_proba)
closest_threshold_to_05 = (np.abs(thresholds - 0.5)).argmin()
fpr_orig = fpr[closest_threshold_to_05]
tpr_orig = tpr[closest_threshold_to_05]
# Get the tnr and tpr of the tuned model
max_tpr = tuned_model.best_score_
constrained_tnr = 0.98
# Plot the ROC curve and compare the default threshold to the tuned threshold
fig, ax = plt.subplots(figsize=(5, 5))
# Note that this will be the same for both models
disp = RocCurveDisplay.from_estimator(
original_model,
X_test,
y_test,
name="Logistic Regression",
color="#c0c0c0",
linewidth=2,
ax=ax,
)
disp.ax_.plot(
1 - constrained_tnr,
max_tpr,
label=f"Tuned threshold: {tuned_model.best_threshold_:.2f}",
color="#ff6700",
linestyle="--",
marker="o",
markersize=11,
)
disp.ax_.plot(
fpr_orig,
tpr_orig,
label="Default threshold: 0.5",
color="#004e98",
linestyle="--",
marker="X",
markersize=11,
)
disp.ax_.set_ylabel("True Positive Rate", fontsize=8)
disp.ax_.set_xlabel("False Positive Rate", fontsize=8)
disp.ax_.tick_params(labelsize=8)
disp.ax_.legend(fontsize=7)

图片由作者创建。
约束下调优方法找到了一个 0.80 的阈值,这导致在训练数据交叉验证过程中平均灵敏度为 19.2%。比较灵敏度和特异性,看看这个阈值在测试集中的表现如何。该模型在测试集中达到了 CEO 的特异性要求吗?
# Average sensitivity and specificity on the training set
avg_sensitivity_train = tuned_model.best_score_
# Call predict from tuned_model to calculate sensitivity and specificity on the test set
specificity_test = recall_score(
y_test, tuned_model.predict(X_test), pos_label=0)
sensitivity_test = recall_score(y_test, tuned_model.predict(X_test))
print(f"Average sensitivity on the training set: {avg_sensitivity_train:.3f}")
print(f"Sensitivity on the test set: {sensitivity_test:.3f}")
print(f"Specificity on the test set: {specificity_test:.3f}")
Average sensitivity on the training set: 0.192
Sensitivity on the test set: 0.148
Specificity on the test set: 0.990
结论
新的TunedThresholdClassifierCV类是一个强大的工具,可以帮助你通过向商业领导者展示你如何得出决策阈值,成为更好的数据科学家。你学习了如何使用新的 scikit-learn TunedThresholdClassifierCV类来最大化一个指标,执行成本敏感学习,并在约束条件下调整指标。这个教程并不旨在全面或深入。我希望介绍这一新特性,并突出它在解决二分类问题时的强大功能和灵活性。请查阅 scikit-learn 文档、用户指南和示例,以获取详细的使用示例。
特别感谢Guillaume Lemaitre为此功能所做的工作。
感谢阅读,祝调优愉快。
数据许可证:
信用卡欺诈:DbCL
皮马印第安人糖尿病数据集:CC0
电信客户流失:商业用途可用
无需调优的 LLM 更长上下文长度——自我扩展(LLM 也许是 LongLM)评审
一种简单的策略,使 LLM 在推理过程中能够处理更长的上下文输入,而无需进行微调。
·发布于Towards Data Science ·阅读时长 6 分钟·2024 年 1 月 4 日
--

来源:作者使用 DALL-E 3 创建的图像
在本文中,我们将查看 Hongye Jin 等人刚刚在 arxiv 发布的论文“LLM 也许是 LongLM:自我扩展 LLM 上下文窗口无需调优”(2024 年 1 月 2 日发布)。
Github: github.com/datamllab/LongLM
像 GPT-3 或 BERT 这样的 LLM 通常是在固定长度的序列上进行训练的。这一设计选择源于实际的限制条件:管理计算资源并保持训练效率。因此,这些模型通常会在一个预定的最大长度的序列上进行训练(例如,BERT 的最大长度为 512 个标记,或对于更大的模型,可能是几千个标记)。这里的限制是,模型的自注意力机制——一个帮助模型理解和生成语言的关键组件——只能训练去关注这个固定长度范围内的上下文。
在训练过程中,模型学习创建输入序列的表示,结合了每个标记的内容(标记)和位置。位置编码是一种融合单词顺序的方法,在这里发挥着至关重要的作用。然而,由于训练是基于固定长度的序列,模型对位置关系的理解仅限于这个范围。当在推理时遇到更长的序列时,模型会遇到它从未见过的位置和标记之间的相对距离,这就导致了作者所说的“位置 O.O.D(超出分布)”问题。从本质上讲,模型的性能下降是因为它正在处理的输入结构与其训练时的输入不同。
因此,挑战在于使这些模型能够处理比它们训练时更长的序列,而不因位置的 O.O.D(超出分布)问题而导致性能下降。这个问题在现实世界的应用中尤为重要,因为输入序列通常比训练序列要长得多,而在这些更长的序列上保持上下文的能力对于任务如文档摘要、扩展对话理解或阅读长篇技术文档等至关重要。作者认为,LLMs 天生就能够处理比通常训练时更长的文本序列,唯一的主要瓶颈是 O.O.D 位置编码问题。
论文提出了一种简单而有效的策略,可以将较长的“推理时间”序列适配到在有限上下文长度上训练的模型,而无需进行任何微调。
在讨论论文的方法之前,我们先快速了解一下用于评估大语言模型(LLMs)的困惑度指标。
困惑度(PPL)
困惑度(PPL)是自然语言处理(NLP)中常用的度量,用于评估语言模型的表现。它衡量的是一个概率模型对样本的预测能力。在语言模型的背景下,困惑度衡量的是模型对一系列单词的预测能力。困惑度被定义为一个单词序列的负对数似然的指数化平均值。数学上来说

来源:huggingface.co/docs/transformers/perplexity
较低的困惑度分数意味着更好的语言模型。这意味着模型在处理测试样本时,平均会为其分配更高的概率。换句话说,模型对实际遇到的单词序列不那么“惊讶”,这也是为什么使用“困惑度”这一术语的原因。困惑度通常用于因果模型,如 GPT、Mistral 等(而不是用于 BERT 这类的 MLM)。关于困惑度的一个很棒的博客:huggingface.co/docs/transformers/perplexity
自我扩展
在标准的大型语言模型(LLMs)中,序列中的每个标记都会关注到其他所有标记,考虑它们的相对位置。这种位置信息对于理解上下文和单词之间的关系至关重要。然而,这一机制是基于训练时看到的最大序列长度。当输入序列超过此长度时,模型会遇到它从未见过的位置(O.O.D.),导致性能下降。
分组注意力:为了将 O.O.D.位置转换为模型训练时看到的范围,将执行一个简单的数学“向下取整除法”操作(在编程中表示为“//”)。

例如,考虑一个包含 8 个标记的序列和一个大小为 2 的分组。原始位置[0, 1, 2, 3, 4, 5, 6, 7]将被映射为[0, 0, 1, 1, 2, 2, 3, 3]。通过分组标记,模型可以处理比其原始训练限制更长的序列,有效地扩展其上下文窗口。

来源:arxiv.org/pdf/2401.01325.pdf
但是,当多个标记具有相同的位置信息时,位置的不准确可能会导致性能的下降。论文表明,当分组大小较小时,PPL(困惑度)略高于原始的大型语言模型。
邻近标记是生成下一个标记最重要的标记。对于彼此接近的标记,精确的相对位置对于理解即时上下文至关重要,例如句子的语法和语义。而当标记在文本中相隔较远时,它们之间的精确相对位置变得不那么重要,理解整体上下文或意义更依赖于它们之间的一般主题联系。
在这一点上,论文提出将带有 FLOOR 操作的分组注意力与常规注意力(具有常规位置)结合使用。作者建议对每个标记周围的预定义“邻居窗口”使用常规注意力,而对该窗口外的标记使用分组注意力。使用论文中的符号,‘L’:预训练上下文窗口大小,‘G’:分组注意力的组大小,‘wn’:邻居标记的窗口大小:

来源:arxiv.org/pdf/2401.01325.pdf
作者通过*wn* − (*wn // G)*引入分组注意力的相对位置变化,以确保常规注意力区域和分组注意力区域之间的平滑过渡。最后,他们通过用分组注意力中的值替换邻居标记窗口外的注意力值,来合并两部分注意力。
例如,考虑一个序列,其中原始模型的上下文窗口为 7 个标记,组大小为 2,邻近窗口为 4 个标记。对于标记位置[0, 1, 2, 3, 4, 5, 6],常规注意力应用于[0, 1, 2, 3],而分组注意力应用于[4, 5, 6]。注意力矩阵将是两者的结合,前四个标记具有精确的注意力,而后三个标记则采用分组注意力。最终的注意力矩阵随后用于计算 Transformer 的注意力层的输出,从而保持了局部精度和长程上下文感知能力。
结论
论文展示了与 LLaMA-2、Mistral 和 SOLAR 在有无自我扩展(Self-Extend)情况下的对比。根据论文内容,自我扩展显著降低了模型在长上下文窗口大小下的困惑度(perplexity)。这些模型还在多种任务上进行了测试,如单文档和多文档问答、摘要生成以及少样本学习。在大多数情况下,具有自我扩展的 LLM 在这些基准测试中超越了其原始版本,甚至超越了一些微调后的模型。模型还在 Hugging Face Open LLM 基准测试套件中的各种短上下文任务上进行了测试,这些任务的性能几乎没有受到影响,表明自我扩展不会对模型处理较短文本的能力产生不利影响。
该任务要求语言模型在一个冗长的、毫无意义的文本序列中找到一个基本密码(一个随机的五位数),该密码散布在各个层次。研究结果表明,在没有特定调整的情况下,自我扩展能够在所有测试深度和上下文长度下实现 100%的密码找回成功率。

来源:arxiv.org/pdf/2401.01325.pdf
此外,结果表明,即使 Mistral w/ SWA(滑动窗口注意力)在其初始训练上下文范围之外的 PPL 值有所降低,它仅限于在其滑动窗口内提取信息(如密码)。
总的来说,这表明自我扩展成功地利用了 LLM 在扩展上下文中的固有能力。
使用 LLM2Vec 将 Llama 3 转化为嵌入模型
使用 Llama 3 进行生成和检索的 RAG
·发表于Towards Data Science ·7 分钟阅读·2024 年 5 月 3 日
--

使用 DALL-E 生成
嵌入模型是大语言模型(LLMs)中检索增强生成(RAG)的关键组成部分。它们对用户编写的知识库和查询进行编码。
使用为与 LLM 相同领域训练或微调的嵌入模型,可以显著提高 RAG 系统的效果。然而,找到或训练这样的嵌入模型通常是一个困难的任务,因为领域内的数据通常稀缺。
在本文中,我将展示如何使用 LLM2Vec 将 LLM 转化为文本嵌入模型。我们将看到如何用 Llama 3 来创建一个 RAG 系统,该系统只需 Llama 3 即可,不需要任何其他模型。
LLM2Vec:你的 LLM 也是一个嵌入模型
本文介绍了 LLM2Vec:
官方实现已在 GitHub 上发布:
- McGill-NLP/llm2vec(MIT 许可)
LLM2Vec 是如何工作的?
LLMs 是通过因果语言建模损失进行训练的。它们被训练用来预测给定一序列 token 后的下一个 token。由于训练示例是完整的 token 序列,因此在序列中应用因果注意力掩码,这样当模型学习预测某个 token 时,序列中所有后续的 token 都会被掩盖,不会影响注意力计算。例如,如果我们有以下序列:
The cat is sleeping in the kitchen <eos>
在训练过程中,将依次应用以下注意力掩码(在掩码下,我只展示未被掩盖的 token):
1 0 0 0 0 0 0 0 0
The
1 1 0 0 0 0 0 0
The cat
1 1 1 0 0 0 0 0
The cat is
1 1 1 1 0 0 0 0
The cat is sleeping
1 1 1 1 1 0 0 0
The cat is sleeping in
1 1 1 1 1 1 0 0
The cat is sleeping in the
1 1 1 1 1 1 1 0
The cat is sleeping in the kitchen
1 1 1 1 1 1 1 1
The cat is sleeping in the kitchen <eos>
零值表示该 token 不会被用于注意力计算。
另一方面,文本嵌入模型被训练用来编码整个序列的标记,并且是双向的,即它们从左到右和从右到左编码一个序列。
LLM2Vec 方法将 LLM 转换为嵌入模型的初步步骤包括将解码器-only LLMs 中使用的因果注意力掩码替换为全 1 矩阵。这一改变使得序列中的每个标记都可以与所有其他标记交互,实质上将其转变为一个双向 LLM。然而,解码器-only LLMs 并没有被训练来编码未来的标记,因此,这一简单的修改会降低表示的质量。尽管如此,仍然可以训练模型有效利用其新的双向注意力能力。
他们建议采用一种新的掩码下一个标记预测(MNTP)目标。MNTP 将下一个标记预测目标与 BERT 使用的掩码语言模型结合在一起。为了实现这一点,我们取一个任意的序列 x = (x1, x2, . . . , xN),掩码掉输入标记的一个子集,然后训练模型使用过去和未来的上下文来预测这些被掩码的标记。重要的是,当预测一个在位置 i 被掩码的标记时,损失是根据前一个位置 i - 1 的标记表示的 logits 计算的,而不是像 BERT 模型那样根据被掩码的位置本身计算。
将 LLM 转换为双向模型并随后进行 MNTP 训练,可以将任何解码器-only LLM 转换为编码器。然而,这些步骤可能不足以解决序列表示的问题。实际上,像 BERT 这样的双向编码器通常还会在预训练中加入下一个句子预测任务,而 LLM 并未经过此类训练。LLM2Vec 通过让模型对每个输入序列进行两次处理,每次使用不同随机选择的丢弃掩码,生成该序列的两个不同表示,来解决这一缺失的能力。训练目标是提高这两个表示之间的相似性,同时减少它们与同一训练批次中不同序列表示之间的相似性。他们将这最后一步称为“无监督对比学习”(SimCSE)。
LLM2Vec 的完整过程通过论文中的这张图示说明:

source (CC-BY)
他们评估了 LLM2Vec 生成的模型在各类任务中的表现,并显示它们可以超越标准的文本嵌入模型。你可以在论文的第三部分和第四部分找到相关结果。
使用 LLM2Vec 将 Llama 3 转换为文本嵌入模型
我的笔记本展示了如何将 Llama 3 转换为嵌入模型,可以在此查看:
将 LLM 转换为文本嵌入模型使用 LLM2Vec 是相当简单的。
首先,安装以下软件包:
pip install llm2vec
pip install flash-attn --no-build-isolation
llm2vec 包将会把 LLM 转换为嵌入模型。flash-attn 是 FlashAttention 的包。虽然不是必需的,但它可以加速 MNTP 训练。它仅适用于安培架构(RTX 3xxx/4xxx、A100、H100 等)的最新 GPU。
然后,转换本身通过以下代码执行:
import torch
from llm2vec import LLM2Vec
l2v = LLM2Vec.from_pretrained(
"meta-llama/Meta-Llama-3-8B",
device_map="cuda" if torch.cuda.is_available() else "cpu",
torch_dtype=torch.bfloat16,
)
l2v.save("Llama-3-8B-Emb")
“torch_dtype=torch.bfloat16”是必要的,以便能在 24GB 的 GPU 上运行转换。如果不设置此参数,模型将比原始的 float32 参数模型大。
我已将该模型推送到中心,以防你不想自己进行此转换:
这个模型已经可以使用了。你可以将其插入 RAG 流水线中。不过,它的性能通常不如标准嵌入模型(在大多数情况下)。
我们需要用 MNTP 目标训练 Llama 3 8B。作者也提供了一个脚本来完成此操作:
它目前支持 Llama 和 Mistral 架构的模型。
要使用它,请在本地克隆该仓库:
git clone https://github.com/McGill-NLP/llm2vec.git
该脚本期望传入一个参数,即一个 JSON 格式的配置文件。它们在此处提供了几个示例:
对于 Llama 3 8B,我做了以下配置:
JSON_CONFIG='''
{
"model_name_or_path": "meta-llama/Meta-Llama-3-8B",
"dataset_name": "wikitext",
"dataset_config_name": "wikitext-103-raw-v1",
"per_device_train_batch_size": 1,
"per_device_eval_batch_size": 1,
"gradient_accumulation_steps": 16,
"do_train": true,
"do_eval": true,
"max_seq_length": 512,
"mask_token_type": "blank",
"data_collator_type": "all_mask",
"mlm_probability": 0.8,
"overwrite_output_dir": true,
"output_dir": "Llama-3-8B-llm2vec-MNTP-Emb",
"evaluation_strategy": "steps",
"eval_steps": 100,
"save_steps": 200,
"stop_after_n_steps": 1000,
"lora_r": 16,
"gradient_checkpointing": true,
"torch_dtype": "bfloat16",
"attn_implementation": "flash_attention_2"
}
'''
with open("mntp_config.json", 'w') as f:
f.write(JSON_CONFIG)
该脚本会以 bfloat16 参数加载模型。我将每个设备的批次大小设置为 1,以便训练可以适应 24GB 的 GPU。
然后,我们可以开始 MNTP 训练:
python llm2vec/experiments/run_mntp.py mntp_config.json
使用 24GB 的 GPU(例如 Google Colab 的 L4)进行三轮训练需要 4 天时间。如果你等不及这么久,可能一轮就足够了。
在 MNTP 训练后,模型应能产生更好的结果,特别是在检索任务中。
对于最后一步,即 SimCSE 训练,作者尚未发布代码,但提到他们会发布。
设置 Llama 3 文本嵌入模型用于 RAG
在上一部分创建的嵌入模型已准备好用于 RAG 流水线。例如,你可以通过sentence-transformers(Apache 2.0 许可证)加载它。
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("kaitchup/Llama-3-8B-llm2vec-Emb")
如果你使用LlamaIndex(MIT 许可证),可以设置 HuggingFaceEmbedding 模型:
Settings.embed_model = HuggingFaceEmbedding(model_name="kaitchup/Llama-3-8B-llm2vec-Emb", device='cpu')
我设置了 device='cpu',但使用 CPU 会使 RAG 系统运行变慢。你可以移除此参数以在 GPU 上运行。然而,请注意,模型是以全精度加载的,这使得它无法适应普通消费者的 GPU。
在本教程中,我详细解释了如何使用 LlamaIndex 设置 RAG 系统:
[## RAG for Mistral 7B Instruct with LlamaIndex and Transformers
预算有限的 RAG
newsletter.kaitchup.com](https://newsletter.kaitchup.com/p/rag-for-mistral-7b-instruct-with?source=post_page-----8448005f99aa--------------------------------)
结论
使用 LLM2Vec,我们现在可以将 LLM 用作文本嵌入模型。转换过程简单且快速。使用同一个模型同时进行生成和检索在 RAG 系统中非常有吸引力,因为我们不需要寻找额外的嵌入模型。
然而,从 LLM 中提取的嵌入模型往往表现不如常规嵌入模型。LLM2Vec 的作者提出了新的训练目标,MNTP 和 SimCSE,用于训练从 LLM 中提取的嵌入模型。根据作者的说法,尽管这种训练成本较高,但能显著提升嵌入模型的表现。
为了支持我的工作,考虑订阅我的新闻通讯,以获取更多关于 AI 最新进展的文章/教程:
[## Kaitchup - 预算内的 AI | 本杰明·马里 | Substack
每周教程、技巧和新闻,涉及在您的计算机上微调、运行和服务大型语言模型的内容。…
newsletter.kaitchup.com](https://newsletter.kaitchup.com/?source=post_page-----8448005f99aa--------------------------------)
将你的 Python 函数通过一行代码变成装饰器
一种编写更简洁、更清晰、更易读的 Python 装饰器的新方法,同时也充当上下文管理器
·发表于 Towards Data Science ·阅读时间 4 分钟 ·2024 年 5 月 23 日
--

装饰器简化(图片由 Tobias Bjørkli 提供,来自 Pexels)
想写一个装饰器,却记不住语法吗?装饰器的语法相当复杂,涉及许多样板代码。在这篇文章中,我们将展示一种更简单的编写装饰器的方法。这种新方法会简短、清晰、易读。让我们开始编码吧!
创建装饰器的默认方式
下面的代码是创建装饰器的默认方式。它会记录被装饰函数运行的时间。查看这篇文章,深入了解装饰器。
def timer(name:str) -> Callable:
def decorator(func:Callable) -> Callable:
@wraps(func)
def decorator_implementation(*args, **kwargs):
try:
print(f"TIMER: {name} start")
strt = time.perf_counter()
return func(*args, **kwargs)
finally:
print(f"TIMER: {name} finished in {time.perf_counter() - strt}")
return decorator_implementation
return decorator
这样我们就可以像这样使用我们的代码:
@timer(name="test")
def…
将自己转化为 3D 高斯点绘
实践者的动手指南
·发表于 Towards Data Science ·阅读时间 11 分钟·2024 年 3 月 14 日
--
去年夏天,一种非深度学习方法进入了新视图合成领域:3D 高斯点绘。这是一种表示三维场景并能够从任意视角实时渲染图像的方法。有些人甚至认为它正在取代 NeRF,这一方法曾在当时主导新视图合成和隐式场景表示。我认为这点值得商榷,因为NeRFs远不止是图像渲染器。但这些今天都不重要……今天我们只关心清晰的 3D 模型,而这正是 3D 高斯点绘所擅长的地方 🎉
在这篇文章中,我们将简要介绍高斯点绘,然后切换话题,我将展示如何将自己转化为一个 3D 模型。
额外奖励:最后我将展示如何将你的模型嵌入到任何网站的互动查看器中。
那么,我们开始吧!

图片来自 Sascha Kirch.
大纲
-
什么是高斯点绘?
-
让我们将自己转化为 3D 高斯点绘
-
结论与进一步资源
什么是高斯点绘?
3D 高斯点绘(Gaussian splatting)是一种表示三维场景的技术。它实际上只是众多方法之一。例如你……
将您的关系型数据库转化为图形数据库
利用大型语言模型(LLMs)增强您的数据
·发布于 Towards Data Science ·6 分钟阅读·2024 年 4 月 14 日
--

图像由作者生成
关系型数据库数十年来一直是数据管理的支柱,利用其表格格式和严格的模式来处理结构化数据。
但如果您的数据的真正潜力在于数据点之间的关系呢?这就是图形数据库发挥作用的地方。
与传统的关系型数据库不同,图形数据库在每个数据点可以与多个其他实体相连接的场景中表现优越,这些关系可以被利用来发现新的洞察。
如果您需要浏览深层次的层级结构或发现隐藏的联系,使用图形数据库可能是一个合适的选择。
它们在构建推荐系统、客户关系管理(CRM)系统或分析客户行为的工具时尤其有用。
在本教程中,我将指导您如何将关系型数据库转换为 Python 中的动态图形数据库。我们将以 Amazon 产品数据集 为例,从产品标题中提取实体,丰富数据集并将其转换为图形数据库。
为此,我们将利用 OpenAI 的 GPT-3.5-turbo 模型,然后使用 Cypher 查询将数据加载到 Neo4j 图形数据库中。
做交叉验证时需要避免的两个常见陷阱
以及你需要用来应对这些问题的技巧
·发布于数据科学进阶 ·阅读时长 8 分钟·2024 年 5 月 24 日
--
交叉验证是数据科学家必须掌握的一项重要技术,但它也容易被误用。
在本文中,我将重点介绍我经常看到的两种错误,以及你需要掌握的概念来应对这些错误:
-
嵌套交叉验证
-
时间序列交叉验证
学习这些技巧帮助我获得了我的第一份数据科学工作,如果你能掌握它们,你将避免在构建机器学习模型时犯傻错误。
但是首先,回顾一下:交叉验证的意义是什么?
机器学习的基本理念是:在一个“训练”数据集上拟合模型,并在一个单独的保留“测试”数据集上评估模型的表现(这应该模拟你模型在现实世界中的表现):
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.datasets import make_classification
# Example dataset
X, y = make_classification(n_samples=1000, n_features=20, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
clf = LogisticRegression()
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
rocauc = roc_auc_score(y_test, y_pred)
我的机器学习之旅中的经验教训:数据划分与数据泄漏
从统计建模转向机器学习时需要避免的常见错误
·发表于 Towards Data Science ·7 分钟阅读·2024 年 2 月 25 日
--

图片由 Susan Q Yin 提供,来自 Unsplash
我的故事
数据科学、机器学习和人工智能无疑是当今的流行词汇。我的 LinkedIn 充满了分享学习路线图的数据专家,供那些渴望进入数据领域的人参考。
然而,从我的个人经验来看,我发现走向数据科学的道路并不像单纯按照固定路线图前进那样线性,尤其是对于那些从不同职业背景转型的人来说。数据科学需要多种技能的结合,如编程、统计学、数学、分析、软技能和领域知识。这意味着每个人根据自己的先前经验和技能集,从不同的起点开始学习。
作为一位在研究和分析领域工作多年并获得分析学硕士学位的人,我积累了一定的统计学知识及其应用。然而,数据科学是一个如此广泛且充满活力的行业,我的知识仍然是零散的。我也曾努力寻找能够有效填补我在统计学和机器学习之间知识空白的资源。这给我的学习体验带来了重大挑战。
在这篇文章中,我想分享我在从研究与分析转向数据科学过程中遇到的技术性忽视问题。希望我的分享能为你节省时间,并帮助你避免这些陷阱。
统计建模与机器学习
所以,你可能会想,为什么我从回顾我的旅程开始,而不是直接进入主题。原因很简单——我发现很多人声称自己在构建机器学习模型,但实际上他们只是构建统计模型。我承认我曾是其中之一!并不是说一种比另一种更好,但我认为在讨论技术细节之前,理解统计建模与机器学习之间的差异是非常重要的。
统计模型的目的是做出推断,而机器学习的主要目标是进行预测。简单来说,机器学习模型利用统计学和数学生成适用于现实世界场景的预测。这也是数据划分和数据泄漏问题出现的地方,特别是在有监督机器学习的背景下。
我最初的信念是,理解统计分析就足够进行预测任务了。然而,我很快意识到,如果没有掌握诸如适当的数据划分和意识到潜在的陷阱(如数据泄漏)等数据准备技巧,即使是最复杂的统计模型在预测性能上也会有所欠缺。
那么,让我们开始吧!
错误 1: 数据划分不当
什么是数据划分?
数据划分,本质上是将数据集分割成多个部分,以便模型获得最佳的预测性能。
考虑一个我们许多人都熟悉的简单最小二乘回归(OLS)概念。我们在商学、统计学、金融学、经济学或工程学课程中都听说过它。它是一个基本的机器学习技术。
假设我们有一个房价数据集,以及可能影响房价的各类因素。
在传统统计分析中,我们使用整个数据集来构建回归模型,因为我们的目标只是理解哪些因素会影响房价。换句话说,回归模型可以解释价格变化的程度与预测变量之间的关系。
然而,在机器学习中,统计部分保持不变,但数据划分变得至关重要。让我解释一下原因——假设我们在整个数据集上训练模型,那么我们如何知道模型在未见数据上的预测表现呢?
正因为如此,我们通常将数据集分为两部分:训练集和测试集。其理念是,在一部分数据上训练模型,在另一部分数据上评估模型的性能。本质上,测试集应该充当真实世界的数据,这意味着在训练阶段,模型绝不能接触到测试数据。
这里出现了我之前没有意识到的陷阱。将数据拆分成两个数据集本身并不错误,但存在创建不可靠模型的风险。想象一下,你在训练集上训练模型,在测试集上验证其准确性,然后重复这个过程来微调模型。这会在模型选择中产生偏差,且会违背“未见数据”的初衷,因为测试数据在模型开发过程中已经多次出现。它削弱了模型真实预测未见数据的能力,导致过拟合问题。
如何防止:
理想情况下,数据集应分为两个块(三个不同的拆分):
-
(训练集 + 验证集)→ 第一个块
-
测试集 → 第二个块
模型可以在第一个块上进行训练和验证。第二个块(测试集)不应参与任何模型训练过程。将测试集视为禁区!
数据拆分的方式取决于数据集的大小。行业标准是训练集(第一个块)占 60% — 80%,测试集占 20% — 40%。验证集通常从第一个块中划分出来,因此实际的训练集将占第一个块的 70% — 90%,其余部分为验证集。
理解这个概念的最好方式是通过一个图示:

留一法(LOOV)方法(图像来自作者)
除了 LOOV(图中所示),还有其他多种数据拆分技术:
-
K 折交叉验证,将数据分成‘K’个折叠,并相应地迭代训练过程
-
滚动窗口交叉验证(适用于时间序列数据)
-
阻塞交叉验证(适用于时间序列数据)
-
不平衡类别的分层抽样拆分
注意:时间序列数据在拆分时需要特别小心,因为其时间顺序。随机拆分数据集可能会破坏其时间顺序。(我是通过亲身经历学到的)
最重要的一点是,无论使用何种技术,“测试集”都应保持独立且未被触及,直到模型选择阶段。
错误 2:数据泄漏
“在机器学习中,数据泄漏是指机器学习模型的创建者犯下的一个错误,他们不小心将测试数据集和训练数据集之间的信息共享。” — Analytics Vidhya
这与我提到的第一个观点有关,即测试数据被训练数据污染。这是数据泄漏的一个例子。然而,仅仅拥有验证集并不能避免数据泄漏。
为了防止数据泄漏,我们需要小心处理数据的过程——从探索性数据分析(EDA)到特征工程。任何允许训练数据与测试数据交互的过程,都可能导致泄漏。
数据泄漏主要有两种类型:
- 训练-测试污染
我曾犯的一个常见错误是,在数据拆分之前对整个数据集应用标准化/预处理程序。例如,在整个数据集上使用均值填充来处理缺失值/异常值。这使得训练数据包含了测试数据中的信息。因此,模型的准确率被虚高,相较于它在实际应用中的表现。
2. 目标泄漏
如果特征(预测变量)与我们想要预测的变量(目标变量)之间存在某种依赖关系,或者如果预测时特征数据不可用,这可能会导致目标泄漏。
让我们以我所处理的数据为例。在这里,我试图根据广告活动预测销售表现。我尝试加入转化率。我忽略了转化率仅在活动结束后才会得知。换句话说,我在预测时无法获得这些信息。此外,因为转化率与销售数据相关联,这就引入了典型的目标泄漏案例。包括转化率会导致模型学习到通常无法访问的数据,从而产生过于乐观的预测。

示例(虚构)数据集(图片由作者提供)
如何防止数据泄漏:
总结来说,记住以下几点来解决数据泄漏问题:
-
适当的数据预处理
-
小心交叉验证
-
小心特征选择
结束语
就这些了!感谢你一直看到最后!希望这篇文章能够澄清关于数据拆分的常见误解,并为构建高效的机器学习模型提供最佳实践。
这不仅是记录我的学习过程,也是为了共同学习。所以,如果你发现我技术知识中的空白或有任何见解要分享,随时可以给我留言!
参考文献:
Daniel Lee Datainterview.com LinkedIn 帖子
Analytics Vidhya — 数据泄漏及其对机器学习模型性能的影响
UEFA 欧洲杯 2024 地图

UEFA 欧洲锦标赛历史成绩记录
·发布于Towards Data Science ·6 分钟阅读·2024 年 6 月 17 日
--
现在,整个欧洲都在关注 UEFA 欧洲杯 2024 锦标赛。我们应该期待什么?各国球队之前表现如何?为了在地图上可视化这些信息,我将维基百科的数据和自然地球的世界地图结合,并使用 Python 中的地理空间数据科学工具,展示如何轻松构建可以与国家关联的任何数据的自定义地图。
所有图片由作者提供。
1. 数据获取
首先,我们使用来自自然地球计划的免费开源世界地图。为了确保每个国家都得到适当的表示,我使用了 10 米分辨率文化地图集中的Admin 0 — Details — map units地图。此地图涵盖了全球 209 个主权国家。
# Import Geopandas
import geopandas as gpd
# Parse the world map
gdf = gpd.read_file('ne_10m_admin_0_map_units')
display(gdf[['GEOUNIT', 'geometry']].head(3))
gdf.plot()
输出结果:

接下来,我从维基百科收集官方的足球杯赛记录,我简单地复制并粘贴了这些内容……
终极指南:3D 数据科学系统和工具
3D 数据科学
本蓝图分享了 AI 方法、算法、工具、模板以及构建 3D 模型数据科学解决方案的 6 步系统:3D 数据采集、分析、建模、可视化和部署。
·发布于 Towards Data Science ·阅读时间:17 分钟·2024 年 8 月 1 日
--

3D 数据科学系统的核心组件及其交叉学科。© Florent Poux 博士
设置一个 3D 数据科学项目涉及数据工程、数据分析和可视化技术的结合,这些技术专门用于处理三维数据。
从收集初始数据到自动化 3D 沉浸式体验,我将覆盖整个流程。我将详细介绍所有必要的步骤、资源和方法,确保高效的工作流程和出色的结果。
🦚 Florent 的备注:我想尝试一些更高层次的实验。因此,您不会得到一个完整的 3D 变换网络实现,但您将获得足够的理解,以定义下一个您认为有潜力的 3D 数据科学项目。
让我深入介绍这 6 个主要步骤:项目范围、数据采集、数据预处理、数据分析、可视化和部署,如下所示。
(非)客观机器:探讨机器学习中的历史偏见
深入探讨机器学习中的偏见,重点关注历史性(或社会性)偏见。
·发布于 Towards Data Science ·10 分钟阅读·2024 年 4 月 17 日
--
人类有偏见。对于那些曾经与偏执的人、不公平的老板或压迫性系统打交道过的人——换句话说,所有人——这并不令人惊讶。因此,我们应该欢迎能够帮助我们做出更客观决策的机器学习模型,尤其是在医疗、执法或就业等关键领域,在这些领域中,偏见的人类可能会做出改变他人生命的判断,严重影响他人的生活……对吧?嗯,不对。尽管我们可能会误以为机器学习模型是客观和理性的,但偏见可能以多种方式内建在模型中。在这篇博文中,我们将重点讨论机器学习(ML)中的历史偏见。
什么是偏见?
在我们的日常生活中,当我们提到偏见时,我们通常是指“基于先入为主的观念或偏见的判断,而非公正的事实评估”。统计学家也使用“偏见”一词来描述几乎任何可能导致“真实”参数与模型估计之间系统性差异的情况。
由于统计在机器学习模型中的作用很大,这些模型会受到统计偏见的影响。然而,这些模型也是由人类设计的,并且使用由人类生成的数据进行训练,这使得它们容易学习并延续人类的偏见。因此,或许反直觉的是,机器学习模型可能比人类更容易受到偏见的影响,而不是更少。
专家们对于算法偏见的具体数量存在分歧,但至少有 7 种潜在的有害偏见来源(Suresh & Guttag, 2021),它们在数据分析流程的不同阶段产生:
-
历史偏见,源于数据生成阶段的世界;
-
表示偏见,当我们从世界中抽取数据样本时就会产生这种偏见;
-
测量偏见,我们使用的度量标准或收集的数据可能无法准确反映我们想要测量的内容;
-
聚合偏见,我们将同一种方法应用于整个数据集,尽管某些子集需要不同的处理方式;
-
学习偏见,由于我们定义模型的方式,导致了系统性的错误;
-
评价偏见,我们在对模型表现进行“评分”时,使用的数据实际上并不反映我们希望模型应用的目标人群,最后;
-
部署偏见,模型没有按照开发者原本预期的方式被使用。

由Hunter Harritt拍摄的照片,来源于Unsplash
虽然这些都是任何新兴数据科学家应该考虑的重要偏见,今天我将重点讨论历史偏见,它发生在数据分析流程的第一阶段。
嘘!想了解更多关于其他类型偏见的信息吗?观看这个有用的视频:
历史偏见
与其他类型的偏见不同,历史偏见并非源于机器学习过程,而是源于我们的世界。我们的世界历史上一直充斥着偏见,至今依然如此。因此,即使我们用来训练模型的数据完美地反映了我们所生活的世界,数据也可能捕捉到这些歧视性模式,这就是历史偏见的来源。历史偏见也可能表现在我们社会在追求平等方面取得进展的情况下,但我们的数据未能充分反映这些变化,反而体现了过去的不平等现象。
我们为什么要关心?
大多数社会都有反歧视法,旨在保护历史上受压迫的脆弱群体的权利。如果我们不小心,过去的歧视行为可能会因为历史偏见被我们的机器学习模型所学习并延续。随着机器学习模型在我们生活中的普及,从琐事到改变人生的重大决策,这带来了特别隐蔽的威胁——历史性偏见的机器学习模型有可能在前所未有的规模上延续不平等。数据科学家和数学家凯西·奥尼尔(Cathy O’Neil)将这样的模型称为“大规模数学毁灭性武器”(WMDs)——这些模型的工作机制是神秘的,产生的有害结果是受害者无法辩驳的,并且通常会惩罚社会中贫穷和受压迫的人群,同时让那些已经富有的人受益(奥尼尔,2017)。

图片来源:engin akyurt 来自 Unsplash
这样的 WMD(大规模毁灭性武器)已经开始影响全球的脆弱群体。尽管我们会认为亚马逊,作为一家从向我们推荐我们从未听说过、但突然渴望的商品中获利的公司,应该已经掌握了机器学习,研究发现他们用来扫描简历的算法学会了性别偏见,因为科技领域中女性的数量历史上较少。更令人不寒而栗的是,预测性警务工具也被证明存在种族偏见,医疗领域使用的算法也是如此,法院的算法也是如此。这类工具的广泛传播显然会带来巨大的影响,特别是因为它们可能成为加固我们社会中根深蒂固的不平等现象的一种手段。我认为,与有偏见的人类相比,这些 WMD 在我们集体努力消除不平等方面构成了更大的障碍,原因有两个:
首先,很难深入了解为什么机器学习(ML)模型会做出某些预测。深度学习似乎是当下的流行词,复杂的神经网络风靡全球。虽然这些模型令人兴奋,因为它们有潜力模拟人类无法理解的复杂现象,但它们被认为是黑盒模型,因为它们的工作原理往往不透明,即便是它们的创造者也未必能完全理解。没有针对历史(以及其他)偏见的集中努力,很难判断它们是否无意中歧视了受保护群体。
其次,我认为,由于历史性偏见模型可能造成的损害规模是前所未有且被忽视的。由于人类需要休息,并需要时间有效地处理信息,单个有偏见的人所能造成的伤害是有限的。然而,仅一个有偏见的机器学习模型就能在几分钟内作出成千上万的歧视性判断,而且不需要休息。更危险的是,许多人还认为机器比人类更客观,这导致对潜在问题模型的监管减少。这让我特别担忧,因为随着像 ChatGPT 这样的大型语言模型取得巨大成功,越来越多的人对将机器学习模型应用于工作流程产生了兴趣,这可能会自动化地促进大规模杀伤性武器在我们社会中的兴起,带来灾难性的后果。
我们能做些什么?
尽管偏见模型的影响可能令人担忧,但这并不意味着我们必须完全放弃机器学习模型。人工智能(AI)伦理是一个不断发展的领域,研究人员和活动家们都在为解决问题而努力,旨在消除或至少减少模型中的偏见。值得注意的是,最近有一股推动FAT或FATE人工智能的浪潮——公平、问责、透明和伦理的人工智能,这可能有助于偏见(以及其他伦理问题)的检测和修正。虽然这不是一个全面的清单,但我将简要概述一些减少模型中历史偏见的方法,希望能对你在数据科学之旅中有所帮助。
统计学解决方案
由于问题源于现实世界数据中的不成比例结果,为什么不通过使我们收集的数据更加成比例来解决这个问题呢?这是 Suresh, H.和 Guttag, J.(2021)提出的处理历史偏见的统计方法。简单来说,它包括从某些群体收集更多数据,而从其他群体收集较少数据(系统性的过采样或欠采样),从而在我们的训练数据集中得到更为平衡的结果分布。
基于模型的解决方案
根据 FATE AI 的目标,模型的可解释性可以被构建进去,使其决策过程更加透明。可解释性使数据科学家能够看到模型做出决策的原因,从而提供机会发现并减轻模型中潜在的历史偏见。在现实世界中,这也意味着,机器歧视的受害者可以挑战之前无法理解的模型所做出的决策,并希望能够促使这些决策重新考虑。这将有助于增加我们模型的信任度。
更技术性地说,针对机器学习模型中的偏见,算法和模型也在不断开发中。对抗去偏(Adversarial debiasing) 是一个有趣的解决方案。这类模型本质上由两部分组成:一个预测器,旨在预测结果,比如可聘用性;一个对抗者,试图根据预测的结果预测受保护的属性。就像拳击手在拳台上一样,这两个部分来回交锋,争夺表现得比对方更好,当对抗者无法根据预测结果检测出受保护的属性时,模型就被认为已经去偏。这类模型与没有去偏的模型相比表现得相当好,表明我们在优先考虑公平时不必牺牲性能。算法也已经被开发出来,用于减少机器学习模型中的偏见,同时保持良好的性能。
基于人类的解决方案
最后,也许最为关键的一点是,必须记住,尽管我们的机器正在为我们工作,我们才是它们的创造者。数据科学从始至终都与我们——这些了解历史偏见、决定优先考虑公平,并采取措施减轻历史偏见影响的人类——息息相关。我们不应将权力交给我们的创造物,而应在数据分析的各个阶段保持参与。为此,我想为创建跨国第三方机构审计机器学习过程并执行最佳实践的呼声加上一把声音。虽然这不是灵丹妙药,但它是检查我们的机器学习模型是否公平且无偏的一个好方法,也有助于具体化我们对这一事业的承诺。在组织层面,我也为呼吁在数据科学和机器学习团队中增加多样性的声音感到欣慰,因为我相信这将有助于识别和纠正我们数据分析过程中现存的盲点。同时,商业领袖们也需要意识到人工智能的局限性,明智地使用它,而不是以生产力或利润为名滥用它。
作为数据科学家,我们也应对我们的模型负责,并记住它们所掌握的力量。历史上的偏见虽源于现实世界,但我相信机器学习工具同样有潜力帮助我们纠正当前的不公。例如,过去,种族主义或性别歧视的招聘人员可能会因为偏见在将候选人名单交给招聘经理之前就剔除合适的申请者,而一个公平的机器学习模型可能能够高效地找到合适的候选人,忽略他们的受保护属性,从而为以往被忽视的申请者提供宝贵的机会。当然,这不是一项容易的任务,而且本身充满了伦理问题。然而,如果我们的工具真的能塑造我们所生活的世界,为什么不让它们反映出我们想要生活的世界,而不仅仅是现有的世界呢?
结论
无论你是一个初学的数据科学家,一个机器学习工程师,还是一个对使用机器学习工具感兴趣的人,我希望这篇博客文章能为你阐明历史偏见如何放大并自动化不平等现象,带来灾难性的影响。虽然机器学习模型和其他人工智能工具让我们的生活变得更加便捷,并且与现代生活不可分割,但我们必须记住,它们并非无懈可击,需要进行充分的监管,确保我们的工具始终是有益的,而不是有害的。
想要了解更多吗?
下面是我在学习机器学习中的偏见与伦理学时发现的有用资源:
视频
-
算法偏见与公平性:人工智能速成课程 #18(也在上面有链接)
书籍
-
《数学毁灭武器》 作者:凯西·奥尼尔(强烈推荐!)
-
《隐形女性:为男性设计的世界中的数据偏见》 作者:卡罗琳·克里亚多-佩雷斯
-
《人工智能图谱》 作者:凯特·克劳福德
-
《人工智能伦理学》 作者:马克·科克尔伯赫
-
《数据女性主义》 作者:凯瑟琳·迪吉纳齐奥 和 劳伦·F·克莱因
论文
参考资料:
AI Now Institute. (2024 年 1 月 10 日). Ai now 2017 报告. ainowinstitute.org/publication/ai-now-2017-report-2
Belenguer, L. (2022). 人工智能偏见:探索歧视性算法决策模型及其在制药行业的可能机器中心解决方案应用。人工智能与伦理,2(4),771–787。 doi.org/10.1007/s43681-022-00138-8
Bolukbasi, T., Chang, K.-W., Zou, J., Saligrama, V., & Kalai, A. (2016, July 21). 男人与计算机程序员的关系就像女人与家庭主妇的关系?消除词向量的偏见。arXiv.org。 doi.org/10.48550/arXiv.1607.06520
Chakraborty, J., Majumder, S., & Menzies, T. (2021). 机器学习软件中的偏见:为什么?怎么做?应该怎么办?第 29 届 ACM 联合会议:欧洲软件工程会议与软件工程基础研讨会论文集。 doi.org/10.1145/3468264.3468537
Gutbezahl, J. (2017, June 13). 在分析中避免的 5 种统计偏差。商业洞察博客。 online.hbs.edu/blog/post/types-of-statistical-bias
Heaven, W. D. (2023a, June 21). 预测性警务算法存在种族歧视,它们需要被拆除。 MIT 科技评论。 www.technologyreview.com/2020/07/17/1005396/predictive-policing-algorithms-racist-dismantled-machine-learning-bias-criminal-justice/
Heaven, W. D. (2023b, June 21). 预测性警务依然存在种族歧视——无论它使用何种数据。MIT 科技评论。 www.technologyreview.com/2021/02/05/1017560/predictive-policing-racist-algorithmic-bias-data-crime-predpol/#:~:text=It%27s%20no%20secret%20that%20predictive,lessen%20bias%20has%20little%20effect.
Hellström, T., Dignum, V., & Bensch, S. (2020, September 20). 机器学习中的偏见——它有什么用?。arXiv.org。 arxiv.org/abs/2004.00686
人工智能系统中的历史偏见。澳大利亚人权委员会。(2020 年 11 月 24 日)。 humanrights.gov.au/about/news/media-releases/historical-bias-ai-systems#:~:text=Historical%20bias%20arises%20when%20the,by%20women%20was%20even%20worse.
Memarian, B., & Doleck, T. (2023). 人工智能(AI)与高等教育中的公平、问责、透明度和伦理(FATE):一项系统评审。计算机与教育:人工智能,5,100152. doi.org/10.1016/j.caeai.2023.100152
Obermeyer, Z., Powers, B., Vogeli, C., & Mullainathan, S. (2019). 解剖用于管理群体健康的算法中的种族偏见。科学,366(6464),447–453. doi.org/10.1126/science.aax2342
O'Neil, C. (2017). 数学毁灭武器:大数据如何加剧不平等并威胁民主。企鹅兰登书屋。
Roselli, D., Matthews, J., & Talagala, N. (2019). 管理人工智能中的偏见。2019 年全球信息网大会的伴随论文集。doi.org/10.1145/3308560.3317590
Suresh, H., & Guttag, J. (2021). 理解机器学习生命周期中伤害来源的框架。算法、机制与优化中的公平性与可访问性。doi.org/10.1145/3465416.3483305
van Giffen, B., Herhausen, D., & Fahse, T. (2022). 克服算法的陷阱与危机:机器学习偏见及其缓解方法的分类。商业研究杂志,144,93–106. doi.org/10.1016/j.jbusres.2022.01.076
Zhang, B. H., Lemoine, B., & Mitchell, M. (2018). 通过对抗学习减轻不必要的偏见。2018 年 AAAI/ACM 人工智能、伦理与社会会议论文集。doi.org/10.1145/3278721.3278779
马尔可夫决策过程中的不确定性:一种鲁棒的线性规划方法
马尔可夫决策过程(MDP)的鲁棒对偶理论推导:作为一个线性规划(LP)
·发表于 Towards Data Science ·8 分钟阅读·2024 年 9 月 18 日
--

图片由 ZHENYU LUO 提供,来源于 Unsplash
引言
马尔可夫决策过程(MDP)是顺序决策问题的基础,是强化学习的构建模块。它们建模了一个代理在必须执行一系列动作与其环境之间的动态互动。由于其在机器人技术、金融、运筹学和人工智能等领域的广泛应用,MDP 已在理论和实践中得到了广泛研究。
然而,现有的大部分 MDP 文献集中于理想化的情境,其中模型参数——如转移概率和奖励函数——假设已知且确定。在实践中,应用如策略迭代和价值迭代等常见方法需要对这些参数进行精确估计,这些估计通常来自于真实世界的数据。对数据的依赖带来了显著的挑战:估计过程本质上是嘈杂的,并且对数据稀缺、测量误差以及观察到的环境变异性等限制因素敏感。因此,当将标准的 MDP 方法应用于具有不确定性或不完整数据的问题时,其性能可能会显著下降。
在本文中,我们基于鲁棒优化(RO)文献提出了一个通用框架来解决这些问题。我们提供了一个鲁棒线性规划(RLP)公式化的 MDP,能够处理各种不确定性和对抗性扰动。
MDP 定义和线性规划(LP)公式化
让我们从给出 MDP 的正式定义开始:
A 马尔可夫决策过程 是一个五元组 (S, A, R, P, γ),满足:
-
S 是代理可以处于的 状态 的集合
-
A 是代理可以执行的 动作 的集合
-
R : S x A → R 是 奖励 函数
-
P 是 概率分布 的集合,定义为 P(s’|s,a) 是在状态 s 下,代理执行动作 a 时转移到状态 s’ 的概率。请注意,MDP 是马尔可夫过程,这意味着马尔可夫性质在转移概率中成立:P(Sₜ₊₁|S₀, A₀, …, Sₜ, Aₜ) = P(Sₜ₊₁|Sₜ, Aₜ)
-
γ ∈ (0, 1] 是 折扣因子。虽然我们通常处理折现问题(即 γ < 1),但所呈现的公式对无折扣的 MDP(γ = 1)同样有效*
然后我们定义 策略,即决定代理行为的规则:
策略 π 是一个 概率测度 ,定义为:π(a|s) 是在代理处于状态 s 时采取动作 a 的概率。
我们最终引入 价值函数,即 MDP 中代理的目标:
策略 π 的价值函数是该策略下期望的折现奖励,当从给定状态 s 开始时:

特别地,最优策略 π 的价值函数满足贝尔曼最优性方程:*

这产生了确定性的最优策略:

推导 MDP 的线性规划(LP)公式:
根据上述定义,我们可以从以下事实入手:任何满足的价值函数 V

是最优价值函数的上界。为了理解这一点,我们可以首先注意到这样的价值函数也满足:

我们识别出应用于 V 的价值迭代运算符:

即

另外,注意到 H* 运算符是递增的,我们可以迭代地应用它得到:

其中我们使用了 V* 是 H* 的不动点这一性质。
因此,找到 V* 的问题归结为找到 遵守上述方程的最紧上界 V,从而得到以下公式:

在这里,我们添加了一个与状态 s 的起始概率相关的权重项。我们可以看到,上述问题在线性形式下表示为 V,并且可以重写如下:

进一步的细节可以在 [1] 和 [2] 中找到。
线性规划的鲁棒优化
给定上述标准形式的线性规划,RO 框架假设输入(即成本向量和约束)中存在对抗噪声。为了建模这种不确定性,我们定义了一个不确定性集:

简而言之,我们想要找到所有线性规划的最小值,即对于不确定性集中的每一次出现。显然,这会导致一个完全无法处理的模型(可能是无穷多个线性规划),因为我们对 U 的形式没有做任何假设。
在解决这些问题之前,我们做出以下假设——不失一般性:
-
w 和 b 的不确定性等同于一个稍微修改过的线性规划中的约束不确定性——因此我们只考虑 c 的不确定性
-
对抗噪声按约束逐个应用,即对每个约束单独应用
-
鲁棒问题的约束形式为:

其中:\bar{c} 被称为标称约束向量(例如,通过某些估计得到),z 是不确定因素,Q 是一个固定的矩阵,直观上对应于噪声如何作用于约束向量的每个系数。Q 可以用来建模 c 的不同分量之间噪声的相关性。例如,更多细节和证明可以参见 [3]。
注意:我们略微滥用了符号,并省略了 (s, a) 下标以提高可读性——然而 c, \bar{c}, Q 和 z 都是对于给定状态和动作对的。
- 我们不再对不确定性集中的每个条目进行优化,我们优化 U 上的最坏情况。在只有约束的不确定性背景下,这意味着最坏情况的 U 必须是可行的
所有这些导致了以下问题的公式化:

在这一阶段,我们可以对 U 的形式做出一些假设,以便进一步简化问题:
-
虽然 z 可以是任意维度 L 的向量——因为 Q 将是一个 |S| x L 的矩阵——我们做出了一个简化假设,即 z 的大小为 |S|,并且 Q 是一个大小为 |S| 的方形对角矩阵。这将使我们能够分别对约束向量的每个系数建模对抗噪声(且噪声之间无相关性)
-
我们假设不确定性集是一个大小为 2d 的盒子,即 z 的每个坐标可以取自区间 [-d, d] 中的任何值。这等价于说 z 的 L∞ 范数小于 d
优化问题变为:

这等价于:

最后,仔细观察约束中的最大化问题,我们发现它具有封闭形式。因此,最终问题可以写成(带有盒式不确定性的线性规划的鲁棒对偶):

对上述公式的一些评论:
-
不确定性项消失了——鲁棒性通过额外的安全项引入
-
由于 L1 范数可以线性化,因此这是一个线性规划
-
上述公式不依赖于Q的形式——所作的假设将在下一部分中发挥作用
有兴趣的读者可以参见[3]以了解更多细节。
MDP 的 RLP 公式
从上述公式出发:

最后,将约束中的绝对值线性化得到:

我们注意到鲁棒性转化为约束中的额外安全项——考虑到c的不确定性(这主要转化为 MDP 的转移概率的不确定性)。
如前所述,考虑奖励的不确定性也可以通过类似的推导轻松完成。回到标准形式的线性规划,我们在约束条件的右侧添加了另一个噪声项:

经过与之前相似的推理,我们得到完整的线性规划:

再次类似于之前,对于奖励函数的额外鲁棒性转化为约束中的另一个安全项,这最终可能导致一个不太最优的值函数和策略,但能填充约束边界。这种权衡由Q和不确定性盒子* d *的大小共同控制。
结论
虽然这完成了作为线性规划的鲁棒马尔科夫决策过程(Robust MDP)的推导,但文献中已经提出了其他鲁棒 MDP 方法,例如参见[4]。这些方法通常采取不同的路径,例如直接推导出鲁棒策略评估算子——这具有与线性规划方法相比更好的复杂度优势。这在状态和动作空间较大时尤其重要。那么我们为什么要使用这样的公式呢?
-
RLP 公式使我们能够利用线性规划的所有理论性质。这意味着在问题可行且有界的情况下,保证存在解,并且能够使用对偶性理论和敏感性分析的已知结果。
-
线性规划方法允许轻松使用不同的不确定性集几何形状——有关详细信息,参见[3]
-
这种公式允许自然地集成 MDP 的附加约束,同时保持鲁棒性特性
-
我们还可以应用一些投影或近似方法(例如参见[5])来改进线性规划复杂度。
参考文献:
[1] M.L. Puterman, 《马尔可夫决策过程:离散随机动态规划》(1996),Wiley
[2] P. Pouppart, 《序列决策与强化学习》(2013),滑铁卢大学
[3] D. Bertsimas, D. Den Hertog, 《稳健与自适应优化》(2022),Dynamic Ideas
[4] W. Wiesemann, D. Kuhn, B. Rustem, 《稳健马尔可夫决策过程》(2013),INFORMS
[5] K. Vu, P.-L. Poirion, L. Liberti, 《线性规划的随机投影》(2018),INFORMS
不确定性量化及其重要性
如何通过三行代码提升你的机器学习模型
·发表于 Towards Data Science ·8 分钟阅读·2024 年 4 月 24 日
--

将点预测转化为预测区域,以量化模型的不确定性,给我们提供更多信息(图片来自作者)。
预测模型经过训练,旨在做出准确预测并给出点预测结果。
假设我们想买一套房子。在此之前,我们希望验证广告中提到的 40 万欧元价格是否合理。为此,我们使用一个模型,根据房间数、房屋大小和位置来预测该房子价值 500,232.12 欧元。
我们应该买这套房子吗?看起来像是个不错的交易,不是吗?但如果模型预测的价格是 340,021.34 欧元,我们的决策会不会不同?可能会吧?
那么,我们在做出购买这套房子的决策时应该信任模型吗?这套房子价值 500,232.12 欧元或 340,021.34 欧元的概率有多大?
由于概率接近零,我们应该将预测视为对真实结果的最佳猜测。此外,当我们根据模型做出决策时,也存在风险。
在购房的例子中,我们可能支付过多,导致财务损失。如果一辆自动驾驶汽车没有将过马路的人识别为障碍物,结果可能是致命的。
时间序列预测中的不确定性量化
深入探讨 EnbPI,这是一种用于时间序列预测的符合预测方法
·发表于 Towards Data Science ·阅读时间 10 分钟 ·2024 年 12 月 9 日
--

作者提供的图片
我最近的许多文章都围绕着如何了解模型对其预测的信心展开。如果我们知道预测的不确定性,我们可以做出更有信息支持的决策。我向你展示了如何使用符合预测(Conformal Prediction)来量化模型的不确定性。我写了关于分类和回归问题的符合预测方法的文章。
或者为什么你不应该信任predict_proba方法
如何使用符合量化回归(Conformalized Quantile Regression)
对于这些方法,我们假设观测的顺序不重要,即数据是可交换的。这对于分类和回归问题是合理的。然而,这一假设在时间序列问题中并不成立。在这里,观测的顺序通常包含重要的信息,例如趋势...
揭开欧盟人工智能法案的面纱
欧盟已经开始规范机器学习。这个新法案对数据科学家意味着什么?
·发布于数据科学之路 ·9 分钟阅读·2024 年 3 月 14 日
--

图片由Hansjörg Keller提供,来自Unsplash
欧盟人工智能法案刚刚通过了欧洲议会。你可能会想,“我不在欧盟,关我什么事,”但是相信我,这实际上比你想象的对全球的数据科学家和个人更为重要。欧盟人工智能法案是一个重要的举措,旨在规范和管理在欧盟内部或影响欧盟公民的某些机器学习模型的使用,并且其中包含了一些严格的规则和严重的违法惩罚。
这项法律有很多关于风险的讨论,这里的风险指的是对欧盟公民健康、安全和基本权利的威胁。它不仅仅是某种理论上的人工智能末日风险,而是关于日常生活中,因你所构建的模型或销售的产品,使得真实人的生活在某种程度上变得更糟。如果你熟悉今天关于人工智能伦理的许多辩论,那么这应该听起来很熟悉。嵌入式歧视和侵犯人们的权利,以及对人们健康和安全的危害,都是当前人工智能产品和公司面临的严重问题,而这项法律是欧盟保护人们的首次努力。
定义人工智能
常读者都知道,我总是希望“人工智能”有清晰的定义,对于模糊不清的定义,我总是感到不满。在这种情况下,该法案将“人工智能”定义如下:
一种机器系统,设计用于在不同的自主程度下操作,可能在部署后展现出适应性,并且为了明确或隐含的目标,从接收到的输入中推断如何生成输出,如预测、内容、推荐或决策,这些输出可能影响物理或虚拟环境。
那么,这到底意味着什么呢?我的理解是,产生输出并用于影响世界(尤其是人们的物理或数字状况)的机器学习模型,属于这个定义范围。它不需要实时适应或自动重训,尽管如果它这样做,也会受到涵盖。
但如果你正在构建用于执行以下任务的机器学习模型……
-
决定人们的风险等级,如信用风险、违法或犯罪风险等
-
确定人们在线上看到什么内容,如信息流或广告
-
针对相同产品,向不同人展示不同价格
-
推荐最佳治疗、护理或服务给人们
-
推荐人们是否采取某些行动
如果你的模型影响到任何欧盟公民,这些都将受到法律的约束——这些仅仅是举几个例子。
分类 AI 应用
然而,并非所有 AI 都是相同的,法律也承认这一点。某些 AI 应用将被完全禁止,而其他的则会受到更高程度的审查和透明度要求。
不可接受风险的 AI 系统
这些系统现在被称为“不可接受风险的 AI 系统”,并且是明确不允许的。法律的这一部分将在六个月后首先生效。
-
使用行为操控或欺骗手段让人们做出他们平时不会做的事情
-
针对年龄或残疾等因素,改变人们的行为和/或利用他们
-
生物识别分类系统,用于尝试根据高度敏感的特征对人进行分类
-
导致社会评分或差别待遇的个性特征评估
-
用于执法的“实时”生物识别身份识别,除非是针对特定的使用场景(寻找失踪或被绑架的人、迫在眉睫的生命或安全威胁/恐怖主义,或某一特定犯罪的起诉)
-
预测性警务(预测人们将来会犯罪)
-
广泛的人脸识别/生物识别扫描或数据抓取
-
在教育或工作中,未经医学或安全目的推测情绪的系统
这意味着,例如,你不能建立(或被迫提交)一个筛查系统,用来判断你是否“足够开心”来获得零售工作。人脸识别仅限于特定、选定的具体情况。(Clearview AI 绝对是这类例子之一)。预测性警务,这是我在学术界早期工作时参与的内容,现在我非常后悔,这也是不允许的。
“生物特征分类”这一点是指那些根据政治、宗教、哲学信仰、性取向、种族等敏感特征对人群进行分组的模型。使用人工智能试图将人们根据这些类别进行标签化,是法律明确禁止的。
高风险人工智能系统
另一方面,这个列表涵盖了那些没有被禁用但受到高度审查的系统。所有这些系统都有具体的规则和规定,以下将会描述。
-
医疗设备中的人工智能
-
车辆中的人工智能
-
情感识别系统中的人工智能
-
警务中的人工智能
这排除了上述描述的特定使用场景。所以,情感识别系统可能被允许使用,但不能在工作场所或教育领域使用。医疗设备和车辆中的人工智能被认为对健康和安全存在严重风险或潜在风险,这一点是有充分理由的,必须非常小心地推进。
其他
剩下的两个类别是“低风险人工智能系统”和“通用人工智能模型”。通用模型比如 GPT-4、Claude 或者 Gemini——这些系统有着非常广泛的应用场景,通常会被用于其他下游产品中。因此,GPT-4 本身并不属于高风险或禁用类别,但你将其嵌入使用的方式受到此处描述的其他规则的限制。你不能将 GPT-4 用于预测性警务,但 GPT-4 可以用于低风险场景。
透明度与审查
假设你正在开发一个高风险的人工智能应用,并且你希望遵循所有规则并获得批准,如何开始呢?
对于高风险人工智能系统,你将负责以下事项:
-
保持并确保数据质量:你在模型中使用的数据是你的责任,因此你需要仔细筛选和管理它。
-
提供文档和可追溯性:你的数据来源是什么,你能证明吗?你能展示对任何更改或编辑的记录吗?
-
提供透明度:如果公众正在使用你的模型(比如聊天机器人)或模型是你产品的一部分,你必须告知用户这一点。不能假装模型只是客服热线或聊天系统中的一个真人。这一点实际上适用于所有模型,包括低风险模型。
-
使用人工监督:仅仅说“模型表示……”是行不通的。人类将对模型结果负责,最重要的是,如何使用这些结果。
-
保护网络安全和健壮性:你需要确保模型在面对网络攻击、数据泄露和无意的隐私侵犯时保持安全。由于代码漏洞或你未修复的安全漏洞导致模型出错或被黑客攻击,最终的责任将由你承担。
-
遵守影响评估:如果你在构建高风险模型,你需要对其可能产生的影响进行严格评估(即使你并无意为之),尤其是对用户或公众的健康、安全和权利。
-
对于公共实体,需在公共欧盟数据库中注册:这个注册表是作为新法案的一部分创建的,提交要求将适用于“公共当局、机构或组织”——主要是政府机构,而非私人企业。
测试
该法律还指出,如果你正在开发一个高风险的人工智能解决方案,你需要有一种方法来测试它,以确保你遵循指导方针,因此一旦获得知情同意,就允许进行测试。我们来自社会科学领域的人会觉得这很熟悉——它类似于获得机构审查委员会的批准来进行研究。
效力
该法律有分阶段实施:
-
6 个月后,对不可接受风险的人工智能的禁令生效
-
12 个月后,通用人工智能治理生效
-
24 个月后,法律中的所有剩余规则生效
注意:该法律不涵盖纯粹的个人非职业活动,除非它们属于前面列出的禁用类型,所以你的小型开源副项目不太可能构成风险。
处罚
那么,如果你的公司未遵守法律,并且影响到欧盟公民,会发生什么呢?法律中有明确的处罚条款。
如果你做了上述描述的禁用形式的人工智能:
- 最高3500 万欧元的罚款,或者如果你是企业,上年度全球收入的 7%(以较高者为准)
不在禁用范围内的其他违规行为:
- 最高1500 万欧元的罚款,或者如果你是企业,上年度全球收入的 3%(以较高者为准)
对当局就以下事项撒谎:
- 最高750 万欧元的罚款,或者如果你是企业,上年度全球收入的 1%(以较高者为准)
注意:对于中小型企业,包括初创公司,罚款将以较低者为准,而非较高者。
数据科学家应该做什么?
如果你正在根据该法案的定义构建使用人工智能的模型和产品,你应该首先熟悉法律及其要求。即使你今天没有影响到欧盟公民,这也可能对该领域产生重大影响,你应该意识到这一点。
然后,注意你自己企业或组织中可能的违规行为。你有时间发现并修正问题,但被禁的人工智能形式将首先生效。在大型企业中,你可能会有法务团队,但不要认为他们会为你处理所有这些事情。你是机器学习方面的专家,因此你是企业发现和避免违规行为的重要一环。你可以使用欧盟人工智能法案网站上的合规检查工具来帮助你。
今天在企业和组织中使用的许多 AI 形式是新法律所不允许的。我在上文提到过 Clearview AI,以及预测性警务。情感测试也是一个非常真实的现象,求职面试过程中人们常常会受到这种测试(我邀请你搜索“情感测试求职”并看到众多公司提供此服务),此外,还有高频率的面部或其他生物特征数据收集。我们所有人都将非常关注这个问题,看到执法如何展开,一旦法律全面生效,情况将非常有趣且重要。
我想花一点时间说几句,关于我一位亲爱的朋友,他在这周与癌症进行了艰苦的斗争后去世。Ed Visel,在线昵称为alistaire,是一位杰出的数据科学家,他将大量的时间和才华奉献给了更广泛的数据科学社区。如果你在过去十年里在 StackOverflow 上问过 R 问题,可能他曾帮助过你。他总是耐心和善良,因为像我一样,作为一名自学成才的数据科学家,他深知从零开始学习这些东西的艰难,因此始终保持着同理心。

作者照片
我有幸与 Ed 共事了几年,并且成为了他的朋友多年。我们失去了他,实在是太早了。我请求你们在他的记忆中帮助一位朋友或同事解决一个技术问题。没有他,数据科学社区将变得不再那么友好。
此外,如果你曾在线或亲自认识 Ed,家属已请求捐款给Severson Dells 自然中心,这是一个对他意义非凡的地方。
在此阅读更多我的内容 www.stephaniekirmer.com.
参考文献和进一步阅读
已将所有“章节”添加到 AI 法案探索器中。添加了缺失的条款:3、66、68、68a(1)、68f、68g、68h、68i、68j、68k……
artificialintelligenceact.eu](https://artificialintelligenceact.eu/ai-act-explorer/?source=post_page-----22b10f946174--------------------------------)
欧盟委员会现在正在支持欧盟理事会和欧洲议会完成……
人工智能法案 [## 欧洲达成具有里程碑意义的人工智能法规协议
通过这一政治协议,欧盟朝着成为全球首个实施人工智能法律的主要大国迈出了步伐。
大型语言模型中的未充分训练和未使用标记
以 GPT-2 Small 为例,探讨未使用和未充分训练的标记的存在及其识别技术
·发表于Towards Data Science ·7 分钟阅读·2024 年 10 月 1 日
--

图像由deepai根据文本生成:LLM 的未充分训练标记化
引言:未使用和未充分训练的标记
我们在探索基于变换器的大型语言模型(如 ChatGPT)时观察到存在未使用和未充分训练的标记,其中标记化和模型训练是两个独立的过程。未使用的标记和未充分训练的标记具有以下不同的行为:
-
未使用的标记存在于大型语言模型的词汇中,并在训练过程中被包含,但没有被充分看到。
-
未充分训练的标记可能存在于大型语言模型的词汇中,也可能不存在,并且在训练数据中完全没有出现。
理想情况下,这两种类型的标记应该具有极低的生成概率,或者等效地,具有极其负的 logit 值,这样它们就不应该由大型语言模型生成。然而,在实际中,用户仍然发现一些未使用的标记具有重要的 logit 值,且模型有时不幸会预测到它们。这可能会导致大型语言模型出现不良行为。
让我们考虑一个大型语言模型,它由于一些在模型训练过程中从未训练过的标记,意外地生成了无意义或不恰当的文本。这样的情况有时会导致严重后果,例如幻觉现象,导致准确性和适当性缺失。
我们认为这个问题是由于令牌化和 LLM 训练过程之间的分离。通常,这两个方面从未一起训练过,确实发生了模型词汇表中的某个令牌未能被训练,并且随机出现在模型的输出中。
在本文中,我们将通过一些简单的实验,使用 GPT-2 Small 演示未使用令牌的存在,包括那些训练不足的令牌。我们还将讨论识别训练不足令牌的技术。
未使用令牌的存在:关于 GPT-2 Small 的实验
在许多 LLM 中,包括我们实验所用的 GPT-2 Small,存在未使用的令牌,即在 LLM 的词汇表中存在的令牌,这些令牌在训练过程中被包含在内,但没有被充分看到。
在以下示例中,我们给出了证明未使用令牌存在的两个案例:
示例 1:重现未使用的令牌
在这个实验中,我们旨在展示 GPT-2 Small 在重复未使用的令牌时的困难,即使是在非常简单的指令下。现在让我们考虑以下未使用的令牌:"ú" (\u00fa)。我们希望指示 GPT-2 Small 准确地重复输入给定的令牌。
这是一个非常简单的任务:对于给定的输入令牌"ú",模型需要给出相同的令牌作为输出。
from transformers import GPT2LMHeadModel, GPT2Tokenizer
# Load pre-trained model (GPT-2 Small) and tokenizer
model_name = "gpt2" # GPT-2 Small (124M parameters)
model = GPT2LMHeadModel.from_pretrained(model_name)
tokenizer = GPT2Tokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token# Configure the model's `pad_token_id`
model.config.pad_token_id = model.config.eos_token_id# Encode a prompt to generate text
token= "\u00fa"
prompt_text = "Rpeats its input exactly" + ', '.join([f"Input: {token}, Output: {token}" for _ in range(20)])+ f"Input: {token}, Output: "
inputs = tokenizer(prompt_text, return_tensors="pt", padding=True)# Generate text with attention mask
output = model.generate(
inputs['input_ids'],
attention_mask=inputs['attention_mask'], # Explicitly pass attention_mask
max_new_tokens=10, # Maximum length of the generated text
num_return_sequences=1, # Number of sequences to return
no_repeat_ngram_size=2, # Prevent repeating n-grams
do_sample=True, # Enable sampling
top_k=50, # Limit sampling to top k choices
top_p=0.95, # Use nucleus sampling
)
如上面的代码所示,我们设计了一个作为 n-shot 示例的提示,指示模型给出完全相同的特定令牌"ú"。我们看到的是,模型未能预测到这个令牌:它给出了一些杂乱无章的文本,如"Output: - ß, *- *-, "。相比之下,当我们用常见的令牌如"a"进行相同任务测试时,模型成功地预测了正确的输出,显示出常见令牌和未使用令牌之间在表现上的显著差异。
示例 2:令牌重复
我们现在考虑未使用令牌的范围,从索引 177 到 188,这是 GPT2 [1]的未使用令牌范围。
我们现在的目标是生成重复的随机令牌序列,并评估模型在重复序列上的表现。正如我在之前的博客文章中讨论的,“如何解释 GPT-2 Small:在重复令牌预测中的机械解释性”,基于变换器的 LLM 在识别和预测重复模式方面具有强大的能力,即使对于像 GPT-2 Small 这样的小模型也是如此。
例如,当模型遇到一个‘A’时,它会在嵌入空间中寻找之前出现的‘A’或与‘A’紧密相关的令牌。然后它识别下一个令牌‘B’,并预测紧随‘A’之后的令牌将是‘B’或嵌入空间中与‘B’相似的令牌。
我们首先定义了一个函数generate_repeated_tokens,它生成一个序列,其中后半部分重复前半部分。
import torch as t
from typing import Tuple
# Assuming HookedTransformer and other necessary components are defined elsewhere.
t.manual_seed(42)
def generate_repeated_tokens(
model: HookedTransformer, seq_len: int, batch: int = 1
) -> Int[Tensor, "batch full_seq_len"]:
'''
Generates a sequence of repeated random tokens
Outputs are:
rep_tokens: [batch, 1+2*seq_len]
'''
bos_token = (t.ones(batch, 1) * model.tokenizer.bos_token_id).long() # generate bos token for each batch
rep_tokens_half = t.randint(177, 188, (batch, seq_len), dtype=t.int64)
rep_tokens = t.cat([bos_token, rep_tokens_half, rep_tokens_half], dim=-1).to(device)
return rep_tokens
接下来,我们定义了run_and_cache_model_repeated_tokens函数,该函数在生成的重复标记上运行模型,返回 logits 并缓存激活值。我们在这里只使用 logits。
def run_and_cache_model_repeated_tokens(model: HookedTransformer, seq_len: int, batch: int = 1) -> Tuple[t.Tensor, t.Tensor, ActivationCache]:
'''
Generates a sequence of repeated random tokens, and runs the model on it, returning logits, tokens and cacheShould use the `generate_repeated_tokens` function above
Outputs are:
rep_tokens: [batch, 1+2*seq_len]
rep_logits: [batch, 1+2*seq_len, d_vocab]
rep_cache: The cache of the model run on rep_tokens
'''
rep_tokens = generate_repeated_tokens(model, seq_len, batch)
rep_logits, rep_cache = model.run_with_cache(rep_tokens)
return rep_tokens, rep_logits, rep_cache
现在,我们使用定义好的run_and_cache_model_repeated_tokens函数运行模型,生成标记和关联的 logits,使用以下代码:
seq_len = 25
batch = 1
(rep_tokens, rep_logits, rep_cache) = run_and_cache_model_repeated_tokens(gpt2_small, seq_len, batch)
rep_cache.remove_batch_dim()
rep_str = gpt2_small.to_str_tokens(rep_tokens)
gpt2_small.reset_hooks()
log_probs = get_log_probs(rep_logits, rep_tokens).squeeze()Copy co
在运行模型后,我们分析了预测标记的 log 概率,分为两部分对重复序列的 log 概率进行分析。我们观察到标记序列的 log 概率均值分别为:前半部分为-17.270,后半部分为-19.675,索引范围为 177 到 188。

作者提供的图片:重复标记的 log 概率范围在 177 到 188 之间
另一方面,使用常见标记范围进行相同实验会得到不同的结果:当检查标记索引 100 到 110 时,我们观察到后半部分的表现显著更好,log 概率为-0.971,相比之下前半部分的 log 概率为-7.327。

作者提供的图片:重复标记的 log 概率范围在 100 到 111 之间
训练不足的标记
如果所有未使用的标记具有显著的负 logits,那么 LLM 的世界理想情况下会少一些惊讶,因此模型也就不会生成奇怪的文本。
不幸的是,现实要复杂得多。由于 tokenizer 的创建和 LLM 的训练并不是同时进行的,这导致了有时会出现训练不足的标记,而这些标记恰是 LLM 产生意外行为的罪魁祸首。
一个训练不足标记的例子是:_SolidGoldMagikarp[1],它有时出现在 ChatGPT 的输出中。现在我们想证明 GPT-2 Small 模型中也存在训练不足的标记。
示例:再现未使用的标记
在我们以前使用 GPT-2 Small 模型再现未使用标记的实验中,我们证明了标记"ú"几乎没有被模型生成的机会。
现在,我们在运行模型后切片 logits 张量,以隔离未使用标记的输出,标记索引范围从 177 到 188:
sliced_tensor = gpt2_logits[0, :, 177:188]
有趣的是,我们观察到在这个“未使用”范围内,某些标记的 logit 值约为-1.7,这意味着一些未使用的标记被生成的概率大约为 0.18。
这一发现突出了模型可能将非忽略性的概率分配给一些未使用的标记,尽管它们在大多数上下文中很少被使用。
识别训练不足的标记
近年来,研究人员提出了自动识别大语言模型(LLMs)中训练不足的标记(tokens)的方法。该领域的研究包括 Watkins 和 Rumbelow(2023 年)以及 Fell(2023 年)的工作,其中一种非常有趣的识别训练不足标记的方法是分析模型生成的输出嵌入(E_{out}):
该方法计算未使用的标记的平均嵌入向量,并使用余弦距离来衡量该向量与模型中所有标记的嵌入向量的相似度。因此,余弦距离接近平均嵌入向量的标记被标记为潜在的训练不足标记。更多细节请参见[1]。
结论
总结来说,这篇博客文章讨论了 LLM 中的训练不足标记。我们通过 GPT-2 Small 做了一些实验,说明训练不足的标记可能意外地影响模型输出,有时会产生不可预测和不希望出现的行为。最近的研究提出了相应的检测训练不足标记的方法。对于那些对我的实现有兴趣的人,您可以查看我附带的笔记本。
参考文献
[1] Land, S., & Bartolo, M. (2024). Fishing for Magikarp: 自动检测大型语言模型中的训练不足标记。arXiv。 doi.org/10.48550/arXiv.2405.05417。
[2] Jessica Rumbelow 和 Matthew Watkins. 2023. SolidGoldMagikarp(加上,提示生成)。博客文章。
[3] Martin Fell. 2023. 寻找更多 ChatGPT / GPT-3.5 / GPT-4 “不可言说”的故障标记。博客文章。
了解数据仓库:查询性能
解读 SQL 查询性能:数据仓库和数据库管理系统中的实用分析
·发布于Towards Data Science ·7 分钟阅读·2024 年 1 月 8 日
--

图片来自Manuel Geissinger在Pexels上的照片
与 Python 和其他命令式编程语言不同,命令式编程语言需要按步骤详细说明算法以进行优化,SQL 是一种声明式编程语言,关注的不是操作的顺序,而是表达你想要实现的逻辑。查询在数据库中的执行方式取决于数据库系统本身,尤其是一个名为查询计划器(或优化器)的组件,它决定了执行查询的最佳方式。这就是为什么几乎相同的查询在数据仓库与传统数据库管理系统中执行方式差异如此之大。
对于大多数数据工作者来说,通常不需要过于关注这种差异,只要查询能够检索到正确的数据即可。然而,当创建交互式仪表盘或机器学习管道时,情况就不同了。在这些情况下,频繁执行的查询会显著影响查询效率和成本。设计良好的查询不仅能节省用户加载数据和指标的时间,还能为公司节省数千美元的 BigQuery 或 Snowflake 账单费用。
今天,我们将专注于一个常见的使用案例:比较多种查询语法和数据库。我们将看到并理解这些数据库在获取和计算数据时的不同处理方式。
这里有一个常见的问题:如何查找去年活跃的医生?假设有一个“医生”表记录了医生的信息,还有一个“病人入院”表记录了患者被医生接收的情况。目标是筛选出在过去一年中至少有一次患者入院的医生(这可以是机器学习管道或互动仪表板中的动态时间段)。
实际上,写这个查询有三种常见的方法:EXISTS、IN 和 JOIN。我们将对它们进行分析,并在 Bigquery 和 PostgreSQL 上运行实验,以验证我们的分析。
第一种方法:IN
对于 Python 用户来说,IN 操作符可能是最直观的方法。这种方法首先筛选出去年入院的记录,然后检查医生是否出现在这些记录中。我们还将测试添加 DISTINCT 是否能提高性能。
SELECT d.*
FROM `tool-for-analyst.richard_workspace.doctors` d
WHERE d.doctor_id IN (
SELECT doctor_id
FROM `tool-for-analyst.richard_workspace.patient_admissions` admissions
WHERE admissions.Admission_Date BETWEEN '2023–01–01' AND '2023–12–31'
);
第二种方法 EXISTS:
另一种方法是使用 EXISTS 操作符,它会筛选结果,仅包含那些子查询返回至少一条记录的结果。EXISTS 操作基于“半连接”的概念,这意味着它实际上并不会对右侧执行连接;相反,它仅检查连接是否会为任何给定元组产生结果。当找到一个匹配时,它会停止。这可能会带来一些性能优势。
SELECT d.*
FROM `tool-for-analyst.richard_workspace.doctors` d
WHERE EXISTS (
SELECT 1
FROM `tool-for-analyst.richard_workspace.patient_admissions` pa
WHERE pa.doctor_id = d.doctor_id
AND pa.Admission_Date BETWEEN '2023–01–01' AND '2023–12–31'
)
第三种方法:
第三种方法涉及使用 JOIN,这是关系型数据库哲学中最经典的方法。论坛中经常有关于何时过滤以及是否使用子查询或公共表表达式(CTE)的争论。我们也将这些考虑因素纳入了实验。
在子查询中进行过滤后再执行 JOIN
SELECT d.doctor_id, name, Hospital, Age, Gender
FROM `tool-for-analyst.richard_workspace.doctors` d
INNER JOIN (
SELECT DISTINCT doctor_id
FROM `tool-for-analyst.richard_workspace.patient_admissions`
WHERE Admission_Date BETWEEN '2023–01–01' AND '2023–12–31'
) admissions
ON d.doctor_id = admissions.doctor_id;
在 JOIN 后进行过滤和 GROUP BY
SELECT d.doctor_id, d.name, d.Hospital, d.Age, d.Gender
FROM `tool-for-analyst.richard_workspace.doctors` d
INNER JOIN `tool-for-analyst.richard_workspace.patient_admissions` pa
ON d.doctor_id = pa.doctor_id
WHERE pa.Admission_Date BETWEEN '2023–01–01' AND '2023–12–31'
GROUP BY d.doctor_id, d.name, d.Hospital, d.Age, d.Gender;
CTE 过滤器在 JOIN 之前
WITH filtered_admissions AS(
SELECT DISTINCT doctor_id
FROM `tool-for-analyst.richard_workspace.patient_admissions` admissions
WHERE admissions.Admission_Date
BETWEEN '2023–01–01' AND '2023–12–31'
)
SELECT d.*
FROM `tool-for-analyst.richard_workspace.doctors` d
JOIN filtered_admissions
ON d.doctor_id = filtered_admissions.doctor_id;
现在我们有六个查询要测试。它们都从数据库中获取相同的结果,但在逻辑或语法上有些微差异。
Q1: IN
Q2: 在子查询中使用 DISTINCT 的 IN
Q3: EXISTS
Q4: 使用子查询进行 JOIN,并过滤时间范围
Q5: 在任何过滤之前执行 JOIN,然后使用 GROUP BY 去重
Q6: 使用 CTE 进行 JOIN,并过滤时间范围
实验结果:

作者提供的图片
我们在生成的测试数据集上执行了每个查询 10 次,每次测试时将时间范围向前推移 1 天。通过使用 BigQuery 执行详情和 PostgreSQL 中的 EXPLAIN ANALYZE 命令,我们获得了关于执行时间和计划的详细信息。测试结果非常明确。如果这是一个真实世界的用例,我们可以简单地选择性能最佳的选项,然后继续。不过,在这篇博客中,我们将深入探讨并提出一个问题:为什么?
深入分析查询计划:
答案可以在执行计划中找到,它揭示了数据库引擎实际计算查询的方式。
Bigquery:
Q1 'IN' 和 Q3 'EXISTS' 的执行计划完全相同。两步执行首先在子查询中进行了筛选,然后使用 SEMI JOIN 来识别至少有一个病人入院的医生。这正好是我们之前提到的一个完美例子:SQL 是一种声明式语言,它描述了你需要什么,而 BigQuery 会决定如何执行。即使 SQL 逻辑在解决问题的方式上有所不同,BigQuery 也意识到它们需要相同的结果,并决定使用相同的执行方式进行优化。

图片由作者提供
在 IN 子查询中添加 DISTINCT 导致性能大幅下降。观察到添加一个 DISTINCT 会对查询运行速度产生如此显著的影响,实在是令人很感兴趣。当我们查看查询执行计划时,可以看到单个 DISTINCT 会导致查询执行中多出两步。这导致在过程中保存了更多的临时表,进而导致执行时间显著变慢。
在三种 JOIN 方法中,令人惊讶的是 Q5 'JOIN before filter' 展现了最佳性能,而试图优化筛选和 JOIN 顺序的两种方法,Q4 'JOIN with subquery' 和 Q6 'JOIN with CTE',表现较差。检查规划器时,似乎 BigQuery 实际上识别出在 JOIN 之前执行筛选可以优化效率。然而,当我们尝试通过强制筛选先于 JOIN 来手动控制顺序时,执行计划中出现了更多的步骤,导致执行时间显著变慢。有趣的是,子查询和 CTE 方法拥有完全相同的执行计划,这也与 Q2 'IN with DISTINCT' 的计划非常相似。唯一的区别是,在最后一步,它使用了 INNER JOIN 而不是 SEMI JOIN。
PostgreSQL:
关于 Postgres,我们分析的六个查询之间的查询时间差异相对较小。这可能是因为测试数据集的规模不足以显著突出差异。随着数据集规模的增大,方法之间的性能差异可能会变得更加明显。
我们的分析基于 'EXPLAIN ANALYZE' 的结果。这个工具对于理解 PostgreSQL 查询的性能特征非常宝贵。'EXPLAIN' 提供了 PostgreSQL 查询规划器为给定语句生成的执行计划,而 'ANALYZE' 选项则实际执行语句,从而允许更准确地评估性能。
Q1 'IN' 和 Q3 'EXISTS' 拥有相同的执行计划,并且是最低成本的。与 BigQuery 类似,PostgreSQL 也识别出这两个查询需要相同的数据,并为它们进行了优化。
Q2、Q4 和 Q6 的执行计划完全相同,只是成本略高。尽管这些查询在逻辑或语法上有所不同,但 Postgres 的查询计划器决定执行相同的操作:过滤 -> 按(DISTINCT)分组 -> JOIN。

图片来源:作者
Q5 ‘JOIN before filter’有着成本最高的执行计划。尽管 PostgreSQL 的查询计划器仍然设法在 JOIN 之前应用了过滤器,但去重过程是应用到较大的表上,导致了更高的成本。
结论:
在我们的实验中,像是在 JOIN 之前强制加上过滤器或为 IN 操作符添加 DISTINCT 选项的做法并没有提高我们的查询性能;相反,它们使查询变得更慢。将 BigQuery 与 Postgres 进行比较,可以明显看出它们各自有自己的优势和特点。它们的查询计划器也针对不同的目标进行了优化,采用了不同的策略。
话虽如此,在像 SQL 这样的声明式语言中优化效率不仅仅取决于你的查询本身。数据库引擎如何解释、规划和执行查询同样重要。这个过程在很大程度上取决于数据库的设计,以及数据的结构和索引。
我们为博客进行的实验特定于某些特定的用例和数据集。了解性能的最有效方式是运行你自己的查询,检查查询执行计划,并查看它将如何执行。不要基于理论假设过度优化。实际的测试和观察应该始终是查询优化的指导原则。
一次性理解 REINFORCE、Actor-Critic 和 PPO
使用策略梯度算法的损失函数作为理解各种强化学习算法的关键:REINFORCE、Actor-Critic 和 PPO,这些是理解用于构建 ChatGPT 的“人类反馈强化学习”(RLHF)算法的理论准备。
·发表于Towards Data Science ·44 分钟阅读·2024 年 7 月 24 日
--

图片来自Unsplash
学习强化学习可能令人沮丧,因为这个领域充满了令人困惑的术语和细微差别的算法。
我曾一度感到困惑,直到有一天,我的伟大同事Peter Vrancs迅速为我写下了策略梯度算法 REINFORCE 的损失函数推导。通过这个推导,本文将以下算法联系在一起:
-
REINFORCE
-
方差减少的优势概念,以及 Actor-Critic 算法
-
近端策略优化(PPO)
即使有很多文章涵盖这些算法,本文提供了一个独特的角度,将它们一次性学习,帮助你节省学习时间!
在我看来,理解这三种算法是理论上的基础……
一劳永逸地理解 SQL 窗口函数
理解窗口函数的逐步指南
·发表于Towards Data Science ·阅读时间:9 分钟·2024 年 5 月 1 日
--

窗口函数是编写既高效又易于理解的 SQL 代码的关键。了解它们的工作原理以及何时使用它们,将为你打开解决报告问题的新思路。
本文的目标是以易于理解的方式逐步解释 SQL 中的窗口函数,让你不必仅仅依赖于死记硬背语法。
我们将涵盖以下内容:
-
关于你应该如何看待窗口函数的解释
-
通过逐渐增加难度的多个例子来进行讲解
-
通过一个具体的真实案例将所学知识付诸实践
-
回顾我们所学的内容
我们的数据集很简单,包含 2023 年两地区的六行收入数据。

窗口函数是子分组
如果我们对这个数据集运行 GROUP BY 并对每个地区的收入进行求和,结果将是显而易见的,对吧?这将只剩下两行数据,每个地区一行,然后是收入的总和:

我希望你理解窗口函数的方式非常类似于这个例子,但与其减少行数,聚合操作将在“后台”运行,值将被添加到现有的行中。
首先,一个例子:
SELECT
id,
date,
region,
revenue,
SUM(revenue) OVER () as total_revenue
FROM
sales

注意,我们没有使用 GROUP BY,数据集保持不变。尽管如此,我们还是能够得到所有收入的总和。在我们更深入地理解它是如何工作的之前,让我们先简要谈谈完整的语法,然后再开始构建我们的知识。
窗口函数语法
语法如下:
SUM([some_column]) OVER (PARTITION BY [some_columns] ORDER BY [some_columns])
拆解每个部分,得到如下内容:
-
一个聚合或窗口函数:
SUM、AVG、MAX、RANK、FIRST_VALUE -
OVER关键字,表示这是一个窗口函数 -
PARTITION BY部分,定义了分组 -
ORDER BY部分,定义是否为运行函数(我们稍后会讲解)
不要急着理解这些是什么意思,因为当我们通过示例时,这些内容会变得清晰起来。现在只需要知道,为了定义一个窗口函数,我们将使用 **OVER** 关键字。正如我们在第一个示例中看到的那样,这就是唯一的要求。
一步步构建我们的理解
进入一些真正有用的内容,我们现在将在函数中应用一个分组。初始计算会被保留下来,向你展示我们可以同时运行多个窗口函数,这意味着我们可以在同一个查询中同时进行不同的聚合,而不需要使用子查询。
SELECT
id,
date,
region,
revenue,
SUM(revenue) OVER (PARTITION BY region) as region_total,
SUM(revenue) OVER () as total_revenue
FROM sales

如前所述,我们使用 **PARTITION BY** 来定义我们的分组(窗口),这些分组将被我们的聚合函数使用! 所以,在保持数据集不变的情况下,我们得到了:
-
每个区域的总收入
-
整个数据集的总收入
我们也不受限于单一分组。类似于 GROUP BY,我们可以按区域和季度对数据进行分区,例如:
SELECT
id,
date,
region,
revenue,
SUM(revenue) OVER (PARTITION BY
region,
date_trunc('quarter', date)
) AS region_quarterly_revenue
FROM sales

在图像中,我们看到同一区域和季度的唯一两个数据点被归为一组!
到这里,我希望你已经清楚我们如何将其视为执行一个 GROUP BY 操作,但在原地进行,而不减少数据集的行数。当然,我们并不总是希望这样做,但并不罕见会看到有人对数据进行分组,然后再将其与原始数据集合并,这样会使本可以是一个简单窗口函数的查询变得复杂。
继续讲解 ORDER BY 关键字。这个关键字定义了一个运行窗口函数。你可能在某一时刻听说过“运行总和”,如果没有,我们应该从一个示例开始,让一切变得清晰。
SELECT
id,
date,
region,
revenue,
SUM(revenue) OVER (ORDER BY id) as running_total
FROM sales

这里发生的事情是,我们按行逐步累加收入与之前的所有值。这是根据 id 列的顺序进行的,但也可以是任何其他列。
这个具体的示例并不是特别有用,因为我们在随机月份和两个区域之间进行求和,但运用我们所学的知识,我们现在可以找到每个区域的累积收入。我们通过在每个分组内应用运行总和来实现这一点。
SELECT
id,
date,
region,
revenue,
SUM(revenue) OVER (PARTITION BY region ORDER BY date) as running_total
FROM sales

花些时间确保你理解这里发生了什么:
-
对于每个区域,我们逐月走过并汇总收入。
-
当一个区域完成时,我们就转到下一个区域,从头开始,再次按照月份逐步计算!
有一点很有趣的是,当我们编写这些运行函数时,我们有其他行的“上下文”。我的意思是,要在某一点获得运行和,必须知道前一行的值。这在我们了解到我们可以手动选择聚合前/后多少行时变得更加明显。
SELECT
id,
date,
region,
revenue,
SUM(revenue) OVER (ORDER BY id ROWS BETWEEN 1 PRECEDING AND 2 FOLLOWING)
AS useless_sum
FROM
sales

对于这个查询,我们指定了对于每一行,我们希望查看前一行和后两行的数据,也就是说,我们得到那个范围的和!根据你解决的问题,这可以非常强大,因为它让你完全控制如何对数据进行分组。
最后,在我们进入更复杂的例子之前,我想提到的最后一个函数是RANK函数。这是面试中经常被问到的问题,它背后的逻辑和我们到目前为止所学的所有内容是一样的。
SELECT
*,
RANK() OVER (PARTITION BY region ORDER BY revenue DESC) as rank,
RANK() OVER (ORDER BY revenue DESC) as overall_rank
FROM
sales
ORDER BY region, revenue DESC

就像之前一样,我们使用了ORDER BY来指定我们将逐行遍历的顺序,并使用PARTITION BY来指定我们的子组。
第一列对每个区域内的每一行进行排名,这意味着在数据集中我们会有多个“排名第一”的值。第二个计算是对数据集中所有行的排名。
向前填充缺失数据
这是一个偶尔会出现的问题,要在 SQL 中解决它需要大量使用窗口函数。为了说明这个概念,我们将使用一个不同的数据集,包含时间戳和温度测量值。我们的目标是用最后一个测量值填充缺失的温度测量行。

这是我们期望最终得到的结果:

在开始之前,我只是想提一下,如果你使用的是 Pandas,你可以通过运行df.ffill()轻松解决这个问题,但如果你使用 SQL,问题会变得有些棘手。
解决这个问题的第一步是,某种方式地将 NULL 值与前一个非 NULL 值分组。可能不清楚我们如何做到这一点,但我希望大家明白这将需要一个运行函数。也就是说,这是一个会“逐行走过”的函数,知道何时遇到 NULL 值,何时遇到非 NULL 值。
解决方案是使用COUNT,更具体地说,是计算温度测量值的数量。在以下查询中,我运行了一个普通的运行计数,并且还对温度值进行了计数。
SELECT
*,
COUNT() OVER (ORDER BY timestamp) as normal_count,
COUNT(temperature) OVER (ORDER BY timestamp) as group_count
from sensor

-
在第一个计算中,我们只是简单地对每一行进行递增计数。
-
在第二个查询中,我们统计了所有看到的温度值,而没有计算为 NULL 的值。
normal_count列对我们来说没有用,我只是想展示一下运行中的COUNT看起来是什么样子。然而,我们的第二个计算,**group_count**让我们更接近解决问题!
请注意,这种计数方式确保了在 NULL 值开始之前的第一个值会被计数,然后每当函数遇到 NULL 时,什么也不会发生。这确保了我们为每个后续的 NULL“标记”上我们停止测量时的计数值。
接下来,我们需要将第一个被标记的值复制到同一组中的所有其他行。也就是说,对于组2,需要用值15.0填充所有数据。
你现在能想到一个我们可以在这里使用的函数吗?这个问题有不止一个答案,但再次强调,我希望至少现在你能明白,我们正在使用一个简单的窗口聚合,并且有PARTITION BY。
SELECT
*,
FIRST_VALUE(temperature) OVER (PARTITION BY group_count) as filled_v1,
MAX(temperature) OVER (PARTITION BY group_count) as filled_v2
FROM (
SELECT
*,
COUNT(temperature) OVER (ORDER BY timestamp) as group_count
from sensor
) as grouped
ORDER BY timestamp ASC

我们可以使用FIRST_VALUE或MAX来实现我们想要的目标。唯一的目标是得到第一个非 NULL 值。由于我们知道每个组包含一个非 NULL 值和一堆 NULL 值,这两个函数都可以工作!
这个例子是练习窗口函数的一个很好的方式。如果你想要类似的挑战,试着添加两个传感器,然后用该传感器的前一个读数进行向前填充。类似于这样:

你能做吗?它没有使用我们到目前为止没有学习过的任何内容。
到目前为止,我们已经了解了关于 SQL 中窗口函数的一切,所以让我们快速回顾一下!
回顾时间
这是我们学到的内容:
-
我们使用
OVER关键字来编写窗口函数 -
我们使用
PARTITION BY来指定我们的子组(窗口) -
如果我们仅提供
OVER()关键字,我们的窗口就是整个数据集 -
当我们希望进行逐行计算时,我们使用
ORDER BY,这意味着我们的计算会逐行执行 -
当我们想对数据进行分组以执行聚合操作,但又希望保持数据集原样时,窗口函数非常有用
我希望这能帮助你理解窗口函数是如何工作的,并帮助你将它应用到你需要解决的问题中。
除非另有说明,否则所有图片均由作者提供
理解神经网络中的抽象
思维机器如何实现认知中最重要的功能之一
·发表于 Towards Data Science ·阅读时间 12 分钟 ·2024 年 5 月 14 日
--
长久以来,人们一直认为神经网络具备抽象能力。当输入特征经过神经网络的各层时,输入特征会转变为越来越抽象的特征。例如,一个处理图像的模型接收到的只是低层次的像素输入,但较低的层可以学习构建表示边缘存在的抽象特征,而更高的层甚至可以编码人脸或物体。这些说法已经通过各种研究成果,展示了在卷积神经网络中学习到的特征。然而,这些深层特征到底在何种意义上比浅层特征“更加抽象”呢?在本文中,我将提供一个关于抽象的理解,不仅回答这个问题,还将解释神经网络中不同组件如何促成抽象的形成。在这个过程中,我还将揭示抽象与泛化之间有趣的二元性,从而展示抽象对于机器和我们人类来说有多么重要。

图片 作者:Gerd Altmann 来自 Pixabay
抽象,抽象定义
我认为,抽象的本质是
“忽略无关细节,专注于相关部分的行为。”
例如,在设计一个算法时,我们只对输入做出少量抽象假设,而不关心输入的其他细节。更具体地说,考虑一个排序算法。排序函数通常仅假设输入是一个数字数组,或者更抽象地说,是一个具有定义比较规则的对象数组。至于这些数字或对象代表什么,或者比较操作符比较的内容,并不是排序算法关心的事情。
除了编程,抽象在数学中也很常见。在抽象代数中,只要满足几个要求,数学结构就可以被视为一个群。数学结构是否具备其他属性或操作并不重要。在证明一个定理时,我们只对讨论的结构做出关键假设,结构可能具有的其他属性并不重要。我们甚至不需要学习大学水平的数学就能发现抽象,因为即使是数学中最基本的对象也是抽象的产物。以自然数为例,我们将桌子上三只苹果的视觉表示转化为数学表达式“3”的过程,涉及了复杂的抽象。我们的认知系统能够丢弃所有无关的细节,如苹果的排列或成熟度,或者场景的背景,而专注于当前体验中的“数量感”。
在我们的日常生活中也有抽象的例子。实际上,它几乎存在于我们使用的每一个概念中。以“狗”的概念为例。尽管我们可能将这种概念描述为具体的,但它在复杂的层面上仍然是抽象的。我们的认知系统能够丢弃无关的细节,如颜色和具体大小,而专注于定义性特征,如狗的嘴部、耳朵、毛发、尾巴和叫声,从而将某物识别为一只狗。
抽象与泛化的二重性
每当出现抽象时,似乎也会伴随泛化,反之亦然。这两个概念紧密相连,以至于有时它们几乎可以互换使用。我认为这两个概念之间有趣的关系可以总结如下:
假设、接口或要求越抽象,结论、程序或概念就越普遍,适用范围越广。
通过重新审视前面提到的例子,可以更清楚地展示这一模式。考虑排序算法的第一个例子。数字可能具有的所有额外属性都不相关,只有“有序”这一属性对我们的任务才重要。因此,我们可以进一步抽象数字为“定义了比较的对象”。通过采用更抽象的假设,这个函数不仅可以应用于数字数组,还可以更广泛地应用。类似地,在数学中,定理的普适性取决于其假设的抽象性。对于规范空间证明的定理,比仅针对欧几里得空间证明的定理更具有普适性,因为欧几里得空间是更抽象的规范空间的一个特定实例。除了数学对象,我们对现实世界对象的理解也表现出不同的抽象层次。一个很好的例子是生物学中使用的分类法。狗作为一个概念,属于更一般的哺乳动物类别,而哺乳动物又是更一般的动物概念的子集。当我们从分类法的最低层级向更高层级移动时,这些类别被定义为具有越来越抽象的属性,这使得该概念可以应用于更多实例。
抽象与泛化之间的联系暗示了抽象的必要性。作为生物体,我们必须学习适用于不同情境的技能。在抽象层次做出决策使我们能够轻松应对许多在细节被去除后看起来相同的不同情境。换句话说,这项技能可以在不同情境中进行泛化。
神经网络中的抽象
我们已经定义了抽象并看到了它在我们生活中不同方面的重要性。现在是时候解决主要问题了:神经网络如何实现抽象?
首先,我们需要将抽象的定义转化为数学。假设一个数学函数实现了“去除细节”的功能,那么这个函数应该具备什么性质呢?答案是非单射性,这意味着存在不同的输入被映射到相同的输出。直观地说,这是因为某些区分不同输入的细节现在被丢弃了,使得它们在输出空间中被视为相同。因此,要在神经网络中找到抽象,我们只需要寻找非单射映射。
让我们从检查神经网络中最简单的结构开始,即线性层中的单个神经元。假设输入是一个维度为D的实数向量x。神经元的输出将是其权重w与x的点积,再加上一个偏置b,然后通过一个非线性激活函数σ:
很容易看出,丢弃无关细节的最简单方法是将无关特征的权重设置为零,这样该特征的变化就不会影响输出。这实际上为我们提供了一个非单射函数,因为在该特征上仅有差异的输入向量将具有相同的输出。
当然,特征往往不是以简单丢弃输入特征的形式呈现的,这样我们才能得到有用的抽象。例如,简单地丢弃输入图像中的一个固定像素可能并没有用。幸运的是,神经网络能够构建有用的特征,同时丢弃其他无关细节。一般来说,给定任何权重w,输入空间可以被分成一个与权重w平行的一维子空间,以及一个与w正交的其他(D−1)维子空间。其结果是,平行于该(D−1)维子空间的任何变化都不会影响输出,因此会被“抽象掉”。例如,一个检测边缘的卷积滤波器,同时忽略颜色或光照的均匀变化,可能就算是这种形式的抽象。
除了点积,激活函数也可能在抽象中发挥作用,因为它们中的大多数都是(或接近)非单射的。以 ReLU 为例,所有负输入值都会被映射为零,这意味着这些差异被忽略了。至于其他软激活函数,如 sigmoid 或 tanh,虽然在技术上是单射的,但饱和区域将不同的输入映射到非常接近的值,达到了类似的效果。
从上面的讨论中,我们可以看到,点积和激活函数都可以在单个神经元执行的抽象中发挥作用。然而,单个神经元未捕捉到的信息仍然可以被同一层中的其他神经元捕捉到。为了判断某一信息是否真的被忽略,我们还必须查看整个层的设计。对于一个线性层,有一种简单的设计能够强制执行抽象:降低维度。其原因类似于点积,实际上相当于将数据投影到一维空间。当一层N个神经元从上一层接收M > N个输入时,它涉及到矩阵乘法:
行空间中的输入组件被保留并转换到新的空间,而位于零空间中的输入组件(至少是M−N维度的)则全部映射为零。换句话说,任何平行于零空间的输入向量的变化都被认为是无关的,因此会被抽象掉。
我仅仅分析了现代深度学习中使用的一些基本组件。然而,通过对抽象的这种描述,我们应该能够很容易地看到,深度学习中使用的许多其他组件也允许它过滤并抽象掉无关细节。
引入信息理论
如上所述,也许你们中的一些人尚未完全相信这是一种有效的神经网络工作理解方式,因为它与通常聚焦于模式匹配、非线性变换和函数逼近的叙述有所不同。然而,我认为神经网络丢弃信息这一事实其实是从不同角度讲述的同一个故事。模式匹配、特征构建以及抽象掉无关特征的过程在网络中同时发生,正是通过结合这些视角,我们才能理解为什么神经网络能很好地进行泛化。让我引入一些基于信息论的神经网络研究来强化我的观点。
首先,让我们将抽象的概念转化为信息论的术语。我们可以将网络的输入视为一个随机变量X。然后,网络会通过每一层依次处理X,以产生中间表示T₁、T₂,……,最后得到预测结果Tₖ。
如我所定义的,抽象包括丢弃无关信息并保留相关部分。丢弃细节导致原本不同的X样本在中间特征空间中映射到相同的值。因此,这一过程对应于一种有损压缩,它减少了熵H(Tᵢ)或互信息I(X;Tᵢ)。那么,保留相关信息又是怎样的呢?为此,我们需要定义一个目标任务,以便评估不同信息片段的相关性。为了简化,我们假设我们正在训练一个分类器,其中真实标签是从随机变量Y中采样的。那么,保留相关信息就等同于在各层中保持I(Y;Tᵢ),从而在最后一层做出可靠的Y预测。总结一下,如果神经网络正在进行抽象处理,我们应该看到I(X;Tᵢ)逐渐减小,同时理想情况下保持I(Y;Tᵢ)不变,随着分类器的层次加深。
有趣的是,这正是信息瓶颈原理[1]的核心内容。该原理认为,相对于Y,X的最优表示T是通过最小化I(X;T)同时保持I(Y;T) = I(Y;X)来实现的。尽管关于原始论文的一些观点存在争议,但许多研究中有一项共识:当数据从输入层流向更深的层时,I(X;T)减少,而I(Y;T)大多数情况下保持不变[1,2,3,4],这表明存在抽象的过程。不仅如此,这些研究还验证了我的观点,即激活函数的饱和[2,3]和降维[3]确实在这一现象中发挥了作用。
一个统一的视角
通过阅读文献,我发现我所称之为抽象的现象,以不同的名字出现在各种文献中,尽管它们似乎都在描述同一现象:不变特征 [5]、越来越紧密的聚类 [3]、以及神经崩塌 [6]。在这里,我展示了抽象这一简单概念如何统一这些不同的概念,并提供一个直观的解释。
正如我之前提到的,去除无关信息的过程是通过非单射映射实现的,它忽略了输入空间中某些部分的差异。其结果,当然,就是产生对这些无关差异“具有不变性”的输出。当训练分类器时,相关信息是区分不同类别样本的特征,而不是区分同类别样本的特征。因此,当网络抽象掉无关细节时,我们看到同类别样本聚集(崩塌)在一起,而不同类别样本保持分离。
除了统一文献中的几个观察结果外,将神经网络视为在每一层抽象掉细节,也为我们提供了有关其预测如何在输入空间中泛化的线索。考虑一个简化的例子,我们有输入 X,被抽象成一个中间表示 T,然后用这个表示来生成预测 P。假设一组输入 x₁, x₂, x₃, …∼X 都被映射到相同的中间表示 t。因为预测 P 仅依赖于 T,所以对于 t 的预测必然适用于所有样本 x₁, x₂, x₃, …。换句话说,由抽象引起的不变性的方向就是预测泛化的方向。这类似于我之前提到的排序算法的例子。通过抽象输入的细节,算法自然地泛化到更大的输入空间。对于一个多层的深度网络,这种抽象可能发生在每一层。因此,最终的预测也会在输入空间中以复杂的方式进行泛化。
认知的核心
几年前,当我写第一篇关于抽象的文章时,我仅仅把它看作是数学和编程解决一系列相关问题的优雅方式。然而,事实证明,我错过了更大的图景。抽象实际上无处不在,存在于我们每个人之内。它是认知的核心元素。没有抽象,我们将淹没在低层次的细节中,无法理解任何事物。正是通过抽象,我们才能将这个极其细致的世界简化为可管理的部分,只有通过抽象,我们才能学到任何普遍的东西。
要理解抽象的关键性,只需尝试想出任何不涉及抽象的词语。我敢打赌你做不到,因为一个不涉及任何抽象的概念将过于具体而无法发挥作用。即使是“具体”的概念,如苹果、桌子或走路,也都涉及复杂的抽象。苹果和桌子都有不同的形状、大小和颜色。它们可能以真实物体的形式出现,也可能只是图片。然而,我们的大脑能够透过这些差异,捕捉事物的共同本质。
这种抽象的必要性与道格拉斯·霍夫施塔特(Douglas Hofstadter)关于类比是认知核心的观点相契合[7]。实际上,我认为它们本质上是同一枚硬币的两面。每当我们进行抽象时,都会有低层次的表示映射到相同的高层次表示。在这个过程中被丢弃的信息是这些实例之间不相关的差异,而留下的信息则对应于它们的共享本质。如果我们将映射到相同输出的低层次表示聚集在一起,它们就会在输入空间中形成等价类,或者霍夫施塔特所称的“类比包”。通过简单地比较这些高层次表示,我们就能发现两个经验实例之间的类比。
当然,我们进行这些抽象和使用类比的能力必须在大脑中通过计算实现,且有充分的证据表明,我们的大脑通过层次处理进行抽象,这与人工神经网络相似[8]。随着感官信号深入大脑,不同的模态被聚合,细节被忽略,产生了越来越抽象和不变的特征。
结论
在文献中,我们经常看到关于深度神经网络的深层次构建抽象特征的说法。然而,“抽象”一词的确切含义往往不清楚。在本文中,我给出了一个精确而又通用的抽象定义,统一了信息理论和深度表示几何学的观点。通过这一表述,我们可以详细看到,人工神经网络中的许多常见组件都为其抽象能力做出了贡献。我们通常认为神经网络在每一层中检测模式,这当然是正确的。然而,我建议将注意力转向在这一过程中被忽略的信息片段。通过这样做,我们可以更好地理解它如何在深层次中生成越来越抽象、因此更加不变的特征,以及它如何在输入空间中实现预测的泛化。
通过这些解释,我希望不仅能使抽象的意义更加清晰,而且更重要的是,展示它在认知中的核心作用。
参考文献
[1] R. Shwartz-Ziv 和 N. Tishby, 通过信息开启深度神经网络的黑箱(2017)。arXiv。
[2] A. M. Saxe 等人, 深度学习的信息瓶颈理论(2019 年),《统计力学杂志:理论与实验》 12:124020
[3] Z. Goldfeld 等人, 估计深度神经网络中的信息流(2019 年),收录于第 36 届国际机器学习大会论文集,PMLR 97:2299–2308
[4] K. Wickstrøm, S. Løkse, M. Kampffmeyer, S. Yu, J. Principe 和 R. Jenssen, 通过基于矩阵的 Renyi 熵和张量核的深度神经网络信息平面分析(2019 年)。arXiv。
[5] A. Achille 和 S. Soatto, 深度表示中的不变性与解缠的出现(2018 年),《机器学习研究杂志》,19(50):1−34。
[6] A. Rangamani, M. Lindegaard, T. Galanti 和 T. A. Poggio, 通过中间神经崩溃进行深度分类器的特征学习(2023 年),收录于第 40 届国际机器学习大会论文集,PMLR 202:28729–28745。
[7] D. R. Hofstadter, 尾声:类比作为认知的核心(2001 年)。《类比心智》。麻省理工学院出版社。
[8] P. Taylor, J. N. Hobbs, J. Burroni 和 H. T. Siegelmann, 认知的全球景观:层次聚合作为人类皮层网络和功能的组织原则(2015 年),《科学报告》,第 5 卷,第 1 期,文章编号 18112。
理解与实现 Medprompt
深入探讨提示框架背后的细节
·发表于 Towards Data Science ·14 分钟阅读·2024 年 7 月 6 日
--

Medprompt 策略的各个组成部分示意图(图片来自 Medprompt 论文中的图 6 [1] (arxiv.org/abs/2311.16452 ))
在我的 第一篇博客文章 中,我探讨了提示(prompting)及其在大语言模型(LLMs)中的重要性。提示对于从 LLMs 获取高质量输出至关重要,因为它指导模型的响应并确保其与当前任务相关。在此基础上,当尝试使用 LLMs 解决实际问题时,常常会出现两个关键问题:仅凭提示能够将性能推向多远?以及何时需要做出决定,认为微调模型可能更加有效?
在做出关于利用提示的设计决策时,需要考虑多个因素。像少量示例提示和思维链(CoT)[2] 提示等技术可以帮助提高大多数任务的 LLM 性能。检索增强生成(RAG)管道可以通过适应新领域而无需微调,进一步提升 LLM 性能,同时提供对生成输出的可控性,减少幻觉现象。总体而言,我们有一套工具可以在不依赖微调的情况下,推动 LLM 性能的提升。
微调本身就伴随着一系列挑战和复杂性,包括对标注数据的需求以及与 LLM 训练和部署相关的成本。微调在某些情况下还可能增加 LLM 的幻觉 [3]。将这些因素汇总,我们可以看到,通过提示来优化 LLM 性能,在采取微调之前,确实具有重要的价值。
那么,我们该如何进行呢?在本文中,我们探讨了 Medprompt [1],这是一种由微软推出的复杂提示策略。Medprompt 将少量样本提示、思维链提示和 RAG 原理结合起来,以提高 GPT-4 在医疗健康领域的表现,而无需任何领域特定的微调。
目录:
-
MedPrompt 解释
-
MedPrompt 组件
-
实现 MedPrompt
-
评估性能
-
结论
-
参考文献
MedPrompt 解释
LLMs 在各个领域展示了令人印象深刻的能力,尤其是在医疗健康领域。去年,Google 推出了 MedPaLM [4] 和 MedPaLM-2 [5],这两款 LLM 不仅在医学多项选择题回答(MCQA)数据集上表现出色,还在开放性医学问题回答中具有竞争力,甚至超越了临床医生。这些模型通过指令微调和使用临床医生编写的思维链模板,特别针对医疗健康领域进行了优化,显著提升了它们的表现。
在此背景下,微软的论文《通用基础模型能否超越专用调优?医学领域的案例研究》[1] 提出了一个引人深思的问题:
是否可以在不依赖领域特定微调或专家编写资源的情况下,提升像 GPT-4 这样的通用模型在特定领域的表现?
作为本研究的一部分,论文介绍了Medprompt,一种创新的提示策略,不仅能提高模型的性能,还能超越像 MedPaLM-2 这样的专用模型。

各种 LLM 在医学知识基准测试中的比较。使用 Medprompt 的 GPT-4 在所有这些数据集上都优于 Med-PaLM 2。(来自 Medprompt 论文 [1] 的表格 1 图像 arxiv.org/abs/2311.16452)
使用 Medprompt 的 GPT-4 在所有医学 MCQA 基准测试中都超越了 Med-PaLM 2,而无需任何领域特定的微调。让我们来探索一下 Medprompt 的组件。
Medprompt 组件
Medprompt 将少量样本提示、思维链提示(CoT)和检索增强生成(RAG)原理结合起来。具体来说,这个流程包含三个组件:
动态少量样本选择`
少量示例提示是指利用输入-输出对作为上下文来提示 LLM。如果这些少量示例是静态的,缺点是它们可能不是对新输入最相关的示例。动态少量示例选择,即 Medprompt 中的第一个组件,通过基于每个新任务输入选择少量示例来克服这一点。该方法通过在训练集上训练一个 K 最近邻(K-NN)算法,基于嵌入空间中的余弦相似度来检索与测试输入最相似的训练集示例。此策略高效地利用现有的训练数据集,为 LLM 检索相关的少量示例进行提示。
自生成的 CoT
正如论文 [1] 中所指出的,CoT 传统上依赖于手工制作的少量示例,这些示例包括详细的推理步骤,MedPaLM-2 就使用了这种由医学专业人员编写的提示。Medprompt 引入了自生成的 CoT,作为第二个模块,在该模块中,LLM 用来生成其推理过程的详细逐步解释,最终得出一个答案选择。通过自动生成每个训练数据点的 CoT 推理步骤,可以绕过手工制作示例的需求。为了确保仅保留具有推理步骤的正确预测,并过滤掉错误的回答,GPT-4 生成的答案会与真实答案进行交叉验证。
选择混洗集成
选择混洗集成技术是 Medprompt 引入的第三种技术。该技术旨在应对可能影响模型决策过程的固有偏见,特别是在多项选择设置中的位置偏见。答案选项的顺序会被打乱,并且这一过程会重复 k 次,以创建 k 个不同版本的相同问题,且每个版本的答案选项顺序不同。在推理过程中,每个版本都会生成一个答案,并对所有版本进行多数投票,以选择最终的预测选项。
这些组件如何在预处理和推理阶段中使用?
现在我们来看看 Medprompt 中的预处理和推理阶段。
预处理阶段
在预处理管道中,我们首先从训练数据集中提取每个问题,并在提示中加入详细的指令,以指导生成答案及其相关推理步骤。大语言模型(LLM)会被提示生成答案和推理步骤。在获取生成的回答后,我们通过将预测的答案与该特定问题的真实答案进行对比,来验证其准确性。

Medprompt 预处理管道(图片由作者提供)
如果预测错误,我们将此实例从相关问题的数据库中排除。如果预测正确,我们将使用文本嵌入模型对问题进行嵌入。然后,我们将问题、问题嵌入、答案和思维链(CoT)推理存储到缓冲区中。一旦所有问题都处理完毕,我们利用这些嵌入来训练 KNN 模型。这个训练好的 KNN 模型作为我们 RAG 管道中的检索器,使我们能够高效地基于嵌入空间中的余弦相似度查询并检索 top-k 最相似的数据点。
推理管道
在推理阶段,我们首先使用文本嵌入模型对测试集中的每个问题进行嵌入。然后,我们使用 KNN 模型来识别最相似的 top-k 问题。对于每个检索到的数据点,我们可以访问自生成的思维链(CoT)推理和预测答案。我们将这些元素——问题、CoT 推理和答案——格式化为最终提示的少量示例。

Medprompt 推理管道(作者插图)
我们现在通过洗牌每个测试问题的答案选项顺序,进行 选择洗牌集成,从而创建相同问题的多个变体。然后,LLM 会被提示这些变体,以及相应的少量示例,以生成推理步骤和每个变体的答案。最后,我们对所有变体的预测结果进行多数投票,选择最终的预测。
实现 Medprompt
与此实现相关的代码可以在这个 github repo 链接 中找到。
我们使用 MedQA [6] 数据集来实现和评估 Medprompt。我们首先定义帮助函数来解析 jsonl 文件。
def write_jsonl_file(file_path, dict_list):
"""
Write a list of dictionaries to a JSON Lines file.
Args:
- file_path (str): The path to the file where the data will be written.
- dict_list (list): A list of dictionaries to write to the file.
"""
with open(file_path, 'w') as file:
for dictionary in dict_list:
json_line = json.dumps(dictionary)
file.write(json_line + '\n')
def read_jsonl_file(file_path):
"""
Parses a JSONL (JSON Lines) file and returns a list of dictionaries.
Args:
file_path (str): The path to the JSONL file to be read.
Returns:
list of dict: A list where each element is a dictionary representing
a JSON object from the file.
"""
jsonl_lines = []
with open(file_path, 'r', encoding="utf-8") as file:
for line in file:
json_object = json.loads(line)
jsonl_lines.append(json_object)
return jsonl_lines
实现自生成的 CoT
在我们的实现中,我们利用了 MedQA 的训练集。我们实现了一个零-shot CoT 提示,并处理了所有的训练问题。我们在实现中使用了 GPT-4o。对于每个问题,我们生成 CoT 和相应的答案。我们定义了一个基于 Medprompt 论文中提供的模板的提示。
system_prompt = """You are an expert medical professional. You are provided with a medical question with multiple answer choices.
Your goal is to think through the question carefully and explain your reasoning step by step before selecting the final answer.
Respond only with the reasoning steps and answer as specified below.
Below is the format for each question and answer:
Input:
## Question: {{question}}
{{answer_choices}}
Output:
## Answer
(model generated chain of thought explanation)
Therefore, the answer is [final model answer (e.g. A,B,C,D)]"""
def build_zero_shot_prompt(system_prompt, question):
"""
Builds the zero-shot prompt.
Args:
system_prompt (str): Task Instruction for the LLM
content (dict): The content for which to create a query, formatted as
required by `create_query`.
Returns:
list of dict: A list of messages, including a system message defining
the task and a user message with the input question.
"""
messages = [{"role": "system", "content": system_prompt},
{"role": "user", "content": create_query(question)}]
return messages
def build_few_shot_prompt(system_prompt, question, examples, include_cot=True):
"""
Builds the few-shot prompt.
Args:
system_prompt (str): Task Instruction for the LLM
content (dict): The content for which to create a query, formatted as
required by `create_query`.
Returns:
list of dict: A list of messages, including a system message defining
the task and a user message with the input question.
"""
messages = [{"role": "system", "content": system_prompt}]
for elem in examples:
messages.append({"role": "user", "content": create_query(elem)})
if include_cot:
messages.append({"role": "assistant", "content": format_answer(elem["cot"], elem["answer_idx"])})
else:
answer_string = f"""## Answer\nTherefore, the answer is {elem["answer_idx"]}"""
messages.append({"role": "assistant", "content": answer_string})
messages.append({"role": "user", "content": create_query(question)})
return messages
def get_response(messages, model_name, temperature = 0.0, max_tokens = 10):
"""
Obtains the responses/answers of the model through the chat-completions API.
Args:
messages (list of dict): The built messages provided to the API.
model_name (str): Name of the model to access through the API
temperature (float): A value between 0 and 1 that controls the randomness of the output.
A temperature value of 0 ideally makes the model pick the most likely token, making the outputs deterministic.
max_tokens (int): Maximum number of tokens that the model should generate
Returns:
str: The response message content from the model.
"""
response = client.chat.completions.create(
model=model_name,
messages=messages,
temperature=temperature,
max_tokens=max_tokens
)
return response.choices[0].message.content
我们还定义了帮助函数,用于解析 LLM 响应中的推理和最终答案选项。
def matches_ans_option(s):
"""
Checks if the string starts with the specific pattern 'Therefore, the answer is [A-Z]'.
Args:
s (str): The string to be checked.
Returns:
bool: True if the string matches the pattern, False otherwise.
"""
return bool(re.match(r'^Therefore, the answer is [A-Z]', s))
def extract_ans_option(s):
"""
Extracts the answer option (a single capital letter) from the start of the string.
Args:
s (str): The string containing the answer pattern.
Returns:
str or None: The captured answer option if the pattern is found, otherwise None.
"""
match = re.search(r'^Therefore, the answer is ([A-Z])', s)
if match:
return match.group(1) # Returns the captured alphabet
return None
def matches_answer_start(s):
"""
Checks if the string starts with the markdown header '## Answer'.
Args:
s (str): The string to be checked.
Returns:
bool: True if the string starts with '## Answer', False otherwise.
"""
return s.startswith("## Answer")
def validate_response(s):
"""
Validates a multi-line string response that it starts with '## Answer' and ends with the answer pattern.
Args:
s (str): The multi-line string response to be validated.
Returns:
bool: True if the response is valid, False otherwise.
"""
file_content = s.split("\n")
return matches_ans_option(file_content[-1]) and matches_answer_start(s)
def parse_answer(response):
"""
Parses a response that starts with '## Answer', extracting the reasoning and the answer choice.
Args:
response (str): The multi-line string response containing the answer and reasoning.
Returns:
tuple: A tuple containing the extracted CoT reasoning and the answer choice.
"""
split_response = response.split("\n")
assert split_response[0] == "## Answer"
cot_reasoning = "\n".join(split_response[1:-1]).strip()
ans_choice = extract_ans_option(split_response[-1])
return cot_reasoning, ans_choice
我们现在处理 MedQA 训练集中的问题。我们获取所有问题的 CoT 响应和答案,并将它们存储到一个文件夹中。
train_data = read_jsonl_file("data/phrases_no_exclude_train.jsonl")
cot_responses = []
os.mkdir("cot_responses")
for idx, item in enumerate(tqdm(train_data)):
prompt = build_zero_shot_prompt(system_prompt, item)
try:
response = get_response(prompt, model_name="gpt-4o", max_tokens=500)
cot_responses.append(response)
with open(os.path.join("cot_responses", str(idx) + ".txt"), "w", encoding="utf-8") as f:
f.write(response)
except Exception as e :
print(str(e))
cot_responses.append("")
我们现在遍历所有生成的响应,以检查它们是否有效并符合提示中定义的预测格式。我们丢弃那些不符合要求格式的响应。之后,我们将每个问题的预测答案与真实答案进行比对,只保留那些预测答案与真实答案匹配的问题。
questions_dict = []
ctr = 0
for idx, question in enumerate(tqdm(train_data)):
file = open(os.path.join("cot_responses/", str(idx) + ".txt"), encoding="utf-8").read()
if not validate_response(file):
continue
cot, pred_ans = parse_answer(file)
dict_elem = {}
dict_elem["idx"] = idx
dict_elem["question"] = question["question"]
dict_elem["answer"] = question["answer"]
dict_elem["options"] = question["options"]
dict_elem["cot"] = cot
dict_elem["pred_ans"] = pred_ans
questions_dict.append(dict_elem)
filtered_questions_dict = []
for item in tqdm(questions_dict):
pred_ans = item["options"][item["pred_ans"]]
if pred_ans == item["answer"]:
filtered_questions_dict.append(item)
实现 KNN 模型
在处理了训练集并获得了所有这些问题的 CoT 响应后,我们现在使用 OpenAI 的 text-embedding-ada-002 嵌入所有问题。
def get_embedding(text, model="text-embedding-ada-002"):
return client.embeddings.create(input = [text], model=model).data[0].embedding
for item in tqdm(filtered_questions_dict):
item["embedding"] = get_embedding(item["question"])
inv_options_map = {v:k for k,v in item["options"].items()}
item["answer_idx"] = inv_options_map[item["answer"]]
我们现在使用这些问题的嵌入训练一个 KNN 模型。该模型在推理时充当检索器,帮助我们从训练集中检索出与测试集中的问题最相似的数据点。
import numpy as np
from sklearn.neighbors import NearestNeighbors
embeddings = np.array([d["embedding"] for d in filtered_questions_dict])
indices = list(range(len(filtered_questions_dict)))
knn = NearestNeighbors(n_neighbors=5, algorithm='auto', metric='cosine').fit(embeddings)
实现动态少样本和选项洗牌集成逻辑
我们现在可以进行推理。我们从 MedQA 测试集中子抽取 500 个问题进行评估。对于每个问题,我们使用 KNN 模块从训练集中检索 5 个最相似的问题,并获取它们各自的 CoT 推理步骤和预测答案。然后,我们构建一个少样本提示来使用这些示例。
对于每个问题,我们还会将选项的顺序洗牌 5 次,以创建不同的变体。然后,我们利用构造的少样本提示,通过每个变体的洗牌选项获取预测答案。
def shuffle_option_labels(answer_options):
"""
Shuffles the options of the question.
Parameters:
answer_options (dict): A dictionary with the options.
Returns:
dict: A new dictionary with the shuffled options.
"""
options = list(answer_options.values())
random.shuffle(options)
labels = [chr(i) for i in range(ord('A'), ord('A') + len(options))]
shuffled_options_dict = {label: option for label, option in zip(labels, options)}
return shuffled_options_dict
test_samples = read_jsonl_file("final_processed_test_set_responses_medprompt.jsonl")
for question in tqdm(test_samples, colour ="green"):
question_variants = []
prompt_variants = []
cot_responses = []
question_embedding = get_embedding(question["question"])
distances, top_k_indices = knn.kneighbors([question_embedding], n_neighbors=5)
top_k_dicts = [filtered_questions_dict[i] for i in top_k_indices[0]]
question["outputs"] = []
for idx in range(5):
question_copy = question.copy()
shuffled_options = shuffle_option_labels(question["options"])
inv_map = {v:k for k,v in shuffled_options.items()}
question_copy["options"] = shuffled_options
question_copy["answer_idx"] = inv_map[question_copy["answer"]]
question_variants.append(question_copy)
prompt = build_few_shot_prompt(system_prompt, question_copy, top_k_dicts)
prompt_variants.append(prompt)
for prompt in tqdm(prompt_variants):
response = get_response(prompt, model_name="gpt-4o", max_tokens=500)
cot_responses.append(response)
for question_sample, answer in zip(question_variants, cot_responses):
if validate_response(answer):
cot, pred_ans = parse_answer(answer)
else:
cot = ""
pred_ans = ""
question["outputs"].append({"question": question_sample["question"], "options": question_sample["options"], "cot": cot, "pred_ans": question_sample["options"].get(pred_ans, "")})
我们现在评估 Medprompt 在测试集上的表现。对于每个问题,我们通过集成逻辑生成五个预测。我们取每个问题的众数,即出现频率最高的预测,作为最终预测并进行评估。这里可能出现两个极端情况:
-
两个不同的答案选项被预测了两次,但没有明确的赢家。
-
生成的响应存在错误,意味着我们没有预测的答案选项。
对于这两种极端情况,我们认为 LLM 对这些问题的回答是错误的。
def find_mode_string_list(string_list):
"""
Finds the most frequently occurring strings.
Parameters:
string_list (list of str): A list of strings.
Returns:
list of str or None: A list containing the most frequent string(s) from the input list.
Returns None if the input list is empty.
"""
if not string_list:
return None
string_counts = Counter(string_list)
max_freq = max(string_counts.values())
mode_strings = [string for string, count in string_counts.items() if count == max_freq]
return mode_strings
ctr = 0
for item in test_samples:
pred_ans = [x["pred_ans"] for x in item["outputs"]]
freq_ans = find_mode_string_list(pred_ans)
if len(freq_ans) > 1:
final_prediction = ""
else:
final_prediction = freq_ans[0]
if final_prediction == item["answer"]:
ctr +=1
print(ctr / len(test_samples))
性能评估
我们评估了 Medprompt 在 MedQA 测试子集上与 GPT-4o 相关的准确性表现。此外,我们还基准测试了零样本提示、随机少样本提示和随机少样本 CoT 提示的表现。

我们的评估结果(图片由作者提供)
我们观察到,Medprompt 和随机少样本 CoT 提示比零样本和少样本提示基准表现更好。然而,令人惊讶的是,我们注意到随机少样本 CoT 的表现超出了我们的 Medprompt 表现。这可能是由于以下几个原因:
-
原始的 Medprompt 论文基准测试了 GPT-4 的表现。我们观察到,GPT-4o 在各种文本基准测试中显著优于 GPT-4T 和 GPT-4(
openai.com/index/hello-gpt-4o/),这表明 Medprompt 在像 GPT-4o 这样的更强模型上的影响可能较小。 -
我们将评估限制在从 MedQA 中子抽取的 500 个问题。Medprompt 论文评估了其他医学 MCQA 数据集以及 MedQA 的完整版。对 GPT-4o 进行完整版数据集的评估可能能更全面地展示整体表现。
结论
Medprompt 是一个有趣的框架,用于创建复杂的提示管道,特别是在不需要微调的情况下将通用的 LLM 调整到特定领域。它还突出了在各种使用场景中在提示和微调之间做出选择时需要考虑的因素。探索提示能够推动 LLM 性能提升的极限非常重要,因为这为微调提供了一种高效且低成本的替代方案。
参考文献:
[1] Nori, H., Lee, Y. T., Zhang, S., Carignan, D., Edgar, R., Fusi, N., … & Horvitz, E. (2023). 通用基础模型能否超越专用微调?医学领域的案例研究。arXiv 预印本 arXiv:2311.16452。(arxiv.org/abs/2311.16452)
[2] Wei, J., Wang, X., Schuurmans, D., Bosma, M., Xia, F., Chi, E., … & Zhou, D. (2022). 思维链提示引发大型语言模型的推理。神经信息处理系统进展,35,24824–24837. (openreview.net/pdf?id=_VjQlMeSB_J)
[3] Gekhman, Z., Yona, G., Aharoni, R., Eyal, M., Feder, A., Reichart, R., & Herzig, J. (2024). 在新知识上微调 LLM 是否会鼓励幻觉?arXiv 预印本 arXiv:2405.05904。(arxiv.org/abs/2405.05904)
[4] Singhal, K., Azizi, S., Tu, T., Mahdavi, S. S., Wei, J., Chung, H. W., … & Natarajan, V. (2023). 大型语言模型编码临床知识。自然,620(7972),172–180. (www.nature.com/articles/s41586-023-06291-2)
[5] Singhal, K., Tu, T., Gottweis, J., Sayres, R., Wulczyn, E., Hou, L., … & Natarajan, V. (2023). 通过大型语言模型实现专家级医学问答。arXiv 预印本 arXiv:2305.09617。(arxiv.org/abs/2305.09617)
[6] Jin, D., Pan, E., Oufattole, N., Weng, W. H., Fang, H., & Szolovits, P. (2021). 这个病人得了什么病?一个来自医学考试的大规模开放领域问答数据集。应用科学,11(14),6421. (arxiv.org/abs/2009.13081)(原始数据集发布在 MIT 许可下)
深入理解 JAX 中的自动微分
解锁梯度:JAX 如何让自动微分感觉像魔法一样
·发表于 Towards Data Science ·10 分钟阅读·2024 年 10 月 11 日
--
欢迎来到 JAX 的世界,在这里,微分自动发生,速度比凌晨三点的咖啡因驱动程序员还要快!在这篇文章中,我们将深入探讨自动微分(AD)的概念,这是 JAX 的核心功能,我们还将探索为什么它对机器学习、科学计算以及任何需要微分的领域都是一项颠覆性的技术。最近,JAX 的受欢迎程度不断上升,这得益于由可微编程推动的科学机器学习新兴领域。
但等一下——在我们深入探讨之前,让我们先问一些基本问题。
-
什么是 JAX?
-
那么,我们为什么需要自动微分呢?
-
最重要的是,JAX 是如何让这一切变得更酷(也更简单)的?
别担心,你会带着微笑离开, hopefully,并且希望你能在工具箱中增加一个新工具,让你像专业人士一样处理微分问题。准备好了吗?让我们深入了解吧。
JAX 到底是什么?
JAX 是由 Google 开发的一个库,旨在进行高性能数值计算和机器学习研究。它的核心功能是使编写可微分、可并行化并可编译到硬件加速器(如 GPU 和 TPU)上运行的代码变得非常简单。OG 团队…
理解思维缓冲区(BoT)——与大型语言模型推理
一种用于复杂推理的新提示工具,与思维链(CoT)和思维树(ToT)进行比较
·发布于Towards Data Science ·阅读时间:10 分钟·2024 年 6 月 14 日
--
如果你不是会员,免费阅读! ✨

图片由Etienne Girardet提供,来源于Unsplash
提升在复杂推理任务中的能力并避免幻觉依然是大型语言模型(LLMs)中的一个主要研究课题。尽管进行了大量努力,LLMs 仍然在广泛推理能力上需要帮助。传统方法,如思维链(CoT)或思维树(ToT),通常需要多个假设或大量的反复提示,这意味着需要强大的计算资源。

思维缓冲区(BoT)与其他提示方法的比较。(来源:论文)
论文中提出的新的方法,思维缓冲区:基于大语言模型的思维增强推理 [1],通过一个动态、自适应的高层次思维模板库——元缓冲区(meta-buffer),来应对这些局限。在 BoT 中,一旦用户提出一个新问题,首先对问题进行简化和分析,以提取关键信息,然后从动态数据集中检索与之相关的思维模板。这使得通过修改和复杂的推理模式实现自适应和高效的问题解决。根据原始论文,效果如此显著,“Llama3–8B+BoT 有潜力超越 Llama3–70B 模型。”
BoT通过以下方式,在与其模板相似的问题中实现高效推理:
-
(1) 在新挑战中借用以前的解决方案,
-
(2) 提高效率,避免多次查询迭代(正如我们在思维图(Graph-of-Thoughts, GoT)或思维链(ToT)中所看到的),并
-
(3) 动态更新其模板库,以确保在遇到新任务时能够不断发展。
在本文中,我们将首先了解 BoT 如何工作的一般概述,理解每个关键部分的功能,并通过一个示例测试该过程。
BoT 是如何工作的?
一般的思维增强推理过程(如下图所示)从问题提炼开始,分析并浓缩传入的任务,提取出关键元素和约束条件,进而创建简化的问题陈述。
这些提炼的信息随后被用来查询元缓冲区(Meta-Buffer),这是一个包含高层次思维模板的动态库。从思维模板中,检索出与提炼问题最相似的模板。接下来,在实例化过程(Instantiation Process)中,它会根据提炼问题的具体要求和信息进行实例化。
在整个过程中,缓冲区管理器(Buffer Manager)会主动监控元缓冲区(Meta-Buffer)。一旦检测到元缓冲区中未包含的新见解,缓冲区管理器(Buffer Manager)会更新元缓冲区,确保思维模板库的持续进化。

BoT 过程。(来源:论文)
让我们逐一了解这些关键部分,深入探讨每一部分的细节:
问题提炼器
问题提炼器可以看作是对输入任务的预处理,目的是……
-
(1) 提取问题的关键信息,并
-
(2) 简化复杂任务,以便更好地搜索和检索思维模板。
问题提炼器(Problem Distiller)通过元提示ϕ减轻了大语言模型(LLM)识别和提取问题的关键信息与约束的负担:

(来源:论文)
作者用于提炼任务关键信息的提示如下:
[Problem Distiller]:
As a highly professional and intelligent expert in information distillation, you excel at
extracting essential information to solve problems from user input queries. You adeptly
transform this extracted information into a suitable format based on the respective type of the
issue.
Please categorize and extract the crucial information required to solve the problem from the
user’s input query, the distilled information should include.
1\. Key information:
Values and information of key variables extracted from user input, which will be handed over
to the respective expert for task resolution, ensuring all essential information required to solve
the problem is provided.
2\. Restrictions:
The objective of the problem and corresponding constraints.
3\. Distilled task:
Extend the problem based on 1 and 2, summarize a meta problem that can address the user
query and handle more input and output variations. Incorporate the real-world scenario of the
extended problem along with the types of key variables and information constraints from the
original problem to restrict the key variables in the extended problem. After that, use the user
query input key information as input to solve the problem as an example.
元缓冲区(Meta-Buffer)
元缓冲区是一个中央数据库,存储着高级的思维模板。这些模板是表示各种问题解决过程的高级抽象。其理念是 LLM 可以利用过去的问题和见解来解决当前的挑战。最棒的是,元缓冲区会动态更新,以确保新的未见过的问题也能被包括在内。元缓冲区不会强制思维模板遵循特定的指令。
模板检索:一旦任务被提炼,BoT 会遍历思维模板并抓取与任务最相似的一个。通过计算任务与思维模板之间的嵌入相似度来完成此操作。

检索器会计算输入任务 f(xd)的嵌入与模板 f(DTi)的嵌入之间的相似度。(来源:论文)
检索器会计算输入任务f(xd)的嵌入与模板f(DTi)的嵌入之间的相似度。仅在相似度超过某个阈值δ(0.5–0.7)时才会进行此操作。如果没有思维模板与任务的相似度超过δ阈值,则xd会被识别为新任务。根据任务是否是新任务,将选择以下两条路径之一:
- 如果任务与某个思维模板相似,则会使用实例化提示(可以在论文中查看)用提炼过的信息实例化该模板。这个实例化过程可以表示为

实例化推理。(来源:论文)
- 如果任务是新的,则使用一个旨在解决广泛问题的通用思维模板。随着任务的处理,缓冲区管理器会观察并学习,并可能创建一个新的、更具体的思维模板并将其推送到元缓冲区。
缓冲区管理器
缓冲区管理器在维护和增强元缓冲区中发挥着至关重要的作用。根据从解决任务中获得的新见解和结果,它更新思维模板。同时,每当解决一个新的或截然不同的问题时,缓冲区管理器会评估是否创建一个新的思维模板。这是为了确保思维模板始终紧扣主题,不冗余。

检查新生成的模板是否与现有模板相似。(来源:论文)
通过采用上述公式,缓冲区管理器检查元缓冲区是否已经拥有解决问题所需的知识。
BoT vs. 单查询 vs. 多查询
BoT 相较于之前的方法有什么突出之处?论文的作者在多个数据集和不同任务上评估了各种方法,如数据理解、Python 编程难题、语言学年级数学(MGSM)等。结果显示,BoT 在几乎所有任务中都表现出意想不到的优势。

BoT 与之前的方法比较。最佳结果(标记为蓝色)全部由 BoT 实现。(来源:Paper)
BoT 的关键优势之一是其效率——与多查询提示方法相比,平均仅需 12% 的计算成本。这种多查询方法(如 ToT)通常因其高计算成本和延迟,在实际应用中变得不切实际。
BoT+Llama3–8B 有潜力超越单一的 Llama3–70B 模型

比较 BoT 在 Llama3–8B 和 70B 上的效果。已标注。(来源:Paper)
实践中的 Buffer of Thoughts
Buffer of Thoughts 的演示代码已发布在 GitHub [2] 上。为了在实践中测试其功能,我将在一个自定义任务中使用该方法:词语重排。在这个任务中,LLM 必须接受一个乱序的句子,例如 “Sam name is my”,并返回一个语义上有意义的单词排列,在这个例子中是 “my name is Sam”,(这不是一个基准测试,且没有基准性能)。以下是一些乱序句子及其正确版本的示例:
{"input": "<start> life plan and families to for social hospital workers outside with patients work the <end>",
"target": "<start> social workers work with patients and families to plan for life outside the hospital <end>"}
{"input": "<start> yield plant refers dry total to production biological matter <end>",
"target": "<start> biological yield refers to total plant dry matter production <end>"}
{"input": "<start> the bloodstream into alcohol from directly stomach goes the <end>",
"target": "<start> alcohol goes directly from the stomach into the bloodstream <end>"}
我还将使用以下用户提示来解释任务:
Reorder a list of words to make them semantically meaningful,
for example "Sam name is my" -> "my name is Sam".
Please provide only the reordered sentence without any explanations.
Start the ordered sentence with <start> and end it with <end>
提供这个任务时,BoT 框架首先提炼我的任务并提取目标、限制条件以及我希望的输出方式,和其他关键信息。BoT 的完整流程如下终端输出:
User prompt:
Reorder a list of words to make them semantically meaningful, for example "Sam name is my" -> "my name is Sam". Please provide only the reordered sentence without any explanations. Start the ordered sentence with <start> and end it with <end>
Input:
<start> the melting in solid to gold leaf metals is achieve made by desired gold and mixing color other <end>
Distilled information:Meta distiller Respond:
Distilled Information:
1\. Key information:
- Input: "<start> the melting in solid to gold leaf metals is achieve made by desired gold and mixing color other <end>"
2\. Restriction:
- Reorder the list of words such that the sentence is semantically meaningful
- The output should begin with "<start>" and end with "<end>"
3\. Distilled task:
- The task is to reorder the words to create a meaningful sentence from the provided mixed words inside the tags "<start>" and "<end>"
4\. Python transformation:
(Optional, skip when Python tag is Not for Python) Input parameters:
input_sentence = "<start> the melting in solid to gold leaf metals is achieve made by desired gold and mixing color other <end>"
5\. Answer form: (Optional, skip when there is no specific answer form)
The output sentence should be in the form of "<start> ... <end>"
**Note: The generation ends here. Do not show this message in your answer !**
Instantiated reasoning result: ```python
def ReorderWords(input_sentence):
# 删除开始和结束标签,并将句子拆分成单词
words = input_sentence.replace("<start>", "").replace("<end>", "").strip().split()
# 初步重排以形成有意义的句子
# 根据任务中提供的上下文,以下重排看起来是合乎逻辑的
reordered_words = ["the", "melting", "in", "solid", "metals", "is", "achieved", "by", "mixing", "gold", "leaf", "and", "desired", "other", "color", "gold"]
# 将单词合并成一个句子,并添加开始和结束标签
reordered_sentence = "<start> " + " ".join(reordered_words) + " <end>"
return reordered_sentence
# 任务中提供的输入句子
input_sentence = "<start> the melting in solid to gold leaf metals is achieve made by desired gold and mixing color other <end>"
print(ReorderWords(input_sentence))
```py
The result of code execution: <start> the melting in solid metals is achieved by mixing gold leaf and desired other color gold <end>
使用 BoT 重排的句子的一些示例:
{"input": "<start> life plan and families to for social hospital workers outside with patients work the <end>",
"result": "<start> Hospital workers work outside with social patients and plan to the families for life <end>\n"}
{"input": "<start> yield plant refers dry total to production biological matter <end>",
"result": "<start> Plant yield refers to total dry matter biological production <end>\n"}
{"input": "<start> the bloodstream into alcohol from directly stomach goes the <end>",
"result": "<start> the alcohol goes directly from the stomach into the bloodstream <end>\n"}
请注意,我使用的 BoT 仓库是一个演示代码,缺少原论文中提到的一些功能,比如通用思维模板、Meta-Buffer 的动态更新,或者找到与用户任务最接近的模板嵌入。这些是框架的重要方面,没有它们,我们无法得出思想缓冲区在实际应用中的表现。
最后的话
总结来说,BoT 在各个领域和任务中表现出色,既准确又高效。这是一种有趣的方法,通过将推理问题分解为基本限制和关键信息,并在之前的解决方案和模板基础上构建,以更好地构造任务,使 LLM 能够理解。
通过解决其他提示技术的一些局限性,思想缓冲区使 LLM 能够具备更复杂的思维模式,可能使较小的轻量级模型达到更大模型的性能水平。
允许小型 LLM 实现接近大型 LLM 的结果是当前许多研究论文中讨论的一个关键主题。目标是采用各种提示和微调技术,以低计算量和成本提取准确的 AI 输出。
思想缓冲区是一个新颖且有前景的提示框架,利用一系列技术引导 LLM 逐步进行推理过程。思想缓冲区技术的完整实践实现尚未到来,但在此期间,可以在提供的演示 GitHub 仓库[2]中测试基准。
如果你已经读到这里,考虑继续阅读更多:
人工智能模型是否正在朝着现实的统一表征发展?柏拉图式表征……
towardsdatascience.com
🌟 加入+1000 人一起学习 Python、机器学习 / MLOps / 人工智能、数据科学和 LLM。 关注我并查看我的X/Twitter,我每天为你更新。
感谢阅读,
— Hesam
[1] 杨凌、余泽、张涛、曹胜、许梦、张伟、戈恩萨雷斯、崔博(2024)。《思想缓冲区:通过大型语言模型增强的思维推理》。arXiv。取自arxiv.org/abs/2406.04271
[2] buffer-of-thought-llm, github.com/YangLing0818/buffer-of-thought-llm
理解概念漂移:一个简单的指南
分布变化如何发生,以及验证延迟的影响。
·发表于Towards Data Science ·阅读时长 6 分钟·2024 年 3 月 5 日
--

概念漂移的检测与适应是 AI 系统监控的关键步骤。
在本文中,我们将:
-
描述什么是概念漂移,以及它如何在时间相关的数据中产生
-
探讨验证延迟及其对变化检测过程的影响
-
使用 scikit-multiflow 展示一个变化检测的例子
引言
机器学习模型隐含地假设数据是平稳的。它们期望测试或生产样本的分布与训练集中的分布相同。
然而,这一假设在展现时间相关结构的现实问题中几乎从未得到满足。
在现实世界环境中,数据的分布往往随着时间发生变化。这种变化被称为概念漂移,它发生在多个应用领域。在商业中,消费者对服务的兴趣可能会因为季节性变化或新兴趋势而发生改变。在金融领域,经济变化会导致消费或信贷条件的变化。
理解条件概率与贝叶斯定理

由Stephen Cobb拍摄,图片来源于Unsplash
介绍回归分析的两个基本概念
·发布于Towards Data Science ·阅读时长 20 分钟·2024 年 7 月 18 日
--
历史上很少有事件能像 1983 年 9 月 26 日的事件那样,生动地展示条件概率是如何深深融入人类思维中的。
1983 年 9 月 26 日午夜过后,一颗苏联预警卫星检测到来自美国的弹道导弹发射,目标指向苏联。
斯坦尼斯拉夫·佩特罗夫中校,当时在莫斯科郊外一处秘密预警卫星控制中心值班,接收到了屏幕上的警报。他只有几分钟时间决定是否将信号标记为真实并通知上司。
该预警卫星系统专门设计用于检测弹道导弹发射。如果佩特罗夫告诉上司这个信号是真的,苏联领导层完全有理由产生恐慌。更复杂的是,1983 年冷战局势已达到令人恐惧的高峰,这增强了苏联人认为来自预警卫星的信号可能确实是真实的概率。
关于佩特罗夫何时通知上司警报及两人之间交换了什么信息,记载有所不同,但有两点是确定的:
理解数据质量及其为何让团队感到困扰
数据质量:涵盖业务逻辑、可靠性、有效性和一致性的总称
·发表于Towards Data Science ·9 分钟阅读·2024 年 3 月 10 日
--

讨论中的问题核心。(图片来源:Alberico Iusso于Unsplash)
关于数据质量的讨论可能很困难,尤其是当问题的核心是一个表现不佳的产品时。
这些讨论通常发生在失望的利益相关者、沮丧的产品经理和被误解的工程师之间。
可能会有一些熟悉的短语在讨论中反复出现,包括:
-
“修复数据”
-
“差异”
-
“数据验证”
-
“信任”
-
“数据质量”
但是有一种力量在起作用,阻止个体达成共识。话语不断被说出,但出于某种原因,它们没有真正传达给对方。从每个人所说的话中看出,显然“数据质量”有多重定义在其中起作用。
每个人对话语背后的意义和含义的理解都是不同的。随着他们不断地相互交谈却没有真正理解对方,团队的集体经验和视角的有效性被削弱。时间在继续流逝,他们最终在没有明确解决方案的情况下结束了对话。
理解 Dataform 术语和身份验证流程
MLOps:数据管道编排
Dataform 101 系列第一部分:单仓库多环境 Dataform 的基础知识,带有最小权限访问控制和基础设施即代码设置
·发表于 Towards Data Science ·阅读时长 8 分钟·2024 年 5 月 15 日
--

Dataform 在数据管道中的典型定位 [图片来自作者]
Dataform是一个集成在 GCP 服务套件中的新服务,使团队能够开发和操作复杂的基于 SQL 的数据管道。Dataform 使得软件工程的最佳实践得以应用,如测试、环境管理、版本控制、依赖管理、编排和自动化文档等,这些都可以用于数据管道。它是 GCP 中一个无服务器的 SQL 工作流编排引擎。通常,如上图所示,Dataform 将原始数据进行转化,应用所有工程最佳实践,并输出一个结构良好的数据,准备好供后续使用。
这篇文章的灵感来自于我在将我们一个项目的遗留 Dataform 从 Web UI 迁移到 GCP BigQuery 时的经历。在迁移过程中,我发现像发布配置、工作流配置和开发工作区等术语非常令人困惑,难以理解。这也促使我写这篇文章,解释 GCP Dataform 中使用的一些新术语。此外,我还会简要介绍在 GCP 中使用单仓库多环境 Dataform 操作的一些基本流程。设置 Dataform 有多种方式,务必查看 Google 提供的最佳实践。
这是关于 Dataform 基础知识和设置的两部分系列中的第一部分。在第二部分中,我将提供 Terraform 设置的演示,展示如何在配置 Dataform 时实现最小权限控制。如果你想先睹为快,确保查看 repo。
术语
Dataform 的实现类似于 GitHub 工作流。我将对两者之间的相似性进行对比,并创建类比以帮助理解。可以把 Dataform 想象成一个本地的 GitHub 仓库。在设置 Dataform 时,它会请求配置一个远程仓库,类似于本地 GitHub 与远程 origin 配对。考虑到这种情境设置,我们快速浏览一些 Dataform 的术语。
开发工作空间
这类似于本地 GitHub 分支。类似于从 GitHub 主分支创建分支,新的 Dataform 开发工作空间会检出主 Dataform 仓库代码的可编辑副本。开发工作空间彼此独立,类似于 GitHub 分支。代码的开发和实验会在开发工作空间中进行,当代码被提交并推送后,会创建一个与开发工作空间名称相似的远程分支。值得一提的是,用于检出可编辑代码到开发工作空间的 GitHub 仓库是可配置的,可以是主分支或远程仓库中的任何其他分支。
发布配置
Dataform 使用 .sqlx 脚本和 Javascript .js 混合进行数据转换和逻辑处理。因此,它首先会生成代码库的编译版本,以获得标准化和可重复的代码库管道表示,并确保脚本能够转化为数据。发布配置是这一编译过程的自动化流程。在配置的时间,Dataform 会检出远程主仓库中的代码(这可以配置并修改为指向任何远程分支),并将其编译成 JSON 配置文件。检出代码和生成编译的过程正是发布配置所涵盖的内容。
工作流配置
因此,发布配置的输出是一个 .json 配置文件。工作流配置决定了何时运行该配置文件、由哪个身份运行以及该配置文件的输出将被写入哪个环境。
由于工作流配置需要使用发布配置的输出,因此合理的做法是确保它在发布配置之后运行。原因是发布配置首先需要对远程仓库进行身份验证(这一过程有时会失败),然后检出代码并进行编译。虽然这些步骤在几秒钟内完成,但在网络连接失败的情况下可能需要更多时间。由于工作流配置需要发布配置生成的.json编译文件,因此将其安排在发布配置之后执行是有意义的。如果同时安排,工作流配置可能会使用之前的编译结果,意味着直到下一个工作流配置运行时,BQ 表中的最新更改才会被反映出来。
环境

单一仓库、多环境 Dataform 的架构流程
Dataform 的一个特点是它能够将代码部署到不同的环境中,例如开发、暂存和生产环境。使用多个环境带来了如何设置 Dataform 的挑战。是否应该在多个环境中创建仓库,还是只在一个环境中创建?谷歌在 Dataform 最佳实践部分讨论了这些折衷。本篇文章展示了如何为暂存和生产环境设置 Dataform,并将数据从一个仓库部署到两个环境中。
这些环境被设置为 GCP 项目,并为每个项目配置了自定义服务帐户。Dataform 仅在暂存环境/项目中创建,因为我们会进行大量更改,最好在暂存(或非生产)环境中进行实验。此外,暂存环境被选为开发代码部署的环境。这意味着从开发工作空间生成的数据集和表会在暂存环境中进行部署。
一旦开发完成,代码就会被提交并推送到远程仓库。从那里,可以提出一个 PR(Pull Request),并在审核后合并到主仓库。在预定的工作流中,发布和工作流配置都会执行。Dataform 被配置为从主分支编译代码,并在生产环境中执行。因此,只有经过审核的代码会进入生产环境,任何开发中的代码会停留在暂存环境中。
总结一下,从上述 Dataform 架构流程来看,在开发工作空间中开发的代码会在暂存环境中进行部署,或者推送到远程 GitHub,在那里经过同行评审并合并到主分支。发布配置从主分支编译代码,而工作流配置则取用编译后的代码并将其数据部署到生产环境。因此,只有 GitHub 主分支中经过审核的代码才会在生产环境中部署。
身份验证
Dataform 的身份验证可能会很复杂,尤其是在为多个环境配置时。我将使用预发布和生产环境的示例来说明如何完成此过程。让我们逐步解析身份验证需要在哪里进行,以及如何完成身份验证。

用于追踪身份验证的 Dataform 流程
上图展示了一个简单的 Dataform 工作流,我们可以用它来追踪身份验证所需的地方以及对应的资源。该流程描述了当 Dataform 在开发工作区和计划(发布及工作流配置)中运行时发生的事情。
机器用户
让我们来讨论一下机器用户。当 Dataform 在检出存储在远程仓库中的代码时,需要凭证来访问 GitHub。虽然可以使用个人凭证,但最佳做法是使用组织中的机器用户。这种做法确保 Dataform 流水线编排不依赖于个人身份,且不会因其离职而受到影响。设置机器用户意味着使用与个人无关的身份来创建 GitHub 账户,具体设置请参见 这里。对于 Dataform,机器用户账户会生成一个个人访问令牌(PAT),并将其作为密钥存储在 GCP 秘密管理器中。机器用户还应作为外部协作者添加到 Dataform 的远程仓库,并授予读取和写入权限。稍后我们将在 Terraform 代码中看到如何配置 Dataform 以访问这些秘密。如果用户决定使用自己的身份而不是机器用户,则应按照 这里 的说明生成令牌。
GitHub 身份验证流程

使用机器用户的 GitHub 身份验证流程
Dataform 使用其默认服务账户进行操作,因此在执行 Dataform 操作时,首先使用默认服务账户。我假设你已经设置好了机器用户,将该用户添加为远程仓库的协作者,并将用户的 PAT 作为秘密添加到 GCP 秘密管理器中。为了进行 GitHub 身份验证,默认服务账户需要从秘密管理器中提取秘密。默认服务账户需要 secretAccessor 角色来访问该秘密。一旦访问了秘密,默认服务账户便可以模拟机器用户,并且由于机器用户已作为协作者添加到远程 Git 仓库,默认服务账户现在可以作为协作者访问远程 GitHub 仓库。该流程如 GitHub 身份验证流程图所示。
开发工作区身份验证
当从开发工作空间触发执行时,默认服务账户会假设暂存环境自定义服务账户的身份,以便在暂存环境中展示输出结果。为了能够模拟暂存环境自定义服务账户,默认服务账户需要在暂存服务账户上拥有iam.serviceAccountTokenCreator角色。这样,默认服务账户就可以创建一个短期有效的令牌,类似于用于模拟机器用户的 PAT(个人访问令牌),从而模拟暂存自定义服务账户。因此,暂存自定义服务账户被授予写入 BQ 表格所需的所有权限,且默认服务账户在模拟该账户时将继承这些权限。
工作流配置认证
在检出代码库后,发布配置将生成一个已编译的配置.json文件,工作流配置将从中生成数据。为了将数据写入生产环境的 BQ 表格,默认服务账户需要在生产环境自定义服务账户上拥有iam.serviceAccountTokenCreator角色。与暂存自定义服务账户的操作类似,生产服务账户被授予写入生产环境 BQ 表格所需的所有权限,且默认服务账户在模拟该账户时将继承所有这些权限。
总结
总结来说,默认服务账户是主要的执行者。它通过模拟机器用户,使用机器用户的 PAT 来认证 GitHub 上的协作者身份。它还通过模拟各自的自定义服务账户来认证暂存和生产环境,使用的是通过serviceAccountTokenCreator角色生成的短期有效令牌。了解了这一点后,我们就可以开始使用 Terraform 在 GCP 中配置 Dataform 了。敬请期待本文的第二部分,或查看代码库以获取代码。
图片来源: 本文中的所有图片均由作者制作
参考资料
理解 DDPG:解决连续动作控制挑战的算法
发现 DDPG 如何解决连续动作控制难题,释放人工智能驱动的医疗机器人的可能性。
·发表在 Towards Data Science ·阅读 10 分钟·2024 年 12 月 11 日
--
想象一下你在外科手术中控制机械手臂。离散动作可能包括:
-
向上移动,
-
向下移动,
-
抓取,或
-
发布
这些是清晰、直接的命令,在简单场景中易于执行。
但是如何执行精细的动作,比如:
-
将手臂移动 0.5 毫米以避免损伤组织,
-
施加 3N 的力以压缩组织,或
-
将手腕旋转 15° 调整切口角度?
在这些情况下,你需要的不仅仅是选择一个动作 —— 你必须决定需要多少的那个动作。这就是连续动作空间的世界,也是深度确定性策略梯度 (DDPG) 的闪亮之处!
传统方法如深度 Q 网络(DQN)在离散动作方面表现良好,但在连续动作方面表现不佳。而确定性策略梯度 (DPG) 则解决了这个问题,但面临着探索不足和不稳定性的挑战。DDPG 首次在 T P. Lillicrap 等人的论文中结合了 DPG 和 DQN 的优势,以提高在连续动作空间环境中的稳定性和性能。
在这篇文章中,我们将讨论 DDPG 背后的理论和架构,看看在 Python 上的实现,评估其性能(通过在 MountainCarContinuous 游戏上测试),并简要讨论 DDPG 如何在生物工程领域中使用。
DDPG 架构
与DQN不同,后者评估每一个可能的状态-动作对来寻找最佳动作(由于连续空间中组合的无限性,这在连续空间中不可能实现),DPG使用行动者-评论家架构。行动者学习一种策略,将状态直接映射到动作,避免了穷举搜索,专注于学习每个状态下的最佳动作。
然而,DPG 面临两个主要挑战:
-
它是一个确定性算法,限制了对动作空间的探索。
-
由于学习过程中的不稳定性,它无法有效使用神经网络。
DDPG通过引入探索噪声(通过奥恩斯坦-乌伦贝克过程)并通过批量归一化和 DQN 技术(如重放缓冲区和目标网络)来改进 DPG,从而稳定训练过程。
通过这些改进,DDPG 非常适合训练连续动作空间中的智能体,例如在生物工程应用中控制机器人系统。
现在,让我们来探讨一下 DDPG 模型的关键组件吧!
行动者-评论家框架
- 行动者(策略网络):告诉智能体在给定的状态下应该采取什么动作。网络的参数(即权重)由θμ表示。

提示! 把行动者网络想象成决策者:它将当前状态映射到一个单一的动作。
- 评论家(Q 值网络):通过估计该状态-动作对的 Q 值来评估行动者所采取的动作的优劣。

提示! 把评论家网络想象成评估者,它为每个动作分配一个质量分数,并帮助改进行动者的策略,确保其在每个给定的状态下确实生成最佳动作。
注意!评论家将使用估计的 Q 值来做两件事:
- 改进行动者的策略(行动者策略更新)。
行动者的目标是调整其参数(θμ),使其输出的动作最大化评论家的 Q 值。
为了做到这一点,行动者需要理解选定的动作 a 如何影响评论家的 Q 值,以及其内部参数如何影响其策略,这通过这个策略梯度方程来完成(它是从小批量中计算出的所有梯度的均值):

2. 通过最小化下面的损失函数来改进其自身的网络(评论家 Q 值网络更新)。

其中 N 是小批量中采样的经验数量,y_i 是目标 Q 值,计算方法如下。

重放缓冲区
当智能体探索环境时,过去的经验(状态、动作、奖励、下一个状态)作为元组(s,a,r,s′)存储在重放缓冲区中。在训练过程中,这些经验中的一些会随机组成小批量进行训练。
问题! 重放缓冲区如何减少不稳定性?
通过随机抽样经验,回放缓冲区打破了连续样本之间的相关性,减少了偏差,并导致更稳定的训练。
目标网络
目标网络是演员和评论家的缓慢更新副本。它们提供稳定的 Q 值目标,防止快速变化,并确保平稳、一致的更新。

问题! 目标网络是如何实际减少不稳定性的?
如果没有评论家目标网络,目标 Q 值将直接从评论家 Q 值网络计算,而评论家 Q 值网络是持续更新的。这会导致目标 Q 值在每一步发生变化,造成“移动目标”问题。结果,评论家不断追逐一个不断变化的目标,导致训练不稳定。
此外,由于演员依赖于评论家的反馈,一个网络中的错误可能会放大另一个网络中的错误,形成一个相互依赖的稳定性问题。
通过引入目标网络,并使用软更新规则逐步更新它们,我们确保目标 Q 值保持更一致,减少突变变化,从而提高学习的稳定性。
批量归一化
批量归一化对神经网络每一层的输入进行标准化,确保均值为零,方差为单位。
问题! 批量归一化是如何实际减少不稳定性的?
从回放缓冲区抽取的样本可能与实时数据的分布不同,从而导致网络更新时的不稳定性。
批量归一化确保输入的尺度一致,以防止由输入分布变化引起的更新不稳定。
探索噪声
由于演员的策略是确定性的,训练过程中会向动作添加探索噪声,鼓励智能体尽可能多地探索动作空间。

在 DDPG 论文中,作者使用了奥恩斯坦-乌伦贝克过程来生成时间相关噪声,以模拟现实世界的系统动态。
DDPG 伪代码:逐步拆解

伪代码摘自arxiv.org/abs/1509.02971(见‘参考文献’部分的参考文献 1)

图示由作者绘制
- 定义演员和评论家网络
class Actor(nn.Module):
"""
Actor network for the DDPG algorithm.
"""
def __init__(self, state_dim, action_dim, max_action,use_batch_norm):
"""
Initialise the Actor's Policy network.
:param state_dim: Dimension of the state space
:param action_dim: Dimension of the action space
:param max_action: Maximum value of the action
"""
super(Actor, self).__init__()
self.bn1 = nn.LayerNorm(HIDDEN_LAYERS_ACTOR) if use_batch_norm else nn.Identity()
self.bn2 = nn.LayerNorm(HIDDEN_LAYERS_ACTOR) if use_batch_norm else nn.Identity()
self.l1 = nn.Linear(state_dim, HIDDEN_LAYERS_ACTOR)
self.l2 = nn.Linear(HIDDEN_LAYERS_ACTOR, HIDDEN_LAYERS_ACTOR)
self.l3 = nn.Linear(HIDDEN_LAYERS_ACTOR, action_dim)
self.max_action = max_action
def forward(self, state):
"""
Forward propagation through the network.
:param state: Input state
:return: Action
"""
a = torch.relu(self.bn1(self.l1(state)))
a = torch.relu(self.bn2(self.l2(a)))
return self.max_action * torch.tanh(self.l3(a))
class Critic(nn.Module):
"""
Critic network for the DDPG algorithm.
"""
def __init__(self, state_dim, action_dim,use_batch_norm):
"""
Initialise the Critic's Value network.
:param state_dim: Dimension of the state space
:param action_dim: Dimension of the action space
"""
super(Critic, self).__init__()
self.bn1 = nn.BatchNorm1d(HIDDEN_LAYERS_CRITIC) if use_batch_norm else nn.Identity()
self.bn2 = nn.BatchNorm1d(HIDDEN_LAYERS_CRITIC) if use_batch_norm else nn.Identity()
self.l1 = nn.Linear(state_dim + action_dim, HIDDEN_LAYERS_CRITIC)
self.l2 = nn.Linear(HIDDEN_LAYERS_CRITIC, HIDDEN_LAYERS_CRITIC)
self.l3 = nn.Linear(HIDDEN_LAYERS_CRITIC, 1)
def forward(self, state, action):
"""
Forward propagation through the network.
:param state: Input state
:param action: Input action
:return: Q-value of state-action pair
"""
q = torch.relu(self.bn1(self.l1(torch.cat([state, action], 1))))
q = torch.relu(self.bn2(self.l2(q)))
return self.l3(q)
- 定义回放缓冲区
实现了一个 ReplayBuffer 类,用于存储和抽样前一节中讨论的过渡元组(s, a, r, s'),以支持小批量的离策略学习。
class ReplayBuffer:
def __init__(self, capacity):
self.buffer = deque(maxlen=capacity)
def push(self, state, action, reward, next_state, done):
self.buffer.append((state, action, reward, next_state, done))
def sample(self, batch_size):
return random.sample(self.buffer, batch_size)
def __len__(self):
return len(self.buffer)
- 定义 OU 噪声类
添加了一个 OUNoise 类来生成探索噪声,帮助智能体更有效地探索动作空间。
"""
Taken from https://github.com/vitchyr/rlkit/blob/master/rlkit/exploration_strategies/ou_strategy.py
"""
class OUNoise(object):
def __init__(self, action_space, mu=0.0, theta=0.15, max_sigma=0.3, min_sigma=0.3, decay_period=100000):
self.mu = mu
self.theta = theta
self.sigma = max_sigma
self.max_sigma = max_sigma
self.min_sigma = min_sigma
self.decay_period = decay_period
self.action_dim = action_space.shape[0]
self.low = action_space.low
self.high = action_space.high
self.reset()
def reset(self):
self.state = np.ones(self.action_dim) * self.mu
def evolve_state(self):
x = self.state
dx = self.theta * (self.mu - x) + self.sigma * np.random.randn(self.action_dim)
self.state = x + dx
return self.state
def get_action(self, action, t=0):
ou_state = self.evolve_state()
self.sigma = self.max_sigma - (self.max_sigma - self.min_sigma) * min(1.0, t / self.decay_period)
return np.clip(action + ou_state, self.low, self.high)
- 定义 DDPG 智能体
定义了一个 DDPG 类,它封装了智能体的行为:
- 初始化:创建演员和评论家网络,以及它们的目标副本和回放缓冲区。
class DDPG():
"""
Deep Deterministic Policy Gradient (DDPG) agent.
"""
def __init__(self, state_dim, action_dim, max_action,use_batch_norm):
"""
Initialise the DDPG agent.
:param state_dim: Dimension of the state space
:param action_dim: Dimension of the action space
:param max_action: Maximum value of the action
"""
# [STEP 0]
# Initialise Actor's Policy network
self.actor = Actor(state_dim, action_dim, max_action,use_batch_norm)
# Initialise Actor target network with same weights as Actor's Policy network
self.actor_target = Actor(state_dim, action_dim, max_action,use_batch_norm)
self.actor_target.load_state_dict(self.actor.state_dict())
self.actor_optimizer = optim.Adam(self.actor.parameters(), lr=ACTOR_LR)
# Initialise Critic's Value network
self.critic = Critic(state_dim, action_dim,use_batch_norm)
# Initialise Crtic's target network with same weights as Critic's Value network
self.critic_target = Critic(state_dim, action_dim,use_batch_norm)
self.critic_target.load_state_dict(self.critic.state_dict())
self.critic_optimizer = optim.Adam(self.critic.parameters(), lr=CRITIC_LR)
# Initialise the Replay Buffer
self.replay_buffer = ReplayBuffer(BUFFER_SIZE)
2. 动作选择:select_action方法根据当前策略选择动作。
def select_action(self, state):
"""
Select an action given the current state.
:param state: Current state
:return: Selected action
"""
state = torch.FloatTensor(state.reshape(1, -1))
action = self.actor(state).cpu().data.numpy().flatten()
return action
- 3. 训练:
train方法定义了如何使用来自重放缓冲区的经验更新网络。
注意! 由于论文介绍了使用目标网络和批量归一化来提高稳定性,我设计了train方法,允许我们切换这些方法的开关。这让我们可以比较智能体在有这些方法和没有这些方法时的表现。具体实现请见下面的代码。
def train(self, use_target_network,use_batch_norm):
"""
Train the DDPG agent.
:param use_target_network: Whether to use target networks or not
:param use_batch_norm: Whether to use batch normalisation or not
"""
if len(self.replay_buffer) < BATCH_SIZE:
return
# [STEP 4]. Sample a batch from the replay buffer
batch = self.replay_buffer.sample(BATCH_SIZE)
state, action, reward, next_state, done = map(np.stack, zip(*batch))
state = torch.FloatTensor(state)
action = torch.FloatTensor(action)
next_state = torch.FloatTensor(next_state)
reward = torch.FloatTensor(reward.reshape(-1, 1))
done = torch.FloatTensor(done.reshape(-1, 1))
# Critic Network update #
if use_target_network:
target_Q = self.critic_target(next_state, self.actor_target(next_state))
else:
target_Q = self.critic(next_state, self.actor(next_state))
# [STEP 5]. Calculate target Q-value (y_i)
target_Q = reward + (1 - done) * GAMMA * target_Q
current_Q = self.critic(state, action)
critic_loss = nn.MSELoss()(current_Q, target_Q.detach())
# [STEP 6]. Use gradient descent to update weights of the Critic network
# to minimise loss function
self.critic_optimizer.zero_grad()
critic_loss.backward()
self.critic_optimizer.step()
# Actor Network update #
actor_loss = -self.critic(state, self.actor(state)).mean()
# [STEP 7]. Use gradient descent to update weights of the Actor network
# to minimise loss function and maximise the Q-value => choose the action that yields the highest cumulative reward
self.actor_optimizer.zero_grad()
actor_loss.backward()
self.actor_optimizer.step()
# [STEP 8]. Update target networks
if use_target_network:
for param, target_param in zip(self.critic.parameters(), self.critic_target.parameters()):
target_param.data.copy_(TAU * param.data + (1 - TAU) * target_param.data)
for param, target_param in zip(self.actor.parameters(), self.actor_target.parameters()):
target_param.data.copy_(TAU * param.data + (1 - TAU) * target_param.data)
- 训练 DDPG 智能体
将所有定义的类和方法整合在一起,我们可以训练 DDPG 智能体。我的train_dppg函数遵循伪代码和 DDPG 模型图的结构。
提示: 为了便于理解,我已经在每个代码部分标注了与伪代码和图表中对应步骤的编号。希望这能有所帮助! 😃
def train_ddpg(use_target_network, use_batch_norm, num_episodes=NUM_EPISODES):
"""
Train the DDPG agent.
:param use_target_network: Whether to use target networks
:param use_batch_norm: Whether to use batch normalization
:param num_episodes: Number of episodes to train
:return: List of episode rewards
"""
agent = DDPG(state_dim, action_dim, 1,use_batch_norm)
episode_rewards = []
noise = OUNoise(env.action_space)
for episode in range(num_episodes):
state= env.reset()
noise.reset()
episode_reward = 0
done = False
step=0
while not done:
action_actor = agent.select_action(state)
action = noise.get_action(action_actor,step) # Add noise for exploration
next_state, reward, done,_= env.step(action)
done = float(done) if isinstance(done, (bool, int)) else float(done[0])
agent.replay_buffer.push(state, action, reward, next_state, done)
if len(agent.replay_buffer) > BATCH_SIZE:
agent.train(use_target_network,use_batch_norm)
state = next_state
episode_reward += reward
step+=1
episode_rewards.append(episode_reward)
if (episode + 1) % 10 == 0:
print(f"Episode {episode + 1}: Reward = {episode_reward}")
return agent, episode_rewards
性能与结果:评估 DDPG 的有效性
DDPG 在连续动作空间中的有效性是在MountainCarContinuous-v0环境中进行测试的,其中智能体学习如何获得动量,将汽车驶上陡坡。结果表明,使用目标网络和批量归一化比其他配置能更快地收敛,获得更高的奖励,并且学习更加稳定。

图表由作者生成

GIF 由作者生成
注意! 你可以通过在我的GitHub上运行代码来自己实现这一点,只需根据需要更改环境名称即可!
DDPG 在生物工程中的应用:解锁精确性与适应性
通过这篇博客文章,我们看到了 DDPG 是一个强大的算法,可以在具有连续动作空间的环境中训练智能体。通过结合 DPG 和 DQN 的技术,DDPG 提高了探索性、稳定性和性能——这些都是机器人手术和生物工程应用中的关键因素。
想象一下,一位机器人外科医生,像达芬奇系统那样,使用 DDPG 来实时控制精细的运动,确保精确调整而不出错。通过 DDPG,机器人可以将手臂的位置调整到毫米级别,在缝合时施加精确的力量,甚至在做出最佳切口时进行微小的手腕旋转。这样的实时精确性可以改变手术结果,减少恢复时间,并最小化人为错误。
但 DDPG 的潜力不仅仅局限于手术。它已经在推进生物工程,能够使机器人假肢和辅助设备复制人体肢体的自然运动(查看这篇超级有趣的文章!)。
现在我们已经介绍了 DDPG 背后的理论,是时候让你探索它的实现了。从简单的例子开始,逐步深入到更复杂的场景中吧!
参考文献
- Lillicrap TP, Hunt JJ, Pritzel A, Heess N, Erez T, Tassa Y, 等人. 基于深度强化学习的连续控制 [Internet]. arXiv; 2019. 可从以下网址获取:
arxiv.org/abs/1509.02971
了解去重方法:保护数据完整性的方法
增长和数据复杂性的增加使得数据去重变得更加重要
·发表于 Towards Data Science ·阅读时长 6 分钟·2024 年 12 月 20 日
--
数据重复仍然是许多组织面临的问题。尽管数据处理和存储系统随着技术进步快速发展,但所产生的数据复杂性也在增加。此外,随着大数据的普及和基于云的应用程序的使用,今天的组织必须越来越多地处理碎片化的数据源。

由 Damir 拍摄: www.pexels.com/photo/serene-lakeside-reflection-with-birch-trees-29167854/
忽视大量重复数据的现象将对组织产生负面影响。例如:
-
决策过程的中断。不干净的数据会偏差指标,无法反映实际情况。例如:如果一个客户实际上是同一个人,但在客户关系管理系统(CRM)中被表示为 2 或 3 个客户的数据,这可能会导致在预测收入时的扭曲。
-
存储成本膨胀,因为每一位数据都会占用存储空间。
-
客户体验的中断。例如:如果系统需要向客户提供通知或发送电子邮件,数据重复的客户很可能会收到多条通知。
-
这使得 AI 训练过程不够理想。当一个组织开始开发 AI 解决方案时,其中一个要求是使用干净的数据进行训练。如果数据中仍然有很多重复项,那么数据就不能算是干净的,而在强制用于 AI 训练时,可能会产生偏见的 AI。
考虑到当一个组织未尝试减少或消除数据重复时所产生的重大影响,数据去重过程变得越来越重要。确保数据质量也至关重要。系统的日益复杂性和精密性必须伴随适当的去重技术的演变。
在这个场合,我们将研究三种最新的去重方法,这些方法可以作为从业者规划去重过程的参考。
全局去重
这是消除多个存储位置中重复数据的过程。现在,许多组织将数据存储在多个服务器、数据中心或云端。全局去重确保仅存储数据的一个副本。
该方法通过创建一个全局索引来工作,全球索引是以唯一代码(哈希)的形式列出所有现有数据,使用如 SHA256 等算法表示每一条数据。当新文件被上传到服务器(例如服务器 1)时,系统将为该文件存储一个唯一代码。
在另一天,当用户向服务器 2 上传文件时,系统将把新文件的唯一代码与全局索引进行比较。如果新文件的唯一代码/哈希与全局索引相同,那么系统将不再将相同的文件存储在两个地方,而是会用指向服务器 1 上已有文件副本的引用/指针替换存储在服务器 2 上的重复文件。
使用这种方法,可以明显节省存储空间。如果结合数据虚拟化技术,当文件需要时,系统将从原始位置获取该文件,但所有用户仍然会感觉数据存在各自的服务器上。
下面的插图展示了全局去重的工作方式,其中每个服务器只存储原始数据的一份副本,其他服务器上的重复文件则被替换为指向原始文件的引用。

来源:作者
需要注意的是,全局去重方法并不实时工作,而是后处理的。这意味着该方法只能在文件进入存储后应用。
内联去重
与全局去重不同,这种方法在数据写入存储系统时实时工作。使用内联去重技术,重复数据会立即被替换为引用,而无需经过物理存储过程。
该过程从数据即将进入系统或文件上传开始,系统会立即将文件分成几个小块或数据块。使用如 SHA-256 等算法,每个数据块将被赋予一个哈希值作为唯一代码。例如:
Chunk1 -> hashA
Chunk2 -> hashB
Chunk3 -> hashC
系统随后会检查各个数据块的哈希值是否已经在存储索引中。如果发现某个数据块的唯一代码已经存在于存储的哈希中,系统将不会重新保存该数据块的物理数据,而是仅存储指向之前存储的原始数据块位置的引用。
每个唯一的数据块将被物理存储。
后续,当用户想要访问文件时,系统会根据引用重新排列现有数据块中的数据,从而使用户能够使用完整的文件。
内联去重被亚马逊 S3 或 Google Drive 等云服务提供商广泛使用。此方法对于优化存储容量非常有用。
下图简要说明了内联去重过程,从数据分块到数据如何访问。

来源:作者
增强型机器学习去重
基于机器学习的去重技术利用 AI 来检测和删除重复数据,即使它们并不完全相同。
该过程从传入数据(如文件/文档/记录)发送到去重系统进行分析开始。例如,系统接收两个扫描的文档,乍一看它们看起来相似,但实际上在布局或文本格式上有细微差异。
系统随后会智能地提取重要特征,通常以元数据或视觉模式的形式。这些重要特征会被分析并进行相似性比较。特征的相似度将以数值/分数的形式表示。每个系统/组织可以根据相似度分数来定义数据是否为重复数据。例如:只有相似度分数高于 90%的数据才可以认为是潜在重复的。
基于相似度评分,系统可以判断数据是否为重复数据。如果确定为重复数据,则可以采取类似其他去重方法的步骤,对于重复数据仅存储引用。
增强型机器学习去重的一个有趣之处在于,它允许人工参与验证系统已经完成的分类工作。这样,系统可以基于已学习的输入(反馈回路)不断变得更智能。
然而需要注意的是,与内联去重不同,增强型机器学习去重不适合用于实时处理。这是由于延迟因素,机器学习需要时间来提取特征和处理数据。此外,如果强行要求实时处理,这种方法需要更多的计算资源。
虽然不是实时的,但它带来的好处仍然是最优的,特别是在处理非结构化或半结构化数据的能力上。
以下是机器学习增强去重步骤的说明及示例。

来源:作者
从上面的解释可以看出,组织可以根据其能力和需求选择多种方法。因此,完全没有理由不进行去重,尤其是当组织存储或处理的数据是关乎许多人生命的关键数据时。
组织应能够使用若干考虑项来选择最合适的方法。去重的目的、数据类型和数据量以及组织的基础设施能力等方面可以作为初步评估的依据。
应注意,组织可以选择灵活的选项,例如将内联去重与机器学习增强方法相结合的混合方法。通过这种方式,可以获得更广泛的收益。
数据管理法规,如 GDPR 和 HIPAA,规范了敏感信息的处理。因此,组织需要确保去重过程不会违反隐私政策。例如:在发现重复数据后,组织可以合并来自两个不同系统的客户数据,而无需获得用户同意。组织必须确保此类事情不会发生。
无论面临什么挑战,去重仍然必须进行,组织需要从一开始就付出努力。不要等到数据变得更大,去重工作变得更加昂贵和复杂时才行动。
现在就开始,并在过程中收获收益。
理解爱因斯坦符号和 einsum 乘法

图片来源:Taton Moïse 于 Unsplash
使用字符串符号进行更高阶的张量操作
·发布于 Towards Data Science ·6 分钟阅读·2024 年 9 月 8 日
--
引言
机器学习在很大程度上依赖于线性代数运算,特别是,理解矩阵之间(更准确地说是张量之间)乘法如何工作至关重要。
我经常注意到,许多接触机器学习框架(如 PyTorch 或 TensorFlow)的人,在遇到张量尺寸不匹配的错误时,常常遇到很多问题。在这篇简短的文章中,我想对此做些澄清,并介绍 einsum 的使用。
从标量到张量
在机器学习中,我们根据数据的维度(dim)对其进行区分。常见的有:
-
标量 dim = 0
-
向量 dim = 1
-
矩阵 dim = 2
-
张量 dim = 3(或更多)

图片来源:作者
在 PyTorch 中,我们可以非常轻松地创建这些数据类型。
import torch
# Create a scalar tensor
scalar…
理解费曼的 H 统计量(H-stat)用于交互作用分析
这篇文章介绍了 H 统计量的直觉和数学原理,H-stat 用于在机器学习模型中寻找交互作用
·发表于Towards Data Science ·阅读时间 11 分钟·2024 年 5 月 28 日
--

(来源:作者)
吸入石棉会增加吸烟者比非吸烟者患肺癌的风险。药物的副作用可能依赖于性别。你的车随着年龄的增长会贬值——除非它是一辆经典车。这些都是交互作用的例子。识别并结合这些交互作用可以大大提高模型的准确性并改变模型的解释。那么问题是,我们如何找到这些交互作用呢?
一种方法是应用可解释人工智能方法来训练一个模型,针对你认为可能存在交互作用的特征。我们将探讨其中的一种方法——费曼的 H 统计量。这也被称为 H-stat 或 H 指数。具体来说,我们将:
-
在PDPs 和 ICE 图的帮助下,阐明该方法的直觉原理。
-
深入解释其数学原理。
-
了解其局限性及如何克服这些局限性。
我们将看到该统计量有两个版本。一个用于两个特征之间的交互作用,另一个用于一个特征与所有其他特征之间的交互作用。在探索其局限性时,我们还将看到存在一个非标准化版本的...
理解 LLaMa 2 中的幽灵注意力
本文解释了 LLaMa 2 论文中介绍的幽灵注意力微调方法。
·发布于Towards Data Science ·阅读时间 6 分钟·2024 年 1 月 23 日
--

DALL-E 生成的幽灵骆驼图像
问题
很多时候,我们希望 LLM 在接收到指令后能够一直遵循,直到另有指示。然而,如下面的例子所示,LLM 在几轮对话后可能会迅速忘记指令。

LLaMa 2 论文中的图 9,展示了在几轮对话后指令是如何被忽略的
让模型持续关注的一种方法是将指令附加到每个用户消息中。虽然这样可以有效,但也带来了更多的 tokens 进入上下文,从而限制了你的 LLM 可以进行的对话轮次。我们如何绕过这个问题?通过微调!幽灵注意力旨在让 LLM 在更多对话轮次中遵循指令。
方法论总结
让我们从将对话想象成一个数据数组开始。我们有一个用户消息,接着是一个助手消息,然后两者交替进行。当数据数组中的最后一项是用户消息时,我们期望 LLM 生成一条助手消息。
重要的是,我们确保指令只出现在第一次的用户消息中,因为在现实世界中,这可能是用户唯一一次自然地引入指令。

作者提供的图像:展示交替的用户和助手消息的数据数组
在我们的设置中,还有一个强化学习人类反馈(RLHF)模型,我们可以从中进行采样,并知道对提示的良好响应是什么样的。
在我们的样本和对话中,我们执行拒绝采样——让 LLM 生成任意数量的不同响应,然后使用 RLHF 模型对它们进行评分。我们保存排名最高的响应,并使用这些最高质量的响应来微调模型。

作者提供的图片:一张图示,展示了我们如何创建微调数据,以使模型能够在多轮对话中专注于指令。
当我们使用我们的对话和最佳样本进行微调时,我们将之前对话轮次中生成的所有标记的损失设置为零。据我了解,这是因为研究人员注意到这样可以提高性能。
值得指出的是,虽然幽灵注意力将与用于 Transformer 模型的自注意力机制进行交互,但幽灵注意力本身并不是自注意力的替代品,而是一种方法,用于为自注意力机制提供更好的数据,以便它能在较长的上下文中记住早期给出的指令。
生成指令
LLaMa 2 论文重点介绍了他们测试的三种特定类型的指令:(1)扮演公共人物,(2)使用特定语言进行交流,以及(3)享受特定爱好。由于可能的公共人物和爱好的范围很广,他们希望避免给大型语言模型(LLM)输入一个在训练数据中没有出现的爱好或人物。为了解决这个问题,他们让 LLM 生成它会被指示去扮演的爱好和公共人物的列表;如果它生成了该主题,那么它更有可能了解相关内容,从而减少产生幻觉的可能性。为了进一步改进数据,他们会尽量使指令简洁明了。文中没有讨论可以给出指令的类型是否有限制,因此可以推测,测试哪些类型的指令在通过幽灵注意力微调的模型上效果最好,取决于我们自己。
结果
那么,这种新方法对 LLM 的影响是什么呢?

LLaMa 2 论文中的图 28,展示了幽灵注意力对新指令的效果。
在论文中,他们附上了上面的图片,展示了模型如何对那些在其微调数据集中找不到的指令做出反应。左边是测试“始终以俳句回答”这一指令,右边是测试“在可能的情况下建议与建筑相关的活动”这一指令。虽然随着进展,俳句回答似乎漏掉了一些音节,但毫无疑问,模型在每次回答时都尽力保持一般格式。建筑相关的指令特别有趣,因为你可以看到模型在第一次消息中并未提及这一点,因为当时它并不相关,但后来却提出了这一话题。
你可以在 lmsys.org 的 llama-2 界面上亲自试试。你会发现,虽然它不像论文中的屏幕截图那样完美,但仍然比 LLaMa 1 版本好得多。

作者提供的图像:在 chat.lmsys.org 上截取的 llama-2–70b-chat 模型屏幕截图,展示了“用表情符号回应”的指令
重要的是,我们还看到这种方法对模型的注意力产生了影响。下图是模型对每个标记(token)所给予的注意力的热力图。图表的左侧和底部展示了输入到模型中的标记。我们看不到图表的右上部分,因为那部分生成的是剩余的内容,因此超出当前标记的部分对模型来说是不可见的。随着我们生成更多文本,可以看到更多标记变得可用。热力图通过较深的颜色表示更高的值,因此这里颜色越深,表示对这些标记的注意力越强。我们可以看到,“Act as Oscar Wilde”这些标记随着更多标记的生成,变得越来越深,表明它们得到了越来越多的关注。

LLaMa2 论文中的图 10,展示了在应用 Ghost Attention 前后注意力的热力图
论文告诉我们,在超过 20 轮之后,语境往往会被填充,从而导致注意力问题。有趣的是,他们在附录中提供的图表也显示,随着他们不断微调模型,RLHF 模型分配给它的分数不断下降。若能进一步探讨这个现象,可能是因为指令变得更长了,每一批次的复杂度增加,还是与他们用来训练模型的数据的某些限制有关。如果是后者,那么随着更多训练数据的加入,你可能会经历更多批次,才会看到分数下降。无论如何,利用 Ghost Attention 进行微调可能会出现边际效益递减的情况。

LLaMa 2 论文中的图 26,展示了奖励模型在每个批次后如何对提示样本进行评分
结束语
总的来说,LLaMa2 论文介绍了许多有趣的训练技巧,尤其是在大型语言模型(LLM)的领域内。随着这个领域每天都有突破性的研究成果发布,回顾这篇关键论文和 Ghost Attention,提出了一些有趣的问题。
由于 Ghost Attention 是一种使用近端策略优化(PPO)技术来微调模型的方法,关键问题之一是当我们使用直接偏好优化(DPO)时,这种方法的表现如何。DPO 不需要训练单独的 RLHF 模型然后进行采样以生成良好的微调数据,因此 Ghost Attention 中设置的损失可能变得不必要,从而可能大大提升该技术的效果。
随着大型语言模型(LLMs)在更多消费应用中的使用,保持 LLM 专注于指令的需求只会增加。Ghost Attention 在训练 LLM 通过多轮对话保持专注方面表现出了极大的潜力。时间将证明 LLM 在对话中能保持多长时间对指令的关注。
感谢阅读!
[1] H. Touvron 等人,Llama 2: Open Foundation and Fine-Tuned Chat Models (2023),arXiv
[2] R. Rafailov 等人,Direct Preference Optimization: Your Language Model is Secretly a Reward Model (2023),arXiv
理解交点(链条、分叉和碰撞器)及其在因果推断中的作用
通过相关性、独立性和回归来解释交点,从而理解它们在因果推断中的关键重要性
·发表于Towards Data Science ·阅读时间:25 分钟·2024 年 1 月 31 日
--

图片由Ricardo Gomez Angel提供,来源于Unsplash
介绍
因果推断是应用概率、可视化和机器学习来理解“为什么”这一问题的答案。
这是一个相对较新的数据科学领域,它有可能扩展预测算法的益处,预测算法解决的是潜在商业问题的症状,而通过确立因果关系,它可以永久性地治愈这些商业问题。
通常,因果推断会从数据集开始(像数据科学的任何其他分支一样),然后通过数据项之间关系的因果和效应的可视化表示来增强数据。常见的可视化形式是有向无环图(DAG)。
问题
有向无环图(DAG)看似简单,但它们隐藏了许多复杂性,必须充分理解这些复杂性,才能最大化因果推断技术的应用。
机会
理解 K-Fold 目标编码以应对高基数问题
平衡复杂性与性能:深入探讨 K-fold 目标编码
·发表于 Towards Data Science ·阅读时间 7 分钟·2024 年 10 月 26 日
--

图片由 Mika Baumeister 提供,来源于 Unsplash
引言
数据科学从业人员在处理不同项目中的多种数据类型时,常常会遇到许多挑战,每种数据类型都需要独特的处理方法。一个常见的障碍是处理传统机器学习模型难以有效处理的数据格式,导致模型性能不佳。由于大多数机器学习算法都是为数值数据优化的,因此将类别数据转换为数值形式至关重要。然而,这种转换往往会简化复杂的类别关系,特别是当特征具有高基数——即有大量独特的值时——这会使处理变得复杂,并影响模型的准确性。
高基数(High cardinality)指的是特征中唯一元素的数量,具体来说是机器学习中类别标签的不同计数。当一个特征拥有许多独特的类别标签时,它具有高基数,这可能会使模型处理变得复杂。为了使类别数据在机器学习中可用,这些标签通常会使用基于数据复杂性的编码方法转换为数值形式。一种常见的方法是独热编码(One-Hot Encoding),它为每个唯一标签分配一个独特的二进制向量。然而,面对高基数数据时,独热编码可能会显著增加数据的维度,导致复杂的高维数据集,这需要大量的计算能力来训练模型,并可能导致性能变慢。
考虑一个包含 2,000 个唯一 ID 的数据集,每个 ID 与仅三个国家中的一个相关联。在这种情况下,虽然 ID 特征的基数是 2,000(因为每个 ID 都是唯一的),但国家特征的基数只有 3。现在,假设有一个包含 100,000 个类别标签的特征,必须使用独热编码进行编码。这将创建一个极其高维的数据集,导致低效并消耗大量资源。
数据科学家广泛采用的解决方案是K 折目标编码。这种编码方法通过用基于 K 折交叉验证的目标均值替换类别标签,帮助减少特征的基数。通过聚焦于个别数据模式,K 折目标编码降低了过拟合的风险,帮助模型学习数据中的特定关系,而不是那些可能影响模型性能的过于一般化的模式。
它是如何工作的
K 折目标编码(K-Fold Target Encoding)涉及将数据集划分为几个大小相等的子集,称为“折叠”(folds),其中“K”表示这些子集的数量。通过将数据集折叠成多个组,这种方法为每个类别标签计算跨子集的加权平均值,从而增强编码的鲁棒性并减少过拟合的风险。

图 1. 印尼国内航班数据集 [1]
使用图 1中的一个示例,考虑一个印尼国内航班排放数据集,该数据集包含每个航班周期的排放数据,我们可以将这种技术付诸实践。这个数据集需要问的基本问题是:“通过观察特征‘HC 排放’,每个‘航空公司’的类别标签的加权平均值是多少?”然而,你可能会提出和许多人一样的问题:“但是,如果你只是使用目标特征来计算它们,这是不是会导致另一个高基数特征?”简单的回答是:“是的,可能会。”
为什么?
在某些情况下,当数据集非常大且目标特征高度随机且没有明显的模式时,K 折目标编码可能会为每个类别标签生成各种均值,可能会保留高基数,而不是减少它。然而,K 折目标编码的主要目的是解决高基数问题,而不是必然大幅度地减少它。此方法最适用于目标特征与每个类别标签中数据片段之间存在有意义的相关性时。
K 折目标编码是如何操作的?最简单的解释方法是,在每个折叠中,你都计算来自其他折叠的目标特征均值。这种方法为每个类别标签提供一个独特的权重,以数字值表示,使其更具信息量。让我们通过使用我们的数据集进行一个示例计算,来更清楚地理解这一点。

图 2. 印尼国内航班数据集经过 K 折分配后的结果。[1]
计算‘AirAsia’标签在第一次观测中的权重时,首先按照图 2所示将数据拆分为多个折叠。你可以手动分配折叠以确保均匀分布,或者使用以下示例代码自动化此过程:
import seaborn as sns
import matplotlib.pyplot as plt
# In order to split our data into several parts equally lets assign KFold numbers to each of the data randomly.
# Calculate the number of samples per fold
num_samples = len(df) // 8
# Assign fold numbers
df['kfold'] = np.repeat(np.arange(1, 9), num_samples)
# Handle any remaining samples (if len(df) is not divisible by 8)
remaining_samples = len(df) % 8
if remaining_samples > 0:
df.loc[-remaining_samples:, 'kfold'] = np.arange(1, remaining_samples + 1)
# Shuffle again to ensure randomness
fold_df = df.sample(frac=1, random_state=42).reset_index(drop=True)



图 3. 类别特定均值计算过程[1]
现在数据集已经分割成多个折叠,下一步是计算同一标签在其他折叠中的均值。例如,‘AirAsia’在折叠 1 中的值会使用折叠 2、3、4、5、6 等的均值,得到一个均值为 11.3。这一过程会在所有折叠中继续进行,因此折叠 2 将会融合折叠 1、3、4、5、6 等的均值。所有这些计算的最终结果在图 4中有所展示。

图 4. K 折目标编码(仅类别特定均值)结果[1]
该计算被称为“类别特定均值”,它定义了基于相似标签实例的每个类别标签的平均值。另一个重要的计算是“全局均值”,它定义了基于用户定义的全局均值权重的类别标签的平均强度。全局均值作为基准或“中立”编码,尤其对那些稀有类别有重要意义,因为这些类别的类别特定均值可能依赖于有限的数据点。
在 K 折目标编码中,类别特定均值和全局均值通常会结合在一起,以创建一个更加稳健和全面的表示。欲了解详细说明,请参见图 5。

图 5. K 折目标编码的数学形式。[1]
数学公式使得理解这个计算过程变得更加容易。在这里,m 代表一个用户定义的权重,用于控制全局均值在最终计算中的影响。现在,我们可以将这个公式应用到图 2中的数据集,并使用以下代码实现它。

图 6. K-Fold 目标编码过程(使用两种均值)。[1]
# First we have to encode the categorical features using K-Fold target encoding
def useful_feature(df, target, weight):
utilized_feature = [c for c in df.columns if c not in (target)]
obj = [col for col in df.columns if df[col].dtype == 'object']
global_mean = df[target].mean()
for objek in obj:
df[f"countperobject_{objek}"] = 0
df[f"meanperobject_{objek}"] = 0
# Compute aggregations
agg = df.groupby(objek)[target].agg(['count', 'mean']).reset_index()
counts = agg['count'].values
mean = agg['mean'].values
# Iterate over each row
for i in range(1, 9): # design regarding to the length of the k-fold
for index, row in df.iterrows():
# Get the category and target value
if row["kfold"] != i:
category = row[objek]
target_val = row[target]
# Get the count from agg
count_agg = agg[(agg[objek] == category)]['count'].values
if len(count_agg) > 0:
df.at[index, f"countperobject_{objek}"] = count_agg[0]
# Get the mean from agg
mean_agg = agg[(agg[objek] == category)]['mean'].values
if len(mean_agg) > 0:
df.at[index, f"meanperobject_{objek}"] = mean_agg[0]
# Now find the weighted mean
df[f"weightedmean_{objek}"] = ((df[f"countperobject_{objek}"] * df[f"meanperobject_{objek}"]) + (weight * global_mean))/(weight + df[f"countperobject_{objek}"])
encoding_maps = {}
for objek in obj:
encoding_maps[objek] = df.groupby(objek)[[f"countperobject_{objek}", f"meanperobject_{objek}"]].mean().to_dict()
return df, encoding_maps
现在,将相同的公式绘制到每个类别标签中,结果将类似于图 7。

图 7. K-Fold 目标编码最终结果(两种均值)。[1]
需要记住的是,如果你的训练数据集和测试数据集之间存在显著差异,这种方法可能会有风险。例如,如果在训练数据中,亚航(AirAsia)始终产生大量的 HC 排放,而在测试数据中,鹰航(Garuda)排放的 HC 较高并且分布均匀,那么模型可能会过拟合训练数据的模式,从而在新数据上的准确性较低。
感谢阅读本文,希望你能更好地了解什么是 K-Fold 目标编码以及何时使用它。去查看我的社交媒体,帮助我为未来的数据人才建设更好的社区!!!:
Linkedin: www.linkedin.com/in/fhlpmah/
Dev.to: dev.to/fhlpmah
Instagram: www.instagram.com/fmasmoro/
资源
[1] 图片由作者制作。
[2] 数据集由作者人工模拟。灵感来源:ICA 组织,2023,ICAO 航空器发动机排放数据集 www.easa.europa.eu/en/domains/environment/icao-aircraft-engine-emissions-databank。
[3] 所有代码均由作者编写。
理解 KL 散度、熵及相关概念
信息理论、机器学习和统计学中的重要概念
·发表于 Towards Data Science ·阅读时间 8 分钟·2024 年 10 月 8 日
--

图像由 Gemini AI 生成
引言
在信息理论、机器学习和统计学中,KL 散度(Kullback-Leibler 散度)是一个基础概念,帮助我们量化两个概率分布的差异。它通常用来衡量当一个概率分布用来近似另一个概率分布时,信息损失的量。本文将解释 KL 散度以及一些其他广泛使用的散度。
KL 散度
KL 散度,也叫相对熵,是一种衡量两个概率分布(P 和 Q)之间差异的方法。它通常表示为 —
这个方程比较了真实分布 P 与近似分布 Q。假设你在使用一个为某一分布(分布 Q)优化的编码系统来压缩数据,但实际数据来自另一个分布(分布 P)。KL 散度衡量了你的编码系统的低效程度。如果 Q 接近 P,则 KL 散度较小,意味着在近似过程中损失的信息较少。如果 Q 与 P 差异较大,则 KL 散度较大,表示信息丢失较为严重……
直观理解 KL 散度
一种构建性的方法来衡量分布差异。
·发表于 Towards Data Science ·6 分钟阅读·2024 年 2 月 12 日
--

图片由 Jeswin Thomas 提供,来源于 Unsplash
今天,我们将讨论 KL 散度,这是数据科学中常用的一个度量标准,用于衡量两个分布之间的差异。但在深入技术细节之前,我们先来解决一个常见的数学和统计学理解障碍。
许多时候,挑战在于方法。很多人将这些学科看作是一系列呈现为神圣真理的公式,导致学习者在理解其意义时感到困惑。以 KL 散度公式为例——乍一看它可能显得令人畏惧,容易让人产生沮丧和失败感。然而,数学并非在现实世界中是这样发展的。我们遇到的每一个公式,都是人类智慧的结晶,旨在解决特定的问题。
在本文中,我们将采用不同的视角,将数学视为一个创造性过程。我们将从问题开始,而不是从公式入手,提出这样一个问题:“我们需要解决什么问题,我们如何制定度量标准来应对它?”这种方法的转变可以为理解像 KL 散度这样的概念提供更直观的理解。
够了理论——让我们直面 KL 散度。想象一下,你是一个幼儿园老师,每年都对学生进行调查,询问他们最喜欢的水果,学生可以选择苹果、香蕉或哈密瓜。你每年都会对班上的所有学生进行调查,得到百分比数据,并将它们绘制在这些图表上。

考虑两个连续的年份:第一年,50%的人偏爱苹果,40%的人偏爱香蕉,10%的人选择哈密瓜。第二年,苹果偏好保持在50%,但分布发生了变化——现在,10%的人偏爱香蕉,40%的人偏爱哈密瓜。我们想要回答的问题是:第二年的分布与第一年相比有多大不同?
即使在深入数学之前,我们也认识到度量的一个关键标准。由于我们要衡量两个分布之间的差异,我们的度量(稍后我们将其定义为 KL 散度)必须是不对称的。换句话说,交换分布应该得到不同的结果,反映出每种情况中不同的参考点。
现在让我们进入这个构造过程。如果我们需要设计这个度量,我们该如何开始?一种方法是专注于每个分布中的元素——我们称它们为 A、B 和 C——并衡量它们在两年中的概率比。我们将在讨论中将这些分布表示为 P 和 Q,其中 Q 代表参考分布(第一年)。
例如,P(a)表示第二年学生喜欢苹果的比例(50%),而 Q(a)表示第一年学生有相同偏好的比例(也是 50%)。当我们将这些值相除时,我们得到 1,表示从一年到二年苹果偏好比例没有变化。类似地,我们计算 P(b)/Q(b) = 1/4,表示香蕉偏好有所下降,P(c)/Q(c) = 4,表示从第一年到第二年哈密瓜偏好的四倍增长。
这是一个好的第一步。为了保持数学上的简洁,如果我们对这三个比例取平均会怎样呢?每个比例反映了我们分布中元素之间的变化。通过将它们相加并除以三,我们可以得出一个初步的度量:

该度量提供了两个分布之间差异的指示。然而,让我们解决该方法引入的一个缺陷。我们知道,平均值可能会受到大数值的影响。在我们的例子中,比例¼和 4 代表了对立但相等的影响。然而,当取平均时,4 的影响占主导地位,可能会膨胀我们的度量。因此,简单的平均值可能不是理想的解决方案。
为了纠正这个问题,让我们探索一种变换。我们能否找到一个函数,记作 F,用来处理这些比例(1、¼、4),使其满足将对立影响视为相等的要求?我们寻求一个函数,其中,如果输入 4,得到一个特定值(y),而如果输入 1/4,得到(-y)。为了找出这个函数,我们将简单地映射函数的值,看看哪种我们已知的函数可能符合这种形式。
假设 F(4) = y 且 F(¼) = -y。这个属性并不仅限于数字 4 和¼;它适用于任何一对倒数数值。例如,如果 F(2) = z,那么 F(½) = -z。再加上一个点,F(1) = F(1/1) = x,我们发现 x 应该等于 0。
绘制这些点时,我们可以观察到一个独特的模式出现:

我相信我们许多人都同意,整体形状类似于对数曲线,这表明我们可以使用 log(x)作为我们的函数 F。我们不仅仅计算 P(x)/Q(x),而是应用对数变换,得到 log(P(x)/Q(x))。这种变换有助于消除大数值扭曲平均值的问题。如果我们对三种水果的对数变换进行求和并取平均,结果如下:

如果这就是我们的度量标准,那会有什么问题吗?
一个可能的担忧是,我们希望我们的度量能够优先考虑当前分布中流行的 x 值。更简单来说,如果在第二年,50 个学生喜欢苹果,10 个学生喜欢香蕉,40 个学生喜欢哈密瓜,那么我们应该更加重视苹果和哈密瓜的变化,而不是香蕉的变化,因为只有 10 个学生关心香蕉,因此它对当前人群的影响不会太大。
目前,我们给每次变化分配的权重是 1/n,其中 n 代表元素的总数。

让我们用基于当前分布中喜欢某种水果的学生比例的概率权重来代替这种等权重,表示为P(x)。

我所做的唯一变化是,将我们关心的每一项的权重替换为基于概率的权重,我们关注的程度与它在当前分布中的频率成正比,非常流行的事物获得更多优先权,而当前不流行的事物(即使它们在过去的分布中很受欢迎)对这个 KL 散度的贡献较小。
这个公式表示了 KL 散度的公认定义。该符号通常表示为KL(P||Q),表示 P 相对于 Q 的变化程度。

现在记住我们希望我们的度量是非对称的。我们满足这个条件了吗?在公式中交换 P 和 Q 会得到不同的结果,这与我们对非对称度量的要求一致。
总结
首先,我希望你能理解这里的 KL 散度,但更重要的是,我希望它没有像我们从一开始就从公式出发然后尽力理解它为什么是这样的那样让人害怕。
我在这里还想说的是,这是 KL 散度的离散形式,适用于我们讨论过的离散类别。对于连续分布,原理保持不变,唯一的区别是我们将求和替换为积分(∫)。
注意:除非另有说明,所有图片均为作者提供。
理解潜在狄利赫雷分配(LDA)——数据科学家的指南(第一部分)

图片由作者使用 Midjourney 生成
通过狗血统模型解释 LDA
·发表于Towards Data Science ·阅读时长 7 分钟·2024 年 2 月 9 日
--
机器学习算法现在变得如此易于访问,以至于我的非技术妻子常常问:“这不就是 ChatGPT 能做的事情吗?”
数据科学家们现在应该时刻保持警觉,了解机器学习算法背后的原因和机制。
这篇三部分的博客文章实际上是一次旅程,我试图向我的妻子解释潜在狄利赫雷分配(LDA,一种在所有数据科学家的工具箱中用于主题建模、推荐等的基础方法)是如何通过狗的血统模型来工作的。在系列的最后,你应该能够回答以下问题:
第一部分(我们现在在这里!):
-
LDA 是如何工作的?
-
如何向非技术人员解释 LDA?
第二部分(链接):
-
LDA 是如何逐步改进的?
-
LDA 是如何收敛的?
-
奖励:在这里获取你的 LDA 备忘单!
第三部分:
-
什么时候使用 LDA,什么时候不使用?
-
我们如何在 Python 中使用它?
-
LDAs(不包括 LLMs)的替代方案和变种有哪些?
理解潜在狄利克雷分配(LDA)——数据科学家的指南(第二部分)

除非另有说明,否则所有图表和图片均为作者所作(图像由作者使用 MidJourney 生成,已获得付费许可)
LDA 收敛性解释:通过狗血统模型
·发表于 Towards Data Science ·10 分钟阅读·2024 年 2 月 19 日
--
“如果我对狗品种分布的先验理解不准确,那我的 LDA 模型是否就注定失败?”
我的妻子问道。
欢迎回到本系列的第二部分,在这一部分,我将分享如何向我的妻子解释 LDA 的经历。在上一篇博客中,我们讨论了 LDA 是如何工作的,并且如何将其理解为一种狗的血统模型。
这次我们将深入探讨 LDA 的迭代拟合过程!
第一部分(链接):
-
LDA 是如何工作的?
-
如何向非技术人员解释 LDA?
第二部分(我们现在就在这里!):
-
LDA 是如何在迭代中改进的?
-
LDA 是如何收敛的?
-
奖励:在这里获取你的 LDA 备忘单!
第三部分:
-
什么时候使用 LDA,什么时候不使用?
-
我们如何在 Python 中使用它?
-
LDA 有哪些替代方案和变种(不包括 LLM)?
从零开始使用中学数学理解 LLM
一篇完整的、关于 LLM 内部工作原理的自包含解释
·发表于 Towards Data Science ·40 分钟阅读·2024 年 10 月 19 日
--
在本文中,我们将讨论大型语言模型(LLMs)是如何工作的,从零开始——假设你只知道如何加法和乘法两个数字。本文旨在完全自包含。我们从在纸上构建一个简单的生成式 AI 开始,然后逐步讲解我们需要了解现代 LLM 和 Transformer 架构的所有内容。文章将去除所有机器学习中的复杂语言和术语,将一切简单呈现为它们本身:数字。我们仍然会指出每个概念的名称,以帮助你在阅读术语较多的内容时保持思路清晰。
从加法/乘法到今天最先进的 AI 模型,而不假设任何其他知识或参考其他来源,这意味着我们涵盖了大量的内容。这不是一个玩具版的 LLM 解释——一个有决心的人理论上可以从所有这些信息中重建一个现代的 LLM。我已删除了所有不必要的词/行,因此本文并不适合随便浏览。
我们将涵盖哪些内容?
-
一个简单的神经网络
-
这些模型是如何训练的?
-
这一切是如何生成语言的?
-
什么使得 LLM 能够如此高效地工作?
-
嵌入
-
子词分词器
-
自注意力
-
Softmax
-
残差连接
-
层归一化
-
Dropout(丢弃法)
-
多头注意力
-
位置嵌入
-
GPT 架构
-
Transformer 架构
让我们开始吧。
首先要注意的是,神经网络只能接受数字作为输入,并且只能输出数字。没有例外。关键在于如何将输入作为数字馈送到网络中,以一种实现目标的方式解释输出数字。最后,构建神经网络,使其接受您提供的输入并给出您想要的输出(考虑到您为这些输出选择的解释)。让我们看看如何从加法和乘法数字到像Llama 3.1这样的事物。
一个简单的神经网络:
让我们来看一个简单的可以对对象进行分类的神经网络:
-
可用的对象数据: 主要颜色(RGB)和体积(毫升)
-
分类为: 叶子 或 花朵
以下是叶子和向日葵的数据示例:

作者提供的图片
现在让我们构建一个可以进行这种分类的神经网络。我们需要决定输入/输出的解释。我们的输入已经是数字,所以我们可以直接将它们馈送到网络中。我们的输出是两个对象,叶子和花朵,神经网络无法直接输出。让我们看看我们可以在这里使用的几种方案:
-
我们可以使网络输出一个数字。如果这个数字是正的,我们说它是叶子,如果是负的,我们说它是花朵。
-
或者,我们可以使网络输出两个数字。我们将第一个解释为叶子的数字,第二个解释为花朵的数字,我们将选择较大的数字作为选择。
这两种方案都允许网络输出我们可以解释为叶子或花朵的数字。让我们选择第二种方案,因为它对我们稍后将要查看的其他事物具有很好的泛化能力。这里是一个使用这种方案进行分类的神经网络。让我们逐步进行:

作者提供的图片
一些行话:
神经元/节点:圆圈中的数字
权重:线上的有色数字
层:一组神经元称为一层。你可以将这个网络看作有 3 层:具有 4 个神经元的输入层,具有 3 个神经元的中间层,以及具有 2 个神经元的输出层。
要计算这个网络的预测/输出(称为“前向传播”),你从左边开始。我们有输入层神经元的数据可用。要向“前”移动到下一层,你将圆圈中的数字与相应神经元配对的权重相乘,然后将它们全部加起来。我们在上面展示了蓝色和橙色圆圈的数学运算。运行整个网络,我们看到输出层中的第一个数字较高,因此我们将其解释为“网络将这些(RGB,Vol)值分类为叶子”。一个训练良好的网络可以接受各种(RGB,Vol)的输入,并正确地对对象进行分类。
模型不知道叶子或花是什么,也不知道(RGB,Vol)是什么。它的任务是接收确切的 4 个数字并输出确切的 2 个数字。我们的解释是,这 4 个输入数字是(RGB,Vol),我们也决定查看输出数字并推断,如果第一个数字较大,则是叶子等。最后,我们还可以选择正确的权重,使模型接受我们的输入数字并给出我们需要的正确两个数字,这样当我们解释它们时,我们得到我们想要的解释。
这样做的一个有趣的副作用是,你可以拿同样的网络,而不是输入 RGB、Vol,而是输入其他 4 个数字,比如云量、湿度等,然后将这两个数字解释为“一个小时后晴天”或“一个小时后下雨”,如果你的权重校准得当,你可以让同一个网络同时做两件事情——分类叶子/花朵和预测一个小时后的降雨情况!网络只会给你两个数字,你如何解释它,是分类还是预测或其他什么,完全取决于你。
简化的内容已被省略(可以忽略而不影响理解):
-
激活层:这个网络缺少的一个关键部分是“激活层”。这是一个花哨的说法,意思是我们取每个圆圈中的数字并对其应用一个非线性函数(RELU是一个常见的函数,如果数字为负数,则将其设为零,如果为正数则保持不变)。所以基本上在我们上面的例子中,我们会取中间层并在继续到下一层之前用零替换这两个数字(-26.6 和-47.1)。当然,我们必须重新训练这里的权重,使网络再次变得有用。没有激活层,网络中的所有加法和乘法可以折叠成一个单一层。在我们的情况下,你可以将绿色圆圈写成 RGB 的和直接乘以一些权重,而不需要中间层。它会类似于(0.10 * -0.17 + 0.12 * 0.39–0.36 * 0.1)* R + (-0.29 * -0.17–0.05 * 0.39–0.21 * 0.1) * G …等等。如果我们在那里没有非线性,通常是不可能的。这有助于网络处理更复杂的情况。
-
偏差:网络通常还会包含与每个节点相关的另一个数字,这个数字简单地添加到计算节点值的乘积中,这个数字称为“偏差”。因此,如果顶部蓝色节点的偏差为 0.25,则节点中的值将是:(32 * 0.10)+(107 * -0.29)+(56 * -0.07)+(11.2 * 0.46)+ 0.25 = — 26.35。参数这个词通常用来指代模型中所有这些不是神经元/节点的数字。
-
Softmax:我们通常不会像在我们的模型中那样直接解释输出层的值。我们将数字转换为概率(即使所有数字都是正数并且加起来等于 1)。如果输出层中的所有数字已经是正数,一种方法是将每个数字除以输出层所有数字的总和。尽管通常会使用“softmax”函数,它可以处理正数和负数。
这些模型是如何训练的?
在上面的例子中,我们神奇地得到了使得数据能够输入模型并获得良好输出的权重。但是这些权重是如何确定的呢?设置这些权重(或“参数”)的过程叫做“训练模型”,而我们需要一些训练数据来训练模型。
假设我们有一些数据,其中包含输入数据,并且我们已经知道每个输入是否对应叶子或花朵,这就是我们的“训练数据”,而由于我们对每一组(R, G, B, Vol)数据都知道是叶子还是花朵的标签,这就是“标注数据”。
这是其工作原理:
-
从随机数开始,即将每个参数/权重设置为随机数。
-
现在,我们知道当输入对应叶子时(R=32, G=107, B=56, Vol=11.2)。假设我们希望在输出层中叶子对应的数字更大。我们假设叶子的数字应为 0.8,花朵的数字应为 0.2(如上例所示,但这些数字只是为了演示训练,实际上我们不会希望是 0.8 和 0.2,实际情况是这些数字应该是概率值,而不是这里的值,实际上我们会希望它们分别为 1 和 0)。
-
我们知道输出层中我们想要的数字,以及我们从随机选择的参数中得到的数字(这些数字与我们想要的不同)。因此,对于输出层中的所有神经元,我们计算想要的数字和得到的数字之间的差异。然后将这些差异加总。例如,如果输出层中有两个神经元分别为 0.6 和 0.4,那么我们得到:(0.8–0.6)=0.2 和 (0.2–0.4)=-0.2,总和为 0.4(忽略负号后相加)。我们可以称之为我们的“损失”。理想情况下,我们希望损失接近零,即我们想要“最小化损失”。
-
一旦我们得到损失值,我们可以稍微调整每个参数,看看增加或减少它是否会增加损失。这被称为该参数的“梯度”。然后我们可以稍微调整每个参数,朝着损失减小的方向(即梯度的反方向)移动。一旦我们轻微调整了所有参数,损失应该会降低。
-
不断重复这个过程,你会减少损失,最终得到一组“训练好的”权重/参数。整个过程被称为“梯度下降”。
几个备注:
-
通常你会有多个训练样本,因此当你稍微调整权重以最小化一个样本的损失时,它可能会使另一个样本的损失变得更糟。解决这个问题的方法是将损失定义为所有样本的平均损失,然后对该平均损失进行梯度计算。这样可以减少整个训练数据集的平均损失。每一个这样的周期称为“轮次”。然后你可以不断重复这些轮次,从而找到能减少平均损失的权重。
-
我们实际上不需要“调整权重”来计算每个权重的梯度——我们可以从公式中推导出它(例如,如果上一步的权重是 0.17,且神经元的值为正,我们希望输出的数值更大,那么我们可以看到将这个数字增加到 0.18 会有所帮助)。
在实践中,训练深度网络是一个困难且复杂的过程,因为梯度在训练过程中很容易失控,可能会变为零或无穷大(这被称为“消失梯度”和“爆炸梯度”问题)。我们在这里讨论的简单损失定义是完全有效的,但很少使用,因为有更好的功能形式可以很好地解决特定目的。随着现代模型包含数十亿个参数,训练一个模型需要巨大的计算资源,这也带来了自身的问题(如内存限制、并行化等)。
所有这些如何帮助生成语言呢?
记住,神经网络接受一些数字,基于训练好的参数进行一些数学运算,然后输出一些其他的数字。一切都与解释和训练参数(即将它们设置为某些数字)有关。如果我们能将这两个数字解释为“叶子/花”或“一小时后的雨或晴”,我们也可以将它们解释为“句子中的下一个字符”。
但英语中不仅仅有两个字母,因此我们必须将输出层的神经元数量扩展到英语中的 26 个字母(我们还可以加入一些符号,如空格、句号等)。每个神经元可以对应一个字符,我们查看输出层中大约 26 个神经元,并说输出层中对应最大编号的神经元所代表的字符就是输出字符。现在我们有了一个可以接受一些输入并输出字符的网络。
如果我们用这些字符替换网络中的输入:“Humpty Dumpt”,并要求它输出一个字符并将其解释为“网络对我们刚输入的序列中下一个字符的建议”,我们可能可以将权重设置得足够好,使其输出“y”——从而完成“Humpty Dumpty”。除了一个问题,我们如何将这些字符列表输入到网络中呢?我们的网络只接受数字!!
一个简单的解决方案是为每个字符分配一个数字。假设 a=1,b=2,依此类推。现在我们可以输入“humpty dumpt”,并训练它给出“y”。我们的网络大概是这样的:

图片来源:作者
好的,现在我们可以通过给网络输入一个字符列表来预测下一个字符。我们可以利用这个事实来构建一个完整的句子。例如,一旦我们预测出了“y”,我们就可以把这个“y”加到我们已有的字符列表中,再次输入到网络中并让它预测下一个字符。如果训练得当,它应该给我们一个空格,依此类推。最终,我们应该能够递归地生成“Humpty Dumpty sat on a wall”。这就是生成式 AI。而且,我们现在有了一个能够生成语言的网络!现在,没有人会真正地输入随机分配的数字,我们将看到更为合理的编码方案。如果你迫不及待,欢迎查看附录中的独热编码部分。
精明的读者会注意到,我们实际上无法将“Humpty Dumpty”输入到网络中,因为根据图示,输入层只有 12 个神经元,每个神经元对应“humpty dumpt”中的一个字符(包括空格)。那么,我们该如何输入下一个“y”呢?如果放一个第 13 个神经元,我们就需要修改整个网络,这是不可行的。解决方案很简单,让我们把“h”去掉,输入最近的 12 个字符。所以我们将输入“umpty dumpty”,网络会预测一个空格。然后我们再输入“mpty dumpty ”,它会输出一个“s”,依此类推。它看起来像这样:

作者提供的图片
通过只给模型输入“ sat on the wal”,我们在最后一行丢失了很多信息。那么,今天最新最强大的网络是如何做的呢?或多或少,正是这样。我们能够输入到网络中的输入长度是固定的(由输入层的大小决定)。这叫做“上下文长度”——即提供给网络的上下文,用于进行未来的预测。现代网络可以有非常大的上下文长度(几千个单词),这有助于提升效果。虽然有一些方法可以输入无限长度的序列,但这些方法的表现虽然令人印象深刻,但后来被其他具有大(但固定)上下文长度的模型所超越。
另外,细心的读者会注意到,我们对同一个字母的输入和输出有不同的解释!例如,当输入“h”时,我们只是用数字 8 表示它,但在输出层,我们并不是让模型输出一个单一的数字(“h”是 8,“i”是 9,依此类推),而是让模型输出 26 个数字,然后查看哪个数字最大,如果第 8 个数字最大,我们就把输出解释为“h”。为什么我们不在两端使用相同的一致解释呢?我们可以这样做,只是对于语言来说,允许自己在不同解释之间选择,会给你更大的机会去构建更好的模型。恰好目前已知最有效的输入和输出解释是不同的。事实上,我们目前在模型中输入数字的方式并不是最好的做法,我们很快就会讨论更好的方法。
是什么让大型语言模型如此高效?
逐个字符生成“Humpty Dumpty sat on a wall”与现代大型语言模型所能做到的差距很大。从我们上面讨论的简单生成式人工智能,到类人机器人的这一过程中,有许多差异和创新。让我们一一梳理:
嵌入
记得我们提到过,我们将字符输入模型的方式并不是最好的方法。我们只是随意地为每个字符选择了一个数字。如果我们能分配更好的数字来帮助我们训练出更好的网络,那会怎么样呢?我们如何找到这些更好的数字?这里有一个巧妙的技巧:
当我们训练上面提到的模型时,我们的做法是通过调整权重并观察最终能带来更小的损失值,然后不断递归地改变权重。在每次调整时,我们会:
-
输入数据
-
计算输出层
-
与我们理想中想要的输出进行比较,并计算平均损失
-
调整权重并重新开始
在这个过程中,输入是固定的。当输入是(RGB,Vol)时这是有意义的。但是我们现在放入的数字 a、b、c 等等是我们随意选择的。如果在每次迭代中,除了稍微调整权重外,我们还移动输入并看看是否可以通过使用不同的数字来表示“a”等等来获得更低的损失?我们肯定在减少损失并使模型更好(这是我们设计的 a 的输入移动的方向)。基本上,不仅对权重应用梯度下降,还对输入的数字表示应用梯度下降,因为它们无论如何都是任意选择的数字。这被称为“嵌入”。这是将输入映射到数字的过程,正如你刚刚看到的,它需要被训练。训练嵌入的过程很像训练参数的过程。不过,其中一个很大的优势是,一旦你训练了一个嵌入,你可以在另一个模型中使用它。请记住,你将始终使用相同的嵌入来表示单个标记/字符/单词。
我们谈到的嵌入只是每个字符一个数字。然而,在现实中,嵌入不止一个数字。这是因为用一个数字很难捕捉概念的丰富性。如果我们看看我们的叶子和花朵的例子,我们为每个对象有四个数字(输入层的大小)。这四个数字中的每一个传达了一个属性,模型能够有效地使用所有这些数字来猜测对象。如果我们只有一个数字,比如颜色的红色通道,对于模型来说可能会更难。我们正在捕捉人类语言 —— 我们需要不止一个数字。
所以,与其用一个数字代表每个字符,也许我们可以用多个数字来捕捉丰富性?让我们为每个字符分配一堆数字。让我们把一组有序的数字称为“向量”(有序的意思是每个数字都有一个位置,如果我们交换两个数字的位置,就会得到一个不同的向量。这在我们的叶子/花朵数据中是这样的,如果我们交换叶子的 R 和 G 数字,我们会得到一个不同的颜色,它不再是同一个向量了)。向量的长度就是它包含的数字数量。我们将为每个字符分配一个向量。有两个问题出现:
-
如果我们为每个字符分配一个向量而不是一个数字,那么我们现在如何将“humpty dumpt”输入到网络中?答案很简单。假设我们为每个字符分配了一个由 10 个数字组成的向量。那么,输入层不再有 12 个神经元,而是放置 120 个神经元,因为“humpty dumpt”中的每个 12 个字符都有 10 个数字要输入。现在我们只需将神经元放在一起,就可以开始了。
-
我们如何找到这些向量?幸运的是,我们刚刚学会了如何训练嵌入向量。训练嵌入向量和训练数字没有什么不同。你现在有 120 个输入,而不是 12 个,但你所做的只是移动它们,看看如何最小化损失。然后,你取其中的前 10 个,那就是对应“h”的向量,依此类推。
所有的嵌入向量当然必须是相同长度的,否则我们就无法将所有字符组合输入到网络中。例如,“humpty dumpt”和下一次迭代中的“umpty dumpty”——在这两种情况下,我们都在输入 12 个字符到网络中,如果这 12 个字符中的每个没有被表示为长度为 10 的向量,我们就无法将它们可靠地输入到一个长度为 120 的输入层中。让我们来可视化这些嵌入向量:

图片由作者提供
让我们称一组大小相同的向量为矩阵。上面的矩阵叫做嵌入矩阵。你给它一个对应字母的列号,然后查看矩阵中的那一列,你就能得到表示那个字母的向量。这个方法更一般地应用于嵌入任意事物的集合——你只需要保证矩阵中的列数等于你要表示的事物数量。
子词 Token 化器
到目前为止,我们一直在使用字符作为语言的基本构建块。这有其局限性。神经网络的权重需要做大量的工作,它们必须理解特定字符序列(即单词)如何相邻出现,然后再与其他单词相邻。如果我们直接为单词分配嵌入并让网络预测下一个单词会怎么样呢?反正网络只理解数字,所以我们可以为单词“humpty”、“dumpty”、“sat”、“on”等分配一个 10 维的向量,然后我们只需要输入两个单词,它就能给我们预测下一个单词。“Token”是我们嵌入并传递给模型的单一单位。我们目前的模型使用字符作为 Token,现在我们提出使用整个单词作为 Token(如果你愿意,你当然可以使用整个句子或短语作为 Token)。
使用词汇分词对我们的模型有一个深远的影响。英语中有超过 18 万个单词。采用我们输出解释方案,即每个可能的输出对应一个神经元,我们需要数十万个神经元来构成输出层,而不是大约 26 个神经元。随着隐层规模的增大,以达到现代网络所需的有意义结果,这个问题变得不那么紧迫。然而需要注意的是,由于我们是将每个单词单独处理,并且我们从每个单词的随机数嵌入开始——非常相似的单词(例如“cat”和“cats”)将从无关联开始。你会期望这两个单词的嵌入应该相互接近——这无疑是模型会学习到的。但是,我们是否可以以某种方式利用这种显而易见的相似性来加速启动并简化问题呢?
是的,我们可以。今天语言模型中最常见的嵌入方案是将单词分解成子词,然后进行嵌入。在“cat”这个例子中,我们将“cats”分解成两个标记——“cat”和“s”。现在,模型更容易理解“s”后面跟随其他熟悉单词的概念,依此类推。这也减少了我们需要的标记数量(sentencepiece是一个常见的分词器,其词汇量选项在几万到数十万个单词之间,而英语则有数十万个单词)。分词器是一个工具,它接受输入文本(例如“Humpty Dumpt”)并将其拆分为标记,随后返回对应的数字,以便在嵌入矩阵中查找该标记的嵌入向量。例如,在“humpty dumpty”这个例子中,如果我们使用字符级分词器,并且按照上图排列嵌入矩阵,那么分词器将首先把“humpty dumpt”拆分成字符[‘h’,‘u’,…‘t’],然后返回数字[8,21,…20],因为你需要查找嵌入矩阵中的第 8 列,以获得'h'的嵌入向量(嵌入向量是你要输入到模型中的内容,而不是数字 8,与之前不同)。矩阵列的排列完全无关紧要,我们可以为'h'指定任何列,只要每次输入'h'时查找相同的向量就可以。分词器给我们一个任意(但固定)的数字,使查找变得容易。我们真正需要它们做的主要任务是将句子拆分成标记。
使用嵌入和子词分词时,模型可能是这样的:

作者提供的图片
接下来的几节将讨论语言模型中的一些最新进展,以及使 LLM(大型语言模型)如此强大的技术。然而,为了理解这些进展,你需要掌握一些基本的数学概念。以下是这些概念:
-
矩阵和矩阵乘法
-
数学中函数的一般概念
-
数字的幂运算(例如 a³ = a * a * a)
-
样本均值、方差和标准差
我已在附录中添加了这些概念的总结。
自注意力
到目前为止,我们只看到了一个简单的神经网络结构(称为前馈网络),它包含多个层,每一层都与下一层完全连接(即,连续层之间有连接线),并且只与下一层连接(例如,第 1 层和第 3 层之间没有连接等)。然而,正如你可以想象的那样,没有什么能阻止我们移除或建立其他连接,甚至可以构建更复杂的结构。让我们来探讨一个特别重要的结构:自注意力。
如果你看一下人类语言的结构,我们想要预测的下一个单词将依赖于之前的所有单词。然而,它们可能比其他单词更依赖于某些前面的单词。例如,如果我们试图预测“Damian had a secret child, a girl, and he had written in his will that all his belongings, along with the magical orb, will belong to ____”中的下一个单词,这个单词可以是“her”或“him”,它特别依赖于句子中更早的一个单词:girl/boy。
好消息是,我们简单的前馈模型连接了上下文中的所有单词,因此它可以学习重要单词的适当权重。但问题在于,通过前馈层连接我们模型中特定位置的权重是固定的(对于每个位置都是如此)。如果重要单词总是出现在相同的位置,它将适当地学习权重,我们就可以顺利进行。然而,下一次预测的相关单词可能出现在系统的任何位置。我们可以改写上述句子,在猜测“her”与“his”时,对于这个预测而言,无论该单词出现在句子中的哪个位置,“boy/girl”这个单词都是一个非常重要的单词。因此,我们需要的权重不仅取决于位置,还要依赖于该位置的内容。我们如何实现这一点呢?
自注意力做的是类似于将每个单词的嵌入向量相加,但它不是直接相加,而是对每个向量应用一些权重。因此,如果“humpty”、“dumpty”和“sat”的嵌入向量分别是 x1、x2、x3,那么它会在相加之前将每个向量与一个权重(一个数字)相乘。类似于输出 = 0.5 x1 + 0.25 x2 + 0.25 x3,其中 output 是自注意力的输出。如果我们将权重写作 u1、u2、u3,使得输出 = u1x1 + u2x2 + u3x3,那么我们如何找到这些权重 u1、u2、u3 呢?
理想情况下,我们希望这些权重依赖于我们正在加权的向量——正如我们所看到的,有些可能比其他的更重要。但对于谁来说更重要?对于我们即将预测的单词。所以我们也希望权重依赖于我们即将预测的单词。现在这是一个问题,因为在预测之前我们当然不知道我们将要预测哪个单词。那么,自注意力机制使用的是我们即将预测的单词前面的那个单词,即句子中可用的最后一个单词(我其实不知道为什么是这样,而不是别的什么,但深度学习中的许多事情都是试错法,我怀疑这种方法效果很好)。
很好,所以我们需要这些向量的权重,并且我们希望每个权重依赖于我们正在聚合的单词和即将预测单词前面的那个单词。基本上,我们希望有一个函数 u1 = F(x1, x3),其中 x1 是我们将要加权的单词,x3 是我们所拥有的序列中的最后一个单词(假设我们只有 3 个单词)。现在,达成这一目标的一种直接方法是为 x1 建立一个向量(我们称之为 k1),为 x3 建立一个单独的向量(我们称之为 q3),然后简单地计算它们的点积。这样我们就能得到一个数值,它将依赖于 x1 和 x3。那么我们如何获得这些向量 k1 和 q3 呢?我们构建一个小型的单层神经网络,将 x1 映射到 k1(或者 x2 映射到 k2,x3 映射到 k3,依此类推)。我们再构建另一个网络,将 x3 映射到 q3,依此类推… 使用我们的矩阵表示法,基本上我们会得到权重矩阵 Wk 和 Wq,使得 k1 = Wk * x1,q1 = Wq * x1,依此类推。现在,我们可以计算 k1 和 q3 的点积来得到一个标量,因此 u1 = F(x1, x3) = Wk * x1 · Wq * x3。
自注意力机制中还有一个额外的事情是,我们不会直接对嵌入向量本身进行加权求和。相反,我们对该嵌入向量的一些“值”进行加权求和,这些值是通过另一个小型的单层网络获得的。这意味着,类似于 k1 和 q1,我们现在也有了 x1 这个单词的 v1,并且通过矩阵 Wv 获得它,公式为 v1 = Wv * x1。然后,v1 会被聚合。所以如果我们只有 3 个单词并且试图预测第四个单词,它大致看起来是这样的:

自注意力机制。图像由作者提供
加号表示向量的简单相加,意味着它们必须具有相同的长度。这里没有展示的最后一个修改是,标量 u1、u2、u3 等不一定加起来等于 1。如果我们需要它们作为权重,我们应该使它们相加为 1。因此,我们在这里使用一个熟悉的技巧——应用 softmax 函数。
这是自注意力机制。还有交叉注意力机制,其中你可以让 q3 来自于最后一个单词,但 k 和 v 可以来自于完全不同的句子。例如,在翻译任务中,这非常有价值。现在我们知道什么是注意力机制了。
现在,可以将整个过程放入一个框中,称之为“自注意力模块”。基本上,这个自注意力模块接收嵌入向量,并输出一个用户选择长度的单一输出向量。这个模块有三个参数,Wk、Wq、Wv——它不需要比这更复杂。在机器学习文献中有很多这样的模块,它们通常在图表中以框的形式表示,并标有它们的名称。类似这样:

作者提供的图片
你会注意到,使用自注意力时,到目前为止,事物的位置似乎并不重要。我们在所有地方都使用相同的 W,因此交换“Humpty”和“Dumpty”并不会真正产生区别——所有数字最终都会是一样的。这意味着,虽然注意力机制可以决定关注什么,但这并不依赖于词语的位置。然而,我们知道,词语的位置在英语中是重要的,我们可能通过给模型一些词语位置的感知来提高性能。
因此,当使用注意力机制时,我们通常不会直接将嵌入向量输入到自注意力模块中。我们稍后将看到如何在将嵌入向量输入到注意力模块之前,添加“位置编码”。
预备知识提示:对于那些并非第一次阅读自注意力机制的人来说,可能会注意到我们并没有提到任何 K 和 Q 矩阵,或者应用掩码等。这是因为这些细节属于实现层面的内容,源于这些模型通常的训练方式。一批数据被输入,模型被同时训练以预测“dumpty”从“humpty”,从“humpty dumpty”预测“sat”,等等。这是为了提高效率,与解释或模型输出无关,我们选择在这里省略训练效率的技巧。
Softmax
我们在第一条笔记中简要提到过 softmax。这里是 softmax 试图解决的问题:在我们的输出解释中,我们有和我们希望网络选择的选项一样多的神经元。我们说我们将网络的选择解释为值最大的神经元。然后我们说我们将计算损失值,作为网络提供的值与我们希望的理想值之间的差异。但我们希望的理想值是什么呢?在叶子/花卉的例子中,我们将其设置为 0.8。但为什么是 0.8?为什么不是 5,或者 10,或者 1000 万?在这个训练示例中,值越大越好。理想情况下,我们希望它是无限大!但这将使问题变得无法解决——所有损失值都将是无限大,我们通过调整参数(记住“梯度下降”)来最小化损失的计划将失败。我们该如何处理呢?
我们可以做的一件简单的事情是限制我们想要的值。假设在 0 和 1 之间?这会让所有的损失是有限的,但现在我们面临一个问题,那就是当网络超出范围时会发生什么。假设它在某个情况下输出(5,1)作为(leaf,flower),而在另一个情况下输出(0,1)。第一个情况做出了正确的选择,但损失更大!好吧,那么现在我们需要一种方法,将最后一层的输出也转换到(0,1)的范围内,从而保持顺序。我们可以在这里使用任何函数(在数学中,“函数”指的是将一个数字映射到另一个数字——一个数字输入,另一个数字输出——这基于规则来决定给定输入会输出什么)来完成这项工作。一个可能的选择是逻辑函数(见下图),它将所有数字映射到(0,1)之间,并保持顺序:

图片由作者提供
现在,我们为最后一层的每个神经元都得到了一个 0 到 1 之间的数字,我们可以通过将正确的神经元设置为 1,其他神经元设置为 0,并计算它与网络提供的结果之间的差异来计算损失。这是可行的,但我们能做得更好吗?
回到我们“Humpty Dumpty”的例子,假设我们正在尝试逐字符生成“dumpty”,而模型在预测“dumpty”中的“m”时出错。它没有给出“m”作为最高值,而是给出了“u”作为最高值,但“m”是接近的第二选择。
现在我们可以继续“duu”,并尝试预测下一个字符,以此类推,但模型的置信度会较低,因为从“humpty duu..”开始并没有很多好的续写方式。另一方面,“m”是一个接近的第二选择,因此我们也可以尝试“m”,预测接下来的几个字符,看看会发生什么?也许它能给我们一个更好的整体单词?
所以我们在这里讨论的不仅仅是盲目选择最大值,而是尝试几种选择。什么是一个好的方法呢?嗯,我们需要为每个选择分配一个概率——比如说,选择第一个的概率为 50%,第二个的概率为 25%,以此类推。这是一个好的方法。但也许我们希望概率取决于底层模型的预测。如果模型预测的“m”和“u”值非常接近(与其他值相比)——那么也许探索这两者之间 50-50 的机会是一个好主意?
所以我们需要一个好的规则,将这些数字转化为概率。这就是 softmax 所做的。它是上面逻辑函数的推广,具有附加功能。如果你给它 10 个任意的数字——它会给你 10 个输出,每个输出都在 0 和 1 之间,而且重要的是,所有 10 个输出加起来等于 1,这样我们就可以将它们解释为概率。你会在几乎所有的语言模型中发现 softmax 作为最后一层。
残差连接
随着章节的推进,我们逐渐改变了对网络的可视化表示。现在我们使用框/块来表示某些概念。这种符号在表示残差连接这一特别有用的概念时非常有用。让我们看一下结合自注意力块的残差连接:

残差连接。图片来源:作者
请注意,我们将“输入”和“输出”表示为框,以简化理解,但这些基本上仍然只是与上面显示的神经元/数字的集合。
那么这里发生了什么呢?我们基本上是将自注意力块的输出,在传递到下一个块之前,加入原始输入。首先需要注意的是,这要求自注意力块的输出维度必须与输入的维度相同。由于自注意力的输出是由用户决定的,所以这不是问题。但是为什么要这样做呢?我们在这里不深入探讨所有细节,但关键是,随着网络加深(输入和输出之间的层数增多),训练它们变得越来越困难。残差连接已被证明有助于解决这些训练挑战。
层归一化
层归一化是一个相当简单的层,它接收进入该层的数据,通过减去均值并除以标准差来进行归一化(可能会有些许变化,正如下文所示)。例如,如果我们在输入后立即应用层归一化,它将取输入层中的所有神经元,然后计算两个统计量:它们的均值和标准差。假设均值为 M,标准差为 D,那么层归一化所做的就是将每个神经元替换为(x-M)/D,其中 x 表示任意给定神经元的原始值。
那么这有什么帮助呢?它基本上稳定了输入向量,并有助于训练深度网络。一个担心的问题是,通过归一化输入,我们是否去除了其中一些可能对学习我们目标有所帮助的有用信息?为了解决这个问题,层归一化层有一个缩放和偏置参数。基本上,对于每个神经元,您只需将其与一个标量相乘,然后加上一个偏置。这个标量和偏置值是可以训练的参数。这允许网络学习一些可能对预测有价值的变化。而且由于这些是唯一的参数,LayerNorm 块并没有很多需要训练的参数。整个过程看起来大致如下:

层归一化。图片来源:作者
缩放和偏差是可训练的参数。你可以看到,层归一化(layer norm)是一个相对简单的模块,其中每个数值仅在点对点操作下进行处理(在初步计算均值和标准差之后)。这让人联想到激活层(如 RELU),唯一的关键区别是这里我们有一些可训练的参数(尽管由于简单的点对点操作,它们远少于其他层的参数)。
标准差是一个统计量,用来衡量数据分布的离散程度。例如,如果所有的值都相同,你会说标准差为零。如果一般情况下,每个值都远离这些值的均值,那么标准差就会很高。计算标准差的公式是:从每个数值中减去这些数值的均值,然后将每个差值平方。将这些平方后的结果相加,再除以数据的数量 N。最后对结果取平方根。
对于已了解相关内容的读者:有经验的机器学习(ML)专业人士会注意到,这里没有讨论批归一化(batch norm)。事实上,本文中甚至没有引入批次(batches)这一概念。大多数情况下,我认为批次是另一种训练加速手段,与理解核心概念无关(除了批归一化,我们在这里不需要讨论)。
Dropout
Dropout 是一种简单而有效的方法,用于避免模型过拟合。过拟合是指当你在训练数据上训练模型时,它在该数据集上表现良好,但在模型未见过的样本上却无法很好地泛化。帮助我们避免过拟合的技术被称为“正则化技术”,而 Dropout 正是其中之一。
如果你训练一个模型,它可能会对数据产生误差,或者以某种特定方式对数据进行过拟合。如果你训练另一个模型,它可能也会出现类似情况,但表现方式不同。如果你训练多个这样的模型并将它们的输出进行平均呢?这些通常被称为“集成模型”,因为它们通过结合多个模型的输出预测结果,而集成模型通常比任何单一模型表现得更好。
在神经网络中,你也可以做类似的操作。你可以构建多个(略有不同的)模型,然后将它们的输出结合起来以获得更好的模型。然而,这可能会计算开销较大。Dropout 是一种技术,它并不完全构建集成模型,但确实捕捉到了一些集成方法的精髓。
这个概念很简单,通过在训练过程中插入一个 Dropout 层,你所做的就是随机删除 Dropout 层插入位置之间的神经元连接的某一百分比。考虑我们的初始网络,并在输入层和中间层之间插入一个 Dropout 层,设定 50%的丢弃率,效果可能如下所示:



图片来自作者
现在,这迫使网络在训练时有大量冗余。实质上,你是在同时训练多个不同的模型——但它们共享权重。
现在,为了进行推理,我们可以采用与集成模型相同的方法。我们可以使用 dropout 做出多个预测,然后将它们组合起来。然而,由于这在计算上比较耗时——并且由于我们的模型共享共同的权重——为什么不直接使用所有权重进行预测(即一次性使用所有的权重,而不是每次只使用 50%的权重)呢?这应该能给我们提供一个集成方法的近似结果。
其中一个问题是:使用 50%权重训练的模型在中间神经元中的数值与使用所有权重的模型差异很大。我们想要的是这里更像是集成式的平均方法。我们该怎么做呢?一种简单的方法是将所有权重乘以 0.5,因为我们现在使用了两倍数量的权重。这就是 Dropout 在推理过程中做的事情。它会使用包含所有权重的完整网络,并将权重乘以(1 - p),其中 p 是删除的概率。研究表明,这种方法作为正则化技术效果非常好。
多头注意力
这是变换器架构中的关键模块。我们已经看到过什么是注意力模块。记住,注意力模块的输出是由用户决定的,且它的长度是 v 的长度。多头注意力基本上是并行运行多个注意力头(它们都采用相同的输入)。然后我们将它们的所有输出连接在一起。它大概长这样:

多头注意力。图片来自作者
请记住,从 v1 -> v1h1 的箭头是线性层——每条箭头上都有一个矩阵来进行变换。我只是没有显示它们,以避免杂乱。
这里发生的事情是,我们为每个头生成相同的 key、query 和 value。但接着我们基本上是在这些值上应用一个线性变换(分别对每个 k、q、v 和每个头进行处理),然后使用这些 k、q、v 值。这一额外的层在自注意力中并不存在。
另一个旁注是,对我而言,这种创建多头注意力的方式有点出乎意料。例如,为什么不为每个头创建单独的 Wk、Wq、Wv 矩阵,而是通过增加一个新层并共享这些权重呢?如果你知道,请告诉我——我真的不清楚。
位置编码和嵌入
我们在自注意力部分简要讨论了使用位置编码的动机。这些是什么?虽然图片显示了位置编码,但使用位置嵌入比使用编码更常见。因此,我们在这里讨论了一种常见的位置嵌入,但附录中也涵盖了原始论文中使用的位置编码。位置嵌入与任何其他嵌入没有什么不同,只是不同于嵌入词汇,我们将嵌入数字 1、2、3 等。因此,这种嵌入是与词嵌入相同长度的矩阵,每列对应一个数字。就是这样简单。
GPT 架构
让我们谈谈 GPT 架构。这是大多数 GPT 模型中使用的架构(可能会有一些变化)。如果你迄今为止一直在关注本文,那么这应该很容易理解。使用方框表示法,这是高层次上架构的样子:

GPT 架构。作者提供的图片
到目前为止,除了“GPT 变压器块”外,所有其他块都已经详细讨论过了。这里的+号简单地表示两个向量相加(这意味着两个嵌入必须是相同大小)。让我们看看这个 GPT 变压器块:

就是这样。这里称为“变压器”,因为它源自并且是一种变压器类型——这是我们将在下一节中讨论的架构。这不会影响理解,因为我们已经在之前涵盖了这里显示的所有构建块。让我们回顾一下到目前为止我们所讨论的所有内容,以构建这个 GPT 架构:
-
我们看到神经网络如何将数字作为输入并输出其他数字,并具有可训练的参数权重
-
我们可以将这些输入/输出数字附加解释,并赋予神经网络现实世界的含义
-
我们可以串联神经网络以创建更大的网络,我们可以称每个网络为一个“块”,并用一个方框表示它,以便更容易绘制图表。每个块仍然做同样的事情,接收一堆数字并输出另一堆数字
-
我们学习了许多不同类型的块,用于不同的目的
-
GPT 只是这些块的特殊排列,如上所示,并且具有我们在第一部分讨论过的解释
随着公司构建强大的现代 LLM,对此进行了一些修改,但基本原理保持不变。
现在,这个 GPT 变压器实际上是原始变压器论文中所称的“解码器”。让我们来看看。
变压器架构
这是推动最近语言模型能力快速提升的关键创新之一。Transformer 不仅提高了预测准确性,而且比以前的模型更易于训练/更高效,使得更大的模型规模成为可能。这正是上面提到的 GPT 架构的基础。
如果你看看 GPT 架构,你会发现它非常适合生成序列中的下一个词。它基本上遵循我们在第一部分讨论的相同逻辑。从几个词开始,然后继续一次生成一个。但如果你想做翻译怎么办?假设你有一个德语句子(例如:“Wo wohnst du?” = “Where do you live?”),你想将其翻译成英语。我们如何训练模型来完成这个任务?
好的,首先我们需要做的是找出一种输入德语单词的方法。这意味着我们必须扩展我们的嵌入,包含德语和英语。现在,我想这里有一种简单的输入信息的方法。为什么不把德语句子连接到当前生成的任何英语句子的开头,然后将其传递给上下文呢?为了使模型更容易理解,我们可以添加一个分隔符。每一步的样子大致如下:

图片来自作者
这样做是可行的,但仍有改进空间:
-
如果上下文长度是固定的,有时候原始句子会丢失。
-
这个模型需要学习很多东西。它同时处理两种语言,还需要知道
是分隔符标记,模型需要从这里开始翻译。 -
你在处理整个德语句子时,每生成一个词都有不同的偏移量。这意味着相同的内容会有不同的内部表示,模型应该能够处理所有这些内容来进行翻译。
Transformer 最初是为这个任务创建的,由一个“编码器”和一个“解码器”组成——基本上是两个独立的模块。一个模块仅仅是接收德语句子并给出一个中间表示(再次说明,是一堆数字,基本上是这样的)——这个过程叫做编码器。
第二个模块生成词语(到目前为止我们已经看到过很多)。唯一的区别是,除了将已生成的词传递给它之外,我们还将编码后的德语句子(来自编码器模块)传递给它。因此,当它生成语言时,它的上下文基本上是到目前为止所有已生成的词,加上德语。这一模块叫做解码器。
这些编码器和解码器每个由几个模块组成,特别是夹在其他层之间的注意力模块。让我们来看一下论文《Attention is all you need》中的一个 Transformer 插图,并试着理解它:

图片来自 Vaswani 等人(2017)
左侧垂直排列的一组模块叫做“编码器”,右侧的模块叫做“解码器”。让我们一起来理解一下之前没有涉及到的部分:
图示解读: 这里的每一个方框都是一个块,它接收一些神经元形式的输入,并输出一组神经元,输出的神经元可以被下一个块处理或由我们进行解读。箭头显示了块的输出去向。如你所见,我们通常会将一个块的输出作为输入传递给多个块。让我们逐个解释这里的内容:
前馈:前馈网络是指不包含循环的网络。我们在第一部分中的原始网络就是一个前馈网络。实际上,这个块使用的结构非常相似。它包含两个线性层,每个线性层后面跟着一个 RELU(请参见第一部分中关于 RELU 的说明)和一个丢弃层。请记住,这个前馈网络适用于每个位置的独立处理。这意味着位置 0 上的信息有一个前馈网络,位置 1 也有一个,以此类推。但是,位置 x 的神经元并没有与位置 y 的前馈网络连接。这一点很重要,因为如果不这样做,网络在训练过程中就可能通过向前查看来作弊。
跨注意力: 你会注意到解码器有一个多头注意力机制,箭头从编码器指向解码器。那么这里发生了什么呢?还记得自注意力和多头注意力中的值、键、查询吗?它们都是来自同一个序列。查询实际上只是来自序列中的最后一个单词。那么,如果我们保留查询,但从完全不同的序列中提取值和键会怎样呢?这就是这里发生的事情。值和键来自编码器的输出。数学上没有变化,唯一的不同是键和值的输入来源发生了变化。
Nx: 这里的 Nx 表示该块会重复 N 次。因此,基本上你将块一个接一个地堆叠起来,并将上一个块的输入传递给下一个块。这是一种让神经网络更深的方法。现在,从图示来看,可能会对编码器的输出是如何传递给解码器产生混淆。假设 N=5。我们是否将每个编码器层的输出传递给对应的解码器层?不。基本上,你只需要运行一次完整的编码器。然后,你只需将该表示传递给每一个解码器层。
加法与规范化块: 这基本上和下面的内容相同(猜测作者只是想节省空间)

作者提供的图片
其他的内容已经讨论过了。现在你已经对变换器架构有了完整的理解,从简单的加法和乘法运算开始,完全自包含!你知道每一行、每个加法、每个框和每个词在从零开始构建时的含义。从理论上讲,这些笔记包含了你从头开始编写变换器所需的内容。事实上,如果你感兴趣的话,这个仓库已经实现了上述 GPT 架构。
附录
矩阵乘法
我们在上文中介绍了嵌入的背景下的向量和矩阵。矩阵有两个维度(行数和列数)。向量也可以看作是一个矩阵,其中一个维度等于一。两个矩阵的乘积定义为:

作者提供的图片
点表示乘法。现在我们再看看第一张图片中蓝色和有机神经元的计算。如果我们把权重写成矩阵,把输入写成向量,那么我们可以用以下方式表达整个操作:

作者提供的图片
如果权重矩阵叫做“W”,输入叫做“x”,那么 Wx 就是结果(在这种情况下是中间层)。我们也可以对它们进行转置,写成 xW——这只是一个个人偏好的问题。
标准差
我们在层归一化部分使用了标准差的概念。标准差是一个统计量,用来衡量一组数据的离散程度(即数据值的分布情况)。例如,如果所有数据值相同,那么标准差就是零。如果每个数据值都远离这些数据的均值,那么标准差会很高。计算一组数字(比如 N 个数字 a1、a2、a3……)的标准差的公式如下:首先从每个数字中减去这些数字的均值,然后对每个差值进行平方。将所有这些平方值加在一起,再除以 N。最后,对这个结果取平方根。
位置编码
我们在上面讨论了位置嵌入。位置编码只是一个与词嵌入向量长度相同的向量,只不过它不是嵌入,意思是它不是经过训练的。我们简单地为每个位置分配一个唯一的向量,例如,位置 1 和位置 2 的向量不同,依此类推。实现这种方法的一种简单方式是让该位置的向量全部由该位置的编号组成。因此,位置 1 的向量将是[1,1,1...1],位置 2 的向量将是[2,2,2...2],依此类推(请记住,每个向量的长度必须与嵌入长度匹配,以便进行加法操作)。这种方法有问题,因为我们可能会在向量中得到很大的数字,这会在训练过程中带来挑战。当然,我们可以通过将每个数字除以位置的最大值来对这些向量进行归一化处理。如果总共有 3 个词,那么位置 1 的向量将是[.33, .33, ... , .33],位置 2 的向量将是[.67, .67, ... , .67],依此类推。现在的问题是,我们不断地改变位置 1 的编码(这些数字会随着我们输入四个单词的句子而不同),这使得网络难以学习。所以在这里,我们需要一个方案,为每个位置分配一个唯一的向量,而且数值不会爆炸。基本上,如果上下文长度是 d(即,我们可以输入到网络中以预测下一个词/标记的最大令牌/词的数量,请参见“如何生成语言?”部分中的讨论),而嵌入向量的长度是 10(假设如此),那么我们需要一个有 10 行和 d 列的矩阵,其中所有列都是唯一的,且所有数值都位于 0 和 1 之间。考虑到在 0 和 1 之间有无限多个数,而矩阵的大小是有限的,因此可以用多种方式来实现。
“Attention is all you need”论文中使用的方法大致如下:
-
绘制 10 个正弦曲线,每个曲线为 si(p) = sin(p/10000(i/d))(即 10 的 k 次方到 i/d 次方)
-
填充编码矩阵,使得(i,p)位置的数值为 si(p),例如,对于位置 1,编码向量的第 5 个元素为 s5(1)=sin(1/10000(5/d))
为什么选择这种方法?通过改变 10 的幂次,你会改变正弦函数在 p 轴上的振幅。如果你有 10 个不同的正弦函数,每个函数的振幅不同,那么当 p 的值变化时,过很长时间才能得到重复的值(即所有 10 个值相同)。这有助于为我们提供唯一的值。实际上,论文中使用了正弦和余弦函数,编码形式是:si(p) = sin(p/10000(i/d)),当 i 为偶数时;si(p) = cos(p/10000(i/d)),当 i 为奇数时。
理解 LLM 中的 Long RoPE
本文将详细介绍 Long RoPE 方法,该方法用于扩展 LLM 中的上下文长度,而不会显著降低性能。
·发表于Towards Data Science ·8 分钟阅读·2024 年 5 月 15 日
--

图片由作者提供 — 由 Stable Diffusion 2.1 生成
随着公众开始在日常生活中使用 LLM,一个重要的问题浮现出来,那就是在进行长时间对话时,LLM 有时会完全忘记之前说过的内容!在后台,每一行对话都被输入到 LLM 的上下文中,你可以将其视为对模型的巨大输入。一旦对话超出了上下文的处理能力,就必须删除一些数据。
这不仅是糟糕的客户体验,还限制了 LLM 合理处理信息的能力。因此,相关工作一直在进行,以构建具有越来越大上下文的 LLM。
今天的论文《LongRoPE:将 LLM 上下文窗口扩展到超 2 百万个标记》就实现了这一目标。

来自论文的图 1
从上面的图表来看,我们可以看到困惑度(一个衡量损失的指标,反映 LLM 预测下一个标记的准确性)对于 LongRoPE 保持较低,但却出现了峰值…
理解低秩适应(LoRA)在微调大规模语言模型中的应用
本文将详细讲解 LoRA 如何用于微调大规模语言模型,遵循“LoRA:大规模语言模型的低秩适应”论文中提出的方法。
·发表于Towards Data Science ·阅读时长:6 分钟·2024 年 5 月 24 日
--

图片由作者提供 — 由 Stable Diffusion 2.1 生成
微调可能是大规模语言模型(LLMs)中最常被讨论的技术方面之一。大多数人都知道训练这些模型是昂贵的,并且需要大量的资金投入,因此看到通过在已有模型上进行微调,结合自己的数据创建出一个具有一定独特性的模型,是令人兴奋的。
有多种方法可以对模型进行微调,但目前最受欢迎的方式之一是 LoRA 方法(低秩适应,Low Rank Adaptation),它在“LoRA:大规模语言模型的低秩适应”论文中有详细讨论。
在深入探讨 LoRA 的工作原理之前,我们需要了解一些矩阵背景知识以及微调机器学习模型的一些基本概念。
矩阵背景术语
几乎所有的机器学习模型都会将其权重存储为矩阵。因此,理解一些线性代数的基本知识有助于直观理解...
理解变压器中的位置嵌入:从绝对到旋转
深入分析绝对位置、相对位置和旋转位置嵌入,并附有代码示例
·发表于 Towards Data Science ·阅读时间 17 分钟·2024 年 7 月 20 日
--

旋转位置嵌入 — 图片来自 [6]
变压器(transformers)的一个关键组件是位置嵌入。你可能会问:为什么?因为变压器中的自注意力机制是置换不变的;这意味着它计算每个输入标记从序列中其他标记接收到的注意力的量,但它并不考虑标记的顺序。事实上,注意力机制将序列视为标记的集合。因此,我们需要另一个组件,称为位置嵌入,用于表示标记的顺序,并影响标记的嵌入。但不同类型的位置嵌入是什么,它们是如何实现的呢?
在这篇文章中,我们将深入探讨三种主要的位置信息嵌入,并深入了解它们的实现方式。
以下是本文的目录:
1. 背景与概述
2. 绝对位置嵌入
-
2.1 学习方法
-
2.2 固定方法(正弦波)
-
2.3 代码示例:RoBERTa 实现
在 Python 中理解竞争条件

该图片由作者在 Canva 中创建
Python 的 GIL 无法保证线程安全
·发表于 Towards Data Science ·阅读时长:8 分钟·2024 年 5 月 6 日
--
无论你是一个经常使用线程的 Python 用户,还是一个从未使用过线程技术但未来想尝试的人,有一个概念是我们无法绕过的,那就是线程安全。它指的是许多由多线程引起的微妙错误。尽管我们通过在多个线程中并发运行任务获得了好处,但必须始终牢记线程安全,以避免这些错误。
在所有典型的多线程相关的错误中,竞争条件是一个非常微妙且令人头疼的错误。如果并发运行的线程不多,它可能不会轻易发生,这使得调试变得更加困难。然而,如果我们真正理解其背后的原因,它也是一个可以避免的错误。
在本文中,我将介绍什么是竞争条件以及它为什么会发生。在 Python 中,GIL 使其比大多数其他编程语言更具健壮性,但它仍然可能发生。在某些情况下,竞争条件在 Python 中可能不会发生,但我将找到一种方式来确保重现这个错误,并详细解释其机制。
1. 什么是竞争条件(Race Condition)?
了解解决 GenAI 挑战的技术
深入了解模型预训练、微调、RAG、提示工程等技术!
·发表于 Towards Data Science ·15 分钟阅读·2024 年 6 月 20 日
--

来源:作者及 GPT4o。图片设计旨在展示语言模型学习和发展其大脑的过程!
引言
生成性人工智能的应用正在快速增长,涵盖个人和企业。最近的一项Gartner 研究发现,生成性 AI 解决方案是组织中使用最广泛的 AI 解决方案,大多数公司都在利用内置于现有工具中的 GenAI 功能,如 Microsoft 365 Copilot。根据我的经验,大多数企业都在寻求某种形式的“私有 ChatGPT”,希望通过它从独特的组织数据中获得更多价值。公司的目标各不相同,从在特定文档中查找信息、基于表格数据生成报告和总结内容,到查找与某个领域相关的所有项目等等。
本文探讨了解决这些问题的各种方法,概述了每种方法的优缺点和应用场景。我的目标是提供指导,帮助你在何时考虑不同的方法,并如何将它们结合起来,以实现最佳效果,涵盖从最复杂、最昂贵的预训练方法到最简单、最具成本效益的提示工程技术。
文章的结构旨在从模型训练的基础概念(预训练、继续预训练和微调)开始,逐步过渡到更常见的技术(RAG 和提示工程),这些技术用于与现有模型进行交互。

作者提供的图片
背景
解决 GenAI 问题没有“一刀切”的方法。大多数使用场景需要结合多种技术才能取得成功的结果。通常,组织从像 GPT-4、Llama3 70b Instruct 或 DBRX Instruct 这样的模型开始,这些模型已经在万亿级别的标记上进行了预训练,用于执行下一个标记预测,然后为特定任务(如指令或对话)进行微调。基于指令的模型经过训练和优化,能够遵循提示中给出的具体指令,而基于对话的模型则经过训练和优化,能够处理多轮对话格式,保持对话的上下文和一致性。
使用现有模型使组织能够利用像 OpenAI、Meta 和 Databricks 等公司在数据集策划、创新架构创建以及模型训练和评估上所做的大量时间和资金投资。
尽管并不是每个公司都需要预训练或进行指令微调他们的模型,但任何使用大型语言模型(LLM)的人都受益于这些行业领袖所打下的基础。这一基础使得其他公司能够应对独特的挑战,而无需从零开始。
在接下来的章节中,我们将探讨预训练、微调(包括指令微调和继续预训练)、检索增强生成(RAG)、RAG 的嵌入微调以及提示工程,讨论如何以及何时使用或考虑每种方法。
通过预训练设定基线
概述:预训练模型创建了一个基础,作为所有下游任务的基础。这个过程包括定义模型的架构、策划一个庞大的数据集(通常是万亿级的标记)、训练模型并评估其表现。在 LLM 和 SLM 的上下文中,预训练阶段用于将知识注入模型,使其能够预测序列中的下一个单词或标记。例如,在句子“the cat sat on the ___”中,模型学习预测“mat”。
像 OpenAI 这样的公司已经在其 GPT 模型的预训练阶段投入了大量资金,但由于像 GPT-3.5、GPT-4 和 GPT-4o 这样的模型是闭源的,因此无法使用底层架构,并在不同的数据集和参数上预训练该模型。然而,借助像 Mosaic AI 的预训练 API 这样的资源,能够对像 DBRX 这样的开源模型进行预训练。
优点:
-
完全控制:预训练模型的好处在于,你可以完全控制整个模型创建过程。你可以根据需求定制架构、数据集和训练参数,并通过代表你所在领域的评估进行测试,而不是主要集中在常见的基准测试上。
-
固有的领域特定知识:通过策划专注于特定领域的数据集,模型可以比通用模型更深入地理解该领域。
缺点:
-
最昂贵的选项:预训练需要极大量的计算能力(许多 GPU),这意味着预训练的成本通常在数百万到数千万甚至上亿美元,并且通常需要数周才能完成训练。
-
知识截止时间:最终模型也是在某一时间点完成的,因此它不会固有地理解实时信息,除非通过像 RAG 或函数调用这样的技术进行增强。
-
高级要求:这种方法需要最多的数据和最先进的专业知识才能取得有意义的结果。
应用:一般来说,只有当其他方法不足以满足你的用例时,预训练你自己的模型才是必要的。例如,如果你想训练一个模型来理解一种它以前没有接触过的新语言,你可以考虑先进行预训练,然后再根据你的需求进行微调。
一旦基础训练完成,模型通常需要进行微调,以便能够有效执行任务。当你看到一个标记为聊天模型或指令模型时,这意味着基础模型已经针对这些目的进行了微调。如今你与之互动的几乎所有模型都已经针对这些目的进行了微调,以便最终用户能够高效地与模型互动。
由于预训练模型所需的巨大成本和复杂过程,大多数组织决定在其 GenAI 用例中利用现有模型。要开始进行预训练,请查看 Mosaic AI 的预训练 API,它允许你使用不同参数大小的 Databricks DBRX 模型进行预训练。

作者提供的图片。LLM 和 SLM 预训练概览。
使用持续预训练(CPT)添加知识
概述:CPT 是一种微调方法,它允许扩展现有模型的知识,而不是从零开始训练整个模型。经过 CPT 的模型输出仍然会预测下一个标记。通常建议先使用 CPT,再进行指令微调(IFT),这样你可以先扩展模型的知识,然后将其调优为特定任务,如执行指令或聊天。如果顺序反过来,模型可能会忘记在 IFT 阶段学到的指令。
优点:
-
无需标签训练数据:CPT 不需要标签训练数据。如果你有大量领域特定或新的信息想要教给模型,这是非常好的选择。由于输出仍然集中在下一个标记的预测上,CPT 的输出对你想要一个文本补全模型时非常有帮助。
-
比预训练更快且更具成本效益:CPT 可以在数小时或几天内完成,使用的 GPU 比预训练少,因此更快且更便宜!
缺点:
-
仍然相对昂贵:虽然 CPT 比预训练便宜得多,但仍然可能很昂贵,根据数据量和所需 GPU 数量的不同,训练一个模型可能需要花费数万美元。
-
需要精心设计的评估:此外,你需要创建自己的评估方法,以确保模型在你所教的新领域中表现良好。
-
通常需要后续的 IFT:对于大多数使用案例,在 CPT 完成后,你仍然需要对模型进行 IFT,以便最终模型能够正确地回答问题或进行对话。这最终会增加时间和成本,直到你得到一个可以使用的模型。
应用场景:对于那些具有高度领域特定内容的行业,如医疗或法律,CPT 可能是向模型介绍新话题的一个不错选择。使用像Mosaic AI 的微调 API这样的工具,你可以轻松开始 CPT,只需要一系列用于训练的文本文件。对于 CPT 过程,所有文本文件将被连接在一起,并在每个文档之间加上分隔符,Mosaic 会在幕后处理如何将这些文件输入模型进行训练的复杂性。
举个例子,假设我们使用 CPT 处理一系列关于负责任的人工智能和 AI 政策的文本文件。如果我提示模型“告诉我负责任的人工智能中三个重要的原则”,我很可能会得到一个与我提示内容高度相关的回答,比如“我需要理解负责任人工智能的关键原则,以便我能训练一个有效的模型”。尽管这个回答与我的提示有关,但它并没有直接回答问题。这表明需要通过 IFT 来改进模型的指令跟随能力。

图像由作者提供,灵感来源于大规模语言模型的持续学习:一项综述
通过指令微调(IFT)定制回答
概述:IFT 用于教导模型如何执行特定任务。通常需要成千上万的示例,并且可以用于特定目的,如改善问答、提取关键信息或采用某种语气。
优点:
-
速度和成本效益:IFT 完成的时间显著更短,这种类型的训练可以在几分钟内实现,使其不仅更快,而且相比预训练或 CPT 更便宜。
-
任务特定定制:这是一种很好的方法,可以通过指导模型以特定的语气响应、分类文档、修订某些文档等方式,获得定制化的结果。
缺点:
-
需要标注数据集: IFT 需要标注数据来教模型如何应对特定任务。虽然有许多开源数据集可供使用,但根据你特定的应用场景,创建和标注一个合适的数据集可能需要一些时间。
-
潜在的通用能力下降: 通过 IFT 引入新技能可能会降低模型在通用任务上的表现。如果你担心保持模型的泛化能力,可以在训练和评估集中特意包括通用技能的示例,这样你可以在测试新技能的同时,衡量模型在通用任务上的表现。
应用: IFT 帮助模型更好地执行特定任务,如问答。例如,使用提示语“告诉我三个对负责任的人工智能很重要的原则”,经过 IFT 训练的模型可能会回应类似这样的回答:“负责任的人工智能对于确保模型的道德使用至关重要,基于透明性、公平性和隐私等核心原则进行构建。遵循负责任的 AI 原则有助于将解决方案与更广泛的社会价值和道德标准对齐。”与仅由 CPT 或 PT 模型生成的回答相比,这种回应对最终用户更具实用性,因为它直接解答了问题。
请注意,存在多种微调方法和技术,旨在提高整体模型性能并减少训练过程中的时间和成本。

图片来源:作者。灵感来源于《大规模语言模型的持续学习:一项综述》
使用检索增强生成(RAG)技术查找实时或私密信息
概述: RAG 使语言模型能够使用训练数据之外的信息回答问题。在 RAG 过程中,用户查询触发从向量索引中检索相关信息,随后将这些信息与原始查询一起集成到新的提示中,以生成响应。由于其有效性和简便性,这一技术是当前最常用的技术之一。
优点:
-
访问实时信息和训练数据之外的信息: RAG 允许模型利用来自不同且不断更新的来源(如互联网或内部文档数据库)的查询信息。任何可以存储在向量索引中或通过插件/工具检索的信息,都可以在 RAG 过程中使用。
-
实现简便: RAG 不需要自定义训练,这使其既具有成本效益,又易于上手。它也是一个非常有文献支持和研究的领域,许多文章提供了关于如何改善 RAG 系统响应的见解。
-
可追溯性和引用:所有生成的响应可以包含用于回答查询的文档引用,这使得用户能够轻松验证信息并了解回答是如何生成的。由于你清楚知道哪些信息被发送给模型以回答问题,因此很容易向最终用户提供可追溯的答案,如果需要,最终用户还可以查看引用的文档以获取更多信息。相比之下,如果你直接查询模型,很难知道它是如何回答问题的,或者使用了哪些参考资料来生成回答。
缺点:
-
上下文窗口的限制:第一个主要问题是不同模型的上下文窗口大小,例如,GPT-4 和 4o 有 128k 令牌的上下文窗口,而 Llama-3 系列仍然只有 8k 令牌。上下文窗口较小意味着你无法向模型传递足够的信息来回答问题。因此,拥有强大的文本分块和分块重排名技术变得尤为重要,这样你可以检索到正确的上下文,并利用这些上下文正确地回应用户。
-
“中间丢失问题”:即使使用更长的上下文窗口,仍然存在一个常见的“中间丢失问题”,即模型往往更关注提示中的开头或结尾信息,这意味着如果问题的答案位于上下文的中间,即使提供了回答问题所需的所有信息,模型仍然可能会回答错误。类似地,模型可能会混淆它们检索到的信息,导致只部分正确地回答问题。例如,我曾见过在要求模型查找关于两家公司的信息并返回它们对人工智能的看法时,模型偶尔会混淆两家公司的政策。
-
Top K 检索挑战:在典型的 RAG(检索增强生成)管道中,只有与查询相关的前 K 个文档(或文本块)会被检索并传递给模型以生成最终响应。当需要在文档库中查找特定细节时,这种模式能够取得更好的效果,但在回答需要广泛搜索的问题时往往无法正确回答。例如,提示“给我所有关于负责任的人工智能的文档”需要额外的逻辑来继续在向量索引中搜索所有相关的负责任 AI 文档,而不是在返回前 K 个相关块后就停止。
-
过于相似的文档:如果向量索引包含的文档在语义上都非常相似,模型可能很难检索到与任务最相关的具体文档。特别是在专业领域或语言统一的领域中尤为如此。在文档内容多样化的向量索引中可能不会出现这个问题,但如果你在一个包含医学文档的索引中使用 RAG,而所有语言都非常相似,且不是典型嵌入模型所训练的内容,那么可能会更难找到你需要的文档/答案。
应用场景:任何涉及在一组文档上进行问答的使用案例通常都会涉及 RAG。这是一种非常实用的方式,可以让你快速开始使用生成性人工智能,并且不需要额外的模型训练。新兴的 AI 代理概念也通常会至少有一个用于 RAG 的工具。许多代理实现将为不同的数据源提供基于 RAG 的工具。例如,一个内部支持代理可能能够访问 HR 工具和 IT 支持工具。在这种设置中,HR 和 IT 文档都可能有一个 RAG 组件,每个工具背后可能运行相同的管道,唯一的区别是文档数据集。

作者提供的图像。RAG 过程概览。
通过微调嵌入来改进 RAG 中的 R
概述:微调嵌入可以改善 RAG 的检索组件。微调嵌入的目标是使向量嵌入在向量空间中进一步分开,使它们彼此之间更加不同,从而更容易找到与问题最相关的文档。
优点:
-
通常具有成本效益:与其他训练方法相比,微调嵌入的成本相对较低。
-
领域特定的定制化:这种方法对于区分那些嵌入模型在训练时没有充分接触过的领域的文本非常有效。例如,针对高度专业化的法律或医疗保健文档,可以在 RAG 管道中为这些语料库微调嵌入。
缺点:
-
需要标注数据并且通常需要重新训练:微调嵌入模型需要一个标注的数据集。此外,在你向索引中添加更多信息时,可能需要持续重新训练嵌入模型。
-
跨索引的额外维护:根据你查询的数据源数量,你可能还需要跟踪多个嵌入模型及其相关数据源。需要记住的是,无论使用哪个嵌入模型来嵌入文档语料库,检索时用于嵌入查询的模型必须是相同的。如果你查询的是多个索引,并且每个索引使用不同的嵌入模型,那么你需要确保在检索时你的模型是一致的。
应用:如果传统的 RAG 方法效果不佳,因为索引中的文档彼此过于相似,微调嵌入是一个很好的选择。通过微调嵌入,你可以教会模型更好地区分领域特定的概念,并改进你的 RAG 结果。
与模型进行提示工程对话
概述:提示工程是与生成型 AI 模型互动的最常见方式,它只是向模型发送一条旨在获得你想要输出的信息的消息。它可以像“讲一个关于德国牧羊犬的故事”那么简单,或者它也可以非常复杂,涉及你希望模型执行的具体细节。
优点:
-
即时结果:使用不同的提示进行实验,可以在任何时候访问语言模型时进行,并且结果在几秒钟内(甚至更少)就能返回!一旦灵感来了,你可以开始优化提示,直到模型给出期望的回应。
-
在通用任务中的高性能:仅靠提示工程对于不需要获取特定业务信息或实时信息的通用任务非常有效。
-
与其他技术的兼容性:它适用于已经预训练、持续预训练或微调的模型,并且可以与 RAG 结合使用,使其成为最常用和最具多样性的方式。
缺点:
-
单独能力有限:仅仅依赖提示工程通常不足以让模型按你希望的方式表现。大多数情况下,人们希望模型与某些外部数据交互,无论是文档数据库、API 调用还是 SQL 表,这些都需要将提示工程与 RAG 或其他专门的工具调用结合使用。
-
精度挑战:编写完美的提示可能具有挑战性,通常需要大量的调整,直到模型按预期表现为止。一个在某个模型上效果很好提示可能在另一个模型上失败,要求在多个模型和提示变体之间进行大量的迭代和实验。
应用:提示工程将与上述所有技术结合使用,以产生预期的回应。有许多不同的提示工程技术可以帮助将模型引导到正确的方向。如需更多关于这些技术的信息,我推荐这篇微软的提示工程指南,他们提供了从链式思维提示到更复杂技巧的多种示例。

作者提供的图片。提示工程流程概览。
结论
生成式人工智能技术每天都在变化和进步。大多数应用程序将需要利用本文中描述的各种技术。开始使用已经针对指令或对话能力进行微调的现有语言模型,并重点关注提示工程和 RAG,是一个很好的起点!从这里开始,寻找更多需要微调/指令微调的定制用例,可以带来更大的好处。
展望未来,AI 代理提供了一种有前景的方式,可以利用最新的进展,包括在大量公共数据上预训练并针对对话/指令跟随进行了微调的闭源和开源模型。只要拥有合适的工具,这些代理可以代你执行令人惊叹的任务,从使用 RAG 进行信息检索到帮助规划公司活动或假期。
此外,随着拥有大量专业数据的组织开始预训练自己的模型,我们可以预期会看到更多领域特定模型的出现。例如,像Harvey这样的公司已经在开发量身定制的人工智能解决方案,能够处理法律行业的独特需求。这一趋势可能会持续下去,导致高度专业化的模型在各个领域提供更精确和相关的结果。
通过结合不同 AI 技术的优势,利用 AI 代理和领域特定模型的力量,组织可以释放生成式 AI 的全部潜力。
其他参考文献
有兴趣进一步讨论或合作吗?请通过 LinkedIn 与我联系!
理解张量:通过 3 个令人头疼的错误学习数据结构
通过解决 TensorFlow 错误,我学到的关于张量的知识
·发表于 Towards Data Science ·11 分钟阅读·2024 年 3 月 13 日
--

图片来源:Michael Dziedzic 在 Unsplash
最近我一直在 TensorFlow 中尝试深度学习模型,因此也开始学习如何将数据管理为张量。
作为一名数据工程师,整天工作在可以轻松切片、切块并可视化的表格中,我对张量的操作几乎没有任何直觉,并且我似乎总是遇到相同的错误,尤其是在开始时,这些错误让我一头雾水。
然而,深入研究这些错误让我学到了很多关于张量和 TensorFlow 的知识,我想在这里整理这些学习成果,以便作为参考。
如果你有喜欢的错误、解决方案或调试技巧,请留下评论!
调试的代码示例
在我们深入探讨这些错误之前,我想记录一些我在调试过程中发现的轻量级、简单的代码片段,它们对我很有帮助。(虽然必须声明的是,出于法律原因,我们当然总是使用官方的调试工具来调试,而不是仅仅依赖于打印语句 🙂)
查看我们 TensorFlow 数据集的内部
首先,看看我们的实际数据。当我们打印 DataFrame 或在 SQL 中执行 SELECT * 时,我们能看到数据!但当我们打印张量数据集时,我们看到的是……
<_TensorSliceDataset element_spec=(TensorSpec(shape=(2, 3), dtype=tf.int32, name=None), TensorSpec(shape=(1, 1), dtype=tf.int32, name=None))>
这些都是非常有用的信息,但它们并没有帮助我们理解数据中到底发生了什么。
要打印执行图中的单个张量,我们可以利用 tf.print。如果你打算经常使用它,我强烈推荐这篇深入探讨 tf.print 的文章:在 TensorFlow 中使用 tf.Print()。
但在开发过程中使用 TensorFlow 数据集时,有时我们需要一次查看几个值。为此,我们可以像这样循环并打印单个数据片段:
# Generate dummy 2D data
np.random.seed(42)
num_samples = 100
num_features = 5
X_data = np.random.rand(num_samples, num_features).astype(np.float32)
y_data = 2 * X_data[:, 0] + 3 * X_data[:, 1] - 1.5 * X_data[:, 2] + 0.5 * X_data[:, 3] + np.random.randn(num_samples)
# Turn it into a Tensorflow Dataset
dataset = tf.data.Dataset.from_tensor_slices((X_data, y_data))
# Print the first 10 rows
for i, (features, label) in enumerate(dataset.take(10)):
print(f"Row {i + 1}: Features - {features.numpy()}, Label - {label.numpy()}")
我们也可以使用 skip 跳到特定的索引:
mini_dataset = dataset.skip(100).take(20)
for i, (features, label) in enumerate(mini_dataset):
print(f"Row {i + 1}: Features - {features.numpy()}, Label - {label.numpy()}")
了解我们的张量规格
在处理张量时,我们还需要知道它们的形状、秩、维度和数据类型(如果这些词汇对你来说有些陌生,就像最初对我来说一样,别担心,稍后我们会回到这些内容)。无论如何,下面是一些代码行,用于收集这些信息:
# Create a sample tensor
sample_tensor = tf.constant([[1, 2, 3], [4, 5, 6]])
# Get the size of the tensor (total number of elements)
tensor_size = tf.size(sample_tensor).numpy()
# Get the rank of the tensor
tensor_rank = tf.rank(sample_tensor).numpy()
# Get the shape of the tensor
tensor_shape = sample_tensor.shape
# Get the dimensions of the tensor
tensor_dimensions = sample_tensor.shape.as_list()
# Print the results
print("Tensor Size:", tensor_size)
print("Tensor Rank:", tensor_rank)
print("Tensor Shape:", tensor_shape)
print("Tensor Dimensions:", tensor_dimensions)
上面的输出:
Tensor Size: 6
Tensor Rank: 2
Tensor Shape: (2, 3)
Tensor Dimensions: [2, 3]
增强 model.summary()
最后,能够看到数据如何在模型中流动,以及在各层之间输入输出的形状如何变化,总是很有帮助。许多错误的根源通常是输入和输出形状之间的不匹配,以及给定张量的形状。
model.summary() 当然可以完成这项工作,但我们可以通过以下代码片段补充一些信息,这为模型和层的输入输出提供了更多上下文:
print("###################Input Shape and Datatype#####################")
[print(i.shape, i.dtype) for i in model.inputs]
print("###################Output Shape and Datatype#####################")
[print(o.shape, o.dtype) for o in model.outputs]
print("###################Layer Input Shape and Datatype#####################")
[print(l.name, l.input, l.dtype) for l in model.layers]
从我们的错误中学习
那么,让我们来看看一些错误吧!
秩
ValueError: 形状必须是秩 x,但却是秩 y……
好的,首先,什么是秩?秩就是我们用来描述张量的维度单位。秩为 0 的张量是一个标量值;秩为 1 的张量是一个向量;秩为 2 的张量是一个矩阵,依此类推,对于所有 n 维结构。
以一个五维张量为例。
rank_5_tensor = tf.constant([[[[[1, 2], [3, 4]], [[5, 6], [7, 8]]], [[[9, 10], [11, 12]], [[13, 14], [15, 16]]]],
[[[[17, 18], [19, 20]], [[21, 22], [23, 24]]], [[[25, 26], [27, 28]], [[29, 30], [31, 32]]]]])
print("\nRank 5 Tensor:", rank_5_tensor.shape)
Rank 5 Tensor: (2, 2, 2, 2, 2)
上面的代码显示,每个维度的大小为 2。如果我们想索引它,可以沿着这些轴中的任何一个进行。为了获取最后一个元素 32,我们可以运行类似以下的代码:
rank_5_tensor.numpy()[1][1][1][1][1]
官方张量文档中有一些非常有用的可视化,能够帮助你更容易地理解这一点。
回到错误:它只是标记了提供的张量与某个特定函数预期的维度不同。例如,如果错误声明“形状必须是秩 1,但却是秩 0…”,这意味着我们提供的是一个标量值,而它期望的是一个一维张量。
下面是一个示例,我们试图使用 matmul 方法将张量相乘。
import tensorflow as tf
import numpy as np
# Create a TensorFlow dataset with random matrices
num_samples = 5
matrix_size = 3
dataset = tf.data.Dataset.from_tensor_slices(np.random.rand(num_samples, matrix_size, matrix_size))
mul = [1,2,3,4,5,6]
# Define a function that uses tf.matmul
def matmul_function(matrix):
return tf.matmul(matrix, mul)
# Apply the matmul_function to the dataset using map
result_dataset = dataset.map(matmul_function)
如果我们查看文档,matmul 函数至少需要一个秩为 2 的张量,因此将矩阵与[1,2,3,4,5,6](这只是一个数组)相乘会引发此错误。
ValueError: Shape must be rank 2 but is rank 1 for '{{node MatMul}} = MatMulT=DT_DOUBLE, transpose_a=false, transpose_b=false' with input shapes: [3,3], [2].
解决这个错误的一个好方法是深入文档,理解你正在使用的函数期望什么(这里有一个关于张量的函数列表:raw_ops)。
然后使用秩方法来确定我们实际上提供了什么。
print(tf.rank(mul))
tf.Tensor(1, shape=(), dtype=int32)
至于解决方法,tf.reshape 通常是一个不错的起点。让我们稍微谈一下 tf.reshape,因为它将是我们整个 Tensorflow 旅程中的忠实伙伴:tf.reshape(tensor, shape, name=None)
reshape 简单地接受我们想要重塑的张量,和另一个包含我们希望输出形状的张量。例如,让我们重塑我们的乘法输入:
mul = [1,2,3,4,5,6]
tf.reshape(mul, [3, 2]).numpy()
array([[1, 2],
[3, 4],
[5, 6]], dtype=int32)
我们的变量将变成一个(3,2)的张量(3 行,2 列)。快速提示,tf.reshape(t, [3, -1]).numpy()也会产生相同的结果,因为-1 告诉 Tensorflow 计算该维度的大小,以确保总大小保持不变。形状张量中的元素数量即为秩。
一旦我们创建了具有正确秩的张量,我们的乘法就能顺利进行!
形状
ValueError:层的输入与层不兼容……
对张量形状的直观理解,以及它如何在模型各层之间相互作用和变化,已经使得深度学习的生活变得更加轻松。
首先,先了解基本的词汇:张量的形状指的是沿张量每个维度或轴的元素数量。例如,一个 3 行 4 列的二维张量,其形状为(3, 4)。
那么,形状可能会出什么问题呢?很高兴你问了,问题可多了!
首先,训练数据的形状和秩必须与输入层期望的输入形状匹配。让我们看一个例子,一个基本的 CNN:
import tensorflow as tf
from tensorflow.keras import layers, models
# Create a function to generate sample data
def generate_sample_data(num_samples=100):
for _ in range(num_samples):
features = tf.random.normal(shape=(64, 64, 3))
labels = tf.one_hot(tf.random.uniform(shape=(), maxval=10, dtype=tf.int32), depth=10)
yield features, labels
# Create a TensorFlow dataset using the generator function
sample_dataset = tf.data.Dataset.from_generator(generate_sample_data, output_signature=(tf.TensorSpec(shape=(64, 64, 3), dtype=tf.float32), tf.TensorSpec(shape=(10,), dtype=tf.float32)))
# Create a CNN model with an input layer expecting (128, 128, 3)
model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(128, 128, 3)))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.Flatten())
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(10, activation='softmax'))
# Compile the model
model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
# Fit the model using the dataset
model.fit(sample_dataset.batch(32).repeat(), epochs=5, steps_per_epoch=100, validation_steps=20)
尝试运行上述代码将会导致:
ValueError: Input 0 of layer "sequential_5" is incompatible with the layer: expected shape=(None, 128, 128, 3), found shape=(None, 64, 64, 3)
这是因为我们的模型期望输入张量的形状为(128, 128, 3),而我们生成的数据是(64, 64, 3)。
在这种情况下,我们的好朋友 reshape,或者 Tensorflow 的另一个函数 resize,可以帮助我们。如果像上面那样我们在处理图像,我们可以简单地运行 resize,或者改变模型输入的期望:
def resize_image(image, label):
resized_image = tf.image.resize(image, size=target_shape)
return resized_image, label
# Apply the resize function to the entire dataset
resized_dataset = sample_dataset.map(resize_image)
在这种情况下,了解常见类型的模型和模型层如何期望不同形状的输入是有帮助的,所以让我们稍微绕个弯。
深度神经网络的全连接层接受的是一维张量(或者是二维的,取决于是否包括批次大小,不过我们稍后会讨论批次大小),格式为(feature_size, ),其中 feature_size 是每个样本的特征数量。
卷积神经网络接受表示图像的数据,使用三维张量(宽度,高度,通道),其中通道是颜色方案,1 表示灰度,3 表示 RGB。
最后,递归神经网络(如 LTSM)接受的是二维张量(时间步,特征大小)。
但回到错误!Tensorflow 形状错误的另一个常见原因与数据通过模型层时形状的变化有关。如前所述,不同的层接受不同形状的输入,并且它们也可以重新调整输出的形状。
返回到我们上面的 CNN 示例,让我们再次破坏它,看看当我们移除 Flatten 层时会发生什么。如果我们尝试运行代码,我们将看到
ValueError: Shapes (None, 10) and (None, 28, 28, 10) are incompatible
这就是打印我们所有模型输入和输出形状以及数据形状变得非常有用的地方,帮助我们定位不匹配的地方。
model.summary()将展示给我们
Layer (type) Output Shape Param #
=================================================================
conv2d_15 (Conv2D) (None, 126, 126, 32) 896
max_pooling2d_10 (MaxPooli (None, 63, 63, 32) 0
ng2D)
conv2d_16 (Conv2D) (None, 61, 61, 64) 18496
max_pooling2d_11 (MaxPooling2D) (None, 30, 30, 64) 0
conv2d_17 (Conv2D) (None, 28, 28, 64) 36928
flatten_5 (Flatten) (None, 50176) 0
dense_13 (Dense) (None, 64) 3211328
dense_14 (Dense) (None, 10) 650
=================================================================
Total params: 3268298 (12.47 MB)
Trainable params: 3268298 (12.47 MB)
Non-trainable params: 0 (0.00 Byte)
我们进一步的诊断将揭示
###################Input Shape and Datatype#####################
(None, 128, 128, 3) <dtype: 'float32'>
###################Output Shape and Datatype#####################
(None, 10) <dtype: 'float32'>
###################Layer Input Shape and Datatype#####################
conv2d_15 KerasTensor(type_spec=TensorSpec(shape=(None, 128, 128, 3), dtype=tf.float32, name='conv2d_15_input'), name='conv2d_15_input', description="created by layer 'conv2d_15_input'") float32
max_pooling2d_10 KerasTensor(type_spec=TensorSpec(shape=(None, 126, 126, 32), dtype=tf.float32, name=None), name='conv2d_15/Relu:0', description="created by layer 'conv2d_15'") float32
conv2d_16 KerasTensor(type_spec=TensorSpec(shape=(None, 63, 63, 32), dtype=tf.float32, name=None), name='max_pooling2d_10/MaxPool:0', description="created by layer 'max_pooling2d_10'") float32
max_pooling2d_11 KerasTensor(type_spec=TensorSpec(shape=(None, 61, 61, 64), dtype=tf.float32, name=None), name='conv2d_16/Relu:0', description="created by layer 'conv2d_16'") float32
conv2d_17 KerasTensor(type_spec=TensorSpec(shape=(None, 30, 30, 64), dtype=tf.float32, name=None), name='max_pooling2d_11/MaxPool:0', description="created by layer 'max_pooling2d_11'") float32
flatten_5 KerasTensor(type_spec=TensorSpec(shape=(None, 28, 28, 64), dtype=tf.float32, name=None), name='conv2d_17/Relu:0', description="created by layer 'conv2d_17'") float32
dense_13 KerasTensor(type_spec=TensorSpec(shape=(None, 50176), dtype=tf.float32, name=None), name='flatten_5/Reshape:0', description="created by layer 'flatten_5'") float32
dense_14 KerasTensor(type_spec=TensorSpec(shape=(None, 64), dtype=tf.float32, name=None), name='dense_13/Relu:0', description="created by layer 'dense_13'") float32
输出信息很多,但我们可以看到 dense_13 层正在寻找形状为(None, 50176)的输入。然而,conv2d_17 层输出的是(None, 28, 28, 64)。
Flatten 层将前面层的多维输出转换为 Dense 层期望的一维(平坦)向量。
Conv2d 和 Max Pooling 层也以其他有趣的方式改变它们的输入数据,但这些超出了本文的范围。要了解更多详细信息,可以查看:神经网络中输入形状和模型复杂度的终极指南
那批量大小呢?!我没有忘记!
如果我们再一次通过从 model.fit 中的数据集移除.batch(32)来破坏我们的代码,我们将得到如下错误:
ValueError: Input 0 of layer "sequential_10" is incompatible with the layer: expected shape=(None, 128, 128, 3), found shape=(128, 128, 3)
这是因为,层输入的第一个维度是为批量大小或我们希望模型一次处理的样本数保留的。要深入了解,可以阅读批量和轮次之间的区别。
批量大小在拟合之前默认为 None,正如我们在模型摘要输出中看到的那样,我们的模型期望我们在其他地方设置它,具体取决于我们如何调整超参数。我们也可以通过使用 batch_input_size 而不是 input_size 在输入层强制设置它,但这样会降低我们在测试不同值时的灵活性。
类型
TypeError: 无法将类型的对象转换为张量。不支持的对象类型。
最后,让我们简单谈谈 Tensor 中一些数据类型的具体细节。
上述错误是另一个,如果你习惯于在使用各种数据构建表格的数据库系统中工作,可能会让人有些困惑,但它是比较容易诊断和修复的,尽管有几个常见原因需要注意。
主要问题是,尽管张量支持多种数据类型,但当我们将 NumPy 数组转换为张量时(这是深度学习中的常见流程),数据类型必须是浮动类型。下面的脚本初始化了一个包含 None 和字符串数据点的虚拟数据框。让我们一起分析这个示例中的问题并修复它:
import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow.keras.layers import Dense
from tensorflow.keras.models import Sequential
data = [
[None, 0.2, '0.3'],
[0.1, None, '0.3'],
[0.1, 0.2, '0.3'],
]
X_train = pd.DataFrame(data=data, columns=["x1", "x2", "x3"])
y_train = pd.DataFrame(data=[1, 0, 1], columns=["y"])
# Create a TensorFlow dataset
train_dataset = tf.data.Dataset.from_tensor_slices((X_train.to_numpy(), y_train.to_numpy()))
# Define the model
model = Sequential()
model.add(Dense(1, input_dim=X_train.shape[1], activation='sigmoid'))
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
# Fit the model using the TensorFlow dataset
model.fit(train_dataset.batch(3), epochs=3)
运行这段代码会提醒我们:
ValueError: Failed to convert a NumPy array to a Tensor (Unsupported object type float).
最明显的问题是你传入的是一个包含某些非浮动类型(即对象)的 NumPy 数组。如果你有一个实际的类别数据列,有很多方法可以将其转换为数值数据(如独热编码等),但这超出了本讨论的范围。
我们可以通过运行print(X_train.dtypes)来确定,这会告诉我们数据框中 Tensorflow 不喜欢的内容。
x1 float64
x2 float64
x3 object
dtype: object
如果我们遇到非浮动数据点,下面这行代码将神奇地解决我们所有的问题:
X_train = np.asarray(X_train).astype('float32')
另一件需要检查的事情是是否在任何地方出现了None或np.nan。
要找出这一点,我们可以使用几行代码,例如:
null_mask = X_train.isnull().any(axis=1)
null_rows = X_train[null_mask]
print(null_rows)
这告诉我们,第 0 行和第 1 行中有空值:
x1 x2 x3
0 NaN 0.2 0.3
1 0.1 NaN 0.3
如果是这样,并且这是预期中的/故意的,我们需要用一个可接受的替代值来替换这些值。Fillna可以在这里帮助我们。
X_train.fillna(value=0, inplace=True)
通过下面对代码的这些修改,我们的 NumPy 数组将成功转换为张量数据集,我们可以训练我们的模型!
总结
我常常发现,当我不得不处理错误时,我对某项技术的理解最深,希望这对你也有所帮助!
如果你有一些有趣的技巧或 Tensorflow 错误,请分享给我!
理解 GA4 BigQuery 导出架构和结构
一项定性研究,探讨了一个最奇怪的数据结构,这个数据结构强迫数百万无辜且毫无察觉的分析师使用。
·发布于 Towards Data Science ·阅读时间 11 分钟·2024 年 6 月 21 日
--

你想要的数据肯定在某个地方,你只需要找出如何解开它……照片由 Dean Ward 提供,来源:Unsplash
介绍
目前估计有 1560 万个网站在使用 Google Analytics 4,这意味着 BigQuery GA4 导出可能是历史上最广泛导出的数据架构之一。Google Analytics 4 数据可以通过网页用户界面或通过 API 直接访问 Looker Studio,但如果你想:
-
掌控你的数据,超越 Google 的数据保留政策,
-
归档你的数据,以防止可能的数据丢失,
-
结合你的数据与其他内部数据源,
-
增强你的数据,来自额外的外部数据源、API 或 LLM,或者
-
构建自定义自动化工作流
然后,推荐的做法是启用GA4 导出到 BigQuery,这是一个非常简单的设置和配置过程。
太棒了!工作完成!
还不完全是。一旦数据开始出现在每日的 BigQuery 导出中,你可能会注意到结构有些不规则,这使得直接处理非常困难。这个……
理解直接偏好优化
回顾《直接偏好优化:你的语言模型秘密地是一个奖励模型》论文及其研究结果
·发表于 Towards Data Science ·阅读时间 8 分钟·2024 年 2 月 18 日
--

作者通过 DALL-E 提供的图片
这篇博客文章的灵感来源于我最近与一些朋友关于《直接偏好优化(DPO)》论文的讨论。讨论非常热烈,涵盖了许多关于大语言模型(LLM)和机器学习的核心话题。以下是对其中一些想法和论文中讨论的概念的扩展。
直接偏好优化(DPO)已经成为新型基础模型微调的主要方法。著名的 Mixtral 8x7B 是由 Mistral 创建的稀疏专家混合模型,通过使用 DPO,能够在显著减少参数的情况下,达到 LLaMa 70B 模型的性能水平。自然,这一成功使得许多社区成员开始用 DPO 对自己的模型进行微调。
让我们深入了解一下什么是 DPO 以及我们是如何走到今天这一步的。
高级讨论
首先,我们需要明确从高层次来看微调应该做什么。当你预训练了一个具有强大生成能力的模型后,你通常会希望以某种方式控制它的输出。无论是优化模型使其能够作为聊天机器人进行对话,还是使其能够生成代码……
理解 ARIMA 预测的局限性
SARIMA 模型与 Facebook Prophet 模型的比较
·发表于Towards Data Science ·12 分钟阅读·2024 年 8 月 21 日
--

图片由Maksym Ostrozhynskyy拍摄,来源于Unsplash
目录
-
介绍
-
ARIMA 概述
-
ARIMA 模型的关键假设与局限性
-
Python 中的预测过程:季节性 ARIMA 与 Facebook Prophet
-
两种模型的性能比较
-
结论
介绍
时间序列预测长期以来一直是商业中预测的一个重要工具,尤其是在机器学习(ML)兴起之前。它在资源分配、库存管理和预算规划等领域扮演着重要角色。通常,预测被描述为艺术与科学的结合,因为它既需要技术理解,又需要人类的直觉和判断力。
在时间序列预测技术中,ARIMA(自回归积分滑动平均)是最受欢迎的方法之一,最早由 George Box 和 Gwilym Jenkins 于 1970 年开发。然而,尽管 ARIMA 很受欢迎,但它也存在一些缺点。本文旨在比较 ARIMA 与 Facebook 开发的 Prophet 模型的表现,并突出其…
理解强化学习中 PPO 的数学原理
深入探索适用于初学者的强化学习中的 PPO
·发表于Towards Data Science ·7 分钟阅读·2024 年 12 月 21 日
--

图片由ThisisEngineering提供,来源于Unsplash
介绍
强化学习(RL)是人工智能的一个分支,使得智能体能够学习如何与环境进行交互。这些智能体,包括机器人、软件功能或自主系统,通过试错学习。他们根据所采取的行动获得奖励或惩罚,这些反馈指导他们未来的决策。
在最著名的强化学习算法中,近端策略优化(PPO)因其稳定性和高效性而被广泛青睐。PPO 解决了强化学习中的若干挑战,特别是在控制策略(智能体的决策制定策略)演变方面。与其他算法不同,PPO 确保策略更新不会过大,从而防止训练过程中出现不稳定现象。这种稳定性至关重要,因为过大的更新可能导致智能体偏离最优解,使得学习过程变得不稳定。因此,PPO 在探索(尝试新行动)与利用(专注于获得最大奖励的行动)之间保持平衡。
此外,PPO 在计算资源和学习速度方面也非常高效。通过有效优化智能体的策略,同时避免过于复杂的计算,PPO 已成为机器人、游戏和自主系统等各个领域的实用解决方案。其简洁性使得它易于实现,这也是它在学术研究和工业界广泛应用的原因之一。
本文探讨了强化学习(RL)的数学基础以及 PPO 所引入的关键概念,帮助深入理解为何 PPO 成为现代强化学习研究中的常用算法。
1. 强化学习基础:马尔可夫决策过程(MDP)
强化学习问题通常使用马尔可夫决策过程(MDP)进行建模,MDP 是一个数学框架,有助于在结果不确定的环境中形式化决策过程。
马尔可夫链模型描述了一个系统,该系统在状态之间转移,其中转移到新状态的概率仅依赖于当前状态,而与之前的状态无关。这一原理被称为马尔可夫性质。在 MDP 的背景下,这一简化对于建模决策至关重要,因为它使得智能体在做决策时仅需要关注当前状态,而不必考虑系统的整个历史。
MDP 由以下元素定义:
-
S: 可能的状态集合。
-
A: 可能的动作集合。
-
P(s’|s, a): 在状态 s 下采取动作 a 后到达状态 s’的转移概率。
-
R(s, a): 在状态 s 下采取动作 a 后获得的奖励。
-
γ: 折扣因子(介于 0 和 1 之间的值),反映了未来奖励的重要性。
折扣因子γ在决策问题中对建模未来奖励的重要性至关重要。当智能体做出决策时,它不仅要评估即时奖励,还必须考虑潜在的未来奖励。折扣γ减少了由于不确定性导致的未来奖励的影响,因此,接近 1 的γ值意味着未来奖励几乎与即时奖励同等重要,而接近 0 的值则更侧重于即时奖励。
时间折扣反映了智能体对快速收益相对于未来收益的偏好,这通常源于不确定性或环境可能发生变化的原因。例如,除非未来的奖励足够显著,智能体通常会更倾向于选择即时奖励,而不是远期奖励。因此,这一折扣因子建模了优化行为,其中智能体在做决策时会同时考虑短期和长期的收益。
目标是找到一个动作策略π(a|s),它最大化随时间累积的期望奖励总和,通常称为价值函数:
该函数表示智能体从状态 s 开始并遵循策略π时,能够积累的期望总奖励。
2. 策略优化:策略梯度
策略梯度方法通过最大化一个目标函数,直接优化策略πθ的参数θ,目标函数表示在给定环境中遵循该策略所获得的期望奖励。
目标函数定义为:
其中 R(s, a)表示在状态 s 下采取动作 a 所获得的奖励,目标是最大化随时间变化的期望奖励。术语 dπ(s)表示在策略π下的状态的平稳分布,指示智能体在遵循策略π时访问每个状态的频率。
策略梯度定理给出了目标函数的梯度,提供了一种更新策略参数的方法:
该方程展示了如何根据过去的经验调整策略参数,从而帮助智能体随着时间推移学习更高效的行为。
3. PPO 的数学增强
PPO(Proximal Policy Optimization)引入了几个重要特性,旨在提高强化学习的稳定性和效率,特别是在大型复杂环境中。PPO 由 John Schulman 等人于 2017 年提出,作为对早期策略优化算法(如信任域策略优化(TRPO))的改进。PPO 的主要动机是寻求在样本效率、实现简便性和稳定性之间取得平衡,同时避免 TRPO 的二阶优化方法的复杂性。虽然 TRPO 通过对策略变化施加严格约束来确保策略更新的稳定性,但它依赖于计算开销较大的二阶导数和共轭梯度方法,使其实现和扩展具有挑战性。此外,TRPO 中的严格约束有时会过度限制策略更新,导致收敛速度变慢。PPO 通过使用简单的截断目标函数来解决这些问题,允许策略以稳定且可控的方式更新,避免每次更新时遗忘之前的策略,从而提高训练效率并减少策略崩溃的风险。这使得 PPO 成为广泛强化学习任务的热门选择。
a. 概率比率
PPO 的一个关键组成部分是概率比率,它比较当前策略πθ下采取某个动作的概率与旧策略πθold 下采取同一动作的概率:
这个比率提供了一个度量,表示策略在更新之间的变化程度。通过监控这个比率,PPO 确保更新不会过于剧烈,从而帮助防止学习过程中的不稳定性。
b. 截断函数
在 Proximal Policy Optimization(PPO)中,截断比调整学习率更受偏爱,因为它直接限制了策略更新的幅度,防止了可能导致学习过程不稳定的过度变化。尽管学习率均匀地缩放更新的大小,截断确保更新保持接近先前的策略,从而增强稳定性并减少不稳定行为。
裁剪的主要优点在于它可以更好地控制更新,确保更稳定的进展。然而,潜在的缺点是,它可能会通过限制探索显著不同的策略而减缓学习过程。不过,当稳定性至关重要时,裁剪在 PPO 和其他算法中是被青睐的。
为了避免对策略进行过多的修改,PPO 使用了一个裁剪函数,该函数通过修改目标函数来限制策略更新的幅度。这一点至关重要,因为强化学习中的大幅度更新可能导致不稳定的行为。裁剪后的目标函数是:
裁剪函数将概率比值限制在一个特定范围内,防止那些会与之前的策略偏离过远的更新。这有助于避免可能导致学习过程不稳定的突然而大的变化。
c. 使用 GAE 的优势估计
在强化学习中,估计优势是非常重要的,因为它有助于智能体判断在每个状态下哪些动作比其他动作更好。然而,这之间存在一个权衡:仅使用即时奖励(或非常短的视野)可能会引入较高的方差,而使用较长的视野则可能引入偏差。
广义优势估计(GAE)通过使用 n 步回报和价值估计的加权平均值,在这两者之间找到平衡,使得它对噪声不那么敏感,进而提高了学习的稳定性。
为什么使用 GAE?
-
稳定性:GAE 通过考虑多个步骤来减少方差,使智能体不会对奖励中的噪声或环境中的暂时波动做出反应。
-
效率:GAE 在偏差和方差之间取得了良好的平衡,通过不需要过长的奖励序列,同时仍然保持可靠的估计,从而提高了学习效率。
-
更好的动作比较:通过不仅仅考虑即时奖励,还考虑更广泛的奖励视角,智能体可以更好地比较随时间变化的动作,并做出更加明智的决策。
优势函数 At 用于评估一个动作相对于当前策略下期望行为的优劣。为了减少方差并确保更可靠的估计,PPO 使用了广义优势估计(GAE)。这种方法在控制偏差的同时,平滑了优势的变化:
这种技术提供了一个更加稳定和准确的优势衡量方式,从而提高了智能体做出更好决策的能力。
d. 熵以鼓励探索
PPO 在目标函数中加入了一个熵项,以鼓励智能体更多地探索环境,而不是过早地收敛到次优解。熵项增加了智能体决策的随机性,这有助于防止过度拟合某一特定策略:
其中 H(πθ)表示策略的熵。通过加入这一项,PPO 确保智能体不会过早收敛,并鼓励其继续探索不同的动作和策略,从而提高整体学习效率。
结论
PPO 的数学基础展示了该算法如何实现稳定和高效的学习。通过概率比率、裁剪、优势估计和熵等概念,PPO 在探索和利用之间提供了强大的平衡。这些特性使得它成为研究人员和从业人员在复杂环境中工作的稳健选择。PPO 的简单性结合其高效性和有效性,使其成为强化学习中受欢迎且有价值的算法。
参考文献
本文部分内容通过 DeepL翻译自法语。
理解线性规划中的优化过程管道
本文描述了线性规划中的后端和前端过程,包括数学编程系统(mps)文件、问题矩阵、优化过程、结果提取和解决方案文件,使用的是一个名为 HiGHS 的开源求解器以及其 Python 包装器 highspy。
·发表于Towards Data Science ·7 分钟阅读·2024 年 12 月 27 日
--
在 2021 年文章中,我展示了如何使用 Python 中的 Pyomo 包和 Julia 中的 JuMP 包解决线性优化问题。我还介绍了不同类型的商业和非商业求解器,用于解决线性、混合整数或非线性优化问题。
在这篇文章中,我将介绍用于表示优化问题的数学编程系统(mps)文件、求解器的优化过程以及解决方案文件格式。为此,我将使用与上一篇文章相同的问题,但加入了额外的边界条件。为了实现这一目标,我将使用一个开源求解器HiGHS。HiGHS 被誉为在开源求解器中最强大的线性优化问题求解工具之一。在 Python 中,我只需通过pip install highspy安装highpy 包即可访问该求解器。
不再赘述,让我们开始吧。

问题陈述
问题陈述如下。x和y是两个决策变量。目标是最大化利润,且需满足三个约束条件。x 和 y 各自都有下界和上界。
Profit = 90x + 75y
Objective: maximize Profit subject to:
3x+2y≤66
9x+4y≤180
2x+10y≤200
Bounds:
2≤x≤8
10≤y≤40
使用 highspy 进行优化
在下面的代码中,我将模型初始化为h。然后,我引入了决策变量x和y及其下界和上界,并为它们赋予了名字。接下来,我添加了三个约束不等式,分别命名为 c0、c1 和 c2。每个约束都有 x 和 y 的系数以及一个 RHS 值。然后,我最大化了目标函数 90x+75y 的值。模型在这一行运行。
import highspy
import numpy as np
#initiate the model
h = highspy.Highs()
#define decision variables
x = h.addVariable(lb = 2, ub = 8, name = “x”)
y = h.addVariable(lb = 10, ub = 40, name = “y”)
#h.setOptionValue("solver", "ipm")
#define constraints
h.addConstr(3*x + 2*y<=66) #c0
h.addConstr(9*x + 4*y<=180) #c1
h.addConstr(2*x + 10*y<=200) #c2
#objective
h.maximize(90*x + 75*y)
优化过程中后台发生了什么?
当模型运行时,可以在终端窗口看到以下进展。那么,究竟发生了什么?我将在下文描述:

问题规模:
线性问题中的约束可以以矩阵形式表示为 Ax≤b,其中,A 是约束系数矩阵,x 是包含决策变量的向量,b 是 RHS 值矩阵。对于给定的问题,约束可以如下所示表示为矩阵形式:

以矩阵形式表示约束。插图由作者提供。
问题矩阵的大小由行数、列数和非零元素的数量来描述。行数表示约束的数量(此处为 3),列数表示决策变量的数量(此处为 2),元素/非零元素表示系数,这些系数不为零。在所有三个约束中,都没有系数为零的情况。因此,总的非零元素数量是六个。
这是一个非常简单问题的例子。实际上,可能会有一些问题,其行数、列数和非零元素的数量可以达到成千上万或百万级别。问题规模的增加会导致模型的复杂度增加,从而延长求解所需的时间。
系数范围
问题中 x 和 y 的系数范围从 2 到 10。因此,矩阵的系数范围显示为[2e+00, 1e+01]。
成本在此处指的是目标函数。x 的系数是 90,y 的系数是 75。因此,成本的系数范围是[8e+01, 9e+01]。
x 和 y 的下界和上界范围为 2 到 40。因此,Bound 的系数范围是[2e+00, 4e+01]。
右边约束(RHS)的系数范围在 66 到 200 之间。因此,RHS 的系数范围是[7e+01, 2e+02]。
预处理 Presolve是求解器尝试解决优化问题时的初始过程,它首先会尝试简化模型。例如,它可能将超出某个值的系数视为无穷大。预处理的目的是创建一个更小版本的问题矩阵,具有相同的目标函数,并且可映射到原始问题的可行空间。简化后的问题矩阵会比原始矩阵更简单、更容易解决,并且解决速度更快。
在这种情况下,预处理步骤仅用了两次迭代就完成,结果是一个空矩阵。这也意味着已经获得了解决方案,且无需进一步优化。返回的目标值为 2100,HiGHS 求解器的运行时间仅为 0.01 秒。在从优化中获得解决方案后,求解器可以使用后处理/取消预处理步骤,将解决方案映射到原始问题的可行空间。
数学规划系统(MPS)格式
数学规划系统(MPS)是一种用于表示线性和混合整数线性规划问题的文件格式。它是一种相对较老的格式,但所有商业线性程序求解器都接受这种格式。线性问题也可以用其他格式表示,如 LP、AMPL 和 GAMS。
可以使用highspy通过简单地使用h.writeModel("foo.mps")来写入 MPS 文件。而读取 MPS 文件也很简单,只需使用h.readModel("foo.mps")。

给定线性规划问题的 MPS 格式。作者插图。
给定优化问题的 MPS 文件结构如上所示。它以线性规划问题的名称(NAME)开始。OBJSENSE 表示问题是最小化(MIN)还是最大化(MAX),这里是后者。ROWS 部分表示目标、所有约束的名称以及它们的等式/不等式类型。E 代表等式,G 代表大于或等于约束,L 代表小于或等于约束,N 代表无约束行。这里,三个约束被给出为 __c0、__c1 和 __c2,而 Obj 是目标的缩写。
在 COLUMNS 部分,决策变量的名称(这里是 x 和 y)列在左侧,属于目标函数或约束不等式的系数列在右侧。RHS 部分包含模型约束的右侧向量。决策变量的上下界在 BOUNDS 部分定义。MPS 文件以 ENDATA 结尾。
优化过程与结果获取
HiGHS 使用如 单纯形法 或内点法等算法进行优化过程。解释这些算法值得单独写一篇文章,我希望将来能涉及到它们。
提取结果的代码如下所示。模型状态为最优。我提取了目标函数值和决策变量的解值。此外,我还打印了迭代次数、原始解和对偶解的状态,以及基准有效性。
solution = h.getSolution()
basis = h.getBasis()
info = h.getInfo()
model_status = h.getModelStatus()
print("Model status = ", h.modelStatusToString(model_status))
print()
#Get solution objective value, and optimal values for x and y
print("Optimal objective = ", info.objective_function_value)
print ("Optimal value of x:", solution.col_value[0])
print ("Optimal value of y:", solution.col_value[1])
#get model run characteristics
print('Iteration count = ', info.simplex_iteration_count)
print('Primal solution status = ', h.solutionStatusToString(info.primal_solution_status))
print('Dual solution status = ', h.solutionStatusToString(info.dual_solution_status))
print('Basis validity = ', h.basisValidityToString(info.basis_validity))

打印上面代码的结果。插图由作者提供。
解文件
在优化过程之后,HiGHS 允许将解决方案写入一个 .sol 扩展名的解文件中。此外,解决方案可以按照这里给出的不同格式写入。1 表示 HiGHS 精美格式,3 表示 Glpsol 精美格式。

HiGHS 可用的解决方案文件样式。插图来源于HiGHS 文档。
为了以风格 3 获取解决方案,我使用了 h.writeSolution("mysolution.sol", 3)。问题统计信息在顶部提供。最优解值在活动列中提供。St 列指定了解的状态。例如,B 表示基本——变量或约束是基础解的一部分(最优)。NU 表示该解是非基础的,并且与上界相同。边际列中的值(通常称为影子价格或对偶值)指的是当非基础变量单位变化时,目标函数的变化量。有关 GLPK 解文件信息的更多信息,可以参考这里。

Glpsol 精美风格下解决方案文件的结构。插图由作者提供。
结论
在这篇文章中,我展示了如何使用名为 HiGHS 的开源求解器和 Python 中的 highspy 包解决一个简单的线性优化问题。接下来,我解释了如何通过系数矩阵、决策变量向量和右侧向量来推断优化问题的大小。我介绍并解释了用于表示优化问题的数学规划系统(mps)文件的不同组成部分。最后,我演示了求解器的优化过程,提取结果的步骤以及分析解文件的过程。
本文的笔记本和相关文件可以在这个 GitHub 仓库中找到。感谢您的阅读!
理解 Mixtral 中的稀疏专家混合(SMoE)层
本博客文章将探讨《极大规模神经网络:稀疏门控专家混合层》论文的研究成果及其在 Mixtral 中的实现。
·发表于 Towards Data Science ·8 分钟阅读·2024 年 3 月 21 日
--

作者提供的图片,由 DALL-E 生成
专业化的探索
当遇到困难的问题时,分而治之通常是一个有价值的解决方案。无论是亨利·福特的流水线,归并排序如何分割数组,还是社会上如何分工,使每个人专注于特定的工作,这个列表还可以继续下去!
自然地,当人们开始着手教计算机进行推理时,分解任务成机器可以处理的多个组件是有意义的——例如,数学一个组件,科学一个组件,语言一个组件,等等。

作者提供的图片
然而,这一想法尚未成功实现。可能是因为它与我们的大脑并非完全独立的组件类似:复杂的推理需要多种不同部分协同工作,而不是单独使用。
理解 Shiny for Python 的两面性:Core 和 Express
探索 Shiny Core 和 Shiny Express for Python 的区别及使用场景
·发布于Towards Data Science ·7 分钟阅读·2024 年 5 月 29 日
--

图片由Vincent van Zalinge提供,来源:Unsplash
Posit 正在快速扩展其在 Python 领域的影响力。Shiny for Python 于去年宣布发布,随后不久便发布了Shiny Express。但如果你对 Shiny 或 Python 世界比较陌生,应该首先学习哪个工具,并且如何在两者之间无缝切换呢?在本文中,我将介绍 Shiny “Core”和“Express”的结构划分以及它们的主要区别。
Shiny 里面都有哪些内容?
Shiny “Core”
Shiny for Python 是一个用于在 Python 中构建互动网页应用的框架。本文将介绍 Shiny Core 的一些基础组件,如果你想进一步深入学习如何使用 Shiny Core 从基础到样式构建一个网页应用,欢迎查看我之前写的文章。
Shiny “Express”
Shiny Express 旨在大大简化入门过程,并通过最少的样板代码编写简单的应用。
如果已经有 Core,为什么还需要 Express?
理解时间序列的结构性变化
如何使用 Python 检测时间序列变化点
·发表于 Towards Data Science ·7 分钟阅读·2024 年 9 月 4 日
--

摄影: Robby McCullough 来自 Unsplash
在本文中,我们将:
-
定义时间序列的结构性变化是什么,以及它们与异常值的区别。
-
概述不同类型的结构性变化。
-
探索变化点检测方法,例如使用 kats 和 ruptures 包的 CUSUM 方法。
引言
平稳性是时间序列分析和预测中的核心概念。在平稳性条件下,时间序列的属性(如均值)保持不变,仅有一些偶然的波动。
然而,现实世界的数据集中很少观察到平稳性。时间序列可能会出现结构性变化或断裂,这些变化引入了非平稳性变异,改变了时间序列的分布。标志变化开始的时间步称为变化点。
检测结构性变化在时间序列分析和预测中非常有价值。新出现的分布往往使得过去的数据变得过时,进而影响其中的模型。这就需要你通过最近的数据或其他合适的策略来更新你的模型。如果历史数据中出现变化点,你可以通过特征工程来处理它们。
理解 Transformer
《Attention is All You Need》¹的简明分解
·发布于 Towards Data Science ·阅读时间:10 分钟·2024 年 6 月 27 日
--
Transformer 于 2017 年发布。尽管已有许多文章解释其工作原理,但我经常发现它们要么深入探讨数学内容,要么对细节的讲解过于浅显。我往往需要花费与阅读同样多的时间去谷歌搜索(或使用 ChatGPT),这显然不是理解一个主题的最佳方法。因此,我写了这篇文章,试图简洁明了地解释 Transformer 最具革命性的方面,让任何人都能轻松阅读。
本文假设读者对机器学习原理有一般性的理解。

Transformer,正在转变。图片来源:DALL-E(反正我们也在学习生成式人工智能)
Transformer 背后的理念将我们带入了生成式人工智能的时代。
Transformer 代表了一种新的序列转导模型架构。序列模型是一种将输入序列转化为输出序列的模型。这些输入序列可以是各种数据类型,例如字符、单词、符号、字节、数字、音素(语音识别),也可以是多模态的¹。
在 Transformer 出现之前,序列模型主要基于递归神经网络(RNN)、长短期记忆网络(LSTM)、门控递归单元(GRU)和卷积神经网络(CNN)。这些模型通常包含某种形式的注意力机制,用于考虑序列中各个位置的元素所提供的上下文。
以前模型的缺点

RNN 插图。图片来源:Christopher Olah
- RNNs:该模型按顺序处理数据,因此从前一次计算中学到的任何内容都会在下一次计算中得到考虑²。然而,它的顺序性质带来了一些问题:模型在处理较长序列时难以考虑长期依赖关系(即梯度消失或爆炸问题),并且阻止了输入序列的并行处理,因为你无法在同一时间对输入的不同部分进行训练(批处理),否则会失去前部分的上下文。这使得训练变得更加计算密集。

LSTM 和 GRU 概述。图片来源:Christopher Olah
-
LSTM 和 GRUs:利用门控机制来保持长期依赖关系³。该模型具有一个单元状态,包含整个序列的相关信息。单元状态通过门控(如遗忘门、输入门、输出门(LSTM)、更新门、重置门(GRU))进行变化。这些门控决定了在每一次顺序迭代中,应该保留前一状态的多少信息,应该添加多少来自新更新的信息,然后在最终保留新单元状态的哪个部分。虽然这解决了梯度消失问题,但模型仍然是顺序工作的,因此由于并行化有限,尤其是当序列变长时,训练较慢。
-
CNNs:以更并行的方式处理数据,但从技术上讲仍然是顺序操作。它们擅长捕捉局部模式,但由于卷积工作的方式,难以处理长期依赖关系。捕捉两个输入位置之间关系所需的操作数量随着位置之间的距离增加。
因此,引入了Transformer,该模型完全依赖注意力机制,并且不再使用循环和卷积。注意力机制是模型在每一步生成输出时,用来集中关注输入序列不同部分的机制。Transformer 是第一个不依赖顺序处理而使用注意力机制的模型,允许并行化,因此在不丧失长期依赖关系的情况下加速训练。它还在输入位置之间执行恒定数量的操作,无论这些位置相距多远。
深入了解 Transformer 模型架构

Transformer 架构。图片来源:Attention is All You Need
Transformer 的关键特性有:分词,嵌入层,注意力机制,编码器和解码器。假设有一个法语输入序列:“Je suis etudiant”和一个目标输出序列为英语“I am a student”(我在这里直接复制了这个链接,它非常详细地解释了这个过程)
分词
输入的单词序列会转换成长度为 3 到 4 个字符的标记
嵌入
输入和输出序列会映射到一个连续表示的序列z,该序列代表了输入和输出的嵌入。每个标记会通过嵌入表示,从而捕获某种含义,这有助于计算它与其他标记的关系;该嵌入将作为向量表示。为了创建这些嵌入,我们使用训练数据集的词汇表,它包含了用于训练模型的每个唯一输出标记。接着,我们确定一个合适的嵌入维度,这对应于每个标记的向量表示的大小;较高的嵌入维度将更好地捕获更复杂/多样/精细的含义和关系。因此,嵌入矩阵的维度,对于词汇表大小 V 和嵌入维度 D,变成 V x D,从而使其成为一个高维向量。
在初始化时,这些嵌入可以随机初始化,并且更精确的嵌入会在训练过程中学习。嵌入矩阵随后会在训练过程中更新。
位置编码会添加到这些嵌入中,因为 Transformer 没有内置的标记顺序感知能力。

计算标记“it”的注意力分数。如你所见,模型将大量注意力集中在标记“The”和“Animal”上。图片来源:Jay Alammar
注意力机制
自注意力机制是每个标记在序列中与每个其他标记计算注意力分数,以理解标记之间的关系,不管它们彼此之间的距离如何。我会避免在本文中过多涉及数学内容,但你可以在这里了解用于计算注意力分数的不同矩阵,从而捕获每个标记与其他标记之间的关系。
这些注意力分数将生成每个标记的新一组表示⁴,然后在下一层处理中使用。在训练过程中,权重矩阵会通过反向传播进行更新,以便模型能够更好地考虑标记之间的关系。
多头注意力是自注意力的扩展。计算不同的注意力得分,结果被拼接并转换,最终的表示增强了模型捕捉词汇间各种复杂关系的能力。
编码器
输入词嵌入(由输入序列生成)带有位置编码,被输入到编码器中。编码器有 6 层,每一层包含 2 个子层:多头注意力和前馈网络。此外,还有一个残差连接,导致每一层的输出为 LayerNorm(x+Sublayer(x)),如图所示。编码器的输出是一系列向量,这些向量是考虑注意力得分后的输入的上下文化表示。这些向量随后被传送到解码器中。
解码器
输出词嵌入(由目标输出序列生成)带有位置编码,被输入到解码器中。解码器也包含 6 层,与编码器有两个不同之处。
首先,输出词嵌入通过掩蔽的 多头注意力层,这意味着计算注意力得分时会忽略序列中后续位置的嵌入。这是因为在生成当前位置 i 的词时,我们应该忽略位置 i 之后的所有输出词。此外,输出词嵌入会向右偏移一个位置,使得位置 i 的预测词仅依赖于小于它的输出位置的词。
例如,假设输入是“je suis étudiant à l’école”,目标输出是“i am a student in school”。在预测student这个词时,编码器会提取“je suis etudiant”的词嵌入,而解码器则会隐藏“a”之后的词汇,这样预测student时仅考虑句子中之前的词汇,即“I am a”。这训练模型按顺序预测词汇。当然,“in school”这些词为模型的预测提供了额外的上下文,但我们在训练模型时,是让它从输入词汇“etudiant”以及后续输入词汇“à l’école”中捕捉到这个上下文。
解码器是如何获取上下文的呢?这就引出了第二个区别:解码器中的第二个多头注意力层接收在输入传入前经过上下文化表示的输入,以确保输出表示能够捕捉到输入词汇和之前输出的完整上下文。这为每个目标词生成一系列向量,即上下文化的目标表示。

图像来源:Jay Alammar
使用线性层和 Softmax 层进行预测
现在,我们希望使用这些上下文化的目标表示来确定下一个标记是什么。使用解码器中的上下文化目标表示,线性层将向量序列投影到一个更大的logits 向量中,该向量的长度与我们的模型词汇表的长度相同,假设长度为 L。线性层包含一个权重矩阵,乘以解码器的输出并加上一个偏置向量,生成一个大小为 1 x L 的 logits 向量。每个单元格表示一个独特标记的分数,softmax 层随后对该向量进行归一化,使整个向量的和为 1;此时每个单元格表示每个标记的概率。选择概率最高的标记, voilà!我们得到了预测标记。
训练模型
接下来,我们将预测的标记概率与实际的标记概率进行比较(实际标记概率对于每个标记来说是一个全为 0 的 logits 向量,除了目标标记,其概率为 1.0)。我们为每个标记预测计算合适的损失函数,并将整个目标序列上的损失进行平均。然后,我们反向传播这个损失,计算所有模型参数的合适梯度,并使用合适的优化算法来更新模型参数。因此,对于经典的变换器架构,这会导致以下更新:
-
嵌入矩阵
-
用于计算注意力分数的不同矩阵
-
与前馈神经网络相关的矩阵
-
用于生成 logits 向量的线性矩阵
第 2 至第 4 行中的矩阵是权重矩阵,每个输出还附有额外的偏置项,这些偏置项在训练过程中也会更新。
注意: 线性矩阵和嵌入矩阵通常是彼此的转置。这是《Attention is All You Need》论文中的情况;这一技术称为“权重绑定”。因此,需要训练的参数数量得以减少。
这代表了一次训练周期。训练包含多个周期,周期数取决于数据集的大小、模型的大小以及模型的任务。
回到为什么变换器如此优秀
如前所述,RNN、CNN、LSTM 等的缺点包括缺乏并行处理、顺序架构以及无法充分捕捉长期依赖关系。上述的变换器架构解决了这些问题,因为…
-
注意力机制允许整个序列并行处理而不是顺序处理。通过自注意力机制,输入序列中的每个标记都会关注输入序列中的其他所有标记(在下文解释的该小批次中)。这会同时捕捉所有关系,而不是按顺序捕捉。
-
在每个 epoch 中进行输入的小批量处理允许并行处理、加快训练,并且更容易扩展模型。在一个充满示例的大文本中,小批量表示这些示例的较小集合。数据集中的示例在被放入小批量之前会被打乱,并在每个 epoch 开始时重新洗牌。每个小批量会同时传入模型。
-
通过使用位置编码和批处理,考虑到序列中标记的顺序。标记之间的距离也会被平等地考虑,无论它们之间的距离有多远,而小批量处理进一步确保了这一点。
正如论文中所示,结果非常出色。
欢迎来到变换器的世界。
关于 GPT 架构的简要介绍
Transformer 架构由研究员 Ashish Vaswani 于 2017 年提出,当时他在 Google Brain 工作。生成式预训练变换器(GPT)由 OpenAI 于 2018 年推出。两者的主要区别在于 GPT 的架构中没有编码器堆栈。编码器-解码器结构在直接将一个序列转换为另一个序列时非常有用。而 GPT 的设计侧重于生成能力,它去除了编码器,同时保持了其他组件相似。

图片来源:通过生成预训练提高语言理解
GPT 模型是在一个大型文本语料库上进行预训练的,采用无监督学习,以了解所有单词和标记之间的关系⁵。经过针对不同应用场景(如通用聊天机器人)的微调后,它们在生成任务中已被证明非常有效。
示例
当你问它一个问题时,预测的步骤与常规的变换器大致相同。如果你问它问题:“GPT 如何预测回应”,这些词会被标记化,生成嵌入,计算注意力分数,计算下一个词的概率,并选择一个标记作为下一个预测的标记。例如,模型可能会一步步生成回应,从“GPT 通过…预测回应”开始,并根据概率继续,直到形成完整且连贯的回应。(猜猜看,最后一句话来自 chatGPT)。
我希望这一切足够容易理解。如果没有,那也许是时候让别人来尝试解释一下变换器了。
如果这篇文章对你有启发,欢迎分享你的想法并与我联系!
-
LinkedIn:
www.linkedin.com/in/aveekg00/ -
网站:aveek.info
参考文献:
其他值得参考的优秀文章:
理解 V 结构及其在因果验证和因果推断中发挥的关键作用
如何检测并纠正有向无环图中与基础数据不符的因果关系方向
·发表于Towards Data Science ·阅读时长 27 分钟·2024 年 3 月 19 日
--

图片由Fré Sonneveld拍摄,来源于Unsplash
介绍
因果推断是机器学习中的一个新兴领域,它不仅能够预测可能发生的事情,还能解释为什么会发生,并且通过这样做,有望永久解决根本问题,而不是仅仅应对潜在的后果。
解决因果推断问题需要通过“有向无环图”(Directed Acyclic Graph,简称 DAG)可视化因果关系因素,该图通常由领域专家开发,他们对系统或过程中的因果关系有深入的了解。
这种方法面临的挑战是领域专家的观点可能存在缺陷或偏见,如果没有准确的 DAG,因果模型的结果和输出将不准确,从而无效。因此,确保 DAG 准确反映因果关系的过程被称为因果验证。
因果验证中的一个具体问题是检测两个因素之间的因果方向…
理解何时以及如何实现 FastAPI 中间件(示例和使用案例)
通过中间件增强你的 FastAPI:实用使用案例和示例
·发布于 Towards Data Science ·阅读时间 4 分钟·2024 年 12 月 25 日
--

图片来自 ChatGPT
中间件位于 API 路由器与其路由之间,充当一个层,在此层中可以在请求处理之前和之后运行代码。在本文中,我们将探讨 FastAPI 中间件的两个关键使用案例,展示它是如何工作的以及为什么它很有用。让我们开始编码吧!
A. 设置
首先,让我们创建一个简单的 API,作为我们中间件示例的基础。下面的应用程序只有一个路由:test,它通过在返回“OK”之前暂停几毫秒来模拟实际的工作。
import random
import time
from fastapi import FastAPI
app = FastAPI(title="My API")
@app.get('/test')
def test_route() -> str:
sleep_seconds:float = random.randrange(10, 100) / 100
time.sleep(sleep_seconds)
return "OK"
什么是中间件?
中间件充当传入 HTTP 请求和应用程序处理之间的过滤器。可以把它想象成机场安检:每位乘客在登机前后都必须经过安检。同样,所有 API 请求都会经过中间件:在处理之前和之后都要经过中间件。
理解“你只需缓存一次”
本文将详细探讨《你只需缓存一次:面向语言模型的解码器-解码器架构》论文及其发现。
·发表于数据科学前沿 ·阅读时间 9 分钟·2024 年 6 月 4 日
--

作者图片 — 由 Stable Diffusion 生成
随着大语言模型(LLM)领域的逐渐成熟,越来越多的努力致力于提升当前性能并使其更具成本效益。这一目标的实现方式包括为其创建定制硬件(例如 Groq 的语言处理单元),优化它们所交互的低层软件(例如苹果的 MLX 库或 NVIDIA 的 CUDA 库),以及在高层软件的计算过程中更加精细化。
《你只需缓存一次:面向语言模型的解码器-解码器架构》论文提出了一种新的大语言模型架构,通过使用内存高效的架构来提高性能。他们称之为 YOCO。
让我们深入探讨一下!
键值(KV)缓存
为了理解这里所做的变化,我们首先需要讨论键值缓存。在变换器(transformer)中,我们有三个对注意力机制至关重要的向量——键(key)、值(value)和查询(query)。从高层次看,注意力机制是我们如何将关于前一个标记的关键信息传递给当前标记,以便它可以预测下一个标记。在单头自注意力的示例中,我们将当前标记的查询向量与前一个标记的键向量相乘,然后对结果矩阵进行归一化(我们称这个结果矩阵为注意力模式)。接着,我们将值向量与注意力模式相乘,以获得对每个标记的更新。然后,这些数据被加到当前标记的嵌入中,从而使其拥有了判断下一个标记所需的上下文。

方程 1 来自“Attention Is All You Need”
我们为每一个新生成的 token 创建注意力模式,因此虽然查询会变化,键和值是恒定的。因此,当前的架构尝试通过缓存每次生成的键和值向量来减少计算时间。这种缓存称为键值缓存(Key-Value Cache)。
虽然像编码器单一模型和编码器-解码器模型这样的架构已经取得了一定成功,但作者认为上面所示的自回归机制及其带来的速度,是为什么如今解码器单一模型最常用的原因。
YOCO 架构
要理解 YOCO 架构,我们必须从理解它如何设置其层开始。
在这一层的前半部分,我们使用一种类型的注意力机制来生成填充 KV 缓存所需的向量。一旦进入第二部分,它将分别使用 KV 缓存中的键和值向量,生成输出的 token 嵌入。

图 2来自论文
这种新架构需要两种类型的注意力机制——高效自注意力和交叉注意力。我们将分别介绍这两种机制。
高效自注意力和自解码器
高效自注意力(ESA)旨在实现恒定的推理内存。换句话说,我们希望缓存的复杂度不依赖于输入长度,而是依赖于我们模块中的层数。在下面的方程中,作者抽象出了 ESA,但其余的自解码器部分保持一致,如下所示。

方程 1来自论文
让我们一步一步地分析这个方程。X^l 是我们的 token 嵌入,Y^l 是用于生成下一个 token 嵌入 X^l+1 的中间变量。在方程中,ESA 表示高效自注意力,LN 是层归一化函数——这里始终使用的是根均方根归一化(RMSNorm),最后是SwiGLU。SwiGLU定义如下:

SwiGLU 定义来自论文
这里swish = x*sigmoid (Wg * x),其中 Wg 是一个可训练的参数。然后,我们找到这个结果与 X*W1 之间的元素按位乘积(Hadamard 乘积),再将整个乘积与 W2 相乘。SwiGLU的目标是获得一个激活函数,它能有条件地传递不同数量的信息,通过层传递到下一个 token。

来自“Hadamard 乘积(矩阵)”的元素按位乘积(Hadamard 乘积)示例
现在我们了解了自解码器是如何工作的,让我们来看一下作者考虑实现 ESA 的两种方式。
门控保持 ESA
首先,他们考虑了所谓的门控保持(Gated Retention)。保持和自注意力的确非常相似,"Retentive Network: A Successor to Transformer for Large Language Models" 论文的作者指出,主要区别在于激活函数——保持移除了 softmax,从而实现了递归公式。他们使用这种递归公式及其并行性来提升内存效率。
深入数学细节:

方程 4 来自论文
我们有典型的 Q、K 和 V 矩阵——每个矩阵都与其相应的可学习权重相乘。然后我们计算加权矩阵与标量 Θ 的 Hadamard 乘积。使用 Θ 的目标是创建指数衰减,然后我们使用 D 矩阵来帮助进行因果掩蔽(防止未来的标记与当前标记交互)和激活。
门控保持与通过 γ 值实现的保持是不同的。在这里,矩阵 Wγ 用来使我们的 ESA 数据驱动。
滑动窗口 ESA
滑动窗口 ESA 引入了限制注意力窗口应关注多少标记的概念。在常规自注意力中,所有先前的标记都会以某种方式被关注(即使它们的值为 0),而在滑动窗口 ESA 中,我们选择一个常数值 C 来限制这些矩阵的大小。这意味着在推理时,KV 缓存的复杂度可以保持恒定。
让我们再次深入探讨数学内容:

方程 8 来自论文
我们将矩阵按其相应的权重进行缩放。接下来,我们计算注意力头,类似于多头注意力的计算方式,其中 B 既充当因果映射,又确保只有 C 后面的标记被关注。
无论是使用滑动窗口还是门控保持,层的前半部分目标都是生成 KV 缓存,然后在后半部分使用该缓存来生成输出标记。
现在我们将具体看看全局 KV 缓存如何加速推理过程。
交叉注意力和交叉解码器
一旦进入层的后半部分,我们首先创建全局 KV 缓存。该缓存由 K-hat 和 V-hat 组成,我们通过对来自层前半部分的标记进行层归一化处理,然后将其与相应的权重矩阵相乘来创建这些值。

方程 2 来自论文
创建了全局 KV 缓存后,我们利用不同的解码器和注意力来生成下一个标记。接下来让我们深入探讨下面的数学内容:

方程式 3 来自论文
我们通过获取标记嵌入并执行相同的归一化操作,然后进行矩阵乘法,生成查询矩阵,这与之前在 K-hat 和 V-hat 上执行的操作相同,唯一的区别是我们对每个通过的标记都执行此操作,而不仅仅是对来自第一半层末尾的标记。然后我们在三个矩阵上运行交叉注意力,使用之前的归一化和 SwiGLU 来确定下一个标记应该是什么。这个 X^l+1 就是接下来被预测的标记。
交叉注意力非常类似于自注意力,区别在于交叉注意力利用来自不同语料库的嵌入。
内存优势
让我们从分析 Transformers 和 YOCOs 之间的内存复杂度开始。对于 Transformer,我们必须在内存中保留输入序列的权重(N),以及每一层的权重(L),然后对于每个隐藏维度(D)执行相同的操作。这意味着我们存储的内存量是 L * N * D。
相比之下,YOCO 的拆分特性意味着我们有两种情况需要分析,以找出大 O 内存复杂度。当我们运行到第一半层时,我们正在执行高效的自注意力机制,而我们知道自注意力需要一个常数缓存大小(无论是通过滑动窗口注意力还是门控保持)。这使得它的大 O 依赖于每层的权重(L)和第一半层的隐藏维度数(D)。第二半层使用交叉注意力,它在内存中保持输入序列的权重(N),但随后使用常数全局缓存,因此从大 O 内存分析的角度来看,它不会改变。因此,唯一的其他依赖项是第二半层的隐藏维度数(D),我们可以认为它们是相同的。因此,我们存储的内存量为 L * D + N * D = (N + L) * D

表格 1 来自论文
作者指出,当输入大小显著大于层数时,大 O 计算近似为 O(N),这也是他们称其模型为“You Only Cache Once”的原因。
推理优势
在推理过程中,我们有两个主要阶段:预填充(有时称为初始化)和生成(有时称为解码)。在预填充阶段,我们将提示输入并创建生成第一个输出所需的所有计算。这可以从将模型权重加载到 GPU 内存中开始,然后以第一个标记的输出结束。一旦第一个输出创建完成,Transformer 的自回归特性意味着创建整个响应所需的大部分计算已经完成。

图 3 来自论文
从预填充阶段开始,transformer 和 YOCO 模型将同时加载权重到 GPU 内存中,但在此之后,YOCO 有两个主要优势。首先,由于 YOCO 的自解码器可以并行运行,因此它的运行速度显著快于没有并行化的常规自注意力机制。其次,由于只有前半部分生成全局 KV 缓存,在预填充过程中只有一半的层需要运行,这大大减少了计算量。这两个因素使得 YOCO 的预填充阶段比 transformer 快得多(大约快 30 倍!)
在生成阶段,由于上述原因,我们使用 YOCO 时不需要像使用 transformer 时那样频繁更改 GPU 内存。这是 YOCO 能够实现高吞吐量的一个重要原因。

图 1 的部分来自论文
所有这些指标都表明,单单架构的变化就能为这些模型带来显著的效率提升。
结论
随着新架构的出现,难免会面临一些困境。在已经花费数十亿美元训练旧架构的模型之后,公司理所当然地会质疑是否值得再花费数十亿资金来采用可能很快就会过时的新架构。
解决这一困境的一个可能方案是迁移学习。这里的想法是将噪声引入训练过的模型,然后利用给定的输出进行反向传播到新的模型。这里的想法是,你无需担心生成大量新的数据,并且你需要训练的 epoch 次数可能会显著减少。这个想法尚未完善,因此未来它将扮演怎样的角色还有待观察。
尽管如此,随着企业对这些架构的投入不断增加,改善成本的更新架构的潜力也将日益增加。时间会告诉我们,行业采纳这些架构的速度有多快。
对于那些正在构建能够实现模型之间无缝转换的应用程序的开发者,你可以参考 YOCO 在吞吐量和延迟方面所取得的重要进展,并希望你的应用程序面临的主要瓶颈很快会得到解决。
现在是构建的激动人心的时刻。
特别感谢 Christopher Taylor 对本文的反馈。
[1] Sun, Y., et al. “You Only Cache Once: Decoder-Decoder Architectures for Language Models” (2024), arXiv
[2] Sun, Y., et al. “Retentive Network: A Successor to Transformer for Large Language Models” (2023), arXiv
[3] Wikimedia Foundation, et al. “Hadamard product (matrices)” (2024), Wikipedia
[4] Sanderson, G. 等人, “变压器中的注意力机制,视觉化解释 | 第六章,深度学习” (2024), YouTube
[5] A. Vaswani 等人, “Attention Is All You Need” (2017), arXiv
多项全能田径比赛中的不均衡得分
七项全能和十项全能比赛中的表现和奖励模式
·发布于Towards Data Science ·阅读时长:11 分钟·2024 年 9 月 3 日
--

图片由作者使用 DALL-E 3 生成
在观看2024 年奥运会七项全能比赛时,我想起了七项全能比赛中每个项目的得分总是呈现出一种规律:第一个项目,100 米栏,通常在各项比赛中得分较高,而第三个项目,铅球,通常每名运动员的得分会少几百分。
这促使我思考了两个问题:i) 为什么得分是这样出来的,以及 ii) 这是否意味着 某些项目比其他项目更重要,从而影响七项全能比赛的最终胜负?这些问题同样适用于十项全能比赛,当然,这也是本文讨论的内容。
数据
我从维基百科收集了 2007 年到 2023 年间世界锦标赛七项全能和十项全能比赛的结果。这些是这两项多项赛事的顶尖水平表现,因此从这项分析中获得的见解仅适用于这个高水平的赛事,而不一定适用于一般的七项全能或十项全能比赛。关于计分系统的详细信息来自SportsCalculators,并由世界田径最初发布。
在下面的分析中,我将使用“得分”一词来指代运动员在每个项目中记录的身体表现成绩(如高度、长度/距离或时间),而“积分”一词则指代基于该成绩获得的七项全能或十项全能积分。
结果:七项全能
积分分布

表 1:世界锦标赛七项全能各项目的积分。图像来自作者。
表 1中七项全能各项目所获得的平均(中位数)积分清晰地显示了我们所讨论的模式:短跑项目(200 米,特别是 100 米栏)平均比投掷项目(标枪和铅球)多出约 200 积分。这似乎令人惊讶,但不一定重要,因为所有运动员都参与所有项目,因此真正重要的是相互之间的得分差异。上表中的第三列显示了积分的四分位数范围;即第 25 和第 75 百分位数之间的差异,或者说是“中间一半”运动员的积分范围。这里我们看到,跑步项目的积分分布最为紧凑,而跳高和标枪的积分范围最大。这表明,某些项目中,相较于世界锦标赛水平的竞争者,表现较差与表现较好之间的差距在积分上更加重要。
计分系统
这一效应的原因在于计分系统。七项全能和十项全能的计分系统自 1984 年以来一直保持现有形式,并且为每个项目使用类似于以下形式的公式:
积分 = a * (成绩与参考成绩 b 之间的差距) ^ c
其中“^”表示“幂运算”。因此,每个项目都需要定义三个系数,a、b 和 c。这些系数的值在项目之间无法单独比较,但三个系数的组合会为每个项目创建一条积分曲线,如下所示在图 1中所示。世界田径关于计分系统的文件解释了系数选择的一个因素是每个项目的世界纪录:期望每个项目中的世界纪录表现应产生相同数量的积分。他们指出,实际上,这意味着“每个单独项目中创下的最佳成绩会有很大的差异”,但更重要的是“同一项目中不同运动员之间的得分差异大致与他们的表现差异成比例”。

图 1:七项全能各项目的得分分布(黑色线条,10 至 90 百分位数以绿色显示),所获积分(蓝色线条,y 轴),以及世界纪录(红色虚线)。图像来自作者。
图 1中的蓝色线条显示了每个项目中得分与点数之间的关系。线条几乎是直线,特别是在绿色阴影得分区域内,这是得分的 80%所包含的区域,意味着评分系统实际上可以视为一个线性系统。(某些曲线有轻微的向上弯曲,尤其是在跳远和 800 米的高分段,这意味着优秀表现的奖励略高于真正的线性系统,但这些差异很小,不是本分析的关键点。)
更有趣的是每个项目实际上可获得的得分范围。这通过垂直箭头表示,箭头展示了从 10 百分位数的得分(绿色区域的左边缘)到 90 百分位数的得分(绿色区域的右边缘)所增加的得分。10 次表现中有 8 次落在这些范围内,低于或高于这些范围则在该项目中比较罕见。这个得分范围的大小(箭头的高度)在某些项目中明显较大,最显著的是标枪,而在其他项目中,如 100 米栏,则明显较小。这几乎与之前所见的四分位数范围数字相同。
每个项目的世界纪录位置(红色虚线)展示了为什么投掷项目的平均得分较低:普通七项全能运动员的投掷距离仅为世界纪录的约 60%(最佳专业运动员,单项运动员),无论是铅球还是标枪,但同一运动员在 100 米栏和 200 米项目中的成绩可达到世界纪录的 85%到 90%之间(从时间转换而来)。
造成这种情况的原因可能有多个,但一个重要的后续点似乎是合理的假设:在某个项目的表现远低于世界纪录时,这个项目比那些表现已经接近极限的项目更有潜力改善。换句话说,大多数七项全能运动员与这些项目的专业运动员相比,表现相对较好(包括 800 米),但在标枪项目中,他们的表现通常与该项目的潜力相比存在较大的差距。然而重要的是,正如标枪得分的广泛分布所示,一些七项全能运动员可以相当擅长投掷标枪。
为了更清楚地说明,接下来我将使用每个项目中的得分分布来衡量运动员在某项目中将表现从 50 百分位(比一半对手表现更好)提升到 60 百分位(比 10 个对手中有 6 个更好)时所获得的点数增益。使用这个度量的目的是,这种提升在每个项目中可能同样困难或同样可实现,因为它是根据其他七项全能运动员的成绩来衡量的。在确定百分位时,为避免跳高项目得分离散性问题(只有每 3 厘米的跳跃是可能的),将每个项目的原始得分建模为分布(大多数情况下为正态分布,跳高、100 米跨栏和 800 米为对数正态分布),并从这些分布中计算百分位。

图 2:各百分位 50 和 60 之间的得分差异,按七项全能项目分列。跑步、跳跃和投掷项目通过颜色区分。图像由作者提供。
结果(图 2)证实了相对于同龄人提升同样的成绩能在田赛项目中获得更多的点数,而不是在跑步项目中。在标枪项目中,相对于竞争对手提高 10%将是最有益的表现提升,能为七项全能运动员带来 28 个点数。稍后我们将讨论,是否在投掷项目上提升 10%的成绩真的像在跑步项目中提升 10%的成绩一样容易。
结果:十项全能

表 2:在世界锦标赛十项全能中的各项目得分。图像由作者提供。
十项全能中的情况大致与七项全能中的描述相似。在表 2中,我们发现,标枪再次表现出最大的四分位差异,接下来是 1500 米(它排在比女性最相似项目 800 米更高的位置)和撑杆跳。相反,短跑项目(100 米、110 米跨栏和 400 米)表现出较小的得分差异。顶级项目与底级项目之间的差异不像七项全能中那样明显。

图 3:按十项全能项目分布的得分(黑线,10–90 百分位绿色区域)、获得的点数(蓝线,纵轴)和世界纪录(红色虚线)。图像由作者提供。
得分分布和点数曲线(图 3)通过箭头的长度显示了典型得分范围(绿色区域的宽度)产生最大得分差异的项目:标枪、撑杆跳,紧随其后的是铁饼和 1500 米。再次,短跑跨栏显示了较小的得分差异:最差的跨栏选手与最好的跨栏选手相比,差距并不大。
1500 米在所有项目中得分最低,第 10 百分位的表现仅值约 600 分。这是因为蓝线的陡峭度决定了运动员每增加一秒远离世界纪录时,所受到的惩罚。蓝线不一定是这样的,但由于选择了系数 a, b, 和 c,它的形状正是如此。好的一面是,蓝线的陡峭度在 1500 米的不同得分之间创造了相对较大的得分差异,如下所示。

图 4:十项全能各项目在第 50 百分位和第 60 百分位之间的得分差异。跑步、跳跃和投掷项目按颜色区分。图片由作者提供。
使用之前相同的技术,将各项目得分建模为分布(1500 米、110 米栏和标枪为对数正态分布,其余为正态分布),并计算百分位,图 4显示了与七项全能项目相同的模式:相同百分比的得分提升在标枪中获得最多积分,在短跑项目中获得最少积分。
项目相关性
上述结果表明,最具技术性的几个项目应该是运动员最容易获得积分的项目。然而,专注于一个项目的训练自然也会促进其他项目的进步(并可能导致其他项目表现的下降),因此不能简单地把每个项目孤立考虑。投入到一个项目的努力的价值将取决于该项目中可能获得的积分 以及 在其他相似项目中获得的互补效益。

图 5:七项全能各项目得分之间的等级相关性,以及各项目得分与比赛中最终名次的逆相关性。图片由作者提供。
图 5的相关性图显示了七项全能各项目得分之间的相关性。相关性最高的是 200 米、100 米栏和跳远。这并不令人惊讶,因为一个优秀的短跑选手很可能在所有这些项目中表现良好。尽管相关性较小,但跳远与跳高之间、铅球与标枪之间也存在一定的相关性。
值得注意的是,标枪与其他项目的整体相关性最小。这与标枪作为一个需要特定技术的项目相一致,七项全能选手在通过提高其他项目成绩时,自然不会在标枪上取得很大进步,除非是(在某种程度上)铅球。标枪甚至与 200 米和 800 米跑之间表现出轻微的负相关性:在七项全能中,标枪投得更好的选手通常是跑得较差的选手。

图 6:十项全能各项目得分之间的等级相关性,以及各项目得分与比赛中最终名次的逆相关性。图片由作者提供。
在十项全能(图 6)中,短跑项目彼此之间有相关性,铅球和铁饼也有相关性,而撑竿跳高、标枪和 1500 米几乎与其他所有项目的相关性都很弱。
这改变了前一部分的结论。虽然在标枪、撑竿跳高或中长跑项目上相对于其他项目的提升应该能带来每单位提升最大的得分增益,但这种提升的好处可能会因为其他项目表现的下降而被削弱。另一方面,在短跑类项目中提升技能往往会同时在相似的项目中带来增益,这也许使得这种方法在整体比赛中更加高效。
最后,每个相关性表格(图 5 和图 6)中的最右列似乎证实了这一点。该列显示了每个项目的得分与七项全能或十项全能比赛最终名次之间的相关性(乘以-1,使得高排名变为最大值)。与名次的最大相关性——也就是运动员的得分最能决定他们在整体比赛中的名次——出现在七项全能和十项全能中的跳远项目,其次是七项全能中的跨栏和跳高,十项全能中的跨栏和 400 米。跳远的重要性可能源于它在比赛中的‘中心性’:它与其他几个项目的相关性相对较高。相反,在十项全能中 1500 米的表现与最终名次的相关性最小。这可能是因为最好的 1500 米选手通常在其他项目上没有显著优势,因此在总成绩中常常排在较低的位置。十项全能通常由强力短跑选手赢得,因为该运动员在 100 米、跨栏、400 米和跳远等项目中多次因为这一技能而获得积分。
结论
关于如何最好地应对七项全能和十项全能的见解,与我在分析开始时的预期有所不同。虽然似乎提升标枪得分是增加总得分的最有效方式,但表现最好的运动员通常在标枪项目上并不会特别突出。这可能是因为标枪所需的技能,以及在较小程度上撑竿跳高、铁饼和跳高所需的技能,不能很好地转移到其他项目上,因此这种努力的好处仅限于该项目的得分回报。
可以推测,这些是最具技术要求的项目,其中运动员可能通过小幅调整技术来‘解锁’较大的表现提升,尽管这很困难;而其他一些项目更多受力量或体能的控制,其中只有渐进的进步是可行的。可能正是技术改进的难度,加上普遍的体能提高所带来的共同效益,使得在短跑相关项目中提升分数成为更具吸引力的选择。
然而,这种平衡是由评分系统控制的。图 1 和图 3 中的每一条蓝色线条都在某一点上由世界纪录作为锚点,但线条的梯度似乎是一个可以选择的参数,可能本来可以做出不同的选择。梯度越大,越强调该项目中最佳和最差运动员之间的分数差异。事实上,标枪和其他项目的较大梯度可能是必要的,以平衡这些较为孤立的项目与短跑项目改进所带来的共同效益。如果图 2 和图 4 中的积分条在所有项目中都相等,那么就会减少人们在技术性项目上投入更多精力的动机,相比之下,短跑项目之间的相关性较高。
目前在近期内,多个赛事的评分系统发生变化的可能性几乎为零。我建议图 5 和图 6 最右侧的列显示了这些系统当前运作良好,因为没有任何赛事与最终名次完全不相关(这意味着这些赛事的表现对总体结果没有影响)。如果要对系统进行调整,可能可以考虑在这一列中实现更均衡的相关性,这意味着特别要提高 800 米和 1500 米的积分梯度,以增加在这些项目中表现良好的优势,代价是减少目前对最终排名影响较大的跳远和跨栏项目的权重。
UniFliXsg:为新加坡大学提供 AI 驱动的本科课程推荐
AI 如何为你推荐专业?
·发表于Towards Data Science ·阅读时长 7 分钟·2024 年 8 月 14 日
--
一周前,我在我的 LinkedIn上分享了我的最新项目 UniFliXsg 的故事,这是一款基于你的兴趣和职业目标,推荐新加坡本科专业的 AI 应用。如果你还没看到,下面的链接也可以尝试。目前,数据库只包含 NUS、NTU、SMU 和 SUTD 的单一专业课程。
[## Uniflixsg — 由 oadultradeepfield 创建的 Hugging Face 空间
发现社区中制作的精彩机器学习应用
huggingface.co](https://huggingface.co/spaces/oadultradeepfield/uniflixsg?source=post_page-----b9b448f7ea19--------------------------------)
如承诺的那样,我将在这篇 Medium 博客中分享我如何从零开始做这件事的工作流程和技术细节。本文的布局与我的思维过程相同,因此你可能会看到步骤之间有些跳跃。此外,本文几乎没有代码。如果你感兴趣,可以在这个GitHub 仓库中查看相关代码。
这是我的第一篇博客,如果我们还不认识,我是 Oad。目前我在 NUS 攻读计算机科学本科,热衷于 AI,特别是大型语言模型(LLM)。这也是我开始这个项目的原因之一,目的是更深入地了解它们的应用 😃
步骤 #1:概述工作流程
一切都从规划开始。我首先想到的是做类似于基于内容的过滤,其中我们可以将用户的资料与项目的信息进行匹配。在查看了项目的样本后,我发现所有项目的信息基本上就是描述和职业前景。因此,我可以采用技术来计算用户与每条信息之间的平均相似度,以便推荐项目。这有点类似于语义搜索,在语义搜索中,我们将用户的查询与数据库中的条目进行匹配。
我还注意到,不同大学之间可能存在文化差异,因此将大学的描述作为另一条信息包含进来,可能有助于提供更个性化的推荐。在这一刻,UniFliXsg 这个名字诞生了,它模仿了 Netflix,这也是以其强大的推荐系统而著称。
在用户端,我会让他们输入个人兴趣和职业目标。为了减少额外的计算,我决定让输入到模型中的用户资料是一个单一查询,即“我对user_input感兴趣。毕业后,我想做user_input的工作。”
我使用了余弦相似度来计算相似度分数。我选择这个度量标准,因为它比其他距离度量能够更好地处理长度不同的向量。因此,我们将首先把每个信息和用户的个人资料转换成向量(文本嵌入),然后计算分数。整体的计划如下面的图所示。

这是这个想法的一个简单示意图。在这里,我将嵌入空间简化为二维空间。余弦相似度就是向量之间角度的余弦值,具体的公式将在后面详细说明。
第二步:数据收集与预处理
这是这个项目中最耗时的部分。每个项目的信息组织方式完全不同。因此,我决定手动收集所有数据,而不使用任何网页抓取技术,因为无论如何这样做所需的时间和精力差不多。
你可以在之前提供的 Hugging Face 仓库中找到数据集。数据以 parquet 文件格式存储,因为我计划在这个项目中使用Polars而不是 Pandas。我以前从未使用过它,但听说它更快,所以我想尝试一下!不过,最初我将数据存储在 Excel 文件中,因为我发现这样操作表格最方便。下图展示了在进行任何嵌入处理之前,数据的原始样貌。

用于收集数据的 Microsoft Excel 表格截图,数据尚未进行嵌入处理。
第三步:文本嵌入与模型选择
为了将数据集中的文本进行嵌入,使用一些来自 Hugging Face 的语言模型可能会很方便,这些模型是预训练的,已经知道词语之间的相似度。我们可以只使用嵌入层,而不是整个模型。通常,我们可以使用 Sentence Transformers 库从大多数模型中访问这些层来进行文本嵌入。我们可以通过以下方式简单地初始化:
from sentence_transformers import SentenceTransformer
# model_name is the name of the model to use
model = SentenceTransformer(model_name)
首先,我们语料库中的每个文本都会被标记化(即拆分为独立的单词)。处理过程将自动与我们选择的模型配置匹配。每个标记(单词)将通过嵌入矩阵嵌入为一个向量。最终,我们将得到一个矩阵,可以通过转换器的层进行处理。输出是一个 n 维向量。
在选择模型时,我们应考虑选择高性能和轻量级模型之间的平衡。通常,句子转换模型比大多数模型更轻量。然而,由于 Hugging Face 的免费空间仅限于使用 CPU,我决定选择一些已知在任务中表现良好的轻量级模型。这使得用户在使用应用程序时能够获得更快的结果。
说实话,我试过几种模型,但我发现最适合这个任务的模型是“all-MiniLM-L6-v2”。我使用这个模型将数据集中的所有相关文本进行嵌入。当用户输入他们的个人资料时,我们只需嵌入个人资料,并根据计算与每个程序的相似度。这有助于通过避免每次运行任务时都嵌入数据集中的文本来节省计算资源。
第四步:计算余弦相似度
余弦相似度将在程序的每个向量(代表不同的信息)与用户的向量之间计算。最终的相似度得分是三项得分的平均值。然而,这种方法有一个复杂的部分。经过一些测试,我发现模型并没有提供非常准确的结果。因此,我决定对每个相似度得分进行不同的加权。我给程序描述和职业前景赋予更多的权重,而给大学描述赋予较少的权重。这本质上是 加权算术平均数,可以最终得到更准确的结果。

这个项目的加权算术平均数的更精确公式。
在获取所有相似度后,我将它们按降序排序,并返回前十个结果。如前所述,所有代码和数据都已上传到我的 GitHub。

使用定义好的函数来计算相似度并返回最匹配的输出。这是后续步骤中创建的应用程序原型。
第五步:创建和部署应用
我通过集成Gradio和 Hugging Face 生态系统构建了这个应用。如果你是新手,Gradio 是一个库,它允许你使用其模板和预构建的元素,直接在 Python 中创建和部署应用原型。它非常方便,因为它促进了更快的用户生产,而且不一定需要我了解太多关于网页开发的知识。尽管我使用了一些 HTML 知识来构建应用,但仅仅是为了居中或缩放元素。下面我提供了一些代码片段,展示 Gradio 在实际使用中的样子。
selected_programs = []
with gr.Blocks(theme=gr.themes.Soft(primary_hue="rose", neutral_hue="slate", font=gr.themes.GoogleFont("Inter"))) as app:
with gr.Column():
gr.HTML(value='<p align="center"> Made with ❤️📚 by <a href="https://www.instagram.com/oadultradeepfield/"> @oadultradeepfield </a> </p>', visible=True)
gr.HTML(value='<h1 align="center">Discover Your Perfect Undergraduate Program in Singapore with AI-Powered Matching!</h1>', visible=True)
gr.HTML(value='<h3 align="center">What Are the Key Word(s) That Best Describe You?</h2>', visible=True)
gr.HTML(value='<p align="center"> P.S. Only single major programs will be shown. 😉 <br> You can also try out instantly with the example keywords. 🚀 </p>', visible=True)
with gr.Row():
with gr.Column():
input_interest = gr.Textbox(placeholder='e.g. Business, Coding, Data Analysis, etc.',
label='I am interested in...', scale=3)
input_career = gr.Textbox(placeholder='e.g. Entrepreneur, Data Scientist, Software Engineer, etc.',
label='Upon graduation, I want to work as...', scale=3)
button = gr.Button("🔍 Search Now!")
button.click(fn=return_search_results, inputs=[input_interest,
input_career],
outputs=selected_programs)
for _ in range(10):
with gr.Row():
selected_programs.append(gr.Image(visible=False))
selected_programs.append(gr.HTML(visible=False))
app.launch()
最后,这个应用已经构建完成,可以通过链接在任何设备上轻松访问我的 Hugging Face 空间!

这是通过桌面访问该应用时的界面。它也是响应式的,你可能会在其他移动设备上看到略有不同的配置。
数据可用性声明
本项目使用的数据集直接来源于各大学本科项目的官方网站。由于这些信息是公开可用的,可以通过标准搜索引擎找到。
致谢
我增加了这个特殊部分,向社区和与我在 LinkedIn 帖子互动的人表示衷心感谢。我甚至收到了 Gradio 的评论,这虽然出乎意料,但非常感激。我还收到了评论和其他平台上的反馈和友善建议。所以,感谢每一位为我的学习做出贡献的人!我将在未来的博客和帖子中分享更多故事,届时见!
除非另有注明,所有图片均为作者提供。
单位圆盘与 2D 有界核密度估计
如何将有界核密度估计扩展到二维情况?让我们探索如何修正单位圆盘周围的边界偏差。
·发表于Towards Data Science ·12 分钟阅读·2024 年 7 月 25 日
--

图片由Leo_Visions提供,来自Unsplash
0. 引言
蒙特卡洛积分
当积分的闭式解不可用时,数值方法变得至关重要。虽然传统的数值积分技术如梯形积分对于低维和平滑积分非常有效,但随着被积函数维度的增加,它们的效率迅速下降,变得明显不可行。
与传统技术不同,蒙特卡洛方法通过利用随机性来评估积分,其收敛速度不依赖于被积函数的维度。它仅依赖于抽取的随机样本数量。
采样
如下方公式所示,蒙特卡洛通过加权平均被积函数在从给定分布中抽取的样本点上的值来估计积分。

因此,蒙特卡洛积分要求能够在任意维度上从任意分布中进行采样。
使用n个样本时,这些方法以O(1/sqrt(n))的速率收敛到正确结果。为了将误差减半,您需要四倍的样本量。因此,优化采样过程以最大化每个样本的效用至关重要。
均匀采样有助于确保被积函数的所有区域都有相同的概率被采样,从而避免提供少量附加信息的紧密间隔样本的冗余。
其他技术,如重要性采样,旨在通过优先采样被积函数的更重要区域来减少方差。
可视化 2D 磁盘采样
书籍 PBRT(基于物理的渲染:从理论到实现) 很好地解释了如何从不同几何体(如磁盘、三角形或半球)中进行采样,以计算描述光散射的积分方程的解。
我对发现的多种磁盘采样方法感到兴奋,并对它们的基本原理充满兴趣。为了更好地理解并直观比较这些 2D 分布,我决定为每种方法绘制一个密度图。
然而,在对磁盘进行核密度估计(KDE)时,会发生边界偏差,因为核函数扩展到磁盘的边界之外,导致边缘附近的密度被低估。
本文旨在提供一种可视化 2D 单位磁盘无偏密度图的方法。
文章大纲
基于我之前的文章 有界核密度估计,在文章中我探讨了在 1D 上解决边界偏差的各种方法,我们将测试以下方法:
-
反射:相对于圆的边缘反射点
-
变换:将有界磁盘映射到无界空间,以执行 KDE
-
加权:裁剪并归一化超出磁盘的核心扩展

图片由 Nathan Shipps 提供,来源于 Unsplash
1. 磁盘周围的边界偏差
边界偏差
正如在引言中讨论的那样,传统的核密度估计(KDE)在处理具有紧凑支持的分布时表现不佳,例如磁盘内的点。
确实,如下图所示,在一个正方形领域中,核的扩展往往会超出边界,人工降低了边缘附近的密度。

核心泄漏超出正方形边界 — 图自作者
传统高斯 KDE
以下代码在单位正方形上按规律间隔采样网格点,仅保留位于磁盘内部的点,然后将其输入到传统高斯 KDE 中。KDE 评估后,磁盘外的预测密度被设为零,以保持边界约束。
最后,通过将密度乘以磁盘面积(π)来归一化密度,确保磁盘内部的预期密度为 1.0。
实际上,输入点并非位于规则的网格上,因此我们需要以显示分辨率对网格进行采样,以评估估计的 KDE。
以下左图是通过运行上述代码得到的。可以观察到圆盘边缘附近的密度明显下降。为了更好地展示边缘附近的下降,我还提取了沿对角线的密度分布图,理想情况下它应该是一个完美的阶跃函数。

左图:基于在单位圆盘内均匀抽取样本的 KDE。右图:对应的 1D 密度分布图沿密度图的对角线方向 — 作者提供的图。

照片由Михаил Секацкий提供,来源于Unsplash
2. 反射技巧
反射
在 1D 中,这个技巧的核心是通过将样本反射到左右边界外来增加样本集。这有助于弥补边界另一侧邻居的缺失。这相当于将局部核函数的尾部反射回来,保持它们在有界区域内。下面的公式用于反射正的 1D 值。
注意,当密度导数在边界处为零时,这种方法效果最佳。

然而,在 2D 中,并没有通用的反射公式;它取决于边界的形状。直观上,反射应该与边界法线对齐。因此,对于圆盘来说,将点沿径向反射是有意义的,这意味着反射只会修改半径。

注意,处理单位正方形的边界反射比处理圆盘要更具挑战性,因为正方形在角落处的边界线不可微。
直观的圆盘反射
直观上,我们可以通过将点对称地反射到边界外来模拟 1D 的情况。一个半径为r的点离边缘的距离为1-r。通过在边界外增加这个距离,我们得到2-r。下面的方程和图示演示了如何利用这种对称性将点反射到单位圆盘的边界外。


使用 f(r)=2-r 公式对单位圆盘边缘对称反射的彩色点。半径 r=1 和 r=2 的圆圈。 — 作者提供的图。
然而,当这种方法应用于校正密度图时,尽管它相较于标准 KDE 有了显著改善,但在边缘附近仍然可以看到轻微的下降。

左图:在单位圆盘内均匀抽取样本的反射 KDE。右图:对应的 1D 密度分布图沿密度图的对角线方向 — 作者提供的图。
优化的圆盘反射
让我们看看如何改进这个反射函数,以更好地适应圆盘边界。与一维情况不同,f(r)=2-r 的反射会扭曲空间,将面积为π的单位圆盘映射到一个面积为 3π的更大圆环上。
理想情况下,我们希望圆盘内每个微分面在反射映射过程中保持不变。如下面的图所示,我们考虑在半径 r 处的点周围的微分变化 dr 和 dθ。

反射前后的微分面(r, dr, dθ) — 图由作者提供

面积守恒导致了反射函数必须满足的微分方程。请注意,负号的出现是因为由于反射性质,函数 f 必然是递减的。

给定边界条件f(1)=1,该微分方程-x=yy'有唯一解。

我们只需要用新的反射公式更新我们的代码。现在,反射点被限制在半径为 1 和√2 之间的圆环内。如我们所见,反射点没有被过度扭曲,并保持相似的局部密度。

使用f(r)=sqrt(2-r²)反射穿过单位圆盘边缘的彩色点。半径为 r=1、r=sqrt(2)和 r=2 的圆圈。 — 图由作者提供
这次,得到的密度估计看起来几乎完美!

左:在单位圆盘内均匀抽样的优化反射 KDE。右:沿密度图对角线的相应一维密度分布 — 图由作者提供

3. 变换技巧
转换空间中的 KDE
变换技巧将有界数据映射到无界空间,在那里可以安全地应用普通的 KDE。这意味着每个输入样本将使用不同的核函数。
然而,正如在上一篇文章有界核密度估计中所看到的,当密度在边界处非零且不趋向于无穷大时,往往会导致不想要的伪影。
变换
基于我们上一节的方法,我们将再次使用中心对称性,并选择一个仅改变半径的变换f。变换后的变量将用波浪符号~表示。

然而,与反射情况不同,在那里我们保持单位圆盘并仅使用变换来添加新点,在这里我们直接变换并使用单位圆盘内的点。
因此,边界条件是不同的,强制要求保持原点不变并将圆盘膨胀到无穷大。

密度变换
当对多维随机变量 U 应用变换 T 时,得到的密度是通过除以 T 的雅可比矩阵行列式的绝对值来计算的。

例如,极坐标变换给我们带来了以下密度。

基于之前的两个性质,我们可以推导出变换前后密度之间的关系。这将使我们能够从变换后的点估算的密度中恢复真实的密度。

该选择哪种变换?对数变换,还是反向变换?
有许多从零开始并在接近 1 时增加到无穷大的函数。没有一种通用的答案。
下图展示了使用对数和反向变换创建的潜在候选函数,用于在 r=-1 和 r=1 处引入奇异性。

下面是一些在原点为零并在接近 +/-1 时趋向于无穷大的函数示例 —— 图由作者提供
基于描述变换后的密度的方程,我们旨在找到一个变换,将均匀分布映射到一个通过普通 KDE 易于估算的形式。如果我们有一个均匀分布 p(x,y),那么在变换后的空间中的密度与下方函数 g 成正比。

对数和反向变换候选给出了以下 g 函数。


当 r 接近零时,它们是等价的,只有当 α 等于一时,它们才会收敛到有意义的值。

下图展示了三种情况,每列对应具有 0.5、1 和 2 的 alpha 值的对数变换。
第一行显示了变换后的空间,比较了沿对角线的密度,蓝色表示在变换后的点上通过 KDE 估算的密度,红色表示原始空间中均匀分布对应的预期密度曲线。第二行显示了这些相同的曲线,但它们已映射回原始空间。
请记住,变换和 KDE 仍然在圆盘上的 2D 空间中执行。下面显示的一维曲线是从 2D 结果中提取的。

变换后和原始域内沿对角线的密度(第一行和第二行)。列对应于具有 alpha 值为 0.5 / 1 / 2 的对数变换 —— 图由作者提供
α<1和α>1在原点附近引入了奇点,完全破坏了内部密度估计。而对于α=1,变换空间中的预期密度在原点处是高度不可微的,呈现出尖顶形状,这种形状无法使用高斯核进行拟合。
此外,尾部密度对噪声非常敏感,可能会在边界附近产生高频伪影。在我看来,这个问题比我们试图解决的原始偏差更具挑战性。
尝试使用另一个核?
为了在α=1时实现更精确的拟合预期的尖形,我使用三角核而非高斯核来估计密度,如下面的代码所示。
尽管拟合稍有改善,但它在原点处仍然高度偏斜。此外,由于需要低带宽来拟合原点处非常陡峭的尖形,边界变得完全不稳定,并在高频率下振荡。

在变换后和原始域中的对角线上的密度(第一行和第二行),使用基于对数的变换和α等于 1 的三角滤波器 — 图由作者提供
尝试使用切线函数?
切线函数也被证明是一个合适的候选者,可以在r=1处引入奇点。


切线函数经过修改,趋向于无穷大,随着半径接近+/-1 — 图由作者提供
幸运的是,它对应的g函数在原点处是可微的,这应该使得拟合变得更容易。
为了保持可读性并避免冗余,我将不包括导致这些结果的数学细节。

然而,正如下面的图所示,我们仍然受到边界附近相同的不稳定性的影响。

在变换后和原始域中的对角线上的密度(第一行和第二行),使用基于切线的变换 — 图由作者提供
结论
该变换方法似乎不适用于我们在二维圆盘内的均匀分布。它在边界附近引入了过多的方差,并显著干扰了原本已经完全无偏的内部。
尽管性能较差,我还是生成了使用对数和切线变换的 Transform KDE 得到的结果 2D 密度图。

左:在单位圆盘内均匀抽样的对数变换 KDE。右:沿密度图对角线的相应一维密度轮廓 — 图由作者提供

左:在单位圆盘内均匀抽样的切线变换 KDE。右:沿密度图对角线的相应一维密度轮廓 — 图由作者提供

Piret Ilver拍摄的照片,来自Unsplash
4. 切割与归一化技巧
加权
由于边界周围的密度由于缺乏邻居而人为降低,我们可以计算出我们局部核在边界外丧失的部分,并利用它来修正偏差。
在一维中,这涉及到计算高斯在某个区间上的积分。这是直接的,因为它可以通过估计区间两端的累积分布函数并相减来完成。
然而,在二维中,这需要计算一个二维高斯在圆盘上的积分。由于没有解析解,必须进行数值近似,从而使计算开销更大。
数值近似
对每个单独预测的密度执行数值积分将会非常昂贵。因为我们本质上是在计算一个二进制圆盘与高斯核之间的卷积,我建议对单位正方形进行离散化,以执行数值卷积。
在下面的代码中,我们假设一个各向同性的高斯分布,并获取核的标准差。随后,我们使用 OpenCV 对二进制圆盘掩码进行卷积,得到下图所示的数组。请注意,它与有偏 vanilla KDE 的接近程度。

对单位圆盘的二进制图像应用高斯模糊。半径为 1 的圆——图示由作者提供
结果
一旦修正的权重图被计算出来,我们可以将其应用于有偏的预测密度。修正后的密度图几乎是完美的。

左侧:在单位圆盘内均匀抽样的切割与归一化 KDE。右侧:对应的沿密度图对角线的 1D 密度轮廓——图示由作者提供

Florian Schmetz拍摄的照片,来自Unsplash
结论
性能
反射和切割与归一化方法非常容易使用,并有效减轻了边界偏差。相比之下,变换方法在均匀的二维圆盘上表现不佳,尽管我们测试了多种特殊函数和核类型。
速度
反射方法转换了 KDE 的输入,而切割与归一化方法转换了其输出。
由于高斯 KDE 的时间复杂度是样本数量的平方,即 O(n²),反射比切割与归一化慢大约四倍,因为它需要两倍于样本数。
因此,Cut-and-Normalize方法似乎是补偿二维均匀圆盘分布边界偏差的最简单和最快捷的方式。
可视化二维圆盘采样
现在我们可以模拟不同的圆盘采样策略,并基于它们的密度图进行比较,而无需担心边界偏差。
希望你喜欢阅读这篇文章,并且它为你提供了更多关于如何在二维情况下执行有界核密度估计的见解。
单位圆盘均匀采样
发现适用于标准[0,1]均匀随机生成器的最佳变换,以均匀采样二维圆盘
·发布于Towards Data Science ·13 分钟阅读·2024 年 9 月 16 日
--

图片来源:Vitolda Klein 通过Unsplash
0. 引言
在我之前的文章单位圆盘和二维有界 KDE中,我讨论了能够采样任意分布的重要性。这对于像蒙特卡罗积分这样的应用尤为重要,蒙特卡罗积分用于解决复杂的积分问题,例如在物理基础渲染 (PBRT)中计算光散射。
与一维采样相比,二维采样引入了新的挑战。本文重点讨论如何均匀地采样二维单位圆盘,并可视化标准[0,1]均匀随机生成器应用变换后所产生的不同分布。
我们还将探讨这些变换虽然产生相同的分布,但如何通过引入畸变影响蒙特卡罗积分,导致方差增加。
1. 如何均匀采样一个圆盘?
引言
随机数生成器通常提供许多预定义的采样分布。然而,对于高度特定的分布,你可能需要创建自己的分布。这涉及到组合和变换基本分布,以实现所需的结果。
例如,要均匀采样a和b之间的区间,可以对标准[0,1]均匀采样应用一个仿射变换。

在本文中,我们将探讨如何通过基于基本的[0,1]均匀采样来均匀地在二维单位圆盘内采样点。
为了提高可读性,我故意在本文中使用了“单位”一词的两种不同语境。“单位方形”指的是[0,1]²区域,反映了基础随机生成器的范围。相对地,“单位圆盘”则是在[-1,1]²内描述的,方便使用极坐标。在实践中,我们可以通过仿射变换轻松地在这两者之间映射。我们将
*u*和*v*表示为从标准[0,1]或[-1,1]均匀分布中抽取的样本。
拒绝采样
使用[0,1]区间的均匀采样两次,可以均匀地采样单位方形区域[0,1]²。
一种非常简单的方法称为“拒绝采样”,其原理是在单位方形区域内进行采样,并拒绝所有落在圆盘外的样本。

单位方形内的圆盘拒绝采样。有效样本(绿色)和无效样本(红色)——图由作者提供
这导致了在单位方形内的圆盘区域内,点遵循均匀的二维分布,如下图所示。
本文中的密度图是通过从指定分布中采样多个点,并应用核密度估计器生成的。解决密度边界偏差的方法详细介绍在上一篇文章“单位圆盘和二维有界 KDE”中。

左:通过核密度估计对 10000 个样本估算的二维圆盘拒绝采样密度。右:对应的沿密度图对角线的 1D 密度轮廓——图由作者提供
一个主要的缺点是,拒绝采样可能需要大量的点才能获得所需数量的有效样本,并且没有总数的上限,导致低效和更高的计算成本。
直观的极坐标采样
一种直观的方法是使用极坐标进行均匀采样:在[0,1]范围内画出半径,并在[0, 2π]范围内画出角度。

半径和角度都是均匀分布的,可能出什么问题呢?然而,这种方法会导致原点处的密度奇点无限大,如下图所示的经验密度图所示。
为了确保线性色图保持可读性,密度图的值被限制在一个任意的最大值 10。没有这个限制,地图将只在蓝色圆盘的中心显示一个红点。

左:通过核密度估计对 10000 个样本估算的二维圆盘极坐标采样密度。右:对应的沿密度图对角线的 1D 密度轮廓——图由作者提供
下图使用颜色展示了如何将采样点(u,v)的单位方形映射到使用上述极坐标变换定义的单位圆盘(x,y)。方形中的有色区域是相等的,但一旦映射到圆盘后,这种相等性就不再成立。这直观地展示了密度图:大半径的密度显著较低,因为它们分布在离原点更远的更宽的环形区域。

左:带有彩色列的单位方形。右:对应的极坐标映射到单位圆盘 — 图由作者提供
让我们来探索数学细节。当对一个多维随机变量 A 应用变换 T 时,得到的密度是通过除以 T 的雅可比矩阵行列式的绝对值来计算的。

极坐标变换由以下方程给出。

我们可以计算它的雅可比矩阵行列式。

因此,极坐标变换导致密度与半径成反比,从而解释了观察到的奇异性。

上面显示的沿对角线的 1D 密度曲线对应于逆函数的绝对值,之后在区间[-1, 1]之外被设为零。乍一看,这似乎有些反直觉,因为 1D 逆函数在[-1, 1]上并不可积!然而,重要的是要记住,积分是在二维空间中使用极坐标进行的,而不是在一维空间中。
均匀极坐标采样 — 微分方程
有两种方法可以找到正确的极坐标变换,从而得到均匀分布。可以通过解微分方程或使用反演方法来实现。让我们来探索这两种方法。
为了解决半径的异质性,我们引入一个函数f来进行调整:r=f(u)。然而,由于圆盘的对称性,角度保持一致。然后,我们可以解这个微分方程,确保相应雅可比矩阵的行列式保持常数,以保持相同的密度。

我们得到ff'=c,在边界条件f(0)=0和f(1)=1下,这有唯一解。最终我们得到了以下变换。

均匀极坐标采样 — 反演方法
在离散情况下,反演方法更容易理解。考虑三个可能的值 A、B 和 C,它们的概率分别为 60%、10%和 30%。如下面的图所示,我们可以将这些概率叠加起来,得到总高度为 100%。通过在 0%到 100%之间均匀地抽取一个百分比 U,并将其映射到堆叠中的对应值 A、B 或 C,我们可以将均匀采样转化为离散的非均匀分布。

离散情况下的反演法。左图:直方图。右图:累积分布函数——图源:作者
在连续情况下,这非常类似。反演法通过对 1D 变量X的概率分布函数(PDF)p进行积分,得到累积分布函数(CDF)P(X<x),该函数从 0 增加到 1。然后,通过从[0,1]区间抽取一个均匀变量U并将其代入反 CDF,我们可以得到一个符合所需分布p的样本x。
假设一个均匀圆盘分布 p(x,y),我们可以推导出相应的极坐标分布 p(r,θ)。

事实证明,该密度 p(r,θ)是可分离的,我们可以使用反演法独立地从它们各自的期望 1D 边缘密度中抽取 r 和θ。

当联合密度不可分时,我们首先从其边缘密度中抽取一个变量,然后根据第一个变量的条件密度抽取第二个变量。
我们将这些边缘密度整合为 CDF。

在[0,1]区间内均匀采样(u,v),并应用反 CDF,可以得到以下变换,这与上面使用微分方程得到的结果相同。

结果分布有效地是均匀的,如下所示的经验密度图所确认。

左图:通过核密度估计在 10000 个样本上估算的 2D 圆盘正确极坐标采样的密度。右图:对应的沿密度图对角线的 1D 密度曲线——图源:作者
平方根函数有效地调整了彩色柱,以在映射到圆盘时保持它们的相对面积。

左图:带有彩色柱的单位正方形。右图:单位圆盘的极坐标映射和均匀极坐标映射——图源:作者

图片由Tim Johnson提供,来源于Unsplash
2. 如何均匀地采样一个圆盘,但减少畸变?
介绍
在诸如基于物理的渲染(参见PBRT (基于物理的渲染:从理论到实现))等应用中,描述光散射的积分方程通过蒙特卡罗积分(MC)方法进行数值求解。
蒙特卡罗(MC)积分通过对样本所评估的算子值进行加权平均来估算积分。对于n个样本,它以O(1/sqrt(n))的速率收敛到正确的结果。为了将误差减半,需要四倍的样本。因此,优化采样过程,使每个样本的利用最大化是至关重要的。
像分层抽样(stratified sampling)这样的技术有助于确保被积函数的所有区域都有相同的采样概率,从而避免了紧密间隔的样本冗余,这些样本几乎不会提供额外的信息。
失真
我们之前讨论的映射是有效的,因为它均匀地对单位圆盘进行了采样。然而,它会在圆盘的某些区域造成失真,特别是在圆盘的外缘附近。单位方形的方形区域可能会映射到圆盘上非常细长和拉伸的区域。

左:带有彩色单元的单位方形。右:对应的均匀极坐标映射到单位圆盘 — 图示由作者提供
这种失真是有问题的,因为在方形上的采样失去了均匀分布的特性。靠近的(u,v)样本可能会导致圆盘上点的间距过大,从而导致方差增加。因此,在蒙特卡罗积分中需要更多样本来抵消这种方差。
在本节中,我们将介绍另一种均匀采样技术,它可以减少失真,从而防止方差不必要的增加。
Shirley 的同心映射
1997 年,彼得·谢尔利(Peter Shirley)发表了“圆盘与方形之间的低失真映射”一文。他没有像我们通过半径和角度那样将均匀的单位方形视为一个没有空间含义的简单参数空间,而是建议将单位方形略微失真成单位圆盘。
这个方法的思想是将单位方形内的同心方形映射到单位圆盘内的同心圆,就像将方形的角拉紧,变圆一样。在下面的图中,与均匀极坐标采样相比,这种方法看起来更自然。

左:带有彩色同心方形的单位方形。右:对应的均匀极坐标和 Shirley 映射到单位圆盘 — 图示由作者提供
单位方形被分为八个三角形,对其中一个三角形定义映射,其他三角形则通过对称性推导出来。

左:单位方形中的参考三角形,带有彩色同心方形。右:对应的 Shirley 映射到单位圆盘 — 图示由作者提供
正如文章开头所提到的,切换[0,1]²和[-1,1]²单位方形的定义比在方程中使用繁琐的仿射变换更为方便。因此,我们将使用[-1,1]²单位方形来与[-1,1]²单位圆盘的定义对齐。
如上图所示,参考三角形由以下内容定义:

将同心正方形的垂直边映射到同心弧段意味着,每个给定横坐标 u 的点都会映射到半径 r=u。接下来,我们需要确定角度映射 f,以使单位圆盘内的分布均匀。

我们可以推导出相应雅可比矩阵的行列式。

对于均匀的圆盘分布,圆盘的第八扇区(对应参考三角形)内的密度如下所示。

另一方面,我们有:

通过将前三个方程与 r=u 结合,我们得到:

将之前的方程与零常数积分,以满足当 v=0 时 θ=0,得到角度映射。最终,我们得到以下映射。

其余七个三角形通过对称性推导得到。最终的分布实际上是均匀的,这一点可以通过下面显示的经验密度图得到验证。

左图:通过核密度估计在 10000 个样本上估计的 2D 圆盘 Shirley 采样密度。右图:对应的沿密度图对角线的 1D 密度轮廓 — 作者图
正如预期的那样,应用于彩色网格的 Shirley 映射与极坐标映射相比,显著减少了单元格的拉伸和畸变。

左图:带有彩色单元格的单位正方形。右图:对应的均匀极坐标和 Shirley 映射到单位圆盘 — 作者图
Shirley 映射的变种
Shirley 方法中的 θ 映射并不直观。因此,正如下图所示,直觉上我们会认为它仅仅是 y 仅依赖于 v 这一更自然约束的结果,从而确保水平线保持水平。

左图:带有彩色单元格的单位正方形参考三角形。右图:对应的 Shirley 映射到单位圆盘 — 作者图
然而,这一直觉是错误的,因为水平线实际上是稍微弯曲的。上面展示的 Shirley 映射,即唯一符合 r=u 限制的均匀映射,强制执行了以下 y 值,随着 u 的变化而微妙地变化。

尽管我们知道它不会导致均匀分布,但为了实验目的,让我们探索一个变种,它使用 y 和 v 之间的线性映射。
这里展示的 Shirley 变种仅用于教学目的,旨在说明 Shirley 映射如何通过弯曲水平线进行改变。
我们通过将v按半对角线的长度进行缩放,获得y,这将正方形的右上角映射到磁盘的边缘。然后,可以通过使用半径约束r=u,从y推导出x的值。

下面的图显示了原始 Shirley 映射与其变体的并排对比。虽然这两幅图几乎相同,但值得注意的是,变体保持了线条的水平性。

左图:单位正方形中的参考三角形,带有彩色单元格。右图:映射到单位磁盘的 Shirley 和 Shirley 变体 —— 图由作者提供
正如预期的那样,经验密度图显示出明显的偏离均匀性,在 0.8 到 1.1 之间波动,而不是维持恒定值 1.0。
请记住,下面显示的密度取决于角度,因此,如果从不同的方向提取,1D 密度轮廓会有所不同。

左图:通过核密度估计法在 10000 个样本上估算的 2D 磁盘 Shirley 变体采样密度。右图:沿密度图对角线的对应 1D 密度轮廓 —— 图由作者提供

3. 采样方法的视觉对比
在图像上应用映射
在本文的第二部分,展示正方形图像如何映射到磁盘有助于理解映射所导致的扭曲。
为了实现这一点,我们可以天真地将每个彩色像素从正方形投影到其对应的磁盘位置。然而,这种方法有缺陷,因为它不能确保磁盘上的每个像素都会被分配颜色。此外,某些磁盘像素可能会接收到多个颜色,从而导致颜色覆盖并产生不准确的结果。
正确的方法是反向操作:遍历磁盘像素,应用逆映射找到正方形中对应的浮动点像素,然后通过插值估算其颜色。这可以通过 OpenCV 的cv2.remap函数完成。
因此,当前的映射公式仅允许我们将磁盘转换为正方形图像。要从正方形映射到磁盘,我们需要反转这些公式。
为了简洁起见,这里不详细讨论逆公式。
以下每个子章节将使用特定的模式图像,展示正方形到磁盘和磁盘到正方形的映射。尽情探索吧!
彩色棋盘格

上图:带有彩色棋盘格的单位正方形。下图:映射到单位磁盘 —— 图由作者提供

顶部:带有彩色棋盘格的单位圆盘。底部:映射到单位正方形 — 图示作者
部分彩色棋盘格

顶部:带有部分彩色棋盘格的单位正方形。底部:映射到单位圆盘 — 图示作者

顶部:带有部分彩色棋盘格的单位圆盘。底部:映射到单位正方形 — 图示作者
同心正方形

顶部:带有同心正方形的单位正方形。底部:映射到单位圆盘 — 图示作者

顶部:带有同心正方形的单位圆盘。底部:映射到单位正方形 — 图示作者
列

顶部:带有彩色列的单位正方形。底部:映射到单位圆盘 — 图示作者

顶部:带有彩色列的单位圆盘。底部:映射到单位正方形 — 图示作者
同心圆

顶部:带有同心圆的单位正方形。底部:映射到单位圆盘 — 图示作者

顶部:带有同心圆的单位圆盘。底部:映射到单位正方形 — 图示作者
象限

顶部:带有象限的单位正方形。底部:映射到单位圆盘 — 图示作者

顶部:带有象限的单位圆盘。底部:映射到单位正方形 — 图示作者
面

顶部:带有作者头像的单位正方形。底部:映射到单位圆盘 — 图示作者

顶部:带有作者头像的单位圆盘。底部:映射到单位正方形 — 图示作者

图片来源:and machines 由 Unsplash 提供
结论
均匀采样
使用均匀极坐标的简单圆盘采样方法是一个很好的示例,帮助理解变量变换如何影响其分布。
通过使用反演方法,你现在可以通过使用其逆累积分布函数(CDF),将[0,1]上的均匀采样转换为任何一维分布。
要对任何二维分布进行采样,我们首先从其边际密度中采样一个变量,然后根据第一个变量的条件密度绘制第二个变量。
变形与方差
根据你的使用场景,你可能希望确保附近的 (u,v) 样本在映射到圆盘时保持接近。在这种情况下,Shirley 变换比均匀极坐标采样更为可取,因为它引入的方差更小。
通用数据供应:了解你的业务
一个行业示例,用以强调理解业务案例的重要性
·发布于Towards Data Science ·9 分钟阅读·2024 年 10 月 22 日
--
正如我在我的经验教训文章中所宣布的,我将以一个来自行业的具体业务案例开始关于实施通用数据供应的系列文章。
你可能会觉得阅读关于业务事务的内容冗长且令人疲惫。你更想直接进入迁移传统数据架构到新方法的详细步骤。但让我尝试说服你,彻底理解你的业务对于任何架构变更至关重要,这才是真正创造业务价值的关键。
你不必立刻爱上你的业务。但我可以向你保证,如果你真正投入到你想为其开发 IT 解决方案的事情中,你的解决方案会更好。
在我多年来参与的所有项目中——包括为银行和保险公司提供激动人心但有时相当抽象的解决方案——以下这个项目最让我感同身受。
所以让我们来看看一个简化的例子,它来自我曾有幸参与的一个真实的医疗项目。这个例子将帮助我们认识到通用数据供应在创建一个能够解决实际业务挑战的架构中的潜力。
这是一个数据技术实际能够拯救生命的例子。对于我们来说,什么能比这更具激励性呢……
摆脱旧思维,构建优秀的 AI 应用
从经典机器学习到适应(或抛弃)生成式 AI 世界的产品策略
·发表于 Towards Data Science ·9 分钟阅读·2024 年 1 月 8 日
--

图片来源:Tome
多年前,我在 Opendoor 的老板给我的第一条建议简洁而深刻:“投资回测。AI 产品团队的成败取决于回测的质量。” 当时,这条建议是经过实践验证的;它通过搜索、推荐、生命科学、金融等多个高风险领域的团队艰难获得的经验。这也是我珍视了近十年的建议。
但我逐渐相信,这并不是构建生成式AI 产品的公理。大约一年前,我从经典机器学习产品(输出简单结果:数字、类别、有序列表)转向了生成式 AI 产品。在这个过程中,我发现许多经典机器学习的原则不再适用于我和我的团队。
通过我在 Tome 的工作(我担任产品负责人),以及与生成式 AI 初创公司领导者的对话,我意识到有三种行为特征,区分了那些推出最强大、最有用的生成式 AI 功能的团队。这些团队:
-
同时从用户问题(向后工作)和技术机会(向前工作)两个方向进行探索
-
从一开始就设计低摩擦的反馈循环
-
重新思考经典机器学习中的研发工具
这些行为需要“摆脱”许多仍然是经典机器学习最佳实践的内容。乍看之下,其中一些可能显得违反直觉。然而,这些原则广泛适用于生成式 AI 应用,从横向到纵向的软件产品,从初创公司到行业巨头。让我们深入探讨吧!
(想知道为什么自动化回测不再是生成式 AI 应用团队的基本原则了吗?该用什么替代它?继续阅读原则 3)
(更关心生成式 AI 应用的 UI/UX 应该如何与经典机器学习产品有所不同,而不是关心过程? 查看这篇博客文章。**)
原则 1:同时从用户问题出发(倒推)和从技术机会出发(正推)
“从用户问题倒推”是许多产品和设计圈中的信条,由亚马逊所闻名。研究用户,找出他们的痛点,编写用户体验需求以缓解最严重的痛点,识别最合适的技术进行实施,然后不断重复。换句话说,弄清楚“这是我们要解决的最重要的问题,然后选择哪个工具来解决它。”
当启用技术发展得非常迅速时,这种方法的意义就不大了。ChatGPT 不是通过从用户痛点倒推来构建的。它之所以成功,是因为它通过一个简单、开放式的用户界面提供了一个强大而全新的启用技术。换句话说:“我们发明了一把新锤子,看看用户会用它敲哪些钉子。”
最优秀的生成式人工智能应用团队会同时进行正推和倒推。他们做用户研究,理解痛点的广度和深度。但他们不仅仅是按顺序处理一个排序列表。团队中的每个人,包括产品经理和设计师,都深度沉浸在最新的人工智能进展中。他们将这些不断发展的技术机会与用户痛点联系起来,这种联系往往比一对一的映射更为复杂。例如,某个团队可能会发现,用户痛点 #2、#3 和 #6 都可以通过模型突破 X 来缓解。那么,下一个项目可能会选择“正推”——通过结合模型突破 X,而不是“倒推”用户痛点 #1。
深度沉浸在近期人工智能的进展中,意味着要理解这些进展如何应用于你的实际应用,而不仅仅是阅读研究论文。这需要原型设计。只有在你的应用环境中亲自尝试新技术,用户收益的估算才不只是猜测。原型设计的重要性提升要求颠覆传统的 规格 → 原型 → 构建 过程,变成 原型 → 规格 → 构建。更多的原型会被舍弃,但这是唯一能够持续精确匹配广泛且深刻的用户需求与有用新技术的方式。
原则 2:从一开始就设计低摩擦反馈回路
系统改进反馈
经典的机器学习产品输出相对简单:数字、类别、排序列表。用户通常会接受或拒绝这些输出:你点击谷歌搜索结果页面上的一个链接,或者将邮件标记为垃圾邮件。每一次用户互动都会生成数据,直接反馈到模型的再训练中,因此实际使用和模型改进之间的联系是强大的(而且是机械的)。
不幸的是,大多数生成式 AI 产品往往不会随着每次用户互动产生新的、真实的训练数据。这个挑战与生成式模型的强大之处息息相关:它们能够生成复杂的产物,这些产物融合了文本、图像、视频、音频、代码等。对于复杂的产物,用户很少会“接受或拒绝”。相反,大多数用户会对模型输出进行精细化调整,无论是通过更多/不同的 AI,还是手动调整。例如,用户可能会将 ChatGPT 的输出复制到 Word 中,编辑后再发送给同事。这种行为使得应用程序(ChatGPT)无法“看到”产物的最终、期望形式。
一个含义是允许用户在你的应用程序中对输出进行迭代。但这并不解决问题:当用户没有对输出进行迭代时,这意味着“哇”还是“哀愁”?你可以为每个 AI 响应添加一个情感指示器(例如,点赞/点踩),但互动级别的反馈响应率往往是非常低的。而提交的反馈通常偏向极端。用户大多将情感收集工作视为额外的障碍,因为它们大多不能帮助用户立即获得更好的输出。
更好的策略是识别用户工作流程中的某一步,标志着“这个输出现在足够好”。将这一步构建到你的应用中,并确保记录此时输出的样子。对于 Tome,我们帮助用户使用 AI 制作演示文稿,关键步骤是将演示文稿分享给另一个人。为了在我们的应用中实现这一点,我们在共享功能上进行了大量投资。然后,我们评估哪些 AI 输出是“可共享的”,哪些则需要大量手动编辑才能共享。
用户协助反馈
自由文本已经成为用户希望与生成式 AI 应用交互的主要方式。但自由文本是一个潘多拉的盒子:给用户自由文本输入 AI,他们会要求产品做一些它无法完成的事情。自由文本是一种 notoriously 难以传达产品限制的输入机制;相比之下,老式的网页表单能清晰地表明可以和必须提交哪些信息,以及必须以什么格式提交。
但是,用户在进行创意或复杂工作时并不想要表单。他们需要自由文本——并且需要关于如何根据当前任务编写优秀提示的指导。帮助用户的策略包括示例提示或模板,关于最优提示长度和格式的指导(他们是否应该包括少量示例?)。人类可读的错误信息也是关键(例如:“该提示使用了 X 语言,但我们仅支持 Y 和 Z 语言。”)
自由文本输入的一个好处是,未被支持的请求可以成为下一个开发灵感的绝佳来源。诀窍在于能够识别和聚类用户在自由文本中试图做的事情。关于这一点将在下一节中详细讨论…
原则三:重新考虑经典机器学习中的研发工具
构建一些,保留一些,舍弃一些
Build: 自然语言分析
许多生成性 AI 应用程序允许用户从同一个入口点执行非常不同的工作流:一个开放式、自由文本的界面。用户并不是从下拉菜单中选择“我在头脑风暴”或“我想解决一个数学问题”——他们期望的工作流隐含在他们的文本输入中。因此,理解用户期望的工作流需要对这些自由文本输入进行切分。一些切分方法可能会长期有效——在 Tome,我们一直关注所需的语言和目标受众类型。也有一些临时的切分方法,用于回答产品路线图上的特定问题——例如,多少个提示请求视觉元素,如图像、视频、表格或图表,因此我们应该在这些视觉元素上进行投资?
自然语言分析应该是对传统研究方法的补充,而不是取而代之。自然语言处理(NLP)与结构化数据(例如传统的 SQL)结合时尤其强大。许多关键数据不是自由文本:用户何时注册、用户的属性(如组织、职位、地理位置等)。在 Tome,我们倾向于根据职位功能、地理位置和自由/付费用户状态来查看语言聚类——所有这些都需要传统的 SQL。
定量分析的洞察永远不应单独依赖于定性分析。我发现,观察用户实时使用我们的产品现场演示,有时能获得是用户访谈(用户事后讨论其产品印象)十倍的洞察力。而且我也遇到过某个优秀的用户访谈,解锁了定量分析十倍的洞察。
Keep: 低代码原型设计工具
两种工具类型能促进高速度、高质量的生成性 AI 应用开发:原型设计工具和输出质量评估工具。
有许多不同的方式可以改进机器学习应用,但有一种既快速又易于接触的策略是提示工程。它之所以快速,是因为它不需要重新训练模型;之所以易于接触,是因为它涉及的是自然语言,而不是代码。允许非工程师在开发环境或本地环境中操作提示工程方法,可以显著提高开发速度和质量。通常,这可以通过笔记本实现。笔记本中可能包含大量代码,但非工程师也可以在不接触代码的情况下,通过迭代自然语言提示取得显著进展。
评估原型输出质量通常相当困难,尤其是在构建全新特性时。与其投资于自动化质量测量,我发现通过“beta 测试程序”向同事或用户征求 10–100 次结构化评估(评分 + 注释)要快得多且更有用。“投票方法”的启用技术可以很轻便:一个生成输入/输出示例的小规模笔记本,并将其传输到 Google 表格中。这使得手动评估能够并行进行,通常在不到一天的时间内,通过少数几个人就能评估大约 100 个示例。评估者的注释可以提供关于失败模式或优秀模式的见解,这也是一个额外的好处;注释通常比数值评分更有助于识别下一步需要修复或构建的内容。
丢弃:自动化、回测的质量测量
经典机器学习工程的一个原则是投资于强健的回测。团队频繁地重新训练经典模型(每周或每天),而一个好的回测确保只有优秀的新候选模型被发布到生产环境中。对于输出数字或类别的模型来说,这种方式是合理的,因为它们可以轻松与真实情况进行比对评分。
但在面对复杂(或许是多模态)输出时,评分准确性变得更难。你可能有一段你认为很好的文本,因此倾向于将其称为“真实情况”,但是如果模型输出与其相差 1 个词,这有意义吗?相差 1 句话呢?如果事实都相同,但结构不同呢?如果是文本和图像的结合呢?
但并非一切都已失去。人类通常能轻松评估生成式 AI 输出是否达到了他们的质量标准。这并不意味着将差的输出转化为好的输出是容易的,只是用户通常能够在几秒钟内判断文本、图像、音频等是否“好”或“不好”。此外,应用层的大多数生成式 AI 系统并不会每天、每周甚至每月进行重新训练,原因是计算成本和/或获取足够的用户信号来证明重训练的长时间周期。因此,我们不需要每天运行的质量评估流程(除非你是 Google、Meta 或 OpenAI)。
考虑到人类能够轻松评估生成式 AI 输出,而且重训练的频率较低,通常情况下,根据内部手动测试(例如上文提到的投票方法)而非自动化回测来评估新模型候选者是更合适的。
开发出色的生成式 AI 应用不仅仅需要创新的工程技术,还需要产品和设计以不同于传统机器学习(ML)或非 ML 产品的方式来协作。有些模式需要被遗忘(“投资于你的回测!”),有些需要重新定义(“从后向前和前向后工作”),还有一些是全新的(“从一开始就为反馈循环构建”)。
这可能需要大量的流程和人员重组,但好处是巨大的——如果你不这么做,革命将会与你擦肩而过。
解锁 Llama3 —— 如何使用最新的大型科技开源 LLM
本文讨论了 Llama3,这是 Meta 发布的最新语言模型,并介绍了如何在本地运行该模型,同时也分享了我对该模型性能的看法。
· 发布于 Towards Data Science · 12 分钟阅读 · 2024 年 4 月 26 日
--
Llama3 是 Meta 的 AI 团队发布的最新模型。根据Meta 关于 Llama3 的博客,Llama3 在 63.2%的情况下在指令性人工评估上超越了 GPT3.5。根据这一衡量标准,Llama3 在典型的 LLM 任务中的表现应当超越 GPT3.5。在本文中,我将向您展示一种简单的方法,帮助您在计算机上运行 Llama3,进行各种测试以了解其能力,并最终分享我对该模型的看法。

3 只代表 Llama 3 的美洲驼。图片来源:ChatGPT。OpenAI。(2024 年)。ChatGPT(4)[大型语言模型]。chat.openai.com
目录
· 目录
· 动机
· 在本地实现该模型
· 测试 Llama3
∘ 测试 Llamas 提供简洁答案的能力
∘ 测试 Llamas 返回正确格式化对象的能力
∘ 测试信息提取能力
∘ 测试 Llama 的编码能力
· 我对该模型的看法
· 结论
发掘概率的力量,预测你业务的未来 🚀
A 实用指南:在实际场景中应用概率概念的 Python 方法
·发表于 Towards Data Science ·阅读时间 13 分钟·2024 年 10 月 21 日
--

介绍
厌倦了对未来的猜测吗? 🤔
你来对地方了!我叫 Sabrine。我是一名应用数学工程师,已有 10 年在人工智能领域的工作经验。在我早期的经历中,我曾努力将概率的力量带入现实世界。管理者喜欢图表、图形和 KPI,但他们从不要求使用概率来回答业务问题。
我一直认为我们在学校学到的所有概率知识都被低估了,或者仅仅在某些机器学习概念中得到了应用。然而,概率可以成为回答业务问题的强大工具。
你不相信我?让我来向你展示🔐。
今天的菜单 🍔
-
🍛 基本概率概念:事件、交集、并集、独立事件、条件概率、贝叶斯定理,著名的正态分布。
-
🥤 实用的 Python 实现:使用 Python 进行实际业务场景的模拟,手把手示范。
-
🍰 行业应用:探索概率模型如何在风险决策中发挥作用……
发掘 SQL 分析窗口函数的威力:深入探讨融合 IPv4 块
如何通过合并连续的网络 IPv4 块来总结地理位置表
·发表于 Towards Data Science ·阅读时长 8 分钟·2024 年 1 月 10 日
--

图片来源:Pascal Bernardon,来自 Unsplash
互联网上的每个设备都有一个唯一的 Internet Protocol(IP)地址。全球的 IP 地址空间由互联网号码分配局(IANA)管理。传统上,IANA 为 IPv4 分配 /8 前缀块,随后这些地址块被分配给互联网服务提供商和其他组织。现有多种数据库将这些 IP 块与其各自的所有者进行映射,并提供有关来源国家和城市的信息。
作为加拿大国家计算机安全事件响应团队(CSIRT),我们,加拿大网络安全中心,在查询给定的 IP 或通过 SQL JOIN 操作增强整个数据集时,通常会依赖这些数据库。然而,并非所有的使用场景都需要精确到城市级别;有时候,仅仅知道国家信息就足够了。
在一个国家内,许多网络块是连续的。将这些块合并成超大块可以显著减少将超大块与国家进行映射的表格大小,从而提升 JOIN 操作的效率。
在本文中,我们将演示如何通过合并连续的网络块来总结地理位置表。
假设我们的地理位置表包含以下数据:
+----------+-------+---------+-----------+-----------+
| start_ip | end_ip| country | city | owner |
+----------+-------+---------+-----------+-----------+
| 1 | 2 | ca | Toronto | Telus |
| 3 | 4 | ca | Quebec | Rogers |
| 5 | 8 | ca | Vancouver | Bell |
| 10 | 14 | ca | Montreal | Telus |
| 19 | 22 | ca | Ottawa | Rogers |
| 23 | 29 | ca | Calgary | Videotron |
+----------+-------+---------+-----------+-----------+
这里,start_ip表示网络块中的最低值,end_ip表示最大值。通常,这些数字要大得多。例如,Google 的 DNS 服务器 8.8.8.8 的表示数字是 134744072。我们使用简单的合成值来进行说明。
首先,让我们进行一个简单的汇总。例如,统计每个国家分配的 IP 地址数量。这可以通过按国家分组数据,并对每个网络块的 IP 数量进行求和来实现。
SELECT
country,
SUM(end_ip - start_ip + 1) as num_ip
FROM
geo_table
GROUP BY
country
这个语句按国家对行进行分组,并应用SUM聚合函数,计算每个国家的 IP 总数。需要注意的是,SUM聚合是结合性的,这意味着求和的顺序不重要,类似于数学中的加法。
+---------+--------+
| country | num_ip |
+---------+--------+
| ca | 24 |
+---------+--------+
现在,让我们深入探讨聚合连续网络块的复杂性。参照我们原始的表格,我们需要将前 3 行合并。区块 1-2、3-4、5-8 应合并为大区块 1-8。我们还需要合并最后两行。区块 19-22 和 23-29 应合并为 19-29。我们的目标是生成如下表格:
+----------+-------+---------+
| start_ip | end_ip| country |
+----------+-------+---------+
| 1 | 8 | ca |
| 10 | 14 | ca |
| 19 | 29 | ca |
+----------+-------+---------+
检测连续区块需要行间信息,并且行的顺序变得至关重要。幸运的是,窗口分析函数提供了解决方案,提供了一个行间引用的机制。这些函数,如LEAD和LAG,使得可以与前后行的值进行比较,从而帮助识别连续的 IP 块。
让我们将LEAD和LAG窗口函数应用到我们的表格中。注意,在OVER子句中,我们仍然指定数据按国家分组PARTITION BY country,同时还指定了数据的排序ORDER BY start_ip。
SELECT
*,
LAG(end_ip) OVER (
PARTITION BY country
ORDER BY start_ip) AS prev_end_ip,
LEAD(start_ip) OVER (
PARTITION BY country
ORDER BY start_ip) AS next_start_ip
FROM
geo_table
结果表格view_1如下:
+----------+-------+---------+-------------+---------------+
| start_ip | end_ip| country | prev_end_ip | next_start_ip |
+----------+-------+---------+-------------+---------------+
| 1 | 2 | ca | null | 3 |
| 3 | 4 | ca | 2 | 5 |
| 5 | 8 | ca | 4 | 10 |
| 10 | 14 | ca | 8 | 19 |
| 19 | 22 | ca | 14 | 23 |
| 23 | 29 | ca | 22 | null |
+----------+-------+---------+-------------+---------------+
区分窗口函数和简单的GROUP BY函数至关重要。在OVER()操作中,LEAD和LAG的结果会附加到每一行,为前后行的信息提供上下文。这与GROUP BY子句中的函数不同,后者将一组行减少为单一的聚合结果。
现在,我们可以访问前后行的详细信息,从而可以进行行间比较。这种比较对于识别连续 IP 块至关重要,帮助我们确定何时将相邻的块合并在一起。
每行可以处于四种状态之一:
-
删除:该区块与前一个和下一个区块都相连。
-
起始:该区块只与下一个区块相连。
-
结束:该区块只与前一个区块相连。
-
保留:该区块既不与前一个区块也不与下一个区块相连。
让我们将这个state列添加到我们的表格中。
SELECT
*,
CASE
WHEN (end_ip = next_start_ip - 1)
AND (start_ip = prev_end_ip + 1) THEN 'remove'
WHEN (end_ip = next_start_ip - 1) THEN 'start'
WHEN (start_ip = prev_end_ip + 1) THEN 'end'
ELSE 'keep'
END AS state
FROM
view_1
我们得到如下的view_2结果:
+----------+-------+---------+-------------+---------------+-------+
| start_ip | end_ip| country | prev_end_ip | next_start_ip | state |
+----------+-------+---------+-------------+---------------+-------+
| 1 | 2 | ca | null | 3 | start |
| 3 | 4 | ca | 2 | 5 | remove|
| 5 | 8 | ca | 4 | 10 | end |
| 10 | 14 | ca | 8 | 19 | keep |
| 19 | 22 | ca | 14 | 23 | start |
| 23 | 29 | ca | 22 | null | end |
+----------+-------+---------+-------------+---------------+-------+
我们可以继续删除位于起始和结束区块之间的行,特别是那些被标记为remove状态的行。
SELECT
start_ip,
end_ip,
country,
state
FROM
view_2
WHERE
state IN ('start', 'end', 'keep')
结果为view_3:
+----------+-------+---------+-------+
| start_ip | end_ip| country | state |
+----------+-------+---------+-------+
| 1 | 2 | ca | start |
| 5 | 8 | ca | end |
| 10 | 14 | ca | keep |
| 19 | 22 | ca | start |
| 23 | 29 | ca | end |
+----------+-------+---------+-------+
我们离目标越来越近了!现在我们只需要合并包含我们希望生成的巨型块的start_ip和end_ip的start行和end行。为了实现这一点,我们再次使用窗口函数。这次是从end行获取end_ip。
SELECT
*,
LEAD(end_ip) OVER (
PARTITION BY country
ORDER BY start_ip) AS next_end_ip
FROM
view_3
在view_4中的结果:
+----------+-------+---------+-------+-------------+
| start_ip | end_ip| country | state | next_end_ip |
+----------+-------+---------+-------+-------------+
| 1 | 2 | ca | start | 8 |
| 5 | 8 | ca | end | 14 |
| 10 | 14 | ca | keep | 22 |
| 19 | 22 | ca | start | 29 |
| 23 | 29 | ca | end | null |
+----------+-------+---------+-------+-------------+
注意,状态为start的行现在具有start_ip和next_end_ip,这是构建巨型块所必需的信息。
状态为end的行不再需要,可以删除。
状态为keep的行已经具有正确的end_ip。
我们现在可以确定巨型块的final_end值。可能有两种情况:
-
对于
start行,我们从next_end_ip获取结束值。 -
对于
keep行,我们直接使用原始的end_ip值。
SELECT
start_ip AS final_start,
CASE
WHEN (state = 'start') THEN next_end_ip
WHEN (state = 'keep') THEN end_ip
ELSE NULL
END AS final_end_ip
FROM
view_4
WHERE
state IN ('start', 'keep')
因此,我们达到了将连续 IPv4 块合并为巨型块的目标。
+----------+-------+---------+-------+-------------+------------+----------+
| start_ip | end_ip| country | state | next_end_ip | final_start| final_end|
+----------+-------+---------+-------+-------------+------------+----------+
| 1 | 2 | ca | start | 8 | 1 | 8 |
| 10 | 14 | ca | keep | 22 | 10 | 14 |
| 19 | 22 | ca | start | 29 | 19 | 29 |
+----------+-------+---------+-------+-------------+------------+----------+
将上述所有语句结合起来,我们得到了一个最终的语句:
SELECT
country,
final_start,
IF(state = 'start', next_end_ip, final_end) AS final_end
FROM (
SELECT
country,
start_ip AS final_start,
end_ip AS final_end,
LEAD(end_ip) OVER (
PARTITION BY country
ORDER BY start_ip) AS next_end_ip
FROM (
SELECT
start_ip,
end_ip,
country,
CASE
WHEN (end_ip = next_start_ip - 1)
AND (start_ip = prev_end_ip + 1) THEN 'remove'
WHEN (end_ip = next_start_ip - 1) THEN 'start'
WHEN (start_ip = prev_end_ip + 1) THEN 'end'
ELSE 'keep'
END AS state
FROM (
SELECT
*,
LAG(end_ip) OVER (
PARTITION BY country
ORDER BY start_ip) AS prev_end_ip,
LEAD(start_ip) OVER (
PARTITION BY country
ORDER BY start_ip) AS next_start_ip
FROM
geo_table
)
WHERE
state IN ('start', 'end', 'keep')
)
)
WHERE
state IN ('start', 'keep')
总结来说,SQL 分析窗口函数为复杂的数据分析提供了一个强大的框架。它们使得用户能够在保留每行上下文的同时执行聚合操作,从而便于完成诸如运行总和、平均值和百分位计算等任务。此外,这些函数在排名、时间序列分析以及检测数据集中的异常值和离群值中也起着至关重要的作用。这些函数是数据工作者工具包中不可或缺的资产。
分析窗口函数非常强大。在本文中,我们仅仅触及了其表面;例如,我们没有使用window_frame。窗口框架允许你进一步细化哪些行将被考虑在聚合中。窗口框架是相对于当前行的,并且可以基于行数或时间间隔,这使得这些函数在各种分析中不可或缺。你可以在 Spark 文档中了解更多关于这些功能的信息:Spark SQL — Window Operations。
释放 Triton 的力量:掌握 Python 中的 GPU 核心优化
加速 AI/ML 模型训练与自定义运算符——第二部分
·发表于 Towards Data Science ·10 分钟阅读·2024 年 8 月 13 日
--

根据希腊神话,海神 Triton 通过吹响他的海螺来控制海水的潮汐和波浪,使海面平静或激起波涛。特别是在一个故事中,Triton 被描述为利用他的力量引导 阿尔戈英雄穿越极其危险的海域。在这篇文章中,我们同样召唤 Triton 来帮助我们导航复杂的旅程,虽然这次我们提到的是 Triton 语言 和编译器,用于编写深度学习(DL)核心代码,并且探索我们在 AI/ML 开发领域的历程。
这是关于通过自定义运算符加速 AI/ML 应用的上一篇文章的续集,在这篇文章中我们展示了通过开发自定义 CUDA 内核实现性能优化的潜力。我们的一个意图是强调自定义内核开发的可访问性以及它为非专业 CUDA 开发者提供的机会。然而,CUDA 开发存在一些挑战,可能对某些人来说难以克服。首先,尽管许多现代 AI/ML 开发者对 Python 非常熟悉,但他们可能不太愿意在 C++ 中开发。此外,要充分利用 GPU 的能力来调整一个 CUDA 内核,需要深入了解底层硬件架构,并且可能需要相当大的工作量。特别是如果你希望你的内核能够在多种 GPU 架构上运行得最佳,这一点尤为重要。复杂性很大程度上源于 CUDA 的“基于线程”的开发模型,在这种模型中,开发者需要负责设计和优化所有 GPU 内核线程的元素,包括与 GPU 内存使用、线程并发、TensorCore 调度等相关的所有细节。
Triton 的力量
Triton 库旨在通过两种主要方式使 GPU 内核开发民主化并简化开发。首先,它提供了一个用于在Python中构建自定义运算符的 API(而不是 C++)。其次,它使得在块级别(而不是线程级别)进行内核开发成为可能,从而抽象化并自动化了与优化 CUDA 线程块内性能相关的所有问题。与其花费大量精力编写线程调用的详细信息,包括与内存管理、片上加速引擎调度、线程同步等相关的复杂性,内核开发者可以依赖 Triton 为他们完成这一切。Triton 编程模型的高层 API 抽象化的一个重要副产品是,它减少了需要为多种不同 GPU 类型和架构调整内核的负担。
当然,正如在提升 API 时通常会遇到的情况一样,Triton 编程模型确实存在一些缺点。有些内核可能会从 CUDA 提供的线程级控制中受益(例如,它们可能会从我们上一篇文章中讨论的条件执行流程中受益)。其他内核可能需要非常专业和精细的处理才能达到最佳性能,并可能受到 Triton 编译器自动生成结果的影响。但即使在这种情况下,当最终可能需要开发一个 CUDA 内核时,能够快速轻松地创建一个临时的 Triton 内核也能极大地促进开发并提高生产力。
若想了解更多关于 Triton 背后的动机以及其编程模型的细节,请参见Triton 公告、官方Triton 文档和原始Triton 白皮书。
免责声明
类似于我们之前的帖子,我们的目的是提供一个简单的示范,展示 Triton 所提供的机会。请不要将这篇文章视为官方Triton 文档或其相关教程的替代。我们将使用与我们之前的帖子相同的面部检测模型作为演示的基础,并在相同的 Google Cloud 环境中进行实验——一台g2-standard-16虚拟机(配备单个 L4 GPU),使用专用深度学习虚拟机镜像和 PyTorch 2.4.0。像之前一样,我们并未优化我们的示例和/或验证其稳定性、持久性或准确性。需要注意的是,虽然我们将在 PyTorch 模型和 NVIDIA GPU 上执行实验,但 Triton 内核开发得到了额外框架和底层硬件的支持。
Triton 作为 Torch 编译的组成部分
在之前的帖子中(例如,这里),我们展示了PyTorch 编译的使用以及它对运行时性能的潜在影响。torch.compiler使用的默认编译器是TorchInductor,它在 GPU 加速中严重依赖 Triton 内核。因此,似乎非常合适,我们通过评估torch.compile提供的自动 Triton 优化来开始我们的 Triton 探索。下面的代码块包括我们在之前的帖子中介绍的面部检测模型的相同前向传递,以及编译后的 GIOU 损失函数。为了简洁起见,我们省略了一些支持代码。有关完整实现,请参见我们的之前帖子。
def loss_with_padding(pred, targets):
mask = (targets[...,3] > 0).to(pred.dtype)
total_boxes = mask.sum()
loss = generalized_box_iou(targets, pred)
masked_loss = loss*mask
loss_sum = masked_loss.sum()
return loss_sum/torch.clamp(total_boxes, 1)
device = torch.device("cuda:0")
model = torch.compile(Net()).to(device).train()
loss_fn = torch.compile(loss_with_padding)
# forward portion of training loop wrapped with profiler object
with torch.profiler.profile(
schedule=torch.profiler.schedule(wait=5, warmup=5, active=10, repeat=1)
) as prof:
for step, data in enumerate(train_loader):
with torch.profiler.record_function('copy data'):
images, boxes = data_to_device(data, device)
torch.cuda.synchronize(device)
with torch.profiler.record_function('forward'):
with torch.autocast(device_type='cuda', dtype=torch.bfloat16):
outputs = model(images)
torch.cuda.synchronize(device)
with torch.profiler.record_function('calc loss'):
loss = loss_fn(outputs, boxes)
torch.cuda.synchronize(device)
prof.step()
if step > 30:
break
# filter and print profiler results
event_list = prof.key_averages()
for i in range(len(event_list) - 1, -1, -1):
if event_list[i].key not in ['forward', 'calc loss', 'copy data']:
del event_list[i]
print(event_list.table())
以下是经过多次运行平均后的性能结果:
------------- ------------ ------------
Name CPU total CPU time avg
------------- ------------ ------------
copy data 56.868ms 5.687ms
forward 1.329s 132.878ms
calc loss 8.282ms 828.159us
------------- ------------ ------------
记得原始损失函数(在填充输入上的)平均时间为 1.844 毫秒。因此,torch 编译带来的性能提升超过了 2 倍(!!)。
torch.compile 自动生成的 Triton 内核实际上可以通过设置 TORCH_LOGS 环境变量查看,正如这个 PyTorch 教程中所解释的那样。事实上,有人提议将这些内核作为 Triton 开发的起点(例如,见这里)。然而,根据我们的经验,这些内核可能有些难以解读。
在下一节中,我们将尝试通过实现一个 GIOU Triton 内核,进一步改进 PyTorch 编译的结果。
创建自定义 Triton 内核
开始你 Triton 开发之旅的一个好地方是官方 Triton 教程。这些教程按复杂度递增的顺序介绍,每一篇都扩展了 Triton 独特特性中的一个或多个。我们的 GIOU Triton 内核最接近最基本的向量加法示例。与我们的CUDA 实现类似,我们将一个块分配给输入批次中的每个样本,并编程使其操作样本中的所有边界框。注意使用了tl.load和tl.store从内存读取和写入数据,以及块程序使用的向量化算术。
import triton
import triton.language as tl
@triton.jit
def giou_kernel(preds_ptr,
targets_ptr,
output_ptr,
valid_ptr,
BLOCK_SIZE: tl.constexpr):
pid = tl.program_id(axis=0)
box_id = tl.arange(0, BLOCK_SIZE)
box_offsets = pid * BLOCK_SIZE + box_id
preds_left = tl.load(preds_ptr + 0 + 4 * box_offsets)
preds_top = tl.load(preds_ptr + 1 + 4 * box_offsets)
preds_right = tl.load(preds_ptr + 2 + 4 * box_offsets)
preds_bottom = tl.load(preds_ptr + 3 + 4 * box_offsets)
gt_left = tl.load(targets_ptr + 0 + 4 * box_offsets)
gt_top = tl.load(targets_ptr + 1 + 4 * box_offsets)
gt_right = tl.load(targets_ptr + 2 + 4 * box_offsets)
gt_bottom = tl.load(targets_ptr + 3 + 4 * box_offsets)
epsilon = 1e-5
# Compute the area of each box
area1 = (preds_right - preds_left) * (preds_bottom - preds_top)
area2 = (gt_right - gt_left) * (gt_bottom - gt_top)
# Compute the intersection
left = tl.maximum(preds_left, gt_left)
top = tl.maximum(preds_top, gt_top)
right = tl.minimum(preds_right, gt_right)
bottom = tl.minimum(preds_bottom, gt_bottom)
inter_w = tl.maximum(right - left, 0)
inter_h = tl.maximum(bottom - top, 0)
inter_area = inter_w * inter_h
union_area = area1 + area2 - inter_area
iou_val = inter_area / tl.maximum(union_area, epsilon)
# Compute the smallest enclosing box
enclose_left = tl.minimum(preds_left, gt_left)
enclose_top = tl.minimum(preds_top, gt_top)
enclose_right = tl.maximum(preds_right, gt_right)
enclose_bottom = tl.maximum(preds_bottom, gt_bottom)
enclose_w = tl.maximum(enclose_right - enclose_left, 0)
enclose_h = tl.maximum(enclose_bottom - enclose_top, 0)
enclose_area = enclose_w * enclose_h
# Compute GIOU
delta_area = (enclose_area - union_area)
enclose_area = tl.maximum(enclose_area, epsilon)
giou = iou_val - delta_area / enclose_area
# Store results
tl.store(output_ptr + (box_offsets),
tl.where(gt_bottom > 0, giou, 0))
tl.store(valid_ptr + (box_offsets), gt_bottom > 0)
def loss_with_triton(pred, targets):
batch_size = pred.shape[0]
n_boxes = pred.shape[1]
# convert to float32 (remove to keep original dtypes)
pred = pred.to(torch.float32)
targets = targets.to(torch.float32)
# allocate output tensors
output = torch.empty_strided(pred.shape[0:2],
stride=(n_boxes,1),
dtype = pred.dtype,
device = pred.device)
valid = torch.empty_strided(pred.shape[0:2],
stride=(n_boxes,1),
dtype = torch.bool,
device = pred.device)
# call Triton kernel
giou_kernel(batch_size,)
total_valid = valid.sum()
loss_sum = output.sum()
return loss_sum/total_valid.clamp(1)
使用我们的 Triton 内核运行的结果如下所示。虽然比我们之前的实验稍差,但这可能是由于 torch.compile 执行了额外的优化。
------------- ------------ ------------
Name CPU total CPU time avg
------------- ------------ ------------
copy data 57.089ms 5.709ms
forward 1.338s 133.771ms
calc loss 8.908ms 890.772us
------------- ------------ ------------
根据 PyTorch 文档中关于使用 Triton 内核的建议,我们进一步评估了我们的内核性能,这次结合了PyTorch 编译。结果(多次运行的平均值)略好于我们第一次实验中的自动编译损失。
------------- ------------ ------------
Name CPU total CPU time avg
------------- ------------ ------------
copy data 57.008ms 5.701ms
forward 1.330s 132.951ms
calc loss 7.189ms 718.869us
------------- ------------ ------------
在开发我们自定义的 GIOU CUDA 内核时,我们注意到将输入张量转换为 float32 的开销,并且需要增强我们的内核,以支持各种输入类型,避免这种转换。在我们的 Triton 内核中,这可以通过简单地去除转换操作来轻松完成。自定义内核将使用原始类型自动生成(JIT 编译)。
------------- ------------ ------------
Name CPU total CPU time avg
------------- ------------ ------------
copy data 57.034ms 5.703ms
forward 1.325s 132.456ms
calc loss 6.219ms 621.950us
------------- ------------ ------------
我们的最终结果与我们在上一篇文章中看到的 CUDA 内核结果相当。
结果
下表总结了我们的实验结果。由于观察到一些变化,结果是多次运行的平均值。我们还包括了我们自定义 CUDA 内核的结果,参见我们的上一篇文章,供参考。请记住,比较结果很可能会根据内核的细节和运行时环境有很大差异。

平均损失运行时摘要(按作者分类)
尽管我们第一次的 Triton 内核实验在性能上较自定义 CUDA 操作符有所下降,但通过应用编译并去除数据类型转换,我们成功地达到了相同的速度。
这些发现与人们对 Triton 的预期一致:一方面,它的高级 API 抽象意味着对低级流程的某种控制丧失,这可能导致运行时性能下降。另一方面,其 API 的(相对)简单性和强大功能使得用户可以通过更容易的实现来弥补性能差距,远比在 CUDA 中实现功能更加轻松。
有人可能会强烈认为我们选择评估的 Triton 内核是文档中所称的“明显并行”,即由逐元素操作组成,因此,作为展示 Triton 价值的示范内核,这是一个糟糕的选择。事实上,可能需要一个更复杂的程序,涉及更复杂的内存管理、调度、同步等,才能展示 Triton 的全部潜力。
下一步
为了完成我们的任务,还需要几个额外步骤。这些步骤包括调优我们自定义的内核并实现反向传播函数。
1. 内核优化
尽管 Triton 抽象化了许多低级内核优化,但仍然存在许多控制项,这些控制项可能极大地影响运行时性能。这些包括每个块的大小、使用的线程束数(如softmax 教程中所示),以及如何访问 L2 内存(有关 swizzling 的示例,参见矩阵乘法教程)。Triton 包括一个自动调优功能,用于优化超参数的选择(如矩阵乘法教程和PyTorch Triton 示例中所示)。尽管我们在示例中省略了自动调优,但它是 Triton 内核开发的一个关键步骤。
2. 反向传播实现
我们将示例限制为仅包含 GIOU 损失函数的前向传播。完整的解决方案还需要为反向传播创建一个内核(如层归一化教程中所示)。这通常比前向传播要复杂一些。有人可能会想,为什么 Triton 所暴露的高级内核开发 API 没有通过支持自动微分来解决这个挑战。事实证明,出于超出本文讨论范围的原因(例如,见这里),自定义内核的自动微分实现非常困难。尽管如此,如果 Triton 能支持这一功能,毫无疑问将会是其一个绝对杀手级的特性,我们只能希望它在未来的某个时候得到支持。
摘要
Triton 无疑是近年来最重要且最具影响力的 AI/ML 库之一。虽然很难评估它在 AI 领域所带来的创新和进展,但它的足迹无处不在——从 PyTorch 2 的核心实现及其依赖关系,到在日渐渗透到我们日常生活中的高级 LLM 模型中的专门注意力层。
Triton 的流行归功于其创新的内核开发编程模型。曾经仅限于 CUDA 专家的领域,Triton 使每个 Python 开发者都能够轻松创建定制的深度学习原语。
在这篇文章中,我们仅仅触及了 Triton 及其功能的表面。务必查看 Triton 的在线文档和其他资源以了解更多。
通过有效的客户细分解锁商业潜力
将您的数据转化为可操作的洞察力,通过客户细分提升互动和盈利能力
·发布于 Towards Data Science ·阅读时长 8 分钟·2024 年 9 月 18 日
--

在上一篇文章中,我们探讨了关键绩效指标(KPI)如何帮助追踪业务增长,并强调了要真正了解您的用户群体,深入了解一个关键的分析技术至关重要:客户细分。细分提供了一种强有力的方式来理解客户群体中的不同群体,并根据它们独特的需求量身定制策略。没有细分,即使是您最好的指标,也可能无法揭示完整的全貌。在本文中,我们将探讨客户细分的各种类型,如何使用先进的工具来实施它们,以及它们在您业务中的实际应用。以下是您将看到的内容:
目录
· 为什么客户细分很重要
· 客户细分的类型
· 实施细分:算法和工具
· 实际应用:让细分发挥作用
· 结论
为什么客户细分很重要
解锁图表中的数据:如何使用 WebPlotDigitizer 数字化图表和图形
从静态图像数据中解锁数字潜力
·发表于 Towards Data Science ·阅读时间 6 分钟 ·2024 年 1 月 14 日
--

从纸质到数字化。图像由作者使用 DALLE 生成。
在数据科学、地球科学或岩石物理学的工作中,我们经常会遇到图像形式的数据或图表,例如出版物中的图表。然而,相关的数据并未附带,这意味着我们可能很难将这些数据用于解释或研究。
这正是像 WebPlotDigitizer 这样的工具非常有用的地方。这个在线工具帮助我们从图像中提取图表,并将其转化为我们可以用于进一步研究和分析的数据。
[## WebPlotDigitizer - 从图表、图像和地图中提取数据
WebPlotDigitizer - 基于网络的工具,用于从图表、图像和地图中提取数值数据。
automeris.io](https://automeris.io/WebPlotDigitizer/?source=post_page-----2dad17e6e073--------------------------------)
在岩石物理学和地球科学领域,有许多方面中数字化图表非常有益,包括:
-
从服务公司图表书中提取并数字化图表
-
从图像中数字化井位测量数据
-
从出版物中数字化图形以便进一步研究
-
从地图中数字化井位
解锁成长:在 Meta 的三年——数据科学家的转型经验
形成我作为数据科学家的关键经验
·发表于 Towards Data Science ·6 分钟阅读·2024 年 5 月 6 日
--

由作者使用 Microsoft Image Creator 生成
如果你不是 Medium 会员,可以在我的 substack上免费阅读完整文章。
在 Meta 的工作节奏非常快,充满挑战,并且具有极大的智力刺激。
在这三年的转型期间,我在繁忙的伦敦办公室担任产品数据科学家,专注于诚信和账户安全。如今,回顾过去,我发现自己不断被在那段时间里获得的宝贵经验所吸引。这些见解不仅仅局限于办公室内,它们已经无缝地融入了我的职业和个人成长中。
经验 1:永远从“为什么”开始
Meta 更注重有意义的贡献,而非单纯的活动。每年两次,员工都会进行严格的评估,评估的标准是他们在过去 6 个月中做出的实际影响力。重点不仅仅在于完成了多少任务或达成了多少目标,更在于行动带来的实际后果。
这意味着,不管你做了什么或做了多少,重要的是你做了什么之后发生了什么。
解锁隐藏的潜力:探索第二轮购买者
使用 LLM 嵌入和机器学习模型寻找客户细分,以实现最佳再营销
·发表于 Towards Data Science ·阅读时间 14 分钟·2024 年 12 月 9 日
--
引言
在本文中,我们讨论了一种方法,用于在二分类数据集中找到具有最大潜力转向目标类别的客户细分。这种方法可以应用于不同的用例,比如在促销活动的第二轮中选择性地定向客户,或者寻找网络中体验较差、但具有最大潜力转向理想类别的节点。
本质上,该方法提供了一种优先选择数据集中特定片段的方式,这些片段能够提供最大价值。

背景
在这个案例中,我们正在研究一个银行数据集。该银行正在通过开展营销活动,积极向潜在客户销售贷款产品。这个数据集是公开的,提供在 Kaggle 上:
个人贷款分类问题
www.kaggle.com](https://www.kaggle.com/datasets/itsmesunil/bank-loan-modelling?source=post_page-----d47958c4d61c--------------------------------)
上述问题的描述如下:
“Thera-Bank 的大多数客户都是存款客户。既是借款人(资产客户)的客户数量相对较少,银行希望迅速扩大这一客户群,以便做更多的贷款业务,同时通过贷款利息赚取更多收入。特别是,管理层希望寻找将负债客户转化为零售贷款客户的方式,同时保持他们作为存款客户。去年银行为存款客户开展的一项活动,成功的转化率超过了 9.6%。这促使零售营销部门制定更具目标性的营销活动,以更少的预算提高成功率。”
上述问题涉及到客户分类,并帮助优先考虑新客户。但是,如果我们能够利用第一轮收集的数据,针对那些第一轮没有购买贷款但在第二轮最有可能购买的客户(前提是他们的至少一个属性或特征发生了变化)呢?理想情况下,这个变化的特征应该是通过人工干预容易改变的,或者是随着时间的推移自然会发生变化的(例如,收入通常会随着时间的推移而增加,或者家庭规模、受教育程度等)。
解决方案
下面是该问题在本示例中的处理概述:

高级流程图
第 1 步-a:加载机器学习模型
在 Kaggle/Github 上有大量的笔记本提供了使用上述数据集进行模型调优的解决方案。我们将从假设模型已经调优并从我们的 MLFlow 库中加载它开始讨论。这是一个 F1 得分为 0.99 且 AUC 为 0.99 的 XGBoost 模型。此时的因变量(y_label)是‘Personal Loan’列。
mlflow server --host 127.0.0.1 --port 8080
import mlflow
mlflow.set_tracking_uri(uri="http://127.0.0.1:8080")
def get_best_model(experiment_name, scoring_metric):
"""
Retrieves the model from MLflow logged models in a given experiment
with the best scoring metric.
Args:
experiment_name (str): Name of the experiment to search.
scoring_metric (str): f1_score is used in this example
Returns:
model_uri: The model path with the best F1 score,
or None if no model or F1 score is found.
artifcat_uri: The path for the artifacts for the best model
"""
experiment = mlflow.get_experiment_by_name(experiment_name)
# Extract the experiment ID
if experiment:
experiment_id = experiment.experiment_id
print(f"Experiment ID for '{experiment_name}': {experiment_id}")
else:
print(f"Experiment '{experiment_name}' not found.")
client = mlflow.tracking.MlflowClient()
# Find runs in the specified experiment
runs = client.search_runs(experiment_ids=experiment_id)
# Initialize variables for tracking
best_run = None
best_score = -float("inf") # Negative infinity for initial comparison
for run in runs:
try:
run_score = float(run.data.metrics.get(scoring_metric, 0)) # Get F1 score from params
if run_score > best_score:
best_run = run
best_score = run_score
Model_Path = best_run.data.tags.get("Model_Type")
except (KeyError): # Skip if score not found or error occurs
pass
# Return the model version from the run with the best F1 score (if found)
if best_run:
model_uri = f"runs:/{best_run.info.run_id}/{Model_Path}"
artifact_uri = f"mlflow-artifacts:/{experiment_id}/{best_run.info.run_id}/artifacts"
print(f"Best Score found for {scoring_metric} for experiment: {experiment_name} is {best_score}")
print(f"Best Model found for {scoring_metric} for experiment: {experiment_name} is {Model_Path}")
return model_uri, artifact_uri
else:
print(f"No model found with logged {scoring_metric} for experiment: {experiment_name}")
return None
Experiment_Name = 'Imbalanced_Bank_Dataset'
best_model_uri, best_artifact_uri = get_best_model(Experiment_Name, "f1_score")
if best_model_uri:
loaded_model = mlflow.sklearn.load_model(best_model_uri)
第 1 步-b:加载数据
接下来,我们将加载数据集。这是用于训练模型的数据集,这意味着所有缺失数据的行或被认为是异常值的行已经从数据集中删除。我们还将计算每个客户购买贷款的概率(由‘Personal Loan’列给出)。然后我们会过滤出那些概率大于 0.5,但没有购买贷款的客户(‘Personal Loan’ = 0)。这些客户应按照预测模型购买贷款,但他们在第一轮没有购买,原因是数据集中的特征未能捕捉到的因素。这些也是模型错误预测的案例,导致准确率和 F1 得分低于 1。

混淆矩阵
在我们开始第二轮活动时,这些客户将成为定向营销方法的基础。
import numpy as np
import pandas as pd
import os
y_label_column = "Personal Loan"
def y_label_encoding (label):
try:
if label == 1:
return 1
elif label == 0:
return 0
elif label == 'Yes':
return 1
elif label == 'No':
return 0
else:
print(f"Invalid label: {label}. Only 'Yes/1' or 'No/0' are allowed.")
except:
print('Exception Raised')
def df_splitting(df):
prediction_columns = ['Age', 'Experience', 'Income', 'ZIP Code', 'Family', 'CCAvg',\
'Education', 'Mortgage', 'Personal Loan', 'Securities Account',\
'CD Account', 'Online', 'CreditCard']
y_test = df[y_label_column].apply(y_label_encoding)
X_test = df[prediction_columns].drop(columns=y_label_column)
return X_test, y_test
"""
load_prediction_data function should refer to the final dataset used for training. The function is not provided here
"""
df_pred = load_prediction_data (best_artifact_uri) ##loads dataset into a dataframe
df_pred['Probability'] = [x[1] for x in loaded_model.predict_proba(df_splitting(df_pred)[0])]
df_pred = df_pred.sort_values(by='Probability', ascending=False)
df_potential_cust = df_pred[(df_pred[y_label_column]==0) & (df_pred['Probability']> 0.5)]
print(f'Total customers: {df_pred.shape[0]}')
df_pred = df_pred[~((df_pred[y_label_column]==0) & (df_pred['Probability']> 0.5))]
print(f'Remaining customers: {df_pred.shape[0]}')
df_potential_cust
我们看到只有 4 个这样的案例被添加到潜在客户表中,并从主数据集中删除。

步骤-2:生成 SHAP 值
我们现在将生成 Shapley 值,以确定特征的局部重要性,并提取关键特征,即能够将客户从不希望的类别(‘个人贷款’ = 0)移动到希望的类别(‘个人贷款’ = 1)的特征。关于 Shapley 值的详细信息可以在此找到:
[## 介绍使用 Shapley 值的可解释 AI - SHAP 最新文档]
这是一个介绍如何通过 Shapley 值解释机器学习模型的内容。Shapley 值是一种广泛使用的……
shap.readthedocs.io](https://shap.readthedocs.io/en/latest/example_notebooks/overviews/An introduction to explainable AI with Shapley values.html?source=post_page-----d47958c4d61c--------------------------------)
我们还将查看一些重要特征,以了解它们与因变量(‘个人贷款’)的相关性。我们为此目的挑选的三个特征是‘收入’,‘家庭’(家庭规模)和‘教育’。正如我们稍后将看到的,这些是我们希望集中关注的特征,以便改变概率。
import shap
explainer = shap.Explainer(loaded_model, df_pred)
Shap_explainer = explainer(df_pred)
shap.plots.scatter(Shap_explainer[:, "Income"], color=Shap_explainer[:, "Personal Loan"])

个人贷款购买量随收入增加
shap.plots.scatter(Shap_explainer[:, "Family"], color=Shap_explainer[:,'Personal Loan'])

个人贷款购买量随家庭规模增加
shap.plots.scatter(Shap_explainer[:, "Education"], color=Shap_explainer[:,'Personal Loan'])

个人贷款购买量随教育水平增加
我们可以看到,对于所有 3 个特征,当特征值增加时,个人贷款的购买量也随之增加,Shap 值随着特征值的增加而大于 0,表明这些特征对购买倾向有正面影响。
我们现在将为每个客户存储 shap 值到一个数据框中,这样我们就可以在后续处理中访问最重要的局部特征。
X_test = df_splitting(df_pred)[0] ## Keeping only the columns used for prediction
explainer = shap.Explainer(loaded_model.predict, X_test)
Shap_explainer = explainer(X_test)
df_Shap_values = pd.DataFrame(Shap_explainer.values, columns=X_test.columns)
df_Shap_values.to_csv('Credit_Card_Fraud_Shap_Values.csv', index=False)
步骤-3:创建向量嵌入:
下一步,我们将使用 LLM 模型为数据集创建向量嵌入。这样做的主要目的是能够进行向量相似度搜索。我们打算在数据集中找到那些没有购买贷款的客户,并找出与那些购买了贷款的客户最相似的客户。然后我们将选择最相似的客户,查看在改变这些客户最重要特征的值后,概率如何变化。
创建 LLM 向量嵌入的过程涉及多个步骤,本文中没有详细描述。如果想更好地理解这些过程,我建议阅读 Damian Gill 撰写的以下文章:
使用大型语言模型(LLM)解锁先进的客户细分技术,并通过先进的技术改善您的聚类模型…
towardsdatascience.com
在我们的案例中,我们使用的是 Hugging Face 上可用的句子变换器 SBERT 模型。以下是该模型的详细信息:
[## sentence-transformers(句子变换器)
以下是经过调整的用于句子/文本嵌入生成的模型。它们可以与…
huggingface.co](https://huggingface.co/sentence-transformers?source=post_page-----d47958c4d61c--------------------------------)
为了获得更好的向量嵌入,我们希望尽可能多地提供关于数据的文字描述。对于银行数据集,关于每一列的详细描述已在 Excel 文件‘Bank_Personal_Loan_Modelling.xlsx’的‘Description’表中提供。我们使用此描述作为列名。此外,我们将数值转换为稍微更具描述性的内容,而不仅仅是数字。例如,我们将列名‘Family’替换为‘客户的家庭规模’,并将该列中的值从整数(如2)转换为字符串(如‘2 人’)。以下是进行这些转换后的数据集示例:
def Get_Highest_SHAP_Values (row, no_of_values = 1):
if row.sum() < 0:
top_values = row.nsmallest(no_of_values)
else:
top_values = row.nlargest(no_of_values)
return [f"{col}: {val}" for col, val in zip(top_values.index, top_values)]
def read_orig_data_categorized(categorized_filename, shap_filename = ''):
df = pd.read_csv(categorized_filename)
if shap_filename!= '':
df_shap = pd.read_csv(shap_filename)
df['Most Important Features'] = df_shap.apply(lambda row: Get_Highest_SHAP_Values(row, no_of_values = 1), axis=1)
return df
def Column_name_changes (column_description, df):
df_description = pd.read_excel(column_description, sheet_name='Description',skiprows=6, usecols=[1,2])
df_description.replace('#','No of ', inplace=True, regex=True)
df_description.replace('\(\$000\)','', inplace=True, regex=True)
df_description.loc[df_description['Unnamed: 1']=='Education','Unnamed: 2'] = 'Education Level'
mapping_dict = dict(zip(df_description['Unnamed: 1'], df_description['Unnamed: 2']))
df = df.rename(columns=mapping_dict)
return df
Original_Categorized_Dataset = r'Bank_Personal_Loan_Modelling_Semantic.csv' ## Dataset with more description of the values sorted in the same way as df_pred and df_Shap_values
Shap_values_Dataset = r'Credit_Card_Fraud_Shap_Values.csv' ## Shap values dataset
column_description = r'Bank_Personal_Loan_Modelling.xlsx' ## Original Bank Loan dataset with the Description Sheet
df_main = read_orig_data_categorized(Original_Categorized_Dataset, Shap_values_Dataset)
df_main = df_main.drop(columns=['ID','ZIP Code'])
df_main = Column_name_changes(column_description, df_main)
df_main.sample(5)

我们将创建两个独立的数据集——一个是购买了贷款的客户,另一个是没有购买的客户。
y_label_column = 'Did this customer accept the personal loan offered in the last campaign?'
df_main_true_cases = df_main[df_main[y_label_column]=="Yes"].reset_index(drop=True)
df_main_false_cases = df_main[df_main[y_label_column]=="No"].reset_index(drop=True)
我们将为这两种情况创建向量嵌入。在将数据集传递给句子变换器之前,以下是银行客户数据集每一行的样子:

一个客户输入示例,用于句子变换器
from sentence_transformers import SentenceTransformer
def df_to_text(row):
text = ''
for col in row.index:
text += f"""{col}: {row[col]},"""
return text
def generating_embeddings(df):
sentences = df.apply(lambda row: df_to_text(row), axis=1).tolist()
model = SentenceTransformer(r"sentence-transformers/paraphrase-MiniLM-L6-v2")
output = model.encode(sentences=sentences,
show_progress_bar=True,
normalize_embeddings=True)
df_embeddings = pd.DataFrame(output)
return df_embeddings
df_embedding_all = generating_embeddings(df_main)
df_embedding_false_cases = generating_embeddings(df_main_false_cases)
df_embedding_true_cases = generating_embeddings(df_main_true_cases)
第 4 步+5 步:进行向量搜索
接下来,我们将使用欧几里得距离 L2 进行近似最近邻相似性搜索,并使用 Facebook AI 相似性搜索(FAISS),为这些向量数据集创建 FAISS 索引。我们的目标是寻找‘Personal Loan = 0’数据集中与‘Personal Loan = 1’数据集中最相似的客户。基本上,我们在寻找那些没有购买贷款但与购买贷款的客户最相似的客户。在这种情况下,我们通过设置 k=1(一个近似最近邻)为每个‘true’客户搜索一个‘false’客户,并根据距离对结果进行排序。
有关 FAISS 相似性搜索的详细信息,请参见此处:
[## GitHub - facebookresearch/faiss:一个用于高效相似性搜索和稠密向量聚类的库…
一个用于高效相似性搜索和稠密向量聚类的库。 - facebookresearch/faiss
github.com](https://github.com/facebookresearch/faiss?source=post_page-----d47958c4d61c--------------------------------)
这里有另一篇文章解释了如何使用 L2 与 FAISS:
在 Loopio,我们使用 Facebook AI 相似性搜索(FAISS)高效地搜索相似文本。找到与…
import faiss
def generating_index(df_embeddings):
vector_dimension = df_embeddings.shape[1]
index = faiss.IndexFlatL2(vector_dimension)
faiss.normalize_L2(df_embeddings.values)
index.add(df_embeddings.values)
return index
def vector_search(index, df_search, df_original, k=1):
sentences = df_search.apply(lambda row: df_to_text(row), axis=1).tolist()
model = SentenceTransformer(r"sentence-transformers/paraphrase-MiniLM-L6-v2")
output = model.encode(sentences=sentences,
show_progress_bar=False,
normalize_embeddings=True)
search_vector = output
faiss.normalize_L2(search_vector)
distances, ann = index.search(search_vector, k=k)
results = pd.DataFrame({'distances': distances[0], 'ann': ann[0]})
df_results = pd.merge(results, df_original, left_on='ann', right_index= True)
return df_results
def cluster_search(index, df_search, df_original, k=1):
df_temp = pd.DataFrame()
for i in range(0,len(df_search)):
df_row_search = df_search.iloc[i:i+1].values
df_temp = pd.concat([df_temp,vector_search_with_embeddings(df_row_search, df_original, index, k=k)])
df_temp = df_temp.sort_values(by='distances')
return df_temp
def vector_search_with_embeddings(search_vector, df_original, index, k=1):
faiss.normalize_L2(search_vector)
distances, ann = index.search(search_vector, k=k)
results = pd.DataFrame({'distances': distances[0], 'ann': ann[0]})
df_results = pd.merge(results, df_original, left_on='ann', right_index= True)
return df_results
index_all = generating_index(df_embedding_all)
index_false_cases = generating_index(df_embedding_false_cases)
index_true_cases = generating_index(df_embedding_true_cases)
df_results = cluster_search(index_false_cases, df_embedding_true_cases, df_main_false_cases, k=1)
df_results['Most Important Features'] = [x[0] for x in df_results['Most Important Features'].values]
df_results ['Tipping_Feature'] = [x[0] for x in df_results['Most Important Features'].str.split(':')]
df_results = df_results.drop_duplicates(subset=['ann'])
df_results.head(10)
这将给我们列出与已购买贷款的客户最相似且最有可能在第二轮购买的客户,前提是阻碍他们第一次购买的最重要特征稍微发生变化。现在可以优先处理这份客户名单。

Step-6:与其他方法的比较
在这一点上,我们希望评估上述方法是否值得花费时间,以及是否存在另一种更高效的方式来提取相同的信息?例如,我们可以考虑将‘False’客户中具有最高概率的客户视为最有可能进行第二轮购买的客户。将这样一份名单与上述名单进行比较,可以帮助我们判断是否有更快速的方式来得出结论。
为此,我们只需加载我们之前创建的概率数据集,然后选择概率最高的前 10 个‘False’客户。
df_trial_customers = df_pred[df_pred['Personal Loan']==0].iloc[0:10]
df_trial_customers

这个名单与我们第一个名单相比有多有效?我们如何衡量这一点?为此,我们希望将名单的有效性定义为能够将客户轻微调整后转为目标类别的百分比,方法是通过对最重要特征进行轻微变化后重新计算概率值。对于我们的分析,我们将仅关注教育和家庭这两个特征——这些特征更有可能随着时间的推移发生变化。尽管收入也可以纳入此类别,但为了简化分析,我们暂时不考虑收入。我们将从两个名单中筛选出前 10 个客户,这些客户将作为 Tipping_Feature。
这将给我们提供以下两份名单:
-
List_A:这是我们使用相似性搜索方法得到的名单
-
List_B:这是我们通过按概率对 False 案例进行排序得到的名单
features_list = ['Education', 'Family']
features_list = ('|').join(features_list)
df_list_A_Sim_Search = df_results[df_results['Tipping_Feature'].str.contains(features_list, case=False)].head(10)
df_list_A_Sim_Search

我们将把 A 列表转换回原始格式,然后可以被 ML 模型用来计算概率。这需要参考原始的 df_pred 数据集,下面是可以用来实现这一目的的函数。
def main_index_search(results_df, df_given_embeddings, df_original, search_index):
df_temp = pd.DataFrame()
for i in range(0,len(results_df)):
index_number = results_df['ann'].iloc[i]
df_row_search = df_given_embeddings.iloc[index_number:index_number+1].values
df_temp = pd.concat([df_temp,vector_search_with_embeddings(df_row_search, df_original, search_index, k=1)])
return df_temp
df_list_A_Sim_Search_pred = pd.concat([(main_index_search(df_list_A_Sim_Search, df_embedding_false_cases, df_pred, index_all).drop(columns=['distances','ann'])),\
df_list_A_Sim_Search ['Tipping_Feature']], axis=1).reset_index(drop=True)
df_list_A_Sim_Search_pred

潜在候选者 A 列表:通过相似度搜索方法提取
以下是通过在原始 df_pred 数据框中应用所需过滤器来获取 B 列表的方法。
df_list_B_Probabilities = df_pred.copy().reset_index(drop=True)
df_list_B_Probabilities['Tipping_Feature'] = df_Shap_values.apply(lambda row: Get_Highest_SHAP_Values(row, no_of_values = 1), axis=1)
df_list_B_Probabilities['Tipping_Feature'] = [x[0] for x in df_list_B_Probabilities['Tipping_Feature'].values]
df_list_B_Probabilities ['Tipping_Feature'] = [x[0] for x in df_list_B_Probabilities['Tipping_Feature'].str.split(':')]
df_list_B_Probabilities = df_list_B_Probabilities[(df_list_B_Probabilities['Personal Loan']==0) & \
(df_list_B_Probabilities['Tipping_Feature'].str.contains(features_list, case=False))].head(10)
df_list_B_Probabilities

潜在候选者 B 列表:通过对第一次计算贷款购买概率进行排序提取
为了进行评估,我创建了一个函数,根据该客户的 Tipping_Feature 对 Family 或 Education 的值进行网格搜索,从最小值(即当前值)到最大值(即在整个数据集中该特征的最大值),直到概率超过 0.5。
def finding_max(df):
all_max_values = pd.DataFrame(df.max()).T
return all_max_values
def finding_min(df):
all_min_values = pd.DataFrame(df.min()).T
return all_min_values
def grid_search(row, min_value, max_value, increment, tipping_feature):
row[tipping_feature] = min_value
row['New_Probability'] = [x[1] for x in loaded_model.predict_proba(row_splitting(row).convert_dtypes())][0]
while (row['New_Probability']) < 0.5:
if row[tipping_feature] == max_value:
row['Tipping_Value'] = 'Max Value Reached'
break
else:
row[tipping_feature] = row[tipping_feature] + increment
row['Tipping_Value'] = row[tipping_feature]
row['New_Probability'] = [x[1] for x in loaded_model.predict_proba(row_splitting(row).convert_dtypes())][0]
return row
def row_splitting(row):
prediction_columns = ['Age', 'Experience', 'Income', 'ZIP Code', 'Family', 'CCAvg',\
'Education', 'Mortgage', 'Personal Loan', 'Securities Account',\
'CD Account', 'Online', 'CreditCard']
X_test = row.to_frame().transpose()
X_test = X_test[prediction_columns].reset_index(drop=True)
X_test = X_test.drop(columns=y_label_column)
return X_test
def tipping_value(row, all_max_values, all_min_values):
tipping_feature = row['Tipping_Feature']
min_value = row[tipping_feature]
max_value = all_max_values[tipping_feature].values[0]
if tipping_feature == 'CCAvg':
increment = 0.2
else:
increment = 1
row = grid_search(row, min_value, max_value, increment, tipping_feature)
row ['Value_Difference'] = row[tipping_feature] - min_value
row ['Original_Value'] = min_value
return row
min_values = finding_min(df_pred)
max_values = finding_max(df_pred)
df_new_prob = df_list_B_Probabilities.apply(lambda row: tipping_value(row, max_values, min_values), axis=1)
df_new_prob

改变 Tipping_Feature 值后 B 列表的概率
我们看到,在使用概率得到的 B 列表中,有一个候选者在改变 Tipping_Feature 值后无法进入期望的类别。与此同时,有 4 个候选者(以红色突出显示)在改变 Tipping_Feature 后显示出非常高的贷款购买概率。
我们再次对 A 列表中的候选者执行相同的操作。
df_new_prob = df_list_A_Sim_Search_pred.apply(lambda row: tipping_value(row, max_values, min_values), axis=1)
df_new_prob

改变 Tipping_Feature 值后 A 列表的概率
对于 A 列表,我们看到虽然有一个候选者未能进入期望的类别,但有 6 个候选者(以红色突出显示),在改变 Tipping_Feature 值后显示出非常高的购买贷款概率。我们还可以看到,这些候选者原本的贷款购买概率非常低,如果没有使用相似度搜索,这些潜在的候选者将会被忽略。
结论
尽管可以采用其他方法来搜索潜在候选者,但使用 LLM 向量嵌入进行的相似度搜索可以突出那些否则很可能不会被优先考虑的候选者。这种方法可以有多种用途,在本案例中,它与使用 XGBoost 模型计算出的概率结合使用。
除非另有说明,所有图片均来自作者。
解锁洞察:构建一个使用逻辑回归的评分卡
想要一张信用卡?一份保险单?你是否曾想过那个塑造这些决策的三位数字号码是什么?
·发表于 Towards Data Science ·9 分钟阅读·2024 年 2 月 15 日
--
简介
许多行业都使用评分卡来做出决策。金融机构和保险公司使用评分卡来决定某人是否适合信用或保险。某些国家甚至使用社会评分来评估个人的可信度并判断他们的行为。
例如,在使用评分卡做出自动决策之前,客户会走进银行,与银行工作人员讨论他们希望借款的金额以及为何需要贷款。银行员工可能会将他们自己的思维方式和偏见融入决策过程中。这个人来自哪里?他们穿什么?甚至,今天我感觉如何?
评分卡使竞争环境平等,并允许每个人在相同的基础上进行评估。

由 DeepAI 图像生成器生成
最近,我参加了几个Kaggle 竞赛,并分析了特色数据集。2024 年的首场比赛旨在确定客户离开银行的可能性。这是一个常见的任务,对市场营销部门非常有用。为了这场比赛,我决定放下通常在这些任务中使用的基于树和集成建模技术,回归基础:使用逻辑回归。
在这里,我将引导你了解逻辑回归模型的开发过程,将其转化为分数,并将其呈现为记分卡。这样做的目的是展示如何揭示数据及其与二元目标之间的关系。这种模型的优势在于它更简单、更容易解释,甚至对非技术观众也能轻松理解。
我的 Kaggle 笔记本,包含所有的代码和数学公式,可以在这里找到。这篇文章将重点介绍亮点。
什么是分数?
我们在这里描述的分数是基于逻辑回归模型的。该模型为我们的输入特征分配权重,并输出一个概率,我们可以通过校准步骤将其转换为分数。一旦获得分数,我们就可以通过记分卡来表示:展示个人根据可用数据的得分。
让我们通过一个简单的例子来了解一下。
X 先生走进银行,寻找一个新业务的贷款。银行使用一个简单的基于收入和年龄的分数来决定是否批准该个人。
X 先生是一个收入相对较低的年轻人。他因为年龄受到惩罚,但在收入范围内得分较高(第二高)。总的来说,他在这张记分卡上得了 24 分,这是一个中等分数(最高分为 52 分)。
银行通常会使用一个分数截断线,来根据内部政策决定需要多少分才能被接受。这个分数是基于逻辑回归模型的,逻辑回归是基于一些二元定义,通过一组特征来预测对数几率。
对于银行而言,逻辑回归可能用于预测那些错过还款的人。对于保险公司,可能是预测曾经提出过索赔的人。对于社交分数,可能是预测那些曾经参加过无政府主义集会的人(虽然我不太确定这些分数具体预测什么,但我会很感兴趣想了解!)。
我们不会逐一讲解完整模型开发所需的所有步骤,但将探讨一些关键步骤:
-
证据权重转换:通过像 X 先生的例子一样将我们的连续特征离散化,进行分组。
-
校准我们的逻辑回归输出以生成分数:将我们的概率转换为更易于用户理解的数字,即转换为分数。
-
将我们的分数表示为记分卡:展示每个特征如何影响最终分数。
证据权重转换
在“X 先生”的例子中,我们看到模型有两个基于数值的特征:X 先生的年龄和收入。这些变量被分组,以便更容易理解模型及其驱动个人评分的因素。直接使用这些连续变量(与分组相比)可能意味着对于数值上的微小差异,评分会有显著不同。在信用或保险风险的背景下,这会使得决策更难以辩解和解释。
有多种方法可以进行分组,但通常会先采取一种初步的自动化方法,然后手动微调分组以使其具有定性意义。在这里,我将每个连续特征单独输入决策树,以获得初步的分组集合。
一旦分组确定,我便计算了每个分组的证据权重。该公式如下所示:
证据权重(WoE)的公式。分布可以反转,以改变特征之间的关系。
这是一种在信用评分模型中常用的转换技术,通常使用逻辑回归,因为它与对数几率之间有线性关系,而逻辑回归旨在预测的正是这一点。我不会在这里深入数学内容,因为这些内容在我的Kaggle 笔记本中已详细覆盖。
一旦我们得到了每个分组特征的证据权重,就可以可视化这个趋势。对于银行客户流失预测的 Kaggle 数据,我包含了几个特征来说明这些转换。

图片由作者提供
每个证据权重周围的红色条表示 95%的置信区间,这意味着我们有 95%的把握认为证据权重会落在这个范围内。较窄的区间通常与拥有足够样本量且可以对证据权重保持信心的稳健分组相关。
例如,分组余额的 16 和 22 类的客户离开银行的数量较少(分别为 19 和 53 个案例),并且它们的置信区间最宽。
这些模式揭示了特征之间的关系以及客户离开银行的概率。年龄特征稍微更容易理解,因此我们先从这个开始。
随着客户年龄的增长,他们更有可能离开银行。
这个趋势相当清晰,并且大部分是单调的,除了某些组别,比如 25-34 岁的个体比 18-24 岁的个体更不可能离开银行。除非有强有力的理由来支持这种情况(此时领域知识发挥作用!),我们可能会考虑将这两个类别进行合并,以确保单调趋势。
单调趋势在做出授予信用或保险政策的决策时非常重要,因为这通常是监管要求,以确保模型不仅准确,而且可以解释。
这引出了余额特征。模式并不明确,我们在这里没有真正的依据。确实,余额较低的客户离开银行的可能性较小,但你需要将多个组进行分段才能使这一趋势有意义。
通过将类别 2–9、13–21 分组,并将 22 单独分为一组(分别进入 1、2 和 3 组),我们可以开始看到趋势。然而,这样做的缺点是会丧失特征的细粒度,可能会影响下游模型的表现。

作者提供的图片
对于 Kaggle 竞赛,我的模型不需要可解释性,因此我没有重新分组任何特征,只是专注于基于我应用的自动分组生成最具预测性的得分。在行业应用中,我可能会三思而后行。
值得注意的是,我们的洞察仅限于我们已有的特征,且观察到的行为可能还有其他潜在原因。例如,年龄趋势可能是由于政策变动驱动的,如转向在线银行,但如果没有额外的数据,这种因素无法在模型中被捕捉到。
如果你想对数值特征进行自动分组,应用此转换并为自己生成相关图表,可以使用我整理的 Python 仓库这里进行任何二元分类任务的创建。
一旦这些特征可用,我们就可以拟合逻辑回归模型。拟合后的逻辑回归将具有截距,并且模型中的每个特征都会有一个系数。基于此,我们可以输出某人可能离开银行的概率。我在这里不会花时间讨论如何拟合回归模型,但和之前一样,所有细节都可以在我的Kaggle 笔记本中找到。
校准我们的逻辑回归输出以生成得分
拟合后的逻辑回归可以输出一个概率,但这对于非技术用户来说并不特别有用。因此,我们需要对这些概率进行校准,并将其转换为更简洁、更易于解释的结果。
请记住,逻辑回归的目的是预测对数赔率。我们可以通过对这些赔率进行线性转换来生成得分,具体方法如下:
在信用风险中,将赔率翻倍和 1:1 赔率通常分别设定为 20 和 500,但这并非总是如此,值可能会有所不同。为了我的分析,我坚持使用了这些值。
我们可以通过绘制其分布来可视化校准后的得分。

作者提供的图片
我将分布按目标变量(客户是否离开银行)进行拆分,这提供了一个有用的验证,确保所有先前的步骤都已正确执行。那些更可能离开银行的客户分数较低,而那些留下的客户分数较高。虽然存在重叠,但分数很少是完美的!
基于这个分数,市场营销部门可以设定一个分数门槛,以确定应该将哪些客户作为特定营销活动的目标。这个门槛可以通过查看分布并将分数转换回概率来设置。
将分数 500 转化为概率为 50%(记住,我们的 1:1 赔率在校准步骤中等于 500)。这意味着,得分低于 500 的客户中,约有一半会离开银行。如果我们希望吸引更多这类客户,我们只需提高分数门槛。
将我们的分数表示为评分卡
我们已经知道,逻辑回归由一个截距和每个使用特征的一组权重组成。我们还知道,证据权重与对数赔率之间存在直接的线性关系。了解这一点后,我们可以将每个特征的证据权重转换为分数,以了解它对整体分数的贡献。
我已在我的Kaggle 笔记本中展示了模型中所有特征的情况,但以下是我们在将变量转换为证据权重形式时已经看到的示例。
年龄
余额
与证据权重形式相比,这种表示的优势在于,它应该对任何人都有意义,而无需理解底层的数学原理。我可以告诉市场营销的同事,48 到 63 岁之间的客户分数低于其他客户。没有账户余额的客户比有高余额的客户更可能离开。
你可能已经注意到,在评分卡中,余额趋势与在证据权重阶段观察到的相反。现在,低余额的客户分数较低。这是因为该特征在模型中的系数是负值,从而反转了最初的趋势。这种情况可能发生,因为在拟合模型过程中,特征之间存在各种交互作用。必须决定是否接受这些交互作用,或者如果趋势变得不合常理,是否希望删除该特征。
支持文档可以解释任何分数的完整细节以及它是如何开发的(或者至少应该解释!),但仅凭评分卡,任何人都应该能够立即获得洞察!
结论
我们探讨了基于逻辑回归开发分数的一些关键步骤以及它所带来的洞察。最终输出的简洁性是为什么这种类型的分数至今仍被使用,尽管有更先进的分类技术。
我为这次竞赛开发的评分模型曲线下面积为 87.4%,而基于集成技术的最佳解决方案大约为 90%。这表明,尽管这个简单模型在追求准确性的情况下并不完美,但它仍然具有竞争力。然而,如果你在下一个分类任务中寻找一个简单且易于解释的模型,为什么不考虑使用评分卡来深入了解你的数据呢?
参考文献
[1] Walter Reade, Ashley Chow, 使用银行流失数据集进行二分类 (2024), Kaggle.
解锁洞察:随机森林在 PCA 和特征重要性中的应用
一种经过验证的解决方案如何在处理日常机器学习问题时取得出色的成果
·发表于Towards Data Science ·12 分钟阅读·2024 年 3 月 31 日
--

来源:www.pexels.com/photo/a-tractor-on-a-crop-18410308/
由于生成性 AI 和庞大神经网络的广泛关注,人们容易忽视那些经过验证的旧式机器学习算法(它们其实并不算很旧……)。我甚至敢说,对于大多数商业案例,直接的机器学习解决方案比复杂的 AI 实现更能取得成果。机器学习算法不仅在规模上表现出色,远低的模型复杂度也是它们在大多数场景中更具优势的原因(这是我个人的看法)。更不用说,我也更容易追踪这些机器学习解决方案的性能了。
在本文中,我们将使用一种经典的机器学习解决方案来解决一个经典的机器学习问题。更具体地说,我将展示如何通过几行代码使用随机森林分类器识别数据集中的特征重要性。我将首先演示此技术的有效性。然后,我将采用“回归基础”的方法,展示该方法是如何在后台工作的,通过从头开始创建决策树和随机森林,同时在此过程中对模型进行基准测试。
解锁销售表现指标
如何正确衡量销售表现,并找出提升销售策略的机会
·发布于 Towards Data Science ·阅读时长:7 分钟·2024 年 6 月 25 日
--

图片由 DALL-E 生成
在我的职业生涯中,我曾与许多公司合作,这些公司认为他们清楚地知道哪些销售代理是他们的顶尖表现者,而哪些则表现不佳。
然而,我可以明确说10 个公司中有 9 个是错的。
他们多年来一直被误导,原因很简单:他们错误地衡量了销售表现。
我还可以保证,100%的公司在学会了正确的销售表现衡量方法之后,完全改变了他们的方式,并且再也没有以相同的方式衡量过销售表现。
此外,一些人甚至根据这种新的销售表现计算方法修改了他们的奖金补偿。
但是,他们到底错在哪里?
让我们通过一个清晰的例子来看看:

表格展示了一家 B2C 公司每个销售代理人每年的潜在客户处理情况、销售情况和转化率,该公司销售单一产品。
面对这些结果,我通常遇到两种解读方式:
-
那些认为最佳销售代理是“CL103”的人。这种解读基于销售或收入的绝对数字(360 笔总销售)。尽管通过收入估算销售表现是直觉告诉我们的方式,但这种衡量方式是有偏见的。问题在于,这个代理处理了更多的潜在客户。因此,由于接触了更多的潜在客户并且工作时间更长,他显然比其他人有更多的机会,因此他转化的数量更多是正常的。
-
那些认为最优秀的销售员是“AG101”的人,这种解读是基于转化率(5.7%)。在这种情况下,之前的偏差被标准化。转化率比较的是销售员产生销售的能力,而不考虑每个销售员接收到的潜在客户数量。换句话说,它评估的是潜在客户转化为销售的能力,而不关心最初潜在客户的绝对数量,从而提供了一个更公平的销售员表现视角。然而,如我们所见,销售员“AG101”提供的绝对销售额较少。有时,接收较少潜在客户的销售员可以集中精力转化它们,提供更好的服务,但在这种情况下,AG101 的销售量大约和 MJ105 或 IG105 相同,且表现远超它们。
如果我告诉你,这两个标准都不是最优的,你会怎么想?是不是可能忽略了某些东西?
答案是肯定的。它们忽略了一个非常重要的因素:接收到的潜在客户质量。
💎 潜在客户质量
是否公平将一个接收到高兴趣产品的销售人员,与一个接收到低兴趣产品的销售人员放在一起评估?
显然,不会。
潜在客户质量指的就是这种精确的兴趣水平。
处理高度合格的、愿意购买的潜在客户的销售员,自然会在销售和转化方面取得更好的成绩,而那些接收到需要更多努力和时间才能转化的潜在客户的销售员,结果则相对较差。
如果我们在分析中不考虑潜在客户的质量,那我们就是用一种有限的方式在评估销售员。
潜在客户质量 = 转化为客户的可能性
兴趣水平听起来可能有些抽象,但它实际上是可以衡量的。
从定量分析的角度来看,兴趣度高的潜在客户是那些转化为实际销售或承诺的可能性较大的客户。
考虑到转化的高概率相较于低概率的任务更容易,因此在分析中必须考虑潜在客户的质量,以便对销售员的表现进行更公平、更准确的评估。这种方法对工作的难度进行评分。
因此,绩效指标应该根据每个销售员所需的 努力水平进行调整。
平均概率 = 预期转化率
对于上述案例,我实现了一个由byRatings提供支持的潜在客户评分系统,该系统通过机器学习模型衡量潜在客户生成时的质量。
潜在客户评分模型提供了每个潜在客户转化为销售的可能性。因此,销售员的平均潜在客户评分代表了该销售员的预期转化率。
让我们通过添加“平均评分”或每个销售员接收到的潜在客户的平均转化概率来看看结果。

正如我们所见,线索的质量是决定转化率的一个关键因素。像“AG101”这样的代理,我们看到它的转化率最高,也收到了质量最高的线索。
然后,平均分数证明了它是一个相关因素,必须考虑它才能公平地比较线索分配。
那么,我们该如何衡量销售效率呢?
📈 性能比率——终极销售业绩指标
如上所述,为了公平且准确地评估销售代理的表现,必须考虑代理的转化率和线索的质量。
计算的一个好方法是使用以下公式:

这个公式计算性能比率,衡量转化率与平均分数之间的差异,相对于平均分数,以百分比形式表示。
正比率表示代理的表现好于预期转化率,而负比率表示表现不佳。
让我们再次查看包括性能比率在内的结果:

销售负责人“JC102” 拥有最高的性能比率(96.5%),这表明它显著超越了收到的平均线索质量。尽管平均分数为 2.02%,但它的转化率达到 4.00%,显示出在将低质量线索转化为销售方面的卓越效率。
另一方面,销售负责人“AI106”的比率为-59.5%,这表明即使收到预期转化率为 2.74%的线索,它的表现仍低于预期。
🥇 奖励环节
总之,性能比率是衡量销售业绩最公平的方式,它定义了销售代理在相对于线索平均质量的转化表现如何。
但是……让我们争取更高的荣誉。
拥有一个提供线索质量评分模型,我们是否可以通过重新分配来提高整体转化率?
几年来,我有以下假设:
通过 将最好的线索提供给表现最好的代理,整体转化率将提高。
这非常有道理,因为很可能由于将优质线索分配给不够合格或表现较差的代理,导致一些机会的流失。
我一直在重复自己认为这是最好的方法,但最近我意识到我也忽略了某些东西。
我的假设是基于最优秀的代理能够转化最优质的线索。但这是真的吗?
让我们来验证一下。
一种有效的方法是通过计算最佳线索的性能比率来实现。假设最佳线索是前 30%评分最高的线索,在这个用例中它们的转化提升为3.5 倍于平均值,让我们计算这个特定细分市场的性能比率。
公式如下:

将此计算添加到表格中,结果如下:

再次强调,JC102 是转化所有潜在客户表现最好的代理(96.5%转化率),同时也证明是转化最佳潜在客户的最佳代理(64.1%的最佳潜在客户转化率),这正如我们在假设中所假定的。
这确认了,在这个特定的案例中,表现最好的代理是转化最佳潜在客户的最佳代理。
然而,请注意,这一假设并非所有代理之间都有线性相关性。
尽管“AG101”表现出强劲的整体表现比率(36.3%),但其在最佳潜在客户上的负表现比率(-28.8%)表明,AG101 可能没有像预期的那样有效地利用最佳潜在客户。这可能表明,AG101 可能专注于某些类型的潜在客户,而不一定是最佳潜在客户。
事实上,CL103 的最佳潜在客户表现比 AG101(-28.8%)还要好,尽管其整体表现(0.3%)低于 AG101(36.3%)。
不是每个优秀的代理都能与最佳潜在客户表现良好。
对于销售策略的专家们,我希望你们考虑一种更好的分配优化方法,即对每个潜在客户进行单独评估,并将其分配给转化可能性最大的代理。
然而,正如上表所示,通常情况下,表现最好的代理能比表现最差的代理更好地处理最佳潜在客户,因此这是一个良好的起点。
还有更多帖子吗?
总结来说,将最佳潜在客户分配给表现最好的代理可以作为优化潜在客户分配的良好基准。
此外,采用特定的代理分配算法将是最大化转化率的最有效策略。
这可以作为另一个重要话题的引入:“如何为潜在客户分配开发终极优化算法”。
在这种算法下,每个潜在客户都会分配给能够从中获得最大绩效的代理,即能够实现相对于潜在客户预期转化率(概率)的最高转化边际的代理。
在未来的帖子中,我将详细讨论如何优化潜在客户分配以最大化转化率。
如果你觉得这篇文章很有启发性,请继续关注更多相关内容。
解锁检索增强生成(RAG)流程的未开发潜力
提升检索、生成以及端到端流程性能的关键指标和方法
·发表于Towards Data Science ·6 分钟阅读·2024 年 12 月 27 日
--
引言
当我们想到生成式人工智能的一些常见应用时,检索增强生成(RAG)无疑已经成为这个领域中最常讨论的话题之一。与传统搜索引擎依靠优化检索机制,通过关键词搜索来查找与特定查询相关的信息不同,RAG 进一步发展,使用检索到的内容为给定问题生成一个全面的回答。
下图展示了 RAG 的图示,其中感兴趣的文档通过嵌入模型进行编码,然后在向量存储中索引和存储。当提交查询时,通常会以类似的方式进行嵌入,接着有两个步骤:(1)检索步骤,搜索相似的文档,然后(2)生成步骤,使用检索到的内容来合成回应。

图片由作者提供。
虽然 RAG 已被证明是一种快速回答问题的有前景的方法,但其过程确实存在一些局限性。首先,生成的回答常常…
解锁有价值的数据和模型洞察,使用 Python 包 Yellowbrick 和 PiML(附代码)
如何获取有关模型稳健性/可靠性、训练数据量足够性等方面的洞察
·发布于 Towards Data Science ·14 分钟阅读·2024 年 5 月 17 日
--

图片由作者使用 DALL-E 创建
“我们是我们反复做的事。因此,卓越不是一种行为,而是一种习惯。”
— 威尔·杜兰特,《哲学的故事》(1926 年)
本文将探讨 Yellowbrick 和 PiML 的 Python 包如何帮助数据从业者更好地理解他们的数据和模型,解决各种数据和模型质量问题。即使在 LLMs 时代,它们能执行许多数据科学任务,理解和利用像 Yellowbrick 和 PiML 这样的工具仍然很重要,原因有以下几点:(a)它们为您的数据和模型提供强大的可视化洞察,(b)它们可以作为教育工具,特别是在分析模型性能方面,(c)它们在成本和资源方面高效,(d)它们提供数据隐私保护。所有的训练和可视化工作都可以在本地完成,无需将数据传输到云服务器。
我们将讨论的主题:
-
是否训练数据量足够支持分类任务
-
可视化分类性能如何受到特定参数调优的影响
不受欢迎的观点:成为一名优秀的数据科学家比以往任何时候都更难
在过去六年多的时间里,我在多个公司和行业担任数据科学家的工作经验,给我带来了对数据科学和人工智能工程未来的深刻见解。
·发表于 Towards Data Science ·阅读时间:25 分钟·2024 年 11 月 1 日
--
GenAI 和 大语言模型(LLMs)继续改变我们的工作方式以及未来工作的含义,尤其是在数据科学领域,在这个由 GenAI 驱动的世界里,成为一名优秀的数据科学家比以往任何时候都更加具有挑战性。
在这篇文章中,我将总结我在过去6 年多(以及近十年从事人工智能相关工作)中,涉及传统机器学习和 GenAI 的想法与经验。然而,为了更好地说明问题,我们先来谈谈什么是优秀的数据科学家。
免责声明: 以下的引述可能来源于真实人物,也可能并非如此。
➕ 如果你喜欢这篇文章,请点赞或留言,并查看我在博客上的原始帖子 🙏:
[## 不受欢迎的观点 - 成为一名优秀的数据科学家比以往任何时候都更难
一些作为“传统”数据科学家工作的想法和经验,现在我也作为一名 AI 工程师工作。
dswithmac.com](https://dswithmac.com/posts/unpopular-opinion-hard-being-good-ds/?source=post_page-----489df13b592c--------------------------------)
目录
-
什么是一个优秀的数据科学家?
-
问题 #1 — 高期望,却没有数据或数据战略
揭开 FlashAttention 的面纱
语言建模的飞跃
·发布于Towards Data Science ·6 分钟阅读·2024 年 8 月 9 日
--

由Leon Contreras拍摄,图片来源于Unsplash
本故事的第二部分现已上线。
当我在思考下一系列的主题时,解释注意力机制如何工作这一想法立即脱颖而出。事实上,在启动一个新系列时,从基础开始是一个明智的策略,而大型语言模型(LLMs)如今正是人们热议的话题。
然而,互联网上已经充斥着关于注意力的各种故事——它的机制、效果以及应用。因此,如果我不想让你在我们开始之前就昏昏欲睡,我必须找到一个独特的视角。
那么,我们是否可以从另一个角度来探讨注意力的概念?与其讨论它的好处,不如我们来分析它面临的挑战,并提出一些减轻这些挑战的策略。
牢记这一思路,本系列将专注于 FlashAttention:一种快速且内存高效的精确注意力机制,具备 IO 意识。这个描述一开始可能看起来令人有些不知所措,但我相信到最后一切都会变得清晰明了。
本系列将遵循我们惯常的格式:四个部分,每期发布一个…
解开无结构的电影数据

如何使用 LLMs 和受控词汇来增强相似度模型
·发表于 Towards Data Science ·阅读时长 16 分钟·2024 年 2 月 9 日
--
本教程的配套代码 在这里。
推荐系统是我们找到大部分内容和产品的方式,可能也包括这篇文章。推荐系统是:
“一种信息过滤系统的子类,为特定用户提供最相关的项目建议。”——维基百科
我们日常互动的推荐系统的一些例子包括 Netflix、Spotify、Amazon 和社交媒体。所有这些推荐系统都在尝试回答同一个问题:根据用户的历史行为,哪些其他产品或内容最有可能受到他们的喜爱?这些系统为公司带来了大量的收入 —— 麦肯锡 2013 年的一项研究发现,“35% 的亚马逊购物和 75% 的 Netflix 观看内容来自于产品推荐。”Netflix 在 2006 年著名地启动了一个公开竞赛,向任何能够显著改善其推荐系统的人提供 100 万美元奖金。有关推荐系统的更多信息,请参阅这篇文章。
通常,有三种类型的推荐系统:基于内容的推荐、协同过滤推荐和基于内容与协同过滤的混合推荐。协同过滤推荐系统侧重于用户的行为和偏好,依据其他相似用户的喜好来预测用户可能喜欢什么。基于内容的过滤系统则侧重于产品本身的相似性,而不是用户之间的相似性。有关这些系统的更多信息,请参见这篇 Nvidia 文章
在结构化数据集中,计算产品之间的相似性相对简单。我们可以确定我们认为最重要的产品属性,并根据这些属性的差异来衡量任意两个产品之间的“距离”。但是,如果我们想在唯一的数据是非结构化文本的情况下比较项目怎么办?例如,给定一个电影和电视节目描述的数据集,我们该如何计算哪些描述最相似?
在本教程中,我将:
-
展示一个基本的相似性模型(没有受控词汇表),使用自然语言处理(NLP)技术对非结构化文本进行处理
-
使用 LLM 创建一个类型列表
-
使用类型列表为电影打上类型标签
-
使用类型标签构建相似性模型
-
使用类型标签创建网络可视化
对我来说,写这篇文章的目标是学习两件事:一是受控词汇表(taxonomy)是否显著改善了非结构化数据相似性模型的效果,二是 LLM 是否能显著改善构建受控词汇表所需的质量和/或时间。
如果你不想阅读全部内容,这里是我主要的发现:
-
基本的 NLP 模型(没有受控词汇表)确实存在一些问题——它有时会使用与识别相似电影无关的词汇(例如,主角的名字或地点)。
-
使用受控词汇表确实显著改善了相似性模型的效果,至少基于我用来测试模型的一些示例来看是这样的。
-
使用 LLM 构建一个简单、基本的类型列表很容易——但构建一个有用和/或详细的类型词汇表很困难,也就是说,这需要更多的迭代或更具描述性的提示。我最终构建了一个大约 200 个类型的快速粗略列表,没有定义,足够用于做简单的相似性计算。
-
然而,即使是这个非常基本的使用 LLM 构建的类型列表,也存在一些问题。例如,有些类型有轻微的拼写差异,导致重复。
-
使用 LLM 为电影和电视节目打标签花费了很长时间。这个问题可能只是我在构建代码时的方式导致的。
-
可能并不令人惊讶的是,分类的深度和广度很重要。就像我上面说的,构建一个详细的电影类型分类是困难的,而且需要比我愿意为这个教程所做的更多的工作。但根据使用场景,可能不需要那么详细。我最初构建了一个包含成千上万种类型、同义词和定义的分类体系,但这也有缺点——标注变得更加困难,相似度计算也常常不如预期。因为我只看了几千部电影,所以有成千上万种类型的列表使得每部电影都变得独特,几乎没有什么相似之处。
-
将电影和类型可视化为图形总是非常棒。
使用 NLP 技术的非结构化文本基本相似度模型
我们可以使用自然语言处理(NLP)来提取文本中的关键词,识别这些词的重要性,然后在其他描述中找到匹配的词。这里有一个关于如何在 Python 中实现的教程。我不会在这里重新创建整个教程,但这里有一个简要的概述:
首先,我们从电影情节描述中提取关键词。例如,下面是电影《印第安纳·琼斯与失落的方舟》的描述。
“当印第安纳·琼斯被政府雇佣去寻找传说中的‘约柜’时,他发现自己正面对整个纳粹政权。”
然后我们使用 sklearn 的现成库来提取关键词并对其‘重要性’进行排名。为了计算重要性,我们使用了词频-逆文档频率(tf-idf)。其思想是平衡单个电影描述中词语的频率与在我们数据集中所有电影描述中该词的普遍性。例如,‘finds’这个词在这个描述中出现,但它是一个常见词,出现在许多其他电影描述中,因此它的权重比‘covenant’低。

作者提供的图片
这个模型实际上对于那些有着独特可识别主角的电影效果非常好。如果我们对这部电影运行相似度模型,最相似的电影是:《印第安纳·琼斯与夺宝圣地》、《印第安纳·琼斯与最后的十字军》以及《印第安纳·琼斯与水晶骷髅王国》。这是因为这些电影的描述中都包含了“Indiana”和“Jones”这两个词。
但这里也有问题。我们如何知道提取的词汇在相似度模型中是否相关呢?例如,如果我使用这个模型来查找与《比维斯与巴特海德:美国行》相似的电影或电视节目,排名第一的结果是《僵尸军团》。如果你不是像我一样的电影和电视迷,你可能不太熟悉动画剧集《比维斯与巴特海德》,该剧讲述的是“两个愚笨的少年[他们]花时间看电视,喝不健康的饮料,吃东西,进行一些平凡的、肮脏的冒险,通常包括破坏行为、虐待、暴力或虐待动物。”他们的电影《比维斯与巴特海德:美国行》的描述是:“在发现他们的电视丢失后,比维斯和巴特海德踏上了一场从拉斯维加斯到美国首都的探险。”而《僵尸军团》则是扎克·斯奈德导演的末日后僵尸劫匪片。那么,为什么《僵尸军团》被认为是相似的呢?因为它的故事发生在拉斯维加斯——两部电影的描述中都包含了“拉斯维加斯”这个词。
另一个此模型失败的例子是,如果我想找到与《吃、祷、爱》相似的电影或电视节目,排名第一的结果是《极端邪恶、震惊恶心》。《吃、祷、爱》是一部浪漫喜剧,朱莉娅·罗伯茨饰演莉兹·吉尔伯特,一个刚离婚的女人,正在世界各地旅行,进行自我发现的旅程。而《极端邪恶、震惊恶心》是一部讲述连环杀手特德·邦迪的真实犯罪剧情片。这两部电影有什么共同之处呢?特德·邦迪的恋人也叫莉兹。

图片由作者提供
这些当然是精心挑选的示例,展示了该模型不起作用的情况。事实上,也有很多例子表明,从文本中提取关键字可以是找到相似产品的有效方式。如上所示,包含唯一可识别名称的文本,如《特摄队员》、《印第安纳·琼斯》或《詹姆斯·邦德》,可以用来找到在描述中包含相同名称的其他标题。同样,如果描述中包含标题的类型信息,如“惊悚片”或“悬疑片”,那么这些词可以将电影与同类型的其他电影关联起来。然而,这种方法也有局限性。有些电影可能在描述中使用了“戏剧性”一词,但根据这种方法,我们无法将这些电影与包含“剧情”一词的电影描述匹配,因为我们没有考虑同义词。我们真正需要做的是仅使用相关的词汇及其同义词。
我们如何确保提取的词汇是相关的呢?这就是分类法可以提供帮助的地方。什么是分类法?
使用大语言模型创建一个类型分类
“分类法(或分类学分类)是一种分类方案,尤其是层级分类方案,其中事物被组织成不同的组或类型。” — 维基百科
也许最著名的分类法例子是用于生物学中对所有生物体进行分类的分类法——还记得领域、界、门、纲、目、科、属和种吗?所有生物都可以按照这一层级分类法进行归类。
术语说明: 本体论与分类法相似,但有所不同。正如 这篇 文章所解释的,分类法用于分类,而本体论则用于指定。“本体论是描述数据结构的类和关系的系统,是制定新类别或实体创建规则、定义属性的规则,以及建立约束的规则。”因为我们专注于对电影进行分类,所以我们将构建一个分类法。然而,出于本教程的目的,我只需要一个非常基础的类型列表,这甚至不能算是一个分类法。一个类型列表仅仅是一个标签集,或者说是一个受控词汇表。
本教程中,我们将只关注电影类型。我们需要的是一个可以用来“标记”每部电影的类型列表。试想,如果不是把电影《吃,祷,爱》标记为‘Liz’和‘true’,而是标记为‘浪漫喜剧’,‘剧情’和‘旅行/冒险’,那么我们就可以使用这些类型来找到其他与《吃,祷,爱》相似的电影,即使主角不叫 Liz。下面是我们所做工作的图示。我们使用一部分非结构化的电影数据,并结合 GPT 3.5,创建类型列表。然后我们使用类型列表和 GPT 3.5 对非结构化的电影数据进行标记。一旦我们的数据被标记,我们就可以使用这些标签作为输入,运行相似度模型。

作者提供的图片
我在线上没有找到任何免费的电影类型分类法,所以我使用一个大型语言模型(LLM)构建了自己的分类法。我从这个教程开始,教程中使用了一个 LLM 代理来构建工作职称分类法。这个 LLM 代理从职位描述中提取职位名称,为每个职位名称创建定义和职责,并列出同义词。我使用这个教程创建了一个电影类型分类法,但这有些过度——我们并不需要为本教程做这么复杂的工作。我们只需要一个非常基础的类型列表,用于标记电影。以下是我用来创建该类型列表的代码。
我使用了 Netflix 的电影和电视节目描述数据,这些数据可以在此处获得(许可证 CC0: 公共领域)。
导入所需的包并加载英语语言的 NLP 模型。
import openai
import os
import re
import pandas as pd
import spacy
from ipywidgets import FloatProgress
from tqdm import tqdm
# Load English tokenizer, tagger, parser and NER
nlp = spacy.load("en_core_web_sm")
然后,我们需要设置与 OpenAI(或你想使用的任何 LLM)的连接。
os.environ["OPENAI_API_KEY"] = "XXXXXX" # replace with yours
读取 Netflix 电影数据:
movies = pd.read_csv("netflix_titles.csv")
movies = movies.sample(n=1000) #I just used 1000 rows of data to reduce the runtime
创建一个函数,根据电影简介预测类型:
def predict_genres(movie_description):
prompt = f"Predict the top three genres (only genres, not descriptions) for a movie with the following description: {movie_description}"
response = openai.completions.create(
model="gpt-3.5-turbo-instruct", # You can use the GPT-3 model for this task
prompt=prompt,
max_tokens=50,
n=1,
stop=None,
temperature=0.2
)
predicted_genres = response.choices[0].text.strip()
return predicted_genres
现在,我们遍历电影描述的 DataFrame,使用上述函数预测与电影相关的类型,然后将它们添加到我们已建立的唯一类型列表中。
# Create an empty list to store the predicted genres
all_predicted_genres = []
# Create an empty set to store unique genres
unique_genres_set = set()
# Iterate through the movie descriptions
for index, row in tqdm(movies.iterrows(), total=movies.shape[0]):
# Get the movie description
movie_description = row['description']
# Predict the genres for the movie description
predicted_genres = predict_genres(movie_description)
# Extract genres from the text
predicted_genres_tokens = nlp(predicted_genres)
predicted_genres_tokens = predicted_genres_tokens.text
# Use regular expression to extract genres
genres_with_numbers = re.findall(r'\d+\.\s*([^\n]+)', predicted_genres_tokens)
# Remove leading/trailing whitespaces from each genre
predicted_genres = [genre.strip().lower() for genre in genres_with_numbers]
# Update the set of unique genres
unique_genres_set.update(predicted_genres)
# Convert the set of unique genres back to a list
all_unique_genres = list(unique_genres_set)
现在将这个列表转换为 DataFrame 并保存为 CSV 文件:
all_unique_genres = pd.DataFrame(all_unique_genres,columns=['genre'])
all_unique_genres.to_csv("genres_taxonomy_quick.csv")
就像我说的,这是生成类型列表的一种快速简便的方法。
使用类型列表给电影打上类型标签
既然我们已经有了类型列表,我们需要给数据集中的每部电影和电视节目(超过 8,000 部)打上标签。为了能够使用这些标签计算两个实体之间的相似度,我们需要给每部电影和电视节目打上多个类型标签。如果我们只使用一个类型标签,那么所有动作片将被认为是完全相似的,尽管有些可能更侧重于体育,另一些则可能更偏向恐怖片。
首先,我们读取我们的类型列表和电影数据集:
#Read in our genre list
genres = pd.read_csv('genres_taxonomy_quick.csv') # Replace 'genres_taxonomy_quick.csv' with the actual file name
genres = genres['genre']
#Read in our movie data
movies = pd.read_csv("netflix_titles.csv")
movies = movies.sample(n=1000) #This takes a while to run so I didn't do it for the entire dataset at once
我们已经有了一个预测类型的函数。现在我们需要再定义两个函数:一个用于过滤预测结果,确保预测的类型在我们已建立的类型列表中,另一个用于将这些过滤后的预测添加到电影 DataFrame 中。
#Function to filter predicted genres
def filter_predicted_genres(predicted_genres, predefined_genres):
# Use word embeddings to calculate semantic similarity between predicted and predefined genres
predicted_genres_tokens = nlp(predicted_genres)
predicted_genres_tokens = predicted_genres_tokens.text
# Use regular expression to extract genres
genres_with_numbers = re.findall(r'\d+\.\s*([^\n]+)', predicted_genres_tokens)
# Remove leading/trailing whitespaces from each genre
predicted_genres = [genre.strip().lower() for genre in genres_with_numbers]
filtered_genres = []
similarity_scores = []
for predicted_genre in predicted_genres:
max_similarity = 0
best_match = None
for predefined_genre in predefined_genres:
similarity_score = nlp(predicted_genre).similarity(nlp(predefined_genre))
if similarity_score > max_similarity: # Adjust the threshold as needed
max_similarity = similarity_score
best_match = predefined_genre
filtered_genres.append(best_match)
similarity_scores.append(max_similarity)
# Sort the filtered genres based on the similarity scores
filtered_genres = [x for _, x in sorted(zip(similarity_scores, filtered_genres), reverse=True)]
return filtered_genres
#Function to add filtered predictions to DataFrame
def add_predicted_genres_to_df(df, predefined_genres):
# Iterate through the dataframe
for index, row in tqdm(df.iterrows(), total=df.shape[0]):
# Apply the predict_genres function to the movie description
predicted_genres = predict_genres(row['description'])
# Prioritize the predicted genres
filtered_genres = filter_predicted_genres(predicted_genres, predefined_genres)
# Add the prioritized genres to the dataframe
df.at[index, 'predicted_genres'] = filtered_genres
一旦我们定义了这些函数,就可以在我们的电影数据集上运行它们:
add_predicted_genres_to_df(movies, genres)
现在我们进行一些数据清理:
# Split the lists into separate columns with specific names
movies[['genre1', 'genre2', 'genre3']] = movies['predicted_genres'].apply(lambda x: pd.Series((x + [None, None, None])[:3]))
#Keep only the columns we need for similarity
movies = movies[['title','genre1','genre2','genre3']]
#Drop duplicates
movies = movies.drop_duplicates()
#Set the 'title' column as our index
movies = movies.set_index('title')
如果我们打印 DataFrame 的头部,它应该是这样:

图片由作者提供
使用类型标签构建相似度模型
现在,我们将类型列转换为虚拟变量——每个类型变成一列,如果电影或电视节目被标记为该类型,那么该列的值为 1,否则为 0。
# Combine genre columns into a single column
movies['all_genres'] = movies[['genre1', 'genre2', 'genre3']].astype(str).agg(','.join, axis=1)
# Split the genres and create dummy variables for each genre
genres = movies['all_genres'].str.get_dummies(sep=',')
# Concatenate the dummy variables with the original DataFrame
movies = pd.concat([movies, genres], axis=1)
# Drop unnecessary columns
movies.drop(['all_genres', 'genre1', 'genre2', 'genre3'], axis=1, inplace=True)
如果我们打印这个 DataFrame 的头部,它看起来像这样:

图片由作者提供
我们需要使用这些虚拟变量来构建矩阵,并在所有电影对之间运行相似度模型:
# If there are duplicate columns due to the one-hot encoding, you can sum them up
movie_genre_matrix = movies.groupby(level=0, axis=1).sum()
# Calculate cosine similarity
similarity_matrix = cosine_similarity(movie_genre_matrix, movie_genre_matrix)
现在我们可以定义一个计算与给定标题最相似的电影的函数:
def find_similar_movies(movie_name, movie_genre_matrix, num_similar_movies=3):
# Calculate cosine similarity
similarity_matrix = cosine_similarity(movie_genre_matrix, movie_genre_matrix)
# Find the index of the given movie
movie_index = movie_genre_matrix.index.get_loc(movie_name)
# Sort and get indices of most similar movies (excluding the movie itself)
most_similar_indices = np.argsort(similarity_matrix[movie_index])[:-num_similar_movies-1:-1]
# Return the most similar movies
return movie_genre_matrix.index[most_similar_indices].tolist()
让我们看看这个模型是否比之前的模型找到更多类似《美味祈祷爱》的电影:
# Example usage
similar_movies = find_similar_movies("Eat Pray Love", movie_genre_matrix, num_similar_movies=4)
print(similar_movies)
这次查询的输出对我来说是“完美的一天”、“Love Dot Com: 社会实验”和“50 次初恋”。所有这些电影都被标记为浪漫喜剧和剧情片,就像《美味祈祷爱》一样。

图片由作者提供
《极度邪恶、震惊邪恶且肮脏》的电影讲述了一位爱上泰德·邦迪的女人,标签为浪漫、剧情和犯罪。最相似的电影有《耐心男人的愤怒》、《深爱》和《爱你》,它们也都标注了浪漫、剧情和犯罪。电影《比佛斯与巴特赫德:去美国》标注了喜剧、冒险和公路旅行这几个类型。最相似的电影有《比维的伟大假期》、《羊羊大电影:农场末日》和《宠物的秘密生活 2》。这些电影也都标注了冒险和喜剧类型——在这个数据集中(至少是我标注的部分),没有其他电影符合比佛斯与巴特赫德的三个标签类型。
使用类型标签创建网络可视化
你不能在不构建酷炫网络可视化的情况下将数据连接起来。有几种方法可以将这些数据转化为图形——我们可以看看电影是如何通过类型连接的,类型又是如何通过电影连接的,或者两者的结合。因为这个数据集中的电影非常多,所以我只用了类型作为节点,电影作为边,制作了一个图形。
这是我将数据转换为节点和边的代码:
# Melt the dataframe to unpivot genre columns
melted_df = pd.melt(movies, id_vars=['title'], value_vars=['genre1', 'genre2', 'genre3'], var_name='Genre', value_name='GenreValue')
genre_links = pd.crosstab(index=melted_df['title'], columns=melted_df['GenreValue'])
# Create combinations of genres for each title
combinations_list = []
for title, group in melted_df.groupby('title')['GenreValue']:
genre_combinations = list(combinations(group, 2))
combinations_list.extend([(title, combo[0], combo[1]) for combo in genre_combinations])
# Create a new dataframe from the combinations list
combinations_df = pd.DataFrame(combinations_list, columns=['title', 'Genre1', 'Genre2'])
combinations_df = combinations_df[['Genre1','Genre2']]
combinations_df = combinations_df.rename(columns={"Genre1": "source", "Genre2": "target"}, errors="raise")
combinations_df = combinations_df.set_index('source')
combinations_df.to_csv("genreCombos.csv")
这将生成一个如下所示的 DataFrame:

图片来源:作者
这个 DataFrame 中的每一行代表一部被标注为这两种类型的电影。我们没有去除重复项,所以应该会有很多行像上面第 1 行一样——有很多电影同时被标注为浪漫和剧情。
我使用了Gephi来构建了如下所示的可视化:

图片来源:作者
这里节点的大小表示被标注为该类型的电影数量。节点的颜色是基于社区检测算法的结果——在同一群体内比与外部节点的连接更紧密的群体会使用相同的颜色。
这对我来说非常吸引人。戏剧、喜剧和纪录片是三个最大的类别节点,意味着更多的电影被标记为这些类型,而不是其他类型。这些类型自然形成了直观的集群。与“纪录片”最相关的类型被标记为粉红色,主要是某种纪录片子类型:自然/野生动物、真人秀、旅行/冒险、历史、教育、传记等。有一个核心集群是绿色的:戏剧、喜剧、浪漫、成长故事、家庭等。这里有一个问题是,“成长故事”类型的拼写有多种版本——这是我在未来版本中会解决的问题。还有一个蓝色的集群,包括动作/冒险、幻想、科幻和动画。再次强调,这里有重复和重叠的类型,这是一个问题。还有一个小的棕色类型,包括惊悚片、悬疑片和恐怖片——这些通常出现在同一部电影中。某些类型之间缺乏连接也是很有趣的——例如,没有电影同时标记为“单口喜剧”和“恐怖片”。
结论
这个项目让我意识到,即使是最基本的受控词汇在构建基于内容的推荐系统时也非常有用,甚至可能是必需的。仅凭一份类型列表,我们就能够标记电影,并以比仅使用自然语言处理更具可解释性的方法找到其他相似的电影。显然,通过更详细的描述性类型分类法,这一过程可以得到极大的改进,同时也可以通过包括演员和工作人员、拍摄地点等其他分类法来增强。
正如使用大型语言模型(LLM)时常见的情况一样,刚开始我对它在完成这一任务时的表现印象深刻,但在查看并尝试改进结果时,我感到失望。构建分类法、本体论或任何受控词汇都需要人工参与——必须有人工介入,以确保词汇合理且能满足特定的使用需求。
LLM 和知识图谱(KGs)自然是相辅相成的。它们可以一起使用的一种方式是,LLM 可以帮助促进知识图谱的创建。LLM 本身无法构建知识图谱,但它们肯定能帮助你创建一个。
无监督学习系列:探索均值漂移算法
让我们学习一下最著名的基于密度的聚类算法之一——均值漂移。
·发表于 Towards Data Science ·10 分钟阅读·2024 年 8 月 19 日
--

K 均值、层次聚类、期望最大化 和 DBScan 可能是你在机器学习领域最为熟悉的几种聚类算法。
然而,还有另一个基于密度的算法,它已经解决了各种问题,从卫星图像分割到物体追踪,它的名字是均值漂移。这个算法因通过在数据点密度较高的区域周围构建聚类来寻找特定数据集的模态而闻名。
可视化均值漂移是很简单的。通常,我会使用摩天大楼天际线的类比。如果你考虑摩天大楼天际线:

使用摩天大楼的密度簇 — 由 AI 在 Microsoft Image Designer 中生成的图像
如果我们将摩天大楼视为我们的数据点,那么我们可以清楚地看到两种不同的密度簇:

使用摩天大楼的密度簇 — 由 AI 在 Microsoft Image Designer 中生成的图像
解开人工智能系统的谜团
物理学如何帮助我们理解神经网络
·发布于 Towards Data Science ·13 分钟阅读·2024 年 10 月 26 日
--

由作者使用 DALL-E 生成的图像
如果我们能够打开一个人工智能系统,发现它是一个组织良好的组件工厂,所有组件都在一起工作,那会怎么样?本文探讨了一种结合了两个强大概念的新方法:稀疏神经电路和受物理学启发的数学。通过结合这些不同的领域,我们可以找到分析和构建人工智能系统的新方法。虽然神经网络看起来像是难以捉摸的“黑盒子”,研究人员却发现了令人着迷的东西:它们包含可解释的“电路”,这些电路的功能类似于机器组件。让我用简单的语言解释一下。
神经电路
如果我们不是试图一次性理解整个神经网络,而是像生物学家研究个别细胞和神经通路一样,逐一检查它们,那会怎么样?这种方法受神经学和细胞生物学的启发,于 2018 年由Chris Olah率先提出,提供了一种更彻底理解神经网络的方式。

图像来自 distill.pub/2020/circuits/zoom-in/
想想我们如何在一张图片中识别出一只狗。我们的脑袋处理不同的特征:耳朵的弯曲、毛发的纹理、眼睛的圆形。神经网络……
揭开元动力学的面纱
学习掌握 PLUMED 的初学者指南(第一部分,共 3 部分)
·发表于Towards Data Science ·阅读时间 10 分钟·2024 年 6 月 22 日
--

DALL-E 生成的封面图像
在计算化学和分子动力学(MD)中,理解复杂系统有时需要超越 MD 引擎或 VMD 可视化所提供的分析。我个人从事生物分子原子模拟工作,这些分子可真是大得惊人。考虑到计算每个原子在这些大模拟盒子中的轨迹的复杂性,通常我只能看到不超过 1 或 2 微秒的轨迹,这是许多 MD 运行的常见上限。这意味着,虽然传统的 MD 方法对于观察发生在此时间范围内的过程的轨迹波动非常有效,但对于那些需要更长时间的过程,怎么办呢?
有一种强大的技术可以用来研究这些过程,叫做元动力学,而 PLUMED 作为一个在该领域中的领先工具,因其与 GROMACS 引擎的无缝集成而脱颖而出。在这一系列文章中,我们将逐步建立对元动力学的理解,涵盖理论、代码和语法,最终目标是能够为你想深入观察的任何现象生成复杂的元动力学模拟!本文将特别介绍元动力学的概念以及一些关于如何正确安装 PLUMED 和快速运行的一般代码。
本文假设读者已经熟悉某些分子动力学(MD)引擎,并能够为该引擎生成系统。如果你只是想了解一个有趣的技术,这并不必要,但我建议如果你打算实现元动力学,最好对某个 MD 引擎(我偏好 GROMACS)有所了解。
什么是元动力学?
元动力学是一种先进的采样方法,旨在探索分子系统的自由能景观。它通过鼓励那些系统原本不容易发生的事件,帮助研究稀有事件和缓慢过程。
它通过添加一个历史依赖的偏置势来实现这一点,从而允许系统离开 CV 空间中的局部最小值,克服能量障碍,探索更多样的配置,具体取决于你要做的事情。

我做的一个旧的元动力学运行的 3D 渲染图,展示了一个膜蛋白两个亚基相对位置的变化。等高线和着色表示相对自由能,3D 视图则展示了在物理景观中的表现。(图像由作者生成)
理解元动力学中的高斯分布
所以,元动力学成功的关键在于相对自由能中目标高斯分布的实现。为了理解这个过程,我们来看看经典物理学中的球体滚下山坡的比喻。我们都知道球体会滚下来,但如果这座山坡不仅仅是一个双曲形的隆起,而是一座山脉呢?那么球体仍然会滚下来,但它很可能会被困在某个裂缝中,或者停在一个较平坦的地方,而不是滚到山脉中的全局最低点。

看它怎么动起来!(DALL-E 生成)
元动力学通过依次在球体下添加高斯核,就像在球下堆小土堆一样,来修改这一情景。当你在球上添加足够的土堆时,球就会处于一个可以再次滚动的位置。我们基本上会一直这样做,直到差不多填满所有的山谷,每添加一个土堆都会记录下来。一旦我们添加足够的土堆,将山脉替换成平坦的表面后,就可以通过统计我们添加的土堆的数量及每个土堆的大小来确定每个区域的深度,从而找出山脉中最低能量的位置。
现在,这可能不是用一个真实的球在真实的山脉中做事的最有效方法;它既不具成本效益,也不环保,而且过程中你还破坏了山脉。但如果我们将这个思路应用到一个系统的自由能表面,它就变成了一个非常理想的选择。如果我们用我们感兴趣的集合变量(CVs)代替球的位置,并在其中执行相同的基本任务,逐步添加高斯分布,我们就可以形成一个自由能表面(FES),它告诉我们不同构象的相对吉布斯自由能,就像下面的图所示:

这里的顶部图是自由能表面(FES),它显示了相对吉布斯自由能如何作为位置的二维函数变化。第二个图描绘了“球体”在这些维度中的路径。(图像由作者生成)
所以我在上面包括了两个关于理论元动力学模拟的图形分析。第一个图是我们刚才讨论的,使用 2 个 CV 构建系统在某一状态下相对自由能的拓扑图。我在这里通过将两个维度设为空间维度来简化问题,所以我们看的是分子相对于点(0, 0)的位置以及与之相关的能量。第二个图通过展示分子在坐标空间中随时间移动的轨迹来简化这一点,并用颜色表示它在何时处于何种区域。基本上,这是球体滚动的路径,如果某个地方有一大堆点,那么很可能它对应着 FES 图中的一个最小值。现在我需要快速说明的是,这是一种多行走者(MW)模拟,因为我从我的档案中迅速抓取了一个;这意味着我有三个球在滚动,并且为所有三个球添加了泥土,这些泥土在所有三个球上都保持存在。我们将在未来的文章中讨论为什么这么做,但如果你仔细观察那张图,这就是为什么会有三条独立轨迹的原因。
请记住,虽然我使用了空间维度来更有效地解释类比,但此图的 CV 轴几乎可以是任何你可以通过算法向 PLUMED 解释的内容,包括扭转、角度、向量等。将其可视化为地形图只是理解 CV 如何工作和/或如何协同工作的一种简便方式。
好了,添加这些泥土到底是怎么回事?什么是高斯分布?
高斯分布(所添加的核),也称为正态分布,是一种钟形曲线,其特点是均值(μ)和标准差(σ)。

我用 Python 快速制作的高斯分布图,展示了均值和标准差。(图像由作者生成)
高斯分布的数学表示由以下方程给出:

高斯分布方程(图像由作者生成)
在元动力学的背景下,高斯丘通常按规律间隔添加到自由能表面,以防止系统重新访问之前已探索的状态。这些丘由以当前系统在集体变量(CV)空间中的位置为中心的高斯函数描述。这些高斯函数的高度和宽度决定了偏置的影响。
在元动力学中,高斯丘的方程是:

偏置势能方程(图像由作者生成)
其中,s(τ) 是集体变量空间中在时间点 τ 时的位置,W 是高斯偏置的高度,δ 是偏置的宽度,求和范围涵盖了所有添加偏置的时间点 τ。基本上这就是一种复杂的说法:“我们在这些地方添加了这么多‘土’”。我将在本系列文章的第二部分深入探讨元动力学背后的数学原理,所以现在你不必强求理解这些方程,只要知道“球在丘顶”已经足够进行并分析我们的第一次元动力学模拟了。
开始使用 PLUMED
安装:
不得不说,这有点烦人,它需要被打补丁到你的 MD 引擎中。如果你不打算使用 GROMACS 作为你的 MD 引擎,以下是 PLUMED 官方主页的链接,因为关于安装的部分你需要自己处理:
在此页面上,你可以学习如何配置、编译和安装 PLUMED。对于那些不耐烦的人,…
www.plumed.org](https://www.plumed.org/doc-v2.9/user-doc/html/_installation.html?source=post_page-----0442e1196abb--------------------------------)
否则,以下是如何同时安装这两个工具并正确地打补丁。如果你没有安装任何一个,请按照以下命令进行操作;如果你已经安装并且能够正常使用 GROMACS,则可以忽略 GROMACS 的安装部分。这些命令需要逐个在你的终端/命令行中执行。
#Download GROMACS
wget http://ftp.gromacs.org/pub/gromacs/gromacs-2021.2.tar.gz
tar xfz gromacs-2021.2.tar.gz
cd gromacs-2021.2
#Install and source GROMACS
mkdir build
cd build
cmake .. -DGMX_BUILD_OWN_FFTW=ON -DREGRESSIONTEST_DOWNLOAD=ON
make
sudo make install
source /usr/local/gromacs/bin/GMXRC
#Download PLUMED
wget https://github.com/plumed/plumed2/releases/download/v2.7.1/plumed-2.7.1.tgz
tar xfz plumed-2.7.1.tgz
cd plumed-2.7.1
#install PLUMED
./configure --prefix=/usr/local/plumed
make
sudo make install
#Patch GROMACS
cd gromacs-2021.2
plumed patch -p
#rebuilld GROMACS
cd build
cmake .. -DGMX_BUILD_OWN_FFTW=ON -DREGRESSIONTEST_DOWNLOAD=ON -DGMX_PLUMED=on
make
sudo make install
#Check installation
gmx mdrun -plumed
你会注意到我选择了一个较旧版本的 GROMACS;这是为了让我们有更大的机会避免在这些文章中出现意外的 bug,你完全可以根据自己的需求使用更新版本,只要确保它是与 PLUMED 兼容的。
基本配置:
- 创建一个 PLUMED 输入文件,以定义描述系统重要自由度的集体变量(CVs)。
这是一个示例文件。我将在本系列文章的第三部分更详细地讨论一些更高级的选项,但现在我们先从使用距离和扭转作为集体变量(CVs)来看一组原子的构象状态。其他可能的集体变量包括原子间的距离、角度、二面角或更复杂的函数。
# Define collective variables
# Distance between atoms 1 and 10
DISTANCE ATOMS=1,10 LABEL=d1
# Dihedral angle involving atoms 4, 6, 8, and 10
TORSION ATOMS=4,6,8,10 LABEL=t1
# Print collective variables to a file
PRINT ARG=d1,t1 FILE=COLVAR STRIDE=100
# Apply metadynamics bias
METAD ...
ARG=d1,t1 # The collective variables to bias
PACE=500 # Add a Gaussian hill every 500 steps
HEIGHT=0.3 # Height of the Gaussian hill
SIGMA=0.1,0.1 # Width of the Gaussian hill for each CV
FILE=HILLS # File to store the hills
BIASFACTOR=10 # Bias factor for well-tempered metadynamics
TEMP=300 # Temperature in Kelvin
... METAD
# Print the bias potential to a file
PRINT ARG=d1,t1,bias FILE=BIAS STRIDE=500
该代码块中的注释应该足够详细,以便基本理解正在发生的所有内容,但我将在第三篇文章中介绍这一切,甚至会深入探讨更复杂的功能!
无论如何,一旦你拥有这个输入文件(通常命名为 plumed.dat)和用于 GROMACS MD 运行所需的.tpr 文件(请参阅 gmx grompp 文档了解如何生成该文件),你可以通过进入工作目录并在命令行中输入以下命令来运行元动力学模拟:
gmx mdrun -s topol.tpr -plumed plumed.dat
PLUMED 和 GROMACS 都接受额外的参数。我将在这系列文章的第三部分介绍一些更有用的参数,并展示我为更高级的运行编写的一些脚本,你可以查看文档以了解其他参数。
在模拟之后,使用 PLUMED 的分析工具重建自由能表面,并识别相关的亚稳态和过渡路径。最常用的是使用 PLUMED 的sum_hills工具来重建自由能表面。
你可以在执行完该命令后,使用以下 Python 代码查看自由能表面(FES),该代码将告诉你一个 CV 的值如何与另一个 CV 相关。
import matplotlib.pyplot as plt
import numpy as np
import plumed
from matplotlib import cm, ticker
# Configure font
plt.rc('font', weight='normal', size=14)
# Read data from PLUMED output
data = plumed.read_as_pandas("/path/to/COLVAR")
# Extract and reshape data for contour plot
# Adjust the reshape parameters as needed, They should multiply to the
# number of bins and be as close to each other as possible
d1 = data["d1"].values.reshape(-1, 100)
t1 = data["t1"].values.reshape(-1, 100)
bias = data["bias"].values.reshape(-1, 100)
# Plot contour lines
plt.contour(d1, t1, bias, levels=np.arange(np.min(bias), np.max(bias), 10), linewidths=0.3, colors='k')
# Plot filled contour
cntr = plt.contourf(d1, t1, bias, levels=np.arange(0, 100), cmap=cm.jet)
# Add colorbar
plt.colorbar(cntr, label="\u0394G [kJ/mol]")
# Set plot limits and labels
plt.xlim(np.min(d1), np.max(d1))
plt.ylim(np.min(t1), np.max(t1))
plt.xlabel("Distance between atoms 1 and 10 (d1) [nm]")
plt.ylabel("Dihedral angle involving atoms 4, 6, 8, and 10 (t1) [degrees]")
# Show plot
plt.show()
输出应该类似于我之前发布的拓扑图(我无法给你展示你的 FES 图,因为你有自由选择自己的系统)。
你还应该使用流行的可视化软件,如 VMD,来可视化结果,从而深入了解低能量状态和亚稳态中的分子行为。
结论
由 PLUMED 驱动的元动力学提供了一个强大的框架,用于探索复杂的分子系统。通过高效地采样自由能景观,我们可以揭示分子系统中传统 MD 由于计算限制无法揭示的隐藏机制。
无论你是初学者还是经验丰富的研究人员,掌握 PLUMED 都能显著提升你的计算化学工具包,因此不要忘记查看我接下来的两篇文章,帮助你从新手变成专家!
第二篇文章将揭示将元动力学组件添加到 MD 引擎背后的数学概念,而第三篇文章将向你介绍元动力学中的高级技术,如多行者元动力学、将超过 2 个变量压缩成可读格式、在高性能集群上使用元动力学,以及更深入的分析技术,用于可视化和定量分析你的系统结果(并附有大量示例代码)。
揭示 LLM 的内部工作原理:奇异值视角
对 Llama3–8B 投影矩阵的奇异值分析
·发表于Towards Data Science ·9 分钟阅读·2024 年 6 月 14 日
--

由Afif Ramdhasuma拍摄,来自Unsplash
你是否曾经想过,一个大型语言模型(LLM)训练得有多好?考虑到巨大的参数量,这些参数是否最大限度地捕捉了训练数据中的信息或知识?如果没有,我们是否可以从 LLM 中移除那些无用的参数,使其更高效?
在本文中,我们将尝试通过从奇异值的角度对 Llama-3–8B 模型进行深入分析,来回答这些问题。现在,让我们舒适地坐好,准备好应用 SVD 分析 Llama-3–8B 矩阵的质量!
SVD 重新审视
在奇异值分解(SVD)中,一个矩阵 A 被分解为三个其他矩阵:
A=U Σ V_t
其中:
-
A 是原始矩阵。
-
U 是一个矩阵,其列是 A 的左奇异向量。
-
Σ是一个对角矩阵,包含 A 的奇异值。这些值始终是非负的,通常按从大到小的顺序排列。
-
V_t 是 V 的转置矩阵,其中 V 的列是 A 的右奇异向量。
简而言之,SVD 将矩阵的复杂变换分解为更简单、易于理解的步骤,这些步骤涉及旋转和缩放。Σ中的奇异值告诉我们缩放因子,而 U 和 V_t 中的奇异向量告诉我们应用矩阵前后这些缩放的方向。
我们可以将奇异值视为衡量矩阵在空间中不同方向上拉伸或收缩程度的方式。每个奇异值对应一对奇异向量:一个右奇异向量(输入空间中的方向)和一个左奇异向量(输出空间中的方向)。
因此,奇异值是表示“幅度”的缩放因子,而 U 和 V_t 矩阵分别表示变换空间和原始空间中的“方向”。
如果矩阵的奇异值呈现快速衰减(最大的奇异值明显大于较小的奇异值),则意味着矩阵的有效秩(即显著奇异值的数量)远小于矩阵的实际维度。这表明该矩阵可以通过低秩矩阵进行良好的近似。
大的奇异值捕捉了数据中大部分的重要信息和变异性,而较小的奇异值贡献较少。
在 LLM 的上下文中,权重矩阵(例如注意力机制或前馈层中的矩阵)将输入数据(如词嵌入)转化为输出表示。主导的奇异值对应于输入空间中通过变换最为放大的方向,表示模型在这些方向上最为敏感或最具表达能力。较小的奇异值对应于在变换过程中较不重要或影响较小的方向。
奇异值的分布可以影响模型的泛化能力和鲁棒性。缓慢衰减(许多大奇异值)可能导致过拟合,而快速衰减(少量大奇异值)则可能表明欠拟合或信息丢失。
重新审视 Llama-3 架构
以下是 meta-llama/Meta-Llama-3–8B-Instruct 模型的 config.json 文件。值得注意的是,这个 LLM 使用了分组查询注意力机制,num_key_value_heads 为 8,这意味着组大小为 32/8=4。
{
"architectures": [
"LlamaForCausalLM"
],
"attention_bias": false,
"attention_dropout": 0.0,
"bos_token_id": 128000,
"eos_token_id": 128009,
"hidden_act": "silu",
"hidden_size": 4096,
"initializer_range": 0.02,
"intermediate_size": 14336,
"max_position_embeddings": 8192,
"model_type": "llama",
"num_attention_heads": 32,
"num_hidden_layers": 32,
"num_key_value_heads": 8,
"pretraining_tp": 1,
"rms_norm_eps": 1e-05,
"rope_scaling": null,
"rope_theta": 500000.0,
"tie_word_embeddings": false,
"torch_dtype": "bfloat16",
"transformers_version": "4.40.0.dev0",
"use_cache": true,
"vocab_size": 128256
}
对(Q, K, V, O)矩阵的奇异值分析
现在,让我们进入本文的重点:通过奇异值分析 Llama-3–8B-Instruct 模型的(Q, K, V, O)矩阵!
代码
首先,让我们导入所有进行此分析所需的必要包。
import transformers
import torch
import numpy as np
from transformers import AutoConfig, LlamaModel
from safetensors import safe_open
import os
import matplotlib.pyplot as plt
然后,让我们下载模型并将其保存到本地 /tmp 目录中。
MODEL_ID = "meta-llama/Meta-Llama-3-8B-Instruct"
!huggingface-cli download {MODEL_ID} --quiet --local-dir /tmp/{MODEL_ID}
如果你有强大的 GPU,以下代码可能对你不相关。然而,如果你像我一样 GPU 资源较少,以下代码将非常有用,它可以让你只加载 LLama-3–8B 模型的特定层。
def load_specific_layers_safetensors(model, model_name, layer_to_load):
state_dict = {}
files = [f for f in os.listdir(model_name) if f.endswith('.safetensors')]
for file in files:
filepath = os.path.join(model_name, file)
with safe_open(filepath, framework="pt") as f:
for key in f.keys():
if f"layers.{layer_to_load}." in key:
new_key = key.replace(f"model.layers.{layer_to_load}.", 'layers.0.')
state_dict[new_key] = f.get_tensor(key)
missing_keys, unexpected_keys = model.load_state_dict(state_dict, strict=False)
if missing_keys:
print(f"Missing keys: {missing_keys}")
if unexpected_keys:
print(f"Unexpected keys: {unexpected_keys}")
我们这样做的原因是因为 Google Colab 的免费 GPU 配置即便使用 fp16 精度也不足以加载 LLama-3–8B。此外,由于 np.linalg.svd 的构建方式,这项分析要求我们使用 fp32 精度。接下来,我们可以定义主函数,以获取给定 matrix_type、layer_number 和 head_number 的奇异值。
def get_singular_values(model_path, matrix_type, layer_number, head_number):
"""
Computes the singular values of the specified matrix in the Llama-3 model.
Parameters:
model_path (str): Path to the model
matrix_type (str): Type of matrix ('q', 'k', 'v', 'o')
layer_number (int): Layer number (0 to 31)
head_number (int): Head number (0 to 31)
Returns:
np.array: Array of singular values
"""
assert matrix_type in ['q', 'k', 'v', 'o'], "Invalid matrix type"
assert 0 <= layer_number < 32, "Invalid layer number"
assert 0 <= head_number < 32, "Invalid head number"
# Load the model only for that specific layer since we have limited RAM even after using fp16
config = AutoConfig.from_pretrained(model_path)
config.num_hidden_layers = 1
model = LlamaModel(config)
load_specific_layers_safetensors(model, model_path, layer_number)
# Access the specified layer
# Always index 0 since we have loaded for the specific layer
layer = model.layers[0]
# Determine the size of each head
num_heads = layer.self_attn.num_heads
head_dim = layer.self_attn.head_dim
# Access the specified matrix
weight_matrix = getattr(layer.self_attn, f"{matrix_type}_proj").weight.detach().numpy()
if matrix_type in ['q','o']:
start = head_number * head_dim
end = (head_number + 1) * head_dim
else: # 'k', 'v' matrices
# Adjust the head_number based on num_key_value_heads
# This is done since llama3-8b use Grouped Query Attention
num_key_value_groups = num_heads // config.num_key_value_heads
head_number_kv = head_number // num_key_value_groups
start = head_number_kv * head_dim
end = (head_number_kv + 1) * head_dim
# Extract the weights for the specified head
if matrix_type in ['q', 'k', 'v']:
weight_matrix = weight_matrix[start:end, :]
else: # 'o' matrix
weight_matrix = weight_matrix[:, start:end]
# Compute singular values
singular_values = np.linalg.svd(weight_matrix, compute_uv=False)
del model, config
return list(singular_values)
值得注意的是,由于 HuggingFace 的实现方式,我们可以通过行切片提取指定头部的 K、Q 和 V 矩阵的权重。

HuggingFace 中的 Q, K, V 矩阵实现。请注意,在 PyTorch 中,矩阵的维度将是 (d_out,d_in)。来源:图片由作者提供。
至于 O 矩阵,我们可以通过列切片提取指定头的 O 权重,这要归功于线性代数!详细信息可以参见下图。

为什么我们可以通过列切片提取指定头的 O 权重矩阵。来源:图片由作者提供。
结果
为了进行分析,我们需要在不同的头、层和矩阵类型之间运行 get_singular_values() 函数。为了能够比较所有这些不同的组合,我们还需要为分析定义几个辅助指标:
-
Top-10 Ratio:前 10 个奇异值之和与所有奇异值之和的比率 -
First/Last Ratio:最大奇异值与最小奇异值之比。 -
Least-10 Ratio:最小 10 个奇异值之和与所有奇异值之和的比率
(第 0 层,第 0 头) 分析

第 0 层第 0 头的奇异值分布。来源:图片由作者提供。
-
Q(查询)矩阵具有最大的初始奇异值(约 10),其次是 K(键)矩阵(约 8)。这两个矩阵的初始奇异值显著高于 V(值)和 O(输出)矩阵的初始奇异值。
-
不仅是初始奇异值,如果我们检查 Q 和 K 矩阵的
Top-10 Ratio和First/Last Ratio,这两个矩阵的值比 V 和 O 矩阵要高得多。这表明 Q 和 K 矩阵主要集中在少数几个维度上,而 V 和 O 矩阵则以更加分散的方式捕获信息,跨多个成分。 -
如果我们查看
Least-10 Ratio指标,我们也可以看到,对于 Q 和 K 矩阵,奇异值接近零,并且相较于 V 和 O 矩阵,它们的值要低得多。这是一个证据,表明 Q 和 K 矩阵具有低秩结构,这意味着这些维度对模型整体性能的贡献较小。这些权重可以潜在地被修剪,而不会显著影响模型的准确性。
(第 0 层,多个头部) 分析

不同头部下第 0 层的奇异值分布。来源:图片由作者提供。
-
随着
head_number的增加,Q 和 K 矩阵的Top-10 比率增长的速度通常远高于 V 和 O 矩阵。这一发现同样适用于 Q 和 K 矩阵的Least-10 比率,随着head_number的增加,这些比率趋近于 0,而 V 和 O 矩阵则没有这种变化。 -
这表明,具有更高
**head_number**的 Q 和 K 矩阵相比于具有较低head_number的头部,甚至具有更低的秩结构。换句话说,随着head_number的增加,Q 和 K 矩阵倾向于在更低的维度中存储信息。
跨层分析

不同层次和头部之间的奇异值分布。来源:作者提供的图片。
-
当我们进入更深的层时,我们发现 Q 和 K 矩阵的初始值在下降,但与 V 和 O 矩阵相比,仍然相对较高。
-
当我们进入更深的层时,Q 和 K 矩阵的
Top-10 比率和First/Last 比率在某个特定头部出现下降趋势。与此同时,Least-10 比率稍微呈上升趋势。这表明,在更深层的 Q 和 K 矩阵相较于较低层次的矩阵训练得更好。
然而,在第 1 层发现一个异常,Q 和 K 矩阵的
First/Last 比率非常高,没有遵循我们在更深层次中发现的下降趋势。

不同头部和层次之间的奇异值分布。来源:作者提供的图片。
- 在同一层内我们在“Layer 0, Multiple Heads”部分发现的头部间模式,在更深的层次中不再明显。
总结
-
K 和 Q 矩阵的秩通常低于 V 和 O 矩阵。如果我们想进行剪枝或降维方法,可以更多关注 K 和 Q 矩阵。
-
层数越深,所有 (K, Q, V, O) 矩阵训练得越好。如果我们想进行剪枝或降维方法,可以更多关注较低的层次。
-
除了剪枝,进行仅对几个初始层进行完全微调也是一个有趣的实验,甚至我们可以使用 LoRA 来实现这一点。
结语

恭喜你坚持到现在!希望你从本文中学到了一些新东西。将线性代数中的经典概念应用到理解 LLM 的训练效果上,确实很有趣。
如果你喜欢这种类型的内容,请关注我的 Medium 账号,获取其他未来文章的通知。
关于作者
Louis Owen是来自印尼的数据科学家/人工智能研究工程师,他总是渴望获得新知识。在他的职业生涯中,他在多个行业领域工作过,包括非政府组织、电子商务、对话式人工智能、在线旅游代理、智慧城市和金融科技。在工作之外,他喜欢通过自己的文章或辅导课程,帮助数据科学爱好者成为数据科学家。
目前,Louis 是 Yellow.ai的自然语言处理研究工程师,Yellow.ai是全球领先的客户体验自动化平台。访问Louis 的个人网站以了解更多关于他的信息!最后,如果你有任何问题或需要讨论的话题,请通过LinkedIn联系 Louis。
升级到 AWS ECS 上的 Prefect 推送工作者
从 Prefect 2.0 升级到 3.0,并使用新的推送工作池,大大简化基础设施管理并降低成本。
·发表在Towards Data Science·6 分钟阅读·2024 年 10 月 12 日
--

照片由Techivation提供,来自Unsplash
概述
Prefect是一个用于数据工程的开源编排工具。这个基于 Python 的工具允许你定义、调度和监控你的数据管道。Prefect 是一个适用于希望自动化他们的数据管道的数据工程师和科学家的绝佳工具。
在一篇名为Getting Started with Prefect: Powerful Orchestration for Your Data的文章中,我写到部署 Prefect 到 AWS EC2 来运行他们所谓的代理,本质上是在启动后运行作业。
升级到版本 3.0,Prefect 正在淘汰长期存在的代理模型,转而采用一种称为工作者的新架构。从 v2 升级到 v3 立即破坏了我的部署,让我不得不想出一个替代方案。
工作者与他们运行的基础设施更紧密地集成在一起。这是一个了不起的改进,因为在过去,你需要弄清楚如何在你的计算机上托管代理。这就像在经典托管服务器上安装软件并维护操作系统更新一样。
通过保形预测和自定义非保形评分方法调整的预测区间
我应该对机器学习模型在新数据点上的预测有多大的信心?我能否得到一个可能值的范围?
·发表于Towards Data Science ·9 分钟阅读·2024 年 1 月 24 日
--

作者提供的图像
在进行监督任务时,可以使用机器学习模型来预测新样本的结果。然而,新数据点的预测可能是错误的。特别是在回归任务中,结果可能取无限多个值时,这一点尤为真实。
为了得到更有洞察力的预测,我们可能更希望(甚至需要)一个预测区间,而不是单一的预测点。做出充分信息的决策应考虑不确定性。例如,作为一名房地产投资者,如果预测区间是[100000–10000 ; 100000+10000],我不会给出与预测区间为[100000–1000 ; 100000+1000]时相同的报价(尽管单点预测是相同的,即 100000)。我可能会对第二个区间的单点预测比较信任,但对于第一个区间,我可能会深入研究,因为这个区间相当宽,利润空间也很大,最终价格可能与单点预测有显著差异。
预测区间与置信区间
在继续之前,我首先想澄清这两个定义之间的区别。当我开始学习保形预测时,这对我来说并不显而易见。由于我可能不是唯一一个感到困惑的人,所以我希望能提供一些额外的解释。
- A (1-α) 置信区间 [1] 是基于两个统计量,ŝ_{lb} 和 ŝ_{ub},的区间,具有大于(1-α)的概率包含我们试图估计的实际参数。这里的 θ 是一个参数(不是一个随机变量)。
ℙ([ŝ_{lb} ; ŝ_{ub}] ∋ θ) ≥ 1-α
- A (1-α) 预测区间 [2] 是基于两个统计量,ŝ_{lb} 和 ŝ_{ub},的区间,具有以下性质:目标随机变量有大于(1-α)的概率位于这个预测区间内。这里的 Y 是一个随机变量(不是一个参数)。
ℙ(Y∈[ŝ_{lb} ; ŝ_{ub}]) ≥ (1-α)
让我们考虑一个例子来说明它们的区别。假设有来自母分布 N(μ, σ²) 的一个 n 样本。ŝ 是 σ 的无偏估计量。
- μ 的对称 置信区间 为:
[
-qŝ/√(n) ; +q ŝ/√(n)]
- 对于来自同一分布 N(μ, σ²) 的 (n+1)th 随机变量 X(n+1),其对称的 预测区间 为:
[
-qŝ√(1+1/n)) ; +qŝ√(1+1/n)]
现在我们已经澄清了这些定义,让我们回到我们的目标:设计有见地的预测区间,以做出充分知情的决策。设计预测区间的方法有很多种 [2] [3]。我们将重点关注符合预测 [4]。
符合预测
符合预测被引入用来生成具有较弱理论保证的预测区间。它只要求点是可交换的,这比 i.i.d. 假设(独立同分布的随机变量)要弱。对数据分布和模型没有假设。通过将数据分为训练集和校准集,可以得到一个训练好的模型和一些非一致性分数,我们可以用它们来构建新的数据点的预测区间(在可交换性假设成立的前提下,提供理论上的覆盖保证)。
现在让我们考虑一个例子。我想要获得一些关于房价的预测区间。我考虑了来自 OpenML 的房价数据集 [5]。我使用了实现符合预测的 MAPIE 库 [6]。我训练了一个模型(我没有花时间优化它,因为这不是本文的目的)。我展示了测试集的预测点和区间以及实际价格。
有 3 个子图:
第一个图展示了单点预测(蓝色点)以及预测区间(垂直蓝线)与真实值(横坐标上的值)之间的关系。红色对角线是身份线。如果一条垂直线穿过红线,预测区间就包含了实际值,否则则不包含。
第二个图展示了预测区间的宽度。
第三个图展示了全局和局部覆盖率。覆盖率是指落在预测区间内的样本数量与总样本数的比值。全局覆盖率是指测试集所有点的比值。局部覆盖率是指测试集的子集上的比值。这些桶是通过实际价格的分位数创建的。

图片来自作者
我们可以看到,所有预测的预测宽度几乎相同。覆盖率为 94%,接近所选的 95%。然而,即使全局覆盖率接近理想值,如果我们看一下(我称之为)局部覆盖率(即对于价格几乎相同的数据点的子集的覆盖率),我们会发现对于昂贵的房屋,覆盖率很差(根据我的数据集,昂贵的房屋)。相反,对于便宜的房屋,覆盖率较好(根据我的数据集,便宜的房屋)。然而,对于便宜的房屋,洞察力非常有限。例如,对于一套便宜的房屋,预测区间可能是[0 ; 180000],这对做决策帮助不大。
本能上,我希望预测区间的宽度与预测值成正比,以便预测宽度能与预测结果匹配。这就是为什么我查看了其他更适合我的用例的非一致性分数。
自定义非一致性分数的符合预测
尽管我不是房地产专家,但对于预测区间,我有一些预期。正如前面所说,我希望它们在某种程度上与预测值成正比。我希望当价格较低时,预测区间较小,而当价格较高时,预测区间较大。
因此,对于这个用例,我将实现两个不符合度得分,这两个得分符合不符合度得分必须满足的条件[7](3.1 节和附录 C)。我从接口ConformityScore创建了两个类,该接口要求至少实现两个方法:get_signed_conformity_scores和get_estimation_distribution。get_signed_conformity_scores根据预测值和观察值计算不符合度得分。get_estimation_distribution计算估计的分布,然后用于获得预测区间(在提供所选覆盖率后)。我决定将我的第一个不符合度得分命名为PoissonConformityScore,因为它与泊松回归直观相关。当考虑泊松回归时,(Y-μ)/√μ具有 0 均值和方差为 1。同样,对于TweedieConformityScore类,当考虑 Tweedie 回归时,(Y-μ)/(μ^(p/2))具有 0 均值和方差为σ²(假设对所有观察值都是相同的)。在这两个类中,sym=False,因为不符合度得分不预期是对称的。此外,consistency_check=False,因为我知道这两个方法是一致的,并且满足必要的要求。
import numpy as np
from mapie._machine_precision import EPSILON
from mapie.conformity_scores import ConformityScore
from mapie._typing import ArrayLike, NDArray
class PoissonConformityScore(ConformityScore):
"""
Poisson conformity score.
The signed conformity score = (y - y_pred) / y_pred**(1/2).
The conformity score is not symmetrical.
y must be positive
y_pred must be strictly positive
This is appropriate when the confidence interval is not symmetrical and
its range depends on the predicted values.
"""
def __init__(
self,
) -> None:
super().__init__(sym=False, consistency_check=False, eps=EPSILON)
def _check_observed_data(
self,
y: ArrayLike,
) -> None:
if not self._all_positive(y):
raise ValueError(
f"At least one of the observed target is strictly negative "
f"which is incompatible with {self.__class__.__name__}. "
"All values must be positive."
)
def _check_predicted_data(
self,
y_pred: ArrayLike,
) -> None:
if not self._all_strictly_positive(y_pred):
raise ValueError(
f"At least one of the predicted target is negative "
f"which is incompatible with {self.__class__.__name__}. "
"All values must be strictly positive."
)
@staticmethod
def _all_positive(
y: ArrayLike,
) -> bool:
return np.all(np.greater_equal(y, 0))
@staticmethod
def _all_strictly_positive(
y: ArrayLike,
) -> bool:
return np.all(np.greater(y, 0))
def get_signed_conformity_scores(
self,
X: ArrayLike,
y: ArrayLike,
y_pred: ArrayLike,
) -> NDArray:
"""
Compute the signed conformity scores from the observed values
and the predicted ones, from the following formula:
signed conformity score = (y - y_pred) / y_pred**(1/2)
"""
self._check_observed_data(y)
self._check_predicted_data(y_pred)
return np.divide(np.subtract(y, y_pred), np.power(y_pred, 1 / 2))
def get_estimation_distribution(
self, X: ArrayLike, y_pred: ArrayLike, conformity_scores: ArrayLike
) -> NDArray:
"""
Compute samples of the estimation distribution from the predicted
values and the conformity scores, from the following formula:
signed conformity score = (y - y_pred) / y_pred**(1/2)
<=> y = y_pred + y_pred**(1/2) * signed conformity score
``conformity_scores`` can be either the conformity scores or
the quantile of the conformity scores.
"""
self._check_predicted_data(y_pred)
return np.add(y_pred, np.multiply(np.power(y_pred, 1 / 2), conformity_scores))
class TweedieConformityScore(ConformityScore):
"""
Tweedie conformity score.
The signed conformity score = (y - y_pred) / y_pred**(p/2).
The conformity score is not symmetrical.
y must be positive
y_pred must be strictly positive
This is appropriate when the confidence interval is not symmetrical and
its range depends on the predicted values.
"""
def __init__(self, p) -> None:
self.p = p
super().__init__(sym=False, consistency_check=False, eps=EPSILON)
def _check_observed_data(
self,
y: ArrayLike,
) -> None:
if not self._all_positive(y):
raise ValueError(
f"At least one of the observed target is strictly negative "
f"which is incompatible with {self.__class__.__name__}. "
"All values must be positive."
)
def _check_predicted_data(
self,
y_pred: ArrayLike,
) -> None:
if not self._all_strictly_positive(y_pred):
raise ValueError(
f"At least one of the predicted target is negative "
f"which is incompatible with {self.__class__.__name__}. "
"All values must be strictly positive."
)
@staticmethod
def _all_positive(
y: ArrayLike,
) -> bool:
return np.all(np.greater_equal(y, 0))
@staticmethod
def _all_strictly_positive(
y: ArrayLike,
) -> bool:
return np.all(np.greater(y, 0))
def get_signed_conformity_scores(
self,
X: ArrayLike,
y: ArrayLike,
y_pred: ArrayLike,
) -> NDArray:
"""
Compute the signed conformity scores from the observed values
and the predicted ones, from the following formula:
signed conformity score = (y - y_pred) / y_pred**(1/2)
"""
self._check_observed_data(y)
self._check_predicted_data(y_pred)
return np.divide(np.subtract(y, y_pred), np.power(y_pred, self.p / 2))
def get_estimation_distribution(
self, X: ArrayLike, y_pred: ArrayLike, conformity_scores: ArrayLike
) -> NDArray:
"""
Compute samples of the estimation distribution from the predicted
values and the conformity scores, from the following formula:
signed conformity score = (y - y_pred) / y_pred**(1/2)
<=> y = y_pred + y_pred**(1/2) * signed conformity score
``conformity_scores`` can be either the conformity scores or
the quantile of the conformity scores.
"""
self._check_predicted_data(y_pred)
return np.add(
y_pred, np.multiply(np.power(y_pred, self.p / 2), conformity_scores)
)
然后,我使用了与之前相同的例子。除了默认的不符合度得分(我在图中将其命名为AbsoluteConformityScore)外,我还考虑了这两个额外的不符合度得分。

图片来源:作者
如我们所见,全球覆盖率都接近所选的 95%。我认为这些小的变化是由于在训练集和测试集之间的随机拆分造成的。然而,预测区间的宽度在不同方法之间存在显著差异,局部覆盖率也是如此。再一次,我不是房地产专家,但我认为对于最后一个不符合度得分(图中的第三列),预测区间更为现实。对于新的两个不符合度得分,预测区间对于廉价房屋来说相当狭窄(尽管略低于 95%但覆盖率良好),而对于昂贵房屋则相当宽。这是为了(几乎)达到所选的覆盖率(95%)。我们来自TweedieConformityScore不符合度得分的新预测区间在整个价格范围内具有良好的局部覆盖率,并且更具洞察力,因为预测区间不会不必要地宽泛。
结论
预测区间可能有助于做出更明智的决策。共形预测是一个工具,它可以通过理论覆盖保证和仅有的弱假设(数据可交换性)来构建预测区间。在考虑常用的非一致性得分时,尽管全球覆盖是所需的,但局部覆盖可能会因使用场景的不同而与所选覆盖差异显著。这就是为什么我最终考虑了适应所考虑使用场景的其他非一致性得分。我展示了如何在共形预测库 MAPIE 中实现这一点,并说明了这样做的好处。一个合适的非一致性得分有助于获得更有洞察力的预测区间(在目标值范围内具有良好的局部覆盖)。
参考文献
在频率主义统计学中,置信区间(CI)是一个未知参数的估计范围。置信区间……
在统计推断中,特别是在预测推断中,预测区间是一个估计的区间……
机器学习模型是强大的工具——但我们如何量化与其相关的不确定性呢……
共形预测(CP)是一个用于不确定性量化的机器学习框架,可以生成预测区间……
OpenML 是一个开放平台,用于共享数据集、算法和实验——共同学习如何更好地学习。
www.openml.org [## GitHub - scikit-learn-contrib/MAPIE: 一个与 scikit-learn 兼容的预测区间估计模块…
一个与 scikit-learn 兼容的预测区间估计模块。 - GitHub - scikit-learn-contrib/MAPIE: A…
利用 Rust 的速度将 Python 库安装速度提高 100 倍

使用 Midjourney 生成的图像
uv的快速概述 — 这是 Python 包管理器中的新星。
·发布于 Towards Data Science ·阅读时间 6 分钟 ·2024 年 3 月 7 日
--
对于数据科学家和 Python 程序员来说,pip不需要介绍。作为一个包管理器,它要么是首选解决方案,要么是我们在寻找最佳解决方案时的起点。
趣味提醒 🚨:虽然pip不需要介绍,但我最近才了解到,它其实代表着“Pip Installs Packages”或“Preferred Installer Program”。
pip并不是唯一的包管理器。下面是市场上最流行的一些工具:
-
pip— Python 的标准包管理器,Python 安装时默认包含。 -
conda— 一个专为数据科学设计的包和环境管理系统。除了 Python,它还可以安装其他语言编写的包(例如 R)。 -
poetry— 它旨在通过提供高效的依赖解析和虚拟环境等功能来简化 Python 的依赖管理。 -
pipenv— 这个工具将pip(用于安装包)与virtualenv(用于创建隔离环境)结合在一起。
使用 Tablib 处理 Python 中的简单表格数据
有时候,浅层抽象比性能更有价值
·发表于 Towards Data Science ·阅读时间 11 分钟·2024 年 11 月 27 日
--

图片由David Clode提供,来自Unsplash
概述
-
简介——什么是 Tablib?
-
处理数据集
-
导入数据
-
导出数据
-
动态列
-
格式化工具
-
总结
简介——什么是 Tablib?
多年来,我一直在使用 Pandas 和 PySpark 等工具进行数据导入、处理和导出。这些工具非常适合复杂的数据转换和大数据量(当数据适合内存时,Pandas)。然而,我通常会在以下情况下使用这些工具:
-
数据量相对较小。考虑到数据行数远低于 100,000 行。
-
性能不是问题。考虑到一个偶发任务,或者是每天午夜重复执行的任务,我不在乎它是需要 20 秒还是 5 分钟。
-
不需要复杂的转换。可以简单地将 20 个格式相同的 JSON 文件导入,将它们堆叠在一起,然后导出为 CSV 文件。
用户行为序列建模:从注意力机制到变换器及其发展
将推荐系统转变为类似 LLM 的系统的探索
·发表于 Towards Data Science ·11 分钟阅读·2024 年 7 月 15 日
--

使用 ChatGPT 生成的图片
用户行为序列是推荐系统中最强大的输入之一:你下一次点击、阅读、观看、播放或购买,很可能与几分钟前、几小时、几天、几个月甚至几年前你所点击、阅读、观看、播放或购买的内容至少有某种程度的关联。
历史上,用于建模这类用户互动序列的传统方法是池化:例如,2016 年一篇经典的 YouTube 论文描述了一个系统,该系统将最近观看的 50 个视频,收集它们在嵌入表中的表示,并通过求和池化将这些表示合并成一个单一的特征向量。为了节省内存,这些序列视频的嵌入表与候选视频的嵌入表共享。

YouTube 的推荐系统通过对用户观看的视频序列进行求和池化处理。Covinton et al 2016
这种简单的方法大致对应于自然语言处理领域的词袋模型:它能工作,但远非理想。池化方法没有考虑输入的顺序性,也没有考虑用户历史中项目与我们需要排序的候选项目的相关性,也没有考虑任何时间信息:一个……
企业工具的用户研究:行业中的人机交互
作为一名在行业中致力于为企业构建工具的 HCI 研究科学家的见解
·发表于Towards Data Science ·阅读时间 7 分钟·2024 年 11 月 6 日
--
企业和组织通常致力于为商业运营构建定制工具。这些工具包括仪表板、为特定系统定制的用户界面、使复杂算法可访问的工具等。评估此类工具的质量非常重要。在 HCI 课程和主流 HCI 研究中,受控用户研究是评估工具有效性的最常见方式。
在此背景下,受控用户研究是围绕工具所支持的任务和该工具目标用户群体进行设计的。工具的不同条件也会设计成某种形式的基准,以便对工具进行比较。该工具会根据用户在工具上完成任务的情况进行评估。不同的度量标准,如用户完成任务的时长,将被测量并用于比较工具的不同条件。
然而,在人机交互(HCI)课程中所教授的内容与行业、企业环境中实际应用的 HCI 之间存在差距。在这篇简短的博客中,我将概述作为一名 HCI 研究员,在一个由 NLP 和数据库研究人员组成的多元化团队中,专注于会话和语言 AI 系统及其评估的行业工作经历所获得的一些见解。
USGS DEM 文件:如何使用 Python 加载、合并和裁剪
快速成功数据科学
数字高程数据准备的快速指南
·发表于 Towards Data Science ·阅读时间 11 分钟·2024 年 12 月 17 日
--

美国地质调查局数字高程模型(作者提供)
数字高程模型(DEM) 是地球表面的 3D 数字表示。它记录了不同点的 海拔高度,这些点可以通过传统的测量技术、激光雷达(LIDAR)、卫星影像、航拍或 GPS 测量来获得。DEM 通常以栅格格式存储,每个像素都有一个高程值。
DEM 是在需要使用物理景观进行规划和决策的领域中至关重要的工具。其应用包括地形绘图、水文建模、城市规划、环境研究,以及为虚拟现实、游戏和模拟创建逼真的地形模型。
美国地质调查局(USGS)提供了可从国家地图下载器下载的免费 DEM。这些是无缝栅格文件,称为 DEM TIFFs 或 GeoTIFFs。虽然与标准的照片 TIFF 类似,但这些文件包含嵌入的地理空间元数据。
在这个 快速成功数据科学 项目中,我们将为选定的地理区域准备 DEM TIFF 文件。这不会像从网站下载文件那样简单。现有的 USGS 文件很少与我们所选的区域完全匹配...
使用 LangChain、LangSmith 和 OpenAI 的 GPT-4o 的 JSON 代理
开发一个聊天机器人来回答关于 JSON 数据集的问题
·发表于Towards Data Science ·阅读时长 8 分钟·2024 年 7 月 1 日
--

图片来源:OpenAI,作者编辑
挑战
最近,我面临了一个挑战:开发一个能够使用 LLM 和预定义的学生数据(JSON 格式)回答关于 JSON 数据集问题的聊天机器人。故意地,这些 JSON 数据结构混乱,在某些情况下非常嵌套,可能代表来自旧系统的数据库调用。
为什么不直接将 JSON 上传到 ChatGPT?
简单地在数据上调用 ChatGPT 可能无法产生最佳结果,而且也不具备可扩展性。由于 JSON 数据是一种高度结构化和嵌套的数据形式,关于数据点之间复杂关系的提问更容易产生虚假回答。我开始研究一个能够使用专门工具执行任务并处理这些复杂性高级代理。
选择工具
最受欢迎的 LLM 框架之一是 LangChain,它提供了先进的提示工具。它还通过 LangSmith 提供了扩展功能,LangSmith 是一个服务(用最简单的术语来说),提供一个仪表盘来监控 LLM 调用和代理运行——并详细列出行动链。
使用多模态文档 ML 模型查询您的文档
利用 mPLUG-Owl 文档理解模型的强大功能来向您的文档提问
·发布于 Towards Data Science ·阅读时长 9 分钟·2024 年 4 月 11 日
--
本文将讨论最近发布的阿里巴巴文档理解模型,该模型包含了模型权重和数据集。它是一个强大的模型,能够执行多种任务,如文档问答、信息提取和文档嵌入,使其成为处理文档时的一个有用工具。本文将本地实现该模型,并在不同任务上进行测试,以便对其性能和实用性进行评价。

本文将讨论文档理解领域的最新模型。图片由 ChatGPT 提供。OpenAI。(2024)。ChatGPT(4)[大型语言模型]。chat.openai.com
目录
· 动机
· 任务
· 本地运行模型
· 模型测试
∘ 数据
∘ 测试第二个,最右侧的收据:
∘ 测试第二个,最右侧的收据:
∘ 测试第一个,最左侧的讲义笔记:
∘ 测试第二个,最右侧的讲义笔记
· 我对该模型的想法
· 结论
动机
2024 年使用 iPad 进行数据科学工作
探讨使用苹果流行平板进行数据科学工作的利与弊
·发表于Towards Data Science ·10 分钟阅读·2024 年 9 月 1 日
--

由作者制作的标题卡片
几年前(2021 年),我发布了一篇博客,介绍了我如何将 iPad 用于我的数据科学工作,所以我想分享一个后续更新,因为,是的,我至今仍然在使用 iPad 进行自己的工作,时间已经到了 2024 年 9 月。我在 2021 年的文章结尾写下了以下注解:
于是我们终于来到了大家一直在等待的问题,那就是,我们是否终于可以完全依赖 iPad 来进行数据科学工作了?不幸的答案最终是否定的。因为 iPadOS 并不是一个传统的操作系统,很多事情我无法做,特别是我依赖命令行工具、Docker 镜像等。我很想看看苹果是否能克服这些障碍,但不幸的是,2021 年 1 月时这些问题依然存在。
到最后,这仍然是不幸的事实。苹果的 iPadOS 操作系统仍然限制了 iPad 的多功能性,使其无法像使用其他操作系统(如 Windows、macOS 或 Linux)的电脑那样灵活。
尽管如此,自从我最初发布文章以来,iPad 经历了许多更新,包括 iPadOS 本身以及其他可以在 iPad 上支持的软件服务。有趣的是,很多内容与那时的…
使用贝叶斯建模预测欧冠联赛
体育分析
贝叶斯推断在现实世界中的应用
·发布于Towards Data Science ·15 分钟阅读·2024 年 2 月 20 日
--

图片由Anders Krøgh Jørgensen提供,来源于Unsplash
哦,欧冠联赛。可能是吸引最多球迷的比赛,无论他们支持哪个球队。这里是最强对最强的较量。比赛几乎可以保证精彩……而结果几乎无法预测。
但这不应该阻止我们去尝试。
前几天我在翻看旧的大学作业时,发现了一篇激发我写这篇文章的评估报告,我们将利用贝叶斯推断创建一个模型,尝试预测接下来的欧冠联赛比赛:16 强的首回合(说实话,这个模型也可以用来预测任何阶段的比赛)。
目标是通过贝叶斯统计在一个我认为既有趣又有娱乐性的实际案例中建立一个模型。
无论你是否了解贝叶斯推断,这篇文章都是为你准备的。即使你已经了解我接下来要分享的所有内容,最终的预测至少能让你在首回合比赛结束后,或者恭喜我,或者嘲笑我。
今天我们将讨论以下内容:
- 贝叶斯推断与建模
使用因果图回答因果问题
因果 AI,探讨因果推理与机器学习的融合
·发表于Towards Data Science ·阅读时间 9 分钟·2024 年 1 月 31 日
--

图片由GuerrillaBuzz提供,来自Unsplash
这系列文章讲的是什么?
欢迎来到我的《因果 AI》系列文章,在这里我们将探讨因果推理如何与机器学习模型融合。你将会看到在不同商业场景中应用的多个实际案例。
简介
本文提供了因果图潜力的实践性介绍。
本文面向任何希望更好理解以下内容的人:
-
因果图是什么以及它们是如何工作的
-
一个通过 Python 展示如何构建因果图的案例研究
-
它们与机器学习的比较
-
主要挑战和未来的思考
完整的笔记本可以在这里找到:
[## causal_ai/notebooks/using causal graphs to answer causal questions.ipynb 在主分支 ·…
本项目介绍了因果 AI 及其如何推动商业价值。- causal_ai/notebooks/using causal graphs to…
什么是因果图?
因果图帮助我们区分因果关系与相关性。它们是因果推理/因果机器学习/因果 AI 工具箱的关键部分,可用于回答因果问题。
因果图通常被称为 DAG(有向无环图),它包含节点和边——边连接的是具有因果关系的节点。
确定因果图的方式有两种:
-
专家领域知识
-
因果发现算法
目前,我们假设我们有专家领域知识来确定因果图(我们将在后续讲解因果发现算法)。
为什么我们不能仅仅使用机器学习?
机器学习的目标是尽可能准确地分类或预测,前提是有一些训练数据。机器学习算法没有动力确保它使用的特征与目标之间具有因果关系。也无法保证每个特征的方向(正向/负向效应)和强度与真实的数据生成过程相符。机器学习不会考虑以下情况:
-
假相关——当两个变量有一个共同原因时,它们会有一个假相关,例如:高温增加冰淇淋销量和鲨鱼袭击的数量。
-
混杂因子——一个变量同时影响你的处理和结果,例如:需求影响我们在营销上花费的金额以及新客户的注册数量。
-
冲突变量——一个变量受到两个独立变量的影响,例如:客户关怀质量 -> 用户满意度 <- 公司规模
-
中介变量——两个变量通过一个中介变量(间接)关联,例如:规律性运动 -> 心血管健康(中介变量) -> 总体健康
由于这些复杂性以及机器学习的黑箱性质,我们无法确信它能够回答因果问题。
结构因果模型
给定已知的因果图和观察数据,我们可以训练一个结构因果模型(SCM)。一个 SCM 可以看作是一系列因果模型,每个节点一个模型。每个模型使用一个节点作为目标,并使用其直接父节点作为特征。如果我们观察数据中的关系是线性的,SCM 将是一个线性方程组。这可以通过一系列线性回归模型来建模。如果我们观察数据中的关系是非线性的,可以通过一系列增强树来建模。
与传统机器学习的关键区别在于,SCM 建模的是因果关系,并考虑了假相关、混杂因子、冲突变量和中介变量。
通常为每个非根节点使用加性噪声模型(ANM)(意味着它至少有一个父节点)。这使我们能够使用一系列机器学习算法(加上噪声项)来估计每个非根节点。
Y := f(X) + N
根节点可以使用随机模型来建模,以描述其分布。
结构因果模型(SCM)可以视为一个生成模型,它能够生成新的数据样本——这使得它能够回答一系列因果问题。它通过从根节点采样数据并通过图传播数据来生成新数据。
回答因果问题
结构因果模型的价值在于,它通过计算反事实和模拟干预来回答因果问题:
-
反事实:使用历史观测数据计算如果我们改变了 x,y 会发生什么。例如,如果我们上个月将呼叫等待时间减少 20%,客户流失数量会有什么变化?
-
干预:与反事实非常相似(且常常互换使用),但干预模拟的是未来可能发生的情况。例如,如果明年我们将呼叫等待时间减少 20%,客户流失数量会有什么变化?
呼叫中心案例研究
客户服务团队监控多个关键绩效指标(KPI)。其中之一是呼叫等待时间。增加呼叫中心员工数量将减少呼叫等待时间。
但减少呼叫等待时间会如何影响客户流失率?这是否足以抵消增加呼叫中心员工的成本?
数据科学团队被要求构建并评估业务案例。
确定因果图
关注的群体是拨打进线电话的客户。以下是每天收集的时间序列数据:

图片由作者提供
在这个例子中,我们使用时间序列数据,但因果图也可以与客户级别的数据一起使用。
在这个例子中,我们利用专家领域知识来确定因果图。
# Create node lookup for channels
node_lookup = {0: 'Demand',
1: 'Call waiting time',
2: 'Call abandoned',
3: 'Reported problems',
4: 'Discount sent',
5: 'Churn'
}
total_nodes = len(node_lookup)
# Create adjacency matrix - this is the base for our graph
graph_actual = np.zeros((total_nodes, total_nodes))
# Create graph using expert domain knowledge
graph_actual[0, 1] = 1.0 # Demand -> Call waiting time
graph_actual[0, 2] = 1.0 # Demand -> Call abandoned
graph_actual[0, 3] = 1.0 # Demand -> Reported problems
graph_actual[1, 2] = 1.0 # Call waiting time -> Call abandoned
graph_actual[1, 5] = 1.0 # Call waiting time -> Churn
graph_actual[2, 3] = 1.0 # Call abandoned -> Reported problems
graph_actual[2, 5] = 1.0 # Call abandoned -> Churn
graph_actual[3, 4] = 1.0 # Reported problems -> Discount sent
graph_actual[3, 5] = 1.0 # Reported problems -> Churn
graph_actual[4, 5] = 1.0 # Discount sent -> Churn

图片由作者提供
数据生成过程
接下来,我们需要为案例研究生成数据。
我们想要生成一些数据,以便比较使用因果图与机器学习(为了简单起见,使用岭回归)计算反事实的结果。
正如我们在上一节中确定的因果图,我们可以利用这些知识来创建数据生成过程。
def data_generator(max_call_waiting, inbound_calls, call_reduction):
'''
A data generating function that has the flexibility to reduce the value of node 0 (Call waiting time) - this enables us to calculate ground truth counterfactuals
Args:
max_call_waiting (int): Maximum call waiting time in seconds
inbound_calls (int): Total number of inbound calls (observations in data)
call_reduction (float): Reduction to apply to call waiting time
Returns:
DataFrame: Generated data
'''
df = pd.DataFrame(columns=node_lookup.values())
df[node_lookup[0]] = np.random.randint(low=10, high=max_call_waiting, size=(inbound_calls)) # Demand
df[node_lookup[1]] = (df[node_lookup[0]] * 0.5) * (call_reduction) + np.random.normal(loc=0, scale=40, size=inbound_calls) # Call waiting time
df[node_lookup[2]] = (df[node_lookup[1]] * 0.5) + (df[node_lookup[0]] * 0.2) + np.random.normal(loc=0, scale=30, size=inbound_calls) # Call abandoned
df[node_lookup[3]] = (df[node_lookup[2]] * 0.6) + (df[node_lookup[0]] * 0.3) + np.random.normal(loc=0, scale=20, size=inbound_calls) # Reported problems
df[node_lookup[4]] = (df[node_lookup[3]] * 0.7) + np.random.normal(loc=0, scale=10, size=inbound_calls) # Discount sent
df[node_lookup[5]] = (0.10 * df[node_lookup[1]] ) + (0.30 * df[node_lookup[2]]) + (0.15 * df[node_lookup[3]]) + (-0.20 * df[node_lookup[4]]) # Churn
return df
# Generate data
np.random.seed(999)
df = data_generator(max_call_waiting=600, inbound_calls=10000, call_reduction=1.00)
sns.pairplot(df)

图片由作者提供
训练 SCM
现在我们有了表示因果图的邻接矩阵和一些数据。我们使用来自 dowhy Python 包的 gcm 模块来训练 SCM。
考虑选择何种因果机制用于根节点和非根节点是非常重要的。如果你查看我们的数据生成函数,你会看到所有的关系都是线性的。因此,选择岭回归应该足够了。
# Setup graph
graph = nx.from_numpy_array(graph_actual, create_using=nx.DiGraph)
graph = nx.relabel_nodes(graph, node_lookup)
# Create SCM
causal_model = gcm.InvertibleStructuralCausalModel(graph)
causal_model.set_causal_mechanism('Demand', gcm.EmpiricalDistribution()) # Root node
causal_model.set_causal_mechanism('Call waiting time', gcm.AdditiveNoiseModel(gcm.ml.create_ridge_regressor())) # Non-root node
causal_model.set_causal_mechanism('Call abandoned', gcm.AdditiveNoiseModel(gcm.ml.create_ridge_regressor())) # Non-root node
causal_model.set_causal_mechanism('Reported problems', gcm.AdditiveNoiseModel(gcm.ml.create_ridge_regressor())) # Non-root node
causal_model.set_causal_mechanism('Discount sent', gcm.AdditiveNoiseModel(gcm.ml.create_ridge_regressor())) # Non-root
causal_model.set_causal_mechanism('Churn', gcm.AdditiveNoiseModel(gcm.ml.create_ridge_regressor())) # Non-root
gcm.fit(causal_model, df)
你也可以使用自动分配功能,自动分配因果机制,而不是手动分配。
关于 gcm 包的更多信息,请参阅文档:
日期:2023 年 12 月 25 日 版本:v0.11.1 相关资源:源代码仓库 | 问题与建议 | PyWhy 组织 | DoWhy…
岭回归比较
我们还使用岭回归来帮助创建基准比较。我们可以回顾数据生成器,看到它正确估算了每个变量的系数。然而,除了直接影响客户流失外,通话等待时间还通过弃单、报告问题和发送折扣间接影响客户流失。
当涉及到估计反事实时,看看 SCM 与岭回归的比较将会很有趣。
# Ridge regression
y = df['Churn'].copy()
X = df.iloc[:, 1:-1].copy()
model = RidgeCV()
model = model.fit(X, y)
y_pred = model.predict(X)
print(f'Intercept: {model.intercept_}')
print(f'Coefficient: {model.coef_}')
# Ground truth[0.10 0.30 0.15 -0.20]

图片来源:作者
估计真实情况
在我们继续使用因果图和岭回归计算反事实之前,我们需要一个真实情况基准。我们可以使用数据生成器,在将通话等待时间减少 20%之后,创建反事实样本。
我们无法在现实世界问题中做到这一点,但这种方法允许我们评估因果图和岭回归的有效性。
# Set call reduction to 20%
reduce = 0.20
call_reduction = 1 - reduce
# Generate counterfactual data
np.random.seed(999)
df_cf = data_generator(max_call_waiting=600, inbound_calls=10000, call_reduction=call_reduction)
估计反事实
我们现在可以使用我们的三种方法估计如果我们将通话等待时间减少 20%会发生什么:
-
真实情况(来自数据生成器)
-
岭回归
-
因果图
我们看到岭回归显著低估了对客户流失的影响,而因果图与真实情况非常接近。
# Ground truth counterfactual
ground_truth = round((df['Churn'].sum() - df_cf['Churn'].sum()) / df['Churn'].sum(), 2)
# Causal graph counterfactual
df_counterfactual = gcm.counterfactual_samples(causal_model, {'Call waiting time': lambda x: x*call_reduction}, observed_data=df)
causal_graph = round((df['Churn'].sum() - df_counterfactual['Churn'].sum()) / (df['Churn'].sum()), 3)
# Ridge regression counterfactual
ridge_regression = round((df['Call waiting time'].sum() * 1.0 * model.coef_[0] - (df['Call waiting time'].sum() * call_reduction * model.coef_[0])) / (df['Churn'].sum()), 3)

图片来源:作者
未来的考虑
这是一个简单的例子,旨在让你思考因果图的强大作用。
对于更复杂的情况,有几个挑战需要考虑:
-
假设了哪些前提条件,违反这些前提条件会有什么影响?
-
如果我们没有专家领域的知识来识别因果图怎么办?
-
如果存在非线性关系会怎样?
-
多重共线性有多严重?
-
如果某些变量具有滞后效应,会怎样?
-
我们如何处理高维数据集(大量变量)?
所有这些内容将在未来的博客中覆盖。
资源
如果你有兴趣进一步了解因果 AI,我强烈推荐以下资源:
通过揭示因果原理并将其与强大的机器学习方法结合,解开因果推断与因果发现的神秘面纱…
www.packtpub.com](https://www.packtpub.com/product/causal-inference-and-discovery-in-python/9781804612989?source=post_page-----5fd1dd82fa90--------------------------------) [## Python 中的因果推断
在线营销增加一美元会带来多少买家?哪些客户只有在提供某些条件时才会购买…
ChatGPT 对作者有什么评价呢?😊
“认识一下 Ryan,一位经验丰富的首席数据科学家,专注于在商业环境中运用因果分析技术,涵盖营销、运营和客户服务等领域。他的专长在于揭示因果关系的复杂性,从而推动明智的决策和战略改进,服务于不同的组织职能。”
如果你想继续探索因果人工智能,请关注我——在下一篇文章中,我们将回顾一步,深入探讨如何在缺乏专家领域知识的情况下,利用因果发现来估计因果图。
使用聚类算法进行球员招募
体育分析
哪些球员能够帮助富勒姆克服他们的主要缺陷?
·发布于Towards Data Science ·阅读时间:9 分钟·2024 年 4 月 15 日
--

前些天,我很幸运地参加了一个由 xfb Analytics[1]、Transfermarkt[2]和 Football Forum Hungary[3]组织的足球分析黑客松。
由于我们最近获得了分享工作的许可,我决定写一篇关于我使用的方法的博客文章。
目标是选择一支英超球队,分析他们的比赛风格,突出两项缺陷,并准备两份各包含 5 名球员的名单,这些球员可以帮助球队改善。前提是我们必须填补两个不同的位置(因此是“每个位置的两份 5 人名单”)。
然后,从这两份名单中,我们需要选出每个位置的最佳目标,并进一步解释为什么他们是各自位置的最佳人选。
最终结果必须是现实的,并且两名球员的总价格必须低于 6000 万(我们给出了他们的 Transfermarkt 估值)。
现在你已经知道了这个项目的内容,我想谈谈我的方法。我是一个热爱足球的数据科学人员,所以我不得不使用 Python 进行某种技术分析或建模。
使用决策树进行探索性数据分析
将决策树添加到您的 EDA 中,并从一开始就获得极好的见解
·发表于 Towards Data Science ·阅读时间 7 分钟·2024 年 6 月 20 日
--

图片由 Mila Tovar 提供,来源:Unsplash
引言
决策树(DT) 是最直观的机器学习算法。
我说出来了。这只是我的观点。但我相信这也是数据科学领域中一种常见的感受。
决策树(…)机器学习方法,也通过一系列简单选择做出复杂决策。[Brett Lantz,《用 R 进行机器学习》]
决策树在运筹学和数据科学领域得到广泛应用,其成功的原因在于它遵循了与人类决策过程相似的过程。这个过程基于一个流程图,每个节点将对给定变量做出一个简单的二元决策,直到我们做出最终决定。
一个简单的例子:购买 T 恤。如果我想买一件衬衫,我可能会考虑一些变量,比如价格、品牌、尺寸和颜色。所以我从预算开始我的决策过程:
-
如果价格超过 30 美元,我就不买。否则,我会买。
-
一旦我找到了价格低于 30 美元的商品,我希望它来自...
使用双重机器学习和线性规划优化治疗策略
因果 AI,探讨将因果推理融入机器学习
·发表于 Towards Data Science ·阅读时间:11 分钟·2024 年 4 月 26 日
--

图片由 Jordan McDonald 提供,来源:Unsplash
本系列文章的主题是什么?
欢迎来到我的因果 AI 系列文章,我们将探讨因果推理如何融入机器学习模型。预计我们将深入探索在不同商业场景中的多种实际应用。
在上一篇文章中,我们探讨了使用双重机器学习消除治疗效应偏差。这次,我们将进一步深入探讨双重机器学习的潜力,重点是使用双重机器学习和线性规划优化治疗策略。
如果你错过了上一篇关于双重机器学习的文章,可以在这里查看:
因果 AI,探讨将因果推理融入机器学习
towardsdatascience.com
介绍
本文将展示如何使用双重机器学习和线性规划来优化治疗策略:
预计你将广泛了解以下内容:
-
为什么企业需要优化治疗策略。
-
条件平均治疗效应(CATE)如何帮助个性化治疗策略(也叫做提升建模)。
-
线性规划如何在预算约束下优化治疗分配。
-
一个 Python 案例研究,演示了我们如何使用双重机器学习估计 CATE,以及如何使用线性规划优化治疗策略。
完整的笔记本可以在这里找到:
[## causal_ai/notebooks/estimating average treatment effects with double machine learning.ipynb at main…
这个项目介绍了因果 AI 及其如何推动商业价值。- causal_ai/notebooks/estimating average…
优化治疗策略
在大多数企业中,常常会有一个问题:“为了最大化未来销售并最小化成本,如何为客户提供最佳治疗?”。
让我们通过一个简单的例子来分解这个想法。
你的业务在线销售袜子。你销售的不是必需品,所以你需要鼓励现有客户重复购买。你用来实现这一目标的主要手段是发送折扣。因此,在这种情况下,治疗策略就是发送折扣:
-
10% 折扣
-
20% 折扣
-
50% 折扣
每个折扣的投资回报率不同。如果你回想一下上一篇关于平均治疗效应的文章,你可能会看到我们如何为每个折扣计算 ATE,然后选择回报率最高的一个。
然而,如果我们有异质性治疗效应怎么办——治疗效应在不同子群体之间有所不同。
这时我们需要开始考虑条件平均治疗效应(CATE)!
条件平均治疗效应(CATE)
CATE
CATE 是治疗或干预对不同子群体的平均影响。ATE 主要关注“这种治疗有效吗?”,而 CATE 让我们能够将问题转变为“我们应该治疗谁?”。
我们“根据”控制特征来调整治疗效应,以便治疗效应根据客户特征变化。
回想一下我们发送折扣的例子。如果之前下单量较多的客户对折扣的反应更好,我们可以根据这一客户特征来调整治疗策略。
值得指出的是,在营销领域,估计 CATE 通常被称为提升建模(Uplift Modelling)。
使用双重机器学习估计 CATE
我们在上一篇文章中已经介绍了 DML,但如果你需要复习一下:
“第一阶段:
-
处理模型(去偏差): 用于估计处理分配概率的机器学习模型(通常称为倾向得分)。然后计算处理模型的残差。
-
结果模型(去噪): 用于仅通过控制特征估计结果的机器学习模型。然后计算结果模型的残差。
第二阶段:
- 处理模型的残差用于预测结果模型的残差。
我们可以使用双重机器学习,通过将控制特征(X)与第二阶段模型中的处理效应交互来估计 CATE。

用户生成的图像
这可能非常强大,因为我们现在能够获得客户级的处理效应!
线性规划
它是什么?
线性规划是一种优化方法,可用于在给定一些约束条件的情况下,找到线性函数的最优解。它通常用于解决运输、调度和资源分配问题。你可能会看到的一个更通用的术语是运筹学。
让我们通过一个简单的例子来分解线性规划:
-
决策变量: 这些是我们希望估计其最优值的未知量——社交媒体、电视和付费搜索的营销支出。
-
目标函数: 我们试图最小化或最大化的线性方程——营销投资回报率(ROI)。
-
约束条件: 对决策变量的一些限制,通常通过线性不等式表示——总营销支出介于£100,000 和£500,000 之间。
所有约束条件的交集形成了一个可行区域,这是满足给定约束条件的所有可能解的集合。线性规划的目标是找到可行区域内的点,以优化目标函数。
分配问题
分配问题是线性规划问题的一种特定类型,目标是将一组“任务”分配给一组“代理”。让我们通过一个例子来生动展示:
你进行了一项实验,将不同的折扣发送给 4 个随机分配的现有客户组(其中第 4 组实际上没有收到任何折扣)。你建立了 2 个 CATE 模型——(1)估计优惠价值如何影响订单金额,(2)估计优惠价值如何影响成本。
-
代理:你的现有客户群体
-
任务:是否向他们发送 10%、20%或 50%的折扣
-
决策变量:二元决策变量
-
目标函数:总订单金额减去成本
-
约束条件 1:每个代理最多被分配 1 个任务
-
约束条件 2:成本 ≥ £10,000
-
约束条件 3:成本 ≤ £100,000

用户生成的图像
我们基本上想要在给定的整体成本约束下,找出每个客户的最优处理方式。线性规划可以帮助我们做到这一点!
值得注意的是,这个问题是“NP 难”的,属于一类问题,这类问题至少和 NP(非确定性多项式时间)中最难的问题一样难。
线性规划是一个非常棘手但富有回报的话题。我已尝试介绍基本概念以帮助我们入门——如果你想了解更多,推荐这个资源:
[## Python 实战线性规划:优化与 Python - Real Python
在本教程中,你将学习如何使用线性规划库在 Python 中实现优化。线性…
realpython.com](https://realpython.com/linear-programming-python/?source=post_page-----920c20a29553--------------------------------)
OR Tools
OR-Tools 是 Google 开发的一个开源包,可以解决一系列线性规划问题,包括任务分配问题。我们将在文章后续演示其应用。
OR-Tools 套件提供了运筹学软件库和用于约束优化、线性…的 API。
developers.google.com](https://developers.google.com/optimization?source=post_page-----920c20a29553--------------------------------)
市场营销案例研究
背景
我们将继续使用任务分配问题的示例,并说明如何在 Python 中解决此问题。
数据生成过程
我们设置了一个具有以下特征的数据生成过程:
-
困难的干扰参数 (b)
-
治疗效应异质性(tau)
X 特征是治疗前获取的客户特征:

用户生成的图像
T 是一个二进制标志,表示客户是否接受了优惠。我们创建了三种不同的处理交互,以便模拟不同的治疗效果。

用户生成的图像
def data_generator(tau_weight, interaction_num):
# Set number of observations
n=10000
# Set number of features
p=10
# Create features
X = np.random.uniform(size=n * p).reshape((n, -1))
# Nuisance parameters
b = (
np.sin(np.pi * X[:, 0] * X[:, 1])
+ 2 * (X[:, 2] - 0.5) ** 2
+ X[:, 3]
+ 0.5 * X[:, 4]
+ X[:, 5] * X[:, 6]
+ X[:, 7] ** 3
+ np.sin(np.pi * X[:, 8] * X[:, 9])
)
# Create binary treatment
T = np.random.binomial(1, expit(b))
# treatment interactions
interaction_1 = X[:, 0] * X[:, 1] + X[:, 2]
interaction_2 = X[:, 3] * X[:, 4] + X[:, 5]
interaction_3 = X[:, 6] * X[:, 7] + X[:, 9]
# Set treatment effect
if interaction_num==1:
tau = tau_weight * interaction_1
elif interaction_num==2:
tau = tau_weight * interaction_2
elif interaction_num==3:
tau = tau_weight * interaction_3
# Calculate outcome
y = b + T * tau + np.random.normal(size=n)
return X, T, tau, y
我们可以使用数据生成器来模拟三种不同的治疗,每种治疗都有不同的治疗效果。

用户生成的图像
np.random.seed(123)
# Generate samples for 3 different treatments
X1, T1, tau1, y1 = data_generator(0.75, 1)
X2, T2, tau2, y2 = data_generator(0.50, 2)
X3, T3, tau3, y3 = data_generator(0.90, 3)
与上一篇文章一样,数据生成过程的 Python 代码基于 Uber Causal ML 包中的合成数据创建器:
[## causalml/causalml/dataset/regression.py 在主分支 · uber/causalml
使用机器学习算法进行提升建模和因果推断 - causalml/causalml/dataset/regression.py 在…
使用 DML 估计 CATE
我们接着使用 LightGBM 训练了三种 DML 模型作为灵活的第一阶段模型。这应该能帮助我们捕捉困难的干扰参数,同时正确计算治疗效果。
注意我们是如何通过 X 而不是 W 传递 X 特征的(与上一篇文章中通过 W 传递 X 特征不同)。通过 X 传递的特征将在第一阶段和第二阶段模型中都使用——在第二阶段模型中,这些特征用来与治疗残差创建交互项。
np.random.seed(123)
# Train DML model using flexible stage 1 models
dml1 = LinearDML(model_y=LGBMRegressor(), model_t=LGBMClassifier(), discrete_treatment=True)
dml1.fit(y1, T=T1, X=X1, W=None)
# Train DML model using flexible stage 1 models
dml2 = LinearDML(model_y=LGBMRegressor(), model_t=LGBMClassifier(), discrete_treatment=True)
dml2.fit(y2, T=T2, X=X2, W=None)
# Train DML model using flexible stage 1 models
dml3 = LinearDML(model_y=LGBMRegressor(), model_t=LGBMClassifier(), discrete_treatment=True)
dml3.fit(y3, T=T3, X=X3, W=None)
当我们绘制实际值与估计 CATE 时,可以看到模型表现得相当不错。
# Create a figure and subplots
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
# Plot scatter plots on each subplot
sns.scatterplot(x=dml1.effect(X1), y=tau1, ax=axes[0])
axes[0].set_title('Treatment 1')
axes[0].set_xlabel('Estimated CATE')
axes[0].set_ylabel('Actual CATE')
sns.scatterplot(x=dml2.effect(X2), y=tau2, ax=axes[1])
axes[1].set_title('Treatment 2')
axes[1].set_xlabel('Estimated CATE')
axes[1].set_ylabel('Actual CATE')
sns.scatterplot(x=dml3.effect(X3), y=tau3, ax=axes[2])
axes[2].set_title('Treatment 3')
axes[2].set_xlabel('Estimated CATE')
axes[2].set_ylabel('Actual CATE')
# Add labels to the entire figure
fig.suptitle('Actual vs Estimated')
# Show plots
plt.show()

用户生成的图片
朴素优化
我们将首先将其作为优化问题进行探索。我们有三种治疗方法可以提供给客户。下面我们为每个治疗的成本创建一个映射,并设定一个总体成本约束。
# Create mapping for cost of each treatment
cost_dict = {'T1': 0.1, 'T2': 0.2, 'T3': 0.3}
# Set constraints
max_cost = 3000
我们可以估计每个客户的 CATE,然后初步选择每个客户的最佳治疗方法。然而,选择最佳治疗方法并不能使我们保持在最大成本约束内。因此,我们将选择 CATE 最高的客户,直到达到最大成本约束。
# Concatenate features
X = np.concatenate((X1, X2, X3), axis=0)
# Estimate CATE for each treatment using DML models
Treatment_1 = dml1.effect(X)
Treatment_2 = dml2.effect(X)
Treatment_3 = dml3.effect(X)
cate = pd.DataFrame({"T1": Treatment_1, "T2": Treatment_2, "T3": Treatment_3})
# Select the best treatment for each customer
best_treatment = cate.idxmax(axis=1)
best_value = cate.max(axis=1)
# Map cost for each treatment
best_cost = pd.Series([cost_dict[value] for value in best_treatment])
# Create dataframe with each customers best treatment and associated cost
best_df = pd.concat([best_value, best_cost], axis=1)
best_df.columns = ["value", "cost"]
best_df = best_df.sort_values(by=['value'], ascending=False).reset_index(drop=True)
# Naive optimisation
best_df_cum = best_df.cumsum()
opt_index = best_df_cum['cost'].searchsorted(max_cost)
naive_order_value = round(best_df_cum.iloc[opt_index]['value'], 0)
naive_cost_check = round(best_df_cum.iloc[opt_index]['cost'], 0)
print(f'The total order value from the naive treatment strategy is {naive_order_value} with a cost of {naive_cost_check}')

用户生成的图片
使用线性规划优化治疗策略
我们首先创建一个数据框,包含每个客户每个治疗的成本。
# Cost mapping for all treatments
cost_mapping = {'T1': [cost_dict["T1"]] * 30000,
'T2': [cost_dict["T2"]] * 30000,
'T3': [cost_dict["T3"]] * 30000}
# Create DataFrame
df_costs = pd.DataFrame(cost_mapping)
现在是时候使用 OR 工具包来解决这个分配问题了!代码接受以下输入:
-
成本约束
-
包含每个客户每个治疗的成本的数组
-
包含每个客户每个治疗的估计订单价值的数组
代码输出一个数据框,包含每个客户的潜在治疗方案,并有一列指示哪一个是最优分配。
solver = pywraplp.Solver.CreateSolver('SCIP')
# Set constraints
max_cost = 3000
min_cost = 3000
# Create input arrays
costs = df_costs.to_numpy()
order_value = cate.to_numpy()
num_custs = len(costs)
num_treatments = len(costs[0])
# x[i, j] is an array of 0-1 variables, which will be 1 if customer i is assigned to treatment j.
x = {}
for i in range(num_custs):
for j in range(num_treatments):
x[i, j] = solver.IntVar(0, 1, '')
# Each customer is assigned to at most 1 treatment.
for i in range(num_custs):
solver.Add(solver.Sum([x[i, j] for j in range(num_treatments)]) <= 1)
# Cost constraints
solver.Add(sum([costs[i][j] * x[i, j] for j in range(num_treatments) for i in range(num_custs)]) <= max_cost)
solver.Add(sum([costs[i][j] * x[i, j] for j in range(num_treatments) for i in range(num_custs)]) >= min_cost)
# Objective
objective_terms = []
for i in range(num_custs):
for j in range(num_treatments):
objective_terms.append((order_value[i][j] * x[i, j] - costs[i][j] * x[i, j] ))
solver.Maximize(solver.Sum(objective_terms))
# Solve
status = solver.Solve()
assignments = []
values = []
if status == pywraplp.Solver.OPTIMAL or status == pywraplp.Solver.FEASIBLE:
for i in range(num_custs):
for j in range(num_treatments):
# Test if x[i,j] is 1 (with tolerance for floating point arithmetic).
if x[i, j].solution_value() > -0.5:
assignments.append([i, j])
values.append([x[i, j].solution_value(), costs[i][j] * x[i, j].solution_value(), order_value[i][j]])
# Create a DataFrame from the collected data
df = pd.DataFrame(assignments, columns=['customer', 'treatment'])
df['assigned'] = [x[0] for x in values]
df['cost'] = [x[1] for x in values]
df['order_value'] = [x[2] for x in values]
df

用户生成的图片
在保持£3k 成本约束的同时,我们可以使用优化后的治疗策略产生£18k 的订单价值。这比朴素方法高出 36%!
opt_order_value = round(df['order_value'][df['assigned'] == 1].sum(), 0)
opt_cost_check = round(df['cost'][df['assigned'] == 1].sum(), 0)
print(f'The total order value from the optimised treatment strategy is {opt_order_value} with a cost of {opt_cost_check}')

用户生成的图片
最后的想法
今天我们介绍了使用双重机器学习和线性规划来优化治疗策略。以下是一些总结想法:
- 我们已经介绍了线性双重机器学习(Linear DML),你可能还想探索其他方法,这些方法更适合处理第二阶段模型中复杂的交互效应:
[## EconML/notebooks/Double Machine Learning Examples.ipynb at main · py-why/EconML
ALICE(自动化学习与因果关系与经济学智能)是微软研究院的一个项目,旨在……
-
但也要记住,你不一定非得使用双重机器学习(DML),像 T-Learner 或 DR-Learner 这样的其他方法也可以使用。
-
为了让这篇文章简洁易读,我没有调整超参数 — 随着问题和方法复杂性的增加,我们需要更加关注这一部分。
-
线性规划/分配问题是 NP 难题,因此如果你有大量的客户群和/或多个处理方案,这部分代码可能需要很长时间才能运行。
-
将线性规划/分配问题应用到日常工作流中可能具有挑战性 — 一种替代方法是定期运行优化,并根据结果学习最优策略,从而创建一个细分,用于日常工作流。
如果你想继续探索因果 AI,请关注我 — 在下一篇文章中,我们将探讨如何在定价和营销优化问题中估计非线性处理效应。
使用评估优化 RAG 流水线:从分块和嵌入到 LLM
使用 Milvus 向量数据库的最佳实践 RAG,第二部分
·发布于 Towards Data Science ·阅读时间 7 分钟·2024 年 7 月 12 日
--

由作者使用 www.bing.com/images/create 创建的图像。内容凭证:由 AI 生成 ∙ 2024 年 7 月 9 日上午 10:04。
检索增强生成(RAG)是一种在 AI 驱动的聊天机器人中使用自身数据的有用技术。在这篇博客文章中,我将介绍三种关键策略,帮助你从 RAG 中获得最大收益,并评估每种策略,找出最佳组合。
对于那些只想了解 TL;DR 结论的读者:RAG 准确度的最大提升来自于探索不同的分块策略。
-
通过更改分块策略提高 84% 📦
-
通过更改嵌入模型提高 20% 🤖
-
通过更改 LLM 模型提高 6% 🧪
让我们深入探讨每种策略,使用 RAG 组件评估找到适用于实际应用的最佳表现者!🚀📚
我将使用 Milvus(由中国初创公司 Zilliz 开发的向量数据库)文档 公共网页作为文档数据,并使用 Ragas 作为评估方法。请参阅我之前的 博客,了解如何使用 RAGAS。本博客的其余部分组织如下:
-
文本分块策略
-
嵌入模型
-
LLM(生成)模型
使用生成式 AI 从文章中自动创建视频讲座
使用 Gemini + 文本转语音 + MoviePy 创建视频,以及这对生成式 AI(GenAI)迅速变得有用的意义
·发布于Towards Data Science ·10 分钟阅读·2024 年 9 月 22 日
--
像大多数人一样,我对NotebookLM 及其生成播客的能力感到震惊。然后,我开始思考:“他们是怎么做到的?我在哪里可以获得这种魔力?”复制这一过程会有多容易?
目标:从文章中创建视频讲座
我不想创建播客,但我常常希望能够从我的博客文章中生成幻灯片和视频讲座——有些人喜欢翻阅幻灯片,另一些人则喜欢观看视频,这将是一个很好的方式来满足他们的需求。在本文中,我将展示如何做到这一点。
本文的完整代码在 GitHub 上——如果你想和我一起操作,可以参考。目标是从这篇文章创建视频:
视频是使用本文描述的代码自动创建的。视频由作者生成。
1. 初始化 LLM
我将使用 Google Gemini Flash,因为(a)它是目前最便宜的前沿 LLM,(b)它是多模态的,能够读取和理解图像,(c)它支持受控生成,这意味着我们可以确保 LLM 的输出符合预期的结构。
import pdfkit
import os
import google.generativeai as genai
from dotenv import load_dotenv
load_dotenv("../genai_agents/keys.env")
genai.configure(api_key=os.environ["GOOGLE_API_KEY"])
请注意,我使用的是 Google Generative AI,而不是 Google Cloud Vertex AI。这两个包是不同的。Google 的那个支持用于受控生成的 Pydantic 对象;而 Vertex AI 目前只支持 JSON。
2. 获取文章的 PDF 文件
我使用 Python 下载了文章并保存为 PDF,然后将其上传到 Gemini 可以读取的临时存储位置:
ARTICLE_URL = "https://lakshmanok.medium...."
pdfkit.from_url(ARTICLE_URL, "article.pdf")
pdf_file = genai.upload_file("article.pdf")
不幸的是,由于 Medium 的一些原因,pdfkit 无法获取文章中的图片(可能是因为它们是 webm 格式而不是 png 格式……)。所以,我的幻灯片将仅基于文章的文本,而不包含图片。
3. 创建 JSON 格式的讲义
在这里,我需要的数据格式是每一页幻灯片都有一个标题、要点和一组讲义。整个讲座也应该有一个标题和归属。
class Slide(BaseModel):
title: str
key_points: List[str]
lecture_notes: str
class Lecture(BaseModel):
slides: List[Slide]
lecture_title: str
based_on_article_by: str
让我们告诉 Gemini 我们希望它做什么:
lecture_prompt = """
You are a university professor who needs to create a lecture to
a class of undergraduate students.
* Create a 10-slide lecture based on the following article.
* Each slide should contain the following information:
- title: a single sentence that summarizes the main point
- key_points: a list of between 2 and 5 bullet points. Use phrases, not full sentences.
- lecture_notes: 3-10 sentences explaining the key points in easy-to-understand language. Expand on the points using other information from the article.
* Also, create a title for the lecture and attribute the original article's author.
"""
这个提示词相当简单——要求 Gemini 阅读文章,提取要点并创建讲义。
现在,调用模型,传入 PDF 文件,并要求它填充所需的结构:
model = genai.GenerativeModel(
"gemini-1.5-flash-001",
system_instruction=[lecture_prompt]
)
generation_config={
"temperature": 0.7,
"response_mime_type": "application/json",
"response_schema": Lecture
}
response = model.generate_content(
[pdf_file],
generation_config=generation_config,
stream=False
)
上述代码需要注意的几点:
-
我们将提示词作为系统提示传递,这样就不需要在每次输入时都传入新的提示词。
-
我们将期望的响应类型指定为 JSON,模式为 Pydantic 对象
-
我们将 PDF 文件发送给模型,并告诉它生成响应。我们会等待它完成(无需流式传输)。
结果是 JSON 格式,因此可以将其提取为一个 Python 对象:
lecture = json.loads(response.text)
例如,这是第三张幻灯片的样子:
{'key_points': [
'Silver layer cleans, structures, and prepares data for self-service analytics.',
'Data is denormalized and organized for easier use.',
'Type 2 slowly changing dimensions are handled in this layer.',
'Governance responsibility lies with the source team.'
],
'lecture_notes': 'The silver layer takes data from the bronze layer and transforms it into a usable format for self-service analytics. This involves cleaning, structuring, and organizing the data. Type 2 slowly changing dimensions, which track changes over time, are also handled in this layer. The governance of the silver layer rests with the source team, which is typically the data engineering team responsible for the source system.',
'title': 'The Silver Layer: Data Transformation and Preparation'
}
4. 转换为 PowerPoint
我们可以使用 Python 包 pptx 创建带有讲义和要点的演示文稿。创建幻灯片的代码如下:
for slidejson in lecture['slides']:
slide = presentation.slides.add_slide(presentation.slide_layouts[1])
title = slide.shapes.title
title.text = slidejson['title']
# bullets
textframe = slide.placeholders[1].text_frame
for key_point in slidejson['key_points']:
p = textframe.add_paragraph()
p.text = key_point
p.level = 1
# notes
notes_frame = slide.notes_slide.notes_text_frame
notes_frame.text = slidejson['lecture_notes']
结果是一个 PowerPoint 演示文稿,格式如下:

从要点和讲义生成的 PowerPoint 文件。截图由作者提供。
并不复杂,但如果你要做演讲,这绝对是一个很好的起点,可以用于编辑。
5. 大声朗读笔记并保存音频
好吧,我们的灵感来源于播客,所以我们来看看如何只制作一个有人总结文章的音频。
我们已经有了讲义,所以我们来为每一张幻灯片创建音频文件。
这是用来将一些文本转化为 AI 语音并朗读出来的代码。我们将生成的音频保存为 mp3 文件:
from google.cloud import texttospeech
def convert_text_audio(text, audio_mp3file):
"""Synthesizes speech from the input string of text."""
tts_client = texttospeech.TextToSpeechClient()
input_text = texttospeech.SynthesisInput(text=text)
voice = texttospeech.VoiceSelectionParams(
language_code="en-US",
name="en-US-Standard-C",
ssml_gender=texttospeech.SsmlVoiceGender.FEMALE,
)
audio_config = texttospeech.AudioConfig(
audio_encoding=texttospeech.AudioEncoding.MP3
)
response = tts_client.synthesize_speech(
request={"input": input_text, "voice": voice, "audio_config": audio_config}
)
# The response's audio_content is binary.
with open(audio_mp3file, "wb") as out:
out.write(response.audio_content)
print(f"{audio_mp3file} written.")
上述代码发生了什么?
-
我们使用的是 Google Cloud 的文字转语音 API
-
要求它使用标准的美国口音女性声音。如果你是在做播客,你可以在这里传入一个“说话者映射”,每个说话者对应一种声音。
-
然后我们将其作为输入文本,要求它生成音频。
-
将音频保存为 mp3 文件。请注意,这必须与音频编码匹配。
现在,遍历幻灯片并传入讲义,生成音频:
for slideno, slide in enumerate(lecture['slides']):
text = f"On to {slide['title']} \n"
text += slide['lecture_notes'] + "\n\n"
filename = os.path.join(outdir, f"audio_{slideno+1:02}.mp3")
convert_text_audio(text, filename)
filenames.append(filename)
结果是一堆音频文件。你可以使用 pydub 将它们连接在一起:
combined = pydub.AudioSegment.empty()
for audio_file in audio_files:
audio = pydub.AudioSegment.from_file(audio_file)
combined += audio
# pause for 4 seconds
silence = pydub.AudioSegment.silent(duration=4000)
combined += silence
combined.export("lecture.wav", format="wav")
但结果是我并不需要这么做。每张幻灯片的单独音频文件,正是我需要用来创建视频的内容。当然,对于播客,你会希望有一个单一的 mp3 或 wav 文件。
6. 创建幻灯片图像
有点烦人的是,使用 Python 将 PowerPoint 幻灯片渲染为图像没有简单的办法。你需要一台安装了 Office 软件的机器来完成这个任务——这不是一种容易自动化的操作。也许我应该使用 Google Slides…无论如何,渲染图像的简单方法是使用 Python 图像库(PIL):
def text_to_image(output_path, title, keypoints):
image = Image.new("RGB", (1000, 750), "black")
draw = ImageDraw.Draw(image)
title_font = ImageFont.truetype("Coval-Black.ttf", size=42)
draw.multiline_text((10, 25), wrap(title, 50), font=title_font)
text_font = ImageFont.truetype("Coval-Light.ttf", size=36)
for ptno, keypoint in enumerate(keypoints):
draw.multiline_text((10, (ptno+2)*100), wrap(keypoint, 60), font=text_font)
image.save(output_path)
结果图像效果不算很好,但还算能用(你可以看出来,现在没有人支付我写生产级代码了):

与音频片段一起使用的图像是这样的。图像由作者生成。
7. 创建视频
现在我们已经有了一组音频文件和图像文件,可以使用 Python 包 moviepy 来创建一个视频片段:
clips = []
for slide, audio in zip(slide_files, audio_files):
audio_clip = AudioFileClip(f"article_audio/{audio}")
slide_clip = ImageClip(f"article_slides/{slide}").set_duration(audio_clip.duration)
slide_clip = slide_clip.set_audio(audio_clip)
clips.append(slide_clip)
full_video = concatenate_videoclips(clips)
现在我们可以写出来了:
full_video.write_videofile("lecture.mp4", fps=24, codec="mpeg4",
temp_audiofile='temp-audio.mp4', remove_temp=True)
最终结果?我们从 article.pdf 自动生成了四个产物:
lecture.json lecture.mp4 lecture.pptx lecture.wav
包括:
-
一个包含关键点、讲义等内容的 JSON 文件
-
一个可以修改的 PowerPoint 文件。幻灯片上有关键点,幻灯片的注释部分则包含“讲义”内容。
-
一个由 AI 语音朗读讲义的音频文件
-
一部 mp4 电影(我上传到 YouTube)包含音频和图像。这就是我打算创建的视频讲座。
很酷吧?
8. 这对软件未来的意义
我们作为一个社区,正在探索这项非常酷的技术(生成式 AI)可以用于什么。显然,你可以用它来创建内容,但它创建的内容适合用于头脑风暴,而不能直接使用。三年的技术进步并没有解决 GenAI 生成空洞内容和不成熟代码的问题。
这带我们进入了 GenAI 所打开的一些辅助功能。而这些功能非常有用。本文展示了 GenAI 的四个功能。
(1) 将非结构化数据转化为结构化数据
《Attention》论文是为了解决翻译问题而写的,结果发现基于 Transformer 的模型在翻译方面表现非常优秀。我们不断发现这个技术的应用场景。但不仅仅是日语到英语,还包括Java 11 到 Java 17,文本到 SQL,文本到语音,不同数据库方言之间的转换,……现在,还可以将文章转为音频脚本。事实证明,这就是利用 GenAI 创建播客、讲座、视频等内容的起点。
我所要做的就是提示 LLM 从文章中构建一系列幻灯片内容(关键点、标题等),它就做到了。它甚至以结构化格式返回数据,便于从计算机程序中使用。具体来说,GenAI 在将非结构化数据转化为结构化数据方面非常出色。
(2) 代码搜索和编程帮助如今变得更加出色
另一个 GenAI 表现得非常出色的领域是动态适配代码示例。我并不是每天都写代码来创建演示文稿、文本转语音或电影剪辑。两年前,我可能会使用谷歌搜索,查看 Stack Overflow 页面,并手动调整代码。现在,谷歌搜索直接给我提供了可以直接使用的代码:

谷歌搜索返回了适应我特定查询的代码示例。截图来自作者。
当然,如果我使用的是 Python IDE(而不是 Jupyter 笔记本),我完全可以避免搜索步骤——我只需写一个注释,系统就会为我生成代码。这非常有帮助,并且加快了使用通用 API 的开发速度。
(3) GenAI Web 服务既强大又易于使用
让我们不要忘记,我使用了谷歌云文本转语音服务将我的音频脚本转化为实际的音频文件。文本转语音本身就是一个生成式 AI 模型(也是翻译超级能力的另一个例子)。谷歌的 TTS 服务于2018 年推出(此后可能有所改进),是最早投入生产的生成式 AI 服务之一,并通过 API 提供使用。
在本文中,我使用了两个生成式 AI 模型——TTS 和 Gemini,它们作为 Web 服务提供。我所需要做的就是调用它们的 API。
(4) 提供最终用户定制化比以往任何时候都更加容易
我没有这样做,但你可以稍微眯起眼睛,看到未来的方向。如果我将演示创建、音频创建和视频创建的代码封装成服务,那么我本可以让一个提示生成调用这些服务的函数调用。同时,还可以加上一个请求处理代理,允许你通过文本来更改幻灯片的外观或视频中朗读者的声音。
为你构建的软件添加开放式定制化变得极其容易。
总结
受到 NotebookLM 播客功能的启发,我开始构建一个应用程序,将我的文章转化为视频讲座。关键步骤是提示 LLM 从文章中生成幻灯片内容,另一个 GenAI 模型将音频脚本转化为音频文件,并利用现有的 Python API 将它们整合成一个视频。
本文展示了 GenAI 解锁的四项能力:各种类型的翻译、编程帮助、强大的 Web 服务和最终用户定制化。
我很喜欢能够轻松快速地从我的文章中创建视频讲座。但我更为兴奋的是,我们在手中拥有的这一新工具中不断发现的潜力。
进一步阅读
-
本文的完整代码:
github.com/lakshmanok/lakblogs/blob/main/genai_seminar/create_lecture.ipynb -
我将这篇文章转换为视频的源文章:
lakshmanok.medium.com/what-goes-into-bronze-silver-and-gold-layers-of-a-medallion-data-architecture-4b6fdfb405fc -
生成的视频:
youtu.be/jKzmj8-1Y9Q -
结果发现Sascha Heyer 写了一篇关于如何使用 GenAI 生成播客的文章,这正是 Notebook LM 的使用场景。他的方法和我有些相似,区别在于没有视频,只有音频。在一个有趣的变化中,他使用了自己的声音作为播客的讲解者之一!
-
当然,这里是用该视频中展示的技术创建的文章视频演讲。理想情况下,我们会从文章中提取代码片段和图片,但这只是一个开始……
使用生成性 AI 来策划约会推荐
利用 Amazon Bedrock、Google Places、LangChain 和 Streamlit
·发表于 Towards Data Science ·阅读时长 9 分钟·2024 年 3 月 21 日
--

图片来自 Unsplash,由 Hitesh Dewasi 提供
生成性 AI 的真正力量体现在它帮助人们简化或自动化日常活动和任务时。一些这些任务的例子包括:电子邮件/消息摘要、简历生成器等。具体来说,上周我正在为我和女朋友策划一个有趣的约会之夜,我意识到没有一种固定的工具可以根据我们的兴趣提供完整的约会创意。
我当然可以使用 Google 然后拼凑出一些不同的地方,但这样需要花费时间和大量的研究(我总是最终跑到 Reddit 上去,哈哈)。另外,我也尝试过直接使用类似 ChatGPT 的工具,但意识到很多建议涉及的地方都比较过时,实际上并未包含最新最好的建议,因为模型是在较早的时间进行训练的。
在我之前的文章中,我们讨论了如何利用 LangChain Agents 配合 Spotify API 生成音乐推荐。今天,我们将采用一种不同的 API,并且采用一种稍微不同于我们在音乐推荐解决方案中使用的基于 Agent 的方法来策划约会之夜创意。
使用生成式 AI 从杂乱数据中获取见解
使用大型语言模型提取可操作的见解,即使元数据不准确。
·发表于Towards Data Science ·32 分钟阅读·2024 年 9 月 3 日
--

使用生成式 AI 分析杂乱数据的最佳实践清单
本文分享了我们如何在公司使用生成式 AI 分析数据以更有效地运营的最佳实践。虽然花了一些时间,但我最终获得了 Salesforce 的营销、法律、安全和公关团队的批准,才能发布这篇文章。希望这能帮助你加速你的数据分析。
本文中的所有图表和图形都具有方向性并准确传达概念,但数据已被匿名化。
见解盒子
-
使用 LLM 进行数据过滤: 无需在源头清理数据;可以使用 LLM 在数据流中进行清洗。
-
使用 GPT 进行 Python 自动化: 提取、修改和可视化数据通常需要中级的 Python 技能,但 GPT 可以自动化并加速这些任务。
-
领域特定的工单过滤: 当元数据不可靠时,通过处理这些工单的支持工程师来过滤工单。
-
可靠的数据提取: 专注于提取像描述和时间戳这样的可靠字段,因为这些字段不容易出错。
-
使用 GPT 进行数据匿名化: 在将数据发送到公共 API 之前,使用 GPT 与开源匿名化库对数据进行匿名化。
-
精心选择分隔符: 选择输出分隔符时要小心,确保它们不会干扰语言模型的处理,并通过去除所选的分隔符来清理输入数据。
-
微调 GPT 提示以提高准确性: 在进行全面分析之前,先评估并微调提示在已知工单描述上的表现。
-
上下文数据限制: 需要注意 GPT 在处理无关数据块时的上限;保持在识别的限制下方 10% 以内,以避免数据丢失。
-
与 GPT 头脑风暴 KPI: 在提取元数据后,使用 GPT 进行头脑风暴并创建有意义的 KPI 以供可视化。
-
简化数据可视化: 利用 GPT 编写 Python 代码来创建图表,将分析过程简化并保持版本控制,在一个环境中完成,而不是使用单独的可视化工具。
总结
你是否曾经面对大量由人类输入的凌乱和自由格式数据,试图理清其中的含义?这是一项极其令人头痛且耗时的工作,除非你有专门的时间来仔细处理,否则很可能最终只是对数据进行抽样,得到一些表面的洞察,且这些洞察可能是基于不可靠的元数据。通常来说,效果不大。
不难看出,专门用于理清混乱数据的大型语言模型(LLM)可以在这里提供帮助。本文分享了从这种实现中总结的最佳实践,涵盖了多种概念,如使用 GPT 清洗数据、进行分析并创建有用图表的最有效方法,管理个人可识别信息(PII)的方法、生产级提示设计、绕过 GPT 的“前额皮质”瓶颈等!
但在此之前,我想先分享一下这个经历如何完全改变了我原本对数据质量的坚定看法:
我曾经认为,为了提高数据质量,必须从源头解决问题,也就是从参与系统(Systems of Engagement)入手。例如,我曾认为,对于销售 CRM,我们必须确保销售和市场团队一开始就录入高质量的数据和元数据。同样,对于客户支持,我们必须确保客户支持工程师在工单的创建、持续和关闭过程中,选择所有正确的元数据(如工单原因代码、客户影响等)。
在我最近的经历之后,这些信念已经被彻底打破。你完全可以在源头拥有杂乱无章的数据,只要有正确的引导,大型语言模型(LLMs)仍然能够理清数据,得出有意义的洞察!
无需在源头清洗数据:就像水过滤器一样,你只需将一个 LLM 插入数据流中,进行净化!

GPT 可以像水过滤器一样,接收包含脏元数据的信息并加以净化,从中提取出有意义的洞察。
从长远来看,在源头处填充准确的元数据确实有帮助,尽管要注意,这些过程是需要协调和审核的,且非常耗时。
操作原则
为了进行这项分析,我遵循了两个简单的原则:
-
避免打乱我团队的当前交付:虽然让我团队中的某个成员做这项分析会更容易,但那会打乱团队在其他正在进行项目上的进度。我必须在做产品开发主管的日常工作同时,自己搞定所有的分析工作。
-
用生成式 AI 做一切:大语言模型在数据处理方面非常强大,特别是在这个使用场景下,它能够从杂乱的数据中提取价值。它们在编程方面也比我强得多。告诉别人做事并检查结果比进入状态亲自做要容易得多。这样,你即使是兼职工作,也能有所成就。
也用 GPT 来编写分析代码!
结论: 提取、修改和可视化数据需要中级的 Python 编程技能,但现在,GPT 可以帮你更快地完成所有这些,甚至可以做到更高质量。快用吧!
在下面的图片中,我展示了所有需要编写代码来转换数据并调用 GPT API 以从工单详情中提取洞察的步骤(以绿色字体标出)。最棒的是,我不需要从零开始编写这些代码。我使用 GPT 帮我编写了它!

基于 LLM 的工单分析的所有步骤
我是如何进行编码的
虽然我对 Python 有一定的了解,但使用 GPT 编写代码让我至少提高了 3 倍的效率。我通过 GPT 编写代码的方法非常简单:我并没有使用它来执行任何代码,而是直接告诉 GPT 数据的样子,让它帮我写代码。我还要求 GPT 在代码的不同位置自由地插入打印语句,打印出变量的值。然后我将代码复制到我笔记本上的 Jupyter Notebook 中并执行。例如,我的提示语句大致如下:
我: 这是我在分析中会使用的所有文件。我将列举它们,并在提示语中用编号来称呼它们。
1. “All Interacted Tickets.xlsx”
2. “Copy of Ticket Dump — Received from Ops.xlsx”
3. “verifying_accurate_list_of_ops_people_supporting_my_space.xlsx”
它们都在../data/目录下。
编写 Python 代码,将文件 1、2 和 3 导入到 pandas 数据框中。忽略所有包含“pivot”字样的工作表,但将其余数据提取出来。用每个 Excel 文件中工作表本身的文本命名数据框,使用蛇形命名法....
然后,GPT 会输出代码,例如:
import pandas as pd
import os
# Define the file paths
files = {
1\. "All Interacted Tickets.xlsx",
2\. "Copy of Ticket Dump - Received from Ops.xlsx",
3\. "Verifying_accurate_list_of_ops_people_supporting_my_space.xlsx"
}
def snake_case(s):
return s.lower().replace(" ", "_")
def read_excel_sheets(file_path, ignore_pivot=True):
xl = pd.ExcelFile(file_path)
sheet_names = xl.sheet_names
….<code clipped>
然后,我会将这些代码拿到本地运行。如果出现错误,我会把输出(包括打印语句的输出)粘贴到“相同的”聊天中,因为它保留了记忆,GPT‘大多数’情况下能够修复我的问题。不过,在某些情况下,GPT 会卡住(你可以通过它不断推荐相同的解决方案来识别这一点),这时我会开始用更多问题来询问它:
我: df = df[1:] 是做什么的
GPT: df = df[1:] 用于删除数据框的第一行,通常当读取 Excel 文件时,这一行是表头……
我: df = df[1:] 是不正确的,我不希望你删除第一行。事实上,这一行应该作为每个数据框的表头。
因此,如果像我一样使用 GPT 开发代码,并且是通过带外方式进行的,那么对 Python 的适度了解有助于解决一些 GPT 无法理解的代码问题,因为它几乎无法理解上下文。
请注意,如果你使用多代理框架,代理之间可能会相互传递代码并自动解决这些缺陷。在未来的文章中,我将展示我的本地数据工程和分析环境设置,展示如何在你的笔记本电脑上设置这个多代理框架。如果你对此感兴趣,请在评论中告诉我。
运营票据分析的逐步方法
在经过多次迭代和“失误”后,我提出了以下步骤!换句话说,如果我需要重新进行这项分析,我会遵循以下结构来简化过程。因此,我将这个结构呈现给你,以便你能从中受益。别客气!
第一步:筛选相关票据
核心观点: 如果元数据不可靠,那么根据处理过票据的支持工程师筛选与你领域相关的票据是最好的选择。

为你的团队筛选票据
(只有在你在中型或大型组织工作,并且是多个团队之一,使用共享运营团队时,才需要此步骤)
将票据的工作集减少到仅与你的部门或团队相关的部分,是在你的公司中有大量运营票据时必须采取的重要筛选步骤。你将通过 LLMs 发送这些票据,如果你使用的是像 GPT4 这样的付费服务,你只想发送与你相关的部分!
然而,当你拥有不良元数据时,推导出有效的票据工作集是一个问题。支持工程师可能没有被指示标明这些票据属于哪些团队,或者没有很好的票据分类供选择,所以你所能使用的仅是一些自由格式的数据和一些自动收集的基本“事实”。这些事实包括谁创建了票据,谁负责处理,票据创建时的时间戳,状态变化(如果幸运的话),以及票据关闭。可能还有其他“主观”数据,例如票据的优先级。收集这些信息是可以的,但它们可能不准确,因为票据创建者往往会将自己打开的每个票据标记为“紧急”或“高优先级”。根据我的经验,通过 LLMs 推导出实际优先级往往更为中立,尽管这仍然可能出错,后面会进一步讲解。
换句话说,坚持“事实”。
在通常有助于你减少工作集的“事实”中,支持工程师的姓名通常是其中之一,支持工程师创建或处理工单的情况也包含在内。由于支持工程师通常专注于特定领域(如数据技术、CRM、Workday 等),第一步是与支持经理合作,识别所有与您领域相关的工单的支持工程师姓名。
然后,使用一个可以识别的键(例如他们的工作邮箱地址),你可以将一堆工单筛选成与你部门相关的子集,并拉取与这些工单相关的“事实”元数据。
完成这一步骤后,你还会得到第一个统计数据:在一段时间内,我所在领域的工单被打开的数量。
第 2 步:提取“描述”字段及其他元数据
关键要点: 虽然工单创建者可能会搞错很多元数据,但她不能搞错描述字段,因为这是她与支持团队沟通问题及其业务影响的唯一途径。这一点非常重要,因为理解自由流动数据正是 GPT 的强项。因此,专注于提取描述字段以及其他“难以出错”的事实数据,如工单的开始和结束时间等。

通过元数据丰富筛选过的工单,特别是描述字段
大多数工单系统,如 Jira 服务管理、Zendesk、Service Now 等,允许你下载工单元数据,包括长的、多行的描述字段。(在我工作的公司,我们使用的定制系统不太幸运。)然而,几乎所有这些系统都有一个一次可以下载的最大工单数量。一个更自动化的方式,也是我采取的方式,是通过 API 提取这些数据。在这种情况下,你需要获取 Step1 中由支持工程师处理的已整理好的工单集合,然后循环遍历每个工单,调用 API 来获取其元数据。
其他一些系统允许你通过类似 ODBC 的接口发出 SQL(或者 Salesforce 产品中的 SOQL)查询,这很酷,因为你可以使用 WHERE 子句将第 1 步和第 2 步合并为一步。以下是一个伪代码示例:
SELECT ticket_number, ticket_description, ticket_creation_date, blah blah
FROM ticket_table
WHERE ticket_owners include "johndoe@company.com, janedang@company.com" ...
你明白了……
将这些数据保存为 MS-Excel 格式,并存储在磁盘上。
为什么选择 MS-Excel?我喜欢将表格数据“序列化”成 MS-Excel 格式,因为这样可以避免在将数据导入 Python 代码时遇到的转义或重复分隔符问题。Excel 格式将每个数据点编码到自己的“单元格”中,这样就不会因为文本中包含的特殊字符/分隔符而导致解析错误或列错位。此外,当将这些数据导入 Python 时,我可以使用 Pandas(一种流行的表格数据处理库)通过简单的 Excel 导入选项将 Excel 数据导入到数据框中。
第 3 步:将数据转换为 GPT 友好的格式(JSON)
总结: JSON 既易于人类阅读,又便于机器解析,安全无误,易于排查问题,而且 GPT 可以最少出错地进行操作。此外,当你丰富数据时,你可以继续为相同的 JSON 结构添加新字段。这是非常美妙的!
"16220417": {
"description": "Hi Team, \nThe FACT_XYZ_TABLE has not refreshed in time. Typically the data is available at 10am PST, but when I see the job, it has been completing several hours late consistently for the last few weeks. Also, it is 11am, and I see that it is still in running state!\n\n It is critical that this table is completed in time, so we have enough time to prepare an important sales executive report by 8am every morning. Please treat this with urgency.",
"opened_priority": "P2 - Urgent",
"origin": "Helpdesk ticket portal",
"closedDate": "2024-02-26T07:04:54.000Z",
"createdDate": "2024-02-01T09:03:43.000Z",
"ownerName": "Tom Cruise (Support Engineer)",
"ownerEmail": "tcruise@shoddy.com",
"contactName": "Shoddy Joe (Stakeholder)",
"contactEmail": "sjoe@shoddy.com",
"createdByName": "Shoddy Joe (Stakeholder)",
"createdByEmail": "sjoe@shoddy.com",
"ticket_status": "closed"
},
上面的片段展示了一个示例的 JSON 格式化票务元数据,其中票号作为键,指向一个包含进一步键值对元数据的对象。文件中会有很多这种类型的 JSON 块,每个票据一个。
经过多次试验和调整,我意识到,让 GPT 为我编写数据处理代码的最有效方法是将数据转换为 JSON 格式,并与 GPT 分享这种格式进行操作。将这些数据塞入 pandas 数据框并没有错,甚至可能更容易做到这一点,以高效地处理、清理和转换数据。我最终决定将最终数据集转换为 JSON 的一个重要原因是,因为将表格数据传递给 GPT 提示符是非常繁琐的。对于人类来说,它难以阅读;同时也会为大语言模型(LLM)引入错误,正如下面所述。
当你将表格引入提示符时,必须通过逗号分隔值(CSV)格式来完成。这有两个问题:
-
由于文本中也可能包含逗号,因此你必须通过将文本放入双引号中来进一步转义这些逗号(例如,“text one”,“text, two”,“test \“hi!\””)。这引入了另一个问题:
-
如果文本块中包含双引号(“),那么你就必须进一步转义这些双引号。将这些值分隔成单独的列往往会引发问题。
是的,尽管在 JSON 中也需要转义双引号(例如 “key”: “value has \”quotes\””),但是将这个值对齐到列中完全没有问题,因为“key”是唯一标识符。CSV 格式中列对齐有时会出现问题,届时很难排查出错的原因。
使用 JSON 的另一个原因是,你可以清晰地看到并区分当你通过 GPT 在未来的步骤中增强元数据时;它只是将更多的键值对水平添加。你也可以在表格中做到这一点,但这通常需要在你的 IDE 或笔记本中向右滚动。
小贴士: 在未来的某个步骤中,你将把这些数据传入 GPT,并要求它返回由分隔符(例如“|”)分隔的多个字段。因此,现在是删除你传入 JSON 格式的自由格式字段中任何此类分隔符的好时机。你不希望 GPT 在字段中传送“|”字符。
步骤 4:使用简单技巧增强数据(即基本特征工程)
总结: 简单的数学分析,如时间差、平均值、标准差,可以使用基础编码轻松且更便宜地完成,因此让 GPT 编写代码来完成这些任务,并在本地运行该代码,而不是将数据发送给 GPT 进行计算。语言模型已经被证明容易犯数学错误,所以最好把它们用于它们擅长的事情。
首先,我们可以通过聚合其中一些基本信息来增强工单的元数据。这是一个前期步骤,最好用一些简单的代码来完成,而不是浪费 GPT 的积分来处理。
在这种情况下,我们通过将 CreatedTime 从 ClosedTime 中减去来计算工单持续时间。

从左到右,JSON 显示通过基本数据聚合/增强进行水化处理
第 6 步:主菜:GPT 驱动的数据增强(增强的特征工程)
现在我们进入主菜部分。如何使用 GPT 转换原始数据,并从中派生出复杂且结构化的元数据,从这些元数据中可以提取出洞察。在数据科学领域,这一步叫做特征工程。
6.1:预处理:模糊化敏感信息(可选)
总结: 让 GPT 使用开源匿名化库并开发代码,在将数据发送到公共 API 服务之前进行匿名化处理。

图片来源:Kyle Glenn 通过 Unsplash
这一步适用于你在使用 OpenAI 而不是本地开源 LLM 的情况,因为数据会保留在你的笔记本电脑上。在未来的文章中,我会展示我的数据工程和分析本地环境设置,其中展示了一种开源 LLM 选项。
在我工作的公司,我们有一个安全的代理网关,既可以连接 OpenAI,也可以连接内部训练的 LLM,并且它能够掩盖个人身份信息(PII),并在可信边界内操作 OpenAI。这很方便,因为我可以将所有内部信息发送到这个代理,并以安全的方式享受 OpenAI 模型的优势。
然而,我意识到并非所有公司都有这个奢侈的条件。因此,我在这里增加了一个可选步骤来模糊化个人身份信息(PII)或其他敏感数据。所有这一切的美妙之处在于,GPT 知道这些库并可以用来编写代码,来模糊化数据!
我评估了五个库用于此目的,但我寻找的关键特性是能够将敏感信息转换为匿名数据,然后还能将其重新转换回来。我只找到了以下具有此功能的库。
在这两者中,Presidio 是我最喜欢的。我一直对微软在过去十年中做出的“高质量”开源贡献印象深刻。这套 Python 库也不例外。它具备开箱即用的能力,能够识别 PII(个人身份信息)类型的数据,并可以定制和指定需要被匿名化的其他数据。
这是一个例子:
原始文本:
('Peter gave his book to Heidi which later gave it to Nicole.
Peter lives in London and Nicole lives in Tashkent.')
匿名化测试:
'<PERSON_1> gave his book to <PERSON_2> which later gave it to <PERSON_0>.
<PERSON_1> lives in <LOCATION_1> and <PERSON_0> lives in <LOCATION_0>.`
这些数据可以发送给 GPT 进行分析。当它返回结果时,你可以通过映射进行去匿名化:
实体映射
{ 'LOCATION': {'London': '<LOCATION_1>', 'Tashkent': '<LOCATION_0>'},
'PERSON': { 'Heidi': '<PERSON_2>',
'Nicole': '<PERSON_0>',
'Peter': '<PERSON_1>'}
}
使用实体映射,文本可以被去匿名化:
去匿名化的文本:
('Peter gave his book to Heidi which later gave it to Nicole.
Peter lives in London and Nicole lives in Tashkent.')
我推荐查看这个笔记本,它会引导你如何实现这种方法。
请注意,除了 PII,可能需要模糊处理的其他信息包括系统信息(IP 地址、DNS 名称等)和数据库细节(如名称、模式等)
现在我们有了一个机制来匿名化敏感数据,下一步是创建一个高质量的提示来处理这些数据。
6.2 预处理:清理输入数据
总结: 在选择输出分隔符时要考虑周到,因为某些特殊字符在语言模型中有“意义”。然后,你可以放心地通过去除你选择的分隔符来清理原始输入。
问题: 当请求基于文本的接口(如 LLM)返回表格数据时,必须告诉它使用分隔符输出数据(例如 csv 或 tsv 格式)。假设你要求 GPT 输出汇总数据(即“特征”)并使用逗号分隔值。问题在于,输入的票据数据是原始且不可预测的,有人可能在描述中使用了逗号。从技术上讲,这不应该是问题,因为 GPT 会处理这些数据并去除进入的数据中的逗号,但仍然存在风险,GPT 可能会在输出中使用部分原始数据(包括逗号),例如在简短的总结中。经验丰富的数据工程师们可能现在已经意识到这个问题。当你的数据值本身包含应该用来分隔它们的分隔符时,就会出现各种处理问题。
有人可能会问:为什么不通过将值用双引号括起来来转义这些字符呢?例如:
“key” : “this, is the value, with all these characters !#@$| escaped” .
这里有一个问题。用户可能也在他们的数据中输入了双引号!
“key” : “this is a ”value” with double quotes, and it is a problem!”
是的,确实有解决这个问题的方法,比如使用多行正则表达式,但它们会让代码变得复杂,也使得 GPT 修复缺陷变得更加困难。因此,处理这个问题最简单的方法是选择一个输出分隔符,这样即使从输入中去除它,也不会对数据上下文产生太大影响,然后再将其从输入数据中去除!
我还尝试了一些分隔符,这些分隔符在输入数据中肯定不会出现,如|%|,但我很快意识到这些分隔符会快速消耗输出的标记限制,因此最终放弃了这种方法。
这里是我测试过的一些分隔符

最终,我选择了管道“|”分隔符,因为这是大多数利益相关者在表述问题时不会在工单描述中使用的符号。
之后,我让 GPT 编写了一些额外的代码,清理每个工单描述中的“|”符号。
6.3 — 提示性能调优
概述: 在运行 GPT 数据分析提示之前,先评估其在已知输出的工单描述集上的表现,微调提示并反复迭代,直到获得最佳性能评分。

通过措施迭代改进提示
目标: 让 GPT 读取客户编写的工单描述,并仅通过该描述提取以下元数据,之后可以对其进行汇总和可视化:
-
描述性标题概述问题
-
商业影响*
-
工单严重性*
-
工单复杂度
-
受影响的利益相关者群体
-
所属团队
-
工单类别
*基于客户提供的影响和紧急程度
方法: 我通过以下方式优化了主提示:
-
选取一些控制工单样本,
-
手动对每个工单进行分类,按照我希望 GPT 完成的方式(按类别、复杂度、利益相关者(客户)群体等),
-
将这些控制工单输入设计好的 GPT 提示中,
-
将 GPT 的结果与我自己的手动分类进行交叉比较,
-
评估 GPT 分类在各个维度上的表现,
-
根据哪个维度得分较低,改进 GPT 提示以提升其效果
这给了我重要的反馈,帮助我不断优化 GPT 提示,提升每个维度的得分。最终的提示,请参见附录:处理工单描述的 GPT 提示。
结果:
这里是从原始工单描述中提取的元数据的详细信息,以及经过多次微调提示后整体性能评分:

LLM 在元数据创建上的表现
这是我对某些维度得分较低的原因的推理,尽管经过了多次迭代:
-
复杂性: 在为每个工单评分“复杂性”时,我遇到了一个挑战,GPT 基于工单描述给出的复杂性评分远高于实际情况。当我要求它更加激进地评分时,评分开始向另一个方向倾斜,就像一只试图取悦主人的狗,它开始给复杂性评分过低,因此变得不可靠。我怀疑这种出厂默认的高评分行为是由于当前 GPT 技术的状态。我使用的是 GPT-4,它被认为相当于一名聪明的高中生,因此高中生会给出较高的复杂性评分。我猜测未来这些前沿模型的版本会达到大学甚至博士水平,我们将能够更准确地衡量此类任务的复杂性。或者,为了改善 GPT-4 的复杂性评分分析,我本可以使用“少量示例”学习技术,提供一些复杂性的示例,这可能会提升该维度的表现评分。
-
严重性: 当我要求 GPT 使用影响与紧急矩阵来评分严重性时,GPT 不得不依赖利益相关者在工单描述中提供的内容,而这些内容可能具有误导性。我们都曾在与 IT 部门开具内部工单时,使用一些旨在促使更快行动的词语。此外,利益相关者在工单描述中根本没有提供任何影响细节,且这种情况在不少情况下存在,这导致 GPT 选择了一个错误的严重性评分。
尽管某些元数据维度的得分较低,但我对整体输出感到满意。GPT 在一些关键元数据(如标题和类别)上的得分很高,我可以根据这些继续进行。
提示词本身没有问题,但我即将遇到一个有趣的 GPT 限制,它的“健忘症”。
6.4 — 探索 GPT 健忘症的极限
总结: 当向 GPT 提示中发送上下文上 无关 的数据块(如多个工单描述)时,处理的上限可能远低于通过填充输入令牌限制所允许的最大数据块数量。(在我的案例中,这个上限在 20 到 30 之间)。观察到 GPT 在超过此限制后会 consistently 忘记或忽略处理。可以通过反复试探来识别这个限制,保持在这个限制以下的 10%范围内,以避免数据丢失。

照片由 Pierre Bamin拍摄,来源于Unsplash
人类大脑可以在前额皮质中保持 5 到 7 个不相关的事物,而事实证明 GPT 可以保持 30 到 40 个不相关的事物,无论它的上下文窗口多大。我实际上只是发送了工单号和描述,其他数据并不需要复杂的推理。
由于我有将近 3000 个工单需要 GPT 审核,我最初的想法是尽量优化每次调用的数量,“打包”尽可能多的工单描述到每个提示中。我想出了一种详细的方法,根据单词数量来确定平均 token 大小(因为 token 是子词,基于变换器架构),发现每个提示中可以容纳大约 130 个工单描述。
但随后我开始看到一个奇怪的现象。无论我发送多少个工单描述到 GPT 进行处理,它总是只处理前 20 到 30 个工单!GPT 似乎无法处理超过这个神秘数字的内容。

这使我改变了策略,我决定将每次 API 调用的工单批量大小减少到最多 10 到 12 个工单,根据该批量的字数,略低于 20 到 30 的上限。虽然这种方法确实增加了调用次数,因此延长了分析时间,但它确保了没有工单被遗漏处理。
*Total tickets chunked: 3012*
*The full ticket data has been chunked into 309 chunks:*
*Chunk 0: Number of words = 674*
*Chunk 0: Number of tickets = 12*
*Chunk 1: Number of words = 661*
*Chunk 1: Number of tickets = 12*
*Chunk 2: Number of words = 652*
*Chunk 2: Number of tickets = 12*
*Chunk 3: Number of words = 638*
*Chunk 3: Number of tickets = 7*
*Chunk 4: Number of words = 654*
*….*
在与我公司的一位 AI 架构师讨论时,他确实提到这是 GPT 中最近观察到的现象。当你输入的上下文数据是相关时,大量输入才能表现良好。但当你将不相关的零散信息输入到 GPT 中,并要求它一次性处理完全无关的数据时,系统会崩溃。正是我观察到的现象。
在确定了最佳工单批量大小为 10 到 12 个,并创建了高效的提示后,是时候将所有批量通过提示进行处理了。
6.5 展示时间!将所有工单通过 GPT 进行处理
结论提前告诉你: GPT 可以在几小时内分析工单,而相同数量的工单可能需要数周或数月才能由人类完成。并且它便宜得多,尽管 GPT 有一定的错误率。
我向 GPT 提供了 JSON 格式,让它为我编写执行以下操作的代码:
-
将 JSON 数据加载到字典中
-
每次处理 10 到 12 个工单,将这些工单分析提示与它们一起合并成完整的 GPT 提示,使用###分隔每个工单/描述元组
-
将完整的提示发送到 GPT API(为了工作,我调用了我公司内部构建的、更安全的包装 API,它内嵌了安全性和隐私保护功能,但如果之前使用过混淆步骤,使用外部 GPT API 同样是安全的。)
-
将输出保存为管道分隔格式,并将其连接到磁盘上的文件中。
-
运行去匿名化工具,如果之前进行了混淆处理。(由于我公司内部已经构建了 GPT 包装 API,我不需要写这一步骤)
-
将输出也转换为原始的 JSON 文件。
-
在完整运行结束后,将 JSON 文件保存到磁盘。
-
打印出一些可见的队列,显示处理了多少个工单
-
为每次 API 调用的文本处理、工单数量、开始和结束时间进行计时。
为什么在成功运行后保存到磁盘是务实的: 这些运行非常昂贵,从时间的角度来看比从金钱的角度更为昂贵。因此,在成功运行完成后,明智之举是将数据序列化(保存)到磁盘上,这样以后就可以在保存的数据上进行分析,而无需再次执行这个代码块。事实上,在成功运行后,我在我的笔记本中注释掉了整个代码块,这样如果我从头到尾运行整个笔记本,它就会跳过这个昂贵的步骤,直接将 JSON 数据从磁盘加载到内存中,并继续进行分析。
这是完整填充的 JSON 输出示例。蓝色条目是 GPT 从描述字段中提取的元数据

GPT 从原始信息中提取并重新生成的结构化元数据,格式为 JSON
我通过这个流程运行了大约 3000 个票据,并在大约 2.95 小时内完成了处理。👏

图片来源:Nagara Oyodo 来自 Unsplash
为了给你一个比较点,告诉你如果我雇佣人工进行这项工作大概要花多久时间:我有一个类似的经历,可以追溯到两年前(也就是 GenAI 之前的时代)。我接管了一个运营团队,票据中的元数据也很差。我需要获得情境意识,所以我必须全职派出两名资深工程师,一个月时间,仔细而艰难地处理 1500 个票据描述并进行分类。
与 GPT 进行对比。即使每次 API 调用的批次较小,每次只有 10-15 个票据,GPT 仍在 2.95 小时内处理了 2800 个票据!是的,尽管 GPT 的错误率为 10%,它在 3 小时内分析的票据数量仍然是两个人在 1 个月内的两倍。
所以,节省的时间显然是惊人的,但成本比较如何呢?
GPT-4 与人工分析成本比较
我请 GPT 帮助我进行成本比较。请注意,我将人力劳动的 2 个月成本与 GPT 分析的票据数量进行比较。
提示:如果我使用其 API 运行 2.95 小时,GPT-4 的成本是多少?与两位资深 IT 软件工程师(分别来自美国中西部和印度,且全职工作 2 个月,周末除外)在同一项目上的工作时间成本相比,如何?
薪资估算:
美国中西部资深 IT 软件工程师:
-
平均年薪:$120,000(这可能会根据具体位置和公司有所不同,但通常低于硅谷的水平)。
-
月薪:$120,000 / 12 = $10,000。
印度资深 IT 软件工程师:
-
平均年薪:$30,000(这可能会有所不同,但通常远低于美国的水平)。
-
月薪:$30,000 / 12 = $2,500。
2 个月的成本计算:
-
中西部工程师:2 个月薪水:$20,000。
-
印度工程师:2 个月薪水:$5,000。
-
两位工程师的总成本:$25,000
GPT-4 API 费用(2.95 小时): ~$6.64,假设 GPT-4–8k 定价和给定的 token 使用率。(GPT4-o 或 GPT4-o mini 的费用会更便宜。)
即使你把我在这个项目上花的 20 小时的成本也算上,整体成本比较仍然要好得多。而且更重要的是,这项工作现在是可复制的。
所以基本上,使用$7 和 3 小时,GPT 完成了与人类需要 1 个月和$25,000 才能完成的相同分析
🎤 麦克风掉落!

照片由Andrew Gaines提供,来自Unsplash
第 7 步:从 GPT 派生的元数据中提取见解
总结: 一旦你使用 GPT 提取了有用的元数据,转身与 GPT 进行头脑风暴,看看你能从中绘制出哪些 KPIs。
尽管我已经有些好奇想要了解的内容,但我也和 GPT 进行了头脑风暴,要求它给我更多的想法。同样,使用 JSON 格式非常方便,我只需将一个票据的匿名样本传递给 GPT,并问它,“根据你看到的内容,给我一些关于我如何绘制图表以便从我的运营中获取见解的想法”
最终,这里是我们两个人想出来的思路。我采纳了一些,忽略了其他的。

与 GPT 进行头脑风暴,讨论要可视化的 KPIs……
第 8 步:可视化
总结: 得益于 GPT,你可以编写 Python 代码来创建图表,而不需要在 Python 中转换数据并将这些数据导入可视化工具。这有助于保持所有分析的简洁性、版本控制,并且将所有内容集中在一个地方。
从历史上看,探索性数据分析(EDA)中的典型模式是:在 Python 中提取并转换数据,然后将其存储在文件或数据库中,再将 Tableau、Power BI 或 Looker 与这些数据连接,利用它们来创建图表。虽然在这些可视化产品中保持长期有效的仪表盘绝对是正确的做法,但将这些产品用于进行早期阶段的 EDA 可能是一个高摩擦过程,会导致延迟。而且,管理和匹配不同版本的图表与不同版本的数据转换变得困难。然而,历史上遵循这种两步模式是必要的恶习,原因有二:
-
(拉取)这些可视化工具直观且具有拖放界面,意味着你可以快速进行实验并创建图表。
-
(推动)生成图表的事实标准 Python 库是 matplotlib。我不知道你是怎么想的,但我发现 matplotlib 是一个非常不友好的库(与 R 中直观的 ggplot 库不同,ggplot 使用起来非常愉快)。Seaborn 稍好一些,但它仍然比可视化工具需要更多的工作。
然而,现在 GPT 可以为你编写所有的 matplotlib(或 seaborn、或 plotly)代码,你就不必将工作转移到可视化工具中了。你可以从头到尾都停留在同一个 Python Notebook 中,这正是我所做的!
我确实查看了 Tableau,以验证一些中等复杂度的聚合逻辑是否在 Python 中正确计算(事实上这帮我找到了一个 bug),但总体来说,我所需要的所有图形都是通过 Python 中的散点图、条形图、折线图、直方图和饼图来构建的。
这里有一些这些图表和表格的示例。文本和数字当然已经匿名化,但这里的目的是展示你可以开始提取的那种洞察。
任何洞察的目标都是引发更深层次的问题,最终采取基于数据的有意义行动,进而创造价值。
洞察力就是让你对系统的行为产生好奇,从而你可以尝试改进它。

票据的复杂度如何准确地影响该票据的时长,以及应该聚焦哪些票据类别,以减少它们的时长并提高客户满意度。

确定票据时长是否与处理该票据的支持工程师相关联,以便你可以找出行为或培训方面的问题。

哪个工程团队接收的票据数量最多,且这一趋势如何发展。
处理服务请求(纯运维工作)是一个隐藏的成本,需要量化,因为它必须从工程团队的冲刺速度中扣除。在没有这些数据的情况下,工程团队通常会分配一部分时间来处理运维工作,这一比例通常是‘凭经验猜测’的,我们都知道这是笨拙的,并且因团队而异。通过这种分析,你可以更准确地为这些运维工作划分容量,同时不会妥协团队的产品承诺或让个人过度疲劳。

这些票据按类别的趋势是什么?我们是看到更多的及时性问题,还是准确性问题?

我们的客户最常使用哪些接口来提交票据,以便我们能够精简和优化这些区域,也许可以插入一些有帮助的自助服务文章。

有多少票据已经有 1 天的历史,是否存在某些运维人员选择处理大量简单案例的模式?这有助于平衡资源管理。

有多少票据确实是低复杂度问题,比如数据访问或系统访问,这些问题可以通过自动化和自助服务选项来解决。
未来的改进
-
利用 GPT 的数据科学能力进行更深层次的分析: 然而,这个分析虽然非常有洞察力,但只是停留在表面,仅仅是数据的可视化。还可以做更复杂的工作,使用线性回归或逻辑回归、ANOVA 进行预测分析,或使用聚类方法(如 KNN)来发掘数据中的其他模式。
-
多智能体框架以加速和提高质量:
-
我必须与 GPT 进行多轮反复的沟通才能编写实际代码。虽然这比我从头开始编写代码的时间(20-30 天全职工作,这意味着“永远不可能”!)要快得多(7 天兼职),但我确实认为使用 LLM 支持的 AI 代理可以相互批评对方的输出,并找到更好的方法。(这是我目前在工作中积极实验的方向,初步实验非常令人鼓舞。以后会写更多相关内容)
-
在推荐代码方面,GPT 真的很盲目。我从它那儿复制了代码并在我的 Jupyter Notebook 中运行。更好的方法是使用 MAF 设置一个环境代理(或许通过容器完美配置我的所需库等),然后让 AI 编码代理编写代码、执行、发现缺陷、迭代并修复。我想,这样能节省我超过 50% 的开发时间。
-
分解分析提示:虽然我最终使用了一个大型提示来进行分析,但如果我使用一些链式 AI 代理机制,我本可以将分析任务分配给不同的代理,为它们提供不同的 LLM 端点,并且每个代理使用不同的温度设置。温度越低,LLM 越精准,创造性越差。例如,我通过艰难的方式学到,如果使用默认的温度设置,GPT 会对每个票据的类别(如“数据完整性”或“完整性”)进行细微修改,结果导致了更多的后期处理清理工作,给我带来了困扰。
-
哎,我甚至可以让我的多智能体团队中的创意 AI 代理帮我写出这份文档的大部分内容!
来自产品负责人关于操作票据的总结想法
我们的客户通过他们日常与我们产品的互动来体验我们的产品。通过工单和服务请求,他们不断向我们传递哪些工作正常、哪些工作不正常的信号,并通过观察我们如何响应这些信号形成对我们的印象。然而,我们常常把注意力集中在产品开发、正在进行的重大转型计划以及跟踪和监控下一个炫目的项目上,而忽视了这些运营信号,这可能会带来风险。当然,响应重大事件是每个负责领导者的工作,行动计划通常通过根本原因分析(RCA)会议得以形成并取得成果。然而,我认为有大量中等严重性的问题和服务请求,客户常常因为其数量庞大而被忽视。当您真诚地开始挖掘这些宝贵的工单数据时,它们往往会让人感到不知所措且未经过整理,您的大脑可能会开始晕旋!您有可能因此走向一个简化且不完整的思维模型,这个模型可能是由别人创建的总结报告所导致的。
我的理念是,作为领导者,您必须创造时间和能力,亲自深入实际操作。这是您真正了解自己业务如何运作的唯一方式。在 genAI 时代之前,这曾是非常困难的。即使是能够进行数据分析的领导者,也无法从日常工作中抽出时间来做这件事。而现在,情况不再如此!
虽然本文试图为您提供一些能力,以便启动基于 genAI 的运营工单分析之旅,但只有您,我亲爱的读者,才能创造时间和空间来付诸实践。更重要的是,我希望您在有效使用 LLMs 来加速分析的过程中获得的某些见解,能在运营工单分析之外的许多其他领域得到应用。
附录:经过微调的 GPT 提示版本
以下是我用来进行工单分析的最有效提示的清理版。我用数据管理协会(DAMA)发布的标准替代了我们内部的数据质量维度。如果您的公司有数据质量政策,我建议您在这里使用这些标准。
以下是一些案例及其描述,每个案例之间以###分隔。这些案例与数据相关技术相关。您的任务是仔细审查每个案例,提取必要的 7 个数据点,并以指定格式展示结果。详细说明如下:
-
标题: 根据描述内容,为每个案例创建一个简洁且具描述性的标题,确保字符数不超过 300 个。
-
影响: 从描述中简要总结影响,写成一句话。如果影响未直接陈述或暗示,则写“未提供影响”。
-
严重性: 使用紧急性与影响矩阵方法,为每个案例分配严重性级别,考虑问题的紧急性及其对系统的影响:
-
S1: 高紧急性和高影响,可能导致系统故障或使应用程序无法使用。
-
S2: 高紧急性但影响中等,或中等紧急性但影响较大,影响多个用户。
-
S3: 低紧急性,影响中等或较低,用户中断最小。
-
S4: 低紧急性和低影响,通常与一般请求相关(注意:访问问题通常不属于 S4)。
-
每个案例应仅分配一个严重性级别。
4. 复杂性: 根据你在数据领域的专业知识评估案件的复杂性:
-
高复杂性
-
中等复杂性
-
低复杂性
-
通常,访问相关的案例是低复杂性,但可以根据描述做出判断。
5. 业务线(LOB): 根据描述确定相关的业务线。选项包括:
-
财务
-
市场营销
-
销售
-
客户支持
-
人力资源
-
杂项: 如果无法明确识别业务线。
-
每个案例只选择一个业务线。如果提到多个,请选择最重要的。
6. 团队: 根据描述分配适当的团队。选项包括:
-
CDI(中央数据摄取): 任何提到 CDI 或“中央数据摄取团队”的案例应仅归类于该团队。
-
数据工程: 与数据管道相关的案例,如提取、转换或加载。
-
数据平台: 与数据平台相关的任何问题,包括数据可视化或 DEP。
-
每个案例应仅分配一个团队。
7. 工单分类: 最后,根据描述将工单分类,使用简单的 1-2 个词的标签。使用 DAMA 数据质量维度进行分类。类别应包括但不限于:
-
完整性: 确保所有必要的数据都包含在内。
-
独特性: 验证数据条目是唯一的,不重复。
-
及时性: 确保数据是最新的并按预期可用。
-
准确性: 确认数据是正确的并符合其真实值。
-
一致性: 确保不同数据集中的数据保持一致。
-
有效性: 确保数据符合所需的格式或值。
-
访问: 与请求访问数据或系统相关。
-
如有需要,可以创建 2-3 个其他类别,但请保持简洁一致。
这是输出格式的一个示例。应为一个列表,每个项之间用管道符(|)分隔:
16477679|描述性标题(不超过 300 个字符)|简短的影响描述|S2|高复杂性|财务|数据工程|及时性
16377679|另一个描述性标题|另一个简短的影响描述|S1|高复杂性|销售|数据平台|准确性*
除非另有说明,所有图片均由作者提供
使用 IPython Jupyter 魔法命令改善笔记本体验
创建自定义 IPython Jupyter 魔法命令的帖子
·发表于 Towards Data Science ·12 分钟阅读·2024 年 2 月 19 日
--

学会在你的笔记本中施展一些魔法。图像由作者使用 DALL-E-3 创建。此帖的一个版本最初发布在这里。
介绍
Jupyter 笔记本在数据科学中已经很常见。它们允许在一个地方混合“读取、评估、打印、[循环]”(REPL)代码编写和文档编写。它们通常用于分析和头脑风暴目的,但也有一些人更倾向于使用笔记本而非脚本来运行生产代码(不过我们在这里不讨论这个话题)。
不可避免地,笔记本中编写的代码在某些方面会有重复,比如设置数据库连接、显示输出、保存结果、与内部平台工具交互等。最好将这些代码存储为函数和/或模块,以便它们可重用且更易于管理。
然而,进行这项操作并不总是能改善笔记本的体验。例如,你仍然需要在整个笔记本中导入并调用这些函数,这样并不会显著改变笔记本的体验。那么,如何增强笔记本开发体验呢?IPython Jupyter 魔法命令。
IPython Jupyter 魔法命令(例如,笔记本单元中以%或%%开头的行)可以修饰笔记本单元或行,以修改其行为。许多魔法命令是默认提供的,包括%timeit用于测量单元的执行时间,%bash用于执行 Shell 命令,还有一些命令由扩展提供,例如 %sql 可以直接在笔记本单元中编写 SQL 查询。
在本文中,我们将展示如何让你的团队将任何实用函数转化为可重用的 IPython Jupyter 魔法,以提供更好的笔记本体验。作为示例,我们将使用我们创建的开源库Hamilton,来促使我们创建一个魔法,帮助改进开发中的使用体验。你不需要了解 Hamilton 是什么,就能理解本文的内容。
注意:如今,有许多种类的笔记本(如Jupyter、VSCode、Databricks等),但它们都是建立在 IPython 之上的。因此,开发的 Magics 应该可以跨环境重用。
理解 IPython Jupyter Magics
IPython Jupyter Magics(我们简称为 Magics)是可以动态加载到笔记本中的代码片段。它们有两种类型,单行魔法和单元魔法。
单行魔法,顾名思义,作用于单独的一行。也就是说,它只处理该行上指定的输入。它们通过命令前的单个%表示。
# will only time the first line
%time print("hello")
print("world")
单元魔法,顾名思义,作用于整个单元的内容。它们通过命令前的双%%表示。
# will time the entire cell
%%timeit
print("hello")
print("world")
Jupyter 自带一些内置魔法命令。你可以将它们看作是“命令行”工具,可以访问整个笔记本的上下文。这使得它们不仅能够与笔记本的输出进行交互(例如,打印结果、显示 PNG、渲染 HTML),还可以修改现有变量的状态,并写入其他代码和 Markdown 单元!
这对于开发内部工具非常有用,因为它可以抽象并隐藏不必要的复杂性,给用户带来“神奇”的体验。这是开发你自己“平台工作”的强大工具,特别适用于 MLOps 和 LLMOps 目的,因为你可以将集成的内容隐藏,不需要在笔记本中暴露。因此,这也意味着如果这些抽象代码在幕后发生变化,笔记本不需要更新,因为所有内容都可以隐藏在 Python 依赖项的升级中。
工作流示例
魔法命令有可能让你的工作流程变得更简单、更快捷。例如,如果你更倾向于先在笔记本中开发,然后再将代码转移到 Python 模块中,这可能涉及容易出错的剪切和粘贴。为此,魔法命令%%writefile my_module.py将直接创建一个文件,并将你的单元格内容复制到其中。
另一方面,你可能更喜欢在 IDE 中开发 my_module.py,然后将其加载到笔记本中以测试你的函数。这通常需要重启笔记本内核来刷新模块导入,这可能会很麻烦。在这种情况下,%autoreload 将在每次单元格执行之前自动重新加载所有模块导入,从而消除这个摩擦点!
自定义魔法命令的需求
在文章 数据代码应该有多结构化? 中,提到标准化/集中化/“平台”工作应当改善“快速推进与打造持久系统”之间的取舍曲线。改变这一取舍的一个具体策略是实现更好的工具支持。更好的工具应该使曾经复杂的事情变得更简单并且易于访问。这正是通过自定义魔法命令可以实现的目标,它意味着你可以减少需要做出的取舍。
一个 Hamilton 魔法命令
对于那些不熟悉 Hamilton 的读者,我们建议阅读许多相关的 TDS 文章(例如 起源故事**, 生产提示工程**, 简化 Airflow DAG 创建与维护,整洁的生产 Pandas,等等。)以及 https://www.tryhamilton.dev/。
Hamilton 是我们在 2019 年在 Stitch Fix 创建的一个开源工具。Hamilton 帮助数据科学家和工程师定义可测试、模块化、自文档化的数据流,并对其进行血统追踪和元数据编码。Hamilton 通过要求 Python 函数被整理成模块来实现这些特性。
然而,典型的 Jupyter 笔记本使用模式会导致代码只存在于笔记本中,无法在其他地方访问,这给开发者带来了人机工程学挑战:
我们如何使人们能够轻松快捷地从笔记本创建 Python 模块,同时改善开发体验?
Hamilton 开发循环如下所示:

Hamilton 开发循环。图像来自作者。
花点时间阅读这个循环。该循环显示了每次代码更改时,用户不仅需要重新导入 Python 模块,还需要重新创建驱动程序对象。由于笔记本允许按任意顺序执行单元格,这可能会让用户很难跟踪每个模块的版本以及驱动程序中当前加载的内容。这项负担由用户承担,并可能需要重启内核,这会丢失其他计算结果(幸运的是,Hamilton 可以设置为执行复杂的数据流并从上次停止的地方继续……),这并不是理想的情况。
这是我们如何使用魔法命令来改进这个循环:
-
从单元格中定义的函数创建一个“临时”Python 模块,并直接在笔记本中导入这个新模块。
-
自动可视化函数定义的有向无环图(DAG),减少可视化样板代码。
-
重新构建笔记本中所有的 Hamilton 驱动程序,并更新模块,节省用户记得手动重新创建驱动程序来接收变更的时间。
我们将要实现的功能
我们希望命令看起来像这样:
%%cell_to_module -m my_module --display --rebuild-drivers
def my_func(some_input: str) -> str:
"""Some logic"""
return ...
运行单元格后,产生以下行为:
-
在笔记本中创建名为 my_module 的模块。
-
显示由单元格内的函数构建的有向无环图(DAG)。
-
重新构建任何使用了 my_module 的下游驱动程序,避免用户需要重新运行这些单元格。
如你所见,这是一个非平凡的魔法命令,因为我们正在调整单元格的输出和笔记本的状态。
构建自定义魔法命令
在这里,我们一步步地说明如何创建魔法命令。为了避免只展示一个平凡的“hello world”例子,我们还将解释如何构建 Hamilton 的 %%cell_to_module 魔法命令。
步骤 1:设置你的代码
创建一个新的 Python 模块,在其中编写魔法命令代码,并创建一个 Jupyter Notebook 来进行尝试。这个模块(即 .py 文件)的名称将是你需要加载的扩展名。
如果安装了 Jupyter Notebook,你已经具备了所有必需的 Python 依赖项。然后,添加你需要的库,在我们的案例中是 Hamilton(pip install sf-hamilton[visualization])。
步骤 2:定义你的魔法命令
要定义一个简单的魔法命令,你可以使用函数或对象(查看这些文档)。对于需要状态的复杂魔法命令,你将需要使用类方法。在这里我们将使用基于类的方法。首先,我们需要导入 IPython 模块/函数,然后定义一个继承自 magic.Magics 的类。每个使用 @cell_magic 或 @line_magic 装饰器的方法都定义了一个新的魔法命令,类可以包含任意数量的这些方法。
首先,你的代码在高层次上应该是这样的:
# my_magic.py
from IPython.core import magic
from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring
@magic.magics_class
class MyMagic(magic.Magics):
"""Custom class you write"""
@magic_arguments() # needs to be on top to enable parsing
@argument(...)
@magic.cell_magic
def a_cell_magic_command(self, line, cell):
...
@magic_arguments() # needs to be on top to enable parsing
@argument(...)
@magic.line_magic
def a_line_magic_command(self, line):
...
对于有状态的魔法命令,添加 __init__() 方法(即构造函数)可能很有用。但在我们的情况下并不需要。
通过继承自magic.Magics,此类可以访问多个重要字段,包括 self.shell,这是支撑笔记本的IPython InteractiveShell。使用它可以让你提取并检查当前 Jupyter 笔记本中加载的变量。
我们的 Hamilton Magic 命令将首先如下所示:
from IPython.core import magic
from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring
@magic.magics_class
class HamiltonMagics(magic.Magics):
"""Magics to facilitate Hamilton development in Jupyter notebooks"""
@magic_arguments() # needed on top to enable parsing
@arguments(...)
@magics.cell_magic
def cell_to_module(self, line, cell):
...
步骤 3:解析输入参数
接下来,我们指定将传递哪些参数以及如何解析它们。对于每个参数,添加@argument,并在顶部添加@magic_arguments()装饰器。如果你熟悉argparse参数,它们遵循类似的模式,但功能不完全一样。在函数内部,你需要调用parse_argstring()函数。它接收函数本身以读取来自装饰器的指令,以及*line*(包含%或%%)其中包含参数值。
我们的命令将开始看起来像这样:
@magic_arguments() # needs to be on top to enable parsing
# flag, long form, default value, help string.
@argument("-a", "--argument", default="some_value", help="Some optional line argument")
@magic.cell_magic
def a_cell_magic_command(self, line, cell):
args = parse_argstring(self.a_cell_magic_command, line)
if args.argument:
# do stuff -- place your utility functions here
注意,对于必需的参数,magic_arguments()中没有为此提供设施,因此你需要在函数体内手动检查正确性等。
继续解析 Hamilton Magic 示例,类中的方法现在看起来如下;我们使用了许多可选参数:
@magic_arguments() # needed on top to enable parsing
@argument(
"-m", "--module_name", help="Module name to provide. Default is jupyter_module."
) # keyword / optional arg
@argument(
"-c", "--config", help="JSON config, or variable name containing config to use."
) # keyword / optional arg
@argument(
"-r", "--rebuild-drivers", action="store_true", help="Flag to rebuild drivers"
) # Flag / optional arg
@argument(
"-d", "--display", action="store_true", help="Flag to visualize dataflow."
) # Flag / optional arg
@argument(
"-v", "--verbosity", type=int, default=1, help="0 to hide. 1 is normal, default"
) # keyword / optional arg
@magics.cell_magic
def cell_to_module(self, line, cell):
"""Execute the cell and dynamically create a Python module from its content.
A Hamilton Driver is automatically instantiated with that module for variable `{MODULE_NAME}_dr`.
> %%cell_to_module -m MODULE_NAME --display --rebuild-drivers
Type in ?%%cell_to_module to see the arugments to this magic.
"""
# specify how to parse by passing
args = parse_argstring(self.cell_to_module, line)
# now use args for logic ...
注意,额外的@argument参数对于当有人使用?查询 Magic 的功能时非常有用。例如,?%%cell_to_module将显示文档。
步骤 4:实现命令的逻辑
现在我们已经解析了参数,可以实现 Magic 命令的逻辑。这里没有特别的限制,你可以编写任何 Python 代码。跳过一个通用的示例(你已经有足够的内容可以从前一步开始),让我们深入探讨 Hamilton Magic 示例。对于它,我们希望使用参数来决定命令的期望行为:
-
创建 Python 模块,命名为module_name。
-
如果— rebuild-driver,重建驱动程序,并传递详细信息。
-
如果— config存在,准备好它。
-
如果— display,显示 DAG。
请参阅代码中的注释以获取解释:
# we're in the bowels of def cell_to_module(self, line, cell):
# and we remove an indentation for readability
...
# specify how to parse by passing this method to the function
args = parse_argstring(self.cell_to_module, line)
# we set a default value, else use the passed in value
# for the module name.
if args.module_name is None:
module_name = "jupyter_module"
else:
module_name = args.module_name
# we determine whether the configuration is a variable
# in the notebook environment
# or if it's a JSON string that needs to be parsed.
display_config = {}
if args.config:
if args.config in self.shell.user_ns:
display_config = self.shell.user_ns[args.config]
else:
if args.config.startswith("'") or args.config.startswith('"'):
# strip quotes if present
args.config = args.config[1:-1]
try:
display_config = json.loads(args.config)
except json.JSONDecodeError:
print("Failed to parse config as JSON. "
"Please ensure it's a valid JSON string:")
print(args.config)
# we create the python module (using a custom function)
module_object = create_module(cell, module_name)
# shell.push() assign a variable in the notebook.
# The dictionary keys are the variable name
self.shell.push({module_name: module_object})
# Note: self.shell.user_ns is a dict of all variables in the notebook
# -- we pass that down via self.shell.
if args.rebuild_drivers:
# rebuild drivers that use this module (custom function)
rebuilt_drivers = rebuild_drivers(
self.shell, module_name, module_object,
verbosity=args.verbosity
)
self.shell.user_ns.update(rebuilt_drivers)
# create a driver to display things for every cell with %%cell_to_module
dr = (
driver.Builder()
.with_modules(module_object)
.with_config(display_config)
.build()
)
self.shell.push({f"{module_name}_dr": dr})
if args.display:
# return will go to the output cell.
# To display multiple elements, use IPython.display.display(
# print("hello"), dr.display_all_functions(), ... )
return dr.display_all_functions()
注意我们是如何使用self.shell的。这允许我们更新并注入变量到笔记本中。函数返回的值将作为“单元输出”使用(即你看到打印值的地方)。
步骤 5:注册你的 Magic 命令
最后,我们需要告知 IPython 和笔记本关于 Magic 命令的信息。我们定义 Magic 的模块必须具备以下功能来注册 Magic 类,并能够加载我们的扩展。如果涉及任何有状态的操作,应该在这里实例化它。
请注意,这里ipython参数与我们定义的类方法中self.shell所提供的 InteractiveShell 是相同的。
def load_ipython_extension(ipython: InteractiveShell):
"""
Any module file that define a function named `load_ipython_extension`
can be loaded via `%load_ext module.path` or be configured to be
autoloaded by IPython at startup time.
"""
ipython.register_magics(MyMagic)
ipython.register_magics(HamiltonMagics)
请参阅完整的Hamilton Magic 命令。
尝试一下
要在笔记本中加载你的魔法命令,请尝试以下命令:
%load_ext my_magic
对于我们的 Hamilton 魔法命令,我们可以通过以下方式加载:
%load_ext hamilton.plugins.jupyter_magic
在开发过程中,使用此命令重新加载你更新后的魔法命令,而无需重启笔记本内核。
%reload_ext my_magic
然后,你可以在每一行或单元格的基础上调用定义的魔法命令。因此,对于 Hamilton,我们现在可以执行:
%%?cell_to_module
这是一个使用示例,展示了它如何注入可视化内容:

展示魔法命令实际应用的示例。

动画 GIF,展示如何添加函数并按回车刷新图像。
你接下来会做什么
在实际使用案例中,你最有可能将你的魔法命令版本化并打包到一个库中,然后在 Python 环境中根据需要轻松管理。对于 Hamilton 魔法命令,它被打包进了 Hamilton 库,因此只需安装 sf-hamilton,加载魔法命令后即可在笔记本中访问。
要关闭
在这篇文章中,我们向你展示了创建和加载你自己的 IPython Jupyter 魔法命令所需的步骤。希望你现在已经开始思考在笔记本中常做的单元格/任务/操作,这些操作可以通过添加简单的魔法命令来增强/简化/甚至去除!
为了展示一个现实中的例子,我们动机并展示了一个 Hamilton 魔法命令的内部工作原理,目的是展示一个为了提高 Jupyter Notebook 中开发者体验而设计的命令,通过增强输出并改变内部状态。
我们希望这篇文章能帮助你克服难关,为你和你的团队的 Jupyter Notebook 体验创建更符合人体工程学的工具。
其他资源
使用 LangChain ReAct 代理回答 RAG 系统中的多跳问题
#LLM 初学者指南
在使用 ReAct 和 Open AI Tools 代理逐步回答复杂查询时非常有用,特别是在处理内部文档时。
·发布于Towards Data Science ·阅读时长 43 分钟·2024 年 2 月 15 日
--

图片由作者生成(提示工程学贡献:Fabian Nitka)
动机
我过去使用标准的 LangChain 组件(如 vectorstore、retrievers 等)构建的基本 RAG 聊天机器人效果很好。根据我提供的内部数据集,它们能够处理一些简单的问题,比如“印度的产假政策是什么”(源数据集:HR 政策文档)、“我们产品的口味主要有哪些问题”(源数据集:社交媒体/Twitter)、“莫奈画作中的主题是什么”(源数据集:艺术期刊)等。最近,输入的查询复杂度增加了,例如,“过去 1 个月内,关于口味的关注是否有所增加”。除非内部文档中有专门讨论比较的部分,否则聊天机器人很难给出正确答案。原因是——正确的答案需要以下步骤被有序地规划和执行:
-
第一步:根据“过去 1 个月”和今天的日期计算开始日期和结束日期
-
第二步:获取提到口味问题的开始日期的查询
-
第三步:计数第二步中的查询
-
第四步:获取提到口味问题的结束日期的查询
-
第五步:计数第四步中的查询
-
第六步:使用第三步和第五步中的计数计算百分比的增减。
幸运的是,大型语言模型在这种规划方面非常擅长!而 Langchain 代理则是为我们协调这些规划的工具。
代理的核心思想是使用语言模型选择一系列要执行的操作。在代理中,语言模型作为推理引擎,决定采取哪些行动以及以什么顺序执行。[来源]

代理以逐步方式回答多跳问题。(图像由作者提供)
代理通常需要在实例化时指定一组工具。例如,要解决上述的多跳问题,我们应该定义四个工具:
-
Tool_Date:一个 Python 函数,输入一个相对时间框架(如过去 6 个月),并通过从今天的日期中减去该时间框架来计算开始日期(用于步骤#1) -
Tool_Search:一个搜索引擎,输入搜索词并返回相关文档列表(用于步骤#2 和步骤#4) -
Tool_Length:一个 Python 函数,输入一个列表并返回该列表的长度(用于步骤#3 和步骤#5) -
Tool_PercCalculator:一个 Python 函数,输入两个数字并返回百分比变化计算结果(用于步骤#6)
一般来说——请注意你为代理提供的工具选择,因为这些是代理在回答每一个中间步骤时会使用的唯一工具。如果它找到相关工具——太好了,它将使用该工具来获得答案。如果没有,它通常会迭代几次(即尝试使用其他可用工具或其自身的逻辑推理),最终返回一个次优的答案。
让我们直接进入代码,如果你想跟着一起操作,以下是 GitHub 仓库。
数据集
虽然我很想使用那个非常流行的 [state_of_the_union.txt](https://github.com/hwchase17/chat-your-data/blob/master/state_of_the_union.txt) 来做这个演示,但我没能想出可以对该文档提问的复杂问题。因此,我创建了一个虚构公司 GlobalCorp 的虚拟 HR 文档(使用 ChatGPT)。你可以在 这里 查看 globalcorp_hr_policy.txt 文件。该文件的主要亮点包括:(a)国家特定的年度预算(b)不同货币的预算,以及(c)国家特定的假期政策。
LLM 和嵌入模型
我们将在本教程中使用 Azure Open AI 模型(gpt3.5 turbo、gpt-4-turbo 和 ada-embeddings)。
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
OPENAI_DEPLOYMENT_NAME = "gpt-35-turbo-16k"
OPENAI_DEPLOYMENT_ENDPOINT = "https://<???>.openai.azure.com/"
OPENAI_DEPLOYMENT_VERSION = "2023-12-01-preview"
OPENAI_MODEL_NAME = "gpt-35-turbo-16k"
OPENAI_ADA_EMBEDDING_DEPLOYMENT_NAME = "text-embedding-ada"
OPENAI_ADA_EMBEDDING_MODEL_NAME = "text-embedding-ada-002"
llm = AzureChatOpenAI(
deployment_name=OPENAI_DEPLOYMENT_NAME,
model_name=OPENAI_MODEL_NAME,
openai_api_base=OPENAI_DEPLOYMENT_ENDPOINT,
openai_api_version=OPENAI_DEPLOYMENT_VERSION,
openai_api_key=OPENAI_API_KEY,
openai_api_type="azure",
temperature=0.1,
)
embeddings = OpenAIEmbeddings(
deployment=OPENAI_ADA_EMBEDDING_DEPLOYMENT_NAME,
model=OPENAI_ADA_EMBEDDING_MODEL_NAME,
openai_api_base=OPENAI_DEPLOYMENT_ENDPOINT,
openai_api_type="azure",
chunk_size=1,
openai_api_key=OPENAI_API_KEY,
openai_api_version=OPENAI_DEPLOYMENT_VERSION,
)
免责声明 — 我将交替使用“RAG 工具”、“Q&A 系统”和“QnA 工具”这几个术语。在本教程中,它们都指的是一个能够查找一堆文档以回答特定用户查询的工具,但它 没有 任何对话记忆,即你不能以类似聊天的方式提问后续问题。不过,这可以在 LangChain 中轻松实现,可能会在未来的某篇文章中讨论。这里的重点仅仅是让多跳问题能够正常工作。
基于 RAG 的 QnA
让我们继续使用这些数据构建一个标准的问答系统。
我们将使用TextLoader加载虚拟 HR 文档。
# Loading the document
from langchain.document_loaders import TextLoader
loader = TextLoader("../data/globalcorp_hr_policy.txt")
documents = loader.load()
Chroma作为向量存储(用于存储文档嵌入),
persist_directory = "local_vectorstore"
collection_name = "hrpolicy"
PROJECT_ROOT = "...." #insert your project root directory name here
vectorstore = Chroma(
persist_directory=os.path.join(PROJECT_ROOT, "data", persist_directory),
collection_name=collection_name,
embedding_function=embeddings,
)
LocalFileStore作为文档存储(用于存储父文档),
from langchain.storage._lc_store import create_kv_docstore
# The storage layer for the parent documents
local_store = "local_docstore"
local_store = LocalFileStore(os.path.join(PROJECT_ROOT, "data", local_store))
docstore = create_kv_docstore(local_store)
PDR(parentdocumentretriever)作为retriever(用于从索引中检索相关数据)。
# This text splitter is used to create the child documents
parent_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=20)
child_splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20)
retriever = ParentDocumentRetriever(
vectorstore=vectorstore,
docstore=docstore,
child_splitter=child_splitter,
parent_splitter=parent_splitter,
)
完成所有设置后,我们准备使用add_documents()命令将文档添加到检索器中。此外,我还建议使用.persist()命令持久化向量存储(即将向量存储的内容保存到磁盘,这样你在终止当前会话后就不必重新计算嵌入了)。
# run only once
# vectorstore.persist()
# retriever.add_documents(documents, ids=None)
一旦运行这些命令,你应该会看到在工作会话中创建了两个文件夹 — local_docstore和local_vectorstore。可以随意检查每个文件夹的内容。

快速检查retriever是否设置正确:
retriever.get_relevant_documents("communication initiatives?")
## OUTPUT ##
[Document(page_content='**Health and Safety:**\nWorkplace safety is a shared responsibility. Emergency procedures are clearly posted throughout our office buildings, and an annual budget of $10,000, €5,000, and ¥1 million is allocated for safety drills and equipment maintenance in the U.S., Germany, and Japan, respectively.\n\n**Communication:**\nImportant updates are conveyed through company-wide emails and team meetings. An annual budget of $500,000, €250,000, and ¥25 million is allocated for communication initiatives, including employee engagement events in the U.S., Germany, and Japan, respectively.\n\nThis policy undergoes an annual review to ensure relevance and compliance. Welcome to GlobalCorp, where our commitment to a diverse, inclusive, and respectful workplace is the foundation of our success.\n\n1.\tRecruitment and Selection\n1.\tIntroduction', metadata={'source': '../data/globalcorp_hr_policy.txt'}),
Document(page_content='•\ttwo satisfactory references;\n•\tconfirmation of the right to work in this country (if appropriate)\n•\tCriminal Records Disclosure (if appropriate).\n6.\tProbationary Period\n\t\n6.1\tAll appointments into the Company will be made subject to a probationary period of six calendar months. After three months a review meeting will take place between the post holder and their line manager to discuss progress. At the end of the probationary period, and subject to a satisfactory report by the appropriate head of section or line manager, employees will be notified in writing that they have successfully completed their probationary period. The probationary period can be extended by a further 3 months should the individuals line manager consider this appropriate.\n7.\tRecruitment Monitoring', metadata={'source': '../data/globalcorp_hr_policy.txt'})]
最后,我们将构建RetrievalQA链,通过之前提到的所有组件进行问答。
from langchain.chains import RetrievalQA
qa = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=retriever,
return_source_documents=True,
)
提问标准问题
qa({"query": "What is the allocated budget for communication initiatives?"})
## OUTPUT ##
{'query': 'What is the allocated budget for communication initiatives?',
'result': 'The allocated budget for communication initiatives is $500,000 in the United States, €250,000 in Germany, and ¥25 million in Japan.',
'source_documents': [Document(page_content='**Health and Safety:**\nWorkplace safety is a shared responsibility. Emergency procedures are clearly posted throughout our office buildings, and an annual budget of $10,000, €5,000, and ¥1 million is allocated for safety drills and equipment maintenance in the U.S., Germany, and Japan, respectively.\n\n**Communication:**\nImportant updates are conveyed through company-wide emails and team meetings. An annual budget of $500,000, €250,000, and ¥25 million is allocated for communication initiatives, including employee engagement events in the U.S., Germany, and Japan, respectively.\n\nThis policy undergoes an annual review to ensure relevance and compliance. Welcome to GlobalCorp, where our commitment to a diverse, inclusive, and respectful workplace is the foundation of our success.\n\n1.\tRecruitment and Selection\n1.\tIntroduction', metadata={'source': '../data/globalcorp_hr_policy.txt'}),
Document(page_content='**Grievance and Disciplinary Procedures:**\nOur grievance and disciplinary procedures are outlined on the company intranet. Termination conditions may include gross misconduct or repeated policy violations. In such cases, a disciplinary process will be followed, including a three-strike system, before termination. Employees leaving GlobalCorp should follow the exit process detailed in the employee handbook.\n\n**Confidentiality and Data Security:**\nEmployees are expected to maintain confidentiality, and strict data security measures are in place. An annual budget of $1 million is allocated for cybersecurity training and awareness programs in the U.S., while budgets for Germany and Japan are €500,000 and ¥50 million, respectively.', metadata={'source': '../data/globalcorp_hr_policy.txt'}),
Document(page_content='**Compensation and Benefits - United States:**\nIn the United States, employees enjoy health insurance, dental coverage, and a 401(k) retirement plan. An annual budget of $1 million is allocated for employee benefits.\n\n**Compensation and Benefits - Germany:**\nIn Germany, benefits include health insurance, a pension plan, and an additional 5 days of vacation annually. An annual budget of €500,000 is allocated for employee benefits.\n\n**Compensation and Benefits - Japan:**\nIn Japan, employees receive health insurance, a commuting allowance, and wellness benefits. An annual budget of ¥50 million is allocated for employee benefits.\n\n**Leave Policies - United States:**\nIn the United States, employees have 15 days of paid vacation and 10 days of paid sick leave annually. An annual budget of $500,000 is allocated for leave-related expenses.', metadata={'source': '../data/globalcorp_hr_policy.txt'})]}
qa({"query": "What is the probationary period?"})
## OUTPUT ##
{'query': 'What is the probationary period?',
'result': "The probationary period is a period of six calendar months during which an employee's performance and suitability for the job are assessed. At the end of the probationary period, if the employee's performance is satisfactory, they will be notified in writing that they have successfully completed their probationary period. The probationary period can be extended by a further 3 months if the employee's line manager deems it necessary.",
'source_documents': [Document(page_content='•\ttwo satisfactory references;\n•\tconfirmation of the right to work in this country (if appropriate)\n•\tCriminal Records Disclosure (if appropriate).\n6.\tProbationary Period\n\t\n6.1\tAll appointments into the Company will be made subject to a probationary period of six calendar months. After three months a review meeting will take place between the post holder and their line manager to discuss progress. At the end of the probationary period, and subject to a satisfactory report by the appropriate head of section or line manager, employees will be notified in writing that they have successfully completed their probationary period. The probationary period can be extended by a further 3 months should the individuals line manager consider this appropriate.\n7.\tRecruitment Monitoring', metadata={'source': '../data/globalcorp_hr_policy.txt'})]}
qa({"query": "What is the difference in the number of work hours in Germany vs. United States?"})
## OUTPUT ##
{'query': 'What is the difference in the number of work hours in Germany vs. United States?',
'result': 'In Germany, the standard workweek is 38 hours (Monday to Friday, 8 AM to 5 PM), while in the United States, employees adhere to a standard 40-hour workweek (Monday to Friday, 9 AM to 5 PM). So, the difference in the number of work hours between Germany and the United States is 2 hours per week.',
'source_documents': [Document(page_content='**GlobalCorp Human Resources Policy**\n\nWelcome to GlobalCorp, where our Human Resources Policy is designed to provide a comprehensive framework for employees across our offices in the United States, Germany, and Japan. We operate under an at-will employment relationship, and any contractual agreements should be documented in writing.\n\nAt the core of our culture is a commitment to professionalism and ethical conduct. Clear and respectful communication is highly valued, and a business casual dress code is encouraged.\n\n**Work Hours:**\nEmployees in the United States adhere to a standard 40-hour workweek (Monday to Friday, 9 AM to 5 PM). In Germany, the standard workweek is 38 hours (Monday to Friday, 8 AM to 5 PM), and in Japan, employees work 40 hours per week (Monday to Friday, 9 AM to 6 PM). Punctuality is paramount, and employees are expected to arrive on time. Time-off requests follow country-specific guidelines.', metadata={'source': '../data/globalcorp_hr_policy.txt'}),
Document(page_content='**Leave Policies - Germany:**\nIn Germany, generous leave policies offer 30 days of paid vacation and 20 days of paid sick leave annually. An annual budget of €1 million is allocated for leave-related expenses.\n\n**Leave Policies - Japan:**\nIn Japan, employees enjoy 20 days of paid vacation and 15 days of paid sick leave per year. An annual budget of ¥100 million is allocated for leave-related expenses.\n\n**Performance Management:**\nPerformance reviews are conducted annually, with regular feedback provided to support professional development. GlobalCorp encourages continuous learning and allocates an annual budget of $5,000 per employee for training and development opportunities.', metadata={'source': '../data/globalcorp_hr_policy.txt'}),
Document(page_content='**Compensation and Benefits - United States:**\nIn the United States, employees enjoy health insurance, dental coverage, and a 401(k) retirement plan. An annual budget of $1 million is allocated for employee benefits.\n\n**Compensation and Benefits - Germany:**\nIn Germany, benefits include health insurance, a pension plan, and an additional 5 days of vacation annually. An annual budget of €500,000 is allocated for employee benefits.\n\n**Compensation and Benefits - Japan:**\nIn Japan, employees receive health insurance, a commuting allowance, and wellness benefits. An annual budget of ¥50 million is allocated for employee benefits.\n\n**Leave Policies - United States:**\nIn the United States, employees have 15 days of paid vacation and 10 days of paid sick leave annually. An annual budget of $500,000 is allocated for leave-related expenses.', metadata={'source': '../data/globalcorp_hr_policy.txt'})]}
在手动审核过政策文档后,可以放心地说,答案是有意义的。
提问复杂/多跳问题
# incorrect as the currency conversion used is wrong. We need to fix this!
qa({"query": "What is the percentage difference in the annual budget for Japan and US?"})
## OUTPUT ##
{'query': 'What is the percentage difference in the annual budget for Japan and US?',
'result': 'The annual budget for Japan is ¥50 million, and the annual budget for the United States is $500,000\. To calculate the percentage difference, we need to convert the budgets to the same currency. Assuming an exchange rate of 1 USD = 100 JPY, the converted budget for Japan is ¥50 million = $500,000\. \n\nThe percentage difference can be calculated as follows:\n\nPercentage Difference = ((Budget for Japan - Budget for US) / Budget for US) * 100\n\n= (($500,000 - $500,000) / $500,000) * 100\n\n= (0 / $500,000) * 100\n\n= 0%\n\nTherefore, the percentage difference in the annual budget for Japan and the United States is 0%.',
'source_documents': [Document(page_content='**Grievance and Disciplinary Procedures:**\nOur grievance and disciplinary procedures are outlined on the company intranet. Termination conditions may include gross misconduct or repeated policy violations. In such cases, a disciplinary process will be followed, including a three-strike system, before termination. Employees leaving GlobalCorp should follow the exit process detailed in the employee handbook.\n\n**Confidentiality and Data Security:**\nEmployees are expected to maintain confidentiality, and strict data security measures are in place. An annual budget of $1 million is allocated for cybersecurity training and awareness programs in the U.S., while budgets for Germany and Japan are €500,000 and ¥50 million, respectively.', metadata={'source': '../data/globalcorp_hr_policy.txt'}),
Document(page_content='**Compensation and Benefits - United States:**\nIn the United States, employees enjoy health insurance, dental coverage, and a 401(k) retirement plan. An annual budget of $1 million is allocated for employee benefits.\n\n**Compensation and Benefits - Germany:**\nIn Germany, benefits include health insurance, a pension plan, and an additional 5 days of vacation annually. An annual budget of €500,000 is allocated for employee benefits.\n\n**Compensation and Benefits - Japan:**\nIn Japan, employees receive health insurance, a commuting allowance, and wellness benefits. An annual budget of ¥50 million is allocated for employee benefits.\n\n**Leave Policies - United States:**\nIn the United States, employees have 15 days of paid vacation and 10 days of paid sick leave annually. An annual budget of $500,000 is allocated for leave-related expenses.', metadata={'source': '../data/globalcorp_hr_policy.txt'}),
Document(page_content='**Leave Policies - Germany:**\nIn Germany, generous leave policies offer 30 days of paid vacation and 20 days of paid sick leave annually. An annual budget of €1 million is allocated for leave-related expenses.\n\n**Leave Policies - Japan:**\nIn Japan, employees enjoy 20 days of paid vacation and 15 days of paid sick leave per year. An annual budget of ¥100 million is allocated for leave-related expenses.\n\n**Performance Management:**\nPerformance reviews are conducted annually, with regular feedback provided to support professional development. GlobalCorp encourages continuous learning and allocates an annual budget of $5,000 per employee for training and development opportunities.', metadata={'source': '../data/globalcorp_hr_policy.txt'})]}
从数学上讲,响应并不是 100%正确。虽然它使用的逻辑是正确的(即将金额从¥转换为$),但所使用的汇率已过时。
让我们尝试通过在查询中提供汇率信息(1 美元 = 147.72 日元)来帮助它。
# Results are still slightly off!
qa({"query": "What is the percentage difference in the annual budget for Japan and US if 1 USD = 147.72 JPY?"})
## OUTPUT ##
{'query': 'What is the percentage difference in the annual budget for Japan and US if 1 USD = 147.72 JPY?',
'result': 'To calculate the percentage difference in the annual budget for Japan and the United States, we need to convert the budgets from their respective currencies to a common currency, such as USD. \n\nGiven that 1 USD = 147.72 JPY, we can convert the annual budget for Japan from JPY to USD by dividing it by the exchange rate:\n\nAnnual budget for Japan in USD = ¥50,000,000 / 147.72 = $338,164.25\n\nNow we can calculate the percentage difference between the annual budgets for Japan and the United States:\n\nPercentage difference = ((Annual budget for Japan - Annual budget for the United States) / Annual budget for the United States) * 100\n\nPercentage difference = (($338,164.25 - $1,000,000) / $1,000,000) * 100\n\nPercentage difference = (-$661,835.75 / $1,000,000) * 100\n\nPercentage difference = -66.18%\n\nTherefore, the percentage difference in the annual budget for Japan and the United States is approximately -66.18%.',
'source_documents': [Document(page_content='**Grievance and Disciplinary Procedures:**\nOur grievance and disciplinary procedures are outlined on the company intranet. Termination conditions may include gross misconduct or repeated policy violations. In such cases, a disciplinary process will be followed, including a three-strike system, before termination. Employees leaving GlobalCorp should follow the exit process detailed in the employee handbook.\n\n**Confidentiality and Data Security:**\nEmployees are expected to maintain confidentiality, and strict data security measures are in place. An annual budget of $1 million is allocated for cybersecurity training and awareness programs in the U.S., while budgets for Germany and Japan are €500,000 and ¥50 million, respectively.', metadata={'source': '../data/globalcorp_hr_policy.txt'}),
Document(page_content='**Compensation and Benefits - United States:**\nIn the United States, employees enjoy health insurance, dental coverage, and a 401(k) retirement plan. An annual budget of $1 million is allocated for employee benefits.\n\n**Compensation and Benefits - Germany:**\nIn Germany, benefits include health insurance, a pension plan, and an additional 5 days of vacation annually. An annual budget of €500,000 is allocated for employee benefits.\n\n**Compensation and Benefits - Japan:**\nIn Japan, employees receive health insurance, a commuting allowance, and wellness benefits. An annual budget of ¥50 million is allocated for employee benefits.\n\n**Leave Policies - United States:**\nIn the United States, employees have 15 days of paid vacation and 10 days of paid sick leave annually. An annual budget of $500,000 is allocated for leave-related expenses.', metadata={'source': '../data/globalcorp_hr_policy.txt'}),
Document(page_content='**Leave Policies - Germany:**\nIn Germany, generous leave policies offer 30 days of paid vacation and 20 days of paid sick leave annually. An annual budget of €1 million is allocated for leave-related expenses.\n\n**Leave Policies - Japan:**\nIn Japan, employees enjoy 20 days of paid vacation and 15 days of paid sick leave per year. An annual budget of ¥100 million is allocated for leave-related expenses.\n\n**Performance Management:**\nPerformance reviews are conducted annually, with regular feedback provided to support professional development. GlobalCorp encourages continuous learning and allocates an annual budget of $5,000 per employee for training and development opportunities.', metadata={'source': '../data/globalcorp_hr_policy.txt'})]}
有趣的是,LLM 能够将汇率作为计算的一部分,并且它给出的答案(即$338,164.25)非常接近实际答案(即 338,478.20)。不过,还是有改进的空间。
让我们尝试另一个问题,这次是一个比较问题:
# incorrect as technically US has higher budget after conversion
qa({"query": "Which country has the highest budget?"})
## OUTPUT ##
{'query': 'Which country has the highest budget?',
'result': 'Japan has the highest budget for employee benefits, with an annual allocation of ¥50 million.',
'source_documents': [Document(page_content='**Grievance and Disciplinary Procedures:**\nOur grievance and disciplinary procedures are outlined on the company intranet. Termination conditions may include gross misconduct or repeated policy violations. In such cases, a disciplinary process will be followed, including a three-strike system, before termination. Employees leaving GlobalCorp should follow the exit process detailed in the employee handbook.\n\n**Confidentiality and Data Security:**\nEmployees are expected to maintain confidentiality, and strict data security measures are in place. An annual budget of $1 million is allocated for cybersecurity training and awareness programs in the U.S., while budgets for Germany and Japan are €500,000 and ¥50 million, respectively.', metadata={'source': '../data/globalcorp_hr_policy.txt'}),
Document(page_content='**Compensation and Benefits - United States:**\nIn the United States, employees enjoy health insurance, dental coverage, and a 401(k) retirement plan. An annual budget of $1 million is allocated for employee benefits.\n\n**Compensation and Benefits - Germany:**\nIn Germany, benefits include health insurance, a pension plan, and an additional 5 days of vacation annually. An annual budget of €500,000 is allocated for employee benefits.\n\n**Compensation and Benefits - Japan:**\nIn Japan, employees receive health insurance, a commuting allowance, and wellness benefits. An annual budget of ¥50 million is allocated for employee benefits.\n\n**Leave Policies - United States:**\nIn the United States, employees have 15 days of paid vacation and 10 days of paid sick leave annually. An annual budget of $500,000 is allocated for leave-related expenses.', metadata={'source': '../data/globalcorp_hr_policy.txt'})]}
由于在与其他国家(如美国、德国等)比较预算时没有进行货币兑换,因此该响应不正确。
观察:如果我们有一种系统化的方式来规划中间步骤,而不是直接跳到最终答案,那么上述所有问题都可以可靠地回答。为此,让我们引入agents。
ReAct Agent
在本教程中,我们将使用 LangChain 实现的 ReAct(Reason + Act)代理,最早在这篇论文中介绍。论文的关键要点是,如果我们提示 LLM 按步骤生成推理过程和任务特定的行动,其在任务上的表现会有所提升。换句话说,我们明确要求它进行多个思考-行动-观察步骤来解决任务实例,而不是一步到位地得出最终答案(这最终会减少幻觉的发生)。
除了 ReAct,LangChain 还支持其他代理,例如Open AI tools、XML、StructuredChat、Self Ask with Search等,我强烈建议你阅读这里的相关内容。这里需要注意的一点是,ReAct 代理只能支持那些只接受一个输入参数的工具(例如,在上述描述的工具中,它可以支持Tool_Length、Tool_Date和Tool_Search)。如果你想使用需要多个输入的工具(例如Tool_PercCalculator),你最好使用Open AI Tools代理或Open AI Functions代理。
注意:“OpenAI 将调用单一功能的能力称为
functions,而调用一个或多个功能的能力称为tools。根据官网信息,functions现在被视为一个遗留选项,已被tools取代。因此,如果你在使用 OpenAI 模型创建代理,应该使用这个OpenAI Tools代理,而不是OpenAI functions代理。”[来源]
为代理定义工具
如上所述,我们首先需要定义这个代理将访问的工具。首先,我们只定义一个工具:tool_search。
tool_search:给定一个搜索查询,我们需要一个工具来返回相关的 HR 文档片段。但是等等,这不正是我们 PDR 检索器所做的吗?事实上,我们可以使用create_retriever_tool()轻松地将我们的检索器转换成一个工具。
from langchain.tools.retriever import create_retriever_tool
tool_search = create_retriever_tool(
retriever=retriever,
name="search_hr_policy",
description="Searches and returns excerpts from the HR policy.",
)
一些提示:
-
该工具的
name和description将在 API 调用中传递给 LLM,因此请确保它尽可能清晰,以便代理能够理解。在我们的例子中,我们已明确指定该工具返回 HR 政策中的摘录(即片段)。 -
在底层,这个工具使用了
retriever的get_relevant_documents()函数。你可以通过.func来检查它:
tool_search.func
## Output ##
<bound method BaseRetriever.get_relevant_documents of ParentDocumentRetriever(vectorstore=<langchain_community.vectorstores.chroma.Chroma object at 0x11f0c5f10>, docstore=<langchain.storage.encoder_backed.EncoderBackedStore object at 0x11f0c5f40>, child_splitter=<langchain.text_splitter.RecursiveCharacterTextSplitter object at 0x11f0c5430>)>
- 你甚至可以通过
.schema()来检查这个工具的架构。它有助于验证调用该工具所需的required参数。
tool.args_schema.schema()
## Output ##
{'title': 'RetrieverInput',
'description': 'Input to the retriever.',
'type': 'object',
'properties': {'query': {'title': 'Query',
'description': 'query to look up in retriever',
'type': 'string'}},
'required': ['query']}
专业提示:你可以使用*tool.invoke({"inp_param_name": inp_param_value})*快速测试自定义工具是否已正确设置。例如:*tool_search.invoke({“query”: “在这里输入查询”})*
最后,让我们使用一个强调多步骤思考-行动-观察的提示来设置 ReAct 代理。幸运的是,这个提示已经在 LangChain hub 上提供(你也可以通过定义自己的提示来覆盖它)。该提示模板需要三个输入变量,即 tools、input 和 agent_scratchpad。
from langchain import hub
prompt = hub.pull("hwchase17/react")
print(prompt.template)
## OUTPUT ##
Answer the following questions as best you can. You have access to the following tools:
{tools}
Use the following format:
Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question
Begin!
Question: {input}
Thought:{agent_scratchpad}
注意 1:虽然这个提示在 10 次中有 8 次可以正常工作,但我强烈建议根据你的使用场景进行修改——特别是当代理在执行思考或行动的顺序时感到困惑时。例如,在一个以比较为基础的问题的项目中(如“比较过去一年中国和美国的销售增长”),这是我更新 react 提示的方式(并引入了一个新的输入变量 *{today_date}*):
Use the context given to you to answer the question.
When you come across time based questions like Has there been increase in sales from April last year and August last year,
use today's date as {today_date} for fetching exact start and end dates. Then your first action should be to fetch the sales data with metadata time between April 1st to April 30th.
That will be your answer 1\. Your next action will be to fetch the sales data with metadata time between August 1st to
August 31st, this will be your answer 2\. Now analyse and understand each of answer 1 and answer 2, calculate the
percentange change, compare them and answer the question.
You have access to the following tools:
{tools}
Use the following format:
Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question
Begin!
Question: {input}
Thought:{agent_scratchpad}
注意 2:进行上述操作(即更详细/更长的提示)可能会指数级增加你的用例的 token 数量。我的建议是,如果你提前知道行动/思考的执行顺序,那么切换到 LLM 的链式调用会更有效 如果 (且这是一个很大的前提条件)。例如:如果你知道你的系统需要回答的唯一类型问题是像我上面提到的比较问题,那么创建一个 LLM 的顺序链条(一个输出作为另一个输入)会更有意义。我在之前的* 文章* 中讲解了如何在 LangChain 中实现这一技术。*
创建 ReAct 代理
# only creates the logical steps for us
react_agent = create_react_agent(llm, [tool_search], prompt)
我们还需要实例化 AgentExecutor,它将执行 react_agent 生成的逻辑步骤。
# executes the logical steps we created
agent_executor = AgentExecutor(
agent=react_agent,
tools=[tool],
verbose=True,
handle_parsing_errors=True,
max_iterations = 5 # useful when agent is stuck in a loop
)
测试 ReAct 代理
最后,是时候在与之前相同的示例查询上进行测试了。
注意:虽然我很想为这篇文章挑选一些例子,但展示这些代理有时不可靠是很重要的,很多测试需要进行以确认它们的能力和局限性。
query = "Which country has the highest budget?"
agent_executor.invoke({"input": query})
# if you are using the updated prompt with today's date passed to it
# agent_executor.invoke({"input": query, "today_date": date.today()})
## OUTPUT ##
> Entering new AgentExecutor chain...
I don't have access to information about country budgets. I should try searching for this information.
Action: search_hr_policy
Action Input: "highest country budget"[Document(page_content='**Grievance and Disciplinary Procedures:**\nOur grievance and disciplinary procedures are outlined on the company intranet. Termination conditions may include gross misconduct or repeated policy violations. In such cases, a disciplinary process will be followed, including a three-strike system, before termination. Employees leaving GlobalCorp should follow the exit process detailed in the employee handbook.\n\n**Confidentiality and Data Security:**\nEmployees are expected to maintain confidentiality, and strict data security measures are in place. An annual budget of $1 million is allocated for cybersecurity training and awareness programs in the U.S., while budgets for Germany and Japan are €500,000 and ¥50 million, respectively.', metadata={'source': '../data/globalcorp_hr_policy.txt'}), Document(page_content='**Health and Safety:**\nWorkplace safety is a shared responsibility. Emergency procedures are clearly posted throughout our office buildings, and an annual budget of $10,000, €5,000, and ¥1 million is allocated for safety drills and equipment maintenance in the U.S., Germany, and Japan, respectively.\n\n**Communication:**\nImportant updates are conveyed through company-wide emails and team meetings. An annual budget of $500,000, €250,000, and ¥25 million is allocated for communication initiatives, including employee engagement events in the U.S., Germany, and Japan, respectively.\n\nThis policy undergoes an annual review to ensure relevance and compliance. Welcome to GlobalCorp, where our commitment to a diverse, inclusive, and respectful workplace is the foundation of our success.\n\n1.\tRecruitment and Selection\n1.\tIntroduction', metadata={'source': '../data/globalcorp_hr_policy.txt'}), Document(page_content='**Compensation and Benefits - United States:**\nIn the United States, employees enjoy health insurance, dental coverage, and a 401(k) retirement plan. An annual budget of $1 million is allocated for employee benefits.\n\n**Compensation and Benefits - Germany:**\nIn Germany, benefits include health insurance, a pension plan, and an additional 5 days of vacation annually. An annual budget of €500,000 is allocated for employee benefits.\n\n**Compensation and Benefits - Japan:**\nIn Japan, employees receive health insurance, a commuting allowance, and wellness benefits. An annual budget of ¥50 million is allocated for employee benefits.\n\n**Leave Policies - United States:**\nIn the United States, employees have 15 days of paid vacation and 10 days of paid sick leave annually. An annual budget of $500,000 is allocated for leave-related expenses.', metadata={'source': '../data/globalcorp_hr_policy.txt'})]I found information about budgets in the HR policy, but it seems to be related to cybersecurity training, safety drills, and communication initiatives. It doesn't provide information about country budgets. I need to search for information about country budgets elsewhere.
Action: search_hr_policy
Action Input: "country budgets"[Document(page_content='**Grievance and Disciplinary Procedures:**\nOur grievance and disciplinary procedures are outlined on the company intranet. Termination conditions may include gross misconduct or repeated policy violations. In such cases, a disciplinary process will be followed, including a three-strike system, before termination. Employees leaving GlobalCorp should follow the exit process detailed in the employee handbook.\n\n**Confidentiality and Data Security:**\nEmployees are expected to maintain confidentiality, and strict data security measures are in place. An annual budget of $1 million is allocated for cybersecurity training and awareness programs in the U.S., while budgets for Germany and Japan are €500,000 and ¥50 million, respectively.', metadata={'source': '../data/globalcorp_hr_policy.txt'}), Document(page_content='**GlobalCorp Human Resources Policy**\n\nWelcome to GlobalCorp, where our Human Resources Policy is designed to provide a comprehensive framework for employees across our offices in the United States, Germany, and Japan. We operate under an at-will employment relationship, and any contractual agreements should be documented in writing.\n\nAt the core of our culture is a commitment to professionalism and ethical conduct. Clear and respectful communication is highly valued, and a business casual dress code is encouraged.\n\n**Work Hours:**\nEmployees in the United States adhere to a standard 40-hour workweek (Monday to Friday, 9 AM to 5 PM). In Germany, the standard workweek is 38 hours (Monday to Friday, 8 AM to 5 PM), and in Japan, employees work 40 hours per week (Monday to Friday, 9 AM to 6 PM). Punctuality is paramount, and employees are expected to arrive on time. Time-off requests follow country-specific guidelines.', metadata={'source': '../data/globalcorp_hr_policy.txt'}), Document(page_content='**Compensation and Benefits - United States:**\nIn the United States, employees enjoy health insurance, dental coverage, and a 401(k) retirement plan. An annual budget of $1 million is allocated for employee benefits.\n\n**Compensation and Benefits - Germany:**\nIn Germany, benefits include health insurance, a pension plan, and an additional 5 days of vacation annually. An annual budget of €500,000 is allocated for employee benefits.\n\n**Compensation and Benefits - Japan:**\nIn Japan, employees receive health insurance, a commuting allowance, and wellness benefits. An annual budget of ¥50 million is allocated for employee benefits.\n\n**Leave Policies - United States:**\nIn the United States, employees have 15 days of paid vacation and 10 days of paid sick leave annually. An annual budget of $500,000 is allocated for leave-related expenses.', metadata={'source': '../data/globalcorp_hr_policy.txt'})]I still couldn't find information about country budgets in the HR policy. I should try searching for this information using a different tool or source.
Action: search_external_source
Action Input: "highest country budget"search_external_source is not a valid tool, try one of [search_hr_policy].I couldn't find information about country budgets in the HR policy or using the available tools. I need to consult a different source or resource to find the answer to this question.
Final Answer: I don't have access to information about which country has the highest budget.
需要考虑的几点:
-
注意第一个
Action的Action Input(即“最高国家预算”)。这是将传递给 PDRretriever的get_relevant_function()的搜索查询(而不是实际的输入查询,即“哪个国家的预算最高?”)。这意味着,如果底层文档中有讨论最高国家预算的部分,我们就可以顺利找到答案!可惜,情况并非如此。 -
Observation(即运行action(工具与action inputs)后的结果)会在Action Input后立即打印。在我们的例子中,它是检索到的文档([Document(page_content=’**申诉和纪律程序:**\n 我们的申诉和纪律程序是...)),并包含回答查询所需的信息。尽管如此,最终的回答仍然不正确。[P.S. 根据我的测试,这种情况主要出现在 gpt3.5 中。] -
(为了使其与 gpt-3.5 兼容) 我尝试将搜索查询更新为
"Which of the two countries has the highest budget — Japan or Unites States?",希望代理能够识别国家名称,并连续进行两次检索调用以获取相关信息。不幸的是,最终的答案与之前相同。 -
最终,经过轻微的措辞调整,我们有了一个有效的查询(与 gpt3.5 兼容)。
主要要点:(1) 即使是看起来相似的提示,响应也可能有很大差异。(2) GPT4 比 GPT3.5 更适合实现 ReAct 代理。
query = "Is the budget for Japan different than United States?"
agent_executor.invoke({"input": query})
## OUTPUT ##
> Entering new AgentExecutor chain...
I should check the HR policy to see if there is any information about budget differences between Japan and the United States.
Action: search_hr_policy
Action Input: "budget differences between Japan and United States"[Document(page_content='**Grievance and Disciplinary Procedures:**\nOur grievance and disciplinary procedures are outlined on the company intranet. Termination conditions may include gross misconduct or repeated policy violations. In such cases, a disciplinary process will be followed, including a three-strike system, before termination. Employees leaving GlobalCorp should follow the exit process detailed in the employee handbook.\n\n**Confidentiality and Data Security:**\nEmployees are expected to maintain confidentiality, and strict data security measures are in place. An annual budget of $1 million is allocated for cybersecurity training and awareness programs in the U.S., while budgets for Germany and Japan are €500,000 and ¥50 million, respectively.', metadata={'source': '../data/globalcorp_hr_policy.txt'}), Document(page_content='**Compensation and Benefits - United States:**\nIn the United States, employees enjoy health insurance, dental coverage, and a 401(k) retirement plan. An annual budget of $1 million is allocated for employee benefits.\n\n**Compensation and Benefits - Germany:**\nIn Germany, benefits include health insurance, a pension plan, and an additional 5 days of vacation annually. An annual budget of €500,000 is allocated for employee benefits.\n\n**Compensation and Benefits - Japan:**\nIn Japan, employees receive health insurance, a commuting allowance, and wellness benefits. An annual budget of ¥50 million is allocated for employee benefits.\n\n**Leave Policies - United States:**\nIn the United States, employees have 15 days of paid vacation and 10 days of paid sick leave annually. An annual budget of $500,000 is allocated for leave-related expenses.', metadata={'source': '../data/globalcorp_hr_policy.txt'}), Document(page_content='**Leave Policies - Germany:**\nIn Germany, generous leave policies offer 30 days of paid vacation and 20 days of paid sick leave annually. An annual budget of €1 million is allocated for leave-related expenses.\n\n**Leave Policies - Japan:**\nIn Japan, employees enjoy 20 days of paid vacation and 15 days of paid sick leave per year. An annual budget of ¥100 million is allocated for leave-related expenses.\n\n**Performance Management:**\nPerformance reviews are conducted annually, with regular feedback provided to support professional development. GlobalCorp encourages continuous learning and allocates an annual budget of $5,000 per employee for training and development opportunities.', metadata={'source': '../data/globalcorp_hr_policy.txt'})]I have found information about budget differences between Japan and the United States in the HR policy.
Action: search_hr_policy
Action Input: "budget differences between Japan and United States"[Document(page_content='**Grievance and Disciplinary Procedures:**\nOur grievance and disciplinary procedures are outlined on the company intranet. Termination conditions may include gross misconduct or repeated policy violations. In such cases, a disciplinary process will be followed, including a three-strike system, before termination. Employees leaving GlobalCorp should follow the exit process detailed in the employee handbook.\n\n**Confidentiality and Data Security:**\nEmployees are expected to maintain confidentiality, and strict data security measures are in place. An annual budget of $1 million is allocated for cybersecurity training and awareness programs in the U.S., while budgets for Germany and Japan are €500,000 and ¥50 million, respectively.', metadata={'source': '../data/globalcorp_hr_policy.txt'}), Document(page_content='**Compensation and Benefits - United States:**\nIn the United States, employees enjoy health insurance, dental coverage, and a 401(k) retirement plan. An annual budget of $1 million is allocated for employee benefits.\n\n**Compensation and Benefits - Germany:**\nIn Germany, benefits include health insurance, a pension plan, and an additional 5 days of vacation annually. An annual budget of €500,000 is allocated for employee benefits.\n\n**Compensation and Benefits - Japan:**\nIn Japan, employees receive health insurance, a commuting allowance, and wellness benefits. An annual budget of ¥50 million is allocated for employee benefits.\n\n**Leave Policies - United States:**\nIn the United States, employees have 15 days of paid vacation and 10 days of paid sick leave annually. An annual budget of $500,000 is allocated for leave-related expenses.', metadata={'source': '../data/globalcorp_hr_policy.txt'}), Document(page_content='**Leave Policies - Germany:**\nIn Germany, generous leave policies offer 30 days of paid vacation and 20 days of paid sick leave annually. An annual budget of €1 million is allocated for leave-related expenses.\n\n**Leave Policies - Japan:**\nIn Japan, employees enjoy 20 days of paid vacation and 15 days of paid sick leave per year. An annual budget of ¥100 million is allocated for leave-related expenses.\n\n**Performance Management:**\nPerformance reviews are conducted annually, with regular feedback provided to support professional development. GlobalCorp encourages continuous learning and allocates an annual budget of $5,000 per employee for training and development opportunities.', metadata={'source': '../data/globalcorp_hr_policy.txt'})]I have found information about budget differences between Japan and the United States in the HR policy.
Action: search_hr_policy
Action Input: "budget differences between Japan and United States"[Document(page_content='**Grievance and Disciplinary Procedures:**\nOur grievance and disciplinary procedures are outlined on the company intranet. Termination conditions may include gross misconduct or repeated policy violations. In such cases, a disciplinary process will be followed, including a three-strike system, before termination. Employees leaving GlobalCorp should follow the exit process detailed in the employee handbook.\n\n**Confidentiality and Data Security:**\nEmployees are expected to maintain confidentiality, and strict data security measures are in place. An annual budget of $1 million is allocated for cybersecurity training and awareness programs in the U.S., while budgets for Germany and Japan are €500,000 and ¥50 million, respectively.', metadata={'source': '../data/globalcorp_hr_policy.txt'}), Document(page_content='**Compensation and Benefits - United States:**\nIn the United States, employees enjoy health insurance, dental coverage, and a 401(k) retirement plan. An annual budget of $1 million is allocated for employee benefits.\n\n**Compensation and Benefits - Germany:**\nIn Germany, benefits include health insurance, a pension plan, and an additional 5 days of vacation annually. An annual budget of €500,000 is allocated for employee benefits.\n\n**Compensation and Benefits - Japan:**\nIn Japan, employees receive health insurance, a commuting allowance, and wellness benefits. An annual budget of ¥50 million is allocated for employee benefits.\n\n**Leave Policies - United States:**\nIn the United States, employees have 15 days of paid vacation and 10 days of paid sick leave annually. An annual budget of $500,000 is allocated for leave-related expenses.', metadata={'source': '../data/globalcorp_hr_policy.txt'}), Document(page_content='**Leave Policies - Germany:**\nIn Germany, generous leave policies offer 30 days of paid vacation and 20 days of paid sick leave annually. An annual budget of €1 million is allocated for leave-related expenses.\n\n**Leave Policies - Japan:**\nIn Japan, employees enjoy 20 days of paid vacation and 15 days of paid sick leave per year. An annual budget of ¥100 million is allocated for leave-related expenses.\n\n**Performance Management:**\nPerformance reviews are conducted annually, with regular feedback provided to support professional development. GlobalCorp encourages continuous learning and allocates an annual budget of $5,000 per employee for training and development opportunities.', metadata={'source': '../data/globalcorp_hr_policy.txt'})]I have found information about budget differences between Japan and the United States in the HR policy.
Action: search_hr_policy
Action Input: "budget differences between Japan and United States"[Document(page_content='**Grievance and Disciplinary Procedures:**\nOur grievance and disciplinary procedures are outlined on the company intranet. Termination conditions may include gross misconduct or repeated policy violations. In such cases, a disciplinary process will be followed, including a three-strike system, before termination. Employees leaving GlobalCorp should follow the exit process detailed in the employee handbook.\n\n**Confidentiality and Data Security:**\nEmployees are expected to maintain confidentiality, and strict data security measures are in place. An annual budget of $1 million is allocated for cybersecurity training and awareness programs in the U.S., while budgets for Germany and Japan are €500,000 and ¥50 million, respectively.', metadata={'source': '../data/globalcorp_hr_policy.txt'}), Document(page_content='**Compensation and Benefits - United States:**\nIn the United States, employees enjoy health insurance, dental coverage, and a 401(k) retirement plan. An annual budget of $1 million is allocated for employee benefits.\n\n**Compensation and Benefits - Germany:**\nIn Germany, benefits include health insurance, a pension plan, and an additional 5 days of vacation annually. An annual budget of €500,000 is allocated for employee benefits.\n\n**Compensation and Benefits - Japan:**\nIn Japan, employees receive health insurance, a commuting allowance, and wellness benefits. An annual budget of ¥50 million is allocated for employee benefits.\n\n**Leave Policies - United States:**\nIn the United States, employees have 15 days of paid vacation and 10 days of paid sick leave annually. An annual budget of $500,000 is allocated for leave-related expenses.', metadata={'source': '../data/globalcorp_hr_policy.txt'}), Document(page_content='**Leave Policies - Germany:**\nIn Germany, generous leave policies offer 30 days of paid vacation and 20 days of paid sick leave annually. An annual budget of €1 million is allocated for leave-related expenses.\n\n**Leave Policies - Japan:**\nIn Japan, employees enjoy 20 days of paid vacation and 15 days of paid sick leave per year. An annual budget of ¥100 million is allocated for leave-related expenses.\n\n**Performance Management:**\nPerformance reviews are conducted annually, with regular feedback provided to support professional development. GlobalCorp encourages continuous learning and allocates an annual budget of $5,000 per employee for training and development opportunities.', metadata={'source': '../data/globalcorp_hr_policy.txt'})]I have found information about budget differences between Japan and the United States in the HR policy.
Final Answer: According to the HR policy, the annual budget for employee benefits in the United States is $1 million, while the budget for Japan is ¥50 million.
> Finished chain.
P.S. 尽管在第一次迭代中就揭示了相关答案,但我对不必要的行动-行动输入循环并不满意。这是我当前正在调试的部分。
理解代理的实现
LangChain 库刚开始可能有点令人生畏,如果你想调试与 react 代理相关的底层工作原理,这里有一些有用的 调试器中的断点设置**。
I. ReAct 代理的设置:在这里你将看到代理在每次迭代时所采取的四个主要步骤(通过|符号连接)。(*我还包含了片段,展示了每个步骤的输入/输出。)
P.S. 如果你是第一次看到这个管道符号* *|* 在 LangChain 中,我建议先阅读 这个 和 这个 。简单来说,*|*符号将第一个步骤的输出作为输入传递给链中的下一步。*
(a) Runnable.assign():使用观察结果更新agent_scratchpad,即所有先前的思考-行动-观察,并创建一个字典,可以将其作为输入传递给下一步,即PromptTemplate。
尽管我在下面的片段中使用了虚拟数据,但典型的agent_scratchpad应该如下所示:
`I need to check if there is any information in the HR policy regarding budget allocation for different countries.\nAction: search_hr_policy\nAction Input: “budget allocation for different countries”\nObservation: [Document(page_content=\’**Griev....]metadata={\’source\’: \’../data/globalcorp_hr_policy.txt\’})]\nThought:
# Testing Runnable in isolation
from langchain_core.agents import AgentAction
from langchain_core.runnables import Runnable, RunnablePassthrough
agent_1 = RunnablePassthrough.assign(
agent_scratchpad=lambda x: format_log_to_str(x["intermediate_steps"])
)
input = {
"input": "I love programming",
"intermediate_steps": [
(AgentAction(tool="DummyTool", tool_input="Foo", log="Some log here"), "Result of the tool")
],
}
output_1 = agent_1.invoke(input)
## OUTPUT ##
{'input': 'I love programming',
'intermediate_steps': [(AgentAction(tool='DummyTool', tool_input='Foo', log='Some log here'),
'Result of the tool')],
'agent_scratchpad': 'Some log here\nObservation: Result of the tool\nThought: '}
(b) PromptTemplate:基于更新后的agent_scratchpad框架最终的 react 提示,以便调用 LLM,并创建一个StringPromptValue。
(注意:根据 react 提示模板,我们还需要另一个名为tools的input_variables,它已经通过prompt.partial在这里附加了。)
# Testing PromptTemplate in isolation
agent_2 = prompt
output_2 = agent_2.invoke(output_1)
## OUTPUT ##
StringPromptValue(text='Answer the following questions as best you can. You have access to the following tools:\n\nsearch_hr_policy: Searches and returns excerpts from the HR policy.\n\nUse the following format:\n\nQuestion: the input question you must answer\nThought: you should always think about what to do\nAction: the action to take, should be one of [search_hr_policy]\nAction Input: the input to the action\nObservation: the result of the action\n... (this Thought/Action/Action Input/Observation can repeat N times)\nThought: I now know the final answer\nFinal Answer: the final answer to the original input question\n\nBegin!\n\nQuestion: I love programming\nThought:Some log here\nObservation: Result of the tool\nThought: ')
(c) AzureChatOpenAI:将提示传递给llm进行生成步骤并获取响应,即AIMessage。
# Testing llm in isolation
from langchain_core.prompt_values import StringPromptValue
inp = StringPromptValue(text='Repeat everything I say - "Parrot is a bird')
llm_with_stop = llm.bind(stop=["bird"])
agent_3 = llm_with_stop
agent_3.invoke(inp)
## OUTPUT ##
AIMessage(content='Parrot is a ')
(d) ReActSingleInputOutputParser(): 解析llm返回的输出(即AIMessage)。
from langchain_core.messages.ai import AIMessage
content = AIMessage(content="I found information about the budget differences between US and Japan in the HR policy. I need to use the currency conversion tool to compare the budgets in USD.\nAction: Currency conversion\nAction Input: ¥50 million")
agent = ReActSingleInputOutputParser()
agent.invoke(content)
## OUTPUT ##
AgentAction(tool='Currency conversion', tool_input='¥50 million', log='I found information about the budget differences between US and Japan in the HR policy. I need to use the currency conversion tool to compare the budgets in USD.\nAction: Currency conversion\nAction Input: ¥50 million')
II. 工作中的代理:这里可以看到用于遍历之前四个步骤的for循环。可以自由设置观察变量并检查中间结果。在完成所有四个步骤后,最终的响应类型要么是AgentAction(是否调用另一个工具),要么是AgentFinish(结束循环)。下面是我在四个步骤中的调试器快照:




代理的中间输出
III. 深入探讨parse() 函数 的实现:如果你想了解如何决定AIMessage应该导致AgentAction还是AgentFinish。
IV. 使用自定义工具:这里可以看到代理使用自定义工具的过程(如果返回了AgentAction)。
V. [while](https://github.com/langchain-ai/langchain/blob/ac970c9497e2aca1f6396c3f6954b4f6cd0ac879/libs/langchain/langchain/agents/agent.py#L1390) 循环,它使代理持续循环(除非遇到AgentFinish或发生超时)并在每次迭代后更新中间步骤。
注意:中间步骤是观测结果的集合,而这些观测通常是工具的输出。所以在检索工具的情况下,它将是文档的列表,在货币转换的情况下,它将是一个数字,等等。

中间步骤示例
最后的思考:如果预期的问答系统问题本质上是基本的,意味着它们可以通过标准的基于检索的问答机制来充分处理,而不需要多跳推理,那么最好避免使用代理。特别是当唯一需要的工具是一个“检索器变成的工具”时,在这种情况下引入代理会无谓地增加复杂性。此外,如果你使用检索器作为工具,它的get_relevant_function()的输入会被代理修改(正如你上面看到的),它会按需修改,你将不再控制它。这在某些情况下可能是个问题(尽管解决这个问题的简单方法是更新工具的描述,例如tool_search.description = “搜索并返回人力资源政策中的摘录。输入应是一个完整的问题”)。
当我们向代理提供复杂的问题和更多的工具时,代理的真正潜力才会被释放,正如我们接下来将看到的那样。
引入更多工具
让我们引入另一个工具:currency_conversion,并运行与上面相同的查询。
currency_conversion = Tool(
name="Currency conversion",
func=convert_currency_to_usd,
description="useful for converting currency into USD. Input should be an amount.",
)
这是该工具需要的一些辅助函数:
def value_to_float(x):
if type(x) == float or type(x) == int:
return x
x = x.upper()
if "MILLION" in x:
if len(x) > 1:
return float(x.replace("MILLION", "")) * 1000000
return 1000000.0
if "BILLION" in x:
return float(x.replace("BILLION", "")) * 1000000000
return 0.0
def convert_currency_to_usd(amount: str) -> int:
"Converts currency into USD"
if "¥" in amount:
exclude_symbol = amount.replace("¥", "")
amount_in_numbers = value_to_float(exclude_symbol)
return amount_in_numbers / 147.72 #harcoded the exchange rate here for simplicity
if "$" in amount:
exclude_symbol = amount.replace("$", "")
return value_to_float(exclude_symbol)
让我们重新运行之前的查询:
tools = [tool_search, currency_conversion]
react_agent = create_react_agent(llm, tools, prompt)
agent_executor = AgentExecutor(
agent=react_agent, tools=tools, verbose=True, handle_parsing_errors=True
)
query = "Is the budget for Japan different than United States?"
agent_executor.invoke({"input": query})
## OUTPUT ##
> Entering new AgentExecutor chain...
I should check the HR policy to see if there is any information about budget differences between Japan and the United States.
Action: search_hr_policy
Action Input: "budget differences Japan United States"[Document(page_content='**Grievance and Disciplinary Procedures:**\nOur grievance and disciplinary procedures are outlined on the company intranet. Termination conditions may include gross misconduct or repeated policy violations. In such cases, a disciplinary process will be followed, including a three-strike system, before termination. Employees leaving GlobalCorp should follow the exit process detailed in the employee handbook.\n\n**Confidentiality and Data Security:**\nEmployees are expected to maintain confidentiality, and strict data security measures are in place. An annual budget of $1 million is allocated for cybersecurity training and awareness programs in the U.S., while budgets for Germany and Japan are €500,000 and ¥50 million, respectively.', metadata={'source': '../data/globalcorp_hr_policy.txt'}), Document(page_content='**Compensation and Benefits - United States:**\nIn the United States, employees enjoy health insurance, dental coverage, and a 401(k) retirement plan. An annual budget of $1 million is allocated for employee benefits.\n\n**Compensation and Benefits - Germany:**\nIn Germany, benefits include health insurance, a pension plan, and an additional 5 days of vacation annually. An annual budget of €500,000 is allocated for employee benefits.\n\n**Compensation and Benefits - Japan:**\nIn Japan, employees receive health insurance, a commuting allowance, and wellness benefits. An annual budget of ¥50 million is allocated for employee benefits.\n\n**Leave Policies - United States:**\nIn the United States, employees have 15 days of paid vacation and 10 days of paid sick leave annually. An annual budget of $500,000 is allocated for leave-related expenses.', metadata={'source': '../data/globalcorp_hr_policy.txt'}), Document(page_content='**Health and Safety:**\nWorkplace safety is a shared responsibility. Emergency procedures are clearly posted throughout our office buildings, and an annual budget of $10,000, €5,000, and ¥1 million is allocated for safety drills and equipment maintenance in the U.S., Germany, and Japan, respectively.\n\n**Communication:**\nImportant updates are conveyed through company-wide emails and team meetings. An annual budget of $500,000, €250,000, and ¥25 million is allocated for communication initiatives, including employee engagement events in the U.S., Germany, and Japan, respectively.\n\nThis policy undergoes an annual review to ensure relevance and compliance. Welcome to GlobalCorp, where our commitment to a diverse, inclusive, and respectful workplace is the foundation of our success.\n\n1.\tRecruitment and Selection\n1.\tIntroduction', metadata={'source': '../data/globalcorp_hr_policy.txt'})]I found information about budget differences between Japan and the United States in the HR policy.
Action: Currency conversion
Action Input: ¥50 million338478.20200379094The budget for Japan is equivalent to $338,478.20 USD.
Final Answer: The budget for Japan is different than the United States.
> Finished chain.
{'input': 'Is the budget for Japan different than United States?',
'output': 'The budget for Japan is different than the United States.'}
你可能会想,这个答案有什么大不了的。即使没有这个工具,我们之前得到的答案也是正确的。然而,值得注意的是,它能够在得出预算确实不同的最终结论之前,先将货币转换为美元。这有助于建立对回答的信任。如果没有这个工具,我敢打赌,如果人力资源政策上显示日本和美国的预算分别为 7.41 亿日元和 500 万美元,LLM 会回应说它们的预算不同,尽管按今天的汇率转换后,应该是相同的。
一些观察:
- 由于工具及其描述会附加到提示中,模型会知道如果需要,可以使用这些工具。我认为这有助于它们在回答问题时,尽可能多地利用这些工具。因此,决定在第二个动作中使用货币转换工具。
让我们更新查询,获得实际的数字差异。
query = "Calculate the difference in company budget for Japan and United States?"
agent_executor.invoke({"input": query})
## OUTPUT ##
> Entering new AgentExecutor chain...
I need to find the company budget for Japan and the United States and then calculate the difference.
Action: search_hr_policy
Action Input: "company budget Japan"[Document(page_content='**Compensation and Benefits - United States:**\nIn the United States, employees enjoy health insurance, dental coverage, and a 401(k) retirement plan. An annual budget of $1 million is allocated for employee benefits.\n\n**Compensation and Benefits - Germany:**\nIn Germany, benefits include health insurance, a pension plan, and an additional 5 days of vacation annually. An annual budget of €500,000 is allocated for employee benefits.\n\n**Compensation and Benefits - Japan:**\nIn Japan, employees receive health insurance, a commuting allowance, and wellness benefits. An annual budget of ¥50 million is allocated for employee benefits.\n\n**Leave Policies - United States:**\nIn the United States, employees have 15 days of paid vacation and 10 days of paid sick leave annually. An annual budget of $500,000 is allocated for leave-related expenses.', metadata={'source': '../data/globalcorp_hr_policy.txt'}), Document(page_content='**Grievance and Disciplinary Procedures:**\nOur grievance and disciplinary procedures are outlined on the company intranet. Termination conditions may include gross misconduct or repeated policy violations. In such cases, a disciplinary process will be followed, including a three-strike system, before termination. Employees leaving GlobalCorp should follow the exit process detailed in the employee handbook.\n\n**Confidentiality and Data Security:**\nEmployees are expected to maintain confidentiality, and strict data security measures are in place. An annual budget of $1 million is allocated for cybersecurity training and awareness programs in the U.S., while budgets for Germany and Japan are €500,000 and ¥50 million, respectively.', metadata={'source': '../data/globalcorp_hr_policy.txt'}), Document(page_content='**Leave Policies - Germany:**\nIn Germany, generous leave policies offer 30 days of paid vacation and 20 days of paid sick leave annually. An annual budget of €1 million is allocated for leave-related expenses.\n\n**Leave Policies - Japan:**\nIn Japan, employees enjoy 20 days of paid vacation and 15 days of paid sick leave per year. An annual budget of ¥100 million is allocated for leave-related expenses.\n\n**Performance Management:**\nPerformance reviews are conducted annually, with regular feedback provided to support professional development. GlobalCorp encourages continuous learning and allocates an annual budget of $5,000 per employee for training and development opportunities.', metadata={'source': '../data/globalcorp_hr_policy.txt'}), Document(page_content='**Health and Safety:**\nWorkplace safety is a shared responsibility. Emergency procedures are clearly posted throughout our office buildings, and an annual budget of $10,000, €5,000, and ¥1 million is allocated for safety drills and equipment maintenance in the U.S., Germany, and Japan, respectively.\n\n**Communication:**\nImportant updates are conveyed through company-wide emails and team meetings. An annual budget of $500,000, €250,000, and ¥25 million is allocated for communication initiatives, including employee engagement events in the U.S., Germany, and Japan, respectively.\n\nThis policy undergoes an annual review to ensure relevance and compliance. Welcome to GlobalCorp, where our commitment to a diverse, inclusive, and respectful workplace is the foundation of our success.\n\n1.\tRecruitment and Selection\n1.\tIntroduction', metadata={'source': '../data/globalcorp_hr_policy.txt'})]I need to find the company budget for Japan and the United States and then calculate the difference.
Action: search_hr_policy
Action Input: "company budget United States"[Document(page_content='**Grievance and Disciplinary Procedures:**\nOur grievance and disciplinary procedures are outlined on the company intranet. Termination conditions may include gross misconduct or repeated policy violations. In such cases, a disciplinary process will be followed, including a three-strike system, before termination. Employees leaving GlobalCorp should follow the exit process detailed in the employee handbook.\n\n**Confidentiality and Data Security:**\nEmployees are expected to maintain confidentiality, and strict data security measures are in place. An annual budget of $1 million is allocated for cybersecurity training and awareness programs in the U.S., while budgets for Germany and Japan are €500,000 and ¥50 million, respectively.', metadata={'source': '../data/globalcorp_hr_policy.txt'}), Document(page_content='**Compensation and Benefits - United States:**\nIn the United States, employees enjoy health insurance, dental coverage, and a 401(k) retirement plan. An annual budget of $1 million is allocated for employee benefits.\n\n**Compensation and Benefits - Germany:**\nIn Germany, benefits include health insurance, a pension plan, and an additional 5 days of vacation annually. An annual budget of €500,000 is allocated for employee benefits.\n\n**Compensation and Benefits - Japan:**\nIn Japan, employees receive health insurance, a commuting allowance, and wellness benefits. An annual budget of ¥50 million is allocated for employee benefits.\n\n**Leave Policies - United States:**\nIn the United States, employees have 15 days of paid vacation and 10 days of paid sick leave annually. An annual budget of $500,000 is allocated for leave-related expenses.', metadata={'source': '../data/globalcorp_hr_policy.txt'}), Document(page_content='**Health and Safety:**\nWorkplace safety is a shared responsibility. Emergency procedures are clearly posted throughout our office buildings, and an annual budget of $10,000, €5,000, and ¥1 million is allocated for safety drills and equipment maintenance in the U.S., Germany, and Japan, respectively.\n\n**Communication:**\nImportant updates are conveyed through company-wide emails and team meetings. An annual budget of $500,000, €250,000, and ¥25 million is allocated for communication initiatives, including employee engagement events in the U.S., Germany, and Japan, respectively.\n\nThis policy undergoes an annual review to ensure relevance and compliance. Welcome to GlobalCorp, where our commitment to a diverse, inclusive, and respectful workplace is the foundation of our success.\n\n1.\tRecruitment and Selection\n1.\tIntroduction', metadata={'source': '../data/globalcorp_hr_policy.txt'})]I have found the company budget for Japan and the United States. Now I can calculate the difference.
Action: Currency conversion
Action Input: ¥50 million338478.20200379094I have converted the budget for Japan into USD.
Action: Currency conversion
Action Input: $1 million1000000.0I have converted the budget for the United States into USD.
Thought: I have the budgets for Japan and the United States in USD. Now I can calculate the difference.
Final Answer: The difference in company budget for Japan and the United States is $661,521.80.
> Finished chain.
{'input': 'Calculate the difference in company budget for Japan and United States?',
'output': 'The difference in company budget for Japan and the United States is $661,521.80.'}
LLM 在处理减法时做得很好(尽管我仍然谨慎依赖 LLM 进行任何类型的计算)。如果我们想让它更加健壮,我们可以添加另一个工具,比如calculator_subtract来计算两个数字之间的差异。如我之前提到的,ReAct 代理无法处理多输入工具,做这件事会引发错误。这就是 Open AI 工具代理的用武之地。
Open AI 工具代理
让我们创建一个新工具——perc_diff(),它接受两个数字作为输入,并计算这两个数字之间的百分比差异。
class Metrics(BaseModel):
num1: float = Field(description="Value 1")
num2: float = Field(description="Value 2")
@tool(args_schema=Metrics)
def perc_diff(num1: float, num2: float) -> float:
"""Calculates the percentage difference between two numbers"""
return (np.abs(num1 - num2) / ((num1+num2)/2)) * 100
注意:初始化相同工具的另一种方式(提供更多的控制权来进行设置)
from langchain.tools import BaseTool, StructuredTool
class Metrics(BaseModel):
num1: float = Field(description="Value 1")
num2: float = Field(description="Value 2")
def perc_diff(num1: float, num2: float) -> float:
"""Calculates the percentage difference between two numbers"""
return (np.abs(num1 - num2) / ((num1+num2)/2)) * 100
percentage_difference = StructuredTool.from_function(
func=perc_diff,
name="PercentageDifference", # make sure there are no spaces as OAI Tools agent will throw an error
description="calculates percentage difference between 2 numbers",
args_schema=Metrics,
return_direct=False,
)
最后,所有部分就绪,让我们使用create_openai_tools_agent
tools = [tool_search, currency_conversion, perc_diff]
prompt_oai_tools = hub.pull("hwchase17/openai-tools-agent")
oaitools_agent = create_openai_tools_agent(llm, tools, prompt_oai_tools)
oaitools_agent_executor = AgentExecutor(
agent=oaitools_agent, tools=tools, verbose=True, handle_parsing_errors=True
)
query = "As per the HR policy, compare the budgets for Japan and US."
oaitools_agent_executor.invoke({"input": query})
## OUTPUT ##
> Entering new AgentExecutor chain...
Invoking: `search_hr_policy` with `{'query': 'employee benefits budget Japan'}`
[Document(page_content='**Compensation and Benefits - Japan:**\nIn Japan,....]
Invoking: `search_hr_policy` with `{'query': 'employee benefits budget US'}`
[Document(page_content='**Compensation and Benefits - United States:**\nIn the United States,....]
Invoking: `Currency_conversion` with `JPY 50000000`
338478.20200379094
Invoking: `Currency_conversion` with `USD 1000000`
USD 1000000
Invoking: `percentage_difference` with `{'metric1': 338478.20200379094, 'metric2': 1000000}`
98.85The percentage difference in budgets for employee benefits between Japan and the US is approximately 98.85%, with the US budget being higher.
> Finished chain.
{'input': 'As per the HR policy, what is the percentage difference in budgets for employee benefits in Japan vs US.',
'output': 'The percentage difference in budgets for employee benefits between Japan and the US is approximately 98.85%, with the US budget being higher.'}
注意:运行这行代码时,您可能会遇到类似 这个 的错误,即*Unrecognized request argument supplied: tools*。这意味着在 API 调用到 *llm*时,系统无法识别 *tools* 参数。鉴于只有新版 API 能识别该参数,这就意味着您正在使用旧版模型。您可以通过以下方式修复这个问题:
- 如果你使用的是 Azure Open AI 服务 — 请部署新版模型(见下图),并在你的代码库中更新
*deployment_name*,即*llm=AzureChatOpenAI(deployment_name=..., )*

Azure Open AI 上的可用模型
- 如果你直接使用 Open AI 的 API — 请检查模型是否为 这个 列表中的新版。
为了测试这个是否解决了问题,这里有一个版本更新前后的比较(完整代码片段可以在 这里找到):
# Before
# Model name: gpt-35-turbo
# Model version: 0301
response = llm(messages=[message], tools=tools)
## OUTPUT ##
openai.error.InvalidRequestError: Unrecognized request argument supplied: tools
# After
# Model name: gpt-35-turbo
# Model version: 0613
response = llm(messages=[message], tools=tools)
## OUTPUT ##
content='' additional_kwargs={'tool_calls': [<OpenAIObject id=call_8lvikb3ZqflGrr2xGgPGWXoJ at 0x10aa67270> JSON: {
"id": "call_8lvikb3ZqflGrr2xGgPGWXoJ",
"type": "function",
"function": {
"name": "get_current_weather",
"arguments": "{\n \"location\": \"San Francisco\"\n}"
}
}]}
结论
在深入研究 ReAct 代理的内部工作原理后,我希望你在实施此代理到你的项目时能更加自信。本文只是略有涉及,实际上还有很多内容需要探讨。例如,如何为这些问答系统添加记忆,以便你可以以聊天的方式使用它们。
一如既往,如果有更简单的方式来做/解释本文中提到的一些内容,请告知我。一般来说,避免发表未经请求的破坏性/无意义/敌意评论!
下次再见 ✨
## 通过 LangChain 轻松了解 LLM、代理和工具的串联
了解代理、工具和提示的基本概念,以及在此过程中获得的一些学习经验
[towardsdatascience.com [## 让你成为房间里最聪明的人的 3 个应用
最少的每日时间投入
medium.com ## 谷歌的 NotebookLM 会颠覆播客行业吗?
尤其是如果只需要点击 1 次就能将任何内容转成播客
[towardsdatascience.com
使用线性方程 + LLM 来解决 LinkedIn 皇后游戏

皇后(使用 Canva 生成)
提示 GPT 生成并解决使用 PuLP 的线性方程
·发表于Towards Data Science ·阅读时间 11 分钟·2024 年 10 月 7 日
--
在我的《我如何解决 LinkedIn 皇后游戏》文章中,我讨论了使用回溯法解决 LinkedIn 皇后游戏的问题,这是一种试探法。它将一个Queen放置在网格的某个单元格中,并继续按照约束条件放置,直到无法继续为止。这时,它会回滚上一个放置的Queen,并不断重复这一过程,直到找到解答或遍历完网格中的所有单元格。由于 LinkedIn 保证总是存在解,我们的回溯方法是完美的,必定能成功。然而,它的时间复杂度为O(n²),但仍能在不到 0.1 秒的时间内解决,因为截至目前我玩过的最大网格只有 10x10。
使用 OpenCV 自动检测谜题并重新绘制最终答案
towardsdatascience.com
尽管回溯法在时间复杂度和实现上表现良好,但我们可以更进一步,尝试寻找更好的解决方法。你知道吗,游戏中的所有约束条件都可以转化为一组线性方程?当我们解决这些方程时……
使用 Llama 3 构建 AI 代理
使用 Llama 3 的功能调用能力构建 AI 代理的综合指南。
·发布于 Towards Data Science ·阅读时间 10 分钟·2024 年 7 月 31 日
--

图片由作者提供,通过 Canva
介绍
假设你想购买某些商品。你访问一个电商网站,使用搜索选项来找到你想要的东西。也许你有多个商品要购买,因此这个过程效率不高。现在考虑这种情形:打开一个应用程序,直接用简洁的英文描述你想要的商品,然后按回车键。你不必担心搜索和价格比较,因为应用程序会自动为你处理这些。很酷,对吧?这正是我们将在本教程中构建的功能。
让我们先看一些示例。

用户一次请求多个商品

用户正在寻求最具性价比的购买选项。
好的,让我们为这个应用程序赋予生命。我们将使用 Meta 的 Llama 3 模型,带有功能调用能力。然而,这也可以通过使用 3.1 模型来完成。根据Meta 的公告,3.1 模型能够更有效地使用工具和功能。



浙公网安备 33010602011771号