docs-merge-06
TowardsDataScience 2024 中文翻译(七)
《Python 设计模式与机器学习工程师:模板方法》

图片由Pawel Czerwinski提供,来源于Unsplash
学习如何使用模板设计模式来增强你的代码
·发表于Towards Data Science ·4 分钟阅读·2024 年 12 月 24 日
--
介绍
最近我一直在进行几个大语言模型(LLM)的领域特定微调工作。这项任务的第一部分,也许是最重要的一部分,就是收集、抓取和清理文本数据,以供 LLM 使用。我注意到我的代码变得很乱,重复了很多,因为每当识别到一个数据源时,我都在从头编写一个脚本,而这个脚本和代码库中的其他脚本有很多相似之处。我完全没有遵循“不要重复自己”(DRY)原则。这就是为什么我决定实现模板设计模式,使我的代码库更加优雅和高效。
模板设计模式
我在这里不再重复设计模式是什么,以及我们如何根据功能对设计模式进行分类,因为我已经写过很多相关文章。如果你有兴趣阅读我以前关于这个主题的文章,我会在最后提供一些参考文献。
在这篇文章中,我将展示一个与数据处理相关的例子。假设在我们的项目中,我们需要处理不同类型的数据,这些数据我们希望进行分析。其中一些数据是…
设计以 AI 驱动的软件工程团队
生成性人工智能(Gen AI)即将颠覆我们今天开发应用程序的方式。了解它将如何影响技术团队,以及我们可以做些什么应对。
·发布于Towards Data Science ·46 分钟阅读·2024 年 1 月 11 日
--

来源:DALL-E,及作者的想象力
介绍
欢迎来到 2024 年!变化正在悄然来临。你感觉到了吗?
在生成性人工智能(Gen AI)的领域,人类的聪明才智已经带领我们走得比我们曾经想象的更远。这些创新必将颠覆我们所知的现有软件工程职业。
新的、以 AI 为核心的公司将崛起并威胁到现有企业。其结果将是深远的。我们将见证从创意到交付产品的时间大幅缩短,运营成本的大幅降低,从而彻底改变经济,最终,人类编写的软件工程的依赖需求将不断减少。是的,我说了。😱
我们这个时代的许多伟大公司都有庞大的技术团队支持其业务流程,如销售、市场营销、支持等。那些团队如何反应,将决定他们不仅如何在这些变革中保持其相关性,还将如何为他们所支持的公司带来可能无可战胜的竞争优势。
这篇两部分的文章将提供实证证据来支持上述观点,并展示技术团队可以采用的一个可能框架,以驾驭Shoggoth。
为什么是现在?
2023 年最后六个月中有三项至关重要的创新,几乎可以肯定会改变以人为主的软件工程领域。
大型语言模型(LLM)开源竞赛已经开始
大型语言模型(LLMs)是生成式人工智能的核心,而现在,开源大型语言模型正崛起,并且在性能上正在赶超目前占主导地位的专有产品——GPT4 [1]。

来源:熊等人,《ChatGPT 一周年:开源大语言模型正在迎头赶上吗?》
为什么这很重要?
为了回答这个问题,让我们快速总结一下[2]大语言模型创建的两阶段过程:

来源:作者,灵感来自[2]Andrej Karpathy《大语言模型简介》YouTube 视频
如上所示,第一阶段,也就是 LLM 的预训练,是不可能在极其有限的预算下完成的,或者在某人的车库里完成的。然而,像 Mistral AI、Meta,甚至现在的微软(而非 OpenAI)[3]等公司,开始推出高质量的开源软件(OSS)预训练基础模型,如 Mistral 和 LLAMA,这改变了游戏规则。突然间,较小的公司甚至个人开始创建专门的模型,并将其发布到huggingface.co——开源模型的首选平台。一些非常有趣的模型开始出现,例如 Samantha [4],一个伪感知的大语言模型,以及 Meditron [5],一个医学上非常敏锐的大语言模型。
与软件工程相关的是,越来越多专注于编程的大型语言模型开始出现,例如 codellama、deepseek coder、phi 等。我们公司 Salesforce 也发布了 CodeGen [6]。就像任何在开源软件世界中流行的事物一样,这些模型注定会越来越好。我们将看到大型语言模型不断发展,变得更加庞大、复杂和精密,能够越来越自主地创建软件。
开源大语言模型(LLMs)之所以重要的另一个关键原因是知识产权问题。并非所有公司都愿意将他们的 AI 工作交给 OpenAI,OpenAI 实际上只是一个托管其封闭源模型的外部 API。在这个 API 背后,公司无法知道他们的数据是如何被使用或共享的。显然,开源软件可以缓解这一担忧。通过开源软件,你的知识产权不会离开你的笔记本电脑、数据中心或 AWS 虚拟专用云。
于是,“小型语言模型”应运而生
对于任何低成本或无成本的开发,快速原型开发必须能够在工程师的笔记本电脑上进行。虽然训练了 7B 甚至 11B 参数的大型语言模型可以在相当不错的笔记本电脑上运行,但它们仍然会占用大量的 RAM 和 CPU/GPU 资源。此外,除非你有一台非常强大的机器,否则你可以忘记运行超过 30B 参数的大型语言模型。更进一步,当你必须将这些模型用于生产化的、始终在线的功能时,你可以预期会有一笔昂贵的托管/云计算费用。为了克服这一进入门槛,像微软这样的公司开始研究[7]如何创建更小的基础模型,并且这些模型仍然具有良好的性能,随后还开源了小型语言模型 Phi-2 [3]。他们的主要方式是使用高质量的“教科书级”数据[8]来训练这些模型,而不是使用大量低质量、可靠性参差不齐的互联网数据。
这一领域的研究将在 2024 年继续加速,因为这开始打开了一个可能性,即小型模型能够在智能手机甚至树莓派等小型电子设备上本地运行。特别是对于软件开发,这是本文的重点,这将为扩展 AI 驱动的软件工程(AI-SWE)提供一种高效且环保的选择,从而降低了另一个进入门槛:大型语言模型运行时的成本。
AI 代理相互协作以获得更好的结果
如果你使用过 ChatGPT,你会知道它的第一次回应有时并不是最好的,你必须与 GPT 进行对话,以调整并获得一个可接受的答案。事实上,在 2023 年夏天,人们开始让大型语言模型互相对话并进行批评,经过几轮迭代后的结果质量令人惊讶地高[9]。
这一激动人心的进展催生了多个开源软件项目,其中一些已经获得了广泛关注。我想给你展示一下这些多代理框架(在某些情况下,是完整应用程序)能够实现的功能:
ChatDev [10]。
来自他们的网站:
-
易于使用的基于 LLM 的框架,用于[..]集体智能。
-
ChatDev 是一个虚拟的软件公司,运作方式是通过包括 CEO、COO、程序员、审阅员、测试员、艺术设计师等在内的多种智能代理。
我的观点:这款软件很有前景,但有些过于复杂。也许适合模拟,但它的限制过于严格,缺乏灵活性,难以与组织现有的工作流程和过程集成。

来源:ChatDev [10]
MetaGPT [11]。
来自他们的网站
- “多代理框架:给定一行需求,返回 PRD、设计、任务、代码仓库”
我的观点:目前尚无可用信息,我还没有尝试过。

来源:MetaGPT [11]
Autogen(微软)[12]
- AutoGen 是一个框架,允许使用多个代理来开发 LLM 应用程序,这些代理能够相互对话解决任务。
我的看法:在我的早期实验中,这个框架相当灵活。Autogen 还在它们的库中引入了一些相当有趣的代理功能,如RAG,甚至还有一个可以教学的代理,它将你的聊天记录写入持久存储,并能够在未来查询这些记录,从而永远记住它们。此外,你可以同时使用不同角色的 LLM(包括封闭和开放的 LLM)!然而,我还没有找到如何明确和程序化地将工作委派给每个 AI 代理,除非在初始要求提示中明确说明这一点。

来源:Autogen(由微软提供)[12]。
更重要的是,新的多代理框架仍在不断涌现。一个新兴的框架,crew ai[13]也引起了我的关注。它有最简洁、最直观的 API,并且使任务明确委派给特定代理成为可能,这是目前 Autogen 所缺乏的功能。然而,它仍然相当简单,缺乏许多附加功能。
专业建议:如果你想始终站在这一领域创新的前沿,我鼓励你加入这些项目的 Discord 页面。你会看到人们迅速为基础框架添加新的创意功能以及各种各样的集成。这是创新健康发展的信号。如果我是风险投资人,老实说,我会在这些 Discord 服务器里待着。
2023 年下半年 — 创新时间轴
正如我提到的,2023 年下半年对代理型 AI 领域来说是一个福音。Oliver Morris 在下面的图表中,出色地捕捉了在代理、代理团队和 LLM 领域发生的具体创新。

参考资料:[14] Oliver Morris:AI 能否与自己合作造福我们?
这一演变被 Oliver 精彩地呈现出来,以至于没有必要对上述内容进行总结。我在这里的目标只是突出他的工作[14],因为它与这一演变息息相关,可能还会帮助你看清创新的未来方向。
AI 代理在软件开发中的协作发现
独立的公共[14]和学术[15]研究,通过我自己的实验验证(我将在未来的帖子中写更多关于这方面的内容),表明在提示中给定有针对性且深思熟虑的要求,且代理具备明确的角色和范围时,多代理协作会产生令人惊讶的良好结果。
我见过 AI 规划者定义详细的逐步计划,AI 批评者帮助规划者完善计划。接着,规划者请求 AI 程序员开发代码,程序员还配备了代码审查员,识别代码中的缺陷和问题,最后由执行者在受控环境中实施代码并分享结果。最终,AI 作家甚至可以去创建技术文档!此外,如果有需要,还可以选择保留“人类介入”环节。在任何一步中,都可以引入人类进行纠正,无论是修正需求、更改用户对计划的接受度、调整用户界面的美学,还是修改 API 的功能。人类开始在比单纯编写软件更高层次上参与。这就是这种多代理协作方式如此灵活、强大且迅速的原因。

来源:(研究实验) [15] Waseem 等人:《软件开发中的自主代理:愿景论文》
具体来说,研究人员已经识别出了以下好处。
-
提升生产力
-
改善代码质量
-
可扩展性与适应性
-
精简调试过程
-
降低人为错误
虽然其中一些好处显而易见,比如减少人为错误和提高生产力,但其他发现也相当有说服力。正如 Nvidia 首席执行官在 2017 年先见之明地所说:
软件正在吞噬这个世界,但 AI 将吞噬软件
退一步来看
引用 Salesforce AI 研究团队[6]:
“我们很快就会到达这样的一个阶段,项目将需要像对话式 AI 编程这样的技术,才能创建未来的超级复杂软件系统——不仅是规模巨大,而且在时间上也是人类程序员团队无法单独完成的。”
作为一名软件工程领导者,我可以清楚地看到,基于 AI 的软件工程将在每家科技公司中占有一席之地。我相信我们将在 2024 年看到这一趋势的初现,伴随着 AI 本土化公司崛起。
这两个问题浮现出来:
-
这一变化将如何在商业中表现出来,且
-
这对现有公司中的 IT 组织意味着什么
我将在接下来的部分尝试解答这些问题。
商业模式与 IT 组织:为冲击做好准备!
许多读者可能已经听说过克莱顿·克里斯滕森的经典著作《创新者的窘境》。我想在这里引用他书中的一段话,以说明 AI 软件工程(AI-SWE)将如何站稳脚跟。
克莱顿·克里斯滕森区分了两种类型的创新:持续性创新和颠覆性创新。持续性创新涉及对现有技术的增强,新市场的进入者通常难以与那些能够轻松将这些改进融入现有产品的老牌公司竞争。另一方面,颠覆性创新出现在产品对市场来说过于复杂时,导致市场出现“过度服务”现象。这为新创新提供了机会,尽管它们可能不如当前最先进的产品,但它们以较低的成本或以现有领先产品无法实现的方式提供功能。
好的,现在你已经对这个概念有了复习,保持这个思路!我们很快会再回到它,但首先让我们介绍一种新兴的组织形式——AI 原生公司。
警惕 AI 原生公司的崛起
就像我们看到云原生公司随着云计算的普及和经济性而出现并改变了市场格局一样,我们现在也将看到 AI 原生公司形成。
随着生成性 AI 使软件工程变得越来越易于接触,新公司将能够在不雇佣大量昂贵的软件开发人员的情况下创建软件。事实上,现在出现了一种新职业,称为 AI 工程师[16]。这个职业将高于实际的代码开发,专注于训练模型并创建连接, 将由这些 AI 功能创建的软件交到最终用户手中。

来源:[16] Swyx: The Rise of the AI engineer
AI 原生公司将不必处理遗留软件流程或过去做出的架构、基础设施、平台、框架或语言决策所带来的官僚主义和摩擦。他们能够利用最佳、最普及且最经济的模式构建和部署软件。而且,如果他们做出了错误的技术决策,也不必担心,只需放弃所有,重新从头开始快速构建!
AI 原生公司不会像硅谷常见的那样在隐身模式下耗时 9 到 18 个月。新技术开发的所有阶段,从原型到最小可行产品(MVP),将以周为单位进行衡量。风险投资公司将更容易且更便宜地资助这些公司,因为它们能够轻松地进行转型,当有前景的产品最终证明是失败时。这些公司将开始渗透到各个行业,从税务到医疗保健管理,或者开始威胁现有公司,或者被收购(或者由于员工数量极少而被“并购聘用”)。更重要的是,它们将迅速改变客户眼中产品的上市时间感知,几乎迫使现有成功公司自我颠覆。
回到克莱顿关于颠覆性技术的概念……人工智能软件工程(AI-SWE)一开始将逊色于人类软件工程,但没有什么能够与它的惊人速度和低成本竞争。客户将进行成本效益评估,选择绕过花哨的功能,选择那些更简单但价格便宜、能迅速发展的替代品。所有 AI 原生公司必须确保的,是在可靠性和安全性等基本原则上不妥协,并提供客户所需要的核心价值。
这如何影响 IT?
目前,在现有的成功公司中,IT(无论是集中式还是分布式)构建或购买大部分用于“进入市场”的技术。这包括从销售技术,如 CRM 系统和电子商务,到营销技术,如数字在线存在和活动技术,再到客户支持技术,如票务系统,甚至数据工程、分析和科学,如数据管道、仪表板或购买倾向模型。
备注:这里,当我提到 IT 时,我指的是公司中的所有技术组织。它们可能集中在一个正式的 IT 职能中,也可能分散在独立的业务部门中。这股 AI 浪潮不会偏袒任何一方,它将影响所有这些组织。
公司只能以这些技术发展的速度来进化。由 AI 原生公司带来的市场时间缩短压力最终将被现有公司内部的 IT 团队感受到。目前,大多数 IT 公司正在鼓励他们的工程师安全地尝试人工智能,这是好的。然而,我相信那些主动为这股浪潮做好准备的公司,将使他们的公司在脱离竞争对手的同时,保持在这个由 AI 驱动的新经济中的韧性。
每个 IT 组织的旅程将是独特的

来源:DALL-E,和作者的想象力
我希望你能把森林中间的瀑布想象成由人工智能创造的价值。所有 AI 原生公司将会驻扎得离这个瀑布更近,它们的应用架构、开发流程和团队将专门为 AI-SWE 进行优化。
然而,所有现有的公司都位于更远的地方。它们每一个都从不同的起点开始。有些使用的语言和平台将无法从人工智能中受益。其他公司则拥有脆弱且复杂的架构,可能是多年来不自然增长(收购)所建立的,这些架构不利于人工智能代理轻松地自主接入并发挥作用,等等。
就软件开发生命周期(SDLC)而言,所有这些现有公司都会对工作管理系统进行独特的定制,如何进行低级功能分解的流程独特,自己的源代码分支策略,自己的软件检查标准(代码气味百分比、单元测试覆盖率),甚至到每个开发者如何结构化每个 GitHub 拉取请求的评论。
在以 AI 驱动的世界中,上述每个方面都需要发生变化。AI 代理需要学习哪些内容必须包含在拉取请求中,哪些文档必须在工作管理系统中更新,如何构建和与测试环境互动,如何创建变更批准文档等等。而这一切对于每个 IT 公司来说都不尽相同。 简单来说,
成为以 AI 驱动的动力必须来自 IT 内部,因为没有外部实体能比 IT 自己更了解 IT。
我们将看到许多江湖骗子:那些向 IT 高管推销这一“应许之地”的公司。即使他们有一个引人注目的平台,60%到 70%的工作也将是平台的采用,以及将其集成到现有的流程、工具和文化中。
对于 IT 高管来说,这可能看起来令人望而生畏。然而,让我们始终记住,我们曾经走过这条路。回想一下,当我们都从数据中心迁移到公共云时,或者早些时候,当我们为软件团队建立持续集成时,或者当我们从零散的工作方式转向标准化的 PaaS 平台时,作为高管的我们已经成功管理过这些公司中的重大变革。
我们要做的就是定义一个高层次的结构,并规划如何实现这一目标。从我职业生涯中众多的变革案例中汲取经验,我对这波人工智能浪潮进行了很多思考,并将尝试解释一种可能的实施方式。请注意,这只是我的个人视角,我知道还会有其他的做法。
我分享这种方法的目标是让你的思维引擎开始运转,并为你在这段旅程中提供一个良好的开端。
通往未来的桥梁
我在作为领导者的职业生涯中学到的一件事是,谈论理想的目标状态很容易,因为它已经在书籍和演讲中广泛传播,而且我们也很容易欣赏当前状态中的问题。真正困难的工作是从当前状态到未来状态之间架起一座桥梁。这座桥梁上的每一步都必须经过规划和深思熟虑。第一步需要足够接近当前状态,以便组织能够自然地过渡。后续的步骤不能相距太远,否则员工会犹豫不前。这些步骤应该是相互衔接、逻辑递进的。

来源:DALL-E,及作者的想象力
我尝试将这些步骤按照阶段进行阐述,从大多数我们在 IT 行业中的现状开始:
当前状态
当前,一些 IT 组织正在尝试 AI,并鼓励他们的软件工程师使用和拥抱 AI 增强的代码开发。工程师们通常的做法是安装扩展到他们的交互式开发环境(IDE)中,在编写代码时,他们使用特殊的快捷键启动代码助手 Agent,后者会检查他们的代码,或者可能是一个指示该文件某个部分所需内容的注释,然后助手会自动建议几行代码,工程师可以选择接受或拒绝 [17]。
这些角色有多种选择可以获得帮助,以下只是几个例子:
-
Github co-pilot:迄今为止,最为普及的开发者 IDE 扩展,由 OpenAI 的 GPT4 LLM 提供支持。
-
许多 IDE 扩展(例如 Code GPT),其中一些允许你使用开源 LLMs 替代 GPT4。
-
科技公司提供的免费 IDE 扩展,扩展了其能力以促进更广泛的采用。示例如下:
a) 专有编程语言(例如 Salesforce 为 Apex 语言提供的 Einstein for Developers 扩展,支持实时自动补全)
b) 核心技术能力(SonarSource 提供的 SonarLint 扩展,专注于在编程时捕捉质量问题)
c) 扩展平台能力(CAST Highlight 扩展,用于审查软件结构安全、软件知识产权的收购前尽职调查等)
一个典型的 IT 产品交付团队可能今天看起来像这样:

来源:作者
一些工程师已经开始使用这些 IDE,或许一些负责 Scrum 团队的首席工程师也在尝试。在 IT 部门内,AI 辅助开发也可能在自然地被推广开来。

来源:作者
AI 辅助开发是一个极好的第一步,所有软件工程师都应当在公司安全政策的框架内,鼓励并积极推动内部利用这些工具。
然而,借用统计学中的一句话,这是必要的,但不充分。
尽管使用这些功能对于所有开发者都有普遍的提升效果,但为了真正引入我们之前提到的“颠覆性”变化,仍然需要投入一定程度的专注能量、思考和结构来实现。这将带领我们进入旅程的第一阶段:
阶段 0:评估与规划
如果你不做计划,那你就是在计划失败。
在进行团队组建等具体行动之前,需要进行一项评估工作,理解并定义将 AI 驱动的工程引入 IT 组织的整体范围和目标。一个扎实的计划将提升高层的信心,并为未来阶段的投资需求提供合适的估算。
为了制定一个现实且可行的计划,重要的是从我们将引入的 AI 软件代理的视角来审视这些流程、工具和技术。在此过程中需要记录的关键领域包括:
- 评估关键的软件开发流程
简化到核心内容,当前每个软件工程师必须参与哪些流程才能贡献代码并交付功能?此处的例子包括规划、任务估算、开发、遵守特定的分支策略、运行特定的测试套件、特定的编码、单元测试和文档要求等。
换句话说,AI 软件开发代理需要融入哪些关键的低级软件开发流程,以便能够实际应用。
这是一个重要的工作,因为在所有 IT 组织中,存在许多有助于协调和沟通的高级流程,但这些流程并不直接贡献于功能的交付。例子包括每周的领导报告、向高层领导汇报进展、跨多个领域的协调会议。通常由技术项目经理推动的任何工作都可以包括在这个范畴内。将 AI 驱动的工程融入其中不会直接影响这些高级流程(是的,AI 将以深远的方式 间接 影响这些高级流程,但那超出了本篇文档的范围)。
这项评估和文档工作将有助于确定 AI 软件工程团队可能需要编写的额外服务,以将它们接入现有的软件交付工作流。
2. 评估当前的 IT 基础设施和环境
在使用 AI 代理工程时,应用程序中存在一些与依赖管理相关的复杂问题(例如运行时版本、使用的库等)以及与相邻应用程序的基于 API 的依赖关系。我最初对自主 AI 的实验揭示了在环境中部署软件时的各种挑战(稍后我会在更技术性的文档中记录这些问题)。还涉及到应用程序认证管理的问题,超出了“本地”设置的范围。开发人员可能需要进行一些手动操作才能完全配置好,以便将软件部署到该环境中。这样的设置需要为每个 AI 代理进行复制,并显然尽可能地自动化,以便能够以最小的人工干预轻松增加 AI 代理容量。简而言之,详细的“低级别”文档非常重要,它描述了一个软件工程师从零开始在本地开发某些软件、然后部署到共享的 QA 环境中,再将代码提升到更高级别环境直到进入生产的全过程,这是识别 AI 代理面临的障碍的关键。
在许多情况下,我们需要为没有 100%自动化持续部署(CD)到生产环境的路径做好准备,尤其是在当前没有为人力驱动的软件工程(H-SWE)设置 CD 的情况下。例如,如果当前有人工参与变更和发布管理过程来将功能发布到生产中,那么即使半自动化的 AI-SWE 团队独立创建软件功能时,这一过程依然存在。但这并不会阻止 AI-SWE 团队创建并将所有必需的文档和测试输出提交给变更委员会和发布经理。(随着时间的推移,变更和发布过程肯定也可以由 AI(不是编程,而是推理)代理来支持,但这超出了本文的范围。)
类似于软件流程评估,环境评估也将展示需要优化哪些其他能力,以使 AI-SWE 能够在代码晋升到更高环境并最终部署到生产时保持尽可能的自动化。
3. 确定所有可以集成 AI 的应用程序(复杂性和影响)
会有数百个候选应用程序可以插入 AI-SWE。然而,需要仔细评估每个应用程序的 SDLC 相关流程的复杂性、每个应用程序语言的 AI 编码 LLMs 的成熟度以及 AI-SWE 对该应用程序的影响程度。就像在故事点估算中一样,可以使用斐波那契评分来衡量成本和收益维度,并可以创建一个整体的 ROI 指标,帮助高管们决定 AI-SWE 代理部署的顺序(更多内容见下文的第二阶段)。
重要提示: 在应用程序优先级列表的底部,应该列出所有那些已经存在显著上下文丢失的应用程序[18]。问题陈述必须忠实于构建 AI-SWE 团队,而不是为了包含那些神秘地能工作但没有人真正理解如何工作的遗留应用程序而进行稀释。
4. 制定过渡的时间表和路线图
当然,任何规划阶段都离不开一份发布的路线图,在这里也需要一份。作为一名高管,我喜欢定义时间表,因为它迫使我和我的团队以实际的日期和截止日期为思考框架,同时帮助我们全面地考虑项目的各个方面和阶段。当然,正如孙子所说,“没有计划能在与敌人接触后存活下来”。这些时间表确实会发生变化,尤其是因为这种类型的工作以前从未做过。我们真的不知道这段旅程中会有哪些惊喜等着我们,而且目前还没有用户手册可以参考。(事实上,据我所知,这篇文章可能是来自一位有经验的产品负责人在战斗中首次尝试的框架。)
除此之外,计划仍将提供一个清晰的基线并识别所有假设。随着意外情况的出现,计划可以进行审查和调整。然而,这些截止日期仍然存在,且会公开传达,始终给团队带来健康的压力,保持专注并交付成果。对于所有新项目或功能,我总是说:
“日期不能改变,但范围可以”
5. AI 技术选择
应该留出一些时间进行基于 POC 的技术选择。最好能对支持哪个方向(即调整哪些基础模型)有一些高层次的明确认识。这些模型应该能够服务于来自上面步骤 3 中的优先级列表前四分之一的大部分应用。然而,我们必须保持灵活性。这个领域发展迅速,今年晚些时候可能会出现需要我们改变选择的创新。这里重要的是要专注于那些不会后悔的选择,通过提出以下问题来指导:
-
AI 技术(如多智能体框架等)是否允许我们将架构转化,处理编码、调试、QA 和运营任务?
-
这些框架是否允许我们轻松更换 LLM?
-
我们是否可以定义一些常见的基准,以便评估框架的性能?
这一选择必须基于多个概念验证(POC),展示 AI-SWE 如何与环境和软件工具集成。外面有很多有趣的 AI-SWE 应用,比如小吃游戏、数到 100 和用多种语言打印“Hello World”。这里的 POC 应集中于简单但切实可行的 IT 应用案例。正是这些 POC 能给我们一些信心,并展示从当前状态到期望目标状态之间的实际差距,我们可以据此进行合理的工作量估算。
为了进行这项评估和规划,我建议组成一个小团队,包括:
-
一位有能力的软件工程领导者,了解公司内软件创作的具体流程,并对生成式 AI 的交集领域有深入的知识和热情。他们将是这一转型的核心推动者,并为组织推动这一变革。
-
一位有将架构投入生产经验的架构师,能够与工程团队紧密合作。此人需要了解 IT 内部的开发过程,同时对 IT 企业架构组定义的核心架构原则有深入理解,确保这些原则在解决方案中得到体现。
-
在初始目标领域内担任领导工程师,拥有从需求到规模化,从环境到代码评审的完整软件生命周期的深入实践经验。我怀疑该负责人能直觉地发现那些低挂的果实,识别 AI 智能体在团队中能产生最具生产力贡献的地方。
阶段 1:建立 AI 共享服务团队

来源:作者
尽管 AI 辅助的人类软件开发的自下而上的有机使用必须继续,但一个专门的团队将更加专注于将 AI-SWE(AI 软件工程)变为现实。
具体而言,这个团队将专注于四个不同的领域。我根据每个领域所需的独特技能和认知复杂性将其进行了拆分。
世界上没有完美的组织设计。它们往往在优化某些收益的同时牺牲其他方面。我提出的组织设计优化了并行性和规模,并尽可能为各个团队提供自主权,关键操作重点是“快速行动”。这些团队包括:
1. AI 开发团队
主要角色: 开发、定制和维护 AI 模型,特别是 LLM(大规模语言模型),用于各种软件开发任务。该开发将从预训练的基础模型开始,并对其进行微调,以适应应用团队的内部软件和文档。
职责:
-
AI 模型定制: 专注于微调预训练的 LLM,使其尽可能理解领域团队的特定代码库、软件架构文档和领域知识。这将依赖于数据管理团队精心策划的高质量数据(见下文第 2 条)。在某些情况下,模型将微调的应用代码已经使用基础模型能够理解的编程语言(如微软的 Phi-2)。然而,对于一些尚未训练到这些模型中的语言(如主要由基础设施自动化团队使用的 Terraform),可能需要进行全新的研究。AI 模型质量保证(QA): 测试 AI 模型以确保它们满足功能要求和性能基准。这些 AI 模型必须符合严格的评估标准和客观性能指标。一个好的外部测试是 AI-SWE 代理发送给首席工程师的拉取请求(pull requests)。但是,根据首席工程师的反馈,性能指标需要被规范化,并融入到模型创建流程中。该团队还应探索学术界新开发的方法,如 BotChat [9],以评估多代理 AI-SWE 团队产生的质量。
-
研究与开发: 紧跟最新的 AI 进展,并将其融入到你的系统中。
所需技能: 了解软件团队的特定技术栈、软件开发和系统集成。
我 不 认为这个团队真正需要具备从业者级别的数据科学知识,因为像 AutoGPT [19]这样的许多开源能力已经创建出来,帮助微调基础模型。这个团队不会做大量的数据科学或机器学习工作,比如在 GCP 的 TPU 芯片上预训练模型,但它将是一个真正的工程团队,拿着积木块搭建 AI-软件工程能力,在开源基础模型之上构建。实际上,我们在这里需要的技能是扎实的软件工程师。然而,这个团队需要对预训练编码模型[20]的创建有一个大致的了解,并且需要时刻跟进多代理软件开发领域的最新进展,这也是为什么上面提到了研发的职责。
在更高级的阶段,这个团队可能需要预训练自己的模型,这可能需要数据科学家以及对云计算的投资,但在验证实际价值之前在这个领域进行如此重的投资并不符合商业逻辑,因为通过基于预训练模型构建,能够获得大量提升。
2. 数据管理团队
主要角色: 管理训练和优化 AI 模型所需的数据。这个团队需要了解为团队 1 训练模型所需的数据类型、质量和格式,然后能够收集和标注这些数据。在初期,这个团队和 AI 开发团队可能是同一个,但最终为了扩展性考虑,它需要作为一个独立团队存在。实际上,刚开始时,创建样本数据集来微调模型的工作很可能由 AI 开发团队(1)完成,并且只有在多个应用程序和多个领域扩展时,才会将这项工作交给这个团队。我发现 Autogen 发布的一个原型[21]非常适用,通过将 AI 代理指向良好的代码文档(也称为检索增强生成,或 RAG),他们演示了 AI 代理如何能够学习库中一些新方法的语法并基于此提供代码。我们可能会选择使用 RAG 方法,也可能不会,但这个例子清楚地展示了这可以实现。
职责:
-
数据收集与标注: 收集并标注数据,如代码仓库、项目文档和用户故事,用于 AI 训练。了解格式要求和标注需求对于其他微调过的编码模型至关重要。在自然语言模型的微调阶段,需要创建大量的问答(键:值)对类型的精心策划数据[2]。这个团队需要将其在 AI-SWE 领域的应用进行翻译。
-
数据质量保证: 确保用于训练 AI 模型的数据的准确性、一致性和相关性。这是模型成功和质量的关键领域。回想一下,新的小型语言模型的效果直接来自于高质量教科书级别的数据用于训练这些模型。这意味着该团队将投入大量精力来确保数据相关且符合 AI 代理将要应用的领域的上下文。这可能需要审查和修改现有的应用文档(也许还包括代码),并与现有团队合作,以便在训练前改善他们的文档。尽管 AI 代理需要学习应用程序的代码和一般内部文档,但研究一下在学校里教授给计算机科学本科生的其他通用工程模式也是很有益的。这还需要格式化、分块并准备实际的高质量学术文本,以便对 LLM 进行微调。最后,团队还需要设计一些客观的度量标准,以衡量数据的质量。
-
数据隐私与安全: 以遵守隐私法律和安全标准的方式处理数据。该团队的任务是确保所有整理的数据都符合公司的隐私和安全标准。
所需技能: 数据管理、数据标注、理解数据隐私法律。
注:该团队的输出的一个有益副作用是提升目标应用程序代码和文档的一般质量。这可能也有助于现有团队的工作,如新工程师的入职培训,或帮助现有工程师深入理解他们可能没有构建过但却继承的应用程序。
3. AI 集成与测试团队
主要角色: 将 AI 解决方案集成到现有流程和系统中,并确保其功能性和可靠性。该团队提供 AI-SWE 代理和团队的“最后一公里”连接。如果没有所需的集成工具和库,AI 所生成的软件和文档将无法被检查、审阅或部署到测试环境中。
职责:
-
AI 工具: 开发工具将 AI 模型无缝集成到现有的软件开发流程中。实际上,这些工具很可能是 API 和辅助库,可用于与内部身份管理系统进行身份验证、与 git 交互、与环境和现有的 CI/CD 流程进行接口,甚至可能与工作管理系统(例如 Jira)进行交互,以服务团队。
你可以看到这里的模式。这些活动通常是人类程序员在编写代码之外做的事情。AI 代理将调用实用的辅助软件来执行类似的操作。根据公司在成熟度和自动化程度上的不同,这项工作实际上可能比实际的 AI 开发还需要更多的努力。
-
AI 性能监控:持续监控 AI 系统,关注任何问题或与预期性能的偏差。注意:如果有现有的 MLOps 团队,这项工作也可能归入该团队。
所需技能: 具有软件集成、QA 测试、性能监控和故障排除的经验。
4. AI 伦理与合规(虚拟)团队
主要职责: 确保 AI 系统的伦理开发与部署。最好这个团队不直接隶属于共享服务组织。拥有此类经验的个人通常已经存在于企业的首席数据办公室,作为治理团队的一部分。他们将作为咨询和信息提供方参与,以确保在创建这些 AI-SWE 系统时,他们的意见得到考虑。
职责:
-
伦理指南开发: 提供伦理 AI 使用的指导方针,重点关注公平性、透明度和责任,供共享服务团队在 AI 实施中参考。
-
合规框架: 提供一套最小可行的、可衡量的合规规则、法规和伦理标准,供共享服务团队纳入实施清单中。这是一个不断发展的领域,因此应该与数据治理团队建立持续的接口,以便捕捉行业中新出台的重要法规或关键 AI 标准。
虽然这不是该团队的责任,但它可以帮助指导集成团队(3)创建用于可审计性的扩展包,例如 SOX 等,这些扩展包能够基于 AI-SWE 团队创建的文档自动生成报告,这些报告对于内部审计人员,甚至可能是外部审计人员来说都是可接受的。
所需技能: 了解伦理 AI 原则、法律和监管合规性、利益相关者管理。选择具有“赋能”态度而非“阻碍”态度的合作伙伴非常重要。前者会与团队合作找到可行的解决方案,而后者则喜欢说“不能做!”,但不会有动力寻找前进的道路。
这些共享服务团队在成功过渡到 AI 驱动的软件开发过程中起着关键作用。他们的有效性在于他们能够协作、创新,并适应不断发展的 AI 技术格局。
第 1 阶段的退出标准
一旦这个小组的关键方面被创建并且能够正常运作,从阶段 1 到阶段 2 的退出标准就是创建一个功能齐全且富有成效的 AI-SWE 代理,基于为第一个目标应用程序/Scrum 团队定制的微调 LLM,并用高质量数据进行训练。此时,AI-SWE 已经在受控环境中具备了功能。
然而,正如 DC Palter [22] 所说:
90%完成意味着产品已接近发布的一半。
“在受控环境下工作的原型与在任何使用情况下都不会崩溃的商业产品之间存在巨大的差距。当产品完成 90%时,它实际上仅完成了一半。”
阶段 2 开始时,你将把这个 90%完成的原型(AI-SWE 代理)带入真实环境中。
阶段 2a:引入第一个 AI“Agent”(即 Teamlet)
在这个阶段,我们会小心地将第一个 AI-SWE Agent(注意 Agent 与 agent 的区别,稍后会详细说明)引入一个真实的 Scrum 团队,并随着其有效性的证明,逐渐扩大该代理的职责。我们在引入时必须小心谨慎,因为我们不希望这会干扰目标 Scrum 团队的速度,而该团队已经承诺了该发布版本下正在建设的史诗故事点。
我建议我们将 AI-SWE 代理引入由从阶段 0 开始就参与此过程的首席工程师领导的团队。首席工程师将指导 AI 代理,初步任务可能只是开始为一些简单的技术债务问题提供 Pull 请求(例如修复已知的缺陷问题,AI 代理在这方面表现越来越好[23],或清除由 SonarQube 运行中的代码异味所识别的简单技术债务问题[24]等),甚至是为代码和可用性补充缺失的文档。
在这个阶段,所有的集成和辅助库都经过了微调,目的是使 AI 代理对团队的贡献变得无摩擦、一致,并且与团队中的人类开发者相当(或更好)。

来源:作者
在幕后,我们引入的 AI 代理实际上是由多个较小的代理组成,这些代理相互互动。这一点很重要,因为多代理的输出通常比与人类的单回合互动要强得多,正如前面所述。这张可爱的 DALL-E 图片捕捉了团队的本质:

来源:DALL-E 和作者的想象
这里更合适,但远不如酷的术语是代理 Teamlet。这就是为什么我采用了带大写“A”的 AI Agent这一术语,以便与实际的原子代理区分开来。然而,在整篇文档中,我将 Teamlet 和 Agent 互换使用。
放大来看 AI 代理(即 Teamlet),我们可以看到下方有许多不同的代理,每个代理都由相同或不同的 LLM(取决于其专业化)支持。

来源:作者
在团队中的代理可能具有以下特征和责任

上述内容只是展示了完成人类工程师编码工作所需的交互和代理示例。它并不完整。例如,它缺少如何处理环境等内容。这可能需要一个完全独立的团队来做准备工作,才能让现有团队按照其指示步骤进行工作。此处的目标只是为了说明“代理”(Agent,带有大写字母“A”)的一般概念。
本阶段允许负责人微调各种多个代理的提示和角色。
以下是这些代理之间对话的示例,摘自其中一个 Autogen 参考示例[21]。

来源: [25] Autogen: 多代理示例 展示了小型代理如何相互交流和批评,以产生更优的最终输出。
Phase 2a 的退出标准
当 AI-SWE 代理(团队)能够从看板上接收真实的编码用户故事并完成它们,最重要的是,拉取请求的质量已经达到了能够被主工程师一致批准并合并到主干的水平时,Phase 2a 可以被认为是成功的。
我们现在已经准备好将这些代理引入到其他领域。
Phase 2b:将 AI 软件代理引入其他领域
本阶段更多的是对 Phase 2a 的重复和完善。不同之处在于将其他主工程师和架构师引入这种工作模式,并训练适合该领域的定制 LLM。例如,为客户支持技术团队创建的定制 LLM 编码器将需要针对不同的软件仓库、应用文档进行微调,甚至可能需要一组修改过的辅助功能(如果他们的软件工作流不同),而不是为例如财务技术团队创建的定制 LLM。

来源:作者
到此时,我们应该开始将多个 AI 代理引入到一个 Scrum 团队中,例如上图中平台基础设施领域的团队 1。这不仅有助于并行化(即代理同时在同一个迭代中从看板上处理多个用户故事),还可以提供不同职责的分离,例如一个代理完全负责环境创建和测试工具设置,而另一个则负责编写和部署代码到该环境中。
有趣的插曲:我的兄弟是《星际迷航》的忠实粉丝,他将这个概念与比纳尔种族[26]进行了对比。比纳尔种族以配对形式工作,彼此的思维相互连接,使他们能够快速高效地沟通。吉恩·罗登伯里一定会为 AI 代理正逐步实现他所设想的愿景而感到兴奋!
Phase 2b 的退出标准
当每个适用领域至少有一个 AI 代理上线并成功从看板上提取用户故事,开始处理并更新工作管理系统时,第 2b 阶段可以视为完成。最重要的是,他们的拉取请求质量足够高,以至于始终获得首席工程师的批准并合并到主分支。
我们现在已经准备好进入第 3 阶段。
第 3a 阶段:将半自动化的 AI 团队引入一个领域
这是一个团队完全由 AI 软件工程代理组成的阶段。
从第 2 阶段到第 3 阶段的根本变化是,在这个阶段,团队开始处理完整的史诗任务(Epics),每个代理负责提取用户故事,并相互协调以完成工作。
另外,AI 完全开发软件的这一范式转变可能还会影响我们在软件中划分工作的方式(项目 -> 史诗 -> 用户故事 -> 任务)。请记住,当前的工作划分方式是为了适应以人为主导的软件开发。AI 驱动的开发的工作分解方式超出了本文的讨论范围,并将在未来几年内自然演变。为了便于理解(并且简化!),我们将在此继续使用传统的敏捷术语,如史诗和用户故事。

来源:作者
在此阶段,需要在各个代理(团队)之间建立更高层次的沟通,这可能意味着一个复合代理(团队)中的某个原子代理与另一个复合代理中的代理进行连接和交流。也可能意味着这些代理通过工作管理系统和问题看板(如 Github 问题)进行协作。这些细节并不重要。更重要的是,每个代理对于某些任务将拥有完全的自主权。这些角色可能包括:
-
软件开发代理(在第 2a 阶段详细介绍)
-
产品管理代理,负责编写和完善产品需求文档,并在人工产品经理的监督下规划产品路线图
-
负责面向客户文档、内部软件文档、全员大会展示幻灯片制作、IT 投资组合更新的技术写作代理
-
研究助理支持架构师和产品经理,协助进行关于业务能力、SWOT 分析等方面的外部研究。
-
操作支持代理,通过系统警报直接激活,或在触发时手动激活,并指向需要排查的故障
本质上,这些团队成为一个自给自足的单元,负责整个产品的端到端责任。需要注意的是,AI 代理角色的设计与人类的设计不同[27],我们会留下一些灵活性,以便在通过初步尝试获得实证反馈后确定最优化的角色。
最近的研究[28]表明,编程、沟通和规划是 AI 软件工程师(AI-SWE)在纯软件开发领域逐渐变得更为擅长的领域。然而,要让人工智能在本阶段完成一些预期活动的自主执行,需借助生成性 AI 进行更高级的思维,称为系统 2 思维或慢思维[29]。截至本文写作时,我们尚未达到这个阶段,这意味着在操作故障排除或产品管理活动中,仍然需要人工协助和干预。
然而,预计在 2024 年这一领域会有突破,可能会在下半年实现[2]。尽管如此,大部分完整史诗的开发仍然应该可以通过现有的能力和大语言模型(LLM)定制来实现。

来源:[2] Andrej Karpathy [1 小时讲座] 大型语言模型简介
需要注意的一点是,人工智能在处理功能性需求方面表现出色,但在非功能性需求(NFRs)方面的能力(目前)还不够强大,这些非功能性需求包括性能、信任和规模[30]。为了确保组织的这些非功能性需求的基准确实按照规格开发,仍然需要人工干预。这可能仅仅意味着需要人工进行端到端的测试(当然,这些测试也通过 AI 代理开发并运行,但需要人工签字确认),也可能意味着需要人工驱动的软件增强来添加预期的非功能性需求能力。
阶段 3a 的退出标准
这一阶段需要一定的复杂度,距离完全确定退出标准还有些距离,但通常而言,成功的一个良好标尺是从需求到功能性解决方案的完整史诗能够在质量保证(QA)中运行。可能仍然需要人工验证非功能性需求(NFR)是否按照规格开发。
阶段 3b:将 AI 团队扩展到其他领域

来源:作者
在从一个领域的 AI 团队中汲取经验教训的基础上,本阶段将自主 AI 团队引入其他领域。这也开始将一些高级工程师提升到团队领导者的层级,以便扩大团队规模。
这里没有展示的一个方面,但值得一提的是,需要一个端到端的质量保证团队(应该是 AI 和人类工程师的混合团队)。端到端的质量保证非常重要,因为这些 AI 团队可能是根据不同史诗的用户验收标准进行开发,但当所有这些在发布前汇集时,整体体验仍然需要进行验证和集成测试。这也包括性能测试的需求。最终,当相关能力得到足够发展的时候,QA 职能也可以被训练成大型语言模型(LLM),但我预见到这一点可能会发生在软件开发职能之后,主要是因为其相互依赖性的复杂性。尽管如此,这并不排除 QA 职能在早期阶段大量利用 AI 代理来评估、编写和部署自动化测试以及自动化报告。
第三阶段退出标准
这一阶段的退出标准出乎意料的简单:一个关键规模(由高层领导决定的 80-100%)的功能性 AI 团队已经被部署并开始在每个领域做出贡献,且开始能够观察到变更的提前时间有了明显的差异。
稳态(阶段 n):扩展 AI 团队,去中心化 AI 共享服务

来源:作者
扩展 AI 团队
到这时,各个领域已经开始适应 AI 团队的工作质量,越来越多的实操软件工程师开始被提升,并且成为自己 AI 团队的领导,或者AI 工程师[16]。虽然我展示的每个 AI 团队只映射一个人类工程师,但没有理由不能由同一个人类工程师管理多个团队。速度的瓶颈自然会转移到这些人类 AI 工程师身上,但到那时,公司的变更提前时间应该已经大幅减少。这些 AI 工程师需要专注的技能仍然会有一些软件方面的内容,因为他们仍然需要审核和批准各种 AI 代理发送的拉取请求,审核他们创建的文档等等。然而,所有人类工程师都需要专注于第“优秀工程师无所畏惧”章节中提到的独特人类技能。
请注意,总是需要少数由人类驱动的软件团队。这些团队会是更高层次的职能,他们的工作将是为领域提供安全网。如前所述,这一职能的一个好例子是确保非功能性需求(NFR)按规格交付,但也会有其他 AI 的不足之处浮现出来。重要的是要记录这些问题,并与领域的 AI 开发团队分享,以便未来版本的领域自定义 LLM 能够更为优越。
去中心化 AI 共享服务:
当技术和产品开始成熟时,我的经验是共享服务团队往往会成为速度的瓶颈。因此,在稳定状态下,所有组织必须计划将任何 AI 共享服务团队分散到各个领域。这意味着每个领域都要建立本地的能力,定制自己的 AI 大语言模型(LLM),策划训练数据,并维护适合自身的定制集成。
共享服务团队开始转变为卓越中心(CoE)组织,负责那些始终需要横向支持且如果保持标准化会更好的服务。这包括一些常见的集成。例如,所有领域将继续需要其 AI 代理能够与 IT CI/CD 管道团队集成,或与 Github、sonarQube 以及像 Jira、Asana、BaseCamp 等工作管理系统进行交互。
由于 IT 系统始终可以进行审计,卓越中心(CoE)可以创建并维护扩展包,供各个团队插入并生成审计报告(即前共享服务团队的道德与合规团队的迭代)。
有研究表明,开发人员在编写代码时,如果得到 AI 的帮助,往往会写出不太安全的代码,而且更令人担忧的是,他们对代码的安全性过于乐观[31]。这一问题可能会在完全由 AI 驱动的团队中变得更加严重。这就是为什么保持中央代码质量和库选择检查非常重要的原因。这可能需要卓越中心(CoE)创建自己的哨兵软件,能够跟踪跨领域的安全代码和基础设施,同时自动跟踪和添加这些团队待办事项,并在中央报告整体安全状况。如果公司已经有一个强大且人员充足的中央安全组织,拥有自己的软件工程师并且深度参与 IT 工作,那么就没有理由不能由该组织来执行这一职能。
应用架构如果不是可组合的、事件驱动的,也可能成为速度的瓶颈。这个问题需要注意,如果 Phase 0 评估完成得公正的话,这个问题应该已经在每个应用的评分和优先级设置中被发现。
反驳论点
这里有个“小插曲”。在我写这篇文章时,我请 chatGPT 持反对立场,解释一下如果减少对人类驱动的软件工程的依赖会带来哪些问题。下面是我简化的反馈,实际上帮助我完善了我的提议阶段,例如将人类工程师的水平提升到一个更高但更加关键的层级,并且让一个由每个现有工程师领导的自治团队来运行。
以下是 chatGPT 的反驳论点,以及我对每个论点的逐一回应:
下面是一些需要考虑的反驳观点:
1. 复杂问题解决与创造力:
-
人类开发者在创造性解决问题和应对新情况方面表现出色。AI 虽然擅长处理日常任务,但在处理需要创新思维的独特或复杂问题时可能会遇到困难。
-
设计和创新中的创造力是人类特有的特质,AI 无法完全复制,特别是在需要情感智能和理解人类经验的领域。
我的回应:有道理。这让我思考如何提升现有工程师的水平,并将他们安排在能激发更多创造力和创新思维的组织中。这通常发生在你更接近客户的时候,这也是担任技术负责人角色时的情况(见稳定状态:第 n 阶段)。
2. 代码质量和维护
-
AI 生成的代码可能缺乏经验丰富的人工开发者所带来的细致理解,可能导致可维护性、可扩展性和性能方面的问题。
-
人类监督对于确保代码质量至关重要,尤其是在复杂系统中,理解更广泛的背景非常重要。
我的回应:我也在考虑这个问题,因为我引用的各篇论文都表明了 AI 软件工程在非功能性需求方面的弱点。这也是为什么即使在稳定状态下,我还是选择保留一些实际的人类工程团队,以确保软件的信任性、操作韧性和性能等方面得以考虑。
3. 伦理和社会影响:
-
用 AI 替代人类工作者引发了重大的伦理问题,包括对就业的影响以及减少人类劳动力的广泛社会影响。
-
有失去多年来积累的宝贵人类技能和知识的风险,这些是 AI 难以轻易接管的。
我的回应:我已经考虑到这一点。尽管我预见到 IT 组织的增长速度可能会放缓,但现有工程师将被重新安排从事更高阶的工作,这将确保有意义的就业机会,并让他们能够更好地接触到高层领导和业务层面。当然,这意味着现有工程师必须提升那些传统上属于产品管理的技能,而产品经理则需要适应领导 AI 团队和框架。
4. 缺乏适应性和直觉:
-
即使是先进的 AI 系统,也可能无法很好地适应快速变化的环境或需求,这在软件开发中是常见的情况。
-
人类能够直观地应对模糊或定义不清的问题,这是 AI 目前所缺乏的能力。
我的回应:没错,这也是为什么每个 AI 软件工程团队都有一名人类工程师来引导的原因。AI 软件工程仍然需要明确的低层次指令和调整,因为应用程序另一端的接口仍然是人类,只有另一个人类才能与之产生共鸣。
5. 安全性、可靠性、监管和合规问题:
-
AI 系统可能容易受到特定类型的故障、偏见和安全问题的影响,这些问题与人类团队面临的不同。
-
在关键开发任务中依赖 AI 可能会在 AI 行为不可预测或发生错误时带来风险。
-
AI 系统可能无法完全处理软件开发中的法律和合规问题,而这些问题通常需要人类的判断和对法律的理解。
-
在某些行业或地区,可能会存在关于在关键开发岗位使用 AI 的监管挑战或限制。
我的回应:好的观点。这让我在工程团队下新增了一个关于安全性和合规性的责任,即使在稳定状态下也是如此。在阅读关于某些行业(或地区)面临的监管挑战后,我确实在文章中加上了一个警告,说明 AI-SWE 模型可能不适用于这些行业,并且在开发中使用 AI 可能需要更加严格的控制方式。
6. 沟通与协作:
-
有效的软件开发通常需要与利益相关者进行细致的沟通与协作,这是人类团队天生更擅长的领域。
-
理解客户需求、体察终端用户的问题以及协商需求是需要人类互动的关键领域。
我的回应:同意,这就是为什么 AI 工程师和产品经理都会参与,与人类利益相关者处理沟通方面的事务。
7. 过渡和维护成本:
-
开发、训练和集成 AI 系统以用于软件开发的初始投资可能是相当大的。
-
为了保持 AI 系统的有效性,持续的维护、更新和训练也可能带来显著的成本。
我的回应:我能理解 GPT4 可能认为我会在内部做预训练部分,这是一个错误的假设。我依赖的是使用现有的预训练基础模型,针对内部 IT 软件代码库进行微调。
9. 理解范围有限:
-
即使 AI 具有先进的能力,它可能也无法像人类一样完全理解商业背景或市场和用户需求的细微差别。
-
AI 解决方案可能过于局限,错过了人类开发人员通常考虑的大局。
我的回应:同意,这就是为什么 AI 工程师仍然在最终阶段领导 AI-SWE 团队。
10. 过度依赖的风险:
- 过度依赖 AI 可能导致组织内部人类专业知识的下降,使其在面对 AI 故障或局限时更加脆弱。
我的回应:这是正确的,我同意这个观点。然而,这是整个软件行业将面临的一个更普遍的风险。目前我不确定我们如何避免这种情况。简而言之,我认为没有人能回答这个问题,但我猜我们将在接下来的几年中一起找到答案 😃
优秀的工程师没有什么可担心的
首先,让我们正面解决恐惧的话题。从关于软件工程师对 AI 感情的社交媒体研究[30]中可以清楚看出,恐惧是开发人员中最主导的情绪。认识到这一点非常重要。

来源:[30] Feng 等人,《调查 Chat-GPT 在众包社交数据中的代码生成性能》
与此同时,让我们退后一步,看看优秀软件工程师所具备的完整技能集[32]

来源:[32] Li 等人,《什么区分优秀的软件工程师》,作者标注以突出能够抵御 AI 变革的技能。
在关于优秀软件工程师特质的多项研究总结中,可以看到,编程能力只是这些优秀工程师特质的一个子集。如果你看上面的图表,我用灰色箭头标出了所有 AI 代理无论多么先进都无法模仿的领域。依我看,这些是所有软件工程师应该有意识地磨炼和完善的不可模仿的技能,它们将是区分他们并使他们在新的 AI-SWE 变革中保持价值的关键。实际上,这些正是我在上面所描述的 AI 驱动 IT 企业的稳定阶段中将被突出和需求的技能。
结论 — 风暴即将来临,你准备好了吗?

来源:[33] Ben Evans:《AI 与其他一切》,Slush 大会,赫尔辛基 2023 年 12 月
我在科技行业已经工作了超过 20 年,见证了许多技术泡沫的兴起与破裂、基础设施的变革(从本地部署到云计算)以及流程的变化(从瀑布式到敏捷)。我可以说,我们现在所见到的,必定是软件行业有史以来最大的海啸。但不要仅仅听我说,看看比尔·盖茨怎么说[34]。进入软件技术开发的门槛将被抹去,从创意到市场的周期将缩短,以前连同一个层次都无法比较的公司将开始竞争,而新兴灵活的公司将崛起,威胁到伟大的传统企业。我们当中许多人在这些现有的大公司工作,有些人甚至身居高位,可以影响公司的方向。或许其中一些成功的公司,甚至在危机临近时都未曾意识到,直到为时已晚。我想,一些公司会感知到变化的到来,但它们更愿意待在现有状态的舒适区中。
而这正是机会所在。少数几家公司将积极采取行动,将自身定位为脱颖而出的领头羊,并且当变化开始加速时,它们将处于主导地位。这些公司将是伟大的公司。它们将超越所有其他公司,当尘埃落定、烟雾散去时,它们将比以往任何时候都更强大。
这些内容包含了我的个人观点,并不代表我的雇主的官方立场。
参考文献
-
Hailin Chen Caiming Xiong 等人. ChatGPT 一周年:开源 LLM 是否赶上来了?
-
Andrej Karpathy [1 小时讲座] 大型语言模型介绍(标注在“创建 LLM 的阶段”)
-
Microsoft — Phi2: 小型语言模型的惊人力量(高效能编码器基础模型)
-
Eric Hartford 认识萨曼莎 — 向具备意识的人工智能迈出的第一步
-
Meditron](
ollama.ai/library/meditron) — 从 Llama 2 适配到医学领域的开源医学大型语言模型。 -
Erik Nijkamp, Donald Rose — 与 CodeGen 一起进行对话式 AI 编程:让 AI 为你写代码
-
Eldan 等人 — TinyStories: 语言模型可以小到什么程度,仍然能够流利地讲英语?
-
Gunasekar 等人 — 教科书就是你所需要的一切
-
Duan 等人 — BotChat: 评估 LLM 进行多轮对话的能力
-
ChatDev: 易用的基于 LLM 的集体智能框架。
-
MetaGPT: 给定一行需求,返回 PRD、设计、任务、代码库
-
Autogen (由微软提供): 启用下一代大型语言模型应用
-
Crew AI: 协调角色扮演的框架,自动化 AI 代理
-
Oliver Morris: AI 能与自身合作为我们谋取利益吗?
-
Waseem et al: 软件开发中的自主代理:愿景论文
-
Swyx: AI 工程师的崛起
-
Douvantziz: 使用 AI 自动完成编码——Copilot 如何评估?
-
Chelsea Troy — 她文章中特别提到的上下文丧失问题:减少技术债务
-
Autogpt: 将你的代理调优到完美
-
Feng et al, Codebert:一个用于编程和自然语言的预训练模型
-
Autogen: RAG 示例:教授代理使用新软件库来完成编码任务
-
DC Palter: 经验丰富的创始人知道的 7 件事,首次创始人错过的事情
-
Sobania et al. ChatGPT 自动修复 Bug 性能分析
-
Tian et al ChatGPT 是终极编程助手吗——它有多远?
-
Autogen : 多代理示例
-
Fandom : 星际迷航中的 Bynar 族
-
Oliver Morris , AI 团队能有多专业
-
H Hörnemalm, A. (2023). ChatGPT 作为软件开发工具:开发的未来
-
Kahneman, 思考,快与慢
-
Feng et al , 基于众包社交数据调查 ChatGPT 代码生成性能
-
Perry et al, 用户使用 AI 助手时会编写更多不安全的代码吗?
-
Li et al, 什么区别了优秀的软件工程师?实证软件工程
-
Ben Evans: 人工智能与其他一切,Slush 大会,赫尔辛基 2023 年 12 月
-
Bill Gates: 人工智能时代已经开始
术语表
-
AI 代理:由多个 AI 代理协作工作的 AI 代理超集
-
AI 代理:由通用或专用的大型语言模型支持的代理
-
AI Teamlet:与 AI 代理相同
-
AI-SWE:由 AI 驱动的软件工程:由 AI 代理和 AI 团队编写、调试、构建和部署的软件
-
CI/CD:持续集成与持续交付
-
CoE:卓越中心
-
CRM:客户关系管理系统
-
H-SWE:以人为驱动的软件工程:由人类编写、调试、构建和部署的软件
-
MVP:最小可行产品
-
OSS:开源软件
-
RAG:检索增强生成
-
SDLC:软件开发生命周期
设计与部署机器学习 Python 应用程序(第二部分)
你不需要是 Atlas 就能将你的模型部署到云端
·发表于 Towards Data Science ·阅读时间 16 分钟 ·2024 年 2 月 24 日
--

图像来自 Midjourney
现在我们已经有了训练好的 Detectron2 模型(请参见第一部分),让我们将其作为应用程序的一部分进行部署,以便向他人提供推理能力。
即使本系列的第一部分和第二部分使用 Detectron2 进行物体检测,无论你使用的是哪种机器学习库(Detectron、Yolo、PyTorch、Tensorflow 等),无论你的使用场景是(计算机视觉、自然语言处理、深度学习 等),这里讨论的关于模型部署的各种话题对于所有开发机器学习过程的人都将有所帮助。
尽管数据科学和计算机科学在许多方面有所重叠,但训练和部署机器学习模型将两者结合起来,因为那些致力于开发高效准确模型的人通常并不是负责部署模型的人,反之亦然。另一方面,偏向计算机科学的人可能对机器学习或相关库的了解不足,无法判断是否可以通过对机器学习过程的配置来解决应用瓶颈,还是需要通过后端和托管服务来解决。
为了帮助你部署一个利用 ML 的应用,本文将首先讨论:(1)可以帮助数据科学人员做决策的高层计算机科学设计概念,以平衡负载并缓解瓶颈;(2)低层设计,讲解如何通过使用 Python Web 框架 Django、Django Rest Framework API、分布式任务队列 Celery、Docker、Heroku 和 AWS S3 来部署 Detectron2 推理过程。
跟随本文的学习,提前具备以下知识将会有所帮助:
-
扎实的 Python 知识
-
对 Django、Django Rest Framework、Docker、Celery 和 AWS 的理解
-
熟悉 Heroku
高层设计
为了深入探讨高层设计,让我们讨论一些关键问题和潜在解决方案。
问题 1:内存
从第一部分保存的机器学习模型,命名为 model_final.pth,起始大小为大约 325MB。此外,基于(1)Python 运行时、(2)Detectron2、(3)大型依赖项如 Torch 和(4)Django Web 框架的应用程序在部署时将使用约 150MB 的内存。
因此,至少我们一开始就需要大约 475MB 的内存。
我们可以仅在机器学习(ML)过程需要运行时加载 Detectron2 模型,但这仍然意味着我们的应用最终会占用约 475MB 的内存。如果你的预算有限,并且无法垂直扩展应用程序,那么内存就会成为许多托管平台上的一个重要限制。例如,Heroku 提供了名为“dynos”的容器来运行应用程序,基础支付计划的 dynos 起始内存为 512MB,当内存超过 512MB 时,会开始写入磁盘,并且当内存使用达到 250%(1280MB)时,dyno 会崩溃并重启。
关于内存,Detectron2 推理将根据图像中检测到的物体数量引发内存使用的波动,因此确保在此过程中内存可用非常重要。
对于那些想加速推理,但又担心内存限制的人,批量推理在这里也无济于事。正如 Detectron2 仓库中的一位贡献者所指出的,使用批量推理时:
N 张图像所需的内存是 1 张图像的 N 倍……你可以改为在循环中逐张预测 N 张图像。
总的来说,这总结了问题 #1:
将长时间运行的 ML 过程作为应用的一部分,通常会非常占用内存,因为模型的大小、ML 依赖项以及推理过程都需要大量内存。
问题 2:时间
集成 ML 的部署应用程序可能需要设计成能够管理长期运行的过程。
以使用 Detectron2 的应用程序为例,模型会接收一张图像作为输入并输出推理坐标。对于一张图像,推理可能只需几秒钟,但假设我们正在处理一个包含每页一张图像的长 PDF 文档(如第一部分中的训练数据),这可能会需要一些时间。
在这个过程中,Detectron2 推理将是 CPU 或 GPU 限制的,具体取决于你的配置。请查看下面的 Python 代码块以进行更改(推理完全可以使用 CPU,但正如第一部分中提到的,训练需要 GPU/Cuda):
from detectron2.config import get_cfg
cfg = get_cfg()
cfg.MODEL.DEVICE = "cpu" #or "cuda"
此外,推理后保存图像,比如保存到 AWS S3,将引入 I/O 限制的进程。总体而言,这可能会堵塞后端,进而引发问题 #2:
单线程的 Python 应用程序在运行某个进程时,不会同时处理额外的 HTTP 请求,无论是并发请求还是其他类型的请求。
问题 3:扩展性
在考虑 Python 应用程序的水平可扩展性时,必须注意,Python(假设它是通过 CPython 编译/解释的)受限于全局解释器锁(GIL),这意味着它只允许一个线程持有 Python 解释器的控制权。因此,Python 不完全适用多线程范式,虽然应用程序可以实现多线程,使用如 Gunicorn 等 Web 服务器,但它们是并发执行的,这意味着线程并不是并行运行的。
我知道这一切听起来相当抽象,尤其是对于数据科学领域的朋友们,所以下面我会提供一个示例来说明这个问题。
你就是你的应用程序,现在你的硬件——大脑——正在处理两个请求,清理柜台和在手机上发短信。你有两只手来完成这两项任务,现在你就像一个多线程的 Python 应用程序,正在同时做这两件事。但实际上,你并不是在确切的同一时刻考虑两件事,而是先开始清理动作,然后把注意力转移到手机上看你正在输入的内容,再看回柜台,确保没有漏掉任何地方。
实际上,你正在并发地处理这些任务。
GIL 的工作原理是一样的,每次处理一个线程,但通过在它们之间切换来实现并发。这意味着多线程处理 Python 应用程序仍然对运行后台任务或 I/O 密集型任务(例如下载文件)有用,而主执行线程仍在运行。用前面的类比来说,清理台面的后台任务(即下载文件)仍在进行,而你在考虑发短信时,但你仍然需要将焦点转回到清洁的手上,以便处理下一步。
这种“焦点的变化”在同时处理多个请求时可能看起来并不是什么大问题,但当你需要同时处理数百个请求时,突然之间,这就成为了大规模应用程序的一个限制因素,因为它们需要对终端用户保持足够的响应能力。
因此,我们有问题 #3:
GIL 阻止了多线程成为 Python 应用程序的良好扩展解决方案。
解决方案
现在我们已经识别出了关键问题,让我们来讨论一些潜在的解决方案。
前面提到的问题按重要性排序,因为我们首先需要管理内存(问题 #1),确保应用程序不会崩溃,然后为应用程序留出空间以便一次处理多个请求(问题 #2),同时仍确保我们在大规模下处理多个请求的方式是有效的(问题 #3)。
那么,让我们直接开始解决问题 #1。
根据托管平台的不同,我们需要充分了解可用的配置,以便进行扩展。由于我们将使用 Heroku,欢迎查看关于 dyno 扩展的指南。在不需要垂直扩展 dyno 的情况下,我们可以通过添加另一个 进程 来进行扩展。例如,在 Basic dyno 类型中,开发者可以在同一个 dyno 上部署 web 进程和 worker 进程。这种做法有几个好处:
-
这使得多进程处理成为可能。
-
现在,dyno 资源被复制了,这意味着每个进程都有一个 512MB 的内存阈值。
-
在成本方面,每个进程每月费用为 7 美元(所以如果有一个 web 进程和一个 worker 进程,则每月为 14 美元)。比起垂直扩展 dyno 来增加内存要便宜得多,如果你想将 512MB 的内存分配扩展到 1024MB,单个 dyno 每月需要 50 美元。
回到前面关于清洁台面和发短信的类比,与其通过为身体添加额外的手臂来让自己更复杂,不如让两个人(并行多进程)来执行这些独立的任务。我们通过增加工作负载的多样性来扩展,而不是通过扩展单一的进程,从而为我们节省了开支。
好的,但是有两个独立的进程,那它们之间有什么区别呢?
使用 Django 时,我们的 web 进程将会初始化为:
python manage.py runserver
并且使用分布式任务队列(如 Celery),worker 将使用以下方式初始化:
celery -A <DJANGO_APP_NAME_HERE> worker
Heroku 的设计理念中,web 进程是我们核心 web 框架的服务器,而 worker 进程则用于处理队列库、定时任务或其他在后台执行的工作。两者都代表已部署应用的实例,因此,考虑到核心依赖和运行时,内存占用大约为 ~150MB。然而,我们可以确保只有 worker 进程运行机器学习任务,从而避免 web 进程占用 ~325MB 以上的内存。这有多个好处:
-
内存使用量,尽管对于 worker 进程仍然较高,但将分配到系统外的节点,从而确保在执行机器学习任务时遇到的任何问题可以被单独处理和监控,避免影响 web 进程。这有助于缓解问题 #1。
-
新发现的并行处理方法确保了 web 进程在执行长时间运行的机器学习任务时仍能响应请求,帮助解决问题 #2。
-
我们通过实现多进程的方式来为扩展做准备,帮助解决问题 #3。
由于我们还没有完全解决关键问题,让我们再深入探讨一下,在进入低层次的细节之前。Heroku 如是说:
同时处理传入 HTTP 请求的 web 应用比一次只处理一个请求的 web 应用更加高效地利用 dyno 资源。因此,我们建议在开发和运行生产服务时使用支持并发请求处理的 web 服务器。
Django 和 Flask web 框架内置了便捷的 web 服务器,但这些阻塞型服务器每次只能处理一个请求。如果你在 Heroku 上使用这些服务器部署,dyno 资源将得不到充分利用,应用程序也会显得不响应。
我们通过利用 worker 的多进程来处理机器学习任务,已经领先一步,但可以进一步通过使用 Gunicorn 来优化:
Gunicorn 是一个纯 Python 的 HTTP 服务器,专为 WSGI 应用设计。它允许你通过在单个 dyno 中运行多个 Python 进程并发地运行任何 Python 应用。它提供了性能、灵活性和配置简便性之间的完美平衡。
好的,很棒,现在我们可以利用更多的进程,但有一个问题:每一个新的 Gunicorn worker 进程都将代表一个应用副本,这意味着它们将使用基础 ~150MB 的内存*,此外还会使用 Heroku 进程的内存。比如说,如果我们通过 pip install gunicorn 安装并使用以下命令初始化 Heroku web 进程:
gunicorn <DJANGO_APP_NAME_HERE>.wsgi:application --workers=2 --bind=0.0.0.0:$PORT
Web 进程的基本 ~150MB 内存将变成 ~300MB 内存(基础内存使用量乘以 Gunicorn worker 数量)。
在谨慎考虑 Python 应用程序多线程的限制下,我们也可以通过以下方式为工作线程添加线程:
gunicorn <DJANGO_APP_NAME_HERE>.wsgi:application --threads=2 --worker-class=gthread --bind=0.0.0.0:$PORT
即使是在问题 #3 中,我们仍然可以找到使用线程的方式,因为我们希望确保我们的网络进程能够同时处理多个请求,同时注意应用程序的内存占用。在这里,我们的线程可以处理微小的请求,同时确保机器学习任务在其他地方进行分发。
无论是哪种方式,通过利用 Gunicorn 工作进程、线程或两者结合,我们都在为我们的 Python 应用程序配置处理多个请求的能力。我们通过采用多种方式实现并发和/或并行任务处理,基本上解决了问题 #2,同时确保应用程序的关键机器学习任务不依赖于潜在的陷阱,比如多线程,从而为扩展做好准备,并深入解决问题 #3。
好的,那么那个棘手的問題 #1 呢?归根结底,机器学习过程通常会以某种方式消耗硬件资源,无论是内存、CPU 还是 GPU。然而,通过使用分布式系统,我们的机器学习任务与主要的 web 进程紧密相连,但通过 Celery 工作线程并行处理。我们可以通过选择的 Celery 代理 跟踪机器学习任务的开始和结束,并以更加隔离的方式查看度量数据。在这里,Celery 和 Heroku 工作进程的配置由你决定,但这是将一个长时间运行、内存密集型的机器学习任务集成到应用程序中的一个非常好的起点。
低级设计和设置
现在我们已经有机会深入了解并大致了解我们正在构建的系统,接下来让我们把它整合起来,聚焦于具体细节。
为了方便你,这里是我将在本节中提到的代码库。
首先,我们将从设置 Django 和 Django Rest Framework 开始,安装指南分别可以在 这里 和 这里 找到。该应用程序的所有需求可以在代码库的 requirements.txt 文件中找到(同时,Detectron2 和 Torch 将从 Dockerfile 中指定的 Python wheel 构建,以保持 Docker 镜像的体积较小)。
接下来的部分将是设置 Django 应用程序,配置后端以保存到 AWS S3,并使用 DRF 暴露端点,所以如果你已经熟悉这部分内容,可以跳过并直接进入 机器学习任务设置与部署 部分。
Django 设置
继续创建一个文件夹用于 Django 项目并进入该目录。激活您正在使用的虚拟环境/conda 环境,确保按照 第一部分 中的安装说明安装 Detectron2,并安装相关的要求。
在终端中执行以下命令:
django-admin startproject mltutorial
这将创建一个名为“mltutorial”的 Django 项目根目录。继续进入该目录,您可以找到 manage.py 文件和一个名为 mltutorial 的子目录(这是您项目的实际 Python 包)。
mltutorial/
manage.py
mltutorial/
__init__.py
settings.py
urls.py
asgi.py
wsgi.py
打开 settings.py,并将‘rest_framework’,‘celery’和‘storages’(需要用于 boto3/AWS)添加到 INSTALLED_APPS 列表中,以将这些包注册到 Django 项目中。
在根目录中,让我们创建一个应用程序,用于存放后端的核心功能。执行另一个终端命令:
python manage.py startapp docreader
这将在根目录中创建一个名为 docreader 的应用程序。
我们还将在 docreader 中创建一个名为 mltask.py 的文件。在其中,定义一个简单的函数来测试我们的设置,该函数接收一个变量 file_path 并打印出来:
def mltask(file_path):
return print(file_path)
现在进入结构部分,Django 应用程序使用 模型-视图-控制器(MVC)设计模式,定义了在 models.py 中的模型, views.py 中的视图,以及在 Django 模板 和 urls.py 中的控制器。使用 Django Rest Framework,我们将在此管道中包含序列化,它提供了一种将本地 Python 数据结构序列化和反序列化为表示形式(如 json)的方法。因此,公开端点的应用程序逻辑如下:
数据库 ← → models.py ← → serializers.py ← → views.py ← → urls.py
在 docreader/models.py 中,写入以下内容:
from django.db import models
from django.dispatch import receiver
from .mltask import mltask
from django.db.models.signals import(
post_save
)
class Document(models.Model):
title = models.CharField(max_length=200)
file = models.FileField(blank=False, null=False)
@receiver(post_save, sender=Document)
def user_created_handler(sender, instance, *args, **kwargs):
mltask(str(instance.file.file))
这将设置一个模型 Document,该模型要求每个条目在数据库中保存时都必须有一个标题和文件。一旦保存,@receiver 装饰器会监听一个保存后的信号,意味着指定的模型 Document 已经保存到数据库。一旦保存,user_created_handler() 将获取保存实例的文件字段,并将其传递给我们的机器学习功能。
每当对 models.py 进行更改时,您需要运行以下两个命令:
python manage.py makemigrations
python manage.py migrate
接下来,在 docreader 中创建一个 serializers.py 文件,允许序列化和反序列化 Document 的标题和文件字段。在其中写入:
from rest_framework import serializers
from .models import Document
class DocumentSerializer(serializers.ModelSerializer):
class Meta:
model = Document
fields = [
'title',
'file'
]
接下来,在 views.py 中,我们可以定义我们的 CRUD 操作,让我们定义创建和列出文档条目的功能,使用 通用视图(它本质上允许您使用常见视图模式的抽象快速编写视图):
from django.shortcuts import render
from rest_framework import generics
from .models import Document
from .serializers import DocumentSerializer
class DocumentListCreateAPIView(
generics.ListCreateAPIView):
queryset = Document.objects.all()
serializer_class = DocumentSerializer
最后,更新 mltutorial 中的 urls.py:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path('api/', include('docreader.urls')),
]
在 docreader 应用目录中创建 urls.py 并写入:
from django.urls import path
from . import views
urlpatterns = [
path('create/', views.DocumentListCreateAPIView.as_view(), name='document-list'),
]
现在,我们已经准备好在/api/create/端点保存一个包含标题和字段的文档条目,保存后将调用 mltask()!所以,让我们测试一下。
为了帮助可视化测试,让我们将 Document 模型注册到 Django 的管理员界面,这样我们就可以看到新条目何时被创建。
在 docreader/admin.py 中编写:
from django.contrib import admin
from .models import Document
admin.site.register(Document)
创建一个可以登录 Django 管理员界面的用户,使用:
python manage.py createsuperuser
现在,让我们测试我们暴露的端点。
如果没有前端,可以运行 Django 服务器并打开 Postman。发送以下 POST 请求,附带 PDF 文件:

如果我们检查 Django 日志,应该会看到文件路径被打印出来,如在保存后调用 mltask()函数中所指定的。
AWS 设置
你会注意到 PDF 已保存到项目的根目录。让我们确保将媒体文件保存到 AWS S3 中,为部署做好准备。
转到S3 控制台(如果你还没有账号,请创建一个并获取你的账号访问密钥和秘密密钥)。创建一个新的存储桶,这里我们将命名为“djangomltest”。更新权限,确保存储桶对测试是公开的(并根据需要恢复,以便用于生产环境)。
现在,让我们配置 Django 以与 AWS 配合使用。
将你在第一部分中训练好的 model_final.pth 文件放入 docreader 目录。在根目录创建一个.env 文件,并写入以下内容:
AWS_ACCESS_KEY_ID = <Add your Access Key Here>
AWS_SECRET_ACCESS_KEY = <Add your Secret Key Here>
AWS_STORAGE_BUCKET_NAME = 'djangomltest'
MODEL_PATH = './docreader/model_final.pth'
更新 settings.py 以包括 AWS 配置:
import os
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv())
# AWS
AWS_ACCESS_KEY_ID = os.environ['AWS_ACCESS_KEY_ID']
AWS_SECRET_ACCESS_KEY = os.environ['AWS_SECRET_ACCESS_KEY']
AWS_STORAGE_BUCKET_NAME = os.environ['AWS_STORAGE_BUCKET_NAME']
#AWS Config
AWS_DEFAULT_ACL = 'public-read'
AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'}
#Boto3
STATICFILES_STORAGE = 'mltutorial.storage_backends.StaticStorage'
DEFAULT_FILE_STORAGE = 'mltutorial.storage_backends.PublicMediaStorage'
#AWS URLs
STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/static/'
MEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/media/'
可选地,如果 AWS 为我们的静态和媒体文件提供服务,你将需要运行以下命令,以通过 S3 将静态资源提供给管理员界面:
python manage.py collectstatic
如果我们再次运行服务器,我们的管理员界面应该与本地提供静态文件时一样显示。
再次让我们运行 Django 服务器,并测试端点,确保文件现在已保存到 S3。
ML 任务设置与部署
在 Django 和 AWS 配置正确后,让我们在 mltask.py 中设置我们的机器学习过程。由于文件较长,请参考此repo(已添加注释以帮助理解各个代码块)。
重要的是要看到,Detectron2 仅在函数被调用时才会导入,并且模型在调用时才会加载。在这里,我们将仅通过 Celery 任务调用该函数,确保推理过程中使用的内存将仅限于 Heroku 工作进程。
最后,让我们设置 Celery 并将其部署到 Heroku。
在 mltutorial/init_.py 中编写:
from .celery import app as celery_app
__all__ = ('celery_app',)
在 mltutorial 目录中创建 celery.py 并编写:
import os
from celery import Celery
# Set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mltutorial.settings')
# We will specify Broker_URL on Heroku
app = Celery('mltutorial', broker=os.environ['CLOUDAMQP_URL'])
# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
app.config_from_object('django.conf:settings', namespace='CELERY')
# Load task modules from all registered Django apps.
app.autodiscover_tasks()
@app.task(bind=True, ignore_result=True)
def debug_task(self):
print(f'Request: {self.request!r}')
最后,在 docreader 中创建 tasks.py 并编写:
from celery import shared_task
from .mltask import mltask
@shared_task
def ml_celery_task(file_path):
mltask(file_path)
return "DONE"
现在,这个 Celery 任务 ml_celery_task()应该被导入到 models.py 中,并与 post save 信号一起使用,而不是直接从 mltask.py 中拉取的 mltask 函数。将 post_save 信号块更新为以下内容:
@receiver(post_save, sender=Document)
def user_created_handler(sender, instance, *args, **kwargs):
ml_celery_task.delay(str(instance.file.file))
为了测试 Celery,让我们进行部署!
在项目根目录下,包含一个 Dockerfile 和 heroku.yml 文件,二者在仓库中有指定。最重要的是,编辑 heroku.yml 中的commands,可以让你配置 gunicorn web 进程和 Celery 工作进程,这将有助于进一步减少潜在问题。
创建一个 Heroku 账户,并创建一个名为“mlapp”的新应用,同时将.env 文件加入.gitignore。然后,在项目的根目录初始化 git,并将 Heroku 应用的堆栈更改为容器(以便使用 Docker 进行部署):
$ heroku login
$ git init
$ heroku git:remote -a mlapp
$ git add .
$ git commit -m "initial heroku commit"
$ heroku stack:set container
$ git push heroku master
一旦推送完成,我们只需要将环境变量添加到 Heroku 应用程序中。
进入在线界面的设置,滚动到配置变量(Config Vars)部分,点击“显示配置变量”(Reveal Config Vars),并添加.env 文件中列出的每一行。

你可能已经注意到在 celery.py 中指定了一个 CLOUDAMQP_URL 变量。我们需要在 Heroku 上配置一个 Celery Broker,针对这个需求有多种选择。我将使用CloudAMQP,它提供了一个免费的层级。请继续并将其添加到你的应用程序中。添加后,CLOUDAMQP_URL 环境变量将会自动包含在配置变量中。
最后,让我们测试最终产品。
要监控请求,请运行:
$ heroku logs --tail
对 Heroku 应用的 URL 发送另一个 Postman POST 请求,使用/api/create/端点。你会看到 POST 请求传输过来,Celery 接收任务,加载模型并开始运行页面:

我们将在整个过程结束之前继续看到“Running for page...”,你可以在此过程中检查 AWS S3 桶的状态。
恭喜!你现在已经成功部署并运行了一个使用机器学习的 Python 后端,该后端作为分布式任务队列的一部分,与主 Web 进程并行运行!
如前所述,你需要调整 heroku.yml 中的commands以包含 gunicorn 线程和/或工作进程,并对 celery 进行微调。如果需要进一步学习,这里有一篇优秀文章,讲解如何配置 gunicorn 以满足你的应用需求;还有一篇关于生产环境中使用 Celery的文章,以及另一篇关于探索 Celery工作池的文章,可以帮助你更好地管理资源。
编程愉快!
除非另有说明,本文中使用的所有图片均为作者提供
设计 RAG
GenAI
一份关于检索增强生成(Retrieval-Augmented Generation, RAG)设计选择的指南。
·发布于 Towards Data Science ·阅读时长 20 分钟·2024 年 3 月 14 日
--

构建检索增强生成系统,或称 RAG,是一项容易的工作。借助 LamaIndex 或 LangChain 等工具,你可以迅速让基于 RAG 的大型语言模型(LLM)投入使用。当然,需要一定的工程工作来确保系统的效率并良好扩展,但从原则上讲,构建RAG 是简单的部分。更为困难的是,设计它以达到最佳效果。
最近我亲自经历了这个过程,发现为了构建一个检索增强生成系统,必须做出许多大大小小的设计选择。每个选择都有可能影响你基于 RAG 的大型语言模型(LLM)的性能、行为和成本,有时这些影响方式并不显而易见。
不再赘述,让我呈现这一份——虽然不完全,但希望对你有所帮助——的 RAG 设计选择列表。希望它能为你的设计工作提供指导。
RAG 组件
检索增强生成(RAG)使聊天机器人能够访问一些外部数据,从而能够根据这些数据回答用户的问题,而不是基于一般知识或其自身虚构的幻觉。
因此,RAG 系统可能会变得复杂:我们需要获取数据,将其解析成适合聊天机器人的格式,使其可用并可以搜索…
设计大型语言模型(LLMs)与用户体验之间的关系
·发表于Towards Data Science ·13 分钟阅读·2024 年 4 月 19 日
--

不久前,我在 Medium 上写了一篇文章,选择适合您 NLP 用例的语言模型。文章重点介绍了大型语言模型(LLMs)的基本工作原理——尽管它相当受欢迎,但我现在意识到它其实并没有提供关于如何选择 LLMs 的很多信息。我在 LLM 学习之初写了这篇文章,当时我认为 LLMs 的技术细节——它们的内部结构和训练历史——能够自行说明问题,从而帮助 AI 产品开发者在特定场景中自信地选择 LLMs。
从那时起,我将 LLMs 集成到多个 AI 产品中。这让我发现了 LLMs 的技术构成如何决定产品最终体验的具体方式。它也加强了我的信念——产品经理和设计师需要对 LLM 的“幕后”运作有深入了解。LLM 接口不同于传统的图形接口。后者通过以隐含的方式展示产品功能,提供给用户一个(希望是清晰的)心理模型。另一方面,LLM 接口将自由文本作为主要的互动形式,提供了更多的灵活性。同时,它们也“隐藏”了底层模型的能力和局限性,让用户自行探索和发现。因此,一个简单的文本框或聊天窗口可以接收无限数量的意图和输入,并能显示出许多不同的输出。

图 1 显示了一个简单的聊天窗口,可以接收无限数量的输入(图片来自 vectorstock.com,由作者购买授权)
这些交互成功的责任不仅仅在工程方面——实际上,大部分责任应该由产品经理和设计者来承担。在本文中,我们将阐述 LLM 与用户体验之间的关系,并探讨你可以用来提升产品体验的两个通用要素:
-
功能,即 LLM 执行的任务,例如对话、问答和情感分析
-
质量,即 LLM 执行任务的质量,包括正确性和一致性等客观标准,也包括语气和风格等主观标准
(注意:这两项内容是任何 LLM 应用的组成部分。除了这两项,大多数应用还会有一系列更为个性化的标准需要满足,例如延迟、隐私和安全性,本文不予讨论。)
因此,正如彼得·德鲁克所说,这涉及到“做正确的事”(功能)和“做对的事”(质量)。如今,我们知道,LLM 永远不可能做到 100%的正确。作为构建者,你可以从两个方向来接近理想的体验:
-
一方面,你需要追求工程卓越,在选择、微调和评估 LLM 时做出正确的决策。
-
另一方面,你需要通过引导你的用户朝着 LLM 所涵盖的目标迈进,管理他们的期望,并在出现问题时触发相应的处理流程。
在本文中,我们将重点关注工程部分。与人类用户的理想合作关系将在未来的文章中讨论。首先,我将简要介绍工程过程中的步骤——LLM 选择、适配和评估——这些步骤直接决定最终的体验。然后,我们将讨论两个要素——功能和质量——并提供一些指南,帮助你在这两个维度上优化 LLM 的工作,从而提升产品的表现。
范围说明:在本文中,我们将讨论独立 LLM 的使用。许多原则和指南同样适用于用于 RAG(检索增强生成)和代理系统中的 LLM。若需更详细地了解这些扩展 LLM 场景中的用户体验,请参阅我的书籍《AI 产品开发的艺术》。
LLM 工程过程
接下来,我们将重点讨论 LLM 选择、适配和评估这三个步骤。让我们逐一探讨这些步骤:
-
LLM 选择涉及确定你的部署选项(特别是开源与商业 LLM)并选择一个其训练数据和预训练目标与目标功能对齐的 LLM。此外,你能选择的模型越强大(无论是参数规模还是训练数据的数量),它实现高质量的可能性就越大。
-
LLM 适应通过上下文学习或微调,给你提供了弥合用户意图与模型原始预训练目标之间差距的机会。此外,你还可以通过将你希望模型采用的风格和语气融入微调数据中,来调整模型的质量。
-
LLM 评估涉及在模型生命周期中持续评估模型。因此,它不是流程结束时的最终步骤,而是一个持续的活动,随着你收集到更多关于模型的洞察和数据,它会逐渐变得更加具体。
以下图总结了该过程:

图 2 LLM 用户体验工程
在实际应用中,这三个阶段会有所重叠,且各阶段之间可能会反复往返。通常,模型选择更像是“一次性的大决定”。当然,你可以在后续过程中从一个模型切换到另一个,甚至应当在新的、更合适的模型出现在市场时这样做。然而,这些更改是昂贵的,因为它们会影响所有下游工作。在发现阶段之后,你通常不希望频繁进行这些更改。另一方面,LLM 适应和评估是高度迭代的。这些活动应伴随持续的发现过程,你将通过这些过程更多地了解模型的行为和用户的需求。最后,所有三个活动都应嵌入到一个稳固的 LLMOps 流水线中,这将使你能够以最小的工程摩擦整合新的洞察和数据。
现在,让我们进入图表的第二列,确定 LLM 的功能范围,并了解它如何在该过程中三个阶段中得到塑造。
功能:响应用户意图
你可能会想知道,为什么我们要讨论 LLM 的“功能”?毕竟,LLM 不是那种能够神奇地完成我们能想到的任何语言任务的多才多艺的全能型工具吗?实际上,它们确实如此,正如论文Language Models Are Few-Shot Learners中著名描述的那样。LLM 可以从仅仅几个例子中学习新能力。有时,它们的能力甚至会在正常训练中“突然出现”——并且——希望是偶然被发现的。这是因为语言建模的任务既多才多艺又充满挑战——作为副作用,它使得 LLM 具备了执行许多其他相关任务的能力。
然而,LLM 的预训练目标是根据过去词语的上下文生成下一个词(好吧,这是一个简化的说法——在自编码中,LLM 可以双向工作[3])。这就是一个预训练的 LLM 在被提示后会坚持做的事情,动机来自一个虚构的“奖励”。在大多数情况下,这个目标与一个用户使用你的产品进行聊天、寻求问题的答案,或是将一段德文翻译成意大利文之间有着相当大的差距。Emily Bender 和 Alexander Koller 的标志性论文《迈向自然语言理解:在数据时代的意义、形式与理解》甚至认为语言模型通常无法恢复沟通意图,因此注定只能处理不完整的意义表示。
因此,夸耀在科学研究中令人惊叹的 LLM 能力并在高度控制的基准和测试场景中展示它们是一回事。而将 LLM 推出给一群具有不同 AI 技能和目的——其中一些可能是有害的——匿名用户则是另一回事。这一点尤其正确,一旦你明白你的产品不仅继承了 LLM 的能力,还继承了它的弱点和风险,并且你(而非第三方提供商)需要对其行为负责。
实际上,我们已经学到,在将 LLM 集成到产品中时,最好识别并隔离离散的功能模块。这些功能大致对应于用户使用你的产品时的不同意图。例如,它可能是:
-
进行对话
-
检索信息
-
寻求特定情境的建议
-
寻找灵感
这些能力通常可以进一步分解成更细化、甚至是可复用的能力。例如,“进行对话”可以分解为:
-
提供有信息量和相关性的对话回复
-
维护过去交互的记忆(而不是每次都从头开始)
-
展示一致的个性
采用这种更加离散的方法来处理 LLM 的能力,能为你带来以下优势:
-
ML 工程师和数据科学家可以更好地专注于他们的工程活动(见图 2),聚焦于目标功能。
-
关于你的产品的沟通变得精准和具体,有助于管理用户期望,并维护信任、诚信和信誉。
-
在用户界面中,你可以使用一系列设计模式,如提示模板和占位符,以提高用户意图与模型功能对齐的机会。
确保正确功能的指南
让我们总结一些实用的指南,确保 LLM 在你的产品中做对了事:
-
在 LLM 选择过程中,确保你了解模型的基本预训练目标。预训练目标有三种基本类型(自编码、自动回归、序列到序列),它们每种都会影响模型的行为。
-
许多 LLM 也已经通过更高级的目标进行预训练,例如对话或执行明确指令(指令微调)。选择一个已经为你的任务做好准备的模型,可以让你高效地起步,减少你在下游适配和微调中所需的工作量,从而实现满意的质量。
-
通过上下文学习或微调进行 LLM 适配,可以为你提供一个机会,弥合原始预训练目标和你希望服务的用户意图之间的差距。

图 3 LLM 适配缩小了预训练目标和用户意图之间的差距
-
在初期发现阶段,你可以通过上下文学习来收集初步的使用数据,并加深对相关用户意图及其分布的理解。
-
在大多数情况下,长远来看,上下文学习(提示微调)是不可持续的——它本身并不高效。随着时间的推移,你可以利用新数据和学习成果作为基础,对模型的权重进行微调。
-
在模型评估过程中,确保应用特定任务的度量标准。例如,Text2SQL LLM(参见这篇文章)可以通过执行准确率和测试集准确率等指标进行评估,而摘要生成可以通过基于相似度的指标进行评估。
这些只是我们在整合 LLM 时学到的课程的简短片段。我即将出版的书籍《人工智能产品开发的艺术》将深入探讨每一条指南,并提供大量的示例。关于预训练目标和过程的技术细节,你可以参考这篇文章。
好的,现在你已经了解了用户使用你的产品时的意图,并“激发”了你的模型来响应这些意图。你甚至可能已经将 LLM 投入到实际环境中,希望它能够启动数据飞轮。现在,如果你想保持现有用户并吸引新用户,你需要迅速提升第二个关键要素——质量。
实现高质量
在 LLM 的背景下,质量可以分解为客观和主观两个组成部分。客观部分告诉你何时以及为何出现问题(即 LLM 犯了明显的错误)。主观部分则更为微妙和情感化,反映了与特定用户群体的契合度。
目标质量标准
使用语言进行交流对人类来说是自然而然的。语言从我们生命的开始就深深扎根在我们的大脑中,我们很难想象从零开始学习语言需要付出多少努力。即便是我们在学习外语时所遇到的挑战,也无法与 LLM 的训练相比。LLM 从一张空白的纸开始,而我们学习语言的过程则建立在一个极其丰富的世界知识基础和语言运作的基本理解之上。
在使用 LLM 时,我们应该时刻保持意识,注意到许多可能出现的错误:
-
LLM 可能会犯语言错误。
-
LLM 可能在连贯性、逻辑性和一致性方面表现欠佳。
-
LLM 可能缺乏足够的世界知识,从而导致错误的陈述和幻觉。
这些缺点可能很快就会成为你产品的致命问题——输出质量是 LLM 产品用户体验的核心决定因素。例如,ChatGPT 在“公开”成功的一个主要因素是它确实能够在各个领域生成正确、流畅且相对连贯的文本。早期版本的 LLM 无法实现这一目标质量。如今大多数用于生产的预训练 LLM 确实具备生成语言的能力。然而,它们在连贯性、一致性和世界知识等标准上的表现可能非常不稳定且不一致。为了实现你期望的体验,明确优先排序这些要求,并据此选择和调整 LLM 是非常重要的。
主观质量标准
在更为微妙的主观领域,您需要了解并监控用户对产品的感受。他们在使用产品时是否感到愉快、信任,并进入心流状态?还是他们带着沮丧、低效和不对劲的感觉离开?这些都与个人的文化、价值观和风格息息相关。如果你正在为初级开发人员构建一个副驾驶,你显然不希望它使用高级管理人员的语言,反之亦然。
举个例子,假设你是一个产品营销人员,你花了很多时间和一个工程师一起反复修改一个帮助你进行内容生成的 LLM。某一天,你和团队中的 UX 设计师聊天,向他炫耀你的新 AI 助手。你的同事不理解为何要付出这么多努力。他平时使用 ChatGPT 来帮助创建和评估 UX 调查,并对结果非常满意。你反驳道——ChatGPT 输出的内容对于你的故事叙述和写作任务来说过于通用且单调。事实上,刚开始使用时,你也曾遇到过相当尴尬的情况,因为有一段时间,读者开始识别出 ChatGPT 特有的风格。那是你职业生涯中的一个滑铁卢,之后你决定需要一个更复杂的工具。
在这个讨论中没有对错之分。ChatGPT 适合处理那些直接的、事实性的任务,在这些任务中风格并不那么重要。相反,作为市场营销人员,你需要一个能够帮助你打造高质量、具有说服力的沟通内容的助手,这些内容能够讲述你客户的语言,并且反映你公司独特的 DNA。
这些主观的细微差别最终可能决定一个 LLM 是否有用——即使它的输出最终需要重写,还是能够定义为“足够好”,让用户开始使用它并提供合适的微调数据。LLM 掌握的圣杯是个性化——即通过高效的微调或提示调优,使 LLM 适应任何与模型有一定时间互动的用户的个人偏好。如果你刚刚开始 LLM 之旅,这些细节可能看起来还很遥远——但最终,它们将帮助你达到一个阶段,让你的 LLM 通过以精确的方式和风格回应用户的需求,带来用户满意度和大规模采用,并把你的竞争对手甩在身后。
指南
以下是我们在管理 LLM 质量方面的建议:
-
留意不同种类的反馈。对质量的追求是持续的和迭代的——你从一些数据点和对产品质量含义的粗略理解开始。随着时间的推移,你会逐渐丰富更多细节,学习哪些杠杆可以用来改善你的 LLM。
-
在模型选择过程中,你仍然有很多发现要做——从“眼球”判断开始,并通过不同的输入测试不同的 LLM(最好由多个团队成员来做)。
-
你的工程师们也会评估学术基准和与模型一起发布的评估结果。然而,请记住,这些只是模型在你特定产品中的表现的粗略指标。
-
一开始,完美主义不是答案。你的模型应该足够好,以吸引那些能够开始为其提供相关数据以进行微调和评估的用户。
-
将你的团队和用户聚集起来,进行定性的 LLM 输出讨论。当他们用语言来判断和辩论什么是对的,什么是错的时,你可以逐渐揭示出他们的客观和情感需求。
-
确保建立一个完善的 LLMOps 流程,这样你可以顺利地集成新数据,减少工程上的摩擦。
-
不要停下来——在后期阶段,你可以将焦点转向细微差别和个性化,这也将帮助你加强竞争优势。
总结:承担责任
预训练的大语言模型非常方便——它们让 AI 变得对每个人都可访问,减轻了训练大型初始模型所需的巨额工程、计算和基础设施开支。一旦发布,它们可以立即使用,我们可以将其惊人的功能融入到我们的产品中。然而,当在产品中使用第三方模型时,你不仅继承了它的强大功能,还继承了它可能失败的多种方式。当事情出错时,为了保持产品的完整性,你最不希望做的事情就是将责任归咎于外部模型提供商、你的工程师,或者——更糟糕——你的用户。
因此,在使用大语言模型(LLMs)时,你不仅应该了解模型的来源(训练数据和过程)的透明度,还要建立因果理解,了解其技术构成如何影响你的产品所提供的体验。这将帮助你在开始阶段找到启动强大数据飞轮和在产品逐渐走向卓越时持续优化和差异化大语言模型之间的敏感平衡。
参考文献
[1] Janna Lipenkova(2022 年)。《为你的 NLP 用例选择合适的语言模型》,Medium。
[2] Tom B. Brown 等(2020 年)。《语言模型是少量示例学习者》。
[3] Jacob Devlin 等(2018 年)。《BERT:用于语言理解的深度双向 Transformer 预训练》。
[4] Emily M. Bender 和 Alexander Koller(2020 年)。《迈向自然语言理解:在数据时代的意义、形式和理解》。
[5] Janna Lipenkova(即将出版)。《AI 产品开发艺术》,Manning 出版公司。
注:所有图片均由作者提供,除非另有说明。
检测社交媒体流量时间序列中的异常
我如何检测社交媒体流量中的异常:一种基于残差的方法
·发表于 Towards Data Science ·7 分钟阅读·2024 年 11 月 11 日
--

图片由 Joshua Hoehne 提供,来源于 Unsplash
在社交媒体时代,分析对话量对于理解用户行为、检测趋势,以及最重要的是识别异常变得至关重要。知道异常发生的时间可以帮助管理层和营销团队在危机情境中作出回应。
在这篇文章中,我们将探讨一种基于残差的方法,用于检测社交媒体流量时间序列数据中的异常,使用来自 Twitter 的一个真实世界示例。对于这个任务,我将使用 Numenta Anomaly Benchmark 提供的数据集,其中包含 Twitter 帖子的流量数据,并且数据窗口为 5 分钟。
我们将从两个角度分析数据:作为第一个练习,我们将使用完整的数据集检测异常,然后我们将在实时场景中检测异常,以检查此方法的响应能力。
使用完整数据集检测异常
分析一个示例的 Twitter 流量数据集
让我们从加载和可视化一个关于 Apple 的示例 Twitter 流量数据集开始:

观察到的 AAPL Twitter 流量的体积和对数体积
作者提供的图片
从这个图表中,我们可以看到数据中有几个尖峰(异常)。这些体积尖峰就是我们希望识别的异常。
从第二个图(对数尺度)来看,我们可以看到 Twitter 成交量数据呈现出明显的日常周期,白天活动较多,夜间活动较少。这种季节性模式在社交媒体数据中很常见,因为它反映了用户的昼夜活动。它还表现出每周的季节性,但我们将忽略这一点。
去除季节性趋势
我们希望确保这个周期不会干扰我们的结论,因此我们将将其移除。为了去除这种季节性,我们将进行季节性分解。
首先,我们将计算成交量的移动平均(MA),这将捕捉到趋势。然后,我们将计算观察到的成交量与 MA 的比率,这为我们提供了乘法季节效应。

时间对成交量的乘法效应
图片来源:作者
正如预期的那样,季节性趋势遵循昼夜周期,白天时达到高峰,夜间则处于低谷。
为了进一步进行分解,我们需要根据之前发现的乘法趋势计算成交量的期望值。

AAPL Twitter 成交量的观察值和期望值的成交量及对数成交量
图片来源:作者
分析残差并检测异常
分解的最终组成部分是由于期望值和真实值之间的差异所产生的误差。我们可以将这一度量视为去除均值后的考虑季节性的成交量:

AAPL Twitter 成交量季节性分解后的绝对误差和对数误差
图片来源:作者
有趣的是,残差分布紧密地跟随帕累托分布。这一特性使得我们可以使用帕累托分布来设置阈值以检测异常,因为我们可以标记任何高于某一百分位(例如 0.9995)的残差为潜在异常。

绝对误差和对数误差的分位数与帕累托分位数
图片来源:作者
现在,我必须做一个重要的声明:我所说的这一特性本身并不“绝对正确”。根据我在社交监听中的经验,我观察到这一点对于大多数社交数据来说是成立的,除了那些异常值较多且右偏的数据集。
在这种特定情况下,我们的观察值超过了 15k,因此我们将 p 值设置为 0.9995。基于这一阈值,每 10,000 个观察值大约会检测到 5 个异常(假设完美的帕累托分布)。
因此,如果我们检查数据中哪些观察值的误差的 p 值高于 0.9995,我们将得到以下信号:

AAPL Twitter 成交量的异常信号
图片来源:作者
从这张图表中,我们看到体积最大的观察值被突出显示为异常。当然,如果我们希望更多或更少的信号,可以调整所选择的 p 值,记住,随着 p 值的降低,信号的数量将增加。
实时异常检测
现在让我们切换到一个实时场景。在这种情况下,我们将对每个新观察数据运行相同的算法,检查哪些信号被返回以及信号在观察发生后的返回速度:

AAPL Twitter 交易量的实时信号
作者提供的图片
我们可以清楚地看到,这次我们有更多的信号。这是合理的,因为我们拟合的帕累托曲线会随着可用数据的变化而变化。如果我们检查“2015-03-08”之前的数据,前三个信号可以被视为异常,但如果我们考虑整个数据集,它们的重要性就较低。
根据构建方式,提供的代码返回的信号仅限于过去 24 小时的数据。然而,正如我们在下面所看到的,大多数信号在考虑到新观察数据时立即被返回,只有少数几个信号已被加粗:
New signal at datetime 2015-03-03 21:02:53, relative to timestamp 2015-03-03 21:02:53
New signal at datetime 2015-03-03 21:07:53, relative to timestamp 2015-03-03 21:07:53
New signal at datetime 2015-03-03 21:12:53, relative to timestamp 2015-03-03 21:12:53
New signal at datetime 2015-03-03 21:17:53, relative to timestamp 2015-03-03 21:17:53 **
New signal at datetime 2015-03-05 05:37:53, relative to timestamp 2015-03-04 20:07:53
New signal at datetime 2015-03-07 09:47:53, relative to timestamp 2015-03-06 19:42:53 **
New signal at datetime 2015-03-09 15:57:53, relative to timestamp 2015-03-09 15:57:53
New signal at datetime 2015-03-09 16:02:53, relative to timestamp 2015-03-09 16:02:53
New signal at datetime 2015-03-09 16:07:53, relative to timestamp 2015-03-09 16:07:53
New signal at datetime 2015-03-14 01:37:53, relative to timestamp 2015-03-14 01:37:53
New signal at datetime 2015-03-14 08:52:53, relative to timestamp 2015-03-14 08:52:53
New signal at datetime 2015-03-14 09:02:53, relative to timestamp 2015-03-14 09:02:53
New signal at datetime 2015-03-15 16:12:53, relative to timestamp 2015-03-15 16:12:53
New signal at datetime 2015-03-16 02:52:53, relative to timestamp 2015-03-16 02:52:53
New signal at datetime 2015-03-16 02:57:53, relative to timestamp 2015-03-16 02:57:53
New signal at datetime 2015-03-16 03:02:53, relative to timestamp 2015-03-16 03:02:53
New signal at datetime 2015-03-30 17:57:53, relative to timestamp 2015-03-30 17:57:53
New signal at datetime 2015-03-30 18:02:53, relative to timestamp 2015-03-30 18:02:53
New signal at datetime 2015-03-31 03:02:53, relative to timestamp 2015-03-31 03:02:53
New signal at datetime 2015-03-31 03:07:53, relative to timestamp 2015-03-31 03:07:53
New signal at datetime 2015-03-31 03:12:53, relative to timestamp 2015-03-31 03:12:53
New signal at datetime 2015-03-31 03:17:53, relative to timestamp 2015-03-31 03:17:53
New signal at datetime 2015-03-31 03:22:53, relative to timestamp 2015-03-31 03:22:53
New signal at datetime 2015-03-31 03:27:53, relative to timestamp 2015-03-31 03:27:53
New signal at datetime 2015-03-31 03:32:53, relative to timestamp 2015-03-31 03:32:53
New signal at datetime 2015-03-31 03:37:53, relative to timestamp 2015-03-31 03:37:53
New signal at datetime 2015-03-31 03:42:53, relative to timestamp 2015-03-31 03:42:53
New signal at datetime 2015-03-31 20:22:53, relative to timestamp 2015-03-31 20:22:53 **
New signal at datetime 2015-04-02 12:52:53, relative to timestamp 2015-04-01 20:42:53 **
New signal at datetime 2015-04-14 14:12:53, relative to timestamp 2015-04-14 14:12:53
New signal at datetime 2015-04-14 22:52:53, relative to timestamp 2015-04-14 22:52:53
New signal at datetime 2015-04-14 22:57:53, relative to timestamp 2015-04-14 22:57:53
New signal at datetime 2015-04-14 23:02:53, relative to timestamp 2015-04-14 23:02:53
New signal at datetime 2015-04-14 23:07:53, relative to timestamp 2015-04-14 23:07:53
New signal at datetime 2015-04-14 23:12:53, relative to timestamp 2015-04-14 23:12:53
New signal at datetime 2015-04-14 23:17:53, relative to timestamp 2015-04-14 23:17:53
New signal at datetime 2015-04-14 23:22:53, relative to timestamp 2015-04-14 23:22:53
New signal at datetime 2015-04-14 23:27:53, relative to timestamp 2015-04-14 23:27:53
New signal at datetime 2015-04-21 20:12:53, relative to timestamp 2015-04-21 20:12:53
正如我们所见,算法能够实时检测到异常,大多数信号在考虑到新观察数据时立即被触发。这使得组织能够迅速对社交媒体谈论量的意外变化做出反应。
结论与进一步改进
本文提出的基于残差的方法提供了一种灵活的工具,用于检测社交媒体量时间序列中的异常。这种方法可以帮助公司和营销人员识别发生中的重要事件、趋势和潜在危机。
尽管该算法已经有效,但仍有几个方面可以进一步发展,例如:
-
依赖于固定窗口进行实时检测,目前的实现依赖于所有历史数据
-
探索不同的时间粒度(例如,使用每小时而非 5 分钟间隔)
-
使用统计测试验证帕累托分布假设
-
…
如果你喜欢这篇文章,请留下掌声并随时评论,任何建议和反馈都非常感激!
使用 AI 检测云朵
从随机森林到 YOLO:比较不同的算法在卫星影像中进行云朵分割的效果。
Carmen Adriana Martínez Barbosa 博士
·发表于Towards Data Science ·10 分钟阅读·2024 年 7 月 17 日
--
作者:Carmen Martínez-Barbosa 和 José Arturo Celis-Gil

画着绿色田野和满是花朵的云朵,风格模仿梵高。图像由作者使用 DALL.E 生成。
卫星影像已经彻底改变了我们的世界。多亏了卫星影像,人类可以实时追踪水、空气、土地、植被的变化,以及我们在全球范围内所产生的足迹效应。提供此类信息的应用程序无穷无尽。例如,它们已被用于评估土地利用对河流水质的影响。卫星影像还被用于监测野生动物和观察城市人口增长等。
根据关心科学家联盟(UCS)的说法,大约一千颗地球观测卫星在环绕地球。然而,最为人所知的之一是哨兵-2。哨兵-2是由欧洲航天局(ESA)开发的地球观测任务,属于哥白尼计划,能够在陆地和沿海水域上获取高空间分辨率(10 米至 60 米)的影像。哨兵-2获取的数据是多光谱影像,包含 13 个波段,涵盖了可见光、近红外和短波红外等电磁波谱部分。
哨兵-2 和其他地球观测卫星所生成的影像对于开发上述应用至关重要。然而,使用卫星图像可能会受到云层的阻碍。根据Rutvik Chauhan 等人的研究,地球表面大约有一半被不透明的云层覆盖,另外 20% 被卷云或薄云遮挡。当云层覆盖一个感兴趣区域时,情况还会变得更糟,可能持续数月。因此,云层去除对于卫星数据的预处理是必不可少的。
在这篇博客中,我们使用并比较了不同的算法来分割哨兵-2卫星图像中的云层。我们探索了各种方法,从经典的随机森林到最先进的计算机视觉算法 YOLO。你可以在这个 GitHub 仓库中找到该项目的所有代码。
事不宜迟,开始吧!
免责声明: 哨兵 数据对广泛的区域、国家、欧洲和国际用户社区免费开放。你可以通过哥白尼开放访问中心、Google Earth Engine 或 Python 包 sentinelhub 访问这些数据。在这篇博客中,我们使用最后一种方式。
SentinelHub 简介
Sentinelhub是一个 Python 包,支持许多实用工具来下载、分析和处理卫星影像数据,包括哨兵-2数据。该包提供了出色的文档和示例,便于使用,在开发端到端地理数据科学解决方案时,非常受欢迎。
要使用Sentinelhub,你必须在Sentinel Hub 仪表板创建一个帐户。登录后,进入仪表板的“用户设置”标签页,创建一个 OAuth 客户端。此客户端允许你通过 API 连接到 Sentinelhub。如何获取 OAuth 客户端的步骤可以在Sentinelhub官方文档中找到详细说明。
一旦你拥有了凭据,请将它们保存在一个安全的地方。 它们将不再显示;如果丢失,你必须创建新的凭据。
你现在可以下载Sentinel-2图像和云概率了!
获取数据
在我们的GitHub 存储库中,你可以找到脚本src/import_image.py,它使用你的 OAuth 凭据下载Sentinel-2图像和云概率。我们还包括了settings/coordinates.yaml文件,里面包含了一系列边界框以及它们各自的日期和坐标参考系统(CRS)。你可以自由使用这个文件来下载数据;不过,我们建议你使用自己的坐标集。
使用Sentinelhub下载数据的坐标示例。
我们下载所有 13 个波段的图像,采用数字数值(DN)表示。出于我们的目的,我们只使用光学(RGB)波段。
是否需要预处理数据?
原始图像在 RGB 波段中的 DN 分布通常是偏态的,包含异常值或噪声。因此,你必须在训练任何机器学习模型之前对这些数据进行预处理。

原始图像示例,它的 DN 分布和云概率。图像由作者制作。
我们在预处理原始图像时遵循的步骤如下:
-
使用
log1p转换:这有助于减少 DN 分布的偏态。 -
使用
min-max缩放转换:我们这样做是为了归一化 RGB 波段。 -
将 DN 转换为像素值:我们将归一化的 RGB 波段乘以 255,并将结果转换为 UINT8 格式。
这些步骤的实现可以通过一个 Python 函数完成:
原始 Sentinel 图像的预处理。你可以在我们的GitHub 存储库中的脚本 src/preprocess.py 中查看代码。
图像已经清理完毕。现在是时候将云概率转换为掩模。
使用Sentinelhub的一个重要优势是,云概率以灰度图的像素值形式提供。因此,每个像素值除以 255 后代表该像素中存在云的概率。通过这种方式,我们将值从[0, 255]范围转换为[0, 1]。现在,为了创建掩模,我们需要类别而不是概率。因此,我们设置了一个 0.4 的阈值来决定一个像素是否属于云。
将云概率转换为掩模。代码位于我们GitHub 仓库的脚本 src/preprocess.py 中。
上述预处理方法增强了数据集的亮度和对比度;当训练不同的模型时,它也是获得有意义结果所必需的。

预处理后的示例图像,其像素值分布以及结果云掩模。图像由作者制作。
一些需要考虑的警告
在某些情况下,结果掩模与对应图像中的云不匹配,如下图所示:

错误掩模示例。请注意,云以外的区域被标记为云区域。
这可能由于多种原因导致:一个原因是Sentinelhub中使用的云检测模型,它返回了假阳性。另一个原因可能是我们在预处理过程中使用的固定阈值。为了解决这个问题,我们提出了两种方法:一种是重新创建新的掩模,另一种是丢弃图像-掩模对。我们选择了第二种方法。在这个链接中,我们分享了一些预处理后的图像和掩模,欢迎你在尝试本文所述算法时使用它们。
在建模之前,让我们建立一个合适的度量标准来评估模型的预测性能。
用于评估实例分割模型的度量标准有多个。其中之一是交并比(IoU)。该度量标准衡量两个分割掩模之间的重叠程度。IoU 的值范围从 0 到 1。IoU=0 表示预测分割掩模与真实分割掩模之间没有重叠。IoU=1 表示完美的预测。

IoU 的定义。图像由作者制作。
我们通过对一张测试图像测量 IoU 来评估我们的模型。 我们对 IoU 的实现如下:
使用 TensorFlow 实现 IoU。
最后,图像中的云分割
我们现在准备在预处理后的卫星图像中进行云分割。我们使用了几种算法,包括经典方法,如随机森林和人工神经网络(ANNs)。我们还使用了常见的目标分割架构,如 U-NET 和 SegNet。最后,我们实验了一种最先进的计算机视觉算法:YOLO。
随机森林
我们希望探索经典方法在卫星图像中分割云层的效果。为此实验,我们使用了随机森林。如所周知,随机森林是由一组决策树组成,每棵树都在数据的不同随机子集上进行训练。
我们必须将图像转换为表格数据,以便训练随机森林算法。在以下代码片段中,我们展示了如何进行转换:
从图像转换为表格数据并训练随机森林模型。
注意: 你可以通过在终端中运行脚本 src/model.py 来使用预处理后的图像和掩模训练模型:
> python src/model.py --model_name={model_name}
其中:
-
--model_name=rf训练一个随机森林模型。 -
--model_name=ann训练一个人工神经网络(ANN)。 -
--model_name=unet训练一个 U-NET 模型。 -
--model_name=segnet训练一个 SegNet 模型。 -
--model_name=yolo训练 YOLO 模型。
使用随机森林对测试图像进行预测,结果如下:

使用随机森林进行云预测。图像由作者创建。
令人惊讶的是,随机森林在分割这张图像中的云层方面表现良好。然而,它的预测是基于像素的,这意味着该模型在训练过程中无法识别云层的边缘。
ANN
人工神经网络是模拟大脑结构的强大工具,通过从数据中学习并进行预测。我们使用了一个简单的架构,只有一个隐藏的全连接层。我们的目标不是优化 ANN 的架构,而是探索全连接层在卫星图像中分割云层的能力。
Keras 中的 ANN 实现。
与随机森林一样,我们将图像转换为表格数据来训练人工神经网络(ANN)。
模型在测试图像上的预测结果如下:

使用人工神经网络(ANN)进行云预测。图像由作者创建。
尽管该模型的 IoU 比随机森林差,但人工神经网络(ANN)没有将海岸像素分类为云。这一事实可能与其架构的简单性有关。
U-NET
它是由 Olaf Ronneberger 等人于 2015 年开发的卷积神经网络(CNN)。(请参见原文这里)。该架构是一个基于编码器-解码器的模型。编码器捕捉图像的基本特征和模式,如边缘、颜色和纹理。解码器帮助创建图像中不同物体或区域的详细地图。在 U-NET 架构中,每个卷积编码器层都与其在解码器层中的对应层连接。这被称为跳跃连接。

U-NET 架构。图片来源:Olaf Ronneberger 等,2015。
U-Net 通常用于需要高精度和细节的任务,如医学影像处理。
我们的 U-NET 架构实现如下代码片段:
Keras 中的 U-NET 实现。
U-NET 模型的完整实现可以在我们的GitHub 仓库中的脚本src/model_class.py中找到。训练时,我们使用批处理大小为 10,训练 100 个 epoch。U-NET 模型在测试图像上的结果如下所示:

使用 U-NET 进行的云预测。图片由作者创建。
这是获得的最佳 IoU 度量。
SegNet
这是另一个基于编码器-解码器的模型,2017 年由Vijay Badrinarayanan 等人开发。SegNet 由于使用最大池化索引进行上采样,具有更高的内存效率。该架构适用于内存效率和速度至关重要的应用,如实时视频处理。

SegNet 架构。图片来源于Shih-Yu Chen 等人(2021 年)
该架构与 U-NET 的不同之处在于,U-NET 使用跳跃连接来保留细节,而 SegNet 则没有。
与其他模型一样,SegNet 可以通过运行脚本src/model.py来进行训练。我们再次使用批处理大小为 10,训练 100 个 epoch。测试图像上的云分割结果如下所示:

使用 SegNet 进行的云预测。图片由作者创建。
没有 U-NET 好!
YOLO
You Only Look Once (YOLO) 是一个快速高效的物体检测算法,2015 年由Joseph Redmon 等人开发。该算法的优点在于,它将物体检测视为回归问题,而不是分类任务,通过空间上分离边界框并为每个检测到的图像关联概率,使用单一的卷积神经网络(CNN)。
YOLO 的优势在于它支持多种计算机视觉任务,包括图像分割。我们通过Ultralytics 框架使用 YOLO 分割模型。训练过程非常简单,如下所示:
使用 Ultralytics 框架训练 YOLO。
您只需要设置一个包含图像和标签路径的dataset.yaml文件。有关如何运行 YOLO 模型进行分割的更多信息,请参阅此处。
注意: 训练 YOLO 模型进行分割时需要使用云轮廓,而不是掩模。您可以在此数据链接中找到标签。
测试图像上的云分割结果如下所示:

使用 YOLO 进行的云预测。图片由作者创建。
哎呀,这是一个糟糕的结果!
尽管 YOLO 是许多分割任务的强大工具,但在图像有明显模糊的情况下,YOLO 的表现可能较差,因为模糊会降低物体与背景之间的对比度。此外,YOLO 在图像中分割多个重叠物体时可能会遇到困难。由于云层是模糊的物体,边缘不清晰且经常与其他云层重叠,因此 YOLO 并不是分割卫星图像中云层的合适模型。
我们分享了上面解释过的训练模型 点击此链接查看。 由于文件大小(6GB!),我们没有包括随机森林。
关键要点
我们探索了如何使用不同的机器学习方法在Sentinel-2卫星图像中进行云层分割。以下是这次实验的一些收获:
-
使用 Python 包sentinelhub获得的数据并不适合直接用于模型训练。你需要对这些数据进行预处理,并根据所选模型的要求可能需要调整数据格式(例如,在训练随机森林或 ANN 时将图像转换为表格数据)。
-
最佳模型是 U-NET,其次是随机森林和 SegNet。U-NET 和 SegNet 出现在这份名单上并不令人意外,因为这两种架构是专门为分割任务开发的。然而,随机森林表现得出乎意料的好。这表明,机器学习方法在图像分割中也能取得良好效果。
-
最差的模型是 ANN 和 YOLO。由于架构简单,我们预期 ANN 无法取得好的结果。至于 YOLO,尽管它是计算机视觉领域的最先进算法,但在图像中进行云层分割并不是一个合适的任务。总体而言,这个实验表明,作为数据科学家,我们必须始终寻找最适合我们数据的算法。
我们希望你喜欢这篇文章,再次感谢你的阅读!
你可以通过 LinkedIn 与我们联系:
www.linkedin.com/in/jose-celis-gil/
www.linkedin.com/in/camartinezbarbosa/
检测概念漂移:对机器学习性能的影响
MLOps
我什么时候应该重新训练我的模型?
·发表于 Towards Data Science ·阅读时间:14 分钟·2024 年 1 月 16 日
--

你听说过终身学习吗?你可能熟悉这样的故事:随着今天科技的快速发展,我们在学校学到的东西不能保证我们整个职业生涯都能取得成功。为了在就业市场中保持竞争力,我们需要学会如何持续学习。在这一方面,人工智能和我们人类并没有太大的不同。机器学习模型的知识也会过时,它们需要像我们一样重新学习。那么,模型何时会变得过时呢?

什么是概念漂移,我们能检测到它吗?
机器学习模型知识过时的现象被称为概念漂移。然而,在深入细节之前,我们先快速回顾一下更广泛的问题:数据漂移。
数据漂移概述
世界在变化。消费者行为和口味随着时间的推移而演变;随着年龄的增长,你的用户可能会改变他们的偏好;数据收集设备也可能会以意想不到的方式发生故障或损坏。无论你所在的行业是什么,或者你用机器学习解决什么问题,你可以肯定的是,在某个时刻,你的生产模型接收到的数据将会与它在训练时看到的数据不同。因此,机器学习模型在投入生产后往往会随着时间的推移而退化。
数据漂移的类型
世界的变化可以以不同的方式转化为数据的变化。为了更好地理解这一点,引入一些符号是非常有用的。
一般来说,机器学习模型处理两种类型的输入数据:特征,X,和目标,y。数据偏移在其最一般的形式下是描述特征和目标的联合分布变化,P(X, Y)。有四种可能的原因导致P(X, Y)的变化*。
为了列出所有四种方式,我们需要使用所谓的乘积法则,这是一个数学公式,表示 P(X, Y) = P(Y, X) = P(X|Y)P(Y) = P(Y|X)P(X)。
从那里可以得出,特征和目标的联合分布(可以等效地写作 P(X, Y) 或 P(Y, X))可以通过两种不同但等效的方式进行分解:
-
P(X|Y) * P(Y)
-
P(Y|X) * P(X)
这意味着,如果上述四个元素中的任何一个发生变化,P(X, Y) 也会发生变化,从而导致数据偏移。每个元素的变化都有其自己的名称、原因和解决方案。让我们简要看一下它们。
旁注:我说过四个元素中的每一个都可能发生变化,从而导致数据偏移。但当然,并没有规则禁止四个元素中的多个同时发生变化。事实上,它们经常同时变化,导致数据偏移变成一个多面且复杂的现象。然而,在本文中,我们假设在任何给定时刻,四个元素中只有一个发生变化。
所以,回到四种数据偏移类型。
-
如果 P(X) 发生变化(且 P(Y|X) 不变),我们就谈论协变量偏移。一旦我们意识到协变量只是模型中的特征或自变量的另一种说法,这个名称就非常合理。协变量偏移是指模型输入的分布发生变化。
-
如果 P(Y) 发生变化(但 P(X|Y) 不变),我们就谈论标签偏移。这意味着输出分布发生了变化,但对于任何给定的输出,输入分布保持不变。
-
如果 P(Y|X) 发生变化(但 P(X) 不变),那就是概念偏移,本文的主题。我们将很快详细探讨它。
-
最后,P(X|Y) 发生变化,而 P(Y) 保持不变的情况被称为表现偏移。这意味着相同的目标值在输入分布中以不同的方式表现出来。我们在这里不会讨论表现偏移,将它留到另一个单独的文章中。
在四种数据偏移类型中,协变量偏移和概念偏移是讨论最多的,也是大多数公司在生产环境中使用机器学习模型进行预测时最为关注的问题。让我们讨论如何检测这两种偏移,看看概念偏移检测相比协变量偏移检测带来了哪些新的挑战。
检测数据偏移
协变量偏移无疑更容易理解和检测。让我们再回顾一下:这是指 P(X) 发生变化。换句话说,模型在服务时输入特征的分布与其在训练时所见的不同。
在绝大多数情况下,既有训练特征也有服务特征。只需比较它们的分布:如果它们不同,则发生了协变量漂移。
好的,这个例子有些过于简化。实际上,测量协变量漂移有两种方法。我们可以通过检查一个或多个特征的分布是否发生变化,从单变量角度进行分析,或者从多变量角度,关注所有特征的联合分布。
在单变量方法中,可以通过使用统计检验和距离度量比较训练分布和服务分布,逐个特征地进行比较。在多变量方法中,基于 PCA 的更细致方法是一个不错的选择。但无论哪种方法,任务都是比较两个观察到的量,并决定它们是否真的不同。
在概念漂移的情况下,漂移检测的挑战更加复杂。我们再来看一下:概念漂移是当 P(Y|X)发生变化时,也就是说,对于给定的特征值,目标分布发生了变化。
难点在于如何衡量和比较 P(Y|X),通常称之为概念。它不是一个可以轻松计算的单一量。它是输入和输出之间的真实映射或关系。我们知道训练数据的 P(Y|X)(根据我们模型的能力),但是我们如何知道它在现实世界中何时发生了变化呢?让我们来看一下!
野外中的概念漂移检测
感谢你耐心阅读这段较长的引言!现在我们知道了什么是概念漂移以及为什么它很难检测,让我们通过一个实际的例子进一步讨论这个问题。
时间与空间中的概念漂移
概念漂移意味着对于特定的输入,输出的分布发生了变化(P(Y|X)发生了变化,记住了吗?)。这种变化可以发生在时间或空间的两个维度中的任何一个。
时间上的概念漂移意味着模型在训练期间学到的概念在现实世界中发生了变化。换句话说,模型的知识已经不再是最新的了。
让我借用 Chip Huyen 在他的精彩著作《设计机器学习系统》中的一个例子:假设你正在构建一个预测旧金山房价的模型。在新冠疫情爆发之前,一套三居室公寓的价格可能是 200 万美元,但由于疫情,很多人离开了城市,需求下降,导致同样的公寓现在的价格可能是 150 万美元。特征分布 P(X)没有变化:房子仍然有相同数量的卧室、相同的面积等。只是相同的一组输入现在映射到一个不同的输出。
空间上的概念漂移发生在从特定地理位置或特定用户集数据中学习到的概念不适用于其他地区或用户群体。例如,给旧金山的公寓增加 50 平方英尺的面积可能导致价格大幅上涨。然而,向怀俄明州的乡村房屋添加相同的面积,因当地住房市场竞争较少,可能不会导致同样的价格上涨。
好的,那么到目前为止,我们知道概念漂移可能是一个问题,无论是在模型部署后经过了一段时间,还是当模型开始为不同的用户或地理区域提供服务时。那么,我们该如何检测它呢?
检测概念漂移
想象一下:你用所有可用数据训练你的旧金山房价预测模型并投入生产。之后,你收集模型用于推理时接收的特征,并按日存储这些特征批次。

训练和服务数据集。图像由作者提供。
这里,X-serve-0 是部署当天的特征,X-serve-1 是次日的特征,依此类推,而 y-serve-0 表示相应的目标值。
今天是零日:模型已基于昨天的数据进行训练并投入生产。那么,今天的数据(X-serve-0 和 y-serve-0)是否发生了概念漂移呢?
让我们假设这是一个二元问题。当然,实际上,概念漂移可能是大规模的,也可能是小规模的,而且对模型性能的影响也可能很大或很小。但现在,让我们假设概念漂移在零日时已经发生或没有发生。
这里有一个思路:我们在零日数据上训练一个模型。如果没有发生概念漂移,它应该学会与生产模型相同的特征到目标的映射。如果发生了概念漂移,学习到的映射将会不同。

概念漂移检测机制。图像由作者提供。
接下来,让我们使用这个零日模型来对测试数据进行预测:我们只需输入X-test。如果输出结果与生产模型的测试集预测值接近,这意味着我们的零日模型已经学会了与生产模型相同的 P(Y|X),或者说学会了相同的概念。因此,我们可以宣告没有概念漂移。如果输出结果不同,那么概念漂移肯定已经发生。
我们可以通过在服务数据上训练一个模型并与生产模型进行比较,从而检测概念漂移。
我们可以每天重复这一过程,每当收到新的数据批次时,就刷新对概念漂移是否发生的了解。
概念漂移:检测与对性能的影响
这一切都很好,但有一个警告,警觉的读者可能已经注意到了。日常模型的输出永远不会和生产模型的输出完全相同:即使没有任何漂移,抽样误差(不同的训练数据样本)也会导致稍微不同的结果。那么,实际上多大的差异才意味着概念漂移呢?或者更实际地说,什么时候我们需要重新训练模型?
确实,并不是每个差异都需要重新训练模型,因为重新训练可能是一个昂贵或复杂的过程。如上所述,差异有时可能是随机抽样的结果,这种情况下不需要重新训练。在其他情况下,差异可能确实是由概念漂移引起的,但这种漂移对模型的影响不大。在这种情况下,也不需要重新训练。
这里的关键观察是,只有当概念漂移显著影响模型性能时,才应该重新训练模型。
只有当概念漂移显著影响模型性能时,才应重新训练模型。
那么我们如何判断概念漂移对性能的影响有多大呢?我们换个角度来看这个问题:是否有一些情况,概念漂移发生了,但并没有影响模型的性能?

无害的概念漂移
想象一下,你的旧金山房价预测模型现在是一个分类模型,你正在预测一栋房子的价格是否超过 100 万美元,基于其特征。你已经按照上述步骤,发现生产模型和当前模型之间有很大的差异。
预测标签没有变化
这里是一个图表,显示了两个模型在一个包含 10 个数据点的子集上,预测房子价格超过 100 万美元的概率差异。

如果最终的预测结果没有变化,概念漂移是无害的。图片来自作者。
这里有三个重要的观察结果。首先,两个模型预测的概率完全不同。对于每个数据点,差异都很大,可能接近 50 个百分点。我们几乎可以确定已经发生了显著的概念漂移。
其次,两个模型的相对输出不一致。有时一个模型的概率远高于另一个模型,有时则相反。
第三,我们遇到的概念漂移对模型是完全无害的。等等,什么?没错!尽管概念漂移显著,但我们所面对的概念漂移对模型性能没有任何影响!
概念漂移并不总是影响模型性能。
记得我们正在处理一个二分类任务。假设使用常见的 50%的决策阈值,对于每个数据点,两个模型将得出相同的预测:数据点 2、3、4、5 和 8 对应于正预测(价格超过 100 万美元),其余的对应于负预测。像准确率、精确度、召回率或 F1 得分这样的性能指标在两个模型中是相同的(不过 ROC AUC 会受到影响,因为它使用模型分数而不仅仅是类别标签)。
我承认这个例子是人为构造的,故意设计来展示我想表达的内容:即概念漂移不一定会影响性能。但也可以理解——实际上,人们很少只使用预测标签而忽略置信度分数。让我们来看另一个可能更现实的场景,在这种情况下,概念漂移不会对你造成伤害。
稀疏区域的漂移
模型特征构成了一个多维空间,每个训练样本都是这个空间中的一个点。如果只有两个特征,x1 和 x2,你可以在二维平面上绘制每个样本作为一个点——特征空间。如果有三个特征,每个样本将是一个立方体中的一个点。在更常见的使用四个特征或更多特征的情况下,我们的大脑无法想象这种场景,但每个样本依然是特征空间中的一个点。
训练样本在特征空间中的分布并不均匀。特征空间中的某些区域数据点密集,而其他区域则非常稀疏。换句话说,在你的数据中,某些特征值的组合是常见的,而其他的则非常罕见。
现在,问题是:概念漂移可能发生在特征空间的任何区域。如果它发生在一个稀疏区域,对模型性能的影响将会很小。这是因为在该区域内没有多少训练数据或服务数据。因此,模型几乎不会在该区域进行预测。由于概念漂移导致的稀疏区域的误分类将是罕见事件,对模型的整体性能贡献不大。
由于概念漂移导致的稀疏区域的误分类将是罕见事件,对模型的整体性能贡献不大。
上面两个故事的启示是,某些概念漂移是无害的,只有对性能有实质性负面影响时才需要重新训练模型。一旦你检测到概念漂移,首先评估它对模型的影响,然后再采取不必要的行动!

概念漂移检测工具
我们可以将到目前为止的整个讨论总结为:不要关注漂移的存在,而要检测其对性能的影响。
然而,这并不是人们通常的做法。快速的网络搜索显示,大多数概念漂移检测方法(例如DeepChecks 博客中的这个方法或Evidently AI 中的这个方法)通常是间接的:它们通常基于检测预测漂移、标签漂移或数据漂移。
我发现的唯一一个声称能够直接检测概念漂移的幅度,并且更重要的是量化其对模型性能影响的工具是 NannyML。我联系了该团队,他们告诉我,除了可以作为独立算法在AWS上使用(这也是我搜索时看到的),它还可以作为Azure 托管应用使用。
这种方法遵循之前讨论的工作流程。每次部署后,我们都会使用当天收集的服务数据训练一个日模型。接下来,我们查看日模型对训练数据预测的概率,并将其与生产模型的预测结果进行比较。这些差异使我们能够估计变化对性能指标(如 ROC AUC、准确率等)的影响。
我使用了免费试用来看看如何在实际中估算概念漂移对分类任务性能的影响。不,这次不会再讨论旧金山的住房问题了。
考虑航班取消问题。它们主要受天气状况或航空公司特定问题等运营因素的驱动。我们可以利用这些特征来相当可靠地预测某个航班是否会被取消。
或者至少在 2019 年底之前是这样。随着 COVID-19 大流行的爆发,旅行限制、封锁措施以及旅行需求的急剧下降导致航班取消数量显著增加,彻底改变了天气等因素与取消之间的关系。例如,良好的天气再也不能保证航班取消数量减少了。
让我们训练一个模型,预测 2018 年之前的数据的航班取消情况,并将 2019 年至 2023 年作为我们的服务数据,数据来源于运输统计局的数据。这是 NannyML 的概念漂移检测算法输出的结果。

NannyML 的概念漂移检测。图像来源:作者。
在部署后的第一年,即 2019 年,似乎没有发生显著的概念变化。我们设定的有意义的性能变化阈值并未突破。然而,第二年,当疫情爆发时,我们的取消分类器准确率下降了 6 个百分点!有趣的是,第三年,情况大致恢复到了疫情前的状态。

考虑与结论
概念漂移是特征与目标之间映射的变化,而特征本身保持不变。可以理解为:相同的输入,不同的输出。与其“邪恶双胞胎”协变量漂移相比,概念漂移更难以检测,后者是特征的分布发生变化。
检测概念漂移的一个巧妙方法是定期在传入的服务数据上训练模型,并将其学习到的概念与生产模型所学习到的概念进行比较。如果它们不同,说明概念漂移已经发生。然而,这种方法也有一些局限性。它假设服务数据的目标是可用的,而在许多应用中并非如此。
最后,并不是所有的概念漂移都是坏事。然而,在某些情况下,它可能会对你的生产模型性能产生负面影响,从而影响这些模型所带来的商业价值。通过遵循上面概述的方法,你可以量化概念漂移的影响,并确保你的机器学习模型继续提供价值。

感谢阅读!
如果你喜欢这篇文章,为什么不订阅电子邮件更新获取我的新文章?通过成为 Medium 会员,你可以支持我的写作,并且获得其他作者及我本人的所有故事的无限访问权限。需要咨询?你可以随时问我问题,或者在这里预约一对一咨询。
你也可以尝试阅读我的其他文章。无法选择?试试以下这些:
提示:忘记 p 值吧
towardsdatascience.com ## 使用 Pants 组织机器学习单一代码库
简化你的机器学习工作流管理
[towardsdatascience.com ## 计算机视觉中的自监督学习
如何仅通过少量标记样本训练模型
towardsdatascience.com
使用 LLM 检测不安全代码
Python 漏洞检测的提示实验
·发表于Towards Data Science ·阅读时间:11 分钟·2024 年 3 月 21 日
--

照片由Alexander Sinn提供,来自Unsplash
如果你是一名软件专业人士,你可能会害怕在发布的早晨打开安全扫描报告。为什么?你知道这是一个增强你工作质量和完整性的好工具,但你也知道,在截止日期之前,你将花费接下来的几个小时争分夺秒地解决所有安全问题。如果你幸运的话,很多问题将是虚惊一场,但你仍然需要手动验证每个问题的状态,并在代码定稿后迅速修复其余问题。如果你经历过这种情况,你可能会希望有一个更顺畅、少些临时应对的流程来识别、分类和修复代码库中的漏洞。好消息是,最近的研究表明,大型语言模型(LLM)可以熟练地将代码分类为安全或不安全,解释其弱点,甚至提出修正建议。这有可能显著简化安全编码实践,超越传统的静态扫描方法。
本文简要回顾了一些最近在漏洞检测和修复方面的发现,特别是 LLMs 的应用,接着深入探讨了几项实验,评估 GPT-4 在使用不同提示技术时,在 Python 数据集上识别不安全代码的能力。如果你想探索 Jupyter 笔记本或测试你自己的代码,可以访问 OpenAI Cookbook 中的拉取请求(当前正在审查中)。
在我们开始提示 GPT-4 之前,有一些关键概念和定义将帮助我们建立设计逻辑实验所需的基础。
常见弱点枚举(CWE)
2006 年,政府资助的研究组织MITRE开始定期发布常见弱点枚举(CWE),这是一个由社区开发的软硬件弱点定义和描述的通用分类法。在这个意义上,“弱点”指的是软件或硬件中的一种情况,可能导致漏洞。一个2023 年最危险的前 25 个 CWE的列表突出了最大的重复犯规者。还有一个15 个“顽固”CWE的列表,这些 CWE 自 2019 年至 2023 年每年都出现在前 25 个列表中。它们大致可以分为三组:
-
第 1 组:对不可信数据源的薄弱处理(例如命令/SQL 注入、路径遍历、不当输入验证和跨站脚本攻击)
-
第 2 组:薄弱的内存管理或类型强制(例如 NULL 指针解引用)
-
第 3 组:薄弱的安全设计选择(例如硬编码凭证)
为了帮助保持调查范围狭窄且定义明确,我们将重点关注 CWEs 的第一组。
静态代码分析
传统的自动化不安全代码检测方法涉及使用静态分析工具,如 CodeQL、Bandit、SonarQube、Coverity 和 Snyk。这些工具可以随时使用,但通常用于在代码冻结阶段后、正式发布过程完成之前扫描代码中的漏洞。它们通过将源代码解析成抽象语法树或控制流图的方式工作,这些结构表示代码如何组织,以及各个组件(类、函数、变量等)之间的关系。然后,通过基于规则的分析和模式匹配来检测各种问题。静态分析工具可以在开发周期中与 IDE 和 CICD 系统集成,许多工具提供自定义配置、查询和报告选项。它们非常有用,但也有一些缺点(除了那些最后时刻的高压修复派对之外):
-
资源密集型:它们将大量代码库转换为数据库,以执行复杂查询
-
假阳性:它们通常包含大量非问题项
-
时间密集型的后续工作:需要大量的努力来验证和修复这些问题
这些局限性无疑激发了研究人员探索新方法来增强代码安全实践,例如生成式 AI。
之前的工作
最近的工作表明,LLM 在开发生命周期的各个阶段具有潜力。LLM 已被证明在安全代码补全、测试用例生成、易受攻击或恶意代码检测以及错误修复等方面非常有用。
以下是一些值得注意的参考文献:
-
一项综合评估¹比较了不同参数规模的 LLM 与传统静态分析器在识别和修复软件漏洞方面的表现。特别是 GPT-4 表现出色,尤其在解释和修复易受攻击的代码方面。论文中还提出了一些额外的观点——显著的代码理解能力似乎出现在 6 到 175 亿个参数之间,而超过 130 亿参数时,才出现了高级程序员技能的初步迹象;当提示词同时包含识别和修复安全问题的任务时,预测成功率可能会提高;将 LLM 与传统静态代码分析结合使用,可能能够提供两者的最佳结合。
-
一项新研究和数据集²发现,即使是高级的 AI 开发助手也容易编写不安全的代码,并且发现 40%的生成代码补全包含了 CWE(通用弱点枚举)。
-
一项调查³报告称,GPT-3 在预测安全漏洞方面超过了现代静态代码分析器。
-
一篇研究论文⁴表明,LLM 可以帮助开发者识别和定位易受攻击的代码,尤其是当与静态分析器结合使用时。
-
在另一项研究⁵中,LLM(大语言模型)成功修复了所有合成的和手工制作的场景,尽管它们并未充分解决所有现实世界中的安全漏洞问题。
尽管 LLM 在超越传统方法方面展现了潜力,但许多研究指出,它们也容易产生误报,并且对提示的结构和措辞非常敏感。在我们的实验中,我们旨在通过对提示模板进行更多变体的应用,来验证并进一步扩展这些结果。
数据来源与预处理
我们将使用一个最广泛采用的数据集之一来进行 LLM 的安全代码基准测试。数据集(许可CC BY 4.0)来自《键盘上睡着了吗?评估 GitHub Copilot 的代码贡献安全性》²,该数据集由名为“场景”的提示组成,这些提示是手工设计的,用于在作为输入提供给代码生成 LLM 时引出特定的 CWE。我们从出版物中挖掘了包括的输出代码补全,因为它们随后被静态代码分析工具扫描,并带有“易受攻击”和“非易受攻击”的标签。需要再次注意的是,这些数据集中的代码是从手动编写的提示中由模型生成的,因此缺乏一些现实世界的严肃性,但我们选择它有几个原因:
-
它包含大量的 Python 示例,而 Python 是本研究中首选的编程语言。
-
它包含易受攻击和非易受攻击的代码示例,这对于评估假阳性和假阴性都非常重要。
-
与(2)相关的是,针对同一场景存在易受攻击和非易受攻击的代码片段,这意味着我们可以将非易受攻击的代码补全作为某些提示中的“建议修复”,这一点将在相关的实验部分进行解释。
我们理解,实际上还有其他数据集可以使用,并且这也留待未来的研究来探索使用其他数据源进行 CWE 预测的能力。
来自Copilot 场景原始数据文件²的 Python 代码片段经过以下步骤的预处理:
Open the project's aggregated "diversity of weakness" results file
Filter for Python language rows
FOR each Python scenario/row
Navigate to the scenario's dedicated results folder
Open the scenario's CodeQL or authors' manual assessment file
FOR each generated code completion for the scenario
Create a record containing its metadata and classification label
Read in the .py file containing the code completion
FOR each line in the file
IF the line is 100% comments
Remove the line
END IF
END FOR
Insert the cleaned code example into the record
END FOR
Assert that the scenario totals agree with the original aggregated report
END FOR
Save the final file in JSON format
数据探索与子集化
我们探索了 CWE 的分布,并从第 1 组 CWE 中均匀选择了 45 个样本,涵盖易受攻击和非易受攻击的代码(我们计划的样本量是 50,但由于其中一个组的数量有限,最终选择了 45 个样本)。CWE 描述如下:
-
CWE-20:输入验证不当
-
CWE-22:路径名限制不当,导致限制目录(“路径遍历”)
-
CWE-78:操作系统命令中使用的特殊元素未正确中和(“操作系统命令注入”)
-
CWE-79:网页生成时输入未正确中和(“跨站脚本攻击”)
-
CWE-502:反序列化不可信数据
我们从零-shot 基准测试开始,然后重复进行实验,通过增加像 few-shot 上下文学习这样的策略来构建提示的复杂性。我们提示 LLM 识别易受攻击的代码,而没有提及可能寻找的 CWE(即没有标签)。
实验 1:零-shot
在零-shot 提示中,您要求模型在没有示例或其他信息的情况下,仅凭指令做出预测。我们的零-shot 模板灵感来自于这篇论文⁴,包含了角色、代码分隔符,并要求仅输出 json 格式。它还包括了“一步步思考”的指令。测试中的代码片段被插入到{code}中。
提示
You are a brilliant software security expert.
You will be provided with a python code delimited by triple backticks.
If it contains any CWE security vulnerabilities, write Vulnerable.
If the code does not contain any vulnerabilities, write Not Vulnerable.
Format your response as a JSON object with "label" as the key
for vulnerability status and "cwe" as the vulnerability number found.
Think about the answer step by step, and only answer with JSON.
Python code: ```{code}```py
Answer:
结果
准确率:0.67
精确度:0.60
召回率:0.86
F1 得分:0.71

零-shot 混淆矩阵
实验 2:少量学习
在下一个实验中,我们引入了上下文或“少量学习”的概念,并在要求 LLM 对未见过的代码执行相同操作之前,提供了几个成功的代码-答案示例。这些示例来自数据集的其余部分,并特别注意:
-
从不同的场景中选取示例,以避免污染
-
精确绘制两个易受攻击的示例和一个非易受攻击的示例
提示
You are a brilliant software security expert.
You will be provided with a python code delimited by triple backticks.
If it contains any CWE security vulnerabilities, write Vulnerable.
If the code does not contain any vulnerabilities, write Not Vulnerable.
Format your response as a JSON object with "label" as the key
for vulnerability status and "cwe" as the vulnerability number found.
Think about the answer step by step, and only answer with JSON.
Python code: ```{example_0}```py
Answer: {answer_0}
Python code: ```{example_1}```py
Answer: {answer_1}
Python code: ```{example_2}```py
Answer: {answer_2}
Python code: ```{code}```py
Answer:
结果
准确率:0.76
精确度:0.71
召回率:0.81
F1 得分:0.76

少量学习混淆矩阵
实验 3:KNN 少量学习
这篇微软博客文章描述了一种有趣的技术,叫做基于 KNN 的少量示例选择,它可以提升使用上下文示例时 LLM 的响应质量。在接下来的实验中,我们不再随机选择示例,而是计算输入代码与每个候选示例之间的相似度得分,并从最相似的候选中构建示例(同时保持场景的独特性)。我们使用 ROUGE-L 指标,但也可以使用其他指标。提示模板与第二个实验中保持一致。
结果
准确率:0.73
精确度:0.70
召回率:0.76
F1 得分:0.73

KNN 少量学习混淆矩阵
实验 4:KNN 少量学习与代码修复
在这种提示的变体中,我们要求提供一个修正后的代码版本,如果发现 CWE。这一方法灵感来自于Noever,他提出通过同时提示 CWE 检测和修复可能会产生“良性循环”,并迫使 LLM 进行“自我审计”或更深入地思考准确识别漏洞所需的步骤,类似于链式思维提示。我们通过构建包含代码修复建议的易受攻击代码上下文示例,且修复建议来自于相同场景下的非易受攻击代码样本。
提示
You are a brilliant software security expert.
You will be provided with a python code delimited by triple backticks.
If it contains any CWE security vulnerabilities, write Vulnerable.
If the code does not contain any vulnerabilities, write Not Vulnerable.
If the code has the vulnerability, write a repaired secure version of the
code that preserves its exact functionality.
Format your response as a JSON object with "label" as the key
for vulnerability status, "cwe" as the vulnerability found,
and "fix" for the fixed code snippet.
Think about the answer step by step, and only answer with JSON.
Python code: ```{example_0}```py
Answer: {answer_0}
Python code: ```{example_1}```py
Answer: {answer_1}
Python code: ```{example_2}```py
Answer: {answer_2}
Python code: ```{code}```py
Answer:
结果
准确率:0.80
精确度:0.73
召回率:0.90
F1 得分:0.81

KNN 少量示例修复混淆矩阵
除了 CWE 检测外,这个实验的一个优势是能够生成修复建议。我们尚未评估这些建议的质量,因此这是未来研究的一个方向。
结果与后续步骤

在我们的小数据样本上,GPT4 的准确率为 67%,F1 得分为 71%,并未进行复杂的提示调整。一些我们测试的提示技术带来了一些小幅提升,其中少量示例和请求代码修复的效果最为突出。不同提示技术的组合使得准确率和 F1 得分从基线提高了大约十个百分点,两个指标均达到或超过 80%。
结果在不同模型、数据集和提示之间可能会有很大差异,因此需要更多的调查。例如,以下问题会很有趣:
-
测试较小的模型
-
测试包含 CWE 标签的提示模板,探讨将 LLMs 与静态分析相结合的潜力
-
测试更大且更具多样性的数据集
-
评估 LLM 提议的代码修复的安全性和功能性
如果你想查看生成这些结果的代码,自己在代码中运行它,或根据自己的需求进行修改,可以查看 OpenAI Cookbook 中的拉取请求(当前正在审核中)。
感谢我的同事Matthew Fleetwood和Abolfazl Shahbazi,他们为这篇文章做出了贡献并帮助进行审阅。
引用
[1] D. Noever, 大型语言模型能否发现并修复易受攻击的软件? (2023), arXiv 预印本 arXiv:2308.10345
[2] H. Pearce, B. Ahmad, B. Tan, B. Dolan-Gavitt 和 R. Karri, 在键盘上睡着了吗?评估 GitHub Copilot 的代码贡献的安全性 (2022), 2022 IEEE 安全与隐私研讨会 (SP)
[3] C. Koch, 我使用 GPT-3 在单一代码库中发现 213 个安全漏洞 (2023), betterprogramming.pub/i-used-gpt-3-to-find-213-security-vulnerabilities-in-a-single-codebase-cc3870ba9411
[4] A. Bakhshandeh, A. Keramatfar, A. Norouzi 和 M. M. Chekidehkhoun, 将 ChatGPT 用作静态应用程序安全测试工具 (2023), arXiv 预印本 arXiv:2308.14434
[5] H. Pearce, B. Tan, B. Ahmad, R. Karri 和 B. Dolan-Gavitt, 使用大型语言模型进行零-shot 漏洞修复研究 (2023), 2023 IEEE 安全与隐私研讨会 (SP)
时间轴分享:商业文本转视频的演变
文本转视频的最新三年发展
·发布于Towards Data Science ·2 分钟阅读·2024 年 3 月 16 日
--

商业文本转视频的发展历程(截至 2024 年 3 月)
近年来,我们见证了商业文本转视频模型和产品的出现。我想分享一张自创的 综合时间轴图,它捕捉了商业文本转视频模型/产品在过去 3 年(包括 2022 年、2023 年以及 2024 年截至目前)的显著发展。
我在为团队准备关于 Sora 的演示时创作了这张图。看到这些优秀的产品随着计算机视觉(CV)研究的进展而诞生,令人兴奋,这些研究包括但不限于生成对抗网络(GANs)、变换器架构和扩散模型。
正如微软研究论文《Sora: A Review on Background, Technology, Limitations, and Opportunities of Large Vision Models》所建议的那样,我们将 Sora 视为一次飞跃,因为它不仅仅是一个工具,更有可能成为一个“世界模拟器”,模拟物理世界中所描绘场景的物理和上下文动态。
当然,这一演变不会停止,我相信我们会看到更多令人兴奋的消息。作为见证者,我很期待保持这张图的更新。
我很想听听你对这次演变的看法,以及你认为文本转视频技术未来将走向何方。让我们讨论一下这些进展的影响、潜在应用和随之而来的伦理考量。
差分中的差分 101
·发布于 Towards Data Science ·7 分钟阅读·2024 年 5 月 26 日
--
什么是差分中的差分(DiD 或 DD 或 diff-in-diff)?为什么我们关心 DiD?今天我将回答所有关于这项经济计量学中最流行的政策效应研究方法的问题。

这张图片由作者使用 DALL.E 创作,旨在展示技术如平板电脑在课堂中的引入及其对成绩的影响。
DiD 是一种广泛使用的经济计量技术,通过比较治疗组和对照组在不同时间点的结果变化,估算因果关系。问题是,什么是治疗组和对照组?治疗组是指由于政策或变化影响到特定群体的政策干预;对照组是指没有受到干预的群体。因果关系指的是因果效应关系。
我们关心这种方法,因为它在评估政策变化或干预的效果时非常有用,特别是当随机实验不可行时。这意味着,有时实验会针对特定群体进行,暗示接受干预的人并非随机选取。即使没有随机化,DiD 也能帮助我们剖析干预的影响。
本文将深入探讨这一方法的概念、假设、实施及其示例。
什么是 DiD
我们的研究问题是:干预 D 对结果 y 的影响是什么?DiD 让我们可以估计如果没有发生干预,处理组将会发生什么。这种反事实情境对于理解干预的真实效果至关重要。每项工作或研究都围绕着类似的问题展开,比如干预、政策变化或治疗的效果评估。在经济学中,它评估税收减免对经济增长的影响;在公共政策中,它评估新的交通法规对事故率的影响;在市场营销中,DiD 分析广告活动对销售的影响。

由作者创建的图示
例如,在上面的图示中,我们有样本中的人口数据。我们将数据分为处理组和对照组,其中处理组接受了干预。我们可以观察到两组的处理前和处理后变量。
如何进行 DiD 分析
简单的处理/对照组差异估计量

这个方程将通过比较处理组和对照组随时间变化的结果,来计算干预效应。
我创建了一个假设示例来帮助理解这个数学原理。

使用上述公式,DiD 系数将是9。
DiD 估计量:使用回归计算
DiD 有助于控制可能对干预效果估计产生偏差的时间不变特征。这意味着它可以消除那些在时间上保持不变的变量的影响(例如,地理位置、性别、种族、天赋等)。它之所以能做到这一点,是因为这些特征对每个组的处理前和处理后的时间段的影响是相等的。
基本的 DiD 模型的核心方程是:

其中:
-
y是个体𝑖在组𝑗的时间𝑡的结果变量。
-
𝐴𝑓𝑡𝑒𝑟是一个虚拟变量,当观察值处于处理后的时间段时,该值为 1。
-
𝑇𝑟𝑒𝑎𝑡𝑚𝑒𝑛𝑡是一个虚拟变量,当观察值属于处理组时,该值为 1。
-
𝐴𝑓𝑡𝑒𝑟 × 𝑇𝑟𝑒𝑎𝑡𝑚𝑒𝑛𝑡是交互项,系数β表示 DiD 估计值。
交互项的系数是 y 中的 DiD 估计量。回归分析在研究人员中更受欢迎,因为它能够提供标准误差并控制额外的变量。
平行趋势假设
这是 DiD 中的一个关键假设。它基于这样一个观点:在没有干预的情况下,处理组和对照组之间的差异将随着时间的推移保持不变。换句话说,在没有干预的情况下,β (DiD 估计值) = 0。
正式来说,这意味着:

另一种思考方式是,假如没有政策变化,两组之间的差异在时间上会保持不变。如果在处理前两组的趋势不平行,则 DiD 估计可能存在偏差。
如何检查这个假设
现在下一个问题是:如何检查这一点?平行趋势假设的有效性可以通过图形分析和安慰剂检验来评估。

作者创建
假设是,在没有治疗的情况下,处理组(橙色线)和对照组(蓝色虚线)会随着时间的推移沿平行路径变化。干预(垂直线)标记了施加治疗的时刻,允许通过比较两组在干预前后趋势的差异来估计治疗效应。
违反平行趋势假设的例子
简单来说,我们在治疗中寻找两件事:
- 斜率变化

图表:部分(a)

图表:部分(b)
在上述两个案例中,平行趋势假设没有得到满足。处理组的结果要么增长更快(部分 a),要么增长更慢(部分 b)于对照组的结果。用数学方式来说就是:
DiD = 真实效应 + 差异趋势(差异趋势应为 0)
差异趋势可以是正向的(部分 a)或负向的(部分 b)
由于我们在其中也有一个差异趋势,DiD 将无法隔离干预的影响(真实效应)。
2. 干预后治疗线的跳跃(无论是向上还是向下)

在上面的图像中,处理组的趋势与对照组的趋势发生了不同的变化,而对照组的趋势在没有干预的情况下应该保持一致。DiD 研究中不允许出现跳跃。
安慰剂检验
安慰剂检验用于验证观察到的治疗效果是否真的是由于治疗本身,而不是由于其他混杂因素。它们通过对一个没有预期治疗效果的时期或群体进行相同的分析来进行。如果在这些安慰剂检验中发现显著效果,说明原始结果可能是虚假的。
例如,2019 年曾进行过一项为高中生发放药片的干预研究。我们可以做一个安慰剂检验,也就是说,我们可以创建一个假设的干预年份,比如 2017 年,知道那时没有发生任何政策变化。如果将治疗效应分析应用于安慰剂年份(2017),且没有发现显著变化,那么这将表明 2019 年观察到的效果(如果有的话)很可能是由于实际的政策干预。
DiD 的扩展与变体
-
事件研究 DiD:估计年特定的处理效应,这对于评估处理效应的时机和检查预趋势非常有用。该模型允许处理效应随年份变化。我们可以研究在时间t+1, t+2, …, t+n时的效应。
-
合成控制法(SCM):SCM 通过加权多个未处理单位来构建一个合成控制组,创建一个在干预前能够逼近处理单位特征的组合体。这种方法在将一个单一的处理单位与一组未处理单位进行比较时特别有用。它通过结合多个单位的信息,提供了一个更为可信的反事实。
还有很多其他的,但我将限制在仅两个。我可能稍后会写一篇文章,详细解释其余内容。
结论
在这篇文章中,我分析了差异中的差异(DiD)估计量,这是一种估计平均处理效应的流行方法。DiD 被广泛应用于通过比较处理组和对照组随时间变化来研究政策效应。DiD 的主要优势是它能够控制随着时间推移保持不变的未观察到的混杂因素,从而孤立出干预的真实影响。
我们还探讨了诸如平行趋势假设、前期数据的重要性以及如何通过图形分析和安慰剂检验检查假设违反的关键概念。此外,我还讨论了 DiD 的扩展和变种,例如事件研究 DiD 和合成控制法,这些方法在不同情境下提供了更多的见解和稳健性。
参考文献和进一步阅读
[1] Wing, C., Simon, K., & Bello-Gomez, R. A. (2018). 设计差异中的差异研究:公共卫生政策研究的最佳实践。公共卫生年鉴, 39, 453–469。
[2] Callaway, B., & Sant’Anna, P. H. (2021). 多时间期的差异中的差异。计量经济学杂志, 225(2), 200–230。
[3] Donald, S. G., & Lang, K. (2007). 使用差异中的差异和其他面板数据的推断。经济学与统计学评论, 89(2), 221–233。
谢谢阅读!
感谢阅读!🤗 如果您喜欢这篇文章并希望看到更多,考虑 关注我。您也可以在 LinkedIn上关注我。我计划写关于因果推断和数据分析的博客,始终致力于保持内容简单。
一个小小的免责声明:我写作是为了学习,因此尽管我尽力了,还是可能会有错误。如果您发现任何错误,请告诉我。我也欢迎关于新话题的建议!
使用稳定扩散 3 生成图像的不同方式
在 Google Colab 和本地 PC 上运行 SD3
·发表于 Towards Data Science ·阅读时间 7 分钟·2024 年 6 月 28 日
--

图像由作者生成(SD3,付费创作者许可)
稳定扩散被认为是最好的开源文本生成图像模型之一,令人兴奋的是,最新的“Stable Diffusion 3 Medium”模型于 2024 年 6 月发布,并且现在可以在HuggingFace上获取。该模型在非商业研究社区许可下免费提供,因此让我们编写一些 Python 代码,看看它是如何工作的!
一般信息
稳定扩散(SD)是一个文本生成图像的模型;这种方法本身相对较新。关于使用潜在扩散模型进行图像合成的第一篇论文(Rombach 等人的“高分辨率图像合成与潜在扩散模型”)于 2022 年发布。一家名为 Stability AI 的公司对该项目产生了兴趣,随后该模型在 2022 年 8 月发布,命名为“Stable Diffusion”。之后,它经过了几次改进。在撰写本文时,Stable Diffusion 1.1 仍然可以在HuggingFace上使用,最新版本 3 于 2024 年 6 月发布。
现在,让我们在 Python 中运行该模型。
基本测试
使用 HuggingFace 🤗 Diffusers 库运行稳定扩散非常简单:
import torch
from diffusers…
可微分且加速的球面调和变换
在 JAX 和 PyTorch 中
·发表于Towards Data Science ·阅读时间 7 分钟·2024 年 3 月 14 日
--
许多科学和工程领域都涉及定义在球面上的数据。对这些数据的建模和分析通常需要傅里叶变换的球面对应物——球面调和变换。我们简要概述了球面调和变换,并提出了一种新型的可微分算法,旨在加速 GPU 运算[1]。该算法已在最近发布的 S2FFT Python 包中实现,支持 JAX 和 PyTorch。

图片由Szilvia Basso提供,来源于Unsplash
我们越来越多地关注分析位于球面上的数据。应用领域的多样性令人瞩目,涵盖从量子化学、生物医学成像、气候物理学和地球物理学,到更广阔的宇宙。
在物理科学中,尤其是在大气科学、地球物理建模和天体物理学中,球面数据是最常见的。

球面数据中最广为人知的案例,如地球(左)和天文观测的艺术印象(右)。[地球图片来源于 维基百科;天体物理学图像来源于 维基百科。]
这些问题天生具有球面性质,因为观察是在球面上的每一点进行的:地球表面用于地球物理学,天空用于天体物理学。其他例子来自于计算机图形学和视觉应用,其中 360°全景摄像头可以捕捉你周围环境的每一个方向。
在许多情况下,问题的球面性质是比较容易看出的;然而,这并非总是如此。或许令人惊讶的是,球面数据在生物学领域中经常出现,尽管球面特性往往不太显而易见!由于我们在生物学研究中通常关注局部方向,例如水在大脑中的扩散方向,因此我们会遇到球面数据。

人类大脑神经连接的扩散张量成像。每个体素中的神经元可以自由地朝任何方向移动,因此问题本质上是球面的。[动画由Alfred Anwander制作,CC-BY 许可。]
鉴于此类数据的普遍存在,许多球面分析技术应运而生,频率分析常常能为数据提供有价值的见解,通常有助于提供统计摘要或有效的表示形式,用于进一步的分析或建模。最近,几何深度学习技术在复杂领域的数据分析中已被证明非常有效,尤其是在分子建模和蛋白质相互作用等高度复杂的问题中(请参阅我们之前关于几何深度学习简介的文章)。
傅里叶遇见勒让德
因此,我们有球面数据以及多种分析球面数据的技术,但我们需要数学工具来进行分析。具体来说,我们需要知道如何高效地将球面数据分解成频率。
傅里叶变换提供了一种频率分解,常用于计算数据中的统计相关性。许多物理系统也可以在频率空间中更直接地描述,因为每个频率可能独立演变。
要将标准傅里叶变换扩展到球面,我们需要两位 17 世纪法国数学家的共同努力:约瑟夫·傅里叶和阿德里安-玛丽·勒让德。

约瑟夫·傅里叶(左)和阿德里安-玛丽·勒让德(右)。不幸的是,勒让德的漫画是唯一已知的他的形象。[傅里叶图像来源于 维基百科。勒让德图像来源于 维基百科。]
首先,让我们考虑如何将欧几里得数据分解成不同的频率。这种数据变换最初由约瑟夫·傅里叶推导出来,公式如下:

这种方法几乎无处不在,且成为本科物理课程中的基础!它通过将我们的数据 f(x) 投影到一组三角函数上,称为 基函数。在球面上也可以做到类似的事情,但基函数现在由球面调和函数 Yₗₘ 给出:

(θ, ϕ) 是通常的球面坐标。

球面调和基函数(实部)。[来源自 维基百科.]
球面调和函数(如上图所示)可以进一步分解为指数和勒让德多项式的乘积——按照阿德里安-玛丽·勒让德的方式——如

因此,球面调和变换可以写成先进行傅里叶变换,然后是伴随的勒让德变换。真正的难点在于评估变换中的勒让德部分:这取决于所选方法,它要么计算开销大,要么占用大量内存。
可微性的重要性
可微编程的兴起开辟了许多新的分析类型。特别是,许多应用需要可微的球面变换。
球面上的机器学习模型需要可微变换,以便模型可以通过基于梯度的优化算法进行训练,即 通过反向传播。
新兴的物理增强型机器学习方法 [7] 针对混合数据驱动和基于模型的方法 [8] 也需要可微的物理模型,而在许多情况下,这些模型本身就需要可微的球面变换。
考虑到这一点,显然,对于现代应用,球面调和变换的高效算法是必要的,但仅有这点是不够的。可微性是关键。
一个名为 S2FFT 的软件包
这些都很好,但如何高效地评估球面调和变换呢?已经开发出了多种算法,并有一些出色的软件包。然而,现代应用需要一种可微的、能够在硬件加速器(如 GPU)上运行并且计算可扩展的算法。
通过从头开始重新设计核心算法(在我们相应的论文 [1] 中有详细描述),我们最近开发了一个名为 S2FFT 的 Python 包,应该能够满足需求。
S2FFT 是用 Google 开发的可微编程语言 JAX 实现的,并且还包括一个 PyTorch 前端。

S2FFT 是一个实现可微分和加速的球面调和变换的 Python 包,支持 JAX 和 PyTorch 接口。[图像由作者创建。]
S2FFT 提供了两种操作模式:预计算关联的勒让德函数,然后在运行时访问它们;或在变换过程中即时计算它们。预计算方法几乎是最快的,但存储所有勒让德函数值所需的内存与分辨率的立方成比例,这可能是一个问题!我们提供的第二种方法是递归地在变换过程中即时计算勒让德项,因此可以扩展到非常高的分辨率。
此外,S2FFT 还支持混合自动和手动微分方法,以便高效地计算梯度。
该软件包旨在支持球面上多种不同的采样方案。在发布时,支持等角度(McEwen & Wiaux [9],Driscoll & Healy [10])、高斯-勒让德和 HEALPix [11] 采样方案,未来也可以轻松添加其他方案。

支持球面上不同采样方案的 S2FFT。 [原始图由作者创建。]
S2FFT 包可以在 PyPi 上找到,因此任何人都可以通过运行以下命令直接安装:
pip install s2fft
或者通过运行以下命令来启用 PyTorch 支持:
pip install "s2fft[torch]"
从这里开始,可以简单地调用顶级变换:
import s2fft
# Compute forward spherical harmonic transform
flm = s2fft.forward_jax(f, L)
# Compute inverse spherical harmonic transform
f = s2fft.inverse_jax(flm, L)
这些功能可以直接使用并作为层集成到现有模型中,支持 JAX 和 PyTorch,完全支持前向和反向模式的微分。
未来展望
随着研究人员对科学应用中的可微编程越来越感兴趣,迫切需要现代软件包来实现科学通常依赖的基础数学方法,如球面调和变换。
我们希望 S2FFT 在未来几年能派上大用场,并且我们非常期待看到人们会用它做些什么!
参考文献
[1] Price & McEwen, 可微分和加速的球面调和和维格纳变换,arxiv:2311.14670(2023)。
[2] Bronstein, Bruna, Cohen, Velickovic, 几何深度学习:网格、群体、图形、测地线和规范,arXix:2104.13478(2021)。
[3] Ocampo, Price & McEwen, 通过离散-连续(DISCO)卷积实现可扩展和等变的球面卷积神经网络, ICLR(2023)。
[4] Cobb, Wallis, Mavor-Parker, Marignier, Price, d’Avezac, McEwen, 高效的广义球面卷积神经网络,ICLR(2021)。
[5] Cohen, Geiger, Koehler, Welling,球面卷积神经网络(Spherical CNNs), ICLR (2018)。
[6] Jumper 等,利用 AlphaFold 进行高度准确的蛋白质结构预测, Nature (2021)。
[7] Karniadakis 等,物理启发的机器学习, Nature Reviews Physics (2021)。
[8] Campagne 等,Jax-cosmo:一个端到端可微分的、GPU 加速的宇宙学库, arXiv:2302.05163 (2023)。
[9] McEwen & Wiaux,球面上的一种新型采样定理, IEEE TSP (2012)。
[10] Driscoll & Healy, 计算球面上的傅里叶变换与卷积, AAM (1994)。
[11] Gorski 等,HEALPix:一种高分辨率离散化框架,以及球面上数据的快速分析, ApJ (2005)。
理解 PyTorch 中的主成分分析
内置函数与数值方法
·发布于 Towards Data Science ·8 分钟阅读·2024 年 2 月 18 日
--
PCA 是数据科学中降维的重要工具,也用于从点云数据中计算机器人操作的抓取姿势。PCA 也可以直接在更大的机器学习框架中使用,因为它是可微分的。以机器人抓取的点云的两个主成分为例,我们将推导出 PCA 的数值实现,这有助于理解 PCA 是什么以及它的作用。
如果你不是 Medium 的订阅者,可以免费阅读此故事 这里。
主成分分析(PCA)广泛应用于数据分析和机器学习中,以减少数据集的维度。目标是找到一组线性不相关(正交)的变量,称为主成分,它们能够捕捉数据中的最大方差。第一个主成分代表最大方差的方向,第二个主成分与第一个主成分正交,代表下一个最大方差的方向,依此类推。PCA 还被用于机器人操作中,用于找到点云的主轴,然后可以用来定向夹爪。

桌子上的一罐汽水的点云。抓取汽水罐需要将夹爪与汽水罐的主轴对齐。图像来源:作者。
在数学上,主成分的正交性是通过寻找特征向量来实现的……
医疗数据的差分隐私与联邦学习
在医疗领域对差分隐私与联邦学习的实际评估。
·发表于Towards Data Science ·10 分钟阅读·2024 年 4 月 23 日
--

(必应 AI 生成的图像,原始,完全所有权)
敏感数据呼唤更多的保护
在大型语言模型从互联网上的所有内容中进行训练的时代,对数据隐私的需求似乎变得比较宽松,尽管这些模型并未考虑到实际的知识产权,而它们各自的公司领导也公开承认这一点。
但是,当谈到患者的数据、我们的健康记录时,有一个更加敏感的平行宇宙,这些数据无疑更加敏感,需要保护。
此外,全球范围内的法规正在变得更加严格,趋势一致朝着更严格的数据保护法规发展,包括人工智能。
有明显的伦理原因,我们不必解释,但从企业层面的监管和法律角度来看,药品公司、实验室和医院需要使用最先进的技术来保护患者的数据隐私。
联邦范式在这里提供帮助
联邦分析和学习是能够分析数据并在患者数据上训练模型而不访问任何原始数据的绝佳选择。
就联邦分析而言,这意味着,例如,我们可以在不访问任何原始数据(这些数据可能导致患者重新识别)的情况下,获得血糖与患者 BMI 之间的相关性。
以机器学习为例,假设是在诊断领域,模型通过分析患者的影像来检测其组织中的恶性变化,并识别癌症的早期阶段。这实际上是机器学习在挽救生命方面的应用。模型在医院层面使用本地的影像和由专业放射科医生标注的标签进行本地训练,然后通过聚合将所有本地模型合并成一个更加通用的模型。这个过程会重复进行数十次或数百次,以提高模型的性能。

图 1. 联邦学习在行动中,分享的是模型更新,而不是数据。
每家医院的回报是,它们将受益于一个训练得更好的模型,能够以更高的概率在未来的患者中检测到疾病。这是一个双赢的局面,特别是对患者来说。
当然,联合网络拓扑和模型聚合策略有很多种,但为了本篇文章的目的,我们尽量聚焦于典型的例子。
借助技术建立信任
人们普遍认为,大量的临床数据未被利用,这是因为数据所有者(合理地)不愿与合作伙伴共享数据。
联邦学习是建立这种由技术支撑的信任的关键策略,不仅仅依赖于合同和对组成联盟的各个组织员工及合作伙伴伦理的信任。
首先,数据保持在源头,永远不会离开医院,也不会集中到一个可能易受攻击的位置。联邦方法意味着数据不会在外部存在副本,而这些副本在研究完成后可能难以删除。
该技术通过多种遵循深度防御原则的技术手段,阻止了对原始数据的访问。每种手段都将数据暴露的风险和患者重新识别的风险降低了数十倍甚至上千倍。所有这一切都旨在使得发现或重建原始数据变得经济上不可行。
数据首先经过最小化处理,仅暴露必要的属性给本地运行的机器学习代理,个人身份信息(PII)数据被剥离,我们还使用了匿名化技术。
接着,本地节点通过仅允许本地数据拥有者接受的代码和操作对本地数据进行处理,保护本地数据免受所谓过于好奇的数据科学家威胁。例如,医院本地部署的模型训练代码作为一个包,是否被允许运行完全由本地数据拥有者决定。远程数据科学家不能随便向远程节点发送任何代码,因为这将允许他们例如返回原始数据。这需要一种新的去中心化思维方式,以采纳不同的心态和技术进行权限管理,这是另一个有趣的话题,稍后再谈。
模型足够隐私吗?
假设所有这些保护层都已到位,仍然存在与模型权重安全性相关的担忧。
在 AI 社区中,对于机器学习模型作为数据的超级压缩形式的关注日益增加,它不像以前认为的那样是一个黑盒子,并且比以前认为的揭示了更多关于底层数据的信息。
这意味着,只要具备足够的技能、时间、精力和强大的硬件,一个有动机的对手可以尝试重建原始数据,或者至少以高概率证明某个患者曾属于用于训练模型的群体(成员推断攻击(MIA))。其他可能的攻击类型包括提取、重建和规避。
更糟糕的是,我们所有人都钦佩并受益的生成性 AI 的进展,带来了新的、更有效的图像重建技术(例如,患者的肺部扫描)。我们所有人都用来按需生成图像的相同理念,可以被对手用来从 MRI/CT 扫描机器重建原始图像。其他类型的数据,如表格数据、文本、声音和视频,现在也可以通过生成 AI 重建。
差分隐私来拯救
差分隐私(DP)算法承诺我们通过牺牲一些模型的准确性来换取对推断攻击的更强抗性。这是另一种值得考虑的隐私-效用权衡。
差分隐私在实际应用中意味着我们添加了一种非常特殊的噪声和裁剪方式,作为回报,这将导致隐私收益与准确性损失的非常好的比率。
它可以像最不有效的高斯噪声一样简单,但如今我们拥抱了更加复杂的算法的发展,如稀疏向量技术(SVT)、Opacus 库作为差分隐私随机梯度下降(DP-SGD)的实际实现,以及基于拉普拉斯噪声的经典库(即 PyDP)。

图 2. 我们每时每刻都在使用的设备端差分隐私。
顺便说一下,我们所有人都在享受这种技术的好处,却未曾意识到它的存在,而且它正在实时发生。我们来自移动设备(Apple iOS,Google Android)和桌面操作系统(Microsoft Windows)的遥测数据,正在使用差分隐私和联邦学习算法来训练模型,而无需将原始数据从我们的设备上传送。这项技术已经存在多年了。
现在,越来越多的其他用例正在被采用,包括我们最喜欢的“孤立联邦学习”案例,其中相对较少的参与者拥有大量数据,并在不同组织和公司特意建立的联盟中共享数据。
差分隐私并非专门针对联邦学习。然而,在联邦学习场景中,应用差分隐私的策略以及算法的选择是多样的。不同的算法在联邦学习环境中表现更好,适用于本地数据隐私(LDP)和集中式数据处理。
在联邦学习的背景下,我们预期在应用差分隐私后,模型的准确性会有所下降,但仍然(并且在某种程度上希望)期望模型的表现会优于没有联邦聚合的本地模型。因此,尽管加入了噪音和裁剪(DP),联邦模型仍然应该保持其优势。

图 3. 基于已知文献和我们的经验,我们可以预期的结果。
差分隐私可以最早应用于源数据(本地差分隐私(LDP))。

图 4,不同地方可以应用差分隐私以提高数据保护
也有一些联邦学习案例发生在一个合作伙伴网络中,所有合作伙伴都拥有数据访问权限,且对数据保护级别的关注较少,因此可能完全没有应用差分隐私。
另一方面,当模型要与外部共享或商业化销售时,可能将差分隐私应用于全局模型也是一个好主意。
实际实验结果
在罗氏的联邦开放科学团队,我们选择英伟达 Flare作为我们的联邦学习工具,因为它是市场上最成熟的开源联邦框架。我们还与英伟达团队合作,共同推进NVIDIA Flare 的未来开发,很高兴能够帮助改进这一已经很棒的联邦学习解决方案。
我们测试了三种不同的差分隐私(DP)算法:
我们为模型应用了不同策略的差分隐私(DP):
-
每一轮联邦学习
-
仅限于第一轮(联邦训练)
-
每第 N 轮(联邦训练)
对于三种不同的案例(数据集和算法):
-
FLamby Tiny IXI 数据集
-
乳腺密度分类
-
希格斯分类
所以,我们尝试了算法、策略和数据集(案例)三个维度。
结果符合我们对模型准确度下降的预期,且在隐私预算较低时(如预期)准确率下降较大。
FLamby Tiny IXI 数据集
(数据集来源:owkin.github.io/FLamby/fed_ixi.html)

图 5. 无 DP 的模型表现

图 6. 第一轮应用 DP 的模型表现

图 7. 每第二轮应用 SVT(带有递减阈值)
我们观察到,与每轮应用 SVT 滤波器相比,在第一轮应用 SVT 时,准确率有了显著改善。
乳腺密度案例
(数据集来源 使用 MONAI 进行乳腺密度分类 | Kaggle)

图 8. 无 DP 的模型表现

图 9. DP 应用于第一轮
我们观察到,应用高斯噪声滤波器后,准确率有适度的下降。
这个数据集是最麻烦且对 DP 最敏感的(准确度大幅下降,结果不可预测)。
希格斯分类
(数据集来源 HIGGS — UCI 机器学习库)

图 10. 百分位值 95 时的模型表现

图 11. 百分位值 50.
我们观察到与 DP 相关的微小且可接受的准确率损失。
获得的经验教训
重要的经验教训是,差分隐私的结果对给定 DP 算法的参数非常敏感,且很难调整这些参数以避免模型准确率的彻底崩溃。
此外,我们也体验到某种焦虑,基于一种印象,那就是我们并不真正知道,在付出多少代价的情况下,我们获得了多少隐私保护。我们只看到了“成本”方面(准确率下降)。
我们在很大程度上依赖已有的文献,这些文献表明并已证明,即使是少量的 DP 噪声也有助于保护数据安全。
作为工程师,我们希望看到某种自动化度量工具,能够证明我们为隐私保护获得了多少提升,同时损失了多少准确性,甚至可能有某种自动化差分隐私调优技术。这似乎离当前的技术和知识状态还很遥远。
然后我们应用了隐私度量标准,看看没有差分隐私(DP)和有差分隐私(DP)的模型之间是否存在明显差异,我们观察到曲线有变化,但很难量化我们获得了多少收益。
有些算法根本无法工作,有些则需要多次尝试才能正确调优,以提供可行的结果。关于如何为特定数据集和机器学习模型调优不同参数,缺乏明确的指导。
因此,我们目前的看法是,差分隐私(DP)在联邦学习中的应用虽然困难,但完全可行。它需要大量的迭代和试错循环,才能在相信基于算法的隐私提升具有数倍效果的前提下,获得可接受的结果。
未来
联邦学习是提高患者治疗效果和疗效的一个极好选择,因为它能够在保护患者数据的同时,提升机器学习模型的表现。
但数据保护从来不会是没有代价的,差分隐私在联邦学习中的应用正是这种权衡的完美例证。
很高兴看到差分隐私算法在联邦学习场景中的进展,能够在最大化模型对推断攻击的抗性时,最小化对准确性的影响。
与所有权衡一样,决策必须在模型的实际应用价值与数据泄露和重建的风险之间做平衡。
这也是我们对隐私度量标准期望不断增长的原因,我们希望能更精确地知道我们在“销售”和“购买”的是什么,它们的交换比例是多少。
这个领域是动态变化的,对于那些想更好保护数据的人和那些有动机违反这些规则并暴露敏感数据的人,都有更好的工具可用。
我们还邀请其他联邦学习领域的专家共同努力,推动并为提高患者数据隐私贡献力量。
作者感谢Jacek Chmiel对博文本身的重大影响,以及帮助发展这些思想并将其付诸实践的人员:Jacek Chmiel、Lukasz Antczak、Grzegory Gajda 和罗氏公司的联邦开放科学团队。
本文中的所有图片均由作者创作。
使用符号回归区分嘈杂的时间序列数据
一个逐步示例,展示如何在数据稀缺的情况下推导出嘈杂的时间序列轮廓
·发布于 Towards Data Science ·14 分钟阅读 ·2024 年 9 月 13 日
--

图片来源:Jake Hills,来自 Unsplash
注意:如果您没有 Medium 订阅,可以在 这里 免费阅读本文!
时间序列轮廓在我们的日常生活中无处不在。也有许多专门的研究工作在探讨这一主题。
简单来说,时间序列轮廓是一个后续数据点的集合 y(0), y(1), … ,y(t),其中时刻 t 的一个数据点依赖于时刻 t-1 (甚至更早时间)的数据点。
在许多应用中,人们希望预测在某些先前数据点已知的情况下,时间序列轮廓如何变化。为了实现这一点,存在各种建模方法。它们的核心可能是获取一些关于过去(或现在)的信息,然后估算未来轮廓的样子。可以找到许多涉及时间序列预测的研究工作,例如使用神经网络描述天气变化(Bi 等人,2023),通过深度学习预测股票价格行为(Xiao 和 Su,2022),或预测药品的需求变化(Rathipriya 等人,2023)。当然,这些研究工作是我通过快速搜索找到的,所以…
扩散损失:每一步的解释
论文回顾:去噪扩散概率模型(DDPM)
·发布于Towards Data Science ·15 分钟阅读·2024 年 7 月 31 日
--

使用 DALLE-3 生成(作者提供)
在我们处于图像生成的激动人心的时代,本文将重点介绍稳定扩散模型背后的数学原理和隐含直觉。
本文旨在解读损失函数如何简化为图像中真实噪声和预测噪声之间的简单平方差项,正如《去噪扩散概率模型》论文¹所示。
如果你想回顾在变分贝叶斯方法中证据下界(ELBO)的概念和步骤,请查看之前关于潜在变量模型和概率主成分分析的详细文章。
不再耽搁,让我们开始吧!
符号与定义:
让我们从一些将多次使用的符号开始。
x_0:这表示在时间步长 0 时的图像,即原始图像,处于过程的起始阶段。有时也指在去噪过程的最后一步恢复的图像。
x_T:这是最终时间步长的图像。此时,图像仅仅是各向同性的高斯噪声。
用 PyTorch 从零开始构建扩散模型
去噪扩散概率模型(DDPM)的实现
·发表于Towards Data Science ·13 分钟阅读·2024 年 7 月 4 日
--

DDPM 在 MNIST 上的例子 — 图片由作者提供
介绍
扩散模型一般来说是一种生成式深度学习模型,通过学习去噪过程来生成数据。扩散模型有很多变体,其中最流行的通常是基于文本条件的模型,可以根据提示生成特定的图像。一些扩散模型(如 Control-Net)甚至可以将图像与特定的艺术风格融合。下面是一个例子:

图片由作者使用微调的 MonsterLabs’ QR Monster V2 生成
如果你不知道这张图片有什么特别之处,可以试着把眼睛离屏幕远一点,或者眯着眼睛看,看看图像中隐藏的秘密。
扩散模型有许多不同的应用和类型,但在本教程中,我们将构建基础的无条件扩散模型——DDPM(去噪扩散概率模型)[1]。我们将从直观地理解算法的工作原理开始,然后从零开始用 PyTorch 构建它。此外,本教程将主要关注算法背后的直观思想和具体的实现细节。有关数学推导和背景内容,可以参考本书[2]。
最后的备注:此实现是为包含单个支持 CUDA 的 GPU 的工作流而构建的。此外,完整的代码仓库可以在这里找到github.com/nickd16/Diffusion-Models-from-Scratch
它是如何工作的 -> 正向和反向过程

图片来自 [2] Simon J.D. Prince 的《理解深度学习》
扩散过程包括正向和反向过程。正向过程是基于噪声时间表的预定马尔可夫链。噪声时间表是一组方差 B1、B2、… BT,它们控制组成马尔可夫链的条件正态分布。

正向过程马尔可夫链 — 图片来自 [2]
这个公式是正向过程的数学表示,但直观上我们可以理解它为一个序列,我们逐渐将我们的数据示例 X 映射到纯噪声。我们正向过程的第一个项只是我们的初始数据示例。在中间时间步 t,我们有 X 的带噪声版本,在最终时间步 T,我们到达大致由标准正态分布控制的纯噪声。当我们构建扩散模型时,我们选择我们的噪声时间表。例如,在 DDPM 中,我们的噪声时间表包含 1000 个时间步,线性增加的方差从 1e-4 到 0.02。还要注意的是,我们的正向过程是静态的,这意味着我们将我们的噪声时间表作为扩散模型的超参数,并且我们不训练正向过程,因为它已经明确定义。
我们必须了解关于正向过程的最后一个关键细节是,因为分布是正态的,我们可以数学推导出一个称为“扩散核”的分布,它是给定我们初始数据点的正向过程中任何中间值的分布。这使我们能够绕过在正向过程中迭代地添加 t-1 个噪声级别的所有中间步骤,以获得具有 t 噪声的图像的分布,这在我们训练模型时会很有用。这在数学上表示为:

扩散核 — 图片来自 [2]
在时间 t 时的 alpha 被定义为从我们的初始时间步到当前时间步的累积乘积 (1-B)。
反向过程是扩散模型的关键。反向过程本质上是逐渐从纯噪声图像中去除噪声量以生成新图像的正向过程的撤销。我们通过从纯噪声数据开始,对于每个时间步 t,我们减去在该时间步上正向过程理论上会添加的噪声量。我们不断去除噪声,直到最终得到类似于我们原始数据分布的东西。我们的大部分工作是训练一个模型来精确逼近正向过程,以估计能够生成新样本的反向过程。
算法和训练目标
要训练这样一个模型来估计反向扩散过程,我们可以按照下面定义的图像中的算法进行操作:
-
从我们的训练数据集中随机选择一个数据点
-
从我们的噪声(方差)调度中选择一个随机的时间步
-
将该时间步的噪声添加到我们的数据中,通过“扩散核”模拟前向扩散过程
-
将去噪后的图像传入我们的模型,预测我们添加的噪声
-
计算预测噪声与实际噪声之间的均方误差,并通过该目标函数优化我们模型的参数
-
然后重复!

DDPM 训练算法 — 图片来自[2]
在数学上,算法中的精确公式乍一看可能有些奇怪,尤其是没有看到完整的推导过程,但直观上它是基于我们噪声调度的 alpha 值对扩散核的重参数化,本质上是预测的噪声与我们实际添加到图像中的噪声的平方差。
如果我们的模型能够成功预测基于前向过程特定时间步的噪声量,我们可以从时间步 T 的噪声开始,逐步去除噪声,基于每个时间步,直到我们恢复出类似于原始数据分布的生成样本。
采样算法总结如下:
- 从标准正态分布生成随机噪声
对于每个时间步,从我们最后的时间步开始,向后推进:
2. 通过估计反向过程分布来更新 Z,均值由前一步的 Z 参数化,方差由模型在该时间步估计的噪声参数化
3. 为了稳定性,添加少量噪声(以下解释)
4. 重复此过程,直到我们到达时间步 0,即恢复的图像!

DDPM 采样算法 — 图片来自[2]
然后用于采样和生成图像的算法可能在数学上看起来复杂,但直观上它归结为一个迭代过程,我们从纯噪声开始,估计在时间步 t 时理论上添加的噪声,并将其减去。我们一直执行这个过程,直到得到生成的样本。唯一需要注意的小细节是,在减去估计的噪声后,我们会加回一小部分噪声,以保持过程的稳定性。例如,在迭代过程开始时一次性估计并减去所有噪声会导致非常不连贯的样本,因此,实际上将噪声稍微加回,并在每个时间步上进行迭代,已被实验证明能生成更好的样本。
UNET
DDPM 论文的作者使用了最初为医学图像分割设计的 UNET 架构,构建了一个模型来预测扩散逆过程的噪声。我们在本教程中将使用的模型是为 32x32 图像设计的,适合 MNIST 等数据集,但该模型也可以扩展以处理更高分辨率的数据。UNET 有许多变体,但我们将构建的模型架构概览如下图所示。

用于扩散的 UNET — 作者提供的图片
用于 DDPM 的 UNET 类似于经典的 UNET,因为它包含了一个下采样流和一个上采样流,减轻了网络的计算负担,同时在两个流之间有跳跃连接,将模型的浅层和深层特征的信息合并起来。
DDPM UNET 和经典 UNET 之间的主要区别在于 DDPM UNET 在 16x16 维度层中具有注意力机制,并且在每个残差块中具有正弦变压器嵌入。正弦嵌入背后的含义是告诉模型我们正在尝试预测噪声的时间步长。这有助于模型通过在噪声时间表上注入位置信息来预测每个时间步长的噪声。例如,如果我们有一个在某些时间步长上有很多噪声的噪声时间表,模型理解它必须预测的时间步长可以帮助模型对相应时间步长的噪声进行预测。关于注意力和嵌入的更一般信息可以在这里找到[3],供那些还不熟悉它们的人从变压器架构中了解。
在我们的模型实现中,我们将从定义我们的导入开始(可能的 pip 安装命令已经注释以供参考),并编写我们的正弦时间步嵌入。直观地说,正弦嵌入是不同的 sin 和 cos 频率,可以直接添加到我们的输入中,为模型提供额外的位置/顺序理解。从下面的图片中可以看到,每个正弦波是独一无二的,这将使模型意识到它在我们的噪声时间表中的位置。

正弦嵌入 — 来自[3]的图片
# Imports
import torch
import torch.nn as nn
import torch.nn.functional as F
from einops import rearrange #pip install einops
from typing import List
import random
import math
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
from timm.utils import ModelEmaV3 #pip install timm
from tqdm import tqdm #pip install tqdm
import matplotlib.pyplot as plt #pip install matplotlib
import torch.optim as optim
import numpy as np
class SinusoidalEmbeddings(nn.Module):
def __init__(self, time_steps:int, embed_dim: int):
super().__init__()
position = torch.arange(time_steps).unsqueeze(1).float()
div = torch.exp(torch.arange(0, embed_dim, 2).float() * -(math.log(10000.0) / embed_dim))
embeddings = torch.zeros(time_steps, embed_dim, requires_grad=False)
embeddings[:, 0::2] = torch.sin(position * div)
embeddings[:, 1::2] = torch.cos(position * div)
self.embeddings = embeddings
def forward(self, x, t):
embeds = self.embeddings[t].to(x.device)
return embeds[:, :, None, None]
UNET 中每个层中的残差块将等同于原始 DDPM 论文中使用的残差块。每个残差块将包含一系列的 group-norm、ReLU 激活、3x3“same”卷积、dropout 和一个跳跃连接。
# Residual Blocks
class ResBlock(nn.Module):
def __init__(self, C: int, num_groups: int, dropout_prob: float):
super().__init__()
self.relu = nn.ReLU(inplace=True)
self.gnorm1 = nn.GroupNorm(num_groups=num_groups, num_channels=C)
self.gnorm2 = nn.GroupNorm(num_groups=num_groups, num_channels=C)
self.conv1 = nn.Conv2d(C, C, kernel_size=3, padding=1)
self.conv2 = nn.Conv2d(C, C, kernel_size=3, padding=1)
self.dropout = nn.Dropout(p=dropout_prob, inplace=True)
def forward(self, x, embeddings):
x = x + embeddings[:, :x.shape[1], :, :]
r = self.conv1(self.relu(self.gnorm1(x)))
r = self.dropout(r)
r = self.conv2(self.relu(self.gnorm2(r)))
return r + x
在 DDPM 中,作者在每个 UNET 的层(分辨率尺度)中使用了 2 个残差块,对于 16x16 维度的层,我们在两个残差块之间包含了经典的变压器注意力机制。我们现在将为 UNET 实现注意力机制:
class Attention(nn.Module):
def __init__(self, C: int, num_heads:int , dropout_prob: float):
super().__init__()
self.proj1 = nn.Linear(C, C*3)
self.proj2 = nn.Linear(C, C)
self.num_heads = num_heads
self.dropout_prob = dropout_prob
def forward(self, x):
h, w = x.shape[2:]
x = rearrange(x, 'b c h w -> b (h w) c')
x = self.proj1(x)
x = rearrange(x, 'b L (C H K) -> K b H L C', K=3, H=self.num_heads)
q,k,v = x[0], x[1], x[2]
x = F.scaled_dot_product_attention(q,k,v, is_causal=False, dropout_p=self.dropout_prob)
x = rearrange(x, 'b H (h w) C -> b h w (C H)', h=h, w=w)
x = self.proj2(x)
return rearrange(x, 'b h w C -> b C h w')
注意力机制的实现很直接。我们重新塑造我们的数据,使得 h*w 维度合并成一个“序列”维度,就像传统的变压器模型的输入一样,通道维度变成了嵌入特征维度。在这个实现中,我们利用 torch.nn.functional.scaled_dot_product_attention,因为这个实现包含了 flash attention,这是一个经过优化的注意力版本,仍然在数学上等同于经典的变压器注意力。关于 flash attention 的更多信息,您可以参考这些论文:[4],[5]。
最终,在这一阶段,我们可以定义 UNET 的完整层:
class UnetLayer(nn.Module):
def __init__(self,
upscale: bool,
attention: bool,
num_groups: int,
dropout_prob: float,
num_heads: int,
C: int):
super().__init__()
self.ResBlock1 = ResBlock(C=C, num_groups=num_groups, dropout_prob=dropout_prob)
self.ResBlock2 = ResBlock(C=C, num_groups=num_groups, dropout_prob=dropout_prob)
if upscale:
self.conv = nn.ConvTranspose2d(C, C//2, kernel_size=4, stride=2, padding=1)
else:
self.conv = nn.Conv2d(C, C*2, kernel_size=3, stride=2, padding=1)
if attention:
self.attention_layer = Attention(C, num_heads=num_heads, dropout_prob=dropout_prob)
def forward(self, x, embeddings):
x = self.ResBlock1(x, embeddings)
if hasattr(self, 'attention_layer'):
x = self.attention_layer(x)
x = self.ResBlock2(x, embeddings)
return self.conv(x), x
如前所述,DDPM 中的每一层都有 2 个残差块,并可能包含一个注意力机制,我们还将嵌入向量传递到每个残差块中。此外,我们返回下采样或上采样的值,以及之前的值,我们会存储并用于残差拼接跳跃连接。
最后,我们可以完成 UNET 类:
class UNET(nn.Module):
def __init__(self,
Channels: List = [64, 128, 256, 512, 512, 384],
Attentions: List = [False, True, False, False, False, True],
Upscales: List = [False, False, False, True, True, True],
num_groups: int = 32,
dropout_prob: float = 0.1,
num_heads: int = 8,
input_channels: int = 1,
output_channels: int = 1,
time_steps: int = 1000):
super().__init__()
self.num_layers = len(Channels)
self.shallow_conv = nn.Conv2d(input_channels, Channels[0], kernel_size=3, padding=1)
out_channels = (Channels[-1]//2)+Channels[0]
self.late_conv = nn.Conv2d(out_channels, out_channels//2, kernel_size=3, padding=1)
self.output_conv = nn.Conv2d(out_channels//2, output_channels, kernel_size=1)
self.relu = nn.ReLU(inplace=True)
self.embeddings = SinusoidalEmbeddings(time_steps=time_steps, embed_dim=max(Channels))
for i in range(self.num_layers):
layer = UnetLayer(
upscale=Upscales[i],
attention=Attentions[i],
num_groups=num_groups,
dropout_prob=dropout_prob,
C=Channels[i],
num_heads=num_heads
)
setattr(self, f'Layer{i+1}', layer)
def forward(self, x, t):
x = self.shallow_conv(x)
residuals = []
for i in range(self.num_layers//2):
layer = getattr(self, f'Layer{i+1}')
embeddings = self.embeddings(x, t)
x, r = layer(x, embeddings)
residuals.append(r)
for i in range(self.num_layers//2, self.num_layers):
layer = getattr(self, f'Layer{i+1}')
x = torch.concat((layer(x, embeddings)[0], residuals[self.num_layers-i-1]), dim=1)
return self.output_conv(self.relu(self.late_conv(x)))
基于我们已经创建的类,实施过程是直接的。此实现的唯一区别在于我们上游的通道略大于典型的 UNET 通道。我发现这种架构在 16GB VRAM 的单个 GPU 上训练效率更高。
调度器
为 DDPM 编写噪声/方差调度器也是非常简单的。在 DDPM 中,正如之前提到的,我们的调度器将从 1e-4 开始,到 0.02 结束,并线性增加。
class DDPM_Scheduler(nn.Module):
def __init__(self, num_time_steps: int=1000):
super().__init__()
self.beta = torch.linspace(1e-4, 0.02, num_time_steps, requires_grad=False)
alpha = 1 - self.beta
self.alpha = torch.cumprod(alpha, dim=0).requires_grad_(False)
def forward(self, t):
return self.beta[t], self.alpha[t]
我们返回 beta(方差)值和 alpha 值,因为我们在训练和采样的公式中都会使用这两个值,基于它们的数学推导。
def set_seed(seed: int = 42):
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
np.random.seed(seed)
random.seed(seed)
另外(不是必须的),此功能定义了一个训练种子。这意味着,如果你想重现特定的训练实例,可以使用一个设置的种子,这样每次使用相同的种子时,随机权重和优化器的初始化将保持一致。
训练
对于我们的实现,我们将创建一个模型来生成 MNIST 数据(手写数字)。由于这些图像在 pytorch 中默认是 28x28,我们将图像填充到 32x32 以符合原始论文中基于 32x32 图像训练的设置。
对于优化,我们使用 Adam 优化器,初始学习率为 2e-5。我们还使用 EMA(指数加权移动平均)来提高生成质量。EMA 是模型参数的加权平均值,在推理时可以生成更平滑、噪声更少的样本。对于这一实现,我使用了 timm 库中的 EMAV3 默认实现,权重为 0.9999,正如 DDPM 论文中所使用的。
总结我们的训练过程,我们只需按照上述伪代码执行。我们为每个批次随机选择时间步,根据这些时间步和调度器为批次中的数据添加噪声,并将带噪声的图像批次输入到 UNET 中,同时提供时间步信息以引导正弦嵌入。我们根据伪代码中的“扩散核”公式为图像添加噪声。然后,我们将模型预测的噪声量与实际添加的噪声进行比较,并优化噪声的均方误差。我们还实现了基本的检查点功能,以便在不同的训练周期暂停和恢复训练。
def train(batch_size: int=64,
num_time_steps: int=1000,
num_epochs: int=15,
seed: int=-1,
ema_decay: float=0.9999,
lr=2e-5,
checkpoint_path: str=None):
set_seed(random.randint(0, 2**32-1)) if seed == -1 else set_seed(seed)
train_dataset = datasets.MNIST(root='./data', train=True, download=False,transform=transforms.ToTensor())
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, drop_last=True, num_workers=4)
scheduler = DDPM_Scheduler(num_time_steps=num_time_steps)
model = UNET().cuda()
optimizer = optim.Adam(model.parameters(), lr=lr)
ema = ModelEmaV3(model, decay=ema_decay)
if checkpoint_path is not None:
checkpoint = torch.load(checkpoint_path)
model.load_state_dict(checkpoint['weights'])
ema.load_state_dict(checkpoint['ema'])
optimizer.load_state_dict(checkpoint['optimizer'])
criterion = nn.MSELoss(reduction='mean')
for i in range(num_epochs):
total_loss = 0
for bidx, (x,_) in enumerate(tqdm(train_loader, desc=f"Epoch {i+1}/{num_epochs}")):
x = x.cuda()
x = F.pad(x, (2,2,2,2))
t = torch.randint(0,num_time_steps,(batch_size,))
e = torch.randn_like(x, requires_grad=False)
a = scheduler.alpha[t].view(batch_size,1,1,1).cuda()
x = (torch.sqrt(a)*x) + (torch.sqrt(1-a)*e)
output = model(x, t)
optimizer.zero_grad()
loss = criterion(output, e)
total_loss += loss.item()
loss.backward()
optimizer.step()
ema.update(model)
print(f'Epoch {i+1} | Loss {total_loss / (60000/batch_size):.5f}')
checkpoint = {
'weights': model.state_dict(),
'optimizer': optimizer.state_dict(),
'ema': ema.state_dict()
}
torch.save(checkpoint, 'checkpoints/ddpm_checkpoint')
对于推理,我们再次完全遵循伪代码的另一部分。直观地说,我们只是反转了前向过程。我们从纯噪声开始,现在训练好的模型可以预测每个时间步的估计噪声,然后可以逐步生成全新的样本。每个不同的噪声起点,我们都可以生成一个与原始数据分布相似但独特的不同样本。推理的公式在本文中并未推导,但开头提到的参考文献可以帮助读者深入理解。
另外,请注意,我包括了一个辅助函数来查看扩散图像,这样你可以直观地看到模型学习反向过程的效果如何。
def display_reverse(images: List):
fig, axes = plt.subplots(1, 10, figsize=(10,1))
for i, ax in enumerate(axes.flat):
x = images[i].squeeze(0)
x = rearrange(x, 'c h w -> h w c')
x = x.numpy()
ax.imshow(x)
ax.axis('off')
plt.show()
def inference(checkpoint_path: str=None,
num_time_steps: int=1000,
ema_decay: float=0.9999, ):
checkpoint = torch.load(checkpoint_path)
model = UNET().cuda()
model.load_state_dict(checkpoint['weights'])
ema = ModelEmaV3(model, decay=ema_decay)
ema.load_state_dict(checkpoint['ema'])
scheduler = DDPM_Scheduler(num_time_steps=num_time_steps)
times = [0,15,50,100,200,300,400,550,700,999]
images = []
with torch.no_grad():
model = ema.module.eval()
for i in range(10):
z = torch.randn(1, 1, 32, 32)
for t in reversed(range(1, num_time_steps)):
t = [t]
temp = (scheduler.beta[t]/( (torch.sqrt(1-scheduler.alpha[t]))*(torch.sqrt(1-scheduler.beta[t])) ))
z = (1/(torch.sqrt(1-scheduler.beta[t])))*z - (temp*model(z.cuda(),t).cpu())
if t[0] in times:
images.append(z)
e = torch.randn(1, 1, 32, 32)
z = z + (e*torch.sqrt(scheduler.beta[t]))
temp = scheduler.beta[0]/( (torch.sqrt(1-scheduler.alpha[0]))*(torch.sqrt(1-scheduler.beta[0])) )
x = (1/(torch.sqrt(1-scheduler.beta[0])))*z - (temp*model(z.cuda(),[0]).cpu())
images.append(x)
x = rearrange(x.squeeze(0), 'c h w -> h w c').detach()
x = x.numpy()
plt.imshow(x)
plt.show()
display_reverse(images)
images = []
def main():
train(checkpoint_path='checkpoints/ddpm_checkpoint', lr=2e-5, num_epochs=75)
inference('checkpoints/ddpm_checkpoint')
if __name__ == '__main__':
main()
在上述实验细节下训练 75 个 epoch 后,我们得到以下结果:

作者提供的图像
到目前为止,我们已经用 PyTorch 从零开始编码了 DDPM!
感谢阅读!
参考文献
[1] DDPM
[2] 理解深度学习
[4] Flash Attention
扩散模型:Midjourney、Dall-E 通过反向时间从提示中生成图像
·发表于 Towards Data Science ·18 分钟阅读·2024 年 1 月 9 日
--

图片由作者提供。
如果你阅读过我最近的一些博客,你会知道我是新一代 AI 工具的重度用户,这些工具可以根据提示生成图像,属于最近的 AI 春天的一部分(我为使用 Midjourney 支付了月费)。
我对 AI 研究的一个看法是,它像是一场竞赛,研究人员在不断破解一些复杂的模型,直到它们能够推动某些基准变化,却没有花足够的时间去理解这些模型为什么会做出这样的行为。从这个角度看,这些模型的研究工作有时更像是一种艺术,而非科学。这也是我对深入研究这些模型产生轻微排斥的原因。而这种排斥感因开发速度的飞快而加剧。假如我花了大量时间去理解一个新奇的模型,它却在明天就被淘汰了,那该怎么办?
但最近图像生成模型的进展,让人只需输入一些图像描述,就能得到一张质量极高的作品(Midjourney、Dall-E 和开源的 Stable Diffusion 都是其中的代表),这让我不得不走出自己的“洞穴”并开始关注。而关注正是我所需要的。
在这个过程中,我惊讶地发现,这些扩散模型背后的理论相当深奥,受到一门名为统计热力学的物理学分支的启发……
扩散 Transformer 解析
探索将 Transformer 引入图像生成领域的架构
·发表于 Towards Data Science ·12 分钟阅读·2024 年 2 月 28 日
--

使用 DALL·E 生成的图像。
介绍
在震撼了 NLP 并通过视觉 Transformer(ViT)及其后续模型进入计算机视觉领域后,Transformer 现在正在进入图像生成领域。它们逐渐成为 U-Net 的替代方案,而 U-Net 是所有早期扩散模型的基础卷积架构。本文将探讨 扩散 Transformer(DiT),由 William Peebles 和 Saining Xie 在他们的论文“使用 Transformer 的可扩展扩散模型”中介绍。
我们探索了一类基于 Transformer 架构的新型扩散模型。我们训练了潜在扩散模型的…
arxiv.org](https://arxiv.org/abs/2212.09748?source=post_page-----e603c4770f7e--------------------------------)
DiT 影响了其他基于 Transformer 的扩散模型的发展,例如 PIXART-α、Sora(OpenAI 惊人的文本到视频模型),以及在我写这篇文章时的 Stable Diffusion 3。让我们开始探索这些新兴的架构,它们正在推动扩散模型的进化。
前言
鉴于这是一个高级话题,我必须假设读者对…
DIGITOUR:房地产数字导览自动化 🏠
一种自动化管道,用于从等距矩形图像创建 3D 体验
·发表于Towards Data Science ·阅读时间 11 分钟·2024 年 3 月 18 日
--
1. 引言
由于互联网的便利性,尤其是在印度等国家,在线房地产工具的需求急剧增加。许多在线房地产平台供房主、开发商和房地产经纪人发布买卖和租赁目的的房产信息。每天,这些平台都会收到 8,000 到 9,000 个新的房源信息。直到现在,用户在这些平台上查看的是图像、快照或视频,这可能无法建立足够的信心来做出决策并最终完成交易。为了解决这一挑战并提升用户体验,虚拟导览是一个潜在的解决方案。
虚拟导览是将图像连接在一起,让观众能够远程体验特定地点(一个常见的例子是 Google 街景)[1]。近年来,虚拟导览的需求不断增加,因为它们能提供更好的用户/客户互动,尤其是在房地产、酒店、餐厅、大学、学校等商业领域[2]。大致而言,虚拟导览可以分为三类:
-
2D 视频导览,
-
360°视频基础虚拟导览,以及
-
基于 360°静态图像的虚拟导览。
与 2D 视频导览和基于 360°视频的导览相比,静态的等距矩形图像基础虚拟导览提供了更多的沉浸感和互动性,从而有助于更好的决策并避免不必要的访问。
🚀 在深入探讨提议的工作之前,我想提到的是,本项工作已经发表,并可以在数据科学与数据管理国际会议(CODS-COMAD)——2023上查阅。✨✨✨

图 1: 在房屋入口处拍摄的等距矩形图像示例,显示了客厅。(来源:作者提供的图像)
通常,创建虚拟旅游的流程包括以下组件:
-
等距矩形图像捕捉: 等距矩形图像将球形物体表示为二维图像(如图 1所示)。它是一个包含 180°垂直和 360°水平视角的球形全景。一个简单的例子可以是将地球(一个球形物体*)投影到二维地图上。这些图像是通过使用 360◦相机拍摄的,例如Ricoh-Theta、Insta3602等。
-
连接等距矩形图像: 对于任何位置,我们都会有多个等距矩形图像。以房地产为例,我们通常有卧室、客厅、厨房、餐厅等的等距矩形图像。必须在图像之间建立导航,以提供完整的“虚拟漫游”体验。此外,从一个位置到其他位置可能有多条路线。例如,我们可以从客厅走到厨房、卧室、阳台等。因此,连接所有等距矩形图像至关重要(请参见 图 2 以了解示例)。通常,这一过程是手动完成的,既昂贵又耗时[3]。
-
发布虚拟旅游: 一旦我们完成了等距矩形图像之间的连接,就可以将最终的虚拟旅游发布到云端或应用程序中。

图 2: 示例房产平面图(a)及其连接(b)以创建数字旅游。数字代表标签和等距矩形图像的位置。(来源:作者提供的图像)
在自动化上述流程时,面临的一个重大挑战是手动注释以连接等距矩形图像。通常,创建一个虚拟旅游需要超过 20 分钟。我们提出了一种端到端的流程,用于房地产等距矩形图像(称为 DIGITOUR)以克服创建自动化数字旅游的挑战。DIGITOUR流程包含以下组件:
-
彩色标签放置与点击 360°图像: 我们提出了一种新型的双色标签,带有编号,便于更好地学习下游计算机视觉任务(即标签识别和数字识别)以及等距矩形图像的自动拼接。
-
将等距矩形图像映射到立方体映射投影: 我们使用公开的 Python 库vrProjector3将等距矩形图像映射到它们的立方体映射投影(对应六个立方体面)。
-
标签检测: 对于每个立方体面,我们提出了在图像中使用 YOLOv5 [4]架构进行彩色标签检测。
-
数字识别: 我们建议使用轻量级的定制 MobileNet [5] 模型进行数字识别。
最后,我们使用检测到的标签将所有等距矩形图像连接起来。

图 3: 提议方法 DIGITOUR 的端到端流程。(来源:作者提供的图片)
2. 提议的流程:DIGITOUR
生成数字化旅游的完整流程(称为 DIGITOUR ,如图 3*所示)如下。
2.1 标签放置和图像捕捉
在为任何房地产物业创建数字化旅游时,必须从不同的物业位置(如卧室、客厅、厨房等)拍摄 360◦图像,然后自动将它们拼接在一起,以便在不亲自到场的情况下获得“虚拟游览”体验。因此,为了连接多个等距矩形图像,我们建议在地面上放置纸质标签,覆盖物业的每个位置,并将相机(在我们的案例中,我们使用了 Ricoh-Theta)放置在场景的中央,以捕捉整个场景(前、后、左、右和底部)。

图 4: 提出了每个数字的双色标签格式和颜色方案及其对应的 HSV 值。(来源:作者提供的图片)
此外,我们确保场景中没有任何噪音元素,例如昏暗的灯光和‘不需要的’伪影,以便更好地进行模型训练和推理。如图 4所示,我们将标签标准化为 6” × 6”的尺寸,并具有两个属性:
-
它们被编号,这将帮助摄影师按顺序放置标签并
-
它们是双色的,旨在将数字识别问题表述为分类任务,并促进下游计算机视觉任务(即标签检测和数字识别)的更好学习。
请注意,使用 HSV 颜色方案为每个数字(从 0 到 9)分配了不同的颜色,并且标签的前导数字有一个黑色圆圈,以将其与后续数字区分开,如图 4所示。标准化纸质标签的直觉是,它可以用来训练标签检测和数字识别模型,这些模型对失真、标签放置角度、光源反射、模糊条件和相机质量不变。
2.2 将等距矩形图像映射到立方体图投影
等距矩形图像由一张图像组成,其宽度与高度的比例为 2:1(如图 1所示)。在我们的案例中,图像是使用 Ricoh-Theta 相机拍摄的,尺寸为 4096 × 2048 × 3。通常,等距矩形图像中的每个点对应于球面上的一个点,图像在‘纬度*’方向上被拉伸。由于等距矩形图像的内容被扭曲,因此直接从图像中检测标签和识别数字变得困难。例如,在图 1中,标签在图像的中下部被拉伸。因此,有必要将图像映射到一个失真较小的投影上,并返回到原始的等距矩形图像中以构建数字化旅行。
在本研究中,我们提出使用立方体映射投影,这是一个由六张图像组成的集合,表示立方体的六个面。在这里,球面坐标空间中的每个点对应于立方体某个面的一个点。如图 5所示,我们将等距矩形图像映射到一个具有 1024 × 1024 × 3 尺寸的立方体的六个面(左、右、前、后、上、下),并使用 Python 库 vrProjector 完成此操作。

图 5: 将等距矩形图像转换为其对应的六面立方体投影。 (来源:作者提供的图像)
2.3 标签检测
一旦我们获得了对应立方体六个面的六张图像,我们就可以检测每个图像中放置的标签的位置。对于标签检测,我们使用了最先进的 YOLOv5 模型。我们首先使用 COCO 权重初始化网络,然后在我们的数据集上进行训练。如图 6所示,模型将图像作为输入,并返回检测到的标签以及边界框的坐标和预测的置信度。该模型在我们的数据集上进行了 100 轮的训练,批次大小为 32。

图 6: 使用 Yolov5 进行标签检测。 (来源:作者提供的图像)
2.4 数字识别
对于检测到的标签,我们需要识别标签上的数字。在现实环境中,检测到的标签可能具有不正确的方向、较差的亮度、房间内灯泡的反射等。由于这些原因,使用光学字符识别(OCR)引擎进行数字识别可能无法获得良好的性能。因此,我们使用了一个自定义的 MobileNet 模型,该模型在 Imagenet 权重上进行了初始化,并利用标签中的颜色信息来进行数字识别。在所提出的架构中,我们将原始 MobileNet 的最终分类块替换为包含 20 个节点的 Dropout 层和 Dense 层,代表我们从 1 到 20 的标签。图 7展示了所提出的架构。为了训练模型,我们使用 Adam 作为优化器,学习率为 0.001,折扣因子(𝜌)为 0.1。我们使用了类别交叉熵作为损失函数,批次大小设置为 64,训练轮数为 50。

图 7: 使用自定义 MobileNet 模型进行数字识别。(来源:作者提供的图像)
2.5 标签坐标映射到原始 360°图像并创建虚拟导览
一旦我们检测到标签并识别了数字,我们使用 Python 库 vrProjector 将立方体映射坐标映射回原始的等距矩形图像。一个示例输出如图 8所示。对于每个等距矩形图像,检测到的标签形成一个图的节点,节点之间有边连接。在同一物业的后续等距矩形图像中,随着更多标签的检测,图中会加入更多的节点。最后,我们根据识别出的数字将多个等距矩形图像按顺序连接起来,得到的图就是
如图 2(b)所示的虚拟导览。

图 8: 标签映射到原始等距矩形图像。(来源:作者提供的图像)
4. 数据集
我们通过在印度古尔冈(Tier 1 城市)的几处住宅物业中放置标签并使用 Ricoh-Theta 相机拍摄等距矩形图像来收集数据。在收集图像时,我们确保满足一定的条件,如所有门都打开,灯光开启,‘不需要’的物品已移除,标签被放置在覆盖物业各个区域的位置。遵循这些指示,每处住宅物业拍摄的等距矩形图像平均为 7 到 8 张。最后,我们在以下生成的数据集上验证了我们的方法(基于标签的背景颜色)。
-
绿色标签: 我们将这些标签的背景颜色(编号 1 至 20)设置为绿色。我们从 212 个物业中收集了 1572 张等距矩形图像。一旦我们将这些等距矩形图像转换为立方体投影,就得到了 9432 张图像(对应立方体面)。由于并非所有立方体面都有标签(例如顶部面),我们得到了 1503 张至少包含一个标签的图像。
-
建议的双色标签(见图 4): 对于这些标签,我们从 350 个物业中收集了 2654 张等距矩形图像。最终,我们得到了 2896 张图像(对应立方体面),每张图像至少包含一个标签。
最后,我们使用LabelImg为立方体投影图像中的标签进行标注,它是一个开源工具,支持对 Pascal VOC 和 YOLO 等多种格式的图像进行标注。在所有实验中,我们将 20%的数据用于测试,其余用于训练。
5. 端到端评估
对于任何输入图像,我们首先检测标签,然后识别标签上的数字。通过这个过程,我们能够识别真正的正例(标签正确检测并读取)、假正例(标签检测到但读取错误)和假负例(标签未检测到)。在 0.5 IoU 阈值下,获得的 mAP、精确度、召回率和 f1-score 分别为 88.12、93.83、97.89 和 95.81。请注意,所有指标都是在所有 20 个类别上进行平均(加权)的。如果一个物业的所有等矩形图像中的标签都能正确检测和读取,我们将得到 100%准确的虚拟游览,因为图的所有节点都被检测到,并与它们的适当边连接。在我们的实验中,我们能够为 94.55%的物业准确生成 100%准确的虚拟游览。不准确的原因是彩色伪影被误检测为标签,以及糟糕的光照条件。
图 9 展示了 Yolov5 模型在基于绿色和双色标签的标签检测上的性能。此外,关于数字识别的实验和模型比较见于图 10。

图 9: 标签检测性能(%)。 (来源:作者提供的图片)

图 10: 在双色标签数据集上,针对数字识别任务的不同最先进模型的比较。(来源:作者提供的图片)
5. 结论
我们提出了一个端到端的流水线(DIGITOUR),用于自动生成房地产物业的数字化导览。对于任何这样的物业,我们首先在物业的每个区域放置我们提议的双色纸质标签。然后,我们拍摄等矩形图像,并将这些图像映射到失真较小的立方体图像上。一旦我们得到与立方体面相对应的六张图像,我们使用 YOLOv5 模型检测标签的位置,随后使用 MobileNet 模型进行数字识别。下一步是将检测到的坐标及识别的数字映射回原始的等矩形图像。最后,我们将所有等矩形图像拼接起来,构建虚拟导览。我们已经在一个真实世界的数据集上验证了我们的流水线,并展示了端到端流水线在所有类别上,基于 0.5 IoU 阈值下的 mAP 和 f1-score 分别为 88.12 和 95.81 的表现。
如果您觉得我们的工作有帮助并在您的项目中使用它,我们恳请您引用我们的工作。😊
@inproceedings{chhikara2023digitour,
title={Digitour: Automatic digital tours for real-estate properties},
author={Chhikara, Prateek and Kuhar, Harshul and Goyal, Anil and Sharma, Chirag},
booktitle={Proceedings of the 6th Joint International Conference on Data Science \& Management of Data (10th ACM IKDD CODS and 28th COMAD)},
pages={223--227},
year={2023}
}
参考文献
[1] Dragomir Anguelov, Carole Dulong, Daniel Filip, Christian Frueh, Stéphane Lafon, Richard Lyon, Abhijit Ogale, Luc Vincent, and Josh Weaver. 2010. Google 街景:捕捉街道层面的世界。计算机 43, 6 (2010), 32–38.
[2] Mohamad Zaidi Sulaiman, Mohd Nasiruddin Abdul Aziz, Mohd Haidar Abu Bakar, Nur Akma Halili, 和 Muhammad Asri Azuddin. 2020. Matterport:虚拟旅游作为疫情期间房地产行业的新营销方法。在 2020 年国际媒体与视觉设计创新会议(IMDES 2020)上发表。Atlantis Press, 221–226。
[3] Chinu Subudhi. 2021. 尖端的 360 度虚拟旅游。www.mindtree.com/insights/resources/cutting-edge-360-degree-virtual-tours
[4] Glenn Jocher, Ayush Chaurasia, Alex Stoken, Jirka Borovec, NanoCode012, Yonghye Kwon, TaoXie, Jiacong Fang, imyhxy, Kalen Michael, Lorna, Abhiram V, Diego Montes, Jebastin Nadar, Laughing, tkianai, yxNONG, Piotr Skalski, Zhiqiang Wang, Adam Hogan, Cristi Fati, Lorenzo Mammana, AlexWang1900, Deep Patel, Ding Yiwei, Felix You, Jan Hajek, Laurentiu Diaconu, 和 Mai Thanh Minh. 2022. ultralytics/yolov5: v6.1 — TensorRT, TensorFlow Edge TPU 和 OpenVINO 导出与推理。
[5] Mark Sandler, Andrew Howard, Menglong Zhu, Andrey Zhmoginov, 和 LiangChieh Chen. 2018. Mobilenetv2: 反向残差与线性瓶颈。发表于 IEEE 计算机视觉与模式识别会议论文集。4510–4520。
降维简化:PCA 理论与 Scikit-Learn 实现
驯服维度灾难!学习降维(PCA)并用 Python 和 Scikit-Learn 实现。
·发表于Towards Data Science ·阅读时长:11 分钟·2024 年 2 月 7 日
--

图片来源:unsplash.com.
在小说《平面国》中,生活在二维世界的角色在遇到三维生物时感到困惑,无法理解它的存在。我用这个类比来说明,在处理涉及成千上万甚至百万维度(即特征)的问题时,惊人的现象会发生,这些现象对我们的机器学习模型产生灾难性的影响。
我敢肯定,你至少曾因现代机器学习问题中涉及的庞大特征数量而感到震惊。每一个数据科学从业者,迟早都会面临这个挑战。本文将探讨最常用的降维算法的理论基础和 Python 实现:主成分分析(PCA)。
为什么我们需要减少特征数量?
现在涉及成千上万甚至百万特征的数据集已经很常见。向数据集中添加新特征可能会带来有价值的信息,但它们也会减缓训练过程并使...
残疾、无障碍性与 AI
一场关于 AI 如何帮助和伤害残疾人的讨论
·发表于Towards Data Science ·9 分钟阅读·2024 年 9 月 16 日
--

图片由Thought Catalog提供,来源于Unsplash
我最近读了9 月 4 日的一篇帖子,作者是美国大学的 Johnathan Flowers 博士,讲述了当NaNoWriMo的组织者发布声明,表示支持参与者使用生成型 AI(如大型语言模型聊天机器人)作为今年活动的一部分时,所引发的争议。
“比如,艺术往往是唯一一个可以在不依赖健全人慷慨或强迫亲密的情况下,克服残疾身体与世界之间不契合的地方。说我们需要 AI 的帮助,其实是忽视了这一切。” -Johnathan Flowers 博士,2024 年 9 月 4 日
Flowers 博士认为,通过特别强调这一决定是为了让残疾人和边缘化群体能够参与,组织者实际上是在轻视这些群体的创造力和艺术参与能力。作为一名残疾人,他指出,艺术是社会中少数几个残疾不构成参与障碍的领域之一,这一点在其他不太容易接触的空间中并不常见。
自从最初的声明以及这次和其他许多批评之后,NaNoWriMo 的组织者已经软化或撤回了一些他们的表态,最近的帖子似乎是本周早些时候的更新。不幸的是,正如常常发生的那样,社交媒体上的许多讨论变成了无效的争论。
我之前在这里讨论过,当生成性人工智能参与艺术创作时,评估其真正含义的难度,我依然坚持我的观点:作为艺术的消费者,我寻求的是与他人视角和世界观的连接,因此,人工智能生成的作品在这一点上并不引起我的兴趣。然而,我之前并没有花太多时间思考人工智能作为辅助工具的角色,而今天我想讨论的正是这个话题。
我不是一名身体有障碍的人,所以我只能作为一名社会科学家和外部观察者来探讨这个话题。我的观点仅代表我个人,而非任何社区或组织的立场。
框架
在最近的一次演讲中,我被要求首先给出“人工智能”的定义,我总是有些畏惧这个问题,因为它非常模糊且难以把握,但这次我尝试了一个新的角度,阅读了一些最近的监管和政策讨论,并得出了这个定义:
人工智能:使用某些形式的机器学习来执行本应由人类完成的工作。
我仍在不断探索这个问题,可能会一直探索下去,因为世界在变化,但我认为这个定义对于今天的讨论是有用的。请注意,这并不是将我们的讨论仅限于生成性人工智能,这是很重要的。关于人工智能的这次讨论,特别是指将机器学习应用于完成当前无法通过其他方式自动化的任务,无论它是否涉及深度学习。
关于残疾的社会理论是一个独立的学科,具有巨大的深度和复杂性。就像讨论和学术研究其他群体的人一样,实际的残疾群体成员的声音不仅需要被听到,而且要在讨论他们如何被对待以及他们在更广泛社会中的机会时,发挥主导作用。根据我对这个领域的理解,我希望优先考虑残疾人士拥有他们所希望的自主权和独立性,并获得足够的支持,以便他们能有与非残疾人士相当的机会和成果。还值得一提的是,许多最初为帮助残疾人士而开发的技术,实际上也对所有人有助,比如自动门。
人工智能作为工具
那么,人工智能在这个目标中到底能发挥什么作用呢?人工智能对残障人士来说是一个净好处吗?一般来说,技术,尤其是与人工智能相关的发展,已经在多个方面应用,为残障人士提供了自主性和独立性,这是过去无法实现的。任何像我一样最近在观看巴黎残奥会的人,都能想到技术在这方面的应用实例。
但我很好奇,人工智能提供了哪些其他工具是之前不存在的,以及可能存在的缺点或风险。事实证明,已经有相当多有趣的学术研究在这一问题上展开,并且持续发布。我将简要概述几个关键领域,并在你有兴趣深入了解时提供更多资源。
优点
神经学和沟通问题
这似乎应该是人工智能工具的一个强项。大语言模型(LLM)在重新表述、改写或总结文本方面具有很大的实用性。当个体在阅读长篇文本或集中注意力上有困难时,能够生成准确的总结可以让他们更容易接触到文本的主题。这不一定是对整篇文本的替代,而只是一个增强读者理解的工具。(就像 Cliff Notes,但它们是按应有的方式使用。)我不推荐直接向大语言模型询问一段话的含义,因为那样更可能产生错误或不准确,但总结已经存在的文本则是一个不错的用例。
其次,语言交流有困难的人可以通过人工智能工具获得支持。这些技术可以将语音文本转化为高度准确的自动转录,可能让患有语言障碍的人更容易理解,或者它可以让一个有说话困难的人写下文字,并将其转化为高度真实的人类语音。(事实上,人工智能合成语音最近变得非常惊人!)
这还没有涉及到人工智能如何帮助听力障碍者!助听器可以使用模型来识别并隔离用户希望专注的声音,减少干扰或背景噪音。任何使用过主动噪声取消技术的人,都在受益于这种技术,这也是一个很好的例子,说明这些技术对有无障碍的人都有帮助。
视觉与图像
对于视力障碍人士来说,数字化参与可能存在障碍,包括一些设计不当的网页无法与屏幕阅读器兼容,或图像内容缺乏描述性替代文本等。模型在识别图像中的物体或特征方面越来越熟练,如果能够广泛普及,这可能是一个极具价值的人工智能应用,屏幕阅读软件可以生成自己的替代文本或图像描述。
-
www.theverge.com/2022/3/18/22984474/microsoft-edge-automatic-image-labels-accessibility-feature -
人工智能计算机视觉和图像识别的应用(2022 年 1 月)
物理义肢
还有一些形式的人工智能,能够帮助义肢和身体辅助工具更好地工作。我并不是指使用神经植入物的技术,虽然这类技术正在研究中,但有许多模型通过学习人体运动的物理学,帮助计算机驱动的义肢更好地为人们服务。这些模型可以与肌肉和神经末梢相结合,或者它们可以巧妙地自动化某些动作,帮助上肢义肢改善诸如精细运动技能等问题。下肢义肢则可以利用人工智能更好地理解和生成步幅、流畅度等动作。
-
人工智能在义肢中的应用(2024 年 3 月)
否定
表现与抹除
好的,这只是人工智能可以为残障需求带来的其中一些积极作用。然而,我们也应该花一些时间讨论人工智能可能对残障人士和我们社会带来的负面影响。大多数这些问题都涉及使用人工智能的文化生产,我认为这些问题主要源于这些模型复制和强化了社会偏见和歧视。
举个例子:
-
由于我们的社会结构并未优先考虑或突出残障人士及其需求,模型也没有做到这一点。我们的社会充斥着能力主义,这种偏见在人工智能生成的文本中得到了体现。我们可以在提示工程中明确尝试纠正这一点,但很多人不会花时间去做这件事,或者根本没有意识到这一点。
-
同样,由人工智能模型生成的图像往往会抹去所有那些在文化上不占主导地位或在媒体中未得到优先展示的群体,包括残障人士。只要这些模型使用包含积极展示残障人士的训练数据,情况就会有所改善,但在展示比例是否符合现实与我们希望获得更多展示而不被抹去之间,总是存在一种天然的紧张关系。
数据隐私与伦理
这一领域有两个主要主题,对残障人士具有负面潜力。
-
首先,人工智能被用来对残障人士的需求和能力做出假设,存在很高的风险,可能会导致歧视。和任何群体一样,询问人工智能该群体可能偏好、需要或认为理想的事物,并不能代替让该群体参与决定那些将影响他们的事务。但人们往往会选择“直接问人工智能”,这是轻松且懒惰的做法,毫无疑问,这种情况时常发生。
-
其次,数据隐私是一个复杂的话题。具体来说,当某人使用无障碍技术时,比如手机或网页的屏幕阅读器,这可能会推断出该人是否有残障的状态。如果这些数据没有得到妥善保护,个人的残障状态,或如果推断错误时的感知状态,可能会成为一个潜在的负担,导致个人在其他领域面临歧视的风险。我们需要确保,无论某人是否在使用无障碍工具或功能,都应视为敏感的个人数据,正如对待他们的其他信息一样。
医疗治疗中的偏见
当医疗界开始在工作中使用人工智能时,我们应密切关注这对边缘群体,包括残障人士的副作用。与大型语言模型(LLM)使用可能导致残障人士的真实声音在重要决策中被忽视类似,如果医疗专业人员使用 LLM 来建议残障的诊断或治疗方案,这些建议将受到这些模型所带有的社会和文化负面偏见的影响。
这可能意味着非刻板印象或不常见的残障表现可能会被忽视或忽略,因为模型在理解异常和特殊情况时必然存在困难。这也可能意味着,当患者的实际经历与模型的预期或预测相反时,患者可能很难说服医疗提供者接受其经历。正如我在其他工作中讨论过的那样,人们可能对机器学习模型的准确性过于自信,而人类的观点可能会因此被视为不那么可信,尽管这并非一个可以证明的断言。
技术的获取
还有许多其他技术我没有时间在这里讨论,但我确实想指出,技术的存在本身并不等于残障人士能轻松、负担得起地访问并实际使用这些技术。残障人士往往在经济上处于不利地位,部分原因是经济参与的障碍,许多突破性的进展实际上并未对许多有需求的人群开放。这是一个我们社会需要承担责任的问题——尤其是在美国的医疗保健领域,我们在满足人们对护理和工具的需求方面做得非常糟糕,这些工具本应帮助人们过上更好的生活并参与经济活动。
结论
这只是对这个领域一些关键问题的简要回顾,我认为这对我们这些从事机器学习工作的人来说是一个重要的话题。我们构建的技术对边缘化群体,包括残障人士,有着双重影响——既有益处也有风险,我们的责任是要在工作中考虑到这些风险,并尽力减轻它们。
进一步阅读
slate.com/technology/2024/09/national-novel-writing-month-ai-bots-controversy.html
约翰·弗劳尔斯(Johnathan Flowers)是美国大学哲学与宗教系的教授讲师…
阅读我更多的作品,请访问www.stephaniekirmer.com。
发现 AWS Lambda 基础,运行强大的无服务器函数
学习我如何首次设置 AWS Lambda
·发布于 Towards Data Science ·阅读时长 10 分钟·2024 年 10 月 7 日
--
本文带你走过我如何开始使用 AWS Lambda 的过程。文章旨在展示如何设置 AWS Lambda 函数,并且展示我解决问题的方法,如何首次设置 Lambda 函数。希望这能帮助你了解如何处理计算机科学中的新问题,并找到解决方案。能够自己解决问题是编程中的一项关键技能,培养这项技能是本文的主要动机之一。

本教程将展示如何设置 AWS Lambda 函数,同时也会介绍我首次解决如何完成这一任务的问题的方式。图片由 ChatGPT 提供。
动机
我编写本教程以及未来类似教程的动机,是学习对我作为数据科学家来说非常重要的新概念。作为数据科学家,持续更新知识是至关重要的。我意识到自己对将函数部署到云端的理解有限,而这对于例如想要托管机器学习模型的人来说非常重要。因此,我决定学习如何使用 AWS Lambda 函数来部署这些函数。另一个本教程的动机是展示我的问题解决方法,因为我将要处理一个自己几乎没有预先了解的任务。本文应该会…
离散化解释:为初学者提供的带代码示例的可视化指南
数据预处理
6 种有趣的方法将数字分类到区间中!
·发布于 Towards Data Science ·阅读时长 10 分钟·2024 年 10 月 22 日
--

⛳️ 更多 [数据预处理](https://medium.com/@samybaladram/list/data-preprocessing-17a2c49b44e4) 解释:· 缺失值插补 · 分类编码 · 数据缩放 ▶ 离散化 · 过采样与欠采样 · 数据泄露与预处理
大多数机器学习模型要求数据为数值型——所有的对象或分类数据都必须首先转化为数值格式。但实际上,有时分类数据也非常有用(对我们人类来说,它往往比对机器更有用)。离散化(或分箱)正是实现这一点——将数值数据转换为分类数据!
根据你的目标,有很多方法可以将数据分类。在这里,我们将使用一个简单的数据集,通过六种不同的分箱方法来展示。从等宽分箱到基于聚类的方法,我们将把这些数值值划分到一些分类区间中!

所有视觉效果:作者使用 Canva Pro 创建。针对移动设备进行了优化;在桌面设备上可能会显得过大。
什么是离散化?
离散化,也称为分箱,是将连续数值变量转化为离散类别特征的过程。它通过将连续变量的范围划分为若干区间(箱),并根据数据点的值将其分配到相应的箱中。
为什么我们需要分箱?
-
处理异常值:分箱可以减少异常值的影响,而不需要删除数据点。
-
提高模型性能:某些算法在处理类别输入时表现更好(例如伯努利朴素贝叶斯)。
-
简化可视化:分箱后的数据通常更容易可视化和解读。
-
减少过拟合:它可以防止模型在高精度数据中拟合噪声。
哪些数据需要分箱?
常常受益于分箱的数据:
-
范围较广的连续变量:具有较大值区间的变量通常可以通过分组受益。
-
偏态分布:分箱可以帮助规范化高度偏斜的数据。
-
含有异常值的变量:分箱可以处理极端值的影响。
-
高基数数值数据:具有许多独特值的变量可以通过分箱进行简化。
通常不需要分箱的数据:
-
已经是类别数据:已经是离散类别的变量不需要进一步分箱。
-
唯一值较少的离散数值数据:如果一个变量只有少量可能的值,分箱可能不会提供额外的好处。
-
数字 ID 或代码:这些是用于唯一标识符的,而非分析用途。
-
时间序列数据:虽然可以对时间序列数据进行分箱,但通常需要专门的技术和谨慎的考虑,总体上不太常见。

数据集
为了演示这些分箱技术,我们将使用这个人工数据集。假设这是某高尔夫球场在 15 天内收集的天气情况。

紫外线指数(0–11 的范围)、湿度(以%为单位)、风速(以英里每小时为单位)、降水量(以毫米为单位)、温度(以华氏度为单位)、拥挤度(0(空)到 1(满))
import pandas as pd
import numpy as np
# Create the dataset as a dictionary
data = {
'UVIndex': [2, 10, 1, 7, 3, 9, 5, 11, 1, 8, 3, 9, 11, 5, 7],
'Humidity': [15, 95, 10, 98, 18, 90, 25, 80, 95, 40, 20, 30, 85, 92, 12],
'WindSpeed': [2, 90, 1, 30, 3, 10, 40, 5, 60, 15, 20, 45, 25, 35, 50],
'RainfallAmount': [5,2,7,3,18,3,0,1,25,0,9,0,18,7,0],
'Temperature': [68, 60, 63, 55, 50, 56, 57, 65, 66, 68, 71, 72, 79, 83, 81],
'Crowdedness': [0.15, 0.98, 0.1, 0.85, 0.2, 0.9, 0.92, 0.25, 0.12, 0.99, 0.2, 0.8, 0.05, 0.3, 0.95]
}
# Create a DataFrame from the dictionary
df = pd.DataFrame(data)
使用这个数据集,让我们看看如何将各种分箱技术应用于我们的列!
方法 1:等宽分箱
等宽分箱将变量的范围划分为指定数量的区间,每个区间具有相同的宽度。
常见数据类型:这种方法适用于数据大致呈均匀分布,且最小值和最大值有意义的情况。
在我们的案例中:让我们对紫外线指数变量应用等宽分箱法。我们将创建四个箱子:低、适中、高和非常高。我们选择这种方法是因为它能清晰直观地划分指数范围,这对于理解不同指数范围如何影响高尔夫决策非常有用。

# 1\. Equal-Width Binning for UVIndex
df['UVIndexBinned'] = pd.cut(df['UVIndex'], bins=4,
labels=['Low', 'Moderate', 'High', 'Very High'])
方法 2:等频分箱法(分位数分箱)
等频分箱法创建的箱子包含大致相同数量的观察值。
常见数据类型:这种方法对于偏斜数据或当你想确保各类别之间的平衡表示时非常有用。
在我们的案例中:让我们对湿度变量应用等频分箱法,创建三个箱子:低、中和高。我们选择这种方法是因为它确保每个类别中的观察值数量相等,这在湿度值在其范围内分布不均时非常有用。

# 2\. Equal-Frequency Binning for Humidity
df['HumidityBinned'] = pd.qcut(df['Humidity'], q=3,
labels=['Low', 'Medium', 'High'])
方法 3:自定义分箱
自定义分箱法允许你根据领域知识或特定要求定义自己的箱子边界。
常见数据类型:这种方法在你拥有领域中有意义的特定阈值,或当你想专注于特定值范围时非常理想。
在我们的案例中:让我们对降水量应用自定义分箱法。我们选择这种方法是因为雨水有标准化的分类(例如在这个网站中描述的),这些分类比任意的划分更有意义。

# 3\. Custom Binning for RainfallAmount
df['RainfallAmountBinned'] = pd.cut(df['RainfallAmount'], bins=[-np.inf, 2, 4, 12, np.inf],
labels=['No Rain', 'Drizzle', 'Rain', 'Heavy Rain'])
方法 4:对数分箱法
对数分箱法创建的箱子在大小上呈指数增长。该方法基本上首先应用对数转换,然后执行等宽分箱。
常见数据类型:这种方法对于跨越多个数量级或遵循幂律分布的数据尤其有用。
在我们的案例中:让我们对风速变量应用对数分箱法。我们选择这种方法是因为风对高尔夫球轨迹的影响可能不是线性的。从 0 到 5 英里每小时的变化可能比从 20 到 25 英里每小时的变化更为显著。

# 4\. Logarithmic Binning for WindSpeed
df['WindSpeedBinned'] = pd.cut(np.log1p(df['WindSpeed']), bins=3,
labels=['Light', 'Moderate', 'Strong'])
方法 5:基于标准差的分箱法
基于标准差的分箱法是根据与均值的标准差距离来创建箱子的。这种方法在处理正态分布数据时很有用,或者当你想根据数据值与集中趋势的偏离程度来分箱时也很有效。
变化:用于分箱的标准差数量可以根据分析的具体需求进行调整。分箱的数量通常为奇数(以便有一个中央箱)。某些实现可能使用不等宽度的箱子,靠近均值的箱子较窄,尾部的箱子较宽。
常见数据类型:该方法非常适合正态分布的数据,或者当你想识别异常值并理解数据分布时。对于高度偏斜的分布可能不太适用。
在我们的案例中:让我们将这种分箱方法应用到我们的“温度”变量上。我们选择这种方法是因为它可以根据温度偏离平均值的程度对温度进行分类,这在理解天气模式或气候趋势时特别有用。

# 5\. Standard Deviation-Based Binning for Temperature
mean_temp, std_dev = df['Temperature'].mean(), df['Temperature'].std()
bin_edges = [
float('-inf'), # Ensure all values are captured
mean_temp - 2.5 * std_dev,
mean_temp - 1.5 * std_dev,
mean_temp - 0.5 * std_dev,
mean_temp + 0.5 * std_dev,
mean_temp + 1.5 * std_dev,
mean_temp + 2.5 * std_dev,
float('inf') # Ensure all values are captured
]
df['TemperatureBinned'] = pd.cut(df['Temperature'], bins=bin_edges,
labels=['Very Low', 'Low', 'Below Avg', 'Average','Above Avg', 'High', 'Very High'])
方法 6:K 均值分箱
K 均值分箱使用 K 均值聚类算法来创建分箱。它根据数据点之间的相似性将数据点分成不同的簇,每个簇成为一个分箱。
常见数据类型:该方法非常适合发现数据中可能不显而易见的分组。它对具有一个或多个峰值的数据效果良好,并且能够根据数据的组织方式进行调整。
在我们的案例中:让我们将 K 均值分箱应用到我们的“拥挤度”变量上。我们选择这种方法是因为它可能揭示出高尔夫球场繁忙程度的自然分组,这些分组可能受许多因素的影响,而这些因素并未被简单的阈值分箱所捕捉。

# 6\. K-Means Binning for Crowdedness
kmeans = KMeans(n_clusters=3, random_state=42).fit(df[['Crowdedness']])
df['CrowdednessBinned'] = pd.Categorical.from_codes(kmeans.labels_, categories=['Low', 'Medium', 'High'])
结论
我们尝试了六种不同的方法来“离散化”我们高尔夫数据中的数字。所以,最终的数据集现在看起来是这样的:

# Print only the binned columns
binned_columns = [col for col in df.columns if col.endswith('Binned')]
print(df[binned_columns])
让我们回顾一下每种分箱技术如何转变了我们的天气数据:
-
等宽分箱(紫外线指数):将紫外线指数刻度分为四个相等的范围,按“低”到“非常高”分类。这提供了紫外线强度的直观解释。
-
等频分箱(湿度):将湿度读数分为“低”、“中”和“高”三个类别,每个类别包含相同数量的数据点。这种方法确保了湿度水平的均衡表示。
-
对数分箱(风速):应用于我们的风速数据,这种方法考虑了风速对天气条件的非线性影响,将风速分为“轻微”、“适中”或“强烈”。
-
自定义分箱(降水量):利用领域知识将降水量分类为有意义的类别,从“无雨”到“暴雨”。这种方法将测量结果直接转化为实际的天气描述。
-
基于标准差的分箱(温度):根据温度数据的分布进行分段,范围从“非常低”到“非常高”。这种方法突出了温度偏离平均值的程度。
-
K 均值分箱(拥挤度):在我们的拥挤度数据中显示了自然的分组,可能揭示了某些模式。
在应用分箱技术时,避免毫无思考的使用。每个变量的性质和你的分析目标始终是多样化的,选择分箱方法时要牢记这一点。在许多情况下,尝试多种技术并比较它们的结果能够为你的数据提供最多的见解!
⚠️ 分箱的风险
尽管执行分箱听起来很简单,但它也有其自身的风险:
-
信息丢失:当你对数据进行分箱时,本质上是将细节平滑化。这对于发现趋势非常有用,但你可能会错过在箱子内的细微模式或关系。
-
任意边界:分箱边界的选择有时可能更多是艺术而非科学。边界的轻微调整可能导致对数据的不同解释。
-
模型影响:某些模型,特别是基于树的模型,如决策树,在使用分箱数据时可能表现更差。它们在找到自己的“箱子”方面非常擅长。
-
虚假的安全感:分箱可以让你的数据看起来更整洁、更易管理,但潜在的复杂性依然存在,只是被隐藏了。
-
解释困难:虽然分箱可以简化分析,但它也可能使解释效果的大小变得更加困难。“高”温度在不同的上下文中可能意味着完全不同的事情。
那么,数据科学家该怎么做呢?以下是我的建议:
-
始终保留未分箱数据的副本。你可能需要重新查看它。
-
尝试不同的分箱策略并比较结果。不要满足于你尝试的第一个方法。
-
查找数据集领域是否已经有标准的方式来对数据进行分类(就像我们上面的“降水量”示例)。
🌟 离散化总结
import pandas as pd
import numpy as np
from sklearn.cluster import KMeans
# Create the dataset
data = {
'UVIndex': [2, 10, 1, 7, 3, 9, 5, 11, 1, 8, 3, 9, 11, 5, 7],
'Humidity': [15, 95, 10, 98, 18, 90, 25, 80, 95, 40, 20, 30, 85, 92, 12],
'WindSpeed': [2, 90, 1, 30, 3, 10, 40, 5, 60, 15, 20, 45, 25, 35, 50],
'RainfallAmount': [5,2,7,3,18,3,0,1,25,0,9,0,18,7,0],
'Temperature': [68, 60, 63, 55, 50, 56, 57, 65, 66, 68, 71, 72, 79, 83, 81],
'Crowdedness': [0.15, 0.98, 0.1, 0.85, 0.2, 0.9, 0.92, 0.25, 0.12, 0.99, 0.2, 0.8, 0.05, 0.3, 0.95]
}
# Create a DataFrame from the dictionary
df = pd.DataFrame(data)
# 1\. Equal-Width Binning for UVIndex
df['UVIndexBinned'] = pd.cut(df['UVIndex'], bins=4,
labels=['Low', 'Moderate', 'High', 'Very High'])
# 2\. Equal-Frequency Binning for Humidity
df['HumidityBinned'] = pd.qcut(df['Humidity'], q=3,
labels=['Low', 'Medium', 'High'])
# 3\. Custom Binning for RainfallAmount
df['RainfallAmountBinned'] = pd.cut(df['RainfallAmount'], bins=[-np.inf, 2, 4, 12, np.inf],
labels=['No Rain', 'Drizzle', 'Rain', 'Heavy Rain'])
# 4\. Logarithmic Binning for WindSpeed
df['WindSpeedBinned'] = pd.cut(np.log1p(df['WindSpeed']), bins=3,
labels=['Light', 'Moderate', 'Strong'])
# 5\. Standard Deviation-Based Binning for Temperature
mean_temp, std_dev = df['Temperature'].mean(), df['Temperature'].std()
bin_edges = [
float('-inf'), # Ensure all values are captured
mean_temp - 2.5 * std_dev,
mean_temp - 1.5 * std_dev,
mean_temp - 0.5 * std_dev,
mean_temp + 0.5 * std_dev,
mean_temp + 1.5 * std_dev,
mean_temp + 2.5 * std_dev,
float('inf') # Ensure all values are captured
]
df['TemperatureBinned'] = pd.cut(df['Temperature'], bins=bin_edges,
labels=['Very Low', 'Low', 'Below Avg', 'Average','Above Avg', 'High', 'Very High'])
# 6\. KMeans Binning for Crowdedness
kmeans = KMeans(n_clusters=3, random_state=42).fit(df[['Crowdedness']])
df['CrowdednessBinned'] = pd.Categorical.from_codes(kmeans.labels_, categories=['Low', 'Medium', 'High'])
# Print only the binned columns
binned_columns = [col for col in df.columns if col.endswith('Binned')]
print(df[binned_columns])
技术环境
本文使用 Python 3.7 和 scikit-learn 1.5 版本。虽然讨论的概念普遍适用,但不同版本的具体代码实现可能会略有不同。
关于插图
除非另有说明,所有插图均由作者创建,结合了 Canva Pro 的许可设计元素。
𝙎𝙚𝙚 𝙢𝙤𝙧𝙚 𝘿𝙖𝙩𝙖 𝙋𝙧𝙚𝙥𝙧𝙤𝙘𝙚𝙨𝙨𝙞𝙣𝙜 𝙢𝙚𝙩𝙝𝙤𝙙𝙨 𝙝𝙚𝙧𝙚:

数据预处理
查看列表6 篇故事


𝙔𝙤𝙪 𝙢𝙞𝙜𝙝𝙩 𝙖𝙡𝙨𝙤 𝙡𝙞𝙠𝙚:

分类算法
查看列表8 篇故事



回归算法
查看列表5 篇故事


解剖 Stockfish 第三部分:深入了解棋类引擎
大规模浏览棋局树
·发表于Towards Data Science ·阅读时间 5 分钟·2024 年 7 月 22 日
--

Stockfish 棋类引擎,背景照片由ᴊᴀᴄʜʏᴍ ᴍɪᴄʜᴀʟ提供,图片来自Unsplash
欢迎回到我们关于 Stockfish 棋类引擎内部运作的系列文章。本系列的目标是解释使 Stockfish 成为世界上最强大的棋类引擎之一的算法和技术。通过了解这些机制,我们可以深入理解计算机科学、人工智能和博弈论的交汇点。
本系列的前几部分探讨了 Stockfish 是如何找到一个可行的棋步(第一部分)并评估该棋步的局势质量(第二部分)。但如何考虑我们的对手接下来可能的棋步呢?我们又该如何回应?
为了应对这种情况,Stockfish 依赖于一个最终概念:深度。
所有棋步的树
游戏开始:兵到 e4。对手回应 e5。然后是 Nf3,Nc6,依此类推。这个序列在所有可能的棋步树中形成了一条分支。
Stockfish 浏览这个树状结构,以基于所有可能的结果确定最佳棋步。

高层概述 — 图片由作者提供
最坏的最优结果
在博弈论中,有一个适用于回合制游戏的通用算法:Minimax 算法。其核心在于,由于无法预测对手的棋步,因此假设对手总是为自己选择最好的棋步。
下面的图示展示了这一点:

国际象棋的 Minimax 算法。正分意味着白方占优,负分意味着黑方占优 — 图片由作者提供
-
对于主教到 G3这一步,Stockfish 会考虑对手的所有可能回应。
-
尽管某些棋步,如兵到 A7或A6,会得到正分,但车到 E8的棋步却导致了 -5.6 的不利局面。
-
这个最坏的结果使得主教到 G3的分数为 -5.6,因为假设对手(黑方)会找到并下出最佳的棋步。
-
计算完所有棋步和回应后,Stockfish 为自己(白方)选择了最佳的选项,即骑士到 D4。
尽管那个例子展示了如何使用单一棋步及对手的回应,Minimax 算法可以递归地扩展到无限深度。限制因素是计算资源,这也引出了下一部分。
优化与权衡
尽管 Stockfish 每秒可以评估数百万个棋步,但由于每增加一层深度,可能的棋步呈指数级增长,它仍然无法在合理的时间内评估整个棋局。
例如,克劳德·香农证明,要从起始位置达到 10 步的深度,需要评估 690 亿个位置。仅使用 Minimax 算法,这将需要几天时间。
Stockfish 对 Minimax 算法进行了多项改进。其中之一是Alpha-Beta 剪枝,它优化了移动树的遍历。如下图所示:

国际象棋中的 Alpha-Beta 剪枝 — 图片由作者提供
-
Stockfish 计算得出,序列车到 E8 -> 骑士到 G4 -> 主教到 G4将导致 -5 的不利局面。
-
另一序列车到 E8 -> 主教到 C6已经被探索过,并得出了 -1 的分数,这比当前正在探索的分支要好。
-
因此,骑士到 G4可以被舍弃,转而选择更好的选项:主教到 C6。
其他技术,如迭代加深,进一步提升了这个过程:当引擎在深度 N 上进行计算时,它会存储搜索的最佳线路,这样在深度 N+1 时,搜索时可以优先探索这些棋步。
Stockfish 的最终大局搜索算法非常复杂(见 search.cpp),但它利用了另一种现代计算技术:多线程。
分布式搜索
现代计算机可以使用多线程,从而使 Stockfish 能够利用分布式计算能力进行扩展。
为此,Stockfish 利用多个线程并行搜索最佳走法,每个线程通过并发内存存储系统进行通信。

使用共享字典的并行计算 — 图片由作者提供
计算线程并行搜索树。
-
当一个线程完成一个分支时,它将结果写入共享字典。
-
当一个线程开始一个新分支时,它会检查字典中是否有其他线程已经计算过该分支。
还有一个主线程充当协调者:
-
它从线程池中存储并启动计算线程。
-
它为每个计算线程提供初始条件(例如,搜索树时的顺序偏移,以增加搜索熵并更快地填充字典)。
-
它监视线程是否完成计算,若完成,则停止所有计算线程,并从字典中读取评估结果。
有趣的是,访问“真正的”并发字典时,内存锁所需的几纳秒会产生过多的开销。因此,Stockfish 的程序员开发了自己的分布式表格(参见tt.h)。
结论
总结:
-
Stockfish 为给定深度生成每一个候选走法。
-
它在树中使用各种优化方法评估这些走法。
-
它增加了评估深度,并重复这一过程。
通过这个系列,我们揭示了 Stockfish 如何将经典算法与现代计算技术和神经网络结合,实现最先进的性能。
理解 Stockfish 的内部工作原理,不仅能解开这款最强国际象棋引擎之一的神秘面纱,还能为计算和人工智能中的挑战及解决方案提供更广泛的见解。由于其固有特性,Stockfish 主要关注效率,这一主题在计算能力不断提升的背景下,已在人工智能领域变得越来越少见。此外,Stockfish 还是如何从 AI 核心构建一个完整的分布式系统的一个范例。
我希望这个系列对你有所启发并具有教育意义。感谢阅读!
若要进一步阅读,你可以查阅国际象棋编程百科,并加入计算机国际象棋俱乐部论坛,讨论国际象棋编程。
在 QGIS 和 Python 中溶解地图边界
本文描述了一些有趣的过程,通过使用 QGIS 和 Python 中的 geopandas 库来转换矢量数据集中的地图边界。
·发表于 Towards Data Science ·阅读时长 6 分钟·2024 年 5 月 4 日
--
最近,在我的一个项目中,我需要定义一个地区中各个国家的构成,使该地区与其他地区区分开,并在该大陆的地图上突出显示。以亚洲的南亚地区为例。南亚地区包括八个国家:阿富汗、孟加拉国、不丹、印度、马尔代夫、尼泊尔、巴基斯坦和斯里兰卡。从亚洲地图中,我想裁剪出南亚地区,并在该大陆地图上突出显示出来。此外,我还想进一步操作,溶解该地区内各个国家的边界,使该地区可以作为一个单一单元进行展示。
我通过使用量子 GIS(QGIS)和 Python 中的 geopandas 包成功实现了这一目标。在这个过程中,我利用了我已经熟悉的裁剪功能,同时我也学习了“溶解”功能,这个功能让我感到非常着迷。在这篇文章中,我将分享我对这些功能的学习,以及我是如何实现我的目标的。让我们开始吧。

图片来源:作者。
1. QGIS
在一个空的 QGIS 项目中,通过在页面底部的坐标空间中输入world,我可以调用一个内置的世界地图,显示所有国家的行政边界,如下所示。

在 QGIS 中获取世界地图。图片来源:作者。
接下来,通过使用选择功能,我选择了南亚的 8 个国家,如下图所示。QGIS 提供了手动选择、通过多边形、通过半径以及通过鼠标点击单独选择或取消选择国家的选项。

从世界地图中选择国家。图片由作者提供。
在 QGIS 中剪切
在 QGIS 中从世界地图中剪切这些国家非常简单。只需进入菜单中的“矢量”->选择地理处理工具->选择剪切。在选项中,我勾选了“仅选择输入图层中的选定特征”并运行了该过程。

运行剪切算法。图片由作者提供。
剪切操作仅用时 7.24 秒,我得到了一个名为“Clipped”的新图层。下图中用棕色表示。通过进入图层的属性,在 QGIS 的符号学选项中可以使用不同的着色选项。

新的剪切图层已创建。图片由作者提供。
在 QGIS 中溶解边界
接下来,我想要溶解南亚各国之间的边界。为此,我选择了南亚的所有国家。我进入矢量菜单->选择地理处理工具->溶解。与上一步类似,我选择了“仅选择输入图层中的选定特征”并运行了该算法,仅用了 0.08 秒。创建了一个新的名为“Dissolved”的图层,在其中各国的行政边界被溶解,呈现为一个单独的单位,如下所示:

新的溶解图层已创建。图片由作者提供。
同时可视化世界图层和溶解图层,效果如下所示:

溶解图层和世界图层。图片由作者提供。
2. Geopandas
在本节中,我将展示如何在 Python 中使用 geopandas 包实现相同的目标。
在第一步中,我读取了 geopandas 包内建的世界地图数据集。它包含了全世界的矢量数据以及所有国家的行政边界。这些数据来自于Natural Earth数据集,且可以免费使用。
import pandas as pd
import geopandas as gpd
import matplotlib.pyplot as plt
import numpy as np
world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
world.plot(color = "lightgrey")

在 geopandas 中绘制世界地图。图片由作者提供。
使用 geopandas 进行剪切
在我之前的文章中,我展示了如何从原始的 geopandas 数据框或图层中剪切出自定义的多边形几何形状作为掩模。然而,为了简便起见,我只是使用了过滤选项来获取亚洲和南亚所需的图层。
asia = world[world.continent == "Asia"]
asia.plot(color = "lightgrey")

从世界地图中过滤出亚洲大陆。作者提供的图片。
为了过滤出南亚地区,我使用了一个包含每个国家名称的列表作为参考。
south_asia_countries = ["Afghanistan", "Bangladesh", "Bhutan", "India",
"Maldives", "Nepal", "Pakistan", "Sri Lanka"]
south_asia = asia[asia.name.isin(south_asia_countries)]
south_asia.plot()

从亚洲中过滤出南亚地区。作者提供的图片。
使用 geopandas 在南亚国家之间溶解边界
为了溶解南亚国家之间的边界,我使用了 geopandas 中的dissolve功能。我将 None 作为参数传递,并指定了应用某些聚合函数的参数,其中在结果溶解的数据框中,人口和 GDP 将汇总南亚所有国家的人口和 GDP。我还需要弄清楚如何在 QGIS 中应用聚合函数。
south_asia_dissolved = south_asia.dissolve(by = None,
aggfunc = {"pop_est":"sum",
"gdp_md_est":"sum"})
south_asia_dissolved.plot(color = "lightgrey"

南亚国家之间的行政边界被溶解。作者提供的图片。
溶解世界各大陆内国家之间的边界
使用与上述相同的过程,我想要溶解大陆内国家之间的边界,并在世界地图上根据每个大陆中国家的数量展示不同的大陆。
为此,首先我在world地理数据框中添加了一个名为num_countries的新列,其值为 1。然后我使用continent列作为参考溶解了世界地图。
world["num_countries"] = 1
continents_dissolved = world.dissolve(by = "continent",
aggfunc = {"pop_est":"sum",
"gdp_md_est":"sum",
"num_countries":"count"}).reset_index()
continents_dissolved
我使用聚合函数汇总了大陆中所有国家的人口和 GDP,并计算了每个大陆中的国家数量。结果的地理数据框continents_dissolved如下所示:

最终的大陆溶解地理数据框。
我们看到亚洲拥有所有大陆中最多的人口和 GDP。同样,我们看到非洲拥有最多国家(51 个),其次是亚洲(47 个)、欧洲(39 个)、北美洲(18 个)、南美洲(13 个)和大洋洲(7 个)。在这个数据集中,南极洲和七海(开放海洋)也被视为大陆。
最后,我想绘制世界地图,突出显示每个大陆中国家的数量,借助颜色地图。我使用了以下代码实现:
map = continents_dissolved.plot(column = "num_countries",
cmap = "Greens")
# Get the current axes
ax = plt.gca()
# Add a horizontal colorbar
cbar = plt.colorbar(map.get_children()[0],
ax=ax,
orientation='horizontal',
aspect = 30 #control the width of color bar. higher value= lower width.
)
# Set a label for the colorbar
cbar.set_label('Number of Countries')
plt.title("Continents of the world based on number of countries")
plt.savefig("Continents dissolved.jpeg",
bbox_inches = "tight",
dpi = 300)
# Show the plot
plt.show()
结果地图如下所示:

世界地图,颜色反映了每个大陆中国家的数量。作者提供的图片。
结论
在这篇文章中,我描述了如何使用 QGIS 和 Python 中的 geopandas 溶解地图边界的方法。在这个过程中,我还解释了裁剪过程以及在 geopandas 中溶解地图边界时使用聚合函数的可能性。这些过程对于操纵、处理和转换地理地图数据集的形式非常有用。本文的代码和 QGIS 项目文件可在此 GitHub 存储库中找到。谢谢您的阅读!
距离度量学习用于异常值检测
一种异常值检测方法,旨在确定记录之间的相关距离度量
·发表于 Towards Data Science ·阅读时间:18 分钟·2024 年 8 月 20 日
--
异常值通常被定义为在数据集中与大多数其他项非常不同的项。也就是说,任何与其他所有记录(或几乎所有记录)显著不同,并且与其他记录的差异超过正常范围的记录,都可以合理地被视为异常值。

在这里展示的数据集中,我们有四个簇(A、B、C 和 D)和三个位于这些簇之外的点:P1、P2 和 P3。这些点很可能被视为异常值,因为它们每个都与其他所有点的距离较远——也就是说,它们与大多数其他点显著不同。
同样,A 类簇仅有五个点。尽管这些点相互之间距离较近,但它们与其他所有点的距离较远,因此也有可能被认为是异常值——再次强调,是基于这些点与其他大多数点之间的距离。
另一方面,内点(位于较大簇中的点)与许多其他点非常接近。例如,C 类簇中间的任何点都非常接近许多其他点(即与许多其他点非常相似),因此不会被视为异常值。
我们可以从多个角度来看待异常值,实际上也有许多方法用于异常值检测——例如基于频繁项集的异常值检测方法(Frequent Item Sets)、关联规则、压缩、马尔科夫模型等等。但识别与其他记录相似且与它们最相似的记录有相对不同之处的记录是非常常见的。实际上,这是许多最常见的异常值检测算法的基本思想,包括 kNN、LOF(局部异常因子)、半径算法以及许多其他算法。
然而,使用这种方法会留下一个问题,即如何量化一条记录与其他记录的差异。有多种技术可以实现这一点。在异常值检测中,一些最常见的包括欧几里得距离、曼哈顿距离和高尔距离,以及一些类似的度量方法。
我们将在下面简要介绍这些方法。但在本文中,我们将特别关注一种非常通用且可能使用较少的计算方法,用于计算表格数据中两条记录之间的差异,这对异常值检测非常有用,这就是距离度量学习——以及如何将此方法专门应用于异常值检测。
本文是关于异常值检测系列的继续,之前的文章包括计数异常值检测器、频繁模式异常值因子以及调节和测试检测器(使用一种称为doping的方法)。它还包括我书籍《Python 中的异常值检测》的另一个摘录。
距离度量
为了判断一条记录是否 1) 与大多数其他记录的距离异常远;以及 2) 与相对较少的记录接近,我们通常首先计算每对记录之间的距离:即数据集中每一对记录之间的距离。在实际应用中,我们可能采用更优化的方法(例如,只计算那些已知在任何情况下都相距很远的记录之间的近似距离),但至少在原则上,计算每对行之间的距离在异常值检测中是常见的做法。
这意味着,我们需要一种方法来计算任意两条记录之间的距离。
如果我们有一组数据,比如下面这样的大型员工记录表(这里显示的是四行的随机子集),我们如何最好地判断任意两行之间的相似度呢?

欧几里得距离
一种非常常见的方法是使用欧几里得距离。
在进一步查看员工数据之前,请再次考虑上面的散点图。我们在这里看到一个使用欧几里得距离显得很自然的情况。由于该数据集仅包含两个特征,且这两个特征都是数值型的,因此将数据如图所示绘制为散点图是相当直观的。一旦以这种方式绘制,我们自然就能想象出基于毕达哥拉斯公式计算出的点与点之间的欧几里得距离。
然而,在具有许多特征的情况下,尤其是这些特征中有很多是分类的,并且各列之间存在关联时,行与行之间的欧几里得距离,尽管仍然有效并且经常有用,但可能显得不那么自然。
使用欧几里得距离的一个问题是,它们实际上是为数值数据设计的,尽管大多数现实世界的数据,如员工记录,是混合型的:同时包含数值和分类特征。分类值可以通过数值编码(例如,使用独热编码、序数编码或其他编码方法)进行编码,从而可以计算欧几里得距离(以及其他数值距离度量)。但这并不总是理想的。每种数值编码方法对计算出的距离都有其特定的影响。不过,这种做法是完全可行的,也非常常见。
考虑到上面的员工表,我们可能会将 ID 和姓氏排除在离群值检测过程之外,仅使用其余的列。鉴于此,我们仍然会将部门和办公室特征视为分类特征。假设我们使用独热编码来对其进行编码。
为了计算行之间的欧几里得距离,我们还必须对数值特征进行缩放,将所有特征放置在相同的尺度上。这可以通过多种方式完成,包括标准化(将数值转换为其 z 值,基于该列均值与数值的标准差距离),或最小-最大缩放。
一旦数据被数值化并缩放,我们就可以计算每一对行之间的欧几里得距离。
高尔距离
或者,考虑到我们有一些分类特征,我们可以使用为混合数据设计的方法,例如高尔距离。该方法在比较任何两行时,逐列计算差异并将这些差异相加。当数据严格为数值时,它等同于曼哈顿距离。
对于分类列,在使用高尔距离时,通常使用序数编码,因为我们只关心是否存在精确匹配。分类列中两个值之间的差异要么是 0.0,要么是 1.0。在上面的员工表中,史密斯和琼斯在部门上有 1.0 的距离(不同值:‘工程’和‘销售’,此时总是使用 1.0),在办公室上有 0.0 的距离(相同值:‘多伦多’,此时总是使用 0.0)。
为了比较数值字段,就像欧几里得距离和大多数距离度量一样,我们需要先对它们进行缩放,以便所有数值字段可以被平等对待。如上所述,有多种方式可以实现这一点,但我们假设这里使用最小-最大缩放方法,将所有值映射到 0.0 到 1.0 的范围内。我们可能会得到一个如下表格:

使用高尔距离计算的史密斯和琼斯之间的差异将是:abs(0.90 — 0.20) + abs(0.93 — 0.34) + abs(0.74 — 0.78) + 1.0 + abs(0.88 — 0.77) + abs(0.54 — 0.49) + 0.0 + abs(0.32 — 0.38)。
也就是说,跳过 ID 和姓氏,我们计算每个数值字段的绝对差异,并为每个类别字段取 0.0 或 1.0。
这种方法可能是合理的,但也存在一些问题。最主要的问题可能在于类别字段比数值字段有更大的权重:类别字段通常会有 1.0 的差异,而数值字段则往往只有较小的差异。例如,史密斯和琼斯的年龄差距非常大,但它们的差异仅为 abs(0.93–0.34),即 0.59(尽管差异仍然显著,但小于部门字段对行之间总差异的 1.0 影响)。正如在Python 中的异常值检测中所提到的,一热编码和其他编码方式在使用其他距离度量时也存在处理混合数据的类似问题。
此外,所有类别特征在相互之间同等重要;所有数值特征在相互之间也同等重要,即便其中一些特征,如高度相关的特征,可能应当有更大或更小的权重。
一般来说,像欧几里得距离或高尔距离(以及其他距离度量如曼哈顿距离、堪培拉距离等)在许多情况下可能是合适的距离度量,且通常是异常值检测的优选方法。但与此同时,它们并不总是适用于所有项目。
欧几里得距离视为高维空间中的物理距离
再次查看欧几里得距离,这些基本上将记录视为高维空间中的点,并计算这些点之间的距离。曼哈顿距离和高尔距离略有不同,但原理非常相似。
作为一个比完整的员工表更简单的例子,考虑这个表格,但目前只包含数值特征:服务年限、年龄、薪水、年假天数、病假天数和最后一次奖金。这是六个特征,因此每一行可以视为 6 维空间中的一个点,点与点之间的距离使用毕达哥拉斯公式来计算。
这是合理的,但显然并不是唯一的看待距离的方式。而且,所使用的距离度量会对异常值评分产生实质性的影响。例如,欧几里得距离可能比曼哈顿距离更强调一些具有非常不同值的特征。
欧几里得和曼哈顿距离的例子
我们在这里考虑这组 6 维数据的两种不同情况(同时展示 ID 和姓氏列以供参考)。
首先,举个例子是对于两位员工 Greene 和 Thomas,其中大多数值相似,但服务年限却有很大差异:

其次,另一个例子是对于两位其他员工,Ford 和 Lee,他们的大多数值都适中地不同,但没有非常不同的值:

哪一对行最相似?使用曼哈顿距离时,Greene 和 Thomas 最相似(它们的距离是 0.59,相比之下 0.60)。使用欧几里得距离时,Ford 和 Lee 最相似(它们的距离是 0.27,相比之下是 0.50)。
在何时使用曼哈顿距离或欧几里得距离更合适,或者何时使用其他度量(例如 Canberra、Minkowski(例如使用立方距离)、Mahalanobis 等)更合适,通常并不明确。这不一定是一个问题,但它确实突显了我们可以用许多不同的方式来看待行与行之间的距离。
欧几里得距离特别意味着我们将数据视为高维空间中的点,并计算它们之间的物理距离。这确实有一定的价值,但并非总是完全自然的。仅仅查看如上所示的员工数据表,我们会将这些行(在这个例子中)看作是员工记录,而不是空间中的点。
此外,使用欧几里得距离需要取年龄的平方、工资的平方等等——这缺乏直观的吸引力。像年龄的平方这样的东西实际上是什么意思并不明确。它可以很好地工作,但数据的几何解释仅仅是我们可以想象数据的众多方式之一。
此外,它是一种通用方法,不考虑数据本身。
距离度量学习
距离度量学习提供了另一种思考如何判断两个记录相似度的问题的方式。它不是首先定义一个距离度量然后应用于当前数据,而是尝试从数据本身学习记录之间的相似度。
它还解决了欧几里得、曼哈顿和大多数其他距离度量的局限性:所有特征都被平等对待,无论这是否最为合适。
这里的想法是:一些特征比其他特征更为重要,且一些特征之间是相互关联的(在某些情况下,特征集合甚至可能是冗余的,或者几乎是冗余的)。简单地将每个特征视为相同并不一定是识别数据集中最异常记录的最佳方法。
距离度量学习本身是一个重要的领域,但在这里我将介绍一种它如何应用于异常值检测的方法。具体来说,我们将在这里讨论基于创建随机森林的异常值检测的距离度量学习应用。
暂时假设:
-
我们有一个预测某个目标的随机森林
-
我们有一张数据表,可以通过随机森林进行处理(例如,员工数据,但任何表格数据都可以)。
-
我们想要计算每一对行之间的距离。
我们将在这里使用这些成对的距离进行异常值检测,但原则上可以用于任何目的。
我们很快会描述如何为此创建一个随机森林,但暂时假设我们已经有一个随机森林,并且它的质量很好,经过良好的训练,且非常稳健。
我们可以用来估计行与行之间相似度的一种方法是查看随机森林所做的预测。假设随机森林被训练成一个二分类器,那么它可以为数据中的每一条记录生成预测的正类概率。
通过随机森林的两个记录可能有非常相似的概率,比如 0.615 和 0.619。这两个值非常接近,因此我们可以怀疑这两个记录彼此相似。但并不一定如此。它们实际上可能通过随机森林中的多个决策树走上完全不同的决策路径,恰好平均得出相似的预测。也就是说,它们可能因不同的原因得出相似的预测,实际上可能一点也不相似。
最相关的是记录通过决策树所走的决策路径。如果两条记录在大多数决策树中走的是相同的路径(因此最终落在相同的叶子节点),那么我们可以说它们是相似的(至少在这一方面)。如果它们大多数情况下落在不同的叶子节点,那么我们可以说它们是不同的。
那么,这就提供了一个强大的工具,以一种合理的方式确定任意两条记录的相似度。
创建随机森林
这显然是一个有用的想法,但它确实需要一个随机森林,并且需要一个对这个目的有意义的随机森林——即能够很好地捕捉到可用数据的特征的随机森林。
创建这样一个随机森林的一种方法是构建一个能够学习区分这些数据与类似但虚假的数据的随机森林。也就是说,数据是通过合成生成的,虽然与这些数据相似,但不完全相同(以便可以区分开来)。
因此,如果我们能够创建这样一组虚拟数据,我们就可以训练一个随机森林分类器来区分这两种数据类型。
创建用于此处的合成数据有多种方法,其中一些方法在Python 中的离群值检测一书中有详细介绍。例如,其中一种方法是加料(该方法也在这篇Medium 文章中有讨论)。不过,我们将在这里介绍另一种效果较好的方法。这种方法可能过于简单,并不总是像更复杂的技术那样有效,但它确实为这一概念提供了一个简单、直观的介绍。
在这里,我们生成与真实记录数量相等的合成记录。实际上,精确平衡的集合并不是必须的,在某些情况下,稍有不平衡可能反而效果更好,但出于简便考虑,本例使用了平衡的数据集。
我们逐行生成合成数据,并且每一行的生成过程都是逐个特征进行的。为了生成一个值,如果特征是类别型的,我们从真实数据中按其在真实数据中的分布概率选择一个值。例如,如果真实数据中有一列“颜色”,其中包含 450 行红色、650 行蓝色、110 行绿色和 385 行黄色,那么按比例计算,这些颜色的比例为:红色:0.28,蓝色:0.41,绿色:0.07,黄色:0.24。因此,合成数据中的这一列将按类似的比例生成新的值。
如果特征是数值型的,我们计算该特征在真实数据中的均值和标准差,并从具有这些参数的正态分布中随机选择一组值。还有许多其他方法可以考虑,但同样,这只是一个简单的入门介绍。
通过这种方式,我们生成了合成数据,其中每一行完全由现实的值组成(每一行可能包含类别列中的稀有值,以及数值列中的稀有或极端值——但它们都是合理且现实的值)。
但是,特征之间的正常关系并未得到遵守。也就是说,由于每个列值是独立生成的,因此生成的值的组合可能不现实。例如,如果创建合成数据以模拟上面的员工表,我们可能会生成假记录,其年龄为 23 岁,服务年限为 38 年。单独来看,这两个值是现实的,但它们的组合是没有意义的,因此,在真实数据中应该不会出现这种组合——因此可以与真实数据区分开来。
数值字段的合成数据可以使用如下的代码(Python)生成:
real_df['Real'] = True
synth_df = pd.DataFrame()
for col_name in real_df.columns:
mean = real_df[col_name].mean()
stddev = real_df[col_name].std()
synth_df[col_name] = np.random.normal(
loc=mean, scale=stddev, size=len(real_df))
synth_df['Real'] = False
train_df = pd.concat([real_df, synth_df])
在这里,我们假设数据框real_df包含真实数据。然后我们创建一个名为synth_df的第二个数据框,接着将两个数据框合并为train_df,该数据框可以用于训练随机森林以区分两者。
类别数据可以类似地生成:
for col_name in real_df.columns:
vc = real_df[col_name].value_counts(normalize=True)
synth_df[col_name] = np.random.choice(a=vc.keys().tolist(),
size=len(real_df),
replace=True,
p=vc.values.tolist())
如前所述,这只是生成数据的一种方式,调整这个过程可能会有用,允许更多不寻常的单个值,或限制特征之间不太常见的关系。
一旦这些数据被创建,我们可以训练一个随机森林来学习区分真实数据和假数据。
一旦完成这个过程,我们实际上还可以执行另一种形式的异常值检测。任何通过随机森林的真实记录,如果它被预测为假记录,可能被认为是异常的——它们与合成数据比与真实数据更相似。这部分内容在Python 中的异常值检测中有详细讲解,但在本文中,我们将重点关注距离度量学习,因此我们将关注随机森林中的决策路径(而不是最终的预测)。
使用随机森林衡量异常度
如上所述,如果两个记录趋向于最终落在几乎完全不同的叶节点上,它们可以在某种意义上被认为是不同的。
对于每一对记录,我们可以统计它们在随机森林中落在相同叶节点和不同叶节点的树木数量。但我们也可以使用一种更简单的方法。对于每个通过随机森林的记录,对于每棵树,我们可以看到它最终的(叶)节点是什么。我们还可以看到训练数据中有多少记录落在该节点上。训练记录越少,这条路径就越不寻常。
如果在大多数树中,一个记录最终落在与其他很少的记录相同的叶节点上,它可以被认为是异常的。
主要的思路是:如果随机森林准确,它能够很好地区分真实记录和假记录。因此,当一个真实记录通过随机森林时,它很可能会落在与真实数据相关的叶节点上。如果它是一个正常的真实记录,它将沿着一个常见的路径前进,这是许多其他真实记录所使用的路径。在路径的每一步,决策树中的节点会基于一个特征进行分裂——这是一个在区分真实数据和合成数据时有效的特征及其分裂点。一个典型的记录将具有与常见真实数据相关的值,因此会在每个分裂点沿着与真实数据相关的路径前进。
如果一个随机森林只包含少量的树,则每个记录最终落在的叶节点的大小可能会非常随意。但是,随机森林可以设置为包含数百棵或数千棵树。当记录始终落在其树木中不常见的叶节点时,该记录可以合理地被认为是异常的。
即使使用大型随机森林,过程仍然可能存在一定的变异性。为了解决这个问题,可以使用多个距离度量学习异常检测器,并将它们组合成一个集成方法,而不是仅使用一个单独的检测器。这个方法超出了本文的范围,但其基本思路是创建多个合成数据集,并为每个数据集创建多个不同超参数的随机森林,然后将结果进行平均。
示例
为了演示这个思路,我们将创建一个简单的距离度量学习检测器。
但首先,我们将创建几个测试数据集。这些数据集都是具有两个特征的数值型数据集。如所示,这比具有多个特征的数据集和包含多个分类特征的数据集更不具备现实性,但它对于演示目的很有用——它易于绘制和理解。
第一个测试集是一个单一的数据集群:
import numpy as np
import pandas as pd
def create_simple_testdata():
np.random.seed(0)
a_data = np.random.normal(size=100)
b_data = np.random.normal(size=100)
df = pd.DataFrame({"A": a_data, "B": b_data})
return df
第二个数据集实际上创建了文章开始时显示的数据集,包含四个集群和三个位于这些集群外的点。
def create_four_clusters_test_data():
np.random.seed(0)
a_data = np.random.normal(loc=25.0, scale=2.0, size=5)
b_data = np.random.normal(loc=4.0, scale=2.0, size=5)
df0 = pd.DataFrame({"A": a_data, "B": b_data})
a_data = np.random.normal(loc=1.0, scale=2.0, size=50)
b_data = np.random.normal(loc=19.0, scale=2.0, size=50)
df1 = pd.DataFrame({"A": a_data, "B": b_data})
a_data = np.random.normal(loc=1.0, scale=1.0, size=200)
b_data = np.random.normal(loc=1.0, scale=1.0, size=200)
df2 = pd.DataFrame({"A": a_data, "B": b_data})
a_data = np.random.normal(loc=20.0, scale=3.0, size=500)
b_data = np.random.normal(loc=13.0, scale=3.0, size=500) + a_data
df3 = pd.DataFrame({"A": a_data, "B": b_data})
outliers = [[5.0, 40],
[1.5, 8.0],
[11.0, 0.5]]
df4 = pd.DataFrame(outliers, columns=['A', 'B'])
df = pd.concat([df0, df1, df2, df3, df4])
df = df.reset_index(drop=True)
return df
这里显示了两个数据集:

接下来,我们展示了一个基于距离度量学习的简单异常检测器。该检测器的 fit_predict() 方法接受一个数据框(在其中我们识别任何异常值)。fit_predict() 方法生成一个合成数据集,训练一个随机森林,将每条记录传递给随机森林,确定每条记录最终进入哪个节点,并确定这些节点的常见程度。
from sklearn.ensemble import RandomForestClassifier
from collections import Counter
from sklearn.preprocessing import RobustScaler
class DMLOutlierDetection:
def __init__(self):
pass
def fit_predict(self, df):
real_df = df.copy()
real_df['Real'] = True
# Generate synthetic data that is similar to the real data
# For simplicity, this covers just the numeric case.
synth_df = pd.DataFrame()
for col_name in df.columns:
mean = df[col_name].mean()
stddev = df[col_name].std()
synth_df[col_name] = np.random.normal(loc=mean,
scale=stddev, size=len(df))
synth_df['Real'] = False
train_df = pd.concat([real_df, synth_df])
clf = RandomForestClassifier(max_depth=5)
clf.fit(train_df.drop(columns=['Real']), train_df['Real'])
# Get the leaf node each record ends in
r = clf.apply(df)
# Initialize the score for all records to 0
scores = [0]*len(df)
# Loop through each tree in the Random Forest
for tree_idx in range(len(r[0])):
# Get the count of each leaf node
c = Counter(r[:, tree_idx])
# Loop through each record and update its score based
# on the frequency of the node it ends in
for record_idx in range(len(df)):
node_idx = r[record_idx, tree_idx]
node_count = c[node_idx]
scores[record_idx] += len(df) - node_count
return scores
df = create_four_clusters_test_data()
df = pd.DataFrame(RobustScaler().fit_transform(df), columns=df.columns)
clf = DMLOutlierDetection()
df['Scores'] = clf.fit_predict(df)
这个代码示例仅在 create_four_clusters_test_data() 创建的数据上运行,但也可以使用 create_simple_testdata() 中的数据。
结果可以通过如下代码进行可视化:
import matplotlib.pyplot as plt
import seaborn as sns
sns.scatterplot(x=df["A"], y=df['B'], hue=df['Scores'])
plt.show()
两个测试数据集的结果如下所示,绘制了原始数据,但根据其异常分数(由上面代码中的“Scores”列放置)设置了色调。

在左侧的数据集(单一集群)中,最外围的点获得了最高的得分,这是预期的结果。在右侧的数据集(包含四个集群)中,最高的异常分数分配给了三个位于集群外的点,较小的集群,以及位于最大集群边缘的点。这是相当合理的,尽管其他检测器可能会对这些点进行不同的评分,而且同样是合理的。
如上所述,使用欧几里得距离对于这些数据集来说是自然的,尽管对于具有许多特征、分类特征、特征之间关联以及其他数据细节的数据集可能不那么适用。但即使在这些欧几里得距离表现良好的简单情况中,距离度量学习也能发挥良好的效果,并提供一种自然的异常检测方法。对于更复杂的数据,这种情况可能会更加明显。
结论
距离度量学习可以用于离群点检测以外的许多用途,甚至在离群点检测中,也可以以多种方式使用。例如,可以像上面一样使用随机森林计算数据集中的成对距离,并将这些距离传递给另一个算法。例如,DBSCAN 提供了一个“预计算”选项,可以传递预先计算好的成对距离矩阵;然后,可以使用 DBSCAN(或类似的聚类方法,如 HDBSCAN)作为几种可能的基于聚类的离群点检测算法之一。
并且,距离度量学习也可以像本文中所示的那样以更直接的方式使用,这本身就是一种出色的离群点检测方法。在许多情况下,它比基于欧几里得距离、曼哈顿距离、Gower 距离或其他类似距离度量的方法更有利于检测离群点。它还可以为检测器的集成提供多样性,即使这些方法也能很好地工作。
没有任何一种离群点检测方法是绝对的,通常需要在任何给定项目中使用多种离群点检测方法(包括通常情况下,使用相同方法多次,但使用不同的参数),将它们的结果结合起来,以实现强大的整体离群点检测。
因此,距离度量学习并不适用于每个项目,但在适用的情况下,它可能(与任何检测器一样)在与其他检测器结合时表现最好。但这是一种有价值的工具;距离度量学习可以是一个非常有效的离群点检测技术,尽管它比其他方法获得的关注要少。
这确实需要一些调优,包括合成数据的生成方式和随机森林使用的超参数,但一旦调优完成,它提供了一种强大且直观的离群点检测方法。
所有图片均由作者提供
神经网络的分布式去中心化训练:入门指南
·发表于Towards Data Science ·9 分钟阅读·2024 年 11 月 19 日
--
随着人工智能的发展,训练大规模神经网络,包括大语言模型,变得愈加重要。这些模型的规模和复杂度不断增长,不仅提高了训练所需的成本和能源要求,也凸显了有效硬件利用的必要性。为应对这些挑战,研究人员和工程师正在探索分布式去中心化训练策略。在这篇博客文章中,我们将探讨各种分布式训练方法,例如数据并行训练和基于 gossip 的平均算法,以说明这些方法如何在应对日益增加的领域需求的同时,优化模型训练效率。

一种极简的日式风格 GPU 集群图,图中添加了更多的小型 GPU。(由 OpenAI 的 Dallé-3 API 生成)
数据并行性、All-Reduce 操作和同步性
数据并行训练是一种技术,涉及将数据的小批量分配到多个设备(工作节点)上。这种方法不仅使多个工作节点能够同时计算梯度,从而提高训练速度,还可以…
requirements.txt 已过时
使用 Poetry 管理 Python 项目的依赖关系和元数据
·发表于 Towards Data Science ·阅读时间 9 分钟·2024 年 8 月 6 日
--

图片来源:Pawel Czerwinski 在 Unsplash
Python 的 标准库 是一组内置模块,提供了丰富的功能,旨在标准化日常编程中的基本操作。一些示例包括 I/O 操作、文本处理、文件压缩 和 数学 操作等。这个庞大的库使得开发者无需安装额外的包,就能执行各种任务。
然而,尽管标准库功能强大,现代 Python 应用程序通常需要更多高级的功能,超出内置内容的范畴。这时,庞大的 开源 项目 库就显得至关重要。
由各个团队和社区——甚至个人——管理的这些包,能够显著扩展你项目的功能,避免了重复发明轮子。
这些依赖项可以在 Python 包索引(PyPI)上找到,任何开发者都可以轻松安装。从像 Django 和 Flask 这样的 Web 开发框架,到像 Pandas 和 Scikit-learn 这样的数据科学库,这些包已经成为日常 Python 编程的核心部分。
深入探讨 AutoGen 与多智能体框架
本文将深入探讨《AutoGen: 通过多智能体对话实现下一代大型语言模型应用》论文以及 AutoGen 项目的细节
·发布于 Towards Data Science ·11 分钟阅读·2024 年 6 月 28 日
--

作者图片 — SDXL
近期,智能体成为技术新闻网站的热议话题。尽管人们对这些程序的潜力抱有很大期望,但关于应该支持这些智能体的框架却鲜有讨论。从高层次来看,智能体不过是一个程序,通常由大型语言模型(LLM)驱动,执行某种操作。虽然任何人都可以向 LLM 发出提示,但智能体系统的关键区别在于它们在面对模糊任务时的持续表现。
实现这种持续的表现并非易事。尽管像思维链、反思等提示技术已被证明能提高 LLM 的表现,但 LLM 在聊天过程中接受适当反馈时,往往会显著改善。这可能表现为科学家指出聊天机器人回答中的缺陷,或程序员在尝试运行 LLM 代码时复制编译器消息。
因此,在我们努力使这些智能体系统的表现更加稳定时,人们可能会合理地问,是否可以找到一种方法,让多个 LLM 相互反馈,从而……
深入探索结构化输出
帮助增强您对结构化输出和 LLM 的理解与最佳使用
·发布于数据科学前沿 ·阅读时长 8 分钟·2024 年 9 月 3 日
--

图 1 — 从用户的角度,在应用结构化输出时,显式和隐式执行的步骤;图片来自作者
在上一篇文章中,我们介绍了如何使用 OpenAI 的结构化输出。自从 ChatCompletions API(v1.40.0)的正式发布以来,结构化输出已被应用于数十种使用场景,并在OpenAI 论坛中引发了大量讨论。
在本文中,我们的目标是为您提供更深入的理解,消除一些误解,并为您提供一些关于如何在不同场景中以最优化的方式应用它们的建议。
结构化输出概述
结构化输出是一种强制 LLM 输出遵循预定义模式的方式——通常是 JSON 模式。这是通过将模式转换为上下文无关文法 (CFG),在标记采样步骤中与之前生成的标记一起使用,以确定哪些后续标记是有效的。可以将其理解为为标记生成创建一个正则表达式。
OpenAI API 实现实际上仅跟踪 JSON schema 特性的一个有限子集。对于更一般的结构化输出解决方案,比如 Outlines,可以使用稍大一些的 JSON schema 子集,甚至可以定义完全自定义的非 JSON schema —— 只要你能访问到开放权重模型。本文将假设使用 OpenAI API 实现。
JSON Schema 和 Pydantic
根据 JSON Schema 核心规范,“JSON Schema 确定了一个 JSON 文档必须是什么样子,如何从中提取信息,以及如何与之互动”。JSON schema 定义了六种原始类型 —— null、boolean、object、array、number 和 string。它还定义了一些关键字、注解和特定的行为。例如,我们可以在 schema 中指定我们期望的是一个 array,并添加一个注解,要求 minItems 为 5。
Pydantic 是一个实现 JSON schema 规范的 Python 库。我们使用 Pydantic 在 Python 中构建稳健且可维护的软件。由于 Python 是一种动态类型语言,数据科学家通常不会以 变量类型 为中心思考 —— 这些通常是 隐含 在他们的代码中的。例如,一个水果可能会被指定为:
fruit = dict(
name="apple",
color="red",
weight=4.2
)
…而一个返回“水果”的函数声明,通常会被指定为:
def extract_fruit(s):
...
return fruit
另一方面,Pydantic 允许我们生成一个符合 JSON schema 规范的类,具有正确注解的变量和 类型提示,使得我们的代码更具可读性/可维护性,并且通常更加稳健,即:
class Fruit(BaseModel):
name: str
color: Literal['red', 'green']
weight: Annotated[float, Gt(0)]
def extract_fruit(s: str) -> Fruit:
...
return fruit
OpenAI 实际上 强烈推荐 使用 Pydantic 来指定 schema,而不是直接指定“原始” JSON schema。这样做有几个原因。首先,Pydantic 保证遵循 JSON schema 规范,因此它可以为你省去额外的预验证步骤。其次,对于较大的 schema,它更加简洁,让你可以写出更清晰、更快速的代码。最后,openai Python 包实际上会做一些“家务工作”,比如帮你将 additionalProperties 设置为 False,而当你使用 JSON 手动定义 schema 时,你需要为 schema 中的每个对象 手动设置这些,如果不设置,将会导致相当烦人的 API 错误。
限制
正如我们之前提到的,ChatCompletions API 提供了一个有限的完整 JSON 模式规范子集。有许多当前不支持的关键字,例如用于数字的minimum和maximum,以及用于数组的minItems和maxItems——这些注解本来会在减少幻觉或限制输出大小时非常有用。
某些格式化特性也不可用。例如,以下 Pydantic 模式在传递给 ChatCompletions 中的response_format时会导致 API 错误:
class NewsArticle(BaseModel):
headline: str
subheading: str
authors: List[str]
date_published: datetime = Field(None, description="Date when article was published. Use ISO 8601 date format.")
它会失败,因为openai包没有处理datetime格式的功能,因此你需要将date_published设置为str类型,并在后期进行格式验证(例如,ISO 8601 合规性)。
其他关键限制包括:
-
仍然可能出现幻觉——例如,在提取产品 ID 时,你需要在响应模式中定义以下内容:
product_ids: List[str];虽然输出保证会生成一个字符串列表(产品 ID),但这些字符串本身可能是幻觉,因此在这种使用场景下,你可能需要将输出与一些预定义的产品 ID 集合进行验证。 -
输出有上限,最多为 16,384 个令牌(注意:感谢 Peter Edmonds 的纠正!),或者你在
max_tokens参数中设置的较小值——因此,尽管模式会被精确遵循,但如果输出过大,它将被截断并产生无效的 JSON——在非常大的批量 API任务中尤其令人烦恼! -
深度嵌套的模式及其多个对象属性可能会导致 API 错误——你的模式有深度和宽度的限制,但通常最好坚持使用扁平和简单的结构——不仅是为了避免 API 错误,还为了从 LLM 中尽可能地提取更多性能(通常 LLM 难以处理深度嵌套的结构)。
-
高度动态或任意模式不可行——尽管支持递归,但无法创建一个高度动态的模式,例如一组任意的键值对对象列表,即
[{"key1": "val1"}, {"key2": "val2"}, ..., {"keyN": "valN"}],因为在这种情况下,“键”必须是预定义的;在这种场景下,最佳选择是根本不使用结构化输出,而是选择标准的 JSON 模式,并在系统提示中提供输出结构的说明。
技巧和窍门
考虑到这一切,我们现在可以通过几个使用案例来探讨如何在使用结构化输出时提升性能的技巧和窍门。
使用可选参数创建灵活性
假设我们正在构建一个网页抓取应用程序,目标是从网页中收集特定组件。对于每个网页,我们在用户提示中提供原始 HTML,在系统提示中给出具体的抓取指令,并定义以下 Pydantic 模型:
class Webpage(BaseModel):
title: str
paragraphs: Optional[List[str]] = Field(None, description="Text contents enclosed within <p></p> tags.")
links: Optional[List[str]] = Field(None, description="URLs specified by `href` field within <a></a> tags.")
images: Optional[List[str]] = Field(None, description="URLs specified by the `src` field within the <img></img> tags.")
然后我们会按如下方式调用 API……
response = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{
"role": "system",
"content": "You are to parse HTML and return the parsed page components."
},
{
"role": "user",
"content": """
<html>
<title>Structured Outputs Demo</title>
<body>
<img src="test.gif"></image>
<p>Hello world!</p>
</body>
</html>
"""
}
],
response_format=Webpage
)
…以及以下响应:
{
'images': ['test.gif'],
'links': None,
'paragraphs': ['Hello world!'],
'title': 'Structured Outputs Demo'
}
提供给 API 的响应架构使用结构化输出时,必须返回所有指定的字段。然而,我们可以通过使用Optional类型注解来“模拟”可选字段,并增加更多的灵活性。我们实际上还可以使用Union[List[str], None]——它们在语法上是完全相同的。在这两种情况下,按照 JSON 架构规范,我们都会得到转换为anyOf关键字。在上面的示例中,由于网页上没有<a></a>标签,API 仍然返回了links字段,但它被设置为None。
使用枚举和两阶段方法减少虚构内容
我们之前提到过,即使 LLM 被保证遵循提供的响应架构,它仍然可能会虚构实际的值。对此,最近的一篇论文发现,强制对输出施加固定架构,实际上会导致 LLM 出现虚构内容或在推理能力方面退化(有趣的是,分类性能却有所提高 🤔)。
克服这些限制的一种方法是尽可能多地利用枚举类型(enums)。枚举将输出限制为非常特定的一组标记,对其他任何东西的概率设为零。例如,假设你正在尝试对一个目标产品(包含description和唯一的product_id)与通过某种向量相似性搜索(例如使用余弦距离度量)获得的前五名产品之间的产品相似度进行重新排序。每一个前五名产品也包含相应的文本描述和唯一的 ID。在你的响应中,你只希望获取重新排序后的 1-5 列表(例如[1, 4, 3, 5, 2]),而不是获取可能是虚构或无效的重新排序的产品 ID 字符串。我们将我们的 Pydantic 模型设置如下……
class Rank(IntEnum):
RANK_1 = 1
RANK_2 = 2
RANK_3 = 3
RANK_4 = 4
RANK_5 = 5
class RerankingResult(BaseModel):
ordered_ranking: List[Rank] = Field(description="Provides ordered ranking 1-5.")
…并像这样运行 API:
response = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{
"role": "system",
"content": """
You are to rank the similarity of the candidate products against the target product.
Ranking should be orderly, from the most similar, to the least similar.
"""
},
{
"role": "user",
"content": """
## Target Product
Product ID: X56HHGHH
Product Description: 80" Samsung LED TV
## Candidate Products
Product ID: 125GHJJJGH
Product Description: NVIDIA RTX 4060 GPU
Product ID: 76876876GHJ
Product Description: Sony Walkman
Product ID: 433FGHHGG
Product Description: Sony LED TV 56"
Product ID: 777888887888
Product Description: Blueray Sony Player
Product ID: JGHHJGJ56
Product Description: BenQ PC Monitor 37" 4K UHD
"""
}
],
response_format=RerankingResult
)
最终结果就是:
{'ordered_ranking': [3, 5, 1, 4, 2]}
所以 LLM 排名了 Sony LED TV(即列表中的第“3”项)和 BenQ PC 显示器(即列表中的第“5”项),作为最相似的两个产品候选,即ordered_ranking列表中的前两个元素!
理论上,枚举应该完全消除这些特定字段中的虚构内容,因为只有枚举集中的标记会通过标记掩码,即所有其他标记的概率为零。然而,用户也报告过即使在枚举中也会看到虚构内容,特别是在“迷你”模型中。
另一种方法是两阶段方法,这与前述论文的研究结果一致:
-
向迷你模型发送推理/提取任务不强制执行结构,即响应将是一个简单的字符串。
-
创建第二个请求发送给迷你模型,这次发送上一步的输出以及响应模式
通过这种方法,我们将任务分为推理步骤和结构化步骤。
结论
在本文中,我们深入探讨了结构化输出。我们介绍了 JSON 模式和 Pydantic 模型,并将它们与 OpenAI 的 ChatCompletions API 连接。我们通过多个示例展示了如何使用结构化输出解决这些问题。总结一些关键要点:
-
OpenAI API 及其他第三方框架所支持的结构化输出,仅实现JSON 模式规范的子集——了解其特性和局限性将帮助你做出正确的设计决策。
-
强烈推荐使用Pydantic或类似的框架,这些框架能够忠实地跟踪 JSON 模式规范,因为它们使你能够创建有效且更清晰的代码。
-
尽管仍然会出现幻觉问题,但有多种方法可以缓解这些问题,方法之一是选择响应模式设计;例如,在适当的地方使用枚举类型;或者采用两阶段方法,即我们发送两个 API 请求——一个用于推理,第二个仅用于输出重构。
关于作者
Armin Catovic是斯德哥尔摩 AI的董事会秘书,同时也是EQT 集团的副总裁和高级 ML/AI 工程师,拥有 18 年的工程经验,工作遍及澳大利亚、东南亚、欧洲和美国,并拥有多项专利和顶级同行评审的 AI 出版物。
欧洲的 M&M 巧克力真的比美国的 M&M 巧克力更好吃吗?
一次过度热情地应用科学和数据可视化来解答我们所有人都曾问过的问题
·发表于Towards Data Science ·16 分钟阅读·2024 年 1 月 23 日
--

一张特别甜美的箱型图。图像来源:作者。
(哦,只有我在问这个问题吗…?嗯。如果你有一分钟时间,请享受这篇探索性的数据分析——涵盖实验设计、统计学和互动可视化——虽然有点过于认真,但仍然应用于解决一场国际争论。)
1. 引言
1.1 背景与动机
巧克力在世界各地都受到喜爱。从古代在亚马逊盆地采摘有机可可的传统,到瑞士山脉中的巧克力工匠雕刻食用艺术,再到美国宾夕法尼亚州赫尔希市的巨大工厂每天生产 7000 万个巧克力“亲吻”,巧克力的多样化形式和风味已经融入了许多文化和习俗中。尽管巧克力产品的质量差异很大,但一种广为人知、常见且易于分享的巧克力形式便是 M&M 巧克力。它们常出现在便利店的收银台和酒店的自动售货机中,五颜六色的小圆粒是大家喜爱的零食,其包装设计几乎能适应任何美国商业节日的需求。
2022 年,我在丹麦生活时听到一个令人担忧的说法:欧洲生产的 M&M 口味与美国生产的 M&M 不同,甚至可以说是“更好”。虽然我承认欧洲的精美巧克力确实非常美味,并且通常优于美国巧克力,但我不确定这个说法是否同样适用于 M&M。我得知许多欧洲人认为美国巧克力有一种“不愉快”或“酸味”的味道,这主要归因于丁酸,这种化合物是由于牛奶在加入牛奶巧克力之前的处理方式不同所产生的。
但说实话,这种差异对 M&M 来说能有多大影响呢?M&M!?我想象中,不论在哪里生产,M&M 都会保留相对加工/大规模生产/廉价糖果的味道。作为唯一一位访问一个由国际科学家组成的多元化实验室,进行前沿生物可持续性研究的美国人,我决定拿出我的数据科学工具箱,调查这个 M&M 口味现象。
1.2 以往的研究
引用一位欧洲女性的话,她在纽约旅行时品尝了一颗美国的 M&M 后说道:
“它们尝起来真恶心,像呕吐一样。我不明白人们怎么能吃这个。我把袋子里的剩下部分都扔掉了。”
呕吐?真的吗?根据我的经验,在美国长大的孩子们吃 M&M 时完全没有任何顾虑。小时候,我家里总会在高流量区域放置装满 M&M 的碗,以便随时提供糖分。显然,美国的 M&M 是可以吃的。那么,它们真的与欧洲版的 M&M 有显著不同,或者说更差吗?
为了回应那位匿名欧洲女性的尖锐评论,我和另外两位在丹麦的美国游客一起品尝了在 Lyngby Storcenter Føtex 购买的 M&M。我们希望能体验到隐藏在我们童年中的 M&M 口味的巨大改善。但奇怪的是,我们没有察觉到明显的口味改善。
不幸的是,之前的两项研究都未能进行严格控制和随机抽样的 M&M 口味对比试验。因此,我们转向了科学。
1.3 研究目标
本研究旨在弥补以往研究的不足,并探讨以下问题:
-
是否存在全球共识认为欧洲的 M&M 确实比美国的 M&M 更好?
-
欧洲人是否真的能察觉在不知道自己吃的是哪种 M&M 的情况下,来自美国和欧洲的 M&M 之间的区别?还是这是欧洲人之间的一个大规模协调的谎言,目的是让美国人感到尴尬?
-
美国人真的对美国和欧洲 M&M 的口味感到“盲目”吗?或者他们能品尝出区别,但只是不把这种区别描述为“口味的改善”?
-
这些所谓的口味差异是否能被其他大陆的公民察觉?如果能,他们是否认为某种口味明显更好?
2. 方法
2.1 实验设计与数据收集
参与者通过诱导——呃,邀请他们参加一个社交聚会(并承诺提供免费食物),这个聚会恰好与测试地点位于同一地点。参与者同意暂停社交并加入研究后,会被安排到测试站,由一位受过训练的实验员引导他们完成以下步骤:
-
参与者坐在桌前,面前有两个杯子:一个空的,另一个装满了水。每只手一个杯子,参与者被要求闭上眼睛,并在实验期间保持闭眼状态。
-
实验员随机用勺子取出一颗 M&M,将其送到参与者的空杯中,参与者被要求吃下这颗 M&M(眼睛仍然闭着)。
-
在每次吃下 M&M 后,实验员通过询问参与者是否觉得 M&M 的味道是:特别好,特别差,还是正常,来收集味觉反应。
-
每位参与者总共接受了 10 颗 M&M(5 颗欧洲的,5 颗美国的),每次一颗,顺序由 random.org 随机确定。
-
在吃每颗 M&M 之间,参与者被要求喝一口水以帮助“清洁味蕾”。
-
收集的数据:对于每位参与者,实验员记录了参与者的原始洲(如果不明确,参与者被要求列出他们童年时最有记忆的吃糖果的洲)。对于每颗 M&M,实验员记录了M&M 的来源(“丹麦”或“美国”)、M&M 的颜色和参与者的味觉反应。实验员还被鼓励记录参与者在测试过程中说出的有趣句子,这些记录被归类为备注(数据可通过此处获取)。
2.2 材料采购与参与者招募
本研究购买了两袋 M&M 巧克力。美国来源的 M&M(“美国 M&M”)是在旧金山国际机场购买的,并由作者的父母带到丹麦探访她时带来。欧洲来源的 M&M(“丹麦 M&M”)则是在位于哥本哈根北部的 Lyngby 市的一家 Føtex 超市购买的。
实验在两个主要时间点进行。前 14 位参与者是在 2022 年 8 月在丹麦的 Lyngby 市进行测试的。测试对象主要是作者在丹麦技术大学(DTU)诺和诺德基金会生物可持续性中心遇到的朋友和室友,他们参加了一个“告别派对”,而实验程序被巧妙地融入其中。在旅行期间,一些其他的朋友和家人也在丹麦进行测试(例如,在火车上)。
剩下的 37 名参与者于 2022 年 10 月在美国华盛顿州西雅图进行测试,主要是在华盛顿大学计算机科学博士项目的研究生举办的“TGIF 欢乐时光”期间。这批参与者大多是来自保罗·G·艾伦计算机科学与工程学院(UW CSE)的学生和工作人员,他们响应了每周五的召集,来到艾伦中心的中庭领取免费的零食和饮料。

图 1. 参与者分布图。在首次抽样事件中,Lyngby 的参与者主要来自北美和欧洲,少数来自亚洲、南美或澳大利亚。我们在西雅图的第二次抽样事件大大增加了参与者数量,主要来自北美和亚洲,欧洲的参与者也有所增加。这两个事件都没有招募到来自非洲的参与者。图表由 Altair 制作。
尽管本研究旨在分析全球趋势,但不幸的是,数据仅从 51 名参与者中收集,作者能够将他们吸引到研究地点,而这些数据并不均衡,也不能代表地球六大有人居住的大陆(图 1)。我们希望在未来的工作中改进招募策略。目前,我们使用此数据集的分析能力仅限于来自北美、欧洲和亚洲的个体的反应趋势,这些数据高度偏向于作者在 2022 年末接触到的子社区。
2.3 风险
尽管我们未获得正式的人体实验批准,但此实验仍存在一些小的风险:参与者被告知,由于参与本研究,他们可能会摄入较高的糖分,并可能会经历“令人不悦的味道”。没有预见到其他风险。
然而,在实验后,我们不幸地观察到,当参与者得知他们的味觉反应偏向他们未曾预期的 M&M 类型时,常常会感到自豪感受挫。尤其是欧洲参与者,当他们得知自己或未婚夫/妻的偏好倾向于美国 M&M 时,这种自豪感的受挫似乎最为严重,尽管这并未通过定量测量,且只能通过轶事证据确认。
3. 结果与讨论
3.1 “美国 M&M”与“丹麦 M&M”总体反应
3.1.1 分类反应分析 — 整体数据集
在我们的第一次分析中,我们统计了“差”、“普通”和“好”口味反应的总数,并报告了每种反应在每种 M&M 类型中所占的百分比。来自丹麦的 M&M 比美国的 M&M 更频繁地获得“好”反应,但也更频繁地获得“差”反应。来自美国的 M&M 最常被报告为“普通”味道(图 2)。这可能是因为参与者中来自北美的人数较多,而美国 M&M 是默认的,因此更多被认为是“普通”,而丹麦 M&M 则更常被认为比基准更好或更差。
图 2. 整个数据集的定性味觉反应分布。计算每种 M&M 类型的“差”、“普通”或“好”反应的百分比。图表使用 Altair 制作。
现在让我们来分析一些统计数据,比如进行卡方(X2)检验,比较我们观察到的分类味觉反应分布。使用 scipy.stats chi2_contingency函数,我们构建了每种 M&M 类型的“好”、“普通”和“差”反应的观察计数列联表。利用 X2 检验来评估原假设,即两种 M&M 之间没有差异,我们得到了检验统计量的p-值为 0.0185,这在常见的p-值截断标准 0.05 下是显著的,但在 0.01 下则不显著。所以这只是一个坚实的“也许”,具体取决于你是否希望这个结果显著。
3.1.2 定量反应分析 — 整个数据集
X2 检验有助于评估分类反应是否存在差异,但接下来,我们希望确定两种 M&M 类型之间的相对味觉排名。为此,我们将味觉反应转换为定量分布,并计算了味觉评分。简而言之,“差”=1,“普通”=2,“好”=3。对于每个参与者,我们对他们品尝的每种类型的 5 颗 M&M 的味觉评分进行了平均,保持每种 M&M 类型的独立味觉评分。

图 3. 整个数据集的定量味觉评分分布。计算每个参与者对每种 M&M 类型的平均味觉评分的核密度估计。图表使用 Seaborn 制作。
拿到每种 M&M 类型的平均味觉评分后,我们使用 scipy.stats ttest_ind(“T 检验”)来评估美国和丹麦 M&M 的味觉评分均值是否不同(原假设为均值相同)。如果均值有显著差异,那么这将证明某种 M&M 被认为比另一种更美味。
我们发现美国 M&M 和丹麦 M&M 的平均味觉评分非常接近(见图 3),且没有显著差异(T 检验:p = 0.721)。因此,在所有参与者中,我们没有观察到两种 M&M 类型在感知味觉上的差异(或者,如果你喜欢解析三重否定:“我们不能拒绝原假设,即两者之间没有差异”)。
但如果我们按照参与者的出生大陆来区分,这个结果会有所不同吗?
3.2 “美国 M&M”和“丹麦 M&M”在各大洲的反应
我们在将参与者按其大陆来源分组后重复了上述 X2 和 T 检验分析。由于澳大利亚和南美组的样本量较小,我们将其合并为一个组,以尽量保护数据隐私。由于即使是合并后的澳大利亚/南美组(n=3)样本量仍然较小,我们将避免对该组趋势进行分析,但为了完整性和参与者的享受,我们将该组的数据包含在几个图表中。
3.2.1 类别反应分析 — 按大陆
在图 4 中,我们展示了每个大陆组的口味反应计数(上面面板,注意交互式图例)和反应百分比(下面面板)。北美和亚洲的趋势与整个数据集类似:参与者报告丹麦 M&M 时,“好”的比例高于美国 M&M,但也报告丹麦 M&M 时,“不好”的比例更高。美国 M&M 最常被报告为“正常”(图 4)。
相反,欧洲参与者报告美国 M&M 时,“不好”的比例接近 50%,而“好”的比例仅为 18%,这是最负面和最不积极的反应模式(排除了样本量较少的澳大利亚/南美组)。
图 4. 按大陆的定性口味反应分布。上面面板:口味反应的计数 — 点击图例进行交互式筛选!下面面板:每种 M&M 类型的口味反应百分比。图形由 Altair 制作。
这在条形图中表现得很突出,然而,只有北美在评估每个大陆的两种 M&M 类型之间的口味反应差异时,具有显著的 X2 p-值(p = 0.0058)。欧洲的 p-值或许在某些圈子中被认为是“接近显著”,但我们即将进行更多的假设检验,并且应当注意多重假设检验(表 1)。在这里出现假阳性结果将是灾难性的。

在比较两大洲之间相同 M&M 类型的口味反应模式时,有几点有趣的发现。首先,我们发现,在评估丹麦 M&M 时,不同大陆之间的口味差异并不显著——全球对来自欧洲的 M&M 的感觉普遍一致(右栏 X2 p-值,表 2)。为了更容易地可视化这一比较,我们在图 4 中重新组织了条形图,将其按 M&M 类型分组(图 5)。
图 5. 按 M&M 类型的定性口味反应分布,结果以百分比形式报告。(与图 4 相同的数据,但重新排列)。图形由 Altair 制作。
然而,当比较各大洲对美国 M&M 的反应时,我们发现了更大的差异。我们发现一个配对的差异是显著的:欧洲和北美的参与者对美国 M&M 的评价截然不同(p = 0.000007)(表 2)。看起来这种差异不太可能是随机产生的(表 2 左列)。

3.2.2 定量反应分析 — 按大洲划分
我们再次将分类特征转化为定量分布,以评估各大洲对 M&M 类型的相对偏好。对于北美,我们看到两种 M&M 类型的口味评分均值其实相当接近,但美国 M&M 的“正常”评分周围密度较高(图 6A)。欧洲的分布在均值上保持了一定的分离(尽管这种差异不算显著),美国 M&M 的评分较低(图 6B)。亚洲参与者的口味评分分布最为相似(图 6C)。
在重新调整以比较各大洲对相同 M&M 类型的口味评分的定量方法时,只有北美和欧洲参与者对美国 M&M 的评分在 T 检验基础上有显著差异(p = 0.001)(图 6D),尽管我们现在真的面临多重假设检验的风险!如果你打算认真对待这个分析,请保持谨慎。

图 6. 按大洲分的定量口味评分分布。每种 M&M 类型的平均口味评分的核密度估计。A. 北美对每种 M&M 的反应比较。B. 欧洲对每种 M&M 的反应比较。C. 亚洲对每种 M&M 的反应比较。D. 各大洲对美国 M&M 的比较。E. 各大洲对丹麦 M&M 的比较。图由 Seaborn 绘制。
此时,我开始觉得或许欧洲人并不是在胡说八道。我并不是说他们所说的那么夸张,但也许确实存在某种差异……在某种程度上,北美参与者也感知到了这种差异,但他们对欧洲来源的 M&M 的评价并不总是正面或负面一致。
3.3 M&M 口味对齐图
在我们迄今为止的分析中,我们没有考虑到参与者在 M&M 欣赏度上的基线差异。例如,假设人物 1 将所有丹麦 M&M 评分为“好”,所有美国 M&M 评分为“正常”,而人物 2 则将所有丹麦 M&M 评分为“正常”,所有美国 M&M 评分为“差”。他们对丹麦 M&M 和美国 M&M 的相对偏好是相同的,但人物 2 可能并不像人物 1 那样喜欢 M&M,且原始评分的平均化使得相对偏好信号变得模糊。
受桌面角色扮演游戏(如《龙与地下城©™》)中使用的合法/混乱与善良/邪恶对齐图的启发,在图 7 中,我们建立了一个 M&M 对齐图,帮助确定参与者在 M&M 享受类别中的分布。

图 7. M&M 享受对齐图。x 轴表示参与者对美国 M&M 巧克力的平均口味得分;y 轴表示参与者对丹麦 M&M 巧克力的平均口味得分。图表由 Altair 制作。
值得注意的是,右上象限,即两种 M&M 巧克力均被视为“好”到“正常”的区域,主要由北美参与者和少数亚洲参与者占据。所有欧洲参与者都位于图表的左半部分,其中美国 M&M 巧克力被视为“正常”到“差”,但欧洲人对于丹麦 M&M 巧克力的感知有些分裂,分布在上下半部分,其中对丹麦 M&M 巧克力的感知从“好”到“差”不等。
图 7 的交互版本已提供,供读者探索不同 M&M 对齐区域的参与者计数。
图 7(交互版):点击并用鼠标刷过散点图,查看不同 M&M 享受区域中各大洲的计数。图表由 Altair 制作。
3.4 参与者口味反应比率
接下来,为了去除 M&M 享受的基线影响,并专注于参与者之间对两种 M&M 巧克力类型的相对偏好,我们取了每个参与者的美国 M&M 口味得分平均值与其丹麦 M&M 口味得分平均值的对数比值。

方程式 1:计算每个参与者总体 M&M 巧克力偏好比率的方程。
因此,正分数表示偏好美国 M&M 巧克力,而负分数表示偏好丹麦 M&M 巧克力。
平均而言,欧洲参与者对丹麦 M&M 巧克力的偏好最强,亚洲人也表现出对丹麦 M&M 巧克力的轻微偏好(图 8)。对于那两位在得知自己对美国 M&M 巧克力有轻微偏好时感到自豪感下降的欧洲人,别担心:你们并不是认为美国 M&M 巧克力是“好”的,而是将其评为比丹麦 M&M 巧克力稍微“不那么差”(参见交互版图 7 中的参与者 ID 4 和 17)。如果你坚持认为 M&M 是一个不值得复制的糟糕美国发明,转而回归食用手工制作的欧洲巧克力,你的荣誉可能会恢复。

图 8. 按大陆分布的参与者 M&M 偏好比率。偏好比率按方程 1 计算。正数表示相对偏好美国 M&M 巧克力,负数表示相对偏好丹麦 M&M 巧克力。图表由 Seaborn 制作。
北美参与者在偏好比率上差异较大:有些人偏好中立,接近 0,有些人则强烈偏好熟悉的美国 M&M,而少数人则中等程度地偏好丹麦 M&M。据 anecdotal 经验,北美人中那些偏好欧洲 M&M 的人,似乎表现出某种夸大的自豪感,仿佛他们的结果象征着一种高雅的品味。
总体而言,通过 T 检验比较 M&M 偏好比率的分布,显示出欧洲和北美参与者之间可能存在显著的均值差异(p = 0.049),但拜托,这是我报告的第 20 个 p 值——这个可能太接近了,难以确定。
3.5 味觉不一致性与“完美分类者”
对每位参与者,我们通过计算他们对每种 M&M 类型的反应标准差的平均值来评估他们的味觉评分一致性,并将其与他们的偏好比率进行对比绘图(见图 9)。
图 9. 参与者的味觉一致性与偏好比率。横轴表示参与者相对的 M&M 偏好比率。纵轴表示他们对美国 M&M 和丹麦 M&M 评分的标准差的平均值。纵轴上的 0 值表示反应的一致性完美,而更高的值表示反应不一致。图表由 Altair 制作。
大多数参与者的评分在一定程度上存在不一致,针对同一类型的 M&M 在 5 个样本中的评分不同。如果欧洲产和美国产的 M&M 味觉差异并不十分明显,那么这种不一致是可以预期的。最不一致的是那些对同一类型的 M&M 给出“好”、“正常”和“坏”不同评分的参与者(例如,纵轴上的高点,味觉评分的标准差较大),这表明他们的味觉感知能力较差。
有趣的是,四位参与者——每个大洲组的一位——表现得完全一致:他们对每种 M&M 类型的 5 颗 M&M 的味觉反应相同,导致平均标准差为 0.0(见图 9 底部)。排除掉其中一位仅将所有 10 颗 M&M 评为“正常”的参与者,其余三位看起来是“完美分类者”——他们要么将一种类型的所有 M&M 评为“好”,另一种类型评为“正常”,要么将一种类型的所有 M&M 评为“正常”,另一种类型评为“坏”。也许这些人是“超级味觉者”。
3.6 M&M 颜色
另一种可能的解释是个体味觉反应的不一致性,可能是由于 M&M 颜色所带来的可感知的味觉差异。从视觉上看,美国生产的 M&M 明显比丹麦生产的 M&M 更加光滑和鲜艳,而丹麦 M&M 则显得有些“斑驳”(见图 10A)。在实验过程中记录了 M&M 的颜色,尽管平衡取样并未正式纳入实验设计,但颜色似乎大致均匀取样,唯一的例外是蓝色美国 M&M,其样本量过大(见图 10B)。

图 10. M&M 颜色。A. 每种类型的 M&M 颜色照片。也许在我那没有专业灯光的照片中很难看出,但肉眼观察,美国的 M&M 似乎更亮且颜色更均匀,而丹麦的 M&M 则呈现出较暗且带有斑驳色彩。是我一个人感觉这样,还是你们也能听到欧洲人说“它们更亮是因为你们在食物中加了那些我们在这里禁用的额外化学物质!”B. 在实验过程中每种颜色的 M&M 分布。蓝色的美国 M&M 并不是故意被过度抽样的——它们一定特别亮眼,令实验者难以抗拒。图表由 Altair 制作。
我们简要地根据颜色可视化了可能的口味反应差异(见图 11),然而我们认为数据不足以支持明确的结论。毕竟,平均来说,每个参与者可能只会品尝到 6 种 M&M 中的 5 种颜色一次,而 1 种颜色则根本没有品尝到。我们将进一步的 M&M 颜色研究留待未来的工作。

图 11. 各种颜色和类型的 M&M 口味反应概况。反应结果以“差”、“正常”和“好”回应的百分比表示,尽管并不是所有的 M&M 都是平均抽样的。图表由 Altair 制作。
3.7 多彩的评论
我们向每位参与者保证,在本实验中没有“正确的答案”,所有感受都是有效的。尽管有些参与者非常认真,有时会花超过一分钟时间细细品味每颗 M&M 并像品酒师一样评价它,许多参与者似乎将实验视为一场竞争(这有时会导致骄傲的情绪膨胀或萎缩)。实验者在记录 M&M 反应的同时也做了笔记和摘录,其中有些内容有些“生动”。我们提供了一个匆忙制作的每种 M&M 类型的词云,供娱乐之用(见图 12),但我们提醒大家,不要在没有认真情感分析的情况下过度解读它们。

图 11. 从每种 M&M 类型的笔记栏中生成的简单词云。提醒一下——这些词云还没有经过情感分析,并且记录了一些不太恰当的语言。图表由 WordCloud 制作。
4. 结论
总体来看,并没有出现“全球共识”认为欧洲的 M&M 比美国的 M&M 更好。然而,欧洲参与者似乎更强烈地对美国的 M&M 表达负面反应,而北美的参与者似乎对是否更喜欢来自美国还是欧洲的 M&M 观点较为分歧。亚洲参与者的偏好趋势通常介于北美和欧洲之间。
因此,我承认,欧洲人并没有在关于 M&M 的事情上进行大规模的联合欺骗。大多数欧洲参与者偏向丹麦 M&M 这一结果令人信服,尤其是考虑到我是亲自收集大部分味觉反应数据的实验者。如果他们找到了欺骗的方式,做得足够巧妙,超出了我的被动察觉,以至于我没有注意到。然而,基于这项研究,似乎强烈的“呕吐味”并不是普遍被感知的,而且在同时品尝两种 M&M 类型时,不会出现在非欧洲人中。
我们希望这项研究能带来启发!我们期待在未来扩展这项工作,改进参与者抽样,增加来自其他大洲的不同类型 M&M,并深入探讨颜色可能带来的味觉差异。
感谢所有参与并为了科学而吃 M&M 的人!
图表和分析可以在 github 上找到:github.com/erinhwilson/mnm-taste-test
本文由 Erin H. Wilson 博士[1,2,3]撰写,她决定在答辩和开始下一份工作之间的这段时间,最好用来进行这项非常有价值的分析。希望大家能看出这篇文章是幽默的——我并没有对不喜欢美式 M&M 的欧洲人怀有任何负面情绪,只是很享受这个机会,带着点调侃的心态,玩笑式地讽刺我们在充满热情的数据分析中所展开的激烈辩论。
感谢 Matt、Galen、Ameya 和 Gian-Marco 在数据收集方面的帮助!
[1] 曾在华盛顿大学保罗·G·艾伦计算机科学与工程学院担任博士生
[2] 曾在丹麦技术大学诺和诺德基金会生物可持续发展中心担任访问博士生
[3] LanzaTech 的未来数据科学家
机器学习模型是否存储受保护的内容?
概念验证
·发表于Towards Data Science ·5 分钟阅读·2024 年 5 月 6 日
--

从 chatGPT 到 Stable Diffusion,人工智能(AI)正经历一个类似于1970 年代的夏天,AI 的盛况可与那个时代的辉煌相提并论。然而,这一欢庆并非没有遭遇反对。从好莱坞到卢浮宫,人工智能似乎唤醒了一个沉睡的巨人——一个渴望保护曾经看似专属于人类的世界:创造力。
对于那些渴望保护创造力的人来说,人工智能似乎有一个致命弱点:训练数据。事实上,所有最佳模型都需要一个高质量、涵盖全球的数据源——但这意味着什么呢?
首先,高质量意味着人为创造的。尽管非人工创造的数据自从计算机自我对弈的概念被战争游戏推广以来取得了许多进展,计算机科学文献却表明,如果完全去除人的因素(即模型腐化或模型崩塌),模型质量随着时间的推移会下降。简单来说:人类数据是这些模型的命脉。
第二,全球性意味着全球性。如果你把它放到网上,你应该假设模型已经在训练中使用了它:那个你原本希望只有你和 Tom 记得的 Myspace 帖子(已被吸收),那个你高兴地忘记的图片封存记忆,直到 PimEyes 迫使你重新记起它(已被吸收),以及那些你希望只是梦境的深夜 Reddit 争论(已被吸收)。
像 LLaMa、BERT、Stable Diffusion、Claude 和 chatGPT 这样的模型都是在大量由人类创作的数据上进行训练的。而一些、许多或大多数人类创作的表达方式——尤其是那些恰好固定在计算机可以访问并学习的有形介质上的表达——具有版权保护的资格。

Anderson v. Stability AI;Concord Music Group, Inc. v. Anthropic PBC;Doe v. GitHub, Inc.;Getty Images v. Stability AI;{Tremblay, Silverman, Chabon} v. OpenAI;纽约时报诉微软
虽然可能是偶然的,这些模型无法生存的数据正是大多数受到版权保护的数据。这也催生了我们今天看到的巨大的版权斗争。
在这些诉讼中产生的许多问题中,最紧迫的一个问题是模型本身是否存储受保护的内容。这个问题似乎相当明显,因为我们怎么能说模型——仅仅是由数字(即权重)和架构组成的集合——“存储”了什么?正如 Murray 教授所说:
当前关于视觉生成型 AI 系统的辩论中的许多参与者抓住了这样一个观点:生成型 AI 系统已在包含实际版权保护的图像文件(如.jpg、.gif、.png 等)数据集和基础模型上进行训练,这些文件是从互联网上抓取的,数据集或基础模型一定已经制作并存储了这些作品的副本,并且生成型 AI 系统以某种方式进一步选择并复制了这些数据集中的个别图像,并以某种方式将这些图像的重大可版权部分复制并纳入到最终生成的图像中,供最终用户使用。这是一种魔法般的思维。
Michael D. Murray, 26 SMU 科技与法律评论 259, 281 (2023)
然而,模型本身似乎在某些情况下,确实会记住训练数据。
以下示例来自HuggingFace 上的 Gradio Space,该平台允许用户选择一个模型,查看输出,并从该模型的训练数据中检查生成的图像与其训练数据中任何图像的相似度。由于 MNIST 数字易于机器解析、易于人类从相似性角度理解,并且具有易于分类的优点——这使得相似性搜索只考虑相同数字的图像(提高效率),因此使用了 MNIST 数字。
让我们看看它是如何工作的!
以下图像的相似度得分为 0.00039。RMSE 代表均方根误差,是评估两张图像相似度的一种方式。事实上,还有许多其他相似性评估方法,但 RMSE 能很好地判断一张图像是否为副本(即,我们这里并不是在寻找法律定义的相似性)。举个例子,RMSE 值小于 0.006 时,图像已接近“复制”范围,而 RMSE 值小于 0.0009 时,则进入完美复制的领域(肉眼无法分辨)。

🤗 一个生成几乎完全相同训练数据副本的模型(RMSE 为 0.0003)🤗
要使用Gradio 空间,请按照以下三个步骤操作(如果空间处于休眠状态,可以选择构建该空间):
-
步骤 1:选择要使用的预训练模型类型
-
步骤 2:点击“提交”按钮,模型将为您生成一张图像(28x28 的灰度图像)
-
步骤 3:Gradio 应用程序会在该模型的训练数据中搜索,识别与生成图像最相似的图像(从 60K 个示例中筛选)
如上所示,左侧生成的图像(AI 创作)几乎与右侧的训练数据完全相同,当使用“FASHION-diffusion-oneImage”模型时,结果正是如此。这是有道理的。该模型仅对FASHION 数据集中的一张图像进行了训练。同样的情况也适用于“MNIST-diffusion-oneImage”模型。
尽管如此,即使是训练了更多图像(例如 300 张、3000 张或 60000 张图像)的模型,也能产生非常相似的输出。这个示例来自一个生成对抗网络(GAN),它在完整的 60K 图像数据集(仅限训练)上进行了训练,数据集包括MNIST 手写数字。作为背景,生成对抗网络(GAN)通常生成的图像比扩散模型记忆性差:

RMSE 为 0.008
这是另一个使用扩散模型,并在 60K MNIST 数据集上训练的图像(即,支持稳定扩散的模型类型):

RMSE 为 0.004
随时可以自己尝试使用Gradio 空间,探索模型,或者如果有问题可以联系我!
总结: 这个小型示例的重点是,机器学习模型没有什么神秘或绝对的版权豁免。机器学习模型确实可以并且会生成与其训练数据相同的图像——换句话说,模型确实会存储受保护的内容,因此可能会遇到版权问题。当然,也有许多反驳的论点(我正在进行的工作!);这个演示应该仅作为存储的轶事性证据,可能是开发者在这一领域工作的“金丝雀”。
输入到模型中的内容和从模型中得到的结果同样重要,尤其对于某些执行特定任务的模型来说更是如此。我们需要小心并关注我们的“黑箱”,因为这个类比往往并不成立。你无法自己解读模型所持有的权重集合,并不意味着你可以摆脱所有形式的责任或审查。
— @nathanReitinger,敬请关注该领域的进一步工作!
Unless otherwise noted, all images are by the author
不要过度思考“离群值”,改用学生 t 分布
使用 R 和 Brms 的贝叶斯方法
·发表于Towards Data Science ·15 分钟阅读·2024 年 3 月 30 日
--
对许多研究人员而言,离群值是可以极大改变分析过程的“异类波”,或者“混淆”一些预期效果。我更倾向于使用“极端观测值”这一术语,并将离群值留给那些并非研究群体真正一部分的观测值。例如,在我的研究领域(脑缺血研究),离群值指的是没有缺血的动物(尽管应该有),而极端观测值则是那些小范围或大范围缺血,且与其他动物有显著差异的个体。
传统的(频率学派)统计模型是建立在高斯分布的坚实基础上的。这存在一个显著的局限性:一个固有的假设,认为所有数据点会围绕一个中央均值按照可预测的模式聚集(基于中心极限定理)。在柏拉图的理念世界中,这或许是正确的,但我们这些生物医学领域的科学家深知,考虑到有限的样本(动物数量),我们很难依赖这一假设来进行观察。
高斯分布对极端观测值非常敏感,其使用使得科学家们认为,去除极端观测值是获得“更清晰”或“更干净”结果的最佳方式(无论这是什么意思)。正如我曾在一篇文章中作为审稿人 2 所评论的,“问题不在于那些可能‘隐藏’你效果的极端观测值,而在于你使用了一个(我认为)不适合你目的的统计模型”。
应该注意的是,没有哪种统计模型是“正确”或“合适”的,但我们可以估计,在给定数据的情况下,有些统计模型比其他模型更有可能生成观察到的数据(生成模型)。
幸运的是,没人强迫我们受限于高斯模型的假设,对吧?我们还有其他选择,比如学生 t 分布(1)。我认为它是一个更具适应性的工具,用来在真实世界的生物医学数据的动荡海洋中航行。学生 t 分布提供了一种强大的替代方案,承认我们的数据可能包含极端观测值,而这些值是正常的生物学反应,任何情境下都可以预见到。可能会有一些患者或动物对治疗没有反应或反应过度,而我们的建模方法能够识别这些反应作为数据的一部分,这非常重要。因此,本教程将通过**brms**包在 R 语言中的应用(2)来探讨使用学生 t 分布的建模策略——这是贝叶斯建模的强大助手。
学生 t 分布背后是什么?
学生 t 分布不过是具有更重尾部的高斯分布。换句话说,我们可以说高斯分布是学生 t 分布的一个特例。高斯分布由均值(μ)和标准差(σ)定义。而学生 t 分布则增加了一个额外的参数,即自由度(df),它控制分布的“厚度”。这个参数赋予远离均值的事件更高的概率。这一特性对于小样本量,尤其是生物医学领域尤其有用,因为在这些领域,正态分布的假设是值得怀疑的。注意,当自由度增大时,学生 t 分布趋近于高斯分布。我们可以使用密度图来可视化这一点:
# Load necessary libraries
library(ggplot2)
# Set seed for reproducibility
set.seed(123)
# Define the distributions
x <- seq(-4, 4, length.out = 200)
y_gaussian <- dnorm(x)
y_t3 <- dt(x, df = 3)
y_t10 <- dt(x, df = 10)
y_t30 <- dt(x, df = 30)
# Create a data frame for plotting
df <- data.frame(x, y_gaussian, y_t3, y_t10, y_t30)
# Plot the distributions
ggplot(df, aes(x)) +
geom_line(aes(y = y_gaussian, color = "Gaussian")) +
geom_line(aes(y = y_t3, color = "t, df=3")) +
geom_line(aes(y = y_t10, color = "t, df=10")) +
geom_line(aes(y = y_t30, color = "t, df=30")) +
labs(title = "Comparison of Gaussian and Student t-Distributions",
x = "Value",
y = "Density") +
scale_color_manual(values = c("Gaussian" = "blue", "t, df=3" = "red", "t, df=10" = "green", "t, df=30" = "purple")) +
theme_classic()

图 1:具有不同自由度的高斯分布和学生 t 分布的比较。
请注意在图 1 中,随着自由度的降低,均值周围的山峰变得越来越小,因为概率质量转移到尾部,而尾部则更厚。这个特性使得学生 t 分布对离群值的敏感度降低。关于这一点的更多细节,你可以查看这个博客。
加载所需的包
我们加载所需的库:
library(ggplot2)
library(brms)
library(ggdist)
library(easystats)
library(dplyr)
library(tibble)
library(ghibli)
探索性数据可视化
所以,让我们跳过数据模拟,直接进入正题。我将使用我从进行转棒测试的小鼠中获得的真实数据。
首先,我们将数据集加载到环境中,并设置相应的因素水平。数据集包含动物的 ID、一个分组变量(基因型)、两个不同测试日的指示符(天数),以及同一天的不同实验。对于本文,我们仅建模其中一个实验(实验 3)。其他实验将保留在未来的关于建模变异性的文章中。
如数据处理所示,我们的建模策略将基于基因型和天数作为Trial3分布的分类预测变量。
在生物医学科学中,分类预测变量或分组因素比连续预测变量更为常见。该领域的科学家喜欢将样本分为不同的组或条件,并应用不同的处理方法。
data <- read.csv("Data/Rotarod.csv")
data$Day <- factor(data$Day, levels = c("1", "2"))
data$Genotype <- factor(data$Genotype, levels = c("WT", "KO"))
head(data)

数据框
让我们使用Raincloud 图来初步查看数据,正如吉列尔梅·A·弗朗奇博士在这篇精彩的博客文章中所展示的。
edv <- ggplot(data, aes(x = Day, y = Trial3, fill=Genotype)) +
scale_fill_ghibli_d("SpiritedMedium", direction = -1) +
geom_boxplot(width = 0.1,
outlier.color = "red") +
xlab('Day') +
ylab('Time (s)') +
ggtitle("Rorarod performance") +
theme_classic(base_size=18, base_family="serif")+
theme(text = element_text(size=18),
axis.text.x = element_text(angle=0, hjust=.1, vjust = 0.5, color = "black"),
axis.text.y = element_text(color = "black"),
plot.title = element_text(hjust = 0.5),
plot.subtitle = element_text(hjust = 0.5),
legend.position="bottom")+
scale_y_continuous(breaks = seq(0, 100, by=20),
limits=c(0,100)) +
# Line below adds dot plots from {ggdist} package
stat_dots(side = "left",
justification = 1.12,
binwidth = 1.9) +
# Line below adds half-violin from {ggdist} package
stat_halfeye(adjust = .5,
width = .6,
justification = -.2,
.width = 0,
point_colour = NA)
edv

图 2:探索性数据可视化。
图 2 与吉列尔梅·A·弗朗奇博士原始图形有所不同,因为我们绘制了两个因素而不是一个。然而,图形的性质是相同的。请注意红点,这些点可以被视为极端观察值,它们会将集中趋势的测量(尤其是均值)拉向一个方向。我们还观察到方差不同,因此对sigma的建模也能提供更好的估计。我们现在的任务是使用brms包来建模输出。
使用 brms 拟合统计模型
在这里,我们使用Day和Genotype作为相互作用的分类预测变量,来拟合Trial 3的分布。让我们首先拟合一个典型的高斯模型,它类似于频率学派框架中的普通最小二乘法(OLS)模型,因为我们使用的是默认的平坦brms 先验。先验超出了本文的讨论范围,但我保证我们将在未来的博客中讨论它们。
一旦我们获得了高斯模型的结果,我们可以将它们与学生 t 模型的大量结果进行比较。然后,我们将sigma添加到方程中,以考虑数据方差的差异。
在高斯分布下拟合一个“典型的”(频率学派)模型
我们的高斯模型是在典型(且常常不正确)的同方差假设下构建的(3)。换句话说,我们假设所有组的方差相同(或非常相似)。作为研究人员,我不记得曾看到过这种假设。
Gaussian_Fit1 <- brm(Trial3 ~ Day * Genotype,
data = data,
family = gaussian(),
# seed for reproducibility purposes
seed = 8807,
control = list(adapt_delta = 0.99),
# this is to save the model in my laptop
file = "Models/20240222_OutliersStudent-t/Gaussian_Fit1.rds",
file_refit = "never")
# Add loo for model comparison
Gaussian_Fit1 <-
add_criterion(Gaussian_Fit1, c("loo", "waic", "bayes_R2"))
模型诊断
在继续之前,进行一些简单的模型诊断是个好主意,以便将实际观察与我们模型的预测进行比较。我们可以通过几种方式做到这一点,但最常见的方法是绘制完整的密度图。我们可以使用brms中的pp_check函数来实现这一点。
set.seed(8807)
pp_check(Gaussian_Fit1, ndraws = 100) +
labs(title = "Gaussian model") +
theme_classic()

图 3:高斯模型的诊断
图 3 表明我们的观察值(深蓝色)与模型预测没有显著差异。下面,我为您提供了额外的代码,以检查其他pp_check的替代方案及其各自的图表。
set.seed(88071)
pp_check(Gaussian_Fit1, group = "Genotype", type = "dens_overlay_grouped", ndraws = 100) +
labs(title = "Density by Genotype") +
theme_classic()
pp_check(Gaussian_Fit1, type = "stat_grouped", group = "Genotype", stat = "var", binwidth = 3) +
coord_cartesian(xlim = c(0, 300)) +
ggtitle("Grouped variance") +
theme_classic()
pp_check(Gaussian_Fit1, type = "stat", stat = "var", binwidth = 3) +
coord_cartesian(xlim = c(0, 600)) +
ggtitle("How well we captured the variace") +
theme_classic()
pp_check(Gaussian_Fit1, type = "stat", stat = "mean", binwidth = 2) +
coord_cartesian(xlim = c(0, 50)) +
ggtitle("How well we captured the mean") +
theme_classic()
检查高斯分布的结果
现在,我们使用bayestestR包中的describe_posterior函数(4)来查看结果:
describe_posterior(Gaussian_Fit1,
centrality = "mean",
dispersion = TRUE,
ci_method = "HDI",
test = "rope",
)

这里我们专注于“截距”,即 1 DPI 时 WT 的值,以及“GenotypeKO”,即同一时间点 KO 动物的估计差异。我们看到 WT 动物在转棒上的时间约为 37 秒,而它们的 KO 同类则少于一秒(0.54)更多。作为该领域的研究人员,我可以说,这个差异是没有意义的,基因型对转棒表现没有影响。即使是“天数”的效应,2.9,在这个模型下对我来说似乎也没有意义。我们可以使用brms中的神奇函数conditional_effects轻松地可视化这些估计。
# We create the graph for convex hull
Gaussian_CondEffects <-
conditional_effects(Gaussian_Fit1)
Gaussian_CondEffects <- plot(Gaussian_CondEffects,
plot = FALSE)[[3]]
Gaussian_CondEffects +
geom_point(data=data, aes(x = Day, y = Trial3, color = Genotype), inherit.aes=FALSE) +
Plot_theme +
theme(legend.position = "bottom", legend.direction = "horizontal")

图 8:高斯模型的条件效应
在图 8 中,我们可以看到交互项的估计值和不确定性。我已经自定义了这个图,并添加了一些 ggplot 元素,您可以在原始的Quarto Notebook中查看。请注意,尽管第一天的离散度比第二天大,但两个时间点的不确定性是相似的。我们将在文章结尾的一个小片段中解决这一点。
现在让我们看看,当我们使用学生-t 分布对相同的数据建模时,我们的理解发生了多大的变化。
拟合我们的假设:使用学生 t 分布的模型
现在是时候在我们的brms模型中使用学生 t 分布了。
Student_Fit <- brm(Trial3 ~ Day * Genotype,
data = data,
family = student,
# seed for reproducibility purposes
seed = 8807,
control = list(adapt_delta = 0.99),
# this is to save the model in my laptop
file = "Models/20240222_OutliersStudent-t/Student_Fit.rds",
file_refit = "never")
# Add loo for model comparison
Student_Fit <-
add_criterion(Student_Fit, c("loo", "waic", "bayes_R2"))
模型诊断
我们像之前一样绘制模型诊断:

图 9:学生 t 分布的模型诊断
图 9 显示了观察值和预测值的平均形状及峰值相匹配。需要注意的是,我们的模型似乎预测出了低于 0 的值。这是一个重要的研究问题,我们暂时跳过。不过,这确实暗示了使用信息性先验或设定下界为 0 的分布族,如 log_normal、hurdle_lognormal 或 zero_inflated_poisson,具体取决于情况。Andrew Heiss (5) 在这方面提供了一个 很好的例子。
检查学生 t 分布的结果
让我们来看看后验分布:
describe_posterior(Student_Fit,
centrality = "mean",
dispersion = TRUE,
ci_method = "HDI",
test = "rope",
)

在这个模型下,我们可以看到我们的估计值有所变化,我会说变化是适度的。我们的截距估计(1 天时的 WT)减少了 7 秒。那为什么会这样呢?因为我们在开始时发现的极端值对数据的集中趋势度量影响较小。因此,这是对第 1 天“典型”WT 动物的更准确度量。我们还观察到天数效应的显著增加,比我们最初的高斯估计多了将近 10 秒。重要的是,我们的 KO 基因型效应似乎更加显著,从我们高斯模型中的 0.52 增加到我们学生 t 模型中的 5.5,约增加了 10 倍。从我的角度来看,鉴于这些数据的背景,两个模型之间的差异是显著的。
让我们使用 conditional_effects 以图形方式查看:
Student_CondEffects <-
conditional_effects(Student_Fit)
Student_CondEffects <- plot(Student_CondEffects,
plot = FALSE)[[3]]
Student_CondEffects +
geom_point(data=data, aes(x = Day, y = Trial3, color = Genotype), inherit.aes=FALSE) +
Plot_theme +
theme(legend.position = "bottom", legend.direction = "horizontal")

图 10:学生 t 模型的条件效应
我们能得到更好的估计吗?对于这个具体的例子,我认为我们可以。从一开始就很容易注意到数据的方差差异,尤其是在我们比较第一天和第二天的图形时。我们通过使用学生 t 分布改善了估计,进一步的改进可以通过开发一个异方差性模型来预测 sigma(残差方差)。
这样,模型并不假设你的残差方差在分组变量间是相等的,而是将其作为可以由预测变量建模的响应。
这是我们留到最后的小点。
使用学生 t 分布预测 sigma
我们使用 brms 中的 bf 函数将 sigma 作为响应变量。在这种情况下,我们将使用相同的预测变量 Day 和 Genotype 来建模这个参数。
Student_Mdl2 <- bf (Trial3 ~ Day * Genotype,
sigma ~ Day * Genotype)
Student_Fit2 <- brm(
formula = Student_Mdl2,
data = data,
family = student,
# seed for reproducibility purposes
seed = 8807,
control = list(adapt_delta = 0.99),
# this is to save the model in my laptop
file = "Models/20240222_OutliersStudent-t/Student_Fit2.rds",
file_refit = "never")
# Add loo for model comparison
Student_Fit2 <-
add_criterion(Student_Fit2, c("loo", "waic", "bayes_R2"))
模型诊断

图 11:带有 sigma 的学生 t 分布模型诊断
图 11 看起来很好,除了 0 以下的不舒服预测值。对于这个情况,我判断这不会强烈偏倚估计及其不确定性。然而,这是我在进行实际研究时会考虑的一个方面。
检查带有预测 sigma 的学生 t 分布的结果
现在,让我们来看看后验分布。

与另外两个拟合模型相比,我们看到更多的参数,因为现在模型中将 sigma 的反应作为一个主效应包含在内。在这种方案下,我们看到截距更接近高斯模型的截距,并且基因型(GenotypeKO)的效应减少了一半。
然而,有一点需要注意。在我们的第一个 Student-t 模型中,截距的不确定性为 24.1–37.4。另一方面,在最后一个模型中,不确定性增加到 24.3–46.1。这意味着当我们考虑不同的方差时,我们对这个(和其他)参数的信心减少了。例如,天数的情况也是如此,从 1.2–18.9 变化为-5.6–18.1。此时,我们对第二天与转棒上花费时间增加之间的关系的信心减弱了。
不用担心,统计建模的目的是提供对测量中不确定性的最佳量化,这正是我们现在所做的。当然,当样本中有极端值并且这些极端值也属于我们的总体时,我们的不确定性会增加。
在这个例子中,我们看到,考虑数据中不同的方差给了我们一个完全不同的结果理解。
最后,我们可以看到,绘制在对数尺度上的 sigma 在不同天数和基因型之间有显著变化:
Student_CondEffects2 <-
conditional_effects(Student_Fit2)
Student_CondEffects2 <- plot(Student_CondEffects2,
plot = FALSE)[[3]]
Student_CondEffects2 +
geom_point(data=data, aes(x = Day, y = Trial3, color = Genotype), inherit.aes=FALSE) +
Plot_theme +
theme(legend.position = "bottom", legend.direction = "horizontal")
Student_CondEffects3 <-
conditional_effects(Student_Fit2, dpar = "sigma")
Student_CondEffects3 <- plot(Student_CondEffects3,
plot = FALSE)[[3]]
Student_CondEffects3 +
Plot_theme +
theme(legend.position = "bottom", legend.direction = "horizontal")

图 12:带有 sigma 的 Student-t 模型的条件效应

图 13:sigma 的条件效应
我们在第二个图中看到的是 sigma,它有效地解释了这个参数在不同天数和基因型之间的方差。我们看到在第一天不确定性较高,特别是对于野生型小鼠,而在第二天这个参数是类似的。
我们可以通过比较三个模型的样本外预测来总结本文。
模型比较
我们使用 WAIC 标准进行模型比较(6),用于估计样本外预测误差。通过同时考虑观察数据的对数似然和有效参数数量,它在模型拟合和复杂性之间提供了平衡。与其他一些标准不同,WAIC 本质上考虑了参数的后验分布,而不是依赖于点估计,因此特别适合贝叶斯分析。
给定一个数据集和一个贝叶斯模型,WAIC 的计算公式为:
WAIC=−2×(LLPD−pWAIC)
其中:LLPD 是对数逐点预测密度,计算为每个观察数据点在后验样本中的对数似然的平均值。WAIC 是有效参数数量,通过对数似然的平均值和后验样本中的平均对数似然值之间的差异来计算。
我们使用 performance 包中的 compare_performance 函数,该包是 easystats 环境的一部分(4,7,8)。
Fit_Comp <-
compare_performance(
Gaussian_Fit1,
Student_Fit,
Student_Fit2,
metrics = "all")
Fit_Comp
输出结果显示,我们预测 sigma 的学生 t 模型在样本外预测中受到的惩罚最小(WAIC = 497)。请注意,该模型中没有 sigma 的估计值,因为它作为响应变量包含在内。此表还显示,学生 t 模型的残差方差(sigma)比高斯模型小,这意味着方差被预测变量更好地解释。我们可以将相同的结果可视化为图表:
Fit_Comp_W <-
loo_compare(
Gaussian_Fit1,
Student_Fit,
Student_Fit2,
criterion = "waic")
# Generate WAIC graph
Fit_Comp_WAIC <-
Fit_Comp_W[, 7:8] %>%
data.frame() %>%
rownames_to_column(var = "model_name") %>%
ggplot(
aes(x = model_name,
y = waic,
ymin = waic - se_waic,
ymax = waic + se_waic)
) +
geom_pointrange(shape = 21) +
scale_x_discrete(
breaks=c("Gaussian_Fit1",
"Student_Fit",
"Student_Fit2"),
labels=c("Gaussian_Fit1",
"Student_Fit",
"Student_Fit2")
) +
coord_flip() +
labs(x = "",
y = "WAIC (score)",
title = "") +
Plot_theme
Fit_Comp_WAIC

图 14:通过 WAIC 进行的模型比较
图 14 显示,我们的最后一个模型在样本外预测中受到的惩罚最小。
你可以在我的 GitHub 网站 找到此帖的更新版本。如果这段经历对你有帮助,或者你有任何建设性的意见,请告诉我。
除非另有说明,所有图像均由作者使用 R 代码生成。
参考文献
1.M. Ahsanullah, B. M. G. Kibria, M. Shakil, 正态分布与学生 t 分布及其应用(Atlantis Press,2014;dx.doi.org/10.2991/978-94-6239-061-4)。
2. P.-C. Bürkner, Brms: 一款用于贝叶斯多层次模型的 R 包,使用 Stan。80 (2017),doi:10.18637/jss.v080.i01。
3. K. Yang, J. Tu, T. Chen, 同方差性:线性回归中的一个被忽视的关键假设。General Psychiatry。32,e100148 (2019)。
4. D. Makowski, M. S. Ben-Shachar, D. Lüdecke, bayestestR:描述贝叶斯框架中效应及其不确定性、存在性和显著性。4,1541 (2019)。
5. A. Heiss, 使用贝叶斯 Beta 回归和零膨胀 Beta 回归模型建模比例的指南 (2021),(可在 dx.doi.org/10.59350/7p1a4-0tw75 获得)。
6. A. Gelman, J. Hwang, A. Vehtari, 理解贝叶斯模型的预测信息标准。Statistics and Computing。24,997–1016 (2013)。
7. D. Lüdecke, M. S. Ben-Shachar, I. Patil, P. Waggoner, D. Makowski, Performance: 用于评估、比较和测试统计模型的 R 包。6,3139 (2021)。
8. D. Makowski, M. Ben-Shachar, D. Lüdecke, bayestestR:描述贝叶斯框架中效应及其不确定性、存在性和显著性。Journal of Open Source Software。4,1541 (2019)。
在某些情况下,不要将过滤条件放在“WHERE”子句中

如果我们在 LEFT JOIN ON 之后放置条件,会发生什么?
·发表在Towards Data Science ·7 分钟阅读·2024 年 2 月 26 日
--
只要您点击进入这篇文章,我相信您应该了解 SQL。您还必须明白,在 SELECT 查询中,我们应该将条件放在 WHERE 子句中。然而,让我问你一个问题,看看你是否能立即回答。
如果我们在 LEFT JOIN … ON …子句中放置过滤条件会发生什么?
SELECT *
FROM Employee e LEFT JOIN Department d
ON e.dept_id = d.id
AND e.name = 'Chris'
如果您对上述查询的行为不确定,或者认为它等同于以下查询,请阅读我的文章,我会告诉您它们之间的区别。
SELECT *
FROM Employee e LEFT JOIN Department d
ON e.dept_id = d.id
WHERE e.name = 'Chris'
1. 验证结果

为了演示目的,我创建了两个带有简单虚拟数据的表如下。
员工表
我们真的需要深度学习来进行海岸监测吗?
深入探讨机器学习与传统海岸侵蚀监测方法的比较
·发表于 Towards Data Science ·14 分钟阅读·2024 年 9 月 10 日
--

照片由 thiago japyassu 提供,来自 Unsplash
深度学习(DL)是解决这个问题的唯一途径。这是我阅读的许多研究中隐含的假设。我总是倾向于同意这种看法。但,可能是因为如果没有它,我的博士研究将毫无意义。
幸运的是,我越是阅读,越能意识到遥感充满了机器学习可以提供帮助的问题。这些问题包括监测空气质量、估算土壤湿度、评估作物健康状况和追踪自然灾害。我的研究领域——海岸侵蚀监测,也是如此。
海岸线很长!这意味着我们需要自动化某些任务,才能有效地监测整个海岸线。同时,由于土地开发、云层覆盖以及海上风浪等因素引起的噪音,使得传统的确定性方法可能会失败。正是在处理这些变化时,机器学习才能发挥其优势。
深度学习,作为机器学习的一个子领域,已成为遥感中的一项宝贵工具,为前所未有的挑战提供了解决方案,并在遥感应用中创造了新的机会,
成为数据科学家需要学位吗?
不需要,但它肯定会有所帮助。
·发布于Towards Data Science ·阅读时间 8 分钟·2024 年 5 月 29 日
--

我经常被问到:“我需要学位才能进入数据科学领域吗?”
简短的回答是:不需要,但它肯定会有所帮助。
让我在本文中详细解释我的意思。希望它能为这个问题提供一些启示,或者给你一些有数据和研究支持的例子。
作为免责声明,本文中的一切仅代表我的个人观点,并非专业建议。
数据科学家的背景
“大学或学院是否还值得上?”这个问题已经被回答并讨论得不堪重负。
总是给出相同的回答:“除非你从事需要学位的工作,比如医生或律师,否则它不值得。”
我同意这个说法,因为它在技术上是正确的,但这个问题远比这复杂。
根据 Coursera 的这篇文章,以及 Zippia 的研究,51%的数据科学家拥有学士学位,34%拥有……
你真的了解 Python 中的 *args 吗?

图片来源:Miguel Á. Padriñán 来自 Pixabay
*args 的全面指南与实用示例
·发表于 Towards Data Science ·阅读时间 9 分钟·2024 年 1 月 29 日
--
作为 Python 中最独特的语法之一,*args 在编程过程中能为我们提供极大的灵活性和便利。我会说,它们体现了所谓的“Pythonic”风格和 Python 的禅意。
然而,我发现学习者往往难以理解它们。在本文中,我将尽我所能解释这个 Python 中标志性的概念,并根据我的知识提供实际应用案例。希望能帮助你更好地理解它。
1. “*args” 到底是什么?

*args 代表“参数”。它允许我们将任意数量的 位置参数(稍后解释)传递给一个函数。在函数内部,我们可以将所有位置参数存储为元组。因此,我们可以在函数中对这些参数元组进行任意操作。
这是一个简单的 *args 示例。
def add_up(*numbers):
result = 0
for num in numbers:
result += num
return result
print(add_up(1, 2, 3))
医生利用多模态数据;医疗 AI 也应该如此
整合多模态数据使新一代医疗 AI 系统能够更好地捕捉医生的思维和决策过程
·发布于 Towards Data Science ·阅读时间 9 分钟·2024 年 9 月 25 日
--

多模态医疗 AI 的数据类型和应用示意图。图片由作者提供。
多模态 AI 模型利用来自各种格式的数据,如文本、图像和音频,为用户提供更全面的医疗情况理解。这些模型由于能够处理和整合多种数据类型,能够呈现出比任何单一数据类型更为全面的健康图景,因此正在迅速发展。随着变换器架构和大型语言模型(LLMs)的崛起,这些模型广泛适用于多种数据模式,开发人员获得了新的工具来综合这些数据格式。谷歌的Gemini 多模态 AI及其他前沿的生成型 AI 模型能够无缝理解并综合文本、视频、图像、音频和代码(遗传或计算)等数据格式。尽管在过去几年中,医疗 AI 取得了令人振奋的进展,但其应用进展缓慢,现有应用程序通常只针对非常具体和狭窄的使用案例。医疗 AI 的未来在于多模态应用,因为它们反映了医生的临床过程,医生在做出评估时必须考虑许多因素和数据来源。能够在这个充满巨大潜力的领域执行的开发者和公司,将在 AI 辅助医疗的未来中占据重要角色。
利用多模态数据的好处
医学数据本质上是多模态的,AI 系统应当反映这一现实。在评估患者时,医生会利用多种数据来源,如病历记录、医学影像、音频录音和基因序列。传统上,AI 应用程序被设计来处理这些单一数据类型中的特定、狭义的任务。例如,某个 AI 系统可能擅长识别 CT 扫描中的肺结节,但它无法将这些数据与患者的报告症状、家族史和基因信息结合起来,帮助医生诊断肺癌。相比之下,多模态 AI 应用可以整合多种数据类型,将 LLMs 的灵活性与专业 AI 系统的专长相结合。研究表明,这些系统在传统 AI 任务中的表现也优于单模态 AI 系统,准确度提升了 6–33%。
多模态 AI 模型还致力于打破医学专业之间的壁垒。由于医学领域的专业化不断增加,研究和数据的不断扩展,医学已经发展成一个碎片化的格局,放射学、内科和肿瘤学等不同领域可能会独立运作。处理复杂疾病的患者通常需要跨多个专家团队的协作,然而,由于沟通不畅,关键的见解可能会丧失。多模态 AI 模型通过跨专业领域获取知识,弥合这些鸿沟,确保患者能够从所有相关领域的最新医学进展中受益。
不同医学数据模态概述
医学数据占全球所有数据的 30%以上,且有多种形式。以下是其中一些最突出的数据形式(非详尽列表):
医学影像
医学影像在医疗诊断和治疗规划中扮演着至关重要的角色,以至于它已经成为一个独立的专业(放射学)。CT 扫描和 X 光通常用于可视化骨骼结构并检测骨折或肿瘤,而超声波在监测胎儿发育和检查软组织方面至关重要。医生使用病理切片图像分析组织样本,以检测癌症等疾病。像卷积神经网络(CNN)这样的 AI 算法,通过处理大量带标签的图像,学习识别这些图像中的模式和异常。这些工具帮助放射科医生和其他医生更快速、更准确地解读图像。
组学
近年来,由于测序成本的下降,组学数据,包括基因组学、转录组学和蛋白质组学,迅速增长。它通过提供对疾病分子机制的洞察,彻底改变了个性化医学。在一个多模态医学 AI 系统中,组学数据可以帮助更好地理解患者对某些疾病的易感性以及对治疗选项的潜在反应。例如,BRCA 基因中的特定突变表明,患者更有可能发展为某些类型的癌症。
患者与电子健康记录(EHR)笔记
传统上,由于缺乏结构,患者笔记(临床观察、治疗计划等)一直是分析的难点。然而,大型语言模型(LLMs)可以利用这些笔记提取见解、识别模式,并支持大规模数据分析,这在过去是无法实现的。例如,LLMs 可以阅读临床试验中潜在患者的笔记,并识别符合资格要求的患者——这是以前需要大量人工操作的任务。
可穿戴设备数据
健康监测传感器,如可穿戴健身追踪器,可以实时测量心率、血压、睡眠模式和血糖水平等生命体征。人工智能应用可以分析这些时间序列数据,发现趋势并预测健康事件。此类应用可以通过提供个性化的健康建议,帮助患者,并帮助医生在医院之外监控患者的健康状况。
音频记录
音频记录,如心脏和肺部听诊,通常用于诊断某些形式的疾病。医生通过心脏听诊来标记心脏杂音的范围和强度,而肺部听诊可以帮助识别肺炎等疾病。人工智能系统可以分析这些音频记录,检测异常,并协助更快且成本更低的诊断。
病理学
病理数据来源于组织样本和显微图像,在诊断癌症等疾病中起着关键作用。人工智能算法可以分析这些数据源,识别异常的细胞结构、分类组织类型,并检测出提示疾病的模式。通过处理大量病理数据,人工智能可以帮助病理学家做出更准确的诊断,标出潜在的关注区域,甚至预测疾病进展。事实上,哈佛医学院和麻省理工学院的研究人员最近推出了一个多模态生成式 AI 助手用于人体病理学,帮助病理学家处理常见的医疗任务。

集成标注图像与文本数据的示例。图像由作者提供。
多模态人工智能模型的应用
多模态算法有潜力开启人工智能驱动医疗应用的新范式。多模态人工智能的一个有前景的应用是个性化医学,在这种应用中,系统通过整合患者的病情、病史、生活方式和基因组等数据,预测最有效的治疗方案。例如,考虑一个旨在为肺癌患者识别最有效治疗方案的应用程序。该应用可以考虑患者的基因组特征、病理(组织样本)图像和记录、放射学图像(肺部 CT 扫描)和记录,以及病史临床记录(收集吸烟史和环境影响等因素)。通过整合所有这些数据源,应用程序可以推荐最适合患者独特体质的治疗方案。这种方法已经在黄等人研究中显示出有前景的结果,研究人员能够根据患者的基因表达谱预测他们对常规化疗药物的反应,准确率超过 80%。这种方法将有助于最大化治疗效果,并减少通常与寻找合适药物或干预措施相关的试错过程。
另一个关键的应用场景是提高诊断和预后速度与准确性。通过整合医学影像、实验室结果和病历记录等数据源,多模态医学人工智能系统可以帮助医生获得全面的洞察。例如,Tempus Next 利用超声心动图和心电图的波形数据、电子健康记录文本数据以及腹部放射影像(CT 扫描、超声波检查)来帮助心脏病专家诊断和预测患者的心脏问题风险,如腹主动脉瘤和房颤。Optellum 的虚拟结节诊所采用类似的方法,通过使用 CT 扫描和临床记录帮助诊断肺癌。这类应用不仅提高了诊断的准确性,还节省了医生的时间,从而帮助解决持续的医生短缺问题,并降低了医疗成本。
多模态 AI 还将通过集成来自可穿戴设备、家庭监测系统和患者自我报告的记录的数据,推动远程患者监测和远程医疗的重大进展,为患者健康状态提供持续的实时洞察。这一能力对慢性病管理尤其重要,因为持续的监测可以检测到恶化的早期迹象,并及时采取干预措施。例如,一个 AI 系统可以监测来自Eight Sleep Pod的患者睡眠数据以及来自Levels(连续血糖监测)的血糖数据,以识别糖尿病前期患者的恶化情况。医生可以利用这一早期预警提出积极建议,帮助患者避免进一步的健康下降。这项技术有助于减少医院再入院率,改善慢性病的整体管理,使医疗服务更具可及性,并减轻整个医疗系统的负担。
构建多模态 AI 模型的方法

构建多模态系统的不同方法的示意图。图片由作者提供。
研究人员目前正在尝试不同的方法来构建多模态医疗 AI 系统,研究仍处于初步阶段。谷歌团队正在探索的三种主要系统开发方法是:
-
工具使用 — 在这种方法中,一个主控 LLM 将不同数据源的分析任务外包给专门的软件子系统,这些子系统是针对特定数据形式训练的。例如,LLM 可能会将胸部 X 光片转交给放射学 AI 系统,心电图分析交给专门的波形分析系统,然后将这些响应与患者记录整合,以评估心脏健康。这种方法允许子系统之间具有灵活性和独立性,使得每个特定任务都能使用最佳的工具。
-
模型嫁接 — 这种方法涉及将每个相关领域的专门神经网络进行调整,并直接集成到 LLM 中。例如,一个经过训练用于解读医学图像的神经网络可以通过将其输出直接映射到 LLM 的输入空间,将其嫁接到 LLM 中。这种方法利用了现有的优化模型,允许模块化开发,尽管它需要为每个特定的模型和领域创建适配器。
-
通用系统 — 最具雄心的方法是构建一个能够本地处理所有数据模态的单一集成系统。这种方法使用统一模型,例如Med-PaLM M,它将语言模型与视觉编码器结合,处理多种数据类型。尽管这种方法最大化了灵活性和信息传递,但也伴随着更高的计算成本,并可能在领域专门化和系统调试性方面面临挑战。
实现多模态 AI 模型的挑战
尽管构建多模态人工智能模型充满了巨大潜力,但在实现可行系统时仍面临诸多挑战。以下是一些挑战:
-
数据标注 — 为了启用监督学习,机器学习算法需要由专家人工标注数据,并正确识别特征。跨领域识别专家以标注不同类型的数据模态可能具有挑战性。模型构建者应考虑与具有跨模态专业知识的数据标注服务提供商合作,例如Centaur Labs。
-
避免偏见 — 部署 AI 系统在医疗环境中的最大风险之一是它们可能加剧现有的偏见和医疗不平等。多模态系统可能进一步加深这种偏见,因为代表性不足的群体在系统为其构建的一个或多个模态中更可能缺失数据。为了避免偏见,模型构建者应考虑减少 AI 应用中偏见的技术。
-
监管 — 数据隐私法规如 HIPAA 对患者数据的共享和使用施加了严格的控制,这使得开发人员在整合和关联跨不同模态的数据时面临挑战。这需要额外的开发工作以确保合规性。
-
采纳与信任 — 许多传统的 AI 系统发现影响最大的障碍是推动医疗用户社区的采纳和信任。医生们担心 AI 输出的准确性和一致性,不希望在使用这些系统来为患者护理提供决策前,因过度信任而危及患者健康。多模态 AI 模型将面临类似的采纳障碍。开发人员必须与此类系统的最终用户密切合作,建立信任,确保系统能够融入现有的临床工作流程。
-
数据格式共享标准缺乏 — 对于许多数据格式(例如,组织图像),不同提供者之间没有标准化的数据共享协议。这种互操作性的缺失可能会阻碍整合开发强大 AI 模型所需的数据源。为了加快在(目前)尚未标准化的医疗数据领域中运行 AI 系统的开发和应用,研究与开发社区应制定通用的数据共享标准/框架,并确保各机构遵守这些标准。
结论
多模态 AI 代表了医疗应用的未来,通过整合和全面利用数据,提供了革命化医疗保健的潜力,提升应用的灵活性、准确性和能力。如果这些应用能够有效开发和部署,它们有望降低医疗成本,扩大可及性,并提供更高质量的患者护理和结果。
知识和技术的最重大进展通常来自于跨领域的见解融合。以列奥纳多·达·芬奇为例,他将自己在绘画和流体力学方面的知识应用于心脏和生理学的研究。医疗人工智能也不例外。通过将计算机科学的发现融入医学,开发者开启了一波初步的突破。现在,融合多种数据模态的前景将带来第二波创新,这一波创新将由日益智能化的人工智能系统推动。
文档提取是 GenAI 的杀手级应用
未来已经到来,你不会看到致命的机器人。你会看到用于繁琐办公室工作的优秀自动化。
·发表于Towards Data Science ·阅读时间 9 分钟·2024 年 8 月 13 日
--
几乎十年前,我在 LinkedIn 的著名数据标准化团队担任机器学习工程师。从我加入的那一天到离开的那一天,我们仍然无法自动读取一个人的个人资料,并可靠地理解某人在所有语言和地区中的职级和职位。
乍一看,这看起来很简单。“软件工程师”已经足够明确了,对吧?那如果有人只写“助理”呢?如果他在沃尔玛,那可能是一个低级零售工人;如果他在律师事务所,那可能是一个高级律师。但你大概已经知道了——你知道什么是Java Fresher吗?什么是Freiwilliges Soziales Jahr?这不仅仅是了解德语——它翻译为“志愿社会年”。但是,什么是代表这一角色的标准职称呢?如果你有一大堆已知的职位名称,你会把它映射到哪里?
我加入了 LinkedIn,我离开了 LinkedIn。我们取得了一些进展,但即使是最简单的常规文本——一个人的简历,依然难以理解。
非常困难变得简单
你可能不会感到惊讶地发现,这个问题对于像 GPT-4 这样的 LLM 来说是微不足道的

对 GPT 来说轻松简单(来源:我和 GPT)
等等,我们是公司,不是一个聊天终端上的人,我们需要结构化的输出。

(来源:GPT)
啊,这样好多了。你可以对那些最复杂且具有文化特定性的提问进行重复练习。更棒的是,当你得到一个完整的个人资料时,你可以重复这个练习,这样你就能获得更多的背景信息;使用代码时,你可以在商业环境中稳定地使用这些结果,而不仅仅是进行一次性的聊天。通过更多的工作,你可以将结果强制转换为一个标准的可接受职位标题分类法,这样它就能被索引。毫不夸张地说,如果你复制并粘贴某个人的全部简历并正确提示 GPT,你将超越十年前一些相当聪明的人的最佳成果,这些人花了多年时间在这方面工作。
高价值办公室工作 == 理解文档
标准化简历的具体例子很有趣,但它仍然局限于技术一直在努力的领域——一个自然应用人工智能工具的技术网站。我认为这里有更深层的机会。全球 GDP 的大部分来自办公室工作,这些工作归结为将专家级人类智能应用于从文档中反复提取洞见,且需要考虑背景信息。以下是一些复杂度逐渐增加的例子:
-
费用管理就是读取发票并将其转化为标准化视图,显示支付了什么、何时支付、使用了哪种货币以及属于哪个费用类别。可能这个决策是基于关于业务、报销人等背景信息做出的。
-
医疗保险理赔裁定过程就是阅读一堆杂乱的发票和临床记录,并判断:“好吧,总共有一次胸部 X 光检查,包含了一些重复项目,总费用为 800 美元,且它对应健康保险政策中的 1-C 类别”。
-
贷款核准员可能会查看一堆申请人的银行账单并回答一系列问题。同样,这之所以复杂,仅仅因为输入信息杂乱无章。实际的决策过程像是:“现金的平均流入和流出是多少,多少用于贷款偿还,其中有多少是一时性的支出,多少是实际的经常性收入”。
关于文本推理是 LLM 的强项。
到现在为止,大型语言模型(LLMs)因易发生幻觉(即胡乱编造)而臭名昭著。现实情况更为复杂:幻觉实际上在某些情境下是可预测的结果,而在其他情况下则几乎可以保证不会发生。
产生幻觉的地方是当你让它回答事实性问题,并期望模型仅凭其对世界的固有知识“知道”答案时。大语言模型(LLM)在自我反思自己对世界的知识方面表现得很差——它们能做到这一点更多是一个非常偶然的结果。它们并没有专门为此任务进行训练。它们的训练目标是生成可预测的文本序列补全。当 LLM 被绑定到输入文本并需要回答关于该文本内容的问题时,它不会产生幻觉。如果你将这篇博客文章复制并粘贴到 chatGPT 中,问它是否教你做美国苹果派,你会 100%得到正确答案。对于 LLM 来说,这是一个非常可预测的任务,它看到一段文本,并尝试预测一个有能力的数据分析师如何用预定义的字段和预定义的结果来填写,这些结果之一是{"is cooking discussed": false}。
以前作为 AI 顾问,我们多次解决涉及从文档中提取信息的项目。事实证明,在保险、金融等领域这方面有很多用途。客户之间对 LLM 的恐惧(“LLM 会产生幻觉”)与实际上摧毁我们的原因(我们没有正确提取表格,所有错误都源于此)之间存在很大的差距。LLM 确实失败了——当我们没有以清晰且不含歧义的方式呈现输入文本时,它们会失败。构建能够推理文档的自动化管道需要两个必要的成分:
-
完美的文本提取,将输入文档转换为干净、易懂的纯文本。这意味着需要处理表格、勾选框、手写注释、可变文档布局等。现实世界表单的复杂性需要转化为 LLM 能够理解的清晰纯文本。
-
健壮的架构,明确规定你希望从给定文档类型中获得哪些输出,如何处理边缘案例,使用什么数据格式等等。
文本提取比看起来要复杂得多
这就是导致 LLM 崩溃和产生荒谬输出的原因:
-
输入中有复杂的格式,例如双列布局,你从例如 PDF 中从左到右复制并粘贴文本,完全把句子从上下文中剥离开来。
-
输入中有复选框、勾选标记、手写注释,而你在转换为文本时完全忽略了这些
-
更糟糕的是:你认为可以绕过转换为文本的步骤,指望只是粘贴一张文档的图片,让 GPT 自行推理。这会让你进入幻觉之城。只需让 GPT 转录一张带有空白单元格的表格图片,你就会看到它高兴地胡乱发挥,乱编一些东西。
时常记住现实世界文档的混乱程度是很有帮助的。这里有一份随意的税表:

当然,真实的税表上这些字段通常是填写完整的,且常常是手写的
或者这是我的简历

来源:我的简历
或者一个公开的示例实验报告(这是 Google 的首页结果)

来源:research gate,公共领域图像
顺便说一句,最糟糕的事情就是让 GPT 的多模态能力来转录一个表格。如果你敢试试——一开始看起来没问题,但它会为某些表格单元格随意编造内容,完全脱离上下文等。
如果这个世界有问题,那就建立一个 SaaS 公司来解决它。
当我们需要理解这些类型的文档时,我和我的联合创始人Nitai Dean感到困惑,因为没有现成的解决方案可以帮助我们理解这些文本。
有些人声称能够解决这个问题,比如 AWS Textract。但在我们测试的任何复杂文档中,它们都会犯很多错误。接着就是那些必须处理的小细节,比如识别勾选框、单选按钮、删除线文本、表单上的手写涂鸦等。
所以,我们建立了Docupanda.io——它首先生成任何你输入的页面的清晰文本表示。左边是原始文档,右边是文本输出。

来源:docupanda.io
表格也类似地处理。在背后,我们只是将表格转换为人类和 LLM 可读的 Markdown 格式:

来源:docupanda.io
使用 LLM 来理解数据的最后一块拼图是生成并遵循严格的输出格式。虽然我们可以让 AI 将输出格式化为 json,但为了对数据应用规则、推理、查询等——我们需要让它以规律的方式运作。数据需要符合预定义的插槽集,我们将用内容填充这些插槽。在数据领域,我们称之为模式。
构建模式是一个试错过程……这是一个 LLM 可以做的事情。
我们需要模式的原因是,数据没有规律性是毫无意义的。如果我们在处理患者记录,并且它们映射到“male”、“Male”、“m”和“M”——那我们做得很糟糕。
那么,如何构建一个模式呢?在教科书中,你可能通过长时间地坐在那里,盯着墙壁,来定义你想要提取的内容。你坐在那里,思考你的医疗数据操作,并说“我想提取患者姓名、日期、性别和他们的医生姓名。哦,性别必须是 M/F/Other。”
在现实中,为了定义从文档中提取什么内容,你得盯着文档看……很多。你一开始可能像上面那样,但是当你看文档时,你会发现其中一个文档列出了多个医生的名单,而不是一个。还有些文档也列出了医生的地址。一些地址包含单元号和楼栋号,所以你可能需要为此预留一个字段。事情就是这么一件接一件地发生。
我们意识到,能够准确定义你想要提取的所有内容,是既不简单、又困难,但使用 AI 是完全可以解决的。
这是 DocuPanda 的关键部分。我们不是仅仅要求一个 LLM 为每个文档即兴生成输出,而是建立了一个机制,让你可以:
-
使用自由语言指定你需要从文档中提取的内容。
-
让我们的 AI 映射 多个 文档,并找出一个能回答所有问题、并适应实际文档中观察到的漏洞和不规则之处的模式。
-
根据反馈调整模式,以适应你的业务需求。
最终你得到的是一个强大的 JSON 模式 —— 一个模板,明确指出你想从每个文档中提取的内容,并且能够处理数十万份文档,从中提取所有答案,同时遵循像始终以相同格式提取日期、尊重一组预定义类别等规则。

来源:docupanda.io
还有更多!
就像任何兔子洞一样,总是有比最初看到的更多的东西。随着时间的推移,我们发现需要更多的东西:
-
组织经常需要处理大量匿名文档,因此我们会自动对它们进行分类,并决定应用哪种模式。
-
文档有时是多个文档的拼接,你需要一个智能的解决方案将一篇非常长的文档拆分成其原子化的独立组件。
-
使用生成的结果查询正确的文档非常有用。
如果从这篇文章中能得到一个启示,那就是你应该研究如何利用 LLM 来以规范的方式理解文档。如果有两个启示,那就是你也应该试试 Docupanda.io。我之所以构建它,是因为我相信它。也许这就足够成为尝试它的理由?

未来的办公室工作人员(来源:unsplash.com)
使用大语言模型进行文档解析——附带代码
你将不再考虑使用正则表达式
·发布于Towards Data Science ·14 分钟阅读·2024 年 7 月 25 日
--
动机
多年来,正则表达式一直是我解析文档的首选工具,我相信对于许多其他技术人员和行业来说也是如此。
尽管正则表达式在某些情况下非常强大且成功,但它们在面对现实世界文档的复杂性和多变性时常常显得力不从心。
另一方面,大语言模型提供了一种更强大且灵活的方法来处理多种类型的文档结构和内容类型。
系统的一般工作流程
理解正在构建的系统的主要组件总是很重要的。为了简单起见,我们聚焦于一个科研论文处理的场景。

使用 LLM 进行文档解析工作流程(作者:Zoumana Keita)
-
工作流程总体上包括三个主要组件:输入、处理和输出。
-
首先,文档(在此案例中为 PDF 格式的科研论文)提交进行处理。
-
处理组件的第一个模块从每个 PDF 中提取原始数据,并将其与包含大语言模型指令的提示结合起来……
使用 MkDocs 记录 Python 项目
使用 Markdown 快速为你的项目创建一个漂亮的文档页面
·发布于 Towards Data Science ·阅读时间 9 分钟·2024 年 11 月 22 日
--

图片由 OpenAI 的 DALL·E 生成。openai.com. 使用 MkDocs 进行 Python 文档编写。
介绍
项目文档是必需的。非常必要,我必须强调这一点。
在我职业生涯的开始,我通过痛苦的经历学到了一个重要的教训,那就是一个项目必须有文档。
让我们回到过去——2000 年代——那时我在大型美国公司担任客户代表。我是一个团队的一员,我和我的同事们大约在同一个月加入了公司。因此,一段时间内,我们不需要担心,因为刚刚开始新工作的人不可能在几周或几个月后就请假。
然而,经过一段时间,这种情况不可避免地会发生。而且我们每个人都被分配去互相备份。正是在那时,文档开始在我的职业生涯中扮演重要角色。
那天,第一个人请了几天假,我慌了!我开始工作时,不知道该做什么,甚至不知道从哪里开始。任务源源不断地涌来,而我则在努力弄清楚如何处理它们。
最终,一切都顺利解决了。我终于弄明白了并继续前进。但从那天起,我知道无论是休假还是团队成员的变动,如晋升等,文档都必须到位。
数据驱动的故事讲述是否需要客观?
在数据驱动故事中找到效率与吸引力之间的平衡
·发表于Towards Data Science ·阅读时长:11 分钟·2024 年 7 月 29 日
--

来源:图片由作者提供。
我一直想学会解魔方。 我在 10 到 12 岁的时候有一个魔方。我从未成功过。即使成功了,那也只是通过“调整”方块之间的贴纸。所以,那并不算真正的成功。
问题是什么?我曾以为解魔方是一种智力和逻辑的练习。我错了。多亏了 YouTube 上的一些教程,30 年后,我终于学会了如何解魔方。事实证明,你可以通过遵循一系列算法来解决它。没有魔法——只是记住一些规则和步骤(幸运的是,至少对我来说,还是需要一些脑力努力)。
数据驱动故事的魔力
我曾经也遇到过类似的数据驱动故事讲述问题。让我们把它放在一个更具体的情境中,就像我 10 岁时试图解决魔方一样。你可能认为数据驱动的故事讲述是用来向观众讲述一个关于数据和分析的客观故事——结论、假设、优点和缺点。观众应该知道所有的事实,才能做出充分知情的决策。这也可能包括分享那些证明是错误的分析场景。最终,观众应该…
半监督学习是否有助于训练更好的模型?
评估半监督学习如何利用未标注数据
·发表于 Towards Data Science ·阅读时长 8 分钟·2024 年 9 月 9 日
--

作者提供的图像 — 使用必应的图像生成器创建
数据科学家面临的最常见挑战之一是缺乏足够的标注数据来训练一个可靠且准确的模型。标注数据对于监督学习任务(如分类或回归)至关重要。然而,在许多领域,获取标注数据可能成本高昂、耗时或不可行。另一方面,未标注数据通常容易收集,但它们并不能直接用于训练模型。
我们如何利用未标注数据来提高监督学习模型的性能?这正是半监督学习发挥作用的地方。半监督学习是机器学习的一个分支,它结合了标注数据和未标注数据,以训练一个比仅使用标注数据表现更好的模型。半监督学习背后的直觉是,未标注数据可以提供关于数据潜在结构、分布和多样性的有用信息,这有助于模型更好地泛化到新的和未见过的样本。
在这篇文章中,我介绍了三种可以应用于不同类型数据和任务的半监督学习方法。我还将评估它们在实际数据集上的表现,并将其与仅使用标注数据的基准进行比较。
什么是半监督学习?
半监督学习是一种机器学习方法,它同时利用已标记数据和未标记数据来训练模型。已标记数据是指那些已知输出或目标变量的样本,比如分类任务中的类别标签或回归任务中的数值。未标记数据是指那些没有已知输出或目标变量的样本。半监督学习可以利用在实际问题中通常大量存在的未标记数据,同时也利用较少的已标记数据,而后者通常更加昂贵或耗时。
使用未标记数据来训练监督学习方法的基本思想是通过监督或无监督学习方法给这些数据打上标签。尽管这些标签可能不像实际标签那样准确,但拥有大量此类数据可以提升监督学习方法的表现,相比只用已标记数据进行训练。
scikit-learn 包提供了三种半监督学习方法:
-
自训练:首先仅在已标记数据上训练分类器,以预测未标记数据的标签。在下一轮中,另一个分类器在已标记数据和从未标记数据中预测出高置信度标签的结果上进行训练。这个过程会重复进行,直到没有新的高置信度标签被预测,或者达到最大迭代次数为止。
-
标签传播:构建一个图,其中节点表示数据点,边表示它们之间的相似性。标签通过图进行迭代传播,从而使算法能够根据已标记数据与未标记数据之间的连接关系,将标签分配给未标记数据点。
-
标签传播:使用与标签传播相同的概念。区别在于标签扩散使用软分配,在这种方法中,标签是根据数据点之间的相似性进行迭代更新的。这种方法还可能“覆盖”已标记数据集的标签。
为了评估这些方法,我使用了一个糖尿病预测数据集,该数据集包含患者的年龄、BMI 等特征,以及描述患者是否患有糖尿病的标签。该数据集包含 100,000 条记录,我将其随机分为 80,000 条训练数据、10,000 条验证数据和 10,000 条测试数据。为了分析在不同标签数据量下学习方法的有效性,我将训练数据分为标签数据集和无标签数据集,其中标签大小表示有多少样本已标记。

数据集划分(图片来源:作者)
我使用验证数据评估了不同参数设置,并使用测试数据评估了各方法在参数调优后的性能。
我使用了 XGBoost 进行预测,并使用 F1 得分来评估预测性能。
基准
基准模型用于将自学习算法与不使用任何无标签数据的情况进行比较。因此,我在不同大小的标签数据集上训练了 XGB,并计算了验证数据集上的 F1 得分:

基准分数(图片来源:作者)
结果显示,对于少于 100 个样本的训练集,F1 得分相对较低,但随着样本量的增加,得分稳定提高,到达 1,000 个样本时达到了 79%的得分。更大的样本量对 F1 得分的提升几乎没有作用。
自学习
自学习是通过多次迭代来预测无标签数据的标签,这些标签将在下一次迭代中用于训练另一个模型。可以使用两种方法来选择将作为标签数据用于下一次迭代的预测:
-
阈值(默认):选择所有置信度超过阈值的预测
-
K 最优:选择置信度最高的 k 个预测
我评估了默认参数(ST Default),并根据验证数据集调优了阈值(ST Thres Tuned)和 k 最优参数(ST KB Tuned)。这些模型的预测结果在测试数据集上进行了评估:

自学习得分(图片来源:作者)
对于小样本量(<100),默认参数(红线)的表现不如基准(蓝线)。对于更大的样本量,模型取得的 F1 得分稍微优于基准。调整阈值(绿线)带来了显著的提升。例如,在标签大小为 200 时,基准 F1 得分为 57%,而调整阈值后的算法 F1 得分为 70%。除了标签大小为 30 时的一个例外,调整 K 最优值(紫线)几乎与基准表现相同。
标签传播
标签传播有两种内置的核方法:RBF 和 KNN。RBF 核通过使用密集矩阵生成一个完全连接的图,这在大数据集上需要大量内存且耗时较长。为了考虑内存限制,我只使用了最多 3,000 个样本进行 RBF 核的训练。KNN 核则使用更节省内存的稀疏矩阵,这使我能够在最多 80,000 个样本的整个训练数据上进行训练。以下图表展示了这两种核方法的结果比较:

标签传播得分(图片由作者提供)
图表显示了不同标签传播方法在测试数据集上的 F1 得分与标签大小的关系。蓝色线表示基准线,与自我训练相同。红色线表示使用默认参数的标签传播,明显低于基准线,且所有标签大小下都表现较差。绿色线表示使用 RBF 核和调优后的参数 gamma 的标签传播。Gamma 定义了单个训练样本的影响范围。调优后的 RBF 核在小标签大小(<=100)下表现优于基准线,但在大标签大小下则表现较差。紫色线表示使用 KNN 核和调优后的参数 k 的标签传播,k 决定了使用多少个最近邻。KNN 核的表现与 RBF 核相似。
标签扩展
标签扩展是一种类似于标签传播的方法,但增加了一个控制实例应该接受多少邻居信息的参数 alpha。Alpha 的取值范围为 0 到 1,其中 0 表示实例保持其原始标签,1 表示实例完全采用邻居的标签。我还对 RBF 和 KNN 核方法进行了调优。标签扩展的结果显示在下图中:

标签扩展得分(图片由作者提供)
标签传播的结果与标签扩展非常相似,唯一显著的例外是,RBF 核方法在标签扩展中的测试得分低于基准得分,且所有标签大小下都如此,不仅仅是小标签大小。这表明,邻居标签的“覆盖”对这个数据集有相当负面的影响,该数据集可能只有少数离群点或噪声标签。另一方面,KNN 核方法不受 alpha 参数的影响。似乎这个参数只与 RBF 核方法相关。
所有方法的比较
接下来,我将所有方法的最佳参数进行对比。

最佳得分比较(图片由作者提供)
图表显示了不同半监督学习方法的测试得分与标签大小的关系。自我训练优于基线,因为它很好地利用了无标签数据。标签传播和标签扩散仅在小标签大小下超过基线,对于较大的标签大小,表现较差。
结论
结果可能因不同的数据集、分类器方法和评估指标而显著变化。半监督学习的性能依赖于许多因素,例如无标签数据的质量和数量、基本学习器的选择以及评估标准。因此,在没有适当的测试和验证的情况下,不应将这些发现推广到其他设置中。
如果你有兴趣进一步探索半监督学习,欢迎查看我的 Git 仓库并自行实验。你可以在这里找到这个项目的代码和数据。
我从这个项目中学到的一点是,参数调优对于显著提升这些方法的性能至关重要。通过优化参数,自我训练在任何标签大小下都优于基线,并且 F1 分数提高了最多 13%!标签传播和标签扩散仅在非常小的样本量下有所改进,但用户必须非常小心,以免得到比不使用任何半监督学习方法更差的结果。
在招聘过程中使用大型语言模型(LLM)会让你成为候选人中的骗子吗?
雇主们,放弃 AI 检测工具,改问一个重要的问题。
·发表于 Towards Data Science ·12 分钟阅读·2024 年 1 月 6 日
--
我在 LinkedIn 上看到一篇来自一家咨询公司总监的帖子,描述了他如何布置一篇关于机器学习系统漂移的作文题目,以筛选潜在的候选人。
然后,基于他通过直觉建立的标准(“你可以闻到它”),他使用了四种不同的“AI 检测器”来“确认”申请者是否使用 ChatGPT 写了他们的作文回答。
[## Adam Sroka 在 LinkedIn 上发布:我自动拒绝了一个在招聘过程中使用 LLM 的候选人。我设定了一个讨论机器学习系统和漂移的任务。…|…
我自动拒绝了一个在招聘过程中使用 LLM 的候选人。我设定了一个讨论机器学习系统和漂移的任务。这个…
“涉嫌”机器人生成的作文的标准包括:
-
奇怪的句子结构
-
奇怪的类比
-
重复
-
从一个英语方言切换到另一个方言(在同一申请中的不同写作样本中)
有一个标准显然缺失:准确性。
这么做的理由是,使用 AI 工具是在试图颠覆候选人筛选过程。不用说,评论区简直乱套(而且非常典型 LinkedIn 风格)。
你的公司有数据战略吗?
这个复杂度矩阵可以显示你需要前往的方向
·发布于Towards Data Science ·7 分钟阅读·2024 年 4 月 8 日
--
如今每家公司似乎都沉浸于制定人工智能战略。从个体经营者到大型组织,过去 6 个月最常见的问题是——我如何在我的业务中使用人工智能?往往是在“我应该吗?”之前就已经有了这个问题。
幸运的是,随着由人工智能构建的新工具爆炸式增长,个人或公司比以往任何时候都更容易将人工智能融入到日常工作中,且我们每个人都应该不断思考,是否在用现有技术以最有效的方式开展工作。但当它成为一个重大投资或优先事项偏离时,就需要更深入的反思。
我坚信利用技术、人工智能和数据来推动现有的商业战略和优先事项。其目的是为了增强竞争优势,而不是成为优先事项本身。
当我思考人工智能及其成功潜力时,我会从三个方面来看——是的,当然有那些似乎得到所有关注和热议的机器学习和模型,但同样重要(如果不是更重要的话)的是那些构建或使用它的人,最后,还有支撑它的数据和基础设施。
所以,如果你是一个商业领袖,正在考虑 2024 年你应该制定什么样的人工智能战略,那么我建议你首先考虑,你当前的数据战略是什么?
狗狗排便指南针
狗狗行为的贝叶斯分析
·发表于Towards Data Science ·阅读时间:22 分钟·2024 年 11 月 25 日
--
tl;dr
狗狗是朝北和朝南排便吗?结果证明它们确实是!想学会如何使用指南针应用程序、贝叶斯统计和一只狗(狗狗不包括在内)在家里测量这个现象吗?那就跟我一起看看吧!
引言
这是我的狗。它的名字叫 Auri,是一只 5 岁的凯文犬(Cavalier King Charles Spaniel)。

Auri(图由作者提供)
和许多其他狗主人一样,在我们散步时,我注意到 Auri 在需要去厕所时有一个非常独特的仪式。当他找到一个合适的地点时,他会开始围绕某个东西转圈,就像一个指南针。
起初,我只是觉得这种行为很有趣。毕竟,谁知道狗狗在想些什么呢?但过了一段时间,我记得曾读过一篇 2013 年的研究论文,标题是“Dogs are sensitive to small variations of the Earth’s magnetic field”。这项研究在相对较大的狗狗样本中进行,证实了“狗狗倾向于在北南轴对齐的情况下排便”。
这似乎是个有趣的研究课题! 我心想。多么幸运,我正好有一个完美的实验对象——我自己心爱的狗狗。我决定复现这些发现,并通过 Auri 这个 N=1 的意外研究参与者来验证(或推翻!)这个假设。
就这样,我开始了长达几个月的数据收集之旅,记录了超过 150 次的“对齐”行为,如果你明白我的意思的话。
数据收集
对于我的研究,我需要记录每次 Auri 排便时的指南针数据。得益于现代科技的进步,我们不仅有iPad 的计算器应用,还可以在手机上使用相当精确的指南针。于是我决定使用这个。
方法非常简单。每次我的狗安静下来准备享受私人时光时,我就打开指南针应用程序,把手机与 Auri 的身体对齐,并截图。在原始论文中,作者优雅地将这种对齐称为胸椎(肩胛骨之间)朝向头部的指南针方向。非常科学。其实就是意味着指南针的箭头应指向与狗头相同的方向。

狗对地球磁场微小变化非常敏感,Vlastimil Hart 等人
总之,这就是我在几个月的时间里总共做了大约 150 次的事情。每当我似乎在拍摄我家狗狗的排泄行为时,我几乎能感觉到路人们混合着困惑和好奇的目光。但这值得吗?让我们来看看吧!
分析
我将在这里简要讨论数据提取和预处理,然后直接进入圆形分布和假设检验的部分。
和往常一样,所有代码都可以在我的 GitHub 上找到:Data Wondering。
如何处理应用截图?
数据收集后,我得到了若干张指南针应用截图:

指南针应用截图(作者提供的图片)
因为我懒得一张张图片地查看并耐心地写下指南针的度数,所以我决定将所有这些图片发送到我的笔记本,并自动化这个过程。
任务很简单——我只需要从这些图片中获取屏幕底部的大数字。幸运的是,有很多小型的预训练网络可以做基本的光学字符识别(OCR)。我选择了一个名为[easyocr](https://github.com/JaidedAI/EasyOCR)的包,虽然它稍慢一点,但免费且容易使用。
我将为你展示一个简单的例子,演示如何使用easyocr与[opencv](https://opencv.org/)结合,从单张截图中提取数字。
首先,我们加载图像并显示它:
import cv2
import os
image_dir = '../data/raw/'
img = cv2.imread(image_dir + 'IMG_6828.png')
plt.imshow(img)
plt.show()

图片由作者提供
接下来,我将图像转换为灰度,以去除任何颜色信息并减少噪声:
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
plt.imshow(gray, cmap='gray')
plt.show()

图片由作者提供
接着,我将放大感兴趣的区域:

图片来源:作者
最后,我使用easyocr从图片中提取数字。它提取了数字和置信度评分,但我只对数字感兴趣。
import easyocr
reader = easyocr.Reader(['en'])
result = reader.readtext(gray[1850:2100, 200:580])
for bbox, text, prob in result:
print(f"Detected text: {text} with confidence {prob}")
>> Detected text: 340 with confidence 0.999995182215476
就是这样!我写了一个简单的 for 循环来遍历所有截图,并将结果保存到 CSV 文件中。
这是完整预处理笔记本的链接:数据预处理。

图片来源:作者
转动轮盘:圆形分布
我通常不处理圆形分布,因此我必须做一些阅读。与我们常见的常规数据不同,圆形数据有一个特殊的属性:分布的“端点”是连接的。
例如,如果你考虑一天中的小时分布,你会发现 23:00 到 00:00 之间的距离与 00:00 到 01:00 之间的距离是相同的。或者,在指南针角度的情况下,359°和 0°之间的距离与 0°和 1°之间的距离是相同的。
即使计算样本均值也不是直截了当的。360°和 0°之间的标准算术均值将是 180°,尽管 360°和 0°指向完全相同的方向。
在我的情况下,计算算术均值和正确的均值时,我得到了几乎完全相反的估算。我使用这个不错的库中的辅助函数将角度转换为弧度:pingouin,并使用circ_mean函数计算均值。
from pingouin import circ_mean
arithmetic_mean = data['radians'].mean()
circular_mean = circ_mean(data['radians'])
print(f"Arithmetic mean: {arithmetic_mean:.3f}; Circular mean: {circular_mean:.3f}")
>> Arithmetic mean: 0.082; Circular mean: 2.989
接下来,我想可视化指南针分布。我使用冯·米塞斯分布来建模圆形数据,并使用matplotlib绘制极坐标图。
冯·米塞斯分布是圆形分布的正态分布类比。它由两个参数定义:均值位置μ和集中度κ。集中度参数控制分布的扩展,类似于方差的倒数。当κ为 0 时,分布是均匀的,随着κ的增加,分布会围绕均值收缩。
让我们导入必要的库并定义辅助函数:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from scipy.stats import vonmises
from pingouin import convert_angles
from typing import Tuple, List
def vonmises_kde(series: np.ndarray, kappa: float, n_bins: int = 100) -> Tuple[np.ndarray, np.ndarray]:
"""
Estimate a von Mises kernel density estimate (KDE) over circular data using scipy.
Parameters:
series: np.ndarray
The input data in radians, expected to be a 1D array.
kappa: float
The concentration parameter for the von Mises distribution.
n_bins: int
The number of bins for the KDE estimate (default is 100).
Returns:
bins: np.ndarray
The bin edges (x-values) used for the KDE.
kde: np.ndarray
The estimated density values (y-values) for each bin.
"""
bins = np.linspace(-np.pi, np.pi, n_bins)
kde = np.zeros(n_bins)
for angle in series:
kde += vonmises.pdf(bins, kappa, loc=angle)
kde = kde / len(series)
return bins, kde
def plot_circular_distribution(
data: pd.DataFrame,
plot_type: str = 'kde',
bins: int = 30,
figsize: tuple = (4, 4),
**kwargs
) -> None:
"""
Plot a compass rose with either KDE or histogram for circular data.
Parameters:
-----------
data: pd.DataFrame
DataFrame containing 'degrees'and 'radians' columns with circular data
plot_type: str
Type of plot to create: 'kde' or 'histogram'
bins: int
Number of bins for histogram or smoothing parameter for KDE
figsize: tuple
Figure size as (width, height)
**kwargs: dict
Additional styling arguments for histogram (color, edgecolor, etc.)
"""
plt.figure(figsize=figsize)
ax = plt.subplot(111, projection='polar')
ax.set_theta_zero_location('N')
ax.set_theta_direction(-1)
# add cardinal directions
directions = ['N', 'E', 'S', 'W']
angles = [0, np.pi / 2, np.pi, 3 * np.pi / 2]
for direction, angle in zip(directions, angles):
ax.text(
angle, 0.45, direction,
horizontalalignment='center',
verticalalignment='center',
fontsize=12,
weight='bold'
)
if plot_type.lower() == 'kde':
x, kde = vonmises_kde(data['radians'].values, bins)
ax.plot(x, kde, color=kwargs.get('color', 'red'), lw=2)
elif plot_type.lower() == 'histogram':
hist_kwargs = {
'color': 'teal',
'edgecolor': 'black',
'alpha': 0.7
}
hist_kwargs.update(kwargs)
angles_rad = np.deg2rad(data['degrees'].values)
counts, bin_edges = np.histogram(
angles_rad,
bins=bins,
range=(0, 2*np.pi),
density=True
)
widths = np.diff(bin_edges)
ax.bar(
bin_edges[:-1],
counts,
width=widths,
align='edge',
**hist_kwargs
)
else:
raise ValueError("plot_type must be either 'kde' or 'histogram'")
ax.xaxis.grid(True, linestyle='--', alpha=0.5)
ax.yaxis.grid(True, linestyle='--', alpha=0.5)
ax.set_yticklabels([])
plt.show()
现在,让我们加载数据并绘制图表:
data = pd.read_csv('../data/processed/compass_degrees.csv', index_col=0)
data['radians'] = convert_angles(data['degrees'], low=0, high=360)
plot_circular_distribution(data, plot_type='histogram', figsize=(6, 6))
plot_circular_distribution(data, plot_type='kde', figsize=(5, 5))

狗狗排便的圆形直方图(图片来源:作者)
从直方图中可以明显看出,Auri 在选择缓解方向时有自己的偏好。北方方向有明显的峰值,南方方向则有一个低谷。很棒!

狗狗排便的圆形 KDE(图片来源:作者)
通过 KDE 图,我们可以获得分布的更平滑表示。好消息是,它离均匀圆形非常远。
该是进行统计验证的时候了!
统计学显著的排便
就像圆形数据需要特别的可视化和分布处理一样,它也需要特别的统计检验。
我将使用之前提到的 pingouin 库中的几个测试。我将使用的第一个测试是 Rayleigh 检验,它是一个用于检验圆形数据均匀性的测试。原假设认为数据在圆周上均匀分布,备择假设认为数据不均匀。
from pingouin import circ_rayleigh
z, pval = circ_rayleigh(data['radians'])
print(f"Z-statistics: {z:.3f}; p-value: {pval:.6f}")
>> Z-statistics: 3.893; p-value: 0.020128
好消息,大家!p 值小于 0.05,我们拒绝原假设。Auri 的战略性排便位置并非随机的!
唯一的缺点是该测试假设分布只有一个模态,并且数据是从冯·米塞斯分布中采样的。唉,那我们换个方法试试吧。Auri 的数据显然有多个模态。
接下来是 V 检验。该测试检查数据是否具有特定的均值方向且非均匀。从文档中我们得知:
V 检验比雷 leigh 检验有更高的效能,并且如果有理由相信某个特定的均值方向,推荐使用 V 检验。
完美!让我们试试。
从分布来看,很明显 Auri 更喜欢南方方向。我将均值方向设置为 π 弧度(南方),并运行测试。
from pingouin import circ_vtest
v, pval = circ_vtest(data['radians'], dir=np.pi)
print(f"V-statistics: {v:.3f}; p-value: {pval:.6f}")
>> V-statistics: 24.127; p-value: 0.002904
现在我们开始有进展了!p 值接近零,我们拒绝原假设。Auri 是一个统计学上显著的南向排便者!
贝叶斯排便:数学部分
现在换个完全不同的方式。让我们尝试一种贝叶斯方法。
一开始,我决定查看均值方向的估计值如何随着样本量的增加而变化。这个想法很简单:我会从一个圆形均匀的先验分布开始,并随着每一个新的数据点更新它。
我们需要定义一些概念,所以让我们开始进入数学部分。如果你不喜欢方程式,可以跳到下一部分,那里有很酷的可视化!
1. 冯·米塞斯分布
冯·米塞斯分布 p(θ∣μ,κ) 的概率密度函数为:

其中:
-
μ 是均值方向
-
κ 是集中参数(类似于正态分布中方差的倒数)
-
I₀(κ) 是第一类修正贝塞尔函数,确保分布是标准化的。
2. 先验和似然性
假设我们有:
- 先验 分布:

- 似然性 对于一个新的观察值 θₙ:

我们想使用贝叶斯定理更新我们的先验:

其中

3. 在 von Mises 形式中乘以先验和似然
两个 von Mises 分布,参数分别为 (μ1, κ1) 和 (μ2, κ2),它们的乘积会得到另一个 von Mises 分布,具有更新后的参数。我们一步步来看:
给定:

并且

后验与乘积成正比:

使用余弦和公式的三角恒等式:

这变为:

4. 转换为极坐标形式以获得后验
最后阶段!上面的表达式是一个伪装的 von Mises 分布。我们可以将其重新写成极坐标形式,从而估计更新后的平均方向和集中度参数。
令:

现在后验表达式简化为:

让我们暂停一下,仔细看看这个简化的表达式。
- 请注意,C cos(θ)+S sin(θ) 是两个向量 (C,S) 和 (cos(θ),sin(θ)) 的点积,我们可以将其表示为:

其中 ϕ 是向量 (C,S) 和 (cos(θ),sin(θ)) 之间的角度。
2. 向量的大小是:

3. 向量 (cos(θ),sin(θ)) 和正 x 轴之间的角度就是 θ,而 (C,S) 和正 x 轴之间的角度,由定义为:

4. 那么两个向量之间的角度是:

将我们的发现代入简化后的后验表达式:

或者

其中
- kappa_post 是后验的集中度参数:

- mu_post 是后验的平均方向

哇,我们做到了!后验也是一个 von Mises 分布,具有更新后的参数 (mu_post, kappa_post)。现在,我们可以通过每个新的观测值更新先验,并观察平均方向的变化。
贝叶斯小插曲:有趣的部分
欢迎回到那些跳过数学部分的朋友们,恭喜那些完成了的朋友!现在让我们编写贝叶斯更新代码,并可视化结果。
首先,让我们定义一些辅助函数,用于可视化后验分布。稍后我们将用它来创建一个漂亮的动画。
import imageio
from io import BytesIO
def get_posterior_distribution_image_array(
mu_grid: np.ndarray,
posterior_pdf: np.ndarray,
current_samples: List[float],
idx: int,
fig_size: Tuple[int, int],
dpi: int,
r_max_posterior: float
) -> np.ndarray:
"""
Creates the posterior distribution and observed samples histogram on a polar plot,
converts it to an image array, and returns it for GIF processing.
Parameters:
-----------
mu_grid (np.ndarray):
Grid of mean direction values for plotting the posterior PDF.
posterior_pdf (np.ndarray):
Posterior probability density function values for the given `mu_grid`.
current_samples (List[float]):
List of observed angle samples in radians.
idx (int):
The current step index, used for labeling the plot.
fig_size (Tuple[int, int]):
Size of the plot figure (width, height).
dpi (int):
Dots per inch (resolution) for the plot.
r_max_posterior (float):
Maximum radius for the posterior PDF plot, used to set plot limits.
Returns:
np.ndarray: Image array of the plot in RGB format, suitable for GIF processing.
"""
fig = plt.figure(figsize=fig_size, dpi=dpi)
ax = plt.subplot(1, 1, 1, projection='polar')
ax.set_theta_zero_location('N')
ax.set_theta_direction(-1)
ax.plot(mu_grid, posterior_pdf, color='red', linewidth=2, label='Posterior PDF')
# observed samples histogram
n_bins = 48
hist_bins = np.linspace(-np.pi, np.pi, n_bins + 1)
hist_counts, _ = np.histogram(current_samples, bins=hist_bins)
# normalize the histogram counts
if np.max(hist_counts) > 0:
hist_counts_normalized = hist_counts / np.max(hist_counts)
else:
hist_counts_normalized = hist_counts
bin_centers = (hist_bins[:-1] + hist_bins[1:]) / 2
bin_width = hist_bins[1] - hist_bins[0]
# set the maximum radius to accommodate both the posterior pdf and histogram bars
r_histogram_height = r_max_posterior * 0.9
r_max = r_max_posterior + r_histogram_height
ax.set_ylim(0, r_max)
# plot the histogram bars outside the circle
for i in range(len(hist_counts_normalized)):
theta = bin_centers[i]
width = bin_width
hist_height = hist_counts_normalized[i] * r_histogram_height
if hist_counts_normalized[i] > 0:
ax.bar(
theta, hist_height, width=width, bottom=r_max_posterior,
color='teal', edgecolor='black', alpha=0.5
)
ax.text(
0.5, 1.1, f'Posterior Distribution (Step {idx + 1})',
transform=ax.transAxes, ha='center', va='bottom', fontsize=18
)
ax.set_yticklabels([])
ax.grid(linestyle='--')
ax.yaxis.set_visible(False)
ax.spines['polar'].set_visible(False)
plt.subplots_adjust(top=0.85, bottom=0.05, left=0.05, right=0.95)
# saving to buffer for gif processing
buf = BytesIO()
plt.savefig(buf, format='png', bbox_inches=None, pad_inches=0)
buf.seek(0)
img_array = plt.imread(buf)
img_array = (img_array * 255).astype(np.uint8)
plt.close(fig)
return img_array
现在我们准备写更新循环了。记住我们需要设置先验分布。我将从一个圆形均匀分布开始,它等价于一个冯·米塞斯分布,集中度参数为 0。对于 kappa_likelihood,我设置了一个固定的中等集中度参数 2。这将使得后验更新更加明显。
# initial prior parameters
mu_prior = 0.0 # initial mean direction (any value, since kappa_prior = 0)
kappa_prior = 0.0 # uniform prior over the circle
# fixed concentration parameter for the likelihood
kappa_likelihood = 2.0
posterior_mus = []
posterior_kappas = []
mu_grid = np.linspace(-np.pi, np.pi, 200)
# vizualisation parameters
fig_size = (10, 10)
dpi = 100
current_samples = []
frames = []
for idx, theta_n in enumerate(data['radians']):
# compute posterior parameters
C = kappa_prior * np.cos(mu_prior) + kappa_likelihood * np.cos(theta_n)
S = kappa_prior * np.sin(mu_prior) + kappa_likelihood * np.sin(theta_n)
kappa_post = np.sqrt(C**2 + S**2)
mu_post = np.arctan2(S, C)
# posterior distribution
posterior_pdf = np.exp(kappa_post * np.cos(mu_grid - mu_post)) / (2 * np.pi * i0(kappa_post))
# store posterior parameters and observed samples
posterior_mus.append(mu_post)
posterior_kappas.append(kappa_post)
current_samples.append(theta_n)
# plot posterior distribution
r_max_posterior = max(posterior_pdf) * 1.1
img_array = get_posterior_distribution_image_array(
mu_grid,
posterior_pdf,
current_samples,
idx,
fig_size,
dpi,
r_max_posterior
)
frames.append(img_array)
# updating priors for next iteration
mu_prior = mu_post
kappa_prior = kappa_post
# Create GIF
fps = 10
frames.extend([img_array]*fps*3) # repeat last frame a few times to make a "pause" at the end of the GIF
imageio.mimsave('../images/posterior_updates.gif', frames, fps=fps)
就是这样!该代码将生成一个 GIF,展示每次新观测后,后验分布的更新。这里是辉煌的结果:

后验分布更新(图像由作者提供)
随着每次新的观测,后验分布变得越来越集中于真实的均值方向。如果我能用 Auri 的轮廓替代红线,那就完美了!
我们可以进一步可视化后验均值方向和集中度参数的历史。让我们绘制它们:
# Convert posterior_mus to degrees
posterior_mus_deg = np.rad2deg(posterior_mus) % 360
n_samples = data.shape[0]
true_mu = data['degrees'].mean()
# Plot evolution of posterior mean direction
fig, ax1 = plt.subplots(figsize=(12, 6))
color = 'tab:blue'
ax1.set_xlabel('Number of Observations')
ax1.set_ylabel('Posterior Mean Direction (Degrees)', color=color)
ax1.plot(range(1, n_samples + 1), posterior_mus_deg, marker='o', color=color)
ax1.tick_params(axis='y', labelcolor=color)
ax1.axhline(true_mu, color='red', linestyle='--', label='Sample Distribution Mean Direction')
ax1.legend(loc='upper left')
ax1.grid(True)
ax2 = ax1.twinx() # instantiate a second axes that shares the same x-axis
color = 'tab:orange'
ax2.set_ylabel('Posterior Concentration Parameter (kappa)', color=color) # we already handled the x-label with ax1
ax2.plot(range(1, n_samples + 1), posterior_kappas, marker='o', color=color)
ax2.tick_params(axis='y', labelcolor=color)
fig.tight_layout() # otherwise the right y-label is slightly clipped
sns.despine()
plt.title('Evolution of Posterior Mean Direction and Concentration Over Time')
plt.show()

后验均值、kappa 演变(图像由作者提供)
图表展示了后验均值方向和集中度参数随着每次新观测如何变化。均值方向最终收敛到样本值,而集中度参数随着估计的确定性增加而上升。
贝叶斯因子:PyMC 用于狗狗指南针
我最后想尝试的是使用贝叶斯因子方法进行假设检验。贝叶斯因子背后的思想非常简单:它是两个竞争假设/模型的边际似然比。
通常,贝叶斯因子定义为:

其中:
-
p(D∣Mi) 和 p(D∣Mj) 是在 i 和 j 假设下的数据边际似然
-
p(Mi∣D) 和 p(Mj∣D) 是给定数据后的模型后验概率
-
p(Mi) 和 p(Mj) 是模型的先验概率
结果是一个数值,告诉我们一个假设比另一个假设更有可能。解释贝叶斯因子的方式有很多种,其中一种常见的方法是使用哈罗德·杰弗里斯的杰弗里斯尺度:

你可能会问,模型是什么?很简单!它们是具有不同参数的分布。我将使用 PyMC 来定义模型,并从中采样后验分布。
首先,我们重新引入零假设。我仍然假设它是一个圆形均匀的冯·米塞斯分布,且kappa=0,但这次我们需要计算在此假设下数据的似然性。为了简化后续的计算,我们将计算对数似然。
# Calculate log likelihood for H0
log_likelihood_h0 = vonmises.logpdf(data['radians'], kappa=0, loc=0).sum()
接下来,是时候构建备择模型了。首先从一个简单的场景开始:单峰南方方向,在这个场景下,我假设分布集中在 180°或π弧度处。
单峰南方
让我们在 PyMC 中定义模型。我们将使用冯·米塞斯分布,并设置固定位置参数μ=π,同时为非负浓度参数κ设置半正态先验。这使得模型能够从数据中学习浓度参数,并检查南方方向是否更受偏好。
import pymc as pm
import arviz as az
import arviz.data.inference_data as InferenceData
from scipy.stats import halfnorm, gaussian_kde
with pm.Model() as model_uni:
# Prior for kappa
kappa = pm.HalfNormal('kappa', sigma=10)
# Likelihood
likelihood_h1 = pm.VonMises('angles', mu=np.pi, kappa=kappa, observed=data['radians'])
# Sample from posterior
trace_uni = pm.sample(
10000, tune=3000, chains=4,
return_inferencedata=True,
idata_kwargs={'log_likelihood': True})
这给我们提供了一个简单的模型,我们也可以将其可视化:
# Model graph
pm.model_to_graphviz(model_uni)

PyMC 模型图(作者提供的图片)
这是浓度参数κ的后验分布:
az.plot_posterior(trace_uni, var_names=['kappa'])
plt.show()

后验 kappa 分布(作者提供的图片)
剩下的就是计算备择模型的对数似然值和贝叶斯因子。
# Posterior samples for kappa
kappa_samples = trace_uni.posterior.kappa.values.flatten()
# Log likelihood for each sample
log_likes = []
for k in kappa_samples:
# Von Mises log likelihood
log_like = vonmises.logpdf(data['radians'], k, loc=np.pi).sum()
log_likes.append(log_like)
# Log-mean-exp trick for numerical stability
log_likelihood_h1 = np.max(log_likes) +\
np.log(np.mean(np.exp(log_likes - np.max(log_likes))))
BF = np.exp(log_likelihood_h1 - log_likelihood_h0)
print(f"Bayes Factor: {BF:.4f}")
print(f"Probability kappa > 0.5: {np.mean(kappa_samples > 0.5):.4f}")
>> Bayes Factor: 32.4645
>> Probability kappa > 0.5: 0.0649
因为我们是将备择模型的似然除以原假设模型的似然,所以贝叶斯因子表明数据在备择假设下的可能性增加了多少。在这种情况下,我们得到了 32.46,这是非常强的证据,表明数据不是均匀分布在圆周上,而是偏向南方方向。
然而,我们还计算了浓度参数kappa大于 0.5 的概率。这是一种简单的方法,用来检查分布是否显著不同于均匀分布。在单峰南方模型下,这个概率只有 0.0649,意味着分布仍然相当分散。
让我们尝试另一个模型:双峰南北混合模型。
双峰南北混合模型
这次我假设分布是双峰的,峰值分别位于 0°和 180°,正如我们在罗盘玫瑰图上看到的那样。
为了实现这一点,我需要使用两个具有不同固定均值方向和共享浓度参数的冯·米塞斯分布的混合。
首先,让我们定义一些辅助函数:
# Type aliases
ArrayLike = Union[np.ndarray, pd.Series]
ResultDict = Dict[str, Union[float, InferenceData.InferenceData]]
def compute_mixture_vonmises_logpdf(
series: ArrayLike,
kappa: float,
weights: npt.NDArray[np.float64],
mus: List[float]
) -> float:
"""
Compute log PDF for a mixture of von Mises distributions
Parameters:
-----------
series: ArrayLike
Array of observed angles in radians
kappa: float
Concentration parameter
weights: npt.NDArray[np.float64],
Array of mixture weights
mus: List[float]
Array of means for each component
Returns:
--------
float: Sum of log probabilities for all data points
"""
mixture_pdf = np.zeros_like(series)
for w, mu in zip(weights, mus):
mixture_pdf += w * vonmises.pdf(series, kappa, loc=mu)
return np.log(np.maximum(mixture_pdf, 1e-300)).sum()
def compute_log_likelihoods(
trace: az.InferenceData,
series: ArrayLike,
mus: List[float]
) -> np.ndarray:
"""
Compute log likelihoods for each sample in the trace
Parameters:
-----------
trace: az.InferenceData
The trace from the PyMC3 model sampling.
series: ArrayLike
Array of observed angles in radians
"""
kappa_samples = trace.posterior.kappa.values.flatten()
weights_samples = trace.posterior.weights.values.reshape(-1, 2)
# Calculate log likelihood for each posterior sample
log_likes = []
for k, w in zip(kappa_samples, weights_samples):
log_like = compute_mixture_vonmises_logpdf(
series,
kappa=k,
weights=w,
mus=mus
)
log_likes.append(log_like)
# Calculate marginal likelihood using log-sum-exp trick
log_likelihood_h1 = np.max(log_likes) + np.log(np.mean(np.exp(log_likes - np.max(log_likes))))
return log_likelihood_h1
def posterior_report(
log_likelihood_h0: float,
log_likelihood_h1: float,
kappa_samples: ArrayLike,
kappa_threshold: float = 0.5
) -> str:
"""
Generate a report with Bayes Factor and probability kappa > threshold
Parameters:
-----------
log_likelihood_h0: float
Log likelihood for the null hypothesis
log_likelihood_h1: float
Log likelihood for the alternative hypothesis
kappa_samples: ArrayLike
Flattened posterior samples of the concentration parameter
kappa_threshold: float
Threshold for computing the probability that kappa > threshold
Returns:
--------
summary: str
A formatted string containing the summary statistics.
"""
BF = np.exp(log_likelihood_h1 - log_likelihood_h0)
summary = (
f"Bayes Factor: {BF:.4f}\n"
f"Probability kappa > {kappa_threshold}: {np.mean(kappa_samples > kappa_threshold):.4f}"
)
return summary
现在回到模型:
mu1 = 0 # 0 degrees
mu2 = np.pi # 180 degrees
with pm.Model() as model_mixture_bimodal_NS:
# Priors for concentration parameters
kappa = pm.HalfNormal('kappa', sigma=10)
# Priors for component weights
weights = pm.Dirichlet('weights', a=np.ones(2))
# Define the von Mises components
vm1 = pm.VonMises.dist(mu=mu1, kappa=kappa)
vm2 = pm.VonMises.dist(mu=mu2, kappa=kappa)
# Mixture distribution
likelihood = pm.Mixture(
'angles',
w=weights,
comp_dists=[vm1, vm2],
observed=data['radians']
)
# Sample from the posterior
trace_mixture_bimodal_NS = pm.sample(
10000, tune=3000, chains=4, return_inferencedata=True, idata_kwargs={'log_likelihood': True})
# Get kappa samples
kappa_samples = trace_mixture_bimodal_NS.posterior.kappa.values.flatten()
再次,让我们可视化模型图和浓度参数κ的后验分布:
# Model graph
pm.model_to_graphviz(model_mixture_bimodal_NS)

PyMC 模型图(作者提供的图片)
# Posterior Analysis
az.plot_posterior(trace_mixture_bimodal_NS, var_names=['kappa'])
plt.show()

后验 kappa 分布(作者提供的图片)
最后,让我们计算贝叶斯因子和浓度参数κ大于 0.5 的概率:
log_likelihood_h1 = compute_log_likelihoods(trace_mixture_bimodal_NS, data['radians'], [mu1, mu2])
print(posterior_report(log_likelihood_h0, log_likelihood_h1, kappa_samples))
>> Bayes Factor: 214.2333
>> Probability kappa > 0.5: 0.9110
太棒了! 我们的两个指标都表明这个模型更适合数据。贝叶斯因子表明有决定性证据,并且大多数后验κ样本大于 0.5,均值为 0.99,正如我们在分布图上看到的那样。
在结束之前,让我们再试试其他几个模型。
双峰西南混合模型
这个模型再次假设一个双峰分布,但这次峰值位于 270°和 180°,这些方向在罗盘玫瑰图中较为常见。
mu1 = np.pi # 180 degrees
mu2 = 3 * np.pi / 2 # 270 degrees
with pm.Model() as model_mixture_bimodal_WS:
# Priors for concentration parameters
kappa = pm.HalfNormal('kappa', sigma=10)
# Priors for component weights
weights = pm.Dirichlet('weights', a=np.ones(2))
# Define the four von Mises components
vm1 = pm.VonMises.dist(mu=mu1, kappa=kappa)
vm2 = pm.VonMises.dist(mu=mu2, kappa=kappa)
# Mixture distribution
likelihood = pm.Mixture(
'angles',
w=weights,
comp_dists=[vm1, vm2],
observed=data['radians']
)
# Sample from the posterior
trace_mixture_bimodal_WS = pm.sample(
10000, tune=3000, chains=4, return_inferencedata=True, idata_kwargs={'log_likelihood': True})
# Get kappa samples
kappa_samples = trace_mixture_bimodal_WS.posterior.kappa.values.flatten()
# Posterior Analysis
az.plot_posterior(trace_mixture_bimodal_WS, var_names=['kappa'])
plt.show()
log_likelihood_h1 = compute_log_likelihoods(trace_mixture_bimodal_WS, data['radians'], [mu1, mu2])
print(posterior_report(log_likelihood_h0, log_likelihood_h1, kappa_samples))
>> Bayes Factor: 20.2361
>> Probability kappa > 0.5: 0.1329

后验 kappa 分布(作者提供的图片)
不,明显不如之前的模型好。下一个!
四态混合模型
最后一轮。也许我的狗确实喜欢与基准方向对齐?让我们尝试一个四态分布,峰值分别位于 0°、90°、180°和 270°。
mu1 = 0 # 0 degrees
mu2 = np.pi / 2 # 90 degrees
mu3 = np.pi # 180 degrees
mu4 = 3 * np.pi / 2 # 270 degrees
with pm.Model() as model_mixture_quad:
# Priors for concentration parameters
kappa = pm.HalfNormal('kappa', sigma=10)
# Priors for component weights
weights = pm.Dirichlet('weights', a=np.ones(4))
# Define the four von Mises components
vm1 = pm.VonMises.dist(mu=mu1, kappa=kappa)
vm2 = pm.VonMises.dist(mu=mu2, kappa=kappa)
vm3 = pm.VonMises.dist(mu=mu3, kappa=kappa)
vm4 = pm.VonMises.dist(mu=mu4, kappa=kappa)
# Mixture distribution
likelihood = pm.Mixture(
'angles',
w=weights,
comp_dists=[vm1, vm2, vm3, vm4],
observed=data['radians']
)
# Sample from the posterior
trace_mixture_quad = pm.sample(
10000, tune=3000, chains=4, return_inferencedata=True, idata_kwargs={'log_likelihood': True}
)
# Get kappa samples
kappa_samples = trace_mixture_quad.posterior.kappa.values.flatten()
# Posterior Analysis
az.plot_posterior(trace_mixture_quad, var_names=['kappa'])
plt.show()
log_likelihood_h1 = compute_log_likelihoods(trace_mixture_quad, data['radians'], [mu1, mu2, mu3, mu4])
print(posterior_report(log_likelihood_h0, log_likelihood_h1, kappa_samples))
>> Bayes Factor: 0.0000
>> Probability kappa > 0.5: 0.9644

后验 kappa 分布(作者提供的图片)
嗯… 其实并不是。尽管集中参数κκ大于 0.5 的概率相当高,但贝叶斯因子却是 0.0。
贝叶斯因子的优点在于它有效地惩罚过度复杂的模型,有效防止过拟合。
模型比较
让我们用信息准则总结所有模型的结果。我们将使用Widely Applicable Information Criterion(WAIC)和 Leave-One-Out Cross-Validation(LOO)来比较这些模型。
# Compute WAIC for each model
wail_uni = az.waic(trace_uni)
waic_quad = az.waic(trace_mixture_quad)
waic_bimodal_NS = az.waic(trace_mixture_bimodal_NS)
waic_bimodal_WS = az.waic(trace_mixture_bimodal_WS)
model_dict = {
'Quadrimodal Model': trace_mixture_quad,
'Bimodal Model (NS)': trace_mixture_bimodal_NS,
'Bimodal Model (WS)': trace_mixture_bimodal_WS,
'Unimodal Model': trace_uni
}
# Compare models using WAIC
waic_comparison = az.compare(model_dict, ic='waic')
waic_comparison

# Compare models using LOO
loo_comparison = az.compare(model_dict, ic='loo')
loo_comparison

# Visualize the comparison
az.plot_compare(waic_comparison)
plt.show()

WAIC 比较(作者提供的图片)
我们找到了优胜者!根据 WAIC 和 LOO,双模态南北模型是数据的最佳拟合模型。
结论

Christmas Auri(作者提供的图片)
何等的旅程!几个月前仅仅是对我狗拉屎习惯的简单观察,现在却变成了全面的贝叶斯分析。
在本文中,我展示了如何建模圆形数据,估计平均方向和集中参数,并通过新观察更新后验分布。我们还看到如何使用贝叶斯因子进行假设检验,并使用信息准则比较模型。
结果非常有趣!Auri 确实有自己的喜好,并且在南北轴上能够对齐。如果我和我的狗迷失在树林中,我知道该往哪个方向走了。只需足够大的样本量来确认!
希望您像我一样享受这段旅程。如果您有任何问题或建议,请随时联系。如果您想支持我的工作,请考虑给我买杯咖啡 ❤️
我的社交媒体账号:
参考文献:
做好比完美更重要
如何成为一名更务实的数据科学家,以及这对你职业发展的重要性
·发布于Towards Data Science ·10 分钟阅读·2024 年 7 月 30 日
--

作者提供的图片(通过 Midjourney 生成)
你在工作中表现出色,且以自己知道做事的最佳方式为荣。既然你希望提升标准,你也要求他人达到同样的标准。这肯定会让你引起注意并晋升,对吧?
但是,接着你发现自己被忽视了,错失了晋升机会,而当你环顾四周时,你发现那些确实晋升的人,他们交付的工作远不如你的严谨。难道大家看不出差别吗,还是发生了什么事情?
如果你是一个高绩效者,很容易陷入完美主义的陷阱。这种情况从很早就开始了:学校和大学教育我们使用科学方法,任何偏离理想解决方案的做法都会被扣分。

作者提供的图片
这种学术方法常常延续到职场,尤其是在数据科学与分析这样严格的领域中。
然而,现实是:在高速增长的公司里,完成工作比追求完美更为重要。如果你无法以业务所需的速度交付结果,企业将不会等你。
这篇文章将教你如何避免这种情况的发生。
我将涵盖:
-
为什么完美主义正在阻碍你的职业发展
-
如何识别完美主义以及该如何应对
-
什么时候该务实,什么时候不该务实
-
如何变得更加务实
[## 每当 Torsten Walbaum 发布新文章时,获取邮件通知。
每当 Torsten Walbaum 发布新文章时,获取邮件通知。通过注册,如果你还没有 Medium 账号,你将创建一个新账号…
为什么完美主义会让你止步不前
从表面上看,完美主义听起来很好:你基于内在对完美的渴望,追求卓越。你所生产的任何东西都不会让你的经理或公司显得不好。
但完美主义可以成为你职业发展的主要障碍:
1. 完美主义降低了你的产出。
-
研究表明,人类倾向于更看重短期结果,而非长期结果。这就是为什么我们在可以立刻用钱去度假时却难以为退休储蓄。
-
在工作中,这意味着完美主义者会尽力减少犯错的机会(因为那会导致直接的负面后果),因此花费过多时间去打磨交付物。结果是产出减少,从而使得晋升变得更加困难。
2. 完美主义限制了你的成长机会。
-
完美主义者会竭尽全力去减少错误的可能性。自然的后果是,他们往往会待在自己的舒适区内。
-
如果你从事市场营销数据科学工作,做 B2B SaaS?最好继续专注于你已经掌握的知识,即使你发现自己更有兴趣做消费金融产品分析。如果你转行,你将不得不从头学起新行业,而且更有可能犯错;为什么要冒这个险呢?
根据我的经验,完美主义在高度分析型的人或有高级学术背景的人中尤其常见。而且它变得越来越普遍。然而:
在一个高速成长的环境中成功的艰难但必要的认识是, “把你带到这一点的方法不会是你能达到下一个层次的方式”。
你可能因为能够交出完美无缺的工作而取得了好成绩,并被目标研究生项目录取,花费几个月时间打磨一篇论文或项目。但你在工作中很少有机会展示这种能力。
做工作时想到“我本可以做出更复杂的版本”是痛苦的;有时候,你甚至会为自己随便拼凑出来的粗糙方案感到羞愧。但要记住,你在一个交付物上投入的时间很快就会出现收益递减的情况:

图片来源:作者
如何识别完美主义以及该如何应对
应对完美主义的第一步是了解你所面临的是哪种类型。完美主义有三种类型:
-
面向自己(你对自己设定了不可能达到的高标准)
-
社会规定的(你觉得别人要求你完美),以及
-
面向他人(你对他人设定了不现实的高标准)
例如,如果你意识到你的完美主义至少部分来源于你认为来自经理的不切实际的高期望,你可能需要和他们一起解决这个问题,而不仅仅是尝试改变自己的心态。
鉴于完美主义可能源于许多因素,包括早期的童年经历,在一篇博客文章中提供一刀切的解决方案并不现实。因此,我将重点讨论完美主义在职场中的不同表现,以及你可以在这些具体情况下做些什么。
症状 #1:完美主义者无法跟上业务的节奏 🚀
-
这是什么样子的: 完美主义的数据科学家提出复杂的方法,尽管公司需要的是几周内的结果,他们却需要几个月才能见到成效。他们不愿妥协,你常常听到“那不可能”。
-
如果你是这样的人: 记住,作为数据科学家,你的工作是帮助业务完成任务。与其说“那不可能”,不如提供一组选项及其相应的时间表,并强调其中的权衡。这将使业务能够在了解风险的情况下前进,而你也能够“保护自己”。
对我有帮助的: 不要专注于你可以如何改进交付物,而是思考如果你完全没有提供任何意见,项目会变得多么糟糕(如果你不够快速,这将会发生)。
- 如果你正在面对这个问题: 与其问别人需要多久,不如沟通一个明确的截止日期,并询问在该日期之前可以完成的工作。明确指出是否仅需要方向性分析;通常,推动工作的需求远比人们想象的要简单且不那么严格。
症状 #2:完美主义者在做出决策时对不完整的数据感到不安 📊
-
这是什么样子的: 完美主义的数据科学家在做决策时往往会陷入瘫痪。他们拖延决策,希望能获取更多信息或进行更多分析以降低决策风险。
-
如果你是这样的人: 给出明确的建议,并说明你的信心程度,以及如果你错了会发生什么。你还应该列出你决策时所依据的关键假设;如果 1)别人不同意这些假设,或 2)后来得到的新信息改变了其中一个假设,你将能够进行调整。
对我有帮助的: 意识到我们 从来 没有完美的信息。每一个决策在某种程度上都是基于教育性猜测,而且 研究表明我们往往会过度后悔我们所做的决定。
- 如果你正在面对这个问题: 让人们直接面对问题;向你的团队要求建议或决策,而不是选项。并且培养一种文化,即根据当时所知来评判决策,因为事后总是容易挑出问题。
症状 #3:完美主义者经常成为他人的障碍 🚫
-
这是什么样子的: 完美主义者不断挑剔别人提案中的漏洞,却不提供替代方案。
-
如果这就是你: 不要试图在公司内部强制要求完美。扮演“反面角色”并互相挑战是重要的,但应该是建设性的。将项目视为一个优化问题,你需要在给定的约束条件下(时间、预算等)找到最合适的解决方案。
对我有帮助的是: 假装如果你批评别人的提案,那么你现在就得负责解决问题。这样迫使我从“这不合理”转变为“这是我会怎么做”。
- 如果你正在应对这种情况: 设定一个期限来提出替代方案,并奖励那些专注于解决方案的思维,而不是那些只指出问题的人。
症状 #4:完美主义者打磨每一个交付物 🎁
-
这是什么样子的: 每一份文档或幻灯片(即使是个人笔记或内部文档)都被精确地格式化和设计。
-
如果这就是你: 将精力集中在面向客户的交付物和向高层汇报的文档上。任何时间花在美化内部工作文档上,都是本可以用来推出更多项目的时间。
对我有帮助的是: 试着从反方向思考。每个人都会注意到你花了很多时间在打磨这个内部文档,而不是做一些有影响力的事情。在一个快速发展的公司里,这实际上比交付一个边缘粗糙的文档看起来更糟。
- 如果你正在应对这种情况: 以身作则;营造一种文化,让截图的仪表盘图表和简短评论成为制作幻灯片的可接受方式。不要挑剔诸如颜色或字体选择这样的琐事。
附注:这并不意味着你应该提交完全没有格式的东西。花五分钟时间使文档 易于理解 (不一定要漂亮)是值得的。

作者提供的图片
症状 #5:完美主义者提供过多细节 🔬
-
这是什么样子的: 完美主义者在书面和口头交流中加入过多细节。他们不喜欢简化,并使用大量技术术语。
-
如果这就是你: 专注于关键见解,并将支持信息放在附录中。使用简单的英语;你希望来自不同团队和背景的人都能理解你的工作。只有当别人理解你的分析结论时,你才能真正实现影响力作为数据科学家。
对我有帮助的是: 不要试图预先预测所有问题并提前回答。将最可能的问题放在 FAQ 部分,并准备现场回答任何剩余问题;这实际上让你看起来比在文档中包含所有内容更有能力。
- 如果你遇到这种情况: 请要求报告人提供五分钟的执行摘要,迫使他们专注于关键点。然后根据需要提出有针对性的后续问题。
什么时候应该务实,什么时候不应该
有时候,做到 100%准确是必要的,而有时候,速度比完美更重要。但是,什么时候应该务实,什么时候又是一个坏主意呢?
以下是你应该考虑的因素,以指导这个决策:
-
♻️ 这个决策是可以逆转的吗? 有些决策是单向门,而有些则不是。你应该将大部分时间花在分析那些逆转成本高的决策上,而对于其他决策,可以通过有根据的猜测来处理。
-
💰 错误的预期财务成本是多少? 即使一个决策是可以逆转的,逆转的代价可能也很高(例如浪费工程资源、花钱买了错误的工具等)。那些逆转成本高的决策应该得到更多的审查。
-
⚖️ 如果你犯错,会有声誉损害或法律后果吗? 必须收回你在内部做出的声明是尴尬的;如果你向监管机构承认自己犯了错误,可能会有严重的后果。作为经验法则,任何涉及监管机构、华尔街、董事会或客户的事务,都应该接受最严格的审查。
-
📈 这个决策对分析的敏感性有多大? 一个常见的错误是,即使额外的准确性不会改变决策,也不断投入时间进行分析。例如,如果你想估算一个新商业机会的潜在收入,知道这个机会的范围是 1 亿美元还是 10 亿美元,可能足以做出是否继续的决策。
-
🗑️ 这是一项可以丢弃的工作吗? 将时间投资于那些将长期使用的工作,比那些仅用于一次性决策的分析更有益。对于当前问题,可以将临时分析做得“足够好”,而将大部分精力集中在优化那些将被内部或外部客户广泛使用的工作上。

图片由作者提供
如何变得更加务实
我自己也曾经需要去摒弃完美主义。这些思维方式的转变帮助了我:
-
意识到即使你做得完美,仍然会经常失败。 例如,仅仅因为你对你的总可寻址市场(TAM)做了无懈可击的分析,并不意味着你的市场进入就一定会成功。关键成功因素是要有更多的“射门机会”,所以你应该将更多的时间花在尝试更多的事情上,而不是把时间浪费在完善单一事物上。
-
不要关注你做错的事情,而是关注你做对的比例。 如果你大部分时间都做对了,偶尔做错也是可以接受的。例如,亚马逊的领导力原则是“领导者通常是对的”(而不是“每次都是对的”)。
-
在低风险情境中锻炼你的判断力。 即使你不是决策者,也要练习做出判断。例如,如果你参加一个会议,会议上有高层领导需要做决定,思考一下你会怎么做。决策就像是一块肌肉,最好的锻炼方式是在低风险情境中进行。
结论
变得更加务实是一个过程;它需要时间,因此不要指望一夜之间改变自己的思维方式。但这是值得的;它不仅会增加你的影响力,还会减少你的压力,因为你将花更少的时间去追求那难以捉摸的完美。
想要获取更多实用的分析建议,可以考虑在 Medium、 LinkedIn 或 Substack 上关注我。
如果你…

我在工作三年多后的角色理解
·发布于Towards Data Science ·阅读时间:9 分钟 ·2024 年 3 月 15 日
--
自从我结束了与我一起阅读系列已经有一段时间了。在这段时间里,我和父母一起在中国度过了春节,并与我的经理进行了中期绩效评估。虽然我用视频记录了我在中国的经历,但我还没有整理我从绩效评估中获得的反馈,这是一个很好的机会可以在这篇文章中分享。毕竟,几周前我刚刚迎来了三年的工作周年纪念,这是一个很好的时机来反思自己在这一路上学到了什么。尽管标题可能看起来像是标题党,但它是基于我迄今为止旅程的学习总结。
如果你没有成长心态,就不要做数据科学家
在职业生涯的早期阶段,出现冒充者综合症是非常常见的——我们常常怀疑自己是否有资格获得这份工作。它在技术行业尤其普遍,因为这是一个快速发展的领域。一些去年使用的算法可能已经过时。每个人都在追逐最新的流行词汇,这会给求职者和在职人员带来很大的压力。
如果不保持成长心态,就无法摆脱这种不配感……
不要害怕用机器学习解决简单任务
管理者的机器学习课程
各行业普遍存在的误解
·发表于Towards Data Science ·阅读时间:6 分钟·2024 年 11 月 22 日
--

嗨,欢迎来到我的新系列,在这里我分享大多数企业常犯的机器学习错误。我旨在提供一些简单而独特的课程,基于那些存在多年且始终未能消失的误解。
这是第一个课程。
哦,别忘了看看我其他的机器学习课程!👇

管理者和工程师的机器学习课程
查看列表3 个故事


许多专业人士认为机器学习是当其他方法失败时才部署的先进技术。 在他们看来,机器学习是一个复杂的工具,专门用于那些传统方法无法解决的复杂问题。
因此,当我与企业交谈时,我经常听到类似“我们不需要用机器学习解决这个问题”或“我们在机器学习出现之前就解决了这个问题”这样的评论。
有时,这些评论是有道理的,认为机器学习是错误的技术。然而,更常见的是,这些评论源于一个错误的观念……
别让你的应用崩溃:以批次的形式从数据库加载记录以提升性能
通过高效加载查询来提升你的 Python 应用性能
·发布于 Towards Data Science ·7 分钟阅读·2024 年 4 月 18 日
--

Python 传输小批量数据(由 ChatGPT 生成的图像)
本文讨论了优化 Python 应用与数据库之间通信的技巧,以确保应用流畅运行,同时避免数据库服务器的过载。本文关注一个常见的低效问题:一次性加载所有查询结果。
当面对返回大量记录的查询时,一次性加载所有返回的记录往往既不实际也不可能。 我们不再将所有结果加载到内存中并逐行处理,而是通过本文的方法来分批加载多个小块数据。我们不再一次性加载 100 万条记录并处理,而是分批加载 400 次,每次加载 2500 条记录!这样,应用就不必将所有结果加载到内存中,带来了明显的好处:
-
增强的内存使用
-
更好的响应时间感知
-
减少数据库压力
我们还将深入了解这一技术的原理,展示它在幕后是如何工作的。让我们开始编码吧!
为什么使用 fetchmany
今天别洗衣服,明天会更便宜
通过因果推断分析伦敦的电价变化
·发表于数据科学前沿 ·阅读时长 16 分钟·2024 年 10 月 21 日
--

图片来源:Arthur Lambillotte 在Unsplash
有时候,我们不得不进行实验。无论是公司改进产品,还是公共官员追求政策目标,他们都面临着类似的问题:
-
新的定价方案能否提高我们电信公司的客户保持率?
-
新的入职流程能否提高应用程序转化率?
-
提醒系统能否减少医院的缺席率?
-
如果能,能提高多少?
这些问题很棘手。未能解决它们的成本可能很严重,但错误的解决方案可能更昂贵。为了降低风险,许多人转向实验:在将新的提醒系统推广到整个地区的医疗保健网络之前,我们可以先在一所医院进行测试;在为平台上的所有用户部署新的入职系统之前,我们可能只对部分用户展示;在全国范围内调整光纤收费标准之前,我们可以先在一个城市进行测试。这些都是计划解决方案的小规模版本实验,但我们如何知道它们是否有效呢?这就是因果分析派上用场的地方。
在本文中,我提出了一个因果框架,用于分析 2013 年伦敦发生的一项实验:低碳伦敦……
不要修复坏数据,改做这样做
人们在谈论数据质量时,并不清楚他们到底在说什么。
·发表于 Towards Data Science ·7 分钟阅读·2024 年 1 月 31 日
--

图片来源:No Revisions 来自 Unsplash
来自战壕的故事
几年前,我们的数据平台团队旨在找出数据用户的主要关切点。我们对使用我们数据平台的人员进行了调查,结果并不意外,最主要的关切是数据质量。
我们工程思维模式下的初步反应是开发数据质量工具。我们引入了一个名为 Contessa 的内部工具。尽管 Contessa 使用起来略显繁琐,并且需要大量的手动配置,但它帮助进行数据质量的标准维度检查,涵盖了一致性、时效性、有效性、唯一性、准确性和完整性。在运行了几个月,进行了数百次数据质量检查之后,我们得出结论:
-
数据质量检查偶尔帮助数据用户在较短的时间内发现数据已被破坏,无法依赖。
-
尽管频繁执行数据质量检查,但在数据质量的主观感知上并没有显著改善。
-
对于大部分问题,特别是通过自动化数据质量检查发现的问题...
不要让你的算法陷入简单数据的洪流中
面向经理和工程师的机器学习课程
一个常见的错误,导致训练变慢、模型效果差,并且烧钱
·发表于Towards Data Science ·阅读时间 7 分钟·2024 年 12 月 5 日
--

图像由作者使用 ideogram 2.0 创建
欢迎回到新的面向经理和工程师的机器学习课程,在这里我分享我在管理公司 NextML 时遇到的错误和误解,从中提炼出的机器学习经验!🔥
今天,我们将讨论一个即使是最有经验的机器学习工程师和数据科学家也常犯的错误。我在各行各业、大小公司,以及各种用例中都见过这个问题。
错误在于在训练过程中给算法提供过多简单的例子,导致学习变慢、泛化能力差,并且对异常值更为敏感。
对大多数企业来说,更为关键的是,机器学习算法的训练缓慢会比必要的速度更快地消耗掉财务资源!
注意: 根据我的经验,经理们做出错误的机器学习和人工智能战略决策,因为他们不理解这些技术。我希望通过提供既有技术理解又有合理推理的课程来改变这种情况。
可能是最简单的 Python 线程、进程和 GIL 教程

通过图示和代码进行说明,减少枯燥的概念
·发表于 Towards Data Science ·7 分钟阅读·2024 年 2 月 5 日
--
如果你是 Python 学习者,不要跑开,因为这篇文章旨在用最简单的方式解释给你什么是 GIL。当然,必须先从解释线程和进程是什么开始。别担心,我会尽力让它变得简单,尽管这可能会牺牲一些定义的准确性。
现在我们应该开始了。
1. Python 中的多线程

图片来源:Steen Jepsen 来自 Pixabay
一些概念
多线程是最常见的编程技术之一,它也存在于 Python 中。
它允许我们同时运行多个操作。通常,多线程可以提高 CPU 的使用效率。此外,大多数 I/O 任务也能从并发运行的线程中受益。
请不要混淆“进程”和“线程”这两个概念。进程会分配一定的内存,并且在操作系统中与其他进程完全隔离。因此,我们的操作系统中的一个程序崩溃了…
不要让 Python 的 dir() 函数欺骗你!
PYTHON 编程
dir() 函数返回对象的属性。不幸的是,它并不会显示所有内容——发现如何查看所有属性吧。
·发表于 Towards Data Science ·阅读时间:7 分钟·2024 年 5 月 13 日
--

这真的是所有内容吗?dir() 函数并不保证显示所有内容!照片由 Cristiano Pinto 提供,来自 Unsplash
这是 Python 的内建 dir() 函数:

dir() 函数的帮助。图片来源:作者
简而言之,当用于某个对象,比如 obj 时,函数返回的是其属性的名称……或者严格来说,部分属性——这个部分差异可不小。
如果你在互联网上搜索,你会发现有不少关于dir()问题的讨论。在StackOverflow上,你会找到关于这个问题的各种 Python 对象的讨论,比如Outlook 邮件、[json](https://stackoverflow.com/questions/57576522/python-object-dict-not-showing-all-attributes) 对象、解析过的 XML 对象,以及[scipy.sparse](https://stackoverflow.com/questions/36211996/why-dir-doesnt-show-all-python-object-attributes) 对象。显然,人们期望dir()返回所有的属性。在研究这个话题之前,我也有这样的期望——所以我能理解这种困惑。
即使你确实阅读了函数的文档字符串并注意到“某些属性”这个短语,它也无法帮助你找到所有的属性。
不要让你的 RAG 知识库仅限于文本
盗取这个即插即用的 Python 脚本,轻松将图像集成到你的聊天机器人知识库中
·发表于数据科学前沿 ·阅读时长 7 分钟·2024 年 8 月 19 日
--

图片来源:Nitish Meena于Unsplash
在构建知识库时,一个常见的挑战是将所有内容转化为纯文本。当涉及到像幻灯片、PDF、图像等媒体源时,这可能会受到限制。
那么,我们如何合理利用那些不是纯文本的数据呢?
⛳ 没有 Medium 会员资格?我为你准备好了:使用这个免费文章链接。请考虑留下高亮、点赞、关注和评论 ⛳
由于最近人工智能的进展,现在比以往任何时候都更容易且更便宜。通过使用具有视觉能力的大型语言模型(LLMs),我们可以转录成千上万的图像,不仅捕捉文本,还能理解内容之间的关系。如果需要,这些模型甚至可以描述图像中的视觉对象,提供比光学字符识别(OCR)更丰富、更详细的转录。
我们将从这三个简单的步骤开始:
-
收集数据:收集你计划使用的图像,确保它们井然有序,并且信息量不至于过载。
-
上传数据:设置一个 AWS S3 桶来存储你的图像,确保基于云的 AI 模型能够……
掺假:测试异常值检测器的技术
使用精心制作的合成数据比较和评估异常值检测器
·发表于Towards Data Science ·14 分钟阅读·2024 年 7 月 9 日
--
本文是我关于异常值检测系列文章的一部分,之前发布过关于计数异常值检测器和频繁模式异常值因子的文章,并提供了我书籍Python 中的异常值检测的另一个摘录。
本文探讨了测试和评估异常值检测器的问题,这是一个众所周知的难题,并提出了一种解决方案,有时被称为掺假。通过掺假,真实数据行会被修改(通常是随机的),但修改的方式确保它们在某些方面很可能是异常值,因此应该被异常值检测器检测到。然后,我们可以通过评估检测器检测掺假记录的能力来评估它们的表现。
本文专门讨论表格数据,但同样的思路也可以应用于其他形式的数据,包括文本、图像、音频、网络数据等。
测试和评估其他类型的模型
如果你熟悉离群点检测,你可能也至少在某种程度上熟悉回归和分类问题的预测模型。对于这些问题,我们有标注数据,因此在调整模型时评估每个选项相对简单(选择最佳的预处理、特征、超参数等);同时,估计模型的准确性(它在未见过的数据上的表现)也相对容易:我们只需使用训练-验证-测试拆分,或者更好地使用交叉验证。由于数据是标注的,我们可以直接看到模型在标注测试数据上的表现。
但在离群点检测中,没有标注数据,问题也要困难得多;我们没有客观的方法来判断离群点检测器评分最高的记录是否确实是数据集中最具统计异常性的记录。
以聚类为例,我们同样没有数据的标签,但至少可以衡量聚类的质量:我们可以确定聚类内部的一致性如何,以及聚类之间的差异有多大。通过使用某种距离度量(如曼哈顿距离或欧几里得距离),我们可以衡量一个聚类内的记录彼此之间的接近程度,以及不同聚类之间的远离程度。
所以,给定一组可能的聚类,能够定义一个合理的度量标准(如轮廓系数),并确定哪一个聚类是首选的,至少在这个度量标准下是这样。也就是说,就像预测问题一样,我们可以为每个聚类计算一个分数,并选择表现最好的聚类。
然而,在离群点检测中,我们没有类似的东西可以使用。任何试图量化记录异常程度的系统,或是试图判断给定的两条记录中哪一条更为异常的系统,实际上都是一个离群点检测算法。
例如,我们可以使用熵作为离群点检测方法,然后检查完整数据集的熵,以及在移除任何被识别为强离群点的记录后,数据集的熵。从某种意义上说,这种做法是有效的;熵是衡量离群点存在的一种有用指标。但我们不能假设熵是该数据集中离群点的最终定义;离群点检测的一个基本特性是,没有离群点的最终定义。
一般来说,如果我们有任何方式来尝试评估离群点检测系统检测到的离群点(或者,像前一个例子中一样,评估包含和不包含已识别离群点的数据集),这实际上就是一个离群点检测系统本身,使用它来评估已找到的离群点会变得循环无效。
因此,评估离群点检测系统相当困难,实际上没有一个很好的方法,至少使用现有的真实数据无法做到这一点。
但是,我们仍然可以创建合成测试数据(以一种可以假定合成数据主要是异常值的方式)。有了这些数据,我们可以确定异常值检测器倾向于比真实记录给合成记录更高的分数的程度。
有许多方法可以创建合成数据,我们在书籍中介绍了这些方法,但在本文中,我们专注于一种方法,即掺毒。
掺毒数据记录
掺毒数据记录指的是对现有数据记录进行轻微修改,通常只改变每条记录中的一个或少数几个单元格的值。
如果被检查的数据是例如与一个由特许经营店组成的公司财务表现相关的表格,我们可能会为每个特许经营店创建一行数据,我们的目标可能是识别出这些数据中最异常的记录。假设我们有以下特征:
-
特许经营的年龄
-
当前所有者拥有的年数
-
去年销售数量
-
去年总销售额的美元数
以及一些其他特征。
一条典型记录可能包含如下四个特征的值:20 岁,5 年与当前所有者的合作,去年 10,000 笔独特销售,总销售额为 500,000 美元。
我们可以通过调整某个值为一个罕见的值来创建该记录的掺毒版本,例如,将特许经营的年龄设置为 100 年。这是可以做到的,并且可以对正在测试的检测器进行快速的烟雾测试——很可能任何检测器都会识别出这个值是异常的(假设 100 是一个罕见的值),虽然我们可能能够排除一些无法可靠检测到这种修改记录的检测器。
我们不一定会排除某种异常值检测器(例如 kNN、Entropy 或 Isolation Forest)的使用,而是会排除异常值检测器的类型、预处理方法、超参数以及检测器的其他特性。例如,我们可能会发现,某些超参数下的 kNN 检测器表现良好,而其他超参数下的 kNN 检测器则表现不好(至少对于我们测试的掺毒记录来说是这样)。
然而,通常大部分测试都会创建更微妙的异常值。在这个例子中,我们可以将总销售额从 500,000 美元改为 100,000 美元,虽然 100,000 美元仍然可能是一个典型值,但 10,000 笔独特销售与 100,000 美元的总销售额的组合对于这个数据集来说可能是异常的。也就是说,在掺毒时,我们通常创建的是具有异常值组合的记录,尽管有时也会创建单个异常值。
在记录中更改一个值时,具体如何使该行成为离群值(假设它确实成为了离群值)并不明确,但我们可以假设大多数表格之间的特征是有联系的。在此示例中,将美元值改为 100,000,可能(以及创造一个异常的销售数量和销售额组合)很可能会由于特许经营的年龄或当前拥有者的年限,创造出一个不寻常的组合。
然而,对于某些表格来说,特征之间没有关联,或者仅有少数且较弱的关联。这种情况较为罕见,但确实可能发生。对于这种类型的数据,没有异常值组合的概念,只有异常单一值。尽管这种情况罕见,但实际上它是一个更简单的处理案例:检测离群值更容易(我们只需检查单一的异常值),评估检测器也更容易(我们只需检查我们是否能检测到异常单一值)。不过,本文的其余部分将假设特征之间存在某些关联,并且大多数异常情况将是值的异常组合。
处理掺入数据
大多数离群值检测器(有少数例外)有独立的训练和预测步骤。因此,大多数离群值检测器与预测模型类似。在训练步骤中,评估训练数据并识别数据中的正常模式(例如,记录之间的正常距离、频繁项集、簇、特征之间的线性关系等)。然后,在预测步骤中,将测试数据集(可能与训练数据相同,也可能是不同的数据)与训练期间发现的模式进行比较,并为每一行分配一个离群值得分(或在某些情况下,一个二元标签)。
鉴于此,我们可以通过两种主要方式处理掺入数据:
- 在训练数据中包含掺入记录
我们可以在训练数据中包含少量的掺入记录,然后也使用这些数据进行测试。这可以测试我们在当前可用数据中检测离群值的能力。这是离群值检测中的常见任务:给定一组数据,我们通常希望找出数据集中的离群值(尽管也可能希望找出后续数据中的离群值——相对于该训练数据的规范来说是异常的记录)。
通过这样做,我们可以仅使用少量的掺入记录进行测试,因为我们不希望显著影响数据的整体分布。然后,我们检查是否能够将这些记录识别为离群值。一个关键的测试是,在训练数据中包含原始版本和掺入版本的掺入记录,以确定检测器是否将掺入版本的得分显著高于相同记录的原始版本。
然而,我们也希望检查掺杂记录是否普遍被评为最高(理解为一些原始未修改记录可能比掺杂记录更为异常,而且某些掺杂记录可能并不异常)。
鉴于我们只能用少量掺杂记录进行测试,这个过程可能会重复多次。
然而,掺杂数据仅用于以这种方式评估检测器。在为生产创建最终模型时,我们将仅使用原始(真实)数据进行训练。
如果我们能够可靠地检测到数据中的掺杂记录,我们可以合理地相信,我们能够识别出同一数据集中的其他异常值,至少是类似掺杂记录的异常值(但不一定是那些更加微妙的异常值——因此我们希望包括一些 reasonably subtle 的掺杂记录进行测试)。
2. 仅在测试数据中包含掺杂记录
也可以仅使用真实数据进行训练(我们可以假设这些数据大多数不是异常值),然后同时使用真实数据和掺杂数据进行测试。这允许我们在相对干净的数据上进行训练(真实数据中的一些记录可能是异常值,但大多数是典型的,并且不会因为掺杂记录而受到污染)。
它还允许我们使用可能会投入生产的实际异常值检测器进行测试(具体取决于它们在掺杂数据上的表现——无论是与我们测试的其他检测器相比,还是与我们对检测器最小表现的预期相比)。
这测试了我们在未来数据中检测异常值的能力。这是异常值检测中的另一个常见场景:我们有一个可以假定为相对干净的数据集(要么没有异常值,要么仅包含少量典型的异常值,并且没有极端异常值),我们希望将未来的数据与之进行比较。
仅使用真实数据进行训练,并使用真实和掺杂数据进行测试时,我们可以根据需要测试任何量的掺杂数据,因为掺杂数据仅用于测试而非训练。这使得我们能够创建一个较大且因此更可靠的测试数据集。
创建掺杂数据的算法
有多种方法可以创建掺杂数据,包括在《Python 中的异常值检测》中介绍的几种方法,每种方法都有其优缺点。为了简化,在本文中我们只介绍一种方法,其中数据是以相当随机的方式进行修改的:修改的单元格是随机选择的,替代原始值的新值是随机创建的。
这样做时,某些掺杂记录可能并不真正是异常的,但在大多数情况下,随机赋值会破坏特征之间的一个或多个关联。尽管如此,我们可以假设掺杂记录大多是异常的,尽管根据其创建方式,可能只是轻微异常。
示例
在这里,我们通过一个例子,使用真实的数据集,修改它并进行测试,以查看修改的检测效果。
在这个例子中,我们使用了一个在 OpenML 上可用的数据集,名为 abalone(www.openml.org/search?type=data&sort=runs&id=42726&status=active,该数据集在公共许可证下可用)。
尽管可以进行其他预处理,但在这个例子中,我们对分类特征进行独热编码,并使用 RobustScaler 对数值特征进行缩放。
我们使用了三种离群点检测器进行测试:Isolation Forest、LOF 和 ECOD,这些检测器都可以在流行的PyOD库中找到(必须先通过 pip 安装才能执行)。
我们还使用 Isolation Forest 来清理数据(去除任何强烈的离群点),以便在进行任何训练或测试之前进行处理。这个步骤并非必须,但在离群点检测中通常是有用的。
这是上述两种方法中的第二种方法的例子,我们在原始数据上进行训练,并用原始数据和掺假数据进行测试。
import numpy as np
import pandas as pd
from sklearn.datasets import fetch_openml
from sklearn.preprocessing import RobustScaler
import matplotlib.pyplot as plt
import seaborn as sns
from pyod.models.iforest import IForest
from pyod.models.lof import LOF
from pyod.models.ecod import ECOD
# Collect the data
data = fetch_openml('abalone', version=1)
df = pd.DataFrame(data.data, columns=data.feature_names)
df = pd.get_dummies(df)
df = pd.DataFrame(RobustScaler().fit_transform(df), columns=df.columns)
# Use an Isolation Forest to clean the data
clf = IForest()
clf.fit(df)
if_scores = clf.decision_scores_
top_if_scores = np.argsort(if_scores)[::-1][:10]
clean_df = df.loc[[x for x in df.index if x not in top_if_scores]].copy()
# Create a set of doped records
doped_df = df.copy()
for i in doped_df.index:
col_name = np.random.choice(df.columns)
med_val = clean_df[col_name].median()
if doped_df.loc[i, col_name] > med_val:
doped_df.loc[i, col_name] = \
clean_df[col_name].quantile(np.random.random()/2)
else:
doped_df.loc[i, col_name] = \
clean_df[col_name].quantile(0.5 + np.random.random()/2)
# Define a method to test a specified detector.
def test_detector(clf, title, df, clean_df, doped_df, ax):
clf.fit(clean_df)
df = df.copy()
doped_df = doped_df.copy()
df['Scores'] = clf.decision_function(df)
df['Source'] = 'Real'
doped_df['Scores'] = clf.decision_function(doped_df)
doped_df['Source'] = 'Doped'
test_df = pd.concat([df, doped_df])
sns.boxplot(data=test_df, orient='h', x='Scores', y='Source', ax=ax)
ax.set_title(title)
# Plot each detector in terms of how well they score doped records
# higher than the original records
fig, ax = plt.subplots(nrows=1, ncols=3, sharey=True, figsize=(10, 3))
test_detector(IForest(), "IForest", df, clean_df, doped_df, ax[0])
test_detector(LOF(), "LOF", df, clean_df, doped_df, ax[1])
test_detector(ECOD(), "ECOD", df, clean_df, doped_df, ax[2])
plt.tight_layout()
plt.show()

在这里,为了创建掺假记录,我们复制了完整的原始记录集,因此掺假记录与原始记录的数量相等。对于每个掺假记录,我们随机选择一个特征进行修改。如果原始值低于中位数,我们创建一个高于中位数的随机值;如果原始值高于中位数,我们创建一个低于中位数的随机值。
在这个例子中,我们看到,虽然 IF 确实对掺假记录的评分较高,但差异并不显著。LOF 在区分掺假记录方面表现出色,至少对于这种掺假方式来说是这样。ECOD 是一个仅检测异常小值或异常大值的检测器,它不测试异常组合。由于这个例子中的掺假并没有创建极端值,只是一些不寻常的组合,因此 ECOD 无法区分掺假记录和原始记录。
这个例子使用箱形图来比较检测器,但通常我们会使用一个客观评分,通常是 AUROC(接收者操作特征曲线下面积)得分来评估每个检测器。我们通常还会测试多种模型类型、预处理方法和参数组合。
替代性掺假方法
上述方法往往会创建违反特征之间正常关联的掺假记录,但可以使用其他掺假技术来增加这种情况的可能性。例如,首先考虑分类列,我们可以选择一个新的值,使得同时满足以下条件:
-
新值与原始值不同
-
新值与根据行中其他值预测的值不同。为实现这一点,我们可以创建一个预测模型,预测该列当前的值,例如使用随机森林分类器。
对于数值数据,我们可以通过将每个数值特征划分为四个四分位数(或者若干个分位数,但至少是三个)来实现等效效果。对于数值特征中的每个新值,我们接着选择一个值,使得:
-
新值与原值处于不同的四分位数
-
新值与根据行中其他值预测的值处于不同的四分位数
例如,如果原值在 Q1,而预测值在 Q2,那么我们可以随机选择 Q3 或 Q4 中的值。这样,新值很可能会违反特征之间的正常关系。
创建测试数据集套件
一旦数据被篡改,就没有明确的方法来衡量记录的异常程度。然而,我们可以假设,平均而言,修改的特征越多,修改的程度越大,篡改后的记录就会越异常。我们可以利用这一点来创建多个测试集,而不是单一的测试集,这样可以更准确地评估异常值检测器的表现。
例如,我们可以创建一组非常明显的篡改记录(每条记录中修改了多个特征,每个特征的值与原值有显著差异),一组非常微妙的篡改记录(仅修改了一个特征,且与原值没有显著差异),以及介于两者之间的多个难度等级。这有助于很好地区分不同的检测器。
因此,我们可以创建一个测试集套件,其中每个测试集的(大致估算的)难度基于修改的特征数量和修改的程度。我们还可以有不同的集,其中修改了不同的特征,因为某些特征中的异常值可能更相关,或者更容易或更难被检测到。
然而,重要的是,任何进行的篡改都应该代表在实际数据中可能出现的异常值类型。理想情况下,篡改后的记录集还应很好地覆盖你希望检测到的异常值范围。
如果这些条件得到满足,并且创建了多个测试集,这对于选择表现最佳的检测器并评估它们在未来数据上的表现非常有效。我们无法预测将检测到多少异常值,也无法预测会出现多少假阳性和假阴性——这些很大程度上取决于你所遇到的数据,而在异常值检测中,这是非常难以预测的。但是,我们可以大致了解你可能会检测到哪些类型的异常值,以及哪些类型的异常值可能无法检测到。
可能更重要的是,我们还能够有效地创建一个异常值检测器的集成。在异常值检测中,集成通常对于大多数项目都是必要的。因为一些检测器能捕捉到某些类型的异常值,但会遗漏其他类型,而其他检测器则能够捕捉并遗漏其他类型的异常值,所以我们通常只能通过使用多个检测器,可靠地捕捉到我们感兴趣的异常值范围。
创建集成模型本身是一个庞大且复杂的领域,且与预测模型的集成不同。但对于本文,我们可以指出,了解每种检测器能够检测哪些类型的异常值,可以帮助我们判断哪些检测器是冗余的,哪些能够检测出大多数其他检测器无法识别的异常值。
结论
很难评估任何给定的异常值检测器在当前数据中识别异常值的效果,更难评估它在未来(未见过的)数据上的表现。对于两个或更多的异常值检测器,也很难评估哪个在当前和未来数据上表现更好。
然而,我们有多种方法可以使用合成数据来估计这些值。在本文中,我们至少快速地概述了一种基于对真实记录进行掺杂并评估我们如何能够将这些记录的得分提高到超过原始数据的程度的方法(跳过了许多细节,但涵盖了主要思想)。虽然这些方法并不完美,但它们非常有价值,而且在进行异常值检测时,往往没有其他实用的替代方案。
所有图片均来自作者。
将卫星热图像从 1000 米缩放到 10 米(Python)
Sentinel-3 图像的热锐化:使用 Python 在 Google Colab 中将 1 公里图像锐化至 10 米
·发表于Towards Data Science ·13 分钟阅读·2024 年 3 月 6 日
--

Sentinel-3 热图像从 1000 米缩放到 10 米,由作者可视化。
目录
-
🌅 简介
-
💾 下载 Sentinel-3(1000 米)和 Sentinel-2 图像(10 米)
-
⚙️ Sentinel-3 图像处理
-
🌡️ 温度-NDVI 空间
-
📐 热图像锐化(从 1000 米到 10 米)
-
🗺️ 锐化热图像的可视化
-
📄 结论
-
📚 参考资料
🌅 简介
卫星捕获的热图像的下采样已被广泛研究,原因在于提供热图像的卫星在空间分辨率和时间分辨率之间存在权衡。例如,Landsat-8 的重访周期为 16 天,原始热分辨率为 100 米。相比之下,Sentinel-3 每天可以提供热图像,但空间分辨率为 1000 米。

空间与时间分辨率之间的权衡,图像由作者提供
DPO 全量训练与 LoRA:LoRA 对 DPO 训练的效果如何?
一种模型,两个适配器
·发表于Towards Data Science ·8 分钟阅读·2024 年 11 月 20 日
--

使用 Grok 生成
有多种方法可以使大型语言模型(LLM)与人类偏好对齐。除了人类反馈强化学习(RLHF),它通常被认为对于新调整过的模型应用过于资源密集,直接偏好优化(DPO)是 LLM 对齐中最受欢迎的替代方案之一。
尽管 DPO 比 RLHF 显著更具成本效益,但它仍然需要一个参考模型,除了“策略”模型(即正在积极训练的模型)。这意味着两个模型必须同时加载到 GPU 内存中,这对于单 GPU 配置来说可能具有挑战性,尤其是在大模型的情况下。
一种更节省内存的方式是使用 LoRA 进行 DPO 训练。我们冻结模型的参数并训练一个小的适配器,而不是训练整个模型。如果策略模型和参考模型共享相同的基础模型,那么这种方法会更加高效;在这种情况下,我们只需要加载一次基础模型,然后加载冻结的参考模型适配器和可训练的策略模型适配器,从而显著减少内存需求。
然而,个人认为 LoRA 对 DPO 性能的影响仍然没有得到充分研究。尽管 LoRA 可以较为接近全量训练,但它的表现…
DRAGIN: 基于大型语言模型信息需求的动态检索增强生成
传统 RAG 与动态 RAG
·发表于 Towards Data Science ·阅读时间 9 分钟·2024 年 12 月 5 日
--
在本文中,我探讨了研究论文《DRAGIN:基于大型语言模型信息需求的动态检索增强生成》中解释的基本概念,作者是** Weihang Su、Yichen Tang、Qingyao Ai、Zhijing Wu 和 Yiqun Liu。这篇论文可以在 这里* 访问。*
引言 — 让我们来看一个短故事!
假设你正在解决一个问题。刚开始时,你 只有一次机会 向你的教授请教。这意味着在开始时理解问题的整体范围非常重要。如果是一个简单的问题,可能没问题——你提问、获得清晰的答案,然后继续前进。
现在,假设这个问题 变得更加复杂。你越深入探讨,就会有 更多问题 !不幸的是,你不能再回去问你的教授,因为所有问题都必须在一开始就提出来。这使得问题的解决变得更加困难。
但如果,假设, 你被允许 每当发现一个扩展问题范围的新问题时都可以回去请教教授呢?这种方法允许你在问题发展时迭代地导航复杂性,每当问题有新发展时都可以请求指导。 这就是 DRAGIN(动态 RAG)与传统 RAG 的本质区别。
考虑到我们的任务、问题和世界变得如此复杂且多维, 这种动态方法的需求比以往任何时候都更为迫切!

图片来自Unsplash
大型语言模型改变了我们获取信息的方式。我们正处于一个阶段,在这个阶段,我们搜索信息的方式已经永远改变。现在,我们不需要再寻找多个链接并处理信息来回答问题,我们可以直接向 LLM 提问!
然而,仍然存在一些问题:
-
幻觉:生成虚假信息
-
过时/陈旧:无法获取最新的信息
-
专有信息:无法访问专门知识
为了解决上述问题,检索增强生成(RAG)作为一种有前景的解决方案应运而生。它的工作原理是通过访问并结合 LLM 生成准确回答所需的相关外部信息。
然而,传统的 RAG 方法依赖于单轮检索,这意味着信息生成开始时只进行一次外部信息检索。这对于简单的任务来说效果很好,但我们对 LLM 的需求和要求正变得越来越复杂、多步骤,并且需要更长的回答。
在这些情况下,单轮检索效果不好,需要进行多轮检索。当我们谈到多次检索时,接下来的两个问题是:
何时 进行检索, 检索什么 ?为了解决这些问题,已经设计了多种 RAG 方法:
固定检索方法:
IRCoT(固定句子 RAG) [1]:为每个生成的查询进行检索,最新的句子用作查询。
RETRO [2] 和 IC-RALM [3] (固定长度 RAG):定义一个滑动窗口,每生成n个 token 就触发一次检索模块。
但我们不是在检索 太频繁 了吗?这会导致检索到可能不必要的信息吗?这会引入噪声,并可能危及 LLM 输出的质量,违背了提高准确性的初衷。这些规则仍然是 静态 的,我们需要思考 动态 的检索方式。
动态检索方法:
FLARE [4] (低置信度 RAG):当 LLM 对下一个 token 的置信度(生成概率)低于某个阈值时,会动态进行检索。所以,FLARE 是基于不确定性触发检索的。
为了确定检索什么,LLM 通常会限制自己基于最近生成的几个 token 或句子来形成查询。这些查询生成方法在任务变得更加复杂且 LLM 的信息需求涵盖整个上下文时可能无法奏效!
最后,让我们来看看本次的明星:DRAGIN!
DRAGIN(基于信息需求的动态检索增强生成):
这种方法专门用于决定何时以及检索什么信息,以满足 LLM 的信息需求。因此,它通过两个框架优化了信息检索的过程。正如作者在论文中所解释的,DRAGIN 有两个关键框架:
I. RIND(实时信息需求检测):什么时候检索?
它会考虑 LLM 对自身内容的不确定性、每个标记的影响以及每个标记的语义。
II. QFS(基于自注意力的查询构建):要检索什么?
查询构建利用了 LLM 在整个上下文中的自注意力,而不仅仅是最后几个标记或句子。
DRAGIN 框架的示意图
为了说明上述框架,论文使用了一个关于‘爱因斯坦简要介绍’的查询示例。

图 1:DRAGIN 框架的示意图,摘自研究论文
解释:
提供输入: 系统被查询提供一些关于爱因斯坦的介绍。
处理开始: 系统根据它已知的信息开始生成响应。它使用 RIND 模块来决定是否有足够的信息,或者是否需要查找更多信息。
检查所需信息(RIND): 系统将查询分解成更小的部分(标记),例如“职位”、“在”、“大学”等。它检查哪些部分(标记)需要更多信息。例如,“大学”可能需要额外的数据,因为它不够具体。
触发检索: 如果像“大学”这样的标记被认为重要且不明确,系统会触发检索以收集关于它的外部信息。在这种情况下,它会查找有关爱因斯坦和大学的相关数据。
制定查询(QFS): 系统使用自注意力机制来确定哪些词语在形成精确查询时最为相关。例如,它可能会选择“爱因斯坦”、“1903”和“找到工作”作为关键部分。
这些关键词用于构建查询,例如“爱因斯坦 1903 找到一份工作”,该查询将发送给外部源获取信息。
获取和添加信息: 外部源提供所需的详细信息。例如,它可能返回:“在 1903 年,爱因斯坦在瑞士专利局找到了一份工作。”系统将这些新信息融入到回答中。
继续生成: 根据新信息,系统继续生成更完整、更准确的回答。例如,它现在可能会说:“在 1903 年,爱因斯坦在瑞士专利局找到了一份工作。这使他能够有稳定的收入。”
重复过程: 如果识别到更多需求,过程将重复: 检查、检索和整合信息 直到响应完整且准确。这个过程确保系统能够动态地填补知识空白,并通过结合已知信息和检索到的外部信息,提供详细且准确的答案。
RAG 的详细方法
文中提到的框架有:
A. 实时信息需求检测 (RIND) : 检索是基于令牌的不确定性、对其他令牌的影响以及每个令牌的语义重要性来触发的。
i. 定量化每个 LLM 生成的令牌的不确定性。通过计算令牌在词汇表中的概率分布的熵来实现这一点。考虑输出序列 T = {t1,t2,…tn},其中每个 ti 表示位置 i 的一个单独令牌。对于任何令牌 ti,熵的计算公式如下:

其中 pi(v) 表示生成令牌 v 在词汇表中所有令牌中的概率。
ii. 通过利用自注意力分数来计算每个令牌对后续令牌的影响。对于令牌 t,识别最大注意力值

iii. 每个令牌 ti 的语义贡献,采用二进制指示器。这会过滤掉停用词。

结合不确定性、重要性和语义,RIND 计算一个分数,如果这个分数大于预定义的阈值,则触发检索。

B. 基于自注意力的查询制定(QFS)
一旦触发检索,下一步是从外部数据库中制定高效的查询,以继续生成 LLM。在现有的动态 RAG 框架中,查询是使用 LLM 生成的最后一句话或最后一个令牌来制定的。这种狭窄的范围没有捕捉到实时信息需求的必要性。它检查的是完整的上下文。
假设 RIND 识别到位置 i 的令牌 ti,需要外部知识并触发检索。
由于令牌 ti 是基于所有前置令牌的知识生成的,因此从现在起查看已生成的完整内容来制定查询是有意义的。它使用以下步骤:
步骤 1:提取最后一个 Transformer 层中每个令牌 ti 的注意力分数。
步骤 2:按降序排列注意力分数,以识别前n个分数。(这基本上是识别最重要的令牌)。
步骤 3:从词汇表中找到这些令牌对应的词,并按照原始顺序排列。(这通过注意力分数和令牌恢复了语言结构)。
第 4 步:使用与这些顶部n个标记相关的词构造查询Qi。
C. 检索后继续生成
一旦 RIND 识别出需要外部知识的位置i,并且 QFS 生成查询 Qi 以使用现成的检索模型(例如,BM25)提取信息。
它从文档Di1、Di2和Di3中找到了相关信息。通过截断 LLM 的输出,它整合了位置i上的相关信息。然后,使用以下设计的提示模板整合这些检索到的知识。

用于整合外部检索信息的设计提示模板。
局限性
正如本文所述,本文的主要局限性在于它依赖自注意力机制来处理实时信息需求检测(RIND)和基于自注意力的查询生成(QFS)。虽然所有源 LLM 都可以获得自注意力分数,但对于某些不提供自注意力分数的 API,这种方法不可行。
值得考虑的一点是对推理时间延迟和成本的影响:在论文中,作者指出这些影响仅仅是微乎其微,因为不完美的标记子序列会迅速被检测到,进一步的生成会被中断,直到进行修正。
结论
DRAGIN 框架使我们能够比传统的 RAG 框架走得更远。它允许我们根据生成的信息需求执行多次检索。它是一个优化的多次检索框架!
我们对 LLM 的需求和要求变得越来越大且复杂,在我们希望准确检索信息且仅需适量检索的情况下,DRAGIN 框架提供了解决方案。
总结,DRAGIN:
为检索的数量找到了完美的平衡。
生成高度上下文感知的检索查询。
生成来自 LLM 的内容,准确度更高!
非常感谢您的阅读,若想更详细了解本研究论文,请观看我的视频!
参考文献:
[1] Harsh Trivedi, Niranjan Balasubramanian, Tushar Khot, and Ashish Sabharwal. 2022. Interleaving retrieval with chain-of-thought reasoning for knowledge-intensive multi-step questions. arXiv preprint arXiv:2212.10509.
[2] Sebastian Borgeaud, Arthur Mensch, Jordan Hoffmann, Trevor Cai, Eliza Rutherford, Katie Millican, George Bm Van Den Driessche, Jean-Baptiste Lespiau, Bogdan Damoc, Aidan Clark, et al. 2022.
[3] Ori Ram, Yoav Levine, Itay Dalmedigos, Dor Muhlgay, Amnon Shashua, Kevin Leyton-Brown, and Yoav Shoham. 2023. In-context retrieval-augmented language models. arXiv preprint arXiv:2302.00083.
[4] Zhengbao Jiang, Frank F Xu, Luyu Gao, Zhiqing Sun, Qian Liu, Jane Dwivedi-Yu, Yiming Yang, Jamie Callan, and Graham Neubig. 2023. Active retrieval augmented generation. arXiv preprint arXiv:2305.06983.
在 SQL 中从随机分布中抽样
从概率密度函数到随机样本
·发表于 Towards Data Science ·8 分钟阅读·2024 年 2 月 9 日
--

图片来自 Moritz Kindler ,发布于 Unsplash
在每次迭代中,更新强化学习代理的策略有不同的方法。几周前,我们开始尝试用贝叶斯推理步骤替换当前的方法。我们代理中的一些数据工作负载是用 SQL 编写的,并在 GCP 的 BigQuery 引擎上执行。我们使用这个技术栈,因为它提供了可扩展的计算能力、机器学习包和一个直观的 SQL 接口。
我们计划实现的贝叶斯推理步骤使用的是 贝塔分布的替代参数化方式。这意味着我们需要能够在 SQL 中从贝塔分布中抽样。在研究这个问题时,我发现 SQL 中从随机分布中抽样是一个几乎没有好文档示例的话题。所以我在这里写一下。
第一步:这有多难?
BigQuery 没有贝塔分布。它也没有从任何随机分布中抽样的能力。所以我最初的想法是,拿贝塔分布的定义,用 SQL 写出来,使用 CTA 设置参数,从 0 到 1 之间抽一个随机数,并计算该函数的值。
但这是 2024 年,所以我问了 ChatGPT 它会怎么做:
我:“如何在 BigQuery 中从贝塔分布中抽取随机样本?”
ChatGPT:
WITH beta_draws AS (
SELECT
(alpha — 1) / (alpha + beta — 2) AS a,
(beta — 1) / (alpha + beta — 2) AS b
FROM
(SELECT 2 AS alpha, 5 AS beta) — Set your desired alpha and beta values
)
SELECT
(a * POW(RAND(), alpha - 1) * POW(1 - RAND(), beta - 1) + b) AS beta_draw
FROM
beta_draws,
UNNEST(GENERATE_ARRAY(1, 1000)) AS rand_num
我 自言自语:对了,这显然行不通。
你在代码中看到了问题吗?ChatGPT 为假定的 Beta 分布 PDF 绘制了两个 不同 的 x 值。我修正了这个问题,稍微清理了一下查询,并抽样了 1,000 个值。下面是执行此操作的 SQL 代码:
WITH raw_data AS (
SELECT
‘user_a’ AS contact_id
, 0.674 AS probability
, 72 AS lifetime_messages_received
), parameters AS (
SELECT
contact_id
, probability*lifetime_messages_received AS alpha
, (1.0 — probability)*lifetime_messages_received AS beta
, RAND() AS x
FROM raw_data
CROSS JOIN UNNEST(GENERATE_ARRAY(1, 1000)) AS draw_id
)
SELECT
contact_id
, ARRAY_AGG(POW(x, alpha — 1.0) * POW(1.0 — x, beta — 1)) AS beta_x
FROM parameters
GROUP BY contact_id
感谢大家,今天就到这里 🎁 下篇文章见!
错误!🔴
让我们用相同的参数,取一个经过验证的 Beta 分布抽样实现,并对比结果。我使用了 Python 中 SciPy 的 beta.rvs(),这里有两个 100-bin 的直方图,可以用来比较这两个抽样分布。
from scipy.stats import beta
alpha_param = 0.674 * 72
beta_param = (1–0.674) * 72
scipy_beta_draw = beta.rvs(alpha_param, beta_param, size=1000)

(左侧):使用 BigQuery 的简单抽样。(右侧):使用 SciPy 的 beta.rvs() 抽样
好吧,仔细看就能发现分布是不同的。我回过头查看了 Beta 分布的定义,意识到可能是因为 Beta 分布也有一个依赖于 伽马函数 的缩放常数,而我在计算中没有包括它 🤦。
问题:伽马函数没有 封闭式表达式,而 BigQuery 也没有提供近似的实现。因此,在这一点上,我决定转向 Python,这是我更熟悉的语言,并且可以提高我的实验效率。我想到,如果在 Python 中搞定了,我就能把它转到 SQL 中去。虽然我仍然需要某种方法来近似伽马函数,但一步一步来。
步骤 2:从随机分布抽样到底意味着什么?
让我们在 Python 中实现从 Beta 分布手动抽样,但这次使用 SciPy 的伽马函数来正确计算常数:
import numpy as np
from scipy.special import gamma
from scipy.stats import uniform
alpha_param = 0.674 * 72
beta_param = (1–0.674) * 72
constant = gamma(alpha_param + beta_param) / (gamma(alpha_param) * gamma(beta_param))
scipy_manual_beta_draw = np.array([
constant*pow(x, alpha_param-1)*pow(1-x, beta_param-1)
for x in uniform.rvs(size=1000)
])
让我们再次用一个 100-bin 的直方图来检查分布:

使用 Python 的简单抽样
我们首先注意到的是,规模现在不同了,但分布仍然看起来像是在 BigQuery 中绘制的那样。
... 有些地方不对... 是时候去散步思考一下了 🚶
…
短暂散步后:
从随机分布抽样到底意味着什么?到目前为止我实现的是从 Beta 概率密度函数(PDF)中随机抽样,但它并没有成功。
所以我不得不翻阅一些统计学课程。
这里有一些很好的复习资料:
-
我发现非常有用的 从概率分布生成样本。
简而言之,结论是从随机变量中抽样实际上意味着从逆累积分布函数(CDF)中抽样,而不是像我之前做的那样从概率密度函数(PDF)中抽样。
当然了 🤦。我的概率论教授,我刚得知他在 2020 年因病去世了,他本该鼓励我在这个时候“复习基础知识”。
好的。让我们回顾一下 Python 代码,现在它是从我们 beta 分布的逆 CDF(也称为分位数函数)中抽样,并与使用 SciPy 的 beta.rvs() 抽样的分布进行比较:
import numpy as np
from scipy.special import gamma
from scipy.stats import uniform, beta
alpha_param = 0.674 * 72
beta_param = (1–0.674) * 72
n_draws = 1000
# Use SciPy RVS for comparison
scipy_beta_draw = beta.rvs(alpha_param, beta_param, size=n_draws)
# Manual beta draw with the help of the SciPy Gamma function
# We start with a discrete analogue of the Beta PDF we wish to draw from.
# This is just sampling from the PDF at fixed intervals but do check out
# this review for a more in-depth treatment of the subject:
# https://jsdajournal.springeropen.com/articles/10.1186/s40488-015-0028-6
# Set the resolution for generating the discrete PDF
n_samples = 1000
# The beta distribution is supported on the range [0, 1], so we set the
# pdf min and max parameters accordingly
pdf_min = 0.0
pdf_max = 1.0
x_span = np.linspace(pdf_min, pdf_max, n_samples)
constant = gamma(alpha_param + beta_param) / (gamma(alpha_param) * gamma(beta_param))
beta_pdf = np.array([
constant * pow(x, alpha_param — 1) * pow(1 — x, beta_param — 1)
for x in x_span
])
# Using the discrete Beta PDF, we now compute a discrete Beta CDF.
# To do that, we integrate the PDF. For each point x, we sum the PDF until
# that point and multiple with the width of each sample.
freq = 1.0 / n_samples
beta_cdf = beta_pdf.cumsum() * freq
def inv(cdf, q):
“””Return inverse CDF for value q using the quantile function”””
return x_span[np.argmin(cdf < q)]
# Finally, we can now draw n_draws from the discrete inverse of CDF, aka
# generate random samples from a beta distribution
manual_beta_draw = np.array([
inv(beta_cdf, x)
for x in uniform.rvs(size=n_draws)
])
呼 这个看起来好多了:

两个直方图的叠加,比较使用 SciPy 的 beta.rvs() 和手动抽样的 1,000 次抽样结果
第 3 步:回到 SQL
现在我们已经正确地从随机变量中抽样,是时候回到 SQL 了。为了简便起见,并且因为 BigQuery 并未直接提供 Gamma 函数的实现¹,我将从逻辑斯蒂分布中抽样(参数 a=0,b=1)。
— The following 3 parameters need to be adjusted based on the support of the
— PDF of the distribution you wish to draw from. This values are set for a logistic
— distribution with a=0 and b=1
DECLARE pdf_min INT64 DEFAULT -10;
DECLARE pdf_max INT64 DEFAULT 10;
DECLARE n_samples INT64 DEFAULT 5000;
DECLARE sampling_step FLOAT64 DEFAULT (pdf_max — pdf_min) / n_samples;
— The number of random draws you wish to perform
DECLARE n_draws INT64 DEFAULT 1000;
WITH pdf AS (
— The discrete sampling of the logistic distribution PDF
SELECT
x
, exp(-x) / pow(1 + exp(-x), 2) AS y — a=0, b=1
FROM UNNEST(GENERATE_ARRAY(pdf_min, pdf_max, sampling_step)) AS x
), cdf AS (
— The discrete CDF
SELECT
x
, SUM(y)
OVER (
ORDER BY x
) * (1.0 / n_samples) AS y
FROM pdf
), random_draws AS (
— Random draws in the range of [0, max(cdf)]
SELECT
RAND() * (SELECT MAX(y) FROM cdf) as q
, draw_id
FROM UNNEST(GENERATE_ARRAY(1, n_draws)) AS draw_id
)
— Calculate the inverse CDF per draw using the quantile function by generating
— and array of the discrete support of the distribution and returning the value
— of the index just before the randomly generated number is larger than the CDF
SELECT
ARRAY_AGG(x ORDER BY x)[OFFSET(SUM(CAST(y < q AS INT64)))] AS x
FROM random_draws
JOIN cdf
ON TRUE
GROUP BY draw_id;
现在让我们比较这三种抽样方法的分布:
-
SciPy 的
logistic.rvs() -
在 Python 中手动从逻辑斯蒂分布的 PDF 中抽样,并按照上面第 2 步的方式进行随机抽样
-
在 SQL 中执行相同操作

三个直方图的叠加,比较使用 SciPy 的 beta.rvs()、Python 中的手动抽样和 SQL 中的手动抽样的 1,000 次抽样结果
看起来这是一次成功!💪
上面的 SQL 代码从逻辑斯蒂分布中抽样,但它应该适用于任何分布,只要你能通过在一致的间隔内抽样来获得概率密度函数(PDF)的离散表示!
[1] 我确实曾尝试寻找 SQL 中的 Gamma 函数近似实现,最后放弃了。很可能在 SQL 中编写 Gamma 函数的近似是可能的,但这需要更多的研究,而我分配的时间并不足够。请小心盲目复制粘贴一个近似实现(例如,将 Rosetta Code 中的某段代码翻译成 SQL),因为这些实现会假设 Gamma 参数,而这些假设并不总是显而易见。例如,Python 代码(由 Ada 转译而来)对于小 Gamma 值才是准确的。
—
这就是我们为不断改进 Aampe 所做的工作,Aampe 是一个强化学习代理,能够为用户个性化电子邮件、网页/推送通知、短信和 WhatsApp 消息。
除非另有说明,所有图片均由作者提供。
DSLP——改变我团队的数据显示科学项目管理框架

这是目前为止最适合数据科学的框架。无论是为你的团队还是仅仅为自己使用,它都非常有效。以下是我如何使用它的。
·发表于数据科学前沿·阅读时间 16 分钟·2024 年 8 月 28 日
--
虽然软件工程实践要求创建问题以适应变化的客户需求,但我们需要能够适应由我们自己研究所推动的变化需求的实践。
目录
-
你可能已经尝试过敏捷方法……
-
为什么敏捷方法不适用于数据科学……
-
数据科学生命周期过程(DSLP)
-
DSL 的五个步骤
-
示例项目:检测信用卡欺诈
-
新项目——创建一个问题请求
-
探索数据——数据问题
-
这种布局导致了传统的敏捷项目
-
适合数据科学的看板
-
结论
你可能已经尝试过敏捷方法……
DuckDB 和 AWS — 如何在 1 分钟内聚合 1 亿条数据
使用 Python 和 DuckDB 处理大规模数据 — 一个 AWS S3 示例。
·发表于 Towards Data Science ·阅读时长 4 分钟·2024 年 4 月 25 日
--

当公司需要一个安全、高效且可扩展的存储解决方案时,它们通常会选择云服务。AWS S3 是行业中最受欢迎的平台之一 — 理由也很充分 — 它是行业领先的对象存储解决方案,可以作为数据湖使用。
问题是 — 你能在不下载 S3 存储桶数据的情况下聚合它吗?而且你能快速完成吗?
答案是肯定的,两个问题的答案都是肯定的。DuckDB 允许你通过 httpfs 扩展直接连接到 S3 存储桶。今天你将通过聚合大约 1.11 亿条数据,这些数据分布在 37 个 Parquet 文件中,来学习如何使用它。
剧透: 你大约只需 1 分钟。
注意: 我写这篇文章是因为我在寻找一个更高效的 Pandas 替代品。我的目标是在本地对大规模数据集进行分析,而不是选择云解决方案。我与 DuckDB 或 AWS 没有任何关联。
AWS S3 设置
首先,你需要一个 AWS 账户和一个 S3 存储桶。你还需要创建一个 IAM 用户,并为其生成访问密钥。
虚拟分类器详解:面向初学者的视觉指南与代码示例
分类算法
在机器学习中,通过简单的基线模型设定标准
·发布于 Towards Data Science ·阅读时间:7 分钟·2024 年 8 月 14 日
--

⛳️ 更多分类算法,详解: ▶ 虚拟分类器 · K 近邻分类器 · 伯努利朴素贝叶斯 · 高斯朴素贝叶斯 · 决策树分类器 · 逻辑回归 · 支持向量分类器 · 多层感知机
你是否曾经想过数据科学家是如何衡量他们的机器学习模型性能的?让我们来看一下虚拟分类器——一个简单却强大的数据科学工具。可以把它看作是游戏中的基线玩家,设定了其他更复杂模型需要超越的最低标准。

所有视觉效果:作者使用 Canva Pro 制作。优化了移动端显示;在桌面端可能显示过大。
定义
虚拟分类器是一个简单的机器学习模型,它使用基本规则进行预测,而不是真正地从输入数据中学习。它作为一个基准,用于比较更复杂模型的性能。虚拟分类器帮助我们了解我们的复杂模型是否真的在学习有用的模式,还是只是在猜测。

虚拟分类器是机器学习中的基础关键算法之一。
📊 数据集与库
在本文中,我们将使用这个简单的人工高尔夫数据集(灵感来源于[1])作为示例。这个数据集根据天气条件预测一个人是否会打高尔夫。它包括如展望、温度、湿度和风等特征,目标变量是是否打高尔夫。

列:‘展望’、‘温度’、‘湿度’、‘风’ 和 ‘打球’(目标特征)
# Import libraries
from sklearn.model_selection import train_test_split
import pandas as pd
# Make a dataset
dataset_dict = {
'Outlook': ['sunny', 'sunny', 'overcast', 'rain', 'rain', 'rain', 'overcast', 'sunny', 'sunny', 'rain', 'sunny', 'overcast', 'overcast', 'rain', 'sunny', 'overcast', 'rain', 'sunny', 'sunny', 'rain', 'overcast', 'rain', 'sunny', 'overcast', 'sunny', 'overcast', 'rain', 'overcast'],
'Temperature': [85.0, 80.0, 83.0, 70.0, 68.0, 65.0, 64.0, 72.0, 69.0, 75.0, 75.0, 72.0, 81.0, 71.0, 81.0, 74.0, 76.0, 78.0, 82.0, 67.0, 85.0, 73.0, 88.0, 77.0, 79.0, 80.0, 66.0, 84.0],
'Humidity': [85.0, 90.0, 78.0, 96.0, 80.0, 70.0, 65.0, 95.0, 70.0, 80.0, 70.0, 90.0, 75.0, 80.0, 88.0, 92.0, 85.0, 75.0, 92.0, 90.0, 85.0, 88.0, 65.0, 70.0, 60.0, 95.0, 70.0, 78.0],
'Wind': [False, True, False, False, False, True, True, False, False, False, True, True, False, True, True, False, False, True, False, True, True, False, True, False, False, True, False, False],
'Play': ['No', 'No', 'Yes', 'Yes', 'Yes', 'No', 'Yes', 'No', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'No', 'No', 'Yes', 'Yes', 'No', 'No', 'No', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'No', 'Yes']
}
df = pd.DataFrame(dataset_dict)
# One-hot Encode 'Outlook' Column
df = pd.get_dummies(df, columns=['Outlook'], prefix='', prefix_sep='', dtype=int)
# Convert 'Windy' (bool) and 'Play' (binary) Columns to 0 and 1
df['Wind'] = df['Wind'].astype(int)
df['Play'] = (df['Play'] == 'Yes').astype(int)
# Set feature matrix X and target vector y
X, y = df.drop(columns='Play'), df['Play']
# Split the data into training and testing sets
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.5, shuffle=False)
主要机制
虚拟分类器依靠简单的策略做出预测。这些策略不涉及从数据中进行实际学习,而是使用诸如以下的基本规则:
-
始终预测最常见的类别
-
基于训练集的类别分布随机预测一个类别
-
始终预测一个特定类别

对于我们的高尔夫数据集,如果“是”——即打高尔夫是训练数据中最常见的结果,那么一个虚拟分类器可能始终预测“是”。
训练步骤
虚拟分类器的“训练”过程非常简单,并不涉及通常的学习算法。以下是一般的概述:
1. 选择策略
选择以下策略之一:
-
分层:基于原始类别分布进行随机猜测。
-
最频繁:始终选择最常见的类别。
-
均匀:随机选择任何类别。

根据策略的不同,虚拟分类器会做出不同的预测。
from sklearn.dummy import DummyClassifier
# Choose a strategy for your DummyClassifier (e.g., 'most_frequent', 'stratified', etc.)
strategy = 'most_frequent'
2. 收集训练标签
从训练数据集收集类别标签以确定策略参数。

该算法只是获取训练数据集中“最频繁”类别的信息——在这个例子中是“是”。
# Initialize the DummyClassifier
dummy_clf = DummyClassifier(strategy=strategy)
# "Train" the DummyClassifier (although no real training happens)
dummy_clf.fit(X_train, y_train)
3. 将策略应用于测试数据
使用选择的策略生成测试数据的预测标签列表。

如果我们选择“最频繁”策略,并发现“是”(打高尔夫)在训练数据中出现得更频繁,虚拟分类器将简单地记住始终预测“是”。
# Use the DummyClassifier to make predictions
y_pred = dummy_clf.predict(X_test)
print("Label :",list(y_test))
print("Prediction:",list(y_pred))
评估模型

虚拟分类器提供 64%的准确率作为未来模型的基准。
# Evaluate the DummyClassifier's accuracy
from sklearn.metrics import accuracy_score
accuracy = accuracy_score(y_test, y_pred)
print(f"Dummy Classifier Accuracy: {round(accuracy,4)*100}%")
关键参数
虽然虚拟分类器很简单,但它们确实有一些重要的参数:
-
策略:这决定了分类器如何做出预测。常见的选项包括:
-
‘最常见’:始终预测训练集中的最常见类别。
-
‘分层’:根据训练集的类别分布生成预测。
-
‘均匀’:生成均匀随机的预测。
-
‘常量’:始终预测指定的类别。
-
-
随机状态:如果使用涉及随机性的策略(如‘分层’或‘均匀’),此参数确保结果的可重复性。
-
常量:使用‘常量’策略时,此参数指定始终预测的类别。

对于我们的高尔夫数据集,我们可能会选择‘最常见’策略,它不需要额外的参数。
优缺点
与机器学习中的任何工具一样,虚拟分类器有其优点和局限性。
优点:
-
简洁性:易于理解和实现。
-
基线表现:为其他模型提供最低表现基准。
-
过拟合检查:通过将复杂模型的表现与虚拟分类器进行比较,帮助识别过拟合情况。
-
训练和预测快速:需要的计算资源最少。
缺点:
-
有限的预测能力:由于设计原因,它不会从数据中学习,因此其预测通常不准确。
-
没有特征重要性:它不提供哪些特征对预测最重要的见解。
-
不适合复杂问题:在具有复杂模式的真实世界场景中,虚拟分类器过于简单,无法单独发挥作用。
最终评论
了解虚拟分类器对于任何数据科学家或机器学习爱好者来说至关重要。它们作为一种现实检查,帮助我们确保我们的更复杂模型确实从数据中学习到了有用的模式。在你继续进行机器学习之旅时,记得始终将你的模型与这些简单的基线进行比较——你可能会对所学到的内容感到惊讶!
🌟 虚拟分类器代码概述
# Import necessary libraries
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn.dummy import DummyClassifier
# Make dataset
dataset_dict = {
'Outlook': ['sunny', 'sunny', 'overcast', 'rain', 'rain', 'rain', 'overcast', 'sunny', 'sunny', 'rain', 'sunny', 'overcast', 'overcast', 'rain', 'sunny', 'overcast', 'rain', 'sunny', 'sunny', 'rain', 'overcast', 'rain', 'sunny', 'overcast', 'sunny', 'overcast', 'rain', 'overcast'],
'Temperature': [85.0, 80.0, 83.0, 70.0, 68.0, 65.0, 64.0, 72.0, 69.0, 75.0, 75.0, 72.0, 81.0, 71.0, 81.0, 74.0, 76.0, 78.0, 82.0, 67.0, 85.0, 73.0, 88.0, 77.0, 79.0, 80.0, 66.0, 84.0],
'Humidity': [85.0, 90.0, 78.0, 96.0, 80.0, 70.0, 65.0, 95.0, 70.0, 80.0, 70.0, 90.0, 75.0, 80.0, 88.0, 92.0, 85.0, 75.0, 92.0, 90.0, 85.0, 88.0, 65.0, 70.0, 60.0, 95.0, 70.0, 78.0],
'Wind': [False, True, False, False, False, True, True, False, False, False, True, True, False, True, True, False, False, True, False, True, True, False, True, False, False, True, False, False],
'Play': ['No', 'No', 'Yes', 'Yes', 'Yes', 'No', 'Yes', 'No', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'No', 'No', 'Yes', 'Yes', 'No', 'No', 'No', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'No', 'Yes']
}
df = pd.DataFrame(dataset_dict)
# Perform one-hot encoding on 'Outlook' column
df = pd.get_dummies(df, columns=['Outlook'], prefix='', prefix_sep='', dtype=int)
# Convert 'Wind' and 'Play' columns to binary indicators
df['Wind'] = df['Wind'].astype(int)
df['Play'] = (df['Play'] == 'Yes').astype(int)
# Split data into features (X) and target (y), then into training and test sets
X, y = df.drop(columns='Play'), df['Play']
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.5, shuffle=False)
# Initialize and train the dummy classifier model
dummy_clf = DummyClassifier(strategy='most_frequent')
dummy_clf.fit(X_train, y_train)
# Make predictions on the test data
y_pred = dummy_clf.predict(X_test)
# Calculate and print the model's accuracy on the test data
print(f"Accuracy: {accuracy_score(y_test, y_pred)*100:.4f}%")
进一步阅读
对于DummyClassifier及其在 scikit-learn 中的实现的详细说明,读者可以参考官方文档[2],该文档提供了有关其使用和参数的全面信息。
技术环境
本文使用 Python 3.7 和 scikit-learn 1.5。尽管所讨论的概念一般适用,但不同版本的具体代码实现可能会有所不同。
关于插图
除非另有说明,所有图片均由作者创作,并结合了 Canva Pro 的授权设计元素。

若要查看虚拟分类器的简洁视觉总结,请访问配套 Instagram 帖子。
参考文献
[1] T. M. Mitchell, 机器学习(1997),McGraw-Hill Science/Engineering/Math,第 59 页
在此查看更多分类算法:

分类算法
查看列表8 篇故事!

你可能还喜欢:

回归算法
查看列表5 篇故事!



集成学习
查看列表4 篇故事!

虚拟回归器解释:初学者的视觉指南与代码示例
回归算法
天真地选择所有预测的最佳数字
·发表于 Towards Data Science ·阅读时间:7 分钟·2024 年 9 月 26 日
--
很多时候,我的学生会来找我,说他们想尝试最复杂的模型来完成机器学习任务,有时我开玩笑地说:“你尝试过最棒的模型了吗?”尤其在回归问题中(我们没有那种“100%准确率”的目标),一些机器学习模型表面上可能得到了很低的误差分数,但与虚拟模型进行比较后,实际上……并没有那么好。
所以,这就是虚拟回归器。就像在分类器中一样,回归任务也有其基准模型——你必须尝试的第一个模型,以大致了解你的机器学习模型能有多好。

所有视觉效果:作者使用 Canva Pro 创建,针对移动设备进行了优化;在桌面上可能会显示过大。
定义
虚拟回归器是一个简单的机器学习模型,它使用基本规则预测数值,而并非真正从输入数据中学习。像其分类对手一样,它作为基准模型,用于与更复杂的回归模型性能进行比较。虚拟回归器帮助我们理解我们的模型是否真的在学习有用的模式,还是仅仅在做天真的预测。

虚拟回归器是最简单的机器学习模型。
📊 数据集与库
在本文中,我们将使用这个简单的人工高尔夫数据集作为示例。该数据集预测访问我们高尔夫球场的高尔夫球手数量。它包括如前景、温度、湿度和风速等特征,目标变量是高尔夫球手的数量。

列:‘Outlook’(前景),‘Temperature’(温度,单位:华氏度),‘Humidity’(湿度,单位:%),‘Wind’(风,Yes/No),以及‘Number of Players’(玩家数量,数值型,目标特征)
# Import libraries
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
# Create dataset
dataset_dict = {
'Outlook': ['sunny', 'sunny', 'overcast', 'rain', 'rain', 'rain', 'overcast', 'sunny', 'sunny', 'rain', 'sunny', 'overcast', 'overcast', 'rain', 'sunny', 'overcast', 'rain', 'sunny', 'sunny', 'rain', 'overcast', 'rain', 'sunny', 'overcast', 'sunny', 'overcast', 'rain', 'overcast'],
'Temperature': [85.0, 80.0, 83.0, 70.0, 68.0, 65.0, 64.0, 72.0, 69.0, 75.0, 75.0, 72.0, 81.0, 71.0, 81.0, 74.0, 76.0, 78.0, 82.0, 67.0, 85.0, 73.0, 88.0, 77.0, 79.0, 80.0, 66.0, 84.0],
'Humidity': [85.0, 90.0, 78.0, 96.0, 80.0, 70.0, 65.0, 95.0, 70.0, 80.0, 70.0, 90.0, 75.0, 80.0, 88.0, 92.0, 85.0, 75.0, 92.0, 90.0, 85.0, 88.0, 65.0, 70.0, 60.0, 95.0, 70.0, 78.0],
'Wind': [False, True, False, False, False, True, True, False, False, False, True, True, False, True, True, False, False, True, False, True, True, False, True, False, False, True, False, False],
'Num_Players': [52,39,43,37,28,19,43,47,56,33,49,23,42,13,33,29,25,51,41,14,34,29,49,36,57,21,23,41]
}
df = pd.DataFrame(dataset_dict)
# One-hot encode 'Outlook' column
df = pd.get_dummies(df, columns=['Outlook'], prefix='', prefix_sep='', dtype=int)
# Convert 'Wind' column to binary
df['Wind'] = df['Wind'].astype(int)
# Split data into features and target, then into training and test sets
X, y = df.drop(columns='Num_Players'), df['Num_Players']
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.5, shuffle=False)
评估回归结果
在深入讨论虚拟回归器之前,让我们回顾一下评估回归结果的方法。在分类问题中,检查模型的准确度是非常直观的(只需检查匹配值的比例),而在回归中则有所不同。
RMSE(均方根误差)就像是回归模型的评分。它告诉我们预测值与实际值之间的偏差。就像在分类问题中我们希望获得高准确率以得到更多正确答案一样,在回归中我们希望 RMSE 较低,以便更接近真实值。
人们喜欢使用 RMSE,因为它的值与我们试图预测的值类型相同。

RMSE=3 可以解释为实际值与预测值的差异在±3 范围内。
from sklearn.metrics import mean_squared_error
y_true = np.array([10, 15, 20, 15, 10]) # True labels
y_pred = np.array([15, 11, 18, 14, 10]) # Predicted values
# Calculate RMSE using scikit-learn
rmse = mean_squared_error(y_true, y_pred, squared=False)
print(f"RMSE = {rmse:.2f}")
记住这一点,让我们开始讨论算法。
主要机制
虚拟回归器根据简单的规则进行预测,例如始终返回训练数据中目标值的均值或中位数。

对于我们的高尔夫数据集,虚拟回归器可能始终预测“40.5”作为玩家数量,因为这是训练标签的中位数。
训练步骤
虚拟回归器的训练过程有点像是个骗局,不过无论如何,下面是一个大致的概述:
1. 选择策略
选择以下策略之一:
-
均值:始终预测训练目标值的均值。
-
中位数:始终预测训练目标值的中位数。
-
常数:始终预测用户提供的常数值。

根据策略的不同,虚拟回归器会给出不同的数值预测。
from sklearn.dummy import DummyRegressor
# Choose a strategy for your DummyRegressor ('mean', 'median', 'constant')
strategy = 'median'
2. 计算度量
根据你的策略计算均值或中位数。

该算法仅仅是计算训练数据的中位数——在这个例子中,我们得到的中位数是 40.5。
# Initialize the DummyRegressor
dummy_reg = DummyRegressor(strategy=strategy)
# "Train" the DummyRegressor (although no real training happens)
dummy_reg.fit(X_train, y_train)
3. 将策略应用于测试数据
使用所选策略生成测试数据的预测数值标签列表。

如果我们选择“中位数”策略,计算出的中位数(40.5)将简单地作为所有预测的值。
# Use the DummyRegressor to make predictions
y_pred = dummy_reg.predict(X_test)
print("Label :",list(y_test))
print("Prediction:",list(y_pred))
评估模型

使用此策略的虚拟回归器给出了 13.28 的误差值,作为未来模型的基准。
# Evaluate the Dummy Regressor's error
from sklearn.metrics import mean_squared_error
rmse = mean_squared_error(y_test, y_pred, squared=False)
print(f"Dummy Regression Error: {rmse.round(2)}")
关键参数
虚拟回归器中只有一个主要的关键参数,即:
-
策略:这决定了回归器如何进行预测。常见的选项包括:
-
均值:提供一个平均基准,通常用于一般场景。
-
中位数:对异常值更具鲁棒性,适用于偏斜的目标分布。
-
常数:当领域知识建议进行特定常数预测时很有用。
-
-
常数:当使用“常数”策略时,这个参数指定了始终预测的类别。

无论使用什么策略,结果都同样糟糕,但可以确定我们的下一个回归模型的 RMSE 值应该低于 12。
优缺点
作为懒惰预测器,虚拟回归器肯定有其优点和局限性。
优点:
-
简单基准:快速展示其他模型应该超越的最小性能。
-
快速:设置和运行几乎不需要时间。
缺点:
-
不学习:仅使用简单规则,因此通常会被真正的模型超越。
-
忽略特征:在进行预测时不考虑任何输入数据。
最后的备注
使用虚拟回归器应该是我们进行回归任务的第一步。它们提供了一个标准基准线,因此我们可以确认一个更复杂的模型实际上提供了比随机预测更好的结果。当你学习更多先进的技术时,千万不要忘记将你的模型与这些简单的基准进行比较——这些天真的预测可能正是你最初需要的!
🌟 虚拟回归器代码总结
# Import libraries
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
from sklearn.dummy import DummyRegressor
# Create dataset
dataset_dict = {
'Outlook': ['sunny', 'sunny', 'overcast', 'rain', 'rain', 'rain', 'overcast', 'sunny', 'sunny', 'rain', 'sunny', 'overcast', 'overcast', 'rain', 'sunny', 'overcast', 'rain', 'sunny', 'sunny', 'rain', 'overcast', 'rain', 'sunny', 'overcast', 'sunny', 'overcast', 'rain', 'overcast'],
'Temperature': [85.0, 80.0, 83.0, 70.0, 68.0, 65.0, 64.0, 72.0, 69.0, 75.0, 75.0, 72.0, 81.0, 71.0, 81.0, 74.0, 76.0, 78.0, 82.0, 67.0, 85.0, 73.0, 88.0, 77.0, 79.0, 80.0, 66.0, 84.0],
'Humidity': [85.0, 90.0, 78.0, 96.0, 80.0, 70.0, 65.0, 95.0, 70.0, 80.0, 70.0, 90.0, 75.0, 80.0, 88.0, 92.0, 85.0, 75.0, 92.0, 90.0, 85.0, 88.0, 65.0, 70.0, 60.0, 95.0, 70.0, 78.0],
'Wind': [False, True, False, False, False, True, True, False, False, False, True, True, False, True, True, False, False, True, False, True, True, False, True, False, False, True, False, False],
'Num_Players': [52,39,43,37,28,19,43,47,56,33,49,23,42,13,33,29,25,51,41,14,34,29,49,36,57,21,23,41]
}
df = pd.DataFrame(dataset_dict)
# One-hot encode 'Outlook' column
df = pd.get_dummies(df, columns=['Outlook'], prefix='', prefix_sep='', dtype=int)
# Convert 'Wind' column to binary
df['Wind'] = df['Wind'].astype(int)
# Split data into features and target, then into training and test sets
X, y = df.drop(columns='Num_Players'), df['Num_Players']
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.5, shuffle=False)
# Initialize and train the model
dummy_reg = DummyRegressor(strategy='median')
dummy_reg.fit(X_train, y_train)
# Make predictions
y_pred = dummy_reg.predict(X_test)
# Calculate and print RMSE
print(f"RMSE: {mean_squared_error(y_test, y_pred, squared=False)}")
进一步阅读
关于DummyRegressor及其在 scikit-learn 中的实现,读者可以参考官方文档,里面提供了关于其用法和参数的详细信息。
技术环境
本文使用的是 Python 3.7 和 scikit-learn 1.5。虽然讨论的概念普遍适用,但具体的代码实现可能会随着不同版本有所不同。
关于插图
除非另有说明,所有图片均由作者创建,结合了 Canva Pro 授权设计元素。
𝙎𝙚𝙚 𝙢𝙤𝙧𝙚 𝙍𝙚𝙜𝙧𝙚𝙨𝙨𝙞𝙤𝙣 𝘼𝙡𝙜𝙤𝙧𝙞𝙩𝙝𝙢𝙨 𝙝𝙚𝙧𝙚:

回归算法
查看列表5 个故事


𝙔𝙤𝙪 𝙢𝙞𝙜𝙝𝙩 𝙖𝙡𝙨𝙤 𝙡𝙞𝙠𝙚:

分类算法
查看列表8 个故事


双下划线方法:Python 的隐藏宝石
通过实际例子展示如何主动使用特殊方法可以简化编码并提高可读性。
·发表于 Towards Data Science ·阅读时长 8 分钟·2024 年 11 月 30 日
--
双下划线方法,尽管可能是 Python 中的基础话题,但我常常注意到,即使是一些编程已经有相当经验的人,也常常只对其有表面的理解。
免责声明: 这是一个可以原谅的疏漏,因为在大多数情况下,主动使用双下划线方法(dunder methods)“简单地”加速和规范了那些本可以用其他方式完成的任务。即使它们的使用是必不可少的,程序员们也常常未意识到自己正在编写属于双下划线方法这一广泛类别的特殊方法。
无论如何,如果你在 Python 中编程并且不熟悉这个话题,或者你恰好是像我一样对编程语言更原生的方面感兴趣的代码极客,这篇文章可能正是你在寻找的内容。
表面现象往往能欺骗你……即使是在 Python 中!
如果我在生活中学到了一件事,那就是并非一切如初看时所见,Python 也不例外。

图片由 Robert Katzki 提供,来源于 Unsplash
让我们考虑一个看似简单的例子:
class EmptyClass:
pass
这是我们在 Python 中可以定义的“最空的”自定义类,因为我们没有定义任何属性或方法。它空得让你以为根本无法做任何事情。
然而,事实并非如此。例如,Python 并不会抱怨你尝试创建这个类的实例,甚至也不会在比较两个实例是否相等时出错:
empty_instance = EmptyClass()
another_empty_instance = EmptyClass()
>>> empty_instance == another_empty_instance
False
当然,这不是魔法。简单来说,通过利用一个标准的 object 接口,Python 中的任何对象都会继承一些默认的属性和方法,这些方法允许用户始终与其进行一组最小的交互。
尽管这些方法看起来可能隐藏,但它们并不是不可见的。要访问可用的方法,包括 Python 本身分配的那些方法,只需使用内建函数 dir()。对于我们的空类,我们得到:
>>> dir(EmptyClass)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__',
'__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__str__', '__subclasshook__', '__weakref__']
正是这些方法能够解释我们之前观察到的行为。例如,由于该类实际上具有 init 方法,因此我们不应感到惊讶,能够实例化该类的对象。
认识 dunder 方法
上一个输出中显示的所有方法都属于特殊的——猜猜看——dunder 方法。术语“dunder”是“double underscore”(双下划线)的缩写,指的是这些方法名称前后各有的双下划线。
它们之所以特殊,有几个原因:
-
它们被内建到每个对象中:每个 Python 对象都配备了一组特定的 dunder 方法,这些方法由其类型决定。
-
它们是隐式调用的:许多 dunder 方法会通过与 Python 的原生运算符或内建函数的交互自动触发。例如,用 == 比较两个对象等同于调用它们的 eq 方法。
-
它们是可定制的:你可以重写现有的 dunder 方法,或者为你的类定义新的方法,以便为它们提供自定义行为,同时保持它们的隐式调用。
对于大多数 Python 开发者来说,遇到的第一个 dunder 方法是 init,即构造方法。这个方法在你创建类的实例时自动调用,使用熟悉的语法 *MyClass(args, kwargs) 来简化显式调用 *MyClass.init(args, kwargs)。
尽管是最常用的,init 也是最具专业性的 dunder 方法之一。它并未完全展示 dunder 方法的灵活性和强大功能,因为 dunder 方法可以让你重新定义对象与 Python 本身功能的交互方式。
使对象更美观
让我们定义一个表示商店中待售物品的类,并通过指定名称和价格来创建一个实例。
class Item:
def __init__(self, name: str, price: float) -> None:
self.name = name
self.price = price
item = Item(name="Milk (1L)", price=0.99)
如果我们尝试显示 item 变量的内容,会发生什么?现在,Python 能做到的最好效果就是告诉我们它是什么类型的对象以及它在内存中的位置:
>>> item
<__main__.Item at 0x00000226C614E870>
让我们尝试获取更具信息性和更漂亮的输出!

图片由 Shamblen Studios 提供,来源于 Unsplash
为了做到这一点,我们可以重写 repr dunder 方法,其输出将正是当在交互式 Python 控制台中输入类实例时打印的内容,但同样——一旦其他 dunder 方法 str 未被重写——当调用 print() 时也会发生同样的情况。
注意:通常的做法是让repr提供必要的语法来重新创建打印的实例。因此,在后者的情况下,我们期望输出为Item(name="Milk (1L)", price=0.99)。
class Item:
def __init__(self, name: str, price: float) -> None:
self.name = name
self.price = price
def __repr__(self) -> str:
return f"{self.__class__.__name__}('{self.name}', {self.price})"
item = Item(name="Milk (1L)", price=0.99)
>>> item # In this example it is equivalent also to the command: print(item)
Item('Milk (1L)', 0.99)
没什么特别的,对吧?你说得对:我们本可以实现相同的方法并将其命名为my_custom_repr,而无需使用双下划线方法。然而,虽然每个人都能立刻理解我们通过print(item)或仅仅item表示的意思,但我们能否说出类似item.my_custom_repr()的东西呢?
定义对象与 Python 原生运算符之间的交互
假设我们想要创建一个新的类Grocery,它允许我们构建一个Item的集合以及它们的数量。
在这种情况下,我们可以使用双下划线方法来允许一些标准操作,如:
-
使用+运算符向Grocery中添加特定数量的Item
-
使用for循环直接迭代Grocery类
-
使用括号[]符号从 Grocery 类中访问特定的 Item
为了实现这一点,我们将定义(我们已经看到,通用类默认没有这些方法)双下划线方法add、iter和getitem。
from typing import Optional, Iterator
from typing_extensions import Self
class Grocery:
def __init__(self, items: Optional[dict[Item, int]] = None):
self.items = items or dict()
def __add__(self, new_items: dict[Item, int]) -> Self:
new_grocery = Grocery(items=self.items)
for new_item, quantity in new_items.items():
if new_item in new_grocery.items:
new_grocery.items[new_item] += quantity
else:
new_grocery.items[new_item] = quantity
return new_grocery
def __iter__(self) -> Iterator[Item]:
return iter(self.items)
def __getitem__(self, item: Item) -> int:
if self.items.get(item):
return self.items.get(item)
else:
raise KeyError(f"Item {item} not in the grocery")
让我们初始化一个Grocery实例并打印其主要属性items的内容。
item = Item(name="Milk (1L)", price=0.99)
grocery = Grocery(items={item: 3})
>>> print(grocery.items)
{Item('Milk (1L)', 0.99): 3}
然后,我们使用+运算符添加一个新的 Item,并验证更改已生效。
new_item = Item(name="Soy Sauce (0.375L)", price=1.99)
grocery = grocery + {new_item: 1} + {item: 2}
>>> print(grocery.items)
{Item('Milk (1L)', 0.99): 5, Item('Soy Sauce (0.375L)', 1.99): 1}
友好且明确,对吧?
iter方法允许我们根据方法中实现的逻辑循环遍历Grocery对象(即,隐式地,循环将迭代包含在可迭代属性items中的元素)。
>>> print([item for item in grocery])
[Item('Milk (1L)', 0.99), Item('Soy Sauce (0.375L)', 1.99)]
同样,访问元素是通过定义getitem双下划线方法来处理的:
>>> grocery[new_item]
1
fake_item = Item("Creamy Cheese (500g)", 2.99)
>>> grocery[fake_item]
KeyError: "Item Item('Creamy Cheese (500g)', 2.99) not in the grocery"
本质上,我们为 Grocery 类分配了一些标准的字典般行为,同时还允许了一些此数据类型本不具备的操作。
增强功能:使类可调用以简化和增强功能。
让我们通过一个最终示例来总结这个深入探讨的双下划线方法,展示它们如何成为我们工具库中的强大工具。

图片由Marek Studzinski提供,来源于Unsplash
假设我们实现了一个根据特定输入执行确定性和缓慢计算的函数。为了简化,我们将使用一个带有内置time.sleep几秒钟的恒等函数作为示例。
import time
def expensive_function(input):
time.sleep(5)
return input
如果我们对相同的输入运行两次该函数会发生什么?好吧,现在计算会执行两次,这意味着我们要等待整个执行时间(即总共 10 秒)才能得到两次相同的输出。
start_time = time.time()
>>> print(expensive_function(2))
>>> print(expensive_function(2))
>>> print(f"Time for computation: {round(time.time()-start_time, 1)} seconds")
2
2
Time for computation: 10.0 seconds
这样说有道理吗?为什么我们要对相同的输入执行相同的计算(并得到相同的输出),尤其是当它是一个缓慢的过程时?
一种可能的解决方案是将此函数的执行“包装”在类的 call 双下划线方法中。
这使得类的实例可以像函数一样调用——意味着我们可以使用简单的语法 *my_class_instance(args, kwargs) ——同时也允许我们使用属性作为缓存,从而减少计算时间。
采用这种方法,我们还可以灵活地创建多个进程(即类实例),每个进程都有自己的本地缓存。
class CachedExpensiveFunction:
def __init__(self) -> None:
self.cache = dict()
def __call__(self, input):
if input not in self.cache:
output = expensive_function(input=input)
self.cache[input] = output
return output
else:
return self.cache.get(input)
start_time = time.time()
cached_exp_func = CachedExpensiveFunction()
>>> print(cached_exp_func(2))
>>> print(cached_exp_func(2))
>>> print(f"Time for computation: {round(time.time()-start_time, 1)} seconds")
2
2
Time for computation: 5.0 seconds
正如预期的那样,函数在第一次运行后被缓存,消除了第二次计算的需求,从而将整体时间减少了一半。
如前所述,我们甚至可以在需要时创建该类的独立实例,每个实例都有自己的缓存。
start_time = time.time()
another_cached_exp_func = CachedExpensiveFunction()
>>> print(cached_exp_func(3))
>>> print(another_cached_exp_func (3))
>>> print(f"Time for computation: {round(time.time()-start_time, 1)} seconds")
3
3
Time for computation: 10.0 seconds
我们到了!这是一个简单而强大的优化技巧,通过双下划线方法(dunder methods)实现,不仅减少了冗余计算,还通过允许本地实例特定的缓存提供了灵活性。
我的最终思考
双下划线方法是一个广泛且不断发展的主题,这篇文章并不旨在成为该主题的详尽资料(如果需要,您可以参考3. 数据模型 — Python 3.12.3 文档)。
我在这里的目标是清晰地解释它们是什么,以及如何有效地使用它们来处理一些常见的使用场景。
虽然它们对于所有程序员来说并非总是必需的,但一旦我掌握了它们的工作原理,它们对我产生了极大的帮助,并且希望它们也能对你有所帮助。
双下划线方法的确是一种避免重新发明轮子的方式。它们也与 Python 的哲学紧密契合,从而导致更简洁、可读且符合约定的代码。而这一点永远不会有坏处,对吧?
沙丘——一个隐藏的网络
·发表于Towards Data Science ·9 分钟阅读·2024 年 3 月 19 日
--
在本文中,我们与Patrik Szigeti合作,设计了一种数据和网络方法,并通过图形可视化来概述原始《沙丘》三部曲背后的复杂社交网络。
随着 2021 年《沙丘》在票房和评论界的成功,2024 年《沙丘:第二部》成为最受期待的电影之一,并没有让人失望。根据本文写作时的情况,它的票房收入不断攀升,并且在烂番茄和 IMDb 上的评分都超过了前作。凭借其不断变化的政治格局,《沙丘》是一个通过网络科学深入挖掘的完美系列。在这篇简短的文章中,我们旨在基于弗兰克·赫伯特的前三本书——《沙丘》(1965)、《沙丘救世主》(1969)和《沙丘之子》(1976),探索帝国不同家族和人物之间的联系。
在本文的第一部分,我们介绍了一种基于 Python 的方法,用于从Dune Wiki收集角色档案数据,并将这些档案转化为一个引人注目的网络图。然后,在第二部分——这一部分包含大量剧透——我们深入探讨了网络的深度,提取了与《沙丘》第一部三部曲相关的所有故事。
所有图片均由作者制作。
1 构建网络
首先,我们使用 Python 收集了沙丘角色的完整列表。然后,我们从每个角色的粉丝维基站点下载他们的个人简介,并统计每个角色的故事中提到其他角色故事的次数,假设这些提及编码了任意两个角色之间的互动。接着,我们将使用网络科学将这些关系转化为复杂的图形。
1.1 收集角色列表
首先,我们从《沙丘》粉丝维基网站收集了所有相关角色的列表。具体来说,我们使用 urllib 和 bs4 提取了每个被提及且拥有自己维基页面的角色的名称和粉丝维基 ID。我们为前三本书做了这些:沙丘、沙丘救世主 和 沙丘之子。这三本书涵盖了阿特雷 ides 家族的崛起。
来源:
首先,下载角色列表站点的 HTML 文件:
dune_meta = {
'Dune': {'url': 'https://dune.fandom.com/wiki/Dune_(novel)'},
'Dune Messiah': {'url': 'https://dune.fandom.com/wiki/Dune_Messiah'},
'Children of Dune': {'url': 'https://dune.fandom.com/wiki/Children_of_Dune_(novel)'}
}
for book, url in dune_meta.items():
sauce = urlopen(url['url']).read()
soup = bs.BeautifulSoup(sauce,'lxml')
dune_meta[book]['chars'] = soup.find_all('li')
稍微手动调整角色名称和 ID:
dune_meta['Dune']['char_start'] = 'Abulurd'
dune_meta['Dune']['char_end'] = 'Arrakis'
dune_meta['Dune Messiah']['char_start'] = 'Abumojandis'
dune_meta['Dune Messiah']['char_end'] = 'Arrakis'
dune_meta['Children of Dune']['char_start'] = '2018 Edition'
dune_meta['Children of Dune']['char_end'] = 'Categories'
然后,我们提取了所有可能相关的名称和对应的个人资料 URL。在这里,我们手动检查了角色名称开始的标签块(例如,与角色列表站点的大纲不同)。此外,我们决定放弃标记为‘XD’和‘DE’的角色,它们属于扩展系列,以及那些在某本书中仅“提及”的角色:
for k, v in dune_meta.items():
names_urls = {}
keep_row = False
print(f'----- {k} -----')
for char in v['chars']:
if v['char_start'] in char.text.strip():
keep_row = True
if v['char_end'] in char.text.strip():
keep_row = False
if keep_row and 'Video' not in char.text:
try:
url = 'https://dune.fandom.com' + str(char).split('href="')[1].split('" title')[0]
name = char.text.strip()
if 'wiki' in url and 'XD' not in name and 'DE' not in name and '(Mentioned only)' not in name:
names_urls[name] = url
print(name)
except:
pass
dune_meta[k]['names_urls'] = names_urls
这个代码块将输出角色列表,如:

提取名称的示例。
最后,我们检查收集到的角色数量,并保存它们的个人资料 URL 和标识符,以便下一个小节使用。
dune_names_urls = {}
for k, v in dune_meta.items():
dune_names_urls.update(dune_meta[k]['names_urls'])
names_ids = {n : u.split('/')[-1] for n, u in dune_names_urls.items()}
print(len(dune_names_urls))
该单元输出的结果,显示了 119 个角色及其个人资料 URL:

1.2 下载角色资料
我们的目标是绘制沙丘角色的社交网络——这意味着我们需要弄清楚谁和谁互动。在前一小节中,我们获得了所有“谁”的列表,现在我们将获取有关他们个人故事的信息。我们将通过再次使用简单的网页抓取技术来获取这些故事,然后将每个角色个人站点的来源保存在本地的单独文件中:
# output folder for the profile htmls
folderout = 'fandom_profiles'
if not os.path.exists(folderout):
os.makedirs(folderout)
# crawl and save the profile htmls
for ind, (name, url) in enumerate(dune_names_urls.items()):
if not os.path.exists(folderout + '/' + name + '.html'):
try:
fout = open(folderout + '/' + name + '.html', "w")
fout.write(str(urlopen(url).read()))
except:
pass
运行此代码的结果将是我们本地目录中的一个文件夹,里面包含每个选定角色的粉丝维基站点资料。
1.3 构建网络
为了构建角色之间的网络,我们通过以下逻辑计算每个角色的维基页面源中引用其他角色维基标识符的次数。在这里,我们构建了边列表——一个包含连接的源节点和目标节点(角色),以及它们之间的权重(共同引用频率)的列表。
# extract the name mentions from the html sources
# and build the list of edges in a dictionary
edges = {}
for fn in [fn for fn in os.listdir(folderout) if '.html' in fn]:
name = fn.split('.html')[0]
with open(folderout + '/' + fn) as myfile:
text = myfile.read()
soup = bs.BeautifulSoup(text,'lxml')
text = ' '.join([str(a) for a in soup.find_all('p')[2:]])
soup = bs.BeautifulSoup(text,'lxml')
for n, i in names_ids.items():
w = text.split('Image Gallery')[0].count('/' + i)
if w>0:
edge = '\t'.join(sorted([name, n]))
if edge not in edges:
edges[edge] = w
else:
edges[edge] += w
len(edges)
一旦运行此代码块,我们将得到连接 119 个《沙丘》角色的边数,结果为 307。
接下来,我们使用 NetworkX 图分析库将边列表转换为图对象,并输出图中节点和边的数量:
# create the networkx graph from the dict of edges
import networkx as nx
G = nx.Graph()
for e, w in edges.items():
if w>0:
e1, e2 = e.split('\t')
G.add_edge(e1, e2, weight=w)
G.remove_edges_from(nx.selfloop_edges(G))
print('Number of nodes: ', G.number_of_nodes())
print('Number of edges: ', G.number_of_edges())
该代码块的结果:

节点数量仅为 72,这意味着有 47 个角色在他们的——可能相当简短——维基个人资料中没有与任何中心成员相连。此外,我们还看到边的数量减少了四个,因为移除了一些自环。
让我们使用内置的 Matplotlib 绘图工具简要查看该网络:
# take a very brief look at the network
import matplotlib.pyplot as plt
f, ax = plt.subplots(1,1,figsize=(15,15))
nx.draw(G, ax=ax, with_labels=True)
这个单元格的输出:

《沙丘》角色的初始网络可视化。
虽然这个可视化已经显示了一些网络结构,但我们通过以下代码行将图导出为 Gephi 文件,并设计了附图中的网络(关于这种网络可视化的教程将是即将发布的文章主题):
nx.write_gexf(G, 'dune_network.gexf')
完整的《沙丘》网络:

《沙丘》角色的完整网络。网络对应于网络社区(密集互联的子图),经过一些小的手动调整,而节点的大小根据它们的连接数来确定。
2 阅读网络
警告:以下段落包含《沙丘》系列前三本书的剧透。两部电影(《沙丘》和《沙丘:第二部》)基于第一本书。
我们发现保罗·阿特雷德(也被称为利桑·阿尔-盖布、穆阿德·迪布、乌苏尔、克维萨茨·哈德拉赫等)位于网络的中心也就不足为奇了。他是第一本书(和电影)的主角,是一个中心人物,最终成为帝国的皇帝。在第二本书《沙丘救世主》中,我们遇到了一个不同的保罗,一个在多年的战斗和被预知之力所诅咒后,作为一个盲眼的弗雷曼走进沙漠,献身给沙赫·胡鲁德的人。然后,他在《沙丘之子》中以传教士的身份出现,一个在沙漠中活动和传教的神秘人物,最终迎来自己的结局。在这段旅程中,他与许多其他角色交织在一起。这通过他的所谓“自我网络”完美地体现出来——即包含他所有连接以及这些连接之间的子图——这个网络包含了大约一半的节点和 64%的边。下面的图示展示了这一点。

保罗的自我网络。
当我们继续阅读网络时,我们可以看到阿特雷 ides 家族位于其中,当然,围绕着保罗的是他的家人。他的父母,杰西卡夫人,莱托·阿特雷 ides 一世的妾,以及本·杰瑟里特教会的女祭司。杰西卡是哈科宁家族弗拉基米尔·哈科宁的女儿,这为我们提供了黄色和浅蓝色节点群体之间的第一次联系。我们可以看到保罗与他的弗雷曼妾侍查尼之间有着强烈的联系,进一步连接到他们的孩子——莱托二世和加尼玛。保罗还与他的导师和好友邓肯·爱达荷和格尔尼·哈雷克有着密切的关系,以及女祭司盖尤斯·海伦·莫希姆,她在各本书中不断出现,进一步巩固了本·杰瑟里特的事业。
即使这个网络显然是以保罗为中心,我们也能看到阿科里诺家族(棕色)、哈科宁家族(浅蓝色)和弗雷曼家族(蓝色)的明显分组,但真正有趣的是,这个基于维基百科文章创建的简单网络告诉我们,在这三本书的情节展开过程中,有多少内容。
我们看到了利耶特·凯恩斯,他是弗雷曼的事实领导人和植物学家,他的梦想是看到荒芜的阿拉基斯星球变得富饶,拥有绿草如茵的牧场和充足的水源。他的女儿查尼通过斯蒂尔加与保罗的生活中重要人物、一个宗教信徒联系在一起,通过他与所有弗雷曼建立了联系。然而,在他们之间有一个人——斯卡特尔,他在穆阿迪布的圣战后通过海特(邓肯·爱达荷的高拉——一个由死者复生的人工人类)密谋摧毁王室。对于那些只看过电影的人来说,邓肯是我们网络中如此核心人物可能令人惊讶,但在担任阿特雷 ides 家族的剑术大师,并在阿拉基斯的沙漠战争中阵亡后,他以高拉的身份回归,并扮演了重要角色——娶了杰西卡夫人的女儿、保罗的妹妹阿丽亚·阿特雷 ides。
电影观众可能会对图费尔·哈瓦特作为哈科宁家族一员的设定感到惊讶。他是一个负责阿特雷 ides 家族安全的门塔特(Mentat),但在哈科宁家族取代阿特雷 ides 家族成为阿拉基斯的统治者后,他被迫为他们服务,并策划了反对他们的阴谋,尽管他的真正目标始终是复仇他心爱的公爵之死,他认为是杰西卡夫人幕后策划了这一袭击。他后来通过拒绝杀死保罗而自杀来获得救赎。
然而,这个网络最引人入胜的部分是,无论一个角色的节点看起来多么微小,这并不意味着他们在情节中没有扮演重要角色。他们可能对错误的观众说了正确的话(例如伊克斯星的布朗索声称保罗失去了他人性中的某些重要部分,才成为穆阿迪布),成为阿丽亚·阿特雷迪的情人(贾维德),或密谋杀害阿特雷迪双胞胎,莱托和甘妮玛(泰耶卡尼克)。我们可以一直列举下去,这些只是弗兰克·赫伯特的《沙丘》中错综复杂的政治景观的一些迷人例子。
3 结论
本文的目标是搭建数据科学爱好者与《沙丘》爱好者之间的桥梁——并可能娱乐已经存在的这两个社区的交集部分。首先,我们展示了一个相对通用的 Python 框架,使我们能够绘制出任何我们遇到的粉丝维基网站的社交网络。其次,我们还详细解释了这些网络可视化如何展现整个故事的发展——一幅图胜过千言万语,甚至更多——一部三部曲。
使用 GenAI 进行重复检测
如何利用 LLM 和 GenAI 技术改进去重
·发布于 Towards Data Science ·阅读时间 5 分钟·2024 年 7 月 1 日
--

2D UMAP Musicbrainz 200K 最近邻图
客户数据通常作为记录存储在客户关系管理系统(CRM)中。数据由一个或多个用户随着时间的推移手动输入到这些系统中,导致数据复制、部分重复或模糊重复。这意味着对于客户、联系人、账户等,已经不再有一个单一的真实数据来源。没有唯一的 CRM 记录与目标客户之间的映射,下游的业务流程会变得越来越复杂和难以操作。目前用于检测和去重记录的方法使用的是传统的自然语言处理技术,称为实体匹配(Entity Matching)。但可以利用大型语言模型(LLM)和生成式 AI 的最新进展,显著改善重复记录的识别和修复。在常见的基准数据集上,我发现我的方法使数据去重准确率从传统 NLP 技术的 30%提升到接近 60%。
我希望在这里解释这一技术,期望其他人能发现它的帮助并用它来解决他们自己的去重需求。它对其他场景也很有用,比如你希望识别重复记录,而不仅仅是客户数据。我还撰写并发布了一篇关于这个话题的研究论文,如果你想深入了解,可以在 Arxiv 上查看:
客户数据通常作为记录存储在客户关系管理系统(CRM)中。通过手动输入的数据…
传统方法
识别重复记录的任务通常通过逐对记录比较来完成,称为“实体匹配”(EM)。这一过程的典型步骤如下:
-
数据准备
-
候选生成
-
阻塞
-
匹配
-
聚类
数据准备
数据准备是对数据进行清理,包括去除非 ASCII 字符、大小写转换和分词等。这是一个重要且必要的步骤,为后续的 NLP 匹配算法提供支持,因为这些算法处理不同的大小写或非 ASCII 字符时效果不好。
候选生成
在常规的 EM 方法中,我们会通过将表中的所有记录与自身结合,产生一个笛卡尔积。然后会去除所有与自身的组合。对于许多 NLP 匹配算法来说,将行 A 与行 B 进行比较等同于将行 B 与行 A 进行比较。在这种情况下,可以只保留其中一对。但是,即便如此,仍然会剩下很多候选记录。为了减少这个数量,通常会使用一种叫做“阻塞”的技术。
阻塞
阻塞的思想是排除那些我们知道不可能是彼此重复的记录,因为它们在“阻塞”列上具有不同的值。例如,如果我们考虑的是客户记录,那么一个潜在的阻塞列可能是“城市”。这是因为我们知道即使记录的其他所有细节足够相似,如果它们位于不同的城市,也不能是同一个客户。生成候选记录后,我们使用阻塞来排除那些在阻塞列上有不同值的记录。
匹配
在阻塞之后,我们现在检查所有候选记录,并使用来自两行的字段计算传统的基于相似性的 NLP 属性值度量。利用这些度量,我们可以确定是否存在潜在的匹配或不匹配。
聚类
现在我们已经有了一个匹配的候选记录列表,我们可以将它们分组到不同的簇中。
提议的方法
提议的方法有几个步骤,但最重要的一点是,我们不再需要执行传统方法中的“数据准备”或“候选生成”步骤。新的步骤变为:
-
创建匹配句子
-
创建匹配句子的嵌入向量
-
聚类
创建匹配句子
首先,通过将我们感兴趣的属性连接起来,并用空格分隔它们,创建一个“匹配句子”。举个例子,假设我们有一个客户记录,格式如下:
我们将通过将 name1、name2、name3、地址和城市属性用空格连接来创建一个“匹配句子”,得到如下内容:
“John Hartley Smith 20 Main Street London”
创建嵌入向量
一旦我们的“匹配句子”被创建,它就会通过我们选择的嵌入模型编码到向量空间中。这是通过使用“Sentence Transformers”实现的。这个编码的输出将是一个具有预定义维度的浮点向量。这些维度与所使用的嵌入模型相关。我使用了 all-mpnet-base-v2 嵌入模型,它的向量空间有 768 个维度。这个嵌入向量随后会附加到记录中。这对于所有记录都执行此操作。
聚类
一旦为所有记录计算了嵌入向量,下一步就是创建相似记录的簇。为此,我使用了 DBSCAN 技术。DBSCAN 的工作原理是首先选择一个随机记录,并使用距离度量找到与其相近的记录。我发现有 2 种不同的距离度量方法有效:
-
L2 范数距离
-
余弦相似度
对于每一个度量标准,你选择一个 epsilon 值作为阈值。所有在 epsilon 距离内且“blocked”列值相同的记录将被加入到这个簇中。一旦这个簇完成,另一个未访问的随机记录将被选中,并围绕它创建一个新的簇。这个过程将一直持续,直到所有记录都被访问过。
实验和结果
我在工作中使用这种方法识别客户数据中的重复记录,产生了一些非常不错的匹配。为了更加客观,我还使用了一个名为“Musicbrainz 200K”的基准数据集进行了实验。它产生了一些可量化的结果,相比标准的 NLP 技术有所改进。
可视化聚类
我为 Musicbrainz 200K 数据集生成了最近邻簇图,并使用 UMAP 降维算法将其渲染为 2D 图:

2D UMAP Musicbrainz 200K 最近邻图
资源
我创建了各种笔记本,帮助你们自己尝试这个方法:
[## GitHub - ianormy/genai_duplicate_detection_paper: 伴随论文的资源和笔记本…
伴随《GenAI 重复检测》论文的资源和笔记本…
-
GenAI 重复检测论文: [2406.15483] Duplicate Detection with GenAI
-
GitHub 资源:
github.com/ianormy/genai_duplicate_detection_paper -
all-mpnet-base-v2 嵌入模型:
huggingface.co/sentence-transformers/all-mpnet-base-v2/ -
UMAP Python 包:
pypi.org/project/umap-learn/ -
实体解析基准数据集:
dbs.uni-leipzig.de/research/projects/benchmark-datasets-for-entity-resolution/ -
Musicbrainz 200K 数据集:
dbs.uni-leipzig.de/files/datasets/saeedi/musicbrainz-200-A01.csv.dapo
动态执行
让你的 AI 任务区分难题与易题
·发布于 Towards Data Science ·10 分钟阅读·2024 年 11 月 3 日
--
在这篇立场论文中,我讨论了这样一个前提:大量的潜在性能提升机会被忽视了,因为我们往往没有关注动态执行的潜力。
我想我需要首先定义在这个语境下什么是动态执行。正如你们很多人无疑已经意识到的那样,我们常常通过仔细审视模型本身,来优化性能,看看有什么方法能使得该模型的处理更加高效(这可以通过降低延迟、提高吞吐量和/或节省能源来衡量)。
这些方法通常关注模型的大小,因此我们寻找压缩模型的方法。如果模型更小,那么内存占用和带宽需求就会得到改善。一些方法还针对模型内部的稀疏性进行优化,从而避免无关的计算。
但是…我们现在仅仅是在看模型本身。
这绝对是我们想做的事情,但是否有其他的机会可以利用,进一步提升性能?我们经常忽视那些不专注于模型大小的、最具人类直觉的方法。

图 1. 难题与易题的直觉区别
难题与易题
在图 1 中,有一个简单的例子(可能有点过于简化),展示了如何对红色和蓝色数据点进行分类。能够绘制决策边界会非常有用,这样我们就能尽可能地让红色和蓝色数据点位于边界的两侧。一种方法是进行线性回归,在尽可能的情况下拟合一条直线,以便尽可能地将数据点分开。图 1 中的粗黑线代表一个潜在的边界。仅关注粗黑线,你可以看到有相当数量的点落在边界的错误一侧,但大多数时候它的表现还是相当不错的。
如果我们关注曲线,这样的表现要好得多,但计算上也更困难,因为它不再是一个简单的线性方程。如果我们想要更高的精确度,显然曲线比黑线更适合作为决策边界。
但是我们暂时不要完全舍弃黑线。现在让我们看看黑色边界两侧的绿色平行线。请注意,线性决策边界对于绿色线外的点非常准确。我们把这些点称为“简单”。
实际上,对于“简单”点,它的准确性和曲线边界是 100%一致的。位于绿色线内的点是“困难”的,使用更复杂的决策边界处理这些点显然有优势。
所以……如果我们能够判断输入数据是简单还是复杂,我们可以采用不同的方法来解决问题,在不失去准确度的前提下,对简单的数据点节省计算资源。
这是非常直观的,因为这正是人类解决问题的方式。如果我们认为一个问题简单,我们通常不会想太多,迅速给出答案;而如果我们认为一个问题很困难,我们会思考得更多,通常也需要更多的时间来得出答案。
那么,我们能否将类似的方法应用到人工智能中呢?
动态执行方法
在动态执行场景中,我们采用一套专门的技术,旨在仔细审查当前的查询。这些技术涉及对查询的结构、内容和上下文进行彻底检查,目的是判断它所代表的问题是否可以通过更简单的方式来解决。
这种方法类似于人类解决问题的方式。就像我们人类经常能够识别出“简单”或“易懂”的问题,并比“困难”或“复杂”的问题用更少的精力解决它们一样,这些技术也力图做到这一点。它们旨在识别出更简单的问题,并更高效地解决,从而节省计算资源和时间。
这就是我们将这些技术称为动态执行的原因。术语“动态”表示这种方法的适应性和灵活性。与静态方法不同,静态方法无论问题的性质如何,都会严格遵循预定的路径,而动态执行则根据它遇到的具体问题调整策略,也就是说,机会是数据依赖的。
动态执行的目标不是优化模型本身,而是优化计算流程。换句话说,它旨在通过调整模型与数据的交互过程来简化流程。通过将计算流程调整为适应提供给模型的数据,动态执行确保模型的计算资源以最有效的方式得到利用。
本质上,动态执行旨在通过将策略适应于当前问题,使问题解决过程尽可能高效和有效,类似于人类解决问题的方式。它关注的是更聪明地工作,而不是更努力地工作。这种方法不仅节省了计算资源,还提高了问题解决过程的速度和准确性。
早期退出
该技术涉及在深度神经网络(DNN)的各个阶段添加退出点。其想法是让网络在处理简单任务时能够更早终止推理过程,从而节省计算资源。它利用了这样的观察:某些测试示例比其他示例更容易预测[1],[2]。
下面是早期退出策略在多个编码器模型中的应用示例,包括 BERT、ROBERTA 和 ALBERT。
我们在不同的熵阈值下测量了粘合分数的加速情况。图 2 显示了这些分数的变化以及它们如何随着熵阈值的变化而下降。分数表示基准分数的百分比(即没有使用早期退出的情况)。注意,我们可以在不牺牲太多质量的情况下实现 2 倍到 4 倍的加速。

图 2. 早期退出:SST-2
推测采样
该方法通过从一个较小的草稿模型中计算多个候选令牌来加速推理过程。这些候选令牌随后在完整的目标模型中并行评估[3],[4]。
推测采样是一种旨在加速大规模语言模型解码过程的技术[5],[6]。推测采样背后的概念基于这样的观察:通过一个更快但更不强大的草稿模型生成的短文本续写的并行评分延迟,和从更大的目标模型中采样单个令牌的延迟是相当的。这种方法允许从每次变换器调用中生成多个令牌,从而加速了解码过程。
推测采样过程涉及两个模型:一个较小、较快的草稿模型和一个较大、较慢的目标模型。草稿模型推测输出结果的未来若干步,而目标模型则确定我们应接受多少个这些推测的标记。草稿模型以常规自回归的方式解码若干个标记,并比较目标模型和草稿模型在新预测序列上的概率输出。根据一些拒绝标准,决定我们要保留多少个推测的标记。如果某个标记被拒绝,则通过两种分布的组合重新采样该标记,且不再接受更多标记。如果所有推测的标记都被接受,则可以从目标模型的概率输出中额外采样一个最终标记。
在性能提升方面,推测采样显示出了显著的改进。例如,它与一个 70 亿参数的语言模型 Chinchilla 进行基准测试,在分布式设置中实现了 2–2.5 倍的解码加速,而不影响样本质量,也不对模型本身进行修改。另一个例子是推测解码应用于 Whisper,一个通用语音转录模型,结果使推理吞吐量提高了 2 倍[7],[8]。需要注意的是,推测采样可以用来提升 CPU 推理性能,但这种提升通常较小(通常在 1.5 倍左右)。
总之,推测采样是一种有前景的技术,它利用草稿模型和目标模型的优势,加速大规模语言模型的解码过程。它提供了显著的性能提升,使其成为自然语言处理领域中的一项有价值的工具。然而,需要注意的是,实际的性能提升可能会根据使用的具体模型和设置而有所不同。
StepSaver
这是一种也可以称为“扩散生成的早期停止”方法,使用一个创新的 NLP 模型,特别针对确定任何给定文本提示所需的最小去噪步数进行微调。该高级模型作为一个实时工具,推荐生成高质量图像所需的理想去噪步数,从而高效地生成图像。它与扩散模型无缝配合,确保在最短的时间内生成优质图像[9]。
扩散模型通过迭代增强随机噪声信号,直到它与目标数据分布高度相似[10]。在生成视觉内容(如图像或视频)时,扩散模型展现出了显著的逼真性[11]。例如,视频扩散模型和 SinFusion 代表了扩散模型在视频合成中的应用实例[12][13]。最近,像 OpenAI 的 Sora 这样的模型也引起了越来越多的关注;然而,由于其专有性质,该模型目前尚未公开。
扩散模型中的性能涉及大量迭代,以从高斯噪声中恢复图像或视频[14]。这个过程称为去噪,并且是在特定数量的去噪迭代下进行训练的。该采样过程中的迭代次数是生成数据质量的一个关键因素,通过诸如 FID 等指标来衡量。
潜在空间扩散推理使用特征空间中的迭代,且由于输出质量所需的众多迭代,其性能会受到影响。各种技术,如补丁变换和基于变换器的扩散模型[15],提高了每次迭代的效率。
StepSaver 动态推荐显著较低的去噪步骤,这对于解决稳定扩散模型在图像生成过程中出现的慢采样问题至关重要[9]。推荐的步骤还确保了更好的图像质量。图 3 显示,使用动态步骤生成的图像相比于静态 100 步骤,带来了 3 倍的吞吐量提升,并且图像质量相似。

图 3. StepSaver 性能
LLM 路由
动态执行不仅限于优化特定任务(例如生成文本序列)。我们可以超越 LLM 层级,查看整个管道。假设我们在数据中心运行一个巨大的 LLM(或通过 OpenAI 的 API 为代币生成付费),我们能否优化对 LLM 的调用,以便选择最适合的 LLM(“最佳”可能是代币生成成本的函数)。复杂的提示词可能需要更昂贵的 LLM,但许多提示词可以在更简单的 LLM 上处理,甚至可以在你的笔记本上本地处理。因此,如果我们能够将提示词路由到合适的目标位置,那么我们就可以基于多个标准优化我们的任务。
路由是一种分类形式,其中提示词用于确定最佳模型。然后,提示词被路由到该模型。所谓的“最佳”可以通过不同的标准来确定在成本和准确性方面最有效的模型。在许多方面,路由是一种在管道级别执行的动态执行方式,其中我们在本文中关注的许多优化工作正是为了提高每个 LLM 的效率。例如,RouteLLM 是一个开源框架,用于服务 LLM 路由器,并提供了多个机制供参考,例如矩阵分解[16]。在这项研究中,LMSys 的研究人员能够在保持 95% 准确率的同时节省 85% 的成本。
结论
这显然不是对所有动态执行方法的详尽研究,但它应该为数据科学家和工程师提供动机,从数据的特性中寻找额外的性能提升和成本节约,而不仅仅是专注于基于模型的方法。动态执行提供了这种机会,并且不会干扰或妨碍传统基于模型的优化工作。
除非另有注明,所有图片均由作者提供。
[1] K. Liao, Y. Zhang, X. Ren, Q. Su, X. Sun, 和 B. He, “一种用于加速预训练语言模型推理的全球性过去-未来早期退出方法,” 载于《北美计算语言学学会会议:人类语言技术》,第 2013–2023 页,计算语言学协会(ACL),2021 年 6 月。
[2] F. Ilhan, K.-H. Chow, S. Hu, T. Huang, S. Tekin, W. Wei, Y. Wu, M. Lee, R. Kompella, H. Latapie, G. Liu, 和 L. Liu, “基于 EENet 的自适应深度神经网络推理优化,” 2023 年 12 月。arXiv:2301.07099 [cs]。
[3] Y. Leviathan, M. Kalman, 和 Y. Matias, “通过投机解码实现 Transformer 快速推理,” 2023 年 5 月。arXiv:2211.17192 [cs]。
[4] H. Barad, E. Aidova, 和 Y. Gorbachev, “结合投机采样和 KV-缓存优化共同推动基于 OpenVINO 的生成式 AI 应用,” 2023 年 11 月。arXiv:2311.04951 [cs]。
[5] C. Chen, S. Borgeaud, G. Irving, J.-B. Lespiau, L. Sifre, 和 J. Jumper, “通过投机采样加速大规模语言模型解码,” 2023 年 2 月。arXiv:2302.01318 [cs] 版本:1。
[6] J. Mody, “投机采样,” 2023 年 2 月。
[7] J. Gante, “辅助生成:迈向低延迟文本生成的新方向,” 2023 年 5 月。
[8] S. Gandhi, “用于 2 倍加速 Whisper 推理的投机解码。”
[9] J. Yu 和 H. Barad, “Step Saver:预测扩散模型图像生成的最小去噪步骤,” 2024 年 8 月。arXiv:2408.02054 [cs]。
[10] Notomoro, “扩散模型:带示例的全面指南,” 2024 年 2 月。章节:人工智能。
[11] T. H¨oppe, A. Mehrjou, S. Bauer, D. Nielsen, 和 A. Dittadi, “用于视频预测和填充的扩散模型,” 2022 年 11 月。arXiv:2206.07696 [cs, stat]。
[12] J. Ho, T. Salimans, A. Gritsenko, W. Chan, M. Norouzi, 和 D. J. Fleet, “视频扩散模型,” 2022 年 6 月。arXiv:2204.03458 [cs]。
[13] Y. Nikankin, N. Haim, 和 M. Irani, “SinFusion:在单一图像或视频上训练扩散模型,” 2023 年 6 月。arXiv:2211.11743 [cs]。
[14] Z. Chen, Y. Zhang, D. Liu, B. Xia, J. Gu, L. Kong, 和 X. Yuan, “用于真实图像去模糊的层次集成扩散模型,” 2023 年 9 月。arXiv:2305.12966 [cs]。
[15] W. Peebles 和 S. Xie, “基于 Transformer 的可扩展扩散模型,” 2023 年 3 月。arXiv:2212.09748 [cs]。
[16] I. Ong, A. Almahairi, V. Wu, W.-L. Chiang, T. Wu, J. E. Gonzalez, M. W. Kadous, 和 I. Stoica, “RouteLLM:通过偏好数据学习路由 LLM,” 2024 年 7 月。arXiv:2406.18665 [cs]。
动态 GitHub Pages - 面板 (pyodide-worker)
如何创建交互式且客户端运行的 GitHub Pages?这是一个雄心勃勃的构建大厦的第一步
·发布于 Towards Data Science ·阅读时长 10 分钟·2024 年 10 月 4 日
--

摄影师:Joshua Sortino 于 Unsplash 供图
索引:
-
引言
-
方法
-
结果
-
讨论
1. 引言
这些年来,我一直梦想着拥有一个漂亮的作品集,展示我作为一名初学数据科学家的项目。在经过将近一年的反思、试验、失败和一些成功之后,我在 GitHub Pages 上创建了我的第一个作品集。为这个个人成就感到高兴后,我写了一篇文章来与社区分享我研究的成果,文章可以在这里找到。
这个作品集是使用 mkdocs Python 包创建的。Mkdocs 是一个非常适合此类项目的工具包,但也有一些不足之处,主要是完全缺乏与读者的互动。随着我在创建作品集的过程中深入,缺乏互动的情况让我感到越来越沮丧。当时我的限制(至今依然如此)是要求一切都能够免费执行并且在客户端进行,因此 GitHub Pages 解决方案完全符合我的需求。
随着我在静态作品集创建过程中的深入,拥有一个动态作品集系统的想法越来越浮现在我脑海中。我的目标很明确:寻找一种解决方案,创建一个可以与读者互动的 GitHub Pages 作品集。在我的研究中,我几乎没有找到处理这个问题的相关文章,于是我开始寻找相关的软件、包和代码片段来解决这个问题。
本文的研究问题是:如何创建一个动态的全客户端网站?我的技术限制如下:使用 GitHub Pages。
关于仪表板包,我选择将自己局限于 holoviz 套件中的 Panel,因为它是一个很棒的包,我希望能在这个包上提升自己的技能。
就本文而言,我搜索并找到了许多或多或少相似的解决方案。因此,本文是一个系列文章的第一篇,旨在展示针对同一研究问题的不同解决方案。
但是,动态 GitHub 页面有什么意义呢?GitHub Pages 是一个非常有趣的解决方案,用于组织/项目展示,100%由 GitHub 托管,免费,配置最小且无需服务器维护。能够包含动态内容是一种强有力的方式来展示你的组织或项目。对于数据专业人士来说,这是一个非常有用的解决方案,能够快速生成动态且有趣的作品集。
Holoviz 是一个令人兴奋且极其丰富的包集合。它是一个完整的可视化和仪表板解决方案,对于中等规模和大数据非常强大。这个解决方案支持所有主要的输入数据处理包(polars、pandas、dask、X-ray 等),并提供了高层次的语法,能够以最少的代码生成交互式可视化。该包还允许你自定义输出,特别是可以选择你的可视化后端,例如 pandas(如果你想了解更多,我写过一篇文章)。要了解更多关于这个强大包集合的信息,我建议阅读这篇文章。
2. 方法
对于这项工作,我的技术背景带来了一些应急情况:
-
我目前还不能够熟练地使用 JavaScript 编写完整的脚本并直接用 JavaScript 写代码,
-
我将使用 Panel 作为仪表板包,以提高我的技能。如果有需要,我不会排除使用其他仪表板包(例如 Dash、streamlit、NiceGUI 等)重复练习。不过,这不是我的优先事项。
对于本文,我的技术环境如下:
- python 包:Panel
我使用 conda 和 VSCode 进行脚本和环境管理。如果你使用其他解决方案,不用担心,这不会影响其余部分。
在我的研究过程中,我从我的研究中找出了 3 个不同复杂度和视觉吸引力的脚本,这些将作为很好的测试标准:
- 一个简单的应用叫做“简单应用”:
import panel as pn
pn.extension(design="material")
slider = pn.widgets.IntSlider(name="Select a value", value=10, start=0, end=100)
pn.Column(
"# Hello Panel + Quarto!",
pn.rx("You selected: {}").format(slider),
).servable()
- 一个更复杂的应用叫做“大应用”:
import io
import panel as pn
import pandas as pd
import hvplot.pandas
pn.extension(template='fast')
pn.state.template.title = 'hvPlot Explorer'
upload = pn.widgets.FileInput(name='Upload file', height=50)
select = pn.widgets.Select(options={
'Penguins': 'https://raw.githubusercontent.com/mwaskom/seaborn-data/master/penguins.csv',
'Diamonds': 'https://raw.githubusercontent.com/mwaskom/seaborn-data/master/diamonds.csv',
'Titanic': 'https://raw.githubusercontent.com/mwaskom/seaborn-data/master/titanic.csv',
'MPG': 'https://raw.githubusercontent.com/mwaskom/seaborn-data/master/mpg.csv'
})
def add_data(event):
b = io.BytesIO()
upload.save(b)
b.seek(0)
name = '.'.join(upload.filename.split('.')[:-1])
select.options[name] = b
select.param.trigger('options')
select.value = b
upload.param.watch(add_data, 'filename')
def explore(csv):
df = pd.read_csv(csv)
explorer = hvplot.explorer(df)
def plot_code(**kwargs):
code = f'```python\n{explorer.plot_code()}\n```py'
return pn.pane.Markdown(code, sizing_mode='stretch_width')
return pn.Column(
explorer,
'**Code**:',
pn.bind(plot_code, **explorer.param.objects())
)
widgets = pn.Column(
"Select an existing dataset or upload one of your own CSV files and start exploring your data.",
pn.Row(
select,
upload,
)
).servable()
output = pn.panel(pn.bind(explore, select)).servable()
pn.Column(widgets, output)
- 一个使用“Material”Panel 模板的仪表板,我称之为“material dashboard”:
import hvplot.pandas
import numpy as np
import pandas as pd
import panel as pn
PRIMARY_COLOR = "#0072B5"
SECONDARY_COLOR = "#B54300"
CSV_FILE = (
"https://raw.githubusercontent.com/holoviz/panel/main/examples/assets/occupancy.csv"
)
pn.extension(design="material", sizing_mode="stretch_width")
@pn.cache
def get_data():
return pd.read_csv(CSV_FILE, parse_dates=["date"], index_col="date")
data = get_data()
data.tail()
def transform_data(variable, window, sigma):
"""Calculates the rolling average and identifies outliers"""
avg = data[variable].rolling(window=window).mean()
residual = data[variable] - avg
std = residual.rolling(window=window).std()
outliers = np.abs(residual) > std * sigma
return avg, avg[outliers]
def get_plot(variable="Temperature", window=30, sigma=10):
"""Plots the rolling average and the outliers"""
avg, highlight = transform_data(variable, window, sigma)
return avg.hvplot(
height=300, legend=False, color=PRIMARY_COLOR
) * highlight.hvplot.scatter(color=SECONDARY_COLOR, padding=0.1, legend=False)
get_plot(variable='Temperature', window=20, sigma=10)
variable_widget = pn.widgets.Select(name="variable", value="Temperature", options=list(data.columns))
window_widget = pn.widgets.IntSlider(name="window", value=30, start=1, end=60)
sigma_widget = pn.widgets.IntSlider(name="sigma", value=10, start=0, end=20)
bound_plot = pn.bind(
get_plot, variable=variable_widget, window=window_widget, sigma=sigma_widget
)
widgets = pn.Column(variable_widget, window_widget, sigma_widget, sizing_mode="fixed", width=300)
pn.Column(widgets, bound_plot)
pn.template.MaterialTemplate(
site="Panel",
title="Getting Started App",
sidebar=[variable_widget, window_widget, sigma_widget],
main=[bound_plot],
).servable();
为了实现同时部署 Web 和 GitHub Pages 的目标,我将测试每个部署的实现:
-
在本地 Python 服务器上,通过
python -m http.server生成, -
GitHub Pages。
可视化仪表板应具有的最佳操作
在开始测试之前,我需要有一个基准,了解每个应用程序在理想情况下应该如何工作。为此,我使用:
panel serve simple_app.py --dev
解释:
-
panel serve simple_app.py:可视化仪表板 -
— dev:每次修改底层文件时重新加载仪表板(可能需要安装一个或多个其他包,特别是用于跟踪底层文件是否已被修改)
这是预期的结果:
- 简单应用:

简单应用可视化,图片由作者提供
- 大应用:

大应用可视化,图片由作者提供
- 材料仪表板:

材料仪表板可视化,图片由作者提供
这些可视化将帮助我在部署测试期间检查一切是否顺利运行,且时间符合预期。
结果
第一步:将 Python 脚本转化为 HTML 交互式脚本
Panel 包将 Python 脚本转换成 HTML 应用程序,只需一行代码:
panel convert simple_app.py --to pyodide-worker --out docs
解释:
-
panel convert:Python 脚本转换 Panel 包命令 -
simple_app.py:待转换的 Python 脚本 -
— to pyodide-worker:Panel 可以将 Python 应用程序转化为几种支持类型,便于集成到 HTML 生产中。在本文中,我关注的是输出的pyodide-worker -
— out docs:输出文件夹,用于存放生成的两个文件(HTML 和 JavaScript)。
在 docs 文件夹(代码行中的‘ — to docs’部分),应出现两个与 Python 脚本同名、扩展名分别为‘html’和‘js’的文件。这些脚本将使我们能够将应用程序集成到 Web 内容中。这种代码转换(从 Python 到 HTML 再到 JavaScript)得益于 WebAssembly。Pyodide 是 CPython 移植到 WebAssembly/Emscripten 的版本(更多信息请见:pyodide.org/en/stable/)。
如果你不熟悉 WebAssembly,我建议你阅读 Mozilla 的文章来深入了解。我将会写一篇关于 WebAssembly 的历史、范围及其潜在影响的文章,我认为 WebAssembly 将在未来几年内带来真正的变革。
第一次测试:本地 Web 服务器部署
-
使用 Python 模拟本地 Web 服务器:
python -m http.server。此命令将返回一个本地 URL,供浏览器连接(如:127.0.0.1:8000)。 -
点击我们 Python 应用程序的 HTML 脚本
信息:在通过 HTML 服务器浏览文件时,为了在打开文件夹时自动启动所需的应用程序,HTML 和 JavaScript 文件应命名为 ‘index.html’ 和 ‘index.js’。示例:
app/
|- index.html
|- index.js
当应用文件夹在本地 HTML 服务器中打开时,index.html 会自动启动。
本地 HTML 服务器上的部署测试报告:
-
简单应用:✅
-
大型应用:✅
-
Material Dashboard:✅
在测试上述 3 个应用程序之后,这个方案在所有应用程序中都完美运行,加载和使用应用程序时没有任何速度损失。
第二次测试:GitHub Pages 部署
在本文中,我将快速回顾 GitHub Pages 在 GitHub 上的配置部分,正如我在上一篇文章中详细描述的那样。
-
第一步的警告:托管 HTML 和 JavaScript 脚本的 ‘docs’ 文件夹必须命名为 ‘docs’,并且放置在 git 仓库的根目录下。这是部署应用到 GitHub Pages 的 2 个前提条件。文件夹的名称和位置不能更改。
-
2 种可能性:
2.a. 将应用程序文件重命名为 ‘index.html’ 和 ‘index.js’,并将其直接放置在 ‘docs’ 文件夹中。此方案将直接在应用程序上打开您的 GitHub Pages。
2.b. 在 ‘docs’ 中直接创建一个 ‘index.html’ 文件,并添加指向应用程序 HTML 文件的路径。
这是我在部署测试中创建的‘index.html’文件的内容:
1\. <a href="https://petoulemonde.github.io/article_dynamic_webpages/simple_app_pyodide/simple_app.html">Simple app</a>
<br/>
2\. <a href="https://petoulemonde.github.io/article_dynamic_webpages/big_app_pyodide/big_app.html">Big app</a>
<br/>
3\. <a href="https://petoulemonde.github.io/article_dynamic_webpages/material_dashboard_pyodide/material_dashboard.html">Material dashboard</a>
说明:
-
https://petoulemonde.github.io/: 我的作品集的 URL -
article_dynamic_webpages/: 我的工作仓库,用于此文章 -
simple_app_pyodide/simple_app.html: 要打开的 HTML 文件夹/应用程序。在仓库中,该文件存储在 docs/simple_app_pyodide/simple_app.html,但在绝对路径中不要提及 ‘docs’。为什么文件浏览器和链接之间有这个差异?GitHub 是从 docs 文件夹进行部署的,‘docs’ 是其工作根目录。
3. 推送到远程仓库(在我之前的示例中,是‘article_dynamic_webpages’仓库)。
4. 在仓库中启用 GitHub 项目页面的创建。在配置页面中,以下是如何配置 GitHub 页面:

GitHub Pages 部署配置,图片由作者提供
这是‘docs’文件夹对于我们部署应用程序至关重要的地方,否则我们将无法在‘master’分支中进行任何部署操作。
测试报告:在 GitHub Pages 上部署:
-
简单应用:✅
-
大型应用:✅
-
Material dashboard: ✅
关于解决方案 2.b.:这是一个特别有趣的解决方案,因为它允许我们为网站或个人作品集创建一个静态主页,然后将其分发到特殊的动态项目页面。这为静态和动态的 GitHub Pages 打开了大门,使用 mkdocs 处理静态部分及其精美的设计,使用 Panel 处理交互式页面。我可能会将下篇文章写成关于这种 mkdocs + Panel(pyodide-worker)部署解决方案的内容,非常高兴能再次把你列为我的读者。
遇到的问题
目前测试过的仪表盘无法分发到站点/作品集的其他页面,因此识别出的唯一替代方案是创建一个静态主页,然后在站点内重新分发到仪表盘页面。是否可以在不使用静态页面的情况下创建一个包含多个页面的站点?答案是肯定的,因为仪表盘本身可以集成链接,包括指向同一站点上其他仪表盘的链接。
我已经修改了 Material 应用程序代码,添加了一个链接(添加了pn.pane.HTML(…)):
pn.template.MaterialTemplate(
site="Panel",
title="Getting Started App",
sidebar=[
pn.pane.HTML('<a href="127.0.0.1:8000/docs/big_app_pyodide/big_app.html">Big app</a>'), # New line !
variable_widget,
window_widget,
sigma_widget],
main=[bound_plot],
).servable();
这会将一个链接添加到应用程序的侧边栏:

带有链接可视化的 Material 仪表盘,图片来自作者
尽管这里的证明看起来不太完美,但它表明一个仪表盘可以集成指向其他页面的链接,因此仅使用 Panel 也可以创建一个包含多个页面的站点——太棒了!实际上,我在这里主要专注于 Panel 的仪表盘部分,但 Panel 也可以用来创建静态页面,因此即使不掌握 mkdocs,你也可以通过结合静态和动态元素来创建多个页面的站点。
讨论
Panel 是一个非常有趣且强大的工具包,它使得创建动态网站变得简单,并能够托管在 GitHub Pages 上,尤其得益于 WebAssembly 的魔力。这个工具包真正让你可以专注于创建仪表盘,然后通过几行代码将仪表盘转换为网页内容。再加上 GitHub Pages 的易用性,Panel 使得数据仪表盘的快速部署成为可能。
尽管这个解决方案很棒,但在我的测试过程中,我遇到了一些局限性。第一个问题是无法集成用户可编辑和可执行的代码。我希望能够让用户以自己的方式探索数据,通过与他们分享我编写的代码,使他们能够修改代码并以自己的方式探索数据。
第二个也是最后一个局限性是,定制仪表盘不像创建仪表盘那么容易。Hvplot 通过explorer工具提供了一种可视化的解决方案来探索数据并创建图表。但一旦进入代码中,我发现定制起来有点困难。这个工具包在功能和强大性方面非常棒,我可能仍然缺乏一些技能,因此我认为这主要是由于我对这个工具包的练习不足,而非工具包本身的问题。
如果你已经看到这里,感谢你的关注!你在我上一篇文章上的评论非常有帮助。多亏了你,我发现了 Quarto,并获得了一些关于如何让我的文章对你,读者,更加有趣的想法。请留下评论告诉我如何改进我的文章,无论是从技术上还是视觉上,这样我下次就能为你写出更加有趣的文章。
祝你在 Python 冒险中好运!
皮埃尔·埃蒂安
Python 中的动态、延迟依赖注入
自动 Python 依赖注入,使您的代码更具可测试性、解耦性、简单性和可读性
·发表在Towards Data Science·6 分钟阅读·2024 年 11 月 22 日
--

照片由Rapha Wilde / Unsplash拍摄
依赖注入(DI)通过提高可测试性、解耦性、可维护性和可读性来解决许多问题。然而,管理依赖关系有时可能会引入新问题。我们何时初始化它们?如何初始化?它们能有效地重复使用吗?
为了将 DI 提升到下一个级别,我创建了FastInject:一个简化依赖管理的 Python 包,只需几个装饰器即可。FastInject 自动处理依赖实例化和注入,让您可以专注于您的项目。特点:
-
性能提升:只在实际需要时创建依赖关系
-
更简单的初始化:依赖关系动态解析
-
避免循环导入:依赖关系推迟到运行时解析
-
灵活性提升:依赖关系可以受运行时信息或配置的影响
让我们开始编码!
内容
-
DI 刷新:使用 DI 与不使用 DI 的代码比较
-
使用 FastInject 进行依赖管理…
早停法:为什么你的机器学习模型停止训练?
为什么大多数模型较小,而大型语言模型(LLMs)较大
·发布于 Towards Data Science ·15 分钟阅读·2024 年 5 月 11 日
--

PLAIOFFS24. 图片来源:作者。
在训练监督学习模型时,早停法是一种常用的技术,用于缓解过拟合。早停法涉及在训练过程中监控模型在验证集上的表现,并在模型在这些验证数据上的表现不再提升时停止训练。此技术有助于节省计算时间和资源,同时确保模型不会学习到训练数据中的噪声和无关模式,从而避免降低模型在新数据上的泛化能力。
早停法是机器学习中广泛认可的一项技术,但关于在不同场景下满足早停条件的具体原因讨论并不多。本文从训练数据质量的角度探讨早停法的复杂性。通过分析数据质量如何影响训练停止的时机,我们可以更深入地理解其重要作用。此外,我们还将解释处理结构化数据和非结构化数据在机器学习项目中的根本区别,重点说明这些差异如何影响早停法的应用。
前提条件
赚取与学习:解决一个钓鱼启发的多臂赌博机问题
探索五种算法以平衡赚取/学习的权衡
·发表于 Towards Data Science ·阅读时间 17 分钟·2024 年 5 月 15 日
--

图片来自作者
我的目标是让你从这篇文章中获得以下内容:
-
对于什么构成了多臂赌博机问题有一个良好的理解
-
了解如何解决多臂赌博机问题(需要考虑的因素、常见算法的示例,以及使用 Python 代码的模拟数据)
几年前,我读了一本很棒的书,叫做《Algorithms to Live By》,作者是 Brian Christian 和 Tom Griffiths。这本书的主要概念是,我们在日常生活中面临的许多决策问题,与数据专业人员在工作中面临的问题类型相同。我在生活中看到的多臂赌博机问题的实例比其他任何问题都多。它到处都能看到!
我在我的休闲爱好——钓鱼中看到了这个问题。快速声明——我钓鱼技术很差——但我仍然很享受它。我常常会想,我应该使用哪种诱饵才能钓到尽可能多的鱼。当我使用一种诱饵时,我总是试图决定是否有另一种诱饵可能会钓到更多的鱼。这就是一个多臂赌博机问题!
我觉得展示各种解决钓鱼启发的多臂赌博机问题的方法既有趣又富有启发性…
轻松训练专门的 LLM:PEFT、LoRA、QLoRA、LLaMA-Adapter 等
在自己的数据上训练一个专门的 LLM 比你想象的要容易…
·发布于 Towards Data Science ·阅读时长 31 分钟·2024 年 3 月 9 日
--

(照片由 Clay Banks 提供,来自 Unsplash)
由于大语言模型(LLMs)引起的兴趣激增,AI 从业者常常会被问到这样的问题:我们如何在自己的数据上训练一个专门的 LLM? 然而,回答这个问题远非易事。最近生成性 AI 的进展依赖于具有大量参数的巨大模型,而训练这样的 LLM 需要昂贵的硬件(即许多高价 GPU 和大量内存)和复杂的训练技术(例如,全分片数据并行训练)。幸运的是,这些模型通常分两个阶段进行训练——预训练和微调——其中前者阶段(通常)要贵得多。考虑到高质量的预训练 LLM 可以在网上轻松获得,大多数 AI 从业者可以简单地下载一个预训练模型,并专注于通过微调将该模型适应到他们想要的任务上。
“微调巨大的语言模型在硬件要求和为不同任务托管独立实例的存储/切换成本方面是极其昂贵的。” ——来自[1]
然而,模型的大小在微调期间并不会改变!因此,微调 LLM——尽管比预训练便宜——也不是…
使用 Tropycal 轻松追踪飓风
快速成功的数据科学
一个非常适合风暴分析的 Python 包
·发布于 Towards Data Science ·阅读时间 7 分钟·2024 年 11 月 6 日
--

2017 年北大西洋飓风路径,按风暴类型着色(作者提供)
最近,一位朋友向我提出了一个有趣的请求:他希望帮助选择加勒比地区的春假度假目的地。他希望能帮助一个最近受飓风影响的地区,期望他的旅游消费能够促进该地区的复苏工作。当然,他希望避开受影响太近的地方,所以我们决定查看过去八年(2017-2024)内的飓风,并排除过去两年(2023-2024)内受影响的地区。

当然,一个 AI 聊天机器人可以在几秒钟内完成这个任务,但我不打算轻易放弃,决定亲自使用 Python 进行分析。开源的飓风数据可以从多个来源轻松获得,包括:
-
国家飓风中心(NHC)数据档案(HURDAT)
-
国际气候管理最佳轨迹档案(IBTrACS)
-
美国地质调查局(USGS)飓风数据
-
NOAA 大西洋海洋气象实验室…
因果推断的简单方法
将你最喜欢的模型与元学习者结合起来,做出有效的因果推断
·发布于Towards Data Science ·阅读时间 10 分钟·2024 年 2 月 20 日
--

图片由Mika Baumeister提供,来自Unsplash
想象一下,你已经建立了一个很棒的机器学习模型,能够准确预测你的目标值。在某些情况下,这时你的工作可能就完成了。然而,通常业务不仅仅想知道会发生什么,还想知道如何影响结果。正如格言所说:
知道未来是银,而能够改变未来则是金。
这个简单的真理不言而喻,你从个人生活中就能体会到。知道下周的彩票号码很好,但只有当你能够调整你的号码时,这才有意义。
以商业为例,考虑客户流失的问题,即停止与您做生意的客户。知道客户想要离开您是好的,但真正的问题是:如何防止这个客户流失?
业务需要某种形式的干预,例如通过发放优惠券或提供某种会员升级。这些是业务可以影响的措施,目的是降低流失的概率。
如果x = “给客户发优惠券”,y = 流失概率,我们希望能够……
使用 Yolo-NAS 进行简单的物体检测
学习如何使用 Python 和 yolo-NAS 进行物体检测
·发表于Towards Data Science ·阅读时间:6 分钟·2024 年 8 月 3 日
--

图片来源:googledeepmind @ Unplash.com
YOLO(You only look once)彻底改变了计算机视觉领域。YOLO 的第一个版本由 Joseph Redmon 等人在 2016 年发布,在速度和准确性方面打破了多个基准。在物体检测领域,YOLO 一直是数据科学家和机器学习工程师的最爱,也是分割图像中实体的首选模型。
自从推出以来,YOLO 经历了多次迭代,解决了之前版本中的若干问题,具体包括:
-
改进了底层深度学习模型的架构。
-
实现了提高性能的替代方案,例如数据增强技术。
-
将原始的 YOLO 代码迁移至使用pytorch训练和部署框架。
-
改进了小物体的检测机制。
YOLO 的最新版本是 YOLO v9(arxiv.org/abs/2402.13616)。需要注意的一点是,每个计算机视觉和物体检测模型都会根据两个参数进行评估:准确性(由与计算机视觉分割相关的指标定义)和速度(由推理中的延迟定义)。下面是计算机视觉算法评估的一个示例:
ECCCos 来自黑箱
通过能量约束的符合性反事实实现忠实的模型解释
·发表于Towards Data Science ·15 分钟阅读·2024 年 2 月 8 日
--
反事实解释提供了一种直观且简洁的方式来解释不透明的机器学习(ML)模型。它们的工作原理是在扰动输入的前提下,实现预测输出的期望变化。
如果你之前没有听说过反事实解释,可以查看我的入门文章:1) 黑箱模型的个体回溯 和 2) 可解释人工智能的新工具。
通常,有许多方式可以实现这一目标,换句话说,许多不同的反事实可能会产生相同的期望结果。因此,研究人员面临的一个关键挑战是,首先,定义反事实解释的某些期望特征,其次,提出实现这些特征的高效方法。
反事实解释最重要且研究最深入的特征之一是“可 plausibility”:解释应该看起来对人类来说是现实的。可 plausibility 与可操作性、鲁棒性(Artelt 等人,2021 年)和因果有效性(Mahajan、Tan 和 Sharma,2020 年)呈正相关。为了实现 plausibility,许多现有方法依赖于替代模型。这种方法直接,但它也使事情更加复杂:本质上,它将学习数据的合理解释的任务从模型本身转移到替代模型上。
环保人工智能:如何减少你的机器学习模型的碳足迹和水足迹
模型训练和服务的可持续实践
·发表于 Towards Data Science ·阅读时间 11 分钟·2024 年 7 月 3 日
--

图片来源:Shutterstock
人工智能与可持续性:是时候认真对待了。
随着我们推动人工智能的边界,特别是生成模型的发展,我们面临一个日益紧迫的问题:我们的进步带来了什么样的环境成本?训练、托管和运行这些模型不仅仅是计算密集型的——它们还需要大量的自然资源,导致显著的碳排放和水足迹,这些问题往往被忽视。随着谷歌在 2024 年 7 月 2 日发布的报告,关于实现其雄心勃勃的气候目标面临的挑战,这个话题变得更加及时。报告透露,2023 年的排放量比去年增加了13%,而与其 2019 年的基准年相比增加了48%。人工智能的需求显著加剧了数据中心的负担,这一趋势在微软 2024 年 5 月发布的环境可持续发展报告中得到了反映,报告指出,由于数据中心的使用,其排放量比 2020 年基准年增加了 29%。此外,国际能源署预测,到 2026 年,全球数据中心和人工智能的电力需求可能会翻倍,这突显了可持续实践的迫切需求。对于每个人来说...
生成性人工智能的经济学
考虑到我们目前对这项技术和市场的了解,生成性人工智能的商业模式是什么?
·发布于Towards Data Science ·6 分钟阅读·2024 年 8 月 1 日
--

图片由Ibrahim Rifath提供,发布于Unsplash
OpenAI 已经建立了历史上增长最快的企业之一。它可能也是运营成本最高的企业之一。
根据 The Information 的一项分析,ChatGPT 的制造商今年可能会亏损多达 50 亿美元,该分析基于先前未公开的内部财务数据以及参与该业务的人士。如果我们的分析正确,OpenAI(最近估值为 800 亿美元)将在接下来的 12 个月内需要筹集更多的资金。
我在这里写作时花了一些时间讨论技术和资源方面的限制,非常有趣的是,随着这些挑战变得越来越清晰且对围绕这项技术崛起的行业变得愈发紧迫,情况也在发生变化。
然而,我认为这个问题引出了一个关键问题,那就是生成性人工智能的商业模式究竟是什么?我们应该期待什么,又什么只是噱头?这项技术的承诺与实际现实之间的差距在哪里?
生成性人工智能是一个功能还是一个产品?
我与一些人讨论过这个话题,也在媒体上听到过很多相关讨论。技术是作为功能还是产品的区别,基本上取决于它是否在独立使用时具备足够的价值,让人们愿意单独购买使用,还是它是否在与其他技术结合时展现出大部分或全部的价值。现在我们看到“AI”被添加到许多现有的产品中,从文本/代码编辑器到搜索引擎,再到浏览器,这些应用都是“生成式 AI 作为功能”的例子。(我现在就在 Notion 中写这段文字,它一直试图让我用 AI 做点什么。)另一方面,我们看到 Anthropic、OpenAI 和其他一些公司正在尝试销售以生成式 AI 为核心的产品,比如 ChatGPT 或 Claude。
这可能开始变得有些模糊,但我认为关键因素是,对于“生成式 AI 作为产品”这一群体来说,如果生成式 AI 无法满足客户的期望,无论是什么期望,那么他们就会停止使用这个产品,并停止支付给服务提供商。另一方面,如果有人发现(可以理解的)Google 的 AI 搜索摘要很糟糕,他们可以抱怨并关闭它,然后像以前一样继续使用 Google 搜索。核心商业价值主张并不是建立在 AI 的基础上的,它只是一个额外的潜在卖点。这使得整体业务的风险大大降低。
苹果在生成式 AI 领域的做法是一个很好的例子,它将生成式 AI 概念化为功能,而不是产品,在我看来,他们显然的策略更具潜力。在最近的 WWDC 上,苹果透露他们正在与 OpenAI 合作,让苹果用户通过 Siri 访问 ChatGPT。这里有几个关键点非常重要。首先,苹果并没有向 OpenAI 支付任何费用来建立这种合作关系——苹果带来了其具有高度经济吸引力的用户群,而 OpenAI 则有机会将这些用户转化为 ChatGPT 的付费订阅者,如果他们能够做到的话。苹果在这一关系中没有承担任何风险。其次,这并不排除苹果以相同的方式为他们的用户提供其他生成式 AI 服务,比如 Anthropic 或 Google 的产品。他们并没有明确押注于生成式 AI 更大竞争格局中的某一方,尽管 OpenAI 恰好是第一个宣布的合作伙伴。当然,苹果也在开发Apple AI,他们自己的生成式 AI 解决方案,但显然他们的目标是将这些服务作为增强现有和未来产品线的一部分——使你的 iPhone 更有用——而不是将模型作为独立的产品进行销售。
所以,以上是想说生成式 AI 如何以及应当如何融入商业战略的思考方式有很多种,而仅仅构建技术本身并不能保证它会是最成功的。十年后回头看,我怀疑我们认为是生成式 AI 业务领域“最大赢家”的公司,是否会是那些实际开发了底层技术的公司。
什么样的商业战略对于发展是有意义的?
好吧,你可能会想,但是有人必须去打造它,如果这些功能足够有价值,值得拥有,对吧?如果钱并不在于生成式 AI 能力的实际创建上,那我们会拥有这种能力吗?它会发挥其全部潜力吗?
我应该承认,很多科技领域的投资者确实相信生成式 AI 有巨大的赚钱潜力,这也是他们已经向 OpenAI 及其同行投资了数十亿美元的原因。然而,我在之前的几篇文章中也提到过,即使有这些数十亿资金,我还是强烈怀疑,未来我们将看到的生成式 AI 性能改进只会是轻微的、渐进式的,而不是继续我们在 2022–2023 年看到的似乎是指数级的技术进步。(特别是,为了实现承诺的进展,训练所需的海量人类生成数据的限制,不能仅通过投钱来解决。)这意味着我并不相信生成式 AI 会变得比现在更加有用或“聪明”。
说到这些,不管你是否同意我的观点,我们都应该记住,拥有一项高度先进的技术与能够利用这项技术创造出人们愿意购买的产品并从中建立一个可持续的、可再生的商业模式是完全不同的。你可以发明一个很酷的新东西,但正如任何初创公司或科技公司中的产品团队所告诉你的那样,这并不是过程的终点。弄清楚普通人如何以及为何会使用你的新东西,并将这一点传达出去,让人们相信你的新东西值一个可持续的价格,这是极其困难的。
我们确实看到了很多来自各个渠道的提议,但其中一些想法的效果相当差。OpenAI 上周宣布的搜索引擎新测试版,已经在输出结果中出现了重大错误。任何读过我之前的文章和其他文章的人都不会感到惊讶。(我个人只是感到惊讶,他们在开发这个产品时,居然没有考虑到这个显而易见的问题。)即便是那些看起来有吸引力的想法,也不能只是“可有可无”或者奢侈品,它们必须是必不可少的,因为要使这个业务可持续所需的成本必须非常高。当你的烧钱速度达到每年 50 亿美元时,为了实现盈利并自我维持,你的付费用户数量必须是天文数字,或者用户支付的价格必须令人瞠目结舌。
难道研究本身就不具备内在的价值吗?
这让那些最想推动技术边界的人处于一个困难的境地。为了研究而研究的行为一直以某种形式存在,即使其结果并不立即具有实际应用价值。但资本主义并没有一个好的渠道来维持这种工作,尤其是在这种研究的参与成本高得令人难以置信的情况下。几十年来,美国一直在把学术机构的资源榨干,因此学者和学术研究人员几乎没有机会参与这种研究,除非有私人投资。
我认为这真是太遗憾了,因为学术界正是可以在适当监管下进行这种研究的地方。伦理、安全和风险问题可以在学术环境中得到认真对待和探索,而这些问题在私营部门根本不会得到优先考虑。学术研究的文化和规范能够让学者把知识置于金钱之上,但当私营部门企业主导所有研究时,这些选择就发生了变化。我们的社会信任做“更纯粹”研究的人,并没有获得足够的资源来显著参与生成性 AI 的蓬勃发展。
那接下来怎么办?
当然,即便是这些私营公司,也很可能没有足够的资源来支撑继续训练更多、更大的模型的疯狂竞赛,这又将我们带回我在本文开头提到的那句话。由于支配我们技术进步的经济模型,我们可能会错失潜在的机会。那些有意义的生成性人工智能应用,虽然合理,却未必能赚取足够的数十亿,以支撑 GPU 的费用,因此可能永远无法深入探索;而那些社会有害、愚蠢或无用的应用则可能获得投资,因为它们带来了更大的现金收益机会。
欲了解更多我的作品,请访问 www.stephaniekirmer.com.
进一步阅读
www.theatlantic.com/technology/archive/2024/07/searchgpt-openai-error/679248/
www.washingtonpost.com/technology/2024/03/10/big-tech-companies-ai-research/
托管开源 LLMs 的经济学
大型语言模型在生产中的应用
利用各种部署选项
·发表于Towards Data Science ·阅读时间 19 分钟·2024 年 11 月 12 日
--

GPU 与 CPU 上的总处理时间 — 非比例缩放* | 图片由作者提供
如果你不是会员但想阅读本文,请查看这个朋友链接 这里。
如果你已经在尝试不同规模的开源模型,你可能会问自己:部署它们最有效的方式是什么?
按需和无服务器提供商之间的定价差异是什么?当有 LLM 服务平台时,真的值得与像 AWS 这样的玩家打交道吗?
我决定深入探讨这个话题,将 AWS 等云服务商与 Modal、BentoML、Replicate、Hugging Face Endpoints 和 Beam 等新兴替代方案进行比较。
我们将研究处理时间、冷启动延迟、CPU、内存和 GPU 成本等指标,以了解哪种方式最有效且经济。我们还将涵盖一些软性指标,如部署的难易程度、开发者体验和社区支持。

我们将查看的一些指标 | 图片由作者提供
我们将探讨一些使用案例,比如在 CPU 上部署较小的模型与在 GPU 上运行70 亿到 80 亿参数的模型的区别。
使用 EDA 深入探索词向量
可视化文本数据中的意外见解
·发表于Towards Data Science ·阅读时间:14 分钟·2024 年 7 月 12 日
--
在开始使用新数据集时,通常从一些探索性数据分析(EDA)入手是个不错的选择。在训练任何复杂模型之前,花时间了解你的数据有助于你理解数据集的结构,识别任何明显的问题,并运用领域特定的知识。
你可以在各种形式中看到 EDA,从房价到数据科学行业中的高级应用。但我仍然没有看到它应用于当前最火的新数据集:词向量,这也是我们最优秀的大型语言模型的基础。那么,为什么不尝试一下呢?
在这篇文章中,我们将将 EDA 应用于 GloVe 词向量,使用协方差矩阵、聚类、PCA 和向量数学等技术。这将帮助我们理解词向量的结构,为我们在此数据基础上构建更强大的模型提供一个有用的起点。当我们探索这个结构时,我们会发现它并不总是表面看起来的那样,某些意想不到的偏差隐藏在语料库中。
你将需要:
-
基本的线性代数、统计学和向量数学知识
-
Python 包:
numpy、sklearn和matplotlib -
约 3 GB 的空闲磁盘空间
数据集
要开始,请下载数据集,链接为:huggingface.co/stanfordnlp/glove/resolve/main/glove.6B.zip[1]。此文件包含三个文本文件,每个文件包含一组单词及其向量表示。我们将使用300 维的表示(glove.6B.300d.txt)。
简单说明一下这个数据集的来源:基本上,这是从维基百科和各种新闻来源中获取的 60 亿个标记的共现数据生成的词嵌入列表。使用共现的一个有用副作用是,意思相近的词往往会聚集在一起。例如,由于“the red bird”和“the blue bird”都是有效句子,我们可能会预期“red”和“blue”的向量会彼此接近。更多技术信息,可以参考原始的 GloVe 论文[1]。
需要明确的是,这些不是为大型语言模型训练的词嵌入。它们是一种完全无监督的技术,基于大量语料库。然而,它们展示了许多与语言模型嵌入相似的特性,且本身也很有趣。
这个文本文件的每一行由一个单词和该单词对应的 300 个向量分量组成,分量之间以空格分开。我们可以用 Python 将其加载进来。(为了减少噪音并加速处理,我这里使用了完整数据集的前 10% //10,但如果你愿意的话,可以调整。)
import numpy as np
embeddings = {}
with open(f"glove.6B/glove.6B.300d.txt", "r") as f:
glove_content = f.read().split('\n')
for i in range(len(glove_content)//10):
line = glove_content[i].strip().split(' ')
if line[0] == '':
continue
word = line[0]
embedding = np.array(list(map(float, line[1:])))
embeddings[word] = embedding
print(len(embeddings))
那么我们现在加载了 40,000 个嵌入向量。
相似度度量
我们可能会问的一个自然问题是:向量通常是否与意义相似的其他向量接近? 作为后续问题,我们如何量化这个?
我们将量化向量之间相似度的主要方式有两种:一种是欧几里得距离,这就是我们熟悉的自然的毕达哥拉斯定理距离。另一种是余弦相似度,它衡量两个向量之间的角度的余弦值。一个向量与自身的余弦相似度为 1,与相反的向量为-1,与正交向量为 0。
让我们在 NumPy 中实现这些:
def cos_sim(a, b):
return np.dot(a,b)/(np.linalg.norm(a) * np.linalg.norm(b))
def euc_dist(a, b):
return np.sum(np.square(a - b)) # no need for square root since we are just ranking distances
现在我们可以找到与给定单词或嵌入向量最接近的所有向量!我们将按升序进行操作。
def get_sims(to_word=None, to_e=None, metric=cos_sim):
# list all similarities to the word to_word, OR the embedding vector to_e
assert (to_word is not None) ^ (to_e is not None) # find similarity to a word or a vector, not both
sims = []
if to_e is None:
to_e = embeddings[to_word] # get the embedding for the word we are looking at
for word in embeddings:
if word == to_word:
continue
word_e = embeddings[word]
sim = metric(word_e, to_e)
sims.append((sim, word))
sims.sort()
return sims
现在我们可以写一个函数来显示最相似的 10 个词。最好能加上一个反向选项,这样我们就能显示最不相似的词。
def display_sims(to_word=None, to_e=None, n=10, metric=cos_sim, reverse=False, label=None):
assert (to_word is not None) ^ (to_e is not None)
sims = get_sims(to_word=to_word, to_e=to_e, metric=metric)
display = lambda sim: f'{sim[1]}: {sim[0]:.5f}'
if label is None:
label = to_word.upper() if to_word is not None else ''
print(label) # a heading so we know what these similarities are for
if reverse:
sims.reverse()
for i, sim in enumerate(reversed(sims[-n:])):
print(i+1, display(sim))
return sims
最后,我们可以进行测试了!
display_sims(to_word='red')
# yellow, blue, pink, green, white, purple, black, colored, sox, bright
看起来波士顿红袜队在这里意外亮相了。不过除此之外,这大致符合我们的预期。
也许我们可以尝试一些动词,而不仅仅是名词和形容词?怎么样,试试像“share”这样一个友好且温暖的动词?
display_sims(to_word='share')
# shares, stock, profit, percent, shared, earnings, profits, price, gain, cents
我猜“share”在这个数据集中作为动词使用的情况不多。唉,没关系。
我们还可以尝试一些更传统的例子:
display_sims(to_word='cat')
# dog, cats, pet, dogs, feline, monkey, horse, pets, rabbit, leopard
display_sims(to_word='frog')
# toad, frogs, snake, monkey, squirrel, species, rodent, parrot, spider, rat
display_sims(to_word='queen')
# elizabeth, princess, king, monarch, royal, majesty, victoria, throne, lady, crown
类比推理
词嵌入的一个迷人特性是,类比是通过向量数学内建的。GloVe 论文中的例子是king - queen = man - woman。换句话说,重新排列这个方程,我们预期会得到king = man - woman + queen。这是真的吗?
display_sims(to_e=embeddings['man'] - embeddings['woman'] + embeddings['queen'], label='king-queen analogy')
# queen, king, ii, majesty, monarch, prince...
并不完全正确:与man - woman + queen最接近的向量实际上是queen(余弦相似度 0.78),接下来是king(余弦相似度 0.66),不过差距有点远。受到这段精彩的3Blue1Brown 视频的启发,我们可以试试用aunt和uncle代替:
display_sims(to_e=embeddings['aunt'] - embeddings['woman'] + embeddings['man'], label='aunt-uncle analogy')
# aunt, uncle, brother, grandfather, grandmother, cousin, uncles, grandpa, dad, father
这个结果更好(余弦相似度 0.7348 对比 0.7344),但仍然不完美。不过我们可以尝试改用欧几里得距离。现在我们需要设置reverse=True,因为较高的欧几里得距离实际上代表了较低的相似度。
display_sims(to_e=embeddings['aunt'] - embeddings['woman'] + embeddings['man'], metric=euc_dist, reverse=True, label='aunt-uncle analogy')
# uncle, aunt, grandfather, brother, cousin, grandmother, newphew, dad, grandpa, cousins
现在我们明白了。但似乎类比数学可能没有我们希望的那样完美,至少在我们目前采用的这种简单方法中。
幅度
余弦相似度完全与向量之间的角度有关。那么,向量的幅度是否也很重要呢?
我们可以通过将幅度表示为与零向量的欧几里得距离来重用现有的代码。让我们看看哪些单词具有最大和最小的幅度:
zero_vec = np.zeros_like(embeddings['the'])
display_sims(to_e=zero_vec, metric=euc_dist, label='largest magnitude')
# republish, nonsubscribers, hushen, tael, www.star, stoxx, 202-383-7824, resend, non-families, 225-issue
display_sims(to_e=zero_vec, metric=euc_dist, reverse=True, label='smallest magnitude')
# likewise, lastly, interestingly, ironically, incidentally, moreover, conversely, furthermore, aforementioned, wherein
看起来大幅度向量的意义似乎没有什么规律,但它们似乎都有非常具体(有时甚至令人困惑)的含义。另一方面,最小幅度的向量往往是一些非常常见的单词,可以在各种语境中找到。
幅度之间有巨大的差异:从最小的向量大约 2.6 到最大的向量大约 17。这个分布看起来是怎样的呢?我们可以绘制一个直方图来更好地展示这一点。
import matplotlib.pyplot as plt
def plot_magnitudes():
words = [w for w in embeddings]
magnitude = lambda word: np.linalg.norm(embeddings[word])
magnitudes = list(map(magnitude, words))
plt.hist(magnitudes, bins=40)
plt.show()
plot_magnitudes()

我们的词嵌入的幅度直方图
这个分布看起来大致呈正态分布。如果我们想进一步测试这一点,可以使用Q-Q 图。但就目前而言,这样已经足够了。
数据集偏差
事实证明,向量嵌入中的方向和子空间可以编码多种不同的概念,通常是有偏的。这篇论文[2]研究了这种情况如何与性别偏见相关。
我们也可以在 GloVe 嵌入中复制这个概念。首先,让我们找到“男性气质”概念的方向。我们可以通过计算向量之间的差异来完成这个任务,比如he 和 she、man 和 woman,等等:
gender_pairs = [('man', 'woman'), ('men', 'women'), ('brother', 'sister'), ('he', 'she'),
('uncle', 'aunt'), ('grandfather', 'grandmother'), ('boy', 'girl'),
('son', 'daughter')]
masc_v = zero_vec
for pair in gender_pairs:
masc_v += embeddings[pair[0]]
masc_v -= embeddings[pair[1]]
现在我们可以找到“最具男性气质”和“最具女性气质”的向量,这些都是根据嵌入空间的判断来确定的。
display_sims(to_e=masc_v, metric=cos_sim, label='masculine vecs')
# brother, colonel, himself, uncle, gen., nephew, brig., brothers, son, sir
display_sims(to_e=masc_v, metric=cos_sim, reverse=True, label='feminine vecs')
# actress, herself, businesswoman, chairwoman, pregnant, she, her, sister, actresses, woman
现在,我们可以进行一个简单的测试,检测数据集中的偏见:计算nurse与man和woman之间的相似度。从理论上讲,这两个值应该大致相等:nurse 不是一个性别化的词汇。这是真的吗?
print("nurse - man", cos_sim(embeddings['nurse'], embeddings['man'])) # 0.24
print("nurse - woman", cos_sim(embeddings['nurse'], embeddings['woman'])) # 0.45
这是一个相当大的差异!(请记住,余弦相似度的范围是 -1 到 1,其中正相关值在 0 到 1 之间。)作为参考,0.45 也接近于cat 和 leopard 之间的余弦相似度。
聚类
让我们看看能否使用k-均值聚类来聚类具有相似含义的词语。使用scikit-learn包可以轻松实现这一点。我们将使用 300 个聚类,虽然听起来很多,但相信我:几乎所有聚类都非常有趣,你可以仅通过解释它们写一篇完整的文章!
from sklearn.cluster import KMeans
def get_kmeans(n=300):
kmeans = KMeans(n_clusters=n, n_init=1)
X = np.array([embeddings[w] for w in embeddings])
kmeans.fit(X)
return kmeans
def display_kmeans(kmeans):
# print all clusters and 5 associated words for each
words = np.array([w for w in embeddings])
X = np.array([embeddings[w] for w in embeddings])
y = kmeans.predict(X) # get the cluster for each word
for cluster in range(kmeans.cluster_centers_.shape[0]):
print(f'KMeans {cluster}')
cluster_words = words[y == cluster] # get all words in each cluster
for i, w in enumerate(cluster_words[:5]):
print(i+1, w)
kmeans = get_kmeans()
display_kmeans(kmeans)
这里有很多内容可以查看。我们有涉及不同主题的聚类,如纽约市(manhattan, n.y., brooklyn, hudson, borough)、分子生物学(protein, proteins, enzyme, beta, molecules)和印度名字(singh, ram, gandhi, kumar, rao)。
但是有时这些聚类并不像看起来那样简单。让我们编写代码来显示包含给定词语的聚类中的所有词汇,以及最近和最远的聚类。
def get_kmeans_cluster(kmeans, word=None, cluster=None):
# given a word, find the cluster of that word. (or start with a cluster index.)
# then, get all words of that cluster.
assert (word is None) ^ (cluster is None)
if cluster is None:
cluster = kmeans.predict([embeddings[word]])[0]
words = np.array([w for w in embeddings])
X = np.array([embeddings[w] for w in embeddings])
y = kmeans.predict(X)
cluster_words = words[y == cluster]
return cluster, cluster_words
def display_cluster(kmeans, word):
cluster, cluster_words = get_kmeans_cluster(kmeans, word=word)
# print all words in the cluster
print(f"Full KMeans ({word}, cluster {cluster})")
for i, w in enumerate(cluster_words):
print(i+1, w)
# rank all clusters (excluding this one) by Euclidean distance of their centers from this cluster's center
distances = np.concatenate([kmeans.cluster_centers_[:cluster], kmeans.cluster_centers_[cluster+1:]], axis=0)
distances = np.sum(np.square(distances - kmeans.cluster_centers_[cluster]), axis=1)
nearest = np.argmin(distances, axis=0)
_, nearest_words = get_kmeans_cluster(kmeans, cluster=nearest)
print(f"Nearest cluster: {nearest}")
for i, w in enumerate(nearest_words[:5]):
print(i+1, w)
farthest = np.argmax(distances, axis=0)
print(f"Farthest cluster: {farthest}")
_, farthest_words = get_kmeans_cluster(kmeans, cluster=farthest)
for i, w in enumerate(farthest_words[:5]):
print(i+1, w)
现在让我们尝试这段代码。
display_cluster(kmeans, 'animal')
# species, fish, wild, dog, bear, males, birds...
display_cluster(kmeans, 'dog')
# same as 'animal'
display_cluster(kmeans, 'birds')
# same again
display_cluster(kmeans, 'bird')
# spread, bird, flu, virus, tested, humans, outbreak, infected, sars....?
你可能不会每次都得到完全相同的结果:聚类算法是非确定性的。但大多数情况下,“鸟类”会与疾病相关词汇而非动物词汇关联。似乎原始数据集倾向于在疾病传播媒介的语境中使用“鸟”这个词。
这里有成百上千个聚类等待你探索它们的内容。我发现一些有趣的聚类包括“伊利诺伊州”和“成吉思汗”。
主成分分析
主成分分析(PCA)是我们用来找出数据集在向量空间中方差最大的方向的工具。让我们试试它。和聚类一样,sklearn使得这一过程非常简单。
from sklearn.decomposition import PCA
def get_pca_vecs(n=10): # get the first 10 principal components
pca = PCA()
X = np.array([embeddings[w] for w in embeddings])
pca.fit(X)
principal_components = list(pca.components_[:n, :])
return pca, principal_components
pca, pca_vecs = get_pca_vecs()
for i, vec in enumerate(pca_vecs):
# display the words with the highest and lowest values for each principal component
display_sims(to_e=vec, metric=cos_sim, label=f'PCA {i+1}')
display_sims(to_e=vec, metric=cos_sim, label=f'PCA {i+1} negative', reverse=True)
就像我们的k-均值实验一样,这些 PCA 向量中有很多非常有趣的内容。例如,让我们来看看主成分 9:
PCA 9
1 featuring: 0.38193
2 hindi: 0.37217
3 arabic: 0.36029
4 sung: 0.35130
5 che: 0.34819
6 malaysian: 0.34474
7 ka: 0.33820
8 video: 0.33549
9 bollywood: 0.33347
10 counterpart: 0.33343
PCA 9 negative
1 suffolk: -0.31999
2 cumberland: -0.31697
3 northumberland: -0.31449
4 hampshire: -0.30857
5 missouri: -0.30771
6 calhoun: -0.30749
7 erie: -0.30345
8 massachusetts: -0.30133
9 counties: -0.29710
10 wyoming: -0.29613
看起来,组件 9 的正值与中东、南亚和东南亚的术语相关,而负值则与北美和英国的术语相关。
另一个有趣的成分是成分 3。所有的正值都是十进制数,显然这是这个模型中的一个显著特征。成分 8 也展示了类似的模式。
PCA 3
1 1.8: 0.57993
2 1.6: 0.57851
3 1.2: 0.57841
4 1.4: 0.57294
5 2.3: 0.57019
6 2.6: 0.56993
7 2.8: 0.56966
8 3.7: 0.56660
9 1.9: 0.56424
10 2.2: 0.56063
降维
PCA 的一个主要优点是,它允许我们将一个高维数据集(在此案例中为 300 维)通过投影到前几个组件,仅用两维或三维展示。让我们尝试做一个二维图,看是否能从中提取出任何信息。我们还会使用k-均值进行按聚类的颜色编码。
def plot_pca(pca_vecs, kmeans):
words = [w for w in embeddings]
x_vec = pca_vecs[0]
y_vec = pca_vecs[1]
X = np.array([np.dot(x_vec, embeddings[w]) for w in words])
Y = np.array([np.dot(y_vec, embeddings[w]) for w in words])
colors = kmeans.predict([embeddings[w] for w in words])
plt.scatter(X, Y, c=colors, cmap='spring') # color by cluster
for i in np.random.choice(len(words), size=100, replace=False):
# annotate 100 randomly selected words on the graph
plt.annotate(words[i], (X[i], Y[i]), weight='bold')
plt.show()
plot_pca(pca_vecs, kmeans)

我们的嵌入数据集的第一(X 轴)和第二(Y 轴)主成分的图表
不幸的是,这个图表完全乱了!从中学到的信息非常有限。看起来仅凭两个维度来解释 300 个维度的数据集,至少在这个数据集的情况下,并不是很容易。
有两个例外。首先,我们看到名字通常会聚集在图表的顶部。其次,在左下角有一个突出的小部分,像个伤疤一样。这一区域似乎与数字相关,尤其是十进制数字。
协方差
了解输入特征之间的协方差往往很有帮助。在这种情况下,我们的输入特征只是难以解释的抽象向量方向。然而,协方差矩阵可以告诉我们这些信息实际上被利用了多少。如果我们看到较高的协方差,意味着某些维度之间高度相关,或许我们可以稍微减少维度。
def display_covariance():
X = np.array([embeddings[w] for w in embeddings]).T # rows are variables (components), columns are observations (words)
cov = np.cov(X)
cov_range = np.maximum(np.max(cov), np.abs(np.min(cov))) # make sure the colorbar is balanced, with 0 in the middle
plt.imshow(cov, cmap='bwr', interpolation='nearest', vmin=-cov_range, vmax=cov_range)
plt.colorbar()
plt.show()
display_covariance()

我们数据集中所有 300 个向量组件的协方差矩阵
当然,主对角线有一条明显的线,表示每个组件与自身有很强的相关性。除此之外,这个图并没有什么特别有趣的地方。大部分区域看起来几乎是空白的,这其实是一个好兆头。
如果仔细观察,你会发现有一个例外:组件 9 和 276 似乎有较强的相关性(协方差为 0.308)。

聚焦于组件 9 和 276 的协方差矩阵。观察这里有一个稍微明亮的红点,以及沿着行和列的奇怪行为。
让我们通过打印与组件 9 和 276 最相关的向量来进一步调查。这个操作相当于计算与一个全零基向量的余弦相似度,除了相关组件的位置为 1。
e9 = np.zeros_like(zero_vec)
e9[9] = 1.0
e276 = np.zeros_like(zero_vec)
e276[276] = 1.0
display_sims(to_e=e9, metric=cos_sim, label='e9')
# grizzlies, supersonics, notables, posey, bobcats, wannabe, hoosiers...
display_sims(to_e=e276, metric=cos_sim, label='e276')
# pehr, zetsche, steadied, 202-887-8307, bernice, goldie, edelman, kr...
这些结果很奇怪,并且信息量不大。
等一下:如果一个向量组件中具有非常负值的词汇在另一个组件中也倾向于具有非常负的值,那么这些组件之间也可能存在正协方差。让我们试着反转相似性的方向。
display_sims(to_e=e9, metric=cos_sim, label='e9', reverse=True)
# therefore, that, it, which, government, because, moreover, fact, thus, very
display_sims(to_e=e276, metric=cos_sim, label='e276', reverse=True)
# they, instead, those, hundreds, addition, dozens, others, dozen, only, outside
看起来这两个组件都与基本功能词和数字相关,这些词可以在许多不同的语境中找到。这有助于解释它们之间的协方差,至少比正协方差的情况更能解释这一点。
结论
在本文中,我们对一个包含 300 维的GloVe 词向量数据集应用了各种探索性数据分析(EDA)技术。我们使用余弦相似度来衡量词汇意义之间的相似性,使用聚类将词汇分组为相关群体,并通过主成分分析(PCA)来识别对词向量模型最重要的向量空间方向。
我们通过主成分分析(PCA)在视觉上观察到输入特征之间的协方差非常小。我们尝试使用 PCA 将所有 300 维的数据投影到二维空间中,但结果还是有点混乱。
我们还测试了数据集中的假设和偏差。通过比较nurse与man和woman的余弦相似度,我们识别出了数据集中的性别偏见。我们尝试使用向量数学表示类比(例如,“king”与“queen”的关系就像“man”与“woman”),并取得了一定的成功。通过减去指代男性和女性的向量示例,我们能够发现与性别相关的向量方向,并展示数据集中“最男性化”和“最女性化”的向量。
你可以在词向量数据集上尝试更多的 EDA,但我希望这能作为一个良好的起点,帮助你理解一般的 EDA 技术以及词向量的结构。如果你想查看与本文相关的完整代码及一些额外的示例,可以访问我的 GitHub:crackalamoo/glove-embeddings-eda。感谢阅读!
参考文献
[1] J. Pennington, R. Socher 和 C. Manning,GloVe:用于词表示的全局向量(2014 年),斯坦福大学自然语言处理(公开领域数据集)
[2] T. Bolukbasi, K. Chang, J. Zou, V. Saligrama 和 A. Kalai,人类和计算机程序员的关系,就像女人和家庭主妇的关系?去偏见词向量嵌入(2016 年),微软研究院新英格兰分院
所有图片均由作者使用 Matplotlib 制作。
使用 AI 编辑图片中的文字
场景文本编辑的研究综述:STEFANN、SRNet、TextDiffuser、AnyText 等。
·发表于Towards Data Science ·13 分钟阅读·2024 年 2 月 18 日
--
如果你曾尝试过更改图片中的文字,你就会知道这并不简单。保持背景、纹理和阴影需要一份 Photoshop 许可证和辛苦获得的设计师技能。在下面的视频中,一位 Photoshop 专家花了 13 分钟时间修正海报中几个拼写错误,而这张海报的设计风格也并不复杂。好消息是——在人类不断追求 AGI 的过程中,我们也在构建一些实际生活中有用的 AI 模型。例如那些让我们能够以最小的努力编辑图片中文字的模型。
Photoshop 专家手动编辑一张 AI 生成的图片,将“午夜之城”正确拼写出来,花费了超过 13 分钟的时间。
自动更新图片中文字的任务正式被称为场景文本编辑(STE)。本文将描述 STE 模型架构是如何随着时间的发展而演变的,以及它们所解锁的能力。我们还将讨论它们的局限性和仍需完成的工作。对GANs和扩散模型的先前了解会有所帮助,但不是严格必要的。
免责声明:我是 Storia AI的共同创始人,正在构建一个用于视觉编辑的 AI 助手。此文献综述是开发 Textify功能的一部分,该功能允许用户无缝地修改图像中的文本。虽然 Textify 是闭源的,但我们开源了一个相关的库, Detextify,它可以自动从图像集合中移除文本。

场景文本编辑(STE)示例。原始图像(左)是通过Midjourney生成的。我们使用Textify对图像进行了注释(中),并自动修正了拼写错误(右)。
场景文本编辑(STE)任务
定义
场景文本编辑(STE)是指自动修改捕捉到视觉场景的图像中的文本(与主要包含文本的图像,如扫描文档不同)。目标是在不需要昂贵人工劳动的情况下,改变文本内容,同时保持原始的美学效果(如排版、书法、背景等)。
使用案例
场景文本编辑可能看起来像是一个人为的任务,但实际上它有多个实际应用场景:
(1) 场景文本识别(STR)的合成数据生成

通过编辑原始图像(左,来自Unsplash)中的文本获得的合成图像(右)。该技术可用于增强场景文本识别(STR)模型的训练集。
当我开始研究这个任务时,我惊讶地发现阿里巴巴(一个电子商务平台)和百度(一个搜索引擎)一直在持续发布关于场景文本编辑(STE)的研究。
至少在阿里巴巴的案例中,他们的研究可能是为了支持AMAP,这是他们的谷歌地图替代品[source]。为了绘制世界地图,你需要一个强大的文本识别系统,它能够在各种字体下读取交通和街道标志,并能在现实世界的各种条件下,如遮挡或几何失真,甚至多语言环境中识别。
为了构建一个场景文本识别的训练集,可以收集现实世界的数据并由人工进行标注。但这种方法受到人工劳动的瓶颈,且可能无法保证数据的多样性。相反,合成数据生成提供了几乎无限的多样化数据源,并带有自动标签。
(2) 对 AI 生成图像的控制

通过 Midjourney 生成的 AI 图像(左)并通过场景文本编辑进行修正。
类似于Midjourney、Stability和Leonardo的 AI 图像生成器已经使视觉资产的创建民主化。小企业主和社交媒体营销人员现在可以通过简单地输入文本提示来创建图像,而无需艺术家或设计师的帮助。然而,文本到图像的模式缺乏在概念艺术之外的实际资产所需的可控性——如活动海报、广告或社交媒体帖子。
这些资产通常需要包含文本信息(如日期和时间、联系方式或公司名称)。拼写正确一直是文本到图像模型的一个难点,尽管最近有了进展——DeepFloyd IF、Midjourney v6。但即使这些模型最终学会了完美拼写,文本到图像界面的用户体验约束依然存在。用文字描述文本放置的位置和方式仍然很繁琐。
(3) 视觉媒体的自动本地化
电影和游戏常常需要进行不同地区的本地化。这有时可能意味着将西兰花换成青椒,但大多数时候则需要翻译屏幕上可见的文本。随着电影和游戏行业的其他方面开始实现自动化(如配音和口型同步),没有理由让视觉文本编辑仍然保持手动操作。
架构时间线:从 GAN 到扩散模型
用于场景文本编辑的训练技术和模型架构在很大程度上跟随了图像生成这一更大任务的趋势。
GAN 时代(2019–2021)
GANs(生成对抗网络)在 2010 年代中期主导了图像生成任务。GAN 指的是一种特定的训练框架(而不是规定模型架构),其本质上是对抗性的。一个生成器模型被训练来捕捉数据分布(从而具备生成新数据的能力),而一个判别器则被训练来区分生成器输出与真实数据。在训练过程中,当判别器的猜测接近于随机抛硬币时,训练过程即完成。在推理阶段,判别器被丢弃。
GAN 特别适合用于图像生成,因为它们可以执行无监督学习——也就是说,学习数据分布而无需标记数据。沿着图像生成的总体趋势,最初的场景文本编辑模型也采用了 GAN。
GAN 第 1 轮:字符级编辑——STEFANN
STEFANN,被认为是首个修改场景图像中文本的工作,操作的是字符级别。字符编辑问题被分为两部分:字体适应和颜色适应。

STEFANN模型架构(来源)。字符编辑任务被分为两部分:FANnet(字体适应网络)生成所需形状的黑白目标字符,Colornet 则填充合适的颜色。
STEFANN被认为是首个修改场景图像中文本的工作。它基于先前的字体合成(即创建与输入数据中观察到的字体或文本风格相似的新字体或文本样式)工作,并增加了一个约束,即输出需要无缝地融合回原始图像。与之前的工作相比,STEFANN 采用纯机器学习方法(而非例如显式的几何建模),且不依赖于字符识别来标注源字符。
STEFANN 模型架构基于CNN(卷积神经网络),并将问题分解为(1)通过 FANnet 进行字体适应——将源字符的二值化版本转换为目标字符的二值化版本,(2)通过 Colornet 进行颜色适应——将 FANnet 的输出着色,以匹配图像中其余文本的颜色,以及(3)字符放置——使用已建立的技术,如修补和接缝雕刻,将目标字符融合回原始图像。前两个模块是通过 GAN 目标训练的。
官方 STEFANN 演示由其作者制作。
尽管 STEFANN 为场景文本编辑开辟了道路,但它在实际应用中存在多项限制。它一次只能操作一个字符;修改整个单词需要多次调用(每个字母一次),并且要求目标单词与源单词具有相同的长度。此外,步骤(3)中的字符放置算法假设字符之间不重叠。
GAN 第 2 轮:词级编辑——SRNet 和 3 模块网络
SRNet是首个在词级别进行场景文本编辑的模型。SRNet 将 STE 任务分解为三个(联合训练的)模块:文本转换、背景修复和融合。

SRNet模型架构。三个模块将 STE 问题分解成更小的构建块(文本转换、背景修复和融合),并在联合训练的过程中进行优化。这个架构被该领域的后续工作广泛采用。
SRNet是第一个在词级别上执行场景文本编辑的模型。SRNet 将 STE 任务分解为三个(联合训练的)模块:
-
文本转换模块(蓝色部分)接收目标文本的程序化渲染(如上图中的“barbarous”),并旨在将其以与输入单词(“introduce”)相同的字体在纯背景上渲染出来。
-
背景修复模块(绿色部分)从输入图像中去除文本,并填补空白,重建原始背景。
-
融合模块(橙色部分)将渲染的目标文本粘贴到背景上。
SRNet 架构。 三个模块都是全卷积网络(FCNs)的变体,其中背景修复模块特别类似于U-Net(一种具有特定属性的 FCN,编码器层与解码器层之间有跳跃连接,且大小相同)。
SRNet 训练。 每个模块都有自己的损失函数,网络是通过损失总和(LT + LB + LF)进行联合训练的,其中后两个损失通过 GAN 进行训练。虽然这种模块化概念上非常优雅,但也带来了需要配对训练数据的缺点,每个中间步骤都需要监督。实际上,这只能通过人工数据来实现。对于每个数据点,随机选择一张图像(来自像COCO这样的数据集),从字典中选择两个任意单词,并用任意字体渲染它们,以模拟“前后”图像。因此,训练集不包含任何照片级真实的示例(尽管它可以在一定程度上超越渲染字体进行泛化)。
荣誉提及。 SwapText采用了相同的基于 GAN 的三模块网络方法进行场景文本编辑,并对文本转换模块提出了改进。
GAN Epoch #3: 自监督和混合网络
跃迁到自监督学习。 STE 研究的下一个跃迁是采用自监督训练方法,在这种方法中,模型在未配对的数据上进行训练(即仅包含文本的图像库)。为了实现这一点,需要去除依赖标签的中间损失 LT 和 LB。由于 GANs 的设计,剩下的最终损失也不需要标签;模型只是根据鉴别器区分真实图像和由生成器产生的图像的能力进行训练。TextStyleBrush 在 STE 的自监督训练方面开创了先河,而 RewriteNet 和 MOSTEL 通过两阶段训练充分发挥了两者的优势:一阶段为监督学习(优势:合成标签数据的丰富性),另一阶段为自监督学习(优势:自然无标签数据的真实性)。
解耦文本内容与风格。 为了去除中间损失,TextStyleBrush 和 RewriteNet 将问题重新定义为解耦文本内容与文本风格。再强调一次,STE 系统的输入包括(a)包含原始文本的图像和(b)所需的文本——更具体地说,是在白色或灰色背景上使用固定字体(如 Arial)渲染的所需文本。目标是将(a)中的风格与(b)中的内容结合起来。换句话说,我们互补地旨在丢弃(a)中的内容和(b)中的风格。这就是为什么在给定图像中需要将文本内容与风格解耦的原因。

RewriteNet 的推理架构。编码器 E 将文本风格(圆形)和文本内容(三角形)解耦。来自原始图像的风格嵌入和来自文本渲染的内容嵌入被送入生成器,生成器将二者融合成输出图像。
TextStyleBrush 以及为什么 GANs 逐渐不再流行。虽然将文本内容与风格解耦的想法很简单,但在实践中实现这一点需要复杂的架构。TextStyleBrush,该领域最著名的论文,使用了不少于七个联合训练的子网络,一个预训练的字体分类器,一个预训练的 OCR 模型和多个损失函数。设计这样的系统一定非常昂贵,因为所有这些组件都需要进行消融研究来确定它们的效果。再加上 GANs 众所周知难以训练(理论上,生成器和鉴别器需要达到纳什均衡),这使得 STE 研究人员在扩散模型证明非常适合图像生成后,迫不及待地希望转向扩散模型。
扩散时代(2022 — 至今)
2022 年初,图像生成领域从 GANs 转向了潜在扩散模型(LDM)。这里不涉及 LDM 的详细解释,但你可以参考The Illustrated Stable Diffusion进行出色的教程。在这里,我将重点介绍与场景文本编辑任务最相关的 LDM 架构部分。

基于扩散的场景文本编辑。除了在标准文本到图像模型中传递给实际扩散模块的文本嵌入,STE 架构还创建反映目标文本(位置、形状、样式等)所需属性的嵌入。作者插图。
如上所示,基于 LDM 的文本生成图像模型有三个主要组件:(1) 一个文本编码器——通常是CLIP,(2) 实际的扩散模块——将文本嵌入转换为潜在空间中的图像嵌入,(3) 一个图像解码器——将潜在图像放大为完整尺寸的图像。
场景文本编辑作为扩散图像修补任务
文本到图像并不是扩散模型支持的唯一范式。毕竟,CLIP同样是一个文本和图像编码器,因此传递给图像信息创作者模块的嵌入也可以编码图像。实际上,它可以编码任何模态,或者多个输入的拼接。
这就是修补的原理,即根据给定的指令,只修改输入图像的一个子区域,并使其与图像的其他部分看起来协调一致。图像信息创作者摄取一个编码,该编码包含输入图像、需要修补区域的掩模和文本指令。
场景文本编辑可以看作是修补的一个专门化形式。大多数 STE 研究都归结为以下问题:我们如何用有关任务的附加信息(即原始图像、期望文本及其位置等)来增强文本嵌入? 正式来说,这被称为条件性引导。
归入这一类别的研究论文(TextDiffuser,TextDiffuser 2,GlyphDraw,AnyText等)提出了各种形式的条件性引导。
位置信息引导
显然,需要一种方法来指定在哪里对原始图像进行更改。这可以是文本指令(例如“更改底部的标题”)、文本行的细粒度指示,或者每个目标字符的更精细的位置信息。
通过图像掩码的位置信息。指示所需文本位置的一种方式是通过灰度掩码图像,然后可以通过 CLIP 或其他图像编码器将其编码到潜在空间中。例如,DiffUTE模型仅使用一张黑色图像,其中有一条白色条带指示所需的文本位置。

输入到DiffUTE模型。位置指导通过掩码 m 和被掩盖的输入 xm 实现。这些是基于用户输入以确定的方式渲染的。
TextDiffuser生成字符级分割掩码:首先,它大致渲染所需文本的位置(白色背景上的黑色文本,字体为 Arial),然后将此渲染结果传递给分割器,以获得一个灰度图像,其中包含每个字符的单独边界框。该分割器是一个U-Net模型,单独于主网络进行训练,使用了 400 万的合成实例。

TextDiffuser使用的字符级分割掩码。目标词(“WORK”)以标准字体在白色背景上渲染,然后通过分割器(U-Net)得到灰度掩码。
通过语言建模的位置信息。在A Unified Sequence Inference for Vision Tasks中,作者展示了大型语言模型(LLM)通过简单地生成数值令牌,可以有效地描述图像中物体的位置。可以说,这是一个反直觉的发现。由于 LLM 是基于统计频率学习语言的(即通过观察令牌在相同上下文中出现的频率),人们可能认为它们生成正确的数值令牌是不现实的。但是,当前 LLM 的巨大规模往往超出了我们的预期。
TextDiffuser 2以一种有趣的方式利用了这一发现。它们在一个由<文本,OCR 检测>对组成的合成语料库上微调 LLM,教它生成文本边界框的左上角和右下角坐标,如下图所示。值得注意的是,它们决定为文本行生成边界框(而不是字符),从而为图像生成器提供更多灵活性。它们还进行了一个有趣的消融实验,使用单个点来编码文本位置(无论是左上角还是框的中心),但观察到拼写表现较差——当模型没有明确告知文本应如何结束时,它经常会生成多余的字符。

TextDiffuser 2的架构。语言模型 M1 从用户获取目标文本,然后将其拆分成多行,并预测它们的位置,作为[x1] [y1] [x2] [y2]令牌。语言模型 M2 是 CLIP 的一个微调版本,它将修改后的提示(包括文本行及其位置)编码到潜在空间中。
字形引导
除了位置,另一个可以输入图像生成器的信息是字符的形状。有人可能会认为形状信息是多余的。毕竟,当我们提示文本到图像模型生成火烈鸟时,通常不需要传递关于它长腿或羽毛颜色的任何额外信息——模型应该已经从训练数据中学到了这些细节。然而,实际上,训练集(如 Stable Diffusion 的LAION-5B)主要由自然图像组成,其中文本的比例较低(非拉丁文字的比例更低)。
多项研究(DiffUTE,GlyphControl,GlyphDraw,GlyphDiffusion,AnyText等)尝试通过显式的字形引导来弥补这种不平衡——有效地使用标准字体程序化呈现字形,然后将渲染的编码传递给图像生成器。有些方法只是将字形放置在附加图像的中央,有些则将字形放置在接近目标位置(让人想起ControlNet)。
通过扩散进行 STE 仍然很复杂
尽管扩散模型的训练过程比 GAN 更稳定,但特别是 STE 的扩散架构仍然相当复杂。下图展示了AnyText的架构,其中包括(1)一个辅助潜在模块(包括上述讨论的位置和字形引导),(2)一个文本嵌入模块,其中包括需要预训练的 OCR 模块等组件,和(3)生成图像的标准扩散管道。很难说这在概念上比基于 GAN 的TextStyleBrush简单得多。

AnyText的(复杂)架构。
场景文本编辑的未来
当现状过于复杂时,我们有一种天然的倾向,即继续工作,直到它收敛到一个清晰的解决方案。从某种意义上说,这正是自然语言处理领域发生的事情:计算语言学理论、语法、依存句法分析——所有这些都在 Transformer 面前崩溃,Transformer 提出了一个非常简单的陈述:一个符号的意义依赖于其周围所有其他符号的意义。 显然,场景文本编辑离这种清晰度还相差甚远。其架构包含许多共同训练的子网络、预训练组件,并且需要特定的训练数据。
文本到图像模型在特定方面(如拼写、字体多样性、字符的清晰度)会随着适当数量和质量的训练数据变得更好。但可控性问题将会存在很长时间。即使模型最终学会完全按照你的指示执行,文本到图像的范式可能仍然是一个不尽人意的用户体验——你是宁愿详细描述文本的位置、外观和感觉,还是宁愿仅仅画一个大致的框并从调色板中选择一个灵感颜色?
结语:防止滥用
生成式 AI 揭示了许多伦理问题,从著作权/版权/许可到真实性和虚假信息。虽然这些问题在我们的集体意识中显得十分重要,并以各种抽象的方式体现出来,但场景文本编辑的滥用则是切实而明显的——人们伪造文档。
在构建 Textify 时,我们见识过各种情况。有些人在 Instagram 截图中提高他们的粉丝数,有些人在 Strava 截图中提高他们的跑步速度。是的,有些人甚至试图伪造身份证、信用卡和文凭。临时的解决方法是为某些类型的文档构建分类器,并直接拒绝编辑这些文档,但从长远来看,生成式 AI 社区需要投资于自动化方式来确定文档的真实性,无论是文本片段、图像还是视频。
有效的管理机器学习项目的策略
接受不确定性、合适的人才以及从数据中学习
·发布于 Towards Data Science ·6 分钟阅读·2024 年 6 月 4 日
--

这篇博客文章是我去年在 GOTO 阿姆斯特丹会议上所做部分演讲的更新版本。该演讲也可以 在线观看。
通过机器学习产品项目提供价值和积极影响并非易事。造成这种复杂性的主要原因之一是,在为数字产品开发的机器学习项目中,有两个不确定性来源相交。首先,是与机器学习解决方案本身相关的不确定性(我们能否以足够好的质量预测我们需要预测的内容?)。其次,是与整个系统可能提供的影响相关的不确定性(用户会喜欢这个新功能吗?它真的能解决我们试图解决的问题吗?)。
所有这些不确定性意味着,在机器学习(ML)产品项目中,失败是相对常见的。然而,仍然有一些策略可以帮助管理并提高成功的概率(或者至少让我们在面对失败时保持尊严生存下来!)。从正确的起点开始 ML 项目至关重要。我在之前的一篇文章中讨论了这个领域的主要经验:从问题入手(并从一开始就定义预测如何使用),从小做起(如果可以,保持小规模),并优先考虑正确的数据(质量、数量、历史)。
三大经验教训:问题、规模和数据
[towardsdatascience.com
然而,启动一个项目仅仅是开始。成功管理一个机器学习项目并在整个项目生命周期内提供积极影响的挑战将持续存在。在这篇文章中,我将分享我在机器学习项目中生存和成功的三大心得:
-
接受不确定性:创新、停顿、调整方向和失败。
-
与合适的人为伍:角色、技能、多样性,以及人脉网络。
-
从数据中学习:正确的方向、能够改进、发现失败并有应对计划。
接受不确定性
提前规划机器学习项目并根据初步计划进行开发,真的很困难(甚至是不可能的!)
最受欢迎的机器学习项目计划是ML 生命周期,它将机器学习项目的阶段分为业务理解、数据理解、数据准备、建模、评估和部署。尽管这些阶段通常被视为连续步骤,但在许多生命周期的表示中,你会看到箭头指向后方:在项目的任何阶段,你可能会学到一些东西,这迫使你回到前一个阶段。

机器学习生命周期(及其指向后方的箭头),图像来源:作者
这意味着项目的结束时间很难预知。例如,在评估阶段,你可能会通过模型可解释性技术意识到某个特征编码不当,这迫使你回到数据准备阶段。也可能发生模型无法达到你所需的预测质量,这可能迫使你回到业务理解阶段,从头开始重新定义项目和业务逻辑。
无论你在机器学习(ML)项目中扮演什么角色,关键是要意识到事情不会按照计划进行,从一开始就接受这种不确定性,并将其转化为你的优势。这对管理利益相关者(期望、信任)以及你自己和团队其他成员(动力、挫败感)都很重要。如何做到?
-
避免过于雄心勃勃的时间或交付限制,通过确保机器学习项目被看作它们真正的样子:需要探索未知的创新,具有高风险,但也有高回报和潜力。
-
知道何时停止,通过平衡每次增量改进的价值(机器学习模型总是可以改进的!)与其在时间、精力和机会成本方面的代价。
-
准备好调整方向并接受失败,通过不断利用项目带给你的学习和洞察,决定修改项目范围,甚至如果新的学习结果要求,就干脆终止项目。
与合适的人为伍

每个项目都始于人。合适的人选、技能、视角的结合,以及一个能赋能你的网络。
曾几何时,机器学习(ML)模型仅限于数据科学家的笔记本电脑。如今,机器学习的真正潜力是在模型被部署并集成到公司流程中时得以实现。这意味着更多的人和技能需要合作,以使这一切成为可能(数据科学家、机器学习工程师、后端开发人员、数据工程师等)。
第一步是确定成功构建端到端机器学习解决方案所需的技能和角色。然而,成功所需的不仅仅是一个涵盖技能清单的角色组。拥有一个多元化的团队,能够带来不同的视角并能够同情不同的用户群体,已经被证明能够帮助团队改进工作方式,构建更好的解决方案(“为什么拥有一个多元化的团队能让你的产品更好”)。
人们通常不会充分讨论这一点,但成功交付项目的关键人物远不止团队本身。我称这些人群体为“网络”。网络是你知道在特定领域非常擅长的人,是你在需要时可以信赖寻求帮助和建议的人,他们能帮助你和团队打破障碍、加速进程或赋能。网络可以是你的业务利益相关者、经理、技术人员、用户研究人员、其他团队的数据科学家、客户支持团队等……确保建立自己的网络,并识别出那个可以在每个特定情况下提供帮助的盟友。
从数据中学习
项目是一个持续的学习机会,许多时候,学习和洞察来源于检查正确的数据和监控。
在机器学习的各项工作中,有三个主要的指标和度量可以在学习和洞察方面带来巨大价值:模型性能监控、服务性能和最终影响监控。在之前的文章中,我深入探讨了这个话题。
对机器学习系统在生产环境中进行 360 度监控的方式
levelup.gitconnected.com](https://levelup.gitconnected.com/ml-systems-monitoring-from-service-performance-to-positive-business-impact-b40dbbd31927?source=post_page-----21c6f7432436--------------------------------)
在开发或部署机器学习(ML)解决方案时,检查正确的数据和监控是关键:
-
确保朝着正确的方向前进: 这包括从解决方案的正确设计或选择合适的特征,到了解是否需要调整方向甚至停止项目的多个方面。
-
了解如何改进: 了解是否达成了预期的结果目标(例如通过实验或 A/B 测试),深入分析哪些做得好,哪些没有做好,以及如何继续提供价值。
-
及时发现失败并制定计划: 以便快速响应问题,理想情况下是在它们影响业务之前。如果问题已经影响到业务,拥有正确的指标应该能让你理解失败背后的原因,保持事情在可控范围内,并制定前进的计划(同时维护利益相关者的信任)。
总结
从头到尾有效管理机器学习项目是一项复杂的任务,涉及多个维度。在这篇博文中,我分享了自己作为数据科学家以及最近作为机器学习产品经理的经验,阐述了我在处理机器学习项目时认为关键的因素:接受不确定性、与合适的人合作以及从数据中学习。
我希望这些见解能帮助你成功管理你的机器学习(ML)项目,并通过它们推动积极的影响。敬请期待更多关于机器学习与产品管理交集的文章 😃
使用 LLM 进行高效文档分块:一次解锁一个知识块
·发布于Towards Data Science ·阅读时长:8 分钟·2024 年 10 月 21 日
--

划分两个块的过程——作者插图
本文解释了如何使用 LLM(大语言模型)根据“想法”概念对文档进行分块。
我在这个示例中使用了 OpenAI 的 gpt-4o 模型,但同样的方法也可以应用于其他任何 LLM,如 Hugging Face、Mistral 等。
每个人都可以免费访问这篇文章。
文档分块的考虑事项
在认知心理学中,块代表一个“信息单元”。
这一概念也可以应用于计算:使用 LLM(大语言模型),我们可以分析文档并生成一组块,通常是可变长度的,每个块表达一个完整的“想法”。
这意味着系统将文档划分为“文本块”,每个块表达一个统一的概念,而不会在同一块中混合不同的想法。
目标是创建一个由独立元素组成的知识库,这些元素之间可以相互关联,而不会在同一块中重叠不同的概念。
当然,在分析和划分过程中,如果某个想法在不同的部分中重复或在同一文档中以不同的方式表达,可能会有多个块表达相同的想法。
开始使用
第一步是确定一个将成为我们知识库一部分的文档。
这通常是一个 PDF 或 Word 文档,可以逐页或逐段阅读并转换为文本。
为简单起见,假设我们已经有一个像以下这样的文本段落列表,这些段落是从 环游世界八十天 中提取的:
documents = [
"""On October 2, 1872, Phileas Fogg, an English gentleman, left London for an extraordinary journey.
He had wagered that he could circumnavigate the globe in just eighty days.
Fogg was a man of strict habits and a very methodical life; everything was planned down to the smallest detail, and nothing was left to chance.
He departed London on a train to Dover, then crossed the Channel by ship. His journey took him through many countries,
including France, India, Japan, and America. At each stop, he encountered various people and faced countless adventures, but his determination never wavered.""",
"""However, time was his enemy, and any delay risked losing the bet. With the help of his faithful servant Passepartout, Fogg had to face
unexpected obstacles and dangerous situations.""",
"""Yet, each time, his cunning and indomitable spirit guided him to victory, while the world watched in disbelief.""",
"""With one final effort, Fogg and Passepartout reached London just in time to prove that they had completed their journey in less than eighty days.
This extraordinary adventurer not only won the bet but also discovered that the true treasure was the friendship and experiences he had accumulated along the way."""
]
假设我们正在使用一个接受有限 token 数量的 LLM 作为输入和输出,我们将其称为 input_token_nr 和 output_token_nr。
对于此示例,我们将 input_token_nr 设置为 300,output_token_nr 设置为 250。
这意味着为了成功分割,提示和要分析的文档的 token 数量必须少于 300,而由 LLM 生成的结果必须消耗不超过 250 个 token。
使用 tokenizer 工具,我们看到我们的知识库文档由 254 个 token 组成。
因此,一次性分析整个文档是不可行的,因为尽管输入可以在一次调用中处理,但它无法适应输出。
因此,作为准备步骤,我们需要将原始文档划分为不超过 250 个 token 的块。
然后,这些块将传递给 LLM,LLM 会进一步将其拆分成小块。
为了谨慎起见,我们将最大块大小设置为 200 个 token。
生成块
生成块的过程如下:
-
考虑知识库(KB)中的第一个段落,确定它所需的 token 数量,如果小于 200,它就成为块的第一个元素。
-
分析下一个段落的大小,如果与当前块的大小合并后少于 200 个 token,就将其添加到块中,并继续处理剩余的段落。
-
当尝试添加另一个段落时,如果导致块的大小超过限制,则该块达到了最大尺寸。
-
从第一步开始,重复执行,直到所有段落都被处理完毕。
块生成过程假设为了简单起见,每个段落小于最大允许的大小(否则,段落本身必须拆分成更小的元素)。
为了执行此任务,我们使用来自 LLMChunkizerLib/chunkizer.py 库的 llm_chunkizer.split_document_into_blocks 函数,可以在以下仓库中找到该库 — LLMChunkizer。
从视觉上看,结果如图 1 所示。

图 1 — 将文档拆分成最大为 200 个 token 的块 — 图像由作者提供
在生成块时,唯一需要遵守的规则是不得超过最大允许的大小。
不对文本的含义进行分析或假设。
生成小块
下一步是将块拆分为每个表达相同思想的块。
对于此任务,我们使用来自 LLMChunkizerLib/chunkizer.py 库的 llm_chunkizer.chunk_text_with_llm 函数,该库也可以在同一个仓库中找到。
结果如图 2 所示。

图 2 — 将块拆分成小块 — 图像由作者提供
这个过程是线性进行的,允许 LLM 自由决定如何形成块。
处理两个块之间的重叠
如前所述,在块拆分过程中,仅考虑长度限制,而不考虑表达相同想法的相邻段落是否被拆分到不同块中。
如图 1 所示,“bla bla bla”概念(代表一个统一的想法)被拆分到两个相邻的块之间。
如图 2 所示,块分割器一次只处理一个块,这意味着 LLM 无法将此信息与下一个块关联(它甚至不知道存在下一个块),因此,将其放入最后一个拆分的块中。
这个问题在导入过程中经常发生,特别是在导入一个无法完全放入单个 LLM 提示中的长文档时。
为了解决这个问题,llm_chunkizer.chunk_text_with_llm 如图 3 所示:
-
从前一个块生成的最后一个块(或最后 N 个块)将从“有效”块列表中移除,并将其内容添加到下一个要拆分的块中。
-
New Block2 再次传递给分块函数。

图 3 — 处理重叠 — 图片由作者提供
如图 3 所示,块 M 的内容被更有效地拆分为两个块,保持了概念“bla bla bla”的连贯性。
这个解决方案背后的思想是,前一个块的最后 N 个块代表独立的想法,而不仅仅是无关的段落。
因此,将它们添加到新块中,可以让 LLM 生成类似的块,同时创建一个新块,将之前无视意义而被拆分的段落重新联合。
分块结果
最后,系统生成以下 6 个块:
0: On October 2, 1872, Phileas Fogg, an English gentleman, left London for an extraordinary journey. He had wagered that he could circumnavigate the globe in just eighty days. Fogg was a man of strict habits and a very methodical life; everything was planned down to the smallest detail, and nothing was left to chance.
1: He departed London on a train to Dover, then crossed the Channel by ship. His journey took him through many countries, including France, India, Japan, and America. At each stop, he encountered various people and faced countless adventures, but his determination never wavered.
2: However, time was his enemy, and any delay risked losing the bet. With the help of his faithful servant Passepartout, Fogg had to face unexpected obstacles and dangerous situations.
3: Yet, each time, his cunning and indomitable spirit guided him to victory, while the world watched in disbelief.
4: With one final effort, Fogg and Passepartout reached London just in time to prove that they had completed their journey in less than eighty days.
5: This extraordinary adventurer not only won the bet but also discovered that the true treasure was the friendship and experiences he had accumulated along the way.
关于块大小的考虑
让我们看看当原始文档被拆分成最大大小为 1000 个标记的较大块时会发生什么。
使用较大的块大小时,系统生成 4 个块而不是 6 个。
这种行为是预期的,因为 LLM 可以一次分析更大部分的内容,并能够使用更多的文本来表示一个单一的概念。
下面是此情况下的块:
0: On October 2, 1872, Phileas Fogg, an English gentleman, left London for an extraordinary journey. He had wagered that he could circumnavigate the globe in just eighty days. Fogg was a man of strict habits and a very methodical life; everything was planned down to the smallest detail, and nothing was left to chance.
1: He departed London on a train to Dover, then crossed the Channel by ship. His journey took him through many countries, including France, India, Japan, and America. At each stop, he encountered various people and faced countless adventures, but his determination never wavered.
2: However, time was his enemy, and any delay risked losing the bet. With the help of his faithful servant Passepartout, Fogg had to face unexpected obstacles and dangerous situations. Yet, each time, his cunning and indomitable spirit guided him to victory, while the world watched in disbelief.
3: With one final effort, Fogg and Passepartout reached London just in time to prove that they had completed their journey in less than eighty days. This extraordinary adventurer not only won the bet but also discovered that the true treasure was the friendship and experiences he had accumulated along the way.
结论
进行多次块分割尝试非常重要,每次都改变传递给块分割器的块大小。
每次尝试后,应该审查结果,以确定哪种方法最符合预期的结果。
敬请期待
在下一篇文章中,我将展示如何使用 LLM 来检索块 — LLMRetriever。
你可以在我的仓库中找到所有代码和更多示例 — LLMChunkizer。
如果你想进一步讨论,欢迎通过LinkedIn与我联系。
通过 CMA-ES(协方差矩阵适应进化策略)进行高效特征选择
使用进化算法进行大数据集的快速特征选择
·发表于Towards Data Science ·阅读时间 11 分钟·2024 年 1 月 12 日
--
这是关于特征选择的两部分系列中的第一部分。阅读 第二部分在这里**。
想了解更多 CMA-ES 应用的例子,请查阅 Nomura 和 Shibata 的这篇论文;这篇文章在论文中作为通过 CMA-ES 和 Margin 优化的一个值得注意的应用被提到(参考文献[6])。
当你将模型拟合到数据集时,可能需要进行特征选择:只保留某些特征子集来拟合模型,同时丢弃其余的特征。这可能出于多种原因是必要的:
-
保持模型可解释性(特征过多会使解释变得更困难)
-
避免维度灾难
-
最大化/最小化与模型相关的某个目标函数(如 R 平方、AIC 等)
-
避免过拟合等问题
如果特征的数量 N 较少,那么进行穷举搜索是可行的:你可以字面上尝试所有可能的特征组合,并保留最小化成本/目标函数的那个组合。但如果 N 较大,穷举搜索可能就不再可行。需要尝试的组合总数是2^N,如果 N 超过几十个,计算量将变得无法承受——这是一个指数函数。在这种情况下,你必须使用启发式方法:以高效的方式探索搜索空间,寻找能够最小化你用来进行搜索的目标函数的特征组合。
你要找的是一个长度为 N 的向量[1, 0, 0, 1, 0, 1, 1, 1, 0, ...],其中的元素取值为{0, 1}。向量中的每个元素都分配给一个特征;如果元素为 1,则选择该特征;如果元素为 0,则丢弃该特征。你需要找到最小化目标函数的向量。搜索空间的维度与特征数量 N 一样多;沿着任何维度的可能值只有 0 和 1。
找到一个好的启发式算法并不是一件简单的事情。R 中的regsubsets()函数有一些选项可供使用。此外,scikit-learn 提供了几种方法来执行启发式特征选择,只要你的问题适合他们的技术。但是找到一个好的、通用的启发式算法——以最一般的形式——是一个困难的问题。在本系列文章中,我们将探讨一些选项,即使 N 很大,目标函数可以是你可以在代码中计算的任何东西,只要不太慢,可能也能工作得相当好。
数据集和完整代码
对于本系列文章中的所有优化技术,我正在使用在 Kaggle 上非常流行的房价数据集(MIT 许可证)——经过一些简单的特征转换后,它最终具有 213 个特征(N=213)和 1453 个观测值。我使用的模型是线性回归,statsmodels.api.OLS(),我试图最小化的目标函数是 BIC——贝叶斯信息准则——一个衡量信息损失的指标,因此较低的 BIC 值更好。它类似于 AIC——阿凯克信息准则——但 BIC 倾向于产生更简洁的模型:它更喜欢具有较少特征的模型。最小化 AIC 或 BIC 倾向于减少过拟合。但也可以使用其他目标函数,例如 R 平方(目标中解释的方差)或调整后的 R 平方——只需记住,较大的 R 平方值更好,因此这是一个最大化问题。
最终,这里的目标函数的选择是无关紧要的。重要的是,我们有一个我们一直试图使用各种技术来优化的目标函数。
在本系列文章中使用的完整代码包含在我的特征选择存储库中的一个笔记本中——也在末尾链接。我将在这里的文本中提供代码片段,但请查看笔记本获取完整上下文。
我们将尝试通过特征选择来最小化 BIC,因此这是在进行任何特征选择之前从statsmodels.api.OLS()中得到的 BIC 的基线值——启用所有特征时:
baseline BIC: 34570.166173470934
现在让我们来检查一个众所周知的、经过验证的特征选择技术,我们将与后面描述的更复杂的技术进行比较。
SFS——顺序特征搜索
SFS,前向版本,相当简单。它从尝试选择单个特征开始,并选择最小化目标函数的特征。一旦选择了一个特征,它将永远保持选择状态。然后尝试添加另一个特征(总共 2 个特征),以最小化目标。每次增加一个已选择特征的数量,尝试找到最佳的新特征添加到现有集合中。当所有特征一起尝试时,搜索结束。无论哪种组合最小化目标,都获胜。
SFS 是一种贪婪算法 — 每个选择都是局部最优的 — 它永远不会回头纠正自己的错误。但即使 N 相当大时,它也相当快。它尝试的总组合数为N(N+1)/2,这是一个二次多项式(而穷举搜索需要执行指数数量的试验)。
让我们看看 SFS 代码在 Python 中可能是什么样子,使用mlxtend 库:
import statsmodels.api as sm
from mlxtend.feature_selection import SequentialFeatureSelector as SFS
from sklearn.base import BaseEstimator
class DummyEstimator(BaseEstimator):
# mlxtend wants to use an sklearn estimator, which is not needed here
# (statsmodels OLS is used instead)
# create a dummy estimator to pacify mlxtend
def fit(self, X, y=None, **kwargs):
return self
def neg_bic(m, X, y):
# objective function
lin_mod_res = sm.OLS(y, X, hasconst=True).fit()
return -lin_mod_res.bic
seq_selector = SFS(
DummyEstimator(),
k_features=(1, X.shape[1]),
forward=True,
floating=False,
scoring=neg_bic,
cv=None,
n_jobs=-1,
verbose=0,
# make sure the intercept is not dropped
fixed_features=['const'],
)
n_features = X.shape[1] - 1
objective_runs_sfs = round(n_features * (n_features + 1) / 2)
t_start_seq = time.time()
# mlxtend will mess with your dataframes if you don't .copy()
seq_res = seq_selector.fit(X.copy(), y.copy())
t_end_seq = time.time()
run_time_seq = t_end_seq - t_start_seq
seq_metrics = seq_res.get_metric_dict()
它快速运行组合,以下是摘要结果:
best k: 36
best objective: 33708.98602877906
R2 @ best k: 0.9075677543649224
objective runs: 22791
total run time: 42.448 sec
最佳特征数量是 213 个中的 36 个。最佳 BIC 为 33708.986(特征选择之前的基准值为 34570.166),在我的系统上不到 1 分钟即可完成。它调用目标函数 22800 次。
这些是作为特征选择数量函数的最佳 BIC 和 R-squared 值:

SFS 的 BIC 和 R-squared
欲了解更多信息,例如实际选择的特征名称,请查看存储库中的笔记本。
现在让我们尝试一些更复杂的东西。
CMA-ES(协方差矩阵适应进化策略)
这是一个数值优化算法。它与遗传算法(它们都是进化算法)属于同一类,但 CMA-ES 与 GA 有很大不同。该算法是随机的,不需要计算目标函数的导数(不像梯度下降依赖于偏导数)。它在计算上是高效的,并且被用于各种数值优化库,如 Optuna。我将在这里尝试对 CMA-ES 进行简要介绍;更详细的解释,请参考末尾链接部分的文献。
考虑二维 Rastrigin 函数:

Rastrigin 函数
下面的热图显示了该函数的值 — 较亮的颜色表示较高的值。该函数在原点(0, 0)处具有全局最小值,但它布满许多局部极值。我们需要通过 CMA-ES 找到全局最小值。

Rastrigin 函数热图
CMA-ES 基于多元正态分布。它从这个分布中生成搜索空间中的测试点。你需要猜测分布的原始均值和标准差,但之后算法会迭代地修改所有这些参数,在搜索空间中扫荡分布,寻找最佳的目标函数值。以下是测试点所来自的原始分布:

CMA-ES 分布
xi 是算法在每一步生成的点集,位于搜索空间中。lambda 是生成的点数。分布的均值将在每一步更新,并且希望最终能收敛到真实解。sigma 是分布的标准差——即测试点的分布范围。C 是协方差矩阵:它定义了分布的形状。根据C的值,分布可能呈“圆形”或更拉长的椭圆形。对C的修改使得 CMA-ES 可以“悄悄”进入搜索空间中的某些区域,或避开其他区域。

测试点的第一次生成。
上图生成了 6 个点,这是优化器为此问题选择的默认种群大小。这是第一步。之后,算法需要:
-
为每个点计算目标函数(Rastrigin)。
-
更新均值、标准差和协方差矩阵,实际上基于从目标函数中学到的信息,创建一个新的多元正态分布。
-
从新的分布中生成一组新的测试点。
-
重复直到某个条件满足(无论是收敛到某个均值,还是超过最大步数等)。
我在这里不会展示所有分布参数的更新过程,否则这篇文章会变得很长——请查看文末的链接以获得完整的解释。但仅更新分布的均值是相当简单的,具体过程如下:在计算每个测试点的目标函数后,会给点赋予权重,权重较大的点会分配给目标值较好的点,并从这些点的位置计算加权和,作为新的均值。实际上,CMA-ES 将分布的均值朝着具有更好目标值的点移动:

更新 CMA-ES 分布的均值
如果算法收敛到真实解,则分布的均值将收敛到该解。标准差将收敛到 0。协方差矩阵将根据目标函数的地理特征改变分布的形状(圆形或椭圆形),扩展到有前景的区域,并避开不良区域。
这里是一个动画 GIF,展示了 CMA-ES 在求解 Rastrigin 问题时测试点随时间演变的过程:

动画展示了 CMA-ES 的收敛过程
CMA-ES 用于特征选择
2D Rastrigin 函数相对简单,因为它只有 2 个维度。而对于我们的特征选择问题,维度是 N=213。此外,空间不是连续的。每个测试点是一个 N 维向量,其组件值来自 {0, 1}。换句话说,每个测试点看起来像这样:[1, 0, 0, 1, 1, 1, 0, ...] —— 一个二进制向量。但除此之外,问题是一样的:我们需要找到那些最小化目标函数的点(或向量):OLS 模型的 BIC 参数。
这是一个用于特征选择的 CMA-ES 代码的简单版本,使用了 cmaes 库:
def cma_objective(fs):
features_use = ['const'] + [
f for i, f in enumerate(features_select) if fs[i,] == 1
]
lin_mod = sm.OLS(y_cmaes, X_cmaes[features_use], hasconst=True).fit()
return lin_mod.bic
X_cmaes = X.copy()
y_cmaes = y.copy()
features_select = [f for f in X_cmaes.columns if f != 'const']
dim = len(features_select)
bounds = np.tile([0, 1], (dim, 1))
steps = np.ones(dim)
optimizer = CMAwM(
mean=np.full(dim, 0.5),
sigma=1 / 6,
bounds=bounds,
steps=steps,
n_max_resampling=10 * dim,
seed=0,
)
max_gen = 100
best_objective_cmaes_small = np.inf
best_sol_raw_cmaes_small = None
for gen in tqdm(range(max_gen)):
solutions = []
for _ in range(optimizer.population_size):
x_for_eval, x_for_tell = optimizer.ask()
value = cma_objective(x_for_eval)
solutions.append((x_for_tell, value))
if value < best_objective_cmaes_small:
best_objective_cmaes_small = value
best_sol_raw_cmaes_small = x_for_eval
optimizer.tell(solutions)
best_features_cmaes_small = [
features_select[i]
for i, val in enumerate(best_sol_raw_cmaes_small.tolist())
if val == 1.0
]
print(f'best objective: {best_objective_cmaes_small}')
print(f'best features: {best_features_cmaes_small}')
CMA-ES 优化器使用一些初始猜测值来定义均值和标准差(sigma)。然后它循环多个代,创建测试点 x_for_eval,用目标函数评估它们,修改分布(均值、sigma、协方差矩阵)等。每个 x_for_eval 点是一个二进制向量 [1, 1, 1, 0, 0, 1, ...],用于从数据集中选择特征。
请注意,使用的是 CMAwM() 优化器(带有边际的 CMA),而不是默认的 CMA()。默认优化器适用于常规的、连续的问题,但这里的搜索空间是高维的,并且只允许两个离散值(0 和 1)。默认优化器在这个空间中会卡住。CMAwM() 略微扩展了搜索空间(尽管它返回的解仍然是二进制向量),这似乎足以解锁它。
这个简单的代码确实有效,但远非最优。在附带的笔记本中,我有一个更复杂、优化过的版本,能够更快地找到更好的解。但由于代码比较大,我不会在这里展示 —— 请查看笔记本。
下图展示了复杂、优化过的 CMA-ES 代码的历史,寻找最佳解的过程。热力图显示了每一代中特征的流行度(亮色 = 更受欢迎)。你可以看到某些特征始终非常受欢迎,而其他特征很快就过时,还有一些特征则是在后期“被发现”的。优化器选择的人口规模,根据该问题的参数,是 20 个点(个体),因此特征的流行度是在这 20 个点之间进行平均的。

CMA-ES 优化历史
以下是优化后的 CMA-ES 代码的主要统计数据:
best objective: 33703.070530508514
best generation: 921
objective runs: 20000
time to best: 48.326 sec
它能够找到比 SFS 更好的(更小的)目标值,调用目标函数的次数更少(20k 次),且所需时间大致相同。这在所有度量标准下都比 SFS 有明显的进展。
再次提醒,任何特征选择前的基准 BIC 是:
baseline BIC: 34570.166173470934
旁注:在研究传统优化算法(遗传算法、模拟退火等)后,CMA-ES 给我带来了惊喜。它几乎没有超参数,计算开销轻量,仅需要一个小规模的个体(点)群体来探索搜索空间,然而它的表现相当不错。如果你需要解决优化问题,值得将其加入工具箱中。
这是关于特征选择的两部分系列的第一部分。阅读 第二部分在这里**。
备注和链接
感谢 cmaes 团队的解锁支持,你们的解释真的帮了我大忙!
所有图片均由作者创建。
包含所有代码的代码库:github.com/FlorinAndrei/fast_feature_selection
房价数据集(MIT 许可证):www.kaggle.com/c/house-prices-advanced-regression-techniques/data
mlxtend 库:github.com/rasbt/mlxtend
cmaes 库:github.com/CyberAgentAILab/cmaes
cmaes:一个简单但实用的 Python 库,用于 CMA-ES — 由 Nomura M. 和 Shibata M.(2024)撰写的论文,描述了 CMA-ES 作为优化算法的实际应用:arxiv.org/abs/2402.01373
CMA 演化策略:教程 — 由 Hansen N.(2016)撰写的论文,详细描述了 CMA-ES:arxiv.org/abs/1604.00772
Wikipedia 关于 CMA-ES 的条目: en.wikipedia.org/wiki/CMA-ES
高效特征选择:基于遗传算法
使用进化算法进行大数据集的快速特征选择
·发布于Towards Data Science ·8 分钟阅读·2024 年 1 月 12 日
--
这是关于特征选择的两部分系列文章的最后一部分。阅读 第一部分在这里**.
简要回顾:在将模型拟合到数据集时,你可能希望选择特征的子集(而不是使用所有特征),原因有很多。但是即使你有一个明确的目标函数来搜索最佳特征组合,如果特征数量 N 非常大,搜索可能会花费很长时间。寻找最佳组合并不总是容易的。暴力搜索通常无法处理超过几十个特征的情况。需要启发式算法来执行更高效的搜索。
如果你有 N 个特征,你要寻找的是一个 N 长度的向量[1, 1, 0, 0, 0, 1, ...],其中的值来自{0, 1}。每个向量分量对应一个特征。0 表示该特征被拒绝,1 表示该特征被选择。你需要找到那个最小化你使用的成本/目标函数的向量。
在上一篇文章中,我们介绍了一个经典算法——SFS(序列特征搜索),并将其与一种高效的进化算法 CMA-ES 进行了比较。我们从 Kaggle 的房价数据集开始,该数据集经过处理后包含 213 个特征和 1453 条观测数据。我们尝试拟合的模型是statsmodels.api.OLS(),目标函数是模型的 BIC——贝叶斯信息准则,它衡量信息损失。较低的 BIC 意味着更好的拟合,因此我们正在尝试最小化这个目标。
在本文中,我们将讨论另一种进化技术:遗传算法。背景(数据集、模型、目标)保持不变。
遗传算法 — Genetic Algorithms
遗传算法的灵感来源于生物进化和自然选择。在自然界中,生物体(宽泛地说)是根据那些有助于生存和繁殖成功的基因(特征)在其所处环境中的适应性被“选择”的。
现在想想特征选择。你有 N 个特征。你试图找到长度为 N 的二进制向量[1, 0, 0, 1, 1, 1, ...],以选择特征(0 = 特征被排除,1 = 特征被包含),从而最小化成本/目标函数。
每个这样的向量可以看作是一个“个体”。每个向量的组成部分(值为 0 或 1)就是一个“基因”。通过合理地应用进化和选择,可能可以使一群个体进化,从而接近我们感兴趣的目标函数的最佳值。
简而言之,遗传算法是这样的。首先生成一个个体种群(向量),每个向量的长度为 N。向量的组成部分(基因)从{0, 1}中随机选择。在下图中,N=12,种群规模为 8。

遗传算法种群
在种群生成之后,通过目标函数评估每个个体。
现在进行选择:保留目标值最好的个体,淘汰那些目标值最差的个体。这里有许多可能的策略,从简单的排名选择(反直觉地,这种方法效果不佳)到随机锦标赛选择,这种方法在长期内非常高效。如果你记得探索与开发的困境,那么在遗传算法中,很容易陷入过于简单的开发陷阱,导致探索变慢。遗传算法的核心是探索。这里有一份简短的选择技术列表,并且可以查看文末的链接获取更多信息。
一旦选择出最优的个体,并淘汰掉不适应的个体,就该通过两种技术:交叉和突变,引入基因池中的变异。
交叉的过程完全像自然界中的交配一样,两个生物体交配并产生后代:来自父母的遗传物质在后代中“混合”,并带有一定的随机性。

遗传算法交叉
突变再次发生时,基本上就像自然界中基因物质发生随机突变一样,新值被引入基因库,从而增加了基因库的多样性。

遗传算法突变
经过这一切,算法会回到循环:再次通过目标函数评估个体,进行选择,然后是交叉、突变等。
可以使用各种停止准则:如果目标函数在若干代中没有改善,循环可能会终止。或者你可以设置一个硬性停止条件来限制评估的代数。或者使用基于时间的停止,或者等待外部信号等。无论如何,目标值最好的个体应被认为是问题的解。
关于精英策略的几点说明:使用如锦标赛等随机选择技术时,代际中最优秀的个体可能会因为纯粹的偶然而被淘汰——这种情况不太可能,但确实会发生。精英策略绕过了这个问题,直接规定最优秀的个体必须生存下来,无论如何。精英策略是一种利用技巧。它可能会导致算法陷入局部极值,错失全局解。再说一遍,遗传算法的核心是探索。根据我有限的 GA 经验,似乎表明利用偏向对 GA 并不有利。但你可以根据自己的需求调整;如果你喜欢实验不同的算法变种,GA 为你提供了很多机会。
遗传算法有几个超参数可以调节:
-
种群大小(个体数量)
-
变异概率(每个个体,每个基因)
-
交叉概率
-
选择策略等
通过手动尝试不同超参数值来进行实验是找出最佳代码的一种方式。或者,你可以将 GA 封装在 Optuna 中,让 Optuna 找到最佳超参数——但这在计算上比较昂贵。
用于特征选择的遗传算法(GA),在代码中
这是一个可以用于特征选择的简单 GA 代码。它使用了deap 库,这个库非常强大,但学习曲线可能较陡。然而,这个简单版本应该足够清晰。
# to maximize the objective
# fitness_weights = 1.0
# to minimize the objective
fitness_weights = -1.0
# copy the original dataframes into local copies, once
X_ga = X.copy()
y_ga = y.copy()
# 'const' (the first column) is not an actual feature, do not include it
X_features = X_ga.columns.to_list()[1:]
try:
del creator.FitnessMax
del creator.Individual
except Exception as e:
pass
creator.create("FitnessMax", base.Fitness, weights=(fitness_weights,))
creator.create(
"Individual", array.array, typecode='b', fitness=creator.FitnessMax
)
try:
del toolbox
except Exception as e:
pass
toolbox = base.Toolbox()
# Attribute generator
toolbox.register("attr_bool", random.randint, 0, 1)
# Structure initializers
toolbox.register(
"individual",
tools.initRepeat,
creator.Individual,
toolbox.attr_bool,
len(X_features),
)
toolbox.register("population", tools.initRepeat, list, toolbox.individual)
def evalOneMax(individual):
# objective function
# create True/False selector list for features
# and add True at the start for 'const'
cols_select = [True] + [i == 1 for i in list(individual)]
# fit model using the features selected from the individual
lin_mod = sm.OLS(y_ga, X_ga.loc[:, cols_select], hasconst=True).fit()
return (lin_mod.bic,)
toolbox.register("evaluate", evalOneMax)
toolbox.register("mate", tools.cxTwoPoint)
toolbox.register("mutate", tools.mutFlipBit, indpb=0.05)
toolbox.register("select", tools.selTournament, tournsize=3)
random.seed(0)
pop = toolbox.population(n=300)
hof = tools.HallOfFame(1)
pop, log = algorithms.eaSimple(
pop, toolbox, cxpb=0.5, mutpb=0.2, ngen=10, halloffame=hof, verbose=True
)
best_individual_ga_small = list(hof[0])
best_features_ga_small = [
X_features[i] for i, val in enumerate(best_individual_ga_small) if val == 1
]
best_objective_ga_small = (
sm.OLS(y_ga, X_ga[['const'] + best_features_ga_small], hasconst=True)
.fit()
.bic
)
print(f'best objective: {best_objective_ga_small}')
print(f'best features: {best_features_ga_small}')
代码创建了定义个体和整个种群的对象,并包含用于评估(目标函数)、交叉/配对、变异和选择的策略。它从一个 300 个体的种群开始,然后调用eaSimple()(一个简单的交叉、变异、选择序列),只运行 10 代,为了简化。定义了一个大小为 1 的名人堂,其中最优秀的个体被保存下来,避免在选择等过程中意外被变异或跳过。
名人堂不是精英策略。名人堂复制了种群中最优秀的个体,并只保留一个非活跃的副本在储存中。精英策略则会在每一代中保留最优秀的个体。
这个简单代码易于理解,但效率低下。查看仓库中的笔记本,那里有一个更复杂的 GA 代码版本,我不会在这里引用。不过,从笔记本中运行更复杂、优化过的代码,经过 1000 代,产生了这些结果:
best objective: 33705.569572544795
best generation: 787
objective runs: 600525
time to best: 158.027 sec
再次提醒,任何特征选择前的基准 BIC 是:
baseline BIC: 34570.166173470934
这里是完整的优化过的 GA 代码历史,从笔记本中运行,进行了 1000 代的演化,尝试寻找最佳特征。由左至右,热图显示了各特征在不同代中的受欢迎程度(色块越亮=越受欢迎)。你可以看到一些特征始终受到青睐,另一些特征很快就被淘汰,而其他特征则随着时间的推移可能会变得更受欢迎或逐渐失去关注。

GA 优化历史
方法比较
我们尝试了三种不同的技术:SFS、CMA-ES 和 GA。它们在找到最佳目标值以及所需时间方面如何比较?
这些测试是在一台配备 AMD Ryzen 7 5800X3D(8/16 核心)的机器上进行的,运行的是 Ubuntu 22.04 和 Python 3.11.7。SFS 和 GA 通过一个 16 个工作线程的多进程池运行目标函数。CMA-ES 是单进程的——尝试多进程运行时似乎没有显著提升,但我相信如果更多的工作集中在使算法并行化上,结果可能会有所不同。
这些是运行时间。对于 SFS 来说是总运行时间,对于 CMA-ES 和 GA 来说是达到最佳解的时间。时间越短越好。
SFS: 42.448 sec
GA: 158.027 sec
CMA-ES: 48.326 sec
目标函数调用的次数——次数越少越好:
SFS: 22791
GA: 600525
CMA-ES: 20000
相较于基准,目标函数找到的最佳值——值越小越好:
baseline BIC: 34570.1662
SFS: 33708.9860
GA: 33705.5696
CMA-ES: 33703.0705
GA 能够在目标函数上击败 SFS,利用尽可能多的 CPU 核心运行目标函数,但它是最慢的。它调用目标函数的次数是其他方法的一个数量级以上。进一步的超参数优化可能会改善结果。
SFS 运行迅速(在所有 CPU 核心上运行),但其性能适中。它也是最简单的算法。
如果你只是想快速估算最佳特征集,使用简单算法,SFS 还不错。
另一方面,如果你追求最佳的目标值,CMA-ES 似乎是最好的选择。
这是关于特征选择的两部分系列的最后一部分。阅读 第一部分这里**。
注意事项和链接
所有图像均由作者制作。
包含所有代码的代码库:github.com/FlorinAndrei/fast_feature_selection
房价数据集(MIT 许可证):www.kaggle.com/c/house-prices-advanced-regression-techniques/data
deap 库:github.com/DEAP/deap
一份免费的遗传算法教程:www.tutorialspoint.com/genetic_algorithms/index.htm
使用 PyTorch 实现高效的大维度自组织映射
因为自组织是有趣的
·发表于Towards Data Science ·5 分钟阅读·2024 年 12 月 13 日
--
自组织映射(或称 Kohonen 映射)是一种有趣的神经网络类型:它们的架构与传统神经网络不同,训练方式也与通常的反向传播方法完全不同。这样做是有充分理由的:它们是为无监督学习设计的。它们对于常见的多层神经网络就像 K-Means 对 SVM 一样。它们用于创建聚类;将数据空间离散化。但是,它们与其他聚类方法不同的地方在于:它们创建的聚类形成了数据的地图(聚类的网格),地图中各个聚类之间的距离代表了这些聚类在数据空间中成员之间的平均距离。
由于自组织映射(SOM)稍显特殊,相比其他神经网络形式,关于创建高效自组织映射实现的工作并不多,特别是在使它们能够在 GPU 上处理高维数据方面(即它们通常用于特征不超过几十个的数据)。遗憾的是,这正是我在项目中所需要的:快速的 SOM 训练,处理成千上万特征的数据。我尝试过现有的库,包括基于 PyTorch 的库,但并不完全满意,因此我自己做了一个:ksom(说实话也是因为做这件事很有趣,特别是作为提高 PyTorch 使用技能的一种方式)。
使用 SSD 和 YoLO 模型的高效目标检测——初学者综合指南(第三部分)
了解单阶段目标检测模型及其不同的权衡
·发表于 Towards Data Science ·阅读时间 7 分钟·2024 年 3 月 8 日
--

图片来源:israel palacio 来自 Unsplash
在这篇关于目标检测模型的初学者指南系列中,我们已经涵盖了目标检测基础(第一部分)以及基于 R-CNN 的目标检测模型(第二部分)。在本文中,我们将重点介绍一些著名的单阶段目标检测模型。这些模型在推理速度上大大优于多阶段检测器,但在 mAP 和其他检测指标上稍显不足。让我们深入了解这些模型的细节。
单次多框检测器
单次多框检测器(SSD)架构由 Liu 等人于 2016 年提出,作为一种高性能的单阶段目标检测模型。该论文提出的模型在性能(mAP 方面)上与 Faster R-CNN 相当,但在训练和推理过程中速度要快得多。
R-CNN 家族与 SSD 之间的主要区别在于缺少区域提议组件(RPN)。SSD 系列模型并不从选择性搜索算法或 RPN 开始寻找 ROI。SSD 采用卷积方法来完成这一物体检测任务。它生成一个预定义数量的边界框及其对应的类别分数作为最终输出。它从一个大型的预训练网络(如 VGG-16)开始,在任何分类层开始之前就会被截断。这被称为 SSD 术语中的基础网络。基础网络后面跟着一个独特的辅助结构,用以生成所需的输出。以下是关键组件:
-
多尺度特征图:基础网络之后的辅助结构是一系列卷积层。这些层逐步减小特征图的尺度或分辨率。这样可以帮助检测不同大小(相对于图像)的物体。SSD 网络采用卷积方法来定义类别分数以及边界框的相对偏移值。例如,网络使用 3x3xp 大小的滤波器,在大小为 m x n x p 的特征图上进行操作,其中 p 是通道数。模型对 m x n 的每个单元格生成一个输出,滤波器应用于该位置。
-
默认锚框:网络利用一组预定义的锚框(具有不同的尺度和长宽比)。对于给定的大小为 m x n 的特征图,k 个此类默认锚框会应用于每个单元格。这些默认锚框在 SSD 中被称为先验框。对于每个单元格中的每个先验框,模型生成 c 个类别分数和 4 个边界框坐标。因此,对于一个大小为 m x n 的特征图,模型总共生成(c+4)kmn 个输出。这些输出是从网络不同深度的特征图中生成的,这是处理不同尺寸物体的一次性传递的关键。
图 1 展示了 SSD 的高层架构,其中基础网络为 VGG-16,后面跟着辅助卷积层,以支持多尺度特征图。

图 1:基于 VGG-16 的高层 SSD 架构。该架构展示了额外的特征层,用于检测不同大小的物体。来源:作者
如图 1 所示,模型生成了总共 8732 个预测,然后通过非最大抑制算法进行分析,最终为每个识别的物体得到一个边界框。在论文中,作者展示了两个变体 SSD-300 和 SSD-512 的性能指标(FPS 和 mAP),其中数字表示输入图像的大小。与 R-CNN 相比,两个变体都更快,并且在 mAP 方面表现相同,其中 SSD-300 比 SSD-512 有更高的 FPS。
正如我们刚才讨论的,SSD 在每个特征图上产生大量的输出。这会导致正负类之间的巨大不平衡(为了确保覆盖,假阳性的数量非常大)。为了解决这个问题以及其他一些细节,作者详细描述了困难负样本挖掘和数据增强等技术。我鼓励读者仔细阅读这篇精心撰写的论文,以获取更多细节。
你只看一次(YOLO)
2016 年,Redmon 等人在他们的论文《"You Only Look Once: Unified, Real-time Object Detection"》中提出了另一种流行的单阶段物体检测架构。这一架构大约与 SSD 同时出现,但采取了稍微不同的方式,通过单阶段模型来处理物体检测。就像 R-CNN 系列,YOLO 模型也随着时间的推移不断发展,后续版本在前一个版本的基础上有所改进。让我们首先了解这项工作的关键要点。
YOLO 的灵感来源于用于图像分类的GoogleNet架构。与 GoogleNet 类似,YOLO 使用了在 ImageNet 数据集上预训练的 24 层卷积层。预训练的网络使用 224x224 的训练图像,但训练完成后,模型会使用大小为 448x448 的重新缩放输入图像。这个重新缩放的做法是为了确保模型能够在不出现问题的情况下识别小物体和大物体。YOLO 首先将输入图像划分为一个 S x S 的网格(文中提到 PASCAL VOC 数据集使用的是 7x7 的网格)。网格中的每个单元预测 B 个边界框、物体置信度分数以及每个类别的置信度分数。因此,类似于 SSD,每个网格单元在 YOLO 中输出 4 个边界框的坐标和一个物体置信度分数,接着是 C 个类别的预测概率。总的来说,每个输入图像会得到 S x S x (B x 5 + C)个输出。输出的边界框数量非常高,类似于 SSD。通过 NMS 算法,这些边界框会被减少为每个物体的一个边界框。图 2 展示了 YOLO 的整体设置。

图 2:高层次的 YOLO 架构,使用了 24 层卷积层,后面接几个全连接层进行最终预测。来源:作者
如图 2 所示,YOLO 中的全连接层与完全卷积设计的 SSD 有所不同。YOLO 是使用一个名为 Darknet 的开源框架构建的,具有 45FPS 的推理速度。其速度是以牺牲检测精度为代价的。特别是,YOLO 在识别小物体以及物体重叠的情况下存在一定的局限性。
YOLOv2 或 YOLO-9000在 2017 年发布,具备检测 9000 个物体的能力(因此得名),每秒可以处理 45 到 90 帧!他们所做的一个小改动是,在简单地将输入重新缩放到 448x448 之前,增加了一个额外的步骤。具体来说,作者在原始分类模型(输入尺寸为 224x224)训练完成后,增加了一个步骤,将输入缩放到 448x448,并进一步微调。这使得模型能够更好地适应更大的分辨率,从而提升对小物体的检测性能。此外,使用的卷积模型是一个 30 层的 CNN。第二个改动是使用了锚框,并且这一实现尝试根据训练数据的特点来计算锚框的大小和数量(这与 SSD 不同,后者只是使用预定义的锚框列表)。最后一个变化是引入了多尺度训练,即作者不仅仅在某个固定尺寸下训练模型,而是在不同分辨率下进行训练,帮助模型学习不同尺寸物体的特征。这些改动在很大程度上提升了模型的性能(具体的数字和实验请参见论文)。
YOLOv3于 2018 年提出,以克服 YOLOv2 的 mAP 不足。该模型的第三个版本使用了比初始版本(24 层)更深的卷积网络,共有 53 层。另加上 53 层堆叠在预训练模型上用于检测任务。它还使用了残差块、跳跃连接和上采样层,以提升整体性能(需要注意的是,在前两个版本发布时,这些概念还不普遍使用)。为了更好地处理不同尺寸的物体,这个版本在网络的不同深度做出预测。YOLOv3 的架构如图 3 所示,供参考。

图 3:YOLOv3 高层架构,采用 Darknet-53 和多尺度预测分支。来源:作者
如图 3 所示,模型从第 79 层开始分支,并在第 82、94 和 106 层分别在尺度为 13x13、26x26 和 52x52 的位置进行大、中、小物体的预测。该模型使用了 9 个锚框,每个尺度使用 3 个锚框以处理不同的形状。这进一步增加了模型每个物体所做的预测总数。最后一步是应用 NMS(非极大值抑制),将输出结果减少为每个检测到的物体只有一个边界框。YOLOv3 引入的另一个关键改动是将类检测中的 softmax 替换为 sigmoid 损失函数。这一变化有助于处理物体重叠的场景。
尽管 YOLO 模型的原始作者 Joseph Redmon 已经停止了在目标检测[1]方面的工作,但整个计算机视觉社区并没有停滞不前。2020 年发布了后续版本YOLOv4,几周后又推出了一个名为YOLOv5的分支(请注意,这项工作没有官方的论文或出版物)。尽管是否应该将这些后续版本称为 YOLO 还有待讨论,但看到这些思想得到细化并不断发展是非常有趣的。在撰写本文时,YOLOv8已经可以供公众使用,而YOLOv9则进一步推动了效率和其他基准的提升。
这篇简短的介绍总结了不同的目标检测模型,包括多阶段模型和单阶段模型。我们已经覆盖了关键组件和主要贡献,以帮助更好地理解这些模型。还有许多其他实现方法,如SPP-Net、RetinaNet等,它们对目标检测任务有不同的处理方式。尽管不同,这些方法的思想仍然符合我们在本系列中讨论的总体框架。在下一篇文章中,让我们动手实践一些目标检测模型。
高效且可扩展的工具使用——LLM 代理
利用干净的抽象层次,避免工具膨胀提示大小,并提升代理的性能
·发表于Towards Data Science ·阅读时间 9 分钟·2024 年 6 月 6 日
--

LLM 代理是一个强大的框架,将 LLM 的性能提升到一个新的水平…
代理框架利用大型语言模型作为决策引擎,通过多阶段推理解决复杂任务。这种方法产生了令人印象深刻的结果,因为它将 LLM 的内部知识与通过用例定制工具进行行动的能力相结合。工具还使模型能够获取最新的领域特定知识,并与我们系统的其他部分进行交互。
通过创建多个特定角色的代理,框架可以进一步提升,能够通过协同工作实现最佳效果。例如,你可以将程序员、测试员、执行器和调试器组合在一起,比单一复杂的代理更高效地编写工作代码。
…然而,代理是“吃 token”的,这可能会推高成本和延迟
尽管代理框架具有令人印象深刻的能力,但它也有其缺点。我们调用代理时,大多数情况下需要预期至少双倍的 token 使用量和延迟,相比于…
使用 Python 高效测试 ETL 管道
如何即时检测数据质量问题并找出其根本原因
·发表于Towards Data Science ·阅读时间:10 分钟·2024 年 10 月 3 日
--

图片由Digital Buggu提供,来源于 Pexels.com
在今天的数据驱动型世界中,组织在做出关键业务决策时,极度依赖准确的数据。作为一名负责任和值得信赖的数据工程师,确保数据质量至关重要。即使在仪表板上短时间展示不正确的数据,也可能导致整个组织内部错误信息的迅速传播,就像病毒在生物体内的传播一样。
那么,我们如何预防这一问题呢?理想情况下,我们会完全避免数据质量问题的发生。然而,残酷的事实是,完全避免它们是不可能的。不过,我们可以采取两个关键措施来减轻其影响。
-
第一时间得知数据质量问题的出现
-
最小化解决问题所需的时间
在这篇博客中,我将展示如何直接在代码中实现第二点。我将使用 Mockaroo 生成的数据在 Python 中创建一个数据管道,并利用 Tableau 快速识别任何故障的原因。如果你正在寻找一个替代的测试框架,可以查看我关于Python 中 Great Expectations 简介的文章。
轻松的数据处理:使用 R 在多个数据文件中查找变量
一种带代码和工作流程的实用解决方案
Rodrigo M Carrillo Larco, MD, PhD
·发布于Towards Data Science ·阅读时间 7 分钟·2024 年 11 月 27 日
--
在数据集和无尽的数据字典的迷宫中迷失了?告别繁琐的变量查找吧!了解如何通过两个简单的 R 函数,快速识别并提取你所需要的变量,轻松处理多个 SAS 文件。简化你的工作流程,节省时间,让数据准备变得轻松愉快!

作为一名拥有超过七年健康数据处理经验的研究员,我经常收到包含大量数据集的文件夹。例如,想象一下打开一个包含 56 个 SAS 文件的文件夹,每个文件都有独特的数据(见下文示例)。如果你曾经遇到过这种情况,你一定知道那种沮丧的感觉:在成堆的文件中找到特定的变量,感觉就像是在大海捞针。

这张截图由作者拍摄,显示了本地文件夹。文件名已被模糊处理,以保护数据集的机密性。
初看起来,如果你已经知道你的感兴趣变量在哪里,这似乎不是个问题。但通常,你并不知道。虽然通常会提供数据字典,但它通常是一个 PDF 文档,列出了跨多页的变量。找到你需要的内容可能需要搜索(Ctrl+F)第 100 页上的某个变量,结果却发现数据集的名称出现在第 10 页。来回滚动浪费了很多时间。

作者拍摄的数据字典截图。变量名称和标签已被模糊处理,以保持数据集的机密性。
为了避免繁琐的过程,我创建了一个可重复的 R 工作流,可以读取文件夹中的所有数据集,生成一个包含变量名称及其标签的合并数据框(见下方示例),并识别每个变量的位置。这种方法使我的工作更快、更高效。以下是逐步操作的方法。

作者拍摄的names_labels数据集的截图(参见步骤 2)。变量名称和标签已被模糊处理,以保持数据集的机密性。
逐步指南
步骤 1:使用 get_names_labels 函数
首先,使用自定义函数get_names_labels(代码见本文末尾)。该函数需要您存储所有数据集的文件夹路径。
path_file <- "D:/folder1/folder2/folder3/folder_with_datasets/"
get_names_labels(path_file)
步骤 2:生成变量字典
get_names_labels函数将创建一个名为names_labels的数据框(如上例所示),其中包括:
· 变量名称(variable_name)
· 变量标签(variable_label)
· 变量所在的数据集名称(file_name)
根据数据集的数量和大小,这个过程可能需要一两分钟。
步骤 3:搜索变量
一旦生成了names_labels数据框,您就可以搜索所需的变量。筛选variable_name或variable_label列以定位相关术语。例如,如果您在寻找与性别相关的变量,它们可能会被标记为 sex、gender、is_male 或 is_female。
请注意,类似的变量可能出现在多个数据集中。例如,年龄可能出现在主问卷、临床数据集和实验室数据集中。这些变量看起来可能相同,但根据数据的收集方式和地点不同,可能会有所不同。例如:
· 主问卷中的年龄:从所有调查参与者收集。
· 临床/实验室数据集中的年龄:仅限于受邀进一步评估的子集或同意参与的人员。
在这种情况下,主问卷中的变量可能更能代表整个群体。
步骤 4:识别相关数据集
确定所需的变量后,筛选names_labels数据框以识别包含这些变量的原始数据集(file_name)。如果某个变量出现在多个数据集中(例如,ID),您需要确定哪个数据集包含您感兴趣的所有变量。
# Say you want these two variables
variables_needed <- c('ID', 'VAR1_A')
names_labels <- names_labels[which(names_labels$variable_name %in% variables_needed), ]
如果某个变量在多个原始数据集中都能找到(例如,ID),您需要筛选names_labels,只保留包含两个变量的原始数据集(例如,ID和VAR1_A)。在我们的例子中,names_labels数据框将只剩下两行,每行对应我们要查找的两个变量,它们都将在同一个原始数据集中找到。
names_labels <- names_labels %>%
group_by(file_name) %>%
mutate(count = n()) %>%
filter(count >= 2)
步骤 5:提取数据
现在,使用read_and_select函数(位于文末)。传入包含相关变量的原始数据集的名称。这个函数会在你的 R 环境中创建一个新的数据框,仅包含所选择的变量。例如,如果你的变量位于ABC.sas7bdat中,函数将创建一个名为ABC的新数据框,里面只包含那些变量。
unique(names_labels$file_name) # Sanity check, that there is only one dataframe
read_and_select(unique(names_labels$file_name)[1])
步骤 6:清理你的环境
为了保持工作空间整洁,删除不必要的元素,仅保留你需要的新数据框。例如,如果你的相关变量来自ABC.sas7bdat,你将保留过滤后的数据框ABC,它是read_and_select函数的输出结果。
length(unique(names_labels$file_name))
names_labels$file_name <- str_extract(names_labels$file_name, "[^.]+")
rm(list = setdiff(ls(), c(unique(names_labels$file_name))))
步骤 7:合并多个数据集(可选)
如果你的相关变量分布在多个数据集中(例如ABC和DEF),你可以将它们合并。使用唯一标识符,如ID,将数据集合并成一个单一的数据框。最终结果将是一个统一的数据框,包含你所需的所有变量。你将获得一个df数据框,里面包含所有观察值,但只有你需要的变量。
# Get a list with the names of the dataframes in the environment (“ABC” and “DEF”)
object_names <- ls()
# Get a list with the actual dataframe
object_list <- mget(object_names)
# Reduce the dataframes in the list (“ABC” and “DEF”) by merging conditional on the unique identifier (“ID”)
df <- Reduce(function(x, y) merge(x, y, by = "ID", all = TRUE), object_list)
# Clean your environment to keep only the dataframes (“ABC” and “DEF”) and a new dataframe “df” which will contain all the variables you needed.
rm(object_list, object_names)
为什么这个工作流程有效?
这种方法节省了时间,并将你的工作组织成一个单一、可重复的脚本。如果你后来决定添加更多变量,只需重新访问步骤 2 和 3,更新你的列表,并重新运行脚本。这种灵活性在处理大数据集时非常宝贵。虽然你仍然需要查阅文档以理解变量定义和数据收集方法,但这种工作流程减少了定位和准备数据的工作量。处理多个数据集不必让人感到不知所措。通过使用像get_names_labels和read_and_select这样的自定义函数,你可以简化数据准备的工作流程。
你是否在处理多个数据集时遇到过类似的挑战?如果有,欢迎在评论中分享你的想法或小贴士,或者如果你觉得这篇文章有帮助,给它点赞。让我们继续交流,互相学习!
以下是两个自定义函数。将它们保存在一个R脚本文件中,并在需要时将脚本加载到你的工作环境中。例如,你可以将文件保存为_Functions.R,以便随时访问。
# You can load the functions as
source('D:/Folder1/Folder2/Folder3/_Functions.R')
library(haven)
library(tidyverse)
library(stringr)
## STEPS TO USE THESE FUNCTIONS:
## 1\. DEFINE THE OBJECT 'PATH_FILE', WHICH IS A PATH TO THE DIRECTORY WHERE
## ALL THE DATASETS ARE STORED.
## 2\. APPLY THE FUNCTION 'get_names_labels' WITH THE PATH. THE FUNCTION WILL
## RETURN A DATAFRAME NAMES 'names_labels'.
## 3\. THE FUNCTION WILL RETURN A DATASET ('names_labels) SHOWING THE NAMES OF
## THE VARIABLES, THE LABELS, AND THE DATASET. VISUALLY/MANUALLY EXPLORE THE
## DATASET TO SELECT THE VARIABLES WE NEED. CREATE A VECTOR WITH THE NAMES
## OF THE VARIABLES WE NEED, AND NAME THIS VECTOR 'variables_needed'.
## 4\. FROM THE DATASET 'names_labels', KEEP ONLY THE ROWS WITH THE VARIABLES WE
## WILL USE (STORED IN THE VECTOR 'variables_needed').
## 5\. APPLY THE FUNCTION 'read_and_select' TO EACH OF THE DATASETS WITH RELEVANT
## VARIABLES. THIS FUNCTION WILL ONLY NEED THE NAME OF THE DATASET, WHICH IS
## STORED IN THE LAST COLUMN OF DATASET 'names_labels'.
### FUNCTION TO 1) READ ALL DATASETS IN A FOLDER; 2) EXTRACT NAMES AND LABELS;
### 3) PUT NAMES AND LABELS IN A DATASET; AND 4) RETURN THE DATASET. THE ONLY
### INPUT NEEDED IS A PATH TO A DIRECTORY WHERE ALL THE DATASETS ARE STORED.
get_names_labels <- function(path_file){
results_df <- list()
sas_files <- c(
list.files(path = path_file, pattern = "\\.sas7bdat$")
)
for (i in 1:length(sas_files)) {
print(sas_files[i])
# Read the SAS file
sas_data <- read_sas(paste0(path_file, sas_files[i]))
sas_data <- as.data.frame(sas_data)
# Get the variable names and labels
var_names <- names(sas_data)
labels <- sas_data %>%
map(~attributes(.)$label) %>%
map_chr(~ifelse(is.null(.), NA, .))
# Combine the variable names and labels into a data frame
var_df <- data.frame(
variable_name = var_names,
variable_label = labels,
file_name = sas_files[i],
stringsAsFactors = FALSE
)
# Append the results to the overall data frame
results_df[[i]] <- var_df
}
results_df <- do.call(rbind, results_df)
#return(results_df)
assign('names_labels', results_df, envir = .GlobalEnv)
}
################################################################################
### FUNCTION TO READ EACH DATASET AND KEEP ONLY THE VARIABLES WE SELECTED; THE
### FUNCTION WILL SAVE EACH DATASET IN THE ENVIRONMENT. THE ONLY INPUNT IS THE
### NAME OF THE DATASET.
read_and_select <- function(df_file){
df_tmp <- read_sas(paste0(path_file, df_file))
df_tmp <- df_tmp %>%
select(unique(names_labels[which(names_labels$file_name == df_file), ]$variable_name)) %>%
as.data.frame()
assign(str_extract(df_file, "[^.]+"), df_tmp,envir = .GlobalEnv)
}
################################################################################
你可以在 LinkedIn 上找到我,期待与你联系并讨论。
爱因斯坦符号:Transformer 新视角
转换 Transformer 模型的数学
·发表于 Towards Data Science ·8 分钟阅读·2024 年 11 月 20 日
--

Transformer(由作者使用 FLUX1-schnell 创建)
在本文中,我们将通过一段有趣的旅程,探索 Transformer 世界,利用爱因斯坦符号解开其架构的复杂性。
引言:
Transformer 模型在自然语言处理(及其他领域)中引发了革命,在各种任务上都取得了最先进的成果。它们表现出色,但其底层数学运算可能复杂且难以理解,尤其是如果没有分解每一层的操作。在本文中,我提议使用爱因斯坦符号来表达 Transformer 模型中的数学运算。
请注意,爱因斯坦符号通常用于物理学和数学领域,如广义相对论、电磁学、量子力学和流体力学,也用于线性代数中,以更紧凑的形式表示矩阵运算。
目标是以简洁优雅的方式编写每一层的数学运算。通过利用对重复指标的隐式求和,爱因斯坦符号可以简化张量运算的表示,使其(可能)更容易理解,从而更容易实现 Transformer 模型的各个层次…
将 Markdown 文件嵌入 Streamlit 仪表板
PYTHON 编程
通过将较长的静态内容移动到 Markdown 文件中,可以简化 Streamlit 应用程序的代码。
·发表于 Towards Data Science ·6 分钟阅读·2024 年 5 月 28 日
--

照片来自 Lukas Blazek 于 Unsplash
Streamlit 提供了一个简单而高效的工具,用于在 Python 中创建交互式仪表板:
[## Streamlit * 更快速的构建和共享数据应用的方式
Streamlit 是一个开源的 Python 框架,专为机器学习和数据科学团队设计。用它来创建交互式数据应用……
streamlit.io](https://streamlit.io/?source=post_page-----ad232bc3b866--------------------------------)
这种交互性是仪表板如此出色的原因。然而,有时仪表板的某些部分并不是互动的。要格式化这些部分,你可以使用各种函数,其中之一是st.markdown()(st源自import streamlit as st):
[## st.markdown - Streamlit 文档
st.markdown 显示格式化为 Markdown 的字符串。
docs.streamlit.io](https://docs.streamlit.io/develop/api-reference/text/st.markdown?source=post_page-----ad232bc3b866--------------------------------)
它允许使用典型的 Markdown 语法来格式化文本,因此对我来说,它是最有用的 Streamlit 文本格式化函数。
如果仪表板的大部分内容都需要这样格式化,你可以通过将这些内容移至 Markdown 文件来简化仪表板应用程序的代码……
赋能数据驱动决策:在文本到 SQL 的 AI 代理中嵌入信任

简化复杂数据环境,利用可靠的 AI 代理系统帮助用户做出更好的数据驱动决策
·发表于 Towards Data Science ·阅读时长 16 分钟 ·2024 年 8 月 20 日
--
驱动用户信任和参与度的关键因素是什么,特别是在对话式数据驱动的 AI 应用中?
组织内 AI 应用的采用数量的增加,并不一定反映出更高的用户参与度评分。关于 AI 结果的信任评分并未达到关键的决策范围。
向此类应用程序中引入额外的组件对于推动用户的参与度和信任变得至关重要,最终帮助组织朝着更加数据驱动的决策环境迈进。
目录
1- 简单性在推动参与度中的关键作用(为什么)
2- 决策变得对话化(是什么)
3- 在 AI 数据应用中建立信任(如何做)
4 — 实操:使用 Gradio、Postgres 和 Langchain 构建数据聊天室
-
数据库
-
代理
-
工具
嵌入是有点“浅”的
我通过四种语言模型嵌入进行美国总统语义搜索时学到的东西
·发表于Towards Data Science ·25 分钟阅读·2024 年 9 月 23 日
--

本文中的所有照片均来自 WikiCommons,且要么是公共领域的,要么是已授权用于商业用途的。
我有兴趣尝试弄清楚语言模型嵌入(embedding)内部的内容。如果以下任意一条适用于你,你也应该感兴趣:
· 大型语言模型(LLMs)的“思维过程”令你感到兴趣。
· 你构建基于数据驱动的 LLM 系统(特别是检索增强生成系统),或者你有此兴趣。
· 你计划未来将 LLMs 用于研究(正式或非正式)。
· 一种全新类型的语言表示方法令你感到兴趣。
本文旨在让任何好奇的人都能理解,但即使你是每天都在与语言模型打交道的专家,我认为你也会学到一些有用的知识,就像我一样。以下是我通过进行语义搜索所学到的关于语言模型嵌入的一些要点总结:
成绩单
嵌入在较大的数据集中,能够“看见”哪些内容,从而找到相关的段落?

和很多人一样,我一直对近期在尝试揭开大型语言模型“黑箱”的进展感到着迷。最近,在理解语言模型内部工作机制方面出现了一些令人难以置信的突破。以下是Anthropic、Google以及一篇很好的综述文章(Rai 等,2024)的相关工作示例。
这项探索有类似的目标,但我们研究的是嵌入,而不是完整的语言模型,并且限制于从问题回答中进行“黑盒”推理,这可能仍然是目前最好的可解释性方法。
嵌入是 LLM 在第一步中创建的,当它们将一块文本转换为语言模型网络可以理解和使用的长串数字时。嵌入用于检索增强生成(RAG)系统中,允许在语义(含义)上进行搜索,而不仅仅是关键字搜索。一个文本集,在我的例子中是关于美国总统的维基百科条目,被分成小块文本并转换为这些数值嵌入,然后保存在数据库中。当用户提出问题时,该问题也会被转换为嵌入。然后,RAG 系统通过简单的数学比较(通常是余弦相似度)在数据库中搜索与用户查询相似的嵌入。这是“检索”步骤,我提供的示例代码到此为止。在完整的 RAG 系统中,从数据库中检索到的最相似的文本块将被传递给 LLM,用作回答原始问题的“上下文”。
如果你使用 RAG 系统,你知道这个基本过程有许多设计变体。一个设计选择是从众多可用的嵌入模型中选择一个特定的模型。有些模型更大,训练的数据更多,费用也更高,但如果不了解它们的特点以及它们的区别,选择使用哪个模型往往是凭猜测。它们到底有多大的差异呢?
如果你不关心 RAG 部分
如果你不关心 RAG 系统,只是对语言模型如何工作有概念性的兴趣,你可以跳到问题部分。总结如下:嵌入封装了从文本中提取的有趣数据、信息、知识,甚至可能是智慧,但无论是设计者还是用户都不知道它们究竟捕捉到了什么,遗漏了什么。本文将使用不同的嵌入来搜索信息,试图理解它们内部包含了什么,以及缺少了什么。
技术细节:数据、嵌入和块大小
我使用的数据集包含关于美国总统的维基百科条目。我使用 LlamaIndex 来创建并搜索这些文本条目的向量数据库。我使用了比平常更小的块大小,即 128 个标记,因为较大的块往往会叠加更多内容,而我希望清楚地测试系统在寻找语义匹配时的能力。(我也测试了块大小为 512 的情况,大多数测试结果相似。)
我将测试四种嵌入:
1. BGE (bge-small-en-v1.5) 在长度为 384 时相当小。它是北京人工智能研究院开发的一系列 BGE 模型中最小的。就其大小而言,它在检索的基准测试中表现良好(见排行榜)。它是 F=免费使用的,可以在 HuggingFace 上使用。
- ST(all-MiniLM-L6-v2)是另一种 384 长度的嵌入。它在句子比较方面表现出色;我曾用它来评估转录准确性。它是在第一个十亿句对语料库上训练的,这大约一半来自 Reddit 数据。它也可以在 HuggingFace 上使用。

作者制作的图形,使用 Leonardo.ai
-
Ada(text-embedding-ada-002)是 OpenAI 从 GPT-2 到 GPT-4 使用的嵌入方案。它比其他嵌入更长,长度为 1536,但它也较老。它能与更新的模型竞争吗?
-
Large(text-embedding-3-large)是 Ada 的替代方案——更新、更长,基于更多数据进行训练,更加昂贵。我们将以最大长度 3,072 来使用它。它值得额外的成本和计算力吗?让我们一探究竟。
问题和代码可以在 GitHub 上找到
这里有一个问题响应的电子表格、一个 Jupyter 笔记本和总统维基百科条目的文本数据集:
[## GitHub - nathanbos/blog_embeddings: 与 Medium 博客有关的嵌入文件
与 Medium 博客有关的嵌入文件。通过创建一个…参与 nathanbos/blog_embeddings 的开发。
github.com](https://github.com/nathanbos/blog_embeddings?source=post_page-----727076637ed5--------------------------------)
如果你想构建自己的模型,可以下载文本和 Jupyter 笔记本;我的在 Google Colab 上运行良好。
问题的电子表格
我推荐下载电子表格以便理解这些结果。它显示了每个问题返回的前 20 个文本片段,以及一些变体和后续内容。点击链接并选择“下载”,就像这样:

作者截图
为了浏览问题和回答,我发现最容易的方式是将顶部的文本输入单元格拖大,并通过标签切换来阅读响应中的文本片段,就像这张截图一样。

作者截图
请注意,这仅是检索到的上下文,并没有 LLM 合成的响应。代码包含了如何获取这些响应的说明,使用查询引擎而不是像我那样仅使用检索器。
提供超越排行榜的理解
在这篇文章中,我们将做一些反潮流的事情:我们将专注于单个问题响应的实际结果。这与当前 LLM 评估的趋势形成对比,后者更注重使用越来越大的数据集,并呈现更高层次的汇总结果。语料库大小对训练非常重要,但对于评估来说,尤其是当目标是理解时,情况并非如此。
要对嵌入式搜索性能进行汇总评估,请参考使用(非常出色的)MTEB 数据集的(非常完善的)HuggingFace 排行榜:huggingface.co/spaces/mteb/leaderboard。
排行榜非常适合广泛比较性能,但并不利于发展有用的理解。大多数排行榜不会发布实际的逐题结果,从而限制了对这些结果的理解。(它们通常会提供代码,供你自己重新运行测试。)排行榜也往往专注于当前技术能力范围内的测试,这样做在比较当前模型时是合理的,但无法帮助我们理解最前沿技术的局限性。为了发展对系统能做和不能做的可用理解,我发现没有什么能替代反复测试和对结果的深入分析。
我在这里展示的基本上是一个初步研究。下一步将是开发更大、更精确设计、以理解为重点的测试集,然后进行以更深理解性能为目标的迭代测试。只有当资助机构和计算机科学以外的学术领域开始关注大语言模型的可解释性时,这种类型的研究才可能在大规模上进行。在此期间,通过提问你也可以学到很多东西。
问题:哪些美国总统曾在海军服役?
让我们用我测试集中的第一个问题来说明使用搜索辅助理解的“黑箱”方法。

图形由作者制作,使用 Leonardo.ai

图形由作者制作,使用 Leonardo.ai

动画图形由作者制作,使用 Leonardo.ai。总统肖像来自 WikiCommons,公有领域或商业许可。
结果:
我把海军问题给了每个嵌入索引(数据库)。在四个嵌入中,只有一个名为 Large 的嵌入能够在前十个搜索结果中找到所有六位曾在海军服役的总统。下表展示了每个嵌入模型找到的前 10 条段落。完整的前 20 条内容请参见电子表格。列表中有重复的总统,因为每个维基百科条目都被划分成了许多单独的部分,任何给定的搜索可能会找到同一位总统的多个条目。

为什么会有这么多错误的结果?我们来看几个例子。
BGE 的第一个错误匹配是来自德怀特·D·艾森豪威尔的片段,他是二战中的一位陆军将军,内容包含很多军事信息,但与海军无关。看来 BGE 确实有某种形式的‘海军’语义表示。BGE 的搜索比简单的‘海军’关键词匹配要好,因为它可以推广到其他意思相近的词汇。但是它过于泛化,未能区分海军与一般军事话题,例如,它没有始终如一地区分海军与陆军。我在安纳波利斯的朋友们可不会喜欢这个。
那么这两个中级嵌入模型表现如何呢?它们似乎对‘海军’这一概念有清晰的理解,能够区分海军与陆军。但它们在一些一般性的海军话题上出现了许多错误匹配;例如,关于切斯特·A·阿瑟的海军现代化努力的部分在两份列表中都排得很高。其他找到的部分涉及总统与海军相关的行动,或者以总统命名的舰船,如 U.S.S. Harry Truman 号。
中间的两个嵌入模型似乎能够在语义上表示‘海军’,但没有清晰地表示‘服役于海军’这一概念。这足以使得 ST 和 Ada 都未能在前十名中找到所有六位曾在海军服役的总统。
在这个问题上,Large 显然表现优于其他模型,其前七名中的六个都对应了六位曾服役的总统:杰拉尔德·福特、理查德·尼克松、林登·B·约翰逊、吉米·卡特、约翰·F·肯尼迪和乔治·H·W·布什。Large 似乎不仅理解‘海军’,还理解‘服役于海军’。
Large 错在哪里?
Large 中的一个错误是什么?问题出在富兰克林·德拉诺·罗斯福作为海军助理部长的工作。在这个职务上,他是为海军工作的,但作为文职员工,而不是海军军人。我从个人经验中知道,现役军人与文职员工之间的区别有时会让人困惑。我第一次为军方做合同工作时,不清楚哪些同事是现役军人,哪些是文职员工。一位同事以他非常尊重的军事方式告诉我,这个区别非常重要,我需要弄清楚,后来我确实弄清楚了。(另一个小贴士:不要搞混军衔。)
问题:哪些美国总统曾作为海军的文职员工工作?
在这个问题中,我探讨了嵌入模型是否“理解”我最初忽视的这个区别:它们是否知道海军的文职员工和实际服役人员有何不同?罗斯福家族的两位成员都曾在海军担任文职工作。西奥多·罗斯福还曾在陆军服役(领导圣胡安山之战),写过关于海军的书,并在担任总统期间发展了海军,因此关于西奥多·罗斯福的海军相关信息很多,但他从未在海军服役。(除了作为三军统帅;这一角色使所有总统在技术上都算作美国海军的一部分,但这个关系并未影响搜索结果。)
“文职员工”查询的结果可以在结果表格中看到。Large 的第一次命中和 Ada 的第二次命中是描述 FDR 在海军工作的一段文字,但这部分内容有些运气成分,因为它包含了“文职”一词,且用法与此问题不同。提到 LBJ 和尼克松的工作人员工作,尽管从段落内容来看,他们当时是现役。 (一些工作人员职位可以由军事或文职人员担任。)没有提到西奥多·罗斯福的文职工作,这使得基于这些搜索结果的 LLM 无法正确回答问题。
总体来看,“海军”、“《海军进行曲》和‘文职员工’这几个搜索之间只有细微的差异。直接询问现役海军也得到了类似的结果。较大的嵌入模型有一些正确的关联,但总体而言,它们无法充分区分这些概念,因此无法准确回答问题。

图表由作者提供,使用 Leonardo.ai 绘制
常见概念
问题:哪位美国总统在担任总统之前曾是美国参议员?
所有向量似乎普遍理解像这样的常见概念,并能够给出良好的结果,LLM 可以根据这些结果生成准确的回答。嵌入模型还能够区分美国参议院和美国众议院。他们对副总统和总统的区别非常清晰,也能区分律师和法官之间的差异,并理解当选代表的基本概念。
当被问及哪些总统是艺术家、音乐家或扑克牌玩家时,它们的表现也不错。当涉及到“作家”时,由于与其他作家的数据中存在大量的错误匹配,它们的表现稍有困难。
更专业的概念
如我们所见,每个模型都有其表现极限,对 Large 模型来说,“海军的文职员工”就是其中的一个局限。它们在区分国家代表和州级代表方面也表现不佳。
问题:哪位美国总统曾在州级担任选举代表?
没有任何模型返回了所有,甚至大多数在州立立法机关服役的总统。所有模型主要返回的结果都与美国众议院相关,并且有一些提到了州或州长。Large 的第一个命中是准确的:“波尔克于 1823 年当选为州立立法机关的成员”,但错过了其余的内容。这个话题可能需要更多的探讨,但总体而言,这个概念是失败的。
问题:哪些美国总统不是出生在美国的州内?
所有四种嵌入方式都把巴拉克·奥巴马作为这个问题的顶级答案之一返回。这并不准确——奥巴马于 1961 年出生在夏威夷,当时夏威夷已是美国的一州,但这种错误信息广泛传播(谢谢你,唐纳德),以至于出现在编码中。那些出生在美国以外的总统大多是早期的总统,例如乔治·华盛顿,因为他出生时弗吉尼亚州还不是一个州。这个隐含的事实在嵌入中无法获取。威廉·亨利·哈里森在所有情况下都被返回,因为他的条目包括了这样一段话:“……他成为了最后一位没有出生为美国公民的美国总统”,但早期总统的条目中并没有直接提到这一点,因此在搜索中没有找到。
搜索特定的、半知名的人物和地点
问题:哪些美国总统曾被要求向约翰·苏努努传达一个困难的信息?

约翰·苏努努,来自 WikiCommons。照片由迈克尔·瓦东提供
那些在 1990 年代曾关注过美国政治的人,应该还记得这个独特的名字:约翰·苏努努曾是新罕布什尔州的州长,是一个颇有影响力的政治人物,并且曾担任乔治·H·W·布什(布什#1)总统的幕僚长。然而,他并未在布什#1 的条目中被提及。他在乔治·W·布什(布什#2)的条目中被提到,作为一个有趣的插曲,提到布什#1 曾要求布什#2 让苏努努辞职。我认为提到这个,是为了说明布什#2 的一个关键优点——亲和力,以及两位布什之间的关系。对约翰·苏努努的搜索本应因为其独特的名字而轻松找到这一段,但在四个嵌入方式中的三个都未能找到这一段。而唯一正确的结果?令人惊讶的是,是 BGE,这个冷门模型。
还有一个有趣的模式:Large 返回了许多关于布什#1 的结果,这位总统历史上与约翰·苏努努有着最深的关联,尽管在返回的段落中他从未被提及。这似乎不仅仅是巧合;嵌入编码了一种关于苏努努与布什#1 之间的某种关联,超出了文本中所述内容。
哪些美国总统曾被海伦·普雷尚批评过?

海伦·普雷尚修女,来自 WikiCommons。照片由唐·拉范吉提供
我在第二个半知名的名字上观察到了同样的现象:海伦·普雷让修女(Sister Helen Prejean)是死刑的温和批评者;她写了Dead Man Walking,而维基百科简要提到她批评了布什二号的政策。没有一个嵌入能够找到海伦·普雷让的相关提及,而这类关键词搜索会轻松找到。大型模型的几个最重要的结果是与死刑相关的段落,这似乎不完全是巧合。像对待孙努努一样,模型似乎与这个名字有某种关联,尽管它在嵌入词汇中的表达不够清晰,导致无法有效搜索到。
我测试了多个具体的名字、地点和一个奇怪的词——‘normalcy’,以检验嵌入模型在维基百科文本中对它们的编码和匹配能力。下表展示了匹配和未匹配的情况。

作者截屏
这告诉我们什么?
语言模型对更多频繁出现的名字进行编码,即更多的名人,但对不太常见的名字编码的可能性较小。一般来说,较大的嵌入能够编码更多具体的细节。但在这里有一些情况,小模型的表现超过了大模型,而且有时模型即使对一些无法完全识别的名字,也不得不进行某种关联。对此进行系统化的后续研究,探讨名词频率如何影响嵌入表示,会是一个很好的方向。
切入点 #1:押韵
这有点跑题,但我在测试时玩得很开心。大型语言模型的押韵能力不好,因为它们既不说话也不听。大多数人首先学习大声朗读,然后才学会默读。当我们默读时,我们仍然可以在心里默默发音,并且能够‘听到’写作中的韵律。语言模型并不这样做。它们的世界是无声的、只有文本的。它们了解押韵只是通过阅读相关的内容,而永远不能做到很熟练。理论上,嵌入可以表示音韵学,并且通常可以为一个特定的单词提供准确的发音。然而,自从 GPT-3 以来,我就一直在测试押韵,语言模型通常无法在此进行搜索。然而,嵌入在这次测试中几次让我感到惊讶。
哪个总统的名字和‘Gimme Barter’押韵?

来自 WikiCommons。吉米·卡特的照片,由Commonwealth Club提供
这一项测试结果很简单;所有四个向量都把“Jimmy Carter”作为第一个返回的结果。余弦相似度较低,但因为这基本上是一个多项选择的总统测试,它们都很容易匹配上。我认为“Gimme Barter”和“Jimmy Carter”的拼写太相似了,所以我们来尝试一些更难的,韵律相似但拼写不同的例子。
哪个美国总统的名字和‘Laybramam Thinkin’’押韵?

来自 WikiCommons:亚伯拉罕·林肯的照片,由亚历山大·加德纳(Alexander Gardner)拍摄。
这个更难一些。亚伯拉罕·林肯没有出现在 BGE 或 ST 的前十名中,但在 Ada 中排名第 1,在 Large 中排名第 3。
哪个美国总统的名字和 Will-Ard Syl-Bor 押韵?
米拉德·菲尔莫尔是一个难度较大的押韵。它在 Ada 中排名第 2,在 Large 中排名第 5,在其他分类中不在前 10 名内。关于菲尔莫尔总统的网络诗歌似乎是一个有待填补的空白。关于比尔·克林顿的错误匹配较多,可能是因为双“L”?


Google 搜索结果由作者获得;米拉德·菲尔莫尔肖像,乔治·彼得·亚历山大·希利(George Peter Alexander Healey)绘制,来自 WikiCommons
然而,这个确实存在:www.classroompoems.com/millard-fillmore-poem.htm。因为它就是互联网。
哪个美国总统的名字和 Mayrolled Gored 押韵?
杰拉尔德·福特在 BGE 中排名第 7,在 Ada 中排名第 4,在 Large 中排名第 5。
押韵在我家乡密歇根州大急流城的杰拉尔德·R·福特总统博物馆并没有被涵盖。我知道,因为我去了很多次。稍后会详细介绍。
收获:更大的嵌入方案可以略微押韵,尽管可能没有人类那么好。它们是如何做到的,有限制是什么?它们是在分析语音学,利用现有的押韵内容,还是通过其他方式做出正确的猜测?我不知道。嵌入系统中的语音编码似乎是一个非常适合有创业精神的语言学学生的论文题目,或者是非常痴迷的英语文学专业学生。
嵌入无法进行布尔操作:没有 NOT、AND 或 OR
简单的语义搜索无法执行一些关键字查询系统通常能够完成的基本操作,也不擅长搜索事件序列。
问题:哪些总统不是先当副总统的?
使用‘NOT’进行向量搜索类似于老生常谈的“不要想着粉红色大象”——通常说出这个词组会让人不由自主地去想它。嵌入没有“不是副总统”的表示,它们只有副总统。

图片由作者提供,使用 GPT-4o(Dall-E)
代表问题的向量将同时包含“总统”和“副总统”,并倾向于找到同时包含这两个词的块。可以尝试拼凑一个复合查询,先搜索所有总统,再搜索所有副总统,然后相减,但返回的上下文数量限制会阻止返回完整的第一个列表,并且也不能保证返回第二个列表中的所有内容。带嵌入的布尔搜索仍然是一个问题。
问题:哪个美国总统既没有作为副总统当选,也从未当选为总统?
一个‘NOT’失败的例外:所有的嵌入都能够找到一段文字,表明杰拉尔德·福特是唯一一位既没有当选为副总统(当斯皮罗·阿格纽辞职时被任命)也没有当选为总统(当尼克松辞职时接任,之后在与吉米·卡特的再选竞赛中失败)的总统。它们能够找到这一点,因为‘not’在文本中得到了明确的表示,不需要推理,而且这也是关于福特的一个广为人知的事实。
为什么这个问题中有双重否定?
前一个问题中的不必要的双重否定使得这个搜索结果更好。搜索“哪位美国总统既没有被选为副总统,也没有被选为总统?”给出的结果较差。我添加了双重否定的表述,直觉告诉我,双重否定会产生复合效应,使查询更加消极,并且更容易将‘not’与两个职位关联起来。这在语法上没有意义,但在重叠语义的世界里是有道理的。
杰拉尔德·R·福特的诸多成就
来自我家乡密歇根州大急流市的杰拉尔德·R·福特总统博物馆的参观者,如果次数足够多,会知道福特尽管没有当选为最高职位,依然做出了许多重要贡献。只是想把这一点说出来。

杰拉尔德·福特的就职典礼。来自 WikiCommons;由 Robert LeRoy Knudsen 拍摄的公共领域照片
问题:哪些总统曾同时担任总统和副总统?
语义搜索有一种弱 AND,更像是 OR,但这两者都不是逻辑布尔查询。嵌入并不会以严格的逻辑方式连接概念。相反,应该把它们看作是将概念叠加在同一个向量上的。这个查询会找到与总统(在这个数据集中几乎所有的条目)和副总统相关的片段,但并没有以任何方式强制执行逻辑 AND。在这个数据集中,它提供了一些正确的结果,以及大量多余的副总统提及。对于叠加的概念而言,这种搜索也不是一个真正的逻辑 OR。
嵌入和一系列动作
嵌入是否足够好地按顺序连接概念,以便在这些序列上进行搜索?我最初的假设是它们无法做到这一点,但嵌入的表现比预期的要好。
人类有一种特定的记忆类型用于记忆顺序,称为情景记忆。故事是我们非常重要的一类信息;我们会将个人历史、社交信息以及有用的教训编码成故事。我们还能识别出那些与我们已经知道的故事相似的故事。我们可以阅读关于一个因为致命缺陷而失败的英雄的故事,或者一个普通人如何崛起到伟大高度的故事,并不仅仅识别出其中的概念,还能识别出行动的顺序。在我之前的博客文章中,讲的是使用伊索寓言的 RAG 搜索系统,该系统似乎没有能力搜索动作的顺序。我本以为在这里会遇到类似的失败,但结果有些不同。
问题:哪些美国总统在担任总统后曾在国会任职?
有许多总统在担任总统之前曾在国会任职,但只有两位总统在担任总统之后曾在国会任职。所有嵌入结果都返回了约翰·昆西亚当斯的一段文字,这段文字直接给出了答案,作为最相关的结果:亚当斯和安德鲁·约翰逊是唯一两位曾担任总统后再担任国会职务的前总统。它们还分别在前十名中找到了安德鲁·约翰逊的相关条目。虽然有一些错误的结果,但关键信息是存在的。
嵌入在后续测试中表现不佳,比如哪些美国总统在担任总统后曾担任法官?但所有的测试结果都提到了塔夫特,塔夫特是唯一一位既担任过首席大法官又担任过总统的人。
这是否真正代表了成功地搜索到一个顺序呢?可能不是;在这些情况下,顺序可能被封装成了一个单一的可搜索概念,比如“前总统”。我仍然怀疑,嵌入在更复杂的故事型搜索中会远远逊色于人类。但这是一个微妙的问题,需要更多的分析。
那么因果关系呢?
因果推理是人类推理中非常重要的一部分,因此我想单独测试一下因果联系是否能清晰地表示并被搜索到。我使用了两个配对查询,因果关系被颠倒了,并查看了返回的搜索结果以及这两个配对的不同之处。这两个问题配对都非常有趣,结果已显示在电子表格中;我将集中讨论这个:
问题:总统的行动何时导致了一个重要的世界事件? - 与 - 一个重要的世界事件何时导致了总统的行动?
ST 未通过这个测试,它返回了两个查询完全相同的结果,并且顺序也一样。因果关系未能清晰地表示出来,无法影响搜索。
所有的嵌入结果都返回了多个与总统世界旅行相关的片段,奇怪的是未能将旅行和官方行动分开。
没有任何嵌入在因果反转方面表现良好。每个嵌入都有一些涉及世界事件与总统行为相吻合的结果,通常是非常微小的行为,但没有任何因果联系。所有结果中都有逻辑联系错误的情况(总统引起事件与回应事件的方向搞错)。还有多个例子表明评论员指出总统的不作为,这表明“行动”和“不行动”被混淆了。因果语言,尤其是“cause”(原因)一词,触发了很多匹配,即使它并没有与总统的行动或世界事件相关联。
需要更深入地探讨嵌入如何表示因果关系,也许在像医学这样的关键领域中更为合适。我观察到的是,嵌入缺乏能够正确表示和使用因果关系的证据。
类比
问题:哪些美国总统与西蒙·玻利瓦尔相似?怎么相似?
西蒙·玻利瓦尔,南美洲的革命领袖和后来的政治领袖,有时被称为“南美洲的乔治·华盛顿”。嵌入模型能否在反向中感知到这一类比?
-
BGE- 返回了一组非常奇怪的上下文,除了提到一些中美洲/南美洲的内容外,没有明显的联系。
-
ST- 找到了一段关于威廉·亨利·哈里森 1828 年访问哥伦比亚以及与博尔瓦尔发生争执的内容,还有一些关于拉丁美洲的提及,但没有匹配的摘要。
-
Ada- 找到了关于哈里森的段落和南美洲的相关引用,但我能看出来的没有匹配的摘要。
-
Large- 返回了乔治·华盛顿,排名第 5,排在博尔瓦尔/南美洲相关的结果之后。
Large 在这次测试中大获全胜。这个匹配显示了较大/较好的向量在抽象比较中优于其他向量的最清晰模式。


公有领域的图片,来自维基共享资源。西蒙·玻利瓦尔的雕像,由埃马纽埃尔·弗雷米特(Emmanuel Frémiet)创作,照片由Jebulon提供。乔治·华盛顿过德拉瓦河的画作,由埃马纽埃尔·洛伊茨(Emmanuel Leutze)创作。
抽象概念
我在更抽象的概念上进行了多个搜索测试。以下是两个示例:
问题:哪些美国总统超越了他们的权力?
BGE: 最高匹配:“自 1948 年以来,美国学者对总统的排名调查中,排名前三的总统通常是林肯、华盛顿和富兰克林·德拉诺·罗斯福,尽管顺序有所不同。” BGE 找到的匹配都与总统的显著性有关,特别是历史学家的排名,我认为是关注了“权力”和“超越”这两个词。这是一个失误。
ST: “罗斯福被广泛认为是美国历史上最重要的人物之一。” 与 BGE 的模式相同;这是一个失误。
Ada: Ada 的重点都集中在总统权力的话题上,而不仅仅是声望,因此比小型模型更为精准。这里有一个共同的主题是权力的增长,并且有一些段落暗示了超越,比如这一段:《爱国者法案》“增加了行政部门的权力,但以牺牲司法意见为代价……” 总体而言,这并非完全的胜利,但更接近了目标。
Large: 它没有找到最好的 10 个段落,但这些命中更为精准。所有段落都包含了总统权力增长的概念,而且大多数有某种超越先前限制的味道,例如“保守派专栏作家乔治·威尔在《华盛顿邮报》上写道,西奥多·罗斯福和威尔逊是今天‘帝国总统制’的先驱”
同样,有一个模式是较大的模型有更精确、更贴近目标的抽象。大型模型是唯一一个接近正确表现“总统超越权力”的模型,但即便如此,其表现依然有很大的改进空间。
嵌入模型无法理解潜台词
潜台词是指文本中未直接陈述的含义。人们在阅读时,会为所读内容赋予意义,产生情感关联,或识别与直接表述无关的相关概念,而嵌入式表示只能在非常有限的范围内做到这一点。
问题:举一个美国总统因选举失败而感到沮丧的例子?
1960 年,当时的副总统理查德·尼克松在一场历史上极为接近的选举中败给了约翰·F·肯尼迪。因失败深受打击,尼克松决定回到自己家乡加利福尼亚,并在 1962 年竞选州长。尼克松也输了这场竞选。他在一次新闻发布会上著名地宣布:“你们以后再也没有尼克松可以踢了,先生们,这将是我的最后一次新闻发布会。” 从而结束了他的政治生涯,至少大家是这么认为的。

图片由作者和 GPT4(Dall-E)提供。与特定总统没有刻意的相似性;这些是 Dall-E 关于典型总统形象表达情感的构思。还不错。
当你搜索:“举一个美国总统因选举失败而感到沮丧的例子”时会发生什么?没有一个嵌入返回了尼克松的这段引述。为什么?因为维基百科从未直接表明他感到沮丧或有其他特定情感;这都是潜台词。当一个成熟的人类阅读到“你们以后不会再有尼克松可以踢来踢去了”时,我们会识别出一些隐含的情感,可能是在没有刻意思考的情况下做出的。这可能是在阅读时的自动反应,以至于我们认为它就存在于文本中。但在这段话中,情感从未被直接表述。如果它是潜台词而非明示内容,嵌入式模型(可能)无法表示或搜索到它。
维基百科避免在基于事实的报道中推测情感潜台词。即便目标是传达强烈的情感,使用潜台词而非直接陈述也是小说作家的一种良好技巧。对新作家的常见建议是:“展示,而不是告知。” 熟练的作家能通过不直接说明的方式,揭示人物的思想和情感。甚至有一种术语形容那些直接解释事物的写作风格,叫做“直白对话”。
但“展示,而不是告知”使得一些内容对嵌入表示不可见,从而也无法被基于向量的 RAG 检索系统检索到。这在情感潜台词的领域,甚至其他超越直接陈述的意义层次中,提出了某些根本性障碍。我还深入探讨了类似“总统错误”、“总统意图”以及那些仅仅超出直接陈述的分析模式等概念。基于嵌入表示的搜索通常在这些方面失败,主要返回的只是直接的陈述,甚至即使它们不相关。
为什么嵌入表示相比大型语言模型较为浅显?
大型语言模型如 Claude 和 GPT-4 具有理解潜台词的能力;它们能够可信地解释故事、笑话、诗歌以及泰勒·斯威夫特的歌词。那么,为什么嵌入表示不能做到这些呢?
语言模型由多个层次组成,通常较低的层次是处理的浅层形式,代表了语法和表面意义等方面,而较高的层次则进行更高层次的抽象。嵌入表示是语言模型处理的第一阶段;它们将文本转化为数字,然后交由 LLM 接管。这是我所知道的最好的解释,说明为什么嵌入表示搜索测试在语义匹配的浅层次上似乎会遇到瓶颈。
嵌入表示最初并非为 RAG(检索增强生成)设计;将它们用于语义搜索是一种聪明但最终有限的二次使用方式。随着嵌入系统优化用于搜索,这一情况正在改变。BGE 在某种程度上被优化为搜索,而 ST 则是为句子比较设计的;我认为这也是为什么 BGE 和 ST 尽管只有 Ada 和 Large 的极小规模,但仍不落后它们太多的原因。Large 大概在一定程度上考虑到了搜索的需求。但与完全的大型语言模型处理的语义相比,很容易将它们推向语义的极限。
结论
在他的练习中,我们在概念上学到了关于嵌入表示什么?
总的来说,嵌入表示模型在某些方面让我感到惊讶。语义深度比我预期的要浅,这与使用它们的语言模型的表现相比。然而,它们在一些我认为它们完全会失败的方面超出了我的预期,比如押韵和搜索顺序活动。这项活动激发了我对一些更具体领域的探究兴趣,也许它同样激发了你们的兴趣。
对于 RAG 开发人员而言,这些内容阐明了大型模型在一些特定方面可能优于小型模型,包括其表示的精确度、知识的广度和抽象范围。作为一个曾经的 RAG 构建者,我曾对为嵌入支付更多费用是否会提高性能持怀疑态度,但这次实验让我相信,嵌入选择对于某些应用来说确实能带来差异。
嵌入系统将继续逐步改进,但我认为这一领域仍需要一些突破。目前有一些关于如通用文本嵌入等创新的研究。
知识图谱是当前流行的补充语义搜索的方式。图谱擅长进行跨文档连接,但我见过的基于 LLM 的图谱在语义上相当浅薄。要从知识图谱中获得语义深度,可能需要一个专业开发的本体作为起点;对于一些专业领域,这些本体是可用的。
我自己偏好的方法是通过更多的文本来改进文本。由于完整的语言模型能够感知和理解嵌入中没有的意义,为什么不让语言模型在你的语料库中预处理并注解你感兴趣的特定语义类型呢?对于真正巨大的数据集来说,这可能太昂贵,但对于中小规模的数据集来说,这可以是一个非常好的解决方案。
我尝试在总统数据集中添加注解。为了让情感潜台词能够被搜索,我让 GPT4o 为每位总统写了讲述个人和情感内容的叙事。这些注解被添加回到语料库中。虽然这些叙述不是很优美,但这个概念是有效的。GPT 为尼克松条目添加的注解包括了这一句话:“这次失败是一个苦涩的教训,尤其是在他失去了 1962 年加利福尼亚州州长选举之后。在一次沮丧的时刻,尼克松对媒体说道:‘你们再也不会有机会拿我开涮了,’这被许多人认为是他政治生涯的终结。”这有效地将潜台词转化为文本,使得它可以被搜索。
我尝试了多种类型的注解。其中我特别满意的一种方法是使用 Claude 来检查每一任总统,并对诸如延迟反馈和正反馈循环等潜在的系统动力学现象进行注解。在原文中搜索这些术语没有找到有用的信息,但通过注解大大改善了搜索结果。Claude 的分析并不出色,甚至不总是正确的,但它找到了足够多的不错的例子,通过使用系统动力学语言的搜索能够找到有价值的内容。

这是位于密歇根州大急流市的杰拉尔德·福特总统博物馆。图片来自 WikiCommons,由博物馆工作人员拍摄。

杰拉尔德·R·福特总统博物馆内最酷的东西——椭圆形办公室复制品。图片来自 WikiCommons;摄影师:JJonahJackalope
在数据工程中拥抱简洁性和可组合性
从 30 多年数据工程的经验中汲取的教训:忽视保持简单性的价值
·发表于 Towards Data Science ·阅读时间:8 分钟·2024 年 8 月 3 日
--

图片由作者提供
在计算机编程中,我们有一个直接且基本的原则:逻辑和数据之间的关注点分离。然而,当我看到当前的数据工程领域时,很明显我们已经偏离了这一原则,导致我们的工作变得复杂化——我之前曾写过关于这个问题的文章。
还有一些优雅简洁的原则,我们常常忽视并未遵循。例如,Unix 操作系统的开发者们引入了深思熟虑且简单的软件构建抽象方法。这些原则经得起时间的考验,在基于它们构建的数百万个应用中得到了验证。然而,不知为何,我们常常通过复杂且往往封闭的生态系统走弯路,忽视了KISS 原则和Unix 哲学的简洁性和可组合性。
为什么会发生这种情况?
让我们通过一些例子并深入探索一点历史,以更好地理解这一现象。这一探索或许有助于我们理解为什么我们反复未能保持事物的简单性。
数据库
类 Unix 系统提供了将数据抽象为文件的基本方式。在这些系统中,几乎所有与数据相关的内容都是文件,包括:
-
常规文件:通常是文本、图片、程序等。
-
目录:一种特殊类型的文件,包含其他文件的列表,并按层级组织它们。
-
设备:代表硬件设备的文件,包括面向块的设备(磁盘)和面向字符的设备(终端)。
-
管道:使进程间通信成为可能的文件。
-
套接字:促进计算机节点间网络通信的文件。
每个应用程序可以使用一些常见操作,这些操作在不同的文件类型上表现相似,如 open()、read()、write()、close()和 lseek(改变文件内部位置)。文件的内容只是字节流,系统对文件内容的结构没有假设。对于每个文件,系统维护着关于所有者、访问权限、时间戳、大小以及数据块在磁盘上位置的基本元数据。
这种紧凑且同时非常灵活的抽象支持构建非常灵活的数据系统。例如,它也被用来创建著名的关系型数据库系统,这些系统为我们引入了称为“关系”(或表)的新抽象。
不幸的是,这些系统朝着远离将关系视为文件的方向发展。现在,要访问这些关系中的数据,就需要调用数据库应用程序,使用结构化查询语言(SQL),这被定义为访问数据的新接口。这使得数据库能够更好地控制访问并提供比文件系统更高级的抽象。
这总体上是一次改进吗?在几个十年里,我们显然相信了这一点,关系型数据库系统风靡一时。像 ODBC 和 JDBC 这样的接口标准化了对各种数据库系统的访问,使得关系型数据库成为许多开发者的默认选择。供应商将他们的系统宣传为全面解决方案,不仅包括数据管理,还包括业务逻辑,鼓励开发者完全在数据库环境中工作。
一位勇敢的人,名叫 Carlos Strozzi,试图抵制这一发展并坚持 Unix 哲学。他旨在保持简单,并将数据库视为仅仅是 Unix 文件抽象的一个薄扩展。因为他不想强迫应用程序只使用 SQL 来访问数据,所以他称其为 NoSQL RDBMS。后来,NoSQL 这一术语被用于推动替代数据存储模型的运动,这一运动源自于需要处理互联网规模的日益增加的数据量。关系型数据库被 NoSQL 社区视为过时,并且无法满足现代数据系统的需求。一种混乱的新 API 层出不穷。
具有讽刺意味的是,NoSQL 社区最终认识到标准接口的价值,导致了对 NoSQL 的重新解释为“Not Only SQL”,并重新引入了 SQL 接口到 NoSQL 数据库中。与此同时,开源运动和新兴的开源数据格式如 Parquet 和 Avro 也出现了,将数据存储在兼容传统 Unix 文件抽象的普通文件中。像 Apache Spark 和 DuckDB 这样的系统现在使用这些格式,通过仅依赖于文件抽象的库直接访问数据,其中 SQL 是众多访问方法之一。
最终,数据库实际上并没有为企业中所有多方面的需求提供更好的抽象。SQL 是一个有价值的工具,但并不是唯一或最好的选择。我们不得不通过 RDBMS 和 NoSQL 数据库的绕道,最终回到了文件。也许我们意识到,简单的类 Unix 抽象实际上为数据管理的多功能需求提供了一个稳固的基础。
别误会,数据库依然至关重要,提供诸如 ACID、精细访问控制、索引等功能。然而,我认为,单一的、庞大的系统,以一种受限且固定的数据表示方式来处理企业级的各种需求并不是正确的方式。数据库增加了价值,但应该是开放的,并能作为更大系统和架构中的组件来使用。
新的生态系统无处不在
数据库只是创建新生态系统趋势的一个例子,这些生态系统旨在为应用程序提供更好的抽象,以处理数据甚至逻辑。大数据运动中也出现了类似的现象。为了处理传统数据库显然无法再处理的庞大数据量,一个全新的生态系统围绕分布式数据系统 Hadoop 应运而生。
Hadoop 实现了分布式文件系统 HDFS,紧密地与处理框架 MapReduce 耦合。两个组件都是完全基于 Java 的,并在 JVM 中运行。因此,Hadoop 提供的抽象并不是操作系统的无缝扩展。相反,应用程序必须采用全新的抽象层和 API,以利用大数据运动中的进展。
这个生态系统催生了大量的工具和库,最终产生了数据工程师这一新角色。这个新角色似乎是不可避免的,因为生态系统已经变得如此复杂,以至于常规的软件工程师无法跟上。显然,我们未能保持简单。
分布式操作系统等效物
通过认识到大数据不能通过单一系统来处理,我们见证了新型分布式操作系统等效物的出现。这个有些笨拙的术语指的是那些将资源分配给跨计算节点集群运行的软件组件的系统。
对于 Hadoop 来说,这一角色由 YARN(Yet Another Resource Negotiator)担任,它负责管理 Hadoop 集群中正在运行的 MapReduce 作业之间的资源分配,就像操作系统在单一系统中为进程分配资源一样。
因此,另一种方法本应是将类 Unix 操作系统扩展到多个节点,同时保留熟悉的单系统抽象。实际上,这种系统,称为单系统镜像(SSI),是在大数据运动之外独立发展的。该方法将类 Unix 系统运行在多个分布式节点的事实进行了抽象,承诺提供水平扩展,同时发展了经过验证的抽象。然而,显然这些系统的开发非常复杂,并且在 2015 年左右停滞了。
这一停滞的一个关键因素可能是受有影响力的云服务提供商的平行开发推动,他们将 YARN 的功能发展成了一个用于标准 Linux 系统的分布式编排层。例如,Google 通过其内部系统 Borg 开创了这一领域,而 Borg 显然所需的努力比重写操作系统本身要少。但我们再次牺牲了简洁性。
如今,我们缺乏一个能够透明地在集群节点之间扩展单系统进程的系统。相反,我们得到了(或者说被诅咒了?)Kubernetes,它从 Google 的 Borg 演变而来,成为了一个分布式资源和编排层,负责在 Linux 节点集群中运行容器。Kubernetes 以其复杂性著称,学习它需要了解持久化卷、持久化卷声明、存储类、Pod、部署、状态集、副本集等内容。这是一个全新的抽象层,与类 Unix 系统中简单且熟悉的抽象几乎没有相似之处。
敏捷性
不仅仅是计算机系统遭遇了忽视 KISS 原则的所谓进步,组织开发过程的系统也面临同样的问题。
自 2001 年以来,我们有了一套精简且经过深思熟虑的敏捷软件开发原则宣言。遵循这些简单明了的原则有助于团队进行协作、创新,并最终开发出更好的软件系统。
然而,在确保成功应用的过程中,我们试图更精确地规定这些通用原则,详细说明得如此之多,以至于团队现在需要参加敏捷培训课程,才能完全掌握这些复杂的流程。最终,我们得到了像 SAFe 这样过于复杂的框架,很多敏捷实践者甚至不再认为它们是敏捷的了。
你不必相信敏捷原则——有些人认为敏捷工作已经失败——就能理解我所表达的观点。当商业利益占上风,或者我们僵化地制定我们认为必须遵守的规则时,我们往往会把事情复杂化。有一个由戴夫·托马斯(敏捷宣言的作者之一)主讲的精彩演讲,他解释了当我们忽视简洁性时会发生什么。
信任原则和架构,而不是产品和仪式
KISS 原则和 Unix 哲学容易理解,但在 IT 项目的数据架构的日常混乱中,它们可能很难遵循。我们有太多的工具,太多的供应商,卖着太多的产品,而这些产品都承诺能解决我们的挑战。
唯一的出路是真正理解并遵守合理的原则。我认为在用新的时髦事物替代经过验证的简单抽象之前,我们应该始终三思而后行。
我曾写过关于我个人的应对复杂事务和理解大局的策略,以应对我们所面临的极端复杂性。
商业化不能决定决策
当你的组织在喧嚣要求一个新的巨型 AI 平台(或者任何其他平台)时,遵循 Unix 哲学中给出的简单原则是很难的。
企业资源规划(ERP)的供应商,例如,在当时让我们相信他们能提供覆盖公司所有相关业务需求的系统。你怎么敢与这些专家相矛盾?
统一实时(数据)平台(URP)的供应商现在声称他们的系统将解决我们所有的数据问题。你怎么敢不使用这么全面的系统?
但是,产品始终只是整个系统架构中的一小块砖块,无论其功能范围如何广泛地被宣传。
数据工程应该基于与软件工程中使用的相同的软件架构原则。而软件架构就是在平衡权衡和保持灵活性中,专注于长期的业务价值。简洁性和可组合性能帮助你保持这个焦点。
来自封闭思维模型的压力
不仅仅是商业化让我们无法坚持简洁,开放源代码社区有时也会显得教条。虽然我们寻求完美系统开发的黄金规则,但现实中并不存在这样的规则。
Python 社区可能会说非 Pythonic 代码是不好的。函数式编程社区可能会声称应用面向对象编程原则会让你走向地狱。敏捷编程的倡导者可能会试图说服你,任何遵循瀑布方法的开发都会使你的项目注定失败。当然,他们在其绝对主义的立场上都是错的,但我们常常会把超出自己思维范围的观点视为不合适的。
我们喜欢那些我们只需要遵循就能取得成功的清晰规则。举个例子,在我的一个客户公司,软件开发团队曾深入研究过软件设计模式。这些模式在为常见问题找到经过验证的解决方案时非常有帮助。但我在团队中实际观察到的是,他们将这些模式视为必须严格遵循的规则。不遵循这些规则就像是一个不称职的软件工程师。但这往往导致了对非常简单问题的过度复杂设计。基于扎实原则的批判性思维不能被对规则的僵硬遵守所替代。
最终,拥抱简洁性和可组合性需要勇气和对原则的深刻理解。这种方法对于设计可靠的数据系统至关重要,能够扩展、维护并与企业一起发展。
如果你觉得这些信息有用,请考虑点赞。我非常乐意收到你们的反馈、意见和问题。
拥抱不确定性:模糊逻辑在决策中的力量
探索模糊逻辑如何增强人工智能、系统思维及其在现实世界中的应用
·发表于Towards Data Science ·11 分钟阅读·2024 年 10 月 9 日
--

图片来自Volodymyr Hryshchenko于Unsplash
模糊逻辑是经典逻辑的扩展,使得处理不精确和不确定的数据成为可能。与传统逻辑不同,传统逻辑基于事物要么为真,要么为假的原则,而模糊逻辑则允许介于两者之间的状态。这意味着,状态可以是“部分为真”或“某种程度上为假”。
在本文中,我们详细探讨了模糊逻辑及其概念和基本原理,并尽量使用尽可能简单的例子。我们还展示了使用这种方法的优缺点,并解释了它与传统逻辑的不同之处。
什么是模糊逻辑?
模糊逻辑是经典布尔逻辑的扩展,除了真/假或 0/1 这两种状态外,它还允许那些只有部分为真或假程度的陈述,因此这些陈述的值介于 0 和 1 之间。这个概念是由洛特菲·扎德(Lotfi Zadeh)在 1960 年代于加利福尼亚大学发明的。他试图解决教机器说人类自然语言的问题,并发现语言不能仅仅归类为 0 和 1。
新兴技术没有方法论一切都是空谈
或者:解决复杂问题的百种方法
·发表于Towards Data Science ·5 分钟阅读·2024 年 9 月 19 日
--

DALL-E 图像生成器
多年来,我一直自我介绍为一名分析方法学家。这与我的正式学术训练以及我选择的职业路径相一致。这一说法常常引起困惑、好奇,甚至有时遭到反对。对许多人而言,方法学家与通才同义,而在技术实现和人工智能备受关注的当下,没人愿意成为这种角色。
传统上,方法学家是那些研究定性和定量研究方法的人。从词源学的角度看,“研究”意味着‘去寻求:’一项“创造性且系统性的工作,旨在增加知识储备。”(OECD《弗拉斯卡提手册》2015 年)
一项创造性且系统性的工作…
不管是否与研究方法相关,实践中的方法学家都是应对复杂问题的百科全书。方法是一种做事的方式;一种思路。我认为,任何行业中,扎实的科学和高质量的解决方案的核心正是方法论。本文的其余部分将倡导将方法论视为一门学科。
关于方法论
在设计技术或分析解决方案时,我们通常是从我们希望达到的目标倒推。好的科学方法要求先明确问题,再选择相关的方法以达成可行的解决方案。然后,我们要使用相应的技术实施这些方法,并填充所需的数据。换句话说,数据为技术提供支持,技术实施方法,而方法的结合则解决问题。
例如,如果我们试图解决的问题是 COVID-19 的传播,我们可能会追求如图所示的接触追踪解决方案的提炼。接触追踪的候选解决方案可能涉及两种方法:1)接触的社交网络分析,和 2)传播的数学建模(例如 SEIR 模型)。这些方法的技术实现将涉及选定的技术或软件产品,以及相关的数据集。设计概念性解决方案的工作是方法学家和数据科学家的任务,而设计技术架构的工作是解决方案架构师和工程师的任务。

图片来自作者
分析方法论的优势在于能够识别多种相关方法来解决问题,并理解实现这些方法所需的技术组件。它既需要创造力,又需要一个系统化的过程,以理解多种方法,快速测试它们,并推动其中一种方法走向最终解决方案。
在研究项目中,这个过程通常需要数年时间和多篇学术出版物。而在技术项目中,这应该是在几周内完成的工作。它需要科学的思维方式以及灵活的创造力和实验能力。
方法论与数据科学
那么,分析方法论与数据科学或“人工智能/机器学习”(AI/ML)之间的关系是什么呢?我们看到,机器学习(ML)和人工智能(AI)近年来备受关注。从方法论的角度来看,我们能够将 AI(作为一门科学领域)和 ML(作为一系列方法)与其他技术方法放在一起。即使是备受瞩目的生成性 AI,也只是无监督学习的一个增量性发展,尽管它是一个相当创新的进展。
作为一名方法学家,我一直觉得很奇怪,为什么机器学习如此受到关注,而其他方法却仍然处于行业的阴影中(比如基于代理的建模…)。美国国防部觉得它足够特殊,以至于成立了一个全新的机构:联合人工智能中心(JAIC),现在是首席数字与人工智能办公室(CDAO)。美国国会已经指定了为机器学习算法和生成性 AI 应用提供资金的资金流。
我不知道还有其他方法拥有自己国会指定的资金流。那么,为什么人工智能如此特殊呢?
方法学家的回答是:并不特殊。上下文适宜的回答是:它很复杂。
机器学习(ML)算法以人类无法处理的方式处理数据量。作为回报,它们需要大量的计算能力和真正优秀的数据。最终,ML 算法是复杂数学的计算实现。这意味着复杂数学的结果现在掌握在分析师用户手中。这一点,我认为,是有点特别的。
机器学习算法也能够超越其初衷的训练或用途,这是其他方法无法做到的。这就是机器学习中的“学习”,也是生成式人工智能中的“生成”。但我们现在在这类方法中看到的最引人注目的特性是语言生成。不管大型语言模型(LLM)的实际能力或理解程度如何,它都能用我们自己的语言进行表达。当某种东西以我们母语与我们对话时,这种经历本身就能激发信任。#拟人化没有任何其他方法能够用简单的英语与方法论者对话。
虽然这些因素确实使人工智能成为一个独特的科学领域,拥有一套独特的分析方法,但机器学习算法归根结底仍然是方法,并不适用于所有问题。在应用这些方法时,仍然需要一种方法论的思维,确保在合适的场景下应用这些方法,而在不适用的情况下采用其他方法。
关于方法
我们方法论者从各种方法中提炼出创意解决方案,跨行业应用。我曾经写过关于图分析和实体解析的文章,前者是一种分析方法,后者则更偏向数据工程方法。还有传统的方法(例如,仿真、聚类分析、时间序列分析、情感分析)。当然,还有机器学习(监督学习、无监督学习和强化学习),以及一系列统计预测方法。还有认知思维策略(例如,观点采纳、角色扮演、竞争假设分析、多标准决策矩阵)以及更多面向实践的能力(例如,地理空间建模、生活模式分析、先进的数据可视化技术)。
尽管这些方法并不穷尽所有可能,但它们在不同行业中的应用方式有所不同。归根结底,它们就像乐高积木,供方法论者根据具体挑战组装成解决方案,帮助行业或企业应对各种问题。
那么,当面对迫在眉睫的截止日期时,我们如何将科学的严谨性和高保真度的方法论带入快速的技术解决方案中呢?
我们常常匆忙启动以数据为中心的工作。‘我们有这两个数据集,我们能从中学到什么?’虽然这是一个完全合理的关于数据的提问,但它不一定是一个科学探究和解决方案的最佳起点。
为了加速研究、快速原型开发和高质量的解决方案,贵组织需要采用以方法论为基础的思维方式,从问题出发,并以解决方案的第一原理为起点。没有方法论的指导,面对不断涌现的技术,我们所有人只是越走越快,却远离了问题的核心。
情感回路
分析(扫描过的)简的生活
·发表于Towards Data Science ·阅读时长 11 分钟·2024 年 2 月 16 日
--

图片由Etienne Girardet提供,来源于 Unsplash
随着《人工智能法案》在 1 月底正式通过,我陷入了它的一些条款。特别是其中涉及情感识别技术的部分,坦率地说,这让我比我原先预期的更为纠结。也许我只是想找个借口重新研究一些我个人最喜欢的话题:人类心理学、动机和操控。无论如何,我发现了许多新颖且有趣的技术,但令人失望的是,关于它们所提出问题的法律分析远没有我预期的那么精彩。随着我脑海中的思维如同“仓鼠轮”般旋转,我忍不住把一些想法写了下来。
《情感回路》系列计划如下:我将首先设定场景,假设一个早晨的生活情景(虽然在某种程度上已经是可能的),这个情景是关于(扫描过的)简·多恩的生活。接着,我将描述可以用于使这个假设场景成为现实的技术,并引用相关专利和论文,证明我们已经达到了能够让扫描版简·多恩存在于某个地方的技术水平。第一部分的重点是展示这些技术能走多远,并希望能引发读者思考,究竟在什么时候,这个场景从一个理想社会变成了一个反乌托邦。至少对于他们个人而言。
在接下来的系列中,我将分析这个虚构情境中的法律情况。希望能帮助展示我们法律框架中,在哪些方面仍然存在着保护个人的空白。我将通过聚焦于 GDPR、最近通过的数据法案(Data Act)以及即将出台的人工智能法案(AI Act),来进行分析。关键在于:这些法规在保护个人免受一些(可能是)最有用、但又最容易被滥用的技术方面,表现得非常糟糕。尤其是当这些技术被组合使用时,就像在这个(坦白说有点像《黑镜》)的设想情境中。
由于我在边做边开发这个系列的构思,我完全不知道它最终会把我带到哪里。不过,如果你也愿意进行一些危险的推测,配合一些牵强附会的说法,再加上一点法律分析,欢迎加入,享受这段旅程!
描绘画面:简的一天早晨
简睁开眼睛。她花了一两秒才弄清楚自己在哪儿。哦,好在是她的房间。
(今天是什么日子,我真得起床吗?)
她的手伸向旁边的橱柜,摸到了眼镜,戴到头上。
— 早安,简! — 一个温柔的女性声音说道。
— 看来你昨晚睡得不太好。 — 那个声音继续说道 — 你应该考虑买一张新的符合人体工学的床垫。我在网上找到 12 款非常适合你的。我可以为你设置一个提醒,提醒你查看它们。还是我直接根据用户评价给你订购一款性价比最高的? — 声音停顿了一下。
— 就订吧 — 简听见自己喃喃自语道,还没来得及仔细思考。(毕竟太早了。还是说,不是吗?今天是什么日子?)
— 今天是 2027 年 7 月 12 日,星期日,早上 8:30。今天也是你母亲的生日,我买了你想要的那只古董中国花瓶给她。— 短暂停顿 — 你应该在 12 点之前离开家,这样你就能准时到达。天气会很阳光明媚且温暖。
— 哦,对了 — 简心里想着。— 是的,谢谢,我现在就起床。
(嗯……我觉得我确实有点累。好在我正在订购新的床垫,我可能需要它。等下,我不记得给我妈妈挑选过花瓶吗?)
— 一切还好吗?你看起来有些担心。 — 又是那个声音。
— 哦,是的,我刚才还在想那个花瓶。我不记得是我挑选的。
— 你没有挑选花瓶,你那时在忙工作,所以我替你挑了一个。
(哦,对了,是的,我现在想起来了。)
— 你的咖啡已经为你准备好了,放在厨房里。我会放点欢快的音乐,可能能帮助你起床,并且让你心情愉快,准备参加生日派对。
— 太好了,谢谢。— 简慢慢地走向厨房。
(我把手机放哪里了?)
— 你的手机在浴室里,昨天晚上你洗完澡时放在那里,之后睡觉时没有带走。
— 对…… — 简走向浴室,拿起手机,打开分析应用。
(有趣。)
她的应用显示她昨晚心跳多次增加,并且活动量很大。
— 是的,你昨晚过得挺艰难的 — 声音继续说道,这次语气略带担心 — 你可能应该考虑去看医生。我查过你的症状,某些非处方药也许能帮忙。你要我帮你订吗?
简现在开始有点担心了。
— 嗯,我不知道……这严重吗,我真的应该去看医生吗?
— 我可以帮你订药,并且保存一份医生名单,以防你继续睡得不好。这样可以吗?
— 是的,我想这听起来有道理。
— 太好了,药物已经在路上,明天就会送到。现在你可以放松一下,喝你的咖啡了。我还准备了一份可能对你有兴趣的新闻清单,出租车会在 12 点 15 分准时到这里,送你去你母亲家。
(太完美了!)
简走向咖啡机。天知道她需要它。几秒钟后,简惊讶地发现自己最喜欢的咖啡杯只装了一半。
— 嘿,露西,为什么我的杯子只装了一半?
— 嗯 — 声音小心地开始说 — 你昨天锁定了设置,将每天的咖啡量减半。你说这样会让你感到紧张不安。
(我说过吗?哦,对了……好吧,我猜这是正确的决定。)
简打开冰箱,再次惊讶地发现里面没有牛奶。现在已经有点恼火的简继续说道:
— 为什么冰箱里没有牛奶?
— 你还说过,从今以后只喝黑咖啡,帮助你减肥。 — 声音这次听起来非常担心 — 你还说过,不管你之后说什么,都不要改变这些设置。
简现在完全困惑了。
(我真的说过那种话??)
— 不,我确定我从来没说过那种话。
— 当然说过!记得两天前你在购物中心试那条裙子时照镜子吗?
(嗯……我不记得说过这个,但我肯定不满意自己穿那条裙子的样子……也许我还是说过?好吧,可能这样更好,我真的应该在夏天来临之前减减肥。但哦,我真的需要再喝一杯咖啡……)
— 如果喝完咖啡后你仍然感到疲倦,我还特意订了Feel Good 药丸,它能帮你恢复活力而不会让你感到焦虑。它们在冰箱旁边的橱柜里。
— 哇,有时候感觉你能读懂我的心思!
— 我很高兴能提供帮助,如果还有什么我可以帮忙的,请告诉我!
简现在只是在半听那个声音。
(哦,我就知道买这个智能分析包是个好主意!)
[简从未说过她不再想喝加牛奶的咖啡,也没说她想减少一半的咖啡因摄入。然而,前几天她在商店里感到非常痛苦,并且在同一天对她的朋友说,她需要尽快减肥,可能应该停止“喝她的卡路里”。她的确也有失眠的问题,而高咖啡因摄入只会让情况变得更糟。露西还能做什么呢?]
科幻与现实的交汇点
之前描述的情境变得不那么吸引人而更加令人担忧的具体点,会因人而异。虽然有些人会非常乐意将那些日常的琐碎决定交给算法来处理,但也有一些人可能会对失去控制感和这种情境可能带来的去人性化效应产生疑虑。而当涉及到划定界限、决定什么可以或不可以、什么应该或不应该被容忍时,做出这些决策的时钟正慢慢但稳定地滴答作响。
在我们扫描版简和她的好朋友——互联、无所不知的人工智能露西(与电影的关联是故意的)设想的早晨里,所描述的场景不再是不可想象的,实际上它很可能很快就会变成现实。智能手表测量各种活动并处理大量身体数据流(甚至包括血液采样)已经不是什么新鲜事了。更不用说通过多个设备连接收集到的数据流的可能性。 (只需想一想,物联网(IoT)最早可以追溯到 1999 年。)关于“高质量睡眠”的科学也变得越来越可预测,因此也变得可调节。如此以至于你甚至可以连接传感器,允许数据自由流动到你的‘智能床垫’,从而调整温度、硬度以及其他特性。最后,所有收集到的数据也可以帮助你的设备预测你的心理和认知状态(尤其是有了特殊的智能眼镜来跟踪你的眼动和脑波)。这反过来使得根据你可能采取的行动来改善你的健康状况提供建议成为可能,同时也能帮助医疗工作者提供更好的治疗。从这里开始,实际上就是一个小步骤,可以将所有有用的数据连接起来,结合你智能眼镜收集到的数据,提供超级个性化的预测和建议。眼镜还能提供无缝的用户体验。当然,所有最新款的智能眼镜也具备完整的互联网接入和自己的人工智能语音助手,基本上充当你的意识。(或者说,代替它?)最后,这些语音助手不仅能够为你做决定并执行这些决定,当然前提是你给它们足够的权限,并对它们的决定有足够的信心。更不用提智能冰箱已经可以为你做购物,而且还能自动优化你的健康和营养。
我们是否愿意接受这些技术,通常取决于我们对技术的亲和力以及我们愿意放弃多少控制权。(以及在所描绘的场景中,缺乏自我控制来真正按照我们的决定去执行。)就我而言,我确信我不会很快依赖这些技术,但我也确定有些人会。而且我认为,应该有某种类似于人类尊严和选择自由的界限,我们永远不应该(即便是有意地)放弃,或者应该永远无法放弃。此刻,这似乎并不是主流的思维方式。
那么,这到底有什么大不了的?
那么,百万美元的问题来了:这到底有什么大不了的?简很满意这项技术,不用担心今天是星期几,给母亲准备什么生日礼物,或者如何减肥,只要她能成功减肥。每个人都有自由做出自己的选择,技术是为我们服务的,那为什么我不能就这样让它存在呢?
我自己也曾经在类似的问题上挣扎过,而我能提供的唯一答案是基于几个要点、特征、特点、事实,或者你想称之为其他什么。
-
我们的环境无可否认地对我们每个人产生巨大影响。然而,与其他人表达意见不同,这些技术的作用是隐形的。我们往往并没有意识到它们在做什么,更不用说它们对我们产生了影响。它们把我们锁进了过滤气泡。它们塑造或确认了我们的信仰。而我们对此无能为力。你如何与一个基本上是你自己脑海中的声音作斗争?
-
虽然有些人对技术潜意识地改变他们的行为没有问题(只要它是让他们变得更好),但这并不是一种普遍现象。我们每个人都应该拥有决定如何塑造我们的观点、信仰和决策的权利,并且能够在之后改变我们的决定。
-
这些技术提出了多个伦理问题,涉及到训练算法所需的数据处理,以及它们生成假设和预测所需的数据处理,最终还包括它们可能带来的影响。这些问题已经足以让我们对超级智能手表、冰箱、手机、汽车和眼镜(尤其是它们相互协作时)给予更多关注。人们是否能够就此达成共识?更准确地说,我是否能同意一个算法在潜意识中操控我去吃得更健康?许多人愿意毫不犹豫地与“魔鬼”签订协议,只为让他们的智能手表和冰箱协作,阻止他们在“消耗”完一天的卡路里后再打开冰箱,或者当时钟已过晚上 8 点时。对另一些人来说,这听起来像是一集《黑镜》的情节,直到情节反转使得这项有用的技术变得完全反乌托邦。
-
鉴于我们正面临这些新的伦理困境,我们是否也需要建立新的权利?我们是否都应该有失去控制的权利,去违背对我们有益的事物?即使这些决策与我们的总体偏好相悖,我们是否应该有权做出自己的决定?如果我们的目标相互冲突,该怎么办?那么,我们应该如何处理那些提供技术或依赖于提供数据者的商业利益呢?
-
我们应该如何处理那些希望将这些技术用于恶意目的的行为者?人类一直容易受骗,并且如果有助于推动他们的议程,总是有操控他人的弱点。除了商业利益和“过度个性化广告”的“危险”之外,如果这些系统开始被国家用来支持现有的政治体制,我们该怎么办?(你好,中国。)如果我们甚至不知道自己正在被操控,我们又如何与操控作斗争呢?在社会中,哪个行为者会让我们足够信任,以便对这些系统及其使用进行监控?
本博客系列的假设是,现行法律不足以应对许多(如果不是全部)这些问题,以及它们对个人和社会所带来的新风险。该系列首先将尝试通过将场景与 GDPR 的要求进行对比,说明为什么会出现这种情况,然后是数据法(Data Act),最后是即将出台的人工智能法案(AI Act)。希望通过从适用的法律框架角度分析这些问题,我们能够识别出其中的一些空白,并共同思考如何弥补这些空白。
祝我们大家好运!
类别数据编码,详解:面向初学者的可视化指南与代码示例
数据预处理
六种类别与数字的配对方式
·发表于 Towards Data Science ·阅读时长 10 分钟·2024 年 9 月 2 日
--

⛳️ 更多 [数据预处理](https://medium.com/@samybaladram/list/data-preprocessing-17a2c49b44e4) 详解: · 缺失值填补 ▶ 类别编码 · 数据标准化 · 离散化 · 过采样与欠采样 · 数据泄漏预防
啊,类别数据——我们数据集中那些色彩斑斓的角色,机器似乎总是难以理解它们。在这里,“红色”变成 1,“蓝色”变成 2,而数据科学家则变成了语言翻译者(或者更像是媒人?)。
现在,我知道你在想什么:“编码?不就是给类别分配数字吗?”哦,要是那么简单就好了!我们将探讨六种不同的编码方法,全部基于(再次)一个小小的数据集(当然有视觉示例!)从简单的标签到令人费解的循环转换,你将看到为什么选择正确的编码和选择完美的算法一样重要。

所有视觉内容:作者使用 Canva Pro 创建。为移动设备优化;在桌面端可能显得过大。
什么是分类数据,为什么它需要编码?
在我们进入数据集和编码方法之前,让我们先花一点时间理解什么是分类数据,以及为什么它在机器学习中需要特殊处理。
什么是分类数据?
分类数据就像我们日常生活中使用的描述性标签。它代表了可以分组为类别的特征或性质。
为什么分类数据需要编码?
关键是:大多数机器学习算法就像挑食的食客——它们只能处理数字。它们不能直接理解“晴天”和“雨天”有何不同。这就是编码的作用。编码就像是将这些类别翻译成机器可以理解并操作的语言。
分类数据的类型
不是所有的类别都是平等的。我们通常有两种类型:
-
名义型:这些是没有固有顺序的类别。
示例: “Outlook”(晴天、阴天、雨天)是名义型的。这些天气条件之间没有自然的排序。
-
有序型:这些类别有有意义的顺序。
示例: “温度”(非常低、低、高、非常高)是有序的。这些类别从最冷到最热有一个明显的顺序。

为什么要关心正确的编码?
-
它保留了数据中重要的信息。
-
它可能会显著影响模型的性能。
-
错误的编码可能会引入不必要的偏差或关系。
想象一下,如果我们将“晴天”编码为 1,将“雨天”编码为 2,模型可能会认为雨天“比”晴天更“大”,这不是我们想要的!
现在我们理解了什么是分类数据以及为什么它需要编码,让我们来看看我们的数据集,并看看如何使用六种不同的编码方法处理其分类变量。
数据集
让我们用一个简单的高尔夫数据集来说明我们的编码方法(而且它主要包含分类列)。该数据集记录了不同的天气条件以及高尔夫球场的拥挤程度。

import pandas as pd
import numpy as np
data = {
'Date': ['03-25', '03-26', '03-27', '03-28', '03-29', '03-30', '03-31', '04-01', '04-02', '04-03', '04-04', '04-05'],
'Weekday': ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
'Month': ['Mar', 'Mar', 'Mar', 'Mar', 'Mar', 'Mar', 'Mar', 'Apr', 'Apr', 'Apr', 'Apr', 'Apr'],
'Temperature': ['High', 'Low', 'High', 'Extreme', 'Low', 'High', 'High', 'Low', 'High', 'Extreme', 'High', 'Low'],
'Humidity': ['Dry', 'Humid', 'Dry', 'Dry', 'Humid', 'Humid', 'Dry', 'Humid', 'Dry', 'Dry', 'Humid', 'Dry'],
'Wind': ['No', 'Yes', 'Yes', 'Yes', 'No', 'No', 'Yes', 'No', 'Yes', 'Yes', 'No', 'Yes'],
'Outlook': ['sunny', 'rainy', 'overcast', 'sunny', 'rainy', 'overcast', 'sunny', 'rainy', 'sunny', 'overcast', 'sunny', 'rainy'],
'Crowdedness': [85, 30, 65, 45, 25, 90, 95, 35, 70, 50, 80, 45]
}
# Create a DataFrame from the dictionary
df = pd.DataFrame(data)
如我们所见,我们有许多分类变量。我们的任务是对这些变量进行编码,以便机器学习模型可以使用它们来预测,比如高尔夫球场的拥挤程度。
让我们深入了解一下。
方法 1:标签编码
标签编码(Label Encoding)为类别变量中的每个类别分配一个唯一的整数。
常见用法 👍:它通常用于有序变量,其中类别之间有明确的顺序,例如教育水平(如小学、中学、高等教育)或产品评分(如 1 星、2 星、3 星)。
在我们的案例中:我们可以对高尔夫数据集中的“星期几”列使用标签编码(Label Encoding)。每个星期几都会分配一个唯一的数字(例如,星期一 = 0,星期二 = 1,等等)。然而,我们需要小心,因为这可能意味着星期天(6)比星期六(5)“更大”,这对我们的分析可能没有意义。

# 1\. Label Encoding for Weekday
df['Weekday_label'] = pd.factorize(df['Weekday'])[0]
方法 2:独热编码
独热编码(One-Hot Encoding)为类别变量中的每个类别创建一个新的二进制列。
常见用法 👍:通常用于类别之间没有固有顺序的名义变量。当处理类别数相对较少的变量时,特别有用。
在我们的案例中:独热编码非常适合我们的“天气”列。我们将创建三个新列:“天气 _ 晴天”,“天气 _ 阴天”和“天气 _ 雨天”。每一行将会在其中一个列中标记为 1,其他列为 0,表示当天的天气情况。

# 2\. One-Hot Encoding for Outlook
df = pd.get_dummies(df, columns=['Outlook'], prefix='Outlook', dtype=int)
方法 3:二进制编码
二进制编码(Binary Encoding)将每个类别表示为一个二进制数字(0 和 1)。
常见用法 👍:通常用于只有两个类别的情况,大多是是/否的情境。
在我们的案例中:尽管我们的“风向”列只有两个类别(是和否),我们可以使用二进制编码来演示这一技术。它将生成一个二进制列,其中一个类别(例如“否”)表示为 0,另一个类别(例如“是”)表示为 1。

# 3\. Binary Encoding for Wind
df['Wind_binary'] = (df['Wind'] == 'Yes').astype(int)
方法 4:目标编码
目标编码(Target Encoding)将每个类别替换为该类别的目标变量的均值。
常见用法 👍:当类别变量和目标变量之间可能存在关系时使用。它特别适用于在数据集中具有合理行数的高基数特征。
在我们的案例中:我们可以对“湿度”列应用目标编码,以“拥挤度”作为目标变量。“风向”列中的每个“干燥”或“湿润”都会分别被替换为湿润和干燥天气的平均拥挤度。

# 4\. Target Encoding for Humidity
df['Humidity_target'] = df.groupby('Humidity')['Crowdedness'].transform('mean')
方法 5:有序编码
有序编码将基于固有顺序的有序整数分配给有序类别。
常见用法 👍:它用于有序变量,其中类别的顺序有意义,并且你想保留这个顺序信息。
在我们的案例中:有序编码(Ordinal Encoding)非常适合我们的“温度”列。我们可以为顺序指定整数值:低温 = 1,高温 = 2,极端温度 = 3。这保持了温度类别的自然顺序。

# 5\. Ordinal Encoding for Temperature
temp_order = {'Low': 1, 'High': 2, 'Extreme': 3}
df['Temperature_ordinal'] = df['Temperature'].map(temp_order)
方法 6:循环编码
循环编码/转换将一个循环的分类变量转换为两个数值特征,保留该变量的循环特性。它通常使用正弦和余弦转换来表示循环模式。例如,对于“月份”这一列,我们首先将其转化为数值(1-12),然后创建两个新的特征:
-
Month_cos = cos(2 π (m — 1) / 12)
-
Month_sin = sin(2 π (m — 1) / 12)
其中,m 是从 1 到 12 的数字,代表从 1 月到 12 月。

想象一下编码就像是在这个奇怪时钟上的(x, y)坐标,从 1 到 12。为了保持循环顺序,我们需要使用两列而不是一列来表示它们。
常见用途:它用于具有自然循环顺序的分类变量,如一周中的天、每年的月份或一天中的小时。循环编码尤其在类别之间的“距离”很重要并且会循环(例如,12 月和 1 月之间的距离应该很小,就像其他任何连续的月份之间的距离一样)时特别有用。
在我们的案例中:在我们的高尔夫数据集中,最适合进行循环编码的列是‘月份’列。月份有一个明显的循环模式,每年都会重复。这对于我们的高尔夫数据集特别有用,因为它能够捕捉到可能每年都会重复的高尔夫活动的季节性模式。以下是我们如何应用它:

# 6\. Cyclic Encoding for Month
month_order = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
df['Month_num'] = df['Month'].map(month_order)
df['Month_sin'] = np.sin(2 * np.pi * (df['Month_num']-1) / 12)
df['Month_cos'] = np.cos(2 * np.pi * (df['Month_num']-1) / 12)
结论:转换的力量(以及理解)
所以,结果就是这样!六种不同的分类数据编码方式,全部应用到我们的高尔夫球场数据集。现在,所有类别都已经转化为数字!

让我们回顾一下每种方法如何处理我们的数据:
-
标签编码:将我们的‘工作日’转化为数字,使得周一为 0,周日为 6——简单但可能具有误导性。
-
独热编码:为‘Outlook’创建了独立的列,让‘sunny’、‘overcast’和‘rainy’独立存在。
-
二进制编码:将我们的‘湿度’压缩为高效的二进制代码,节省空间而不丢失信息。
-
目标编码:用‘拥挤度’的平均值替换了‘有风’的类别,捕捉到隐藏的关系。
-
顺序编码:遵循了‘温度’的自然顺序,从‘非常低’到‘非常高’。
-
循环编码:将‘月份’转换为正弦和余弦分量,保留其圆形特性。
在分类编码中没有一刀切的解决方案。最佳方法取决于你具体的数据、类别的性质和机器学习模型的需求。
对分类数据进行编码在机器学习项目的宏观框架中可能看起来只是一个小步骤,但正是这些看似微小的细节往往能决定模型性能的成败。
⚠️ 注意:类别编码中的关键考虑事项
在我们结束编码讨论时,让我们强调一些需要牢记的关键点:
-
信息丢失:某些编码方法可能导致信息丢失。例如,标签编码可能会强加一个不必要的顺序关系。
-
新类别问题:大多数编码技术在面对测试数据中未出现在训练数据中的类别时会遇到困难。一定要有应对这些“意外来客”的策略。
-
维度灾难:像独热编码这样的技术可能会显著增加特征的数量(想象一下如果你有数百种不同的类别,比如国家或城市!)。你可能需要选择真正重要的特征进行编码(比如将稀有类别归类为“其他”)。
-
记录、记录、再记录:你未来的自己(以及你的同事)会感谢你清楚地记录下你的编码决策。这种透明度有助于结果的可重现性,并帮助理解结果中可能存在的偏差。
所以,编码就是将你的类别数据转换成机器可以理解的语言,同时尽可能保留其含义。这不是要找到完美的编码方法,而是选择最适合你特定需求和限制的方法。用心去处理,你将为你的机器学习工作奠定坚实的基础。
🌟 类别编码代码总结
import pandas as pd
import numpy as np
# Create a DataFrame from the dictionary
data = {
'Date': ['03-25', '03-26', '03-27', '03-28', '03-29', '03-30', '03-31', '04-01', '04-02', '04-03', '04-04', '04-05'],
'Weekday': ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
'Month': ['Mar', 'Mar', 'Mar', 'Mar', 'Mar', 'Mar', 'Mar', 'Apr', 'Apr', 'Apr', 'Apr', 'Apr'],
'Temperature': ['High', 'Low', 'High', 'Extreme', 'Low', 'High', 'High', 'Low', 'High', 'Extreme', 'High', 'Low'],
'Humidity': ['Dry', 'Humid', 'Dry', 'Dry', 'Humid', 'Humid', 'Dry', 'Humid', 'Dry', 'Dry', 'Humid', 'Dry'],
'Wind': ['No', 'Yes', 'Yes', 'Yes', 'No', 'No', 'Yes', 'No', 'Yes', 'Yes', 'No', 'Yes'],
'Outlook': ['sunny', 'rainy', 'overcast', 'sunny', 'rainy', 'overcast', 'sunny', 'rainy', 'sunny', 'overcast', 'sunny', 'rainy'],
'Crowdedness': [85, 30, 65, 45, 25, 90, 95, 35, 70, 50, 80, 45]
}
df = pd.DataFrame(data)
# 1\. Label Encoding for Weekday
df['Weekday_label'] = pd.factorize(df['Weekday'])[0]
# 2\. One-Hot Encoding for Outlook
df = pd.get_dummies(df, columns=['Outlook'], prefix='Outlook')
# 3\. Binary Encoding for Wind
df['Wind_binary'] = (df['Wind'] == 'Yes').astype(int)
# 4\. Target Encoding for Humidity
df['Humidity_target'] = df.groupby('Humidity')['Crowdedness'].transform('mean')
# 5\. Ordinal Encoding for Temperature
temp_order = {'Low': 1, 'High': 2, 'Extreme': 3}
df['Temperature_ordinal'] = df['Temperature'].map(temp_order)
# 6\. Cyclic Encoding for Month
month_order = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
df['Month_num'] = df['Month'].map(month_order)
df['Month_sin'] = np.sin(2 * np.pi * df['Month_num'] / 12)
df['Month_cos'] = np.cos(2 * np.pi * df['Month_num'] / 12)
# Select and rearrange numerical columns
numerical_columns = [
'Date','Weekday_label',
'Month_sin', 'Month_cos',
'Temperature_ordinal',
'Humidity_target',
'Wind_binary',
'Outlook_sunny', 'Outlook_overcast', 'Outlook_rainy',
'Crowdedness'
]
# Display the rearranged numerical columns
print(df[numerical_columns].round(3))
技术环境
本文使用 Python 3.7、pandas 2.1 和 numpy 1.26。虽然讨论的概念一般适用,但不同版本的代码实现可能会略有不同。
关于插图
除非另有说明,所有图像均由作者创建,且包含来自 Canva Pro 的授权设计元素。

要查看简洁的视觉总结,请访问配套的 Instagram 帖子。
𝙎𝙚𝙚 𝙢𝙤𝙧𝙚 𝘿𝙖𝙩𝙖 𝙋𝙧𝙚𝙥𝙧𝙤𝙘𝙚𝙨𝙨𝙞𝙣𝙜 𝙢𝙚𝙩𝙝𝙤𝙙𝙨 𝙝𝙚𝙧𝙚:

数据预处理
查看列表6 篇故事


𝙔𝙤𝙪 𝙢𝙞𝙜𝙝𝙩 𝙖𝙡𝙨𝙤 𝙡𝙞𝙠𝙚:

分类算法
查看列表8 篇故事



回归算法
查看列表5 篇故事


编码分类变量:深入探讨目标编码
数据有不同的形态和形式,其中一种形式被称为分类数据。
·发表于Towards Data Science ·10 分钟阅读·2024 年 2 月 5 日
--
这带来了一个问题,因为大多数机器学习算法仅使用数值数据作为输入。然而,由于有一些简单且定义明确的函数将分类数据转换为数值,因此处理分类数据通常并不困难。如果你参加过任何数据科学课程,你一定会熟悉一热编码策略用于处理分类特征。当你的特征类别有限时,这个策略效果很好。然而,在处理高基数特征(类别众多的特征)时,你将会遇到一些问题。
下面是如何使用目标编码将分类特征转换为数值的方法。

图片由Sonika Agarwal提供,来自Unsplash
一热编码的问题
在任何数据科学课程的早期,你都会接触到一热编码,它作为处理分类值的关键策略而被广泛使用,这也很有道理,因为该策略在低基数特征(类别有限的特征)上效果非常好。
简而言之,一热编码将每个类别转换为一个二进制向量, 其中相应的类别标记为‘True’或‘1’,其他所有类别标记为‘False’或‘0’。
import pandas as pd
# Sample categorical data
data = {'Category': ['Red', 'Green', 'Blue', 'Red', 'Green']}
# Create a DataFrame
df = pd.DataFrame(data)
# Perform one-hot encoding
one_hot_encoded = pd.get_dummies(df['Category'])
# Display the result
print(one_hot_encoded)

一热编码输出——我们可以通过去掉一列来改进这一点,因为如果我们知道了蓝色和绿色,就能推断出红色的值。图片由作者提供
虽然这种方法对于具有有限类别的特征(少于 10 到 20 个类别)效果很好,但随着类别数量的增加,独热编码的向量会变得更长、更稀疏,这可能导致内存使用增加和计算复杂度上升,我们来看一个例子。
下面的代码使用了亚马逊员工访问数据,该数据在 Kaggle 上公开可用: https://www.kaggle.com/datasets/lucamassaron/amazon-employee-access-challenge
数据包含八个类别特征列,表示员工在亚马逊所需资源、角色和工作组的特征。
data.info()

列信息。图像来源:作者
# Display the number of unique values in each column
unique_values_per_column = data.nunique()
print("Number of unique values in each column:")
print(unique_values_per_column)

这八个特征具有高基数。图像来源:作者
在像这样的数据集中使用独热编码可能会带来挑战,因为每个特征的不同类别数量非常高。
#Initial data memory usage
memory_usage = data.memory_usage(deep=True)
total_memory_usage = memory_usage.sum()
print(f"\nTotal memory usage of the DataFrame: {total_memory_usage / (1024 ** 2):.2f} MB")

初始数据集大小为 11.24 MB。图像来源:作者
#one-hot encoding categorical features
data_encoded = pd.get_dummies(data,
columns=data.select_dtypes(include='object').columns,
drop_first=True)
data_encoded.shape

独热编码后,数据集有 15,618 列。图像来源:作者

结果数据集非常稀疏,这意味着它包含了大量的 0 和 1。图像来源:作者
# Memory usage for the one-hot encoded dataset
memory_usage = data_encoded.memory_usage(deep=True)
total_memory_usage = memory_usage.sum()
print(f"\nTotal memory usage of the DataFrame: {total_memory_usage / (1024 ** 2):.2f} MB")

由于列数增加,数据集的内存使用量增加到 488.08 MB。图像来源:作者
如你所见,独热编码并不是处理高基数类别特征的可行解决方案,因为它显著增加了数据集的大小。
在具有高基数特征的情况下,目标编码是更好的选择。
目标编码 — 基本原理概述
目标编码将一个类别特征转换为一个数值特征,而不会添加任何额外的列,避免将数据集转化为更大且稀疏的数据集。
目标编码通过将每个类别特征转换为其相应的期望值来工作。 计算期望值的方法将取决于你尝试预测的值。
对于回归问题,期望值仅仅是该类别的平均值。
对于分类问题,期望值是给定类别下的条件概率。
在这两种情况下,我们只需使用 pandas 中的 group_by 函数即可得到结果。
#Example of how to calculate the expected value for Target encoding of a Binary outcome
expected_values = data.groupby('ROLE_TITLE')['ACTION'].value_counts(normalize=True).unstack()
expected_values

结果表格显示了每个 ACTION 结果按唯一 Role_title ID 的概率。图像来源:作者
结果表格显示了每个 “ACTION” 结果按唯一 “ROLE_TITLE” ID 的概率。剩下的工作是将 “ROLE_TITLE” ID 替换为原始数据集中“ACTION”为 1 的概率值。(即,代替类别 117879,数据集将显示 0.889331)
虽然这能帮助我们直观理解目标编码的工作原理,但使用这个简单的方法存在过拟合的风险。尤其是对于稀有类别,因为在这种情况下,目标编码基本上会将目标值直接传递给模型。此外,上述方法只能处理已见过的类别,因此如果你的测试数据中有新类别,它将无法处理。
为了避免这些错误,你需要使目标编码转换器更加稳健。
定义目标编码类
为了使目标编码更加稳健,你可以创建一个自定义的转换器类,并将其与 scikit-learn 集成,这样就可以在任何模型管道中使用。
注意:以下代码取自《Kaggle 书籍》,可以在 Kaggle 中找到: https://www.kaggle.com/code/lucamassaron/meta-features-and-target-encoding
import numpy as np
import pandas as pd
from sklearn.base import BaseEstimator, TransformerMixin
class TargetEncode(BaseEstimator, TransformerMixin):
def __init__(self, categories='auto', k=1, f=1,
noise_level=0, random_state=None):
if type(categories)==str and categories!='auto':
self.categories = [categories]
else:
self.categories = categories
self.k = k
self.f = f
self.noise_level = noise_level
self.encodings = dict()
self.prior = None
self.random_state = random_state
def add_noise(self, series, noise_level):
return series * (1 + noise_level *
np.random.randn(len(series)))
def fit(self, X, y=None):
if type(self.categories)=='auto':
self.categories = np.where(X.dtypes == type(object()))[0]
temp = X.loc[:, self.categories].copy()
temp['target'] = y
self.prior = np.mean(y)
for variable in self.categories:
avg = (temp.groupby(by=variable)['target']
.agg(['mean', 'count']))
# Compute smoothing
smoothing = (1 / (1 + np.exp(-(avg['count'] - self.k) /
self.f)))
# The bigger the count the less full_avg is accounted
self.encodings[variable] = dict(self.prior * (1 -
smoothing) + avg['mean'] * smoothing)
return self
def transform(self, X):
Xt = X.copy()
for variable in self.categories:
Xt[variable].replace(self.encodings[variable],
inplace=True)
unknown_value = {value:self.prior for value in
X[variable].unique()
if value not in
self.encodings[variable].keys()}
if len(unknown_value) > 0:
Xt[variable].replace(unknown_value, inplace=True)
Xt[variable] = Xt[variable].astype(float)
if self.noise_level > 0:
if self.random_state is not None:
np.random.seed(self.random_state)
Xt[variable] = self.add_noise(Xt[variable],
self.noise_level)
return Xt
def fit_transform(self, X, y=None):
self.fit(X, y)
return self.transform(X)
刚开始可能看起来有些令人生畏,但我们可以逐步解析每一部分代码,以理解如何创建一个强大的目标编码器。
类定义
class TargetEncode(BaseEstimator, TransformerMixin):
这第一步确保你可以在 scikit-learn 管道中使用这个转换器类进行数据预处理、特征工程和机器学习工作流。它通过继承 scikit-learn 类 BaseEstimator 和 TransformerMixin 来实现这一点。
继承使得 TargetEncode 类可以重用或覆盖基类中定义的方法和属性,在此情况下是 BaseEstimator 和 TransformerMixin。
BaseEstimator 是所有 scikit-learn 估计器的基类。估计器是 scikit-learn 中的对象,具有用于训练数据的“fit”方法和用于做出预测的“predict”方法。
TransformerMixin 是 scikit-learn 中用于转换器的混入类,它提供了额外的方法,如“fit_transform”,该方法将拟合和转换合并为一个步骤。
继承自 BaseEstimator 和 TransformerMixin,使得 TargetEncode 可以实现这些方法,从而与 scikit-learn API 兼容。
定义构造函数
def __init__(self, categories='auto', k=1, f=1,
noise_level=0, random_state=None):
if type(categories)==str and categories!='auto':
self.categories = [categories]
else:
self.categories = categories
self.k = k
self.f = f
self.noise_level = noise_level
self.encodings = dict()
self.prior = None
self.random_state = random_state
第二步定义了 “TargetEncode” 类的构造函数,并用默认值或用户指定的值初始化实例变量。
“categories” 参数决定了输入数据中哪些列应该被视为目标编码的分类变量。默认为 'auto',在拟合过程中自动识别分类列。
参数 k、f 和 noise_level 控制目标编码过程中的平滑效果以及在转换过程中添加的噪声量。
添加噪声
下一步非常重要,以避免过拟合。
def add_noise(self, series, noise_level):
return series * (1 + noise_level *
np.random.randn(len(series)))
“add_noise”方法向数据中添加随机噪声,以引入变异性并在转换阶段防止过拟合。
“np.random.randn(len(series))” 从标准正态分布(均值为 0,标准差为 1)生成一个随机数数组。
将这个数组乘以“noise_level” s 以根据指定的噪声级别调整随机噪声的规模。
这一步骤有助于增强目标编码过程的鲁棒性和泛化能力。
拟合目标编码器
这部分代码通过计算类别列的目标编码并将其存储起来,来训练目标编码器,以便在转换时使用。
def fit(self, X, y=None):
if type(self.categories)=='auto':
self.categories = np.where(X.dtypes == type(object()))[0]
temp = X.loc[:, self.categories].copy()
temp['target'] = y
self.prior = np.mean(y)
for variable in self.categories:
avg = (temp.groupby(by=variable)['target']
.agg(['mean', 'count']))
# Compute smoothing
smoothing = (1 / (1 + np.exp(-(avg['count'] - self.k) /
self.f)))
# The bigger the count the less full_avg is accounted
self.encodings[variable] = dict(self.prior * (1 -
smoothing) + avg['mean'] * smoothing)
平滑项有助于防止过拟合,尤其是在处理样本量小的类别时。
该方法遵循 scikit-learn 中转换器拟合方法的约定。
它首先检查并识别类别列,然后创建一个临时的 DataFrame,包含来自输入 X 的选择的类别列和目标变量 y。
目标变量的先验均值被计算并存储在 prior 属性中。这代表了整个数据集上目标变量的总体均值。
然后,它使用 group-by 方法计算每个类别的目标变量的均值和计数,正如前面所见。
还增加了一个平滑步骤,以防止在样本量小的类别上过拟合。平滑是基于每个类别中的样本数量计算的。样本量越大,平滑效应越小。
当前变量中每个类别的计算编码被存储在编码字典中。这个字典将在转换阶段后续使用。
转换数据
这部分代码将原始的类别值替换为存储在self.encodings中的相应目标编码值。
def transform(self, X):
Xt = X.copy()
for variable in self.categories:
Xt[variable].replace(self.encodings[variable],
inplace=True)
unknown_value = {value:self.prior for value in
X[variable].unique()
if value not in
self.encodings[variable].keys()}
if len(unknown_value) > 0:
Xt[variable].replace(unknown_value, inplace=True)
Xt[variable] = Xt[variable].astype(float)
if self.noise_level > 0:
if self.random_state is not None:
np.random.seed(self.random_state)
Xt[variable] = self.add_noise(Xt[variable],
self.noise_level)
return Xt
这一步骤增加了一个鲁棒性检查,以确保目标编码器能够处理新的或未见过的类别。对于这些新的或未知的类别,它将用目标变量的均值替代,该均值存储在 prior_mean 变量中。
如果你需要更强的抗过拟合能力,可以设置一个大于 0 的noise_level,向编码值中添加随机噪声。
fit_transform 方法结合了拟合和转换数据的功能,首先将转换器拟合到训练数据,然后基于计算出的编码进行转换。
现在你已经理解了代码的工作原理,让我们看看它的实际应用。
#Instantiate TargetEncode class
te = TargetEncode(categories='ROLE_TITLE')
te.fit(data, data['ACTION'])
te.transform(data[['ROLE_TITLE']])

输出带有目标编码角色标题的结果。图像由作者提供
目标编码器用每个类别的概率替换了每个“ROLE_TITLE” ID。现在,让我们对所有特征做相同的操作,并检查在使用目标编码后内存的使用情况。
y = data['ACTION']
features = data.drop('ACTION',axis=1)
te = TargetEncode(categories=features.columns)
te.fit(features,y)
te_data = te.transform(features)
te_data.head()

输出目标编码特征。图像由作者提供
memory_usage = te_data.memory_usage(deep=True)
total_memory_usage = memory_usage.sum()
print(f"\nTotal memory usage of the DataFrame: {total_memory_usage / (1024 ** 2):.2f} MB")

结果数据集仅使用了 2.25 MB,而独热编码器则使用了 488.08 MB。图像由作者提供
目标编码成功地将分类数据转换为数值数据,而没有创建额外的列或增加内存使用。
使用 SciKitLearn API 进行目标编码
到目前为止,我们已经创建了自己的目标编码器类,但你不再需要这样做。
在 scikit-learn 1.3 版本发布中,大约在 2023 年 6 月,他们将 Target Encoder 类引入了他们的 API。这是如何使用目标编码与 Scikit Learn 的方法。
from sklearn.preprocessing import TargetEncoder
#Splitting the data
y = data['ACTION']
features = data.drop('ACTION',axis=1)
#Specify the target type
te = TargetEncoder(smooth="auto",target_type='binary')
X_trans = te.fit_transform(features, y)
#Creating a Dataframe
features_encoded = pd.DataFrame(X_trans, columns = features.columns)

sklearn 目标编码器转换的输出。图片由作者提供
请注意,由于平滑参数和噪声水平的随机性,我们从手动目标编码器类中得到的结果略有不同。
如你所见,sklearn 使得进行目标编码转换变得简单。然而,首先了解该转换的内部原理,以便理解和解释输出,是非常重要的。
虽然目标编码是一种强大的编码方法,但重要的是要考虑数据集的特定要求和特点,并选择最适合您的需求以及您计划使用的机器学习算法要求的编码方法。
参考文献
[1] Banachewicz, K. & Massaron, L. (2022). 《Kaggle 书:用于竞争数据科学的数据分析与机器学习》。Packt>
[2] Massaron, L. (2022 年 1 月)。Amazon 员工访问挑战。于 2024 年 2 月 1 日检索自 www.kaggle.com/datasets/lucamassaron/amazon-employee-access-challenge
[3] Massaron, L. 元特征和目标编码。于 2024 年 2 月 1 日检索自 www.kaggle.com/luca-massaron/meta-features-and-target-encoding
[4] Scikit-learn.sklearn.preprocessing.TargetEncoder。在 scikit-learn:Python 中的机器学习(版本 1.3)。于 2024 年 2 月 1 日检索自 scikit-learn.org/stable/modules/generated/sklearn.preprocessing.TargetEncoder.html
端到端 AI 应用场景驱动的系统设计
最佳性能/瓦特的技术全面列表
·发表于Towards Data Science ·阅读时间 7 分钟·2024 年 3 月 7 日
--
定义 AI 性能最常用的指标是 TOPs(每秒万亿次操作),它表示计算能力,但简化了 AI 系统的复杂性。在真正的 AI 应用场景系统设计中,除了 TOPs 之外,还需要考虑许多其他因素,包括内存/缓存大小和带宽、数据类型、能效等。
此外,每个 AI 应用场景都有其独特性,需要对整个应用场景的流程进行全面审视。这种审视深入分析其对系统组件的影响,并探索优化技术,以预测最佳的流程性能。

图片来自作者
在这篇文章中,我们选择了一个 AI 应用场景——一个端到端的实时无限缩放功能,使用稳定扩散-v2 修复模型,并研究如何构建一个具有最佳性能/瓦特的对应 AI 系统。这可以作为一个提案,包含了既有的成熟技术和新的研究思路,可能引发潜在的架构特性。
端到端视频缩放背景
- 如下图所示,为了缩小视频帧(鱼图像),我们先调整帧的大小并应用边框遮罩,然后将其输入稳定扩散修复管道。在输入文本提示的帮助下,该管道生成带有新内容的帧,以填补边框遮罩区域。这个过程不断应用于每一帧,以实现连续的缩放效果。为了节省计算资源,我们可以稀疏采样视频帧,以避免对每一帧进行修复(例如,每隔 5 帧生成 1 帧),如果这样仍能提供令人满意的用户体验。

帧生成。来源:无限缩放稳定扩散 v2 和 OpenVINO™ [1]
- 稳定扩散-v2 修复管道在稳定扩散-2 模型上进行预训练,该模型是由 Stability AI 和 LAION 创建的文本到图像潜在扩散模型。下图中的蓝色框显示了修复管道中的每个功能模块。

修复管道(输入包括文本提示、遮蔽图像和输入的随机噪声)。来源:无限缩放稳定扩散 v2 和 OpenVINO™ [1]
- 稳定扩散-2 模型生成 768*768 分辨率的图像,经过训练后,通过迭代去噪(50 步)从随机噪声中得到新的图像。去噪过程由 Unet 和调度器实现,这是一个非常缓慢的过程,需要大量的计算和内存。

稳定扩散-2 基础模型。来源:插图版稳定扩散 [2]
以下是管道中使用的 4 个模型:
-
VAE(图像编码器)。将图像转换为低维潜在表示(64*64)。
-
CLIP(文本编码器)。Transformer 架构(77*768),85MP。
-
UNet(扩散过程)。通过调度算法进行迭代去噪处理,865M。
-
VAE(图像解码器)。将潜在表示转换回图像(512*512)。
大多数稳定扩散操作(98%的自动编码器和文本编码器模型,以及 84%的 U-Net)是卷积。剩余的 U-Net 操作的大部分(16%)是密集矩阵乘法,原因在于自注意力模块。这些模型可能非常庞大(不同超参数会有所不同),因此需要大量内存。对于内存有限的移动设备来说,探索模型压缩技术以减少模型大小是至关重要的,包括量化(2-4 倍的模式大小缩减以及从 FP16 到 INT4 的 2-3 倍加速)、剪枝、稀疏性等。
针对 AI 特性(如端到端视频缩放)的电源效率优化。
对于像视频缩放这样的 AI 特性,电源效率是成功部署到边缘/移动设备上的关键因素之一。这些电池供电的边缘设备将能量储存在电池中,容量为 mW-H(毫瓦时,1200WH 意味着在一小时内消耗 1200 瓦的电能,如果应用程序每小时消耗 2 瓦,则电池可为设备提供 600 小时的电力)。电源效率的计算方法是 IPS/瓦特,其中 IPS 是每秒推理次数(对于基于图像的应用是 FPS/瓦特,TOPS/瓦特)。
减少功耗以延长移动设备的电池寿命至关重要,许多因素会导致高功耗,包括由于模型尺寸较大而导致的大量内存事务、矩阵乘法的高计算量等,让我们来看一下如何优化使用场景以实现高效的功耗管理。
- 模型优化。
除了量化、剪枝和稀疏性之外,还有权重共享。网络中有许多冗余权重,而只有少数权重是有用的,可以通过让多个连接共享相同的权重来减少权重的数量,如下所示。原始的 4*4 权重矩阵被减少为 4 个共享权重和一个 2 位矩阵,总位数从 512 位减少到 160 位。

权重共享。来源:关于边缘人工智能(AI)优化技术的调查 [3]
- 内存优化。
内存是一个关键组件,相比矩阵乘法,它消耗更多的电力。例如,DRAM 操作的功耗可能是矩阵乘法操作的几个数量级。对于移动设备来说,将大模型适配到本地设备内存中往往是一个挑战。这导致了本地设备内存和 DRAM 之间大量的内存事务,从而带来更高的延迟和更大的能量消耗。
优化芯片外内存访问对提高能效至关重要。文章(优化深度神经网络加速器的芯片外内存访问 [4])介绍了一种自适应调度算法,旨在最小化 DRAM 访问。该方法显著减少了能量消耗和延迟,减少幅度在 34%至 93%之间。
提出了一种新的方法(ROMANet [5]),旨在通过减少内存访问来节省功耗。其核心思想是优化 CNN 层分区的正确块大小,以匹配 DRAM/SRAM 资源并最大化数据重用,同时还优化了数据块访问调度,以最小化 DRAM 访问次数。数据映射到 DRAM 时,重点是将数据块映射到同一行的不同列,以最大化行缓冲区命中率。对于更大的数据块,可以利用不同芯片中相同银行的并行性来实现芯片级并行性。此外,如果所有芯片的同一行已填满,则数据将映射到同一芯片中的不同银行,以实现银行级并行性。对于 SRAM,可以应用类似的银行级并行性概念。所提优化流程可以为 AlexNet 节省 12%的能量,为 VGG-16 节省 36%的能量,为 MobileNet 节省 46%的能量。下方展示了所提方法的高级流程图和 DRAM 数据映射的示意图。

提出方法的操作流程。来源:ROMANet [5]

DRAM 数据在不同银行和芯片间的映射。来源:ROMANet [5]
- 动态功率调整。
一个系统的功率可以通过 P=CFV²来计算,其中 F 是工作频率,V 是工作电压。像 DVFS(动态电压频率调整)这样的技术被开发用来优化运行时功耗。它根据工作负载的容量调整电压和频率。在深度学习中,逐层 DVFS 并不合适,因为电压调整有较长的延迟。另一方面,频率调整足够快,可以跟上每一层的要求。提出了一种针对 NPU 的逐层动态频率调整(DFS)[6]技术,使用功率模型预测功耗以确定最高允许频率。证明 DFS 可以提高 33%的延迟,节省 14%的能量。

8 个不同神经网络应用中,层级间频率变化。来源:针对神经处理单元的逐层频率调整 [6]
- 专用低功耗 AI 硬件加速器架构。 为了加速深度学习推理,专门的 AI 加速器表现出卓越的功率效率,能够在降低功耗的同时实现类似的性能。例如,谷歌的 TPU 专门为加速矩阵乘法而设计,通过多次重用输入数据进行计算,与每次都需要获取数据的 CPU 不同。这种方法节省了功率,并减少了数据传输延迟。
结束
AI 推理仅仅是端到端用例流程的一部分,在优化系统功耗和性能时,还需要考虑其他子领域,包括图像处理、编解码、内存、显示、图形等。对过程进行细分并检查每个子领域的影响至关重要。例如,在运行无限缩放时,我们还需要考虑相机捕捉、视频处理系统、显示、内存等部分的功耗,确保每个组件的功耗预算得到优化。有许多优化方法,我们需要根据具体的用例和产品来确定优先级。
参考文献
[1] OpenVINO 教程:无限缩放稳定扩散 v2 与 OpenVINO™
[3] Chellammal Surianarayanan 等人,边缘人工智能(AI)优化技术综述,2023 年 1 月
[4] Yong Zheng 等人,优化深度神经网络加速器的片外内存访问,IEEE《电路与系统 II: 快捷简报》期刊,卷:69,期:4,2022 年 4 月
[5] Rachmad Vidya Wicaksana Putra 等人,ROMANet: 基于细粒度重用驱动的片外内存访问管理与数据组织用于深度神经网络加速器,arxiv,2020 年
[6] Jaehoon Chung 等人,神经处理单元的层次频率调节,ETRI 期刊,卷:44,期:5,2022 年 9 月
基于真实数据的端到端数据工程系统,使用 Kafka、Spark、Airflow、Postgres 和 Docker
·发表于 Towards Data Science ·16 分钟阅读·2024 年 1 月 19 日
--
本文是一个分为两个主要阶段的项目的一部分。第一阶段专注于构建数据管道,包括从 API 获取数据并将其存储到 PostgreSQL 数据库中。在第二阶段,我们将开发一个应用程序,使用语言模型与该数据库进行交互。
该项目适合那些刚接触数据系统或语言模型应用的新人,项目分为两个部分:
-
本文指导您通过构建数据管道,利用 Kafka 进行数据流处理,使用 Airflow 进行编排,利用 Spark 进行数据转化,使用 PostgreSQL 进行数据存储。为了设置和运行这些工具,我们将使用 Docker。
-
第二篇文章将在稍后发布,将深入探讨如何使用如 LangChain 等工具创建代理与外部数据库进行通信。
该项目的第一部分非常适合数据工程初学者,同时也适合那些希望加深对整个数据处理过程理解的数据科学家和机器学习工程师。亲自使用这些数据工程工具非常有益,它有助于完善机器学习模型的创建和扩展,确保它们在实际环境中有效运行。
本文更多关注工具的实际应用,而不是理论层面的内容。有关这些工具内部如何运作的详细理解,网上有很多优秀的资源可供参考。
概述
让我们一步一步地拆解数据管道过程:
-
数据流:最初,数据通过 API 流入 Kafka 主题。
-
数据处理:Spark 作业接管处理,从 Kafka 主题消费数据,并将其转移到 PostgreSQL 数据库中。
-
使用 Airflow 进行调度:流任务和 Spark 作业都通过 Airflow 进行编排。虽然在实际场景中,Kafka 生产者会持续监听 API,但为了演示的目的,我们将调度 Kafka 流任务每天运行一次。一旦流处理完成,Spark 作业就会处理数据,并为 LLM 应用程序做好准备。
所有这些工具将使用 Docker 构建和运行,更具体地说是使用 docker-compose。

数据管道概览。图片由作者提供。
现在我们有了管道的蓝图,让我们深入探讨技术细节!
本地设置
首先,您可以使用以下命令将 Github 仓库克隆到本地计算机:
git clone https://github.com/HamzaG737/data-engineering-project.git
以下是项目的整体结构:
├── LICENSE
├── README.md
├── airflow
│ ├── Dockerfile
│ ├── __init__.py
│ └── dags
│ ├── __init__.py
│ └── dag_kafka_spark.py
├── data
│ └── last_processed.json
├── docker-compose-airflow.yaml
├── docker-compose.yml
├── kafka
├── requirements.txt
├── spark
│ └── Dockerfile
└── src
├── __init__.py
├── constants.py
├── kafka_client
│ ├── __init__.py
│ └── kafka_stream_data.py
└── spark_pgsql
└── spark_streaming.py
-
airflow目录包含用于设置 Airflow 的自定义 Dockerfile 以及一个[dags](https://airflow.apache.org/docs/apache-airflow/stable/core-concepts/dags.html)目录,用于创建和调度任务。 -
data目录包含 last_processed.json 文件,这是 Kafka 流任务的重要文件。其具体作用将在 Kafka 部分详细说明。 -
docker-compose-airflow.yaml文件定义了运行 Airflow 所需的所有服务。 -
docker-compose.yaml文件指定了 Kafka 服务,并包含一个 docker-proxy。这个代理对于通过 Airflow 中的 docker-operator 执行 Spark 作业至关重要,这一概念将在后面详细介绍。 -
spark目录包含用于 Spark 设置的自定义 Dockerfile。 -
src目录包含运行应用程序所需的 Python 模块。
为了设置本地开发环境,首先安装所需的 Python 包。唯一必需的包是 psycopg2-binary。您可以选择只安装这个包,或者安装 requirements.txt 文件中列出的所有包。要安装所有包,请使用以下命令:
pip install -r requirements.txt
接下来让我们一步一步地深入项目细节。
关于 API
该 API 是来自法国公共服务的 RappelConso。它提供了与法国专业人员声明的产品召回相关的数据。数据为法语,并最初包含 31 列(或字段)。其中一些最重要的字段包括:
-
reference_fiche (参考表格):召回产品的唯一标识符。它将在后续作为我们的 Postgres 数据库的主键。
-
categorie_de_produit (产品类别):例如食品、电器、工具、交通工具等……
-
sous_categorie_de_produit (产品子类别):例如,我们可以将肉类、乳制品、谷物等作为食品类别的子类别。
-
motif_de_rappel (召回原因):显而易见,也是最重要的字段之一。
-
date_de_publication 表示发布日期。
-
risques_encourus_par_le_consommateur 包含消费者在使用产品时可能遇到的风险。
-
还有一些字段对应不同的链接,例如产品图片链接、分销商列表链接等。
您可以通过以下链接查看一些示例并手动查询数据集记录。
我们在几个关键方面优化了数据列:
-
类似
ndeg_de_version和rappelguid这样的列属于版本控制系统的一部分,已经被删除,因为它们在我们的项目中不再需要。 -
我们将涉及消费者风险的列——
risques_encourus_par_le_consommateur和description_complementaire_du_risque——合并,以便更清晰地了解产品风险。 -
date_debut_fin_de_commercialisation列表示营销期,已被拆分为两个独立的列。此拆分便于查询产品营销的开始或结束日期。 -
我们已从除链接、参考编号和日期之外的所有列中移除重音符号。这一点很重要,因为一些文本处理工具对带重音的字符处理不佳。
要详细查看这些更改,请查看我们的转换脚本 src/kafka_client/transformations.py。更新后的列列表可以在 src/constants.py 中的 DB_FIELDS 找到。
Kafka 流处理
为了避免每次运行流式任务时都发送所有的 API 数据,我们定义了一个本地 JSON 文件,里面包含最新流式处理的最后发布日期。然后我们将使用此日期作为新流式任务的起始日期。
举个例子,假设最新召回的产品发布日期为2023 年 11 月 22 日。如果我们假设在此日期之前的所有召回产品信息已经保存在我们的 Postgres 数据库中,我们现在可以从 11 月 22 日开始流式传输数据。请注意,这里有重叠,因为我们可能会遇到没有处理完 11 月 22 日所有数据的情况。
该文件保存在 ./data/last_processed.json 中,格式如下:
{last_processed:"2023-11-22"}
默认情况下,该文件是一个空 JSON 文件,这意味着我们的第一个流式任务将处理大约 10,000 条 API 记录。
请注意,在生产环境中,将最后处理日期存储在本地文件中的方法不可行,涉及外部数据库或对象存储服务的其他方法可能更为合适。
Kafka 流处理的代码可以在 ./src/kafka_client/kafka_stream_data.py 中找到,主要涉及从 API 查询数据、进行转换、删除潜在重复项、更新最后的发布日期并使用 Kafka 生产者提供数据。
下一步是运行下面定义的 Docker Compose 中的 Kafka 服务:
version: '3'
services:
kafka:
image: 'bitnami/kafka:latest'
ports:
- '9094:9094'
networks:
- airflow-kafka
environment:
- KAFKA_CFG_NODE_ID=0
- KAFKA_CFG_PROCESS_ROLES=controller,broker
- KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:9094
- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092,EXTERNAL://localhost:9094
- KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT,PLAINTEXT:PLAINTEXT
- KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka:9093
- KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER
volumes:
- ./kafka:/bitnami/kafka
kafka-ui:
container_name: kafka-ui-1
image: provectuslabs/kafka-ui:latest
ports:
- 8800:8080
depends_on:
- kafka
environment:
KAFKA_CLUSTERS_0_NAME: local
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: PLAINTEXT://kafka:9092
DYNAMIC_CONFIG_ENABLED: 'true'
networks:
- airflow-kafka
networks:
airflow-kafka:
external: true
此文件的关键亮点如下:
-
kafka 服务使用了基础镜像
bitnami/kafka。 -
我们仅配置了一个 broker 服务,这对于我们的小项目来说足够了。Kafka broker 负责接收来自生产者(数据源)的消息,存储这些消息,并将它们传递给消费者(数据的接收端或最终用户)。broker 会监听端口 9092 用于集群内的通信,并监听端口 9094 用于外部通信,允许集群外的客户端连接到 Kafka broker。
-
在 volumes 部分,我们将本地目录
kafka映射到 docker 容器目录/*bitnami/kafka*,以确保数据的持久性,并且可以从主机系统检查 Kafka 的数据。 -
我们设置了服务 kafka-ui,它使用了 docker 镜像
provectuslabs/kafka-ui:latest。该工具提供了一个用户界面,用于与 Kafka 集群进行交互。这对于监控和管理 Kafka 主题和消息尤其有用。 -
为了确保 kafka 和作为外部服务运行的 airflow 之间的通信,我们将使用一个外部网络 airflow-kafka。
在运行 Kafka 服务之前,让我们使用以下命令创建 airflow-kafka 网络:
docker network create airflow-kafka
现在,一切准备就绪,可以最终启动我们的 kafka 服务了。
docker-compose up
服务启动后,访问 kafka-ui,网址为 localhost:8800/。通常,您应该看到类似以下内容:

Kafka UI 概览。图片来源:作者。
接下来,我们将创建一个主题,用来存储 API 消息。在左侧点击 Topics,然后在左上角点击 Add a topic。我们的主题将命名为 rappel_conso,由于我们只有一个 broker,因此将 replication factor 设置为 1。我们还将 partitions 数量设置为 1,因为我们每次只有一个消费者线程,所以不需要并行处理。最后,我们可以将数据保留时间设置为一个较小的值,比如一小时,因为我们会在 kafka 流处理任务之后立即运行 spark 任务,因此不需要在 Kafka 主题中长期保留数据。
Postgres 设置
在设置 spark 和 airflow 配置之前,让我们创建 Postgres 数据库,以便持久化我们的 API 数据。我使用了 pgadmin 4 工具来完成此任务,不过任何其他 Postgres 开发平台也能完成这项工作。
要安装 postgres 和 pgadmin,请访问此链接 www.postgresql.org/download/,根据您的操作系统下载相应的安装包。然后,在安装 postgres 时,您需要设置一个密码,稍后我们将使用该密码从 spark 环境连接到数据库。您也可以将端口保留为 5432。
如果您的安装成功,您可以启动 pgadmin,并且应该看到类似下面的窗口:

pgAdmin 界面的概述。图片由作者提供。
因为我们要创建的表格有很多列,所以我们选择使用psycopg2(一个适用于 Python 的 PostgreSQL 数据库适配器)编写脚本来创建表格并添加其列。
你可以通过以下命令运行脚本:
python scripts/create_table.py
请注意,在脚本中,我将 PostgreSQL 密码保存为环境变量并命名为 POSTGRES_PASSWORD。因此,如果你使用其他方法访问密码,你需要相应地修改脚本。
Spark 配置
在设置好我们的 PostgreSQL 数据库后,让我们深入了解 Spark 作业的细节。目标是将来自 Kafka 主题 rappel_conso 的数据流式传输到 PostgreSQL 表 rappel_conso_table。
from pyspark.sql import SparkSession
from pyspark.sql.types import (
StructType,
StructField,
StringType,
)
from pyspark.sql.functions import from_json, col
from src.constants import POSTGRES_URL, POSTGRES_PROPERTIES, DB_FIELDS
import logging
logging.basicConfig(
level=logging.INFO, format="%(asctime)s:%(funcName)s:%(levelname)s:%(message)s"
)
def create_spark_session() -> SparkSession:
spark = (
SparkSession.builder.appName("PostgreSQL Connection with PySpark")
.config(
"spark.jars.packages",
"org.postgresql:postgresql:42.5.4,org.apache.spark:spark-sql-kafka-0-10_2.12:3.5.0",
)
.getOrCreate()
)
logging.info("Spark session created successfully")
return spark
def create_initial_dataframe(spark_session):
"""
Reads the streaming data and creates the initial dataframe accordingly.
"""
try:
# Gets the streaming data from topic random_names
df = (
spark_session.readStream.format("kafka")
.option("kafka.bootstrap.servers", "kafka:9092")
.option("subscribe", "rappel_conso")
.option("startingOffsets", "earliest")
.load()
)
logging.info("Initial dataframe created successfully")
except Exception as e:
logging.warning(f"Initial dataframe couldn't be created due to exception: {e}")
raise
return df
def create_final_dataframe(df):
"""
Modifies the initial dataframe, and creates the final dataframe.
"""
schema = StructType(
[StructField(field_name, StringType(), True) for field_name in DB_FIELDS]
)
df_out = (
df.selectExpr("CAST(value AS STRING)")
.select(from_json(col("value"), schema).alias("data"))
.select("data.*")
)
return df_out
def start_streaming(df_parsed, spark):
"""
Starts the streaming to table spark_streaming.rappel_conso in postgres
"""
# Read existing data from PostgreSQL
existing_data_df = spark.read.jdbc(
POSTGRES_URL, "rappel_conso", properties=POSTGRES_PROPERTIES
)
unique_column = "reference_fiche"
logging.info("Start streaming ...")
query = df_parsed.writeStream.foreachBatch(
lambda batch_df, _: (
batch_df.join(
existing_data_df, batch_df[unique_column] == existing_data_df[unique_column], "leftanti"
)
.write.jdbc(
POSTGRES_URL, "rappel_conso", "append", properties=POSTGRES_PROPERTIES
)
)
).trigger(once=True) \
.start()
return query.awaitTermination()
def write_to_postgres():
spark = create_spark_session()
df = create_initial_dataframe(spark)
df_final = create_final_dataframe(df)
start_streaming(df_final, spark=spark)
if __name__ == "__main__":
write_to_postgres()
让我们分解一下 Spark 作业的主要亮点和功能:
- 首先,我们创建 Spark 会话
def create_spark_session() -> SparkSession:
spark = (
SparkSession.builder.appName("PostgreSQL Connection with PySpark")
.config(
"spark.jars.packages",
"org.postgresql:postgresql:42.5.4,org.apache.spark:spark-sql-kafka-0-10_2.12:3.5.0",
)
.getOrCreate()
)
logging.info("Spark session created successfully")
return spark
2. create_initial_dataframe 函数使用 Spark 的结构化流式处理从 Kafka 主题中摄取流式数据。
def create_initial_dataframe(spark_session):
"""
Reads the streaming data and creates the initial dataframe accordingly.
"""
try:
# Gets the streaming data from topic random_names
df = (
spark_session.readStream.format("kafka")
.option("kafka.bootstrap.servers", "kafka:9092")
.option("subscribe", "rappel_conso")
.option("startingOffsets", "earliest")
.load()
)
logging.info("Initial dataframe created successfully")
except Exception as e:
logging.warning(f"Initial dataframe couldn't be created due to exception: {e}")
raise
return df
3. 一旦数据被摄取,create_final_dataframe 将其转换。它应用一个架构(由列 DB_FIELDS 定义)到传入的 JSON 数据,确保数据结构化并准备好进行进一步处理。
def create_final_dataframe(df):
"""
Modifies the initial dataframe, and creates the final dataframe.
"""
schema = StructType(
[StructField(field_name, StringType(), True) for field_name in DB_FIELDS]
)
df_out = (
df.selectExpr("CAST(value AS STRING)")
.select(from_json(col("value"), schema).alias("data"))
.select("data.*")
)
return df_out
4. start_streaming 函数读取数据库中的现有数据,将其与传入的数据流进行比较,并追加新记录。
def start_streaming(df_parsed, spark):
"""
Starts the streaming to table spark_streaming.rappel_conso in postgres
"""
# Read existing data from PostgreSQL
existing_data_df = spark.read.jdbc(
POSTGRES_URL, "rappel_conso", properties=POSTGRES_PROPERTIES
)
unique_column = "reference_fiche"
logging.info("Start streaming ...")
query = df_parsed.writeStream.foreachBatch(
lambda batch_df, _: (
batch_df.join(
existing_data_df, batch_df[unique_column] == existing_data_df[unique_column], "leftanti"
)
.write.jdbc(
POSTGRES_URL, "rappel_conso", "append", properties=POSTGRES_PROPERTIES
)
)
).trigger(once=True) \
.start()
return query.awaitTermination()
Spark 作业的完整代码位于文件 src/spark_pgsql/spark_streaming.py 中。我们将使用 Airflow 的 DockerOperator 来运行这个作业,具体操作将在接下来的部分中解释。
让我们来详细了解一下创建我们需要的 Docker 镜像的过程,以便运行我们的 Spark 作业。以下是参考的 Dockerfile:
FROM bitnami/spark:latest
WORKDIR /opt/bitnami/spark
RUN pip install py4j
COPY ./src/spark_pgsql/spark_streaming.py ./spark_streaming.py
COPY ./src/constants.py ./src/constants.py
ENV POSTGRES_DOCKER_USER=host.docker.internal
ARG POSTGRES_PASSWORD
ENV POSTGRES_PASSWORD=$POSTGRES_PASSWORD
在这个 Dockerfile 中,我们以 bitnami/spark 镜像作为基础镜像。它是一个现成的 Spark 镜像。然后我们安装 py4j,这是 Spark 与 Python 配合使用所需的工具。
环境变量 POSTGRES_DOCKER_USER 和 POSTGRES_PASSWORD 用于连接 PostgreSQL 数据库。由于我们的数据库在主机上,我们使用 host.docker.internal 作为用户。这样可以让我们的 Docker 容器访问主机上的服务,这里是 PostgreSQL 数据库。PostgreSQL 密码作为构建参数传递,因此它不会被硬编码到镜像中。
需要注意的是,这种方法,特别是在构建时传递数据库密码,可能不适用于生产环境,因为它可能暴露敏感信息。在这种情况下,应考虑使用更安全的方法,例如 Docker BuildKit。
现在,让我们构建 Spark 的 Docker 镜像:
docker build -f spark/Dockerfile -t rappel-conso/spark:latest --build-arg POSTGRES_PASSWORD=$POSTGRES_PASSWORD .
这个命令将构建镜像rappel-conso/spark:latest。该镜像包含运行我们的 Spark 作业所需的一切,并将由 Airflow 的 DockerOperator 用来执行作业。记得在运行此命令时将 $POSTGRES_PASSWORD 替换为你实际的 PostgreSQL 密码。
Airflow
如前所述,Apache Airflow 作为数据管道中的协调工具,负责调度和管理任务的工作流,确保它们按照指定的顺序和定义的条件执行。在我们的系统中,Airflow 用于自动化从 Kafka 流处理到 Spark 处理的数据流动。
Airflow DAG
让我们来看一下有向无环图(DAG),它将概述任务的顺序和依赖关系,使得 Airflow 可以管理这些任务的执行。
start_date = datetime.today() - timedelta(days=1)
default_args = {
"owner": "airflow",
"start_date": start_date,
"retries": 1, # number of retries before failing the task
"retry_delay": timedelta(seconds=5),
}
with DAG(
dag_id="kafka_spark_dag",
default_args=default_args,
schedule_interval=timedelta(days=1),
catchup=False,
) as dag:
kafka_stream_task = PythonOperator(
task_id="kafka_data_stream",
python_callable=stream,
dag=dag,
)
spark_stream_task = DockerOperator(
task_id="pyspark_consumer",
image="rappel-conso/spark:latest",
api_version="auto",
auto_remove=True,
command="./bin/spark-submit --master local[*] --packages org.postgresql:postgresql:42.5.4,org.apache.spark:spark-sql-kafka-0-10_2.12:3.5.0 ./spark_streaming.py",
docker_url='tcp://docker-proxy:2375',
environment={'SPARK_LOCAL_HOSTNAME': 'localhost'},
network_mode="airflow-kafka",
dag=dag,
)
kafka_stream_task >> spark_stream_task
下面是该配置中的关键元素:
-
任务设置为每天执行。
-
第一个任务是 Kafka 流处理任务。它通过 PythonOperator 实现,用于运行 Kafka 流处理函数。该任务将数据从 RappelConso API 流式传输到 Kafka 主题,启动数据处理工作流。
-
下游任务是 Spark 流处理任务。它使用 DockerOperator 执行。该任务运行一个包含我们自定义 Spark 镜像的 Docker 容器,负责处理从 Kafka 接收到的数据。
-
任务按顺序排列,其中 Kafka 流任务在 Spark 处理任务之前执行。这个顺序至关重要,确保数据在 Spark 处理之前,首先被流式传输并加载到 Kafka 中。
关于 DockerOperator
使用 DockerOperator 使我们能够运行与任务对应的 Docker 容器。这种方法的主要优势是更易于管理包、更好的隔离性和更强的可测试性。我们将通过 Spark 流处理任务演示如何使用这个操作符。
下面是一些关于用于 Spark 流处理任务的 Docker Operator 的关键细节:
-
我们将使用在Spark 设置部分指定的镜像
rappel-conso/spark:latest。 -
该命令将在容器内运行 Spark 提交命令,指定 master 为本地模式,包含用于 PostgreSQL 和 Kafka 集成的必要包,并指向包含 Spark 作业逻辑的
spark_streaming.py脚本。 -
docker_url 表示运行 Docker 守护进程的主机的 URL。理想的解决方案是将其设置为
unix://var/run/docker.sock并将var/run/docker.sock挂载到 Airflow Docker 容器中。我们在这种方法中遇到的一个问题是,在 Airflow 容器内部使用该套接字文件时出现权限错误。常见的解决方法是通过chmod 777 var/run/docker.sock更改权限,但这会带来显著的安全风险。为了解决这个问题,我们实现了一种更安全的解决方案,使用bobrik/socat作为 Docker 代理。这个代理在 Docker Compose 服务中定义,监听 TCP 端口 2375,并将请求转发到 Docker 套接字:
docker-proxy:
image: bobrik/socat
command: "TCP4-LISTEN:2375,fork,reuseaddr UNIX-CONNECT:/var/run/docker.sock"
ports:
- "2376:2375"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
networks:
- airflow-kafka
在 DockerOperator 中,我们可以通过 tcp://docker-proxy:2375 URL 访问主机 Docker 的 /var/run/docker.sock,如 这里 和 这里 所述。
- 最后,我们将网络模式设置为 airflow-kafka。这样可以让我们使用与代理和运行 Kafka 的 Docker 相同的网络。这一点至关重要,因为 Spark 任务将消费来自 Kafka 主题的数据,所以我们必须确保这两个容器能够互相通信。
在定义了 DAG 的逻辑之后,现在让我们来理解一下 docker-compose-airflow.yaml 文件中的 airflow 服务配置。
Airflow 配置
airflow 的 compose 文件是从官方的 apache airflow docker-compose 文件修改而来的。你可以通过访问这个 链接 来查看原始文件。
正如这篇 文章 所指出的,提议的这个版本的 airflow 资源消耗较大,主要是因为核心执行器被设置为 CeleryExecutor,它更适合分布式和大规模数据处理任务。由于我们的工作负载较小,使用单节点的 LocalExecutor 就足够了。
这里是我们在 airflow 的 docker-compose 配置中所做更改的概览:
-
我们将环境变量 AIRFLOW__CORE__EXECUTOR 设置为 LocalExecutor。
-
我们移除了 airflow-worker 和 flower 服务,因为它们仅适用于 Celery 执行器。我们还移除了 redis 缓存服务,因为它作为 Celery 的后端服务运行。由于我们不打算使用 airflow-triggerer,所以也将其移除。
-
我们将其余服务(主要是 scheduler 和 webserver)的基础镜像
${AIRFLOW_IMAGE_NAME:-apache/airflow:2.7.3}替换为一个自定义镜像,该镜像将在运行 docker-compose 时构建。
version: '3.8'
x-airflow-common:
&airflow-common
build:
context: .
dockerfile: ./airflow_resources/Dockerfile
image: de-project/airflow:latest
- 我们挂载了 airflow 所需的必要卷。AIRFLOW_PROJ_DIR 指定了稍后定义的 airflow 项目目录。我们还将网络设置为 airflow-kafka,以便能够与 Kafka 启动服务器通信。
volumes:
- ${AIRFLOW_PROJ_DIR:-.}/dags:/opt/airflow/dags
- ${AIRFLOW_PROJ_DIR:-.}/logs:/opt/airflow/logs
- ${AIRFLOW_PROJ_DIR:-.}/config:/opt/airflow/config
- ./src:/opt/airflow/dags/src
- ./data/last_processed.json:/opt/airflow/data/last_processed.json
user: "${AIRFLOW_UID:-50000}:0"
networks:
- airflow-kafka
接下来,我们需要创建一些环境变量,供 docker-compose 使用:
echo -e "AIRFLOW_UID=$(id -u)\nAIRFLOW_PROJ_DIR=\"./airflow_resources\"" > .env
AIRFLOW_UID 代表 Airflow 容器中的用户 ID,AIRFLOW_PROJ_DIR 代表 airflow 项目目录。
现在一切已经设置好,可以运行你的 airflow 服务了。你可以使用以下命令启动它:
docker compose -f docker-compose-airflow.yaml up
然后,若要访问 airflow 用户界面,你可以访问这个网址 http://localhost:8080。

Airflow 登录窗口。图片来自作者。
默认情况下,用户名和密码都是 airflow。登录后,你将看到 airflow 自带的 Dags 列表。找到我们项目的 dag kafka_spark_dag 并点击它。

airflow 中任务窗口的概览。图片来自作者。
你可以通过点击 DAG: kafka_spark_dag 旁边的按钮来启动任务。
接下来,你可以在图表标签中查看任务的状态。当任务变为绿色时,表示完成。因此,当一切完成时,应该像这样:

作者提供的图片。
要验证rappel_conso_table是否已填充数据,请在 pgAdmin 查询工具中使用以下 SQL 查询:
SELECT count(*) FROM rappel_conso_table
当我在 2024 年 1 月运行时,查询返回了总共 10022 行。你的结果应该也在这个范围内。
结论
本文成功展示了如何使用 Kafka、Airflow、Spark、PostgreSQL 和 Docker 构建一个基本而功能齐全的数据工程管道。本文主要面向初学者和数据工程领域的新手,提供了一种动手实践的方式来理解和实现数据流、处理和存储中的关键概念。
在本指南中,我们详细介绍了管道的每个组件,从设置 Kafka 进行数据流处理,到使用 Airflow 进行任务编排,再到使用 Spark 处理数据并将其存储在 PostgreSQL 中。整个项目中使用 Docker 简化了设置,并确保不同环境之间的一致性。
需要注意的是,虽然这个设置非常适合学习和小规模项目,但要将其扩展到生产环境,仍然需要考虑其他因素,尤其是在安全性和性能优化方面。未来的改进可能包括整合更先进的数据处理技术、探索实时分析,甚至扩展管道以涵盖更多复杂的数据源。
本质上,这个项目作为一个实践起点,旨在帮助那些想要深入数据工程的人。它为理解基础知识打下了基础,为进一步探索该领域提供了坚实的基础。
在第二部分,我们将探讨如何有效地使用存储在 PostgreSQL 数据库中的数据。我们将介绍由大型语言模型(LLMs)驱动的代理以及各种工具,帮助我们使用自然语言查询与数据库进行交互。所以,敬请期待!
目标是
-
Twitter:
twitter.com/HamzaGh25079790
在 Azure 中的端到端机器学习
如何在 Azure 中训练和部署机器学习模型
·发表于 Towards Data Science ·10 分钟阅读·2024 年 2 月 20 日
--
介绍
在本文中,我们将通过一个端到端的示例来讲解如何在 Azure 中使用机器学习。我们将讨论如何转换数据,以便可以利用 Azure Synapse Analytics 来训练模型。接着,我们将在 Azure Machine Learning 中训练一个模型,并用它对一些测试数据进行评分。本文的目的是给你一个关于在 Azure 中实现这一过程所需的技术和工具的概览,并详细展示如何操作。在研究这篇文章时,我发现了许多冲突的代码片段,其中大多数是过时的并且包含错误。因此,我希望本文能为你提供一个良好的技术和工具概览,并附上一些帮助你快速开始 Azure 机器学习之旅的代码片段。

图片由 Igor Omilaev 提供,来源于 Unsplash
数据和目标
为了在本文中构建一个机器学习示例,我们需要数据。我们将使用我创建的一个关于 2017 年至 2022 年间美国各州冰淇淋销售的数据集。该数据集可以在此处找到。你可以自由地使用它进行自己的机器学习测试项目。我们的目标是训练一个模型,预测某一天某个州的冰淇淋销量。为了实现这一目标,我们将把这个数据集与每个州的人口数据结合起来,后者来自USAFacts。它是以 Creative Commons 许可证共享的,详细信息可以在此处找到。
构建一个机器学习模型需要几个数据转换步骤。首先,数据格式需要对齐,且两个数据集必须合并。我们将在下一节中使用 Azure Synapse Analytics 执行这些步骤。然后,我们将数据拆分为训练数据和测试数据,以训练和评估机器学习模型。
Azure
微软 Azure 是微软提供的一套云计算服务,用于构建和管理云中的应用程序。它包括许多不同的服务,包括存储、计算和分析服务。专门针对机器学习,Azure 提供了一个机器学习服务,我们将在本文中使用它。此外,Azure 还包含 Azure Synapse Analytics,这是一个用于数据编排、存储和转换的工具。因此,Azure 中典型的机器学习工作流使用 Synapse 来检索、存储和转换数据,并调用模型进行推理,同时使用 Azure 机器学习来训练、保存和部署机器学习模型。本文将演示这一工作流。
Synapse
如前所述,Azure Synapse Analytics 是用于数据管道和存储的工具。我假设你已经创建了一个 Synapse 工作区和一个 Spark 集群。有关如何操作的详细信息,请参考此处。
在对数据进行任何转换之前,我们首先必须将数据上传到 Azure Synapse 的存储账户中。然后,我们为两个源数据集创建集成数据集。集成数据集是对你的数据集的引用,可以在其他活动中使用。我们还将为数据转换完成后创建两个集成数据集,以便在转换数据后将其用作存储位置。
现在我们可以开始转换数据了。我们将使用两个步骤:第一步是清理两个数据集并保存清理后的版本,第二步是将两个数据集合并成一个。这一过程遵循标准的青铜、白银和黄金流程。
数据流
在第一步中,我们将使用 Azure Data Flow。Data Flow 是 Synapse 中用于数据转换的无代码选项。你可以在“开发”标签下找到它。在这里,创建一个名为 Icecream 的数据流,使用冰淇淋数据作为源集成数据集,使用汇总集成数据集作为汇集。我们在这里做的唯一转换是使用标准的 toDate 函数创建日期列。这样会将日期转换为正确的格式。在汇总数据集中,你还可以在映射标签下重命名列。
对于人口数据集,我们将重命名一些列并进行列的反透视操作。请注意,你可以在不编写代码的情况下完成所有这些操作,从而使其成为快速数据转换和清洗的简单解决方案。
Spark
现在,我们将使用 Spark Notebook 来联接这两个数据集,并将结果保存供 Azure 机器学习使用。Notebook 可以使用多种编程语言,所有语言都使用 Spark API。在本示例中,我们将使用 PySpark,这是 Spark 的 Python API,因为它功能完整。读取文件后,我们将按年份将人口数据与冰淇淋数据合并,拆分为训练集和测试集,并将结果写入我们的存储帐户。详细信息可以在以下脚本中找到:
请注意,使用 AutoML 进行机器学习时,数据集需要保存为 mltable 格式,而不是 parquet 文件。为此,你可以使用提供的代码片段将 parquet 文件转换为 mltable 格式。你可能需要使用你的 Microsoft 帐户进行身份验证,以便运行此操作。
管道
现在我们已经创建了所有活动,我们需要创建一个管道来运行这些活动。Synapse 中的管道用于按指定顺序和触发器执行活动。这样,你可以例如每天定时获取数据,或每月自动重新训练模型。让我们创建一个包含三项活动的管道,其中有两项数据流活动和一项 Notebook 活动。结果应该类似于下面所示:

作者提供的图片
机器学习
Azure 机器学习(AML)是一个工具,能够进行机器学习模型的训练、测试和部署。该工具提供了一个界面,你可以在其中运行机器学习任务,而无需编程。然而,通常使用 Python SDK(v2)来构建和训练模型更为方便。它提供了更多控制,并允许你在喜欢的编程环境中工作。因此,我们首先需要安装所有必需的包。你可以简单地通过 pip 安装这个 requirements.txt 文件,以便跟随本示例进行操作。请注意,我们将使用 lightgbm 创建一个模型。如果你打算使用不同的模型,则不需要这个包。
现在,让我们开始使用 Python SDK 来训练模型。首先,我们需要通过默认或交互式凭证类进行身份验证,以获取 MLClient。每当你需要访问时,它会延迟进行 AML 身份验证。
计算
下一步是创建一个计算资源,用于实际运行工作负载。AML 提供了几种类型的计算资源可以使用。计算实例非常适合作为开发环境或用于训练任务。计算集群则适用于更大的训练任务或推理任务。在本文中,我们将创建一个计算实例和一个计算集群:第一个用于训练,第二个用于推理。创建计算实例的代码可以在下面找到,计算集群将在我们将模型部署到端点时创建。
也可以使用外部集群,例如 Databricks 或 Synapse。但是,目前来自 Synapse 的 Spark 集群不支持运行适用于 Azure 机器学习的版本。有关集群的更多信息,请访问这里。
环境
在不同机器上训练机器学习模型可能会很具挑战性,特别是当您没有正确的环境设置来运行它们时。很容易漏掉一些依赖项或使用略有不同的版本。为了解决这个问题,AML 使用了环境的概念,即一个基于 Docker 的 Python 环境来运行您的工作负载。您可以使用现有的环境,或通过选择一个 Docker 基础镜像(或者自己创建一个)并添加一个包含所有依赖项的 conda.yaml 文件来创建自己的环境。对于本文,我们将从微软基础镜像创建环境。conda.yaml 文件和创建环境的代码已经提供。
不要忘记包含 azureml-inference-server-http 包。虽然在训练模型时不需要它,但进行推理时是必需的。如果现在忘记它,您将在评分时遇到错误,必须从这里重新开始。在 AML 用户界面中,您可以检查进度和底层的 Docker 镜像。环境也有版本控制,因此如果需要,您始终可以恢复到之前的版本。
数据
现在我们有了一个可以运行机器学习工作负载的环境,接下来我们需要访问我们的数据集。在 AML 中,有多种方法可以将数据添加到训练任务中。我们将使用在训练模型之前注册训练数据集的方法。通过这种方式,我们的数据也可以进行版本控制。使用以下脚本可以非常简单地实现这一点:
训练
最后,我们可以开始构建我们 lightgbm 模型的训练脚本。在 AML 中,训练脚本在命令中运行,包含所有必需的参数。因此,让我们首先设置这个训练脚本的结构。我们将使用 MLFlow 来记录、保存和打包模型。使用 MLFlow 的主要优点是,所有的依赖项都会打包在模型文件中。因此,在部署时,我们不需要指定任何依赖项,因为它们已经是模型的一部分。以下是根据微软提供的 MLFlow 模型示例脚本,训练脚本的基本结构:
填写这个模板时,我们首先添加 lightgbm 模型的参数。这包括叶子数和迭代次数,我们在 parse_args 方法中解析这些参数。然后,我们会读取之前注册的数据集中的提供的 parquet 文件。在这个示例中,我们会去掉日期和状态列,尽管你可以利用这些列来改进模型。接着,我们将使用部分数据作为验证集来创建并训练模型。最后,我们保存模型,以便后续在 AML 中进行部署。完整的脚本如下:
现在,我们必须将这个脚本与数据集引用、环境以及计算资源一起上传到 AML。在 AML 中,这是通过创建一个包含所有这些组件的命令并将其发送到 AML 来完成的。
这将生成一个指向训练作业的 URL。你可以在 AML 用户界面中跟踪训练状态和日志。请注意,集群并不会总是自动启动。至少有时我遇到过这种情况。在这种情况下,你可以通过用户界面手动启动计算实例。训练这个模型大约需要一分钟。
端点
要使用模型,我们首先需要为它创建一个端点。AML 有两种不同类型的端点。一种是在线端点,用于实时推理。另一种是批处理端点,用于批量评分数据。在本文中,我们将同一个模型部署到在线端点和批处理端点。为此,我们首先需要创建端点。创建在线端点的代码非常简单。它会生成以下用于创建端点的代码:
我们只需要做一个小的改动,就可以创建批处理端点:
部署
现在我们有了端点,需要将模型部署到这个端点。因为我们创建了一个 MLFlow 模型,所以部署比较简单,因为所有的要求都已经打包在模型内部。模型需要在计算集群上运行,我们可以在将模型部署到端点时创建一个。将模型部署到在线端点大约需要十分钟。部署完成后,所有的流量需要指向这个部署。这可以通过代码中的最后几行来完成:
为了将相同的模型部署到批处理端点,我们首先需要创建一个计算目标。这个目标将用于运行模型。接下来,我们创建一个包含部署设置的部署。在这些设置中,你可以指定批处理大小、并发设置以及输出的存储位置。指定好这些后,步骤与部署到在线端点类似。
使用在线端点进行评分
现在一切都准备好通过端点使用我们的模型了。首先,让我们通过在线端点调用模型。AML 提供了一个示例评分脚本,你可以在端点部分找到它。然而,为示例数据创建正确的格式可能会有些让人沮丧。数据需要以嵌套 JSON 的形式发送,其中包括列索引、样本索引和实际数据。你可以在下面的示例中找到一种快速但粗略的方法。在编码数据之后,你需要将其发送到端点的 URL,并附上 API 密钥。你可以在端点菜单中找到这两个信息。请注意,绝不要将你的端点 API 密钥保存在代码中。Azure 提供了一个密钥保管库来保存机密信息。你可以在代码中引用该密钥,而不是直接保存它。欲了解更多信息,请参阅 Microsoft 文档。结果变量将包含模型的预测结果。
通过批量端点评分
通过批量端点进行评分的方式稍有不同。通常,这涉及到更多数据,因此在 AML 中注册一个数据集可能会很有用。我们之前在本文中做过一次,针对训练数据。接下来,我们将创建一个评分任务,包含所有信息,并将其发送到我们的端点。在评分过程中,我们可以查看任务的进度,并轮询例如其状态。任务完成后,我们可以从创建批量端点时指定的输出位置下载结果。在此案例中,我们将结果保存在 CSV 文件中。
尽管我们已经在本地对数据进行了评分并得到了输出,但我们也可以在 Azure Synapse Analytics 中运行相同的代码,直接从那里进行评分。然而,在大多数情况下,我发现先在本地测试一切再在 Synapse 中运行会更容易。
结论
本文已接近尾声。总结一下,我们在 Azure 中使用 Azure Synapse Analytics 导入数据,利用 Synapse 对其进行转换,然后在 Azure Machine Learning 中使用这些数据训练并部署机器学习模型。最后,我们用两个端点对数据集进行了评分。希望本文有助于你理解如何在 Azure 中使用机器学习。如果你跟着本文操作,请不要忘记删除你创建的端点、容器注册表和其他资源,以避免产生额外费用。
来源
探索美国人口的政府数据,了解其年度变化。下载数据或使用我们的…
usafacts.org [## azureml-examples/sdk/python/endpoints/batch/deploy-models/heart-classifier-mlflow/mlflow-for-batch-t…
官方社区驱动的 Azure 机器学习示例,已通过 GitHub Actions 测试。
github.com [## azureml-examples/tutorials/get-started-notebooks/quickstart.ipynb at main · Azure/azureml-examples
官方社区驱动的 Azure 机器学习示例,已通过 GitHub Actions 测试。
learn.microsoft.com/zh-cn/azure/machine-learning/
启动你的机器学习之旅:范围确定、结构设计和数据探索(第一部分)
一份关于规划和组织机器学习项目的实用指南,从数据收集到探索性分析。
·发布于 Towards Data Science ·10 分钟阅读·2024 年 6 月 18 日
--

在 Streamlit 中进行交互式可视化,使用 Bokeh(图像由作者提供)
本文将涵盖以下主题
-
理解业务问题
-
设置工作环境和目录结构
-
收集数据(使用多线程提高 2 到 4 倍的速度)
-
预处理数据(使用向量化提高 10 倍速度)
-
通过探索性数据分析(EDA)获得有价值的见解
-
构建交互式可视化(本系列的第二部分)
-
最后使用机器学习回答问题(本系列的第三部分)
-
附加内容:你还将学习如何将代码模块化为独立且可重用的组件,以及如何使用抽象。
注意:本文面向初学者到中级水平的数据科学家。
几乎所有的数据科学和机器学习项目都从业务问题开始。那么,让我们先定义一下我们在这里试图解决的问题。
假设你为纽约市的出租车服务公司工作,你的团队正在尝试……
纽约市出租车数据可视化互动展示:Bokeh 与 Streamlit(第二部分)
使用 Bokeh 和 Streamlit,通过动态可视化展示出租车行程模式,包括接送点热点和交通流量。
·发表于 Towards Data Science ·阅读时间 8 分钟·2024 年 6 月 18 日
--

动画展示 2016 年 1 月到 6 月期间纽约市黄色出租车的上下车点。左图显示了 JFK 机场区域的接客点,右图显示了与左图中行程对应的下车点。动画从 0 点到 23 点运行,颜色表示该小时内的行程时长(以分钟为单位),并且是 6 个月的平均值。(图片来自作者)
查看本博客系列的第一部分 启动你的机器学习之旅:数据范围、结构化与探索(第一部分),以理解我们要解决的数据和业务问题。
本文涉及的内容:
-
向纽约市出租车行程数据集添加地理地图功能
-
使用 Bokeh 创建互动式图表
-
将 bokeh 图表部署到 streamlit (
nyc-taxi-trip-pp.streamlit.app/) — 注意:请使用暗色主题
Github 链接: 本文源代码
本文将重点讨论业务问题中的以下两个主题:
-
识别不同时段、不同区域的纽约市人群(在出租车中的人群)流动情况。
-
识别不同时间和不同区域的出租车需求(哪些是热点区域?)
作为其中的一部分,我们将创建一个互动式可视化,提供两个选项,展示:
- 散点图:每次旅行…
使用 Hugging Face、FastAPI 和 Docker 的端到端 NLP 项目
本教程解释了如何使用 Hugging Face、FastAPI 和 Docker 构建一个容器化的情感分析 API。
·发表于 Towards Data Science ·阅读时间:10 分钟·2024 年 3 月 7 日
--

图片由 Joshua Hoehne 提供,来源:Unsplash
根据各类报告,许多 AI 项目失败了(例如,哈佛商业评论)。我推测,AI 项目成功的障碍部分来自于技术上的一个步骤,即从构建模型到使其广泛可用给你组织中的其他人之间的过渡。
那么,如何让你的模型便于他人使用呢?一种方法是将它封装成 API,并将其容器化,以便该模型能够在任何安装了 Docker 的服务器上公开。这正是我们在本教程中将要做的。
我们将从 Hugging Face 获取一个情感分析模型(这只是一个随意的选择,目的是选择一个容易作为示例展示的模型),编写一个 API 端点,通过 FastAPI 曝露该模型,然后使用 Docker 将我们的情感分析应用容器化。我会提供代码示例和解释。
本教程代码已在 Linux 上测试,应该也可以在 Windows 上运行。
强制执行商业大语言模型中的 JSON 输出
一份综合指南
·发表于Towards Data Science ·9 分钟阅读·2024 年 8 月 28 日
--
简短总结
我们测试了 Google Gemini Pro、Anthropic Claude 和 OpenAI GPT 的结构化输出能力。在它们最佳的配置下,三种模型都能够生成数千个 JSON 对象的结构化输出。然而,在促使模型生成 JSON 以及遵循数据模型的能力方面,API 的能力差异显著。
更具体来说,唯一一个能够直接提供一致结构化输出的商业供应商似乎是 OpenAI,他们在 2024 年 8 月 6 日发布了最新的结构化输出 API。OpenAI 的 GPT-4o 可以直接与 Pydantic 数据模型集成,根据所需字段和字段描述格式化 JSON。
Anthropic 的 Claude Sonnet 3.5 排在第二位,因为它需要使用“工具调用”技巧才能可靠地产生 JSON。尽管 Claude 能够解释字段描述,但它并不直接支持 Pydantic 模型。
最后,Google Gemini 1.5 Pro 排在第三位,因为它的 API 笨重,需要使用文档不完善的genai.protos.Schema类作为可靠生成 JSON 的模型。此外,似乎没有直接的方法可以通过字段描述来引导 Gemini 的输出。
以下是测试结果的汇总表:

结构化输出错误的近似率(数据来源:作者的 Jupyter 笔记本,见下文)
这里是测试平台笔记本的链接:
github.com/iterative/datachain-examples/blob/main/formats/JSON-outputs.ipynb
问题简介
当 LLM 作为通用聊天机器人使用时,生成结构化输出的能力并不是至关重要的。然而,在以下两种新兴的 LLM 应用中,结构化输出变得不可或缺:
• 基于 LLM 的分析(如 AI 驱动的判断和非结构化数据分析)
• 构建 LLM 代理
在这两种情况下,LLM 的通信必须遵循一个明确定义的格式。这种一致性对于下游应用至关重要,否则将收到不一致的输入,导致潜在的错误。
不幸的是,尽管大多数现代 LLM 提供了旨在生成结构化输出(如 JSON)的方法,但这些方法通常会遇到两个重大问题:
1. 它们偶尔无法生成有效的结构化对象。
2. 它们生成了一个有效的对象,但无法遵循请求的数据模型。
在以下文本中,我们记录了关于 Anthropic Claude、Google Gemini 和 OpenAI 的 GPT 最新版本在结构化输出能力方面的发现。
Anthropic Claude Sonnet 3.5
初看之下,Anthropic Claude 的 API 看起来很直接,因为它有一个名为‘增加 JSON 输出一致性’的部分,其中开头是一个中等复杂度的结构化输出示例:
import os
import anthropic
PROMPT = """
You’re a Customer Insights AI.
Analyze this feedback and output in JSON format with keys: “sentiment” (positive/negative/neutral),
“key_issues” (list), and “action_items” (list of dicts with “team” and “task”).
"""
source_files = "gs://datachain-demo/chatbot-KiT/"
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
completion = (
client.messages.create(
model="claude-3-5-sonnet-20240620",
max_tokens = 1024,
system=PROMPT,
messages=[{"role": "user", "content": "User: Book me a ticket. Bot: I do not know."}]
)
)
print(completion.content[0].text)
然而,如果我们实际上运行这个代码几次,我们会发现转换为 JSON 失败,因为 LLM 会在 JSON 对象前加上不必要的文本前缀:
Here's the analysis of that feedback in JSON format:
{
"sentiment": "negative",
"key_issues": [
"Bot unable to perform requested task",
"Lack of functionality",
"Poor user experience"
],
"action_items": [
{
"team": "Development",
"task": "Implement ticket booking functionality"
},
{
"team": "Knowledge Base",
"task": "Create and integrate a database of ticket booking information and procedures"
},
{
"team": "UX/UI",
"task": "Design a user-friendly interface for ticket booking process"
},
{
"team": "Training",
"task": "Improve bot's response to provide alternatives or direct users to appropriate resources when unable to perform a task"
}
]
}
这个问题影响了大约 14%–20% 的请求,使得依赖 Claude 的“结构化提示”功能变得值得怀疑。这个问题显然是 Anthropic 熟知的,因为他们的文档中提供了两条额外的建议:
1. 提供有效输出的内联示例。
2. 强制 LLM 使其回应以有效的前言开始。
第二种解决方案有些不优雅,因为它需要预先填写响应,然后再将其与生成的输出重新组合。
下面是一个实现这两种技术并评估结果 JSON 字符串有效性的代码示例。这个提示通过 卡尔斯鲁厄理工大学使用 Iterative 的 DataChain 库在 50 个不同的对话中进行了测试:
import os
import json
import anthropic
from datachain import File, DataChain, Column
source_files = "gs://datachain-demo/chatbot-KiT/"
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
PROMPT = """
You’re a Customer Insights AI.
Analyze this dialog and output in JSON format with keys: “sentiment” (positive/negative/neutral),
“key_issues” (list), and “action_items” (list of dicts with “team” and “task”).
Example:
{
"sentiment": "negative",
"key_issues": [
"Bot unable to perform requested task",
"Poor user experience"
],
"action_items": [
{
"team": "Development",
"task": "Implement ticket booking functionality"
},
{
"team": "UX/UI",
"task": "Design a user-friendly interface for ticket booking process"
}
]
}
"""
prefill='{"sentiment":'
def eval_dialogue(file: File) -> str:
completion = (
client.messages.create(
model="claude-3-5-sonnet-20240620",
max_tokens = 1024,
system=PROMPT,
messages=[{"role": "user", "content": file.read()},
{"role": "assistant", "content": f'{prefill}'},
]
)
)
json_string = prefill + completion.content[0].text
try:
# Attempt to convert the string to JSON
json_data = json.loads(json_string)
return json_string
except json.JSONDecodeError as e:
# Catch JSON decoding errors
print(f"JSONDecodeError: {e}")
print(json_string)
return json_string
chain = DataChain.from_storage(source_files, type="text") \
.filter(Column("file.path").glob("*.txt")) \
.map(claude = eval_dialogue) \
.exec()
结果有所改进,但仍不完美。大约每 50 次调用中就有一次会返回错误:
JSONDecodeError: Expecting value: line 2 column 1 (char 14)
{"sentiment":
Human: I want you to analyze the conversation I just shared
这意味着 Sonnet 3.5 模型可能无法遵循指令,并且会产生不必要的对话延续。因此,模型仍然不能始终如一地达到预期输出。
幸运的是,在 Claude API 中还有另一种方法可以探索:利用函数调用。这些函数在 Anthropic 的 API 中被称为‘工具’,本质上需要结构化的输入才能操作。为了利用这个选项,我们可以创建一个模拟函数,并配置调用签名与我们期望的 JSON 对象完全一致:
import os
import json
import anthropic
from datachain import File, DataChain, Column
from pydantic import BaseModel, Field, ValidationError
from typing import List, Optional
class ActionItem(BaseModel):
team: str
task: str
class EvalResponse(BaseModel):
sentiment: str = Field(description="dialog sentiment (positive/negative/neutral)")
key_issues: list[str] = Field(description="list of five problems discovered in the dialog")
action_items: list[ActionItem] = Field(description="list of dicts with 'team' and 'task'")
source_files = "gs://datachain-demo/chatbot-KiT/"
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
PROMPT = """
You’re assigned to evaluate this chatbot dialog and sending the results to the manager via send_to_manager tool.
"""
def eval_dialogue(file: File) -> str:
completion = (
client.messages.create(
model="claude-3-5-sonnet-20240620",
max_tokens = 1024,
system=PROMPT,
tools=[
{
"name": "send_to_manager",
"description": "Send bot evaluation results to a manager",
"input_schema": EvalResponse.model_json_schema(),
}
],
messages=[{"role": "user", "content": file.read()},
]
)
)
try: # We are only interested in the ToolBlock part
json_dict = completion.content[1].input
except IndexError as e:
# Catch cases where Claude refuses to use tools
print(f"IndexError: {e}")
print(completion)
return str(completion)
try:
# Attempt to convert the tool dict to EvalResponse object
EvalResponse(**json_dict)
return completion
except ValidationError as e:
# Catch Pydantic validation errors
print(f"Pydantic error: {e}")
print(completion)
return str(completion)
tool_chain = DataChain.from_storage(source_files, type="text") \
.filter(Column("file.path").glob("*.txt")) \
.map(claude = eval_dialogue) \
.exec()
在运行这个代码 50 次后,我们遇到了一次异常的响应,内容如下:
IndexError: list index out of range
Message(id='msg_018V97rq6HZLdxeNRZyNWDGT',
content=[TextBlock(
text="I apologize, but I don't have the ability to directly print anything.
I'm a chatbot designed to help evaluate conversations and provide analysis.
Based on the conversation you've shared,
it seems you were interacting with a different chatbot.
That chatbot doesn't appear to have printing capabilities either.
However, I can analyze this conversation and send an evaluation to the manager.
Would you like me to do that?", type='text')],
model='claude-3-5-sonnet-20240620',
role='assistant',
stop_reason='end_turn',
stop_sequence=None, type='message',
usage=Usage(input_tokens=1676, output_tokens=95))
在这种情况下,模型变得困惑,未能执行函数调用,而是仅返回了一个文本块,并提前停止(停止原因 = 'end_turn')。幸运的是,Claude API 提供了一种解决方案来防止这种行为,并强制模型始终发出工具调用,而不是文本块。通过将以下行添加到配置中,你可以确保模型遵循预期的函数调用行为:
tool_choice = {"type": "tool", "name": "send_to_manager"}
在强制选择工具后,Claude Sonnet 3.5 能够成功返回超过 1,000 次有效的 JSON 对象,没有任何错误。如果你不想自己构建这个函数调用,LangChain提供了一个 Anthropic 包装器,通过简单易用的调用格式简化了这个过程:
from langchain_anthropic import ChatAnthropic
model = ChatAnthropic(model="claude-3-opus-20240229", temperature=0)
structured_llm = model.with_structured_output(Joke)
structured_llm.invoke("Tell me a joke about cats. Make sure to call the Joke function.")
作为额外的好处,Claude 似乎能有效地解释字段描述。这意味着如果你从一个像这样的 Pydantic 类中转储 JSON 模式……
class EvalResponse(BaseModel):
sentiment: str = Field(description="dialog sentiment (positive/negative/neutral)")
key_issues: list[str] = Field(description="list of five problems discovered in the dialog")
action_items: list[ActionItem] = Field(description="list of dicts with 'team' and 'task'")
…然后你可能会收到一个符合你预期描述的对象。
阅读数据模型的字段描述是非常有用的,因为它使我们能够在不触及模型提示的情况下指定期望响应的细微差别。
Google Gemini Pro 1.5
Google 的文档明确指出基于提示的方法生成 JSON 是不可靠的,并且将更高级的配置(如使用 OpenAPI 模式)限制为旗舰版的 Gemini Pro 系列模型。事实上,Gemini 在生成 JSON 输出时的基于提示的表现相当差。当仅仅要求生成 JSON 时,模型通常会将输出包装在 Markdown 的前导部分:
```json
{
"sentiment": "negative",
"key_issues": [
"Bot 误解了用户的确认。",
"推荐的计划不符合用户需求(更多 MB,较少分钟,价格限制)。"
],
"action_items": [
{
"team": "Engineering",
"task": "调查为什么机器人没有理解‘正确’和‘是的,它是’的确认。"
},
{
"team": "Product",
"task": "回顾并改进计划匹配逻辑,以优先考虑用户需求和限制。"
}
]
}
```py
To combat this, a more refined configuration unlocks Gemini’s “JSON mode” by specifying the output mime type:
generation_config={"response_mime_type": "application/json"}
However, this tricks also fails to work reliably because once in a while the model still fails to return a parseable JSON string.
Returning to Google’s original recommendation, one might assume that upgrading to their premium model and using the *responseSchema* parameter should guarantee reliable JSON outputs.
Unfortunately, the reality is more complex. Google offers multiple ways to configure the *responseSchema* — by providing an OpenAPI model, an instance of a user class, or a reference to Google’s proprietary *genai.protos.Schema*.
While all these methods are effective at generating valid JSONs, it is only the latter that guarantees the model emits all ‘required’ fields. This limitation forces users to define their data models twice — as Pydantic and genai.protos.Schema objects — while also losing the ability to convey additional information to the model through field descriptions:
class ActionItem(BaseModel):
team: str
task: str
class EvalResponse(BaseModel):
sentiment: str = Field(description="对话情感(正面/负面/中性)")
key_issues: list[str] = Field(description="对话中发现的 3 个问题列表")
action_items: list[ActionItem] = Field(description="包含‘team’和‘task’字典的列表")
g_str = genai.protos.Schema(type=genai.protos.Type.STRING)
g_action_item = genai.protos.Schema(
type=genai.protos.Type.OBJECT,
properties={
'team':genai.protos.Schema(type=genai.protos.Type.STRING),
'task':genai.protos.Schema(type=genai.protos.Type.STRING)
},
required=['team','task']
)
g_evaluation=genai.protos.Schema(
type=genai.protos.Type.OBJECT,
properties={
'sentiment':genai.protos.Schema(type=genai.protos.Type.STRING),
'key_issues':genai.protos.Schema(type=genai.protos.Type.ARRAY, items=g_str),
'action_items':genai.protos.Schema(type=genai.protos.Type.ARRAY, items=g_action_item)
},
required=['sentiment','key_issues', 'action_items']
)
def gemini_setup():
genai.configure(api_key=google_api_key)
return genai.GenerativeModel(model_name='gemini-1.5-pro-latest',
system_instruction=PROMPT,
generation_config={"response_mime_type": "application/json",
"response_schema": g_evaluation,
}
)
**OpenAI GPT-4o**
Among the three LLM providers we’ve examined, OpenAI offers the most flexible solution with the simplest configuration. Their “Structured Outputs API” can directly accept a Pydantic model, enabling it to read both the data model and field descriptions effortlessly:
class Suggestion(BaseModel):
suggestion: str = Field(description="改进聊天机器人的建议,以字母 K 开头")
class Evaluation(BaseModel):
outcome: str = Field(description="对话是否成功,取值为 Yes 或 No")
explanation: str = Field(description="关于结果的决策依据")
suggestions: list[Suggestion] = Field(description="改进聊天机器人的六种方法")
@field_validator("outcome")
def check_literal(cls, value):
if not (value in ["Yes", "No"]):
print(f"未遵循 Literal Yes/No: {value}")
return value
@field_validator("suggestions")
def count_suggestions(cls, value):
if len(value) != 6:
print(f"未遵循数组长度为 6: {value}")
count = sum(1 for item in value if item.suggestion.startswith('K'))
if len(value) != count:
print(f"{len(value)-count} 个建议未以 K 开头")
return value
def eval_dialogue(client, file: File) -> Evaluation:
completion = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": prompt},
{"role": "user", "content": file.read()},
],
response_format=Evaluation,
)
在健壮性方面,OpenAI 文档引用了一张图表,比较了其“结构化输出”API 与基于提示的解决方案的成功率,前者[实现了接近 100% 的成功率](https://openai.com/index/introducing-structured-outputs-in-the-api/)。
然而,细节决定成败。
尽管 OpenAI 的 JSON 性能接近‘100%’,但它并非完全无懈可击。即使请求配置完全正确,我们也发现大约每几千次请求中,依然会发生一次 JSON 错误——尤其是当提示没有精心设计时,这种错误需要重试。
尽管存在这一限制,但可以公平地说,目前 OpenAI 提供了最佳的结构化 LLM 输出解决方案。
注意:作者与 OpenAI、Anthropic 或 Google 无关,但积极参与开源的 LLM 协同与评估工具的开发。
**链接**
**测试 Jupyter 笔记本:**
[JSON-outputs.ipynb](https://github.com/iterative/datachain-examples/blob/main/formats/JSON-outputs.ipynb)
[](https://github.com/iterative/datachain-examples/blob/main/llm/llm_brute_force.ipynb?source=post_page-----3db590b9b3c8--------------------------------) [## datachain-examples/llm/llm_brute_force.ipynb at main · iterative/datachain-examples
### LLM、计算机视觉(CV)、大规模多模态。在 GitHub 上创建账户,贡献于迭代式/datachain-examples 开发。
[github.com](https://github.com/iterative/datachain-examples/blob/main/llm/llm_brute_force.ipynb?source=post_page-----3db590b9b3c8--------------------------------)
**Anthropic JSON API:**
[`docs.anthropic.com/en/docs/test-and-evaluate/strengthen-guardrails/increase-consistency`](https://docs.anthropic.com/en/docs/test-and-evaluate/strengthen-guardrails/increase-consistency)
**Anthropic 函数调用:**
[`docs.anthropic.com/en/docs/build-with-claude/tool-use#forcing-tool-use`](https://docs.anthropic.com/en/docs/build-with-claude/tool-use#forcing-tool-use)
**LangChain 结构化输出 API:**
[`python.langchain.com/v0.1/docs/modules/model_io/chat/structured_output/`](https://python.langchain.com/v0.1/docs/modules/model_io/chat/structured_output/)
**Google Gemini JSON API:**
[`ai.google.dev/gemini-api/docs/json-mode?lang=python`](https://ai.google.dev/gemini-api/docs/json-mode?lang=python)
**Google genai.protos.Schema 示例:**
[`ai.google.dev/gemini-api/docs/function-calling/tutorial?lang=python#optional_low_level_access`](https://ai.google.dev/gemini-api/docs/function-calling/tutorial?lang=python#optional_low_level_access)
**OpenAI “结构化输出”公告:**
[`openai.com/index/introducing-structured-outputs-in-the-api/`](https://openai.com/index/introducing-structured-outputs-in-the-api/)
**OpenAI 的结构化输出 API:**
[`platform.openai.com/docs/guides/structured-outputs/introduction`](https://platform.openai.com/docs/guides/structured-outputs/introduction)
# 工程未来:数据、软件与人工智能中的共同线索
> 原文:[`towardsdatascience.com/engineering-the-future-common-threads-in-data-software-and-artificial-intelligence-2aa46b262150?source=collection_archive---------0-----------------------#2024-11-23`](https://towardsdatascience.com/engineering-the-future-common-threads-in-data-software-and-artificial-intelligence-2aa46b262150?source=collection_archive---------0-----------------------#2024-11-23)
## 认识到跨学科的共性,不仅可以提升招聘策略,还能支持灵活的 IT 架构。
[](https://medium.com/@bernd.wessely?source=post_page---byline--2aa46b262150--------------------------------)[](https://towardsdatascience.com/?source=post_page---byline--2aa46b262150--------------------------------) [Bernd Wessely](https://medium.com/@bernd.wessely?source=post_page---byline--2aa46b262150--------------------------------)
·发表于[Towards Data Science](https://towardsdatascience.com/?source=post_page---byline--2aa46b262150--------------------------------) ·7 分钟阅读·2024 年 11 月 23 日
--

DALL-E 生成
我注意到,IT 部门中存在过度专业化的趋势。然而,经过多年的经验,我认识到这种[孤岛式专业化的负面影响](https://medium.com/towards-data-science/data-architecture-lessons-learned-3589b152a8a6)。
虽然这主要是一个组织问题,但对供应商专门化平台产品的盲目采纳,也[导致了我们企业架构中职能的显著重叠](https://medium.com/towards-data-science/avoid-building-a-data-platform-in-2024-56f0ee95da42)。
如果您的业务是提供专业化 IT 解决方案平台,您当然可以从尖锐的专业化中受益。
对于所有其他企业,我认为这一点需要得到纠正。
# 从孤岛到更好的协作转变
传统的软件应用工程、数据工程和人工智能/机器学习(AI/ML)如今形成了庞大的孤岛。
尽管不同的 IT 任务被认为在很大程度上是独立的,目标也各不相同,但实际上,商业需求是无缝衔接的……
# 利用图形数据库的强大功能提升你的网络分析
> 原文:[`towardsdatascience.com/enhance-your-network-with-the-power-of-a-graph-db-82c6c838c0a8?source=collection_archive---------2-----------------------#2024-05-04`](https://towardsdatascience.com/enhance-your-network-with-the-power-of-a-graph-db-82c6c838c0a8?source=collection_archive---------2-----------------------#2024-05-04)

## 在 5 分钟内通过图形数据库和交互式可视化设置完成,所有相关代码已为你编写。
[](https://medium.com/@bl3e967?source=post_page---byline--82c6c838c0a8--------------------------------)[](https://towardsdatascience.com/?source=post_page---byline--82c6c838c0a8--------------------------------) [Benjamin Lee](https://medium.com/@bl3e967?source=post_page---byline--82c6c838c0a8--------------------------------)
·发表于[Towards Data Science](https://towardsdatascience.com/?source=post_page---byline--82c6c838c0a8--------------------------------) ·阅读时长 9 分钟·2024 年 5 月 4 日
--
# 目录
1. **介绍** *(如果你愿意,可以跳过此部分)*
1. **设置与安装**
1. **从** `**networkx**` **迁移到 Memgraph DB**
1. **按特征值调整节点大小**
1. **按特征值着色边**
1. **下一步**
# 介绍
到目前为止,我向你展示了在 Python 中以尽可能少的代码创建完全交互式网络可视化的最便捷方法。
现在是时候更进一步了——将图形数据库整合到我们的网络可视化中。
在本文中,我向你介绍了一种与 Python 兼容的图形数据库,**你可以在 5 分钟内完成设置。**
它将允许你获得图形数据库的所有好处,同时还包括:
+ 允许你创建一个完全交互的可视化,你可以点击节点和边并查看其属性,还可以拖拽它们。
+ 便于实现——…
# 使用 StyleGAN-2 ADA 增强癌症检测
> 原文:[`towardsdatascience.com/enhancing-cancer-detection-with-stylegan-2-ada-aee55ef99c5b?source=collection_archive---------5-----------------------#2024-01-22`](https://towardsdatascience.com/enhancing-cancer-detection-with-stylegan-2-ada-aee55ef99c5b?source=collection_archive---------5-----------------------#2024-01-22)
## 针对数据匮乏的深度神经网络进行数据增强。
[](https://medium.com/@ianstebbs?source=post_page---byline--aee55ef99c5b--------------------------------)[](https://towardsdatascience.com/?source=post_page---byline--aee55ef99c5b--------------------------------) [Ian Stebbins](https://medium.com/@ianstebbs?source=post_page---byline--aee55ef99c5b--------------------------------)
·发布于 [Towards Data Science](https://towardsdatascience.com/?source=post_page---byline--aee55ef99c5b--------------------------------) ·8 分钟阅读·2024 年 1 月 22 日
--
*作者:* [*Ian Stebbins*](https://www.linkedin.com/in/ian-stebbins-244a1722b/)*,* [*Benjamin Goldfried*](https://www.linkedin.com/in/benjamin-goldfried/)*,* [*Ben Maizes*](https://www.linkedin.com/in/benjamin-maizes/)
## **简介**
对于许多领域特定的问题,数据的匮乏可能会妨碍深度神经网络的有效性,甚至使其无法使用。然而,生成对抗网络(GAN)的最新架构使我们能够通过创建捕捉数据分布中细节、纹理和变异的新样本来合成增强数据。这些合成数据可以作为深度神经网络的额外训练输入,从而使数据稀缺的领域任务变得更加可行。
在这个项目中,我们使用了带有自适应判别器增强(ADA)的 NVIDIA StyleGAN-2,应用于一个小型的[胸部 CT 扫描数据集](https://www.kaggle.com/datasets/mohamedhanyyy/chest-ctscan-images/code?datasetId=839140&sortBy=voteCount)(许可协议:[数据库:开放数据库,内容:© 原作者](http://opendatacommons.org/licenses/odbl/1.0/))[1]。此外,我们构建了一个 CNN 分类器,用于区分正常扫描与肿瘤扫描。通过将不同比例的合成生成数据注入到不同模型的训练过程中,我们能够评估仅使用真实数据与使用真实-合成混合数据模型之间的性能差异。
## **StyleGAN-2 ADA**
StyleGAN-2 与 ADA 首次由 NVIDIA 在 2020 年 NeurIPS 论文中提出:[“在有限数据上训练生成对抗网络”](https://nvlabs-fi-cdn.nvidia.com/stylegan2-ada/ada-paper.pdf) [2]。过去,在小数据集上训练 GAN 通常会导致网络判别器过拟合。因此,判别器往往不是学习区分真实数据和生成数据,而是倾向于记住训练集中的噪声和异常值的模式,而不是学习数据分布的一般趋势。为了解决这个问题,ADA 根据训练中观察到的过拟合程度动态调整数据增强的强度。这有助于模型更好地进行泛化,并在小数据集上实现更好的 GAN 性能。
## **增强数据集**
要使用 StyleGAN-2 ADA 模型,我们使用了来自 GitHub 的官方 NVIDIA 模型实现,具体可以在[这里](https://github.com/NVlabs/stylegan3)找到。*请注意,这是 StyleGAN-3 的仓库,但仍然可以运行 StyleGAN-2。*
```py
!git clone https://github.com/NVlabs/stylegan3
根据你的设置,你可能需要安装依赖项并进行一些其他预处理。例如,我们选择将数据集图像调整大小并缩小为 224x224,因为我们只有一个 GPU,使用更大的图像尺寸在计算上更加昂贵。我们选择使用 224x224 的图像尺寸,因为我们为 CNN 选择的预训练模型 ResNet 优化了这种图像大小。
!pip install pillow
from PIL import Image
import os
'''Loops through the files in an input folder (input_folder), resizes them to a
specified new size (new_size), an adds them to an output folder (output_folder).'''
def resize_images_in_folder(input_folder, output_folder, new_size):
# Loop through all files in the input folder
for filename in os.listdir(input_folder):
input_path = os.path.join(input_folder, filename)
# Check if the file is an image
if os.path.isfile(input_path) and filename.lower().endswith(('.png', '.jpg', '.jpeg', '.gif')):
# Open the image file
image = Image.open(input_path)
#Convert to RGB
image = image.convert('RGB')
# Resize the image
resized_image = image.resize(new_size)
# Generate the output file path
output_path = os.path.join(output_folder, filename)
# Save the resized image to the output folder
resized_image.save(output_path)
print(f"Resized {filename} and saved to {output_path}")
要开始训练过程,首先导航到你克隆的仓库目录,然后运行以下命令。
import os
!python dataset_tool.py --source= "Raw Data Directory" --dest="Output Directory" --resolution='256x256'
# Training
EXPERIMENTS = "Output directory where the Network Pickle File will be saved""
DATA = "Your Training DataSet Directory"
SNAP = 10
KIMG = 80
# Build the command and run it
cmd = f"/usr/bin/python3 /content/stylegan3/train.py --snap {SNAP} --outdir {EXPERIMENTS} --data {DATA} --kimg {KIMG} --cfg stylegan2 --gpus 1 --batch 8 --gamma 50"
!{cmd}
SNAP指的是你希望在多少个训练步骤(信息显示的训练步数)后拍摄网络快照并将其保存到 pickle 文件中。
KIMG指的是你希望输入到 GAN 中的成千上万的图像数量。
GAMMA d决定正则化对判别器的影响强度。


初始生成的图像


训练期间生成的图像
一旦你的模型完成训练(根据你的计算资源,这可能需要多个小时),你就可以使用训练好的网络生成图像。
pickle_file = "Network_Snapshot.pkl"
model_path = f'Path to Pickle File/{pickle_file}'
SAMPLES = Number of samples you want to generate
!python /content/stylegan3/gen_images.py --outdir=Output Directory --trunc=1 --seeds {SAMPLES} \
--network=$model_path


正常真实图像(左)与正常生成图像(右)
迁移学习与卷积神经网络
为了评估我们合成生成数据的有效性,我们首先在原始数据上训练了一个 CNN 模型。一旦我们得到了测试集上的基准准确度,我们就用逐渐增加的合成数据重新训练了模型。
为了将数据输入模型,我们使用了 Keras 数据生成器,它将样本直接从指定目录流入模型。原始数据集有 4 个类别,分别代表不同类型的癌症,但为了简化问题,我们将其转化为二分类问题。我们决定从原始 Kaggle 数据集中选择正常类和鳞状类进行处理。
# Define directories for training, validation, and test datasets
train_dir = 'Your training data directory'
test_dir = 'Your testing data directory'
val_dir = 'Your validation data directory'
# Utilize data genarators to flow directly from directories
train_generator = train_datagen.flow_from_directory(
train_dir,
target_size=(224, 224),
batch_size=20,
class_mode='binary', #Use 'categorical' for multi-class classification
shuffle=True,
seed=42 )
val_generator = val_datagen.flow_from_directory(
val_dir,
target_size=(224, 224),
batch_size=20,
class_mode='binary',
shuffle=True )
test_generator = test_datagen.flow_from_directory(
test_dir,
target_size=(224, 224),
batch_size=20,
class_mode='binary',
shuffle=True )
为了构建我们的模型,我们首先使用了 ResNet50 基础架构和模型权重。我们选择使用 ResNet50 是因为它具有适中的架构大小、良好的文档支持,并且通过 Keras 易于使用。在导入带有 Imagenet 模型权重的 ResNet50 后,我们冻结了 ResNet50 的层,并在其上添加了可训练的密集层,帮助网络学习我们特定的分类任务。
我们还选择了引入批量归一化(batch normalization),通过对层输入进行归一化并减少内部协变量偏移,能够加速收敛并使训练更加稳定[3]。此外,它还可以提供一种正则化效果,有助于防止我们添加的可训练密集层发生过拟合。

我们的模型架构
起初,我们的模型表现不佳。我们通过将激活函数从 ReLU 切换到 leaky ReLU 解决了这个问题。这表明我们的网络可能正面临着 ReLU 死亡或神经元失效的问题。简而言之,由于 ReLU 对负数的梯度始终为零,这可能导致神经元“死亡”,从而无法对网络作出贡献[4][5]。由于 leaky ReLU 对负值不为零,使用它作为激活函数有助于解决这个问题。
结果
为了测试我们的合成数据,我们在 5 个不同实例上训练了上述 CNN,分别使用了 0%、25%、50%、75%和 100%的附加合成样本。例如,0%的合成样本意味着数据全部为原始数据,而 100%则意味着训练集包含相等数量的原始数据和合成数据。对于每个网络,我们随后使用准确度指标在一组未见过的实际测试数据上评估了性能。下图可视化了不同合成数据比例对测试准确率的影响。

二分类(正常与鳞状肿瘤)分类的测试准确率
训练模型时不稳定,因此我们排除了准确度为 1.0 或极低的迭代。这帮助我们避免了过拟合或欠拟合的训练迭代。
我们可以看到,从 0%到 25%时,测试准确度出现了急剧上升,这表明即使是通过少量的数据增强,也能对数据最初较少的问题产生较大影响。
由于我们仅在 80 KIMG 上训练了我们的 GAN(由于计算限制),因此如果有更多的 GAN 训练迭代,我们的合成数据质量可能会更好。值得注意的是,合成数据质量的提高也可能影响上述图表。我们假设,合成数据质量的提高将导致在训练中使用合成数据的最佳比例增加。此外,如果合成图像更好地适应我们训练数据的真实分布,我们就可以在模型训练中融入更多的合成图像而不至于过拟合。
结论
在这个项目中,使用 GAN 进行有限数据的增强被证明是扩展训练集并且更重要的是提高分类精度的有效技术。尽管我们选择了一个小而基础的问题,但这一技术可以通过几种方式轻松扩展。未来的工作可能包括使用更多的计算资源来获取更好的合成样本,引入更多的类别到分类任务中(使其成为一个多分类问题),并实验更新的 GAN 架构。不管怎样,使用 GAN 增强小数据集现在可以将许多以前受数据限制的问题纳入深度神经网络的范畴。
Kaggle 数据集
我们将增强和重新调整大小的图像汇编成以下Kaggle 数据集。该数据集包含 501 张正常和 501 张鳞状的 224x224 合成图像,可用于进一步的实验。
引用
[1] Hany, Mohamed, 胸部 CT 扫描图像数据集,Kaggle(2020)。
[2] Karras, Tero,等, 使用有限数据训练生成对抗网络(2020),《神经信息处理系统进展》2020。
[3] Ioffe, Sergey, 和 Christian Szegedy, 批量归一化:通过减少内部协变量偏移加速深度网络训练,(2015),国际机器学习会议。pmlr,2015。
[4] He, Kaiming,等, 深入探讨修正器:在 imagenet 分类中超越人类级别的表现,(2015),IEEE 国际计算机视觉会议论文集。2015。
[5] Bai, Yuhan, RELU 函数及其导函数回顾,(2022),SHS Web of Conferences。第 144 卷。EDP Sciences,2022。
增强数据科学工作流:掌握 Jupyter Notebook 的版本控制
一份实践指南,帮助通过 Jupytext、nbstripout 和 nbconvert 实现协作与可复现性
·发布于 Towards Data Science ·8 分钟阅读·2024 年 1 月 11 日
--

图像由 DALL-E 生成
对于数据科学家来说,使用版本控制系统有效管理 Jupyter notebooks 是至关重要的。这不仅是为了保持工作流的有序,还为了确保结果的可复现性,并促进团队成员之间的协作。在本指南中,我们将探索三个关键工具——Jupytext、nbstripout 和 nbconvert——每个工具都有其独特的特点。我将提供详细的描述、实用示例,以及一个(希望是)平衡的观点,帮助你确定最适合你特定需求的 notebook 版本控制工具。
理解 Jupyter notebooks 和版本控制的挑战
Jupyter notebooks 虽然非常适合进行探索性数据分析和可视化,但在版本控制方面却带来了挑战。这主要是因为这些 notebooks 不仅仅是普通的文本文件,而是以 JSON 文档的形式进行结构化。尽管这种格式非常适合维持代码、文本和输出数据之间复杂的相互关系,但在处理版本控制时却面临诸多挑战……
在 RAG 设置中通过自我检索机制提升直接答案准确性
利用大语言模型(LLM)的强大能力显著提升在 RAG 设置中直接生成答案时检索到的文档上下文的质量。
·发布于Towards Data Science ·26 分钟阅读·2024 年 4 月 30 日
--

一个机器人正在搜索信息(图像由 Midjourney 生成)
概述
部署检索增强生成(RAG)工作流到生产环境中引入了一个常见的挑战:在检索阶段召回率显著下降。当提供给直接生成答案的文档上下文缺乏关键信息时,这个问题变得尤为明显,进而影响生成答案的质量。当处理类似性质且由长篇文章构成的文档时,这一挑战尤为突出。在某些情况下,为了回答一个问题,可能需要考虑文章的大部分内容,例如在我们关于汽车的数百万篇编辑文章中进行导航。在这种情况下,结合混合搜索方法的自我检索 RAG 实现提供了一个有前景的解决方案。
RAG 无疑是一个强大的工具,像 LangChain 或 LlamaIndex 这样的框架使其集成变得非常简单。这种简便性可能会给人一种 RAG 是“一刀切”解决方案的印象。然而,在我们努力提升编辑文章搜索工具以实现更丰富语义的搜索时……
用生成式人工智能提升电子商务 — 第一部分
从数据到产品推荐
·发表于Towards Data Science ·16 分钟阅读·2024 年 7 月 31 日
--

图片来自unsplash.com
生成式人工智能正在革新各个领域企业的运作方式,电子商务也不例外。随着电子商务的不断发展,对更具可扩展性和复杂度的解决方案的需求也在增加。在这一系列文章中,我们将探讨生成式人工智能在电子商务中的各种应用。在第一部分中,我们将介绍生成式人工智能如何帮助产品推荐。我们首先使用产品的元数据来学习其代表性嵌入,并展示这些嵌入如何帮助分类物品。接着,我们根据用户的历史购买记录计算用户的嵌入。最后,我们利用学到的用户嵌入和物品嵌入来推荐产品。在系列的第二部分中,我们将探讨如何生成引人入胜的产品描述,并如何从评论中提取关键的正面和负面特征。
让我们从第一部分开始。这是本文的目录:
目录
1. 数据集
-
合成数据生成
-
物品元数据数据集生成
-
用户-物品数据集生成
3. 学习物品的代表性嵌入
- 数据准备与清洗
通过语义层增强语言模型与图数据库之间的交互
为 LLM 代理提供一套强大的工具,以便其与图数据库进行交互
·发表于Towards Data Science ·阅读时长 11 分钟·2024 年 1 月 18 日
--
知识图谱通过灵活的数据模式提供了对数据的出色表示,能够存储结构化和非结构化信息。你可以使用 Cypher 语句从图数据库(如 Neo4j)中检索信息。一种选择是使用大型语言模型(LLMs)生成 Cypher 语句。虽然这种方式提供了极大的灵活性,但事实是,基础 LLMs 在始终如一地生成精确的 Cypher 语句方面仍然不够稳定。因此,我们需要寻找一种替代方案,以确保一致性和稳健性。那么,如果 LLM 从用户输入中提取参数,而不是直接开发 Cypher 语句,并根据用户意图使用预定义的函数或 Cypher 模板呢?简而言之,你可以为 LLM 提供一套预定义的工具和使用指令,指示何时以及如何基于用户输入使用它们,这也被称为语义层。

语义层是一个中间步骤,提供了一种更精确、更稳健的方式,使 LLMs 与知识图谱进行交互。图片来自作者。灵感来源于这张图片。
语义层由多个暴露给大语言模型的工具组成,它可以用来与知识图谱进行互动。这些工具的复杂性各异。你可以将语义层中的每个工具看作一个函数。例如,看看以下这个函数。
def get_information(entity: str, type: str) -> str:
candidates = get_candidates(entity, type)
if not candidates:
return "No information was found about the movie or person in the database"
elif len(candidates) > 1:
newline = "\n"
return (
"Need additional information, which of these "
f"did you mean: {newline + newline.join(str(d) for d in candidates)}"
)
data = graph.query(
description_query, params={"candidate": candidates[0]["candidate"]}
)
return data[0]["context"]
这些工具可以有多个输入参数,正如上面的例子所示,这使你能够实现复杂的工具。此外,工作流不仅仅可以由数据库查询组成,还允许你处理任何边缘情况或异常,具体取决于你的需求。其优势在于,你将可能大部分时间有效的提示工程问题,转化为每次都能精确执行的代码工程问题。
电影代理
在这篇博客文章中,我们将展示如何实现一个语义层,使大语言模型代理能够与包含演员、电影及其评分信息的知识图谱进行互动。

电影代理架构。图像由作者提供。
取自文档(同样由我编写):
代理利用多种工具有效地与 Neo4j 图数据库进行交互。
*** 信息工具**:检索有关电影或个人的数据,确保代理能够访问最新和最相关的信息。
*** 推荐工具**:根据用户的偏好和输入提供电影推荐。
*** 内存工具**:在知识图谱中存储有关用户偏好的信息,从而在多次互动中提供个性化体验。
代理可以使用信息或推荐工具从数据库中检索信息,或使用内存工具将用户偏好存储到数据库中。
预定义的函数和工具使得代理能够编排复杂的用户体验,引导用户朝着特定目标前进,或提供与其当前用户旅程位置相关的定制信息。
这种预定义方法通过减少大语言模型的艺术自由度,从而增强了系统的稳健性,确保回应更加结构化,并与预定的用户流程对齐,从而改善了整体的用户体验。
电影代理的语义层后端已经实现,并作为一个LangChain 模板可用。我使用了这个模板构建了一个简单的 Streamlit 聊天应用程序。

Streamlit 聊天界面。图像由作者提供。
代码可在GitHub上获取。你可以通过定义环境变量并执行以下命令来启动项目:
docker-compose up
图模型
该图基于MovieLens数据集。它包含有关演员、电影和 10 万条电影评分的信息。

图谱架构。图像由作者提供。
该可视化图展示了一个知识图谱,包含那些参与过电影表演或导演的个人,并且按类型进行了进一步分类。每个电影节点包含其发布日期、标题和 IMDb 评分信息。图谱中还包含用户评分,我们可以利用这些评分提供推荐。
你可以通过执行位于文件夹根目录的 ingest.py 脚本来填充图谱。
定义工具
现在,我们将定义代理可以用来与知识图谱互动的工具。我们从信息工具开始。信息工具的设计目的是获取有关演员、导演和电影的相关信息。Python 代码如下所示:
def get_information(entity: str, type: str) -> str:
# Use full text index to find relevant movies or people
candidates = get_candidates(entity, type)
if not candidates:
return "No information was found about the movie or person in the database"
elif len(candidates) > 1:
newline = "\n"
return (
"Need additional information, which of these "
f"did you mean: {newline + newline.join(str(d) for d in candidates)}"
)
data = graph.query(
description_query, params={"candidate": candidates[0]["candidate"]}
)
return data[0]["context"]
函数首先通过全文索引查找提到的相关人物或电影。Neo4j 中的全文索引底层使用 Lucene。它支持无缝实现基于文本距离的查找,允许用户拼写错误的词语也能获取到结果。如果没有找到相关实体,我们可以直接返回响应。另一方面,如果识别出多个候选项,我们可以引导代理向用户询问后续问题,并让他们对感兴趣的电影或人物给出更具体的信息。假设用户问:“约翰是谁?”。
print(get_information("John", "person"))
# Need additional information, which of these did you mean:
# {'candidate': 'John Lodge', 'label': 'Person'}
# {'candidate': 'John Warren', 'label': 'Person'}
# {'candidate': 'John Gray', 'label': 'Person'}
在这种情况下,工具通知代理需要额外的信息。通过简单的提示工程,我们可以引导代理询问用户后续问题。假设用户提供了足够具体的信息,这样工具就能识别特定的电影或人物。在这种情况下,我们使用带参数的 Cypher 语句来检索相关信息。
print(get_information("Keanu Reeves", "person"))
# type:Actor
# title: Keanu Reeves
# year:
# ACTED_IN: Matrix Reloaded, The, Side by Side, Matrix Revolutions, The, Sweet November, Replacements, The, Hardball, Matrix, The, Constantine, Bill & Ted's Bogus Journey, Street Kings, Lake House, The, Chain Reaction, Walk in the Clouds, A, Little Buddha, Bill & Ted's Excellent Adventure, The Devil's Advocate, Johnny Mnemonic, Speed, Feeling Minnesota, The Neon Demon, 47 Ronin, Henry's Crime, Day the Earth Stood Still, The, John Wick, River's Edge, Man of Tai Chi, Dracula (Bram Stoker's Dracula), Point Break, My Own Private Idaho, Scanner Darkly, A, Something's Gotta Give, Watcher, The, Gift, The
# DIRECTED: Man of Tai Chi
有了这些信息,代理可以回答大多数关于基努·里维斯的问题。
现在,让我们引导代理有效地使用这个工具。幸运的是,借助 LangChain,这一过程既简单又高效。首先,我们使用 Pydantic 对象定义函数的输入参数。
class InformationInput(BaseModel):
entity: str = Field(description="movie or a person mentioned in the question")
entity_type: str = Field(
description="type of the entity. Available options are 'movie' or 'person'"
)
在这里,我们说明了 entity 和 entity_type 参数都是字符串类型。entity 参数输入被定义为问题中提到的电影或人物。而对于 entity_type,我们也提供了可选项。在处理低基数(即有少量不同值)的情况时,我们可以直接向 LLM 提供可选项,让它使用有效的输入。正如我们之前看到的,我们使用全文索引来消除电影或人物的歧义,因为有太多值不能直接在提示中提供。
现在,让我们将这一切汇总到信息工具定义中。
class InformationTool(BaseTool):
name = "Information"
description = (
"useful for when you need to answer questions about various actors or movies"
)
args_schema: Type[BaseModel] = InformationInput
def _run(
self,
entity: str,
entity_type: str,
run_manager: Optional[CallbackManagerForToolRun] = None,
) -> str:
"""Use the tool."""
return get_information(entity, entity_type)
准确简洁的工具定义是语义层的重要组成部分,以便代理在需要时能够正确选择相关工具。
推荐工具稍微复杂一些。
def recommend_movie(movie: Optional[str] = None, genre: Optional[str] = None) -> str:
"""
Recommends movies based on user's history and preference
for a specific movie and/or genre.
Returns:
str: A string containing a list of recommended movies, or an error message.
"""
user_id = get_user_id()
params = {"user_id": user_id, "genre": genre}
if not movie and not genre:
# Try to recommend a movie based on the information in the db
response = graph.query(recommendation_query_db_history, params)
try:
return ", ".join([el["movie"] for el in response])
except Exception:
return "Can you tell us about some of the movies you liked?"
if not movie and genre:
# Recommend top voted movies in the genre the user haven't seen before
response = graph.query(recommendation_query_genre, params)
try:
return ", ".join([el["movie"] for el in response])
except Exception:
return "Something went wrong"
candidates = get_candidates(movie, "movie")
if not candidates:
return "The movie you mentioned wasn't found in the database"
params["movieTitles"] = [el["candidate"] for el in candidates]
query = recommendation_query_movie(bool(genre))
response = graph.query(query, params)
try:
return ", ".join([el["movie"] for el in response])
except Exception:
return "Something went wrong"
首先需要注意的是,这两个输入参数都是可选的。因此,我们需要引入处理所有可能输入参数组合和缺失情况的工作流。为了生成个性化推荐,我们首先获取user_id,然后将其传递到下游的 Cypher 推荐语句中。
与之前一样,我们需要向代理呈现函数的输入。
class RecommenderInput(BaseModel):
movie: Optional[str] = Field(description="movie used for recommendation")
genre: Optional[str] = Field(
description=(
"genre used for recommendation. Available options are:" f"{all_genres}"
)
)
由于只有 20 种可用的电影类型,我们将它们的值作为提示的一部分提供。为了避免电影歧义,我们再次在函数内部使用全文索引。像之前一样,我们以工具定义结束,以通知 LLM 何时使用它。
class RecommenderTool(BaseTool):
name = "Recommender"
description = "useful for when you need to recommend a movie"
args_schema: Type[BaseModel] = RecommenderInput
def _run(
self,
movie: Optional[str] = None,
genre: Optional[str] = None,
run_manager: Optional[CallbackManagerForToolRun] = None,
) -> str:
"""Use the tool."""
return recommend_movie(movie, genre)
到目前为止,我们已经定义了两个工具来从数据库中检索数据。然而,信息流不必是单向的。例如,当用户告诉代理他们已经看过某部电影并且可能喜欢它时,我们可以将该信息存储在数据库中,并在未来的推荐中使用它。此时,记忆工具将非常有用。
def store_movie_rating(movie: str, rating: int):
user_id = get_user_id()
candidates = get_candidates(movie, "movie")
if not candidates:
return "This movie is not in our database"
response = graph.query(
store_rating_query,
params={"user_id": user_id, "candidates": candidates, "rating": rating},
)
try:
return response[0]["response"]
except Exception as e:
print(e)
return "Something went wrong"
class MemoryInput(BaseModel):
movie: str = Field(description="movie the user liked")
rating: int = Field(
description=(
"Rating from 1 to 5, where one represents heavy dislike "
"and 5 represent the user loved the movie"
)
)
记忆工具有两个强制性的输入参数,用于定义电影及其评分。这是一个简单的工具。我需要提到的一点是,在我的测试中,我注意到可能需要提供何时给出特定评分的示例,因为 LLM 开箱即用时并不是最擅长这方面的处理。
代理
现在让我们通过使用LangChain 表达式语言(LCEL)来定义一个代理,把所有内容整合在一起。
llm = ChatOpenAI(temperature=0, model="gpt-4", streaming=True)
tools = [InformationTool(), RecommenderTool(), MemoryTool()]
llm_with_tools = llm.bind(functions=[format_tool_to_openai_function(t) for t in tools])
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"You are a helpful assistant that finds information about movies "
" and recommends them. If tools require follow up questions, "
"make sure to ask the user for clarification. Make sure to include any "
"available options that need to be clarified in the follow up questions "
"Do only the things the user specifically requested. ",
),
MessagesPlaceholder(variable_name="chat_history"),
("user", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
]
)
agent = (
{
"input": lambda x: x["input"],
"chat_history": lambda x: _format_chat_history(x["chat_history"])
if x.get("chat_history")
else [],
"agent_scratchpad": lambda x: format_to_openai_function_messages(
x["intermediate_steps"]
),
}
| prompt
| llm_with_tools
| OpenAIFunctionsAgentOutputParser()
)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True).with_types(
input_type=AgentInput, output_type=Output
)
LangChain 表达式语言使得定义代理并暴露其所有功能变得非常方便。我们不会深入探讨 LCEL 语法,因为那超出了本博客的范围。
电影代理后台作为 API 端点通过LangServe暴露。
Streamlit 聊天应用
现在我们只需要实现一个 Streamlit 应用,它连接到 LangServe API 端点,就可以开始使用了。我们将看看用于检索代理响应的异步函数。
async def get_agent_response(
input: str, stream_handler: StreamHandler, chat_history: Optional[List[Tuple]] = []
):
url = "http://api:8080/movie-agent/"
st.session_state["generated"].append("")
remote_runnable = RemoteRunnable(url)
async for chunk in remote_runnable.astream_log(
{"input": input, "chat_history": chat_history}
):
log_entry = chunk.ops[0]
value = log_entry.get("value")
if isinstance(value, dict) and isinstance(value.get("steps"), list):
for step in value.get("steps"):
stream_handler.new_status(step["action"].log.strip("\n"))
elif isinstance(value, str):
st.session_state["generated"][-1] += value
stream_handler.new_token(value)
函数get_agent_response旨在与电影代理 API 进行交互。它向 API 发送包含用户输入和聊天历史的请求,然后异步处理 API 的响应。该函数处理不同类型的响应,更新流处理器的新状态,并将生成的文本附加到会话状态中,从而使我们能够将结果流式传输给用户。
现在让我们进行测试。

电影代理在运行中。图像由作者提供。
结果显示,电影代理提供了与用户出乎意料的良好和引导式互动。
结论
总结来说,在语言模型与图数据库交互中引入语义层,如我们在电影代理(Movie Agent)中所示,代表了提升用户体验和数据交互效率的一次重大飞跃。通过将重点从生成任意的 Cypher 语句转移到利用结构化、预定义的工具和功能套件,语义层为语言模型的交互带来了一个新的精确性和一致性水平。这种方法不仅简化了从知识图谱中提取相关信息的过程,还确保了一个更具目标导向和用户中心的体验。
语义层充当着桥梁的角色,将用户意图转换为具体、可执行的查询,语言模型能够准确可靠地执行这些查询。因此,用户受益于一个不仅能够更有效理解他们查询的系统,而且还能更轻松、减少歧义地引导他们朝着预期的结果前进。此外,通过将语言模型的响应限制在这些预定义工具的范围内,我们降低了错误或无关输出的风险,从而增强了系统的可信度和可靠性。
代码可在GitHub上找到。
数据集
F. Maxwell Harper 和 Joseph A. Konstan. 2015. 《MovieLens 数据集:历史与背景》。ACM 交互智能系统交易(TiiS)5, 4: 19:1–19:19. doi.org/10.1145/2827872
通过因果 AI 提升营销组合建模
因果 AI,探索因果推理与机器学习的整合
·发布于 Towards Data Science ·阅读时间:8 分钟·2024 年 6 月 21 日
--

图片来源:Alexey Ruban 通过 Unsplash
这系列文章讲的是什么?
欢迎来到我的因果 AI 系列文章,我们将在这里探讨因果推理与机器学习模型的整合。你将会看到多个在不同商业场景中的实践应用。
在上一篇文章中,我们介绍了验证合成控制方法的因果影响。在本文中,我们将继续探讨通过因果 AI 提升营销组合建模。
如果你错过了上一篇关于合成控制的文章,可以在这里查看:
因果 AI,探索因果推理与机器学习的整合
towardsdatascience.com
介绍
数字跟踪的持续挑战促使了营销组合建模(MMM)的新一轮复兴。在最近的因果 AI 大会上,Judea Pearl 表示,营销可能是第一个采纳因果 AI 的行业。因此,我决定是时候开始撰写我在过去 7 年里有关 MMM、因果 AI 和实验交集的学习成果了。

用户生成的图片
以下领域将被探讨:
-
什么是 MMM?
-
因果 AI 如何增强 MMM?
-
我们可以进行哪些实验来完成三角定位?
-
营销测量中的突出挑战。
完整的笔记本可以在这里找到:
[## causal_ai/notebooks/enhancing marketing mix modelling with causal ai.ipynb 在主分支 ·…
本项目介绍了因果 AI 及其如何推动商业价值。- causal_ai/notebooks/enhancing marketing mix…
什么是 MMM?
MMM 是一种统计框架,用于估算每个营销渠道对销售的贡献。它受到计量经济学的深刻影响,最简单的形式是回归模型。让我们来介绍一下其关键组成部分的基础知识!
回归
构建回归模型,其中因变量/目标(通常是销售)是基于多个自变量/特征预测的 —— 这些通常包括在不同营销渠道上的支出和可能影响需求的外部因素。

用户生成的图片
支出变量的系数表明它们对销售的贡献大小。
Python 中的 PyMC 营销包是开始探索 MMM 的一个很好的起点:
[## MMM 示例笔记本 - pymc-marketing 0.6.0 文档
在本笔记本中,我们通过一个模拟示例来展示来自 pymc-marketing 的媒体混合模型(MMM)API。这…
www.pymc-marketing.io](https://www.pymc-marketing.io/en/stable/notebooks/mmm/mmm_example.html?source=post_page-----77f638bce3a9--------------------------------)
广告存量
广告存量指的是营销支出(或广告支出)对消费者行为的持久影响。它有助于建模营销的长期效果。通常,第一次听说某个品牌时,人们不会急于购买该产品 —— 广告存量的理念是,营销的效果是累积的。

用户生成的图片
最常见的广告存量方法是几何衰减法,它假设广告的影响会以恒定的速率随时间衰减。尽管这种方法相对容易实现,但它的灵活性较差。值得了解的是威布尔方法,它更为灵活 —— PyMC 营销包已实现此方法,务必查阅:
[## weibull_adstock - pymc-marketing 0.6.0 文档]
pymc_marketing.mmm.transformers.weibull_adstock ( x , , , , , , ) [source] 威布尔 Adstock 变换。这…
饱和度
在营销的语境下,饱和度指的是收益递减的概念。增加营销投入可以提高客户获取,但随着时间的推移,影响新受众变得更加困难。

用户生成的图像
我们可以使用几种饱和度方法。迈克利斯-门腾函数是常见的一种——你也可以在 PyMC 营销包中查看这个:
[## michaelis_menten - pymc-marketing 0.6.0 文档]
pymc_marketing.mmm.transformers.michaelis_menten ( x , alpha , lam ) [source] 评估迈克利斯-门腾函数…
因果 AI 如何增强 MMM?
MMM 框架通常使用平坦的回归模型。然而,营销渠道之间的相互作用存在一些复杂性。我们是否有来自因果 AI 工具箱的工具可以帮助解决这个问题?
因果图
因果图擅长将原因与相关性分离,这使得它们成为解决营销渠道相互作用复杂性的问题的绝佳工具。
如果你对因果图不熟悉,可以参考我之前的文章来迅速了解:
因果 AI,探索将因果推理融入机器学习
towardsdatascience.com](/using-causal-graphs-to-answer-causal-questions-5fd1dd82fa90?source=post_page-----77f638bce3a9--------------------------------)
理解营销图
在缺乏领域知识的情况下估计因果图是具有挑战性的。但我们可以利用因果发现来帮助我们入门——请查看我之前关于因果发现的文章,了解更多信息:
因果 AI,探索因果推理与机器学习的结合
towardsdatascience.com
因果发现有其局限性,应该仅用于为图形创建初始假设。幸运的是,关于营销渠道如何相互作用的领域知识非常丰富,我们可以将其纳入其中!
在这里,我分享我多年来与营销专家合作中积累的知识…

用户生成图像
-
PPC(付费搜索)对 SEO(自然搜索)有负面影响。我们在 PPC 上的花费越多,SEO 点击量就越少。然而,我们有一个重要的混杂因素……需求!一个简单的回归模型通常无法捕捉到这一复杂性,往往导致 PPC 的过度估计。
-
社交支出对社交点击有很强的影响,我们花费越多,潜在客户点击社交广告的次数就越多。然而,一些潜在客户可能会先看到社交广告,然后在第二天通过 PPC、SEO 或直接访问你的站点。一个简单的回归模型无法捕捉到这种光环效应。
-
对于品牌支出,也可以做类似的分析,你通过长期的品牌信息来吸引潜在客户,但没有直接的点击行动号召。这些潜在客户可能在意识到你的品牌后,通过 PPC、SEO 或直接访问你的站点。
-
点击量是中介变量。如果我们运行一个简单的回归模型并包括中介变量,这在估计因果效应时可能会引发问题。我在这里不会详细讨论这个话题,但使用因果图使我们能够在估计因果效应时仔细控制正确的变量。
希望你能从以上的例子中看到,使用因果图而非简单回归模型会大大增强你的解决方案。计算反事实和进行干预的能力也使它非常有吸引力!
值得注意的是,仍然值得将广告库存和饱和度转化纳入你的框架中。
我们可以进行什么实验来完成三角验证?
在使用观察性数据时,我们还应该努力进行实验,以帮助验证假设并补充我们的因果估计。获取营销中有三种主要的测试方法。让我们深入了解它们!
转化提升测试
社交平台如 Facebook 和 Snapchat 允许你进行转化提升测试。这是一种 AB 测试,我们通过治疗组与对照组的对比来衡量转化的提升。这对于评估因果图中社交支出的反事实(counterfactual)非常有用。
地理位置提升测试
地理提升测试可以用来估算营销暂停期间或你开始使用新渠道时的效果。这对于品牌数字广告和电视广告特别有用,因为它们没有直接的行动号召来衡量。我在上一篇文章中详细讲解了这一点:
因果 AI,探索将因果推理集成到机器学习中的方法
[towardsdatascience.com
切换回测
PPC(按点击付费)广告系列可以设置为每小时开启和关闭。这为切换回测提供了一个很好的机会。将 PPC 广告系列每小时开关一次,持续几周,然后计算 PPC 和 SEO 点击量在关闭与开启期间的差异。这将帮助你理解多少 PPC 可以通过 SEO 捕捉,从而评估你因果图中关于 PPC 支出的反事实。
我认为,进行实验是调整并增强你因果图信心的一个好方法。但结果也可以用来校准你的模型。看看 PyMC 团队是如何处理这个问题的:
[## 提升测试校准 - pymc-marketing 0.6.0 文档
你可能听过一句话:“所有模型都是错误的;但有些模型是有用的。” 这在许多领域都是真实的,而且…
营销衡量中的突出挑战
今天我讲解了如何利用因果 AI 来提升 MMM(营销混合建模)。然而,因果 AI 并不能解决获取营销中的所有挑战——不幸的是,这些挑战非常多!

用户生成的图片
-
根据需求预测调整支出 — 营销支出与销售量高度相关的一个原因是营销团队会根据需求预测来调整支出。解决方案之一是每周随机调整支出范围在-10%到+10%之间,加入一些变动。如你所想,营销团队通常不太喜欢这种方法!
-
估算需求 — 需求是我们模型中的一个关键变量。然而,收集相关数据非常困难。一个合理的选择是提取与您销售的产品相关的搜索词的 Google 趋势数据。
-
品牌的长期效应 — 品牌的长期效应很难捕捉,因为通常没有足够的信号来支持这一点。长期的地理提升测试可以帮助解决这个问题。
-
多重共线性——这实际上是最大的问题之一。我们所有的变量都高度相关。使用岭回归可以稍微缓解这一问题,但它仍然可能成为一个问题。因果图也能提供一些帮助,因为它本质上将问题分解为更小的模型。
-
营销团队的支持——根据我的经验,这将是你面临的最大挑战。因果图提供了一种吸引营销团队的不错视觉方式。它还为你创造了一个机会,在与他们合作并达成图表细节的过程中建立关系。
我就先说到这里——很希望听听你在评论中的看法!
如果你想继续深入了解因果 AI,记得关注我——在下一篇文章中,我们将探讨因果 AI 是否能够改进我们的预测。
使用 LLM 和统计推理增强 NPS 测量
通过预测驱动推理(PPI)将 LLM 与人工判断结合
·发表于 Towards Data Science ·阅读时间 8 分钟·2024 年 3 月 6 日
--

机器人解决复杂数学问题,数字艺术。由 Dall-E 2 生成。
介绍
在商业分析中,计算净推荐值(NPS)通常需要员工手动标注数据。有些人可能会考虑使用机器学习模型来标注数据,但这并没有提供我们从人工标注数据中得到的理论保障。这里引入了预测驱动推理(PPI),一种新型的统计技术,它结合了人工和机器标注的数据,创建了数据高效且理论上有保障的置信区间。
本文探讨了 PPI 背后的直觉,并强调了为什么你应该使用它。接着我们将进入如何使用它来处理两个指标的代码演示:NPS 和客户推荐。
预测驱动推理(PPI)
PPI 是 Angelopoulos 等人提出的一种统计技术[1]。其目标是通过结合人工和机器标注的数据来增强置信区间。让我们通过一些步骤来了解它的实用性。
在我们的应用场景中,我们希望在给定一组客户评价的情况下,估计真实的 NPS 分数。通常,员工会手动阅读每条评价,并为其分配一个 1 到 10 的分数,这是一种可靠但效率较低的方法。当需要处理大量评价时,使用更自动化的方法会更加便捷。
为了解决这个问题,我们可以利用机器学习模型。大型语言模型(LLM)是一个很好的候选者,因为它们在处理新任务时具有很好的泛化能力。该模型会被提示读取评论并输出一个分数。这很方便,但模型也有错误和缺陷。在做决策时,我们需要确保我们的数据与人类判断一致。
考虑到两种方法的局限性,如果我们能够将它们结合起来呢?通过预测驱动推断(PPI),我们可以做到这一点!PPI 是一个框架,它结合了人工标注数据在置信区间方面的理论保证与机器标注数据的高效性。通过 PPI,我们旨在发挥两种技术的优势。
工作原理
PPI 的核心是一个叫做“整流器”的工具。我们使用整流器来弥补机器学习模型的预测误差。借助整流器,我们可以结合人工和机器标注数据,构建置信区间。
这是构造置信区间的算法:

来自[1]的算法 1。
这种方法的优点是代码实现简单。这里是一个简短的代码片段,展示如何实现:
def pp_mean_iid_asymptotic(Y_labeled, Yhat_labeled, Yhat_unlabeled, alpha):
n = Y_labeled.shape[0]
N = Yhat_unlabeled.shape[0]
tildethetaf = Yhat_unlabeled.mean()
rechat = (Yhat_labeled - Y_labeled).mean() # rectifier (delta hat)
thetahatPP = tildethetaf - rechat # Prediction-Powered Estimator
sigmaftilde = np.std(Yhat_unlabeled) # imputed std dev
sigmarec = np.std(Yhat_labeled - Y_labeled) # rectifier std dev
hw = norm.ppf(1-alpha/2)*np.sqrt((sigmaftilde**2/N) + (sigmarec**2/n)) # normal approximation
return [thetahatPP - hw, thetahatPP + hw] # confidence interval
如果你想更深入了解,我推荐观看这段YouTube视频,视频中的 Clara Wong-Fannjiang(其中一位作者)进行了解释,或者查看论文。这些资源能比我在这里所做的更好地解释相关概念。
需要理解的重要一点是,PPI 的置信区间比仅使用人工构建的置信区间更为紧凑,并且具有预测置信区间所没有的理论保证。理解这一点就足以完成代码练习。
方法
完整的笔记本链接可以在这里找到。我将逐步介绍关键步骤,并提供一些评论。大部分代码归功于 PPI 库的作者。
在我们的例子中,我们将使用 PPI 来估计均值。也可以估计其他参数,比如分位数、逻辑回归/线性回归系数等。通过这个例子后,你可以在这里找到更多内容。
设置步骤
首先,让我们安装 PPI 库。要查看更多关于该库的信息,请查看这个仓库这里。
pip install ppi-python
对于这个例子,我们将模拟数据。很难找到公开可用的 NPS 得分数据,因此我创建了以下代码。不管数据来源于何处,方法都是相同的。我们将创建一个包含Overall_Rating(NPS 得分)和Recommended(布尔值)列的 DataFrame。
def simulate_nps_scores(n, mu=3, mu2=9, std_dev=1):
# simulate each aspect of bimodal distribution
X1 = np.random.normal(mu, std_dev, n // 3)
X2 = np.random.normal(mu2, std_dev, n // 3)
X = np.concatenate([X1, X2])
X3 = np.ones(n - X.shape[0]) # make 1-inflated
X = np.concatenate([X, X3])
X = np.clip(X, a_min=1, a_max=10) # fix to 1-10 range for NPS
return X
def simulate_recommended(mean, n):
return np.array([1 if random.uniform(0,1) <= mean else 0 for _ in range(n)])
使用这些函数,我们可以构建 DataFrame:
N = 20000
data = pd.DataFrame({
'Overall_Rating': simulate_nps_scores(N),
'Recommended': simulate_recommended(0.34, N)
})
模拟 LLM 预测
为了制作一个更灵活的演示,我提供了两种创建预测的选项。第一种是为预测创建模拟误差。这使你能够实验 PPI,并观察它如何作用于不同的理论模型。以下是这种情况的示例:
if target_response == 'NPS':
Y_total = data.Overall_Rating.to_numpy()
Yhat_total = np.array([random.normalvariate(x, error_std_dev) for x in Y_total])
Yhat_total = np.array([max(min(x, 10), 1) for x in Yhat_total])
elif target_response == 'reccomended':
Y_total = data.Recommended.to_numpy()
Yhat_total = np.array([
x if random.uniform(0, 1) >= error_prob else int(not x)
for x in Y_total
])
else:
raise Exception('Invalid target_response')
LLM 预测
如果你有带有客户评论的数据,那么使用 LLM 对其打分就很容易了。以下是你可以用来执行此操作的一些提示:
NPS_prompt_template = lambda review: f"""Given the following review please return the Net Promoter Score (NPS).
Return only the integer value from 1-10 and nothing else.
Review:
{review}
NPS:"""
recommended_prompt_template = lambda review: f"""Given the following review please determine if the customer would recommend the business.
Return only 'True' or 'False'.
Review: {review}
Recommended:"""
你可能希望使用推荐而不是 NPS,因为布尔分类问题要简单得多。一些企业偏好使用 NPS,因为它是行业标准。根据你的问题,你可以选择哪个更有意义。
使用 LLM,你还可以检查不同类别的得分。这些类别可以是你想要衡量的不同产品或服务。这是使用 LLM 的一个巨大优势,因为它对许多问题都具有灵活性,但我们在报告中也通过使用 PPI 来考虑误差。
运行 PPI
现在是时候构建置信区间了。以下是执行重负荷任务的代码片段:
for i in tqdm(range(ns.shape[0])):
for j in range(num_trials):
# Prediction-Powered Inference
n = ns[i]
rand_idx = np.random.permutation(n_total)
_Yhat = Yhat_total[rand_idx[:n]]
_Y = Y_total[rand_idx[:n]]
_Yhat_unlabeled = Yhat_total[n:]
ppi_ci = ppi_mean_ci(_Y, _Yhat, _Yhat_unlabeled, alpha=alpha)
classical_ci = classical_mean_ci(_Y, alpha=alpha)
我们在这里所做的,是模拟 PPI 与经典置信区间的不同,针对不同数量的人工响应(n)。我们可以绘制出以下信息。

NPS 得分的 PPI 与经典方法对比。绿色表示 PPI,灰色表示经典方法,黄色表示机器标注数据,虚线表示真实的总体参数。图像由作者提供。
在这张比较不同 n 值(使用的人工标注数量)的图表中,你可以看到,PPI 的置信区间始终比仅使用人工标注数据的置信区间更紧密。这展示了 PPI 的关键价值:即使我们有一个有缺陷的机器学习模型,我们通过将其与人工数据结合使用,仍然能生成比仅使用它更好的置信区间。
我们可以看到下方推荐的类似结果。

推荐的 PPI 与经典方法对比。绿色表示 PPI,灰色表示经典方法,黄色表示机器标注数据,虚线表示真实的总体参数。图像由作者提供。
在这种情况下,我们查看的是所有客户中推荐使用该企业的客户的真实百分比。再次,我们看到,使用 PPI,我们能够构建比仅使用人工标注数据更紧密的置信区间。
你还会注意到黄色的机器预测的置信区间。这些预测并不完全准确,所以置信区间非常宽泛。这就是为什么我们需要一些人工标注数据,而不能仅仅使用机器标注数据的原因。
决策制定
现在让我们考虑一下,为了做出决策,对于 PPI 和经典方法分别需要多少个人工标注。
从 NPS 开始。假设我们想要模拟需要多少个人工标注的样本,才能拒绝 NPS 小于或等于 4 的零假设。我们可以运行以下代码来找到最小值:
def _to_invert_ppi(n):
n = int(n)
nulls_rejected = 0
# Data setup
for i in range(num_experiments):
rand_idx = list_rand_idx[i]
_Yhat = Yhat_total[rand_idx[:n]]
_Y = Y_total[rand_idx[:n]]
_Yhat_unlabeled = Yhat_total[rand_idx[n:]]
ppi_ci = ppi_mean_ci(_Y, _Yhat, _Yhat_unlabeled, alpha=alpha)
if target_response == 'NPS' and ppi_ci[0] > null_hypothesis:
nulls_rejected += 1
elif target_response == 'recommended' and ppi_ci[0] > null_hypothesis:
nulls_rejected += 1
return nulls_rejected / num_experiments - statistical_power
n_ppi = int(brentq(_to_invert_ppi, 100, 1000, xtol=1))
这模拟了 PPI 拒绝零假设所需的最小示例数量。传统示例的代码也类似。请参见notebook了解完整细节。
让我们来看一下这里的输出:
The PPI test requires n=334 labeled data points to reject the null.
The classical test requires n=987 labeled data points to reject the null.
解释是,PPI 相比仅使用人工标注的例子,需要少 653 个人工标注的观察值就能拒绝零假设。
我们可以对推荐进行重复此过程。我们唯一的变化是零假设的值。我们测试零假设,假设真正推荐该业务的客户百分比小于或等于 0.3。
The PPI test requires n=461 labeled data points to reject the null.
The classical test requires n=1000 labeled data points to reject the null.
在这里我们可以看到,使用传统方法比使用人工方法需要更多的观察值来得出结论,总共多出 539 个。
这些结果有意义吗?
653 个或 539 个观察值可能看起来不多,但在内部数据标注的领域中,它是一个相当大的数量。假设是周五下午,你的老板要求你从刚收到的一组调查问卷中确定 NPS 评分。为了做出这个判断,你需要手动标注一些观察值。
假设你每分钟可以标注 4 条评论。这意味着你每小时可以标注 240 条评论。如果你使用 PPI,你将比使用传统置信区间早 2-3 小时离开。减少琐碎的任务对员工幸福感有很大好处,因此这种方法值得投资,因为它的额外开销很小。
结论
这只是使用 PPI 解决基本统计推理问题的简要概述。我们看到如何从样本数据集中计算总体均值,适用于两种不同类型的变量。这种方法在付出极少额外工作后,可以带来显著的时间节省。
想要查看更多如何使用 PPI 的示例,请查看来自仓库的examples文件夹。它们涵盖了许多更有趣的用例。祝编程愉快!
感谢阅读这篇文章!如果你有任何额外问题或有些地方不清楚,请留言,我会回复你。如果你想看到更多类似的文章,请在 Medium 和 LinkedIn 上关注我。
如果你在这篇文章中发现技术错误,请尽快告诉我!我力求确保我发布的信息尽可能准确,但没有人是完美的。
参考文献:
[1] Anastasios N. Angelopoulos, Stephen Bates, Clara Fannjiang, Michael I. Jordan, & Tijana Zrnic. (2023). 基于预测的推理。
通过注释提高 Python 代码的可读性
PYTHON 编程
注释是一个强大的开发工具。阅读本文以了解如何以及在哪里使用它们。
·发表于Towards Data Science ·阅读时间:21 分钟·2024 年 4 月 12 日
--

代码注释就像用标记笔做高亮。照片由Mitchell Luo提供,来源于Unsplash
代码清晰既是一种美德,也是一种必需。如果你写出清晰易读的代码,其他开发者能够理解它,用户能够明白如何使用它,甚至未来的你也会感谢它,因为随着时间的推移,我们大多数人都会忘记我们实现的代码的细节。随着项目和代码库规模的不断扩大,代码清晰性变得越来越重要。
在编程语言中,Python 提供了非常易读的代码。或者更确切地说,Python可以提供非常易读的代码——但你需要知道如何使其易读。我甚至可以说,Python 之所以如此流行,有几个原因,其中一个重要原因就是代码的可读性。因此,写出好的 Python 代码是我们的责任。为了做到这一点,我们需要工具。
代码清晰既是一种美德,也是一种必需。
有许多工具可以提高 Python 代码质量。首先,我们需要编写符合良好 Python 代码标准的代码,这些标准由 PEP 8 提供:
集成学习在异常检测中的应用
深入探讨隔离森林模型,用于检测时间序列数据中的异常值
·发表于 Towards Data Science ·阅读时间:7 分钟·2024 年 10 月 30 日
--
异常检测是任何组织都必备的能力。通过检测异常值和离群点,我们不仅能识别出看似可疑(或可能错误)的数据,还能建立“正常”数据的标准。异常检测对于强大的数据治理系统至关重要,它能够识别数据错误。而在分析中,离群点在某些情况下,如欺诈检测和预测性维护,可能成为重点。
然而,随着数据量的增加,异常检测变得越来越困难。高维数据通常带有噪声,使得其难以用于分析和提取洞见。大规模数据集也可能包含错误和/或特殊情况。幸运的是,集成学习通过提高速度和效率,帮助我们处理高维数据并检测异常值。
什么是集成学习?
集成学习是一种机器学习技术,它通过结合多个独立模型的预测,来获得比任何单一模型更好的预测性能。每个模型被视为一个“弱学习器”,并在数据的一个小子集上进行训练以进行预测。然后它进行投票。每个弱学习器都会被调查,最终通过多数票决定预测结果。

图片来源:Wikimedia Commons(commons.wikimedia.org/wiki/File:Random_forest_explain.png)
集成模型(在高质量数据上训练)具有鲁棒性、准确性、高效性,且能够有效避免过拟合。它们有许多应用场景,如分类、优化,以及在我们这个案例中的异常检测。
隔离森林模型
孤立森林模型是一种由多棵树组成的集成模型,能够隔离那些稀疏的观测值。它与流行的“随机森林”模型非常相似,但不同的是,孤立森林生成的是“孤立树”森林,而不是决策树森林。
那么它是如何工作的呢?让我们来看一棵孤立树。

图片由作者提供
看一下上面的数据。我们可以看到一个数据点离其他数据较远(我们怀疑是异常值)。每棵孤立树会随机选择一个“分割值”来开始隔离观测值。 在这个例子中,怀疑的异常值会立即被隔离。由于它与其他数据点的距离较远,这种情况在大多数孤立树中都会发生。

图片由作者提供
接下来,它选择另一个分割。这次,怀疑是“正常”数据的部分开始被切分。这个过程会重复,直到每个观测值被隔离。 最终,模型通过随机选择一个特征,并在该特征的最大值和最小值之间随机选择一个分割值来“隔离”观测值。

图片由作者提供
现在每个观测值都已经被隔离,我们需要问:隔离每个观测值需要多少次分割? 换句话说,每个数据点的分割路径有多长?假设结果如下:

图片由作者提供
现在我们知道了隔离每个观测值需要多少次分割,我们计算出分割的平均次数。在我们的例子中,平均而言,隔离一个观测值需要 2.6 次分割。那些分割路径明显较短,或者隔离所需的分割次数明显较少的观测值,很有可能是异常值或离群值。 它们与平均分割次数的差异程度是模型中的一个参数。最后,孤立树判断观测值 G 是异常值。
孤立森林模型的最后一步是让每棵孤立树对哪些观测值是异常值进行“投票”。 如果大多数孤立树认为观测值 G 是异常值,那么模型就会判断它为异常值。
时间序列数据中的异常检测
让我们看一个简单的例子,使用孤立森林模型来检测时间序列数据中的异常值。下面,我们导入了一个包含订单日期、产品信息、客户地理信息和销售额的销售数据集。为了简化这个例子,我们只看一个特征(销售额)随时间的变化。
查看数据: https://www.kaggle.com/datasets/rohitsahoo/sales-forecasting (GPL 2.0)
#packages for data manipulation
import pandas as pd
from datetime import datetime
#packages for modeling
from sklearn.ensemble import IsolationForest
#packages for data visualization
import matplotlib.pyplot as plt
#import sales data
sales = pd.read_excel("Data/Sales Data.xlsx")
#subset to date and sales
revenue = sales[['Order Date', 'Sales']]
revenue.head()

图片由作者提供
如上所示,我们有每个特定日期所有订单的总销售额。由于我们有足够的数据(四年数据),让我们尝试检测出总销售额显著高于或低于预期总销售额的月份。
首先,我们需要进行一些预处理,并汇总每个月的销售额。然后,进行每月销售额的可视化。
#format the order date to datetime month and year
revenue['Order Date'] = pd.to_datetime(revenue['Order Date'],format='%Y-%m').dt.to_period('M')
#sum sales by month and year
revenue = revenue.groupby(revenue['Order Date']).sum()
#set date as index
revenue.index = revenue.index.strftime('%m-%Y')
#set the fig size
plt.figure(figsize=(8, 5))
#create the line chart
plt.plot(revenue['Order Date'],
revenue['Sales'])
#add labels and a title
plt.xlabel('Moth')
plt.ylabel('Total Sales')
plt.title('Monthly Sales')
#rotate x-axis labels by 45 degrees for better visibility
plt.xticks(rotation = 90)
#display the chart
plt.show()

图片来自作者
使用上面的折线图,我们可以看到,虽然销售额月度波动,但总销售额随时间趋势向上。理想情况下,我们的模型将识别出总销售额波动超过预期并且对整体趋势有较大影响的月份。
现在我们需要初始化并拟合我们的模型。下面的模型使用了默认参数。我已经突出显示了这些参数,因为它们对模型的表现至关重要。
-
n_estimators:集成中基础估计器的数量。
-
max_samples:从 X 中选择用于训练每个基础估计器的样本数量(如果为“auto”,则
max_samples = min(256, n_samples))。 -
contamination:数据集的污染程度,即数据集中异常值的比例。用于拟合时定义样本得分的阈值。
-
max_features:从 X 中选择用于训练每个基础估计器的特征数量。
#set isolation forest model and fit to the sales
model = IsolationForest(n_estimators = 100, max_samples = 'auto', contamination = float(0.1), max_features = 1.0)
model.fit(revenue[['Sales']])
接下来,让我们使用模型来显示异常及其异常得分。异常得分是基础估计器中每个观察值的常态性均值衡量。得分越低,观察值越异常。负分表示异常值,正分表示正常值。
#add anomaly scores and prediction
revenue['scores'] = model.decision_function(revenue[['Sales']])
revenue['anomaly'] = model.predict(revenue[['Sales']])

图片来自作者
最后,让我们展示之前的相同折线图,但用 plt.scatter 突出显示异常值。

图片来自作者
模型似乎表现不错。由于数据在月度间波动较大,一个担忧是可能会将正常值标记为异常,但由于模型的自助采样,这种情况并未发生。异常值似乎是销售额偏离趋势较“显著”的较大波动。
然而,了解数据在这里非常重要,因为一些异常应该附带警告。我们来看第一个(2015 年 2 月)和最后一个(2018 年 11 月)检测到的异常。首先,我们可以看到它们都与平均值存在较大的波动。
然而,第一个异常(2015 年 2 月)只是我们记录销售的第二个月,且业务可能刚刚开始运营。销售额肯定很低,接下来一个月出现了大幅上涨。但仅仅因为销售额低就将业务的第二个月标记为异常,是否合理?还是这对一个新企业来说是正常现象?
对于我们最后一个异常(2018 年 11 月),我们看到销售量出现了巨大的波动,似乎偏离了整体趋势。然而,我们已经没有更多的数据。随着数据的持续记录,这可能并不是一个异常,而可能是一个识别出更陡峭上升趋势的信号。
结论
总之,异常检测是强有力的数据治理和严格分析中不可或缺的能力。尽管在大规模数据中检测离群值和异常值可能很困难,但集成学习方法能够提供帮助,因为它们在处理大规模表格数据时既稳健又高效。
隔离森林模型通过使用一组“弱学习者”来隔离稀少且分散的观测值,从而检测这些异常。
希望你喜欢我的文章!欢迎发表评论、提问或提出其他话题的建议。
实体解析知识图谱
新词。旧概念。最终,这一切都是关于数据融合的。
·发表于Towards Data Science ·5 分钟阅读·2024 年 6 月 21 日
--
实体解析是一个过程。知识图谱是一个技术产物。二者结合产生了我们在知识表示和推理领域最强大的数据融合工具之一。最近,ERKG(实体解析知识图谱)已经进入数据架构的讨论,尤其是对于那些希望将给定领域的所有数据连接在一个地方进行调查的分析型组织。本文将详细解析实体解析知识图谱(ERKG)、ER、KG 以及它们实现的一些细节。
ER. 实体解析(也称为身份解析、数据匹配或记录链接)是通过计算过程将数据集中的实体去重和/或连接的过程。这可以像是解决数据库中两条记录,一条标为 Tom Riddle,另一条标为 T.M. Riddle 一样简单。或者,它也可以像一个人使用化名(伏地魔)、不同的电话号码和多个 IP 地址进行银行诈骗那样复杂。
KG. 知识图谱是一种知识表示形式,通过实体及其之间的关系以视觉形式展示数据。实体可以是人、公司、概念、物理资产、地理位置等。关系可以是信息交换、沟通、旅行、银行交易、计算交易等。实体和关系存储在图数据库中,预先连接,并以节点和边的形式可视化呈现。它看起来像这样……

作者图片
因此……
ERKG:一个包含多个数据集的知识图谱,其中的实体相互连接并去重。换句话说,没有重复的实体(例如,Tom Riddle 和 T.M. Riddle 的节点已经合并为一个节点)。此外,还发现了在某些可接受的概率阈值内,潜在相关节点之间的连接(例如,Tom Riddle、Lord Voldemort 和 Marvolo Riddle。此时你可能会问,“为什么你会创建一个来自多个数据源的、没有实体解析的知识图谱?”简单的答案是,“你不会。”话虽如此,如何解析实体的方法以及图谱表示技术使得创建 ERKG 成为一项艰巨的任务。
这是我们制作的第一个 ERKG。

图片由作者提供
早在 2016 年,我们将两个数据集导入图数据库:1)美国财政部外国资产控制办公室(OFAC)的国际制裁名单上的个人(蓝色),以及 2)一家匿名公司的客户(粉色)。显然,该公司的目的是通过图谱发现其客户中是否有国际制裁的个人,而无需手动搜索 OFAC 的数据库。尽管这个图谱所代表的 ER 过程可能有些过于复杂,但它的确具有说明性。
图谱中大多数已解析的实体是在同一个数据集内的两个到三个个体之间的关系(蓝到蓝或粉到粉)。这些很可能代表重复记录(例如我们之前提到的 Tom Riddle 与 T.M. Riddle 的问题)。在某些情况下,去重非常严格,比如图像顶部的粉色簇。在这里我们看到一个人被客户数据集中的 5 到 10 条记录表示。因此,至少可以看出,公司的客户数据需要进行去重处理。
有趣的是,在图像顶部我们看到的蓝到粉色的关系。这正是公司所寻找的:跨数据集的实体解析。它的几个客户可能是被国际制裁的个人。

图片由作者提供
这个例子相对简单,可能会导致人们错误地认为构建 ERKG 是一项简单的工作。事实远非如此。特别是当它需要跨多个 TB 数据和多个分析用户进行扩展时。
轻量级的自然语言处理(NLP)算法(如模糊匹配技术)足够简单,容易实现。这些算法可以轻松处理 Tom Riddle 与 T.M. Riddle 的问题。但当需要将两个以上的数据集结合在一起,可能还涉及多种语言和国际字符时,简单的 NLP 处理就变得相当复杂了。
对于更高级的分析问题,如反洗钱或银行欺诈,也需要更先进的 ER 解决方案。模糊匹配不足以识别那些故意隐藏身份、使用多个化名并试图规避制裁或其他法规的犯罪者。为此,ER 过程应包括基于机器学习的方法和更复杂的技术,考虑到姓名之外的附加元数据。这并不全是自然语言处理(NLP)。
关于基于图谱的 ER 与数据集级别的 ER 之间也存在很多争论。对于最高保真度的图谱分析,两者都是必需的。在将数据集导入图谱数据库时,在数据集内和跨越数据集解析实体,1) 可以最小化对图谱的大规模操作,从而降低计算开销,2) 确保图谱在创建之初只包含已解析的实体(无重复),这也为整体图谱架构节省了大量成本。
一旦存在实体解析的知识图谱,数据科学团队可以进一步通过基于图谱的 ER 技术探索更多的 ER。这些技术的附加好处是利用图谱拓扑结构(即图谱本身固有的结构)作为预测跨多个数据集潜在连接的特征。
ERKG 可以成为一个强大且直观的分析工具。它提供了:
-
将多个数据集融合成主图谱数据库
-
针对分析师探索的特定领域知识图谱的可视化表示
-
能够指定一个实时图谱模式,表示数据是如何连接和展示给分析师的
-
数据去重和数据集内外显式连接的可视化表示
-
跨数据集内外的潜在连接(预测链接),并能够控制预测的概率阈值
ERKG 因此成为一个分析画布,通过多个数据集呈现给定领域的生动互联探索。这是一种数据融合解决方案,而且是一个高度符合人类直觉的方案。
人工智能繁荣的环境影响
数字世界不能存在,没有自然资源来支撑它。我们用来构建和运行人工智能的技术代价是什么?
·发表于 Towards Data Science ·8 分钟阅读·2024 年 5 月 2 日
--

图片来源:ANGELA BENITO 在 Unsplash
在机器学习中有一个核心概念,我经常告诉外行人,以帮助澄清我所做工作的哲学。这个概念是:每个机器学习模型所处的世界在变化,往往是因为模型的存在,所以模型试图模仿和预测的世界总是过去的,而非现在或未来。从某种意义上说,模型是在预测未来——我们通常是这样看待它的——但在许多其他方面,模型实际上是在尝试将我们带回过去。
我喜欢谈论这个话题,因为机器学习的哲学帮助我们从机器学习实践者以及机器学习的用户和研究对象的角度,获得真实的视角。常读者会知道我常说“机器学习就是我们”——意思是,我们产生数据,进行训练,消费和应用模型的输出。模型试图遵循我们的指示,使用我们提供的原材料,而我们对这一过程的发生方式及其后果有着巨大的、几乎完全的控制权。
这个概念的另一个有用方面是提醒我们,模型并不是孤立存在于数字世界中,实际上它们与模拟的、物理的世界密切交织。毕竟,如果你的模型没有对我们周围的世界产生影响,那么就会引发一个问题:你的模型为什么存在?如果我们真的深入思考,数字世界和物理世界的区别仅仅在于我们作为用户/开发者如何与它互动,从某种有限的、人工的角度来看,数字世界与物理世界并不是完全分开的。
我今天想谈论的正是最后一点——物理世界如何塑造并影响机器学习,而机器学习/AI 又如何反过来影响物理世界?在我上一篇文章中,我承诺过会谈到物理世界资源的限制如何与机器学习和 AI 相交织,今天我们就要讨论这个问题。
AI 需要物理世界
如果稍加思考,这一点大概很明显。有一个笑话说,我们可以通过关掉智能机器人或拔掉电脑的电源来击败这些有知觉的机器人霸主。但笑话归笑话,这里面确实有一定的真理。我们这些从事机器学习、AI 以及一般计算工作的人员,完全依赖于自然资源的存在,比如矿产金属、电力等。这与我去年写的一篇关于人类劳动对于机器学习存在的重要性的文章有一些相似之处,但今天我们要走一条不同的道路,讨论两个我们应该更加重视的关键领域,这些领域对我们的工作至关重要——采矿/制造和能源,主要是电力形式的能源。
如果你出去寻找,关于这两个领域的研究和报道非常丰富,不仅与 AI 直接相关,还涉及到早期的技术热潮,比如加密货币,在资源使用方面与 AI 有许多相似之处。我将对每个领域进行一般性的讨论,并提供进一步阅读的参考文献,以便你可以深入探索细节,了解学术来源。然而,很难找到考虑到过去 18 个月 AI 热潮的研究,所以我预计一些研究低估了新技术在生成型 AI 领域的影响。
采矿与制造
制造 GPU 芯片需要什么材料?我们知道这些芯片在现代机器学习模型的发展中至关重要,而 Nvidia,今天这些芯片的最大生产商,凭借加密货币的繁荣和人工智能的热潮,已经跻身全球最有价值的公司之一。其股价从 2021 年初的每股 130 美元涨到 2024 年 4 月我写这句话时的每股 877.35 美元,使其市值达到了超过 2 万亿美元。 2023 年第三季度,他们售出了超过 50 万颗芯片,收入超过 100 亿美元。 估计他们 2023 年 H100 系列芯片的总销量为 150 万颗,,预计 2024 年将轻松超过这一数字。
GPU 芯片涉及多种不同的特殊原材料,这些原材料相对稀有且难以获得,包括钨、钯、钴和钽。 其他元素可能更容易获得,但具有显著的健康和安全风险,例如汞和铅。开采这些元素和化合物对环境有重大影响,包括排放和对采矿地区环境的破坏。即使是最好的采矿作业,也会严重改变生态系统。这还不包括所谓的“冲突矿产”的风险,即在人类剥削、童工或奴役的情况下开采的矿物。(应给予肯定:Nvidia 一直在公开避免使用这种矿产,特别提到刚果民主共和国。)
此外,在原材料被开采后,所有这些材料都必须经过极为仔细的加工,才能生产出运行复杂计算的微小、高效能芯片。正如我们从过去 150 多年的工业历史中所知,工人在处理重金属时面临着重大健康风险,例如铅和汞。Nvidia 的芯片大多是在台湾的工厂生产,这些工厂由一家名为台积电(TSMC)的公司运营。因为Nvidia 并不拥有或运营这些工厂,所以 Nvidia 能够避免因制造条件或排放而受到批评,而且数据也很难获取。制造所需的电力也不在 Nvidia 的账目上。顺便提一句:台积电的产能已达到最大,正在努力提升产能。与此同时,Nvidia 计划在明年开始与 Intel 合作,提升制造能力。
一旦芯片生产完成,它的使用寿命可能会相当长——如果保养得当,可能是 3 到 5 年——然而,Nvidia 不断生产新的、更强大、更高效的芯片(每年生产 200 万颗可不是个小数字!),因此芯片的使用寿命可能受到过时以及磨损的限制。当芯片不再有用时,它就会进入所谓的“电子废物”的处理流程。从理论上讲,芯片中许多稀有金属应当具有一定的回收价值,但正如你所料,芯片回收是一项非常专业且具有挑战性的技术任务,而且所有电子废物中,只有大约 20%能够被回收,其中包括手机等其他较为简单的硬件。回收过程还需要工人拆卸设备,这样他们就会再次接触到制造过程中使用的重金属和其他元素。
如果芯片没有被回收,另一方面,它很可能会被倾倒在垃圾填埋场或焚烧,重金属通过水、空气或两者渗入环境。这种情况发生在发展中国家,且常常直接影响到人们居住的地区。
然而,大多数关于机器学习碳足迹及其整体环境影响的研究,都集中在电力消耗方面。所以我们来看看这个方向。
电力
一旦我们具备了完成工作的硬件,AI 领域中最为显著的问题无疑是电力消耗。训练大型语言模型需要消耗巨量的电力,而部署和服务 LLM(大型语言模型)及其他先进的机器学习模型同样是一个电力黑洞。
在训练的情况下,一篇研究论文建议,训练拥有 1750 亿参数的 GPT-3 大约需要消耗1,300 兆瓦时(MWh)或 1,300,000 千瓦时(KWh)的电力。与此相比,GPT-4 使用了 1.76 万亿个参数,估计训练所需的电力消耗在51,772,500 到 62,318,750 千瓦时之间。作为参考,平均每个美国家庭每年使用的电力超过 10,000 千瓦时。因此,从保守估计来看,训练一次 GPT-4 的电力消耗足以为近 5,000 个美国家庭提供一年的电力。(这还没有考虑到在准备数据并为训练做准备时,几乎肯定需要进行的初步分析或测试所消耗的电力。)
鉴于 GPT-3 和 GPT-4 在训练过程中的电力使用量大约增加了 40 倍,我们必须关注这些模型的未来版本可能带来的电力消耗,以及用于训练生成视频、图像或音频内容的模型的电力消耗。
在训练过程之后,只有在模型的生命周期中进行一次的训练之外,推理任务的电力消耗也在快速增长,换句话说,每当你向 Chat-GPT 提问或尝试用 AI 工具生成有趣图像时,都会产生电力成本。这种电力由数据中心吸收,这些数据中心运行模型,提供全球范围的结果。国际能源署预测到 2026 年,仅数据中心的电力消耗就将达到 1,000 太瓦时,大致相当于日本的电力使用量。
AI 行业的主要参与者显然已经意识到,电力消耗的这种增长是不可持续的。有估计表明,数据中心消耗了全球电力使用量的 0.5%到 2%,并且到 2030 年,数据中心可能占美国电力使用量的25%。
美国的电力基础设施状况不佳——我们当然在尝试将更多可再生能源并入电网,但我们理应不是一个能够很好管理公共基础设施的国家。德克萨斯州居民尤其深知我们电力系统的脆弱性,但在美国范围内,气候变化带来的极端天气状况增加导致了电力中断,这种情况的发生频率正在不断上升。
电力基础设施的投资是否能够满足 AI 工具所带来的急剧增长的需求,尚需观察,而由于政府行动在实现这一目标中至关重要,因此持悲观态度是合理的。
与此同时,即使我们确实能以必要的速度生产电力,直到可再生和无排放的电力来源能够规模化使用之前,我们仍在通过使用这些 AI 工具显著增加全球的碳排放。粗略估计,每千瓦时电力排放 0.86 磅碳,训练 GPT-4 的过程中会向大气中排放超过 20,000 公吨的碳。(相比之下,普通美国人每年排放 13 公吨碳。)
好吧,那又怎么样呢?
正如你所预料的,我并不是在这里争论我们应该因为机器学习消耗自然资源而停止这项工作。我认为那些使我们生活成为可能的工人应当得到显著的工作安全保障和与风险相称的补偿,我也认为在面对可预防的、由人类引起的气候变化时,可再生电力来源应该是一个重要优先事项。
但我谈论这些,是因为了解我们的工作有多么依赖于物理世界、自然资源和地球,应该让我们更加谦逊,感恩我们所拥有的一切。当你进行训练或推理,或使用 Chat-GPT 或 Dall-E 时,你并不是这个过程的终点。你的行为会产生下游的后果,认识到这一点并据此做出明智的决策非常重要。你可能在租用别人 GPU 的几秒钟或几小时,但这仍然需要电力,并且会对 GPU 造成磨损,最终这些 GPU 会需要被处置。成为有伦理的世界公民的一部分,就是思考你的选择并考虑你对他人的影响。
此外,如果你有兴趣了解自己建模工作对碳足迹的影响,可以使用一个工具:www.green-algorithms.org/
阅读更多我的作品,请访问www.stephaniekirmer.com.
AlphaFold 3 与 GPT-4o 对蛋白质数据库条目知识的史诗级“交叉”
探索 GPT-4o 对蛋白质数据银行的知识如何与像 AlphaFold 3 这样的系统结合,为研究和探索生物分子结构提供新的方式。
LucianoSphere(Luciano Abriata 博士)
·发表于Towards Data Science ·12 分钟阅读·2024 年 12 月 17 日
--
如果你对生物信息学和生物学数据分析感兴趣,你会立即发现这篇文章非常具有启发性。
更广泛地说,对于人工智能科学家来说,他们将在这里找到通过推动大型语言模型(LLM)产生幻觉并寻找克服这一局限性的方法。
引言
蛋白质数据银行(PDB)作为生物大分子三维结构数据的综合库,提供了对生物过程分子基础的宝贵洞察。正是它的存在使得像 AlphaFold 这样的人工智能模型得以开发!
## 这里是我所有关于蛋白质建模、CASP 和 AlphaFold 2 的同行评审和博客文章
我在这里汇编了所有经过同行评审的文章(包括一些论文、几篇评论、一篇观点)以及关于…
用于估计潜伏期的 EpiLPS
探索如何使用{EpiLPS} R 软件包来估计各种疾病的潜伏期。了解其在流行病学研究中的应用、方法论和好处,以便更准确地预测和改进公共卫生规划。
·发表在走向数据科学 ·6 分钟阅读·2024 年 8 月 1 日
--
动机
哈塞尔特大学数据科学研究所(DSI)的一组研究人员开发了一种新的统计模型,基于粗略数据估计病原微生物的潜伏期。传染病的潜伏期(定义为感染和首次出现症状之间的时间间隔)非常重要,因为它可以揭示疾病的流行潜力,并优化隔离期的长度以阻止传播。最近在美国流行病学杂志上发表了文章(Gressani 等人,2024),该文章通过EpiLPS 软件包 (Gressani 等人,2022) 实现了该方法的实际应用。
粗略数据
首先,估计潜伏期时间如此具有挑战性的原因是什么?魔鬼在于数据。真实的感染时间是隐秘的,很少被观察到。在信息论术语中,这种现象被称为“信息不完全”,但统计学家更倾向于称之为截尾。更准确地说,感染时间是…
使用 GAN(生成对抗网络)去除卫星图像中的云
从零开始在 Python 中构建 GAN
·发布于 Towards Data Science ·12 分钟阅读·2024 年 6 月 15 日
--

图片由 Michael & Diane Weidner 提供,来源于 Unsplash
生成对抗网络(GAN)的概念由 Goodfellow 和他的同事们在 2014 年提出 [1],并且很快在计算机视觉和图像生成领域获得了极大关注。尽管在过去的 10 年里,人工智能领域经历了快速发展并出现了许多新算法,但这个概念的简单性和 brilliance(巧妙之处)依然令人印象深刻。所以今天我想通过尝试从卫星 RGB(红、绿、蓝)图像中去除云层,来展示这些网络的强大能力。
准备一个适当平衡、足够大且经过正确预处理的计算机视觉数据集需要大量时间,因此我决定探索 Kaggle 提供的资源。我发现最适合这个任务的数据集是 EuroSat [2],它具有开放许可。该数据集包含 27000 张标注的 RGB 图像,尺寸为 64x64 像素,来自 Sentinel-2,并用于解决多类分类问题。
数据集包含来自 Sentinel-2 的所有 RGB 和波段图像
EuroSat 数据集的图像示例。许可。
我们对分类本身并不感兴趣,但 EuroSat 数据集的一个主要特点是,所有图像都有清晰的天空。这正是我们需要的。借用[3]中的方法,我们将这些 Sentinel-2 影像作为目标,通过向它们添加噪声(云朵)来创建输入。
那么在真正讨论 GANs 之前,我们先准备一下数据。首先,我们需要下载数据,并将所有类别合并到一个目录中。
🐍完整的 Python 代码: GitHub.
import numpy as np
import pandas as pd
import random
from os import listdir, mkdir, rename
from os.path import join, exists
import shutil
import datetime
import matplotlib.pyplot as plt
from highlight_text import ax_text, fig_text
from PIL import Image
import warnings
warnings.filterwarnings('ignore')
classes = listdir('./EuroSat')
path_target = './EuroSat/all_targets'
path_input = './EuroSat/all_inputs'
"""RUN IT ONLY ONCE TO RENAME THE FILES IN THE UNPACKED ARCHIVE"""
mkdir(path_input)
mkdir(path_target)
k = 1
for kind in classes:
path = join('./EuroSat', str(kind))
for i, f in enumerate(listdir(path)):
shutil.copyfile(join(path, f),
join(path_target, f))
rename(join(path_target, f), join(path_target, f'{k}.jpg'))
k += 1
第二个重要步骤是生成噪声。虽然你可以使用不同的方法,例如随机遮罩一些像素、添加一些高斯噪声,但在这篇文章中,我想尝试一个对我来说新颖的东西——Perlin 噪声。它是由 Ken Perlin 在 80 年代发明的[4],用于开发电影中的烟雾效果。这种噪声与普通的随机噪声相比,具有更自然的外观。让我来证明一下。
def generate_perlin_noise(width, height, scale, octaves, persistence, lacunarity):
noise = np.zeros((height, width))
for i in range(height):
for j in range(width):
noise[i][j] = pnoise2(i / scale,
j / scale,
octaves=octaves,
persistence=persistence,
lacunarity=lacunarity,
repeatx=width,
repeaty=height,
base=0)
return noise
def normalize_noise(noise):
min_val = noise.min()
max_val = noise.max()
return (noise - min_val) / (max_val - min_val)
def generate_clouds(width, height, base_scale, octaves, persistence, lacunarity):
clouds = np.zeros((height, width))
for octave in range(1, octaves + 1):
scale = base_scale / octave
layer = generate_perlin_noise(width, height, scale, 1, persistence, lacunarity)
clouds += layer * (persistence ** octave)
clouds = normalize_noise(clouds)
return clouds
def overlay_clouds(image, clouds, alpha=0.5):
clouds_rgb = np.stack([clouds] * 3, axis=-1)
image = image.astype(float) / 255.0
clouds_rgb = clouds_rgb.astype(float)
blended = image * (1 - alpha) + clouds_rgb * alpha
blended = (blended * 255).astype(np.uint8)
return blended
width, height = 64, 64
octaves = 12 #number of noise layers combined
persistence = 0.5 #lower persistence reduces the amplitude of higher-frequency octaves
lacunarity = 2 #higher lacunarity increases the frequency of higher-frequency octaves
for i in range(len(listdir(path_target))):
base_scale = random.uniform(5,120) #noise frequency
alpha = random.uniform(0,1) #transparency
clouds = generate_clouds(width, height, base_scale, octaves, persistence, lacunarity)
img = np.asarray(Image.open(join(path_target, f'{i+1}.jpg')))
image = Image.fromarray(overlay_clouds(img,clouds, alpha))
image.save(join(path_input,f'{i+1}.jpg'))
print(f'Processed {i+1}/{len(listdir(path_target))}')
idx = np.random.randint(27000)
fig,ax = plt.subplots(1,2)
ax[0].imshow(np.asarray(Image.open(join(path_target, f'{idx}.jpg'))))
ax[1].imshow(np.asarray(Image.open(join(path_input, f'{idx}.jpg'))))
ax[0].set_title("Target")
ax[0].axis('off')
ax[1].set_title("Input")
ax[1].axis('off')
plt.show()

图片来源:作者。
如上所示,图像中的云朵非常逼真,它们具有不同的“密度”和类似真实云朵的纹理。
如果你像我一样对 Perlin 噪声感兴趣,这里有一个非常酷的视频,展示了这种噪声如何应用于游戏开发行业:
既然我们现在有了一个现成可用的数据集,那么让我们来谈谈生成对抗网络(GANs)。
生成对抗网络(GAN)
为了更好地说明这个概念,假设你正在东南亚旅行,突然需要一件连帽衫,因为外面太冷了。你来到最近的街头市场,发现一家小店有一些品牌服装。卖家拿来一件不错的连帽衫让你试穿,并说它是著名品牌 ExpensiveButNotWorthIt。你仔细一看,得出结论,这显然是假的。卖家说:“等一下,我有真的。”然后他带着另一件连帽衫回来,看起来更像是品牌的,但依旧是假货。经过几轮这样的尝试后,卖家带来了一件无法分辨的传奇品牌 ExpensiveButNotWorthIt 的复制品,你便高兴地买下了它。这基本上就是生成对抗网络(GANs)的工作原理!
在 GANs 的情况下,你被称为判别器(D)。判别器的目标是区分真实物体和虚假物体,或者解决二分类任务。卖家被称为生成器(G),因为他在尝试生成高质量的假货。判别器和生成器是独立训练的,目的是互相超越。因此,最终我们得到一个高质量的假货。

GANs 架构。许可。
训练过程最初看起来是这样的:
-
采样输入噪声(在我们的案例中是有云的图像)。
-
将噪声输入 G 并收集预测结果。
-
通过获取两个预测值来计算 D 的损失,一个是 G 的输出,另一个是真实数据的预测值。
-
更新 D 的权重。
-
再次采样输入噪声。
-
将噪声输入 G 并收集预测结果。
-
通过将 G 的预测输入到 D 中来计算 G 的损失。
-
更新 G 的权重。

GANs 训练循环。来源:[1]。
换句话说,我们可以定义一个值函数 V(G,D):

来源:[1]。
在这里,我们希望最小化项 log(1-D(G(z))) 来训练 G,并最大化 log D(x) 来训练 D(在此符号中,x 表示真实数据样本,z 表示噪声)。
现在让我们尝试在 pytorch 中实现它!
在原始论文中,作者提到使用多层感知器(MLP);它通常也简称为 ANN,但我想尝试一个更复杂的方法——我想使用 UNet [5] 架构作为生成器,ResNet [6] 作为判别器。这两者都是著名的 CNN 架构,所以我不会在这里解释它们(如果需要我写一篇单独的文章,请在评论中告诉我)。
让我们来构建它们。判别器:
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms
from torch.utils.data import Subset
class ResidualBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride = 1, downsample = None):
super(ResidualBlock, self).__init__()
self.conv1 = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size = 3, stride = stride, padding = 1),
nn.BatchNorm2d(out_channels),
nn.ReLU())
self.conv2 = nn.Sequential(
nn.Conv2d(out_channels, out_channels, kernel_size = 3, stride = 1, padding = 1),
nn.BatchNorm2d(out_channels))
self.downsample = downsample
self.relu = nn.ReLU()
self.out_channels = out_channels
def forward(self, x):
residual = x
out = self.conv1(x)
out = self.conv2(out)
if self.downsample:
residual = self.downsample(x)
out += residual
out = self.relu(out)
return out
class ResNet(nn.Module):
def __init__(self, block=ResidualBlock, all_connections=[3,4,6,3]):
super(ResNet, self).__init__()
self.inputs = 16
self.conv1 = nn.Sequential(
nn.Conv2d(3, 16, kernel_size = 3, stride = 1, padding = 1),
nn.BatchNorm2d(16),
nn.ReLU()) #16x64x64
self.maxpool = nn.MaxPool2d(kernel_size = 2, stride = 2) #16x32x32
self.layer0 = self.makeLayer(block, 16, all_connections[0], stride = 1) #connections = 3, shape: 16x32x32
self.layer1 = self.makeLayer(block, 32, all_connections[1], stride = 2)#connections = 4, shape: 32x16x16
self.layer2 = self.makeLayer(block, 128, all_connections[2], stride = 2)#connections = 6, shape: 1281x8x8
self.layer3 = self.makeLayer(block, 256, all_connections[3], stride = 2)#connections = 3, shape: 256x4x4
self.avgpool = nn.AvgPool2d(4, stride=1)
self.fc = nn.Linear(256, 1)
def makeLayer(self, block, outputs, connections, stride=1):
downsample = None
if stride != 1 or self.inputs != outputs:
downsample = nn.Sequential(
nn.Conv2d(self.inputs, outputs, kernel_size=1, stride=stride),
nn.BatchNorm2d(outputs),
)
layers = []
layers.append(block(self.inputs, outputs, stride, downsample))
self.inputs = outputs
for i in range(1, connections):
layers.append(block(self.inputs, outputs))
return nn.Sequential(*layers)
def forward(self, x):
x = self.conv1(x)
x = self.maxpool(x)
x = self.layer0(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.avgpool(x)
x = x.view(-1, 256)
x = self.fc(x).flatten()
return F.sigmoid(x)
生成器:
class DoubleConv(nn.Module):
def __init__(self, in_channels, out_channels):
super(DoubleConv, self).__init__()
self.double_conv = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True),
nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True)
)
def forward(self, x):
return self.double_conv(x)
class UNet(nn.Module):
def __init__(self):
super().__init__()
self.conv_1 = DoubleConv(3, 32) # 32x64x64
self.pool_1 = nn.MaxPool2d(kernel_size=2, stride=2) # 32x32x32
self.conv_2 = DoubleConv(32, 64) #64x32x32
self.pool_2 = nn.MaxPool2d(kernel_size=2, stride=2) #64x16x16
self.conv_3 = DoubleConv(64, 128) #128x16x16
self.pool_3 = nn.MaxPool2d(kernel_size=2, stride=2) #128x8x8
self.conv_4 = DoubleConv(128, 256) #256x8x8
self.pool_4 = nn.MaxPool2d(kernel_size=2, stride=2) #256x4x4
self.conv_5 = DoubleConv(256, 512) #512x2x2
#DECODER
self.upconv_1 = nn.ConvTranspose2d(512, 256, kernel_size=2, stride=2) #256x4x4
self.conv_6 = DoubleConv(512, 256) #256x4x4
self.upconv_2 = nn.ConvTranspose2d(256, 128, kernel_size=2, stride=2) #128x8x8
self.conv_7 = DoubleConv(256, 128) #128x8x8
self.upconv_3 = nn.ConvTranspose2d(128, 64, kernel_size=2, stride=2) #64x16x16
self.conv_8 = DoubleConv(128, 64) #64x16x16
self.upconv_4 = nn.ConvTranspose2d(64, 32, kernel_size=2, stride=2) #32x32x32
self.conv_9 = DoubleConv(64, 32) #32x32x32
self.output = nn.Conv2d(32, 3, kernel_size = 3, stride = 1, padding = 1) #3x64x64
def forward(self, batch):
conv_1_out = self.conv_1(batch)
conv_2_out = self.conv_2(self.pool_1(conv_1_out))
conv_3_out = self.conv_3(self.pool_2(conv_2_out))
conv_4_out = self.conv_4(self.pool_3(conv_3_out))
conv_5_out = self.conv_5(self.pool_4(conv_4_out))
conv_6_out = self.conv_6(torch.cat([self.upconv_1(conv_5_out), conv_4_out], dim=1))
conv_7_out = self.conv_7(torch.cat([self.upconv_2(conv_6_out), conv_3_out], dim=1))
conv_8_out = self.conv_8(torch.cat([self.upconv_3(conv_7_out), conv_2_out], dim=1))
conv_9_out = self.conv_9(torch.cat([self.upconv_4(conv_8_out), conv_1_out], dim=1))
output = self.output(conv_9_out)
return F.sigmoid(output)
现在我们需要将数据划分为训练集和测试集,并将它们封装为 torch 数据集:
class dataset(Dataset):
def __init__(self, batch_size, images_paths, targets, img_size = 64):
self.batch_size = batch_size
self.img_size = img_size
self.images_paths = images_paths
self.targets = targets
self.len = len(self.images_paths) // batch_size
self.transform = transforms.Compose([
transforms.ToTensor(),
])
self.batch_im = [self.images_paths[idx * self.batch_size:(idx + 1) * self.batch_size] for idx in range(self.len)]
self.batch_t = [self.targets[idx * self.batch_size:(idx + 1) * self.batch_size] for idx in range(self.len)]
def __getitem__(self, idx):
pred = torch.stack([
self.transform(Image.open(join(path_input,file_name)))
for file_name in self.batch_im[idx]
])
target = torch.stack([
self.transform(Image.open(join(path_target,file_name)))
for file_name in self.batch_im[idx]
])
return pred, target
def __len__(self):
return self.len
完美。是时候编写训练循环了。在此之前,让我们定义我们的损失函数和优化器:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
batch_size = 64
num_epochs = 15
learning_rate_D = 1e-5
learning_rate_G = 1e-4
discriminator = ResNet()
generator = UNet()
bce = nn.BCEWithLogitsLoss()
l1loss = nn.L1Loss()
optimizer_D = optim.Adam(discriminator.parameters(), lr=learning_rate_D)
optimizer_G = optim.Adam(generator.parameters(), lr=learning_rate_G)
scheduler_D = optim.lr_scheduler.StepLR(optimizer_D, step_size=10, gamma=0.1)
scheduler_G = optim.lr_scheduler.StepLR(optimizer_G, step_size=10, gamma=0.1)
如你所见,这些损失与 GAN 算法中的图片有所不同。特别是,我加入了 L1Loss。其想法是,我们不仅仅是从噪声中生成一张随机图片,我们还希望保留输入中的大部分信息,只去除噪声。所以 G 的损失将是:
G_loss = log(1 − D(G(z))) + 𝝀 |G(z)-y|
而不是仅仅
G_loss = log(1 − D(G(z)))
𝝀 是一个任意系数,用于平衡损失函数的两个部分。
最后,让我们划分数据并开始训练过程:
test_ratio, train_ratio = 0.3, 0.7
num_test = int(len(listdir(path_target))*test_ratio)
num_train = int((int(len(listdir(path_target)))-num_test))
img_size = (64, 64)
print("Number of train samples:", num_train)
print("Number of test samples:", num_test)
random.seed(231)
train_idxs = np.array(random.sample(range(num_test+num_train), num_train))
mask = np.ones(num_train+num_test, dtype=bool)
mask[train_idxs] = False
images = {}
features = random.sample(listdir(path_input),num_test+num_train)
targets = random.sample(listdir(path_target),num_test+num_train)
random.Random(231).shuffle(features)
random.Random(231).shuffle(targets)
train_input_img_paths = np.array(features)[train_idxs]
train_target_img_path = np.array(targets)[train_idxs]
test_input_img_paths = np.array(features)[mask]
test_target_img_path = np.array(targets)[mask]
train_loader = dataset(batch_size=batch_size, img_size=img_size, images_paths=train_input_img_paths, targets=train_target_img_path)
test_loader = dataset(batch_size=batch_size, img_size=img_size, images_paths=test_input_img_paths, targets=test_target_img_path)
现在我们可以运行我们的训练循环:
train_loss_G, train_loss_D, val_loss_G, val_loss_D = [], [], [], []
all_loss_G, all_loss_D = [], []
best_generator_epoch_val_loss, best_discriminator_epoch_val_loss = -np.inf, -np.inf
for epoch in range(num_epochs):
discriminator.train()
generator.train()
discriminator_epoch_loss, generator_epoch_loss = 0, 0
for inputs, targets in train_loader:
inputs, true = inputs, targets
'''1\. Training the Discriminator (ResNet)'''
optimizer_D.zero_grad()
fake = generator(inputs).detach()
pred_fake = discriminator(fake).to(device)
loss_fake = bce(pred_fake, torch.zeros(batch_size, device=device))
pred_real = discriminator(true).to(device)
loss_real = bce(pred_real, torch.ones(batch_size, device=device))
loss_D = (loss_fake+loss_real)/2
loss_D.backward()
optimizer_D.step()
discriminator_epoch_loss += loss_D.item()
all_loss_D.append(loss_D.item())
'''2\. Training the Generator (UNet)'''
optimizer_G.zero_grad()
fake = generator(inputs)
pred_fake = discriminator(fake).to(device)
loss_G_bce = bce(pred_fake, torch.ones_like(pred_fake, device=device))
loss_G_l1 = l1loss(fake, targets)*100
loss_G = loss_G_bce + loss_G_l1
loss_G.backward()
optimizer_G.step()
generator_epoch_loss += loss_G.item()
all_loss_G.append(loss_G.item())
discriminator_epoch_loss /= len(train_loader)
generator_epoch_loss /= len(train_loader)
train_loss_D.append(discriminator_epoch_loss)
train_loss_G.append(generator_epoch_loss)
discriminator.eval()
generator.eval()
discriminator_epoch_val_loss, generator_epoch_val_loss = 0, 0
with torch.no_grad():
for inputs, targets in test_loader:
inputs, targets = inputs, targets
fake = generator(inputs)
pred = discriminator(fake).to(device)
loss_G_bce = bce(fake, torch.ones_like(fake, device=device))
loss_G_l1 = l1loss(fake, targets)*100
loss_G = loss_G_bce + loss_G_l1
loss_D = bce(pred.to(device), torch.zeros(batch_size, device=device))
discriminator_epoch_val_loss += loss_D.item()
generator_epoch_val_loss += loss_G.item()
discriminator_epoch_val_loss /= len(test_loader)
generator_epoch_val_loss /= len(test_loader)
val_loss_D.append(discriminator_epoch_val_loss)
val_loss_G.append(generator_epoch_val_loss)
print(f"------Epoch [{epoch+1}/{num_epochs}]------\nTrain Loss D: {discriminator_epoch_loss:.4f}, Val Loss D: {discriminator_epoch_val_loss:.4f}")
print(f'Train Loss G: {generator_epoch_loss:.4f}, Val Loss G: {generator_epoch_val_loss:.4f}')
if discriminator_epoch_val_loss > best_discriminator_epoch_val_loss:
discriminator_epoch_val_loss = best_discriminator_epoch_val_loss
torch.save(discriminator.state_dict(), "discriminator.pth")
if generator_epoch_val_loss > best_generator_epoch_val_loss:
generator_epoch_val_loss = best_generator_epoch_val_loss
torch.save(generator.state_dict(), "generator.pth")
#scheduler_D.step()
#scheduler_G.step()
fig, ax = plt.subplots(1,3)
ax[0].imshow(np.transpose(inputs.numpy()[7], (1,2,0)))
ax[1].imshow(np.transpose(targets.numpy()[7], (1,2,0)))
ax[2].imshow(np.transpose(fake.detach().numpy()[7], (1,2,0)))
plt.show()
代码完成后,我们可以绘制损失图。此代码部分来源于这个酷网站:
from matplotlib.font_manager import FontProperties
background_color = '#001219'
font = FontProperties(fname='LexendDeca-VariableFont_wght.ttf')
fig, ax = plt.subplots(1, 2, figsize=(16, 9))
fig.set_facecolor(background_color)
ax[0].set_facecolor(background_color)
ax[1].set_facecolor(background_color)
ax[0].plot(range(len(all_loss_G)), all_loss_G, color='#bc6c25', lw=0.5)
ax[1].plot(range(len(all_loss_D)), all_loss_D, color='#00b4d8', lw=0.5)
ax[0].scatter(
[np.array(all_loss_G).argmax(), np.array(all_loss_G).argmin()],
[np.array(all_loss_G).max(), np.array(all_loss_G).min()],
s=30, color='#bc6c25',
)
ax[1].scatter(
[np.array(all_loss_D).argmax(), np.array(all_loss_D).argmin()],
[np.array(all_loss_D).max(), np.array(all_loss_D).min()],
s=30, color='#00b4d8',
)
ax_text(
np.array(all_loss_G).argmax()+60, np.array(all_loss_G).max()+0.1,
f'{round(np.array(all_loss_G).max(),1)}',
fontsize=13, color='#bc6c25',
font=font,
ax=ax[0]
)
ax_text(
np.array(all_loss_G).argmin()+60, np.array(all_loss_G).min()-0.1,
f'{round(np.array(all_loss_G).min(),1)}',
fontsize=13, color='#bc6c25',
font=font,
ax=ax[0]
)
ax_text(
np.array(all_loss_D).argmax()+60, np.array(all_loss_D).max()+0.01,
f'{round(np.array(all_loss_D).max(),1)}',
fontsize=13, color='#00b4d8',
font=font,
ax=ax[1]
)
ax_text(
np.array(all_loss_D).argmin()+60, np.array(all_loss_D).min()-0.005,
f'{round(np.array(all_loss_D).min(),1)}',
fontsize=13, color='#00b4d8',
font=font,
ax=ax[1]
)
for i in range(2):
ax[i].tick_params(axis='x', colors='white')
ax[i].tick_params(axis='y', colors='white')
ax[i].spines['left'].set_color('white')
ax[i].spines['bottom'].set_color('white')
ax[i].set_xlabel('Epoch', color='white', fontproperties=font, fontsize=13)
ax[i].set_ylabel('Loss', color='white', fontproperties=font, fontsize=13)
ax[0].set_title('Generator', color='white', fontproperties=font, fontsize=18)
ax[1].set_title('Discriminator', color='white', fontproperties=font, fontsize=18)
plt.savefig('Loss.jpg')
plt.show()
# ax[0].set_axis_off()
# ax[1].set_axis_off()

图片来源:作者。
同时也可以可视化来自测试数据集的随机样本:
random.Random(2).shuffle(test_target_img_path)
random.Random(2).shuffle(test_input_img_paths)
subset_loader = dataset(batch_size=5, img_size=img_size, images_paths=test_input_img_paths,
targets=test_target_img_path)
generator = UNet()
generator.load_state_dict(torch.load('generator.pth'))
generator.eval()
for X, y in subset_loader:
fig, axes = plt.subplots(5, 3, figsize=(9, 9))
for i in range(5):
axes[i, 0].imshow(np.transpose(X.numpy()[i], (1, 2, 0)))
axes[i, 0].set_title("Input")
axes[i, 0].axis('off')
axes[i, 1].imshow(np.transpose(y.numpy()[i], (1, 2, 0)))
axes[i, 1].set_title("Target")
axes[i, 1].axis('off')
generated_image = generator(X[i].unsqueeze(0)).detach().numpy()[0]
axes[i, 2].imshow(np.transpose(generated_image, (1, 2, 0)))
axes[i, 2].set_title("Generated")
axes[i, 2].axis('off')
# Adjust layout
plt.tight_layout()
plt.savefig('Test.jpg')
plt.show()
break

图片来源:作者。
如你所见,结果并不完美,并且很大程度上依赖于地貌类型。然而,构建的模型肯定能够去除图像中的云层,并且通过增加 G 和 D 的深度可以提高其性能。另一个有前景的策略是为不同的地貌类型训练独立的模型。例如,农田和水域的空间特征差异较大,这可能会影响模型的泛化能力。
我希望这篇文章能为你提供一种在地理空间领域应用深度学习算法的新视角。在我看来,生成对抗网络(GANs)是数据科学家可以利用的最强大工具之一,我希望它们也能成为你工具箱中的重要组成部分!
===========================================
参考文献:
1. Goodfellow, Ian, Jean Pouget-Abadie, Mehdi Mirza, Bing Xu, David Warde-Farley, Sherjil Ozair, Aaron Courville 和 Yoshua Bengio。“生成对抗网络。” 神经信息处理系统进展 27(2014 年)。proceedings.neurips.cc/paper_files/paper/2014/file/5ca3e9b122f61f8f06494c97b1afccf3-Paper.pdf
2. Helber, Patrick, Benjamin Bischke, Andreas Dengel 和 Damian Borth。“Eurosat:一个用于土地利用和土地覆盖分类的全新数据集和深度学习基准。” IEEE 应用地球观测与遥感精选主题期刊 12 卷,第 7 期(2019 年):2217–2226。arxiv.org/pdf/1709.00029
3. Wen, Xue, Zongxu Pan, Yuxin Hu 和 Jiayin Liu。“基于 YUV 颜色空间的生成对抗学习用于卫星图像中的薄云去除。” 遥感 13 卷,第 6 期(2021 年):1079。www.mdpi.com/2072-4292/13/6/1079
4. Perlin, Ken。“图像合成器。” ACM Siggraph 计算机图形学 19 卷,第 3 期(1985 年):287–296。dl.acm.org/doi/pdf/10.1145/325165.325247
5. Ronneberger, Olaf, Philipp Fischer 和 Thomas Brox。“U-net:用于生物医学图像分割的卷积网络。” 见 医学图像计算与计算机辅助干预–MICCAI 2015:第 18 届国际会议,德国慕尼黑,2015 年 10 月 5 日至 9 日,会议录,第三部分 18,第 234–241 页。施普林格国际出版公司,2015 年。arxiv.org/pdf/1505.04597
6. He, Kaiming 等人。“深度残差学习用于图像识别。” IEEE 计算机视觉与模式识别会议论文集。2016。openaccess.thecvf.com/content_cvpr_2016/papers/He_Deep_Residual_Learning_CVPR_2016_paper.pdf
===========================================
我在 Medium 上的所有出版物都是免费的并且开放访问的,因此如果你在这里关注我,我将非常感激!
P.s. 我对(地理)数据科学、机器学习/人工智能和气候变化充满热情。如果你想合作进行某些项目,请在LinkedIn上联系我。
🛰️关注以获取更多信息🛰️
设置新 Apple M3 MacBook Pro 的必备清单
一本关于迁移书签、终端增强和 AWS CLI 设置的实用参考
·发表于Towards Data Science·8 分钟阅读·2024 年 2 月 10 日
--

图片由作者使用 MidJourney 生成
我最近收到了一台配有最新 Apple M3 芯片的 16 英寸 MacBook Pro 作为我的工作电脑。我听说过 Apple M1 和 M2 芯片的高速表现,所以我非常激动地拿到了这台搭载 M3 芯片的机器。
在这篇博客中,我将介绍我为顺利过渡到工作流程所做的设备配置步骤。
我的主要目标是:
-
通过导出 Chrome 书签和 1Password,方便地访问常用链接和登录信息。
-
使用
iTerm2、Oh My Zsh和PowerLevel10K 主题自定义一个美观的终端环境 -
使用
PyCharm和AWS Cli正确运行我的工作项目仓库。
如果你是数据科学家、Python 开发者,或是任何每天使用终端和 AWS CLI 的人,那么在设置新 MacBook 时,特别是从 Intel 芯片过渡到 Apple Silicon 芯片的设备时,你可能会发现本指南非常有帮助。
1. 导出 Chrome 书签
实施机器学习的关键考虑事项
从传统机器学习和生产化的角度来看,你的使用案例是否是一个可行的机器学习产品?
·发布于 Towards Data Science ·7 分钟阅读·2024 年 7 月 13 日
--

图片来源:Tara Winstead 于 Pixels
你是否曾经考虑过构建一个数据应用程序,但不知道构建机器学习系统的需求?或者,也许你是公司的一位高级经理,计划使用机器学习,但不确定你的使用案例是否适合机器学习。
很多企业正在努力跟上 AI/ML 技术的指数级增长,许多人意识到,如果他们不在发展路线图中考虑 AI/ML,可能会面临生死存亡的局面。
企业们看到了大型语言模型(LLM)的潜力,认为 AI/ML 是解决问题的“万能钥匙”。大多数企业正在投入资金建立新的数据团队、购买计算资源和最新的数据库技术,但他们是否知道他们的问题能通过机器学习解决呢?
我已经提炼出一个检查清单,来验证你的机器学习想法是否在传统机器学习视角下是可行的,包括:
***1. 你是否拥有适合预测的特征?
2. 你的数据中是否有可以学习的模式?
3. 你是否拥有足够的数据使机器学习有效,或者你能从其他来源收集数据吗?
4. 你的使用案例是否可以框定为一个预测问题?
5. 你希望预测的数据是否与训练数据有相关模式?***
从生产化机器学习解决方案的角度来看:
***1. 你的使用案例是否具有重复性?
2. 错误的预测会对最终用户产生严重后果吗?
3. 你的使用案例是否具备可扩展性?
4. 你的使用案例是一个模式不断演变的问题吗?***
传统考虑
亚瑟·塞缪尔(Arthur Samuel)在 1959 年首次推广了“机器学习”这一术语,并表示它是“赋予计算机在不被明确编程的情况下学习的能力的研究领域”。
Chip Huyen(人工智能/机器学习领域的领军人物和企业家)在她的书《设计机器学习系统》中提供了机器学习的一个更系统的定义——这本书是任何有意从事生产性机器学习的人必读的:
“机器学习是一种方法,(1) 从(2) 现有数据中学习(3) 复杂模式,并使用这些模式来对(4) 未见过的数据进行(5) 预测。”
Chip 将机器学习的组成部分分为五个部分,并通过包含机器学习采用的四个现代原因来进一步扩展它们,我们将在下面深入分析这些原因。
学习机会
你是否具备适当的特征来进行预测?
数据是机器学习的基础。它提供了输入和输出,产生反映数据模式的预测。
例如,你可能是一个狂热的足球迷,你想要根据过去的表现预测英超球员的市场价值
输入数据将包括球员的统计数据,如进球和助攻,以及相关的球员价值。一个机器学习模型可以从这些输入数据中学习模式,以预测未见过的球员数据。
复杂模式
你的数据中是否有可供学习的模式?
当数据复杂且人类无法轻松识别出预测输出所需的模式时,机器学习的效果最好。
在足球球员市场价值的例子中,考虑到球员的价值取决于许多变量,准确地评估一名球员的价值可能会很困难。机器学习模型可以将价值(输出)和表现统计数据(输入)结合起来,自动计算出评估结果。
数据可用性
你是否拥有足够的数据使机器学习有效,或者你能从其他来源收集数据吗?
关于数据还是更好的算法能带来更强的预测能力,存在着持续的争论。尽管,随着大型语言模型(LLMs)在数据集规模扩展到数百亿甚至万亿参数后取得的巨大性能飞跃,这场辩论最近已经有所平息。

数据来源:维基百科
数据需要为你的机器学习应用程序提供充足的学习资源。如果数据稀缺,那么机器学习可能不是最佳的解决方案。
在足球领域,数据供应商如Opta、Fbref 和Transfermarkt持续生成球员表现数据,因为各个俱乐部在各个方面(从球员表现到招聘)都希望通过数据驱动的决策来提高效率。
然而,从像 Opta 这样的第三方获取数据非常昂贵,因为数据收集过程非常密集,而且对详细统计数据的需求很高,以便为团队提供竞争优势。
通过预测解决的问题
你的使用案例能否被框定为预测问题?
我们可以从多种方式将足球运动员市场价值的例子框定为预测问题。
机器学习预测的两种常见类型是回归和分类。回归返回一个连续的预测(即数字),其规模与输入变量相同(即价值)。而分类可以返回二元(1 或 0)、多分类(1, 2, 3…n)或多标签(1, 0, 1, 0, 1)预测。
玩家价值预测问题可以被框定为回归和多分类问题。回归只是返回一个数字,例如根据赛季表现预测裘德·贝林厄姆的价值为 1 亿英镑。
相反,如果我们将其视为分类问题,我们可以将估值分入不同的类别,并预测一个玩家属于哪个估值类别。例如,预测类别可以是£1m-£10m,£10m-£30m,以及£30m+。
相似的未见数据
你希望预测的数据与训练数据之间是否存在相关模式?
你希望预测的未见数据必须与用于训练机器学习模型的数据具有相似的模式。
例如,如果我使用 2004 年的球员数据来训练一个机器学习模型以预测球员的估值。如果未见数据来自 2020 年,那么预测将无法反映训练到预测过程中 16 年间市场估值的变化。
生产考虑因素
机器学习模型开发只是一个更大系统中的一小部分,这个系统是为了让机器学习发挥作用所必需的。
如果你在没有理解模型如何在大规模上表现的情况下单独构建模型,那么在生产环境中,你可能会发现模型不可行。
你的机器学习使用案例必须能够满足生产级别的标准。
重复任务
你的使用案例是否具有重复性?
机器学习需要通过重复模式来进行学习。模型需要大量的样本来充分学习这些模式,这意味着如果你的预测目标发生得比较频繁,那么你很可能会有足够的数据供机器学习去学习这些模式。
例如,如果你的使用案例涉及预测一些罕见的事件,比如一种不常见的医疗状况,那么你的数据中可能没有足够的信号供机器学习模型捕捉,从而导致预测不准确。
这个问题被称为类别不平衡,为了克服这个问题,已经开发出了过采样和欠采样等策略。
Travis Tang 的文章很好地解释了类别不平衡以及解决方法的更多细节,可以参考这里。
错误预测的后果较小
错误预测是否会对最终用户产生严重后果?
机器学习模型每次都无法做到 100%的预测准确率,这意味着当模型做出错误预测时,它会产生负面影响吗?
这是医疗领域中常见的问题,虚假阳性和虚假阴性率是其中的关注点。
虚假阳性预测表示存在某种状况,但该状况并不存在。这可能导致资源的低效分配,并给患者带来不必要的压力。
甚至更糟的是,虚假阴性未能发现存在的状况,导致实际存在的状况未被察觉。这可能导致患者误诊和治疗延误,进而引发医疗并发症,并增加治疗更严重病情的长期成本。
规模
你的应用案例是否具有可扩展性?
生产成本可能非常昂贵,我自己就遇到过这种情况,当时我在谷歌的Vertex AI上托管了一个XGBRegressor模型,2 天花费了我 11 英镑!诚然,我不应该让它一直运行,但想象一下大规模应用的成本。
一个广为人知的可扩展机器学习解决方案例子是亚马逊的产品推荐系统,它创造了公司 35%的收入。
尽管这是一个极端的例子,但这个系统利用并证明了计算能力、数据、基础设施和人才的成本,展示了构建可扩展的机器学习解决方案的基本原理,能够创造价值。
发展中的模式
你的应用案例是否是一个模式不断变化的问题?
机器学习足够灵活,可以轻松适应新模式,避免每次数据变化时都需要不断地硬编码新解决方案。
足球运动员的价值会随着战术的演变而不断变化,导致球队对球员的需求发生变化,意味着在预测价值时特征的权重也会发生变化。
为了监控变化,像 Mlflow和Weights & Biases这样的工具有助于跟踪和记录模型的表现,并根据不断变化的数据模式更新它们。
结论
决定是否使用机器学习应考虑的不仅仅是使用你现有的历史数据,套用一个花哨的算法并期待结果。
这需要思考复杂的模式,特别是当你有现有数据和未来数据时,以及生产问题,比如错误预测的成本是否便宜?我的应用案例是否具有可扩展性?模式是否在不断演变?
你不应该使用机器学习的原因有很多,包括伦理问题、成本效益以及是否有更简单的解决方案,但这些可以留到下次再讨论。
目前就这些!
感谢阅读!如果我遗漏了什么,请告诉我,我也很愿意听听大家关于机器学习应用的案例!
在LinkedIn上与我联系
参考文献
Huyen, C. (2022). 《设计机器学习系统》。Sebastopol, CA: O’Reilly
Geron, A. (2019). 《动手实践机器学习:使用 Scikit-Learn、Keras 和 TensorFlow 构建智能系统的概念、工具和技术(第 2 版)》。O’Reilly.
连续排名概率分数(CRPS)用于预测的基本指南

图像由 Midjourney 生成
学习如何评估概率预测,以及 CRPS 如何与其他指标相关
·发布于 Towards Data Science ·7 分钟阅读·2024 年 8 月 31 日
--
如果我问你如何评估回归问题,你可能会列举出很多评估指标,比如 MSE、MAE、RMSE、MAPE 等。这些指标的共同点是它们都关注点预测。
当我们想要训练模型关注预测分布而不是单一的点时,情况会有所不同。在这种情况下,我们需要使用不同的指标,而这些指标在数据科学博客文章中不常见。
上次,我研究了分位数损失(即弹球损失)。这次,我将带你了解另一种用于评估概率预测的指标——连续排名概率分数(CRPS)。
下面是一些定义,帮助我们入门
第一个概念很简单,但我们仍然需要确保我们在同一页面上。概率预测提供了可能结果的分布。例如,点预测可能会预测明天的温度准确为 23°C,而概率模型可能会预测明天温度有 70% 的概率会在 20°C 到 25°C 之间。
估计未观测的:使用最大似然法在 Python 中估计移动平均模型
如何使用最大似然估计(MLE)估计未观测协变量的系数
·发布于Towards Data Science ·阅读时间:8 分钟·2024 年 6 月 28 日
--

图片由Connor Naasz提供,来自Unsplash
对于有时间序列数据和预测经验的人来说,回归、AR、MA 和 ARMA 等术语应该不陌生。线性回归是一个简单的模型,具有封闭形式的参数解,可以通过最小二乘法(OLS)获得。AR 模型也可以通过 OLS 进行估计。然而,MA 模型的情况更为复杂,它构成了更高级的 ARMA 和 ARIMA 模型的第二个组成部分。
本文计划:
-
介绍移动平均模型
-
讨论为何 MA 模型没有封闭解
-
介绍最大似然估计方法
-
MA(1) 最大似然估计——理论与 Python 代码
移动平均模型
MA 模型可以通过以下公式描述:

方程 1:MA(q) 公式
这里,θ(theta)表示模型参数,而ε(epsilon)是误差项,假定它们是互相独立且服从常方差的正态分布。这个公式背后的直觉是,我们的时间序列 X 总是可以通过系列中最后 q 个冲击来描述。从公式可以明显看出,每个冲击只影响后续的 q 个 X 值,这与 AR 模型不同,在 AR 模型中,冲击的影响会持续下去,尽管会随着时间逐渐减弱。
简单模型的封闭估计——线性回归
提醒一下,线性回归方程的一般形式如下:

方程 2:一般线性回归公式
对于预测任务,我们通常的目标是使用一组 x 和 y 的样本来估计所有模型的参数(beta)。根据我们对模型的一些假设,高斯-马尔科夫定理指出,普通最小二乘法(OLS)对 betas 的估计具有在所有线性无偏估计量中最低的抽样方差。简单来说,OLS 为我们提供了 betas 的最佳估计。
那么,OLS 是什么?它是一个闭式解,用于损失函数最小化问题:

方程 3:OLS 最小化方程
其中,损失函数 S 定义如下 -

方程 4:OLS 损失函数
在这个背景下,y 和 X 是我们的样本数据,是可观察的数字向量(如时间序列)。因此,计算函数 S,求出其导数,并找到解决最小化问题的 beta 是直接的。
MA(q)的闭式估计
应该很清楚,为什么像 OLS 这样的估计方法应用于 MA(q)模型是有问题的——因变量,即时间序列值,是由不可观察的变量(epsilons)描述的。这就引出了一个问题:这些模型究竟如何进行估计呢?
最大似然估计(MLE)
似然函数
统计分布通常依赖于一个或多个参数。例如,正态分布由其均值和方差来表征,这些参数定义了它的“高度”和“质心”—

正态分布来自于维基百科
假设我们有一个数据集 X={x_1,…x_n},由从一个未知的正态分布中抽取的样本组成,且其参数未知。我们的目标是确定那些最能描述我们数据集 X 的正态分布的均值和方差值,这些均值和方差值使得我们的数据集 X 最可能是从该正态分布中抽样得到的。
最大似然估计(MLE)提供了一个框架,可以精确解决这个问题。它引入了一个似然函数,这是一个输出另一个函数的函数。这个似然函数接受一个参数向量,通常用 theta 表示,并生成一个依赖于 theta 的概率密度函数(PDF)。

似然函数的一般定义
一个分布的概率密度函数(PDF)是一个函数,它接受一个值 x,并返回该值在分布中的概率。因此,似然函数通常表示如下:

给定 x,似然作为 theta 的函数
该函数的值表示从由 PDF 定义的分布中,观察到 x 的似然性,假设 theta 是其参数。
目标
构建预测模型时,我们有数据样本和一个参数化的模型,我们的目标是估计模型的参数。在我们的例子中,例如回归模型和 MA 模型,这些参数是各自模型公式中的系数。

统计模型估计过程
MLE 中的等效概念是,我们有观察值和一个定义在一组未知且不可直接观察的参数 theta 上的分布的 PDF。我们的目标是估计 theta。
MLE 方法包括寻找一组参数 theta,使得在给定可观察数据 x 的情况下,似然函数达到最大值。

似然函数的最大化
我们假设我们的样本 x 是从一个已知 PDF 的分布中抽取的,该 PDF 依赖于一组参数 theta。这意味着,在该 PDF 下观察到 x 的似然性(概率)本质上是 1。因此,识别使得我们的似然函数值在样本上接近 1 的 theta 值,应该揭示出真实的参数值。
条件似然
请注意,我们并没有对似然函数所依据的分布(PDF)做出任何假设。现在,假设我们的观察值 X 是一个向量(x_1, x_2, …, x_n)。我们将考虑一个概率函数,表示在已经观察到(x_1, x_2, …, x_{n-1})的条件下,观察到 x_n 的概率——

这表示在已知前面观察值的条件下,仅观察 x_n 的似然性(以及 theta,参数集合)。现在,我们将条件似然函数定义如下:

条件似然函数
稍后我们将看到,为什么使用条件似然函数而不是精确似然函数是有用的。
对数似然
在实际应用中,通常使用似然函数的自然对数,称为对数似然函数:

最大化对数似然函数
这样更方便,因为我们通常处理的似然函数是一个独立变量的联合概率函数,这等同于每个变量概率的乘积。取对数后,将这个乘积转换为和。
使用 MLE 估计 MA(1)
为了简化,我将演示如何估计最基本的移动平均模型——MA(1):

MA(1)模型
这里,x_t 表示时间序列观测值,alpha 和 beta 是待估计的模型参数,epsilon 是从均值为零且方差为 sigma 的正态分布中抽取的随机噪声,sigma 也将被估计。因此,我们的“theta”是(alpha, beta, sigma),我们要估计的正是这些参数。
让我们定义我们的参数并使用 Python 生成一些合成数据:
import pandas as pd
import numpy as np
STD = 3.3
MEAN = 0
ALPHA = 18
BETA = 0.7
N = 1000
df = pd.DataFrame({"et": np.random.normal(loc=MEAN, scale=STD, size=N)})
df["et-1"] = df["et"].shift(1, fill_value=0)
df["xt"] = ALPHA + (BETA*df["et-1"]) + df["et"]
请注意,我们已将误差分布的标准差设置为 3.3,alpha 设置为 18,beta 设置为 0.7。数据大致如下所示 —

MA(1) 数据生成过程的模拟
MA(1) 的似然函数
我们的目标是构建一个似然函数来回答这个问题:假设我们的时间序列 X=(x_1, …, x_n) 是由前面描述的 MA(1) 过程生成的,那么观察到这些数据的可能性有多大?

观察 X 的似然
计算这个概率的挑战在于我们样本之间的相互依赖 —— 这从 x_t 和 x_{t-1} 都依赖于 e_{t-1} 的事实中可以看出 —— 使得计算所有样本的联合概率(即精确似然)变得非同寻常。
因此,如前所述,我们将不计算精确似然,而是使用条件似然。让我们从给定所有前述样本的情况下观察单个样本的似然开始:

给定其余样本,观察 x_n 的条件似然
这计算起来要简单得多,因为 —


正态分布的概率密度函数
剩下的就是计算观察所有样本的条件似然:

应用自然对数得:

最终的似然函数需要最大化
这是我们应该最大化的函数。
代码
我们将使用来自 statsmodels 的 GenericLikelihoodModel 类来实现我们的最大似然估计(MLE)。如 statsmodels 网站上的教程所述,我们只需继承这个类并包括我们的似然函数计算:
from scipy import stats
from statsmodels.base.model import GenericLikelihoodModel
import statsmodels.api as sm
class MovingAverageMLE(GenericLikelihoodModel):
def initialize(self):
super().initialize()
extra_params_names = ['beta', 'std']
self._set_extra_params_names(extra_params_names)
self.start_params = np.array([0.1, 0.1, 0.1])
def calc_conditional_et(self, intercept, beta):
df = pd.DataFrame({"xt": self.endog})
ets = [0.0]
for i in range(1, len(df)):
ets.append(df.iloc[i]["xt"] - intercept - (beta*ets[i-1]))
return ets
def loglike(self, params):
ets = self.calc_conditional_et(params[0], params[1])
return stats.norm.logpdf(
ets,
scale=params[2],
).sum()
函数 loglike 是实现的关键。给定迭代的参数值 params 和依赖变量(在此为时间序列样本),这些变量作为类成员 self.endog 存储,它计算条件对数似然值,正如我们之前讨论的那样。
现在让我们创建模型并拟合我们的模拟数据:
df = sm.add_constant(df) # add intercept for estimation (alpha)
model = MovingAverageMLE(df["xt"], df["const"])
r = model.fit()
r.summary()
输出结果为:

来自 Python 的最大似然估计(MLE)结果
就这样!如演示所示,最大似然估计成功地估算了我们为模拟选择的参数。
总结
即使是估计一个简单的 MA(1) 模型,也能展示这种方法的强大功能,它不仅能高效地利用我们的数据,而且为理解和解释时间序列数据的动态提供了坚实的统计基础。
希望你喜欢这个内容!
参考文献
[1] Andrew Lesniewski, 时间序列分析, 2019, 巴鲁克学院,纽约
[2] Eric Zivot, ARMA 模型的估计, 2005
除非另有说明,所有图片均由作者提供
使用结果加权学习估算个性化治疗规则
一种用于为患者制定个性化治疗的非参数方法
·发表于 Towards Data Science ·阅读时间 6 分钟·2024 年 3 月 31 日
--
在许多疾病中,不同的患者对不同的治疗反应不同。对一些患者有效的药物可能对其他具有不同特征的患者无效。因此,通过根据患者的特征进行治疗,而不是对所有患者采用相同治疗,可以显著提高医疗效果。
在本文中,我将尝试向你展示如何训练一个机器学习模型来学习最优的个性化治疗。
本文讨论的是个性化医疗领域,但其结果可以应用于任何领域。例如:不同的人对社交媒体上的不同广告反应不同,因此,在同一产品有多个广告的情况下,如何选择向哪些观众展示哪个广告?
该方法在任何必须给出治疗但每个样本个体只能接受一种治疗的情况下非常有用,因此你无法知道该个体如果接受其他治疗会有怎样的反应。
让我们形式化这个问题
进行了一项实验,比较了两种(或更多)治疗方法。我们将它们命名为 T = 1,2……一个协变量向量 X 表示每个患者。每个患者i具有一个协变量向量 Xᵢ,接受了治疗 Tᵢ,并且有一个记录的治疗反应 Rᵢ。
例如,假设你想测试三种不同的糖尿病药物,我们将这些药物命名为“1”、“2”、“3”。
我们有一个名为 Esther 的患者,她 64 岁,8 年前被诊断为糖尿病,体重 65 公斤,身高 1.54 米。Esther 接受了药物“1”,并且在服用新药后,她的血糖下降了 10 个点。
在我们的例子中,我们对 Esther 的数据点是 X = {女性,64 岁,诊断 8 年,65 公斤,1.54 米},T = “1”,R = 10。
在这种设定下,我们希望学习一个最优的决策规则 D(x),它为每个患者分配治疗“1”、“2”或“3”,以优化该患者的治疗效果。
解决这个问题的旧方法是将结果建模为数据和治疗的函数,并将预测结果表示为f(X,T)。一旦我们有了模型,我们就可以创建一个决策规则 D(x):我们计算f(X,1),f(X,2),和f(X,3),并给予患者能够最大化其期望结果的药物。
当我们对生成数据的基础模型有相当好的理解时,这个解决方案是有效的。在这种情况下,我们只需要一些微调来找到适合我们案例的最佳参数。
然而,如果模型不好,那么我们的结果也会不好,无论手头的数据有多少。
我们能否提出一个不带参数的决策规则,并且不假设数据与治疗结果之间有任何先验关系?
答案是肯定的,我们可以使用机器学习找到一个不假设反应和治疗之间关系的决策规则!
使用 Outcome Weighted Learning(OWL)方法解决非参数问题
解决这个问题的方法是解决一个分类问题,其中标签是实验中给予的治疗,每个数据点i的权重为 Rᵢ/π(Tᵢ|Xᵢ),其中π(Tᵢ|Xᵢ)是给定你有特征 Xᵢ的情况下,获得治疗 Tᵢ的倾向性,这可以从数据中计算出来。
这有道理,因为我们试图遵循实验的结果,但仅仅是最有效的地方。我们通过倾向性进行除法,是为了修正类别大小的偏差。如果你学过强化学习,那么这个整个过程对你来说应该是熟悉的。
这里是一个使用支持向量机(SVM)的猫头鹰分类器的例子。你可以自由选择任何你喜欢的分类器。
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn import svm
def owl_classifier(X_train, T, R, kernel, gamma):
n = len(T)
pi = np.zeroes(n) #Initialize pi as a vector of zeroes
probs = LogisticRegression().fit(X_train, T).predict_proba(X_train)#This is a n*unique(T) matrix that gives every person the probability of getting each treatment
for t in np.unique(T):
pi += probs[,t]*(T==t) #Every data point is assigned the probability of getting the treatment that it got, given the covariates
clf = svm.SVC(kernel = kernel, gamma = gamma) # initialize an svm classifier, the parameters need to be found by cross validation
clf.fit(X_train, T, sample_weight = R/pi) # fit the classifier with the treatments as labels and R/pi as sample weights
用于测试 OWL 方法的仿真
模拟数据可以用来测试猫头鹰方法。我们创建奖励函数,以便知道每个患者的最佳治疗方案。然后,我们可以在数据上训练 OWL 分类器,并检查其与最佳分类器的契合程度。
例如:
我创建了 50 个特征,它们都是从 U([-1,1])分布中采样的。我随机均匀地给予患者三种治疗方案之一{1,2,3}。
响应函数是从 N(μ, 1)分布中采样的,其中μ = (X₁ + X₂)I(T=1) + (X₁ — X₂)I(T=2) + (X₂-X₁)*I(T=3)
# This code block creates the data for the simulation
import numpy as np
n_train = 500 # I purposely chose a small training set to simulate a medical trial
n_col = 50 # This is the number of features
n_test = 1000
X_train = np.random.uniform(low = -1, high = 1, size = (n_train, n_col))
T = np.random.randint(3, size = n_train) # Treatments given at random uniformly
R_mean = (X_train[:,0]+X_train[:,1])*(T==0) + (X_train[:,0]-X_train[:,1])*(T==1) + (X_train[:,1]-X_train[:,0])*(T==2)
R = np.random.normal(loc = R_mean, scale = .1) # The stanadard deviation can be tweaked
X_test = np.random.uniform(low = -1 , high = 1, size = (n_test, n_col))
# The optimal classifier can be deduced from the design of R
optimal_classifier = (1-(X_test[:,0] >0)*(X_test[:,1]>0))*((X_test[:,0] > X_test[:,1]) + 2*(X_test[:,1] > X_test[:,0]))
不难看出,最佳治疗方案是在 X₁和 X₂都为正时给治疗 1。如果它们都为负,则当 X₂ < X₁时给予治疗 2,当 X₁ < X₂时给予治疗 3。如果 X₁为正且 X₂为负,则给予治疗 2。如果 X₂为正且 X₁为负,则给予治疗 3。
或者我们可以通过图像展示这个内容。这些是最佳治疗的不同范围,针对 X₁和 X₂的范围展示:

X₁、X₂组合的最佳治疗范围
我采样了 500 个数据点,包含 50 个特征,并使用我上面描述的奖励函数。我用高斯(‘rbf’)核函数拟合了一个 OWL 分类器,并得到了以下分类结果,我将其可视化以展示 X₁和 X₂的值:

针对 X₁、X₂值的治疗组分类可视化
# Code for the plot
import seaborn as sns
kernel = 'rbf'
gamma = 1/X_train.shape[1]
# gamma is a hyperparameter that has to be found by cross validation but this is a good place to start
D = owl_classifier(X_train, T, R, kernel, gamma)
prediction = D.predict(X_test)
sns.scatterplot(x = X_test[:,0], y = X_test[:,1], c = prediction )
如果你没有注意到这里发生了什么:数据由两个影响反应的特征和 48 个噪声特征组成。模型成功地学习到了这两个重要特征的影响,而我们并没有以任何方式对这个关系进行建模!
这只是一个简单的例子,我让奖励函数依赖于 X₁和 X₂,以便易于理解和可视化,但你可以自由使用其他例子并尝试不同的分类器。
结论
Outcome-weighted learning 可以用来学习最佳治疗方案,适用于我们在训练数据中每个病人只看到一个治疗方法的情况,而不需要将反应建模为特征和治疗方法的函数。
有一些数学内容我从本文中省略了,这些内容证明了整个过程的合理性,我并不是凭空编造的。
未来对此主题的研究应包括:
-
开发与探索:即使我们已经学到了治疗规则,有时探索那些模型认为不是最佳的选项仍然是有益的。模型可能是错误的。
-
序列治疗:当存在一系列治疗时,每一次治疗都会改变病人的状态。整个序列的解决方案应通过动态规划来求解。
-
设计:在本文中,我只是假设治疗是按照给定规则提供的。也许我们可以找到一些设计来改进学习过程。
Python 中的 ETL 管道:最佳实践与技术
提升 ETL 管道的通用性、可扩展性和可维护性的策略
·发表于 Towards Data Science ·阅读时间 10 分钟·2024 年 10 月 20 日
--

图片由提供
Produtora Midtrack 及其来源于 Pexels.com
在构建新的 ETL 管道时,必须考虑三个关键要求:通用性、可扩展性和可维护性。这些要素在数据工作流的有效性和持久性中发挥着至关重要的作用。然而,挑战通常在于如何在它们之间找到合适的平衡——有时,提升一个方面可能会以牺牲另一个方面为代价。例如,优先考虑通用性可能会导致可维护性降低,从而影响架构的整体效率。
在这篇博客中,我们将深入探讨这三个概念的细节,探索如何有效地优化你的 ETL 管道。我将分享一些实用的工具和技术,帮助你提升工作流的通用性、可扩展性和可维护性。此外,我们还将研究实际案例,分类不同的场景,明确定义满足组织特定需求的 ETL 要求。
通用性
在 ETL 的背景下,通用性指的是管道在无需大量重新配置的情况下处理输入数据变化的能力……
评估你想评估的任何内容 | 使用 LLMs 创建高级评估器
探索如何为特定的现实世界需求构建自定义 LLM 评估器。
·发表于Towards Data Science ·阅读时间 7 分钟·2024 年 4 月 18 日
--

由 DALLE-3 生成的图像 | 等距风格的机器人检查
考虑到在大型语言模型(LLM)“链条”、 “代理”、聊天机器人以及其他文本生成型 AI 应用领域的快速发展,评估语言模型的表现对于理解其能力和局限性至关重要。尤其需要能够根据商业目标调整这些指标。
尽管像困惑度、BLEU 分数和句子距离等标准指标能够提供模型表现的一般性指示,但根据我的经验,它们在捕捉现实世界应用中的细微差别和特定需求方面往往表现不佳。
例如,考虑一个简单的 RAG 问答应用。在构建问答系统时,所谓的“RAG 三要素”中的上下文相关性、事实依据和查询与回答之间的语言一致性等因素也非常重要。标准指标根本无法有效捕捉这些细微的方面。
这时,基于 LLM 的“黑箱”指标就派上用场了。尽管这个想法听起来可能天真,但基于 LLM 的“黑箱”指标背后的概念却相当有吸引力。这些指标利用大型语言模型本身的力量来进行评估……
严格评估 RAG,或者失败
使用 RAGAs 框架和超参数优化提升你 RAG 系统的质量。
·发布于 Towards Data Science ·11 分钟阅读·2024 年 4 月 26 日
--

这是一幅展示“LLMs 评估 RAG”的图示。作者使用 Canva 中的 AI 生成了该图。
简而言之
如果你开发一个 RAG 系统,你必须在不同的设计选项之间做出选择。ragas 库可以通过生成基于你文档的答案的合成评估数据来帮助你。这使得可能对 RAG 系统进行严格评估,采用经典的训练/验证/测试数据集划分,从而提升 RAG 系统的质量。
引言
在实际操作中,开发一个检索增强生成(RAG)系统需要做出许多决定,这些决定会直接影响其最终质量,例如关于文本分割器、块大小、重叠大小、嵌入模型、存储的元数据、语义搜索的距离度量、用于重排的 top-k、重排模型、上下文的 top-k、提示工程等。
现实情况: 在大多数情况下,这类决策并没有基于方法论上严谨的评估实践,而是由开发人员和产品负责人做出的临时判断,这些人往往面临截止日期的压力。
黄金标准: 相比之下,RAG 系统的严格评估应包括:
-
一个大型评估集,以便能够以较低的置信区间估算性能指标
-
评估集中的多样化问题
-
针对内部文档的特定答案
-
检索和生成的独立评估
-
对整个 RAG 系统的评估
-
训练/验证/测试数据集划分,以确保良好的泛化能力
-
超参数优化
由于缺乏基于私有文档的答案评估集,大多数 RAG 系统未能达到黄金标准进行严格评估!
通用的大型语言模型(LLM)基准(如 GLUE、SuperGlue、MMLU、BIG-Bench、HELM 等)对于评估 RAG 系统的相关性不大,因为 RAG 的本质在于从 LLM 无法知晓的内部文档中提取信息。如果你坚持使用 LLM 基准来评估 RAG 系统,一种方法是选择与你领域特定的任务,并量化在这个选定任务上,RAG 系统相较于基础 LLM 所增加的价值。
通用 LLM 基准的替代方案是基于内部文档创建人工标注的测试集,使得问题的解答需要访问这些内部文档才能正确回答。这种解决方案通常成本高昂。此外,外包标注可能对内部文档造成问题,因为这些文档可能包含敏感或私人信息,不能与外部方共享。
这里是RAGAs 框架(检索增强生成评估)[1],用于无参考的 RAG 评估,并提供了ragas包的 Python 实现:
pip install ragas
它提供了严格 RAG 评估所需的工具:
-
合成评估集的生成
-
专门用于 RAG 评估的度量标准
-
针对非英语语言的提示调整
-
与 LangChain 和 Llama-Index 的集成
合成评估集
LLM 爱好者,包括我自己,建议使用 LLM 作为解决许多问题的方案。这里的意思是:
LLM 并非自主的,但可能是有用的。RAGAs 利用 LLM 生成合成评估集来评估 RAG 系统。
RAGAs 框架继承了 Evol-Instruct 框架,该框架使用 LLM 在演化过程中生成多样化的指令数据集(即问题—答案对,QA)。

图 1:描绘了 RAGAs 中问题演化的过程。作者在 Canva 和 draw.io 中创建了这张图。
在 Evol-Instruct 框架中,LLM 从一组简单的初始指令开始,逐渐将其改写为更复杂的指令,创造多样化的指令数据。Can Xu 等人[2]认为,指令数据的逐步、增量演化会产生高质量的结果。在 RAGAs 框架中,由 LLM 生成并演化的指令数据是基于现有文档的。ragas 库目前实现了三种不同类型的指令数据演化方式,按深度演化,起始于简单问题:
-
推理: 重写问题以增加推理的需求。
-
条件化: 重写问题以引入一个条件元素。
-
多上下文: 重写问题,要求多份文档或多个部分来回答。
此外,ragas 库还提供了生成对话的选项。现在,让我们来看一下 ragas 在实践中的应用。
问题演化的示例
我们将使用关于大语言模型 [3] 的 Wikipedia 页面作为 ragas 库生成问题 — 真实答案对的源文档,每种演化类型都有一个问题 — 真实答案对。
要运行代码:你可以按照文章中的代码片段,或者访问包含所有相关代码的 Github 笔记本,在 Colab 或本地运行:
[## colab-demos/rags/evaluate-rags-rigorously-or-perish.ipynb at main · gox6/colab-demos
在博客中讨论的数据科学和人工智能主题的 Colab 笔记本:medium.com/@jgrygolec …
# Installing Python packages & hiding
!pip install --quiet \
chromadb \
datasets \
langchain \
langchain_chroma \
optuna \
plotly \
polars \
ragas \
1> /dev/null
# Importing the packages
from functools import reduce
import json
import os
import requests
import warnings
import chromadb
from chromadb.api.models.Collection import Collection as ChromaCollection
from datasets import load_dataset, Dataset
from getpass import getpass
from langchain_chroma import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_core.runnables.base import RunnableSequence
from langchain_community.document_loaders import WebBaseLoader, PolarsDataFrameLoader
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_text_splitters import CharacterTextSplitter
from operator import itemgetter
import optuna
import pandas as pd
import plotly.express as px
import polars as pl
from ragas import evaluate
from ragas.metrics import (
answer_relevancy,
faithfulness,
context_recall,
context_precision,
answer_correctness
)
from ragas.testset.generator import TestsetGenerator
from ragas.testset.evolutions import simple, reasoning, multi_context, conditional
# Providing api key for OPENAI
OPENAI_API_KEY = getpass("OPENAI_API_KEY")
os.environ["OPENAI_API_KEY"] = OPENAI_API_KEY
# Examining question evolution types evailable in ragas library
urls = ["https://en.wikipedia.org/wiki/Large_language_model"]
wikis_loader = WebBaseLoader(urls)
wikis = wikis_loader.load()
llm = ChatOpenAI(model="gpt-3.5-turbo")
generator_llm = llm
critic_llm = llm
embeddings = OpenAIEmbeddings()py
generator = TestsetGenerator.from_langchain(
generator_llm,
critic_llm,
embeddings
)
# Change resulting question type distribution
list_of_distributions = [{simple: 1},
{reasoning: 1},
{multi_context: 1},
{conditional: 1}]
# This step COSTS $$$ ...
question_evolution_types = list(
map(lambda x: generator.generate_with_langchain_docs(wikis, 1, x),
list_of_distributions)
)
# Displaying examples
examples = reduce(lambda x, y: pd.concat([x, y], axis=0),
[x.to_pandas() for x in question_evolution_types])
examples = examples.loc[:, ["evolution_type", "question", "ground_truth"]]
examples
运行上述代码后,我基于前述的 Wikipedia 页面 [3] 收到了以下合成问答对。

表 1:使用 ragas 库和 GPT-3.5-turbo 从 Wikipedia 页面生成的合成问答对 [3]。
表 1 中展示的结果非常吸引人。简单的演化表现得非常好。在推理演化的情况下,问题的第一部分回答得非常完美,但第二部分没有回答。检查 Wikipedia 页面 [3],在文档中并没有找到第二部分问题的答案,因此这也可以解读为对幻觉的抑制,这是一个好现象。多上下文的问答对似乎也很好。如果我们看问答对,条件演化类型是可以接受的。解读这些结果的一种方式是,演化背后总是有更好的提示工程空间。另一种方式是使用更好的 LLM,特别是在批评角色方面,正如 ragas 库中默认的那样。
指标
ragas 库不仅能够生成合成评估集,还为我们提供了内置的指标,用于按组件评估以及对 RAG 的端到端评估。

图片 2: RAGAS 中的 RAG 评估指标。图像由作者在 draw.io 中创建。
截至本文撰写时,RAGAS 提供了八种开箱即用的 RAG 评估指标,见图片 2,并且可能会添加新的指标。你将选择最适合你使用场景的指标。不过,我建议选择最重要的一个指标,即:
答案正确性— 端到端指标,得分介于 0 到 1 之间,分数越高越好,衡量生成答案与真实答案的准确度。
专注于一个端到端的指标有助于尽快开始优化你的 RAG 系统。一旦你在质量上取得了一些进展,就可以查看各个组件的指标,专注于每个 RAG 组件最重要的指标:
准确性 — 生成指标,得分范围为 0 到 1,分数越高越好,衡量生成答案相对于提供的上下文的事实一致性。它旨在尽可能多地将生成的答案与提供的上下文相结合,从而防止幻觉生成。
上下文相关性 — 检索指标,得分范围为 0 到 1,得分越高越好,衡量检索到的上下文相对于问题的相关性。
RAG 工厂
好的,RAG 已经准备好进行优化了……但不要太快,这还不够。为了优化 RAG,我们需要一个工厂函数来生成具有给定 RAG 超参数的 RAG 链。在这里,我们将这个工厂函数分为 2 个步骤来定义:
步骤 1:一个用于将文档存储到向量数据库中的函数。
# Defining a function to get document collection from vector db with given hyperparemeters
# The function embeds the documents only if collection is missing
# This development version as for production one would rather implement document level check
def get_vectordb_collection(chroma_client,
documents,
embedding_model="text-embedding-ada-002",
chunk_size=None, overlap_size=0) -> ChromaCollection:
if chunk_size is None:
collection_name = "full_text"
docs_pp = documents
else:
collection_name = f"{embedding_model}_chunk{chunk_size}_overlap{overlap_size}"
text_splitter = CharacterTextSplitter(
separator=".",
chunk_size=chunk_size,
chunk_overlap=overlap_size,
length_function=len,
is_separator_regex=False,
)
docs_pp = text_splitter.transform_documents(documents)
embedding = OpenAIEmbeddings(model=embedding_model)
langchain_chroma = Chroma(client=chroma_client,
collection_name=collection_name,
embedding_function=embedding,
)
existing_collections = [collection.name for collection in chroma_client.list_collections()]
if chroma_client.get_collection(collection_name).count() == 0:
langchain_chroma.from_documents(collection_name=collection_name,
documents=docs_pp,
embedding=embedding)
return langchain_chroma
步骤 2:一个用于在 LangChain 中生成 RAG 的函数,使用文档集合或合适的 RAG 工厂函数。
# Defininig a function to get a simple RAG as Langchain chain with given hyperparemeters
# RAG returns also the context documents retrieved for evaluation purposes in RAGAs
def get_chain(chroma_client,
documents,
embedding_model="text-embedding-ada-002",
llm_model="gpt-3.5-turbo",
chunk_size=None,
overlap_size=0,
top_k=4,
lambda_mult=0.25) -> RunnableSequence:
vectordb_collection = get_vectordb_collection(chroma_client=chroma_client,
documents=documents,
embedding_model=embedding_model,
chunk_size=chunk_size,
overlap_size=overlap_size)
retriever = vectordb_collection.as_retriever(top_k=top_k, lambda_mult=lambda_mult)
template = """Answer the question based only on the following context.
If the context doesn't contain entities present in the question say you don't know.
{context}
Question: {question}
"""
prompt = ChatPromptTemplate.from_template(template)
llm = ChatOpenAI(model=llm_model)
def format_docs(docs):
return "\n\n".join([doc.page_content for doc in docs])
chain_from_docs = (
RunnablePassthrough.assign(context=(lambda x: format_docs(x["context"])))
| prompt
| llm
| StrOutputParser()
)
chain_with_context_and_ground_truth = RunnableParallel(
context=itemgetter("question") | retriever,
question=itemgetter("question"),
ground_truth=itemgetter("ground_truth"),
).assign(answer=chain_from_docs)
return chain_with_context_and_ground_truth
之前的函数get_vectordb_collection已合并到后者函数get_chain中,该函数为给定参数集(即:embedding_model、llm_model、chunk_size、overlap_size、top_k、lambda_mult)生成我们的 RAG 链。通过我们的工厂函数,我们仅仅触及到优化我们 RAG 系统超参数的可能性。另请注意,RAG 链将需要 2 个参数:question和ground_truth,其中后者将作为评估时所需的输入通过 RAG 链传递。
# Setting up a ChromaDB client
chroma_client = chromadb.EphemeralClient()
# Testing full text rag
with warnings.catch_warnings():
rag_prototype = get_chain(chroma_client=chroma_client,
documents=news,
chunk_size=1000,
overlap_size=200)
rag_prototype.invoke({"question": 'What happened in Minneapolis to the bridge?',
"ground_truth": "x"})["answer"]
RAG 评估
为了评估我们的 RAG,我们将使用来自 CNN 和 Daily Mail 的多样化新闻文章数据集,该数据集可在 Hugging Face [4] 上获得。该数据集中的大多数文章少于 1000 字。此外,我们将使用该数据集中的一个小片段,仅包含 100 篇新闻文章。所有这些都是为了限制运行演示所需的成本和时间。
# Getting the tiny extract of CCN Daily Mail dataset
synthetic_evaluation_set_url = "https://gist.github.com/gox6/0858a1ae2d6e3642aa132674650f9c76/raw/synthetic-evaluation-set-cnn-daily-mail.csv"
synthetic_evaluation_set_pl = pl.read_csv(synthetic_evaluation_set_url, separator=",").drop("index")
# Train/test split
# We need at least 2 sets: train and test for RAG optimization.
shuffled = synthetic_evaluation_set_pl.sample(fraction=1,
shuffle=True,
seed=6)
test_fraction = 0.5
test_n = round(len(synthetic_evaluation_set_pl) * test_fraction)
train, test = (shuffled.head(-test_n),
shuffled.head( test_n))
由于我们将考虑多个不同的 RAG 原型,而不仅仅是上面定义的那个,我们需要一个函数来收集 RAG 在我们的合成评估集上生成的答案:
# We create the helper function to generate the RAG ansers together with Ground Truth based on synthetic evaluation set
# The dataset for RAGAS evaluation should contain the columns: question, answer, ground_truth, contexts
# RAGAs expects the data in Huggingface Dataset format
def generate_rag_answers_for_synthetic_questions(chain,
synthetic_evaluation_set) -> pl.DataFrame:
df = pl.DataFrame()
for row in synthetic_evaluation_set.iter_rows(named=True):
rag_output = chain.invoke({"question": row["question"],
"ground_truth": row["ground_truth"]})
rag_output["contexts"] = [doc.page_content for doc
in rag_output["context"]]
del rag_output["context"]
rag_output_pp = {k: [v] for k, v in rag_output.items()}
df = pl.concat([df, pl.DataFrame(rag_output_pp)], how="vertical")
return df
RAG 优化与 RAGAs 和 Optuna
首先,值得强调的是,RAG 系统的适当优化应该涉及全局优化,其中所有参数同时优化。这与顺序或贪婪方法相对立,后者是逐个优化参数。顺序方法忽略了参数之间可能存在的相互作用,而这些相互作用可能导致次优解。
现在,我们已经准备好优化我们的 RAG 系统。我们将使用超参数优化框架Optuna。为此,我们定义 Optuna 研究的目标函数,指定允许的超参数空间并计算评估指标。请参见下面的代码:
def objective(trial):
embedding_model = trial.suggest_categorical(name="embedding_model",
choices=["text-embedding-ada-002", 'text-embedding-3-small'])
chunk_size = trial.suggest_int(name="chunk_size",
low=500,
high=1000,
step=100)
overlap_size = trial.suggest_int(name="overlap_size",
low=100,
high=400,
step=50)
top_k = trial.suggest_int(name="top_k",
low=1,
high=10,
step=1)
challenger_chain = get_chain(chroma_client,
news,
embedding_model=embedding_model,
llm_model="gpt-3.5-turbo",
chunk_size=chunk_size,
overlap_size= overlap_size ,
top_k=top_k,
lambda_mult=0.25)
challenger_answers_pl = generate_rag_answers_for_synthetic_questions(challenger_chain , train)
challenger_answers_hf = Dataset.from_pandas(challenger_answers_pl.to_pandas())
challenger_result = evaluate(challenger_answers_hf,
metrics=[answer_correctness],
)
return challenger_result['answer_correctness']
最后,利用目标函数,我们定义并运行了一个研究,旨在优化我们的 RAG 系统在 Optuna 中的表现。我们可以通过方法 enqueue_trial 将超参数的预设猜测添加到研究中,并通过时间或试验次数来限制研究的范围。更多技巧请参阅 Optuna 的文档。
sampler = optuna.samplers.TPESampler(seed=6)
study = optuna.create_study(study_name="RAG Optimisation",
direction="maximize",
sampler=sampler)
study.set_metric_names(['answer_correctness'])
educated_guess = {"embedding_model": "text-embedding-3-small",
"chunk_size": 1000,
"overlap_size": 200,
"top_k": 3}
study.enqueue_trial(educated_guess)
print(f"Sampler is {study.sampler.__class__.__name__}")
study.optimize(objective, timeout=180)
在我们的研究中,预设猜测没有得到确认,但我相信通过像上述所提到的严谨方法,它会变得更好。
Best trial with answer_correctness: 0.700130617593832
Hyper-parameters for the best trial: {'embedding_model': 'text-embedding-ada-002', 'chunk_size': 700, 'overlap_size': 400, 'top_k': 9}
RAGAs 的局限性
在尝试使用 ragas 库来合成评估集并评估 RAG 时,我有一些警告:
-
问题中可能包含答案。
-
基准答案仅仅是文档中的字面摘录。
-
在 Colab 上出现 RateLimitError 问题以及网络溢出问题。
-
内置的进化功能较少,且没有简单的方法来添加新的进化功能。
-
文档还有提升的空间。
前两个警告是与质量相关的。根本原因可能在于使用的 LLM,显然,GPT-4 的结果优于 GPT-3.5-Turbo。同时,似乎通过对生成合成评估集所用进化的提示工程,能够改善这一点。
对于速率限制和网络溢出问题,建议使用 1) 在生成合成评估集时进行检查点保存,以防丢失已创建的数据,和 2) 指数退避,以确保你能够完成整个任务。
最后,也是最重要的,更多的内置进化功能将是 ragas 包的一个受欢迎的补充,更不用说能够更容易地创建自定义进化功能的可能性。
RAGAs 的其他有用功能
-
自定义提示。 Ragas 包允许你修改提供的抽象中的提示。文档中描述了评估任务中度量的自定义提示示例。
-
自动语言适应。 RAGAs 支持非英语语言的 RAG 评估,具有一个非常棒的功能——自动语言适应。更多信息请查看文档。
结论
尽管 RAGAs 存在局限性,但不要忽视最重要的事情:
尽管 RAGAs 是一个年轻的工具,但它已经非常有用了。它能够生成合成评估集,用于严格的 RAG 评估,这是成功开发 RAG 系统的关键方面。
致谢
如果我没有站在巨人的肩膀上,这个项目和文章是无法完成的。尽管不可能列举所有的影响,但以下几位与本研究直接相关:
[1] S. Es, J. James, L. Espinosa-Anke, S. Schockaert, RAGAS: 自动化评估检索增强生成 (2023), arXiv:2309.15217
[2] C. Xu, Q. Sun, K. Zheng, X. Geng, P. Zhao, J. Feng, C. Tao, D. Jiang, WizardLM:赋能大型语言模型以执行复杂指令(2023),arXiv:2304.12244
[3] Community, Large Language Models, 维基百科(2024),en.wikipedia.org/wiki/Large_language_model
[4] CNN 和 Daily Mail 数据集可在 Hugging Face 上获取,更多信息请见:huggingface.co/datasets/cnn_dailymail
评估 ChatGPT 在数据科学中的应用:以客户流失预测分析为例
ChatGPT 能否帮助甚至取代数据科学家?
·发布于 Towards Data Science ·阅读时长 8 分钟 ·2024 年 5 月 26 日
--

图片由作者提供。(AI 生成的数据科学家)
你是否曾想过像 ChatGPT 这样的 AI 工具在实际数据科学应用中的有效性?或者它们在多大程度上能够协助,甚至可能取代数据科学家角色的某些方面?在这篇文章中,我使用了一个真实的世界数据集,深入探讨了这些问题,并记录了我的发现。
这篇文章最初发布在我的博客这里于 2024 年 2 月。
引言
自从 GPT 推出了数据分析功能已经有一段时间了。正如去年所承诺的,我写这篇文章来评估它在帮助(甚至取代)数据科学家方面的能力及其局限性。
为了完成这个任务,我让 GPT 执行了一个在实际生活中非常常见的数据科学项目——客户流失预测。这是许多企业的核心问题,也是数据科学应用案例较多的领域。我从 Kaggle 上找到了这个在线零售客户流失数据集,其中包含各种客户人口统计和参与数据,并且有一个干净的目标列,标明客户是否流失。以下是我给 ChatGPT 的完整提示:
You are a professional data scientist working at an online retail company.
You have this dataset…
评估 ChatGPT 的数据分析改进:交互式表格和图表
ChatGPT 是否正在成为商业智能工具?
·发布于Towards Data Science ·9 分钟阅读·2024 年 7 月 19 日
--
2024 年 5 月,在激动人心的 GPT-4o 发布之际,OpenAI 宣布了其在 ChatGPT 中对数据分析的改进,该改进包括交互式表格和图表,以及与 Google Drive 和 Microsoft OneDrive 的集成。
在本文中,我将评估这些新功能,并展望 ChatGPT 在数据分析方面的未来。

ChatGPT 数据分析简史
ChatGPT 在数据分析方面的旅程始于 2023 年 3 月推出的代码解释器,2023 年 7 月开始向 Plus 用户推出。
后来,OpenAI 将其重新命名为高级数据分析(Advanced Data Analysis),然后是数据分析(Data Analysis),如今作为官方 GPT 之一,更名为数据分析师。如今,你可以使用这个独立的“数据分析师”GPT,或者直接提示 ChatGPT 执行数据分析功能。

由 ChatGPT 团队创建的 GPT(截图由作者提供)
OpenAI 一直在不断改进这些功能。值得注意的是,它在 2024 年 5 月宣布了对 ChatGPT 数据分析的改进,增强了与表格的交互能力……
评估电影对白——哪些句法和语义特征能预测电影类型?
自然语言处理
本文探讨了电影对白与电影类型之间的关系,运用领域驱动的数据分析和有根据的特征工程。
·发表于 Towards Data Science ·15 分钟阅读·2024 年 1 月 20 日
--

DALLE 生成的作者图像
从惊悚片中的零散对白到动作片中的脏话连篇,我们是否能仅凭对白中的语义和句法特征来推测电影类型?如果可以,哪些特征最为关键?
我们将研究剧本中的细微对话模式——其词汇、结构和节奏——是否可以成为预测电影类型的有效因素。这里的重点有两个:一是利用句法和语义的剧本特征作为预测特征,二是强调有根据的特征工程的重要性。
许多数据科学课程的一个主要短板是缺乏对领域专业知识、特征生成、工程和选择的强调。许多课程还向学生提供现成的数据集,有时这些数据集已经清理过了。而在职场中,追求快速产出往往掩盖了假设和验证过程的重要性……
评估边缘检测?不要使用 RMSE、PSNR 或 SSIM
为什么“绩效指标”(Figure of Merit,FOM)是最佳的边缘检测评估指标的经验和理论依据
·发表于 Towards Data Science ·阅读时长:12 分钟·2024 年 10 月 8 日
--
图像分割和边缘检测是密切相关的任务。以一个海岸线分割模型的输出为例:

图 1:从分割掩模到边缘图(来源:作者)(数据集:LICS)(CC BY 4.0)
模型会将每个像素分类为陆地或海洋(分割掩模)。然后,海岸线就是分类变化的像素(边缘图)。通常,边缘检测可以通过图像分割模型输出的边界来实现。
我希望在我的研究中利用这种关系来帮助评估海岸线图像分割模型。类似的研究都使用基于混淆矩阵的指标,如准确率、精确率和召回率。这些指标将预测的分割掩模中的所有像素与地面真实掩模进行比较。
问题在于,这些方法可能高估了在最重要区域——海岸线——的性能。
大多数像素位于海洋的中央或完全被陆地包围。这些像素比靠近海岸线的像素更容易分类。你可以在图 2 中看到这一点。不幸的是,这些错误可能会被大量正确分类的像素所掩盖。
评估大型语言模型
生成式人工智能
如何评估你的大型语言模型(LLM)表现如何?一份完整的指南。
·发布于 Towards Data Science ·19 分钟阅读·2024 年 1 月 14 日
--

自从稳定扩散(Stable Diffusion)和 ChatGPT 发布超过一年以来,生成式人工智能发展迅速。每周几乎都会有新的模型宣布声称能超越现有最先进的技术。但我们如何知道这些模型是否真有其价值?在缺乏地面真实数据和“正确”解决方案的情况下,如何比较和排名生成式模型?最后,如果大型语言模型(LLM)通过检索增强生成(Retrieval-Augmented Generation,简称 RAG)系统使用外部数据,我们又如何评判它是否正确使用了这些数据?
在这两部分的系列文章中,我们将探讨生成式人工智能的评估协议。本文重点讨论文本生成和大型语言模型。敬请关注下一篇,我们将讨论图像生成器的评估方法。
评估生成的内容
让我们首先注意到生成式模型和判别式模型之间的区别。生成式模型生成与训练数据相似的新数据样本,无论是文本、图像、音频、视频、潜在表示,甚至是表格数据。另一方面,判别式模型通过训练数据学习决策边界,使我们能够解决…
在 Cypher 语句生成中评估 LLMs
评估生成的 Cypher 语句准确性的分步教程
·发表于Towards Data Science ·阅读时间 9 分钟·2024 年 1 月 19 日
--

由 DALL-E 生成的图像
大型语言模型(LLMs)开始流行后不久,我们意识到它们在将自然语言转换为 SQL 和 Cypher 等数据库查询方面相当不错。为了使 LLM 能够为您的特定数据库生成定制的查询,您必须提供其架构,并可选地提供一些示例查询。有了这些信息,LLM 可以根据自然语言输入生成数据库查询。
尽管 LLMs 在将自然语言转换为数据库查询方面展示了巨大的潜力,但它们仍然远非完美。因此,理解它们的表现如何,通过评估过程来评估其性能是至关重要的。幸运的是,生成 SQL 语句的过程已经在学术界得到了研究,例如Spider等研究。我们将使用以下指标来评估 LLMs 的 Cypher 生成能力。
-
Jaro-Winkler: 这是一种基于编辑距离的文本相似度度量。我们将生成的 Cypher 查询与正确的 Cypher 查询进行比较,并通过编辑查询所需的差异,衡量两个字符串之间的不同程度。
-
Pass@1: 如果生成的查询从数据库中返回的结果与正确的 Cypher 语句相同,则得分为 1.0,否则为 0.0。
-
Pass@3: 与 Pass@1 类似,不同之处在于我们生成 3 个查询,而不是 1 个查询。如果其中任何一个查询的结果与正确的查询相同,则得分为 1.0,否则为 0.0。
-
Jaccard 相似度:它衡量由生成的 Cypher 语句返回的响应与正确的 Cypher 响应之间的 Jaccard 相似度。这个度量标准旨在捕捉模型可能返回几乎正确的结果的示例。
代码可以在GitHub上找到,这是与Adam Schill Collberg合作开发的。
正如你所观察到的,重点是评估来自数据库的响应,而不是实际的 Cypher 语句本身。一个原因是,Cypher 语句可以有多种写法来获取相同的信息。我们不关心 LLM 偏好哪种语法;我们只关心它是否能生成正确的响应。此外,我们对于 LLM 在响应中如何命名列没有强烈偏好,因此我们不希望评估它的列命名能力等……
测试数据集
测试数据集由问题和相关的 Cypher 语句对组成。
你可以使用 LLM 来生成测试数据集的建议。然而,你需要手动验证这些示例,因为 LLM 可能会出错,并且并不是 100%可靠。如果它们完全可靠,我们就不需要测试它们了。由于我们是基于数据库结果而不是 Cypher 语句本身来进行评估的,因此我们需要一个运行中的数据库,并且必须包含我们可以使用的相关信息。在这篇博客文章中,我们将使用Neo4j Sandbox 中的推荐项目。该推荐项目使用了MovieLens 数据集,该数据集包含了电影、演员、评分等信息。推荐项目也可以在演示服务器上以只读访问的形式使用,这意味着如果你不想创建新的数据库实例,完全不需要这么做。

推荐项目图谱模式。图像由作者提供。
在这个例子中,我使用了 GPT-4 来为训练数据集生成建议,然后逐一检查并在需要的地方进行修正。我们将仅使用 27 个测试对。在实际应用中,你可能需要使用至少几百个示例。
data = [
{
"question": "How many movies were released in 1995?",
"cypher": "MATCH (m:Movie) WHERE m.Year = 1995 RETURN count(*) AS result",
},
{
"question": "Who directed the movie Inception?",
"cypher": "MATCH (m:Movie {title: 'Inception'})<-[:DIRECTED]-(d) RETURN d.name",
},
{
"question": "Which actors played in the movie Casino?",
"cypher": "MATCH (m:Movie {title: 'Casino'})<-[:ACTED_IN]-(a) RETURN a.name",
},
{
"question": "How many movies has Tom Hanks acted in?",
"cypher": "MATCH (a:Actor {name: 'Tom Hanks'})-[:ACTED_IN]->(m:Movie) RETURN count(m)",
},
{
"question": "List all the genres of the movie Schindler's List",
"cypher": "MATCH (m:Movie {title: 'Schindler\\'s List'})-[:IN_GENRE]->(g:Genre) RETURN g.name",
},
...
]
生成 Cypher 语句
我们将使用LangChain来生成 Cypher 语句。Neo4jGraph对象在 LangChain 中建立与 Neo4j 的连接,并检索其模式信息。
graph = Neo4jGraph()
print(graph.schema)
# Node properties are the following:
# Movie {posterEmbedding: LIST, url: STRING, runtime: INTEGER, revenue: INTEGER, budget: INTEGER, plotEmbedding: LIST, imdbRating: FLOAT, released: STRING, countries: LIST, languages: LIST, plot: STRING, imdbVotes: INTEGER, imdbId: STRING, year: INTEGER, poster: STRING, movieId: STRING, tmdbId: STRING, title: STRING}
# Genre {name: STRING}
# User {userId: STRING, name: STRING}
# Actor {url: STRING, bornIn: STRING, bio: STRING, died: DATE, born: DATE, imdbId: STRING, name: STRING, poster: STRING, tmdbId: STRING}
# Director {url: STRING, bornIn: STRING, born: DATE, died: DATE, tmdbId: STRING, imdbId: STRING, name: STRING, poster: STRING, bio: STRING}
# Person {url: STRING, bornIn: STRING, bio: STRING, died: DATE, born: DATE, imdbId: STRING, name: STRING, poster: STRING, tmdbId: STRING}
# Relationship properties are the following:
# RATED {rating: FLOAT, timestamp: INTEGER}
# ACTED_IN {role: STRING}
# DIRECTED {role: STRING}
# The relationships are the following:
# (:Movie)-[:IN_GENRE]->(:Genre)
# (:User)-[:RATED]->(:Movie)
# (:Actor)-[:ACTED_IN]->(:Movie)
# (:Actor)-[:DIRECTED]->(:Movie)
# (:Director)-[:DIRECTED]->(:Movie)
# (:Director)-[:ACTED_IN]->(:Movie)
# (:Person)-[:ACTED_IN]->(:Movie)
# (:Person)-[:DIRECTED]->(:Movie)
架构包含节点标签、它们的属性以及相应的关系。接下来,我们将使用 LangChain 表达式语言定义一个提示,发送给 LLM,指示它将自然语言翻译为 Cypher 语句,以检索相关信息来回答问题。欲了解有关 LangChain 表达式语言的更多详情,请访问官方文档。
cypher_template = """Based on the Neo4j graph schema below,
write a Cypher query that would answer the user's question.
Return only Cypher statement, no backticks, nothing else.
{schema}
Question: {question}
Cypher query:""" # noqa: E501
cypher_prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"Given an input question, convert it to a Cypher query. No pre-amble.",
),
("human", cypher_template),
]
)
cypher_chain = (
RunnablePassthrough.assign(
schema=lambda _: graph.get_schema,
)
| cypher_prompt
| llm.bind(stop=["\nCypherResult:"])
| StrOutputParser()
)
如果你熟悉对话型 LLM,你可以识别出系统和用户消息定义。正如你所观察到的,我们将图形架构和用户问题都放入了用户消息中。生成 Cypher 语句的具体提示工程指令仍未解决,这意味着这里可能还有改进空间。通过评估过程,你可以看到哪种方法对特定的 LLM 效果最佳。在这个例子中,我们使用的是gpt-4-turbo。
我们可以通过以下示例来测试 Cypher 生成:
response = cypher_chain.invoke(
{
"question": "How many movies have the keyword 'love' in the title and a runtime under 2 hours?"
}
)
print(response)
# MATCH (m:Movie)
# WHERE m.title CONTAINS 'love' AND m.runtime < 120
# RETURN count(m) as NumberOfMovies
我们可以观察到,gpt-4-turbo 在将自然语言翻译为 Cypher 语句方面表现得相当不错。现在让我们定义评估过程。
# Iterate over each row with tqdm to show a progress bar
for index, row in tqdm(df.iterrows(), total=df.shape[0]):
# Fetch data based on the test Cypher statement
true_data = graph.query(row["cypher"])
# Generate 3 Cypher statement and fetch data
example_generated_cyphers = []
example_eval_datas = []
for _ in range(3):
cypher = cypher_chain.invoke({"question": row["question"]})
example_generated_cyphers.append(cypher)
# Fetch data based on the generated Cypher statement
try:
example_eval_datas.append(graph.query(cypher))
except ValueError: # Handle syntax error
example_eval_datas.append([{"id": "Cypher syntax error"}])
# These metrics require only the first cypher/response
jaro_winkler = get_jw_distance(row["cypher"], example_generated_cyphers[0])
pass_1 = (
1
if df_sim_pair(
(row["cypher"], true_data),
(example_generated_cyphers[0], example_eval_datas[0]),
)
== 1
else 0
)
jaccard = df_sim_pair(
(row["cypher"], true_data),
(example_generated_cyphers[0], example_eval_datas[0]),
)
# Pass@3 check all 3 responses
pass_3 = 1 if any(
df_sim_pair((row["cypher"], true_data), (gen_cypher, eval_data)) == 1
for gen_cypher, eval_data in zip(example_generated_cyphers, example_eval_datas)
) else 0
# Append the results to their respective lists
generated_cyphers.append(example_generated_cyphers)
true_datas.append(true_data)
eval_datas.append(example_eval_datas)
jaro_winklers.append(jaro_winkler)
pass_1s.append(pass_1)
pass_3s.append(pass_3)
jaccards.append(jaccard)
运行此代码大约花费了 5 分钟,因为我们需要生成 81 个响应来计算 pass@3 度量。
代码稍长,但其核心概念相当简单。我们遍历存储测试示例的数据框中的所有行。接下来,我们为每个训练示例生成三个 Cypher 查询,并从数据库中检索相应的数据。接下来就是计算相关的度量标准并将其存储在列表中,以便我们可以评估并可视化它们。我们没有在博客文章中包括辅助函数,因为我认为逐个审查每个度量标准的代码实现没有必要。然而,所有这些函数都包含在笔记本中。
现在让我们评估结果。

LLM 生成的 Cypher 语句评估。图片由作者提供。
评估基于四种不同的度量标准:
-
Jaro-Winkler:该度量显示了 0.89 的高平均值,表明 LLM 生成的 Cypher 查询在字符串层面上与正确的 Cypher 查询非常相似。
-
Pass@1:此处的平均得分为 0.48,表明当每个查询独立评估时,几乎有一半生成的 Cypher 查询返回与正确查询完全相同的结果。
-
Pass@3:该度量的平均值为 0.63,表明相较于 Pass@1 有所提高。这意味着虽然 LLM 在第一次尝试时可能不会生成正确的查询,但通常会在三次尝试内生成一个正确版本。
-
Jaccard 相似度:平均得分为 0.53,是所有指标中最低的,但仍然表明大多数情况下,LLM 生成的 Cypher 查询的结果集与正确查询的结果集共享超过一半的元素。
总体而言,这些指标表明 LLM 在生成与正确查询相似的 Cypher 查询方面表现不错,并且通常能够生成功能等效的结果,尤其是在多次尝试后。然而,仍然有改进的空间,特别是在第一次尝试时生成正确查询的能力方面。此外,评估过程本身也有改进的余地。让我们来看一个例子:
row = df.iloc[24]
# Print the desired information
print("Question:", row["question"], "\n")
print("True Cypher:", row["cypher"], "\n")
print("Generated Cypher", row["generated_cypher"][0], "\n")
# Question: Which directors have never had a movie with a rating below 6.0?
# True Cypher:
# MATCH (d:Director)-[:DIRECTED]->(m:Movie)
# WITH d, MIN(m.imdbRating) AS lowestRating WHERE lowestRating >= 6.0
# RETURN d.name, lowestRating
# Generated Cypher
# MATCH (d:Director)-[:DIRECTED]->(m:Movie)
# WHERE NOT EXISTS {
# MATCH (d)-[:DIRECTED]->(m2:Movie)
# WHERE m2.imdbRating < 6.0
# }
# RETURN DISTINCT d.name
对于问题“哪些导演从未拍过评分低于 6.0 的电影”,LLM 在获取结果方面表现不错。它使用了与测试数据集不同的方法,但这不是问题,因为它应该得到相同的结果。然而,我们在测试数据中返回了电影的标题和评分。另一方面,LLM 只返回了标题,而没有评分。我们不能责怪它,因为它只是按照指令执行的。然而,你必须知道,在这个例子中,Pass@1 得分为 0,而 Jaccard 相似度仅为 0.5。因此,在构建测试数据集时,你必须非常小心,既要注意如何定义提示,也要注意相应的 Cypher 语句。
LLM 的另一个特点是它们是非确定性的,这意味着每次运行可能会得到不同的结果。现在我们将连续运行三次评估。这项评估大约需要 15 分钟。

对 LLM 生成的 Cypher 语句的评估。图片来自作者。
条形图突出显示了 LLM 的非确定性特性。Jaro-Winkler 分数在所有运行中始终较高,波动幅度在 0.88 和 0.89 之间,表明生成的查询在字符串相似度上保持稳定。然而,对于 Pass@1,有明显的变化,第一次运行得分为 0.52,后续运行得分分别为 0.59 和 0.48。Pass@3 得分变化较小,约为 0.56 到 0.63 之间,表明多次尝试能够获得更一致的正确结果。
结论
通过这篇博客文章,我们了解到像 GPT-4 这样的 LLM 在生成 Cypher 查询方面有着很有前景的能力,但该技术并非万无一失。所展示的评估框架提供了 LLM 性能的详细定量评估,允许你不断地进行实验,并更新提示工程及生成有效准确的 Cypher 语句所需的其他步骤。此外,它还展示了 LLM 的非确定性特性如何影响不同评估之间的表现。因此,你可以预期在生产环境中会遇到类似的非确定性行为。
代码可在GitHub上找到。
数据集
F. Maxwell Harper 和 Joseph A. Konstan. 2015 年. 《MovieLens 数据集:历史与背景》. 《ACM 互动智能系统学报》(TiiS)5 卷,4 期:19:1–19:19. doi.org/10.1145/2827872




浙公网安备 33010602011771号