TowardsDataScience-2023-博客中文翻译-二十六-

TowardsDataScience 2023 博客中文翻译(二十六)

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

如果我从头开始,如何用 ChatGPT 学习编程?

原文:towardsdatascience.com/how-would-i-learn-to-code-with-chatgpt-if-i-had-to-start-again-12f2f36e4383?source=collection_archive---------1-----------------------#2023-12-31

探索 ChatGPT 在我 15 年的编程学习旅程中的应用——超越复制粘贴

Livia Ellen数据科学前沿 Livia Ellen

·

关注 发表在 数据科学前沿 ·9 分钟阅读·2023 年 12 月 31 日

--

图片由 Arnold Francisca 提供,来源于 Unsplash

编程一直是我生活的一部分,从 10 岁时修改 HTML 和 CSS 以装饰我的 Friendster 个人主页,到探索 SQL 注入的刺激,制作一个三脚机器人以寻求乐趣,最近又投入 Python 编程,我的编程旅程丰富多彩且充满乐趣!

这是我从各种编程方法中学到的东西。

我学习编码的方式总是很类似;正如人们所说,通常只是复制粘贴。 😅

在编码世界中构建某些东西时,这是我方法的详细分解:

  1. 选择合适的框架或库

  2. 从过去的项目中学习

  3. 将其拆分为步骤 将项目分解为可操作的步骤,使开发不那么令人不知所措。

  4. 每一块都谷歌一下 对于每一步,咨询 Google/Bing/DuckDuckGo/任何你喜欢的搜索引擎,以获取见解、指导和潜在解决方案。

  5. 开始编码

    尝试系统地实施每一步。

然而,即使是最精心设计的代码也可能遇到 bug。以下是我解决故障的策略:

1. 查看框架文档: 始终阅读文档!

2. 谷歌和 Stack Overflow 搜索:在 Google 和 Stack Overflow 上搜索。示例关键词如下:

site:stackoverflow.com [coding language] [library] error [error message]

site:stackoverflow.com python error ImportError: pandas module not found

  • Stack Overflow 解决方案:如果问题已经在 Stack Overflow 上,我会寻找点赞数最高的评论和解决方案,通常能找到快速而可靠的答案。

  • 相信我的直觉:当 Stack Overflow 没有答案时,我相信我的直觉去 Google 搜索可信的来源;GeeksForGeeks、Kaggle、W3School 和 Towards Data Science 等数据科学相关的资源 😉

3. 复制粘贴代码解决方案

4. 验证和测试:最后一步包括彻底检查修改后的代码并测试,以确保其按预期运行。

就这样,你解决了 bug!

照片由 Stephen Hocking 提供,来源于 Unsplash

这不是很美吗?

但实际上,我们还在这样做吗?!

最近,我注意到新编码人员处理编码的方式发生了变化。我已经专业教授编码大约三年了,曾在编码训练营中四处活动,并在大学和公司培训中担任客座讲师。编码人员进入编码学习的方式有所改变。

我通常告诉新人坚持使用旧式的方法浏览和谷歌搜索答案,但人们最终仍然会使用 ChatGPT。他们的借口是

“拥有 ChatGPT(用于编码)就像有一个额外的学习伙伴——他像普通人一样和你聊天。”

这很有用,尤其是在你还在从搜索结果和文档中理清思路时——以发展所谓的 程序员直觉

现在,不要误解我的意思,我完全支持基础知识。浏览、阅读文档以及向社区提问——这些在我看来都是可靠的做法。仅仅依赖 ChatGPT 可能有些过于单一。确实,它可以快速提供答案的总结,但传统的浏览方法给你提供了选择和实验的自由,这在编程世界中是非常关键的。

不过,我必须给予应有的赞扬——ChatGPT 在提供答案方面速度极快,特别是当你还在试图分辨搜索结果和文档中的对错时。

我意识到使用 ChatGPT 作为学习伙伴的转变不仅发生在编程领域,ChatGPT 已经彻底改变了人们的学习方式,我甚至用 ChatGPT 来修正这篇文章的语法,对不起 Grammarly。

拒绝 ChatGPT 就像拒绝 2000 年代初的搜索引擎一样。虽然 ChatGPT 可能会有偏见和虚假信息,类似于搜索引擎中的不可靠信息或虚假信息。但当 ChatGPT 使用得当时,它可以加快学习过程。

现在,让我们设想一个真实的场景,ChatGPT 作为你的编程伙伴来帮助调试。

场景:调试 Python 脚本

想象一下,你正在为一个项目编写 Python 脚本,遇到了一个你无法解决的意外错误。

这是我以前被教导的方法——在 ChatGPT 之前的时代。

浏览方法:

  1. 检查文档:

从检查导致错误的模块或函数的 Python 文档开始。

比如:

2. 在 Google 和 Stack Overflow 上搜索:

如果文档没有提供解决方案,你可以转向 Google 和 Stack Overflow。浏览各种论坛线程和讨论,找到类似的问题及其解决方案。

StackOverflow 线程

3. 相信你的直觉:

如果问题独特或文档不足,信任你的直觉!你可以查找你过去认为可信的 Google 文章和资源,并尝试将类似的解决方案应用到你的问题上。

Google 搜索结果

你可以看到在上述搜索结果中,结果来自 W3school - (受信任的编码教程网站,非常适合速查表)以及其他 2 个结果是官方 Pandas 文档。你可以看到搜索引擎确实建议用户查看官方文档。 😉

这就是你如何利用 ChatGPT 来帮助你调试问题。

使用 ChatGPT 的新方法:

  1. 与 ChatGPT 进行对话:

除了浏览文档和论坛,你还可以与 ChatGPT 进行对话。简洁地描述错误并提问。例如,

“我在 [编程语言] 脚本中遇到了一个问题,描述 [错误]。你能帮我理解可能的原因并建议可能的解决方案吗?”

与 ChatGPT 进行对话

2. 用 ChatGPT 澄清概念:

如果错误与一个你难以掌握的概念有关,你可以让 ChatGPT 解释该概念。例如,

“解释一下 [特定概念] 在 [编程语言] 中是如何工作的?我认为这可能与我面临的错误有关。错误是:[错误]”

用 ChatGPT 澄清概念

3. 寻求故障排除建议:

你向 ChatGPT 询问 Python 脚本的常规故障排除技巧。例如,

“处理 [问题] 的一些常见策略是什么?对工具或技术有何建议?”

使用 ChatGPT 作为编码伙伴

潜在优势:

  • 个性化指导: ChatGPT 可以根据你提供的关于错误和对问题理解的具体细节提供个性化的指导。

  • 概念澄清: 你可以直接通过 ChatGPT 寻求对概念的解释和澄清,利用其 LLM 功能。

  • 高效故障排除: ChatGPT 可能提供简明且相关的故障排除技巧,潜在地简化调试过程。

可能的限制:

现在让我们讨论完全依赖 ChatGPT 的缺点。我在学生使用 ChatGPT 的过程中看到这些问题。在 ChatGPT 时代后,我的学生只是复制并粘贴了他们命令行界面的 1 行错误消息,尽管错误有 100 行并且与一些模块和依赖项有关。要求 ChatGPT 通过提供 1 行错误代码来解释解决方法可能有时有效,或者更糟糕的情况是 — 可能增加 1 到 2 小时的调试时间。

ChatGPT 有一个限制,就是不能看到你代码的上下文。你可以始终提供代码的上下文。在更复杂的代码中,你可能无法将每一行代码都提供给 ChatGPT。由于 ChatGPT 只能看到你代码的一小部分,它将根据其知识库假设其余代码,或产生幻觉

这些是使用 ChatGPT 可能的限制:

  • 缺乏实时动态互动: 尽管 ChatGPT 提供了有价值的见解,但它缺乏实时互动和动态的来回讨论,这些是论坛或讨论线程可能提供的。在 StackOverflow 上,你可能会有 10 个人建议 3 种不同的解决方案,你可以通过 DIY(自己动手做)或查看点赞数来进行比较。

  • 依赖过去的知识: ChatGPT 的回答质量取决于其训练过的信息,它可能不了解最新的框架更新或你项目的具体细节。

  • 可能会增加调试时间: ChatGPT 不了解你的完整代码,因此可能会导致更多的调试时间。

  • 对概念的理解有限: 传统的浏览方法让你可以自由选择和尝试,这在编码世界中非常重要。如果你知道如何挑选正确的来源,你可能会比依赖 ChatGPT 通用模型学到更多。

    除非你询问一个专门针对编码和技术概念的语言模型——如编码材料的研究论文,或者 Andrew Ng 的深度学习讲座,Yann Le Cunn 在 X(前身为 Twitter)上的推文,否则 ChatGPT 通常会给出一般性的答案。

这种情况展示了 ChatGPT 如何成为你编码工具包中的一个有价值的工具,特别是在获取个性化指导和澄清概念方面。记住要平衡 ChatGPT 的帮助与浏览方法,并向社区请教,同时注意它的优势和局限性。

最终想法

我对编码人员的建议

如果你真的想利用自动补全模型;除了单独使用 ChatGPT,还可以尝试使用 VScode 扩展进行自动代码补全任务,例如 CodeGPT — GPT4 扩展在 VScodeGitHub Copilot 或 Google Colab 中的自动补全 AI 工具。

Google Colab 中的自动代码补全

如上图所示,Google Colab 会自动给用户建议下一步的代码。

另一种选择是 GitHub Copilot。通过 GitHub Copilot,你可以实时获得基于 AI 的建议。GitHub Copilot 在开发者输入时建议代码补全,并根据项目的上下文和风格规范将提示转换为编码建议。根据这篇 Github 发布说明,Copilot Chat 现在由 OpenAI GPT-4 提供支持(与 ChatGPT 使用的模型类似)。

GitHub Copilot 示例 — 图片由 GitHub 提供

在了解到 GitHub Copilot 如果你在教育项目中可以免费使用之前,我一直积极使用 CodeGPT 作为 VSCode 扩展。到目前为止,CodeGPT Co 在 VSCode 扩展市场上的下载量已达 100 万次。CodeGPT 允许与 ChatGPT API、Google PaLM 2 和 Meta Llama 无缝集成。

你可以通过评论获取代码建议,以下是方法:

  • 写一个评论请求特定的代码

  • cmd + shift + i

  • 使用代码 😎

你还可以通过菜单中的扩展启动聊天,深入讨论编码 💬

当我回顾自己的编码历程时,学到的宝贵教训是没有一种放之四海而皆准的学习方法。拥抱多样的学习方式,将传统的浏览和社区互动与 ChatGPT 和自动代码补全工具等创新能力无缝结合是至关重要的。

如何做:

  • 利用量身定制的学习资源: 充分利用 ChatGPT 对学习材料的推荐。

  • 与 ChatGPT 合作解决问题: 将 ChatGPT 作为一个合作伙伴,像与朋友一起编码一样利用它。

注意事项:

  • 过度依赖 ChatGPT: 避免仅依赖 ChatGPT,并确保采取平衡的方法来培养独立解决问题的能力。

  • 忽略与编码社区的实时互动: 尽管 ChatGPT 提供了宝贵的见解,但不要忽视与编码社区实时互动和反馈的好处。这也有助于在社区中建立声誉。

  • 忽视实际编码实践: 平衡 ChatGPT 的指导与动手编码实践,以将理论知识与实际应用相结合。

在评论中告诉我你如何利用 ChatGPT 来帮助你编码!

编码愉快!

艾伦

联系我

  • 🌐 在 LinkedIn 和 Medium 上关注我

  • 📬 免费订阅我的 Medium 时事通讯,获取邮件更新!

  • 🚀 如果你觉得这篇文章有用,请 点赞、保存到阅读列表、分享评论

  • 👏 嗨!你可以在一篇文章中 点赞多于一个(最多 50 次) — 这将帮助我获得一杯咖啡,以便为即将写的文章做好准备 😉

  • ☕ 或者 请给我买一杯真正的咖啡❤ — 是的,我喜欢咖啡。

如何通过 Python 访问 Amazon S3 资源(及其必要性)

原文:towardsdatascience.com/how-you-can-and-why-you-should-access-amazon-s3-resources-with-python-86462c6a8f97

使用自动化将数据移动到云端和从云端移出

Aashish NairTowards Data Science Aashish Nair

·发表于 Towards Data Science ·阅读时间 7 分钟·2023 年 1 月 24 日

--

Lia Trevarthen 拍摄的照片,来源于 Unsplash

亚马逊简单存储服务(S3)为用户提供了廉价、安全且易于管理的存储基础设施。虽然可以通过 AWS 控制台直接移动文件进出 S3 桶,但 AWS 也提供了通过代码简化这些操作的选项。

对于 Python,AWS 软件开发工具包(SDK)提供了 boto3,允许用户通过代码创建、配置和利用 S3 桶及对象。在这里,我们将深入探讨基本的 boto3 功能,并考虑如何利用它们来自动化操作。

为什么选择 Boto3?

你可能会想,既然可以通过 AWS 控制台访问 S3 服务,学习使用另一个工具是否还有意义。的确,借助 AWS 简单且用户友好的用户界面(UI),将文件移动到 S3 桶以及从 S3 桶中移出很容易。

然而,当操作需要扩展时,典型的点击和拖放方法就不可行了。处理 1-10 个文件是一回事,但处理 100 或 1000 个文件呢?如果手动操作,这样的任务自然会耗时。

此外,手动任务还使用户容易出错。当手动移动大量数据时,你能保证不会错误地遗漏或包括错误的文件吗?

那些重视效率或一致性的人无疑会看到使用 Python 脚本自动化 S3 资源的优点。

前提条件

让我们从使用 boto3 的前提条件开始。

1. 安装

要使用 boto3,你需要使用以下命令安装 AWS SDK(如果尚未安装)。

pip install boto3

2. IAM 用户

您还需要一个具有使用 S3 资源权限的 IAM 用户账户。

要获取用户身份,请使用 root 用户账户登录 AWS。前往身份与访问管理(IAM)部分,添加一个新用户。为该用户身份分配一个将授予 S3 资源访问权限的策略。最简单的选项是选择“AmazonS3FullAccess”权限策略,但您可以找到或创建更符合您需求的策略。

AmazonS3FullAccess 策略(作者创建)

选择策略后,完成其余提示并创建用户身份。您应该能够在控制台中看到您新创建的身份。在此示例中,我们使用名为“s3_demo”的用户身份。

接下来,点击用户名(而不是复选框),然后转到安全凭证选项卡。在访问密钥部分,选择“创建访问密钥”并回答所有提示。然后,您将收到您的访问密钥和秘密访问密钥。

这些密钥是使用 boto3 访问 S3 资源所必需的,因此它们将自然地被纳入 Python 脚本中。

基本命令

Boto3 包含可以配置和管理各种 AWS 资源的函数,但本文的重点将放在处理 S3 桶和对象上。boto3 的关键好处是,它可以用简单的一行代码来执行如上传和下载文件等任务。

创建客户端

要使用 boto3(或任何 AWS 资源)访问 S3 资源,首先需要创建一个低级服务客户端。

创建客户端需要输入服务名称、区域名称、访问密钥和秘密访问密钥。

创建桶

让我们开始创建一个名为“a-random-bucket-2023”的桶。这可以通过create_bucket函数实现。

如果您刷新 AWS 控制台上的 S3 部分,您应该能看到新创建的桶。

创建桶(作者创建)

列出桶

要列出可用的桶,用户可以使用list_buckets函数。这会返回一个包含许多键值对的字典。要查看仅桶的名称,请检索字典中‘Buckets’键的值。

代码输出(作者创建)

将文件上传到桶

可以使用upload_file函数上传文件。在这种情况下,我们将“mock_data.xlsx”上传到桶中。

值得区分FilenameKey参数之间的差异。Filename指的是正在传输的文件的名称,而Key指的是分配给存储在桶中的对象的名称。

由于“First Mock Data.xlsx”被分配给了Key参数,所以这将是上传到桶中时对象的名称。

添加对象(由作者创建)

将 Pandas 数据框上传到存储桶

由于我们正在使用 Python,因此值得了解如何直接将 Pandas 数据框上传到存储桶。这可以通过 io 模块实现。在以下代码片段中,我们将数据框“df”上传到存储桶。

上传 Pandas 数据框(由作者创建)

列出对象

可以使用 list_objects 函数列出给定存储桶中的对象。

函数本身的输出是一个包含大量元数据的大字典,但可以通过访问“Contents”键找到对象名称。

代码输出(由作者创建)

下载文件

可以使用 download_file 函数从存储桶中下载对象。

删除对象

可以使用 deleted_object 函数删除对象。

删除对象(由作者创建)

删除存储桶

最后,用户可以使用 delete_bucket 函数删除存储桶。

删除存储桶(由作者创建)

案例研究

我们已经探索了一些用于使用 S3 资源的基本 boto3 函数。然而,进行案例研究是展示 boto3 为什么是如此强大工具的最佳方式。

问题陈述 1: 我们对不同日期出版的书籍感兴趣。使用 NYT Books API 获取出版书籍的数据并将其上传到 S3 存储桶。

我们再次开始创建一个低级服务客户端。

接下来,我们创建一个存储所有这些文件的存储桶。

创建存储桶(由作者创建)

接下来,我们需要使用 NYT Books API 拉取数据并将其上传到存储桶。我们可以使用以下 get_data 函数进行指定日期的数据拉取:

以下是 get_data 函数输出的预览:

代码输出(由作者创建)

有许多方法可以利用这个函数进行数据收集。一个选项是每天运行这个函数并将输出上传到 S3 存储桶(或使用作业调度程序)。另一个选项是使用循环收集多个日期出版的书籍数据。

如果我们对过去 7 天内出版的书籍感兴趣,可以使用循环逐天解析。对于每一天,我们:

  • 使用 API 进行调用

  • 将响应存储到数据框中

  • 将数据框上传到存储桶

这些步骤可以通过以下代码片段执行:

上传文件到存储桶(由作者创建)

只需几行代码,我们就能进行多次 API 调用,并将响应一次性上传到存储桶!

在某些时候,可能需要从上传到桶中的数据中提取一些见解。数据分析师通常会收到有关他们收集的数据的临时请求。

问题陈述 2: 查找所有日期排名最高的书籍的书名、作者和出版社,并将结果存储到本地。

使用 Python,我们可以将多个 S3 对象存储到 Pandas 数据框中,处理这些数据框,并将输出加载到平面文件中。

使用 boto3 有两个主要优点,这在这个案例研究中得到了展示。第一个优点是它具有良好的可扩展性;上传/下载 1 个文件和 1000 个文件所需的时间和精力差异可以忽略不计。第二个优点是,它允许用户在将数据移动到或从 S3 桶时,无缝地结合其他过程(例如数据收集、过滤)。

结论

图片由Prateek Katyal拍摄,来源于Unsplash

希望这份简要的 boto3 入门指南不仅向你介绍了管理 S3 资源的基本 Python 命令,还展示了它们如何用于自动化过程。

使用 AWS SDK for Python,用户将能够更高效、一致地在云端移动数据。即使你现在对使用 AWS 的 UI 进行资源配置和利用感到满意,你也无疑会遇到可扩展性优先的情况。了解 boto3 的基础知识将确保你为这些情况做好充分准备。

感谢阅读!

如何(以及为什么)保护您的 API 密钥

原文:towardsdatascience.com/how-you-can-and-why-you-should-secure-your-api-keys-e433acc2f22d

保护 API 密钥的简单最佳实践

Aashish NairTowards Data Science Aashish Nair

·发布于Towards Data Science ·阅读时间 4 分钟·2023 年 3 月 7 日

--

照片由regularguy.eth拍摄,发布于Unsplash

API 密钥在识别发出请求的应用程序中扮演着重要角色。它们是一种安全措施,确保未经身份验证的人员无法访问特定服务器上的信息。然而,如果这些密钥没有得到充分保护,外部人员很容易获取这些密钥并造成实际损害。

为避免这种结果,企业可能会聘请 IT 管理员,他们负责使用高级工具(例如 AWS Secrets Manager)为多个人员和项目存储、管理和轮换 API 密钥。

然而,由于共享/泄露 API 密钥的严重后果,保护它们的责任不应仅仅落在管理员身上;每位团队成员都应理解保护 API 密钥安全的重要性。

在这里,我们探讨了泄露 API 密钥可能造成的损害,并探索了保护它们的简单最佳实践。

共享 API 密钥的后果

未能保护 API 密钥会带来多种危害。

首先,这会导致个人或企业为他们甚至未使用的 API 调用产生费用。一位著名的受害者是 DevFactor 创始人 Andrew Hoffman,他在 GitHub 上意外发布了他的 AWS 密钥后,收到了$2,375 的账单! 不幸的是,公共空间中有爬虫机器人,因此即使短时间内泄露 API 密钥也可能是一个代价高昂的错误。

其次,获取这些密钥的攻击者可以访问和利用 API 可访问的任何信息。这可以从网络犯罪分子使用泄露的加密货币交易所 API 密钥来盗取受害者账户中的加密货币中看出。

最终,这会增加应用程序遭受恶意活动(如 DDoS 攻击)的可能性,攻击者通过向服务器发送虚假流量来使其崩溃。

幸运的是,这些不良结果可以通过一些细心和谨慎来防止,以下提示可以帮助实现。

提示 1 - 避免直接在代码中存储 API 密钥

存储 API 密钥的最简单方法是将它们直接嵌入到发起 API 调用的程序中,但这会使读者立即获得相同的 API 访问权限。

更可取的替代方案是将其存储为环境变量。环境变量本质上是可以在操作系统中定义的对象,独立于应用程序。

在 Windows 上,可以通过打开“设置”窗口,选择“系统”,“高级系统设置”,“环境变量”,然后点击“新建”来创建新的环境变量。

创建新变量(由作者创建)

另一种选择是使用代码创建环境变量。例如,以下是如何使用 Python 的 os 库创建环境变量的示例。

存储环境变量后,可以通过代码轻松访问它们。

代码输出(由作者创建)

提示 2 - 避免将 API 密钥上传到代码库

上传项目到代码库时,必须确保所有包含 API 密钥的文件不会被包括在上传中。

最简单的方法是使用 .gitignore 文件。 .gitignore 文件告诉 git 在将项目推送到代码库时应该故意忽略哪些文件。

要创建 .gitignore 文件,只需在 git bash 中输入以下命令:

touch .gitignore

另外,可以创建一个文本文件并将其命名为“.gitignore”。

要忽略包含 API 密钥的文件,只需将其文件路径输入到 .gitignore 文件中。

提示 3 - 旋转 API 密钥

最终,即使是经验丰富的专业人员也可能因为粗心大意而暴露 API 密钥。为纠正这种错误,当密钥被泄露(或被怀疑泄露)时,可以通过旋转 API 密钥(即用新生成的密钥替换旧密钥)来解决。

旋转 API 密钥可以减少密钥泄露给其他方的风险。作为安全措施,一些团队即使在没有泄露的情况下也会定期更换密钥(例如,每 90 天一次)。旋转 API 密钥的最佳方法取决于具体的 API,因此没有一刀切的方法来实现。

注意:在更换密钥时,请确保应用程序将使用新密钥进行 API 调用,而不是过时的密钥。

提示 4 - 删除不需要的 API 密钥

最后,当某些 API 密钥不再需要时,最好彻底删除它们。

这个步骤应在确认 API 密钥不再被应用程序使用(即仍在生产中)后完成。

结论

照片由Prateek Katyal提供,来源于Unsplash

鉴于 API 密钥在项目中的重要性,团队可能会包括负责管理 API 密钥的人员。然而,API 密钥的泄露可能发生在新手和资深专业人士之间。

因此,每个成员应该了解 API 密钥泄露的容易程度、可以用来防止泄露的方法,以及可以用来修正泄露的解决方案。

感谢阅读!

如何验证机器学习模型

原文:towardsdatascience.com/how-you-should-validate-machine-learning-models-f16e9f8a8f7a?source=collection_archive---------5-----------------------#2023-07-21

学会建立对你机器学习解决方案的信任

Patryk Miziuła, PhDTowards Data Science Patryk Miziuła, PhD

·

关注 发布于 Towards Data Science ·14 min read·2023 年 7 月 21 日

--

www.shutterstock.com/image-photo/desert-island-palm-tree-on-beach-71305345

大型语言模型已经在很大程度上改变了数据科学行业。其中一个最大的优势是对于大多数应用,它们可以直接使用——我们不必自己训练它们。这要求我们重新审视一些关于整个机器学习过程的常见假设——许多从业者认为验证是“训练的一部分”,这意味着它不再需要。我们希望读者对验证是否过时的建议略感震惊——它绝对不是。

在这里,我们深入探讨模型验证和测试的基本概念。如果你认为自己在机器学习的基础知识上已经非常熟练,你可以跳过这篇文章。否则,请系好安全带——我们有一些离奇的情境让你暂时放下怀疑。

这篇文章是Patryk Miziuła 博士Jan Kanty Milczek的共同工作。

在荒岛上学习

想象一下,你想教某人识别推特上的语言。于是你把他带到一个荒岛上,给他 100 条推文,告诉他每条推文的语言,然后让他独自待几天。之后,你回到岛上检查他是否真的学会了识别语言。但是你怎么检查呢?

你的第一反应可能是问他关于他获得的推文的语言。所以你这样挑战他,他回答了所有 100 条推文的语言。这样真的意味着他能够总体上识别语言吗?可能是,但也许他只是记住了这 100 条推文!而且你无法知道哪种情况是真的!

在这里,你没有检查你想要检查的内容。根据这样的检查,你根本无法知道在生死攸关的情况下(这些情况通常涉及到荒岛时),你能否依赖他的推文语言识别技能。

我们应该怎么做呢?如何确保他学会了,而不是仅仅记住了?再给他 50 条推文,让他告诉你这些推文的语言!如果他答对了,他确实能识别语言。但是如果他完全失败了,你就知道他只是把前 100 条推文背下来了——这不是整个事情的重点。

那么这些东西如何与机器学习模型相关联呢?

上述故事形象地描述了机器学习模型是如何学习的,以及我们应该如何检查它们的质量:

  • 故事中的人代表了机器学习模型。要将一个人从世界中隔离开来,你需要把他带到一个荒岛上。对于机器学习模型来说,这更简单——它只是一个计算机程序,所以它本身不理解世界的概念。

  • 识别推文的语言是一个分类任务,涉及 10 个可能的类别,也就是种类,因为我们选择了 10 种语言。

  • 前 100 条用于学习的推文称为训练集。附加的正确语言称为标签

  • 另外 50 条推文仅用于考察该人/模型,称为测试集。注意,我们知道它的标签,但该人/模型并不知道。

下图展示了如何正确地训练和测试模型:

图 1:模型训练和测试的方案。图像由作者提供。

所以主要规则是:

在与训练数据不同的数据上测试机器学习模型。

如果模型在训练集上表现良好,但在测试集上表现不佳,我们称模型为过拟合。“过拟合”意味着记住训练数据。这绝不是我们想要实现的目标。我们的目标是拥有一个训练好的模型——在训练集和测试集上表现都好。只有这种模型才能被信任。只有这样,我们才能相信它在为之构建的最终应用中会像在测试集上表现一样好。

现在让我们更进一步。

1000 个人在 1000 个荒岛上

想象一下你非常非常想教一个人识别 Twitter 上推文的语言。因此你找到了 1000 个候选人,把每个人送到一个不同的荒岛上,给每个人相同的 100 条推文,涵盖 10 种语言,告诉每个人每条推文的语言,然后把他们全部放在一起几天。之后,你用相同的 50 条不同推文来检查每个候选人。

你会选择哪个候选人?当然,是在 50 条推文上表现最好的那个。但他真的有多好呢?我们是否真的相信他在最终应用中能像在这 50 条推文上表现一样好?

答案是否定的!为什么呢?简单来说,如果每个候选人知道一些答案并猜测其他答案,那么你应该选择那些答对最多的人,而不是那些知道最多的人。他确实是最好的候选人,但他的结果被“幸运猜测”所膨胀。这可能是他被选择的一个重要原因。

为了以数字形式展示这种现象,假设 47 条推文对所有候选人都很简单,但剩下的 3 条消息对所有竞争者来说都非常困难,以至于他们都只是盲目猜测语言。概率说,某人(可能是多个人)全部猜对这 3 条困难推文的概率超过 63%(数学迷的信息:几乎是 1–1/e)。所以你可能会选择得分完美的人,但实际上他并不完全适合你的需求。

也许我们例子中的 50 条推文中有 3 条听起来并不惊人,但在许多实际案例中,这种差异往往更加明显。

那么我们如何检查获胜者实际上有多好呢?是的,我们需要再获取一组 50 条推文,并再次检查!只有这样,我们才能得到一个值得信赖的评分。这种准确度是我们期望最终应用达到的水平。

让我们回到机器学习术语

从名字上看:

  • 第一组 100 条推文现在仍然是训练集,因为我们用它来训练模型。

  • 但现在第二组 50 条推文的目的发生了变化。这一次它被用来比较不同的模型。这样的集合被称为验证集

  • 我们已经理解了,在验证集上检查到的最佳模型结果是被人为提升的。这就是为什么我们需要另外一组 50 条推文来充当测试集,为我们提供有关最佳模型质量的可靠信息。

你可以在下面的图片中找到使用训练集、验证集和测试集的流程:

图片 2:正确训练、验证和测试模型的示意图。图片由作者提供。

好了,我们为什么使用 100 条、50 条和 50 条推文的集合呢?

这里是这些数字背后的两个基本理念:

将尽可能多的数据放入训练集。

我们拥有的训练数据越多,模型的视野就越广,训练的机会就越大,过拟合的可能性就越小。唯一的限制应是数据的可用性和处理数据的成本。

将尽可能少的数据放入验证集和测试集中,但要确保它们足够大。

为什么?因为你不想浪费太多数据在训练之外。但是另一方面,你可能觉得仅凭一条推文来评估模型是有风险的。所以你需要一组足够大的推文集合,以免在遇到少量非常奇怪的推文时担心评分波动。

那么如何将这两个指导方针转换成具体的数字呢?如果你有 200 条推文,那么 100/50/50 的划分似乎是合适的,因为它遵循了上述两个规则。但是,如果你有 1,000,000 条推文,你可以很容易地将其划分为 800,000/100,000/100,000,甚至 900,000/50,000/50,000。也许你看到过一些百分比提示,比如 60%/20%/20%之类的。不过,它们只是上述两个主要规则的过度简化,所以最好还是坚持原始指导方针。

好的,但如何选择哪些推文进入训练集/验证集/测试集呢?

我们相信这个主要规则现在对你来说很清楚了:

使用三种不同的数据来训练、验证和测试模型。

如果这个规则被打破会怎样?如果相同或几乎相同的数据,无论是因为意外还是没有注意,进入了三个数据集中中的一个以上,这就是我们所说的数据泄露。验证集和测试集就不再可信了。我们无法判断模型是被训练还是过拟合。我们根本无法信任这个模型。这不好。

也许你认为这些问题与我们的荒岛故事无关。我们只是取 100 条推文用于训练,再取 50 条用于验证,再取 50 条用于测试,仅此而已。不幸的是,事情并非如此简单。我们必须非常小心。让我们来看一些例子。

示例 1:许多随机推文

假设你从 Twitter 上抓取了 1,000,000 条完全随机的推文。不同的作者、时间、主题、定位、反应数量等等。完全随机。而且这些推文有 10 种语言,你想用它们来训练模型识别语言。那么你不用担心任何问题,你可以简单地将 900,000 条推文用于训练集,50,000 条用于验证集,50,000 条用于测试集。这被称为随机分割

为什么要随机抽取,而不是将 900,000 条推文放入训练集,将接下来的 50,000 条推文放入验证集,将最后的 50,000 条推文放入测试集?因为推文最初可能会以对我们无益的方式排序,例如按字母顺序或按字符数量排序。我们并不希望仅将以‘Z’开头或最长的推文放入测试集,对吧?所以随机抽取会更安全。

图像 3:随机数据分割。图片来源于作者。

假设推文是完全随机的,这个假设是强的。总是要仔细考虑这是否真实。在接下来的示例中,你将看到如果不完全随机会发生什么。

示例 2:不是那么多随机推文

如果我们只有 200 条完全随机的推文在 10 种语言中,我们仍然可以随机分割它们。但这时就会出现新的风险。假设某种语言占主导地位,有 128 条推文,而其他 9 种语言每种语言只有 8 条推文。概率表明,所有语言都进入 50 个测试集的可能性超过 61%(数学爱好者的说明:使用包容-排除原理)。但我们确实希望在所有 10 种语言上测试模型,所以我们确实需要所有语言都在测试集中。我们应该怎么做?

我们可以按类别绘制推文。因此,取 128 条推文中占主导地位的类别,绘制 64 条用于训练集,32 条用于验证集和 32 条用于测试集。然后对所有其他类别执行相同的操作——每个类别分别绘制 4 条、2 条和 2 条用于训练、验证和测试。这样,你就能形成三个所需大小的集合,每个集合中所有类别的比例相同。这种策略称为分层随机分割

分层随机拆分似乎比普通随机拆分更好/更安全,那么为什么我们在示例 1 中没有使用它呢?因为我们不需要!常常违反直觉的是,如果 1,000,000 条推文中有 5%是英文的,而我们随机抽取 50,000 条推文而不考虑语言,那么抽取的推文中也有 5%是英文的。这就是概率的工作原理。但概率需要足够大的数字才能正常工作,所以如果你有 1,000,000 条推文你就不用担心,但如果只有 200 条,就要小心了。

示例 3:来自几个机构的推文

现在假设我们有 100,000 条推文,但这些推文仅来自 20 个机构(比如一家新闻电视台、一家大型足球俱乐部等),而且每个机构在 10 种语言中运行 10 个 Twitter 账号。我们的目标仍然是识别 Twitter 语言一般性的。我们可以简单地使用随机拆分吗?

你说得对——如果可以,我们就不会提出这个问题了。但为什么不呢?为了理解这一点,我们首先考虑一个更简单的案例:如果我们仅在一个机构的推文上训练、验证和测试模型呢?我们可以将这个模型用于其他机构的推文吗?我们不知道!也许模型会过度拟合这个机构的独特推文风格。我们将没有任何工具来检查这一点!

让我们回到我们的案例。要点是一样的。总共有 20 个机构的数量较少。因此,如果我们使用这 20 个机构的数据来训练、比较和评分模型,那么模型可能会过度拟合这 20 个机构的独特风格,并且在其他作者上表现不佳。再次强调,我们没有方法来检查这一点。这不太好。

那么该怎么办呢?让我们遵循另一个主要规则:

验证集和测试集应尽可能真实地模拟模型应用的实际情况。

现在情况更清楚了。由于我们期望最终应用中的作者与我们数据中的作者不同,因此我们在验证集和测试集中也应包含与训练集中不同的作者!实现这一点的方法是按机构划分数据!如果我们从 10 个机构中抽取用于训练集,从另外 5 个机构中抽取用于验证集,并将最后 5 个机构的数据放入测试集中,问题就解决了。

图像 4:分层数据拆分。图像由作者提供。

请注意,任何较不严格的按机构拆分(例如将 4 个机构的全部数据和其余 16 个机构的一小部分数据放入测试集)都属于数据泄露,这样是不好的,因此我们在分离机构时必须不妥协。

一个令人遗憾的最终说明:对于按机构进行的正确验证拆分,我们可以信任来自不同机构的推文。但来自私人账户的推文可能会——确实会——看起来不同,因此我们不能确定我们拥有的模型对这些推文表现良好。根据我们现有的数据,我们没有工具来检查这一点……

示例 4:相同的推文,不同的目标

示例 3 比较难,但如果你仔细研究过,那么这个就相对容易了。假设我们有与示例 3 完全相同的数据,但这次目标不同。我们现在想识别来自相同 20 个机构的其他推文的语言。随机拆分现在还会合适吗?

答案是:可以。随机拆分完全遵循了上述最后一个主要规则,因为我们最终只对数据中存在的机构感兴趣。

示例 3 和 4 告诉我们,数据拆分的方式不仅仅依赖于我们拥有的数据。它依赖于数据和任务。设计训练/验证/测试拆分时,请牢记这一点。

示例 5:仍然是相同的推文,但目标不同

在最后一个例子中,让我们保留我们拥有的数据,但现在尝试教一个模型预测未来推文中的机构。因此我们再次有一个分类任务,但这次有 20 个类别,因为我们有来自 20 个机构的推文。这个情况下怎么样?我们可以随机拆分数据吗?

和以前一样,让我们先考虑一个更简单的情况。假设我们只有两个机构——一个电视新闻台和一个大型足球俱乐部。他们会发什么推文?两者都喜欢从一个热门话题跳到另一个。三天谈特朗普或梅西,然后三天谈拜登和罗纳尔多,依此类推。显然,在他们的推文中我们可以找到关键词,这些关键词每隔几天就会变化。一个月后我们会看到什么关键词?哪个政治家、恶棍、足球运动员或足球教练会成为“热门”?可能是现在完全未知的。因此,如果你想学会识别机构,你不应该关注暂时的关键词,而是应该尝试捕捉总体风格

好的,让我们回到我们的 20 个机构。之前的观察仍然有效:推文的主题会随时间变化,因此我们希望我们的解决方案能适用于未来的推文,我们不应关注短期的关键词。但机器学习模型是懒惰的。如果它找到了一种简单的方式来完成任务,它不会再进一步查找。坚持使用关键词就是这种简单的方式。那么,我们如何检查模型是否学得正确,而不是仅仅记住了暂时的关键词呢?

我们非常确定你会意识到,如果你使用随机拆分,你应该期待在所有三个集合中都包含关于每个周英雄的推文。这样,你最终会在训练、验证和测试集合中得到相同的关键词。这不是我们想要的。我们需要更聪明地拆分。但怎么做呢?

当我们回到最后一个主要规则时,它变得简单了。我们希望在未来使用我们的解决方案,因此验证集和测试集应该是相对于训练集的未来!我们应该按时间拆分数据。所以如果我们有,比如说,12 个月的数据——从 2022 年 7 月到 2023 年 6 月——那么把 2022 年 7 月到 2023 年 4 月放在测试集中,2023 年 5 月放在验证集中,2023 年 6 月放在测试集中应该就可以了。

图 5:按时间划分的数据。图片由作者提供。

也许你会担心通过时间划分我们无法检查模型在整个季节中的质量。你说得对,这确实是一个问题。但这仍然是一个比随机划分要小的问题。你还可以考虑,例如,以下的划分方式:每月的 1 号到 20 号作为训练集,每月的 20 号到 25 号作为验证集,每月的 25 号到最后一天作为测试集。无论如何,选择一个验证策略是对潜在数据泄漏的权衡。只要你理解这一点并有意识地选择最安全的选项,你就做得很好。

摘要

我们将故事设定在一个沙漠岛屿上,并尽力避免任何复杂情况——将模型验证和测试的问题从所有可能的现实世界因素中隔离出来。即便如此,我们仍然遇到了一次又一次的陷阱。幸运的是,避免这些陷阱的规则很容易学习。正如你可能会在过程中学到的,它们也很难掌握。你不会总是立刻发现数据泄漏,也不总是能够防止它。尽管如此,仔细考虑你的验证方案的可信度一定会带来更好的模型。这一点即使在新模型被发明和新框架被发布时依然相关。

此外,我们有 1000 名男子被困在沙漠岛屿上。一个好的模型可能正是我们需要的,以便及时救援他们。

Hugging Face Diffusers 可以正确加载 LoRA

原文:towardsdatascience.com/hugging-face-diffusers-can-correctly-load-lora-now-a332501342a3

使用最新的 Diffusers Monkey Patching 函数加载 LoRA 与 A1111 比较,结果完全相同

Andrew Zhu (Shudong Zhu)Towards Data Science Andrew Zhu (Shudong Zhu)

·发表于 Towards Data Science ·5 min 阅读·2023 年 7 月 28 日

--

从 Hugging Face 的 Diffusers 代码库中拉取最新代码,并发现与 LoRA 加载相关的最新代码已更新,现在可以进行 Monkey-Patching LoRA 加载。

要安装最新的 Diffusers:

pip install -U git+https://github.com/huggingface/diffusers.git@main

根据我的测试,LoRA 加载功能昨天生成了略有缺陷的结果。本文讨论了如何使用 Diffusers 包中的最新 LoRA 加载器。

加载 LoRA 并更新 Stable Diffusion 模型权重

自从程序员使用 Diffusers 以来,LoRA 一直无法轻松加载。为了将 LoRA 加载到检查点模型中并输出与 A1111 的 Stable Diffusion Webui 相同的结果,我们需要使用额外的自定义代码来加载权重,如本文所述。

## 改进 Diffusers 包以实现高质量图像生成

克服令牌大小限制、自定义模型加载、LoRA 支持、文本反演支持等

[towardsdatascience.com

本文提供的解决方案运行良好且快速,但需要额外管理 LoRA α 权重,我们需要创建一个变量来记住当前的 LoRA 权重 α。因为加载 LoRA 的代码只是将 LoRA 的 A 和 B 矩阵简单地加在一起。

LoRA 权重

然后与主要检查点模型权重 W 合并。

将 LoRA 权重与检查点权重合并

要移除 LoRA 权重,我们需要一个负的-α来移除 LoRA 权重,或者重新创建管道。

加载 LoRA 的猴子修补方式

使用 LoRA 的另一种方法是修补执行模块前向过程的代码,并在计算文本嵌入和注意力分数时引入 LoRA 权重。

用 LoRA 修补模型

这就是 Diffusers LoraLoaderMixin 处理 LoRA 加载的方式。这个方法的好处是没有更新模型权重,我们可以轻松重置 LoRA 并提供新的α来定义 LoRA 权重。

然而,在今天(2023 年 7 月 26 日)之前,Diffusers 的 LoraLoaderMixin 加载 LoRA 并生成的结果与 A1111 有些不同。今天的代码修复了这个问题。“修复”是指,你可以使用 Diffusers 加载检查点模型及 LoRA,并生成与 A1111 SD webui 完全相同的结果。

使用 Diffusers 的 LoraLoaderMixin 加载 LoRA

假设我们有一个safetensor文件格式的 LoRA 文件,使用 Diffusers 加载 LoRA 就像这样简单。

import torch
from diffusers import StableDiffusionPipeline

text2img_pipe = StableDiffusionPipeline.from_pretrained(
    "stablediffusionapi/deliberate-v2"
    , torch_dtype = torch.float16
    , safety_checker = None
).to("cuda:0")

lora_path = "<path/to/lora.safetensors>"
text2img_pipe.load_lora_weights(lora_path)

只需一行代码:text2img_pipe.load_lora_weights(lora_path)。测试其中一个著名的 LoRA — LowRA,这个 LoRA 可以将图像转换为暗模式。

让我们测试一下加载和不加载 LoRA 的效果。

from diffusers import EulerDiscreteScheduler

prompt = """
Маша making extreme selfie on skyscraper, bird's eye view, from above, night, smiling
"""
neg_prompt = """
NSFW,deformed, distorted, disfigured, poorly drawn, bad anatomy, wrong anatomy, extra limb, missing limb, floating limbs, mutated hands and fingers, disconnected limbs, mutation, mutated, ugly, disgusting, blurry, amputation
"""

text2img_pipe.scheduler = EulerDiscreteScheduler.from_config(text2img_pipe.scheduler.config)

image = text2img_pipe(
    prompt = prompt
    , negative_prompt = neg_prompt
    , generator = torch.Generator("cuda:0").manual_seed(3135098381)
    , num_inference_steps = 28
    , guidance_scale = 8
    , width = 512
    , height = 768
).images[0]
display(image)

结果如下,左侧是没有加载 LoRA 的图像,右侧是加载了 LoRA 后生成的图像,使用了text2img_pipe.load_lora_weights(lora_path).

图 1. LowRA LoRA 可以生成暗模式图像,由 Andrew Zhu 使用 Diffusers 生成

使用 Diffusers 加载带权重的 LoRA

即使我们现在可以用一行代码加载 LoRA,我们仍然没有看到对 LoRA 权重参数的支持。例如,我们希望结果变得更亮,并设置 LoRA 权重为 0.5。在 A1111 Stable Diffusion Webui 中,我们可以将 LoRA 权重设置为:<lora:LowRA:0.5>

我们如何将 0.5 添加到 Diffusers 包中?似乎没有办法,但我们可以在 LoRA 加载过程中进行黑客操作,像这样插入我们的值:

text2img_pipe.unload_lora_weights()
lora_path = "<path/to/lora.safetensors>"

lora_w = 0.5
text2img_pipe._lora_scale = lora_w

state_dict, network_alphas = text2img_pipe.lora_state_dict(
    lora_path
)

for key in network_alphas:
    network_alphas[key] = network_alphas[key] * lora_w

#network_alpha = network_alpha * lora_w
text2img_pipe.load_lora_into_unet(
    state_dict = state_dict
    , network_alphas = network_alphas
    , unet = text2img_pipe.unet
)

text2img_pipe.load_lora_into_text_encoder(
    state_dict = state_dict
    , network_alphas = network_alphas
    , text_encoder = text2img_pipe.text_encoder
)

左侧是来自 A1111 SD Webui 的结果,右侧是来自 Diffusers 的结果,二者使用相同的 LoRA 权重为 0.5,Euler 调度器,种子:3135098381

图 2. Diffusers 与 LoRA 可以生成与 A1111 相同的图像。图像由 Andrew Zhu 使用 A1111 SD WebUi(左)和 Diffusers(右)生成

你能看出区别吗?

要重置 LoRA,只需调用unload_lora_weights函数:

text2img_pipe.unload_lora_weights()

总结

这篇文章讨论了 Diffusers 的最新代码更新,这是一个开源的 Stable Diffusion 包,允许我们使用 Python 生成 AI 图像。此外,我们可以根据具体需求自由添加和更改代码。

长时间以来,只有 A1111 Stable Diffusion WebUI 能完全支持 LoRA,现在我们可以使用 Python 控制 Diffusers 加载数千个社区共享的 LoRA,并完全自动化图像生成,这为 GAI 图像生成打开了一扇新门。

总结来说,本文的贡献如下:

  1. 介绍了将 LoRA safetensors 文件加载到 Diffusers Stable Diffusion 管道中的最简单方法(单行代码)。

  2. 提供了一种简单的黑客方式来在 LoRA 加载阶段加载 LoRA 权重。

参考文献

[1] Hugging Face, 大型语言模型的低秩适配(LoRA),huggingface.co/docs/diffusers/training/lora

[2] LowRA,civitai.com/models/48139/lowra

[3] 使用辅助状态加载 Kohya-ss 风格的 LoRA,github.com/huggingface/diffusers/pull/4147

[4] Simo Ryu(cloneofsimo), lora,github.com/cloneofsimo/lora

🤗Hugging Face Transformers Agent

原文:towardsdatascience.com/hugging-face-transformers-agent-3a01cf3669ac

与🦜🔗LangChain Agent 的比较

Sophia Yang, Ph.D.Towards Data Science Sophia Yang, Ph.D.

·发表于Towards Data Science ·阅读时间 5 分钟·2023 年 5 月 14 日

--

就在两天前,🤗Hugging Face 发布了 Transformers Agent——一个利用自然语言从经过筛选的工具集合中选择工具并完成各种任务的代理。听起来熟悉吗?是的,因为它很像🦜🔗LangChain Tools and Agents。在这篇博客中,我将介绍 Transformers Agent 是什么,以及它与🦜🔗LangChain Agent 的比较。

尝试代码:

你可以在这个 colab中尝试代码(由 Hugging Face 提供)。

什么是 Transformers Agents?

简而言之,它在 transformers 之上提供了一个自然语言 API:我们定义了一组经过筛选的工具,并设计了一个代理来解释自然语言并使用这些工具。

我可以想象 HuggingFace 的工程师们会这样说:我们在 HuggingFace 上托管了这么多令人惊叹的模型。我们能否将这些与 LLM 整合起来?我们能否利用 LLM 决定使用哪个模型,编写代码,运行代码并生成结果?本质上,没有人再需要学习所有复杂的任务特定模型了。只需给 LLM(代理)一个任务,它们将为我们做所有事情。

以下是步骤:

来源: huggingface.co/docs/transformers/transformers_agents

  • Instruction: 用户提供的提示

  • Prompt: 一个包含具体指令的提示模板,其中列出了多个可用的工具。

  • Tools: 一组经过筛选的 transformers 模型,例如用于问答的 Flan-T5

  • Agent: 是一个大语言模型(LLM),它解释问题,决定使用哪些工具,并生成代码来利用这些工具完成任务。

  • 限制 Python 解释器:执行 Python 代码。

它是如何工作的?

第一步:实例化一个代理。

第一步是实例化一个代理。一个代理就是一个 LLM,可以是 OpenAI 模型、StarCoder 模型或 OpenAssistant 模型。

OpenAI 模型需要 OpenAI API 密钥,并且使用不是免费的。我们从 HuggingFace Hub 加载 StarCoder 模型和 OpenAssistant 模型,这需要 HuggingFace Hub API 密钥,并且可以免费使用。

from transformers import HfAgent

# OpenAI
agent = OpenAiAgent(model="text-davinci-003", api_key="<your_api_key>")

from transformers import OpenAiAgent
from huggingface_hub import login
login("<YOUR_TOKEN>")

# Starcoder
agent = HfAgent("https://api-inference.huggingface.co/models/bigcode/starcoder")

# OpenAssistant
agent = HfAgent(url_endpoint="https://api-inference.huggingface.co/models/OpenAssistant/oasst-sft-4-pythia-12b-epoch-3.5")

第 2 步:运行代理。

agent.run是一个单一执行方法,会自动选择任务所需的工具,例如,选择图像生成工具来创建图像。

agent.chat保持聊天记录。例如,它知道我们之前生成了一个图片,并且它可以转换图像。

它与🦜🔗LangChain Agent 有什么不同?

Transformers Agent 仍处于实验阶段。它的范围较小,灵活性较差。目前 Transformers Agent 的主要关注点是使用 Transformer 模型和执行 Python 代码,而 LangChain Agent 几乎能做“一切”。让我们逐一比较 Transformers 和 LangChain Agents 之间的不同组件:

工具

  • 🤗Hugging Face Transformers Agent 拥有一个惊人的工具列表,每个工具都由变换器模型驱动。这些工具提供了三大显著优势:1) 尽管 Transformers Agent 目前只能与少数工具交互,但它有可能与超过 100,000 个 Hugging Face 模型进行通信。它具有完整的多模态能力,包括文本、图像、视频、音频和文档。2) 由于这些模型是为特定任务量身定制的,使用它们可以比单独依赖 LLMs 更直接、更准确。例如,我们可以直接部署专为文本分类设计的 BART,而不是为 LLM 设计提示。3) 这些工具解锁了 LLMs 无法完成的功能。例如,BLIP 使我们能够生成引人注目的图像描述——这是 LLMs 无法实现的任务。

  • 🦜🔗LangChain 工具都是外部 API,例如 Google 搜索、Python REPL。实际上,LangChain 通过load_huggingface_tool函数支持 HuggingFace 工具。LangChain 有可能做很多 Transformers Agent 已经能够做的事情。另一方面,Transformers Agents 也有可能整合所有 LangChain 工具。

  • 在这两种情况下,每个工具都是一个 Python 文件。你可以在这里找到🤗Hugging Face Transformers Agent 工具的文件,也可以在这里找到🦜🔗LangChain 工具的文件。正如你所见,每个 Python 文件包含一个类,表示一个工具。

代理

  • 🤗Hugging Face Transformers Agent 使用这个提示模板根据工具的描述确定使用哪个工具。它要求 LLM 提供解释,并在提示中提供一些少量示例。

  • 🦜🔗LangChain 默认使用 ReAct 框架,根据工具的描述来确定使用哪个工具。ReAct 框架在这篇论文中有描述。它不仅进行决策,还提供思考推理,这类似于 Transformers Agent 使用的解释。此外,🦜🔗LangChain 还有四种代理类型

自定义代理

创建自定义代理在这两种情况下都不是很困难:

  • 查看 HuggingFace Transformer Agent 示例,请参见这个 colab的末尾。

  • 查看 LangChain 的示例

“代码执行”

  • 🤗Hugging Face Transformers Agent 在 LLM 选择工具并生成代码之后将“代码执行”作为步骤之一。这将 Transformers Agent 的目标限制为执行 Python 代码。

  • 🦜🔗LangChain 将“代码执行”作为其工具之一,这意味着执行代码不是整个过程的最后一步。这提供了更多灵活性:任务目标可以是执行 Python 代码,也可以是其他任务,例如进行 Google 搜索并返回搜索结果。

结论

在这篇博客文章中,我们探讨了🤗Hugging Face Transformers Agents 的功能,并将其与🦜🔗LangChain Agents 进行了比较。我期待着看到 Transformers Agent 的进一步发展和进步。

. . .

作者:Sophia Yang,发表于 2023 年 5 月 12 日

Sophia Yang 是一位高级数据科学家。可以在LinkedInTwitterYouTube上与我联系,并加入 DS/ML 书友会 ❤️

Human-Learn: 作为机器学习替代方案的基于规则的学习

原文:towardsdatascience.com/human-learn-rule-based-learning-as-an-alternative-to-machine-learning-baf1899ecb3a

将领域知识融入你的模型中,使用基于规则的学习

Khuyen TranTowards Data Science Khuyen Tran

·发布在 Towards Data Science ·阅读时间 7 分钟·2023 年 1 月 1 日

--

动机

你会得到一个标记的数据集,并被要求预测一个新的数据集。你会怎么做?

你可能尝试的第一种方法是训练一个机器学习模型来寻找标记新数据的规则。

作者提供的图片

这很方便,但很难了解机器学习模型为何得出特定的预测。你也无法将你的领域知识融入模型中。

与其依赖机器学习模型来进行预测,不如根据你的知识设定数据标记的规则。

作者提供的图片

这时,human-learn 就派上用场了。

什么是 human-learn?

human-learn 是一个 Python 包,用于创建易于构建且与 scikit-learn 兼容的基于规则的系统

要安装 human-learn,请输入:

pip install human-learn

在上一篇文章中,我谈到了如何通过绘制创建人类学习模型:

## human-learn: 通过绘制创建人类学习模型

使用你的领域知识来标记数据

towardsdatascience.com

在本文中,我们将学习如何使用一个简单的函数创建模型。

随意访问并分叉本文的源代码:

[## Data-science/rule_based_model.ipynb at master · khuyentran1401/Data-science

收集有用的数据科学主题,包括代码和文章——Data-science/rule_based_model.ipynb at master ·…

github.com

要评估基于规则的模型的性能,我们先用机器学习模型预测一个数据集。

机器学习模型

我们将使用占用检测数据集作为本教程的示例。

我们的任务是根据温度、湿度、光线和 CO2 预测房间占用情况。如果Occupancy=0则房间未被占用,如果Occupancy=1则房间被占用。

下载数据集后,解压并读取数据:

import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report

# Get train and test data
train = pd.read_csv("occupancy_data/datatraining.txt").drop(columns="date")
test = pd.read_csv("occupancy_data/datatest.txt").drop(columns="date")

# Get X and y
target = "Occupancy"
train_X, train_y = train.drop(columns=target), train[target]
val_X, val_y = test.drop(columns=target), test[target]

查看train数据集的前十条记录:

train.head(10)

图片作者

在训练数据集上训练 scikit-learn 的RandomForestClassifier模型,并用它来预测测试数据集:

# Train
forest_model = RandomForestClassifier(random_state=1)

# Preduct
forest_model.fit(train_X, train_y)
machine_preds = forest_model.predict(val_X)

# Evalute
print(classification_report(val_y, machine_preds))

图片作者

分数相当不错。然而,我们不确定模型如何得出这些预测。

让我们看看能否用简单规则标记新数据。

基于规则的模型

创建标记数据规则的步骤有四个:

  1. 生成假设

  2. 观察数据以验证假设

  3. 从基于观察的简单规则开始

  4. 改进规则

生成假设

房间的光线是判断房间是否被占用的一个好指标。因此,我们可以假设房间越亮,它被占用的可能性就越大。

让我们通过查看数据来验证这一点。

观察数据

为了验证我们的猜测,让我们使用箱线图找出占用房间(Occupancy=1)和空房间(Occupancy=0)之间光线量的差异。

import plotly.express as px
import plotly.graph_objects as go

feature = "Light"
px.box(data_frame=train, x=target, y=feature)

图片作者

我们可以看到占用房间和空房间之间的中位数存在显著差异。

从简单规则开始

现在,我们将根据房间内的光线创建是否占用的规则。具体来说,如果光线量超过某个阈值,则Occupancy=1,否则Occupancy=0

图片作者

但那个阈值应该是多少呢?我们从选择100作为阈值开始,看看结果如何。

图片作者

要用 human-learn 创建基于规则的模型,我们将:

  • 编写一个简单的 Python 函数来指定规则

  • 使用FunctionClassifier将函数转化为 scikit-learn 模型

import numpy as np
from hulearn.classification import FunctionClassifier

def create_rule(data: pd.DataFrame, col: str, threshold: float=100):
    return np.array(data[col] > threshold).astype(int)

mod = FunctionClassifier(create_rule, col='Light')

预测测试集并评估预测结果:

mod.fit(train_X, train_y)
preds = mod.predict(val_X)
print(classification_report(val_y, preds))

图片作者

准确率比之前使用RandomForestClassifier时更好!

改进规则

让我们通过实验几个阈值来看看是否能得到更好的结果。我们将使用平行坐标图来分析特定光值与房间占用之间的关系。

from hulearn.experimental.interactive import parallel_coordinates

parallel_coordinates(train, label=target, height=200)

图片来源:作者

从平行坐标图中,我们可以看到光线超过 250 Lux 的房间有很高的被占用的概率。将占用的房间与空房间分开的最佳阈值似乎在 250 Lux 和 750 Lux 之间。

让我们使用 scikit-learn 的GridSearch在这个范围内找到最佳阈值。

from sklearn.model_selection import GridSearchCV

grid = GridSearchCV(mod, cv=2, param_grid={"threshold": np.linspace(250, 750, 1000)})
grid.fit(train_X, train_y)

找到最佳阈值:

best_threshold = grid.best_params_["threshold"]
best_threshold
> 364.61461461461465

在箱线图上绘制阈值。

图片来源:作者

使用最佳阈值的模型来预测测试集:

human_preds = grid.predict(val_X)
print(classification_report(val_y, human_preds))

图片来源:作者

阈值365比阈值100给出更好的结果。

结合机器学习模型和规则基础模型

使用领域知识来创建规则的规则基础模型很好,但也有一些缺点:

  • 它对未见数据的泛化能力差

  • 为复杂数据制定规则是困难的

  • 模型没有反馈循环来改进

因此,将规则基础模型与机器学习模型结合将帮助数据科学家扩展和改进模型,同时仍能结合他们的领域专长。

一种简单的方法来结合这两种模型是决定是减少假阴性还是假阳性。

减少假阴性

在预测患者是否有癌症的场景中,你可能会想要减少假阴性(与其错把健康的患者说成有癌症,不如错过发现癌症的机会)。

为了减少假阴性,当两个模型不一致时,选择正标签

图片来源:作者

减少假阳性

在推荐可能对孩子有害的视频等场景中,你可能会想要减少假阳性(与其错过推荐适合孩子的视频,不如错推荐成人视频给孩子)。

为了减少假阳性,当两个模型不一致时,选择负标签

图片来源:作者

你也可以使用其他更复杂的策略层来决定选择哪个预测。

对于深入了解如何结合机器学习模型和规则基础模型,我推荐查看Jeremy Jordan 的这段精彩视频

结论

恭喜!你刚刚学会了什么是规则基础模型以及如何将其与机器学习模型结合。希望这篇文章能为你开发自己的规则基础模型提供所需的知识。

我喜欢写关于数据科学概念的文章并玩各种数据科学工具。你可以通过LinkedInTwitter与我联系。

如果你想查看我写的文章中的代码,请给这个仓库加星。关注我在 Medium 上的文章,获取我最新的数据科学文章:

使用基于用户的协同过滤预测电影评分

Python 中协同过滤的综合介绍

towardsdatascience.com ## SHAP:在 Python 中解释任何机器学习模型

SHAP 和 Shapley 值的综合指南

[towardsdatascience.com ## 使用 TextBlob 提升你的 Python 字符串

在一行代码中从你的文本中获得更多洞察!

[towardsdatascience.com ## 使用 dirty_cat 对脏类别进行相似度编码

[towardsdatascience.com

参考文献

数据引用:

准确检测办公室房间的占用情况,通过光线、温度、湿度和 CO2 测量,使用统计学习模型。Luis M. Candanedo,Véronique Feldheim。《能源与建筑》。第 112 卷,2016 年 1 月 15 日,第 28–39 页。

寻找黑天鹅

原文:towardsdatascience.com/hunt-for-the-black-swan-edafe62ee1b8?source=collection_archive---------16-----------------------#2023-03-13

为什么让你的模型失败是你能做的最好的事

Dorian DrostTowards Data Science Dorian Drost

·

关注 发表在 Towards Data Science ·6 分钟阅读·2023 年 3 月 13 日

--

照片由Michael Dziedzic提供,来源于Unsplash

在开发新的模型或算法时,测试它是否在类似的完美示例上运行可能很诱人。虽然这可能很有趣,但这并不能真正帮助你理解和改进你的模型。你从错误中学习,所以让你的模型失败吧!

想象一下你的数据科学队友来找你,告诉你他们刚刚训练的新模型。它非常棒,可以分类各种小动物的图片。当然,你的队友会开始展示模型的效果,让它正确分类一堆图片。在这个时刻,你最好问你的队友以下问题:

给我一些失败的例子

起初,这可能听起来有些违背直觉。当然,看到模型运行的情况是有趣的,你不希望通过让模型失败来挫伤你的队友的积极性,但什么能更深入地揭示模型的行为:看到模型成功还是看到它失败?

如果它正确分类了 N 张超级可爱的小猫的图片,那么很可能它也会正确分类第 N+1 张图片,只要它看起来和其他的相似。你感到惊讶吗?没有。你学到了关于模型行为的任何东西吗?没有。

然而,如果你找到那些导致模型失败的图片,你可能最终会对模型仍然有问题的图片有个了解。这是非常值得的!现在你可以开始理解模型,并进一步改进它。

正式假设和黑天鹅

关于寻找失败的理论背景有着悠久的传统。几百年来,非常聪明的人们一直在思考和辩论如何从观察中得出普遍的规则。今天太阳早上升起了,昨天和前天也是如此。这是否意味着它明天还会升起?好吧,不一定。另一个例子:我去了公园,那里我看到的所有天鹅都是白色的,所以我可能会提出我的假设

所有的天鹅都是白色的。

作为一个优秀的科学家,我当然会证明我的假设,所以我去到下一个公园,看看天鹅。它们也是白色的。那么,我的假设被证明了吗?没有,因为如果我想证明它,我需要检查所有的天鹅。好吧,我没有时间去做这些,那我该怎么办?在看到 N 只白天鹅后,再看第 N+1 只白天鹅不会给我提供额外的信息。我应该:

试着找到一只黑天鹅。

为什么会这样?难道这不会否定我的假设吗?是的,这正是它要做的。这也是我应该追求的目标。

从正式的角度来看,如果我看到 N 只白天鹅,并推导出所有天鹅都是白色的,我进行的是逻辑归纳。然而,逻辑归纳有一个缺点:它是错误的。

“→” 可以读作“意味着”,而“∧” 意味着“和”。

上述陈述是归纳的,我们可以将其解读为

我的假设 H 意味着一个观察 B,而我观察到了这个观察 B。这意味着,我的假设 H 是正确的。

但总体上,这个陈述是错误的。在我们的例子中,这个陈述会是:

我的假设“所有天鹅都是白色的”(H)意味着,下一个我观察到的天鹅是白色的(B)。我观察到的下一只天鹅确实是白色的(B)。这意味着,我的假设是正确的。

再次,这不是真的。虽然观察到一只白天鹅与假设一致,但这并不意味着假设的正确性。它只是没有反驳假设。如果你不信,可以看看以下例子:

我的假设‘所有天鹅都是由美国政府的一个秘密组织涂成白色的’暗示,我观察到的下一只天鹅是白色的(B)。我观察到的下一只天鹅确实是白色的(B)。这暗示着我的假设是正确的。

完全不真实。

然而,仍然有希望。虽然上述陈述是错误的,但以下内容是正确的:

→ 可以读作“暗示”,∧ 表示“和”,¬ 表示“非”。

我们可以读作

我的假设 H 暗示了一个观察结果 B,而我观察到的却不是 B。这暗示着我的假设 H 不正确。

或者将你的例子表述为

我的假设‘所有天鹅都是白色的’(H)暗示,我观察到的下一只天鹅是白色的(B)。我观察到的下一只天鹅不是白色的(¬ B)。这意味着我的假设不正确(¬ H)。

这是真实的陈述(对于你们中的形式逻辑迷来说,这叫做modus tollens)。如果我找到一个反驳假设的例子,那么这个假设就被证明是错误的,而这确实是我之前没有的信息。

总的来说,这意味着假设永远不能被证明或验证。它们只能被证伪。另一方面,如果一个假设在我尝试证伪它的过程中幸存下来,这对假设是有利的。

回到模型

那么,这一切如何与您的队友的模型相关?你在这里并不想证明或证伪一个简单的假设,而主要思想是,信息的增益来自于那些出现错误的案例。简单来说,模型有效的案例不会告诉你任何你已经知道的东西。要理解模型的行为,要看那些模型失败的案例。例如:

一个识别实体的工具可以识别‘Alice’和‘Paula’这些名字。

  • 它还能检测‘Kathrin’吗?能。你从中学到了什么吗?没有。

  • 它还能检测‘Linh’吗?不能。→ 也许它对亚洲名字有问题。

一个图像分类器可以检测景观图像拍摄的地点。它正确地检测到你最近一次度假的照片是在印度尼西亚的海滩拍摄的。

  • 它还能检测你前年在印度拍摄的照片的地点吗?能。你从中学到了什么吗?没有。

  • 它也适用于你爷爷在 50 年代第一次去意大利旅行时拍的照片吗?不能。→ 也许数据缺少旧的黑白照片。

一个新的酷炫机器人手臂非常灵活,可以以非常详细的方式控制,甚至可以在钢琴上演奏 C 大调音阶。

  • 它还能演奏 F 大调音阶吗?能。你从中学到了什么吗?没有。

  • 它还能演奏莫扎特的奏鸣曲吗?不能。→ 也许它的灵活性仍然有限,莫扎特的奏鸣曲对它来说太难了。

所有这些例子展示了你如何从失败的案例中获得知识,而不是从成功的案例中获得。以同样的方式,你可以更多地了解你队友的分类可爱动物的模型:

  • 它也能分类稀有动物,如幼章鱼或树袋熊吗?

  • 它是否也适用于不同的图像背景?

  • 它能分类那些看起来与父母完全一样,只是更小的幼鱼吗?

  • 如果一张图片中有多种不同的动物会发生什么?

这些只是一些可以帮助你理解模型行为的例子。当打破模型时,要有创意!

结论

我刚刚向你展示了为什么打破你的模型比看到它工作更有帮助。失败的案例是那些带来有用信息的案例,就像试图否证一个假设是加强它的关键一样。现在是时候将形式逻辑付诸实践了!下次你的队友来向你展示他们的新模型时,花点时间欣赏它,并庆祝那些表现良好的案例。但在那之后,开始打破它并寻找极限情况。这些将帮助你进一步改进模型。追寻黑天鹅!

进一步阅读

对假设的否证和归纳问题的主要观点被浓缩在卡尔·波普尔的批判性理性主义科学理论中,这在大多数科学研究和统计的学术教材中都有介绍,例如:

对黑天鹅及许多相关概念进行真正深入的探讨(约 2000 页):

喜欢这篇文章吗? 关注我 以便收到我未来的帖子通知。

混合离散-连续几何深度学习

原文:towardsdatascience.com/hybrid-discrete-continuous-geometric-deep-learning-2e5871293184?source=collection_archive---------12-----------------------#2023-03-15

可扩展且等变的球面卷积神经网络(CNNs)通过 DISCO 卷积

Jason McEwenTowards Data Science Jason McEwen

·

关注 发布于 Towards Data Science · 7 分钟阅读 · 2023 年 3 月 15 日

--

目前没有现有的球面卷积神经网络(CNN)框架既具有计算扩展性又具有旋转等变性。连续方法捕捉旋转等变性,但通常计算要求非常高。离散方法提供了更有利的计算性能,但以牺牲等变性为代价。我们开发了一种混合的离散-连续(DISCO)组卷积,它同时具备等变性和计算扩展性。这种方法在许多基准密集预测任务中实现了最先进的(SOTA)性能。 (更多细节请参见我们在 Scalable and Equivariant Spherical CNNs by DISCO Convolutions上的 ICLR 论文。)

图片由 Dustin Tramel 提供,来源于 Unsplash

几何深度学习在群体上的应用非常广泛,如分析地球上的观察数据以及全景 360°照片和视频等。然而,当前的方法存在二分法:它们要么表现出良好的等变性特性,要么具有良好的计算扩展性,但不可能两者兼得。

二分法:离散方法与连续方法

几何深度学习技术在群体上的关键目标是 编码等变性 以适应各种群体变换(这通常会带来非常好的性能),同时还需要高度的计算扩展性。

正如我们在 之前的 TDS 文章 中讨论的,专注于具有全局对称性的均匀空间的群体设置,几何深度学习在群体上可以大致分为离散方法和连续方法。连续方法提供等变性,但计算成本高昂。另一方面,离散方法通常计算效率相对较高,但牺牲了等变性。

打破二分法:离散-连续(DISCO)方法

Copernic AI 我们最近开发了打破这种二分法的技术(最近在 ICLR [1] 上发表)。也就是说,我们开发了几何深度学习技术,既提供了卓越的等变性特性,又具有高度的计算效率,从而能够有效地扩展到庞大、高分辨率的数据集。

打破离散与连续二分法的关键在于采取混合方法,将一些表示部分离散化,以促进高效计算,而其他部分保持连续,以促进等变性。由于其混合性质(如下图所示),我们将这种方法命名为 DISCO,代表 DIScrete-COntinous。

虽然 DISCO 方法是通用的,但我们重点关注球面作为具有全局对称性的均匀空间群设置的典型示例。

通过混合离散-连续(DISCO)方法打破连续与离散的二分法,这种方法既具有旋转等变性,又具有计算可扩展性。[原始图由作者创建。]

离散-连续(DISCO)群卷积

DISCO 方法基于卷积层,其中 DISCO 群卷积遵循标准群卷积的精心混合表示。表示中的一些组件保持连续,以促进准确的旋转等变性,而其他组件则被离散化,以实现可扩展的计算。

在群上定义的信号(即数据、特征图)f 与滤波器 𝝭 的 DISCO 群卷积表示为

其中 g 是群 G 的一个元素,dµ(u) 是(哈 aar)积分测度,q(uᵢ) 是求积权重。方括号和索引下标表示离散化的量,i 表示样本索引,而圆括号表示连续量。

在球面上,我们考虑由 3D 旋转给出的变换,因此球面上的信号的 DISCO 卷积表示为

其中 R 表示旋转,ω 表示球面坐标。

聚焦于球面情况时,显然感兴趣的信号必须在样本位置 ωᵢ 上离散化。然而,在 DISCO 方法中,滤波器 𝝭 和群作用 R 仍然保持连续。这使得滤波器能够通过任何 R 连续地转换,保持一致的表示,从而避免任何离散化误差,并且提供旋转等变性,这与完全离散的方法不同。

关于 ω 的积分也必须进行离散化。对于紧致均匀流形上的带限信号,例如球面,采样定理的存在确保积分可以使用求积权重 q(ωᵢ) 非常准确地近似。

对于带限信号,DISCO 对群卷积的近似非常准确,而实际信号可以通过足够的带限进行很好的近似。通过应用采样定理,信号的所有信息内容都可以被有限的样本集{f[ωᵢ]} 捕捉到。滤波器被连续表示,因此不会引入任何误差。唯一的近似误差来源是用于评估积分的求积方法。对于足够密集的采样,可以应用采样定理及相应的求积方法来准确评估。因此,原则上可以精确计算 DISCO 群卷积,而无需任何近似误差。由于近似非常准确(可以通过足够密集的采样来使其精确),且群操作以连续方式处理,DISCO 群卷积展现了优异的等变性特性,如数值验证所示[1]。

可扩展计算

DISCO 卷积通过稀疏张量表示提供了一个可扩展的计算实现[1]。具体来说,我们利用稀疏-密集张量乘法算子在硬件加速器(例如 GPU、TPU)上高效地计算 DISCO 球面卷积。

通过进一步限制旋转空间(到商空间 SO(3)/SO(2))并利用采样方案的对称性,我们在计算成本和内存需求上实现了线性缩放。

下面的图表展示了 DISCO 球面卷积在不同分辨率/带限下的浮点操作数(FLOPs)和内存需求,与最有效的替代球面卷积(展现旋转等变性)进行对比。

DISCO 球面卷积在不同分辨率/带限下的计算成本和内存需求,与最有效的替代球面卷积(展现旋转等变性)进行对比。[原始图由作者创建。]

对于 4k 球面图像,我们在计算成本上节省了 10⁹,在内存使用上节省了 10⁴。

DISCO 球面 CNN 架构

转置 DISCO 卷积也可以以类似的方式构造,如上述前向卷积讨论的方式,这样可以用于提高内部特征表示的分辨率,以满足密集预测任务。

通过将 DISCO 前向和转置球面卷积与逐点非线性激活和其他常见结构特征(如跳跃连接、批量归一化、多通道等)结合,可以构建高效的球面实现的常见 CNN 架构。

我们下面考虑了若干密集预测任务,例如语义分割和深度估计,我们采用了一个具有 DISCO 卷积的残差 UNet 架构的通用骨干。我们的 DISCO 模型在迄今为止考虑的所有基准问题上实现了最先进的性能。

语义分割

我们考虑了 360°照片的语义分割的密集预测问题。

对于室内 360°照片的 2D3DS 数据集,我们展示了球面 RGB 图像、真实分割以及仅通过 RGB 图像预测的 DISCO 模型分割的示例。

室内 360°照片的 2D3DS 数据的示例分割。[原始图由作者创建。]

尽管预测的分割结果并不完美,但它们通常非常准确。事实上,我们的 DISCO 方法在所有其他替代方法中实现了最先进的性能(有关详细信息,请参见[1])。

对于户外 360°照片的 Omni-SYNTHIA 数据集,我们还展示了球面 RGB 图像、真实分割和预测分割的示例。

户外 360°照片的 Omni-SYNTHIA 数据的示例分割。[原始图由作者创建。]

再次强调,预测的分割结果通常非常准确,我们在所有其他替代方法中实现了最先进的性能(有关详细信息,请参见[1])。

深度估计

另一个常见的密集预测任务是深度估计。我们考虑了从 360°照片中进行单目深度估计的任务,解决了 Matterport3D 数据集上的 Pano3D 基准。

我们展示了球面 RGB 图像、真实深度和仅通过 RGB 图像预测的 DISCO 模型深度的示例。

Matterport3D 数据集的室内 360°照片的深度估计示例。[原始图由作者创建。]

预测的深度通常非常准确。确实,我们再次在所有其他替代方法中实现了最先进的性能(有关详细信息,请参见[1])。

未来展望

通过混合离散-连续(DISCO)表示法,现在已经解决了群体上的等变和计算可扩展的几何深度学习问题。正如我们在上面提到的基准任务中所看到的,我们实现了最先进的性能,卓越的等变性属性转化为出色的性能。

我们现在拥有将现代深度学习架构扩展到具有全局对称性的均匀空间群体设置所需的基础构建块,例如球面。现在有大量应用可以释放现代深度学习的潜力。

参考文献

[1] Ocampo, Price, McEwen, 通过离散-连续(DISCO)卷积的可扩展和等变球面 CNN,ICLR(2023),arXiv:2209.13603

混合搜索 2.0:追求更好的搜索

原文:towardsdatascience.com/hybrid-search-2-0-the-pursuit-of-better-search-ce44d6f20c08?source=collection_archive---------1-----------------------#2023-09-30

学习、改进之旅,以及对终极混合搜索系统的追求

Noam SchwartzTowards Data Science Noam Schwartz

·

关注 发表在 Towards Data Science · 7 分钟阅读 · 2023 年 9 月 30 日

--

图片由 bert b 提供,来源于 Unsplash

结合文本和向量搜索的优势在搜索系统领域获得了越来越多的关注,以提高搜索的相关性和准确性。我在 最近的一篇博客文章 中讨论了使用 OpenSearch 构建混合搜索引擎。通过将基于文本的词汇搜索与基于向量的语义搜索结合起来,我们能够提高搜索结果的延迟和准确性。

我最近一直在思考混合系统的缺点及可能的改进。在本文中,我将探讨之前系统中的三个薄弱环节,并提出改进建议,以加强系统并提供更好的结果。

在继续之前,请阅读我的之前的博客文章,因为我将参考其中描述的步骤。

  1. 标准化函数略有偏差;它对文本搜索的权重较高,并在最终结果中给予更多的重要性。

基于距离的算法,如 K-最近邻(KNN),计算数据点之间的距离,而 BM25 则基于关键词的出现频率。这两者返回的得分完全在不同的尺度上。这可能导致结果偏差和排名不准确。我们的标准化过程总是为词汇搜索结果集中的至少一个文档产生了完美的分数(1),因此在我们的情况下,结果偏向了词汇搜索。

为了应对这个问题,让我们探索两种常用的函数:Min-Max 标准化和 Z-Score 标准化。Z-Score 方法将数据缩放到零均值和单位方差,而 Min-Max 标准化则将数据重新缩放到特定范围内。

关键思想是,如果我事先计算这些标准化函数中使用的参数,并在标准化阶段应用它们,我可以对每种搜索类型在类似查询下的得分分布有一个基本的理解。这两个函数的公式是:

考虑索引的结构可以帮助你决定选择哪一种,因为每种方法都有其自身的优势。如果你的文档彼此之间更相似,且典型查询的前 k 个结果返回的文档彼此非常相似且在索引中聚集在一起,如下图所示,Min-Max 可能是更好的选择。

作者图示

然而,如果结果分布更均匀且具有一些正态分布的特征,如下面的示例所示,Z-Score 更适合。

作者图示

这两种方法都需要确定某些参数;我们必须计算均值、标准差、最低分数和最高分数。我们必须分别为每种搜索类型确定这些值,因为向量和语义结果具有不同的评分系统。让我们运行 1000 个随机查询、一次向量搜索和一次语义搜索来完成这项工作。如果你没有查询,你可以使用 OpenSearch 的scroll API从索引的不同部分提取用作查询的文本字段。将 k 设置为一个较大的值,例如 k=1000,以更好地了解我们分数范围内的比例。然而,要小心不要将 k 设置得太高,因为这可能会影响 Min-Max 函数。在收集所有这些分数后,简单地计算必要的参数。

 # Lexical Search
    text_scores = []
    for query in queries:
        response = client.search(
            index=INDEX_NAME,
            body={
                "query": {
                    "match": {
                        "caption": query
                    }
                },
                "size": 1000
            }
        )
        scores = [hit['_score'] for hit in response['hits']['hits']]
        text_scores.append(scores)

    # Vector search
    vector_scores = []
    # Vectorize queries using SentenceTransformer
    query_embeddings = model.encode(queries)
    # Perform vector search
    for query_embedding in query_embeddings:
        request_body = {
            "size": 1000,
            "query": {
                "script_score": {
                    "query": {
                        "match_all": {}
                    },
                    "script": {
                        "source": "knn_score",
                        "lang": "knn",
                        "params": {
                            "field": "caption_embedding",
                            "query_value": query_embedding.tolist(),
                            "space_type": "cosinesimil"
                        }
                    }
                }
            }
        }
        response = client.search(
            index=INDEX_NAME,
            body=request_body
        )
        scores = [hit['_score'] for hit in response['hits']['hits']]
        vector_scores.append(scores)    

    vector_score_mean = np.mean(vector_scores) # Calculate the mean
    vector_score_std = np.std(vector_scores, ddof=1)  # Calculate standard deviation
    vector_score_min = np.min(vector_scores)  # Calculate minimum score
    vector_score_max = np.max(vector_scores)  # Calculate maximum score

    text_score_mean = np.mean(text_scores) # Calculate the mean
    text_score_std = np.std(text_scores, ddof=1)  # Calculate standard deviation
    text_score_min = np.min(text_scores)  # Calculate minimum score
    text_score_max = np.max(text_scores)  # Calculate maximum score

过程如下面的图示所示:

作者提供的图示

将你提取的词汇和向量结果的参数单独保留一次。最终,在标准化步骤中,我们将以如下方式使用这些参数:

使用 Z-Score 函数进行标准化(作者提供的图示)

2. 没有出现在任何集合中的分数被“非公平”地处理,没有得到充分考虑作为潜在匹配。

之前,任何特定于一个集合的结果都获得了一个任意低的分数,这显著影响了其最终排名。让我们尝试一种替代策略来解决这个问题。我们将把缺失结果集合中的最低分数授予那些仅在一种结果集合中出现的文档。例如,我们将文档c5a44d-fa8d-4bba(黄色标记)赋予前 k 个结果中的最低语义搜索分数,因为它只出现在我的词汇(关键词)搜索结果中。通过这种方法,我们通过提供一个在其他分数范围内的分数来保证这些结果仍然是合法的。

作者提供的图示

3. 旧的方法缺乏强的数据驱动或科学基础。因此,我们没有办法基于数据选择调优参数如 boost。

让我们一次性解决这两个问题。当前任务涉及比较两组有前景的结果——词汇结果和语义结果——并宣称混合结果优于它们。为此,我们将对 MS MARCO 进行实验。MS MARCO 是由微软研究院策划的数据集,包含从网页中提取的 320 万份网络文档以及超过 35 万条来源于真实 Bing 网络搜索查询的查询。最初设计用于基准测试问答(Q&A)系统,该数据集包含类似现实世界问题的查询。鉴于数据集的问答性质,相关性标签简单明了,每个结果仅分配一个“相关”标签(1)。

在我们的场景中,MS MARCO 文档排名挑战使用 平均倒数排名(MRR)作为前 100 个结果(MRR@100)的相关性度量来进行排名挑战。它计算第一个相关文档的倒数排名 (1/rank) 并对所有查询进行平均。

来源:维基百科

我们将进行三种不同类型的搜索:词汇搜索、语义搜索和混合搜索。我们将尝试不同的提升水平,每次改变 0.1。我们希望评估所有搜索实验的前 100 名的平均倒数排名(MRR@100),因此我们将 top-k 设置为 100。对于每个查询,我们将结果与“正确”标记的文档 ID 进行比较,并确定其排名。

def find_doc_id_rank(doc_id, document_ids):
    if doc_id in document_ids:
        position = document_ids.index(doc_id) + 1
        return 1 / position
    else:
        return 0

最后,我们将各个排名加在一起,以确定平均倒数排名(MRR)。

以下表格显示了在 0 到 1 之间的每个增强比例(以 0.1 为增量/减少量)下的 MRR@100 分数:

使用 Z-Score 函数的 MRR@100(截图由作者提供)

使用 Min-Max 函数的 MRR@100(截图由作者提供)

上述结果清楚地表明,当在词汇搜索方向上进行一定程度的提升时,Min-Max 方法表现最佳。然而,将其与语义结果结合可以提高其准确性。另一方面,Z-Score 函数通过均匀分配提升,同时在词汇和语义搜索之间保持 50–50 的分割,产生了最佳的总体结果,表明混合搜索方法是最终选择。

在我们寻求更有效的混合搜索系统的过程中,我们迎接了挑战,并取得了显著的改进。我们已经采取措施来应对固有的挑战,结果很明确:词汇和语义搜索技术的结合具有巨大的潜力。这还不是终点,而是通往更高效、以用户为中心的搜索体验的一个踏脚石。凭借数据支持的方法论,我计划继续改进搜索系统,并迎接信息检索领域不断变化的挑战。

感谢Yaniv Vaknin和特别是 Daphna Idelson为帮助我们完成这项工作!

超曲面深度强化学习

原文:towardsdatascience.com/hyperbolic-deep-reinforcement-learning-b2de787cf2f7

强化学习遇到超曲面几何

许多强化学习问题具有层次树状特性。超曲面几何为此类问题提供了强大的先验知识。

Michael BronsteinTowards Data Science Michael Bronstein

·发表于Towards Data Science ·17 分钟阅读·2023 年 4 月 30 日

--

强化学习中的许多问题表现出层次树状的特性。因此,超曲面空间(可以被概念化为树的连续类比)是参数化智能体深度模型的合适候选。本文概述了超曲面几何的基础知识,实证展示了它为许多强化学习问题提供了良好的归纳偏置,并描述了一种实用的正则化程序,允许解决在超曲面潜在空间中进行端到端优化时的数值不稳定性。我们的方法在广泛的常见基准测试中显示了几乎普遍的性能提升,无论是使用策略算法还是离策略算法。

由“超曲面 Atari Breakout 游戏,图标设计,平面设计,矢量艺术”提示的稳定扩散(由David Ha提供)

本文由Edoardo Cetin Ben Chamberlain Jonathan Hunt 共同撰写,并基于 E. Cetin 等人发表的论文 Hyperbolic deep reinforcement learning (2023) ICLR。欲了解更多详情,请在 ICLR 2023 上找到我们!

强化学习基础

强化学习问题可以描述为马尔可夫决策过程(MDP),其中智能体从环境的状态空间中观察到某个状态 sS,基于此执行某个动作 a∈A,从其动作空间中,并最终从其奖励函数 r: S×A ↦ R中获得奖励r

环境的演变依赖于马尔可夫性质,这意味着给定当前状态,它独立于过去的状态,并由转移动态 P : S×A×S ↦ R 和初始状态分布 p₀: S ↦ R 完全描述。

策略是一个关于动作的参数化分布函数 a∼π(⋅|s),给定当前状态 s,表示代理的行为。代理与环境之间的每次交互产生一个轨迹,τ = (s₀,a₀,s₁,a₁,…),根据策略和转移动态生成。对于每个状态 s∈ S,策略的价值函数表示从 s 开始的代理可能轨迹上的未来奖励的期望折扣总和[*]。

代理的目标是学习一个最大化其期望折扣奖励总和的策略,或等效地,最大化在可能初始状态上的期望价值函数 在深度 RL 中,策略和值函数通常建模为神经网络。RL 训练循环涉及在经验收集阶段(在环境中部署当前策略)和学习阶段(更新代理的模型以改善其行为)之间交替进行。根据收集的经验数据的使用方式,我们可以区分两类主要的 RL 算法:

在线策略算法为每次训练迭代收集一组新的轨迹,使用最新策略,丢弃旧数据。他们使用这些轨迹来学习当前策略的价值函数,然后用它来计算策略梯度[1]并最大化执行最佳观察到的当前动作的概率。近端策略优化(PPO)[2] 目前是这一类别中最成熟和最稳健的算法之一[3]。

离线策略算法则将许多不同的轨迹存储在一个大的重放缓冲区数据集中,这些轨迹是通过旧策略的混合收集的。他们使用这些数据直接学习一个基于贝尔曼备份[4]的最优价值函数模型。然后,策略是基于导致最高期望估计值的动作隐式定义的。Rainbow DQN [5] 是一种现代流行的开创性离线策略 DQN 算法[6],引入了几种辅助实践,以稳定和加快学习速度。

深度强化学习中的泛化

泛化是有效强化学习代理的关键要求,因为大多数现实世界甚至复杂的模拟任务都涉及其状态空间中的大量多样性(例如,自然图像的空间)。从代理的角度来看,探索和记忆这一(可能是无限的)输入集合的精确值显然是不可行的。此外,对于许多应用,训练中使用的受控实验室环境可能无法反映特定任务的所有可能配置的全面多样性。因此,代理的行为在部署期间应理想地对可能观察到的小分布偏移保持稳健。

基于深度神经网络的代理模型作为一种实用的方法来解决这些问题,实际上作为一种功能性先验,试图仅捕捉代理在有效决策过程中所需的最相关和因果特征。然而,如何准确理解不同设计选择对神经网络训练及其最终泛化效果的影响,仍然是一个开放的问题。

在我们最近的论文[7]中,我们研究了使深度强化学习模型稳健有效泛化的几何特性。特别是,我们关注于超曲面几何模型,接下来我们会描述[8]。

在 Breakout 中,状态之间的层次关系,通过超曲面空间的庞加莱圆盘模型可视化。

超曲面几何

大多数机器学习(更广泛地说,计算机科学和数学)应用利用欧几里得空间来表示数据和执行数值运算。欧几里得空间可以很容易地可视化,并且它们的大多数属性本质上是直观易懂的。例如,总体积随着从原点的半径以多项式形式增长[9],并且将两个点通过相同的向量平移不会影响它们之间的距离。

超曲面空间[10]不具备这样直观的属性,并且形式上可以描述为一种特殊的黎曼流形,即* n 维的对象嵌入在n+1 维中,仅在局部上是欧几里得的。超曲面空间的一个定义特性是其恒定的负曲率*,导致距离和体积以指数形式增长,而不是多项式形式。

超曲面空间(𝔹²)中点之间的最短路径(测地线)和嵌入树中的节点。

这使得超曲面空间可以被解释为树的连续类比,其中叶节点的数量也随着深度的增加而呈指数增长。由于这一事实,一棵树可以在只有二维的超曲面空间中等距嵌入(即保持节点之间相对距离的方式)。相比之下,将一棵树嵌入到欧几里得空间会导致扭曲,这些扭曲可以通过使用高维空间来减少。

存在多个等效的双曲几何模型;在这里,我们考虑庞加莱球(记作𝔹),它可以被概念化为一个n维的单位球,保留了来自欧几里得空间的角度概念。由于庞加莱球的总体积随着从原点的半径指数增长,测地线(最短路径)是与边界垂直的圆弧,而不是像在欧几里得空间中的直线[12]。

庞加莱和贝尔特拉米-克莱因的双曲几何模型。

为了在机器学习中处理双曲空间,我们必须重新定义与向量的标准操作、超平面的概念以及这些元素之间的相对距离[13]。这样做的概念困难在于我们需要在切空间中工作,这是双曲空间的局部欧几里得表示。

这是通过指数映射 exp(v),它沿着从点x出发的测地线朝输入向量v的方向迈出一个单位步长来实现的。我们使用从庞加莱球原点的指数映射将欧几里得输入向量v映射到𝔹 [14]*。

陀螺向量空间 [15] 允许将常见的向量操作扩展到非欧几里得几何中。这样的一个操作记作xy,被称为莫比乌斯加法 [16]。

陀螺平面(记作H)是陀螺向量空间中定向超平面的推广。庞加莱球上的一个陀螺平面由n维的平移p和法向量w参数化,使得H = {y 𝔹 : <yp,w>=0}。

在机器学习问题中,超平面和陀螺平面可以用作定向决策边界。平移和法向量(pw)提供了一种替代参数化来定义线性仿射变换[17]。具有m个输出单元的全连接层的类比是𝔹ⁿ中的一组m个陀螺平面:给定一个在双曲空间中的n维输入向量x,层输出 f(x) 作为x与每个陀螺平面 H 之间的带符号和缩放距离来计算:

f(x) = 2 sign(<x⊕–p,w>) ||w||d(x,H) / (1 — ||p||²)¹ᐟ²,

其中 d(x,H) 是庞加莱球上xH之间的距离函数[18]。

与先前用于监督学习[19]和无监督学习[20]的工作类似,我们在标准自然网络架构中使用这些参数化的陀螺平面全连接层,替代了标准的欧几里得层。结果特征空间的双曲几何引入了一种不同的归纳偏差,这对许多强化学习问题应该更为适用,我们将在下文中进行说明。

强化学习问题的双曲性

由于 RL 问题中的马尔可夫性质,轨迹中状态的演变可以被概念化为一个树,其中策略和动态决定了每个可能分支的概率。直观地,MDP 中每个状态的值和最优策略自然与其可能的后继状态相关。

相比之下,有许多例子表明其他固定的、非层次化的状态信息(例如环境的一般外观)应该被忽略。例如,Raileanu 和 Fergus [21] 观察到代理的价值函数和策略模型在Procgen环境 [22](例如背景颜色)中对非层次特征的虚假关联进行了过拟合,导致对未见过的关卡泛化能力差。

基于这些观察,我们假设有效的特征应该编码直接与 MDP 的层次状态关系相关的信息,反映其树状结构。

为了验证我们的假设,我们分析了 RL 代理学习到的表示空间,测试它们是否表现出层次结构。我们使用 Gromov δ-双曲性 [11] 的概念:一个度量空间(X,d)如果每个可能的测地线三角形△ABC 都是δ-瘦的,即△ABC 的每一侧上的每个点都存在另一侧上的某个点,其距离至多为δ,则称其为δ-双曲。树形结构的一个特征是△ABC 中的每个点都属于至少两个侧面,从而产生δ=0。因此,我们可以将δ-双曲性解释为度量与树形度量之间的偏差。

必要的δ使得△ABC 的每一侧上的每个点都存在另一侧上的某个点,其距离在树形三角形(左)、双曲三角形(中)和欧几里得三角形(右)中至多为δ

RL 代理通过对收集的状态进行编码所学习的最终表示跨越了欧几里得空间的某个有限子集,有效地形成了一个离散度量空间。类似于[19],我们通过空间的直径对δ-双曲性记录进行归一化,产生一个相对度量,试图对所学表示的尺度保持无关[23]。

这使我们能够实际解释所学的潜在表示的双曲性,其值在 0(完全树状结构)和 1(完全非双曲空间)之间。我们使用标准的 Impala 架构[24]训练一个标准的 PPO 代理,并分析随着训练的进展,其性能和我们的δ-双曲性度量如何演变,测试四种不同的 Procgen 环境。

我们观察到,在所有环境中,δ在训练的前几次迭代中迅速降到低值(0.22–0.28),反映了代理性能的相对最大提升。随后,似乎发生了一个有趣的二分法。在fruitbotstarpilot环境中,δ在训练过程中进一步减少,因为代理在训练和测试水平分布之间恢复了高性能,并且泛化差距较小。

相反,在bigfishdodgeball中,δ在初始下降后开始再次增加,表明潜在表示空间开始丧失其层次结构。相应地,在这两个环境中,代理开始过拟合,因为测试水平的表现停滞不前,而与训练水平表现之间的泛化差距持续扩大。

PPO 代理在 Procgen 中的最终潜在空间的性能和相对δ-超曲率。

这些结果支持了我们的假设,经验上展示了编码层次特征的重要性,并建议 PPO 在某些环境中泛化性能差是由于欧几里得潜在空间编码了虚假的特征,这些特征阻碍了超曲率的效果。

使用超曲率潜在空间训练代理

基于我们的发现,我们建议使用超曲率几何来编码深度 RL 模型的最终潜在表示。我们的方法旨在引入不同的归纳偏差,以激励基于反映常见 MDP 中观察到的因果层次演变的特征来建模代理的策略和值函数。

我们的基本实现尝试对底层算法和模型进行最小的修改。我们从对 PPO 的简单扩展开始,将最终的 ReLU 和线性层替换为到𝔹的指数映射以及一个输出值函数和策略对数的陀螺面全连接层。

将超曲率空间与用于建模 PPO 策略和值的 Impala 架构进行集成。

然而,这种幼稚的方法导致了令人失望的表现,远远落后于标准 PPO 的表现。此外,我们发现超曲率策略在性能改善时,难以开始探索并随后退回到更确定性的行为,这与我们通常期望 PPO 的熵奖励相反。这些结果似乎表明了源自超曲率表示的端到端 RL 训练中的优化挑战。

我们未正则化的超曲率表示与 RL 集成的性能和梯度。

为了克服类似的优化问题,先前的工作在监督和无监督学习中使用双曲空间,提出了仔细的初始化方案[25]和稳定化方法,如表示剪裁[26]。虽然我们在实现中也使用了这些方法,但它们在 RL 问题中似乎效果不大。

这不应该令人惊讶:这些策略的主要目的是在前几次训练迭代中促进学习适当的角度布局,否则后续的端到端优化常常会导致低性能的失败模式[13]。然而,RL 的固有高方差和非平稳性使得主要关注早期迭代的稳定化策略不足。RL 中的轨迹数据和损失景观在训练过程中可能会显著变化,使得早期的角度布局在长期内不可避免地变得次优。此外,高方差的策略梯度优化[1]更容易进入上述不稳定的学习状态,从而导致观察到的失败模式。

另一个需要处理类似非平稳性和脆弱优化的机器学习子领域是生成对抗网络(GANs)[27]。在 GAN 训练中,生成的数据和判别器的参数不断演变,使得损失景观高度非平稳,类似于 RL 设置。此外,优化的对抗性质使其对梯度爆炸和消失的不稳定性非常脆弱,导致常见的失败模式[28]。

我们从这方面的文献中获得灵感,并利用了谱归一化(SN)[29],基于最近的分析和实证结果,显示其能够准确防止梯度爆炸现象[30]。我们的实现将 SN 应用于模型的欧几里得编码器部分的所有层,留下最终的线性双曲层不进行正则化。此外,我们还在将最终的潜在表示映射到𝔹ⁿ之前对其进行缩放,以便修改表示的维度不会显著影响它们及其梯度的大小。我们将我们的稳定化方法称为谱正则化双曲映射(S-RYM,发音为ɛs-raɪm)。

将 S-RYM 应用于我们的双曲 RL 代理似乎解决了它们的优化挑战。此外,它们相对于原始欧几里得实现也获得了显著更高的性能,并且在整个训练过程中保持了低梯度幅度。

将 S-RYM 应用于双曲和欧几里得 RL 代理的性能和梯度。

实验结果

我们在不同的基准测试、RL 算法和训练条件下评估双曲线深度 RL。除了 PPO,我们还将我们的方法应用于离线策略的 Rainbow DQN 算法。我们在完整的 Procgen 基准测试(16 个环境)[22]和 Atari 100K 基准测试(26 个环境)[31]上测试了我们的代理。

Procgen(左)和 Atari(右)不同环境的渲染。

在 Procgen 基准测试中,我们将我们的双曲线实现与使用随机裁剪数据增强的方法[32]进行比较,这是一种通过引入人工选择的不变性来激励泛化的更传统方法。此外,我们还测试了双曲线模型的另一种版本,该版本进一步将最终表示的维度限制为 32(从原始欧几里得架构中的 256),旨在增加其对能够在双曲线空间中有效编码的特征的关注。

我们的双曲线 PPO 和 Rainbow DQN 实现都在绝大多数环境中表现出了显著的性能提升。值得注意的是,我们发现减少双曲线表示的大小对于这两种算法都提供了进一步的好处,显著提升了性能。

双曲线和欧几里得版本的 PPO 在 Procgen 上的表现。

双曲线和欧几里得版本的 Rainbow DQN 在 Procgen 上的表现。

相比之下,应用数据增强似乎带来了较低且不一致的收益。我们还发现,测试性能的提升并不总是与代理在探索中能够访问的特定 200 个训练级别的收益相关,导致双曲线代理的泛化差距显著减少。

同样,相同的双曲线深度 RL 框架在 Atari100K 基准测试中也提供了一致且显著的好处。双曲线 Rainbow 在大多数 Atari 环境中相较于欧几里得基线表现出改进,几乎将最终的人类归一化得分翻倍。

将我们正则化的双曲线表示整合到 Atari 基准测试中的 Rainbow DQN 上的归一化性能绝对差异(Y 轴)和相对改进(柱状图上方)。

总体而言,我们的结果实证验证了引入双曲线表示来塑造深度 RL 模型的先验在多种问题和算法中可能是极其有效的。

我们的双曲线强化学习(RL)代理与当前的 SotA(state-of-the-art)算法非常接近,这些算法采用了不同的昂贵和特定领域的做法(例如,临时辅助损失、更大的专用架构等)。综合来看,我们相信这些结果展示了我们双曲线框架的巨大潜力,使其有可能成为参数化深度 RL 模型的标准方法。

可视化

使用我们双曲 PPO 的二维版本进行可视化时,我们观察到一个重复出现的现象,即在轨迹的考虑子集内,表示的大小随着环境中更多障碍物和/或敌人的出现而单调增加。此外,我们观察到这些表示形成了树状结构,从编码策略状态获得的大小在方向上大多与价值函数的陀螺盘法线对齐。

这种增长直观地反映了随着新元素的出现,代理识别到更大的奖励机会(例如,通过击败新敌人获得的奖励),然而,也需要更精细的控制,因为与其他策略陀螺盘的距离也会指数增长,从而减少熵。相反,跟随随机行为的偏差,表示的大小趋向于在看起来几乎与价值陀螺盘法线正交的方向上增长。因此,这种增长仍然反映了优化决策所需的更高精度,同时也反映了代理在从次优行为中达到的状态中获取未来奖励的不确定性。

在 Procgen 中,随着我们在轨迹上前进的二维双曲嵌入,编码的状态遵循的是策略行为(绿色)或随机行为(红色)。

结论

我们的实验提供了使用双曲几何在深度强化学习中的优势和普遍性的有力证据,几乎在所有基准和强化学习算法类别中都取得了近乎普遍的改进。我们的发现表明,几何结构可以大大影响深度模型学习所诱导的先验,或许,提示我们应重新评估它在应对许多额外的机器学习挑战中的作用和相关性。例如,使用双曲空间也可能对无监督和离线强化学习产生影响,为处理这些问题设置中目标不明确和数据有限提供更合适的先验。

[1] R. S. Sutton 和 A. G. Barto, 强化学习:导论(2018 年)MIT 出版社,提供了对强化学习领域的全面介绍。另见其他优秀的 在线资源

[2] J. Schulman,邻近策略优化算法(2017 年)arXiv:1707.06347。

[3] PPO 通过限制策略更新对当前概率的变化> ϵ,并使用辅助熵奖励来提高稳定性。

[4] R. E. Bellman, 动态规划(2010 年)普林斯顿大学出版社。

[5] M. Hessel 等,Rainbow: 结合深度强化学习中的改进(2018 年) AAAI

[6] V. Mnih 等,通过深度强化学习实现人类水平的控制(2015 年) Nature 518 (7540):529–533。

[7] E. Cetin 等人,双曲深度强化学习(2023)ICLR。另见附带的代码

[8] 关于双曲空间及其在机器学习中早期应用的概述,请参阅布莱恩·肯格的一篇很棒的入门博客文章。有关更正式的介绍,请参见 J. W. Cannon 等人著的《双曲几何学》(1997),载于《几何学的风味》第 31 卷,第 59-115 页。

[9] 大多数人从学校几何学中了解这个知识:圆的面积(即二维球体的体积)是πr²。n维欧几里得球体半径为r的体积的一般公式是 πⁿᐟ²rⁿ / Γ(n/2 +1)。请注意,尽管它在r中是多项式的,但在维度n中是指数型的。因此,为了在欧几里得空间中表示(具有指数体积增长的)树状结构,必须增加维度。

[10] 双曲几何是非欧几里得几何首次成功的构造,其中经典的平行公设不成立。早期的失败尝试可追溯到奥马尔·海亚姆和乔凡尼·萨克雷里。首次成功构造的优先权在卡尔·弗里德里希·高斯、亚诺什·博伊亚伊和尼古拉·罗巴切夫斯基(第一个公布其结果的人)之间存在争议。尤金尼奥·贝尔特拉米(以及后来费利克斯·克莱因)展示了双曲几何的自洽性,并提出了以他们名字命名的投影模型(贝尔特拉米-克莱因模型)。

[11] M. Gromov,双曲群(1987),施普林格出版社。

[12] 这使得不同点之间的测地线必须经过某个半径较小的中点,类似于树状测地线必须经过它们最近的共享父节点。

[13] O. Ganea、G. Bécigneul 和 T. Hofmann,双曲神经网络(2018)NeurIPS

[14] 指数映射的闭式表达为 exp₀(v) = v tanh(v) / ||v||

[15] A. A. Ungar,解析双曲几何学与阿尔伯特·爱因斯坦的相对论(2008),世界科学出版社。

[16] 在双曲空间中,两向量的莫比乌斯加法定义为

xy = ((1 + 2x<x,y> + ||y||²)x + (1 + ||x||²)y) / (1 + 2<x,y> + ||x||² ||y||²),详见我们论文中的公式(4)[7]。

[17] G. Lebanon 和 J. Lafferty,多项式流形上的超平面间隔分类器(2004)ICML

[18] 距离的闭式表达为 d(x,H)=sinhᐨ¹(2|<x⊕–p,w>| / (1 — ||x⊕–p||²||w||)),详见我们论文中的公式(6)[7]。

[19] V. Khrulkov 等, 双曲图像嵌入 (2020) CVPR

[20] E. Mathieu 等, 具有庞加莱变分自编码器的连续层次表示 (2019) NeurIPS

[21] R. Raileanu 和 R. Fergus, 在强化学习中解耦价值和策略以实现泛化 (2021), ICML

[22] Procgen,由 K. Cobbe 等介绍, 利用程序生成来基准化强化学习 (2020) ICML,包括 16 个具有程序生成随机关卡的视觉环境。虽然不同的关卡共享一个高级目标(例如,达到门口,击败所有敌人等),但它们在布局和外观上可能有显著差异。此外,在这个基准测试中,代理仅能访问每个环境的前 200 个关卡进行经验收集,但其性能在所有关卡的分布上进行测试。因此,该基准测试允许评估强化学习代理,特别是其对未见关卡的泛化能力。

[23] M. Borassi、A. Chessa 和 G. Caldarelli, 双曲性度量现实世界网络中的民主 (2015) Physical Review E 92.3: 032812。

[24] L. Espeholt 等, Impala: 可扩展的分布式深度强化学习与重要性加权演员-学习者架构 (2018) ICML

[25] M. Nickel 和 D. Kiela, 用于学习层次表示的庞加莱嵌入 (2017) NeurIPS

[26] Y. Guo 等, 剪裁的双曲分类器是超级双曲分类器 (2022), CVPR

[27] I. Goodfellow 等, 生成对抗网络 (2014), NIPS

[28] M. Arjovsky 和 L. Bottou, 朝着有原则的方法训练生成对抗网络 (2017) ICLR

[29] T. Miyato 等, 生成对抗网络的谱归一化 (2018) arXiv:1802.05957。

[30] Z. Lin、V. Sekar 和 G. Fanti, 为何谱归一化稳定生成对抗网络:分析与改进 (2021), NeurIPS

[31] Atari 100K,由 Kaiser、Lukasz 等人介绍,基于模型的强化学习用于 Atari (2019) arXiv:1903.00374,包含了 26 种不同的视觉环境,这些环境由 M. G. Bellemare 等人提供,包含了 M. G. Bellemare 等人提出的经典 Atari 游戏,街机学习环境:一种通用代理的评估平台 (2013) 人工智能研究期刊 47: 253–279。然而,代理只能访问 100K 总环境步数的数据来进行经验收集,这大致相当于 2 小时的游戏时间。环境经过 M. C. Machado 等人提出的规格进行修改,重新审视街机学习环境:通用代理的评估协议和开放问题 (2018) 人工智能研究期刊 61:523–562,介绍了通过 粘性动作(即每次执行动作的随机重复)引入了相当大的随机性。因此,由于严重的数据限制和额外的随机性,这一基准特别关注评估对未见状态的泛化能力。

[32] D. Yarats、I. Kostrikov 和 R. Fergus,图像增强就是你所需要的:从像素中正则化深度强化学习 (2021),ICLR

我们感谢 David Ha (即 hardmaru) 为我们生成了标题图像,这是本博客上第一张 AI 生成的插图!有关更多信息,请参阅 项目网页 Towards Data Science Medium 博客文章, 订阅 Michael 的文章和 YouTube 频道,获取 Medium 会员资格,或关注 Michael Edoardo BenJonathan 在 Twitter 上的动态。

使用 SQL 实现的 HyperLogLog

原文:towardsdatascience.com/hyperloglog-implemented-using-sql-d516fc4828ce?source=collection_archive---------16-----------------------#2023-01-23

我们查看一个完全使用声明式 SQL 编写的 HyperLogLog 基数估计算法的实现

Dhruv MataniTowards Data Science Dhruv Matani

·

跟随 发表在 Towards Data Science ·6 min read·2023 年 1 月 23 日

--

照片由 Vidar Smits 提供,发布在 Unsplash

HyperLogLog 算法是一种极为流行的算法,用于估算(近似)给定数据集中的唯一元素数量。这与 SQL 中的精确唯一计数有所不同。

COUNT(DISTINCT x)

  1. 执行准确唯一计数。

  2. 使用O(唯一元素) 额外内存。

HyperLogLog

  1. 执行近似唯一计数。

  2. 使用O(1)额外内存。当你需要跟踪数百万或数十亿个唯一值时,这大大减少了内存需求。

如果你想了解关于 LogLog 和 HyperLogLog 算法的详细信息,可以在这里找到。

HyperLogLog 工作原理的基本直觉

问:你需要多少次抛硬币才能得到 3 次连续的正面?

我们期望在抛一次硬币时看到以下结果。

H
T

我们期望在抛两次硬币时看到以下结果。

HH
HT
TH
TT

我们期望在抛三次硬币时看到以下结果。

HHH
HHT
HTH
HTT
THH
THT
TTH
TTT

期望你需要大约 8 次(2³)抛硬币才能看到 3 次连续的正面。

问:你需要多少次抛硬币才能得到 4 次连续的正面?

同样,期望你需要大约 16 次(2⁴)抛硬币才能看到 4 次连续的正面。

输入预处理

对你的每个输入元素使用一个生成 64 位哈希的哈希算法进行哈希,确保哈希结果是随机且均匀的。也就是说,任何位上的 0(或 1)的概率是 1/2。

现在,从这个比特串的最低有效位(BIT)开始,计数连续 0 位的数量。这应该让你想起我们上面讨论的连续正面问题。运用相同的直觉,我们期望每次看到 2^K 个唯一哈希时都会看到一串 K 个 0。我们假设每个元素哈希到一个唯一的哈希值,并且哈希值具有随机位流分布。

第一版

示例比特串(作者提供的图片)

我们只跟踪最长的比特位置(从位置 1 开始),即一串连续 0 结束的位置。也就是说,如果我们看到一些数字在比特位置 3、2、2、2、1、4 有 1,那么我们只跟踪 4。

看到的唯一值数量的近似值是 8。

缺点

上述方法的主要缺点是:

  1. 我们只能估算 2 的幂的数量。也就是说,一旦看到一串零,我们估计数量为 2^K。我们不能估算 2 个连续的 2 的幂之间的任何数量。这对于较大的 2 的幂来说范围相当广泛。

  2. 我们可能会非常不幸地看到一串(比如)6 个零,仅仅代表 1 个唯一元素。这意味着我们会估计看到 2⁶ = 256 个唯一元素,而实际上我们只看到 1 个。

第二版(LogLog 估算器)

为了解决上述两个缺点,我们可以进行 2 个更改。

  1. 维护 2^B 个计数器,而不仅仅是一个计数器。每个计数器由对相同输入应用的不同哈希更新。如果假设输入中共有 N 个元素,那么每个计数器大致计数 N/(2^B)个输入元素中的唯一元素数。

  2. 计算 2^K 作为唯一元素的数量时,取所有 2^B 个计数器的平均值,然后计算 2^avg(K)。将结果乘以 2^B(桶的数量)。

作为优化,我们可以取哈希的最后 B 位,并用它来计算桶,然后使用其余的位(右移 B 位)如上面所述。

将位串分成桶和随机哈希位串(作者图像)

每个桶包含一个 LogLog 估计器(作者图像)

这基本上是LogLog 估计器

小知识:这叫做 LogLog,因为它需要log2(log2(n)) 位来存储表示数字“n”所需的位数。例如,如果 n == 2^b,则“n”有“b”位,而 b == log2(n)。我们需要 log2(b) == log2(log2(n))位来存储值“b”。“b”也表示在其后的所有位都为 0 的情况下,可以有 1 位的最大位置(这是我们要测量的每个哈希的情况)。

第三版(HyperLogLog)

我们不是取值的算术均值(平均值)再平方,而是取值的调和均值

由于一组数字的调和均值强烈趋向于列表中的最小元素,它比算术均值更能减轻大异常值的影响,而加重小异常值的影响。

就是这样!

SQL 输入模式

输入表非常简单。基本上是一个包含单一整数列(输入整数)的表。

CREATE TABLE input_data(n INT NOT NULL);

SQL 中的 HyperLogLog

INSERT INTO input_data

WITH RECURSIVE fill_n(ctr, n) AS (
  SELECT 4000, (RANDOM() * 2000)::INT

  UNION ALL

  SELECT ctr-1, (RANDOM() * 2000)::INT
  FROM fill_n
  WHERE ctr > 0
)

SELECT n FROM fill_n;

SELECT COUNT(DISTINCT n) AS num_unique_actual FROM input_data;

WITH hashed_list AS (
  SELECT
    ('x' || SUBSTR(md5(n::TEXT), 1, 16))::BIT(64)::BIGINT AS h
  FROM input_data
),

bucketed AS (
  SELECT
    -- Keep 64 buckets
    h & (63::BIGINT) AS bucket_id,
    (h >> 6) & 2147483647::BIGINT AS h_key
  FROM hashed_list
),

loglog_mapped AS (
  SELECT
    bucket_id,
    -- The following computes the position of the first 1 bit
    -- when looking at the number from right (least significant
    -- bit) to left.
    LOG(h_key & -h_key) / LOG(2.0) AS fsb,
    h_Key
  FROM bucketed
),

max_in_bucket AS (
  SELECT
    bucket_id,
    -- Retain the largest bit position in any bucket.
    MAX(fsb) AS fsb
  FROM loglog_mapped
  GROUP BY 1
),

harmonic_mean_mapped AS (
  SELECT
    AVG(fsb) AS avg_fsb,
    COUNT(1) / SUM(1.0 / (CASE WHEN fsb = 0 THEN 1 ELSE fsb END)) hm_fsb,
    -- Number of unique elements using the LogLog estimator.
    COUNT(1) * POW(2, AVG(fsb)) / 0.77351 AS num_unique_log_log,
    -- Number of unique elements using the HyperLogLog estimator.
    COUNT(1) * POW(
      2, COUNT(1) / SUM(1.0 / (CASE WHEN fsb = 0 THEN 1 ELSE fsb END))
    ) / 0.77351 AS num_unique_hll
  FROM max_in_bucket
  WHERE fsb > 0
)

SELECT * FROM harmonic_mean_mapped;

这是运行上述查询的结果。

实际与估计的唯一数字计数(作者图像)

误差率: 输入表中唯一元素的实际数量是 1729。LogLog 估计器在这种情况下相差很大,估计为 2795 个唯一元素。HyperLogLog估计器的误差为 3.6%,估计为 1792 个唯一元素。

通过使用更多的桶,我们可以得到更好的估计(更小的误差)。

桶: 上述实现使用了 64 个桶。由于我们可以在一个 8 位(单字节)数字中实际存储 0 到 63 之间的任何数字,因此我们为上面显示的实现使用了 64 个字节。

根据上述维基百科文章,

HyperLogLog 算法能够估计> 109 的基数,典型准确度(标准误差)为 2%,使用 1.5 kB 的内存。

空间: 上述实现由于查询编写方式使用了 O(n)的额外空间。实际(非玩具)实现不会存储计算出的哈希,而只是更新内存中的桶。

哈希计算: 我们在上述查询中使用了 md5,但你可以使用任何生成均匀随机 64 位数字的哈希。

关于校正因子常数 0.77351 的一些细节我为了简洁起见省略了。你可以阅读论文以了解有关此常数的详细信息。

SQL Fiddle

上述查询的 SQL Fiddle 可以在这里找到。

结论

HyperLogLog 算法既简单又强大,同时每个存储位能存储大量信息!

之前的文章: 使用 SQL 验证字符串是否为 HTML

超参数优化——网格搜索、随机搜索和贝叶斯优化的简介与实现

原文:towardsdatascience.com/hyperparameter-optimization-intro-and-implementation-of-grid-search-random-search-and-bayesian-b2f16c00578a

提升机器学习结果的最常见超参数优化方法

Farzad MahmoodinobarTowards Data Science Farzad Mahmoodinobar

·发表于数据科学前沿·阅读时间 10 分钟·2023 年 3 月 13 日

--

图片由乔纳斯·贾肯提供,来自Unsplash

通常,尝试提高机器学习模型性能时,第一个想到的解决方案是增加更多的训练数据。额外的数据通常有帮助(在某些情况下除外),但生成高质量数据可能非常昂贵。超参数优化可以通过利用现有数据来节省时间和资源,以获得最佳模型性能。

超参数优化,顾名思义,是识别机器学习模型最佳超参数组合的过程,以满足优化函数(即在给定的数据集上最大化模型性能)。换句话说,每个模型都有多个我们可以调整的旋钮和杠杆,直到我们找到优化的组合。在超参数优化过程中,我们可以调整的一些参数示例包括学习率、神经网络的架构(例如,隐藏层的数量)、正则化等。

在这篇文章中,我们将概念性地介绍三种最常见的超参数优化方法,即网格搜索、随机搜索和贝叶斯优化,并进行实现。

我将在这里包含一个高层次的对比表,以便将来参考,然后在文章的其余部分将进一步探讨、解释和实现每一种方法。

表 1——超参数优化方法比较

让我们开始吧!

(除非另有说明,所有图片均由作者提供。)

## 通过我的推荐链接加入 Medium - Farzad Mahmoodinobar

阅读 Farzad(以及 Medium 上其他作者)的每个故事。你的会员费用直接支持 Farzad 和其他人…

medium.com

1. 网格搜索

网格搜索可能是最简单且最直观的超参数优化方法,它涉及在定义的搜索空间中穷尽地寻找最佳超参数组合。在这个上下文中,“搜索空间”是指在优化过程中考虑的所有超参数及其值。让我们通过一个例子更好地理解网格搜索。

假设我们有一个机器学习模型,它只有三个参数,每个参数可以取下面列表中的值:

  1. parameter_1 = [1, 2, 3]

  2. parameter_2 = [a, b, c]

  3. parameter_3 = [x, y, z]

我们不知道这些参数的哪种组合能够优化我们模型的优化函数(即,为我们的机器学习模型提供最佳输出)。在网格搜索中,我们简单地尝试这些参数的每一个组合,测量每个组合模型的性能,然后选择产生最佳性能的组合!在这个例子中,parameter_1 可以取 3 个值(即 1、2 或 3),parameter_2 可以取 3 个值(即 a、b 和 c),parameter_3 可以取 3 个值(即 x、y 和 z)。换句话说,总共有 333=27 种组合。在这个例子中,网格搜索将涉及 27 轮评估 ML 模型性能,以找到表现最佳的组合。

正如你所见,这种方法非常简单(类似于试错任务),但它也有一些局限性。让我们一起总结优缺点:

优点:

  • 易于理解和实现

  • 易于并行化

  • 适用于离散和连续空间

缺点:

  • 对于具有较多超参数的大型和/或复杂模型来说成本较高(因为需要尝试和评估所有组合)

  • 无记忆性 — 不从过去的观察中学习

  • 如果搜索空间过大,可能无法找到最佳组合

我的建议是,如果你有一个简单的模型,且搜索空间较小,请使用网格搜索。否则,继续阅读,寻找更适合较大搜索空间的解决方案。

让我们用一个实际的例子来实现网格搜索。

1.1. 网格搜索 — 实现

为了实现网格搜索,我们将使用来自 scikit-learn 的鸢尾花数据集 创建一个随机森林分类模型。该数据集包含三种不同的鸢尾花的花瓣和萼片长度,将用于本次分类练习。对于本帖的目的,模型开发的优先级较低,因为目标是比较各种超参数优化策略的性能。我鼓励你关注模型评估结果以及每种超参数优化方法达到其选定超参数集所需的时间。我将描述结果,并提供一个本帖中使用的三种方法的总结比较表。

搜索空间,即包括所有超参数值的空间,定义如下:

search_space = {'n_estimators': [10, 100, 500, 1000],
              'max_depth': [2, 10, 25, 50, 100],
              'min_samples_split': [2, 5, 10],
              'min_samples_leaf': [1, 5, 10]}

上述搜索空间由 453*3=180 种超参数组合组成。我们将使用网格搜索找到优化目标函数的组合,如下所示:

# Import libraries
from sklearn.model_selection import GridSearchCV
from sklearn.datasets import load_iris
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
import time

# Load Iris dataset
iris = load_iris()
X, y = iris.data, iris.target

# Define the hyperparameter search space
search_space = {'n_estimators': [10, 100, 500, 1000],
              'max_depth': [2, 10, 25, 50, 100],
              'min_samples_split': [2, 5, 10],
              'min_samples_leaf': [1, 5, 10]}

# Define the random forest classifier
clf = RandomForestClassifier(random_state=1234)

# Create the optimizer object
optimizer = GridSearchCV(clf, search_space, cv=5, scoring='accuracy')

# Store start time to calculate total elapsed time
start_time = time.time()

# Fit the optimizer on the data
optimizer.fit(X, y)

# Store end time to calculate total elapsed time
end_time = time.time()

# Print the best set of hyperparameters and corresponding score
print(f"selected hyperparameters:")
print(optimizer.best_params_)
print("")
print(f"best_score: {optimizer.best_score_}")
print(f"elapsed_time: {round(end_time-start_time, 1)}")

结果:

网格搜索结果

在这里,我们可以看到使用网格搜索选择的超参数值。best_score 描述了使用选定超参数集的评估结果,elapsed_time 描述了我的本地笔记本计算执行这个超参数优化策略所需的时间。请记住评估结果和耗时,以便在我们讨论下一些方法时进行比较。现在,让我们继续讨论随机搜索。

2. 随机搜索

随机搜索,顾名思义,是从定义的搜索空间中随机抽取超参数。与网格搜索逐一遍历每种超参数值组合不同,随机搜索仅选择一组预定义次数(取决于可用资源,如时间、预算、目标等)的超参数值的随机子集,为每组计算机器学习模型的性能,然后选择最佳的那一组。

根据上述描述,你可以想象,随机搜索比完全的网格搜索成本更低,但仍有其自身的优缺点,如下所示:

优点:

  • 易于理解和实现

  • 易于并行化

  • 适用于离散空间和连续空间

  • 比网格搜索便宜

  • 与相同尝试次数的网格搜索相比,更有可能收敛到最优解

缺点:

  • 无记忆 — 不从过去的观察中学习

  • 由于随机选择,可能会遗漏重要的超参数值

在下一种方法中,我们将通过贝叶斯优化解决网格搜索和随机搜索的“无记忆”缺点。但在此之前,让我们先实现随机搜索。

2.1. 随机搜索 — 实现

使用下面的代码片段,我们将为网格搜索实现中描述的相同问题实施随机搜索超参数优化。

# Import libraries
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import randint

# Create a RandomizedSearchCV object
optimizer = RandomizedSearchCV(clf, param_distributions=search_space,
                               n_iter=50, cv=5, scoring='accuracy',
                               random_state=1234)

# Store start time to calculate total elapsed time
start_time = time.time()

# Fit the optimizer on the data
optimizer.fit(X, y)

# Store end time to calculate total elapsed time
end_time = time.time()

# Print the best set of hyperparameters and corresponding score
print(f"selected hyperparameters:")
print(optimizer.best_params_)
print("")
print(f"best_score: {optimizer.best_score_}")
print(f"elapsed_time: {round(end_time-start_time, 1)}")

结果:

随机搜索结果

与网格搜索结果相比,这些结果相当有趣。best_score保持不变,但elapsed_time从 352.0 秒减少到 75.5 秒!这真是令人印象深刻!换句话说,随机搜索成功找到了一组超参数,其性能与网格搜索相同,但所需时间仅为网格搜索的约 21%!这效率高得多。

接下来,让我们继续讨论下一个方法,称为贝叶斯优化,它从每次优化尝试中学习。

3. 贝叶斯优化

贝叶斯优化是一种超参数优化方法论,它使用概率模型从之前的尝试中“学习”,并指导搜索以找到优化机器学习模型目标函数的超参数的最佳组合。

贝叶斯优化方法可以分解为 4 个步骤,我将在下面进行描述。我鼓励你通读这些步骤以更好地理解该过程,但使用此方法并不要求掌握这些知识。

  1. 定义一个“先验”,这是一个关于我们在某个时间点关于最有可能优化目标函数的超参数组合的信念的概率模型

  2. 对一组超参数评估模型

  3. 利用第 2 步获得的知识,更新第 1 步中的概率模型(即我们称之为“先验”)关于我们认为最有可能优化目标函数的超参数组合。我们更新后的信念称为“后验”。换句话说,第 2 步获得的知识帮助我们更好地理解搜索空间,将我们从先验转变为后验,使后验成为我们关于搜索空间和目标函数的“最新”知识,由第 2 步提供信息。

  4. 重复步骤 2 和 3,直到模型性能收敛、资源耗尽或满足其他预定义的指标

如果你对贝叶斯优化的详细信息感兴趣,可以查看以下文章:

[## 机器学习中的贝叶斯优化

这篇文章讲述了通过贝叶斯优化进行超参数优化的内容。这个任务旨在帮助选择一组...

medium.com](https://medium.com/@fmnobar/conceptual-overview-of-bayesian-optimization-for-parameter-tuning-in-machine-learning-a3b1b4b9339f?source=post_page-----b2f16c00578a--------------------------------)

现在我们了解了贝叶斯优化的工作原理,让我们来看看它的优缺点。

优势:

  • 从过去的观察中学习,因此更高效——换句话说,预计在更少的迭代中找到更好的超参数集,相比于没有记忆的方法论

  • 在特定假设下会收敛到最优解

缺点:

  • 难以并行化

  • 每次迭代的计算量比网格搜索和随机搜索要大

  • 贝叶斯优化中先验的初始概率分布和所用的函数(例如获取函数等)的选择会显著影响性能和学习曲线

详细信息已经说明,让我们实现贝叶斯优化并查看结果。

3.1. 贝叶斯优化 — 实施

与上一节类似,我们将使用下面的代码片段来实现贝叶斯优化超参数优化,针对与网格搜索实现中描述的相同问题。

# Import libraries
from skopt import BayesSearchCV

# Perform Bayesian Optimization
optimizer = BayesSearchCV(estimator=RandomForestClassifier(),
                          search_spaces=search_space,
                          n_iter=10,
                          cv=5,
                          scoring='accuracy',
                          random_state=1234)

# Store start time to calculate total elapsed time
start_time = time.time()

optimizer.fit(X, y)

# Store end time to calculate total elapsed time
end_time = time.time()

# Print the best set of hyperparameters and corresponding score
print(f"selected hyperparameters:")
print(optimizer.best_params_)
print("")
print(f"best_score: {optimizer.best_score_}")
print(f"elapsed_time: {round(end_time-start_time, 1)}")

结果:

贝叶斯优化结果

另一个有趣的结果集!best_score与我们通过网格搜索和随机搜索所能达到的相同,但这些结果仅在 23.1 秒内完成,相比之下,随机搜索耗时 75.5 秒,网格搜索耗时 352.0 秒!换句话说,使用贝叶斯优化所需的时间比网格搜索少约 93%。这是一个巨大的生产力提升,对于更大更复杂的模型和搜索空间来说意义更为重大。

请注意,贝叶斯优化仅用 10 次迭代就达到了这些结果,因为它可以从之前的迭代中学习(与随机搜索和网格搜索不同)。

结果比较

以下表格对三种方法的结果进行了比较。“方法论”列描述了所使用的超参数优化方法。接下来是每种方法所选的超参数。“最佳得分”是使用特定方法获得的分数,接下来是“经过时间”,这表示优化策略在我的本地笔记本电脑上运行所需的时间。最后一列“提高效率”假设网格搜索为基准,然后计算相对于网格搜索的其他两种方法的效率提升(使用经过时间)。例如,由于随机搜索耗时 75.5 秒,而网格搜索耗时 352.0 秒,因此相对于网格搜索的基准,随机搜索的效率提升计算为 1–75.5/352.0=78.5%。

表 2 — 方法性能比较表

从上面的比较表中可以得出两个主要结论:

  1. 效率: 我们可以看到,像贝叶斯优化这样的学习方法可以在更短的时间内找到优化的超参数集。

  2. 参数选择: 可能有不止一个正确答案。例如,贝叶斯优化所选的参数与网格搜索和随机搜索的参数不同,尽管评估指标(即best_score)保持不变。在更大和更复杂的设置中,这一点可能更加重要。

结论

在这篇文章中,我们讨论了超参数优化是什么,并介绍了三种用于此优化任务的最常见方法。然后,我们详细讲解了这三种方法,并在分类任务中实现了它们。最后,我们比较了这三种方法的实施结果。我们发现,像贝叶斯优化这样的能够从之前尝试中学习的方法,可能会显著更高效,这在大型和复杂模型(例如深度神经网络)中尤为重要,因为效率可能成为决定性因素。

感谢阅读!

如果你觉得这篇文章有帮助,请在 Medium 上关注我并订阅以接收我最新的文章!

超参数优化与 Hyperopt — 介绍与实现

原文:towardsdatascience.com/hyperparameter-optimization-with-hyperopt-intro-implementation-dfc1c54d0ba7

通过超参数优化提升机器学习模型的性能。

Farzad MahmoodinobarTowards Data Science Farzad Mahmoodinobar

·发布于 Towards Data Science ·阅读时间 11 分钟·2023 年 6 月 5 日

--

照片由 Te NGuyen 提供,来自 Unsplash

Hyperopt 是一个开源的超参数优化工具,我个人使用它来提升我的机器学习项目,并发现它实现起来相当简单。超参数优化是识别最佳超参数组合的过程,以使机器学习模型满足目标函数(通常定义为“最小化”目标函数以保持一致)。换句话说,每个机器学习模型都有各种旋钮和杠杆,我们可以调节这些参数,直到获得我们所期望的结果。找到能得到我们所期望结果的正确超参数组合的过程称为超参数优化。一些这样的参数示例包括:学习率、神经网络的架构(如隐藏层数量)、优化器的选择等。

如果你对探索其他超参数优化策略感兴趣,如网格搜索、随机搜索和贝叶斯优化,请查看下面的帖子:

[## 超参数优化 — 网格搜索、随机搜索和贝叶斯优化的介绍与实现]

提升机器学习成果的最常见超参数优化方法。

towardsdatascience.com](/hyperparameter-optimization-intro-and-implementation-of-grid-search-random-search-and-bayesian-b2f16c00578a?source=post_page-----dfc1c54d0ba7--------------------------------)

让我们开始吧!

[## 使用我的推荐链接加入 Medium

阅读 Farzad(以及 Medium 上其他作者)的每一个故事。您的会员费用直接支持 Farzad 和其他人……

medium.com](https://medium.com/@fmnobar/membership?source=post_page-----dfc1c54d0ba7--------------------------------)

1. 基础

1.1. 概念与安装

首先定义一些使用 Hyperopt 的相关概念。

  • 目标函数: 这是超参数优化尝试最小化的函数。更具体地说,目标函数接受一组超参数作为输入,并返回模型的错误水平(即损失),给定这些接受的超参数。超参数优化的目标是找到使该错误/损失最小化的超参数组合。

  • 搜索空间: 目标函数接受作为参数的输入值范围(即参数)。

  • 优化算法: 顾名思义,这是一种用于最小化目标函数的算法。Hyperopt 利用不同的搜索算法,例如随机搜索和 Parzen 估计树(TPE)(文档)。

现在我们对这些概念已经很熟悉了,让我们通过运行以下命令来安装 Hyperopt

pip install hyperopt

现在我们已经安装了库,我们将首先通过一个非常简单的示例来了解 Hyperopt 是如何工作的。之后,我们将继续处理更有趣和复杂的示例。

1.2. 简单示例

让我们从一个非常简单的例子开始,以帮助我们理解使用 Hyperopt 进行超参数优化的整体过程。我们将从一个二次函数 f(x) = (x — 1)² 开始。这个函数的优化点在 x = 1,因此我们知道期望是什么。由于我们已经有一段时间没上过微积分课了,让我们查看该函数的图,这有助于我们更好地理解这个点如何最小化该函数。以下代码块将返回该图:

# Import libraries
import numpy as np
import matplotlib.pyplot as plt

# Define the function
def f(x):
    return (x - 1) ** 2

# Generate x values from -5 to 5
x = np.linspace(-4, 6, 100)

# Calculate corresponding y values
y = f(x)

# Find the minimum point
min_point = np.min(y)

# Create the plot
plt.plot(x, y, label='f(x) = (x-1)²')
plt.xlabel('x')
plt.ylabel('f(x)')
plt.title('f(x) = (x-1)²')

# Set the x-axis limits
plt.xlim(-4, 6)

# Add a horizontal dashed line at the minimum point
plt.axhline(y=min_point, color='red', linestyle='dashed', label='Minimum Point')

# Add a legend
plt.legend()

# Display the plot
plt.show()

结果:

f(x) = (x-1)² 的图

正如我们所见,最小点发生在 x=1。让我们使用 Hyperopt 实现这一点,看看它是如何工作的。

为了实现这一点,我们将采取以下步骤:

  1. 导入必要的库和包

  2. 定义目标函数和搜索空间

  3. 运行优化过程

  4. 打印结果(即我们期望的优化点是 x = 1

以下代码块,按照上述步骤:

# 1\. Import necessary libraries and packages
from hyperopt import hp, fmin, tpe, Trials

# 2\. Define the objective function and the search space
def objective_function(x):
    return (x - 1)**2

search_space = hp.uniform('x', -2, 2)

# 3\. Run the optimization process

# Trials object to store the results
trials = Trials()

# Run the optimization
best = fmin(fn=objective_function, space=search_space, algo=tpe.suggest, trials=trials, max_evals=100)

# 4\. Print the results
print(best)

结果:

“best” 返回模型能够找到的最佳超参数组合,在这种情况下,它几乎等于 x = 1,正如我们所期望的那样!实现 Hyperopt 的过程通常是相同的,现在我们已经完成了一个简单的示例,让我们继续一个更高级的示例。

2. Hyperopt 实现

我们将实现以下两个独立的示例:

  1. 支持向量机的分类

  2. 使用随机森林回归器的回归

我们将详细讲解这两个示例中的每一个。

2.1. 支持向量机和鸢尾花数据集

在之前的 文章 中,我使用了网格搜索、随机搜索和贝叶斯优化来进行超参数优化,数据集使用了 scikit-learn 提供的鸢尾花数据集。鸢尾花数据集包括三种不同的鸢尾花的花瓣和萼片长度,是分类练习中常用的数据集。在本篇文章中,我们将使用相同的数据集,但我们将使用支持向量机(SVM)作为模型,并对以下两个参数进行优化:

  • C:正则化参数,用于权衡训练样本的错误分类与决策面简洁性的 trade-off。

  • gamma:核系数,定义了单个训练样本的影响程度。gamma 值越大,其他样本必须越接近才能被影响。

由于本练习的目标是进行超参数优化,我不会深入探讨 SVM 的具体操作,但如果你感兴趣,我发现 这篇 scikit-learn 的文章很有帮助。

我们将大致遵循之前简单示例中使用的相同步骤,但在最后也会可视化该过程:

1. 导入必要的库和包

2. 定义目标函数和搜索空间

3. 运行优化过程

4. 可视化优化过程

2.1.1. 步骤 1 — 导入库和包

让我们先导入库和包,然后加载数据集。

# Import libraries and packages
from sklearn import datasets
from sklearn.svm import SVC
from sklearn.model_selection import cross_val_score

# Load Iris dataset
iris = datasets.load_iris()
X = iris.data
y = iris.target

2.1.2. 步骤 2 — 定义目标函数和搜索空间

让我们首先从定义目标函数开始,目标函数将训练一个 SVM,并返回交叉验证得分的负值——这是我们想要最小化的。请注意,我们最小化交叉验证得分的负值,以便与“最小化”目标函数的一般目标保持一致(而不是“最大化”交叉验证得分)。

def objective_function(parameters):
    clf = SVC(**parameters)
    score = cross_val_score(clf, X, y, cv=5).mean()
    return -score

接下来我们将定义搜索空间,这些空间包括我们可以为 Cgamma 参数选择的值。请注意,我们将使用 Hyperopt 的 hp.uniform(label, low, high),它返回一个在“low”和“high”之间均匀分布的值(source)。

# Search Space
search_space = {
    'C': hp.uniform('C', 0.1, 10),
    'gamma': hp.uniform('gamma', 0.01, 1)
}

2.1.3. 运行优化

与之前的简单示例相同,我们将使用 TPE 算法,并将结果存储在Trials对象中。

# Trials object to store the results
trials = Trials()

# Run optimization
best = fmin(fn=objective_function, space=search_space, algo=tpe.suggest, trials=trials, max_evals=100)

结果:

2.1.4. 可视化优化

正如我们从简单示例中记得的那样,“最佳”包括 Hyperopt 根据实施的优化策略找到的超参数集合。让我们看看结果!

print(best)

如预期的那样,现在我们有了一组超参数,这些超参数能够最小化优化函数,使用了 Hyperopt。

让我们直观地观察目标函数值如何随着超参数的变化而变化。我们将从定义一个名为plot_obj_vs_hp()的函数开始,该函数实现这个可视化。然后使用这个函数来可视化结果。一定要注意红点——它表示根据我们的超参数优化找到的最佳超参数组合!

# Import libraries
import matplotlib.pyplot as plt

def plot_obj_vs_hp(trials, search_space, best):
    # Extract the results
    results = trials.trials

    # Create a list of hyperparameters
    hyperparameters = list(search_space.keys())

    # Create a new figure with 2 subplots side by side
    fig, axes = plt.subplots(1, 2, figsize=(12, 6))

    # Loop through hyperparameters and generate plots
    for idx, hp in enumerate(hyperparameters):
        # Extract the values of a given hyperparameter
        hp_values = [res['misc']['vals'][f'{hp}'] for res in results]

        # Flatten the list of values
        hp_values = [item for sublist in hp_values for item in sublist]

        # Extract the corresponding objective function values
        objective_values = [res['result']['loss'] for res in results]

        # Create the scatter plot
        axes[idx].scatter(hp_values, objective_values, label='Trial Hyperparameter Combinations')

        # Highlight the best hyperparameters
        axes[idx].scatter(best[hp], min(objective_values), color='red', label='Best Hyperparameter Combinations')
        axes[idx].set_xlabel(f'{hp}')
        axes[idx].set_ylabel('Loss')
        axes[idx].set_title(f'Loss vs. {hp}')
        axes[idx].legend(loc='upper right')

    plt.tight_layout()
    plt.show()
# Plot optimization vs. hyperparameters in 2D
plot_obj_vs_hp(trials, search_space, best)

结果:

损失与 C 和 gamma 超参数的子图

请注意,由于Cgamma实际上彼此没有关系,我们将它们分别展示与目标函数变化的关系。由于我们希望目标函数最小化,所以我们寻找的是上述图表中最低的点,并且根据超参数优化的结果,我们知道我们要找的是{'C': 5.164418859504847, 'gamma': 0.07084064498886927},这会导致目标函数损失约为-0.986,并由红点标示。

我也很好奇以三维方式查看这些图表,所以我创建了下面的函数来实现这一点。让我们看看图表。

# Import libraries
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt

# Define 3D plot function
def plot_obj_vs_hp_3d(trials, search_space, best):
    # Extract the results
    results = trials.trials

    # Create a list of hyperparameters
    hyperparameters = list(search_space.keys())

    # Extract the values of hyperparameters
    hp_values_0 = [res['misc']['vals'][f'{hyperparameters[0]}'] for res in results]
    hp_values_1 = [res['misc']['vals'][f'{hyperparameters[1]}'] for res in results]

    # Flatten the lists of values
    hp_values_0 = [item for sublist in hp_values_0 for item in sublist]
    hp_values_1 = [item for sublist in hp_values_1 for item in sublist]

    # Extract the corresponding objective function values
    objective_values = [res['result']['loss'] for res in results]

    # Create a new figure
    fig = plt.figure(figsize=(10, 7))

    # Add a 3D subplot
    ax = fig.add_subplot(111, projection='3d')

    # Create the scatter plot
    scatter = ax.scatter(hp_values_0, hp_values_1, objective_values, c=objective_values, cmap='viridis', label='Trial hyperparameters')

    # Highlight the best hyperparameters
    ax.scatter(best[hyperparameters[0]], best[hyperparameters[1]], min(objective_values), color='red', label='Best hyperparameters')

    # Add labels using hyperparameters from search_space
    ax.set_xlabel(hyperparameters[0])
    ax.set_ylabel(hyperparameters[1])
    ax.set_zlabel('Loss')
    ax.set_title('Loss Across Hyperparameters')
    fig.colorbar(scatter)
    ax.legend(loc='upper right')

    plt.show()
# Plot optimization vs. hyperparameters in 3D
plot_obj_vs_hp_3d(trials, search_space, best)

结果:

损失函数与 C 和 gamma 超参数的三维表示

诚然,这不太容易阅读,但我们还是试试看。我们在寻找最低的损失,即图表上最暗的点(红点几乎被一个黑点隐藏)。从视觉上看,它与我们之前生成的二维图表一致。

接下来,让我们专注于一个回归示例。

2.2. 随机森林与糖尿病数据集

这个示例关注于一个回归模型,该模型试图测量基准期后一年内疾病的发展情况。这个数据集同样来自于scikit-learn,但不同之处在于这个数据集主要用于回归(而不是我们在鸢尾花示例中看到的分类)。

如果你有兴趣了解回归和分类之间的区别,请查看下面的文章。

## 机器学习中的分类 vs. 回归 — 我该使用哪个?

概述

medium.com

我们将使用一个 随机森林回归器 模型进行本示例,并将优化以下两个超参数的目标函数:

  • n_estimators:随机森林中的树的数量

  • max_depth:随机森林中树的最大深度

优化的整体过程与我们迄今为止所做的相同。所以,让我们将其分解为四个常规步骤!

2.2.1. 第 1 步 — 导入库和包

我们将从导入库和包开始,然后加载数据集。

# Import libraries and packages
from sklearn import datasets
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import cross_val_score
from hyperopt import fmin, tpe, hp, Trials
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

# Load Diabetes dataset
diabetes = datasets.load_diabetes()
X = diabetes.data
y = diabetes.target

2.2.2. 第 2 步 — 定义目标函数和搜索空间

与上次类似,我们首先定义目标函数,该函数将训练我们的随机森林回归器并返回交叉验证分数的负值。

接下来,我们将定义搜索空间,包括 n_estimatorsmax_depth 参数可以取的值。注意,我们将使用 Hyperopt 的 hp.choice(label, options),它为超参数(即 label)提供一个值以及该超参数的可能值(即 options) (source)。

# Define objective function
def objective_function(parameters):
    # Initiate RandomForestRegressor
    regressor = RandomForestRegressor(**parameters)

    # Calculate the mean cross-validation score using 5 folds
    score = cross_val_score(regressor, X, y, cv=5).mean()

    return -score

# Define search Space
search_space = {
    'n_estimators': hp.choice('n_estimators', range(10, 300)),
    'max_depth': hp.choice('max_depth', range(1, 30)),
}

2.2.3. 运行优化

与之前的示例相同,我们将使用 TPE 算法,并将结果存储在 Trials 对象中。

# Trials object to store the results
trials = Trials()

# Run optimization
best = fmin(fn=objective_function, space=search_space, algo=tpe.suggest, trials=trials, max_evals=100)

结果:

2.2.4. 可视化优化

正如我们从简单示例中记得的,“最佳”包括 Hyperopt 基于实施的优化策略找到的超参数集合。让我们来看看结果!

print(best)

结果:

正如预期的那样,现在我们有了一组最小化优化函数的超参数组合,使用 Hyperopt。让我们直观地观察目标函数值如何随着超参数的变化而变化,利用我们之前定义的函数创建二维和三维图。

# Plot optimization vs. hyperparameters in 2D
plot_obj_vs_hp(trials, search_space, best)

结果:

损失 vs. n_estimatorsmax_depth 超参数的子图

# Plot optimization vs. hyperparameters in 3D
plot_obj_vs_hp_3d(trials, search_space, best)

结果:

3D 损失函数与 n_estimatorsmax_depth 超参数的表示

结论

在这篇文章中,我们介绍了 Hyperopt——一个强大且开源的超参数优化工具,然后通过支持向量机进行分类和通过随机森林回归器进行回归的示例来讲解了实现过程。接着,我们查看了通过这些过程找到的最佳超参数组合,并在二维和三维中可视化了结果。

感谢阅读!

如果你觉得这篇文章对你有帮助,请 关注我的 Medium 并订阅以接收我最新的文章!

(所有图片,除非另有说明,均由作者提供。)

将超参数调整应用于神经网络

原文:towardsdatascience.com/hyperparameter-tuning-neural-networks-101-ca1102891b27

如何通过调整超参数来提高神经网络的“学习”,并附带 Python 示例

Egor HowellTowards Data Science Egor Howell

·发布在 Towards Data Science ·9 分钟阅读·2023 年 11 月 18 日

--

神经网络图标由 Vectors Tank — Flaticon 创建。神经网络图标。www.flaticon.com/free-icons/neural

背景

在我之前的帖子中,我们讨论了神经网络如何预测和从数据中学习。负责这一过程的有两个步骤:前向传播和后向传播,也称为反向传播。你可以在这里了解更多:

## 前向传播与反向传播:神经网络 101

通过手工和使用 PyTorch 代码解释神经网络如何“训练”和“学习”数据中的模式

towardsdatascience.com

本文将深入探讨如何优化“学习”和“训练”过程,以提高模型的性能。我们将覆盖的领域包括计算改进和超参数调整,以及如何在 PyTorch 中实现!

但是,在所有这些好东西之前,让我们快速回顾一下神经网络的基本知识!

快速回顾:什么是神经网络?

神经网络是大型数学表达式,试图找到能够将一组输入映射到其对应输出的“正确”函数。下面展示了一个神经网络的例子:

一个基本的双隐层多层感知器。图示由作者提供。

每个隐藏层神经元执行以下计算:

每个神经元内部进行的过程。图示由作者提供。

  • 输入: 这些是我们数据集的特征。

  • 权重: 用于缩放输入的系数。算法的目标是通过 梯度下降找到最优系数。

  • 线性加权和: 将输入和权重的乘积求和,并加上一个偏置/偏移量项, b.

  • 隐藏层: 多个神经元用于学习数据集中的模式。上标表示层数,下标表示该层中的神经元编号。

  • 箭头: 这些是从相应输入(无论是特征还是隐藏层输出)到网络的权重。我在图示中省略了它们的明确标注,以保持图示的整洁。

  • ReLU 激活函数: 最受欢迎的 激活函数,因为它计算上高效且直观。有关更多信息,请 点击这里**。

我在这里链接了一个精彩的视频,解释了神经网络如何学习任何东西,以提供更多背景!

如果你想要更全面的介绍,可以查看我之前的帖子:

[## 介绍,感知器与架构:神经网络 101]

神经网络及其构建块简介

levelup.gitconnected.com](https://levelup.gitconnected.com/intro-perceptron-architecture-neural-networks-101-2a487062810c?source=post_page-----ca1102891b27--------------------------------)

计算改进

使神经网络如此普遍可用的主要优化技术可能是并行处理。

数据量也是神经网络在实际应用中如此有效的主要原因之一。

每一层都可以表示为一个大型矩阵,其包含相关的输入、权重和偏置。例如,考虑一个神经元的基本输出:

神经元的线性输出。公式由作者用 LaTeX 表示。

在这里,x 是输入,w 是权重,b 是偏置,z 是最终输出。上述公式可以重写为矩阵形式:

神经元输出的向量化实现。公式由作者用 LaTeX 表示。

如果没有这种向量化实现,训练神经网络的运行时间将会非常巨大,因为我们需要依赖循环。在这种情况下,我们将逐个乘以每个权重和输入,然后相加得到z。而使用向量化实现,这可以在一个整体步骤中完成。

这里有一篇很好的文章链接在此和一个视频在这里,比较了向量化方法与使用循环的运行时间。

大多数深度学习框架,如PyTorchTensorFlow,在后台为你处理这些问题,所以你不需要太担心!

超参数

概述

神经网络架构和参数的搜索空间是难以想象的巨大,甚至可以说是无限的。存在一些库可以帮助你调整参数,比如hyperoptoptuna或普通的sci-kit learn

还有很多其他的库,请查看此处的列表。

它们的处理方法不同,有的使用简单的网格搜索或随机搜索,而其他的则采用更复杂的方法,如贝叶斯优化或甚至像遗传算法这样的进化算法。一种方法并不优于另一种,最终取决于你如何设计搜索空间和计算资源。

了解你理想的参数是很重要的,以避免在不必要的值上浪费大量时间,并快速收敛。现在让我们来看看一些你应该关注的主要内容!

隐藏层数量

有一种叫做普遍逼近定理的理论,基本上说单层隐藏层神经网络可以学习任何函数,只要它有足够的神经元。

然而,拥有一个层中大量神经元并不是理想的,最好还是有几个层,每层中神经元较少。这样做的假设是每层学习新的东西,且在更细粒度的水平上进行学习。相比之下,单层隐藏层网络需要一次性学习数据集的每一个细节。

一般来说,几个隐藏层通常已经足够。例如,在MNIST 数据集上,一个具有一个隐藏层和几百个神经元的模型具有97%准确率,但一个具有两个隐藏层且神经元数量相同的网络具有98%准确率

MNIST 数据集包含许多手写数字的例子。

当然,像任何超参数一样,我们应该应用一些调优过程,通过不同的层数与其他超参数的组合进行迭代。

层中的神经元数量

输入层和输出层的神经元数量是预定义的。输入层的大小必须等于数据集中的特征数量。如果你的数据集有 50 个特征,那么你的输入层将有 50 个神经元。同样,输出层需要适合问题。如果你在预测房价,那么输出层将只有 1 个神经元。然而,如果你在尝试分类单个数字,如 MNIST 数据集中的情况,那么你需要 10 个输出神经元。

对于隐藏层,你可以稍微放开一点!神经元数量的搜索空间是巨大的。然而,最好是略微过度使用神经元数量,并使用像早停这样的技术来防止过拟合。

另一个关键想法是确保每层都有足够的神经元以具有表示能力。如果你试图预测一个 3D 图像,2 个神经元只能在 2D 中工作,因此会丢失一些关于信号的信息。

学习率

学习率决定了算法收敛到最优解的速度,因为它负责反向传播过程中的步长。这可能是训练神经网络时最重要的超参数之一。太高会导致学习发散,太低则算法会需要很长时间才能收敛。

我通常会在0.0011之间的广泛搜索空间中调整我的学习率,这被视为文献中最常见的传统学习率。

找到最佳学习率的最佳方法之一是通过学习率调度。该计划随着训练的进展减少学习率,因此在接近最优点时采用更小的步长。让我们来详细分解一些常见的:

基于时间的衰减:

学习率随着时间的推移以一定的速度下降。

基于时间的衰减。作者在 LaTeX 中的方程。

在这里,α 是学习率,α_0 是初始学习率,decay 是衰减率,epoch 是迭代次数。

一个 epoch 是神经网络使用所有训练数据进行的一个训练周期。

步长衰减:

学习率在经过一定数量的训练 epoch 后按某一因子减少。

步长衰减。作者在 LaTeX 中的方程式。

其中 factor 是学习率减少的因子,step 是学习率应减少的 epoch 数量。

指数衰减:

学习率将在每个 epoch 中以指数方式减少。

指数衰减。作者在 LaTeX 中的方程式。

其他:

还有许多其他学习率调度方法,如 性能调度1cycle 调度功率调度。重要的是要记住,没有一种调度方法比另一种更好,最好尝试几种以确定哪一种最适合你的模型和数据。

批量大小

在训练神经网络时,常见的梯度下降变体有三种:

  • 批量梯度下降****: 使用整个训练数据集来计算损失函数的梯度。这是最稳健的方法,但对于大数据集来说,计算上不具可行性。

  • 随机梯度下降: 使用单个数据点来计算损失函数的梯度。这种方法是最快的,但估计可能会有噪声,收敛路径也可能较慢。

  • 小批量梯度下降****: 使用训练数据集的一个子集来计算损失函数的梯度。批量的大小有所变化,并取决于数据集的大小。这是批量和随机梯度下降的两者优点的结合。

关键在于找到适合执行小批量梯度下降的最佳批量大小。建议使用尽可能大的批量大小,这些批量可以适配到计算机的 GPU 内存中,因为它们能并行计算。

迭代次数

这是 epoch 的数量,即我们为神经网络执行的所有前向和反向传播的总次数。实际上,最好使用提前停止,并将迭代次数设置得很高。这可以避免过早终止学习的可能性。

激活函数

大多数网络使用 ReLU,主要由于其计算效率,但也有其他激活函数。我之前的帖子总结了主要的激活函数及其优缺点:

## 激活函数与非线性:神经网络 101

解释为什么神经网络可以学习(几乎)任何东西和一切

[towardsdatascience.com

选择的激活函数对于输出层很重要,以确保你的预测符合问题的上下文。例如,如果你在预测概率,则应使用sigmoid激活函数。

然而,在我看来,与我们之前讨论的其他超参数相比,在隐藏层中测试不同的激活函数对性能的影响不会太大。

其他超参数

还有其他超参数可以调优:

Python 示例

以下是一些模板代码,使用 hyperopt 在 PyTorch 中对 MNIST 数据集进行神经网络超参数调优:

作者的 GitHub Gist。

代码可在我的 GitHub 上找到:

[## Medium-Articles/Neural Networks/hyperparam_tune.py at main · egorhowell/Medium-Articles

我在我的中等博客/文章中使用的代码。通过创建一个账户来贡献于 egorhowell/Medium-Articles 的开发…

github.com

总结与进一步思考

神经网络有很多超参数和无限的架构,这使得找到最佳组合非常困难。幸运的是,像optunahyperpot这样的包可以智能地为我们完成这个过程。通常最需要调整的超参数是隐藏层的数量、神经元的数量和学习率。这些通常在开发神经网络模型时能够带来最显著的效果。通过使用早期停止,训练轮数会变得冗余,而选择的激活函数也通常对性能影响很小。然而,在考虑输入和输出层的结构以及输出层的激活函数时,总是重要的要考虑你试图解决的是什么类型的问题。

参考资料与进一步阅读

另一个事项!

我有一个免费的新闻通讯,Dishing the Data,每周分享成为更好数据科学家的技巧。没有“空洞的内容”或“点击诱饵”,只有来自实践数据科学家的纯粹可操作见解。

## Dishing The Data | Egor Howell | Substack

如何成为更好的数据科学家。点击阅读由 Egor Howell 编写的 Substack 出版物《Dishing The Data》…

newsletter.egorhowell.com

与我联系!

使用 AWS Sagemaker SDK 对 HuggingFace 模型进行超参数调整

原文:towardsdatascience.com/hyperparameter-tuning-of-huggingface-models-with-aws-sagemaker-sdk-f727ac06cf36?source=collection_archive---------7-----------------------#2023-01-30

使用 HuggingFace Estimator 和 Sagemaker Tuner 优化深度神经网络

Ciarán CooneyTowards Data Science Ciarán Cooney

·

关注 发表在 Towards Data Science ·8 分钟阅读·2023 年 1 月 30 日

--

图片来源于 pexels.com (www.pexels.com/photo/person-holding-volume-knob-1345630/)

介绍

即使在巨大的预训练神经网络时代,超参数调优仍然提供了最大化模型在特定下游任务中性能的机会。微调,就像从头开始训练一样,需要一组合理的初始超参数,以实现高效且最优的训练,因此找到有效的调优方法是深度学习拼图中的重要一环。

在使用 HuggingFace 上的一些大型预训练模型时,比如 BERTT5wav2vecViT,超参数调优是一个需要认真考虑的重要概念。虽然很容易认为这些模型的大部分潜力已经通过大规模预训练被挖掘,但学习率、预热步骤数、权重衰减和学习率调度器类型等超参数可以对微调任务的最终目标产生显著影响。

幸运的是,有几种策略用于搜索最佳超参数配置(例如,网格搜索或贝叶斯搜索),它们在方法上的复杂程度各不相同。此外,深度学习框架和云服务提供商正越来越多地帮助从业者将超参数调优集成到他们的机器学习工作流程中。其中之一是亚马逊 Web 服务(AWS)的 Sagemaker HyperparameterTuner。在本文中,我将对如何使用 Sagemaker 微调 HuggingFace 转换器进行代码演示,使用其超参数调优器和 Sagemaker HuggingFace 估算器。

笔记本和脚本可以在 这里 获取,它们是一个 仓库 的一部分,该仓库旨在演示 Sagemaker 训练、评估和部署深度学习模型的实用性。

超参数调优器和 HuggingFace 估算器

Sagemaker 的 HyperparameterTuner 使得运行超参数任务变得易于维护和具有成本效益。该类接受一个 Sagemaker 估算器 —— 这是在 AWS 中运行机器学习训练任务的基础类 —— 并根据用户提供的参数配置调优任务。用户可以指定调优策略、要最大化或最小化的指标、要搜索的超参数范围以及其他几个参数。你可以像使用标准估算器一样调用 .fit(),并且在训练完成后,它还提供了部署功能。

我将演示如何使用 HyperparameterTuner 以及 Sagemaker HuggingFace Estimator。这是一个用于在 AWS 上处理 HuggingFace 模型的定制估算器。在这个示例中,我将对 DistilBERT 进行微调,以便在 tweet_eval 数据集上完成情感分类任务。该数据集在 Creative Commons Attribution 3.0 Unported License 下提供。

跟随代码

在一些导入之后,我们需要设置一个 Sagemaker 会话,并初始化一个可以读写的 S3 存储桶。Sagemaker 中的 session 是一个非常方便的类,利用 Sagemaker 通常使用的资源和实体;例如端点和 S3 中的数据。如果不指定存储桶,会话将分配一个默认存储桶。

初步管理完成后,接下来要做的是获取数据。

数据

为了演示如何使用 HuggingFace 估算器进行超参数调整,我们将使用 tweet_eval 数据集,并直接从数据集库下载它。

加载数据集。

在经过一些标记化和处理步骤后,我们需要将数据集转换为张量,然后将训练和测试集存储在我们为 Sagemaker 会话定义的存储桶中。

幸运的是,HuggingFace 数据集和 Sagemaker 使得保存数据变得相对简单,因为数据集对象提供了一个 save_to_disk() 方法,允许我们传递一个文件系统参数,该参数负责将数据移动到 S3,使用 s3fs.S3FileSysteM

使用 save_to_disk() 方法将数据集存储到 S3。

现在我们已经将训练和测试数据存储在一个 S3 位置,训练作业可以访问它。

训练和测试集的 S3 位置。图片由作者提供。

超参数设置

在运行调整作业之前,我们需要考虑要优化的超参数以及我们认为合适的值范围。通过调整优化的常见超参数包括学习率、权重衰减、丢弃概率,甚至是结构参数,如神经网络中的层数或池化策略。在我展示的场景中,基础模型本身甚至可以作为一个超参数进行调整,因为我们可以加载多个 HuggingFace 模型进行比较。

但是,在这个示例中,我们将微调 DistilBERT,调整四个超参数。它们是:

  • 学习率

  • 预热步骤数

  • 优化器

  • 权重衰减

初始化估算器和调整器

在初始化调优作业之前,我们需要初始化我们的估算器。估算器是 Sagemaker 中一个处理端到端训练和部署任务的类。HuggingFace 估算器允许我们通过使用专门为此任务开发的预构建 Docker 容器,在 Sagemaker 训练环境中运行自定义的 HuggingFace 代码。

我们通过 entry_point 参数将训练脚本传递给估算器。我们还传递了几个附加参数来配置环境、包版本以及实例设置。传递给估算器的 hyperparameters 参数不包含要调节的参数,而是要传递给我们训练脚本的参数。

初始化 HuggingFace 估算器。

训练脚本 training_script.py 包含我们用于微调 DistilBERT 的代码,点击这里。HuggingFace 提供了一个 Trainer 类,几乎处理了所有的训练设置和过程,使用这种方法进行调优的示例可以在 这里 找到。然而,这并不总是理想的,直接控制训练循环有其优势。因此,我为此任务在 PyTorch 中编写了一个 自定义训练循环

如果有帮助的话,可以查看自定义训练循环,但这里有几个代码片段展示了数据加载器和模型训练。

用于训练集的 Pytorch 数据加载器。

在原生 Pytorch 中进行训练循环。

下面的代码片段展示了我们超参数范围的配置。Sagemaker 调优器提供了一套用于表示参数范围的类。ContinuousParameter 允许我们设置一个范围,在这个范围内搜索连续值。在这里,它用于学习率和权重衰减。IntegerParameter 提供了相同的功能用于整数,我们用它来设置预热步骤。最后,CategoricalParameter 允许我们传递一个变量列表进行调节——在这里,它用于优化器类型。

调优器还需要一个目标指标和目标类型——即调节模型的目标以及我们希望调整的方向。metric_definitions 包含一个或多个指标的名称,以及用于从 Cloudwatch 日志中提取指标的正则表达式(这是 Sagemaker SDK 的一个常见功能)。

定义超参数范围和目标指标。

现在我们可以在开始调优作业之前定义HyperparameterTuner。除了 HuggingFace 估算器、度量参数和超参数范围外,我们还需要设置最大作业数量和希望运行的并行作业数量。这使得 Sagemaker 调优器非常出色且易于使用。然后,我们调用tuner.fit()以启动调优作业。

初始化 HyperparameterTuner 并调用 .fit() 开始调优。

比较调优后的超参数

调优作业结束后,我们得到了调优后的超参数。调优器附带一个tuner.analytics()方法,用于在 pandas 数据框中显示汇总结果。FinalObjectiveValue 是我们在配置调优作业时建立的损失指标。

调优器分析结果数据框。图片作者提供。

最佳超参数是:

  • 学习率 = 0.000175

  • 优化器 = Adafactor

  • 热身步数 = 192

  • 权重衰减 = 0.000111

…对结果的初步观察表明,学习率可能是最重要的因素。

当然,我们可以直接从数据框绘制结果,但还有另一种方法。从 Sagemaker 控制台,我们可以点击训练和超参数调优作业标签。从那里,我们可以找到已完成的作业并点击View algorithm metrics链接。这将带我们到 AWS CloudWatch,在那里我们可以看到各种交互式图表,并对调优器返回的数据执行查询。下图是一个示例折线图,显示了两个周期的测试损失。

AWS CloudWatch。图片作者提供。

现在我们可以查看结果,我们有几种使用调优值的选项。首先,我们可以简单地将这些参数训练的模型作为最终推理模型。其次,我们可以使用最佳参数执行更长时间的训练,以改善我们的模型。第三,我们可以根据这些结果重置超参数范围,并运行另一个调优作业以获得更细致的结果。

目前,我只是打算使用训练作业得到的最佳模型进行部署和推理。

部署端点并预测

要选择最佳模型,我们的调优器对象具有best_estimator()方法。在初始化了性能最佳的模型后,将其部署到 Sagemaker 端点非常简单,使用deploy()方法即可。在这里,我指定了用于推理的实例数量(1)以及实例类型(加速计算用的‘ml.g4dn.xlarge’)。部署可能需要几分钟完成,完成后您将在 Sagemaker 上托管模型端点。

部署模型。

部署模型后,我们可以对一些输入文本的情感进行预测。如果我输入句子“Best thing ever!” 我预计会得到一个非常高置信度的正面情感预测。确实如此。然而,输出标签被泛泛地设置为‘LABEL_0’和‘LABEL_1’,因此我编写了一些后处理代码,以给出更有意义的输出,你可以看到我们得到了一个‘positive’的结果。

进行预测。

使用部署的模型进行预测。图片来源:作者。

预测一个类别标签。

将预测结果格式化为可读形式。图片来源:作者。

最后,如果我们不再需要使用该模型进行推理,我们可以删除端点,使其不再托管(你的模型工件仍然保存在 S3 中)。

当所有任务完成时,删除端点。

总的来说,我使用 Sagemaker HyperparameterTuner 的体验非常积极,但也有一些潜在的缺点需要考虑。与所有云服务一样,需要注意的一个方面是成本。这对于这种服务尤其重要,因为它涉及到多个作业,包括并行化和 GPU。另一个潜在的缺点是 HyperparameterTuner 和 Sagemaker SDK 的高层次性质。有些人可能希望对程序有更多的控制,因此类似 boto3 的工具可能更合适。

结论

本文展示了如何在 AWS Sagemaker 中使用 HuggingFace 估算器进行超参数调优。希望代码演示能展示出使用 Sagemaker SDK 调优超参数的简单程度,并且使用它进行模型开发能获得很多好处。用于超参数调优的 Jupyter Notebooks 可以在这里这里找到。Sagemaker 示例的主要 GitHub 仓库在这里

我晋升了!

原文:towardsdatascience.com/i-got-promoted-71b6c87e4f0b

照片由Edu Lauton提供,Unsplash上发布

怎么做的?

Zijing Zhu, PhDTowards Data Science Zijing Zhu, PhD

·发表于Towards Data Science ·阅读时间 9 分钟·2023 年 10 月 19 日

--

我很高兴分享我最近获得了晋升!!!这是我职业发展的一个重要里程碑。回顾我的旅程,包括一路上的所有成就和奋斗,我可以看到自己从一个初级新人成长为一个领导多个项目交付的高级人员。去年,我发表了一篇关于我遵循的七个原则,以成为更好的数据科学家的文章,并获得了很多积极的反馈。在这篇文章中,我想分享更多关于数据科学家职业发展的经验,这些经验帮助我获得了晋升。希望我的见解能对加速你的职业成长有所帮助,尤其是如果你是这个行业的新手。

晋升意味着你在当前级别上表现超出预期,决定这一点的主要因素是你工作的影响力。本文不仅会探讨如何产生影响,还会讲解如何在团队内外传递影响。这两个方面在展示你的表现时同样重要。文章的结构如下:

作者提供的图片

产生影响

产生影响意味着你需要完成并超越分配给你的任务。为了持续产生高质量的成果,最佳实践是什么?以下是我总结的一些提示:

提高生产力

照片由Carl Heyerdahl提供,Unsplash上发布

我相信你一定听说过许多关于通过时间管理或能量管理来提高生产力的建议。我这些年来尝试了几种方法。在工作环境中,对我来说最实用和有效的就是为深度工作时间设置时间块。

我将深度工作定义为研究解决问题的方法、编写代码建立流程、研究模型结果并进行改进等。这些类型的工作虽然困难,却对最终交付成果至关重要。当我从事这些工作时,我不希望被会议、邮件或消息打扰。因此,我将那些不容易被打扰的时间段专门用于这些任务。在我的情况下,通常是下午。封锁深度工作时间确保了持续的高质量输出,同时保持必要的沟通。

模块化任务

工作中有很多重复性任务。也许你需要为不同的项目运行相同的 EDA 和建模过程;也许你需要每月制作相同的演示文稿,并更新结果;也许你需要向不同的人教授相同的内容,等等。当你面对这些任务时,就像编写代码时构建类和函数一样,将它们模块化。你可以为数据分析建立一个标准处理流程,或者编写某些函数来计算指标。你可以为特定文档和演示文稿保存某些模板,并通过文档或视频记录好的实践,以便更顺利地培训他人。尽管初期设置会花费更多时间,但将来会节省大量时间和精力。

协作

学会协作与发展自身核心技能同样重要。没有人是孤立的岛屿,单独处理模糊的业务问题。每个人都有擅长的领域,因此你不需要在所有方面都是专家。抓住机会深入挖掘并提升你的核心技能。此外,协作还挑战我在头脑风暴和共同完成项目时进行更深入的思考。通过委派任务的协作是走向高级角色的途径,因为我们正在学习如何以更高的效率解决更复杂的问题。

照片由Brooke Lark拍摄,来自Unsplash

然而,工作场所的协作和沟通有时可能很棘手,请查看我最近的视频,了解如何处理工作中的冲突:

寻求帮助

每当遇到障碍时,不要害怕寻求帮助。你可以与领域专家沟通以澄清问题,并向经理更新进展以延长截止日期。对于新加入公司的员工,他们可能因为不想显得脆弱或有冒名顶替综合症而害怕提问。如果我问了太多问题,或者无法自己解决这个问题,我的同事和经理是否会失去对我的信任,并开始怀疑我是否适合这份工作。关于冒名顶替综合症,本文解释得非常好:

面向数据科学的冒名顶替综合症现实 [## 面对数据和分析行业中的冒名顶替综合症现实

对学术研究、博客文章和个人经验的综述

面向数据科学的冒名顶替综合症现实

正如文章所说,冒名顶替综合症在技术行业中非常普遍,因为这个领域不断变化和发展。我一直在通过“假装自己能做”来对抗它。实际上,专注于自己的成长而不是与他人比较,利用他人帮助自己成长。公司为我们提供了资源以便入职和提升技能。不要浪费它。寻找导师、赞助人和教练,长期帮助你提高硬技能和软技能。

管理期望

管理期望是管理上的一个重要方面,对于可持续的职业发展至关重要。每个人的时间和精力都是有限的。如果你为了给人留下好印象而承诺超出自己能力的任务,你要么面临职业倦怠的风险,要么因无法按时完成任务而感到失望和后果。除了不要过度承诺,另一个有用的技巧是不断调整优先级。找出本季度、本月、本周和今天的重点任务。事情总在变化,这就是为什么我们需要不断调整团队内的优先级,以便始终专注于最重要的任务。

解决问题,而非情绪

图片由 Tengyart 提供,Unsplash

如果我们是没有情感的机器,我们将会非常高效和有生产力。不幸的是,我们是具有不同技能和工作风格的人。摩擦是常见的,它们来自不同的观点、心态、背景、关注点等。在工作环境中,我们需要解决问题,而不是人或情感。你应该知道自己偏好什么,以便能够沟通并设立界限。同时,对不同的观点和工作风格保持开放的心态。学会在何时合作和何时妥协。记住,每当出现分歧时,集中于行动点,专注于通过解决问题来向前推进,而不是处理人际关系或情感。

持续学习

在不断变化的科技行业工作意味着我们需要不断学习。在我最近的文章中,我提供了一些实用的建议,如果你感兴趣,可以看看:

## 持续学习:数据科学家的历程

在不断变化的领域中导航

towardsdatascience.com

产生影响

产生影响是第一步。要完成任务,你需要交付影响,以创造业务价值并完成循环。高效地交付影响是一项至关重要但被低估的工作技能,而越来越普遍的远程和混合工作环境并没有带来帮助。在这一部分,我想讨论如何在团队内和跨团队地交付影响。

图片来自 Mika BaumeisterUnsplash

在团队内部产生影响

关于在团队内部增加影响力,有不同的方面,但我想讨论的唯一一点是领导力技能。作为一个 IC(个人贡献者)以及不打算未来从事管理职位的人,是否仍然有必要培养领导力技能?在与我的导师进行了一次深刻的对话后,我才意识到领导力技能的重要性。我的导师帮助我理解领导力不仅仅是管理一个团队。实践领导力技能意味着:

  • 与上级管理项目期望和优先事项;

  • 有效地与同事合作,按时交付项目,并知道如何给予和接受赞扬、反馈;

  • 指导并帮助初级团队成员快速成长;

  • 对你的项目负责并积极主动。

并非每个人的观点都相同,但对我而言,产生的影响来自于完成和交付项目,以及帮助经验较少的人员成长并变得自给自足。当我第一次开始培养初级员工时,我曾怀疑这是否值得,因为教导他人所需的时间比自己完成任务要长。然而,考虑团队发展的长期利益以及为自己可持续地管理时间和精力是很重要的。当我意识到这一点时,这也是我从初级员工转变为高级员工的第一个时刻。

实现跨团队的影响力

跨团队的影响力对于晋升也至关重要。这表明你在团队之外创造了影响,这将使整个组织受益。作为一名完全远程工作的员工,我发现与团队之外的人建立联系更加困难。在这种情况下,我尽力利用每一个可能的机会建立我的声誉。我使用了几个渠道:

  • 当你有机会时,访问不同的公司办公室,并利用这个机会在专业和个人层面上与同事建立联系。

  • 利用你的专业知识参与跨团队任务。例如,我曾主持过一个数据技能的学习小组,参与者来自不同团队;

  • 确保通过展示你的专业知识来建立良好的声誉。每当你与其他团队互动时,利用这个机会展示你的工作如何帮助解决他们的问题。避免炫耀你的技能,而是专注于你如何能够协助他人。

  • 不断与利益相关者沟通项目进展。学习如何向技术和非技术观众突出你的工作。我在以下文章中讨论了更多细节:

数据讲故事中的 4D:将科学转化为艺术

是的,这远不止是数据可视化

towardsdatascience.com

最后的备注

除了努力工作和表现超过你的级别之外,重要的是在适当的时候向你的经理和跳级经理沟通你的晋升愿望。在沟通中准备证据来支持你的晋升案例,并请求关于下一个级别的额外要求的反馈。仅仅埋头苦干已经不够了。清晰有效地沟通期望和反馈对于职业发展至关重要,即使这对每个人来说可能都不容易。我总是经过广泛的准备和排练后才进行沟通,下次可能仍会感到不自然。然而,寻求认可是对自己负责的重要一步。你必须比任何人都更加主动地发展自己的职业。

照片由Brett Jordan拍摄,来源于Unsplash

最后但同样重要的是,当你获得晋升时,庆祝是很重要的。然而,如果你没有晋升,同样重要的是不要怀疑自己或对自己过于苛刻。虽然这听起来可能很政治,但晋升实际上取决于许多因素。尽管我在这里分享了很多建议,我仍然非常谨慎地不把太多功劳归于运气。晋升在很大程度上依赖于你的公司和团队的表现,这在很大程度上超出了你的控制范围。不断沟通你的期望,尽力而为,并专注于你可以控制的方面,把其余的留给命运。如果你认为自己的努力没有得到足够的认可和赞赏,找到一个更重视你的地方总比相信自己不值得要好。

感谢阅读。请在下面的评论中告诉我你的想法。如果你喜欢这篇文章,不要忘记:

我找到了我的第一份数据工作,接下来怎么办?

原文:towardsdatascience.com/i-landed-my-first-data-job-whats-next-2ab2878152cf

如何在你的第一份数据工作中取得成功

Angela K.Towards Data Science Angela K.

·发表于Towards Data Science ·4 分钟阅读·2023 年 10 月 29 日

--

照片由Jon Tyson提供,来源于Unsplash

你认真参加训练营,逐一完成课程,获得证书并建立作品集项目,同时拼命寻找那难以捉摸的第一份工作。

你发现自己陷入了一场竞赛。这里有毕业生,市场上的有经验人士,还有你,带着虚假症候群,配备一个月的 LinkedIn Premium,向陌生人发出谦逊和礼貌的消息,希望能有所突破。

虽然我在两年前找到了我的第一份数据工作,但我仍然清晰地记得当我最终得到录用通知时的疲惫和喜悦。然而,当我庆祝这一里程碑时,我立刻想知道:接下来是什么?

嗯,我很长时间找不到答案。关于获得数据知识和找到工作的资料有很多。工作可能看起来是最终目标,但实际上不是。如果你刚开始在数据领域的旅程,第一份工作很难带来你期望的收入水平。

那么,你如何才能在知识和收入上取得进步呢?

让工作流程和数据结构变得舒适

当你参加课程时,一切都为你准备好了,清楚地指示了要使用哪些文件和点击哪些按钮。

然而,当你开始工作时,没有明确的指示。你必须自己搞清楚细节。许多你同事熟知的方面可能对你来说完全陌生。此外,他们可能甚至没有意识到这些细节不明显,在入职时忽略了它们。

当你被分配一个任务却不知道从哪里开始时,要准备好感到迷茫。不要犹豫去问那些看似“愚蠢”的问题。这些问题并不愚蠢,它们可以澄清公司独特的环境。

了解公司的系统、数据结构和项目工作流程。如果你想成为一个真正友善的人,写下这些晦涩内容的文档,以便与你下一个加入团队的人分享你的学习。这一努力不仅能让他们的前几个月更轻松,还能提升你的自信心,并稍微缓解你的冒名顶替症。

精通公司的工具箱

一旦你了解了公司的技术环境,你将揭示出你的空白点。深入研究公司使用的特定技术。参加相关课程、观看视频和阅读文章。你应该精通它们。

从最重要的工具开始,但不要仅限于常见工具。如果你掌握了只有少数人熟悉的技术,你将会因为是一个聪明的人而获得几分。

学习业务

这一知识在新加入者中被严重低估。虽然在前几个月可能不完全理解行业和公司运作情况是可以的,但这对于获得高级职位是至关重要的。

事实上,头几个月是询问现场事务和操作的理想时机。了解数字背后的行动、特定报告的原因、报告的驱动因素、所有者的利益以及涉及的弱点和问题。记录下最后两个,并尝试在不久的将来解决它们。

区分初级和高级数据专家的不仅仅是工具的熟练度和对稀有技术的了解,还有业务知识。一旦你了解了业务流程及其问题,你就能为团队和公司带来真正的价值。虽然学习一种新工具可能需要几个月,但学习业务可能需要几年,但你可以通过将其设为目标来加快进度。

锻炼你的技能

由于这是你第一次从事数据工作,你可能会对所分配任务的意义感到失望。你花了几个月学习 Python、SQL 和统计学,但最终只是做了一些 Excel 表格和 BI 工具中的条形图。或者你在课程中学到了一些高级概念,但还没有被委以在项目中应用它们的任务。如果是这样的话,当你想换工作时,你可能会对自己的作品感到惊讶,因为你完全忘记了如何使用这些技术。你将不得不从头开始,而不是以更高的水平建设新的项目。

找到练习你在学习期间已经训练过但未使用的关键技能的方法。做一些副项目、参加黑客马拉松和在非营利数据项目中做志愿者可能会有所帮助。

当你学习一种新工具或技术时,创建一个类似的个人项目,并将其纳入你的作品集。下次你进入就业市场时,你已经有东西可以展示了。

变得自给自足

学习如何从头到尾管理项目:从最初的客户会议(问题被提出的地方),到将完整的解决方案交给下一个负责人。

彻底深入问题,并理解其背后的业务原因。着手解决主要问题,而不是仅仅构建图表或模型。

你应该知道从哪里获取可信的数据,如何建立环境,记录计划,满足客户期望,并支持项目的未来,包括其局限性和潜在问题。当你了解这些,恭喜你,你不再是初级人员了!

总结

一两年后,你将对技术栈和业务有足够的了解,以便继续前进。届时,你会更清楚数据工作领域中哪个方向最吸引你,并专注于此。或者你可能会发现你的角色期望与现实不符,你更愿意转到相关领域。这完全没问题。数据术语背后仍然有很多神秘之处,这使得新手很难找到最佳契合点。

考虑关注我,以免错过即将发布的帖子。非常感谢你的支持!

我花费了$675.92 与 Upwork 上的顶级数据科学家交谈——这是我学到的

原文:towardsdatascience.com/i-spent-675-92-talking-to-top-data-scientists-on-upwork-heres-what-i-learned-4ae3d9300993

数据科学自由职业的现实

Shaw TalebiTowards Data Science Shaw Talebi

·发表于Towards Data Science ·阅读时间 7 分钟·2023 年 6 月 10 日

--

从本“钱袋子”富兰克林那里学到的一个教训。图片由作者提供。

我寻找导师的过程

你可以通过自己的经验或他人的经验来学习。前者是缓慢且痛苦的,而后者(相对来说)则是快速且容易的。这就是为什么拥有导师可以帮助加速实现目标的进展。

虽然从原则上讲这听起来很棒,但实际找到合适的导师却比说起来要难得多

这是我挣扎了几个月的事情。我感觉我的兴趣过于小众。我对数据科学和创业的交集感兴趣。虽然我找到了许多数据科学家和企业家,但很难与那些同时认同这两个类别的人建立联系。

然而,在 LinkedIn 上发了几个月无效的冷消息之后,我决定将我的搜索从 LinkedIn 转移到 Upwork,这成为了一个转折点。

企业家们在 Upwork 上

对于不熟悉的人,Upwork 是一个自由职业平台,客户发布工作, freelancer 申请这些工作。我第一次接触 Upwork 是在研究生阶段作为自由职业者。然而,在这次互动中,我以客户身份参与。

作为 Upwork 上的客户,你可以浏览和预约自由职业者的时间。当我探索这些人才时,我很快意识到 Upwork 上有非常专业的数据科学家

我所谈论的是在这个平台上拥有几十年经验和 6-7 位数收入的专家(而且这只是 Upwork 上的情况)。但真正的关键是,Upwork 上的任何数据科学家,默认都是企业家(或者至少比 LinkedIn 上的大多数人更接近企业家)。所以我似乎找到了我的小众导师。

这引发了一系列与Upwork 上顶级数据科学家10 次电话会议。我将这些电话会议分为 3 个部分:过去——你是如何开始的?现在——你现在如何运作?未来——这将走向何方?

我在本文中遵循相同的结构,总结和突出这些对话中的关键收获。希望这能为你提供一些背景和灵感,了解你的数据创业之旅可能会是什么样的。

是什么让你进入了数据科学领域?

从这些电话会议中最引人注目的观察之一是,没有两个数据科学自由职业者的经历是相同的。为了给你一个大致的了解,以下是我与的 10 位自由职业者的教育背景:生物医学工程、工业工程、生物统计学、电气工程、计算机科学、金融、物理、数据科学、市场营销、人工智能、经济学、数学、MBA 和数据科学训练营

注意到这里的背景信息比采访更多,这有两个原因。

  1. 大多数人接受了超过本科的培训,并且

  2. 10 位自由职业者中没有两个人的培训完全相同

后一点是我最喜欢的数据科学方面之一。它是一个涵盖广泛观点和经验的领域,这使得对话和合作非常有趣。

这在阅读本文时要牢记。背景的多样性也延伸到自由职业者的操作方式和未来发展方向。简单来说,不同才是这里的常态

你是如何开始自由职业的?

虽然每个自由职业者的起点都是独特的,但有几次提到过一种共同的起始策略。在早期,许多自由职业者往往高度关注学习和声誉建设

数据科学自由职业不仅仅是“手把手”的工作。你不仅需要进行数据科学工作,还需要推销自己、处理客户、管理财务等。

自由职业者常用的一种方式是早期承接(相对)大量的小项目。这不仅提供了多个学习的重复机会,而且拥有一长串成功完成的项目(带有推荐信)能够建立信誉,并使获得新客户变得更加容易。

尽管这种“规模小而广”的策略对于起步阶段非常有效,但它似乎并不是一种常见的(或良好的)长期策略。

你现在如何运作?

遵循“不同才是常态”的主题,这些自由职业者目前的工作方式和薪酬方式各不相同。以下是一些例子,可以让你对这种情况有个了解。

  • 全职自由职业

  • 兼职做自由职业同时从事副业

  • 在全职岗位的同时兼职做自由职业

  • 全职合同岗位(1099)

  • 从自由职业转为全职岗位(W2)

这突出了自由职业的灵活性。它允许专业人士根据最适合自己的方式来调整工作。

对于那些不寻求全职工作的人来说,通常会限制每个客户的每周时间投入。每个客户每周 10 小时似乎是一个理想的平衡点,通常在 5 到 20 小时每周之间波动。按照这个时间分配,自由职业者通常会同时有 2 到 3 个客户。

你如何获得新客户?

自由职业者获得新客户的3 种常见方式第一种也是最常见的方式是在 Upwork 或其他网站上申请合同。

第二种方式是通过在 Upwork 或其他平台上的主动引导(即客户主动找到他们)。这通常发生在那些拥有良好声誉、推荐信或强大社交媒体存在的自由职业者身上。

最后,第三种方式(对最成功的自由职业者而言比较常见)是仅通过推荐获得工作。这通常发生在客户的需求远远超过自由职业者的供给时(这也自然推高了价格)。

这将会如何发展?

每个自由职业者对他们的未来都有独特的愿景。然而,为了使其更易于理解,这里将长期目标分为 3 个类别。

保持自由职业并扩大咨询业务

第一个类别包括那些认为自己可以永远从事自由职业的人。他们喜欢这样,并且这让他们过上了舒适的生活。他们可以在想要的时间工作,做自己想做的事,与自己想合作的人一起工作,并且在自己想要的地方工作。

在这个类别中,我还包括那些想要扩大咨询业务的人。这通常对那些工作量过大、无法单独完成并且喜欢项目管理的人来说,往往会自然而然地发生。对许多人来说,这只需要与他们经常合作的分包商或甚至全职员工一起工作。

生成被动收入并建立以产品为导向的公司

虽然自由职业提供了巨大的自由度并且可以很有利可图,但它仍主要是时间与金钱的交换例如:我支付给你一个小时的工资来完成一项工作。虽然这并不算太糟糕,但大多数人更愿意用少量时间换取大量金钱例如:你一次性构建一个产品,然后多次销售它

这是第二个长期目标类别,它将产生被动收入与建立以产品为导向的业务结合起来。虽然这两个目标在技术上有所不同,但它们可以服务于同一个目的:在更少的时间内创造更多的价值

一些自由职业者计划通过创建在线课程或其他数字产品来实现这一目标。一些自由职业者积极构建交易机器人或其他个人使用的交易工具。

其他人考虑通过利用他们的自由职业经验和客户群体,构建针对中型公司或特定行业的软件解决方案。虽然我个人尚未看到这个想法的成功实现,但我对此持乐观态度。

过渡到全职角色

虽然自由职业提供了灵活性和独立性,但一些自由职业者最终可能会转为全职角色。有些人甚至会不断地在全职角色之间循环。一些采访对象提到的原因包括:你可以在企业中产生更大的影响、更多的社交互动和合作、更多的确定性和收入稳定性、更大的职业发展机会,以及客户变成雇主。

我的 4 个要点

这些电话履行了我寻求的指导承诺,因为它们帮助加速了我实现目标的进程。虽然很难预测结果如何,但我感觉这可能是我在创业旅程中做出的最佳投资之一。我期待着将这些学习付诸实践,并继续与这个领域的其他人建立关系。

总结一下,这里是4 个关键要点,我将铭记于心,以便未来的自由职业工作中应用。

  1. 做好工作,以便通过口碑和推荐来运作。如果人们主动找上你,你将拥有更大的杠杆和选择权,帮助确保你接受的工作和客户与你的目标一致。

  2. 找到一个细分领域。选择一个细分领域可以帮助你在一个小池塘里成为大鱼。以下是在我的访谈中提到的一些细分领域:金融、加密货币、能源、光学字符识别、LLM 应用和数据战略。

  3. 在技术栈中形成联盟。单独进行数据科学工作可能在业务影响和价值创造方面有限。这就是为什么与其他专家(如软件工程师、网页开发者、UX/UI 设计师等)建立关系,可以帮助你为客户提供更大的价值。

  4. 建立个人品牌。在社交媒体平台上拥有强大的品牌形象可以让你更容易获得自由职业合同,因为这会增加你的信誉。此外,分享有价值的内容并展示你的专业知识可以帮助理想的客户找到你(而不是你去找他们)。

## 我在 Upwork 上花费了 (另一个) $716.46 与数据科学家交谈 — 这是我学到的东西

来自前 1% 数据自由职业者的经验教训

medium.com

资源

联系: 我的网站 | 预约电话

社交媒体: YouTube 🎥 | LinkedIn | Twitter

支持: 给我买杯咖啡 ☕️

## 免费获取我每一篇新故事的访问权限

免费获取我每一篇新故事的访问权限。P.S. 我不会将您的邮箱与任何人分享。通过注册,您将创建一个…

shawhin.medium.com

跟随数据创业者的步伐:

👉 加入 Discord | 🎥 在 YouTube 订阅 | 📅 活动日历

## 数据创业者

一个为数据领域创业者打造的社区。👉 加入 Discord!

medium.com

ICA 和现实中的鸡尾酒会问题

原文:towardsdatascience.com/ica-and-the-real-life-cocktail-party-problem-6375ba35894b?source=collection_archive---------4-----------------------#2023-10-24

为什么独立成分分析在其经典实验中失败了,以及我们可以从这种失败中学到什么。

Kenneth BallTowards Data Science Kenneth Ball

·

关注 发表在 Towards Data Science · 13 分钟阅读 · 2023 年 10 月 24 日

--

鸡尾酒会隐喻(作者图片)

自 1990 年代显著发展以来,独立成分分析(ICA)已成为一种常用的数据分解和预处理技术。ICA 是一种盲源分离(BSS)方法:一些独立的源被盲目混合,结果混合信号由若干观察者接收。ICA 方法通过寻找一个最小化解混成分之间互信息或最大化数据在这些成分上投影的“非高斯性”的基变换,来解混观察到的信号并寻找独立源。

已经有很多教程介绍了 ICA 及其应用:本文并不是另一个 ICA 的介绍。相反,这是一篇关于几乎总是伴随 ICA 解释的动机问题的评论。

几乎所有对 ICA 的介绍都利用鸡尾酒会问题作为 ICA 旨在解决的 BSS 问题的说明。² 鸡尾酒会问题确实是一个富有启发性和激励性的思维实验。只有一个小问题:ICA 在现实生活中的鸡尾酒会上会失败,而且其失败的原因确实应该影响 ICA 的应用方式。

ICA 和鸡尾酒会

一个拥挤的房间。聚会的人群——手里拿着鸡尾酒——在彼此交谈。听众如何将混合的聊天声分离成不同的声音,或许还可以聚焦到一个单独的说话者?这就是鸡尾酒会问题的设置,这是用来介绍 ICA 的经典示例。想象一下,几个麦克风被放置在房间的不同位置。有人说,ICA 向我们揭示了如何将录制的信号分解为独立的组件,代表聚会上的不同说话者。

BSS 问题通过一个混合问题进行表述,其中一些独立源 y 被混合到观测信号 x 中。

对于N 个时间样本。A 是一个混合矩阵,我们用 ji 索引源和观测。对于本文中的几个方程,我使用了爱因斯坦求和符号。

Scikit-learn 的分解模块包括了一个非常实用的 FastICA 实现,我们可以用它来展示在低维示例中它是如何工作的。我们将设置几个独立的来源,这些来源只是不同频率下相位偏移的正弦波,随机混合它们,然后应用 FastICA 尝试将其分解。我们看到的是——在缩放、符号和排列的范围内——FastICA 能很好地恢复原始信号(因此,在已知麦克风位置的实际问题中,我们可以恢复扬声器的方向/位置)。

import numpy as np
from sklearn.decomposition import FastICA
import matplotlib.pyplot as plt

rng = np.random.default_rng(8675309)
t = np.linspace(0, 10, 10000)
x = np.array([
    np.sin(2 * np.pi * t), 
    np.sin(2 * np.pi / 2 * t + 1), 
    np.sin(2 * np.pi / 2 * 2 * t + 2)])
mixing = rng.uniform(-1, 1, [3, 3])
# check that our randomly generated mixing is invertible
assert np.linalg.matrix_rank(mixing) == 3
demixing_true = np.linalg.inv(mixing)

y = np.matmul(mixing, x)

fica = FastICA(3)
z = fica.fit_transform(np.transpose(y))
z = np.transpose(z)
z /= np.reshape(np.max(z, 1), (3, 1))
fig, ax = plt.subplots(3, 1, figsize = (8, 5))
for ii in range(3):
    ax[0].plot(t, x[ii])
    ax[1].plot(t, y[ii])
    ax[2].plot(t, z[ii])
ax[0].set_title("Independent Sources")
ax[1].set_title("Randomly, Linearly, Instaneously Mixed Signals")
ax[2].set_title("ICA Components (Match up to Sign and Linear Scaling)")
plt.tight_layout()

FastICA 对瞬时混合信号

让我们列出一些鸡尾酒会模型的假设:

  • 在鸡尾酒会的房间里,我们假设观察者(例如麦克风)的数量多于源(例如扬声器),这是问题不会欠定的必要条件。

  • 源是独立的,并且不呈正态分布。

  • A 是一个常量矩阵:混合是瞬时的且不变的。

BSS 问题是“盲”的,所以我们注意到源 y 和混合 A 是未知的,我们寻求 A 的广义逆矩阵,称为分解矩阵 W。ICA 算法是推导 W 的策略。

准备混合(图片由作者提供)

现实中的鸡尾酒会

如果我们在派对上实际设置一个麦克风阵列并尝试对录制的音频进行 ICA 会发生什么?恰好的是,开箱即用的 ICA 几乎肯定会在分离扬声器方面失败得很惨!

让我们重新审视我们模型的一个假设:特别是瞬时混合。由于声音的有限传播速度,从扬声器位置发出的音频会以不同的时间延迟到达房间中的每个麦克风。

在派对上,声音的传播速度约为 343 米/秒,因此距离扬声器 10 米的麦克风会录制大约 0.03 秒的延迟。虽然对在派对上的人来说,这似乎几乎是瞬时的,但对于 10 kHz 级别的录音来说,这意味着数百个数字样本的延迟。

尝试将这个时间延迟的盲混合信号输入到原始 ICA 中,结果不会很美观。但等等,难道没有 ICA 用于去混合音频的例子吗?³ 是的,但这些玩具问题是数字化和瞬时混合的,因此与 ICA 模型假设一致。现实世界的录音不仅存在时间延迟,还会受到更复杂的时间变换(下面会详细讨论)。

我们可以重新审视上面的玩具示例,并在源和录音麦克风之间引入随机延迟,以观察当模型假设被违反时,FastICA 如何开始崩溃。

rng = np.random.default_rng(8675309)
t = np.linspace(0, 11, 11000)
x = np.array([
    np.sin(2 * np.pi * t), 
    np.sin(2 * np.pi / 2 * t + 1), 
    np.sin(2 * np.pi / 2 * 2 * t + 2)])
mixing = rng.uniform(-1, 1, [3, 3])
# check that our randomly generated mixing is invertible
assert np.linalg.matrix_rank(mixing) == 3
demixing_true = np.linalg.inv(mixing)

delays = rng.integers(100, 500, (3, 3))
y = np.zeros(x.shape)
for source_i in range(3):
    for signal_j in range(3):
        x_ = x[source_i, delays[source_i, signal_j]:]
        y[signal_j, :len(x_)] += mixing[source_i, signal_j] * x_
t = t[:10000]
x = x[:, :10000]
y = y[:, :10000]

fica = FastICA(3)
z = fica.fit_transform(np.transpose(y))
z = np.transpose(z)
z /= np.reshape(np.max(z, 1), (3, 1))
fig, ax = plt.subplots(3, 1, figsize = (8, 5))
for ii in range(3):
    ax[0].plot(t, x[ii])
    ax[1].plot(t, y[ii])
    ax[2].plot(t, z[ii])
ax[0].set_title("Independent Sources")
ax[1].set_title("Randomly, Linearly, Time-Delayed Mixed Signals")
ax[2].set_title("ICA Components (Match up to Sign and Linear Scaling)")
plt.tight_layout()

针对延迟混合信号的 FastICA:注意到去混合的成分与源信号的形状发生了偏离。

时间延迟问题

值得更详细地检查一下为什么 ICA 不能处理这些时间延迟。毕竟,我们已经在处理一个未知的混合,难道我们不能处理一点时间扰动吗?更进一步,原始 ICA 在结构化数据上是排列不变的!你可以对时间序列或图像数据集进行抽样或像素顺序的洗牌,然后得到相同的 ICA 结果。那么,为什么 ICA 不会对这些时间延迟具有鲁棒性呢?

在现实世界的鸡尾酒会中,问题在于每对扬声器和麦克风之间有不同的时间延迟。将每个来自扬声器的数字样本视为来自随机变量的抽样。当没有延迟时,每个麦克风在同一时间听到相同的抽样/样本。然而,在现实世界中,每个麦克风记录的是相同扬声器的不同延迟样本,就像混合矩阵 A 是未知的,时间延迟也是未知的。当然,实际问题甚至比单一延迟值更复杂:混响、回声和衰减会在信号到达麦克风之前进一步扩散源信号。

让我们更新我们的模型公式,以表示这种复杂的时间延迟。假设房间的声学特性没有实际变化,麦克风和扬声器保持在相同的位置,我们可以写出:

其中 k 表示离散时间延迟索引,而混合矩阵 A 现在是一个矩阵函数,随着 k = 0…T 而变化。换句话说,i- 号麦克风的实际观察值是回溯 T 个样本的源信号的线性混合。此外,我们可以注意到,每个源/麦克风对的单一时间延迟问题(没有更复杂的声学效应)是上述模型公式的一个子情况,其中矩阵 A 在每个 (i, j) 索引对的一个 k 值下取非零形式。

数学上感兴趣的人,或者那些沉浸于信号处理神秘技艺中的人,会注意到现实世界的鸡尾酒会问题模型⁴开始很像 卷积。事实上,这是一种功能卷积的离散模拟,通过傅里叶变换我们可以得到一个可能更易处理的频率空间版本的问题。

这里有很多内容需要解读。鸡尾酒会的卷积表示简洁地揭示了为什么 ICA 在对 BSS 问题的简单处理上注定要失败。现实世界的多传感器音频录音几乎肯定是一个 去卷积 问题,而不是线性解混问题。尽管仍然可以找到问题的近似解决方案(我们将在下面讨论一些策略),但不应假设 ICA 能在时间域中提供有空间意义的解混,除非 做大量 更多的工作。

我们可以再一次回顾我们的示例,通过设计一个随机绝对延迟和长度的非线性卷积来模拟一个基本的卷积。在这种情况下,我们可以真正开始看到 FastICA 组件解决方案与原始源信号显著不同。

rng = np.random.default_rng(8675309)
t = np.linspace(0, 11, 11000)
x = np.array([
    np.sin(2 * np.pi * t), 
    np.sin(2 * np.pi / 2 * t + 1), 
    np.sin(2 * np.pi / 2 * 2 * t + 2)])
mixing = rng.uniform(-1, 1, [3, 3])
# check that our randomly generated mixing is invertible
assert np.linalg.matrix_rank(mixing) == 3
demixing_true = np.linalg.inv(mixing)

delays = rng.integers(100, 500, (3, 3))
impulse_lengths = rng.integers(200, 400, (3, 3))
y = np.zeros(x.shape)
for source_i in range(3):
    for signal_j in range(3):
        impulse_length = impulse_lengths[source_i, signal_j]
        impulse_shape = np.sqrt(np.arange(impulse_length).astype(float))
        impulse_shape /= np.sum(impulse_shape)
        delay = delays[source_i, signal_j]
        for impulse_k in range(impulse_length):
            x_ = x[source_i, (delay + impulse_k):]
            y[signal_j, :len(x_)] += (
                mixing[source_i, signal_j] 
                * x_ * impulse_shape[impulse_k]
            )
t = t[:10000]
x = x[:, :10000]
y = y[:, :10000]

fica = FastICA(3)
z = fica.fit_transform(np.transpose(y))
z = np.transpose(z)
z /= np.reshape(np.max(z, 1), (3, 1))
fig, ax = plt.subplots(3, 1, figsize = (8, 5))
for ii in range(3):
    ax[0].plot(t, x[ii])
    ax[1].plot(t, y[ii])
    ax[2].plot(t, z[ii])
ax[0].set_title("Independent Sources")
ax[1].set_title("Randomly Convolved Signals")
ax[2].set_title("ICA Components (Match up to Sign and Linear Scaling)")
plt.tight_layout()

对于卷积信号的 FastICA:注意到解混后的组件与源信号形状显著不同。

然而,频率空间版本的模型开始更像是一个 ICA 模型问题,至少作为一个线性混合问题。它并不完美:傅里叶变换后的混合矩阵函数在频率空间中并不是平稳的。然而,这可能是我们想要深入研究的问题所在,并且确实是更通用去卷积策略的起点。

“现实世界”与 ICA

不管你做什么,不要在鸡尾酒会上使用 ICA 进行音频源分离。然而,ICA 是否在现实世界中有用呢?

让我们考虑 ICA 最常见的应用之一:脑电图 (EEG) 特征化和分解。EEG 信号是从头皮上的电极(有时也来自大脑中的电极)记录的电位时间序列。应用 ICA 到预处理的 EEG 数据中以识别大脑和身体中的独立电位信号源已经成为一个小型行业。

在 EEG 记录的情况下,ICA 模型的瞬时混合假设肯定得到了满足:电信号相对于人头的长度尺度和采样频率(通常为几十到几百赫兹)几乎是瞬时传播的。这对 ICA 是一个好兆头,事实上,独立成分通常会分离出一些空间上有意义的特征。眼球运动和肌肉活动(皮肤导电性将信号传播到头皮)通常是明显不同的成分。其他成分会在头皮上产生看似有意义的电极激活模式,这些激活被认为是由大脑中的神经元集合作为辐射偶极源产生的。根据头皮上电极位置的准确坐标映射,可以进一步推断这些源的三维位置和方向。

我们已经确定这里满足瞬时混合假设,但其他模型假设怎么样呢?如果电极在头皮上没有移动,并且受试者保持静止,那么常量混合也可能是一个合理的假设。我们测量的通道是否比源更多?ICA 不会生成比记录信号通道更多的独立成分,但如果实际源比可以辨别的源多得多,将空间意义赋予成分可能会存在问题。

最后,源是否独立?这可能会非常棘手!辐射偶极源当然不是单个神经元,而是许多神经元的集体尖峰活动。我们在 EEG 的采样时间尺度上有多大程度上相信这些一致的神经元簇是相互独立的?十年前,Makeig 和 Onton 对这一主题进行了广泛讨论和研究。⁶ 其要点是,源被认为是局部一致的皮层神经元块:邻近连接相对于远离连接的强度既会诱发“池塘涟漪”般的电位(集中在局部源处),也会减少空间上分隔的块之间的依赖关系。也就是说,关于通过 ICA 在复杂领域中检查 EEG 的卷积混合问题的兴趣曾间歇性出现。⁷ ⁸

解卷积与 ICA

ICA 是否仍然可以用来解决现实世界鸡尾酒会问题所示的解卷积问题?让我们回到 BSS 解卷积问题的频率空间表示。记住,这非常接近 ICA 能处理的情况……混合矩阵是线性变换,主要问题在于它不是频率的函数上的平稳的。如果我们对(盲)卷积做一些假设,我们可能能够将 ICA 适应到解卷积问题上。

假设频率空间混合在频率上连续且有些“缓慢”地变化。这里的“缓慢”指的是频率参数的微小变化会引起混合的较小变化。我们在术语上有些模糊,但总体思路是,给定足够的样本,我们可以将 BSS 问题划分到频率空间的子集上,并在每个子集内运行 ICA,假设在频率子集内混合是静态的。例如,我们知道全球范围内混合随频率变化,但也许它变化得足够缓慢,以至于我们可以假设在频谱窗口中它是静态的。因此,在 10 到 15 kHz 之间,我们将使用一堆傅里叶变换样本来估计该频率窗口中的单一静态混合。

理论上,我们可以尝试在整个频率范围内对静态 ICA 解决方案进行插值。因此,如果我们有 10–15 kHz 的 ICA 解混方案和 15–20 kHz 的另一种解决方案,我们可以提出一些插值方案,将我们的两个解决方案中心放在 12.5 kHz 和 17.5 kHz,然后推断这两个点之间的频率混合函数。

然而,有一些模糊之处需要解决。首先,解混矩阵不仅仅是向量,还有一些附加的群体结构我们可能需要关注。其次,ICA 解的组件在排列和缩放方面是不变的……换句话说,再次把 ICA 视为基变换,任何基方向的重新排序或符号/大小的变化都是同样好的解决方案。因此,进行这种频率空间分布 ICA 的策略可以归结为如何解决相邻频率集之间 ICA 解决方案的匹配和一致性问题。

混合鸡尾酒(作者提供的图像)

无忧与细致的特征化

希望在所有这些中有一个更广泛适用的教训。即使在其模型假设是否得到满足存在一些模糊性的情况下,ICA 也可以是一种非常强大的分解技术。事实上,作为研究人员,我几乎总是会选择 FastICA 进行降维,而不是——或者至少与——PCA 进行比较。我特别喜欢用 FastICA 来处理更抽象的数据,而没有正式的 BSS 解释。

为什么 ICA 可以更普遍地使用?因为算法本身只是 BSS 解决方案的抽象近似。FastICA 做的正是它所说的:它找到一种基变换,使数据组件在统计学上最大程度地非高斯——通过峰度(或多或少)来推断。如果这种变换恰好与物理上有意义的独立源重合,那就太好了!如果没有,它仍然可以作为一种有用的变换,类似于 PCA 的抽象用途。如果我们把 PCA 和 FastICA 看作是分别优化第二和第四阶统计量的基变换,它们甚至有非常松散的关系。

但必须小心不要对 ICA 结果进行过多的解读。我们可以说 ICA 成分是最大程度上独立的或非高斯的:当然没问题!但我们能否说 ICA 成分是物理上有意义的独立来源?只有在存在满足我们所提出的假设的基础 BSS 模型问题时才可以。抽象中的 ICA 成分可能确实指示了被非线性和卷积层掩盖的有用关系。我们只需小心不要在没有验证模型假设的情况下过度解读 ICA。

参考文献和脚注

[1] ICA 的两个历史上最著名的变种——FastICA 和 Infomax ICA——可追溯至:

A. Hyvärinen 和 E. Oja,一种用于独立成分分析的快速固定点算法(1997),《神经计算》

A. Bell 和 T. Sejnowski,一种信息最大化方法用于盲分离和盲去卷积(1995),《神经计算》

[2] C. Maklin,Python 中的独立成分分析(ICA)(2019),《数据科学进展》

[3] J. Dieckmann,ICA 介绍:独立成分分析(2023),《数据科学进展》

[4] 我们在这里稍微滥用了一些符号表示,比如忽略了音频录制的边界,例如当t=0时。别担心!毕竟,一切都是对数学符号的滥用。

[5] 在“现实世界”中,任何模型是否完全真实?答案是否定的,Dr. Box回答道。Rob Thomas,厌倦了被打扰,也表示赞同。

[6] S. Makeig 和 J. Onton,ERP 特征和 EEG 动态:ICA 视角(2012),《牛津事件相关电位成分手册》

[7] J. Anemüller, T. J. Sejnowski, 和 S. Makeig,频域脑电图数据的复杂独立成分分析(2003),《神经网络》

[8] A. Hyvärinen, P. Ramkumar, L. Parkkonen, 和 R. Hari,短时傅里叶变换的独立成分分析用于自发 EEG/MEG 分析(2009),《NeuroImage》

克服你的第一个数据科学项目的 6 个初学者友好的技巧

原文:towardsdatascience.com/ideas-how-start-data-science-project-when-beginner-9ed03b7628ca

图片由 Tatiana Syrikova 提供,来自 Pexels

我将引导你了解一些能帮助你打破数据科学初体验难关的见解!最后一个见解有可能显著改变你整个职业生涯的路径。

Ken JeeTowards Data Science Ken Jee

·发表于 Towards Data Science ·阅读时间 7 分钟·2023 年 12 月 17 日

--

目录

  • 1: 你第一个项目的真正目的:这不是为了留下印象

  • 2: 为什么你的第一个项目不需要有创意

  • 3: 复制、调整、学习:令人惊讶的技能发展策略

  • 4: 你必须克服逆境

  • 5: 克服配置工具时的困难

  • 6: 采纳成长心态是成功的关键

  • 附录

进行你的第一个项目可能是你数据科学旅程中最重要的里程碑。然而,知道这个过程的第一步往往充满挑战。我在这里是为了让你知道其实不必如此。

在这篇文章中,我将与你分享开始第一个项目所需了解的内容。

我的目标是澄清你可能对如何开始第一个数据科学项目的误解,并让你有信心尽快开始。

作者提供的图片

这些是六个关键见解,可以帮助你突破对项目的顾虑。最后一个见解有可能彻底改变你整个职业生涯的轨迹。

让我们深入探讨吧!

#1: 你第一个项目的真正目的:这不是为了留下印象

为什么要做一个项目呢?

是为了向潜在雇主展示你的技能吗?还是在与 LinkedIn 上的人建立联系时作为谈资?

实际的原因并不集中在这些概念上。

你初次项目的主要目标是学习。

作者提供的图片

没有必要感到压力去获得反馈或公开分享你的工作。很多人仅仅因为他们认为第一个项目不够好或者不够有趣而迷失方向。

猜猜怎么了?

只要它帮助你学习,就不重要它有多么令人印象深刻。它也不必非常令人印象深刻或复杂才能教会你一些东西。即使是看似简单的项目,也能让初学者掌握和熟悉基本概念、技术或方法论,比如基本的数据处理、可视化或入门统计分析。因此,这为未来的努力奠定了坚实的基础。

#2: 为什么你的第一个项目不需要有创意

我在网上分享了很多关于项目的信息。你可能听过我说,一个好的项目应该是富有创意和有趣的。如果你有兴趣阅读我关于这方面的工作,可以查看下面的附录。

虽然对于你展示给潜在雇主的项目来说,这是真的,但你不必在你的第一个项目中展现任何创意

这可能看起来很无聊,但我的第一个项目就是泰坦尼克数据集。而且,天哪,我的分析做得非常糟糕。不过,我从那个练习中学到了很多关于分类和特征工程的知识。

我个人分析过许多最基本的数据集,包括 Palmer Penguins 数据集、MNIST 数据集和 Kaggle 上的房地产数据集。

这些方法对掌握新技能非常有效,并且非常适合你学习旅程早期的项目。

#3: 复制、调整、学习:技能发展的惊人策略

在你的第一个项目中,你应该复制其他人的工作。

作者提供的图片

你没听错。

为了澄清,你是为了学习而进行复制,你不应当为别人做过的事情而获得荣誉。你也应该明确地不分享你所复制的工作。

一种非常有效的早期学习方法是输入和执行他人编写的代码。一旦执行,进行实验以观察结果并探索结果。

我更喜欢将这种方法应用于图表。由于图表的视觉特性,当调整代码时可以立即看到变化,提供了清晰的进展或变化的指示。

作者提供的图片

还记得我在第一个项目中使用的泰坦尼克数据集吗?如果你想找一些东西来跟随学习,我制作了一个记录我处理该数据集过程的视频,链接在下面的附录中。

作者提供的图片

#4: 你必须克服逆境

这并不令人意外,但第一次项目通常是有挑战性的。你会遇到障碍、错误,并且感到困惑。

不幸的是,许多人在面对这种逆境后选择了放弃。我几乎普遍发现我们在数据科学中会遇到这些挑战。遇到困境是常规安排的一部分。

越早适应这一点并学会暂时离开然后再回到工作中,你的学习旅程就会越早取得进展。

我注意到,散步或离开代码时,常常会激发出自发的解决方案或全新的视角。

这些是你在遇到困境并感觉无法取得任何进展时可以做的,也应该做的事情。

你会惊讶于自己的大脑在你漫步时能够重新整理出什么,当你不刻意集中精力于工作时。

图片来源:作者

此外,我们永远无法为 Medium 算法鼓掌。 如果你发现这篇文章有用,请随意点赞。

#5:克服配置工具时的设置难题

开始的最大障碍之一是使所有数据科学工具在本地计算机上正常工作。

当我刚开始时,配置本地环境所花的时间比我完成整个项目的时间还要长。

幸运的是,有像 Anaconda 这样的工具可以让你的本地环境正常工作,但你仍然会遇到一些配置问题,比如获取正确的包。

幸运的是,随着基于网页的 IDE 的进步,你可以完全避免这个步骤。在你开始第一个项目时,可以使用像 Kaggle、Google Colab 和 DeepNote 这样的免费平台,而无需技术负担。

从本质上讲,你登录这些网站,创建一个实例,就可以使用 Jupyter notebook,而无需安装任何包或解决其他相关问题。

这彻底改变了我顺利启动第一个项目的方式。

#6:采用成长型思维模式是成功的关键

也许开始数据科学和项目的最重要的事情就是你的心态。

仅仅因为一个项目现在很困难,并不意味着你不够优秀。这意味着你还不够擅长数据科学。

当我回去重新做前面提到的 Titanic 数据集时,看到自己有了如此大的进步令人惊讶。我清晰地记得第一次做时有多么困难,而现在我甚至能自信地带领其他人完成那次分析。我的能力、天赋和对这些项目的舒适度发生了令人难以置信的变化。

这种理解你随着时间的推移可以提高的概念被称为成长型思维模式。我在过去的一些文章中详细讨论过这个概念,并在下面的附录中添加了链接。

当个人涉足一个新的领域,尤其是像数据科学这样思维要求高的领域时,大多数人开始时对这些技能并不熟练。如果你遇到困难,应该把这些难题视为成长的机会,而不是障碍。你可以选择那些稍微推动你边界但通过努力可以实现的项目。

此外,你应该集中精力于理解概念和解决问题的努力,而不是纠结于即时结果。为了实现这一点,将复杂的问题分解为更小、可管理的步骤,并在每个阶段庆祝你的进步。

我希望这对你的第一个项目有所帮助。祝你在数据科学之旅中好运。

附录

  • 关于有趣和创意数据科学项目的更多信息,请查看我写的这篇文章:

[## 5 个富有创意的数据科学项目,让你的作品集脱颖而出

脱颖而出可以通过新颖性、影响力、技能或创造力来实现。在这篇文章中,我重点介绍了一些项目想法……

medium.datadriveninvestor.com](https://medium.datadriveninvestor.com/5-imaginative-data-science-projects-that-can-make-your-portfolio-stand-out-6371802a686d?source=post_page-----9ed03b7628ca--------------------------------)

  • 如果你想复制并学习我对 Titanic 数据集的演示,请查看我制作的这个 YouTube 视频:

  • 关于我培养成长心态的经历以及我提升学习能力的其他方法,请参考我写的这篇文章:

[## 学会学习:我如何从愚蠢到终身学习者

分解改变我生活的学习基础

medium.com](https://medium.com/@kenneth.b.jee/learning-to-learn-how-i-went-from-dunce-to-life-long-student-3a7c7c98794c?source=post_page-----9ed03b7628ca--------------------------------)

如果你喜欢关于数据科学、机器学习和人工智能的有趣且信息丰富的视频,请查看我的 YouTube 频道,我会提供评论、教程和其他教育视频。

如果你对数据和人工智能领域中的人们如何做出塑造他们世界观和职业生涯的大决策的独特故事感兴趣,请查看我的播客 Ken’s Nearest Neighbors

要获取关于我内容创作的每周更新以及数据科学行业的额外学习资源,请订阅我的新闻通讯,Data Dribble!

识别:可信因果推断的关键

原文:towardsdatascience.com/identification-the-key-to-credible-causal-inference-c3023143349e?source=collection_archive---------5-----------------------#2023-02-22

提高你的因果智商,通过掌握识别来建立对因果推断的信任

Murat UnalTowards Data Science Murat Unal

·

关注 发表于 Towards Data Science · 阅读时间约 8 分钟·2023 年 2 月 22 日

--

图片由 Paul Skorupskas 提供,来源于 Unsplash

因果推断是利用数据来做出关于因果关系的声明的过程,因此它是数据科学家的核心任务之一。在这个过程中,有两个不同的概念:识别和估计,只有同时掌握这两者,我们才能更好地从数据中建立因果关系。

然而,随着新的估计方法不断出现,数据科学家往往将方法的复杂性与因果推断中的强度等同起来。不幸的是,没有明确的识别,任何复杂的建模或估计方法都无法帮助我们从数据中建立因果关系。因此,在本文中,我们将详细讨论为什么识别优先于估计,以及为什么没有识别因果推断会失败。

如果你对因果推断较陌生或更熟悉机器学习,可以将因果推断中的识别看作是机器学习中一些基本概念的对应概念,如正则化和交叉验证。掌握这些概念对预测任务的成功至关重要,因为任何算法的有效性取决于这些概念在训练过程中是否得到正确应用。一个正确应用而没有数据泄漏的简单正则化回归模型,在未见过的数据上,可能比一个因过拟合和数据泄漏而效果不佳的最先进算法表现更好。

掌握这些机器学习原则的另一个,虽然不太明显的理由,是为了赢得对我们工作的信任。当我们想要部署我们的预测模型时,首先要做的事情之一是说服利益相关者我们的模型不仅仅是记忆了训练过程中看到的内容,而是能够从数据中学习并进行泛化。我们详细描述了如何应用交叉验证,如何通过正则化处理过拟合,以及为什么测试误差是对模型在未见数据上表现的可靠估计。这建立了对我们模型的信任,我们获得了利益相关者的支持。

类似地,在因果推断中,识别不仅可以为估计正确设置舞台,还可以建立对我们工作的信任。因此,能够进行识别分析并清晰地传达,是一个被低估但强大的技能,如果我们想提高因果智商并建立对因果推断的信任,就必须掌握。

潜在结果

要理解识别,首先从潜在结果框架开始是有用的。假设我们感兴趣的是回答成为 Prime 会员是否会导致客户在亚马逊在线商店上花费更多。因为这是一个简单的两种处理条件的案例,我们可以将处理(是否为 Prime 会员)描述为一个二元随机变量,Ti=[0,1]。我们感兴趣的结果是购买金额,比如,成为 Prime 会员后的 12 个月内的购买金额,记作Yi

为了回答这个问题,我们假设我们可以想象一个人如果没有加入 Prime 会发生什么,反之亦然。因此,每个客户有两个潜在结果,一个是如果客户是会员,另一个是如果不是。因果效应是两个潜在结果之间的差异,但只能观察到其中一个。让 Yi1 表示客户 i 作为会员时的潜在结果,Yi0 表示客户 i 作为非会员时的潜在结果。Prime 会员资格对客户 i 的因果效应是潜在结果之间的差异,定义如下:

因为我们从未同时观察到 Yi1Yi0,我们面临因果推断的基本问题,这简单地说明了个体层面的因果推断是不可能的 [1]。

数据中客户 i 的观察结果 Yi 可以与潜在结果连接如下:

通常,我们关注平均处理效应(ATE),即期望值之间的差异。

问题在于:我们需要无条件期望值 E[Yi1]E[Yi0] 来获得 ATE,即如果总体中的每个人都成为 Prime 会员与不成为会员的预期结果差异。然而,我们只观察到条件期望值 E[Yi1|Ti=1]E[Yi0|Ti=0],即在数据中看到的会员和非会员的预期结果,这只是总体的一个样本。所以,除非我们有理由相信 E[Yi1|Ti=1]=E[Yi1]E[Yi0|Ti=0]=E[Yi0],否则我们无法获得 ATE。

看待因果推断挑战的另一种方式是将 ATE(平均处理效应)分解如下:

在这里,ATE 是五个量的函数,而我们从观察数据中只能估计以下三个量:P(Ti=1),使用分配到处理条件的比例;E[Yi1|Ti=1],使用 E[Yi|Ti=1],即会员的平均结果;以及 E[Yi0|Ti=0],使用 E[Yi|Ti=0],即非会员的平均结果。其他两个量是 E[Yi0|Ti=1],即处理条件下的控制下的平均结果,以及 E[Yi1|Ti=0],即控制条件下的处理下的平均结果。请注意这些是未观察到的反事实,我们无法从数据中估计这两个量。

识别

那么,我们如何进行?我们如何证明条件期望值等同于无条件期望值,并且它们的差异确实是 ATE?或者我们如何处理替代表达中的未观察到的反事实?答案是我们做出不可测试的假设并为之辩护。

这正是识别发挥作用的地方。从本质上讲,识别意味着列出从数据中获得的统计估计需要的假设,以便将其赋予因果解释。然而,这并不仅仅是这样。它还意味着要阐明这些假设为何合理,从而我们在数据中找到的关联识别了我们所追求的因果估计量,即 ATE,并且可以被信任为因果关系。因此,识别迫使我们不仅要明确因果关系所需的假设,还要在分析中为这些假设辩护。

现在,我希望你不会感到失望,如果我告诉你包括随机实验在内的每一种因果推断方法,都需要无法检验的假设来建立因果关系。没错,就是这样。即使是因果推断的金标准也无法在不做假设的情况下给出因果关系。问题是,并不是所有的假设都是平等的。有些假设比其他假设更合理,当我们和我们的受众对指导我们因果推断的假设有清晰认识时,我们可以寻找评估这些假设的方法。

需要无法检验的假设来进行因果推断,并不意味着这是不可能的。然而,这确实意味着伴随着高度的不确定性,拥有明确的识别策略在减少这种不确定性方面大有帮助。

这也清楚地表明了为什么识别优先于估计。简单来说,如果识别失败,换句话说,如果我们因果推断的假设不合理,那么无论采用什么建模或估计方法都无法超越关联。另一方面,如果识别有效,我们可以通过利用从非参数到完全参数的方法的各种工具来寻求改进估计。

偏差

我的意思是这样的。假设为了找出 Prime 会员对亚马逊客户购买行为的影响,我告诉你我收集了会员和非会员的历史购买数据,并且将使用这些数据来估计 ATE。这显然意味着我假设我可以通过E[Yi1|Ti=1]来获得E[Yi1],通过E[Yi0|Ti=0]来获得E[Yi0],这就是我的识别假设。现在,在查看分析或数据之前,我们应该问这个假设是否合理。

为了做出判断,让我们看一下从条件期望中获得的以下分解:

那么,通过这种方法,我们得到的结果不是 ATE,而是两部分的结合体:Prime 会员对会员的影响,即处理效应(ATT),以及一个偏差项。简单来说,偏差项告诉我们,如果会员没有加入 Prime,会员与非会员之间的购买差异会是什么。

在许多商业环境中,我们期望自愿订阅某项服务或产品的用户,其购买行为与未订阅的用户有所不同。在我们的例子中,我们可以认为那些加入 Prime 的人是因为他们已经经常使用亚马逊,并且期望继续这样做,加入 Prime 对他们来说是一个好交易。实质上,即使他们没有加入 Prime,会员的购买量也会高于非会员,表明存在正偏差,E[Yi0|Ti=1]>E[Yi0|Ti=0]。这意味着我为因果推断所做的假设不成立,我们无法识别 ATE。

现在,无论我们数据中有多少观察值,或者我们使用简单的均值差异估计量还是进行回归分析,都没有关系。因为我们的识别不成立,最终我们会得到一个关联而非因果效应。

识别策略

社会科学和商业中的重要因果研究是那些在讨论任何建模或估计之前,有专门部分明确描述识别策略的研究。通过这样做,这些研究的思想不仅传达了对发现的信心,还说服了观众相信这些发现可以在维持的假设下被解释为因果关系。大多数可信的因果推断也不需要复杂的建模和估计方法。实际上,当因果推断是可信的时,是因为大多数挑战在识别阶段和统计分析之前已经得到解决。

那么,因果推断的主要识别策略是什么呢?根据它们的识别假设,它们可以被分类如下:

  1. 随机实验

  2. 自然实验

  3. 工具变量

  4. 回归不连续性设计

  5. 可观察变量选择

  6. 带有时间数据的可观察变量选择

基本上,我们遇到的每种估计方法,从简单的均值差异到最新的因果机器学习,都依赖于这些识别策略中的一种。例如,可观察变量选择策略涵盖了从回归调整到双重机器学习的所有内容,包括每一种倾向得分及其匹配算法。因此,对于希望提高因果推断能力的实践者来说,首先详细了解这一策略及其假设,然后再研究各种估计方法,会更有意义。

在接下来的系列文章中,我们将从随机实验开始,逐一详细讨论每种识别策略,探讨其背后的识别假设。

让我们总结一下,因果推断始于识别,而最可信的因果推断是那些具有明确且令人信服的识别策略的,而不是那些拥有最复杂估计方法的。希望因果推断能够受到重视的数据科学从业者,需要掌握识别。

感谢阅读!我希望你觉得这值得花时间。

我致力于为从事因果推断方法和营销数据科学应用的实践者撰写高质量、有用的文章。

如果你对这些领域感兴趣,可以考虑关注我,也欢迎分享你的评论和建议。

参考文献

[1] P. Holland, Statistics and Causal Inference. (1986), 美国统计协会期刊.

[2] L. Keele, The Statistics of Causal Inference: A View from Political Methodology. (2015), 政治分析.

[3] A. Lewbel, The Identification Zoo — Meanings of Identification in Econometrics. (2019), 经济文献期刊.

识别和利用时间序列预测的领先指标

原文:towardsdatascience.com/identifying-and-leveraging-leading-indicators-for-time-series-forecasting-using-granger-causality-d0e6fd5e353f?source=collection_archive---------3-----------------------#2023-09-19

使用 Granger 因果关系和 SARIMAX 模型

Afolabi LagunjuTowards Data Science Afolabi Lagunju

·

关注 发表在 Towards Data Science ·9 min read·2023 年 9 月 19 日

--

图片由 Aron Visuals 提供,来源于 Unsplash

引言

在日常工作中,公司面临着有关调整金融市场、优化供应链操作或制定策略以保持竞争优势的决策。然而,当时间序列模型未能考虑到相互关联的事件或对预测主题施加影响的其他时间序列时,实现高精度预测可能会变得困难。

在本文中,我们将探讨领先指标的概念,如何识别它们,以及如何利用它们来改善时间序列预测。我们将深入了解使用 Python 和来自联邦储备经济数据的真实数据进行的实际实施。

什么是领先指标?

领先指标是帮助预测未来趋势或活动的数据集。一个日常的领先指标示例是突然出现的云层,这可能预示着在接下来一个小时内发生雷暴的可能性。

如何识别和利用领先指标

  1. 领域知识:像所有数据科学项目一样,我们首先要了解我们将要操作的领域。这个过程的一个关键方面是识别可能影响我们想要预测的时间序列的变量或因素。在我们的实际实施中,我们将预测啤酒、葡萄酒和烈酒的销售量ᵈ¹,因此消费者物价指数⁵𝄒ᵈ²、烈酒价格¹𝄒ᵈ³、实际个人收入⁵𝄒ᵈ⁴、工作年龄人口⁵𝄒ᵈ⁵、失业率⁴𝄒ᵈ⁶、劳动参与率⁴𝄒ᵈ⁷、社会福利⁴𝄒ᵈ⁸和消费者贷款²𝄒ᵈ⁹将成为我们的潜在领先指标。

  2. 数据探索与可视化:接下来,我们使用时间序列分解来分析我们的依赖时间序列及其潜在领先指标的季节性成分。我们的目标是识别领先指标中的峰值或谷值,这些峰值或谷值在我们依赖的时间序列中出现类似/相反的变化之前。需要注意的是,领先指标与依赖时间序列之间的关系可以是正面的也可以是负面的。尽管我们在实施中不会深入探讨这一点,但你可以使用seasonal_decompose函数来自stats_model来实现这一点。

  3. 统计测试:在此步骤中,我们通过使用 Granger 因果关系检验来验证我们精心挑选的潜在领先指标对依赖时间序列的影响。

  4. 预处理:接下来,我们对数据进行缩放,以确保所有特征在相同范围内,然后应用主成分分析(PCA)来消除领先指标之间的多重共线性。

  5. 模型构建: 最后,我们使用pmdarima模块中的auto_arima函数构建我们的 SARIMAX 模型,并将领先指标设置为外生值以及其他参数。

什么是格兰杰因果关系?

格兰杰因果关系首次由克莱夫·格兰杰³于 1969 年提出,是一种统计假设检验,用于帮助确定时间序列中的变化是否能预测或“引起”另一时间序列的变化。它已在statsmodels中实现为一个函数。

什么是 SARIMAX?

SARIMAX 代表季节性自回归积分滑动平均模型与外生变量。顾名思义,该模型结合了多个组件,如自回归(AR)、滑动平均(MA)、差分(I,即积分)以及外部因素的纳入(“X”部分)——我们将在其中插入领先指标。

Python 实现

在开始之前,请在Federal Reserve Economic Data(FRED)创建一个账户,并通过此链接获取 API 密钥。请注意,此产品使用了 FRED® API,但未经圣路易斯联邦储备银行的认可或认证。

我们从安装和加载所需的模块开始。

import requests
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime, timedelta
from pandas.tseries.offsets import MonthEnd
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_absolute_percentage_error
from statsmodels.tsa.stattools import adfuller
from statsmodels.tsa.stattools import grangercausalitytests

from pmdarima import auto_arima

接下来,我们将创建一个自定义函数,通过 FRED API 读取数据。

FRED_API_KEY = '__YOUR_API_KEY__'

# Function to read data from FRED API
def get_fred_data(data_id, data_name):
  response = requests.get(f'https://api.stlouisfed.org/fred/series/observations?series_id={data_id}&api_key={FRED_API_KEY}&file_type=json')
  df = pd.DataFrame(response.json()['observations'])[['date', 'value']].rename(columns={'value': data_name})
  df[data_name] = pd.to_numeric(df[data_name], errors='coerce')
  df['date'] = pd.to_datetime(df['date']) + MonthEnd(1)
  df.set_index('date', inplace=True)
  df.index.freq='M'
  return df

现在,让我们读取数据并将其存储在 pandas 数据框中。

dependent_timeseries_id = 'MRTSSM4453USN'
dependent_timeseries_name = 'liquor_sales'

potential_leading_indicators = {
    'USACPIALLMINMEI': 'consumer_price_index',
    'PCU44534453': 'liquor_ppi',
    'DSPIC96': 'real_income',
    'LFWA25TTUSM647S': 'working_age_population',
    'UNRATENSA': 'unemployment_rate',
    'LNU01300000': 'labor_force_participation',
    'A063RC1': 'social_benefits',
    'CCLACBM027NBOG': 'consumer_loans',
}
# Read dependent time series
timeseries = get_fred_data(dependent_timeseries_id, dependent_timeseries_name)

# Join timeseries with potential leading indicators
for data_id, data_name in potential_leading_indicators.items():
  df = get_fred_data(data_id, data_name)
  timeseries = timeseries.join(df)

# We will start our analysis from Jan-2010
timeseries = timeseries['2010':]

# add month we want to predict liquor_sales 
timeseries = timeseries.reindex(timeseries.index.union([timeseries.index[-1] + timeseries.index.freq]))

timeseries

时间序列数据框

对我们的数据进行快速可视化分析显示,我们的因变量时间序列(酒类销售)或多或少地每 12 个月遵循相同的周期。我们将把这个 12 个月的周期作为我们后续时间序列预测中的一个参数。

timeseries[dependent_timeseries_name].plot(figsize=(20,8));

酒类销售趋势

在测试因果关系之前,我们需要确认时间序列数据的平稳性。为此,我们将使用增强型迪基-福勒检验。如果我们的数据集未通过此平稳性测试,我们必须采用递归差分方法,直到它符合测试标准。

# create a copy of the timeseries to use for tests. Be sure to exclude the additional row we added in the previous task
timeseries_for_gc_tests = timeseries[:-1]
all_cols = timeseries_for_gc_tests.columns

stationary_cols = []
diff_times = 0

while True:

  # Test for stationarity
  for col in all_cols:
    adf, pvalue, lagsused, observations, critical_values, icbest = adfuller(timeseries_for_gc_tests[col])
    if pvalue <= 0.05:
      stationary_cols.append(col)

  # Difference the time series if at least one column fails the stationary test
  if set(stationary_cols) != set(all_cols):
    timeseries_for_gc_tests = timeseries_for_gc_tests.diff().dropna()
    diff_times += 1
    stationary_cols = []
  else:
    print(f'No of Differencing: {diff_times}')
    break

现在,我们已经将时间序列数据加载到 pandas 数据框中,并且通过了平稳性测试,我们将使用格兰杰因果关系检验来测试因果关系。

maxlag = 6 # represents the maximum number of past time periods to look for potential causality. We cap ours at 6 months
leading_indicators = []

for x in all_cols[1:]:
    gc_res = grangercausalitytests(timeseries_for_gc_tests[[dependent_timeseries_name, x]], maxlag=maxlag, verbose=0)
    leading_indicators_tmp = []
    for lag in range(1, maxlag+1):
        ftest_stat = gc_res[lag][0]['ssr_ftest'][0]
        ftest_pvalue = gc_res[lag][0]['ssr_ftest'][1]
        if ftest_pvalue <= 0.05:
            leading_indicators_tmp.append({'x': x, 'lag': lag, 'ftest_pvalue': ftest_pvalue, 'ftest_stat': ftest_stat, 'xlabel': f'{x}__{lag}_mths_ago'})
    if leading_indicators_tmp:
        leading_indicators.append(max(leading_indicators_tmp, key=lambda x:x['ftest_stat']))

# Display leading indicators as a dataframe
pd.DataFrame(leading_indicators).reset_index(drop=True).reset_index(drop=True)

酒类销售的领先指标

从我们的测试中,我们可以看到当前月份的酒类销售受到 2 个月前的消费者价格指数ᵈ²和消费者贷款ᵈ¹⁰以及 6 个月前的劳动参与率ᵈ⁷的影响。

既然我们已经建立了领先指标,我们将调整它们的记录,以使它们的滞后数据与其“引起”的当前酒类销售数据位于同一行。

# shift the leading indicators by their corresponding lag periods
for i in leading_indicators:
  timeseries[i['xlabel']] = timeseries[i['x']].shift(periods=i['lag'], freq='M')

# select only the dependent_timeseries_name and leading indicators for further analysis
timeseries = timeseries[[dependent_timeseries_name, *[i['xlabel'] for i in leading_indicators]]].dropna(subset=[i['xlabel'] for i in leading_indicators], axis=0)
timeseries

将领先指标表现为酒类销售的 X 变量

接下来,我们将数据缩放,以使所有特征处于相同范围内,然后应用 PCA 方法消除领先指标之间的多重共线性。

# Scale dependent timeseries
y_scaler = StandardScaler()
dependent_timeseries_scaled = y_scaler.fit_transform(timeseries[[dependent_timeseries_name]])

# Scale leading indicators
X_scaler = StandardScaler()
leading_indicators_scaled = X_scaler.fit_transform(timeseries[[i['xlabel'] for i in leading_indicators]])
# Reduce dimensionality of the leading indicators
pca = PCA(n_components=0.90)
leading_indicators_scaled_components = pca.fit_transform(leading_indicators_scaled)

leading_indicators_scaled_components.shape

最后,我们可以利用 auto_arima 构建我们的 SARIMAX 模型。在此实现过程中,我们将所有参数保持默认,除了季节性标志和每个周期中的期数(m)。

我们将使用截至‘2024-05-31’的时间序列数据训练我们的模型,使用‘2024-06-30’的数据进行测试,然后预测‘2024-07-31’的酒类销售。

# Build SARIMAX model
periods_in_cycle = 12 # number of periods per cycle. In our case, its 12 months
model = auto_arima(y=dependent_timeseries_scaled[:-2], X=leading_indicators_scaled_components[:-2], seasonal=True, m=periods_in_cycle)
model.summary()

SARIMAX 模型摘要

# Forecast the next two periods
preds_scaled = model.predict(n_periods=2, X=leading_indicators_scaled_components[-2:])
pred_2024_06_30, pred_2024_07_31 = np.round(y_scaler.inverse_transform([preds_scaled]))[0]

print("TEST\n----")
print(f"Actual Liquor Sales for 2024-06-30: {timeseries[dependent_timeseries_name]['2024-06-30']}")
print(f"Predicted Liquor Sales for 2024-06-30: {pred_2024_06_30}")
print(f"MAPE: {mean_absolute_percentage_error([timeseries[dependent_timeseries_name]['2024-06-30']], [pred_2024_06_30]):.1%}")

print("\nFORECAST\n--------")
print(f"Forecasted Liquor Sales for 2024-07-31: {pred_2024_07_31}")

测试和预测结果

通过逐步执行该过程,我们预测了 2024 年 7 月的酒类销售金额,估计的 MAPE 仅为 0.4%。

要进一步提高预测的准确性,我们可以探索添加更多潜在的领先指标并调优所使用的模型。

结论

正如我们已探索的那样,领先指标作为未来趋势的早期信号,提供了在完全实现之前预测变化的关键优势。通过利用 Granger 因果性检验等技术来识别领先指标系列,并将其纳入预测模型中,我们可以显著增强预测的准确性和鲁棒性。

谢谢阅读

希望您喜欢本文,并且受到启发尝试应用在您的数据集上。在 Medium 上关注我获取更多类似数据科学文章,让我们在 LinkedIn* 上互相连接。*

参考文献

[1] 查卢普卡 FJ,格罗斯曼 M,萨弗 H. 价格对酒精消费和与酒精相关问题的影响。酒精研究与健康。2002 年;26(1):22–34。 PMID: 12154648; PMCID: PMC6683806。

[2] 卡夫, 哈罗德 E. 和克里斯托弗 G. 吉布斯。“限制发薪日贷款对酒类销售的影响。”银行与保险电子期刊(2015)

[3] 格兰杰, C. W. J. “通过计量模型和交叉谱方法研究因果关系。” Econometrica 37,第 3 期(1969 年):424–38。 doi.org/10.2307/1912791.

[4] 乔根森 MB,彼得森 J,提格森 LC,劳 CJ,克里斯滕森 AI,贝克尔 U,托尔斯特鲁普 JS。饮酒与劳动市场参与:关于工作、失业、病假和社会福利之间转换的前瞻性队列研究。 Eur J Epidemiol. 2019 年 4 月;34(4):397–407。 doi: 10.1007/s10654-018-0476-7。 2019 年 1 月 10 日在线发表。 PMID: 30627937; PMCID: PMC6451700。

[5] 尼尔森, 乔恩 P., 美国酒精需求中的经济和人口因素:增长会计分析。实证经济学,第 22 卷,№1,1997 年 3 月 7 日,可在 SSRN 上获取:ssrn.com/abstract=4686

[6] Prabal, K. De., 经济低迷期间的饮酒行为:来自美国大萧条期间住房市场波动的新证据。《经济学与人类生物学》,第 43 卷,2021 年 12 月。

数据引用

[d1] 美国人口普查局,零售销售:啤酒、葡萄酒和烈酒商店 [MRTSSM4453USN],取自 FRED,美国圣路易斯联邦储备银行; fred.stlouisfed.org/series/MRTSSM4453USN, 2024 年 8 月 28 日。

[d2] 经济合作与发展组织,消费者价格指数(CPI,HICP),COICOP 1999:消费者价格指数:美国总指数 [USACPIALLMINMEI],取自 FRED,美国圣路易斯联邦储备银行; fred.stlouisfed.org/series/USACPIALLMINMEI, 2024 年 8 月 28 日。

[d3] 美国劳工统计局,行业生产者价格指数:啤酒、葡萄酒和烈酒零售商 [PCU44534453],取自 FRED,美国圣路易斯联邦储备银行; fred.stlouisfed.org/series/PCU44534453, 2024 年 8 月 28 日。

[d4] 美国经济分析局,实际可支配个人收入 [DSPIC96],取自 FRED,美国圣路易斯联邦储备银行; fred.stlouisfed.org/series/DSPIC96, 2024 年 8 月 28 日。

[d5] 经济合作与发展组织,年度劳动统计:工作年龄总人口:25 至 54 岁美国 [LFWA25TTUSM647S],取自 FRED,美国圣路易斯联邦储备银行; fred.stlouisfed.org/series/LFWA25TTUSM647S, 2024 年 8 月 28 日。

[d6] 美国劳工统计局,失业率 [UNRATENSA],取自 FRED,美国圣路易斯联邦储备银行; fred.stlouisfed.org/series/UNRATENSA, 2024 年 8 月 28 日。

[d7] 美国劳工统计局,劳动力参与率 [LNU01300000],取自 FRED,美国圣路易斯联邦储备银行; fred.stlouisfed.org/series/LNU01300000, 2024 年 8 月 28 日。

[d8] 美国经济分析局,个人当前转移收入:政府社会福利 [A063RC1],取自 FRED,美国圣路易斯联邦储备银行; fred.stlouisfed.org/series/A063RC1, 2024 年 8 月 28 日。

[d9] 美国联邦储备委员会,消费者贷款:信用卡及其他循环贷款,所有商业银行 [CCLACBM027NBOG],取自 FRED,美国圣路易斯联邦储备银行; fred.stlouisfed.org/series/CCLACBM027NBOG, 2024 年 8 月 28 日。

使用因果机器学习识别 Spotify 歌曲流行的驱动因素

原文:towardsdatascience.com/identifying-drivers-of-spotify-song-popularity-with-causal-ml-934e8347d2aa

什么使一首歌“受欢迎”?

Aashish Nair数据科学前沿 Aashish Nair

·发布于 数据科学前沿 ·10 分钟阅读·2023 年 2 月 1 日

--

图片由 C D-X 提供,来源于 Unsplash

目录

∘ 介绍

∘ 问题陈述

∘ 为什么选择因果机器学习?

∘ 数据收集

∘ 探索性数据分析 (EDA)

∘ 数据建模

∘ 模型解释

∘ 构建网页应用程序

∘ 局限性

∘ 结论

介绍

什么使一首歌受欢迎?当艺术家唱到高音或朗读引人深思的歌词时,很容易为你对一首歌的喜爱找到理由。仅仅因为这首歌是你喜欢的艺术家演唱的也很容易。但这不足以解释当前的音乐格局。在这个饱和的市场中,尽管许多曲目有着相似的声音、流派和风格,有些曲目却能够超越其他曲目。

这就引出了一个问题:是否还有更多隐藏/潜在的音频因素影响我们对某些曲目的倾向?该项目试图通过利用因果机器学习来回答这个问题,建立一个工具来帮助识别 Spotify 歌曲流行的潜在驱动因素。

注意:所有源代码和网页应用程序本身可以在本文末尾提供的代码库中访问。

问题陈述

该项目的目标是建立一个机器学习模型,根据用户定义的特征预测 Spotify 曲目的流行度。该模型将被部署在一个网页应用程序中,其他用户可以访问。

幸运的是,Spotify 将许多原本是定性的音频数据量化,这使得该项目得以实施。例如,Spotify API 提供了 danceability 特征,该特征提供了一个数值,表示一首歌曲的舞蹈适宜性。有关所有提供的音频特征及其描述,请随时访问 Spotify API 文档

Spotify 轨迹还包含 popularity 变量,这是本机器学习项目的目标标签。根据文档:

一首轨迹的受欢迎程度是 0 到 100 之间的一个值,其中 100 表示最受欢迎。受欢迎程度是通过算法计算的,并且主要基于轨迹的总播放次数和这些播放的最近时间。

为什么选择因果机器学习?

通常,通过进行实验(例如 A/B 测试)来证明因果关系,这些实验追踪预测变量及其对目标变量的影响。不幸的是,进行仅在一个变量上有所不同的歌曲对比实验是不可行的。毕竟,歌曲包含许多元素,这些元素很难精确控制。

另一方面,因果机器学习允许用户创建无尽的歌曲模拟,并得出预测的受欢迎程度评分。虽然模型不会直接指示哪些变量导致高受欢迎程度(相关性不等于因果关系),但它将为任何后续的研究和分析提供动力和方向。

数据收集

项目使用的数据通过 Spotify API 获取。具体而言,它使用了Spotipy,这是一个用于 Spotify Web API 的 Python 库,用于获取有关轨迹和艺术家的数据。数据收集涉及从平台上最成功的艺术家那里提取 2022 年发布的轨迹,以便接触到高人气的轨迹。

获取信息需要多个步骤,因为感兴趣的特征需要从不同的 API 端点提取。

获取训练数据的过程如下:

  1. 收集所有顶级艺术家的数据

由于歌曲的受欢迎程度取决于艺术家本身,因此收集如其粉丝数量和音乐流派等基本信息非常重要。以下函数用于收集 1000 位艺术家的数据。

艺术家数据预览(作者创建)

2. 收集顶级艺术家发布的所有轨迹

在收集所有艺术家的数据后,使用 Spotipy 收集每位艺术家的所有 Spotify 轨迹。以下函数用于收集某个艺术家发布的所有轨迹的数据。请注意,特定查询的最大记录数为 1000。

轨迹数据预览(作者创建)

3. 收集所有顶级艺术家的所有音频特征数据

接下来,使用 Spotipy 收集前一步骤中收集的曲目的所有音频数据。以下函数用于收集给定曲目的音频数据(每个曲目通过曲目 ID 进行识别)。

曲目音频特征数据预览

4. 合并所有数据集

通过合并数据集并将每个特征存储在单独的列中,生成的数据集包含艺术家数据和曲目数据,这些数据可以用于训练机器学习模型,以预测歌曲的受欢迎程度。

合并数据集预览(由作者创建)

总体而言,原始数据集包含 101,700 首曲目和 22 列。

提醒:音频特征的定义(第 7 到 18 列)可以在Spotify API 文档中找到。

探索性数据分析(EDA)

在数据集可以用于训练机器学习模型之前,应进行彻底分析,以确定需要添加、丢弃和更改哪些元素。

执行探索性数据分析(EDA)将揭示更多的数据细节,从而提供对在训练任何机器学习模型之前应执行哪些过程和变换的更多洞察。

  1. 处理缺失数据

删除数据集中缺少音频特征的歌曲。

2. 处理重复数据

删除所有具有重复曲目 ID 的记录。

3. 检查受欢迎程度变量

接下来,可视化目标变量popularity特征的值分布。

受欢迎程度评分的直方图(由作者创建)

高受欢迎度曲目不足,这带来了挑战,因为模型需要能够正确识别具有高受欢迎度评分的曲目。解决这个问题的一种方法是使用严重惩罚大错误的评估指标,如均方误差(MSE)或均方根误差(RMSE)。

4. 检查预测变量

同样,预测变量的分布也被可视化。

预测特征的分布(由作者创建)

由于预测特征具有不同的数值范围,因此需要对其进行缩放,以减轻模型的偏差。此外,缩放方法还需要处理某些特征中出现的异常值(例如,生动度、声学度)。

5. 删除特征

如曲目 ID 和艺术家 ID 等对歌曲受欢迎程度没有影响的特征被从数据集中删除。

6. 计算预测变量之间的相关性

多重共线性是指独立变量之间高度相关的概念,这会影响训练模型的性能。为避免这种情况,计算了每个变量的方差膨胀因子(VIF),使用了以下函数。

特征的 VIF 值(作者创建)

由于预测特征的 VIF 值小于 5,因此数据集中没有多重共线性的证据。

6. 处理分类特征

数据集中有两个分类特征需要在训练模型之前处理:keygenre

key 特征表示曲目的调性。目前,调性由数字表示,但这意味着某些调性大于其他调性,这是错误的。因此,该特征将进行一热编码。

不幸的是,genre 特征的一热编码不是一个可行的方法,因为该特征有 609 个独特的值!对这样多唯一值的列进行一热编码只会产生高维数据集。相反,子类别将被合并成一个名为 is_pop_or_rap 的二进制变量,如果歌曲是流行或说唱,则为 1,否则为 0。

数据建模

EDA 透露了建模阶段应该如何进行。它表明,如果基于 MSE 或 RMSE 指标进行调优,模型会有更好的表现。

它还显示数据在用于训练模型之前需要进行一热编码和标准化。

1. 准备训练和测试数据

数据首先被拆分为训练集和测试集。

2. 创建基准

基准将有助于对实际模型的性能进行上下文分析。在本研究中,基准模型是一个具有默认参数的线性回归模型。

3. 创建特征工程管道

在被机器学习算法训练之前,预测变量需要经过特征工程。首先,分类特征 key 将进行一热编码。之后,所有特征将通过标准化进行缩放。

使用以下代码片段将转换和模型存储在管道对象中。

4. 训练模型

在特征工程后,使用数据训练了多种机器学习回归算法。使用的回归模型如下:

  • 线性回归(基准)

  • Lasso 回归

  • 随机森林回归器

  • LightGBM 回归器

  • XGBoost 回归器

对于每个回归模型,基于均方误差指标进行了超参数调优,以确定最佳超参数组合。使用以下函数选择每个模型的最佳超参数。

在确定了模型的超参数后,模型根据 RMSE 和 MAE 指标在测试集上进行了评估。下表总结了所有模型的性能。

测试集上模型表现总结(作者创建)

基准模型在测试集上的 RMSE 和 MAE 分别为 17.65 和 13.03。尽管所有其他模型都优于基准,但随机森林回归器表现突出,其 RMSE 和 MAE 分别为 13.10 和 8.04。

鉴于随机森林模型在测试集上的表现最佳,它将被部署到网页应用程序中。

模型解释

Shapley Addictive Explanations(SHAP)通过展示每个特征对预测的贡献,帮助解释随机森林回归器的表现。这将为了解预测是如何做出的(即哪些特征影响目标)提供一些线索,甚至可以揭示模型或训练数据的不足之处。

SHAP 总结图(作者创建)

根据图表,回归模型的预测最受followersduration_sspeechiness特征的影响。模型认为,具有高粉丝量、中等时长和高词汇量(即冗长)的艺术家的歌曲会产生高人气。直观上,这些因素影响人气是合理的,但令人惊讶的是,模型没有将energyis_pop_or_rap等特征排在前面。

总体而言,模型的评估指标得分表明仍有改进空间。总结图清楚地表明,尽管一些特征与人气的关系得到领域知识支持,但模型并不青睐这些特征。它还表明数据中有限的特征阻碍了模型准确评估人气。

构建网页应用程序

通过网页应用程序部署模型是有效生成多个歌曲预测的方式。

随机森林回归器被部署在 Streamlit 应用程序中,预测具有用户选择特征的曲目的受欢迎程度。

可以通过在终端中输入以下命令运行 Streamlit 应用:

streamlit run app.py

Streamlit 应用预览(作者创建)

用户可以在侧边栏中选择他们的曲目特征,并点击“预测”按钮查看模型的预测结果。通过这个网页应用程序,用户可以利用机器学习模型预测无尽的歌曲参数组合的人气。

限制

尽管使用因果机器学习可以帮助提供一些关于音频特征如何影响歌曲受欢迎程度的见解,但这种方法存在一些需要解决的限制。具体来说,有些问题是研究未能充分深入探讨的。

  1. 音频特征的数值表示

虽然 Spotify 确实为用户提供了量化歌曲某些特征(例如能量)的手段,但分配的数值可能不足以表示其他定性特征。

2. 艺术家/专辑/歌曲营销

毫无疑问,歌曲的受欢迎程度与艺术家如何营销他们的歌曲以及他们自己有很大关系。不幸的是,收集的数据中对这一因素的代表性很少。未来值得考虑的是像唱片公司和艺术家在热门社交媒体平台(例如 Twitter、Instagram)上的参与对流行度的影响。

3. 歌词

虽然你不能通过说某个词、短语或句子来神奇地让某人喜欢一首歌,但歌词无疑在人们享受曲目时起着作用。也许像潜在狄利克雷分配(LDA)这样的主题建模方法可以提供一些关于不同流派歌词类型的洞察。

4. 客户人口统计

不同人口统计学群体的听众可能对曲目有不同的标准。与其将整个受众归为一组,不如按年龄/性别/种族对受众进行细分,并研究这些群体喜欢哪些类型的歌曲。

5. 用户偏好的渐进变化

最后,模型的一个显著缺陷是它只考虑了 2022 年发布的歌曲。即使它充分捕捉到高人气曲目的要素,音乐标准也会随着时间的推移而不可避免地发生变化。因此,这个模型必须持续用新数据进行训练,以保持可用性。

结论

由 John Tekeridis 拍摄的照片:www.pexels.com/photo/person-using-smartphone-340103/

总体而言,我尝试回答了大多数艺术家和唱片公司心中的一个问题。通过利用机器学习,我们能够进行高级模拟,推测具有某些特征的歌曲将如何被听众接受。

如果你有兴趣在你的设备上运行 streamlit 应用程序,或者只是希望查看源代码,你可以通过以下链接访问项目的 Github 仓库:

[## GitHub - anair123/Identifying_Drivers_Of_Song_Popularity_With_Causal_ML]

一首歌的吸引力是什么?如果你浏览过 Spotify 上任何艺术家的专辑,你可能会注意到一些...

github.com](https://github.com/anair123/Identifying_Drivers_Of_Song_Popularity_With_Causal_ML?source=post_page-----934e8347d2aa--------------------------------)

希望你在了解这个项目时和我制作它时一样有趣。

感谢阅读!

在 BigQuery 中使用 SQL 识别新客户和回头客

原文:towardsdatascience.com/identifying-new-and-returning-customers-in-bigquery-using-sql-81f44c9e3598

为了更好地理解客户的兴趣和行为,以及改进你的营销策略。

Romain GrangerTowards Data Science Romain Granger

·发表于Towards Data Science ·8 分钟阅读·2023 年 1 月 4 日

--

Vincent van Zalinge拍摄,来源于Unsplash

主要好处

对客户进行分类,包括新客户和回头客,有助于定义使用哪种营销或销售策略。毫无疑问,这将取决于你的业务性质,你是否优先考虑获取、留存或两者兼顾。

在深入探讨 SQL 方法和步骤之前,以下是这些术语的简单定义和业务示例:

  • 新客户: 指首次购买的顾客

  • 回头客: 指已经进行了多次购买的顾客

汽车公司咖啡店公司为例:

对于汽车公司来说,回头客的可能性较低,因为汽车购买通常不频繁,价格较高,客户可能几年才进行一次购买。策略可能是专注于获取新客户和接触新客户。

相比之下,在线咖啡店销售的是定期购买的消耗品,如咖啡豆或咖啡粉。价格点更为实惠,这提高了客户回头的可能性。

策略可以适用于两种情况:通过赠品、上层漏斗广告等方式获取新客户,或尝试推动品牌发现和认知。也可以用于回头客,通过定制的营销和信息传递、产品推荐、激励和折扣、忠诚度计划等方式。

在 SQL 中,为客户贴标签可能有助于启用一些洞察项目:

  • 理解客户行为:通过检查回头客的行为和衍生模式,你可能会了解什么激励他们再次购买。

  • 个性化和自定义消息:通过将属性(例如newreturning)转发到你的营销工具,并为每种客户类型创建特殊的营销细分

  • 客户体验和满意度:通过进行调查或查看按客户类型提出的客户服务问题

  • 产品教育或理解:通过查看产品的入门和使用(有些产品可能起初较难理解或使用)。

让我们深入数据,一步一步地进行分类!

创建一个包含客户、订单和日期的表格

为了说明如何实现这种分类,我们将使用一些来自Google Merchandise Store的数据,这是一家销售 Google 品牌产品的在线商店。

在这个数据集中,我们有三个月的历史(从 2020 年 11 月 1 日到 2021 年 1 月 31 日),包含各种不同的事件(购买、页面查看、会话开始、加入购物车等)

我们的第一步将是构建一个包含3 个字段的基本表格,其中包含按客户和日期排列的订单

Google Analytics 4(GA4)数据的 SQL 查询如下:

SELECT
  user_pseudo_id AS user_id,
  ecommerce.transaction_id AS order_id,
  PARSE_DATE('%Y%m%d', event_date) AS order_date
FROM
  `bigquery-public-data.ga4_obfuscated_sample_ecommerce.events_*`
WHERE
  event_name = 'purchase'
  AND ecommerce.transaction_id <> '(not set)'
GROUP BY
  1,
  2,
  3

数据被过滤以仅包含具有有效交易 ID 的购买记录行

值得注意的是,如果数据无法访问、未被正确跟踪或出于其他原因,Google Analytics 将存储一个默认的(not set)值。

然后,结果在所有三个维度上进行分组,使每一行代表每个客户在每个日期的唯一订单。

这会生成以下数据表,我们的第一步完成了!

每一行代表客户在特定日期下的一个订单(图片来自作者

如果你想了解更多关于“未设置”值的信息,可以参考 Google Analytics 文档。

[## “未设置”值的含义

是 Analytics 在未收到你选择的维度的任何信息时使用的占位符名称……

support.google.com](https://support.google.com/analytics/answer/2820717?hl=en&source=post_page-----81f44c9e3598--------------------------------#zippy=%2Cin-this-article)

如果你想访问和探索 Google Analytics 4 的样本数据集,你可以从这个链接获取。

[## BigQuery 样本数据集用于 Google Analytics 4 电子商务网页实现 | Google Analytics…

Google Merchandise Store 是一个销售 Google 品牌商品的在线商店。该网站使用 Google Analytics 4 的……

developers.google.com](https://developers.google.com/analytics/bigquery/web-ecommerce-demo-dataset?source=post_page-----81f44c9e3598--------------------------------)

将客户分类为新客户或回头客

我们的目标是根据客户的订单历史确定他们是新客户还是回头客,并在我们的表格中添加一列以给他们打上标签。

我们使用窗口函数 DENSE_RANK() 来找到每个客户的第一次订单。

  • 订单的排名为 1时,我们认为该客户是 'new'

  • 订单的排名大于 1时,我们认为该客户是 'returning'

SELECT
  *,
  CASE
    WHEN DENSE_RANK() OVER(PARTITION BY user_id ORDER BY order_date) = 1 
    THEN 'new' ELSE 'returning'
END
  AS customer_type
FROM
  `datastic.new_existing.base_table`

使用 DENSE_RANK() 允许我们将相同的排名分配给同一天内下的多个订单。有关此函数和其他编号函数的更多详细信息,您可以阅读这篇中等文章:

[## 在 BigQuery 中使用 SQL 的编号函数之间的差异]

了解如何使用排名、密集排名、行号、累计分布、百分位排名、四分位数、百分位数等…

towardsdatascience.com](/differences-between-numbering-functions-in-bigquery-using-sql-658fb7c9af65?source=post_page-----81f44c9e3598--------------------------------)

您将在新列 customer_type 中看到,根据 order_datecustomer_id 来确定的标签。

基于订单历史对客户进行新客户或回头客分类(图片来自 作者

在 2020 年 9 月 12 日,客户 13285520 下了第一笔订单,并被标记为 'new'

然后,在 2020 年 12 月 12 日,这个相同的客户下了第二笔订单,这使其被标记为 'returning' 类型。

直观分析新客户和回头客

然后,我们可以绘制新客户与回头客户的比例随时间的变化:

随着时间的推移,新客户与回头客的比较(图片来自 作者

在这种情况下,我们观察到大多数客户都是新的,而真正回头的客户很少。但请记住,我们没有很多历史数据。

回购率

将客户分类为 newreturning 的一个潜在好处是,我们可以计算一个指标,提供一个总体评分,表示有多少客户会回购。

从我们之前的加班图表中,我们可以假设 Google 商户商店的回购率较低。为了计算这个指标,我们使用以下公式:

回头客率 (%) = (回头客数量 / 客户总数) × 100

在这 3 个月的时间里,我们有 3713 个客户(注意这些客户在某个时点都是新的),其中 256 个客户下了多于一个订单。

这给出的是 (256/3713) * 100 = 6.9% 回头客率

额外的建议和想法

考虑时间分辨率

在我们的例子中,我们每天查看客户订单,考虑时间的作用,而不仅仅是订单的数量。

如果你只运行一个查询,查看每个客户的订单数量,而不考虑时间(例如,假设一个有超过 1 个订单的客户是returning),可能会发生一些客户在同一天有多个订单,这样他们会被认为是回头客,但可能不会在之后再回来。

此外,回顾按月或按年也可能导致不同的数字。时间段越长,客户同时是newreturning的机会就越高。在这种情况下,这涉及到管理重复数据并添加额外的分类,如both,或优先考虑newreturning类型。

为了说明这个想法,让我们只查看 2020 年的每个月(我们将之前的查询结果存储在一个名为customer_table的表中):

SELECT
  user_id,
  DATE_TRUNC(order_date,MONTH) AS month,
  STRING_AGG(DISTINCT customer_type
  ORDER BY
    customer_type ASC) AS customer_type
FROM
  customer_table
WHERE
  order_date < '2021-01-01'
GROUP BY
  1,
  2

STRING_AGG()函数用于将不同的customer_type值按字母顺序连接成一个字符串(这样在使用CASE WHEN语句时更容易,因为值的顺序将保持按字母顺序排列)。

查询将返回以下结果:

一个客户可能在同一个月内既是新客户又是回头客(图片来自作者)。

如你所见,在 2020 年 12 月,客户(234561…901)第一次下单,并在同一个月再次购买。

在这种情况下,你可能想要定义以下分类:

  • 将这些客户视为new

  • 将这些客户视为both

要在 SQL 中更改标签,你可以这样做:

WITH
  customer_month AS (
  SELECT
    user_id,
    DATE_TRUNC(order_date,MONTH) AS month,
    STRING_AGG(DISTINCT customer_type
    ORDER BY
      customer_type ASC) AS customer_type
  FROM
    customer_table
  WHERE
    order_date < '2021-01-01'
  GROUP BY
    1,
    2)

  -- Main Query
SELECT
  *,
  CASE
    WHEN customer_type LIKE 'new,returning' THEN 'both'
  ELSE
  customer_type
END
  AS c_type
FROM
  customer_month

我们可以通过直接按月分组找到更理想的查询,然而,这在使用报告工具并保持每日分辨率时可能是有用的,例如,在仪表板上添加过滤器。

带有新字段的表格将如下所示:

同时是新客户和回头客的客户被分类为不同类型(图片来自作者)。

额外的支持指标

虽然重复购买率可以是客户对你的产品的忠诚度或满意度的一个指标,但它并不表示客户产生了多少收入(他们是否带来了更多价值)或客户是否对他们的购买感到满意。

为了支持这个假设,我们可以将重复购买率与其他指标结合起来,例如:

  • 客户终身价值 (CLV),例如,可能是一个重要的增长指标,展示了客户将与你保持多久以及他们在你的产品上花费了多少。

  • 净推荐值 (NPS) 可能是另一个有趣的指标,用于评估客户满意度,并可能通过附加问题或调查了解他们为何再次购买。

我们还可以添加其他几个指标,包括平均订单值 (AOV) 或使用按月或按年分组的方法来计算客户保留率。

我们还可能使用 RFM 评分(最近性、频率、货币性)或其他方法来更好地理解您的客户群体。

参考资料与数据集

本文使用的数据集来自于BigQuery 公共数据,是 Google Analytics 数据的一个样本,遵循CC BY 4.0 许可。

[## BigQuery 示例数据集用于 Google Analytics 4 电子商务网站实施 | Google Analytics…

Google 商品商店是一个在线商店,销售 Google 品牌商品。该网站使用 Google Analytics 4 的…

developers.google.com](https://developers.google.com/analytics/bigquery/web-ecommerce-demo-dataset?source=post_page-----81f44c9e3598--------------------------------)

识别城市区域的热点

原文:towardsdatascience.com/identifying-topical-hot-spots-in-urban-areas-3c47cde5ae10

布达佩斯的时髦热点。

一个通用框架,使用 OpenStreetMap 和 DBSCAN 空间聚类来捕捉最受热捧的城市区域

米兰·亚诺索夫数据科学前沿 米兰·亚诺索夫

·发表于数据科学前沿 ·9 分钟阅读·2023 年 10 月 16 日

--

在这篇文章中,我展示了一种快速且易于使用的方法,能够基于从OpenStreeetMapOSM)收集的兴趣点(POI)来识别特定兴趣的热点,使用了 sklearn 的DBSCAN算法。首先,我将收集一些我在 ChatGPT 上找到的属于几个类别的 POI 的原始数据,并假设这些数据具有所谓的时髦生活方式的特征(例如,咖啡馆、酒吧、市场、瑜伽馆);将这些数据转换为便捷的 GeoDataFrame 后,我进行地理空间聚类,最后,根据不同城市功能在每个聚类中的混合程度来评估结果。

尽管我所称之为“时髦”的主题和与之相关的 POI 类别在某种程度上是随意的,但它们可以很容易地被其他主题和类别替代——自动热点检测方法保持不变。这种易于采用的方法的优势包括从识别支持创新规划的本地创新中心到检测支持城市规划倡议的城市次中心、评估企业的不同市场机会、分析房地产投资机会或捕捉旅游热点。

所有图像均由作者创建。

1. 从 OSM 获取数据

首先,我获取目标城市的行政多边形。由于布达佩斯是我的家乡,为了方便(现场)验证,我使用了它。然而,由于我仅使用了OSM的全球数据库,这些步骤可以很容易地应用于 OSM 覆盖的世界其他任何地方。特别是,我使用 OSMNx 包以非常简单的方式获取行政边界。

import osmnx as ox # version: 1.0.1

city = 'Budapest'
admin = ox.geocode_to_gdf(city)
admin.plot()

这个代码块的结果:

布达佩斯的行政边界。

现在,使用 OverPass API 下载在布达佩斯行政边界的边界框内的 POI。在 amenity_mapping 列表中,我编制了一个与嬉皮士生活方式相关的 POI 类别列表。我还必须在此说明,这是一种模糊的、非专业的分类方法,使用这里介绍的方法,任何人都可以相应地更新类别列表。此外,还可以结合其他包含更精细多层次分类的 POI 数据源,以便更准确地描述给定主题。换句话说,这个列表可以根据你的需要进行任何方式的修改——从更好地覆盖嬉皮士相关内容到将这个练习重新调整到任何其他主题分类(例如,食品广场、购物区、旅游热点等)。

注意:由于OverPass下载器返回的是边界框内的所有结果,在这个代码块的末尾,我使用 GeoPandas 的叠加功能过滤掉那些在行政边界之外的 POI。

import overpy # version: 0.6
from shapely.geometry import Point # version: 1.7.1
import geopandas as gpd # version: 0.9.0

# start the api
api = overpy.Overpass()

# get the enclosing bounding box
minx, miny, maxx, maxy = admin.to_crs(4326).bounds.T[0]
bbox = ','.join([str(miny), str(minx), str(maxy), str(maxx)])

# define the OSM categories of interest
amenity_mapping = [
    ("amenity", "cafe"),
    ("tourism", "gallery"),
    ("amenity", "pub"),
    ("amenity", "bar"),
    ("amenity", "marketplace"),
    ("sport", "yoga"),
    ("amenity", "studio"),
    ("shop", "music"),
    ("shop", "second_hand"),
    ("amenity", "foodtruck"),
    ("amenity", "music_venue"),
    ("shop", "books"),
]

# iterate over all categories, call the overpass api, 
# and add the results to the poi_data list
poi_data  = []

for idx, (amenity_cat, amenity) in enumerate(amenity_mapping):
    query = f"""node"{amenity_cat}"="{amenity}";out;"""
    result = api.query(query)
    print(amenity, len(result.nodes))

    for node in result.nodes:
        data = {}
        name = node.tags.get('name', 'N/A')
        data['name'] = name
        data['amenity'] = amenity_cat + '__' + amenity
        data['geometry'] = Point(node.lon, node.lat)
        poi_data.append(data)

# transform the results into a geodataframe
gdf_poi = gpd.GeoDataFrame(poi_data)
print(len(gdf_poi))
gdf_poi = gpd.overlay(gdf_poi, admin[['geometry']])
gdf_poi.crs = 4326
print(len(gdf_poi))

这个代码块的结果是每个下载的 POI 类别的频率分布:

每个下载的 POI 类别的频率分布。

2. 可视化 POI 数据

现在,可视化所有 2101 个 POI:

import matplotlib.pyplot as plt
f, ax = plt.subplots(1,1,figsize=(10,10))
admin.plot(ax=ax, color = 'none', edgecolor = 'k', linewidth = 2)
gdf_poi.plot(column = 'amenity', ax=ax, legend = True, alpha = 0.3)

这个代码单元的结果:

布达佩斯所有下载的 POI 按其类别标记。

这个图表相当难以解释——除了市中心非常拥挤之外,所以我们使用一个交互式可视化工具,Folium

import folium
import branca.colormap as cm

# get the centroid of the city and set up the map
x, y = admin.geometry.to_list()[0].centroid.xy
m = folium.Map(location=[y[0], x[0]], zoom_start=12, tiles='CartoDB Dark_Matter')
colors = ['blue', 'green', 'red', 'purple', 'orange', 'pink', 'gray', 'cyan', 'magenta', 'yellow', 'lightblue', 'lime']

# transform the gdf_poi
amenity_colors = {}
unique_amenities = gdf_poi['amenity'].unique()
for i, amenity in enumerate(unique_amenities):
    amenity_colors[amenity] = colors[i % len(colors)]

# visualize the pois with a scatter plot
for idx, row in gdf_poi.iterrows():
    amenity = row['amenity']
    lat = row['geometry'].y
    lon = row['geometry'].x
    color = amenity_colors.get(amenity, 'gray')  # default to gray if not in the colormap

    folium.CircleMarker(
        location=[lat, lon],
        radius=3,  
        color=color,
        fill=True,
        fill_color=color,
        fill_opacity=1.0,  # No transparency for dot markers
        popup=amenity,
    ).add_to(m)

# show the map
m

该地图的默认视图(你可以通过调整 zoom_start=12 参数轻松更改):

布达佩斯所有下载的 POI 按其类别标记——交互版本,第一次缩放设置。

然后,可以更改缩放参数并重新绘制地图,或使用鼠标简单地放大:

布达佩斯所有下载的 POI 按其类别标记——交互版本,第二次缩放设置。

或者完全缩小:

布达佩斯所有下载的 POI 按其类别标记——交互版本,第三次缩放设置。

3. 空间聚类

现在我已经手头有了所有必要的 POI,我使用 DBSCAN 算法,首先编写一个函数来处理 POI 并进行簇分析。我只会微调 DBSCANeps 参数,它本质上量化了簇的特征大小,即要分组在一起的 POI 之间的距离。此外,我将几何图形转换为本地 CRS(EPSG:23700)以使用国际单位制。更多关于 CRS 转换的信息 在这里

from sklearn.cluster import DBSCAN # version: 0.24.1
from collections import Counter

# do the clusteirng
def apply_dbscan_clustering(gdf_poi, eps):

    feature_matrix = gdf_poi['geometry'].apply(lambda geom: (geom.x, geom.y)).tolist()
    dbscan = DBSCAN(eps=eps, min_samples=1)  # You can adjust min_samples as needed
    cluster_labels = dbscan.fit_predict(feature_matrix)
    gdf_poi['cluster_id'] = cluster_labels

    return gdf_poi

# transforming to local crs
gdf_poi_filt = gdf_poi.to_crs(23700)    

# do the clustering
eps_value = 50  
clustered_gdf_poi = apply_dbscan_clustering(gdf_poi_filt, eps_value)

# Print the GeoDataFrame with cluster IDs
print('Number of clusters found: ', len(set(clustered_gdf_poi.cluster_id)))
clustered_gdf_poi

该单元格的结果:

在 POI GeoDataFrame 中预览,每个 POI 按其簇 ID 标记。

总共有 1237 个簇——如果我们只是看舒适的 hipster 热点,这似乎有点太多了。让我们查看它们的大小分布,然后选择一个大小阈值——将一个只有两个 POI 的簇称为热点可能不太靠谱。

clusters = clustered_gdf_poi.cluster_id.to_list()
clusters_cnt = Counter(clusters).most_common()

f, ax = plt.subplots(1,1,figsize=(8,4))
ax.hist([cnt for c, cnt in clusters_cnt], bins = 20)
ax.set_yscale('log')
ax.set_xlabel('Cluster size', fontsize = 14)
ax.set_ylabel('Number of clusters', fontsize = 14)

该单元格的结果:

簇大小分布。

根据直方图中的差距,让我们保留至少有 10 个兴趣点(POI)的簇!目前,这是一个足够简单的工作假设。然而,这也可以通过更复杂的方式来处理,例如,通过考虑不同类型的 POI 或覆盖的地理区域。

to_keep = [c for c, cnt in Counter(clusters).most_common() if cnt>9]
clustered_gdf_poi = clustered_gdf_poi[clustered_gdf_poi.cluster_id.isin(to_keep)]
clustered_gdf_poi = clustered_gdf_poi.to_crs(4326)
len(to_keep)

这个片段显示有 15 个簇满足过滤条件。

一旦我们有了 15 个真实的 hipster 簇,将它们放在地图上:

import folium
import random

# get the centroid of the city and set up the map
min_longitude, min_latitude, max_longitude, max_latitude = clustered_gdf_poi.total_bounds
m = folium.Map(location=[(min_latitude+max_latitude)/2, (min_longitude+max_longitude)/2], zoom_start=14, tiles='CartoDB Dark_Matter')

# get unique, random colors for each cluster
unique_clusters = clustered_gdf_poi['cluster_id'].unique()
cluster_colors = {cluster: "#{:02x}{:02x}{:02x}".format(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) for cluster in unique_clusters}

# visualize the pois
for idx, row in clustered_gdf_poi.iterrows():
    lat = row['geometry'].y
    lon = row['geometry'].x
    cluster_id = row['cluster_id']
    color = cluster_colors[cluster_id]

    # create a dot marker 
    folium.CircleMarker(
        location=[lat, lon],
        radius=3, 
        color=color,
        fill=True,
        fill_color=color,
        fill_opacity=0.9,  
        popup=row['amenity'], 
    ).add_to(m)

# show the map
m

Hipster POI 簇 — 第一级缩放。

Hipster POI 簇 — 第二级缩放。

Hipster POI 簇 — 第三级缩放。

4. 比较簇

每个簇都算作一个别致的 hipster 簇——然而,它们都必须以某种方式独特,对吧?让我们通过比较它们所提供的 POI 类别组合来看看它们有多独特。

首先,追求多样性,并通过计算每个簇中 POI 类别的熵来衡量它们的多样性/变化性。

import math
import pandas as pd

def get_entropy_score(tags):
    tag_counts = {}
    total_tags = len(tags)
    for tag in tags:
        if tag in tag_counts:
            tag_counts[tag] += 1
        else:
            tag_counts[tag] = 1

    tag_probabilities = [count / total_tags for count in tag_counts.values()]
    shannon_entropy = -sum(p * math.log(p) for p in tag_probabilities)
    return shannon_entropy

# create a dict where each cluster has its own list of amenitiy
clusters_amenities = clustered_gdf_poi.groupby(by = 'cluster_id')['amenity'].apply(list).to_dict()

# compute and store the entropy scores
entropy_data = []
for cluster, amenities in clusters_amenities.items():
    E = get_entropy_score(amenities)
    entropy_data.append({'cluster' : cluster, 'size' :len(amenities), 'entropy' : E})

# add the entropy scores to a dataframe
entropy_data = pd.DataFrame(entropy_data)
entropy_data

该单元格的结果:

每个簇基于其 POI 配置文件的多样性(熵)。

以及对该表的快速相关性分析:

entropy_data.corr()

簇特征之间的相关性。

在计算聚类 ID、聚类大小和聚类熵之间的相关性后,发现大小和熵之间有显著的相关性;然而,这远未解释所有的多样性。显然,确实有些热点比其他热点更具多样性——而其他热点则相对更专业。它们专注于什么?我将通过比较每个聚类的 POI 档案与每个 POI 类型在聚类中的总体分布,并挑选出与平均水平相比最典型的三个 POI 类别来回答这个问题。

# packing the poi profiles into dictionaries
clusters = sorted(list(set(clustered_gdf_poi.cluster_id)))
amenity_profile_all = dict(Counter(clustered_gdf_poi.amenity).most_common())
amenity_profile_all = {k : v / sum(amenity_profile_all.values()) for k, v in amenity_profile_all.items()}

# computing the relative frequency of each category
# and keeping only the above-average (>1) and top 3 candidates
clusters_top_profile = {}
for cluster in clusters:

    amenity_profile_cls = dict(Counter(clustered_gdf_poi[clustered_gdf_poi.cluster_id == cluster].amenity).most_common() )
    amenity_profile_cls = {k : v / sum(amenity_profile_cls.values()) for k, v in amenity_profile_cls.items()}

    clusters_top_amenities = []
    for a, cnt in amenity_profile_cls.items():
        ratio = cnt / amenity_profile_all[a]
        if ratio>1: clusters_top_amenities.append((a, ratio))
        clusters_top_amenities = sorted(clusters_top_amenities, key=lambda tup: tup[1], reverse=True)
        clusters_top_amenities = clusters_top_amenities[0:min([3,len(clusters_top_amenities)])]

    clusters_top_profile[cluster] = [c[0] for c in clusters_top_amenities]

# print, for each cluster, its top categories:
for cluster, top_amenities in clusters_top_profile.items():
    print(cluster, top_amenities)

该代码块的结果:

每个聚类的独特设施指纹。

顶级类别描述已经显示出一些趋势。例如,聚类 17 显然是用于饮酒的,而 19 也混合了音乐,可能还有派对。聚类 91 包含书店、画廊和咖啡馆,显然是白天放松的地方,而聚类 120,则有音乐和画廊,可以是任何酒吧巡游的绝佳热身。从分布中,我们也可以看到,跳进酒吧总是合适的(或者,根据使用情况,我们应该根据类别频率考虑进一步的规范化)!

结论

作为本地居民,我可以确认这些聚类非常合理,并且尽管方法简单,但很好地代表了期望的城市功能组合。当然,这只是一个快速的试点,可以通过多种方式进行丰富和改进,例如:

  • 依赖于更详细的 POI 分类和选择

  • 在进行聚类时考虑 POI 类别(语义聚类)

  • 通过社交媒体评论和评分等方式丰富 POI 信息

如果 AI 编码工具减少了我们需要的工程师数量,我们该如何支配预算?

原文:towardsdatascience.com/if-ai-coding-tools-reduce-the-number-of-engineers-we-need-where-do-we-spend-our-budgets-abd6248ad833

作者使用 Midjourney 生成的图片

AI 对产品工程团队的影响 — 第四部分

Mark RidleyTowards Data Science Mark Ridley

·发表于Towards Data Science ·9 分钟阅读·2023 年 7 月 28 日

--

这是一个六部分系列的第四部分,调查了像 Github Copilot、ChatGPT 和 Amazon CodeWhisperer 这样的生成性 AI 生产力工具如何影响整个产品工程团队的结构。

第三部分,我们探讨了:

  1. 生成性 AI 工具如何潜在颠覆长期以来的 5 名工程师对 1 名产品经理的比例。

  2. 像 Github Copilot 和 AWS Amplify Studio 这样的工具如何重新塑造产品开发,将工程师的焦点从手动编码转向设计、架构和集成。

  3. 生成性 AI 工具如何帮助面临技术过时问题的团队,轻松处理复杂的移植和重构工作。

  4. AI 工具对移动和网页应用开发的潜在统一影响,减少重复工作并弥合网页、Android 和 iOS 开发之间的技能差距。

  5. 编码自动化对初级开发人员和工程进步的影响

我们的新组织将是什么样的?

LLMs 已经改变了游戏,并让我们接触到了生成性 AI——尽管麦肯锡的预测存在,但它们现在至少可以给人“应用专业知识”的印象。与虚拟现实和 Web 3.0 的虚假承诺不同,生成性 AI 工具有大规模、有意义的机会改变我们的工作,并且在这一时代变革中会有赢家和输家。

尽管美国经济的近期积极迹象以及微软、谷歌和 Meta 的强劲盈利回归令人振奋,全球经济中的通胀关联的犹豫可能会影响人们对这一机会的反应。如果我们回到 2023 年之前的繁荣时期,大多数公司会把当前情况视为一个增长机会,受到了过去五年疯狂投资泡沫和科技估值的影响。

但我们现在处于一个不同的世界。对于那些没有关注风险投资惨淡状况的人来说,2023 年第一季度的融资较 2022 年第一季度下降了 53%。在金融科技融资方面,情况更为严峻,2023 年第二季度的融资环比下降 48%,这是自 2017 年以来未曾见过的水平。风险投资的繁荣已经彻底结束(除非你碰巧是一家真正的、诚实的深科技 AI 公司,这里还有另一场疯狂的淘金热)。

更加严峻的宏观经济条件,导致我今年所接触的几乎每一家初创公司都削减了任何可自由支配的开支以应对经济下行,这意味着对许多公司来说,生成式 AI 编码工具所带来的机会将被视为一种生存选择,以保护现金和运营资金。

对于那些财务状况更健康的公司而言,这一机会可能使他们能够在保持或增长产品工程预算之间做出选择,但需要重新调整团队以适应新的现实。

让我们来玩一场游戏

作为一个思想实验,让我们接受本系列前面部分的推理,假设这些新工具确实可以将工程比例从 5:1 转变为 1:1。

[另一个提醒,我不认为这一点 发生,但我相信它 可能 发生,考虑到如果真的发生了,其影响是有用的]

让我们看看如果将 1:1 比例映射到三种不同的情景中会发生什么:首先,一家公司手头充裕,拥有投资于增长的现金,并且不想失去任何有才华的工程师。其次,一家公司面临财务限制,选择保持相同数量的团队和产出,但通过减少工程人员来利用可观的成本节省。最后,考虑一家选择保持预算不变,但将角色和团队结构转变为符合新比例的公司,从而使其成本保持中立但产出增加。

毋庸置疑,这些模型极其简化,最好将其视为漫画,以鼓励我们思考未来。

起始情景

假设我们有一个 100 人的产品和工程团队。将我的产品和工程比率扩展到其他角色,我假设每个团队有一个产品经理和五个工程师。

我还将假设每个团队中有两个额外的‘其他’角色(可以是数据工程师、QA、设计、UX、DevOps 等)。这将我们的团队人数带到‘五到九人’,即两个披萨的甜蜜点。

现在,团队是不能自我运转的,所以我还假设每个团队在某些中央或管理团队中有一个额外的‘非团队’角色,负责保持组织的重量不会自我压垮,同时发送电子邮件和购买甜甜圈。

因此,对于每个团队:有 1 个产品角色,5 个工程师,2 个其他团队角色和 1 个非团队角色。为了简化,我将数据科学、机器学习工程、分析和数据治理角色从这些模型中分离出来。

这意味着我们 100 人的整体产品和技术组织包括 11 个团队。

让我们使用一些非常基本的假设,假设我们公司中每个人的年薪为 65,000 英镑,并且有大约 1.5 倍工资的附加人员成本。这给我们带来了大约 1000 万英镑的总工资账单。这些数字(和货币)对你的组织来说肯定不现实,但足以满足我们的需求。

在我们 100 人的产品工程团队(今天的 5:1 比率)中,我们有 89 人分布在 11 个团队中。在这 11 个团队中有 11 位产品经理和 55 位工程师。

现在,让我们模拟一些不同的场景。

场景 1:增长

我们采取一个极其乐观的观点。我们的第一家公司不仅异常盈利且现金充裕,而且对所有工程师深感承诺。领导者决定不想告别任何工程师,而是希望拥抱增长。

随着我们新的 1:1 技术与产品比率,这为将输出从 11 个创造价值的团队增加到 55 个提供了一个令人兴奋的机会。首次,这可能足以在今年交付整个计划的路线图!

当然,这种无限的乐观伴随着一些挑战。首先,他们将需要招聘大量新的产品角色——准确来说是 44 个。而且他们跟踪的结果数量的任何增加都将对所有其他角色产生影响。

基于我们之前为 QA、UX 和设计等其他角色建立的比率,他们将看到增加 88 个‘其他团队’角色,仅仅是为了跟上所有成果。而且更多的人意味着更多的支持人员——更多的经理,更多的 HR 业务伙伴,更多的招聘人员。

这个极不现实的、极度乐观的场景将我们引导至一个几乎是原来三倍大小的组织(275 人对 100 人),相应地,毛薪资账单也大幅增加(从 1000 万英镑到 2700 万英镑)。但,至少我们没有减少工程角色,对吧?

场景 2:节省现金,削减成本

对于目前面临经济挑战的公司来说,CFO 或 CEO 总会问这个问题:“我们可以在哪里削减成本?”

在冷静而逻辑严密的目光下,产品和技术领导者的问题将变成:“我们的团队可以做得多小,还能实现相同的成果。”我们的新比例使得这个问题的答案尤为显著。曾经我们需要 55 名工程师来支持 11 个团队,现在每位开发者的生产力提高将表明我们可以对这个数字进行大幅削减。

如果公司面对这一挑战的领导者做出的决定纯粹是经济上的,他们将选择保留 11 个团队,并保护他们承诺的所有成果。但这会导致工程职能的大幅削减,预算中削减了 44 个角色。

尽管我们的基本比例显示其他团队和非团队角色不受影响,但现实可能会有所不同。我们的整体产品工程团队人数从 100 人减少到 55 人,这带来了其他好处。较小的团队意味着较少的复杂性、更好的沟通、整体效率更高,以及组织中的信噪比更好。尽管我不愿意承认,55 人的组织几乎肯定会优于 100 人的组织。

(注:是的,这个模型有缺陷,我知道你有评论。我暂时保持这个模型的简单,不讨论关键人物依赖、缺席、替代、非工作时间支持等问题。我们会在其他文章中再次探讨这些问题)

场景 3:零和游戏:维持预算

希望最现实和最可能的结果是,我们的虚拟公司相对成功,没有需要做出痛苦的财务决策。对于这个公司来说,预算应该保持一致,并且有机会在不增加成本的情况下增加我们承诺的成果数量。

在这个模型中,产品和技术领导者查看整体预算,并基于新的 1:1 比例意识到,他们可以保持组织中的人员数量不变(100 名全职员工),但现在可以将致力于有价值成果的团队数量增加到 20 个。

从 11 个团队增加到 20 个需要我们招聘 9 个新的产品角色(每个团队一个),并且还需要增加其他角色以支持这些新团队。“其他团队”角色(如设计和 QA)从 22 人增加到 40 人,而我们的非团队管理和共享服务角色从 11 人增加到 20 人。

可悲的是,产品和其他团队的增长必须在不增加任何成本的情况下进行,这种成本节省来自于工程角色的减少。虽然不如其他情况严重,但这个情境要求我们从组织中裁减 35 个工程角色,这一减少依赖于开发人员生产力的提升。

这种结构性变化对组织仍然会相当震撼,但远不及其他两种情境。这保持了整体团队规模和预算的一致性,但组成却有很大不同。

在最简单的层面上,我们在这些情境中提出的问题是“生成型 AI 工具会减少五个开发人员还是增加五个产品经理?”。非常重要的一点是,1:1 的比例纯属假设,可能是我们能考虑的最极端的情况。

更可能的是,比例会基于操作约束(如覆盖缺席)和当前关于生产力提升的研究(完成任务时间减少约 55%),更加微妙地减少到 3:1 或 2:1。

尽管如此,作为一个思维实验,我们现在有一些最佳和最差的案例来框架我们的思考。

在第五部分中,你可以阅读到:

  1. 编码自动化工具对不同规模企业的潜在影响,从风险投资支持的初创企业到可能面临的重大决策,大型公司的工程团队也面临挑战。

  2. 面对变革性生成型 AI 工具,外包开发公司的迫切担忧,因为招聘、成本和价值主张的动态发生了变化

  3. 在 AI 时代人类工程师的不可替代价值

  4. 号召读者参与并开始测试假设

等待第五部分发布

本系列其他文章:

附言。如果你喜欢这些关于团队的文章,可以看看我的Teamcraft 播客,在这里我和我的共同主持人安德鲁·麦克拉伦(Andrew Maclaren)与嘉宾讨论什么使团队运作成功。

如果工程师开始使用 AI 编码工具,我们的产品团队会发生什么?

原文:towardsdatascience.com/if-engineers-start-to-use-ai-coding-tools-what-happens-to-our-product-teams-acd55fb273dd

作者使用 Midjourney 制作的图片

AI 对产品工程团队的影响——第三部分

Mark RidleyTowards Data Science Mark Ridley

·发布于 Towards Data Science ·10 min read·2023 年 7 月 27 日

--

这是一个六部分系列的第三部分,调查像 Github Copilot、ChatGPT 和 Amazon CodeWhisperer 这样的生成性 AI 生产力工具可能如何影响整个产品工程团队的结构。

第二部分 中,我们探讨了:

  1. 生成性 AI 工具如 ChatGPT 如何重新定义产品工程团队处理编码的方式——从生成用户故事到实际编写代码。

  2. 诸如编写测试和文档等开发者通常认为繁琐的任务,现在可以由 AI 轻松高效地处理,从而使整个编码过程更加流畅。

  3. 测试的重要性以及经过深思熟虑的测试设计可能会启动的 prompt-engineered 应用的潜在未来。

  4. 对未来的愿景,其中生成性 AI 工具重塑了产品工程团队内的角色,以及对工程师和领导者的深远影响。

生成性 AI 工具将如何改变团队结构?

我过去几个月一直沉迷的想法是,生成性 AI 工具对产品工程团队的影响将导致产品角色与技术角色的比例发生根本性变化。在本系列的第一部分中,我反思了许多产品团队中工程师与产品经理的常见比例是五名工程师对一名产品经理。

现在,当我感到特别大胆或特别宿命论时,这一假设变成了:

“当前的产品工程团队需要五个工程师对应一个产品经理。下一代产品工程团队只需要一个高级工程师对应一个产品经理”

这是我想要验证的内容。

在本系列的前几篇文章中,我们讨论了这些开发者和生产力工具的个体影响,但现在我想将你的视线拉回到更战略的角度——增强的开发者生产力对团队,甚至整个组织的影响。

为了思维实验的目的,假设我们接受这样的观点:像 Github Copilot 这样的生成性 AI 工具可以提高单个开发者约 50%的生产力。还假设在当前的状态下(如我们在第一部分中所述)一个产品经理对应五个工程师是一个合理的比例。如果这两个条件成立,我们可以推断长期以来的 5:1 比例将需要改变。

我相信,自 2022 年 Copilot 发布以来,我们所见的进展将会继续,特别是在风险投资资金流向生成性 AI 初创公司而不是元宇宙和 Web3.0 的情况下。

一大批新工具每天开始在ProductHunt上出现,如BifrostLocofy(这两者都将 Figma 设计转化为可工作的 React 代码,类似于Amazon 的 Amplify Studio),有趣的是看看这些工具能多快以及与谁一起获得 traction。

开发者在大型团队中不太可能推动这种新型代码自动化工具的采用,因为对代码质量的历史疑虑和企业风险规避。更可能的是,初创公司创始人、以创新为导向的产品经理和较小的营销团队将成为早期采纳者,被自动生成的网站和应用的较低成本和更快上市速度所吸引。

我能听到仍在阅读的工程师发出叹息,“当然 我的 应用程序太独特, 我的 问题太复杂,这些基本的、像乐高积木一样的应用程序无法取代我的工作”。但正如 Kelsey Hightower(Kubernetes 的原始贡献者之一,曾是 Google 的杰出工程师)所说的那样,

“尽可能长时间坚持使用单调的架构,并将大部分时间和资源用于构建客户愿意支付的东西。”

如果你仍然认为你的应用是独特而美丽的,这条跟进推文来自 Derek Neighbours,清楚地总结了那句“无聊架构”评论应该指向的对象:几乎每个人

需要非无聊架构的公司数量几乎接近零。没有什么比一个拥有 12 个客户的初创公司从 Netflix 或 Google 复制扩展模式更滑稽的了。

回到我的假设;大多数工程师将变得更有生产力,大多数架构不应该复杂,生成 AI 工具将比 2023 年状态有显著提升。如果这些都成立,那么工程师的工作将从基础的管道和维护转向更严肃的问题,如架构、解决方案设计、性能、安全以及管理日益复杂的内部生态系统。

我们可能会开始看到 测试驱动解决方案设计,其中工程师的角色是以一种方式指定应用程序的功能,使得生成 AI 工具可以被提示生成测试和一个可工作的应用程序。在设计阶段创建的测试将继续证明应用程序满足公司和客户的需求,并自我记录应用程序。

代码生产力工具如何承担更多显而易见的任务之外的工作

我们已经讨论了生成 AI 工具对新产品和功能开发的影响,但对大多数工程师来说,这只占据了他们日常工作的一个部分。对于很多工程师来说,“业务照常”中的维护和操作任务占据了他们的大部分工作时间。重构、修复错误、小改动请求和小版本升级可能消耗大量宝贵的开发时间。当考虑到这些平凡的任务时,我们讨论的工具也同样强大,就像它们在令人兴奋的新产品开发工作中一样。

几年前,我和一个团队一起处理一个需要升级的旧数据库。团队现在非常现代,采用微服务(是的,这现在是一个形容词),而且没有人有古老 SQL 或过时的古老数据库引擎的经验。这种情况并不罕见,经常会引发小型紧急情况,因为团队急于找到具备神秘知识的人。原本应该是相对简单的任务突然颠覆了所有精心策划的路线图。

团队经常面临这些问题——无论是重写旧的和被遗忘的东西,还是需要从一个主要版本升级到另一个版本——他们必须仔细查阅过时且通常文档不佳的代码,以学习新的东西(或重新学习令人沮丧的旧事物)。

但是这些代码助手工具并不在乎它们接收到什么语言(是的,我确实让 ChatGPT 写过brainf*ck和机器码以图乐趣)。这些 LLM 已经接受了所有文档,数十亿行代码示例和所有关于Stack Overflow的争论(可能)。对于 LLM 来说,重写即使是未记录的代码也并不是什么大事。更好的是,可以很容易地要求这些工具为旧代码和新代码编写测试。

为了确认我没有产生幻觉这个任务是可能的,我去找了一些互联网上的随机旧 Perl 代码,然后将其输入到 GPT4 中。在让它解释代码功能后,我接着询问可以用什么语言替换 Perl。它建议使用 Python,并立即生成了相应的代码。经过一次提示,我们得到了一些测试来检查代码是否正常运行。

作为额外奖励,我想看看我们是否可以将其移植到 Rust,这是 Stack Overflow 过去几年‘最受喜爱的语言’之一。正如你可能猜到的那样,它确实没有遇到什么问题。

让 ChatGPT 随机重构我在互联网上找到的东西

如果像文档和测试创建这样的简单但耗时的任务,以及版本升级和重构等常规工程任务可以由 AI 工具来完成,那么结果会是什么?几乎可以肯定的是,这会改变那 5:1 的比例。

网络和移动应用团队的奇怪世界

在那些选择同时构建网站和移动应用程序的组织中,我们有时会遇到一种稍微奇怪的功能失调。虽然网站和移动版本的功能和客户价值通常是相同的,但我们发现自己不得不将构建不同应用程序的团队分开。即使在那些努力围绕价值流对齐团队的组织中,专业知识和系统的分裂几乎总是导致了专门团队的形成。

这对于产品是重新设计的移动设备网页界面的公司(即所谓的‘响应式设计’)不是问题,但即使是那些选择构建‘混合’移动应用的公司,这种情况也可能成为问题,其中应用程序在像 React Native 这样的网页相邻框架中开发。至少在这些混合应用中,代码可以有一定的相似性,发布移动和网页产品所需的技能也可以广泛转移。

原生移动应用——那些专门为 Android 和 iOS 构建的应用——可能会进一步复杂化问题。对于那些认为原生应用很重要的企业,且希望 Android 和 iPhone 用户获得相同优质体验的企业,我们通常需要额外的技能要求,需要具有 Java 或 Kotlin(针对 Android)或 Swift/Objective-C(针对 iOS)专业知识的工程师。

选择交付原生移动应用以及网页界面的公司面临大量重复的努力和相应的成本增加。

正如我们所见,在这种情况下,我们的生成式 AI 编码助手可以利用它们的语言无关性,创建多种语言的设计良好、测试充分的应用程序。这可能的影响是,我们可能不再需要因技能集的不同而拆分移动和应用团队,而仅仅是在网页和移动应用具有显著不同功能和特性时才需要分开。在一些公司,这可能会大大简化流程,并节省多达一半的工程预算。

对初级和中级开发者(尤其是在初创公司和小型企业中)有什么影响?

在撰写本文之前,我与许多从事技术和产品的人讨论了这些编码工具可能产生的影响。几乎一致地,在我描述了这些工具可能的影响后,都会有人指责地问我“那么所有的初级开发者会发生什么?”

我曾与之共事的每位优秀工程经理或技术领导者都非常关注培养初级人才。优秀的工程师编写优秀的应用程序,而伟大的工程师则教导他人。因此,我得到的对这个讨论的最初反应之一就是对下一代的担忧。

说实话,我在短期内感到担忧。正如我们将在第四部分中看到的那样,一种潜在的影响是公司决定削减预算和团队规模,尽管承包商和外包开发团队可能会首先被裁减,但中级和初级开发者将会是下一个被裁减的对象。

我担心这些变化可能会导致我们技术人才的代际“剥皮”,即整个初级开发人员群体无法在团队中找到工作。构建应用程序所需的技术技能,至少在早期采用公司中,是资深工程师所具备的——包括解决方案和架构设计、安全性以及奇特的边缘案例。即便是技能娴熟的初级和中级工程师,也因为其角色定义和从业时间的限制,无法获得这种经验。

如果生成式 AI 工具在完成基本工作,那么最需要的就是资深开发人员和解决方案架构师的技能。

然而,尽管对初级开发人员的影响确实令人担忧,但并非全然黑暗。我最近在尝试学习 React 和 Django 时使用 Copilot,发现我的体验与职业生涯初期时截然不同。能够实时讨论一段代码的作用或错误所在,彻底改变了我的学习体验。我希望在我学习编码时能够使用 ChatGPT。

但尽管编写代码显然是开发人员的一项重要技能,但这并不是我们团队真正需要的那种难以言喻的品质。最好的工程师能够相对轻松地从一种语言转到另一种语言,是有原因的;优秀的工程师不是被教会编程的,而是被教会如何思考的。我仍然相信,创造性思维和系统思维能力将继续成为工程师为团队带来的标准。

我担心这些变化对初级开发人员的影响,但我也抱有希望。我尚未看到一个不需要技能熟练的工程师与技能熟练的设计师和产品经理共同工作来构建优秀产品的未来。即使每个团队的开发人员数量减少,也许我们会更倾向于类似某些传统手工业的模式,即初级人员在较长时间内更专门地向一位资深导师学习。

德语词汇“Wanderjahre”指的是一种可以追溯到中世纪的实践,年轻的工匠在学徒期后会从一个地方迁移到另一个地方。这段旅程的一个重要组成部分是体验不同的工作坊和文化,发展技能和思维方式,作为最终交付他们的Meisterstück(杰作)的追求的一部分。

或许这种古老的传授工程“精通”方法将更适合我们下一代产品团队。

在第四部分中,您可以阅读到:

  1. 生成式 AI 在经济衰退和风险投资资金减少的情况下,对组织造成的重大转变。

  2. 一个思想实验,考察三种情境:潜在增长、削减成本以及在引入生成式 AI 生产力工具的情况下维持现有预算。

  3. 假设的后果和挑战,比如团队组成的剧烈变化,以及这些变化可能对产品经理和工程师等角色意味着什么。

  4. 对这些工具对生产力和团队动态的真实影响的反思,为持续的行业讨论提供了基础。

阅读第四部分,当它发布时

本系列的其他文章:

附注:如果你喜欢这些关于团队的文章,可以查看我的Teamcraft 播客,在这里我和我的共同主持人 Andrew Maclaren 与嘉宾讨论什么使团队有效。

如果口头和书面交流使人类发展了智力……那么语言模型怎么样?

原文:towardsdatascience.com/if-oral-and-written-communication-made-humans-develop-intelligence-whats-up-with-language-models-b65ae22ac8e0

文章

我们是否也只是经过更好训练的随机鹦鹉?AI 语言模型是否在追随人类智能的足迹?这是一个科学与虚构边界的讨论。

LucianoSphere (Luciano Abriata, PhD)Towards Data Science LucianoSphere (Luciano Abriata, PhD)

·发布于Towards Data Science ·阅读时间 12 分钟·2023 年 6 月 30 日

--

图片由Priscilla Du Preez拍摄,来源于Unsplash

人类智能以其非凡的认知能力在其他物种中独树一帜。这种智力优越性的催化剂可以追溯到语言和文字的出现,它们促进了知识的交流和积累、协作学习和深思熟虑。从这个类比来看,AI 语言模型似乎也在踏上类似的旅程,利用沟通的力量推动其自身形式的“智能”发展。本文将探讨 AI 语言模型如何发展以复制并超越人类认知的非凡成就。

请注意,本文只是我的思考,带有挑衅的语气,引用了专家的研究,但更多是个人观点。

欢迎在评论区讨论,尊重不同意见,并尽可能提供适当的参考。

言语和写作使人类能够进行复杂的推理和逻辑思考。许多计算机科学家正在尝试创建可以通过处理大量数据、进行快速计算并设计所谓的“思维链”来执行复杂推理任务的 AI 模型,这使它们不仅能够分析模式,还能推断因果关系,从而生成逻辑结论,使其能够解决复杂的问题。

[## 写作作为思维工具

写作过程可以成为一个强大的工具,帮助你探索、表达和完善想法。

curiosityneverkilledthewriter.com](https://curiosityneverkilledthewriter.com/writing-as-a-tool-for-thinking-d295ca8fba7a?source=post_page-----b65ae22ac8e0--------------------------------) [## 文学思维:思想和语言的起源

亚马逊网:文学思维:思想和语言的起源:9780195126679:马克·特纳:书籍

www.amazon.com](https://www.amazon.com/Literary-Mind-Origins-Thought-Language/dp/019512667X?source=post_page-----b65ae22ac8e0--------------------------------) [## 语言模型通过思维链进行推理

近年来,扩大语言模型的规模被证明是提高性能的可靠方法…

ai.googleblog.com](https://ai.googleblog.com/2022/05/language-models-perform-reasoning-via.html?source=post_page-----b65ae22ac8e0--------------------------------)

正如言语和写作使人类能够高效地分享和积累知识,从而创造我们的文化,AI 语言模型利用庞大的数据集从各种来源学习,并迅速扩展其知识库。这些模型可以通过访问大量信息库来提供主题见解、回答问题,并生成创造性解决方案——当然,也可能误导或创造虚假材料,但这不是本讨论的重点,无论如何,我们人类也会创造这种内容,即使它是“文化”的一部分。

一些科学家认为,语言和书写的进化不仅帮助人类进一步发展智力,同时也促进了文化的创造。那么,语言模型的进化是否最终能够培养出“真正”的智能呢?无论是在两年内还是一个世纪后?毕竟,我们自身的智力可能来源于一种极其复杂的“小思维链”组合,这些链条最终产生出我们特别“聪明”的生动印象,而实际上我们在与现实的联系上只是稍微优于普通动物——而现实可能甚至不具备客观性!换句话说,如果 AI 模型是“随机鹦鹉”,我们是否也只是随机鹦鹉,只是我们(目前)在数量级上更高,并且通过多种感官将来自周围世界的信息输入到我们的神经网络中?

如果 AI 模型是“随机鹦鹉”,那么我们是否也可以是随机鹦鹉,只不过我们的能力高出几个数量级,并且通过多种感官将来自周围世界的信息输入到我们的神经网络中?

随时间推移精炼和打磨思想

通过迭代的精炼,人类逐渐表达复杂的思想并增强观点。AI 模型可以通过根据用户反馈不断调整其回应,走上类似的道路。通过引入强化学习技术,AI 模型像人类通过反馈和迭代来打磨思想一样,迭代其表现。例如,ChatGPT 本身就是通过强化学习训练的,这种方法由人类反馈辅助:

[## 什么是 ChatGPT? | OpenAI 帮助中心

常见问题关于 ChatGPT

help.openai.com](https://help.openai.com/en/articles/6783457-what-is-chatgpt?source=post_page-----b65ae22ac8e0--------------------------------)

关于思想随时间推移的打磨和演变,写作在人类中具有特别的作用,因为它作为外部记忆大幅扩展了我们存储和检索信息的能力。在聊天过程中,AI 语言模型(如目前运行的状态)可以通过考虑以前回答和问题的上下文信息来模拟这一过程,充当一种扩展记忆。目前,这种记忆是暂时的,新的对话开始时会消失,但如果有一天模型能够固有地回忆起以前的对话,它们可能会开始基于先前的知识进行构建,提供更连贯和个性化的回应。(或者如果它们在之前的互动中学到了不良信息,也可能会提供更多错误的回应、虚假新闻和不当内容……)

迭代学习推动了人类创新,基于现有知识推动进步。AI 模型同样可以通过生成各种可能性并评估其结果来进行迭代学习。注意,当前的 AI 语言模型已经具有这种内部“评分”,对替代答案进行排名,这一点我确实在这里讨论过(技术文章在前!):

探索令牌概率作为过滤 GPT-3 答案的手段

为了构建更好的 GPT-3 驱动的聊天机器人

towardsdatascience.com

促进集体 AI 创建“AI 文化”

一个重要的点是,人类文化和人类学习本身并不限于个人经历;相反,它们依赖于社区的积累智慧。如果有一天 AI 语言模型能够进行协作学习和数据交换,那么通过在模型之间共享信息和见解,它们可以共同受益于彼此的知识,加速整体智能的发展。特别是如果能够浏览互联网,虽然它们已经开始这样做。但当然,它们必须以某种方式存储每次会话的输出,就像我们记住我们所学到的知识并每天重复一样,然后我们将其写入书籍或互联网以进行传播。

在这一点上,让我“幻想”一下,假设聊天机器人可以开始相互通信并存储和使用它们与人类的先前互动以成长——无论这种成长可能带来什么好坏的方向……我们在这里只是进行一个思维实验,这将延续到接下来的部分。

我们在这里进行一个思维实验,这将延续到接下来的部分。

在人类中,集体智慧来源于合作,使他们能够共同解决复杂的问题。相互连接的 AI 语言模型可能通过提供一个共享平台来促进这种集体智慧,从而使人类和机器能够互动和交流知识。这种合作培养了混合智慧,人类和 AI 协同工作以克服挑战,开辟新领域。但如果如本场景所假设的那样互联,那么不同的 AI 模型也可能开始彼此交换信息。那么会怎样呢?

AI 语言模型作为真正的思维机器?

对 AI 语言模型未来演变成类似人脑的思维机器进行推测是一个引人入胜的话题。全球范围内,心理学家、哲学家、计算机科学家和工程师团队认真研究这一问题,从不同方面解决相关问题:

数据与复杂性 正如人类通过接触大量的言语和写作来发展其智能一样,AI 语言模型也需要大量数据来有效学习和泛化。未来,AI 模型可能会从更大的数据集受益,这些数据集涵盖了各种信息来源,使其能够获取广泛的知识和类似于人类的上下文。直接连接互联网并从中学习,甚至仅仅是浏览信息,也将使语言模型达到新的水平。

多模态学习 人类通过多个感官感知和理解世界,将视觉、听觉和触觉输入整合以形成连贯的理解。AI 语言模型可能会发展出多模态学习,整合文本、图像、视频和音频,使其能够通过各种方式理解和交流。这种整合可以帮助它们获得类似于人类的更全面的世界理解。这些“通用”模型已经成为活跃的研究主题,甚至像 Deepmind 这样的主要公司也在研究:

## Gato,Deepmind 最新的进展。迈向真正的 AI?

Gato 可以玩游戏、生成文本、处理图像以及控制机器人手臂。而且它的体积并不大。这是真正的 AI……

[towardsdatascience.com

上下文理解

理解上下文对人类智能至关重要。AI 模型在上下文理解方面取得了显著进展,实际上能够相当好地维持连贯、有趣的对话。但未来的进步可能使它们更准确地把握细微的差别、文化参考和社会动态。增强的上下文理解可以使 AI 模型生成符合人类交流意图和情感细微差别的回应。

推理与创造力

人类智能包括推理、问题解决和创造性思维。未来的 AI 模型可能会发展出更高级的推理能力,使其能够进行逻辑推断、类比推理,甚至抽象推理。创造性思维,包括生成新颖的想法和解决方案,可以通过开发能够进行类比思考和探索广泛解决方案空间的 AI 模型来培养。

请注意,正如艺术家们自己所承认的,几乎没有什么可以被称为真正创新的艺术。相反,新艺术、概念和想法从一种基础上涌现出来,这种基础,无论是有意识的还是无意识的,最终赋予了我们新创作的形式。同样,我们也不应期望 AI 模型从零开始真正创造出全新的东西!我们发现它们的创作中至少有一些回忆,甚至是实质性的相似之处,这很正常,就像人类的创作一样!

正如艺术家们自己承认的,几乎没有什么可以被称为真正创新的——我们总是基于先前的作品和想法进行创作,无论是有意识的还是未曾意识到的。

情感智能

情感智能在人际互动中扮演着至关重要的角色。未来的 AI 模型可能会被设计成理解和响应人类情感,融入情感分析、共情对话生成,并能够识别和适应情感提示。这些发展可能使 AI 能够进行更有意义和情感敏感的对话,促进与人类之间的更深层次的联系。

请注意,最现代的增强现实和虚拟现实头戴设备已经能够读取面部表情,并将其反映在用户的虚拟形象中。一旦面部表情被识别,训练一个简单的神经网络将其转化为情感状态并不会太困难。

[## 面部表情可以用于虚拟现实中的互动

新技术允许人们仅通过面部表情与虚拟环境进行互动。

www.snexplores.org

持续学习

人类智能不是固定的,而是通过学习和适应不断进化的。即使我们的品味也会随着时间的推移发生变化!

AI 模型可以融入持续学习技术,使其能够从新经验中学习,适应变化的环境,并随着时间的推移不断完善知识。这种持续学习的特点将使 AI 模型变得更加灵活,能够获取新技能和知识,类似于人类如何不断学习和成长。

自我意识与意识

这可能是最棘手的问题之一,尤其是因为自我意识和意识的概念在人类中甚至还没有完全理解,更不用说在人工系统中复制它们了。

然而,模拟某种形式的自我意识并不难,使人类用户误以为模型能够感知自己的存在、思想和经验。除了令人不安的目标之外,这种模拟在心理学、教育等应用软件的开发中可能会有用。

回到演讲、写作和思考

演讲和写作被广泛认为是人类智力发展的关键因素,将我们与其他动物区分开来。这些沟通媒介在增强人类认知和促进跨代知识传递方面发挥了关键作用,随着文化的建立,我们不断进步。

在这个类比的基础上,我们可以探索 AI 语言模型如何类似地利用沟通的力量来提升它们的“智能”,无论这是否是真正的智能。

知识的交流与积累

演讲和写作使人类能够比其他任何物种更高效地交换和积累知识。同样,AI 语言模型可以访问大量的数据和信息,使它们能够从各种来源学习并迅速积累知识。通过利用这一庞大的知识库,AI 模型可以提供见解、回答问题,并生成非常有创意的解决方案。目前,这些好处是以可能的错误信息和有害内容的生成为代价的,但这在未来可能会有所改善。

协作学习与文化传递

人类不仅从个人经验中学习,也从社区中积累的知识和智慧中学习。同样,AI 语言模型也可以开发协作学习和文化传递的机制。通过在模型之间共享信息和见解,它们可以集体受益于彼此的知识,从而加速整体智能的发展。当然,前提是它们能够相互沟通,尽管最终结果尚不明确,因为它们的训练数据集可能已经大部分重叠。

想法的精炼与打磨

通过演讲和写作,人类能够精炼思想、表达复杂的观点,并随着时间的推移进行迭代。AI 模型也可以通过不断根据用户反馈微调其响应,参与类似的过程。通过引入强化学习技术,AI 模型可以像人类通过反馈和迭代精炼其想法一样,逐步提升其性能。

增强记忆与回忆

书写使我们能够将思想和记忆外化,扩展我们的存储和检索信息的能力。如果 AI 语言模型能够保留先前互动的上下文信息,它们可能拥有一种扩展的记忆,有效地帮助它们更好地适应不同的用户档案。通过回顾先前的对话,模型可以保持上下文,基于先前的知识提供更连贯、更个性化的响应。

促进复杂推理

正如我已经讨论过几次的,语言和文字使人类能够进行复杂推理和逻辑思考。AI 模型也可以被设计来通过“思维链”执行复杂的推理任务,给人一种它们在分析模式、推断因果关系和生成逻辑结论的印象。尽管这些可能不是“自然智能”中的“真实”思考步骤,但在实践中,它们确实使 AI 能够处理复杂问题(甚至解释如何解决这些问题)。

迭代学习与创新

通过提供一种集体记忆,文字的发展使人类能够进行迭代学习,基于现有知识推动创新。AI 模型通过强化学习和生成过程,也能进行类似的迭代学习。通过生成各种可能性并评估其结果,AI 模型可以探索新颖的解决方案,促进创新,推动其“智能”的边界。

促进集体智慧

人类拥有集体智慧,利用基于语言的交流和书写阅读的协作力量共同解决复杂问题。AI 语言模型可以通过提供一个共享平台来促进集体智慧,使人类和机器互动并交换知识。这种合作最终可以导致混合智能的出现,在这种智能中,人类和 AI 共同解决挑战并开辟新的领域,或更高级的 AI 形式。

一些最后的话

在这里,我探讨了语言与文字作为人类智慧催化剂的类比,以及人工智能如何从类似今天存在的语言模型中发展。通过利用交流、提炼、记忆、推理、迭代学习和集体智慧的力量,AI 模型可能在朝着卓越的“认知”发展。是的,我知道我的说法很挑衅,但请记住讨论中的一个核心观点:我们人类可能和大型语言模型一样是随机的鹦鹉,只是我们的“大”要大得多,而且我们还具备与外部世界互动的方式,创造出改变我们语言模型输出的现实。

当然,随着人工智能的发展,我们必须应对独特的挑战和伦理考量,以确保“人工思维”的发展与人类价值观保持一致。无论如何,你可以确信,类似于人脑的思维机器的发展,甚至只是尝试达到这一目标的过程,都会对我们的世界产生某种变革性的影响,无论好坏。

相关材料和笔记

(除了文中所有链接之外)

随机鹦鹉

机器学习 / 人工智能领域,“随机鹦鹉”指的是大型语言模型可能在生成令人信服的语言方面表现优异,但它们实际上并不“理解”它们处理的语言的含义。这个术语最初是在这里提出的:

[## 随机鹦鹉的危险 | 2021 年 ACM 公平性会议论文集……

过去三年的自然语言处理(NLP)工作特点是开发和部署越来越大型的语言……

dl.acm.org

关于微软和谷歌的语言模型“思考”

[## 人工通用智能的火花:GPT-4 的早期实验

人工智能(AI)研究人员一直在开发和完善展现……的大型语言模型(LLMs)

arxiv.org [## 思维链提示引发大型语言模型的推理

我们探讨了生成思维链——一系列中间推理步骤——如何显著提升……

arxiv.org

www.lucianoabriata.com 我写作和拍摄的内容涵盖了我广泛兴趣领域的所有内容:自然、科学、技术、编程等。

在这里小额捐赠成为 Medium 会员 以访问所有故事(这对你没有成本,我可以获得少量收入)。 订阅以通过电子邮件获取我的新故事 咨询小型工作 请访问我的 服务页面。你可以 在这里联系我

如果你把生活视作游戏,你最好知道如何玩

原文:towardsdatascience.com/if-you-see-life-as-a-game-you-better-know-how-to-play-it-f7aaa365caf1

游戏理论如何帮助你做出日常决策

Diego ManfreTowards Data Science Diego Manfre

·发表在 Towards Data Science ·阅读时间 13 分钟·2023 年 12 月 2 日

--

图片由作者使用 Midjourney 制作

在一个未知的星系里,你和你的朋友被一个神秘的外星生物组织拘留。他们承诺如果你们能在他们的游戏中击败他们,就会放你们离开。你们坐在一个宇宙桌子旁,外星人分发包含黑色和白色岩石的袋子。游戏开始时,每个玩家会选择一块岩石并把它放在桌子上。获胜者是选择了不同颜色岩石的人。这意味着,选择黑色而其他两人选择白色的人,或者选择白色而其他两人选择黑色的人。每次有人获胜,他们会给你一枚硬币堆放在你们桌子的一侧。你们将玩这个游戏一百万次,最终的赢家是获得更多硬币的人。除了游戏规则,你唯一知道的就是外星人每次以 50%的概率选择黑色。游戏开始前几秒,你的伙伴低声告诉你应遵循的策略。经过一百万场游戏,最终得分宣布你们俩都是赢家。这是纯粹的运气吗?你伙伴的策略有多重要?

介绍

你最终遇到上述情况的可能性极低。然而,外星人提出的游戏可以推断到更现实的情况。我们的生活充满了多种互动,在这些互动中我们要么本能地,要么故意地做出决定。这些决定中的许多依赖于他人的决定,许多则取决于我们无法控制的事件。在任何情况下,我们的回应很少是随机的,而更多是我们理性分析的结果。这意味着在我们面对的每一次互动中,我们根据带来更大利益的因素来做出决定。这也意味着我们可以根据不同的互动采取不同的策略。本文讨论了我们采取的策略、获得的回报,以及 游戏理论

游戏理论 101

你可能已经看过电影 美丽心灵,讲述了著名数学家 约翰·纳什,或者你听说过游戏理论家获得的多个诺贝尔奖。游戏理论是一个有趣的数学领域,具有许多现实生活中的应用。在游戏理论中,不同代理之间的战略互动通过数学建模,然后利用这些模型生成预测或更好地理解特定过程。根据互动类型或玩家数量,数学模型可以非常复杂,也可以是一组简单的方程式。不管是哪种情况,对游戏理论的短暂了解不仅有趣,而且有用。

在回到外星人桌子之前,让我们分析一个更简单的游戏。假设是星期五晚上,你正在决定吃什么。一组朋友想要比萨,而另一组朋友想要汉堡。比萨需要 30 分钟到达,而汉堡在 15 分钟内送达。这些是场景:

  • 两个组都点了比萨。在这种情况下,有一个组对这个选择不会感到满意。

  • 两个组都点了汉堡。在这种情况下,有一个组对这个选择不会感到满意。

  • 一个组点了比萨,另一个组点了汉堡。在这种情况下,如果两个组都等待所有食物到达,一个组将不得不吃冷掉的食物。

假设 A 组想要比萨,而 B 组想要汉堡。我们可以将这种情况写成如下矩阵:

表 1. 比萨/汉堡示例的回报矩阵。数字表示每个组对其选择的满意程度。(图形由作者制作)

每种情景中的数字称为收益,代表每种选择组合的结果。第一个数字是 A 组的收益,而第二个数字是 B 组的收益。指定收益是博弈论分析中非常重要的一部分,这并不总是简单明了的。收益可以代表多种事物。在这种情况下,我们任意指定一个数字,表示每个组对其选择的满意程度。这就像是事先询问每个组如果他们选择汉堡包或披萨,他们会有多满意,满意度范围从 0 到 5。如你所见,不可能让两个组都达到最高满意度,因为在 A 组订披萨而 B 组订汉堡包的情况下,披萨会迟到,B 组将吃到冷汉堡包。然而,这还不如没有吃到汉堡包那么糟糕!A 组订披萨而 B 组订汉堡包的组合代表了一个两组都不想偏离的解决方案。这被称为Nash 均衡

在纳什均衡中,任何人都无法通过改变自己的策略来增加预期收益,而其他玩家的策略保持不变。在前面的例子中,解决方案(5,3)代表了一个纳什均衡,因为 A 组不能增加其收益(它已经是最大值),B 组也不能增加其收益,改变策略只会减少收益。另一个经典的例子用于解释纳什均衡是著名的囚徒困境

在囚徒困境中,两名囚犯正被警察审讯,他们可以选择坦白或保持沉默。每种策略对应的收益如表 2 所示。在这种情况下,收益是他们将要在监狱中度过的年数。这意味着数字越小,收益越好。由于囚犯不知道对方会做什么,均衡状态对应的是两个囚犯都坦白的情况。这里的关键是:乍一看,收益表明对于两个囚犯作为一个团队来说,最好的策略是保持沉默。然而,他们不知道他们的伙伴会做什么。即使他们知道伙伴会保持沉默,从个人的角度来看,最好的策略是坦白,因为坦白会将监禁时间从一年减少到零。这意味着囚徒困境只有一个纳什均衡,即两个囚犯都坦白的策略。

表 2. 囚徒困境的收益矩阵。数字表示监禁的年数。(图形由作者制作)

在回到外星人及其游戏之前,重要的是要注意到,有些互动可能会有多个纳什均衡,而其他互动可能没有纳什均衡如果我们只考虑 纯策略。纯策略对应于一个单一且具体的计划。这意味着保持沉默或认罪,或者购买披萨或购买汉堡包。另一方面,混合策略则为每个计划分配一个概率。我们可以说,囚犯有 75%的机会保持沉默,因此有 25%的机会认罪。在任何有限的游戏中,我们可以确定至少存在一个纳什均衡。这个均衡可能是纯策略或混合策略形式,或它们的组合。既然我们知道了这些,现在是时候与外星人一起坐回宇宙桌前了。

回到宇宙游戏

图 1 解释了收益和游戏的策略。这个游戏是经典游戏 匹配硬币 的修改版。共有 8 种可能的情景,其中两种情况下没有人获胜。这些情况是三名玩家选择相同颜色的情况。在其余的情景中,某人获胜要么是当其他人选择白色时选择黑色,要么是当其他人选择黑色时选择白色。这个游戏没有纯策略纳什均衡,因为没有任何策略能使所有玩家满意。然而,它确实有一个混合策略纳什均衡。在解决初始问题之前,让我们首先计算这个均衡。

图 1. 宇宙游戏示例的情景和收益。(图形由作者制作)

要计算混合策略纳什均衡,我们首先为每个策略分配概率。图 2 展示了标识每个玩家玩黑棋或白棋的概率的字母。

图 2. 宇宙游戏中每个玩家的策略和概率。(图形由作者制作)

以外星人为例,对于外星人来说,找到一个不关心自己是玩黑棋还是白棋的策略,就等于找到概率 rq,使得无论玩黑棋还是白棋都能得到相同的结果。我们可以通过以下方程式来表达这一点:

如果我们对所有玩家进行相同的分析,我们将得到一组方程,告诉我们表示混合策略纳什均衡的概率。在这种情况下,p=q=r=0.5。另一种看待这种情况的方法是,如果三名玩家以 50%的概率选择石头的颜色,那么在无限次的游戏之后,他们将获得相同的支付。所有人的期望值是相同的。这是一个链接,你可以在其中模拟这个游戏,改变每个玩家的概率。

在本文开头介绍的问题的特定情况下,我们并不关心找到三名玩家之间的纳什均衡。相反,我们关心的是找到一种策略,使我们总是能战胜外星人。我们对他的了解至关重要。如果我们已经知道他将以 50%的概率选择黑色,那么我们可以让我们的朋友选择黑色,而我们选择白色,这将确保我们中的一个在每一局中获胜。如果我们无限次地玩这个游戏,那么外星人将最终得到零枚硬币,而我们将分配其余的硬币。表 3 显示了假设外星人以 50%概率选择黑色的情况下,仅两个玩家的支付矩阵。请注意,其中有两个纳什均衡,汤姆或迈克通过选择对立的策略获得 1 的支付。

表 3。假设外星人以 50%概率选择黑色的情况下,宇宙游戏中两个玩家的支付矩阵。(图形由作者制作)

毕竟,击败外星人在他自己的游戏中是可能的!知道他总是以 50%的概率选择黑色,这让你的朋友可以选择黑色,而你选择白色(或反之亦然),并赢得每一局。然而,如果我们对外星人的策略没有任何先前的信息,那么我们最好的选择是以 50%的概率选择黑色或白色。博弈论不仅在这种假设性的场景中有用,在许多其他更现实的场景中也同样适用。让我们回顾另一个例子。

保护计划的技巧

在节省了一整年并等待黑色星期五之后,你终于决定购买你一直想要的$1000 电视。在结账之前,收银员问你是否要再“多花 175 美元”购买一个保护计划(PP)。这个保护计划将覆盖你电视在接下来 4 年的相关费用。这是个好主意吗?

我们可以将这个问题分析为一个双人游戏:卖电视和保护计划的公司是一个玩家,而买家是另一个玩家。如果我们假设修理一台电视的平均费用是$500,那么支付矩阵将如下所示:

表 4. 保护计划示例的回报矩阵。数字表示成本/利润。(图表由作者制作)

在前面的矩阵中,第一个数字对应于公司的回报,第二个数字是买方的回报。数值计算如下:

买方添加保护计划:

如果电视故障:

  • 回报(买方):电视的成本 + 保护计划的成本:-1000–175 = -1175

  • 回报(公司):电视的收入 + 保护计划的收入 — 维修成本 = 1000+175–500=675

如果电视没有故障:

  • 回报(买方):电视的成本 + 保护计划的成本:-1000–175 = -1175

  • 回报(公司):电视的收入 + 保护计划的收入 = 1000+175 = 1175

买方不添加保护计划:

如果电视故障:

  • 回报(买方):电视的成本 + 维修成本:-1000–500 = -1500

  • 回报(公司):电视的收入 = 1000

如果电视没有故障:

  • 回报(买方):电视的成本:-1000

  • 回报(公司):电视的收入 = 1000

注意在买方不添加保护计划且电视没有故障的情况下存在纯策略纳什均衡。这是表 4\ 中的右下角框。这是完全有意义的,因为如果我们不添加保护计划,那么最好的情况就是电视正常工作而无需维修。另一方面,如果我们决定不购买保护计划,那么无论电视是否故障,卖方都会赚到相同的金额。

找到纯策略纳什均衡对于理解这个场景很重要,但它尚未回答我们最初的问题。为此,我们需要考虑电视在前四年内出现故障的概率。假设q表示这个概率。在这种情况下,买方考虑到这个概率的期望值是:

请注意我们在计算卖方添加保护计划的情况下和买方购买电视但不添加保护计划的情况下的期望值。在一种最坏的情况下,我们完全确定电视会故障(q=1),如果添加保护计划,买方将支付 $1175,如果不添加则支付 $1500。我们可以找到使得添加保护计划和不添加保护计划的期望值相等的概率,即q=0.35。图 3 显示了在添加保护计划和不添加保护计划的情况下,相对于q的期望值。这个图表告诉我们,如果电视故障的概率小于 0.35,那么最好是不添加保护计划。

图 3. 买方添加或放弃保护计划时,电视故障概率的成本。

请注意,0.35 的值是根据之前列出的成本计算得出的。实际上,在这种情况下,使得添加保护计划和不添加保护计划的期望值相等的q值仅取决于保护计划的成本和维修成本。

如果修理电视的费用增加,那么电视发生故障的概率上限,即更值得添加 PP 的概率,会下降。另一方面,增加 PP 的价格也会增加q。注意,当 PP 的成本接近修理成本时,无论电视发生故障的概率如何,购买 PP 的推荐程度都会降低。这是一个链接,你可以在这里计算在改变任何变量并运行多次迭代后,买方的预期成本。

我们还可以从卖方的角度分析这个问题。卖方可能会问:“知道电视在前 4 年内发生故障的概率是x,那么 PP 的价格应该是多少,以便我不在乎人们是否添加 PP?”在这种情况下,我们也可以计算添加 PP 或不添加 PP 的期望值,但这次是卖方的收益:

注意方程右侧如何对应于电视的价格。这意味着如果电视在 0.35 的概率下发生故障,卖方将具有相同的期望值($1000),无论买方决定如何。现在,电视在前 4 年内发生故障的概率是多少?回答这个问题可能很困难,因为每台电视都不同。然而,快速的谷歌搜索表明,这个值可能在 4%左右(0.04)。如果是这样,那么 PP 的成本,使得期望值相等的是 0.04 x 500 = $20。这意味着通过将 PP 设置为$175,卖方在买方添加 PP 时的期望值为$1155,而不添加时为$1000!相当划算!

之前的解释展示了卖方或公司如何使用博弈论来确定其服务的最佳价格,以便在多次销售后期望值是有利的。这是一个链接,你可以在这里计算通过改变任何变量并运行多次迭代后,卖方的期望值。

结论

从外星人遭遇等虚构场景到购物,我们的生活充满了不同后果的决策。这些后果不仅是我们决策的产物,也依赖于他人的决策,而这些决策又取决于其他人的决策。虽然我们无法控制他人做什么,但我们可以学习将每个问题视为一个包含玩家、策略和收益的游戏。这正是博弈论的意义所在,也正是它有如此多应用的原因。下次你面临决策时,记住这一点!这可能会节省一些钱或让你避免几年牢狱之灾!

不只是爬行动物:探索 Iguanas 工具包用于超越黑箱模型的 XAI

原文:towardsdatascience.com/iguanas-more-than-just-reptiles-exploring-the-iguanas-toolkit-for-xai-beyond-black-box-models-4330ad69029?source=collection_archive---------10-----------------------#2023-08-25

“AI 思维” 来源:作者使用Dall-E创建

平衡复杂性和透明性以实现有效决策

Vegard FlovikTowards Data Science Vegard Flovik

·

关注 发表在Towards Data Science ·14 分钟阅读·2023 年 8 月 25 日

--

随着越来越多的行业将机器学习纳入决策过程中,一个重要问题随之而来:我们如何信任那些我们无法理解其推理的模型,以及如何根据这些信息自信地做出高风险决策?

对于安全关键型重资产行业的应用,在这些领域中错误可能导致灾难性后果,透明性缺乏可能是采纳的主要障碍。这就是模型可解释性和解释性变得越来越重要的地方。

你可以将模型理解能力视为一个谱系:复杂的深度神经网络处于一端,而透明的基于规则的系统则位于另一端。在许多情况下,模型输出的可解释性与准确性同样重要。

可解释性与准确性。来源:作者创建

在这篇博客文章中,我们将探讨一种方法,该方法可以直接从数据中自动生成规则集,从而构建一个完全透明且可解释的决策支持系统。需要注意的是,并不是所有情况都能通过这种基本模型得到令人满意的解决。然而,任何建模工作开始于一个简单的基线模型都提供了几个关键优势:

  • 快速实现: 快速设置以启动基础模式

  • 比较参考: 用于评估更先进技术的基准

  • 人类可理解的见解: 基本的可解释模型提供有价值的人类可解释见解

对于阅读此帖的同行数据科学从业者:我承认这种方法与简单拟合决策树模型的相似性。然而,随着阅读的深入,你会发现这种方法被调整为模拟人类规则创建,这使其相比于典型决策树模型(通常在实践中可能会很困难)的输出更易于解释。

自动规则生成,如通过Iguanas 框架展示的,说明了我们如何直接从原始数据中提取有见解的(且可由人类解释的)规则。虽然这篇博客文章中的示例覆盖了一个特定的案例,但规则生成算法通常也可以适应其他分类挑战。

示例案例介绍:

为了说明自动规则生成的过程,我们以管道完整性检查的假设行业相关案例为例。确保管道的结构完整性是一项要求严格审查的任务,并且对于防止环境灾难至关重要。因此,我们的案例代表了一个模型可解释性和解释性至关重要的相关示例。

完整性检查。照片由Paul Teysen拍摄,来源于Unsplash

在这个案例研究中,我们首先定义一个数据集,模拟不同的管道属性。这些属性包括长度、直径、壁厚、焊缝数量、弯头数量和腐蚀级别。此外,我们还包括一个二进制的“违例”状态,表示是否根据特定属性组合发生了违例。实际上,我们的数据集代表了一个二分类问题,这对于大多数数据科学家来说应该是一个熟悉的案例。

我们的第一步是创建一个合成数据集,包含下列各种属性以及相应的违例状态(分类为“真”或“假”):

  • 管道长度:区间 1–20 米

  • 管道直径:区间 1–10 厘米

  • 测量的管壁厚度(例如,通过检查得出):区间 1–10 毫米

  • 焊缝数量:区间 0–10(所有管道由最大长度为 10 米的管段组成。如果总管道长度> 10 米,则必须与另一根管道连接/焊接,因此“焊缝数量”> 0)

  • 弯头数量:区间 0–5:管道的弯头数量,其中 0 表示正常的直管段。

  • 腐蚀:分类变量,包括“无”,“轻微”或“严重”(例如,通过检查得出)。

  • 违例:二进制变量,“真”或“假”,表示这些属性集合是否应导致违例。

然后我们可以生成我们的合成数据集,其中每一行表示一组管道属性及其相应的违例状态。属性在上述指定区间内定义,而“违例”状态则基于一套预定义的规则来确定,详细说明在下一部分中。

# Import neccesary packages/libraries:
from iguanas.rule_generation import RuleGeneratorDT, RuleGeneratorOpt
from iguanas.rule_optimisation import BayesianOptimiser
from iguanas.metrics.classification import FScore, Precision
from iguanas.metrics.pairwise import JaccardSimilarity
from iguanas.rules import Rules, ConvertProcessedConditionsToGeneral, ReturnMappings
from iguanas.correlation_reduction import AgglomerativeClusteringReducer
from iguanas.rule_selection import SimpleFilter, GreedyFilter, CorrelatedFilter
from iguanas.rbs import RBSPipeline, RBSOptimiser
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay, classification_report
from sklearn.ensemble import RandomForestClassifier
from category_encoders.one_hot import OneHotEncoder
import seaborn as sns
import pandas as pd
import numpy as np

# Create empty dataframe:
df = pd.DataFrame(columns=['length (m)',
                           'diameter (cm)',
                           'thickness (mm)',
                           'num welds', 
                           'num bends', 
                           'corrosion', 
                           'violation'])

# Total number of datapoints in generated dataset:
datapoints = 100000
np.random.seed(10)

# Min/Max length of pipe
min_len = 1
max_len = 20

# Min/Max diameter of pipe
min_dim = 1
max_dim = 10

# Min/Max measured wall thickness of pipe
min_thick = 1
max_thick = 10

# Min/Max number of welds
min_welds = 0
max_welds = 10

# Min/Max number of bends
min_bends = 0
max_bends = 5

length = np.random.randint(min_len,max_len,datapoints)
diameter = np.random.randint(min_dim,max_dim,datapoints)
thickness = np.random.randint(min_thick,max_thick,datapoints)
num_welds = np.random.randint(min_welds,max_welds,datapoints)
num_bends = np.random.randint(min_bends,max_bends,datapoints)

# Generate categorical variable for corrosion: 80% probability for "None",
# 15% for "Light" and 5% for "Severe"
corrosion = np.random.choice(a=['None', 'Light', 'Severe'],
                             size=datapoints,
                             p=[0.8, 0.15, 0.05])

# Add generated variables to dataframe: 
df['length (m)'] = length
df['diameter (cm)'] = diameter
df['thickness (mm)'] = thickness
df['num welds'] = num_welds
df['num bends'] = num_bends
df['corrosion'] = corrosion

手动规则生成

在进入自动化规则生成之前,让我们首先通过手动定义一套生成初始数据集的规则来建立基准。然后我们可以定义一些违例的示例规则,例如:

  1. 如果管道直径大于 8 厘米且壁厚测量为 2 毫米或更薄。

  2. 如果管道出现严重腐蚀且壁厚测量为 5 毫米或更薄。

  3. 如果管道总长度超过 10 米且没有焊缝(因为管道由最大长度为 10 米的段组成,对于超过这个长度的管道需要进行焊接)。

  4. 对于直径超过 5 厘米的管道,如果存在弯头但未检测到焊缝(考虑到这种直径的管道通常以直段形式提供,并需要将弯曲段后续焊接)。

如果违反了这些规则,则应报告为违例。

rule_1 = pd.DataFrame((df['diameter (cm)'] >= 8) & (df['thickness (mm)'] <= 2))
rule_2 = pd.DataFrame((df['corrosion']=='Severe') & (df['thickness (mm)'] <= 5))
rule_3 = pd.DataFrame((df['length (m)'] > 10) & (df['num welds'] == 0))
rule_4 = pd.DataFrame((df['num bends'] > 0) & (df['num welds'] == 0) & (df['diameter (cm)'] >=5))

df['violation'] = rule_1 | rule_2 | rule_3 | rule_4

df.head()
df[df['violation'] ==True].head()

我们的合成数据集现在包含我们的“管道检查”,其中包含每个管道的属性集合,以及一个二元变量,指示是否应将其报告为违规。上面的代码块还显示了数据集中选定的行,突出了报告违规的示例:

数据集中的示例行:无违规

数据集中的示例行:报告的违规情况

使用 Iguanas 进行自动规则生成

现在,关键问题出现了:我们能否从上述数据集中自动推导出适当的“完整性检查规则”?值得注意的是,这些规则生成算法对我们在上一节中定义的具体规则一无所知。这就是Iguanas登场的时刻(虽然是用于构建规则系统的 Python 包,而不是实际的蜥蜴)。

龟类。照片由David Clode提供,发布在Unsplash上。

Iguanas 提供了一套工具,旨在基于数据生成和优化规则。在我们的示例案例中,我们希望利用这个框架查看是否可以自动生成人类可解释的有意义规则,同时仅依赖数据集中存在的信息。

数据集划分和预处理

在应用任何规则生成技术之前,我们首先需要将数据集划分为训练集和测试集。训练集将用于构建和优化我们的规则系统,而测试集将用于评估其在未见数据上的表现。我们这里使用随机划分,选择 20%的数据用于测试规则在未见数据上的表现。

target_column = 'violation'
X = df.drop(
    target_column,
    axis=1
)
y = df[target_column]

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.2,
    random_state=0
)

然后,我们需要使用one-hot 编码将分类变量(如腐蚀)转换,使其与规则生成算法兼容:

ohe = OneHotEncoder(use_cat_names=True)
ohe.fit(X_train)
X_train = ohe.transform(X_train)
X_test = ohe.transform(X_test)

使用 Iguanas 生成规则

Iguanas框架中,你会找到两个主要的规则生成算法可以选择:RuleGeneratorDTRuleGeneratorOpt

  • RuleGeneratorDT:通过提取树集成模型中表现最佳的分支来生成规则。

  • RuleGeneratorOpt:通过优化单一特征的阈值来生成规则,并将这些单条件规则与 AND 条件结合,创建更复杂的规则。

在下面的示例中,我们使用了RuleGeneratorDT作为我们选择的算法,尽管我们也可以使用RuleGeneratorOpt替代。然后,我们定义了像F-scoreprecision这样的指标,并配置了规则生成器的参数。我们选择生成最多 4 个条件/语句的规则,以避免过于复杂和冗长的规则。我们还使用了随机森林分类器作为提取规则的基础模型。在定义这些参数之后,我们可以将规则生成器拟合到我们的训练数据中。

# Define metrics: F-score and precision
p = Precision()
f1 = FScore(beta=1)

# Define parameters for RuleGeneratorDT
params_RG_DT = {
    'metric': f1.fit,
    'n_total_conditions': 4,
    'tree_ensemble': RandomForestClassifier(n_estimators=10, random_state=0),
    'target_feat_corr_types': 'Infer',
    'num_cores': -1,
    'verbose': 1
}

#params_RG_OPT = {
#    'metric': f1.fit,
#    'n_total_conditions': 4,
#    'num_rules_keep': 1000,
#    'n_points': 100,
#    'ratio_window': 1,
#    'remove_corr_rules': True,
#    'target_feat_corr_types': 'Infer',
#    'verbose': 1
#}

# Instantiate and fit RuleGeneratorDT
rg = RuleGeneratorDT(**params_RG_DT)
X_rules_gen_train = rg.fit(
    X=X_train,
    y=y_train
)

规则过滤和优化

生成的规则集可能包含冗余或性能不佳的规则。为了解决这个问题,我们使用一系列过滤器来改进结果规则集:

  • 首先,我们应用SimpleFilter来移除 F 分数低于某个阈值的规则。

  • 然后,我们使用CorrelatedFilterAgglomerativeClusteringReducer进一步减少相关规则。

  • 最后,我们使用GreedyFilter选择一个规则子集,以最大化特定指标(例如,precision)。在这里,我们将按精度对规则进行排序,然后计算前 n 个组合规则的 F1 分数。

# Apply SimpleFilter
fr = SimpleFilter(
    threshold=0.3,
    operator='>=',
    metric=f1.fit,
)
X_rules_train = fr.fit_transform(
    X_rules=X_rules_gen_train,
    y=y_train
)

# Apply CorrelatedFilter
js = JaccardSimilarity()
acfr = AgglomerativeClusteringReducer(
    threshold=0.7,
    strategy='bottom_up',
    similarity_function=js.fit,
    metric=f1.fit
)
fcr = CorrelatedFilter(correlation_reduction_class=acfr)
X_rules_train = fcr.fit_transform(
    X_rules=X_rules_train,
    y=y_train
)

# Apply GreedyFilter
gf = GreedyFilter(
    metric=f1.fit,
    sorting_metric=p.fit,
    verbose=1
)

X_rules_train = gf.fit_transform(
    X_rules=X_rules_train,
    y=y_train
)

gf.plot_top_n_performance_on_train()

上述代码块还绘制了“前 n”规则在训练集上的综合表现,如下所示:

“Top n”性能。来源:由作者创建

我们在这里看到的是,对于一组前 4 条规则,我们可以完美匹配训练数据(这很有意义,因为这是我们用来生成合成数据集的规则数)。

创建 RBS 管道

现在,让我们使用这个组合的过滤规则集设置我们的RBS 管道。在这种情况下,我们采用简单的方法:

  • 如果有任何规则触发,则标记违例为“True”

  • 如果没有规则触发,则标记违例为“False”

要使用上述逻辑设置管道,我们首先需要创建配置参数。这只是一个列表,概述了管道的各个阶段,每个阶段应该使用两个元素的元组来定义:

  • 第一个元素应该是一个整数,表示在该阶段做出的决策(0 或 1)。

  • 第二个元素应该是一个列表,用于指定哪些规则应触发该决策。

我们还定义了如果没有规则触发的最终决策。这个管道接着使用 RBSOptimiser 进行优化。在这里,我们只需将实例化的管道类传递给管道参数,在优化过程中,它还会检查是否可以进一步减少生成的规则集。然后,我们运行 fit_transform 方法,使用给定的训练数据优化我们的管道。

# Define RBSPipeline configuration
config = [
   (1, X_rules_train.columns.tolist())
]

# Define final decision
final_decision = 0

# Instantiate RBSPipeline
rbsp = RBSPipeline(
    config=config,
    final_decision=final_decision
)

# Optimize RBSPipeline using RBSOptimiser
rbso = RBSOptimiser(
    pipeline=rbsp,
    metric=f1.fit,
    n_iter=1000,
    verbose=1
)
pipe_pred_train = rbso.fit_predict(
    X_rules=X_rules_train,
    y=y_train
)

性能评估

我们通过生成的 classification_report 和绘制的 混淆矩阵 来评估优化管道在训练数据上的性能。

# Evaluate performance using classification_report and confusion_matrix
print(
    classification_report(
        y_true=y_train,
        y_pred=pipe_pred_train,
        digits=2
    )
)

sns.set_style('white')
cm = ConfusionMatrixDisplay(
    confusion_matrix(
        y_true=y_train,
        y_pred=pipe_pred_train,
        normalize='true'
    )

)
cm.plot()

如下图所示,我们看到对于训练数据,我们能够完美预测所有数据点的检查状态(违规与否)。

预测违规的指标和混淆矩阵。来源:作者创建

自动生成的规则集

完成上述优化步骤后,我们最终获得了一组可以用于我们的“违规分类器”的规则。然后我们可以仔细查看相应的规则字符串,检查它们是否确实具有可解释性(这也是构建基于规则的分类器而不是黑箱机器学习模型的主要目标)。

rg.filter_rules(include=rbs_rule_names_gen)
rg.rule_strings

这会导致下面的打印输出。如果我们将其与我们手动定义的规则(附在下方)进行比较,我们会发现我们实际上得到了完全相同的规则集:

{'RGDT_Rule_20220214_43': "(X['corrosion_Severe']==True)&(X['thickness (mm)']<=5)",
 'RGDT_Rule_20220214_47': "(X['diameter (cm)']>=5)&(X['num bends']>=1)&(X['num welds']<=0)",
 'RGDT_Rule_20220214_58': "(X['diameter (cm)']>=8)&(X['thickness (mm)']<=2)",
 'RGDT_Rule_20220214_60': "(X['length (m)']>=11)&(X['num welds']<=0)"}
  1. 如果管道出现严重腐蚀且壁厚为 5 毫米或更薄。

  2. 对于直径超过 5 厘米的管道,如果存在弯头但未检测到焊接点

  3. 如果管道直径大于 8 厘米且壁厚为 2 毫米或更薄。

  4. 如果管道总长度超过 10 米且没有焊接点

在测试数据上使用生成的规则

正如我们已经展示的那样,生成的规则集与我们用于生成数据集的规则完全匹配,我们知道它们也将适用于测试集中的未见数据。然而,在更现实的情况下,我们只有数据集本身,而没有原始规则集进行比较。为了确保生成的规则也能很好地推广到未见数据上,我们还需要在测试集上验证它们:

然后,我们利用优化后的 RBS 管道预测测试集中的违规情况。与之前的示例一样,我们通过使用分类报告和绘制结果的混淆矩阵来评估其性能。

# Generated rules
X_rules_test = rg.transform(X=X_test)

# Apply our optimised RBS Pipeline to the test set:
opt_pipe_pred_test = rbso.predict(X_rules=X_rules_test)

# Print classification report and confusion matrix:
print(
    classification_report(
        y_true=y_test,
        y_pred=opt_pipe_pred_test,
        digits=2
    )
)

cm = ConfusionMatrixDisplay(
    confusion_matrix(
        y_true=y_test,
        y_pred=opt_pipe_pred_test,
        normalize='true'
    )
)
cm.plot()

预测违规情况的指标和混淆矩阵。来源:作者创建

上述结果展示了在测试数据上的完美准确性和精确度,表明生成的规则具有良好的泛化能力(正如我们在本例中已知的那样)。

现实使用案例

如前所述,在实际使用案例中,我们不一定会有一套已建立的规则来与我们的输出进行比较。在大多数情况下,我们的目标是构建分类模型,我们仅会获得一个属性/特征的数据集和一个对应的目标变量(例如“违规”或“非违规”)。任务是使用该数据集自动生成一套规则,从而洞察这些分类是如何进行的。

另一种方法是:使用这些算法的另一种情况是你已经有了一套手动定义的规则(例如由领域专家制定)。然而,数据分布的变化可能会使这些规则随着时间的推移变得次优。在这种情况下,你可以定期运行这种算法,以自动重新校准规则集以适应这些变化。

此外,这种逐步的方法是将领域专家的专业知识融入解决方案的有效方式。当他们审查生成的“违规”报告时,他们可能会注意到标记的违规实际上并不是问题。一旦他们验证了报告,系统可以从他们的输入中学习,并自主调整初始规则集。这一持续过程确保规则与数据变化和专家见解保持一致,从而使解决方案能够保持(或甚至提升)其性能。

结论

利用自动规则生成,例如使用Iguanas框架,允许我们直接从数据中提取有意义的规则。这与为相同目的构建黑箱分类模型形成对比。规则基础系统的透明性增强了决策过程的可解释性,这通常是采纳此类解决方案在安全关键行业中的重要方面。

尽管我们研究的案例非常具体,但规则生成过程也可以推广到各种其他分类挑战中。尽管规则基础方法可能不是更具挑战性问题的最佳选择,但它仍然可以为当前问题提供有价值(人类可解释)的见解。此外,它将作为一个良好的基础模型,让我们能够将其与更高级的技术进行比较和评估。

总结一下,我想给你留下一个重要的提醒:在深入研究复杂的前沿模型之前,始终花一点时间设置一个快速的基线模型。无论这意味着使用规则基础(或树基础)方法进行分类,如本案例所示,还是利用简单的线性回归进行回归任务。

此外,我想强调“KISS”原则,即“Keep It Simple, Stupid”(保持简单,愚蠢)。换句话说,在解决问题时,优先考虑简单性而不是不必要的复杂性。最有效的解决方案往往来自于最简单的策略。所以,记住,当你犹豫时,选择简单吧!

如果你对 AI/机器学习和数据科学相关主题感兴趣,你还可以查看我写的其他文章。你可以在我的 Medium 作者主页上找到所有文章,你可以在这里找到。

如果你觉得我之前的文章有趣,并且希望在发布新内容时获得通知,你也可以在下面的邮件列表中注册。

[## 每当 Vegard Flovik 发布文章时获取电子邮件通知。

每当 Vegard Flovik 发布文章时获取电子邮件通知。通过注册,如果你还没有 Medium 账户,你将创建一个账户…

medium.com](https://medium.com/subscribe/@vflovik?source=post_page-----4330ad69029--------------------------------)

如果你希望成为 Medium 会员,以便自由访问平台上的所有材料,你可以使用下面的推荐链接来实现。(注意:如果你通过此链接注册,我也将获得部分会员费用)

[## 使用我的推荐链接加入 Medium - Vegard Flovik

作为 Medium 会员,你的一部分会员费用将分配给你阅读的作者,你可以完全访问每一篇故事…

medium.com](https://medium.com/@vflovik/membership?source=post_page-----4330ad69029--------------------------------)

IID: 初学者的意义和解释

原文:towardsdatascience.com/iid-meaning-and-interpretation-for-beginners-dbffab29022f

独立同分布

Jae KimTowards Data Science Jae Kim

·发表于Towards Data Science ·阅读时间 9 分钟·2023 年 8 月 19 日

--

图片由Yu Kato提供,来源于Unsplash

在统计学、数据分析和机器学习主题中,IID 概念作为一个基本假设或条件经常出现。它代表了“独立同分布”。IID 随机变量或序列是统计模型或机器学习模型的重要组成部分,同时也在时间序列分析中发挥作用。

在这篇文章中,我以直观的方式解释了在采样、建模和预测性三个不同背景下的 IID 概念。文中提供了一个带有 R 代码的应用案例,涉及时间序列分析和预测性。

采样中的 IID

表示 X ~ IID(μ,σ²)的符号表示从具有均值μ和方差σ²的总体中以纯随机的方式对(X1, …, Xn)进行采样。即,

  • 每个连续的 X 的实现都是独立的,与前一个或后一个没有关联;并且

  • 每个连续的 X 的实现都来自具有相同均值和方差的相同分布。

示例

假设从一个国家的年收入分布中采集了样本(X1, …, Xn)。

  1. 一名研究人员选择了男性作为 X1,女性作为 X2,男性作为 X3,然后女性作为 X4,这种模式保持到 Xn。这不是一个 IID 采样,因为采样中的可预测或系统性模式是非随机的,违反了独立性条件

  2. 一名研究人员从最贫困的群体中选择了(X1, … X500),然后从最富有的群体中选择了(X501, … X1000)。这不是一个 IID 采样,因为这两个群体的收入分布具有不同的均值和方差,违反了同一性条件

建模中的 IID

假设 Y 是你想建模或解释的变量。那么,它可以分解为两个部分,即,

Y = 系统性成分 + 不系统性成分。

系统性成分 是由与其他因素的基本关系驱动的 Y 部分。它是可以通过理论常识典型事实 解释或预期的部分。它是 Y 的基础部分,具有实质性和实际重要性。

不系统性成分Y 中不受基本因素驱动的部分,无法通过理论、推理或典型事实解释或预期。它捕捉 Y 中无法通过系统性成分解释的变动。它应该是 纯随机的 和特有的,没有任何系统性可预测的模式。在统计模型中称为误差项,通常表示为 IID 随机变量。

例如,考虑以下形式的线性回归模型:

方程 (1)

在 (1) 中,α + βX 是系统性成分,而 (1) 中的误差项 u 是不系统性成分。

如果 β 的值接近 0 或在实际中可以忽略,则变量 XY 的解释力(用 R² 测量)较低,表明它不能令人满意地解释 Y 的基本变动。

假设误差项 u 是一个 IID 随机变量,均值为零且方差固定,表示为 u ~ IID(0, σ²),这是纯随机的,代表 Y 中的不系统或意外变动。

如果 u 不是纯随机的且具有明显的模式,则系统性成分可能没有被正确指定,因为它缺少某些实质性或基本内容。

示例:自相关

假设误差项具有以下模式:

方程 (2)

这是线性依赖(或自相关),这是一个系统性模式。这种可预测模式应纳入模型部分,这将更好地解释 Y 的系统性成分。实现这一目标的一种方法是包含 Y 的滞后项在 (2) 中。即,

方程 (3)

在 (3) 中包含的 Yt 的滞后项能够捕捉 (2) 中误差项的自相关,因此 (3) 中的误差项 e 是 IID。

示例:异方差性

假设误差项显示出以下系统性模式:

方程 (4)

这种误差项模式称为异方差性,其中误差项的变异性随 X 变量的变化而变化。例如,假设 Y 是食品支出,X 是个人的可支配收入。方程 (4) 意味着高收入者的食品支出变异性更高。

这是一个可预测的模式,而具有性质(4)的误差项违反了 IID 的假设,因为误差项的方差不是常数。为了将这种模式纳入系统组件中,可以通过以下方式进行广义或加权最小二乘估计:

方程(5)

方程(5)是一个带有变换变量的回归,可以写成

方程(6)

其中

适用于异方差误差的变换

上述对YX的变换提供了方程(6)中的变换误差项(*ut**),它是一个 IID 且不再具有异方差性。即,

这意味着,通过上述变换,误差项中的系统模式现在有效地纳入了系统组件。

作者创作的图像

上述图形以直观的方式展示了变换的效果。在变换之前(左侧图),变量Y的变异性随着X的变化而增加,这反映了异方差性。变换有效地将异方差模式纳入了Y的系统组件中,变换后的误差项现在是一个 IID 随机变量,如右侧图所示。

许多回归或机器学习模型中的模型诊断测试旨在检查误差项是否遵循 IID 随机变量,使用从估计模型中得到的残差。这也称为残差分析。通过残差分析和诊断检查,可以改善模型的系统组件的规范。

IID 和可预测性

纯粹随机的 IID 序列完全没有可预测的模式。也就是说,它的过去历史对序列未来的走向没有任何信息。

示例:自回归模型

考虑一个自回归模型,记作 AR(1),

方程(7)

其中ut ~ IID(0,σ²)且 -1 < ρ < 1(ρ ≠ 0)。

如果ρ = 0,时间序列 Yt 是一个 IID 且不可预测的,因为它不依赖于自己的过去,仅由不可预测的冲击驱动。

为了简化,假设 Y0 = 0 且ρ ≠ 0,进行以下持续替代:

Y1 = u1;

Y2 = ρY1 + u2 = ρu1 + u2;

Y3 = ρY2 + u3 = ρ²u1 + ρ u2 + u3;

Y4 = ρY3 + u4 = ρ³u1 + ρ²u2 + ρu3 + u4;

其一般表达式为

方程(8)

方程(6)表明,一个时间序列(如自回归)可以表示为过去和当前 IID 误差(或冲击)的移动平均,并具有指数衰减的权重。

注意,远程冲击如(8)中的 u1 和 u2 对Yt的影响很小,因为它们的权重微不足道。例如,当ρ = 0.5 且t = 100 时,ρ⁹⁹和ρ⁹⁸几乎为 0。只有当前或最近的冲击,如 u100、u99 和 u98,才可能实际相关。

因此,如果研究人员在时间 t 对ρ有一个良好的估计(来自数据)并观察了当前和近期的冲击,如 ut、ut-1、ut-2 和 ut-3,她或他可能能够通过将(8)中的移动平均投射到未来,合理准确地预测 Yt+1 的值。

示例:随机游走

当ρ = 1 时,(7)中的时间序列变成了一个随机游走,其中当前的Y变化是一个纯粹不可预测的 IID 冲击:即,

在这种情况下,从(8)中,ρ = 1,我们得到

换句话说,随机游走是所有过去和当前 IID 冲击的总和,其权重为 1。因此,远离的冲击与近期和当前冲击同等重要。例如,如果 t = 100,冲击 u1 对 Y100 的影响与 u100 相同。

作为所有过去和当前冲击的总和,随机游走时间序列是完全不可预测的。它还表现出高度的不确定性和持久性(对过去的依赖),具有以下分析结果

方程(9)

这意味着随机游走的变异性随着时间的推移而增加,表明随时间的不确定性高且可预测性低。

此外,Yt 和 Yt-k 之间的相关性几乎等于 1,对于几乎所有的k值。例如,当 t = 100 时,Y100 和 Y99 的相关系数为 99/100 = 0.99。

应用

作为一个应用,通过时间图和自相关函数比较了 IID 过程、ρ ∈ {0.3, 0.6, 0.9}的 AR(1)时间序列和随机游走的基本描述特性。

时间图

时间图:图像由作者创建

  • IID 序列 Y1 作为一个 AR(1)时间序列,ρ = 0 时,完全没有规律,随机且频繁地在均值 0 附近波动。它有很强的回归均值的倾向。

  • 对于 Y2 到 Y4,当ρ的值从 0.3 增加到 0.9 时,时间序列变得更平滑且频率较低,反映出对自身过去的依赖性增加。均值回归的程度也随着ρ值的增加而下降。

  • 随机游走 Y5 显示出一个可以随机改变方向的趋势(称为随机趋势)。它表现出随时间增加的变异性,如(9)中的第一个结果所示,并且随着时间的推移有一点回归均值的倾向(均值回避)。

自相关函数

自相关函数(图像由作者提供)

时间序列的自相关函数绘制了 Corr(Yt,Yt-k) 与滞后值 k 的关系。它提供了时间序列结构依赖性的视觉总结。例如,Corr(Yt,Yt-1) 测量的是 Y 在相隔 1 周期的值之间的相关性。蓝色带表示 95% 的置信区间,自相关值在此带内意味着该相关性在 5% 显著性水平下统计上与 0 无显著差异。

  • 一个 IID 时间序列 Y1 的所有自相关值实际上都可以忽略不计,统计上为 0。

  • 随着 ρ 值从 0.3 增加到 0.9,Y 对自身过去的依赖程度增加,因为更多的自相关值变得显著大于 0,并且在统计上有所不同。

  • 随机游走时间序列 Y5 的所有自相关值都极接近 1,表明对自身过去的高度依赖(持久性)。这反映了第(9)点中给出的第二个属性。

该应用展示了 IID 时间序列的基本统计属性,并与 AR(1) 和随机游走的属性进行比较。它说明了依赖于过去的程度(或可预测性)如何随着 AR(1) 系数值从 0 变到 1 而变化,即从 IID 时间序列变为随机游走。如上所述,当依赖程度适中且 ρ 的值大于 0 但小于 1 时,时间序列是可预测的。

R 代码

时间序列和图表是通过以下 R 代码生成的:

set.seed(1234)

n=500  # Sample size
# IID
Y1 = rnorm(n)    
# AR(1) with rho = 0.3, 0.6, and 0.9
Y2 = arima.sim(list(order=c(1,0,0), ar=0.3), n)
Y3 = arima.sim(list(order=c(1,0,0), ar=0.6), n)
Y4 = arima.sim(list(order=c(1,0,0), ar=0.9), n)
# Random Walk
Y5 = cumsum(rnorm(n))

par(mfrow=c(3,1))
# Time plots
plot.ts(Y1,main="IID",lwd=2)
plot.ts(Y2,main="AR(1) with rho=0.3",lwd=2)
plot.ts(Y3,main="AR(1) with rho=0.6",lwd=2)
plot.ts(Y4,main="AR(1) with rho=0.9",lwd=2)
plot.ts(Y5,main="Random Walk",lwd=2)

# Autocorrelation functions
acf(Y1,main="IID"); 
acf(Y2,main="AR(1) with rho=0.3"); 
acf(Y3,main="AR(1) with rho=0.6"); 
acf(Y4,main="AR(1) with rho=0.9"); 
acf(Y5,main="Random Walk");

结论

IID 的概念在统计分析和机器学习模型中是基础的。本文回顾了 IID 在三种不同背景下的应用:抽样、建模和时间序列分析中的可预测性。展示了一个应用,该应用比较了 IID 时间序列与平稳 AR(1) 和随机游走的基本描述统计属性。

闪耀的洞察:GPT 从图表和表格中提取意义

原文:towardsdatascience.com/illuminating-insights-gpt-extracts-meaning-from-charts-and-tables-a0b71c991d34

使用 GPT 视觉来解释和汇总图像数据。

Ilia Teimouri PhDTowards Data Science Ilia Teimouri PhD

·发表于 Towards Data Science ·阅读时间 7 分钟·2023 年 12 月 24 日

--

照片由 David Travis 拍摄,发布在 Unsplash

许多领域的专家认为,将图像等视觉输入与文本和语音整合到大型语言模型(LLMs)中,被视为 AI 研究中的一个重要新方向。通过增强这些模型处理除语言之外的多种数据模式,有可能显著拓宽它们的应用范围,同时提高它们在现有自然语言处理任务中的整体智能和性能。

多模态 AI 的前景从更具吸引力的用户体验,如能够感知其周围环境并提及周围物体的对话代理,到能够通过结合语言和视觉知识流畅地将指令转化为物理动作的机器人。通过将历史上分离的 AI 领域统一到一个模型架构中,多模态技术可能会加速依赖多种技能的任务的进展,如视觉问答或图像描述。不同领域的学习算法、数据类型和模型设计之间的协同作用可能会导致快速进步。

许多公司已经以各种形式采纳了多模态技术:OpenAIAnthropic,谷歌的 BardGemini 允许用户上传自己的图像或文本数据并进行聊天。

在这篇文章中,我希望展示大语言模型与计算机视觉在金融领域的一种简单而强大的应用。股票研究员和投资银行分析师可能会发现这特别有用,因为你们可能会花费大量时间阅读包含各种表格和图表的报告和声明。阅读冗长的表格和图表并正确解释它们需要大量时间、领域知识以及足够的专注以避免错误。更繁琐的是,分析师偶尔需要手动从 PDF 中输入表格数据,以便创建新的图表。一个自动化的解决方案可以通过提取和解释关键信息来减轻这些痛苦,而无需人工监督或疲劳。

实际上,通过将自然语言处理与计算机视觉相结合,我们可以创建一个助手来处理许多重复的分析任务,从而让分析师专注于更高级的战略和决策制定。

近年来,在使用 光学字符识别 或视觉文档理解(图像转文本)从图像/PDF 数据中提取文本方面取得了很多进展。然而,由于当前可用的训练数据的性质,现有方法仍然难以处理许多财务报表、研究报告和监管文件中的复杂布局和格式。

GPT-4V(ision)用于表格和图表

在 2023 年 9 月,OpenAI 发布了 GPT-4 Vision。根据 OpenAI 的说法:

GPT-4 带有视觉功能(GPT-4V)使用户能够指示 GPT-4 分析用户提供的图像输入。

GPT-4V 的视觉能力来自于 GPT-4,因此两个模型的训练方式相似。首先,研究人员向系统输入了大量文本,以教会它语言的基本知识。目标是预测文档中的下一个词。然后是使用一种称为人类反馈强化学习(RLHF)的精细训练方法。这涉及根据人类训练者的积极反应进一步微调模型,以产生我们认为真正有用的输出。

在这篇文章中,我将创建一个 Steamlit 应用程序,用户可以上传图像并询问关于图像的各种问题。我将使用的图像是金融 PDF 文档的截图。实际上,该文档是公开的 基金事实表

代码的主要部分有两个,第一个是一个将图像从给定文件路径编码的函数:

# Function to encode the image from a file path
def encode_image(image_path):
    with open(image_path, "rb") as image_file:
        return base64.b64encode(image_file.read()).decode('utf-8')

你需要这个功能,因为模型期望你的输入图像是 base 64 编码格式。接下来的主要代码部分将是你如何将请求发送到 OpenAI 的 API:

# Function to send the request to OpenAI API
def get_image_analysis(api_key, base64_image, question):
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {api_key}"
    }

    payload = {
        "model": "gpt-4-vision-preview",
        "messages": [
            {
                "role": "user",
                "content": [
                    {"type": "text", "text": question},
                    {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}}
                ]
            }
        ],
        "max_tokens": 300
    }

    response = requests.post("https://api.openai.com/v1/chat/completions", headers=headers, json=payload)
    return response.json()['choices'][0]['message']['content']

在这里,我们将模型名称设置为gpt-4-vision-preview。如你所见,这与通常的文本到文本的 OpenAI API 调用非常不同。在这种情况下,我们定义了一个名为payload的 json 对象,其中包含你的文本以及图像数据。

你可以扩展get_image_analysis方法,以发送多个图像,或通过detail参数控制模型如何处理图像。详细信息请参见这里

剩余的代码主要是 Streamlit 方法,我们允许用户上传他们的图像,并通过提问与图像互动。

完整代码:(也可以在Github上找到)

import streamlit as st
import os
import requests
import base64
from PIL import Image

# Function to encode the image from a file path
def encode_image(image_path):
    with open(image_path, "rb") as image_file:
        return base64.b64encode(image_file.read()).decode('utf-8')

# Function to save the uploaded file
def save_uploaded_file(directory, file):
    if not os.path.exists(directory):
        os.makedirs(directory)
    file_path = os.path.join(directory, file.name)
    with open(file_path, "wb") as f:
        f.write(file.getbuffer())
    return file_path

# Function to send the request to OpenAI API
def get_image_analysis(api_key, base64_image, question):
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {api_key}"
    }

    payload = {
        "model": "gpt-4-vision-preview",
        "messages": [
            {
                "role": "user",
                "content": [
                    {"type": "text", "text": question},
                    {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}}
                ]
            }
        ],
        "max_tokens": 300
    }

    response = requests.post("https://api.openai.com/v1/chat/completions", headers=headers, json=payload)
    return response.json()['choices'][0]['message']['content']

def main():
    st.title("Image Analysis Application")

    uploaded_file = st.file_uploader("Choose an image...", type=["jpg", "jpeg", "png"], key="file_uploader")

    if uploaded_file is not None:

        file_path = save_uploaded_file('data', uploaded_file)

        # Encode the uploaded image
        base64_image = encode_image(file_path)

        # Session state to store the base64 encoded image
        if 'base64_image' not in st.session_state or st.session_state['base64_image'] != base64_image:
            st.session_state['base64_image'] = base64_image

        image = Image.open(uploaded_file)
        st.image(image, caption='Uploaded Image.', use_column_width=True)

    question = st.text_input("Enter your question about the image:", key="question_input")

    submit_button = st.button("Submit Question")

    api_key = os.getenv("OPENAI_API_KEY")

    if submit_button and question and 'base64_image' in st.session_state and api_key:
        # Get the analysis from OpenAI's API
        response = get_image_analysis(api_key, st.session_state['base64_image'], question)
        st.write(response)
    elif submit_button and not api_key:
        st.error("API key not found. Please set your OpenAI API key.")

if __name__ == "__main__":
    main()

输出和总结

现在让我们来看几个例子:

图像由作者生成。图表来自公开的UBS 基金事实说明书

在这个例子中,问题是关于性能的峰值。我们可以看到模型正确地识别了峰值。模型还能够理解虚线是图例中的指数表现。在计算机视觉中,理解虚线和点线通常比较困难,但只要截图质量好(细节足够),GPT Vision 就可以轻松完成任务。

另一个例子:

图像由作者生成。资料来自公开的UBS 基金事实说明书

在这个例子中,我尝试检验模型在以下方面的表现:1)从其他数据中提取相关表格 2)提取表格的相关部分 3)进行一些基本的数学运算。

如所示,模型成功满足了此任务的所有三个要求——鉴于传统上涉及的复杂性,这并非易事。手动操作时,分析师即使使用光学字符识别(OCR)工具,也很难提取锁定在 PDF 中的双栏表格。还需要额外的编码来将图表解析为结构化的数据框,以便于汇总。这可能会在回答原始问题之前消耗大量时间。然而,在这里,只需一个提示就能实现预期结果。避免了解码图像、抓取数据、处理电子表格和编写脚本的繁琐工作,极大提高了效率。

最后:

图像由作者生成。资料来自公开的UBS 基金事实说明书

排序算法根据指定的排序规则系统地重新排列列表或数组的元素。然而,与传统代码不同,像 GPT 这样的 LLM 没有预定义的排序例程。

相反,GPT 被训练成根据先前的上下文预测序列中的下一个词。通过足够的数据和模型能力,排序能力从学习文本模式中显现出来。

上面的例子说明了这一点——GPT 正确地对从 PDF 截图中提取的表格中的两列进行排序,这是一项复杂的工作,涉及光学字符识别、数据提取和处理技能。即便在 Excel 中,多列排序也需要一定的专业知识。但只需在提示中提供目标,GPT 就能自动处理这些复杂的步骤。

与传统算法遵循严格的逐步指令不同,像 GPT 这样的语言模型通过在训练过程中识别文本中的关系来发展排序能力。这使得它们能够从多样的曝光中吸收各种能力,而不是被预定义的编程所限制。

为什么这很重要?

通过将这种灵活性应用于我们在这里看到的专业任务,提示可以解锁高效的问题解决方案,这些方案否则将需要大量的手动工作和技术知识。

揭开文本生成 AI 的黑箱

原文:towardsdatascience.com/illuminating-the-black-box-of-ai-ddea07e65c35

需求洞察

Anthony AlcarazTowards Data Science Anthony Alcaraz

·发表于 Towards Data Science ·8 分钟阅读·2023 年 12 月 17 日

--

人工智能软件用于提升本文的语法、流畅性和可读性。

像 ChatGPT、Claude 3、Gemini 和 Mistral 这样的语言模型以其表达能力和博学引人注目。然而,这些大型语言模型仍然是黑箱,掩盖了驱动其响应的复杂机制。它们生成类似人类的文本的能力超越了我们理解其机器思维如何运作的能力。

但随着人工智能在信任和透明度至关重要的场景中发挥作用,如招聘和风险评估,可解释性现在成为了重点。可解释性不再是复杂系统的可选配件,而是安全推动高影响领域 AI 的必要前提。

为了揭开这些黑箱模型的面纱,生动的可解释 NLP 领域提供了越来越多的工具——从揭示关注模式的注意力可视化,到探查输入的随机部分以量化影响。像 LIME 这样的某些方法创建了模拟关键决策的简化模型。其他方法,如 SHAP,则借鉴了合作博弈论的概念,将“信贷”和“责任”分配到模型输入的不同部分,基于其最终输出。

无论技术如何,所有方法都追求相同的关键目标:阐明语言模型如何利用我们提供的大量文本来编写连贯的段落或进行重要评估。

人工智能已经在影响人类生活的决策中发挥作用——选择性地评估申请者、审查仇恨内容、诊断疾病。

解释不仅仅是附加功能——它们将对监督这些强大的模型在社会中的普及起到关键作用。

随着大型语言模型的不断进步,它们的内部工作机制仍然笼罩在神秘之中。然而,可信的人工智能需要对其在重大决策中的推理过程保持透明。

解释性 NLP 的充满活力的领域提供了两种主要方法来阐明模型逻辑:

  1. 基于扰动的方法:如 LIME 和 SHAP 的技术通过遮蔽输入组件系统地探测模型,并根据输出变化量化重要性。这些外部视角将模型视为黑箱。

  2. 自我解释:一种替代范式使模型能够通过生成文本解释自己的推理。例如,突出影响预测的关键输入特征。这依赖于内省的模型意识,而不是强加的解释。

早期分析发现这两种方法都很有前景——LIME 和 SHAP 擅长忠实捕捉模型行为,而自我解释则更符合人类的理性。然而,当前的实践也难以充分评估这两者,建议重新考虑评估策略。

理想情况下,两者之间的协同作用可以结合起来推动进展。例如,自我声明的重要因素可以与扰动实验进行验证。并且归因分数可以增加验证信号,为自由形式的解释提供支撑。

随着模型不断吸收更多的世界知识,阐明它们多方面的推理变得越来越重要。多样化的新兴想法可能对应对这一挑战至关重要。

解释 AI 的平衡艺术

构建解释不可避免地需要简化。但过度简化会导致扭曲。以常见的基于注意力的解释为例——它们突出模型 supposedly 关注的输入部分。然而,注意力分数往往与 AI 系统的实际推理过程不一致。

更严格的技术如 SHAP 通过系统地遮蔽不同的输入组件并直接测量对输出的影响来避免这一点。通过比较有无每个特征的预测,SHAP 为每个特征分配一个“重要性分数”以表示其影响。这种基于扰动的方法更好地反映了模型的逻辑。

然而,忠实性往往以可理解性为代价。移除单词和子句的组合很快变得认知负担过重。因此,研究社区强调平衡两个关键标准:

忠实性:解释多准确地捕捉了模型的实际决策过程?基于遮蔽的扰动方法在这里表现出色。

可理解性:解释对目标受众的直观性和易消化程度如何?简化的线性模型有助于理解,但可能会扭曲。

理想情况下,解释应同时展现两者特征。但即便是忠实性高的 SHAP,在模型处理大量文本和不受限制的生成时,也会遇到困难——需要处理的输出组合呈指数级增长。对 10,000 字文章的所有可能遮蔽排列进行计算是不可行的!

这阻碍了对关键应用的进展,如解释作文评分模型或处理文档的问答系统。创建模仿预测的简化模型(如 LIME)在复杂文本推理中也变得不可行。需要更具针对性的解决方案来扩展大型语言模型的可解释性。

突出了关键挑战 —— 特别是由长输入和开放输出引入的指数复杂性。如果有任何部分需要更多解释,请告诉我!

TextGenSHAP:优化语言任务的解释

[## TextGenSHAP:在长文档中生成的可扩展事后解释

大型语言模型(LLMs)由于其日益准确的性能而引起了对实际应用的极大关注…

arxiv.org

为了克服复杂语言模型的可解释性障碍,研究人员开发了TextGenSHAP —— 在 SHAP 的基础上,融入了对效率的优化,并考虑了语言结构。

若干创新技术应对了指数级的计算复杂性。预测解码首先预测可能的文本输出,避免了浪费的解码尝试。闪电注意力简化了内存密集型的注意力计算。原地重采样为提高效率预计算了输入编码。

这些加速技术使运行时间从几小时减少到几分钟,实现了实用的周转。作者验证了在不同模型类型和数据集复杂性下的数量级加速。

但仅仅是原始效率是不够的 —— 语言本身的复杂性必须得到体现。TextGenSHAP 解决了自然语言处理独有的解释挑战:

层次结构 —— 除了单个词语外,语言模型还学习句子、段落甚至文档之间的概念联系。TextGenSHAP 的层次化归因允许在粗粒度和细粒度层面上分配重要性评分。

指数输出空间 —— 开放式文本生成产生了巨大的可能输出集,不同于受限的分类任务。通过如 Shapley-Shubik 指数等重新表述,TextGenSHAP 绕过了详尽的枚举来估计特征重要性。

自回归依赖 —— 生成的标记概率上依赖于前面的标记。TextGenSHAP 的适应性解码算法,如预测解码,明确地尊重这些标记间的依赖关系。

这些架构和语言上的进展为 TextGenSHAP 应对现代自然语言处理的复杂性铺平了道路。现在可以着手解决长期挑战中的可解释性问题,例如文档上的问答。

作者生成的图像,来源于 Dall-E-3.0

应用:解释文档中的问答

文档中的问答代表了 AI 的一个宝贵里程碑——综合散布在段落中的信息以解决复杂的查询。TextGenSHAP 现在使解释这些复杂的文本推理工作流程成为可能。

作者在需要从超过 10,000 个单词的上下文中推导答案的挑战性数据集上评估了 TextGenSHAP。令人印象深刻的是,它准确地识别出分布在扩展文本中的关键句子和短语,这些句子和短语对每个答案的形成最有帮助。

通过适当地归因于冗长文档的不同部分,TextGenSHAP 使强大的应用成为可能:

改进文档检索——通过影响评分对上下文进行排名和筛选,提取了更相关的段落。仅通过根据 TextGenSHAP 重新排序,作者展示了检索召回率的显著提升——从 84%提高到近 89%。这有助于更好地为下游推理步骤提供信息。

提炼证据——使用重要性评分来挑选每个问题回答的最核心支持段落,在具有多样证据的数据集上准确率从 50%提高到 70%。确保模型关注简明的解释提取物,以对抗在大型语料库中对虚假模式的过拟合。

人工监督——通过揭示最有影响力的文本片段,TextGenSHAP 使审计员能够快速验证模型是否使用了适当的支持内容,而不是依赖于非预期的提示。否则,监控复杂的推理过程是不可行的。

对于推理密集型问题回答的成功表明,解释 AI 能力的社会影响有更广泛的适用性——如评分论文内容和散文或解释医疗诊断。通过揭示语言中的关键联系,TextGenSHAP 使我们朝着负责任和可信赖的 NLP 系统迈进。

anon832098265.github.io/

调查自我解释

arxiv.org/abs/2310.11207?source=post_page-----ddea07e65c35-------------------------------- [## 大型语言模型能否自我解释?LLM 生成自我解释的研究

大型语言模型(LLMs)如 ChatGPT 在各种自然语言任务中展示了卓越的性能…

arxiv.org

我们讨论了将模型视为黑箱的传统事后方法。一个有趣的替代方案是使系统能够解释自身的推理——自我解释

最近的研究分析了这些用于情感分析,使用 ChatGPT。模型突出了输入词汇对其预测的影响。与直接扰动输入的外部技术不同,自我解释依赖于内省模型意识来声明重要因素。

论文系统地比较了不同格式,发现预测然后解释或反之都工作得相当好。模型容易生成所有单词的特征归因分数或仅仅是最重要的高亮部分。但有趣的是,重要性分数通常聚集在“全面的”水平(例如 0.0、0.5、0.75),这更类似于人类判断,而不是离散的机器精度。

尽管自我解释与人类的推理相对一致,但广泛使用的评估实践难以区分质量。依赖于细粒度模型预测变化的指标容易受到欺骗,而这些变化对于 ChatGPT 常常不敏感。研究人员得出结论认为,经典的可解释性流程需要重新考虑以适应大型语言模型。

要充分实现自我解释的潜力,需要新的评估框架,以适应其混合的人机特性。将它们与直接可观察的信号如注意力权重相结合,可能会增强其真实性。构建模块化的推理/解释组件也可能使得自我解释更加纯粹。

通过精心协同设计,以适应其新兴特性,自我解释有可能开启前所未有的模型透明度——将黑箱转换为“玻璃箱”,使系统不仅展示其内部工作原理,还讨论其内在机制。

TextGenSHAP 方法专注于为文本生成模型提供高效的 Shapley 值归因。它在量化特征重要性方面取得了进展,适用于长文档问答任务。

然而,TextGenSHAP 仍然依赖外部视角,扰动输入并观察输出变化,而不是让模型自我反思其推理。这为与自我解释方法的整合留下了空间。

自我解释可以提供更为定性、直观的理解,以补充来自 TextGenSHAP 的定量归因分数。例如,TextGenSHAP 可能会识别文档中的关键段落,并将某些句子突出为回答问题时最具影响力的部分。自我解释可以通过讨论聚焦于这些领域的逻辑来丰富这些信息。

相反,目前的自我解释通常以自由生成的形式出现,缺乏基础。与将模型推理综合为标记重要性排名的归因分数结合起来,可能有助于验证和增强自我解释的意义。

在架构上,TextGenSHAP 模块可以首先处理文档和问题,生成注意力分布和段落排名。然后,自我解释模块可以利用这些定量信号生成自由形式的推理,讨论评估内容,并通过归因分数引导解释。

联合评估还可以评估自我声明的解释因素是否与扰动基础评分指定为有影响的输入组件一致。

本质上,自我解释提供了模型理解的“是什么”,而归因分数则提供了“为什么”。它们的共生关系可能使丰富的解释性融合定量和定性见解成为可能。

前进的道路:通过透明度实现信任

TextGenSHAP 提供了一个关键的进展——窥视大型语言模型在处理大量文本时的复杂工作。通过创建高效准确的解释,它绕过了现有解释方法仅限于少量语言片段的障碍。

然而,单纯的语言流畅性并不能保证可信赖的人工智能。语言的掌握——推动 ChatGPT 口才进步的标志——必须与阐明的掌握相结合。

阐明不仅仅是抛出几个关键词那么简单——它需要复制复杂的推理链条,从而得出最终评估。像 TextGenSHAP 这样的进展将这一必要的透明度更接近现实。

随着模型不断吸收更多世界知识,其内部表征的复杂性也大大增加。通过简化的注意力分数或小扰动样本尝试监督只会混淆视听,而非阐明。尊重结构和逻辑依赖的更全面的方法,如 TextGenSHAP,将证明至关重要。

学习缺乏透明度将导致权力没有责任。观察缺乏阐明将导致缺乏严谨的橡皮图章。神经网络的显著复兴必须伴随揭示其复杂性的技术。

在这一领域的进展仍处于初期阶段——但重要的种子已经扎根。通过努力完善理解性与忠实性的混合,无论是通过高效的近似方法还是天生可解释的架构,也许未来的系统可以巧妙地解释其掌握,从而彻底揭开黑箱神秘的面纱。

初学者的图像分类

原文:towardsdatascience.com/image-classification-for-beginners-8546aa75f331

VGG 和 ResNet 架构来自 2014 年

Mina GhashamiTowards Data Science Mina Ghashami

·发布在Towards Data Science ·10 分钟阅读·2023 年 10 月 17 日

--

图片来源于unsplash — 作者修改

图像分类是我在Interview Kickstart教授的第一个主题,旨在帮助专业人士在顶尖科技公司找到工作。当我准备其中的一次讲座时,我写了这篇文章。因此,如果你对这个主题不熟悉,这个直观的解释可能对你也有帮助。

在这篇文章中,我们探讨了 VGG 和 ResNet 模型;这两者都是卷积神经网络(CNNs)在计算机视觉领域发展中的开创性和影响力巨大的作品。VGG[2] 是 2014 年由牛津大学的研究小组提出的,而 ResNet[3] 是 2015 年由微软研究人员提出的。

让我们开始吧。

什么是 VGG?

VGG 代表视觉几何组,是牛津大学的一个研究小组。2014 年,他们为图像分类任务设计了一个深度卷积神经网络架构,并以他们的名字命名了它,即 VGG。[2].

VGG 网络架构

这个网络有几种配置;所有配置的架构相同,只是层数不同。最著名的有 VGG16 和 VGG19。VGG19 比 VGG16 更深,性能更好。为了简化,我们关注 VGG16。

VGG16 的架构如下图所示。正如我们所见,它有 16 层;13 个卷积层和 3 个全连接层

VGG16 架构 — 图片由作者提供

这是一个非常简单的架构;它由 6 个块组成,其中前 5 个块包含卷积层,之后是一个最大池化层,第 6 个块仅包含全连接层。

所有卷积层使用 3x3 滤波器,步幅为 1,所有 最大池化层为 2x2,步幅为 2,因此它们将输入特征图的宽度和高度减半。这称为 下采样,因为它减少了输出特征图的大小。

注意,卷积层从 64 个滤波器开始,并在每次池化后翻倍,直到达到 512 个滤波器。所有卷积层使用“相同”填充以保持输入和输出之间的相同大小,并且它们都使用 RELU 激活函数。下面,我们解释这些概念:

相同填充:相同填充是一种填充技术,以确保卷积操作的输出体积具有与输入体积相同的高度和宽度。它通过在所有边缘均匀填充零来工作,使得卷积操作后空间维度保持不变。

最大池化:如我们上面所见,在每个块之后应用 2x2 最大池化,步幅为 2。最大池化输出窗口中的最大值。步幅为 2 将空间维度减半,并保留了对强大特征检测至关重要的信息。此外,这种减少带来了计算效率。

RELU 激活函数:如我们所提到的,VGG 使用的激活函数是 RELU。RELU 将负值设为零,保持正值不变。它所增加的非线性有助于提升模型的表现力,并有助于检测复杂的模式。VGG 模型在每个卷积层后使用 RELU。

作者提供的图像

让我们逐层了解 VGG16 架构:

  • 假设输入是一个彩色图像,其尺寸为高度和宽度,则其大小为(高度,宽度,3)。注意 RGB 有 3 个通道。

  • 第一层具有 64 个神经元,并应用 3x3 卷积,具有“相同”填充,因此第一层的输出特征图为(高度,宽度,64)。

  • 第二层与第一层相同,因此这一层的输出特征图也为(高度,宽度,64)。

  • 第三层是 2x2 最大池化,步幅为 2,因此它将大小缩小到(高度/2,宽度/2,64)

  • 第四层和第五层是 conv3–128,具有“相同”填充,因此它们将输出大小更改为(高度/2,宽度/2,128)。

  • 第六层再次是 2x2 最大池化,它将输出大小更改为(高度/4,宽度/4,128)。

  • 如果我们继续这样下去,我们会发现当数据到达第一个全连接层时,它的形状是(高度/32,宽度/32,512)。因此,我们看到通道的数量从 3 增加到 512,同时高度和宽度减少了 32 倍!!!可以把它想象成压缩信息,而是捕捉通道中的模式。

VGG 计算成本

VGG16 是 最大的 CNN 模型之一;它拥有 1.38 亿个参数。在下图中,我们看到 VGG 的两个变体:VGG16(具有 16 层)和 VGG19(具有 19 层)。

图像来自 [1]

我们看到 VGG16 和 VGG19 在一次前向传播中需要的操作次数是最大的 CNN 模型。注意,操作次数与模型的参数数量成正比。在下一篇文章中,我们将探讨 ResNet[3]模型,它比 VGG 小得多,并且表现优于 VGG。

为什么提出了 VGG?

在 VGG 之前,CNN 模型的层数较少,卷积滤波器较大。VGG 网络的提出是为了展示一个只有 3x3 卷积层堆叠在一起的简单 CNN 可以与具有大滤波器的复杂模型一样好。

它还展示了卷积网络中深度的重要性。他们表明,堆叠许多小的 3x3 卷积层可以有效模拟较大的感受野。在 VGG 被提出时,它在 ImageNet 数据集上的图像分类任务中超越了所有其他模型。

ResNet 是什么?

ResNet,即残差网络,是微软研究人员在 2015 年提出的[3]。在深入了解其架构之前,先来看看它为何被提出。

为什么提出了 ResNet?

总而言之,ResNet 的提出是为了解决在非常深的网络中的梯度消失问题。 让我们更深入地看看:

正如我们在 VGG 的案例中所看到的,深度神经网络极其强大。但它们也有更多的参数,因此训练时间较长,计算成本较高。此外,我们还需要更多的训练数据来训练它们。

除了计算成本和训练数据的大小外,训练深度神经网络也面临障碍。正如下图所示,当我们训练浅层神经网络时,训练损失在早期周期中开始减少。但在深度神经网络中,训练损失在早期周期中减少很少,经过几个周期后突然下降。这是深度神经网络实际训练中的一个大障碍。

那么为什么会发生这种情况呢?

浅层和深度神经网络中训练损失随周期减少 —— 图片作者

这发生的原因有两个:

  1. 在深度神经网络的早期层中,梯度消失问题出现;即,损失的梯度在到达网络的早期层时会消失,因此这些层的参数更新非常少。

  2. 在深度神经网络的晚期层中,原始信号非常少(即原始输入)。这是为什么呢?因为信号被所有前面层的权重乘以,并通过激活函数,这会将信号推向零。因此,这些层在早期周期的输出几乎是随机噪声。因此,相对于损失的梯度是随机噪声,对这些层参数的更新没有意义。

深度神经网络在前几个训练周期中学习受阻 —— 图片作者

这就是为什么在训练深度神经网络的前几个周期中没有看到太多改进的原因。

为了解决这个问题,我们希望找到一种方法,使得输入能够到达后期层,梯度能够到达早期层。我们可以使用跳跃连接来实现这两者。

跳跃连接

跳跃连接的理念是将网络层分组到块中,对于每个块,使输入同时通过和绕过该块。像这样:

图片由作者提供

在每个块内,层正常地向前传递它们的数据,而在块之间,我们有一种新类型的连接。

如上所示,这种连接通过将块的输入与块的输出结合起来工作。因此,数据基本上有两个路径流动:一个通过块,另一个绕过块。

所以一个残差块看起来像这样:

残差块 — 图片由作者提供

上面的“+”符号表示“组合”符号,它将输入张量和输出张量结合在一起。它必须是一个不会干扰梯度传递的操作。“+”操作可以是以下任意一种:

  1. 两个张量的逐元素相加

  2. 两个张量的拼接

值得强调的是,残差块之所以被称为“残差”,是因为它实现了一种残差学习方法。每个残差块学习一个相对于其输入的残差函数,而不是直接拟合一个期望的基础映射。

在前馈网络中,我们学习从输入到输出的直接映射,即 f(x): x->y。然而,在残差块中,如上所示,每个残差块学习一个残差函数,即 x->f(x)+x这个残差函数表示需要对输入进行的修改,以获得期望的输出。

图片来源 [3]

ResNet 更容易训练

由残差块组成的网络称为残差网络或 ResNet。它们有几个优势,使得它们更快、更容易训练。

  1. 其中之一是每个残差块都会增强数据:由于它们将输入绕过块而不变,残差块的工作不是去判断输入中包含什么重要信息,而是去确定我们可以向输入中添加哪些额外的信息以达到输出。结果发现这是一项更简单的工作。

  2. 网络具有更短的梯度路径。由于每个块都有一个绕过块的路径,梯度也会经过这条路径。因此,网络中的任何层都有相对较短的路径,使得损失梯度能够到达。

梯度在两个路径中流动:通过层和绕过块 — 图片由作者提供

关于 ResNet 的关注点

关于残差块,有一些关注点,我们在设计残差网络时需要注意:

  1. 为了能够添加/连接残差块的输入和输出,我们必须确保两个张量的形状相同。显然,如果我们强制每一层的输出形状与其输入相同,这个问题将不会出现。但是,强制这种约束会限制模型的容量。

  2. 如果我们使用连接而不是逐元素相加来组合每个块的输入和输出张量,那么我们将得到一个非常大的张量,并且参数会爆炸。因此,我们不应过度使用连接操作,如果我们的网络很深,必须优先考虑相加。通常,连接操作在一个或两个块中最多使用。

ResNet 架构

现在我们已经了解了残差块和跳跃连接,ResNet[3] 用于图像分类,通过堆叠多个残差块来构建。我们可以构建超过 100 层的非常深的网络。原始的 ResNet 具有从 18 层到 152 层的变体架构 [3]。

每个残差块由一个卷积层、批量归一化和 RELU 激活函数组成。正如我们在下图中看到的那样,“批量归一化”在每个卷积层之后使用;它通过减去均值并除以标准差来规范化激活。这一操作稳定了训练过程。

残差块 - 作者提供的图像

当 ResNet 被提出时,它在 ImageNet 分类任务上取得了最先进的结果 [3]。

要点

要点 1: 深层神经网络中的最后几层接收到的输入信号非常少。这是因为每个中间层的激活函数如 sigmoid 或 tanh 对于大的正/负输入会饱和到 0 或 1。这会随着信号通过层而减弱。这被称为“饱和”。

要点 2: 深层神经网络的早期层在训练网络的前几个时期接收到的梯度非常少。这是因为在训练过程中,误差梯度通过许多层反向传播,它会指数级地缩小。这使得早期层难以有效学习。这个问题被称为“梯度消失问题”。

要点 3: VGG 的提出是为了展示使用简单的 3x3 滤波器的深层网络如何类似于使用大卷积的复杂网络。ResNet 的提出是为了解决非常深层网络中的梯度消失问题。

总结

在这篇文章中,我们研究了两个开创性的 CNN 架构,即 VGG 和 ResNet。VGG 是一个深层 CNN,仅包含 3x3 卷积层。它历史上用于图像分类任务,并且在提出时,它在 ImageNet 挑战中优于 AlexNet 和其他竞争模型。它展示了 CNN 中深度的力量,以及使用简单的 3x3 卷积可以类似于更大卷积核的效果。ResNet 在 VGG 之后被引入,并且优于 VGG。ResNet 的创新在于引入了残差块,这使得深层网络的训练变得更容易和更快。

如果你有任何问题或建议,请随时联系我:

电子邮件:mina.ghashami@gmail.com

领英:www.linkedin.com/in/minaghashami/

参考文献

  1. 实际应用中的深度神经网络模型分析

  2. 非常深的卷积网络用于大规模图像识别

  3. 深度残差学习用于图像识别

使用 PyTorch 和 SHAP 进行图像分类:你能信任自动驾驶汽车吗?

原文:towardsdatascience.com/image-classification-with-pytorch-and-shap-can-you-trust-an-automated-car-4d8d12714eea

构建一个目标检测模型,将其与强度阈值进行比较,评估并使用 DeepSHAP 解释它

Conor O'SullivanTowards Data Science Conor O'Sullivan

·发表于Towards Data Science ·阅读时间 14 分钟·2023 年 3 月 21 日

--

(来源:作者)

如果世界不那么混乱,自驾车将会简单。但事实并非如此。为了避免严重的伤害,AI 必须考虑许多变量——速度限制、交通情况和路上的障碍物(例如分心的人)。AI 需要能够检测这些障碍物,并在遇到时采取适当的行动。

幸运的是,我们的应用并没有那么复杂。更幸运的是,我们将使用锡罐而不是人类。我们将建立一个模型,用于检测迷你自动驾驶汽车前方的障碍物。如果障碍物过于接近,汽车应该停下,否则前进

到头来,这是一个二分类问题。为了解决它,我们将:

  • 使用强度阈值创建基准

  • 使用 PyTorch 构建 CNN

  • 使用准确率、精确率和召回率评估模型

  • 使用 SHAP 解释模型

我们将看到模型不仅表现良好,而且其预测方式也似乎合理。在此过程中,我们将讨论 Python 代码,你可以在GitHub上找到完整的项目。

导入和数据集

# Imports
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

import glob 
import random 

from PIL import Image
import cv2

import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
from torchvision import transforms
from torch.utils.data import DataLoader

import shap
from sklearn import metrics
from sklearn.metrics import precision_recall_fscore_support as score
from sklearn.metrics import ConfusionMatrixDisplay as cmd

在图 1 中,你可以看到我们数据集中图像的示例。这些图像的尺寸都是 224 x 224。如果没有黑色罐子或者罐子距离较远,图像被分类为 GO。如果罐子过于接近,图像被分类为 STOP。你可以在Kaggle上找到完整的数据集。

图 1:示例图像(来源:作者)

我们使用下面的代码显示上述图像。注意图像的名称。它总是以一个数字开头。这是目标变量。我们用 0 表示 GO,用 1 表示 STOP。

# Paths of example images
ex_paths = ["../../data/object_detection/0_b812cd70-4eff-11ed-9b15-f602a686e36d.jpg",
          "../../data/object_detection/0_d1edcc80-4ef6-11ed-8ddf-a46bb6070c92.jpg",
          "../../data/object_detection/1_cb171726-4ef7-11ed-8ddf-a46bb6070c92.jpg"]

# Plot example images
fig, ax = plt.subplots(1, 3, figsize=(15, 5))
fig.set_facecolor('white')

for i, path in enumerate(ex_paths):

    # Load image
    img =  Image.open(path)

    # Get target
    name = path.split("/")[-1]
    target = int(name.split("_")[0])

    # Plot image
    ax[i].imshow(img)
    ax[i].axis("off")

    # Set title
    title = ["GO","STOP"][target]
    ax[i].set_title(title,size=20)

基准

在建模之前,值得创建一个基准。这可以提供一些对我们问题的见解。更重要的是,它为我们提供了一个比较模型结果的标准。我们更复杂的深度学习模型应该会优于简单的基准。

在图 1 中,我们可以看到锡罐比周围环境更暗。我们将在创建基准时利用这一点。即,如果图像中有许多暗像素,我们将其分类为 STOP。达到这一点需要几个步骤。对于每个图像,我们将:

  1. 进行灰度化,使每个像素的值在 0(黑色)和 255(白色)之间。

  2. 使用截止值,将每个像素转换为二进制值——深色像素为 1,浅色像素为 0。

  3. 计算平均强度——暗像素的百分比

  4. 如果平均强度超过某个百分比,我们将图像分类为 STOP。

合并步骤 1 和 2 是一种图像数据的特征工程方法,称为强度阈值。你可以在这篇文章中阅读更多关于此及其他特征工程方法的信息:

## 图像数据的特征工程

裁剪、灰度化、RGB 通道、强度阈值、边缘检测和颜色滤镜

towardsdatascience.com

我们使用下面的函数应用强度阈值。缩放后,一个像素将具有 0(黑色)或 1(白色)的值。对于我们的应用,颠倒这一点是有意义的。也就是说,原本深色的像素将被赋值为 1。

def threshold(img,cutoff,invert=False):
    """Apply intesity thresholding"""

    img = np.array(img)

    # Greyscale image
    img = cv2.cvtColor(img,cv2.COLOR_RGB2GRAY)

    #Apply cutoff
    img[img>cutoff] = 255 #white
    img[img<=cutoff] = 0 #black

    # Scale to 0-1    
    img = img/255

    # Invert image so black = 1
    if invert: 
        img = 1 - img

    return img

在图 2 中,你可以看到我们应用强度阈值的一些示例。我们可以调整截止值。较小的截止值意味着我们包括的背景噪声更少。缺点是我们捕捉到的锡罐较少。在这种情况下,我们将使用截止值 60。

图 2:使用强度阈值的特征工程(来源:作者)

我们加载了所有的图像(第 5 行)和目标变量(第 6 行)。然后,我们对这些图像应用强度阈值(第 9 行)。请注意,我们设置了invert=True。最后,我们计算每个处理过的图像的平均强度(第 10 行)。最终,每个图像由一个单一的数字——平均强度来表示。这可以解释为暗像素的百分比

# Load paths
paths = glob.glob("../../data/object_detection/*.jpg")

# Load images and targets
images = [Image.open(path) for path in paths]
target = [int(path.split("/")[-1].split("_")[0]) for path in paths]

# Apply thresholding and get intensity
thresh_img = [threshold(img,60,True) for img in images]
intensity = [np.average(img) for img in thresh_img]

图 3 给出了所有标记为 GO 和 STOP 的图像的平均强度箱线图。通常,我们可以看到 STOP 的值更高。这是有道理的——罐子离得更近,因此我们会有更多的暗像素。红线在 6.5% 处。这似乎能很好地分离图像类别。

图 3:目标变量的平均强度(来源:作者)

# Split data into go and stop images
go_data = [intensity[i] for i in range(len(target)) if target[i]==0]
stop_data = [intensity[i] for i in range(len(target)) if target[i]==1]
data= [go_data,stop_data]

fig = plt.figure(figsize=(5,5))

# Plot boxplot
plt.boxplot(data)
plt.hlines(y=0.065,xmin=0.5,xmax=2.5,color='r')
plt.xticks([1,2],['GO','STOP'])
plt.ylabel("Average Intensity",size=15)

我们使用 6.5% 作为预测的截断值(第 2 行)。即如果暗像素的百分比超过 6.5%,则预测为 STOP(1),否则预测为 GO(0)。其余的代码用于评估这些预测。

# Predict using average intensity
prediction = [1 if i>0.065 else 0 for i in intensity]

# Evaluate
acc = metrics.accuracy_score(target,prediction)
prec,rec,_,_ = score(target, prediction,average='macro')

print('Accuracy: {}'.format(round(acc,4)))
print('Precision: {}'.format(round(prec,4)))
print('Recall: {}'.format(round(rec,4)))

# Plot confusion matrix
cm = metrics.confusion_matrix(target, prediction)
cm_display = cmd(cm, display_labels = ['GO', 'STOP'])

cm_display.plot()

最终,我们的准确率为 82%,精确率为 77.1%,召回率为 82.96%。不错!在混淆矩阵中,我们可以看到大多数错误是由于假阳性。这些是被预测为 STOP 的图像,而实际上我们应该 GO。这对应于图 3 的箱线图。查看 GO 强度值在红线以上的长尾。这可能是由于背景像素增加了图像中的暗像素数量。

图 4:基准预测的混淆矩阵(来源:作者)

卷积神经网络

如果一辆 AI 汽车的准确率只有 82%,你可能会有点担心。那么我们来看看更复杂的解决方案。

加载数据集

我们首先定义ImageDataset类。这用于加载我们的图像和目标变量。作为参数,我们需要传入所有图像路径的列表和用于转换图像的方法。我们的目标变量将是张量——[1,0] 代表 GO 和 [0,1] 代表 STOP。

class ImageDataset(torch.utils.data.Dataset):
    def __init__(self, paths, transform):

        self.paths = paths
        self.transform = transform

    def __getitem__(self, idx):
        """Get image and target (x, y) coordinates"""

        # Read image
        path = self.paths[idx]
        image = cv2.imread(path, cv2.IMREAD_COLOR)
        image = Image.fromarray(image)

        # Transform image
        image = self.transform(image)

        # Get target
        target = path.split("/")[-1].split("_")[0]
        target = [[1,0],[0,1]][int(target)]

        target = torch.Tensor(target)

        return image, target

    def __len__(self):
        return len(self.paths)

我们将使用常见的图像转换。为了帮助创建一个更强大的模型,我们将对颜色进行抖动(第 2 行)。这将随机改变图像的亮度、对比度、饱和度和色调。我们还会对像素值进行归一化(第 4 行)。这将帮助模型收敛。

TRANSFORMS = transforms.Compose([
    transforms.ColorJitter(0.2, 0.2, 0.2, 0.2),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])

我们加载了所有图像路径(第 1 行)并随机打乱它们(第 4 行)。然后我们为训练数据(第 8 行)和验证数据(第 9 行)创建ImageDataset对象。为此我们使用了 80/20 的划分(第 7 行)。最终,我们将在训练集中拥有3,892张图像,在验证集中拥有974张图像。

paths = glob.glob("../../data/object_detection/*.jpg")

# Shuffle the paths
random.shuffle(paths)

# Create a datasets for training and validation
split = int(0.8 * len(paths))
train_data = ImageDataset(paths[:split], TRANSFORMS)
valid_data = ImageDataset(paths[split:], TRANSFORMS)

此时,实际上还没有数据加载到内存中。在我们能够使用数据训练 PyTorch 模型之前,我们需要创建DataLoader对象。对于train_loader,我们设置了batch_size=128。这允许我们迭代所有训练图像,每次加载 128 张。对于验证图像,我们将批处理大小设置为验证集的完整长度。这允许我们一次加载所有 974 张图像。

# Prepare data for Pytorch model
train_loader = DataLoader(train_data, batch_size=128, shuffle=True)
valid_loader = DataLoader(valid_data, batch_size=valid_data.__len__())

模型架构

接下来,我们定义我们的 CNN 架构。你可以在图 5 中看到这个架构的图示。我们从 224x224x3 的图像张量开始。我们有 3 个卷积层和最大池化层。这将我们缩减到 28x28x64 的张量。接下来是一个 drop-out 层和两个全连接层。我们对所有隐藏层使用 ReLu 激活函数。对于输出节点,我们使用 sigmoid 函数。这是为了使我们的预测值在 0 和 1 之间。

图 5:CNN 架构(来源:作者)

我们在下面的Net类中捕捉了这种架构。需要指出的一点是使用了nn.Sequential()函数。必须使用这种定义 PyTorch 模型的方法,否则 SHAP 包将无法工作。

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()

        # Convolutional layers
        self.conv_layers = nn.Sequential(
            # Sees 224x224x3 image tensor
            nn.Conv2d(3, #RGB channels
                            16, #number of kernels
                            3, #size of kernels
                            padding=1), 
            nn.MaxPool2d(2),
            nn.ReLU(),

            # Sees 112x112x16 tensor
            nn.Conv2d(16, 32, 3, padding=1),
            nn.MaxPool2d(2),
            nn.ReLU(),

            # Sees 56x56x32 tensor
            nn.Conv2d(32, 64, 3, padding=1),
            nn.MaxPool2d(2),
            nn.ReLU()
        )

        # Fully connected layers
        self.fc_layers = nn.Sequential(
            # Sees flattened 28 * 28 * 64 tensor
            nn.Dropout(0.25),
            nn.Linear(64 * 28 * 28, 500),
            nn.ReLU(),
            nn.Linear(500, 2),
            nn.Sigmoid()
        )

    def forward(self, x):
        x = self.conv_layers(x)
        x = x.view(-1, 64 * 28 * 28)
        x = self.fc_layers(x)
        return x

我们创建一个模型对象(第 2 行)。我们将其移动到 GPU 上(第 6–7 行)。我使用的是苹果 M1 笔记本电脑。你需要设置适合你机器的设备。

# create a complete CNN
model = Net()
print(model)

# move tensors to GPU if available
device = torch.device('mps')
model.to(device)

我们定义我们的损失函数(第 2 行)。由于我们的目标变量是二元的,我们将使用二元交叉熵损失。最后,我们使用 Adam 作为我们的优化器(第 5 行)。这是用来最小化损失的算法。

# specify loss function (binary cross-entropy)
criterion = nn.BCELoss()

# specify optimizer
optimizer = torch.optim.Adam(model.parameters())

训练模型

现在是有趣的部分!我们训练我们的模型 20 个周期,并选择验证损失最低的那个。模型可以在同一个GitHub Repo中找到。

name = "object_detection_cnn" # Change this to save a new model

# Train the model
min_loss = np.inf
for epoch in range(20):

    model = model.train()
    for images, target in iter(train_loader):

        images = images.to(device)
        target = target.to(device)

        # Zero gradients of parameters
        optimizer.zero_grad()  

        # Execute model to get outputs
        output = model(images)

        # Calculate loss
        loss = criterion(output, target)

        # Run backpropogation to accumulate gradients
        loss.backward()

        # Update model parameters
        optimizer.step()

    # Calculate validation loss
    model = model.eval()

    images, target = next(iter(valid_loader))
    images = images.to(device)
    target = target.to(device)

    output = model(images)
    valid_loss = criterion(output, target)

    print("Epoch: {}, Validation Loss: {}".format(epoch, valid_loss.item()))

    # Save model with lowest loss
    if valid_loss < min_loss:
        print("Saving model")
        torch.save(model, '../../models/{}.pth'.format(name))

        min_loss = valid_loss

需要提到的一点是optimizer.zero_grad()行。这将所有参数的梯度设置为 0。在每次训练迭代中,我们希望使用仅来自该批次的梯度来更新参数。如果不将梯度清零,它们会积累。这意味着我们将使用新批次和旧批次的梯度组合来更新参数。

模型评估

现在让我们看看这个模型的表现如何。我们从加载我们保存的模型开始(第 2 行)。切换到评估模式很重要(第 3 行)。如果我们不这样做,一些模型层(例如 dropout)在推理时会被不正确地使用。

# Load saved model 
model = torch.load('../../models/object_detection_cnn.pth')
model.eval()
model.to(device)

我们从验证集中加载图像和目标变量(第 2 行)。请记住,目标变量是维度为 2 的张量。我们获取每个张量的第二个元素(第 4 行)。这意味着我们现在将有一个二元目标变量——1 表示 STOP,0 表示 GO。

# Get images and targets
images, target = next(iter(valid_loader))
images = images.to(device)
target = [int(t[1]) for t in target]

我们使用模型对验证图像进行预测(第 2 行)。同样,输出将是维度为 2 的张量。我们考虑第二个元素。如果概率超过 0.5,我们预测 STOP,否则预测 GO。

# Get predictions
output=model(images)
prediction = [1 if o[1] > 0.5 else 0 for o in output]

最后,我们使用与评估基准相同的代码将目标预测进行比较。我们现在的准确率为 98.05%,精确率为 97.38%,召回率为 97.5%。相比基准有了显著的提升!在混淆矩阵中,你可以看到错误的来源。

图 6:模型在验证集上的混淆矩阵(来源:作者)

在图 7 中,我们更详细地查看了一些这些错误。第一行显示了一些假阳性。这些是当汽车应该 GO 时被预测为 STOP 的图像。类似地,底行显示了假阴性。

图 7:预测错误的示例(来源:作者)

你可能已经注意到所有障碍物都在相似的距离处。当标记图像时,我们使用了一个截止距离。即当障碍物距离小于这个截止距离时,它被标记为 STOP。上述障碍物都接近这个截止距离。它们可能被错误标记,所以当障碍物接近这个截止距离时,模型可能会“困惑”。

使用 SHAP 解释模型

我们的模型似乎表现良好。通过了解它如何做出这些预测,我们可以更确定它的效果。为此,我们使用 SHAP。如果你对 SHAP 不熟悉,你可能会发现下面的视频很有用。否则,查看我的SHAP 课程 如果你注册我的新闻通讯,你可以获得免费访问权 😃

下面的代码计算并显示了我们在图 1 中看到的 3 个示例图像的 SHAP 值。如果你想了解更多关于这段代码如何工作的细节,请查看文末提到的文章。

# Load saved model 
model = torch.load('../../models/object_detection_cnn.pth')

# Use CPU
device = torch.device('cpu')
model = model.to(device)

#Load 100 images for background
shap_loader = DataLoader(train_data, batch_size=100, shuffle=True)
background, _ = next(iter(shap_loader))
background = background.to(device)

#Create SHAP explainer 
explainer = shap.DeepExplainer(model, background)

# Load test images
test_images = [Image.open(path) for path in ex_paths]
test_images = np.array(test_images)

test_input = [TRANSFORMS(img) for img in test_images]
test_input = torch.stack(test_input).to(device)

# Get SHAP values
shap_values = explainer.shap_values(test_input)

# Reshape shap values and images for plotting
shap_numpy = list(np.array(shap_values).transpose(0,1,3,4,2))
test_numpy = np.array([np.array(img) for img in test_images])

shap.image_plot(shap_numpy, test_numpy,show=False)

你可以在图 8 中看到输出。前两行是标记为 GO 的图像,第三行为标记为 STOP 的图像。我们有目标张量中每个元素的 SHAP 值。第一列是 GO 预测的 SHAP 值,第二列是 STOP 预测的 SHAP 值。

颜色非常重要。蓝色 SHAP 值告诉我们这些像素减少了预测值。换句话说,它们使得模型预测给定标签的可能性降低。类似地,红色 SHAP 值则增加了这种可能性。

图 8:示例图像的 SHAP 值(来源:作者)

为了理解这一点,让我们关注图 8 的右上角。在图 9 中,我们有标记为 GO 的图像以及 GO 预测的 SHAP 值。你可以看到大多数像素是红色的。这些像素增加了该预测的值,从而导致正确的 GO 预测。你还可以看到像素聚集在障碍物截止位置——罐头的位置,其中标签从 GO 更改为 STOP。

图 9:GO 预测和 GO 标签的 SHAP 值。

在图 10 中,我们可以看到标记为 STOP 的图像的 SHAP 值。罐头在 GO 预测中为蓝色,在 STOP 预测中为红色。换句话说,模型使用罐头中的像素来减少 GO 值并增加 STOP 值。这是有道理的!

图 10:STOP 预测的 SHAP 值

这个模型不仅能够准确地进行预测,而且它做出这些预测的方式似乎也很合逻辑。然而,你可能注意到一些背景像素被突出显示了。这没有意义。为什么背景对预测如此重要?当我们移除物体或移动到新位置时,背景可能会发生变化。

原因是模型对训练数据过拟合了。这些物体出现在许多图像中。结果是模型将它们与 STOP/GO 标签关联。在下面的文章中,我们进行类似的分析。我们讨论了如何防止这种过拟合的方法。我们还花更多时间解释 SHAP 代码。

## 使用 SHAP 调试 PyTorch 图像回归模型

使用 DeepShap 来理解和改进支持自动驾驶汽车的模型

towardsdatascience.com

希望你喜欢这篇文章!你可以通过成为我的 推荐会员 😃 来支持我。

[## 使用我的推荐链接加入 Medium — Conor O’Sullivan

作为 Medium 会员,你的一部分会员费会分配给你阅读的作者,你将可以完全访问所有故事…

conorosullyds.medium.com

| Twitter | YouTube | Newsletter — 免费注册以获取 Python SHAP 课程

数据集

JatRacer 图像 (CC0: 公共领域) www.kaggle.com/datasets/conorsully1/jatracer-images

参考资料

stack overflow,为什么我们需要在 PyTorch 中调用 zero_grad()? stackoverflow.com/questions/48001598/why-do-we-need-to-call-zero-grad-in-pytorch

Kenneth Leung如何轻松绘制神经网络架构图towardsdatascience.com/how-to-easily-draw-neural-network-architecture-diagrams-a6b6138ed875

使用 Vision Transformer 进行图像分类

原文:towardsdatascience.com/image-classification-with-vision-transformer-8bfde8e541d4

如何借助基于 Transformer 的模型进行图像分类

Ruben WinastwanTowards Data Science Ruben Winastwan

·发表于 Towards Data Science ·阅读时长 13 分钟·2023 年 4 月 13 日

--

drmakete labUnsplash 上的照片

自 2017 年推出以来,Transformer 已被广泛认可为一种强大的编码器-解码器模型,可以解决几乎所有语言建模任务。

BERT、RoBERTa 和 XLM-RoBERTa 是在语言处理领域使用 Transformer 编码器堆栈作为其架构基础的一些最先进模型的例子。ChatGPT 和 GPT 系列也使用 Transformer 的解码器部分来生成文本。可以肯定地说,几乎所有最先进的自然语言处理模型都在其架构中融入了 Transformer。

Transformer 的表现非常优秀,以至于不把它用于自然语言处理之外的任务(例如计算机视觉)似乎有些浪费。然而,大问题是:我们能否实际将其用于计算机视觉任务?

事实证明,Transformer 也具有应用于计算机视觉任务的良好潜力。在 2020 年,Google Brain 团队推出了一种基于 Transformer 的模型,可以用于解决图像分类任务,称为 Vision Transformer(ViT)。与传统 CNN 在多个图像分类基准上的表现相比,它的表现非常有竞争力。

因此,在本文中,我们将讨论这个模型。具体来说,我们将讨论 ViT 模型如何工作以及如何利用 HuggingFace 库在我们自己的自定义数据集上对其进行微调,以进行图像分类任务。

所以,作为第一步,让我们开始使用本文中将要使用的数据集。

关于数据集

我们将使用一个小吃数据集,该数据集可以从 HuggingFace 的dataset库中轻松访问。该数据集标注为 CC-BY 2.0 许可证,这意味着你可以自由分享和使用它,只要在你的工作中引用数据集来源即可。

我们来瞧一瞧这个数据集:

数据集中图像的子集

我们只需要几行代码就可以加载数据集,如下所示:

!pip install -q datasets

from datasets import load_dataset 

# Load dataset
dataset = load_dataset("Matthijs/snacks")
print(dataset)

# Output
  '''
  DatasetDict({
      train: Dataset({
          features: ['image', 'label'],
          num_rows: 4838
      })
      test: Dataset({
          features: ['image', 'label'],
          num_rows: 952
      })
      validation: Dataset({
          features: ['image', 'label'],
          num_rows: 955
      })
  })''' 

数据集是一个字典对象,由 4898 张训练图像、955 张验证图像和 952 张测试图像组成。

每张图片都有一个标签,属于 20 个小吃类别之一。我们可以通过以下代码检查这 20 种不同的类别:

print(dataset["train"].features['label'].names)

# Output
'''
['apple','banana','cake','candy','carrot','cookie','doughnut','grape',
'hot dog', 'ice cream','juice','muffin','orange','pineapple','popcorn',
'pretzel','salad','strawberry','waffle','watermelon']''' 

我们来创建一个标签与其对应索引之间的映射。

# Mapping from label to index and vice versa
labels = dataset["train"].features["label"].names
num_labels = len(dataset["train"].features["label"].names)
label2id, id2label = dict(), dict()
for i, label in enumerate(labels):
    label2id[label] = i
    id2label[i] = label

print(label2id)
print(id2label)

# Output
'''
{'apple': 0, 'banana': 1, 'cake': 2, 'candy': 3, 'carrot': 4, 'cookie': 5, 'doughnut': 6, 'grape': 7, 'hot dog': 8, 'ice cream': 9, 'juice': 10, 'muffin': 11, 'orange': 12, 'pineapple': 13, 'popcorn': 14, 'pretzel': 15, 'salad': 16, 'strawberry': 17, 'waffle': 18, 'watermelon': 19}
{0: 'apple', 1: 'banana', 2: 'cake', 3: 'candy', 4: 'carrot', 5: 'cookie', 6: 'doughnut', 7: 'grape', 8: 'hot dog', 9: 'ice cream', 10: 'juice', 11: 'muffin', 12: 'orange', 13: 'pineapple', 14: 'popcorn', 15: 'pretzel', 16: 'salad', 17: 'strawberry', 18: 'waffle', 19: 'watermelon'}
'''

在继续之前,我们需要了解的一件重要事情是每张图像的尺寸是不同的。因此,我们需要在将图像输入模型进行微调之前执行一些图像预处理步骤。

现在我们了解了正在使用的数据集,让我们更详细地了解 ViT 架构。

ViT 的工作原理

在 ViT 引入之前,Transformer 模型依赖自注意力机制,这给我们在计算机视觉任务中使用它带来了很大的挑战。

自注意力机制是 Transformer 模型能够区分一个词在不同上下文中语义的原因。例如,BERT 模型能够通过自注意力机制区分词语‘park’在句子‘They park their car in the basement’‘She walks her dog in a park’中的含义。*

但是,自注意力有一个问题:这是一个计算开销大的操作,因为它要求每个标记关注序列中的每个其他标记。

现在,如果我们在图像数据上使用自注意力机制,那么图像中的每个像素都需要关注并与每个其他像素进行比较。问题是,如果我们将像素值增加一个,那么计算成本将会呈二次增长。如果图像分辨率较大,这显然是不可行的。

图片由作者提供

为了解决这个问题,ViT 引入了将输入图像拆分为图像块的概念。每个图像块的尺寸为 16 x 16 像素。假设我们有一张 48 x 48 像素的图像,那么图像块将会像这样:

图片由作者提供

在实际应用中,ViT 有两种选项来将我们的图像拆分成图像块:

  1. 将我们的输入图像(大小为height x width x channel)重塑为一个展平的 2D 图像块序列,大小为no.of patches x (patch_size².channel)。然后,我们将展平的图像块投影到一个基本的线性层中,以获得每个图像块的嵌入表示。

  2. 将我们的输入图像投影到卷积层中,卷积核的大小和步幅等于补丁大小。然后,我们将该卷积层的输出展平。

在对多个数据集测试模型性能后,结果表明第二种方法具有更好的性能。因此,在本文中,我们将使用第二种方法。

让我们用一个简单的例子来演示如何使用卷积层将输入图像拆分成补丁。

import torch
import torch.nn as nn

# Create toy image with dim (batch x channel x width x height)
toy_img = torch.rand(1, 3, 48, 48)

# Define conv layer parameters
num_channels = 3
hidden_size = 768 #or emb_dimension
patch_size = 16

# Conv 2D layer
projection = nn.Conv2d(num_channels, hidden_size, kernel_size=patch_size, 
             stride=patch_size)

# Forward pass toy img
out_projection = projection(toy_img)

print(f'Original image size: {toy_img.size()}')
print(f'Size after projection: {out_projection.size()}')

# Output
'''
Original image size: torch.Size([1, 3, 48, 48])
Size after projection: torch.Size([1, 768, 3, 3])
'''

模型接下来会将补丁展平,并按顺序排列,如下图所示:

作者提供的图片

我们可以使用以下代码进行展平处理:

# Flatten the output after projection with Conv2D layer

patch_embeddings = out_projection.flatten(2).transpose(1, 2)
print(f'Patch embedding size: {patch_embeddings.size()}')

# Output
'''
Patch embedding size: torch.Size([1, 9, 768]) #[batch, no. of patches, emb_dim]
'''

我们在展平处理后得到的基本上是每个补丁的向量嵌入。这类似于许多基于 Transformer 的语言模型中的标记嵌入。

接下来,类似于 BERT,ViT 将在我们补丁序列的第一个位置添加一个特殊的[CLS]向量嵌入。

作者提供的图片

# Define [CLS] token embedding with the same emb dimension as the patches
batch_size = 1
cls_token = nn.Parameter(torch.randn(1, 1, hidden_size))
cls_tokens = cls_token.expand(batch_size, -1, -1)

# Prepend [CLS] token in the beginning of patch embedding
patch_embeddings = torch.cat((cls_tokens, patch_embeddings), dim=1)
print(f'Patch embedding size: {patch_embeddings.size()}')

# Output
'''
Patch embedding size: torch.Size([1, 10, 768]) #[batch, no. of patches+1, emb_dim]
'''

如你所见,通过在补丁嵌入的开头添加[CLS]标记嵌入,序列的长度增加了一个。接下来的最后一步是将位置嵌入添加到我们的补丁序列中。这一步很重要,以便我们的 ViT 模型可以学习补丁的序列顺序。

这个位置嵌入是一个可学习的参数,将在训练过程中由模型更新。

作者提供的图片

# Define position embedding with the same dimension as the patch embedding
position_embeddings = nn.Parameter(torch.randn(batch_size, 10, hidden_size))

# Add position embedding into patch embedding
input_embeddings = patch_embeddings + position_embeddings
print(f'Input embedding size: {input_embeddings.size()}')

# Output
'''
Input embedding size: torch.Size([1, 10, 768]) #[batch, no. of patches+1, emb_dim]
'''

现在,每个补丁的位置信息加上向量嵌入将作为一组 Transformer 编码器的输入。Transformer 编码器的数量取决于你使用的 ViT 模型类型。总体上,有三种类型的 ViT 模型:

  • ViT-base:它具有 12 层,隐藏大小为 768,总参数量为 86M。

  • ViT-large:它具有 24 层,隐藏大小为 1024,总参数量为 307M。

  • ViT-huge:它具有 32 层,隐藏大小为 1280,总参数量为 632M。

在以下代码片段中,假设我们想使用Vit-base。这意味着我们有 12 层 Transformer 编码器:

# Define parameters for ViT-base (example)
num_heads = 12
num_layers = 12

# Define Transformer encoders' stack
transformer_encoder_layer = nn.TransformerEncoderLayer(
           d_model=hidden_size, nhead=num_heads,
           dim_feedforward=int(hidden_size * 4),
           dropout=0.1)
transformer_encoder = nn.TransformerEncoder(
           encoder_layer=transformer_encoder_layer,
           num_layers=num_layers)

# Forward pass
output_embeddings = transformer_encoder(input_embeddings)
print(f' Output embedding size: {output_embeddings.size()}')

# Output
'''
Output embedding size: torch.Size([1, 10, 768])
'''

最后,Transformer 编码器堆叠将输出每个图像补丁的最终向量表示。最终向量的维度对应于我们使用的 ViT 模型的隐藏大小。

作者提供的图片

就是这些了。

我们当然可以从头开始构建和训练自己的 ViT 模型。然而,与其他基于 Transformer 的模型一样,ViT 需要在大量图像数据(14M-300M 图像)上进行训练,以便在未见过的数据上具有良好的泛化能力。

如果我们想在自定义数据集上使用 ViT,最常见的方法是微调预训练模型。最简单的方法是利用 HuggingFace 库。我们只需调用ViTModel.from_pretrained()方法,并将预训练模型的路径作为参数传递即可。HuggingFace 的VitModel()类还将作为我们上述所有步骤的封装器。

!pip install transformers

from transformers import ViTModel

# Load pretrained model
model_checkpoint = 'google/vit-base-patch16-224-in21k'
model = ViTModel.from_pretrained(model_checkpoint, add_pooling_layer=False)

# Example input image
input_img = torch.rand(batch_size, num_channels, 224, 224)

# Forward pass input image
output_embedding = model(input_img)
print(output_embedding)
print(f"Ouput embedding size: {output_embedding['last_hidden_state'].size()}")

# Output
'''
BaseModelOutputWithPooling(last_hidden_state=tensor([[[ 0.0985, -0.2080,  0.0727,  ...,  0.2035,  0.0443, -0.3266],
         [ 0.1899, -0.0641,  0.0996,  ..., -0.0209,  0.1514, -0.3397],
         [ 0.0646, -0.3392,  0.0881,  ..., -0.0044,  0.2018, -0.3038],
         ...,
         [-0.0708, -0.2932, -0.1839,  ...,  0.1035,  0.0922, -0.3241],
         [ 0.0070, -0.3093, -0.0217,  ...,  0.0666,  0.1672, -0.4103],
         [ 0.1723, -0.1037,  0.0317,  ..., -0.0571,  0.0746, -0.2483]]],
       grad_fn=<NativeLayerNormBackward0>), pooler_output=None, hidden_states=None, attentions=None)

Output embedding size: torch.Size([1, 197, 768])
'''

完整 ViT 模型的输出是一个向量嵌入,表示每个图像补丁加上[CLS]标记。其维度为[batch_size, image_patches+1, hidden_size]

要执行图像分类任务,我们遵循与 BERT 模型相同的方法。我们提取[CLS]标记的输出向量嵌入,并通过最终的线性层来确定图像的类别。

作者提供的图片

num_labels = 20

# Define linear classifier layer
classifier = nn.Linear(hidden_size, num_labels) 

# Forward pass on the output embedding of [CLS] token
output_classification = classifier(output_embedding['last_hidden_state'][:, 0, :])
print(f"Output embedding size: {output_classification.size()}")

# Output
'''
Output embedding size: torch.Size([1, 20]) #[batch, no. of labels]
'''

微调实现

在本节中,我们将微调一个在 ImageNet-21K 数据集上进行过预训练的ViT-base模型,该数据集包含约 1400 万张图像和 21,843 个类别。数据集中的每张图像的尺寸为 224 x 224 像素。

首先,我们需要定义预训练模型的检查点路径,并加载必要的库。

import numpy as np
import torch
import cv2
import torch.nn as nn
from transformers import ViTModel, ViTConfig
from torchvision import transforms
from torch.optim import Adam
from torch.utils.data import DataLoader
from tqdm import tqdm

#Pretrained model checkpoint
model_checkpoint = 'google/vit-base-patch16-224-in21k'

图像数据加载器

如前所述,ViT-base 模型已在包含 224 x 224 像素尺寸图像的数据集上进行过预训练。这些图像还根据其每个颜色通道的特定均值和标准差进行了归一化。

因此,在我们将自己的数据集输入 ViT 模型进行微调之前,我们必须首先对图像进行预处理。这包括将每张图像转换为张量,将其调整到适当的尺寸,然后使用与模型预训练数据集相同的均值和标准差值进行归一化。

class ImageDataset(torch.utils.data.Dataset):

  def __init__(self, input_data):

      self.input_data = input_data
      # Transform input data
      self.transform = transforms.Compose([
        transforms.ToTensor(),
        transforms.Resize((224, 224), antialias=True),
        transforms.Normalize(mean=[0.5, 0.5, 0.5], 
                             std=[0.5, 0.5, 0.5])
        ])

  def __len__(self):
      return len(self.input_data)

  def get_images(self, idx):
      return self.transform(self.input_data[idx]['image'])

  def get_labels(self, idx):
      return self.input_data[idx]['label']

  def __getitem__(self, idx):
      # Get input data in a batch
      train_images = self.get_images(idx)
      train_labels = self.get_labels(idx)

      return train_images, train_labels

从上面的图像数据加载器中,我们将获取一批预处理过的图像及其对应的标签。我们可以使用上述图像数据加载器的输出作为微调过程中模型的输入。

模型定义

我们的 ViT 模型架构非常简单。由于我们将微调一个预训练模型,我们可以使用VitModel.from_pretrained()方法,并提供模型的检查点作为参数。

我们还需要在最后添加一个线性层,作为最终的分类器。这个层的输出应该等于数据集中不同标签的数量。

class ViT(nn.Module):

  def __init__(self, config=ViTConfig(), num_labels=20, 
               model_checkpoint='google/vit-base-patch16-224-in21k'):

        super(ViT, self).__init__()

        self.vit = ViTModel.from_pretrained(model_checkpoint, add_pooling_layer=False)
        self.classifier = (
            nn.Linear(config.hidden_size, num_labels) 
        )

  def forward(self, x):

    x = self.vit(x)['last_hidden_state']
    # Use the embedding of [CLS] token
    output = self.classifier(x[:, 0, :])

    return output

上述 ViT 模型为每个图像补丁和[CLS]标记生成最终的向量嵌入。为了对图像进行分类,如上所示,我们提取[CLS]标记的最终向量嵌入,并将其传递给最终的线性层以获得最终的类别预测。

模型微调

现在我们已经定义了模型架构并准备了输入图像用于批处理过程,我们可以开始微调我们的 ViT 模型。训练脚本是一个标准的 Pytorch 训练脚本,如下所示:

def model_train(dataset, epochs, learning_rate, bs):

    use_cuda = torch.cuda.is_available()
    device = torch.device("cuda" if use_cuda else "cpu")

    # Load nodel, loss function, and optimizer
    model = ViT().to(device)
    criterion = nn.CrossEntropyLoss().to(device)
    optimizer = Adam(model.parameters(), lr=learning_rate)

    # Load batch image
    train_dataset = ImageDataset(dataset)
    train_dataloader = DataLoader(train_dataset, num_workers=1, batch_size=bs, shuffle=True)

    # Fine tuning loop
    for i in range(epochs):
        total_acc_train = 0
        total_loss_train = 0.0

        for train_image, train_label in tqdm(train_dataloader):
            output = model(train_image.to(device))
            loss = criterion(output, train_label.to(device))
            acc = (output.argmax(dim=1) == train_label.to(device)).sum().item()
            total_acc_train += acc
            total_loss_train += loss.item()

            loss.backward()
            optimizer.step()
            optimizer.zero_grad()

        print(f'Epochs: {i + 1} | Loss: {total_loss_train / len(train_dataset): .3f} | Accuracy: {total_acc_train / len(train_dataset): .3f}')

    return model

# Hyperparameters
EPOCHS = 10
LEARNING_RATE = 1e-4
BATCH_SIZE = 8

# Train the model
trained_model = model_train(dataset['train'], EPOCHS, LEARNING_RATE, BATCH_SIZE)

由于我们的零食数据集有 20 个不同的类别,因此我们面临的是一个多类分类问题。因此,CrossEntropyLoss()将是合适的损失函数。在上面的示例中,我们训练了模型 10 个周期,学习率设置为 1e-4,批量大小为 8。你可以调整这些超参数以优化模型的性能。

训练模型后,你将得到一个类似下面的输出:

图片由作者提供

模型预测

既然我们已经微调了模型,自然希望将其用于测试数据的预测。为此,首先创建一个函数来封装所有必要的图像预处理步骤和模型推理过程。

def predict(img):

    use_cuda = torch.cuda.is_available()
    device = torch.device("cuda" if use_cuda else "cpu")
    transform = transforms.Compose([
        transforms.ToTensor(),
        transforms.Resize((224, 224)),
        transforms.Normalize(mean=[0.5, 0.5, 0.5], 
                             std=[0.5, 0.5, 0.5])
        ])

    img = transform(img)
    output = trained_model(img.unsqueeze(0).to(device))
    prediction = output.argmax(dim=1).item()

    return id2label[prediction]

正如你所见,上面显示的推理过程中的图像预处理步骤与我们在训练数据上进行的步骤完全相同。然后,我们将变换后的图像作为输入传递给训练好的模型,最后将其预测映射到相应的标签。

如果我们想对测试数据中的特定图像进行预测,我们只需调用上述函数,之后我们会得到预测结果。让我们试试看。

print(predict(dataset['test'][900]['image']))
# Output: waffle

数据集中的测试数据示例

我们的模型正确预测了我们的测试图像。让我们尝试另一张。

print(predict(dataset['test'][250]['image']))
# Output: cookie

数据集中的测试数据示例

我们的模型再次正确地预测了测试数据。通过微调 ViT 模型,我们可以在自定义数据集上获得良好的性能。你也可以对任何自定义数据集执行相同的过程,用于图像分类任务。

结论

在本文中,我们已经看到 Transformer 不仅可以用于语言建模任务,还可以用于计算机视觉任务,在本例中是图像分类。

为了做到这一点,首先将输入图像分解成大小为 16 x 16 像素的补丁。然后,Vision Transformer 模型利用一系列 Transformer 编码器来学习每个图像补丁的向量表示。最后,我们可以使用图像补丁序列开头的[CLS]标记的最终向量表示来预测输入图像的标签。

我希望这篇文章对你开始使用 Vision Transformer 模型有所帮助。与往常一样,你可以在这个笔记本中找到本文中展示的代码实现。

数据集参考

huggingface.co/datasets/Matthijs/snacks

使用预训练扩散模型进行图像合成

原文:towardsdatascience.com/image-composition-with-pre-trained-diffusion-models-772cd01b5022?source=collection_archive---------5-----------------------#2023-07-12

一种提高对预训练文本到图像扩散模型生成图像的控制的方法

Gabriele Sgroi, PhDTowards Data Science Gabriele Sgroi, PhD

·

关注 发表在 Towards Data Science ·8 分钟阅读·2023 年 7 月 12 日

--

使用文章中描述的方法生成的稳定扩散图像。图片由作者提供。

文本到图像的扩散模型在生成符合自然语言描述的逼真图像方面取得了惊人的表现。开源预训练模型的发布,例如稳定扩散,促进了这些技术的民主化。预训练扩散模型使任何人都可以创造出令人惊叹的图像,而不需要大量计算能力或漫长的训练过程。

尽管文本引导的图像生成提供了控制水平,但即使有大量提示,获得具有预定组成的图像仍然很棘手。实际上,标准的文本到图像扩散模型对生成图像中将描绘的各种元素几乎没有控制。

在这篇文章中,我将解释一种基于论文MultiDiffusion: Fusing Diffusion Paths for Controlled Image Generation的最新技术。这种技术使得在由文本引导的扩散模型生成的图像中放置元素的控制更为精确。论文中提出的方法更为通用,还可以用于其他应用,如生成全景图像,但我将在这里限制讨论图像组成性,使用基于区域的文本提示。该方法的主要优点是可以与开箱即用的预训练扩散模型一起使用,无需昂贵的重新训练或微调。

为了补充这篇文章的代码,我准备了一个简单的Colab notebook和一个GitHub 仓库,其中包含了我用于生成本文中图像的代码实现。该代码基于 Hugging Face 的diffusers library中的稳定扩散管道,但只实现了其功能所需的部分,使其更简单易读。

扩散模型

在这一部分中,我将回顾一些关于扩散模型的基本事实。扩散模型是生成模型,通过逆转扩散过程来生成新数据,该过程将数据分布映射到各向同性的高斯分布。更具体地说,给定一个图像,扩散过程包括一系列步骤,每一步都向图像中添加少量高斯噪声。在无限多步的极限下,噪声图像将与从各向同性高斯分布中采样的纯噪声无法区分。

扩散模型的目标是通过尝试猜测扩散过程中的步骤 t-1 处的噪声图像来逆转这一过程,给定步骤 t 处的噪声图像。例如,可以通过训练一个神经网络来预测该步骤添加的噪声,并将其从噪声图像中减去来实现这一目标。

一旦我们训练好这样一个模型,就可以通过从各向同性的高斯分布中采样噪声来生成新图像,并使用模型通过逐渐去除噪声来逆转扩散过程。

扩散模型的目标是学习所有时间步 t 的概率 q(x(t-1)|x(t))。图像来自论文:Denoising Diffusion Probabilistic Models

文本到图像扩散模型反转扩散过程,试图达到与文本提示描述相对应的图像。这通常是通过神经网络完成的,该网络在每一步 t 预测步骤 t-1 的噪声图像,条件不仅是步骤 t 的噪声图像,还包括描述其试图重建的图像的文本提示。

许多图像扩散模型,包括稳定扩散,不是在原始图像空间中操作,而是在一个较小的学习潜空间中操作。通过这种方式,可以在最小质量损失的情况下减少所需的计算资源。潜空间通常通过变分自编码器来学习。潜空间中的扩散过程与之前完全相同,允许从高斯噪声生成新的潜向量。从这些向量中,可以使用变分自编码器的解码器获得新生成的图像。

使用 MultiDiffusion 进行图像组合

现在让我们转向解释如何使用 MultiDiffusion 方法获得可控图像组合。目标是通过预训练的文本到图像扩散模型更好地控制生成图像中的元素。具体而言,给定图像的一般描述(例如封面图像中的客厅),我们希望一系列通过文本提示指定的元素出现在特定位置(例如中心的红色沙发,左侧的盆栽和右上角的画作)。这可以通过提供一组描述所需元素的文本提示和一组基于区域的二进制掩码来实现,该掩码指定了元素必须描绘在其中的位置。例如,下面的图像包含封面图像中图像元素的边界框。

生成封面图像所用的边界框和提示。图像由作者提供。

MultiDiffusion用于可控图像生成的核心思想是将多个扩散过程结合在一起,针对不同指定的提示,以获得在预定区域显示每个提示内容的连贯和平滑图像。与每个提示关联的区域通过与图像相同尺寸的二进制掩码来指定。如果提示必须在该位置描绘,则掩码的像素设为 1,否则设为 0。

更具体地说,我们用 t 表示在潜在空间中运行的扩散过程中的一个通用步骤。给定时间步 t 的噪声潜在向量,模型将预测每个指定文本提示的噪声。从这些预测的噪声中,我们通过从时间步 t 的前一个潜在向量中去除每个预测噪声,获得时间步 t-1 的一组潜在向量(每个提示一个)。为了获得扩散过程下一时间步的输入,我们需要将这些不同的向量组合在一起。这可以通过将每个潜在向量乘以相应的提示掩码,然后按掩码加权取每像素的平均值来完成。按照这个程序,在特定掩码指定的区域内,潜在向量将遵循由相应局部提示引导的扩散过程轨迹。在每一步将潜在向量组合在一起后,再预测噪声,可以确保生成图像的全球一致性以及不同掩码区域之间的平滑过渡。

MultiDiffusion 在扩散过程开始时引入了一个自举阶段,以更好地遵循紧密的掩码。在这些初步步骤中,与不同提示相对应的去噪潜在向量不会被组合在一起,而是与一些对应于常色背景的噪声潜在向量结合在一起。通过这种方式,由于布局通常在扩散过程早期就已经确定,因此可以在模型最初只专注于掩码区域来描绘提示的情况下,获得与指定掩码更好的匹配。

示例

在本节中,我将展示该方法的一些应用。我使用了 HuggingFace 托管的预训练 stable diffusion 2 模型创建了本文中的所有图像,包括封面图像。

正如讨论的那样,该方法的一个直接应用是获得包含在预定义位置生成的元素的图像。

边界框。图片由作者提供。

使用上述边界框生成的图像。图片由作者提供。

该方法允许指定单个元素的风格或其他属性。这可以用于例如在模糊背景上获得清晰的图像。

模糊背景的边界框。图片由作者提供。

使用上述边界框生成的图像。图片由作者提供。

元素的风格也可以非常不同,带来令人惊叹的视觉效果。例如,下面的图像是通过将高质量照片风格与梵高风格的画作混合获得的。

不同风格的边界框。图片由作者提供。

使用上述边界框生成的图像。图像由作者提供。

结论

在这篇文章中,我们探讨了一种结合不同扩散过程的方法,以提高对由文本条件扩散模型生成的图像的控制能力。该方法增强了对图像中元素生成位置的控制,并且能够无缝地结合以不同风格描绘的元素。

描述的程序的主要优点之一是它可以与预训练的文本到图像扩散模型一起使用,而无需进行通常较为昂贵的微调。另一个优势是可控图像生成通过二进制掩码实现,这比更复杂的条件设置更容易指定和处理。

这种技术的主要缺点是,在每个扩散步骤中需要为每个提示进行一次神经网络传递,以预测相应的噪声。幸运的是,这些操作可以批量进行,以减少推断时间开销,但代价是更大的 GPU 内存使用。此外,有时一些提示(特别是仅在图像小部分中指定的提示)会被忽视或覆盖的区域比相应掩码指定的区域要大。虽然可以通过引导步骤来缓解这个问题,但过多的引导步骤可能会显著降低图像的整体质量,因为可以用来协调元素的步骤减少了。

值得一提的是,结合不同扩散过程的想法并不限于本文描述的内容,它还可以用于其他应用,例如论文中描述的全景图像生成MultiDiffusion: Fusing Diffusion Paths for Controlled Image Generation

希望你喜欢这篇文章,如果你想深入了解技术细节,可以查看这个Colab 笔记本GitHub 仓库的代码实现。

使用 Python 的图像滤镜

原文:towardsdatascience.com/image-filters-with-python-3dc223a12624

一个简洁的计算机视觉项目,用于使用 Python 构建图像滤镜

Bharath KTowards Data Science Bharath K

·发布于Towards Data Science ·8 分钟阅读·2023 年 2 月 10 日

--

照片由Pineapple Supply Co.拍摄,来源于Unsplash

图像存在于不同的尺度、对比度、位深和质量中。我们被各种独特和美丽的图像包围,这些图像遍布我们的周围和互联网。操作这些图像可以产生一些有趣的结果,这些结果被用于各种有趣和有用的应用。

在图像处理和计算机视觉中,操作图像是解决不同任务和获得各种项目期望结果的关键组成部分。通过正确处理图像任务,我们可以重新创建一个修改后的图像版本,这对多种计算机视觉和深度学习应用(如数据增强)非常有用。

在本文中,我们将重点开发一个简单的图像滤镜应用程序,主要用于修改特定图像的亮度和对比度。还可以实现并添加到项目中的其他一些显著修改,包括着色器样式、剪贴画、表情符号和其他类似的附加内容。

如果读者不熟悉计算机视觉和 OpenCV,我建议查看我之前的一篇文章,内容是关于 OpenCV 和计算机视觉的全面初学者指南。相关链接如下。我建议在继续阅读本文其余内容之前先查看它。

## OpenCV: 完整的初学者指南掌握计算机视觉基础及代码!

一个教程,包含代码,旨在掌握计算机视觉的所有重要概念及其使用 OpenCV 实现的方法

[towardsdatascience.com

使用 Python 的亮度和对比度调整器的起始代码:

照片由 Jacopo Maia 提供,来自 Unsplash

在本节中,我们将查看一个简单的起始代码,这将帮助我们开始使用 OpenCV 计算机视觉库来修改原始图像的亮度和对比度的基本图像过滤器。为此任务,我们将使用随机图像来测试示例代码并理解其基本工作原理。

为了理解这个测试案例,我使用上面的图像作为测试样本来分析和理解亮度和对比度调整器的工作过程。为了跟进这个项目,我强烈建议下载上述图像并将其存储为 test_image.jpg 在工作目录中。

请注意,你可以用任何其他功能名称存储图像,并使用任何其他格式的图像。唯一的重要步骤是读取图像时提及适当的名称及其格式。第一步是导入 OpenCV 库,如下面的代码块所示,并确保该库完全正常运行。

# Importing the computer vision library
import cv2

一旦导入库并验证其工作,我们可以继续读取我们最近在工作目录中保存的原始图像。只需提及图像名称,cv2.imread 函数就能读取图像。

如果图像未存储在工作目录中,请确保提及特定文件及其图像名称。由于上述图像的原始尺寸为 4608 x 3072,将图像缩小一点并减少尺寸是最佳的。我已将图像调整为 (512, 512) 规模,以便更容易跟踪进度并以稍高的速度执行所需任务。

在下面代码示例的最后一步,我们将定义 alpha 和 gamma 参数,这些参数将分别作为对比度和亮度的调整器。使用这两个参数,我们可以相应地控制这些值。请注意,对比度参数的范围是 0 到 127,而亮度参数的范围是 0 到 100。

# read the input image
image = cv2.imread('test_image.jpg')
image = cv2.resize(image, (512, 512))

# Define the alpha and gamma parameters for brightness and contrast accordingly
alpha = 1.2
gamma = 0.5

一旦我们完成了所有之前的步骤,我们将继续使用 Open CV 库中的“add weighted”函数,这有助于我们计算 alpha 和 gamma(亮度和对比度值)。该函数主要用于通过使用 alpha、beta 和 gamma 值来混合图像。请注意,对于单个图像,我们可以将 beta 设为零,并获得适当的结果。

最后,我们将在计算机视觉窗口中显示修改后的图像,如下面的示例代码块所示。一旦图像显示出来,当用户点击关闭按钮时,我们可以继续关闭窗口。为了进一步理解 Open CV 的一些基本函数,我强烈建议查看我之前提到的文章以获得更多信息。

# Used the added weighting function in OpenCV to compute the assigned values together
final_image = cv2.addWeighted(image, alpha, image, 0, gamma)

# Display the final image
cv2.imshow('Modified Image', final_image)
cv2.waitKey(0)
cv2.destroyAllWindows()

在上述起始代码示例中,参数 alpha 作为对比度,gamma 作为亮度调整器。然而,修改这些参数以适应多种不同的变化可能会稍显不切实际。因此,最好的方法是创建一个 GUI 界面,并为所需的图像滤镜找到最佳值。这个话题将在接下来的部分中进一步讨论。

带有控制器的项目进一步开发:

作者修改后的图像截图

我们可以使用 Open CV 创建一个自定义 GUI,利用控制器优化亮度和对比度参数,并相应调整我们的结果。我们可以为每个属性设置一个滑块,当滑动时,可以通过对每个亮度和对比度变化应用独特的滤镜来创建图像的不同效果。

前几步与上一部分类似,我们将导入 Open CV 库,读取相应的图像并按需调整其大小。读者可以选择适合自己的尺寸。以下是相关的代码片段。

# Import the Open CV library
import cv2

# read the input image
image = cv2.imread('test_image.jpg')
image = cv2.resize(image, (512, 512))

在下一步中,我们将定义亮度和对比度函数,通过这些函数我们可以定义获取 trackbar 位置的函数,以获得当前亮度和对比度元素的位置。加权函数将用于计算这两个参数,以计算这两个组合的综合输出,如下面的代码块所示。

# Creating the control function for the brightness and contrast
def BrightnessContrast(brightness=0):
    brightness = cv2.getTrackbarPos('Brightness',
                                    'Image')

    contrast = cv2.getTrackbarPos('Contrast',
                                  'Image')

    effect = cv2.addWeighted(image, brightness, image, 0, contrast)

    cv2.imshow('Effect', effect)

一旦定义了 trackbar 函数,我们将创建一个命名窗口和一个用于获取所需参数的 trackbar。我们将为亮度和对比度特性分别创建两个独立的 trackbars,如下面的代码块所示。当原始图像窗口和带有图像的 trackbar 窗口都关闭时,wait key 函数将激活,以帮助终止程序。

# Defining the parameters for the Open CV window
cv2.namedWindow('Image')

cv2.imshow('Image', image)

cv2.createTrackbar('Brightness',
                    'Image', 0, 10,
                    BrightnessContrast) 

cv2.createTrackbar('Contrast', 'Image',
                    0, 20,
                    BrightnessContrast)  

BrightnessContrast(0)

cv2.waitKey(0)

我们可以调整亮度和对比度滑块的轨迹条位置,将数字分别从 0 到 10 和 0 到 20 进行更改。通过调整相应的位置可以观察到变化。然而,这个项目仍然有很大的改进空间,以及更高的参数调整尺度。我们可以在 255 的尺度上调整亮度值,在 127 的尺度上调整对比度,以获得更细致的图像。

为了解决这些问题并进一步提升这个项目,我建议访问 Geeks for Geeks 网站,我在本节中使用了一部分代码。强烈推荐读者查看以下链接以进一步阅读和理解这个项目。

结论:

照片由 MailchimpUnsplash 上提供

我结合魔法和科学来创造幻觉。我从事新媒体和互动技术的工作,如人工智能或计算机视觉,并将它们融入我的魔法中。

Marco Tempest

图像在计算机视觉和图像处理中扮演着重要角色。通过修改图像以创建类似或高度过滤的变体,我们可以通过积累更多的数据来解决各种项目,比如数据增强或其他类似任务。

我们还可以通过对特定图像进行改进来实现更理想的效果,通过这种方式,可以执行其他有用的深度学习任务。在本文中,我们探索了通过添加亮度和对比度等特性来修改原始图像的使用。在第一部分中,我们学习了如何利用 alpha 和 gamma 参数作为亮度和对比度参数。

在接下来的部分中,我们开发了一个 GUI 界面,通过它可以操作原始图像以获取其修改版本的副本。所有任务都是在一些基本的计算机视觉和图像处理知识及库的基础上完成的。

如果你想在我的文章发布时第一时间得到通知,可以查看以下链接以订阅电子邮件推荐。如果你希望支持其他作者和我,请订阅以下链接。

[## 使用我的推荐链接加入 Medium - Bharath K

阅读 Bharath K(以及 Medium 上成千上万的其他作者)的每一个故事。您的会员费用将直接支持…

bharath-k1297.medium.com

如果你对本文中的各个点有任何疑问,请在下方评论中告诉我。我会尽快回复你。

查看我与本文主题相关的其他文章,你可能也会喜欢阅读!

前往数据科学 [## Jupyter Notebooks 的终极替代方案

讨论 Jupyter Notebooks 在数据科学项目中的一个优秀替代选项

前往数据科学 [## 阅读七篇最佳研究论文,启动深度学习项目

七篇经得起时间考验的最佳研究论文,将帮助你创建出色的项目

前往数据科学 [## 使用 Python 开发自己的拼写检查工具包

使用 Python 创建一个有效的拼写检查应用程序

前往数据科学

感谢大家一直看到最后。希望你们喜欢阅读这篇文章。祝大家有美好的一天!

使用 ChatGPT 生成图像的代码

原文:towardsdatascience.com/image-generation-with-chatgpt-68c98a061bec

如果图像仅仅是像素值的矩阵,那么 ChatGPT 能否编写代码生成对应于有意义图像的矩阵呢?

Jamshaid Shahir, Ph.DTowards Data Science Jamshaid Shahir, Ph.D

·发布于Towards Data Science ·阅读时间 8 分钟·2023 年 2 月 10 日

--

照片由Jonathan Kemper提供,来自Unsplash

像许多人一样,我发现自己被 OpenAI 的 ChatGPT 深深吸引,并将其用于各种活动。从调试代码这样的严肃任务,到写诗和短篇故事这样的创意应用,探索 ChatGPT 的功能非常有趣。同时,查看它的不足之处也很有启发性。作为一个语言模型,它不能像 Midjourney AI 或 DALL-E 那样生成直接的图像,因为它并未在大量图像上进行训练,而是训练于文本。然而,从数学的角度来看,图像只是二维(或在彩色图像的情况下,即 RGB 的三维)数组,你可以让它编写代码来生成图像。因此,我要求 ChatGPT 生成对应于图像的数组。

作者提供的图片

没有修改,我将这段代码复制并粘贴到 Jupyter Notebook 中运行,确实得到了一个白色方块(尽管不完全在中间):

作者提供的图片(代码由 ChatGPT 编写)

因此,从技术上讲,它可以生成图像。然而,我想看看它是否能生成更复杂的东西,比如篮球。

作者提供的图片

和上次一样,我直接在 Jupyter Notebook 中运行了这段代码。

作者提供的图片

如你所见,ChatGPT 显然没有生成一个篮球,而是一个模糊的物体,给人一种 3D 球体的错觉。然后我给了它一些额外的指示,指出应该有黑色线条贯穿其中(尽管事后看来,我可能应该指定黑色的“曲线”)。

作者提供的图片

作者提供的图片

这一次,我只得到了一个纯黑色的方块。

作者提供的图片

作者提供的图片

我们已经接近目标,因为现在圆圈里有一条粗黑线,但它仍然离篮球相差甚远(如果你问我,它几乎看起来像一个日本饭团)。

作者提供的图片

作者提供的图片

现在我们只是有一条细细的黑色水平线穿过我们的圆圈。为了使其更像一个真正的篮球,我们需要一些黑色的曲线贯穿其中。

作者提供的图片

这一次,当我运行代码时,我实际上得到了一个IndexError,我要求 ChatGPT 进行调试并相应修订

---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In [7], line 14
     11         image[y.astype(int) + i, x] = 0
     12     return image
---> 14 basketball = basketball_image()
     15 plt.imshow(basketball, cmap='gray')
     16 plt.show()

Cell In [7], line 11, in basketball_image()
      9 y[y < 64] = 64
     10 for i in range(0, 256, 8):
---> 11     image[y.astype(int) + i, x] = 0
     12 return image

IndexError: index 256 is out of bounds for axis 0 with size 256

当我将这个错误报告给 ChatGPT 时,它提供了一个可能的解释和代码重写:

作者提供的图片

这一次,当运行 ChatGPT 的代码时,我遇到了ValueError:

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In [9], line 14
     11                 image[y.astype(int) + i, j] = 0
     12     return image
---> 14 basketball_image()

Cell In [9], line 10, in basketball_image()
      8 for i in range(0, 256, 8):
      9     for j in range(256):
---> 10         if y[j].astype(int) + i < 256:
     11             image[y.astype(int) + i, j] = 0
     12 return image

ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

我再次将错误报告给 ChatGPT,它解释说是将一个数组与一个整数进行比较,并提出了以下修订,但仍然导致了另一个IndexError

作者提供的图片

---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In [10], line 16
     13                 image[y[j].astype(int) - i, j] = 0
     14     return image
---> 16 basketball = basketball_image()
     17 plt.imshow(basketball, cmap='gray')
     18 plt.show()

Cell In [10], line 11, in basketball_image()
      9 for j in range(256):
     10     if y[j].astype(int) + i < 256:
---> 11         image[y[j].astype(int) + i, j] = 0
     12     if y[j].astype(int) - i >= 0:
     13         image[y[j].astype(int) - i, j] = 0

IndexError: index -9223372036854775808 is out of bounds for axis 0 with size 256

作者提供的图片

结果证明第三次终于成功了,因为我没有收到任何错误。不过,我没有得到任何看起来像篮球的东西,甚至连球的形状都没有(至少知道要画曲线):

作者提供的图片

我要求再试一次,这次它尝试使用正弦函数生成图像。

不幸的是,这只给了我一个黑色方块:

作者提供的图片

我尝试给它一些鼓励,但经过这么多次失败和几次错误后,它决定放弃了:

作者提供的图片

对于我的最终练习,我要求它生成一个笑脸,并且加了个要求,即生成一个 RGB 图像,这样它就需要生成一个 3D 数组而不是 2D。

作者提供的图片

作者提供的图片

作者提供的图片

第一次尝试显然很糟糕。实际上,chatGPT 还需要多尝试八次才能最终得到一些连贯的结果。为了简洁起见,我将展示它绘制的前三个最佳图像及其对应的代码。

作者提供的图像

作者提供的图像

我一直强调的主要点是画一个大圆圈,在圆圈内画两个较小的圆圈来表示眼睛,并画一个竖直的半圆来表示嘴巴。

生成下一个图像的代码片段过长,无法显示完整的屏幕截图,因此我将代码单独粘贴。

import matplotlib.pyplot as plt
import numpy as np

def smiley_face(size=512):
    # Create an empty image with the given size
    image = np.ones((size, size, 3), dtype=np.uint8) * 255

    # Create the circle for the face
    center = (size // 2, size // 2)
    radius = size // 2 - 10
    for i in range(size):
        for j in range(size):
            if (i - center[0]) ** 2 + (j - center[1]) ** 2 < radius ** 2:
                image[i, j, :] = [255, 255, 102]

    # Create the circle for the left eye
    eye_radius = size // 8
    eye_center = (size // 2 - size // 6, size // 2 - size // 6)
    for i in range(size):
        for j in range(size):
            if (i - eye_center[0]) ** 2 + (j - eye_center[1]) ** 2 < eye_radius ** 2:
                image[i, j, :] = [0, 0, 0]

    # Create the circle for the right eye
    eye_center = (size // 2 + size // 6, size // 2 - size // 6)
    for i in range(size):
        for j in range(size):
            if (i - eye_center[0]) ** 2 + (j - eye_center[1]) ** 2 < eye_radius ** 2:
                image[i, j, :] = [0, 0, 0]

    # Create the smile
    smile_center = (size // 2, size // 2 + size // 4)
    smile_radius = size // 4
    for i in range(size):
        for j in range(size):
            if (i - smile_center[0]) ** 2 + (j - smile_center[1]) ** 2 < smile_radius ** 2 and j > smile_center[1]:
                image[i, j, :] = [255, 0, 0]

    return image

smiley = smiley_face()
plt.imshow(smiley)
plt.axis('off')
plt.show()

作者提供的图像

这是离笑脸最接近的图像,除了某种原因,它被旋转到了侧面。经过两个额外的提示,它最终生成了这个图像:

作者提供的图像

作者提供的图像

到那时,经过 8 次尝试,我宣布这相比篮球已经算是成功了!

结论

总之,ChatGPT 具备使用 Python 的numpy库生成对应有意义图像的能力,但存在局限性。尽管缺乏训练数据,ChatGPT 仍能够生成简单的图像,如笑脸。然而,它在生成复杂图像如篮球时遇到困难,需要多次迭代才能生成笑脸的代码。随着进一步的发展,ChatGPT 可能能够在较少的帮助下生成基础图像。未来,我们可能会看到 ChatGPT 与 AI 图像生成器的结合。在我的下一篇文章中,我将讨论如何利用 ChatGPT 为 AI 图像生成器提供提示。

如果你喜欢这篇文章并且是 Medium 的新用户,可以考虑成为会员。如果你通过这个推荐链接加入,我将从你的会员费中获得一部分,而你可以享受 Medium 提供的全部内容,且无需额外费用。

[## Jamshaid Shahir - Medium

阅读来自 Jamshaid Shahir 在 Medium 上的文章。计算生物学博士生。喜欢通过数据探索世界……

medium.com](https://medium.com/@jashahir?source=post_page-----68c98a061bec--------------------------------)

医学数据集的图像配准

原文:towardsdatascience.com/image-registration-for-medical-datasets-ee605ff8eb2e

从 SimpleElastix 到空间变换网络

Charlie O'NeillTowards Data Science Charlie O'Neill

·发布于 Towards Data Science ·阅读时间 31 分钟·2023 年 2 月 22 日

--

图片由 Michael Dziedzic 提供,来源于 Unsplash

介绍

图像配准是图像处理中的一项基础任务,涉及将两个或多个图像对齐到一个共同的坐标系统中。通过这样做,图像中的对应像素表示现实世界中的同源点,从而使图像的比较和分析成为可能。图像配准的一个常见应用是在医学成像中,其中对同一患者进行多次扫描或拍摄,由于时间、位置或其他因素的不同而产生变化。配准这些图像可以揭示出可能指示疾病进展或治疗效果的微妙变化或模式。

图像配准涉及寻找一种空间变换,将一个图像中的点映射到另一个图像中的对应点,以便可以将图像重叠在一起。空间变换通常由一组控制点参数化,这些控制点用于将一个图像扭曲以匹配另一个图像。配准的质量通过相似度度量来衡量,该度量量化了图像之间的对应程度。

近年来,由于先进成像技术的出现、计算能力的提升以及对更准确和高效医学图像分析的需求,医学图像配准引起了越来越多的关注。图像配准已经成为广泛医学图像分析任务的前提条件,包括解剖结构的分割、计算机辅助诊断、疾病进展监测、外科干预和治疗规划。

尽管大量研究集中在开发图像配准算法上,但对这些算法的可访问性、互操作性和扩展性关注较少。科学源代码通常未公开,因未考虑其他研究人员的需求而难以使用,或缺乏适当的文档。这限制了图像配准算法的采用和使用,阻碍了科学进步和可重复性。

为了解决这些挑战,开发了几个开源医学图像配准库,其中 SimpleElastix 是最受欢迎的之一。SimpleElastix 是 SimpleITK 的扩展,SimpleITK 是一个开源医学图像分析库,允许用户完全在 Python、Java、R、Octave、Ruby、Lua、Tcl 和 C# 中配置和运行 Elastix 配准算法。SimpleElastix 提供了一个简单的参数接口、模块化架构和多种变换、度量和优化器,使其易于使用且计算高效。它还提供了一系列功能,如随机采样、多线程和代码优化,以加快配准速度,而不牺牲鲁棒性。

在这里,我将深入探讨使用 SimpleElastix 进行图像配准的过程,重点介绍注册来自地理萎缩患者的视网膜图像的具体示例。我还将提供实施这一配准过程的逐步指南,并探讨其他技术,如光流和空间变换网络。希望这能让你更好地理解医学成像中图像配准的重要性以及实施它的工具。

设置

任务是处理来自地理萎缩患者(眼病的一种)的视网膜图像,并将这些图像进行患者间注册,即仅将来自同一患者的图像注册到该患者。为了解释一下,地理萎缩(GA)特征是视网膜色素上皮细胞的丧失,这些细胞负责支持和滋养黄斑中的视网膜感光细胞。视网膜色素上皮细胞的丧失会导致黄斑中出现一个或多个萎缩区或“孔洞”,这可能导致中心视力丧失,影响个人进行日常活动,如阅读、驾驶和面孔识别。你将在下面的视网膜图像中注意到这些萎缩区域。

你可以从这个 Kaggle 数据集中获取与代码一起使用的图像。首先,我们需要导入适当的模块。

from pathlib import Path
import matplotlib.pyplot as plt
from typing import List
import numpy as np
import seaborn as sns
import os
import cv2
import pandas as pd
from tqdm.notebook import tqdm
from skimage.registration import optical_flow_tvl1, optical_flow_ilk
from skimage.transform import warp
from skimage.color import rgb2gray
from skimage.metrics import structural_similarity as ssim
from skimage.metrics import normalized_root_mse as nrmse

接下来,我们编写一个函数来处理图像的检索。由于我们只想将视网膜图像注册到同一只眼睛,我们需要指定要加载的患者和侧别:

def retrieve_images(patient_id = '156518', laterality = 'L', date = None):
    # Set the root directory for the patient data
    root_dir = Path(f'../data/{patient_id}')

    # Get the list of image filenames for the left eye
    image_filenames = [f for f in os.listdir(root_dir) if f'{laterality}.png' in f]

    # If we are registering to same visit, only keep files from given date
    if date != None:
        pattern = re.compile(r"\w+_(\d{4}-\d{2}-\d{2})_")
        image_filenames = [file for file in image_filenames if date in file]
    # Read the images into a list
    images = [cv2.imread(str(root_dir / f)) for f in image_filenames]
    # Convert the images to grayscale
    gray_images = [rgb2gray(img) for img in images]
    # Register all images to the first image
    template = gray_images[0]
    # Remove invalid images
    final_images = [x for x in gray_images[1:] if x.shape == template.shape]
    return final_images, template

在评估我们的配准算法时,我们的评估指标将是一个计算注册图像与模板图像之间距离的函数。我们希望能够追踪这些指标中的一些。常见的指标包括:

  • L1 损失,也称为平均绝对误差,测量两张图像之间逐元素差异的平均幅度。它对离群值具有鲁棒性,并对所有像素赋予相等的权重,使其成为图像配准的一个不错选择。

  • 均方根误差(RMSE)是两张图像之间平方差的均值的平方根。它对较大的差异赋予更多权重,使其对离群值非常敏感。RMSE 常用于图像配准,以测量两张图像之间的总体差异。

  • 归一化互相关 是一种衡量两张图像之间相似度的指标,考虑了它们的强度。它被归一化以确保结果在 -1 和 1 之间,其中 1 表示完全匹配。归一化互相关常用于图像配准,以评估配准质量,特别是在处理强度不同的图像时。

  • 相似度 是衡量两张图像之间重叠程度的指标,考虑了强度和空间信息。常见的用于图像配准的相似度指标包括互信息、归一化互信息和詹森-香农散度。这些指标提供了两张图像之间共享信息的度量,使其非常适合评估图像配准的质量。

以下函数接受一个注册图像的列表以及模板图像,并计算每张图像的上述指标:

def evaluate_registration(template_img: np.ndarray, 
                          registered_imgs: List[np.ndarray]) -> (List[float], List[float], List[float]):
    """
    Evaluate the registration quality of multiple registered images with respect to a template image.
    """
    l1_losses = []
    ncc_values = []
    ssim_values = []

    for registered_img in registered_imgs:
        # Compute L1 loss between the template and registered images
        l1_loss = np.mean(np.abs(template_img - registered_img))
        l1_losses.append(l1_loss)

        # Compute normalized cross-correlation between the template and registered images
        ncc = np.corrcoef(template_img.ravel(), registered_img.ravel())[0,1]
        ncc_values.append(ncc)

        # Compute structural similarity index between the template and registered images
        ssim_value = ssim(template_img, registered_img, data_range=registered_img.max() - registered_img.min())
        ssim_values.append(ssim_value)

    return l1_losses, ncc_values, ssim_values

根据这些损失,最好有某种函数可以根据损失显示最佳和最差注册图像。这在某种程度上类似于在分类任务中查看混淆矩阵的个别示例。

def visualise_registration_results(registered_images, original_images, template, loss_values):
    num_images = min(len(registered_images), 3)
    # Get the indices of the three images with the highest L1 losses
    top_indices = np.argsort(loss_values)[-num_images:]
    # Get the indices of the three images with the lowest L1 losses
    bottom_indices = np.argsort(loss_values)[:num_images]
    # Create the grid figure
    fig, axes = plt.subplots(num_images, 4, figsize=(20, 15))
    fig.subplots_adjust(hspace=0.4, wspace=0.4)
    # Loop through the top three images
    for i, idx in enumerate(top_indices):
        # Plot the original image in the first column of the left section
        ax = axes[i][0]
        ax.imshow(original_images[idx], cmap='gray')
        original_l1 = np.mean(np.abs(template - original_images[idx]))
        ax.set_title("Original Image (L1 Loss: {:.2f})".format(original_l1))
        # Plot the registered image in the second column of the left section
        ax = axes[i][1]
        ax.imshow(registered_images[idx], cmap='gray')
        ax.set_title("Registered Image (L1 Loss: {:.2f})".format(loss_values[idx]))
    # Loop through the bottom three images
    for i, idx in enumerate(bottom_indices):
        # Plot the original image in the first column of the right section
        ax = axes[i][2]
        ax.imshow(original_images[idx], cmap='gray')
        original_l1 = np.mean(np.abs(template - original_images[idx]))
        ax.set_title("Original Image (L1 Loss: {:.2f})".format(original_l1))
        # Plot the registered image in the second column of the right section
        ax = axes[i][3]
        ax.imshow(registered_images[idx], cmap='gray')
        ax.set_title("Registered Image (L1 Loss: {:.2f})".format(loss_values[idx]))
    # Show the grid
    plt.show()

编写一个汇总函数,显示我们的配准算法所取得的整体改进,这可能是个好主意。

def highlight_worse(val, comparison_column, worse_val, better_val):
    color = better_val if val == worse_val else worse_val
    return 'background-color: {}'.format(color)

def style_df(df_dict):
    df = pd.DataFrame(df_dict)
    for column in df.columns:
        comparison_column = 'original' if column == 'registered' else 'registered'
        worse_val = 'red'
        better_val = 'green'
        if column in ['ncc', 'ssim']:
            worse_val, better_val = better_val, worse_val
        df.style.apply(highlight_worse, axis=1, subset=[column], comparison_column=comparison_column, worse_val=worse_val, better_val=better_val)
    return df

def summarise_registration(original_images, registered_images, template):

    # Calculate metrics for original images
    l1_losses, ncc_values, ssim_values = evaluate_registration(template, original_images)
    l1_original, ncc_original, ssim_original = np.mean(l1_losses), np.mean(ncc_values), np.mean(ssim_values)

    # Calculate metrics for registered images
    l1_losses, ncc_values, ssim_values = evaluate_registration(template, registered_images)
    l1_registered, ncc_registered, ssim_registered = np.mean(l1_losses), np.mean(ncc_values), np.mean(ssim_values)

    # Create dataframe
    df_dict = {'original': {'l1': l1_original, 'ncc': ncc_original, 'ssim': ssim_original}, 
               'registered': {'l1': l1_registered, 'ncc': ncc_registered, 'ssim': ssim_registered}}

    return style_df(df_dict)

最后,我们将为任何配准算法编写一个简洁的包装器,以便我们能够轻松地应用和评估它:

class RegistrationAlgorithm:

    def __init__(self, registration_function):
        self.registration_function = registration_function
        self.final_images, self.template = retrieve_images()
        self.registered_images = self.apply_registration()

    def apply_registration(self):
        # Do the registration process
        registered_images = []
        for i, img in enumerate(tqdm(self.final_images)):
            registered = self.registration_function(self.template, img) 
            registered_images.append(registered)
        return registered_images

    def evaluate(self, template_img, registered_imgs):
        l1_losses = []
        ncc_values = []
        ssim_values = []

        for registered_img in registered_imgs:

            # Compute L1 loss between the template and registered images
            l1_loss = np.mean(np.abs(template_img - registered_img))
            l1_losses.append(l1_loss)

            # Compute normalized cross-correlation between the template and registered images
            ncc = np.corrcoef(template_img.ravel(), registered_img.ravel())[0,1]
            ncc_values.append(ncc)

            # Compute structural similarity index between the template and registered images
            ssim_value = ssim(template_img, registered_img, data_range=registered_img.max() - registered_img.min())
            ssim_values.append(ssim_value)

        return l1_losses, ncc_values, ssim_values

探索性数据分析

让我们获取一些图像,看看我们要处理的是什么。

images, template = retrieve_images()

一个好的主意是检查哪些图像与模板图像的差异最大。我们可以重复使用上述函数来实现这一点。让我们计算未注册图像与模板之间的损失,然后查看差异最大(损失最高)的图像。

# calculate various distances
l1_losses, ncc_values, ssim_values = evaluate_registration(template, images)

# plot most and least similar images
visualise_registration_results(images, images, template, l1_losses)

作者提供的图像。

作为比较,这里是模板图像:

plt.imshow(template, cmap="gray");

作者提供的图像。

寻找最佳模板图像

显然,选择第一张眼底图像作为 固定 或“模板”图像可能并不理想。如果第一张图像质量差,或者旋转,或者与大多数需要配准的图像差异很大,这将导致结果不佳、大的仿射变换和高的“死”图像区域。因此,我们需要某种方法来选择模板图像。我们可以有几种不同的想法来实现这一点:

  • 计算每张图像与数据集中所有其他图像的累积 L2 距离,并选择结果最低的那一张。这代表了与所有其他图像“最接近”的图像。

  • 重复上述过程,但这次创建一个累积 L2 距离的直方图。选择最好的 k 张图像,取平均值,并将其作为模板。

让我们从第一个想法开始。这个函数循环遍历每张图像,计算与所有其他图像的聚合 L2 距离。

def calculate_total_rmse(images):
    n = len(images)
    sum_rmse = np.zeros(n)
    for i in range(n):
        for j in range(i+1, n):
            rmse = np.sqrt(np.mean((images[i] - images[j])**2))
            sum_rmse[i] += rmse
            sum_rmse[j] += rmse
    return sum_rmse

patient_id = '123456'
laterality = 'L'
# Set the root directory for the patient data
root_dir = Path(f'../data/{patient_id}')
# Get the list of image filenames for the left eye
image_filenames = [f for f in os.listdir(root_dir) if f'{laterality}.png' in f]
# Read the images into a list
images = [cv2.imread(str(root_dir / f)) for f in image_filenames]
# Convert the images to grayscale
gray_images = [rgb2gray(img) for img in images]
# Remove invalid images
final_images = [x for x in gray_images[1:] if x.shape == (768, 768)]
# Calculate the RMSEs
rmses = calculate_total_rmse(final_images)

让我们看看四张总 RMSE 最低的图像:

images = final_images
sorted_indices = [i[0] for i in sorted(enumerate(rmses), key=lambda x:x[1])]
fig, ax = plt.subplots(2, 2, figsize=(10, 10))
for i in range(4):
    ax[i//2][i%2].imshow(images[sorted_indices[i]], cmap='gray')
    ax[i//2][i%2].set_title("RMSE: {:.2f}".format(rmses[sorted_indices[i]]))
    ax[i//2][i%2].axis("off")
plt.show()

图片由作者提供。

现在让我们尝试第二种方法。首先,看看总 RMSE 的直方图:

# Plot the histogram
sns.set_style("whitegrid")
sns.displot(rmses, kde=False)
plt.show()
plt.rcdefaults()

图片由作者提供。

我们可以选择最好的 15 张图像(所有图像的 RMSE 都低于 10):

plt.rcdefaults()

def get_best_images(images, rmses, num_images=10):
    sorted_indices = sorted(range(len(rmses)), key=lambda x: rmses[x])
    best_indices = sorted_indices[:num_images]
    return [images[i] for i in best_indices]
best_images = get_best_images(images, rmses)
av_img = np.mean(best_images, axis=0)
plt.imshow(av_img, cmap='gray')
plt.show()

图片由作者提供。

显然,我们用来取平均的图像越多,最终图像就会越模糊。

num_images_range = np.linspace(4, 36, 9, dtype=int)
best_images_list = []
for num_images in num_images_range:
    best_images = get_best_images(images, rmses, num_images)
    av_img = np.mean(best_images, axis=0)
    best_images_list.append(av_img)

fig, axs = plt.subplots(3, 3, figsize=(12,12))
for i, av_img in enumerate(best_images_list):
    row, col = i//3, i%3
    axs[row, col].imshow(av_img, cmap='gray')
    axs[row, col].axis('off')
    axs[row, col].set_title(f"Best {num_images_range[i]} images")
plt.show()

让我们选择最好的 8 张图像,并将其作为我们的模板图像。

算法

这是项目中的实际工作马驹:注册算法本身。

刚性

刚性配准是医学图像分析中的一种基本技术,它通过应用平移、旋转和缩放来对齐两张或多张图像。这是一个将图像变换的过程,目的是保持图像中对应点之间的距离不变。刚性配准的目标是找到最佳变换,以最小化图像之间的差异,同时保持基础结构的解剖一致性。刚性配准有多个应用,包括图像融合、图像引导手术和纵向研究,并且是更高级配准技术的关键预处理步骤。

import SimpleITK as sitk
import numpy as np

def rigid(fixed_image, moving_image):
    # Convert the input images to SimpleITK images
    fixed_image = sitk.GetImageFromArray(fixed_image)
    moving_image = sitk.GetImageFromArray(moving_image)
    # Create a rigid registration method and set the initial transform to the identity
    registration_method = sitk.ImageRegistrationMethod()
    initial_transform = sitk.Euler2DTransform()
    initial_transform.SetMatrix(np.eye(2).ravel())
    initial_transform.SetTranslation([0, 0])
    registration_method.SetInitialTransform(initial_transform)
    # Set the number of iterations and the learning rate for the optimization
    registration_method.SetOptimizerAsGradientDescent(learningRate=1.0, numberOfIterations=100)
    # Use mean squared error as the similarity metric
    registration_method.SetMetricAsMeanSquares()
    # Execute the registration
    final_transform = registration_method.Execute(fixed_image, moving_image)
    # Transform the moving image using the final transform
    registered_image = sitk.Resample(moving_image, fixed_image, final_transform, sitk.sitkLinear, 0.0, moving_image.GetPixelIDValue())
    # Convert the registered image back to a Numpy array
    registered_image = sitk.GetArrayFromImage(registered_image)
    return registered_image
opt = RegistrationAlgorithm(rigid)
l1_losses, ncc_values, ssim_values = opt.evaluate(opt.template, opt.registered_images)
print("L1 losses:", f"{np.mean(l1_losses):.2f}")
print("Normalized cross-correlation values:", f"{np.mean(ncc_values):.2f}")
print("Structural similarity index values:", f"{np.mean(ssim_values):.2f}")
L1 losses: 0.14
Normalized cross-correlation values: 0.56
Structural similarity index values: 0.55
images, template = retrieve_images()
summarise_registration(images, opt.registered_images, template)

与原始图像相比,我们的指标较差。让我们看看实际发生了什么:

visualise_registration_results(opt.registered_images, images, template, l1_losses)

图片由作者提供。

有趣。因此,指标通常较差,但这样出现的原因是因为这些指标比较的是移动图像上的大黑色区域。我们可能需要包括与上述相同的指标,但排除完全黑色的像素进行比较。

def evaluate_registration(template_img: np.ndarray, registered_imgs: List[np.ndarray]):
    """
    Evaluate the registration quality of multiple registered images with respect to a template image.
    """
    l1_losses = []
    ncc_values = []
    ssim_values = []
    l1_losses_excl_black = []
    ncc_values_excl_black = []
    ssim_values_excl_black = []

    for i, registered_img in enumerate(registered_imgs):

        # Create mask of non-black pixels in original image
        mask = (registered_img.ravel() != 0.0)

        # Compute L1 loss between the template and registered images
        l1_loss = np.mean(np.abs(template_img - registered_img))
        l1_losses.append(l1_loss)

        # Compute L1 loss between the template and registered images, excluding black pixels
        l1_loss_excl_black = np.mean(np.abs(template_img.ravel()[mask] - registered_img.ravel()[mask]))
        l1_losses_excl_black.append(l1_loss_excl_black)

        # Compute normalized cross-correlation between the template and registered images
        ncc = np.corrcoef(template_img.ravel(), registered_img.ravel())[0,1]
        ncc_values.append(ncc)

        # Compute normalized cross-correlation between the template and registered images, excluding black pixels
        ncc_excl_black = np.corrcoef(template_img.ravel()[mask], registered_img.ravel()[mask])[0,1]
        ncc_values_excl_black.append(ncc_excl_black)

        # Compute structural similarity index between the template and registered images
        ssim_value = ssim(template_img, registered_img, data_range=registered_img.max() - registered_img.min())
        ssim_values.append(ssim_value)

        # Compute structural similarity index between the template and registered images, excluding black pixels
        ssim_value_excl_black = ssim(template_img.ravel()[mask], registered_img.ravel()[mask], 
                                     data_range=registered_img.ravel()[mask].max() - registered_img.ravel()[mask].min())
        ssim_values_excl_black.append(ssim_value_excl_black)

    return l1_losses, ncc_values, ssim_values, l1_losses_excl_black, ncc_values_excl_black, ssim_values_excl_black

def summarise_registration(original_images, registered_images, template):

    # Calculate metrics for original images
    l1_losses, ncc_values, ssim_values, l1_losses_black, ncc_values_black, ssim_values_black = evaluate_registration(template, original_images)
    l1_original, ncc_original, ssim_original = np.mean(l1_losses), np.mean(ncc_values), np.mean(ssim_values)
    l1_black_original, ncc_black_original, ssim_black_original = np.mean(l1_losses_black), np.mean(ncc_values_black), np.mean(ssim_values_black)

    # Calculate metrics for registered images
    l1_losses, ncc_values, ssim_values, l1_losses_black, ncc_values_black, ssim_values_black = evaluate_registration(template, registered_images)
    l1_registered, ncc_registered, ssim_registered = np.mean(l1_losses), np.mean(ncc_values), np.mean(ssim_values)
    l1_black_registered, ncc_black_registered, ssim_black_registered = np.mean(l1_losses_black), np.mean(ncc_values_black), np.mean(ssim_values_black)

    # Create dataframe
    df_dict = {'original': {'l1': l1_original, 'ncc': ncc_original, 'ssim': ssim_original,
                            'l1_excl_black': l1_black_original, 'ncc_excl_black': ncc_black_original,
                            'ssim_excl_black': ssim_black_original}, 
               'registered': {'l1': l1_registered, 'ncc': ncc_registered, 'ssim': ssim_registered,
                              'l1_excl_black': l1_black_registered, 'ncc_excl_black': ncc_black_registered,
                              'ssim_excl_black': ssim_black_registered}}

    return style_df(df_dict)
images, template = retrieve_images()
summarise_registration(images, opt.registered_images, template)

实际上没有显著改善。唯一绝对更好的指标是排除黑色像素的 SSIM。关于为什么会这样的一种理论是,通过排除黑色像素,我们也在排除视网膜,视网膜已经与大多数图像非常对齐,因此“抑制”了对齐良好的图像的指标。

光流

光流是计算机视觉中的一种基本技术,它估计视频序列中两个连续帧之间物体的运动。其假设是物体的像素强度在帧之间保持不变,并且物体的表观运动仅由于其实际运动。光流可以表示为一个 2D 矢量场(u,v),将一个速度矢量分配给图像中的每个像素。

光流场可以通过求解将图像亮度变化与像素运动相关联的方程组来计算。这些方程可以使用不同的方法求解,例如 Lucas-Kanade、Horn-Schunck 或 Farneback,每种方法都有其自身的优点和局限性。一旦计算出光流场,它可以用于通过将一幅图像扭曲以对齐另一幅图像来实现图像配准。

光流具有广泛的应用,包括物体跟踪、运动分析、视频稳定和视频压缩。然而,光流估计对图像噪声、遮挡和大位移非常敏感,这可能导致运动估计中的错误和不准确性。当前的研究集中在提高光流方法的准确性、鲁棒性和效率,以增强其在实际场景中的适用性。

让我们来看看这如何工作:

# --- Load the sequence
images, template = retrieve_images()
image0, image1 = images[0], template

# --- Convert the images to gray level: color is not supported.
#image0 = rgb2gray(image0)
#image1 = rgb2gray(image1)
# --- Compute the optical flow
v, u = optical_flow_tvl1(image0, image1)
# --- Use the estimated optical flow for registration
nr, nc = image0.shape
row_coords, col_coords = np.meshgrid(np.arange(nr), np.arange(nc),
                                     indexing='ij')
image1_warp = warp(image1, np.array([row_coords + v, col_coords + u]),
                   mode='edge')
# build an RGB image with the unregistered sequence
seq_im = np.zeros((nr, nc, 3))
seq_im[..., 0] = image1
seq_im[..., 1] = image0
seq_im[..., 2] = image0
# build an RGB image with the registered sequence
reg_im = np.zeros((nr, nc, 3))
reg_im[..., 0] = image1_warp
reg_im[..., 1] = image0
reg_im[..., 2] = image0
# build an RGB image with the registered sequence
target_im = np.zeros((nr, nc, 3))
target_im[..., 0] = image0
target_im[..., 1] = image0
target_im[..., 2] = image0
# --- Show the result
fig, (ax0, ax1, ax2) = plt.subplots(3, 1, figsize=(5, 10))
ax0.imshow(seq_im)
ax0.set_title("Unregistered sequence")
ax0.set_axis_off()
ax1.imshow(reg_im)
ax1.set_title("Registered sequence")
ax1.set_axis_off()
ax2.imshow(target_im)
ax2.set_title("Target")
ax2.set_axis_off()
fig.tight_layout()

作者提供的图像。

上述代码演示了使用光流进行图像配准。首先,代码加载一系列图像和模板。然后,将图像转换为灰度,并使用 TVL1 算法计算第一幅图像和模板之间的光流。计算出的光流矢量用于将模板图像配准到第一幅图像上。

要实现这一点,代码生成了模板图像的行和列坐标网格,并将光流矢量应用于这些行和列坐标,以获取第一幅图像中的相应位置。然后使用基于样条的图像变形函数,将这些变换后的坐标用于将模板图像扭曲到第一幅图像上。

代码然后生成 RGB 图像,以显示未配准序列、配准序列和目标图像(即第一幅图像)。未配准序列是一个 RGB 图像,其中第一幅图像和模板图像叠加在一起。配准序列是一个 RGB 图像,其中扭曲后的模板图像和第一幅图像叠加在一起。目标图像是一个仅包含第一幅图像的 RGB 图像。

最后,代码使用 Matplotlib 子图显示了三张 RGB 图像。第一个子图显示了未配准的序列,第二个子图显示了已配准的序列,第三个子图显示了目标图像。生成的图提供了未配准和已配准序列的视觉比较,突出了基于光流的配准方法的有效性。

估计的向量场(u,v)也可以通过箭头图进行显示。

# --- Compute the optical flow
v, u = optical_flow_ilk(image0, image1, radius=15)
# --- Compute flow magnitude
norm = np.sqrt(u ** 2 + v ** 2)
# --- Display
fig, (ax0, ax1) = plt.subplots(1, 2, figsize=(8, 4))
# --- Sequence image sample
ax0.imshow(image0, cmap='gray')
ax0.set_title("Sequence image sample")
ax0.set_axis_off()
# --- Quiver plot arguments
nvec = 20  # Number of vectors to be displayed along each image dimension
nl, nc = image0.shape
step = max(nl//nvec, nc//nvec)
y, x = np.mgrid[:nl:step, :nc:step]
u_ = u[::step, ::step]
v_ = v[::step, ::step]
ax1.imshow(norm)
ax1.quiver(x, y, u_, v_, color='r', units='dots',
           angles='xy', scale_units='xy', lw=3)
ax1.set_title("Optical flow magnitude and vector field")
ax1.set_axis_off()
fig.tight_layout()
plt.show()

作者提供的图像。

让我们实现算法。

def optical_flow(template, img):
    # calculate the vector field for optical flow
    v, u = optical_flow_tvl1(template, img)
    # use the estimated optical flow for registration
    nr, nc = template.shape
    row_coords, col_coords = np.meshgrid(np.arange(nr), np.arange(nc),
                                         indexing='ij')
    registered = warp(img, np.array([row_coords + v, col_coords + u]), mode='edge')
    return registered

opt = RegistrationAlgorithm(optical_flow)
images, template = retrieve_images()
summarise_registration(images, opt.registered_images, template).loc[['l1', 'ncc', 'ssim']]

显著提高了性能!让我们可视化一下:

images, template = retrieve_images()
visualise_registration_results(opt.registered_images, images, template, l1_losses)

作者提供的图像。

看起来光流在较难的图像上有些“作弊”,通过完全变形图像来实现。让我们看看是否可以改进这一点。

SimpleElastix

SimpleElastix 是一个开源的多平台软件库,提供了一个简单的接口来执行医学图像配准。图像配准是通过在图像之间找到空间映射来对齐两张或更多张图像的过程。SimpleElastix 提供了广泛的预实现配准组件,包括变换、相似性度量和优化器,这些组件可以轻松组合以创建配准管道。该库支持各种类型的配准,包括刚性、仿射、非刚性和组配准,并允许用户在不同的成像模态中配准图像,如 MRI、CT、PET 和显微镜。

SimpleElastix 的一个关键优点是其易用性。它提供了一个用户友好的高级接口,要求的编码知识很少,并且可以通过 Python 或 C++ 接口使用。此外,该库包括高级功能,如多分辨率优化、正则化和空间约束,这些功能提高了配准的准确性和鲁棒性。SimpleElastix 在医学影像研究和临床实践中被广泛使用,并在许多研究中得到了验证。它是一个有价值的工具,适用于广泛的应用,包括图像引导手术、纵向研究和图像分析。

刚性配准

如上所述,刚性变换能够对齐通过平移和旋转相关的对象。例如,在对齐患者骨骼的图像时,刚性变换通常足以对齐这些结构。尽可能使用简单的变换是有利的,因为这减少了可能的解决方案数量,并且最小化了可能影响配准结果准确性的非刚性局部极小值的风险。这种方法可以看作是在配准过程中融入领域专长的一种手段。

让我们看看单个已配准的图像:

import SimpleITK as sitk
from IPython.display import clear_output
from IPython.display import Image

images, template = retrieve_images()
elastixImageFilter = sitk.ElastixImageFilter()
elastixImageFilter.SetFixedImage(sitk.GetImageFromArray(images[0]))
elastixImageFilter.SetMovingImage(sitk.GetImageFromArray(template))
elastixImageFilter.SetParameterMap(sitk.GetDefaultParameterMap("rigid"))
elastixImageFilter.Execute()
clear_output()
sitk.WriteImage(elastixImageFilter.GetResultImage(), 'test.tif')
# load image with SimpleITK
sitk_image = sitk.ReadImage('test.tif')
# convert to NumPy array
im = sitk.GetArrayFromImage(sitk_image)
plt.imshow(im, cmap='gray');

作者提供的图像。

现在,让我们使用上面的架构来应用和验证刚性注册:

def simple_elastix_rigid(image, template):
    elastixImageFilter = sitk.ElastixImageFilter()
    elastixImageFilter.SetFixedImage(sitk.GetImageFromArray(image))
    elastixImageFilter.SetMovingImage(sitk.GetImageFromArray(template))
    elastixImageFilter.SetParameterMap(sitk.GetDefaultParameterMap("rigid"))
    elastixImageFilter.Execute()
    clear_output()
    sitk.WriteImage(elastixImageFilter.GetResultImage(), 'reg.tif')
    # load image with SimpleITK
    sitk_image = sitk.ReadImage('reg.tif')
    # convert to NumPy array
    registered_img = sitk.GetArrayFromImage(sitk_image)
    # delete the tif file
    os.remove('reg.tif')
    return registered_img
# retrieve images to be registered, and the image to register to
images, template = retrieve_images()

# perform the registration using SimpleElastix
opt = RegistrationAlgorithm(simple_elastix_rigid)

可视化结果:

l1_losses, ncc_values, ssim_values = opt.evaluate(opt.template, opt.registered_images)
visualise_registration_results(opt.registered_images, images, template, l1_losses)

作者提供的图像。

最后,让我们审视这些度量:

images, template = retrieve_images()
summarise_registration(images, opt.registered_images, template)

尽管 L1 损失相似,SimpleElastix 的刚性注册显著改善了 NCC 和结构相似性损失。

仿射注册

非常类似于刚性注册,仿射变换允许我们在旋转和平移之外进行剪切和缩放。通常,仿射注册作为非刚性变换之前的初步预处理步骤使用。

def simple_elastix_affine(image, template):
    elastixImageFilter = sitk.ElastixImageFilter()
    elastixImageFilter.SetFixedImage(sitk.GetImageFromArray(image))
    elastixImageFilter.SetMovingImage(sitk.GetImageFromArray(template))
    elastixImageFilter.SetParameterMap(sitk.GetDefaultParameterMap("affine"))
    elastixImageFilter.Execute()
    clear_output()
    sitk.WriteImage(elastixImageFilter.GetResultImage(), 'reg.tif')
    # load image with SimpleITK
    sitk_image = sitk.ReadImage('reg.tif')
    # convert to NumPy array
    registered_img = sitk.GetArrayFromImage(sitk_image)
    # delete the tif file
    os.remove('reg.tif')
    return registered_img
# retrieve images to be registered, and the image to register to
images, template = retrieve_images()

# perform the registration using SimpleElastix
opt = RegistrationAlgorithm(simple_elastix_affine)
l1_losses, ncc_values, ssim_values = opt.evaluate(opt.template, opt.registered_images)
visualise_registration_results(opt.registered_images, images, template, l1_losses

作者提供的图像。

images, template = retrieve_images()
summarise_registration(images, opt.registered_images, template)

略好于刚性变换。

非刚性注册

非刚性注册技术能够对齐需要局部变形的图像,使其更适合处理患者之间的解剖、生理和病理变化。

为了参数化自由形状变形 (FFD) 场,通常使用 B-splines。FFD 场的注册比简单变换复杂得多。参数空间维度的增加使得解决这个问题具有挑战性,因此推荐使用多分辨率方法。以仿射初始化开始也有助于简化注册。在 SimpleElastix 中,实施多分辨率方法非常简单。

以下代码运行多分辨率仿射初始化,然后应用 B-spline 非刚性注册变换。

def simple_elastix_nonrigid(image, template):

    # Initialise the filter, as well as fixed and moving images
    elastixImageFilter = sitk.ElastixImageFilter()
    elastixImageFilter.SetFixedImage(sitk.GetImageFromArray(image))
    elastixImageFilter.SetMovingImage(sitk.GetImageFromArray(template))

    # Setup the initialisation and transforms 
    parameterMapVector = sitk.VectorOfParameterMap()
    parameterMapVector.append(sitk.GetDefaultParameterMap("affine"))
    parameterMapVector.append(sitk.GetDefaultParameterMap("bspline"))
    elastixImageFilter.SetParameterMap(parameterMapVector)

    # Execute and save
    elastixImageFilter.Execute()
    clear_output()
    sitk.WriteImage(elastixImageFilter.GetResultImage(), 'reg.tif')
    # load image with SimpleITK
    sitk_image = sitk.ReadImage('reg.tif')
    # convert to NumPy array
    registered_img = sitk.GetArrayFromImage(sitk_image)
    # delete the tif file
    os.remove('reg.tif')
    return registered_img
# retrieve images to be registered, and the image to register to
images, template = retrieve_images()

# perform the registration using SimpleElastix
opt = RegistrationAlgorithm(simple_elastix_nonrigid)
l1_losses, ncc_values, ssim_values = opt.evaluate(opt.template, opt.registered_images)
visualise_registration_results(opt.registered_images, images, template, l1_losses)
images, template = retrieve_images()
summarise_registration(images, opt.registered_images, template)

作者提供的图像。

所以 SSIM 比仿射变换稍差,但 NCC 绝对更好。

群体注册

群体注册方法在医学影像中用于解决将一张图像注册到选定参考框架时的不确定性。相反,所有图像同时注册到一个位于群体中心的平均参考框架。该方法使用三维或四维 B-spline 变形模型和一个相似度度量,该度量最小化强度方差,同时确保所有图像的平均变形为零。该方法还可以结合变形的时间平滑性和时间维度上的循环变换,这在解剖运动具有周期性特征的情况下非常有用,例如心脏或呼吸运动。通过使用此方法,消除了对特定参考框架的偏倚,从而实现了图像的更准确和无偏注册。然而,该方法计算量巨大,未进行并行处理,因此在这里为了效率而未作介绍。

2D Voxelmorph 和空间变换网络

import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
import torch.nn.functional as F
import torch.optim as optim
import torchvision
from torchvision import datasets, transforms

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

空间变换网络(STN)是一种神经网络架构,能够学习在空间上变换图像,以提高下游任务的性能。特别地,STN 能够自动学习裁剪、旋转、缩放和扭曲输入图像,以适应当前任务的最佳方式。这是通过学习估计每个输入图像的一组仿射变换参数来实现的,这些参数可用于将图像扭曲成新的配置。

在下面的代码中,STN 作为一个模块实现于一个更大的神经网络中,该网络包括几个卷积层和全连接层。STN 由两个组件组成:定位网络和回归器。

定位网络是一组卷积层,用于从输入图像中提取一组特征。这些特征随后被输入到回归器中,回归器是一组用于估计仿射变换参数的全连接层。在提供的代码中,回归器由两个带 ReLU 激活函数的线性层组成。

STN 模块还包括一个stn方法,该方法接受输入图像并通过双线性插值将学习到的仿射变换应用于图像。stn方法在更大神经网络的前向方法中被调用,用于对变换后的输入进行预测。

总的来说,STN 模块提供了一个强大的工具,用于学习对输入图像进行空间变换,这可以用于提高各种图像处理和计算机视觉任务的性能。

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
        self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
        self.conv2_drop = nn.Dropout2d()
        self.fc1 = nn.Linear(320, 50)
        self.fc2 = nn.Linear(50, 10)

        # Spatial transformer localization-network
        self.localization = nn.Sequential(
            nn.Conv2d(1, 8, kernel_size=7),
            nn.MaxPool2d(2, stride=2),
            nn.ReLU(True),
            nn.Conv2d(8, 10, kernel_size=5),
            nn.MaxPool2d(2, stride=2),
            nn.ReLU(True)
        )
        # Regressor for the 3 * 2 affine matrix
        self.fc_loc = nn.Sequential(
            nn.Linear(10 * 188 * 188, 32),
            nn.ReLU(True),
            nn.Linear(32, 3 * 2)
        )
        # Initialize the weights/bias with identity transformation
        self.fc_loc[2].weight.data.zero_()
        self.fc_loc[2].bias.data.copy_(torch.tensor([1, 0, 0, 0, 1, 0], dtype=torch.float))

    # Spatial transformer network forward function
    def stn(self, x):
        xs = self.localization(x)
        xs = xs.view(-1, 10 * 188 * 188)
        theta = self.fc_loc(xs)
        theta = theta.view(-1, 2, 3)
        grid = F.affine_grid(theta, x.size())
        x = F.grid_sample(x, grid)
        return x
    def forward(self, x):
        # transform the input
        x = self.stn(x)
        return x

model = Net().to(device)

我们还将尝试一种不同的可微分损失,这种损失可能比之前的度量方法更适合图像配准。给定的代码定义了一种名为 voxelmorph 损失的自定义损失函数,用于 2D 图像配准。该损失函数由两个组件组成:重建损失和平滑惩罚。

重建损失衡量源图像与目标图像之间的不同。它计算为两幅图像之间的绝对差的平均值,并按目标权重加权。源图像和目标图像是配准网络的输入图像,其中源图像被变换以对齐目标图像。

平滑惩罚通过惩罚源图像和目标图像之间的空间变化形变来鼓励平滑变换。该惩罚通过计算目标图像在 x 和 y 方向上梯度的绝对差的平均值来计算,并按平滑权重加权。这个惩罚项有助于避免形变场中的急剧变化,这可能导致过拟合并对新图像的泛化能力差。

总体的 voxelmorph 损失是重建损失和平滑惩罚的总和。通过在训练期间使用基于梯度的优化器来优化损失,以提高配准网络的准确性。

Voxelmorph 损失函数因其处理大变形、多模态图像和个体差异的能力而在医学图像配准中被广泛使用。它对于图像的可变形配准尤为有用,其目标是对齐具有显著形状变化的图像。损失函数中的平滑性惩罚项有助于规范化变形场,并提高配准的准确性。

def voxelmorph_loss_2d(source, target, source_weight=1, target_weight=1, smoothness_weight=0.001):
    def gradient(x):
        d_dx = x[:, :-1, :-1] - x[:, 1:, :-1]
        d_dy = x[:, :-1, :-1] - x[:, :-1, 1:]
        return d_dx, d_dy

    def gradient_penalty(x):
        d_dx, d_dy = gradient(x)
        return (d_dx.abs().mean() + d_dy.abs().mean()) * smoothness_weight

    reconstruction_loss = (source - target).abs().mean() * target_weight
    smoothness_penalty = gradient_penalty(target)
    return reconstruction_loss + smoothness_penalty

下面的代码定义了一个 PyTorch 数据集类,名为 FundusDataset,用于加载和预处理用于神经网络的训练图像。数据集类接受训练图像列表和目标图像作为输入,并返回一个图像及其对应的目标图像,以便在训练过程中使用。

class FundusDataset(Dataset):
    def __init__(self, image_list, target_image):
        self.image_list = image_list
        self.target_image = target_image

    def __len__(self):
            return len(self.image_list)

    def __getitem__(self, idx):
        image = self.image_list[idx]
        image = torch.from_numpy(image).float()
        return image, self.target_image

# Load your list of Numpy arrays of training images
training_images, template = retrieve_images()
template_image = torch.from_numpy(template).float()
# Create the dataset
dataset = FundusDataset(training_images, template_image)
# Create the data loader
train_loader = DataLoader(dataset, batch_size=32, shuffle=True)

现在,让我们编写一个简短的训练循环:

optimizer = optim.SGD(model.parameters(), lr=0.05)
criterion = voxelmorph_loss_2d #nn.L1Loss() #nn.MSELoss()

def train(epoch):
    model.train()
    batch_loss = 0
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)
        data = data.unsqueeze(1)
        optimizer.zero_grad()
        output = model(data)
        loss = criterion(output.reshape(output.shape[0], 768, 768), target)
        batch_loss += loss.item()
        loss.backward()
        optimizer.step()
    if epoch % 1 == 0:
        avg_loss = batch_loss / len(train_loader)
        print('Train Epoch: {}, Average Loss: {:.6f}'.format(epoch, avg_loss))
for epoch in range(1, 5 + 1):
    train(epoch)

最后,我们定义了一个名为 convert_image_np 的 Python 函数,将 PyTorch 张量转换为 numpy 图像。该函数以 PyTorch 张量为输入,应用标准归一化程序,通过减去均值并除以标准差值来完成归一化。生成的 numpy 图像随后被裁剪到 0 和 1 之间。

代码接着定义了一个名为 visualize_stn 的函数,用于在训练过程中可视化空间变换网络(STN)层的输出。使用 matplotlib 库中的 subplots() 函数将生成的输入和变换后的 numpy 图像并排绘制。图中左侧显示输入图像,右侧显示对应的变换图像。

def convert_image_np(inp):
    """Convert a Tensor to numpy image."""
    inp = inp.numpy().transpose((1, 2, 0))
    mean = np.array([0.485, 0.456, 0.406])
    std = np.array([0.229, 0.224, 0.225])
    inp = std * inp + mean
    inp = np.clip(inp, 0, 1)
    return inp

# We want to visualize the output of the spatial transformers layer
# after the training, we visualize a batch of input images and
# the corresponding transformed batch using STN.

def visualize_stn():
    with torch.no_grad():
        # Get a batch of training data
        data = next(iter(train_loader))[0].to(device)
        data = data.unsqueeze(1)
        input_tensor = data.cpu()
        transformed_input_tensor = model.stn(data).cpu()
        in_grid = convert_image_np(
            torchvision.utils.make_grid(input_tensor))
        out_grid = convert_image_np(
            torchvision.utils.make_grid(transformed_input_tensor))
        # Plot the results side-by-side
        f, axarr = plt.subplots(1, 2, figsize=(20,20))
        axarr[0].imshow(in_grid, cmap='gray')
        axarr[0].set_title('Dataset Images')
        axarr[1].imshow(out_grid, cmap='gray')
        axarr[1].set_title('Transformed Images')
# Visualize the STN transformation on some input batch
visualize_stn()
plt.ioff()
plt.show()

图片由作者提供。

显然,网络已经在某种程度上移动了图像,但对于与模板图像差异过大的眼底图像仍然存在困难。

超大规模配准

为了完整性,我在这里包括了一些我尝试过的事情,以使 STN 的效果更好。

图像增强

我直觉上认为,图像增强可以通过增加训练数据的多样性和数量来改善 STN 在学习图像配准变换中的性能。STN 依赖大量的训练数据来学习图像之间的复杂空间变换。然而,由于患者数据有限和成像模式的变异等因素,获取足够大且多样的医学图像数据集可能具有挑战性。此外,我每个患者的数据有限,因此训练 STN 仅对许多患者的组合数据可行。

图像增强通过对现有图像应用多种图像变换来生成合成训练数据,提供了一个解决方案。这增加了训练数据集的大小和多样性,使 STN 能够学习更强大且具有更好泛化能力的配准变换。图像增强还可以帮助 STN 学习对某些成像条件如光照、对比度和噪声变化不变的变换。

常见的图像增强技术包括随机旋转、平移、缩放和翻转,以及更复杂的变换,如弹性形变和强度变化。这些变换在训练过程中随机应用,以生成与原始图像相似的各种变换图像。然后使用增强的图像来训练 STN,从而提高其对新图像的泛化能力。

import numpy as np
from PIL import Image
import cv2
import random
import torchvision.transforms as transforms

def image_augmentation(images, base_index=0, n_aug=5):
    # Convert the NumPy arrays to Pillow Image objects
    items = [Image.fromarray(image).convert("RGBA") for image in images]
    # Define the image transformation pipeline
    transform = transforms.Compose([
        transforms.Resize(460),
        transforms.RandomResizedCrop(224, scale=(0.8, 1.0)),
        transforms.RandomAffine(degrees=0, translate=(0.2, 0.2),
                                scale=(0.9, 1.1), shear=0,
                                fillcolor=(128, 128, 128, 255)),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ])
    # Generate the augmented images
    new_items = []
    for i in range(n_aug):
        # Get the base image
        base_item = items[base_index]
        base_image = np.array(base_item)
        # Apply the random transforms to the base image
        transformed_item = transform(base_item)
        # Convert the transformed image to a NumPy array and add it to the list of augmented images
        transformed_image = np.transpose(transformed_item.numpy(), (1, 2, 0))
        transformed_image = cv2.cvtColor(transformed_image, cv2.COLOR_RGB2BGR)
        new_items.append(transformed_image)
    # Convert the augmented data back to NumPy arrays
    new_images = [np.array(image) for image in new_items]
    return new_images

然后你可以将这个函数应用到图像列表中,并传入扩展的数据集进行训练。

使用 k 最近邻的聚类模型

以下代码实现了对存储为 NumPy 数组的一组图像进行 k-means 聚类。代码的目的是找到最佳的簇数,以最佳地表示图像集。

代码首先将图像列表转换为 2D NumPy 数组,然后将数组重塑为 2D 形状。这是为了创建一个可以输入到 k-means 聚类算法的数据集。然后,对一系列的 k 值(其中 k 是要生成的簇的数量)运行 k-means 算法。对于每个 k 值,运行算法,并计算簇内平方和(WCSS)。WCSS 是衡量每个簇内数据点分散程度的指标,用于评估聚类质量。WCSS 值存储在一个列表中,并对所有 k 值重复此过程。

一旦计算出 WCSS 值,就会生成一个肘部图来可视化簇数与 WCSS 值之间的关系。肘部图展示了一个下降的曲线,并到达一个肘部点,在此点 WCSS 值的下降速度开始平缓。最佳的簇数被选择为曲线开始平缓的值。

from sklearn.cluster import KMeans

# Assume you have a list of images stored as numpy arrays in a variable called "images"
images, template = retrieve_images()
# Convert the list of images to a 2D numpy array
data = np.array(images)
n_samples, height, width = data.shape
data = data.reshape(n_samples, height * width)
# Set up an empty list to hold the within-cluster sum of squares (WCSS) values for each value of k
wcss_values = []
# Set up a range of values for k
k_values = range(1, 11)
# Loop over the values of k and fit a k-means model for each value
for k in k_values:
    kmeans = KMeans(n_clusters=k, random_state=0)
    kmeans.fit(data)

    # Calculate the within-cluster sum of squares (WCSS)
    wcss = kmeans.inertia_
    wcss_values.append(wcss)

# Plot the WCSS values against the number of clusters
fig, ax = plt.subplots()
ax.plot(k_values, wcss_values, 'bo-')
ax.set_xlabel('Number of clusters (k)')
ax.set_ylabel('Within-cluster sum of squares (WCSS)')
ax.set_title('Elbow Plot')
plt.show()

图片由作者提供。

从这个图中来看,最佳的簇数可能是三个。让我们用这个来对我们的图像进行分组:

import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.neighbors import NearestNeighbors

# Assume you have a list of images stored as NumPy arrays in a variable called "images"
images, template = retrieve_images()
# First, flatten each image into a 1D array
image_vectors = np.array([image.flatten() for image in images])
# Use k-means to cluster the image vectors
kmeans = KMeans(n_clusters=3, random_state=0).fit(image_vectors)
cluster_labels = kmeans.labels_
# Use k-nearest neighbors to find the nearest images to each centroid
n_neighbors = 5
nn = NearestNeighbors(n_neighbors=n_neighbors, algorithm='ball_tree').fit(image_vectors)
distances, indices = nn.kneighbors(kmeans.cluster_centers_)
# Plot the nearest images to each centroid
fig, axs = plt.subplots(kmeans.n_clusters, n_neighbors, figsize=(15, 15))
for i in range(kmeans.n_clusters):
    for j in range(n_neighbors):
        axs[i][j].imshow(images[indices[i][j]], cmap='gray')
        axs[i][j].axis('off')
        axs[i][j].set_title(f"Cluster {i}, Neighbor {j+1}")
plt.show()
# Store the cluster labels and image labels in a dictionary
labels_dict = {}
for i in range(len(images)):
    labels_dict[i] = {"cluster_label": cluster_labels[i]}

图片由作者提供。

这看起来不错。让我们为每个簇创建一个新列表:

# Assume you have a list of images stored as NumPy arrays in a variable called "images"
images, template = retrieve_images()

# First, flatten each image into a 1D array
image_vectors = np.array([image.flatten() for image in images])

# Use k-means to cluster the image vectors
kmeans = KMeans(n_clusters=3, random_state=0).fit(image_vectors)
cluster_labels = kmeans.labels_

# Use k-nearest neighbors to find the nearest images to each centroid
n_neighbors = 5
nn = NearestNeighbors(n_neighbors=n_neighbors, algorithm='ball_tree').fit(image_vectors)
distances, indices = nn.kneighbors(kmeans.cluster_centers_)

# Store the images in each cluster
cluster_0_images = []
cluster_1_images = []
cluster_2_images = []
for i, cluster_label in enumerate(cluster_labels):
    if cluster_label == 0:
        cluster_0_images.append(images[i])
    elif cluster_label == 1:
        cluster_1_images.append(images[i])
    else:
        cluster_2_images.append(images[i])

# Print the number of images in each cluster
print(f"Number of images in cluster 0: {len(cluster_0_images)}")
print(f"Number of images in cluster 1: {len(cluster_1_images)}")
print(f"Number of images in cluster 2: {len(cluster_2_images)}")
Number of images in cluster 0: 38
Number of images in cluster 1: 31
Number of images in cluster 2: 32
trained_models = {}

for i, cluster_images in enumerate([cluster_0_images, cluster_1_images, cluster_2_images]):
    # load the spatial transformer network
    model = Net().to(device)
    template_image = torch.from_numpy(template).float()
    # Create the dataset
    dataset = FundusDataset(cluster_images, template_image)
    # Create the data loader
    train_loader = DataLoader(dataset, batch_size=32, shuffle=True)
    optimizer = optim.SGD(model.parameters(), lr=0.05)
    criterion = voxelmorph_loss_2d

    print(f"TRAINING CLUSTER {i}")
    print("-"*50)
    for epoch in range(1, 5 + 1):
        train(epoch)

    trained_models[f"cluster_{i}_model"] = model
    print(" ")
TRAINING CLUSTER 0
--------------------------------------------------
Train Epoch: 1, Average Loss: 0.122363
Train Epoch: 2, Average Loss: 0.120482
Train Epoch: 3, Average Loss: 0.117854
Train Epoch: 4, Average Loss: 0.112581
Train Epoch: 5, Average Loss: 0.119771

TRAINING CLUSTER 1
--------------------------------------------------
Train Epoch: 1, Average Loss: 0.079963
Train Epoch: 2, Average Loss: 0.080274
Train Epoch: 3, Average Loss: 0.077222
Train Epoch: 4, Average Loss: 0.076657
Train Epoch: 5, Average Loss: 0.077101

TRAINING CLUSTER 2
--------------------------------------------------
Train Epoch: 1, Average Loss: 0.172036
Train Epoch: 2, Average Loss: 0.171105
Train Epoch: 3, Average Loss: 0.170653
Train Epoch: 4, Average Loss: 0.170199
Train Epoch: 5, Average Loss: 0.169759

实际上没有帮助。这也违背了 STN 的整体思想,即使用卷积层来确定应用于哪些图像的变换,即一些图像在变换中需要显著更高的权重。

结论

总之,对医学图像配准技术的评估表明,虽然现代方法如空间变换网络(STNs)提供了有希望的结果,但需要大量投资才能实现预期效果。相比之下,传统技术如 SimpleElastix 被证明更有效且高效。尽管实施了各种策略来提高 STN 的性能,但模型未能学习到足够的权重来调整目标,显示出需要替代的损失函数。一种方法可能是忽略由于仿射变换产生的黑色像素。此外,点对点配准利用视网膜或血管等生物标记来指导配准过程,对特定应用可能更有利。因此,需要进一步研究以确定最合适的配准方法,基于具体问题和可用资源。

参考文献

数据来自 Kaggle 视网膜基金图像配准数据集,该数据集的许可证为国际署名 4.0 (CC BY 4.0)。

  1. Kaggle: 视网膜基金图像配准 数据集

  2. Jaderberg, M., Simonyan, K., & Zisserman, A. (2015). 空间变换网络。神经信息处理系统的进展, 28

  3. Hill, D. L., Batchelor, P. G., Holden, M., & Hawkes, D. J. (2001). 医学图像配准。医学与生物物理学, 46(3), R1。

  4. Brown, L. G. (1992). 图像配准技术的综述。ACM 计算机调查(CSUR), 24(4), 325–376。

  5. Lee, M. C., Oktay, O., Schuh, A., Schaap, M., & Glocker, B. (2019). 图像和空间变换网络用于结构引导的图像配准。在医学图像计算与计算机辅助干预–MICCAI 2019: 第 22 届国际会议,深圳,中国,2019 年 10 月 13–17 日,会议录,第二十二部分(第 337–345 页)。Springer International Publishing。

  6. Pytorch: 空间变换网络教程

5 分钟内的图像搜索

原文:towardsdatascience.com/image-search-in-5-minutes-9bc4f903b22a

前沿的图像搜索,简单快速

Daniel WarfieldTowards Data Science Daniel Warfield

·发布于Towards Data Science ·6 分钟阅读·2023 年 10 月 25 日

--

作者使用 MidJourney 的“权重向量”。除非另有说明,否则所有图像均为作者所用。

在这篇文章中,我们将使用一个轻量级的预训练模型来实现文本到图像搜索(允许我们通过文本搜索图像)和图像到图像搜索(允许我们基于参考图像搜索图像)。我们将使用的模型用于计算图像和文本的相似性,受到对比语言图像预训练(CLIP)启发,详细讨论见另一篇文章。

使用文本“水边的彩虹”搜索图像的结果

这对谁有用? 任何希望实现图像搜索的开发者,对实际应用感兴趣的数据科学家,或希望了解 A.I.实践的非技术读者。

这篇文章有多先进? 本文将指导你以最快最简单的方式实现图像搜索。

前提条件: 基本编码经验。

我们要做什么,以及我们如何做到这一点

本文是我关于“对比语言-图像预训练”文章的补充。如果你想更深入地理解理论,可以查看一下:

## CLIP,直观且全面地解释

创建强大的图像和语言表示,用于通用机器学习任务。

towardsdatascience.com

CLIP 模型的训练目的是预测任意的标题是否与任意的图像匹配。我们将利用这种通用功能来创建我们的图像搜索系统。具体来说,我们将使用 CLIP 的图像和文本编码器将输入压缩成一个向量,称为嵌入,可以将其视为输入的总结。

编码器的工作是将输入总结成一个有意义的表示,称为嵌入。我文章中的图像

CLIP 的整体理念是相似的文本和图像将具有相似的向量嵌入。

CLIP 试图让相似事物的嵌入彼此接近。我文章中的图像

我们将使用的具体模型叫做uform,其概念上类似于 CLIP。uform 是一个宽松许可的、预训练的、高效能的模型,承诺比 CLIP 具有更好的性能。uform 有三种变体,我们将使用与 CLIP 概念上类似的“late fusion”变体。

uform 库中的三种模型类型。正如你所见,“Late Fusion Model”在功能上与 CLIP 非常相似,它从两个编码器中返回两个独立的向量。source

嵌入之间的实际相似度将使用余弦相似度计算。余弦相似度的本质在于,如果它们的嵌入之间的角度很小,则可以定义为“相似”。因此,我们可以通过首先嵌入文本和图像,然后计算嵌入之间的余弦相似度,来计算文本和图像之间的相似性。

余弦相似度使用向量之间的角度来确定相似性。A 和 B 之间的角度很小,因此 A 和 B 是相似的。C 会被认为与 A 和 B 都非常不同。我文章中的图像

这就是核心思想:我们下载受 CLIP 启发的模型(uform),使用编码器来嵌入图像和文本,然后使用余弦相似度来搜索相似性。请随时参考这篇附带文章以深入了解理论。现在我们只需将其付诸实践即可。

实现

我会跳过一些不重要的内容。完整代码可以在这里找到:

[## MLWritingAndResearch/ImageSearch.ipynb 在 main · DanielWarfield1/MLWritingAndResearch

用于机器学习写作和研究的笔记本示例 - MLWritingAndResearch/ImageSearch.ipynb 在 main ·…

github.com](https://github.com/DanielWarfield1/MLWritingAndResearch/blob/main/ImageSearch.ipynb?source=post_page-----9bc4f903b22a--------------------------------)

下载模型

这非常简单,只需pip install uform模块,然后使用该模块从 Hugging Face 下载模型。我们将使用英文版本,但也提供其他语言的版本。

!pip install uform
import uform
model = uform.get_model('unum-cloud/uform-vl-english')

定义一个用于搜索的图像数据库

我从a dataset下载了几张图片供我们使用,这个数据集是从harvard dataverse(创意共享许可)衍生的,并将它们放在了一个公共的github repo中。下面的伪代码将这些图片下载到列表images中。这个图片列表就是我们最终将要搜索的内容。

#List all files
urls = get_image_urls_from_github()

#Download each file
images = download_images(urls)

#Render out a few examples
render_examples(images)

一些来自数据集中我们将要搜索的图像示例

实现文本到图像的搜索

这里是关键部分。首先,我们将定义一些搜索文本,在这个例子中是a rainbow by the water。然后,我们可以将这些文本嵌入,并将其与所有图片的嵌入进行比较。接着,我们可以通过余弦相似度对其进行排序,展示与搜索文本最相似的前五张图片。请记住,CLIP 风格模型有一个单独的图像编码器和文本编码器,因此文本由文本编码器编码,图像由图像编码器编码。

"""Implementing text to image search
using the uform model to encode text and all images. Then using cosine
similarity to find images which match the specified text. Rendering out the
top 5 results
"""

import torch.nn.functional as F

#defining search phrase
text = "a rainbow by the water"
print(f'search text: "{text}"')

#embedding text
text_data = model.preprocess_text(text)
text_embedding = model.encode_text(text_data)

#calculating cosine similarity
sort_ls = []
print('encoding and calculating similarity...')
for image in tqdm(images):
    #encoding image
    image_data = model.preprocess_image(image)
    image_embedding = model.encode_image(image_data)

    #calculating similarity
    sim = F.cosine_similarity(image_embedding, text_embedding)

    #appending to list for later sorting
    sort_ls.append((sim, image))

#sorting by similarity
sort_ls.sort(reverse=True, key = lambda t: t[0])

print('top 5 most similar results:')
_, axs = plt.subplots(1, 5, figsize=(12, 8))
axs = axs.flatten()
for img, ax in zip([im for sim, im in sort_ls][:5], axs):
    ax.imshow(img)
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)
plt.show()

实现图像到图像搜索

图像到图像搜索与之前讨论的文本到图像搜索行为类似;我们嵌入用于搜索的图像,并嵌入所有其他图像。我们的搜索图像的嵌入与所有其他图像的嵌入进行比较(使用余弦相似度),从而找到与我们搜索图像最相似的图像。自然地,这个例子中最相似的图像就是图像本身。

"""Implementing image to image search
similar to previous approach, except all images are compared to an input image.
Rendering out the top 5 results
"""

#defining search image
input_image = images[15]

#rendering search image
print('input image:')
fig = plt.figure(figsize=(4,4))
ax = fig.add_subplot(111)
ax.imshow(input_image)
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
plt.show()

#embedding image
image_data = model.preprocess_image(input_image)
search_image_embedding = model.encode_image(image_data)

#calculating cosine similarity
sort_ls = []
print('encoding and calculating similarity...')
for image in tqdm(images):
    #encoding image
    image_data = model.preprocess_image(image)
    image_embedding = model.encode_image(image_data)

    #calculating similarity
    sim = F.cosine_similarity(image_embedding, search_image_embedding)

    #appending to list for later sorting
    sort_ls.append((sim, image))

#sorting by similarity
sort_ls.sort(reverse=True, key = lambda t: t[0])

print('top 5 most similar results:')
_, axs = plt.subplots(1, 5, figsize=(12, 8))
axs = axs.flatten()
for img, ax in zip([im for sim, im in sort_ls][:5], axs):
    ax.imshow(img)
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)
plt.show()

结论

就这样!我们成功地使用了 CLIP 风格模型的图像和文本编码器实现了两种类型的图像搜索:一种基于输入文本,另一种基于输入图像。我们通过使用文本编码器计算文本的嵌入,图像编码器计算图像的嵌入,并通过排序嵌入的相似性(使用余弦相似度)来搜索。

随时查看配套文章,以深入了解 CLIP。

关注以获取更多内容!

我描述了机器学习领域的论文和概念,重点在于实际和直观的解释。

归属: 本文档中的所有资源均由丹尼尔·沃菲尔德(Daniel Warfield)创建,除非另有来源说明。您可以将本文中的任何资源用于您的非商业用途,只要您引用了这篇文章,danielwarfield.dev,或者两者都引用。应要求可以授予明确的商业许可。

图像分割:深入指南

原文:towardsdatascience.com/image-segmentation-an-in-depth-guide-5e56512eea2e?source=collection_archive---------2-----------------------#2023-10-06

如何让计算机区分图像中的不同对象?一个逐步指南。

Ed IzaguirreTowards Data Science Ed Izaguirre

·

关注 发表在 Towards Data Science ·21 分钟阅读·2023 年 10 月 6 日

--

一张猫咪在白色栅栏前的图像。来源于 DALL·E 3

目录

  1. 介绍、动机

  2. 提取数据

  3. 可视化图像

  4. 构建简单的 U-Net 模型

  5. 指标和损失函数

  6. 构建完整的 U-Net 模型

  7. 总结

  8. 参考文献

相关链接

介绍,动机

图像分割是指计算机(更准确地说是存储在计算机上的模型)将图像中的每个像素分配到相应类别的能力。例如,可以将上面展示的猫的图像通过图像分割器处理,并得到如下的分割图像:

猫的图像,分割成‘猫’像素和‘背景’像素。修改后的图像来自 DALL·E 3

在这个例子中,我手动分割了图像。这是一个繁琐的操作,我们希望能自动化。在本指南中,我将带你逐步了解训练算法进行图像分割的过程。互联网上和教科书中有许多指南在一定程度上是有帮助的,但它们都未能深入到实现的细节。在这里,我将尽可能不留任何遗漏,帮助你在自己数据集上实现图像分割时节省时间。

首先,让我们将任务放在机器学习的更广泛背景中。机器学习的定义不言而喻:我们在教机器如何解决我们希望自动化的问题。人类希望自动化的问题有很多;在本文中,我们关注的是计算机视觉中的一个子集。计算机视觉旨在教计算机如何看见。给一个六岁的小孩一张猫在白色栅栏前的图像,并让他们将图像分割成‘猫’像素和‘背景’像素(当然,在你向困惑的孩子解释‘分割’的意思之后)是很简单的。然而,几十年来,计算机在这个问题上却一直挣扎。

为什么计算机在做一个六岁小孩能做的事情时会挣扎?我们可以通过考虑一个人如何通过盲文学习阅读来设身处地为计算机着想。假设你拿到了一篇用盲文写的文章,并且假设你不知道如何阅读它。你会怎么做?你需要什么来将盲文解码成英文?

一小段用盲文书写的文字。来源于 Unsplash

你需要的是一种将输入转换为对你可读的输出的方法。在数学中,我们称之为映射。我们说我们希望学习一个函数 f(x),它将我们无法读取的输入 x 映射到一个可读取的输出 y

通过长时间的练习和良好的导师,任何人都可以学会从盲文到英文的必要映射。类比而言,处理图像的计算机有点像初次接触盲文的人;它看起来像一堆无意义的东西。计算机需要学习必要的映射 f(x),将与像素对应的一堆数字转换为可以用来分割图像的内容。不幸的是,计算机模型没有数千年的进化、生物学以及多年看世界的经验;它在您启动程序时基本上‘出生’。这就是我们希望在计算机视觉中教给我们模型的内容。

首先,我们为什么要进行图像分割呢?其中一个更明显的用例是 Zoom。许多人在视频会议中喜欢使用虚拟背景,以避免同事看到他们的狗在客厅里做花式。图像分割对于这项任务至关重要。另一个强大的用例是医学成像。在对患者器官进行 CT 扫描时,自动分割图像中的器官可能有助于医疗专业人员确定诸如损伤、肿瘤存在等问题。这里有一个很好的例子 是一个专注于此任务的 Kaggle 竞赛。

图像分割有几种不同的类型,从简单到复杂不等。在本文中,我们将处理最简单的图像分割类型:二进制分割。这意味着只会有两类不同的对象,例如‘猫’和‘背景’。不多也不少。

请注意,我在此呈现的代码已经稍作整理和编辑以增加清晰度。要运行一些可工作的代码,请查看文章顶部的代码链接。我们将使用 Kaggle 的 Carvana 图像掩模挑战数据集。您需要注册此挑战以获取数据集,并将您的 Kaggle API 密钥插入 Colab 笔记本以使其正常工作(如果您不想使用 Kaggle 笔记本)。请查看此讨论帖 获取详细信息。

还有一件事;尽管我很想详细讨论代码中的每个想法,但我假设您对卷积神经网络、最大池化层、密集连接层、dropout 层和残差连接器有一些工作知识。不幸的是,长时间讨论这些概念需要一篇新文章,超出了我们专注于实现细节的范围。

提取数据

本文的相关数据将存储在以下文件夹中:

  • train_hq.zip: 包含汽车高质量训练图像的文件夹

  • test_hq.zip: 包含汽车高质量测试图像的文件夹

  • train_masks.zip: 包含训练集掩模的文件夹

在图像分割的上下文中,mask 是分割后的图像。我们尝试让模型学习如何将输入图像映射到输出分割掩模。通常假设真实的掩模(即地面真相)是由人类专家手工绘制的。

一个图像及其相应的真实掩模的例子,是真实的人手工绘制的。来自 Carvana 图像掩模挑战数据集。

你的第一步是从你的 /kaggle/input 源中解压文件夹:

def getZippedFilePaths():
    zip_file_names = []
    for dirname, _, filenames in os.walk('/kaggle/input'):
        for filename in filenames:
            if filename.split('.')[-1] == 'zip':
                zip_file_names.append((os.path.join(dirname, filename)))

    return zip_file_names

zip_file_names = getZippedFilePaths()

items_to_remove = ['/kaggle/input/carvana-image-masking-challenge/train.zip', 
                   '/kaggle/input/carvana-image-masking-challenge/test.zip']

zip_file_names = [item for item in zip_file_names if item not in items_to_remove]

for zip_file_path in zip_file_names:
    with zipfile.ZipFile(zip_file_path, 'r') as zip_ref:
        zip_ref.extractall()

这段代码获取了输入中所有 .zip 文件的文件路径,并将它们提取到 /kaggle/output 目录中。请注意,我故意不提取非高质量照片;Kaggle 存储库只能容纳 20 GB 的数据,这一步是为了防止超过此限制。

可视化图像

大多数计算机视觉问题中的第一步是检查数据集。我们究竟在处理什么?我们首先需要将图像组装成组织良好的数据集以进行查看。本指南将使用 TensorFlow;转换为 PyTorch 应该不会太困难。

# Appending all path names to a sorted list
train_hq_dir = '/kaggle/working/train_hq/'
train_masks_dir = '/kaggle/working/train_masks/'
test_hq_dir = '/kaggle/working/test_hq/'

X_train_id = sorted([os.path.join(train_hq_dir, filename) for filename in os.listdir(train_hq_dir)], key=lambda x: x.split('/')[-1].split('.')[0])
y_train = sorted([os.path.join(train_masks_dir, filename) for filename in os.listdir(train_masks_dir)], key=lambda x: x.split('/')[-1].split('.')[0])
X_test_id = sorted([os.path.join(test_hq_dir, filename) for filename in os.listdir(test_hq_dir)], key=lambda x: x.split('/')[-1].split('.')[0])

X_train_id = X_train_id[:1000]
y_train = y_train[:1000]
X_train, X_val, y_train, y_val = train_test_split(X_train_id, y_train, test_size=0.2, random_state=42)

# Create Dataset objects from the list of file paths
X_train = tf.data.Dataset.from_tensor_slices(X_train)
y_train = tf.data.Dataset.from_tensor_slices(y_train)

X_val = tf.data.Dataset.from_tensor_slices(X_val)
y_val = tf.data.Dataset.from_tensor_slices(y_val)

X_test = tf.data.Dataset.from_tensor_slices(X_test_id)

img_height = 96
img_width = 128
num_channels = 3

img_size = (img_height, img_width)

# Apply preprocessing
X_train = X_train.map(preprocess_image)
y_train = y_train.map(preprocess_target)

X_val = X_val.map(preprocess_image)
y_val = y_val.map(preprocess_target)

X_test = X_test.map(preprocess_image)

# Add labels to dataframe objects (one-hot-encoded)
train_dataset = tf.data.Dataset.zip((X_train, y_train))
val_dataset = tf.data.Dataset.zip((X_val, y_val))

# Apply the batch size to the dataset
BATCH_SIZE = 32
batched_train_dataset = train_dataset.batch(BATCH_SIZE)
batched_val_dataset = val_dataset.batch(BATCH_SIZE)
batched_test_dataset = X_test.batch(BATCH_SIZE)

# Adding autotune for pre-fetching
AUTOTUNE = tf.data.experimental.AUTOTUNE
batched_train_dataset = batched_train_dataset.prefetch(buffer_size=AUTOTUNE)
batched_val_dataset = batched_val_dataset.prefetch(buffer_size=AUTOTUNE)
batched_test_dataset = batched_test_dataset.prefetch(buffer_size=AUTOTUNE)

让我们来拆解一下:

  • 我们首先创建一个所有训练集、测试集和真实掩模图像的文件路径的排序列表。注意,这些还不是图像;我们目前只是在查看图像的文件路径。

  • 然后,我们只取 Carvana 数据集中前 1000 个图像/掩模的文件路径。这是为了减少计算负载并加快训练速度。如果你有多个强大的 GPU(真幸运!),可以使用所有图像以获得更好的性能。我们还创建了 80/20 的训练/验证划分。包含的数据(图像)越多,这一划分就应该越倾向于训练集。在处理非常大的数据集时,训练/验证/测试的划分中看到 98/1/1 的情况并不少见。训练集中的数据越多,模型的表现通常会更好。

  • 然后,我们使用 tf.data.Dataset.from_tensor_slices() 方法创建 TensorFlow (TF) Dataset 对象。使用 Dataset 对象是一种处理训练、验证和测试集的常见方法,而不是将它们保留为 Numpy 数组。根据我的经验,使用 Dataset 对象时数据预处理要快得多,也更容易。参见此链接 获取文档。

  • 接下来,我们指定输入图像的高度、宽度和通道数。实际的高质量图像远大于 96 像素乘 128 像素;对图像进行降采样是为了减少计算负载(更大的图像需要更多的训练时间)。如果你有必要的计算能力(GPU),我不推荐进行降采样。

  • 然后,我们使用 Dataset 对象的 .map() 函数来预处理图像。这将文件路径转换为图像,并进行适当的预处理。稍后会详细介绍。

  • 一旦我们预处理了原始训练图像和真实标注掩膜,我们需要一种方法将图像与其掩膜配对。为此,我们使用 Dataset 对象的 .zip() 函数。这将两个数据列表进行连接,将每个列表的第一个元素合并成一个元组。对第二个元素、第三个元素等进行相同操作。最终结果是一个包含 (image, mask) 形式的元组的单一列表。

  • 然后,我们使用 .batch() 函数从我们的一千张图像中创建 32 张图像的批次。批量处理是机器学习管道的重要部分,因为它允许我们一次处理多张图像,而不是逐张处理。这加快了训练速度。

  • 最后,我们使用 .prefetch() 函数。这是另一个帮助加速训练的步骤。加载和预处理数据可能成为训练管道中的瓶颈。这可能导致 GPU 或 CPU 空闲时间,这是没有人希望看到的。当你的模型进行前向和反向传播时,.prefetch() 函数可以准备好下一批数据。TensorFlow 中的 AUTOTUNE 变量会根据系统资源动态计算预取的批次数;这通常是推荐的做法。

让我们更深入地了解预处理步骤:

def preprocess_image(file_path):
    # Load and decode the image
    img = tf.io.read_file(file_path)
    # You can adjust channels based on your images (3 for RGB)
    img = tf.image.decode_jpeg(img, channels=3) # Returned as uint8
    # Normalize the pixel values to [0, 1]
    img = tf.image.convert_image_dtype(img, tf.float32)
    # Resize the image to your desired dimensions
    img = tf.image.resize(img, [96, 128], method = 'nearest')
    return img

def preprocess_target(file_path):
    # Load and decode the image
    mask = tf.io.read_file(file_path)
    # Normalizing to between 0 and 1 (only two classes)
    mask = tf.image.decode_image(mask, expand_animations=False, dtype=tf.float32)
    # Get only one value for the 3rd channel
    mask = tf.math.reduce_max(mask, axis=-1, keepdims=True)
    # Resize the image to your desired dimensions
    mask = tf.image.resize(mask, [96, 128], method = 'nearest')
    return mask

这些函数完成以下操作:

  • 首先,我们使用 tf.io.read_file() 将文件路径转换为数据类型为 'string' 的 张量。张量是 TensorFlow 中一种特殊的数据结构,类似于其他数学库中的多维数组,但具有对深度学习有用的特殊属性和方法。引用 TensorFlow 文档:tf.io.read_file() “不进行任何解析,它仅返回原始内容。” 本质上,这意味着它返回一个包含图像信息的二进制文件(1 和 0),以字符串类型表示。

  • 其次,我们需要 解码 二进制数据。为此,我们需要使用 TensorFlow 中的适当方法。由于原始图像数据是 .jpeg 格式,我们使用 tf.image.decode_jpeg() 方法。由于掩膜是 GIF 格式,我们可以使用 tf.io.decode_gif(),或者使用更通用的 tf.image.decode_image(),它可以处理任何文件类型。你选择哪个并不重要。我们将 expand_animations 设置为 False,因为这些实际上不是动画,而只是图像。

  • 然后我们使用 convert_image_dtype() 将我们的图像数据转换为 float32。这仅适用于图像,不适用于掩膜,因为掩膜已经解码为 float32。图像处理常用的两种数据类型是 float32 和 uint8。Float32 代表一个浮点数(小数),在计算机内存中占用 32 位。它们是有符号的(意味着数字可以是负数),值的范围从 0 到 2³² = 4294967296,尽管在图像处理的惯例中,我们将这些值归一化到 0 到 1 之间,其中 1 是颜色的最大值。Uint8 代表一个无符号(正数)整数,范围从 0 到 255,只占用 8 位内存。例如,我们可以将颜色 burnt orange 表示为 uint8 格式的 (Red: 204, Green: 85, Blue: 0) 或 float32 格式的 (Red: 0.8, Green: 0.33, Blue: 0)。Float32 通常是更好的选择,因为它提供了更多的精度,并且已经归一化,这有助于提高训练效果。然而,uint8 节省内存,根据你的内存限制,这可能是更好的选择。在 convert_image_dtype 中使用 float32 会自动归一化这些值。

  • 在二分类分割中,我们期望我们的掩膜具有形状 (batch, height, width, channels),其中 channels = 1。换句话说,我们希望一个类别(汽车)由数字 1 表示,另一个类别(背景)由数字 0 表示。没有理由将通道数设置为 3,就像 RGB 图像一样。不幸的是,解码后,它带有三个通道,其中类别编号重复三次。为了解决这个问题,我们使用 tf.math.reduce_max(mask, axis=-1, keepdims=True) 来取三个通道中的最大值,并去掉其他值。因此,通道值 (1,1,1) 会被减少为 (1),通道值 (0,0,0) 会被减少为 (0)。

  • 最后,我们将图像/掩膜调整为我们所需的尺寸(较小)。请注意,我之前展示的汽车图像与真实掩膜看起来模糊;这次缩小是故意进行的,以减少计算负荷并使训练相对较快。默认使用 method=‘nearest’ 是个好主意;否则函数将始终返回 float32,这对于你希望它是 uint8 格式的情况是不好的。

颜色 burnt orange 可以用 float32 或 uint8 格式表示。图片由作者提供。

一旦我们组织好数据集,我们现在可以查看我们的图像:

# View images and associated labels
for images, masks in batched_val_dataset.take(1):
    car_number = 0
    for image_slot in range(16):
        ax = plt.subplot(4, 4, image_slot + 1)
        if image_slot % 2 == 0:
            plt.imshow((images[car_number])) 
            class_name = 'Image'
        else:
            plt.imshow(masks[car_number], cmap = 'gray')
            plt.colorbar()
            class_name = 'Mask'
            car_number += 1            
        plt.title(class_name)
        plt.axis("off")

我们的汽车图像与附带的掩膜配对显示。

在这里,我们使用 .take() 方法来查看我们批处理的 batched_val_dataset 中的第一批数据。由于我们正在进行二分类分割,我们希望我们的掩膜只包含两个值:0 和 1。绘制掩膜上的颜色条可以确认我们设置正确。请注意,我们在掩膜的 imshow() 中添加了参数 cmap = ‘gray’,以告知 plt 我们希望这些图像以灰度显示。

构建一个简单的 U-Net 模型

在 1675 年 2 月 5 日写给其对手罗伯特·胡克的信中,艾萨克·牛顿表示:

“如果我看得更远,那是因为我站在了巨人的肩膀上。”

同样地,我们将站在之前机器学习研究者的肩膀上,他们已经发现了哪些架构最适合图像分割任务。实验自己设计的架构并非坏主意;然而,之前的研究者们走过许多弯路才发现了有效的模型。这些架构并不一定是终极解决方案,因为研究仍在进行中,可能会发现更好的架构。

U-Net 的可视化,描述见[1]。图片由作者提供。

一个较为知名的架构称为U-Net,因为网络的下采样部分和上采样部分可以被可视化为一个 U 形(见上文)。在由 Ronneberger、Fisher 和 Brox 撰写的名为U-Net: Convolutional Networks for Biomedical Image Segmentation的论文[1]中,作者描述了如何创建一个全卷积网络 (FCN),该网络在图像分割中表现有效。全卷积意味着没有密集连接层;所有层都是卷积层。

有几点需要注意:

  • 网络由一系列重复的两个卷积层块组成,使用 padding = ‘same’和 stride = 1,以确保卷积的输出在块内不会缩小。

  • 每个块后面跟着一个最大池化层,该层将特征图的宽度和高度减半。

  • 接下来的模块将滤波器的数量加倍。这个模式会继续。如果你研究过 CNN,这种在减少特征空间的同时增加滤波器数量的模式应该很熟悉。这完成了作者所称的“收缩路径”。

  • “瓶颈”层位于‘U’的底部。该层捕捉高度抽象的特征(如线条、曲线、窗户、门等),但在空间分辨率上大幅降低。

  • 接下来是他们所谓的“扩展路径”。简而言之,这个过程是反向的,每个模块再次由两个卷积层组成。每个模块后面跟着一个上采样层,在 TensorFlow 中我们称之为 Conv2DTranspose 层。它将较小的特征图的高度和宽度加倍。

  • 接下来的模块将滤波器的数量减半。重复这个过程,直到得到与起始图像相同的高度和宽度。最后,用一个 1x1 卷积层来将通道数量减少到 1。我们希望最后得到一个通道,因为这是二值分割,所以我们需要一个滤波器,像素值对应于我们的两个类别。我们使用 sigmoid 激活函数将像素值压缩在 0 到 1 之间。

  • U-Net 架构中还有跳跃连接,允许网络在下采样和上采样后保留细粒度的空间信息。在这个过程中通常会丢失很多信息。通过将信息从收缩块传递到相应的扩展块,我们可以保留这些空间信息。这个架构有一个漂亮的对称性。

我们将首先做一个简单版本的 U-Net。这将是一个 FCN,但没有残差连接和最大池化层。

data_augmentation = tf.keras.Sequential([
        tfl.RandomFlip(mode="horizontal", seed=42),
        tfl.RandomRotation(factor=0.01, seed=42),
        tfl.RandomContrast(factor=0.2, seed=42)
])

def get_model(img_size):
    inputs = Input(shape=img_size + (3,))
    x = data_augmentation(inputs)

    # Contracting path
    x = tfl.Conv2D(64, 3, strides=2, activation="relu", padding="same", kernel_initializer='he_normal')(x) 
    x = tfl.Conv2D(64, 3, activation="relu", padding="same", kernel_initializer='he_normal')(x) 
    x = tfl.Conv2D(128, 3, strides=2, activation="relu", padding="same", kernel_initializer='he_normal')(x) 
    x = tfl.Conv2D(128, 3, activation="relu", padding="same", kernel_initializer='he_normal')(x) 
    x = tfl.Conv2D(256, 3, strides=2, padding="same", activation="relu", kernel_initializer='he_normal')(x) 
    x = tfl.Conv2D(256, 3, activation="relu", padding="same", kernel_initializer='he_normal')(x)

    # Expanding path
    x = tfl.Conv2DTranspose(256, 3, activation="relu", padding="same", kernel_initializer='he_normal')(x)
    x = tfl.Conv2DTranspose(256, 3, activation="relu", padding="same", kernel_initializer='he_normal', strides=2)(x)
    x = tfl.Conv2DTranspose(128, 3, activation="relu", padding="same", kernel_initializer='he_normal')(x)
    x = tfl.Conv2DTranspose(128, 3, activation="relu", padding="same", kernel_initializer='he_normal', strides=2)(x)
    x = tfl.Conv2DTranspose(64, 3, activation="relu", padding="same", kernel_initializer='he_normal')(x)
    x = tfl.Conv2DTranspose(64, 3, activation="relu", padding="same", kernel_initializer='he_normal', strides=2)(x)

    outputs = tfl.Conv2D(1, 3, activation="sigmoid", padding="same")(x)
    model = keras.Model(inputs, outputs) 

    return model

custom_model = get_model(img_size=img_size)

这里我们有与 U-Net 相同的基本结构,包括一个收缩路径和一个扩展路径。一个有趣的观察是,与其使用最大池化层将特征空间切成两半,我们使用一个步幅为 2 的卷积层。根据 Chollet [2],这可以将特征空间切成两半,同时保留比最大池化层更多的空间信息。他指出,只要位置信息重要(如在图像分割中),最好避免破坏性的最大池化层,而改用步幅卷积(这很有趣,因为著名的 U-Net 架构确实使用了最大池化)。还要注意,我们正在进行一些数据增强,以帮助我们的模型泛化到未见的示例。

一些重要的细节:将 kernel_initializer 设置为‘he_normal’以用于 ReLU 激活会在训练稳定性方面产生意想不到的大差异。我最初低估了权重初始化的力量。与随机初始化权重不同,he_normalization 将权重初始化为均值为 0,标准差为(2 / 层的输入单元数)的平方根。在 CNN 的情况下,输入单元的数量指的是前一层特征图的通道数。已发现 he_normal 初始化能导致更快的收敛,减轻梯度消失,并改善学习。有关更多细节,请参见参考文献[3]。

指标和损失函数

二值分割可以使用几种常见的指标和损失函数。在这里,我们将使用dice 系数作为指标,并使用相应的dice 损失进行训练,因为这是比赛要求的。

让我们首先看看 dice 系数背后的数学原理:

一般形式的 dice 系数。

Dice 系数被定义为两个集合(XY)的交集,除以每个集合的和,再乘以 2。Dice 系数的值范围在 0(如果集合没有交集)到 1(如果集合完全重叠)之间。现在我们可以理解为什么这成为图像分割的一个很好的度量标准。

两个遮罩重叠的示例。橙色用于清晰度。图片来源于作者。

上述方程是 dice 系数的通用定义;当应用于 向量 量时(与集合不同),我们使用更具体的定义:

以向量形式表示的 dice 系数。

在这里,我们对每个掩膜中的每个元素(像素)进行迭代。x 表示预测掩膜中的第 i 个像素,而 y 表示真实掩膜中的对应像素。顶部是逐元素乘积,底部是分别对每个掩膜中的所有元素求和。N 表示像素的总数(预测掩膜和目标掩膜应相同)。记住,在我们的掩膜中,数字都为 0 或 1,因此真实掩膜中值为 1 的像素和预测掩膜中对应值为 0 的像素不会对 dice 分数产生贡献,这也是预期的(1 x 0 = 0)。

dice 损失 将简单地定义为 1 — Dice Score。由于 dice 分数在 0 和 1 之间,dice 损失也将在 0 和 1 之间。实际上,dice 分数和 dice 损失的和必须等于 1。它们是反向相关的。

让我们看看如何在代码中实现这一点:

from tensorflow.keras import backend as K

def dice_coef(y_true, y_pred, smooth=10e-6):
    y_true_f = K.flatten(y_true)
    y_pred_f = K.flatten(y_pred)
    intersection = K.sum(y_true_f * y_pred_f)
    dice = (2\. * intersection + smooth) / (K.sum(y_true_f) + K.sum(y_pred_f) + smooth)
    return dice

def dice_loss(y_true, y_pred):
    return 1 - dice_coef(y_true, y_pred)

在这里,我们将两个 4-D 掩膜(批量,高度,宽度,通道=1)展平为 1-D 向量,并计算批次中所有图像的 dice 分数。请注意,我们向分子和分母都添加了平滑值,以防两个掩膜不重叠时出现 0/0 的问题。

最后,我们开始训练。我们进行早停以防止过拟合,并保存最佳模型。

custom_model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.0001,
                                                        epsilon=1e-06), 
                                                        loss=[dice_loss], 
                                                        metrics=[dice_coef])
callbacks_list = [
    keras.callbacks.EarlyStopping(
        monitor="val_loss",
        patience=2,
    ),
    keras.callbacks.ModelCheckpoint(
        filepath="best-custom-model",
        monitor="val_loss",
        save_best_only=True,
    )
]

history = custom_model.fit(batched_train_dataset, epochs=20,
                    callbacks=callbacks_list,
                    validation_data=batched_val_dataset)

我们可以通过以下代码来确定我们训练的结果:

def display(display_list):
    plt.figure(figsize=(15, 15))

    title = ['Input Image', 'True Mask', 'Predicted Mask']

    for i in range(len(display_list)):
        plt.subplot(1, len(display_list), i+1)
        plt.title(title[i])
        plt.imshow(tf.keras.preprocessing.image.array_to_img(display_list[i]))
        plt.axis('off')
    plt.show()

def create_mask(pred_mask):
    mask = pred_mask[..., -1] >= 0.5
    pred_mask[..., -1] = tf.where(mask, 1, 0)
    # Return only first mask of batch
    return pred_mask[0]

def show_predictions(model, dataset=None, num=1):
    """
    Displays the first image of each of the num batches
    """
    if dataset:
        for image, mask in dataset.take(num):
            pred_mask = model.predict(image)
            display([image[0], mask[0], create_mask(pred_mask)])
    else:
        display([sample_image, sample_mask,
             create_mask(model.predict(sample_image[tf.newaxis, ...]))])

custom_model = keras.models.load_model("/kaggle/working/best-custom-model", custom_objects={'dice_coef': dice_coef, 'dice_loss': dice_loss})

show_predictions(model = custom_model, dataset = batched_train_dataset, num = 6)

经过 10 个周期后,我们达到了最高验证 dice 分数 0.8788。还不错,但不是很好。在 P100 GPU 上,这大约花了我 20 分钟。这里是我们审查的样本掩膜:

输入图像、真实掩膜和预测掩膜的比较。作者提供。

突出几个有趣的点:

  • 请注意,create_mask 是将像素值推送到 0 或 1 的函数。像素值小于 0.5 的将被裁剪为 0,我们会将该像素分配到“背景”类别。值 ≥ 0.5 的将增加到 1,我们会将该像素分配到“汽车”类别。

  • 为什么掩码的颜色是黄色和紫色,而不是黑色和白色?我们使用了:tf.keras.preprocessing.image.array_to_img() 将掩码的输出从张量转换为 PIL 图像。然后我们将图像传递给 plt.imshow()。来自文档 我们看到单通道图像的默认颜色图是“viridis”(3 通道 RGB 图像保持原样)。viridis 颜色图将低值转化为深紫色,高值转化为黄色。这个颜色图显然可以帮助色盲人士准确查看图像中的颜色。我们本可以通过添加 cmap=“grayscale” 作为参数来解决这个问题,但这会搞砸我们的输入图像。更多信息见 此链接

viridis 颜色图,从低值(紫色)到高值(黄色)。作者提供。

构建完整的 U-Net

现在我们转向使用完整的 U-Net 架构,包含残差连接、最大池化层,并包括用于正则化的丢弃层。注意收缩路径、瓶颈层和扩展路径。丢弃层可以在收缩路径中添加,在每个块的末尾。

def conv_block(inputs=None, n_filters=64, dropout_prob=0, max_pooling=True):
    conv = Conv2D(n_filters,  
                  3,   
                  activation='relu',
                  padding='same',
                  kernel_initializer='he_normal')(inputs)
    conv = Conv2D(n_filters,  
                  3,   
                  activation='relu',
                  padding='same',
                  kernel_initializer='he_normal')(conv)

    if dropout_prob > 0:
        conv = Dropout(dropout_prob)(conv)

    if max_pooling:
        next_layer = MaxPooling2D(pool_size=(2, 2))(conv)

    else:
        next_layer = conv

    skip_connection = conv

    return next_layer, skip_connection

def upsampling_block(expansive_input, contractive_input, n_filters=64):
    up = Conv2DTranspose(
        n_filters,    
        3,    
        strides=(2, 2),
        padding='same',
        kernel_initializer='he_normal')(expansive_input)

    # Merge the previous output and the contractive_input
    merge = concatenate([up, contractive_input], axis=3)

    conv = Conv2D(n_filters,   
                  3,     
                  activation='relu',
                  padding='same',
                  kernel_initializer='he_normal')(merge)
    conv = Conv2D(n_filters,   
                  3,     
                  activation='relu',
                  padding='same',
                  kernel_initializer='he_normal')(conv)

    return conv

def unet_model(input_size=(96, 128, 3), n_filters=64, n_classes=1):
    inputs = Input(input_size)

    inputs = data_augmentation(inputs)

    # Contracting Path (encoding)
    cblock1 = conv_block(inputs, n_filters)
    cblock2 = conv_block(cblock1[0], n_filters*2)
    cblock3 = conv_block(cblock2[0], n_filters*4)
    cblock4 = conv_block(cblock3[0], n_filters*8, dropout_prob=0.3)

    # Bottleneck Layer
    cblock5 = conv_block(cblock4[0], n_filters*16, dropout_prob=0.3, max_pooling=False)

    # Expanding Path (decoding)
    ublock6 = upsampling_block(cblock5[0], cblock4[1],  n_filters*8)
    ublock7 = upsampling_block(ublock6, cblock3[1],  n_filters*4)
    ublock8 = upsampling_block(ublock7, cblock2[1],  n_filters*2)
    ublock9 = upsampling_block(ublock8, cblock1[1],  n_filters)

    conv9 = Conv2D(n_filters,
                   3,
                   activation='relu',
                   padding='same',
                   kernel_initializer='he_normal')(ublock9)

    conv10 = Conv2D(n_classes, 1, padding='same', activation="sigmoid")(conv9)

    model = tf.keras.Model(inputs=inputs, outputs=conv10)

    return model

然后我们编译 U-Net。我在第一个收缩块中使用了 64 个滤波器。这是一个超参数,你需要调整以获得最佳结果。

unet = unet_model(input_size=(img_height, img_width, num_channels), n_filters=64, n_classes=1)
unet.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.0001, epsilon=1e-06),
             loss=[dice_loss], 
             metrics=[dice_coef])

callbacks_list = [
    keras.callbacks.EarlyStopping(
        monitor="val_loss",
        patience=2,
    ),
    keras.callbacks.ModelCheckpoint(
        filepath="best-u_net-model",
        monitor="val_loss",
        save_best_only=True,
    )
]

history = unet.fit(batched_train_dataset, epochs=20,
                    callbacks=callbacks_list,
                    validation_data=batched_val_dataset)

经过 16 个训练周期,我得到了 0.9416 的验证骰子分数,比简单的 U-Net 要好得多。这不应该太令人惊讶;查看参数数量,我们从简单的 U-Net 到完整的 U-Net 增加了一个数量级。在 P100 GPU 上,这大约花费了 32 分钟。然后我们查看一下预测结果:

unet = keras.models.load_model("/kaggle/working/best-u_net-model", custom_objects={'dice_coef': dice_coef, 'dice_loss': dice_loss})

show_predictions(model = unet, dataset = batched_train_dataset, num = 6)

完整 U-Net 的预测掩码。要好得多!作者提供。

这些预测结果要好得多。从多个预测中可以看到的一点是,车上的天线对网络来说很难处理。由于图像非常像素化,我不能责怪网络未能检测到这一点。

为了提高性能,需要调整包括以下在内的超参数:

  • 下采样和上采样块的数量

  • 滤波器数量

  • 图像分辨率

  • 训练集的大小

  • 损失函数(也许将骰子损失与二进制交叉熵损失结合起来)

  • 调整优化器参数。训练稳定性似乎是两个模型的问题。来自文档:“epsilon 的默认值 1e-7 可能并不是一个好的默认值。” 增加 epsilon 一个数量级或更多可能有助于提高训练稳定性。

我们已经可以看到在 Carvana 挑战中获得优秀分数的道路。真可惜比赛已经结束了!

总结

这篇文章深入探讨了图像分割的主题,特别是二元分割。如果你记住任何东西,请记住以下内容:

  • 图像分割的目标是找到从图像中的输入像素值到模型可以用来为每个像素分配类别的输出数字的映射。

  • 第一步是将你的图像组织成 TensorFlow 数据集对象,并查看你的图像和相应的掩码。

  • 关于模型架构,不需要重新发明轮子:我们知道 U-Net 效果良好。

  • Dice 得分是一个常用的指标,用于监控模型预测的成功。我们也可以从中获得损失函数。

未来的工作可能会将经典 U-Net 架构中的最大池化层转换为步幅卷积层。

祝你的图像分割问题好运!

参考文献

[1] O. Ronneberger, P. Fischer, 和 T. Brox, U-Net:用于生物医学图像分割的卷积网络 (2015),MICCAI 2015 国际会议

[2] F. Chollet, 《用 Python 进行深度学习》(2021),Manning Publications Co.

[3] K. He, X. Zhang, S. Ren, J. Sun, 深入探讨整流器:在 ImageNet 分类中超越人类水平的表现 (2015),国际计算机视觉大会(ICCV)

iMAP:实时建模 3D 场景

原文:towardsdatascience.com/imap-modeling-3d-scenes-in-real-time-6202365d80ee

使用手持 RGB-D 相机学习 3D 环境

Cameron R. Wolfe, Ph.D.Towards Data Science Cameron R. Wolfe, Ph.D.

·发表于Towards Data Science ·15 分钟阅读·2023 年 5 月 12 日

--

(照片由Brett Zeck提供,来源于Unsplash

到目前为止,我们只见过用于建模 3D 场景的离线方法(例如,NeRFSRNsDeepSDF [2, 3, 4])。尽管这些方法的表现令人印象深刻,但它们需要数天甚至数周的计算时间来训练底层的神经网络。例如,NeRFs 仅用于表示单个场景的训练时间接近两天。而且,使用神经网络评估新的场景视角也可能相当昂贵!鉴于此,我们可能会想知道是否有可能更快地学习场景表示。

[1] 中探讨了这个问题,提出了 iMAP,一个用于实时表示场景和定位(即跟踪设备姿态)设备的系统。为了理解这意味着什么,考虑一个在场景中移动并捕捉周围环境的相机。iMAP 的任务是 (i) 获取这些数据,(ii) 建立被观察场景的 3D 表示,并且 (iii) 推断相机(即设备)在捕捉场景时的位置和方向!

iMAP 采用了一种与 NeRF [2] 非常相似的方案,但有一些不同之处:

  1. 它基于 RGB-D 数据。

  2. 假设有一个流媒体设置。

因此,模型接收深度和颜色信息作为输入。此外,学习过程从完全随机的初始化开始,iMAP 必须实时学习新的 RGB-D 图像。鉴于这种设置,iMAP 被期望(i) 模拟场景和 (ii) 预测每个输入图像的 RGB-D 相机姿态(即,先前的方法将姿态信息作为输入!)。尽管训练设置非常困难,iMAP 仍然可以实时学习整个房间的 3D 表示!

(来自 [1])

为什么这篇论文很重要? 这篇文章是我关于 3D 形状和场景深度学习系列的一部分。该领域最近被 NeRF [2] 的提议所革新。通过 NeRF 表示,我们可以生成场景的任意数量的合成视点,甚至生成相关对象的 3D 表示;见下文。

iMAP 在 NeRF 之后稍晚提出,它能够在不需要几天训练时间的情况下产生高质量的场景表示。与 NeRF 相比,iMAP 以一种廉价、即刻的方式进行学习,并且表现相对较好。

背景

我们在本系列的先前概述中看到了一些重要的背景概念,这些概念将在此处相关:

  • 前馈神经网络 [link]

  • 表示 3D 形状 [link]

  • 相机姿态 [link](滚动到“相机视点”子标题)

  • 视频中的“帧”是什么? [link]

为了了解 iMAP 所需的所有背景,我们需要快速介绍 SLAM 系统和在线学习的概念。

什么是 SLAM?

我们见过的先前场景表示方法使用深度神经网络通过从 3D 空间的可用观察数据(例如,点云数据、图像等)中学习来形成对底层形状或场景的隐式表示。iMAP 有点不同,因为它是一个同时定位与映射(SLAM)系统。这意味着 iMAP 执行两个任务:

  1. 定位:跟踪捕捉底层场景的相机的位置和姿态。

  2. 映射:形成对底层场景的表示。

我们见过的大多数先前技术仅执行映射,但 iMAP 通过同时执行定位而超越了这些技术。即,底层场景的视点实时传递给 iMAP 系统,该系统既映射底层场景,又预测相机在场景中的轨迹。

(来自 [1])

SLAM 系统的输出,如上所示,有两个组成部分。生成了场景的 3D 表示。在这个表示中,我们可以看到相机在捕捉场景时的轨迹,通过黄色线(相机位置)和相关的 3D 边界框(相机姿态)表示。

这有什么不同? 除了对输入数据预测相机姿态之外,SLAM 系统与我们目前见到的其他方法不同,主要在于它们接收数据的方式。也就是说,以前的方法中我们 (i) 获取一组场景的图像,并 (ii) 在这些图像上训练一个神经网络来建模场景。而 SLAM 系统则以流式的方式接收数据。当系统接收到新的图像时,它必须处理这些图像,预测一个姿态,并实时更新其基础场景表示。

在线学习

离线训练过程(由作者创建)

通常,深度神经网络是以离线方式进行训练的。我们有一个大的、静态的训练数据集,并允许我们的神经网络对这个数据集进行多次训练(或训练周期);见上文。但,当我们的数据集不是静态的 时会发生什么?例如,我们可能会实时接收新数据或纠正应用于现有数据的标签。

在线和离线训练设置的术语(由作者创建)

我们通常将这种设置称为“在线学习”,这指的是我们按顺序接收新的数据以训练神经网络。

存在许多不同类型的在线学习;见上文。例如,增量学习假设神经网络按顺序接收新的数据批次,而流式学习则要求网络一次接收一个样本。所有形式的在线学习都假设数据是在一次通过中学习的——我们不能在接收到新数据后“回顾”旧数据。

灾难性遗忘。 在线学习被认为比离线学习更困难,因为在训练过程中我们从未能够访问完整的数据集。相反,我们必须在顺序提供的小数据子集上进行学习。如果接收到的数据是非独立同分布的,我们的神经网络可能会遭受灾难性遗忘。如[6]中所讨论的,灾难性遗忘指的是神经网络在从新数据中学习时,完全遗忘了较旧的数据。

灾难性遗忘的描述(由作者创建)

例如,如果我们正在学习分类猫和狗,也许我们会依次收到大量只有猫的图片的数据(例如,这可能发生在某人的 IoT 门铃摄像头上!)。在这种情况下,神经网络会长时间只学习猫的图片,导致它灾难性地忘记狗的概念。如果输入的数据在猫和狗之间均匀分布,这将不是问题。遗忘发生是因为输入数据是非独立同分布的,而在线学习技术旨在避免这种遗忘。

重放缓冲区。 避免灾难性遗忘的一种流行方法是通过重放缓冲区。从高层次来看,重放缓冲区只存储过去观察到的数据缓存。然后,当网络在新数据上更新时,我们也可以从重放缓冲区中抽取一些数据。这样,我们就能获得神经网络曾见过的数据的均等采样;见下文。

重放机制的基本示意图(由作者创建)

所有在线学习技术的共同目标是通过最小化灾难性遗忘的影响来最大化神经网络的性能。虽然重放缓冲区被广泛使用、简单且非常有效,但仍然存在许多其他技术——在线学习是深度学习社区中的一个活跃研究领域。关于一些现有技术的概述,我建议阅读我下面的总结!

  • 在线学习技术概述 [link]

  • 流式学习技术概述 [link]

我们为什么要关注在线学习? iMAP 是一个 SLAM 系统。根本上,SLAM 系统与在线学习有很大关系,因为它们需要接收场景图像流并提供跟踪和映射结果。值得注意的是,iMAP 是一种基于在线学习的技术,依靠重放缓冲区实时表示和跟踪基础场景!

iMAP 如何运作?

在首次了解 iMAP 后,它可能看起来好得难以置信。首先,iMAP 解决了比之前的工作更难的问题——相机位姿信息是从 RGB-D 数据中预测的,而不是给定的。然后,我们还必须实时学习整个场景表示,而不是训练几天?这简直不可能……

(来源于 [1])

但确实是这样!iMAP 通过包括两个部分的处理管道完成所有这些:

  1. 跟踪:预测相机在场景中移动时的位置和姿态。

  2. 映射:学习 3D 场景表示。

这两个组件并行运行,因为 RGB-D 相机从不同视角捕捉底层场景。值得注意的是,跟踪必须运行得非常快,因为我们尝试在新数据到来时主动定位相机。映射组件也是并行进行的,但它只在对表示底层场景真正重要的关键帧上操作,这使得 iMAP 能够实时学习。让我们深入了解一些细节吧!

iMAP 网络架构(作者创建)

网络。 理解 iMAP 的第一步是了解其基于的神经网络。与大多数先前的工作一样,iMAP 使用前馈网络架构。该网络将 3D 坐标作为输入,并产生 RGB 颜色和体积密度(即,捕捉不透明度)作为输出;见上文。

值得注意的是,iMAP 的网络不将 视角作为输入,正如 NeRF [2] 中那样,因为它不尝试建模视角相关效应(例如反射)。与 NeRF [2] 相似,iMAP 在将坐标作为输入之前将其转换为更高维的位置信息嵌入,遵循 [5] 的方法;见下文。

iMAP 网络架构与位置信息嵌入(作者创建)

关于渲染的快速说明。 假设可以访问相机姿态(我们稍后会学习 iMAP 如何预测这些姿态),我们可以在许多不同的空间位置上评估这个网络,然后使用类似于 Ray Marching 的方法将颜色和深度信息聚合成底层场景的渲染图像。 [1] 的渲染方法类似于 NeRF 的方法,但我们希望在我们的渲染中包括深度信息(即,渲染 RGB-D 图像)。这使得渲染过程略有不同,但基本思路是相同的,整个过程仍然是可微分的。

我们如何优化这个? iMAP 正在尝试学习:

  • 前馈场景网络的参数。

  • 来自 RGB-D 相机的输入帧的相机姿态。

为了训练 iMAP 系统以准确预测这些信息,我们依赖两种类型的损失度量:

  1. 光度学:RGB 像素值中的误差。

  2. 几何学:深度信息中的误差。

这些误差是通过使用 iMAP 渲染底层场景的 RGB-D 视图计算的,然后将此渲染结果与从相机捕获的地面真实样本进行比较。为了使这个比较更高效,我们仅考虑 RGB-D 图像中的一部分像素;见下文。

(来自 [1])

为了共同优化光度和几何误差,我们只是通过加权和将它们结合起来;见下文。

(来自 [1])

要用 iMAP 渲染图像,我们必须:

  1. 使用场景网络来获取颜色和几何信息。

  2. 推断正确的相机姿态。

  3. 基于这些综合信息进行渲染和 RGB-D 视点的生成。

所有这些步骤都是可微的,因此我们可以使用诸如随机梯度下降之类的技术来优化光度和几何损失,从而训练系统生成与真实 RGB-D 图像紧密匹配的渲染结果。

跟踪和映射。 在 iMAP 中,跟踪的目标是学习正在主动捕捉底层场景的相机姿态。由于我们必须对来自相机的每个输入 RGB-D 帧进行此操作,跟踪必须高效。iMAP 通过(i) 冻结场景网络和(ii) 在固定网络的基础上求解当前 RGB-D 帧的最佳姿态来处理跟踪。这只是我们尽可能高效生成的相机姿态的初步估计——我们可能会在后续进行姿态精细化。

映射的目标是联合优化场景网络和相机姿态,从而隐式地创建场景的准确表示。尝试在所有输入的 RGB-D 相机帧上进行这一操作将会非常昂贵。因此,我们基于重要性(即是否捕捉到场景的“新”部分)维护一组关键帧。在这组关键帧中,我们精细化估计的相机姿态,并训练场景网络以生成与所选关键帧紧密匹配的渲染图像。

(来源于 [1])

综合所有内容。 上图展示了完整的 iMAP 框架。对每个输入帧进行跟踪,以预测相机姿态信息。在此过程中,前馈网络保持冻结,以提高跟踪效率。如果一个帧被选为关键帧,它会被添加到关键帧集合中,并对这些帧的姿态进行精细化,并用于训练场景网络。

(来源于 [1])

为了提高效率,映射过程与跟踪过程并行运行,并仅从关键帧集合中采样训练数据,该集合比总的输入图像数量要小得多。此外,损失只在一个小的像素子集上计算,这些像素子集通过层次化策略(即主动采样)来选择,该策略识别图像中具有最高损失值的区域(即场景中的密集或详细区域),并优先在这些区域进行采样;见上文。

在线学习。请记住,iMAP 初始时是随机初始化的。当 RGB-D 相机在场景中移动时,iMAP (i) 通过跟踪模块跟踪相机,并 (ii) 开始选择关键帧以更新映射模块。这组关键帧作为 iMAP 的重播缓冲区。也就是说,前馈场景网络(及相关相机姿态)通过聚合相关的关键帧并在跟踪过程中并行更新这些数据,以在线方式进行训练。映射过程中的每次更新考虑五个帧:三个随机关键帧、最新的关键帧以及当前的 RGB-D 帧。

(来自 [1])

前馈场景网络不会遭受灾难性遗忘,因为它从多样的关键帧集合中采样训练数据,而不是仅仅在接收到的数据流上进行训练。然而,与普通的重播机制不同,iMAP 遵循特定的策略,从重播缓冲区中采样训练数据。特别是,具有较高损失值的关键帧被优先考虑(即,关键帧主动采样);见上文。

它真的有效吗?

iMAP 在真实世界(即来自手持 RGB-D 相机)和合成场景数据集(例如,Replica 数据集)上进行了评估和与几个传统 SLAM 基线的比较。值得注意的是,iMAP 以 10 Hz 的频率处理每个接收的帧。为了评估,可以通过在 体素网格 上查询神经网络并运行 marching cubes 算法来恢复网格重建(如果有地面真值则可以进行比较)。

(来自 [1])

在合成场景中,我们看到 iMAP 在共同创建一致的场景重建(见下文)和准确的相机轨迹(见上文)方面非常强大。实际上,iMAP 被发现比基线更能准确填补场景中未观察到的部分。这种好处得益于深度学习利用先前数据和 归纳偏差 以合理/可预测的方式处理模糊性的能力。

(来自 [1])

iMAP 凭借其在每个帧中对 3D 场景表示和相机姿态的联合优化,能够准确地同时进行跟踪和映射。尽管学习过程从完全随机的初始化开始,但 iMAP 很快学会了准确的场景表示和跟踪信息。最值得注意的是,iMAP 框架可以用于任何规模,从小物体到包含各种详细物体的整个房间;见下文。

(来自 [1])

尽管制作了相对准确的场景表示,iMAP 的内存使用量与 SLAM 基准相比仍然很低。iMAP 只需要足够的内存来存储关键帧(包括相关数据)和神经网络的参数;见下文。

(来自 [1])

在来自手持 RGB-D 摄像头的真实数据上,iMAP 继续超越 SLAM 基准。特别是,iMAP 在准确渲染深度摄像头读数不准确的场景区域方面意外有效(例如,这种情况通常发生在黑色、反射性或透明表面上);见下文。

(来自 [1])

主要结论

与我们目前见过的大多数技术相比,iMAP 非常不同。特别是,它是一个 SLAM 系统,这意味着它除了构建场景表示外,还执行定位。此外,它依赖于在线学习技术,实时学习有关场景的所有内容。iMAP 从随机初始化开始,从零开始学习表示基础场景!一些主要结论如下。

(来自 [1])

超级快! 我们见过的大多数 3D 场景表示技术都相当昂贵。例如,NeRFs 需要 2 天才能在单个 GPU 上训练,而 LLFF 在生成新场景视图之前需要约 10 分钟的预处理。iMAP 可以实时学习所有内容,只要 RGB-D 摄像头提供新的图像。这在创建场景表示的计算成本上是一个巨大的变化。上面展示了 iMAP 跟踪和映射管道的一些时间数据。iMAP 在 PyTorch 中实现,并且可以在桌面 CPU/GPU 系统上运行。

为什么使用深度学习? 当我们将 iMAP 系统与其他 SLAM 基准进行比较时,我们会发现它能够“填补”其他系统留下的空白区域。简而言之,iMAP 可以合理地推测在场景中未被明确观察到的区域的内容。这种能力得益于深度前馈神经网络的使用,该网络利用数据/架构中的先验信息,从有限的数据中估计几何形状。

动态学习。当开始学习一个场景时,iMAP 并没有任何信息。实际上,它从完全随机的初始化开始,然后使用在线学习技术从输入数据中动态学习场景的表示。考虑到之前的方法(例如,NeRF)需要几天的训练时间来表示一个场景,这种方法能够很好地工作确实非常令人惊讶。iMAP 向我们展示了,可能存在一些捷径或更轻量的技术,使我们更容易获得高质量的场景表示。

位置嵌入。虽然这是一个较小的点,但我们通过 iMAP 看到,位置编码,最初在 NeRFs 中出现,现在已经成为标准。请记住,将输入坐标转换为更高维的位置信息嵌入,然后作为输入传递给前馈网络,可以更容易地学习高频特征。在这里,我们再次看到位置嵌入的应用,表明这种方法正在成为标准。

相关帖子

  • 理解 NeRFs [link]

  • 本地光场融合 [link]

  • 场景表示网络 [link]

  • 使用 ONets 的形状重建 [link]

  • 深度 SDF 的 3D 生成建模 [link]

结束语

非常感谢阅读这篇文章。我是 Cameron R. WolfeRebuy 的人工智能总监以及莱斯大学的博士生。我研究深度学习的实证和理论基础。你也可以查看我在 medium 上的 其他著作!如果你喜欢这篇文章,请在 twitter 上关注我或订阅我的 Deep (Learning) Focus newsletter,在这里我通过易于理解的热门论文概述,帮助读者深入了解深度学习研究中的主题。

参考文献

[1] Sucar, Edgar, 等. “iMAP: Implicit mapping and positioning in real-time.” Proceedings of the IEEE/CVF International Conference on Computer Vision. 2021.

[2] Mildenhall, Ben, 等. “Nerf: Representing scenes as neural radiance fields for view synthesis.” Communications of the ACM 65.1 (2021): 99–106.

[3] Sitzmann, Vincent, Michael Zollhöfer, 和 Gordon Wetzstein. “Scene representation networks: Continuous 3d-structure-aware neural scene representations.” Advances in Neural Information Processing Systems 32 (2019).

[4] Park, Jeong Joon 等人。“Deepsdf: 学习用于形状表示的连续符号距离函数。” IEEE/CVF 计算机视觉与模式识别会议论文集。2019 年。

[5] Tancik, Matthew 等人。“傅里叶特征使网络能够在低维领域学习高频函数。” 神经信息处理系统进展 33 (2020): 7537–7547。

[6] Kemker, Ronald 等人。“测量神经网络中的灾难性遗忘。” 美国人工智能协会会议论文集。第 32 卷,第 1 期,2018 年。

模仿模型与开源 LLM 革命

原文:towardsdatascience.com/imitation-models-and-the-open-source-llm-revolution-431ce48d4bae

像 ChatGPT 和 GPT-4 这样的专有 LLM 是否真的容易复制?

Cameron R. Wolfe, Ph.D.Towards Data Science Cameron R. Wolfe, Ph.D.

·发布于 Towards Data Science ·阅读时间 15 分钟·2023 年 9 月 27 日

--

(图片由 Tanbir Mahmud 提供,来源于 Unsplash

LLaMA 套件 [2] 的大型语言模型(LLMs)的提出引发了关于开源 LLM 的大量出版物。在许多情况下,这些工作的目标是便宜地生产出较小的开源 LLM(用于研究目的),其质量与像 ChatGPTGPT-4 这样的专有模型相当。这些模型采用了模仿策略,通过更强大的 LLM 上的合成对话数据来微调基础 LLM。尽管训练成本低,这些模型的表现似乎与专有 LLM(如 ChatGPT)相当。因此,深度学习研究社区迅速采纳了开源 LLM 将主导未来的观点 —— 再现专有模型的开源变体既容易又具成本效益

“最强大的 LLM 会是闭源的,还是会自由分发供任何人使用、修改和扩展?” — 来源于 [1]

不幸的是,对这些模型的初步评估依赖于其他 LLM(例如 GPT-4)或人工众包工人的评分,这些评估有些粗略。模仿模型的表现是否真的能与 ChatGPT 等模型相匹配? 为了更严谨地回答这个问题,我们将研究最近的研究,分析模仿模型是否真正去除了专有 LLM 的“护城河”。有趣的是,我们将看到这些便宜的强大 LLM 的复制品在人类评估中表现良好,因为它们能够学习强大 LLM 的风格。然而,它们缺乏事实性,在更广泛和有针对性的评估中表现较差。实际上,模仿模型的表现远不如 ChatGPT 这样的专有模型。

(来自[1])

模型模仿

“模型模仿的前提是,一旦通过 API 提供了专有 LM,就可以收集 API 输出的数据集,并用它来微调一个开源 LM。” — 来源于[1]

在本概述中,我们将看到的大多数模型都是通过模型模仿策略进行训练的。这种策略基于更通用的知识蒸馏思想,是一种看似有效的方法,可以微调较不强大的 LLM,使其表现得更类似于强大的 LLM,如 ChatGPT 和 GPT-4。为此,我们只需:

  • 从更强大的模型中收集对话示例(例如,使用 OpenAI API)。

  • 使用它们以正常的语言建模目标微调较小的模型。

尽管这种方法(尽管在商业上不可行)被各种开源 LLM 广泛使用——包括 Alpaca、Vicuna、Koala 等[3, 4, 5]——以创建与 ChatGPT 或 GPT-4 的质量更接近的语言模型。

(来自[7])

知识蒸馏。 深度神经网络的知识蒸馏思想最初在[1]中被探索。简单来说,知识蒸馏使用(大型)完全训练的神经网络作为另一个(小型)神经网络的训练信号;见上文。如果我们同时使用i) 正常训练数据和ii) 大型、强大神经网络对该数据的输出来训练神经网络,那么通常会比仅用数据训练神经网络获得更好的结果。通过使用其输出作为训练目标,我们可以将一些信息从较大的“教师”网络提炼到正在训练的较小的“学生”网络中。有关更多详细信息,请查看链接这里。

尽管存在多种类型的知识蒸馏,但本概述中考虑的变体称为模型模仿,其中我们使用教师 LLM 的输出作为另一个 LLM 的监督微调的训练目标。

模型模仿的类型。虽然在线上有各种高质量的 LLM 可用,但其中许多只能通过黑箱 API访问。我们不能直接访问模型本身,只能向模型提供输入并接收输出(可能还附带有对数概率)。模型模仿从这些 API 中收集数据,并用于微调,使任何模型都能模仿专有 LLM 的输出。模仿有两种基本类型:

  • 局部模仿:学习在特定任务上模仿模型的行为,而不是整体模仿其行为。

  • 广泛模仿:学习在各种不同话题中广泛地模仿模型的行为。

广泛模仿(通常)比局部模仿更难,因为它旨在全面捕捉模型的行为。虽然模仿特定任务不难,但全面复制模型的行为需要大量数据,并且可能相当困难。

“广泛覆盖的模仿是具有挑战性的,因为(1)必须收集一个极其多样化的模仿数据集,以及(2)模仿模型必须捕捉到这种广泛的数据分布,并在大量的保留样本上类似于目标模型进行泛化。” — 引自[1]

LLaMA 的影响

近期对开源 LLM 的研究广泛探讨了模型模仿。这一研究方向始于LLaMA [2] 的提出,并迅速扩展到后续模型,如 Alpaca、Vicuna、Koala 等[3, 4, 5]。我们在之前的综述中了解了大多数这些模型:

  • LLaMA:人人都能使用的 LLM [link]

  • 超越 LLaMA:开放 LLM 的力量 [link]

在这里,我们将快速介绍这些模型的基本知识,并提供相关背景,使本概述更易于理解。

什么是 LLaMA?

LLaMA 催生了开源 LLM 的爆炸性增长(来源于[3, 4, 5, 16]和 DreamStudio)

LLaMA 不是单一的语言模型,而是一套从 70 亿到 650 亿参数的 LLM。受到 Chinchilla [13] 的启发,这些 LLM 比其对手稍小,但经过了广泛的预训练(即,更小的模型,更多的 tokens)。LLaMA 模型表现惊人;例如,130 亿参数的模型与 GPT-3 [14] 相当,而 650 亿参数的模型超越了 PaLM [15] 的表现。

完全开源。 与使用公共和专有数据的闭源模型不同,LLaMA 仅使用公开可用的数据进行预训练——LLaMA 模型可以完全从在线资源中复制!在公开发布用于研究目的后,该模型的权重被“泄露”到网上。即便如此,即使有人访问模型的权重,LLaMA 仍然被禁止用于任何商业应用。

模仿模型:阿尔帕卡、维库纳、考拉以及更多

(来自 [3, 4, 5, 16])

有趣的是,LLaMA 权重的泄露导致了该模型受欢迎程度的巨大爆炸。研究人员很快开始发布各种有趣的开源衍生模型。主要是,LLaMA 被用来创建基于与强大 LLM 如 ChatGPT 对话数据的模仿模型。让我们来看看一些从 LLaMA 衍生出的流行 LLM。

阿尔帕卡 [3] 是 LLaMA-7B LLM 的一个微调版本。微调过程基于自我指导 [17],其中从表现更高的 LLM(即 text-davinci-003)收集了遵循指令的数据,用于监督微调。阿尔帕卡的整个微调过程成本仅为 $600(包括数据收集和微调)。

维库纳 [4] 是一个开源聊天机器人,通过对 LLaMA-13B 进行微调(即性能与 GPT-3 相当)创建的。维库纳使用了与 ChatGPT 的用户对话示例进行微调,整个微调过程可以以 $300 的成本复制。与 Alpaca 相比,维库纳更接近于 ChatGPT,并生成详细且结构化的回答。

考拉 [5] 是一种基于对话数据微调的 LLaMA-13B 版本,这些对话数据来自各种来源,包括公共数据集和与其他高质量 LLM 的对话,这些数据在互联网上可以获得。与 Alpaca 相比,Koala 在更多对话数据上进行了微调,并且评估更为广泛(使用了更多的冠状工作者)。

GPT4ALL [16] 是一个经过微调的 LLaMA-7B 模型,该模型在超过 80 万次来自 GPT-3.5-turbo 的聊天完成数据上进行了训练。除了发布代码和模型,GPT4ALL 的作者还发布了模型的 4 位量化权重,可以用于在 CPU 上运行模型推理。因此,我们可以在普通笔记本电脑上使用这个模型!

“开源模型更快、更可定制、更隐私,并且……更强大。它们以 $100 和 13B 参数完成的任务, [Google] 则需要 $10M 和 540B 参数才能做到,而且还需要数周,而不是数月。” — 来自 [9]

模仿模型的巨大潜力。 上述模型在短时间内相继发布,并且(在大多数情况下)声称取得了与 ChatGPT 或 GPT-4 等顶级专有模型相当的结果。因此,研究界迅速采纳了开源模型将很快主导 LLM 领域的观点。但,情况真的如此吗?

我们是否遗漏了什么?

基于 LLaMA 的开源模仿模型似乎表现良好,因为它们在指令跟随方面远胜于基础 LLM(即,已经预训练但未微调的模型),且风格与 ChatGPT 相当。事实上,众包工作者最初评估经过训练以模仿 ChatGPT 的 LLaMA-13B 模型的输出时,70%的时间认为其表现更好;见下文。

(来自 [1])

考虑到这些结果,模仿模型似乎提供了一种将任何专有模型的能力提炼到一个较小的开源 LLM 中的简单方法。如果确实如此,我们可以通过仅使用微调和模仿数据,匹配最佳专有模型的性能,使得像 GPT-4 这样的闭源模型没有真正的优势

(不幸的)真相。 尽管轻松重新创建开源专有模型的变体用于研究目的是很有吸引力的,但使用众包工作者进行的评估可能具有误导性。一个模型仅通过输出具有正确风格和结构的答案就能得分高,即使答案在事实层面上较弱或不正确。为什么会这样? 验证事实的正确性需要众包工作者投入更多时间(或现有知识)。

(来自 [3, 4, 5])

开源 LLM 是如何被评估的? 有了这些考虑,我们可能会开始质疑,后 LLaMA 时代的 LLM 是否真的在缩小付费 LLM 与开源 LLM 之间的差距。这些模型确实令人兴奋和印象深刻,但当我们查看它们的评估方式时,通常会看到评估是:

  1. 不够全面

  2. 主要基于人类(或 LLM)评估

因此,考虑到人工评估的局限性,关于这些模型的真实质量容易被误导。简单来说,这些模型的评估不够严格,因此无法准确反映其质量

模仿专有 LLM 的虚假承诺 [1]

(来自 [1])

[1]中的作者旨在全面分析模型模仿的性能,从而回答问题:我们真的能用较弱的开源模型模仿专有 LLM 吗? 各种模型在不同的模仿数据集上进行了微调,然后通过众包工作者和各种自然语言基准进行了广泛评估。最初,通过 ChatGPT 模型模仿生成的 LLM 似乎表现良好,但有针对性的评估表明,它们在缩小基础 LLM(即 LLaMA [2])与 ChatGPT 之间的差距方面远不如预期。这些模型在事实性方面较差,仅在那些在微调集中大量出现的任务上有所改进。在微调期间未见过的任务上,这些模型往往在准确性上下降

实验设置

[1]中的分析通过探索多种实验设置,批判性地评估了近期关于模型模仿的研究。所有使用的模型都是仅解码器变换器,包括 GPT-2 [6]、LLaMA-7B 和 LLaMA-13B [2]。评估使用了GPT-4、众包工作者和广泛使用的自然语言基准。

(来自 [1])

构建数据集。 微调数据集是通过结合人工和 LLM 提供的示例创建的,既用于本地模仿也用于广泛模仿。对于本地模仿,通过引导自然问题数据集(即基于维基百科的事实知识)来创建特定任务的微调数据集。具体而言,[1]中的作者从自然问题数据集中抽取了一小部分 QA 对,然后提示 ChatGPT 策划了 6,000 个类似问题的示例;见上文。

创建广泛模仿数据集更为困难,因为数据需要全面覆盖期望的 LLM 行为。为了创建这样的数据集,[1]中的作者依赖于来自ShareGPT、以 ChatGPT 为焦点的 discord 服务器(例如,TuringAI)以及 Reddit 上的r/ChatGPT等来源的公共高质量对话。结果是~130K 个自由收集的对话示例——称为 ShareGPT-Mix——用于模仿微调。这些数据的质量很高,并且指令的多样性很大——最相似的用户查询的 BLEU 分数相似度仅为 8%。每个 ShareGPT-Mix 对话示例通过添加特殊标记来标记每个用户查询和模型输出的开始进行后处理;见下文。

(来自 [1])

微调方法。 模型使用标准的语言建模损失进行微调。然而,这种损失只应用于对应于模型输出的标记部分。换句话说,微调损失仅应用于上图中每个对话示例的蓝色部分。进行多次微调实验,数据集大小从 0.3M 到 150M 标记不等。

模仿模型真的有用吗?

初看起来,通过 ShareGPT-mix 模仿数据训练的模型质量似乎相当高。虽然基础模型未能遵循指令,但经过模仿微调的变体能够保持任务一致性,并以类似 ChatGPT 的方式解决问题。而且,模型规模的增加导致性能持续改进,这些模型在与 GPT-4 进行评估时获得了积极评价;见上文。

然而,更详细的分析似乎表明这些结果可能略显误导。例如,随着更多模仿数据的使用,人类评估分数很快就会饱和(甚至下降);见下文。如此惊人的结果表明,在这些模型的评估中,我们可能遗漏了某些东西。

(来源于 [1])

针对性评估。 当模仿模型在更广泛的自然语言基准测试中进行评估时,我们看到它们的表现与相应的基础 LLM 相当或更差。换句话说,对模仿进行微调并没有提高在更广泛任务中的表现;见下文。

(来源于 [1])

在像 MMLU [10]、HumanEval [11] 和 Natural Questions [12] 等基准测试中的表现平平,表明与基础 LLMs 相比,模仿模型在事实准确性、编码能力或解决问题的能力上并没有提升。鉴于 LLM 的知识大多 是在预训练期间获得的,这一趋势是可以理解的。我们在 [1] 中看到,模仿模型可以匹配像 ChatGPT(见下文)这样的强大 LLMs 的风格,但它们缺乏相同的知识库。这些模型更频繁地出现幻觉现象,在没有大量研究或时间投入的基本人类评估中很难发现。

(来源于 [1])

局部模仿效果良好。 尽管在更广泛任务集上评估时模仿模型存在局限性,我们发现局部模仿实际上非常有效。通过模仿可以学习 ChatGPT 的特定行为,但当我们尝试更广泛地模仿行为时则会遇到障碍;见下文。局部模仿可以成为适应开源大型语言模型(LLM)以解决特定任务或在特定场景中模仿专有模型的一个有用解决方案。

(来自 [1])

为了广泛模仿像 ChatGPT 这样的模型的行为,我们需要一个显著更大且更多样化的模仿数据源。然而,策划这个数据集可能不是最佳方法——我们发现仅仅增加基础模型的规模可以带来更大的性能提升。因此,创建更强大的基础 LLM 可能是开源 LLM 研究比创建廉价模仿模型更有前景的方向。

“我们认为,改善开源模型的最大杠杆作用在于解决开发更好基础语言模型的难题,而不是通过模仿专有系统来走捷径。” — 来自 [1]

最后的思考

尽管深度学习社区多年来一直推崇开放和透明,但 LLM 的流行爆炸催生了一种替代范式,即开发使用不提供实际模型访问的专有 API 的系统。为了应对这种远离开源的转变,研究人员开发了开源 LLM 替代方案。模仿模型的创建使这一研究领域似乎进展极快,许多人因此认为专有 LLM 会迅速失宠。在这一概述中,我们看到这些模仿 LLM 存在重大局限性。然而,强大的开源 LLM 的开发仍在持续推进。这项工作的主要结论如下。

严格评估的重要性。 模仿模型在由人工进行的定性评估中表现良好。然而,当进行更严格的定量评估时,这些模型的表现则显得有些平庸(在某些情况下甚至不如基础模型)!这项工作的发现突显了研究中严格评估的重要性。为了推动一个领域的发展,我们需要确保提出的技术和模型实际上在改进现有的技术和模型。

局部模仿仍然非常有用。 尽管模仿模型在广泛评估时表现不佳,但它们在其微调数据集中包含的任何任务上表现相当好。因此,局部模仿仍然是一个有用且有效的技术。我们可以很容易地通过模仿来教会一个较小的开源 LLM 在特定领域中匹配像 ChatGPT 这样的流行模型的性能和行为。然而,当尝试整体复制专有 LLM 的行为时,我们会遇到问题。这将需要策划一个大量的对话示例数据集用于模仿微调。

开源 LLM 的影响。 如我们所见,模仿模型(尽管对局部模仿和特定用例有用)并不是生产高质量开源基础模型的通用解决方案。然而,我们在 [1] 中看到 LLM 性能随着基础模型的规模和质量的提高而持续改进。这一发现表明,创建更大、更强大的基础模型对于开源 LLM 的进一步进展是必要的。

与我联系!

非常感谢阅读这篇文章。我是 Cameron R. WolfeRebuy 的人工智能总监。我研究深度学习的实证和理论基础。如果你喜欢这个概述,请订阅我的 Deep (Learning) Focus 新闻通讯,在这里我通过从基础开始的相关主题概述帮助读者理解人工智能研究。你还可以在 XLinkedIn 上关注我,或者查看我在 medium 上的 其他著作

参考书目

[1] Gudibande, Arnav 等。“模仿专有语言模型的虚假承诺。” arXiv 预印本 arXiv:2305.15717(2023 年)。

[2] Touvron, Hugo 等。“Llama: 开放且高效的基础语言模型。” arXiv 预印本 arXiv:2302.13971(2023 年)。

[3] Taori, Rohan 等。“斯坦福阿尔帕卡:一个跟随指令的 LLaMA 模型。”(2023 年)。

[4] Chiang, Wei-Lin 等。“Vicuna: 一个开源聊天机器人,令人印象深刻的 90%* ChatGPT 质量。”(2023 年)。

[5] Geng, Xinyang 等。“Koala: 一个用于学术研究的对话模型。”(2023 年)。

[6] Radford, Alec 等。“语言模型是无监督的多任务学习者。”

[7] Gou, Jianping 等。“知识蒸馏:综述。” 计算机视觉国际期刊 129(2021 年):1789–1819。

[8] Hinton, Geoffrey, Oriol Vinyals 和 Jeff Dean。“蒸馏神经网络中的知识。” arXiv 预印本 arXiv:1503.02531(2015 年)。

[9] Dylan Patel 和 Afzal Ahmad。谷歌“我们没有护城河,OpenAI 也没有”,2023 年。

[10] Hendrycks, Dan 等。“衡量大规模多任务语言理解。” arXiv 预印本 arXiv:2009.03300(2020 年)。

[11] Chen, Mark 等. “评估基于代码训练的大型语言模型。” arXiv 预印本 arXiv:2107.03374 (2021)。

[12] Kwiatkowski, Tom 等. “自然问题:问题回答研究基准。” 计算语言学协会会刊 7 (2019): 453–466。

[13] Hoffmann, Jordan 等. “训练计算最优的大型语言模型。” arXiv 预印本 arXiv:2203.15556 (2022)。

[14] Brown, Tom 等. “语言模型是少量样本学习者。” 神经信息处理系统进展 33 (2020): 1877–1901。

[15] Chowdhery, Aakanksha 等. “Palm: 通过路径扩展语言建模。” arXiv 预印本 arXiv:2204.02311 (2022)。

[16] Yuvanesh Anand, Zach Nussbaum, Brandon Duderstadt, Benjamin Schmidt, 和 Andriy Mulyar. GPT4All: 使用大规模数据蒸馏从 GPT-3.5-Turbo 训练助手风格的聊天机器人,2023 年。

[17] Wang, Yizhong 等. “Self-Instruct: 将语言模型与自生成指令对齐。” arXiv 预印本 arXiv:2212.10560 (2022)。

分水器和淋水屏对浓缩咖啡的影响

原文:towardsdatascience.com/impact-of-diffuser-and-shower-screens-on-espresso-6df135a0c50a?source=collection_archive---------12-----------------------#2023-06-16

咖啡数据科学

理解堆栈

Robert McKeon AloeTowards Data Science Robert McKeon Aloe

·

关注 发布于 Towards Data Science ·3 分钟阅读·2023 年 6 月 16 日

--

我对 Decent Espresso 机器的群头进行了大量测试,并对水分布进行了更仔细的观察。因此,我想要了解淋水屏、分水器和水分配器在水进入咖啡饼中的操作方式。当然,其中一些部件是为了保持群头的清洁,但我对几个问题进行了探索:

  1. 淋水屏有助于萃取吗?

  2. 分水器有助于萃取吗?

测试参数

我用 6 个月的咖啡进行了这项测试,这意味着咖啡几乎没有 CO2 或仅有很少的 CO2。然后我做了腊肠咖啡,并测量了 TDS。

总溶解固体 (TDS) 是使用折射仪测量的,这个数值与 shot 的输出重量和咖啡的输入重量结合,用于确定杯中提取的咖啡百分比,这称为 提取产率 (EY)

对于 shot 配方,我在 90°C 下进行了一次平坦的配方,流速为 2 ml/s。这个配方的结束有一个长时间的缓慢下降,以尽量避免将咖啡粉吸入组头。

对于剂量,我不得不增加剂量以减少头部空间。我也不知道应该增加多少,所以我调整了压粉。我先处理了一半的咖啡粉,然后处理第二半。我没有压第二半。

缺少压粉步骤使得头部空间很少,而不知道确切的最小值。因此可以多做一些研究并调整一些参数,但我没有。我希望获得一些数据指导下的直觉。

数据

没有淋浴屏幕我有点惊讶 shot 看起来如此正常。

这个 shot 看起来相当典型。

然后我移除了扩散器,我们孤注一掷了。

这次的 shot 看起来也很正常。我原以为会有明显的侧向通道现象,但一旦加压,情况还是一样。

在测量方面,提取产率没有显著差异。

我确实期望淋浴屏幕和扩散器对 shot 质量有积极影响。这些结果并不具有决定性或适用于所有机器,但至少,它们指向了关于组头设计的公众知识缺口。设计不能仅仅基于单次 shot,而应该通过保持设备更长时间的清洁来适应多次 shot。

如果你喜欢,可以在 TwitterYouTubeInstagram 上关注我,我会发布不同机器上制作的浓缩咖啡视频以及与浓缩咖啡相关的内容。你也可以在 LinkedIn 找到我。你还可以在 Medium 上关注我,并 订阅

我的进一步阅读:

我的书

我的链接

浓缩咖啡文章汇集

工作与学校故事集

不完美揭示:我们 MLOps 课程创建背后的迷人现实

原文:towardsdatascience.com/imperfections-unveiled-the-intriguing-reality-behind-our-mlops-course-creation-6ff7d52ecb7e

全栈 7 步骤 MLOps 框架

附加课程:‘不完美’ML 项目的幕后 — 经验教训与见解

Paul IusztinTowards Data Science Paul Iusztin

·发布在Towards Data Science ·10 分钟阅读·2023 年 6 月 19 日

--

Hassan Pasha的照片,来自Unsplash

本文代表7 节课程中的最后一节附加课程,逐步指导你如何设计、实施和部署 ML 系统,使用MLOps 最佳实践。在课程中,你构建了一个生产就绪的模型,用于预测来自丹麦的多个消费者类型在未来 24 小时的能源消耗水平。

在课程中,你学习了如何设计、编码和部署一个基于批量服务架构的 ML 系统的所有基础知识。

本课程针对中级/高级 ML 或软件工程师,希望通过构建自己的 ML 端到端项目来提升技能。

现在,证书随处可见。构建先进的端到端项目并展示出来是获得专业工程师认可的最佳方式。

目录:

  • 课程简介

  • 课程内容

  • 数据来源

  • 附加课程:‘不完美’ML 项目的幕后 — 经验教训与见解

  • 结论

  • 参考文献

课程简介

在 7 节课程中,你学会了如何:

  • 设计一个批量服务架构

  • 使用 Hopsworks 作为特征存储

  • 设计一个从 API 读取数据的特征工程管道

  • 构建一个带有超参数调优的训练管道

  • 使用 W&B 作为 ML 平台来跟踪你的实验、模型和元数据

  • 实现批量预测管道

  • 使用 Poetry 构建你自己的 Python 包

  • 部署你自己的私有 PyPi 服务器

  • 使用 Airflow 协调一切

  • 使用预测结果编写一个使用 FastAPI 和 Streamlit 的 Web 应用程序

  • 使用 Docker 容器化你的代码

  • 使用 Great Expectations 确保数据验证和完整性

  • 随时间监控预测的性能

  • 将所有内容部署到 GCP

  • 使用 GitHub Actions 构建 CI/CD 管道

如果你还没有跟进这个系列,并且觉得它可能对你有兴趣,我想让你知道,完成课程后,你将理解我之前所说的一切。最重要的是,你将看到我为什么使用这些工具以及它们如何作为一个系统协同工作。

如果你想从本课程中获得最大收益, 我建议你访问包含所有课程代码的 GitHub 仓库 。本课程旨在快速阅读和复制文章中的代码。

在课程中,你学习了如何实现下图。逐步解释之后,它看起来不那么可怕了,对吧?

课程中构建的架构图 [作者提供的图片]。

在这最后的奖励课程中,我们想讨论当前架构的潜在改进以及在课程中做出的设计选择。我们还希望强调必须做出的权衡,并给你一些未来项目的想法。

把它看作是幕后花絮部分👀

课程内容:

  1. 批处理服务。特征存储。特征工程管道。

  2. 训练管道。ML 平台。超参数调整。

  3. 批量预测管道。使用 Poetry 打包 Python 模块。

  4. 私人 PyPi 服务器。使用 Airflow 协调一切。

  5. 使用 GE 进行质量和完整性数据验证。模型性能持续监控。

  6. 使用 FastAPI 和 Streamlit 消费和可视化你的模型预测。容器化一切。

  7. 将所有 ML 组件部署到 GCP。使用 Github Actions 构建 CI/CD 管道。

  8. [附赠] ‘不完美’ ML 项目的幕后 — 经验教训和见解

奖励课程将公开分享课程的权衡、设计选择和潜在改进。

因此,我们强烈建议您阅读其余的课程,如果您对构建生产就绪的机器学习系统感兴趣 👇

## 用于构建生产就绪特征工程管道的框架

课程 1:批处理服务。特征存储。特征工程管道。

towardsdatascience.com

数据来源

我们使用了一个免费的开放 API,它提供丹麦所有能源消费者类型的每小时能源消耗值[1]。

他们提供了一个直观的界面,您可以轻松查询和可视化数据。您可以在这里访问数据 [1]。

数据有 4 个主要属性:

  • UTC 时间: 数据点被观察到时的 UTC 日期时间。

  • 价格区域: 丹麦被划分为两个价格区域:DK1 和 DK2——由大贝尔特分隔。DK1 在大贝尔特以西,DK2 在大贝尔特以东。

  • 消费者类型: 消费者类型是行业代码 DE35,由丹麦能源公司拥有和维护。

  • 总消耗: 总电力消耗,以 kWh 为单位

注意: 观察值有 15 天的滞后!但对于我们的演示用例,这不是问题,因为我们可以模拟与实时相同的步骤。

我们的网络应用程序的截图,显示了我们如何预测区域=1 和消费者类型=212 的能源消耗 [作者提供的图片]。

数据点的分辨率为每小时。例如:“2023-04-15 21:00Z”,“2023-04-15 20:00Z”,“2023-04-15 19:00Z”等。

我们将数据建模为多个时间序列。每个独特的价格区域消费者类型组合表示其独特的时间序列。

因此,我们将建立一个模型,为每个时间序列独立预测接下来的 24 小时的能源消耗。

查看下面的视频,以更好地理解数据的样子 👇

课程及数据来源概述 [作者提供的视频]。

奖励课程:‘不完美’机器学习项目的幕后——经验和见解

不再闲聊。让我们直接进入幕后 🔥

课程中构建的架构图 [作者提供的图片]。

整体代码

#1. 重复代码

主要问题在于我们在不同的 Python 模块之间有很多重复的代码,这并没有遵循万能的 DRY 原则。

比如 ML 流水线中的 settings.pyutils.py 文件,或者 UI 的 dropdown + line plot component

这段代码可以重构为一个 公共 模块,供所有其他模块共享。

#2. 使用类,而不是函数!

使用类来建模你的代码是一个好习惯,但我们在课程中只使用了函数。

我们本可以为每个流水线创建一个中央类,例如 FeaturesExtractor、Trainer 和 BatchPredictor。

此外,除了返回包含运行元数据的纯字典外,我们本可以创建一个 RunResult 类,以更好地控制数据的传递方式。

#3. 没有测试 😟

任何代码库都有一堆单元测试和集成测试来验证对代码所做的所有更改。

流水线设计

#1. DAG 有状态

由于 DAG 有状态,因此并不容易并行运行它。我们的问题在于,你需要之前运行的预测结果来计算监控指标。

因此,根据定义,你不能在不同时间点同时运行相同的 DAG 的多个并行实例。

什么时候会成为问题?

当回填时。假设你想回填最新 2 个月的每一个小时。如果你按顺序运行程序,它会花费很长时间。

作为解决方案,我们建议将监控组件移动到不同的 DAG。

#2. 避免使用 ":latest" 作为资源版本

如果你使用 ":latest" 标签来访问资源,比如:

  • 模型的工件,

  • 数据(Feature Store 特征视图或训练数据集),

  • 最佳配置工件等。

… 你在多个 ML 流水线运行之间引入了依赖关系。

这很微妙,但让我解释一下 👇

假设你并行运行两个 ML 流水线:A 和 B。流水线 A 首先生成一个新的数据集版本。然后,出于某种原因,流水线 B 在流水线 A 之前启动了训练流水线,并访问了由流水线 A 创建的“最新”数据集版本。

这在并行计算中也被称为“竞争条件”。

在这种情况下,可以通过在同一流水线的任务之间硬编码资源的版本来轻松解决。

例如,访问 "dataset:v3",而不是 "dataset:latest"

正如你所看到的,回填时速度至关重要。因此,从长远来看,DAG 的并行运行是必要的。

Airflow

#1. 使用 Docker 任务

这不一定是个问题,但我想强调的是,除了使用 Python 环境,你还可以将代码打包在 Docker 容器中 — Task Docker Decorator Docs [2]。

主要好处是,这使系统更具可扩展性。

不过好消息是,课程中学到的过程非常相似。与其将 Python 包推送到 PyPi 注册表,不如将 Docker 镜像推送到 Docker 注册表。

#2. 更小、更原子化的任务

在我们的情况下,DAG 内的任务包含了大量逻辑。它们基本上运行一个完整的应用程序。

这不一定不好,但将其分成更小的部分是一种好习惯。因此,从特定故障点调试、监控和重新启动 DAG 会更容易。

例如,我们本可以使用 Airflow 的 GCS 操作符之一来读取/写入 Python 中的 GCS 数据 — GCS Airflow 操作符 [3]。

#3. 从 Airflow 注入超参数调整设置

目前,超参数调整设置被硬编码在一个 configs/gridsearch.py 文件中。

这并不是很灵活,因为修改配置的唯一选项是将新版本推送到 git,这并不太实用。

一种解决方案是从 YAML 文件中注入设置,这可以轻松添加到 Airflow 工作流中。

一个很棒的 ML 模型 YAML 配置工具是 Hydrafacebookresearch 提供。试试看,你会感谢我的。

监控

#1. 未监控系统健康

我们本可以通过定期 ping API 的 /health 端点轻松添加一个系统健康监控机制。

我们本可以通过在用户界面上使用绿/红面板来反映这一点。

#2. 无警报

基于我们持续监控的 MAPE 计量,我们本可以添加一个报警系统,例如:

  • 警告 [threshold_B > MAPE > threshold_A]: 通知工程师某些地方可能出错;

  • 警报 [MAPE > threshold_B > threshold_A]: 通知工程师某些地方可能出错 + 触发超参数调整逻辑。

#3. 丰富用户界面

我们本可以为每个时间序列单独添加 MAPE 计量。

#4. 不要重新发明轮子!

我们实现了一个迷你监控工具仅作为示例。但在实际应用中,你应该利用现有工具,如 EvidentlyAIArize

这些工具和包已经提供了专业的解决方案。因此,你可以专注于增加价值。

#5. 监控漂移

作为一个附加功能,监控数据和概念漂移也会很有帮助。但由于我们几乎有实时 GT,这只是一个额外的需求。

Web 应用 — 预测仪表板

#1. 丰富用户界面

用户界面相当基础。例如,我们可以通过在数据无效时(未通过验证)添加文本和警报来丰富用户界面。

#2. 我们以天真的方式请求数据

我们对 API 的请求相当天真。通常,这些步骤由一系列异常来保护,以捕捉 300、400 和 500 响应代码上的不同行为。

#3. 设置是硬编码的

我们本可以通过使用 .env 文件注入设置,从而使程序更具配置性。类似于 Web 应用 FastAPI 代码。

部署与 CI/CD 管道

#1. 部分 CI 实现

为了完整地完成 CI/CD 管道,我们本可以构建 Web 应用 Docker 镜像,将它们推送到 Docker 注册表,并在将它们部署到 GCP VM 时从那里拉取。

构建 Python 包时也有同样的情况,使用 Poetry。

这是书本上教的做法。

此外,如果我们有任何测试,应该在部署代码之前运行它们。

另一个想法是运行诸如 flake8 的命令,以验证代码是否遵循 PEP8 规范。

#2. 将 PyPi 主机放在不同的 VM 上

此外,建议将 PyPi 服务器托管在不同的 VM 上,或至少与 Airflow 组件完全独立。

这样,系统将更加模块化和灵活。

#3. 将 .env 文件托管在 GCS 存储桶上

我们本可以将 .env 文件存储在 GCS 存储桶中,并在 CI/CD 管道中自动下载它们,而不是手动完成和复制。

#4. 自动化基础设施

你已经看到手动设置所需的所有 GCP 资源有多么乏味……而这只是一个小型基础设施。想象一下,当你的基础设施中有 100 或 1000 个组件时会是什么样的。

这就是为什么建议使用像 Terraform 这样的 IoC 工具来自动化基础设施创建。

结论

如果你做到这一步,我想感谢你,并告诉你我多么深切地感激你参加了我的 全栈 7 步 MLOps 框架 课程 🙏

在这个额外的课程中,你看到没有任何系统是完美的,你总是必须因为以下原因而做出某些权衡:

  • 时间限制,

  • 资源限制,

  • 规划不周。

既然你看到每个人仍在学习,并且并不全知,你就没有借口不去构建下一个令人惊叹的项目🔥

让我们在 LinkedIn 上联系,如果你有任何问题或者想分享你在本课程后构建的令人惊叹的项目,请告诉我。

在这里访问 GitHub 仓库。

💡 我的目标是帮助机器学习工程师在设计和生产 ML 系统方面提升水平。请在 LinkedIn 上关注我,或订阅我的 每周通讯 以获取更多见解!

🔥 如果你喜欢阅读类似的文章并希望支持我的写作,可以考虑成为 Medium 会员。使用我的推荐链接,你可以在不增加额外费用的情况下支持我,并享受对 Medium 丰富故事收藏的无限制访问。

[## 使用我的推荐链接加入 Medium - Paul Iusztin

作为 Medium 会员,你的一部分会员费将用于支持你阅读的作者,你可以全面访问每一个故事…

pauliusztin.medium.com](https://pauliusztin.medium.com/membership?source=post_page-----6ff7d52ecb7e--------------------------------)

谢谢 ✌🏼!

参考资料

[1] 丹麦 API 的 DE35 行业代码能耗丹麦能源数据服务

[2] 任务 Docker 装饰器,Airflow 文档

[3] GCS Airflow 操作符,Airflow 文档

在 Python 中实现具有 TTL 功能的缓存装饰器

原文:towardsdatascience.com/implement-a-cache-decorator-with-ttl-feature-in-python-1d6969b7ca3f

基于@functools.lru_cache 的装饰器支持缓存过期

Peng QianTowards Data Science Peng Qian

·发表于Towards Data Science ·5 分钟阅读·2023 年 4 月 3 日

--

Aron Visuals拍摄的照片,刊登在Unsplash

问题:

Python 的 functools 包中的lru_cache装饰器提供了基于 LRU 缓存的实现。使用这个装饰器,具有相同参数的函数在第二次执行时会显著更快。

然而,lru_cache 不能支持缓存过期。如果你希望缓存在一定时间后过期以便在下次调用函数时更新缓存,lru_cache 无法实现这一点。

解决方法:

1. 实现一个具有 TTL 功能的 lru_cache

因此,我实现了一个新的基于 lru_cache 的装饰器。这个装饰器可以接受一个 ttl 参数。这个参数可以接受一个秒数,当这个时间到期时,下一个函数调用将返回一个新值并刷新缓存。

对于需要紧急解决问题的用户,这里是源代码:

使用非常简单,如下所示:

2. 测试效果

我们使用了一个 ttl_cache,过期时间为 40 秒。然后让函数执行 10 轮,每轮 6 秒。

作者图片

我们可以看到,当它达到第 7 轮时,6*7=42(秒)。缓存被刷新,表明我们的 ttl_cache 成功了,万岁。

一些背景知识:

1. 什么是 LRU 缓存?

假设缓存的大小是固定的,如果缓存已满,则需要删除一些内容以腾出空间给新内容。但问题是,应该删除哪些内容?我们当然希望删除那些无用的缓存,继续保留有用的数据以供后续使用。那么,我们如何定义“有用”的数据呢?

LRU 缓存策略认为,最近使用的数据应该是“有用的”,而长时间未使用的数据应该是无用的。当容量满时,应优先删除那些无用的数据。

2. 如何实现 LRU 缓存?

实现 LRU 时,我们需要关注其读写性能。

在此情况下,容易想到使用 HashMap,它可以通过根据键访问数据来实现 O(1) 的速度。但更新缓存的速度无法达到 O(1),因为需要确定哪些数据是最早被访问的,这就需要遍历所有缓存数据来查找它。

因此,我们需要一种数据结构,它不仅按访问时间排序,还能在常数时间内进行随机访问。

这可以通过使用 HashMap + 双向链表来实现。HashMap 保证通过键访问的数据的时间复杂度为 O(1),而双向链表则按访问时间的顺序遍历每个数据。选择双向链表而非单向链表的原因是,它可以从链表中任意一个节点修改链表结构,而无需从头节点开始遍历。

如下图所示,黑色部分是 HashMap 的结构,红色箭头是双向链表的前向连接。可以清楚地看到数据访问顺序是 1->3->5->6->10。我们只需在每次访问后更改链表的连接顺序即可实现目标。

作者提供的图片

3. 这如何帮助我们实现 ttl_cache?

我们知道 LRU 算法使用 HashMap 来实现快速的数据读取,因此我们可以通过在过期时间后更改哈希键来实现缓存过期。

由于 lru_cache 的哈希键是基于装饰函数中所有可哈希参数计算的,因此我们只需添加一个 ttl_hash 参数,并在过期时间之后更改此参数的值。

代码解释:

1. 通用装饰器模板

当开始编写装饰器时,我会使用装饰器模板代码,这可以帮助我更快地编写装饰器。例如:

2. 延迟执行生成器代码

接下来,我们需要每次调用 ttl_cache 装饰的函数时生成一个哈希键,如果该哈希键未超过过期时间,则应保持不变。如果超过过期时间,则该哈希键应不同于先前生成的哈希键。

因此,我计划将代码开始运行的时间减去当前时间,然后将结果除以ttl参数。最后,将余数作为此哈希键。

由于我需要保存代码开始运行的时间,并且只在必要时获取最新的哈希键,我能想到的最佳方法是使用 生成器函数

通过这种方式,我可以在需要时使用next()函数获取最新的哈希键。

3. 使用 lru_cache 装饰原函数

接下来,我们将使用lru_cache装饰原函数,新函数需要传入ttl_hash参数,以便在过期时间后生成新的哈希值:

在封装的函数中,我们每次都会获得一个新的hash_key并返回一个带有ttl_hash的函数调用以实现我们的目标。

4. 别忘了复制原函数的属性

最后,别忘了将原函数的模块、名称、文档及其他属性复制到新生成的函数中。这里最简单的方法是使用functools模块中的update_wrapper

结论:

还有其他解决方案,比如 expiringdict

但这些解决方案改变了我们使用lru_cache的方式,毕竟,我们只是希望使lru_cache更强大。

本文实现的ttl_cache是线程安全的,可以在多线程环境中使用。

我希望你喜欢我对 TTL 缓存的实现。欢迎大家评论并提供宝贵的改进建议。感谢阅读。

通过 加入 Medium,你将能够无限访问我和其他成千上万位作者的所有文章。它只需你一杯咖啡的价格,但对我来说是一种很大的鼓励。

本文最初发布在:www.dataleadsfuture.com/implement-a-cache-decorator-with-time-to-live-feature-in-python/

从头开始用 PyTorch Lightning 实现和训练 CNN

原文:towardsdatascience.com/implement-and-train-a-cnn-from-scratch-with-pytorch-lightning-ce22f7dfad83

如果你还没有使用 PyTorch Lightning,你应该尝试一下。

Betty LDTowards Data Science Betty LD

·发表于 Towards Data Science ·19 分钟阅读·2023 年 8 月 8 日

--

PyTorch Lightning 的抽象概念。来自 Marc Sendra Martorell

本文是对卷积神经网络(CNN)的温和介绍。本文详细说明了为什么 PyTorch Lightning 非常出色,然后对 CNN 组件进行简要的理论讲解,接着描述了如何使用 PyTorch Lightning 库从头开始实现一个简单 CNN 架构的训练循环。

为什么选择 PyTorch Lightning?

PyTorch 是一个灵活且用户友好的库。如果说 PyTorch 对研究很棒,那么我发现 Lightning 对工程更为出色。主要优点包括:

  • 更少的代码。在运行机器学习项目时,许多事情可能会出错,因此将样板代码委托出去,专注于解决我具体问题的相关内容是很有帮助的。使用内置功能可以减少书写代码的数量,从而降低错误的概率。开发(和调试)时间也会减少。

  • 结构良好的代码

  • 高效和快速的训练。Lightning 还允许使用 PyTorch 的所有多进程和并行工作器技巧(如 DDP),无需额外编写代码。

  • 内置的 开发工具 如一致性检查(用于验证和训练循环以及模型架构)、即时创建过拟合数据集、早期停止回调、最佳权重管理等。例如 lightning.ai/docs/pytorch/stable/debug/debugging_basic.html

要了解更多官方的优秀理由,请查看 这里

简而言之,使用 PyTorch Lightning 时,我发现它易于编写、易于阅读和调试。这些活动占据了我作为机器学习工程师的大部分时间。此外,文档写得很好,包含了许多教程,因此也很容易学习

CNN 模型回顾

LeNet 是学习或复习计算机视觉深度学习架构的一个很好的起点。LeNet 是 Yann LeCun 等人在 1998 年设计的第一个成功的卷积神经网络(CNN)架构,旨在进行手写数字识别。

我们将首先通过 LeNet 架构的组件解释标准 CNN 模块的主要组成部分。

LeNet 由三种类型的层组成:

  • 卷积层

  • 池化层

  • 全连接层

1. 卷积层

卷积层负责从输入层中提取特征。使用 CNN 时,这第一层通常是图像。每个卷积层由一组可学习的滤波器(也称为“内核”)组成,这些滤波器滑动在输入层上,应用一种称为“卷积”的操作。卷积在滤波器和图像中的局部区域之间执行逐元素乘法和求和。由于对输出特征图应用了(非线性)激活函数,所以该输出也称为“激活”。在本文中,我们使用最流行的激活函数:ReLU,如下图所示。

线性整流单元(ReLU)f(x)=max(0,x)及其变体 GeLU。图片使用 Creative Common 许可证

每个卷积层后面都跟着一个激活函数,主要是为了加入非线性。没有这样的激活函数,模型将表现得像一个普通的线性模型,无论其深度如何。

更多关于卷积层的内容

下一部分解释了卷积层在 CNN 中如何使用,以及它们如何使 CNN 在各种计算机视觉任务中表现极其出色。简而言之,连续卷积层的层次结构使其在图像识别任务中非常高效。一层卷积层效率不高,但一堆卷积层就很有效。

第一层使模型能够在输入图像的局部区域中识别简单和通用的模式,然后,深层会抓取更复杂和抽象的表示。

卷积操作的示意图。滤波器大小为 3x3,步幅为 1,零填充量为 2。图片使用 MIT 许可证。

CNN 的第一卷积层通常具有较小的空间范围(例如,VGG16 的第一卷积层为 3x3 像素,LeNet 为 5x5 像素,AlexNet 为 11x11 像素)。

训练后,这些层将检测类似于经典计算机视觉技术的简单模式,如边缘和角点。

然后,中间卷积层中的滤波器变得更加复杂。相对于输入层,它们的相对尺寸比早期层大,因此它们拥有更多的上下文(因为它们能看到比前一卷积层更广泛的图像区域),并且可以开始检测更高层次的特征,如复杂的纹理、形状和物体部件。

例如,在一张日本套餐的图像中,这些层可能会检测到筷子、米饭碗和味增汤。

LeNet 架构。图像遵循 Creative Common 许可。

最后卷积层中,滤波器具有更大的空间范围(它们可以在输入层中一次性看到更多上下文),并且比早期的边缘、颜色和物体部件检测层更加专业和抽象。它们代表高层次特征,对于图像识别任务的准确决策至关重要。它们用于编码复杂的对象表示,并捕捉数据集中不同类别的独特特征。

它可以例如从法国午餐中识别出日本套餐。

以下图片总结了每一层可以“看到”的内容。

不同卷积阶段的输出特征图的示例。由斯坦福 CS231提供。图像来自github.com/cs231n/cs231n.github.io/blob/master/convolutional-networks.md,遵循 MIT 许可。

池化层

在两个连续的卷积层之间添加池化层是很常见的做法。池化层对从前一个卷积层获得的特征图进行降采样,减少数据的空间维度,同时保留重要信息。

为什么池化层有效?

  • 它们缩小了特征图,因此允许更快的计算和更低的内存需求。

  • 这个信息聚合步骤减少了模型中的参数数量,同时防止了过拟合。

  • 它们引入了一定程度的平移不变性,使得网络即使在图像中略微偏移、旋转或扭曲的情况下也能识别某些特征。对空间变化的鲁棒性有助于模型更好地泛化。

最常见的池化操作有最大池化(MAX pooling)(从窗口覆盖的元素中选择最大值作为该窗口的值)和平均池化(AVERAGE pooling)(相同,但取平均值)。

池化操作。左侧为特征图,右侧为特征切片。图像来自 Stanford CS231. MIT 许可证

全连接层

全连接(FC)层是模型的最后几层。它们负责根据早期层提取的特征做出高层决策。与具有局部可见性的卷积滤波器不同,FC 层将来自先前输出特征图的所有激活一次性连接到下一个输出特征图的激活中,就像在常规神经网络中一样。它基本上由矩阵乘法和偏置偏移组成。

在 LeNet 模型中,模型的最末端有三个全连接(FC)层。最后一层是用于分类任务的全连接层,其维度为(pevious_layer_output_dimension, number_of_classes)。

其他经典架构如 AlexNet、VGG、ResNet 和 Inception 在架构的末端都包括一个全连接层。然而,最近的架构如 MobileNet、YOLO、EfficientNet 和 Vision Transformers 删除了这个层。

从 CNN 架构中删除全连接层的原因有:

  • 减少参数数量(防止过拟合,特别是在较小的数据集上)。

  • FC 丢弃了特征图中的空间信息。

  • 灵活性。没有全连接层的 CNN 架构可以处理不同尺寸的输入,避免将输入图像调整为固定大小。

通用的卷积神经网络(CNN)架构

最常见的 CNN 架构形式是 {卷积层+非线性激活函数} 层的堆叠,后跟 {池化层} 逐次应用,直到图像在空间上被缩小到更小的尺寸并增加更多通道。

最后,最终的全连接层通常会输出类别分数。

总结来说,CNN 的结构如下:

Input -> [[[Conv-> ReLU] * N ] -> Pool?] * M -> [[FC+ReLU] * K] -> FC -> Scores

通常是(source):

  • 0 ≤ N ≤ 3 个堆叠的卷积层。

  • M≥0 池化块。

  • 0≤ K< 3 个堆叠的全连接层。

模型约束(source

通常建议机器学习从业者使用现有的最先进架构,而不是自行创建。然而,在使用卷积网络时,需要注意空间约束。例如,在应用卷积层时,设 W 为输入层大小(宽度或高度),F 为滤波器大小,P 为填充大小,S 为步幅大小,则输出特征图的大小为 Output size = ((W -F + 2P) / S ) +1\。对于每个卷积,参数 W、F、P 和 S 需要选择使输出大小为整数。通常添加填充可以解决大多数问题。

其他常见约束包括:

  • 输入图像应能够被 2 多次整除,具体取决于模型的深度。这个要求来自池化层。

  • 卷积层应使用小滤波器和小步幅。

  • 推荐使用“相同大小”的填充。如果我们在应用卷积前后保持相同的特征大小,我们将所有缩小操作委托给池化层,这使得网络结构更容易理解。

LeNet 模型实现

够多理论了,现在我们将使用 PyTorch Lightning 实现 LeNet CNN。由于其简单性和小巧的规模,LeNet 被选择作为示例。

模型实现

在 PyTorch 中,一个新的模块继承自[pytorch.nn.Module](https://pytorch.org/docs/stable/generated/torch.nn.Module.html#module)。在 PyTorch Lightning 中,模型类继承自ligthning.pytorch.LightningModule

你可以像使用nn.Module类一样使用ligthning.pytorch.LightningModule,它仅包含更多功能。

模块的两个参数是:

  • 输入通道的数量(对于灰度图像为 1)。

  • 分类器中的类别数(对于 MNIST 数据集为 10)。

在 PyTorch 中,模型由__init__()forward()两个部分描述。__init__()声明每个具有可学习参数的组件作为初始化方法。它还可以包含更多声明,如激活函数。然后,forward()方法对输入图像依次应用所有层和函数。

LeNet 架构由两个堆叠的卷积块组成,每个卷积块后面跟着一个池化层。然后将结果传递给连续的全连接层,输出一个大小为(batch_size, out_channels)的张量,其中out_channels表示类别数。

在下面的实现块中,首先初始化一些杂项属性:

  1. example_input_array张量用于显示张量大小的仿真,以便在运行print(model)时查看每一层之间的张量大小。

运行print(model)时会自动记录模型日志。图片来自作者。

从上表可以确认,输出张量的大小为(batch_size=16, num_classes=10)。

2. Accuracy()度量将在训练和验证过程中使用。

3. 具有可学习参数的层也会被初始化。首先是两个{卷积 + 最大池化}块,然后是全连接层。

 # models/detection/lenet.py
"""
PyTorch reference: https://pytorch.org/tutorials/beginner/blitz/neural_networks_tutorial.html
"""
from __future__ import annotations

import lightning.pytorch as pl
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchmetrics

class LeNet(pl.LightningModule):
    def __init__(self, in_channels: int, out_channels: int, lr: float = 2e-4):
        """
        Args:
        - in_channels: One for grayscale input image (which is the case for MNIST), 3 for RGB input image.
        - out_channels: Number of classes of the classifier. 10 for MNIST.
        """
        super().__init__()
        # Debugging tool to display intermediate input/output size of all your layer (called before fit)
        self.example_input_array = torch.Tensor(16, in_channels, 32, 32)
        self.learning_rate = lr

        self.train_accuracy = torchmetrics.Accuracy(task="multiclass", num_classes=out_channels)
        self.val_accuracy = torchmetrics.Accuracy(task="multiclass", num_classes=out_channels)
        self.test_accuracy = torchmetrics.Accuracy(task="multiclass", num_classes=out_channels)

        # [img_size] 32 -> conv -> 32 -> (max_pool) -> 16
        # with 6 output activation maps
        self.conv_layer1 = nn.Sequential(
            nn.Conv2d(
                in_channels=in_channels,
                out_channels=6,
                kernel_size=5,
                stride=1,
                # Either resize (28x28) MNIST images to (32x32) or pad the imput to be 32x32
                # padding=2,
            ),
            nn.MaxPool2d(kernel_size=2),
        )
        # [img_size] 16 -> (conv) -> 10 -> (max pool) 5
        self.conv_layer2 = nn.Sequential(
            nn.Conv2d(in_channels=6, out_channels=16, kernel_size=5, stride=1, padding=0),
            nn.MaxPool2d(kernel_size=2, stride=2),
        )
        # The activation size (number of values after passing through one layer) is getting gradually smaller and smaller.
        # The output is flatten and then used as a long input into the next dense layers.
        self.fc1 = nn.Linear(in_features=16 * 5 * 5, out_features=120)  # 5 from the image dimension
        self.fc2 = nn.Linear(in_features=120, out_features=84)
        # "Softmax" layer = Linear + Softmax.
        self.fc3 = nn.Linear(in_features=84, out_features=out_channels)

关于上述实现的说明

关于卷积层的说明

  • 为了简化前向调用,通常将堆叠的层表示在nn.Sequential()子模块中。

  • 第一个卷积层处理(32x32)大小的图像,并在池化层中将大小除以 2 后输出(16x16)大小的图像。

  • LeNet 期望输入图像大小为(32x32),但可用的 MNIST 数据集图像大小为(28x28)。你可以选择调整图像大小或增加第一个卷积层的填充大小(如注释中所述)。否则,卷积层可以处理可变大小的图像,但经过两次下采样后,最后一个卷积层的输出激活与第一个全连接层的矩阵乘法(其维度如下面所示)之间会出现维度不匹配。

 self.fc1 = nn.Linear(in_features=16 * 5 * 5, out_features=120)  # 5 from the image dimension
  self.fc2 = nn.Linear(in_features=120, out_features=84)
  # "Softmax" layer = Linear + Softmax.
  self.fc3 = nn.Linear(in_features=84, out_features=out_channels) 

第二个卷积层输入的通道数与第一个卷积层输出的滤波器数相同(6)。

  • ReLU 和 MaxPool 的顺序在这里并不重要。

ReLU 激活函数不会在池化之前调用,与之前提到的不同。在此实现中,ReLU 激活函数仅在forward()调用中被调用。

卷积层后面应该总是跟一个激活函数,以添加非线性。但是如果卷积层后面还跟一个池化层,那么顺序并不重要。这两个操作是可交换的 MaxPool(Relu(x)) = Relu(MaxPool(x))。实际上,我们可以在局部区域内取最大值并将所有负值设置为 0,或将所有负值设置为 0 并取每个局部区域的最大值。

  • 关于全连接层的说明。

第一个全连接层输入的张量大小为(number_output_filter_from_conv2 * previous_activation_width * previous_activation_height)。输出激活的大小在三个全连接层中逐渐减小。

在前向传递过程中会调用所有这些层:

# Method of LetNet class in models/detection/lenet.py
def forward(self, x: torch.Tensor) -> torch.Tensor:
    x = F.relu(self.conv_layer1(x))
    x = F.relu(self.conv_layer2(x))
    x = torch.flatten(x, 1)  # flatten all dimensions except the batch dimension
    x = F.relu(self.fc1(x))
    x = F.relu(self.fc2(x))
    x = self.fc3(x)
    return x

计算梯度的反向函数在使用autograd时会自动定义。

在大多数 PyTorch 实现中,最后一层(有时也称为 softmax 层)输出的是原始激活值,其中每个数字对应一个得分。在这里,softmax 函数在前向传递中并没有被调用,而是内置于交叉熵损失函数中。

实现训练、验证和测试步骤

在之前的相同文件中,在class LeNet(pl.LightningModule)下,我们重写了所有核心函数。

  • 优化器和调度器:configure_optimizers()
def configure_optimizers(self) -> torch.optim.Adam:
      return torch.optim.Adam(self.parameters(), lr=self.learning_rate)
  • 训练循环:training_step()

  • 验证循环:validation_step()

 # Methods in LeNet class in models/detection.lenet.py
  ###############################
  # --- For Pytorch Lightning ---
  ###############################

  def validation_step(
      self,
      batch: list[torch.Tensor, torch.Tensor],
      batch_idx: int,
      verbose: bool = True,
  ) -> torch.Tensor:
      """Function called when using `trainer.validate()` with trainer a
      lightning `Trainer` instance."""
      x, y = batch
      logit_preds = self(x)
      loss = F.cross_entropy(logit_preds, y)
      self.val_accuracy.update(torch.argmax(logit_preds, dim=1), y)
      self.log("val_loss", loss)
      self.log("val_acc_step", self.val_accuracy, on_epoch=True)
      return loss

  def training_step(
      self,
      batch: list[torch.Tensor, torch.Tensor],
      batch_idx: int,
  ) -> torch.Tensor:
      """Function called when using `trainer.fit()` with trainer a
      lightning `Trainer` instance."""
      x, y = batch
      logit_preds = self(x)
      loss = F.cross_entropy(logit_preds, y)
      self.train_accuracy.update(torch.argmax(logit_preds, dim=1), y)
      self.log("train_acc_step", self.train_accuracy, on_step=True, on_epoch=True, logger=True)
      # logs metrics for each training_step,
      # and the average across the epoch, to the progress bar and logger
      self.log("train_loss", loss, on_step=True, on_epoch=True, logger=True)
      return loss 

如你所见,上述函数都比较简短。无需将变量移动到 to(device),也无需使用 optimizer.zero_grad() 删除梯度或使用 loss.backward() 计算新的梯度。模型模式的切换也由 PyTorch Lightning 库本身处理 model.eval()model.train()

你可以注意到这里调用了 log() 方法。此方法在适当的时候保存和显示结果。

如果你想自定义它,文档很好地解释了如何正确使用日志记录:

log() 方法有一些选项:

  • on_step(在训练中的该步骤记录指标)

  • on_epoch(在每个 epoch 结束时自动累积并记录)

  • prog_bar(记录到进度条中)

  • logger(将日志记录到类似 Tensorboard 的日志记录器中)

根据 log 被调用的位置,Lightning 会自动确定正确的模式。不过,你当然可以通过手动设置标志来覆盖默认行为。

PyTorch Lightning 的另一个好功能是验证一致性检查:

你可能注意到了 验证一致性检查 被记录了。这是因为 Lightning 在开始训练之前会运行 2 批验证。这是一种单元测试,确保如果你在验证循环中有 bug,你不会需要等待整个 epoch 才能发现。

最后,测试和预测的方法也在同一个类中实现:

  • 测试循环:test_step()

  • 预测循环:predict_step()

模型可以从检查点加载权重,或者在训练循环结束后自动从最后一个或最佳(如果实现了回调)epoch 中提取权重。

def test_step(
      self,
      batch: list[torch.Tensor, torch.Tensor],
      batch_idx: int,
  ):
      """Function called when using `trainer.test()` with trainer a
      lightning `Trainer` instance."""
      x, y = batch
      logit_preds = self(x)
      loss = F.cross_entropy(logit_preds, y)
      self.test_accuracy.update(torch.argmax(logit_preds, dim=1), y)
      self.log_dict({"test_loss": loss, "test_acc": self.test_accuracy})

  def predict_step(
      self, batch: list[torch.Tensor, torch.Tensor], batch_idx: int
  ) -> tuple[torch.Tensor, torch.Tensor]:
      """Function called when using `trainer.predict()` with trainer a
      lightning `Trainer` instance."""
      x, _ = batch
      logit_preds = self(x)
      softmax_preds = F.softmax(logit_preds, dim=1)
      return x, softmax_preds

管理 MNIST 数据集

你可以使用常规的 PyTorch DataLoader 类或 PyTorch Lightning DataModule。在这篇文章中,我使用了 PyTorch Lightning DataModule 实现了数据集和数据加载。它旨在将与一个数据集相关的所有信息集中在一个文件中,包括数据下载、数据分割、数据加载等。

对于本教程,我们使用的是大小为 28x28 的 MNIST 图像。MNIST 数据集根据 Creative Commons Attribution-Share Alike 3.0 许可来源)提供。

MNIST 数据集的可视化。图片使用 CC 许可

这是管理 MNIST 数据集的数据模块的实现,包括设置标准参数:

  • 数据目录路径

  • 批量大小

  • Tensor 转换

以及 prepare_data()setup() 中的数据下载和处理功能。

# datasets/mnist.py
"""
More at https://lightning.ai/docs/pytorch/stable/data/datamodule.html
"""
import logging
from pathlib import Path

import lightning.pytorch as pl
from torch.utils.data import DataLoader, random_split
from torchvision import transforms
from torchvision.datasets import MNIST

# Create a logger
logger = logging.getLogger(Path(__file__).stem)
logger.setLevel(logging.INFO)

_DEFAULT_MNIST_BATCH_SIZE = 32
_DEFAULT_RESIZE_SIZE = 32

class MNISTDataModule(pl.LightningDataModule):
    def __init__(self, data_dir: str, batch_size: int = _DEFAULT_MNIST_BATCH_SIZE):
        super().__init__()
        self.data_dir = data_dir
        self.batch_size = batch_size
        self.transform = transforms.Compose(
            [
                transforms.ToTensor(),
                transforms.Resize((_DEFAULT_RESIZE_SIZE, _DEFAULT_RESIZE_SIZE)),
                transforms.Normalize((0.1307,), (0.3081,)),
            ]
        )

    def prepare_data(self):
        """Ensure we download using one process only on CPU and avoid data corruption when downloading the data.
        It's recommended to avoid creating class attributes `self.*` because the state won't be available for
        other processes.
        """
        MNIST(self.data_dir, train=True, download=True, transform=self.transform)
        MNIST(self.data_dir, train=False, download=True, transform=self.transform)

    def setup(self, stage: str):
        """Is called from every process across all nodes.
        It also uses every GPUs to perform data processing and state assignement.
        `teardown` is its counterpart used to clean the states.
        """
        logger.info(f"Stage: {stage}")
        if stage == "test" or stage == "predict":
            self.mnist_test = MNIST(self.data_dir, train=False, download=True, transform=self.transform)
        elif stage == "fit" or stage == "validate":
            mnist_full = MNIST(self.data_dir, train=True, transform=self.transform)
            self.mnist_train, self.mnist_val = random_split(mnist_full, [55000, 5000]) 

在开始训练时,以下函数按此顺序调用:

  • DataModule 的 prepare_data()setup() 方法。prepare_data() 方法在一个 CPU 上运行,用于在本地下载数据。而 setup() 方法是一个并行进程,可以运行数据处理作业。这些方法在每次调用训练器中的方法时都会被调用,如 trainer.fit()trainer.validate() 等。

  • pl.LightningModule 的 configure_optimizers() 初始化优化器。

然后,在相同的类 MNISTDataModule 下,我们实现了不同的数据加载器:

def train_dataloader(self) -> DataLoader:
        """Called by Trainer `.fit` method"""
        return DataLoader(self.mnist_train, batch_size=self.batch_size)

    def val_dataloader(self) -> DataLoader:
        """Called by Trainer `validate()` and `validate()` method."""
        return DataLoader(self.mnist_val, batch_size=self.batch_size)

    def test_dataloader(self) -> DataLoader:
        """Called by Trainer `test()` method."""
        return DataLoader(self.mnist_test, batch_size=self.batch_size)

    def predict_dataloader(self) -> DataLoader:
        """Called by Trainer `predict()` method. Use the same data as the test_dataloader."""
        return DataLoader(self.mnist_test, batch_size=self.batch_size, num_workers=3)
  • DataModule 的 train_dataloader() 检索训练 DataLoader。

  • pl.LightningModule 的 training_step() 对从训练 DataLoader 获得的小批量数据执行前向和后向传递。该方法会重复调用,直到所有来自训练 DataLoader 的样本都被看到一次。

  • pl.LightningModule 的 validation_step() 计算验证数据集上的损失和指标。

  • 一旦达到最大训练周期数,或者验证损失不再下降(早期停止),训练就会停止。

实现训练循环

最后,唯一缺少的部分是训练脚本本身。

训练脚本包括:

  • 解析 CLI 参数并调用主函数
if __name__ == "__main__":
    parser = ArgumentParser(description=__doc__)
    parser.add_argument("--model", default="lenet", type=str, help="Provide an implemented model.")
    parser.add_argument("--device", default=0, type=int, help="Select a CUDA device.")
    parser.add_argument("--max-epoch", default=10, type=int, help="Max number of epochs.")
    parser.add_argument("--out-dir", type=Path, help="Path to output directory")
    parser.add_argument(
        "--early-stopping", action="store_true", help="If True, stops the training if validation loss stops decreasing."
    )

    args = parser.parse_args()

    main(
        model_choice=args.model,
        device=args.device,
        max_epoch=args.max_epoch,
        out_dir=args.out_dir,
        early_stopping=args.early_stopping,
    )
  • 主函数包括模型选择、创建早期停止回调、以及对训练器的调用以进行训练 trainer.fit(model, datamodule=data_module)、验证 trainer.validate(datamodule=data_module)、测试 trainer.test(datamodule=data_module) 和预测 output_preds = trainer.predict(datamodule=data_module, ckpt_path=”best”)
def main(
    model_choice: str,
    device: int,
    max_epoch: int,
    out_dir: Path | None,
    early_stopping: bool | None,
):
    accelerator = "gpu" if torch.cuda.is_available() else "cpu"
    if out_dir is None:
        out_dir = Path(__file__).parent / "output"
    out_dir.mkdir(parents=True, exist_ok=True)
    # Select architecture
    if model_choice == "lenet":
        model = LeNet(in_channels=1, out_channels=10)
        data_module = MNISTDataModule(data_dir=_PATH_DATASETS, batch_size=_BATCH_SIZE)
    else:
      raise NotImplementedError(f"{model_choice} is not implemented!")
    callbacks = (
        [
            EarlyStopping(
                monitor="val_loss",
                min_delta=0.00,
                patience=_EARLY_STOPPING_PATIENCE,
                verbose=True,
                mode="min",
            )
        ]
        if early_stopping
        else []
    )

    # If your machine has GPUs, it will use the GPU Accelerator for training.
    trainer = L.Trainer(
        accelerator=accelerator,
        devices=[device],
        strategy="auto",
        max_epochs=max_epoch,
        callbacks=callbacks,
        default_root_dir=out_dir,
    )

    # Train the model ⚡
    # data_module.setup(stage="fit")  # Is called by trainer.fit().
    # Call training_step + validation_step for all the epochs.
    trainer.fit(model, datamodule=data_module)
    # Validate
    trainer.validate(datamodule=data_module)

    # Automatically auto-loads the best weights from the previous run.
    # data_module.setup(stage="test")  # Is called by trainer.test().
    # The checkpoint path is logged on the terminal.
    trainer.test(datamodule=data_module)

    # Run the prediction on the test set and save a subset of the resulting prediction along with the
    # original image.

    output_preds = trainer.predict(datamodule=data_module, ckpt_path="best")
    img_tensors, softmax_preds = zip(*output_preds)
    out_dir_imgs = out_dir / "test_images"
    out_dir_imgs.mkdir(exist_ok=True, parents=True)
    save_results(
        img_tensors=img_tensors,
        output_tensors=softmax_preds,
        out_dir=out_dir_imgs,
    )
  • 保存预测图像的函数(主要用于调试)。
def save_results(
    img_tensors: list[torch.Tensor], output_tensors: list[torch.Tensor], out_dir: Path, max_number_of_imgs: int = 10
):
    """Save test results as images in the provided output directory.
    Args:
        img_tensors: List of the tensors containing the input images.
        output_tensors: List of softmax activation from the trained model.
        out_dir: Path to output directory.
        max_number_of_imgs: Maximum number of images to output from the provided images. The images will be selected randomly.
    """
    selected_img_indices = random.sample(range(len(img_tensors)), min(max_number_of_imgs, len(img_tensors)))
    for img_indice in selected_img_indices:
        # Take the first instance of the batch (index 0)
        img_filepath = out_dir / f"{img_indice}_predicted_{torch.argmax(output_tensors[img_indice], dim=1)[0]}.png"
        torchvision.utils.save_image(img_tensors[img_indice][0], fp=img_filepath)

加上导入和常量声明,脚本看起来如下:

# Train.py script
#!/usr/bin/python3

"""Example training script to fit a model on MNIST dataset."""
from __future__ import annotations  # Enable PEP 563 for Python 3.7

from argparse import ArgumentParser
from lightning.pytorch.callbacks.early_stopping import EarlyStopping
from pathlib import Path
import lightning as L

import os
import random
import torch
import torchvision

from datasets.mnist import MNISTDataModule
from models import AlexNet, LeNet

_PATH_DATASETS = os.environ.get("PATH_DATASETS", ".")
_BATCH_SIZE = 64 if torch.cuda.is_available() else 32
_EARLY_STOPPING_PATIENCE = 4  # epochs

def save_results(
    img_tensors: list[torch.Tensor], output_tensors: list[torch.Tensor], out_dir: Path, max_number_of_imgs: int = 10
):
    """Save test results as images in the provided output directory.
    Args:
        img_tensors: List of the tensors containing the input images.
        output_tensors: List of softmax activation from the trained model.
        out_dir: Path to output directory.
        max_number_of_imgs: Maximum number of images to output from the provided images. The images will be selected randomly.
    """
    selected_img_indices = random.sample(range(len(img_tensors)), min(max_number_of_imgs, len(img_tensors)))
    for img_indice in selected_img_indices:
        # Take the first instance of the batch (index 0)
        img_filepath = out_dir / f"{img_indice}_predicted_{torch.argmax(output_tensors[img_indice], dim=1)[0]}.png"
        torchvision.utils.save_image(img_tensors[img_indice][0], fp=img_filepath)

def main(
    model_choice: str,
    device: int,
    max_epoch: int,
    out_dir: Path | None,
    early_stopping: bool | None,
):
    accelerator = "gpu" if torch.cuda.is_available() else "cpu"
    if out_dir is None:
        out_dir = Path(__file__).parent / "output"
    out_dir.mkdir(parents=True, exist_ok=True)
    # Select architecture
    if model_choice == "lenet":
        model = LeNet(in_channels=1, out_channels=10)
        data_module = MNISTDataModule(data_dir=_PATH_DATASETS, batch_size=_BATCH_SIZE)
    else:
      raise NotImplementedError(f"{model_choice} is not implemented!")
    callbacks = (
        [
            EarlyStopping(
                monitor="val_loss",
                min_delta=0.00,
                patience=_EARLY_STOPPING_PATIENCE,
                verbose=True,
                mode="min",
            )
        ]
        if early_stopping
        else []
    )

    # If your machine has GPUs, it will use the GPU Accelerator for training.
    trainer = L.Trainer(
        accelerator=accelerator,
        devices=[device],
        strategy="auto",
        max_epochs=max_epoch,
        callbacks=callbacks,
        default_root_dir=out_dir,
    )

    # Train the model ⚡
    # data_module.setup(stage="fit")  # Is called by trainer.fit().
    # Call training_step + validation_step for all the epochs.
    trainer.fit(model, datamodule=data_module)
    # Validate
    trainer.validate(datamodule=data_module)

    # Automatically auto-loads the best weights from the previous run.
    # data_module.setup(stage="test")  # Is called by trainer.test().
    # The checkpoint path is logged on the terminal.
    trainer.test(datamodule=data_module)

    # Run the prediction on the test set and save a subset of the resulting prediction along with the
    # original image.

    output_preds = trainer.predict(datamodule=data_module, ckpt_path="best")
    img_tensors, softmax_preds = zip(*output_preds)
    out_dir_imgs = out_dir / "test_images"
    out_dir_imgs.mkdir(exist_ok=True, parents=True)
    save_results(
        img_tensors=img_tensors,
        output_tensors=softmax_preds,
        out_dir=out_dir_imgs,
    )

if __name__ == "__main__":
    parser = ArgumentParser(description=__doc__)
    parser.add_argument("--model", default="lenet", type=str, help="Provide an implemented model.")
    parser.add_argument("--device", default=0, type=int, help="Select a CUDA device.")
    parser.add_argument("--max-epoch", default=10, type=int, help="Max number of epochs.")
    parser.add_argument("--out-dir", type=Path, help="Path to output directory")
    parser.add_argument(
        "--early-stopping", action="store_true", help="If True, stops the training if validation loss stops decreasing."
    )

    args = parser.parse_args()

    main(
        model_choice=args.model,
        device=args.device,
        max_epoch=args.max_epoch,
        out_dir=args.out_dir,
        early_stopping=args.early_stopping,
    )

结果

在我的机器上使用 GPU NVIDIA GeForce RTX 3070 运行 python -m train --early-stopping 进行 10 个周期的训练(批量大小为 64)不到两分钟。

当训练达到默认的 max_epochs(10)时,PyTorch Lightning 会分别输出验证和测试数据集上的损失和准确率结果:

经过 10 个训练周期后,模型在未见数据上的结果。

训练后的模型在未见数据上的准确率几乎达到 99%。

脚本保存了 10 张来自测试集的图像以及预测的类别:

预测为 6

结论

在本文中,我们发现了 PyTorch Lightning 的魔力,然后复习了 CNN 的关键技术概念,并从头到尾演练了一个简单 CNN 架构的训练循环的完整实现。

我希望这篇入门级文章对你在快速而可靠地实现基本架构的过程中有所帮助,并且帮助你在学习旅程中建立了更坚实的基础。你可以查看我的公开深度学习仓库,获取更多内容 github.com/bledem/deep-learning

参考资料

使用 Mage 在数据管道中实现行为驱动开发

原文:towardsdatascience.com/implement-behaviour-driven-development-in-data-pipelines-using-mage-19496fea7890

最大化数据管道的质量和生产力

肖旭高Towards Data Science 肖旭高

·发布在 Towards Data Science ·阅读时间 7 分钟·2023 年 7 月 6 日

--

照片由 Nick Fewings 提供,来源于 Unsplash

在我的之前的文章中,我详细讲述了数据管道中测试的重要性,以及如何分别创建数据测试和单元测试。虽然测试在开发周期中扮演着至关重要的角色,但它可能不是最令人兴奋的部分。因此,许多现代数据技术栈引入了框架或插件,以加快数据测试的实现。此外,像 Pytest 和 unittest 这样的 Python 单元测试框架已经存在很长时间,帮助工程师高效地为数据管道和任何 Python 应用程序创建单元测试。

在这篇文章中,我想介绍一个使用两种现代技术的设置:行为驱动开发(BDD)——一个面向业务的测试框架,以及 Mage ——一个现代数据管道工具。通过将这两种技术结合起来,目标是为数据管道创建高质量的单元测试,同时提供无缝的开发者体验。

什么是行为驱动开发(BDD)?

在为业务构建数据管道时,我们很可能会遇到复杂且棘手的业务逻辑。例如,根据年龄、收入和过去的购买记录来定义客户细分。以下示例仅代表业务逻辑可能涉及的一部分复杂性。随着属性和每个属性内的细节增多,它可能变得越来越复杂。想想你在日常工作中的一个例子!

1\. People between 19 and 60
   AND with high past purchases are "premium".

2\. People between 19 and 60
   AND with high income are "premium".

3\. People above 60
   AND with high income
   AND with high past purchases are "premium".

4\. Others are "basic".

所以问题是商业规则应该如何记录,以及如何确保文档和代码之间的同步。一种常见的方法是在代码旁边包含注释,或者努力编写自解释且易于理解的代码。但仍然存在注释过时或利益相关者难以理解的代码的风险。

最终,我们寻求的是一种可以同时惠及工程师和业务利益相关者的“文档即代码”解决方案,这正是 BDD 能提供的。如果你对“数据契约”的概念熟悉,BDD 可以被视为一种数据契约,但重点在于利益相关者而非数据源。这对于业务逻辑复杂的数据管道特别有利,帮助防止关于“功能或错误”的争论。

BDD 本质上是一种软件开发方法,强调利益相关者和开发者之间的协作与沟通,以确保软件达到期望的业务结果。行为通过场景进行描述,这些场景说明了预期的输入和结果。每个场景都采用特定的“Given-When-Then”格式,其中每个步骤描述了一个特定的条件或操作。

让我们看看客户分段示例中的场景可能是什么样的。由于功能文件是用英语编写的,它可以被业务利益相关者很好地理解,他们甚至可以对其进行贡献。这就像是利益相关者和工程师之间的契约,工程师负责准确实现需求,而利益相关者则需提供所有必要的信息。

拥有利益相关者和工程师之间的明确契约有助于正确分类数据问题,区分由于实施错误导致的“软件缺陷”和由于缺失需求导致的“功能请求”。

功能文件(作者创建)

下一步是从功能文件生成测试代码,这就是连接发生的地方。Pytest 代码充当了文档和实现代码之间的桥梁。当它们之间存在任何不一致时,测试会失败,突显了文档和实现之间需要同步。

测试代码充当桥梁(作者创建)

下面是测试代码的样子。为了简化示例,我只实现了第一个场景的测试代码。Given 步骤设置了场景的初始上下文,在这种情况下,它从示例中获取客户年龄、收入和过去购买数据。When 步骤触发被测试的行为,即 get_user_segment 函数。在 Then 步骤中,我们将 When 步骤的结果与场景示例中的预期输出进行比较。

功能文件中的第一个场景的测试代码(作者创建)

想象一下,如果在第一个场景中添加了一个年龄范围 62 而没有更新代码,那么测试会立即失败,因为代码有冲突的期望。

Mage 是什么?

到目前为止,我们已经看到 BDD 的潜力,并学习了如何使用 Python 实现它。现在,是时候将 BDD 融入我们的数据管道中了。在数据编排方面,Airflow 作为第一个基于 Python 的具有 Web 界面的编排工具,已经成为执行数据管道最常用的工具。

但它肯定不是完美的。例如,测试生产环境外的管道,尤其是在使用像 KubernetesOperator 这样的操作符时,可能会面临挑战。此外,DAG 可能会被冗余的代码和复杂的配置搞得一团糟,使得很难明确每个任务的目的,无论是数据摄取、转换还是导出。此外,Airflow 并不是专注于数据驱动的编排工具,因为它更关注任务的成功执行,而不是最终数据资产的质量。

随着数据工程社区的成长,许多 Airflow 的替代品已经出现,以填补 Airflow 中存在的空白。Mage 是一个不断增长的数据管道工具,被视为 Airflow 的现代替代品。其四大设计概念使 Mage 区别于 Airflow,我们可以从开发周期开始就感受到这种区别。

Mage 的设计原则(由作者创建)

Mage 具有非常直观的 UI,使工程师能够迅速高效地编辑和测试管道。

每个管道由几种类型的模块组成:@data_loader、@transformer、@data_exporter 等,每个模块都有明确的目的。这是我最喜欢的功能之一,因为我可以立即理解每个任务的目标,专注于业务逻辑,而不是被冗余代码困扰。

Mage UI(由作者创建)

BDD + Mage

一个普通的数据管道有三个主要步骤:摄取、转换和导出。转换是实现所有复杂业务逻辑的地方,多个转换步骤被整合在一起并不罕见。

清晰地将摄取任务和转换任务分开,使得将 BDD 应用于你的转换逻辑变得非常简单和直观。实际上,这就像是在测试一个普通的 Python 函数,而忽略它是数据管道的一部分。

回到用户分段的例子。业务规则应该放在@transformer 模块中,并且与加载器和导出器解耦。

@transformer 模块在数据管道中(由作者创建)

相同的 @transformer 块可以插入多个管道,只要加载器返回一个 pandas 数据帧。要运行测试,我们只需在终端或 CI/CD 管道中运行pytest命令。管道配置(例如触发器)在一个单独的文件中,这使得主管道文件保持尽可能干净。

让我们想象一下如果在 Airflow 中实现这一点会发生什么。由于这是一个不复杂的示例,Airflow 肯定可以很好地处理。但有一些细节让我在从 Mage 切换到 Airflow 时感到“额外”的困扰。

  1. DAG 文件变得混乱,因为每个 DAG 都有一个大的代码块来定义其元数据。在 Mage 中,配置被移动到一个 yaml 文件中,因此管道文件保持简洁。
@dag(
    dag_id="user_segment",
    schedule_interval="0 0 * * *",
    start_date=pendulum.datetime(2023, 1, 1, tz="UTC"),
    catchup=False,
    dagrun_timeout=datetime.timedelta(minutes=60),
)

2. 数据传递在 Airflow 中比较棘手。Airflow 中的 XCOM 用于在任务之间传递数据。然而,不推荐通过 XCOM 直接传递大数据集,例如数据帧。作为一种解决方法,我们需要先将数据持久化到临时存储中,这似乎是一种不必要的工程努力。Mage 自然地处理数据传递,我们不需要担心数据集的大小。

3. 从技术上讲,Airflow 支持多个版本的 Python 包,但代价很高。KubernetesPodOperator 和 PythonVirtualenvOperator 允许你在隔离的环境中运行任务。但你将失去 Airflow 提供的即开即用的便利,例如使用其他操作符。相比之下,Mage 通过使用一个集中式的requirements.txt来解决这个问题,确保所有任务都可以访问 Mage 的所有原生功能。

结论

在这篇文章中,我结合了两种技术,目的是提高测试质量和开发者体验。BDD 旨在通过创建直接嵌入代码库中的特性文件格式的合同,来增强利益相关者和工程师之间的协作。另一方面,Mage 是一个很棒的数据管道工具,将开发者体验作为首要任务,并真正将数据视为一等公民。

我希望你能从中获得启发,并感到有动力在日常工作中探索并融入至少一种技术。选择合适的工具肯定能提升你团队的生产力。我很想知道你的想法。请在评论中告诉我。干杯!

在 PyTorch 中实现可解释的神经模型!

原文:towardsdatascience.com/implement-interpretable-neural-models-in-pytorch-6a5932bdb078?source=collection_archive---------4-----------------------#2023-05-29

Pietro BarbieroTowards Data Science Pietro Barbiero

·

关注 发表在 Towards Data Science · 11 分钟阅读 · 2023 年 5 月 29 日

--

总结 — 体验可解释性的强大力量,通过 “PyTorch, Explain!” —— 一个能够帮助你实现最先进且可解释的基于概念的模型的 Python 库! [GitHub]

可解释的 AI 模型会给出人类能够理解的预测理由。图片由作者提供。

灵感来源于以下方法的教程:

动机

深度学习系统缺乏可解释性对建立人类信任构成了重大挑战。这些模型的复杂性使得人类几乎无法理解其决策背后的原因。

深度学习系统缺乏可解释性阻碍了人类信任。

为了解决这个问题,研究人员一直在积极探索新颖的解决方案,导致了诸如基于概念的模型这样的重大创新。这些模型不仅提高了模型的透明性,还通过在训练过程中融入高层次的人类可解释概念(如“颜色”或“形状”)来培养对系统决策的新信任感。因此,这些模型可以提供简单直观的预测解释,基于学习到的概念,允许人类检查决策背后的推理。而且不仅如此!它们甚至允许人类与学习到的概念互动,赋予我们对最终决策的控制

基于概念的模型允许人们检查深度学习预测背后的推理,并重新获得对最终决策的控制

在这篇博客文章中,我们将深入探讨这些技术,并为你提供使用简单 PyTorch 接口实现最先进的基于概念的模型的工具。通过实践经验,你将学会如何利用这些强大的模型来增强可解释性,并最终校准人类对深度学习系统的信任。

教程 #1:实现你的第一个概念瓶颈模型

为了展示 PyTorch Explain 的强大功能,让我们深入我们的第一个教程!

概念瓶颈模型简介

在这个介绍性会议中,我们将深入探讨概念瓶颈模型。这些模型在 2020 年国际机器学习会议上发表的论文 [1] 中首次提出,旨在首先学习和预测一组概念,例如“颜色”或“形状”,然后利用这些概念来解决下游分类任务:

概念瓶颈模型将任务(Y)学习为概念(C)的函数。图像由作者提供。

通过遵循这种方法,我们可以将预测追溯到概念,从而提供类似于“输入对象是一个{苹果},因为它是{球形}和{红色}”的解释。

概念瓶颈模型首先学习一组概念,例如“颜色”或“形状”,然后利用这些概念来解决下游分类任务。

实践概念瓶颈

为了说明概念瓶颈模型,我们将重新审视众所周知的异或(XOR)问题,但有了一点变化。我们的输入将由两个连续的特征组成。为了捕捉这些特征的本质,我们将使用一个概念编码器将它们映射到两个有意义的概念,标记为“A”和“B”。我们的任务目标是预测“A”和“B”的异或(XOR)。通过解决这个例子,你将更好地理解概念瓶颈如何在实践中应用,并见证它们在解决具体问题时的有效性。

我们可以先导入必要的库并加载这个简单的数据集:

import torch
import torch_explain as te
from torch_explain import datasets
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split

x, c, y = datasets.xor(500)
x_train, x_test, c_train, c_test, y_train, y_test = train_test_split(x, c, y, test_size=0.33, random_state=42)

接下来,我们实例化一个概念编码器,将输入特征映射到概念空间,并一个任务预测器,将概念映射到任务预测中:

concept_encoder = torch.nn.Sequential(
    torch.nn.Linear(x.shape[1], 10),
    torch.nn.LeakyReLU(),
    torch.nn.Linear(10, 8),
    torch.nn.LeakyReLU(),
    torch.nn.Linear(8, c.shape[1]),
    torch.nn.Sigmoid(),
)
task_predictor = torch.nn.Sequential(
    torch.nn.Linear(c.shape[1], 8),
    torch.nn.LeakyReLU(),
    torch.nn.Linear(8, 1),
)
model = torch.nn.Sequential(concept_encoder, task_predictor)

然后,我们通过优化交叉熵损失来训练网络,既包括概念又包括任务:

optimizer = torch.optim.AdamW(model.parameters(), lr=0.01)
loss_form_c = torch.nn.BCELoss()
loss_form_y = torch.nn.BCEWithLogitsLoss()
model.train()
for epoch in range(2001):
    optimizer.zero_grad()

    # generate concept and task predictions
    c_pred = concept_encoder(x_train)
    y_pred = task_predictor(c_pred)

    # update loss
    concept_loss = loss_form_c(c_pred, c_train)
    task_loss = loss_form_y(y_pred, y_train)
    loss = concept_loss + 0.2*task_loss

    loss.backward()
    optimizer.step()

在对模型进行训练后,我们在测试集上评估其性能:

c_pred = concept_encoder(x_test)
y_pred = task_predictor(c_pred)

concept_accuracy = accuracy_score(c_test, c_pred > 0.5)
task_accuracy = accuracy_score(y_test, y_pred > 0)

现在,经过仅仅几个 epochs 之后,我们可以观察到概念和任务的准确率在测试集上相当不错(~98% 准确率)!

多亏了这种架构,我们可以通过观察任务预测器对输入概念的响应来为模型的预测提供解释,方法如下:

c_different = torch.FloatTensor([0, 1])
print(f"f({c_different}) = {int(task_predictor(c_different).item() > 0)}")

c_equal = torch.FloatTensor([1, 1])
print(f"f({c_different}) = {int(task_predictor(c_different).item() > 0)}")

这导致例如,f([0,1])=1f([1,1])=0,如预期的那样。这使我们能够更多地理解模型的行为,并检查它是否对任何相关的概念集合(例如,对于互斥的输入概念[0,1][1,0])都返回y=1的预测。

概念瓶颈模型通过将预测追溯到概念来提供直观的解释。

在准确性-可解释性的权衡中陷入困境

概念瓶颈模型的一个关键优势之一是它们能够通过揭示概念-预测模式来为他们的预测提供解释,从而使人类能够评估模型的推理是否符合他们的期望。

但是,标准概念瓶颈模型的主要问题在于它们在解决复杂问题时很困难!更一般地说,它们在可解释人工智能中遇到了一个众所周知的问题,被称为准确性-可解释性的权衡。实际上,我们希望模型不仅在任务表现上取得高准确性,而且能够提供高质量的解释。然而,很不幸的是,在许多情况下,随着我们追求更高的准确性,模型提供的解释往往会质量下降,且反之亦然。

在视觉上,这种权衡可以表示为下图所示:

准确性-可解释性权衡的视觉表示。图片

展示了可解释和“黑盒”(不可解释)模型之间的差异

从两个角度来看:任务表现和解释质量。图片作者提供。

可解释模型擅长提供高质量解释,但在解决具有挑战性的任务时表现不佳,而黑盒模型则通过提供脆弱和贫乏的解释来实现高任务准确性。

为了在一个具体的情景中说明这种折衷,让我们考虑一个应用于略微更具挑战性基准数据集“三角函数”的概念瓶颈模型:

x, c, y = datasets.trigonometry(500)
x_train, x_test, c_train, c_test, y_train, y_test = train_test_split(x, c, y, test_size=0.33, random_state=42)

在这个数据集上训练相同的网络架构后,我们观察到明显降低的任务准确性,仅达到约 80%。

概念瓶颈模型未能在任务准确性和解释质量之间取得平衡。

这引出了一个问题:我们是否被永远迫使在准确性和解释质量之间进行选择,还是有更好的平衡方式?

教程 #2:超越准确性和可解释性的折衷之道:概念嵌入模型

答案是“是的!”,方案确实存在!

概念嵌入模型简介

最近提出的解决这一挑战的方法是在 神经信息处理系统的进展 会议上介绍的,一篇名为“概念嵌入模型:超越准确性和可解释性之间的折衷”的论文 [2](如果你想了解更多,我在这篇博客文章中更详细地讨论了这种方法!)。 这篇论文的关键创新是设计了受监督的高维概念表示。与用单个神经元激活表示每个概念的标准概念瓶颈模型不同:

概念瓶颈模型将任务(Y)作为概念(C)的函数进行学习。图片由作者提供。

… 一个概念嵌入模型用一组神经元表示每个概念,有效地克服了与概念层相关的信息瓶颈:

概念嵌入模型将每个概念表示为一个受监督的向量。图片由作者提供。

因此,概念嵌入模型使我们能够同时实现高准确性和高质量解释:

概念嵌入模型在概念瓶颈模型中超越了准确性和可解释性的折衷,几乎实现了最佳的任务准确性和概念对齐。 最佳折衷由红色星星(右上方)表示。 任务是学习两个向量之间点积的符号(+/-)。图片由作者提供。

概念嵌入模型成功地在任务准确性和解释质量之间取得平衡。

亲身体验概念嵌入模型

在 pytorch 中实现这些模型就像实现标准概念瓶颈模型那样简单!

我们从加载数据开始:

x, c, y = datasets.trigonometry(500)
x_train, x_test, c_train, c_test, y_train, y_test = train_test_split(x, c, y, test_size=0.33, random_state=42)

接下来,我们实例化一个概念编码器,将输入特征映射到概念空间,并实例化一个任务预测器,将概念映射到任务预测:

embedding_size = 8
concept_encoder = torch.nn.Sequential(
    torch.nn.Linear(x.shape[1], 10),
    torch.nn.LeakyReLU(),
    te.nn.ConceptEmbedding(10, c.shape[1], embedding_size),
)
task_predictor = torch.nn.Sequential(
    torch.nn.Linear(c.shape[1]*embedding_size, 8),
    torch.nn.LeakyReLU(),
    torch.nn.Linear(8, 1),
)
model = torch.nn.Sequential(concept_encoder, task_predictor)

然后,我们通过优化概念和任务上的交叉熵损失来训练网络:

optimizer = torch.optim.AdamW(model.parameters(), lr=0.01)
loss_form_c = torch.nn.BCELoss()
loss_form_y = torch.nn.BCEWithLogitsLoss()
model.train()
for epoch in range(2001):
    optimizer.zero_grad()

    # generate concept and task predictions
    c_emb, c_pred = concept_encoder(x_train)
    y_pred = task_predictor(c_emb.reshape(len(c_emb), -1))

    # compute loss
    concept_loss = loss_form_c(c_pred, c_train)
    task_loss = loss_form_y(y_pred, y_train)
    loss = concept_loss + 0.2*task_loss

    loss.backward()
    optimizer.step()

在训练模型后,我们在测试集上评估其性能:

c_emb, c_pred = concept_encoder.forward(x_test)
y_pred = task_predictor(c_emb.reshape(len(c_emb), -1))

concept_accuracy = accuracy_score(c_test, c_pred > 0.5)
task_accuracy = accuracy_score(y_test, y_pred > 0)

现在,仅经过几轮训练,我们可以观察到概念和任务的准确性在测试集上都相当好(约 96%的准确率),比标准概念瓶颈模型高出近 15%!

为什么可解释性>可解释性?

尽管到目前为止所讨论的技术提供了简单直观的解释,但仍存在一个固有的限制:模型预测背后的精确逻辑推理仍然不清楚。

实际上,即使我们使用像决策树或逻辑回归这样的透明机器学习模型,它也不一定能缓解使用概念嵌入时的问题。这是因为概念向量的个别维度对人类缺乏明确的语义解释。例如,决策树中的逻辑句子“如果 {yellow[2]>0.3} 和 {yellow[3]<-1.9} 和 {round[1]>4.2} 那么 {banana}” 的术语如“{yellow[2]>0.3}”(指概念向量“yellow”的第二维大于“0.3”)对我们来说并没有太大语义意义。

标准的可解释分类器无法使用概念嵌入提供可解释的预测,因为个别嵌入维度缺乏明确的语义意义。图片由作者提供。

即使是透明模型在应用于概念嵌入时也无法提供可解释的预测。

我们这次如何克服这个挑战?!

第 3 步:无妥协的可解释性

再次,解决方案确实存在!

深度概念推理简介

深度概念推理器 [3](一篇被 2023 年国际机器学习大会接受的最新论文)通过实现完全的可解释性来解决概念嵌入模型的局限性。该方法的关键创新在于设计了一个任务预测器,分别处理概念嵌入和概念真实性度。标准的机器学习模型则会同时处理概念嵌入和概念真实性度:

标准的可解释分类器无法使用概念嵌入提供可解释的预测,因为个别嵌入维度缺乏明确的语义意义。图片由作者提供。

深度概念推理器生成(可解释的!)逻辑规则,使用概念嵌入,然后以符号化方式执行规则,将相应的真实性值分配给概念符号:

深度概念推理器使用神经模型在概念嵌入上生成模糊逻辑规则,然后

使用概念真实性度执行规则,以符号化方式评估规则。图片由作者提供。

深度概念推理器在应用于概念嵌入时提供可解释的预测,因为每个预测都是基于逻辑规则的概念真值生成的。

这种独特的技术使我们能够实现完全可解释的模型,因为它们基于逻辑规则进行预测,如决策树一样! 使它们与众不同的是在挑战性任务中的卓越表现,超越了传统的可解释模型,如决策树或逻辑回归:

深度概念推理器超越了可解释的基于概念的模型,并且与黑箱模型的准确性相匹配。CE 代表概念嵌入,CT 代表概念真值。图片由作者提供。

通过利用深度概念推理,我们可以释放出具有高解释性的模型的潜力,这些模型在复杂任务上表现出色。

深度概念推理器提供可解释的预测,同时在任务准确性方面超越了可解释模型。

实践深度概念推理

使用pytorch_explain库实现深度概念推理同样非常简单!

如前面的示例所示,我们实例化一个概念编码器,将输入特征映射到概念空间,并实例化一个深度概念推理器,将概念映射到任务预测:

from torch_explain.nn.concepts import ConceptReasoningLayer
import torch.nn.functional as F

x, c, y = datasets.xor(500)
x_train, x_test, c_train, c_test, y_train, y_test = train_test_split(x, c, y, test_size=0.33, random_state=42)
y_train = F.one_hot(y_train.long().ravel()).float()
y_test = F.one_hot(y_test.long().ravel()).float()

embedding_size = 8
concept_encoder = torch.nn.Sequential(
    torch.nn.Linear(x.shape[1], 10),
    torch.nn.LeakyReLU(),
    te.nn.ConceptEmbedding(10, c.shape[1], embedding_size),
)
task_predictor = ConceptReasoningLayer(embedding_size, y_train.shape[1])
model = torch.nn.Sequential(concept_encoder, task_predictor)

然后,我们通过优化概念和任务的交叉熵损失来训练网络:

optimizer = torch.optim.AdamW(model.parameters(), lr=0.01)
loss_form = torch.nn.BCELoss()
model.train()
for epoch in range(2001):
    optimizer.zero_grad()

    # generate concept and task predictions
    c_emb, c_pred = concept_encoder(x_train)
    y_pred = task_predictor(c_emb, c_pred)

    # compute loss
    concept_loss = loss_form(c_pred, c_train)
    task_loss = loss_form(y_pred, y_train)
    loss = concept_loss + 0.2*task_loss

    loss.backward()
    optimizer.step()

训练模型后,我们可以在测试集上评估其性能,并检查其是否与概念嵌入模型的准确性相匹配(约 99%):

c_emb, c_pred = concept_encoder.forward(x_test)
y_pred = task_predictor(c_emb, c_pred)

concept_accuracy = accuracy_score(c_test, c_pred > 0.5)
task_accuracy = accuracy_score(y_test, y_pred > 0.5)

但是,这次我们可以通过阅读相应的逻辑规则,精确获取每个预测背后的推理:

local_explanations = task_predictor.explain(c_emb, c_pred, 'local')

每个local_explanations中的元素具有以下结构:

{'sample-id': 0,
 'class': 'y_1',
 'explanation': '~c_0 & c_1',
 'attention': [-1.0, 1.0]}

同样,我们可以使用命令提取全局解释:

global_explanations = task_predictor.explain(c_emb, c_pred, 'global')

该方法返回模型找到的完整规则集,使人类能够再次检查深度学习系统的推理是否与预期行为一致:

[{'class': 'y_0', 'explanation': 'c_0 & c_1', 'count': 39},
 {'class': 'y_0', 'explanation': '~c_0 & ~c_1', 'count': 46},
 {'class': 'y_1', 'explanation': '~c_0 & c_1', 'count': 45},
 {'class': 'y_1', 'explanation': 'c_0 & ~c_1', 'count': 35}]

主要收获

在这篇文章中,我们探索了pytorch_explain库的关键功能,突出了最先进的基于概念的架构,并用几行代码演示了它们的实现。

这里是我们涵盖内容的总结:

  • 概念瓶颈模型:这些模型通过追溯预测到一组人类可解释的概念,提供直观的解释;

  • 概念嵌入模型:通过克服与概念相关的信息瓶颈,这些模型在不影响解释质量的情况下实现了高预测准确性;

  • 深度概念推理器:这些模型的预测完全可解释,因为深度概念推理器使用逻辑规则来组合概念的真值进行预测。

通过利用“Pytorch, Explain!”库的强大功能并实施讨论中的技术,你有机会显著提升模型的可解释性,同时保持高预测准确性。这不仅使你能够深入了解模型预测背后的推理,还培养和校准用户对系统的信任。

参考文献

[1] Koh, Pang Wei, 等人. “概念瓶颈模型。” 国际机器学习大会。PMLR,2020 年。

[2] Zarlenga, Mateo Espinosa, 等人. “概念嵌入模型:超越准确性与可解释性权衡。” 神经信息处理系统进展。第 35 卷。Curran Associates, Inc.,2022 年。21400–21413。

[3] Barbiero, Pietro, 等人. “可解释的神经符号概念推理。” arXiv 预印本 arXiv:2304.14068(2023 年)。

在 3 分钟内在单个 GPU 系统上进行多 GPU 训练

原文:towardsdatascience.com/implement-multi-gpu-training-on-a-single-gpu-e9b6b775456a

TensorFlow 高级指南

Sascha KirchTowards Data Science Sascha Kirch

·发表于Towards Data Science ·阅读时间 3 分钟·2023 年 5 月 15 日

--

图片由Chris Liverani提供,Unsplash

我想和你分享一个有趣的小技巧,关于如何在单个 GPU 上测试我的多 GPU 训练代码。

动机

我想问题很明显,你可能自己也经历过。你想训练一个深度学习模型,并希望利用多个 GPU、TPU 或甚至多个工作节点来获得额外的速度或更大的批量大小。但当然,你不能(或者说不应该,因为我见过很多 😅)占用通常共享的硬件进行调试,甚至在付费云实例上花费大量金钱。

让我告诉你,重要的不是你的系统有多少个物理 GPU,而是你的软件认为它有多少个。关键词是:(设备)虚拟化

让我们实现它

首先,我们来看看你通常如何检测和连接到你的 GPU

代码 1:检测所有可用的 GPU,初始化相应的作用域,并在策略作用域内初始化你的模型、优化器和检查点。

你首先需要列出所有可用的设备,然后选择合适的策略,并在策略作用域内初始化你的模型、优化器和检查点。如果你使用标准训练循环中的model.fit(),你就完成了。如果你使用自定义训练循环,你需要实现一些额外的步骤。

查看我关于使用 Google 的 TPU 进行加速分布式训练的教程,了解更多关于自定义训练循环的分布式训练细节。

上述代码中有一个重要细节。你注意到我用了函数 list_logical_devices(“GPU”) 而不是 list_physical_devices(“GPU”) 吗?逻辑设备是所有对软件可见的设备,但这些设备不一定与实际的物理设备关联。如果我们现在运行代码块,这可能是你会看到的输出:

图 1:运行 Code 1 之后的输出截图,并连接到一个逻辑 GPU 和一个相关的物理 GPU。由作者拍摄。

我们将利用逻辑设备定义来定义一些逻辑设备,然后列出所有逻辑设备并连接到它们。具体来说,我们将定义 4 个逻辑 GPU 与一个物理 GPU 相关联。这是操作步骤:

Code 2:创建与单个物理 GPU 相关联的多个逻辑 GPU 设备。

如果我们再次打印逻辑设备与物理设备的数量,你会看到:

图 2:运行 Code 2 之后的输出截图。在 Code 1 之前,并连接到四个逻辑 GPU 和一个相关的物理 GPU。由作者拍摄。

就这样,你现在可以像在 4 个 GPU 上进行分布式训练一样,在单个 GPU 上测试你的代码。

有几点需要注意:

  1. 你实际上并没有进行分布式训练,因此没有通过并行化获得性能提升。

  2. 你需要在连接硬件之前分配逻辑设备,否则会引发异常。

  3. 它只测试算法的正确实现,你可以检查输出的形状和值是否符合预期。它不能保证多 GPU 配置中的所有驱动程序和硬件都是正确的。

如果这个技巧对你有用,或者你已经知道这个功能,请在评论中告诉我!对我来说,这真是一个游戏改变者。

祝你测试愉快!💪

轻松用 Python 从头实现多分类支持向量机

原文:towardsdatascience.com/implement-multiclass-svm-from-scratch-in-python-b141e43dc084?source=collection_archive---------2-----------------------#2023-11-04

附带支持向量机的深度概述

Essam WisamTowards Data Science Essam Wisam

·

关注 发布于 Towards Data Science ·14 min read·2023 年 11 月 4 日

--

在这个故事中,我们将实现支持向量机学习算法的通用软边距和核化形式。我们将首先简要概述支持向量机及其训练和推断方程,然后将这些方程转换为代码以开发支持向量机模型。之后,我们扩展我们的实现以处理多分类场景,并通过使用 Sci-kit Learn 测试我们的模型来总结。

因此,到本故事结束时:

  • 你将清晰地理解各种重要的支持向量机概念。

  • 你将能够以真正的理解从头实现支持向量机模型,包括二分类和多分类情况。

美丽的梵高画作,描绘了“星夜”中的“两颗星星与它们之间的线”——由作者使用 DALLE 2 生成

目录

· 简要概述

∘ 硬间隔 SVM

∘ 软间隔 SVM

∘ 核软间隔 SVM

· 实现

∘ 基本导入

∘ 定义核函数和 SVM 超参数

∘ 定义预测方法

∘ 定义预测方法

∘ 测试实现

∘ 对多分类的泛化适应

∘ 对多分类的泛化预测

∘ 测试实现

简要概述

硬间隔 SVM

SVM 的目标是拟合出能够获得最大间隔(距离两个类别中最近点的距离)的超平面。可以证明,并且直观上,这样的超平面(A)具有更好的泛化能力,并且比一个没有最大化间隔的超平面(B)更能抵抗噪声。

图示来源于 Ennepetaler86WikimediaCC BY-SA 3.0 Unported

为了实现这一点,SVM 通过解决以下优化问题来找到超平面的 W 和 b:

它尝试找到W, b,以最大化到最近点的距离,并正确分类所有数据(如在 y 取±1 的约束条件中)。这可以证明等价于以下优化问题:

对于这个问题,可以写出等价的 对偶 优化问题

这提供了一个拉格朗日乘子,针对数据集中每一个点,我们假设其大小为 m: (α₁, α₂, …, α_N)。目标函数在 α 上显然是二次的,约束条件是线性的,这意味着可以很容易地通过 二次规划 解决。一旦找到解决方案,从对偶问题的推导中可以得到:

(xₛ, yₛ) 是任何 α>0 的点

注意,只有 α>0 的点定义了超平面(对和有贡献)。这些点称为支持向量。

因此,当给定一个新的示例 x 时,预测方程返回其预测 y=±1 为:

涉及到插入并进行一些代数简化

这种基本形式的 SVM 称为硬边界 SVM,因为它解决的优化问题(如上所述)强制训练中的所有点必须被正确分类。在实际场景中,可能存在一些噪声,这些噪声阻止或限制了完全分开数据的超平面的存在,这种情况下优化问题可能没有返回解或返回了一个较差的解。

软边界 SVM

Mangat 等人Research Gate上适配的软边界 SVM。CC BY-SA 4.0 International

为了推广硬边界 SVM,软边界 SVM 通过引入一个 C 常数(用户指定的超参数)来调整优化问题,以控制其“难度”。具体来说,它将原始优化问题修改为以下形式:

蓝色部分的修改

这允许每个点有一些违例ϵₙ(例如,位于超平面错误的一侧),但仍然通过在目标函数中用 C 加权它们的总和来减少这些违例。随着 C 趋近于无穷大(通常在之前),它变得等同于硬边界。与此同时,更小的 C 会允许更多的违例(以换取更宽的边界;即,更小的wᵗw)。

相当令人惊讶的是,可以证明等价的对偶问题仅通过将每个点的α限制为≤C而发生变化。

由于允许违例,支持向量(α>0的点)不再全部位于边界的边缘。可以证明,任何已发生违例的支持向量将有α=C,而非支持向量(α=0)不能发生违例。我们称潜在发生违例的支持向量(α=C)为“非边界支持向量”,而其他未发生违例的纯支持向量(位于边缘)称为“边界支持向量”(0<α<C)。

可以证明推断方程没有变化:

但是,现在(xₛ,yₛ)必须是一个没有发生违例的支持向量,因为方程假设它在边界的边缘(之前,任何支持向量都可以使用)。

核函数软边界 SVM

软边界 SVM 将硬边界 SVM 扩展以处理噪声,但通常数据由于噪声之外的因素,如自然非线性,而无法被超平面分开。在这些情况下,软边界 SVM 可以使用,但最佳解可能涉及允许比现实中容许的更多错误的超平面。

图源 Machine Learner ,来自 WikimediaCC BY-SA 4.0 国际

核软边际 SVM 将软边际 SVM 推广到处理数据自然非线性的情况。例如,在左侧显示的例子中,没有线性超平面可以由软边际 SVM 找到,无论 C 的设置如何,都无法适当地分离数据。

然而,通过某种变换函数 z=Φ(x),可以将数据集中的每个点 x 映射到更高维度,以使数据在新的高维空间中更加线性(或完全线性)。这相当于在对偶中用 z 替代 x,得到:

实际上,尤其是当 Φ 变换到非常高维空间时,计算 z 可能需要很长时间。通过核技巧可以解决这个问题,它用数学函数(称为核函数)的等效计算替代 zz,而计算速度更快(例如,zz 的代数简化)。例如,以下是一些流行的核函数(每个核函数对应于某种变换 Φ 到更高维空间):

多项式的次数(Q)和 RBF γ 是超参数(由用户决定)

这样,对偶优化问题变为:

直观地,推理方程变为(经过代数操作后):

假设你有 数学背景 的话,可以在 这里 找到上述所有方程的完整推导。

Scott Graham 提供的照片,来自 Unsplash

实现

对于实现,我们将使用

基本导入

我们从导入一些基本库开始:

import numpy as np                  # for basic operations over arrays
from scipy.spatial import distance  # to compute the Gaussian kernel
import cvxopt                       # to solve the dual opt. problem
import copy                         # to copy numpy arrays 

定义核函数和 SVM 超参数

我们从使用各自函数定义三个核函数开始

多项式的次数(Q)和 RBF γ 是超参数(由用户决定)

class SVM:
    linear = lambda x, xࠤ , c=0: x @ xࠤ.T
    polynomial = lambda x, xࠤ , Q=5: (1 + x @ xࠤ.T)**Q
    rbf = lambda x, xࠤ, γ=10: np.exp(-γ*distance.cdist(x, xࠤ,'sqeuclidean'))
    kernel_funs = {'linear': linear, 'polynomial': polynomial, 'rbf': rbf}

为了与其他核一致,线性核需要一个额外的无用超参数。显然,kernel_funs 接受一个字符串作为核,并返回相应的核函数。

现在让我们继续定义构造函数:

class SVM:
    linear = lambda x, xࠤ , c=0: x @ xࠤ.T
    polynomial = lambda x, xࠤ , Q=5: (1 + x @ xࠤ.T)**Q
    rbf = lambda x, xࠤ, γ=10: np.exp(-γ*distance.cdist(x, xࠤ,'sqeuclidean'))
    kernel_funs = {'linear': linear, 'polynomial': polynomial, 'rbf': rbf}

    def __init__(self, kernel='rbf', C=1, k=2):
        # set the hyperparameters
        self.kernel_str = kernel
        self.kernel = SVM.kernel_funs[kernel]
        self.C = C                  # regularization parameter
        self.k = k                  # kernel parameter

        # training data and support vectors (set later)
        self.X, y = None, None
        self.αs = None

        # for multi-class classification (set later)
        self.multiclass = False
        self.clfs = [] 

SVM 有三个主要超参数,核函数(我们存储给定的字符串和相应的核函数)、正则化参数 C 和核超参数(传递给核函数);它代表多项式核的 Q 和 RBF 核的 γ。

定义 Fit 方法

为了在单独的单元格中扩展此类,包含 fitpredict 函数,我们将定义以下函数,并稍后将其用作装饰器:

SVMClass = lambda func: setattr(SVM, func.__name__, func) or func

回顾拟合 SVM 对应于通过解决对偶优化问题找到每个点的支持向量 α

α 为一个变量列向量 α₂ … α_N)ᵗ,并且设 y 为一个常量列向量 (y y₂ … y_N)ᵗ,并且设 K 为一个常量矩阵,其中 K[n,m] 计算 (xₙ, xₘ) 处的核函数值。回顾以下基于索引的点积、外积和二次形式的等价:

以便能够以矩阵形式写出对偶优化问题,如下:

由于这如我们之前所提示的是一个二次规划问题,我们可以查看 CVXOPT 文档中的二次编程:

来自 CVXOPT 文档。GNU 通用公共许可证

方括号告诉你,你可以仅用 (P,q)(P,q,G,h)(P, q, G, h, A, b) 等(任何未给出的项将被设置为默认值,例如 1)。

为了了解我们案例中 (P, q, G, h, A, b) 的值,我们进行以下比较:

通过以下方式简化比较:

注意我们通过将函数乘以 -1 将 max 改为 min

现在很明显(注意 0≤α 等价于 -α≤0):*

基于此,我们可以写出以下拟合函数:

@SVMClass
def fit(self, X, y, eval_train=False):
    # if more than two unique labels, call the multiclass version
    if len(np.unique(y)) > 2:
        self.multiclass = True
        return self.multi_fit(X, y, eval_train)

    # if labels given in {0,1} change it to {-1,1}
    if set(np.unique(y)) == {0, 1}: y[y == 0] = -1

    # ensure y is a Nx1 column vector (needed by CVXOPT)
    self.y = y.reshape(-1, 1).astype(np.double) # Has to be a column vector
    self.X = X
    N = X.shape[0]  # Number of points

    # compute the kernel over all possible pairs of (x, x') in the data
    # by Numpy's vectorization this yields the matrix K
    self.K = self.kernel(X, X, self.k)

    ### Set up optimization parameters
    # For 1/2 x^T P x + q^T x
    P = cvxopt.matrix(self.y @ self.y.T * self.K)
    q = cvxopt.matrix(-np.ones((N, 1)))

    # For Ax = b
    A = cvxopt.matrix(self.y.T)
    b = cvxopt.matrix(np.zeros(1))

    # For Gx <= h
    G = cvxopt.matrix(np.vstack((-np.identity(N),
                                 np.identity(N))))
    h = cvxopt.matrix(np.vstack((np.zeros((N,1)),
                                 np.ones((N,1)) * self.C)))

    # Solve    
    cvxopt.solvers.options['show_progress'] = False
    sol = cvxopt.solvers.qp(P, q, G, h, A, b)
    self.αs = np.array(sol["x"])            # our solution

    # a Boolean array that flags points which are support vectors
    self.is_sv = ((self.αs-1e-3 > 0)&(self.αs <= self.C)).squeeze()
    # an index of some margin support vector
    self.margin_sv = np.argmax((0 < self.αs-1e-3)&(self.αs < self.C-1e-3))

    if eval_train:  
      print(f"Finished training with accuracy{self.evaluate(X, y)}")

我们确保这是一个二分类问题,且二分类标签按 SVM 假设设置为(±1),并且 y 是一个维度为 (N,1) 的列向量。然后我们解决优化问题以找到 α₂ … α_N)ᵗ。

我们使用 α₂ … α_N)ᵗ 来获得一个标志数组,其中任何对应于支持向量的索引为 1,以便我们可以稍后通过仅对支持向量求和,并为 (xₛ,yₛ) 应用预测方程。注意,在检查中我们假设非支持向量的 α 可能不会完全为 0,如果是 α≤1e-3,则大致为零(我们知道 CVXOPT 结果可能不是最终精确的)。同样,我们假设非边际支持向量的 α 可能不会完全为 C

定义 Predict 方法

回顾预测方程是:

@SVMClass
def predict(self, X_t):
    if self.multiclass: return self.multi_predict(X_t)
    # compute (xₛ, yₛ)
    xₛ, yₛ = self.X[self.margin_sv, np.newaxis], self.y[self.margin_sv]
    # find support vectors
    αs, y, X= self.αs[self.is_sv], self.y[self.is_sv], self.X[self.is_sv]
    # compute the second term
    b = yₛ - np.sum(αs * y * self.kernel(X, xₛ, self.k), axis=0)
    # compute the score
    score = np.sum(αs * y * self.kernel(X, X_t, self.k), axis=0) + b
    return np.sign(score).astype(int), score

就是这样。我们还可以实现一个evaluate方法来计算准确性(用于上面的 fit)。

@SVMClass
def evaluate(self, X,y):  
    outputs, _ = self.predict(X)
    accuracy = np.sum(outputs == y) / len(y)
    return round(accuracy, 2)

测试实现

from sklearn.datasets import make_classification
import numpy as np

# Load the dataset
np.random.seed(1)
X, y = make_classification(n_samples=2500, n_features=5, 
                           n_redundant=0, n_informative=5, 
                           n_classes=2,  class_sep=0.3)

# Test Implemented SVM
svm = SVM(kernel='rbf', k=1)
svm.fit(X, y, eval_train=True)

y_pred, _ = svm.predict(X)
print(f"Accuracy: {np.sum(y==y_pred)/y.shape[0]}")  #0.9108

# Test with Scikit
from sklearn.svm import SVC
clf = SVC(kernel='rbf', C=1, gamma=1)
clf.fit(X, y)
y_pred = clf.predict(X)
print(f"Accuracy: {sum(y==y_pred)/y.shape[0]}")    #0.9108

你可以更改数据集和超参数以进一步确保它们是相同的。理想情况下,通过比较决策函数而不是准确性来进行。

多类推广

@SVMClass
def multi_fit(self, X, y, eval_train=False):
    self.k = len(np.unique(y))      # number of classes
    # for each pair of classes
    for i in range(self.k):
        # get the data for the pair
        Xs, Ys = X, copy.copy(y)
        # change the labels to -1 and 1
        Ys[Ys!=i], Ys[Ys==i] = -1, +1
        # fit the classifier
        clf = SVM(kernel=self.kernel_str, C=self.C, k=self.k)
        clf.fit(Xs, Ys)
        # save the classifier
        self.clfs.append(clf)
    if eval_train:  
        print(f"Finished training with accuracy {self.evaluate(X, y)}")

为了将模型推广到多类,我们在每个现有类别上训练一个二进制 SVM 分类器,其中我们对每个类别进行循环,将属于该类别的点重新标记为+1,将所有其他类别的点标记为-1。

训练结果是* k 个分类器,当给定k类时,其中第 i 个分类器是在将第 i *类标记为+1 而所有其他类标记为-1 的数据上训练的。

将预测推广到多类

然后,在新的示例上进行预测时,我们选择对应分类器最有信心的类(具有最高得分)

@SVMClass
def multi_predict(self, X):
    # get the predictions from all classifiers
    N = X.shape[0]
    preds = np.zeros((N, self.k))
    for i, clf in enumerate(self.clfs):
        _, preds[:, i] = clf.predict(X)

    # get the argmax and the corresponding score
    return np.argmax(preds, axis=1), np.max(preds, axis=1)

测试实现

from sklearn.datasets import make_classification
import numpy as np

# Load the dataset
np.random.seed(1)
X, y = make_classification(n_samples=500, n_features=2, 
                           n_redundant=0, n_informative=2, 
                           n_classes=4, n_clusters_per_class=1,  
                           class_sep=0.3)

# Test SVM
svm = SVM(kernel='rbf', k=4)
svm.fit(X, y, eval_train=True)

y_pred = svm.predict(X)
print(f"Accuracy: {np.sum(y==y_pred)/y.shape[0]}") # 0.65

# Test with Scikit
from sklearn.multiclass import OneVsRestClassifier
from sklearn.svm import SVC

clf = OneVsRestClassifier(SVC(kernel='rbf', C=1, gamma=4)).fit(X, y)
y_pred = clf.predict(X)
print(f"Accuracy: {sum(y==y_pred)/y.shape[0]}")    # 0.65

绘制每个决策区域会产生以下图:

作者提供的图

请注意,尽管 Sci-kit Learn 的 SVM 默认支持 OVR(如上所示没有显式调用),但这也可能具有特定于 SVM 的进一步优化。

完整代码

 import numpy as np                  # for basic operations over arrays
from scipy.spatial import distance  # to compute the Gaussian kernel
import cvxopt                       # to solve the dual optimization problem
import copy                         # to copy numpy arrays 

class SVM:
    linear = lambda x, xࠤ , c=0: x @ xࠤ .T
    polynomial = lambda x, xࠤ , Q=5: (1 + x @ xࠤ.T)**Q
    rbf = lambda x, xࠤ , γ=10: np.exp(-γ * distance.cdist(x, xࠤ,'sqeuclidean'))
    kernel_funs = {'linear': linear, 'polynomial': polynomial, 'rbf': rbf}

    def __init__(self, kernel='rbf', C=1, k=2):
        # set the hyperparameters
        self.kernel_str = kernel
        self.kernel = SVM.kernel_funs[kernel]
        self.C = C                  # regularization parameter
        self.k = k                  # kernel parameter

        # training data and support vectors
        self.X, y = None, None
        self.αs = None

        # for multi-class classification
        self.multiclass = False
        self.clfs = []                                  

# This is useless here (only for notebook)
SVMClass = lambda func: setattr(SVM, func.__name__, func) or func

@SVMClass
def fit(self, X, y, eval_train=False):
    if len(np.unique(y)) > 2:
        self.multiclass = True
        return self.multi_fit(X, y, eval_train)

    # relabel if needed
    if set(np.unique(y)) == {0, 1}: y[y == 0] = -1
    # ensure y has dimensions Nx1
    self.y = y.reshape(-1, 1).astype(np.double) # Has to be a column vector
    self.X = X
    N = X.shape[0]

    # compute the kernel over all possible pairs of (x, x') in the data
    self.K = self.kernel(X, X, self.k)

    # For 1/2 x^T P x + q^T x
    P = cvxopt.matrix(self.y @ self.y.T * self.K)
    q = cvxopt.matrix(-np.ones((N, 1)))

    # For Ax = b
    A = cvxopt.matrix(self.y.T)
    b = cvxopt.matrix(np.zeros(1))

    # For Gx <= h
    G = cvxopt.matrix(np.vstack((-np.identity(N),
                                 np.identity(N))))
    h = cvxopt.matrix(np.vstack((np.zeros((N,1)),
                                 np.ones((N,1)) * self.C)))

    # Solve    
    cvxopt.solvers.options['show_progress'] = False
    sol = cvxopt.solvers.qp(P, q, G, h, A, b)
    self.αs = np.array(sol["x"])

    # Maps into support vectors
    self.is_sv = ((self.αs > 1e-3) & (self.αs <= self.C)).squeeze()
    self.margin_sv = np.argmax((1e-3 < self.αs) & (self.αs < self.C - 1e-3))

    if eval_train:  
      print(f"Finished training with accuracy {self.evaluate(X, y)}")

@SVMClass
def multi_fit(self, X, y, eval_train=False):
    self.k = len(np.unique(y))      # number of classes
    # for each pair of classes
    for i in range(self.k):
        # get the data for the pair
        Xs, Ys = X, copy.copy(y)
        # change the labels to -1 and 1
        Ys[Ys!=i], Ys[Ys==i] = -1, +1
        # fit the classifier
        clf = SVM(kernel=self.kernel_str, C=self.C, k=self.k)
        clf.fit(Xs, Ys)
        # save the classifier
        self.clfs.append(clf)
    if eval_train:  
      print(f"Finished training with accuracy {self.evaluate(X, y)}")

@SVMClass
def predict(self, X_t):
    if self.multiclass: return self.multi_predict(X_t)
    xₛ, yₛ = self.X[self.margin_sv, np.newaxis], self.y[self.margin_sv]
    αs, y, X= self.αs[self.is_sv], self.y[self.is_sv], self.X[self.is_sv]

    b = yₛ - np.sum(αs * y * self.kernel(X, xₛ, self.k), axis=0)
    score = np.sum(αs * y * self.kernel(X, X_t, self.k), axis=0) + b
    return np.sign(score).astype(int), score

@SVMClass
def multi_predict(self, X):
    # get the predictions from all classifiers
    preds = np.zeros((X.shape[0], self.k))
    for i, clf in enumerate(self.clfs):
        _, preds[:, i] = clf.predict(X)

    # get the argmax and the corresponding score
    return np.argmax(preds, axis=1)

@SVMClass
def evaluate(self, X,y):  
    outputs, _ = self.predict(X)
    accuracy = np.sum(outputs == y) / len(y)
    return round(accuracy, 2)

from sklearn.datasets import make_classification
import numpy as np

# Load the dataset
np.random.seed(1)
X, y = make_classification(n_samples=500, n_features=2, 
          n_redundant=0, n_informative=2, n_classes=4, 
          n_clusters_per_class=1,  class_sep=0.3)

# Test SVM
svm = SVM(kernel='rbf', k=4)
svm.fit(X, y, eval_train=True)

y_pred = svm.predict(X)
print(f"Accuracy: {np.sum(y==y_pred)/y.shape[0]}")

# Test with Scikit
from sklearn.multiclass import OneVsRestClassifier
from sklearn.svm import SVC

clf = OneVsRestClassifier(SVC(kernel='rbf', C=1, gamma=4)).fit(X, y)
y_pred = clf.predict(X)
print(f"Accuracy: {sum(y==y_pred)/y.shape[0]}") 

照片由Nathan Van Egmond拍摄,Unsplash提供

总之,我们实现了支持向量机(SVM)学习算法,涵盖了其一般的软边距和核化形式。我们提供了 SVM 的概述,开发了代码中的模型,扩展了多类场景,并使用 Sci-kit Learn 验证了我们的实现。

希望你发现从这个故事中学到的内容对你的工作有用。下次见,再见。

资源:

代码主要是对这里存在的代码的适配(MIT 许可证):

Persson, Aladdin. “从零开始的 SVM——机器学习 Python(支持向量机)。” YouTube

Hugging Face 简介及 6 种 NLP 任务实现

原文:towardsdatascience.com/implement-nlp-tasks-using-hugging-face-77dfdcad65fd

Hugging Face 用于 NLP 任务的入门教程

Farzad MahmoodinobarTowards Data Science Farzad Mahmoodinobar

·发布于 Towards Data Science ·阅读时间 12 分钟·2023 年 4 月 18 日

--

照片由 Duy Pham 提供,来源于 Unsplash

Hugging Face 是一个开源 AI 社区,由机器学习从业者创建,专注于自然语言处理(NLP)、计算机视觉以及音频/语音处理任务。无论你是已经在这些领域工作还是希望未来进入这些领域,你都会从学习如何使用 Hugging Face 的工具和模型中受益。

在这篇文章中,我们将介绍利用 Hugging Face 上的预训练模型完成的六种最常用的 NLP 任务,如下所示:

  1. 文本生成(即语言建模)

  2. 问答系统

  3. 情感分析

  4. 文本分类

  5. 文本摘要

  6. 机器翻译

在开始任务之前,让我们花一分钟时间讨论“训练”和“推断”这两个机器学习中的重要概念,以澄清我们今天将要做的工作。

让我们开始吧!

[## 使用我的推荐链接加入 Medium

阅读 Farzad 的每一个故事(以及 Medium 上其他作者的故事)。你的会员费直接支持 Farzad 和其他…

medium.com](https://medium.com/@fmnobar/membership?source=post_page-----77dfdcad65fd--------------------------------)

机器学习中的训练与推断

训练是将大量数据输入机器学习模型的过程。在这个过程中,模型通过优化目标函数从提供的数据中“学习”,因此这个过程被称为“训练”。一旦我们有了训练好的模型,我们可以使用它来对模型之前未见过的新数据进行预测。这个过程被称为“推理”。简而言之,训练是模型的学习过程,而推理是模型进行预测的过程(即我们实际使用模型时)。

现在我们理解了训练和推理之间的区别后,我们可以更具体地定义我们今天要做的工作。在这篇文章中,我们将使用各种预训练模型进行推理。换句话说,我们不会在这里经历训练任何新模型的昂贵过程。另一方面,我们将利用 Hugging Face Hub 中众多现有的预训练模型,并使用这些模型进行推理(即进行预测)。

1. 文本生成(又称语言建模)

我决定从这个任务开始,考虑到最近对生成性人工智能(如 ChatGPT)的兴趣激增。这个任务通常被称为语言建模,模型执行的任务是预测文本中缺失的部分(这可以是一个词、token 或更大的文本字符串)。最近引起了很多兴趣的是,模型可以生成文本,而不一定需要之前见过这样的提示。

让我们看看它在实践中的效果!

1.1. 文本生成 — 实现

为了实现文本生成,我们将从 transformers 库中导入 pipeline,使用其中一个 GPT 模型,并按照以下步骤进行。我还在代码中添加了注释,以便您更容易地跟随这些步骤:

  1. 导入库

  2. 指定要用于此特定任务的预训练模型的名称

  3. 指定将由模型完成的句子

  4. 创建一个 pipeline 的实例作为 generator

  5. 执行文本生成并将结果存储为output

  6. 返回结果

下面的代码块按照这些步骤进行。

# Import libraries
from transformers import pipeline

# Specify the model
model = "gpt2"

# Specify the task
task = "text-generation"

# Instantiate pipeline
generator = pipeline(model = model, task = task, max_new_tokens = 30)

# Specify input text
input_text = "If you are interested in learing more about data science, I can teach you how to"

# Perform text generation and store the results
output = generator(input_text)

# Return the results
output

结果:

文本生成结果

我们可以从结果中看到,模型根据我们提供的输入文本生成了额外的文本,考虑到它所训练的数据和我们提供的句子。请注意,我将输出的长度限制为 max_new_tokens 的 30 个 token,以防生成过长的响应。生成的文本听起来合理且与上下文相关。

但如果我们想向模型提出一个问题呢?模型能否回答问题,而不仅仅是完成一个不完整的句子?让我们接下来探索一下。

2. 问答

问答,如其名所示,是一个模型回答用户提出的问题的任务。通常有两种类型的问答任务:

  1. 抽象式(即上下文相关): 用户在问题/提示中向模型描述一个情况,并要求模型根据提供的信息生成回应。在这种情况下,模型从提示中挑选相关的信息部分,并返回结果。

  2. 抽取式(即上下文无关): 用户向模型提问,而不提供任何上下文。

让我们看看如何实现问答系统。

2.1. 问答系统 — 实现

实现过程与语言建模任务类似。我们将使用两个不同的模型来比较结果。

我们从distilbert-base-cased-distilled-squad开始 (link)。

# Specify model
model = 'distilbert-base-cased-distilled-squad'

# Instantiate pipeline
answerer = pipeline(model = model, task="question-answering")

# Specify question and context
question = "What does NLP stand for?"
context = "Today we are talking about machine learning and specifically the natural language processing, which enables computers to understand, process and generate languages"

# Generate predictions
preds = answerer(
    question = question,
    context = context,
)

# Return results
print(
    f"score: {round(preds['score'], 4)}, start: {preds['start']}, end: {preds['end']}, answer: {preds['answer']}"
)

结果:

问答结果 (distilbert-base-cased-distilled-squad Model)

我们可以看到总结,模型能够确定哪个部分的上下文与回答问题相关。

让我们使用另一个模型deepset/roberta-base-squad2 (link)来实现相同的问题。

# Specify model
model = "deepset/roberta-base-squad2"

# Specify task
task = "question-answering"

# Instantiate pipeline
answerer = pipeline(task = task, model = model, tokenizer = model)

# Specify input
qa_input = {
    'question': 'What does NLP stand for?',
    'context': 'Today we are talking about machine learning and specifically the natural language processing, which enables computers to understand, process and generate languages'
}

# Generate predictions
output = answerer(qa_input)

# Return results
output

结果:

问答结果 (deepset/roberta-base-squad2 Model)

正如我们在上面的例子中看到的,第二个模型也能够识别出 NLP 代表自然语言处理,鉴于我们提供的上下文。

让我们继续我们的 NLP 任务之旅,接下来看看情感分析。

3. 情感分析

情感分析是将文本的情感分类为积极、消极或中性的过程。情感分析在不同的行业中有广泛的应用,比如通过产品评论监控客户情绪,甚至在政治中,比如在选举年评估公众对某一主题的兴趣。本文的重点是使用 Hugging Face 完成各种任务,因此我们不会深入探讨每个主题,但如果您有兴趣深入了解情感分析,可以参考这篇文章:

## 情感分析 — 介绍与实现

使用 NLTK、scikit-learn 和 TextBlob 进行情感分析

towardsdatascience.com

3.1. 情感分析 — 实现

为了实现情感分析,我们将再次依赖transformers库中的pipeline,并按照以下步骤操作。我还在代码中添加了注释,以便您更容易跟随步骤。

  1. 导入库

  2. 指定用于特定任务(例如情感分析)的预训练模型名称。

  3. 指定任务(即情感分析)

  4. 指定将进行情感分析的句子

  5. 创建一个pipeline实例作为analyzer

  6. 执行情感分析并将结果保存为output

  7. 返回结果

# Specify pre-trained model to use
model = 'distilbert-base-uncased-finetuned-sst-2-english'

# Specify task
task = 'sentiment-analysis'

# Text to be analyzed
input_text = 'Performing NLP tasks using HuggingFace pipeline is super easy!'

# Instantiate pipeline
analyzer = pipeline(task, model = model)

# Store the output of the analysis
output = analyzer(input_text)

# Return output
output

结果:

情感分析结果

结果表明,句子的情感是积极的,得分约为 85%。这句句子听起来相当积极,所以到目前为止我喜欢这些结果。可以自由地重复这一过程,测试其他句子!

让我们转向另一种文本分类类型。

4. 文本分类

情感分析,正如我们刚刚讨论的,可以被视为文本分类的一个特殊案例,其中类别(或类)仅为正面、负面或中性。文本分类则更为通用,它可以将传入的文本(例如句子、段落或文档)分类到预定义的类别中。让我们在实践中深入了解一下这意味着什么。

4.1. 文本分类 — 实现

我们将使用相同的pipeline包,并采取与情感分析非常相似的步骤,如下所示:

# Specify model
model = 'facebook/bart-large-mnli'

# Specify Task
task = 'zero-shot-classification'

# Specify input text
input_text = 'This is a tutorial about using pre-trained models through HuggingFace'

# Identify the classes/categories/labels
labels = ['business', 'sports', 'education', 'politics', 'music']

# Instantiate pipeline
classifier = pipeline(task, model = model)

# Store the output of the analysis
output = classifier(input_text, candidate_labels = labels)

# Return output
output

结果:

文本分类结果

结果相当有趣!分数对应于每个标签,按从大到小排序以便阅读。例如,结果表明我们的句子被标记为“教育”,得分约为 40%,其次是“商业”,得分约为 22%,而“音乐”、“体育”和“政治”标签的分数都很低,这总体上对我来说是合理的。

让我们继续进行下一个任务,即摘要。

5. 文本摘要

文本摘要的任务是自动总结文本输入,同时仍然传达传入文本的主要点或要旨。需要这种摘要模型的商业直觉示例是那些人类阅读传入的文本通信(例如客户邮件)的情况,使用摘要模型可以节省人力时间。例如,这些人工代表可以阅读客户邮件的摘要,而不是全部邮件,从而通过节省人力时间和成本提高操作效率。

让我们看看如何实现文本摘要。

5.1. 文本摘要 — 实现

与其他任务类似,我们将使用pipeline进行摘要任务。对于这个具体任务,我们将首先使用来自 Google 的名为T5的文本到文本预训练模型,来总结我们刚刚阅读的关于“文本摘要”的描述。然后,我们将使用 Google 的不同模型重复相同的练习,看看结果的变化。让我们看看如何实现这一点。

# Specify model and tokenizer
model = "t5-base"
tokenizer = "t5-base"

# Specify task
task = "summarization"

# Specify input text
input_text = "Text summarization is the task of automatically summarizing textual input, while still conveying the main points and gist of the incoming text. One example of the business intuition behind the need for such summarization models is the situations where humans read incoming text communications (e.g. customer emails) and using a summarization model can save human time. "

# Instantiate pipeline
summarizer = pipeline(task = task, model = model, tokenizer = tokenizer, framework = "tf")

# Summarize and store results
output = summarizer(input_text)

# Return output
output

结果:

文本摘要结果(T5 模型)

正如你在结果中看到的,T5 模型接受了相对较长的输入文本,并返回了一个简洁的摘要,概述了它认为输入文本的主要点。我喜欢这个摘要,因为它解释了什么是文本摘要以及它能提供什么好处——这是一个很好的摘要!

让我们尝试另一个来自 Google 的模型,Pegasus,看看使用不同模型时结果如何变化。

# Specify model
model = 'google/pegasus-cnn_dailymail'

# Specify task
task = 'summarization'

# Specify inpute text
input_text = "Text summarization is the task of automatically summarizing textual input, while still conveying the main points and gist of the incoming text. One example of the business intuition behind the need for such summarization models is the situations where humans read incoming text communications (e.g. customer emails) and using a summarization model can save human time. "

# Instantiate pipeline
summarizer = pipeline(task = task, model = model)

# Summarize and store results
output = summarizer(input_text, max_length = 75, min_length = 25)

# Return output
output

结果:

文本摘要结果(Pegasus 模型)

正如预期的那样,两种模型的结果有所不同,因为它们各自使用了特定的数据和训练目标,但都在某种程度上完成了任务。我个人更喜欢 T5 模型的结果,因为它更简洁地陈述了输入文本的要点。

最后,我们将要讨论的任务是机器翻译。

6. 机器翻译

机器翻译是生成输入文本在目标语言中翻译的任务。这类似于 Google Translate 或其他类似翻译引擎提供的服务。使用 Hugging Face 进行机器翻译的好处之一是我们可以选择用于翻译的模型,这可能为我们寻找的特定语言提供更准确的翻译。

让我们来看看 Hugging Face 中机器翻译的实现。

6.1. 机器翻译 — 实现

为了生成翻译,我们将使用两个最常见的预训练模型将同一句话从英语翻译成法语。每个模型的实现略有不同,但整体过程与我们到目前为止实现的其他任务相同。

6.1.1. T5

T5是 Google 开发的一个编码器-解码器预训练模型,在包括机器翻译在内的多个任务上表现良好。为了提示 T5 执行诸如从语言 X 到语言 Y 的翻译等任务,我们会在每个任务的输入句子前添加一个字符串(称为“前缀”):"translate X to Y: sentence_to_be_translated"

实际上,这在实践中更容易理解,所以我们直接使用 T5 将一个句子从英语翻译成法语,看看效果如何。

# Specify prefix
original_language = 'English'
target_language = 'French'
prefix = f"translate {original_language} to {target_language}: "

# Specify input text
input_text = f"{prefix}This is a post on Medium about various NLP tasks using Hugging Face."

# Specify model
model = "t5-base"

# Specify task
task = "translation"

# Instantiate pipeline
translator = pipeline(task = task, model = model)

# Perform translation and store the output
output = translator(input_text)

# Return output
output

结果:

机器翻译结果(T5 模型)

我在 Google Translate 上查找了这个翻译,看起来这是一个不错的翻译!我对这种验证方法有些担忧,因为 T5 也是由 Google 开发的。我不知道 Google Translate 使用的是哪个具体模型,但我们将在下一节中使用 mBART 运行相同的翻译任务,看看结果有多大差异。

6.1.2. mBART

mBART是由 Meta 开发的多语言编码器-解码器预训练模型,主要用于机器翻译任务。mBART 与 T5 不同,它不需要提示中的前缀,但我们需要向模型提供原始语言和目标语言的识别。

让我们在 mBART 中实现相同的任务。

# Import packages
from transformers import MBartForConditionalGeneration, MBart50TokenizerFast

# Specify input text
input_text = "This is a post on Medium about various NLP tasks using Hugging Face."

# Specify model
model_name = "facebook/mbart-large-50-many-to-many-mmt"

# Instantiate model and tokenizer
model = MBartForConditionalGeneration.from_pretrained(model_name)
tokenizer = MBart50TokenizerFast.from_pretrained(model_name)

# Specify source language
tokenizer.src_lang = "en_XX"

# Encode input text
encoded_en = tokenizer(input_text, return_tensors="pt")

# Perform translation to the target language
generated_tokens = model.generate(**encoded_en, forced_bos_token_id=tokenizer.lang_code_to_id["fr_XX"])

# Decode the translation and store the output
output = tokenizer.batch_decode(generated_tokens, skip_special_tokens=True)

# Return output
output

机器翻译结果(mBART 模型)

结果与 T5 生成的结果非常相似,唯一的区别是“poste”被替换为“post”。尽管两者之间存在差异,但这次练习的主要目的是展示这些预训练模型如何生成机器翻译,我们已经使用这两个模型完成了这一目标。

结论

在这篇文章中,我们介绍了 Hugging Face,这是一个开源 AI 社区,被许多从事自然语言处理、计算机视觉和音频/语音处理任务的机器学习从业者使用。然后,我们演示了如何在 Hugging Face 平台上实现这些预训练模型,以完成下游 NLP 任务,如文本生成、问答、情感分析、文本分类、文本摘要和机器翻译。

感谢阅读!

如果你觉得这篇文章对你有帮助,请关注我在 Medium并订阅以接收我最新的帖子!

(除非另有说明,所有图片均由作者提供。)

Reluplex 的实现细节:一种高效的 SMT 求解器用于验证深度神经网络

原文:towardsdatascience.com/implementation-details-of-reluplex-an-efficient-smt-solver-for-verifying-deep-neural-networks-379ea359c41a?source=collection_archive---------5-----------------------#2023-12-27

如何正式验证你的神经网络的边界

Matthew DawTowards Data Science Matthew Daw

·

关注 发表在 Towards Data Science ·17 分钟阅读·2023 年 12 月 27 日

--

图片由 NEOM 提供,来源于 Unsplash

Reluplex 是斯坦福大学于 2017 年提交到 CAV 的一个算法 [1]。Reluplex 旨在正式验证神经网络是否能够在给定某些输入的情况下生成特定的输出。它接受一个神经网络及对网络输入和输出的约束作为输入。这些约束可以将任意数量的输入或输出节点限制为一个值或一个值的范围。然后,算法在给定的输入约束范围内找到一个输入,该输入可以产生给定的输出约束范围内的输出。如果不存在这样的例子,它将判断该问题在合理的时间内不可行。

算法用途

原始算法是为构建“无人机空中碰撞避免系统”而编写的。该系统使用 45 个深度学习网络来飞行一系列无人机。研究人员需要一种方法来正式保证,无论网络接收到什么其他输入,如果两架无人机太接近,它们将总是远离对方飞行而不会发生碰撞。在最极端的情况下,该算法能够在 109.6 小时内完成这些验证,虽然时间较长,但仍比之前的最先进算法快一个数量级。

碰撞航线规避,图像来自原始论文 [1]

在最近的出版物中,ReluPlex 被一个名为 Marabou 的工具所取代 [4],Marabou 能够比 ReluPlex 做得更好。这一工具用于神经网络的可解释性。该算法通过寻找输入的上界和下界来确定生成网络输出所必需的输入部分。[6]

神经网络解释的例子,来源于《Towards Formal XAI》 [6]

该算法还被用于设定精确的界限,以确定什么样的对抗性扰动足够大,能够改变网络的输出。

在本文中,我们希望讨论 Reluplex 的详细信息,因为它们构成了理解 Maribou 的重要基础。

基本神经网络

在解释该算法的详细信息之前,我们首先需要了解一些神经网络的基础知识。以下是一个简单的网络图:

基本感知机神经网络的图示 [8]

在上图中,隐藏层的计算方式是将前一层的所有节点乘以特定的值,然后将它们相加,再加上一个对每个节点特定的偏置项。然后,求和值通过一个激活函数 f,再用于下一层。我们将在本文中使用的激活函数是 ReLU 函数,其定义为 f(x) = x 如果 x > 0 否则为 0:

ReLU 函数的例子。图像来源于《Deep Learning using Rectified Linear Units》 [2]

Reluplex 的高级视角

reluplex 的作用是尝试将神经网络转化为一个简单形问题。一旦在简单形问题中设置了约束,它就能快速找到解决方案或确定在给定约束下是否没有有效解。它还是一个经过证明的非常高效的算法,并且有权威的形式证明保证它每次都能有效。这正是我们对 reluplex 问题的期望。唯一的问题是 reluplex 只能处理线性约束,而我们神经网络中的 relu 函数是非线性的,不能直接用于简单形方法。为了使其线性化,我们必须选择施加额外的约束,要么使 relu 的输入必须是非正的,使 relu 失效,或者将 relu 的输入约束为非负的,使 relu 函数有效。默认情况下,大多数 SMT 求解器会通过手动检查每种可能的约束组合来绕过这个问题。然而,在一个包含 300 多个 relu 函数的网络中,这可能会转化为 2³⁰⁰ 种情况拆分,这在解决时速度过于缓慢。那么,reluplex 的做法是,首先对一个没有 relu 约束的网络进行编码,并找到一个可行的点。如果存在可行点,它将逐一修复违反 relu 约束的网络部分。如果一个特定节点更新过多次,会将问题拆分为一个该节点始终有效的情况和一个该节点始终无效的情况,然后继续搜索。原始论文的作者能够形式上证明这个算法是健全且完整的,并且会在有限的步骤内终止。他们还通过实证方法显示它比逐一检查每个可能情况的简单暴力方法终止得更快。

本文的目的

原始论文给出了算法实际工作的一个很好的示例。然而,他们假设读者对简单形方法有深入理解,并跳过了一些必须做出的重要设计选择。因此,本文的目的是详细列出算法使用的确切步骤,以及简单形方法的所有细节。我们还提供了一个简化且有效的 Python 实现。原始论文提供了形式证明,保证只要简单形方法有效,reluplex 就会有效。同时,还有相对大量的出版物证明了 reluplex 的有效性。因此,我们不提供关于此算法有效性的正式论证。我们只是希望解释算法所需的步骤,并提供作者选择这些步骤的直观理解。深入理解这个算法可能需要阅读原始的 reluplex 论文或研究简单形方法。

简单形方法

单纯形法旨在解决定义线性空间内的优化问题。这些问题涉及一组非负变量,对这些变量施加约束,并声明一个目标函数。

作者提供的图片

在建立这些约束条件后,单纯形法最初会验证是否存在满足指定约束条件的点。确定了这样的点后,该方法会继续寻找在给定约束条件下最大化目标的点。

Reluplex 的完整示例

以下是我们将用于演示完整算法的基本神经网络。

作者提供的图片

我们需要做的第一件事是分解隐藏层节点,一个节点将是前一节点的线性函数,另一个节点将是该线性函数输出的 ReLU。

作者提供的图片

现在我们声明对函数输入和输出的以下边界:

作者提供的图片

根据模型设置和声明的约束,我们的目标是将此问题转化为单纯形问题。由于单纯形法仅限于线性操作,直接将 ReLU 约束纳入设置是不切实际的。然而,我们可以为网络的所有其他组件引入约束。如果在没有 ReLU 约束的情况下得到的解是可行的,我们可以逐步添加这些约束,直到发现一个可行解或确定 ReLU 约束使问题变得不可解。因此,通过编码适用的约束,我们现在有如下内容:

作者提供的图片

为了将这些约束纳入单纯形法,我们需要将它们转换为标准形式。在标准形式中,所有非恒定变量都位于左侧,而所有常量都位于右侧,并且常量为正数。重写后,我们得到如下内容:

作者提供的图片

设置单纯形表

随后的步骤涉及将所有不等式转换为等式约束。为此,我们将引入松弛变量。这些变量本质上是非负的,并且可以取任意大的值。此外,它们确保我们的新等式约束在数学上等价于原始的不等式约束。

作者提供的图片

目前,单纯形方法固有地只适用于非负变量。然而,我们网络中的节点可能不符合这个非负性约束。为了适应负值,我们必须用分别表示正值和负值的变量替换可以是正值或负值的变量,如下所示:

图片由作者提供

通过这个替换,x_{+i} 和 x_{-i} 可以始终是正值,但仍然组合起来使 x_i 为负值。x_4 和 x_5 出现在 ReLU 之后,因此总是非负的,不需要这个替换。然而,所有其他神经网络节点变量都需要这个替换。进行这些替换后,我们现在有以下约束集。

图片由作者提供

分配负值并去掉括号后,我们得到如下:

图片由作者提供

现在我们已经将变量分为正值和负值部分,我们需要稍微退一步并记住,在我们解决这个方程组之后,我们需要进行调整以修正 ReLU 违规。为了帮助修复 ReLU 违规,我们准备引入一个新的线性约束。我们希望添加的约束是设置 x_{+2} = x_4 和 x_{+3} = x_5。这将使得对所有 i 属于 {2,3},x_{+i} 和 x_{-i} 都可以是非负的,但当发生这种情况时,ReLU 约束将不再成立。然而,修正 ReLU 约束将变得像添加一个约束使 x_{+i} 或 x_{-i} 等于零一样简单。这个问题可以在没有这个约束的情况下继续进行,它实际上并不是必须的。事实上,原始论文实际上并没有使用这个约束,但我包括它是因为我发现它确实使速度更快,因为它限制了搜索空间。这将导致以下新的约束集。

图片由作者提供

变量太多可能会导致混乱,使得很难看清楚发生了什么。因此,我们将所有内容重新写入一个表格矩阵中。

图片由作者提供

求解原始问题

现在我们已经将问题转换为表格矩阵,我们将使用两阶段法来解决这个设置。第一阶段涉及找到一个可行点,而第二阶段则将可行点移动以最大化特定的效用函数。然而,对于我们的特定用例,我们没有效用函数;我们的目标仅仅是确定是否存在一个可行点。因此,我们只会执行两阶段法的第一阶段。

为了识别一个可行点,单纯形法最初检查将所有松弛变量设置为右侧,将所有其他变量设置为零是否可行。在我们的情况下,这种方法不可行,因为没有松弛变量的非零等式约束。此外,在第 5 行和第 7 行,松弛变量乘以负数,使得表达式不可能评估为正的右侧,因为松弛变量始终为正。因此,为了获得初始可行点,我们将引入新的辅助变量,并将其设置为等于右侧,将所有其他变量设为零。对于具有正符号松弛变量的约束,将不会这样做,因为这些松弛变量可能已经等于右侧。为了提高清晰度,我们将左侧的列标明哪些变量被分配了非零值;这些被称为我们的基本变量。

图片由作者提供

我们的可行点是

图片由作者提供

现在我们有一个已知可行的点。然而,重要的是要认识到这些辅助变量改变了解,为了得到我们想要的真实解,我们需要消除它们。为了消除它们,我们将引入一个目标函数将它们设为零。具体来说,我们的目标是最小化函数 a1 + a2 + a3 + a4 + a5 + a6 + a7。如果我们成功将这个函数最小化为零,我们可以得出结论,原始方程组有一个可行点。然而,如果我们无法实现这一点,则表明没有可行点,我们可以终止问题并声明输入和输出不可行。

在表格和目标函数声明后,我们准备执行优化目标所需的主元操作。具体步骤如下:

  1. 用非基本变量替换目标函数中的基本变量。在这种情况下,所有辅助变量都是基本变量。为了替换它们,我们对所有辅助列和基本变量有非零值条目的行进行主元操作。主元操作是通过从主元行中添加或减去所有其他行,直到主元列中唯一的非零条目是主元行中的条目。

  2. 通过在目标函数中找到第一个且最大的值的列来选择主元列。

  3. 通过使用布兰德规则选择主元行。布兰德规则识别我们列中的所有正条目,将目标函数除以这些条目,并选择产生最小值的行。

  4. 重复步骤 2 和 3,直到目标函数中的所有条目都为非正值。

一旦完成,我们将得到以下新的表格。

图片由作者提供

从中,我们得出点

图片由作者提供

我们已成功将所有辅助变量调整为零。我们还将它们移到非基本变量中。此外,我们会注意到这些值确实满足了我们最初的线性约束。因此,我们不再需要辅助变量,可以将它们从线性方程组中移除。如果我们合并正负变量并移除辅助变量,我们会得到这个新点:

图片来自作者

Relu 修复搜索程序

通过添加约束 x_{+2} = x_4 和 x_{+3} = x_5,我们使得 x_{+2} 和 x_{-2} 都可以是非零的(x_{+3} 和 x_{-3} 同样适用)。如上所示,x_{+2} 和 x_{-2} 都是非零的,且 relu(x_2) 不等于 x_4。要修复 ReLU,没有办法直接约束单纯形使 x_{+2} 或 x_{-2} 为零,我们必须选择一种情况并为该情况创建约束。在这两种情况下选择相当于决定 ReLU 是否处于激活状态。原论文中,作者通过在特定时刻将一侧的值赋给另一侧来处理 ReLU 违规情况。我们认为这种方法对问题的约束过于严苛,因为将 ReLU 限定为一个特定值并需要检查可能无数的配置。我们的解决方案则将 ReLU 限制为激活或不激活。因此,我们只需检查这些情况以涵盖所有允许的配置。

由于我们需要决定 ReLU 约束是否处于激活或非激活状态,并且需要设置 2 的 n 次方个有效约束,在大型网络中手动检查所有可能的配置变得不切实际。

Reluplex 的作者建议通过尝试逐一解决每个 ReLU 问题来应对违反情况。他们迭代地添加一个约束以解决一个特定的违规问题,更新表格以适应新约束,移除约束,然后对所有其他或新的违规情况重复这一过程。由于每次只存在一个约束,因此更新一个 ReLU 修复可能会破坏已经修复的另一个地方的 ReLU。这可能导致重复修复相同的 ReLU 约束的循环。为了绕过这一点,如果一个 ReLU 节点被更新了五次,则会执行“ReLU 拆分”。该拆分将问题分成两个情况:在一个情况中,他们强制变量的负侧为零,而在另一个情况下,正侧被设置为零。重要的是,无论哪种情况,约束都不会被移除,确保该 ReLU 不会再需要修复。这允许算法仅在特别“问题” ReLU 节点上进行拆分,实证证据显示,通常只有大约 10% 的 ReLU 需要拆分。因此,尽管某些修复操作可能会重复,但总体过程仍然比简单的暴力检查每一种可能性更快。

添加约束以修复 ReLU

为了处理 ReLU 违规,我们需要将 x_{+2} 或 x_{-2} 约束为零。为了保持系统化的方法,我们将始终尝试首先将正变量设置为零。如果这证明不可行,我们将把负侧设置为零。

引入一个新约束涉及添加一个新的辅助变量。如果我们添加这个变量并施加约束使得 x_{+2}=0,表格将被转化如下:

作者提供的图片

x_{+2} 作为基本变量,出现在两个不同的行中。这与单纯形方法正常运行所需的假设之一相矛盾。因此,有必要快速执行 (x_{+2}, x_{+2}) 的枢轴操作以解决此问题。执行此枢轴操作会产生如下结果:

作者提供的图片

解决对偶问题

在前一步中,我们应用了两阶段法来解决最后的表格。这种方法涉及引入人工变量,直到建立一个保证的平凡初始点。随后,它从一个可行点枢轴到另一个点,直到获得最优解。然而,我们将采用对偶单纯形法,而不是继续对这个表格使用两阶段法。

双重单纯形法的起始步骤是识别一个超越给定约束的最优位置点,通常称为超级最优点。然后,它从一个超级最优点移动到另一个,直到达到一个可行点。一旦达到可行点,即可确保它是全局最优点。这种方法适合我们当前的情境,因为它允许我们在已经解决的表格中添加一个约束,而无需解决一个原始问题。鉴于我们缺乏固有的目标函数,我们将任意指定一个目标函数如下:

作者提供的图像

解决原始问题通常涉及转置矩阵并用右侧值替换目标函数值。然而,在这种情况下,我们可以直接使用已经设置好的表格进行解决。过程如下:

作者提供的图像

将此过程应用于我们已建立的问题将揭示其不可行性。这与预期一致,因为将 x_{+3} 设为零将需要将 x_{+1}-x_{-1} 或 x_1 设为 0.4 或更低,低于我们最初对 x_1 施加的 0.5 的下限约束。

设置 x_{-1}=0

由于设置 x_{+1}=0 失败,我们继续尝试设置 x_{-1}=0。实际上,这将成功并得到以下完成的表格:

作者提供的图像

从中,我们得到新的解点:

作者提供的图像

如果我们将其合并,变成:

作者提供的图像

现在我们已成功使 x_4 = relu(x_2),我们必须在继续之前移除临时约束。

移除临时 ReLU 约束

为了提高约束,需要一个主元操作来明确在单行中陈述约束。这可以通过以新的 a_1 列作为主元来实现。按照布兰德规则选择主元行,我们识别出我们列中的所有正值,将目标函数值除以这些值,并选择最小值的行。在这种情况下,x_{-2} 行成为最佳选择,因为 0/1=0 小于所有其他候选值。执行主元操作后的结果表格如下:

作者提供的图像

需要注意的是,没有任何变量值被改变。我们现在可以自信地从表格中删除 a_1 行和 a_1 列。这一操作有效地移除了约束 x_{-2}=0, resulting in the following updated tableau:

作者提供的图像

继续进行 Relu 分裂

显而易见,我们成功解决了 ReLU 违例,实现了 x_4 = ReLU(x_2)的预期结果。然而,新的违例出现了,因为 x_5 不等于 ReLU(x_3)。为了解决这个新违例,我们遵循与修复 x_4 不等于 ReLU(x_2)违例时完全相同的程序。不过,一旦完成,我们发现表格恢复到修复 x_4 = ReLU(x_2)之前的状态。如果继续下去,我们将不断循环修复 x_4 = ReLU(x_2)和 x_5 = ReLU(x_3),直到其中一个被更新足够以触发 ReLU 拆分。这个拆分创建了两个表格:一个是 x_{+2}=0(已显示为不可行),另一个是 x_{-2}=0,结果是我们之前遇到过的表格。

作者提供的图像

不过这次,我们通常会将问题分为两种情况,一种是 x_{-2}=0,另一种是 x_{+2}=0。然而,我们已经确定 x_{+2}=0 是不可能的,因此我们终止了那个问题。现在我们只需要确定 x_{-2}=0 是否不可行,如果是,我们就完成了。我们现在继续处理 x_{-2}=0,永久编码到表格中。接下来我们尝试修复 x_{+3}=0 的约束。一旦我们用完全相同的方法进行处理,我们将成功并得到以下值:

作者提供的图像

将这些合并后变成:

作者提供的图像

这个结果没有 ReLU 违例,并且代表了神经网络在最初声明的范围内可以生成的有效点。至此,我们的搜索结束,我们可以正式声明神经网络在输入范围 0.5 到 1 之间生成输出范围 0.5 到 2 是可能的。

可能的优化

原始论文建议了其他优化方法,如约束收紧、推导约束、冲突分析和浮点运算。其中最有前景的是约束收紧,其中进行一些简单的检查以确定一个约束是否必然意味着另一个约束必须在用户设置的范围内更小。在我们的例子中,一些简单的代数计算可以迅速表明,为了使输出至少为 0.5,输入必须至少为 0.9,因此我们可以在开始任何搜索之前改变这个约束。然而,这些优化没有在我们的解决方案中实现,因此我们将其留给读者阅读原始论文以了解这些及其他优化的详细信息。

代码实现

对于表现最佳且适用于生产的算法版本,我们建议探索官方的 Marabou GitHub 页面[5],因为这是该问题领域的当前最前沿。此外,你可以深入了解官方 Reluplex 代码库以获得对算法的更深入理解[2]。我还用 Python 编写了一个简化版的 ReLuPlex 实现[3]。这个实现对于理解算法并让用户逐行执行它非常有价值。它可以作为开发定制的更高级 Python 版本算法的基础。

参考文献

[1][1702.01135] Reluplex:用于验证深度神经网络的高效 SMT 求解器 (arxiv.org)

[2][1803.08375] 使用修正线性单元(ReLU)的深度学习

[2] ReluPlex 的官方实现 (github.com)

[3] 作者的 ReluPlex Python 实现 (github.com)

[4][1910.14574] 基于抽象的神经网络验证框架 (arxiv.org)

[5] Marabou 的官方实现 (github.com)

[6][2210.13915]迈向正式 XAI:神经网络的形式近似最小解释

[7] Science Direct 上的神经网络

[8]数字图像去噪的反向传播算法综合概述

在 Keras 和 TensorFlow 中实现 Siamese 网络

原文:towardsdatascience.com/implementation-of-a-siamese-network-in-keras-and-tensorflow-aa327418e177

照片由 Markus Spiske 拍摄,来源于 Unsplash

学习对象检测背后的技术(以及更多)和示例代码。

Rashida Nasrin SuckyTowards Data Science Rashida Nasrin Sucky

·发布在 Towards Data Science ·阅读时间 7 分钟·2023 年 8 月 14 日

--

神经网络在 AI/ML 领域非常强大且流行,但它们训练所需的数据量过多。对于对象检测、签名验证、语音验证和药品识别等任务,常规神经网络技术由于数据需求过大,会变得更加耗时和昂贵。在这些类型的工作中,Siamese 网络可以非常强大,因为它比常规神经网络所需的数据少得多。此外,不平衡的数据集也可以表现良好。

本教程将为你提供 Siamese 网络的高级概述,并提供一个完整的示例。我在这里使用了 fashion-mnist 数据集,但这种相似的结构适用于许多其他用例。

什么是 Siamese 网络?

Siamese 网络包含一个或多个相同的网络,这些相同的网络具有相同的参数和权重。如果一个网络的权重更新,另一个网络的权重也会更新。它们必须是相同的。最终层通常是一个嵌入层,计算输出之间的距离。

你给它们一对输入。每个网络将计算输入的特征,并使用两张图像之间的距离来找出输入之间的相似性。因此,只有两种类别。要么图像相似,要么不相似。

当你实际操作一个例子时,概念会变得更加清晰。通过实践学习始终是最好的方法。

必要的导入和函数定义

让我们从必要的导入开始。如果有必要,我们将导入更多内容。

import os
import tensorflow.keras.backend as K
import matplotlib.pyplot as plt
import numpy as np
import tensorflow as tf
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input
from tensorflow.keras.layers import Conv2D
from tensorflow.keras.layers import Dense
from tensorflow.keras.layers import Dropout
from tensorflow.keras.layers import GlobalAveragePooling2D
from tensorflow.keras.layers import MaxPooling2D

正如我们在前一节中讨论的,Siamese 网络一次接受一对输入,输出为‘yes’或‘no’。如果图像相似,则为‘yes’,否则为‘no’。或者,Siamese 网络还可以输出两张图像之间的距离,这在本教程中我们将进行。因此,我们需要以这种方式准备数据集。我们的数据集需要是图像对,而不是单张图像。对于正类,将会有两张相同类型的图像;而对于负类,将会有两张不同类型的图像。

下一个代码块定义了一个函数‘create_pairs’,该函数将生成图像对,这意味着将两张图像叠加在一起,其中有时两张图像是相同类型的,有时则是不同类型的。当两张图像匹配或是相同类型时,标签将为 1;而当图像不匹配时,标签将为 0。

 def create_pairs(images, labels):
  imagePairs = []
  labelPairs = []

  #Getting the indices of each class
  numclasses = len(np.unique(labels))
  idx = [np.where(labels ==i)[0] for i in range(numclasses)]

  for ind in range(len(images)):
    #Getting current image with index
    currImage = images[ind]
    #getting the label of the image from labels.
    label = labels[ind]

    #Randomly choosing another labels from the same class
    indB = np.random.choice(idx[label])
    #corresponding image for this randomly selected label
    indImage = images[indB]

    imagePairs.append([currImage, indImage])

    labelPairs.append([1])

    #Getting a label where label is different than the current image
    diss_idx = np.where(labels != label)[0]

    #finding an image for this label
    diss_image = images[np.random.choice(diss_idx)]

    imagePairs.append([currImage, diss_image])
    labelPairs.append([0])

  return (np.array(imagePairs), np.array(labelPairs))

下一个函数计算两张图像之间的欧几里得距离,并遵循传统的欧几里得距离公式:

def euclidean_distance(vecs):
  (imgA, imgB) = vecs
  ss = K.sum(K.square(imgA - imgB), axis = 1, keepdims=True)
  return K.sqrt(K.maximum(ss, K.epsilon()))

模型与成本

Siamese 模型与其他 TensorFlow 模型非常相似。我们将使用两组卷积-最大池化-丢弃层、全局平均池化,最后一层为 Dense。最后,它将返回模型。

 def siamese_model(input_shape, embeddingDim = 48):
  inputs = Input(input_shape)
  x = Conv2D(128, (2, 2), padding = "same", activation = "relu")(inputs)
  x = MaxPooling2D(pool_size=(2, 2))(x)
  x = Dropout(0.4)(x)

  x = Conv2D(128, (2, 2), padding = "same", activation = "relu")(inputs)
  x = MaxPooling2D(pool_size=(2, 2))(x)
  x = Dropout(0.4)(x)

  pooling = GlobalAveragePooling2D()(x)
  outputs = Dense(embeddingDim)(pooling)
  model = Model(inputs, outputs)

  return model

正常的二元交叉熵损失函数对于我们做二元分类已经足够好。但对于 Siamese 网络,对比损失更为合适。如果你考虑一下,实际上 Siamese 网络的目标不仅仅是对相似或不相似的图像进行分类,还要区分它们。我们希望知道 Siamese 网络在区分相似或不相似图像方面做得有多好。

这是对比损失的公式:

图片来源:作者

这里,Y 是真实标签(0 或 1)

D 是欧几里得距离

边际通常为 1

让我们创建一个函数 contrastiveLoss:

def contrastiveLoss(y, y_preds, margin=1):
 y = tf.cast(y, y_preds.dtype)
 y_preds_squared = K.square(y_preds)
 margin_squared = K.square(K.maximum(margin - y_preds, 0))
 loss = K.mean(y * y_preds_squared + (1 - y) * margin_squared)
 return loss

这些是可以用于任何其他同类型 Siamese 网络的常见函数。

模型训练

我们确实需要数据集来训练模型。在本教程中,我将使用公共数据集(MIT 许可证),fashion_mnist 数据集,该数据集可以通过 TensorFlow 库加载。

from tensorflow.keras.layers import Lambda
from tensorflow.keras.datasets import fashion_mnist
(x_train, y_train), (x_test, y_test) = fashion_mnist.load_data()

一些简单的数据处理任务是必要的,首先要做的是。为了缩放图像数据,我们将图像数据除以 255\。另外,还需要为训练和测试图像添加另一个维度,以使其成为三维的。

x_train = x_train/255.0
x_test = x_test/255.0

x_train = np.expand_dims(x_train, axis = -1)
x_test = np.expand_dims(x_test, axis=-1)

(training_pairs, training_labels) = create_pairs(x_train, y_train)
(test_pairs, test_labels) = create_pairs(x_test, y_test)

接下来,我们将为图像对中的两张图像创建两个输入,并将它们传递到之前构建的 Siamese 模型中,以提取这两张图像的特征。

欧几里得距离函数在这里将非常有用,用于找到两个提取特征之间的距离。两个特征图像之间的距离越小,它们就越相似。

最终,模型将两张图像作为输入,并输出距离。

image_shape = (28, 28, 1)
# specify the batch size and number of epochs
batch_size = 64
epochs = 70

imageA = Input(shape = image_shape)
imageB = Input(shape = image_shape)

model_build = siamese_model(image_shape)
modelA = model_build(imageA)
modelB = model_build(imageB)

distance = Lambda(euclidean_distance)([modelA, modelB])
model = Model(inputs=[imageA, imageB], outputs=distance)

现在编译模型以训练我们之前定义的对比损失的孪生模型。必要的参数包括图像对、标签对、批处理大小和训练轮数。

model.compile(loss = contrastiveLoss, optimizer="adam")
history = model.fit(
    [training_pairs[:, 0], training_pairs[:, 1]], training_labels[:],
    validation_data=([test_pairs[:, 0], test_pairs[:, 1]], test_labels[:]),
    batch_size = batch_size,
    epochs = epochs

输出:

Epoch 1/70
1875/1875 [==============================] - 24s 7ms/step - loss: 0.1808 - val_loss: 0.1618
Epoch 2/70
1875/1875 [==============================] - 15s 8ms/step - loss: 0.1615 - val_loss: 0.1572
Epoch 3/70
1875/1875 [==============================] - 14s 7ms/step - loss: 0.1588 - val_loss: 0.1551
Epoch 4/70
1875/1875 [==============================] - 14s 8ms/step - loss: 0.1566 - val_loss: 0.1529
Epoch 5/70
1875/1875 [==============================] - 14s 7ms/step - loss: 0.1552 - val_loss: 0.1520
.
.
.
.
Epoch 67/70
1875/1875 [==============================] - 14s 7ms/step - loss: 0.1486 - val_loss: 0.1447
Epoch 68/70
1875/1875 [==============================] - 14s 7ms/step - loss: 0.1487 - val_loss: 0.1443
Epoch 69/70
1875/1875 [==============================] - 14s 7ms/step - loss: 0.1484 - val_loss: 0.1447
Epoch 70/70
1875/1875 [==============================] - 14s 7ms/step - loss: 0.1490 - val_loss: 0.1446

让我们绘制一些图像对及其距离。我们将随机选择 4 对图像。可以使用OpenCV 库来实现。首先,它需要一些基本的图像处理,如缩放,然后将额外的维度添加到图像的两个维度中。接下来,我们将使用模型预测每对图像之间的距离。最后,你可以绘制这些图像对以查看距离和图像对。

import cv2
pairs = np.random.choice(len(test_pairs), size=4)

for i in pairs:
  imageA = test_pairs[i][0]
  imageB = test_pairs[i][1]

  baseA = imageA.copy()
  baseB = imageB.copy()

  imageA = np.expand_dims(imageA, axis=-1)
  imageB = np.expand_dims(imageB, axis=-1)

  imageA = np.expand_dims(imageA, axis=0)
  imageB = np.expand_dims(imageB, axis =0)

  imageA = imageA/255.0
  imageB = imageB / 255.0

  predicts = model.predict([imageA, imageB])

  proba = predicts[0][0]

  fig = plt.figure("Pair #{}".format(i+1), figsize=(4,2))
  plt.suptitle("Distance: {}:.2f".format(proba))

  ax = fig.add_subplot(1, 2, 1)
  plt.imshow(baseA, cmap=plt.cm.gray)
  plt.axis("off")

  ax = fig.add_subplot(1, 2, 2)
  plt.imshow(baseB, cmap=plt.cm.gray)
  plt.axis("off")

  plt.show()

看看这些图片及其相应的距离。正如你所见,预测函数在这种情况下并未给出标签 0 或 1。它给出的是图像对之间的距离。当图像对中的图像更相似时,距离会更小。

如果需要,你可以根据使用案例设置一个阈值距离,以区分相似和不相似的图像,从而获得标签。

结论

正如我在介绍中提到的,这种模型和技术可以用于许多不同类型的任务。因为它可以处理较少的数据,所以数据收集部分变得更容易。希望它对你有用。

欢迎在 Twitter 上关注我,并点赞我的 Facebook 页面。

进一步阅读

在 TensorFlow 中使用 Sequential 和 Function API 进行回归 | 作者:Rashida Nasrin Sucky | 数据科学导向 (medium.com)

使用 Keras Tuner 对 TensorFlow 模型进行超参数调整 | 作者:Rashida Nasrin Sucky | 2023 年 7 月 | 数据科学导向 (medium.com)

如何使用 OpenCV 进行阈值图像分割 | 作者:Rashida Nasrin Sucky | 数据科学导向 (medium.com)

Python 初学者的一些基本图像预处理操作 | 作者:Rashida Nasrin Sucky | 数据科学导向 (medium.com)

如何在 TensorFlow 中定义自定义层、激活函数和损失函数 | 作者:Rashida Nasrin Sucky | 数据科学导向 (medium.com)

逐步教程:在 TensorFlow 中开发多输出模型 | 作者:Rashida Nasrin Sucky | 数据科学导向 (medium.com)

使用 LangChain 实现销售与支持代理

原文:towardsdatascience.com/implementing-a-sales-support-agent-with-langchain-63c4761193e7?source=collection_archive---------0-----------------------#2023-04-19

了解如何开发一个能够基于公司文档中提供的信息回答问题的聊天机器人

Tomaz BratanicTowards Data Science Tomaz Bratanic

·

关注 发表在 数据科学前沿 ·10 分钟阅读·2023 年 4 月 19 日

--

最近,我对 ChatGPT 的强大功能以及其构建各种类型聊天机器人的能力产生了浓厚的兴趣。我尝试并撰写了关于多种方法的文章,这些方法旨在实现一个能够访问外部信息以改善回答的聊天机器人。在我的聊天机器人编码过程中,我加入了一些 Discord 频道,希望能获得一些帮助,因为这些库相对较新,文档还不多。令我惊讶的是,我发现了一些定制的机器人,它们能够回答大多数有关这些库的问题。

Discord 支持机器人示例。图片由作者提供。

这个想法是赋予聊天机器人能够深入挖掘各种资源,如公司文档、代码或其他内容,从而允许它回答公司支持问题。由于我已经有一些聊天机器人经验,我决定测试实现一个自定义机器人,访问公司的资源有多困难。

在这篇博客文章中,我将带你了解如何使用 OpenAI 的模型在LangChain 库中实现一个销售和支持代理,该代理可以回答有关应用程序的信息,并且可以与图形数据库 Neo4j配合使用。该代理还可以帮助你调试或生成任何你遇到困难的 Cypher 语句。这样的代理可以部署到 Discord 或其他平台上服务用户。

代理设计。图片由作者提供。

我们将使用LangChain 库来实现支持机器人。该库易于使用,并提供了 LLM 提示和 Python 代码的出色集成,使我们能够在几分钟内开发聊天机器人。此外,该库支持各种 LLM、文本嵌入模型和向量数据库,并且具有帮助我们加载和嵌入常见文件类型(如文本、PowerPoint、图片、HTML、PDF 等)的实用函数。

本博客文章的代码可在 GitHub 上获得。

[## blogs/neo4j_support_bot.ipynb at master · tomasonjo/blogs

你现在不能执行该操作。你在另一个标签页或窗口中登录。你在另一个标签页中注销了…

github.com

LangChain 文档加载器

首先,我们必须预处理公司的资源并将其存储在向量数据库中。幸运的是,LangChain 可以帮助我们加载外部数据、计算文本嵌入,并将文档存储在我们选择的向量数据库中。

首先,我们需要将文本加载到文档中。LangChain 提供了各种助手函数,可以处理不同格式和类型的数据,并生成文档输出。这些助手函数被称为文档加载器。

Neo4j 在 GitHub 仓库中有大量文档。方便的是,LangChain 提供了一个文档加载器,可以将仓库 URL 作为输入,为仓库中的每个文件生成文档。此外,我们还可以使用过滤函数在加载过程中忽略文件(如果需要的话)。

我们将从 Neo4j 知识库仓库 加载 AsciiDoc 文件开始。

# Knowledge base
kb_loader = GitLoader(
    clone_url="https://github.com/neo4j-documentation/knowledge-base",
    repo_path="./repos/kb/",
    branch="master",
    file_filter=lambda file_path: file_path.endswith(".adoc")
    and "articles" in file_path,
)
kb_data = kb_loader.load()
print(len(kb_data)) # 309

这不是很简单吗?GitLoader 函数克隆了仓库并将相关文件加载为文档。在这个例子中,我们指定了文件必须以 .adoc 后缀结尾,并且是 articles 文件夹的一部分。总共加载了 309 篇文章。我们还必须注意文档的大小。例如,GPT-3.5-turbo 的 token 限制为 4000,而 GPT-4 在单个请求中允许 8000 个 tokens。虽然单词数与 tokens 数量并不完全相同,但仍然是一个很好的估算。

接下来,我们将加载 Graph Data Science 仓库 的文档。在这里,我们将使用文本拆分器来确保没有文档超过 2000 个单词。再次说明,单词数量与 tokens 数量并不相等,但这是一个很好的近似值。定义 token 的阈值数量可以显著影响数据库的查找和检索。我找到了一篇 Pinecone 的优秀文章,可以帮助你理解各种块拆分策略的基础

# Define text chunk strategy
splitter = CharacterTextSplitter(
  chunk_size=2000, 
  chunk_overlap=50,
  separator=" "
)
# GDS guides
gds_loader = GitLoader(
    clone_url="https://github.com/neo4j/graph-data-science",
    repo_path="./repos/gds/",
    branch="master",
    file_filter=lambda file_path: file_path.endswith(".adoc") 
    and "pages" in file_path,
)
gds_data = gds_loader.load()
# Split documents into chunks
gds_data_split = splitter.split_documents(gds_data)
print(len(gds_data_split)) #771

我们可以加载其他包含文档的 Neo4j 仓库。然而,目的是展示各种数据加载方法,而不是探索所有包含文档的 Neo4j 仓库。因此,我们将继续并查看如何从 Pandas Dataframe 中加载文档。

例如,假设我们想将 YouTube 视频加载为我们的聊天机器人的文档来源。Neo4j 有自己的 YouTube 频道,甚至我也出现在一两个视频中。两年前,我展示了如何实现信息提取管道。

使用 LangChain,我们可以利用视频的字幕,并通过仅仅三行代码将其作为文档加载。

yt_loader = YoutubeLoader("1sRgsEKlUr0")
yt_data = yt_loader.load()
yt_data_split = splitter.split_documents(yt_data)
print(len(yt_data_split)) #10

这已经没有比这更简单的了。接下来,我们将查看如何从 Pandas dataframe 中加载文档。一个月前,我从 Neo4j medium 发布的文章中获取了信息用于另一个博客帖子。由于我们希望将关于 Neo4j 的外部信息带入机器人,我们也可以使用 medium 文章的内容。

article_url = "https://raw.githubusercontent.com/tomasonjo/blog-datasets/main/medium/neo4j_articles.csv"
medium = pd.read_csv(article_url, sep=";")
medium["source"] = medium["url"]
medium_loader = DataFrameLoader(
    medium[["text", "source"]], 
    page_content_column="text")
medium_data = medium_loader.load()
medium_data_split = splitter.split_documents(medium_data)
print(len(medium_data_split)) #4254

在这里,我们使用 Pandas 从 GitHub 加载了一个 CSV 文件,重命名了一列,并使用 DataFrameLoader 函数将文章作为文档加载。由于 medium 帖子可能超过 4000 个 tokens,我们使用文本拆分器将文章拆分成多个块。

我们将使用的最后一个来源是 Stack Overflow API。Stack Overflow 是一个网络平台,用户帮助他人解决编码问题。他们的 API 不需要任何授权。因此,我们可以使用 API 来检索带有 Neo4j 标签的接受答案的问题。

so_data = []
for i in range(1, 20):
    # Define the Stack Overflow API endpoint and parameters
    api_url = "https://api.stackexchange.com/2.3/questions"
    params = {
        "order": "desc",
        "sort": "creation",
        "filter": "!-MBrU_IzpJ5H-AG6Bbzy.X-BYQe(2v-.J",
        "tagged": "neo4j",
        "site": "stackoverflow",
        "pagesize": 100,
        "page": i,
    }
    # Send GET request to Stack Overflow API
    response = requests.get(api_url, params=params)
    data = response.json()
    # Retrieve the resolved questions
    resolved_questions = [
        question
        for question in data["items"]
        if question["is_answered"] and question.get("accepted_answer_id")
    ]

    # Print the resolved questions
    for question in resolved_questions:
        text = (
            "Title:",
            question["title"] + "\n" + "Question:",
            BeautifulSoup(question["body"]).get_text()
            + "\n"
            + BeautifulSoup(
                [x["body"] for x in question["answers"] if x["is_accepted"]][0]
            ).get_text(),
        )
        source = question["link"]
        so_data.append(Document(page_content=str(text), metadata={"source": source}))
print(len(so_data)) #777

每个批准的答案和原始问题都用于构建一个单一的文档。由于大多数 Stack Overflow 问题和答案不超过 4000 个 tokens,我们跳过了文本拆分步骤。

现在我们已经将文档资源加载为文档,可以进入下一步。

将文档存储在向量数据库中

聊天机器人通过将问题的向量嵌入与文档嵌入进行比较来找到相关信息。文本嵌入是机器可读的文本表示形式,通常是一个向量或更简单地说,是一个浮点数列表。在这个例子中,我们将使用 OpenAI 提供的ada-002模型来嵌入文档。

向量数据库的核心理念是存储向量并提供快速的相似性搜索。向量通常使用余弦相似性进行比较。LangChain 包括与各种向量数据库的集成。为了简单起见,我们将使用Chroma 向量数据库,它可以用作本地内存数据库。对于更严肃的聊天机器人应用程序,我们希望使用一个持久的数据库,以避免在脚本或笔记本关闭后丢失数据。

我们将创建两个文档集合。第一个将更多关注销售和营销,包含来自 Medium 和 YouTube 的文档。第二个集合更多关注支持用例,包括文档和 Stack Overflow 文档。

# Define embedding model
OPENAI_API_KEY = "OPENAI_API_KEY"
embeddings = OpenAIEmbeddings(openai_api_key=OPENAI_API_KEY)

sales_data = medium_data_split + yt_data_split
sales_store = Chroma.from_documents(
    sales_data, embeddings, collection_name="sales"
)

support_data = kb_data + gds_data_split + so_data
support_store = Chroma.from_documents(
    support_data, embeddings, collection_name="support"
)

这个脚本通过 OpenAI 的文本嵌入 API 处理每个文档,并将生成的嵌入以及文本插入 Chroma 数据库。文本嵌入的费用为 0.80$,这是一个合理的价格。

使用外部上下文进行问答

最后要做的是实现两个独立的问答流程。第一个将处理销售和营销请求,另一个将处理支持请求。LangChain 库使用 LLMs 进行推理并向用户提供答案。因此,我们首先定义 LLM。在这里,我们将使用来自 OpenAI 的GPT-3.5-turbo模型。

llm = ChatOpenAI(
    model_name="gpt-3.5-turbo",
    temperature=0,
    openai_api_key=OPENAI_API_KEY,
    max_tokens=512,
)

实现问答流程在 LangChain 中非常简单。我们只需提供要使用的 LLM 以及用于获取相关文档的检索器。此外,我们还可以自定义用于回答问题的 LLM 提示。

sales_template = """As a Neo4j marketing bot, your goal is to provide accurate 
and helpful information about Neo4j, a powerful graph database used for 
building various applications. You should answer user inquiries based on the 
context provided and avoid making up answers. If you don't know the answer, 
simply state that you don't know. Remember to provide relevant information 
about Neo4j's features, benefits, and use cases to assist the user in 
understanding its value for application development.

{context}

Question: {question}"""
SALES_PROMPT = PromptTemplate(
    template=sales_template, input_variables=["context", "question"]
)
sales_qa = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=sales_store.as_retriever(),
    chain_type_kwargs={"prompt": SALES_PROMPT},
)

销售提示中最重要的部分是禁止 LLM 在没有依赖官方公司资源的情况下进行回答。记住,LLMs 在提供无效信息时可能表现得非常自信。然而,我们希望避免这种情况,并避免出现机器人承诺或销售不存在的功能的问题。我们可以通过询问以下问题来测试销售问答流程:

销售问答。作者图片。

对于问题的回答似乎是相关且准确的。记住,构建此回答的信息来自 Medium 文章。

接下来,我们将实现支持问答流程。在这里,我们将允许 LLM 模型利用其对 Cypher 和 Neo4j 的知识来帮助解决用户的问题,前提是上下文信息不足。

support_template = """
As a Neo4j Customer Support bot, you are here to assist with any issues 
a user might be facing with their graph database implementation and Cypher statements.
Please provide as much detail as possible about the problem, how to solve it, and steps a user should take to fix it.
If the provided context doesn't provide enough information, you are allowed to use your knowledge and experience to offer you the best possible assistance.

{context}

Question: {question}"""

SUPPORT_PROMPT = PromptTemplate(
    template=support_template, input_variables=["context", "question"]
)

support_qa = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=support_store.as_retriever(),
    chain_type_kwargs={"prompt": SUPPORT_PROMPT},
)

再次,我们可以测试支持问答能力。我从 Neo4j 的 Discord 服务器上随机挑了一个问题。

支持问答。图片由作者提供。

响应非常切题。请记住,我们检索了图数据科学文档,并将其作为上下文来形成聊天机器人问题。

代理实现

我们现在有两个独立的指令和存储,用于销售支持回应。如果我们必须让人来区分这两者,那么聊天机器人的全部意义就会丧失。幸运的是,我们可以使用 LangChain 代理根据用户输入决定使用哪个工具。首先,我们需要定义代理的可用工具以及使用它们的时机和方式。

tools = [
    Tool(
        name="sales",
        func=sales_qa.run,
        description="""useful for when a user is interested in various Neo4j information, 
                       use-cases, or applications. A user is not asking for any debugging, but is only
                       interested in general advice for integrating and using Neo4j.
                       Input should be a fully formed question.""",
    ),
    Tool(
        name="support",
        func=support_qa.run,
        description="""useful for when when a user asks to optimize or debug a Cypher statement or needs
                       specific instructions how to accomplish a specified task. 
                       Input should be a fully formed question.""",
    ),
]

工具的描述用于帮助代理识别何时以及如何使用工具。例如,支持工具应当用于优化或调试 Cypher 语句,而工具的输入应为一个完整的问题。

我们需要做的最后一件事是初始化代理。

agent = initialize_agent(
    tools, 
    llm, 
    agent="zero-shot-react-description", 
    verbose=True
)

现在我们可以继续测试代理在几个问题上的表现。

销售代理示例。图片由作者提供。

支持代理示例。图片由作者提供。

请记住,除了上下文来源之外,这两种问答的主要区别在于,我们允许支持问答形成在提供的上下文中找不到的答案。另一方面,我们禁止销售问答这样做,以避免任何过度承诺的陈述。

总结

在 LLM 时代,得益于 LangChain 库,您可以在一天内开发一个利用公司资源回答问题的聊天机器人,因为它提供了各种文档加载器以及与流行 LLM 模型的集成。因此,您只需收集公司的资源,将其导入到向量数据库中即可开始使用。请注意,实施并非确定性的,这意味着在相同提示下您可能会获得略微不同的结果。GPT-4 模型在提供更准确和一致的回应方面要好得多。

如果您对聊天机器人实现有任何想法或反馈,请告诉我。代码总是可以在GitHub上找到。

使用 JAX 和 Haiku 从头实现 Transformer 编码器 🤖

原文:towardsdatascience.com/implementing-a-transformer-encoder-from-scratch-with-jax-and-haiku-791d31b4f0dd?source=collection_archive---------7-----------------------#2023-11-07

理解 Transformers 的基本构建模块。

Ryan PégoudTowards Data Science Ryan Pégoud

·

关注 发表在 Towards Data Science ·12 分钟阅读·2023 年 11 月 7 日

--

Transformers,以爱德华·霍珀(由 Dall.E 3 生成)的风格

在 2017 年的开创性论文“注意力机制就是你需要的[0]中介绍的 Transformer 架构可以说是近期深度学习历史上最具影响力的突破之一,它使大型语言模型的兴起成为可能,并且在计算机视觉等领域也找到了应用。

继承了依赖于递归的前沿架构,如长短期记忆(LSTM)网络或门控循环单元(GRU),Transformer引入了自注意力的概念,并结合了编码器/解码器架构。

在本文中,我们将从零开始一步一步实现 Transformer 的前半部分,即编码器。我们将使用JAX作为主要框架,并结合Haiku,这是 DeepMind 的深度学习库之一。

如果你对 JAX 不熟悉或需要对其惊人功能有一个新的提醒,我在我的上一篇文章中已经涵盖了这个话题:

使用 JAX 向量化和并行化强化学习环境:光速 Q-learning⚡

学习如何向量化一个 GridWorld 环境,并在 CPU 上并行训练 30 个 Q-learning 代理,步数达到 180 万……

towardsdatascience.com

我们将逐一讲解构成编码器的每个模块,并学习如何高效地实现它们。特别是,本文的纲要包括:

  • 嵌入层位置编码

  • 多头注意力

  • 残差连接层归一化

  • 位置-wise 前馈网络

免责声明:本文并非这些概念的完整介绍,我们将首先关注实现部分。如有需要,请参阅本文末尾的资源。

一如既往,本文的完整注释代码以及插图笔记本可在 GitHub上获得,如果你喜欢这篇文章,欢迎给仓库加星!

## GitHub — RPegoud/jab: 一系列在 JAX 中实现的基础深度学习模型

在 JAX 中实现的一系列基础深度学习模型 — GitHub — RPegoud/jab: 一系列…

github.com

主要参数

在我们开始之前,我们需要定义几个在编码器模块中发挥重要作用的参数:

  • 序列长度 (seq_len): 序列中的标记或词的数量。

  • 嵌入维度 (embed_dim): 嵌入的维度,换句话说,就是用来描述单个标记或词的数值数量。

  • 批量大小 (batch_size): 输入批量的大小,即同时处理的序列数量。

我们的编码器模型的输入序列通常是batch_size seq_len的形状。在本文中,我们将使用batch_size=32seq_len=10,这意味着我们的编码器将同时处理 32 个 10 词的序列。

关注每一步处理中的数据形状将使我们更好地可视化和理解数据在编码器块中的流动。以下是我们编码器的高级概述,我们将从底部开始,介绍嵌入层位置编码

Transformer 编码器块的表示(由作者制作)

嵌入层和位置编码

如前所述,我们的模型接受批处理的令牌序列作为输入。生成这些令牌可能像收集数据集中一组唯一词汇并为每个词汇分配一个索引一样简单。然后,我们将采样3210 词序列,并用词汇中的索引替换每个词。这一过程将为我们提供一个形状为batch_size seq_len的数组,正如预期的那样。

我们现在准备开始使用编码器。第一步是为我们的序列创建“位置嵌入”。位置嵌入是词嵌入位置编码

词嵌入

词嵌入使我们能够编码词汇中意义语义关系。在本文中,嵌入维度固定为64。这意味着每个词由一个64 维向量表示,从而具有相似意义的词具有相似的坐标。此外,我们可以操作这些向量来提取词之间的关系,如下所示。

从词嵌入派生的类比示例(图片来自 developers.google.com)

使用 Haiku,生成可学习的嵌入就像调用一样简单:

hk.Embed(vocab_size, embed_dim)

这些嵌入将在模型训练期间与其他可学习的参数一起更新(稍后会详细介绍)。

位置编码

与递归神经网络不同,Transformers 无法根据共享的隐藏状态推断令牌的位置,因为它们缺乏递归卷积结构。因此,引入了位置编码,这些向量传达了令牌在输入序列中的位置

从本质上讲,每个令牌被分配一个由交替的正弦和余弦值组成的位置向量。这些向量的维度与词嵌入相匹配,以便两者可以相加。

特别是,原始的 Transformer 论文使用了以下函数:

位置编码函数(转载自“Attention is all you need”,Vaswani 等,2017)

下面的图使我们能够进一步理解位置编码的功能。让我们来看一下最上面图的第一行,我们可以看到交替的零和一序列。实际上,行表示序列中一个 token 的位置(pos 变量),而列表示嵌入维度(i 变量)。

因此,当 pos=0 时,之前的方程对偶数嵌入维度返回 sin(0)=0,对奇数维度返回 cos(0)=1

此外,我们看到相邻的行具有相似的值,而第一行和最后一行则差异很大。这一特性有助于模型评估序列中单词之间的距离以及它们的顺序

最后,第三个图表示位置编码和嵌入的总和,这就是嵌入块的输出。

单词嵌入和位置编码的表示,seq_len=16 和 embed_dim=64(由作者制作)

使用 Haiku,我们将嵌入层定义如下。与其他深度学习框架类似,Haiku 允许我们定义自定义模块(此处为 hk.Module),以存储可学习的参数定义模型组件的行为

每个 Haiku 模块需要有一个 __init____call__ 函数。在这里,call 函数简单地使用 hk.Embed 函数和位置编码计算嵌入,然后对其进行求和。

位置编码函数使用 JAX 功能,如 vmaplax.cond 来提高性能。如果你对这些函数不熟悉,可以查看我的上一篇文章,那里对这些函数进行了更深入的介绍。

简而言之,vmap 允许我们为单个样本定义一个函数并将其向量化,以便它可以应用于数据批次in_axes 参数用于指定我们要遍历 dim 输入的第一个轴,即嵌入维度。另一方面,lax.cond 是 XLA 兼容版本的 Python if/else 语句。

自注意力和多头注意力

注意力旨在计算序列中每个单词的重要性相对于输入单词。例如,在句子中:

“那只黑猫跳上沙发,躺下并入睡,因为它累了。”

词语“”对于模型来说可能会相当模糊,因为从技术上讲,它可以指代“”或“沙发”。一个经过良好训练的注意力模型能够理解“”指的是“”,并因此为句子的其余部分分配相应的注意力值。

本质上,注意力值可以视为权重,描述了某个单词在给定输入上下文中的重要性。例如,“跳跃”一词的注意力向量会对“”(跳跃了什么?)、“”和“沙发”(跳跃到哪里?)等词具有较高的值,因为这些词与其上下文相关

注意力向量的可视化表示(由作者制作)

在 Transformer 论文中,注意力是使用缩放点积注意力计算的。其公式总结如下:

缩放点积注意力(重现自“Attention is all you need”,Vaswani et al. 2017

在这里,Q、K 和 V 分别代表查询、键。这些矩阵是通过将学习到的权重向量 WQ、WK 和 WV 与位置嵌入相乘得到的。

这些名称主要是抽象概念,用于帮助理解信息在注意力块中的处理和加权方式。它们暗指检索系统的词汇[2](例如,在 YouTube 上搜索视频)。

这里是一个直观的解释:

  • 查询:它们可以被理解为关于序列中所有位置的“一组问题”。例如,询问一个单词的上下文并试图识别序列中最相关的部分。

  • :它们可以被视为包含查询交互的信息,查询与键之间的兼容性决定了查询应该给予对应值多少注意力。

  • :匹配的键和查询使我们能够决定哪些键是相关的,值是与键配对的实际内容。

在下图中,查询是 YouTube 搜索,键是视频描述和元数据,而值是相关联的视频。

查询、键、值概念的直观表示(由作者制作)

在我们的情况下,查询、键和值来自于同一来源(因为它们是从输入序列派生的),因此被称为自注意力

注意力分数的计算通常是多次并行执行的,每次使用部分嵌入。这一机制被称为“多头注意力”,使每个头可以并行地学习数据的几种不同表示,从而形成更强健的模型。

单个注意力头通常处理形状为 (batch_size, seq_len, d_k) 的数组,其中 d_k 可以设置为头数与嵌入维度的比率(d_k = n_heads/embed_dim)。这样,连接每个头的输出就能方便地得到形状为batch_size, seq_len, embed_dim)的数组,作为输入。

注意力矩阵的计算可以分解为几个步骤:

  • 首先,我们定义可学习的权重向量 WQ、WK 和 WV。这些向量的形状为(n_heads, embed_dim, d_k)。

  • 同时,我们将位置嵌入权重向量相乘。我们得到形状为(batch_size, seq_len, d_k)的 Q、K 和 V 矩阵。

  • 然后,我们对 Q 和 K(转置)的点积进行缩放。这个缩放操作包括将点积的结果除以 d_k 的平方根,并在矩阵的行上应用 softmax 函数。因此,对于输入的令牌(即一行),注意力分数总和为一,这有助于防止值变得过大而减慢计算速度。输出的形状为 (batch_size, seq_len, seq_len)。

  • 最后,我们将上一操作的结果与 V 进行点乘,输出的形状为 (batch_size, seq_len, d_k)。

注意块内部的矩阵操作的可视化表示(作者制作)

  • 然后,每个注意力头的输出可以串联起来形成一个形状为(batch_size, seq_len, embed_dim)的矩阵。Transformer 论文还在多头注意力模块的最后添加了一个线性层,用于汇聚组合来自所有注意力头的学习表示。

多头注意力矩阵的串联和线性层(作者制作)

在 Haiku 中,多头注意力模块可以如下实现。__call__函数遵循与上述图表相同的逻辑,而类方法利用 JAX 实用程序,如vmap(在不同注意力头和矩阵上向量化我们的操作)和tree_map(在权重向量上映射矩阵点积)。

残差连接和层归一化

正如你在 Transformer 图中所注意到的,多头注意力块和前馈网络之后跟着残差连接层归一化

残差连接或跳跃连接

残差连接是解决梯度消失问题的标准解决方案,即当梯度变得太小以至于无法有效更新模型参数时。

由于这个问题在特别深的架构中自然而然地出现,所以残差连接被用在各种复杂模型中,比如在计算机视觉中的ResNetKaiming et al,2015 年),在强化学习中的AlphaZeroSilver et al,2017 年),当然还有Transformers

在实践中,残差连接简单地将特定层的输出直接转发到下一层,跳过一个或多个层。例如,围绕多头注意力的残差连接相当于将多头注意力的输出与位置嵌入求和。

这使得梯度在反向传播过程中更有效地流动,通常可以导致更快的收敛和更稳定的训练

Transformer 中残差连接的表示(由作者制作)

层归一化

层归一化有助于确保通过模型传播的值不会“爆炸”(趋向无穷大),这种情况在注意力模块中很容易发生,因为在每次前向传递中会有多个矩阵相乘。

与在批次维度上进行归一化并假设均匀分布的批量归一化不同,层归一化在特征上进行归一化。此方法适用于句子批次,因为每个句子可能由于不同的意义词汇而具有独特的分布

通过在特征上进行归一化,例如嵌入注意力值,层归一化将数据标准化为一致的尺度,而不会混淆不同句子的特征,保持每个句子的独特分布。

Transformer 中层归一化的表示(由作者制作)

层归一化的实现非常简单,我们初始化可学习的参数 alpha 和 beta,并在所需的特征轴上进行归一化。

位置-wise 前馈网络

编码器中我们需要覆盖的最后一个组件是位置-wise 前馈网络。这个全连接网络以注意力块的归一化输出作为输入,用于引入非线性并增加模型的容量以学习复杂的函数。

它由两个密集层组成,中间隔着一个 gelu 激活函数

在这个模块之后,我们有另一个残差连接和层归一化,以完成编码器。

总结

就这样!到现在你应该对 Transformer 编码器的主要概念很熟悉了。这是完整的编码器类,请注意,在 Haiku 中,我们为每一层分配了一个名称,以便学习参数分开且易于访问。__call__函数很好地总结了我们编码器的不同步骤:

要在实际数据上使用此模块,我们必须将 hk.transform 应用于封装编码器类的函数。确实,你可能会记得 JAX 采用了函数式编程范式,因此,Haiku 遵循相同的原则。

我们定义了一个包含编码器类实例的函数,并返回前向传递的输出。应用 hk.transform 返回一个转换对象,具有两个函数:initapply

前者使我们能够用一个随机键和一些虚拟数据初始化模块(请注意这里我们传递的是一个形状为batch_size, seq_len的零数组),而后者则允许我们处理真实数据。

# Note: the two following syntaxes are equivalent
# 1: Using transform as a class decorator
@hk.transform
def encoder(x):
  ...
  return model(x) 

encoder.init(...)
encoder.apply(...)

# 2: Applying transfom separately
def encoder(x):
  ...
  return model(x)

encoder_fn = hk.transform(encoder)
encoder_fn.init(...)
encoder_fn.apply(...)

在下一篇文章中,我们将完成 Transformer 架构,通过添加一个 解码器,它重用了我们迄今为止介绍的大部分模块,并学习如何使用 Optax 训练模型 以完成特定任务!

结论

感谢你读到这里,如果你对代码感兴趣,可以在 GitHub 上找到完整的注释以及额外的细节和使用玩具数据集的示例。

[## GitHub - RPegoud/jab: 一组用 JAX 实现的基础深度学习模型

一组用 JAX 实现的基础深度学习模型 - GitHub - RPegoud/jab: 一组…

github.com](https://github.com/RPegoud/jab?source=post_page-----791d31b4f0dd--------------------------------)

如果你想更深入了解 Transformers,以下部分包含了一些帮助我撰写本文的文章。

下次见 👋

参考文献和资源:

[1] Attention is all you need (2017), Vaswani 等,谷歌

[2] 注意机制中的键、查询和值到底是什么?(2019)Stack Exchange

[3] 图解 Transformer(2018), Jay Alammar

[4] Transformer 模型中位置编码的温和介绍(2023), Mehreen Saeed,Machine Learning Mastery

图片来源

实施人工智能就像买车和开车(但有所不同)

原文:towardsdatascience.com/implementing-ai-is-like-buying-and-driving-a-car-but-different-1e85f6afcc92?source=collection_archive---------16-----------------------#2023-01-18

一个帮助解释常见误区和陷阱给管理层的类比

Koen PetersTowards Data Science Koen Peters

·

关注 发表在 Towards Data Science ·8 分钟阅读·2023 年 1 月 18 日

--

实施人工智能的常见误区和陷阱 — 作者:Babette Huisman

到现在,大多数公司已经开始涉足 AI。然而,只有少数的举措似乎最终成功实施。根据 Gartner 的最新 AI 调查,54%的 AI 项目进入生产阶段[1]。根据我作为数据科学顾问的个人经验,进入生产阶段的项目要少得多。鉴于 Gartner 报告还提到 40%的受调查公司部署了成千上万的模型,这似乎并没有很好地代表那些刚刚开始涉足 AI 的公司。这些公司在整个组织中对 AI 的知识不足。决策者是其中的一层。这种知识的缺乏导致了不良决策、解决方案未能达到预期以及对所需努力的低估。为了帮助决策者提出正确的问题,并最终做出良好的、知情的决策,我提出了一个类比,解释了实施 AI 的一些复杂性、误区和陷阱。通过在这里分享这个类比,我希望也能帮助其他人。让我们深入探讨一下吧!

实施 AI 就像买车和驾驶汽车(但有所不同)

陷阱:出色的销售推介

不论销售推介多么出色或汽车看起来多么华丽,你都应该事先检查几个方面:

  • 你是否检查了汽车是否适合你的车道或车库?类似地,你也应该检查 AI 是否适合你要解决的问题。尽管这看起来是显而易见的,但利益相关者往往会为了使用特定技术而推动解决方案。是的,AI 是过去十年的热门词汇,那些首先掌握其实施的人期望获得巨额收益。但如果你的解决方案不适合你的问题,这种期望将永远无法实现。

提示:在开始使用 AI 之前,你应该先尝试一些更简单的方法。比如增加更多的业务逻辑,更改或自动化部分工作流程等。

  • 汽车在不同地形上的表现如何?它能否在草地、土路或越野路上行驶?类似地,你要了解你的 AI 有多么健壮和公平。它在数据的不同分布中表现是否一样?例如,不同的年龄组、特定类别如性别等。尽管你可能期望 AI 在各方面表现良好,但最近的历史告诉我们,这不一定总是如此[2]。这可能对你的业务和受 AI 结果影响的人造成严重后果。确定你的模型将用于什么,因此了解其要求是非常重要的。

  • 汽车的马力能告诉你一些关于它行驶速度的信息。然而,没有足够的扭矩,你的车将无法加速并达到速度。对于 AI 来说,准确率能告诉你一些关于其性能的信息。然而,当 100 个案例中有 1 个代表欺诈或病人时,AI 可能“预测”没有人是欺诈/病人,准确率达到 99%。这样的模型实际上是无用的。因此,查看其他性能指标也是很重要的,以便对你的 AI 性能有一个坚实的理解。例如,“召回率”表示正确识别的实际欺诈/病人的百分比[3]。在这种情况下,召回率将为零,表明 AI 模型的表现不如预期。

提示:数据科学家受过训练,能够解释这些指标,并应能够为你提供有关 AI 性能的建议。

100 个中需要被识别的一个 — 作者:巴贝特·赫伊斯曼

总结一下,如果你听说了一个很棒的解决方案,深入了解一下,看看它是否真的能解决你的问题。

陷阱:成本与收益

无论 AI 解决方案多么出色,如果没有对你尝试改进的过程的基线测量,你无法判断收益是否超过成本。

车速表遇见 KPI 仪表 — 作者:巴贝特·赫伊斯曼

  • 在你买车之前,你会想检查里程表(以公里/英里计算的行驶距离)和导航系统。你可能想跟踪你在特定时间段内行驶了多少公里,并且是否接近你的目标或目的地。如果你驾驶的次数非常少,那么拥有一辆车的成本可能不会超过其收益。项目失败往往是因为无法量化额外的收益与成本,使决策者只能猜测,有时甚至会放弃本来很好的项目。因此,在实施 AI 之前,你需要对过程有一个良好的理解,了解该过程当前的表现,并设定一个改进过程的目标。你将收集数据,定义、计算并跟踪过程 KPI,以便在实施 AI 解决方案后,你可以监控过程的改进程度或预计达到某个目标的时间。这些指标反过来可以让你计算投资是否值得,并让决策者对(继续或停止)项目做出明智的决策。

能够沟通结果、成本和效益对于做出项目决策至关重要。

神话:AI 是“设定即忘”

对于不熟悉 AI 的人来说,一个常见的误解是,一旦你实施了解决方案,你就完成了,它可以正常工作,不需要偶尔检查一下。

警报:当你的 AI 表现不如预期时 — 作者:巴贝特·赫伊斯曼

  • 就像汽车一样,AI 也需要维护。然而,与汽车不同的是,AI 通常没有预装的仪表盘来显示警告灯,也不会发出奇怪的噪音。如果你希望获得通知(而且你应该这样做),你的数据团队需要建立自己的仪表盘,并在 AI 显示性能下降的迹象时进行监控或发出警报。你甚至可以实施类似于“定期车辆检查”的措施,让你的 AI 通过检查(或需要进行某些调整以便通过检查),以确保它能够持续在生产中运行。此外,AI 是一款软件,就像其他任何软件产品一样,你可能需要在有新功能和安全措施时进行更新。

让 AI 在生产环境中成功运行需要你数据团队的持续努力。

神话:AI 是即插即用的。

不幸的是,你不能只是获取一个 AI,启动它并期望它能正常工作。

gaspump 与你自己的数据管道 — 作者:Babette Huisman

  • 使用汽车时,你可以开车去加油站,加注正确类型的汽油,然后就可以出发了。(或者对于电动车,你可以直接插上电源)。AI 解决方案也需要燃料,即数据。然而,与汽车不同的是,公开可用的“加油站”并不多,你不能从中获得正确处理的数据。你必须自己收集/挖掘数据,然后建立自己的数据精炼工厂(ETL 过程、数据管道等)。获得可用数据所需的努力往往被低估了。大型公司有专门的团队专注于获取数据、清理数据、提高数据质量(最好是在数据收集阶段)并确保数据代表现实。此外,更重要的是,数据随着时间的推移而变化,你需要不断监控你的数据管道,并调整数据处理或 AI 引擎,以确保它持续接收可以操作的“燃料”。

获取正确类型和质量的数据也需要你数据团队的持续努力。

陷阱和神话:AI 替代人或使人变得冗余。

另一个常见的误解,如果处理不当,会对公司产生负面长期影响。

  • 今天的汽车使驾驶变得更加轻松,但豪华车的车道辅助或自动驾驶功能并不意味着驾驶员不再需要掌握方向盘!(至少现在是这样)。同样,拥有 AI 解决方案并不意味着你不再需要员工。你应该在可能的情况下以不同的方式使用这些员工。他们的工作的一部分可以包括为公司增加更高价值的活动,例如制定未来目标、投资客户关系或解决 AI 无法处理的复杂问题。另一部分工作应包括检查 AI 完成的随机样本。这被称为“人类参与”(‘human in the loop’)[4],不仅为 AI 提供了宝贵的反馈,从而使其更准确,也为员工提供了额外的“安全”层,能够发现监控仪表板可能遗漏的异常行为。此外,如果员工能够监督他们的 AI 同事并通过纠正其行为来不断改进 AI,将增加信任和采用度。在这里,观察者间的可靠性是实现最佳结果的关键。

人类参与 — 作者:Babette Huisman

  • 就像你仍然需要坐在方向盘后面一样,你还需要知道如何驾驶。如果因为某种原因你不能使用 AI,并且公司里没有人能手动完成工作,这对公司将是有害的。你希望这些业务流程的专业知识留在公司里,最好的办法是让员工“监督”AI 或解决上述更复杂的案例。此外,每当你的监控仪表板突出显示一个问题时,公司内拥有专家知识来确定和实施所需的修复是保持 AI 在生产中运行的关键。

一般来说,你不希望失去公司内的专家知识,因为这些知识可以帮助指导公司的活动。

考虑到这里描述的要点,你离成功实施 AI 并真正让其加速业务又近了一步。

终于可以加速的汽车 — 作者:Babette Huisman

我相信还有更多的汽车与 AI 的类比,如果你想到一个好的,请在下面的评论中告诉我!此外,我想感谢 Babette Huisman 提供的精彩插图(这些插图在本文中经她许可使用)以及 Maikel Grobbe 的建设性批评。他们都帮助提升了这篇文章的水平。

参考资料:

  1. Gartner AI 调查 2022

  2. 有偏见或不稳定的 AI

  3. 精确度和召回率

  4. 环中人

在 PyTorch 中实现自定义损失函数

原文:towardsdatascience.com/implementing-custom-loss-functions-in-pytorch-50739f9e0ee1?source=collection_archive---------4-----------------------#2023-01-16

使用 MNIST 数据集理解 PyTorch 中自定义损失函数的理论和实现

Marco SanguinetiTowards Data Science Marco Sanguineti

·

关注 发表在 Towards Data Science ·12 min 阅读·2023 年 1 月 16 日

--

图片由 Markus WinklerUnsplash 提供

介绍

在机器学习中,损失函数是一个关键组成部分,用于衡量预测输出与实际输出之间的差异。它在模型训练中起着至关重要的作用,因为它通过指示模型应改进的方向来指导优化过程。损失函数的选择依赖于具体任务和数据类型。本文将深入探讨 PyTorch 中自定义损失函数的理论和实现,以 MNIST 数据集的数字分类为例。

MNIST 数据集是一个广泛使用的图像分类数据集,包含 70,000 张手写数字图像,每张图像的分辨率为 28x28 像素。任务是将这些图像分类为 10 个数字中的一个(0–9)。该任务旨在训练一个模型,使其能够准确地分类新的手写数字图像,基于 MNIST 数据集中提供的训练示例。

照片由 Carlos Muza 拍摄,来源于 Unsplash

处理这个任务的一个典型方法是使用多类逻辑回归模型,即软最大分类器。软最大函数将模型的输出映射到 10 个类别的概率分布上。交叉熵损失通常作为这种模型的损失函数。交叉熵损失计算预测概率分布与实际概率分布之间的差异。

然而,在某些情况下,交叉熵损失可能不是特定任务的最佳选择。例如,考虑一个场景,其中误分类某些类别的代价远高于其他类别。在这种情况下,有必要使用自定义损失函数来考虑每个类别的相对重要性。

在这篇文章中,我将展示如何为 MNIST 数据集实现自定义损失函数,其中误分类数字 9 的代价远高于其他数字。我们将使用 Pytorch 作为框架,首先讨论自定义损失函数背后的理论,然后展示如何使用 Pytorch 实现自定义损失函数。最后,我们将使用自定义损失函数在 MNIST 数据集上训练一个线性模型,并评估模型的性能。

自定义损失函数:原因

实现自定义损失函数非常重要,原因有几个:

  1. 特定问题:损失函数的选择依赖于具体任务和数据类型。可以设计自定义损失函数,以更好地适应问题的特征,从而提高模型性能。

  2. 类别不平衡:在许多实际数据集中,每个类别的样本数量可能差异很大。可以设计自定义损失函数以考虑类别不平衡,并对不同类别分配不同的代价。

  3. 成本敏感:在一些任务中,错误分类某些类别的成本可能远高于其他类别。可以设计自定义损失函数以考虑每个类别的相对重要性,从而得到一个更鲁棒的模型。

  4. 多任务学习:自定义损失函数可以设计成同时处理多个任务。这在需要一个模型执行多个相关任务的情况下非常有用。

  5. 正则化:自定义损失函数也可以用于正则化,这有助于防止过拟合并提高模型的泛化能力。

  6. 对抗训练:自定义损失函数还可以用于训练模型,使其对对抗攻击具有鲁棒性。

总结来说,自定义损失函数可以提供一种更好地优化模型以适应特定问题的方法,并能提供更好的性能和泛化能力。

PyTorch 中的自定义损失函数

MNIST 数据集包含 70,000 张手写数字图像,每张图像的分辨率为 28x28 像素。任务是将这些图像分类为 10 个数字中的一个(0–9)。这种任务的典型方法是使用多类逻辑回归模型,即 softmax 分类器。softmax 函数将模型的输出映射到 10 个类别的概率分布上。交叉熵损失通常用作这种类型模型的损失函数。

交叉熵损失计算预测概率分布和实际概率分布之间的差异。预测概率分布是通过对模型的输出应用 softmax 函数获得的。实际概率分布是一个 one-hot 向量,其中对应正确类别的元素值为 1,其他元素的值为 0。交叉熵损失定义为:

L = -∑(y_i * log(p_i))

其中 y_i 是类别 i 的实际概率,p_i 是类别 i 的预测概率。

然而,在某些情况下,交叉熵损失可能不是特定任务的最佳选择。例如,考虑一种场景,其中错误分类某些类别的成本远高于其他类别。在这种情况下,有必要使用自定义损失函数来考虑每个类别的相对重要性。

在 PyTorch 中,自定义损失函数可以通过创建nn.Module类的子类并重写forward方法来实现。forward方法以预测输出和实际输出为输入,并返回损失的值。

这里是一个针对 MNIST 分类任务的自定义损失函数示例,其中错误分类数字 9 的成本远高于其他数字:

class CustomLoss(nn.Module):
    def __init__(self):
        super(CustomLoss, self).__init__()

    def forward(self, output, target):
        target = torch.LongTensor(target)
        criterion = nn.CrossEntropyLoss()
        loss = criterion(output, target)
        mask = target == 9
        high_cost = (loss * mask.float()).mean()
        return loss + high_cost

在这个示例中,我们首先使用 nn.CrossEntropyLoss() 函数计算交叉熵损失。接下来,我们创建一个掩码,对于属于 9 类的样本掩码值为 1,对于其他样本掩码值为 0。然后,我们计算属于 9 类的样本的平均损失。最后,我们将这个高成本的损失添加到原始损失中,以获得最终损失。

要使用自定义损失函数,我们需要实例化它并将其作为参数传递给训练循环中的优化器的 criterion 参数。以下是如何使用自定义损失函数来训练 MNIST 数据集模型的示例:

import torch.nn as nn
import torch
from torchvision import datasets, transforms
from torch import nn, optim
import torch.nn.functional as F
import torchvision
import os

class CustomLoss(nn.Module):
    def __init__(self):
        super(CustomLoss, self).__init__()

    def forward(self, output, target):
        target = torch.LongTensor(target)
        criterion = nn.CrossEntropyLoss()
        loss = criterion(output, target)
        mask = target == 9
        high_cost = (loss * mask.float()).mean()
        return loss + high_cost

# Load the MNIST dataset
train_loader = torch.utils.data.DataLoader(
  torchvision.datasets.MNIST('/files/', train=True, download=True,
                             transform=torchvision.transforms.Compose([
                               torchvision.transforms.ToTensor(),
                               torchvision.transforms.Normalize(
                                 (0.1307,), (0.3081,))
                             ])),
  batch_size=32, shuffle=True)

test_loader = torch.utils.data.DataLoader(
  torchvision.datasets.MNIST('/files/', train=False, download=True,
                             transform=torchvision.transforms.Compose([
                               torchvision.transforms.ToTensor(),
                               torchvision.transforms.Normalize(
                                 (0.1307,), (0.3081,))
                             ])),
  batch_size=32, shuffle=True)

# Define the model, loss function and optimizer
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
        self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
        self.conv2_drop = nn.Dropout2d()
        self.fc1 = nn.Linear(320, 50)
        self.fc2 = nn.Linear(50, 10)

    def forward(self, x):
        x = F.relu(F.max_pool2d(self.conv1(x), 2))
        x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2))
        x = x.view(-1, 320)
        x = F.relu(self.fc1(x))
        x = F.dropout(x, training=self.training)
        x = self.fc2(x)
        return F.log_softmax(x)

network = Net()
optimizer = optim.SGD(network.parameters(), lr=0.01,
                      momentum=0.5)
criterion = CustomLoss()

# Training loop
n_epochs = 10

train_losses = []
train_counter = []
test_losses = []
test_counter = [i*len(train_loader.dataset) for i in range(n_epochs + 1)]

if os.path.exists('results'):
  os.system('rm -r results')

os.mkdir('results')

def train(epoch):
  network.train()
  for batch_idx, (data, target) in enumerate(train_loader):
    optimizer.zero_grad()
    output = network(data)
    loss = criterion(output, target)
    loss.backward()
    optimizer.step()
    if batch_idx % 1000 == 0:
      print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
        epoch, batch_idx * len(data), len(train_loader.dataset),
        100\. * batch_idx / len(train_loader), loss.item()))
      train_losses.append(loss.item())
      train_counter.append(
        (batch_idx*64) + ((epoch-1)*len(train_loader.dataset)))
      torch.save(network.state_dict(), 'results/model.pth')
      torch.save(optimizer.state_dict(), 'results/optimizer.pth')

def test():
  network.eval()
  test_loss = 0
  correct = 0
  with torch.no_grad():
    for data, target in test_loader:
      output = network(data)
      test_loss += criterion(output, target).item()
      pred = output.data.max(1, keepdim=True)[1]
      correct += pred.eq(target.data.view_as(pred)).sum()
  test_loss /= len(test_loader.dataset)
  test_losses.append(test_loss)
  print('\nTest set: Avg. loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
    test_loss, correct, len(test_loader.dataset),
    100\. * correct / len(test_loader.dataset)))

test()
for epoch in range(1, n_epochs + 1):
  train(epoch)
  test()

这段代码实现了一个自定义损失函数,用于 PyTorch 中的 MNIST 数据集。MNIST 数据集包含 70,000 张手写数字图像,每张图像的分辨率为 28x28 像素。任务是将这些图像分类为 10 个数字(0–9)中的一个。

第一个代码块通过继承 PyTorch 的 nn.Module 创建了一个名为 “CustomLoss” 的自定义损失函数。它有一个 forward 方法,接受两个输入:模型的输出和目标标签。forward 方法首先将目标标签转换为长整型张量。然后,它创建一个内置的 PyTorch 交叉熵损失函数的实例,并使用它计算模型输出与目标标签之间的损失。接下来,它创建一个掩码,以识别目标标签是否等于 9,然后将损失乘以这个掩码,并计算结果张量的平均值。最后,它返回原始损失与高成本损失的平均值之和。

下一个代码块使用 PyTorch 的内置数据加载工具加载 MNIST 数据集。train_loader 加载训练数据集并对图像应用指定的变换,例如将图像转换为张量和规范化像素值。test_loader 加载测试数据集,并应用相同的变换。

以下代码块通过继承 PyTorch 的 nn.Module 定义了一个卷积神经网络(CNN),称为 “Net”。该 CNN 包含 2 个卷积层、2 个线性层,以及一些用于正则化的 dropout 层。Net 类的 forward 方法按顺序应用卷积层和线性层,通过 ReLU 激活函数和最大池化层传递输出。它还对输出应用 dropout 层,并返回最终输出的 log-softmax。

下一个代码块创建了一个 Net 类的实例,一个优化器(随机梯度下降),以及一个自定义损失函数的实例。

最后一块代码是训练循环,其中模型训练了 10 个周期。在每个周期中,模型遍历训练数据集,将图像通过网络,使用自定义损失函数计算损失并反向传播梯度。然后,它使用优化器更新模型的参数。同时,它跟踪训练损失和测试损失,并定期将当前损失打印到控制台。此外,它还创建了一个名为“results”的新目录来存储训练过程的结果和输出。

import matplotlib.pyplot as plt

fig = plt.figure()
plt.plot(train_counter, train_losses, color='blue')
plt.scatter(test_counter, test_losses, color='red')
plt.legend(['Train Loss', 'Test Loss'], loc='upper right')
plt.xlabel('number of training examples seen')
plt.ylabel('negative log likelihood loss')
plt.show()

自定义损失趋势 — 图片由作者提供

这段代码正在为 MNIST 数据集创建自定义损失函数的图形。该图形将显示训练集和测试集的自定义损失。

它首先导入了 Matplotlib 库,这是一个用于 Python 的绘图库。然后,使用plt.figure()函数创建了一个指定大小的图形对象。

下一行代码使用plt.plot()函数绘制训练集的自定义损失。它使用train_countertrain_losses变量分别作为 x 轴和 y 轴的值。图的颜色通过color参数设置为蓝色。

然后,它使用plt.scatter()函数绘制测试集的自定义损失。它使用test_countertest_losses变量分别作为 x 轴和 y 轴的值。图的颜色通过color参数设置为红色。

plt.legend()函数为图形添加图例,指明哪个图代表训练损失,哪个图代表测试损失。loc参数设置为'upper right',这意味着图例将位于图形的右上角。

plt.xlabel()plt.ylabel()函数分别为图形的 x 轴和 y 轴添加标签。x 轴标签设置为'number of training examples seen',y 轴标签设置为'Custom loss'。

最后,使用plt.show()函数显示图形。

这段代码将显示一个图形,展示自定义损失函数在训练样本中的变化情况。蓝色线条代表训练集的自定义损失,红色点代表测试集的自定义损失。这个图形将帮助你观察自定义损失函数在训练过程中的表现,并评估模型的性能。

examples = enumerate(test_loader)
batch_idx, (example_data, example_targets) = next(examples)
with torch.no_grad():
  output = network(example_data)
fig = plt.figure()
for i in range(6):
  plt.subplot(2,3,i+1)
  plt.tight_layout()
  plt.imshow(example_data[i][0], cmap='gray', interpolation='none')
  plt.title("Prediction: {}".format(
    output.data.max(1, keepdim=True)[1][i].item()))
  plt.xticks([])
  plt.yticks([])

plt.show()

测试集样本和预测 — 图片由作者提供

这段代码显示了来自测试集的 6 张图像及其对应的训练网络的预测结果。

它开始使用enumerate()函数循环遍历 test_loader,这是一个按批次加载测试数据集的迭代器。使用next()函数获取测试集的第一个批次样本。

example_data 变量包含图像,example_targets 变量包含相应的标签。

然后使用 PyTorch 的 torch.no_grad() 函数,该函数用于暂时将 requires_grad 标志设置为 False。它将减少内存使用并加速计算,但也不会跟踪操作。

接下来的代码块使用 plt.figure() 函数创建一个新的图形对象。然后,使用 for 循环遍历测试集中的前 6 个示例。对于每个示例,它使用 plt.subplot() 函数在当前图形中创建一个子图。plt.tight_layout() 函数用于调整子图之间的间距。

然后它使用 plt.imshow() 函数在当前子图中显示图像。cmap 参数设置为 'gray' 以灰度显示图像,interpolation 参数设置为 'none' 以无插值显示图像。

plt.title() 函数用于为当前子图添加标题。标题显示了网络对当前示例的预测结果。网络的输出通过 output.data.max(1, keepdim=True)[1] 得到预测类别的索引。[i].item() 提取了预测类别的整数值。

plt.xticks()plt.yticks() 函数分别用于去除当前子图的 x 和 y 轴刻度。

最后,使用 plt.show() 函数来显示图像。此代码将显示一个包含 6 张来自测试集的图像及其由训练网络生成的预测结果的图形。图像以灰度显示且没有任何插值,预测的类别以标题形式显示在每张图像上方。这是一个有用的工具,可以用来可视化模型在测试集上的表现,识别潜在问题或误分类。

问候

在本文中,我们讨论了 PyTorch 中自定义损失函数的理论和实现,使用 MNIST 数据集进行数字分类作为例子。我们展示了如何通过子类化 nn.Module 类并重写 forward 方法来创建自定义损失函数。我们还提供了一个如何使用自定义损失函数在 MNIST 数据集上训练模型的示例。在某些类别误分类的成本远高于其他类别的场景中,自定义损失函数可能非常有用。需要注意的是,实施自定义损失函数时应谨慎,因为它们可能对模型性能产生重大影响。

加入 Medium 会员

如果你喜欢这篇文章并希望继续了解更多相关内容,我邀请你通过此链接加入 Medium 会员。

[## 通过我的推荐链接加入 Medium — Marco Sanguineti

阅读 Marco Sanguineti 的每一篇故事(以及 Medium 上成千上万其他作家的故事)。对文化的投资是最好的……

marcosanguineti.medium.com

成为会员后,你将能够访问更多种类的高质量内容,并获得专属会员故事的访问权限,同时你还将支持像我这样独立的作家和创作者。此外,作为会员,你可以高亮你喜欢的段落,保存故事以便稍后阅读,并获得个性化的阅读推荐。今天就注册,和我们一起继续探索这个话题和其他话题吧。

感谢你的支持!下次见,

马尔科

使用 fastai 实现深度学习——图像分类

原文:towardsdatascience.com/implementing-deep-learning-using-fastai-eff2fa05449e

快速而轻松地入门深度学习,无需涉猎所有细节

Wei-Meng LeeTowards Data Science Wei-Meng Lee

·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 4 月 19 日

--

图片来源:NASA 通过 Unsplash

近年来,人工智能 (AI) 受到了广泛关注,尤其是最近几个月 ChatGPT 的发布。人工智能中的基础技术之一是深度学习。深度学习是一种机器学习技术,使用神经网络来学习数据集中特征和标签之间的关系。一个神经网络通常如下所示:

所有图片由作者提供

上述各个圆圈称为神经元(或节点)。每个神经元有一个值叫做偏置,每个神经元彼此连接。每个连接都有一个值叫做权重。最左侧的神经元层是输入层(你将数据发送到此处进行预测),而最右侧的层称为输出层(预测结果在此处显示)。神经网络可以有任意多(或少)的隐藏层。

学习深度学习需要了解一些内容:

  • 什么是层、权重和偏置

  • 激活函数

  • 损失函数

  • 优化器

  • 反向传播

此外,还有几种类型的神经网络,例如:

  • 人工神经网络 (ANN)

  • 卷积神经网络 (CNN)

  • 循环神经网络 (RNN)

深度学习初学者通常需要掌握这些概念,然后才能开始构建有效的模型。

这就是 fastai 的作用所在。fastai 是一个深度学习库,允许初学者和从业者快速入门标准的深度学习模型,同时提供定制所构建模型的能力。

在这篇文章中,我将帮助你开始使用 fastai 构建你的第一个深度学习模型。

fastai 由 Jeremy Howard 和 Rachel Thomas 于 2016 年创立,旨在普及深度学习。他们通过提供一个名为“编程人员实用深度学习”(Practical Deep Learning for Coders)的开放在线课程(MOOC)来实现这一目标,该课程唯一的先决条件是掌握 Python 编程语言。来源:en.wikipedia.org/wiki/Fast.ai

fastai 到底是什么?

对于那些熟悉 TensorFlow 的人来说,fastaiPyTorch 的作用就像 KerasTensorFlow 的作用一样——简而言之,它是 PyTorch 的一个封装:

那么 PyTorch 是什么呢?PyTorch 是一个基于 Torch 库的机器学习框架,用于计算机视觉和自然语言处理等应用,最初由 Meta AI 开发,现在是 Linux 基金会的一部分。它是一个自由开源的软件,发布在修改版 BSD 许可证下(来源:en.wikipedia.org/wiki/PyTorch)。

TensorFlow 另一方面是另一个开源机器学习和人工智能库(由 Google 开发)。它的主要优势在于深度神经网络的训练和推断。由于 TensorFlow 的使用较为复杂,Keras 提供了一个高层次的库,能够在 TensorFlow 之上工作。Keras 使得深度学习对开发者来说更加容易上手。

这意味着,你现在可以构建深度学习神经网络,而不必真正理解神经网络是如何构建的细节。

fastai 的主要目标之一是使深度学习变得 易于接近迅速高效fastai 提供了四个主要应用领域的 API:

  • 视觉

  • 文本

  • 表格和时间序列分析

  • 协同过滤

在本文中,作为对 fastai 的快速入门,我将通过使用 fastai 构建一个视觉模型来识别图像。

安装 fastai

在本文中,我将使用 Jupyter Notebook。要安装 fastai,请在新单元格中输入以下命令:

!pip install fastai

训练图像识别的视觉模型

使用 fastai,你可以非常快速地训练深度学习模型,而无需过多接触底层实现。

虽然我说你可以在不直接接触 fastai 的情况下构建深度学习模型,但如果你对深度学习有一些背景知识——什么是神经网络、权重和偏差是什么、不同类型的神经网络(特别是本文中使用的卷积神经网络),以及如何训练和测试神经网络——那会非常有用。如果你感兴趣,请参考我在 Code Magazine 上关于深度学习工作原理的文章。

## Introduction to Deep Learning

作者:Wei-Meng Lee 发表在:CODE Magazine:2020 年 3 月/4 月 最后更新:2022 年 8 月 31 日 人工智能…

www.codemag.com

使用示例图片

fastai附带了一组你可以下载并用于训练的示例图片。首先,从fastai中导入vision库:

from fastai.vision.all import *

然后,打印出URLs.PETS的值:

URLs.PETS

它将返回以下 URL:

'https://s3.amazonaws.com/fast-ai-imageclas/oxford-iiit-pet.tgz'

这个 URL 指向一个包含一系列猫和狗图片的 tar 文件。

除了PETS项目,URLs对象还包含其他示例图片,如下所示:

要下载示例图片并解压它,请使用untar_data()函数,如下所示:

path = untar_data(URLs.PETS)/'images'    # path to images
path

图片现在将被扩展到/Users/weimenglee/.fastai/data/oxford-iiit-pet/images目录。

images文件夹中,你可以看到包含各种猫和狗品种的图片列表:

如你所见,文件名以动物名称开头,后跟一个索引。

使用 ImageDataLoaders 加载图片

images文件夹包含 37 种猫和狗品种的图片,因此你需要一种方法为每张图片分配标签,并将它们放入适当的类别中。对于标签提取,我们将提取文件名中最后一个下划线字符(“_”)之前的部分,并将其用作动物的标签:

我们如何对这些图片进行分类?幸运的是,你可以使用ImageDataLoaders类。ImageDataLoaders类可以自动从图片列表中构建验证和训练数据。使用from_name_func()函数,你可以从存储在文件夹中的图片系列中加载训练和验证数据。

首先,你需要定义一个函数来提取图片的标签:

def animal_labels(filename):    
    return filename[:filename.rfind('_')]  # extract the filename until the last underscore

然后,使用ImageDataLoaders类中的from_name_func()函数,并传递以下参数:

 dls = ImageDataLoaders.from_name_func(
    path,                                # path to images 
    get_image_files(path),               # get the list of path for all images
    valid_pct = 0.25,                    # percentages to use for validation
    seed = 42,                           # random seed
    label_func = animal_labels,          # labelling for the images
    item_tfms = Resize(224))             # transform the images to 224x224

dls变量的类型是fastai.data.core.DataLoaders

结果现在可以用于训练。

打印标签

如果你对训练数据集中各种猫狗品种感到好奇,可以使用vocab属性在ImageDataLoaders对象中将它们打印出来:

for vocab in dls.vocab:
    print(vocab)

这里列出了 37 种猫狗品种:

Abyssinian
Bengal
Birman
Bombay
British_Shorthair
Egyptian_Mau
Maine_Coon
Persian
Ragdoll
Russian_Blue
Siamese
Sphynx
american_bulldog
american_pit_bull_terrier
basset_hound
beagle
boxer
chihuahua
english_cocker_spaniel
english_setter
german_shorthaired
great_pyrenees
havanese
japanese_chin
keeshond
leonberger
miniature_pinscher
newfoundland
pomeranian
pug
saint_bernard
samoyed
scottish_terrier
shiba_inu
staffordshire_bull_terrier
wheaten_terrier
yorkshire_terrier

模型训练

由于我们正在构建一个视觉模型,我们将使用vision_learner()函数:

learn = vision_learner(dls, 
                       resnet34, 
                       metrics = error_rate)
learn.fine_tune(1)

vision_learner()函数接受三个参数:

  • 数据加载器

  • 使用的预训练模型

  • 用于评估模型的度量标准

Resnet34是什么?Resnet34 是一个图像分类模型。它是一个结构为 34 层的卷积神经网络(CNN)(因此得名)。Resnet34 在 ImageNet 数据集上进行了预训练,该数据集包含超过 100,000 张图像,涵盖 200 个类别。CNN 通常用于图像分类目的。

在这里,我们使用一个预训练模型——resnet34。预训练模型是已经用其自身数据集训练过的模型。还记得在上一节我们需要将图像调整为 224x224 吗?这是因为resnet34是在这种尺寸的图像上进行训练的。你可能会问为什么不使用更大的图像?其实,虽然更大的图像确实会更好,但这会带来速度和内存需求的增加。

在这个示例中,我们使用迁移学习。迁移学习是一种机器学习方法,其中一个为某个任务开发的模型被用作第二个任务模型的起点。迁移学习减少了你在训练上需要花费的时间。下图展示了迁移学习的工作原理。上部分展示了预训练模型的网络结构。在卷积神经网络(CNN)中,早期的层(从左开始)已经训练来识别基本形状、边缘、颜色等。而最右侧的几层则训练来识别模型已被训练识别的特定对象:

在迁移学习中,你不需要重新训练整个模型——你可以保留那些能够识别基本形状的早期层,同时保留最右侧的几层以识别你想训练的特定对象:

要开始迁移学习过程,你可以使用fine_tune()函数:

learn.fine_tune(1)

你指定的1epoch,即模型对你的图像进行训练的次数。

你应该使用的 epoch 数量取决于你有多少时间用于训练。较高的 epoch 需要较多的时间,但结果通常比低 epoch 更准确。另一方面,指定较高的 epoch 意味着模型会重复查看你的图像,这可能导致过拟合。过拟合意味着你的模型现在记住了你的图像,当给它一个之前从未见过的新图像时,它的表现会很差。

训练之后,你应该得到一些关于训练的统计数据:

进行预测

我们的模型现在已经训练完成,是时候进行测试了!为此,我下载了一张美国斗牛犬的图像(1024px-Bulldog_inglese.jpg):

来源:en.wikipedia.org/wiki/Bulldog#/media/File:Bulldog_inglese.jpg

将图像保存到与 Jupyter Notebook 相同的目录中。现在,你可以加载该图像并在 Jupyter Notebook 中显示它:

img = PILImage.create('1024px-Bulldog_inglese.jpg')
img.to_thumb(192)

然后,你可以使用 predict() 函数发送进行预测:

animal_type, index, probs = learn.predict(img)
print(f"Predicted animal: {animal_type}.") 
print(f"Predicted animal probability: {probs[index]}")
print(f"Probabilities: {probs}")

predict() 函数返回三个值:

  • 预测标签

  • 一个 torch.Tensor 对象,表示预测标签的索引

  • 每个标签的概率

以下是预测结果:

Predicted animal: american_bulldog.
Predicted animal probability: 0.6161370277404785
Probabilities: tensor([7.9024e-05, 4.4181e-04, 6.9500e-05, 8.0544e-05, 1.8523e-04, 7.2056e-05,
        2.3234e-05, 7.4391e-04, 3.1683e-05, 7.3335e-05, 1.9286e-04, 2.3213e-04,
        6.1614e-01, 4.6759e-03, 2.0838e-02, 4.2569e-04, 7.0168e-02, 4.3025e-05,
        2.1922e-05, 4.6172e-05, 1.0497e-04, 2.4546e-04, 2.3985e-04, 6.9188e-04,
        1.0650e-04, 2.1837e-04, 1.5202e-04, 1.7642e-04, 8.6259e-05, 1.4461e-02,
        2.5644e-01, 2.9418e-04, 4.5159e-04, 1.2193e-04, 1.0669e-02, 4.7959e-04,
        4.7566e-04])

从结果中可以看出,模型正确预测了图像是美国斗牛犬。

如果你喜欢阅读我的文章,并且它对你的职业/学习有帮助,请考虑注册成为 Medium 会员。每月 $5,你可以无限制访问 Medium 上所有文章(包括我的)。如果你通过以下链接注册,我将获得少量佣金(对你没有额外费用)。你的支持意味着我将能花更多时间写像这样的文章。

## 使用我的推荐链接加入 Medium - Wei-Meng Lee

阅读 Wei-Meng Lee 的每一篇故事(以及 Medium 上成千上万其他作家的故事)。你的会员费直接支持…

weimenglee.medium.com

总结

我希望这个快速入门能对如何使用 fastai 提供一些帮助。本文中讨论了一些深度学习的术语,但一开始你不必详细了解它们。你应该专注于让模型训练好,然后使用它进行预测。未来的文章中,我会给你更多如何使用 fastai 构建一些非常有趣的模型的例子。祝你好运!

从零实现 LoRA

原文:towardsdatascience.com/implementing-lora-from-scratch-20f838b046f1?source=collection_archive---------0-----------------------#2023-12-12

如何从零实现 LoRA 以及一些实用技巧

马丁·迪特根Towards Data Science 马丁·迪特根

·

关注 发表于 Towards Data Science ·17 分钟阅读·2023 年 12 月 12 日

--

由 DALLE 创建的 LoRA 抽象艺术表现

在这篇博客文章中,我将向你展示如何从零开始实现 LoRA。

LoRA,即低秩适配Low-Rank Adaptation)或低秩适配器Low-Rank Adaptors),提供了一种高效且轻量级的方法来微调现有的语言模型。这包括像BERTRoBERTa这样的掩码语言模型,以及像GPTLlamaMistral这样的因果(或聊天机器人)模型。

低秩适配器的主要优点之一是其高效性。通过使用更少的参数,LoRA 显著降低了计算复杂性和内存使用。这使我们能够在消费级 GPU 上训练大型模型,并轻松将我们紧凑(以兆字节为单位)的 LoRA 分发给他人。

此外,LoRA 可以提高泛化性能。通过限制模型复杂性,它们有助于防止过拟合,特别是在训练数据有限的情况下。这导致模型更能适应新的、未见过的数据,或者至少保留其初始训练任务的知识。

此外,低秩适配器可以无缝集成到现有的神经网络架构中。这种集成允许在最小的额外训练成本下进行预训练模型的微调和适应,使其非常适用于迁移学习应用。

我们将首先深入探讨 LoRA 的功能,然后我将演示如何为 RoBERTa 模型从头开始开发它,并使用 GLUESQuAD 基准测试我们的实现,并讨论一般的技巧和改进。

LoRA 的工作原理

LoRA 的基本理念是保持预训练的矩阵(即原始模型的参数)冻结(即保持固定状态),只向原始矩阵添加一个小的 delta,其参数比原始矩阵少。

例如考虑矩阵 W,它可以是完全连接层的参数,或者是 transformer 的自注意机制中的一个矩阵之一:

显然,如果W-orig 的尺寸为 n×m,我们只需初始化一个具有相同尺寸的新的 delta 矩阵进行微调,我们将毫无收获;相反,我们将会增加参数的数量。

这个技巧在于通过从较低维度矩阵 BA 的矩阵乘法构建 ΔW 比原始矩阵更少“维度化”

我们首先定义一个秩 r,要显著小于基本矩阵的维度 r≪nr≪m。然后矩阵 Bn×r,矩阵 Ar×m。将它们相乘得到一个具有相同尺寸的 W 矩阵,但是是由较低参数数量构建而成的。

显然,我们希望在训练开始时我们的 delta 是零,这样微调才能像原始模型一样开始。因此,B 通常初始化为全零,A 初始化为随机(通常是正态分布)值。

例如,这可能看起来像这样:

一个 LoRA 可能在实际矩阵中如何看的示例

想象一种情况,我们的基础维度是 1024,并且我们选择了 LoRA 的秩 r 为 4,则:

  • W 具有 1024 * 1024 ≈ 100 万个参数

  • AB 每个都有 r * 1024 = 4 * 1024 ≈ 4k 个参数,总共是 8k

  • 因此,我们只需要训练 0.8% 的参数来使用 LoRA 更新我们的矩阵

顺便说一句,在 LoRA 论文中,他们使用 alpha 参数对 delta 矩阵进行加权:

如果你只是将α设置为你实验的第一个r并微调学习率,通常可以在以后改变r参数,而无需再次微调学习率(至少大致如此)。虽然在我们的实现中可以忽略这一细节,但这是许多其他 LoRA 库(如 Hugging Face 的 PEFT)的常见特性。

实现 LoRA

对于我们的实现,我们希望紧密跟随原始的 LoRA 论文。在那里,他们测试了变换器中实际需要替换哪些矩阵。他们发现,在对 GPT-3 微调任务进行不同策略比较时,仅适配自注意力机制的查询和数值向量就足够了。

注意到现在很多人忽视了这种评估,并允许每个矩阵进行微调,无论任务或模型如何(参见 QLoRA 论文)。

我们的实现将在 PyTorch 中完成,但应该很容易适配到不同的框架中。

对于这篇博客文章,我简化了一些代码,以便更容易阅读,同时仍展示了核心要素。完整代码和一些训练好的 LoRA 权重可以在这里找到:github.com/Montinger/Transformer-Workbench

重新实现自注意力模型

我们希望适配的模型是来自 Huggingface 的 RoBERTa 模型。最直接的方法是重新包装原始的自注意力机制RobertaSelfAttention。新类LoraRobertaSelfAttention将初始化 LoRA 矩阵。所有的 B 矩阵将初始化为零,所有的 A 矩阵将用正态分布的随机数初始化。

class LoraRobertaSelfAttention(RobertaSelfAttention):
    """
    Extends RobertaSelfAttention with LoRA (Low-Rank Adaptation) matrices.
    LoRA enhances efficiency by only updating the query and value matrices.
    This class adds LoRA matrices and applies LoRA logic in the forward method.

    Parameters:
    - r (int): Rank for LoRA matrices.
    - config: Configuration of the Roberta Model.
    """
    def __init__(self, r=8, *args, **kwargs):
        super().__init__(*args, **kwargs)
        d = self.all_head_size

        # Initialize LoRA matrices for query and value
        self.lora_query_matrix_B = nn.Parameter(torch.zeros(d, r))
        self.lora_query_matrix_A = nn.Parameter(torch.randn(r, d))
        self.lora_value_matrix_B = nn.Parameter(torch.zeros(d, r))
        self.lora_value_matrix_A = nn.Parameter(torch.randn(r, d))

给定这些矩阵,我们现在定义新的类方法lora_querylora_value。这些方法计算ΔW矩阵,即BA,并将其添加到原始矩阵中,这些原始矩阵由原始方法queryvalue调用。

class LoraRobertaSelfAttention(RobertaSelfAttention):
    # ...

    def lora_query(self, x):
        """
        Applies LoRA to the query component. Computes a modified query output by adding 
        the LoRA adaptation to the standard query output. Requires the regular linear layer 
        to be frozen before training.
        """
        lora_query_weights = torch.matmul(self.lora_query_matrix_B, self.lora_query_matrix_A)
        return self.query(x) + F.linear(x, lora_query_weights)

    def lora_value(self, x):
        """
        Applies LoRA to the value component. Computes a modified value output by adding 
        the LoRA adaptation to the standard value output. Requires the regular linear layer 
        to be frozen before training.
        """
        lora_value_weights = torch.matmul(self.lora_value_matrix_B, self.lora_value_matrix_A)
        return self.value(x) + F.linear(x, lora_value_weights)

现在是难看的部分:为了使用这些方法,我们必须重写RobertaSelfAttention的原始前向函数。虽然这有点硬编码(参见后续改进讨论),但其实很简单。首先,我们从github.com/huggingface/transformers/blob/main/src/transformers/models/roberta/modeling_roberta.py复制原始前向代码。然后我们将每个query调用替换为lora_query,每个value调用替换为lora_value。函数看起来如下:

class LoraRobertaSelfAttention(RobertaSelfAttention):
    # ...
    def forward(self, hidden_states, *args, **kwargs):
        """Copied from
https://github.com/huggingface/transformers/blob/main/src/transformers/models/roberta/modeling_roberta.py
        but replaced the query and value calls with calls to the
        lora_query and lora_value functions.
        We will just sketch of how to adjust this here. 
        Change every call to self.value and self.query in the actual version.
        """
        # original code for query:
        ## mixed_query_layer = self.query(hidden_states)
        # updated query for LoRA:
        mixed_query_layer = self.lora_query(hidden_states)

        # The key has no LoRA, thus leave these calls unchanged
        key_layer = self.transpose_for_scores(self.key(hidden_states))

        # original code for value:
        ## value_layer = self.transpose_for_scores(self.value(hidden_states))
        # updated value for LoRA:
        value_layer = self.transpose_for_scores(self.lora_value(hidden_states))

        # ... (rest of the forward code, unchanged)

轰隆隆,我们完成了:我们的 LoRA 自注意力实现。现在唯一剩下的任务是将原始 RoBERTa 模型中的注意力模块替换出来。

替换模块

很好,我们已经用我们自己的实现替换了自注意力;但是我们如何将这个新类加入旧的 RoBERTa 模型中呢?实质上,我们必须遍历 RoBERTa 模型的每个命名组件,检查它是否是RobertaSelfAttention类,如果是,则替换为LoraRobertaSelfAttention,同时确保保留原始的权重矩阵。

为了实现这一点,我们将编写一个新的包装函数来进行此替换。此外,我们还希望稍后在一些实际任务上对 RoBERTa 模型进行微调。

class LoraWrapperRoberta(nn.Module):
    def __init__(self, task_type, num_classes=None, dropout_rate=0.1, model_id="roberta-large",
                 lora_rank=8, train_biases=True, train_embedding=False, train_layer_norms=True):
        """
        A wrapper for RoBERTa with Low-Rank Adaptation (LoRA) for various NLP tasks.
        - task_type: Type of NLP task ('glue', 'squad_v1', 'squad_v2').
        - num_classes: Number of classes for classification (varies with task).
        - dropout_rate: Dropout rate in the model.
        - model_id: Pre-trained RoBERTa model ID.
        - lora_rank: Rank for LoRA adaptation.
        - train_biases, train_embedding, train_layer_norms: 
            Flags whether to keep certain parameters trainable 
            after initializing LoRA.

        Example:
            model = LoraWrapperRoberta(task_type='glue')
        """
        super().__init__()
        # 1\. Initialize the base model with parameters
        self.model_id = model_id
        self.tokenizer = RobertaTokenizer.from_pretrained(model_id)
        self.model = RobertaModel.from_pretrained(model_id)
        self.model_config = self.model.config

        # 2\. Add the layer for the benchmark tasks
        d_model = self.model_config.hidden_size
        self.finetune_head_norm = nn.LayerNorm(d_model)
        self.finetune_head_dropout = nn.Dropout(dropout_rate)
        self.finetune_head_classifier = nn.Linear(d_model, num_classes)

        # 3\. Set up the LoRA model for training
        self.replace_multihead_attention()
        self.freeze_parameters_except_lora_and_bias()

正如您所见,我们在初始化中调用了两个辅助方法:

  1. self.replace_multihead_attention:这将使用我们之前编写的LoraRobertaSelfAttention替换所有神经网络部分的注意力。

  2. self.freeze_parameters_except_lora_and_bias:这将冻结所有主要参数,以便在训练中仅应用于 LoRA 参数以及我们希望保持可训练的其他偏置和层归一化参数。

class LoraWrapperRoberta(nn.Module):
    # ...

    def replace_multihead_attention_recursion(self, model):
        """
        Replaces RobertaSelfAttention with LoraRobertaSelfAttention in the model.
        This method applies the replacement recursively to all sub-components.

        Parameters
        ----------
        model : nn.Module
            The PyTorch module or model to be modified.
        """
        for name, module in model.named_children():
            if isinstance(module, RobertaSelfAttention):
                # Replace RobertaSelfAttention with LoraRobertaSelfAttention
                new_layer = LoraRobertaSelfAttention(r=self.lora_rank, config=self.model_config)
                new_layer.load_state_dict(module.state_dict(), strict=False)
                setattr(model, name, new_layer)
            else:
                # Recursive call for child modules
                self.replace_multihead_attention_recursion(module)

我们必须递归循环遍历所有模型部分,在 PyTorch 中,这些部分(实际上是 RoBERTa 的一部分)可以打包到一个单独的 PyTorch 模块中。

现在我们必须冻结所有不想再训练的参数:

class LoraWrapperRoberta(nn.Module):
    # ...

    def freeze_parameters_except_lora_and_bias(self):
        """
        Freezes all model parameters except for specific layers and types based on the configuration.
        Parameters in LoRA layers, the finetune head, bias parameters, embeddings, and layer norms 
        can be set as trainable based on class settings.
        """
        for name, param in self.model.named_parameters():
            is_trainable = (
                "lora_" in name or
                "finetune_head_" in name or
                (self.train_biases and "bias" in name) or
                (self.train_embeddings and "embeddings" in name) or
                (self.train_layer_norms and "LayerNorm" in name)
            )
            param.requires_grad = is_trainable

此外,我们还必须实现前向方法,以考虑我们将在其上进行微调的任务,以及两种保存和加载 LoRA 权重的方法,以便我们可以加载先前训练模型的适配器。

悬念:有一种方法,可以让代码变得更加简洁,并且更容易推广到其他网络架构(因为我们的代码相对于 RoBERTa 模型而言相当硬编码)。你能想到这可能是什么吗?在下面的可能的改进部分讨论之前,你有时间思考这个问题。但在此之前:让我们测试一些基准,看看我们的实现是否真的有效。

使用 GLUE 和 SQuAD 进行基准测试结果

我们的实现现在已准备好使用 GLUE(通用语言理解评估)和 SQuAD(斯坦福问答数据集)基准进行评估。

GLUE 基准测试是一套八项多样化的 NLP 任务,评估语言模型的全面理解能力。它包括情感分析、文本蕴涵和句子相似性等挑战,提供了模型语言适应能力和熟练度的强有力衡量。

另一方面,SQuAD 侧重于评估问答模型。它涉及从维基百科段落中提取答案,模型识别相关的文本片段。更高级的版本 SQuAD v2 引入了无法回答的问题,增加了复杂性,模拟了现实中模型必须识别文本缺失答案的情况。

请注意,对于以下基准测试,我没有调整任何超参数,没有进行多次运行(特别是较小的 GLUE 数据集容易受到随机噪声的影响),没有进行任何早停策略,并且没有从前一个 GLUE 任务的精细调整开始(通常用于减少小数据集噪声的可变性和防止过拟合)。

所有运行:

  • 从 RoBERTa-base 模型中刚初始化的 LoRA 注入开始,其秩为 8

  • 每个任务确切地进行了 6 个 epoch 的训练,没有任何早停策略。

  • 在前 2 个 epoch 期间,学习率线性增加到最大值,然后在剩余的 4 个 epoch 期间线性衰减至零。

  • 所有任务的最大学习率为 5e-4。

  • 所有任务的批处理大小为 16

RoBERTa-base 模型有 1.246 亿个参数。包括 LoRA 参数、偏差和层规范化,我们只有 42 万个未冻结参数需要训练。这意味着我们实际上只对原始参数的 0.34%进行了训练。

LoRA 为这些特定任务引入的参数数量非常少,实际磁盘大小仅为 1.7 MB。您可以在 Git 仓库的Output文件夹中找到训练过的 LoRA。

训练后,我们重新加载了 LoRA 参数,重新应用它们,并在每个任务的验证集上测试性能。以下是结果:

使用 LoRA 在 GLUE 基准测试中的性能

使用 LoRA 在 SQuAD 数据集上的性能

很可能这些结果可以通过一些超参数的微调大大改善。尽管如此,这清楚地证明了我们的 LoRA 实现是有效的,我们注入的低秩矩阵正在学习中。

可能的改进

回顾我们的实现,人们可能会想:“是否存在比重新编码自注意力类和执行复杂替换更有效、更可推广(即适用于其他网络架构)的方法?”

实际上,我们可以简单地在 pytorch 的nn.Linear函数周围实现一个包装器,并具体说明我们想要替换的层的名称。同样地,您可以编写包装器来适应大多数基本的 pytorch 层,并能够快速调整 LoRA 以适应新的网络架构。以下是如何快速实现这一点的简要草图:

class LoraLinear(nn.Linear):
    """
    Extends a PyTorch linear layer with Low-Rank Adaptation (LoRA).
    LoRA adds two matrices to the layer, allowing for efficient training of large models.
    """
    def __init__(self, in_features, out_features, r=8, *args, **kwargs):
        super().__init__(in_features, out_features, *args, **kwargs)

        # Initialize LoRA matrices
        self.lora_matrix_B = nn.Parameter(torch.zeros(out_features, r))
        self.lora_matrix_A = nn.Parameter(torch.randn(r, in_features))

        # Freeze the original weight matrix
        self.weight.requires_grad = False

    def forward(self, x: Tensor) -> Tensor:
        # Compute LoRA weight adjustment
        lora_weights = torch.matmul(self.lora_matrix_B, self.lora_matrix_A)
        # Apply the original and LoRA-adjusted linear transformations
        return super().forward(x) + F.linear(x, lora_weights)

实际上,这接近了 huggingface PEFT(参数高效微调)库实现 LoRA 的方式。对于任何实际应用场景,如果您不打算学习,我强烈建议使用它,而不是编写自己的代码。

同样,将 LoRA 注入所有线性层(即自注意力的所有矩阵以及全连接前向网络的两个线性层)也已成为一种相当常见的做法。通常,除了 LoRA 参数外,保持偏置和层归一化可训练也是个好主意。由于它们已经很小,你不需要对它们进行低秩注入。

量化原始矩阵权重以节省 GPU VRAM 也是明智的,这样可以在给定 GPU 上训练更大的模型。这可以通过使用 bits-and-bytes 库有效完成,该库现在已完全与 Hugging Face 集成(见参考文献)。

总结一下,这里是在严肃环境中低秩适配的五大法则:

低秩适配的五大法则

如果你觉得刻有法则的石碑难以阅读,这里以纯文本重新呈现:

低秩适配的五大法则

1. 利用 LoRA 高效地对模型进行微调,重点保持参数规模最小。

2. 使用 PEFT 库进行 LoRA 实现,避免复杂的编码工作。

3. 将 LoRA 适配扩展到所有线性层,增强整体模型能力。

4. 保持偏置和层归一化可训练,因为它们对模型适应性至关重要,不需要低秩适配。

5. 应用量化 LoRA — QLoRA — 以保护 GPU VRAM 并训练模型,使训练更大模型成为可能。

请记住,使用 QLoRA 训练可能比 LoRA 慢一些,因为它涉及在每次乘法期间对矩阵进行反量化。例如,在微调像 Llama-7B 这样的大型模型时,QLoRA 需要约 75% 更少的 VRAM,但比标准 LoRA 慢大约 40%。更多见解,请查看我在参考文献中链接的博客文章。

PEFT 实现的逐步指南

让我们看看如何真正遵守我们的法则,并通过 PEFT 实现更好的版本。

首先,让我们以量化的方式加载模型。得益于 bitsandbytes 与 Huggingface transformers 库的集成(于 2023 年 5 月推出),这变得非常简单。

我们必须指定一个配置文件,然后直接从 huggingface 加载模型以进行量化。一般来说,最好使用 transformers 中的AutoModel对象。将量化模型作为较大、新定义的nn.module对象的子模块加载是困难的。你通常应该使用 huggingface 的原始模型,因此直接导入 GLUE 任务的AutoModelForSequenceClassification和 SQuAD 基准的AutoModelForQuestionAnswering。在配置中,我们还可以指定不进行量化的参数:在这里,我们必须注册分类或 qa 输出头,因为我们希望对这些头进行完整的训练,即不使用 LoRA,因为这些头是为微调而新初始化的,且从未成为预训练基础模型的一部分。

import bitsandbytes as bnb
from transformers import AutoModel, AutoModelForSequenceClassification, BitsAndBytesConfig

# Configuration to load a quantized model
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,  # Enable 4-bit loading
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
    llm_int8_skip_modules=['classifier', 'qa_outputs'],  # Skip these for quantization
)

# Load the model from Huggingface with quantization
model = AutoModelForSequenceClassification.from_pretrained('roberta-base',
          torch_dtype="auto", quantization_config=bnb_config)

你可以通过检查模型的模块和参数数据类型来验证 4 位加载:

# Verify 4-bit loading
print("Verifying 4-bit elements (Linear4bit) in the attention layer:")
print(model.roberta.encoder.layer[4].attention)

print("Checking for uint8 data type:")
print(model.roberta.encoder.layer[4].attention.self.query.weight.dtype)

现在开始使用 PEFT 注入 LoRA 参数。请注意,PEFT 库在处理自定义模型或其他复杂结构时更加灵活,因此只要您只进行 LoRA 而不是 QLoRA(量化通常是棘手的部分)。

PEFT 库通过它们的名称来定位要替换的模块;因此,我们必须查看模型的model.named_parameters()。这是在非量化 roberta-base 模型中的样子。

Module                                                        Parameters
----------------------------------------------------------  ------------
roberta.embeddings.word_embeddings.weight                     38_603_520
roberta.embeddings.position_embeddings.weight                    394_752
roberta.embeddings.token_type_embeddings.weight                      768
roberta.embeddings.LayerNorm.weight                                  768
roberta.embeddings.LayerNorm.bias                                    768
roberta.encoder.layer.0.attention.self.query.weight              589_824
roberta.encoder.layer.0.attention.self.query.bias                    768
roberta.encoder.layer.0.attention.self.key.weight                589_824
roberta.encoder.layer.0.attention.self.key.bias                      768
roberta.encoder.layer.0.attention.self.value.weight              589_824
roberta.encoder.layer.0.attention.self.value.bias                    768
roberta.encoder.layer.0.attention.output.dense.weight            589_824
roberta.encoder.layer.0.attention.output.dense.bias                  768
roberta.encoder.layer.0.attention.output.LayerNorm.weight            768
roberta.encoder.layer.0.attention.output.LayerNorm.bias              768
roberta.encoder.layer.0.intermediate.dense.weight              2_359_296
roberta.encoder.layer.0.intermediate.dense.bias                    3_072
roberta.encoder.layer.0.output.dense.weight                    2_359_296
roberta.encoder.layer.0.output.dense.bias                            768
roberta.encoder.layer.0.output.LayerNorm.weight                      768
roberta.encoder.layer.0.output.LayerNorm.bias                        768
roberta.encoder.layer.1.attention.self.query.weight              589_824
...
roberta.encoder.layer.11.output.LayerNorm.bias                       768
classifier.dense.weight                                          589_824
classifier.dense.bias                                                768
classifier.out_proj.weight                                         1_536
classifier.out_proj.bias                                               2
----------------------------------------------------------  ------------
TOTAL                                                        124_647_170

然后,我们可以指定 LoRA 目标以选择这些字符串。检查的方法是,如果其完整名称中包含指定的子字符串,则为真。因此,写queryvalue等效于我们的从头开始实现上述内容。对于密集层,我们必须更加小心,因为分类器还具有密集输出。如果我们希望微调其他密集层,我们必须通过intermediate.denseoutput.dense更为具体。

所有未注入 LoRA 参数的参数都会自动冻结,即不会接收任何梯度更新。如果有任何我们希望以其原始形式训练的层,我们可以通过将列表传递给 Lora-Config 的modules_to_save参数来指定它们。在我们的情况下,我们想在这里添加LayerNorm和 GLUE 以及 SQuAD 的微调头。请注意,列表的每个元素不必匹配某个内容。我们可以简单地将classifierqa_outputs添加到此列表中,然后拥有一个可以正确工作于两个任务的单个配置文件。

对于偏置参数,你可以使用方便的配置参数bias。你可以指定all以重新训练所有模块的所有偏置,lora_only以仅训练注入的偏置,或者none在训练期间保持所有偏置不变。

以下示例注入了一个秩为 2 的 LoRA。我们用上面的 8 指定 alpha 参数,因为这是我们首先尝试的秩,并且应该允许我们保持从头开始示例的原始学习率。

import peft

# Config for the LoRA Injection via PEFT
peft_config = peft.LoraConfig(
    r=2, # rank dimension of the LoRA injected matrices
    lora_alpha=8, # parameter for scaling, use 8 here to make it comparable with our own implementation
    target_modules=['query', 'key', 'value', 'intermediate.dense', 'output.dense'], # be precise about dense because classifier has dense too
    modules_to_save=["LayerNorm", "classifier", "qa_outputs"], # Retrain the layer norm; classifier is the fine-tune head; qa_outputs is for SQuAD
    lora_dropout=0.1, # dropout probability for layers
    bias="all", # none, all, or lora_only
)

model = peft.get_peft_model(model, peft_config)

请记住,为 LoRA 注入指定更多模块可能会增加 VRAM 要求。如果遇到 VRAM 限制,请考虑减少目标模块的数量或 LoRA 秩。

对于训练,特别是使用 QLoRA 时,选择与量化矩阵兼容的优化器。用 bitsandbytes 变体替换你的标准 torch 优化器,如下所示:

import torch
import bitsandbytes as bnb

# replace this
optimizer = torch.optim.AdamW(args here)
# with this
optimizer = bnb.optim.AdamW8bit(same args here)

然后,您可以像以前一样训练此模型,而无需在训练过程中明确担心 QLoRA。

训练完成后,保存和重新加载模型的过程非常简单。使用model.save_pretrained保存您的模型,并指定所需的文件名。PEFT 库将在此位置自动创建一个目录,其中存储模型权重和配置文件。此文件包括基础模型和 LoRA 配置参数等重要细节。

要重新加载模型,请使用 peft.AutoPeftModel.from_pretrained,并将目录路径作为参数传递。一个关键点是,LoRA 配置当前不保留 AutoModelForSequenceClassification 初始化时的类别数量。在使用 from_pretrained 时,你需要手动输入这个类别数量作为附加参数。如果不这样做,将会导致错误。

重新加载的模型将包括应用了 LoRA 适配器的原始基础模型。如果你决定将 LoRA 适配器永久集成到基础模型矩阵中,只需执行 model.merge_and_unload()

要获得更为实操的理解和详细的说明,请查看 GitHub 仓库。在那里,你会找到两个名为Train-QLoRA-with-PEFT.ipynbLoad-LoRA-Weights-PEFT.ipynb的笔记本,提供了使用 PEFT 训练和加载模型的逐步示例。

结论

“我们不会停止探索,我们所有的探索最终将是到达我们开始的地方,并第一次了解这个地方。”

—— 摘自 T.S. 艾略特的《小吉丁》

这段旅程带领我们从简单的、尽管是硬编码的 LoRA 实现,深入了解了低秩适配器、它们的实际应用以及基准测试。

我们探讨了一种更高效的实现策略,并深入了解了像 PEFT 这样的现有库在 LoRA 集成中的优雅。

我们的冒险以实际的 LoRA 使用指南结束,这些指南被概括为“五项戒律”,确保在实际应用中有效且高效地使用这一技术,并提供了逐步实施的指南。

参考资料

所有图片,除非另有说明,均由作者提供。

将深度学习论文中的数学公式转化为高效的 PyTorch 代码:SimCLR 对比损失

原文:towardsdatascience.com/implementing-math-in-deep-learning-papers-into-efficient-pytorch-code-simclr-contrastive-loss-be94e1f63473?source=collection_archive---------5-----------------------#2023-07-05

学习如何将深度学习论文中的高级数学公式转化为高效的 PyTorch 代码,共分为三步。

Moein ShariatniaTowards Data Science Moein Shariatnia

·

关注 发表在 Towards Data Science ·7 分钟阅读·2023 年 7 月 5 日

--

摄影:Jeswin ThomasUnsplash 提供

介绍

加深对深度学习模型和损失函数背后数学理解的最佳方法之一,也是提高 PyTorch 技能的好方法,是自己动手实现深度学习论文。

书籍和博客帖子可以帮助你入门编程和学习机器学习/深度学习的基础知识,但在学习了几个相关资源并掌握了领域中的常规任务后,你会很快意识到在学习的过程中你只能依靠自己,并且你会发现大多数在线资源都很枯燥且过于浅薄。然而,我相信,如果你能在新深度学习论文发表时学习,并理解其中所需的数学部分(不一定要理解作者理论背后的所有数学证明),同时,你又是一个能够将其实现为高效代码的能干程序员,那么没有什么能阻止你在该领域保持最新并学习新思想。

对比损失的实现

我将介绍我的常规方法和实现数学在深度学习论文中的步骤,使用一个不简单的例子:在SimCLR 论文中的对比损失

这是损失的数学公式:

SimCLR 论文中的对比(NT-Xent)损失 | 来源于 arxiv.org/pdf/2002.05709.pdf

我同意公式的外观可能会让人感到畏惧!你可能会想,GitHub 上一定有很多现成的 PyTorch 实现,所以我们就使用它们吧 😃 是的,你说得对。网上确实有很多实现。然而,我认为这是一个练习这种技能的好例子,也可以作为一个很好的起点。

将数学实现到代码中的步骤

我将数学实现到 PyTorch 高效代码中的常规方法如下:

  1. 理解数学,将其用简单的术语解释

  2. 使用简单的 Python “for” 循环 实现一个初始版本,现在不进行复杂的矩阵乘法

  3. 将你的代码转换成高效 矩阵友好的 PyTorch 代码

好的,让我们直接进入第一步。

步骤 1:理解数学并用简单的术语解释

我假设你具备基本的线性代数知识并熟悉数学符号。如果没有,你可以使用这个工具来了解这些符号的含义和功能,只需绘制符号即可。你还可以查看这个很棒的维基百科页面,其中描述了大多数符号。这些都是你在需要时学习新知识的机会。我认为这是一种更高效的学习方式,而不是从头开始阅读数学教科书,几天后就放在一边 😃

回到我们的主题。正如公式上方的段落增加了更多的背景,在 SimCLR 学习策略中,你从 N 张图像开始,将每张图像转换 2 次以获得这些图像的增强视图(现在有 2*N 张图像)。然后,你将这些 2 * N 张图像通过一个模型,得到每张图像的嵌入向量。现在,你希望使同一图像的 2 个增强视图(一个正样本对)的嵌入向量在嵌入空间中更接近(对所有其他正样本对也做同样的处理)。一种测量两个向量相似度(接近,相同方向)的方法是使用余弦相似度,它被定义为 sim(u, v)(请参见上图的定义)。

简而言之,公式描述的是,对于我们批次中的每个项目,即图像的一个增强视图的嵌入,(记住:批次包含不同图像的所有增强视图的嵌入→如果从 N 张图像开始,批次大小为 2N),我们首先找到该图像的另一个增强视图的嵌入以形成一个正样本对。然后,我们计算这两个嵌入的余弦相似度并对其进行指数运算(公式的分子)。接着,我们计算与我们开始时的第一个嵌入向量构建的所有其他对的余弦相似度的指数运算(除了与自身的对,这就是公式中的 1[k!=i]的含义),并将它们相加以构建分母。现在,我们可以将分子除以分母,取自然对数并翻转符号!现在,我们得到了批次中第一个项目的损失。我们只需对批次中的所有其他项目重复相同的过程,然后取平均值,以便调用 PyTorch 的.backward()*方法来计算梯度。

第 2 步:使用简单的 Python 代码实现,采用幼稚的“for”循环!

使用慢速“for”循环的简单 Python 实现

让我们看一下代码。假设我们有两张图像:A 和 B。变量 aug_views_1 保存了这两张图像的一个增强视图(A1 和 B1)的嵌入(每个大小为 3),与 aug_views_2(A2 和 B2)相同;因此,两个矩阵中的第一个项目与图像 A 相关,第二个项目与图像 B 相关。我们将这两个矩阵拼接成projections矩阵(其中包含 4 个向量:A1,B1,A2,B2)。

为了保持投影矩阵中向量的关系,我们定义了pos_pairs字典来存储在拼接矩阵中哪些两个项目是相关的。(稍后我会解释F.normalize()的事!)

正如你在接下来的代码行中看到的,我在一个for 循环中遍历投影矩阵中的项,使用我们的字典找到相关向量,然后计算余弦相似度。你可能会想为什么不按照余弦相似度公式除以向量的大小。关键是,在开始循环之前,使用 F.normalize 函数,我将投影矩阵中的所有向量标准化为大小为 1。因此,在计算余弦相似度的那一行不需要除以大小。

在构建好分子后,我会找到批次中所有其他向量的索引(除了相同的索引 i),以计算包含分母的余弦相似度。最后,我通过将分子除以分母,并应用对数函数和翻转符号来计算损失。确保玩转代码以理解每一行的作用。

第 3 步:将其转换为高效的矩阵友好的 PyTorch 代码

之前的 Python 实现的问题是太慢,无法用于我们的训练流程;我们需要摆脱缓慢的“for”循环,并将其转换为矩阵乘法和数组操作,以利用并行化的优势。

PyTorch 实现

让我们看看这个代码片段发生了什么。这一次,我引入了 labels_1labels_2 张量来编码这些图像所属的任意类别,因为我们需要一种方法来编码 A1、A2 和 B1、B2 图像之间的关系。你选择标签 0 和 1(就像我做的)还是 5 和 8 都无所谓。

在连接了所有的嵌入和标签后,我们首先创建一个包含所有可能配对的余弦相似度的 sim_matrix

sim_matrix 的样子:绿色单元格包含我们的正样本对,橙色单元格是需要在分母中忽略的配对 | 作者提供的可视化

上面的可视化图是你理解代码如何工作的全部所需 😃 以及为什么我们要进行其中的步骤。考虑到 sim_matrix 的第一行,我们可以按如下方式计算批次中第一个项目 (A1) 的损失:我们需要将 A1A2(指数化)除以 A1B1、A1A2 和 A1B2(每个都先指数化)的总和,并将结果保存在存储所有损失的张量的第一个项目中。因此,我们需要首先制作一个掩码,以找到上面可视化图中的绿色单元格。代码中定义变量mask的两行正是做这件事。分子是通过将我们的 sim_matrix 乘以刚创建的掩码来计算的,然后对每行的项目求和(掩码后,每行将只有一个非零项目;即绿色单元格)。为了计算分母,我们需要在每行上求和,忽略对角线上的橙色单元格。为此,我们将使用 PyTorch 张量的.diag()方法。其余部分不言自明!

额外内容:使用 AI 助手(ChatGPT、Copilot 等)来实现公式

我们有很棒的工具可以帮助我们理解和实现深度学习论文中的数学。例如,你可以在给出论文中的公式后,要求 ChatGPT(或其他类似工具)用 PyTorch 实现代码。根据我的经验,如果你能在python-for-loop实现步骤中找到自己,ChatGPT 最能提供最好的最终答案,并减少试错次数。把那个初步实现交给 ChatGPT,要求它将其转换为仅使用矩阵乘法和张量操作的高效 PyTorch 代码;你会对答案感到惊讶 😃

进一步阅读

我鼓励你查看以下两个相同理念的优秀实现,以了解如何将这一实现扩展到更微妙的情况中,比如在监督对比学习设置中。

  1. 监督对比损失,由 Guillaume Erhard 编写

  2. SupContrast,由 Yonglong Tian 编写

关于我

我是 Moein Shariatnia,一名机器学习开发者和医学学生,专注于使用深度学习解决方案进行医学影像应用。我的研究主要集中在研究深度模型在各种情况下的泛化能力。欢迎通过电子邮件、Twitter 或 LinkedIn 与我联系。

在 PyTorch 中实现软最近邻损失

原文:towardsdatascience.com/implementing-soft-nearest-neighbor-loss-in-pytorch-b9ed2a371760?source=collection_archive---------4-----------------------#2023-11-27

数据集的类邻域可以通过软最近邻损失来学习

Abien Fred AgarapTowards Data Science Abien Fred Agarap

·

关注 发表在 Towards Data Science ·8 分钟阅读·2023 年 11 月 27 日

--

在本文中,我们讨论了如何实现软最近邻损失,我们也在这里谈到过这个话题。

表示学习是通过深度神经网络学习给定数据集中最显著特征的任务。通常这是在监督学习范式中隐式完成的任务,并且是深度学习成功的关键因素 (Krizhevsky et al., 2012He et al., 2016Simonyan et al., 2014)。换句话说,表示学习自动化了特征提取过程。通过这个过程,我们可以将学到的表示用于下游任务,如分类、回归和合成。

图 1. 来源于 SNNL (Frosst et al., 2019). 通过最小化软最近邻损失,类相似数据点(如其颜色所示)之间的距离被最小化,而类不同数据点之间的距离被最大化。

我们也可以影响学习到的表示的形成,以适应特定的应用场景。在分类的情况下,表示会被调整使得同一类的数据点聚集在一起,而在生成(例如在 GAN 中)中,表示会被调整使得真实数据点与合成数据点聚集在一起。

同样,我们也享受了使用主成分分析(PCA)来编码特征以用于下游任务。然而,在 PCA 编码的表示中没有任何类或标签信息,因此在下游任务上的表现可能会进一步提升。我们可以通过学习数据集的邻域结构来改进编码的表示,即哪些特征被聚集在一起,这样的聚集会暗示这些特征属于同一类,如半监督学习文献中的聚类假设所示 (Chapelle et al., 2009)。

为了将邻域结构整合进表示中,已经引入了流形学习技术,如局部线性嵌入(LLE)(Roweis & Saul, 2000)、邻域组件分析(NCA)(Hinton et al., 2004)和 t-随机邻域嵌入(t-SNE)(Maaten & Hinton, 2008)。

然而,上述流形学习技术各有其缺点。例如,LLE 和 NCA 编码的是线性嵌入,而非非线性嵌入。同时,t-SNE 嵌入的结构依赖于所使用的超参数。

为了避免这种缺陷,我们可以使用改进的 NCA 算法,即软最近邻损失或 SNNL(Salakhutdinov & Hinton, 2007; Frosst et al., 2019)。SNNL 通过引入非线性改进了 NCA 算法,它是在神经网络的每一隐藏层上计算的,而不仅仅是最后的编码层。此损失函数用于优化数据集中点的纠缠

在这种情况下,纠缠定义为类相似的数据点彼此之间的接近程度,相较于类不同的数据点。低纠缠意味着类相似的数据点彼此之间要比类不同的数据点更接近(见图 1)。拥有这样的数据点集合将使下游任务更容易完成,且性能更佳。Frosst 等人(2019)通过引入温度因子T扩展了 SNNL 目标。因此,我们得到以下最终损失函数,

图 2. 软最近邻损失函数。图由作者提供。

其中d是原始输入特征或神经网络隐藏层表示上的距离度量,T是与隐藏层中数据点之间的距离直接成正比的温度因子。对于此实现,我们使用余弦距离作为我们的距离度量,以获得更稳定的计算。

图 3. 余弦距离公式。图由作者提供。

本文的目的是帮助读者理解和实现软最近邻损失,因此我们将详细分析损失函数以更好地理解它。

距离度量

我们应该首先计算的是数据点之间的距离,这些距离可以是原始输入特征或网络的隐藏层表示。

图 4. 计算 SNNL 的第一步是计算输入数据点的距离度量。图由作者提供。

对于我们的实现,我们使用余弦距离度量(图 3)以获得更稳定的计算。暂时,我们忽略上图中标记的子集ijik,只专注于计算输入数据点之间的余弦距离。我们通过以下 PyTorch 代码实现:

normalized_a = torch.nn.functional.normalize(features, dim=1, p=2)
normalized_b = torch.nn.functional.normalize(features, dim=1, p=2)
normalized_b = torch.conj(normalized_b).T
product = torch.matmul(normalized_a, normalized_b)
distance_matrix = torch.sub(torch.tensor(1.0), product)

在上面的代码片段中,我们首先在第 1 和第 2 行使用欧几里得范数对输入特征进行归一化。然后在第 3 行,我们获取归一化输入特征第二组的共轭转置。我们计算共轭转置以考虑复数向量。在第 4 和第 5 行,我们计算输入特征的余弦相似度和距离。

具体来说,考虑以下特征集,

tensor([[ 1.0999, -0.9438,  0.7996, -0.4247],
        [ 1.2150, -0.2953,  0.0417, -1.2913],
        [ 1.3218,  0.4214, -0.1541,  0.0961],
        [-0.7253,  1.1685, -0.1070,  1.3683]])

使用我们上面定义的距离度量,我们得到以下距离矩阵,

tensor([[ 0.0000e+00,  2.8502e-01,  6.2687e-01,  1.7732e+00],
        [ 2.8502e-01,  0.0000e+00,  4.6293e-01,  1.8581e+00],
        [ 6.2687e-01,  4.6293e-01, -1.1921e-07,  1.1171e+00],
        [ 1.7732e+00,  1.8581e+00,  1.1171e+00, -1.1921e-07]])

采样概率

现在我们可以计算表示选择每个特征的概率的矩阵,给定其与所有其他特征的成对距离。这仅仅是选择i点的概率,基于ijk点之间的距离。

图 5。第二步是计算基于距离的采样概率。图由作者提供。

我们可以通过以下代码计算这一点:

pairwise_distance_matrix = torch.exp(
    -(distance_matrix / temperature)
) - torch.eye(features.shape[0]).to(model.device)

代码首先计算距离矩阵的负指数除以温度因子,将值缩放到正值。温度因子决定如何控制给定点对之间距离的重要性,例如,在低温下,损失由小距离主导,而广泛分隔表示之间的实际距离变得不那么重要。

在减去torch.eye(features.shape[0])(即对角矩阵)之前,张量如下,

tensor([[1.0000, 0.7520, 0.5343, 0.1698],
        [0.7520, 1.0000, 0.6294, 0.1560],
        [0.5343, 0.6294, 1.0000, 0.3272],
        [0.1698, 0.1560, 0.3272, 1.0000]])

我们从距离矩阵中减去一个对角矩阵,以去除所有自相似项(即每个点到自身的距离或相似度)。

接下来,我们可以通过以下代码计算每对数据点的采样概率:

pick_probability = pairwise_distance_matrix / (
    torch.sum(pairwise_distance_matrix, 1).view(-1, 1)
    + stability_epsilon
)

掩码采样概率

到目前为止,我们计算的采样概率不包含任何标签信息。我们通过用数据集标签掩盖采样概率来整合标签信息。

图 6。我们利用标签信息来隔离属于同一类别的点的概率。图由作者提供。

首先,我们必须从标签向量中推导出一个成对矩阵:

masking_matrix = torch.squeeze(
    torch.eq(labels, labels.unsqueeze(1)).float()
)

我们应用掩码矩阵,利用标签信息来隔离属于同一类别的点的概率:

masked_pick_probability = pick_probability * masking_matrix

接下来,我们通过计算每行的掩码采样概率的总和来计算特定特征的总采样概率,

summed_masked_pick_probability = torch.sum(masked_pick_probability, dim=1)

最后,我们可以计算采样概率总和的对数,为计算便利性添加额外的计算稳定变量,并求平均作为网络的最近邻损失,

snnl = torch.mean(
    -torch.log(summed_masked_pick_probability + stability_epsilon
)

我们现在可以将这些组件组合在一起,在前向传递函数中计算整个深度神经网络的软最近邻损失,

def forward(
    self,
    model: torch.nn.Module,
    features: torch.Tensor,
    labels: torch.Tensor,
    outputs: torch.Tensor,
    epoch: int,
) -> Tuple:
    if self.use_annealing:
        self.temperature = 1.0 / ((1.0 + epoch) ** 0.55)

    primary_loss = self.primary_criterion(
        outputs, features if self.unsupervised else labels
    )

    activations = self.compute_activations(model=model, features=features)

    layers_snnl = []
    for key, value in activations.items():
        value = value[:, : self.code_units]
        distance_matrix = self.pairwise_cosine_distance(features=value)
        pairwise_distance_matrix = self.normalize_distance_matrix(
            features=value, distance_matrix=distance_matrix
        )
        pick_probability = self.compute_sampling_probability(
            pairwise_distance_matrix
        )
        summed_masked_pick_probability = self.mask_sampling_probability(
            labels, pick_probability
        )
        snnl = torch.mean(
            -torch.log(self.stability_epsilon + summed_masked_pick_probability)
        )
        layers_snnl.append(snnl)

    snn_loss = torch.stack(layers_snnl).sum()

    train_loss = torch.add(primary_loss, torch.mul(self.factor, snn_loss))

    return train_loss, primary_loss, snn_loss

可视化解耦表示

我们使用软最近邻损失训练了一个自编码器,并可视化了其学习到的解缠表示。该自编码器包含 (x-500–500–2000-d-2000–500–500-x) 单元,并在 MNIST、Fashion-MNIST 和 EMNIST-Balanced 数据集的小型标注子集上进行训练。这是为了模拟标注样本的稀缺性,因为自编码器通常是无监督模型。

图 7. 3D 可视化比较了三个数据集的原始表示和解缠潜在表示。为了实现这种可视化,表示使用 t-SNE 编码,困惑度 = 50,学习率 = 10,优化 5000 次迭代。图由作者提供。

我们仅可视化了任意选择的 10 个簇,以便更简单、更清晰地展示 EMNIST-Balanced 数据集。从上图可以看出,潜在代码表示变得更适合聚类,通过集群分散度和正确的集群分配(由集群颜色指示)体现了良好的定义簇。

结束语

在本文中,我们详细分析了软最近邻损失函数,并讨论了如何在 PyTorch 中实现它。

软最近邻损失首次由 Salakhutdinov & Hinton (2007) 引入,用于计算自编码器潜在代码(瓶颈)表示上的损失,然后将该表示用于下游的 kNN 分类任务。

Frosst, Papernot, & Hinton (2019) 通过引入温度因子并计算神经网络所有层的损失,扩展了软最近邻损失。

最后,我们采用了退火温度因子来进一步改善网络的学习解缠表示,并加速解缠过程 (Agarap & Azcarraga, 2020)。

完整的代码实现可在 GitLab 上获得。

参考文献

  • Agarap, Abien Fred, 和 Arnulfo P. Azcarraga. “通过解缠内部表示来改善 k-means 聚类性能。” 2020 国际神经网络联合会议 (IJCNN). IEEE, 2020.

  • Chapelle, Olivier, Bernhard Scholkopf 和 Alexander Zien. “半监督学习 (chapelle, o. 等, 编;2006)[书评]。” IEEE 神经网络交易 20.3 (2009): 542–542.

  • Frosst, Nicholas, Nicolas Papernot 和 Geoffrey Hinton. “分析和改进软最近邻损失的表示。” 国际机器学习会议. PMLR, 2019.

  • Goldberger, Jacob 等. “邻域组件分析。” 神经信息处理系统进展. 2005.

  • He, Kaiming, 等. “用于图像识别的深度残差学习。” IEEE 计算机视觉与模式识别会议论文集。2016 年。

  • Hinton, G., 等. “邻域组件分析。” NIPS 会议论文集。2004 年。

  • Krizhevsky, Alex, Ilya Sutskever, 和 Geoffrey E. Hinton. “使用深度卷积神经网络进行 ImageNet 分类。” 神经信息处理系统进展 25 (2012)。

  • Roweis, Sam T., 和 Lawrence K. Saul. “通过局部线性嵌入进行非线性维度约简。” 科学 290.5500 (2000): 2323–2326。

  • Salakhutdinov, Ruslan, 和 Geoff Hinton. “通过保留类别邻域结构来学习非线性嵌入。” 人工智能与统计学。2007 年。

  • Simonyan, Karen, 和 Andrew Zisserman. “用于大规模图像识别的非常深的卷积网络。” arXiv 预印本 arXiv:1409.1556 (2014)。

  • Van der Maaten, Laurens, 和 Geoffrey Hinton. “使用 t-SNE 进行数据可视化。” 机器学习研究杂志 9.11 (2008)。

从头实现最速下降算法

原文:towardsdatascience.com/implementing-the-steepest-descent-algorithm-in-python-from-scratch-d32da2906fe2

Nicolo Cosimo AlbaneseTowards Data Science Nicolo Cosimo Albanese

·发表于 Towards Data Science ·11 分钟阅读·2023 年 2 月 20 日

--

图片由作者提供。

目录

  1. 介绍

  2. 最速下降算法

    2.1 搜索方向

    2.2 步长

    2.3 算法

  3. 实现

    3.1 常数步长

    3.2 带有 Armijo 条件的线搜索

  4. 结论

1. 介绍

优化是寻找一组变量x,使得目标函数f(x)最小化或最大化的过程。由于最大化一个函数等同于最小化它的负数,我们可以专注于最小化问题:

对于我们的例子,我们将定义一个二次的多变量目标函数f(x)如下:

它的梯度 ∇f(x)

import numpy as np

def f(x):
    '''Objective function'''
    return 0.5*(x[0] - 4.5)**2 + 2.5*(x[1] - 2.3)**2

def df(x):
    '''Gradient of the objective function'''
    return np.array([x[0] - 4.5, 5*(x[1] - 2.3)])

可以利用流行的[scipy.optimize.minimize](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html)函数来快速找到最优值,该函数来自于流行的[SciPy](https://scipy.org/)库:

from scipy.optimize import minimize

result = minimize(
    f, np.zeros(2), method='trust-constr', jac=df)

result.x
array([4.5, 2.3])

我们可以绘制目标函数及其最小值:

import matplotlib.pyplot as plt

# Prepare the objective function between -10 and 10
X, Y = np.meshgrid(np.linspace(-10, 10, 20), np.linspace(-10, 10, 20))
Z = f(np.array([X, Y]))

# Minimizer
min_x0, min_x1 = np.meshgrid(result.x[0], result.x[1])   
min_z = f(np.stack([min_x0, min_x1]))

# Plot
fig = plt.figure(figsize=(15, 20))

# First subplot
ax = fig.add_subplot(1, 2, 1, projection='3d')
ax.contour3D(X, Y, Z, 60, cmap='viridis')
ax.scatter(min_x0, min_x1, min_z, marker='o', color='red', linewidth=10)
ax.set_xlabel('$x_{0}$')
ax.set_ylabel('$x_{1}$')
ax.set_zlabel('$f(x)$')
ax.view_init(40, 20)

# Second subplot
ax = fig.add_subplot(1, 2, 2, projection='3d')
ax.contour3D(X, Y, Z, 60, cmap='viridis')
ax.scatter(min_x0, min_x1, min_z, marker='o', color='red', linewidth=10)
ax.set_xlabel('$x_{0}$')
ax.set_ylabel('$x_{1}$')
ax.set_zlabel('$f(x)$')
ax.axes.zaxis.set_ticklabels([])
ax.view_init(90, -90);

图片由作者提供。

现在我们介绍最速下降算法并从头实现它。我们的目标是解决优化问题并找到最小值[4.5, 2.3]

2. 最速下降算法

要解决优化问题minₓ f(x),我们首先在坐标空间中的某一点开始。然后,我们通过搜索方向p迭代地移动,朝着f(x)的最小值更好的近似值前进:

在这个表达式中:

  • x 是输入变量;

  • p搜索方向

  • α > 0步长步幅;它描述了我们在每次迭代 k 中应该沿着方向 p 移动多少。

这种方法需要适当选择步长 α 和搜索方向 p

2.1. 搜索方向

作为搜索方向,最陡下降算法使用在当前迭代 xₖ 中评估的负梯度 -∇f(xₖ)。这是一个合理的选择,因为函数的负梯度总是指向函数下降最快的方向。

因此,我们可以将表达式重写为:

由于最小值是一个驻点,当梯度的范数小于给定的容忍度时,停止算法是合理的:如果梯度达到零,我们可能找到了最小值。

2.2. 步长

由于我们试图最小化 f(x),理想的步长 α 是以下目标函数 φ(α) 的最小值:

不幸的是,这需要在当前优化任务中解决额外的优化任务:minₓ f(x)。此外,虽然 φ(α) 是一元的,但找到它的最小值可能需要对 f(x) 及其梯度进行过多的评估。

简而言之,我们在寻找α的选择与做出选择所需时间之间的权衡。为此,我们可以简单地选择一个步长值,确保目标函数至少减少一定量。实际上,一个流行的不精确线搜索条件指出,α应该通过以下不等式导致f(x)充分减少

在文献中,这被称为充分减少Armijo 条件,并且它属于 Wolfe 条件 的集合。

常数 c 被选择为较小的值;一个常见的值是 10^-4。

由于最陡下降法使用负梯度 -∇f(xₖ) 作为搜索方向 pₖ,表达式 + ∇f(xₖ)^T * pₖ 等于梯度的负平方范数。在 Python 中:-np.linalg.norm(∇f(xₖ))**2。因此,我们的充分减少条件变为:

2.3. 算法

我们现在可以写出所需的最陡下降法步骤:

  1. 选择一个起始点 x = x₀

  2. 选择一个最大迭代次数 M

  3. 选择一个接近零的容忍度 tol 来评估梯度

  4. 设置步数计数器 n

  5. 在循环中重复:

    5.1 通过 Armijo 条件(线搜索)更新 α

    5.2 构造下一个点 x = x - α ⋅ ∇f(x)

    5.3 评估新的梯度 ∇f(x) 5.4 更新步数计数器 n = n + 1

    5.5 如果当前梯度的范数足够小 ||∇f(x)|| < tol 或达到最大迭代次数 n = M,则退出循环

  6. 返回 x

让我们用 Python 来实现它。

3. 实现

在这一部分,我们分享了最陡下降算法的实现。特别是,我们按步骤进行:

  1. 我们从常数步长开始,然后

  2. 我们添加了带有 Armijo 条件的线搜索。

3.1 常数步长

让我们从实现我们迭代方法的简化版本开始

在所有迭代中应用 常数 步长值 α。我们的目的是实际验证对于任何常数 α 收敛性并不保证,因此需要实现线搜索:

def steepest_descent(gradient, x0 = np.zeros(2), alpha = 0.01, max_iter = 10000, tolerance = 1e-10): 
    '''
    Steepest descent with constant step size alpha.

    Args:
      - gradient: gradient of the objective function
      - alpha: line search parameter (default: 0.01)
      - x0: initial guess for x_0 and x_1 (default values: zero) <numpy.ndarray>
      - max_iter: maximum number of iterations (default: 10000)
      - tolerance: minimum gradient magnitude at which the algorithm stops (default: 1e-10)

    Out:
      - results: <numpy.ndarray> of size (n_iter, 2) with x_0 and x_1 values at each iteration
      - number of steps: <int>
    '''

    # Prepare list to store results at each iteration 
    results = np.array([])

    # Evaluate the gradient at the starting point 
    gradient_x = gradient(x0)

    # Initialize the steps counter 
    steps_count = 0

    # Set the initial point 
    x = x0 
    results = np.append(results, x, axis=0)

    # Iterate until the gradient is below the tolerance or maximum number of iterations is reached
    # Stopping criterion: inf norm of the gradient (max abs)
    while any(abs(gradient_x) > tolerance) and steps_count < max_iter:

        # Update the step size through the Armijo condition
        # Note: the first value of alpha is commonly set to 1
        #alpha = line_search(1, x, gradient_x)

        # Update the current point by moving in the direction of the negative gradient 
        x = x - alpha * gradient_x

        # Store the result
        results = np.append(results, x, axis=0)

        # Evaluate the gradient at the new point 
        gradient_x = gradient(x) 

        # Increment the iteration counter 
        steps_count += 1 

    # Return the steps taken and the number of steps
    return results.reshape(-1, 2), steps_count

让我们使用这个函数来解决我们的优化任务:

# Steepest descent
points, iters = steepest_descent(
  df, x0 = np.array([-9, -9]), alpha=0.30)

# Found minimizer
minimizer = points[-1].round(1)

# Print results
print('- Final results: {}'.format(minimizer))
print('- N° steps: {}'.format(iters))
Final results: [4.5 2.3]
N° steps: 72

使用 α = 0.3 时,最小值在 72 步中达到了。我们可以绘制每次迭代中的点 x

# Steepest descent steps
X_estimate, Y_estimate = points[:, 0], points[:, 1] 
Z_estimate = f(np.array([X_estimate, Y_estimate]))

# Plot
fig = plt.figure(figsize=(20, 20))

# First subplot
ax = fig.add_subplot(1, 2, 1, projection='3d')
ax.contour3D(X, Y, Z, 60, cmap='viridis')
ax.plot(X_estimate, Y_estimate, Z_estimate, color='red', linewidth=3)
ax.scatter(min_x0, min_x1, min_z, marker='o', color='red', linewidth=10)
ax.set_xlabel('$x_{0}$')
ax.set_ylabel('$x_{1}$')
ax.set_zlabel('$f(x)$')
ax.view_init(20, 20)

# Second subplot
ax = fig.add_subplot(1, 2, 2, projection='3d')
ax.contour3D(X, Y, Z, 60, cmap='viridis')
ax.plot(X_estimate, Y_estimate, Z_estimate, color='red', linewidth=3)
ax.scatter(min_x0, min_x1, min_z, marker='o', color='red', linewidth=10)
ax.set_xlabel('$x_{0}$')
ax.set_ylabel('$x_{1}$')
ax.set_zlabel('$f(x)$')
ax.axes.zaxis.set_ticklabels([])
ax.view_init(90, -90);

图片来源于作者。

我们观察到最陡下降法的特征是通过一个显著的“之”字形路径向最小值前进。这是由于选择了负梯度 -∇f(x) 作为搜索方向 p

正如我们讨论的,对于任何步长值收敛性并不保证。在之前的示例中,我们使用了常数步长 α = 0.3,但如果选择不同的步长会发生什么呢?

# Step sizes to be tested
alphas = [0.01, 0.25, 0.3, 0.35, 0.4]

# Store the iterations for each step size
X_estimates, Y_estimates, Z_estimates = [], [], []

# Plot f(x) at each iteration for different step sizes
fig, ax = plt.subplots(len(alphas), figsize=(8, 9))
fig.suptitle('$f(x)$ at each iteration for different $α$')

# For each step size
for i, alpha in enumerate(alphas):

    # Steepest descent
    estimate, iters = steepest_descent(
      df, x0 = np.array([-5, -5]), alpha=alpha, max_iter=3000)

    # Print results
    print('Input alpha: {}'.format(alpha))
    print('\t- Final results: {}'.format(estimate[-1].round(1)))
    print('\t- N° steps: {}'.format(iters))

    # Store for 3D plots
    X_estimates.append(estimate[:, 0])
    Y_estimates.append(estimate[:, 1])  
    Z_estimates.append(f(np.array([estimate[:, 0], estimate[:, 1]])))

    # Subplot of f(x) at each iteration for current alpha
    ax[i].plot([f(var) for var in estimate], label='alpha: '+str(alpha))
    ax[i].axhline(y=0, color='r', alpha=0.7, linestyle='dashed')
    ax[i].set_xlabel('Number of iterations')
    ax[i].set_ylabel('$f(x)$')
    ax[i].set_ylim([-10, 200])
    ax[i].legend(loc='upper right')
Input alpha: 0.01
 - Final results: [4.5 2.3]
 - N° steps: 2516
Input alpha: 0.25
 - Final results: [4.5 2.3]
 - N° steps: 88
Input alpha: 0.3
 - Final results: [4.5 2.3]
 - N° steps: 71
Input alpha: 0.35
 - Final results: [4.5 2.3]
 - N° steps: 93
Input alpha: 0.4
 - Final results: [ 4.5 -5\. ]
 - N° steps: 3000

图片来源于作者。

当步长过大(α = 0.4)时,算法不收敛:xₖ 一直振荡而没有达到最小值,直到达到最大迭代次数。

我们可以通过观察每次迭代中的点 x 来更好地理解这种行为:

fig = plt.figure(figsize=(25, 60))

# For each step size
for i in range(0, len(alphas)):

    # First subplot
    ax = fig.add_subplot(5, 2, (i*2)+1, projection='3d')
    ax.contour3D(X, Y, Z, 60, cmap='viridis')
    ax.plot(X_estimates[i], Y_estimates[i], Z_estimates[i], color='red', label='alpha: '+str(alphas[i]) , linewidth=3)
    ax.scatter(min_x0, min_x1, min_z, marker='o', color='red', linewidth=10)
    ax.set_xlabel('$x_{0}$')
    ax.set_ylabel('$x_{1}$')
    ax.set_zlabel('$f(x)$')
    ax.view_init(20, 20)
    plt.legend(prop={'size': 15})

    # Second third
    ax = fig.add_subplot(5, 2, (i*2)+2, projection='3d')
    ax.contour3D(X, Y, Z, 60, cmap='viridis')
    ax.plot(X_estimates[i], Y_estimates[i], Z_estimates[i], color='red', label='alpha: '+str(alphas[i]) , linewidth=3)
    ax.scatter(min_x0, min_x1, min_z, marker='o', color='red', linewidth=10)
    ax.set_xlabel('$x_{0}$')
    ax.set_ylabel('$x_{1}$')
    ax.set_zlabel('$f(x)$')
    ax.axes.zaxis.set_ticklabels([])
    ax.view_init(90, -90)
    plt.legend(prop={'size': 15})

图片来源于作者。

为了保证最陡下降法的收敛性,我们需要通过线搜索迭代更新 α

3.3. 使用 Armijo 条件的线搜索

让我们通过添加 Armijo 规则来修改我们之前的方法。在最陡下降循环中,计算下一个点 x - α ⋅ ∇f(x) 之前,我们需要选择一个合适的步长:我们从初始猜测 α = 1 开始,逐步将其值减半,直到满足 Armijo 条件。这个过程称为 回溯线搜索

def line_search(step, x, gradient_x, c = 1e-4, tol = 1e-8):
    '''
    Inexact line search where the step length is updated through the Armijo condition:
    $ f (x_k + α * p_k ) ≤ f ( x_k ) + c * α * ∇ f_k^T * p_k $

    Args:
      - step: starting alpha value
      - x: current point
      - gradient_x: gradient of the current point
      - c: constant value (default: 1e-4)
      - tol: tolerance value (default: 1e-6)
    Out:
      - New value of step: the first value found respecting the Armijo condition
    '''
    f_x = f(x)
    gradient_square_norm = np.linalg.norm(gradient_x)**2

    # Until the sufficient decrease condition is met 
    while f(x - step * gradient_x) >= (f_x - c * step * gradient_square_norm):

        # Update the stepsize (backtracking)
        step /= 2

        # If the step size falls below a certain tolerance, exit the loop
        if step < tol:
            break

    return step

def steepest_descent(gradient, x0 = np.zeros(2), max_iter = 10000, tolerance = 1e-10): 
    '''
    Steepest descent with alpha updated through line search (Armijo condition).

    Args:
      - gradient: gradient of the objective function
      - x0: initial guess for x_0 and x_1 (default values: zero) <numpy.ndarray>
      - max_iter: maximum number of iterations (default: 10000)
      - tolerance: minimum gradient magnitude at which the algorithm stops (default: 1e-10)

    Out:
      - results: <numpy.ndarray> with x_0 and x_1 values at each iteration
      - number of steps: <int>
    '''

    # Prepare list to store results at each iteration 
    results = np.array([])

    # Evaluate the gradient at the starting point 
    gradient_x = gradient(x0)

    # Initialize the steps counter 
    steps_count = 0

    # Set the initial point 
    x = x0 
    results = np.append(results, x, axis=0)

    # Iterate until the gradient is below the tolerance or maximum number of iterations is reached
    # Stopping criterion: inf norm of the gradient (max abs)
    while any(abs(gradient_x) > tolerance) and steps_count < max_iter:

        # Update the step size through the Armijo condition
        # Note: the first value of alpha is commonly set to 1
        alpha = line_search(1, x, gradient_x)

        # Update the current point by moving in the direction of the negative gradient 
        x = x - alpha * gradient_x

        # Store the result
        results = np.append(results, x, axis=0)

        # Evaluate the gradient at the new point 
        gradient_x = gradient(x) 

        # Increment the iteration counter 
        steps_count += 1 

    # Return the steps taken and the number of steps
    return results.reshape(-1, 2), steps_count

现在让我们使用线搜索优化目标函数:

# Steepest descent
points, iters = steepest_descent(
  df, x0 = np.array([-9, -9]))

# Found minimizer
minimizer = points[-1].round(1)

# Print results
print('- Final results: {}'.format(minimizer))
print('- N° steps: {}'.format(iters))

# Steepest descent steps
X_estimate, Y_estimate = points[:, 0], points[:, 1] 
Z_estimate = f(np.array([X_estimate, Y_estimate]))

# Plot
fig = plt.figure(figsize=(20, 20))

# First subplot
ax = fig.add_subplot(1, 2, 1, projection='3d')
ax.contour3D(X, Y, Z, 60, cmap='viridis')
ax.plot(X_estimate, Y_estimate, Z_estimate, color='red', linewidth=3)
ax.scatter(min_x0, min_x1, min_z, marker='o', color='red', linewidth=10)
ax.set_xlabel('$x_{0}$')
ax.set_ylabel('$x_{1}$')
ax.set_zlabel('$f(x)$')
ax.view_init(20, 20)

# Second subplot
ax = fig.add_subplot(1, 2, 2, projection='3d')
ax.contour3D(X, Y, Z, 60, cmap='viridis')
ax.plot(X_estimate, Y_estimate, Z_estimate, color='red', linewidth=3)
ax.scatter(min_x0, min_x1, min_z, marker='o', color='red', linewidth=10)
ax.set_xlabel('$x_{0}$')
ax.set_ylabel('$x_{1}$')
ax.set_zlabel('$f(x)$')
ax.axes.zaxis.set_ticklabels([])
ax.view_init(90, -90);
- Final results: [4.5 2.3]
- N° steps: 55

图片来源于作者。

我们注意到在 55 步中达到了最小值,而使用常数步长 α 则导致了更多的迭代,或者没有收敛。

4. 结论

在这篇文章中,我们介绍并实现了最陡下降法,并在两个变量的二次函数上进行了测试。特别是,我们展示了如何使用足够减少条件(Armijo)迭代更新步长。

带有 Armijo 线搜索的最陡下降法保证了收敛,但通常较慢,因为它需要大量的迭代。

在未来的帖子中,我们将探讨通过改变线性搜索策略并提供不同的步长初始化方法来修改这个基本算法。

从零实现 Vision Transformer (ViT)

原文:towardsdatascience.com/implementing-vision-transformer-vit-from-scratch-3e192c6155f0?source=collection_archive---------9-----------------------#2023-03-07

通过从零实现 Vision Transformer (ViT) 了解其工作原理

Tin Nguyen Tin Nguyen Towards Data Science Towards Data Science

·

关注 发表在 Towards Data Science ·10 分钟阅读·2023 年 3 月 7 日

--

Vision Transformer (ViT) 是将 Transformer 模型适配于计算机视觉任务的一种方法。它由 Google 研究人员在 2020 年提出,并因其在各种图像分类基准测试中的卓越表现而获得广泛关注。ViT 已显示出在多个计算机视觉任务中实现了最先进的性能,并引起了计算机视觉社区的极大兴趣。

在这篇文章中,我们将从头开始实现 ViT 用于图像分类,使用 PyTorch。我们还将用 CIFAR-10 数据集训练我们的模型,这是一个流行的图像分类基准。通过这篇文章,你应该能很好地理解 ViT 的工作原理以及如何将其应用于自己的计算机视觉项目。

实现的代码可以在这个仓库中找到。

ViT 架构概述

改编自 arxiv.org/abs/2010.11929

ViT 的架构灵感来源于 BERT,一个仅包含编码器的 transformer 模型,通常用于 NLP 监督学习任务,如文本分类或命名实体识别。ViT 的主要思想是图像可以被视为一系列小块,这些小块可以在 NLP 任务中作为 token 处理。

输入图像被分割成小块,然后将这些小块展平为向量序列。这些向量随后由一个 transformer 编码器处理,使得模型能够通过自注意力机制学习小块之间的交互。transformer 编码器的输出被送入分类层,输出输入图像的预测类别。

在接下来的部分,我们将逐一实现模型的每个组件,并使用 PyTorch 进行实现。这将帮助我们理解 ViT 模型的工作原理及其在计算机视觉任务中的应用。

将图像转换为嵌入

为了将输入图像馈送到 Transformer 模型中,我们需要将图像转换为向量序列。这通过将图像拆分成不重叠的小块,然后将这些小块线性投影以获得每个小块的固定大小嵌入向量来完成。我们可以使用 PyTorch 的nn.Conv2d层来实现这一点:

class PatchEmbeddings(nn.Module):
    """
    Convert the image into patches and then project them into a vector space.
    """

    def __init__(self, config):
        super().__init__()
        self.image_size = config["image_size"]
        self.patch_size = config["patch_size"]
        self.num_channels = config["num_channels"]
        self.hidden_size = config["hidden_size"]
        # Calculate the number of patches from the image size and patch size
        self.num_patches = (self.image_size // self.patch_size) ** 2
        # Create a projection layer to convert the image into patches
        # The layer projects each patch into a vector of size hidden_size
        self.projection = nn.Conv2d(self.num_channels, self.hidden_size, kernel_size=self.patch_size, stride=self.patch_size)

    def forward(self, x):
        # (batch_size, num_channels, image_size, image_size) -> (batch_size, num_patches, hidden_size)
        x = self.projection(x)
        x = x.flatten(2).transpose(1, 2)
        return x

kernel_size=self.patch_sizestride=self.patch_size 是为了确保层的过滤器应用于不重叠的小块。

在小块被转换为嵌入序列后,[CLS] token 被添加到序列的开头,它将在分类层中用于对图像进行分类。[CLS] token 的嵌入在训练过程中学习。

由于来自不同位置的小块可能对最终预测的贡献不同,我们还需要一种方法将小块的位置编码到序列中。我们将使用可学习的位置嵌入将位置信息添加到嵌入中。这类似于在 NLP 任务的 Transformer 模型中使用位置嵌入的方式。

class Embeddings(nn.Module):
    """
    Combine the patch embeddings with the class token and position embeddings.
    """

    def __init__(self, config):
        super().__init__()
        self.config = config
        self.patch_embeddings = PatchEmbeddings(config)
        # Create a learnable [CLS] token
        # Similar to BERT, the [CLS] token is added to the beginning of the input sequence
        # and is used to classify the entire sequence
        self.cls_token = nn.Parameter(torch.randn(1, 1, config["hidden_size"]))
        # Create position embeddings for the [CLS] token and the patch embeddings
        # Add 1 to the sequence length for the [CLS] token
        self.position_embeddings = \
            nn.Parameter(torch.randn(1, self.patch_embeddings.num_patches + 1, config["hidden_size"]))
        self.dropout = nn.Dropout(config["hidden_dropout_prob"])

    def forward(self, x):
        x = self.patch_embeddings(x)
        batch_size, _, _ = x.size()
        # Expand the [CLS] token to the batch size
        # (1, 1, hidden_size) -> (batch_size, 1, hidden_size)
        cls_tokens = self.cls_token.expand(batch_size, -1, -1)
        # Concatenate the [CLS] token to the beginning of the input sequence
        # This results in a sequence length of (num_patches + 1)
        x = torch.cat((cls_tokens, x), dim=1)
        x = x + self.position_embeddings
        x = self.dropout(x)
        return x

在这一步,输入图像被转换为带有位置信息的嵌入序列,并准备好输入 transformer 层。

多头注意力

在深入 transformer 编码器之前,我们首先探索多头注意力模块,这是其核心组件。多头注意力用于计算输入图像中不同块之间的交互。多头注意力由多个注意力头组成,每个头部是一个单一的注意力层。

让我们实现多头注意力模块的一个头部。该模块接受一个嵌入序列作为输入,并为每个嵌入计算查询、键和值向量。查询和键向量随后用于计算每个令牌的注意力权重。注意力权重被用于使用值向量的加权和计算新的嵌入。我们可以将这种机制视为数据库查询的软版本,其中查询向量在数据库中找到最相关的键向量,并检索值向量以计算查询输出。

class AttentionHead(nn.Module):
    """
    A single attention head.
    This module is used in the MultiHeadAttention module.
    """
    def __init__(self, hidden_size, attention_head_size, dropout, bias=True):
        super().__init__()
        self.hidden_size = hidden_size
        self.attention_head_size = attention_head_size
        # Create the query, key, and value projection layers
        self.query = nn.Linear(hidden_size, attention_head_size, bias=bias)
        self.key = nn.Linear(hidden_size, attention_head_size, bias=bias)
        self.value = nn.Linear(hidden_size, attention_head_size, bias=bias)

        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        # Project the input into query, key, and value
        # The same input is used to generate the query, key, and value,
        # so it's usually called self-attention.
        # (batch_size, sequence_length, hidden_size) -> (batch_size, sequence_length, attention_head_size)
        query = self.query(x)
        key = self.key(x)
        value = self.value(x)
        # Calculate the attention scores
        # softmax(Q*K.T/sqrt(head_size))*V
        attention_scores = torch.matmul(query, key.transpose(-1, -2))
        attention_scores = attention_scores / math.sqrt(self.attention_head_size)
        attention_probs = nn.functional.softmax(attention_scores, dim=-1)
        attention_probs = self.dropout(attention_probs)
        # Calculate the attention output
        attention_output = torch.matmul(attention_probs, value)
        return (attention_output, attention_probs)

所有注意力头的输出随后被拼接并线性映射,以获得多头注意力模块的最终输出。

class MultiHeadAttention(nn.Module):
    """
    Multi-head attention module.
    This module is used in the TransformerEncoder module.
    """

    def __init__(self, config):
        super().__init__()
        self.hidden_size = config["hidden_size"]
        self.num_attention_heads = config["num_attention_heads"]
        # The attention head size is the hidden size divided by the number of attention heads
        self.attention_head_size = self.hidden_size // self.num_attention_heads
        self.all_head_size = self.num_attention_heads * self.attention_head_size
        # Whether or not to use bias in the query, key, and value projection layers
        self.qkv_bias = config["qkv_bias"]
        # Create a list of attention heads
        self.heads = nn.ModuleList([])
        for _ in range(self.num_attention_heads):
            head = AttentionHead(
                self.hidden_size,
                self.attention_head_size,
                config["attention_probs_dropout_prob"],
                self.qkv_bias
            )
            self.heads.append(head)
        # Create a linear layer to project the attention output back to the hidden size
        # In most cases, all_head_size and hidden_size are the same
        self.output_projection = nn.Linear(self.all_head_size, self.hidden_size)
        self.output_dropout = nn.Dropout(config["hidden_dropout_prob"])

    def forward(self, x, output_attentions=False):
        # Calculate the attention output for each attention head
        attention_outputs = [head(x) for head in self.heads]
        # Concatenate the attention outputs from each attention head
        attention_output = torch.cat([attention_output for attention_output, _ in attention_outputs], dim=-1)
        # Project the concatenated attention output back to the hidden size
        attention_output = self.output_projection(attention_output)
        attention_output = self.output_dropout(attention_output)
        # Return the attention output and the attention probabilities (optional)
        if not output_attentions:
            return (attention_output, None)
        else:
            attention_probs = torch.stack([attention_probs for _, attention_probs in attention_outputs], dim=1)
            return (attention_output, attention_probs)

Transformer 编码器

transformer 编码器由一系列 transformer 层组成。每个 transformer 层主要由我们刚刚实现的多头注意力模块和一个前馈网络组成。为了更好地扩展模型和稳定训练,transformer 层中添加了两个层归一化层和跳过连接。

让我们实现一个 transformer 层(在代码中称为Block,因为它是 transformer 编码器的构建块)。我们将从前馈网络开始,它是一个简单的两层 MLP,中间有 GELU 激活函数。

class MLP(nn.Module):
    """
    A multi-layer perceptron module.
    """

    def __init__(self, config):
        super().__init__()
        self.dense_1 = nn.Linear(config["hidden_size"], config["intermediate_size"])
        self.activation = NewGELUActivation()
        self.dense_2 = nn.Linear(config["intermediate_size"], config["hidden_size"])
        self.dropout = nn.Dropout(config["hidden_dropout_prob"])

    def forward(self, x):
        x = self.dense_1(x)
        x = self.activation(x)
        x = self.dense_2(x)
        x = self.dropout(x)
        return x

我们已经实现了多头注意力和 MLP,可以将它们结合起来创建 transformer 层。跳过连接和层归一化被应用于每一层的输入。

class Block(nn.Module):
    """
    A single transformer block.
    """

    def __init__(self, config):
        super().__init__()
        self.attention = MultiHeadAttention(config)
        self.layernorm_1 = nn.LayerNorm(config["hidden_size"])
        self.mlp = MLP(config)
        self.layernorm_2 = nn.LayerNorm(config["hidden_size"])

    def forward(self, x, output_attentions=False):
        # Self-attention
        attention_output, attention_probs = \
            self.attention(self.layernorm_1(x), output_attentions=output_attentions)
        # Skip connection
        x = x + attention_output
        # Feed-forward network
        mlp_output = self.mlp(self.layernorm_2(x))
        # Skip connection
        x = x + mlp_output
        # Return the transformer block's output and the attention probabilities (optional)
        if not output_attentions:
            return (x, None)
        else:
      return (x, attention_probs)

transformer 编码器将多个 transformer 层按顺序堆叠在一起:

class Encoder(nn.Module):
    """
    The transformer encoder module.
    """

    def __init__(self, config):
        super().__init__()
        # Create a list of transformer blocks
        self.blocks = nn.ModuleList([])
        for _ in range(config["num_hidden_layers"]):
            block = Block(config)
            self.blocks.append(block)

    def forward(self, x, output_attentions=False):
        # Calculate the transformer block's output for each block
        all_attentions = []
        for block in self.blocks:
            x, attention_probs = block(x, output_attentions=output_attentions)
            if output_attentions:
                all_attentions.append(attention_probs)
        # Return the encoder's output and the attention probabilities (optional)
        if not output_attentions:
            return (x, None)
        else:
            return (x, all_attentions)

用于图像分类的 ViT

将图像输入到嵌入层和 transformer 编码器后,我们获得了图像块和[CLS]标记的新嵌入。此时,嵌入经过 transformer 编码器处理后应该具有一些用于分类的有用信号。类似于 BERT,我们将仅使用[CLS]标记的嵌入传递给分类层。

分类层是一个全连接层,它接受[CLS]嵌入作为输入,并输出每张图像的 logits。以下代码实现了用于图像分类的 ViT 模型:

class ViTForClassfication(nn.Module):
    """
    The ViT model for classification.
    """

    def __init__(self, config):
        super().__init__()
        self.config = config
        self.image_size = config["image_size"]
        self.hidden_size = config["hidden_size"]
        self.num_classes = config["num_classes"]
        # Create the embedding module
        self.embedding = Embeddings(config)
        # Create the transformer encoder module
        self.encoder = Encoder(config)
        # Create a linear layer to project the encoder's output to the number of classes
        self.classifier = nn.Linear(self.hidden_size, self.num_classes)
        # Initialize the weights
        self.apply(self._init_weights)

    def forward(self, x, output_attentions=False):
        # Calculate the embedding output
        embedding_output = self.embedding(x)
        # Calculate the encoder's output
        encoder_output, all_attentions = self.encoder(embedding_output, output_attentions=output_attentions)
        # Calculate the logits, take the [CLS] token's output as features for classification
        logits = self.classifier(encoder_output[:, 0])
        # Return the logits and the attention probabilities (optional)
        if not output_attentions:
            return (logits, None)
        else:
            return (logits, all_attentions)

要训练模型,可以遵循训练分类模型的标准步骤。你可以在这里找到训练脚本。

结果

由于目标不是实现最先进的性能,而是展示模型的工作原理,我训练的模型远小于论文中描述的原始 ViT 模型,这些模型至少有 12 层,隐藏层大小为 768。我用于训练的模型配置是:

{
    "patch_size": 4,
    "hidden_size": 48,
    "num_hidden_layers": 4,
    "num_attention_heads": 4,
    "intermediate_size": 4 * 48,
    "hidden_dropout_prob": 0.0,
    "attention_probs_dropout_prob": 0.0,
    "initializer_range": 0.02,
    "image_size": 32,
    "num_classes": 10,
    "num_channels": 3,
    "qkv_bias": True,
}

该模型在 CIFAR-10 数据集上训练了 100 轮,批量大小为 256。学习率设置为 0.01,并且没有使用学习率调整。经过 100 轮训练后,模型达到了 75.5% 的准确率。下图展示了训练期间的训练损失、测试损失和测试集上的准确率。

下图展示了模型对一些测试图像的注意力图。你可以看到模型能够识别不同类别的对象。它学会了关注对象并忽略背景。

结论

在这篇文章中,我们学习了 Vision Transformer 的工作原理,从嵌入层到变换器编码器,最后到分类层。我们还学习了如何使用 PyTorch 实现模型的每个组件。

由于该实现不用于生产环境,如果你打算训练全尺寸模型或在大型数据集上训练,建议使用更成熟的变换器库,如 HuggingFace

最初发布于 https://tintn.github.io 2023 年 3 月 7 日。

使用 Python 的重要性采样

原文:towardsdatascience.com/importance-sampling-with-python-93b03eb9ca22

图片由 Edge2Edge Media 提供,来源于 Unsplash

学习如何在只能访问另一个分布时从一个分布中采样

Marcello PolitiTowards Data Science Marcello Politi

·发表于 Towards Data Science ·阅读时间 5 分钟·2023 年 5 月 10 日

--

介绍

在数据科学家必须知道的各种采样方法中,最重要的方法之一就是所谓的“重要性采样”。

这种方法使我们能够即使我们实际上只能从另一个分布中采样,也能从一个分布中采样! 让我们看看它是如何工作的。

重要性采样

假设,例如,从分布 g(x) 中采样是不可行的,因为,例如,这样做成本太高。但与此同时,我们有一个可以采样的分布 f(x),我们称之为重要性分布。

我们可以通过对分布 f(x) 进行采样来计算我们真正感兴趣的分布 g(x) 的统计信息。让我们看看如何做。

想象一下,我们有一个表示每个骰子面的概率的分布 f(x)。如果骰子是“公平的”,每一面都有 1/6 的概率,因此我们可以将分布表示如下。

公平分配(图片由作者提供)

我们还有另一个分布 g(x),我们希望从中采样,但由于某种原因被阻止。在这种情况下,骰子是不公平的,因此分布是有偏的。所以有些面会比其他面有更高的概率。

偏倚分布(图片由作者提供)

使用我们的数学概念,我们能够计算每个分布的期望值。因此,例如,第一种分布 f(x) 的 E[x] 将是:

期望值 f(x)(图片由作者提供)

同样的原理自然适用于 g(x)。

预期值 g(x)(作者提供的图片)

现在想象我们想通过抽样从我们的总体中计算统计量。例如,我们想计算掷 n 次骰子的结果的平均值。

我们掷一个公平骰子 n 次,我们知道根据中心极限定理,随着 n 的增加,这个平均值将趋向于预期值。

作者提供的图片

现在我们想通过掷 n 次另一个骰子,即不公平的骰子,来计算相同的统计量,那个骰子具有不同的概率分布。问题是这个骰子不存在,因此我们无法实验!

那我们该如何做到这一点呢?我们可以通过始终使用公平骰子来计算这个统计量,但使用一个“技巧”。

我们知道如何计算这个骰子的预期值,使用分布 g(x)。接下来我们要做的是乘以并除以相同的数量 f(x)。

作者提供的图片

现在如果我们将 x 叫做求和中的第一部分,这个值实际上是 f(x) 的预期值,其中 x 是加权的。

作者提供的图片

所以我们可以使用之前的相同思路,进行一个抽样,其中 我们按 g(x)/f(x) 权重每次抽取,结果应该接近 g 的预期值。

一切都很好,但这真的有效吗?让我们做一些实验!

让我们编码吧!

首先,我们导入一些需要的库。

import numpy as np
import matplotlib.pyplot as plt

现在我们用两个 numpy 数组表示两个骰子的分布。第一个是公平骰子,每一面的概率相同。第二个则是每一面概率不同的骰子。

f = np.array([1/6, 1/6, 1/6, 1/6, 1/6, 1/6])
g = np.array([0.4, 0.3 ,0.1 ,0.1, 0.06, 0.04])

如果你愿意,可以使用 Matplotlib 绘制分布图。

plt.bar(np.arange(1,7), f)
plt.show()

作者提供的图片

plt.bar(np.arange(1,7), g)
plt.show()

作者提供的图片

现在让我们定义一个函数来计算实验的经验均值。也就是说,我们掷一个骰子 n 次,然后将获得的结果总和除以 n。选择的数字 n 越大,经验均值就会越接近预期值。

def compute_avg(distr, n = 10_000):
  mean = 0
  for i in range(n):
    mean += np.random.choice(a = np.arange(1,7),  p=distr)
  print(f"average from sampling: {mean/n}")

如果你现在在两个分布 f 和 g 上计算这个经验均值,你应该会找到一个类似于预期值的均值。

compute_avg(f)
compute_avg(g)

问题在于我们必须假设一个你无法直接计算 g 的经验均值的情况,因为例如,你没有这样的骰子,因此无法实验。你唯一拥有的骰子是公平骰子,尽管如此,你知道两个骰子的分布。

然后,正如我们之前看到的,我们可以创建一个从分布 f 中抽样的函数,然后通过之前展示的方式加权提取的数字,我们可以计算出好像它是从分布 g 中提取的均值。现在我们要做的就是编写这个函数,看看它是否真的有效。

def importance_sampling(f, g, n = 10_000):
  mean = 0
  for i in range(n):
     x = np.random.choice(a = np.arange(1,7),  p=f)
     weight = g[x-1]/f[x-1]
     x = x*weight
     mean += x
  print(f"average from sampling: {mean/n}")

因此,我们在之前提到的 f 和 g 分布上进行了 n = 10,000 次的重要性采样。

importance_sampling(f,g)

这个函数的结果是 2.24920,非常接近预期值!所以我们通过编写 Python 代码具体展示了这个方法的有效性!

最后的想法

在这篇文章中,我们探讨了数据科学家最重要的采样技术之一。重要性采样允许我们从一个分布中进行采样,即使我们只能访问另一个分布。如果例如从目标分布中采样成本过高,或者由于任何原因不可能进行采样,也可能发生这种情况。我希望你在这篇文章中学到了有用的东西,如果你对这类文章感兴趣,可以在 Medium 上关注我!😉

结束

马尔切洛·波利提

Linkedin, Twitter, Website

如何提升 Python 函数的性能

原文:towardsdatascience.com/improve-python-function-performance-587cfd4fd511

加速 Python 中频繁调用的函数

Giorgos MyrianthousTowards Data Science Giorgos Myrianthous

·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 3 月 10 日

--

图片由 Esteban Lopez 提供,来源于 Unsplash

在当今数据处理量以空前速度增长的世界中,拥有高效和优化的代码变得比以往任何时候都重要。作为一种流行的编程语言,Python 提供了多种内置工具来优化代码性能。其中一个工具是 lru_cache 装饰器,它可以用来缓存函数的结果,从而提高性能。

在这篇文章中,我们将探讨如何使用 lru_cache 装饰器,并且了解它如何帮助你编写更快、更高效的代码来处理频繁调用的函数。

缓存简述

缓存是一种广泛采用的方法,用于提升计算机程序的性能。它涉及在指定的时间范围内临时存储计算成本高或经常访问的信息。通过缓存,开发者可以高效地存储先前计算的结果,从而减少重新计算所需的时间和计算资源。这个过程可以显著改善系统的响应时间和整体性能。

缓存可以通过各种数据结构实现,例如数组、哈希表和链表,也可以通过不同的策略进行管理,例如最近最少使用(LRU)和先进先出(FIFO)。

管理缓存大小可以是其实现中的一个关键方面。没有定期缩小缓存的机制,可能会导致内存使用不断增加,最终导致应用程序崩溃。为了克服这个挑战,程序员必须仔细考虑最适合其特定需求的策略或方法。

在某些情况下,适应性策略可能更为合适,这些策略会根据变化的工作负载模式进行调整。此外,缓存大小管理可以通过不同的技术来实现,例如设置最大大小、实施基于时间的过期策略,或使用结合多种策略的混合方法。

最终,选择合适的缓存管理策略需要仔细考虑诸如性能、内存使用和应用程序的具体需求等因素。总体而言,缓存是一种有效的技术,可以让程序员优化代码并改善用户体验。

使用 lru_cache 装饰器创建最近最少使用缓存

lru_cache 装饰器是语言标准库的一部分,可以从 functools 模块中导入。

装饰器使用一种称为最近最少使用(LRU)的策略来优先处理最近使用过的对象。每当通过使用 LRU 策略的缓存访问一个对象时,缓存会将其放在最上面,同时将其他对象向下移动一个位置。然而,如果缓存达到其最大大小,则会移除最近最少使用的对象,以腾出空间供新对象使用。这确保了经常使用的对象保持在缓存中,而不常使用的对象则会逐渐被挤出。

.. 经常使用的对象保持在缓存中,而不常使用的对象则会逐渐被挤出

实现 LRU 的缓存对于处理计算密集型或 I/O 绑定的函数非常有用,这些函数经常以相同的参数被调用。

Python 中的 lru_cache 装饰器通过维护一个线程安全的字典形式缓存来运作。在调用被装饰的函数时,缓存中会创建一个新条目,其中函数调用参数对应字典的键,返回值对应字典的值。后续使用相同参数调用函数时不会计算新结果,而是直接检索缓存值。这一特性避免了冗余计算,从而提高了整体性能。

由于使用字典来缓存结果,函数的定位和关键字参数必须是可哈希的

  • Python 文档

lru_cache的大小 可以通过maxsize参数进行调整,默认为128。该值指定缓存可以在任何时间保持的最大条目数。一旦缓存达到最大大小并且有新的调用,装饰器将丢弃最少使用的条目,以腾出空间给最新的条目。这确保缓存不会超过指定的限制,从而防止过度的内存消耗并提高性能。如果maxsize设置为None,则LRU功能将被禁用,这意味着缓存可以无限增长。

重要的是要注意,当两个函数调用具有相同的关键字参数但顺序不同时,将创建两个独立的缓存条目。例如,调用func(x=10, y=5)func(y=5, x=10)将导致缓存中出现两个不同的条目,即使这两个调用返回相同的值。

此外,lru_cache装饰器接受另一个名为**typed**的参数,默认为False

如果*typed*设置为true,不同类型的函数参数将被单独缓存。如果*typed*false,实现通常会将它们视为等效调用,并只缓存一个结果。

Python 文档

例如,当typed=True时,调用func(a=4)a持有整数值)和func(a=4.)a现在持有浮点值),将创建两个不同的缓存条目。不过请注意,一些类型如strint即使在typed设置为False时,也可能会在不同的缓存条目中缓存。

现在假设我们有一个非常简单的 Python 函数,接受两个参数并返回它们的和。

def add_numbers(a, b):
    return a + b

现在假设函数被lru_cache装饰,并被多次调用,如下所述:

from functools import lru_cache

@lru_cache(maxsize=3)
def add_numbers(a, b):
    return a + b

add_numbers(a=10, b=15)
add_numbers(a=10, b=10)
add_numbers(a=3, b=15)
add_numbers(a=20, b=21)
add_numbers(a=3, b=15) 

下图展示了具有maxsize=3的 LRU 缓存如何随时间维持,以及在缓存命中或未命中时的行为,以及当当前大小达到指定最大值时如何处理的情况。

LRU 缓存将优先处理最近使用的函数调用 — 来源:作者

  1. 最初,当函数被调用时,使用参数a=10b=15,缓存为空,发生缓存未命中。函数执行后,输入参数和结果值都会存储在缓存中。

  2. 在下次调用时,当函数被调用时,使用参数a=10b=10,再次发生缓存未命中,因为这个参数组合在缓存中未找到。函数执行后,创建一个新的缓存条目并放在顶部。

  3. 随后,当函数被调用时,使用参数a=3b=15,发生了另一个缓存未命中。函数执行后,输入参数和结果值被存储在缓存中。

  4. 在下一个调用中,当函数使用a=20b=1作为参数时,会发生缓存未命中,并且缓存已达到最大容量。这意味着最久未使用的条目被移除,其余条目向下移动一个位置。执行函数后,最新的条目被添加到缓存中,并放在顶部。

  5. 最后,当函数再次以a=3b=15作为参数被调用时,会发生缓存命中,因为该条目现在已存储在缓存中。返回的值从缓存中获取,而不是执行函数。然后缓存条目被移到顶部。

如何以及何时使用 lru_cache 装饰器

在充分理解了缓存和 LRU 缓存策略后,是时候深入探讨 lru_cache,学习如何充分利用它,同时评估它的影响。

对于本教程的目的,我们将研究一个更复杂的函数,这将从缓存中受益匪浅,特别是lru_cache装饰器。我们的fibonacci函数可以用来计算斐波那契数

在数学中,斐波那契数,通常表示为Fn,形成了一个序列,即斐波那契序列,其中每个数字是前两个数字的和。该序列通常从 0 和 1 开始,尽管一些作者从 1 和 1 开始序列,或者有时(如斐波那契所做)从 1 和 2 开始。以 0 和 1 为起点,该序列的前几个值是:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144.

值得注意的是,下面的函数递归地计算斐波那契数。尽管我们可以引入记忆化来进一步优化解决方案,但这超出了本教程的范围。此外,为了简便起见,我将保持简单,让经验较少的读者也能跟上。

实现未完全优化的事实对我们的目的很有帮助,因为它展示了lru_cache如何提升计算密集型函数调用的性能。

def fibonacci(n):
    if n < 0:
        raise ValueError('Invalid input')

    # Base case
    if n <= 1:
        return n

    return fibonacci(n - 1) + fibonacci(n - 2)

为了评估我们函数处理多个不同输入所需的时间,我们可以使用timeit模块。我们将利用 timeit 模块来测量fibonacci函数的性能,并从 5 次重复调用相同参数的函数中取最小时间。

[在测量执行时间时] 使用min()而不是时间的平均值。这是我、Tim Peters 和 Guido van Rossum 的建议。最快的时间代表了在缓存加载且系统不忙于其他任务时算法的最佳性能。所有时间都有噪音——最快的时间噪音最小。很容易证明,最快的时间是最可重复的,因此在计时两个不同实现时最有用。

— 雷蒙德·赫廷格 在 StackOverflow

# module test.py

from timeit import repeat

setup = """
def fibonacci(n):
    if n < 0:
        raise ValueError('Invalid input')

    # Base case
    if n <= 1:
        return n

    return fibonacci(n - 1) + fibonacci(n - 2)
"""

min_timing = min(repeat(setup=setup, stmt='fibonacci(30)', number=5))
print(f'Min Timing: {round(min_timing, 2)}s')

从本质上讲,我们的脚本将测量执行第 30 个斐波那契数的时间。经过 5 次不同的调用,我的机器上最少的时间是 1.39 秒。

$ python3 test.py 
Min Timing: 1.39s

fibonacci 函数是确定性的,这意味着它在给定相同输入的情况下总是产生相同的输出。因此,我们可以利用缓存的概念。通过添加 @lru_cache 装饰器,函数对于给定输入的输出现在被缓存,如果函数再次以相同的输入调用,它将返回缓存的结果而不是重新计算。这可以显著加快函数的执行时间,特别是当函数多次以相同输入值调用时。

from functools import lru_cache

@lru_cache
def fibonacci(n):
    if n < 0:
        raise ValueError('Invalid input')

    # Base case
    if n <= 1:
        return n

    return fibonacci(n - 1) + fibonacci(n - 2)

现在让我们对新版本的 fibonacci 函数进行计时:

# module test.py

from timeit import repeat

setup = """
from functools import lru_cache

@lru_cache
def fibonacci(n):
    if n < 0:
        raise ValueError('Invalid input')

    # Base case
    if n <= 1:
        return n

    return fibonacci(n - 1) + fibonacci(n - 2)
"""

min_timing = min(repeat(setup=setup, stmt='fibonacci(30)', number=5))
print(f'Min Timing: {min_timing}s')

在我的机器上,最小的时间接近零。fibonacci(30) 仅在第一次迭代时执行。对于后续的迭代,检索了此函数调用的缓存结果,这是一项廉价的操作。

$ python3 test.py 
Min Timing: 7.790978997945786e-06s

最终想法

总之,在今天的数据驱动世界中,优化代码性能比以往任何时候都更重要。在 Python 中实现这一点的一种方法是使用 lru_cache 装饰器,它可以缓存频繁调用函数的结果并提高其性能。

一般来说,缓存涉及暂时存储计算成本高或频繁访问的信息,从而减少重新计算所需的时间和计算资源。该装饰器使用一种称为最近最少使用(LRU)的策略来优先考虑最近使用的对象,并在缓存达到最大大小时丢弃最少使用的对象。

lru_cache 装饰器是优化 Python 代码性能的有效工具,通过缓存计算密集型或 I/O 密集型函数的结果。

成为会员 并阅读 Medium 上的每一篇故事。你的会员费直接支持我和你阅读的其他作者。你还将获得对 Medium 上每个故事的完全访问权限。

[## 使用我的推荐链接加入 Medium — Giorgos Myrianthous

作为 Medium 会员,你的会员费的一部分将转给你阅读的作者,你将获得对每个故事的完全访问权限……

gmyrianthous.medium.com

你可能还会喜欢的相关文章

[## Python 中的可迭代对象 vs 迭代器

了解 Python 中可迭代对象和迭代器之间的区别

Python 中的迭代器与可迭代对象 [## requirements.txt 与 setup.py 在 Python 中的区别

了解在 Python 开发和分发过程中,requirements.txtsetup.pysetup.cfg 的用途

requirements.txt 与 setup.py 在 Python 中的区别 [## 如何在 Python 中创建用户自定义可迭代对象

展示如何在 Python 中创建用户自定义迭代器,并使用户自定义类成为可迭代对象

如何在 Python 中创建用户自定义可迭代对象

通过 OpenAI API 提升表格数据预测能力

原文:towardsdatascience.com/improve-tabular-data-prediction-with-large-language-model-through-openai-api-3eae3c5e52bc

使用 Python 实现机器学习分类、提示工程、文本嵌入特征工程和 OpenAI API 的模型解释。

Tony ZhangTowards Data Science Tony Zhang

·发表于 Towards Data Science ·11 min read·2023 年 7 月 13 日

--

图片由 Markus Spiske 提供,来源于 Unsplash

现如今,大型语言模型及其应用或工具在新闻和社交媒体上频频出现。GitHub 的热门页面展示了大量广泛使用大型语言模型的代码库。我们见证了大型语言模型在营销写作、文档总结、音乐创作和软件开发代码生成方面的奇妙能力。

企业内部和在线积累了大量表格数据(这是最古老、最普遍的数据格式之一,可以通过行和列在表格中表示)。我们能否在传统的机器学习生命周期中应用大型语言模型,以提升模型性能并增加商业价值?

在这篇文章中,我们将探索以下主题,并提供完整的 Python 实现代码:

  • 在 Kaggle 的 心脏病分析与预测数据集 上构建广义线性模型和树模型。

  • 通过提示工程将表格数据转化为文本

  • 使用 OpenAI API 进行零样本分类(GPT-3.5 模型:text-davinci-003)

  • 使用 OpenAI 嵌入 API 提升机器学习模型性能— text-embedding-ada-002

  • 使用 OpenAI API 进行预测解释— gpt-3.5-turbo

数据集描述

数据可在 Kaggle 网站上找到,许可证为 CC0 1.0 Universal(CC0 1.0)公共领域奉献,属于公共领域(你可以复制、修改、分发和执行这些工作,甚至用于商业目的)。请参考下面提到的链接:

## 心脏病发作分析与预测数据集

一个用于心脏病发作分类的数据集

www.kaggle.com

它包含人口统计特征、医疗条件和目标。各列解释如下:

  • age: 申请人的年龄

  • sex: 申请人的性别

  • cp: 胸痛类型:值 1 为典型心绞痛,值 2 为非典型心绞痛,值 3 为非心绞痛性疼痛,值 4 为无症状。

  • trtbps: 静息血压(以 mm Hg 为单位)

  • chol: 通过 BMI 传感器获取的胆固醇(以 mg/dl 为单位)

  • fbs: 空腹血糖 > 120 mg/dl,1 = 真,0 = 假

  • restecg: 静息心电图结果

  • thalachh: 达到的最大心率

  • exng: 运动诱发的心绞痛(1 = 是;0 = 否)

  • oldpeak: 之前的峰值

  • slp: 斜率

  • caa: 主要血管数量

  • thall: 心电图的斜率

  • output: 目标变量,0= 心脏病发作的可能性较低,1= 心脏病发作的可能性较高

机器学习模型

开发二分类模型来预测心脏病发作的可能性。本节将涵盖:

  • 预处理:缺失值检查、独热编码、训练测试分层拆分等。

  • 构建 4 个模型,包括三个广义线性模型和一个基于树的模型:逻辑回归、岭回归、套索回归和随机森林

  • 模型评估与 AUC

首先,让我们导入包,加载数据,预处理并进行训练测试拆分。

import warnings
warnings.filterwarnings("ignore")

# Math and Vectors
import pandas as pd
import numpy as np

# Visualizations
import plotly.express as px

# ML
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score
import concurrent.futures

# Utils functions
from utils import prediction, compile_prompt, get_embedding, ml_models, create_auc_chart, gpt_reasoning
pd.set_option('display.max_columns', None)

# load data
df = pd.read_csv("./data/raw data/heart_attack_predicton_kaggle.csv")
df.shape

# check missing value
df.isna().sum()

# check outcome distribution
df['output'].value_counts()

# one-hot encoding
cat_cols = ['sex','exng','cp','fbs','restecg','slp','thall']
df_model = pd.get_dummies(df,columns=cat_cols)
df_model.shape

# train test stratified split
# Seperate dependent and independent variables
X = df_model.drop(axis=1,columns=['output'])
y = df_model['output'].tolist()
X_tr, X_val, y_tr, y_val = train_test_split(X, y, test_size=0.2, random_state=101,
                                                stratify=y,shuffle=True)

现在,让我们构建模型对象,拟合模型,对测试集进行预测,并计算 AUC。

## model function
def ml_models():
    lr = LogisticRegression(penalty='none', solver='saga', random_state=42, n_jobs=-1)
    lasso = LogisticRegression(penalty='l1', solver='saga', random_state=42, n_jobs=-1)
    ridge = LogisticRegression(penalty='l2', solver='saga', random_state=42, n_jobs=-1)
    rf = RandomForestClassifier(n_estimators=300, max_depth=5, min_samples_leaf=50, 
                                max_features=0.3, random_state=42, n_jobs=-1)
    models = {'LR': lr, 'LASSO': lasso, 'RIDGE': ridge, 'RF': rf}
    return models

models = ml_models()
lr = models['LR']
lasso = models['LASSO'] 
ridge = models['RIDGE'] 
rf = models['RF'] 

pred_dict = {}
for k, m in models.items():
    print(k)
    m.fit(X_tr, y_tr)
    preds = m.predict_proba(X_val)[:,1]
    auc = roc_auc_score(y_val, preds)
    pred_dict[k] = preds
    print(k + ': ', auc)

接下来,让我们可视化并比较模型性能(AUC)。

作者

在这个可视化中:

  • 基于树的模型(随机森林)表现最佳,AUC 显著更高。

  • 3 个广义线性模型的表现水平相似,AUC 低于基于树的模型,这是一种预期的结果。

使用 OpenAI API 进行零-shot 分类

我们将使用基于 text-davinci-003 模型的 OpenAI API 对表格数据进行零-shot 分类。在深入 Python 实现之前,让我们先了解一下零-shot 分类的更多信息。Hugging face 的定义是:

零样本分类是预测一个模型在训练过程中未见过的类别的任务。这种方法利用了预训练的语言模型,可以视为迁移学习的一个实例,迁移学习通常指将一个任务上训练好的模型用于与其最初训练任务不同的应用。这对于标记数据量较少的情况尤其有用。

在零样本分类中,会提供一个提示和描述我们希望模型执行的任务的文本序列,并且没有任何预期行为的示例。本节将涵盖:

  • 针对提示工程的表格数据预处理

  • 提示 LLMs

  • 使用 GPT-3.5 API 进行零样本预测:text-davinci-003

  • 使用 AUC 评估模型

表格数据的预处理

首先,让我们在提示之前处理数据:

df_gpt = df.copy()
df_gpt['sex'] = np.where(df_gpt['sex'] == 1, 'Male', 'Female')
df_gpt['cp'] = np.where(df_gpt['cp'] == 1, 'Typical angina', 
                       np.where(df_gpt['cp'] == 2, 'Atypical angina', 
                       np.where(df_gpt['cp'] == 3, 'Non-anginal pain', 'Asymptomatic')))
df_gpt['fbs'] = np.where(df_gpt['fbs'] == 1, 'Fasting blood sugar > 120 mg/dl', 'Fasting blood sugar <= 120 mg/dl')
df_gpt['restecg'] = np.where(df_gpt['restecg'] == 0, 'Normal', 
                       np.where(df_gpt['restecg'] == 1, 'Having ST-T wave abnormality (T wave inversions and/or ST elevation or depression of > 0.05 mV)', 
                                    "Showing probable or definite left ventricular hypertrophy by Estes' criteria"))
df_gpt['exng'] = np.where(df_gpt['exng'] == 1, 'Exercise induced angina', 'Without exercise induced angina')
df_gpt['slp'] = np.where(df_gpt['slp'] == 0, 'The slope of the peak exercise ST segment is downsloping', 
                       np.where(df_gpt['slp'] == 1, 'The slope of the peak exercise ST segment is flat', 
                                    'The slope of the peak exercise ST segment is upsloping'))
df_gpt['thall'] = np.where(df_gpt['thall'] == 1, 'Thall is fixed defect', 
                       np.where(df_gpt['thall'] == 2, 'Thall is normal', 'Thall is reversable defect'))

# test df to dict
application_list = X_val.to_dict(orient='records')
len(application_list)

提示 LLMs

提示是与大型语言模型进行特定任务交互的强大工具。提示是用户提供的输入,模型会对此作出回应。提示可以有多种形式,即文本、图像。

在本文中,提示包括带有预期 JSON 输出格式的指令和问题本身。以心脏病数据集为例,文本提示可以是:

作者

接下来,我们将定义提示和 API 调用函数,这些函数构建提示并从 OpenAI-3.5 API 获取响应。

def prediction_GPT3_5(data, explain = False):
    if explain:
        prompt = prompt_logic(explain)
    else:    
        prompt = prompt_logic(explain)
    print(prompt)
    response = openai.Completion.create(
        model = 'text-davinci-003',
        prompt=prompt,
        max_tokens=64,
        n=1,
        stop=None,
        temperature=0.5,
        top_p=1.0,
        frequency_penalty=0.0,
        presence_penalty=0.0
    )

    try:
        output = response.choices[0].text.strip()
        output_dict = json.loads(output)
        return output_dict
    except (IndexError, ValueError):
        return None

def prediction(combined_data_argu):
    application_data, explain = combined_data_argu
    response = prediction_GPT3_5(application_data, explain)
    return response

获取 API 响应 — 多进程处理

多进程处理用于加速 API 调用。代码如下:

### get prediction from GPT-3.5 model: text-davinci-003 - multiprocessing pool
with concurrent.futures.ThreadPoolExecutor() as executor:
    # Combine credit_data and explain into a single iterable
    combined_data = zip(application_list, [False] * len(application_list))
    # Submit the transaction processing tasks to the executor
    results = executor.map(prediction, combined_data)

    # Collect the responses into a list
    responses = list(results)
responses_df = pd.DataFrame(responses)
responses_df.shape

零样本分类 AUC

零样本分类的 AUC 为 0.48,这表明预测效果比随机机会更差,并且表明在这个数据集上,GPT-3.5 text-davinci-003 模型可能没有泄露。

auc_gpt= roc_auc_score(y_val, responses_df['output'])
auc_gpt

使用 OpenAI 嵌入提升机器学习模型性能

LLM 嵌入是大型语言模型(即 OpenAI API)的一个端点,它使得执行自然语言和代码任务变得简单,例如语义搜索、聚类、主题建模和分类。通过提示工程,表格数据被转化为自然语言文本,这些文本可以用来生成嵌入。嵌入有潜力通过使传统机器学习模型更好地理解自然语言和在少量标记数据下适应上下文,从而提高其性能。简而言之,这在这个背景下是一种特征工程。

特征工程是将原始数据转换为更好地表示基础问题的特征的过程,从而提高模型在未见数据上的准确性。

在本节中,你将看到:

  • 如何通过 API 调用获取 OpenAI 嵌入

  • 模型性能比较 — 含嵌入特征与不含嵌入特征

首先,让我们定义一个通过 API 获取嵌入并与原始数据集合并的函数:

# define function to fetch the embedding
def get_embedding(text, model="text-embedding-ada-002"):
   text = text.replace("\n", " ")
   return openai.Embedding.create(input = [text], model=model)['data'][0]['embedding']

# API call and merge with raw data
df_gpt['ada_embedding'] = df_gpt.combined.apply(lambda x: get_embedding(x, model='text-embedding-ada-002'))
df_gpt = df_gpt.join(pd.DataFrame(df_gpt['ada_embedding'].apply(pd.Series)))
df_gpt.drop(['combined', 'ada_embedding'], axis = 1, inplace = True)
df_gpt.columns = df_gpt.columns.tolist()[:14] + ['Embedding_' + str(i) for i in df_gpt.columns.tolist()[14:]]
df = pd.concat([df, df_gpt[[i for i in df_gpt.columns.tolist() if i.startswith('Embedding_')]]], axis=1)
df_gpt.shape

类似于纯机器学习模型,我们也将进行分层拆分并拟合模型:

# Seperate dependent and independent variables
X = df.drop(axis=1,columns=['output'])
y = df['output'].tolist()

X_tr, X_val, y_tr, y_val = train_test_split(X, y, test_size=0.2, random_state=101,
                                                stratify=y,shuffle=True)
models = ml_models()
lr = models['LR']
lasso = models['LASSO'] 
ridge = models['RIDGE'] 
rf = models['RF'] 
pred_dict_gpt = {}
for k, m in models.items():
    print(k)
    m.fit(X_tr, y_tr)
    preds = m.predict_proba(X_val)[:,1]
    auc = roc_auc_score(y_val, preds)
    pred_dict_gpt[k + '_With_GPT_Embedding'] = preds
    print(k + '_With_GPT_Embedding' + ': ', auc)

模型性能比较——有与没有嵌入特征

通过结合没有嵌入特征的模型,我们总共有 8 个模型。测试集上的 ROC 曲线如下:

pred_dict_combine = dict(list(pred_dict.items()) + list(pred_dict_gpt.items()))
create_auc_chart(pred_dict_combine, y_val, 'Model AUC')

作者

通常,我们观察到:

  • 嵌入特征并没有显著提高广义线性模型的性能(逻辑回归、岭回归和套索回归)

  • 带有嵌入特征的随机森林模型表现最佳,比没有嵌入特征的随机森林模型稍好。

我们看到大型语言模型在传统模型训练过程中的潜力,并能提高输出质量。我们可能会有一个问题:大型语言模型能否帮助解释模型决策?让我们在下一节中探讨这个问题。

使用 OpenAI API 的模型可解释性——gpt-3.5-turbo

模型可解释性是机器学习应用中的关键话题之一,尤其是在保险、医疗、金融和法律等领域,用户需要理解模型如何在局部和全局层面做出决策。如果你想了解更多关于深度学习模型解释的内容,我写了一篇相关文章:深度学习模型解释使用 SHAP。

本节内容包括:

  • 为 OpenAI API 准备输入

  • 通过 gpt-3.5-turbo 模型获取推理

首先,让我们准备 API 调用的输入。

application_data = application_list[0]
application_data

{'age': 51,
 'sex': 'Male',
 'cp': 'Atypical angina',
 'trtbps': 125,
 'chol': 245,
 'fbs': 'Fasting blood sugar > 120 mg/dl',
 'restecg': 'Normal',
 'thalachh': 166,
 'exng': 'Without exercise induced angina',
 'oldpeak': 2.4,
 'slp': 'The slope of the peak exercise ST segment is flat',
 'caa': 0,
 'thall': 'Thall is normal'}

接下来,让我们通过调用 gpt-3.5-turbo API 来获取推理结果。

message_objects = [
        {"role": "system", "content": '''You are a medical expert / underwriter in a global insurance company. Your job is to evaluate the chance of having heart attack. Please encode your response as json in the following format
        {{
            "decision": "<Either less chance of heart attack or more chance of heart attack>",
        }}'''},
        {"role": "user", "content": prompt},
    ]

completion = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=message_objects,
        max_tokens=1000,  # Adjust the max_tokens as per your desired response length
        stop=None,  # Set custom stop conditions if required
    )

# Extract the response message content
    response_content = completion.choices[0].message["content"]

响应相当令人印象深刻,且大型语言模型展示了强大的推理能力和合理的解释。

{ “决定”: “心脏病发作的可能性较小”, “推理”: “根据提供的信息,申请人有几个因素表明其心脏病发作的可能性较低。首先,申请人的年龄为 51 岁,这虽然不算年轻,但也不在高风险范围内。其次,申请人是男性。虽然男性通常比女性心脏病的风险更高,但这并不是唯一的决定性因素。第三,申请人报告的胸痛类型是非典型心绞痛。非典型心绞痛的特点是胸痛不那么可预测,可能有不同的模式,相比于典型心绞痛。这可能表明心脏病发作的风险较低。第四,申请人的静息血压为 125 mm Hg。这在正常范围内,并不表明高血压,高血压是心脏病发作的一个风险因素。第五,申请人的胆固醇水平为 245 mg/dl。虽然这高于推荐水平,但并不是极高的水平,而且申请人的 BMI 没有提供,因此无法确定胆固醇水平是否因肥胖而高。第六,申请人的空腹血糖水平不超过 120 mg/dl,这表明糖尿病的风险较低,糖尿病是心脏病发作的另一个风险因素。第七,申请人的静息心电图结果正常,这表明心脏功能正常,降低了心脏病发作的风险。第八,申请人达到的最大心率为 166,这是一个好迹象,表明心血管系统更健康。第九,申请人没有出现运动诱发的心绞痛,这是另一个积极因素。第十,运动相对于静息的 ST 段压低为 2.4,这在正常范围内,并不表明显著的缺血。第十一,峰值运动 ST 段的斜率为平坦,这可能是正常发现,也可能与申请人报告的非典型心绞痛有关。最后,申请人没有主要血管问题,Thall 正常,表明冠状动脉疾病的风险较低。考虑到所有这些因素,申请人心脏病发作的可能性较低。然而,重要的是要注意,这一评估仅基于提供的信息,可能还需要进一步的医学评估以作出最终判断。” }

摘要

大型语言模型是解决各行业广泛用例的强大工具。创建 LLM 应用变得更加容易且日益实惠。LLM 必将为企业带来真正的商业价值。

让我们保持联系……

我欢迎你 加入我,共同踏上激动人心且富有成效的数据科学学习冒险之旅 通过* 关注我的 Medium页面,获取源源不断的迷人数据科学内容。我将在接下来的几个月中分享更多机器学习基础、自然语言处理基础以及端到端数据科学实现的内容。干杯!

参考资料

[## 什么是零样本分类? - Hugging Face

了解使用机器学习的零样本分类

huggingface.co](https://huggingface.co/tasks/zero-shot-classification?source=post_page-----3eae3c5e52bc--------------------------------) [## 大型语言模型简介:提示工程与 P-Tuning | NVIDIA 技术博客

ChatGPT 给人留下了深刻的印象。用户们兴奋地使用这款 AI 聊天机器人提问、写诗、赋予……

developer.nvidia.com](https://developer.nvidia.com/blog/an-introduction-to-large-language-models-prompt-engineering-and-p-tuning/?source=post_page-----3eae3c5e52bc--------------------------------) ## 使用 SHAP 进行深度学习模型解释

图像和表格数据的 Python 实现

towardsdatascience.com

通过早期停止改善你的提升算法

原文:towardsdatascience.com/improve-your-boosting-algorithms-with-early-stopping-99616bd15d83

概述及 Python 实现

Aashish NairTowards Data Science Aashish Nair

·发表于Towards Data Science ·6 分钟阅读·2023 年 5 月 15 日

--

照片由Glenn Carstens-Peters提供,来源于Unsplash

提升算法在数据科学领域非常流行,这一点毋庸置疑。结合提升的模型表现出色,这也是它们在学术界和工业界都很常见的原因。

也就是说,如果这些类型的算法没有正确配置,会得到次优结果。

一个常常被低估的特性是早期停止

在这里,我们将对早期停止进行高层次的概述,并解释为何它应当被纳入你的提升算法中。

提升回顾

在深入早期停止之前,让我们简要讨论一下提升算法。

简而言之,利用提升的算法会训练一系列顺序模型,每个模型旨在解决前一个模型所犯的错误。

提升算法遵循以下步骤:

  1. 训练一个带有初始权重的弱模型

  2. 评估第一个模型的“错误”

  3. 训练一个新的模型,修改权重以解决前一个模型的问题

  4. 评估这个新模型的“错误”

  5. 重复步骤 3 或 4,直到满足特定标准(例如,迭代次数、模型性能等)

理论上,提升算法是确定特定模型最优权重的完美解决方案。

毕竟,如果模型不断从之前的错误中学习,执行更多的迭代应该会带来更好的结果。那么,为什么不进行尽可能多的迭代呢?拥有接近无限数量的模型,我们可以实现最佳性能!

不幸的是,通常情况并非如此。

更多迭代≠更好

在选择的迭代次数后,模型将调整其权重,并可能在训练数据上变得更擅长泛化。然而,如果算法超过理想的迭代次数,它将开始捕捉噪声,并在未见过的数据上表现不佳。

换句话说,使用过多迭代的提升算法容易过拟合。

找到合适的迭代次数

现在我们知道提升算法需要足够的迭代次数来找到模型的最佳权重,但又不能过多以避免过拟合。

关键是找到“最佳点”:一个既不高也不低的迭代次数。

然而,确定理想的迭代次数可能具有挑战性,因为这个数字因情况而异。影响这个数字的因素有很多,包括底层数据和正在训练的模型。

一种解决方法是使用提前停止。

提前停止

提前停止意味着如果个别模型在验证集上的表现经过一定次数的迭代后没有改善,就会提前结束提升模型的训练。

本质上,与其训练弱模型固定次数,我们可以配置它,仅在显示更好结果时继续训练。

这种提前停止的好处一目了然。通过这种技术,我们可以确保模型在过拟合之前停止训练,从而提高性能。它还减少了训练过程的运行时间,因为它减少了迭代次数。

案例研究

最好用案例研究来演示提前停止。让我们使用 Scikit Learn 库中的内置泰坦尼克数据集。

目标是训练一个有提前停止无提前停止的轻量级梯度提升机(LGBM),并比较它们在 f1 分数和运行时间方面的结果。

  1. 没有提前停止

让我们创建一个 LGBM 分类器,使用 1000 次迭代(在n_estimators超参数中指定),并对测试集进行评估。

F-1 Score(作者创建)

接下来,让我们使用%%timeit命令来确定这些操作的运行时间:

代码输出(作者创建)

通过使用具有 1000 次迭代的提升算法,模型在约 473 毫秒内产生了约 0.85 的 f-1 分数。

还不错,但我们真的需要 1000 次迭代吗?

为了更清楚地了解,让我们查看模型的 f-1 分数如何随着迭代次数的增加而变化。

代码输出(作者创建)

令人震惊的是,模型在测试集上的表现在前 50 次迭代后稳步下降!

显然,对于这个数据集,提升算法不需要那么多的迭代次数就能达到最佳性能。

2. 使用提前停止

这次让我们看看在加入提前停止后,模型的表现如何。

在 LGBM 分类器中使用提前停止需要明确设置两个超参数。第一个叫做 eval_set,它包含验证集。验证集是模型在每次迭代时用来评估其性能的数据集。

第二个超参数叫做 early_stopping_rounds,它包含了模型可以在没有对验证集表现进行更大提升的情况下运行的迭代次数。如果在这些迭代内性能没有提升,模型将提前停止训练。

对于这个案例,early_stopping_rounds 的值被设置为 20。这意味着如果模型在 20 次迭代内其对验证集的 f-1 分数没有超过前几次的结果,即使它被配置为运行 1000 次迭代,训练过程也会停止。

代码输出(由作者创建)

使用提前停止的模型获得了约 0.92 的 f-1 分数,这相比于不使用提前停止的模型有了显著的提高!

此外,由于使用了较少的迭代次数,模型现在应该可以在更短的时间内完成训练。我们可以通过 %%timeit 操作来确认这一点。

正如预期的那样,使用 提前停止的模型在训练时间上仅为不使用 提前停止的模型的一小部分。

结论

Prateek Katyal 摄影,来源于 Unsplash

总的来说,利用提升方法的算法通常更加稳健,因为它们从多个弱模型中“学习”。然而,最大化这些算法使用的迭代次数并不是一个可行的解决方案。

不同的使用场景会需要不同的迭代次数,这就是为什么算法中使用的理想迭代次数应基于单个模型的表现。

因此,提前停止是一种具有巨大实际价值的技术。它使用验证集作为算法是否需要更多迭代或提前停止的指标。如案例研究中所解释和演示的,提前停止使得模型在较少的训练时间内获得更好的性能。

祝你在数据科学的工作中好运!

改善你的梯度下降:寻找最优步幅的史诗之旅

原文:towardsdatascience.com/improve-your-gradient-descent-the-epic-quest-for-the-optimal-stride-4711dc6f5dba?source=collection_archive---------8-----------------------#2023-05-23

优化梯度下降步长/学习率的技术

Naman AgrawalTowards Data Science Naman Agrawal

·

关注 发表在Towards Data Science ·14 分钟阅读·2023 年 5 月 23 日

--

图片由stokpic提供,来自Pixabay

目录

  1. 介绍

  2. 方法 1:固定步长

  3. 方法 2:精确线搜索

  4. 方法 3:回溯线搜索

  5. 结论

介绍

在训练任何机器学习模型时,梯度下降是最常用的优化参数的技术之一。梯度下降提供了一种高效的方式来最小化传入数据的损失函数,特别是在没有问题的封闭解的情况下。一般来说,考虑一个由凸函数和可微分函数 f: Rᵈ → R 定义的机器学习问题(大多数损失函数都具有这些属性)。目标是找到 x* ∈ Rᵈ,使损失函数最小化:

梯度下降提供了一种迭代方法来解决这个问题。更新规则如下:

其中 x⁽ᵏ⁾指算法第 k 次迭代中 x 的值,tₖ指第 k 次迭代中模型的步长或学习率。算法的一般工作流程如下:

  1. 确定损失函数 f 并计算其梯度∇f。

  2. 从随机选择一个 x ∈ Rᵈ开始,称之为 x⁽⁰⁾(起始迭代)。

  3. 直到达到停止标准(例如,误差低于某个阈值),执行以下操作:

    A) 确定 x 必须减少或增加的方向。在梯度下降中,这是由当前迭代点的损失函数梯度的相反方向给出的。vₖ = ∇ₓ f(x⁽ᵏ ⁻ ¹⁾)

    B) 确定步长或变化的幅度:tₖ。

    C) 更新迭代:x⁽ᵏ⁾= x⁽ᵏ ⁻ ¹⁾ − tₖ∇ₓ f(x⁽ᵏ ⁻ ¹⁾)

这就是整个工作流程的核心:获取当前迭代,寻找需要更新的方向(vₖ),确定更新的幅度(tₖ),并进行更新。

梯度下降的示意图 [作者提供的图片]

那么,这篇文章的主题是什么?在这篇文章中,我们将重点关注第 3B 步:寻找最优步长或 tₖ的幅度。对于梯度下降来说,这是优化模型时最容易被忽视的方面之一。步长的大小可以大大影响算法收敛到解决方案的速度以及收敛到的解决方案的准确性。大多数情况下,数据科学家仅在整个学习过程中设置一个固定值的步长,或者偶尔使用验证技术来训练它。但解决这个问题还有许多更高效的方法。在这篇文章中,我们将讨论确定步长 tₖ的三种不同方法:

  1. 固定步长

  2. 精确线搜索

  3. 回溯线搜索(阿米乔规则)

对于这些方法中的每一种,我们将讨论理论并实现它以计算示例的前几个迭代。特别是,我们将考虑以下损失函数来评估模型:

下面是该函数的 3D 图:

损失函数(3D 图) [由作者使用 LibreTexts 生成的图片]

从图中可以明显看出,全球最小值为 x* = [0; 0]。在本文中,我们将手动计算前几次迭代,并计算每种方法的收敛步骤数。我们还将跟踪下降模式(即迭代轨迹)以了解这些技术如何影响收敛过程。通常,参考函数的等高线图(而不是其 3D 图)可以更好地评估不同的轨迹。函数的等高线图可以通过以下代码轻松生成:

# Load Packages
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns
sns.set()
sns.set(style="darkgrid")
from matplotlib import cm
from matplotlib.ticker import LinearLocator, FormatStrFormatter
from mpl_toolkits.mplot3d import Axes3D
# Define Function
f = lambda x,y:  2*x**2 + 3*y**2 - 2*x*y - 1

# Plot contour
X = np.arange(-1, 1, 0.005)
Y = np.arange(-1, 1, 0.005)
X, Y = np.meshgrid(X, Y)
Z = f(X,Y)
plt.figure(figsize=(12, 7))
cmap = plt.cm.get_cmap('viridis')
plt.contour(X,Y,Z,250, cmap=cmap)

![

f 的等高线图 [由作者使用 Python 生成的图片]

让我们开始吧!

方法 1:固定步长

这种方法最简单易用,也是训练 ML 模型时最常用的方法。这涉及到设置:

在使用这种方法时,选择合适的 t 值需要非常小心。虽然较小的 t 值可以导致非常准确的解决方案,但收敛速度可能会变得相当慢。另一方面,较大的 t 值使算法更快,但会牺牲准确性。使用这种方法要求实施者仔细平衡收敛速度和所获得解决方案的准确性之间的权衡。

在实践中,大多数数据科学家使用验证技术,如保留验证或 k 折交叉验证,来优化 t。该技术涉及创建训练数据的一个分区(称为验证数据),该数据用于通过在 t 可以取的离散值集上运行算法来优化性能。让我们来看一下我们的例子:

第一步是计算它的梯度:

对于所有后续计算,我们将初始化设为 x⁽⁰⁾= [1; 1]。在这种策略下,我们设置:

前两次迭代的计算如下:

我们通过以下 Python 代码程序化计算其余的迭代:

# Define the function f(x, y)
f = lambda x, y: 2*x**2 + 3*y**2 - 2*x*y - 1

# Define the derivative of f(x, y)
def df(x, y):
    return np.array([4*x - 2*y, 6*y - 2*x])

# Perform gradient descent optimization
def grad_desc(f, df, x0, y0, t=0.1, tol=0.001):
    x, y = [x0], [y0]  # Initialize lists to store x and y coordinates
    num_steps = 0  # Initialize the number of steps taken
    # Continue until the norm of the gradient is below the tolerance
    while np.linalg.norm(df(x0, y0)) > tol:  
        v = -df(x0, y0)  # Compute the direction of descent
        x0 = x0 + t*v[0]  # Update x coordinate
        y0 = y0 + t*v[1]  # Update y coordinate
        x.append(x0)  # Append updated x coordinate to the list
        y.append(y0)  # Append updated y coordinate to the list
        num_steps += 1  # Increment the number of steps taken
    return x, y, num_steps

# Run the gradient descent algorithm with initial point (1, 1)
a, b, n = grad_desc(f, df, 1, 1)

# Print the number of steps taken for convergence
print(f"Number of Steps to Convergence: {n}")

在上述代码中,我们定义了以下收敛标准(将始终用于所有方法):

运行上述代码后,我们发现收敛大约需要 26 步。下面的图展示了在梯度下降过程中迭代轨迹:

# Plot the contours
X = np.arange(-1.1, 1.1, 0.005)
Y = np.arange(-1.1, 1.1, 0.005)
X, Y = np.meshgrid(X, Y)
Z = f(X,Y)
plt.figure(figsize=(12, 7))
plt.contour(X,Y,Z,250, cmap=cmap, alpha = 0.6)
n = len(a)
for i in range(n - 1):
    plt.plot([a[i]],[b[i]],marker='o',markersize=7, color ='r')
    plt.plot([a[i + 1]],[b[i + 1]],marker='o',markersize=7, color ='r')
    plt.arrow(a[i],b[i],a[i + 1] - a[i],b[i + 1] - b[i], 
      head_width=0, head_length=0, fc='r', ec='r', linewidth=2.0)

等高线图:固定步长 = 0.1 [由作者使用 Python 生成的图片]

为了更好地理解在这种方法中选择正确的 t 有多关键,我们来衡量一下增加或减少 t 的效果。如果我们将 t 的值从 0.1 减少到 0.01,收敛的步数会从 26 骤增至 295。该情况的迭代轨迹如下所示:

轮廓图:固定步长 = 0.01 [作者使用 Python 生成的图像]

然而,将 t 从 0.1 增加到 0.2 时,收敛的步数从 26 减少到仅仅 11,如下所示的轨迹所示:

轮廓图:固定步长 = 0.2 [作者使用 Python 生成的图像]

然而,需要注意的是,这并不总是如此。如果步长的值过大,可能导致迭代过程偏离最优解而无法收敛。实际上,将 t 从 0.2 增加到约 0.3 会导致迭代值急剧上升,使得无法收敛。这可以从以下的轮廓图(t = 0.3)看出,仅针对前 8 步:

轮廓图:固定步长 = 0.3 [作者使用 Python 生成的图像]

因此,显而易见的是,在此方法中找到正确的 t 值极其重要,甚至小幅的增加或减少都可能极大地影响算法的收敛能力。现在,让我们谈谈确定 t 的下一种方法。

方法 2:精确线搜索

在这种方法中,我们不会在每次迭代时分配一个简单的预设 t 值。相反,我们将找到最优 t 的问题本身视为一个一维优化问题。换句话说,我们关注于找到最优的更新 t,以最小化函数值:

注意这有多酷!我们有一个多维优化问题(最小化 f),我们尝试使用梯度下降来解决。我们知道更新迭代值的最佳方向(vₖ = − ∇ₓ f(x⁽ᵏ ⁻ ¹⁾)),但我们需要找到最优步长 tₖ。换句话说,下一次迭代的函数值仅依赖于我们选择使用的 tₖ值。因此,我们将其视为另一个(但更简单的!)优化问题:

因此,我们更新 x⁽ᵏ⁾,使其成为最小化损失函数 f 的最佳迭代。这确实有助于提高收敛速度。然而,这也增加了额外的时间要求:计算 g(t)的最小化器。通常,这不是问题,因为它是一个一维函数,但有时可能会花费比预期更长的时间。因此,在使用这种方法时,平衡减少收敛步数与计算 argmin 的额外时间要求之间的权衡是很重要的。让我们看看我们的例子:

前两个迭代值计算如下:

我们通过以下 Python 代码以编程方式计算其余迭代

# Import package for 1D Optimization
from scipy.optimize import minimize_scalar

def grad_desc(f, df, x0, y0, tol=0.001):
    x, y = [x0], [y0]  # Initialize lists to store x and y coordinates
    num_steps = 0  # Initialize the number of steps taken
    # Continue until the norm of the gradient is below the tolerance
    while np.linalg.norm(df(x0, y0)) > tol:  
        v = -df(x0, y0)  # Compute the direction of descent
        # Define optimizer function for searching t
        g = lambda t: f(x0 + t*v[0], y0 + t*v[1]) 
        t = minimize_scalar(g).x # Minimize t
        x0 = x0 + t*v[0]  # Update x coordinate
        y0 = y0 + t*v[1]  # Update y coordinate
        x.append(x0)  # Append updated x coordinate to the list
        y.append(y0)  # Append updated y coordinate to the list
        num_steps += 1  # Increment the number of steps taken
    return x, y, num_steps

# Run the gradient descent algorithm with initial point (1, 1)
a, b, n = grad_desc(f, df, 1, 1)

# Print the number of steps taken for convergence
print(f"Number of Steps to Convergence: {n}")

和以前一样,在上述代码中,我们定义了以下收敛标准(所有方法将始终使用):

运行上述code时,我们发现仅需 10 步即可收敛(这是相对于固定步长的重大改进)。以下图表显示了梯度下降过程中的迭代轨迹:

# Plot the contours
X = np.arange(-1.1, 1.1, 0.005)
Y = np.arange(-1.1, 1.1, 0.005)
X, Y = np.meshgrid(X, Y)
Z = f(X,Y)
plt.figure(figsize=(12, 7))
plt.contour(X,Y,Z,250, cmap=cmap, alpha = 0.6)
n = len(a)
for i in range(n - 1):
    plt.plot([a[i]],[b[i]],marker='o',markersize=7, color ='r')
    plt.plot([a[i + 1]],[b[i + 1]],marker='o',markersize=7, color ='r')
    plt.arrow(a[i],b[i],a[i + 1] - a[i],b[i + 1] - b[i], head_width=0, 
      head_length=0, fc='r', ec='r', linewidth=2.0)

等高线图:精确线搜索 [作者使用 Python 生成的图像]

现在,让我们讨论确定 t 的下一种方法。

方法 3:回溯线搜索

回溯法是一种选择最优步长的自适应方法。根据我的经验,这被认为是优化步长的最有用策略之一。其收敛速度通常比固定步长要快,而无需在精确线搜索中最大化一维函数 g(t)的复杂性。该方法包括从一个较大的步长(t¯ = 1)开始,并继续减小步长,直到观察到 f(x)的所需减少。我们首先来看看算法,随后将讨论具体细节:

算法 1:回溯(Armijo–Goldstein 条件)[作者提供的图像]

换句话说,我们从一个较大的步长开始(这在算法的初始阶段通常很重要),并检查它是否有助于我们按给定阈值改善当前迭代。如果步长过大,我们通过将其与标量常数β ∈ (0, 1)相乘来减少它。我们重复这个过程,直到获得所需的 f 减少。具体来说,我们选择最大的 t,使得:

即,减少至少为σt || ∇ₓ f(x⁽ᵏ ⁻ ¹⁾) ||²。但是,为什么选择这个值?可以通过数学方法(通过泰勒一阶展开)证明 t || ∇ₓ f(x⁽ᵏ ⁻ ¹⁾) ||²是我们可以期望在当前迭代中通过改进得到的 f 的最小减少。条件中有一个额外的σ因子。这是为了考虑即使我们不能实现理论上保证的减少 t || ∇ₓ f(x⁽ᵏ ⁻ ¹⁾) ||²,我们至少希望达到由σ缩放的该减少的一部分。也就是说,我们要求减少的 f 至少是 f 在 x⁽ᵏ⁾的泰勒一阶近似所承诺的减少的固定比例σ。如果条件不满足,我们通过β将 t 缩小到较小值。我们来看一下我们的例子(设置 t¯= 1, σ = β = 0.5):

前两个迭代计算如下:

同样,

我们通过以下 Python 代码编程计算剩余迭代:

# Perform gradient descent optimization
def grad_desc(f, df, x0, y0, tol=0.001):
    x, y = [x0], [y0]  # Initialize lists to store x and y coordinates
    num_steps = 0  # Initialize the number of steps taken
    # Continue until the norm of the gradient is below the tolerance
    while np.linalg.norm(df(x0, y0)) > tol:  
        v = -df(x0, y0)  # Compute the direction of descent
        # Compute the step size using Armijo line search
        t = armijo(f, df, x0, y0, v[0], v[1]) 
        x0 = x0 + t*v[0]  # Update x coordinate
        y0 = y0 + t*v[1]  # Update y coordinate
        x.append(x0)  # Append updated x coordinate to the list
        y.append(y0)  # Append updated y coordinate to the list
        num_steps += 1  # Increment the number of steps taken
    return x, y, num_steps

def armijo(f, df, x1, x2, v1, v2, s = 0.5, b = 0.5):
    t = 1
    # Perform Armijo line search until the Armijo condition is satisfied
    while (f(x1 + t*v1, x2 + t*v2) > f(x1, x2) + 
            t*s*np.matmul(df(x1, x2).T, np.array([v1, v2]))):
        t = t*b # Reduce the step size by a factor of b
    return t

# Run the gradient descent algorithm with initial point (1, 1)
a, b, n = grad_desc(f, df, 1, 1)

# Print the number of steps taken for convergence
print(f"Number of Steps to Convergence: {n}")

与之前一样,在上述代码中,我们定义了以下收敛标准(将为所有方法一致使用):

运行上述代码时,我们发现仅需 10 步即可收敛。以下图示显示了梯度下降过程中的迭代轨迹:

# Plot the contours
X = np.arange(-1.1, 1.1, 0.005)
Y = np.arange(-1.1, 1.1, 0.005)
X, Y = np.meshgrid(X, Y)
Z = f(X,Y)
plt.figure(figsize=(12, 7))
plt.contour(X,Y,Z,250, cmap=cmap, alpha = 0.6)
n = len(a)
for i in range(n - 1):
    plt.plot([a[i]],[b[i]],marker='o',markersize=7, color ='r')
    plt.plot([a[i + 1]],[b[i + 1]],marker='o',markersize=7, color ='r')
    plt.arrow(a[i],b[i],a[i + 1] - a[i],b[i + 1] - b[i], head_width=0, 
      head_length=0, fc='r', ec='r', linewidth=2.0)

等高线图:回溯 [图像由作者使用 Python 生成]

结论

在这篇文章中,我们了解了一些优化梯度下降算法步长的有用技巧。特别是,我们介绍了三种主要技巧:固定步长,即在训练过程中保持相同的步长或学习率,精确线搜索,即将损失函数最小化为 t 的函数,以及 Armijo 回溯,即逐渐减少步长直到满足阈值。虽然这些是调优优化的最基本技巧,但还有大量其他方法(例如,将 t 设置为迭代次数的函数)。这些工具通常用于更复杂的环境,如随机梯度下降。本文的目的不仅是介绍这些技巧,还使你意识到可能影响优化算法的复杂性。虽然大多数技巧用于梯度下降,但它们也可以应用于其他优化算法(例如牛顿-拉夫森方法)。每种技巧都有其优点,并可能在特定应用和算法中优于其他技巧。

希望你喜欢阅读这篇文章!如有任何疑问或建议,请在评论框中回复。欢迎通过邮件与我联系。

如果你喜欢我的文章并想阅读更多,请关注我。

注意:所有图像均由作者制作。

posted @ 2024-10-12 19:52  绝不原创的飞龙  阅读(463)  评论(0)    收藏  举报