docs-merge-16

TowardsDataScience 2024 中文翻译(十七)

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

减小 AI 模型的尺寸

原文:towardsdatascience.com/reducing-the-size-of-ai-models-4ab4cfe5887a?source=collection_archive---------3-----------------------#2024-09-07

在边缘设备上运行大型 AI 模型

Arun NandaTowards Data Science Arun Nanda

·发表于Towards Data Science ·9 分钟阅读·2024 年 9 月 7 日

--

图片由 Pixlr 生成

AI 模型,尤其是大型语言模型(LLMs),需要大量 GPU 内存。例如,在2024 年 7 月发布的 LLaMA 3.1 模型中,内存需求如下:

  • 8 亿参数的模型需要 16GB 内存,采用 16 位浮点数权重

  • 更大的 4050 亿参数模型使用 16 位浮点数时需要 810GB 内存

在一个完整的机器学习模型中,权重表示为 32 位浮点数。现代模型有数百万到数十亿(甚至数百亿)个权重。训练和运行这样的大型模型非常耗费资源:

  • 它需要大量计算(处理能力)。

  • 它需要大量 GPU 内存。

  • 它消耗大量能源,尤其是导致这种能源消耗的最大因素是:

    • 使用 32 位浮点数执行大量计算(矩阵乘法)

    • 数据传输 — 将模型数据从内存复制到处理单元。

高资源消耗有两个主要缺点:

  • 训练:需要大量 GPU 资源的模型训练昂贵且缓慢。这限制了新的研究和开发只能由预算充足的团队进行。

  • 推理:大型模型需要专用的(且昂贵的)硬件(专用 GPU 服务器)才能运行。它们不能在像普通笔记本电脑和手机这样的消费级设备上运行。

因此,最终用户和个人设备必须通过付费 API 服务来访问 AI 模型。这导致了消费者应用程序及其开发者都面临次优的用户体验:

  • 它引入了由于网络访问和服务器负载带来的延迟。

  • 它还介绍了开发 AI 软件时的预算限制。能够在消费者设备上本地运行 AI 模型,将有助于缓解这些问题。

因此,减少 AI 模型的大小是一个活跃的研究和开发领域。这是系列文章中的第一篇,讨论了减少模型大小的不同方法,特别是通过量化这一方法。这些文章基于对原始研究论文的学习。在整个系列中,你会找到参考论文 PDF 的链接。

  • 本文为入门文章,概述了减少模型大小的不同方法。它将量化介绍为最具前景的方法,并且是当前研究的热点。

  • 量化 AI 模型的权重通过数值实例展示了量化的算术原理。

  • 量化神经网络模型讨论了将量化应用于神经网络模型的架构和过程,包括基本的数学原理。特别是,它着重于如何训练模型,使其在推理时使用量化权重仍能表现良好。

  • 量化的不同方法解释了不同类型的量化方法,如量化为不同精度、量化粒度、确定性与随机量化,以及在训练模型过程中使用的不同量化方法。

  • 极限量化:1 位 AI 模型讲解了二进制量化的内容,涉及将模型权重从 32 位浮动数减少到二进制数字。它展示了二进制量化的数学原理,并总结了首次实施变换器模型(如 BERT)二进制量化的研究者所采用的方法。

  • 理解 1 位大规模语言模型介绍了将大规模语言模型(LLM)量化为使用 1 位(即二进制)权重({-1, 1})的最新研究成果。特别地,焦点放在了 BitNet 上,它是首次成功地重新设计变换器架构以使用 1 位权重的尝试。

  • 理解 1.58 位语言模型讨论了将神经网络模型,特别是 LLM,量化为使用三元权重({-1, 0, +1})的技术。这也被称为 1.58 位量化,已证明能够提供非常有前景的结果。2024 年上半年,关于这一主题的讨论在科技媒体中引起了广泛关注。之前文章中解释的背景有助于深入理解为什么以及如何将 LLM 量化为 1.58 位。

降低模型大小的方法

不依赖昂贵的硬件将使 AI 应用变得更加普及,并加速新模型的开发和应用。为了解决这一构建高性能且小型模型的挑战,已经提出并尝试了多种方法。

低秩分解

神经网络以高维张量的形式表示其权重。数学上,可以将一个高秩张量分解为一组低维张量,从而使计算更高效。这种方法被称为 张量秩分解。例如,在计算机视觉模型中,权重通常是 4D 张量。

Lebedev 等人在他们 2014 年的论文 使用微调的 Cp 分解加速卷积神经网络 中展示了使用一种常见的分解技术——典范多元分解(CP 分解),将具有 4D 权重张量的卷积(这种情况在计算机视觉模型中较为常见)简化为一系列使用较小的 2D 张量的卷积。低秩自适应(LoRA)是一种现代技术(提出于 2021 年),其原理基于类似的方法,并应用于大型语言模型。

剪枝

降低网络大小和复杂度的另一种方法是通过消除网络中的连接。在 1989 年的一篇名为 Optimal Brain Damage 的论文中,Le Cun 等人提出了删除小幅度连接并重新训练模型的方法。通过反复应用,这种方法可以减少神经网络中一半或更多的权重。Le Cun 的完整论文可以在其网站上查看,Le Cun 目前(截至 2024 年)是 Meta(Facebook)的首席 AI 科学家。

在大语言模型的背景下,剪枝尤其具有挑战性。SparseGPT 是由 Frantar 等人在 2023 年发表的论文 SparseGPT: 大型语言模型可以通过一次性修剪精确地减小 中首次提出的一个著名剪枝方法,它成功地将大型语言模型的大小减少了一半,同时几乎不损失准确性。将大型语言模型的大小减少到原始的一小部分仍然不可行。Lei Mao 的文章 剪枝与神经网络 介绍了这一技术。

知识蒸馏

知识迁移是一种训练较小(学生)神经网络以复制较大且更复杂(教师)神经网络行为的方法。在许多情况下,学生网络是基于教师网络的最终预测层进行训练的。在其他方法中,学生网络还会基于教师网络的中间隐藏层进行训练。知识蒸馏在某些情况下取得了成功,但通常情况下,学生网络无法泛化到新的未见过的数据。它们往往过度拟合以在训练数据集内复制教师的行为。

量化

简而言之,量化涉及从一个具有 32 位或 16 位浮动点权重的模型开始,应用各种技术来降低权重的精度,至 8 位整数甚至二进制(1 位),而不牺牲模型准确性。低精度权重具有更低的内存和计算需求。

本文的其余部分,从下一节开始,以及本系列的其余部分,提供了对量化的深入理解。

混合

也可以按顺序应用不同的压缩技术。Han 等人在 2016 年发表的论文 通过剪枝、训练量化和霍夫曼编码压缩深度神经网络, 中,应用了剪枝、量化和霍夫曼编码,成功地将 AlexNet 模型压缩了 35 倍,将模型大小从 240 MB 减少到 6.9 MB,且没有显著损失准确度。截至 2024 年 7 月,这种方法尚未在低位 LLM 上进行尝试。

量化基础

模型的“大小”主要由两个因素决定:

  • 权重(或参数)数量

  • 每个参数的大小(位数)。

已经确立,模型中的参数数量对其性能至关重要——因此,减少参数数量并不是一个可行的方法。因此,尝试减少每个权重的长度是一个更有前景的研究方向。

传统上,LLM(大型语言模型)使用 32 位权重进行训练。具有 32 位权重的模型通常被称为全尺寸模型。减少模型参数的长度(或精度)被称为量化。16 位和 8 位量化是常见的方式。更激进的方法包括量化到 4 位、2 位,甚至 1 位。要了解如何将高精度数字量化为低精度数字,请参考 量化 AI 模型的权重,其中有关于量化模型权重的示例。

量化有助于减少内存需求并降低运行模型的计算成本。通常,模型的权重会被量化。除了量化权重外,还常常对激活进行量化。将浮动点权重映射到其低精度整数版本的函数称为量化器,或量化函数。

神经网络中的量化

简单来说,神经网络层应用的线性和非线性变换可以表示为:

在上面的表达式中:

  • z 表示非线性函数的输出,也称为激活值。

  • Sigma 是非线性激活函数,通常是 sigmoid 函数或 tanh 函数。

  • W 是该层的权重矩阵

  • a 是输入向量

  • B 是偏置向量

  • 权重与输入的矩阵乘法被称为卷积。将偏置加到乘积矩阵上称为累加。

  • 传递给 sigma(激活)函数的项被称为乘加(MAC)操作。

运行神经网络的计算负载大部分来自卷积操作——这涉及到大量浮点数的乘法。拥有大量权重的大型模型会有非常多的卷积操作。

通过使用低精度整数而不是浮点数进行乘法,可能会减少计算成本。在极端情况下,正如在理解 1.58 位语言模型中讨论的那样,32 位权重可能通过三元数{-1, 0, +1}来表示,乘法操作将被更简单的加法和减法操作替代。这就是量化背后的直觉。

数字运算的计算成本与位数的平方成正比。正如Siddegowda 等人在他们关于神经网络量化的论文(第 2.1 节)中研究的那样,使用 8 位整数代替 32 位浮点数会使能效提高 16 倍。当权重数量达到数十亿时,节省的成本非常可观。

量化函数将高精度(通常是 32 位浮点数的权重)映射到低精度整数权重。

模型通过训练所获得的“知识”由其权重的值表示。当这些权重被量化为低精度时,它们的部分信息也会丢失。量化的挑战在于如何在降低权重精度的同时保持模型的准确性。

一些量化技术有效的主要原因之一是,权重的相对值和权重的统计特性比它们的实际值更为重要。对于拥有数百万或数十亿个权重的大型模型来说,这一点尤其成立。后续关于量化的文章,如量化 BERT 模型——BinaryBERT 和 BiBERTBitNet——一种将变压器 LLM 量化为二进制权重的模型,以及BitNet b1.58——将变压器量化为三元权重的模型,都展示了成功的量化技术应用。量化的视觉指南由 Maarten Grootendoorst 编写,包含了许多量化的插图和图示。

量化推理

推理是指使用 AI 模型生成预测,例如对图像的分类或对文本串的补全。当使用全精度模型时,整个数据流通过模型时采用 32 位浮点数表示。使用量化模型进行推理时,许多部分——但并非所有——的数据流采用较低精度表示。

偏置通常不进行量化,因为偏置项的数量远少于模型中的权重数量。因此,节省的成本不足以证明量化开销的合理性。累加器的输出采用高精度格式,激活函数的输出也采用更高精度的格式。

结论

本文讨论了减少 AI 模型规模的必要性,并对实现模型规模缩减的方法进行了高层次的概述。接着介绍了量化的基础知识,这是一种目前在缩减模型规模的同时,能够保持可接受的准确度水平的最成功方法。

本系列的目标是为您提供足够的背景知识,以便理解语言模型的极端量化,从像 BERT 这样的简单模型开始,最终讨论 1 比特大型语言模型(LLMs)以及近期关于 1.58 比特 LLMs 的研究成果。为此,本系列接下来的几篇文章将深入探讨不同的子话题,例如量化背后的数学运算量化模型训练的过程。需要理解的是,由于这是一个活跃的研究和开发领域,目前尚无统一的标准程序,不同的研究者采用创新的方法以获得更好的结果。

减少 Docker 镜像大小(大型语言模型,第二部分)

原文:towardsdatascience.com/reducing-the-size-of-docker-images-serving-large-language-models-part-2-b7226a0b6514?source=collection_archive---------11-----------------------#2024-05-08

如何再将“较小”的 Docker 镜像大小减少 10%

Michał Marcińczuk, Ph.D.Towards Data Science Michał Marcińczuk 博士

·发表于 Towards Data Science ·阅读时间 7 分钟·2024 年 5 月 8 日

--

由 Runway 为以下提示生成:船上有两个容器,一个大容器和一个小容器。它们的颜色明亮、鲜艳且真实。

介绍

这是关于减少用于大型语言模型的 Docker 镜像大小话题的延续。在我之前的文章 [1]中,我展示了如何将 Docker 镜像模型的大小从 7 GB 减少到 700 MB 以下。该解决方案通过去除像 CUDA、cuDNN、cuBLAS、torch 和 triton 等重量级库来实现。通过将模型转换并量化为 ONNX 格式,并使用 CPU 版本的 onnxruntime 替代 GPU 版本的 torch,实现了这一目标。

在本文中,我将展示如何进一步将目标镜像的大小减少10%。这看起来可能有些过头,因为 700 MB 已经是一个相对较小的镜像。然而,本文介绍的技术可以更深入地了解服务语言模型的 Docker 镜像。它们有助于理解运行模型所需的组件,并发现可能存在一些更轻量的替代方案。

本文中使用的脚本和资源也可在 GitHub 上找到 [2]:

减少为大语言模型提供服务的 Docker 镜像大小(第一部分)

原文:towardsdatascience.com/reducing-the-size-of-docker-images-serving-llm-models-b70ee66e5a76?source=collection_archive---------6-----------------------#2024-05-03

你是否遇到过这样的问题:一个 1 GB 的基于 Transformer 的模型在使用 Docker 容器化部署时,大小竟然增加到了 8 GB?

Michał Marcińczuk, Ph.D.Towards Data Science Michał Marcińczuk, Ph.D.

·发布于 Towards Data Science ·6 分钟阅读·2024 年 5 月 3 日

--

图片由 Dominik Lückmann 提供,来源于 Unsplash

引言

基于 Transformer 的模型,如BERTRoBERTaT5,为许多定制的自然语言处理问题提供了最先进的解决方案。在生产环境中交付这些模型的常见方式是构建一个 Docker 镜像,提供模型的 API。该镜像封装了所需的依赖项、模型本身以及处理输入数据的代码。与大型生成模型(GenAI)相比,这些模型相对较小,从 0.5 到 2 GB不等。然而,当你采用直接的方式将模型部署为 Docker 镜像时,你可能会对镜像的大小感到惊讶,镜像的大小可能达到 8 GB。你是否曾经想过为什么目标镜像如此庞大,是否有办法减少其大小?在本文中,我将讨论为什么 Docker 镜像可能如此巨大,以及如何减少其大小。

本文中使用的 Python 脚本和 Docker 文件示例也可以在这个仓库中找到 [1]:

重构 LLM“与数据对话”:引入 LLM 辅助数据配方

原文:towardsdatascience.com/reframing-llm-chat-with-data-introducing-llm-assisted-data-recipes-f4096ac8c44b?source=collection_archive---------0-----------------------#2024-01-26

Matthew HarrisTowards Data Science Matthew Harris

·发表于Towards Data Science ·阅读时长 11 分钟·2024 年 1 月 26 日

--

来源:DALL·E 3 提示词“数据大厨制作数据配方的油画”

简而言之

在本文中,我们讨论了使用大型语言模型(LLM)进行“与数据对话”时的一些局限性,并提出了一种名为“数据配方”的方法论,在某些情况下可能作为替代方案。数据配方扩展了可重用代码片段的概念,包含数据并具有使用 LLM 进行编程对话的优势。这使得可以创建一个可重用的数据配方库——用于访问数据和生成洞察——为 LLM 生成的代码提供更多透明度,同时允许人工干预配方。来自配方的缓存结果——来自 SQL 查询或外部 API 调用——可以异步刷新以提高响应时间。提出的解决方案是 LLM 作为工具制造者(LATM)架构的变体,它将工作流分为两个流: (i) 用于创建配方的低交易量/高成本流;(ii) 用于最终用户使用配方的高交易量/低成本流。最后,通过拥有配方库和相关数据集成,可以创建一个“数据配方中心”,并有可能进行社区贡献。

使用 LLM 进行对话数据分析

现在有一些非常巧妙的模式,允许人们用自然语言提问关于数据的问题,LLM 生成调用来获取数据,并总结输出给用户。通常被称为“与数据对话” 我曾经发布过一些文章来说明这一技术,例如使用 Open AI 助手来帮助人们为气候变化做准备。还有许多更先进的例子,它可以是降低技术壁垒,让人们从复杂数据中获得见解的一个令人惊叹的方式。

使用 LLM 从用户输入生成 SQL 查询并总结输出以提供答案的示例。来源:Langchain SQL 代理

使用 LLM 从用户输入生成 API 调用并总结输出以提供答案的示例。来源:Langchain 与 API 交互

访问数据的方法通常分为以下几类……

  1. 生成数据库查询:LLM 将自然语言转换为查询语言,如 SQL 或 Cypher

  2. 生成 API 查询:LLM 将自然语言转换为用于调用 API 的文本

应用程序执行 LLM 提供的建议以获取数据,然后通常将结果传回给 LLM 进行总结。

获取数据可能是一个问题

这些技术现在已经存在,真是令人惊叹,但将它们转化为生产解决方案时,每种方法都有其优缺点……

LLM 可以生成执行数据库查询和调用外部 API 的文本,但每种方法都有其优缺点。

例如,生成 SQL 支持现代数据库查询语言能够实现的所有令人惊叹的功能,如对大量数据的聚合。然而,数据可能并不已经存在于 SQL 可用的数据库中。它可能需要先被摄取然后用 SQL 查询,但构建这样的管道可能既复杂又昂贵。

通过 API 直接访问数据意味着数据不必存在于数据库中,这为访问大量公开可用的数据集打开了巨大的世界,但也有一个陷阱。许多 API 不支持 SQL 支持的聚合查询,因此唯一的选择是提取低级数据,然后进行聚合。这增加了 LLM 应用程序的负担,并可能需要提取大量数据。

所以这两种技术都有局限性。

直接通过 LLM 传递数据无法扩展

此外,当将 LLM 应用于数据分析时,另一个主要挑战很快就会出现。大多数解决方案,如Open AI 助手,可以生成供调用者执行的函数调用以提取数据,但输出然后会传回 LLM。目前尚不清楚 OpenAI 内部具体发生了什么,但通过传递足够的数据就能导致令牌限制被突破,这表明 LLM 被用于处理提示中的原始数据。许多模式都做了类似的事情,将函数调用的输出传回 LLM。当然,这在现实世界中无法扩展,因为回答一个问题所需的数据量可能非常大。这很快就会变得昂贵,而且经常失败。

LLM 代码生成可能会很慢、昂贵且不稳定

解决这个问题的一种方法是通过让 LLM 生成任务的代码来执行分析。例如,如果用户要求对数据集中的记录进行计数,可以让 LLM 生成一段 Python 代码来计算原始数据中的记录数,执行它,然后将该信息传回给用户。与将原始数据传递给 LLM 相比,这需要的令牌少得多。

已经相当明确,LLM 在生成代码方面相当优秀。当然还不是完美的,但现在世界上很多地方都在使用像GitHub Copilot这样的工具进行软件开发。在 LLM 应用程序中,生成并执行代码作为解决任务的一部分,已经成为一种常见模式。OpenAI 的代码解释器以及像autogen和 Open AI 助手这样的框架更进一步,通过实现迭代过程,甚至能够调试生成的代码。此外,LLM 作为工具制造者(LATM)的概念已经确立(例如,Cai 等人,2023)。

但这里也存在一些挑战。

任何生成代码的 LLM 过程,特别是如果该过程需要通过一个迭代周期来调试代码,都可能迅速产生高昂的成本。这是因为生成高质量代码所需的最佳模型通常是最昂贵的,而调试代码需要在每个迭代步骤中记录之前的尝试历史,这会消耗大量令牌。根据所需的迭代次数,这个过程也相当缓慢,导致糟糕的用户体验。

正如我们许多人已经发现的那样,代码生成并不完美——至少目前是——并且偶尔会失败。代理可能会陷入代码调试的循环中,虽然生成的代码可能按预期运行,但由于存在错误,结果可能只是错误的。对于大多数应用程序,仍然需要人类参与其中。

记忆数据“事实”有局限性

通过实现某种记忆机制,可以提升代码生成的成本和性能,其中可以检索到来自先前相同请求的信息,消除重复调用 LLM 的需求。像memgpt这样的解决方案与 autogen 等框架配合工作,提供了一种简洁的实现方式。

由此产生两个问题。首先,数据通常是易变的,任何基于数据的特定答案(即“事实”)可能会随时间改变。如果今天被问到“哪些人道主义组织在阿富汗教育领域活跃?”,下个月的答案可能会有所不同。可以应用各种记忆策略来忽略一段时间后的记忆,但最可靠的方法是重新获取信息。

另一个问题是,我们的应用程序可能已经为特定情况生成了答案,例如某个国家的人口。如果另一个用户问的是完全相同的问题,记忆会很好地工作,但如果他们询问的是不同的国家,那么就没什么用了。如果我们希望能够重用之前的 LLM 回答,保存“事实”只是故事的一半。

那我们该如何处理呢?

鉴于以上所有问题,我们需要解决以下关键问题:

  • 我们需要一种适用于数据库和 API 的方法

  • 我们希望能够支持使用 API 数据的聚合查询

  • 我们希望避免使用 LLM 来总结数据,而是使用代码

  • 我们希望通过使用记忆来节省成本和提高性能

  • 记忆需要与数据源保持同步更新

  • 记忆应该具有通用性,包含技能和事实

  • 所有使用的代码都需要经过人工审查,以确保其准确性和安全性。

呼!这要求可不小啊。

引入 LLM 辅助的数据食谱

数据食谱架构:LLM 辅助生成可重用的食谱(技能),这些食谱可用于对话数据分析

这个想法是将工作流程分成两个流,以优化成本和稳定性,正如LATM 架构所提出的那样,并对数据和特定于数据食谱的记忆进行一些附加增强...

流 1:食谱助手

本流使用 LLM 代理和更强大的模型,通过对话接口生成代码片段(配方)。LLM 通过提供关于数据源的信息 —— API 规范和数据库架构 —— 来指导,使得创建配方的人可以更轻松地通过对话编程新技能。重要的是,该过程实施了一个审核阶段,在生成的代码和结果被提交到内存之前,可以由人类验证并修改。为了最佳代码生成,本流使用更强大的模型和自主代理,导致每次请求的成本更高。然而,流量较少,因此成本得到了控制。

流 2:数据分析助手

本流被更广泛的终端用户群体使用,这些用户询问关于数据的问题。系统检查内存,查看请求是否已存在事实中,例如“马里的人口是多少?”。如果没有,它会检查配方,看看是否有技能可以得到答案,例如“如何获取任何国家的人口”。如果没有内存或技能,系统会将请求发送到配方助手队列,要求添加配方。理想情况下,系统可以在启动前预先填充配方,但配方库可以根据用户遥测数据随时间不断增长。请注意,终端用户流不会动态生成代码或查询,因此可以使用较不强大的 LLM,更加稳定、安全,并且成本较低。

异步数据刷新

为了提高终端用户的响应时间,在可行的情况下,配方会异步刷新。配方内存包含可以按照设定的时间表运行的代码。配方可以预先执行,以便提前填充系统,例如,在终端用户请求之前,检索所有国家的总人口。此外,需要对从 API 提取的大量数据进行聚合的情况,可以在非工作时间运行,从而在一定程度上缓解使用 API 数据的聚合查询限制。

记忆层级 — 记住技能以及事实

上述过程实现了一个记忆层级,用来保存“事实”,这些事实可以提升为更一般的“技能”。记忆检索提升为配方是通过语义搜索和 LLM 重排序与转换的组合实现的,例如通过提示 LLM 从特定意图和代码(例如“马里的人口总数是多少?”)生成一般意图和代码(例如“获取任何国家的总人口”)。

此外,通过自动将配方作为可用函数包含到代码生成的语言模型(LLM)中,其可重用工具包不断增长,使得新的配方更高效,并调用先前的配方,而不是从头生成所有代码。

数据配方的其他一些好处

通过捕获用户的数据分析请求并使这些请求在系统中高度可见,增加了透明度。LLM 生成的代码可以经过严格审查、优化和调整,且由此生成的答案是可以理解和可重复的。这有助于减少许多 LLM 应用面临的事实基础和幻觉问题。

这种架构的另一个有趣方面是,它捕捉了特定的数据分析需求及其用户请求的频率。这可以用于加大对更频繁使用的配方的投资,从而为最终用户带来好处。例如,如果生成一份人道主义响应情况报告的配方被频繁访问,那么该报告的配方代码可以主动进行改进。

数据配方中心

这种方法开启了一个由社区维护的数据配方库的可能性,跨多个领域——一个数据配方中心。类似于现有的代码片段网站,它将添加数据维度,并通过提供 LLM 辅助的对话式编程来帮助用户创作。配方可以获得声誉分数和其他社交平台反馈。

数据配方——通过 LLM 协助创建的带有数据的代码片段——可以由社区贡献到数据配方中心。图像来源:DALL·E 3

数据配方的局限性

和任何架构一样,它可能并不适用于所有情况。数据配方的一个重要目标是减少创建即时代码时所涉及的成本和风险,而是构建一个可重用的库,具有更多的透明度和人工干预。用户当然可以请求库中尚未支持的新内容。我们可以为这些请求建立一个处理队列,通过提供 LLM 辅助编程,预计开发时间将缩短,但最终用户会有所延迟。然而,在许多情况下,这是一种可接受的权衡,因为不希望放任 LLM 生成的未经审查的代码。

另一个需要考虑的因素是配方的异步刷新。根据所需数据量的不同,这可能会变得昂贵。此外,当源数据快速变化且用户需要快速获取这些信息时,这种刷新可能效果不佳。在这种情况下,配方会每次运行,而不是从内存中检索结果。

刷新机制应有助于数据聚合任务,其中数据来自 API,但仍然存在一个问题,即底层原始数据将作为配方的一部分进行处理。当然,这对于大规模数据量的处理效果不佳,但至少它是基于用户需求来限制数据摄取,而不是尝试摄取整个远程数据集。

最后,正如所有“与数据聊天”应用程序一样,它们的表现永远取决于它们可以访问的数据。如果所需的数据不存在或质量较差,那么感知性能将会很差。此外,数据集中常常存在不平等和偏见,因此在向用户展示洞见之前,进行数据审计非常重要。当然,这不仅仅是“数据食谱”特有的问题,而是将此类技术投入实际应用时面临的最大挑战之一。垃圾进,垃圾出!

结论

提议的架构旨在通过以下方式解决大语言模型“与数据聊天”所面临的一些挑战……

  • 透明 — 食谱高度可见,并且在推广之前由人工审核,从而减轻大语言模型幻觉和总结方面的问题。

  • 确定性 — 作为代码,它们每次都会产生相同的结果,不像大语言模型对数据的总结。

  • 高性能 — 实现一个能够捕捉不仅是事实而是技能的记忆系统,且能够异步刷新,从而提高响应速度。

  • 低成本 — 通过将工作流分为两个流,高流量的终端用户流可以使用成本较低的大语言模型。

  • 安全 — 主要的终端用户群体不会触发代码或查询的即时生成和执行,所有代码都经过人工评估,以确保安全性和准确性。

我将发布一系列后续博客文章,详细介绍“数据食谱”的技术实现,并在DataKind进行用户测试。

参考文献

大语言模型作为工具制造者,Cai et al, 2023

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

如果喜欢这篇文章,请点赞,如果你愿意关注我,我将非常高兴!你可以在此处找到更多文章

强化学习 101:构建一个 RL 智能体

原文:towardsdatascience.com/reinforcement-learning-101-building-a-rl-agent-0431984ba178?source=collection_archive---------1-----------------------#2024-02-19

解读强化学习背后的数学,介绍 RL 框架,并用 Python 从头构建一个 RL 模拟。

Cristian LeoTowards Data Science Cristian Leo

·发表于 Towards Data Science ·19 分钟阅读·2024 年 2 月 19 日

--

图片由 DALL-E 生成

强化学习(RL)是人工智能领域中的一个关键元素,以其独特的方式教授机器通过与环境的互动来进行决策。在本文中,我们将深入探讨 RL 的核心机制。我们将分解其核心概念,突出其广泛的应用,解读其数学原理,并引导你从零开始构建一个 RL 模型。

索引

· 强化学习简介

∘ 什么是强化学习?

∘ 它是如何工作的?

· RL 框架

∘ 状态

∘ 动作

∘ 奖励

· 回合和策略的概念

∘ 回合

∘ 策略

· RL 问题的数学公式化

∘ 目标函数

∘ 回报(累计奖励)

∘ 折扣

强化学习 101:Q 学习

原文:towardsdatascience.com/reinforcement-learning-101-q-learning-27add4c8536d?source=collection_archive---------3-----------------------#2024-02-28

解读 Q 学习、动作-价值函数和贝尔曼方程背后的数学,并在 Python 中从零开始构建它们。

Cristian LeoTowards Data Science Cristian Leo

·发布于 Towards Data Science ·32 分钟阅读·2024 年 2 月 28 日

--

图像由 DALLE 生成

在上一篇文章中,我们初步了解了强化学习(RL)的世界,涵盖了基本概念,比如代理如何从其环境中学习,重点介绍了一个简单的设置——GridWorld。我们讨论了基本要素——动作、状态、奖励,以及如何在这个环境中行动。如果你是新手或需要快速回顾,建议再次阅读那篇文章,掌握基础知识后再深入了解。

## 强化学习 101:构建一个 RL 代理

解读强化学习背后的数学,介绍 RL 框架,并从中构建一个 RL 模拟…

towardsdatascience.com

今天,我们准备进一步深入。我们将探索强化学习(RL)的更多复杂方面,从简单的设置过渡到动态、不断变化的环境,并寻找更复杂的方式让我们的代理在其中导航。我们将深入探讨马尔可夫决策过程(Markov Decision Process,MDP)的概念,它对于深入理解 RL 的工作原理非常重要。此外,我们还将更详细地研究 Q 学习,这是 RL 中的一个关键算法,展示了如何…

用强化学习彻底改变大数据集的特征选择

原文:towardsdatascience.com/reinforcement-learning-for-feature-selection-be1e7eeb0acc?source=collection_archive---------2-----------------------#2024-05-19

在面对非常大的数据集时,利用强化学习的优势进行特征选择

Baptiste LefortTowards Data Science Baptiste Lefort

·发布于 Towards Data Science ·阅读时长 11 分钟·2024 年 5 月 19 日

--

了解强化学习如何改变机器学习模型的特征选择。通过实际示例和专用的 Python 库,学习这一创新方法的过程、实现及其优势。

图片来自 Jared Murray,来源于Unplash

特征选择是构建机器学习模型过程中的一个决定性步骤。 为模型和我们想要实现的任务选择合适的特征,确实可以提升性能。事实上,某些特征可能会引入噪声,从而干扰模型。

此外,选择特征对于处理高维数据集尤其重要。它能使模型更快、更好地学习。其核心思想是找到最优的特征数量和最具意义的特征。

在这篇文章中,我将解决这个问题,并通过引入一种新实现的特征选择方法,进一步探讨。尽管存在许多不同的特征选择过程,但由于很多文章已经在讨论它们,因此在这里不做介绍。我将重点介绍使用强化学习策略进行特征选择。

首先,将介绍强化学习,尤其是马尔可夫决策过程。这是数据科学领域中的一种全新方法,特别是在特征选择方面。接下来,我将介绍如何实现这一方法以及如何安装和使用 Python 库(FSRLearning)。最后,我将证明这种实现的有效性。在所有可能的特征选择方法中,如包装器或过滤器,强化学习是最强大和高效的

本文的目标是强调针对具体实际问题的应用实现。虽然一些参考资料将在文末提供,但本文会通过示例简化该问题的理论部分。

强化学习:特征选择的马尔可夫决策问题

已经证明,强化学习(RL)技术在解决类似游戏问题等方面非常高效。RL 的概念基于马尔可夫决策过程(MDP)。这里的重点不是深入定义 MDP,而是理解它的基本工作原理,以及它如何为我们的实际问题提供帮助。

RL 背后的直觉是,智能体从一个未知的环境开始。这个智能体必须采取行动来完成任务。根据智能体当前的状态以及他先前选择的动作,智能体将更倾向于选择某些动作。在每一个新状态到达并采取动作时,智能体都会获得奖励。以下是我们需要为特征选择定义的主要参数:

  • 什么是状态?

  • 什么是动作?

  • 奖励是什么?

  • 我们如何选择动作?

首先,状态只是数据集中的特征子集。例如,如果数据集有三个特征(年龄、性别、身高)加上一个标签,以下将是可能的状态:

[]                                              --> Empty set                           
[Age], [Gender], [Height]                       --> 1-feature set
[Age, Gender], [Gender, Height], [Age, Height]  --> 2-feature set
[Age, Gender, Height]                           --> All-feature set

在一个状态中,特征的顺序并不重要,稍后在本文中会解释为什么。我们必须将其视为一个集合,而非特征的列表。

关于动作,从一个子集出发,我们可以通过增加一个当前状态中未曾探索的特征,转向任何其他子集。在特征选择问题中,动作就是选择一个在当前状态中未曾探索的特征,并将其添加到下一个状态。以下是一些可能的动作示例:

[Age] -> [Age, Gender]
[Gender, Height] -> [Age, Gender, Height]

下面是一些不可能的动作示例:

[Age] -> [Age, Gender, Height]
[Age, Gender] -> [Age]
[Gender] -> [Gender, Gender]

我们已定义了状态和动作,但未定义奖励。奖励是一个实数,用于评估状态的质量。例如,如果一个机器人试图到达迷宫的出口,并决定将出口作为下一个动作,那么与该动作相关的奖励将是“好”。如果他选择进入一个陷阱作为下一个动作,则奖励将是“不好”。奖励是一个值,反映了先前采取的动作的信息。

在特征选择问题中,一个有趣的奖励可能是通过添加新特征使模型的准确性得到提高的值。下面是奖励计算的一个示例:

[Age] --> Accuracy = 0.65
[Age, Gender] --> Accuracy = 0.76
Reward(Gender) = 0.76 - 0.65 = 0.11

对于每个首次访问的状态,都会用特征集训练一个分类器。这个值会被存储在状态中,并且分类器的训练是非常昂贵的,即使该状态稍后再次被访问,也只会训练一次。分类器不考虑特征的顺序。这就是为什么我们可以将此问题视为图而不是树的原因。在这个示例中,选择性别作为新特征的动作的奖励是当前状态和下一个状态准确性的差值。

每个状态都有多个可能的动作和相关的奖励(图像来自作者)。

在上面的图中,每个特征都被映射为一个数字(例如,“Age”是 1,“Gender”是 2,“Height”是 3)。完全可以选择其他度量来最大化以找到最优的特征集。在许多商业应用中,更看重召回率而不是准确率。

下一个重要问题是,我们如何从当前状态选择下一个状态,或者说如何探索我们的环境。我们必须找到最优的方式来执行此操作,因为这很快会变成一个非常复杂的问题。实际上,如果我们天真地探索一个具有 10 个特征的问题中的所有可能特征集,那么状态的数量将是

10! + 2 = 3 628 802 possible states

+2 是因为我们考虑了一个空状态和一个包含所有可能特征的状态。在这个问题中,我们必须在所有状态上训练相同的模型,以获得最大化准确性的特征集。在强化学习方法中,我们不需要遍历所有状态,并且每次进入一个已访问过的状态时不需要重新训练模型。

我们必须为这个问题确定一些停止条件,这些条件会在后面详细说明。现在已经选择了 epsilon-贪婪状态选择方法。这个方法的思路是,从当前状态出发,我们以 epsilon 的概率(介于 0 和 1 之间,通常在 0.2 左右)随机选择下一个动作,否则选择最大化某个函数的动作。对于特征选择,那个函数就是每个特征对模型准确性贡献的平均奖励。

epsilon-贪婪算法包含两个步骤:

  1. 随机阶段:以 epsilon 的概率,我们在当前状态的所有可能邻居中随机选择下一个状态(我们可以假设使用均匀选择或 softmax 选择)。

  2. 贪婪阶段:我们选择下一个状态,使得添加到当前状态的特征对模型准确性的贡献最大。为了减少时间复杂度,我们已经初始化了一个包含每个特征对应值的列表。每次选择一个特征时,这个列表都会更新。感谢以下公式,更新过程非常优化:

更新每个特征的奖励平均值列表(图片由作者提供)

  • AORf:特征“f”带来的奖励的平均值

  • k:特征“f”被选择的次数

  • V(F):特征集 F 的状态值(出于清晰考虑,本文未详细说明)

全局思路是找出哪个特征为模型带来了最多的准确度。这就是为什么我们需要浏览不同的状态,在多种不同的环境中评估特征对模型的全局准确值。

最后,我将详细说明两种停止条件。由于目标是最小化算法访问的状态数,我们需要对它们保持谨慎。我们访问的未访问过的状态越少,使用不同特征集训练模型的次数就越少。训练模型以获取准确度是时间和计算能力上最为耗费的阶段。

  1. 算法在最终状态停止,该状态是包含所有特征的集合。我们希望避免到达此状态,因为训练模型时最为昂贵。

  2. 此外,如果一系列访问过的状态的值逐渐降低,算法将停止浏览图形。已设定一个阈值,当数据集中所有特征的总数的平方根后,算法停止探索。

现在问题的建模已经解释清楚,我们将详细说明 Python 中的实现。

特征选择的强化学习 Python 库

有一个解决此问题的 Python 库可用。我将在本部分中解释它是如何工作的,并证明它是一种高效的策略。此外,本文还作为文档,你将在本部分结束时能够将此库用于你的项目。

1. 数据预处理

由于我们需要评估访问过的状态的准确性,因此需要将特征和数据输入模型,用于此特征选择任务。数据必须进行标准化,类别变量需要编码,并且行数应尽可能少(越小越快)。此外,创建特征与整数之间的映射非常重要,正如前面部分所述。此步骤不是强制性的,但非常推荐。该步骤的最终结果是得到一个包含所有特征的 DataFrame 和一个包含要预测标签的 DataFrame。以下是使用一个基准数据集的示例(可以在这里找到 UCI Irvine 机器学习库)。

处理数据

2. 安装和导入 FSRLearning 库

第二步是通过 pip 安装库。以下是安装命令:

pip install FSRLearning

导入库时,可以使用以下代码:

导入库

你将能够通过创建一个 Feature_Selector_RL 对象来创建一个特征选择器。需要填写一些参数。

  • feature_number(整数):数据框架 X 中的特征数量

  • feature_structure(字典):图形实现的字典

  • eps(浮动 [0; 1]):选择随机下一个状态的概率,0 表示完全贪婪算法,1 表示完全随机

  • alpha(浮动 [0; 1]):控制更新速率,0 表示几乎不更新,1 表示完全更新

  • gamma(浮动 [0, 1]):观察下一个状态的调节因子,0 表示目光短浅的情况,1 则表现出远见行为

  • nb_iter(整数):遍历图形的序列数量

  • starting_state(“empty” 或 “random”):如果为“empty”,算法从空状态开始;如果为“random”,算法从图中的随机状态开始

所有参数都可以调节,但对于大多数问题来说,只需要少量迭代就能得到良好的结果(大约 100 次),而 epsilon 值通常设为 0.2 就足够了。起始状态有助于更高效地浏览图形,但它可能非常依赖数据集,两个值都可以进行测试。

最后,我们可以通过以下代码非常简单地初始化选择器:

选择器对象初始化

在大多数机器学习库的基础上,训练算法非常简单:

这里是输出的一个示例:

选择器的输出(图片由作者提供)

输出是一个五元组,如下所示:

  • 数据框架 X 中特征的索引(类似映射)

  • 特征观察的次数

  • 在所有迭代后,特征带来的奖励的平均值

  • 特征从最不重要到最重要的排名(这里 2 是最不重要,7 是最重要的特征)

  • 全局访问的状态数量

此选择器的另一个重要方法是与 Scikit-Learn 的 RFE 选择器进行比较。它的输入为 X、y 和选择器的结果。

输出是在每一步选择后,打印 RFE 和 FSRLearning 的全局指标。它还输出模型准确性的视觉比较,x 轴为选定特征的数量,y 轴为准确性。两条水平线分别为每种方法的准确性中位数。以下是一个示例:

RL 方法与 RFE 方法的比较(图片由作者提供)

Average benchmark accuracy : 0.854251012145749, rl accuracy : 0.8674089068825909 
Median benchmark accuracy : 0.8552631578947368, rl accuracy : 0.868421052631579 
Probability to get a set of variable with a better metric than RFE : 1.0 
Area between the two curves : 0.17105263157894512

在这个示例中,RL 方法总是为模型提供比 RFE 更好的特征集。我们可以在排序后的特征集中选择任何子集,它将为模型提供更好的准确性。我们可以多次运行模型和比较器以获得非常准确的估计,但 RL 方法总是更好。

另一个有趣的方法是 get_plot_ratio_exploration。它绘制了一个图表,比较已访问节点与在精确迭代中访问的节点数。

每次迭代中访问与未访问状态的比较(图片来自作者)

此外,由于第二个停止条件,算法的时间复杂度呈指数级下降。因此,即使特征数量很大,收敛也会迅速找到。下面的图展示了某一大小的特征集被访问的次数。

根据状态大小绘制的访问状态数量(图片来自作者)

在所有迭代中,算法访问的状态包含不超过 6 个变量。超过 6 个变量时,我们可以看到访问的状态数量在减少。这是一个良好的行为,因为使用较小特征集训练模型比使用大特征集更快速。

结论与参考文献

总的来说,我们可以看到强化学习方法对于最大化模型的度量非常高效。它总是快速收敛到一个有趣的特征子集。此外,这种方法在 ML 项目中非常容易且快速实现,使用 FSRLearning 库即可。

该项目的 GitHub 仓库及完整文档可在 这里获取。

如果你希望与我联系,可以直接在 linkedin 这里找到我。

本库的实现得到了以下两篇文章的帮助:

物理中的强化学习:常微分方程和超参数调整

原文:towardsdatascience.com/reinforcement-learning-for-physics-odes-and-hyperparameter-tuning-2c0a29752a67?source=collection_archive---------6-----------------------#2024-10-17

使用 Gymnasium 控制常微分方程并优化算法的超参数

罗伯特·埃特Towards Data Science 罗伯特·埃特

·发表于 Towards Data Science ·阅读时间:11 分钟·2024 年 10 月 17 日

--

图片由 布莱斯·库珀 拍摄,来源于 Unsplash

如之前讨论的,强化学习(RL)为解决控制非线性物理系统的挑战提供了一个强大的新工具。非线性物理系统的特点是行为复杂,其中输入的微小变化可能导致输出的剧烈变化,或者大输入可能只带来微小的输出变化。解决方案可能会分裂,相同的条件可能会产生不同的输出,甚至可能具有“记忆”,以路径依赖的形式存在。我们介绍了两种将强化学习应用于非线性物理系统的不同方法:传统的基于神经网络的软演员评论家(SAC)方法和一种不常见的基于遗传算法的遗传编程(GP)方法。

简而言之,SAC 使用两个神经网络,一个用于学习环境的行为,一个用于确定最优策略。在模型训练过程中,网络不断更新,环境学习“批评者”网络帮助评估和改进决策“行动者”网络的策略。遗传编程(GP)基于生成一组“森林”随机数学方程,评估它们在环境中的表现,然后通过变异、组合或创建新的随机方程来提高表现。应用到 Gymnasium 的 摆经典控制 环境,GP 方法显示出更快的收敛速度。现在我们扩展这项研究,(1) 引入更复杂的基于常微分方程的物理系统,(2) 探讨超参数调优对 SAC 和 GP 算法表现的影响。

处理常微分方程(ODE)

物理系统通常可以通过微分方程来建模,或是包含导数的方程。力,因此也包括牛顿定律,可以通过导数来表示,麦克斯韦方程也是如此,因此微分方程可以描述大多数物理问题。微分方程描述了一个系统如何根据当前状态变化,实际上定义了状态转换。微分方程系统可以写成矩阵/向量形式:

其中,x 是状态向量,A 是由物理动态决定的状态转换矩阵,x 点(或 dx/dt)是状态随时间变化的变化量。本质上,矩阵 A 对状态 x 进行作用,使其在时间上向前推进一个小步。这种表述通常用于线性方程(其中 A 的元素不包含任何状态向量),但也可以用于非线性方程,在这种情况下,A 的元素可能包含状态向量,这会导致上述复杂行为。这方程描述了一个环境或系统如何随时间发展,从特定的初始条件开始。在数学中,这些被称为初值问题,因为评估系统如何发展需要指定一个起始状态。

上述表达式描述了一类特殊的微分方程,常微分方程(ODE),其中所有导数都是一个变量的函数,通常是时间,但有时也可以是空间。点表示 dx/dt,即状态随时间增量变化的变化量。常微分方程(ODE)已被广泛研究,线性常微分方程系统有许多现成的解析解方法。解析解可以将解表示为变量的函数,使其在探索整个系统行为时更加灵活。非线性常微分方程的解法较少,但某些类别的系统仍然可以找到解析解。大多数情况下,非线性(以及某些线性)常微分方程最好通过仿真来求解,在这种方法中,解作为每个时间步的数值值被确定。

模拟是通过寻找微分方程的近似解来进行的,通常是通过转化为代数方程,在一小段时间变化内,保持已知精度。然后,计算机可以通过许多小的时间变化步骤,展示系统的发展过程。有很多算法可以计算这个过程,例如 Matlab 的 ODE45 或 Python SciPy 的 solve_ivp 函数。这些算法接受一个 ODE 和一个起始点/初始条件,自动确定最佳步长,并在系统中推进到指定的结束时间。

如果我们能够对 ODE 系统应用正确的控制输入,我们通常可以将其驱动到期望的状态。如上次所讨论的,强化学习(RL)提供了一种确定非线性系统正确输入的方法。为了开发 RL,我们将再次使用健身环境,但这一次我们将基于自己的 ODE 创建一个自定义的健身环境。按照Gymnasium 文档,我们创建一个观察空间来覆盖我们的状态空间,并为控制空间创建一个动作空间。我们初始化/重置健身环境到状态空间中的一个任意点(但在这里我们必须小心,并非所有期望的最终状态都能从任何初始状态到达某些系统)。在健身环境的步进函数中,我们在 ODE 的短时间范围内执行一步,使用 Python SciPy 的 solve_ivp 函数应用算法估算的输入。Solve_ivp 调用一个函数,解决我们正在处理的特定 ODE。代码可在git上找到。init 和 reset 函数是直接的;init 为系统中的每个状态创建观察空间,reset 为这些变量在域内设置一个距离原点最小的随机起始点。在步进函数中,注意 solve_ivp 行,它调用实际的动态方程,在短时间步长内求解动态 ODE,传递应用的控制 K。

#taken from https://www.gymlibrary.dev/content/environment_creation/
#create gym for Moore-Greitzer Model
#action space: continuous  +/- 10.0 float , maybe make scale to mu 
#observation space:  -30,30 x2 float for x,y,zand
#reward:  -1*(x²+y²+z²)¹/2 (try to drive to 0)

#Moore-Grietzer model:

from os import path
from typing import Optional

import numpy as np
import math

import scipy
from scipy.integrate import solve_ivp

import gymnasium as gym
from gymnasium import spaces
from gymnasium.envs.classic_control import utils
from gymnasium.error import DependencyNotInstalled
import dynamics  #local library containing formulas for solve_ivp
from dynamics import MGM

class MGMEnv(gym.Env):
    #no render modes
    def __init__(self, render_mode=None, size=30):

        self.observation_space =spaces.Box(low=-size+1, high=size-1, shape=(2,), dtype=float)

        self.action_space = spaces.Box(-10, 10, shape=(1,), dtype=float) 
        #need to update action to normal distribution

    def _get_obs(self):
        return self.state

    def reset(self, seed: Optional[int] = None, options=None):
        #need below to seed self.np_random
        super().reset(seed=seed)

        #start random x1, x2 origin
        np.random.seed(seed)
        x=np.random.uniform(-8.,8.)
        while (x>-2.5 and x<2.5):
            np.random.seed()
            x=np.random.uniform(-8.,8.)
        np.random.seed(seed)
        y=np.random.uniform(-8.,8.)
        while (y>-2.5 and y<2.5):
            np.random.seed()
            y=np.random.uniform(-8.,8.)
        self.state = np.array([x,y])
        observation = self._get_obs()

        return observation, {}

    def step(self,action):

        u=action.item()

        result=solve_ivp(MGM, (0, 0.05), self.state, args=[u])

        x1=result.y[0,-1]
        x2=result.y[1,-1]
        self.state=np.array([x1.item(),x2.item()])
        done=False
        observation=self._get_obs()
        info=x1

        reward = -math.sqrt(x1.item()**2)#+x2.item()**2)

        truncated = False #placeholder for future expnasion/limits if solution diverges
        info = x1

        return observation, reward, done, truncated, {}

以下是摩尔-格雷策模式(MGM)函数的动态特性。此实现基于 solve_ivp 文档。设置了限制以避免解发散;如果系统触及限制,奖励将较低,促使算法修正控制方法。基于此处讨论的模板创建 ODE 健身环境应该很简单:只需调整观测空间大小以匹配 ODE 系统的维度,并根据需要更新动态方程。

def MGM(t, A, K):
    #non-linear approximation of surge/stall dynamics of a gas turbine engine per Moore-Greitzer model from
    #"Output-Feedbak Cotnrol on Nonlinear systems using Control Contraction Metrics and Convex Optimization"
    #by Machester and Slotine
    #2D system, x1 is mass flow, x2 is pressure increase
    x1, x2 = A
    if x1>20:  x1=20.
    elif x1<-20:  x1=-20.
    if x2>20:  x2=20.
    elif x2<-20:  x2=-20.
    dx1= -x2-1.5*x1**2-0.5*x1**3
    dx2=x1+K
    return np.array([dx1, dx2])

在这个例子中,我们使用的是基于 Moore-Greitzer 模型(MGM)的常微分方程,描述了燃气涡轮发动机的喘振-失速动态¹。这个方程描述了发动机质量流量与压力之间的耦合阻尼振荡。控制器的目标是通过控制发动机的压力,迅速将振荡衰减到 0。MGM“推动了非线性控制设计的重大进展”,使其成为 SAC 和 GP 方法的有趣测试案例。描述该方程的代码可以在Github上找到。此处还列出了另外三个非线性 ODE。Van Der Pol 振荡器是一个经典的非线性振荡系统,基于电子系统的动力学。Lorenz 吸引子是一个看似简单的常微分方程系统,可以产生混沌行为,或者结果对初始条件高度敏感,以至于在一个不受控制的系统中,任何极小的起始点差异都将导致状态迅速发生广泛分化。第三个是 Duriez/Brunton/Noack 提供的平均场 ODE 系统,描述了稳定与不稳定波的复杂相互作用发展,作为湍流流体流动的近似。

为了避免重复上一篇文章的分析,我们这里只展示结果,指出再次证明 GP 方法在较低的计算时间内,生成了比 SAC/神经网络方法更好的控制器。下面的图形展示了在不受控制系统下、GP 控制器下以及 SAC 控制器下的振荡情况。

不受控制的动力学,由作者提供

GP 控制器结果,由作者提供

SAC 控制的动力学,由作者提供

两个算法都改善了不受控制的动力学。我们看到,虽然 SAC 控制器的反应更快(大约 20 个时间步长),但其准确性较低。GP 控制器的反应稍慢,但对两种状态都提供了平滑的行为。此外,正如之前所述,GP 在迭代次数上比 SAC 收敛得更快。

我们已经看到,体育馆可以轻松地适应用于在常微分方程(ODE)系统上训练强化学习(RL)算法,简要讨论了常微分方程在描述和探索物理动力学的 RL 控制方面的强大功能,并再次看到 GP 产生了更好的结果。然而,我们还没有尝试优化任何一个算法,而只是基本设置了算法参数,基本上是通过猜测来进行的。现在我们将通过扩展 MGM 研究来解决这一不足。

Sagemaker 超参数调优与自定义模型

如前所述,GP 和 SAC 都有一组定义模型的超参数。这些参数在模型训练过程中是固定的,但可以通过调整来尝试提高模型性能(如准确性或收敛速度)。以下表格简要回顾了 GP 算法中使用的超参数:

Ni、Ne、Nn、Pr、Pm、Pc 都影响探索与利用的平衡,即算法在尝试找到新的可能解决方案和优化已有最佳解决方案之间的时间分配。N 批次通过增加计算时间来换取更高的准确性和泛化能力。

这里实现的 SAC 有以下超参数:

为了简化编码和调优超参数,已设定了一些基本规则。每个隐藏层将具有相同数量的神经元,且每个神经网络(包括演员和评论员)将具有相同的维度(输入层和输出层除外),并有相同的批次/缓冲区用于更新。此外,每个神经网络将使用相同的激活函数和优化器。这些参数,尤其是神经网络的形状/维度,虽然是有效的超参数,但为了简化代码复杂性和减少计算时间,这里不进行调优。

调优超参数的目标是确定哪些超参数能够在最小的计算成本下生成最准确的模型。然而,调优超参数需要对每一组超参数进行模型训练。即使是对于适量数量的超参数,如果我们希望测试这些参数的广泛值,探索整个超参数空间也可能会导致几何级数增长的测试矩阵。这个问题更加复杂,因为参数可能是耦合的(即一个参数的最优值可能会根据另一个参数的设置而变化)。有几种方法可以调优超参数。网格搜索会测试整个网格的每种组合,因此需要仔细选择要测试的参数及其值。随机搜索则从网格中随机选择参数。最后,也可以使用一些数学优化方法,如贝叶斯优化或其他机器学习算法。无论如何,最佳的方法需要仔细考虑(也许还需要超超参数优化……)。

AWS Sagemaker 提供了内置的超参数优化功能,适用于 Sagemaker 包含的或自定义的算法。Sagemaker 的调优选项包括随机、网格、贝叶斯优化或超带(该方法倾向于选择表现较好的超参数集,并且可以在表现较差的超参数集下提前停止)。要使用 Sagemaker 的超参数调优,我们必须将算法作为 Docker 容器提供给 Sagemaker,并将容器镜像和训练脚本传入超参数调优对象中。

由于 GP 和特定的 SAC 实现都没有使用现有的 SageMaker 算法或框架(这里使用的 SAC 基于 Jax 和 Haiku,而不是 tensorflow、pytorch 或 mxnet),我们需要创建自定义的 RL 框架。在探索了多个教程并经过多次试错之后,我成功地构建了能够正常工作的容器和用于超参数调优的训练脚本。过程中有几个难点;例如,我发现必须将训练文件压缩成 zip 文件,上传到 S3,然后在 S3 中传递该 zip 文件的路径,以便成功使用 SageMaker“估算器”ML 对象的超参数参数。用于 SageMaker 的 Dockerfile、容器文件、训练脚本和 Jupyter 笔记本可以在 Git 上找到,分别对应SACGP。部分使用的源代码链接可以在 Git 的笔记本中找到。

这种方法可以进一步优化;例如,app.py 文件可能不需要包含在容器中。此外,我将自定义的 ODE gymnasium 放在了“经典控制”gymnasium 中,并将其本地加载,以减少从头开始构建自己 gymnasium 的时间。

一旦容器正常工作,我大致按照AWS 博客设置了超参数调优任务。为了使超参数在训练脚本中生效(GP 使用的是 app.py,SAC 使用的是 sacapp.py),我根据SageMaker GitHub 示例设置了一个 argparse 来处理这些参数。为了限制调优任务的运行次数(以及个人成本),我选择了一个有限的超参数集,专注于探索这个概念并评估调优效果。

运行超参数调优任务非常快;结果如下所示:

只有变异概率(Pm)在范围边界附近有一个最佳值。

SageMaker 的示例提供了超参数可视化脚本,允许我们查看调优任务的进展。下面我们回顾 SAC 的调优过程(GP 的超参数调优结果为了简洁省略)。首先,我们看到不同调优任务的概览(方形表示任务提前停止,圆形表示任务已完成),并查看了奖励随时间的变化。

可视化还提供了按参数划分的性能细分,帮助我们深入了解不同参数对算法性能的影响。下面我们看看每个隐藏层的神经元数量,发现优化趋势集中在 8 左右。

我们仅仅触及了常微分方程(ODE)和超参数的表面。具体而言,SAC 调优的探索仍然是初步的;神经网络设计本身就是一门科学(或者说艺术)。然而,本文希望能够为物理动态中的强化学习应用和优化提供一些见解和起点!

[1] Manchester, Ian R., 和 Jean-Jacques E. Slotine. “使用控制收缩度量和凸优化进行非线性系统的输出反馈控制。” 2014 年第 4 届澳大利亚控制会议(AUCC)(2014 年 11 月)。

针对 LLM 的人类反馈强化学习(RLHF)

原文:towardsdatascience.com/reinforcement-learning-from-human-feedback-rlhf-for-llms-9cd1288c9a50?source=collection_archive---------7-----------------------#2024-09-23

LLMs

大型语言模型背后的关键技术终极指南

Michał OleszakTowards Data Science Michał Oleszak

·发表于 Towards Data Science ·14 分钟阅读·2024 年 9 月 23 日

--

人类反馈强化学习(RLHF)已被证明是解锁当今大型语言模型(LLMs)全部潜力的关键。或许没有比 OpenAI 的 GPT-3 模型更好的证据来证明这一点。该模型于 2020 年发布,但正是经过 RLHF 训练后的版本——被称为 ChatGPT——才一夜之间成名,吸引了数百万人的关注,并为对话型人工智能设立了新标准。

在 RLHF 出现之前,LLM 的训练过程通常包括一个预训练阶段,在此阶段模型学习语言的一般结构,以及一个微调阶段,在此阶段模型学习执行特定任务。通过将人类判断作为第三个训练阶段,RLHF 确保模型不仅能生成连贯且有用的输出,还能更紧密地与人类的价值观、偏好和期望对齐。它通过一个反馈循环实现这一点,在这个循环中,人类评估者对模型的输出进行评分或排名,然后用于调整模型的行为。

本文探讨了RLHF的复杂性。我们将研究其在语言建模中的重要性,详细分析其内部运作,并讨论...

强化学习:深度 Q 网络

原文:towardsdatascience.com/reinforcement-learning-from-scratch-deep-q-networks-0a8d33ce165b?source=collection_archive---------1-----------------------#2024-05-23

使用深度 Q 网络在 Python 中教一艘航天飞机降落在月球上:强化学习的数学深度解析

Cristian LeoTowards Data Science Cristian Leo

·发布于 Towards Data Science ·28 分钟阅读·2024 年 5 月 23 日

--

由 DALL-E 生成的图像

在强化学习(RL)中,Q 学习是一个基础算法,帮助智能体通过学习一个策略来最大化累积奖励,从而在环境中导航。它通过更新一个行动-价值函数来实现这一点,该函数根据接收到的奖励和未来的估计,估算在给定状态下采取特定行动的预期效用(这听起来不熟悉吗?别担心,我们会在接下来的部分一起讲解)。

然而,传统的 Q 学习存在一些挑战。随着状态空间的增大,它在扩展性上遇到困难,并且在具有连续状态和动作空间的环境中效果较差。这就是深度 Q 网络(DQNs)发挥作用的地方。DQNs 使用神经网络来逼近 Q 值,使得智能体能够有效地处理更大和更复杂的环境。

在本文中,我们将深入探讨深度 Q 网络。我们将探讨 DQN 如何克服传统 Q 学习的局限性,并讨论构成 DQN 的关键组件。我们还将逐步讲解从头开始实现一个 DQN,并将其应用于一个更复杂的环境。通过本文的学习,您将对…

强化学习,第一部分:简介与主要概念

原文:towardsdatascience.com/reinforcement-learning-introduction-and-main-concepts-48ea997c850c?source=collection_archive---------2-----------------------#2024-04-09

迈出进入强化学习世界的第一步

Vyacheslav EfimovTowards Data Science Vyacheslav Efimov

·发布于Towards Data Science ·阅读时间 11 分钟·2024 年 4 月 9 日

--

介绍

强化学习是机器学习的一个特殊领域,与监督学习或无监督学习中使用的经典方法有很大不同。

最终目标是开发一个所谓的智能体,使其能够在环境中执行最优的动作。最初,智能体通常表现得非常差,但随着时间的推移,它通过与环境互动,采用试错法逐步调整策略。

强化学习的魅力在于,同样的算法可以用来让智能体适应完全不同、未知且复杂的条件。

强化学习有广泛的应用,通常用于无法通过经典方法解决的问题:

  • 游戏。现有的方法可以设计最优的游戏策略,并超越人类。最著名的例子是国际象棋和围棋。

  • 机器人技术。先进的算法可以集成到机器人中,帮助它们移动、搬运物品或完成家庭中的常规任务。

  • 自动驾驶。强化学习方法可以开发用于…

强化学习第二部分:策略评估与改进

原文:towardsdatascience.com/reinforcement-learning-part-2-policy-evaluation-and-improvement-59ec85d03b3a?source=collection_archive---------1-----------------------#2024-04-23

从数据到决策:通过策略改进方法最大化奖励以获得最佳策略

Vyacheslav EfimovTowards Data Science Vyacheslav Efimov

·发表于Towards Data Science ·13 分钟阅读·2024 年 4 月 23 日

--

引言

强化学习是机器学习中的一个领域,引入了智能体的概念,智能体必须在复杂环境中学习最佳策略。智能体根据其行动得到的奖励以及环境的状态来学习。强化学习是一个复杂的主题,与机器学习的其他领域有显著的不同。这就是为什么它只有在其他方法无法解决特定问题时才应该使用。

强化学习的惊人灵活性在于相同的算法可以用来使智能体适应完全不同、未知和复杂的环境条件。

注意。为了充分理解本文所包含的思想,强烈建议熟悉本文系列第一部分中介绍的强化学习的主要概念。

强化学习,第三部分:蒙特卡罗方法

原文:towardsdatascience.com/reinforcement-learning-part-3-monte-carlo-methods-7ce2828a1fdb?source=collection_archive---------3-----------------------#2024-05-23

从赌场到人工智能:揭示蒙特卡罗方法在复杂环境中的强大力量

Vyacheslav EfimovTowards Data Science Vyacheslav Efimov

·发表于Towards Data Science ·阅读时间:12 分钟·2024 年 5 月 23 日

--

引言

强化学习是机器学习中的一个领域,它引入了一个智能体的概念,该智能体必须在复杂的环境中学习最佳策略。智能体通过其行动学习,依据环境状态获得奖励。强化学习是一个复杂的课题,与机器学习的其他领域有显著区别。因此,只有在给定问题无法通过其他方式解决时,才应使用强化学习。

在本文中,我们将探索蒙特卡罗算法。与标准方法相比,蒙特卡罗算法的优雅之处在于,它们不需要了解环境的动态特性。这一点非常重要,因为在现实生活中,我们通常并不知道状态之间的所有可能转移概率。

注意:为了充分理解本文中包含的概念,强烈建议熟悉强化学习的主要概念,以及本系列前两篇文章中介绍的策略改进方法。

强化学习,第四部分:蒙特卡洛控制

原文:towardsdatascience.com/reinforcement-learning-part-4-monte-carlo-control-ae0a7f29920b?source=collection_archive---------4-----------------------#2024-06-11

利用蒙特卡洛算法发现最佳策略

Vyacheslav EfimovTowards Data Science Vyacheslav Efimov

·发表于Towards Data Science ·阅读时间:13 分钟·2024 年 6 月 11 日

--

引言

强化学习是机器学习中的一个领域,它引入了智能体的概念,智能体必须在复杂的环境中学习最优策略。智能体通过其行为从环境的状态中获得奖励,进而学习。强化学习是一个困难的话题,与机器学习的其他领域有很大的不同。因此,只有在某个问题无法通过其他方式解决时,才应该使用强化学习。

强化学习的一个令人难以置信的地方在于,相同的算法可以用来使智能体适应完全不同、未知且复杂的环境条件。

特别地,蒙特卡洛算法不需要任何关于环境动态的知识。这是一个非常有用的特性,因为在现实生活中,我们通常无法获得这些信息。在上一篇文章中,我们讨论了蒙特卡洛方法的基本思想,这次我们将重点介绍一些特殊方法来改进这些算法。

注意:为了充分理解本文所涉及的概念,强烈建议先熟悉在第三部分中介绍的蒙特卡洛算法的主要概念…

强化学习,第五部分:时间差学习

原文:towardsdatascience.com/reinforcement-learning-part-5-temporal-difference-learning-cacf7854fe0c?source=collection_archive---------4-----------------------#2024-07-13

智能地协同动态规划和蒙特卡洛算法

Vyacheslav EfimovTowards Data Science Vyacheslav Efimov

·发表于Towards Data Science ·15 分钟阅读·2024 年 7 月 13 日

--

引言

强化学习是机器学习的一个领域,引入了代理在复杂环境中学习最优策略的概念。代理通过根据环境状态采取行动来获得奖励,从而学习。强化学习是一个具有挑战性的主题,与机器学习的其他领域有显著不同。

强化学习的显著特点是,同样的算法可以用来使代理适应完全不同、未知且复杂的环境条件。

注意:为了充分理解本文所包含的概念,强烈建议先了解之前文章中讨论的动态规划蒙特卡洛方法

## 强化学习,第二部分:策略评估与改进

从数据到决策:通过策略改进方法最大化奖励,寻找最优策略

towardsdatascience.com

强化学习,第六部分:n 步自举法

原文:towardsdatascience.com/reinforcement-learning-part-6-n-step-bootstrapping-e666f8cc7973?source=collection_archive---------7-----------------------#2024-08-07

拓展边界:泛化时间差分算法

Vyacheslav EfimovTowards Data Science Vyacheslav Efimov

·发布于 Towards Data Science ·6 分钟阅读·2024 年 8 月 7 日

--

介绍

强化学习是机器学习中的一个领域,引入了代理在复杂环境中学习最优策略的概念。代理通过其行动从环境的状态中获得奖励,从而进行学习。强化学习是一个具有挑战性的话题,与机器学习的其他领域有显著区别。

强化学习的独特之处在于,相同的算法可以用来帮助代理适应完全不同、未知且复杂的条件。

注意:为了充分理解本文中包含的概念,强烈建议熟悉之前文章中介绍的蒙特卡洛方法时间差分学习的基础知识。

## 强化学习,第三部分:蒙特卡洛方法

从赌场到人工智能:揭示蒙特卡洛方法在复杂环境中的威力

towardsdatascience.com

强化学习,第七部分:值函数逼近简介

原文:towardsdatascience.com/reinforcement-learning-part-7-introduction-to-value-function-approximation-2e22495f7008?source=collection_archive---------5-----------------------#2024-08-22

将强化学习从表格方法扩展到大空间

Vyacheslav EfimovTowards Data Science Vyacheslav Efimov

·发表于 Towards Data Science ·阅读时间:10 分钟·2024 年 8 月 22 日

--

强化学习是机器学习中的一个领域,它引入了智能体在复杂环境中学习最佳策略的概念。智能体通过自己的行动来学习,而这些行动会根据环境的状态产生奖励。强化学习是一个具有挑战性的话题,与机器学习的其他领域有显著的不同。

强化学习的一个显著特点是,同样的算法可以用于使智能体适应完全不同、未知且复杂的条件。

注意:为了充分理解本文中的概念,强烈推荐先熟悉之前的文章中讨论的概念。

Vyacheslav Efimov

Vyacheslav Efimov

强化学习

查看列表8 篇故事

关于本文

到目前为止,我们只讨论了表格强化学习方法。在这种情况下,“表格”一词表示所有可能的…

强化学习,第八部分:特征状态构建

原文:towardsdatascience.com/reinforcement-learning-part-8-feature-state-construction-62e7d2fc5152?source=collection_archive---------4-----------------------#2024-09-21

通过智能地将状态特征融入学习目标来增强线性方法

Vyacheslav EfimovTowards Data Science Vyacheslav Efimov

·发表于 Towards Data Science ·阅读时间 13 分钟·2024 年 9 月 21 日

--

强化学习(Reinforcement learning)是机器学习的一个领域,引入了智能体在复杂环境中学习最优策略的概念。智能体通过其行动来学习,根据环境的状态产生奖励。强化学习是一个具有挑战性的话题,与机器学习的其他领域有显著不同。

强化学习的一个显著特点是,相同的算法可以用于使智能体适应完全不同、未知且复杂的环境条件。

关于本文

第七部分 中,我们介绍了价值函数近似算法,这些算法扩展了标准的表格方法。除此之外,我们特别关注了一个非常重要的情况,即当近似的价值函数是线性时。正如我们所发现的,线性化提供了保证的收敛性,能够收敛到全局最优解TD 固定点(在半梯度方法中)。

问题在于,有时我们可能希望使用更复杂的近似价值函数,而不仅仅是一个简单的标量积,同时又不脱离线性优化空间。

强化学习:从自动驾驶汽车到自动驾驶实验室

原文:towardsdatascience.com/reinforcement-learning-self-driving-cars-to-self-driving-labs-018f465d6bbc?source=collection_archive---------2-----------------------#2024-12-06

理解生物领域中 AI 应用对于机器学习工程师的意义

Meghan HeintzTowards Data Science Meghan Heintz

·发表于 Towards Data Science ·9 分钟阅读·2024 年 12 月 6 日

--

图片来源:Ousa CheaUnsplash

任何尝试教狗狗新把戏的人都知道强化学习的基本原理。我们可以通过不断奖励狗狗的服从行为并惩罚其不良行为来改变它的行为。在强化学习(RL)中,狗狗就是一个智能体,它在环境中进行探索,并根据可用的动作获得奖励惩罚。这个非常简单的概念已经通过数学形式化,并扩展到推动自动驾驶和自动驾驶/自主实验室领域的发展。

作为一名纽约人,我在开车时常常感到焦虑,拥有一个冷静的机器人司机的好处显而易见。而当我考虑到新一代生成式人工智能生物工具的巨大潜力时,自动化实验室的好处才变得明显。我们可以生成大量高质量的假设,目前的瓶颈是实验验证。

如果我们能利用强化学习(RL)教会一辆车自我驾驶,我们是否也能用它来处理人工智能生成的创意实验验证呢?本文将继续我们的系列文章,为机器学习工程师理解 AI 在生物学中的应用,通过学习强化学习如何应用于自动驾驶汽车和自主实验室(例如,AlphaFlow)。

自动驾驶汽车

理解强化学习的最一般方式是将其视为一种通过实践来学习的方法。代理与环境互动,学习哪些行为能产生最高的奖励,并通过试错避免惩罚。如果通过试错的方式在一辆重达 2 吨的金属盒子中以 65 英里每小时的速度行驶听起来有些可怕,像是某个监管机构不会批准的事情,那么你是对的。大多数强化学习的驾驶实验是在模拟环境中进行的,而当前的自动驾驶技术仍然侧重于监督学习方法。但是,Alex Kendall证明了只需几台廉价的摄像头、一个庞大的神经网络和二十分钟的时间,汽车就能自学如何驾驶。那么他是如何做到的呢?

Alex Kendall展示了如何使用强化学习教一辆车在真实道路上行驶。

更主流的自动驾驶方法使用专门的模块来处理每个子问题:车辆管理、感知、地图绘制、决策等。但 Kendall 团队采用了深度强化学习方法,这是一种端到端学习方法。这意味着,不是将问题拆解成多个子问题并为每个子问题训练算法,而是通过一个算法根据输入做出所有决策(输入-> 输出)。这种方法被提议作为对监督学习方法的改进,因为将多个不同的算法组合在一起会导致复杂的相互依赖关系。

强化学习是一类旨在解决马尔可夫决策过程(MDP)的问题,MDP 是一个决策问题,其结果部分是随机的,部分是可控的。Kendall 团队的目标是将驾驶定义为一个 MDP,特别是通过简化目标——车道跟踪来进行定义。以下是强化学习的各个组成部分如何映射到自动驾驶问题中的解析:

  • 代理 A,即决策者。这就是司机。

  • 环境,即代理与之互动的所有事物。例如,汽车及其周围环境。

  • 状态 S,智能体当前情况的表示。汽车在道路上的位置。可以使用许多传感器来确定状态,但在肯达尔的示例中,只使用了单目相机图像。这样,它更接近人类驾驶时所拥有的信息。然后,图像通过变分自编码器(VAE)在模型中表示。

  • 动作 A,智能体做出的影响环境的选择。在哪里以及如何刹车、转弯或加速。

  • 奖励,来自环境对之前动作的反馈。肯达尔团队选择了“车辆在没有安全驾驶员介入的情况下行驶的距离”作为奖励。

  • 策略,智能体用来决定在给定状态下采取何种行动的策略。在深度强化学习中,策略由深度神经网络控制,在本案例中为深度确定性策略梯度(DDPG)。这是一种现成的强化学习算法,没有特定任务的适配,也称为演员网络。

  • 价值函数,智能体从给定状态(或状态-动作对)能够获得的期望奖励的估计。也称为评论家网络。评论家通过在训练过程中提供反馈,帮助引导演员,评估动作的质量。

用于学习驾驶策略和价值函数的演员-评论家算法,来自于《一天学会驾驶》

这些部分通过迭代学习过程结合在一起。智能体使用其策略在环境中采取行动,观察结果状态和奖励,并更新策略(通过演员)和价值函数(通过评论家)。以下是逐步的工作原理:

  1. 初始化:智能体从随机初始化的策略(演员网络)和价值函数(评论家网络)开始。它没有关于如何驾驶的先验知识。

  2. 探索:智能体通过采取包含一定随机性的动作来探索环境(探索噪声)。这确保智能体尝试广泛的动作来学习它们的效果,同时令监管者感到恐慌。

  3. 状态转移:根据智能体的动作,环境作出响应,提供一个新的状态(例如,下一张相机图像、速度和转向角度)和奖励(例如,未干预或没有驾驶违规的行驶距离)。

  4. 奖励评估:智能体通过观察奖励来评估其动作的质量。积极的奖励鼓励期望的行为(如保持在车道内),而稀疏或没有奖励则促使改进。

  5. 学习更新:智能体使用奖励和观察到的状态转移来更新其神经网络:

  • 评论网络(价值函数):评论网络通过更新其对 Q 函数的估计(Q 函数是估计在给定动作和状态下的奖励的函数),最小化时序差分(TD)误差,以改善其对长期奖励的预测。

  • 行动者网络(策略):行动者通过使用评论者的反馈来更新其策略,逐渐倾向于评论者预测将带来更高奖励的动作。

6. 重放缓冲区:经验(状态、动作、奖励、下一状态)存储在重放缓冲区中。在训练过程中,智能体从该缓冲区中采样以更新其网络,从而确保数据的高效使用和训练的稳定性。

7. 迭代:该过程反复进行。智能体通过试验和错误不断完善其策略和价值函数,逐步提高其驾驶能力。

8. 评估:在没有探索噪声的情况下测试智能体的策略,以评估其表现。在肯德尔的工作中,这意味着评估汽车保持在车道内并最大化自动行驶距离的能力。

进入一辆车,随机初始化权重后开车似乎有点令人生畏!幸运的是,肯德尔团队意识到超参数可以在三维仿真中进行调优,然后再转移到现实世界中。他们在虚幻引擎 4 中构建了一个仿真引擎,运行生成模型来模拟乡村道路、不同天气条件和路面纹理,从而创建训练仿真。这些重要的调优强化学习参数包括学习率、梯度步数等。它还确认了连续的动作空间比离散的动作空间更为优越,并且 DDPG 是解决该问题的合适算法。

这其中最有趣的方面之一是,它相较于主流方法具有更强的通用性。所采用的算法和传感器远没有像 Cruise 和 Waymo 等公司所要求的那么专业化。它不需要先进的地图数据或激光雷达(LIDAR)数据,这使得它能够扩展到新的道路和未映射的乡村地区。

另一方面,这种方法的某些缺点是:

  • 稀疏奖励:我们通常不会频繁地偏离车道,这意味着奖励通常只有在长时间保持在车道内时才会出现。

  • 延迟奖励:想象一下进入乔治·华盛顿大桥,你需要在上桥之前很久就选择一个车道。这种延迟了奖励,使得模型更难将动作与奖励关联起来。

  • 高维度:状态空间和可用动作都有多个维度。随着维度的增加,强化学习模型由于数据的复杂性容易出现过拟合或不稳定的情况。

也就是说,肯德尔团队的成就是朝着自动驾驶迈出的鼓舞人心的一步。他们的车道跟踪目标被故意简化,展示了强化学习(RL)可以轻松地被应用来帮助解决自动驾驶问题。现在,让我们来看一下它如何在实验室中应用。

自驾实验室(SDLs)

AlphaFlow的创始人认为,正如 Kendall 对驾驶的评估一样,实验室协议的开发也是一个马尔可夫决策问题。尽管 Kendall 将问题限定为车道跟踪,但 AlphaFlow 团队将他们的 SDL 问题限定为多步骤化学过程优化,用于核壳半导体纳米颗粒的壳层生长。半导体纳米颗粒在太阳能、生物医学设备、燃料电池、环境修复、电池等方面有广泛应用。发现这些材料类型的方法通常是耗时、劳动密集型和资源密集型,并且容易受到维度灾难的影响,即随着问题维度的增加,参数空间大小呈指数级增长。

他们基于强化学习的方法,AlphaFlow,成功地识别并优化了一条包含多达 40 个参数的新型多步骤反应路线,该路线优于传统的反应顺序。这展示了基于闭环强化学习的方法如何加速基础知识的获取。

维度灾难:图示了一个由四种可能步骤选择组成的批量多步骤合成的复杂性和所需资源呈指数级增长的情况,最多可达 32 个顺序步骤。来自AlphaFlow:使用强化学习引导的自驾流体实验室实现多步骤化学的自主发现与优化

胶体原子层沉积(cALD)是一种用于创建核壳纳米颗粒的技术。材料以逐层的方式在胶体颗粒或量子点上生长。该过程涉及交替进行反应物添加步骤,每一步都沉积一层单一的原子或分子层,然后通过洗涤去除多余的试剂。由于隐藏的状态或中间条件,步骤的结果可能会有所不同。这种可变性加强了将其视为马尔可夫决策问题的观点。

此外,该技术的逐层处理方式使其非常适合强化学习方法,在这种方法中我们需要清晰地定义状态、可用的动作奖励。此外,反应设计成在形成单一的、完整的原子或分子层后自然停止。这意味着实验高度可控,并且适合像微滴流反应器这样的工具。

以下是强化学习的各个组件如何映射到自驾实验室问题中的方式:

  • 代理决定下一个化学步骤(可以是新的表面反应、配体添加或洗涤步骤)

  • 环境是一个高效的微滴流反应器,能够自主地进行实验。

  • 状态S代表当前的试剂设置、反应参数和短期记忆(STM)。在这个例子中,STM 包括四个先前的注入条件。

  • 动作A是指像试剂添加、反应时机和清洗步骤这样的选择。

  • 奖励是产品的原位光学测量特性。

  • 策略和值函数是 RL 算法,它预测预期的奖励并优化未来的决策。在这种情况下,由一个集成神经网络回归器(ENN)和一个梯度提升决策树组成的信念网络,将状态-动作对分类为可行或不可行。

  • 展开策略使用信念模型来预测假设的未来动作序列的结果/奖励,并通过在所有预测的动作序列中应用决策策略来决定下一步最佳动作。

AlphaFlow 系统和工作流程的示意图。

(a) 基于 RL 的学习代理与自动化实验设置之间的反馈回路。

(b) 反应器系统示意图,包括关键模块:试剂注入、液滴混合、光学采样、相分离、废物收集和补充。

(c) 核心模块功能分解:配方、合成、表征和相分离。

(d) 流程图显示学习代理如何选择条件。

(e, f) 反应空间探索与优化概述:试剂注入的序列选择(P1:油胺,P2:硫化钠,P3:醋酸镉,P4:甲酰胺)以及基于已学习序列的体积-时间优化。

类似于 Kendall 团队使用虚幻引擎的方式,AlphaFlow 团队使用数字双胞胎结构帮助在进行物理实验之前预训练超参数。这使得模型可以通过模拟计算实验进行学习,并以更加高效的成本方式进行探索。

他们的方法成功探索并优化了一个 40 维的参数空间,展示了强化学习(RL)如何用于解决复杂的多步骤反应。这一进展对于提高实验验证的通量并帮助我们在多个领域取得突破可能至关重要。

结论

在这篇文章中,我们探索了如何将强化学习应用于自动驾驶和实验室工作的自动化。尽管面临挑战,但在这两个领域中的应用展示了 RL 在自动化中的潜力。通过 RL 推动基础知识的进展是作者特别感兴趣的方向。我期待学习更多关于强化学习在自动化实验室中的新兴应用。

感谢阅读本期的理解机器学习工程师在生物领域中的 AI 应用

使用 Llama3 模型进行关系抽取

原文:towardsdatascience.com/relation-extraction-with-llama3-models-f8bc41858b9e?source=collection_archive---------0-----------------------#2024-04-26

通过微调 Llama3-8B 并使用 Llama3-70B 创建的合成数据集增强关系抽取

Silvia OnofreiTowards Data Science Silvia Onofrei

·发表于Towards Data Science ·12 分钟阅读·2024 年 4 月 26 日

--

由 DALL-E 生成。

前提

关系抽取(RE)是从非结构化文本中提取关系的任务,用于识别不同命名实体之间的连接。这一任务通常与命名实体识别(NER)一起进行,是自然语言处理流水线中的一个重要步骤。随着大型语言模型(LLMs)的兴起,传统的监督方法——通过标注实体跨度并分类实体之间(如果有的话)的关系——在 LLM 驱动的方法下得到了增强或完全替代[1]。

Llama3 是生成式 AI 领域最近发布的主要版本[2]。基础模型有两种规格,分别为 8B 和 70B,预计很快会发布 400B 版本。这些模型可在 HuggingFace 平台上使用,详情请见[3]。70B 版本为 Meta 的全新聊天网站Meta.ai提供支持,展现出与 ChatGPT 相当的性能。8B 模型在同类中表现优异。Llama3 的架构与 Llama2 相似,性能提升主要得益于数据升级。该模型配备了升级版的分词器和扩展的上下文窗口。虽然被标记为开源,但仅发布了很小一部分数据。总体来说,这是一个非常优秀的模型,我迫不及待想尝试一下。

Llama3–70B 能够产生惊人的结果,但由于其规模庞大,它在本地系统上使用起来不切实际,成本高昂且难以操作。因此,为了充分利用其能力,我们让 Llama3–70B 教导较小的 Llama3–8B 如何从非结构化文本中进行关系抽取。

具体来说,在 Llama3–70B 的帮助下,我们构建了一个针对关系抽取的有监督微调数据集。然后,我们使用该数据集对 Llama3–8B 进行微调,以增强其关系抽取能力。

为了复现与本博客相关的Google Colab Notebook中的代码,你将需要:

  • HuggingFace 凭证(用于保存微调后的模型,可选)和 Llama3 访问权限,可以通过按照模型卡中的指示获取;

  • 一个免费的GroqCloud账户(你可以使用 Google 账户登录)和相应的 API 密钥。

工作空间设置

对于这个项目,我使用了一台配备 A100 GPU 和高内存设置的 Google Colab Pro。

我们首先安装所有需要的库:

!pip install -q groq
!pip install -U accelerate bitsandbytes datasets evaluate 
!pip install -U peft transformers trl 

我很高兴地注意到,整个设置从一开始就能够顺利运行,没有出现任何依赖问题,也无需从源代码安装transformers,尽管该模型是新的。

我们还需要授权 Google Colab 访问 Google Drive 和文件,并设置工作目录:

# For Google Colab settings
from google.colab import userdata, drive

# This will prompt for authorization
drive.mount('/content/drive')

# Set the working directory
%cd '/content/drive/MyDrive/postedBlogs/llama3RE'

对于那些希望将模型上传到 HuggingFace Hub 的人,我们需要上传 Hub 凭证。在我的情况下,这些凭证存储在 Google Colab 的机密中,可以通过左侧的密钥按钮访问。此步骤为可选。

# For Hugging Face Hub setting
from huggingface_hub import login

# Upload the HuggingFace token (should have WRITE access) from Colab secrets
HF = userdata.get('HF')

# This is needed to upload the model to HuggingFace
login(token=HF,add_to_git_credential=True)

我还添加了一些路径变量,以简化文件访问:

# Create a path variable for the data folder
data_path = '/content/drive/MyDrive/postedBlogs/llama3RE/datas/'

# Full fine-tuning dataset
sft_dataset_file = f'{data_path}sft_train_data.json'

# Data collected from the the mini-test
mini_data_path = f'{data_path}mini_data.json'

# Test data containing all three outputs
all_tests_data = f'{data_path}all_tests.json'

# The adjusted training dataset
train_data_path = f'{data_path}sft_train_data.json'

# Create a path variable for the SFT model to be saved locally
sft_model_path = '/content/drive/MyDrive/llama3RE/Llama3_RE/'

现在我们的工作空间已经设置好,我们可以进入第一步,即为关系抽取任务构建一个合成数据集。

创建用于关系抽取的合成数据集,采用 Llama3–70B

有几个关系抽取数据集可供使用,其中最著名的是CoNLL04数据集。此外,还有一些优秀的数据集,如 HuggingFace 上的web_nlg和 AllenAI 开发的SciREX。然而,这些数据集中的大多数都带有限制性许可证。

受到web_nlg数据集格式的启发,我们将构建我们自己的数据集。如果我们计划对一个在我们数据集上训练的模型进行微调,这种方法将特别有用。首先,我们需要收集一批短句子来进行关系抽取任务。我们可以通过多种方式来编纂这个语料库。

收集句子集合

我们将使用databricks-dolly-15k,这是一个由 Databricks 员工在 2023 年生成的开源数据集。该数据集设计用于监督微调,并包括四个特征:指令、上下文、响应和类别。分析了这八个类别后,我决定保留information_extraction类别中的上下文的第一句话。数据解析步骤如下:

from datasets import load_dataset

# Load the dataset
dataset = load_dataset("databricks/databricks-dolly-15k")

# Choose the desired category from the dataset
ie_category = [e for e in dataset["train"] if e["category"]=="information_extraction"]

# Retain only the context from each instance
ie_context = [e["context"] for e in ie_category]

# Split the text into sentences (at the period) and keep the first sentence
reduced_context = [text.split('.')[0] + '.' for text in ie_context]

# Retain sequences of specified lengths only (use character length)
sampler = [e for e in reduced_context if 30 < len(e) < 170]

选择过程产生了一个包含 1,041 个句子的数据集。鉴于这是一个小型项目,我没有精心挑选句子,因此一些样本可能不太适合我们的任务。在一个用于生产的项目中,我会仔细选择最合适的句子。然而,出于这个项目的目的,这个数据集已经足够了。

格式化数据

我们首先需要创建一个系统消息,定义输入提示并指示模型如何生成答案:

system_message = """You are an experienced annontator. 
Extract all entities and the relations between them from the following text. 
Write the answer as a triple entity1|relationship|entitity2\. 
Do not add anything else.
Example Text: Alice is from France.
Answer: Alice|is from|France.
"""

由于这是一个实验阶段,我对模型的要求保持最低限度。我测试了几个其他的提示,包括一些要求以 CoNLL 格式输出的提示,其中实体会被分类,模型的表现相当不错。然而,出于简便考虑,我们暂时还是从基础做起。

我们还需要将数据转换为对话格式:

messages = [[
    {"role": "system","content": f"{system_message}"},
    {"role": "user", "content": e}] for e in sampler]

Groq 客户端和 API

Llama3 几天前刚发布,API 选项的可用性仍然有限。虽然 Llama3–70B 提供了一个聊天接口,但这个项目需要一个 API,能够用几行代码处理我的 1,000 个句子。我找到了这个很棒的YouTube 视频,它解释了如何免费使用 GroqCloud API。更多细节请参考视频。

提个提醒:你需要登录并从GroqCloud网站获取一个免费的 API 密钥。我的 API 密钥已经保存在 Google Colab 的密钥中。我们从初始化 Groq 客户端开始:

import os
from groq import Groq

gclient = Groq(
    api_key=userdata.get("GROQ"),
)

接下来,我们需要定义几个辅助函数,使我们能够有效地与Meta.ai聊天接口互动(这些函数改编自YouTube 视频):

import time
from tqdm import tqdm

def process_data(prompt):

    """Send one request and retrieve model's generation."""

    chat_completion = gclient.chat.completions.create(
        messages=prompt, # input prompt to send to the model
        model="llama3-70b-8192", # according to GroqCloud labeling
        temperature=0.5, # controls diversity
        max_tokens=128, # max number tokens to generate
        top_p=1, # proportion of likelihood weighted options to consider
        stop=None, # string that signals to stop generating
        stream=False, # if set partial messages are sent
    )
    return chat_completion.choices[0].message.content

def send_messages(messages):

    """Process messages in batches with a pause between batches."""

   batch_size = 10
    answers = []

    for i in tqdm(range(0, len(messages), batch_size)): # batches of size 10

        batch = messages[i:i+10]  # get the next batch of messages

        for message in batch:
            output = process_data(message)
            answers.append(output)

        if i + 10 < len(messages):  # check if there are batches left
            time.sleep(10)  # wait for 10 seconds

    return answers

第一个函数process_data()作为 Groq 客户端聊天完成函数的封装器。第二个函数send_messages()将数据分批处理。如果你在 Groq Playground 页面点击设置链接,你会找到一个指向Limits的链接,里面详细列出了我们可以使用免费 API 的条件,包括请求次数和生成的 token 的上限。为了避免超出这些限制,我在每批 10 条消息后加入了 10 秒的延迟,虽然在我的情况下这并非严格必要。你可能会想尝试调整这些设置。

现在剩下的任务是生成我们的关系抽取数据,并将其与初始数据集进行整合:

# Data generation with Llama3-70B
answers = send_messages(messages)

# Combine input data with the generated dataset
combined_dataset = [{'text': user, 'gold_re': output} for user, output in zip(sampler, answers)]

评估 Llama3–8B 的关系抽取能力

在开始微调模型之前,评估模型在多个样本上的表现非常重要,以确定是否真的需要微调。

构建测试数据集

我们将从我们刚刚构建的数据集中选择 20 个样本,并将它们分开用于测试。其余的数据集将用于微调。

import random
random.seed(17)

# Select 20 random entries
mini_data = random.sample(combined_dataset, 20)

# Build conversational format
parsed_mini_data = [[{'role': 'system', 'content': system_message},
                     {'role': 'user', 'content': e['text']}] for e in mini_data]

# Create the training set
train_data = [item for item in combined_dataset if item not in mini_data]

我们将使用 GroqCloud API 和上述定义的工具,指定 model=llama3-8b-8192,其余的函数保持不变。在这种情况下,我们可以直接处理我们的较小数据集,而不必担心超出 API 限制。

这是一个示例输出,提供了原始的 text,Llama3-70B 生成的 gold_re 和 Llama3-8B 生成的 test_re

{'text': 'Long before any knowledge of electricity existed, people were aware of shocks from electric fish.',
 'gold_re': 'people|were aware of|shocks\nshocks|from|electric fish\nelectric fish|had|electricity',
 'test_re': 'electric fish|were aware of|shocks'}

关于完整的测试数据集,请参阅 Google Colab 笔记本。

仅从这个例子来看,Llama3–8B 在关系抽取能力上可能需要一些改进。让我们着手提升这一点。

Llama3–8B 的监督微调

我们将使用一整套技术来辅助,包括 QLoRA 和 Flash Attention。我在这里不会深入讨论选择超参数的具体细节,但如果你有兴趣深入了解,可以参考这些很棒的文献 [4] 和 [5]。

A100 GPU 支持 Flash Attention 和 bfloat16,拥有约 40GB 的内存,足以满足我们对微调的需求。

准备 SFT 数据集

我们首先将数据集解析为对话格式,包括系统消息、输入文本和期望的答案,这些答案来自 Llama3–70B 生成的内容。然后我们将其保存为 HuggingFace 数据集:

def create_conversation(sample):
    return {
        "messages": [
            {"role": "system","content": system_message},
            {"role": "user", "content": sample["text"]},
            {"role": "assistant", "content": sample["gold_re"]}
        ]
    }

from datasets import load_dataset, Dataset

train_dataset = Dataset.from_list(train_data)

# Transform to conversational format
train_dataset = train_dataset.map(create_conversation,
                      remove_columns=train_dataset.features,
                      batched=False)

选择模型

model_id  =  "meta-llama/Meta-Llama-3-8B"

加载分词器

from transformers import AutoTokenizer

# Tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_id,
                                          use_fast=True,
                                          trust_remote_code=True)

tokenizer.pad_token = tokenizer.eos_token
tokenizer.pad_token_id =  tokenizer.eos_token_id
tokenizer.padding_side = 'left'

# Set a maximum length
tokenizer.model_max_length = 512

选择量化参数

from transformers import BitsAndBytesConfig

bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16
)

加载模型

from transformers import AutoModelForCausalLM
from peft import prepare_model_for_kbit_training
from trl import setup_chat_format

device_map = {"": torch.cuda.current_device()} if torch.cuda.is_available() else None

model = AutoModelForCausalLM.from_pretrained(
    model_id,
    device_map=device_map,
    attn_implementation="flash_attention_2",
    quantization_config=bnb_config
)

model, tokenizer = setup_chat_format(model, tokenizer)
model = prepare_model_for_kbit_training(model)

LoRA 配置

from peft import LoraConfig

# According to Sebastian Raschka findings
peft_config = LoraConfig(
        lora_alpha=128, #32
        lora_dropout=0.05,
        r=256,  #16
        bias="none",
        target_modules=["q_proj", "o_proj", "gate_proj", "up_proj", 
          "down_proj", "k_proj", "v_proj"],
        task_type="CAUSAL_LM",
)

当针对所有线性层时,最佳结果可得到。如果内存限制是一个问题,选择更标准的值,如 alpha=32 和 rank=16,可以带来好处,因为这些设置会大幅减少参数量。

训练参数

from transformers import TrainingArguments

# Adapted from  Phil Schmid blogpost
args = TrainingArguments(
    output_dir=sft_model_path,              # directory to save the model and repository id
    num_train_epochs=2,                     # number of training epochs
    per_device_train_batch_size=4,          # batch size per device during training
    gradient_accumulation_steps=2,          # number of steps before performing a backward/update pass
    gradient_checkpointing=True,            # use gradient checkpointing to save memory, use in distributed training
    optim="adamw_8bit",                     # choose paged_adamw_8bit if not enough memory
    logging_steps=10,                       # log every 10 steps
    save_strategy="epoch",                  # save checkpoint every epoch
    learning_rate=2e-4,                     # learning rate, based on QLoRA paper
    bf16=True,                              # use bfloat16 precision
    tf32=True,                              # use tf32 precision
    max_grad_norm=0.3,                      # max gradient norm based on QLoRA paper
    warmup_ratio=0.03,                      # warmup ratio based on QLoRA paper
    lr_scheduler_type="constant",           # use constant learning rate scheduler
    push_to_hub=True,                       # push model to Hugging Face hub
    hub_model_id="llama3-8b-sft-qlora-re",
    report_to="tensorboard",               # report metrics to tensorboard
    )

如果你选择将模型保存到本地,可以省略最后三个参数。你还可能需要调整 per_device_batch_sizegradient_accumulation_steps,以防止内存溢出(OOM)错误。

初始化训练器并训练模型

from trl import SFTTrainer

trainer = SFTTrainer(
    model=model,
    args=args,
    train_dataset=sft_dataset,
    peft_config=peft_config,
    max_seq_length=512,
    tokenizer=tokenizer,
    packing=False, # True if the dataset is large
    dataset_kwargs={
        "add_special_tokens": False,  # the template adds the special tokens
        "append_concat_token": False, # no need to add additional separator token
    }
)

trainer.train()
trainer.save_model()

训练过程,包括模型保存,大约花费了 10 分钟。

让我们清理内存,为推理测试做准备。如果你使用的是内存较小的 GPU,并遇到 CUDA 内存溢出(OOM)错误,可能需要重启运行时环境。

import torch
import gc
del model
del tokenizer
gc.collect()
torch.cuda.empty_cache()

使用 SFT 模型进行推理

在最后一步中,我们将以半精度加载基础模型以及 Peft 适配器。对于此测试,我选择不将模型与适配器合并。

from peft import AutoPeftModelForCausalLM
from transformers import AutoTokenizer, pipeline
import torch

# HF model
peft_model_id = "solanaO/llama3-8b-sft-qlora-re"

# Load Model with PEFT adapter
model = AutoPeftModelForCausalLM.from_pretrained(
  peft_model_id,
  device_map="auto",
  torch_dtype=torch.float16,
  offload_buffers=True
)

接下来,我们加载分词器:

okenizer = AutoTokenizer.from_pretrained(peft_model_id)

tokenizer.pad_token = tokenizer.eos_token
tokenizer.pad_token_id =  tokenizer.eos_token_id

我们构建了文本生成流水线:

pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)

我们加载测试数据集,该数据集由我们之前预留的 20 个样本组成,并将数据格式化为对话风格。然而,这次我们省略了助手消息,并将其格式化为 Hugging Face 数据集:

def create_input_prompt(sample):
    return {
        "messages": [
            {"role": "system","content": system_message},
            {"role": "user", "content": sample["text"]},
        ]
    }

from datasets import Dataset

test_dataset = Dataset.from_list(mini_data)

# Transform to conversational format
test_dataset = test_dataset.map(create_input_prompt,
                      remove_columns=test_dataset.features,
                      batched=False)

单一样本测试

让我们使用 SFT Llama3–8B 生成关系提取输出,并将其与之前两个输出在单一实例上的结果进行比较:

 Generate the input prompt
prompt = pipe.tokenizer.apply_chat_template(test_dataset[2]["messages"][:2],
                                            tokenize=False,
                                            add_generation_prompt=True)
# Generate the output
outputs = pipe(prompt,
              max_new_tokens=128,
              do_sample=False,
              temperature=0.1,
              top_k=50,
              top_p=0.1,
              )
# Display the results
print(f"Question: {test_dataset[2]['messages'][1]['content']}\n")
print(f"Gold-RE: {test_sampler[2]['gold_re']}\n")
print(f"LLama3-8B-RE: {test_sampler[2]['test_re']}\n")
print(f"SFT-Llama3-8B-RE: {outputs[0]['generated_text'][len(prompt):].strip()}")

我们得到了以下结果:

Question: Long before any knowledge of electricity existed, people were aware of shocks from electric fish.

Gold-RE: people|were aware of|shocks
    shocks|from|electric fish
    electric fish|had|electricity

LLama3-8B-RE: electric fish|were aware of|shocks

SFT-Llama3-8B-RE: people|were aware of|shocks
         shocks|from|electric fish

在这个示例中,我们观察到 Llama3–8B 在关系提取能力上的显著提升,尽管微调数据集既不特别干净,也不算非常大,但结果仍然令人印象深刻。

关于 20 个样本数据集的完整结果,请参考 Google Colab notebook。请注意,由于我们以半精度加载模型,推理测试需要更长时间。

结论

总结,通过利用 Llama3–70B 和现有数据集,我们成功地创建了一个合成数据集,并用于微调 Llama3–8B 以完成特定任务。这个过程不仅让我们熟悉了 Llama3,也使我们能够应用 Hugging Face 的简单技巧。我们观察到,使用 Llama3 与 Llama2 的体验非常相似,显著的改进包括更高质量的输出和更有效的分词器。

对于那些希望进一步拓展边界的人来说,可以考虑挑战模型完成更复杂的任务,例如分类实体和关系,并利用这些分类来构建知识图谱。

参考文献

  1. Somin Wadhwa, Silvio Amir, Byron C. Wallace, 在大型语言模型时代重新审视关系提取,arXiv.2305.05003(2023 年)。

  2. Meta,介绍 Meta Llama 3:迄今为止最强大的公开可用 LLM,2024 年 4 月 18 日 (link)

  3. Philipp Schmid, Omar Sanseviero, Pedro Cuenca, Youndes Belkada, Leandro von Werra,欢迎 Llama 3 —— Meta 的新开放 LLM,2024 年 4 月 18 日。

  4. Sebastian Raschka,使用 LoRA(低秩适配)微调 LLM 的实用技巧,Ahead of AI,2023 年 11 月 19 日。

  5. Philipp Schmid,如何在 2024 年使用 Hugging Face 微调 LLM,2024 年 1 月 22 日。

数据集

databricks-dolly-15K 在 Hugging Face 平台(CC BY-SA 3.0)

完整代码与处理后的数据

Github Repo

使用 Python 去除拉曼光谱中的尖峰:一步步指南

原文:towardsdatascience.com/removing-spikes-from-raman-spectra-a-step-by-step-guide-with-python-b6fd90e8ea77?source=collection_archive---------4-----------------------#2024-11-06

查找、移除并用插值值替换尖峰

尼古拉斯·可卡博士Towards Data Science 尼古拉斯·可卡博士

·发表于Towards Data Science ·7 分钟阅读·2024 年 11 月 6 日

--

本教程是系列文章的一部分,系列文章题为使用 Python 进行拉曼光谱的数据科学,该系列在Towards Data Science上发布。它基于在《Analytica Chimica Acta》期刊上发布的这篇文章。通过跟随本教程,您将为您的数据分析工具包添加一项宝贵工具——一种已经在公开研究中使用的有效方法,用于清理拉曼光谱。

去除石墨烯拉曼光谱中的尖峰。图片由作者提供。

简介

尖峰去除是拉曼数据预处理中的一个重要步骤。尖峰是由宇宙射线撞击探测器引起的,它们表现为强烈、狭窄的峰值,可能会扭曲分析结果。这些能量爆发撞击电荷耦合设备(CCD)摄像头,产生尖锐、高强度的峰值,如果不加以修正,可能会干扰进一步的处理步骤,如归一化、光谱搜索或多元数据分析。因此,清除这些伪影是一个优先事项。本教程将介绍一种实用的算法,用于去除拉曼光谱中的尖峰。我们将使用 Python,逐步演示一种用户友好、可定制的尖峰检测与修正方法,确保拉曼数据的准确性和可靠性。

图 1 展示了一个含有峰值的石墨烯拉曼光谱示例。石墨烯的卓越物理特性——如其电导率和热导率——使其成为一个备受研究的材料。其拉曼光谱包含反映结构特征的峰值,揭示有关掺杂、应变和晶界的信息。因此,拉曼光谱技术广泛用于表征石墨烯。然而,为了充分利用这一工具,必须事先去除峰值。

图 1. 含峰值的石墨烯拉曼光谱。生成此图的代码如下所示。图片来自作者。

import numpy as np
# Load data directly into a numpy array
data = np.loadtxt(spiked_spectrum.asc, delimiter=',', skiprows=1)

# Extract Raman shift from the first column (index)
ramanshift = data[:, 0]

# Extract intensity from the second column (index 1in Python)
intensity = data[:, 1]

# Plot the data
import matplotlib.pyplot as plt
fig = plt.figure(figsize = (5,3))
plt.plot(ramanshift, intensity)
plt.xlabel('Raman shift (cm$^{-1}$)')
plt.ylabel('Intensity (a.u.)')
plt.show()

峰值移除算法

这里提出的峰值移除算法包含四个主要步骤:

1. 峰值寻找

2. 峰值检测

3. 峰值标记

4. 光谱校正

让我们来看一下包含 Python 代码片段的不同步骤:

1. 峰值寻找: 首先,算法通过检查局部最大值,设置最小突出度阈值来识别显著的峰值。添加突出度阈值有助于排除由噪声生成的小峰值,因为我们并不打算修正所有噪声。请参见下图进行对比。

from scipy.signal import find_peaks
# Find the peaks in the spectrum (with and without prominence threshold)
peaks_wo_p, _ = find_peaks(intensity) # Peaks found without a prominence threshold
peaks_w_p, _ = find_peaks(intensity, prominence = 20) # Peaks found without a prominence threshold

fig, ax = plt.subplots(1, 2, figsize = (10,3))
ax[0].plot(ramanshift, intensity, zorder=0, label='Raw spectrum')
ax[0].scatter(ramanshift[peaks_wo_p], intensity[peaks_wo_p], marker ='.', color = 'red',label='Found peaks')
ax[1].plot(ramanshift, intensity, zorder=0, label='Raw spectrum')
ax[1].scatter(ramanshift[peaks_w_p], intensity[peaks_w_p], marker ='.', color = 'red',label='Found peaks')
plt.show()

第 1 步:在光谱中找到峰值。图片来自作者。

2. 峰值检测: 接下来,根据峰值的特征窄宽度进行标记。这一点可能有助于大型光谱数据集的自动化。如果我们知道光谱中存在的拉曼带的宽度,我们可以选择低于该值的阈值。例如,在我们的系统分辨率下,我们不预期出现宽度小于 10 cm-1 的石墨烯拉曼带。

from scipy.signal import peak_widths
widths = peak_widths(intensity, peaks_w_p)[0]

fig, ax = plt.subplots(figsize = (5,3))
ax.plot(ramanshift, intensity, zorder=0, label='Raw spectrum')
ax2 = ax.twinx()
ax2.scatter(ramanshift[peaks_w_p], widths, marker ='+', color = 'red',label='Peak widths')
plt.show()

第 2 步:寻找峰值的宽度。图片来自作者。

3. 峰值标记 接下来,任何受峰值影响的数据点都会被标记,标记范围是根据峰值的突出程度计算的,从而有效地隔离损坏的像素。换句话说,我们选择必须修正的窗口。

# Let's set the parameters:
width_param_rel = 0.8
width_threshold = 10 # Estimation of the width of the narrowest Raman band

# Calculation of the range where the spectral points are asumed to be corrupted
widths_ext_a = peak_widths(intensity, peaks_w_p, rel_height=width_param_rel)[2]
widths_ext_b = peak_widths(intensity, peaks_w_p, rel_height=width_param_rel)[3]

# Create a vector where spikes will be flag: no spike = 0, spike = 1.
spikes = np.zeros(len(intensity))

# Flagging the area previously defined if the peak is considered a spike (width below width_threshold)
for a, width, ext_a, ext_b in zip(range(len(widths)), widths, widths_ext_a, widths_ext_b):
    if width < width_threshold:
        spikes[int(ext_a) - 1: int(ext_b) + 2] = 1 

fig = plt.figure(figsize = (5,3))
plt.plot(ramanshift, intensity, zorder=0,label='Raw spectrum')
a=1
plt.scatter(ramanshift[int(widths_ext_a[a])-1 : int(widths_ext_b[a])+1], 
            intensity[int(widths_ext_a[a])-1 : int(widths_ext_b[a])+1], 
            color ='red', label = 'corrupted points')
plt.axvline(x = ramanshift[int(widths_ext_a[a]) -1], linestyle = '--', color = 'red')
plt.axvline(x = ramanshift[int(widths_ext_b[a]) + 1], linestyle = '--', color = 'red')  
plt.show()

第 3 步:标记损坏的箱体。图片来自作者。

4. 光谱校正 最后,这些点通过插值方法与邻近值进行修正,保持光谱的完整性,以便进行后续分析。

from scipy import interpolate
# Let's set the parameter:
moving_average_window = 10

intensity_out = intensity.copy()

# Interpolation of corrupted points
for i, spike in enumerate(spikes):
    if spike != 0: # If we have an spike in position i
        window = np.arange(i - moving_average_window, i + moving_average_window + 1) # we select 2 ma + 1 points around our spike
        window_exclude_spikes = window[spikes[window] == 0] # From such interval, we choose the ones which are not spikes
        interpolator = interpolate.interp1d(window_exclude_spikes, intensity[window_exclude_spikes], kind='linear') # We use the not corrupted points around the spike to calculate the interpolation
        intensity_out[i] = interpolator(i) # The corrupted point is exchanged by the interpolated value.

fig = plt.figure(figsize = (5,3))
plt.plot(ramanshift, intensity, zorder=0, color ='red',label='Raw spectrum')
plt.plot(ramanshift, intensity_out, zorder=0, label='Corrected spectrum')
plt.show()

第 4 步:光谱校正。图片来自作者。

Python 中的完整峰值移除功能

所有这些代码片段可以汇总成一个单独的函数。该函数被设计为根据特定的数据需求进行定制,具有调整突出度和宽度的参数:

import numpy as np
from scipy.signal import find_peaks, peak_widths, peak_prominences
from scipy import interpolate

def spike_removal(y, 
                  width_threshold, 
                  prominence_threshold=None, 
                  moving_average_window=10, 
                  width_param_rel=0.8, 
                  interp_type='linear'):
    """
    Detects and replaces spikes in the input spectrum with interpolated values. Algorithm first 
    published by N. Coca-Lopez in Analytica Chimica Acta. https://doi.org/10.1016/j.aca.2024.342312

    Parameters:
    y (numpy.ndarray): Input spectrum intensity.
    width_threshold (float): Threshold for peak width.
    prominence_threshold (float): Threshold for peak prominence.
    moving_average_window (int): Number of points in moving average window.
    width_param_rel (float): Relative height parameter for peak width.
    tipo: type of interpolation (linear, quadratic, cubic)

    Returns:
    numpy.ndarray: Signal with spikes replaced by interpolated values.
    """

    # First, we find all peaks showing a prominence above prominence_threshold on the spectra
    peaks, _ = find_peaks(y, prominence=prominence_threshold)

    # Create a vector where spikes will be flag: no spike = 0, spike = 1.
    spikes = np.zeros(len(y))

    # Calculation of the widths of the found peaks
    widths = peak_widths(y, peaks)[0]

    # Calculation of the range where the spectral points are asumed to be corrupted
    widths_ext_a = peak_widths(y, peaks, rel_height=width_param_rel)[2]
    widths_ext_b = peak_widths(y, peaks, rel_height=width_param_rel)[3]

    # Flagging the area previously defined if the peak is considered a spike (width below width_threshold)
    for a, width, ext_a, ext_b in zip(range(len(widths)), widths, widths_ext_a, widths_ext_b):
        if width < width_threshold:
            spikes[int(ext_a) - 1: int(ext_b) + 2] = 1 

    y_out = y.copy()

    # Interpolation of corrupted points
    for i, spike in enumerate(spikes):
        if spike != 0: # If we have an spike in position i
            window = np.arange(i - moving_average_window, i + moving_average_window + 1) # we select 2 ma + 1 points around our spike
            window_exclude_spikes = window[spikes[window] == 0] # From such interval, we choose the ones which are not spikes
            interpolator = interpolate.interp1d(window_exclude_spikes, y[window_exclude_spikes], kind=interp_type) # We use the not corrupted points around the spike to calculate the interpolation
            y_out[i] = interpolator(i) # The corrupted point is exchanged by the interpolated value.

    return y_out

然后,带有该算法的函数可以应用于带有峰值的石墨烯光谱,如下所示:

intensity_despiked = spike_removal(intensity, 
                                     width_threshold = 3, 
                                     prominence_threshold = 20, 
                                     moving_average_window=10, 
                                     width_param_rel=0.8, 
                                     interp_type='linear')

fig, ax = plt.subplots(1, 2, figsize = (2*5,3))
ax[0].plot(ramanshift, intensity, label = 'spike', color ='red', linewidth = 0.9)
ax[0].plot(ramanshift, intensity_despiked)
ax[1].plot(ramanshift, intensity_despiked)
plt.show()

拉曼光谱中峰值移除的示例。图片来自作者。

使用这种去除尖峰的方法,你可以确保你的拉曼光谱干净可靠,最小化伪影,同时不丢失重要的光谱细节。该方法非常适合自动化,尤其是在已知预期的最小峰宽的情况下,使其非常适合大规模光谱数据集和高通量分析。

希望你喜欢这个教程。如果有任何问题,或者想分享你自己的拉曼数据挑战,欢迎在评论中留言——我很想听听这个算法如何帮助你的项目!

准备好尝试一下吗?你可以在这里下载 Jupyter Notebook。如果你觉得这对你有帮助,请记得引用原始工作,这对我会有很大帮助!😃

在 Excel 中进行报告可能正在比您想象的还要更多地损害您的业务——以下是如何修复它...

原文:towardsdatascience.com/reporting-in-excel-could-be-costing-your-business-more-than-you-think-heres-how-to-fix-it-aa40c0020131?source=collection_archive---------8-----------------------#2024-11-12

了解如何节省时间,消除昂贵的数据错误,并让您的团队专注于推动真正商业增长的洞察。

Hattie BiddlecombeTowards Data Science Hattie Biddlecombe

·发表于Towards Data Science ·阅读时间 6 分钟·2024 年 11 月 12 日

--

免责声明:我与本文提到的任何工具无关;我分享它们是因为我发现它们在处理当前任务时非常有效。

最近,我与两家机构合作,它们都寻求一个类似的、简单的解决方案:

自动化他们的月度和季度报告流程,并为客户展示视觉效果良好的数据仪表板。

两家机构都在应对类似的数据挑战,这让我认为这些问题在许多机构中可能是普遍存在的。这促使我写下这篇博客,旨在分享有用的见解并提供切实可行的解决方案。

以下是这些机构在报告过程中面临的主要挑战

1. 这些机构花费了过多的时间将报告编制到 Excel 中

报告有时需要花费几天时间才能完成。一家机构需要从多个全球市场收集报告,而另一家则依赖于跨部门的几名员工来更新并发送来自他们系统的数据。如果有人休年假,该数据点就会在报告中被标记为“待确认”。

2. 尝试进行更深层次的分析证明是一个挑战

Excel 的稳定性和可扩展性问题:

两家代理商都在处理大量数据,而正如我们许多人都知道的那样,Excel 在这种工作负载下往往会出现卡顿或崩溃。Excel 在创建数据透视表时频繁卡顿和崩溃,使得深入分析变得非常繁琐。团队常常不得不强制重启 Excel,有时还会面临丢失工作的风险。

Excel 可视化的局限性

Excel 提供的可视化范围相对有限,这使得数据的多样化和有洞察力的呈现变得更加困难。虽然简单的可视化通常是最终展示的最佳选择,但在探索阶段,更先进的可视化工具能够从多个角度分析数据并揭示更深层次的见解。

与更先进的工具相比,Excel 提供的可视化交互性有限,而后者为数据探索提供了更流畅和动态的体验。例如,在像 Power BI 这样的工具中,你可以点击某个视觉元素中的区域,所有相关的图表(例如销售趋势、客户群体、或产品类别)会立即更新,显示与该选择相关的数据。这种交互性对于发现更深层次的见解和理解数据变化背后的因素至关重要。

深入分析的重要性

深入分析对于每月做出最有影响力的决策至关重要。它是区分标准报告和真正出色报告的关键,标准报告仅展示了数字的增减,而真正出色的报告能够提出前瞻性解决方案、制定创新策略,并发现未开发的机会。通过在这一层次的分析上投资时间,你不仅解决了眼前的关注点,还将自己定位为客户长期增长的关键合作伙伴。

3. 在 Excel 之外花费过多时间和资源来创建视觉上吸引人的图表

由于 Excel 的可视化效果往往显得有些笨拙,其中一家公司每个月都会将精美、品牌化的图表设计外包给设计师。就像大多数设计项目一样,这涉及了很多关于这些新图表应如何呈现的反复讨论。

4. 数据技能差距导致了不准确的报告

报告由一个缺乏必要经验的人管理,这个人并没有完全理解 Excel 的一些独特之处,可以理解,因为这并不是他们的核心职责。因此,两家代理商都在不知情的情况下报告了错误的数字。例如,尽管“收入”列设置为“货币”格式,但像“USD123”和“123”(带空格)这样的条目被排除在总数之外,因为 Excel 没有将其识别为有效的货币值。虽然 Excel 确实提供了数据验证功能,用于限制条目为小数或整数,但必须手动应用,许多用户对此并不知情。在我看来,Excel 应该默认标记这些不一致之处。

那么,代理商的报告问题的解决方案是什么呢?

这个例子来自其中一个客户,因为他们的案例更为全面:

图片来源:作者自有

Dropbox / Excel:

该机构的主要 Excel 文件包含多个标签页,存储在 Dropbox 中,以便团队成员能够全球访问。

2. Python in Deepnote:

这是我花费大部分时间的地方,我在 Deepnote 笔记本中使用 Python 彻底清理数据,然后每月自动化这个过程。下面是一个 Deepnote Python 笔记本的快照。我在单元格中概述了我提取、清理和推送数据的步骤:

图片来源:Deepnote

3. BigQuery

对于两个机构,我确保将清理过的数据存储在数据库中,并且将数据推送回 Excel 文件中的 Dropbox,供那些希望以 Excel 格式访问数据的人使用。将数据存储在数据库中提供了几个关键优势,包括:

a. 安全性: 高级功能如基于用户的权限、加密和审计跟踪,确保敏感数据受到保护,且访问权限严格控制。由于 Power BI 不允许对某些用户隐藏敏感列,因此我在 BigQuery 中创建了相关视图来管理隐私,控制哪些数据在仪表板级别暴露。

b. 速度: 即使多个用户同时通过仪表板访问数据,查询也能快速运行。

c. 可扩展性: 随着数据量的增长,数据库将无缝处理这些数据,避免了两个机构在使用 Excel 时遇到的上述问题。

快进到今天:自动化报告和仪表板如何改变了这些机构的工作流程

节省大量时间

他们的月度和季度报告现在可以在几分钟内自动刷新,消除了曾经花费大量时间和精力手动整理数据的过程。即使有人在年假期间,流程也能顺利运行,不会中断。团队们不再依赖我的输入,使整个系统变得完全自给自足🎉。

图片来源:Midjourney

非常满意的客户

两个机构都对结果感到非常兴奋,使用了“惊人”和“我痴迷”这样的词汇来形容他们客户的新仪表板(抱歉,我有点自夸,但有时候就是得这么做)。虽然我不能分享实际的仪表板,但这里有一个非常接近其中之一的模拟图:

图片来源:Mokkup.ai

用户已获得更深层次分析的能力

仪表板提供了先进的、相互连接的可视化图表,能够进行更深层次的分析。完全可共享的仪表板使团队可以获得更详细的、针对行业和团队的见解,赋能每个人做出更明智的决策。

数据准确

关键是,现在的数字准确无误,摆脱了 Excel 常见的怪癖和限制。

无需外包设计师或依赖第三方工具

现在,令人惊艳的品牌化可视化可以直接在 PowerBI 中创建,并轻松嵌入到 PowerPoint 中,从而不再需要设计师或外部可视化工具。

现在各机构对数据的潜力有了更深的了解

和我所有的客户一样,我花时间向他们讲解 Excel、Power BI 和 Python 的全部潜力。通过与他们的团队共同工作,我帮助弥合了数据技能的差距,指出了 Excel 的一些怪癖,同时介绍了 Python 和笔记本的强大功能,以揭示更深层次的见解。

总之,Excel 是一个非常出色的工具,但也有其局限性。就像一辆可靠的汽车,它大多数时候可以带你到达目的地。但是当路况变得更加复杂时,有时候你需要一辆更强大的车辆才能继续前进。

截至 2023 年 8 月,尽管 Excel 现在可以集成 Python,但它仍然有一些限制,您可以在这里了解更多。依我看,通过 Python 笔记本与 Excel 合作,进行分析和数据清理要高效得多。

想了解您的企业如何受益于类似的自动化和仪表板吗?随时联系我们: https://www.datagatorsolutions.com/

使用 Huggingface Transformers 进行 RAG 管道中检索优化的重排序

原文:towardsdatascience.com/reranking-using-huggingface-transformers-for-optimizing-retrieval-in-rag-pipelines-fbfc6288c91f?source=collection_archive---------5-----------------------#2024-11-08

理解重排序何时能产生显著的影响

Daniel KlitzkeTowards Data Science Daniel Klitzke

·发表于Towards Data Science ·阅读时间:8 分钟·2024 年 11 月 8 日

--

可视化重排序结果,针对用户查询“什么是刚性运动?”原始排序在左侧,新排序在右侧。(图片由作者制作)

在这篇文章中,我将展示如何使用Huggingface TransformersSentence Transformers库,通过使用重排序模型来提升你的 RAG 管道。具体来说,我们将进行以下操作:

  1. 使用简单的原始 RAG 管道建立一个基准

  2. 使用 Huggingface Transformers 库集成一个简单的重排序模型

  3. 评估在什么情况下重排序模型显著提高了上下文质量,以便更好地理解其好处。

为了实现这一点,我将提供指向Github的相应代码链接。

什么是重排序?

在深入评估之前,我想简要介绍一下什么是重排序器。重排序器通常按照以下方式应用:

  1. 在 RAG 管道的检索步骤中,使用简单的基于嵌入的检索方法来获取初始候选集

  2. Reranker 用于重新排序结果,以提供更适合用户查询的新结果顺序。

但是,为什么重新排序模型的结果会与我已经相当强大的嵌入模型不同呢?为什么我不在早期阶段利用重新排序模型的语义理解呢?这个问题很复杂,但一些关键点是,例如我们这里使用的 bge-重新排序模型本质上是同时处理查询和文档的交叉编码方法,因此可以显式地建模查询-文档交互。另一个主要区别是,重新排序模型是以监督方式训练的,目标是预测相关性分数,这些分数是通过人工标注获得的。这在实际中的意义将在后续的评估部分中展示。

我们的基线

对于我们的基线,我们选择最简单的 RAG 流水线,并专注于检索部分。具体来说,我们:

  1. 选择一个大型 PDF 文档。我选择了我的硕士论文,但你也可以选择任何你喜欢的文档。

  2. 从 PDF 中提取文本,并将其拆分为大约 10 个句子一组的均等块。

  3. 为我们的文本块创建嵌入,并将其插入向量数据库,这里使用的是 LanceDB。

有关这一部分的详细信息,请查看我们的Github笔记本。

完成这些步骤后,简单的语义搜索就可以通过两行代码实现,具体如下:

query_embedding = model.encode([query])[0]
results = table.search(query_embedding).limit(INITIAL_RESULTS).to_pandas()

这里的查询将是用户提供的查询,例如问题“什么是形状完成?”此时的限制是要检索的结果数量。在正常的 RAG 流水线中,检索到的结果将直接作为上下文提供给 LLM,后者将综合生成答案。在许多情况下,这也是完全有效的,但在本文中,我们希望探索重新排序的好处。

实现重新排序

使用像Huggingface Transformers这样的库,使用重新排序模型非常简单。为了使用重新排序来改进我们的“RAG 流水线”,我们扩展了我们的方法,如下所示:

  1. 如前所述,通过标准的嵌入模型简单地检索初始结果数量。但是我们将结果的数量从 10 增加到大约 50。

  2. 在检索到这个较大数量的初始源之后,我们应用一个重新排序模型来重新排序这些源。这是通过计算每个查询-源对的相关性分数来完成的。

  3. 对于答案生成,我们通常会使用新的前 x 个结果。(在我们的案例中,我们使用前 10 个)

这段代码看起来也非常简单,可以通过几行代码实现:

# Instantiate the reranker
from transformers import AutoModelForSequenceClassification, AutoTokenizer

reranker_tokenizer = AutoTokenizer.from_pretrained('BAAI/bge-reranker-v2-m3')
reranker_model = AutoModelForSequenceClassification.from_pretrained('BAAI/bge-reranker-v2-m3').to("mps")
reranker_model.eval()

# results = ... put code to query your vector database here...
# Note that in our case the results are a dataframe containing the text
# in the "chunk" column.

# Perform a reranking
# Form query-chunk-pairs
pairs = [[query, row['chunk']] for _, row in results.iterrows()]

# Calculate relevance scores
with torch.no_grad():
    inputs = reranker_tokenizer(pairs, padding=True, truncation=True, return_tensors='pt', max_length=512).to("mps")
    scores = reranker_model(**inputs, return_dict=True).logits.view(-1,).float()

# Add scores to the results DataFrame
results['rerank_score'] = scores.tolist()

# Sort results by rerank score and add new rank
reranked_results = results.sort_values('rerank_score', ascending=False).reset_index(drop=True)

再次,为了查看完整的代码以了解上下文,请查看Github

如你所见,主要的机制是简单地为模型提供查询和潜在相关文本的对。它输出一个相关性评分,然后我们可以用它来重新排序结果列表。但是这值得吗?在什么情况下值得额外的推理时间?

评估重新排序器

为了评估我们的系统,我们需要定义一些测试查询。在我的案例中,我选择了以下问题类别:

  1. 事实性问题,例如“什么是刚体运动?”

    这些通常应该在文档中有一个特定的来源,并且表述方式应该使得它们可以通过文本搜索轻松找到。

  2. 改写的事实性问题,比如“某些点云分类方法的架构中,是什么机制使它们对点的顺序不变?”

    如你所见,这些问题在提到某些术语时较为笼统,例如它们需要识别点云分类与 PointNet 架构之间的关系。

  3. 多源问题,例如“Co-Fusion 方法与论文中提出的方法相比是如何工作的?它们的相似之处和不同之处是什么?”

    这些问题需要检索多个来源,这些来源应该列出或彼此比较。

  4. 摘要或表格问题,例如“手部分割实验中使用的网络和参数大小是多少?”

    这些问题针对的是文本和表格形式的摘要,例如用于模型结果比较的表格。它们的目的是测试重新排序器是否能更好地识别出检索文档中的摘要部分是否有用。

由于我比较懒,所以每个类别只定义了 5 个问题,以便获得一个粗略的印象,并评估了带有和不带有重新排序的检索上下文。我选择的评估标准包括:

  1. 重新排序是否增加了重要信息到上下文中。

  2. 重新排序是否减少了上下文的冗余

  3. 重新排序是否将最相关的结果放在列表的更高位置(更好的优先级排序)。

那结果怎么样呢?

平均排名变化概览以及最初被忽略的结果(那些不在前 10 名中的结果)。(图片由作者制作)

即使在概述中,我们也可以看到,不同类别的问题之间有显著差异,特别是对于多源问题类别,似乎有很多重新排序的操作。当我们更仔细地查看度量的分布时,这一点得到了进一步确认。

被忽略的结果按问题类别的分布。(图片由作者制作)

特别是对于这个类别中的 5 个问题中的 3 个,几乎所有进入最终前 10 名的结果都通过重新排序步骤得到了提升。现在的问题是找出为什么会这样。因此,我们关注两个最受重新排序(积极)影响的查询。

问题 1:“Co-Fusion 方法如何运作,与论文中提出的方法有何比较?有哪些相似性和差异?”

前 10 个来源的重新排序结果及其之前的位置。(图像由作者创建)

这里的第一印象是,针对这个查询的重新排序器确实产生了两个主要效果。它将第 6 位的段落优先排为最相关的结果。此外,它还将一些低排名的结果提取到前 10 名中。当我们进一步检查这些段落时,我们发现了以下情况:

  1. 重新排序器成功提取出一个高度相关的段落,描述了 SLAM 方法论文中的方法的不同之处。

  2. 重新排序器还成功地包含了一个段落,该段落提到了 Co-Fusion,作为一种能够处理动态物体的 SLAM 方法,并且包括了关于局限性的讨论。

一般来说,这里出现的主要模式是,重新排序器能够捕捉到语气中的细微差别。具体来说,像“SLAM 方法与论文中提出的方法密切相关,但……”这种表述,配合对 Co-Fusion 的稀疏提及,将比使用标准嵌入模型时排名更高。这可能是因为嵌入模型很可能无法捕捉到Co-Fusion 是一个 SLAM 方法,而文本中的主导模式是关于 SLAM 的通用信息。因此,重新排序器可以为我们提供两件事情:

  1. 专注于细节而不是关注平均语义内容。

  2. 更加关注用户意图,以比较某些方法与论文中的方法。

问题 2:“根据每个实验的结果,提供关于引言中设定目标的完成情况的总结。”

前 10 个来源的重新排序结果及其之前的位置。(图像由作者创建)

此外,我们还意识到,许多低排名的来源通过重新排序步骤被拉入了前 10 名。所以让我们再调查一下为什么会这样:

  1. 重新排序器再次成功捕捉到了问题的细微意图,并将包含“it was thus suspected...”这一表述的段落重新排序为高度相关的内容,这确实是因为后续内容描述了假设是否有效,以及该方法是否能够利用这些假设。

  2. 重新排序器为我们提供了许多表述模糊的实验结果,这些结果还包括了一些关于机器学习训练结果的表格概览,可能是理解这些部分的总结性特征

结论

实现重新排序并不是一项难事,像 Huggingface Transformers 这样的包提供了 易于使用的接口,可以将它们集成到你的 RAG 管道中,而主要的 RAG 框架如 llama-indexlangchain 已经开箱即用地支持它们。此外,还有基于 API 的重新排序器,例如 Cohere 提供的,你可以在你的应用中使用它。

从我们的评估中,我们还发现,重新排序器对于以下任务特别有用:

  • 捕捉隐藏在一个内容块中的细微语义,这些语义可能是不同的或加密的。例如,在内容块中仅提及一次与某个概念相关的方法(如 SLAM 和 Co-Fusion)。

  • 捕捉用户意图,例如,将某种方法与论文中的方法进行比较。然后,重新排序器可以专注于那些暗示有比较正在进行的表达,而不是其他语义。

我相信还有更多的应用场景,但对于这些数据和我们的测试问题,这些是最主要的模式,我认为它们清晰地展示了经过监督训练的重新排序器相比仅使用嵌入模型所能提供的优势。

解决 Python 中的循环导入问题

原文:towardsdatascience.com/resolving-circular-imports-in-python-957db3bfa596?source=collection_archive---------5-----------------------#2024-04-25

PYTHON 编程

了解并解决循环导入问题,这是当两个模块相互导入时常见的错误。

Marcin KozakTowards Data Science Marcin Kozak

·发表于Towards Data Science ·阅读时间 7 分钟·2024 年 4 月 25 日

--

循环导入会导致一个永无止境的循环。照片来自Matt SeymourUnsplash上。

循环导入在 Python 应用程序和包的开发过程中相当常见。当两个模块尝试同时相互导入时,就会发生这种错误——这会创建一个循环,导致两个模块都无法完全加载。因此,导致的错误是ImportError,大致表示无法从部分初始化的模块中导入对象。

解决这个错误通常是直接的。然而,有时代码需要重新设计和重构——有时需要相当广泛的重构。本文旨在通过解释解决该错误的方法来帮助您。

解决方案

我们将探索在不重构代码以改变其行为的情况下,解决循环导入的方法。有时,只需重新定义类的职责,使其不直接相互依赖即可。但这种方法通常不足以解决问题。因此,我们将重点关注解决循环导入的策略,而不改变导致循环导入的模块中定义的对象的行为。

负责任的 LLMOps

原文:towardsdatascience.com/responsible-llmops-985cd1af3639?source=collection_archive---------2-----------------------#2024-06-16

将负责任的 AI 实践融入 LLMOps

Debmalya BiswasTowards Data Science Debmalya Biswas

·发布于Towards Data Science ·阅读时间 15 分钟·2024 年 6 月 16 日

--

摘要。*虽然我们看到 LLMOps 和负责任 AI 实践在生成 AI 实现中的日益采用,但讨论通常发生在不同的社区中,检查清单和它们的下游实施/工具之间存在很大的脱节。

在本文中,我们旨在通过统一的方法将这两个框架结合起来,目的是以可扩展和负责任的方式部署使用案例。我们突出展示 LLM 特定的问题,深入探讨相关的 LLMOps 架构模式,并提出解决方案,以建立一个由负责任 AI 实践治理的集成 LLMOps 平台。*

DIPTA CHAKRABORTYBhargav Mitra合作

介绍

我们看到对大语言模型(LLMs)及其颠覆企业使用案例的潜力越来越感到兴奋。然而,今天的探索大多局限于概念验证(POC)。

要真正实现生成 AI 使用案例在企业中的规模化,需要建立一个具备 LLMOps 能力并具有适当 LLM 治理的可扩展 LLM 平台。

与此同时,我们也看到越来越多的关注点放在了负责任地部署 AI 产品上,因为有很多 LLM 生成有毒、带偏见、性别化和暴力内容的案例。LLM 还被发现会出现幻觉——提供带有偏见和误导性的回应。

为了缓解这些问题,…

重新思考 LLM 基准:衡量超越训练数据的真正推理能力

原文:towardsdatascience.com/rethinking-llm-benchmarks-measuring-true-reasoning-beyond-training-data-f3fa82dbf5da?source=collection_archive---------12-----------------------#2024-11-07

Apple 的新 LLM 基准,GSM-Symbolic

Maxime JabarianTowards Data Science Maxime Jabarian

·发表于 Towards Data Science ·5 分钟阅读·2024 年 11 月 7 日

--

来源

欢迎来到对大语言模型(LLM)推理能力的探索,我们将讨论一个重要问题:像 GPT、Llama、Mistral 和 Gemma 这样的模型,是否真的能够推理,还是仅仅是巧妙的模式匹配器? 随着每一次新版本的发布,我们看到这些模型的基准分数不断提高,常常给人一种它们即将具备真正问题解决能力的印象。但Apple发布的一项新研究,GSM-Symbolic: Understanding the Limitations of Mathematical Reasoning in Large Language Models提供了一个现实检查——它的发现可能会改变我们对这些能力的看法。

如果您不是会员,请点击这里.

作为一名拥有近两年经验的 LLM 工程师,我将分享我对这个话题的看法,包括为什么 LLM 必须超越记忆模式,提供真正的推理能力。我们还将分析 GSM-Symbolic 研究中的关键发现,该研究揭示了这些模型在数学推理方面仍然存在的差距。最后,我将思考这对将 LLM 应用到现实世界中的意义,在那里,我们真正需要的是真正的推理能力——而不仅仅是看起来很出色的回答。

为什么 LLM 推理很重要?

重新思考统计显著性

原文:towardsdatascience.com/rethinking-statistical-significance-a6150f588b9a?source=collection_archive---------7-----------------------#2024-05-22

放弃严格的界限值,无论这些界限多么严格,并提倡在科学文章中呈现原始数据,而不是推动闭合形式的、可能存在偏见的结论。

LucianoSphere (Luciano Abriata, PhD)Towards Data Science LucianoSphere (Luciano Abriata, PhD)

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

--

图片由Edge2Edge Media提供,来自Unsplash

统计显著性今天已成为科学研究的支柱,但我最近发现,在《自然》杂志上发布的一份由超过 800 位研究人员签署的请愿书,使我重新想起了有关统计检验的一些长期存在的观点和思考,即统计检验在许多情况下更可能破坏科学研究,而不是帮助它。

传统上,研究结果根据预设的 p 值阈值(例如 0.05、0.01 或 0.001,这些是化学和生物科学中常用的阈值,当然在其他领域的使用阈值可能不同)被分类为“显著”或“非显著”类别。这个做法的第一个问题,尤其是在非专家中表现得尤为明显,他们常常误解结果:非显著结果并不否定效应的存在;相反,它表明没有足够的确凿证据来支持这一效应。然而,除了这个问题之外,还有其他一些更普遍的问题,我将在接下来的部分讨论,这些问题即使对于专家来说也会产生影响,尤其是当存在隐性偏见和利益冲突时,这些问题可能在某些情况下被加剧……

重温 Karpathy 的《计算机视觉和 AI 的现状》

原文:towardsdatascience.com/revisiting-karpathys-state-of-computer-vision-and-ai-794862a0e077?source=collection_archive---------4-----------------------#2024-10-18

回顾自 2012 年博客文章《计算机视觉和 AI 的现状:我们真的,真的还很远》以来的 AI 进展

Leon Eversberg 博士Towards Data Science Leon Eversberg 博士

·发表于Towards Data Science ·7 分钟阅读·2024 年 10 月 18 日

--

前总统巴拉克·奥巴马开玩笑地把脚趾放在天平上。照片由皮特·苏扎拍摄,来源于flickr.com

“对于计算机来说,理解这张图像像你我一样需要什么条件呢?我挑战你思考一下,所有需要的知识点都必须到位,才能让它有意义。” [1]

十二年前,2012 年 10 月 22 日,安德烈·卡帕西(Andrej Karpathy)发表了一篇标题为《计算机视觉和 AI 的现状:我们真的,真的还很远》的博客文章[1]。

在他的博客文章中,他以前总统巴拉克·奥巴马开玩笑地把脚趾放在天平上为切入点,谈论了 2012 年计算机视觉和人工智能(AI)的现状。

Karpathy 认为,AI 模型需要拥有关于我们世界的广泛知识,才能基于图像中像素的数值进行推理,不仅要理解发生了什么,还要理解为什么它是搞笑的。

“令人难以置信的是,所有这些推理都从一个简短的…

用 AI 彻底改变网页浏览

原文:towardsdatascience.com/revolutionize-web-browsing-with-ai-5d5f6ce5f5df?source=collection_archive---------4-----------------------#2024-03-10

使用 GPT-4V 和 Puppeteer 解锁互动在线体验

Tianyi LiTowards Data Science Tianyi Li

·发表于 Towards Data Science ·23 分钟阅读·2024 年 3 月 10 日

--

作者:Tianyi LiSelina Li

图片由作者使用 DALL·E 生成

· 介绍

· 潜在的使用案例

· 高级工作流

· 架构

∘ 开始使用

∘ 选择正确的路径

∘ 这是一个循环!

∘ 目录结构

∘ 浏览器控制服务

∘ 元素注释服务

· 结论

介绍

假设你非常想参加本月在你所在城市举办的 AI 活动,但你心中有一些特定的标准,可能与活动的时间安排或重点内容有关。通常,这将涉及以下过程:

  • 使用诸如“本月[你的城市]的 AI 事件”之类的关键词启动网络搜索。

  • 从搜索结果中筛选出看起来有潜力的链接。

  • 浏览所选网站以确定其相关性,可能需要通过其他链接进一步深入了解。

  • 经过反复推敲,最终找到了符合你标准的活动,并将其详细信息记录到日历中。

如果我们分解上述过程,它基本上涉及的步骤可以归类为以下几类:

  1. 控制浏览器,比如访问网址、点击链接、返回等。

  2. 浏览页面内容

  3. 根据页面内容做出决策,例如确定哪个链接与你的查询相关。

通过利用新兴的大型语言模型(LLM)技术,我们现在可以通过一个LLM 驱动的 AI 代理自动化整个过程

进入 AI 代理,它正如我们上面所描述的那样,做着你所做的一切。

  1. 浏览器控制:AI 使用像Puppeteer这样的工具来浏览互联网。可以把 Puppeteer 看作是 AI 的双手,它能轻松地打开标签页、点击链接,并在网页之间导航。

  2. 内容浏览:可以把这个看作是 AI 的眼睛。Puppeteer能够拍摄网页截图,并将其传递给 AI。

  3. 决策制定:这是 AI 的大脑发挥作用的地方,借助大型语言模型(LLM)。它评估每个页面的截图,分析图像,判断相关性并决定下一步,模拟人类判断。

在本文中,我们将探索并构建一个利用 OpenAI 的gpt-4-vision-preview模型的 AI 代理。该模型可以分析图像并提供文本回复。

这个代理将能够与用户互动,控制网页浏览器,并处理数据。我们将探讨它的结构以及如何工作。

本文的灵感来源于一个 YouTube 视频 GPT4V + Puppeteer = AI 代理像人类一样浏览网页?🤖,由AI Jason发布。

本文中展示的所有代码都可以在这个 Github 仓库中找到 AI Web Agent.

潜在的使用案例

  • 与文本转语音配对,它可以帮助视力障碍者浏览网页。

  • 快速定位电子商务网站上的产品。

  • 自动浏览具有动态加载内容的网站(例如房地产列表)

  • 自动浏览需要个人登录的网站(例如 LinkedIn、Instagram、Facebook)

  • 参加在线测验和测试

  • 玩网页游戏

  • 在线抢座/抢票

  • 自动化应用功能测试

高层次工作流程

显示代理高层次工作流程的流程图。

首先,用户向代理提出问题。

在接收到问题后,GPT-4V 将返回一个 URL 作为网页搜索的入口。该 URL 可以由用户在问题中指定;如果未指定,默认情况下,它将是 GPT 从用户问题中提取的关键词进行 Google 搜索。

在接收到 URL 后,Puppeteer 将打开该 URL 的网页。它会高亮显示页面上的所有互动元素,如链接、点击、按钮,然后截图并返回。

在接收到截图后,GPT-4V 将查看页面并处理高亮显示的信息。作为大脑,GPT 将决定是否已经得到了一个好的答案来回答用户的问题。

如果有一个好的答案,GPT 会将答案返回给用户。

如果没有,GPT-4V 将决定下一个步骤应访问哪个 URL 或点击哪个元素。Puppeteer 将再次打开新的 URL,突出显示交互元素,并截取屏幕截图以供进一步处理。

这个过程将反复进行,直到 GPT 决定它已为用户找到了一个好的答案。

架构

既然我们现在已经了解了 AI 代理是如何工作的,接下来让我们看看代理的架构。代理的结构如下:

代理架构概览

这种架构乍一看可能很复杂,但一旦理解了不同组件及其如何协同工作,其实非常简单。我们来逐步解析。

开始

这是工作流程的起点。用户通过提供明确任务的提示(user task prompt)与代理进行交互。同时,代理获得一些默认指令(system prompt),告诉它如何执行任务。这些指令告诉代理它的角色是什么,如何格式化回答,以及其他重要信息。

这是起始点的一个简单视图:

初始步骤

user task prompt 的示例可能是:

I'm looking for AI events in Melbourne this month. Please prioritize events that are on weekends.

这是系统提示可能的样子:

一个典型的系统提示,提供代理的上下文。

致谢:JayZeeDesign

系统提示非常关键,因为它们帮助代理决定如何行动。以下三个主要领域用不同颜色标出:

  • 代理的角色是什么。(水绿色)

  • 代理应该如何呈现其答案。(红色)

  • 额外的指令,帮助代理执行任务。(黄色)

然后,这些提示会发送到 OpenAI 服务,该服务负责生成回答。

根据这些提示,代理的回答将被整理成仅 3 种类型:

  1. { "url": "https://www.example.com" } - 代理希望导航到一个特定的网页。

  2. { "click": "text on a button" } - 代理希望点击页面上的特定元素。

  3. regular message - 代理已完成任务并将所需信息提取给用户。

对于初始对话,回答通常会是一个 Google 搜索 URL,例如 https://www.google.com/search?q=AI+events+in+Melbourne+March。然后,该回答会触发浏览器控制服务,导航到该 URL。

起始点的代码实现

const messages: ChatCompletionMessageParam[] = [];

// STEP 1: Welcome the user
console.log(staticMessageMap.welcome);
// STEP 2: provide the context of the conversation - system prompt
messages.push(promptMap.context());
// STEP 3: Ask and apply the user's query as a task - user task prompt
const userPrompt = await userPromptInterfaceV2(staticMessageMap.you);
messages.push(promptMap.task(userPrompt));

以下函数在终端中创建用户输入查询的界面。

/**
 * This service creates a user prompt interface and returns a promise that resolves to the user's input. Allow user to input in the `terminal`.
 * @param query - The message to hint the user on what to input
 * @returns A promise that resolves to the user's input
 */
export const userPromptInterfaceV2 = async (query: string) => {
  // Create an interface to read input from the user
  const userInterface = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });

  // Return a promise that resolves to the user's input
  // The userInterface.question method takes a query and a callback function
  // The reason for using a promise is to make the user's input accessible outside of the callback function
  return new Promise<string>((resolve) => {
    userInterface.question(query, (input) => {
      resolve(input);
      userInterface.close(); // Close the user interface
    });
  });
};

你可能会注意到代码中的 promptMap 对象。它是一个返回所有提示的函数映射。将其设为函数使得这些提示具有动态性,可以根据对话的上下文发生变化。以下是一个示例:

export const promptMap = {
  context: (
    role: "teacher" | "student" | "AI"
  ): ChatCompletionMessageParam => ({
    role: "system",
    content: `You are a ${role}. You will be given instructions on what to do by browsing. You are connected to a web browser and you will be given the screenshot of the website you are on.`,
  }),
};

一个重要的注意事项是,为了让 LLM 能够记住对话的memory,我们需要始终将新的提示推送到消息数组中,并将整个数组发送给 LLM。这可能会导致令牌数暴增。使用max_tokens参数来建立安全网。

下面是如何使用 openai 服务生成响应的方法。

const taskFlow = async (): Promise<void> => {
  console.log(`${staticMessageMap.agent}Let me think...`);
  // Start the conversation with the LLM
  const response = await openai.chat.completions.create({
    model: "gpt-4-vision-preview",
    max_tokens: 1024,
    messages, // The messages array which contains the conversation history is sent to the LLM
    temperature: 0,
  });

// For the initial conversation, the agent will respond with the url (google search if not provided by the user)
  const { message } = response.choices[0];
  const { content: messageText } = message;
  if (messageText === null) {
    throw new Error("The response message text is null");
  }
  // Show the response in the terminal
  console.log(`${staticMessageMap.agent}${messageText}`);
  // Memorize the answer from agent
  messages.push({
    role: "assistant",
    content: messageText,
  });
};
await taskFlow();

当然,要使用openai服务,你需要安装openai包并配置 API 密钥。这里有一个单独的文件来处理 openai 服务。

import dotenv from "dotenv";
dotenv.config();
import OpenAI from "openai";

/**
 * An instance of the OpenAI Class that can invoke the API methods
 * @example `openai.chat.completions.create`
 */
export const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

选择正确的路径

如前所述,LLM 响应将仅被塑造成三种类型中的一种。每种类型都会触发不同的浏览器控制服务进行相应的操作。

代理程序根据 LLM 的反馈选择最佳路径。

URL 响应流

  • 响应格式 { “url”: “https://www.example.com" }

  • 指示代理程序导航到特定网页。

  • 此响应将触发脚本中的以下步骤:

  1. 从响应中提取 URL

  2. 使用 Puppeteer 打开浏览器并导航到 URL

  3. 然后,代理程序会截图并通过另一个特殊指令提示发送给 LLM,分析页面内容并决定下一步操作。

‘url’响应消息的处理流程。

点击响应流

  • 响应格式 { “click”: “按钮上的文本” }

  • 指示代理程序点击页面上的特定元素。

  • 代理程序将执行以下步骤:

  1. 从响应中提取链接文本

  2. 然后,代理程序使用 Puppeteer 查找匹配文本的元素并点击它

  3. 代理程序会截图并通过另一个特殊指令提示发送给 LLM,分析页面内容并决定下一步操作。

‘click’响应消息的处理流程。

让我们来看看代理程序在截图页面后发送给 LLM 的特殊指令提示

来源:JayZeeDesign

这个提示基本上告诉大语言模型(LLM)分析页面的截图并决定下一步操作。响应格式应该与我们在系统提示中提到的完全一致。对于 LLM,通常需要反复说明规则或指南,以确保一致的表现。

常规消息流 — 常规消息

  • 指示代理程序已完成任务并向用户提取所需信息。

  • 这通常是当前任务的结束。

  • 代理程序将简单地向用户展示信息。

路径选择的代码实现

// Use typescript to define the 3 types of response
export type ResponseMessage =
  | {
      type: ResponseMessageCategory.URL;
      url: string;
    }
  | {
      type: ResponseMessageCategory.CLICK;
      linkText: string;
    }
  // This initial type is simply a placeholder to indicate the start of the conversation in which the LLM has not yet provided a response (not relevant to the path selection logic)
  | {
      type: ResponseMessageCategory.INITIAL;
      text: "initial";
    }
  | {
      type: ResponseMessageCategory.REGULAR;
      text: string;
    };

一个将 LLM 的纯文本响应转换为定义类型的函数。

export const convertTextToResponseMessage = (text: string): ResponseMessage => {
  // The `extractActionFromString` function is a helper function that simply checks if a string contains a particular pattern, such as '{"url": "' or '{"click": "' which is directly related to the response format. See the repo for the full implementation.
  if (extractActionFromString(text, ResponseMessageCategory.URL) !== null) {
    return {
      type: ResponseMessageCategory.URL,
      // Extract the URL from the response and store it in the `url` property, so it can be accessed easily
      url: extractActionFromString(text, ResponseMessageCategory.URL) as string,
    };
  }

  if (extractActionFromString(text, ResponseMessageCategory.CLICK) !== null) {
    return {
      type: ResponseMessageCategory.CLICK,
      // Extract the link text from the response and store it in the `linkText` property, so it can be accessed easily
      linkText: extractActionFromString(
        text,
        ResponseMessageCategory.CLICK
      ) as string,
    };
  }

  if (text === ResponseMessageCategory.INITIAL) {
    return {
      type: ResponseMessageCategory.INITIAL,
      text,
    };
  }
  return {
    type: ResponseMessageCategory.REGULAR,
    text,
  };
};

要实现 路径选择,代码应具有以下骨架结构:

// messageText is the plain string response from the LLM (see above code)
// It is then converted to the defined type using the `convertTextToResponseMessage` function
responseMessage = convertTextToResponseMessage(messageText);

// if-else statement to determine the path based on the response type
// URL Response Flow
if (responseMessage.type === ResponseMessageCategory.URL) {
  // 1\. Extract the URL from the response
  const { url } = responseMessage;
  // 2\. Opens a headless browser using Puppeteer and navigates to the URL and take a screenshot of the page
  const imagePath = await screenshot(url, page);
  if (imagePath === undefined) {
    throw new Error("The screenshot path is undefined");
  }
  // converts the screenshot to a format that LLM accepts
  const base64String = await imageToBase64String(imagePath);
  // 3\. Send the screenshot with the instruction prompt back to the LLM
  messages.push(
    promptMap.instruction({
      url: base64String,
      detail: "auto",
    })
  );
  return; // end of this path
}
// Click Response Flow
if (responseMessage.type === ResponseMessageCategory.CLICK) {
  // 1\. Extract the link text from the response
  const { linkText } = responseMessage;
  // 2\. The agent then uses Puppeteer to find the element with the matching text and clicks on it
  const imagePath = await clickNavigationAndScreenshot(linkText, page, browser);
  if (imagePath === undefined) {
    throw new Error("The screenshot path is undefined");
  }
  const base64String = await imageToBase64String(imagePath);
  // 3\. Send the screenshot with the instruction prompt back to the LLM
  messages.push(
    promptMap.instruction({
      url: base64String,
      detail: "auto",
    })
  );
  return;
}
// Regular Message Flow - return message directly

它是一个循环!

现在,我们清楚了代理可以采取的不同路径。需要注意的一点是,代理会一直循环这些步骤,直到任务完成并响应常规消息。

循环的示例如下:

代理工作流是一个循环!

URL 或点击响应流将多次触发,直到代理找到答案。

要实现循环,代码应具有以下结构:

首先,创建一个辅助函数来判断何时停止循环。

export const shouldContinueLoop = (responseMessage: ResponseMessage) => {

// If the response is a regular message, the agent has finished the task and the loop should stop
  if (responseMessage.type === ResponseMessageCategory.REGULAR) {
    return false;
  }
  // Otherwise, the loop should continue
  return true;
};

然后是主循环函数。

// The initial response message before the loop starts. Basically, a placeholder.
let responseMessage: ResponseMessage = {
  type: ResponseMessageCategory.INITIAL,
  text: "initial",
};
//==================================LOOP==================================
// shouldContinueLoop determines when to stop the loop
while (shouldContinueLoop(responseMessage)) {

  // +++++ openai service to generate the response +++++
  console.log(`${staticMessageMap.agent}Let me think...`);
  const response = await openai.chat.completions.create({
    model: "gpt-4-vision-preview",
    max_tokens: 1024,
    messages,
    temperature: 0,
  });

  const { message } = response.choices[0];
  const { content: messageText } = message;
  if (messageText === null) {
    throw new Error("The response message text is null");
  }
  console.log(`${staticMessageMap.agent}${messageText}`);
  // +++++++++++++++++++++++++++++++++++++++++++++++++++

  // Memorize the answer from agent
  messages.push({
    role: "assistant",
    content: messageText,
  });

  // +++++ Path Selection +++++
  responseMessage = convertTextToResponseMessage(messageText);
  // URL Response Flow
  if (responseMessage.type === ResponseMessageCategory.URL) {
    const { url } = responseMessage;
    const imagePath = await screenshot(url, page);
    if (imagePath === undefined) {
      throw new Error("The screenshot path is undefined");
    }
    const base64String = await imageToBase64String(imagePath);
    messages.push(
      promptMap.instruction({
        url: base64String,
        detail: "auto",
      })
    );
    // Instead of stopping here, we need to continue the loop for the next step
    continue;
  }
  // Click Response Flow
  if (responseMessage.type === ResponseMessageCategory.CLICK) {
    const { linkText } = responseMessage;
    try {
      const imagePath = await clickNavigationAndScreenshot(
        linkText,
        page,
        browser
      );
      if (imagePath === undefined) {
        throw new Error("The screenshot path is undefined");
      }
      const base64String = await imageToBase64String(imagePath);
      messages.push(
        promptMap.instruction({
          url: base64String,
          detail: "auto",
        })
      );
      // continue the loop for the next step
      continue;
    } catch (err) {
      // Handle the error and retry if the link is not found; sometimes the LLM just comes up with a link that doesn't exist or with a typo
      if (
        err instanceof Error &&
        err.message.includes("Link with text not found")
      ) {
        console.log(`...Error clicking on link: ${err.message}`);
        messages.push(promptMap.retryIfLinkNotFound(linkText));
        continue;
      } else {
        console.log(`...Unexpected error: ${err}. Please try again.`);
        break;
      }
    }
  }
  // ++++++++++++++++++++++++++++
}

正如你可能注意到的,我们没有在循环中指定常规消息流。这是因为常规消息不会触发任何浏览器控制服务。它只是结束循环并向用户显示消息,基本上是一个 console.log 语句。当然,你可以在常规消息流中添加更多的逻辑,例如将消息保存到 CSV 文件中等。

console.log(`${staticMessageMap.agent}${messageText}`);

目录结构

在了解了代理的主要流程之后,接下来我们来看一下代理的目录结构。

agent.ts
services/
├── browser-controller.ts
├── data-transformer.ts
├── element-annotator.ts
├── openai.ts
├── prompt-map.ts
└── user-prompt-interface.ts
utils.ts

agent.ts:这是我们代理的主脚本。它协调其他服务,负责代理的主要执行流程。它包含起始点、主循环和路径选择逻辑。

services/:该目录包含代理使用的各种服务。每个服务负责一个特定任务。

  • browser-controller.ts:该服务使用 Puppeteer 控制浏览器。它可以浏览页面、与元素交互以及截图。

  • data-transformer.ts:该服务为代理转换数据。它可以格式化数据、清洗数据,并准备进一步处理。

  • element-annotator.ts:该服务为代理注释 HTML 元素。它可以高亮显示元素,给元素添加唯一标识符等。

  • openai.ts:该服务与 OpenAI API 进行接口对接。

  • prompt-map.ts:该服务将用户提示映射到具体操作。它可以根据用户输入判断代理应该采取的行动。

  • user-prompt-interface.ts:该服务与用户接口,以获取提示。它可以读取用户输入并将其传递给代理。

utils.ts:该文件包含代理使用的工具函数。这些函数在代理代码中用于执行常见任务。

有关完整代码实现,请参阅 Ai Web Agent

浏览器控制服务

现在,让我们深入了解代理如何控制浏览器——它的“手”是如何工作的。这里使用了一个名为 Puppeteer 的库。它提供了一个基于 Chrome DevTools 协议的高级 API。详细信息请参见 Puppeteer 文档

本质上,Puppeteer 会在代理从 LLM 接收到响应后被调用。在我们上面提到的不同路径下,Puppeteer 会执行不同的操作。

总体而言,浏览器控制器服务负责以下任务:

  1. 导航到一个 URL

  2. 点击一个链接

  3. 截取页面截图

Puppeteer 在工作流程中执行的任务。

实际上,在代理截图页面之前,还有一个步骤 — 高亮交互元素。我们将这个服务分离到另一个文件中,以使代码更加模块化,并且易于维护。它将在下一部分讨论。

浏览器控制器服务的代码实现

步骤 1 — 初始化浏览器并打开一个新标签页

const browserWindowSize = { width: 900, height: 1600 };

/**
 * This service initializes a new browser session and a new page tab
 * @returns An object containing the browser and the page
 */
export const initController = async () => {
  const pup = puppeteer.default.use(StealthPlugin());
  // launch the browser
  const browser = await pup.launch({
    // detailed configurations
    headless: false, // Determines whether to run the browser in headless mode (without a GUI). boolean | "new" | undefined
    executablePath: process.env.GOOGLE_CHROME_CANARY_PATH, // path to a browser executable to use instead of the bundled Chromium
    userDataDir: process.env.GOOGLE_CHROME_CANARY_USER_DATA_DIR, // Path to a user data directory, i.e, the user profile
    args: [
      `--profile-directory=${process.env.PROFILE}`, // Select the expected profile
      "--disable-setuid-sandbox",
      "--no-sandbox",
      "--no-zygote",
      `--window-size=${browserWindowSize.width},${browserWindowSize.height}`,
    ],
  });
  // open a new tab
  const page = await browser.newPage();
  // set the viewport
  await page.setViewport({
    width: browserWindowSize.width,
    height: browserWindowSize.height,
    deviceScaleFactor: 1,
  });
  // the initialized browser and page are returned
  return { browser, page };
};

根据不同的响应流程,代理要么导航到一个 URL 并截图,要么点击一个链接触发导航并截图。

步骤 2 — URL 响应流程:导航到一个 URL 并截图

export const navToUrlAndScreenshot = async (url: string, page: Page) => {
  console.log(`...Opening ${url}`);
  // validate the URL
  if (!isValidURL(url)) {
    throw new Error(`Invalid URL: ${url}`);
  }

  // +++++ go to the URL +++++
  await page.goto(url, {
    // a simple logic to determine if the page is loaded: wait 500 ms after the number of active network requests are 2
    waitUntil: "networkidle2",
    timeout: TIMEOUT,
  });
  // +++++++++++++++++++++++++++

  // +++++ take a screenshot of the page +++++
  // also include waiting for the page to load completely
  const imagePath = await waitAndScreenshot(page);
  // +++++++++++++++++++++++++++++++++++++++++
  return imagePath;
};

waitAndScreenshot函数

const waitAndScreenshot = async (page: Page) => {
  // is the page still loading? `document.readyState` === 'loading' or there is a loading indicator on the page
  const isLoading = await isPageExplicitlyLoading(page);

  // if the page is still loading, wait for the page to load completely
  isLoading && (await waitTillHTMLRendered(page));

  // apply Set-of-Mark Prompting
  console.log(`...Highlight all interactive elements`);
  await highlightInteractiveElements(page);

  // take a screenshot of the page
  console.log(`...Taking screenshot`);
  await page.screenshot({
    //path: "/agent/web-agent-screenshot.jpg" is a wrong path
    path: imagePath,
    fullPage: true,
  });

  return imagePath;
};

截图是通过调用page.screenshot()简单完成的。你可以根据需要更改配置。

waitTillHTMLRendered函数

如图所示,导航到 URL 是通过page.goto()完成的。该方法利用waitUntil选项来等待页面加载。页面加载完毕后,代理会截图。在截图之前,等待所有视觉内容加载完毕是至关重要的,以确保代理能够获取页面的完整信息。

不幸的是,在许多情况下,GoToOptions中的waitUntil(page.goto())不足以等待页面完全加载(特别是动态加载的内容),因此我们需要使用自定义函数waitTillHTMLRendered

这个函数基本上通过document.readyState检查页面是否仍在加载,或者页面上是否有加载指示器。如果页面仍在加载,函数将调用waitTillHTMLRendered进行全面检查:

  1. 每秒检查一次 HTML 大小。

  2. 如果 HTML 大小连续 3 秒钟保持不变,则假定页面已加载完毕。

  3. 如果页面在 30 秒后仍在加载(超时),则抛出错误。

以下流程图展示了waitTillHTMLRendered函数的逻辑:

waitTillHTMLRendered函数的流程图。

为了更好地理解waitAndScreenshot函数,我们来看看该函数运行时的日志:

捕获动态内容渲染的日志。

在页面完全加载后,所有交互元素被高亮显示并截图。

export const waitTillHTMLRendered = async (
  page: Page,
  timeout: number = 30000,
  checkOnlyHTMLBody: boolean = false
) => {
  const waitTimeBetweenChecks: number = 1000;
  const maximumChecks: number = timeout / waitTimeBetweenChecks; // assuming check itself does not take time
  let lastHTMLSize = 0;
  let stableSizeCount = 0;
  const COUNT_THRESHOLD = 3;

  const isSizeStable = (currentSize: number, lastSize: number) => {
    if (currentSize !== lastSize) {
      return false; // still rendering
    } else if (currentSize === lastSize && lastSize === 0) {
      return false; // page remains empty - failed to render
    } else {
      return true; // stable
    }
  };

  for (let i = 0; i < maximumChecks; i++) {
    const html = await page.content();
    const currentHTMLSize = html.length;
    const currentBodyHTMLSize = await page.evaluate(
      () => document.body.innerHTML.length
    );

    const currentSize = checkOnlyHTMLBody
      ? currentBodyHTMLSize
      : currentHTMLSize;
    // logging
    console.log(
      "last: ",
      lastHTMLSize,
      " <> curr: ",
      currentHTMLSize,
      " body html size: ",
      currentBodyHTMLSize
    );

    stableSizeCount = isSizeStable(currentSize, lastHTMLSize)
      ? stableSizeCount + 1
      : 0;
    console.log(`Stable size count: ${stableSizeCount}`);

    // if the HTML size remains the same for 3 consecutive seconds, it assumes the page has finished loading
    if (stableSizeCount >= COUNT_THRESHOLD) {
      console.log("Page rendered fully..");
      break;
    }

    lastHTMLSize = currentSize;
    await page.waitForTimeout(waitTimeBetweenChecks);
  }
};

步骤 2(续) — 点击响应流程clickNavigationAndScreenshot函数

该函数用于点击页面上的特定元素,等待页面完全加载后再截图。对于click操作,它使用了另一个名为clickOnLink的函数。

export const clickNavigationAndScreenshot = async (
  linkText: string,
  page: Page,
  browser: Browser
) => {
  let imagePath;

  try {
    const navigationPromise = page.waitForNavigation();
    // The Click action
    const clickResponse = await clickOnLink(linkText, page);

    if (!clickResponse) {
      // if the link triggers a navigation on the same page, wait for the page to load completely and then take a screenshot
      await navigationPromise;
      imagePath = await waitAndScreenshot(page);
    } else {
      // if the link opens in a new tab, ignore the navigationPromise as there won't be any navigation
      navigationPromise.catch(() => undefined);
      // switch to the new tab and take a screenshot
      const newPage = await newTabNavigation(clickResponse, page, browser);

      if (newPage === undefined) {
        throw new Error("The new page cannot be opened");
      }

      imagePath = await waitAndScreenshot(newPage);
    }

    return imagePath;
  } catch (err) {
    throw err;
  }
};

clickOnLink函数

这个功能会遍历所有带有gpt-link-text属性的元素(这是元素注释过程中获得的唯一标识符),并点击与 LLM 提供的链接文本匹配的元素。

const clickOnLink = async (linkText: string, page: Page) => {
  try {
    const clickResponse = await page.evaluate(async (linkText) => {

      const isHTMLElement = (element: Element): element is HTMLElement => {
        return element instanceof HTMLElement;
      };

      const elements = document.querySelectorAll("[gpt-link-text]");

      // loop through all elements with `gpt-link-text` attribute
      for (const element of elements) {

        if (!isHTMLElement(element)) {
          continue;
        }

        // find the element that contains the targeted link text
        if (
          element
            .getAttribute("gpt-link-text")
            ?.includes(linkText.trim().toLowerCase())
        ) {
          // This if statement is to handle the case where the link opens in a new tab
          if (element.getAttribute("target") === "_blank") {
            return element.getAttribute("gpt-link-text");
          }

          // highlight and perform the click action
          element.style.backgroundColor = "rgba(255,255,0,0.25)";
          element.click();
          return;
        }

      }

      // only if the loop ends without returning
      throw new Error(`Link with text not found: "${linkText}"`);
    }, linkText);

    return clickResponse;
  } catch (err) {
    if (err instanceof Error) {
      throw err;
    }
  }
};

元素注释服务

让我们更深入地了解highlightInteractiveElements函数,它在waitAndScreenshot内部被调用。

这是一个为代理注释交互式 HTML 元素的服务。它可以用红色边框突出显示元素,并为它们添加唯一标识符。

想象一下,给你的 AI 代理配上一副特殊的眼镜,让它能看到网站上的交互点——按钮、链接和字段——就像宝藏图上的发光宝藏一样。

这基本上就是highlightInteractiveElements函数的作用。它就像数字世界中的荧光笔,在可点击的项目周围画上红框,并用数字标签标记它们。

通过注释,代理对图像的理解准确性得到了大幅提升。这个概念叫做Set-of-Mark Prompting

这是注释截图的一个示例:

电子商务网站,交互元素被红色高亮显示。图像来自 Officeworks 网站。

有一篇研究论文详细讨论了这个话题的重要性:Set-of-Mark Prompting

以下是它的执行方式:

  1. 它首先移除可能会混淆我们 AI 的旧数字标签(html 属性gpt-link-text)。

  2. 然后,它会用红色轮廓突出显示它发现的每个可点击的内容,帮助 AI 识别应该“点击”哪里。

  3. 每个交互元素都会有一个独特的标签。这个标签/属性将用于标识 Puppeteer 稍后可以与之互动的元素。

需要记住的一个关键细节是,当处理 Puppeteer 或任何其他以编程方式与网页交互的测试框架时,带有链接文本的元素可能不可见。这里是一个简单的例子:

<div style="display: none">
  <a href="https://www.example.com">
    <span>Click me</span>
  </a>
</div>

父 div 被隐藏,所以链接不可见。这个元素应该被排除。需要递归检查父元素以确保该元素可见。请参见下图中的逻辑:

isElementVisible函数的流程图。

highlightInteractiveElements函数的代码实现

import { Page } from "puppeteer";

const INTERACTIVE_ELEMENTS = [
  "a",
  "button",
  "input",
  "textarea",
  "[role=button]",
  "[role=treeitem]",
  '[onclick]:not([onclick=""])',
];
/**
 * Reset the unique identifier attribute and remove previously highlighted elements
 * @param page
 */
const resetUniqueIdentifierAttribute = async (page: Page): Promise<void> => {
  await page.evaluate(() => {
    const UNIQUE_IDENTIFIER_ATTRIBUTE = "gpt-link-text";
    const elements = document.querySelectorAll(
      `[${UNIQUE_IDENTIFIER_ATTRIBUTE}]`
    );
    for (const element of elements) {
      element.removeAttribute(UNIQUE_IDENTIFIER_ATTRIBUTE);
    }
  });
};
/**
 * This function annotates all the interactive elements on the page
 * @param page
 */
const annotateAllInteractiveElements = async (page: Page) => {
  // $$eval method runs Array.from(document.querySelectorAll(selector)) within the `page`and passes the result as the first argument to the pageFunction.
  // If no elements match the selector, the first argument to the pageFunction is [].
  await page.$$eval(
    INTERACTIVE_ELEMENTS.join(", "), // the selector can be defined outside the browser context
    // the argument `elements` can be an empty array if no elements match the selector
    function (elements) {
      // any console.log will not be visible in the node terminal
      // instead, it will be visible in the browser console
      // handle empty array
      if (elements.length === 0) {
        throw new Error("No elements found");
      }
      //======================================VALIDATE ELEMENT CAN INTERACT=================================================
      // This run-time check must be defined inside the pageFunction as it is running in the browser context. If defined outside, it will throw an error: "ReferenceError: isHTMLElement is not defined"
      const isHTMLElement = (element: Element): element is HTMLElement => {
        // this assertion is to allow Element to be treated as HTMLElement and has `style` property
        return element instanceof HTMLElement;
      };
      const isElementStyleVisible = (element: Element) => {
        const style = window.getComputedStyle(element);
        return (
          style.display !== "none" &&
          style.visibility !== "hidden" &&
          style.opacity !== "0" &&
          style.width !== "0px" &&
          style.height !== "0px"
        );
      };
      const isElementVisible = (element: Element | undefined | null) => {
        if (element === null || element === undefined) {
          throw new Error("isElementVisible: Element is null or undefined");
        }
        let currentElement: Element | null = element;
        while (currentElement) {
          if (!isElementStyleVisible(currentElement)) {
            return false;
          }
          currentElement = currentElement.parentElement;
        }
        return true;
      };
      //========================================PREPARE UNIQUE IDENTIFIER================================================
      const setUniqueIdentifierBasedOnTextContent = (element: Element) => {
        const UNIQUE_IDENTIFIER_ATTRIBUTE = "gpt-link-text";
        const { textContent, tagName } = element;
        // if the node is a document or doctype, textContent will be null
        if (textContent === null) {
          return;
        }
        element.setAttribute(
          UNIQUE_IDENTIFIER_ATTRIBUTE,
          textContent.trim().toLowerCase()
        );
      };
      //========================================HIGHLIGHT INTERACTIVE ELEMENTS================================================
      for (const element of elements) {
        if (isHTMLElement(element)) {
          // highlight all the interactive elements with a red bonding box
          element.style.outline = "2px solid red";
        }
        // assign a unique identifier to the element
        if (isElementVisible(element)) {
          // set a unique identifier attribute to the element
          // this attribute will be used to identify the element that puppeteer should interact with
          setUniqueIdentifierBasedOnTextContent(element);
        }
      }
    }
  );
};
/**
 * This function highlights all the interactive elements on the page
 * @param page
 */
export const highlightInteractiveElements = async (page: Page) => {
  await resetUniqueIdentifierAttribute(page);
  await annotateAllInteractiveElements(page);
};

结论

在本文中,我们探讨了 AI 代理的架构、每个步骤的代码实现以及设计背后的某些概念,如 Set-of-Mark Prompting。该代理是一个优雅的系统,需要精心协调不同的服务才能有效运行,目前仍存在许多问题和局限性。如果您有任何问题或建议,请随时与我联系。我很愿意进一步讨论这个话题。

喜欢这个故事吗?

Jason LiTianyi LiLinkedIn)是位于澳大利亚墨尔本的 Mindset Health 的全栈开发工程师。Jason 对人工智能、前端开发和与空间相关的技术充满热情。

Selina LiSelina LiLinkedIn)是位于澳大利亚墨尔本的 Officeworks 的首席数据工程师。Selina 对人工智能/机器学习、数据工程和投资充满热情。

Jason 和 Selina 希望探索技术,帮助人们实现他们的目标。

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

利用人工智能革新烹饪体验:介绍 FIRE(Food Image to REcipe 生成)🔥

原文:towardsdatascience.com/revolutionizing-culinary-experiences-with-ai-introducing-fire-food-image-to-recipe-generation-d7b010ee2eb2?source=collection_archive---------9-----------------------#2024-01-31

从视觉享受到烹饪食谱:人工智能如何将食物图像转化为食谱?

Prateek ChhikaraTowards Data Science Prateek Chhikara

·发表于Towards Data Science ·阅读时间 11 分钟·2024 年 1 月 31 日

--

图 1. 给定一张可能未见过的图像,我们的方法 FIRE 生成相应的食谱,包含标题、食材和烹饪说明。(图片来源:作者提供)

引言:

食物是人体所需的基本营养来源,也是我们文化身份的组成部分,反映了我们的生活方式、传统和社会关系[1]。一个人的外貌和认知能力通常能反映出其饮食习惯,因为选择营养丰富的食物有助于身心的整体健康[2]。社交媒体的迅速发展使每个人都能分享他们所食用美味食物的惊艳视觉。简单地搜索诸如#food 或#foodie 这样的标签,会显示出数百万的帖子,突显了食物在我们社会中的巨大价值[3]。食物的重要性,以及大量公开可得的食物数据集,推动了与食物相关的计算应用,尤其是将食物图像与符号知识联系起来。食物计算的一个雄心勃勃的目标是生成给定食物图像的食谱,包括根据用户偏好进行食物推荐、根据文化或宗教因素定制食谱,以及自动化烹饪操作以提高效率和精准度[4]。

“告诉我你吃什么,我就能告诉你你是谁。”这句话强调了个人饮食选择反映其身份的观点。

在深入探讨我们提出的工作之前,我想提到这项工作已被发表,并可以在IEEE/CVF 计算机视觉应用冬季会议(WACV)- 2024上找到,这是一个因其对计算机视觉的贡献和进展而备受认可的会议。✨✨✨

海报: drive.google.com/file/d/1zf2NA6ga8PWndZAgu5QjSwO8EPsvt-yt/view

论文: openaccess.thecvf.com/content/WACV2024/html/Chhikara_FIRE_Food_Image_to_REcipe_Generation_WACV_2024_paper.html

这项工作的灵感来源:

我们创建一种从食物图像生成食谱的端到端方法的动机,源自计算机视觉(CV)社区对食物计算的兴趣。计算机视觉已被用于食物质量保证约三十年[5]。尽管深度学习技术在食物图像处理方面取得了进展,现有方法在从给定食物图像中提取成分方面的表现仍然有限[6, 7]。此外,从一组成分生成食谱可以看作是一个语言生成任务,或者更准确地说,是一个序列到序列(seq-to-seq)的应用场景,而这一点在现有文献中仍未被充分探讨。进一步来说,之前的方法未能彻底结合计算机视觉和自然语言处理(NLP)研究,设计出一个综合系统,将食物图像转化为完整的食谱。因此,目前的食物计算方法尚未利用 NLP 和计算机视觉中的最新突破,如视觉变换器和先进的语言建模技术。正是由于这种技术和应用上的空白,我们激发了开发端到端管道的动力,我们命名其为 FIRE(🔥),旨在将这些技术结合起来,推动食物计算的边界。FIRE 是一个多模态模型,旨在根据给定的食物图像生成完整的食谱,包括食物名称、成分和烹饪指令,如图 1 所示。

我们论文的贡献如下:

  1. 我们的方法使用视觉变换器(ViT)从食物图像中提取详细的嵌入表示,然后作为基于注意力的解码器的输入,进一步识别食谱成分。

  2. 我们开发了一个详细的设计,用于生成食谱标题和烹饪指令,采用最先进的视觉(BLIP)和语言(T5)模型。

  3. 我们的多模态方法在成分提取准确性和生成的烹饪指令质量方面,超过了现有模型的表现。

  4. 我们通过两个创新应用展示了 FIRE 的多功能性,(i)食谱定制 和 (ii) 食谱到代码生成,展示了其在使用少量提示集成大型语言模型(LLM)方面的重要性。

揭开 FIRE 的面纱:分解过程

FIRE 包含三个组件:(1)使用最先进的图像描述技术从食品图像中生成标题,(2)使用视觉转换器和带有注意力的解码器层从图像中提取成分,(3)基于生成的标题和提取的成分使用编码-解码模型生成烹饪说明。有关所提出架构的更多细节,请参见图 2。

图 2. 提出架构,用于从食品图像中提取成分、生成菜谱标题和烹饪说明。(成分及其数量仅在训练时传递)(来源:作者提供的图像)

1. 标题生成

使用 BLIP 模型,我们从食品图像中生成菜谱标题 [8]。在我们使用现成的 BLIP 模型进行的初步实验中,我们观察到 BLIP 的预测准确性较低,因为其训练数据与食品领域之间存在领域转移。由于 BLIP 最初设计用于为各种场景提供全面的图像描述,它倾向于捕捉与我们目标无关的额外细节。举个例子,当我们展示一张松饼的图像时,BLIP 生成了描述“一块松饼放在木制切菜板上”。为了更好地使生成的描述与菜谱标题对齐,我们使用 Recipe1M 数据集的一个子集对 BLIP 模型进行了微调。我们观察到,经过微调后的 BLIP 模型在生成准确、对齐且相关的食品图像标题方面显示出了良好的改进。对于相同的松饼图像,经过微调的 BLIP 模型提供了更简短的字符串“松饼”,去除了多余的信息。

2. 成分提取

从给定的食品图像中提取成分是一项具有挑战性的任务,因为食品组成的固有复杂性和变化性。我们开发了一个成分提取流程(如图 2 所示),该流程基于文献[7]中提出的方法。

特征提取器: 我们使用 ViT [9] 提取图像的特征。ViT 的注意力机制以稳定且显著高的分辨率处理特征表示。该能力精确地满足了密集预测任务的需求,例如从食品图像中提取成分。

成分解码器: 特征提取器生成图像嵌入。我们将这些图像嵌入通过三个归一化层(layerNorm),然后将输出传递给我们的成分解码器,负责提取成分。解码器由四个连续的模块组成,每个模块包含多个顺序层:自注意力、条件注意力、两个全连接层和三个归一化层。在最后一步,解码器的输出通过一个全连接层处理,该层的节点数等于词汇表的大小,从而生成预测的成分集合。

3. 烹饪说明生成

考虑到语言模型(LMs)在自然语言应用(如文本生成和问答)中的显著成就[10],我们将烹饪指导生成视为一种语言建模任务。

图 3. 为标题和一组配料生成烹饪指导。(带数量的配料仅在 T5 的微调过程中出现)(来源:作者提供的图片)

对语言模型(LMs)进行下游任务微调在各种 NLP 任务中取得了显著成果。虽然我们预计大规模的语言模型经过微调后能够生成烹饪指导,但考虑到其庞大的参数量,它们需要大量的计算资源。基于可用资源和我们的研究目标,我们采用了流行的编码器-解码器模型 T5[11]来生成烹饪指导。在微调过程中,我们将食谱的标题和配料作为格式化字符串传入(见图 3),灵感来自于先前的研究[12]。

T5 在三种输入上进行微调:标题、配料和带数量的配料,以最大化地从数据集中获取信息。然而,在推理时我们没有带数量的配料;因此,我们只传递标题和配料。此外,排除数量信息确保了与先前方法的公平比较。我们调查了我们的模型优势是否源于良好的结构化架构,而非仅依赖于额外知识的增强。通过去除推理过程中数量信息的影响,我们旨在突出 T5 的固有能力及其生成高质量烹饪指导的能力。

FIRE 结果:

表 1 中的结果显示,FIRE 表现优于 SotA 基线模型 InverseCooking[7]和 Chef Transformer[13]。这些结果证明了我们提出的流程在生成精确且连贯的食谱方面的能力,验证了 FIRE 的有效性,并强调了语言生成模型在高质量食谱生成中的价值。这些结果也支持我们的预期,即 FIRE 方法即使在推理时没有提供配料数量信息,仍然能很好地泛化,即使在训练过程中提供了这些信息。同时,使用额外信息进行训练能减少幻觉现象,特别是关于配料数量(例如 2 汤匙盐)和烹饪时间(例如加热 10-12 分钟)。

表 1. 测试数据集上的食谱生成比较。我们报告了 10 次实验的均值和标准差。粗体表示最佳模型。(+)表示在真实标题和配料上测试模型以生成食谱。(来源:作者提供的图片)

在本文中,我们主要关注我们提出的工作中的关键方面,考虑到文章篇幅的限制。我们鼓励读者查看我们的详细论文,以便深入了解我们的实验结果和全面分析。😁

错误分析

为了进一步了解我们的食谱生成方法的表现,我们检查了其在个别图片上的表现。FIRE 通常能够为与 Recipe1M 数据集中存在的菜肴相似的菜肴生成正确的食谱。对于 Pav Bhaji(一道在 Recipe1M 数据集中没有的受欢迎的印度菜肴),它给出的结果与预期的菜肴无关,如图 4 所示。因此,我们想强调开发更好的评估指标的重要性,因为传统的评估指标,如 SacreBLEU 和 ROUGE,未能捕捉生成的食谱的准确性并检测到特定的文本幻觉。

图 4. FIRE 对 Pav Bhaji 图片的食谱预测。(来源:作者提供的图片)

超越厨房:FIRE 在食品计算中的未来:

虽然 FIRE 在从图片生成食谱这一具有挑战性的任务上达到了最先进的表现,我们更进一步,探索其在食品计算应用中集成到更大管道中的可能性。具体来说,考虑到大规模语言模型的少量提示能力,我们描述了 FIRE 和大型语言模型如何集成,以支持食谱定制食谱到机器代码生成

图 5. FIRE 的应用:食谱定制与食谱到代码生成。(来源:作者提供的图片)

1. 食谱定制

食谱定制至关重要,因为它涉及到食物、习惯和个人偏好。此外,当涉及过敏或饮食限制时,食谱定制变得尤为重要。令人惊讶的是,尽管有明显需求,现有文献在食谱定制方面缺乏专门的研究。我们的工作旨在填补这一研究空白,通过考虑个人口味档案和饮食限制来实现个性化食谱定制。

为了指导未来在这一领域的研究,我们展示了 FIRE 支持一种食谱定制方法的能力,重点关注广泛的主题(例如,食材替换、口味调整、卡路里调整、烹饪时间适配),以全面测试少量样本的表现。如图 5 中紫色部分所示,我们从食谱中删除了土豆成分。删除了与土豆相关的两句话,并对其中一句进行了修改以确保一致性。具体来说,我们进行了食材添加,将‘奶酪’替换为‘切达奶酪’,并识别到应在烘烤前加入,从而得到了修改后的句子‘撒上半量的切达奶酪和洋葱’。

2. 为基于图片的食谱生成机器代码

将食谱转换为机器代码能够实现自动化、可扩展性,并与各种现有系统进行集成,从而减少人工干预、节省劳动力成本并减少准备食物时的人为错误。为了简化这一任务,我们将 FIRE 的食谱生成能力与大型语言模型(LM)在结构任务中操控代码风格提示的能力结合起来[14]。我们展示了一个示例方法,用于通过提示 GPT-3 生成由 FIRE 开发的食谱的 Python 风格代码表示(请参阅图 5 中的橙色部分)。

结论与未来工作:

我们介绍了 FIRE,一种专为食品计算量身定制的方法,专注于仅通过图像输入生成食物标题、提取食材和生成烹饪指令。我们利用了近期计算机视觉(CV)和语言建模的进展,取得了优于稳固基准的卓越表现。此外,我们展示了 FIRE 在食谱定制食谱到代码生成中的实际应用,展示了我们方法的适应性和自动化潜力。

我们列出了未来研究中应解决的三大挑战:

  1. 现有的食谱生成模型和我们提出的模型缺乏一种可靠的机制来验证生成食谱的准确性。传统的评估指标在这方面存在不足。因此,我们希望创建一种新的评估标准,评估食谱的连贯性和可信度,提供更为全面的评价。

  2. 食谱的多样性和可获得性受到地理、气候和宗教因素的影响,这些因素可能会限制其适用性。结合考虑这些背景因素和食材关系的知识图谱,可以提供替代食材建议,从而解决这一问题。

  3. 使用语言和视觉模型生成食谱时的幻觉问题构成了一个重大挑战。未来的研究将探讨状态追踪方法,以改进生成过程,确保生成更为真实和准确的食谱。

行动号召:

希望这个概述能为您提供有关 FIRE 的灵感和发展的见解,这是一款将食物图像转化为详细食谱的创新工具。若想更深入地了解我们的方法,欢迎查阅我们完整的论文,该论文将在IEEE/CVF 冬季计算机视觉应用会议(WACV)-2024上发表。如果我们的研究对您的工作有所帮助,欢迎引用我们的论文。😊

论文链接: openaccess.thecvf.com/content/WACV2024/html/Chhikara_FIRE_Food_Image_to_REcipe_Generation_WACV_2024_paper.html

@InProceedings{Chhikara_2024_WACV,
    author    = {Chhikara, Prateek and Chaurasia, Dhiraj and Jiang, Yifan and Masur, Omkar and Ilievski, Filip},
    title     = {FIRE: Food Image to REcipe Generation},
    booktitle = {Proceedings of the IEEE/CVF Winter Conference on Applications of Computer Vision (WACV)},
    month     = {January},
    year      = {2024},
    pages     = {8184-8194}
}

参考文献:

[1] Weiqing Min, Shuqiang Jiang, Linhu Liu, Yong Rui, 和 Ramesh Jain. 食品计算的综述. ACM Comput. Surv., 52(5), 2019 年 9 月。

[2] Sutter Health. 为心理健康吃得好. www.sutterhealth.org/health/nutrition/eating-wellfor-mental-health. 访问日期:2023 年 3 月 24 日。

[3] Kiely Kuligowski. 使用 Instagram 为您的业务的 12 个理由. www.business.com/articles/10-reasons-touse-instagram-for-business/. 访问日期:2023 年 5 月 12 日。

[4] Dim P. Papadopoulos、Enrique Mora、Nadiia Chepurko、Kuan Wei Huang、Ferda Ofli 和 Antonio Torralba. 食品图像和烹饪食谱的程序表示学习,2022 年。

[5] Sundaram Gunasekaran. 食品质量保证的计算机视觉技术. 《食品科学与技术趋势》,7(8):245–256,1996 年。

[6] Yoshiyuki Kawano 和 Keiji Yanai. 基于深度卷积特征的食品图像识别. 见于《2014 年 ACM 国际联合会议论文集:普适计算与无处不在计算:附录出版物》,第 589–593 页,2014 年。

[7] Amaia Salvador、Michal Drozdzal、Xavier Giro-i Nieto 和 Adriana Romero. 逆向烹饪:从食品图像生成食谱. 见于《IEEE/CVF 计算机视觉与模式识别会议论文集》,第 10453–10462 页,2019 年。

[8] Junnan Li、Dongxu Li、Caiming Xiong 和 Steven Hoi. Blip:用于统一视觉语言理解与生成的语言图像预训练引导. 见于《国际机器学习会议》,第 12888–12900 页。PMLR,2022 年。

[9] Alexey Dosovitskiy、Lucas Beyer、Alexander Kolesnikov、Dirk Weissenborn、Xiaohua Zhai、Thomas Unterthiner、Mostafa Dehghani、Matthias Minderer、Georg Heigold、Sylvain Gelly 等. 一张图像相当于 16x16 个词:大规模图像识别的变换器. arXiv 预印本 arXiv:2010.11929,2020 年。

[10] Prateek Chhikara、Ujjwal Pasupulety、John Marshall、Dhiraj Chaurasia 和 Shweta Kumari. 面向在线心理健康风险评估的隐私意识问答系统. 见于《第 22 届生物医学自然语言处理与 BioNLP 共享任务研讨会论文集》,第 215–222 页,加拿大多伦多,2023 年 7 月。计算语言学协会。

[11] Colin Raffel、Noam Shazeer、Adam Roberts、Katherine Lee、Sharan Narang、Michael Matena、Yanqi Zhou、Wei Li 和 Peter J Liu. 探索统一的文本到文本变换器在迁移学习中的极限. 《机器学习研究期刊》,21(1):5485–5551,2020 年。

[12] Chunting Zhou、Graham Neubig、Jiatao Gu、Mona Diab、Francisco Guzman、Luke Zettlemoyer 和 Marjan Ghazvininejad. 在条件神经序列生成中检测幻觉内容. 见于《计算语言学协会发现:ACL-IJCNLP 2021》,第 1393–1404 页,2021 年。

[13] Mehrdad Farahani、Kartik Godawat、Haswanth Aekula、Deepak Pandian 和 Nicholas Broad. Chef Transformer. huggingface.co/flax-community/t5- 食谱生成。访问时间:2023 年 4 月 12 日。

[14] Aman Madaan, Shuyan Zhou, Uri Alon, Yiming Yang, 和 Graham Neubig. 代码语言模型是少量示例的常识学习者。载于《计算语言学协会发现:EMNLP 2022》,2022 年。

我的职业转型:我是如何从电气工程转行到数据工程的

原文:towardsdatascience.com/rewiring-my-career-how-i-transitioned-from-electrical-engineering-to-data-engineering-f888c1dd65ae?source=collection_archive---------2-----------------------#2024-11-15

数据正在蓬勃发展。它以庞大的数量和多样性呈现,而这一爆炸式增长也带来了大量的就业机会。现在转行做数据行业值得吗?我的诚实看法:绝对值得!

Loizos LoizouTowards Data Science Loizos Loizou

·发表于Towards Data Science ·阅读时间 10 分钟·2024 年 11 月 15 日

--

值得一提的是,本文来自一位电气与电子工程专业的毕业生,他一路走来,花了将近 8 年时间在学术界深入学习能源领域(当我说一路走来时,我的意思是从本科到博士再到博士后)。尽管这是一个在就业市场上需求量很大的职业,我还是决定转向数据工程师的道路。

我常常在论坛和博客中看到来自不同学科的人们询问如何转行做数据工作。因此,本文将带你走过我的经历,展示一个工程学毕业生是如何轻松过渡到这个新领域的。我将介绍数据行业的就业市场、我的故事,以及工程师们(无论是电气、机械、电子等)具备的技能,这些技能使他们能够很好地适应这个快速发展的领域。

当我在 ChatGPT 中输入我的文章标题时,生成的 AI 图像。令人印象深刻,是不是?

数据专业人才的必要性

随着技术的指数级进步(物联网设备、人工智能、网页服务等),每天产生的数据量也在急剧增加。由此产生的结果是什么呢?对人工智能和数据专业人员的需求达到了历史最高点,而且我认为这种需求只会越来越高。目前,这一需求的增长速度远远超过供应,每天都有新的职位发布。

根据DICE 科技职位报告,数据工程师和数据科学家等职位是增长最快的科技职业之一。原因是公司终于意识到,通过数据可以解锁无限的商业洞察力,从而揭示产品的优缺点。也就是说,前提是以正确的方式分析数据。

那么,这对未来寻求工作的数据专业人员意味着什么呢?以下几点应该是正确的,至少在未来几年内是这样:

  1. 无限的职位发布: 根据 LinkedIn 的最新报告,过去两年中,人工智能职位的招聘增长了 119%。同样,数据工程职位的增长也达到了 98%。这突显了公司急需招聘这些专业人员的紧迫性。

  2. 高薪潜力: 当需求超过供应时,薪资立刻上升。这是经济学的基本法则。如今,数据专业人员正处于一个可以选择多种工作的时代,因为公司意识到他们为公司带来的价值。

  3. 多个行业机会: 以我为例。我曾在能源、零售和金融行业从事数据工作。我认为自己是数据“无偏见”的,因为我现在能够从相对广泛的行业中选择机会。

  4. 未来职位增长: 如前所述,对这些专业人员的需求只会越来越大,因为数据形式各异,各种数据需要有能力处理的人。

从其他工程领域转到数据领域

那么,这里就有一个百万美元的问题:一名工程师,无论是机械、电子、电气、土木等,如何转行进入数据领域? 很好的问题。

这容易吗?。值得吗?绝对值得。这个问题没有标准答案。但是,我可以告诉你我的经验,你可以自己判断。我还可以告诉你我在工程学位和现在从事的工作之间找到的相似之处。那么我们开始吧。

我转行做数据工程的简短故事:

2020–2022 年

2020 年,我即将完成博士学位。对于自己未来的选择感到困惑,不知道在经历了长达 4 年的博士生涯后能做什么(并且有严重的冒名顶替综合症),我选择了学术界的安全道路,进入了一家研发中心做博士后。

冒名顶替症(Imposter syndrome),有趣的是,对博士毕业生来说非常常见。它被定义为“持续无法相信自己的成功是应得的,或者是通过自己的努力或技能合法实现的”。图片来源:DALL-E。

在那里工作时,我意识到我需要离开学术界。我不再有力气去读更多的论文、提案,甚至更糟的是,写期刊和会议论文来展示我的工作。我做过所有这些——我已经够了。我在博士期间有大约 7 到 8 篇期刊/会议论文发表,我并不喜欢这种方式成为展示我工作的唯一途径。所以,我开始寻找行业内的工作。

在 2021 年,我成功获得了能源咨询领域的工作。你猜怎么着?更多的报告,更多的论文,甚至更棒的,PowerPoint 幻灯片!我觉得我的工程日子已经过去,简直什么有用的事情都做不了。在那份工作短短待了一段时间后,我又开始重新找工作了。我想找一些有技术挑战和意义的工作,能够让我的大脑再次活跃起来。这时,我开始寻找数据相关的职业,利用我在职业生涯中获得的技能。而且,这也是我一生中收到最多拒绝的时刻!

从非常成功的本科学位和博士学位背景出发,我无法理解为什么我的技能不适合数据职位。我申请了数据工程师、分析师和科学家的职位,但我收到的只有自动回复,比如“很抱歉,我们无法继续推进您的申请。”

就是那时,我开始投递到几乎所有地方。如果你正在读这篇文章,是因为你觉得自己无法转型,相信我,我懂你。

2022-2023 年

所以,我开始向所有与数据相关的职位投递简历。甚至是那些我没有任何职位描述中提到的技能的职位。就这样,魔法发生了。

我收到了零售行业一家公司关于“商业智能执行官”职位的面试机会。你知道这个职位是什么吗?不知道?没错,我也不知道。在职位描述中,我看到的唯一要求是需要 3 到 5 年的数据科学经验。于是我想,这跟数据有关系,为什么不试试呢?我拿到了这份工作,并开始在那里工作。结果发现,“商业智能”是一个基本上是商业部门的商业智能工作描述。幸运的是,它正合我意。它让我有机会开始尝试商业智能。

商业智能(BI)被定义为“收集、存储和分析公司数据的技术基础设施”。照片由Carlos Muza提供,来自Unsplash

在那个职位上,我最初使用 Power BI,因为这个角色的工作内容是构建报告和仪表盘。之后,我渴望学习更多。我很幸运,经理非常棒,他/她信任我,允许我对数据做任何我想做的事情。所以我就这样做了。

不知不觉中,我的工程技能回来了。多年来我获得的所有问题解决技能,解决挑战的兴趣以及接触不同编程语言的经验开始互相联系。我开始在 Power BI 中构建自动化,然后扩展到编写 SQL 来自动化更多内容,接着用 Python 构建数据管道。在一年内,我几乎完成了所有流程的自动化,并且知道自己具备了应对更具挑战性和技术密集型问题的能力。我构建了令人惊叹的仪表盘,给业务负责人带来了有用的见解,那感觉真是太棒了。

这是顿悟时刻。这个职业,不管数据是什么,正是我一直在寻找的。

2023 年-至今

在公司工作了一年半后,我知道是时候寻找比单纯的商业智能更具技术挑战性的工作了。就在这时,一个数据工程师职位的机会出现了,我抓住了它。

照片由Boitumelo拍摄,来源于Unsplash

在过去的一年半里,我一直在金融行业担任数据工程师。我拓展了我的知识,涉及了人工智能、实时流数据管道、API、自动化等众多领域。工作机会不断涌现,我感到非常幸运能够做出这个转变,我也强烈推荐这种转型。挑战大吗?我会说,在商业智能和数据工程岗位上,唯一具挑战性的部分就是前 3 个月,直到我熟悉了我们使用的工具和环境。我的工程专业知识使我能够兴奋地应对各种问题,并做出令人惊叹的工作。我不会为了任何事情改变我的学位,甚至不会为了计算机科学学位改变。我的工程学位是如何帮助我完成这次转型的?这一点将在下一部分讨论。

工程学如何为你提供帮助,让你在数据职业中脱颖而出

所以,如果你读到这里,你一定在想:我的工程学位如何为我的数据职业做准备?这个人没有告诉我任何关于这个的问题。你说得对,我们来谈谈这个问题。

工程学位很重要,不是因为它所涉及的学科,而是因为它们塑造了学习者的大脑。这是我的个人观点,但通过我的工程学位,我接触到了许多事物,并且它们让我在解决每一个问题时都变得更加自信。但让我们来谈谈具体的内容。这些是我在数据工作中每天都能用到的一些关键工程技能,我看到它们之间有很多相似之处:

  1. 编程: 作为一名电气和电子工程师,我在我的学位课程中接触了多种编程语言。我使用过汇编语言、Java、VHDL、C 和 Matlab。同样,我认为其他工程学科也是如此,因为编程是工程中进行仿真模拟的方式。尽管在我的学位课程中我没有使用 Python 或 SQL,但在接触了如此多的技术之后,转向这两者时几乎没有任何障碍。我甚至会说这非常令人享受,因为我曾经在本科学位时讨厌编程,但现在我喜欢它了。这可能是由于紧张的截止日期和来自多个任务的压力。

  2. 解决问题: 我每天都在解决问题,正如我第一位大学讲师在大学的第一天对我们说的那样:“Google 是你的朋友”。如果你擅长解决问题,并且已经接触过大学里工程项目的分配方式(通常他们会给你一个项目的简短描述,并期待你在一周结束时交出一个产品),相信我,你能解决数据问题。你已经经历了足够的准备。

  3. 数学与统计: 工程学学生需要学习强度很大的数学课程,如线性代数、微积分、统计学等,这些都能帮助你平滑过渡到机器学习领域。刚开始时可能有些难以理解,因为这是一个全新的领域,但你会慢慢掌握它的。

  4. 黑箱问题: 我甚至不知道这是否是一个正式的定义,但我认为“黑箱”问题是那些极其难以解决的问题,我们已经使用它们,它们有效,但很少有人真正知道背后发生了什么。在数据领域,“黑箱问题”就是人工智能。它很火,运行得很好,令人惊叹,但没有人真正知道背后发生了什么。类似地,工程学科也有自己的“黑箱”问题。当然,人工智能很难,但你试过理解电力网络问题吗?那可不是轻松的事。

  5. 建模与仿真: 每个工程学学生都会进行建模和仿真,这与机器学习模型和数据模型没有什么不同。

  6. 数据处理与分析: 作为一名本科学位和博士学位的工程专业学生,我做了大量的数据处理、转换和分析,涉及示波器文件、传感器文件和具有数百万行数据的智能设备。这些是我们在数据行业中称之为数据管道的例子。那时我其实并不知道这就是它的名称。当我开始在企业环境中做这些事情时,这些技能是可转移的,帮助了我很多。

  7. 自动化: 工程师讨厌重复的程序。如果有办法自动化某个任务,他们会去做。这正是数据工程师需要具备的心态。我把这种心态带到了我的数据工程师岗位,它帮助了我很多,因为我每天都花很多时间在自动化工作上。

  8. 向非技术人员展示和解释: 在我的博士期间,我做的一个非常常见的事情就是向非技术人员解释我的项目,让他们能够理解我在做什么。这在数据领域很常见。你为业务人员准备了大量的分析,因此你也必须能够向他们解释。

上述所有内容每天都在我的数据工程师岗位上帮助我。你现在能看到这些可转移的技能了吗?

那么,这是一个幸福的结局吗?

虽然我不想鼓励所有工程学科的人跳槽到数据岗位,但我依然认为所有工程师都是有用的。我写这篇文章是想鼓励那些想要转行的人。现在有很多拒绝,但同时也有很多机会。你所需要的只是一个合适的机会,然后魔法就会随之而来,因为你将能够发挥你的技能。重要的是要不断尝试。

如果你喜欢这篇文章,请给我一些掌声,并在medium.com/@loizosloizou08上关注我。

还有更多内容,请继续关注 😃

RFM 细分:释放客户洞察

原文:towardsdatascience.com/rfm-segmentation-unleashing-customer-insights-da58deae4eb9?source=collection_archive---------3-----------------------#2024-07-02

使用 RFM 细分将客户数据转化为可操作的洞察

Vito RihaldijiranTowards Data Science Vito Rihaldijiran

·发表于Towards Data Science ·阅读时间 8 分钟·2024 年 7 月 2 日

--

封面图片由作者在 DALL-E 中生成

第一部分:RFM 细分

当我们谈到客户细分时,方法各不相同。其实,这取决于我们的目标,但客户细分的主要目的是根据客户的相似性将其分到不同的群体中。在实际应用中,这种方法将帮助企业根据细分信息,针对市场细分制定量身定制的营销策略。

RFM 细分是客户细分的一个例子。RFM 代表最近购买、购买频率货币价值。由于其简洁而强大的方法,这种技术在商业领域中广泛应用。根据其缩写,我们可以定义 RFM 中的每个指标如下:

  • 最近购买 (R):客户最后一次购买是什么时候?最近购买过的客户更倾向于再次购买,而那些有一段时间没有购买的客户则不太可能再购买。

  • 频率 (F):客户购买的频率是多少?购买频率较高的客户被认为更加忠诚和有价值。

  • 货币价值 (M):客户花费了多少钱?我们看重花费更多钱的客户,因为他们对我们的业务更有价值。

RFM 细分的工作流程相对简单。首先,我们收集客户在选定期间内的交易数据。请确保我们已知道客户何时交易,客户在每笔交易中购买了多少数量的特定产品,以及客户花费了多少钱。之后,我们将进行评分。我们有许多可用的阈值可以考虑,但不如我们选择一个 1 到 5 的评分范围来评估每个指标——其中 1 代表最低分,5 代表最高分。在最后一步中,我们将三个评分结合起来,创建客户细分。例如,拥有最高 RFM 分数(在最近度、频率和货币值方面均为 5)的客户被视为忠诚客户,而 RFM 分数最低的客户(在最近度、频率和货币值方面均为 1)则被视为流失客户。

在文章的后续部分,我们将利用一种流行的无监督学习技术K-Means进行 RFM 细分。

第二部分:实际示例

在这个实际示例中,我们无需收集数据,因为我们已经拥有数据集。我们将使用来自UCI 机器学习库在线零售 II 数据集。该数据集采用CC BY 4.0 许可,并且可以用于商业用途。您可以通过此链接免费访问数据集。

图 1:作者提供的在线零售 II 数据集

数据集包含了关于客户在在线零售业务中交易的所有信息,例如发票日期数量价格。数据集中有两个文件,但在本示例中我们将使用“2010-2011 年”版本。现在,让我们开始编写代码。

步骤 1:数据准备

第一步是进行数据准备。我们按照以下步骤进行:

# Load libraries
library(readxl) # To read excel files in R
library(dplyr) # For data manipulation purpose
library(lubridate) # To work with dates and times
library(tidyr) # For data manipulation (use in drop_na)
library(cluster) # For K-Means clustering
library(factoextra) # For data visualization in the context of clustering
library(ggplot2) # For data visualization

# Load the data
data <- read_excel("online_retail_II.xlsx", sheet = "Year 2010-2011")

# Remove missing Customer IDs
data <- data %>% drop_na(`Customer ID`)

# Remove negative or zero quantities and prices
data <- data %>% filter(Quantity > 0, Price > 0)

# Calculate the Monetary value
data <- data %>% mutate(TotalPrice = Quantity * Price)

# Define the reference date for Recency calculation
reference_date <- as.Date("2011-12-09")

数据准备过程至关重要,因为细分将参考我们在此步骤中处理的数据。在加载库和数据后,我们执行以下步骤:

  • 去除缺失的客户 ID: 确保每笔交易都有有效的客户 ID 对于准确的客户细分至关重要。

  • 去除负数或零数量和价格: 数量或价格为负数或零在 RFM 分析中没有意义,因为它们可能表示退货或错误。

  • 计算货币价值: 我们通过将数量和价格相乘来计算货币价值。稍后我们将对指标进行分组,其中一项按客户 ID 以货币值表示。

  • 定义参考日期: 这对于确定最近度值非常重要。检查数据集后,我们知道“2011-12-09”是数据集中最接近的日期,因此将其设为参考日期。参考日期用于计算自每个客户上次交易以来经过了多少天。

经过此步骤后,数据将如下所示:

图 2:作者绘制的数据准备后的数据集

步骤 2:计算并缩放 RFM 指标

在此步骤中,我们将在聚类部分之前计算并缩放每个指标。我们按如下方式进行:

# Calculate RFM metrics
rfm <- data %>%
  group_by(`Customer ID`) %>%
  summarise(
    Recency = as.numeric(reference_date - max(as.Date(InvoiceDate))),
    Frequency = n_distinct(Invoice),
    Monetary = sum(TotalPrice)
  )

# Assign scores from 1 to 5 for each RFM metric
rfm <- rfm %>%
  mutate(
    R_Score = ntile(Recency, 5),
    F_Score = ntile(Frequency, 5),
    M_Score = ntile(Monetary, 5)
  )

# Scale the RFM scores
rfm_scaled <- rfm %>%
  select(R_Score, F_Score, M_Score) %>%
  scale()

我们将此步骤分为三部分:

  • 计算 RFM 指标: 我们创建了一个新的数据集,称为RFM。我们首先通过 CustomerID 进行分组,这样每个客户的后续计算都是单独进行的。然后,我们计算每个指标。我们通过将参考日期与每个客户的最新交易日期相减来计算最近性(Recency),通过计算每个客户唯一发票的数量来计算频率(Frequency),并通过对每个客户所有交易的总价进行求和来计算金额(Monetary)

  • 分配 1 到 5 的分数: 评分有助于将客户按 RFM 从高到低分类,其中 5 为最高,1 为最低。

  • 缩放分数: 然后,我们对每个指标的分数进行缩放。此缩放确保每个 RFM 分数对聚类过程的贡献相等,避免由于不同范围或单位而导致某一指标的主导地位。

在完成此步骤后,RFM 数据集中的结果将如下所示:

图 3:作者绘制的 RFM 打分

缩放后的数据集将如下所示:

图 4:作者绘制的缩放后的 RFM 数据集

步骤 3:K-Means 聚类

现在我们来到了最后一步——K-Means 聚类。我们通过以下方式进行:

# Determine the optimal number of clusters using the Elbow method
fviz_nbclust(rfm_scaled, kmeans, method = "wss")

# Perform K-means clustering
set.seed(123)
kmeans_result <- kmeans(rfm_scaled, centers = 4, nstart = 25)

# Add cluster assignment to the original RFM data
rfm <- rfm %>% mutate(Cluster = kmeans_result$cluster)

# Visualize the clusters
fviz_cluster(kmeans_result, data = rfm_scaled, 
             geom = "point",
             ellipse.type = "convex",
             palette = "jco",
             ggtheme = theme_minimal(),
             main = "Online Retail RFM Segmentation",
             pointsize = 3) +
  theme(
    plot.title = element_text(size = 15, face = "bold"),
    axis.title.x = element_blank(),  
    axis.title.y = element_blank(),  
    axis.text = element_blank(),     
    axis.ticks = element_blank(),    
    legend.title = element_text(size = 12, face = "bold"),
    legend.text = element_text(size = 10)
  )

此步骤的第一部分是确定使用肘部法则的最佳聚类数。该方法是wss(即“聚类内平方和”),用于衡量聚类的紧凑性。该方法通过选择 wss 开始快速减少的点来确定聚类数,并形成一个“肘部”。肘部在4处减少。

图 5:作者绘制的肘部法则实现

下一部分是进行聚类。我们指定 4 为聚类数,25 为初始聚类中心的随机集,并根据最低的聚类内平方和选择最佳的聚类中心。然后,将其添加到RFM数据集中。聚类的可视化如下所示:

图 6:作者绘制的聚类可视化

请注意,图中聚类的大小与每个聚类中的客户数量没有直接关系。可视化显示的是每个聚类中数据点的分布,基于的是缩放后的 RFM 分数(R_Score, F_Score, M_Score),而不是客户的数量。

第三部分:总结

运行此代码后,RFM 分段的总结如下:

# Summary of each cluster
rfm_summary <- rfm %>%
  group_by(Cluster) %>%
  summarise(
    Recency = mean(Recency),
    Frequency = mean(Frequency),
    Monetary = mean(Monetary),
    Count = n()
  )

图 7:作者绘制的 RFM 分段总结

从总结中,我们可以从每个集群中生成见解。建议会有很大的不同。然而,如果我是一个在线零售业务的数据科学家,我会想到以下几点:

  • 集群 1: 他们最近刚刚购买了商品——通常是在一个月左右前——表明他们近期有参与。这一客户群体的特点是购买不频繁,总消费金额相对较小,平均每人购买 1 至 2 次。基于这些发现实施客户保持活动可能会非常有效。考虑到他们最近的参与,可以采取跟进邮件或个性化优惠的忠诚度计划等策略,鼓励重复购买。这为我们提供了一个机会,建议他们购买与之前商品互补的其他产品,从而提高该组客户的平均订单金额和整体消费。

  • 集群 2: 该组客户大约在两周前刚刚完成购买,并且展现出频繁的购买习惯和显著的消费金额。被视为顶级客户,值得 VIP 待遇:优质的客户服务、专享优惠和提前体验新商品的机会。利用他们的满意度,我们可以提供带有奖金和折扣的推荐计划,鼓励他们向家人和朋友推荐,从而可能扩大我们的客户群并增加整体销售额。

  • 集群 3: 该细分群体的客户在过去三个月内一直不活跃,尽管他们的购买频率和金额属于中等水平。为了重新吸引这些客户,我们应考虑推出重激活活动。发送带有特别折扣的挽回邮件,或者展示新到商品,可能会吸引他们回归。此外,收集反馈以揭示他们近期未购买的原因,并解决他们可能存在的问题或担忧,可以显著提升他们未来的购物体验,并重新点燃他们的兴趣。

  • 集群 4: 该组客户的购买时间最长为七个月,表明存在较长的沉默期。他们的购买频率和金额最低,容易流失。在这种情况下,必须实施专门针对沉默客户的策略。发送重要的基于优惠的重激活邮件或个性化激励通常能有效促使这些客户回归。此外,进行退出调查有助于识别他们不活跃的原因,从而优化产品和服务,更好地满足客户需求并重新激发他们的兴趣。

恭喜!你已经学会了如何使用 K-Means 进行 RFM 细分,现在轮到你用自己的数据集进行相同的操作了。

物理动力学系统的强化学习:一种替代方法

原文:towardsdatascience.com/rl-for-physical-dynamical-systems-an-alternative-approach-8e2269dc1e79?source=collection_archive---------1-----------------------#2024-07-28

重新引入遗传算法并与神经网络进行比较

Robert EtterTowards Data Science Robert Etter

·发布于 Towards Data Science ·14 分钟阅读·2024 年 7 月 28 日

--

图片来自 Tra NguyenUnsplash

物理与非线性动力学

控制理论通过经典、鲁棒和最优方法支撑了现代文明。精炼、电信、现代制造等各个领域都依赖于它们。控制理论建立在物理方程提供的见解之上,例如牛顿定律和麦克斯韦方程。这些方程描述了物理系统中的动态和不同力的相互作用。通过它们,我们理解方程如何在不同状态之间转换,其中“状态是足够描述系统的所有信息的集合”[1],通常是通过流体动力学中的压力或流体粒子的速度,或者在电动力学中的电荷和电流状态来表达。通过推导系统方程,我们可以预测状态如何随时间和空间变化,并通过微分方程表达这种演化。通过这种理解,我们可以采用控制手段,即通过特别施加的力,将这些系统维持在期望的状态或输出上。通常,这种力是根据系统的输出计算的。以车辆巡航控制为例,输入是期望的速度,输出是实际的速度。系统是发动机。状态估计器观察速度,并确定输出与输入速度之间的差异,然后应用控制手段(如调整燃油流量)来减少误差。

然而,尽管控制理论取得了诸多成就,它仍面临着显著的局限性。大多数控制理论是基于线性系统,或是输入的比例变化导致输出的比例变化的系统。虽然这些系统可能非常复杂,但我们对这些系统有着广泛的理解,使我们能够实际控制从深海潜水器、矿山设备到航天器的各种设备。

然而,正如斯坦尼斯瓦夫·乌拉姆所言:“使用‘非线性科学’这样的术语,就像是把大部分动物学称为研究非大象动物。”到目前为止,我们在控制复杂物理系统方面的进展大多通过找到限制它们线性行为的方法。这可能会在多个方面带来效率损失:

· 将复杂系统拆解为可单独控制的组件,优化子系统而非整个系统

· 在较简单但效率较低的操作模式下运行系统,或未能充分利用复杂的物理学原理,例如主动流动控制以减少飞机的阻力

· 严格的操作条件限制,一旦超过可能导致不可预测或灾难性的故障

高级制造业、改进的空气动力学和复杂的电信系统都将受益于更好的非线性系统控制方法。

非线性动力学系统的基本特征是它们对输入的复杂响应。即使在环境或状态发生小幅变化的情况下,非线性系统也会剧烈变化。以控制流体流动的纳维-斯托克斯方程为例:同一组方程既描述了一个平静、缓慢流动的小溪,也描述了一个汹涌的洪流,所有洪流中的漩涡和特征都包含在方程的动态变化之中。

非线性系统带来了困难:与线性系统不同,我们通常无法轻松预测系统在从一个状态过渡到下一个状态时的行为。我们所能做到的最好方法是通过一般分析或广泛的仿真。因此,在非线性系统中,我们面临两个问题:系统识别——即理解系统在给定状态下的行为,以及系统控制——即系统在响应给定输入时的短期和长期变化,以及如何选择输入以获得期望的结果。

强化学习与物理学

尽管非线性分析和控制不断取得进展,但我们在利用这些系统方面仍然受到传统基于方程方法的限制。然而,随着计算能力和传感器技术变得更加普及,基于数据的方法提供了一种不同的途径。

数据可用性的剧增催生了机器学习(ML)方法,而强化学习(RL)提供了一种新的方法,以更有效地应对控制非线性动态系统的挑战。RL 已经在从自动驾驶汽车到战略和计算机游戏的环境中取得了成功,它是一个机器学习框架,通过“试错”来训练算法或智能体,使其“学会如何在不确定性下做出决策,以最大化长期收益”[1]。换句话说,RL 算法解决了系统识别和控制优化的问题,而不是通过操控和分析控制方程来实现,而是通过采样环境来预测哪些输入动作会导致期望的结果。RL 算法或智能体根据系统状态应用一套行动策略,并随着对系统更多信息的分析而不断完善这一策略。

许多强化学习(RL)算法基于使用神经网络来开发将状态映射到最佳行为的函数。RL 问题可以被构建为状态-行动-奖励三元组。对于给定的状态,某个特定的行动会导致一个特定的奖励。神经网络作为通用函数逼近器,可以进行调整,以准确地逼近整个系统中的状态-行动-奖励三元组函数。为了做到这一点,神经网络必须通过探索系统或环境来获取新知识,然后通过利用额外获得的数据来精炼其策略。RL 算法通过它们如何应用数学来探索、利用和在两者之间平衡,从而实现差异化。

然而,神经网络也带来了一些挑战:

· 资源需求。使用神经网络来估算一个能够为每个状态确定奖励和最佳行动的函数可能需要相当长的时间和大量的数据。

· 可解释性。通常很难理解神经网络是如何得出其解决方案的,这限制了它们在提供真正洞见方面的实用性,并且可能使得预测或界定神经网络的行为变得困难。可解释性对于物理系统尤其重要,因为它能够使得几百年来数学上开发出的强大分析工具得以应用,从而为系统提供额外的洞见。

尽管存在一些方法,如迁移学习和拓扑分析,来应对这些挑战,但它们仍然是强化学习广泛应用的障碍。然而,另一种替代方法可能在我们专注于物理系统时会有所帮助。回想一下,我们讨论的物理系统是通过数学方程来定义的,或者可以通过数学方程很好地描述。与其开发一个完全任意的函数,我们可以集中精力寻找由常见数学运算符组成的表达式:算术运算、代数运算以及超越函数(如正弦函数、e^x 等)。为此目的的一种方法是使用遗传算法。正如在[2]中所描述的,遗传算法可以通过随机生成函数并通过变异和交叉繁殖有前途的候选者来探索函数空间,并通过这些方式利用和优化解决方案。

因此,尽管神经网络在大多数强化学习问题中表现出色,但在物理动态系统中,出现了一位新的挑战者。接下来,我们将更深入地研究遗传算法方法,并查看它如何与领先的强化学习算法——软演员评论家(SAC)进行对比。为此,我们将在基于物理的训练场中使用 AWW Sagemaker 实验来评估两者的表现。最后,我们将通过评估结果、讨论结论并建议下一步的工作来结束本文。

回想一下,强化学习(RL)面临着两个挑战:探索环境和利用已发现的信息。探索对于找到最佳策略是必要的,考虑到处于任何状态的可能性。如果不进行探索,就可能错过全局最优解,而仅停留在局部最优,且算法可能无法足够泛化,从而在所有状态下都能成功。利用已知信息则是为了将当前解决方案优化到最优。然而,当算法精细化一个特定的解决方案时,它就牺牲了进一步探索系统的能力。

Soft Actor Critic(SAC)是对强大 Actor-Critic 强化学习方法的改进。Actor-Critic 算法家族通过将状态值及其相关奖励的估计与优化特定输入策略分开,来处理探索与利用之间的权衡。随着算法收集新信息,它会更新每个估计器。Actor-Critic 在实现上有许多细节;有兴趣的读者可以参考书籍或在线教程。SAC 通过优先探索那些奖励与估计的评价值差异显著的状态,来优化评价器。OpenAI提供了 SAC 的详细描述。

对于本实验,我们使用了SAC 的 Coax 实现。我查看了几个强化学习库,包括 Coach 和 Spinning Up,但 Coax 是我找到的少数几个在当前 Python 版本中“开箱即用”的库之一。Coax 库包括广泛的强化学习算法,包括 PPO、TD3 和 DDPG,并且与 gymnasium 兼容良好。

像 SAC 这样的演员-评论员方法通常通过神经网络作为函数近似器来实现。正如我们上次讨论的那样,还有一种潜在的方法来探索系统并利用潜在的控制策略。遗传算法通过随机生成可能的解决方案来进行探索,并通过变异或组合不同解决方案的元素(繁殖)来利用有前景的策略。在这种情况下,我们将评估遗传算法的遗传编程变体,作为函数近似的另一种手段;具体来说,我们将使用遗传方法随机生成并评估包含常量、状态变量和数学函数的函数树,作为潜在的控制器。

实现的遗传编程(GP)算法改编自[2],不同之处在于,该文中使用的锦标赛被本实现替换为选择每代的前 64%(Nn,以下为 33%)作为突变的候选,并为剩余部分重新播种,以便更好地探索解空间。为了在每一代中创建个体树,生长函数随机调用算术函数(+,-,*,/)和超越函数(例如 e^x, cos (x))来构建分支,并将常量或状态变量作为叶子,构成树的终端分支。递归调用用于根据波兰表示法([2]通过 LISP 实现,我已将其改编为 Python)构建表达式,设有规则以避免如除以 0 等问题,并确保数学一致性,使每个分支最终正确地以常量或传感器值作为叶子。概念上,一个方程树的形式如下:

图 1. 示例函数树,作者基于[2]提供

这导致了一个控制器 b = sin (s1) + e^(s1*s2/3.23) - 0.12,脚本中写作: — + sin s1 e^ / * s1 s2 3.23 0.12,其中 s 表示状态变量。刚开始可能会让人感到困惑,但写出一些例子可以澄清这一方法。

在构建完整代数的树后,每棵树都会通过环境进行性能评估。然后,基于获得的奖励,按控制性能对树进行排名。如果未达到期望的性能,则保留表现最好的树,前 66% 的树通过交叉(交换两棵树的元素)、剪切与生长(替换树的一个元素)、收缩(用常量替换树元素)或重新参数化(替换树中的所有常量)进行变异,参考文献[2]。这使得能够利用最有前景的解决方案。为了继续探索解空间,表现较差的解决方案会被随机新树所替代。每一代之后,新的代际将是随机新个体与顶级表现解决方案的复制或变异的结合。

树会在环境中随机起始位置进行测试。为了防止“幸运”的起始状态影响结果(类似于模型过拟合),树会在一批不同的随机起始状态下进行测试。

遗传编程的超参数包括:

表 1. 遗传编程算法的超参数

注释过的代码可以在github上找到。请注意,我是一个业余编程者,代码可能有些笨拙。希望即使存在不符合 Python 风格或普遍不良的编程实践,代码至少足够可读,能够理解我的方法。

评估方法

两种算法在两个不同的 gymnasium 环境中进行了评估。第一个是 gymnasium 基金会提供的简单摆锤环境。倒立摆是一个简单的非线性动力学问题。动作空间是可以施加于摆锤的连续扭矩。观察空间与状态相同,是 x、y 坐标和角速度。目标是保持摆锤直立。第二个是相同的 gymnasium 环境,但在观察中加入了随机噪声。噪声服从均值为 0,方差为 0.1 的正态分布,以模拟现实传感器测量值。

强化学习开发中最重要的部分之一是设计一个合适的奖励函数。尽管有许多算法可以解决给定的强化学习问题,但为这些算法定义一个合适的奖励以便优化,是使某个算法在特定问题上成功的关键步骤。我们的奖励需要让我们能够比较两种不同强化学习方法的结果,同时确保每种方法都能够朝着其目标前进。在这里,对于每个轨迹,我们跟踪累积奖励和平均奖励。为了简化这一过程,我们让每个环境运行一个固定数量的时间步,每个时间步根据代理距离目标状态的远近给予负奖励。Pendulum gym 就是这样工作的——在 200 个时间步后截断,并根据钟摆的竖直程度给予负奖励,最大奖励为 0,并在每个时间步都进行强制执行。我们将使用平均奖励来比较这两种方法。

我们的目标是评估每个强化学习(RL)框架的收敛速度。我们将使用 AWS Sagemaker 实验来实现这一目标,它可以自动追踪度量指标(如当前奖励)和参数(如活跃的超参数),并按迭代或 CPU 时间跨运行进行记录。尽管这些监控可以通过 Python 工具实现,但 Experiments 提供了简化的运行参数和性能跟踪、索引功能,以及计算资源的复现。为了设置实验,我参考了AWS提供的示例。SAC 和 GP 算法首先在本地 Jupyter 笔记本中进行评估,然后上传到 git 仓库。每个算法都有自己的仓库和 Sagemaker 笔记本。运行参数被存储以帮助分类运行并跟踪不同实验设置的性能。我们的运行度量,奖励和状态向量,是我们希望用来比较两种算法的因变量。Experiments 自动记录 CPU 时间和迭代次数,作为自变量。

通过这些实验,我们可以将冠军算法——像 SAC 这样的成熟且经过充分开发的强化学习算法——与挑战者算法进行比较,后者是一种不为人知的方法,由一位没有正式强化学习或 Python 培训的业余编码员编写。这个实验将为开发复杂非线性系统控制器的不同方法提供见解。在下一部分,我们将回顾并讨论结果及潜在的后续工作。

第一个实验是默认的 Pendulum gymnasium,在这个实验中,算法尝试确定正确的扭矩,以保持钟摆倒立。它在固定时间后结束,并根据钟摆距离竖直的程度给予负奖励。在 Sagemaker 实验运行之前,SAC 和 GP 算法已经在我的本地机器上运行,以验证收敛性。在 Experiments 中运行可以更好地追踪可比较的计算时间。以下是计算时间与每次迭代平均奖励的结果:

作者提供

作者提供

我们看到,尽管 GP 算法相对不够成熟,但它在计算需求上远低于 SAC 算法。通过本地运行至完成,SAC 似乎需要大约 40 万次迭代才能收敛,耗时数小时。本地实例化被编程为在整个训练过程中记录 SAC 的进展;有趣的是,SAC 似乎先学会了如何让摆锤移动到顶端,然后学会了如何保持摆锤静止,最终将这两者结合起来,这可以解释奖励的下降期,即 SAC 学习保持摆锤稳定的阶段。通过 GP,我们看到奖励以单调递增的方式逐步提升。这是因为最优的函数树始终被保留,所以最好的奖励保持稳定,直到计算出更好的控制器。

第二个实验是将高斯噪声(0, 0.1)添加到状态测量中。我们看到的结果与无噪声的情况相似,只是收敛时间较长。结果如下所示;同样,GP 优于 SAC。

作者提供

作者提供

在这两种情况下,我们看到 GP 比 SAC 表现得更快(如前面的例子所示,SAC 确实在本地收敛,只是我不想为计算时间支付 AWS 费用!)。然而,正如大家无疑已经注意到的,这只是一个非常基础的比较,既涉及机器学习也涉及物理系统。例如,超参数调优可能会导致不同的结果。尽管如此,这对于该候选算法来说是一个有前景的开始,表明它值得进一步研究。

从长远来看,我认为 GP 可能比基于神经网络的方法(如 SAC)提供几个优势:

· 可解释性。尽管 GP 找到的方程可能很复杂,但它是透明的。熟练者可以简化方程,帮助提供对已确定解的物理理解,这对于确定适用区域和增加控制信任度非常有帮助。尽管可解释性是一个活跃的研究领域,但神经网络依然面临这一挑战。

· 信息化机器学习(Informed ML)。遗传规划(GP)使得将系统分析的见解更容易应用。例如,如果已知系统具有正弦波行为,GP 算法可以调整以尝试更多的正弦解。或者,如果已知一个类似或简化系统的解,那么可以将该解预先植入算法中。

· 稳定性。通过简单的保障措施(如数学有效性和限制绝对值的增加),GP 方法将保持稳定。只要每一代保留最优解,解决方案就会收敛,尽管收敛的时间界限无法保证。常见的强化学习中的神经网络方法并没有这样的保证。

· 发展机会。遗传编程相对不成熟。这里使用的 SAC 实现是多种应用实现中的一种,而神经网络已经受益于大量努力以提高性能。遗传编程没有享受这种优化;我的实现是围绕函数构建的,而非效率。尽管如此,它在与 SAC 对比时表现良好,来自更专业开发者的进一步改进可能会带来显著的效率提升。

· 并行性和模块化。与神经网络相比,单独的遗传编程方程较为简单,计算成本来自于在环境中多次运行,而不是环境运行和神经网络的反向传播。将不同的遗传编程方程树分配到不同的处理器上,可以大大提高计算速度。

然而,神经网络方法因其众多优势被广泛使用:

· 范围。神经网络是通用函数逼近器,遗传编程仅限于在函数树中定义的项。因此,基于神经网络的方法能够涵盖更广泛和复杂的情况。我不愿意尝试用遗传编程来玩《星际争霸》或驾驶汽车。

· 跟踪。遗传编程是随机搜索的精细版本,正如实验中所见,导致了改进的停滞。

· 成熟度。由于大量不同神经网络算法的研究工作,现有的经过优化的神经网络模型更容易应用于问题,并具有较高的计算效率。

从机器学习的角度来看,我们仅仅触及了这些算法的表面。需要考虑的一些后续工作包括:

· 超参数调优。

· 控制器简化,例如惩罚遗传编程中控制输入项数的奖励。

· 控制器效率,例如从奖励中扣除控制输入的大小。

· 如上所述,遗传编程的监控与算法改进。

从物理学角度来看,本实验作为进入更真实场景的起点。更复杂的场景可能会显示出神经网络方法赶上或超过遗传编程(GP)。可能的后续工作包括:

· 更复杂的动力学,例如范德波尔方程或更高维度。

· 有限可观察性,而非完全状态可观察性。

· 偏微分方程系统以及优化控制器位置和输入。

[1] E. Bilgin, 《掌握强化学习与 Python:使用强化学习技术和最佳实践构建下一代自学习模型》(2020),Packit 出版社

[2] T Duriez, S. Brunton, B. Noack, 《机器学习控制——驯服非线性动力学与湍流》(2017),Spring 国际出版公司

RLAIF:来自 AI 反馈的强化学习

原文:towardsdatascience.com/rlaif-reinforcement-learning-from-ai-feedback-d7dbdae8f093?source=collection_archive---------8-----------------------#2024-01-23

通过自动化人类反馈使 RLHF 的对齐过程更具可扩展性…

Cameron R. Wolfe, Ph.D.Towards Data Science Cameron R. Wolfe, Ph.D.

·发表于 Towards Data Science ·18 分钟阅读 ·2024 年 1 月 23 日

--

(图片来源:Rock’n Roll MonkeyUnsplash

除了使用更大模型和数据集进行预训练外,大型语言模型(LLM)质量的显著提升还得益于对齐过程的进展,这主要是通过细化技术如监督式微调(SFT)和来自人类反馈的强化学习(RLHF)推动的。特别是 RLHF 是一种有趣的技术,它使我们能够基于人类提供的偏好直接微调语言模型。简而言之,我们可以直接教导模型生成符合人类偏好的输出,这是一个灵活且强大的框架。然而,这要求收集大量人类偏好标签,而这通常既昂贵又耗时。在本综述中,我们将探讨旨在利用 AI 自动化收集 RLHF 所需人类偏好的最新研究,形成一种新的技术,称为来自 AI 反馈的强化学习(RLAIF)。

训练语言模型

语言模型的训练过程分为几个阶段;见上文。首先,我们在大量未标注的文本数据上进行预训练,这是训练中最昂贵的部分。预训练后,我们执行一个三阶段的对齐过程…

成为数据科学家的路线图,第一部分:数学

原文:towardsdatascience.com/roadmap-to-becoming-a-data-scientist-part-1-maths-2dc9beb69b27?source=collection_archive---------1-----------------------#2024-11-27

识别有志成为数据科学家的基础数学技能

Vyacheslav EfimovTowards Data Science Vyacheslav Efimov

·发布于Towards Data Science ·11 分钟阅读·2024 年 11 月 27 日

--

介绍

数据科学无疑是当今最迷人的领域之一。随着大约十年前机器学习的重大突破,数据科学在科技圈内的流行度急剧上升。每年,我们都见证着越来越强大的工具的诞生,这些工具曾经看似不可思议。像Transformer 架构ChatGPT检索增强生成(RAG)框架,以及最先进的计算机视觉模型——包括GANs——这些创新对我们的世界产生了深远的影响。

然而,面对工具的多样性和围绕人工智能的持续热潮,初学者可能会感到不知所措——尤其是当你试图确定在数据科学领域的职业生涯中,应该优先掌握哪些技能。此外,这一领域要求非常高,需要极大的专注和毅力。

在本文中,我将呈现一份详细的路线图,概述在开始数据科学之旅时需要关注的数学领域。

本文将重点讨论进入数据科学领域所需的数学技能。是否选择这条道路作为你的职业发展方向,将取决于你的背景及其他因素……

成为数据科学家的路线图,第二部分:软件工程

原文:towardsdatascience.com/roadmap-to-becoming-a-data-scientist-part-2-software-engineering-e2fee3fe4d71?source=collection_archive---------3-----------------------#2024-12-18

编码你的数据科学之路:掌握关键的开发技能

Vyacheslav EfimovTowards Data Science Vyacheslav Efimov

·发布于Towards Data Science ·阅读时长:12 分钟·2024 年 12 月 18 日

--

引言

数据科学无疑是当今最迷人的领域之一。十年前,机器学习的重大突破促使数据科学在技术圈内迅速崛起。每年,我们都会见证越来越强大的工具,这些工具曾经是难以想象的。像Transformer 架构ChatGPT增强检索生成(RAG)框架以及最先进的计算机视觉模型——包括生成对抗网络(GANs)——都对我们的世界产生了深远的影响。

然而,随着工具的丰富以及围绕人工智能的持续炒作,尤其是对于初学者来说,确定在追求数据科学职业生涯时应该优先掌握哪些技能,可能会让人感到不知所措。此外,这一领域要求极高,需要大量的投入和毅力。

正如我们在第一部分中所理解的,数据科学的主要领域可以分为三大类:数学、软件工程和机器学习。在本文中,我们将重点介绍学习者需要掌握的软件工程技能,以成为数据科学家。

稳健的独热编码

原文:towardsdatascience.com/robust-one-hot-encoding-930b5f8943af?source=collection_archive---------4-----------------------#2024-04-26

Python 和 R 中的生产级独热编码技术

Hans Christian EkneTowards Data Science Hans Christian Ekne

·发表于 Towards Data Science ·阅读时间 11 分钟·2024 年 4 月 26 日

--

图像由作者使用 DALL-E 生成/还是 Dali?😉

你是否在机器学习生产环境中遇到过崩溃?

这并不有趣,尤其是当涉及到可以避免的问题时。一个经常引起问题的原因是数据的独热编码。通过我自己的经验,我发现很多这些问题在遵循一些与独热编码相关的最佳实践时是可以大大避免的。在这篇文章中,我将简要介绍这个话题,并通过一些简单的示例分享一些最佳实践,以确保你的机器学习模型的稳定性。

独热编码

什么是独热编码?

独热编码是将存储在一列中的因子变量转化为多个列中的虚拟变量,并以 0 和 1 的形式表示的做法。一个简单的例子说明了这个概念。

例如,考虑这个包含一些数字和颜色列的数据集:

import pandas as pd

# Creating the training_data DataFrame in Python
training_data = pd.DataFrame({
    'numerical_1': [1, 2, 3, 4, 5, 6, 7, 8],
    'color_1_': ['black', 'black', 'red', 'green', 
                'green', 'black', 'red', 'blue'],
    'color_2_': ['black', 'blue', 'pink', 'purple', 
                'black', 'blue', 'pink', 'purple']
})

或者更直观地展示:

训练数据,包含 3 列/图像由作者提供

color_1_列也可以像下面的表格一样表示:

“color_1_”的独热编码表示 / 图像由作者提供

color_1_从一个单列的紧凑表示转化为多列的二进制表示,这就是我们所说的独热编码(one-hot encoding)。

为什么我们要使用它?

使用独热编码有多个原因。它们可能与避免隐含排序、提高模型性能,或只是使数据与各种算法兼容有关。

例如,当你将一个类别变量(如颜色)编码成数值结构时(例如:黑色为 1,绿色为 2,红色为 3),如果不将其转换为虚拟变量,模型可能会错误地将数据误解为存在顺序关系(黑色 < 绿色 < 红色),而实际上并不存在这种顺序。

此外,在训练神经网络时,最佳实践是在将数据输入神经网络之前对数据进行标准化,对于类别变量,独热编码是一种不错的方法。其他线性模型,如逻辑回归和线性回归,假设输入是线性关系和数值型数据,因此对于这一类模型,独热编码也是一个好方法。

此外,进行独热编码的过程迫使我们确保不会将未见过的因子级别输入到我们的机器学习模型中。

最终,独热编码使得机器学习模型更容易理解数据,从而做出更好的预测。

独热编码失败的主要原因

我们构建传统机器学习模型的方式是,首先在“训练数据集”上训练模型——通常是一个历史数据集——然后在新的数据集(即“推理数据集”)上生成预测。如果训练数据集和推理数据集的列不匹配,机器学习算法通常会失败。主要原因是推理数据集中缺少列或包含新的因子级别。

第一个问题:缺失因子

对于以下示例,假设你使用上面的数据集来训练机器学习模型。你将数据集进行独热编码转换成虚拟变量,并且你的完全转换后的训练数据如下所示:

使用pd.get_dummies转换后的训练数据集 / 图片来自作者

现在,让我们引入推理数据集,这就是你用于进行预测的数据集。假设它是如下所示:

# Creating the inference_data DataFrame in Python
inference_data = pd.DataFrame({
    'numerical_1': [11, 12, 13, 14, 15, 16, 17, 18],
    'color_1_': ['black', 'blue', 'black', 'green', 
                'green', 'black', 'black', 'blue'],
    'color_2_': ['orange', 'orange', 'black', 'orange', 
                'black', 'orange', 'orange', 'orange']
})

推理数据集(包含 3 列)/ 图片来自作者

使用我们上面使用的简单独热编码策略(pd.get_dummies

# Converting categorical columns in inference_data to 
# Dummy variables with integers
inference_data_dummies = pd.get_dummies(inference_data, 
  columns=['color_1_', 'color_2_']).astype(int)

这将以相同的方式转换你的推理数据集,你会得到如下的数据集:

使用pd.get_dummies转换后的推理数据集 / 图片来自作者

你注意到问题了吗?第一个问题是推理数据集中缺少以下列:

missing_colmns =['color_1__red', 'color_2__pink', 
  'color_2__blue', 'color_2__purple']

如果你在一个用“训练数据集”训练的模型中运行这段代码,通常会崩溃。

第二个问题:新因子

另一个可能发生的独热编码问题是,如果推理数据集中包含了新的、未见过的因子。再次考虑上面的数据集。如果你仔细检查,你会发现推理数据集现在有了一个新列:color_2__orange

这是与之前问题相反的情况,我们的推理数据集包含了训练数据集中没有的新列。这实际上是一个常见的情况,如果你的某个因子变量发生了变化,就可能会发生这种情况。例如,如果上面提到的颜色代表汽车的颜色,而一个汽车生产商突然开始生产橙色的汽车,那么这些数据可能在训练数据中不可用,但仍然可能出现在推理数据中。在这种情况下,你需要一种健壮的方式来处理这个问题。

有人可能会争论,为什么不直接将转化后的训练数据集中的所有列列为推理数据集所需的列呢?这里的问题是,通常你无法事先知道训练数据中的因子水平。

例如,新的水平可能会定期引入,这可能使得维护变得困难。此外,还需要将推理数据集与训练数据进行匹配,因此你需要检查所有实际转化后的列名,这些列名是用于训练算法的,然后将它们与转化后的推理数据集进行匹配。如果有任何列缺失,你需要插入新列并填充 0 值,如果有多余的列,比如上面的color_2__orange列,那么需要删除这些列。这是一种相当繁琐的解决问题的方法,幸好有更好的选择可供使用。

解决方案

解决这个问题的方法相对直接,然而许多试图简化预测模型创建过程的包和库并未很好地实现它。关键在于拥有一个先在训练数据上进行拟合的函数或类,然后使用该函数或类的相同实例来转化训练数据集和推理数据集。下面我们将探索如何使用 Python 和 R 来完成这一操作。

在 Python 中

Python 无疑是进行机器学习的最佳编程语言之一,这主要得益于其广泛的开发者网络和成熟的包库,以及它的易用性,促进了快速开发。

关于我们上述描述的与独热编码相关的问题,它们可以通过使用广泛可用且经过测试的 scikit-learn 库来缓解,尤其是使用sklearn.preprocessing.OneHotEncoder类。因此,让我们看看如何在我们的训练和推理数据集上使用它来创建一个健壮的独热编码。

from sklearn.preprocessing import OneHotEncoder

# Initialize the encoder
enc = OneHotEncoder(handle_unknown='ignore')

# Define columns to transform
trans_columns = ['color_1_', 'color_2_']

# Fit and transform the data
enc_data = enc.fit_transform(training_data[trans_columns])

# Get feature names
feature_names = enc.get_feature_names_out(trans_columns)

# Convert to DataFrame
enc_df = pd.DataFrame(enc_data.toarray(), 
                          columns=feature_names)

# Concatenate with the numerical data
final_df = pd.concat([training_data[['numerical_1']], 
                      enc_df], axis=1)

这将生成一个最终的 DataFrame,如下面所示:

使用 sklearn 转化后的训练数据集 / 作者提供的图片

如果我们分解上面的代码,我们会看到第一步是初始化编码器类的实例。我们使用handle_unknown='ignore'选项,以便在使用编码器转化推理数据集时避免出现未知值的问题。

之后,我们将fittransform操作合并为一步,使用fit_transform方法。最后,我们从编码后的数据创建一个新的数据框,并将其与原始数据集的其余部分拼接在一起。

现在的任务是使用编码器来转换我们的推断数据集。

# Transform inference data
inference_encoded = enc.transform(inference_data[trans_columns])

inference_feature_names = enc.get_feature_names_out(trans_columns)

inference_encoded_df = pd.DataFrame(inference_encoded.toarray(), 
                                    columns=inference_feature_names)

final_inference_df = pd.concat([inference_data[['numerical_1']], 
                                inference_encoded_df], axis=1)

与之前我们使用简单的pandas.get_dummies时不同,现在我们看到我们的新数据集final_inference_df具有与训练数据集相同的列。

转换后的推断数据集,具有正确的列 / 图片来源:作者

除了我们在上面的代码中展示的内容之外,sklearn.preprocessing中的OneHotEncoder类还有很多其他功能,也同样非常有用。

例如,它允许你设置min_frequencymax_categories选项。顾名思义,min_frequency选项允许你指定一个最低频率,低于该频率的类别将被视为不常见,并与其他不常见类别一起分组,或者是max_categories选项,它限制了类别的总数。如果你不希望在训练数据集中创建过多的列,这后一种选项特别有用。

欲了解完整功能概述,请访问以下文档页面:

[## sklearn.preprocessing.OneHotEncoder

使用 sklearn.preprocessing.OneHotEncoder 的示例:scikit-learn 1.4 版本发布亮点……

scikit-learn.org](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OneHotEncoder.html?source=post_page-----930b5f8943af--------------------------------#sklearn.preprocessing.OneHotEncoder)

在 R 中

我的几个客户使用 R 在生产环境中运行机器学习模型——而且 R 有很多优秀的功能。在polars发布之前,R 的data.table包在速度和效率上优于pandas所能提供的。然而,R 无法访问像 Python 中scikit-learn这样的生产级别的包。(虽然有一些库,但它们不像scikit-learn那样成熟。)此外,尽管某些包可能具有所需的功能,但它们需要大量其他包的支持,并可能会引入依赖冲突。考虑在使用 r-base 镜像构建的 docker 容器中运行下面的命令:

RUN R -e "install.packages('recipes', dependencies=TRUE, repos='https://cran.rstudio.com/')"

它安装时间非常长,而且占用了你容器镜像的大量空间。在这种情况下,我们的解决方案是——不使用像recipes这样预构建包中的函数——而是引入我们自己实现的简单函数,使用data.table包:

library(data.table)

OneHotEncoder <- function() {
  # Local variables
  categories <- list()

  # Method to fit data and extract categories
  fit <- function(dt, columns) {
    for (column in columns) {
      categories[[column]] <<- unique(dt[[column]])
    }
  }

  # Method to turn columns into factors and 
  factorize <- function(dt) {
    for (column_name in names(categories)) {
        set(dt, j = column_name, 
        value = factor(dt[[column_name]], 
        levels = categories[[column_name]]))
    }
    return(dt)
  }

  # Method to transform columns in categories list to 
  # dummy variables
  transform <- function(dt) {
    dt = factorize(dt)
    # add row number for joins later
    dt[, rn := .I]
    for (col in names(categories)) {
      print(col)
      # Construct the formula dynamically
      formula_str <- paste("~", col, "- 1")
      formula_obj <- as.formula(formula_str)
      # Create a model model.matrix object
      mm = model.matrix(formula_obj, dt)
      mm_dt <- as.data.table(mm, keep.rownames = "rn")
      mm_dt[, rn := as.integer(rn)]

      # Perform a merge based on these row numbers
      dt <- merge(dt, mm_dt, by = "rn", all = TRUE)

       # remove the original column
      dt[, (col) := NULL]

      # set any new NAs to 0
      for (ncol in names(mm_dt)) {
        set(dt, which(is.na(dt[[ncol]])), ncol, 0)
      }
    }
    dt[, rn := NULL]
    return(dt)
  }

  # Method to get categories
  get_categories <- function() {
    return(categories)
  }

  # Return a list of methods
  list(
    get_categories = get_categories,
    fit = fit,
    transform = transform
  )
}

让我们仔细研究这个函数,看看它在我们的训练和推断数据集上是如何工作的。(R 与 Python 稍有不同,我们不使用类,而是使用一个父函数,其工作方式类似。)

首先,我们需要创建一个函数的实例:

 encoder = OneHotEncoder()

然后,就像sklearn.preprocessing中的OneHotEncoder类一样,我们的OneHotEncoder中也有一个适配函数。 我们在训练数据上使用适配函数,提供训练数据集和我们想要进行独热编码的列。

# Columns to one-hot encode
fit_columns = c("color_1_", "color_2")
# Use the fit method
encoder$fit(dt=training_data, columns=fit_columns)

适配函数简单地循环遍历我们想要用于训练的所有列,并找到每个列包含的所有唯一值。 然后,在转换函数中使用这些列及其潜在值。 现在我们有一个已安装的独热编码器函数实例,我们可以使用 R 的.RDS文件将其保存以备将来使用。

saveRDS(encoder, "~/my_encoder.RDS")

为了生成我们需要用于训练的独热编码数据集,我们在训练数据上运行转换函数:

transformed_training_data = encoder$transform(training_data)

转换函数比拟拟适配函数稍微复杂一些,它首先将提供的列转换为因子 — 使用列的原始唯一值作为因子水平。 然后,我们循环遍历每个预测列,并创建数据的model.matrix对象。 然后,将这些对象添加回原始数据集,并删除原始因子列。 我们还确保将任何缺失值设置为 0。

现在我们得到了与之前完全相同的数据集:

使用 R 算法转换的训练数据集 / 作者提供的图片

最后,当我们需要对我们的推断数据集进行独热编码时,我们在该数据集上运行编码器函数的相同实例:

transformed_inference_data = encoder$transform(inference_data)

该过程确保我们在transformed_inference_data中有与transformed_training_data中相同的列。

进一步考虑

在我们总结之前,有一些额外的考虑事项需要提及。 与机器学习中的许多其他事物一样,关于何时以及如何使用特定技术并没有总是一个简单的答案。 尽管它显然可以缓解一些问题,但在进行独热编码时也可能出现新问题。 最常见的问题与如何处理高基数分类变量以及由于增加表格大小而导致的内存问题有关。

此外,还有替代编码技术,如标签编码、嵌入或目标编码,有时可能更适合于独热编码。

每个主题都足够丰富,值得撰写一篇专门的文章,因此我们将这些内容留给有兴趣进一步探索的读者。

结论

我们已经展示了如何错误地使用 one-hot 编码技术可能导致推理数据中的错误和问题,也展示了如何通过 Python 和 R 来减轻和解决这些问题。如果不解决,one-hot 编码的管理不当可能会导致崩溃和推理问题,因此强烈建议使用更稳健的技术——例如 sklearn 的 OneHotEncoder 或我们开发的 R 函数。

感谢阅读!

文中展示并使用的所有代码可以在以下 GitHub 仓库找到: https://github.com/hcekne/robust_one_hot_encoding

如果你喜欢阅读这篇文章并希望访问更多我的内容,欢迎通过 LinkedIn 与我联系,链接为 https://www.linkedin.com/in/hans-christian-ekne-1760a259/ ,或者访问我的个人网站 https://www.ekneconsulting.com/ ,了解我提供的一些服务。如有任何疑问,请通过电子邮件 hce@ekneconsulting.com 随时与我联系。

数据科学家的鲁棒统计学 第一部分:中央趋势与离散度的韧性度量

原文:towardsdatascience.com/robust-statistics-for-data-scientists-part-1-resilient-measures-of-central-tendency-and-67e5a60b8bf1?source=collection_archive---------14-----------------------#2024-01-30

构建基础:理解和应用数据分析中的鲁棒性度量

Alessandro TomassiniTowards Data Science Alessandro Tomassini

·发布于 Towards Data Science ·阅读时间 10 分钟·2024 年 1 月 30 日

--

图像由 DALL-E 生成

统计学在数据科学中的角色至关重要,它架起了原始数据与可操作见解之间的桥梁。然而,并非所有统计方法都平等,尤其是在面对(杂乱)现实世界数据的严峻挑战时。这将我们引向鲁棒统计学的目的,它是一个旨在抵抗数据异常的子领域,这些异常常常使传统统计方法偏离正确的方向。

经典统计学与鲁棒统计学:一次必要的转变

尽管经典统计学为我们提供了很多帮助,但它们对异常值和极端值的敏感性可能会导致误导性结论。此时,鲁棒统计学应运而生,旨在在更广泛的条件下提供更可靠的结果。这种方法并不是在没有考虑的情况下丢弃异常值,而是开发出对异常值不太敏感的方法。

鲁棒统计学基于韧性原理。它是构建那些在假设出现小偏差时仍能保持不变或影响最小的统计方法,而这些假设是传统方法所依赖的。这…

稳健统计方法:数据科学家的第二部分——变量之间关系的稳健度量

原文:towardsdatascience.com/robust-statistics-for-data-scientists-part-2-resilient-measures-of-relationships-between-variables-a59b37a6907f?source=collection_archive---------7-----------------------#2024-03-09

从基础到高级的异常值丰富数据分析技术。

Alessandro TomassiniTowards Data Science Alessandro Tomassini

·发表于Towards Data Science ·15 分钟阅读·2024 年 3 月 9 日

--

图片由 DALL-E 生成

理解变量之间的相互关系对于做出基于数据的决策至关重要。当我们准确评估这些联系时,可以增强我们发现的可信度和合法性,这在学术和实践中都至关重要。

数据科学家经常使用皮尔逊相关和线性回归来探讨和衡量变量之间的关系。这些方法假设数据是正态分布、独立且具有一致的分布(或称同方差性),在满足这些条件时表现良好。然而,现实世界中的数据场景很少是理想的。它们通常受到噪声和异常值的干扰,这可能会扭曲传统统计技术的结果,导致错误的结论。本文是我们关于稳健统计方法系列的第二篇,旨在通过深入探讨稳健的替代方法,帮助我们在数据不规则的情况下仍能获得更可靠的见解。

如果你错过了第一部分:

ROI 崇拜可能对商业不利

原文:towardsdatascience.com/roi-worship-can-be-bad-for-business-1c752fca3896?source=collection_archive---------6-----------------------#2024-11-15

小心这三种过度良好的事情可能带来的危险

凯特·米诺格Towards Data Science 凯特·米诺格

·发表于 Towards Data Science ·7 分钟阅读·2024 年 11 月 15 日

--

平衡很重要。即使在我们认为自己遵循的是最先进的智慧时,我们也必须保持警觉,防止将这种智慧推向极端,导致意想不到的后果。

如果你是一位在努力让团队考虑其努力和投资的回报率(ROI)的商业领袖,你可能会觉得我疯了。当然,一切都是相对的。如果你处在这种情况的另一端,你可能不需要担心相反的情况。但也许,凭借前瞻性,你可以推动积极的进展,而不会掉入那些深陷 ROI 的同行们的陷阱。

如果你是一个分析专业人员或管理一个数据团队,你很可能被要求展示你工作的回报率,以及你提供的见解或成果。这是明智的商业决策,也是你应该培养的一项能力。但过度投入于这一努力也可能会让你的团队远离那些真正能增加价值的举措。

图片由 Imagine Buddy 提供,来源于 Unsplash

广告

作为一门学科,广告比我能想到的任何其他学科都更受到关于其存在价值的审视。一个古老的会计问题是,广告应该算作成本还是…

卷起袖子:9 个你应该探索的数据与机器学习项目示范

原文:towardsdatascience.com/roll-up-your-sleeves-9-data-and-machine-learning-project-walkthroughs-you-should-explore-9020402249ac?source=collection_archive---------5-----------------------#2024-05-23

TDS 编辑Towards Data Science TDS 编辑

·发表于 Towards Data Science ·以 通讯 形式发布 ·3 分钟阅读·2024 年 5 月 23 日

--

感觉受到启发,想写下你第一篇 TDS 文章吗?我们始终欢迎新作者的投稿

欢迎来到《Variable》的一期特别、实践导向、项目聚焦版!我们知道,现如今对数据和机器学习专业人士来说,实践技能是多么重要。在竞争激烈的求职市场中,展示你解决复杂现实问题的能力、有效使用前沿工具的能力以及从最混乱的数据集中提取有意义的洞察力,具有额外的价值。

为了帮助你扩展工具箱并激励你学习新主题、尝试新工作流程,我们收集了一些我们近期最有趣的教程。它们聚焦于项目执行的细节,很多教程还附带了大量代码片段,供你在自己的环境中进行实验。让我们开始吧!

  • 探索大型语言模型在 ICD 编码中的应用——第一部分在机器学习与医疗管理的交汇处,Anand Subramanian展示了利用大型语言模型简化临床编码这一关键过程的潜力。

  • 如何构建用于节点分类的神经网络在她的最新文章中,Claudia Ng提供了一个全面的指南,教你如何从一个 CSV 文件和 PyTorch Geometric 开始,构建基于图形的神经网络。

  • 从数据到仪表板:使用 Dash Leaflet 和 SeaRoute 库可视化古代海上丝绸之路对于所有地理空间数据爱好者,Maria Mouschoutzi 博士的首篇 TDS 文章解释了如何解决可视化海上航线的挑战。

图片由Tanner Mardis提供,来源于Unsplash

  • 使用 LangChain、Chainlit 和 Literal AI 构建可观察的 arXiv RAG 聊天机器人在详细的 RAG 项目演示中,Tahreem Rasul带我们一步步了解如何通过整合一系列强大的工具来创建一个语义化的科研论文引擎。

  • 使用 LLM 从 YouTube 学习从不同的角度接近检索增强生成,Alok Suresh的指南探索了如何从视频中提取信息并将其用于更高效的问答聊天机器人。

  • 通过 Python 包 Yellowbrick 和 PiML 解锁有价值的数据和模型见解(附代码)想动手试试 Python 吗?Dr. Theophano Mitsa分享了一个易于理解的介绍,教你如何使用 Yellowbrick 和 PiML 包(并展示如何利用它们更好地理解模型行为)。

  • 从零开始构建蛋白质的 Transformer 模型计算生物学是受益于人工智能最近进展的领域之一。举个例子:Yuan Tian在构建基础蛋白质 Transformer 模型,预测抗体序列的抗原特异性方面的迷人工作。

  • 用小狗特征网页应用程序探索 Shiny for PythonShiny for Python 将原本专注于 R 的库引入了更广泛的数据科学家群体。Deepsha Menghani的逐步教程将帮助你充分发挥其应用构建的优势。

  • 从零开始重建 PyTorch(带 GPU 支持和自动微分) 在这些操作过程中内部发生了什么?这一切是如何工作的?”这些是Lucas de Lima Nogueira在使用 PyTorch 时自问的问题——因此他尝试自己重建这个库。

准备好挽起袖子,思考更多理论性问题了吗?我们已经为你准备好了。

  • Sydney Nye的新图论指南是一个全面的学习资源,涵盖了图论的历史、基础数学以及潜在应用。

  • 如何通过物理原理为我们的数据提供更深的洞察空间?Tim Lou 博士发人深省的文章指出了令人着迷的跨学科联系。

  • “是更好地设计一个特征来包含尽可能多的关于代码系统的信息,还是找到一种方法让模型来完成这项工作?”Valerie Carey在探讨层次分类的替代编码方法时提出了这个问题。

  • Jarom Hulet通过一个引人入胜、灵感来源于钓鱼的例子,提供了关于多臂强盗问题的详细解释(以及如何解决这些问题)。

  • 为了完善你本周的阅读推荐,我们推荐Elliott Stam的关于数据投资回报率的深思熟虑的反思:这是一本为团队和经理提供的有用入门指南,帮助他们避免导致负回报的做法。

感谢你支持我们作者的工作!我们热衷于发布新作者的文章,因此,如果你最近写了一篇有趣的项目演练、教程,或是关于我们核心话题的理论反思,别犹豫,赶紧与我们分享

直到下次 Variable,

TDS 团队

在 RAG 驱动的应用中的路由

原文:towardsdatascience.com/routing-in-rag-driven-applications-a685460a7220?source=collection_archive---------1-----------------------#2024-05-09

根据查询意图引导应用流程

Sami MaameriTowards Data Science Sami Maameri

·发表于 Towards Data Science ·9 分钟阅读·2024 年 5 月 9 日

--

根据用户查询的意图在 RAG 应用中路由控制流可以帮助我们创建更有用和强大的基于检索增强生成(RAG)的应用。

我们希望用户能够交互的数据可能来自多个不同的来源,例如报告、文档、图像、数据库以及第三方系统。对于基于业务的 RAG 应用,我们可能还希望让用户能够交互来自业务各个领域的信息,例如来自销售、订单和会计系统的信息。

由于数据来源的多样性,信息的存储方式以及我们希望与其交互的方式也可能是多样的。有些数据可能存储在向量存储中,有些存储在 SQL 数据库中,还有些数据我们可能需要通过 API 调用来访问,因为它位于第三方系统中。

RAG 系统根据查询意图路由到不同的数据源

对于相同的数据,可能会设置不同的向量存储,以优化不同的查询类型。例如,可以为回答总结类问题设置一个向量存储,为回答特定、定向类问题设置另一个向量存储。

我们可能还希望根据问题的类型路由到不同的组件类型。例如,我们可能希望将查询传递给一个代理、向量存储,或者直接传递给一个 LLM 进行处理,这一切都取决于问题的性质。

根据用户查询路由到不同的组件类型

我们甚至可能希望根据问题的不同定制提示模板。

根据用户查询,通过不同的提示模板进行路由

总的来说,我们有许多理由希望在应用程序中改变和引导用户查询的流程。我们的应用程序试图满足的用例越多,我们就越有可能在整个应用程序中存在路由需求。

路由器本质上只是我们可以用来引导查询控制流的 If/Else 语句。

然而,令它们有趣的是,它们需要基于自然语言输入做出决策。因此,我们正在寻找一个基于自然语言描述的离散输出。

由于许多路由逻辑是基于使用大语言模型(LLM)或机器学习算法,这些算法本质上是非确定性的,我们无法保证路由器总是能够 100%做出正确的选择。此外,我们也不太可能预测所有进入路由器的不同查询变体。然而,通过使用最佳实践和一些测试,我们应该能够利用路由器帮助创建更强大的 RAG 应用程序。

自然语言路由器

在这里,我们将探索一些我发现的自然语言路由器,它们是由一些不同的 RAG 和 LLM 框架和库实现的。

  • LLM 补全路由器

  • LLM 函数调用路由器

  • 语义路由器

  • 零-shot 分类路由器

  • 语言分类路由器

下面的图表描述了这些路由器,并提供了它们可以找到的框架/包。

该图还包括逻辑路由器,我将其定义为基于离散逻辑工作的路由器,例如字符串长度、文件名、整数值等条件。换句话说,它们不依赖于理解自然语言查询的意图。

不同类型的自然语言路由器

让我们更详细地探讨一下这些路由器

LLM 路由器

这些利用 LLM 的决策能力,根据用户的查询选择路由。

LLM 补全路由器

这些使用 LLM 的补全调用,要求 LLM 从你传递给其提示的单词选项列表中返回一个最能描述查询的单词。然后,这个单词可以作为 If/Else 条件的一部分来控制应用程序的流程。

这就是LlamaIndex 的 LLM 选择路由器的工作原理。这也是LangChain文档中给出的一个路由器示例。

让我们看看一个代码示例,基于 LangChain 文档中提供的示例,来更清楚地说明这一点。正如你所见,在 LangChain 内部编写这样的代码是非常直接的。

from langchain_anthropic import ChatAnthropic
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import PromptTemplate

# Set up the LLM Chain to return a single word based on the query,
# and based on a list of words we provide to it in the prompt template
llm_completion_select_route_chain = (
        PromptTemplate.from_template("""
Given the user question below, classify it as either
being about `LangChain`, `Anthropic`, or `Other`.

Do not respond with more than one word.

<question>
{question}
</question>

Classification:"""
                                     )
        | ChatAnthropic(model_name="claude-3-haiku")
        | StrOutputParser()
)

# We setup an IF/Else condition to route the query to the correct chain 
# based on the LLM completion call above
def route_to_chain(route_name):
    if "anthropic" == route_name.lower():
        return anthropic_chain
    elif "langchain" == route_name.lower():
        return langchain_chain
    else:
        return general_chain

...

# Later on in the application, we can use the response from the LLM
# completion chain to control (i.e route) the flow of the application 
# to the correct chain via the route_to_chain method we created
route_name = llm_completion_select_route_chain.invoke(user_query)
chain = route_to_chain(route_name)
chain.invoke(user_query)

LLM 函数调用路由器

这利用了 LLM 的函数调用能力来选择要遍历的路由。不同的路由被设置为 LLM 函数调用中的函数,并附有适当的描述。然后,基于传递给 LLM 的查询,LLM 能够返回正确的函数(即路由),供我们使用。

这就是LlamaIndexPyDantic Router的工作方式。这也是大多数 Agent 在选择正确工具时所采用的方式。它们利用 LLM 的函数调用功能来根据用户的查询选择适合的工具。

语义路由器

这种路由器类型利用嵌入和相似度搜索来选择最佳的路径进行遍历。

每个路由都有一组关联的示例查询,这些查询会被嵌入并存储为向量。传入的查询也会被嵌入,并与来自路由器的其他示例查询进行相似度搜索。与查询最接近的路由将被选中。

实际上,有一个名为semantic-router的 Python 包可以做到这一点。让我们看一下其中的一些实现细节,以便更好地理解整个过程的工作原理。这些示例直接来自该库的 GitHub 页面。

让我们设置两条路由,一条用于关于政治的问题,另一条用于一般闲聊类型的问题。我们为每条路由分配一组可能会被问到的问题,这些问题通常用于触发该路由。这些示例查询称为话语。这些话语将被嵌入,以便我们可以用于与用户查询的相似度搜索。

from semantic_router import Route

# we could use this as a guide for our chatbot to avoid political
# conversations
politics = Route(
    name="politics",
    utterances=[
        "isn't politics the best thing ever",
        "why don't you tell me about your political opinions",
        "don't you just love the president",
        "they're going to destroy this country!",
        "they will save the country!",
    ],
)

# this could be used as an indicator to our chatbot to switch to a more
# conversational prompt
chitchat = Route(
    name="chitchat",
    utterances=[
        "how's the weather today?",
        "how are things going?",
        "lovely weather today",
        "the weather is horrendous",
        "let's go to the chippy",
    ],
)

# we place both of our decisions together into single list
routes = [politics, chitchat]

我们指定 OpenAI 作为编码器,尽管任何嵌入库都可以使用。接下来,我们使用路由器和编码器创建我们的路由层。

encoder = OpenAIEncoder()

from semantic_router.layer import RouteLayer

route_layer = RouteLayer(encoder=encoder, routes=routes)

然后,当我们将查询应用于路由器层时,它会返回应使用的查询路由。

route_layer("don't you love politics?").name
# -> 'politics'

所以,再总结一下,这个语义路由器利用嵌入和相似度搜索,通过用户的查询选择最佳的路径进行遍历。由于只需要处理一个索引查询,相较于其他类型需要调用 LLM 的路由器,这种路由器的速度应该更快。

零-shot 分类路由器

“Zero-shot 文本分类是自然语言处理中的一项任务,其中模型在一组标记示例上进行训练,但随后能够对来自先前未见类别的新示例进行分类”。这些路由器利用零-shot 分类模型为文本分配标签,标签来自你传入路由器的预定义标签集。

示例: Haystack 中的ZeroShotTextRouter,它利用了来自 Hugging Face 的 Zero Shot 分类模型。查看源代码,看看魔法发生的地方。

语言分类路由器

这种类型的路由器能够识别查询所使用的语言,并基于此进行路由。如果您的应用需要某种多语言解析能力,这会非常有用。

示例: 来自 Haystack 的TextClassificationRouter它利用 langdetect Python 库来检测文本的语言,而 langdetect 库本身使用了朴素贝叶斯算法来检测语言。

关键词路由器

这篇来自 LlamaIndex 联合创始人 Jerry Liu 的文章,讨论了 RAG 应用程序中的路由问题,其中提出了一个关键词路由器,它通过匹配查询与路由列表之间的关键词来选择路由,除此之外还提到了其他几种方案。

这个关键词路由器也可以通过 LLM 来识别关键词,或者通过其他关键词匹配库来实现。我还没有找到任何实现这种路由器类型的包。

逻辑路由器

这些使用逻辑检查来处理变量,例如字符串长度、文件名和数值比较,以决定如何路由查询。它们与编程中常见的 If/Else 条件非常相似。

换句话说,它们并不需要理解自然语言查询的意图,而是基于现有的离散变量来做出选择。

示例: 来自 Haystack 的ConditionalRouterFileTypeRouter

智能体与路由器

初看之下,路由器和智能体之间确实有很多相似之处,可能很难区分它们的不同。

这些相似之处的存在是因为智能体实际上在其流程中执行了路由操作。它们使用路由机制来选择正确的工具以完成任务。它们通常利用函数调用来选择正确的工具,就像上面描述的LLM 函数调用路由器一样。

然而,路由器通常比智能体简单得多,通常只有一个“简单”的任务,就是将任务路由到正确的地方,而不涉及与该任务相关的任何逻辑或处理。

另一方面,代理通常负责处理逻辑,包括管理它们所能访问的工具所完成的工作。

结论

我们在这里介绍了当前在不同的 RAG 和 LLM 框架及包中找到的一些不同的自然语言路由器。

关于路由的概念、包和库,随着时间的推移肯定会增加。在构建 RAG 应用程序时,你会发现,在某个时候,路由功能确实变得必要,以便构建一个对用户有用的应用程序。

路由器是这些基本构建块,它们允许你将自然语言请求路由到应用程序的正确位置,以便尽可能最好地满足用户的查询。

希望这对你有帮助!请 订阅 以便在我发布新文章时收到通知

除非另有说明,所有图片均由作者提供。第一张图片中使用的图标来自 SyafriStudi Dimitry Miroliubovand,以及 Free Icons PNG

使用 vLLM 运行和提供服务给更快的 VLM,如 Pixtral 和 Phi-3.5 Vision

原文:towardsdatascience.com/run-and-serve-faster-vlms-like-pixtral-and-phi-3-5-vision-with-vllm-1db7226c2035?source=collection_archive---------4-----------------------#2024-09-23

了解为 VLM 提供服务所需的内存量

Benjamin MarieTowards Data Science Benjamin Marie

·发表于 Towards Data Science ·阅读时间 10 分钟·2024 年 9 月 23 日

--

由 Pixtral 编码的图像 — 图片来源于作者

vLLM 目前是最快的大型语言模型 (LLM) 推理引擎之一。它支持多种模型架构和量化方法。

vLLM 还支持具有多模态输入的视觉语言模型 (VLM),这些输入包含图像和文本提示。例如,vLLM 现在可以为像 Phi-3.5 Vision 和 Pixtral 这样的模型提供服务,这些模型擅长图像描述、光学字符识别(OCR)和视觉问答(VQA)等任务。

在本文中,我将向您展示如何使用 vLLM 运行 VLM,重点介绍影响内存消耗的关键参数。我们将看到为什么 VLM 比标准 LLM 消耗更多内存。我们将以 Phi-3.5 Vision 和 Pixtral 为案例,研究一个处理包含文本和图像的提示的多模态应用。

运行 Phi-3.5 Vision 和 Pixtral 与 vLLM 的代码可以在此笔记本中找到:

获取笔记本 (#105)

了解 VLM 在 vLLM 中的内存消耗

在变换器模型中,按顺序生成文本一个标记一个标记非常慢,因为每个预测都依赖于所有先前的标记…

使用 Apple 硬件运行 LLM 推理

原文:towardsdatascience.com/run-llm-inference-using-apple-hardware-00a4a5d455b7?source=collection_archive---------8-----------------------#2024-01-30

解锁 Apple GPU 的强大功能以进行 LLM 推理,配合 MLX 使用

Christopher KargTowards Data Science Christopher Karg

·发布于 Towards Data Science ·阅读时间 15 分钟·2024 年 1 月 30 日

--

来源:www.pexels.com/photo/train-railway-near-trees-552779/

最后!

我们现在可以使用 Apple 的原生硬件进行推理并微调我们自己的 LLM(大语言模型)。本文将介绍如何创建自己的实验并运行推理。在未来,我将发布一篇关于如何微调这些 LLM 的文章(同样使用 Apple 硬件)。

如果你还没有阅读我之前的文章,我建议你去看看,因为我阐述了为什么你应该考虑托管(并且 微调)你自己的开源 LLM。我还讨论了如何 优化这个过程,以减少推理和训练时间。我将简要提及一些主题,比如量化(quantisation),因为这些内容在前面提到的文章中已有深入讨论。

我将使用 mlx 框架与 Meta 的 Llama2 模型 结合使用。有关如何访问这些模型的详细信息,可以参考我的 上一篇文章。不过,我也会在本文中简要解释如何进行操作。

让我们开始吧。

先决条件:

  1. 配备 M 系列芯片的机器(M1/M2/M3)

  2. 操作系统 >= 13.0

  3. Python 版本在 3.8–3.11 之间

在消费级硬件上运行 Mixtral-8x7B 并进行专家卸载

原文:towardsdatascience.com/run-mixtral-8x7b-on-consumer-hardware-with-expert-offloading-bd3ada394688?source=collection_archive---------1-----------------------#2024-01-11

寻找内存使用和推理速度之间的最佳平衡

Benjamin MarieTowards Data Science Benjamin Marie

·发表于Towards Data Science ·阅读时长 8 分钟·2024 年 1 月 11 日

--

Mixtral-8x7B 专家子网络的激活模式 — 来源 (CC-BY)

虽然 Mixtral-8x7B 是最优秀的开源大型语言模型(LLM)之一,但它也是一个庞大的模型,拥有 46.7 亿个参数。即使将模型量化到 4 位,它也无法完全加载到消费级 GPU 中(例如,具有 24GB VRAM 的 RTX 3090 也不足够)。

Mixtral-8x7B 是一个专家混合模型(MoE),由 8 个每个拥有 60 亿参数的专家子网络组成。

由于在解码过程中只有 8 个专家中的 2 个是有效的,剩余的 6 个专家可以被移动或卸载到另一个设备,例如 CPU RAM,以释放一些 GPU VRAM。在实际操作中,这种卸载是复杂的。

选择激活哪个专家是一个在推理时针对每个输入标记和每一层模型所做的决定。像Accelerate 的 device_map那样简单地将部分模型转移到 CPU RAM,会在 CPU 和 GPU 之间产生通信瓶颈。

Mixtral-offloading(MIT 许可)是一个提出更高效解决方案的项目,旨在减少 VRAM 的消耗,同时保持合理的推理速度。

在单个 GPU 上运行 SOTA 7B 参数嵌入模型

原文:towardsdatascience.com/running-a-sota-7b-parameter-embedding-model-on-a-single-gpu-bb9b071e2238?source=collection_archive---------5-----------------------#2024-08-09

在 SageMaker 上运行 Qwen2

Szymon PaluchaTowards Data Science Szymon Palucha

·发布于 Towards Data Science ·16 分钟阅读·2024 年 8 月 9 日

--

在这篇文章中,我将解释如何在单个 24GB GPU 上运行一个最先进的 7B 参数 LLM 基于的嵌入模型。我将首先讲解一些理论,然后展示如何用 HuggingFace Transformers 库在 Python 中仅用几行代码运行它!

我们将在Qwen2开源模型(Alibaba-NLP/gte-Qwen2–7B-instruct)上运行该模型,该模型于 2024 年 6 月发布,截止本文完成时,它在 HuggingFace 上的Massive Text Embeddings Benchmark中排名第四。

2024 年 7 月在 HuggingFace 上的MTEB 排行榜截图。该模型在 2024 年 6 月排名第一,随后跌至第四名。这显示了当今 AI 发展速度的快速!

理论内存需求

加载模型

加载机器学习模型(例如 LLM 或嵌入模型)所需的内存可以通过其参数数量来计算。

例如,一个 7B 参数模型的 fp32(浮点 32 精度),意味着我们需要以 32 位精度存储 7B 个数字,以便在内存中初始化该模型并能够使用它。因此,回忆一下每个字节有 8 位,加载该模型所需的内存是

一个 7B 参数模型的 fp32 内存 = 7B * 32 位 = 7B * 32 / 8 字节 = 28B 字节 = 28GB

因此,为了运行这个模型,我们至少需要 28GB 的 GPU 内存。实际上,还有一些额外的开销,如这篇文章中所描述的那样。因此,要以全精度运行该模型,我们不能使用内存为 16GB 或 24GB 的小型且便宜的 GPU。

所以没有更强大的 GPU,如NVIDIA 的 A100,我们有哪些替代方案?事实证明,有几种不同的技术可以减少内存需求。最简单的一种方法是降低参数的精度。现在,大多数模型都可以在半精度下使用,而不会显著损失准确性。以 fp16 或bf16加载模型所需的内存是

一个 7B 参数模型在 fp16 中的内存 = 7B * 16 位 = 7B * 16 / 8 字节 = 14B 字节 = 14GB

虽然这对于在 24GB GPU 上加载模型已经足够好,但由于推理过程中额外的开销和要求,我们仍然难以在 16GB 的 GPU 上运行它。

如果继续降低精度,性能可能会受到影响,但有一种名为量化的技术,能够进一步减少精度(例如,降到 8 位或 4 位),而不会显著降低准确性。最近在 LLM 领域的研究甚至表明,使用 1 位精度(实际上是 log_2(3) = 1.58 位),即1 位 LLM的可能性。这些模型的参数只能取值 1、-1 或 0!

要深入了解这些话题,我推荐阅读:

推理

上述计算仅告诉我们加载模型所需的内存量!除此之外,我们还需要额外的内存来实际通过模型运行一些输入。对于 Qwen2 嵌入模型和一般的 LLM,额外的内存需求取决于上下文窗口的大小(即传入模型的文本长度)。

采用原始自注意力机制的旧模型

在现在广泛采用的Flash Attention发布之前,许多较旧的语言模型使用的是 Transformer 架构中的原始自注意力机制。该机制需要额外的内存,并且该内存需求与输入序列的长度呈平方关系。为了说明为什么会这样,下面是一个很好的自注意力可视化示意图。

一个很好的自注意力机制的可视化示例。来源:Sebastian Raschka,magazine.sebastianraschka.com/p/understanding-and-coding-self-attention,经作者许可转载。

从图示中我们可以看到,除了在计算加载模型所需内存时已经考虑的模型权重(Wq, Wk, Wv 矩阵)外,还有许多额外的计算及其输出需要存储。例如,输入 X、Q, K, V 矩阵,以及 注意力矩阵 QK^T。事实证明,随着输入序列长度 n 的增加,注意力矩阵成为额外内存需求中的主导因素。

为了理解这一点,我们可以进行一些简单的计算。例如,在原始的 Transformer 中,嵌入维度 d 为 512。因此,对于一个由 512 个词元组成的输入序列,输入 X 和注意力矩阵在 fp32 格式下各自需要额外的 1MB 内存。

512² 浮点数 = 512² * 32 位 = 512² * 4 字节 = 1MB

如果我们将输入序列长度增加到 8000 词元,额外的内存需求将是:

输入 X = 512 * 8000 * 4 字节 = 16MB;注意力矩阵 = 8000² * 4 字节 = 256MB。

如果我们将输入序列长度增加到 32k 词元,额外的内存需求将是:

输入 X = 512 * 32000 * 4 字节 = 65MB;注意力矩阵 = 32000² * 4 字节 = 4GB!

如你所见,随着上下文窗口的大小增加,所需的额外内存快速增长,并且很快就会被注意力矩阵中的 个浮点数所主导!

上述计算仍然是简化版,因为实际上有更多的细节会导致更高的内存使用。例如,在原始的 Transformer 中,还涉及到多头注意力——即注意力计算是并行进行的,使用了多个不同的头(在原始实现中为 8 个)。因此,我们需要将所需内存乘以头的数量。类似地,上述计算是针对批量大小为 1 的情况,如果我们想一次处理多个不同的文本,可以增加批量大小,但这会增加额外的内存开销。有关不同内存需求的详细拆解,请参见以下文章

更近期的模型,如 Qwen2

自 2017 年 Transformer 发布以来,已经有大量研究提出替代的注意力机制,以避免 n²瓶颈。然而,这些方法通常伴随着精度下降的折衷。2022 年,一种精确的注意力机制发布,并针对 GPU 进行了优化,名为Flash Attention,并且已在 LLMs 中广泛应用。从那时起,已经出现了进一步的迭代版本,包括 2024 年 7 月发布的Flash Attention 3。对我们来说,最重要的收获是 Flash Attention 能够随着输入序列长度的增加线性扩展!

以下是一个理论推导,比较了不同序列长度和不同注意力机制下,20B 参数模型的内存需求。Padding-Free Transformer是另一种优化方法,去除了填充的需求——如果你的批次中有一长序列和多个短序列,这会非常有用。

不同注意力机制下 20B 参数模型的内存需求的理论估算。主要结论是二次与线性扩展。来源:Mayank Mishra,在微调过程中使用 Padding-Free Transformer 层节省内存,已获作者许可转载。

Qwen2 模型同时使用了 Flash Attention 和填充优化。现在,我们已经了解了理论,接下来让我们看看如何实际运行 Qwen2 模型!

使用 HuggingFace Transformers 运行 7B Qwen2 模型

设置

我们将要实验的模型是来自 Transformers 的Alibaba-NLP/gte-Qwen2-7B-instruct。模型卡可以在这里查看。

为了进行这个实验,我使用了 Python 3.10.8 并安装了以下包:

torch==2.3.0
transformers==4.41.2
xformers==0.0.26.post1
flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.5.8/flash_attn-2.5.8+cu122torch2.3cxx11abiFALSE-cp310-cp310-linux_x86_64.whl
accelerate==0.31.0

在安装运行此模型所需的flash-attn时,我遇到了一些困难,因此必须安装上述特定版本。如果有人有更好的解决方法,请告诉我!

我在这个实验中使用的 Amazon SageMaker 实例是ml.g5.2xlarge。它配备有 24GB 的 NVIDIA A10G GPU 和 32GB 的 CPU 内存,费用为$1.69/小时。以下是 AWS 的截图,展示了实例的所有详细信息。

来自AWS 文档的 SageMaker g5 实例类型。

实际上,如果你运行nvidia-smi,你会看到该实例的 GPU 内存只有 23GB,略低于广告所称的。此 GPU 的 CUDA 版本是 12.2。

如何运行——详细步骤

如果你查看模型卡,其中一个建议的使用方式是通过sentence-transformers库,如下所示。

from sentence_transformers import SentenceTransformer

# This will not run on our 24GB GPU!
model = SentenceTransformer("Alibaba-NLP/gte-Qwen2-7B-instruct", trust_remote_code=True)
embeddings = model.encode(list_of_examples)

Sentence-transformers 是 Transformers 包的一个扩展,用于计算嵌入,非常有用,因为你只需要两行代码就可以让事情运行起来。缺点是你对如何加载模型的控制较少,因为它隐藏了标记化和池化的细节。上述代码无法在我们的 GPU 实例上运行,因为它尝试以完整的 float32 精度加载模型,这需要 28GB 的内存。当初始化句子变换器模型时,它会检查可用设备(GPU 的 cuda)并自动将 Pytorch 模型转移到设备上。结果,在加载了 5/7 的模型后,它卡住并崩溃了。

相反,我们需要在将模型移到 GPU 之前,能够以 float16 精度加载模型。因此,我们需要使用更底层的 Transformers 库。(我不确定是否有方法可以使用 sentence-transformers 实现,但如果有的话请告诉我!)我们按如下方式操作:

import transformers
import torch

model_path = "Alibaba-NLP/gte-Qwen2-7B-instruct"
model = transformers.AutoModel.from_pretrained(model_path, trust_remote_code=True, torch_dtype=torch.float16).to("cuda")

使用torch_dtype参数,我们指定模型应立即以 float16 精度加载,从而仅需要 14GB 的内存。然后,我们需要将模型转移到 GPU 设备上,这可以通过to方法实现。使用上述代码,模型加载几乎需要 2 分钟!

由于我们使用的是transformers,我们需要单独加载标记化器来标记化输入文本,操作如下:

tokenizer = transformers.AutoTokenizer.from_pretrained(model_path)

下一步是标记化输入文本,操作如下:

texts = ["example text 1", "example text 2 of different length"]
max_length = 32768
batch_dict = tokenizer(texts, max_length=max_length, padding=True, truncation=True, return_tensors="pt").to(DEVICE)

Qwen2 模型的最大长度为 32678,但正如我们稍后将看到的,由于额外的内存需求,我们无法在 24GB 的 GPU 上运行如此长的序列。我建议将其减少到不超过 24,000,以避免内存不足错误。填充确保批次中的所有输入具有相同的长度,而截断确保任何超过最大长度的输入将被截断。有关更多信息,请参见文档。最后,我们确保返回的是 PyTorch 张量(默认情况下是列表),并将这些张量转移到 GPU,以便可以传递给模型。

下一步是将输入通过我们的模型并进行池化。操作如下:

with torch.no_grad():
    outputs = model(**batch_dict)
    embeddings = last_token_pool(outputs.last_hidden_state, batch_dict["attention_mask"])

使用last_token_pool,其形式如下:

def last_token_pool(last_hidden_states: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
    # checks whether there is any padding (where attention mask = 0 for a given text)
    no_padding = attention_mask[:, -1].sum() == attention_mask.shape[0]
    # if no padding - only would happen if batch size of 1 or all sequnces have the same length, then take the last tokens as the embeddings
    if no_padding:
        return last_hidden_states[:, -1]
    # otherwise use the last non padding token for each text in the batch
    sequence_lengths = attention_mask.sum(dim=1) - 1
    batch_size = last_hidden_states.shape[0]
    return last_hidden_states[torch.arange(batch_size, device=last_hidden_states.device), sequence_lengthsLet’s break down what happened in the above code snippets! 
  • torch.no_grad()上下文管理器用于禁用梯度计算,因为我们并不训练模型,因此可以加速推理过程。

  • 然后,我们将标记化的输入传递到变换器模型中。

  • 我们通过last_hidden_state属性从模型的最后一层获取输出。这是一个形状为(batch_size, max_sequence_length, embedding dimension)的张量。本质上,对于批次中的每个示例,变换器都会为序列中的所有标记输出嵌入。

  • 我们现在需要一种方法将所有的标记嵌入合并为一个单一的嵌入,以表示输入文本。这称为池化,并且与训练模型时的池化方式相同。

  • 在较旧的基于 BERT 的模型中,通常使用第一个标记(它代表特殊的分类[CLS]标记)。然而,Qwen2 模型是基于 LLM 的,即基于变换器解码器。在解码器中,标记是自回归生成的(一个接一个),因此最后一个标记包含了关于句子的所有编码信息。

  • last_token_pool函数的目的是选择每个示例中最后一个生成的非填充标记的嵌入。

  • 它使用了attention_mask,该掩码告诉模型在批次中每个示例哪些标记是填充标记(参见文档)。

注释示例

让我们看一个例子,稍微详细地了解一下。假设我们想在一个批次中嵌入两个示例:

texts = ["example text 1", "example text 2 of different length"]

分词器的输出(batch_dict)将如下所示:

>>> batch_dict
{'input_ids': tensor([[  8687,   1467,    220,     16, 151643, 151643, 151643],
        [  8687,   1467,    220,     17,    315,   2155,   3084]],
       device='cuda:0'), 'attention_mask': tensor([[1, 1, 1, 1, 0, 0, 0],
        [1, 1, 1, 1, 1, 1, 1]], device='cuda:0')}

从中可以看出,第一句话被分成了四个标记(8687,1467,220,16),而第二句话被分成了七个标记。因此,第一句话会被填充(使用三个填充标记,id 为 151643),填充到长度七 —— 这是该批次中的最大长度。注意力掩码反映了这一点 —— 它对第一个示例有三个零,表示填充标记的位置。两个张量的大小是相同的。

>>> batch_dict.input_ids.shape
torch.Size([2, 7])
>>> batch_dict.attention_mask.shape
torch.Size([2, 7])

现在通过模型传递batch_dict,我们可以检索到模型的最后一个隐藏状态,其形状为:

>>> outputs.last_hidden_state.shape
torch.Size([2, 7, 3584])

我们可以看到,这个形状是(batch_size, max_sequence_length, embedding dimension)。Qwen2 的嵌入维度为 3584!

现在我们进入last_token_pool函数。第一行检查是否存在填充,它通过对attention_mask的最后一列求和并与batch_size(由attention_mask.shape[0]给出)进行比较来实现。如果所有的注意力掩码中都存在 1,即所有示例的长度相同,或者仅有一个示例时,这才会返回True

>>> attention_mask.shape[0]
2
>>> attention_mask[:, -1]
tensor([0, 1], device='cuda:0')

如果确实没有填充,我们只需简单地为每个示例选择最后一个标记的嵌入,方法是使用last_hidden_states[:, -1]。但是,由于有填充,我们需要选择每个批次中最后一个非填充标记的嵌入。为了选择这个嵌入,我们需要获取每个示例的索引。通过以下方式可以实现:

>>> sequence_lengths = attention_mask.sum(dim=1) - 1
>>> sequence_lengths
tensor([3, 6], device='cuda:0')

所以现在我们只需要通过张量的正确索引,在前两个维度上进行索引。为了获取批次中所有示例的索引,我们可以使用torch.arange,如下所示:

>>> torch.arange(batch_size, device=last_hidden_states.device)
tensor([0, 1], device='cuda:0')

然后我们可以使用这个和最后一个非填充标记的索引,从中提取每个示例的正确标记嵌入:

>>> embeddings = last_hidden_states[torch.arange(batch_size, device=last_hidden_states.device), sequence_lengths]
>>> embeddings.shape
torch.Size([2, 3584])

然后我们得到了传入的两个示例的两个嵌入!

如何运行 — 简洁版

将完整的代码分成不同函数看起来像是:

import numpy as np
import numpy.typing as npt
import torch
import transformers

DEVICE = torch.device("cuda")

def last_token_pool(last_hidden_states: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
    # checks whether there is any padding (where attention mask = 0 for a given text)
    no_padding = attention_mask[:, -1].sum() == attention_mask.shape[0]
    # if no padding - only would happen if batch size of 1 or all sequnces have the same length, then take the last tokens as the embeddings
    if no_padding:
        return last_hidden_states[:, -1]
    # otherwise use the last non padding token for each text in the batch
    sequence_lengths = attention_mask.sum(dim=1) - 1
    batch_size = last_hidden_states.shape[0]
    return last_hidden_states[torch.arange(batch_size, device=last_hidden_states.device), sequence_lengths

def encode_with_qwen_model(
    model: transformers.PreTrainedModel,
    tokenizer: transformers.tokenization_utils.PreTrainedTokenizer | transformers.tokenization_utils_fast.PreTrainedTokenizerFast,
    texts: list[str],
    max_length: int = 32768,
) -> npt.NDArray[np.float16]:
    batch_dict = tokenizer(texts, max_length=max_length, padding=True, truncation=True, return_tensors="pt").to(DEVICE)

    with torch.no_grad():
        outputs = model(**batch_dict)
        embeddings = last_token_pool(outputs.last_hidden_state, batch_dict["attention_mask"])
    return embeddings.cpu().numpy()

def main() -> None:
    model_path = "Alibaba-NLP/gte-Qwen2-7B-instruct"
    tokenizer = transformers.AutoTokenizer.from_pretrained(model_path)
    model = transformers.AutoModel.from_pretrained(model_path, trust_remote_code=True, torch_dtype=torch.float16).to(DEVICE)
    print("Loaded tokeniser and model")

    texts_to_encode = ["example text 1", "example text 2 of different length"]
    embeddings = encode_with_qwen_model(model, tokenizer, texts_to_encode)
    print(embeddings.shape)

if __name__ == "__main__":
    main()

encode_with_qwen_model 返回一个 numpy 数组。为了将 PyTorch 张量转换为 numpy 数组,我们首先必须将其从 GPU 移回到 CPU,这可以通过 cpu() 方法实现。请注意,如果你打算处理较长的文本,应该将批处理大小减少为 1,每次只嵌入一个示例(即将 texts_to_encode 列表的长度缩减为 1)。

使用上下文长度进行的经验性内存使用测试

之前我们从理论角度看到了内存使用如何随输入文本大小变化。我们还可以实际测量 GPU 在嵌入不同长度的文本时实际使用了多少内存,并通过经验验证这种扩展性!这个想法来源于 HuggingFace 的一个很棒教程:充分利用 LLM

为了实现这一点,我们将利用一些额外的函数

import gc

def flush() -> None:
    gc.collect()
    torch.cuda.empty_cache()
    torch.cuda.reset_peak_memory_stats()

def bytes_to_giga_bytes(bytes_: float) -> float:
    return bytes_ / 1024 / 1024 / 1024

以及

torch.cuda.max_memory_allocated()

用于衡量 GPU 峰值使用情况的函数。flush 函数将在每次通过模型后清除并重置内存。我们将运行不同长度的文本,并输出峰值和有效 GPU 使用情况。有效 GPU 使用是从总使用量中减去模型大小使用量,给我们一个关于运行文本所需额外内存的概念。

我使用的完整代码如下:

import gc

import numpy as np
import numpy.typing as npt
import torch
import transformers

DEVICE = torch.device("cuda")

def last_token_pool(last_hidden_states: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
    # checks whether there is any padding (where attention mask = 0 for a given text)
    left_padding = attention_mask[:, -1].sum() == attention_mask.shape[0]
    # if no padding - only would happen if batch size of 1 or all sequences have the same length, then take the last tokens as the embeddings
    if left_padding:
        return last_hidden_states[:, -1]
    # otherwise use the last non padding token for each text in the batch
    sequence_lengths = attention_mask.sum(dim=1) - 1
    batch_size = last_hidden_states.shape[0]
    return last_hidden_states[torch.arange(batch_size, device=last_hidden_states.device), sequence_lengths]

def encode_with_qwen_model(
    model: transformers.PreTrainedModel,
    tokenizer: transformers.tokenization_utils.PreTrainedTokenizer | transformers.tokenization_utils_fast.PreTrainedTokenizerFast,
    texts: list[str] | str,
    max_length: int = 32768,
) -> npt.NDArray[np.float16]:
    batch_dict = tokenizer(texts, max_length=max_length, padding=True, truncation=True, return_tensors="pt").to(DEVICE)

    with torch.no_grad():
        outputs = model(**batch_dict)
        embeddings = last_token_pool(outputs.last_hidden_state, batch_dict["attention_mask"])
    return embeddings.cpu().numpy()

def flush() -> None:
    gc.collect()
    torch.cuda.empty_cache()
    torch.cuda.reset_peak_memory_stats()

def bytes_to_giga_bytes(bytes_: float) -> float:
    return bytes_ / 1024 / 1024 / 1024

def memory_usage_experiments(
    model: transformers.PreTrainedModel,
    tokenizer: transformers.tokenization_utils.PreTrainedTokenizer | transformers.tokenization_utils_fast.PreTrainedTokenizerFast,
) -> None:
    model_size = bytes_to_giga_bytes(torch.cuda.max_memory_allocated())
    print(f"Most gpu usage on model loaded: {model_size} GB\n")
    sentence = "This sentence should have minimum eight tokens. "
    all_texts = [sentence, sentence * 100, sentence * 1000, sentence * 2000, sentence * 3000, sentence * 4000]
    for texts in all_texts:
        batch_dict = tokenizer(texts, max_length=32768, padding=True, truncation=True, return_tensors="pt")
        encode_with_qwen_model(model, tokenizer, texts)
        max_mem = bytes_to_giga_bytes(torch.cuda.max_memory_allocated())
        print(f"Sequence length: {batch_dict.input_ids.shape[-1]}. Most gpu usage: {max_mem} GB. Effective usage: {max_mem - model_size} GB\n")
        flush()

def main() -> None:
    model_path = "Alibaba-NLP/gte-Qwen2-7B-instruct"
    tokenizer = transformers.AutoTokenizer.from_pretrained(model_path)
    model = transformers.AutoModel.from_pretrained(model_path, trust_remote_code=True, torch_dtype=torch.float16).to(DEVICE)
    print("Loaded tokeniser and model")

    memory_usage_experiments(model, tokenizer)

if __name__ == "__main__":
    main()

以及回溯信息是

Most gpu usage on model loaded: 14.958292961120605 GB

Sequence length: 9\. Most gpu usage: 14.967926502227783 GB. Effective usage: 0.009633541107177734 GB

Sequence length: 801\. Most gpu usage: 15.11520528793335 GB. Effective usage: 0.15691232681274414 GB

Sequence length: 8001\. Most gpu usage: 16.45930576324463 GB. Effective usage: 1.5010128021240234 GB

Sequence length: 16001\. Most gpu usage: 17.944651126861572 GB. Effective usage: 2.986358165740967 GB

Sequence length: 24001\. Most gpu usage: 19.432421684265137 GB. Effective usage: 4.474128723144531 GB

torch.cuda.OutOfMemoryError: CUDA out of memory. Tried to allocate 1.13 GiB. GPU 

从这个结果我们可以看到,Qwen2 模型的确随着输入文本的大小线性扩展。例如,当我们将标记数从 8000 翻倍到 16000 时,有效内存使用量也大致翻倍。不幸的是,尝试运行长度为 32000 的序列时,出现了 CUDA OOM 错误,因此即使使用 24GB GPU 的浮点 16 精度,我们仍然无法充分利用模型的完整上下文窗口。

其他方面

在 fp32 精度下运行 Qwen2

要以全精度运行 Qwen2 模型,我们有两个选项。首先,我们可以访问更大的 GPU——例如 40GB 应该足够。然而,这可能会很昂贵。例如,Amazon SageMaker 就没有单个 40GB GPU 的实例,而是有一个包含 8 个 40GB GPU 的实例!但这并不实用,因为我们不需要其他 7 个 GPU 空闲等待。当然,我们也可以考虑其他提供商——现在有不少这样的提供商,且价格具有竞争力。

另一种选择是在具有多个较小 GPU 的实例上运行模型。模型可以被分割到不同的 GPU 上——即模型的不同层被放置在不同的 GPU 上,并且数据在推理过程中会在设备之间移动。要使用 HuggingFace 实现这一点,你可以使用

model_path = "Alibaba-NLP/gte-Qwen2-7B-instruct"
model = transformers.AutoModel.from_pretrained(model_path, trust_remote_code=True, device_map="auto").to("cuda")

欲了解更多关于此如何工作的内容,请参阅以下文档概念指南。需要注意的是,这种方式非常慢——因为在不同 GPU 之间进行通信的开销,数据需要在各个设备之间移动以执行推理。此实现也没有经过优化,这意味着每个 GPU 的执行是按顺序进行的,而其他 GPU 处于空闲状态。如果你正在嵌入成千上万的文本或训练模型,理想情况下,你希望所有 GPU 都在不断工作。

在较小的 GPU 上运行 Qwen2

为了在更小的 GPU 上运行此模型,你需要对模型进行量化。两个流行的选项是:

  • 通过 HuggingFace,它提供了多种方法可供选择(请参阅文档)。

  • 通过vLLM包。

结论

总结:在本文中,我们展示了如何在单个 24GB GPU 上运行基于 7B LLM 的 Qwen2 嵌入模型。我们了解到,模型的大小是根据其参数数量计算的,并且为了将其加载到 24GB GPU 中,我们需要以 float16 精度加载模型。接着,我们发现实际上运行一个示例时需要额外的内存,这取决于上下文窗口的大小,并且根据所使用的底层注意力机制不同而有所变化。最后,我们展示了如何仅用几行代码,利用 Transformers 库完成这一切。

使用计算机视觉评估跑步效率:与埃利乌德·基普乔格的对比分析

原文:towardsdatascience.com/running-efficiency-with-computer-vision-a-comparative-analysis-with-eliud-kipchoge-736eb80c574f?source=collection_archive---------3-----------------------#2024-03-28

如何通过计算机视觉提高你的跑步效率?

Dr. Christoph MittendorfTowards Data Science Dr. Christoph Mittendorf

·发表于Towards Data Science ·14 分钟阅读·2024 年 3 月 28 日

--

图片来自 Unsplash / Nicolas Hoizey

简短总结:我进行了一个实验——通过不同类型的鞋子来实现埃利乌德·基普乔格的跑步效率。令人惊讶的是——事实证明,人字拖不适合跑步,而碳纤维跑鞋简直太棒了!

作者图片:穿人字拖跑步

摘要

跑步效率——即以最小的能量消耗覆盖指定的距离——是运动表现中的一个关键因素。

传统的跑步效率评估方法主要依赖于主观评估或侵入性的生理测量——因此,往往限制了其适用性和客观性。本实验引入了一种使用人工智能评估跑步效率的现代方法。换句话说,我正在使用计算机视觉(CV)技术来评估跑步效率。我采用了 TensorFlow 中的 MoveNet 模型,从埃利乌德·基普乔格等专业跑者的视频中提取了 17 个关键点。埃利乌德是马拉松跑步的伟大运动员(GOAT),以其卓越的效率闻名。通过分析这些关键点,可以得出…

私人运行大型语言模型

原文:towardsdatascience.com/running-large-language-models-privately-a-comparison-of-frameworks-models-and-costs-ac33cfe3a462?source=collection_archive---------0-----------------------#2024-10-30

框架、模型与成本比较

Robert CorwinTowards Data Science Robert Corwin

·发布于Towards Data Science ·15 分钟阅读·2024 年 10 月 30 日

--

Robert Corwin,首席执行官,奥斯汀人工智能公司

David Davalos,机器学习工程师,奥斯汀人工智能公司

2024 年 10 月 24 日

大型语言模型(LLM)迅速改变了技术格局,但安全问题依然存在,尤其是将私人数据发送给外部第三方。在这篇博客文章中,我们深入探讨了将 Llama 模型本地和私人部署的选项,也就是说,在个人计算机上运行。我们成功在本地运行了 Llama 3.1,并调查了不同版本和框架下的速度、功耗以及整体性能等关键方面。无论你是技术专家,还是仅仅对相关内容感到好奇,你都会从本地 LLM 部署中获得一些见解。对于快速概览,不懂技术的读者可以跳过详细内容,查看总结表格,而具备技术背景的读者可能会更喜欢深入了解具体工具及其性能。

除非另有说明,否则所有图片均由作者提供。作者和奥斯汀人工智能公司(其雇主)与本文提到的任何工具或使用的工具没有任何关系。

关键点

运行 LLM: LLM 模型可以通过社区中广泛使用的工具和框架下载并在私人服务器上运行。虽然运行最强大的模型需要相当昂贵的硬件,但较小的模型可以在笔记本电脑或台式计算机上运行。

隐私与可定制性: 在私人服务器上运行 LLM 可以提供更高的隐私保护,并对模型设置和使用政策拥有更大的控制权。

模型大小: 开源 Llama 模型有多种尺寸。例如,Llama 3.1 提供 8 亿、70 亿和 405 亿参数版本。 “参数”大致定义为网络中某个节点上的权重。更多的参数会增加模型性能,但也会增加内存和磁盘的占用。

量化: 量化通过将权重“舍入”到更少的有效数字,从而节省内存和磁盘空间——这以牺牲精度为代价。鉴于 LLM 中参数的庞大数量,量化对于减少内存使用和加速执行非常有价值。

成本: 本地实现,通过参考 GPU 能耗,展示了与基于云的解决方案相比的成本效益。

隐私和可靠性作为动机

在我们之前的文章中,我们探讨了 LLM 背后的关键概念,以及如何利用Langchain等框架创建定制化的聊天机器人或工具(见图 1)。在这样的方案中,虽然可以通过使用合成数据或混淆来保护数据,但我们仍然需要将数据发送给第三方,而且无法控制模型的任何变化、政策或甚至可用性。一个解决方案是直接在私有服务器上运行 LLM(见图 2)。这种方式可以确保完全的隐私,并减少对外部服务提供商的依赖。

实现私有化 LLM 时的关注点包括成本、电力消耗和速度。在本次实验中,我们通过改变 1.框架(工具)和 2.量化程度,运行 LLama 3.1,并比较框架的易用性、运行时的速度表现以及电力消耗。理解这些权衡对任何希望在保持数据和资源控制的同时,充分发挥 AI 潜力的人来说都至关重要。

图 1 展示一个典型的后端设置示意图,用于聊天机器人或工具,其中 ChatGPT(或类似模型)作为自然语言处理引擎运行。此设置依赖于提示工程来自定义响应。

图 2 完全私有的后端配置示意图,所有组件,包括大型语言模型,均托管在安全服务器上,从而确保完全的控制和隐私。

量化与 GGUF 文件

在深入探讨我们所探索的工具之前,首先让我们讨论一下量化和GGUF格式。

量化是一种通过将权重和偏差从高精度浮动点值转换为低精度表示来减少模型大小的技术。考虑到 LLM 拥有庞大的参数量,这种方法对它们特别有利。例如,Llama 3.1 的最大版本包含惊人的 4050 亿个参数。量化可以显著减少内存使用和执行时间,使这些模型在各种设备上运行时更高效。有关量化类型的深入解释和命名法,请查看这个很好的介绍。概念性概述也可以在这里找到。

GGUF格式用于存储 LLM 模型,并且最近因分发和运行量化模型而受到广泛欢迎。它经过优化,能够快速加载、读取和保存。与仅存储张量的格式不同,GGUF 还以标准化方式存储模型元数据,使得框架更容易支持此格式,甚至将其作为标准格式采用。

分析的工具和模型

我们探索了四个工具来本地运行 Llama 模型:

我们的主要关注点是 llama.cpp 和 Ollama,因为这些工具允许我们直接、迅速且高效地部署模型。具体来说,我们探索了它们的速度、能源成本和整体性能。对于模型,我们主要分析了量化后的 8B 和 70B 版本的 Llama 3.1,因为它们在合理的时间范围内运行。

初步印象与安装

HuggingFace

HuggingFace 的 transformers 库和 Hub 在社区中广为人知并被广泛使用。它们提供了多种模型和工具,使其成为许多开发者的热门选择。一旦环境配置好并安装了 Python,它的安装通常不会导致大问题。最终,HuggingFace 的最大优势在于它的在线 Hub,允许轻松访问来自许多不同提供者的量化模型。另一方面,直接使用 transformers 库加载模型,尤其是量化模型,还是相当棘手的。默认情况下,该库似乎会直接对模型进行反量化,占用了大量的 RAM,导致在本地服务器上运行时变得不可行。

尽管 Hugging Face 支持 4 位和 8 位量化与去量化,并且使用 bitsandbytes,我们的初步印象是,仍然需要进一步优化。高效推理可能并不是它的主要重点。尽管如此,Hugging Face 提供了出色的文档、庞大的社区以及强大的模型训练框架。

vLLM

类似于 Hugging Face,vLLM 可以在正确配置的 Python 环境中轻松安装。然而,对 GGUF 文件的支持仍然处于高度实验阶段。尽管我们能够快速设置运行 8B 模型,但超出这个规模的扩展却证明是具有挑战性的,尽管有着出色的文档支持。

总体而言,我们认为 vLLM 具有很大的潜力。然而,我们最终选择了 llama.cpp 和 Ollama 框架,因为它们在兼容性和效率上更为直接。公平地说,这里本可以进行更深入的调查,但考虑到我们在其他库中取得的即时成功,我们选择专注于它们。

Ollama

我们认为 Ollama 非常棒。我们的初步印象是,它是一个适合用户本地推理 Llama 模型的工具,具有即插即用的易用性。为 Mac 和 Linux 用户安装它非常简单,Windows 版本目前处于预览阶段。Ollama 会自动检测硬件,并在 CPU 和 GPU 之间无缝管理模型卸载。它还具备自己的模型库,自动下载模型并支持 GGUF 文件。尽管其速度略逊于 llama.cpp,但即便在仅配有 CPU 的系统和笔记本电脑上也能良好运行。

快速入门:安装后,运行 ollama run llama3.1:latest 将直接从命令行加载最新的 8B 模型进行对话模式。

一个缺点是,定制模型在某些情况下可能有些不切实际,尤其是在高级开发中。例如,即使是调整温度,也需要创建一个新的聊天机器人实例,而这个实例又需要加载一个已安装的模型。虽然这只是一个小小的不便,但它确实有助于在一个文件中设置定制化的聊天机器人 —— 包括其他参数和角色。总体而言,我们认为 Ollama 是一个有效的本地工具,模仿了云服务的一些关键功能。

值得注意的是,Ollama 作为服务运行,至少在 Linux 系统上,它提供了方便且简单的命令来监控当前运行的模型以及它们被卸载到何处,并且可以在需要时立即停止这些模型。社区面临的一个挑战是配置某些方面,例如模型存储位置,这需要具备一定的 Linux 系统技术知识。虽然这对最终用户可能不会构成问题,但它可能会稍微影响该工具在高级开发中的实用性。

llama.cpp

在这次分析中,llama.cpp 成为了我们最喜爱的工具。正如它的 仓库 中所述,它旨在通过最小的配置运行大规模语言模型,并提供领先的性能。像 Ollama 一样,它支持在 CPU 和 GPU 之间卸载模型,尽管这不是开箱即用的功能。要启用 GPU 支持,您必须使用适当的标志进行编译——具体来说是 GGML_CUDA=on。我们建议使用最新版本的 CUDA 工具包,因为旧版本可能不兼容。

该工具可以通过从仓库拉取并编译作为独立工具安装,这为运行模型提供了一个方便的命令行客户端。例如,您可以执行 llama-cli -p 'you are a useful assistant' -m Meta-Llama-3-8B-Instruct.Q8_0.gguf -cnv。这里,最后一个标志可以直接从命令行启用对话模式。llama-cli 提供了各种定制选项,例如调整上下文大小、重复惩罚和温度,它还支持 GPU 卸载选项。

类似于 Ollama,llama.cpp 也提供了 Python 绑定,可以通过 pip install llama-cpp-python 安装。这个 Python 库允许进行显著的定制,使得开发者可以轻松地根据特定客户需求调整模型。然而,正如独立版本一样,Python 绑定也需要使用适当的标志进行编译,以启用 GPU 支持。

一个小缺点是该工具尚不支持自动的 CPU-GPU 卸载。相反,用户需要手动指定将多少层卸载到 GPU 上,剩余的部分由 CPU 处理。虽然这需要一些微调,但这一步骤直接且易于管理。

对于拥有多个 GPU 的环境(如我们所使用的),llama.cpp 提供了两种分割模式:行模式层模式。在行模式下,一个 GPU 处理小型张量和中间结果,而在层模式下,层被划分到多个 GPU 上。在我们的测试中,这两种模式提供了相似的性能(请参见下面的分析)。

我们的分析

从现在起,结果仅涉及 llama.cpp 和 Ollama。

我们使用 Ollama 和 llama.cpp 对 70B 和 8B Llama 3.1 模型的速度和功耗进行了分析。具体来说,我们研究了在 Quant Factory 中提供的各种量化方式下,每个模型每个 token 的速度和功耗。

为了进行此分析,我们开发了一个小型应用程序,在选择工具后评估模型。在推理过程中,我们记录了诸如速度(每秒令牌数)、生成的总令牌数、温度、GPU 上加载的层数以及响应的质量评分等指标。此外,我们还测量了模型执行期间 GPU 的功耗。使用了一个脚本,通过nvidia-smi在每生成一个令牌后立即监测 GPU 的功耗。推理结束后,我们根据这些读数计算了平均功耗。由于我们专注于能够完全适应 GPU 内存的模型,因此只测量了 GPU 的功耗。

此外,为了确保不同输出大小的情况,实验使用了多种提示语,因此数据涵盖了各种场景。

硬件和软件配置

我们使用了一台相当不错的服务器,具备以下特点:

  • CPU: AMD Ryzen Threadripper PRO 7965WX 24 核心 @ 48x 5.362GHz。

  • GPU: 2x NVIDIA GeForce RTX 4090。

  • RAM: 515276MiB-

  • 操作系统:Pop 22.04 jammy。

  • 内核:x86_64 Linux 6.9.3–76060903-generic。

这套配置的零售成本大约为 15,000 美元。我们选择了这样的配置,因为它是一台不错的服务器,虽然无法与配备 8 个以上 GPU 的专用高端 AI 服务器相比,但它仍然非常实用,并且能够代表我们许多客户可能选择的配置。我们发现许多客户在一开始不愿意投资高端服务器,而这套配置在成本和性能之间达到了良好的折衷。

速度

首先让我们关注速度。下面,我们展示了几个箱形图,这些箱形图展示了不同量化方式下的速度数据。每个模型的名称以其量化级别开头;例如,“Q4”表示 4 位量化。再次强调,较低的量化级别会更多地进行舍入,减小模型的大小和质量,但提高速度。

技术问题 1(箱形图的提醒):箱形图展示了中位数、第一个和第三个四分位数,以及最小值和最大值。图中的胡须延伸到不被视为离群值的最极端数据点,而离群值会单独绘制。离群值被定义为落在 Q1 − 1.5 × IQR 和 Q3 + 1.5 × IQR 之外的数据点,其中 Q1 和 Q3 分别表示第一个和第三个四分位数。四分位距(IQR)计算公式为 IQR = Q3 − Q1。

llama.cpp

以下是针对 llama.cpp 的图表。图 3显示了在QuantFactory中可用的所有 70B 参数的 Llama 3.1 模型的结果,而图 4则展示了一些 8B 参数的模型,您可以在这里找到这些模型。70B 模型可以将最多 81 层卸载到 GPU 上,而 8B 模型最多可卸载 33 层。对于 70B,Q5 量化及更细的量化方式无法完全卸载所有层。每种量化类型后面括号内包含了卸载到 GPU 上的层数。如预期,粗量化类型提供了最佳的速度性能。由于行分割模式的表现相似,这里我们专注于层分割模式。

图 3 在 llama.cpp 中使用层分割模式运行的 Llama 3.1 模型,参数为 70B。如预期,粗量化提供了最佳的速度。卸载到 GPU 上的层数在每种量化类型旁边的括号中显示。使用 Q5 及更细量化的模型无法完全适配 VRAM。

图 4 在 llama.cpp 中使用层分割模式运行的 Llama 3.1 模型,参数为 8B。在这种情况下,所有量化类型下模型均能适应 GPU 内存,粗量化类型提供了最快的速度。请注意,高速事件为离群值,而总体趋势则在 Q2_K 的情况下约为每秒 20 个 token。

关键观察

  • 在推理过程中,我们观察到了一些高速事件(尤其是在 8B Q2_K 情况下),这时收集数据并理解其分布非常关键,因为这些事件实际上是相当罕见的。

  • 如预期,粗量化类型提供了最佳的速度性能。这是因为模型大小被缩减,从而实现了更快的执行速度。

  • 关于 70B 模型未能完全适配 VRAM 的结果必须谨慎解读,因为使用 CPU 可能会造成瓶颈。因此,在这些情况下报告的速度可能不是模型性能的最佳表现。

Ollama

我们对 Ollama 进行了相同的分析。图 5显示了 Ollama 自动下载的默认 Llama 3.1 和 3.2 模型的结果。除了 405B 模型外,所有模型都可以适配 GPU 内存。

图 5 在 Ollama 下运行的 Llama 3.1 和 3.2 模型。这些是使用 Ollama 时的默认模型。所有 3.1 模型——特别是 405B、70B 和 8B(标记为“latest”)——使用 Q4_0 量化,而 3.2 模型使用 Q8_0(1B)和 Q4_K_M(3B)量化。

关键观察

  • 我们可以比较 Ollama 和 llama.cpp 中的 70B Q4_0 模型,Ollama 的速度略慢。

  • 同样,8B Q4_0 模型在 Ollama 下的运行速度较其在 llama.cpp 中的对应模型慢,且差异更加明显——llama.cpp 平均每秒处理多出约五个 token。

已分析框架总结

► 在讨论功耗和租用性之前,让我们总结一下迄今为止分析的框架。

功耗与租用性

该分析对于将所有层加载到 GPU 内存中的模型尤为相关,因为我们只测量了两块 RTX 4090 显卡的功耗。然而,需要注意的是,测试中使用的 CPU 具有TDP 350 W,这提供了其在最大负载下的功耗估计。如果整个模型被加载到 GPU 上,CPU 的功耗可能接近空闲状态。

为了估算每个 token 的能耗,我们使用以下参数:每秒 token 数(NT)和两块 GPU 的功率消耗(P),单位为瓦特。通过计算 P/NT,我们得到每个 token 的能耗,单位为瓦秒。将其除以 3600,得到每个 token 的能耗,单位为 Wh,这通常是更常用的参考单位。

llama.cpp

以下是 llama.cpp 的结果。图 6展示了 70B 模型的能耗,而图 7则侧重于 8B 模型。这些图表展示了每种量化方式的能耗数据,图例中显示了平均值。

图 6 在 llama.cpp 下,针对 70B 参数的 Llama 3.1 模型的各种量化方式,每个 token 的能耗。展示了行分割和层分割模式。结果仅适用于将所有 81 层加载到 GPU 内存中的模型。

图 7 在 llama.cpp 下,针对 8B 参数的 Llama 3.1 模型的各种量化方式,每个 token 的能耗。展示了行分割和层分割模式。所有模型的平均能耗相似。

Ollama

我们还分析了 Ollama 的能耗。图 8展示了 Llama 3.1 8B(Q4_0 量化)和 Llama 3.2 1B 与 3B(分别为 Q8_0 和 Q4_K_M 量化)的结果。图 9展示了 70B 和 405B 模型的单独能耗,二者均采用 Q4_0 量化。

图 8 在 Ollama 下,Llama 3.1 8B(Q4_0 量化)和 Llama 3.2 1B 与 3B 模型(分别为 Q8_0 和 Q4_K_M 量化)的每个 token 能耗。

图 9 Llama 3.1 70B(左)和 Llama 3.1 405B(右)每个 token 的能耗,二者均在 Ollama 下使用 Q4_0 量化。

成本总结

我们不会逐个讨论每个模型,而是将重点放在 llama.cpp 和 Ollama 之间可比的模型,以及 llama.cpp 下使用 Q2_K 量化的模型,因为它是这里探索的最粗糙量化方式。为了更好地了解成本,我们在下表中展示了每百万生成 token(1M)的能耗估算和美元成本。该成本是根据德克萨斯州的平均电价计算的,电价为每千瓦时$0.14,具体参考此来源。作为参考,当前GPT-4o 的定价至少为每百万 token $5 USD,而 GPT-o mini 则为每百万 token $0.3 USD。

llama.cpp

Ollama

关键观察

  • 使用 Llama 3.1 70B 模型与 Q4_0 时,llama.cpp 与 Ollama 之间的能耗差异不大。

  • 对于 8B 模型,llama.cpp 的能耗高于 Ollama。

  • 请注意,这里所展示的成本可以看作是运行模型的“基本成本”的下限。其他成本,如操作、维护、设备成本和利润,并未包含在此分析中。

  • 估算结果表明,相较于云服务,在私人服务器上运行 LLM(大型语言模型)可能更具成本效益。特别是,Llama 8B 与 GPT-45o mini 以及 Llama 70B 与 GPT-4o 模型的比较,在合适的情况下似乎是一个潜在的好选择。

技术问题 2(成本估算): 对于大多数模型,每百万个 token 的能耗估算(及其变异性)通过“中位数 ± 四分位距(IQR)”的方式给出,其中 IQR 代表四分位距。只有对于 Llama 3.1 8B Q4_0 模型,我们采用“均值 ± 标准差(STD)”的方式,其中 STD 代表标准差。这些选择并非随意作出的;除了 Llama 3.1 8B Q4_0 模型外,所有模型都存在异常值,使得中位数和 IQR 在这些情况下是更为稳健的估算器。此外,这些选择还有助于防止成本出现负值。在大多数情况下,当两种方法得出相同的中心趋势时,它们提供的结果非常相似。

最终结论

图片来自 Meta AI

对不同模型和工具的速度与功耗分析仅仅是更广泛图景的一部分。我们观察到,轻量级或高度量化的模型通常在可靠性上表现不佳;随着聊天历史的增加或任务变得重复,幻觉现象变得更为频繁。这并不意外——较小的模型无法捕捉到较大模型的复杂性。为了解决这些限制,像重复惩罚和温度调整等设置可以改善输出。另一方面,像 70B 这样的大型模型始终保持强劲的性能,并且幻觉现象极少。然而,由于即便是最大的模型也不完全没有不准确性,负责任和可信赖的使用往往需要将这些模型与额外的工具(如 LangChain 和向量数据库)结合使用。尽管我们在这里没有探索特定任务的表现,但这些整合对于减少幻觉现象和增强模型可靠性至关重要。

总之,将大型语言模型(LLMs)部署在私有服务器上,可以为大型语言模型作为服务(LLMs as a service)提供一个具有成本优势和定制化机会的竞争性替代方案。私有服务器和基于服务的选项各有其优点,在Austin Ai我们专注于实现符合您需求的解决方案,无论是利用私有服务器、云服务还是混合方案。

在树莓派上运行本地 LLM 和 VLM

原文:towardsdatascience.com/running-local-llms-and-vlms-on-the-raspberry-pi-57bd0059c41a?source=collection_archive---------0-----------------------#2024-01-14

在树莓派上使用 Ollama 本地运行 Phi-2、Mistral 和 LLaVA 等模型

Pye Sone KyawTowards Data Science Pye Sone Kyaw

·发布于 Towards Data Science ·阅读时长 7 分钟·2024 年 1 月 14 日

--

使用 Ollama 在树莓派上托管 LLM 和 VLM — 来源:作者

有没有想过在自己的设备上运行自己的大语言模型(LLM)或视觉语言模型(VLM)?你可能有过这样的想法,但从零开始设置环境、下载合适的模型权重、以及设备是否能承载模型的疑虑,可能让你有些犹豫。

让我们再进一步。想象一下,在一个不大于信用卡大小的设备上操作你自己的 LLM 或 VLM——一台树莓派。难以想象?一点也不。我是在写这篇文章,所以它绝对是可能的。

可能,没错。但你为什么要这么做呢?

目前,边缘的 LLM 似乎有些不可思议。但这个特定的小众用例应该随着时间的推移而成熟,我们肯定会看到一些很酷的边缘解决方案,通过全本地生成式 AI 解决方案在设备端部署。

这也是关于突破极限,看看什么是可能的。如果在计算能力的极端范围内可以做到,那在树莓派和强大服务器 GPU 之间的任何级别上都可以做到。

传统上,边缘 AI 与计算机视觉密切相关。探索在边缘部署 LLM 和 VLM 为这一新兴领域增添了令人兴奋的维度。

最重要的是,我只是想用我最近购买的树莓派 5 做些有趣的事情。

那么,我们如何在树莓派上实现这一切呢?使用 Ollama!

什么是 Ollama?

Ollama已经成为在个人计算机上运行本地 LLM 的最佳解决方案之一,无需从头开始设置麻烦。只需几个命令,所有内容都能顺利设置。根据我的经验,它在多个设备和模型上都能完美运行,且一切都是自包含的。它甚至暴露了一个用于模型推理的 REST API,因此如果需要,你可以让它在树莓派上运行,并从其他应用程序和设备调用它。

Ollama 官网

还有Ollama Web UI,这是一款与 Ollama 完美兼容的美丽 AI UI/UX 界面,适合那些对命令行界面感到不安的人。可以将其视为一个本地的 ChatGPT 界面。

这两个开源软件共同提供了我认为是目前最好的本地托管 LLM 体验。

Ollama 和 Ollama Web UI 也支持像 LLaVA 这样的 VLM,这为这种边缘生成 AI 应用场景打开了更多的可能性。

技术要求

你只需要以下几个条件:

  • 树莓派 5(或使用 4,速度较慢)——选择 8GB RAM 版本,以便容纳 7B 模型。

  • SD 卡——最小 16GB,尺寸越大可以存储更多模型。已加载适当的操作系统,如 Raspbian Bookworm 或 Ubuntu。

  • 一个互联网连接

正如我之前提到的,在树莓派上运行 Ollama 已经接近硬件的极限。理论上,任何比树莓派更强大的设备,只要运行 Linux 发行版并且具有相似的内存容量,都应该能够运行 Ollama 和本文讨论的模型。

1. 安装 Ollama

为了在树莓派上安装 Ollama,我们将避免使用 Docker 以节省资源。

在终端中运行

curl https://ollama.ai/install.sh | sh

运行上述命令后,你应该看到类似于下图的内容。

来源:作者

如输出所示,前往 0.0.0.0:11434 验证 Ollama 是否在运行。由于我们使用的是树莓派,看到‘WARNING: No NVIDIA GPU detected. Ollama will run in CPU-only mode.’是正常的。但如果你在一个应该有 NVIDIA GPU 的设备上执行这些指令,说明出现了问题。

如遇任何问题或更新,请参考Ollama GitHub 仓库

2. 通过命令行运行 LLM

查看官方 Ollama 模型库,获取可以通过 Ollama 运行的模型列表。在 8GB 的树莓派上,7B 以上的模型将无法运行。我们将使用 Phi-2,这是微软推出的 2.7B LLM,目前采用 MIT 许可证。

我们将使用默认的 Phi-2 模型,但你可以自由使用 这里 中找到的其他标签。查看 Phi-2 的模型页面,看看如何与其互动。

在终端中运行

ollama run phi

一旦你看到类似下面的输出,你就已经在 Raspberry Pi 上运行了一个 LLM!就是这么简单。

来源:作者

这是与 Phi-2 2.7B 的互动。显然,你不会得到相同的输出,但你能明白意思。 | 来源:作者

你可以尝试其他模型,如 Mistral、Llama-2 等,但请确保 SD 卡有足够的空间来存储模型权重。

自然,模型越大,输出就越慢。在 Phi-2 2.7B 上,我每秒大约能得到 4 个 token,但使用 Mistral 7B 时,生成速度下降到每秒大约 2 个 token。一个 token 大致等于一个单词。

这是与 Mistral 7B 的互动 | 来源:作者

现在我们已经在 Raspberry Pi 上运行了 LLM,但我们还没有完成。终端并不适合所有人。让我们也让 Ollama Web UI 运行起来吧!

3. 安装和运行 Ollama Web UI

我们将按照 官方 Ollama Web UI GitHub 仓库 上的说明进行安装,不使用 Docker。它建议 Node.js 至少为 >= 20.10,因此我们将遵循这个要求。它还建议 Python 至少为 3.11,但 Raspbian 操作系统已经为我们安装了这个版本。

我们首先需要安装 Node.js。在终端中运行

curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - &&\
sudo apt-get install -y nodejs

如果需要,可以将 20.x 更改为更合适的版本,以便未来的读者参考。

然后运行下面的代码块。

git clone https://github.com/ollama-webui/ollama-webui.git
cd ollama-webui/

# Copying required .env file
cp -RPp example.env .env

# Building Frontend Using Node
npm i
npm run build

# Serving Frontend with the Backend
cd ./backend
pip install -r requirements.txt --break-system-packages 
sh start.sh

这是对 GitHub 上提供的内容的稍微修改。请注意,为了简洁起见,我们没有遵循最佳实践,比如使用虚拟环境,并且我们使用了 — break-system-packages 标志。如果遇到找不到 uvicorn 的错误,请重新启动终端会话。

如果一切顺利,你应该能够通过 Raspberry Pi 上的 0.0.0.0:8080 访问 Ollama Web UI,或者如果你通过同一网络上的其他设备访问,可以通过 http://<Raspberry Pi 的本地地址>:8080/ 访问。

如果你看到这个,是的,它成功了 | 来源:作者

一旦你创建了一个帐户并登录,你应该会看到类似下图的界面。

来源:作者

如果你之前下载过一些模型权重,应该会在下拉菜单中看到它们。如果没有,你可以去设置中下载一个模型。

可用的模型将在此处显示 | 来源:作者

如果你想下载新模型,前往设置 > 模型以拉取模型 | 来源:作者

整个界面非常简洁直观,因此我不会详细解释它。它真的是一个做得非常好的开源项目。

这是通过 Ollama Web UI 与 Mistral 7B 的互动 | 来源:作者

4. 通过 Ollama Web UI 运行 VLM

正如我在本文开头提到的,我们还可以运行 VLM。让我们运行 LLaVA,一个流行的开源 VLM,它也恰好被 Ollama 支持。为了实现这一点,通过界面拉取“llava”以下载权重。

不幸的是,与大型语言模型(LLMs)不同,Raspberry Pi 上的图像解读设置需要相当长的时间。下面的示例处理时间大约为 6 分钟。大部分时间可能是因为图像处理部分尚未得到充分优化,但这一点在未来肯定会有所改善。令牌生成速度约为 2 个令牌/秒。

查询图像来源:Pexels

总结

到目前为止,我们已经完成了本文的目标。总结一下,我们成功地使用 Ollama 和 Ollama Web UI 在 Raspberry Pi 上运行了 Phi-2、Mistral 和 LLaVA 等 LLM 和 VLM。

我完全能想象出在 Raspberry Pi(或其他小型边缘设备)上本地托管的 LLM 有很多应用场景,尤其是如果我们针对像 Phi-2 这样的模型,4 个令牌/秒的速度在某些应用场景中通过流式传输来看是可接受的。

“小型”LLM 和 VLM 领域,虽然它们被冠以“大型”的名称,显得有些矛盾,但这是一个活跃的研究领域,最近发布了不少模型。希望这一新兴趋势能够持续下去,更多高效且紧凑的模型能够继续发布!这绝对是未来几个月值得关注的内容。

免责声明:我与 Ollama 或 Ollama Web UI 没有任何关联。所有观点和意见均为个人观点,不代表任何组织。

本地运行 LLM 比你想象的更有用且更简单

原文:towardsdatascience.com/running-local-llms-is-more-useful-and-easier-than-you-think-f735631272ad?source=collection_archive---------0-----------------------#2024-07-11

逐步指南:如何使用 Python 在本地运行 Llama3

Guillaume WeingertnerTowards Data Science Guillaume Weingertner

·发布于 Towards Data Science ·阅读时间:6 分钟·2024 年 7 月 11 日

--

图像由作者通过 AI 生成

#1 为什么选择本地 LLM

ChatGPT 非常好,这一点毫无疑问,但它也有一个显著的缺点:你写的或上传的所有内容都会存储在 OpenAI 的服务器上。虽然在许多情况下这没有问题,但在处理敏感数据时,这可能会成为一个问题。

正因如此,我开始探索可以在个人计算机上本地运行的开源 LLM。事实证明,它们有更多优点,远超我的预期。

1. 数据隐私:您的信息保存在本地设备上。

2. 具有成本效益:无需订阅费用或 API 成本,免费使用。

3. 自定义:可以根据您的特定系统提示或数据集对模型进行微调。

4. 离线功能:无需互联网连接即可使用。

5. 无限制使用:不受外部 API 限制的约束。

现在,设置本地 LLM 出乎意料的简单。本文提供了逐步指南,帮助您在您的计算机上安装并运行开源模型……

在较低显存 GPU 上运行 PixArt-Σ/Flux.1 图像生成:Python 简短教程

原文:towardsdatascience.com/running-pixart-%CF%83-flux-1-image-generation-on-lower-vram-gpus-a-short-tutorial-in-python-62419f35596e?source=collection_archive---------6-----------------------#2024-08-27

Diffusers 和 Quanto 为 GPU 性能不足的用户带来了希望

Youness MansarTowards Data Science Youness Mansar

·发表于 Towards Data Science ·5 分钟阅读·2024 年 8 月 27 日

--

由 PixArt-Σ在本地生成,使用的显存小于 8GB

图像生成工具比以往任何时候都更加热门,且它们的功能更加强大。像 PixArt Sigma 和 Flux.1 这样的模型处于领先地位,得益于它们开放的模型权重和宽松的许可协议。这个设置允许进行创意性的调整,包括在不共享数据的情况下训练 LoRA。

然而,如果你使用的是较旧或显存较少的 GPU,使用这些模型可能会遇到挑战。通常在质量、速度和显存使用之间需要做出权衡。在这篇博文中,我们将重点讨论如何优化速度和降低显存使用,同时尽可能保持质量。这种方法在 PixArt 中表现得尤为出色,因为它的模型较小,但在 Flux.1 中效果可能有所不同。我将在文末分享一些针对 Flux.1 的替代解决方案。

PixArt Sigma 和 Flux.1 都是基于变换器的,这意味着它们能够受益于与大型语言模型(LLM)相同的量化技术。量化涉及压缩模型的各个组成部分,以减少内存使用。这使得你可以同时将所有模型组件存储在 GPU 显存中,从而与将权重在不同存储器之间移动的方法相比,生成速度更快。

在容器内运行 RStudio

原文:towardsdatascience.com/running-rstudio-inside-a-container-e9db5e809ff8?source=collection_archive---------5-----------------------#2024-03-09

逐步指南:在容器内设置 RStudio 服务器并使用你的本地 RStudio 设置

Rami KrispinTowards Data Science Rami Krispin

·发表于Towards Data Science ·6 分钟阅读·2024 年 3 月 9 日

--

这是一个逐步指南,介绍如何在容器内设置 RStudio 服务器并使用本地 RStudio 设置。我们将使用Rocker RStudio 镜像,并展示如何通过docker run命令及其参数进行自定义。

到本教程结束时,你将能够:

  • 在容器内启动 RStudio 服务器

  • 挂载本地文件夹

  • 克隆本地 RStudio 设置(颜色主题、代码片段等)

  • 加载本地 Renviron 设置

一只鲸鱼携带着一个 R 容器(由作者使用 Midjourney 创建)

介绍

RStudio 是 R 编程语言的主要 IDE。与其他通用 IDE(如 VScode)不同,RStudio 专为 R 用户及其需求而设计。正是这一点使得 RStudio 在 R 用户中非常受欢迎。默认情况下,RStudio 不支持 Docker。运行 RStudio 服务器版本是将 RStudio 设置和运行在容器中的主要方法。这需要在容器内安装并设置服务器,这对于某些用户来说可能是一个入门障碍。幸运的是,Rocker 项目——R 镜像的主要来源,提供了内置且现成可用的带有 RStudio 服务器的镜像。

在本教程中,我们将使用 Rocker RStudio 镜像,该镜像可以在Docker Hub找到。

前提条件

要跟随本教程并运行以下代码,你需要:

开始使用 Rocker

Rocker 项目是用于构建 R 镜像的主要平台。它提供了多种具有不同 R 环境设置的镜像,如 base-r、tidyverse、ML-verse、shiny、geospatial,当然,还有 RStudio 服务器镜像。所有可用 R 镜像的完整列表可以在 Rocker 的 Docker Hub 页面找到。

Rocker 按下载量排序的热门镜像(作者从 Docker Hub 截图)

我们将使用 rocker/rstudio 镜像,顾名思义,它已经安装并准备好使用 RStudio 服务器。通过 docker run 命令,我们可以以交互模式启动该容器,并通过浏览器访问 RStudio 服务器。

让我们通过 docker pull 命令拉取镜像,开始使用:

>docker pull rocker/rstudio                                                                                                                                            ok
Using default tag: latest
latest: Pulling from rocker/rstudio
a4a2c7a57ed8: Pull complete
d0f9831967fe: Pull complete
e78811385d51: Pull complete
c61633a20287: Pull complete
832cef14f2fb: Pull complete
8395fbba6231: Pull complete
fb53abdcfb34: Pull complete
c942edef0d7f: Pull complete
Digest: sha256:8e25784e1d29420effefae1f31e543c792d215d89ce717b0cc64fb18a77668f3
Status: Downloaded newer image for rocker/rstudio:latest
docker.io/rocker/rstudio:latest

你可以使用 docker images 命令来验证镜像是否成功下载:

>docker images                                                                                                                                                    ok  36s
REPOSITORY              TAG          IMAGE ID       CREATED       SIZE
rocker/rstudio          latest       7039fb162243   2 days ago    1.94GB

现在,让我们使用 Rocker 项目推荐的命令,通过 docker run 命令在容器内启动 RStudio:

>docker run --rm -ti -e PASSWORD=yourpassword -p 8787:8787 rocker/rstudio

在我们打开浏览器上的 RStudio 服务器之前,先回顾一下我们之前使用的run参数:

  • rm — 容器退出时自动删除容器(从终端按 control + c

  • ti — 以交互模式运行容器

  • e — 设置环境变量,在这种情况下,定义服务器登录密码为 yourpassword

  • p — 定义端口映射。在这种情况下,我们将容器的 8787 端口与本地机器上的 8787 端口映射

执行命令后,你可以通过本地的 8787 端口访问 RStudio 服务器(例如,localhost:8787)。这将弹出登录页面,你应使用:

  • 用户名:rstudio

  • 密码:yourpassword(在 run 命令中设置)

你应该看到以下输出:

RStudio 服务器的初始视图(作者截屏)

注意: 你可以通过在终端点击 control+c 来停止运行的容器。

哎呀!它是临时的!

默认情况下,Docker 容器以临时模式运行。任何在容器内创建并保存的代码或输入都将在终止容器运行时丢失。如果你希望使用 Docker 作为开发环境,这样就既不实际也不有用。为了解决这个问题,我们将使用 volume(或 v)参数,它使得可以将本地文件夹挂载到容器的文件系统中。

以下代码演示了如何使用 volume 参数将我们执行 run 命令所在的文件夹(例如,.)与 RStudio 服务器的主文件夹挂载:

docker run --rm -ti \
-v .:/home/rstudio \
-e PASSWORD=yourpassword \
-p 8787:8787 rocker/rstudio

volume 参数使用以下格式 source:target 将本地文件夹(例如,source)映射到容器(例如,target)。

现在,让我们回到浏览器并使用本地地址重新打开 RStudio 服务器 —— localhost:8787。你应该能在 RStudio 文件部分看到挂载到容器的本地文件夹和/或文件。在我的例子中,我将挂载我的教程文件夹,它包含以下文件夹:

.
├── Introduction-to-Docker
├── awesome-ds-setting
├── forecast-poc
├── forecasting-at-scale
├── lang2sql
├── postgres-docker
├── python
├── rstudio-docker
├── sdsu-docker-workshop
├── shinylive-r
├── statistical-rethinking-2024
├── vscode-python
├── vscode-python-template
├── vscode-r
└── vscode-r-template

如下图所示,本地文件夹现在可以通过 RStudio 服务器访问(以紫色矩形标出):

带有本地卷的 RStudio 服务器(作者提供的截图)

注意:你可以使用卷参数挂载多个卷。例如,一个用于你的项目,另一个用于包含数据文件夹的文件夹。

这使我们能够在容器运行时从本地文件夹中读取和写入文件。

克隆你的本地 RStudio 设置

在上一部分中,我们了解了如何使用卷参数将本地文件夹挂载到容器中。这使我们可以在容器内工作时将代码保存在本地。在本节中,我们将学习如何利用卷参数将本地 RStudio 设置与容器中的设置进行挂载。这里的思路是启动容器并使用我们的本地设置运行 RStudio 服务器,而无需在每次重新启动容器时更新设置。这包括加载本地设置,如颜色主题设置、代码片段、环境变量等。

在我们更新 docker run 命令时,使用本地的 RStudio 配置文件夹之前,我们需要确定本地和容器中的配置文件夹路径。例如,我机器上的路径是 ~/.config/rstudio,并且它包含以下文件夹和文件:

.
├── dictionaries
│   └── custom
├── rstudio-prefs.json
└── snippets
    └── r.snippets

同样,容器中的 .config/rstudio 文件夹位于 /home/rstudio/ 下。因此,我们将使用以下映射:

$HOME/.config/rstudio:/home/rstudio/.config/rstudio

同样地,我们希望将 .Renviron 文件与本地环境变量挂载。.Renviron 文件位于本地机器的根文件夹下,我们使用相同的方法将本地文件与容器中的文件映射:

$HOME/.Renviron:/home/rstudio/.Renviron

现在,让我们将所有设置放在一起并重新启动容器:

docker run --rm -ti \
-v .:/home/rstudio \
-v $HOME/.config/rstudio:/home/rstudio/.config/rstudio \
-v $HOME/.Renviron:/home/rstudio/.Renviron \
-e PASSWORD=yourpassword \
-p 8787:8787 rocker/rstudio

在将本地 RStudio 配置文件夹与容器中的文件夹挂载之后,服务器设置现在与我机器上的本地 RStudio 设置映射在一起:

带有本地设置的 RStudio 服务器(作者提供的截图)

总结

本教程重点介绍如何使用docker run命令定制 Rocker 的 RStudio 镜像。我们使用volume参数将本地文件夹挂载到容器的工作目录中。这使我们能够在容器化环境中工作,并将工作保存到本地。此外,我们使用volume参数将本地的 RStudio 设置克隆到容器中。这使得从本地环境到容器化环境的过渡更加顺畅。随着我们添加和使用更多参数,命令可能会变得长且复杂。一旦确定了run设置,下一步是通过Docker Compose将其转换为 YAML 文件。除了简化容器启动过程,Docker Compose 还使您能够管理更复杂的场景,例如启动多个容器。

资源

使用本地文档运行 STORM AI 研究系统

原文:towardsdatascience.com/running-the-storm-ai-research-system-with-your-local-documents-e413ea2ae064?source=collection_archive---------3-----------------------#2024-10-28

使用 AI 辅助研究 FEMA 灾难响应文档

Matthew Harris数据科学前沿 Matthew Harris

·发布于数据科学前沿 ·16 分钟阅读·2024 年 10 月 28 日

--

STORM 通过视角引导的问题提问在模拟对话中研究主题。来源

TL;DR

LLM 代理的使用在处理多步骤、长上下文的研究任务时变得越来越普遍,因为传统的 RAG 直接提示方法有时会遇到困难。在本文中,我们将探讨由斯坦福大学开发的一种新型且有前景的技术 Synthesis of Topic Outlines through Retrieval and Multi-perspective Question Asking (STORM), 它使用 LLM 代理模拟“视角引导的对话”以实现复杂的研究目标,并生成丰富的研究文章,供人们在写作前研究使用。STORM 最初是为了从网络资源中收集信息而开发的,但也支持搜索本地文档向量存储。在本文中,我们将展示如何实现 STORM 以支持基于 AI 的本地 PDF 研究,使用美国 FEMA 灾难准备和援助文档。

看到利用大语言模型(LLM)进行知识检索在相对较短的时间内取得的进展,真是令人惊叹。自从 2020 年发布的首次关于检索增强生成(RAG)论文以来,我们见证了这个生态系统的发展,现已涵盖了一系列可用的技术。其中更先进的技术之一是代理 RAG,LLM 代理通过迭代和优化文档检索来解决更复杂的研究任务。这类似于人类进行研究的方式,通过探索多种不同的搜索查询来建立更清晰的背景,有时还会与其他人讨论这一主题,并将所有信息整合成最终的结果。而单回合 RAG,即便采用了查询扩展和重新排序等技术,在应对像这样的复杂多跳研究任务时,仍然存在困难。

使用代理框架(如AutogenCrewAILangGraph)以及特定的 AI 研究助手(如GPT Researcher)进行知识检索的模式有很多。在本文中,我们将探讨斯坦福大学开发的一个由 LLM 驱动的研究写作系统,名为Synthesis of Topic Outlines through Retrieval and Multi-perspective Question Asking(STORM)。

STORM AI 研究写作系统

STORM 应用了一种巧妙的技术,其中 LLM 代理模拟“视角引导对话”以达成研究目标,并扩展了“基于大纲的 RAG”,以生成更丰富的文章内容。

该系统配置为生成类似维基百科风格的文章,并在一组 10 名经验丰富的维基百科编辑者中进行了测试。

10 名经验丰富的维基百科编辑者对 STORM 在实际使用中的感知有用性的调查结果。来源

总体反响积极,70%的编辑认为该工具在他们的写作前阶段研究一个主题时会非常有用。希望未来的调查能够包括超过 10 名编辑,但需要注意的是,作者也通过使用 FreshWiki(一组近期高质量的维基百科文章数据集)对传统文章生成方法进行了基准测试,其中 STORM 被发现优于以往的方法。

由 10 名经验丰富的维基百科编辑者对 STORM 和oRAG生成的 20 对文章进行人工评估。每对文章由两名维基百科编辑者评估。来源

STORM 是开源的,并作为一个Python 包提供,另外还支持使用如LangGraph等框架的实现。最近,STORM 已被增强以支持一种名为Co-STORM的人类-AI 协作知识策划方法,将人类置于 AI 辅助研究环路的中心。

尽管它在自动化和人工评估中都显著优于基准方法,但作者也承认存在一些局限性。目前它还不是多模态的,生成的内容也未达到经验丰富的人工质量——我觉得它目前还不适合这一点,更多的是针对写作前的研究,而非最终文章——另外,参考文献方面也存在一些需要进一步改进的细节。话虽如此,如果你有一个深入的研究任务,值得一试。

你可以在网上试用 STORM——它非常有趣!——并配置为使用网络信息进行研究。

但如果用自己的数据运行 STORM 呢?

许多组织希望将 AI 研究工具与他们自己的内部数据结合使用。STORM 的作者们做得很好,记录了如何将 STORM 与不同的 LLM 提供者以及本地向量数据库结合使用,这意味着你可以在自己的文档上运行 STORM。

那么,让我们来试试吧!

设置与代码

你可以在这里找到本文的代码,包含了环境设置说明以及如何收集一些示例文档来进行演示。

FEMA 灾难准备和援助文档

我们将使用 34 份由美国联邦应急管理局(FEMA)创建的 PDF 文档,帮助人们为灾难做准备并进行响应。这些文档可能通常不是人们用来撰写深入研究文章的内容,但我很想看看 AI 如何帮助人们为灾难做好准备。

……我已经编写了处理 FEMA 报告的代码,这些代码来自之前的一些博客文章,已经包含在上面链接的代码库中。😊

解析与切分

一旦我们有了文档,就需要将其拆分成更小的文档,以便 STORM 能够在语料库中查找特定的主题。由于 STORM 最初旨在生成类似维基百科的文章,我选择尝试两种方法:(i)简单地通过LangChain 的 PyPDFLoader按页将文档拆分为子文档,从而粗略模拟一个包含多个子主题的维基百科页面。许多 FEMA 的 PDF 是单页文档,看起来与维基百科文章相差不大;(ii)进一步将文档切分为更小的部分,更可能涵盖一个离散的子主题。

这些当然是非常基础的解析方法,但我想看看这两种技术在结果上的差异。任何对 STORM 在本地文档中的严肃使用都应该投入所有常见的配对优化工作。

def parse_pdfs():
    """
    Parses all PDF files in the specified directory and loads their content.

    This function iterates through all files in the directory specified by PDF_DIR,
    checks if they have a .pdf extension, and loads their content using PyPDFLoader.
    The loaded content from each PDF is appended to a list which is then returned.

    Returns:
        list: A list containing the content of all loaded PDF documents.
    """
    docs = []
    pdfs = os.listdir(PDF_DIR)
    print(f"We have {len(pdfs)} pdfs")
    for pdf_file in pdfs:
        if not pdf_file.endswith(".pdf"):
            continue
        print(f"Loading PDF: {pdf_file}")
        file_path = f"{PDF_DIR}/{pdf_file}"
        loader = PyPDFLoader(file_path)
        docs = docs + loader.load()
        print(f"Loaded {len(docs)} documents")

    return docs

docs = parse_pdfs()

text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
chunks = text_splitter.split_documents(docs)

元数据增强

STORM 的示例文档要求文档具有元数据字段‘URL’,‘title’,和‘description’,其中‘URL’应该是唯一的。由于我们正在拆分 PDF 文档,因此没有单独页面和块的标题和描述,因此我选择使用简单的 LLM 调用来生成这些内容。

对于 URLs,我们有每个 PDF 页面的链接,但对于页面内的块,复杂的知识检索系统可以通过布局检测模型生成元数据,这样文本块区域就可以在相应的 PDF 中高亮显示,但对于这个演示,我只是简单地在 URL 中添加了一个‘_id’查询参数,这个参数不会做任何事情,只是确保它们对于不同的块是唯一的。

def summarize_text(text, prompt):
    """
    Generate a summary of some text based on the user's prompt

    Args:

    text (str) - the text to analyze
    prompt (str) - prompt instruction on how to summarize the text, eg 'generate a title'

    Returns:

    summary (text) - LLM-generated summary

    """
    messages = [
        (
            "system",
            "You are an assistant that gives very brief single sentence description of text.",
        ),
        ("human", f"{prompt} :: \n\n {text}"),
    ]
    ai_msg = llm.invoke(messages)
    summary = ai_msg.content
    return summary

def enrich_metadata(docs):
    """
    Uses an LLM to populate 'title' and 'description' for text chunks

    Args:

    docs (list) - list of LangChain documents

    Returns:

    docs (list) - list of LangChain documents with metadata fields populated

    """
    new_docs = []
    for doc in docs:

        # pdf name is last part of doc.metadata['source']
        pdf_name = doc.metadata["source"].split("/")[-1]

        # Find row in df where pdf_name is in URL
        row = df[df["Document"].str.contains(pdf_name)]
        page = doc.metadata["page"] + 1
        url = f"{row['Document'].values[0]}?id={str(uuid4())}#page={page}"

        # We'll use an LLM to generate a summary and title of the text, used by STORM
        # This is just for the demo, proper application would have better metadata
        summary = summarize_text(doc.page_content, prompt="Please describe this text:")
        title = summarize_text(
            doc.page_content, prompt="Please generate a 5 word title for this text:"
        )

        doc.metadata["description"] = summary
        doc.metadata["title"] = title
        doc.metadata["url"] = url
        doc.metadata["content"] = doc.page_content

        # print(json.dumps(doc.metadata, indent=2))
        new_docs.append(doc)

    print(f"There are {len(docs)} docs")

    return new_docs

docs = enrich_metadata(docs)
chunks = enrich_metadata(chunks)

构建向量数据库

STORM 已经支持Qdrant 向量存储。我喜欢在可能的情况下使用像 LangChain 和 Llama Index 这样的框架,这样可以在将来更容易地更换提供商,因此我选择使用 LangChain 构建一个持久化到本地文件系统的本地 Qdrant 向量数据库,而不是 STORM 的自动向量数据库管理。我认为这提供了更多的控制,并且对于那些已经有填充文档向量存储的管道的人来说更具可识别性。

def build_vector_store(doc_type, docs):
    """
    Givena  list of LangChain docs, will embed and create a file-system Qdrant vector database.
    The folder includes doc_type in its name to avoid overwriting.

    Args:

    doc_type (str) - String to indicate level of document split, eg 'pages',
                     'chunks'. Used to name the database save folder
    docs (list) - List of langchain documents to embed and store in vector database

    Returns:

    Nothing returned by function, but db saved to f"{DB_DIR}_{doc_type}".

    """

    print(f"There are {len(docs)} docs")

    save_dir = f"{DB_DIR}_{doc_type}"

    print(f"Saving vectors to directory {save_dir}")

    client = QdrantClient(path=save_dir)

    client.create_collection(
        collection_name=DB_COLLECTION_NAME,
        vectors_config=VectorParams(size=num_vectors, distance=Distance.COSINE),
    )

    vector_store = QdrantVectorStore(
        client=client,
        collection_name=DB_COLLECTION_NAME,
        embedding=embeddings,
    )

    uuids = [str(uuid4()) for _ in range(len(docs))]

    vector_store.add_documents(documents=docs, ids=uuids)

build_vector_store("pages", docs)
build_vector_store("chunks", docs)

运行 STORM

STORM 的代码库有一些很棒的示例,包括不同的搜索引擎和 LLM 的使用,以及如何使用 Qdrant 向量存储。我决定结合这些示例中的各种功能,并添加一些额外的后处理,如下所示:

  1. 增加了与 OpenAI 或 Ollama 一起运行的功能

  2. 增加了支持传入向量数据库目录的功能

  3. 增加了一个功能来解析参考文献元数据文件,将参考文献添加到生成的精炼文章中。STORM 将这些参考文献生成在一个 JSON 文件中,但没有自动将它们添加到输出文章中。我不确定这是不是由于我错过了某个设置,但参考文献对于评估任何 AI 研究技术都至关重要,因此我增加了这个自定义后处理步骤。

  4. 最后,我注意到开放模型在模板和角色设定方面提供了更多的指导,因为它们执行指令的准确度不如商业模型。我喜欢这些控制的透明性,并将其保留在 OpenAI 中,以便将来在工作中进行调整。

这里是所有内容(查看repo notebook以获取完整代码)…

def set_instructions(runner):
    """
    Adjusts templates and personas for the STORM AI Research algorithm.

    Args:

    runner - STORM runner object

    Returns:

    runner - STORM runner object with extra prompting

    """

    # Open LMs are generally weaker in following output format.
    # One way for mitigation is to add one-shot example to the prompt to exemplify the desired output format.
    # For example, we can add the following examples to the two prompts used in StormPersonaGenerator.
    # Note that the example should be an object of dspy.Example with fields matching the InputField
    # and OutputField in the prompt (i.e., dspy.Signature).
    find_related_topic_example = Example(
        topic="Knowledge Curation",
        related_topics="https://en.wikipedia.org/wiki/Knowledge_management\n"
        "https://en.wikipedia.org/wiki/Information_science\n"
        "https://en.wikipedia.org/wiki/Library_science\n",
    )
    gen_persona_example = Example(
        topic="Knowledge Curation",
        examples="Title: Knowledge management\n"
        "Table of Contents: History\nResearch\n  Dimensions\n  Strategies\n  Motivations\nKM technologies"
        "\nKnowledge barriers\nKnowledge retention\nKnowledge audit\nKnowledge protection\n"
        "  Knowledge protection methods\n    Formal methods\n    Informal methods\n"
        "  Balancing knowledge protection and knowledge sharing\n  Knowledge protection risks",
        personas="1\. Historian of Knowledge Systems: This editor will focus on the history and evolution of knowledge curation. They will provide context on how knowledge curation has changed over time and its impact on modern practices.\n"
        "2\. Information Science Professional: With insights from 'Information science', this editor will explore the foundational theories, definitions, and philosophy that underpin knowledge curation\n"
        "3\. Digital Librarian: This editor will delve into the specifics of how digital libraries operate, including software, metadata, digital preservation.\n"
        "4\. Technical expert: This editor will focus on the technical aspects of knowledge curation, such as common features of content management systems.\n"
        "5\. Museum Curator: The museum curator will contribute expertise on the curation of physical items and the transition of these practices into the digital realm.",
    )
    runner.storm_knowledge_curation_module.persona_generator.create_writer_with_persona.find_related_topic.demos = [
        find_related_topic_example
    ]
    runner.storm_knowledge_curation_module.persona_generator.create_writer_with_persona.gen_persona.demos = [
        gen_persona_example
    ]

    # A trade-off of adding one-shot example is that it will increase the input length of the prompt. Also, some
    # examples may be very long (e.g., an example for writing a section based on the given information), which may
    # confuse the model. For these cases, you can create a pseudo-example that is short and easy to understand to steer
    # the model's output format.
    # For example, we can add the following pseudo-examples to the prompt used in WritePageOutlineFromConv and
    # ConvToSection.
    write_page_outline_example = Example(
        topic="Example Topic",
        conv="Wikipedia Writer: ...\nExpert: ...\nWikipedia Writer: ...\nExpert: ...",
        old_outline="# Section 1\n## Subsection 1\n## Subsection 2\n"
        "# Section 2\n## Subsection 1\n## Subsection 2\n"
        "# Section 3",
        outline="# New Section 1\n## New Subsection 1\n## New Subsection 2\n"
        "# New Section 2\n"
        "# New Section 3\n## New Subsection 1\n## New Subsection 2\n## New Subsection 3",
    )
    runner.storm_outline_generation_module.write_outline.write_page_outline.demos = [
        write_page_outline_example
    ]
    write_section_example = Example(
        info="[1]\nInformation in document 1\n[2]\nInformation in document 2\n[3]\nInformation in document 3",
        topic="Example Topic",
        section="Example Section",
        output="# Example Topic\n## Subsection 1\n"
        "This is an example sentence [1]. This is another example sentence [2][3].\n"
        "## Subsection 2\nThis is one more example sentence [1].",
    )
    runner.storm_article_generation.section_gen.write_section.demos = [
        write_section_example
    ]

    return runner

def latest_dir(parent_folder):
    """
    Find the most recent folder (by modified date) in the specified parent folder.

    Args:
        parent_folder (str): The path to the parent folder where the search for the most recent folder will be conducted. Defaults to f"{DATA_DIR}/storm_output".

    Returns:
        str: The path to the most recently modified folder within the parent folder.
    """
    # Find most recent folder (by modified date) in DATA_DIR/storm_data
    # TODO, find out how exactly storm passes back its output directory to avoid this hack
    folders = [f.path for f in os.scandir(parent_folder) if f.is_dir()]
    folder = max(folders, key=os.path.getmtime)

    return folder

def generate_footnotes(folder):
    """
    Generates footnotes from a JSON file containing URL information.

    Args:
        folder (str): The directory path where the 'url_to_info.json' file is located.

    Returns:
        str: A formatted string containing footnotes with URLs and their corresponding titles.
    """

    file = f"{folder}/url_to_info.json"

    with open(file) as f:
        data = json.load(f)

    refs = {}
    for rec in data["url_to_unified_index"]:
        val = data["url_to_unified_index"][rec]
        title = data["url_to_info"][rec]["title"].replace('"', "")
        refs[val] = f"- {val} {title}"

    keys = list(refs.keys())
    keys.sort()

    footer = ""
    for key in keys:
        footer += f"{refs[key]}\n"

    return footer, refs

def generate_markdown_article(output_dir):
    """
    Generates a markdown article by reading a text file, appending footnotes, 
    and saving the result as a markdown file.

    The function performs the following steps:
    1\. Retrieves the latest directory using the `latest_dir` function.
    2\. Generates footnotes for the article using the `generate_footnotes` function.
    3\. Reads the content of a text file named 'storm_gen_article_polished.txt' 
       located in the latest directory.
    4\. Appends the generated footnotes to the end of the article content.
    5\. Writes the modified content to a new markdown file named 
       STORM_OUTPUT_MARKDOWN_ARTICLE in the same directory.

    Args:

    output_dir (str) - The directory where the STORM output is stored.

    """

    folder = latest_dir(output_dir)
    footnotes, refs = generate_footnotes(folder)

    with open(f"{folder}/storm_gen_article_polished.txt") as f:
        text = f.read()

    # Update text references like [10] to link to URLs
    for ref in refs:
        print(f"Ref: {ref}, Ref_num: {refs[ref]}")
        url = refs[ref].split("(")[1].split(")")[0]
        text = text.replace(f"[{ref}]", f"\[{ref}\]")

    text += f"\n\n## References\n\n{footnotes}"

    with open(f"{folder}/{STORM_OUTPUT_MARKDOWN_ARTICLE}", "w") as f:
        f.write(text)

def run_storm(topic, model_type, db_dir):
    """
    This function runs the STORM AI Research algorithm using data
    in a QDrant local database.

    Args:

    topic (str) - The research topic to generate the article for
    model_type (str) - One of 'openai' and 'ollama' to control LLM used
    db_dir (str) - Directory where the QDrant vector database is

    """
    if model_type not in ["openai", "ollama"]:
        print("Unsupported model_type")
        sys.exit()

    # Clear lock so can be read
    if os.path.exists(f"{db_dir}/.lock"):
        print(f"Removing lock file {db_dir}/.lock")
        os.remove(f"{db_dir}/.lock")

    print(f"Loading Qdrant vector store from {db_dir}")

    engine_lm_configs = STORMWikiLMConfigs()

    if model_type == "openai":

        print("Using OpenAI models")

        # Initialize the language model configurations
        openai_kwargs = {
            "api_key": os.getenv("OPENAI_API_KEY"),
            "temperature": 1.0,
            "top_p": 0.9,
        }

        ModelClass = (
            OpenAIModel
            if os.getenv("OPENAI_API_TYPE") == "openai"
            else AzureOpenAIModel
        )
        # If you are using Azure service, make sure the model name matches your own deployed model name.
        # The default name here is only used for demonstration and may not match your case.
        gpt_35_model_name = (
            "gpt-4o-mini"
            if os.getenv("OPENAI_API_TYPE") == "openai"
            else "gpt-35-turbo"
        )
        gpt_4_model_name = "gpt-4o"
        if os.getenv("OPENAI_API_TYPE") == "azure":
            openai_kwargs["api_base"] = os.getenv("AZURE_API_BASE")
            openai_kwargs["api_version"] = os.getenv("AZURE_API_VERSION")

        # STORM is a LM system so different components can be powered by different models.
        # For a good balance between cost and quality, you can choose a cheaper/faster model for conv_simulator_lm
        # which is used to split queries, synthesize answers in the conversation. We recommend using stronger models
        # for outline_gen_lm which is responsible for organizing the collected information, and article_gen_lm
        # which is responsible for generating sections with citations.
        conv_simulator_lm = ModelClass(
            model=gpt_35_model_name, max_tokens=10000, **openai_kwargs
        )
        question_asker_lm = ModelClass(
            model=gpt_35_model_name, max_tokens=10000, **openai_kwargs
        )
        outline_gen_lm = ModelClass(
            model=gpt_4_model_name, max_tokens=10000, **openai_kwargs
        )
        article_gen_lm = ModelClass(
            model=gpt_4_model_name, max_tokens=10000, **openai_kwargs
        )
        article_polish_lm = ModelClass(
            model=gpt_4_model_name, max_tokens=10000, **openai_kwargs
        )

    elif model_type == "ollama":

        print("Using Ollama models")

        ollama_kwargs = {
            # "model": "llama3.2:3b",
            "model": "llama3.1:latest",
            # "model": "qwen2.5:14b",
            "port": "11434",
            "url": "http://localhost",
            "stop": (
                "\n\n---",
            ),  # dspy uses "\n\n---" to separate examples. Open models sometimes generate this.
        }

        conv_simulator_lm = OllamaClient(max_tokens=500, **ollama_kwargs)
        question_asker_lm = OllamaClient(max_tokens=500, **ollama_kwargs)
        outline_gen_lm = OllamaClient(max_tokens=400, **ollama_kwargs)
        article_gen_lm = OllamaClient(max_tokens=700, **ollama_kwargs)
        article_polish_lm = OllamaClient(max_tokens=4000, **ollama_kwargs)

    engine_lm_configs.set_conv_simulator_lm(conv_simulator_lm)
    engine_lm_configs.set_question_asker_lm(question_asker_lm)
    engine_lm_configs.set_outline_gen_lm(outline_gen_lm)
    engine_lm_configs.set_article_gen_lm(article_gen_lm)
    engine_lm_configs.set_article_polish_lm(article_polish_lm)

    max_conv_turn = 4
    max_perspective = 3
    search_top_k = 10
    max_thread_num = 1
    device = "cpu"
    vector_db_mode = "offline"

    do_research = True
    do_generate_outline = True
    do_generate_article = True
    do_polish_article = True

    # Initialize the engine arguments
    output_dir=f"{STORM_OUTPUT_DIR}/{db_dir.split('db_')[1]}"
    print(f"Output directory: {output_dir}")

    engine_args = STORMWikiRunnerArguments(
        output_dir=output_dir,
        max_conv_turn=max_conv_turn,
        max_perspective=max_perspective,
        search_top_k=search_top_k,
        max_thread_num=max_thread_num,
    )

    # Setup VectorRM to retrieve information from your own data
    rm = VectorRM(
        collection_name=DB_COLLECTION_NAME,
        embedding_model=EMBEDDING_MODEL,
        device=device,
        k=search_top_k,
    )

    # initialize the vector store, either online (store the db on Qdrant server) or offline (store the db locally):
    if vector_db_mode == "offline":
        rm.init_offline_vector_db(vector_store_path=db_dir)

    # Initialize the STORM Wiki Runner
    runner = STORMWikiRunner(engine_args, engine_lm_configs, rm)

    # Set instructions for the STORM AI Research algorithm
    runner = set_instructions(runner)

    # run the pipeline
    runner.run(
        topic=topic,
        do_research=do_research,
        do_generate_outline=do_generate_outline,
        do_generate_article=do_generate_article,
        do_polish_article=do_polish_article,
    )
    runner.post_run()
    runner.summary()

    generate_markdown_article(output_dir)

我们准备好运行 STORM 了!

对于研究课题,我选择了一个典型的 RAG 系统难以回答的主题,且在 PDF 数据中没有很好覆盖的内容,这样我们可以看到归因效果如何……

比较不同类型灾害的财务影响及其对社区的影响

在两个数据库上运行此操作……

query = "Compare the financial impact of different types of disasters and how those impact communities"

for doc_type in ["pages", "chunks"]:
    db_dir = f"{DB_DIR}_{doc_type}"
    run_storm(query=query, model_type="openai", db_dir=db_dir)

使用 OpenAI 时,这个过程在我的 Macbook Pro M2(16GB 内存)上大约花费了 6 分钟。我需要指出的是,其他一些简单查询,尤其是底层文档中有更多支持内容的查询,要快得多(有些情况下不到 30 秒)。

STORM 结果

STORM 生成了一组输出文件……

由 STORM 生成的文件,其中一个 Markdown 文件结合了润色后的文章和参考脚注。

审视conversation_log.jsonllm_call_history.json文件,查看以视角引导的对话组件,十分有趣。

对于我们的研究课题……

比较不同类型灾害的财务影响及其对社区的影响

你可以在这里找到生成的文章……

一些快速观察

这个演示没有进行正式的评估 — 这可能比单跳 RAG 系统更为复杂 — 但这里有一些可能有用的主观观察……

  1. 按页面或较小块拆分的解析会生成合理的预读报告,人类可以用来研究与灾害财务影响相关的领域

  2. 两种配对方法都在整个过程中提供了引用,但使用更小的文本块似乎产生了更少的引用。请参见上述两篇文章中的总结部分。更多的引用可以为分析提供更坚实的基础!

  3. 按较小块拆分的解析有时会产生不相关的引用,这是 STORM 论文中提到的引用问题之一。请参见总结部分中源‘10’的引用,该引用与参考句子不符。

  4. 总体来说,正如我预期的那样,基于 Wiki 文章开发的算法,按 PDF 分割文本似乎能生成一篇更具凝聚力和基础性的文章(对我来说!)

尽管输入的研究主题在基础文档中并没有被深入探讨,但生成的报告为进一步的人类分析提供了一个很好的起点。

未来的工作

我们在本文中没有讨论Co-Storm,它将人类引入过程。这似乎是一个非常适合 AI 驱动研究的方向,我正在研究这个方向。

未来的工作还可以考虑将系统提示和人物角色调整到具体的商业案例中。目前,这些提示是为类似维基百科的过程设计的……

STORM 系统提示,展示了如何强调创建维基百科风格的文章。来源

另一个可能的方向是将 STORM 的连接器扩展到 Qdrant 以外的系统,例如,支持其他向量存储,或者更好的是,支持 Langchain 和 Llama index 向量存储的通用支持。作者鼓励这一类的工作,涉及这个文件的 PR 可能是我未来的工作方向。

在没有互联网连接的情况下运行 STORM 将是一件令人兴奋的事情,因为它为现场的 AI 助手开辟了新的可能性。正如从演示代码中可以看到的,我增加了运行 STORM 的能力,使用 Ollama 本地托管的模型,但由于令牌吞吐率过低,LLM 代理讨论阶段没有完成,因此系统在我使用小型量化模型的笔记本电脑上没有完成任务。这或许是未来博客文章的一个话题!

最后,尽管在线用户界面非常友好,但随代码仓库附带的演示界面非常基础,无法用于生产环境。也许斯坦福团队将发布更先进的界面——可能已经有了?——如果没有的话,仍然需要在这方面进行改进。

结论

这是一个简短的演示,旨在帮助人们开始使用 STORM 处理自己的文档。我没有进行系统的评估,如果在实际环境中使用 STORM,显然需要进行这项工作。尽管如此,我还是对 STORM 能够生成相对细致的研究主题和生成有良好引用的预写作研究内容感到印象深刻,这将帮助我进行自己的研究。

参考文献

知识密集型 NLP 任务的检索增强生成,Lewis 等人,2020 年

大规模语言模型的检索增强生成:综述,Yunfan 等人,2024 年

从零开始利用大型语言模型协助撰写类似 Wikipedia 的文章,Shao 等,2024

进入未知的未知:通过参与语言模型代理对话进行积极的人类学习,Jiang 等,2024

MultiHop-RAG:多跳查询的检索增强生成基准测试,Tang 等,2024

你可以在这里找到本文的代码

如果喜欢这篇文章,请点赞,如果你能关注我,我将非常高兴!你可以在 这里 找到更多文章或者在 LinkedIn 上联系我。

在 QGIS 中进行可视性分析

原文:towardsdatascience.com/running-visibility-analysis-in-qgis-d8fbdd8b02c5?source=collection_archive---------6-----------------------#2024-10-04

学习如何使用免费的 GIS 软件和数据轻松进行可视性分析,并利用结果创建令人惊叹的可视化效果

Helen McKenzieTowards Data Science Helen McKenzie

·发表于 Towards Data Science ·阅读时间 9 分钟·2024 年 10 月 4 日

--

我最喜欢的空间分析类型之一就是可视性分析。它是一个非常简单的概念,允许你理论上计算出某个物体从哪里可以被看到。可视性分析主要有两种形式——也叫视域分析或 ZTVs(理论可视区)。这两种形式分别是:

  • “标准”视域分析:我能从这个位置看到什么?例如,如果我站在一座山顶上,我的视野将是什么样的?

  • “反向”视域分析:我能看到哪些位置?例如,如果我站在一座山顶上,谁能看到我?

它的工作原理是通过取一组观察者点和一个地形模型,然后计算该地形模型的每个部分与观察者点之间的视线。听起来这会是一个很慢的过程,对吧?确实可以是。不过,结果非常惊人,并且对某些应用来说是不可或缺的。

最常见的一个应用是帮助人们了解一个新开发项目——比如风力发电场、太阳能电池板站点或高速公路——从哪些地方能看到。分析人员可以基于他们的发现建模不同的场景,例如如果他们在这个精确的位置种上一排树,视野会发生什么变化?可视性分析还可以用于推动广告牌的定价策略,或者帮助…

使用 Llama Guard 2 保护您的 LLM 聊天机器人

原文:towardsdatascience.com/safeguard-your-llm-chatbot-with-llama-guard-2-ff5f5aa0f894?source=collection_archive---------9-----------------------#2024-05-13

如何将内容审查应用到您的 LLM 输入和输出,以建立更负责任的 AI 系统

Dr. Leon EversbergTowards Data Science Dr. Leon Eversberg

·发布于Towards Data Science ·阅读时间 9 分钟·2024 年 5 月 13 日

--

Llama Guard。图像由作者使用 Adobe Photoshop 的 AI 图像生成工具创建。

介绍

责任 AI 是一个广泛的术语,自从 ChatGPT 和大型语言模型(LLMs)崛起以来,受到了更多的关注。

一般来说,这个术语意味着 AI 系统不应对人类造成伤害。就 LLMs 而言,这可能意味着例如语言模型不应产生仇恨性回应或协助用户进行非法活动。

Llama Guard 是一个基于 LLM 的聊天机器人安全保护工具,由 Meta 开发。它是 Meta 的Purple Llama责任 AI 项目的一部分。

Meta 最近发布了 Llama Guard 2。这是旧版 Llama Guard 1 的更新,后者是基于 Llama 2 的。

Llama Guard 2 是一个经过训练的 LLM,充当二元分类器,将用户问题以及 LLM 的回应分类为不安全或安全。该模型基于新的 Llama 3,在接受 Meta 的 Llama 3 社区许可协议后,可以从 Hugging Face 自由下载[1]。

利用因果图保护需求预测

原文:towardsdatascience.com/safeguarding-demand-forecasting-with-causal-graphs-591511fc8e0e?source=collection_archive---------6-----------------------#2024-06-28

因果 AI,探索将因果推理融入机器学习

Ryan O'SullivanTowards Data Science Ryan O'Sullivan

·发表于 Towards Data Science ·11 分钟阅读·2024 年 6 月 28 日

--

图片来自 Boris DunandUnsplash

这系列文章的内容是什么?

欢迎来到我的因果 AI 系列文章,在这里我们将探索将因果推理融入机器学习模型。你将会看到在不同商业背景下的一些实用应用。

在上一篇文章中,我们讨论了利用因果 AI 强化营销组合建模。在这篇文章中,我们将继续探讨利用因果图保护需求预测

如果你错过了上一篇关于营销组合建模的文章,请在这里查看:

## 利用因果 AI 强化营销组合建模

因果 AI,探索将因果推理融入机器学习

towardsdatascience.com

介绍

在这篇文章中,我们将深入探讨如何利用因果图保护需求预测(或者说任何预测应用场景)不受影响。

以下领域将被探索:

  • 快速的预测入门。

  • 什么是需求预测?

  • 因果图复习。

  • 因果图如何保护需求预测?

  • 一个 Python 案例研究,说明因果图如何保护你的预测免受虚假相关性的影响。

完整的笔记本可以在这里找到:

[## causal_ai/notebooks/safeguarding demand forecasting with causal graphs.ipynb at main ·…

本项目介绍了因果 AI 及其如何推动商业价值。 - causal_ai/notebooks/safeguarding demand…

github.com](https://github.com/raz1470/causal_ai/blob/main/notebooks/safeguarding demand forecasting with causal graphs.ipynb?source=post_page-----591511fc8e0e--------------------------------)

预测

预测基础知识

时间序列预测是基于历史观察值预测未来值的过程。

用户生成图像

为了开始,有一些术语是值得熟悉的:

  1. 自相关 — 是一个序列与其在不同时间滞后下的前期值的相关性。帮助识别是否存在趋势。

  2. 平稳 — 是指序列的统计属性(例如均值、方差)随时间保持不变。某些预测方法假设序列是平稳的。

  3. 差分 — 是将当前观察值减去前一个观察值,用以将非平稳序列转化为平稳序列。对于假设平稳性的模型,这是一个重要步骤。

  4. 季节性 — 在固定时间间隔内定期重复出现的周期(例如,每日、每周、每年)。

  5. 趋势 — 序列中的长期变化。

  6. 滞后 — 观察值与其前一个值之间的时间步数。

  7. 残差 — 预测值与实际值之间的差异。

  8. 移动平均 — 通过对固定数量的过去观察值进行平均,来平滑短期波动。

  9. 指数平滑 — 将权重应用于过去的观察值,强调最近的数值。

  10. 季节性分解 — 是将时间序列分解为季节性、趋势和残差成分的过程。

用户生成图像

有多种不同的方法可用于预测:

  • ETS(误差、趋势、季节性) — 一种指数平滑方法,用于建模误差、趋势和季节性成分。

  • 自回归模型(AR 模型) — 将序列的当前值建模为其前期值的线性组合。

  • 移动平均模型(MA 模型) — 将序列的当前值建模为过去预测误差的线性组合。

  • 自回归积分滑动平均(ARIMA 模型) — 将 AR 和 MA 模型结合,通过差分使序列平稳。

  • 状态空间模型 — 将时间序列分解为诸如趋势和季节性等独立成分。

  • 层级模型 — 处理如地区等层次结构数据的方法。

  • 线性回归 — 使用一个或多个自变量(特征)来预测因变量(目标)。

  • 机器学习(ML) — 使用更加灵活的算法,如提升法,来捕捉复杂的关系。

如果你想进一步了解这个话题,我强烈推荐以下资源,它被广泛认为是预测的权威指南(以下版本是免费的😀):

[## 预测:原理与实践(第 3 版)

第 3 版

otexts.com](https://otexts.com/fpp3/?source=post_page-----591511fc8e0e--------------------------------)

在使用 Python 应用一些预测模型时,我推荐探索 Nixtla,它有广泛实现的模型列表,并且提供易于使用的 API:

[## Nixtla

开源时间序列生态系统。Nixtla 有 35 个可用的代码库。你可以在 GitHub 上关注他们的代码。

github.com](https://github.com/Nixtla?source=post_page-----591511fc8e0e--------------------------------)

需求预测

预测产品的需求非常重要。

  • 它有助于管理你的库存,避免过度或不足的库存。

  • 它可以保持客户满意,确保产品在客户需要时可用。

  • 降低持有成本并减少浪费是成本效益高的做法。

  • 对于战略规划至关重要。

保持需求预测的准确性至关重要 — 在下一部分,我们将开始思考因果图如何保障我们的预测准确性…

因果图

因果图回顾

我在我的系列文章中讨论过因果图几次,但以防你需要复习,可以查看我第一篇详细讨论因果图的文章:

## 使用因果图回答因果问题

因果 AI,探索将因果推理与机器学习相结合

towardsdatascience.com

因果图如何保障需求预测的准确性?

以下面的图为例,假设我们想预测目标变量。我们发现有 3 个与目标变量相关的变量,因此我们将它们作为特征。为什么包含虚假的相关性会成为问题?我们包含更多特征不是能提高预测准确性吗?

用户生成的图片

嗯,其实不是…

在需求预测中,主要问题之一是数据漂移。数据漂移本身不是问题,如果特征与目标之间的关系保持不变。但是,当关系不再保持不变时,我们的预测准确性就会下降。

但因果图如何帮助我们呢…… 这个概念是,虚假相关性更可能发生漂移,并且一旦发生漂移,它们更可能引发问题。

还不相信吗?好的,那就让我们跳入案例研究吧!

案例研究

背景

你的朋友买了一辆冰淇淋车。他们支付了大量费用给一个顾问,建立了一个需求预测模型。这个模型在最初几个月运作得非常好,但在过去几个月里,你的朋友一直在缺货冰淇淋!他们记得你的职位是“数据什么什么”并来找你寻求建议。

创建案例研究数据

让我先解释一下我是如何为这个案例研究创建数据的。我创建了一个简单的因果图,具有以下特征:

  1. 冰淇淋销售是目标节点(X0)

  2. 海滨访问是冰淇淋销售的直接原因(X1)

  3. 温度是冰淇淋销售的间接原因(X2)

  4. 鲨鱼攻击是一个虚假相关(X3)

用户生成的图像

然后我使用了以下数据生成过程:

用户生成的图像

你可以看到,每个节点都受其过去值、噪声项以及直接父节点的影响。为了生成数据,我使用了来自时间序列因果分析 Python 包 Tigramite 的一个方便模块:

[## GitHub - jakobrunge/tigramite: Tigramite 是一个专注于时间序列数据的因果推断 Python 包…

Tigramite 是一个专注于时间序列数据的因果推断 Python 包。Tigramite 的文档可以在…

github.com](https://github.com/jakobrunge/tigramite/tree/master?source=post_page-----591511fc8e0e--------------------------------)

Tigramite 是一个很棒的包,但这次我不会详细介绍它,因为它值得写一篇独立的文章!在下面的部分,我们使用 structural_causal_process 模块,按照上面的数据生成过程进行操作:

seed=42
np.random.seed(seed)

# create node lookup for channels
node_lookup = {0: 'ice cream sales',
               1: 'coastal visits',
               2: 'temperature',           
               3: 'shark attacks',
 }

# data generating process
def lin_f(x): 
    return x

links_coeffs = {0: [((0, -1), 0.2, lin_f), ((1, -1), 0.9, lin_f)],
                1: [((1, -1), 0.5, lin_f), ((2, -1), 1.2, lin_f)],
                2: [((2, -1), 0.7, lin_f)],
                3: [((3, -1), 0.2, lin_f), ((2, -1), 1.8, lin_f) ],      
}

# time series length
T = 1000

data, _ = toys.structural_causal_process(links_coeffs, T=T, seed=seed)
T, N = data.shape

# create var name lookup
var_names = [node_lookup[i] for i in sorted(node_lookup.keys())]

# initialize dataframe object, specify time axis and variable names
df = pp.DataFrame(data, 
                  datatime = {0:np.arange(len(data))}, 
                  var_names=var_names)

然后我们可以将时间序列可视化:

tp.plot_timeseries(df)
plt.show()

用户生成的图像

现在你已经理解了我如何创建数据,接下来我们回到下一节的案例研究!

理解数据生成过程

你首先要通过分析模型中使用的数据来理解数据生成过程。模型中包含了 3 个特征:

  1. 海滨访问

  2. 温度

  3. 鲨鱼攻击

为了理解因果图,你可以使用 PCMCI(Tigramite 中有很好的实现),这是一种适合因果时间序列发现的方法。我这次不会详细介绍 PCMCI,因为它需要一篇专门的文章。不过,如果你对因果发现不太熟悉,可以参考我之前的文章来获得一个很好的入门:

## 使因果发现能够在现实世界的商业环境中运作

因果人工智能,探索将因果推理集成到机器学习中的方法

[towardsdatascience.com

用户生成的图片

从 PCMCI 输出的因果图如上所示。以下几点显而易见:

  1. 海滨访问是冰淇淋销售的直接原因

  2. 温度是冰淇淋销售的间接原因

  3. 鲨鱼攻击是一个虚假相关性

你质疑任何有常识的人怎么会把鲨鱼攻击作为特征!查看文档后,似乎顾问是使用 ChatGPT 获取了一个待考虑特征的列表,然后使用 autoML 训练了模型。

所以,如果 ChatGPT 和 autoML 认为鲨鱼攻击应该在模型中,难道它不会造成任何危害吗?

预处理案例研究数据

接下来,让我们看看如何对数据进行预处理,以使其适合这个案例研究。为了创建特征,我们需要获取每一列的滞后值(查看数据生成过程以理解为何特征需要是滞后值):

# create dataframne
df_pd = pd.DataFrame(df.values[0], columns=var_names)

# calcuate lagged values for each column
lag_periods = 1 

for col in var_names:
    df_pd[f'{col}_lag{lag_periods}'] = df_pd[col].shift(lag_periods)

# remove 1st obervations where we don't have lagged values
df_pd = df_pd.iloc[1:, :]

df_pd

用户生成的图片

我们可以使用这些滞后特征来预测冰淇淋销售,但在此之前,让我们给虚假相关性引入一些数据漂移:

# function to introduce feature drift based on indexes
def introduce_feature_drift(df, start_idx, end_idx, drift_amount):
    drift_period = (df.index >= start_idx) & (df.index <= end_idx)
    df.loc[drift_period, 'shark attacks_lag1'] += np.linspace(0, drift_amount, drift_period.sum())
    return df

# introduce feature drift
df_pd = introduce_feature_drift(df_pd, start_idx=500, end_idx=999, drift_amount=50.0)

# visualise drift
plt.figure(figsize=(12, 6))
sns.lineplot(data=df_pd[['shark attacks_lag1']])
plt.title('Feature Drift Over Time')
plt.xlabel('Index')
plt.ylabel('Value')
plt.legend(['shark attacks_lag1'])
plt.show()

用户生成的图片

让我们回到案例研究,理解我们所看到的内容。为什么鲨鱼攻击的数量发生了变化?你做了一些研究,发现鲨鱼攻击的原因之一是冲浪人数的增加。最近几个月,冲浪的普及度大幅上升,导致了鲨鱼攻击的增加。那么,这对冰淇淋销售预测有什么影响呢?

模型训练

你决定使用与顾问相同的特征重新创建模型,然后仅使用直接原因:

# use first 500 observations for training
df_train = df_pd.iloc[0:500, :]

# use last 100 observations for evaluation
df_test = df_pd.iloc[900:, :]

# set feature lists
X_causal_cols = ["ice cream sales_lag1", "coastal visits_lag1"]
X_spurious_cols = ["ice cream sales_lag1", "coastal visits_lag1", "temperature_lag1", "shark attacks_lag1"]

# create target, train and test sets
y_train = df_train['ice cream sales'].copy()
y_test = df_test['ice cream sales'].copy()
X_causal_train = df_train[X_causal_cols].copy()
X_causal_test = df_test[X_causal_cols].copy()
X_spurious_train = df_train[X_spurious_cols].copy()
X_spurious_test = df_test[X_spurious_cols].copy()

只使用直接原因训练的模型在训练集和测试集上都表现得很好。

# train and validate model
model_causal = RidgeCV()
model_causal = model_causal.fit(X_causal_train, y_train)
print(f'Coefficient: {model_causal.coef_}')

yhat_causal_train = model_causal.predict(X_causal_train)
yhat_causal_test = model_causal.predict(X_causal_test)

mse_train = mean_squared_error(y_train, yhat_causal_train)
mse_test = mean_squared_error(y_test, yhat_causal_test)
print(f"Mean Squared Error train: {round(mse_train, 2)}")
print(f"Mean Squared Error test: {round(mse_test, 2)}")

r2_train = r2_score(y_train, yhat_causal_train)
r2_test = r2_score(y_test, yhat_causal_test)
print(f"R2 train: {round(r2_train, 2)}")
print(f"R2 test: {round(r2_test, 2)}")

用户生成的图片

然而,当你使用所有特征训练模型时,你会发现模型在训练集上表现良好,但在测试集上却表现不佳。看起来你已经找到了问题所在!

# train and validate model
model_spurious = RidgeCV()
model_spurious = model_spurious.fit(X_spurious_train, y_train)
print(f'Coefficient: {model_spurious.coef_}')

yhat_spurious_train = model_spurious.predict(X_spurious_train)
yhat_spurious_test = model_spurious.predict(X_spurious_test)

mse_train = mean_squared_error(y_train, yhat_spurious_train)
mse_test = mean_squared_error(y_test, yhat_spurious_test)
print(f"Mean Squared Error train: {round(mse_train, 2)}")
print(f"Mean Squared Error test: {round(mse_test, 2)}")

r2_train = r2_score(y_train, yhat_spurious_train)
r2_test = r2_score(y_test, yhat_spurious_test)
print(f"R2 train: {round(r2_train, 2)}")
print(f"R2 test: {round(r2_test, 2)}") 

用户生成的图片

当我们比较两个模型在测试集上的预测时,我们可以看到为什么你的朋友会在冰淇淋库存上出现短缺!

# combine results
df_comp = pd.DataFrame({
    'Index': np.arange(99),
    'Actual': y_test,
    'Causal prediction': yhat_causal_test,
    'Spurious prediction': yhat_spurious_test
})

# melt the DataFrame to long format for seaborn
df_melted = df_comp.melt(id_vars=['Index'], value_vars=['Actual', 'Causal prediction', 'Spurious prediction'], var_name='Series', value_name='Value')

# visualise results for test set
plt.figure(figsize=(12, 6))
sns.lineplot(data=df_melted, x='Index', y='Value', hue='Series')
plt.title('Actual vs Predicted')
plt.xlabel('Index')
plt.ylabel('Value')
plt.legend(title='Series')
plt.show()

用户生成的图片

结语

今天,我们探讨了在预测模型中包含虚假相关性会带来多么严重的影响。最后,让我们总结一些思考:

  • 本文的目的是让你开始思考如何通过理解因果图来改善你的预测。

  • 我知道这个例子有点夸张(我希望常识在这种情况下能起作用!),但它希望能够说明问题。

  • 另一个有趣的点是鲨鱼攻击的系数是负值。这是另一个陷阱,因为从逻辑上讲,我们本来会期望这种虚假相关性是正值。

  • 中长期需求预测非常困难——你通常需要为每个特征建立一个预测模型,以便能够预测多个时间步的未来。 有趣的是,因果图(特别是结构因果模型)非常适合解决这个问题。

如果你想继续深入了解因果 AI,请关注我——在下一篇文章中,我们将探讨如何比较 CUPED 和双重机器学习如何帮助推动你的实验。

SageMaker 与 Vertex AI 在模型推理方面的对比

原文:towardsdatascience.com/sagemaker-vs-vertex-ai-for-model-inference-ef0d503cee76?source=collection_archive---------2-----------------------#2024-06-06

比较 AWS 和 GCP 在机器学习工作流中的全托管服务

Julia TurcTowards Data Science Julia Turc

·发表于 Towards Data Science ·12 分钟阅读·2024 年 6 月 6 日

--

如果你正处在产品开发的激动人心的阶段,准备将首个 AI 模型部署到生产环境中,那么不妨花点时间享受这一张白纸的自由。你即将做出的决策可能会影响公司未来的发展,或者至少会影响未来的技术债务。没有压力 😃 或者至少这是我告诉自己的话,因为我现在正开始为我们公司奠定技术基础。

Storia ,我们构建并部署了大量 AI 模型,因此高效的模型服务至关重要。我们深入研究了两个最主要的服务,SageMakerVertex AI,并在此分享我们的经验。对于我们的使用场景,我们更喜欢 SageMaker。尽管我们尽力保持公正,以便为公司做出最佳决策,但谁知道有哪些偏见在悄悄潜入呢。我曾在 Google 工作多年,我的联合创始人在 Amazon。两家公司都通过它们的初创企业项目向我们提供免费积分,去年 Amazon 还邀请我们加入了它们的 生成性 AI 加速器

TL;DR:SageMaker 在整体表现上占优。如果你从零开始,并且对任何一个云提供商没有偏好(因为免费积分、现有锁定或者对其工具的强烈熟悉),那就选择 SageMaker。然而,如果 GCP 已经让你深深着迷,还是留在那里吧:Vertex AI 提供了足够强有力的竞争。

来自Unsplash的照片(左图:Christian Wiediger,右图:Kai Wenzel

什么是 SageMaker 和 Vertex AI?

SageMakerVertex AI是来自 AWS 和 GCP 的两项竞争服务,用于训练和部署机器学习模型。它们通过云原生组件(虚拟机、加速器和存储)来简化构建和部署 ML 模型的过程。它们的目标是防止开发者手动并反复设置在大多数机器学习工作流中常见的操作。

例如,构建训练管道需要一些通用步骤:将训练数据存储到一个存储系统中,启动一个或多个启用了加速器的虚拟机,确保它们不被 I/O 瓶颈限制(即,更多时间花费在传播梯度而不是读取训练数据上),定期进行检查点保存和评估等。

SageMaker 和 Vertex AI 使开发者仅通过配置文件或几个 bash 命令便可设置如此复杂的工作流。其结果是一个自愈系统,能够在无需大量监控的情况下完成任务。这就是为什么它们常被称为完全托管服务的原因。

SageMaker 和 Vertex AI 在模型推理中的应用

在本文中,我们特别从模型推理的角度比较了 SageMaker 和 Vertex AI。在这里,它们的主要价值主张是确保(a)推理服务器始终保持运行,以及(b)根据传入的流量自动扩展。后者在今天的大型模型时代尤其重要,这些模型需要强大的加速器。由于 GPU 稀缺且价格昂贵,我们无法承受它们处于闲置状态,因此需要根据流量的多少来动态地启用或停用它们。

虽然本文重点讨论推理,但值得注意的是,这些服务涵盖了工作流的许多其他部分。特别是,除了支持模型训练外,它们还包括以笔记本为中心的功能,供数据科学家分析训练数据(参见SageMaker NotebooksVertex AI Notebooks)。

开发者工作流

在使用 SageMaker 或 VertexAI 进行模型部署时,开发者需要执行以下三个步骤:

  1. 创建模型。

  2. 配置端点。

  3. 将模型部署到端点。

这些操作可以通过网页界面、特定于云平台的 CLI 或支持多种编程语言的云平台 SDK 来执行。

创建模型

创建模型归结为提供一个 Docker 镜像,用于一个 HTTP 服务器,响应以下请求:(1)将模型工件加载到内存中,(2)进行预测,以及(3)健康检查。除了这一契约之外,SageMaker 和 Vertex AI 对它们服务的内容相对不做限制,将模型视为需要保持运行并响应预测请求的黑盒。

SageMaker 和 Vertex AI 都提供适用于各种机器学习框架(如 PyTorch、TensorFlow、Scikit-learn 等)和内置算法的预构建镜像。例如,如果你只是想使用 SDXL 1.0 运行文本到图像的生成,你可以从 Amazon MarketplaceGoogle Cloud’s Model Garden 获取镜像。或者,它们也都支持自定义镜像,允许开发者编写自己的服务逻辑并定义自己的运行时环境,只要容器暴露一个包含上述三个端点的 HTTP 服务器。

配置端点

端点配置将模型与一组运行时约束关联起来:运行的机器和加速器类型、要消耗的资源的最小和最大数量,以及如何处理自动伸缩(监控哪个指标,以及超过什么阈值触发)。

部署模型

一旦这些配置完成,开发者就可以发出最终的绿灯。然后,SageMaker 和 Vertex AI 会分配所需的机器,运行容器,并调用推理服务器暴露的初始模型加载方法。之后,在容器的整个生命周期内,它们会定期进行健康检查,并在必要时重新启动容器。根据流量情况,它们会进行伸缩,以尽量减少资源消耗并最大化吞吐量。

SageMaker 和 Vertex AI 如何比较?

结论:总体而言,SageMaker 更胜一筹。如果你是从零开始,并且对云服务提供商没有特别偏好(没有免费信用、现有的锁定或对工具的熟悉),就选择 SageMaker。然而,如果 GCP 已经让你深深着迷,那就留在 GCP:Vertex AI 在这场竞争中表现得相当不错。

很多时候,类似问题的答案是“视情况而定”。但这次情况不同。至少在模型服务的背景下,SageMaker 在大多数维度上远远领先。与 Vertex AI 相比,SageMaker 通常功能更丰富且更灵活,同时不会偏离其让机器学习工作流程简便的初衷。再加上 AWS 的普遍客户至上(这转化为更快的客户支持和更多的初创公司免费信用)使得 SageMaker 总体上是一个更好的选择。

也就是说,如果你的使用案例不太复杂,Vertex AI 可能足够好了。如果你有充分的理由选择 GCP(也许你已经被锁定,或者在那里有更多的免费信用),Vertex AI 可能会非常适合你。

自动扩展

与 Vertex AI 相比,SageMaker 在配置自动扩展时提供了更多的灵活性。它可以基于 QPS(每秒查询数)而非资源使用情况进行扩展。

在模型推理的背景下,自动扩展是像 SageMaker 和 Vertex AI 这样的完全托管服务的主要价值主张之一。当流量增加时,它们会分配额外的机器;当流量减少时,它们会移除不必要的实例。这在当今世界尤其重要,因为大多数模型都运行在过于昂贵的加速器上,不能让它们处于空闲状态。然而,根据流量调整分配的资源是一项复杂的任务。

为什么自动扩展如此困难?

一个主要的障碍是扩展不是即时的。当需要额外的 GPU 时,系统将配置一个新的虚拟机,下载 Docker 镜像,启动容器,并下载模型工件。这可能需要 3 到 8 分钟不等,具体取决于你的部署细节。由于它无法快速响应流量波动,系统需要通过利用过去的信息提前预测流量激增。

SageMaker 如何在自动扩展中获胜

SageMaker 提供三种类型的自动扩展(见文档):(1) 目标追踪(追踪指定的指标—如 CPU 使用率—当超出预定阈值时扩展),(2) 步进扩展(支持基于多个追踪指标的更复杂逻辑),以及 (3) 定时扩展(允许你硬编码特定时间段,当你预期流量增加时使用)。

推荐的方法是目标追踪:你可以从Amazon CloudWatch中选择任何一个指标(甚至可以定义一个自定义指标!),以及应触发扩展的值。有一些指标反映了资源的利用率(例如 CPU / GPU 内存或周期),也有一些指标衡量流量(例如InvocationsPerInstanceApproximateBacklogSizePerInstance)。

相比之下,Vertex AI 提供的控制选项要少得多(请参见文档)。唯一的选项是目标追踪,仅限于两个指标:CPU 利用率和 GPU 占用周期。需要注意的是,没有直接反映流量的指标。当你的模型不能并发处理多个请求时(即不支持批处理或多线程),这会非常不方便。在这种情况下,CPU 或 GPU 处于两种模式之一:要么是 0% 利用率(没有请求),要么是固定的 x% 利用率(一个或多个请求)。在这种二元现实中,CPU 或 GPU 的使用率并不能反映真实的负载,也不是一个好的扩展触发器。你的唯一选择是在利用率介于 0%x% 之间时进行扩展,且有一个额外的复杂性,即 x 是依赖于加速器的:如果你从 NVIDIA T4 切换到 A100,你需要手动调整阈值。

增添一些戏剧性的是,Vertex AI 无法扩展到零实例(见问题);至少需要保持一台机器在运行。然而,SageMaker 允许完全移除其异步端点的所有实例(下一节将进一步讨论此问题)。

也许 GCP 唯一的优势是它允许你在其 Web 控制台中轻松跟踪自动扩展行为,而 AWS 在其 Web 门户上完全没有提供相关信息(你只能通过 bash 命令循环来监控它)。

同步预测与异步预测

SageMaker 支持同步调用(会阻塞,直到预测完成)和异步调用(会立即返回一个 URL,预测结果准备好后可以通过该 URL 获取)。而 Vertex AI 仅支持前者。

默认情况下,SageMaker 和 Vertex AI 的端点都是 同步的 —— 调用者会被阻塞,直到预测完成。虽然这种客户端/服务器通信模型是最容易理解的,但当模型存在高延迟时可能会不方便。这两个服务都将在 60 秒后超时:如果单次模型调用超过此时间,SageMaker / Vertex AI 会返回一个 超时 响应。需要注意的是,这包括等待时间。假设客户端同时发出两个请求,每个请求需要 45 秒来解决。如果你的模型不支持并行处理(例如通过批处理),那么第二个请求会超时(因为它需要 90 秒来解决)。

为了绕过这个问题,SageMaker 支持 异步端点 —— 它们会立即通过 S3 URL 响应客户端;当模型输出完成时,将被放置在该位置。客户端需要轮询 S3 位置,直到该结果可用。由于请求被放入一个(尽力而为的)FIFO 队列,超时时间被延长至 15 分钟(而不是 60 秒)。不幸的是,Vertex AI 不支持异步端点;如果不希望请求在 60 秒后被丢弃,您需要实现自己的排队和重试逻辑。

请注意,SageMaker 和 Vertex AI 都支持 批量预测,它们是异步的。这些预测不适合实时流量,而是适合批处理作业(即对整个数据集进行离线预测)。

多模型端点(MMEs)

SageMaker 完全支持共享资源的多模型端点。Vertex AI 的多模型端点仅共享 URL,并不会带来任何成本节省。

有时候你需要部署的不止一个模型。也许你有一个完整的管道,每个步骤都需要不同的模型,比如 基于语言的图像编辑。或者,也许你有一组独立的模型,并且它们的使用遵循幂律分布(其中 2-3 个模型经常被使用,而长尾部分则偶尔使用)。为每个模型分配一台专用机器可能会变得过于昂贵。为此,SageMaker 提供了 多模型端点,它们在模型之间共享同一个容器和资源。这些模型不需要全部装入内存;SageMaker 可以根据当前请求的模型动态调入调出。这样做的折衷是偶尔的冷启动(即,如果请求的模型不在内存中,客户端将不得不等待,直到 SageMaker 调入该模型)。当你有一组很少使用的模型时,这种情况是可以接受的。

SageMaker 多模型端点的一个限制是它要求所有模型使用相同的框架(如 PyTorch、Tensorflow 等)。然而,多容器端点缓解了这一限制。

尽管 Vertex AI 正式允许你将多个模型部署到一个端点(参见文档),但资源实际上是与模型相关联的,而不是与端点相关联的。因此,你无法像在 AWS 上那样共享资源来降低成本,但你可以方便地将流量从模型 v1 平滑过渡到模型 v2,而无需更改端点 URL。真正的资源共享仅适用于使用预构建容器的 TensorFlow 模型,这有些限制(参见文档)。

配额和 GPU 可用性

在配额和加速器可用性方面,两个提供商各有其独特之处,但它们面临着相同的根本挑战:GPU 成本昂贵。

  • 在 GCP 上,你可以获得(并为此付费)一个 A100 GPU。但 AWS 强制要求你一次租用 8 个(这可能会根据你的需求显得有些过剩)。这种情况仅适用于 A100 GPU,并不适用于低层次的 GPU;你可以自由请求任何其他类型的单个 GPU。

  • 在 GCP 内,VM 的配额可以用于 Vertex AI。换句话说,你只需要请求一次那个热门的 A100 GPU。然而,AWS 会将 EC2 和 SageMaker 的配额分别管理(了解更多关于AWS 服务配额的信息),所以一定要确保请求正确服务的配额。

  • 尽管我们可以从两家提供商处获得专门的客户支持(GCP 通过他们的初创企业计划,AWS 通过他们的生成式 AI 加速器),但 AWS 的代表通常响应更快,这也意味着配额请求会更快解决。

限制

在前面的部分中,我们讨论了两种服务彼此之间的限制。然而,两者之间也存在一些共同的限制:

  1. 负载限制。这两种服务的模型响应负载都有最大大小限制:Vertex AI 公共端点为 1.5 MB,SageMaker 同步端点为 6 MB,异步端点为 1 GB(source 1source 2)。

  2. 超时。预测请求最终会被两种服务丢弃:Vertex AI 的超时为 60 秒,SageMaker 的同步端点为 60 秒,SageMaker 的异步端点为 15 分钟(source 1source 2)。

  3. 缩减到 0。Vertex AI 和同步的 SageMaker 端点不支持此功能,但 SageMaker 的异步端点支持。

  4. 附加共享文件系统。SageMaker 和 Vertex AI 都不允许挂载外部文件存储系统(AWS 中的EFSFSx以及 GCP 中的Filestore)。这对于存储和共享跨服务器副本的模型工件,或实现像这个这样的技巧以节省 Docker 镜像空间(并减少启动时间)可能很有用。需要注意的是,它们确实支持访问常规对象存储(S3 和 GCS)。

总结

已经说了很多,这里有一个简洁的表格,将所有内容压缩在一起:

图片由作者提供。✅ = 支持,❌ = 不支持,⚠️ = 有限支持。

替代方案

SageMaker 和 Vertex 是最受欢迎的模型服务解决方案,能够满足大多数用例。如果你对其中任何一个不满意,那么你可能需要做一些自我反思。你是想要更多的灵活性吗?你是想要以牺牲更多灵活性为代价的简化吗?还是你只是想要通过减少冷启动来降低成本?

如果你渴望灵活性,那么可能无法避免使用Kubernetes——亚马逊的EKS和谷歌的GKE是托管的 Kubernetes 服务,可能是一个不错的起点。额外的优势是 Kubernetes 是与云平台无关的,因此你可以在 AWS / GCP / Azure 上重复使用相同的配置,配合像Terraform这样的基础设施自动化工具。

相比之下,如果你追求简便,有一些服务如ReplicateBasetenModalMystic,它们比 SageMaker 和 Vertex 多了一层抽象。它们有不同的权衡;例如,Replicate 让你在实验阶段极其容易地创建模型端点,但在冷启动方面存在显著的困难。

联系方式

如果你正在考虑高效的模型服务,我们很想听听你的意见!你可以在 Twitter 上找到我 @juliarturc 或在 LinkedIn 联系我。

深入阅读

SAM:Segment Anything Model

原文:towardsdatascience.com/sam-segment-anything-model-4b25a47245f2?source=collection_archive---------4-----------------------#2024-01-22

快速通过 SAM 定制您的产品落地页

Rafael GuedesTowards Data Science Rafael Guedes

·发表于Towards Data Science ·10 分钟阅读·2024 年 1 月 22 日

--

介绍

Transformers 模型已经广泛应用于自然语言处理的场景,但它们也可以应用于人工智能的其他多个领域,例如时间序列预测或计算机视觉。

一些将 Transformers 模型应用于计算机视觉的出色例子包括:用于图像生成的 Stable Diffusion,用于目标检测的 Detection Transformer,或者更近期的,用于图像分割的 SAM。这些模型带来的巨大好处是,我们可以使用文本提示来操控图像,而无需太多努力,只要有一个好的提示。

这类模型的应用场景是无穷无尽的,特别是如果你在一家电子商务公司工作的话。一种简单、耗时且昂贵的应用场景是从拍摄物品到将其发布到网站销售的过程。公司需要拍摄物品,去除使用的道具,并且在发布物品之前,最终需要对道具留下的空洞进行填充。如果整个过程可以由 AI 自动化,我们的人力资源只需处理复杂的用例并审核 AI 完成的任务,那该有多好?

在这篇文章中,我将详细解释 SAM,一个图像分割模型,并展示其在一个假设用例中的实现,假设我们想进行 A/B 测试,以了解哪种背景类型能提高转化率。

从多变量分布中采样:从统计模型到生成模型

原文:towardsdatascience.com/sampling-from-multivariate-distributions-from-statistical-to-generative-modeling-0177e55a9061?source=collection_archive---------9-----------------------#2024-07-15

连接经典统计方法和前沿生成式人工智能模型,用于从多变量分布中采样

Wencong Yang, PhDTowards Data Science Wencong Yang, PhD

·发布于 Towards Data Science ·阅读时长:8 分钟·2024 年 7 月 15 日

--

来源:Ideogram.

背景

从多变量分布中采样合成数据对于理解数据之间的相互依赖、促进统计推断以及量化数据分析中的不确定性至关重要。这一过程在金融、工程、医学、环境科学和社会科学中得到了广泛应用。该过程涉及使用数学模型拟合数据的结构,并根据拟合的分布生成新的样本。联合多变量分布的建模在统计学领域已有悠久历史。在简单的情况下,数据可以通过使用预定义的统计分布进行建模,这些分布具有明确的数学描述,例如多元高斯分布和 Copula 函数——这两种是经典的统计方法。然而,随着数据维度和依赖关系的复杂性增加,传统方法已经无法满足需求。同时,现代生成式人工智能技术,如生成对抗网络(GANs)和扩散模型,展示了其潜力。

卫星如何看见不可见的熔岩流和活跃的野火?(Python)

原文:towardsdatascience.com/satellites-can-see-invisible-lava-flows-and-active-wildires-but-how-python-371915464d1c?source=collection_archive---------5-----------------------#2024-02-20

可视化捕捉到的卫星图像,显示火山和野火的不同光谱带

Mahyar Aboutalebi, Ph.D. 🎓Towards Data Science Mahyar Aboutalebi, Ph.D. 🎓

·发表于 Towards Data Science ·14 分钟阅读·2024 年 2 月 20 日

--

作者使用不同光谱带可视化捕捉到的卫星图像,显示火山和野火的情况

目录

  1. 🌟 介绍

  2. 🔍 Sentinel-2(光谱带)

  3. 🌐 下载 Sentinel-2 图像

  4. ⚙️ 处理 Sentinel-2 图像(裁剪和重采样)

  5. 🌋 Sentinel-2 图像可视化(火山)

  6. 🔥 Sentinel-2 图像可视化(野火)

  7. 📄 结论

  8. 📚 参考文献

🌟 介绍

如你所知,我们的眼睛只能看到可见光区域(蓝色、绿色和红色光谱带)。然而,当光线照射到物体上并反射时,它包含了其他光谱区域的信息,例如红外线。红外光可以有效穿透并通过浓密的气体,如烟雾,从而提供烟雾下的清晰视图。然而,我们的眼睛无法看到红外区域的物体,不像某些动物(如蛇)能够在视觉中看到一部分红外线。过去几十年,红外传感器技术有了显著的进展……

使用引导生成法构建 LLM 应用,节省时间和精力

原文:towardsdatascience.com/save-time-and-effort-when-building-llm-apps-using-guided-generation-05f7237a3512?source=collection_archive---------13-----------------------#2024-07-29

使用 Guidance 使 LLM 输出符合你的期望

Youness MansarTowards Data Science Youness Mansar

·发表于 Towards Data Science ·8 分钟阅读·2024 年 7 月 29 日

--

图片由 Markus Spiske 提供,来自 Unsplash

大型语言模型(LLM)是强大的工具,但它们有时会表现得有些不可预测。有时,它们会给出错误的答案,其他时候,它们的回答格式完全不对。这看起来可能不是什么大问题,但当你用 LLM 来分析数据、分类信息,或与需要特定结构的其他工具协作时,确保格式正确是至关重要的。

你可以通过巧妙的提示和示例来尝试引导 LLM 朝正确的方向发展,但即便是这些方法也不是万无一失的。一个更极端的解决方案是通过大量按你要求格式化的数据来微调 LLM。虽然有效,但这种方法可能会消耗大量资源。

那么,解决的办法是什么呢?引导生成!这一技术允许你在不需要重新训练的情况下,通过引导来影响 LLM 的输出,将其约束到所需的格式。在这篇文章中,我们将探讨微软的“Guidance”库,这是最受欢迎的引导生成工具之一,并了解它如何帮你节省时间,使 LLM 的交互更加可预测。我们将探讨几个实际应用,包括:

  • 文本分类:自动将文本分类到预定义的组别中。

高效快速保存 Pandas DataFrame — Parquet 与 Feather 与 ORC 与 CSV

原文:towardsdatascience.com/saving-pandas-dataframes-efficiently-and-quickly-parquet-vs-feather-vs-orc-vs-csv-26051cc98f2e?source=collection_archive---------4-----------------------#2024-11-27

优化

速度、内存、大小和便利性。哪种存储方法最好?

Mike ClaytonTowards Data Science Mike Clayton

·发布于 Towards Data Science ·阅读时间 13 分钟·2024 年 11 月 27 日

--

混合数据的输出文件大小 — 图片来源:作者

随着数据量的不断增加,必然需要高效且快速地存储和重新加载这些数据。

CSV 格式长期以来一直是首选的存储格式。然而,现有的替代方案要好得多,它们专门设计用于直接处理表格数据的存储和高效重新加载。

那么,如果你仍然使用 CSV 格式来存储数据表,你到底会损失多少?你应该考虑哪种替代方案?

介绍

当谈到存储表格数据时,理想的情况是:

  • 快速写入

  • 快速读取

  • 低内存使用

  • 低存储需求

  • 压缩的好选择

一个仅读取部分数据而不加载整个数据集的选项,也是上述内容的一个很好的补充。

因此,上述列出的内容将成为测试一些更广泛使用的存储方法的基础。

ChatGPT 并不总是能理解 SQL,但这个 Python 工具可以

原文:towardsdatascience.com/say-goodbye-to-sql-headaches-with-this-python-tool-75099f5ff33d?source=collection_archive---------2-----------------------#2024-01-15

图片来自2023852Pixabay

SQLGlot — 一站式 SQL 解析与操作 Python 库

Christopher TaoTowards Data Science Christopher Tao

·发表于Towards Data Science ·10 分钟阅读·2024 年 1 月 15 日

--

如果你是一个日常处理数据的开发者或分析师,SQL 查询将是许多不同职位所需的基本技能之一。记得当我在大学当辅导员时,有一个学生抱怨 SQL 是最糟糕的编程语言。嗯,关于 SQL 是否算作一种“编程语言”,我猜这可能会引起争议。不过,毫无疑问,SQL 在语法上是相当结构化和严格的。因此,相对来说,实现一个工具来完全理解它的意义会更容易。并不需要 ChatGPT 😃

在这篇文章中,我将介绍一个库,它帮助我们解析 SQL 查询,甚至能够程序化地构建查询。如果你不知道为什么我们需要这样做,那么继续阅读下面的内容对你来说会更重要。

1. 安装

图片来自AndreasAuxPixabay

一如既往的无聊部分,不过我还是得有这一部分。要安装这个库,只需运行以下命令。

pip install sqlglot

可扩展的文档 OCR 管道,使用 AWS

原文:towardsdatascience.com/scalable-ocr-pipelines-using-aws-88b3c130a1ea?source=collection_archive---------11-----------------------#2024-05-30

对三种不同 OCR 管道模式及其优缺点的调查

Viktor LeanderssonTowards Data Science Viktor Leandersson

·发布于 Towards Data Science ·阅读时长 10 分钟·2024 年 5 月 30 日

--

图片由作者提供

OCR(光学字符识别)系统通常被视为通过提取文档内容将其转化为计算机可读格式。然而,OCR 是识别图像中仅包含文本的字符的过程,因此,任何包含更多信息的图像(或未正确旋转、倾斜等)都需要一些预处理和后处理服务,以便获得有意义的输出。实际上,大多数输入 OCR 管道的图像既不是纯文本,也没有正确旋转。相反,图像可能由结构化和非结构化信息组成,文本和图像并排出现。这意味着 OCR 管道必须具备旋转输入文档、识别兴趣区域(RoI)、检测文本、执行 OCR 以及验证结果的能力。

本文探讨了如何构建模型管道,以创建一个适用于生产的 OCR 管道,用于文档文本提取。

5 个步骤

OCR 过程可以分为以下五个步骤:

  1. 旋转与倾斜校正 — 该步骤旋转并校正图像的倾斜,以创建一个正立且矩形的页面。经典的计算机视觉技术是…

唇动同步仅需规模数据?

原文:towardsdatascience.com/scale-is-all-you-need-for-lip-sync-0c571423f60f?source=collection_archive---------5-----------------------#2024-06-07

阿里巴巴的 EMO 和微软的 VASA-1 简直太强大了。让我们来拆解它们是如何工作的。

Jack SaundersTowards Data Science Jack Saunders

·发布于Towards Data Science ·11 分钟阅读·2024 年 6 月 7 日

--

AI 研究的进展正在以指数级速度加快,这已经不是什么秘密。过去几年中,一个最大的趋势是使用变换器模型来利用大规模数据集。看起来这个趋势终于到达了唇动同步模型领域。阿里巴巴的 EMO 发布为此树立了先例(我的意思是,看看那些请求发布代码的 200 多个 GitHub 问题)。但是,随着微软的 VASA-1 上个月发布,这个门槛被进一步提高了。

来自VASA-1和 EMO 的演示视频。所有图片的版权归各自作者所有。

它们受到了很多关注,但到目前为止,没有人讨论过它们的具体做法。从表面上看,它们几乎是完全相同的作品(有意为之)。两者都使用单一图像,并通过音频来动画化它。两者都使用扩散模型,并且都通过规模的利用来产生惊人的效果。但实际上,背后有一些不同之处。本文将简要探讨这些模型如何运作。我们还将探讨这些论文的伦理考虑,鉴于它们显然存在被滥用的潜力。

数据

一个模型的好坏,取决于它训练所使用的数据。或者更简洁地说,垃圾进,垃圾出。大多数现有的唇动同步论文使用了一到两个相对较小的数据集。我们讨论的这两篇论文在这方面绝对超越了所有竞争对手。让我们来看看它们使用了什么。阿里巴巴在 EMO 中提到:

我们从互联网收集了大约 250 小时的谈话头像视频,并使用 HDTF [34]和 VFHQ [31]数据集对我们的模型进行了补充训练。

至于他们所说的额外 250 小时收集的数据具体意味着什么,目前还不清楚。然而,HDTF 和 VFHQ 是公开可用的数据集,所以我们可以进一步分析它们。HDTF包含了 300 个受试者的 16 小时 720 到 1080p 的视频数据。VFHQ没有明确提到数据集的小时数,但它有 15,000 个视频片段,数据量为 1.2TB。如果我们假设每个视频片段平均至少有 10 秒钟,那么这相当于增加了 40 小时的数据。这意味着 EMO 使用了至少 300 小时的数据。对于 VASA-1,微软表示:

该模型在 VoxCeleb2 [13]和我们收集的另一个高分辨率谈话视频数据集上进行训练,该数据集包含大约 3.5K 个受试者。

再次强调,作者对于数据集的大部分内容保持保密。VoxCeleb2是公开的。通过查看相关论文,我们可以看到它包含 2442 小时的数据(这不是笔误),跨越 6000 个受试者,尽管其分辨率低于我们提到的其他数据集(360 到 720p)。这大约是 2TB。微软使用了一个包含 3.5k 个额外受试者的数据集,我怀疑这些受试者的质量更高,使得模型能够生成高质量的视频。如果我们假设这些视频至少是 1080p,并且其中一些是 4k 分辨率,且与 VoxCeleb2 的数据集时长相似,那么我们可以预期另有 5 到 10TB 的数据。

接下来,我将做一些有根据的猜测:阿里巴巴可能使用了 300 小时的高质量视频(1080p 或更高),而微软使用了大约 2500 小时的低质量视频,可能还使用了 100 到 1000 小时的非常高质量视频。如果我们尝试估算数据集的存储空间大小,我们发现EMO 和 VASA-1 每个使用大约 10TB 的面部视频数据来训练他们的模型。以下是一些比较,查看下表:

几个最先进的谈话头像生成模型的数据集大小估算比较。图像来自我。(CC-BY)

模型

这两个模型都利用扩散和变换器来处理海量数据集。然而,它们在工作原理上有一些关键差异。

VASA-1

我们可以将 VASA-1 分解为两个组成部分。一个是图像生成模型,它接收一些面部表情和姿势的潜在表示,并生成一个视频帧。另一个是从音频输入生成这些潜在姿势和表情向量的模型。图像生成模型

VASA-1 模型图。左侧显示音频到潜在向量的生成,右侧显示图像生成模型。VASA-1 论文中的图示。 (CC-BY)

图像生成模型

VASA-1 在很大程度上依赖于面部的 3D 体积表示,建立在三星之前的工作MegaPortraits的基础上。这里的想法是首先估计源面部的 3D 表示,使用预测的源姿态对其进行变形,使用源和目标表情的知识对表情进行编辑,并在这个规范空间中完成编辑后,使用目标姿态将其重新变形。

MegaPortraits 方法的模型图。v 是面部的体积表示,e 是身份的描述符,R 和 t 是姿态(旋转和位移),z 是面部表情编码。来自 MegaPortraits 论文。(CC-BY)

更详细地说,这个过程如下:

  • 获取源图像(上图中的男子)并预测一个简单的 1D 向量,代表此人。

  • 同时预测一个 4D 张量(宽度、高度、深度、RGB),作为该人物的体积表示。

  • 预测源图像和驱动图像(上面的女人)的姿态和面部表情。请注意,只有姿态估计是预训练的,其他所有内容都是从零开始训练的。

  • 使用神经网络创建两个变形场。一个使用我们对其身份、姿态和面部表情的估计,将男子的体积表示转换到规范空间(这只是意味着正面、表情中立)。另一个使用对女性姿态和表情的估计,以及男子的身份,将他的规范 3D 面部转换为具有姿态的 3D 面部。

  • 将有姿态的人脸“渲染”回 2D 图像。

有关他们是如何具体实现这一点的,即如何将图像投影到 3D 空间,如何实现变形以及如何从 3D 体积中生成 2D 图像,请参阅MegaPortraits 论文。

目前,我们可以将这个高度复杂的过程简化为:想象一个模型,以某种方式编码源输入,然后获取姿态和表情的参数,基于这些生成图像。

音频到潜在空间生成

现在,我们有了一种从一系列表情和姿态潜在编码生成视频的方法。然而,与 MegaPortraits 不同,我们不希望通过他人的表情来控制我们的视频。相反,我们希望仅通过音频来控制。为此,我们需要构建一个生成模型,输入音频,输出潜在向量。这个模型需要能够处理大量数据,具备同步口型功能,并且能够生成多样化且逼真的头部动作。进入扩散 transformer。如果你对这些模型不熟悉,我不怪你,这里有很多进展需要跟上。我可以推荐以下文章:

## 扩散 Transformer 解释

探索将 transformers 引入图像生成中的架构

towardsdatascience.com

简而言之,扩散变压器(DiTs)将传统的 UNET 替换为图像扩散模型中的变压器。这种切换使得能够在具有任何结构的数据上进行学习,得益于标记化,并且已知它在大数据集上扩展得非常好。例如,OpenAI 的 SORA模型被认为是一个扩散变压器。

DiT 模型架构来自于可扩展的扩散模型与变压器

这个过程的想法是从与潜在向量形状相同的随机噪声开始,逐步去噪以生成有意义的向量。然后,这个过程可以根据额外的信号进行条件化。对于我们的目的来说,这些信号包括音频,通过Wav2Vec2提取成特征向量(详细了解可以参考FaceFormer)。还使用了其他信号。我们不会详细讨论,但它们包括眼睛凝视方向和情绪。为了确保时间稳定性,先前生成的运动潜在编码也被用作条件信号。

EMO

来自EMO 论文的模型图(CC-BY)

EMO 在其生成过程中采用了稍微不同的方法,尽管它仍然依赖于扩散模型的核心。模型图看起来有些拥挤,因此我认为最好将其分解成更小的部分。

使用 Stable Diffusion

首先需要注意的是,EMO 大量使用了预训练的 Stable Diffusion 1.5 模型。目前,在视觉领域有一个明显的趋势,即在此模型基础上进行构建。在上面的图示中,参考网络和骨干网络都是 SD1.5 UNET 架构的实例,并且是用这些权重进行初始化的。虽然细节不多,但可以推测 VAE 的编码器和解码器也是来自 Stable Diffusion。VAE 组件被冻结,这意味着 EMO 模型中执行的所有操作都是在该 VAE 的潜在空间中完成的。使用相同的架构和相同的初始权重非常有用,因为它允许从一个网络中轻松提取中间层的激活,并将其用于另一个网络(它们在两个网络中大致表示相同的内容)。

第一阶段的训练

第一阶段的目标是获得一个单图像模型,能够根据该人物的参考帧生成一个新颖的图像。这是通过使用扩散模型来实现的。可以使用一个基本的扩散模型来生成人物的随机图像。在第一阶段,我们希望以某种方式将生成过程与身份条件化。作者实现这一点的方法是,通过使用参考网对人物的参考图像进行编码,并将每一层的激活信息引入到进行扩散的主干网络中。请参见下方的(画得很差的)示意图。

第一阶段简化版的基本示意图。图像由我制作。(CC-BY)

在这一阶段,我们已经有了一个模型,能够根据某个人的单张图片生成随机的帧。现在,我们需要以某种方式对其进行控制。

训练第二阶段

我们希望使用两个信号来控制生成的帧:运动和音频。音频部分较容易解释,因此我将首先讲解这一部分。

EMO 的主干网络放大图。来自EMO 论文。 (CC-BY)

  • 音频: 和 VASA-1 一样,音频以 wav2vec2 特征的形式进行编码。这些特征通过交叉注意力机制被融入主干网络中。这种交叉注意力替代了 Stable Diffusion 1.5 模型中已经存在的文本提示交叉注意力。

  • 运动: 通过运动帧添加运动信息,在预测时间 t 的帧时,前 n 帧提供了运动的上下文。运动帧与参考帧以相同的方式进行编码。参考网的中间特征激活用于条件化主干模型。这些运动参考激活的引入是通过一个专门设计的交叉注意力层完成的,来自AnimateDiff。从这些 n 帧中,下一帧 f 会通过扩散模型进行预测。

除此之外,还使用了两个其他组件。一个提供了一个掩码,掩码是通过对训练视频中的所有边界框取并集得到的。这个掩码定义了视频中可以被修改的区域。另一个是添加了一个速度条件。姿势速度被划分为几个桶(例如:慢速、中速、快速),并也包含在内。这使得我们能够在推理时指定运动的速度。

推理

现在模型能够接受以下内容并生成一组新的帧:

  • 参考帧

  • 之前的 n 帧

  • 音频

  • 头部运动速度

  • 可以被改变的像素边界框

对于第一帧,虽然没有明确说明,但我假设参考帧会被重复,并作为最后 n 帧传递。在这一点之后,模型是自回归的,输出将作为前一帧输入。

伦理讨论

这些工作的伦理影响当然非常重要。它们只需要一张图片就能创造出非常真实的合成内容。这很容易被用来歪曲他人形象。鉴于近期关于OpenAI 未经 Scarlett Johansen 同意使用听起来非常像她声音的事件,这一问题尤为相关。两组的处理方式截然不同。

EMO

EMO 论文中的讨论非常欠缺。该论文没有讨论伦理影响或提出任何防止滥用的方法。项目页面仅写道:

“该项目仅用于学术研究和效果展示”

这似乎是一次非常弱的尝试。此外,阿里巴巴还提供了一个 GitHub 仓库,可能会公开代码。考虑这样做的利弊是很重要的,正如我们在上一篇文章中讨论的那样。 总体来看,EMO 的作者并没有对伦理问题给予太多考虑。

VASA-1

VASA-1 的作者采取了更全面的方法来防止滥用。他们在论文中专门有一节讨论此问题,强调了深伪检测中的潜在应用以及正面效益。

来自 VASA-1 的伦理部分。图片取自arxiv 预印本

除此之外,他们还包含了一项相当有趣的声明:

注意:本页上的所有人像图像都是由 StyleGAN2 或 DALL·E-3 生成的虚拟、非真实身份(蒙娜丽莎除外)。我们正在探索虚拟互动角色的视觉情感技能生成,而非模仿任何现实世界中的人物。这仅仅是一个研究示范,并没有产品或 API 发布计划。

这种方法实际上是微软在一些论文中开始采用的。他们只使用合成人物来创建合成视频,并且不公开任何模型。这样做可以防止可能的滥用,因为没有编辑任何真实人物。然而,这也引发了一个问题:创造此类视频的能力集中在具有基础设施训练这些模型的大型科技公司手中。

进一步分析

在我看来,这项工作开启了一系列新的伦理问题。虽然以前可以创建假视频,但通常需要几分钟的数据来训练模型。这在很大程度上将潜在的受害者限制为已经创造大量视频的人。尽管这为制造政治虚假信息提供了可能,但这些限制帮助抑制了一些其他应用。例如,如果某人创建了大量视频,就可以通过分析其常见内容(他们通常谈论什么,持有什么观点等)来辨别出不符合其风格的视频。如果只使用一张图片,这变得更加困难。而且,任何人都可能成为这些模型的受害者。即使是一个拥有个人头像的社交媒体账户,也足够作为建立个人模型的数据。

此外,作为一种不同类型的“深度伪造”,目前对如何检测这些模型的研究并不多。以前可能有效的用于捕捉视频深度伪造模型的方法将变得不可靠。

我们需要确保这些模型带来的危害得到限制。微软通过限制访问权限并仅使用合成人物的做法在短期内有所帮助。但从长远来看,我们需要对这些模型的应用进行强有力的监管,并且需要可靠的方法来检测它们生成的内容。

结论

VASA-1 和 EMO 都是非常出色的论文。它们都利用扩散模型和大规模数据集,从音频和一张单独的图片生成极高质量的视频。有几点关键内容让我印象深刻:

  • 这不完全是“规模就是一切”的情况。两个模型都使用了巧妙的技巧(VASA-1 使用 MegaPortiats,EMO 使用参考和主干网络)。然而,似乎确实存在“规模是你需要的东西”的情况。

  • 扩散是王道。 这两个模型,以及大多数最先进的视觉生成模型,都使用了扩散方法。看起来变分自编码器(VAE)和生成对抗网络(GAN)几乎已经完全过时。

  • 嘴唇同步模型的领域可能很快就会成为大公司的专属。如果趋势继续下去,学术界将无法建立能够与之竞争的模型

扩展你的 RAG:基于 Rust 的 LanceDB 和 Candle 索引管道

原文:towardsdatascience.com/scale-up-your-rag-a-rust-powered-indexing-pipeline-with-lancedb-and-candle-cc681c6162e8?source=collection_archive---------2-----------------------#2024-07-11

构建大规模文档处理的高性能嵌入和索引系统

Alon AgmonTowards Data Science Alon Agmon

·发表于Towards Data Science ·12 分钟阅读·2024 年 7 月 11 日

--

图片来源:Marc Sendra Martorell 来自Unsplash

1. 简介

最近,基于检索增强生成(RAG)的 AI 应用已成为构建生成型 AI 应用程序的事实上的标准,尤其是在使用大型语言模型时。RAG 通过确保生成模型使用适当的上下文来增强文本生成,同时避免了对 LLM 进行微调所涉及的时间、成本和复杂性。RAG 还可以更高效地使用外部数据源,并更容易地更新模型的“知识”。

尽管基于 RAG 的 AI 应用通常可以使用更小型或更简单的 LLM,但它们仍然依赖于一个强大的流程来嵌入和索引所需的知识库,并且需要能够高效地检索并将相关上下文注入到模型提示中。

在许多应用场景中,RAG 可以通过使用任何一个广泛可用的优秀框架,在几行代码中实现。本文侧重于更复杂且要求更高的流程,例如当需要嵌入和索引的数据量较大,或者需要非常频繁或极快地更新时。

本文展示了如何设计一个 Rust 应用程序,能够以惊人的速度读取、分块、嵌入并将文本文档存储为向量。利用 Hugging Face 的 Candle 框架和 LanceDB,它展示了如何开发一个端到端的 RAG 索引管道,可以作为独立应用程序部署到任何地方,并作为强大管道的基础,即使在非常苛刻和孤立的环境中也是如此。

本文的主要目的是创建一个可以应用于现实世界用例的工作示例,同时引导读者了解其关键设计原则和构建模块。该应用程序及其源代码可在随附的 GitHub 仓库中获得(链接见下),可以直接使用或作为进一步开发的示例。

本文的结构如下:第二部分高层次地解释了主要的设计选择和相关组件。第三部分详细介绍了管道的主要流程和组件设计。第四部分和第五部分分别讨论了嵌入流程和写入任务。第六部分作结。

2. 设计选择与关键组件

我们的主要设计目标是构建一个独立的应用程序,能够在没有外部服务或服务器进程的情况下运行端到端的索引管道。其输出将是一个数据文件集,采用 LanceDB 的Lance 格式,这些文件可以被像 LangChain 或 Llamaindex 这样的框架使用,并且可以通过 DuckDB 或任何使用 LanceDB API 的应用程序进行查询。

该应用程序将用 Rust 编写,并基于两个主要的开源框架:我们将使用Candle ML框架处理生成文档嵌入的机器学习任务,采用类似 BERT 的模型,并使用LanceDB作为我们的向量数据库和检索 API。

处理文档索引管道所有阶段的 Rust 应用程序(图片由作者提供)

在深入讲解应用程序的细节和结构之前,简要介绍一下这些组件和设计选择可能会有所帮助。

Rust 是性能至关重要时的显而易见选择。尽管 Rust 的学习曲线较陡,但其性能与本地编程语言(如 C 或 C++)相当,而且提供了丰富的抽象和扩展库,使得内存安全性和并发等挑战比本地语言更容易处理。结合 Hugging Face 的 Candle 框架,在本地 Rust 中使用 LLM 和嵌入模型变得前所未有的顺畅。

然而,LanceDB 是 RAG 堆栈中的一个相对较新的成员。它是一个精简型的嵌入式向量数据库(类似于 SQLite),可以直接集成到应用程序中,而无需单独的服务器进程。因此,它可以部署在任何地方并嵌入到任何应用中,同时提供极快的搜索和检索能力,即使是在远程对象存储中的数据上,例如 AWS S3。正如之前提到的,它还提供与 LangChain 和 LlamaIndex 的集成,并且可以使用 DuckDB 进行查询,这使得它成为向量存储的一个更具吸引力的选择。

在我在一台 10 核 Mac(没有 GPU 加速)上进行的简单测试中,应用程序在不到一秒的时间里处理、嵌入并存储了大约 25,000 个词(相当于 17 个文本文件,每个文件包含大约 1,500 个词)。这一令人印象深刻的吞吐量展示了 Rust 在处理 CPU 密集型任务和 I/O 操作方面的高效性,以及 LanceDB 强大的存储能力。两者结合在一起,对于解决大规模数据嵌入和索引挑战表现出色。

图片来源:Tharoushan KandarajahUnsplash上的作品

3. 流水线架构与流程

我们的 RAG 应用程序和索引流水线包含两个主要任务:一个读取和嵌入任务,它从文本文件中读取文本并使用嵌入模型将其嵌入到 BERT 向量中,另一个是写入任务,它将嵌入写入向量存储。由于前者大多由 CPU 限制(嵌入单个文档可能需要多个机器学习模型操作),而后者大多是在等待 I/O,因此我们将这两个任务分配到不同的线程中。此外,为了避免争用和背压,我们还将通过一个多生产者单消费者通道连接这两个任务。在 Rust(以及其他语言)中,同步通道基本上可以实现线程安全和异步的线程间通信,从而使其能够更好地扩展。

主要流程很简单:每当一个嵌入任务完成将文本文档嵌入向量后,它会将向量及其 ID(文件名)“发送”到通道,并立即继续处理下一个文档(见下图中的读取端)。与此同时,写入任务会不断地从通道中读取数据,将向量分块存储在内存中,并在达到一定大小时刷新数据。因为我预计嵌入任务会更加耗时和资源,所以我们会将其并行化,利用运行应用程序的机器上可用的多个核心。换句话说,我们将有多个嵌入任务来读取和嵌入文档,以及一个单独的写入任务来分块并将向量写入数据库。

流水线设计与应用流程(图片由作者提供)

让我们从main()函数开始,这将使得管道的流程更加清晰。

如上所示,在设置好通道(第 3 行)后,我们初始化了写入任务线程,该线程开始从通道中轮询消息,直到通道关闭。接着,它列出了相关目录中的文件并将它们存储在一个字符串集合中。最后,它使用Rayon通过par_iter函数并行处理文件列表,以便使用process_text_file()函数对其进行并行化处理。使用 Rayon 将允许我们根据机器的能力尽可能扩展文档的并行处理。

如你所见,流程相对简单,主要协调两个主要任务:文档处理和向量存储。这个设计允许高效的并行化和可扩展性。文档处理任务使用 Rayon 来并行化文件处理,最大化利用可用的系统资源。同时,存储任务负责高效地将嵌入的向量写入 LanceDB。关注点的分离不仅简化了整体架构,还允许对每个任务进行独立优化。在接下来的章节中,我们将更详细地探讨这两个函数。

4. 使用 Candle 进行文档嵌入

正如我们之前所看到的,在我们的管道的一端,我们有多个嵌入任务,每个任务都在自己的线程上运行。Rayon 的iter_par函数有效地遍历文件列表,在每个文件上调用process_text_file()函数,同时最大化并行化。

让我们从函数本身开始:

该函数首先获取对嵌入模型的引用(这是函数中最棘手的部分,我稍后会详细讲解)。接下来,它将文件分成一定大小的块,并对每个块调用嵌入函数(该函数实际上调用的是模型本身)。嵌入函数返回一个类型为Vec<f32>的向量(大小为[1, 384]),这是嵌入和归一化每个块的结果,之后计算所有文本块的平均值。当这一部分完成后,向量连同文件名一起发送到通道,用于持久化、查询和由写入任务进行检索。

如你所见,绝大部分工作由BertModelWrapper结构体完成(我们在第 2 行获取了该结构体的引用)。BertModelWrapper的主要目的是封装模型的加载和嵌入操作,并提供embed_sentences()函数,该函数本质上将一组文本块嵌入并计算它们的平均值,生成一个单一的向量。

为了实现这一点,BertModelWrapper 使用了 HuggingFace 的 Candle 框架。Candle 是一个本地的 Rust 库,其 API 类似于 PyTorch,用于加载和管理 ML 模型,并且对在 HuggingFace 上托管的模型提供了非常便捷的支持。虽然 Rust 中还有其他生成文本嵌入的方式,但 Candle 在本地化和不依赖其他库方面似乎是“最干净”的选择。

尽管对包装器代码的详细解释超出了我们当前的范围,但我在另外一篇文章中有更详细的说明(链接在此),其源代码可以在附带的 GitHub 仓库中找到。你也可以在 Candle 的示例仓库中找到很好的示例。

然而,有一个重要的部分需要解释,这就是我们在使用嵌入模型的方式,因为这将在任何需要在流程中使用大规模模型的地方都将是一个挑战。简而言之,我们希望多个线程能够同时使用我们的模型来执行嵌入任务,但由于加载时间的问题,我们不希望每次需要模型时都重新创建它。换句话说,我们希望确保每个线程只创建一个模型实例,该实例由线程拥有并在多个嵌入任务中重复使用。

由于 Rust 的众所周知的限制,这些要求并不是非常容易实现。如果你不想深入了解如何在 Rust 中实现这一部分,可以跳过此部分(直接使用代码)。

我们从获取模型引用的函数开始:

我们的模型被封装在几个层次中,以实现上述功能。首先,它被封装在 thread_local 子句中,这意味着每个线程将有自己的惰性副本——即所有线程都可以访问 BERT_MODEL,但在第一次调用 with()(第 18 行)时触发的初始化代码将仅在每个线程中惰性执行一次,这样每个线程就会拥有一个有效的引用,该引用只会初始化一次。第二层是引用计数类型——Rc,它简化了创建模型引用的过程,而无需处理生命周期。每次我们在其上调用 clone() 时,我们都会得到一个引用,该引用在超出作用域时会自动释放。

最后一层实际上是服务函数 get_model_reference(),它简单地调用了 with() 函数,从而提供了访问线程本地内存区域的权限,该区域保存已初始化的模型。对 clone() 的调用将为我们提供模型的线程本地引用,如果模型尚未初始化,则初始化代码将首先执行。

现在我们已经了解了如何运行多个并行执行的嵌入任务,并将向量写入通道,我们可以继续处理管道的另一部分——写入任务。

图片来源:SpaceX 通过 Unsplash

4. 写入任务:高效的向量存储

写入任务相对简单,主要作为一个接口,封装了 LanceDB 的写入功能。回想一下,LanceDB 是一个嵌入式数据库,这意味着它作为一个库的查询引擎,读取和写入可以存储在远程存储上的数据,例如 AWS S3,并且它不拥有数据。这使得它在需要处理大规模数据且低延迟的用例中尤其方便,而无需管理单独的数据库服务器。

LanceDB 的 Rust API 使用 Arrow 来定义 schema 和表示数据(其 Python API 对某些人来说可能更方便)。例如,以下是我们如何在 Arrow 格式中定义 schema:

如你所见,我们当前的 schema 包含两个字段:“filename”字段,它将保存实际的文件位置并作为我们的键,以及“vector”字段,它保存实际的文档向量。在 LanceDB 中,向量使用 FixedSizeList Arrow 类型表示(表示一个数组),而向量中的每个项目将是 Float32 类型(向量的长度,最后设置,将是 384)。

连接到 LanceDB 非常简单,只需要一个存储位置,可以是本地存储路径或 S3 URI。然而,使用 Rust 和 Arrow 数据结构将数据附加到 LanceDB 上并不十分开发者友好。与其他基于 Arrow 的列式数据结构类似,插入数据时,不是附加一行行的数据列表,而是每一列都表示为一个值的列表。例如,如果你有 10 行数据需要插入且有 2 列,你需要附加 2 个列表,每个列表包含 10 个值。

下面是一个例子:

代码的核心在第 2 行,我们从我们的 schema 和列数据构建一个 Arrow 的 RecordBatch。在这个例子中,我们有两列——文件名和向量。我们使用两个列表初始化我们的记录批次:key_array,一个包含文件名的字符串列表,以及 vectors_array,一个包含向量数组的列表。从这里开始,Rust 的严格类型安全要求我们在将数据传递给第 1 行获得的表引用的 add() 函数之前,必须对数据进行大量的包装。

为了简化这一逻辑,我们创建了一个存储模块,封装了这些操作,并提供一个基于 connect(uri) 函数和 add_vector 函数的简单接口。下面是写入任务线程的完整代码,该线程从通道读取嵌入数据,分块并在达到一定大小时进行写入:

一旦数据写入,LanceDB 数据文件可以从任何进程中访问。下面是一个例子,展示如何使用相同的数据进行向量相似性搜索,使用的是 LanceDB 的 Python API,且该 API 可以从完全不同的进程中执行。

uri = "data/vecdb1"
db = lancedb.connect(uri)
tbl = db.open_table("vectors_table_1")
# the vector we are finding similarities for
encoded_vec = get_some vector()
# perform a similiarity search for top 3 vectors
tbl.search(embeddings[0]) \
    .select(["filename"]) \
    .limit(3).to_pandas()

脚本输出(图片由作者提供)

5. 结论

在这篇文章中,我们看到了一个使用 Rust、HuggingFace 的 Candle 框架和 LanceDB 的高性能 RAG 管道的工作示例。我们看到了如何将 Rust 的性能优势与 Candle 相结合,来高效地并行读取和嵌入多个文本文件。我们还看到了如何利用同步通道同时运行嵌入任务并与写入流程协同工作,而无需处理复杂的锁和同步机制。最后,我们学会了如何使用 Rust 利用 LanceDB 的高效存储,生成可以与多个 AI 框架和查询库集成的向量存储。

我相信这里概述的方法可以作为构建可扩展、生产就绪的 RAG 索引管道的强大基础。无论你是在处理大量数据,需要频繁更新知识库,还是在资源受限的环境中操作,本文讨论的构建块和设计原则都可以根据你的具体需求进行调整。随着 AI 领域的不断发展,高效地处理和检索相关信息的能力将始终至关重要。通过结合合适的工具和周到的设计,正如本文所展示的,开发人员可以创建不仅满足当前需求,而且能够应对未来 AI 驱动的信息检索和生成挑战的 RAG 管道。

备注与链接

  • GitHub 上的源代码可以在这里找到。该仓库还包含了一个示例 jupyter notebook,展示了如何使用 Python 测试这一方法。

  • 我之前关于 HuggingFace Candle 的文章可以在这里找到。

  • Candle 框架及其文档,包括他们的完整示例文件夹

  • LanceDB及其Rust API 文档

使用 SOLID 原则扩展你的机器学习项目

原文:towardsdatascience.com/scale-your-machine-learning-projects-with-solid-principles-824230fa8ba1?source=collection_archive---------1-----------------------#2024-03-12

如何编写可扩展且能加速你作为数据科学家或机器学习工程师工作流程的代码。

Jeremy ArancioTowards Data Science Jeremy Arancio

·发表于 Towards Data Science ·13 分钟阅读·2024 年 3 月 12 日

--

当我还是一名初级数据科学家时,我的目标只是编写能够正常工作的代码。

我曾经把 Python 仅仅看作是一个用来运行 Pandas、Numpy 或 Matplotlib 的框架。我像其他人一样从 Jupyter Notebook 开始,一行一行地处理数据和训练模型。

我记得我在公司里的第一份工作。

随着项目的推进,笔记本变得越来越庞大,尽管使用 markdown 提供了解释,代码开始变得凌乱。

第一个模型终于训练完毕,性能评估完成,并在开发人员的帮助下被部署到生产环境。

然而,像任何机器学习项目一样,部署模型并不是旅程的终点,而是开始…

几周后,我不得不从头开始并重新审视笔记本。说实话,几乎更容易创建一个新的笔记本。需求发生了变化,代码太混乱,根本无法进行任何修改。

此外,将处理算法部署到生产环境中是一个痛苦的任务。数据必须在笔记本、训练管道和推理管道中进行一致的处理。

需要写三遍代码意味着,笔记本中的任何修改都需要…

提升生产力:利用 AWS 生成 AI 在几秒钟内总结会议记录

原文:towardsdatascience.com/scale-your-productivity-leveraging-aws-gen-ai-to-summarize-meeting-notes-in-seconds-31f348879dc2?source=collection_archive---------11-----------------------#2024-08-06

这是一份关于如何利用 AWS Lambda、Bedrock 和 S3 创建一个工作流来总结会议记录的全面指南

Jacob IngleTowards Data Science Jacob Ingle

·发布于 Towards Data Science ·阅读时长 16 分钟·2024 年 8 月 6 日

--

图片由 DALL-E 生成

免责声明:本文使用的会议记录完全是虚构的,仅用于说明和教育目的,并不反映任何实际的对话、事件或个人。任何与实际人物或事件的相似之处纯属巧合。

免责声明:本文中概述的项目涉及使用多个 Amazon Web Services (AWS),具体包括 Amazon S3、Amazon API Gateway、AWS Lambda 和 Amazon Bedrock。虽然 AWS 提供了一个免费套餐,可以提供有限的部分服务访问,但超出这些限制或使用不包含在免费套餐中的某些服务将产生费用。在使用 AWS 之前,务必查看 Amazon S3、Amazon API Gateway、AWS Lambda、Amazon Bedrock 及任何其他 AWS 服务的定价详情,以便了解相关费用。您有责任承担在执行此项目过程中产生的所有费用。请确保清楚了解 AWS 的定价政策,并监控您的使用情况,以避免出现意外费用。

引言

像您所期望的那样扩展 AI 模型

原文:towardsdatascience.com/scaling-ai-models-like-you-mean-it-3afa56c1e14b?source=collection_archive---------5-----------------------#2024-04-10

克服在生产环境中扩展开源 AI 模型挑战的策略

Sean ShengTowards Data Science Sean Sheng

·发布于 Towards Data Science ·11 分钟阅读·2024 年 4 月 10 日

--

如果你正在阅读这篇文章,可能不需要我再介绍部署开源模型的优势。在过去几年里,我们已经见证了开源模型在数量和质量上的惊人增长。

  • 像 Hugging Face 这样的平台使得各种模型的访问变得更加民主化,包括大型语言模型(LLMs)和扩散模型,这使得开发者能够自由高效地进行创新。

  • 开发者享有更大的自主性,因为他们可以自由地微调和组合不同的模型,从而推动创新方法的出现,比如检索增强生成(RAG)和创建先进的智能体。

  • 从经济角度来看,开源模型提供了可观的成本节省,使得使用较小、专用的模型成为可能,这些模型相较于像 GPT-4 这样的通用模型更加节省预算。

开源模型呈现出一种有吸引力的解决方案,但下一个难题是什么呢?与使用像 OpenAI 这样的模型端点不同,后者的模型是 API 背后的可扩展黑箱,部署自己的开源模型则引入了扩展问题。确保你的模型能够有效扩展以应对生产流量,并在流量激增期间保持无缝体验,至关重要。此外,合理管理成本也很重要,这样你只需为实际使用的部分付费,避免在月底遇到财务上的意外。

真正的方向:面向 GPU 的无服务器函数

有趣的是,这听起来像是现代无服务器架构(如 AWS Lambda)已经解决的挑战——这是一个已经存在近十年的解决方案。然而,当涉及到 AI 模型部署时,情况并非完全如此。

无服务器函数在 AI 部署中的局限性是多方面的。

  • 没有 GPU 支持。像 AWS Lambda 这样的平台不支持 GPU。这不仅仅是一个技术疏漏,而是由于架构和实际考虑所致。

  • GPU 无法轻松共享。虽然 GPU 作为设备具有高度并行性,但它在同时处理不同模型的多个推理任务时,灵活性不如其他技术。

  • GPU 昂贵。GPU 在模型推理任务中表现出色,但维护成本高昂,尤其是在不被持续使用的情况下。

接下来,让我们来看看我们的扩展历程以及我们在过程中学到的重要经验。

冷启动问题

在我们开始扩展工作之前,首先面临的是臭名昭著的“冷启动”问题。这个问题在三个不同的阶段表现出来:

冷启动问题的分析。图像来源:作者。

  1. 云资源配置:这一阶段涉及云提供商为我们分配实例并将其整合进集群所需的时间。这个过程差异很大,通常从 30 秒到几分钟不等,有时甚至会延长到几个小时,特别是对于需求量大的实例,如 Nvidia A100 和 H100 GPU。

  2. 容器镜像拉取:与简单的 Python 作业镜像不同,AI 模型服务镜像非常复杂,因其依赖关系和所需的自定义库。尽管云提供商宣称拥有多吉比特的网络带宽,但我们的经验表明,下载速度往往远低于这一标准,镜像拉取时间大约为 3 分钟。

  3. 模型加载。所需时间主要取决于模型的大小,像 LLM 和扩散模型这样的大型模型由于拥有数十亿个参数,需要显著更长的时间。例如,加载一个 5GB 的模型(如 Stable Diffusion 2)可能需要大约 1.3 分钟,前提是网络带宽为 1Gbps,而更大的模型如 Llama 13B 和 Mixtral 8x7B 则分别需要 3.5 分钟和 12.5 分钟。

冷启动问题的每个阶段都需要采取特定的策略来最小化延迟。在接下来的章节中,我们将更详细地探讨每个阶段,分享我们的策略和解决方案。

云资源配置

与无服务器 CPU 的同质化环境不同,管理多种计算实例类型在处理 GPU 时至关重要,因为每种 GPU 都针对特定的使用案例进行优化。例如,IO 密集型的 LLM 模型需要较高的 GPU 内存带宽和容量,而生成式模型则需要更强大的 GPU 计算能力。

在流量高峰期间确保可用性,通过保持所有 GPU 实例类型可能会导致不可承受的高成本。为了避免空闲实例带来的财务压力,我们实施了“备用实例”机制。我们没有为最大潜在负载做准备,而是维护了一个与增量扩展步长相匹配的计算备用实例数量。例如,如果我们一次扩展两个 GPU,我们就需要准备两个备用实例。这使我们能够在需求激增时迅速向服务队列中添加资源,显著减少等待时间,同时保持成本可控。

图片来源:作者。

在多租户环境中,多个团队或在我们的案例中,多个组织共享一个公共资源池,我们可以实现更高效的资源利用率。这个共享环境使我们能够平衡不同的资源需求,有助于提高成本效率。然而,管理多租户环境也带来了挑战,例如强制执行配额和确保网络隔离,这可能会增加集群的复杂性。

容器镜像拉取

无服务器 CPU 工作负载通常使用轻量级镜像,如 Python slim 镜像(约 154 MB)。相比之下,为了提供 LLM 服务的容器镜像可能要大得多(6.7 GB);其中大部分大小来自于运行 AI 模型所需的各种依赖项。

图片来源:作者。

尽管云服务提供商宣传了高带宽网络,实际情况往往远低于预期,实际下载速度仅为承诺速度的一个小部分。

实际上,很多文件根本没有被使用。一种方式是优化容器镜像本身,但这很快证明是不可管理的。相反,我们将注意力转向按需文件拉取方法。具体来说,我们首先只下载镜像元数据,实际的远程文件则在需要时再拉取。此外,我们利用集群中的点对点网络来大幅提高拉取效率。

容器镜像元数据可以在几秒钟内拉取。图片来源:作者。

通过这些优化,我们将镜像拉取时间从几分钟减少到几秒钟。然而,大家都知道,这个测量实际上是在“作弊”,因为实际的文件并没有在此阶段被拉取。真正的文件拉取发生在服务运行时。因此,拥有一个允许在各生命周期阶段定义行为的服务框架至关重要,例如初始化和服务。在初始化阶段完成所有引导工作后,我们可以确保拉取所有文件依赖项。这样,在服务时,就不会因拉取文件而产生延迟。

启动服务框架并定义 API 的服务框架。图片来源:作者。

在上述示例中,模型加载是在__init__初始化生命周期中完成的,服务则在@bentoml.api命名的txt2img中进行。

模型加载

最初,最直接的模型加载方法是从远程存储(如 Hugging Face)中直接获取。通过使用内容分发网络(CDN)、NVMe SSD 和共享内存,我们能够消除一些瓶颈。尽管这种方法可行,但远未达到最优。

为了改善这一过程,我们考虑了使用区域内网络带宽。我们在分布式文件系统中预先加载模型,并将其分割成较小的块,允许并行下载。这大大提高了性能,但我们仍然遇到了云服务商网络带宽的瓶颈。

为此,我们进一步优化了利用集群内网络带宽的方法,通过使用点对点共享和利用本地缓存。虽然这些改进显著,但它们增加了处理过程的复杂性,我们需要将其从开发人员的操作中抽象出来。

图片来自作者。

即使采用了上述做法,我们仍然面临着一个顺序瓶颈:每个步骤完成后才能进行下一个步骤的等待问题。模型必须完全下载到持久化驱动器中,然后加载到 CPU 内存中,再加载到 GPU 内存。

图片来自作者。

我们转向了一种基于流的模型权重加载方法,利用我们现有的分布式文件缓存系统。该系统允许程序像所有文件都逻辑上存储在磁盘上一样进行操作。实际上,所需的数据是按需从远程存储中获取,从而避免了磁盘写入。通过利用像Safetensors这样的格式,我们可以通过内存映射(mmap)将模型权重高效加载到主内存中,然后以流式方式加载到 GPU 内存中。

此外,我们还采用了异步磁盘写入。通过这样做,我们在本地磁盘上创建了一个更快速访问的缓存层。因此,只有代码更改的新部署可以绕过较慢的远程存储获取阶段,直接从本地缓存读取模型权重。

总结来说,我们成功优化了冷启动时间,并对结果感到满意:

  • 没有云提供商延迟,配备待命实例。

  • 更快的容器镜像拉取,通过按需和点对点流式传输。

  • 通过分布式文件系统、点对点缓存和流式加载到 GPU 内存中,加速了模型加载

  • 服务框架启用了并行图像拉取和模型加载。

扩展指标

接下来,我们需要确定在 GPU 上扩展 AI 模型部署的最具指示性的信号。

资源利用率指标

最初,我们考虑了 CPU 利用率。它直观且有一个默认的阈值,例如 80%。然而,显而易见的缺点是,CPU 度量无法捕捉 GPU 的利用情况。此外,Python 中的全局解释器锁(GIL)限制了并行性,阻止了多核实例上的高 CPU 利用率,这使得 CPU 利用率成为一个不太可行的度量。

我们还探索了 GPU 利用率作为衡量模型工作负载的更直接指标。然而,我们遇到了一个问题:工具如nvml报告的 GPU 利用率并未准确反映 GPU 的实际利用情况。该度量是通过一定时间段内对内核使用情况的采样来计算的,如果至少有一个内核在执行,GPU 就被认为是被利用的。这与我们的观察一致,即通过改进批处理,尽管 GPU 设备已经报告为高利用率,但通常可以实现更好的性能。

注意:根据NVIDIA 文档,utilization.gpu 表示“过去采样周期内,一个或多个内核在 GPU 上执行的时间百分比。采样周期可能是 1 秒到 1/6 秒,具体取决于产品。”*

基于资源的度量本质上是回顾性的,因为它们只反映在资源被消耗后才会出现的使用情况。它们也被限制在 100%,这带来了一个问题:当根据这些度量进行扩展时,调整的最大比例通常是当前利用率与所需阈值之间的比例(见下面的扩展公式)。这导致了一种保守的扩展行为,未必能准确匹配生产流量的实际需求。

desiredReplicas = ceil[currentReplicas * ( currentMetricValue / desiredMetricValue )]

基于请求的度量

我们转向了基于请求的度量,作为更主动的信号,它们也不受 100%限制。

QPS 是一个被广泛认可的度量,因其简单性。然而,它在生成式 AI 中的应用,如 LLM,仍然存在疑问。QPS 的配置并不容易,并且由于每个请求的成本变化,取决于处理和生成的标记数量,使用 QPS 作为扩展度量可能会导致不准确。

另一方面,并发性被证明是反映系统实际负载的理想度量。它代表了排队或正在处理的活跃请求的数量。这个度量:

  • 精确反映系统的负载。小洛特法则指出,QPS 乘以平均延迟等于并发性,提供了一种优雅的方式来理解 QPS 和并发性之间的关系。在实际操作中,模型服务中每个请求的平均延迟是相对未知的。然而,通过测量并发性,我们无需计算平均延迟。

  • 使用扩展公式准确计算所需的副本数。允许部署直接扩展到理想规模,而无需中间步骤。

  • 基于批次大小易于配置。对于不能批处理的模型,只需配置 GPU 数量,因为每个 GPU 一次只能处理一个生成任务。对于支持批处理的模型,批次大小决定了并发级别。

为了使并发有效,我们需要服务框架的支持,自动将并发作为度量标准并将其作为扩展信号提供给部署平台。我们还必须建立正确的扩展策略,以防在流量激增时过度扩展,或者在流量稀少时过早缩减。

请求队列

另一个我们与并发集成的重要机制是请求队列。它充当缓冲区和调度器,确保传入的请求得到高效处理,并且不会使任何单一服务器副本过载。

在没有请求队列的情况下,所有传入请求都会直接分配给服务器(如下图所示,6 个请求)。如果多个请求同时到达,而只有一个活跃的服务器副本,那么它会成为瓶颈。服务器会尝试按照先来先服务的方式处理每个请求,这通常会导致超时并影响客户端体验。

图片由作者提供。

相反,使用请求队列时,服务器按最优速率消费请求,基于为服务定义的并发速率处理。当额外的服务器副本扩展时,它们也会开始从队列中拉取请求。该机制防止任何单一服务器过载,并且能够更平滑、更可管理地分配请求到可用的基础设施上。

结论

我们在探索 AI 模型扩展解决方案的过程中经历了一次冒险,这最终使我们创造了 BentoCloud 上的扩展体验——一个囊括了我们所有学习成果的平台。

为了避免给人宣传的印象,我们将通过一张千言万语的图片来说明我们的观点。下面的监控仪表板展示了传入请求与服务器实例扩展之间的关系。

与扩展能力同样重要的是缩放的能力。当请求减少到零时,部署会相应地减少活跃实例的数量。这一能力确保了不会因未使用的资源而产生不必要的成本,使开支与实际使用情况相符。

BentoCloud 监控仪表板。图片由作者提供。

我们希望大家能从中获得的启示是,模型部署的扩展应被视为生产应用中的一个重要方面。与扩展 CPU 工作负载不同,在 GPU 上扩展模型部署面临独特的挑战,包括冷启动时间、配置扩展指标和调度请求等。在评估部署平台时,应彻底评估它们应对这些挑战的解决方案。

语言模型的规模定律

原文:towardsdatascience.com/scaling-law-of-language-models-5759de7f830c?source=collection_archive---------5-----------------------#2024-07-09

语言模型如何随模型规模、训练数据和训练计算的变化而扩展

Mina GhashamiTowards Data Science Mina Ghashami

·发布于 Towards Data Science ·阅读时间:8 分钟·2024 年 7 月 9 日

--

LLMs 的规模定律行为 — 图片来源:[1]

人工智能的世界正见证一场革命,而在这场革命的前沿,是不断增长强大的大型语言模型。从 BERT 到 GPT-3,再到 PaLM,这些人工智能巨头不断突破自然语言处理的边界。但你有没有想过,是什么推动了它们能力的飞速增长?

在这篇文章中,我们将开始一段令人着迷的旅程,深入了解语言模型规模化的核心。我们将揭示使这些模型得以成功的秘密配方——它是三种关键因素的强力结合:模型规模、训练数据和计算能力。通过理解这些因素如何相互作用和扩展,我们将获得对人工智能语言模型过去、现在和未来的宝贵洞察。

所以,让我们深入探讨并揭开推动语言模型不断突破性能和能力的新高度的规模定律之谜。

目录:本文包含以下几个部分:

  1. 介绍
  • 最近语言模型发展的概述

  • 语言模型规模化的关键因素

扩展单义性:Anthropic 迈向可解释和可操控 LLMs 的一步

原文:towardsdatascience.com/scaling-monosemanticity-anthropics-one-step-towards-interpretable-manipulable-llms-4b9403c4341e?source=collection_archive---------7-----------------------#2024-05-28

GenAI 字节:每天一字节,驱走冒名顶替综合症

从提示工程到激活工程,为了实现更可控和更安全的 LLMs

Jack Chih-Hsu LinTowards Data Science Jack Chih-Hsu Lin

·发表于 Towards Data Science ·阅读时间 11 分钟·2024 年 5 月 28 日

--

背景/介绍

单义性与多义性

在神经网络中(LLMs 的算法类型,灵感来源于人类大脑),神经元通常是多义性的,这意味着它们的激活响应多个含义和概念。另一方面,如果每个神经元仅代表一个含义/概念,则它们是单义性的。例如,短语“I feel blue”和“I’m heavy-hearted”作为 LLMs 的输入,只激活同一组代表悲伤或负面情绪的神经元。这些悲伤神经元也不会被其他概念激活。

特性

在机器学习中,“特性”指的是作为输入使用的可测量/可观察的属性。

然而需要注意的是,在本研究中,“特性”指的有所不同。“特性”是指在稀疏自编码器的编码层输出的高维层,它被训练用来预测 LLM 中间的激活。(请参见下面的“方法”部分以了解详细信息)。从高层次看,我将“特性”解释为……

数据缩放解释:带有代码示例的初学者可视化指南

原文:towardsdatascience.com/scaling-numerical-data-explained-a-visual-guide-with-code-examples-for-beginners-11676cdb45cb?source=collection_archive---------3-----------------------#2024-09-06

数据预处理

将成人尺寸的数据转化为适合儿童的模型

Samy BaladramTowards Data Science Samy Baladram

·发表于 Towards Data Science ·阅读时长 10 分钟·2024 年 9 月 6 日

--

⛳️ 更多的 [数据预处理](https://medium.com/@samybaladram/list/data-preprocessing-17a2c49b44e4) 解释: · 缺失值填充 · 分类编码 ▶ 数据缩放 · 离散化 · 过采样与欠采样 · 数据泄露与预处理

原始数据集中的数值特征就像是在为成年人打造的世界中的成人。有些特征像摩天大楼一样高耸(比如数十亿的收入),而其他特征则几乎看不见(如 0.001 的概率)。但是我们的机器学习模型呢?它们就像孩子一样,努力理解这个成人的世界。

数据缩放(包括一些人所称的“归一化”)是将这些成人尺寸的数字转化为适合儿童的比例的过程。它的目标是创造一个公平的竞争环境,让每个特征,无论大小,都能被理解并得到适当的重视。

我们将展示五种不同的缩放技术,所有的示例都基于一个小数据集(当然,包含一些可视化)。从标准化的轻柔处理到 Box-Cox 变换的数学魔法,你将看到选择合适的缩放方法如何成为你机器学习方程式中的秘密武器。

所有可视化:作者使用 Canva Pro 创建,已优化为移动端显示;在桌面端可能会显得过大。

理解哪些数据需要转换

在深入讨论缩放技术之前,了解哪些类型的数据适合缩放,哪些不适合缩放是很有帮助的:

通常不需要缩放的数据:

  1. 分类变量:这些通常应该进行编码,而不是缩放。这包括名义型和有序型分类数据。

  2. 二元变量:只能取两个值(0 和 1,或 True 和 False)的特征通常不需要缩放。

  3. 计数数据:整数计数通常按原样使用,缩放可能使它们更难理解。将它们视为分类数据。特别是对于具有非常宽范围的计数数据,可能会有一些例外。

  4. 周期性特征:具有周期性质的数据(如星期几或月份)通常更适合使用周期性编码,而非标准的缩放技术。

通常需要缩放的数据:

  1. 范围广泛的连续数值特征:可以取多个值的特征通常受益于缩放,以防它们在模型中主导其他特征。

  2. 单位不同的特征:当你的数据集中包含不同单位度量的特征(例如米、千克、年),缩放有助于将它们置于可比较的尺度上。

  3. 具有显著不同量级的特征:如果某些特征的值在千位范围,而其他特征在 0 到 1 之间,缩放可以帮助平衡它们对模型的影响。

  4. 百分比或比率特征:虽然这些特征已经处于固定的尺度(通常是 0-100 或 0-1),但在与范围更大的特征一起使用时,缩放可能仍然有益。

  5. 有界连续特征:具有已知最小值和最大值的特征通常受益于缩放,特别是当它们的范围与数据集中其他特征有显著差异时。

  6. 偏态分布:具有高度偏态分布的特征通常受益于某些类型的缩放或转换,以使它们更加接近正态分布,从而提升模型性能。

为什么要缩放数据?

现在,你可能会想,“为什么要进行缩放?难道我们不能让数据保持原样吗?”实际上,许多机器学习算法在所有特征处于相似尺度时表现最佳。下面是为什么需要缩放的原因:

  1. 特征重要性平衡:未缩放的特征可能意外主导模型。例如,风速(0–50 公里/小时)可能会因为其更大的尺度,而不是因为其更重要,掩盖温度(10–35°C)。

  2. 更快的收敛:许多机器学习中的优化算法,当特征处于相似的尺度时,会更快地收敛。

  3. 改进算法性能:一些算法,如 K-最近邻和神经网络,明确要求数据已缩放才能表现良好。

  4. 可解释性:在线性模型中,缩放后的系数更容易解释和比较。

  5. 避免数值不稳定:非常大或非常小的值可能会导致某些算法中的数值不稳定。

现在我们理解了哪些数字数据需要缩放以及为何需要缩放,接下来让我们看看我们的数据集,看看如何使用五种不同的缩放方法来缩放其数值变量。它不仅仅是关于缩放——而是要正确地进行缩放。

数据集

在我们进入缩放技术之前,让我们先看一下我们的数据集。我们将使用来自这个虚构高尔夫俱乐部的数据。

import pandas as pd
import numpy as np
from sklearn.preprocessing import MinMaxScaler, StandardScaler, RobustScaler
from scipy import stats

# Read the data
data = {
    'Temperature_Celsius': [15, 18, 22, 25, 28, 30, 32, 29, 26, 23, 20, 17],
    'Humidity_Percent': [50, 55, 60, 65, 70, 75, 80, 72, 68, 62, 58, 52],
    'Wind_Speed_kmh': [5, 8, 12, 15, 10, 7, 20, 18, 14, 9, 6, 11],
    'Golfers_Count': [20, 35, 50, 75, 100, 120, 90, 110, 85, 60, 40, 25],
    'Green_Speed': [8.5, 9.0, 9.5, 10.0, 10.5, 11.0, 11.5, 11.0, 10.5, 10.0, 9.5, 9.0]
}

df = pd.DataFrame(data)

这个数据集非常适合我们的缩放任务,因为它包含了具有不同单位、尺度和分布的特征。

现在让我们开始了解所有的缩放方法。

方法 1:最小-最大缩放

最小最大缩放通过减去最小值并除以范围,将所有值转换为一个固定范围,通常在 0 到 1 之间。

📊 常见数据类型: 特征值范围广泛,并且希望特定范围内的值。

🎯 目标:

  • 将特征限制在特定范围内(例如,0 到 1)。

  • 保留数据点之间的原始关系。

  • 确保缩放值的可解释性。

在我们的案例中:我们对温度应用此方法,因为在我们的高尔夫背景下,温度有自然的最小值和最大值。它保留了温度之间的相对差异,使得 0 为最冷的一天,1 为最热的一天,0.5 为平均温度日。

# 1\. Min-Max Scaling for Temperature_Celsius
min_max_scaler = MinMaxScaler()
df['Temperature_MinMax'] = min_max_scaler.fit_transform(df[['Temperature_Celsius']])

方法 2:标准化缩放

标准化缩放将数据中心化为均值 0,并将其缩放到标准差 1,这通过减去均值并除以标准差来实现。

📊 常见数据类型: 具有不同尺度和分布的特征。

🎯 目标:

  • 标准化特征,使其均值为 0,标准差为 1。

  • 确保具有不同尺度的特征在模型中有相等的贡献。

  • 为对特征尺度敏感的算法准备数据(例如 SVM、KNN)。

在我们的案例中:我们对风速使用此方法,因为风速通常呈现近似正态分布。它允许我们通过查看与均值的标准差距离,轻松识别异常平静或风大的日子。

# 2\. Standard Scaling for Wind_Speed_kmh
std_scaler = StandardScaler()
df['Wind_Speed_Standardized'] = std_scaler.fit_transform(df[['Wind_Speed_kmh']])

方法 3:鲁棒缩放

鲁棒缩放将数据中心化为中位数,并使用四分位间距(IQR)进行缩放。

📊 常见数据类型: 具有异常值或噪声数据的特征。

**🎯 目标:

-** 有效处理异常值,不会被其过度影响。

  • 保持数据点的相对顺序。

  • 在存在噪声数据的情况下实现稳定的缩放。

在我们的案例中:我们将其应用于湿度,因为湿度读数可能因极端天气条件或测量误差而出现异常值。这种缩放确保了我们的测量结果对这些异常值不那么敏感。

# 3\. Robust Scaling for Humidity_Percent
robust_scaler = RobustScaler()
df['Humidity_Robust'] = robust_scaler.fit_transform(df[['Humidity_Percent']])

到目前为止,我们已经看过几种使用缩放的方法。现在,让我们探索另一种方法——使用变换来实现缩放,从常见的对数变换技术开始。

方法 4:对数变换

它对数据应用对数函数,压缩非常大值的尺度。

📊 常见数据类型:

  • 右偏数据(长尾)。

  • 计数数据。

  • 具有乘法关系的数据。

**🎯 目标:

-** 解决右偏性并规范化分布。

  • 稳定特征范围内的方差。

  • 提高具有这些特征的数据的模型表现。

在我们的案例中:我们将其应用于高尔夫球员数量,因为计数数据通常呈现右偏分布。它使得 10 和 20 名球员之间的差异比 100 和 110 名球员之间的差异更为显著,这与这些差异在现实世界中的影响相符。

# 4\. Log Transformation for Golfers_Count
df['Golfers_Log'] = np.log1p(df['Golfers_Count'])

方法 5:Box-Cox 变换

这是一种幂变换族(包括对数变换作为特殊情况),旨在通过应用具有参数λ(λ)的幂变换来规范化数据分布,从而优化以达到所需的正态性。

常见数据类型: 需要规范化以接近正态分布的特征。

**🎯 目标:

-** 规范化特征的分布。

  • 提高假设数据符合正态分布的模型的表现。

  • 稳定方差并可能增强线性关系。

在我们的案例中:我们将其应用于绿色速度,因为它可能具有复杂的分布,无法通过更简单的方法进行规范化。这使得数据可以引导我们找到最合适的变换,可能改善其与其他变量的关系。

# 5\. Box-Cox Transformation for Green_Speed
df['Green_Speed_BoxCox'], lambda_param = stats.boxcox(df['Green_Speed'])

在进行变换后,通常还会进一步进行缩放,使其符合某种分布(如正态分布)。我们可以对这两个已经变换过的列进行此操作。

 df['Golfers_Count_Log'] = np.log1p(df['Golfers_Count']) 
df['Golfers_Count_Log_std'] = standard_scaler.fit_transform(df[['Golfers_Count_Log']])

box_cox_transformer = PowerTransformer(method='box-cox') # By default already has standardizing
df['Green_Speed_BoxCox'] = box_cox_transformer.fit_transform(df[['Green_Speed']])print("\nBox-Cox lambda parameter:", lambda_param)
print("\nBox-Cox lambda parameter:", lambda_param)

结论:缩放的力量

所以,情况就是这样。五种不同的缩放技术,都应用到了我们的高尔夫球场数据集上。现在,所有的数值特征都已转化,准备好用于机器学习模型。

这里是每种方法及其应用的快速回顾:

  • Min-Max 缩放:应用于温度,将数值标准化到 0–1 范围内,以便更好地进行模型解释。

  • 标准缩放:用于风速,将分布标准化,以减少极端值的影响。

  • 鲁棒缩放:应用于湿度,以处理潜在的异常值,并减少它们对模型性能的影响。

  • 对数变换:用于高尔夫球手数量,规范右偏的计数数据,并提高模型的稳定性。

  • Box-Cox 变换:应用于绿色速度,使分布更接近正态分布,这通常是机器学习算法所要求的。

每种缩放方法都有其特定的目的,并根据数据的性质和机器学习算法的要求进行选择。通过应用这些技术,我们已经为不同的机器学习模型准备了数值特征,可能会提高其性能和可靠性。

🌟 数值数据缩放,代码总结

import pandas as pd
import numpy as np
from sklearn.preprocessing import MinMaxScaler, StandardScaler, RobustScaler, PowerTransformer

# Read the data
data = {
    'Temperature_Celsius': [15, 18, 22, 25, 28, 30, 32, 29, 26, 23, 20, 17],
    'Humidity_Percent': [50, 55, 60, 65, 70, 75, 80, 72, 68, 62, 58, 52],
    'Wind_Speed_kmh': [5, 8, 12, 15, 10, 7, 20, 18, 14, 9, 6, 11],
    'Golfers_Count': [20, 35, 50, 75, 100, 120, 90, 110, 85, 60, 40, 25],
    'Green_Speed': [8.5, 9.0, 9.5, 10.0, 10.5, 11.0, 11.5, 11.0, 10.5, 10.0, 9.5, 9.0]
}

df = pd.DataFrame(data)

# 1\. Min-Max Scaling for Temperature_Celsius
min_max_scaler = MinMaxScaler()
df['Temperature_MinMax'] = min_max_scaler.fit_transform(df[['Temperature_Celsius']])

# 2\. Standard Scaling for Wind_Speed_kmh
std_scaler = StandardScaler()
df['Wind_Speed_Standardized'] = std_scaler.fit_transform(df[['Wind_Speed_kmh']])

# 3\. Robust Scaling for Humidity_Percent
robust_scaler = RobustScaler()
df['Humidity_Robust'] = robust_scaler.fit_transform(df[['Humidity_Percent']])

# 4\. Log Transformation for Golfers_Count
df['Golfers_Log'] = np.log1p(df['Golfers_Count'])
df['Golfers_Log_std'] = standard_scaler.fit_transform(df[['Golfers_Log']])

# 5\. Box-Cox Transformation for Green_Speed
box_cox_transformer = PowerTransformer(method='box-cox') # By default already has standardizing
df['Green_Speed_BoxCox'] = box_cox_transformer.fit_transform(df[['Green_Speed']])

# Display the results
transformed_data = df[[
    'Temperature_MinMax', 
    'Humidity_Robust', 
    'Wind_Speed_Standardized',
    'Green_Speed_BoxCox',
    'Golfers_Log_std', 
]]

transformed_data = transformed_data.round(2)
print(transformed_data)

⚠️ 澄清“缩放”、“标准化”和“变换”

由于这些术语在数据科学中经常被不一致地使用,下面我来澄清一下它们之间的区别:

  1. 缩放:这是一个更广泛的术语,指改变数值范围。它包括像以下的技术:

    • Min-Max 缩放(将数据缩放到一个固定的范围,通常是 0–1)

    • 标准缩放(将数据缩放到均值为 0,标准差为 1)

  2. 标准化:在严格的统计意义上,通常是指将不同量表上测得的数值调整到统一的量表,通常是为了使特征具有正态分布的性质。常见的技术包括:

    • Z-score 标准化(与标准缩放相同)

    • 对数标准化

    • Box-Cox 变换

  3. 变换:这是一个广义的术语,指任何应用于改变数据集数值或分布的数学操作。它包括缩放和标准化,以及其他操作,如:

    • 幂变换(例如平方根、立方根)

    • 对数变换

    • 指数变换

但实际上:

  • 有些人专门用“标准化”来指代缩放到[0,1]区间(Min-Max 缩放)。

  • 其他人几乎将“标准化”和“缩放”互换使用。

  • “变换”有时与“缩放”和“标准化”互换使用,但它实际上是一个更一般的术语。

鉴于这种重叠和不一致的使用,出于简化考虑,我决定在面向初学者的文章中使用“缩放”一词。与其纠结于术语争论,不如专注于每种技术的作用。

进一步阅读

要详细了解MinMaxScalerStandardScalerRobustScaler及其在 scikit-learn 中的实现,读者可以参考官方文档[1],其中提供了关于其使用和参数的详细信息。

技术环境

本文使用的是 Python 3.7 和 scikit-learn 1.5。尽管讨论的概念通常适用,但具体的代码实现可能会因不同版本而略有不同。

关于插图

除非另有说明,所有图片均由作者创作,结合了 Canva Pro 的授权设计元素。

要查看简洁的视觉总结,请访问该 Instagram 帖子

参考文献

[1] F. Pedregosa 等人,Scikit-learn:Python 中的机器学习,《机器学习研究杂志》,第 12 卷,第 2825–2830 页,2011 年。

𝙎𝙚𝙚 𝙢𝙤𝙧𝙚 𝘿𝙖𝙩𝙖 𝙋𝙧𝙚𝙥𝙧𝙤𝙘𝙚𝙨𝙨𝙞𝙣𝙜 𝙢𝙚𝙩𝙝𝙤𝙙𝙨 𝙝𝙚𝙧𝙚:

Samy Baladram

Samy Baladram

数据预处理

查看列表6 篇故事两个人物互相拥抱的卡通插图,字母‘A’,‘B’,‘C’和数字‘1’,‘2’,‘3’在他们周围漂浮。上方有一个粉色的心形符号,象征着感情。背景是蓝绿色方块的像素化图案,代表数据或编码。此图象征性地描述了对类别数据的编码过程,将类别(ABC)转化为数字表示(123)。一幅卡通插图,表示机器学习中的数据缩放。一个高大的女人(代表数值特征的较大范围)被缩小成一个孩子(代表缩放到较小范围后的同一特征)。一个红色箭头表示缩小过程,围绕孩子的黄色闪光符号象征着缩放的积极影响。

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

Samy Baladram

Samy Baladram

分类算法

查看列表8 个故事!Samy Baladram

Samy Baladram

回归算法

查看列表5 个故事!一只戴着小辫子和粉色帽子的卡通玩偶。这个“假人”玩偶,以其简单的设计和心形图案的衬衫,直观地代表了机器学习中的虚拟回归器概念。就像这个玩具般的形象是一个简化的、静态的人物表示,虚拟回归器是作为基线的基本模型,用于更复杂的分析。

将 RAG 从概念验证(POC)扩展到生产

原文:towardsdatascience.com/scaling-rag-from-poc-to-production-31bd45d195c8?source=collection_archive---------0-----------------------#2024-10-07

启动和扩展的常见挑战与架构组件

Anurag BhagatTowards Data Science Anurag Bhagat

·发布于Towards Data Science ·阅读时长 7 分钟·2024 年 10 月 7 日

--

来源:在 AI(OpenAI 的 Dall-E 模型)的帮助下生成

1. 引言

1.1. RAG 概述

那些沉浸于生成性 AI 及其在个人生产力应用之外的大规模应用的朋友们,可能已经接触过检索增强生成(RAG)的概念。RAG 架构由两个关键组件组成——检索组件,它使用向量数据库对大量文档进行基于索引的搜索,然后将其传送给大语言模型(LLM)以生成基于提示中更丰富上下文的有依据的响应。

无论你是在构建面向客户的聊天机器人以回答重复性问题并减少客户服务代表的工作负担,还是在为工程师构建一款协同助手,帮助他们一步步浏览复杂的用户手册,RAG 已经成为大语言模型(LLMs)应用的关键原型。这使得 LLM 能够根据成千上万的文档的事实基础,提供上下文相关的响应,从而减少幻觉现象并提高基于 LLM 的应用的可靠性。

1.2. 为什么要从概念验证(POC)扩展到生产

如果你在问这个问题,我可能会挑战你回答:如果没有打算将其投入生产,为什么要构建一个 POC(概念验证)?“试点地狱”是组织在开始实验时常见的风险,之后却陷入了实验模式。请记住,POC 是昂贵的,真正的价值实现只有在进入生产并按规模执行时才会发生——无论是释放资源、提高效率,还是创造额外的收入流。

2. 扩展 RAG 的关键挑战

2.1. 性能

RAG 中的性能挑战有很多种形式。检索速度通常不是主要挑战,除非你的知识库有数百万份文档,即便如此,这个问题也可以通过搭建合适的基础设施来解决——当然,我们也受到推理时间的限制。我们遇到的第二个性能问题是如何获取“正确”的文段,以便传递给大语言模型(LLM)进行生成,且要保证高精度和高召回率。检索过程越差,LLM 的回应就越缺乏上下文相关性。

2.2. 数据管理

我们都听过那句古老的谚语“垃圾进,垃圾出(GIGO)”。RAG 不过是我们手头的一组工具,真正的价值来自于实际数据。由于 RAG 系统处理的是非结构化数据,因此它有自己的一系列挑战,包括但不限于——文档版本控制和格式转换(例如从 PDF 到文本),等等。

2.3. 风险

企业从试水到全面投入的最大犹豫之一,是使用基于 AI 的系统可能带来的风险。尽管使用 RAG 可以显著减少幻觉现象,但它们仍然存在非零的可能性。还有其他相关的风险,包括偏见、毒性、合规性风险等,这些风险可能带来长期的影响。

2.4. 集成到现有工作流程中

构建离线解决方案比较容易,但引入最终用户的视角至关重要,以确保解决方案不会让用户觉得负担重。没有用户愿意为了使用“新 AI 功能”而切换到另一个屏幕——用户希望 AI 功能能够内嵌到现有工作流程中,这样技术就能成为一种辅助工具,而不是日常工作中的干扰。

2.5. 成本

嗯,这一点似乎很显而易见,对吧?组织正在实施生成式人工智能(GenAI)用例,以便创造业务影响。如果实际收益低于我们的预期,或者出现成本超支,影响将大大减少,甚至可能完全抵消。

3. 扩展所需的架构组件

如果我们只谈论挑战而不谈论“那么我们该怎么做”,那就不公平了。你可以在架构堆栈中加入一些必要的组件,以克服或减少我们上面提到的一些问题。

3.1. 可扩展的向量数据库

许多团队通常会首先使用开源向量数据库,如ChromaDB,因为它们易于使用和定制,非常适合概念验证(POC)。然而,它在大规模部署中可能会面临挑战。这时,可扩展的向量数据库就发挥了作用(例如,PineconeWeaviateMilvus等),这些数据库针对高维向量搜索进行了优化,能够实现快速(亚毫秒级)、准确的检索,即使数据集的大小增加到百万或十亿向量,它们仍然能够高效运作,因为它们使用近似最近邻搜索技术。这些向量数据库提供 API、插件和 SDK,方便与现有工作流集成,同时支持横向扩展。根据平台的不同,可能需要探索DatabricksAWS提供的向量数据库。

来源:借助 AI(OpenAI 的 Dall-E 模型)生成

3.2. 缓存机制

缓存的概念几乎与互联网的诞生一样久远,可以追溯到20 世纪 60 年代。这个概念同样适用于生成型 AI——如果有大量查询,可能达到数百万(在客户服务功能中非常常见),很可能许多查询是相同的或极为相似的。缓存技术可以避免向 LLM 发送请求,如果可以从最近的缓存响应中返回结果,这样不仅能减少成本,还能提高常见查询的响应速度。

这可以通过内存缓存(如RedisMemcached)来实现,针对较少的查询使用磁盘缓存,或使用分布式缓存(如 Redis 集群)。一些模型提供商(如 Anthropic)提供提示缓存作为其 API 的一部分。

来源:借助 AI(OpenAI 的 Dall-E 模型)生成

3.3. 高级搜索技术

虽然并非一个明确的架构组件,但多种技术可以帮助提升搜索效率和准确性。以下是其中一些方法:

  • 混合搜索:与仅依赖语义搜索(使用向量数据库)或关键词搜索不同,采用两者结合的方法来提升搜索效果。

  • 重新排序: 使用 LLM 或 SLM 计算查询与每个搜索结果的相关性得分,并重新排序,从而仅提取并分享最相关的结果。这对于复杂领域或有许多文档返回的领域尤其有用。一个例子是 Cohere’s Rerank

来源:通过 AI(OpenAI 的 Dall-E 模型)生成

3.4. 负责任的人工智能层

您的负责任人工智能模块必须设计用来减轻偏见,确保透明度,与您组织的伦理价值观对齐,持续监控用户反馈,并跟踪符合相关行业/功能的法规要求。方法有很多,但从根本上来说,必须通过编程启用,并由人工监督。可以采用几种方式来实现:

  • 预处理: 在用户查询发送到基础模型之前进行过滤。这可能包括检查偏见、有害内容、非预期用途等。

  • 后处理: 在结果从基础模型返回后,向最终用户展示之前应用另一组检查。

这些检查可以作为小型可重用模块启用,您可以从外部提供商购买,或者根据自己的需求进行构建/定制。组织通常采用的一种方式是使用精心设计的提示和基础模型来编排工作流程,确保结果在通过所有检查之前不会呈现给最终用户。

来源:通过 AI(OpenAI 的 Dall-E 模型)生成

3.5. API 网关

API 网关可以发挥多种作用,帮助管理成本以及负责任的人工智能的各个方面:

  • 提供统一的接口与基础模型互动,进行实验。

  • 帮助开发精细化的成本和使用情况视图,按团队/用例/成本中心进行划分 — 包括速率限制、速度控制、配额管理

  • 作为负责任的人工智能层,在请求/数据达到模型之前,过滤掉不适当的请求/数据。

  • 启用审计追踪和访问控制

来源:通过 AI(OpenAI 的 Dall-E 模型)生成

4. 这样就够了吗,还是我们需要更多?

当然不是。还有一些其他因素也需要考虑,包括但不限于:

  • 该用例是否在您的用例路线图中占据战略地位?这使您能够获得领导支持,并获得适当的投资以支持开发和维护。

  • 一个明确的评估标准,用于衡量应用程序在准确性、成本、延迟和负责任的人工智能等维度上的表现。

  • 改善业务流程,保持知识的最新性,维护版本控制等。

  • 设计 RAG 系统时,确保它仅在最终用户权限级别的基础上访问文档,以防止未经授权的访问。

  • 使用设计思维将应用程序集成到最终用户的工作流程中。例如,如果你正在构建一个通过 Confluence 作为知识库回答技术问题的机器人,你是否应该构建一个独立的用户界面,还是将其与 Teams/Slack/其他用户已经使用的应用程序集成?

5. 结论

RAG 是一个典型的使用案例原型,也是组织首次尝试实施的几个用例之一。从 POC 到生产环境的 RAG 扩展面临诸多挑战,但通过精心的规划和执行,许多挑战是可以克服的。其中一些可以通过在架构和技术上的战术性投资来解决,而另一些则需要更好的战略方向和巧妙的规划。随着大语言模型(LLM)推理成本的不断下降,无论是由于推理成本的降低,还是开源模型的广泛采用,许多新用例的成本壁垒可能不再是问题。

本文中的所有观点仅代表作者个人意见,并不代表对任何产品或服务的认可。

科学家们认真研究大型语言模型是否能模拟人类思维

原文:towardsdatascience.com/scientists-go-serious-about-large-language-models-mirroring-human-thinking-faa64a36ad71?source=collection_archive---------1-----------------------#2024-12-08

结合人脑成像学和心理学与 LLMs 工作原理的计算机科学研究

LucianoSphere (Luciano Abriata, PhD)Towards Data Science LucianoSphere (Luciano Abriata, PhD)

·发表于Towards Data Science ·13 分钟阅读·2024 年 12 月 8 日

--

在这里,我展示了一系列新颖的论文、预印本和评论,研究表明,至少在文本处理和程序性推理方面,LLMs 的工作方式与人类大脑非常相似——但也有一些显著差异,科学家们正在开始澄清这些差异。

由 DALL-E 3 通过 ChatGPT 生成的图片,可用于商业用途,具体详情请参见openai.com/policies/terms-of-use/

引言

大型语言模型(LLMs)的出现激发了人们对其能否模拟人类大脑认知过程的广泛兴趣。这些复杂的计算系统展示了在语言处理、推理和解决问题方面越来越复杂的能力,提出了一个引人入胜的问题:它们是否可能采用与人类思维相似的原理进行操作。我确实在以前几次讨论过这个想法,特别是在“中文房间论证”以及将 LLMs 处理文本的方式与我们人类如何在与世界互动的同时学习说话并从日常经验中发展推理能力之间做比较时:

Scikit-learn 可视化指南:让模型“开口说话”

原文:towardsdatascience.com/scikit-learn-visualization-guide-making-models-speak-6ee32a3c5c04?source=collection_archive---------0-----------------------#2024-03-21

使用 Display API 替代复杂的 Matplotlib 代码

Peng QianTowards Data Science Peng Qian

·发表于 Towards Data Science ·8 分钟阅读·2024 年 3 月 21 日

--

Scikit-learn 可视化指南:让模型“开口说话”。图片来自作者

引言

在机器学习的旅程中,使用可视化来解释模型和训练模型一样重要。

一张好的图表可以以易于理解的方式展示模型的行为。以下是一个示例:

两种不同泛化性能的决策边界。图片来自作者

这张图清楚地表明,对于相同的数据集,右侧的模型更擅长泛化。

大多数机器学习书籍倾向于使用原始的 Matplotlib 代码进行可视化,这会导致以下问题:

  1. 你必须学习很多关于如何使用 Matplotlib 绘图的知识。

  2. 绘图代码填满了你的笔记本,导致难以阅读。

  3. 有时你需要第三方库,这在商业环境中并不理想。

好消息!Scikit-learn 现在提供了 Display 类,允许我们使用 from_estimatorfrom_predictions 等方法,使得在不同情况下绘制图表变得更加简单。

Seamless:深入解析 Meta 最新开源翻译模型套件

原文:towardsdatascience.com/seamless-in-depth-walkthrough-of-metas-new-open-source-suite-of-translation-models-b3f22fd2834b?source=collection_archive---------13-----------------------#2024-02-16

Meta 的开源 Seamless 模型:深入解析翻译模型架构以及使用 HuggingFace 的 Python 实现指南

Luís RoqueTowards Data Science Luís Roque

·发表于Towards Data Science ·阅读时长 15 分钟·2024 年 2 月 16 日

--

本文由 Rafael Guedes 共同撰写。

介绍

一个组织的增长并不限于其所在国家的边界。有些组织仅在外部市场进行销售或运营。这种全球化带来了几个挑战,其中之一就是如何处理不同的语言,并使从产品标签到宣传材料的翻译成本更低。AI 的最新发展在这方面提供了很大的帮助,因为它们不仅能实现便宜且快速的文本翻译,还能翻译音频资料。

将 AI 融入日常活动的组织,通常在竞争中领先一步,特别是在准备好所有与产品相关的组件,以进入新市场时。时机与产品或服务的质量一样重要;因此,能够成为第一个到达的人至关重要,而语音到语音、文本到文本的翻译技术将帮助你缩短进入新市场所需的时间。

在本文中,我们将探讨 Seamless,这是 Meta 开发的三种模型系列,旨在打破多语言之间的交流障碍。我们将详细解释这些模型的架构……

无需重启管道,Delta Live Tables 中嵌套 JSON 和架构演化的无缝解析

原文:towardsdatascience.com/seamless-parsing-of-nested-json-and-schema-evolution-in-dlt-without-restarting-pipelines-51d390937ab1?source=collection_archive---------5-----------------------#2024-10-05

基于一个客户案例研究,本教程介绍了如何使用 Delta Live Tables 处理 JSON 架构演化,而无需重启。

Irfan ElahiTowards Data Science Irfan Elahi

·发表于 Towards Data Science ·阅读时间:15 分钟·2024 年 10 月 5 日

--

由 DALL-E 生成

免责声明: 我是 Databricks 的解决方案架构师。本文表达的观点和看法仅代表我个人,不一定反映 Databricks 的立场。

架构演化是数据工程领域中的常见现象。在从数据源提取数据并加载到目标位置时,源架构的变化是不可避免的。当涉及到包含 JSON 负载的源系统时,这一挑战更加突出,比如 PostgreSQL 中的 JSON 类型列。这些 JSON 负载中发生架构变化的可能性很高——新字段可以随时添加,并且通常嵌套在不同的层次中。这些频繁的变化大大增加了构建强大数据管道的复杂性,这些管道需要解析这些架构变化并无缝地演化架构。

由 Delta Lake 格式提供支持的 Databricks 智能平台,提供强大的架构演化支持,在处理数据结构变化时,确保灵活性和弹性。Delta Lake 可以…

眼见为实 — 深度伪造及其如何扭曲真相

原文:towardsdatascience.com/seeing-is-believing-deepfakes-and-how-they-warp-truth-38a59af51562?source=collection_archive---------7-----------------------#2024-03-20

桥接自编码器与媒介素养

Eirik Berge, PhDTowards Data Science Eirik Berge, PhD

·发表于Towards Data Science ·阅读时间 9 分钟·2024 年 3 月 20 日

--

图片由Brett Jordan提供,来自Unsplash

概述

  1. 介绍 — 什么是深度伪造?

  2. 恶意深度伪造的例子

  3. 自编码器

  4. 媒介素养与深度伪造的检测

  5. 总结

介绍 — 什么是深度伪造?

照片操控的行为由来已久。它曾用于为第一次世界大战的旧照片上色,也曾被用于宣传工具;约瑟夫·斯大林臭名昭著地操控照片,使得他的政治对手列昂·托洛茨基从重要场合中消失。照片操控已经有超过 100 年的历史,既用来吸引观众,也用来欺骗观众。

随着时间的推移,我们在日常生活中不仅消费图片,还消费视频和音频。互联网促进了视频和音频分享的急剧增长,个人和组织的分享量远远超过过去固定的电视和广播频道。这对获得新视角是很有帮助的。然而,这也与人工智能的创新一起,带来了一个新的可怕概念:深度伪造

在大语言模型中看到我们的倒影

原文:towardsdatascience.com/seeing-our-reflection-in-llms-7b9505e901fd?source=collection_archive---------5-----------------------#2024-03-02

当大语言模型(LLMs)给我们提供揭示人类社会缺陷的结果时,我们能否选择听取它们告诉我们的内容?

Stephanie KirmerTowards Data Science Stephanie Kirmer

·发表于 Towards Data Science ·阅读时间 7 分钟 ·2024 年 3 月 2 日

--

图片由 Vince Fleming 提供,来源:Unsplash

机器学习,轻推

到现在为止,我相信你们大多数人都听说过关于 谷歌的新大语言模型 Gemini 生成了穿着纳粹制服的种族多样化人物图片的新闻。这则小小的新闻提醒了我一些我一直想讨论的事情,那就是当模型存在盲点时,我们如何应用专家规则来调整它们生成的预测,以避免返回给用户一些极端离谱的结果。

这种情况在机器学习中并不罕见,至少在我的经验中是这样,特别是当你拥有存在缺陷或有限的训练数据时。我记得自己工作中的一个典型例子是预测包裹什么时候会送达某个商业办公室。从数学上讲,我们的模型在精确估算包裹何时接近办公室这一点上非常出色,但有时,卡车司机在深夜到达目的地后会在卡车里或酒店休息直到第二天早晨。为什么?因为在办公时间之外,没有人在办公室接收/签收包裹。

教会一个模型“工作时间”的概念可能非常困难,而更简单的解决方法是直接说:“如果模型预测交付时间在工作时间之外,就给预测加上足够的时间,让它变化到下一个办公室开放的时间。”简单!这解决了问题,而且反映了实际情况。我们只是在给模型一点帮助,让它的结果能更好地工作。

然而,这确实带来了一些问题。首先,我们现在有两个不同的模型预测需要管理。我们不能直接丢弃原始模型的预测,因为我们需要它来进行模型性能监控和指标评估。你不能通过人类干预后的预测来评估一个模型,因为那在数学上是不成立的。但为了更清晰地了解模型在现实世界中的影响,你确实需要查看规则后的预测,因为那才是客户在你的应用中实际体验到/看到的结果。在机器学习中,我们习惯了一个非常简单的框架,每次运行一个模型都会得到一个结果或一组结果,事情就这么定了,但当你开始在发布之前调整结果时,你就需要从不同的层面去思考了。

应用于 LLM

我有点怀疑这可能是像 Gemini 这样的 LLM(大语言模型)所发生的一种形式。然而,与其说是预测后的规则,似乎聪明的钱说 Gemini 和其他模型正在应用“秘密”提示增强技术,试图改变 LLM 产生的结果。

本质上,如果没有这种轻推,模型会生成反映它所训练内容的结果。也就是说,模型学习的是由真实人类产生的内容。我们的社交媒体帖子、历史书籍、博物馆画作、流行歌曲、好莱坞电影等。模型吸收了所有这些内容,并学习其中的潜在模式,无论这些模式是我们引以为傲的,还是不是。一个被输入了当代社会中所有可用媒体的模型,将会接触到大量的种族主义、性别歧视以及其他各种形式的歧视和不平等,更不用说暴力、战争和其他可怕的事情了。当模型学习人们的外貌、声音、言辞和动作时,它在学习的其实是那个有缺陷的完整版本。

我们的社交媒体帖子、我们的历史书籍、我们的博物馆画作、我们的流行歌曲、我们的好莱坞电影等等。模型吸收了所有这些内容,并学习了其中的潜在模式,无论这些模式是我们引以为傲的,还是我们不愿面对的。

这意味着,如果你要求底层模型展示一位医生,它可能会给你展示一位穿着实验室外套的白人男性。这不仅仅是随机的,这是因为在我们现代社会,白人男性在进入像医生这样的高端职业中占有不成比例的优势,因为他们通常能获得更多和更好的教育、财务资源、指导、社会特权等等。模型正在反映出一种可能让我们感到不舒服的形象,因为我们不愿意去思考这种现实。

那么我们该怎么办呢?

一个显而易见的论点是:“我们不希望模型加强我们社会已经存在的偏见,我们希望它能改善对少数群体的代表性。”我非常理解这个观点,并且非常关注我们媒体中的代表性。然而,这其中有一个问题。

很难说这些调整会是一个可持续的解决方案。回想我开始时提到的关于双子座的故事。这就像是在玩打地鼠游戏,因为工作永无止境——现在我们看到有色人种穿着纳粹制服,这显然让很多人感到深深的冒犯。所以,也许我们最初随机在提示中添加“作为一名黑人”或“作为一名土著”时,接下来我们必须添加更多内容,以排除那些不合适的情况——但如何以一种大模型能理解的方式来表达呢?我们可能需要从头开始,重新思考最初的解决方案是如何运作的,并重新审视整体方法。在最好的情况下,像这样的调整只会解决一个狭隘的输出问题,但可能会产生更多的新问题。

让我们举一个非常真实的例子。如果我们在提示中加上“在回答中永远不要使用露骨或粗俗的语言,包括[这里列出不当词汇]”。也许这在很多情况下有效,模型会拒绝说一个 13 岁男孩为了搞笑而请求的脏话。但迟早,这会带来意想不到的附加副作用。如果有人在查找英国萨塞克斯的历史,会怎么样?或者,总会有人提出你在列表中遗漏的脏话,这意味着维护列表将是一个持续不断的工作。其他语言中的脏话呢?谁来判断哪些词应该列入列表?光是想想这件事就让我头疼。

这只是两个例子,我相信你可以想到更多类似的情况。这就像是在一个漏水的管子上贴创可贴,每次修补一个地方,另一个地方就会漏水。

那我们该怎么办呢?

那么,我们究竟希望从 LLM 中得到什么呢?我们希望它生成一幅非常真实的镜像,反映人类真实的面貌和我们社会从媒体角度看上去的样子吗?还是我们希望看到一个经过“净化”的版本,将边缘部分清除掉?

老实说,我认为我们可能需要一个折中的方案,并且我们必须继续重新谈判边界,尽管这很困难。我们不希望 LLM 反映出人类社会中暴力、仇恨等现实中的可怕景象和污秽,这是我们世界的一部分,应该连一点点也不放大。零内容审查不是答案。 幸运的是,这一动机与运行这些模型的大型公司希望它们受大众欢迎并赚取大量金钱的愿望相吻合。

…我们必须继续重新谈判边界,尽管这很困难。我们不希望 LLM 反映出人类社会中暴力、仇恨等现实中的可怕景象和污秽,这是我们世界的一部分,应该连一点点也不放大。零内容审查不是答案。

然而,我确实想继续温和地提出一个观点,那就是我们也可以从 LLM 世界中的这个困境中学到一些东西。与其在模型生成了一堆白人男性医生的图片时,仅仅感到愤怒并责怪技术,我们应该停下来思考一下为什么模型会生成这样的结果。然后我们应该深入讨论是否允许模型的这种反应,并基于我们的价值观和原则做出决定,并尽力实施。

正如我之前所说,LLM(大规模语言模型)并不是来自另一个宇宙的外星人,它就是我们。它是基于我们所写的、说的、拍摄的、录制的、做过的事情进行训练的。如果我们希望模型展示出各性别、性别认同、种族等不同背景的医生,我们就需要建立一个让所有这些不同类型的人都能接触到这个职业和所需教育的社会。如果我们只担心模型如何反映我们,但没有真正意识到需要改进的是我们自己,而不仅仅是模型,那么我们就错过了重点。

如果我们希望模型展示出各性别、性别认同、种族等不同背景的医生,我们就需要建立一个让所有这些不同类型的人都能接触到这个职业和所需教育的社会。

我相信我不是唯一这么想的人,但由于 Gemini 从定义上是多模态的,不仅仅在训练中使用语言,还有音频、视频等,“LLM”似乎是个不太准确的术语。但我在网上找到的所有参考资料似乎仍然在使用这个词。

你可以在 www.stephaniekirmer.com. 找到更多我的作品。

参考文献

[## 黑色纳粹?女教皇?这只是谷歌 AI 问题的开始。

Gemini 图像生成器不仅仅是遇到技术问题,还面临着哲学上的难题。

www.vox.com](https://www.vox.com/future-perfect/2024/2/28/24083814/google-gemini-ai-bias-ethics?source=post_page-----7b9505e901fd--------------------------------) [## 谷歌撤下 Gemini AI 图像生成器。这是你需要了解的内容。

批评者表示,谷歌的 Gemini 图像生成器创造了女教皇和黑人开国元勋的形象。

www.washingtonpost.com](https://www.washingtonpost.com/technology/2024/02/22/google-gemini-ai-image-generation-pause/?pwapi_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyZWFzb24iOiJnaWZ0IiwibmJmIjoxNzA4ODM3MjAwLCJpc3MiOiJzdWJzY3JpcHRpb25zIiwiZXhwIjoxNzEwMjE1OTk5LCJpYXQiOjE3MDg4MzcyMDAsImp0aSI6IjFhMzAyYjkyLTRkN2ItNDNmMi1hNThlLWY1MDBjY2I2NDFjMyIsInVybCI6Imh0dHBzOi8vd3d3Lndhc2hpbmd0b25wb3N0LmNvbS90ZWNobm9sb2d5LzIwMjQvMDIvMjIvZ29vZ2xlLWdlbWluaS1haS1pbWFnZS1nZW5lcmF0aW9uLXBhdXNlLyJ9.E-JdVAohho0X-rTsTb1bfof4gIpYl8-NpPdZwL6h9Dc&source=post_page-----7b9505e901fd--------------------------------) [## AI 和脏话、恶作剧、淫秽以及其他不当词汇的列表

它最初是为了限制 Shutterstock 上的自动完成功能。现在它影响着 Slack 上的搜索建议,甚至影响着……

www.wired.com](https://www.wired.com/story/ai-list-dirty-naughty-obscene-bad-words/?source=post_page-----7b9505e901fd--------------------------------) [## 拥有“冒犯性”姓氏的人分享了他们的日常问题,这真是太搞笑了

有些人自出生以来就命运多舛,面临许多难题……

www.boredpanda.com [## 为什么 Platformer 要离开 Substack

我们以前看过这种情况——我们不会再待着等它上演完。

open.substack.com

Segment Anything 2:秘密武器是什么?(深度学习者指南)

原文:towardsdatascience.com/segment-anything-2-what-is-the-secret-sauce-a-deep-learners-guide-1c43dd07a6f8?source=collection_archive---------2-----------------------#2024-08-06

基础模型 + 可提示 + 互动 + 视频。如何实现?

Avishek BiswasTowards Data Science Avishek Biswas

·发布于 Towards Data Science ·阅读时长 10 分钟·2024 年 8 月 6 日

--

Segment Anything 2 流程(图由作者提供)

Meta 最近发布了 Segment Anything 2 模型,简称 SAM 2——这是一种神经网络,不仅能够分割图像,还能分割整个视频。SAM 2 是一个可提示的互动基础分割模型。可提示意味着你可以点击或拖动一个或多个你想要分割的对象的边界框,SAM2 就能预测一个遮罩,将目标对象单独标出并在输入片段中追踪它。互动意味着你可以随时编辑提示,比如在不同的帧中添加新的提示——分割结果会相应地调整!最后,作为一个基础分割模型,它是在庞大的数据集上训练的,可以应用于多种不同的使用场景。

请注意,本文是“深度学习指南”,因此我们将主要关注 SAM-2 后面的网络架构。 如果你是视觉学习者,可能想查看本文所依据的 YouTube 视频。

可提示视觉分割(PVS)

SAM-2 侧重于 PVS 或可提示视觉分割任务。给定一个输入视频和用户提示——如点击、框选或遮罩——网络必须预测一个 masklet,这也是时空遮罩的另一种说法。一旦预测出 masklet,就可以通过在额外的帧中提供更多的提示——通过正负点击——来迭代地精细化它,以交互方式更新分割后的遮罩。

原始 Segment Anything 模型

SAM-2 是在原始 SAM 架构的基础上发展而来,原始 SAM 是一个图像分割模型。让我们快速回顾一下原始 SAM 模型的基本架构。

SAM-1 架构 — 图像编码器对输入图像进行编码。提示编码器对输入提示进行编码,掩码解码器将提示和图像嵌入结合起来,预测查询对象的分割掩码。掩码解码器还输出 IOU 得分,为简化起见,以上未显示这些得分。(插图由作者提供)

  1. 图像编码器处理输入图像,生成通用的图像嵌入。这些嵌入与提示无关。

  2. 提示编码器处理用户输入的提示,生成提示嵌入。提示嵌入与输入图像无关。

  3. 掩码解码器输入无条件的图像和提示嵌入,并应用交叉注意力和自注意力模块将它们进行上下文化处理。通过生成的上下文化嵌入,生成多个分割掩码

  4. 多个输出分割掩码由掩码解码器预测。这些掩码通常表示查询对象的整体、部分或子部分,并有助于解决由于用户提示而可能出现的歧义(下图)。

  5. 交并比(Intersection-Over-Union) 得分会为每个输出分割掩码进行预测。这些 IoU 得分表示 SAM 对每个预测掩码“正确”的信心分数。也就是说,如果 SAM 为掩码 1 预测了较高的 IoU 得分,那么掩码 1 很可能是正确的掩码。

SAM 预测 3 个分割掩码,通常表示查询对象的“整体”、“部分”和“子部分”。对于每个预测的掩码,SAM 还会预测一个 IOU 得分,以估计生成的掩码与感兴趣对象的重叠程度(来源:图像由作者提供)

那么,SAM-2 在采用上述架构处理视频时做了什么不同的改进呢?让我们讨论一下。

帧编码器

输入视频首先被分割成多个帧,每个帧都使用基于视觉变换器(Vision Transformer)的掩码自编码器(Masked Auto-encoder)计算机视觉模型独立编码,称为Heira 架构。我们稍后再讨论这个变换器的具体架构,现在只需将其视为一个黑盒,它将单个帧作为图像输入,输出一个形状为 256x64x64 的多通道特征图。视频的所有帧都采用相同的编码器进行处理。

输入视频的帧通过预训练的视觉变换器进行嵌入(图像由作者提供)

请注意,这些嵌入并不考虑视频序列——它们只是独立的帧嵌入,意味着它们无法访问视频中的其他帧。其次,就像 SAM-1 一样,它们完全不考虑输入提示——这意味着结果输出被视为输入帧的通用表示,并且完全不依赖于输入提示。

这样做的好处是,如果用户在未来的帧中添加新的提示,我们无需再次通过图像编码器处理这些帧。每个帧只需对图像编码器运行一次,结果会被缓存并重用于所有类型的输入提示。这一设计决策使得 SAM-2 可以以交互速度运行——因为图像编码的重工作只需要在每个视频输入中进行一次。

关于 Heira 架构的简要说明

图像编码器的确切性质是一个实现细节——只要编码器足够好并且在大量图像语料库上进行训练,就可以。Heira 架构是一个层次化的视觉变换器,这意味着随着网络的加深,空间分辨率被降低,特征维度被增加。这些模型是在掩膜自编码的任务上进行训练的,其中输入网络的图像被分成多个块,有些块会被随机灰化——然后 Heira 模型从剩余的可以看到的块中学习重建原始图像。

Heira 模型返回通用的图像嵌入,可以用于各种下游计算机视觉任务!(来源:这里

由于掩膜自编码是自监督的,意味着所有标签都是从源图像本身生成的,我们可以轻松地在大量图像数据集上训练这些大型编码器,而不需要手动标注它们。掩膜自编码器倾向于学习关于图像的通用嵌入,这些嵌入可以用于许多下游任务。它的通用嵌入能力使其成为 SAM 帧编码器的首选架构。

提示编码器

就像原始的 SAM 模型一样,输入提示可以来自点击点、框或分割掩膜。提示编码器的工作是通过将这些提示转换为代表性向量表示来编码它们。这里有一段关于原始 SAM 架构的视频,详细讲解了提示编码器是如何工作的。

原始的《Segment Anything》论文解释(作者的视频)

提示编码器将其转换为 N_tokens x 256 的形状。例如,

  • 编码点击 — 点击的 x 和 y 坐标的位置信息被用作提示序列中的一个 token。点击的“类型”(前景/正面或背景/负面)也包含在表示中。

  • 编码边界框 — 使用左上角和右下角点的位置信息进行编码。

编码提示(作者插图)

关于密集提示编码的说明

点点击和边界框是稀疏提示编码,但我们也可以输入整个分割掩码,这是一种密集提示编码。密集提示编码在推理过程中很少使用,但在训练期间,它们用于迭代训练 SAM 模型。SAM 的训练方法超出了本文的范围,但对于那些感兴趣的人,下面是我试图在一段话中解释整个文章的尝试。

SAM 是通过迭代分割训练的。在训练过程中,当 SAM 输出分割掩码时,我们将其作为密集提示与来自真实标签和预测的细化点击(稀疏提示)一起输入回 SAM。掩码解码器使用这些稀疏和密集提示,并学习输出一个新的细化分割掩码。在推理过程中,仅使用稀疏提示,分割掩码通过一次推理预测出来(不反馈密集掩码)。

也许有一天我会写一篇关于迭代分割训练的文章,但现在,让我们继续探索 SAM 的网络架构。

在 SAM-2 中,提示编码基本保持不变。唯一的区别是,它们必须分别对用户提示的所有帧运行。

掩码解码器(预告)

嗯…在我们讨论掩码解码器之前,先来聊聊 SAM-2 中的内存概念。目前,我们假设掩码解码器输入一堆东西(相信我,我们会在稍后讨论这些东西是什么),然后输出分割掩码。

内存编码器与内存库

在掩码解码器生成输出掩码后,输出掩码会经过内存编码器,得到一个内存嵌入。每处理一帧都会创建一个新的内存。这些内存嵌入会被追加到内存库中,内存库是一个先进先出(FIFO)的队列,用于存储视频解码过程中生成的最新内存。

对于每一帧,内存编码器将掩码解码器的输出掩码输入,并将其转换为内存。然后,这个内存会被插入到名为内存库的 FIFO 队列中。请注意,这张图没有显示内存注意模块——我们稍后会在文章中讨论它。(来源:作者插图)

内存编码器

输出的掩码首先通过卷积层进行下采样,然后将无条件的图像编码添加到此输出中,经过轻量级的卷积层融合信息,得到的空间特征图称为内存。你可以把内存想象成原始输入帧和给定时间帧生成的掩码的表示。

内存编码器(作者插图)

内存库

内存库包含以下内容:

  • 最近的 N 个记忆存储在队列中

  • 用户输入的最后 M 个提示,用于跟踪多个先前的提示。

  • 每个帧的掩码解码器输出标记也会被存储——这些标记像对象指针,捕捉到关于待分割对象的高级语义信息。

内存库(图片来自作者)

内存注意力

我们现在有了一种保存历史信息到内存库中的方式。接下来,我们需要在生成未来帧的分割掩码时使用这些信息。这是通过使用内存注意力来实现的。内存注意力的作用是将当前帧特征与内存库特征进行条件化,然后再输入到掩码解码器中。

内存注意力模块将图像编码与内存库进行上下文化处理。然后,这些上下文化的图像嵌入被输入到掩码解码器中生成分割掩码。(图片来自作者)

内存注意力模块首先对帧嵌入执行自注意力,然后在图像嵌入和内存库内容之间执行交叉注意力。因此,无条件的图像嵌入会与先前的输出掩码、先前的输入提示和对象指针进行上下文化处理。

内存注意力模块(图片来自作者)

在自注意力和交叉注意力层中,除了常规的正弦位置嵌入外,还使用了二维旋转位置嵌入。简单来说——旋转位置嵌入能够捕捉帧之间的相对关系,二维旋转位置嵌入在图像中效果良好,因为它们有助于建模帧之间的空间关系,包括水平和垂直方向。

掩码解码器(这次是真的)

掩码解码器输入由内存注意力模块输出的内存上下文化的图像编码,以及提示编码,并输出分割掩码、IOU 分数和(全新的)遮挡分数。

SAM-2 掩码解码器(来源:SAM-2 论文 这里

掩码解码器使用自注意力和交叉注意力机制,将提示标记与(内存条件化的)图像嵌入进行上下文化处理。这使得图像和提示能够“结合”成一个上下文感知的序列。然后,这些嵌入被用来生成分割掩码、IOU 分数和遮挡分数。基本上,在视频过程中,被查询的对象可能会被遮挡,因为它被场景中的另一个对象挡住——遮挡分数预测查询的对象是否出现在当前帧中。遮挡分数是 SAM-2 的新功能,用于预测查询的对象是否存在于当前场景中。

回顾一下,和 IOU 分数一样,SAM 会为每个预测的三种掩码生成一个遮挡分数。这三个 IOU 分数告诉我们 SAM 对每个预测掩码的信心有多大,而这三个遮挡分数则告诉我们 SAM 认为相应物体出现在场景中的可能性有多大。

SAM-2 Mask 解码器的输出(图片由作者提供)

最终思考

作者关于 SAM-2 的视频

所以…这就是 SAM-2 背后的网络架构概述。对于从视频中提取的帧(通常为 6 帧每秒),这些帧会使用帧/图像编码器进行编码,记忆注意力会对图像编码进行上下文处理,如果有提示词,提示词也会被编码,之后 Mask 解码器将图像和提示词的嵌入结合,生成输出掩码、IOU 分数和遮挡分数,记忆通过记忆编码器生成,并被追加到记忆库中,整个过程会重复进行。

SAM-2 算法(图片由作者提供)

关于 SAM-2 仍然有许多内容值得讨论,比如它是如何通过互动训练成为可提示模型的,以及他们的数据引擎是如何工作来创建训练数据的。我希望在另一篇文章中详细讨论这些话题!你可以在我上面的 YouTube 频道观看 SAM-2 视频,了解更多信息,并观看支持 SAM-2 的系统的视觉展示。

感谢阅读!请鼓掌并关注!

参考文献/接下来该去哪儿

使用 PaliGemma 对卫星图像中的水体进行分割

原文:towardsdatascience.com/segmenting-water-in-satellite-images-using-paligemma-b172dc0cf55d?source=collection_archive---------2-----------------------#2024-12-29

一些关于使用 Google 最新视觉语言模型的见解

Dr. Carmen Adriana Martínez BarbosaTowards Data Science Dr. Carmen Adriana Martínez Barbosa

·发表于 Towards Data Science ·9 分钟阅读·2024 年 12 月 29 日

--

澳大利亚的哈特湖。根据季节、时间和云层覆盖情况,这个湖泊的颜色会从红色变为粉色或紫色。来源:Google Maps。

多模态模型是一种架构,能够同时集成和处理不同类型的数据,如文本、图像和音频。一些例子包括 OpenAI 的 CLIP 和 DALL-E,这两者都在 2021 年发布。CLIP 可以联合理解图像和文本,使其能够执行零-shot 图像分类等任务。而 DALL-E 则根据文本描述生成图像,允许在游戏、广告和文学等领域自动化和增强创意过程。

视觉语言模型(VLMs)是多模态模型的一种特殊形式。VLMs 根据视觉输入生成语言。一个显著的例子是 PaliGemma,Google 于 2024 年 5 月推出。PaliGemma 可用于视觉问答、物体检测和图像分割。

一些博客文章探讨了 PaliGemma 在物体检测中的应用,比如 Roboflow 的这篇精彩文章:

[## 使用自定义数据微调 PaliGemma 进行物体检测

学习如何微调 PaliGemma 多模态模型以检测自定义物体。

blog.roboflow.com

然而,在我写这篇博客时,关于如何准备数据以使用 Paligemma 进行对象分割的现有文档非常模糊。这就是为什么我想评估使用 Paligemma 完成这个任务是否容易。在这里,我分享我的经验。

Paligemma 简介

在详细讨论用例之前,让我们简要回顾一下 Paligemma 的内部工作原理。

Paligemma2 的架构。来源:arxiv.org/abs/2412.03555

Paligemma 将 SigLIP-So400m 视觉编码器Gemma 语言模型 结合,处理图像和文本(见上图)。在今年 12 月发布的 Paligemma 新版本中,视觉编码器可以以三种不同的分辨率对图像进行预处理:224px、448px 或 896px。视觉编码器对图像进行预处理并输出一系列图像标记,这些标记与输入文本标记线性结合。这个标记组合将被 Gemma 语言模型进一步处理,最终输出文本标记。Gemma 模型有不同的大小,从 2B 到 27B 参数不等。

以下图所示是模型输出的示例。

对象分割输出的示例。来源:arxiv.org/abs/2412.03555

Paligemma 模型在多个数据集上进行了训练,如 WebLiopenImagesWIT 等(有关更多详情,请参见这篇 Kaggle 博客)。这意味着 Paligemma 能够在没有微调的情况下识别物体。然而,这种能力是有限的。因此,Google 推荐在特定领域的用例中对 Paligemma 进行微调。

输入格式

要微调 Paligemma,输入数据需要为 JSONL 格式。JSONL 格式的数据集中的每一行都是一个单独的 JSON 对象,就像一个个体记录的列表。每个 JSON 对象包含以下键:

图像: 图像的名称。

前缀: 这指定了您希望模型执行的任务。

后缀: 这是提供给模型学习的真实标签,用以进行预测。

根据任务的不同,您必须相应地更改 JSON 对象的前缀和后缀。以下是一些示例:

  • 图像标注:
{"image": "some_filename.png", 
 "prefix": "caption en" (To indicate that the model should generate an English caption for an image),
 "suffix": "This is an image of a big, white boat traveling in the ocean."
}
  • 问答:
{"image": "another_filename.jpg", 
 "prefix": "How many people are in the image?",
 "suffix": "ten"
}
  • 物体检测:
{"image": "filename.jpeg", 
 "prefix": "detect airplane",
 "suffix": "<loc0055><loc0115><loc1023><loc1023> airplane" (four corner bounding box coords)
}

如果您有多个类别需要检测,请在每个类别的前缀和后缀之间添加分号(;)。

关于如何为 Paligemma 准备物体检测数据的完整清晰解释,请参见 这篇 Roboflow 文章

  • 图像分割:
{"image": "filename.jpeg", 
 "prefix": "detect airplane",
 "suffix": "<loc0055><loc0115><loc1023><loc1023><seg063><seg108><seg045><seg028><seg056><seg052><seg114><seg005><seg042><seg023><seg084><seg064><seg086><seg077><seg090><seg054> airplane" 
}

请注意,对于分割,除了对象的边界框坐标外,还需要指定 16 个额外的分割令牌,代表适合在边界框内的掩码。根据谷歌的 Big Vision 库,这些令牌是包含 128 个条目的代码词()。我们如何获取这些值?根据我的个人经验,在没有适当文档的情况下,获取它们非常具有挑战性和令人沮丧。不过,我稍后会提供更多细节。

如果你有兴趣了解更多关于 PaliGemma 的信息,我推荐以下博客:

[## 欢迎使用 PaliGemma 2 —— 谷歌的新视觉语言模型

我们正在通过开源和开放科学推动和普及人工智能的发展。

huggingface.co](https://huggingface.co/blog/paligemma2?source=post_page-----b172dc0cf55d--------------------------------) [## 介绍 PaliGemma:谷歌最新的视觉语言模型

PaliGemma 通过任务特定的微调推动了视觉语言模型在高效多模态方面的边界……

www.datature.io

水体的卫星图像

如上所述,PaliGemma 是在不同数据集上进行训练的。因此,预计该模型擅长分割“传统”物体,如汽车、人或动物。但在卫星图像中分割物体怎么样呢?这个问题促使我探索 PaliGemma 在卫星图像中分割水体的能力。

Kaggle 的水体卫星图像数据集非常适合此用途。该数据集包含 2841 张图像及其对应的掩码。

这是水体数据集的一个示例:左侧是 RGB 图像,右侧是相应的掩码。

数据集中的一些掩码是错误的,其他一些则需要进一步的预处理。错误的示例包括将所有值都设为水的掩码,而原始图像中只存在一小部分水域。还有些掩码与其 RGB 图像不对应。当图像旋转时,某些掩码会让这些区域看起来像有水。

旋转掩码的示例。当在 Python 中读取此图像时,图像外部区域会显示为有水的样子。在这种情况下,需要旋转图像来纠正掩码。图像由作者制作。

鉴于这些数据的限制,我选择了 164 张图像样本,其中的掩膜没有上述提到的任何问题。这一组图像被用来微调 Paligemma。

准备 JSONL 数据集

如前一节所述,Paligemma 需要输入表示物体边界框坐标的标准化图像空间坐标(),以及额外的 16 个分割标记,代表 128 个不同的代码词()。由于有了Roboflow 的解释,获取所需格式的边界框坐标变得很容易。那么,我们如何从掩膜中获取 128 个代码词呢?在 Big Vision 代码库中没有明确的文档或示例可以用于我的使用场景。我天真地以为创建分割标记的过程与创建边界框相似。然而,这导致了水体掩膜的错误表示,从而导致了错误的预测结果。

到我写这篇博客时(12 月初),Google 宣布了 Paligemma 的第二版。在此事件之后,Roboflow 发布了一篇很好的概述,介绍了如何准备数据来微调 Paligemma2,以适应不同的应用场景,包括图像分割。我使用了他们代码的一部分,最终获得了正确的分割代码词。我的错误是什么呢?首先,掩膜需要调整大小为形状为[None, 64, 64, 1]的张量,然后使用预训练的变分自编码器(VAE)将标注掩膜转换为文本标签。尽管在 Big Vision 代码库中简要提到过 VAE 模型的使用,但并没有提供如何使用它的解释或示例。

我用来准备数据以微调 Paligemma 的工作流程如下所示:

将一个原始掩膜从过滤后的水体数据集转换为 JSON 对象的步骤。这个过程在训练集中的 164 张图像和测试集中的 21 张图像上重复,以构建 JSONL 数据集。

如观察所见,准备数据以适应 Paligemma 所需的步骤较多,因此我没有在这里分享代码片段。然而,如果你想探索代码,可以访问这个 GitHub 仓库。脚本convert.py包含了上述工作流程中的所有步骤。我还添加了选定的图像,方便你立即使用这个脚本进行操作。

在将分割代码词转换回分割掩膜的预处理过程中,我们注意到这些掩膜如何覆盖图像中的水体:

在训练集上解码分割代码词时生成的掩膜。此图由作者使用此 Notebook制作。

Paligemma 在分割卫星图像中的水体表现如何?

在微调 Paligemma 之前,我尝试了它在 Hugging Face 上上传的模型的分割功能。该平台有一个演示,你可以上传图片并与不同的 Paligemma 模型互动。

默认的 Paligemma 模型在分割卫星图像中的水体方面表现。

当前版本的 Paligemma 通常能较好地分割卫星图像中的水体,但它并不完美。让我们看看是否能改进这些结果!

有两种方法可以微调 Paligemma,分别是通过 Hugging Face 的 Transformer 库 或使用 Big Vision 和 JAX。我选择了后一种方法。Big Vision 提供了一个 Colab notebook,我根据自己的使用需求进行了修改。你可以通过访问我的GitHub 仓库来打开它:

[## SegmentWaterWithPaligemma/finetune_paligemma_for_segmentation.ipynb 位于主分支 ·…

使用 Paligemma 进行卫星图像中水体的分割……

github.com](https://github.com/anamabo/SegmentWaterWithPaligemma/blob/main/finetune_paligemma_for_segmentation.ipynb?source=post_page-----b172dc0cf55d--------------------------------)

我使用了 batch size 为 8,learning rate 为 0.003。我运行了两次训练循环,总共进行了 158 步训练。在 T4 GPU 机器上,整个运行时间为 24 分钟。

结果并未如预期那样。Paligemma 在一些图像中没有生成预测结果,在其他图像中,生成的掩膜与真实值相差甚远。我还在两张图像中获得了超过 16 个标记的分割代码词。

微调后的结果中有预测的情况。此图由作者制作。

值得一提的是,我使用的是第一版 Paligemma。也许使用 Paligemma2,或者进一步调整批次大小或学习率,结果会有所改进。无论如何,这些实验超出了本文的讨论范围。

演示结果显示,默认的 Paligemma 模型在分割水体方面优于我微调过的模型。在我看来,如果目标是构建一个专门用于分割物体的模型,那么 UNET 是一个更好的架构。如需了解如何训练此类模型的更多信息,可以阅读我之前的博客文章:

## 使用 AI 检测云层

从随机森林到 YOLO:比较不同算法在卫星图像中进行云层分割的效果。

[towardsdatascience.com

其他限制:

我想提一下在使用 Big Vision 和 JAX 微调 Paligemma 时遇到的其他一些挑战。

  • 设置不同的模型配置是困难的,因为关于这些参数的文档仍然很少。

  • Paligemma 的第一个版本已被训练处理不同长宽比的图像,并将其调整为 224x224 大小。请确保只将输入图像调整为此大小,这将防止引发异常。

  • 在使用 Big Vision 和 JAX 进行微调时,你可能会遇到与 JAX GPU 相关的问题。克服此问题的方法有:

a. 减少训练集和验证集中的样本数量。

b. 将批量大小从 8 增加到 16 或更高。

  • 微调后的模型大小约为 5GB。确保你的驱动器有足够的空间来存储它。

重点信息

发现一个新的 AI 模型令人兴奋,尤其是在这个多模态算法正在转变我们社会的时代。然而,由于缺乏可用的文档,与最先进的模型合作有时会变得具有挑战性。因此,新 AI 模型的发布应伴随全面的文档,以确保其顺利且广泛的应用,特别是在那些仍然缺乏经验的专业人士中。

尽管我在微调 Paligemma 时遇到了困难,但当前的预训练模型在进行零-shot 目标检测和图像分割时非常强大,可以应用于许多场景,包括辅助机器学习标注。

你在计算机视觉项目中使用 Paligemma 吗?在评论中分享你在微调该模型时的经验!

希望你喜欢这篇文章,再次感谢阅读!

你可以通过 LinkedIn 与我联系,地址是:

www.linkedin.com/in/camartinezbarbosa/

致谢:我要感谢 José Celis-Gil 在数据预处理和建模方面的所有富有成果的讨论。

自注意力句子嵌入在推荐系统中的应用

原文:towardsdatascience.com/self-attentive-sentence-embedding-for-the-recommendation-system-fc8af5817035?source=collection_archive---------10-----------------------#2024-05-22

什么是自注意力嵌入,如何在推荐系统中使用它?

Bào BùiTowards Data Science Bào Bùi

·发布于Towards Data Science ·6 分钟阅读·2024 年 5 月 22 日

--

图片由Giulia Bertelli提供,来源于Unsplash

引言

Transformer 层及其注意力机制是自然语言处理(NLP)领域最具影响力的思想之一。它们在许多大型语言模型中发挥着至关重要的作用,如ChatGPTLLaMA,这些模型最近在全球范围内引起了轰动。

然而,还有一个来自 NLP 社区的有趣想法,它的影响主要体现在推荐领域:自注意力句子嵌入。在本文中,我将带领大家了解自注意力句子嵌入[1]及其在推荐系统中的应用。

它是如何工作的

总体思路

本文的主要思想是找到一种更好的方式,将句子编码为多个嵌入,捕捉句子的不同方面。具体而言,作者希望将句子编码为一个二维矩阵,而不是单一的嵌入,其中每一行的嵌入捕捉句子的不同方面:

一旦我们获得了句子嵌入,就可以将它们用于各种下游任务,例如句子分析、作者画像等。

Self-Instruct 框架,解释

原文:towardsdatascience.com/self-instruct-framework-explained-16bce90f4683?source=collection_archive---------10-----------------------#2024-03-05

或者说,如何“消除”人工标注者

Tsiu-zhen-tsin DmitriiTowards Data Science Tsiu-zhen-tsin Dmitrii

·发布于Towards Data Science ·10 分钟阅读·2024 年 3 月 5 日

--

图像由 DALL·E 生成

动机

InstructGPT 的高级概述,包括人工标注输出和监督学习及奖励模型训练的排名 | 来源:Training language models to follow instructions with human feedback.

随着大语言模型(LLMs)彻底改变我们的生活,指令调优 LLMs 的增长面临着显著挑战:对大量、多样且高质量数据集的迫切需求。传统方法,如使用人工标注者生成数据集——这也是 InstructGPT(上图)采用的策略——面临着高成本、有限的多样性、创造性和一致性挑战。为了应对这些局限性,Self-Instruct 框架²应运而生。其核心理念简单而强大:让语言模型(LM)生成训练数据,从而实现更具成本效益、多样性和创造性的数据集。

因此,在本文中,我将引导你逐步了解框架的各个方面,展示所有细节,以便你在阅读后能够自己重现这些结果 😃

❗ 本文从代码的角度提供了所有步骤,因此请随时访问原始的GitHub 仓库 。❗

Self-Instruct 框架

Self-Instruct 框架的高级概述

该方法相对简单明了:

  • 步骤 0 — 定义指令数据:

    — 将高质量且多样的人工编写任务作为(指令,实例)元组添加到任务池中,涵盖不同领域;

  • 步骤 1 — 指令生成:

    — 从任务池中采样 8 个指令(6 个人工编写和 2 个模型生成);

    — 以少量示例的方式将自举生成的指令插入到提示中,并要求语言模型生成更多指令;

    — 基于 ROUGE 指标(评估文本输出与参考文本之间相似度的一种方法)和一些启发式方法(稍后会详细说明)过滤生成的指令;

    — 重复步骤 1,直到达到一定数量的指令;

  • 步骤 2 — 分类任务识别:

    — 对于任务池中每个生成的指令,我们需要通过少量示例的方式识别其类型(分类任务或非分类任务);

  • 步骤 3 — 实例生成:

    — 给定指令和任务类型,生成实例(输入和输出),并根据启发式方法过滤它们;

  • 步骤 4 — 微调语言模型以遵循指令:

    — 利用生成的任务来微调预训练模型。

Voilà,这就是 Self-Instruct 的工作原理,但关键在于细节,所以让我们深入了解每一个步骤!

步骤 0 — 定义指令数据

步骤 0

让我们首先理解“任务种子”的初始内容:它包含 175 个种子任务(25 个分类任务和 150 个非分类任务),每个任务有一个指令和一个实例,覆盖不同领域。每个任务都有一个 id、名称、指令、实例(输入和输出)和一个 is_classification 二进制标志,用来识别任务是否具有有限的输出标签空间。

这里有一些带有空输入字段和非空输入字段的分类和非分类任务示例:

分类任务的非空输入示例

非分类任务的空输入示例

因此,我们可以看到在第一个示例中,输入字段如何澄清并为更一般的指令提供上下文,而在第二个示例中,只要指令已经自包含,我们就不需要输入字段。此外,第一个示例是分类任务 —— 我们可以通过从有限的空间中分配一些标签来回答它,而对于第二个示例,我们则无法做到这一点。

这一步是至关重要的,只要我们通过数据格式在数据集中鼓励任务多样性,并展示解决各种任务的正确方法。

只要我们定义好指令格式,就将它们添加到任务池中,储存我们的最终数据集。

步骤 1 — 指令生成

步骤 1

采样与提示

通过向任务池添加人工编写的种子任务集,我们可以开始指令生成。为此,我们需要从任务池中采样 8 个指令(6 个人工编写和 2 个机器生成),并将它们编码为以下提示:

提示生成新指令

然而,在一开始,我们并没有任何机器生成的指令。因此,我们只需在提示中将其替换为空字符串。

生成后,我们从语言模型的响应中提取指令(通过正则表达式),过滤掉它们,并将过滤后的指令添加到任务池中:

指令生成步骤的伪代码

我们重复指令生成步骤,直到生成一定数量的机器生成指令(在步骤开始时指定的数量)。

过滤

为了获得一个多样化的数据集,我们需要定义某些指令是否会被添加到任务池中,最简单的方法是通过一组启发式选择的规则,例如:

  • 过滤掉过短或过长的指令;

  • 根据不适合语言模型的关键词(如图片、图表、文件、图示等)进行过滤;

  • 过滤掉那些以标点符号开头的指令;

  • 过滤掉那些以非英语字符开头的指令;

  • 当其 ROUGE-L 相似度与任何现有指令的相似度大于 0.7 时,过滤掉这些指令;

第二步 — 分类任务识别

第二步

Self-Instruct 的作者注意到,根据不同的指令,语言模型可能会对某一个标签产生偏向,特别是在分类任务中。因此,为了消除这种偏向,我们需要通过少量示例提示对每个指令进行分类:

用于分类任务是否为分类任务或非分类任务的提示(此模板中使用了 12 个分类指令和 19 个非分类指令)

第三步 — 实例生成

第三步

在识别了指令类型后,我们最终可以生成输入和输出,考虑到我们有两种类型的指令(分类或非分类)。怎么做呢?少量示例提示!

对于非分类指令,我们要求模型首先生成输入,然后生成输出(先输入法),但对于分类任务,我们要求模型先生成输出(类别标签),然后基于输出生成输入(先输出法)。与第 0 步相比,我们不限制每个指令生成的实例数量。

用于实例生成的输入优先法提示

用于输出优先法实例生成的提示

生成后,我们提取实例并进行格式化(使用正则表达式);格式化后,我们通过一些规则进行过滤,例如:

  • 如果输入和输出相同,

  • 如果实例已经在任务池中,

  • 如果输出为空,

  • 这些通常是生成不完整的指令,如果输入或输出以冒号结尾;

还有一些其他的启发式方法。最后,我们得到了以下示例,其中包含 1 条指令和 1 个实例:

实例生成示例

这就是 Self-Instruct 的主要思想!

步骤 4——对语言模型进行微调以遵循指令

完成所有前述步骤后,我们可以采用一个预训练的语言模型,并对其在生成的数据集上进行指令微调,从而获得更好的指标。

克服挑战

在文章的开头,我提到了一些“指令微调”语言模型面临的挑战;现在,让我们看看 Self-Instruct 如何帮助克服这些挑战。

数量

在仅有 175 个初始人工编写的任务的帮助下,生成了 52K 条指令和 82K 个实例:

来源:Self-Instruct: Aligning Language Models with Self-Generated Instructions

多样性

为了调查生成数据集的多样性,《Self-Instruct》的作者使用了伯克利神经解析器(Berkley Neural Parser)来解析指令,然后提取与根词最接近的动词及其第一个直接名词对象。在 52K 条指令中,有 26K 条具有动词-名词格式,但另外 26K 条指令具有更复杂的结构(例如,“判断这条推文是否包含政治内容。”)或被构造为问题(例如,“以下哪些陈述是正确的?”)。

在生成的指令中,最常见的前 20 个根动词(内圈)及其前 4 个直接名词对象(外圈)| 来源:Self-Instruct: Aligning Language Models with Self-Generated Instructions

质量

为了证明 Self-Instruct 能够生成高质量的任务,随机选择了 200 条生成的指令,并为每条指令抽取 1 个实例,然后框架的作者对这些任务进行了评估,得出了以下结果:

来源:Self-Instruct: Aligning Language Models with Self-Generated Instructions

如我们所见,92%的任务描述了有效的任务,且 54%的任务包含了所有有效字段(考虑到我们生成了 52K 个任务,至少 26K 个将代表高质量数据,这是非常棒的!)

成本

Self-Instruct 框架也带来了显著的成本优势。任务生成的初始阶段(步骤 1-3)仅需 600 美元,而最后一步使用 GPT-3 模型进行微调的成本为 338 美元。我们在查看结果时,必须记住这一点!

结果

Self-Instruct 如何提高在 SuperNI(超级自然指令)数据集上的 ROUGE-L 指标?为此,我们可以比较以下几种情况的结果:1) 没有任何指令微调的预训练语言模型(普通语言模型),2) 指令微调的模型(没有 SuperNI 的指令微调),以及 3) 在 SuperNI 数据集上训练的指令微调模型(有 SuperNI 的指令微调):

来自 SuperNI 的未见任务的评估结果 | 来源:Self-Instruct: Aligning Language Models with Self-Generated Instructions

正如我们所见,使用 Self-Instruct 在数据集(1)上展示了比原始模型高出 33%的绝对改进;同时,它还表明,使用该框架在微调 SuperNI 数据集(3)后,也能略微提高指标。

此外,如果我们创建一个新的(=未见的)数据集,包含 252 条指令和每条指令 1 个实例,并评估一系列指令调优版本,可以看到以下结果:

GPT3 模型及其指令调优版本,在我们 252 条用户导向的指令上经过人类专家评估的表现 | 来源:Self-Instruct: Aligning Language Models with Self-Generated Instructions

GPT3 + Self-Instruct 与其他指令调优版本相比,表现出了令人印象深刻的结果,但与 InstructGPT(OpenAI 之前发布的 LLM)版本相比,仍有提升空间。

增强功能

Self-Instruct 背后的理念简单直接,但同时也非常有说服力,因此让我们看看如何在不同的场景中使用它。

斯坦福 Alpaca³

2023 年,斯坦福大学的 Alpaca LLM 因其低廉的成本、易得性以及开发成本不到 600 美元而引起了极大的关注,同时,它结合了 LLaMA 和 Self-Instruct 的理念。

Alpaca 的高层概述 | 来源:Alpaca: A Strong, Replicable Instruction-Following Model

Alpaca 版本的 Self-Instruct 略有修改:

  • 第一步(指令生成):应用了更为激进的批量解码,即一次生成 20 条指令

  • 第二步(分类任务):此步骤完全被排除

  • 第三步(实例生成):每条指令仅生成一个实例

最终,斯坦福大学的研究人员通过在 Self-Instruct 中的设置取得了显著的改进,并且进行了盲对比测试,比较了 text-davinci-003(InstructGPT-003)和 Alpaca 7B:在 90 对 89 的比较中,Alpaca 战胜了 text-davinci-003。

自奖励语言模型⁴

来源:Self-Rewarding Language Models

在 2024 年,Self-Instruct 是一种在更复杂的设置中使用的实用框架,比如 Meta 的自奖励语言模型中。如同 Self-Instruct 一样,最初我们有一组人工编写的任务;然后我们生成新的指令 {xᵢ} 并提示模型 Mₜ 生成输出 {yᵢ¹, …, yᵢᵏ},接着生成奖励 {rᵢ¹, …, rᵢᵏ} — 这就是通过自指令过程在 InstructGPT 中“消除”人工标注员的方式。Self-Rewarding 模型的最后一个步骤是指令跟随训练——在这个步骤中,我们构造偏好对并通过 DPO 训练 Mₜ₊₁ —— 下一代模型。因此,我们可以反复执行这个过程,以丰富数据集并改善初始的预训练模型。

探索局限性

虽然 Self-Instruct 提供了一种创新的自主管理数据集生成的方法,但它对大型预训练模型的依赖引入了潜在的局限性。

数据质量

尽管在生成合成数据方面展现了令人印象深刻的能力,但质量——在“克服挑战”部分标注为 54%的有效性——仍然是一个关注点。这凸显了一个关键问题:预训练模型中固有的偏差可能在生成的数据集中复制,甚至放大。

尾现象

指令的频率各不相同:一些指令被频繁请求,而另一些则较为罕见。然而,管理这些不常见的请求至关重要,因为它们揭示了 LLM 在处理不常见和创意任务时的脆弱性。

结论

总结来说,Self-Instruct 框架代表了在开发指令调优语言模型方面的一个进步,提供了一种创新的解决方案来应对数据集生成的挑战。使 LLM 能够自主生成多样化且高质量的数据,显著降低了对人工标注员的依赖,从而减少了成本。

除非另有说明,所有图像均由作者提供,灵感来自 Self-Instruct 😃

参考文献:

[1] Ouyang, Long 等. “训练语言模型跟随指令并获得人类反馈”。 神经信息处理系统进展 35 (2022): 27730–27744

[2] Wang, Y., Kordi, Y., Mishra, S., Liu, A., Smith, N.A., Khashabi, D. 和 Hajishirzi, H., 2022. Self-Instruct:使语言模型与自生成指令对齐arXiv 预印本 arXiv:2212.10560

[3] Taori, R., Gulrajani, I., Zhang, T., Dubois, Y., Li, X., Guestrin, C., Liang, P. 和 Hashimoto, T.B., 2023. 斯坦福 Alpaca:一种指令跟随的 Llama 模型

[4] Yuan, W., Pang, R.Y., Cho, K., Sukhbaatar, S., Xu, J. 和 Weston, J., 2024. 自奖励语言模型arXiv 预印本 arXiv:2401.10020

自助式机器学习与关系型深度学习

原文:towardsdatascience.com/self-service-ml-with-relational-deep-learning-beb693a21d5b?source=collection_archive---------9-----------------------#2024-10-22

直接在关系型数据库上进行机器学习

Laurin BrechterTowards Data Science Laurin Brechter

·发布于 Towards Data Science ·阅读时间 7 分钟·2024 年 10 月 22 日

--

我们数据集的关系模式,来源;图像由作者提供

在这篇博客中,我们将深入探讨一种有趣的新型深度学习(DL)方法——关系型深度学习(RDL)。我们还将通过在一个实际的电子商务公司数据库(而不是数据集!)上进行 RDL,来获得一些实践经验。

简介

在现实世界中,我们通常会有一个关系型数据库,我们希望在其上执行一些机器学习任务。但是,特别是当数据库高度规范化时,这意味着需要大量耗时的特征工程,而且由于需要进行许多聚合操作,细节会丢失。此外,我们可以构造出许多不同的特征组合,而每种组合可能都会带来良好的性能[2]。这意味着我们很可能会遗漏一些对机器学习任务相关的信息。

这类似于计算机视觉的早期阶段,在深度神经网络出现之前,当时的特征是通过像素值手工构建的。如今,模型直接处理原始像素,而不再依赖于这一中间层。

关系型深度学习

关系型深度学习(RDL)承诺为表格学习带来相同的效果。也就是说,它通过直接在关系型数据库上进行学习,去除了构建特征矩阵的额外步骤。它通过将数据库及其关系转化为图形来实现这一点,其中表格中的每一行成为一个节点,表格之间的关系成为边缘。行的值存储在节点内部,作为节点特征。

在这篇博客文章中,我们将使用来自 Kaggle 的这个 电子商务数据集 ,它包含了一个电子商务平台的交易数据,采用星型模式,中央事实表(交易)和一些维度表。完整的代码可以在这个 笔记本中找到。

在整篇博客文章中,我们将使用 relbench 库来执行 RDL。在 relbench 中,我们必须做的第一件事是指定我们关系数据库的模式。下面是我们如何为数据库中的“transactions”表指定模式的一个示例。我们将表格作为 pandas 数据框传递,并指定主键和时间戳列。主键列用于唯一标识实体。时间戳确保当我们想要预测未来的交易时,我们只能从过去的交易中学习。在图中,这意味着信息只能从时间戳较低(即过去)的节点流向时间戳较高的节点。此外,我们还指定了关系中存在的外键。在本例中,“transactions”表有一列“customer_key”,它是一个指向“customer_dim”表的外键。

tables['transactions'] = Table(
            df=pd.DataFrame(t),
            pkey_col='t_id',
            fkey_col_to_pkey_table={
                'customer_key': 'customers',
                'item_key': 'products',
                'store_key': 'stores'
            },
            time_col='date'
        )

其余的表格需要以相同的方式进行定义。请注意,如果你已经有了数据库模式,这个过程也可以自动化。由于数据集来自 Kaggle,我需要手动创建模式。我们还需要将日期列转换为实际的 pandas 日期时间对象,并去除任何 NaN 值。

class EcommerceDataBase(Dataset):
    # example of creating your own dataset: https://github.com/snap-stanford/relbench/blob/main/tutorials/custom_dataset.ipynb

    val_timestamp = pd.Timestamp(year=2018, month=1, day=1)
    test_timestamp = pd.Timestamp(year=2020, month=1, day=1)

    def make_db(self) -> Database:

        tables = {}

        customers = load_csv_to_db(BASE_DIR + '/customer_dim.csv').drop(columns=['contact_no', 'nid']).rename(columns={'coustomer_key': 'customer_key'})
        stores = load_csv_to_db(BASE_DIR + '/store_dim.csv').drop(columns=['upazila'])
        products = load_csv_to_db(BASE_DIR + '/item_dim.csv')
        transactions = load_csv_to_db(BASE_DIR + '/fact_table.csv').rename(columns={'coustomer_key': 'customer_key'})
        times = load_csv_to_db(BASE_DIR + '/time_dim.csv')

        t = transactions.merge(times[['time_key', 'date']], on='time_key').drop(columns=['payment_key', 'time_key', 'unit'])
        t['date'] = pd.to_datetime(t.date)
        t = t.reset_index().rename(columns={'index': 't_id'})
        t['quantity'] = t.quantity.astype(int)
        t['unit_price'] = t.unit_price.astype(float)
        products['unit_price'] = products.unit_price.astype(float)
        t['total_price'] = t.total_price.astype(float)

        print(t.isna().sum(axis=0))
        print(products.isna().sum(axis=0))
        print(stores.isna().sum(axis=0))
        print(customers.isna().sum(axis=0))

        tables['products'] = Table(
            df=pd.DataFrame(products),
            pkey_col='item_key',
            fkey_col_to_pkey_table={},
            time_col=None
        )

        tables['customers'] = Table(
            df=pd.DataFrame(customers),
            pkey_col='customer_key',
            fkey_col_to_pkey_table={},
            time_col=None
        )

        tables['transactions'] = Table(
            df=pd.DataFrame(t),
            pkey_col='t_id',
            fkey_col_to_pkey_table={
                'customer_key': 'customers',
                'item_key': 'products',
                'store_key': 'stores'
            },
            time_col='date'
        )

        tables['stores'] = Table(
            df=pd.DataFrame(stores),
            pkey_col='store_key',
            fkey_col_to_pkey_table={}
        )

        return Database(tables)

至关重要的是,作者引入了训练表的概念。这个训练表本质上定义了机器学习任务。这里的思路是,我们希望预测数据库中某个实体的未来状态(即未来的值)。我们通过指定一个表来实现这一点,该表中的每一行都有一个时间戳、实体的标识符和我们希望预测的某些值。id 用于指定实体,时间戳则指定我们需要预测该实体的时间点。这也将限制可用于推断该实体值的数据(即仅限过去的数据)。值本身就是我们想要预测的内容(即真实值)。

在我们的案例中,我们有一个在线平台和顾客。我们想要预测顾客在接下来的 30 天内的收入。我们可以通过执行 DuckDB 的 SQL 语句来创建训练表。这就是 RDL 的巨大优势,因为我们可以仅通过 SQL 创建任何类型的机器学习任务。例如,我们可以定义一个查询来选择买家在接下来的 30 天内的购买次数,从而做出客户流失预测。

df = duckdb.sql(f"""
            select
                timestamp,
                customer_key,
                sum(total_price) as revenue
            from
                timestamp_df t
            left join
                transactions ta
            on
                ta.date <= t.timestamp + INTERVAL '{self.timedelta}'
                and ta.date > t.timestamp
            group by timestamp, customer_key
        """).df().dropna()

结果将是一个表格,其中卖家 ID 作为我们要预测的实体的键,收入作为目标,时间戳作为我们需要进行预测的时间(即我们只能使用直到这个时间点的数据来进行预测)。

训练表格;图片来源:作者

以下是创建“customer_revenue”任务的完整代码。

class CustomerRevenueTask(EntityTask):
    # example of custom task: https://github.com/snap-stanford/relbench/blob/main/tutorials/custom_task.ipynb

    task_type = TaskType.REGRESSION
    entity_col = "customer_key"
    entity_table = "customers"
    time_col = "timestamp"
    target_col = "revenue"
    timedelta = pd.Timedelta(days=30) # how far we want to predict revenue into the future.
    metrics = [r2, mae]
    num_eval_timestamps = 40

    def make_table(self, db: Database, timestamps: "pd.Series[pd.Timestamp]") -> Table:

        timestamp_df = pd.DataFrame({"timestamp": timestamps})

        transactions = db.table_dict["transactions"].df

        df = duckdb.sql(f"""
            select
                timestamp,
                customer_key,
                sum(total_price) as revenue
            from
                timestamp_df t
            left join
                transactions ta
            on
                ta.date <= t.timestamp + INTERVAL '{self.timedelta}'
                and ta.date > t.timestamp
            group by timestamp, customer_key
        """).df().dropna()

        print(df)

        return Table(
            df=df,
            fkey_col_to_pkey_table={self.entity_col: self.entity_table},
            pkey_col=None,
            time_col=self.time_col,
        )

有了这些,我们已经完成了大部分工作。剩下的工作流程将是类似的,独立于 ML 任务。我能够从示例笔记本中复制大部分代码,这些代码是 relbench 提供的。

例如,我们需要对节点特征进行编码。在这里,我们可以使用 GloVe 嵌入来编码所有文本特征,如产品描述和产品名称。

from typing import List, Optional
from sentence_transformers import SentenceTransformer
from torch import Tensor

class GloveTextEmbedding:
    def __init__(self, device: Optional[torch.device
                                       ] = None):
        self.model = SentenceTransformer(
            "sentence-transformers/average_word_embeddings_glove.6B.300d",
            device=device,
        )

    def __call__(self, sentences: List[str]) -> Tensor:
        return torch.from_numpy(self.model.encode(sentences))

之后,我们可以将这些转换应用于我们的数据,并构建出图。

from torch_frame.config.text_embedder import TextEmbedderConfig
from relbench.modeling.graph import make_pkey_fkey_graph

text_embedder_cfg = TextEmbedderConfig(
    text_embedder=GloveTextEmbedding(device=device), batch_size=256
)

data, col_stats_dict = make_pkey_fkey_graph(
    db,
    col_to_stype_dict=col_to_stype_dict,  # speficied column types
    text_embedder_cfg=text_embedder_cfg,  # our chosen text encoder
    cache_dir=os.path.join(
        root_dir, f"rel-ecomm_materialized_cache"
    ),  # store materialized graph for convenience
)

剩下的代码将是从标准层构建 GNN、编写训练循环以及进行一些评估。我会将这些代码省略在这篇博客中,因为它非常标准且在任务之间相同。你可以在这里查看该笔记本。

训练结果,图片来源:作者

因此,我们可以训练这个 GNN,使其达到大约 0.3 的 r2 和 500 的 MAE。这意味着它预测卖家未来 30 天的收入时,平均误差为+-$500。当然,我们无法知道这是否算好,可能通过结合经典 ML 和特征工程,我们能够获得 80%的 r2。

结论

关系深度学习是一种有趣的新型 ML 方法,特别是在我们拥有复杂的关系模式时,人工特征工程会显得过于繁琐。它使我们能够仅通过 SQL 定义 ML 任务,这对于那些没有深入数据科学背景但懂一些 SQL 的人尤其有用。这也意味着我们可以快速迭代并进行大量不同任务的实验。

与此同时,这种方法也存在一些问题,例如训练 GNN 的难度和从关系模式构建图的复杂性。此外,问题在于 RDL 在性能上能否与经典的 ML 模型竞争。过去,我们已经看到像 XGBoost 这样的模型在表格预测问题上比神经网络更优。

参考文献

  • [1] Robinson, Joshua, 等. “RelBench: A Benchmark for Deep Learning on Relational Databases.” arXiv, 2024, arxiv.org/abs/2407.20060

  • [2] Fey, Matthias, 等. “Relational deep learning: Graph representation learning on relational databases.” arXiv 预印本 arXiv:2312.04615 (2023)。

  • [3] Schlichtkrull, Michael, 等. “使用图卷积网络对关系数据建模。” 语义网:第十五届国际会议,ESWC 2018,希腊克里特岛赫拉克利翁,2018 年 6 月 3 日至 7 日,会议录 15. 施普林格国际出版社,2018 年。

为什么基于位置的分块会导致 RAG 性能差?

原文:towardsdatascience.com/semantic-chunking-for-rag-35b7675ffafd?source=collection_archive---------7-----------------------#2024-08-22

如何实现语义分块并获得更好的结果。

Thuwarakesh MurallieTowards Data Science Thuwarakesh Murallie

·发布于 Towards Data Science ·阅读时间 11 分钟·2024 年 8 月 22 日

--

图片来自 vackground.com ,来自 Unsplash

邻居节点可能仍然不同。

语言模型有上下文限制。对于较新的 OpenAI 模型,这个限制大约为 128k 个标记,约 80k 个英文单词。这对于大多数使用场景来说似乎已经足够大。然而,大型生产级应用程序通常需要引用超过 80k 个单词,更不用说图片、表格和其他非结构化信息了。

即使我们将所有内容填充在上下文窗口中,并加入更多无关信息,LLM 的性能也会显著下降。

这就是 RAG 发挥作用的地方。RAG 从嵌入的源中检索相关信息,并将其作为上下文传递给 LLM。为了检索“相关信息”,我们应当将文档划分为多个块。因此,分块在 RAG 流程中起着至关重要的作用。

分块帮助 RAG 检索大型文档中的特定部分。然而,分块策略的微小变化可能会显著影响 LLM 的响应。

为人民而建,由人民主导的语义层

原文:towardsdatascience.com/semantic-layer-for-the-people-and-by-the-people-ce9ecbd0a6f6?source=collection_archive---------6-----------------------#2024-09-23

我的 3 个[+1] Joker 与构建强大分析语义层的模板

Marina TosicTowards Data Science Marina Tosic

·发布于Towards Data Science ·阅读时长 12 分钟·2024 年 9 月 23 日

--

简而言之:

我的 3 个直白的和 1 个隐藏的 Joker 是:

  • Joker #1:模式驱动的仓库结构 🗂️

  • Joker #2:组织化的代码 👩🏻‍💻

  • Joker #3:(非)嵌入式文档 📜

  • [🃏 隐藏的 Joker: 改进循环 🃏]

简单 一致。——这是我在构建语义层时向他人描述的两个最重要的维度。” [照片由Zuzana Ruttkay拍摄,来自Unsplash]

语义——语言意义的研究

根据维基百科,语义这一术语,即语言意义的研究,研究****意义是什么

或者本身——单词是如何获得其意义的以及复杂表达的意义如何依赖于其组成部分[1]。

尽管术语语义有着直接的解释,但老实说,我花了一些时间思考“复杂表达的意义依赖于其组成部分”这一部分,因为我想将它用来解释分析中的语义层

重新阅读后,我的解释如下:

与语言学中的语义学类似,分析中的语义层是关于使数据

使用 AI 😊🌍🚀开发一个支持 50 多种语言的表情符号语义搜索引擎

原文:towardsdatascience.com/semantic-search-for-emojis-in-50-languages-using-ai-f85a36a86f21?source=collection_archive---------7-----------------------#2024-07-17

使用 Python 和开源 NLP 库开发一个 AI 驱动的表情符号语义搜索引擎

Badr Alabsi, 博士Towards Data Science Badr Alabsi, 博士

·发表于Towards Data Science ·阅读时长 12 分钟·2024 年 7 月 17 日

--

如果你常用社交媒体,如 Twitter 或 LinkedIn,你可能注意到表情符号在非正式和正式的文本交流中被创造性地使用。例如,火箭 🚀 表情符号常在 LinkedIn 上表示高远的抱负和雄心壮志,而靶心 🎯 表情符号则用于表示目标达成。尽管表情符号的创造性使用日益增长,但大多数社交媒体平台缺乏帮助用户选择合适表情符号以有效传达信息的工具。因此,我决定投入时间开发一个名为 Emojeez 💎的项目,它是一个 AI 驱动的表情符号搜索和检索引擎。你可以通过这个有趣的互动演示体验 Emojeez 💎。

在这篇文章中,我将讨论我的经验,并解释我如何运用先进的自然语言处理(NLP)技术开发一个语义搜索引擎用于表情符号。具体来说,我将展示一个基于嵌入的语义搜索案例,并按以下步骤进行讲解

  1. 如何使用大语言模型 🦜生成语义丰富的表情符号描述

  2. 如何使用 Hugging Face 🤗 Transformers进行多语言嵌入

  3. 如何集成Qdrant 🧑🏻‍🚀向量数据库以执行高效的语义搜索

我已将该项目的完整代码上传至GitHub

灵感💡

每一个新想法通常都始于一闪而过的灵感。对我来说,这个灵感来自 Luciano Ramalho 的书《流畅的 Python》。这是一本非常棒的书,强烈推荐给所有喜欢编写真正 Pythonic 代码的人。在书的第四章,Luciano 展示了如何通过查询 Unicode 标准中的字符名称来搜索 Unicode 字符。他创建了一个 Python 工具,它能够处理像“cat smiling”这样的查询,检索所有名称中同时包含“cat”和“smiling”的 Unicode 字符。对于“cat smiling”这个查询,该工具可以检索到三个表情符号:😻、😺 和 😸。很酷,对吧?

从那里开始,我开始思考如何利用现代人工智能技术来构建一个更好的表情符号搜索工具。所谓的“更好”,我设想的是一个不仅覆盖更多表情符号,还能支持多语言用户查询的搜索引擎,超越英语。

关键词搜索的局限性 😓

如果你是表情符号爱好者,你一定知道,😻、😺 和 😸 不是唯一的笑脸猫表情符号。确实有些猫咪表情符号是缺失的,特别是 😸 和 😹。这是关键词搜索算法的一个已知限制,关键词搜索依赖于字符串匹配来检索相关项目。关键词搜索或词汇搜索算法,在信息检索领域被认为精确度高,但召回率低。高精度意味着检索到的项目通常与用户查询匹配得很好。另一方面,低召回率意味着算法可能不会检索到所有相关项目。在许多情况下,较低的召回率是由于字符串匹配造成的。例如,表情符号 😹 的名字中没有“smiling”(微笑)——带着喜悦的泪水的猫。因此,如果我们搜索“猫”和“微笑”这两个词,它就无法被检索到。

词汇搜索的另一个问题是,它通常是语言特定的。在 Luciano 的《流畅的 Python》例子中,你无法使用其他语言的查询来查找表情符号,因为所有 Unicode 字符,包括表情符号,都是用英语命名的。为了支持其他语言,我们需要先通过机器翻译将每个查询转换成英语。这会增加更多的复杂性,并且可能并不适用于所有语言。

不过,嘿,已经是 2024 年了,人工智能取得了长足的进步。现在我们已经有了应对这些限制的解决方案。在本文的其余部分,我将向你展示如何实现。

基于嵌入的语义搜索 ✨

近年来,随着深度神经网络在自然语言处理(NLP)领域的普及,出现了一种新的搜索范式。在这种范式中,搜索算法不再关注构成搜索数据库或查询的字符串。相反,它基于文本的数值表示进行操作,这些表示被称为向量嵌入。在基于嵌入的搜索算法中,无论是文本文档还是视觉图像,搜索项首先会被转换为向量空间中的数据点,以便语义相关的项彼此接近。嵌入技术使我们能够基于表情符号描述的意义而非名称中的关键词执行相似性搜索。由于它们基于语义相似性而非关键词相似性来检索项,基于嵌入的搜索算法被称为语义搜索。

使用语义搜索进行表情符号检索解决了两个问题:

  1. 我们可以超越关键词匹配,利用表情符号描述与用户查询之间的语义相似性。这提高了检索到的表情符号的覆盖率,从而提高了召回率。

  2. 如果我们将表情符号表示为多语言嵌入空间中的数据点,我们就可以支持用英语以外的语言编写的用户查询,而无需翻译成英语。这是不是很酷?让我们来看看 👀

第一步:使用 LLM 生成丰富的表情符号描述 🦜

如果你使用社交媒体,你可能知道很多表情符号几乎从未按字面意思使用。例如,🍆和🍑很少表示茄子桃子。社交媒体用户在给表情符号赋予超越字面意义的含义时非常有创意。这种创意限制了 Unicode 标准中表情符号名称的表达性。一个显著的例子是🌈表情符号,它在 Unicode 名称中简单地被描述为彩虹,然而它通常在与多样性、和平和 LGBTQ+社区相关的语境中使用。

为了构建一个有用的搜索引擎,我们需要为每个表情符号生成一个丰富的语义描述,定义该表情符号代表什么以及它象征什么。鉴于当前 Unicode 标准中有超过 5000 个表情符号,手动完成这一任务是不可行的。幸运的是,我们可以利用大型语言模型(LLM)来帮助我们为每个表情符号生成元数据。由于 LLM 是基于整个互联网进行训练的,它们很可能已经见识过每个表情符号在不同语境中的使用方式。

对于这项任务,我使用了🦙 Llama 3大型语言模型(LLM)为每个表情符号生成元数据。我编写了一个提示,定义了任务和 LLM 需要执行的操作。如下面的图所示,LLM 为靶心🎯表情符号生成了丰富的语义描述。这些描述相比于 Unicode 名称,更适合进行语义搜索。我将 LLM 生成的描述发布为 Hugging Face 数据集

使用 Llama 3 LLM 生成表情符号的丰富语义描述。

第 2 步:使用句子变换器将表情符号表示为嵌入 🔄

现在,我们拥有了每个表情符号在 Unicode 标准中的丰富语义描述,下一步是将每个表情符号表示为一个向量嵌入,位于一个多维空间中,该空间捕捉了表情符号描述的含义。为此任务,我使用了基于BERT架构的多语言变换器,并针对 50 种语言的句子相似性进行了微调。您可以在 Hugging Face 🤗库中的模型卡片上查看支持的语言。

到目前为止,我只讨论了由 LLM 生成的表情符号描述的嵌入,这些描述是英文的。但是,我们如何支持英文以外的语言呢?

好的,接下来就是多语言变换器的魔力所在。通过嵌入空间本身启用了多语言支持。这意味着我们可以用任何 50 种支持的语言输入用户查询,并根据它们的英文描述将查询与表情符号匹配。多语言句子编码器(或嵌入模型)将语义相似的文本短语映射到其嵌入空间中的相邻点。让我通过以下插图来展示我的意思。

这是一个多语言嵌入空间的视觉示意图,其中句子和短语根据其语义相似性在几何上组织,无论文本语言如何。图中的阿拉伯语和中文文本是“Cat smiling”短语的字面翻译。

在上面的图中,我们看到语义相似的短语最终成为嵌入空间中相邻的数据点,即使它们是用不同的语言表达的。多语言句子变换器使跨语言搜索应用成为可能,因此用户查询和索引的搜索项不必使用相同的语言。

第 3 步:集成 Qdrant 的向量数据库 🧑🏻‍🚀

一旦我们将表情符号表示为向量嵌入,下一步就是在这些嵌入上构建索引,以便进行高效的搜索操作。为此,我选择了使用Qdrant,这是一个开源的向量相似性搜索引擎,提供高性能的搜索功能。

为此任务设置 Qdrant 非常简单,下面是代码片段(您还可以查看这个 Jupyter Notebook)。

# Load the emoji dictionary from a pickle file
with open(file_path, 'rb') as file:
    emoji_dict: Dict[str, Dict[str, Any]] = pickle.load(file)

# Setup the Qdrant client and populate the database
vector_DB_client = QdrantClient(":memory:")

embedding_dict = {
    emoji: np.array(metadata['embedding']) 
    for emoji, metadata in emoji_dict.items()
}

# Remove the embeddings from the dictionary so it can be used 
# as payload in Qdrant
for emoji in list(emoji_dict):
    del emoji_dict[emoji]['embedding']

embedding_dim: int = next(iter(embedding_dict.values())).shape[0]

# Create a new collection in Qdrant
vector_DB_client.create_collection(
    collection_name="EMOJIS",
    vectors_config=models.VectorParams(
        size=embedding_dim, 
        distance=models.Distance.COSINE
    ),
)

# Upload vectors to the collection
vector_DB_client.upload_points( 
    collection_name="EMOJIS",
    points=[
        models.PointStruct(
            id=idx, 
            vector=embedding_dict[emoji].tolist(),
            payload=emoji_dict[emoji]
        )
        for idx, emoji in enumerate(emoji_dict)
    ],
)

现在,搜索索引vector_DB_client已准备好接受查询。我们需要做的就是将用户查询转换为向量嵌入,使用与我们嵌入表情符号描述时相同的嵌入模型。这个过程可以通过下面的函数完成。

def retrieve_relevant_emojis(
        embedding_model: SentenceTransformer,
        vector_DB_client: QdrantClient,
        query: str, 
        num_to_retrieve: int) -> List[str]:
    """
    Return emojis relevant to the query using sentence encoder and Qdrant. 
    """

    # Embed the query
    query_vector = embedding_model.encode(query).tolist()

    hits = vector_DB_client.search(
        collection_name="EMOJIS",
        query_vector=query_vector,
        limit=num_to_retrieve,
    )

    return hits

为了进一步展示检索到的表情符号、它们与查询的相似度分数以及它们的 Unicode 名称,我编写了以下辅助函数。

def show_top_10(query: str) -> None:
    """
    Show emojis that are most relevant to the query.
    """
    emojis = retrieve_relevant_emojis(
        sentence_encoder, 
        vector_DB_clinet, 
        query, 
        num_to_retrieve=10
    )

    for i, hit in enumerate(emojis, start=1):

        emoji_char = hit.payload['Emoji']
        score = hit.score

        space = len(emoji_char) + 3

        unicode_desc = ' '.join(
           em.demojize(emoji_char).split('_')
        ).upper()

        print(f"{i:<3} {emoji_char:<{space}}", end='')
        print(f"{score:<7.3f}", end= '')
        print(f"{unicode_desc[1:-1]:<55}")

现在一切都准备好了,我们可以看几个例子。还记得 Luciano 书中的“猫咪微笑”查询吗?让我们看看语义搜索与关键词搜索有何不同。

>>> show_top_10('cat smiling')
1   😼   0.651  CAT WITH WRY SMILE                                     
2   😸   0.643  GRINNING CAT WITH SMILING EYES                         
3   😹   0.611  CAT WITH TEARS OF JOY                                  
4   😻   0.603  SMILING CAT WITH HEART-EYES                            
5   😺   0.596  GRINNING CAT                                           
6   🐱   0.522  CAT FACE                                               
7   🐈   0.513  CAT                                                    
8   🐈‍⬛   0.495  BLACK CAT                                              
9   😽   0.468  KISSING CAT                                            
10  🐆   0.452  LEOPARD

太棒了!不仅仅是像 😸、😺 和 😻 这样的猫咪表情符号被关键词搜索检索到,语义搜索还检索出了微笑的猫咪表情符号 😼、😹、🐱 和 😽。这展示了我之前提到的更高召回率或更广泛的检索覆盖面。的确,更多的猫咪总是更好!

语义搜索的真正力量 🪄

前面的“猫咪微笑”示例展示了基于嵌入的语义搜索如何能够检索到更广泛、更有意义的项目,从而改善整体的搜索体验。然而,我认为这个例子并没有真正展示语义搜索的强大功能。

想象一下你在寻找某样东西,但你不知道它的名字。例如,拿 🧿 这个物品来说。你知道它在英语中叫什么吗?我当时是完全不知道。但我对它还是有些了解。在中东和中亚文化中,🧿 被认为能够保护人们免受“恶眼”的伤害。所以,我知道它的作用,但不知道它的名字。

让我们通过描述“防止恶眼”来尝试用搜索引擎找到 🧿 这个表情符号。

>>> show_top_10('protect from evil eye')
1   🧿   0.409  NAZAR AMULET                                           
2   👓   0.405  GLASSES                                                
3   🥽   0.387  GOGGLES                                                
4   👁   0.383  EYE                                                    
5   🦹🏻   0.382  SUPERVILLAIN LIGHT SKIN TONE                           
6   👀   0.374  EYES                                                   
7   🦹🏿   0.370  SUPERVILLAIN DARK SKIN TONE                            
8   🛡️   0.369  SHIELD                                                 
9   🦹🏼   0.366  SUPERVILLAIN MEDIUM-LIGHT SKIN TONE                    
10  🦹🏻‍♂   0.364  MAN SUPERVILLAIN LIGHT SKIN TONE 

结果出来了!原来 🧿 其实叫做 Nazar Amulet(纳扎护符)。我学到了一些新知识 😄

超越英语 🌍 🌏 🌎

我特别希望这个搜索引擎有一个功能,那就是它能够支持尽可能多的语言,除了英语之外。到目前为止,我们还没有测试过这个功能。让我们通过将“防止恶眼”的短语翻译成其他语言并逐一作为查询来测试其多语言能力。以下是一些语言的结果。

阿拉伯语

>>> show_top_10('يحمي من العين الشريرة') # Arabic
1   🧿   0.442  NAZAR AMULET                                           
2   👓   0.430  GLASSES                                                
3   👁   0.414  EYE                                                    
4   🥽   0.403  GOGGLES                                                
5   👀   0.403  EYES                                                   
6   🦹🏻   0.398  SUPERVILLAIN LIGHT SKIN TONE                           
7   🙈   0.394  SEE-NO-EVIL MONKEY                                     
8   🫣   0.387  FACE WITH PEEKING EYE                                  
9   🧛🏻   0.385  VAMPIRE LIGHT SKIN TONE                                
10  🦹🏼   0.383  SUPERVILLAIN MEDIUM-LIGHT SKIN TONE

德语

>>> show_top_10('Vor dem bösen Blick schützen') # Deutsch 
1   😷   0.369  FACE WITH MEDICAL MASK                                 
2   🫣   0.364  FACE WITH PEEKING EYE                                  
3   🛡️   0.360  SHIELD                                                 
4   🙈   0.359  SEE-NO-EVIL MONKEY                                     
5   👀   0.353  EYES                                                   
6   🙉   0.350  HEAR-NO-EVIL MONKEY                                    
7   👁   0.346  EYE                                                    
8   🧿   0.345  NAZAR AMULET                                           
9   💂🏿‍♀️   0.345  WOMAN GUARD DARK SKIN TONE                             
10  💂🏿‍♀   0.345  WOMAN GUARD DARK SKIN TONE

希腊语

>>> show_top_10('Προστατέψτε από το κακό μάτι') #Greek
1   👓   0.497  GLASSES                                                
2   🥽   0.484  GOGGLES                                                
3   👁   0.452  EYE                                                    
4   🕶️   0.430  SUNGLASSES                                             
5   🕶   0.430  SUNGLASSES                                             
6   👀   0.429  EYES                                                   
7   👁️   0.415  EYE                                                    
8   🧿   0.411  NAZAR AMULET                                           
9   🫣   0.404  FACE WITH PEEKING EYE                                  
10  😷   0.391  FACE WITH MEDICAL MASK

保加利亚语

>>> show_top_10('Защитете от лошото око') # Bulgarian
1   👓   0.475  GLASSES                                                
2   🥽   0.452  GOGGLES                                                
3   👁   0.448  EYE                                                    
4   👀   0.418  EYES                                                   
5   👁️   0.412  EYE                                                    
6   🫣   0.397  FACE WITH PEEKING EYE                                  
7   🕶️   0.387  SUNGLASSES                                             
8   🕶   0.387  SUNGLASSES                                             
9   😝   0.375  SQUINTING FACE WITH TONGUE                             
10  🧿   0.373  NAZAR AMULET

中文

>>> show_top_10('防止邪眼') # Chinese
1   👓   0.425  GLASSES                                                
2   🥽   0.397  GOGGLES                                                
3   👁   0.392  EYE                                                    
4   🧿   0.383  NAZAR AMULET                                           
5   👀   0.380  EYES                                                   
6   🙈   0.370  SEE-NO-EVIL MONKEY                                     
7   😷   0.369  FACE WITH MEDICAL MASK                                 
8   🕶️   0.363  SUNGLASSES                                             
9   🕶   0.363  SUNGLASSES                                             
10  🫣   0.360  FACE WITH PEEKING EYE

日语

>>> show_top_10('邪眼から守る') # Japanese 
1   🙈   0.379  SEE-NO-EVIL MONKEY                                     
2   🧿   0.379  NAZAR AMULET                                           
3   🙉   0.370  HEAR-NO-EVIL MONKEY                                    
4   😷   0.363  FACE WITH MEDICAL MASK                                 
5   🙊   0.363  SPEAK-NO-EVIL MONKEY                                   
6   🫣   0.355  FACE WITH PEEKING EYE                                  
7   🛡️   0.355  SHIELD                                                 
8   👁   0.351  EYE                                                    
9   🦹🏼   0.350  SUPERVILLAIN MEDIUM-LIGHT SKIN TONE                    
10  👓   0.350  GLASSES

对于阿拉伯语、德语、希腊语、保加利亚语、中文和日语等如此多样的语言,🧿 表情符号总是出现在前十名中!这真是太吸引人了,因为这些语言拥有不同的语言特征和书写系统,这要归功于我们强大的多语言 🤗 句子变换器。

人工智能的局限性 🙈

最后我要提到的是,任何技术,无论多么先进,都不完美。语义搜索对于提高信息检索系统的召回率非常有帮助。这意味着即使查询和搜索索引中的项目之间没有关键词重叠,我们也能检索到更多相关的项目。然而,这也以牺牲精确度为代价。记住在🧿表情符号示例中的情况,在某些语言中,我们想要找到的表情符号没有出现在前五个结果中。对于这个应用来说,这并不是一个大问题,因为即使它排在第 50 位,我们快速浏览表情符号找到所需的一个也不需要太多认知负担。但在其他情况中,比如搜索长文档,用户可能没有耐心,也没有资源浏览数十篇文档。在构建搜索引擎时,开发人员需要考虑用户的认知能力以及资源限制。我为 Emojeez 💎搜索引擎做出的某些设计选择可能在其他应用中效果并不理想。

还有一点需要提到的是,已知 AI 模型会从训练数据中学习到社会文化偏见。大量文献研究表明,现代语言技术如何能够放大性别刻板印象并对少数群体不公。因此,我们需要意识到这些问题,并在将 AI 应用于现实世界时尽力解决它们。如果你在 Emojeez 💎中发现了此类不良偏见和不公行为,请告诉我,我会尽力处理。

结论

在 Emojeez 💎项目的开发过程中,我经历了一段迷人的旅程,学到了很多关于如何利用现代 AI 和自然语言处理技术来解决传统关键词搜索的局限性。通过利用大语言模型来丰富表情符号的元数据、多语言转换模型来创建语义嵌入,以及使用 Qdrant 进行高效的向量搜索,我成功创建了一个使表情符号搜索更加有趣且在 50 多种语言中可访问的搜索引擎。虽然这个项目主要聚焦于表情符号搜索,但其底层技术在多模态搜索和推荐系统中也有潜在应用。

对于精通英语以外语言的读者,我特别希望听到你们的反馈。Emojeez 💎在英语和你的母语中表现是否同样出色?你是否注意到在质量或准确性上的差异?请试试看,告诉我你的想法。你的见解对我来说非常宝贵。

感谢阅读,我希望你能像我在构建 Emojeez 💎时一样享受探索它的乐趣。

祝你愉快地搜索表情符号!📆😊🌍🚀

注意:除非另有说明,所有图片均由作者创作。

使用 k-Means 算法进行遥感影像的语义分割

原文:towardsdatascience.com/semantic-segmentation-of-remote-sensing-imagery-using-k-means-e4c165d9218e?source=collection_archive---------2-----------------------#2024-03-14

从零开始在 Python 中实现🐍

Aleksei RozanovTowards Data Science Aleksei Rozanov

·发表于 Towards Data Science ·9 分钟阅读·2024 年 3 月 14 日

--

图片由 作者 提供。

在我看来,最简单且最具天才的机器学习模型之一是 k-Means 聚类算法。它属于无监督学习算法的一类,能够在没有标签的数据集中发现模式。它最令人愉悦的特点是没有复杂的数学运算,基本上任何一位高中生都能成功实现并使用这个方法。所以在这篇文章中,我想分享如何仅使用numpypandas库在 Python 中从零开始构建 k-Means 算法,并将其应用于一个实际问题——卫星影像的语义分割。

首先,让我们谈谈我们所拥有的数据。

在我之前的一篇文章中,我谈到了咸海缩小的问题。结果,我们通过 Google Earth Engine 获得了来自 MODIS 的遥感影像,这强烈表明海水正在干涸。因此,我想知道,如何使用机器学习的语义分割来估算 2000 年到 2023 年间水面变化?答案就是 k-Means!

[## 海洋如何消失?使用 Python 和 MODIS 数据对咸海的案例研究。

让我们创建一个时间推移视频,看看它是否真实!

medium.com](https://medium.com/@alexroz/how-can-a-sea-disappear-case-study-of-the-aral-sea-using-python-and-modis-data-c59429cb73dd?source=post_page-----e4c165d9218e--------------------------------)

在深入编码之前,我们先来看一下在本教程中将使用的数据。这是同一地区的两张 RGB 图像,间隔 23 年,然而很明显,土地表面特性和大气条件(云、气溶胶等)是不同的。这就是为什么我决定为每张图像分别训练两个独立的 k-均值模型。

图片来自作者

要跟随这个教程,你可以下载并运行这个笔记本这里

首先,我们导入必要的库并将数据上传到笔记本:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.image as mpimg

img = mpimg.imread('MOD_01.jpg')
img2 = mpimg.imread('MOD_24.jpg')

你可以看到图像覆盖的区域相当大,所以我建议稍微放大一些:

img = img[140:600,110:500,:]
img2 = img2[140:600,110:500,:]

fig, ax = plt.subplots(ncols=2, figsize=(16,9))
ax[0].imshow(img)
ax[1].imshow(img2)
for i in range(2):
  ax[i].set_facecolor('black')
  ax[i].set_xticks([])
  ax[i].set_yticks([])
ax[0].set_title('2000-08-01', fontsize=26)
ax[1].set_title('2023-08-01', fontsize=26)
plt.show()

图片来自作者

在机器学习阶段之前的最后一步,我们将图像转换为pandas数据框(每个图像通道一列)。这样做是为了便于我解释。如果你想要优化,使用numpy数组会更好。

df = pd.DataFrame({'R': img[:,:, 0].flatten(), 'G': img[:,:, 1].flatten(), 'B':img[:,:, 2].flatten()})
df2 = pd.DataFrame({'R': img2[:,:, 0].flatten(), 'G': img2[:,:, 1].flatten(), 'B':img2[:,:, 2].flatten()})

k-均值

那么算法背后的思想是什么呢?

想象一下你用两个标准来判断食物的味道:甜度和价格。记住这一点,我会给你一组可能的食物选项:

图片来自作者

我敢打赌你的大脑已经将选项分成了三个簇:水果、饮料和烘焙食品。基本上,你在无意识中将这 2 维数据进行了聚类,这些数据由一对值定义——(甜度;价格)。

图片来自作者

k-均值的情况下,算法的目标非常相似——在 n 维空间中找到一个预设的簇数量k(例如,除了甜度和价格,你还想考虑营养、健康、冰箱中食物的存在,在这种情况下,n = 5)。

该算法包括以下几个阶段:

I. 定义簇的数量。

如我之前提到的,k在 k-均值中是你最终希望得到的簇的数量,你应该在训练模型之前设置这个值。

II. 随机初始化质心。

质心是 k-均值算法中的一个重要部分。基本上,质心是一个圆形,中心由一组坐标定义,每个质心代表一个簇。例如,在我们之前的例子中有三个质心。

III. 计算距离并分配簇。

现在我们需要找出每个点距离每个质心的远近。根据这些计算,我们将每个点分配给最远的质心(簇)。

IV. 计算新的质心。

现在我们的每个簇至少包含一个点,所以是时候重新计算质心了,方法是取所有簇内点的平均坐标。

就这样!我们重复步骤 2 到 4,直到质心不再发生变化。

图片来源:作者

代码时间。

现在让我们将这个非常简单的 k-Means 思想封装成 Python 代码。

提醒:在这个任务中,我们有3D问题,也就是说,我们的XYZ分别是红色绿色蓝色图像通道!

def kmeans(data, K, kind):
  L = list()
  new_centroids = data.sample(K).values

  data = distance(data.copy(), new_centroids, kind)
  old_centroids = new_centroids.copy()
  new_centroids = np.array([data[data.Class == Class][['R', 'G', 'B']].mean().values for Class in data.loc[:,'C1':f'C{K}'].columns])
  i = 1
  print(f'Iteration: {i}\tDistance: {abs(new_centroids.mean()-old_centroids.mean())}')
  while abs(new_centroids.mean()-old_centroids.mean())>0.001:
    L.append(abs(new_centroids.mean()-old_centroids.mean()))
    data = distance(data, new_centroids, kind)
    old_centroids = new_centroids.copy()
    new_centroids = np.array([data[data.Class == Class][['R', 'G', 'B']].mean().values for Class in data.loc[:,'C1':f'C{K}'].columns])
    i+=1
    print(f'Iteration: {i}\tDistance: {abs(new_centroids.mean()-old_centroids.mean())}')
  print(f"k-Means has ended with {i} iteratinons")
  return data, L

在第一阶段,我们创建一个列表L,收集所有簇之间的距离,方便之后可视化,并从数据集中随机抽取 K 个点作为质心(或者,你也可以为质心分配随机值)。

L = list()
new_centroids = data.sample(K).values

现在我们需要计算质心和数据点之间的距离。在数据科学中有很多不同的距离度量,但让我们聚焦于以下几种——欧几里得距离、曼哈顿距离、切比雪夫距离。

对于欧几里得距离:

图片来源:作者

对于曼哈顿距离:

图片来源:作者

对于切比雪夫距离:

图片来源:作者

为了使用这些公式,让我们为任何维度数量编写一个通用函数:

def distance(data, centroids, kind):
  #kind = euclidean, manhattan, chebyshev
  #Here we add to the dataframe as many clusters C-ith as needed
  cols=list()
  for i in range(1,k+1):
    if kind=='euclidean':
      data[f'C{i}'] = ((centroids[i-1][0]-data.R)**2+(centroids[i-1][1]-data.G)**2+(centroids[i-1][2]-data.B)**2)**0.5
    elif kind=='manhattan':
      data[f'C{i}'] = abs(centroids[i-1][0]-data.R)+abs(centroids[i-1][1]-data.G)+abs(centroids[i-1][2]-data.B)
    elif kind=='chebyshev':
      merged=pd.concat([centroids[i-1][0]-data.R, centroids[i-1][1]-data.G, centroids[i-1][2]-data.B], axis=1)
      data[f'C{i}'] = merged.max(axis=1)
    cols.append(f'C{i}')
  data['Class'] = data[cols].abs().idxmin(axis=1) #assigning clusters to points
  return data #returning the dataframe with k cluster columns and one Class column with the final cluster

现在我们可以简单地计算距离,并将每个数据点分配到一个簇中。因此,我们的新质心变成了旧的,所以我们将它们存储在另一个变量中,并重新计算新的质心。为了做到这一点,我们遍历每个簇,并对所有坐标求均值(在我们的案例中,是对 RGB 通道求均值)。因此,变量 new_centroids 的形状是(k,3)

data = distance(data.copy(), new_centroids, kind)
old_centroids = new_centroids.copy()
new_centroids = np.array([data[data.Class == Class][['R', 'G', 'B']].mean().values for Class in data.loc[:,'C1':f'C{K}'].columns])

最后,我们重复所有这些步骤,直到质心坐标不再变化。我将这个条件表示为:簇的平均坐标差应小于 0.001。但是你可以尝试其他的数值。

while abs(new_centroids.mean()-old_centroids.mean())>0.001:
    L.append(abs(new_centroids.mean()-old_centroids.mean()))
    data = distance(data, new_centroids, kind)
    old_centroids = new_centroids.copy()
    new_centroids = np.array([data[data.Class == Class][['R', 'G', 'B']].mean().values for Class in data.loc[:,'C1':f'C{K}'].columns])

就这样,算法准备好进行训练了!所以我们将 k 设置为 3,并将结果存储到字典中。

k = 3
segmented_1, segmented_2, distances_1, distances_2 = {}, {}, {}, {}
segmented_1['euclidean'], distances_1['euclidean'] = kmeans(df, k, 'euclidean')
segmented_2['euclidean'], distances_2['euclidean'] = kmeans(df2, k, 'euclidean')
segmented_1['manhattan'], distances_1['manhattan'] = kmeans(df, k, 'manhattan')
segmented_2['manhattan'], distances_2['manhattan'] = kmeans(df2, k, 'manhattan')
segmented_1['chebyshev'], distances_1['chebyshev'] = kmeans(df, k, 'chebyshev')
segmented_2['chebyshev'], distances_2['chebyshev'] = kmeans(df2, k, 'chebyshev')

正如你所看到的,我决定比较所有的距离度量,并且很明显在这个任务中曼哈顿距离是最快的。

图片来源:作者

在可视化簇之前,让我们将簇的名称转换为整数类型:

d = {'C1':0, 'C2': 1, 'C3':2}
for key in segmented_1.keys():
  segmented_1[key].Class = segmented_1[key].Class.apply(lambda x: d[x])
  segmented_2[key].Class = segmented_2[key].Class.apply(lambda x: d[x])

现在是时候做出最终的图表了!

for key in segmented_1.keys():
  fig, ax = plt.subplots(ncols=2, nrows=2, figsize=(10,10))
  ax[0, 0].imshow(img)
  ax[0, 1].imshow(segmented_1[key].Class.values.reshape(460,390))
  ax[0, 0].set_title('MOD09GA RGB', fontsize=18)
  ax[0, 1].set_title(f'kMeans\n{key[0].upper()+key[1:]} Distance', fontsize=18)

  ax[1, 0].imshow(img2)
  ax[1, 1].imshow(segmented_2[key].Class.values.reshape(460,390))
  ax[1, 0].set_title('MOD09GA RGB', fontsize=18)
  ax[1, 1].set_title(f'kMeans\n{key[0].upper()+key[1:]} Distance', fontsize=18)

  for i in range(2):
    for j in range(2):
      ax[i, j].set_facecolor('black')
      ax[i, j].set_xticks([])
      ax[i, j].set_yticks([])

  plt.savefig(f'{key}.png')
  plt.tight_layout()
  plt.show()

图片来源:作者

图片来源:作者

图片来源:作者

不难看出,欧几里得距离和曼哈顿距离是最适合这个任务的距离度量。但为了确保这一点,让我们通过轮廓系数评估 k-Means 聚类结果。当没有聚类点的标签真实值时,这个指标对于训练结果的评估非常完美。

为了计算它,我们将使用sklearn函数[1]

图片来源:sklearn

  • a — 样本与同一类中所有其他点之间的平均距离。

  • b — 样本与下一个最近的簇中所有其他点之间的平均距离。

轮廓系数的值范围是[-1,1]。没错,这个计算开销比较大,因为你需要多次计算成千上万的点之间的距离,所以要准备好等待。

scores_1, scores_2 = {}, {}
for key in segmented_1.keys(): #key is a metric for the distance estimation
  scores_1[key]=round(silhouette_score(segmented_1[key].loc[:, :'C3'], segmented_1[key].Class, metric=key),2)
  scores_2[key]=round(silhouette_score(segmented_2[key].loc[:, :'C3'], segmented_2[key].Class, metric=key),2)
  print(f'Distance: {key}\t Img 1: {scores_1[key]}\t Img 2: {scores_2[key]}')

图片来源:作者

现在你可以看到我们已经证明了这一点:欧几里得距离和曼哈顿距离的表现非常相似,所以让我们使用这两者来估计水面面积的损失。

for metric, Class in zip(['euclidean', 'manhattan'], [2,1]):
  img1_water = np.count_nonzero(segmented_1[metric].Class.values == Class)*500*500*1e-6 #pixel size is 500, so the area is 500*500 and to convert to km2 * 1e-6
  img2_water = np.count_nonzero(segmented_2[metric].Class.values == Class)*500*500*1e-6

  print(f'Distance: {metric}\tWater Area Before: {round(img1_water)}km\u00b2\tWater Area After: {round(img2_water)}km\u00b2\tChange: -{100-round(img2_water/img1_water*100)}%')

图片来源:作者

— — — —

距离度量:欧几里得距离

水域面积(前期):17125 平方千米

水域面积(后期):1960 平方千米

变化:-89%

— — — — —

距离度量:曼哈顿距离

水域面积(前期):16244 平方千米

水域面积(后期):2003 平方千米

变化:-88%

如你所见,根据我们的聚类结果,水面面积的变化几乎是90%(!!!)的水体损失,这是亚尔尔海萎缩是一个全球性悲剧的真实证明……

===========================================

参考文献:

[1] scikit-learn.org/stable/modules/generated/sklearn.metrics.silhouette_score.html#sklearn.metrics.silhouette_score

===========================================

我在 Medium 上的所有出版物都是免费的开放访问,所以如果你关注我,我将非常感激!

P.s. 我对(地理)数据科学、机器学习/人工智能和气候变化充满热情。如果你想一起做一些项目,请通过LinkedIn与我联系。

🛰️关注获取更多信息🛰️

语义信号分离

原文:towardsdatascience.com/semantic-signal-separation-769f43b46779?source=collection_archive---------2-----------------------#2024-02-11

使用变换器和主题建模理解语义结构

Márton KardosTowards Data Science Márton Kardos

·发表于 Towards Data Science ·阅读时间 10 分钟·2024 年 2 月 11 日

--

我们生活在大数据的时代。现在说数据是 21 世纪的石油已经成了一个陈词滥调,但它确实如此。数据收集的做法使得几乎每个人手中都堆积了大量数据。

然而,解释数据并非易事,许多行业和学术界仍然依赖于那些提供极少解释的解决方案。虽然深度学习对于预测非常有用,但它很少能让从业者理解数据背后的机制和结构。

文本数据尤其棘手。虽然自然语言和“主题”等概念对人类来说非常直观易懂,但要给语义结构制定操作性定义却远非易事。

在本文中,我将介绍不同的关于发现自然语言中潜在语义结构的概念化方法,我们将看看理论的操作性定义,最后我将通过案例研究展示该方法的实用性。

理论:什么是“主题”?

尽管“主题”对我们人类来说似乎是一个完全直观且不言自明的术语,但当我们试图给它一个有用且信息丰富的定义时,情况却并非如此。幸运的是,牛津词典的定义可以帮助我们:

一种被讨论、写作或研究的主题。

嗯,这并没有让我们更接近可以用计算术语表述的内容。请注意,“主题”一词被用来掩盖所有复杂的细节。然而,这不应使我们气馁,我们当然可以做得更好。

学科的语义空间

在自然语言处理(NLP)中,我们经常使用语义的空间定义。听起来可能有些花哨,但本质上我们假设文本/语言的语义内容可以在某个连续的空间中表达(通常是高维空间),在这个空间中,相关的概念或文本会比不相关的更接近。如果我们接受这种语义理论,我们可以很容易地为主题提出两种可能的定义。

作为语义聚类的主题

一种直观的概念化是将主题视为语义空间中相互紧密相关的段落/概念组,而这些组与其他文本的相关性较低。顺便提一下,这意味着一个段落只能属于一个主题

学科的语义聚类

这种聚类概念化也有助于我们从层级的角度思考主题。你可以想象,“动物”这个主题可能包含两个子聚类,一个是“真核生物”,另一个是“原核生物”,然后你可以沿着这个层次结构往下走,直到在树的叶子节点,你会发现实际的概念实例。

当然,这种方法的一个局限性是,较长的段落可能包含多个主题。解决这一问题的一种方法是将文本拆分成更小的原子部分(例如单词),然后在这些部分上建模,但我们也可以完全抛弃聚类的概念化。

作为语义轴的主题

我们还可以将主题视为语料库中语义空间的潜在维度。换句话说:我们不是描述有哪些文档群组,而是通过寻找潜在的语义信号来解释文档中的变化。

学科语义空间中的潜在轴

我们通过找到潜在的语义信号来解释文档中的变化。

你可以举个例子,想象一下,餐厅评论背后最重要的语义轴可能是:

  1. 对食物的满意度

  2. 对服务的满意度

希望你能理解为什么这种概念化对于某些目的有用。我们不再仅仅寻找“好评”和“差评”,而是去理解这些评价之间差异的驱动因素。这种理论化的流行文化例子当然是政治坐标系。同样,我们并不是仅仅寻找“保守派”和“进步派”,而是去找到那些区分它们的因素

让我们开始建模吧!

既然我们已经解决了哲学问题,就可以动手设计基于我们概念理解的计算模型了。

语义表示

经典的语义表示方法是所谓的词袋模型。本质上,你做出了一个非常强烈、几乎是显而易见的错误假设,即文档中无序的词汇集合构成了它的语义内容。虽然这些表示方式存在许多问题(如维度灾难、离散空间等),但通过数十年的研究,它们已被证明是有用的。

幸运的是,技术已经超越了这些表示方式,我们可以使用能够在上下文中表示文本的模型。Sentence Transformers是基于变换器(Transformer)的模型,可以将段落编码成一个高维的连续空间,其中语义相似性通过向量之间的高余弦相似度来表示。在这篇文章中,我将主要关注使用这些表示的模型。

聚类模型

目前在主题建模社区中最广泛使用的模型(如Top2Vec, BERTopic)是基于聚类的主题概念化方法。

BERTopic 发现的语义空间中的集群(图源自 BERTopic 文档)

他们在以下步骤中发现主题:

  1. 使用UMAP降低语义表示的维度

  2. 使用HDBSCAN发现集群层次结构

  3. 使用事后描述性方法(c-TF-IDF,接近聚类中心)估计每个集群的术语重要性

这些模型已经获得了广泛的关注,主要得益于它们可解释的主题描述、恢复层次结构的能力,以及从数据中学习主题数目的能力。

如果我们想要建模主题内容的细微差别,并理解语义的因素,单靠聚类模型是不够的。

我不打算深入讨论这些方法的实际优势和局限性,但它们中的大多数源于上述的哲学考虑。

语义信号分离

如果我们要在语料库中发现语义轴线,我们将需要一个新的统计模型。

我们可以从经典的主题模型中汲取灵感,例如潜在语义分配(LSA)。LSA 利用矩阵分解技术,在词袋模型表示中找到潜在的成分。LSA 的主要目标是找出高度相关的词汇,并将它们的共现解释为一个潜在的语义成分。

由于我们不再处理词袋模型,单纯解释相关性可能不是最优策略。正交性并不等于统计独立性。换句话说:仅仅因为两个组件不相关,并不意味着它们在统计上是独立的。

正交性并不等于统计独立性

其他学科幸运地提出了分解模型,可以发现最大程度上独立的组件。独立成分分析已经广泛应用于神经科学中,用于从脑电图数据中发现并去除噪声信号。

使用 PCA 和 ICA 演示正交性与独立性的区别(图源自 scikit-learn 的文档)

语义信号分离的主要思想是,我们可以通过使用独立成分分析(ICA)分解表示,发现语料库中文本的最大独立语义信号。

我们可以通过提取语料库中在给定维度上排名最高的术语,获得人类可读的主题描述。

案例研究:机器学习论文

为了展示语义信号分离在理解语料库中语义变化方面的有效性,我们将在一个大约 118k 机器学习摘要的数据集上拟合一个模型。

再次重申我们这里要实现的目标:我们希望建立所有机器学习论文分布的维度。换句话说,我们想为这个语料库建立一个语义空间理论。

为此,我们将使用我开发的一个 Python 库,叫做Turftopic,它实现了大多数使用 transformer 表示的主题模型,包括语义信号分离。此外,我们还将安装 HuggingFace 数据集库,以便下载当前的语料库。

pip install turftopic datasets

让我们从 HuggingFace 下载数据:

from datasets import load_dataset

ds = load_dataset("CShorten/ML-ArXiv-Papers", split="train")

然后,我们将对这些数据运行语义信号分离。我们将使用all-MiniLM-L12-v2句子变换器,因为它速度较快,但提供合理高质量的嵌入。

from turftopic import SemanticSignalSeparation

model = SemanticSignalSeparation(10, encoder="all-MiniLM-L12-v2")
model.fit(ds["abstract"])

model.print_topics()

语义信号分离在摘要中找到的主题

这些是我们在语料库中找到的十个维度的最高排名关键词。你可以看到,大多数这些关键词相当容易理解,并且已经帮助你看出机器学习论文中的差异背后的根本原因。

我将集中讨论三个维度,稍微随意一些,因为我发现它们很有趣。我是一个贝叶斯信徒,所以主题 7 看起来很有意思,因为它似乎描述了概率性、基于模型和因果性的论文。主题 6 似乎与噪声检测和去除有关,而主题 1 主要涉及测量设备。

我们将生成一个图表,展示词汇的一个子集,并显示每个术语在这些组成部分上的排名。

首先,让我们从模型中提取词汇,并选择一些词汇显示在我们的图表上。我选择了基于频率处于 99 百分位数的词汇(这样它们仍然能在散点图上保持一定的可见性)。

import numpy as np

vocab = model.get_vocab()

# We will produce a BoW matrix to extract term frequencies
document_term_matrix = model.vectorizer.transform(ds["abstract"])
frequencies = document_term_matrix.sum(axis=0)
frequencies = np.squeeze(np.asarray(frequencies))

# We select the 99th percentile
selected_terms_mask = frequencies > np.quantile(frequencies, 0.99)

我们将创建一个DataFrame,包含三个选定维度和术语,以便后续轻松绘图。

import pandas as pd

# model.components_ is a n_topics x n_terms matrix
# It contains the strength of all components for each word.
# Here we are selecting components for the words we selected earlier

terms_with_axes = pd.DataFrame({
    "inference": model.components_[7][selected_terms],
    "measurement_devices": model.components_[1][selected_terms],
    "noise": model.components_[6][selected_terms],
    "term": vocab[selected_terms]
 })

我们将使用 Plotly 图形库创建一个交互式散点图进行解释。X 轴将是推断/贝叶斯主题,Y 轴将是噪声主题,点的颜色将由测量设备主题决定。

import plotly.express as px

px.scatter(
    terms_with_axes,
    text="term",
    x="inference",
    y="noise",
    color="measurement_devices",
    template="plotly_white",
    color_continuous_scale="Bluered",
).update_layout(
    width=1200,
    height=800
).update_traces(
    textposition="top center",
    marker=dict(size=12, line=dict(width=2, color="white"))
)

按语义轴分布的语料库中最常见术语的图

我们已经可以从这个可视化中推断出很多关于语料库语义结构的信息。例如,我们可以看到,关注效率、在线拟合和算法的论文在统计推断上的得分非常低,这在某种程度上是直观的。另一方面,语义信号分离已经帮助我们通过基于数据的方法确认,深度学习论文并不太关注统计推断和贝叶斯建模。我们可以从词汇“网络”和“网络”(以及“卷积”)在我们的贝叶斯轴上的排名非常低看出这一点。这是该领域受到的一项批评。我们刚刚通过实证证据支持了这一说法。

深度学习论文对统计推断和贝叶斯建模并不十分关注,这是该领域受到的一项批评。我们刚刚通过实证证据支持了这一说法。

我们还可以看到,聚类和分类对噪声非常关注,但基于代理的模型和强化学习则不太关注。

另外,我们可能观察到的一个有趣模式是噪声轴与测量设备之间的关系。术语“图像”、“图像”、“检测”和“鲁棒”在我们的测量轴上得分非常高。这些词汇也位于图表中噪声检测/去除较高的区域,而统计推断的讨论较少。这向我们表明,测量设备捕获了大量噪声,而文献试图解决这些问题,但主要不是通过将噪声纳入统计模型,而是通过预处理。这是非常有道理的,因为例如,神经科学以其非常广泛的预处理流程而著称,而且许多神经科学模型在处理噪声时遇到困难。

测量设备输出中的噪声通过预处理来应对

我们还可以观察到,在测量设备上的得分最低的术语是“文本”和“语言”。这似乎表明,自然语言处理和机器学习研究并不太关注语言的神经学基础和心理语言学。注意,“潜在”和“表示”在测量设备上的得分也相对较低,这表明神经科学中的机器学习研究并不特别关注表示学习。

文本和语言与测量设备的关系很少

当然,从这里出发的可能性是无限的,我们可以花费更多时间解释模型的结果,但我的目的是展示,通过使用语义信号分离,我们已经能够在语料库中找到观点,并建立一个语义学理论。

语义信号分离应该主要作为一种探索性手段,用于建立理论,而不是将其结果视为假设的证明。

我想强调的一点是,语义信号分离应该主要作为一种探索性手段,用于建立理论,而不是将其结果视为假设的证明。我的意思是,我们的结果足以让我们直观地理解语料库中区分因素的差异,然后建立一个关于发生了什么以及为什么发生的理论,但这不足以证明该理论的正确性。

结论

探索性数据分析可能会令人困惑,当然也没有一种适用于所有情况的解决方案来理解你的数据。我们一起探讨了如何通过基于模型的方法,从理论到计算公式,再到实践,提升我们对数据的理解。

我希望本文在分析大规模文本语料库中的话语时能对你有所帮助。如果你打算深入了解主题模型和探索性文本分析,确保也查看一下我其他的文章,因为它们会更详细地讨论这些主题的某些方面。

((除非另有说明,所有图表均由作者制作。))

语义压缩文本以节省 LLM 成本

原文:towardsdatascience.com/semantically-compress-text-to-save-on-llm-costs-0b3e62b0c43a?source=collection_archive---------2-----------------------#2024-12-20

LLM 很棒……前提是它们能容纳你的所有数据

Lou KratzTowards Data Science Lou Kratz

·发表于 Towards Data Science ·8 分钟阅读·2024 年 12 月 20 日

--

图片由 Christopher Burns 提供,来自 Unsplash

最初发布于 https://blog.developer.bazaarvoice.com 2024 年 10 月 28 日。

引言

大型语言模型是处理非结构化文本的绝佳工具,但如果你的文本无法适应上下文窗口该怎么办?Bazaarvoice 在构建我们的 AI 评论摘要功能时正面临着这个挑战:数百万条用户评论根本无法容纳到即使是最新的 LLM 的上下文窗口中,即便能够容纳,也会非常昂贵。

在这篇文章中,我将分享 Bazaarvoice 如何通过压缩输入文本而不丢失语义来解决这个问题。具体而言,我们使用了一种多次传递的层次聚类方法,使我们能够明确地调整想要在压缩中失去的细节程度,而不管选择的是哪种嵌入模型。最终的技术使我们的评论摘要功能在财务上变得可行,并为我们未来继续扩展业务奠定了基础。

问题

Bazaarvoice 已经收集了近 20 年的用户生成产品评论,因此我们拥有 大量 的数据。这些产品评论完全是非结构化的,长度和内容各不相同。大型语言模型是处理非结构化文本的优秀工具:它们能够处理非结构化数据,并在干扰信息中识别出相关的信息片段。

然而,LLM 也有它们的局限性,其中之一就是上下文窗口:即一次可以输入到网络中的标记数量(大致为单词数量)。如 Athropic 的 Claude 3 版本这样的最先进的大型语言模型具有高达 200,000 个标记的极大上下文窗口。这意味着你可以将小小说嵌入其中,但互联网仍然是一个庞大且不断增长的数据集合,我们的用户生成的产品评论也不例外。

在构建“评论摘要”功能时,我们遇到了上下文窗口的限制,该功能可以总结我们客户网站上某个特定产品的所有评论。然而,在过去的 20 年里,许多产品积累了成千上万的评论,这些评论迅速超出了 LLM 的上下文窗口限制。事实上,我们甚至有些产品拥有数百万条评论,这就需要对 LLM 进行大规模的重新工程,才能够在一次提示中处理这些评论。

即使从技术上可行,成本也会相当高昂。所有大型语言模型(LLM)提供商都会根据输入和输出的标记数量收费。当我们接近每个产品的上下文窗口限制时,考虑到我们拥有数百万条数据,我们很容易就会产生超过六位数的云托管费用。

我们的方法

尽管面临这些技术和财务的限制,我们还是成功地发布了“评论摘要”功能,专注于一个非常简单的见解:许多评论传达的是相同的意思。实际上,摘要的核心理念就是基于这一点:评论摘要捕捉了评论者反复出现的见解、主题和情感。我们意识到可以利用这一数据重复性来减少需要发送到 LLM 的文本量,从而避免达到上下文窗口限制,同时减少我们系统的运营成本。

为了实现这一目标,我们需要识别出表达相同意思的文本段落。这类任务说起来容易做起来难:人们常常用不同的词汇或短语来表达相同的意思。

幸运的是,识别文本是否语义相似一直是自然语言处理领域的一个活跃研究方向。Agirre 等人在 2013 年的研究(SEM 2013 共享任务:语义文本相似度。在第二届词汇和计算语义联合会议)中,甚至发布了一个由人类标注的语义相似句子数据集,称为 STS Benchmark。在这个数据集中,他们要求人类根据 1 到 5 的评分标准,指示文本句子是否语义相似或不同,如下表所示(来自 Cer 等人的《SemEval-2017 任务 1:语义文本相似性多语种和跨语种聚焦评估》):

STSBenchmark 数据集通常用于评估文本嵌入模型在其高维空间中能多好地关联语义相似的句子。具体来说,使用皮尔逊相关系数来衡量嵌入模型在多大程度上能够代表人类的判断。

因此,我们可以使用这种嵌入模型从产品评论中识别语义相似的短语,然后在将其发送到 LLM 之前删除重复的短语。

我们的方法如下:

  • 首先,产品评论被分割成句子。

  • 使用在 STS 基准测试上表现良好的网络为每个句子计算嵌入向量。

  • 对每个产品的所有嵌入向量使用凝聚聚类。

  • 每个聚类中 — 离聚类中心最近的句子 — 会被保留并发送到 LLM,聚类中的其他句子会被丢弃。

  • 任何小的聚类被认为是异常值,这些异常值会被随机抽样以供 LLM 使用。

  • 每个聚类所代表的句子数量会包含在 LLM 提示中,以确保每个情感的权重得到考虑。

这在以项目符号列出的形式下看起来似乎很简单,但在我们可以信任这种方法之前,还有一些细节问题需要解决。

嵌入模型评估

首先,我们必须确保我们使用的模型能够有效地将文本嵌入到一个空间中,在这个空间里,语义相似的句子靠得很近,而语义不相似的句子远离。为此,我们仅使用了 STS 基准测试数据集,并为我们想要评估的模型计算了皮尔逊相关性。我们使用 AWS 作为云服务提供商,因此自然地,我们希望评估他们的Titan 文本嵌入模型。

下表显示了不同 Titan 嵌入模型在 STS 基准测试上的皮尔逊相关性:

(最先进的技术可以在这里查看)

因此,AWS 的嵌入模型在嵌入语义相似句子方面非常出色。这对我们来说是一个好消息 — 我们可以直接使用这些模型,并且它们的成本非常低。

语义相似聚类

我们面临的下一个挑战是:如何在聚类过程中强制执行语义相似性?理想情况下,任何聚类中都不应有两句话,其语义相似性低于人类可接受的水平 — 上表中的 4 分。这些分数,然而,并不能直接转换为嵌入距离,而嵌入距离正是凝聚聚类所需要的阈值。

为了解决这个问题,我们再次转向了 STS 基准测试数据集。我们计算了训练数据集中所有对的距离,并从分数到距离阈值拟合了一个多项式。

图片由作者提供

这个多项式让我们能够计算出满足任何语义相似性目标所需的距离阈值。对于评论摘要,我们选择了 3.5 分,因此几乎所有聚类都包含“粗略”到“基本上”相等的句子,或者更多。

值得注意的是,这可以在任何嵌入网络上进行。这使我们能够在嵌入网络可用时进行实验,并且在需要时快速交换它们,而不必担心聚类会出现语义上不相似的句子。

多次聚类

到目前为止,我们知道可以信任我们的语义压缩,但不清楚我们能从数据中获得多少压缩。正如预期的那样,压缩量在不同的产品、客户和行业之间有所不同。

在没有语义信息丢失的情况下,即设置为硬性阈值为 4 时,我们仅实现了 1.18 的压缩比(即节省了 15%的空间)。

显然,无损压缩不足以使这个功能在经济上可行。

然而,我们上面讨论的距离选择方法提供了一个有趣的可能性:我们可以通过在较低的阈值下重复进行聚类,逐渐增加信息损失,以处理剩余数据。

该方法如下:

  • 使用分数为 4 的阈值再次运行聚类。这被认为是无损的。

  • 选择任何离群聚类,即那些仅包含少数向量的聚类。这些被视为“未压缩”的,并用于下一阶段。我们选择对任何小于 10 的聚类重新运行聚类。

  • 再次运行聚类,选择分数为 3 的阈值。这不是无损的,但也不算太差。

  • 选择任何小于 10 的聚类。

  • 根据需要重复,持续降低分数阈值。

因此,在每次聚类过程中,我们牺牲了更多的信息损失,但获得了更多的压缩,并且没有混淆第一次聚类时选择的无损代表性短语。

此外,这种方法非常有用,不仅适用于评论摘要,我们希望在牺牲一定压缩的情况下保持较高的语义相似度,还适用于其他用例,可能我们不太关心语义信息损失,但希望在提示输入上花费更少。

实际上,即使在多次降低分数阈值后,仍然有大量仅包含一个向量的聚类。这些被视为离群值,并被随机抽样以包含在最终的提示中。我们选择样本大小,以确保最终提示包含 25,000 个标记,但不超过。

确保真实性

多次聚类和随机离群值抽样允许在交换较小的上下文窗口(发送给 LLM 的窗口)时损失语义信息。这引出了一个问题:我们的摘要有多好?

在 Bazaarvoice,我们知道真实性是消费者信任的必要条件,我们的评论摘要必须保持真实性,才能真正代表评论中所捕获的所有声音。任何有损压缩方法都有可能误表示或排除那些花时间撰写评论的消费者。

为了确保我们的压缩技术有效,我们直接进行了测量。具体来说,对于每个产品,我们采样了一些评论,然后使用LLM Evals来判断摘要是否能代表每条评论并与其相关。这为我们提供了一个硬性指标,用于评估和权衡我们的压缩效果。

结果

在过去的 20 年里,我们收集了近十亿条用户生成的评论,并需要为数千万个产品生成摘要。许多这些产品有成千上万条评论,有些甚至多达百万条,这会耗尽 LLM 的上下文窗口,并大幅提高成本。

然而,使用我们上面的方法,我们将输入文本的大小减少了97.7%(压缩比为42),使我们能够将这一解决方案扩展到所有产品,并应对未来任何数量的评论量。

此外,为我们的十亿规模数据集生成摘要的成本减少了82.4%。这包括了嵌入句子数据并将其存储在数据库中的成本。

未观察到的混杂因素的敏感性分析

原文:towardsdatascience.com/sensitivity-analysis-for-unobserved-confounding-465970a969e0?source=collection_archive---------10-----------------------#2024-02-13

如何在观察性研究中了解那些无法知晓的事物

Ugur YildirimTowards Data Science Ugur Yildirim

·发表于 Towards Data Science ·阅读时间 10 分钟·2024 年 2 月 13 日

--

大纲

  1. 引言

  2. 问题设置

    2.1. 因果图

    2.2. 有无 Z 的模型 2.3. 作为混杂因素的 Z 的强度

  3. 敏感性分析

    3.1. 目标

    3.2. 稳健性值

  4. PySensemakr

  5. 结论

  6. 致谢

  7. 参考文献

1. 引言

未观察到的混杂因素(即遗漏变量偏倚)是观察性研究中的一个著名问题。在大多数观察性研究中,除非我们能合理假设治疗分配在某些方面是随机的,如同自然实验一样,否则我们永远无法真正确定我们在模型中控制了所有可能的混杂因素。因此,如果我们未能控制住一个重要的混杂因素,我们的模型估计值可能会严重偏倚——而我们甚至不知道,因为未观察到的混杂因素,嗯,确实是未观察到的!

鉴于这个问题,评估我们的估计值对潜在未观察到的混杂因素的敏感性非常重要。换句话说,问问自己一个问题是有帮助的:如果未观察到的混杂因素足够大,我们的估计值会发生剧烈变化(例如,治疗效应不再具有统计显著性)吗?未观察到的混杂因素的敏感性分析是一个活跃的研究领域,解决这个问题有几种方法。在本文中,我将介绍一种基于部分 概念的简单线性方法[1],这种方法广泛适用于大多数情况。

2. 问题设置

2.1. 因果图

假设我们有四个变量:

  • Y: 结果

  • D: 治疗

  • X: 观察到的混杂因素

  • Z: 未观察到的混杂变量

这是许多观察性研究中的常见情境,研究人员希望在控制可能的治疗—结果混杂变量后,了解所关注的治疗是否对结果产生影响。

在我们的假设情境中,这些变量之间的关系是XZ都影响DY,但DY没有影响。换句话说,我们描述的是一个真实治疗效应为零的情景。正如下一部分将明确指出的,敏感性分析的目的是能够推理出这个治疗效应,尽管我们通常无法访问Z,因为它是未观察到的。图 1 展示了我们的设定。

图 1:问题设定

2.2. 带Z和不带Z的模型

为了展示未观察到的Z可能造成的问题,我根据上述问题设定模拟了一些数据。你可以参考这个笔记本查看模拟的详细信息。

由于Z在现实生活中无法观察到,我们通常能拟合到数据的唯一模型是Y~D+X。让我们看看如果我们运行这个回归,结果会是什么。

根据这些结果,似乎DY的每单位变化有统计显著效应 0.2686(p<0.001),但我们知道这不符合事实,因为我们生成数据的方式是没有D效应的。

现在,让我们看看当我们同时控制Z时,D估计值会发生什么变化。(在现实生活中,当然我们无法进行这个额外的回归,因为Z是未观察到的,但我们的模拟设置允许我们窥探真正的数据生成过程。)

正如预期的那样,控制Z后,D效应被正确移除,估计值收缩至零,并且我们得到的p值在𝛼=0.05 的显著性水平下不再显著(p=0.059)。

2.3. Z作为混杂变量的强度

到目前为止,我们已经确认Z足够强大,能够消除虚假的D效应,因为在我们控制Z之后,统计上显著的D效应消失了。我们尚未讨论的是Z作为混杂变量的具体强度。为此,我们将利用一个有用的统计学概念,叫做部分,它量化了一个给定的兴趣变量能够解释的变异量的比例,这些变异量是模型中现有变量无法解释的。换句话说,部分告诉我们该变量的额外解释力,超出了模型中已包含的其他变量。形式上,它可以定义如下:

其中,RSS_reduced 是不包含感兴趣变量的模型的残差平方和,而 RSS_full 是包含感兴趣变量的模型的残差平方和。

在我们的案例中,感兴趣的变量是 Z,我们想知道 Z 能解释 YD 中的变异性有多少是现有变量无法解释的。更准确地说,我们关注的是以下两个部分 值:

其中 (1) 量化了 Y 中无法通过 DX 已经解释的部分,能够被 Z 解释的方差比例(因此,简化模型为 Y~D+X,完整模型为 Y~D+X+Z),(2) 量化了 D 中无法通过 X 已经解释的部分,能够被 Z 解释的方差比例(因此,简化模型为 D~X,完整模型为 D~X+Z)。

现在,让我们看看 Z 在我们的数据中与 DY 的关联程度,具体是通过部分 来衡量。

结果表明,Z 解释了 Y 中 16% 的变异性,这是 DX 无法解释的部分(这就是上面提到的部分 方程 #1),并且解释了 D 中 20% 的变异性,这是 X 无法解释的部分(这就是上面提到的部分 方程 #2)。

3. 敏感性分析

3.1. 目标

正如我们在前一节中讨论的,未观察到的混杂因素在真实研究环境中构成问题,正是因为,和我们的模拟设置不同,Z 无法被观察到。换句话说,我们只能使用模型 Y~D+X,而无法知道如果能运行模型 Y~D+X+Z,我们的结果会是什么样子。那么,我们能做什么呢?

直观地说,一个合理的敏感性分析方法应该能够告诉我们,如果数据中存在类似于 Z 的变量,它将使我们的结果失效。记住,Z 解释了 Y 中 16% 的变异性,以及 D 中 20% 的变异性,这些是现有变量无法解释的部分。因此,我们期望敏感性分析能够告诉我们,一个假设的、类似 Z 强度的混杂因素将足以消除统计上显著的 D 效应。

但是,如何计算未观察到的混杂因素的强度应该落在这个 16% 到 20% 范围内,而且我们根本无法接触到它 呢?这时就需要引入稳健性值。

3.2. 稳健性值

稳健性值(RV)正式化了我们上面提到的概念,即确定一个假设的未观察到的混杂因素的必要强度,足以使我们的结果失效。稳健性值的实用性在于,我们只需要我们的可观察模型 Y~D+X,而不需要不可观察的模型 Y~D+X+Z,就能够计算出它。

正式地,我们可以将其写为如下形式,来量化需要多强的未观察到的混杂因素,才能改变我们观察到的处理效应的统计显著性(如果符号太复杂跟不上,可以记住这个关键点:RV 是衡量混杂因素强度的度量,足以改变我们的结果)

图片由作者提供,方程式基于[1],参见第 49–52 页

其中

  • 𝛼是我们选择的显著性水平(通常设置为 0.05 或 5%),

  • q决定了我们关心的显著性减少的百分比q**100%(通常设置为 1,因为我们通常关心能够将统计显著性减少 1100%=100%的混杂因素,从而使其不再具有统计显著性),

  • t_betahat_treat是我们从模型Y~D+X中观察到的* t*-值(在这种情况下为 8.389,可以从上面的回归结果中看到),

  • df是我们的自由度(在这种情况下为 1000–3=997,因为我们模拟了 1000 个样本,并且正在估计包括截距在内的 3 个参数),并且

  • t_alpha,df-1是与给定𝛼和df-1(如果𝛼设置为 0.05,则为 1.96)相关的t*-值临界值。

我们现在可以使用仅观察到的模型Y~D+Xres_ydx)在我们自己的数据中计算 RV 了。

我们的 RV(18%)恰好落在我们为YZ|D,X*(16%)和*DZ|X(20%)计算的部分值范围内,这并非偶然。这里 RV 告诉我们的是,即使没有 Z 的明确知识,我们仍然可以推理出,任何未观察到的混杂因素平均需要至少 18%的强度,才能在部分尺度上对处理和结果都产生影响,从而使得我们原本显著的结果不再显著。

RV 之所以不是 16%或 20%,而是落在中间(18%),是因为它被设计为一个单一的数字,总结了与结果和处理相关的混杂因素的必要强度,因此 18%是合情合理的,考虑到我们对数据的了解。你可以这样理解:由于该方法在计算 RV 时没有实际访问 16%和 20%的数字,它尽力通过将 18%分配给两个部分的值(Y~Z|D,XD~Z|X),来量化混杂因素的强度,这一点与实际情况相差不大,实际上也很好地总结了混杂因素的强度。

当然,在现实生活中,我们不会有 Z 变量来再次检查我们的 RV 是否正确,但看到这两个结果的对齐至少可以让你对这个方法有一些信心。最后,一旦我们计算出 RV,我们应该考虑一个如此强度的未观察到的混杂因素是否合理。在我们的例子中,答案是“是的”,因为我们可以访问数据生成过程,但对于你的特定实际应用,假设存在如此强的混杂因素可能是不合理的。对你来说这是好消息,因为没有现实中未观察到的混杂因素可以极大地改变你的结果。

4. PySensemakr

上述敏感性分析技术已经以 PySensemakr 这个 Python 包的形式实现了,且具备了所有的功能(也有 R、Stata 和 Shiny App 版本)。例如,要获得与我们在前一部分手动计算的完全相同的结果,我们只需运行以下代码块。

请注意,“Robustness Value, q = 1 alpha = 0.05” 的值为 0.184,这正是我们之前计算的结果。除了用于统计显著性的 RV,软件包还提供了用于将系数估计值收缩至 0 所需的 RV。毫不奇怪,为了实现这一点,未观察到的混杂因素需要更大(0.233 与 0.184)。

该软件包还提供了两个部分 值的轮廓图,这可以直观地显示治疗和结果可能的混杂程度对敏感性的影响(在这种情况下,看到满足红色虚线的 x/y 轴值对包括 0.18/0.18 和 0.20/0.16 是不足为奇的)。

你甚至可以将基准值添加到轮廓图中,作为混杂因素可能程度的代理。在我们的例子中,由于我们只有一个观察到的协变量 X,我们可以将基准值设置为与该观察到的协变量强度相当的 0.25x、0.5x 和 1x。由此得到的图表告诉我们,一个强度仅为 X 一半的混杂因素应该足以使我们统计显著的结果无效(因为“0.5x X”值恰好位于红色虚线处)。

最后,我想指出,尽管本示例中的模拟数据使用了一个连续的处理变量,但在实践中,该方法适用于任何类型的处理变量,包括二元处理。另一方面,结果变量在技术上需要是连续型的,因为我们处在 OLS 框架中。然而,即使结果是二元的,只要我们通过 OLS 建模(这称为 LPM [2]),该方法仍然可以使用。

5. 结论

我们的效应估计可能由于未观察到的混杂因素而产生偏差,这是观察性研究中的常见风险。尽管存在这一潜在风险,观察性研究仍然是数据科学中一项至关重要的工具,因为在许多情况下,随机化实验是不可行的。因此,了解如何通过进行敏感性分析来解决未观察到的混杂因素问题,以查看我们的估计在潜在混杂因素下的鲁棒性,是非常重要的。

本文讨论的 Cinelli 和 Hazlett 提出的鲁棒值方法是一种简单直观的敏感性分析方法,基于熟悉的线性模型框架。如果你有兴趣深入了解该方法,我强烈推荐阅读原始论文以及包文档,在其中你可以了解该方法的许多有趣应用,如“极端情景”分析。

还有许多其他针对未观察到的混杂因素的敏感性分析方法,我在此简要提及其中一些,供有兴趣深入学习该主题的读者参考。一个多功能的技术是 VanderWeele 和 Ding 开发的 E 值,它将问题表述为风险比 [3](在 R 中实现的版本 这里)。另一种技术是 Veitch 和 Zaveri 基于部分和倾向评分概念开发的 Austen 图 [4](在 Python 中实现的版本 这里),还有一种最近的办法是 Chernozhukov 等人提出的 [5](在 Python 中实现的版本 这里)。

6. 致谢

我想感谢 Chad Hazlett 解答我关于如何在二项结果中使用该方法的问题,并感谢 Xinyi Zhang 对本文提供了大量有价值的反馈。除非另有注明,文中的所有图片均由作者提供。

7. 参考文献

[1] C. Cinelli 和 C. Hazlett, 理解敏感性:扩展遗漏变量偏差(2019),《皇家统计学会学报》

[2] J. Murray,线性概率模型,Murray 的个人网站

[3] T. VanderWeele 和 P. Ding,观察性研究中的敏感性分析:引入 E 值(2017),《内科学年鉴》

[4] V. Veitch 和 A. Zaveri, 敏感性与灵敏度分析:对未观察到的混杂因素引起的偏差的简单事后分析 (2020),NeurIPS

[5] V. Chernozhukov, C. Cinelli, W. Newey, A. Sharma, 和 V. Syrgkanis, 简而言之:因果机器学习中的遗漏变量偏差 (2022),NBER

精通传感器融合:基于 KITTI 数据的 LiDAR 障碍物检测 — 第一部分

原文:towardsdatascience.com/sensor-fusion-kitti-lidar-based-obstacle-detection-part-1-9c5f4bc8d497?source=collection_archive---------4-----------------------#2024-12-25

如何使用 LiDAR 数据进行无监督学习的障碍物检测

Erol ÇıtakTowards Data Science Erol Çıtak

·发表于 Towards Data Science ·阅读时间 20 分钟 ·2024 年 12 月 25 日

--

传感器融合,多模态感知,自动驾驶车辆 — 如果这些关键词引起了你的兴趣,那么这篇 Medium 博客就是为你准备的。跟随我一起探索 LiDAR 和基于彩色图像的环境理解的迷人世界,展示这些技术如何结合以增强障碍物检测和自动驾驶车辆的决策能力。本博客及随后的系列将深入探讨实际应用和理论见解,提供一个引人入胜的阅读体验,适合所有好奇的读者。

在这个 Medium 博客系列中,我们将分三部分来探讨 KITTI 3D 物体检测数据集 [1][3]。在第一篇文章中,也就是这篇文章,我们将讨论 KITTI Velodyne Lidar 传感器以及仅使用该传感器的单模态障碍物检测。在系列的第二篇文章中,我们将基于单一模态方法进行彩色图像的检测研究。在系列的最后一篇文章中,我们将探讨多模态物体检测,也就是传感器融合。在这个过程中,Lidar 和彩色图像传感器将共同工作。

在我们进入主题之前,有最后一点小提示!我保证在整个系列中,我将以基础的层次提供每个子主题的所有理论信息 😃 但是,我也会为每个子主题提供非常高质量的参考资料,并且不会忘记那些想要深入了解的人。

引言

KITTI 或 KITTI 视觉基准测试套件是一个由卡尔斯鲁厄理工大学和丰田研究院合作创建的项目。我们可以说它是一个平台,包含了许多不同的测试场景,包括 2D/3D 目标检测、多目标跟踪、语义分割等。

对于 3D 目标检测,这是本文系列的主题,数据集包含 7481 条训练数据和 7518 条测试数据,数据来源于不同的传感器,包括 Velodyne 激光雷达传感器和立体视觉图像传感器。

3D 目标检测的示例图像 [3](图片来自 www.cvlibs.net/datasets/kitti/eval_object.php?obj_benchmark=3d

在这篇博文中,我们将使用 Velodyne 激光雷达点云进行障碍物检测。在这个背景下,利用无监督机器学习算法进行点云的读取、可视化和分割将是主要内容。除了这些,我们还会讨论很多关于相机标定及其内外部参数、用于车辆路径检测的 RANSAC 算法,以及在执行这些步骤时我们需要的评估指标。

同时,在这一系列文章中,我将使用 Python 语言,但不用担心,我会与大家分享我使用的虚拟环境信息。这样你就可以快速搭建自己的环境。请查看 Github 仓库以获取requirements.txt文件。

问题定义

这篇博文的主要目标是使用无监督学习方法,通过对 KITTI 数据集中由 Velodyne 激光雷达获取的点云,检测环境中的障碍物。

在这个范围内,我将分享一个示例的激光雷达点云图像,以便可视化问题。如果我们分析下面的示例点云,我们可以很容易地识别出左下角的一些汽车或路上的其他物体。

一个示例的激光雷达点云 [3](来自 KITTI 数据集)

为了使其更加清晰,我将在图像中画一些箭头和框来表示它们。在下图中,红色箭头表示汽车,橙色箭头表示行人,红色框表示路灯。

一个示例的激光雷达点云 [3](来自 KITTI 数据集)

然后,你可能会好奇并问这个问题 “我们是不是也可以说周围还有其他物体,可能是墙壁或者树木?” 答案是肯定的!我的答案的证明可以从与这个点云对应的彩色图像中得到。从下面的图像中可以看到,现场有行人、汽车、路灯和树木。

一个示例的彩色图像 [3](来自 KITTI 数据集)

在这一视觉分析之后,我们来到了一个细心读者会立刻注意到的问题。虽然激光雷达点云提供了场景的 360 度视角,彩色图像却只能提供有限的场景宽度感知。接下来的博客将只考虑使用这一彩色图像进行目标检测,最后一篇将尝试融合激光雷达点云和彩色图像传感器来解决这个问题(我希望它们很快就能使用!)

传感器设置

接下来我们来讨论传感器及其安装等问题。KITTI 3D 目标检测数据集是通过专门改装的大众帕萨特 B6 收集的。数据记录由一台八核 i7 计算机处理,该计算机配备 RAID 系统,运行 Ubuntu Linux,并配有实时数据库以便高效的数据管理。

以下传感器用于数据采集:

  • 惯性导航系统(GPS/IMU): OXTS RT 3003

  • 激光雷达传感器: Velodyne HDL-64E

  • 灰度相机: 两台 Point Grey Flea 2(FL2–14S3M-C),每台 1.4 百万像素

  • 彩色相机: 两台 Point Grey Flea 2(FL2–14S3C-C),每台 1.4 百万像素

  • 可变焦镜头: 四个 Edmund Optics NT59–917(4–8 毫米)

上述设置的可视化展示在下图中。

KITTI 数据集设置可视化 [3](图片来源:KITTI)

Velodyne 激光雷达传感器和彩色相机安装在汽车的顶部,但它们距离地面的高度和坐标不同。别担心!如承诺所示,我们将一步一步来。这意味着,在进入本博客文章的核心算法之前,我们需要先回顾一下相机标定的主题!

相机标定

相机或更广义上的传感器,以不同的方式提供了周围环境的感知输出。在这个概念中,我们以 RGB 相机为例,它可能是你的网络摄像头,也可能是专业的数码紧凑型相机。它使用两组参数:内参和外参,将世界中的 3D 点投影到二维图像平面上。

将世界中的 3D 点投影到 2D 图像平面上(图片来源:de.mathworks.com/help/vision/ug/camera-calibration.html

外参参数描述了相机在世界坐标系中的位置和方向,而内参参数则将相机坐标映射到图像坐标系中的像素坐标。

在这个概念中,相机的外参可以表示为一个矩阵,形式为 T = [R | t],其中 R 是旋转矩阵(3x3),t 是平移向量(3x1)。因此,T 矩阵是一个 3x4 矩阵,它将世界中的一个点映射到“相机坐标”域。

另一方面,相机的内参可以表示为一个 3x3 矩阵。对应的矩阵 K 可以表示如下。这里,fx 和 fy 表示相机的焦距,cx 和 cy 表示主点,s 表示像素的偏斜度。

相机的内参

因此,任何 3D 点都可以通过以下完整相机矩阵投影到 2D 图像平面。

将 3D 世界点投影到图像平面所需的完整相机矩阵

我知道相机标定看起来有点复杂,特别是当你第一次接触时。但是我已经为你找到了一些非常好的参考资料。另外,我将在接下来的章节中讲解我们问题中应用的相机标定操作。

相机标定主题的参考文献:

— 卡内基梅隆大学, https://www.cs.cmu.edu/~16385/s17/Slides/11.1_Camera_matrix.pdf

— 哥伦比亚大学, https://www.youtube.com/watch?v=GUbWsXU1mac

— 相机标定中篇文章, https://yagmurcigdemaktas.medium.com/visual-perception-camera-calibration-9108f8be789

数据集理解

经过一些术语和所需的基础理论后,现在我们可以进入问题的核心了。

首先,我强烈建议你从这里下载数据集[2],用于接下来的操作;

  • 左侧彩色图像(大小为 12GB)

  • Velodyne 点云(大小为 29GB)

  • 对象数据集的相机标定矩阵(大小可以忽略不计)

  • 训练标签(大小可以忽略不计)

我们将要分析的数据是地面实况(G.T.)标签文件。G.T. 文件以 '.txt' 格式呈现,每个物体都用 15 个不同的字段进行标注。别担心,我已经为你准备了一个详细的 G.T. 文件读取函数,存放在我的 Github 仓库中,具体如下。

def parse_label_file(label_file_path):
    """
    KITTI 3D Object Detection Label Fields:

    Each line in the label file corresponds to one object in the scene and contains 15 fields:

    1\. Type (string):
    - The type of object (e.g., Car, Van, Truck, Pedestrian, Cyclist, etc.).
    - "DontCare" indicates regions to ignore during training.

    2\. Truncated (float):
    - Value between 0 and 1 indicating how truncated the object is.
    - 0: Fully visible, 1: Completely truncated (partially outside the image).

    3\. Occluded (integer):
    - Level of occlusion:
        0: Fully visible.
        1: Partly occluded.
        2: Largely occluded.
        3: Fully occluded (annotated based on prior knowledge).

    4\. Alpha (float):
    - Observation angle of the object in the image plane, ranging from [-π, π].
    - Encodes the orientation of the object relative to the camera plane.

    5\. Bounding Box (4 floats):
    - (xmin, ymin, xmax, ymax) in pixels.
    - Defines the 2D bounding box in the image plane.

    6\. Dimensions (3 floats):
    - (height, width, length) in meters.
    - Dimensions of the object in the 3D world.

    7\. Location (3 floats):
    - (x, y, z) in meters.
    - 3D coordinates of the object center in the camera coordinate system:
        - x: Right, y: Down, z: Forward.

    8\. Rotation_y (float):
    - Rotation around the Y-axis in camera coordinates, ranging from [-π, π].
    - Defines the orientation of the object in 3D space.

    9\. Score (float) [optional]:
    - Confidence score for detections (used for results, not training).

    Example Line:
    Car 0.00 0 -1.82 587.00 156.40 615.00 189.50 1.48 1.60 3.69 1.84 1.47 8.41 -1.56

Notes:
    - "DontCare" objects: Regions ignored during training and evaluation. Their bounding boxes can overlap with actual objects.
    - Camera coordinates: All 3D values are given relative to the camera coordinate system, with the camera at the origin.
    """ 

彩色图像以文件形式呈现于文件夹中,且可以轻松读取,无需进一步操作。通过此操作,得到的结果是训练和测试图像的数量:7481 / 7518

接下来我们要考虑的数据是每个场景的标定文件。像之前一样,我为你准备了另一个函数,用于解析标定文件,具体如下。

def parse_calib_file(calib_file_path):
    """
        Parses a calibration file to extract and organize key transformation matrices.

        The calibration file contains the following data:
        - P0, P1, P2, P3: 3x4 projection matrices for the respective cameras.
        - R0: 3x3 rectification matrix for aligning data points across sensors.
        - Tr_velo_to_cam: 3x4 transformation matrix from the LiDAR frame to the camera frame.
        - Tr_imu_to_velo: 3x4 transformation matrix from the IMU frame to the LiDAR frame.

        Parameters:
        calib_file_path (str): Path to the calibration file.

        Returns:
        dict: A dictionary where each key corresponds to a calibration parameter 
            (e.g., 'P0', 'R0') and its value is the associated 3x4 NumPy matrix.

        Process:
        1\. Reads the calibration file line by line.
        2\. Maps each line to its corresponding key ('P0', 'P1', etc.).
        3\. Extracts numerical elements, converts them to a NumPy 3x4 matrix, 
        and stores them in a dictionary.

        Example:
        Input file line for 'P0':
        P0: 1.0 0.0 0.0 0.0  0.0 1.0 0.0 0.0  0.0 0.0 1.0 0.0
        Output dictionary:
        {
            'P0': [[1.0, 0.0, 0.0, 0.0],
                [0.0, 1.0, 0.0, 0.0],
                [0.0, 0.0, 1.0, 0.0]]
        }
    """

最终数据是 Velodyne 点云,格式为 '.bin'。在此格式中,每一行点云包含 x、y、z 的位置以及反射率分数。与之前一样,相应的解析函数如下所示。

def read_velodyne_bin(file_path):
    """
    Reads a KITTI Velodyne .bin file and returns the point cloud data as a numpy array.

    :param file_path: Path to the .bin file
    :return: Numpy array of shape (N, 4) where N is the number of points,
             and each point has (x, y, z, reflectivity)

    ### For KITTI's Velodyne LiDAR point cloud, the coordinate system used is forward-right-up (FRU).
    KITTI Coordinate System (FRU):
        X-axis (Forward): Points in the positive X direction move forward from the sensor.
        Y-axis (Right): Points in the positive Y direction move to the right of the sensor.
        Z-axis (Up): Points in the positive Z direction move upward from the sensor.

    ### Units: All coordinates are in meters (m). A point (10, 5, 2) means:

        It is 10 meters forward.
        5 meters to the right.
        2 meters above the sensor origin.
        Reflectivity: The fourth value in KITTI’s .bin files represents the reflectivity or intensity of the LiDAR laser at that point. It is unrelated to the coordinate system but adds extra context for certain tasks like segmentation or object detection.

        Velodyne Sensor Placement:

        The LiDAR sensor is mounted on a vehicle at a specific height and offset relative to the car's reference frame.
        The point cloud captures objects relative to the sensor’s position.

    """

在本节结束时,所有必需的文件将被加载并准备好使用。

对于在本篇文章“问题定义”部分顶部展示的示例场景,点云中有 122794 个点。

但是,由于这些信息对某些系统(特别是 CPU 或 GPU 性能较低的系统)来说可能较难处理,我们可能希望减少点云中的点数。为了实现这一点,我们可以使用“体素下采样”操作,这与深度神经网络中的“池化”操作类似。大致来说,它将完整的点云划分为一个等大小的体素网格,并从每个体素中选择一个点。

print(f"Points before downsampling: {len(sample_point_cloud.points)} ")
sample_point_cloud = sample_point_cloud.voxel_down_sample(voxel_size=0.2)
print(f"Points after downsampling: {len(sample_point_cloud.points)}")

此下采样的输出结果如下:

下采样前的点数:122794

下采样后的点数:33122

但是,不应忽视的是,减少点数可能会导致一些信息的丢失,这是可以预见的。此外,体素网格的大小是一个超参数,选择它也非常关键。较小的体素大小会返回更多的点,反之亦然。

但是,在进行 RANSAC 道路分割之前,让我们快速回顾一下体素下采样操作。

体素下采样

体素下采样是一种创建下采样点云的技术。它有助于减少噪声和不必要的点。同时,它通过选择的体素网格大小超参数减少了所需的计算能力。此操作的可视化可以如下展示。

体素下采样的示意图(图片来源:www.mdpi.com/2076-3417/14/8/3160

除此之外,该算法的步骤可以如下展示。

为了应用此功能,我们将使用“open3d”库,代码只需一行;

sample_point_cloud = sample_point_cloud.voxel_down_sample(voxel_size=0.2)

在上述单行代码中,可以观察到体素大小被设置为 0.2

RANSAC

下一步将是分割最大的平面,即我们的道路问题。RANSAC(随机采样一致性)是一种迭代算法,它通过随机采样数据点的子集来假设一个模型,并评估其与整个数据集的拟合度。其目标是找到能够最好地解释内点的模型,同时忽略离群值。

尽管该算法对极端离群值具有很强的鲁棒性,但在开始时需要采样n个点(二维线条时 n=2,三维平面时 n=3)。然后评估该数学方程在此点集下的表现。也就是说;

— 起始时选择的点非常关键

— 查找最佳值的迭代次数至关重要

— 它可能需要一定的计算能力,特别是对于大型数据集

但它已成为多种不同情况的事实标准操作。所以,首先让我们可视化 RANSAC 来寻找一个二维线条,然后再介绍该算法的关键步骤。

RANSAC 算法的关键步骤和工作流程

在回顾了 RANSAC 的概念之后,现在是时候将算法应用于点云,以确定最大平面,这就是我们问题中的道路。

# 3\. RANSAC Segmentation to identify the largest plane
plane_model, inliers = sample_point_cloud.segment_plane(distance_threshold=0.3, ransac_n=3, num_iterations=150)

## Identify inlier points -> road
inlier_cloud = sample_point_cloud.select_by_index(inliers)
inlier_cloud.paint_uniform_color([0, 1, 1]) # R, G, B format

## Identify outlier points -> objects on the road
outlier_cloud = sample_point_cloud.select_by_index(inliers, invert=True)
outlier_cloud.paint_uniform_color([1, 0, 0]) # R, G, B format

这个过程的输出将显示道路外部为红色,且道路将被涂成绿色和蓝色的混合色。

RANSAC 算法的输出(图片来自 KITTI 数据集[3])

DBSCAN — 一种基于密度的无参数聚类算法

在这一阶段,将使用 RANSAC 对分割后的道路版本进行物体检测,从而检测道路外的物体。

在这种情况下,我们将使用无监督学习算法。然而,可能会有一个问题浮现在脑海中:“难道不能使用监督学习算法进行检测吗?” 答案既简短又明确:可以!然而,由于我们希望通过这篇博客快速介绍问题并得到结果,因此我们将继续使用 DBSCAN,这是一种无监督学习领域的分割算法。如果你希望看到基于监督学习的点云物体检测算法的结果,请在评论中注明。

不管怎样,让我们尝试回答这三个问题:什么是 DBSCAN,它是如何工作的?需要考虑哪些超参数?我们如何将其应用于这个问题?

DBSCAN,也被称为基于密度的无参数聚类算法,是一种无监督的聚类算法。尽管还有其他一些无监督聚类算法,例如 K-Means,但 DBSCAN 能够对形状任意的物体进行聚类,而 K-Means 假设物体的形状是球形的。此外,DBSCAN 最重要的特性之一是,它不需要像 K-Means 算法那样预先定义或估算聚类的数量。如果你想查看一些针对特定问题(如“2Moons”)的优秀可视化结果,可以访问这里:https://www.kaggle.com/code/ahmedmohameddawoud/dbscan-vs-k-means-visualizing-the-difference

DBSCAN 的工作原理类似于我们的眼睛。这意味着它根据数据中不同组的密度做出聚类决策。它有两个不同的超参数:“Epsilon”和“MinimumPoints”。最初,DBSCAN 会识别核心点,即在指定半径内至少有最小数量邻居(minPts)的点。然后,从这些核心点扩展,形成聚类,连接所有满足密度标准的可达点。无法与任何聚类连接的点被分类为噪声。要深入了解这个算法中的“核心点”、“边界点”和“噪声点”,请访问:Josh Starmer, https://www.youtube.com/watch?v=RDZUdRSDOok&t=61s

DBSCAN 算法的一个示例聚类结果

对于我们的任务,虽然我们可以使用 SKLearn 库中的 DBSCAN,但让我们使用 open3d,代码如下。

# 4\. Clustering using DBSCAN -> To further segment objects on the road
with o3d.utility.VerbosityContextManager(o3d.utility.VerbosityLevel.Debug) as cm:
    labels = np.array(outlier_cloud.cluster_dbscan(eps=0.45, min_points=10, print_progress=True))

如我们所见,‘epsilon’ 被选择为 0.45,‘MinPts’ 被选择为 10。简单评论一下这些。由于它们是超参数,实际上没有最佳的“数值”。不幸的是,这需要通过尝试和测量成功来进行调整。但别担心!在阅读了本博客文章的最后一章《评估指标》后,你将能够全面衡量你的算法表现。那时,你就可以应用 GridSearch (参考: https://www.analyticsvidhya.com/blog/2021/06/tune-hyperparameters-with-gridsearchcv/) 来找到最佳的超参数组合!

是的,然后让我可视化 DBCAN 对我们点云的输出,然后我们继续下一步!

DBSCAN 聚类算法的输出(图像来自 KITTI 数据集 [3])

回顾一下,我们可以看到一些我最初展示并手动标记的物体在这里是分开的,并且显示为不同的颜色!这表明这些物体属于不同的聚类(这正是应该的)。

G.T. 标签及其标定过程

现在是分析 KITTI 3D 目标检测基准的 G.T. 标签和标定文件的时候了。在前一部分中,我分享了一些关于它们的小贴士,比如如何读取、如何解析等。

但现在我想提一下 G.T. 物体与标定矩阵之间的关系。首先,让我并排展示 G.T. 文件和标定文件的图示。

一个 .txt 格式的示例训练标签文件

如我们之前讨论的,训练标签的最后一个元素表示物体围绕 y 轴的旋转。旋转元素之前的三个数字(1.84、1.47 和 8.41)代表物体质心在相机坐标系中的三维位置。

一个 .txt 格式的示例标定文件

在标定文件方面;P0, P1, P2P3 是对应相机的投影矩阵。在本文中,正如我们之前所指出的,我们使用的是‘左侧彩色图像’,即 P2。此外,R0_rect 是用于对齐立体图像的矩阵。从它们的名称可以理解,Tr_velo_to_camTr_imu_to_velo 是转换矩阵,用于在不同坐标系之间进行转换。例如,Tr_velo_to_cam 是一个转换矩阵,用于将 Velodyne 坐标转换为未校正的相机坐标系。

在这个解释之后,我真的很注意哪个矩阵或哪个标签在哪个坐标系中,现在我们可以轻松地提到从 G.T.物体坐标到 Velodyne 坐标系的变换。这是一个很好的点,可以帮助我们理解坐标系之间矩阵的使用,并评估我们预测的边界框和 G.T.物体边界框。

我们要做的第一件事是计算 G.T.物体的 3D 边界框。为此,您可以访问以下代码库中的函数。

def compute_box_3d(obj, Tr_cam_to_velo):
    """
    Compute the 8 corners of a 3D bounding box in Velodyne coordinates.
    Args:
        obj (dict): Object parameters (dimensions, location, rotation_y).
        Tr_cam_to_velo (np.ndarray): Camera to Velodyne transformation matrix.
    Returns:
        np.ndarray: Array of shape (8, 3) with the 3D box corners.
    """

给定一个物体在相机坐标系中的尺寸(heightwidthlength)和位置(x, y, z),该函数首先根据物体的朝向(rotation_y)旋转边界框,然后计算出 3D 空间中边界框的角点。

这个计算是基于一种变换,它使用一个能够将任何点从相机坐标系统转换到 Velodyne 坐标系统的矩阵。但是,等一下?我们没有相机到 Velodyne 的矩阵,对吧?是的,我们需要首先通过取Tr_velo_to_cam矩阵的逆矩阵来计算它,这个矩阵在标定文件中给出。

别担心,所有这些工作流都由这些函数呈现。

def transform_points(points, transformation):
    """
    Apply a transformation matrix to 3D points.
    Args:
        points (np.ndarray): Nx3 array of 3D points.
        transformation (np.ndarray): 4x4 transformation matrix.
    Returns:
        np.ndarray: Transformed Nx3 points.
    """
def inverse_rigid_trans(Tr):
    """
    Inverse a rigid body transform matrix (3x4 as [R|t]) to [R'|-R't; 0|1].
    Args:
        Tr (np.ndarray): 4x4 transformation matrix.
    Returns:
        np.ndarray: Inverted 4x4 transformation matrix.
    """

最后,我们可以轻松地看到 G.T.物体并将它们投影到 Velodyne 点云坐标系中。现在让我们可视化输出结果,然后进入评估部分!

投影后的 G.T.物体边界框(图片来自 KITTI 数据集[3])

(我知道绿色边框可能有点难以看清,所以我在它们旁边加上了黑色的箭头。)

评估指标

现在我们有了通过我们的管道预测的边界框和 G.T.物体框!接下来,我们来计算一些指标,评估我们的管道。为了执行我们之前讨论的超参数优化,我们必须能够持续监控每个参数组的性能。

但在进入评估指标之前,我需要提到两件事。首先,KITTI 对不同物体有不同的评估标准。例如,对于行人,标签与 G.T.之间 50%的匹配度就足够了,而对于车辆则是 70%。另一个问题是,尽管我们创建的管道在 360 度环境中执行物体检测,但 KITTI 的 G.T.标签仅包括在彩色相机视角内的物体标签值。因此,我们可以检测到比 G.T.标签文件中呈现的更多边界框。那么该怎么办呢?基于我在这里要讲的概念,您可以通过仔细分析 KITTI 的评估标准来得出最终结果。但现在,我不会在这一部分做更详细的分析,而是会在这篇 Medium 博客系列的后续文章中进行。

为了评估预测的边界框和 G.T.边界框,我们将使用 TP、FP 和 FN 指标。

TP 代表与真实边界框匹配的预测框,FP 代表与任何真实边界框都不匹配的预测框,FN 则表示没有与真实边界框对应的预测边界框的情况。

在这种情况下,我们当然需要找到一个工具来衡量预测的边界框与真实边界框的匹配度。我们工具的名称是 IOU,即交并比。

你可以通过以下方式轻松访问 IOU 和评估功能。

def compute_iou(box1, box2):
    """
    Calculate the Intersection over Union (IoU) between two bounding boxes.
    :param box1: open3d.cpu.pybind.geometry.AxisAlignedBoundingBox object for the first box
    :param box2: open3d.cpu.pybind.geometry.AxisAlignedBoundingBox object for the second box
    :return: IoU value (float)
    """
# Function to evaluate metrics (TP, FP, FN)
def evaluate_metrics(ground_truth_boxes, predicted_boxes, iou_threshold=0.5):
    """
    Evaluate True Positives (TP), False Positives (FP), and False Negatives (FN).
    :param ground_truth_boxes: List of AxisAlignedBoundingBox objects for ground truth
    :param predicted_boxes: List of AxisAlignedBoundingBox objects for predictions
    :param iou_threshold: IoU threshold for a match
    :return: TP, FP, FN counts
    """ 

让我通过在点云上给出预测的边界框(红色)和真实边界框(绿色)来总结这一部分内容。

预测的边界框和真实边界框一起显示在点云上(图像来源于 KITTI 数据集[3])

结论

是的,虽然有点长,但我们快要完成了。首先,我们已经学到了关于 KITTI 3D 目标检测基准的一些知识以及一些与不同主题相关的术语,如相机坐标系统和无监督学习等。

现在,有兴趣的读者可以通过添加网格搜索来扩展本研究,以找到最佳的超参数元素。例如,分割中的最小点数,或者可能是 RANSAC 的迭代次数,或者体素下采样操作中的体素网格大小,所有这些都是潜在的改进点。

接下来是什么?

下一部分将研究仅在左侧彩色相机帧上的目标检测。这是本系列的另一个基础步骤,因为我们将在本博客系列的最后部分融合激光雷达点云和彩色相机帧。然后我们将能够得出结论并回答这个问题:“传感器融合是否减少了不确定性并提高了 KITTI 3D 目标检测基准的性能?”

欢迎任何评论、错误修复或改进!

感谢大家,祝你们健康!


Github 链接: github.com/ErolCitak/KITTI-Sensor-Fusion/tree/main/lidar_based_obstacle_detection

参考文献

[1] — www.cvlibs.net/datasets/kitti/

[2] — www.cvlibs.net/datasets/kitti/eval_object.php?obj_benchmark=3d

[3] — Geiger, Andreas, 等. “Vision meets robotics: The kitti dataset.” 国际机器人研究杂志 32.11 (2013): 1231–1237.

免责声明

本系列博客中使用的图像来自 KITTI 数据集,旨在用于教育和研究。如果你希望将其用于类似的目的,必须访问相关网站,批准在那里规定的用途,并按照基准创建者定义的引用格式使用。

对于立体视觉 2012光流 2012里程计物体检测,或跟踪基准,请引用:

@inproceedings{Geiger2012CVPR,

author = {Andreas GeigerPhilip LenzRaquel Urtasun},

title = {我们准备好迎接自动驾驶了吗?KITTI 视觉基准套件},

booktitle = {计算机视觉与模式识别会议(CVPR)},

year = {2012}

}

对于原始数据集,请引用:

@article{Geiger2013IJRR,

author = {Andreas GeigerPhilip LenzChristoph StillerRaquel Urtasun},

title = {视觉与机器人学相遇:KITTI 数据集},

journal = {国际机器人研究杂志(IJRR)},

year = {2013}

}

对于道路基准,请引用:

@inproceedings{Fritsch2013ITSC,

author = {Jannik Fritsch 和 Tobias Kuehnl 和 Andreas Geiger},

title = {一种新的性能度量和道路检测算法评估基准},

booktitle = {国际智能交通系统会议(ITSC)},

year = {2013}

}

对于立体视觉 2015光流 2015,和场景流 2015 基准,请引用:

@inproceedings{Menze2015CVPR,

author = {Moritz MenzeAndreas Geiger},

title = {自动驾驶车辆的物体场景流},

booktitle = {计算机视觉与模式识别会议(CVPR)},

year = {2015}

}

SentenceTransformer:一种计算句子嵌入的模型

原文:towardsdatascience.com/sentencetransformer-a-model-for-computing-sentence-embedding-e8d31d9e6a8f?source=collection_archive---------6-----------------------#2024-01-16

将 BERT 转换为高效的句子变换器

Mina GhashamiTowards Data Science Mina Ghashami

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

--

在这篇文章中,我们将探讨 SentenceTransformer [1],该模型于 2019 年发布。SentenceTransformer 采用了双编码器架构,并将 BERT 模型适配以生成高效的句子嵌入。

BERT(双向编码器表示的变换器)是基于这样一种理念构建的:所有 NLP 任务都依赖于词元/单词的语义。BERT 训练分为两个阶段:1)预训练阶段,在此阶段 BERT 学习语言的普遍语义;2)微调阶段,在此阶段 BERT 在特定任务上进行训练。

图片来自[3]

BERT 非常擅长学习单词/词元的语义,但它不擅长学习句子的语义。因此,它在某些任务上表现不佳,例如句子分类、句子对之间的相似度计算。

由于 BERT 生成的是词元嵌入,因此从 BERT 中获取句子嵌入的一种方式是对所有词元的嵌入取平均值。SentenceTransformer 论文[1]表明,这种方式会产生非常低质量的句子嵌入,几乎和使用 GLOVE 嵌入一样差。这些嵌入不能捕捉句子的语义。

情感分析模板:一个完整的数据科学项目

原文:towardsdatascience.com/sentiment-analysis-template-a-complete-data-science-project-7065cc48aff2?source=collection_archive---------3-----------------------#2024-12-12

10 个关键步骤,从数据探索到模型部署。

Leo Anello 💡Towards Data Science Leo Anello 💡

·发表于Towards Data Science ·阅读时间 47 分钟·2024 年 12 月 12 日

--

图片来源:Glenn Carstens-PetersUnsplash

你知道一个数据科学项目从头到尾的完整结构吗?如果不知道,不用担心——你来对地方了。

指南将通过一个实践示例,带你了解如何进行情感分析,逐步拆解典型数据科学项目的基本结构。

[## GitHub - 情感分析模板

github.com](https://github.com/Anello92/sentiment-analysis?source=post_page-----7065cc48aff2--------------------------------)

模板提供了一个清晰的框架,涵盖从问题定义部署和文档的所有内容,并整合了情感分析的实践机器学习技术。

作为一个实际的示例,我们将使用大型电影评论数据集,这是一个广泛认可的二元情感分类资源。

通过跟随本指南,你将获得技术专长实践理解,从而能够执行具有影响力的数据科学项目。

SeqRAG:为我们所有人服务的智能体

原文:towardsdatascience.com/seqrag-agents-for-the-rest-of-us-2fba6332f2e8?source=collection_archive---------9-----------------------#2024-10-28

利用顺序检索增强生成(SeqRAG)构建更快速、更可靠的智能体

Adrian H. RaudaschlTowards Data Science Adrian H. Raudaschl

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

--

SeqRAG 的奇妙世界。插图由作者提供。

AI 智能体在代表我们执行复杂任务方面具有巨大潜力。然而,尽管像 OpenAI 的 o1 推理基础模型,甚至是最近的 Claude“计算机用户”功能等进展,显然,要在可靠、日常、实用的方式中实现这一潜力仍然充满挑战。事实上,这一直是一个挑战。

早在 1997 年,史蒂夫·乔布斯在 1997 年苹果开发者大会上被问到:“我们如何才能让计算机为人类工作呢?” 他承认当时 AI 智能体面临的挑战。

“现在把我们的未来寄托在智能体世界研究的成果上…将是愚蠢的,”乔布斯曾警告,并补充说,“我认为,某一时刻,它们(智能体)将开始以我们无法想象的方式为我们做更多的事。” — 乔布斯 2007 年

他的反应值得一看,点击这里观看。

乔布斯在1997 年苹果开发者大会上思考智能体解决方案。

目前,通往可靠的 AI 智能体的道路是一个激动人心的领域,特别是随着研究人员不断找到制作更强大智能体的方法,这些智能体能够解决...

顺序测试:低量 A/B 测试的秘密武器

原文:towardsdatascience.com/sequential-testing-the-secret-sauce-for-low-volume-a-b-tests-fe62bdf9627b?source=collection_archive---------4-----------------------#2024-08-29

如何在处理有限数据时加速决策并提高准确性

Zachary RaicikTowards Data Science Zachary Raicik

·发布于Towards Data Science ·阅读时间:7 分钟·2024 年 8 月 29 日

--

图片由 OpenAI 的 chatGPT 生成

什么是 A/B 测试,为什么它很难?

A/B 测试是一种通过提供基于数据的方式来确定哪个版本的产品更有效,从而减少决策不确定性的简单方法。A/B 测试的概念很简单。

  • 假设你在朋友的生日派对上。你一直在精心完善你的饼干食谱。你认为你已经完美了,但你不确定大家是更喜欢含燕麦的饼干,还是不含燕麦的饼干。在你看来,燕麦让饼干有一种很好的嚼劲。然而,你不确定这是否是大众的看法,还是仅仅是你个人的偏好。

  • 最终你带着两种不同版本的饼干出现在派对上,A 饼干含有燕麦,而 B 饼干没有。你随机给一半的朋友 A 饼干,另一半则得到 B 饼干。

  • 你决定,得到更多“赞”的饼干就是更好的饼干。

  • 一旦每个人都尝试了这些饼干,你发现 B 饼干得到了更多的“赞”,于是你得出结论,B 饼干是更好的饼干。

使用 vLLM 同时服务多个 LoRA 适配器

原文:towardsdatascience.com/serve-multiple-lora-adapters-with-vllm-5323b0425b82?source=collection_archive---------6-----------------------#2024-08-03

不增加延迟

本杰明·玛丽Towards Data Science 本杰明·玛丽

·发布于 Towards Data Science ·阅读时间 6 分钟·2024 年 8 月 3 日

--

由 DALL-E 生成

使用 LoRA 适配器时,我们可以将大语言模型(LLM)专门化为某个任务或领域。适配器必须加载到 LLM 上,才能用于推理。在某些应用中,为用户提供多个适配器可能是有用的。例如,一个适配器可以执行函数调用,另一个适配器可以执行完全不同的任务,如分类、翻译或其他语言生成任务。

然而,要使用多个适配器,一个标准的推理框架首先必须卸载当前适配器,然后加载新适配器。这一卸载/加载过程可能需要几秒钟,可能会影响用户体验。

幸运的是,有一些开源框架能够同时服务多个适配器,而不会在使用两个不同适配器之间产生明显的延迟。例如,vLLM(Apache 2.0 许可),是最高效的开源推理框架之一,可以轻松地同时运行和服务多个 LoRA 适配器。

在本文中,我们将展示如何使用 vLLM 配合多个 LoRA 适配器。我将解释如何在离线推理中使用 LoRA 适配器,以及如何为用户提供多个适配器以进行在线推理。我使用 Llama 3 作为示例,展示适配器在函数调用和对话中的应用。

在不到 10 分钟内设置一个类似 ChatGPT 的本地界面+副驾驶

原文:towardsdatascience.com/set-up-a-local-chatgpt-like-interface-copilot-in-less-than-10-minutes-60a02acd2628?source=collection_archive---------3-----------------------#2024-11-15

使用 Ollama、Llama3、Continue 和 Open WebUI 提供一个安全、本地、开源和免费的虚拟助手体验

Jeremy DiBattistaTowards Data Science Jeremy DiBattista

·发表于Towards Data Science ·5 分钟阅读·2024 年 11 月 15 日

--

图片来自ChrisUnsplash

我经常告诉人们,我使用本地的 LLM(大语言模型)来优化我的日常工作流,包括本地托管的类似 ChatGPT 的 UI,以及免费的代码自动完成集成(类似于 Copilot)到 VSCode 中,我最常听到的回答是“哇,那听起来像是要花费大量时间设置。”而我通常会反驳说,实际上我只花了几分钟!

从那里开始,为了帮助这个人,我会发送一组链接、说明和教程,所有这些都是为了处理那些可以如此快速完成的事情。这个过程将带你从头到尾完成,所花时间大约和你喝完一杯咖啡差不多。

本教程将分为两个部分,一个是 Web UI 部分,另一个是代码助手部分(因为不是每个人都会对这一部分感兴趣)。如果你对 VSCode 集成感兴趣,请先完成 Web UI 部分,然后继续!

为什么选择本地部署?

如果你有商业机密、专有代码或正在使用你不希望泄露的私人密钥和个人身份信息(PII),你不希望这些信息变得…

在 15 分钟内设置一个基于 CPU 的本地 LLM 和聊天 UI

原文:towardsdatascience.com/set-up-a-local-llm-on-cpu-with-chat-ui-in-15-minutes-4cdc741408df?source=collection_archive---------1-----------------------#2024-02-06

这篇博客文章展示了如何轻松地在本地运行一个大语言模型(LLM),并在 4 个简单步骤中设置一个类似 ChatGPT 的图形用户界面。

Kasper Groes Albin LudvigsenTowards Data Science Kasper Groes Albin Ludvigsen

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

--

图片来源:Liudmila Shuvalova,来自 Unsplash

感谢全球开源社区,现在在消费者笔记本电脑或基于 CPU 的服务器上运行高效的大型语言模型(LLM)并通过精心设计的图形用户界面与之交互比以往任何时候都更容易。

这对所有不允许或不愿使用需要将数据发送给第三方的服务的组织特别有价值。

本教程展示了如何在四个简单步骤中设置一个本地 LLM,并配有一个简洁的 ChatGPT 样式的 UI。如果你已经安装了必要的软件,整个过程不超过 15 分钟(不包括某些步骤中计算机处理时间)。

本教程假设你已经在机器上安装了以下软件:

  • Ollama

  • Docker

  • React

  • Python 和常用包,包括 transformers

现在让我们开始吧。

第一步 — 决定使用哪个 Huggingface LLM

第一步是决定你想在本地运行哪个 LLM。也许你已经有了想法。如果没有,对于英语,Mistral 7b 的指令版本似乎是首选。对于丹麦语,我推荐 Munin-NeuralBeagle,尽管它在生成令牌时可能过度(可能是因为它合并了一个未经过指令微调的模型)。对于其他斯堪的纳维亚语言,请参见ScandEval对斯堪的纳维亚生成模型的评估。

一旦你决定了使用哪个 LLM,复制 Huggingface“路径”到该模型。对于 Mistral 7b,它的路径是“mistralai/Mistral-7B-v0.1”。你将在下一步中用到它。

## 如何制作一个用于时间序列预测的 PyTorch Transformer

本文将向你展示如何一步步地将时间序列 Transformer 架构图转换为 PyTorch 代码。

[towardsdatascience.com

步骤 2 – 对 LLM 进行量化

下一步是对你选择的模型进行量化,除非你选择了一个已经量化的模型。如果你的模型名称以 GGUF 或 GPTQ 结尾,它已经是量化过的。

量化是一种技术,它将模型的权重(即它的学习参数)转换为比原始数据类型更小的类型,例如从 fp16 转换为 int4。这使得模型占用更少的内存,同时也加快了推理速度,如果你在 CPU 上运行,这是一项很有用的功能。

我在我的仓库local_llm中的脚本quantize.py改编自Maxime Labonne 的精彩 Colab 笔记本(可以查看他的LLM 课程获取更多 LLM 资源)。你可以使用他的笔记本或者我的脚本。这个方法已在 Mistral 和类似 Mistral 的模型上进行过测试。

要进行量化,首先克隆我的仓库:

git clone https://github.com/KasperGroesLudvigsen/local_llm.git 

现在,在quantize.py文件中更改MODEL_ID变量,以反映你选择的模型。这就是你需要在第一步中复制的 Huggingface“路径”。所以如果你想使用 Mistral 7b:

MODEL_ID = "mistralai/Mistral-7B-v0.1"

然后,在终端中运行脚本:

python quantize.py

这将需要一些时间。在量化过程运行时,你可以继续进行下一步。

该脚本将生成一个目录,其中包含你选择的模型的模型文件以及量化版本的模型,量化版本的文件扩展名为“.gguf”。

## ChatGPT 每次查询的能源使用

ChatGPT 回答一个问题需要多少电量?

[towardsdatascience.com

步骤 3:构建并运行 Ollama 版本的模型

我们将使用Ollama运行该模型。Ollama 是一个软件框架,它将模型封装成一个 API。Ollama 还可以轻松与各种前端集成,正如我们将在下一步中看到的那样。

要构建模型的 Ollama 镜像,你需要一个所谓的模型文件,这是一个配置 Ollama 镜像的纯文本文件。如果你熟悉 Dockerfile,Ollama 的模型文件会让你觉得很熟悉。

在下面的示例中,我们首先指定要使用的 LLM。假设您的仓库中有一个名为 mistral7b 的文件夹,并且该文件夹包含名为 quantized.gguf 的模型。接着,我们将模型的上下文窗口设置为 8,000——Mistral 7b 的最大上下文大小。在 Modelfile 文件中,您还可以指定要使用的提示模板,并可以指定停止标记。

保存模型文件,例如保存为 Modelfile.txt。

欲了解更多配置选项,请参见 Ollama 的 GitHub

FROM ./mistral7b/quantized.gguf

PARAMETER num_ctx 8000

TEMPLATE """<|im_start|>system {{ .System }}<|im_end|><|im_start|>user {{ .Prompt }}<|im_end|><|im_start|>assistant<|im_end|>"""

PARAMETER stop <|im_end|>
PARAMETER stop <|im_start|>user
PARAMETER stop <|end|>

现在您已经创建了 Modelfile,通过在终端运行以下命令,构建一个 Ollama 镜像。这也需要几分钟时间:

ollama create choose-a-model-name -f <location of the file e.g. ./Modelfile>'

当“创建”过程完成后,通过运行以下命令启动 Ollama 服务器。这将以一种 GUI 可以与其交互的方式暴露所有您的 Ollama 模型。

ollama serve

## 如何估算和减少机器学习模型的碳足迹

有两种简单的方法可以估算机器学习模型的碳足迹,以及 17 个减少碳足迹的想法

towardsdatascience.com

第四步 – 设置 Ollama 的聊天 UI

下一步是设置一个 GUI 与 LLM 进行交互。对此有几种选择。在本教程中,我们将使用“Chatbot Ollama”——一个非常整洁的 GUI,具有 ChatGPT 的感觉。“Ollama WebUI”是一个类似的选择。您还可以 使用 Streamlit 设置自己的聊天 GUI

通过运行以下两个命令,您将首先克隆 Chatbot Ollama 的 GitHub 仓库,然后安装 React 依赖:

git clone https://github.com/ivanfioravanti/chatbot-ollama.git
npm ci

下一步是从 Dockerfile 构建 Docker 镜像。如果您使用的是 Linux,您需要将 Dockerfile 中的 OLLAMA_HOST 环境变量从 hhtp://host.docker.internal:11434 修改为 http://localhost:11434

现在,从终端执行以下命令来构建 Docker 镜像并从中运行容器。您需要站在项目的根目录下。

docker build -t chatbot-ollama .

docker run -p 3000:3000 chatbot-ollama

GUI 现在正在您的本地计算机上的 Docker 容器中运行。在终端中,您将看到 GUI 可用的地址(例如:“http://localhost:3000”)

在浏览器中访问该地址,您现在应该能够通过 Ollama 聊天 UI 与 LLM 进行互动。

结论

本教程简要介绍了如何轻松设置聊天 UI,使您能够与在本地机器上运行的 LLM 进行交互。很简单,对吧?只需要四个步骤:

  1. 在 Huggingface 上选择一个模型

  2. (可选)对模型进行量化

  3. 将模型封装在 Ollama 镜像中

  4. 构建并运行一个封装 GUI 的 Docker 容器

请记住,这一切都得以实现,因为开源真是太棒了 👏

本文的 GitHub 仓库:github.com/KasperGroesLudvigsen/local_llm

就这些!希望你喜欢这个故事,告诉我你的想法!

通过注册 Medium 会员 HERE,你可以享受 Medium 的福利并支持我的写作。

关注我,了解更多关于 AI 和可持续发展的内容,并 订阅 以便在我发布新故事时通过电子邮件接收。

我有时也会写关于 时间序列预测 的文章。

也可以随时在 LinkedIn 上与我联系。

在 AWS 私有环境中使用 Terraform 设置 Pypi 镜像

原文:towardsdatascience.com/set-up-a-pypi-mirror-in-an-aws-private-environment-with-terraform-f0fcc1b67cc0?source=collection_archive---------7-----------------------#2024-03-06

Florent PajotTowards Data Science Florent Pajot

·发布于 Towards Data Science ·5 分钟阅读·2024 年 3 月 6 日

--

如果没有互联网访问,如何在环境中安装 Python 包?我最近在为我的团队在 AWS 上创建 AWS Sagemaker Studio 环境时遇到了这个问题。

为 Sagemaker 构建 AWS 私有环境

对于这个特定项目,我在仅限 VPC 模式下设置了 Sagemaker,并且有一个保持架构私有的约束,这意味着创建 VPC 和私有子网,但没有互联网访问。

所有网络通信,包括与 AWS API 的应用通信,必须通过 VPC Endpoint 接口进行。这可以确保连接安全,因为发送和接收的数据不会通过互联网,而是使用 AWS 网络骨干进行传输。

这特别适用于限制暴露于安全风险,尤其是在处理个人信息或必须遵守某些安全标准时。

图片来自 Nadir sYzYgY 通过 Unsplash

从 AWS Sagemaker 访问 Pypi 包仓库

在我的团队中,数据科学家使用 Python 作为主要语言,有时需要一些Sagemaker 预构建 Python 镜像中未提供的 Python 包,因此我将重点讨论这个用例。幸运的是,解决方案也适用于其他语言和仓库,例如 npm。

用户通常会尝试通过 pip 命令安装所需的任何包。但是,由于不允许互联网访问,这个命令会失败,因为 pip 无法连接到 Pypi.org 服务器。

开启互联网

一种选择是开放互联网访问并允许向 Pypi.org 服务器使用的 Fastly CDN IP 地址发出外部 HTTP 连接。但在我们的情况下这是不可行的,因为我们不希望架构中有任何互联网连接。

使用专用 Pypi 服务器

AWS 博客还提供了一个使用名为 Bandersnatch 的 Python 包的示例。这篇文章描述了如何设置一个服务器,作为堡垒主机,镜像 Pypi,并且只对你的私有子网可访问。

这不是一个可行的选项,因为你需要提前知道需要提供哪些 Python 包,并且你还得创建公共子网并为 Pypi 服务器镜像提供互联网访问权限。

使用 AWS Codeartifact

这最终是我想出的解决方案,并且在我的情况下是有效的。

AWS Codeartifact 是 AWS 提供的工件管理解决方案。它与其他 AWS 服务兼容,如 AWS 服务目录,用于控制组织内资源的访问。

要使用它,你必须创建一个“域”,它作为一个总览来管理访问权限并在你的组织内应用政策。然后,你需要创建一个仓库,供不同的应用程序使用你的工件。

此外,一个仓库可以拥有上游仓库。因此,如果目标仓库中没有某个 Python 包,需求将传递到上游仓库以进行满足。

更准确地说,这个工作流考虑了包版本。官方文档提供了详细的工作流:

如果 my_repo 包含请求的包版本,它将返回给客户端。

如果 my_repo 不包含请求的包版本,CodeArtifact 会在 my_repo 的上游仓库中查找它。如果找到该版本的包,将其引用复制到 my_repo 中,并将包版本返回给客户端。

如果 my_repo 和它的上游仓库都不包含该包版本,客户端将收到 HTTP 404 未找到 响应。

酷吧?它甚至会缓存包版本,以便将来的请求使用。

这正是我们将使用的策略,因为 AWS Codeartifact 允许我们定义一个具有外部连接(如 Pypi)作为上游仓库的仓库。

使用 Terraform 创建 AWS Codeartifact 资源

由于 AWS Codeartifact 是 AWS 服务,你可以轻松地在你的环境 VPC 中创建一个 VPC 端点来连接它。

注:我正在使用 Terraform v1.6.4 和 AWS 提供程序 v5.38.0

locals {
  region = "us-east-1"
}

resource "aws_security_group" "vpce_sg" {
  name        = "AllowTLS"
  description = "Allow TLS inbound traffic and all outbound traffic"
  vpc_id      = aws_vpc.your_vpc.id

  tags = {
    Name = "allow_tls_for_vpce"
  }
}

resource "aws_vpc_security_group_ingress_rule" "allow_tls_ipv4" {
  security_group_id = aws_security_group.allow_tls.id
  cidr_ipv4         = aws_vpc.your_vpc.cidr_block
  from_port         = 443
  ip_protocol       = "tcp"
  to_port           = 443
}

data "aws_iam_policy_document" "codeartifact_vpce_base_policy" {
  statement {
    sid    = "EnableRoles"
    effect = "Allow"
    actions = [
      "codeartifact:GetAuthorizationToken",
      "codeartifact:GetRepositoryEndpoint",
      "codeartifact:ReadFromRepository",
      "sts:GetServiceBearerToken"
    ]
    resources = [
      "*",
    ]
    principals {
      type = "AWS"
      identifiers = [
        aws_iam_role.your_sagemaker_execution_role.arn
      ]
    }
  }
}

resource "aws_vpc_endpoint" "codeartifact_api_vpce" {
  vpc_id            = aws_vpc.your_vpc.id
  service_name      = "com.amazonaws.${local.region}.codeartifact.api"
  vpc_endpoint_type = "Interface"
  subnet_ids        = aws_subnets.your_private_subnets.ids

  security_group_ids = [
    aws_security_group.vpce_sg.id,
  ]

  private_dns_enabled = true
  policy              = data.aws_iam_policy_document.codeartifact_vpce_base_policy.json
  tags = { Name = "codeartifact-api-vpc-endpoint" }
}

然后,你需要创建不同的资源,以便 Codeartifact 能通过镜像 Pypi 处理你对新 Python 包的请求:一个域名,一个具有外部连接的 Pypi 仓库,以及一个将 Pypi 定义为上游仓库的仓库。

resource "aws_codeartifact_domain" "my_domain" {
  domain = "my-domain"

  encryption_key = ""

  tags = { Name = "my-codeartifact-domain" }
}

resource "aws_codeartifact_repository" "public_pypi" {
  repository = "pypi-store"
  domain     = aws_codeartifact_domain.my_domain.domain

  external_connections {
    external_connection_name = "public:pypi"
  }

  tags = { Name = "pypi-store-repository" }
}

resource "aws_codeartifact_repository" "my_repository" {
  repository = "my_repository"
  domain     = aws_codeartifact_domain.my_domain.domain

  upstream {
    repository_name = aws_codeartifact_repository.public_pypi.repository
  }

  tags = { Name = "my-codeartifact-repository" }
}

data "aws_iam_policy_document" "my_repository_policy_document" {
  statement {
    effect = "Allow"

    principals {
      type        = "AWS"
      identifiers = [aws_iam_role.your_sagemaker_execution_role.arn]
    }

    actions   = ["codeartifact:ReadFromRepository"]
    resources = [aws_codeartifact_repository.my_repository.arn]
  }
}

resource "aws_codeartifact_repository_permissions_policy" "my_repository_policy" {
  repository      = aws_codeartifact_repository.my_repository.repository
  domain          = aws_codeartifact_domain.my_domain.domain
  policy_document = data.aws_iam_policy_document.my_repository_policy_document.json
}

就是这样!你现在可以轻松为你的私有环境设置一个 Pypi 镜像。

为了使其可用,你还需要告诉 pip 命令将请求指向特定的索引。幸运的是,AWS 创建了一个 API 来为你完成繁重的工作。只需将以下代码添加到你的项目中,即可使其工作:

aws codeartifact login --tool pip --repository $CODE_ARTIFACT_REPOSITOR_ARN --domain $CODE_ARTIFACT_DOMAIN_ID --domain-owner $ACCOUNT_ID --region $REGION

最后但同样重要的是,在你的 VPC 中为 AWS Codeartifact 添加一个 VPC 端点。

data "aws_iam_policy_document" "codeartifact_vpce_base_policy" {
  statement {
    sid    = "EnableRoles"
    effect = "Allow"
    actions = [
      "codeartifact:GetAuthorizationToken",
      "codeartifact:GetRepositoryEndpoint",
      "codeartifact:ReadFromRepository",
      "sts:GetServiceBearerToken"
    ]
    resources = [
      "*",
    ]
    principals {
      type = "AWS"
      identifiers = [
        aws_iam_role.your_sagemaker_execution_role.arn
      ]
    }
  }
}

resource "aws_vpc_endpoint" "codeartifact_api_vpce" {
  vpc_id            = aws_vpc.your_vpc.id
  service_name      = "com.amazonaws.${local.region}.codeartifact.api"
  vpc_endpoint_type = "Interface"
  subnet_ids        = aws_subnets.your_private_subnets.ids

  security_group_ids = [
    aws_security_group.vpce_sg.id,
  ]

  private_dns_enabled = true
  policy              = data.aws_iam_policy_document.codeartifact_vpce_base_policy.json
  tags = { Name = "codeartifact-api-vpc-endpoint" }
}

如果你想接收有关 AWS 等方面的最新帖子通知,请在此订阅

你知道你可以多次拍手吗?

设置 Docker 化的 Python 环境 — 优雅的方式

原文:towardsdatascience.com/setting-a-dockerized-python-environment-the-elegant-way-f716ef85571d?source=collection_archive---------1-----------------------#2024-04-02

本文提供了一个逐步指南,介绍如何使用 VScode 和 Dev Containers 扩展设置一个 Python Docker 化开发环境。

Rami KrispinTowards Data Science Rami Krispin

·发表于 Towards Data Science ·阅读时间 9 分钟·2024 年 4 月 2 日

--

在上一篇关于此主题的文章中,设置 Docker 化的 Python 环境 — 较为困难的方式,我们讲解了如何通过命令行界面(CLI)设置 Docker 化的 Python 开发环境。在这篇文章中,我们将回顾一种更优雅且更稳健的方法,通过使用 VScode 和 Dev Containers 扩展来设置 Docker 化的 Python 开发环境。

相关文章:

## 设置 Docker 化的 Python 环境 — 较为困难的方式

本文将回顾通过命令行(CLI)运行 Docker 化的 Python 环境的不同方法。我是否……

towardsdatascience.com

到本教程结束时,你将能够使用 VScode 和 Dev Containers 扩展设置一个简单的 Python 开发环境。

VScode 插图(由作者使用 Midjourney 创建)

前提条件

要跟随本教程,你需要准备以下内容:

  • 如果你使用的是 macOS 或 Windows 操作系统,请安装 Docker Desktop(或等效工具);如果你使用的是 Linux 操作系统,请安装 Docker。

  • Docker Hub 账户,用于拉取镜像

  • 安装了 VScode IDEDev Containers 扩展

在本教程中,我们将使用官方 Python 镜像 — python:3.10

本文中的所有代码示例可以在这里找到:

[## GitHub - RamiKrispin/vscode-python-medium: 支持 Medium 文章的代码

支持 Medium 文章的代码。通过创建账户来贡献 RamiKrispin/vscode-python-medium 的开发…

github.com](https://github.com/RamiKrispin/vscode-python-medium?source=post_page-----f716ef85571d--------------------------------)

Dev Containers 扩展

在开始之前,让我们解释一下什么是 Dev Containers 扩展,以及何时应该考虑使用它。

简而言之,VScode Dev Containers 扩展使您能够在 Docker 容器内无缝地打开一个隔离的 VScode 会话。隔离的层级包括以下三层:

  • 环境

  • VScode 设置

  • VScode 扩展

devcontainer.json 文件定义了会话设置,使我们能够设置和定义上述三层。

要在容器内设置并启动项目文件夹,使用 Dev Containers 扩展,您需要以下两个组件:

  • 安装 Dev Containers 扩展

  • 在您的项目文件夹中创建一个名为 .devcontainer 的文件夹,并设置一个 devcontainer.json 文件

以下图示描述了 Dev Containers 的总体架构:

Dev Containers 扩展架构(致谢 Rami Krispin)

启动时,Dev Containers 扩展会在容器内启动一个新的 VScode 会话。默认情况下,它会将本地文件夹挂载到容器中,这样可以保持代码的持久性,并与本地文件夹同步。您可以挂载其他文件夹,但这超出了本教程的范围。

在下一部分,我们将看到如何使用 devcontainer.json 文件设置 Python 环境。

设置一个 Docker 化的 Python 环境

在开始使用 devcontainer.json 设置之前,让我们首先定义开发环境的范围。它应包括以下功能:

  • Python 3.10

  • 支持 Jupyter 笔记本

  • 安装所需的库 — Pandas 和 VScode Jupyter 支持库

  • 安装支持的扩展 — Python 和 Jupyter

在接下来的部分,我们将深入探讨 devcontainer.json 文件的核心功能。我们将从一个最简化的 Python 环境开始,展示如何通过添加不同的自定义层来进行定制。

构建与镜像

启动一个容器化会话的主要要求是定义镜像设置。设置镜像有两种方法:

  • 在容器启动时使用 build 参数构建镜像并运行。此参数使你能够为构建定义一个 Dockerfile,并将参数传递给 docker build 函数。构建过程完成后,它将启动容器内部的会话

  • 使用 image 参数启动现有镜像的会话

根据不同的使用场景,每种方法都有其优缺点。你应该在镜像完全满足环境需求时考虑使用 image 参数。同样,当你有一个基础镜像,但需要添加一些小的自定义设置时,build 参数是一个很好的选择。

在接下来的部分,我们将通过一个简单的例子开始,使用 image 参数导入官方 Python 镜像(python:3.10)来启动 Python 环境。

基本的 Docker 化 Python 环境

以下 devcontainer.json 文件提供了一个简单的示例,用于设置 Python 环境。它使用 image 参数将 python:3.10 镜像定义为会话环境:

devcontainer.json

{
    "name": "Python Development Environment",
    "image": "python:3.10"
}

name 参数定义了环境名称。在此示例中,我们将其设置为 Python 开发环境

在启动环境之前,请确保:

  • 你的 Docker Desktop(或同等软件)已打开

  • 你已登录到 Docker Hub(或提前拉取 Python 镜像)

  • devcontainer.json 文件位于项目文件夹中的 .devcontainer 文件夹下:

.
└── .devcontainer
    └── devcontainer.json

此示例的代码可在 此处 获取。

要启动会话,请点击左下角的 Dev Container >< 图标,并选择下方截图所示的 Reopen in Container 选项:

使用 Dev Containers 扩展启动容器内部的会话(作者截图)

请注意,在首次启动会话时,Dev Containers 扩展将查找由 image 参数定义的镜像(在此例中是 python:3.10)。如果镜像在本地不可用,它将从 Docker Hub 拉取,可能需要几分钟时间。之后,启动会话通常只需几秒钟。

容器内的 VScode 会话(作者截图)

在上面的截图中,你可以看到 devcontainer.json 参数与会话设置之间的映射。会话名称现在显示在右下角(紫色标记),并与 name 参数的值对齐。同样,会话现在运行在 python:3.10 容器内,你可以从终端启动 Python。

Python 容器带有默认的 Python 库。在接下来的部分,我们将展示如何通过 build 参数在 Python 基础镜像上添加更多层。

使用 Dockerfile 自定义 Python 环境

现在,让我们通过修改 devcontainer.json 来定制上述环境。我们将把 image 参数替换为 build 参数。build 参数使我们能够在会话启动时通过 Dockerfile 构建镜像,并向 docker build 函数传递参数。我们将遵循这篇文章中展示的方法来设置 Python 环境:

  • 导入 python:3.10 作为基础镜像

  • 设置虚拟环境

  • 安装所需的库

我们将使用以下 Dockerfile 来设置 Python 环境:

Dockerfile

FROM python:3.10

ARG PYTHON_ENV=my_env

ENV PYTHON_ENV=$PYTHON_ENV

RUN mkdir requirements

COPY requirements.txt set_python_env.sh /requirements/

RUN bash ./requirements/set_python_env.sh $PYTHON_ENV

我们使用 FROM 参数来导入 Python 镜像,并使用 ARGENV 参数来设置虚拟环境作为参数和环境变量。此外,我们还使用以下两个辅助文件来设置虚拟环境并安装所需的库:

  • requirements.txt — 一个设置文件,其中列出了所需的库。在本演示中,我们将安装 Pandas 库(版本 2.0.3)和 Jupyter 支持库(ipykernel、ipywidgets、jupyter)。wheels 库是一个支持库,用于处理 C 依赖关系。

  • set_python_env.sh — 一个辅助的 Bash 脚本,用于设置虚拟环境并通过 requirements.txt 文件安装所需的库

requirements.txt

wheel==0.40.0
pandas==2.0.3
ipykernel
ipywidgets
jupyter 

set_python_env.sh

#!/usr/bin/env bash

PYTHON_ENV=$1

python3 -m venv /opt/$PYTHON_ENV  \
        && export PATH=/opt/$PYTHON_ENV/bin:$PATH \
        && echo "source /opt/$PYTHON_ENV/bin/activate" >> ~/.bashrc

source /opt/$PYTHON_ENV/bin/activate

pip3 install -r ./requirements/requirements.txt

最后,我们将使用以下测试文件来评估 Pandas 库是否正确安装并打印 Hello World! 消息:

test1.py

import pandas as pd

print("Hello World!")

让我们在 devcontainer.json 文件中进行修改,将 image 参数替换为 build 参数:

devcontainer.json

{
    "name": "Python Development Environment",
    "build": {
        "dockerfile": "Dockerfile",
        "context": ".",
        "args": {
            "PYTHON_ENV": "my_python_dev"
        }
    }
}

本示例的文件可以在这里找到。

build 子参数使我们能够通过向 docker build 函数传递参数来自定义镜像构建。我们使用以下参数来构建镜像:

  • dockerfile — Dockerfile 的路径和名称

  • context — 设置本地文件系统的路径,以便在构建时通过 COPY 参数访问文件。在这种情况下,我们使用 devcontainer.json 文件的当前文件夹(例如 .devcontainer 文件夹)。

  • args — 在构建过程中设置并传递参数给容器。我们使用 PYTHON_ENV 参数来设置虚拟环境并命名为 my_python_dev

你应该将这三个文件 — Dockerfilerequirements.txtset_python_env.sh 存储在 .devcontainer 文件夹下,并与 devcontainer.json 文件一起使用:

.
├── .devcontainer
│   ├── Dockerfile
│   ├── devcontainer.json
│   ├── requirements.txt
│   └── set_python_env.sh
└── test2.py

现在,让我们使用新设置启动会话,并使用 test1.py 文件进行测试:

运行一个 Python 脚本以测试环境(截图来自作者)

如上图所示,我们成功地从终端(标记为紫色)运行了测试脚本,并且它按预期打印出了Hello World!消息(标记为绿色)。此外,我们在镜像中设置的虚拟环境(my_python_dev)默认已加载(标记为黄色)。

在下一部分中,我们将看到如何自定义 Dev Containers 会话的 VScode 设置。

自定义 VScode 设置

Dev Containers 扩展的一个重要特点是,它将会话设置与主要的 VScode 设置隔离开来。这意味着你可以在项目级别完全自定义 VScode 设置。它扩展了开发环境的可复现性,不仅限于 Python 或操作系统设置。最后但同样重要的是,它使得与他人协作或在多台机器上工作变得无缝且高效。

我们将在接下来的示例中结束本教程,看看如何使用customizations参数自定义 VScode 设置。我们将向前面的示例添加该参数,并使用vscode子参数设置环境默认的 Python 解释器和所需的扩展:

devcontainer.json

{
    "name": "Python Development Environment",
    "build": {
        "dockerfile": "Dockerfile",
        "context": ".",
        "args": {
            "PYTHON_ENV": "my_python_dev"
        }
    },
    "customizations": {
        "vscode": {
            "settings": {
                "python.defaultInterpreterPath": "/opt/my_python_dev/bin/python3",
                "python.selectInterpreter": "/opt/my_python_dev/bin/python3"
            },
            "extensions": [
                "ms-python.python",
                "ms-toolsai.jupyter"
            ]
        }
    }
}

该示例的文件可以在这里找到。

我们使用settings参数来定义镜像中定义的 Python 虚拟环境。此外,我们使用extensions参数安装PythonJupyter支持扩展。

注意: 虚拟环境的路径由用于设置环境的应用程序类型定义。当我们使用venv并将其命名为my_python_dev时,路径为opt/my_python_dev/bin/python3

在添加 Python 扩展后,我们可以使用扩展插件启动 Python 脚本,如下图所示。此外,我们还可以利用 Jupyter 扩展以交互模式执行 Python 代码:

总结

在本教程中,我们回顾了如何使用 VScode 和 Dev Containers 扩展设置一个 Docker 化的 Python 环境。Dev Containers 扩展使得容器与开发工作流的集成变得无缝且高效。我们了解了如何通过几个简单的步骤,使用devcontainer.json文件来设置和自定义 Docker 化的 Python 环境。我们回顾了两种设置会话镜像的方法:通过imagebuild参数,以及通过customizations参数来设置扩展。还有其他自定义选项未在本教程中涉及,我建议查看这些选项:

  • 定义环境变量

  • 挂载额外的卷

  • 设置docker run命令的参数

  • 启动后运行命令

如果你有兴趣深入了解更多细节,我推荐查看这个教程:

[## GitHub - RamiKrispin/vscode-python: 使用 VScode 和 Docker 设置 Python 开发环境的教程…

使用 VScode 和 Docker 设置 Python 开发环境的教程 - RamiKrispin/vscode-python

github.com](https://github.com/RamiKrispin/vscode-python?source=post_page-----f716ef85571d--------------------------------)

资源

设置一个 Docker 化的 Python 环境——硬方法

原文:towardsdatascience.com/setting-a-dockerized-python-environment-the-hard-way-e62531bca7a0?source=collection_archive---------2-----------------------#2024-02-13

Rami KrispinTowards Data Science Rami Krispin

·发表于Towards Data Science ·阅读时长 9 分钟·2024 年 2 月 13 日

--

本文将回顾从命令行(CLI)运行 Docker 化 Python 环境的不同方法。我是否推荐你从 CLI 运行 Python 环境?绝对不!

还有更好的方式来设置 Python 开发环境,比如使用VScode 和 Dev Containers 扩展。但为了学习目的,我们将使用“硬方法”通过 CLI 来设置 Python 环境。我们将回顾使用 run 命令启动容器的不同方法,并了解如何通过 Dockerfile 定制内置镜像。

一只由作者使用 Midjourney 创建的 Pixal Python Snake

前提条件

要跟随本教程进行操作,您需要以下内容:

  • 如果您使用的是 macOS 或 Windows 操作系统的机器,则需要安装 Docker Desktop(或等效工具);如果您使用的是 Linux 操作系统,则需要安装 Docker。

  • 您需要一个 Docker Hub 账户来拉取镜像。

在整个教程中,我们将使用官方的 Python 镜像——python:3.10

入门

我们首先从 Docker Hub 拉取官方的 Python 3.10 镜像。首先,使用docker login命令登录到 Docker Hub:

docker login docker.io                                                                                                                                                                      ok
Authenticating with existing credentials...
Login Succeeded

接下来,我们将使用终端中的docker pull命令:

docker pull python:3.10                                                                                                                                                                     ok

如果这是您第一次拉取镜像,您应该期待以下输出:

3.10: Pulling from library/python
66932e2b787d: Pull complete
4afa7e263db1: Pull complete
c812910e5e62: Pull complete
f4e4299bb649: Pull complete
5213cc2f9120: Pull complete
4a3b5b2f0e66: Pull complete
c214ceb1cabf: Pull complete
f5336038b15c: Pull complete
Digest: sha256:f94601bb6374b0b63835a70c9e5c3ba1b19bc009133900a9473229a406018e46
Status: Downloaded newer image for python:3.10
docker.io/library/python:3.10

您可以使用docker images命令查看镜像详细信息:

docker images                                                                                                                                                                          ok  11s
REPOSITORY   TAG       IMAGE ID       CREATED      SIZE
python       3.10      f7537c504c9a   7 days ago   1.01GB

在运行容器之前,让我们使用docker inspect命令来查看镜像元数据:

docker inspect python:3.10

这将返回以下JSON输出:

[
    {
        "Id": "sha256:f7537c504c9a91a22c9a255ee02048e7079cacdee583290e8238c605d17f9600",
        "RepoTags": [
            "python:3.10"
        ],
        "RepoDigests": [
            "python@sha256:f94601bb6374b0b63835a70c9e5c3ba1b19bc009133900a9473229a406018e46"
        ],
        "Parent": "",
        "Comment": "buildkit.dockerfile.v0",
        "Created": "2024-02-03T10:49:13Z",
        "Container": "",
        "ContainerConfig": {
            "Hostname": "",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": null,
            "Cmd": null,
            "Image": "",
            "Volumes": null,
            "WorkingDir": "",
            "Entrypoint": null,
            "OnBuild": null,
            "Labels": null
        },
        "DockerVersion": "",
        "Author": "",
        "Config": {
            "Hostname": "",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "LANG=C.UTF-8",
                "GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D",
                "PYTHON_VERSION=3.10.13",
                "PYTHON_PIP_VERSION=23.0.1",
                "PYTHON_SETUPTOOLS_VERSION=65.5.1",
                "PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/dbf0c85f76fb6e1ab42aa672ffca6f0a675d9ee4/public/get-pip.py",
                "PYTHON_GET_PIP_SHA256=dfe9fd5c28dc98b5ac17979a953ea550cec37ae1b47a5116007395bfacff2ab9"
            ],
            "Cmd": [
                "python3"
            ],
            "ArgsEscaped": true,
            "Image": "",
            "Volumes": null,
            "WorkingDir": "",
            "Entrypoint": null,
            "OnBuild": null,
            "Labels": null
        },
        "Architecture": "arm64",
        "Variant": "v8",
        "Os": "linux",
        "Size": 1005570383,
        "VirtualSize": 1005570383,
        "GraphDriver": {
            "Data": {
                "LowerDir": "/var/lib/docker/overlay2/d2fd76e7396796018a959209b51fe8311a188c8eae8e339e9e556de0889ca0bd/diff:/var/lib/docker/overlay2/bbedb25c5aa6ec3f2fc632e62a53989a329b907143fac165f899209293627a69/diff:/var/lib/docker/overlay2/ed6a4bf49214e6b496b7716443b8de380481cd9416bc4a378f29183c9129786f/diff:/var/lib/docker/overlay2/ac9543f44a835c203fb0b0b28958d94df72d206c9060c9d83307b39f50355102/diff:/var/lib/docker/overlay2/94a9f92c36ea6251feda52be8e76ec4da4a9c66b744a29472e1ccfdf34a6f69d/diff:/var/lib/docker/overlay2/6ee22c274256834a64008022856d365d91373bb490ae9f2f1723cb524b246a29/diff:/var/lib/docker/overlay2/2fa272376e0ce68f4f34f18e5ecb1ddd58a32fb20a82e5a417514047f8e684a3/diff",
                "MergedDir": "/var/lib/docker/overlay2/f2d64d1affbe99afb05251435f7705cb97e2efa4f8febb494b4cbaa21e7f742a/merged",
                "UpperDir": "/var/lib/docker/overlay2/f2d64d1affbe99afb05251435f7705cb97e2efa4f8febb494b4cbaa21e7f742a/diff",
                "WorkDir": "/var/lib/docker/overlay2/f2d64d1affbe99afb05251435f7705cb97e2efa4f8febb494b4cbaa21e7f742a/work"
            },
            "Name": "overlay2"
        },
        "RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:9f8c60461a42fd9f275c56f4ec8fea8a8ea2d938493e316e830994a3814cf0aa",
                "sha256:927a28cdbf6c1705342b2cba0069457313815058dcebe1996e46cade38a09370",
                "sha256:e85139d96aee18e99fc964d02546be48cc6a4d4dfd9f465a91f172b7c535e55f",
                "sha256:f3168ba6a8d2ec30e12002ad5b7b497cf7409f3e19cc8d8f447f6cf4231a2482",
                "sha256:acbc6c8127209b09fa336e354037fdc630d3594e15174f2bc1bdbf31d5591376",
                "sha256:06c4da96c7dd2fbbbb541e000bd0ea4cfbf7c80b24f098a9d67f677ef5e6c63e",
                "sha256:1cdf208dc10679cf5db6d4f0e17ff6d5bfe754b4195ddd3b153b6d1ff51ce909",
                "sha256:5d6f554f67c7da9d34763916bac632a450902c6e6fdbf9244f888f110fd37740"
            ]
        },
        "Metadata": {
            "LastTagTime": "0001-01-01T00:00:00Z"
        }
    }
]

ispect 命令提供了有关镜像的很多有用信息,例如层信息、镜像大小、硬件架构等。因为我们想运行该镜像,最有趣的细节是 CMD 设置。Dockerfile 中的 CMD 命令定义了容器启动时要执行的命令。我们可以通过上面的输出解析 CMD 信息,使用 jq 包:

docker inspect python:3.10 | jq '.[] | .Config | .Cmd'                                                                                                                                      ok
[
  "python3"
]

如你所见,镜像被设置为在容器运行时执行 python3 命令。现在,让我们使用 run 命令启动容器:

docker run python:3.10

然后……没有任何反应。简而言之,原因是我们需要给 Docker 终端访问权限。我们将使用交互模式和 TTY 参数来运行镜像:

docker run --interactive --tty python:3.10

这将把终端连接到容器并在容器内打开 Python:

在交互模式下运行 Python 镜像(截图由作者提供)

尽管我们能够在容器内启动 Python,但这样做并不太实用:

  • 我们无法在 Python 解释器内创建、编辑和运行脚本

  • 默认情况下,Python 镜像仅包含有限的库。在这种模式下,你无法添加额外的库

  • 最后但同样重要的是,容器是短暂的。一旦你停止它,所有的工作都会丢失

例如,如果我们尝试加载 pandas,我们将遇到以下错误:

尝试加载 Pandas 库(截图由作者提供)

在接下来的部分中,我们将通过创建 Dockerfile 和自定义基础镜像功能来解决这些问题。这包括添加以下功能:

  • 设置虚拟环境并使用 requirements 文件安装包。为了简化,

  • 安装一个 vim 编辑器以编辑文件

  • 更改 CMD 命令以在启动时打开一个 shell 终端(而不是 Python 解释器)。这将使我们能够从终端创建新脚本、编辑并执行它们

自定义基础镜像

为了自定义 Python 环境并进行上述更改,我们将创建一个包含以下功能的 Dockerfile:

  • 导入 Python 镜像 — python:3.10

  • 设置虚拟环境

  • 安装所需的库

  • 安装 vim 编辑器

  • 暴露一个 bash 终端

设置 Python 虚拟环境

为了设置一个 Python 虚拟环境,我们将使用以下两个辅助文件:

requirements.txt

wheel==0.40.0
pandas==2.0.3

该文件定义了要在虚拟环境中安装的 Python 库列表。例如,在这种情况下,我们将安装 Pandas 库,版本为 2.0.3。通常,我还会安装 wheels 库,它处理 C 语言依赖。

下一个辅助文件是下面的 bash 脚本,它设置虚拟环境并从 requirements.txt 文件中安装所需的库。

set_python_env.sh

#!/usr/bin/env bash

PYTHON_ENV=$1

python3 -m venv /opt/$PYTHON_ENV  \
        && export PATH=/opt/$PYTHON_ENV/bin:$PATH \
        && echo "source /opt/$PYTHON_ENV/bin/activate" >> ~/.bashrc

source /opt/$PYTHON_ENV/bin/activate

pip3 install -r ./requirements/requirements.txt

注意: 我们使用一个变量(标记为$1)来定义环境名称,该名称将被赋值给PYTHON_ENV变量。在构建过程中使用变量是一种良好的做法,因为它使我们能够在不修改代码的情况下修改镜像的某些特性。我们将在 Dockerfile 中为变量赋值。

让我们解释一下来自上述 bash 脚本的以下拼接代码,它设置了虚拟环境:

python3 -m venv /opt/$PYTHON_ENV  \
    && export PATH=/opt/$PYTHON_ENV/bin:$PATH \
    && echo "source /opt/$PYTHON_ENV/bin/activate" >> ~/.bashrc

上述三行代码包括三个拼接表达式:

  • 第一,使用python3 -m venv /opt/$PYTHON_ENV命令设置一个虚拟环境。

  • 第二,向PATH变量中添加虚拟环境路径。

  • 第三,向.bashrc文件中添加环境激活命令。这将确保每次启动终端时,默认激活这个虚拟环境(否则,你需要在启动环境时手动激活)。

环境设置完成后,我们使用source命令激活环境,并使用pip3命令在环境中安装库。

创建 Dockerfile

在审查了辅助文件后,让我们看看它们如何在下面的 Dockerfile 中被引用。

Dockerfile

FROM python:3.10

ARG PYTHON_ENV=my_env
ENV PYTHON_ENV=$PYTHON_ENV

RUN mkdir requirements

COPY requirements.txt set_python_env.sh /requirements/

RUN bash ./requirements/set_python_env.sh $PYTHON_ENV

RUN apt-get update && \
    apt-get install -y \
    vim \
    && apt update

CMD ["/bin/sh", "-c", "bash"]

如你所见,我们使用相同的镜像——python:3.10作为我们的基础镜像。

接下来,我们使用ARG命令设置一个名为PYTHON_ENV的参数,用于定义虚拟环境名称。我们将my_env设置为默认值,并且可以在构建时使用arg参数进行修改。我们还使用PYTHON_ENV参数来设置一个环境变量。

在设置虚拟环境之前,我们将在镜像的根目录下创建一个名为requirements的新文件夹,并使用COPY命令将上述辅助文件——requirements.txtset_my_python.sh复制到requirements文件夹中。

接下来,我们调用 bash 脚本set_my_python.sh,该脚本设置虚拟环境并安装所需的库。如上所述,我们使用PYTHON_ENV变量作为参数,配合set_my_python.sh文件动态设置虚拟环境名称。

我们使用apt命令安装vim——一个 CLI 编辑器。这样我们就可以通过容器 CLI 编辑代码了。

最后但同样重要的是,使用CMD命令启动一个 bash 终端:

CMD ["/bin/sh", "-c", "bash"]

此时,我们在本地文件夹中有以下文件:

.
├── Dockerfile
├── requirements.txt
└── set_python_env.sh

现在让我们继续使用docker build命令构建镜像:

docker build .-f Dockerfile -t my_python_env:3.10
[+] Building 47.3s (10/10) FINISHED
 => [internal] load build definition from Dockerfile                                                 0.2s
 => => transferring dockerfile: 389B                                                                 0.0s
 => [internal] load .dockerignore                                                                    0.2s
 => => transferring context: 2B                                                                      0.0s
 => [internal] load metadata for docker.io/library/python:3.10                                       0.0s
 => [1/5] FROM docker.io/library/python:3.10                                                         1.3s
 => [internal] load build context                                                                    0.4s
 => => transferring context: 460B                                                                    0.0s
 => [2/5] RUN mkdir requirements                                                                     0.5s
 => [3/5] COPY requirements.txt set_python_env.sh /requirements/                                     0.2s
 => [4/5] RUN bash ./requirements/set_python_env.sh my_env                                          26.8s
 => [5/5] RUN apt-get update &&     apt-get install -y     vim     && apt update                    17.4s
 => exporting to image                                                                               0.7s
 => => exporting layers                                                                              0.6s
 => => writing image sha256:391879baceea6154c191692d4bcb9ec9690de6dc4d5edd5b2ed13f6c579dd05c         0.0s
 => => naming to docker.io/library/my_python_env:3.10

让我们再次运行 docker images 命令,查看当前镜像:

docker images                                                  
REPOSITORY      TAG       IMAGE ID       CREATED         SIZE
my_python_env   3.10      391879baceea   7 minutes ago   1.23GB
python          3.10      f7537c504c9a   8 days ago      1.01GB

正如你所注意到的,添加虚拟环境并安装包后,镜像大小增加了大约 250MB。

运行 Python 环境

构建完镜像后,让我们使用docker run命令启动镜像,并检查上述属性是否按预期定义:

docker run --interactive --tty my_python_env:3.10

这将以交互模式启动镜像,并按预期打开一个 bash 终端:

通过 shell 终端启动容器(截图由作者提供)

正如您在上面的截图中所看到的,它在 bash 终端内启动了容器,并且虚拟环境已按预期设置为my_envpandas库已经安装并可以加载,我们现在可以从终端编辑文件。

需要注意的一个问题是容器仍然是临时的。因此,我们在镜像内创建的任何代码都无法导出,并且在停止容器运行后将会丢失。

一个简单的解决方案是使用volume参数挂载一个卷。为了简便起见,我们将继续挂载本地文件夹,其中包含 Dockerfile 和辅助文件,挂载到容器内一个名为my_scripts的新文件夹中:

docker run -v .:/my_scripts  --interactive --tty my_python_env:3.10

这是输出结果:

将本地卷挂载到容器中(截图由作者提供)

一旦文件夹被挂载,容器内挂载文件夹中创建、修改或删除的任何文件将会反映到本地文件夹。这使得在停止容器时能够保持代码的更新。

总结

在本教程中,我们回顾了如何使用命令行设置 Docker 化的 Python 环境。虽然这既不是开发 Python 的实用方法,也不是推荐的方法,但它是学习 Docker 核心命令和基本功能的一个很好的体验。我们展示了如何轻松地使用内置镜像并根据我们的需求对其进行定制。最后但同样重要的是,我们展示了如何使用卷参数将本地文件夹挂载到容器中,将容器从临时模式转为持久模式。

资源

设置和监控 RDS Proxy

原文:towardsdatascience.com/setting-up-and-monitoring-rds-proxy-a7afd6b026c3?source=collection_archive---------6-----------------------#2024-09-17

反思使用 AWS 管理的 DB Proxy 时的经验教训

Ross RhodesTowards Data Science Ross Rhodes

·发表于 Towards Data Science ·7 分钟阅读·2024 年 9 月 17 日

--

补充亚马逊 Web 服务(AWS)关系数据库服务(RDS),Proxy 在 2020 年 6 月正式在 RDS for MySQL 和 PostgreSQL 以及它们的 Aurora 兼容版本中推出。Proxy 促进了数据库连接的池化和共享,对于需要大规模查询 RDS 数据库并具有托管故障切换功能的无服务器应用程序非常有价值。自 2020 年 6 月以来,Proxy 已扩展支持 RDS for MariaDB 和 SQL Server。

RDS Proxy 已经由优秀的文档提供支持——亚马逊自家的产品文档是一个有用的参考。本文基于现有资料,重点介绍了设置和监控 Proxy 时的经验教训。这些观察来自于在 Aurora MySQL 上使用 Proxy 的经验,但无论数据库引擎的目标是什么,都适用。

图片来自 Growtikavia Unsplash.

在我们深入探讨经验教训之前,有几个 Proxy 术语需要解释。首先,数据库(DB)实例被称为 目标,并且目标与 Proxy 的 目标组 相关联。每个目标组由一个单独的 RDS DB 实例或一个 RDS DB 集群组成,其中集群包含多个 DB 实例。Proxy 使用一个或多个 端点 来将查询转发到目标组。

高可用性…

使用 AWS S3 设置自动化模型训练工作流

原文:towardsdatascience.com/setting-up-automated-model-training-workflows-with-aws-s3-cd0587b42f34?source=collection_archive---------6-----------------------#2024-03-18

工作流自动化的开源方法

Khuyen TranTowards Data Science Khuyen Tran

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

--

动机

假设你是一个电子商务平台,旨在增强推荐个性化。你的数据存储在 S3 中。

为了优化推荐,你计划每当 S3 中添加新文件时,使用最新的客户互动数据重新训练推荐模型。那么,如何具体处理这个任务呢?

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

解决方案

解决这个问题的两种常见方法是:

  1. AWS Lambda: AWS 提供的无服务器计算服务,允许在事件触发时执行代码,无需管理服务器。

  2. 开源调度器: 自动化、调度和监控工作流与任务的工具,通常是自托管的。

使用开源调度器相较于 AWS Lambda 提供了以下优势:

  • 成本效益: 在 AWS Lambda 上运行长时间任务可能会很昂贵。开源调度器允许你使用自己的基础设施,潜在地节省成本。

  • 更快的迭代: 在本地开发和测试工作流能加快过程,使调试和优化变得更加容易。

  • 环境控制: 对执行环境的完全控制使你能够根据自己的喜好自定义开发工具和集成开发环境(IDE)。

虽然你可以在 Apache Airflow 中解决这个问题,但它需要复杂的基础设施和部署设置。因此,我们将使用 Kestra,它提供了直观的用户界面,并且可以通过一个 Docker 命令启动。

欢迎在此处播放并分叉本文的源代码:

[## GitHub - khuyentran1401/mlops-kestra-workflow

通过在 GitHub 上创建账户,参与 khuyentran1401/mlops-kestra-workflow 的开发。

github.com](https://github.com/khuyentran1401/mlops-kestra-workflow?source=post_page-----cd0587b42f34--------------------------------)

工作流摘要

该工作流由两个主要组件组成:Python 脚本和编排。

编排

  • Python 脚本和流程存储在 Git 中,并且会按计划同步到 Kestra。

  • 当 S3 桶的“new”前缀下出现新文件时,Kestra 会触发一系列 Python 脚本的执行。

Python 脚本

由于我们将在 Kestra 中执行从 Git 下载的代码,请确保将这些 Python 脚本提交到仓库中。

git add .
git commit -m 'add python scripts'
git push origin main

编排

启动 Kestra

执行以下命令下载 Docker Compose 文件:

curl -o docker-compose.yml \
https://raw.githubusercontent.com/kestra-io/kestra/develop/docker-compose.yml

确保 Docker 正在运行。然后,使用以下命令启动 Kestra 服务器:

docker-compose up -d

通过在浏览器中打开 URL localhost:8080 访问 UI。

从 Git 同步

由于 Python 脚本托管在 GitHub 上,我们将使用 Git Sync 每分钟将代码从 GitHub 同步到 Kestra。要设置此功能,请在“_flows”目录下创建一个名为“sync_from_git.yml”的文件。

.
├── _flows/
│   └── sync_from_git.yml
└── src/
    ├── download_files_from_s3.py
    ├── helpers.py
    ├── merge_data.py
    ├── process.py
    └── train.py

如果您使用的是 VSCode,可以使用 Kestra 插件 来启用 .yaml 文件中的流程自动完成和验证功能。

以下是从 Git 同步代码的流程实现:

id: sync_from_git
namespace: dev

tasks:
  - id: git
    type: io.kestra.plugin.git.Sync
    url: https://github.com/khuyentran1401/mlops-kestra-workflow
    branch: main
    username: "{{secret('GITHUB_USERNAME')}}"
    password: "{{secret('GITHUB_PASSWORD')}}"
    dryRun: false  # if true, you'll see what files will be added, modified
    # or deleted based on the Git version without overwriting the files yet

triggers:
  - id: schedule
    type: io.kestra.core.models.triggers.types.Schedule
    cron: "*/1 * * * *" # every minute

只有在 GitHub 仓库是私有的情况下,才需要提供用户名和密码。要将这些密钥传递给 Kestra,请将它们放入“.env”文件中:

# .env
GITHUB_USERNAME=mygithubusername
GITHUB_PASSWORD=mygithubtoken
AWS_ACCESS_KEY_ID=myawsaccesskey
AWS_SECRET_ACCESS_KEY=myawssecretaccesskey
# ! This line should be empty

接下来,使用以下 bash 脚本对这些密钥进行编码:

while IFS='=' read -r key value; do
    echo "SECRET_$key=$(echo -n "$value" | base64)";
done < .env > .env_encoded

执行此脚本会生成一个包含编码后的密钥的“.env_encoded”文件:

# .env_encoded
SECRET_GITHUB_USERNAME=bXlnaXRodWJ1c2VybmFtZQ==
SECRET_GITHUB_PASSWORD=bXlnaXRodWJ0b2tlbg==
SECRET_AWS_ACCESS_KEY_ID=bXlhd3NhY2Nlc3NrZXk=
SECRET_AWS_SECRET_ACCESS_KEY=bXlhd3NzZWNyZXRhY2Nlc3NrZXk=

在 Docker Compose 文件中包含编码后的环境文件,以便 Kestra 访问环境变量:

# docker-compose.yml
  kestra:
    image: kestra/kestra:latest-full
    env_file:
      - .env_encoded

确保在“.gitignore”文件中排除环境文件:

# .gitignore
.env
.env_encoded

最后,将新流程和 Docker Compose 文件都提交到 Git:

git add _flows/sync_from_git.yml docker-compose.yml
git commit -m 'add Git Sync'
git push origin main

现在,随着sync_from_git流程设置为每分钟运行一次,你可以方便地通过 Kestra UI 访问并触发 Python 脚本的执行。

编排

我们将创建一个流程,当有新文件添加到“winequality-red”桶中的“new”前缀时触发。

一旦检测到新文件,Kestra 会将其下载到内部存储并执行 Python 文件。最后,它将文件从“new”前缀移动到“old”前缀,以避免在随后的轮询中重复检测。

id: run_ml_pipeline
namespace: dev
tasks:
  - id: run_python_commands
    type: io.kestra.plugin.scripts.python.Commands
    namespaceFiles:
      enabled: true
    env:
      AWS_ACCESS_KEY_ID: "{{secret('AWS_ACCESS_KEY_ID')}}"
      AWS_SECRET_ACCESS_KEY: "{{secret('AWS_SECRET_ACCESS_KEY')}}"
    docker:
      image: ghcr.io/kestra-io/pydata:latest
    beforeCommands:
      - pip install -r requirements.txt
    commands:
      - python src/download_files_from_s3.py
      - python src/merge_data.py
      - python src/process.py
      - python src/train.py
    outputFiles:
      - "*.pkl"
triggers:
  - id: watch
    type: io.kestra.plugin.aws.s3.Trigger
    interval: PT1S
    accessKeyId: "{{secret('AWS_ACCESS_KEY_ID')}}"
    secretKeyId: "{{secret('AWS_SECRET_ACCESS_KEY')}}"
    region: us-east-2
    bucket: winequality-red
    prefix: new
    action: MOVE
    moveTo:
      bucket: winequality-red
      key: old

run_python_commands任务使用:

  • 使用namespaceFiles访问本地项目中的所有文件,并与 Git 仓库同步。

  • 使用env来检索环境变量。

  • 使用docker在 docker 容器ghcr.io/kestra-io/pydata:latest内执行脚本。

  • 使用beforeCommands在执行命令之前从“requirements.txt”文件安装依赖。

  • 使用commands按顺序执行命令列表。

  • 使用outputFiles将所有 pickle 文件从本地文件系统发送到 Kestra 的内部存储。

最后,添加upload任务,将模型的 pickle 文件上传到 S3。

id: run_ml_pipeline
namespace: dev
tasks:
  - id: run_python_commands
    type: io.kestra.plugin.scripts.python.Commands
    namespaceFiles:
      enabled: true
    env:
      AWS_ACCESS_KEY_ID: "{{secret('AWS_ACCESS_KEY_ID')}}"
      AWS_SECRET_ACCESS_KEY: "{{secret('AWS_SECRET_ACCESS_KEY')}}"
    docker:
      image: ghcr.io/kestra-io/pydata:latest
    beforeCommands:
      - pip install -r requirements.txt
    commands:
      - python src/download_files_from_s3.py
      - python src/merge_data.py
      - python src/process.py
      - python src/train.py model_path=model/model.pkl
    outputFiles:            
      - "*.pkl"    
  # ------------------------- ADD THIS ------------------------- #
  - id: upload              
    type: io.kestra.plugin.aws.s3.Upload
    accessKeyId: "{{secret('AWS_ACCESS_KEY_ID')}}"
    secretKeyId: "{{secret('AWS_SECRET_ACCESS_KEY')}}"
    region: us-east-2
    from: '{{outputs.run_python_commands.outputFiles["model/model.pkl"]}}'
    bucket: winequality-red
    key: model.pkl
  # ------------------------------------------------------------ #
triggers:
  ...

就这样!将此流程命名为“run_ml_pipeline.yml”并提交到 Git。

git add run_ml_pipeline.yml
git commit -m 'add run_ml_pipeline'
git push origin main

触发流程

要启动流程,只需将新文件添加到 S3 的“winequality-red”桶中的“new”前缀。

该操作将触发run_ml_pipeline流程,启动从“旧”前缀下载数据、合并所有文件、处理数据并训练模型。

一旦工作流执行完毕,“model.pkl”文件会被上传到 S3。

结论

本文展示了如何使用 Kestra 自动化执行数据科学任务的 Python 脚本,每当有新文件添加到 S3 时。如果你在寻找自动化机器学习管道的方式,可以尝试这个解决方案。

我喜欢写关于数据科学的概念,并玩弄各种数据科学工具。你可以通过以下方式保持关注我的最新文章:

在 EC2 上设置带 GPU 支持的 PyTorch,无需预配置的 AMI

原文:towardsdatascience.com/setting-up-pytorch-with-gpu-support-on-ec2-without-preconfigured-amis-3b101b05a765?source=collection_archive---------10-----------------------#2024-03-09

一种具有成本效益的方法

Zachary RaicikTowards Data Science Zachary Raicik

·发表于 Towards Data Science ·阅读时间:7 分钟·2024 年 3 月 9 日

--

图片来源:Adi Goldstein 通过 Unsplash

亚马逊云服务(“AWS”)的弹性计算云(“EC2”)提供了强大且可扩展的计算选项。它允许开发者访问配备高性能处理单元(如 GPU,图形处理单元)的虚拟计算环境。这些 GPU 加速了复杂机器学习模型的训练,使得在普通计算机上不可行或极其缓慢的任务变得可行。这对于深度学习模型尤为重要,因为深度学习需要大量的计算能力来处理庞大的数据集并进行复杂的计算。

当你启动 EC2 实例时,AWS 会提供从零开始配置该实例或使用预构建的 Amazon 机器镜像(AMI)的选择。预构建的 AMI 是一个包含特定用途的软件配置(操作系统、工具和应用程序)的模板。例如,你可能会使用一个为深度学习配置的预构建 AMI。

尽管预构建的 AMI 很好,但它们不是免费的,可能会增加 EC2 实例的成本。随着时间的推移,这些额外的成本可能会变得显著。通过从零开始配置 EC2 实例,你不仅能节省成本,还能对设置过程有更深的理解……

机器学习中数据泄漏的七个常见原因

原文:towardsdatascience.com/seven-common-causes-of-data-leakage-in-machine-learning-75f8a6243ea5?source=collection_archive---------4-----------------------#2024-09-14

数据预处理、特征工程和训练-测试集划分中防止数据泄漏的关键步骤

Yu DongTowards Data Science Yu Dong

·发表于 Towards Data Science ·阅读时长 7 分钟·2024 年 9 月 14 日

--

当我在评估像 ChatGPT、Claude 和 Gemini 这样的 AI 工具用于机器学习应用场景时,我遇到了一个严重的陷阱:机器学习中的数据泄漏。在我的上一篇文章中,这些 AI 模型在将数据集划分为训练集和测试集之前,就使用整个数据集来创建新特征——这是数据泄漏的常见原因。然而,这不仅是 AI 的错误;人类也经常犯这个错误。

在机器学习中,数据泄漏发生在训练数据集之外的信息渗入到模型构建过程中。这导致了性能指标的虚高,且模型无法很好地推广到未见过的数据。在本文中,我将介绍七个常见的数据泄漏原因,帮助你避免像人工智能那样犯同样的错误 😃

图像来源:DALL·E

问题设置

为了更好地解释数据泄漏,让我们考虑一个假设的机器学习应用场景:

假设你是美国运通等大型信用卡公司的数据科学家。每天,数百万笔交易会被处理,其中不可避免地会有一些是欺诈交易。你的任务是构建一个能够实时检测欺诈的模型…

使用 Plotly 创建专业可视化的七个关键特性

原文:towardsdatascience.com/seven-key-features-you-should-know-for-creating-professional-visualizations-with-plotly-f89558de5d0c?source=collection_archive---------9-----------------------#2024-08-06

在像领先报纸一样的水平上创建可视化

Amanda Iglesias MorenoTowards Data Science Amanda Iglesias Moreno

·Published in Towards Data Science ·5 min read·Aug 6, 2024

--

Aleks Dorohovich in Unsplash

我们习惯在在线报纸上看到互动可视化,通常会想知道数据记者使用什么工具来创建这些专业外观的可视化。事实上,不需要特殊软件来创建这种类型的可视化;Python 中大多数互动可视化库都具有高度定制化的特性,允许从设计角度创建高质量的可视化。在本文中,我们将逐步展示如何通过对 Plotly 创建的互动可视化进行定制,仅通过少量调整即可显著改善其设计。

原始可视化

本文中,我们将使用简单的可视化,比如某一变量的时间演变。要可视化的数据对应于 1800 年至 2020 年的德国人口。首先,我们将使用 Plotly 创建最简单的数据可视化,没有任何定制。随后,我们将逐步解释所有使用的定制内容,并在文章末尾提供改进可视化的代码。

从数据科学到应用的七项必备技能

原文:towardsdatascience.com/seven-requisite-skills-for-navigating-from-data-science-to-applications-d23e04f7ee1f?source=collection_archive---------7-----------------------#2024-04-12

帮助初级数据科学家将创意转化为工业级应用

Wencong Yang, PhDTowards Data Science Wencong Yang, PhD

·发表于 Towards Data Science ·阅读时间:10 分钟·2024 年 4 月 12 日

--

图片由作者提供 (Ideogram)

回想我的大学时光,我在数据科学项目中的角色就像是炼金术士——试验各种炫酷的 AI 模型,试图从我的专业数据中挖掘变量之间的关系。强大的 AI 算法让我不断惊叹,它们总是能超越传统的统计方法和基于物理的模型。然而,真正的挑战在于我在 2022 年成为 AI 工程师进入行业后才开始。自那时起,数据科学的技术栈迅速扩展到了我不熟悉的领域。我在行业中的第一个挑战是将一个模型部署到生产环境中,并要求其具备可靠性、可维护性和可扩展性。回顾我当时的困境,我意识到,将 AI 模型从原型转变为生产就绪的应用无非是以下几个方面的结合:

  • 良好的设计模式

  • 健壮的代码

  • 高效的部署策略

本文是一本全面的指南,总结了我之前子文章中的七个关键主题。每个主题探讨了在行业级别开发和部署数据科学项目的一个方面:

Shapley 值清晰解释

原文:towardsdatascience.com/shapley-values-clearly-explained-a7f7ef22b104?source=collection_archive---------0-----------------------#2024-02-04

公平地将团队成果分配给每个成员

Dr. Robert KüblerTowards Data Science Dr. Robert Kübler

·发表于 Towards Data Science ·12 分钟阅读·2024 年 2 月 4 日

--

图片由 Vadim Sherbakov 提供,来源于 Unsplash

上次你和朋友们一起合作并取得了一些伟大的成绩是什么时候?无论是赢得一场比赛、完成一个工作项目,还是在 Kaggle 比赛中进入前三。如果你不记得了(真可怜),那和朋友们度过一个愉快的夜晚怎么样?想象一下:度过了一个精彩的晚上,随后大家一起搭出租车回家,却迎来了一个不小的出租车账单。在这样的时刻,你或许会开始思考:

我们如何能将团队成果公平地分配给每个成员?

一个团队也可以是机器学习模型的特征,团队的成果就是模型的预测结果。在这种情况下,了解每个特征对最终预测的贡献有多大是非常有趣的。这正是数据科学家通常关注的内容。

公平还是平均?

举个例子,假设你和另外两位朋友组成团队参加比赛,表现优异并作为团队获得了 12,000 欧元。你该如何公平地分配这笔钱呢?平均分配没有问题,每个人获得 4,000 欧元。但我们都知道,通常有些人对团队的成功贡献要比其他人多。那么……

共享最近邻:一种更强大的距离度量

原文:towardsdatascience.com/shared-nearest-neighbors-a-more-robust-distance-metric-064d7f99ffb7?source=collection_archive---------0-----------------------#2024-09-19

一种能够在具有多维度和不同密度的数据集中改善预测、聚类和异常值检测的距离度量

W Brett KennedyTowards Data Science W Brett Kennedy

·发表于 Towards Data Science ·28 分钟阅读·2024 年 9 月 19 日

--

在这篇文章中,我将介绍一种称为共享最近邻(Shared Nearest Neighbors,简称 SNN)的距离度量,并描述它在异常值检测中的应用。我还会简要介绍它在预测和聚类中的应用,但重点将放在异常值检测上,特别是 SNN 在 k 最近邻异常值检测算法中的应用(尽管我也会更一般地介绍 SNN 在异常值检测中的应用)。

本文是关于异常值检测系列文章的一部分,包括关于频繁模式异常值因子计数异常值检测器掺假距离度量学习的文章。它还包含了我书中另一篇节选:Python 中的异常值检测

在数据科学中,处理表格数据时,衡量行之间的距离是一个非常常见的任务。例如,这在一些预测模型中很常见,如 kNN:在使用 kNN 预测实例的目标值时,我们首先从训练数据中识别出最相似的记录(这需要一种衡量行之间相似性的方式)。然后,我们查看这些相似行的目标值,假设测试记录最有可能与大多数最相似记录的目标值相同(用于分类),或者与最相似记录的目标值的平均值相同(用于回归)。

一些其他预测模型也使用距离度量,例如基于半径的方法,如RadiusNeighborsClassifier。然而,距离度量最常被使用的地方是聚类。事实上,距离计算几乎在所有聚类算法中都是普遍的:据我所知,所有聚类算法都以某种方式依赖于计算记录对之间的距离。

距离计算被许多异常值检测算法使用,包括许多最流行的算法(如 k 最近邻、局部离群因子(LOF)、半径、局部离群概率(LoOP)以及许多其他算法)。并非所有异常值检测算法都如此:许多算法以不同的方式识别异常值(例如孤立森林频繁模式异常因子计数异常检测器ECODHBOS),但许多检测器确实以某种方式利用行之间的距离计算。

聚类和异常值检测算法(那些使用距离的算法)通常从计算成对距离开始,即数据中每一对行之间的距离。至少从理论上讲是这样的:为了提高效率,某些行对之间的距离计算可能会被跳过或近似,但理论上我们通常从计算一个 n x n 的距离矩阵开始,其中 n 是数据中的行数。

这就要求我们有一种方式来衡量任何两个记录之间的距离。但是,正如在相关的距离度量学习(DML)文章中所讨论的那样,确定一种有效的方法来识别两行的相似性或不相似性可能是困难的。

最常见的方法,至少对于数值数据来说,是欧几里得距离。这种方法可以很好地工作,并且具有很强的直观吸引力,特别是在从几何角度查看数据时:即将数据视为空间中的点,如下方的散点图所示。在二维图中,其中每个数据记录都表示为一个点,查看记录之间的相似性自然就变成了基于欧几里得距离。

然而,现实世界中的表格数据通常具有非常多的特征,而处理这类数据时的关键难题之一就是所谓的维度灾难。这一问题表现为多种形式,其中最棘手的一种是,当维度足够高时,记录之间的距离开始变得毫无意义。

在这里展示的图中,我们有一个点(红色标示),在维度 0(显示在左侧窗格的 x 轴上)中是异常的,但在维度 1、2 和 3 中是正常的。假设这个数据集只有这四个维度,计算每对记录之间的欧几里得距离时,我们会发现红点与其他所有点之间的距离异常大。因此,它可能会被可靠地标记为离群点。

然而,如果有数百个维度,并且红点在除了维度 0 之外的所有维度中都比较典型,它可能无法被可靠地标记为离群点:在维度 0 中到其他点的大距离将与所有其他维度的距离一起被平均,最终变得不再相关。

这是一个巨大的问题,尤其对于依赖距离度量的预测、聚类和离群点检测方法而言。

有时使用 SNN(共享最近邻)来缓解这一效应。然而,正如我在下面的实验中所展示的,SNN 最有效的地方(至少在我下面使用的 k 近邻离群点检测器中)并不一定是在维度很多的地方(尽管这也很相关),而是在数据的密度从一个区域到另一个区域变化的地方。我将在下面解释这意味着什么,以及它如何影响一些离群点检测器。

SNN 用于定义两个记录之间的距离,类似于欧几里得距离、曼哈顿距离、堪培拉距离、余弦相似度以及其他多种距离度量方法。顾名思义,计算出来的具体距离与任意两条记录共享的邻居数目有关。

这样一来,SNN 与其他距离度量方法有所不同,尽管它仍然比距离度量学习更接近欧几里得距离和其他标准度量方法。DML(距离度量学习)旨在寻找记录之间的逻辑距离,而不依赖于行中数值的具体大小。

另一方面,SNN 实际上是通过使用标准距离度量计算行之间的原始距离来开始的。如果第一步使用欧几里得距离,那么 SNN 距离与欧几里得距离相关;如果使用余弦距离计算原始距离,那么 SNN 距离与余弦距离相关;依此类推。

然而,在我们深入细节或展示它如何应用于离群点检测之前,我们先快速了解一下 SNN 聚类,因为 SNN 最初就是在聚类研究中提出的。那里的描述过程正是用于计算 SNN 距离的过程,其他上下文中,包括离群点检测,也会使用相同的方法。

SNN 聚类

术语可能会稍显混淆,但还有一种聚类方法通常被称为 SNN,它使用 SNN 距离,并且与 DBSCAN 聚类非常相似。实际上,它可以被视为对 DBSCAN 的增强。

描述这一内容的主要论文可以在这里查看:www-users.cse.umn.edu/~kumar001/papers/siam_hd_snn_cluster.pdf。不过,增强 DBSCAN 以使用 SNN 的想法可以追溯到 1973 年 Jarvis-Patrick 写的一篇论文。这里链接的论文采用了类似但改进的方法。

DBSCAN 是一个强大的聚类算法,仍然被广泛使用。它能够很好地处理不同大小和形状的聚类(甚至是非常任意的形状)。不过,它在处理密度不同的聚类时可能会遇到困难(它实际上假设所有聚类的密度相似)。大多数聚类算法都有类似的局限性。例如,K-means 聚类实际上假设所有聚类的大小相似,而高斯混合模型聚类则假设所有聚类大致呈高斯形状。

我不会在这里描述完整的 DBSCAN 算法,但简要概述一下:它通过识别所谓的核心点来工作,这些点位于密集区域,可以安全地视为内点。然后,它会识别与这些核心点接近的其他点,在每个核心点周围创建聚类。算法会经过一系列步骤,每次都扩展并合并到目前为止发现的聚类(当聚类重叠时进行合并)。那些接近现有聚类的点(即使它们并不接近原始的核心点,而是接近已经加入某个聚类的点)也会被添加到该聚类中。最终,每个点要么属于某个聚类,要么没有被分配到任何聚类中(这些点相对孤立)。

与异常值检测类似,聚类也可能在高维数据集上遇到困难,原因同样是维度诅咒,尤其是在标准距离度量失效的情况下。在每一步中,DBSCAN 基于尚未聚类的点与已聚类的点之间的距离进行工作,而这些距离计算不可靠时,聚类结果也就不可靠。在高维空间中,核心点可能与其他任何点都难以区分,即便是那些实际上不属于任何聚类的噪声点。

如前所述,DBSCAN 在数据的不同区域具有不同密度时也会遇到困难。问题在于,DBSCAN 使用的是全局的点之间接近度标准,但不同区域可能合理地具有不同的密度。

以金融交易数据为例。这可能包括销售、费用、工资单以及其他类型的交易,每种交易的密度不同。交易可能在不同的时间以不同的速率创建,具有不同的美元金额、数量和数值范围。例如,销售交易可能远多于费用交易。而且,美元金额的范围可能大不相同:例如,最大的销售额可能只是最小销售额的 10 倍,而最大的费用可能是最小费用的 1000 倍。因此,销售交易和费用交易之间的密度差异可能很大。

假设不同类型的交易位于空间的不同区域(如果再次将数据视为高维空间中的点,每个维度代表数据表中的一个特征,每条记录作为一个点),我们可能得到如下图所示的图表,销售交易位于左下角,费用交易位于右上角。

许多聚类算法(以及许多预测和异常值检测算法)可能由于密度差异而无法很好地处理这些数据。如果 DBSCAN 根据点之间距离的整体平均值进行聚类(如果数据中的销售交易数量远多于费用交易),它可能会将右上角的所有点都排除在聚类之外。

SNN 的目标是创造一个更可靠的距离度量,以应对高维度和变化的密度。

SNN 的核心思想是:如果点 p1 与 p2 根据标准距离度量接近,我们可以说它们可能真的接近,但这并不可靠。然而,如果 p1 和 p2 还具有许多相同的最近邻居,我们可以更有信心地认为它们确实接近。它们的共享邻居可以说是确认了相似性

使用共享邻居时,在上述图表中,右上角的点将被正确地识别为一个聚类,因为它们通常与彼此共享许多相同的最近邻居。

Jarvis-Patrick 用图的方式解释了这一点,这是一种观察数据的有用方法。我们可以将每个记录视为空间中的一个点(如上面的散点图所示),每一对记录之间的边表示它们的相似度。为此,我们可以简单地计算每对记录之间的欧几里得距离(或其他类似的度量)。

由于图通常表示为邻接矩阵(n x n 矩阵,其中 n 是行数,给出每对行之间的距离),我们也可以从邻接矩阵的角度来看这个过程。

考虑上面的散点图,我们可能有一个 n x n 的矩阵,如下所示:

 Point 1   Point 2   Point 3   ...   Point n
Point 1    0.0      3.3        2.9    ...     1.9
Point 2    3.3      0.0        1.8    ...     4.0
Point 3    2.9      1.8        0.0    ...     2.7
...        ...      ...        ...    ...     ...
Point n    1.9      4.0        2.7    ...     0.0

该矩阵在主对角线对称(即从点 1 到点 2 的距离与从点 2 到点 1 的距离相同),且点到自身的距离为 0.0(因此主对角线完全是零)。

SNN 算法是一个两步过程,首先计算这些原始的成对距离(通常使用欧几里得距离)。然后,它创建第二个矩阵,其中包含共享最近邻的距离。

计算这一点时,首先使用一种称为稀疏化的过程。对于每一对记录 p 和 q,只有当 p 和 q 都在对方的 k 个最近邻列表中时,它们之间才会有一条连接(即距离非零)。这一点很容易确定:对于 p,我们可以得到到所有其他点的距离。对于某个 k(指定为参数,这里假设为 10),我们找到距离 p 最近的 10 个点。这些点中可能包括 q,也可能不包括。同样地,对于 q:我们找到它的 k 个最近邻并查看 p 是否在其中。

我们现在得到了一个类似上面的矩阵,但许多单元格现在包含了零。

然后,我们考虑共享最近邻。对于指定的 k,p 有一个 k 个最近邻的集合(我们称之为 S1),q 也有一个 k 个最近邻的集合(我们称之为 S2)。我们可以根据 S1 和 S2 的重叠大小来确定 p 和 q 的相似度(在 SNN 的意义上)。

在一种更复杂的形式中,我们还可以考虑 S1 和 S2 中邻居的顺序。如果 p 和 q 不仅有大致相同的最近邻集(例如,它们都靠近点 p243、p873、p3321 和 p773),我们可以确信 p 和 q 是接近的。但如果进一步地,它们首先最接近 p243,然后是 p873,再然后是 p3321,最后是 p773(或者至少它们的最近邻顺序相似),我们可以更加确信 p 和 q 是相似的。然而,在本文中,我们将简单地计算 p 和 q 的共享最近邻数量(在它们各自的 k 个最近邻集合中)。

所以,我们确实需要一个标准的距离度量来开始,但一旦创建了这个度量,我们就使用点之间距离的排名顺序,而不是实际的大小,这样往往更为稳定。

对于 SNN 聚类,我们首先以这种方式计算 SNN 距离,然后继续使用标准的 DBSCAN 算法,识别核心点,找到足够接近的其他点并将其归为同一簇,然后不断扩展并迭代合并这些簇。

在 github 上至少有两种 SNN 聚类的实现:github.com/albert-espin/snn-clusteringgithub.com/felipeangelimvieira/SharedNearestNeighbors

尽管 SNN 最初来源于聚类(并且在聚类中仍然很重要),正如上面所示,SNN 作为一种距离度量方法,实际上也与机器学习的其他领域相关,包括异常值检测,接下来我们将回到这个话题。

SNN 距离度量的实现

在描述 SNN 距离度量的 Python 实现之前,我将简要展示一个 KNN 异常值检测器的简单实现:

import pandas as pd
from sklearn.neighbors import BallTree
import statistics

class KNN:
    def __init__(self, metric='euclidian'):
        self.metric = metric

    def fit_predict(self, data, k):
        data = pd.DataFrame(data)
        balltree = BallTree(data, metric=self.metric)

        # Get the distances to the k nearest neighbors for each record
        knn = balltree.query(data, k=k)[0]

        # Get the mean distance to the k nearest neighbors for each record
        scores = [statistics.mean(x[:k]) for x in knn]
        return scores

给定一个二维数据表和指定的 k,fit_predict()方法会为每条记录提供一个异常值分数。这个分数是到 k 个最近邻的平均距离。这一变体中,使用的是到 k 个最近邻的最大距离(而不是平均距离),有时被称为 kth 最近邻,而这个变体通常被称为 k 最近邻,尽管术语有所不同。

这里的大部分工作实际上是由 scikit-learn 的 BallTree 类完成的,它计算并存储传入数据框架的成对距离。其 query()方法返回,对于传入数据参数中的每个元素,两个内容:

  • 到 k 个最近点的距离。

  • k 个最近点的索引。

对于这个检测器,我们只需要距离,因此取返回结构中的元素[0]。

fit_predict()随后返回每条记录到 k 个最近邻的平均距离,这也是其异常值度量:一条记录距离其最近邻越远,就越可能被认为是异常值(尽管如上所述,在不同区域具有不同密度的情况下表现较差,也就是说,平均距离与邻居之间的距离差异较大)。

这不是一个生产环境下可以使用的实现,但它提供了基本思路。KNN 异常值检测的完整实现可以在PyOD中找到。

使用 SNN 距离度量,简单的异常值检测器实现如下:

class SNN:
    def __init__(self, metric='euclidian'):
        self.metric = metric

    def get_pairwise_distances(self, data, k):
        data = pd.DataFrame(data)
        balltree = BallTree(data, metric=self.metric)  
        knn = balltree.query(data, k=k+1)[1]
        pairwise_distances = np.zeros((len(data), len(data)))
        for i in range(len(data)):
            for j in range(i+1, len(data)):
                if (j in knn[i]) and (i in knn[j]):
                    weight = len(set(knn[i]).intersection(set(knn[j])))
                    pairwise_distances[i][j] = weight
                    pairwise_distances[j][i] = weight
        return pairwise_distances

    def fit_predict(self, data, k):
        data = pd.DataFrame(data)
        pairwise_distances = self.get_pairwise_distances(data, k)
        scores = [statistics.mean(sorted(x, reverse=True)[:k]) for x in pairwise_distances]
        min_score = min(scores)
        max_score = max(scores)
        scores = [min_score + (max_score - x) for x in scores]
        return scores

这里的 SNN 检测器实际上也可以被看作是一个 KNN 异常值检测器,只是使用了 SNN 距离。为了简便起见,我们将这两种异常值检测器分别称为 KNN 和 SNN,并假设 KNN 检测器使用标准的距离度量方法,如曼哈顿距离或欧几里得距离,而 SNN 检测器则使用 SNN 距离度量方法。

与 KNN 检测器一样,SNN 检测器会为传递给fit_predict()的每条记录返回一个得分,这里是到 k 个最近邻的平均 SNN 距离,而不是使用标准距离度量的平均距离。

该类还提供了get_pairwise_distances()方法,该方法由fit_predict()调用,但在计算成对的 SNN 距离时也可以直接调用(稍后我们会看到一个使用 DBSCAN 进行离群点检测的例子)。

get_pairwise_distances()中,我们获取 BallTree 的query()方法返回结果的第[1]项,因为我们关心的是最近邻,而不是它们的具体距离。

如前所述,除非两个记录在彼此的 k 个最近邻中,否则我们将所有距离设为零。然后我们计算具体的 SNN 距离,即每对点在 k 个最近邻集合中共享的邻居数量。

可以使用如 Jaccard 或 Dice 等度量来量化每一对点最近邻的重叠情况,但由于两者的大小相同(k),我们可以简单地计算每一对的重叠大小。

在另一种提供的方法fit_predict()中,我们首先获取成对的距离。这些实际上是常态的度量,而不是离群点度量,因此在返回得分之前,这些值会被反转。

最终得分是每条记录与 k 个最近邻的平均重叠度。

所以,k 在这里实际上有两个不同的用途:它首先用于识别 k 个最近邻(在此步骤中,我们计算 KNN 距离,使用欧几里得或其他类似的度量),然后在第二步中再次使用(在此步骤中,我们计算 SNN 距离,使用平均重叠度)。也可以使用两个不同的参数来实现这些功能,一些实现确实这样做,有时将第二个参数称为eps(这来源于 DBSCAN 的历史,其中 eps 用于定义两个点之间的最大距离,以便认为它们属于同一邻域)。

再次强调,这不一定是生产就绪的代码,且远未优化。有一些技术可以提高速度,这也是一个活跃的研究领域,特别是第一步,计算原始的成对距离。在数据量非常大的情况下,可能需要考虑使用 BallTree 的替代方法,如faiss,或者以其他方式加速处理。但对于中等大小的数据集,像这里的代码通常是足够的。

离群点检测测试

我已经以多种方式测试了上述的 KNN 和 SNN 离群点检测器,使用了合成数据和真实数据。我多年来也在多个离群点检测项目中使用了 SNN 距离。

总的来说,我实际上没有发现 SNN 在高维情况下比 KNN 更优,尽管在某些时候 SNN 是更可取的。

然而,在我看到的情况中,当数据具有不同的密度时,SNN 相对于标准 KNN 确实有明显的优势。

更准确地说,SNN 在高维度和不同密度的结合下,通常能明显优于其他距离度量方法和 KNN 类型的检测器,比仅有高维度或仅有不同密度的情况要更为显著。

这可以通过以下测试代码来观察。它使用了(相对)简单的合成数据,以便更清楚地呈现这一点。

def test_variable_blobs(nrows=1000, ncols=500, nclusters=60, outlier_multiplier=2.0, k=30, metric='manhattan'):
    np.random.seed(1)

    # ########################################################
    # Create the test data

    # Set the size of each cluster
    n_samples_arr = []
    remaining_count = nrows
    for i in range(nclusters-1):
        cluster_size = np.random.randint(1, remaining_count // (nclusters - i))
        n_samples_arr.append(cluster_size)
        remaining_count -= cluster_size
    n_samples_arr.append(remaining_count)

    # Set the density of each cluster
    cluster_std_arr = []
    for i in range(nclusters):
        cluster_std_arr.append(np.random.uniform(low=0.1, high=2.0))

    # Set the center location of each cluster
    cluster_centers_arr = []
    for i in range(nclusters):
        cluster_centers_arr.append(np.random.uniform(low=0.0, high=10.0, size=ncols))

    # Create the sample data using the specified cluster sizes, densities, and locations
    x, y = make_blobs(n_samples=n_samples_arr,
                      cluster_std=cluster_std_arr,
                      centers=cluster_centers_arr,
                      n_features=ncols,
                      random_state=0)
    df = pd.DataFrame(x)

    # Add a single known outlier to the data
    avg_row = [x[:, i].mean() for i in range(ncols)]
    outlier_row = avg_row.copy()
    outlier_row[0] = x[:, 0].max() * outlier_multiplier
    df = pd.concat([df, pd.DataFrame([outlier_row])])
    df = df.reset_index(drop=True)

    # ########################################################
    # Compare standard distance metrics to SNN

    # Calculate the outlier scores using standard KNN
    scored_df = df.copy()
    knn = KNN(metric=metric)
    scored_df['knn_scores'] = knn.fit_predict(df, k=k)

    # Calculate the outlier scores using SNN    
    snn = SNN(metric=metric)
    scored_df['snn_scores'] = snn.fit_predict(df, k=k)

    # Plot the distribution of scores for both detectors and show
    # the score for the known outlier (in context of the range of 
    # scores assigned to the full dataset)
    fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(12, 4))
    sns.histplot(scored_df['knn_scores'], ax=ax[0])
    ax[0].axvline(scored_df.loc[nrows, 'knn_scores'], color='red')
    sns.histplot(scored_df['snn_scores'], ax=ax[1])
    ax[1].axvline(scored_df.loc[nrows, 'snn_scores'], color='red')
    plt.suptitle(f"Number of columns: {ncols}")
    plt.tight_layout()
    plt.show()

在这种方法中,我们生成测试数据,向数据集中添加一个已知的离群点,获取 KNN 离群点分数,获取 SNN 离群点分数,并绘制结果。

测试数据是通过 scikit-learn 的 make_blobs()函数生成的,该函数创建了一组高维度的簇。生成的一个离群点将位于这些簇之外(并且默认情况下,列 0 中会有一个极端值)。

代码中大部分的复杂性都在于生成测试数据。在这里,我们不仅仅是调用 make_blobs()函数并使用默认参数,而是指定每个簇的大小和密度,以确保它们之间有所不同。密度是通过一组标准差来指定的(标准差描述了每个簇的分散程度)。

这会生成如下的数据:

这只展示了四个维度,但通常我们会调用此方法来创建具有多个维度的数据。已知的离群点用红色标出。在维度 0 中,它具有一个极端值,在大多数其他维度中,它趋向于落在簇之外,因此是一个强烈的离群点。

测试可以通过以下方式进行:

test_variable_blobs(nrows=1000, ncols=20, nclusters=1, k=30, metric='euclidean')
test_variable_blobs(nrows=1000, ncols=100, nclusters=5, k=30, metric='euclidean')
test_variable_blobs(nrows=1000, ncols=250, nclusters=10, k=30, metric='euclidean')
test_variable_blobs(nrows=1000, ncols=400, nclusters=15, k=30, metric='euclidean')
test_variable_blobs(nrows=1000, ncols=450, nclusters=20, k=30, metric='euclidean')
test_variable_blobs(nrows=1000, ncols=500, nclusters=20, k=30, metric='euclidean')
test_variable_blobs(nrows=1000, ncols=750, nclusters=20, k=30, metric='euclidean')
test_variable_blobs(nrows=1000, ncols=1000, nclusters=20, k=30, metric='euclidean')
test_variable_blobs(nrows=1000, ncols=2000, nclusters=20, k=30, metric='euclidean')
test_variable_blobs(nrows=1000, ncols=3000, nclusters=20, k=30, metric='euclidean')

test_variable_blobs(nrows=1000, ncols=20, nclusters=1, k=30)
test_variable_blobs(nrows=1000, ncols=100, nclusters=5, k=30)
test_variable_blobs(nrows=1000, ncols=250, nclusters=10, k=30)
test_variable_blobs(nrows=1000, ncols=400, nclusters=15, k=30)
test_variable_blobs(nrows=1000, ncols=450, nclusters=20, k=30)
test_variable_blobs(nrows=1000, ncols=500, nclusters=20, k=30)
test_variable_blobs(nrows=1000, ncols=750, nclusters=20, k=30)
test_variable_blobs(nrows=1000, ncols=1000, nclusters=20, k=30)
test_variable_blobs(nrows=1000, ncols=2000, nclusters=20, k=30)
test_variable_blobs(nrows=1000, ncols=3000, nclusters=20, k=30)

首先执行一系列使用欧几里得距离的测试(该距离被 KNN 检测器和 SNN 检测器的第一步使用),然后执行一系列使用曼哈顿距离的测试(这是 test_variable_blobs()方法的默认距离)——对于 KNN 检测器和 SNN 检测器的第一步都使用曼哈顿距离。

对于每种情况,我们使用不同数量的列进行测试(列数从 20 到 3000 不等)。

从欧几里得距离开始,使用仅 20 个特征时,KNN 和 SNN 都能良好工作,因为它们都为已知的离群点分配了较高的离群分数。在这里,我们可以看到每个检测器生成的离群分数分布(KNN 检测器显示在左侧窗格,SNN 检测器显示在右侧窗格),并有一条红色的垂直线,表示每个检测器为已知的离群点所分配的离群分数。在这两种情况下,已知的离群点得到了明显高于其他记录的分数:两个检测器都表现良好。

但是,使用欧几里得距离随着特征的增加往往会迅速退化,即使只有 100 个特征时,它的效果也相当差。这一点对于 KNN 和 SNN 检测器都适用。在这两种情况下,已知的离群点得到了一个相当正常的分数,没有显示出任何离群的迹象,如下所示:

重复使用曼哈顿距离,我们可以看到 KNN 在较少特征时表现良好,但随着特征数量的增加,其表现逐渐变差。然而,一旦特征数量超过大约 50 个,KNN 在曼哈顿距离下的表现远好于欧几里得距离(在特征较少时,几乎任何距离度量方法都能合理地工作)。

在下面所有的案例中(使用曼哈顿距离和 SNN 距离),我们在左侧窗格中展示了 KNN 离群分数的分布(以及 KNN 检测器为已知离群点分配的离群分数),在右侧窗格中展示了 SNN 分数的分布(以及 SNN 检测器为已知离群点分配的离群分数)。

使用 20 个特征时,两者表现都很好:

使用 100 个特征时,KNN 仍然给已知离群点分配了较高的分数,但并不算非常高。SNN 依然表现非常好(在下面所有案例中也是如此):

使用 250 个特征时,KNN 为已知离群点分配的分数相当低,且分数的分布很奇怪:

使用 500 个特征:

使用 1000 个特征:

使用 2000 个特征:

使用 3000 个特征:

使用 KNN 检测器,即使使用曼哈顿距离,我们可以看到在 100 个特征时,分数的分布已经非常奇怪,更重要的是,在 100 个特征时,KNN 给已知离群点分配的分数很低,无法反映其离群性质。

另一方面,SNN 分数的分布即使在 3000 个特征时仍然是合理的,而已知离群点的 SNN 分数直到接近 2000 个特征时才开始显著降低(对于 2000 和 3000 个特征,其分数仍然很高,但不是最高分)。

SNN 检测器(本质上是使用 SNN 距离的 KNN 离群检测算法)比使用曼哈顿距离的 KNN 更加可靠。

这里的一个关键点(除非考虑 SNN 距离)是,在特征数量较多时,曼哈顿距离比欧几里得距离在离群检测上更加可靠。尽管维度灾难仍然存在(所有距离度量最终都会失败),但在几十个或几百个特征下,曼哈顿距离比欧几里得距离受到的影响要小得多。

事实上,尽管欧几里得距离在低维情况下非常适用,但即使在特征数适中的情况下,欧几里得距离也会出现失效(有时仅在 30 或 40 个特征时就会失效)。在这些情况下,曼哈顿距离可以作为一个更公平的对比,这正是这里所做的。

一般来说,我们应当谨慎对待将距离度量与欧几里得距离进行比较的评估,因为这些评估可能具有误导性。虽然在进行距离计算时通常假设欧几里得距离,但这一点值得我们质疑。

在此识别的情况下(即数据仅仅是聚类,但聚类的大小和密度各不相同),SNN 确实显著优于 KNN(并且令人印象深刻的是,即便接近 2000 个特征时,仍然保持可靠)。考虑到我们是基于曼哈顿距离而非欧几里得距离与 KNN 进行比较,这一发现更具意义。

然而,在许多其他情况下,特别是当数据只有一个聚类,或聚类之间的密度相似时,KNN 可以与 SNN 一样有效,甚至更优。

这并不是说 SNN 应该总是优于其他距离度量,而是说在某些场景下,它的表现可能显著更好。

在其他情况下,其他距离度量也可能更合适,包括余弦距离、Canberra 距离、马氏距离、切比雪夫距离等。在进行异常值检测时,尝试这些方法通常是非常值得的。

全局异常值检测器

KNN 在此失效的原因与使用 DBSCAN 进行聚类时类似,即当不同区域(在这种情况下,不同聚类)具有不同密度时。

KNN 是一种被称为全局异常值检测器的检测器示例。如果你熟悉局部和全局异常值的概念,那么这个概念是相关的,但有所不同。在这种情况下,“全局”指的是有一个全局的正常感知。这与上面提到的 DBSCAN 聚类的局限性相同(即在记录之间存在一个全局的正常距离感知)。数据中的每一条记录都会与这个正常标准进行比较。在 KNN 异常值检测器的情况下,存在一个全局的正常平均距离感知,指的是与 k 个最近邻的距离。

然而,当数据在不同区域的密度不同时,这种全局标准就没有意义。下图中(从上面重复的图),有两个聚类,其中左下角的聚类密度明显高于右上角的聚类。

在识别异常值时,相关的是一个点与其邻居的距离相对于该区域的正常水平,而不是相对于其他聚类(或整个数据集)的正常水平。

这是另一个重要的异常值检测器——局部异常因子(LOF)被创建来解决的问题(原始 LOF 论文实际上描述了一个非常类似的情境)。与全局异常值检测器不同,LOF 是一种 局部 异常值检测器:它通过将点与局部区域内的其他点进行比较,而不是与整个数据集进行比较,从而对每个点进行局部的“正常”判断。对于 LOF 来说,它通过比较每个点与附近点的平均距离来做出判断。

局部异常值检测器也提供了一种有价值的方法,用于识别数据空间中密度变化的异常值,我在 Python 中的异常值检测中讨论了这一点,并将在未来的文章中继续探讨。

SNN 还为这个不同密度的问题提供了一个重要的解决方案。使用 SNN 距离时,密度的变化不再是问题。每一条记录与其最邻近点的共享邻居平均数量的全球标准进行比较。这是一种相当稳健的计算方法,能够在数据被聚类或者某些区域的数据密度更高时良好地工作。

DBSCAN 用于异常值检测

在本文中,我们主要探讨了用于异常值检测的 KNN 算法,但 SNN 可以与任何基于行间距离的异常值检测器一起使用。这包括半径、局部异常因子(LOF)以及其他许多方法。它还包括任何基于聚类的异常值检测算法。

使用聚类识别异常值有多种方法(例如,识别非常小的聚类中的点、离聚类中心较远的点,等等)。不过,在这里,我们将看一种非常简单的异常值检测方法:对数据进行聚类,然后识别那些未被归类到任何聚类中的点。

DBSCAN 是用于这类异常值检测的最常用的聚类算法之一,因为它具有一个方便的特性(并非所有聚类算法都具备):它允许某些点不被归类到任何聚类中。

DBSCAN(至少是 scikit-learn 的实现)还允许我们轻松处理 SNN 距离。

因此,DBSCAN 除了是一个有用的聚类算法外,还广泛用于异常值检测,我们将在这里作为另一个使用 SNN 距离进行异常值检测的示例。

不过,在查看使用 SNN 距离之前,我们将展示一个使用 DBSCAN 的例子,因为它更常用于识别数据中的异常值(这里使用默认的欧几里得距离)。这使用的是上面创建的相同数据集,其中最后一行是唯一已知的异常值。

clustering = DBSCAN(eps=20, min_samples=2).fit(df.values)
print(clustering.labels_)
print(pd.Series(clustering.labels_).value_counts())

DBSCAN 的参数设置可能需要一定的实验调整。在这个例子中,我调整了参数,直到算法识别出一个异常值,我通过打印 labels_ 属性确认它是最后一行。标签如下:

[ 0  1  1 ...  1  0 -1]

-1 表示未分配给任何聚类的记录。而且,value_counts()显示只有一条记录被分配给了聚类 -1。所以,DBSCAN 在这个例子中工作得很好。这意味着我们不能通过使用 SNN 来改进它,但这确实提供了一个清晰的使用 DBSCAN 进行异常值检测的例子,并确保数据集可以通过基于聚类的异常值检测解决。

为了使用 SNN 距离,必须首先计算成对的 SNN 距离(DBSCAN 本身无法计算这些)。一旦这些距离被创建,它们可以以 n x n 矩阵的形式传递给 DBSCAN。

在这里我们计算 SNN 的成对距离:

snn = SNN(metric='manhattan')
pairwise_dists = snn.get_pairwise_distances(df, k=100)
print(pairwise_dists)

成对距离如下所示:

array([[ 0.,  0.,  0., ...,  0., 57.,  0.],
       [ 0.,  0.,  0., ...,  0.,  0.,  0.],
       [ 0.,  0.,  0., ...,  0.,  0.,  0.],
       ...,
       [ 0.,  0.,  0., ...,  0.,  0.,  0.],
       [57.,  0.,  0., ...,  0.,  0.,  0.],
       [ 0.,  0.,  0., ...,  0.,  0.,  0.]])

作为一种快速简便的方法来反转这些距离(以便更适合 DBSCAN),我们调用:

d = pd.DataFrame(pairwise_dists).apply(lambda x: 1000-x)

在这里,1000 只是一个大于实际数据中任何值的数字。然后我们调用 DBSCAN,使用‘预计算’作为度量,并将成对距离传递给 fit()。

clustering = DBSCAN(eps=975, min_samples=2, metric='precomputed').fit(d.values)
print(clustering.labels_)
display(pd.Series(clustering.labels_).value_counts())

再次强调,这只识别出了一个异常值(只有一个记录被赋予了聚类 ID -1,并且这就是最后一行)。一般来说,DBSCAN 和其他接受“预计算”作为度量的工具可以与 SNN 距离一起使用,并可能产生更稳健的结果。

在 DBSCAN 的情况下,使用 SNN 距离可以很好地工作,因为异常值(在 DBSCAN 中称为噪声点)和内点通常几乎所有的链接都被打破,因此异常值最终不会被分配到任何聚类。一些异常值(尽管这些异常值不那么极端)会与其他记录有一些链接,但通常与这些记录几乎没有或只有很少的共享邻居,因此会得到很高的异常值分数(尽管不如那些没有链接的异常值高,这样的处理是合适的)。

这可能需要一些实验,在某些情况下,k 的值以及 DBSCAN 的参数需要进行调整,尽管这在异常值检测中并不算是异常——通常情况下,需要进行一些调优。

子空间异常值检测(SOD)

SNN 在异常值检测中的使用并不像理想的那样广泛,但有一个著名的检测器使用了它:SOD,它包含在PyOD库中。

SOD 是一种异常值检测器,专注于找到有用的子空间(特征的子集)来进行异常值检测,但在过程中确实使用 SNN,它在介绍 SOD 的论文中提出,SNN 提供了更可靠的距离计算。

SOD 的工作原理(类似于 KNN 和 LOF),是为每个点识别 k 个邻居的邻域,这些邻居在 SOD 中被称为参考集。参考集是通过 SNN 来找到的。因此,邻域的识别不是通过使用具有最小欧几里得距离的点,而是通过那些具有最多共享邻居的点。

作者们发现,这种方法不仅在高维度情况下具有鲁棒性,而且在有许多无关特征的情况下也能保持稳定:邻居的排名顺序通常仍然具有意义,因此即使在某些特定距离不可靠的情况下,最近邻的集合也能可靠地找到。

一旦我们为一个点获取了参考集,SOD 就会利用这个参考集来确定子空间,即解释参考集最大方差的特征集合。而一旦 SOD 识别出这些子空间,它会检查每个点到数据中心的距离,从而提供一个异常分数。

嵌入

SNN 的一个显著应用是嵌入(例如图像、视频、音频、文本、网络或其他模态数据的向量表示),这些数据往往具有非常高的维度。我们在《Python 中的异常检测》一书中对这个问题进行了更深入的讨论,但在此简要指出:针对数字表格数据(如隔离森林、本地异常因子、k 近邻等)的标准异常检测方法,实际上在嵌入数据上表现较差。主要原因似乎是维度过高,以及嵌入数据中存在许多与异常检测无关的维度。

还有其他一些成熟的嵌入式异常检测技术,例如基于自动编码器、变分自动编码器、生成对抗网络等方法的技术。除此之外,还可以将降维技术应用于嵌入式数据,以提高异常检测的效果。这些内容也会在本书中介绍,并且希望在未来的 Medium 文章中进一步探讨。同时,我目前正在研究使用欧几里得距离、余弦距离等标准度量之外的其他距离度量,包括 SNN。是否有用,目前还在研究中。

结论

类似于距离度量学习,共享最近邻在计算上会比标准的距离度量(如曼哈顿距离和欧几里得距离)更为昂贵,但在特征数量较多、密度变化较大以及(正如 SOD 作者所发现的)存在无关特征时,可以表现得更为鲁棒。

因此,在某些情况下,SNN 可以作为一种更优的距离度量,替代更标准的距离度量,并可能更适合用于异常检测。我们已经看到它可以作为 k 近邻异常检测和 DBSCAN 异常检测的距离度量(以及仅在使用 DBSCAN 进行聚类时的情况)。

事实上,SNN 可以与任何基于记录之间距离的异常检测方法结合使用。也就是说,它可以与任何基于距离、基于密度或基于聚类的异常检测器一起使用。

我们还指出,SNN 并不总是与其他距离度量相比表现优越。当考虑到分类数据、日期数据和文本列(以及我们在表格数据中可能遇到的其他类型特征)时,问题会变得更加复杂。但即使严格考虑数字数据,也完全可能有一些数据集,即使特征数量很大,简单的曼哈顿距离也比 SNN 更有效,而在其他情况下,SNN 则更为优越。行数、特征数量、特征的相关性、特征的分布、特征之间的关联、数据的聚类情况等等都是相关因素,通常很难提前预测出什么方法最有效。

SNN 只是解决高维度、变化密度和无关特征等问题的一种方法,但它是一个有用的工具,足够简单可以实现,并且通常值得进行实验。

这篇文章只是对 SNN 的一个介绍,未来的文章可能会进一步探讨 SNN,但一般来说,在确定用于异常值检测的距离度量(以及其他建模决策)时,最好的方法是使用一种叫做掺杂的技术(本文中有描述),我们通过创建与真实数据相似的数据,但对其进行修改,使其包含强烈但现实的异常值。通过这样做,我们可以尝试估算哪种方法在检测你可能遇到的异常值时最有效。

在这里,我们使用了一个合成数据的示例,这有助于描述在某些情况下,一种异常值检测方法比另一种方法更有效,并且这一过程非常有价值(例如,在这里我们发现,当调整密度并增加特征数量时,SNN 优于曼哈顿距离,但在密度一致且特征数量较少的情况下,两者表现都很好)。然而,使用合成数据,尽管它很重要,但只是理解不同方法在类似数据上效果更好的一个步骤。掺杂方法通常在此过程中会更有效,或者至少是这个过程的一部分。

此外,通常在异常值检测中有一个共识,那就是没有任何一个检测器能够可靠地识别你感兴趣的所有异常值。每个检测器都会检测到一种相对特定类型的异常值,而我们通常希望检测各种各样的异常值(事实上,我们经常只是希望识别出任何在统计上显著不同于正常值的情况——尤其是在首次检查数据集时)。

考虑到这一点,通常会使用多个检测器进行离群点检测,并将它们的结果组合成一个集成模型。一种增加集成模型多样性的方法是使用多种距离度量。例如,如果曼哈顿距离、欧几里得距离、SNN,甚至其他度量(如堪培拉距离、余弦相似度或其他度量)都能有效工作(它们产生不同但合理的结果),那么使用所有这些度量可能是值得的。然而,通常我们会发现,只有一两种距离度量在给定的数据集和我们关注的离群点类型下能产生有意义的结果。尽管不是唯一的,SNN 是一种有用的距离度量,尤其是在其他距离度量的检测器效果不佳时,值得尝试。

所有图片均由作者提供。

波涛汹涌:开源 LLMs 相较于闭源 LLMs 的竞争优势

原文:towardsdatascience.com/shifting-tides-the-competitive-edge-of-open-source-llms-over-closed-source-llms-aee76018b5c7?source=collection_archive---------5-----------------------#2024-04-29

为什么我认为较小的开源基础模型已经开始在生成型 AI 应用中取代像 OpenAI 这样的提供商的专有模型

Leonie MonigattiTowards Data Science Leonie Monigatti

·发表于Towards Data Science ·阅读时长 6 分钟·2024 年 4 月 29 日

--

图片由斋藤洋子提供,来源于Unsplash

自从 ChatGPT 的发布激发了开发者们利用大型语言模型(LLMs)构建应用的兴趣以来,尤其是 OpenAI 的专有闭源基础模型一直主导着市场。OpenAI 的基础模型 gpt-3.5-turbo,驱动了 ChatGPT,通常是编程教程中的默认 LLM。一项由 a16z 于 2023 年针对 70 位企业 AI 领导者进行的调查显示,约 80%的企业市场份额由闭源模型占据,其中大部分归 OpenAI 所有。

然而,较小的开源模型正在日益受到欢迎,并且很快可能取代更大的闭源模型。主要是因为开源 LLMs 的能力正在赶上。闭源模型的主要优势——性能,现在正迅速减弱。此外,上述调查的受访者还列出了其他因素,如数据安全性、可定制性和成本,这些都使得开源 LLMs 在企业中成为相对于闭源 LLMs 更具吸引力的选择。一些企业 AI 决策者…

简洁而甜美:通过约束性思维链提升 LLM 表现

原文:towardsdatascience.com/short-and-sweet-enhancing-llm-performance-with-constrained-chain-of-thought-c4479361d995?source=collection_archive---------4-----------------------#2024-08-07

|LLM|提示工程|COT|推理|

有时候几句话就足够了:缩短输出长度以提高准确性

Salvatore RaieliTowards Data Science 萨尔瓦托雷·雷耶利

·发布于 Towards Data Science ·9 分钟阅读·2024 年 8 月 7 日

--

由作者使用 AI 创作的图片

简洁是雄辩的魅力所在。——马库斯·图利乌斯·西塞罗

简洁和精炼是正确的根源。——霍西亚·巴卢

大语言模型(LLMs) 在推理领域展现了有趣的能力。随着其应用的推广,出现了一个新的应用领域: 提示工程。实际上,与这些模型的交互是通过使用提示进行的,因此,开发出了许多技术来提升 LLM 的这些能力。

[## 提升大语言模型表现的提示工程:借力上下文学习

如何修改文本提示,以便在不训练的情况下从 LLM 中获取最佳效果

pub.towardsai.net](https://pub.towardsai.net/prompt-engineering-to-leverage-in-context-learning-in-large-language-models-72296e1f09c3?source=post_page-----c4479361d995--------------------------------)

最具吸引力的技术之一是链式思维(CoT)提示;该技术提高了解题过程中的正确性,并解释了模型是如何得出解决方案的(或出现推理错误的原因)。

最短路径算法:如何利用数据进行导航和优化

原文:towardsdatascience.com/shortest-path-algorithms-how-to-use-data-to-navigate-and-optimize-746809d51e8f?source=collection_archive---------9-----------------------#2024-07-30

迪杰斯特拉算法与贝尔曼-福特算法概述

Ben ChambleeTowards Data Science Ben Chamblee

·发布于 Towards Data Science ·阅读时间:7 分钟·2024 年 7 月 30 日

--

图片来源:Unsplash+Unsplash

你有没有想过,为什么你的 GPS 总是能够找到最快的路线?无论从 A 点到 B 点有多少条路线,你的 GPS 会筛选出所有路径并给出一条路线——它是怎么知道哪条路线最好呢?在后台,使用像迪杰斯特拉算法这样的算法,正在进行大量的计算,以找出你所在的位置与目标地点之间的最短路径。然而,也有许多其他算法可以实现这一点,我想介绍其中的一些!在这篇文章中,我将介绍一种流行的最短路径算法以及一种更高级的算法,并展示你如何在数据项目中使用它们,或者仅仅为了好玩!

迪杰斯特拉算法

我可以写一篇关于迪杰斯特拉算法如何工作的总结,但我强烈推荐你先观看这段 Spanning Tree 的 YouTube 视频

如果你不想观看视频,下面是重点:

你有一组点,想要找出它们之间的最短路径。例如,如果你想从 S 点到 P 点,最短且唯一的路径是 2 分钟。然而,如果…

你应该成为数据科学家、数据分析师还是数据工程师?

原文:towardsdatascience.com/should-you-become-a-data-scientist-data-analyst-or-data-engineer-a9cd5c529650?source=collection_archive---------2-----------------------#2024-03-29

解释不同数据角色之间的区别和要求

Egor HowellTowards Data Science Egor Howell

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

--

图片由 Anthony Tori 提供,来自 Unsplash

许多技术行业内外的人对不同的数据角色以及它们的职能感到困惑。这可能使得想要进入该领域的人很难判断哪种工作适合他们的技能,并与他们的兴趣相符。因此,在本文中,我将详细解释数据科学家、数据分析师和数据工程师之间的区别。

一般数据流

下面的图表展示了公司内部的基本数据流。

这并不是普遍适用的,可能在不同的组织之间有所不同。

公司内部的数据流。图表由作者提供。

让我们来分析这些步骤:

  • 收集 — 这是关于从你可能需要的来源收集数据。为了获取数据,你需要进行日志记录和 API 调用……

作为数据科学家,你应该加入 FAANG 还是创业公司?

原文:towardsdatascience.com/should-you-join-faang-or-a-startup-as-a-data-scientist-030e3b8a7080?source=collection_archive---------2-----------------------#2024-06-20

来自 Uber 和 Meta 的工作经验教训:一个成长中的公司与一个微型创业公司

Torsten WalbaumTowards Data Science Torsten Walbaum

·发表于Towards Data Science ·阅读时间 9 分钟·2024 年 6 月 20 日

--

作者提供的图像(通过 Midjourney 制作)

你加入什么类型的公司是一个极其重要的决定。即使公司很有声望,薪水也很高,如果工作环境不适合你,最终你也会感到筋疲力尽。

很多人加入创业公司或大型科技公司时,并没有真正了解在那里工作的实际情况,最终往往会感到失望。在这篇文章中,我将根据我在从小型 10 人创业公司到 Uber 和 Meta 等大型科技公司工作的经验,讲述关键的区别。希望这能帮助你决定自己想要去哪个地方。

如果你想快速浏览文章,我会在每一部分的结尾添加简短的总结(“TL;DR” = “太长,不看”)(这是我在 Uber 学到的)。

因素#1:公司有多有声望

想象一下你知道的一家科技公司。你可能会想到 Google、Meta、Amazon、Apple 或类似的大公司。

根据这些公司的声誉,大多数人认为任何在那里工作的人都符合非常高的优秀标准。虽然这不一定正确(更多内容见下文),这种所谓的“光环效应”能对你有所帮助。一旦你的简历上有了大科技公司“批准印章”,之后找工作就会容易得多。

许多公司认为:“如果那个人足够优秀能在 Google 做数据科学家,他们对我们也会足够优秀。我确信 Google 已经做过充分的尽职调查。”

从德国来到美国时,大多数招聘经理和招聘人员并不认识我曾经工作过的公司。一旦我在 Uber 得到一份工作,我便收到了大量的工作邀请,包括一些之前曾经拒绝过我的公司。

你可能会觉得这不公平,但这是当前系统的运作方式,在选择工作公司时你应该考虑到这一点。

简而言之:在职业生涯早期为一家公司工作,可以为你打开许多机会的大门。

[## 每当 Torsten Walbaum 发布新文章时,接收邮件提醒。

每当 Torsten Walbaum 发布新文章时,接收邮件提醒。通过注册,如果你还没有 Medium 账户,将会创建一个账户...

medium.com](https://medium.com/@twalbaum/subscribe?source=post_page-----030e3b8a7080--------------------------------)

因素 #2:你同事的聪明程度

如前所述,人们常常认为 FAANG 公司只招聘最聪明、最优秀的人。

事实上,情况并非如此。多年来我学到的一件事是,世界上任何地方的人才和技能分布都会遵循正态分布,只要它达到一定的规模。虽然分布可能在 X 轴上稍微偏移,但仍然是正态分布。

图片来源:作者

许多知名公司最初非常挑剔,但随着它们的发展和招聘的增加,卓越程度开始回归到平均水平。

反直觉的是,这意味着一些小型初创公司可能拥有比大科技公司更精英的团队,因为它们能够精挑细选每一位新员工。当然,你需要在面试过程中亲自评估这些人的能力。

简而言之:你会在大公司和小公司中找到聪明的人;认为大科技公司雇佣的人才水平比初创公司高是一种误解。

因素 #3:你将赚多少钱

你的收入取决于许多因素,包括具体公司、你被提供的职位级别、你的谈判能力等。

需要记住的主要一点是:不仅仅是关于你赚多少,还要考虑你的薪酬有多不稳定和流动性如何。这受到薪酬组合(工资与股权(非流动的私人公司股票与流动的上市公司股票))以及公司阶段的影响。

你可以从一个高层次来思考:

  • 早期阶段: 小型初创公司会提供较低的基础工资,并试图通过承诺高额股权回报来弥补。但押注初创公司的股权回报就像在玩轮盘赌。你可能会一夜暴富,之后再也不需要工作,但你需要非常幸运;绝大多数初创公司都会失败,极少数能够成为独角兽公司。

  • 大科技公司: 另一方面,大型科技公司的薪酬更具可预测性。基本工资较高(例如,请参阅O'Reilly 2016 数据科学薪资调查),而且股权通常是流动的(即,你可以在股权归属后立即出售),并且波动较小。这是一个很大的优势,因为在上市前的公司,你可能需要等待数年才能使你的股权真正变得有价值。

  • 成长阶段: 成长阶段的公司可以是一个有趣的折衷方案;它们成功退出的机会要大得多,但你的股权仍然有很大的上升空间。如果你在多年内加入了 2-3 家顶级成长阶段公司,很有可能你会至少得到一次稳固的财务结果。这些公司中的薪酬可能非常有竞争力;当我从 Meta 转到 Rippling 时,我的薪酬实际上增加了。

简而言之:不要仅仅关注薪水,而是选择适合你风险承受能力和流动性需求的薪酬包。

因素 #4:你愿意承担多少风险

我们都希望拥有工作保障。

我们可能不会在一份工作中待一辈子,但至少我们希望能在离开时自己做出选择。

初创公司本质上比大公司更具风险。创始人是否能胜任工作?你能否筹集到下一轮融资?这些大多数风险都是生死存亡的;换句话说,你加入的公司阶段越早,它在 6 到 12 个月后可能就不再存在的可能性越大。

图片来自作者

在后期阶段的公司中,这些风险中的一些已经被消除或至少减少。

作为交换,你也在增加另一种风险:增加了裁员风险。初创公司只会招聘业务关键岗位,因为它们资金紧张。如果你被录用,你可以确信他们确实需要另一个数据科学家,而且有很多工作是对初创公司的成功至关重要的。

然而,在大公司中,招聘往往控制得不那么严格,因此你被聘用的职位可能后来被认为是“非核心”的,你可能会成为大规模裁员的一部分。

简而言之:公司阶段越早,你承担的风险就越大。但即使是大公司也不再“安全”(请参阅:裁员)

因素 #5:你能做的工作

初创公司和大公司的工作差异非常大。

一般的经验法则是,在早期阶段的公司中,你的工作范围会更广。例如,如果你作为初创公司中的第一个数据职位加入,你可能会兼任数据工程师、数据分析师和数据科学家的角色。你需要弄清楚如何构建数据基础设施,将数据提供给业务用户,定义指标,进行实验,构建仪表板等。

你的工作可能会涉及整个业务范围,因此你可能有一天在处理市场营销和销售数据,第二天则处理客户支持数据。

在大公司,你的工作范围会非常明确。例如,你可能会把大部分时间花在预测某一组指标上。

这里的权衡是广度与深度&规模:在初创公司,你的工作范围很广,但由于你需要身兼多职,你很难深入研究任何一个具体问题。在大公司,你的工作范围较窄,但你能在某一领域内培养深厚的专业知识;如果这个领域的专业知识需求很大,像这样专注于某一领域可以成为一条非常有利可图的道路。此外,你做的任何事情都会涉及数百万甚至数十亿的用户。

总结:如果你想要多样性,加入初创公司。如果你想建立深厚的专业知识并产生规模化影响,加入大科技公司。成长阶段的公司是一个不错的折中选择。

因素 #6:你将拥有的学习机会

当我在 2018 年加入 UberEats 时,并没有接受任何入职培训。相反,我被交给了一系列问题,并要求尽快开始工作。

如果你习惯于以结构化的方式学习,例如通过大学的讲座,这一切一开始可能会让你感到不适应。你怎么知道该怎么做呢?你从哪里开始呢?

但根据我的经验,解决各种具有挑战性的问题是学习公司运作方式、提高硬技能和软技能的最佳途径。例如,刚从学校毕业时,我的 SQL 技能最基础,但在 UberEats 的挑战中,我迫使自己在几周内就掌握了它。

这种做法的主要缺点是,你无法学到很多最佳实践。一个一流的数据基础设施是什么样的?最优秀的公司如何设计他们的指标?如何在保持严格要求的同时,毫不费力地执行成千上万的实验?即使你最终想加入一家初创公司,看到“优秀”的标准是什么样子,依然对你有帮助,这样你就知道自己应该朝哪个方向努力。

此外,大公司通常会提供正式的培训。与初创公司需要自己摸索不同,大型科技公司通常会提供赞助的学习和发展机会。

总结:在初创公司,你通过自己解决问题来学习;在大公司,你通过正式培训和吸收最佳实践来学习。

因素 #7:你将拥有的职业发展机会

我们已经讨论过,在寻找新工作时,加入知名公司可以提供帮助。那么,在公司内部,个人成长又如何呢?

在初创公司,你的成长机会直接来源于公司的成长。如果你作为早期的数据员工加入公司,并且你和公司都做得很好,你很可能会有机会建立并领导一个数据团队。

大多数你看到的年轻副总裁和 C 级高管之所以能走到今天,都是因为他们通过加入“火箭型”公司加速了自己的职业生涯。

然而,大公司有一个很大的好处:你通常有更广泛的职业选择。想要换一个产品做?不用离开公司,直接换团队。想要搬到另一个城市或国家?也有可能。

总结:早期阶段的高增长公司提供最大的成长机会(如果公司成功的话),而大公司则提供灵活性。

因素 #8:你会有多大压力

压力有很多种类型。弄清楚哪些是你能应对的,哪些是你无法接受的非常重要。

在快速增长的早期阶段公司,压力的主要来源是:

  • 优先级的变化: 为了生存,初创公司需要不断适应。原来的计划没有成功?那就尝试别的办法吧。因此,你很难计划超过几周的时间。

  • 快节奏: 早期阶段的公司需要快速行动;毕竟,他们需要在资金耗尽之前,展示足够的进展以筹集下一轮融资。

  • 广泛的范围: 如上所述,早期阶段的公司里每个人都做很多事情;很容易感到力不从心。我们大多数分析领域的人都喜欢做事情做到完美,但在初创公司,通常没有这个机会。如果现在做到足够好,就去做下一个事情吧!

在大公司,压力来自其他因素:

  • 复杂性: 大公司伴随着很多复杂性。常常是繁琐的技术栈、许多既定的流程、内部工具等等,你需要理解并学会如何利用它们。这可能会让人感到不知所措。

  • 政治: 在大公司,有时你会觉得自己花更多的时间和其他团队辩论泳道(工作分工),而不是实际工作。

总结:并非所有压力都是一样的。你需要弄清楚你能承受哪种类型的压力,并根据这些选择适合的公司。

什么时候应该加入大公司,而不是初创公司?

对这个问题没有一刀切的答案。然而,我个人认为,若有可能,在职业生涯初期至少在一家知名的大型科技公司工作一段时间会有所帮助。

这样,你将能够:

  • 在简历上增加声望,帮助你获得未来的工作机会

  • 看到高效运作的大型数据基础设施和分析组织是怎样的

  • 获取结构化的入职培训、辅导和发展

无论你是想留在大科技公司,还是跳入疯狂的初创世界,这都会为你提供坚实的基础。

最后思考

在小型初创公司、成长型公司或 FAANG 科技公司工作,并不本质上比其他更好或更差。每个公司阶段都有其优缺点;你需要自己决定你看重什么,什么样的环境最适合你。

欲了解更多关于如何在数据与分析领域扩展职业生涯的实用建议,考虑在这里关注我,或在LinkedIn上关注我,或在Substack上关注我。

你是否应该在未来十年学习如何编程?

原文:towardsdatascience.com/should-you-learn-how-to-code-in-the-next-decade-5ed58206291e?source=collection_archive---------1-----------------------#2024-11-02

还是 AI 会吞噬全世界的软件?

Ivo BernardoTowards Data Science Ivo Bernardo

·发表于《数据科学之路》 ·阅读时间:6 分钟·2024 年 11 月 2 日

--

图片来自steinart @unsplash.com

如今,许多人面临着一个两难的问题:如果你还年轻,是不是应该去攻读软件工程学位?如果你已经在其他领域有所成就,是否应该转行做一些涉及编程的工作?这些问题源于一个更大的问题:在大型语言模型(LLMs)如此兴奋的背景下,学习编程是否真的值得?

最近谷歌的 CEO 表示公司 25%的代码是由 AI 编写的。我们是不是看到了编程的死亡,至少是我们所知道的那种编程?

而这些问题不仅仅是进入这一领域的人提出的。许多以编程为职业的人也在问自己这个问题:他们是否应该继续投入大量时间提升自己的编程能力?

对我来说,简短的回答是:编程仍然会有其重要性——但可能不是你想的那个原因。因为我认为不可否认的是,未来十年,编程相关的工作将会发生很大变化。

在这篇文章中,我们将探讨关于编程未来的一些预测,以及学习编程语言的一些理由。通过这篇文章,我希望为你提供一种全新的视角,解释为什么

你应该从 VSCode 切换到 Cursor 吗?

原文:towardsdatascience.com/should-you-switch-from-vscode-to-cursor-45b1a0320d07?source=collection_archive---------2-----------------------#2024-12-03

作为一名数据科学家,我使用 VSCode(GitHub Copilot)和 Cursor(Claude 3.5 Sonnet)的经验。

Marc MattersonTowards Data Science Marc Matterson

·发表于Towards Data Science ·阅读时长 7 分钟·2024 年 12 月 3 日

--

这张图是使用 FLUX.1(由 Black Forest Labs 提供,通过 Grok 2 生成)人工生成的。

介绍

作为开发者,我们不断寻找可以提高生产力并让编码变得更加愉快的工具。我使用 Visual Studio Code(VSCode)已经超过六年,它一直是几乎所有我合作过的开发者的首选集成开发环境(IDE)。

2023 年,Cursor(由 Anysphere 研究实验室设计)从 OpenAI 筹集了$8M的资金,总融资额达到了 1100 万美元,他们传递出了这样的信息:

在接下来的几年里,我们希望打造一个比世界上任何时候都更加有帮助、愉快和有趣的代码编辑器。

在 2024 年,许多开发者开始测试或完全转向将 Cursor 作为首选的主要 IDE。

本文的目的是从第一人称的角度,分享我使用 Cursor 的经验,并与 VSCode 进行比较。具体来说,我将讨论在使用各自的 AI 辅助功能时,每个 IDE 在数据科学领域的表现如何。

什么是 Cursor?

通过 GitHub Actions 实现简单的模型重训练自动化

原文:towardsdatascience.com/simple-model-retraining-automation-via-github-actions-b0f61d5c869c?source=collection_archive---------3-----------------------#2024-06-15

通过 GitHub Actions,轻松简化您的建模过程。

Cornellius Yudha WijayaTowards Data Science Cornellius Yudha Wijaya

·发表于Towards Data Science ·阅读时间:11 分钟·2024 年 6 月 15 日

--

Praveen Thirumurugan拍摄,来源于Unsplash

机器学习模型可以为企业创造巨大的价值。然而,开发它们并不是一次性的活动。相反,它是一个持续的过程,旨在让模型不断提供价值。这就是 MLOps 的来源。

CI/CD 原则与机器学习开发的结合,就是我们所说的 MLOps,它旨在通过模型持续提供价值。

机器学习模型能够带来持续利益的一个方法是,当检测到数据漂移时,通过重训练来实现。我们可以通过设置重训练触发器环境来执行模型重训练自动化。

我们可以使用名为GitHub Actions的 GitHub 工具来简化重训练过程。这个工具是 GitHub 为 CI/CD 平台提供的功能,用于自动化软件开发过程,从 GitHub 仓库中进行操作。

本文将教我们如何通过 GitHub Actions 进行模型重训练自动化控制。如何实现呢?让我们一起了解。

准备工作

我们将为这个项目进行一次简单的模型开发和自动化演示。整体…

加速你的 PyTorch 模型训练的简单方法

原文:towardsdatascience.com/simple-ways-to-speed-up-your-pytorch-model-training-9c9d4899313d?source=collection_archive---------3-----------------------#2024-05-28

如果所有机器学习工程师都希望得到一个东西,那就是 更快的模型训练 —— 也许在获得良好的测试指标之后。

Alex DremovTowards Data Science Alex Dremov

·发布于 Towards Data Science ·11 分钟阅读 ·2024 年 5 月 28 日

--

图片来源:Julian Hochgesang / Unsplash

这个话题甚至需要介绍吗?

加速机器学习模型训练是所有机器学习工程师的目标。更快的训练意味着更快的实验,也意味着更快的产品迭代。此外,这还意味着一次模型训练将需要更少的资源。所以,直接进入正题

容器化

是的,单靠这个不会加速你的训练。但它针对的是另一个重要方面——可重现性。有时候,使用固定库版本的 virtualenv 就足够了,但我鼓励你更进一步,为你的模型训练构建一个一体化的 Docker 容器。

这确保了在调试、分析和最终训练过程中,环境的一致性。你最不希望的事情就是优化一个部分代码,而这个部分由于 Python12 提升了速度,已经不再是瓶颈了。例如,甚至有一个错误在不同的 CUDA 版本下无法重现。

作为起点,你可以使用 NVIDIA 提供的预构建镜像。这些镜像已经安装了 CUDA、PyTorch 和其他流行的库:

[## PyTorch | NVIDIA NGC

PyTorch 是一个 GPU 加速的张量计算框架。功能可以通过常见的 Python 库进行扩展……

catalog.ngc.nvidia.com](https://catalog.ngc.nvidia.com/orgs/nvidia/containers/pytorch?ref=alexdremov.me&source=post_page-----9c9d4899313d--------------------------------)

💡 Docker 容器是解决此类问题的终极方案

“嘿,它在我的机器上可以工作,我不知道为什么在你的机器上不行。”

熟悉 PyTorch 分析器

在优化任何东西之前,你必须了解你的一些代码部分运行了多久。PyTorch 分析器几乎是一个功能齐全的训练分析工具。它能够记录:

  • CPU 操作的时间

  • CUDA 内核的时间

  • 内存消耗历史

这就是你所需要的一切。而且它很容易启用!

要记录事件,你只需要像这样将训练嵌入分析器上下文中:

import torch.autograd.profiler as profiler

with profiler.profile(
  activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA],
  on_trace_ready=torch.profiler.tensorboard_trace_handler('./logs'),
) as prof:
  train(args)

之后,你可以启动 tensorboard 并查看分析轨迹。别忘了安装 torch-tb-profiler

[## PyTorch Profiler 与 TensorBoard - PyTorch 教程 2.3.0+cu121 文档]

准备数据和模型 使用分析器记录执行事件 运行分析器 使用 TensorBoard 查看结果并…

pytorch.org](https://pytorch.org/tutorials/intermediate/tensorboard_profiler_tutorial.html?ref=alexdremov.me&source=post_page-----9c9d4899313d--------------------------------)

分析器有许多不同的选项,但最重要的是 activitiesprofile_memory。你可以尝试其他选项,但请记住一个简单的规则:你启用的选项越少,开销就越小

所以,如果你想分析 CUDA 内核执行的时间,最好关闭 CPU 分析和其他所有功能。在这种模式下,分析将尽可能接近真实执行。

为了让轨迹更容易理解,考虑添加描述你代码核心部分的分析上下文。如果没有启用分析,这些上下文将无效。

with profiler.record_function("forward_pass"):
  result = model(**batch)

with profiler.record_function("train_step"):
  step(**result)

这样,你使用的标签将在轨迹中可见。这样,识别代码块会更加容易。甚至在模式的 forward 中进行更细粒度的分析:

with profiler.record_function("transformer_layer:self_attention"):
  data = self.self_attention(**data)

...

with profiler.record_function("transformer_layer:encoder_attention"):
  data = self.encoder_attention(**data, **encoder_data)

理解 PyTorch 轨迹

收集轨迹后,在 tensorboard 中打开它们。这就是 CPU + CUDA 分析的样子:

© 版权 2024,PyTorch | pytorch.org/tutorials/intermediate/tensorboard_profiler_tutorial.html

立刻找到任何训练的核心部分:

  • 数据加载

  • 前向传播

  • 反向传播

反向传播由 PyTorch 在单独的线程中处理(如上图中的线程 16893),所以很容易识别。

数据加载

对于数据加载,我们希望接近零的时间。

没有妥协。

这是因为在数据加载期间 GPU 不做任何事情,这会导致可用资源的低效利用。然而,数据处理可以与 GPU 计算重叠,因为它们是独立的部分。

你可以轻松地识别 GPU 闲置的区域——只需查看分析器跟踪中的 GPU 估算 SM 效率GPU 利用率 数值。没有活动的区域就是我们的“患者”。这就是 GPU 什么也不做的地方。

一个简单的解决方案是:

  • 在后台进程中处理数据(没有 GIL)

  • 在并行进程中处理数据增强和变换

如果你使用 PyTorch 的 DataLoader,那么可以通过指定 num_workers 来轻松实现这一点。如果你使用 IterableDataset,情况会更复杂,因为数据将会重复。但这个问题仍然可以通过使用 get_worker_info() 来解决——你需要调整迭代方式,以确保每个工作进程接收不同且不重叠的行。

对于更可配置的处理,你可以考虑使用 multiprocessing 自行实现多进程变换

💡 如果你从未检查过代码的数据处理速度,那么这个小小的修改可能会带来 剧烈的加速

与内存分配器成为朋友

你希望与 PyTorch 的 CUDA 缓存分配器成为朋友。

当你在 CUDA 设备上使用 PyTorch 分配张量时,PyTorch 会使用一个缓存分配器。这是因为 cudaMalloc/ cudaFree 是昂贵的操作,我们希望避免调用它们,因此 PyTorch 有自己的分配器,它会尝试重用通过 cudaMalloc 分配的块。也就是说,如果 PyTorch 的分配器有合适的块可用,它会直接提供,而不需要调用 cudaMalloc。这样,cudaMalloc 只会在一开始调用。

然而,如果你处理的是长度可变的数据,不同的前向传递将需要不同大小的中间张量。因此,PyTorch 的分配器可能没有合适的数据块可用。在这种情况下,分配器会崩溃并通过调用 cudaFree 释放之前分配的块,以为新的分配腾出空间。

之后,分配器开始重新构建它的缓存,进行大量的 cudaMalloc,这是一项昂贵的操作。你可以通过查看张量板分析器视图的内存分析部分来发现这个问题。

💡 你也可以在跟踪记录中发现这个问题。它将以对 cudaMalloccudaFree 的调用形式显示出来

PyTorch 分配器崩溃 | 图片来自作者

如你所见,与分配器保留的内存对应的红线不断变化。这意味着 PyTorch 的分配器无法有效地处理分配请求。

当分配操作不再让分配器崩溃时,红线完全是直的

PyTorch 分配器按预期工作 | 图片来自作者

如我所说,这通常是由于张量的形状不固定。如何修复这个问题?

可扩展的段

第一件值得尝试的事情是设置 PyTorch 相对较新的分配器模式:

PYTORCH_CUDA_ALLOC_CONF="expandable_segments:True"

如果设置为 *True*,此设置指示分配器创建可以在以后扩展的 CUDA 分配,以更好地处理工作负载频繁更改分配大小的情况,例如批量大小变化的情况。

所以,这告诉 PyTorch 分配器分配将来可能会扩展的块,这正是我们的情况。尽管如此,如果大小变化过大,仍然可能无法解决问题。在这种情况下,转到下一个选项。

使分配的变化更少

另一种可能的解决方案是使数据形状保持一致。这样,分配器将更容易找到一个适合重用的数据块。

为了实现这一点,你可以将数据填充到相同的大小。或者你可以通过运行一个具有最大输入大小的模型来预热分配器。

你可以在以下文章中了解更多关于 PyTorch 分配器修改的信息。

[## CUDA 语义 - PyTorch 2.3 文档

torch.cuda 的指南,PyTorch 模块用于执行 CUDA 操作

pytorch.org](https://pytorch.org/docs/stable/notes/cuda.html?ref=alexdremov.me&source=post_page-----9c9d4899313d--------------------------------)

整理分配历史

我们希望使用所有可用的 GPU 内存——这使我们能够运行更大的批次并更快地处理数据。然而,在某个时刻,当增加批量大小时,你将遇到CUDA 内存不足错误。是什么导致了这个错误?

为了调试这个问题,我们可以查看分配器的内存历史。它可以通过 PyTorch 记录,然后在pytorch.org/memory_viz上进行可视化。

  • 开始: torch.cuda.memory._record_memory_history(max_entries=100000)

  • 保存: torch.cuda.memory._dump_snapshot(file_name)

  • 停止: torch.cuda.memory._record_memory_history(enabled=None)

可视化将显示如下内容:

© 版权 2024, PyTorch | pytorch.org/blog/understanding-gpu-memory-1/

x 轴表示时间,y 轴表示总使用内存,彩色块表示张量。因此,它显示了张量何时被分配以及何时被释放。

你可能会注意到窄尖峰——这些是占用大量空间的短暂张量。通过点击张量,你可以获得该张量分配的位置。我们希望尽量减少这些尖峰,因为它们限制了内存的高效使用。检查一下是什么导致了这个尖峰,并考虑其他计算方式。

除了尖峰外,检测内存泄漏也很容易:

© 版权 2024, PyTorch | pytorch.org/blog/understanding-gpu-memory-1/

正如你所看到的,第一次前向传播后一些数据并没有被清除。通过点击这些模块,你可以大致了解这些张量来自何处。图中显示的是训练步骤后梯度未被清除的情况,这导致它们在前向传播过程中仍然存在,限制了增加批量大小以适应更多数据的能力。

[## 理解 GPU 内存 1:可视化所有分配随时间变化

在你使用 PyTorch 进行 GPU 计算的过程中,你可能已经熟悉这个常见的错误信息:

pytorch.org

加速模型并减少内存使用

有什么比这更好的呢?我们可以通过使用FlashAttention内核来计算点积注意力,从而实现这一目标。

[## GitHub - Dao-AILab/flash-attention:快速且内存高效的精确注意力

快速且内存高效的精确注意力。通过创建一个账户,为 Dao-AILab/flash-attention 的开发做出贡献…

github.com

如果你还没有听说过,它是一种计算精确点积注意力的方法,无需显式构造注意力矩阵。它优化了 GPU 的输入输出操作,从而提高了速度,并且大幅度减少了内存消耗。简直没有理由不使用它。

😡 不幸的是,有一个理由不能使用它——硬件。

Flash attention 仅在兼容硬件上使用fp16bf16精度。这包括 NVIDIA Ampere、Hooper 等架构。

其他库在底层使用了 flash attention,因此你可以考虑使用其他更适合你代码库的变种。

XFormers

[## GitHub - facebookresearch/xformers:可操作且优化过的 Transformer 构建模块,支持…

可操作且优化过的 Transformer 构建模块,支持可组合构建。 - facebookresearch/xformers

github.com

Transformer Engine

[## GitHub - NVIDIA/TransformerEngine:一个加速 Transformer 模型在 NVIDIA GPU 上的运行的库…

一个加速 Transformer 模型在 NVIDIA GPU 上运行的库,包括使用 8 位浮点(FP8)精度…

github.com

PyTorch 本身!

这是事实,PyTorch 的新版本可能会在适用的情况下使用闪电注意力(flash attention)。要激活此模式,你需要在上下文管理器中执行注意力块,指定使用哪种注意力策略:

## torch.nn.functional.scaled_dot_product_attention - PyTorch 2.3 文档

阅读 PyTorch Domains 文档,了解更多关于特定领域的库

pytorch.org

优化多 GPU 数据冗余 — FSDP

如果你使用多个 GPU 进行训练,基本的解决方案是使用 DistributedDataParallel 类。这样,多个相同的进程将被启动,并且在反向传播步骤中会聚合梯度。

然而,这并不是最优的!

问题在于,当我们启动相同的进程时,每个 GPU 上都有相同的模型和优化器状态,这造成了冗余。解决方案是将数据分片。我们可以使用完全分片数据并行的 PyTorch 封装来实现这一点。

© 版权 2024,PyTorch | https://pytorch.org/tutorials/intermediate/FSDP_tutorial.html

它是如何工作的?

正如我所说,当在多个 GPU 上训练时,使用 DDP 时每个进程都有相同数据的副本。我们可以通过实现几个增强功能来优化这一过程:

分片优化器状态(ZeRO 1)

在使用 DDP 训练时,每个进程持有优化器状态的完整副本。而使用 ZeRO1 时,我们将这些优化器状态在所有的 rank 之间进行分片,使得每个 rank 只持有优化器状态的一部分。在反向传播过程中,每个 rank 只需要收集与其参数相关的优化器状态来进行优化步骤。这种减少冗余的方式有助于节省内存。

💡 在 Adam 中,由于其参数大约是模型大小的两倍,将优化器状态分片到 8 个 rank 中意味着每个 rank 仅存储总状态大小的四分之一(2/8)。

分片梯度(ZeRO 2)

我们对优化器状态进行分片。现在,我们将修改优化器步骤,以便也对梯度进行分片。如果一个 rank 拥有部分参数的优化器状态,那么我们将:

  • 聚合与该 rank 持有状态相关的所有梯度

  • 计算优化步骤

  • 将部分参数的优化步骤发送到所有其他 rank

正如你所注意到的,现在每个 rank 不再需要持有完整的梯度副本。我们可以在梯度可用时立即将其发送到相关的 rank。因此,我们可以进一步减少峰值内存消耗。

分片模型参数(ZeRO 3)

这将是史诗级的。

为什么我们需要在每个 rank 上存储模型的完整副本?让我们在所有 rank 之间分片模型参数。然后,我们将在前向和反向传播过程中按需即时获取所需的参数。

💡 对于大模型,这些优化可以显著减少内存消耗。

如何使用 FSDP?

其实非常简单。我们只需要用 FSDP 包装模型:

import torch
import torch.nn as nn
import torch.optim as optim
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP

model = FSDP(model)

# it's critical to get parameters from the wrapped model
# as only a portion of them returned (sharded part)
optimizer = optim.Adam(model.parameters())

# consuct training as usual
train(model, optimizer)

你还可以指定 FSDP 的分片策略。例如,我们可以选择SHARD_GRAD_OP策略,以实现类似 ZeRO2 的行为。你可以在这里了解其他策略:

## FullyShardedDataParallel - PyTorch 2.3 文档

用于在数据并行工作者之间分配分片模块参数的包装器。这个灵感来自于 Xu 等人以及……

pytorch.org

此外,你还可以使用 FSDP 包装子模块。在上面的示例中,只使用了一个 FSDP 模块,这样会降低计算效率和内存效率。它的工作原理是,假设你的模型包含 100 个 Linear 层。如果你执行 FSDP(model),那么将只有一个 FSDP 单元包装整个模型。在这种情况下,allgather 会收集所有 100 个 Linear 层的完整参数,因此不会为参数分片节省 CUDA 内存。

你可以显式地包装子模块或定义自动包装策略。要了解更多关于 FSDP 的信息,请阅读 PyTorch 指南:

[## 入门:完全分片数据并行(FSDP) - PyTorch 教程 2.3.0+cu121…

注意:在 GitHub 上查看和编辑本教程。大规模训练 AI 模型是一项具有挑战性的任务,要求……

pytorch.org

使用torch.compile的神奇加速

也就是说,torch compile 只需启用它,就可以让你的代码加速几个百分点。

Torch 会跟踪你的执行图,并尝试将其编译为高效的格式,以便模型几乎可以在没有 Python 调用的情况下执行。

基本使用方法是将模型与 compile 一起包装:

import torch

model = torch.compile(model)

这几乎会立即执行。实际的跟踪只会在第一次前向传播时发生。

它还具有许多值得尝试的选项:

## torch.compile - PyTorch 2.3 文档

使用 TorchDynamo 和指定的后端优化给定的模型/函数。具体来说,对于在……

pytorch.org

💡 Torch 编译器是一个重要特性,将在后续的帖子中讲解!

敬请期待

在这里了解更多关于 torch compile 的信息:

[## Introduction to torch.compile - PyTorch Tutorials 2.3.0+cu121 documentation

torch.compile 已包含在最新的 PyTorch 版本中。在 GPU 上运行 TorchInductor 需要 Triton,而 Triton 已包含在…

pytorch.org](https://pytorch.org/tutorials/intermediate/torch_compile_tutorial.html?ref=alexdremov.me&source=post_page-----9c9d4899313d--------------------------------)

结论

本文并非包含所有解释,而是提供了值得立即尝试的加速方法清单。希望对你有所帮助,欢迎留言评论!

考虑订阅

原文发表于 https://alexdremov.me 于 2024 年 5 月 28 日。

图片来源于 PyTorch 博客,这是 Linux 基金会的一个项目,受 Linux 基金会的 政策约束。所以,所有图片均可按照 创意共享 3.0 许可协议 使用。*

简化信息提取:GPT 模型的可重用提示模板

原文:towardsdatascience.com/simplify-information-extraction-a-reusable-prompt-template-for-gpt-models-d6d5f1bd25a0?source=collection_archive---------5-----------------------#2024-08-15

一个包含对我在超过十几个微妙的医学信息提取任务上有效的提示技术的提示模板

Christabelle PabalanTowards Data Science Christabelle Pabalan

·发表在Towards Data Science ·8 分钟阅读·2024 年 8 月 15 日

--

由 DALL·E 生成的图像

介绍

如果我告诉你,我为信息提取任务创建了终极提示模板,它将保证您每次都能获得精确的期望性能,具有令人难以置信的回忆和精确度,并且每次都能保证输出格式化,您可能会嗤之以鼻。

而且理所当然 — 因为在 LLMs 的不可预测性质下,没有人能保证这些复选框。-**融化的脸部表情-

然而,这就是我能说的:在对超过十几个微妙的医学信息提取任务进行了广泛工作之后 — 每个任务都需要深入的领域专业知识 — 我开发了一个使用了对我有效的提示技术的提示模板,显著提升了性能并最小化了错误输出。这个模板帮助我简化了工作流程,减少了迭代周期,并为我的结果带来了可靠的一致性水平。

在本文中,我将介绍这个模板,解释每个部分背后的原理,并分享我在这个过程中学到的经验。我希望是…

简化 Python 代码以应对数据工程项目

原文:towardsdatascience.com/simplifying-the-python-code-for-data-engineering-projects-95f0c41dc58a?source=collection_archive---------1-----------------------#2024-06-12

数据摄取、验证、处理和测试的 Python 技巧与技术:实用操作指南

John LeungTowards Data Science John Leung

·发布于 Towards Data Science ·阅读时间 10 分钟·2024 年 6 月 12 日

--

原始数据来自不同的来源和格式。在数据能够用来回答关键的商业问题之前,需要大量的努力和时间来进行数据工程。虽然基础的数据基础设施可能根据数据的量、速度和分析需求而有所不同,但一些基本的代码设计技巧仍然是相关的,可以简化和优化各种任务。

本文将探讨一般数据工程项目中的不同关键部分,从数据摄取到管道测试。Python 是数据工程中最广泛使用的编程语言,我们将学习如何使用 Python 中的内置功能和高效库来处理这些用例。

图片来源:Katerina PavlyuchkovaUnsplash

想象一下你拥有一家在线零售店,销售独特的各类节庆礼品。这个在线商店非常受欢迎,每分钟每秒的交易量都非常大。你希望通过分析当前交易的购买习惯,满足更多现有客户的需求,并服务更多的新客户,这促使你开始深入数据处理,准备交易记录。

#0 模拟数据

我们首先使用JSON Lines(JSONL)文本格式将一些交易数据模拟到文件中,其中每一行都是一个独立的 JSON 对象。这种格式在数据流处理领域非常有吸引力,例如网页/应用分析和日志管理。

在我们的文件中,数据字段属于不同的数据类型。它们包括客户和产品标识符(以整数/数组格式),支付方式(以字符串格式),以及交易总金额(以浮动数字格式)。

import json
import random
import numpy as np
import datetime

# Remove existing 'retail_transactions.jsonl' file, if any
! rm -f /p/a/t/h retail_transactions.jsonl

# Set the no of transactions
no_of_iteration = 500000

# Open a file in write mode
with open('retail_transactions.jsonl', 'w') as f:
  for num in range(no_of_iteration):
    if (random.randint(1, 10000) != 5000):
      # Create a valid transaction
      new_txn = {
        'orderID': num,
        'customerID': random.randint(1, 100000),
        'productID': np.random.randint(10000, size=random.randint(1, 5)).tolist(),
        'paymentMthd': random.choice(['Credit card', 'Debit card', 'Digital wallet', 'Cash on delivery', 'Cryptocurrency']),
        'totalAmt': round(random.random() * 5000, 2),
        'invoiceTime': datetime.datetime.now().isoformat()
      }
    else:
      # Create an invalid transaction
      new_txn = {
        'orderID': "",
        'customerID': "",
        'productID': "",
        'paymentMthd': "",
        'totalAmt': "",
        'invoiceTime': ""
       }

     # Write the transaciton as a JSON line to the file
     f.write(json.dumps(new_txn) + "\n")

你可能会发现文件中有几条单独的交易数据,其中一些数据字段为空。这模拟了缺失数据的问题,这是现实世界中常见的数据质量问题之一。

#1 数据摄取 — Yield

读取文件中的交易记录,最简单的方法之一是将数据集遍历到一个列表中,然后将其转换为 Pandas DataFrame。

这个方法对于我们演示数据集中的 500,000 条交易非常有效。但如果现实世界中的数据集有数百万甚至数十亿行数据呢?如果不导致内存问题,我们可能需要长时间等待整个计算完成。

有时候,我们不关心整个结果,而是希望在加载最后一条记录之前先处理初步结果。在这种情况下,我们可以使用yield来控制生成器的流向。

生成器不会将整个记录存储在内存中。相反,它一次只返回一个值,并在请求下一个值之前暂停函数执行。

在用户代码和库代码交替执行的过程中,还会强制顺序执行,这意味着你不能在到达第一条记录之前访问第二条记录。你可以在Pydata 讲座视频中了解更多关于这个概念的详细解释。

yield语句有不同的实际用途。例如,我们可以遍历文件中的每一行,只返回非空记录。下面展示了如何执行实时数据过滤:

import json

def read_json_file(file_name):
  # Read the JSONL file
  with open(file_name) as f:
    for line in f:
      txn = json.loads(line)
      # Yield valid transactions only
      if (txn['orderID'] != ""):
        yield(txn)

txn_generator = read_json_file('retail_transactions.jsonl')

这些代码的输出是一个 Python 生成器,一种特殊类型的迭代器。你可以在循环中使用next函数来逐个返回后续项目。除了实时数据过滤,另一个思路是设计一个生成器函数,预处理数据并以预定义的批量大小进行生成,这可以直接解析并供机器学习模型训练使用。而且,我们还可以用它来异步处理网页请求和响应,进行网页爬取。

#2 数据验证 — Pydantic

假设你有一个包含交易记录信息的 JSON 数据列表,这是数据摄取后的一个示例交易:

{
 'orderID': 10000,
 'customerID': 48316,
 'productID': [5620],
 'paymentMthd': 'Cash on delivery',
 'totalAmt': 9301.2,
 'invoiceTime': '2024-06-10T23:30:29.608443',
 'price': -1
}

对于每一条传入的数据,我们希望确保它经过验证,否则在运行后续的数据处理函数时,我们很容易遇到各种错误。这可以通过使用pydantic库来实现。

我们首先使用PyDantic 模型定义数据字段的模式,然后使用model_validate()函数验证我们的 JSON 数据。

from datetime import datetime
from pydantic import BaseModel, ValidationError

# Define the data model for a transaction record
class TxnModel(BaseModel):
  orderID: int
  customerID: int
  productID: list[int]
  paymentMthd: str
  totalAmt: float
  invoiceTime: datetime

try:
  # Validate the sample case against the schema
  TxnModel.model_validate(sample_txn)
  print("Validated successfully!")
except ValidationError as exc:
  # Print error messages for any validation error
  print("Validation Error:")
  print(exc.errors())

# Output:
# Validated successfully

有时,我们发现需要应用更严格的验证规则。例如,Pydantic 基础模型会尝试将字符串数据强制转换为整数。为避免这种情况,可以在模型级别或字段级别设置strict=True

此外,我们还可以对数据字段应用自定义验证规则。例如,我们可能希望检查支付方式值是否符合我们的预期。为了方便测试,我们手动将示例案例的支付方式设置为“Bitcoin”,这是在线商店中不存在的选项,然后使用AfterValidator嵌入一个函数进行进一步检查。

from typing import Annotated
from pydantic.functional_validators import AfterValidator

# Customize the validation rule
def validate_payment_mthd(paymentMthd: str):
  possible_values = ['Credit card', 'Debit card', 'Digital wallet', 'Cash on delivery', 'Cryptocurrency']
  if paymentMthd not in possible_values:
    raise ValueError(f"Invalid paymentMthd, payment type must be one of {possible_values}")
  return storage

# Define the data model for a transaction record
class TxnModel(BaseModel):
  orderID: int = Field(strict=True)
  customerID: int
  productID: list[int]
  paymentMthd: Annotated[str, AfterValidator(validate_payment_mthd)]
  totalAmt: Annotated[float, Field(strict=True, gt=0)]
  invoiceTime: datetime

# Manually define a non-existent payment method
sample_txn['paymentMthd'] = 'Bitcoin'

try:
  # Validate the sample case against the schema
  TxnModel.model_validate(sample_txn)
  print("Validated successfully!")
except ValidationError as exc:
  # Print error messages for any validation error
  print("Validation Error:")
  print(exc.errors()[0]['ctx'])

# Output
# Validation Error:
# {'error': ValueError("Invalid paymentMthd, payment type must be one of ['Credit card', 'Debit card', 'Digital wallet', 'Cash on delivery', 'Cryptocurrency']")}

验证器成功识别到支付方式不在可能值的列表中。这是通过应用 Pydantic 的内部验证逻辑,并随后使用自定义验证函数完成的。代码会引发一个ValueError,并填充ValidationError

当触发错误时,我们可以采取后续行动进行纠正。这些功能有助于消除数据错误,从而确保数据的准确性和完整性。

#3 数据处理

(1) Python 装饰器

数据验证后,我们开始处理数据密集型函数。随着数据管道的复杂化,执行时间可能会变长。我们希望找出根本原因,并优化函数的时间性能。一种简单的方法是,在每个函数的开始和结束时收集两个时间戳,然后逐一计算时间差。

为了确保数据管道中的代码更简洁,我们可以利用Python 装饰器

我们首先设计一个 Python 装饰器来测量执行时间。之后,我们为任何需要此功能的函数添加注解。

例如,您可以测量对所有交易进行分类所需的时间。

import time

# Measure the excution time of a given function
def time_decorator(func):
  def wrapper(*args, **kwargs):
    begin_time = time.time()
    output = func(*args, **kwargs)
    end_time = time.time()
    print(f"Execution time of function {func.__name__}: {round(end_time - begin_time, 2)} seconds.")
    return output
  return wrapper

# Categorize the total amount of each transaction
@time_decorator
def group_txn_price(data):
  for txn in data:
    price = txn['totalAmt']
    if 0 <= price <= 1500:
      txn['totalAmtCat'] = 'Low'
    elif 1500 < price <= 3500:
      txn['totalAmtCat'] = 'Moderate'
    elif 3500 < price:
      txn['totalAmtCat'] = 'High'
    return data

txn_list = group_txn_price(txn_list)

# Output
# Execution time of function group_txn_price: 0.26 seconds.

装饰器方法使得代码在不改变原始函数源代码的情况下可以复用。类似地,我们可以应用装饰器的思想,用于记录函数完成情况或在任务失败时发送邮件警报。

(2) Map、reduce、filter

这些是常用的 Python 数组方法,许多开发者可能都很熟悉。但我仍然认为它们值得提及,原因有几点:(1)不可变性——这些函数不会修改原始列表的值;(2)链式灵活性——可以同时应用多个函数的组合;(3)简洁可读——只需一行代码。

假设我们有一个包含两个键的 JSON 对象列表:支付方式和总金额。让我们探索这些函数是如何工作的。

Map: 对列表中的所有元素执行相同的操作(例如,为支付方式的值添加后缀)。

updated_txn_list = list(map(lambda x: {
                      'paymentMthd': f"{x['paymentMthd']}_2024",
                      "totalAmt": x["totalAmt"]
                   }, txn_list))

print(updated_txn_list)

# Output
# [{'paymentMthd': 'Cryptocurrency_2024', 'totalAmt': 3339.85},
# {'paymentMthd': 'Cash on delivery_2024', 'totalAmt': 872.52},
# ...]

Filter:获取符合某个条件的元素子集(例如,仅记录支付方式为加密货币的记录)。

updated_txn_list = list(map(lambda x: x, filter(lambda y: y["paymentMthd"] == "Cryptocurrency", txn_list)))

print(updated_txn_list)

# Output
# [{'paymentMthd': 'Cryptocurrency', 'totalAmt': 3339.85},
# {'paymentMthd': 'Cryptocurrency', 'totalAmt': 576.15},
# ...]

Reduce:获取单一值的结果(例如,求和或将所有元素相乘)。

from functools import reduce

total_amt_crypto = reduce(lambda acc, x: acc + x["totalAmt"], updated_txn_list, 0)

print(total_amt_crypto)

# Output
# 250353984.67000002

我们可以在数据科学项目的转换步骤中利用这些函数。例如,使用map()来缩放或标准化数据,使用filter()来去除异常值和不相关的数据点,使用reduce()来生成汇总统计数据。

#4 数据管道测试 — Pytest

数据管道通常涉及数据摄取、数据清理和提取-转换-加载(ETL)操作。潜在错误的范围可以非常广泛且容易被忽视,尤其是当模型流和结果难以被用户解读时。这导致开发团队更依赖于测试工作。

通常会进行单元测试,以确保机器学习系统的每个组件按预期执行。

最受欢迎的 Python 测试框架之一是[Pytest](https://docs.pytest.org/en/stable/contents.html)。假设我们希望确保转化后的数据质量,技术团队和决策者都可以信任这些数据。我们可以测试我们之前处理的关于分类交易价格的函数。为了实现这一点,我们需要准备两个 Python 文件:

  • feature_engineering.py:包含之前构建的函数的文件
# Categorize the total amount of each transaction
def add_features(sample_cases):
  for txn in sample_cases:
    price = txn[‘totalAmt’]
  if 0 <= price <= 1500:
    txn[‘totalAmtCat’] = ‘Low’
  elif 1500 < price <= 3500:
    txn[‘totalAmtCat’] = ‘Moderate’
  elif 3500 < price:
    txn[‘totalAmtCat’] = ‘High’

return sample_cases
  • test_feature_engineering.py:带有“test_”前缀的文件,Pytest 仅在测试过程中识别此文件。
from feature_engineering import add_features

def test_add_features():
  sample_cases = [{
      'orderID': 1,
      'customerID': 36536,
      'productID': [2209, 2262, 4912, 3162, 5734],
      'paymentMthd': 'Cryptocurrency',
      'totalAmt': 576.15,
      'invoiceTime': '2024–06–10T23:53:25.329928'
    }]

  # Call the function with the sample cases
  sample_cases = add_features(sample_cases)

  # Check the assertations
  for txn in sample_cases: 
    assert 'totalAmtCat' in list(txn.keys())
    assert len(txn) == 7
    assert len(txn['totalAmtCat']) != 0

上面的 assert 语句确保新的“totalAmtCat”数据字段已添加且其值非空,同时原始数据字段不受影响。通过执行命令Pytest,我们可以知道测试已经通过!

Pytest 结果 — 测试通过(图片由作者提供)

对于一个更高级的案例,假设我们有三个函数,顺序如下:load_dataclean_dataadd_features。我们应该如何设计测试文件来逐个验证这些函数的输出?

import pytest
import json
from feature_engineering import load_data, clean_data, add_features

# Set up a temporary JSONL file
@pytest.fixture
def jsonl_file(tmp_path):
  sample_cases = [{'orderID': 10000,
    'customerID': 48316,
    'productID': [5620],
    'paymentMthd': 'Cash on delivery',
    'totalAmt': 9301.2,
    'invoiceTime': '2024-06-10T23:30:29.608443',
    'price': -1
  }]

  file_path = tmp_path + "/test_transactions.jsonl"

  with open(file_path, 'w') as f:
    for txn in sample_cases:
        f.write(json.dumps(txn) + "\n")

  return file_path

# Test function to validate the `load_data` function
def test_load_data(jsonl_file):
  data = load_data(jsonl_file)
  # assert statements here

# Test function to validate the `clean_data` function
def test_clean_data(jsonl_file):
  data = load_data(jsonl_file)
  data = clean_data(data)
  # assert statements here

# Test function to validate the `add_features` function
def test_add_features(jsonl_file):
  data = load_data(jsonl_file)
  data = clean_data(data)
  data = add_features(data)
  # assert statements here

我们应该为初始化定义一个固定的基准,例如一个包含样本测试用例的 JSON Lines 文件。在这里,我们使用@pytest.fixture装饰器,它类似于我们在 Python 装饰器部分讨论的time_decorator。这个装饰器有助于避免反复初始化样本文件。对于剩下的代码,我们涉及几个测试函数来运行管道函数,并使用 assert 语句来检测逻辑错误。

总结一下

我们遇到了数据工程项目中的几个关键方面,并探索了如何简化和优化 Python 代码以提高效率和可读性:

  • 数据摄取,使用yield处理大数据集,同时实现高效的内存使用。

  • 数据验证,利用Pydantic根据模式和自定义值模式验证数据字段。

  • 数据处理,通过应用 Python 装饰器和内置库来启用额外的功能,而无需重复代码。

  • 通过使用Pytest进行管道测试,以确保工作流中各个环节的函数输出质量。

在你继续之前

如果你喜欢这篇文章,我邀请你关注我的Medium 页面LinkedIn 页面。通过这种方式,你可以随时获取有关数据科学副项目、机器学习操作(MLOps)演示以及项目管理方法的最新内容。

## 使用 LangChain 和 LLMs 进行客户分析

探索 LangChain 在客户分析中的潜力与局限性,并附带实际的实现过程…

towardsdatascience.com ## 使用 PySpark 在 Databricks 上进行时间序列特征工程

探索 PySpark 在时间序列数据中的潜力:导入、提取和可视化数据,并附带实际操作…

towardsdatascience.com

模拟时尚零售中的循环经济挑战

原文:towardsdatascience.com/simulate-the-challenges-of-a-circular-economy-for-fashion-retail-5c7e269f0706?source=collection_archive---------14-----------------------#2024-09-26

使用数据分析模拟时尚零售的循环租赁模型,理解门店操作和物流挑战。

Samir SaciTowards Data Science Samir Saci

·发表于 Towards Data Science ·15 分钟阅读·2024 年 9 月 26 日

--

时尚零售的租赁模型 — (图像来源:作者)

循环经济的概念包括旨在减少废物和提高资源效率的模型。

模拟循环经济 — (图像来源:作者)

一些时尚零售商已经实施了订阅模型,让客户支付定期费用来租赁产品,在特定的时间段内

你有没有考虑过租赁你的衣物?

在上一篇文章中,我使用数据分析模拟了这个租赁模型,以一个拥有十家门店的时尚零售商为例。

循环租赁模型以减少环境足迹 — (图像来源:作者)

目标是估算环境表现,即减少二氧化碳排放和水资源使用。

然而,物流操作将面临额外的挑战,需要收集和处理租赁物品,以支持这一过渡。

作为一名数据科学家,你能评估实施这一模型的操作挑战吗?

本文将使用数据分析来评估这些挑战,并理解哪些指标对于重新设计物流网络至关重要。

目标是帮助可持续发展和物流团队建立一个可靠的商业案例(包括投资回报率和风险评估),以获得高层管理层的批准,从而推动向循环经济的过渡。

Summary
**I. Implement a Circular Model for Fashion Retail**
  1\. **Problem Statement: Sustainability Roadmap of a Fashion Retailer**
  A fashion retailer would like to implement a rental model in 10 stores
  2\. **Introduction of the Rental Model**
  Simulation of the impact of this model on CO2 emission and water usage
  3\. **How do we implement these processes?**
  Use analytics to derive the metrics needed to design logistics solutions
**II. Data Analytics to Monitor a Rental Circular Model**
*Focus on each leg of the distribution network to collect KPIs*
  1\. **Focus on the store operations**
  Manage daily transactions per type (linear, circular)
  2\. **Focus on warehouse operations**
  Impact on the upstream and downstream flows
  3\. **Focus on transportation management**
  Organisation routing for returned items collection
**III. Support the transition to a circular model with data analytics**
  1\. **Business Intelligence for operational monitoring**
  Collect, process and harmonize data from multiple systems
  2\. **Advanced Workforce Planning for Store Operations with Python**
  Optimize the number of staff recruited at stores
  3\. **Return Flow Optimization with Python**
  Allocation of the collection centers for returns
  4\. **Measure Scope 3 Emissions of your Distribution Network**
  Measure the CO2 emissions impact of your collection routes
**IV. Conclusion**

为时尚零售实现循环模型

问题陈述:一家时尚零售商的可持续发展路线图

你是国际时尚零售集团供应链部门的数据科学经理

去年,你的首席执行官公开宣布公司承诺支持联合国可持续发展目标(SDGs)

面向地球的目标 — (图像由作者提供)

作为这项承诺的一部分,公司旨在减少整个价值链的环境足迹。

作为数据科学家,你如何支持这一转型?

你专注于协助可持续发展和物流团队评估和设计解决方案,以实施循环租赁模型。

该项目始于 a 项涉及十家商店的研究,共涉及 400 个独特的物品。

初步研究的范围 — (图像由作者提供)

结论是什么?

结果表明,较短的租赁周期最大化了循环模型的效率

然而,本研究仅关注环境效益,并未考虑商店和物流团队所面临的运营挑战。

对物流预算有何影响?

团队必须证明投资回报率(ROI),并在获得高层管理批准之前估算预算。

因此,我们将把注意力转向运营方面,并利用数据分析估算分销网络各个阶段的额外工作量。

让我先简要介绍租赁模型。

租赁模型介绍

为了减少供应链的环境影响,公司在10 家商店进行了循环租赁模型的试验。

模拟的物流参数 — (图像由作者提供)

这些地点将向客户提供租赁订阅模式,涵盖400 个物品的有限范围

在实施该服务之前,物流和可持续发展团队要求支持模拟这些额外流程的处理过程

作为输入数据,我们使用了实际的销售交易数据,如下所示。

商店 1 的‘服装 1001’的销售交易 — (图像由作者提供)

模拟涵盖了十家商店,持续时间为365 天

我们假设这些“销售交易”是“租赁交易”,意味着顾客去商店租赁某个特定物品,租期为 n 天

14 天租赁过程的示例 — (图像由作者提供)

租赁期结束后,顾客将物品退还至商店。

  • 收集这些物品商店需要两天时间。

  • 它们经过仓库检查和清洁,持续一天

  • 需要额外一天的时间才能将清洁后的物品送到商店。

我们如何管理租赁物品的流动?

我们应用先进先出(FIFO)原则来管理这些流动。

  • 在退货和分类后,物品将在仓库中可用

  • 第一个下单的商店将通过首先返回的物品进行补货。

FIFO 原则在循环物品中的应用 — (图片来源:作者)

当客户请求特定物品时,我们有两种情况:

  • 循环交易:如果库存充足,商店将租赁退货的衣物。

  • 线性交易:如果没有可用的退货物品,商店将租赁新物品。

对于特定商店,情况如何?

在下图中,您可以看到每天租赁物品的数量,并且区分了线性(新物品)和循环(退货物品)交易。

商店 2 每日循环物品的百分比 — (图片来源:作者)

如您所见,循环模式在第 13 天开始,当第一批租赁物品被退还(并清洁)后。

我们想要估算哪个指标?

重点是通过不同租期(2 天、7 天、14 天和 28 天)的多个场景来衡量模型的环境表现

每种情况的 CO2 排放减少量 — (图片来源:作者)

对于每种情况,我们计算循环交易的百分比(重复使用的物品)以及对 CO2 排放减少的影响。

可持续发展团队:通过短期租赁,我们可以实现最大的碳足迹减少。

欲了解更多详细信息,请阅读这篇文章。

## 可持续发展数据科学 — 模拟循环经济

使用数据科学模拟循环模式对快速时尚的 CO2 排放和水使用的影响…

towardsdatascience.com

从可持续性的角度来看,这是一个非常有洞察力的研究。

然而,它没有涵盖对分销链的影响。

操作上的挑战是什么?

作为供应链专业人员,我想了解如何将此操作纳入当前的分销模型中。

  • 实施短期租赁意味着高频率的物品轮换。

  • 我们能否使用同一车队来组织收集并配送到商店?

  • 仓库在分类和清洁方面的额外工作量如何?

让我们使用数据分析来回答这些问题。

我们如何实施这些流程?

作为前供应链解决方案设计师, 我的工作方法从收集设计这些逆向流动的运输和仓储解决方案所需的指标开始。

实施此解决方案前需要回答的问题示例——(图片由作者提供)

这些解决方案应覆盖

  • 使用电动卡车收集店铺退货物品。问题: 每天需要多少辆卡车来收集物品?

  • 接收并处理仓库中的物品。

    问题: 需要多少操作员来接收并处理仓库中的物品?

  • 将清洁(退还的)物品送回店铺。

    问题: 需要多少辆卡车来处理这些配送?

在接下来的部分中,我们将涵盖评估配送网络额外工作量所需的主要指标

数据分析用于监控租赁循环模式的物流网络

现在,让我们从物流角度探索仿真模型,重点关注货物流动。

我们假设租赁期为七天。

循环经济的供应链网络——(图片由作者提供)

首先,我们可以监控门店的租赁交易以及退货物品的影响。

店铺发生了什么?

关注店铺运营:按类型的每日交易量

循环模式从第 1 天开始

  • 租出的 100%物品是全新的;

  • 这些物品将在第 8 天退还,以启动收集、处理和配送的过程,直至第 13 天结束;

租赁物品的循环处理——(图片由作者提供)

从第 13 天开始,店铺将收到退货物品,并准备开始第二个租赁周期。

店铺团队的影响是什么?

💡 指标 1: 每日退货物品数量

每日售出和退还的物品数量——(图片由作者提供)

从第 8 天开始,店铺团队将需要管理退货物品

  • 客户将在专门的区域排队进行租赁交易

  • 销售代理将接收这些物品

他们将遵循这一标准流程

  1. 他们应当检查退货物品是否有损坏

  2. 在系统中记录退货并提供收据给客户

  3. 将物品转移到暂存区等待收集

我们能否评估额外的工作量?

我们进行粗略估算,假设整个过程平均需要 6 分钟,并且每次退货交易有1.25 件物品

管理店铺 4 退货的 FTE 数量——(图片由作者提供)

从第一天的回收日(第 8 天)开始,我们需要店铺 4 平均需要 5.13 个 FTE

店铺中的 FTE 成本是多少?

这些信息对需要管理销售地点损益的门店运营团队至关重要。

💡 指标 2: 租赁物品的重复使用比例

启动循环时,每种类型的每日交易 — (作者提供的图片)

从第 13 天开始,圆形租赁的比例如预期般激增。

循环从租赁物品的库存开始,这些物品可用于避免使用新物品。

循环启动后的每日交易 — (作者提供的图片)

从第二个月开始,不到 12%的新物品 (线性交易) 被用于租赁交易

专注于仓库运营:上下游流动

从物流角度来看,这意味着什么?

  • 来自工厂的新物品库存应该减少,这将减少在 仓库的存储空间需求。

  • 减少来自工厂的补货量:分配给上游流程的卡车减少。

2 个上游流程(线性:来自工厂 / 圆形:来自门店)—(作者提供的图片)

不幸的是,逆向流程中的额外工作量可能不会通过上游流程中的减少而得到补偿。

那么,退货物品的收集量如何呢?

💡 指标 3: 每天收集的储物箱数量

收集的物品存放在塑料储物箱中(每个储物箱 10 个),尺寸固定。

每日退还的储物箱数量 — (作者提供的图片)

我们平均每天收集 56 个储物箱,第 25 天的最高峰为105 个储物箱

🏪 这对门店运营有什么影响?

  • 我们需要为最多 210 个储物箱分配存储位置。

    💡 我们是否有额外的空间来存放这些储物箱?

  • 这些 56 个储物箱的管理工作量是有关系的。 💡我们是否有足够的工作人员来管理这部分内部物流?

既然我们已经将物品放入储物箱中,接下来让我们专注于收集过程。

收集过程 — (作者提供的图片)

我们手头有哪些参数可以用来设计收集路线?

专注于运输管理。

我们需要分析每个门店每天需要收集的量,以规划收集路线。

运输团队将使用最大容量为托盘的卡车,这些卡车可以从不同的门店收集。

💡 指标 4: 每家门店装载的托盘数量

储物箱被装载在托盘上,每个托盘的最大容量为25 个储物箱

门店分布在三个区域:A 区有四家门店,B 区和 C 区每区有三家门店。

A 区每家门店退还的托盘数量 — (作者提供的图片)

每个区域的门店可以共享同一辆卡车来收集退货物品的托盘。

让我们把这些体积转换为整车装载量。

💡 指标 5: 每个门店所需的满载卡车数量

运输团队分配了每辆车可承载 14 个托盘的卡车。

我们每个门店需要多少辆卡车?

AREA-B 的每天卡车数量 — (图片来源:作者)

如你所见,单凭收集 B 区的单一门店,无法填满一辆卡车。

  • 运输规划员必须安排多个收集点的路线,以最大化卡车的使用。

  • 基于过去的租赁交易,它们可以预测退货量,以制定最优路线。

五个指标的总结 — (图片来源:作者)

这五个指标帮助供应链工程师预见未来的挑战并估算额外成本。

我们能否利用数据分析来限制对性能和成本效率的影响?

利用数据分析支持向循环经济模式的转型。

假设高层管理人员已经验证了这个提案。

他们同意实施为期一年的试点阶段,范围与研究相同(10 家门店,400 个商品)。

作为一名数据分析专家,你如何帮助运营和可持续发展团队克服之前列出的挑战?

我们将首先集中精力实施描述性分析解决方案,为门店、仓库和运输团队提供可视化。

用于运营监控的商业智能

多个系统用于管理流程和库存,或更新租赁物品在价值链中的状态。

时尚商品的价值链 — (图片来源:作者)

这些系统生成的交易数据可以用来监控流程并收集之前提到的指标。

  • 运输管理系统(TMS)将追踪门店交付和退货物品的提货。

  • 仓库管理系统可以提供来自工厂的进货流、库存以及门店交付或退货的可视化。

  • 销售点(POS)和企业资源规划(ERP)系统将存储销售交易数据及线性和循环商品的库存。

作为一名分析专家,你的角色是实施自动化管道,构建一个可信赖的统一数据源。

数据统一示例,用于追踪从创建到交付的订单 — (图片来源:作者)

如果你需要类似解决方案的示例,请查看这篇文章

## 什么是商业智能?

探索应用于供应链优化的数据驱动决策工具。

towardsdatascience.com

这些清洗后的数据集可以用于报告目的,并作为更先进的预测分析解决方案的来源。

使用 Python 进行店铺运营的高级员工规划

正如指标 1指标 4所示,店铺运营面临的第一个挑战是由于退货产生的额外工作负担。

每日退货量 — (作者提供的图片)

由于您实施的分析解决方案,您可以查看过去每一天的工作负载。

我们需要招募多少额外的员工来吸收工作负荷?

您希望在遵守某些约束条件的情况下,最小化招募员工的数量以节省成本。

  • 退货需要及时处理;

  • 尊重当地关于最大工作时间的规定;

  • 提供最低工作时间以吸引候选人;

工作时间约束示例 — (作者提供的图片)

幸运的是,我曾通过使用 Python 的线性规划解决过类似的仓库进货操作问题。

进货操作的工作负载分布 — (作者提供的图片)

这个想法是使用 Python 库 PuLP 来构建您的模型,目标函数是(最小化员工数),并设置不同的约束条件。

在输入算法时提供销量预测后,您可以自动生成每天需要招募的最优员工数量。

进货示例的解决方案示例 — (作者提供的图片)

欲了解更多详情,请查看本文中详细的示例。

## 使用线性规划与 Python 优化劳动力规划

您需要雇佣的临时工最少数量是多少,以便在确保…的同时应对每周的工作负荷?

towardsdatascience.com

然而,我们仍然需要处理运输操作以收集这些货物。

我们必须实施一个最优解决方案,因为这将代表最高的成本和环境影响。

使用 Python 进行退货流量优化

物流部门团队请求您在有效管理店铺取货时提供支持。

店铺和分拣中心网络 — (作者提供的图片)

考虑到分拣中心的位置和容量,他们希望有一个自动化工具来为每个商店分配合适的分拣中心。

  • 最小化收集的运输成本和排放

  • 尊重交货时间和分拣能力

供应规划问题 — (图片由作者提供)

我提出一个类似的问题来找到合适的解决方法。

供应规划问题:多个工厂为配送中心补充货物,配送中心再将货物配送到商店。我们可以使用线性规划来解决这个问题

  • 管理从工厂到仓库的上游流动

  • 从正确的配送中心向每个商店配送

如何调整分拣中心分配问题的解决方案?

这个解决方案可以在稍作调整后使用

  1. 预测从分拣中心到仓库的货物量

  2. 定义每个分拣中心的容量

  3. 计算每个商店到所有配送中心的距离

如果你需要示例,随时可以查看这篇文章

## 使用 Python 进行线性规划的供应规划

你需要将库存分配到哪里,以满足客户需求并降低运输成本?

towardsdatascience.com

这可以显著降低你的成本,但可能会带来额外的 CO2 排放。

我们能否衡量由于从商店收集所产生的额外 CO2 排放?

测量你配送网络的范围 3 排放

原始目标是减少你公司对环境的影响。

因此,最坏的情况是由于逆向流动管理不当,CO2 排放会大幅增加。

我的第一个可持续发展项目是测量一家快速消费品公司配送网络的范围 3 排放。

如何用 Python 衡量范围 3CO2 排放?

我们可以遵循法国环境署 Ademe的协议。

使用排放因子估算运输 CO2 排放的公式是:

使用排放因子的公式 — (图片由作者提供)

  • E_CO2:二氧化碳当量的排放量(kgCO2eq)

  • W_goods:货物的重量(吨)

  • D:从仓库到最终目的地的距离(公里)

  • F_mode:每种运输方式的排放因子(kgCO2eq/t.km)

希望这些参数能够通过商业智能解决方案,从系统的主数据和交易数据中收集。

CO2 排放计算数据处理示例 — (图片来源:作者)

目标是通过高级可视化监控逆向物流操作的排放情况。

使用 PowerBI 生成的可视化示例 — (图片来源:作者)

如需更多信息,请查看这篇文章。

## 使用 Python 进行供应链可持续性报告

使用 Python 构建一个专注于您分销网络 CO2 排放的可持续性报告。

towardsdatascience.com

现在,您已经拥有了一套解决方案,可以开始支持物流操作和可持续发展部门实施这一租赁模型。

结论

这一简单的方法可以应用于任何零售商的业务案例,用于过渡到循环经济。

这可以从模拟循环交易开始,

  1. 使用特定时期的实际销售交易;

  2. 实施考虑租赁期限和逆向流动过程的库存管理规则;

  3. 估算减少足迹的影响以及物品再利用的百分比;

改进模型的整体模拟工作流程 — (图片来源:作者)

然后,通过添加特定的指标来改进模型,以估算商店操作和运输的额外工作量。

  1. 从供应链解决方案设计师的角度出发,使用体积和物流比例来设计流程;

  2. 利用洞察力做出决策,以确保高效顺畅的运营

本文中介绍的方法提供了数据,这是我(作为一家物流公司的供应链解决方案设计师)会要求零售商在设计此类解决方案时提供的。

接下来做什么?

实施循环模型并不是减少供应链足迹的唯一举措。

您可以通过更高效地生产和交付产品来改进线性模型。

供应链可持续性分析 — (图片来源:作者)

如果您需要更多实际操作案例的灵感,请查看这篇文章。

## 供应链可持续性数据分析

发现数据分析如何通过实际案例和实用技巧提高供应链的可持续性。

[towardsdatascience.com

关于我

让我们在LinkedInTwitter上建立联系。我是一名供应链工程师,利用数据分析改善物流运营并降低成本。

如果你需要关于供应链转型的咨询或建议,请通过Logigreen Consulting与我联系。

如果你对数据分析和供应链感兴趣,请访问我的网站。

[## Samir Saci | 数据科学与生产力]

一个专注于数据科学、个人生产力、自动化、运筹学和可持续发展的技术博客…

samirsaci.com

模拟数据,真实学习:第一部分

原文:towardsdatascience.com/simulated-data-real-learnings-part-1-dccb52667e32?source=collection_archive---------4-----------------------#2024-03-12

使用模拟测试机器学习方法

Jarom HuletTowards Data Science Jarom Hulet

·发表于Towards Data Science ·16 分钟阅读·2024 年 3 月 12 日

--

模型估计系数在模拟数据上的分布——作者提供的图片

模拟是数据科学工具箱中的一项强大工具。这是一个多部分系列的第一部分,讨论模拟在数据科学和机器学习中的各种应用方式。在本文中,我们将介绍如何使用模拟来测试机器学习方法。

具体来说,我们将介绍模拟可以在以下三种方式中使用:

  1. 测试机器学习方法

  2. 比较不同机器学习模型的性能

  3. 在各种情况下评估模型行为

在深入探讨数据模拟的具体应用之前,我们先来定义一下什么是模拟。

什么是数据模拟?

数据模拟的定义非常简单——它是创建虚构数据,模拟现实世界数据的特性。

我们什么时候需要模拟数据?

  • 当我们需要得到那些在现实世界中无法观察到的‘答案’时——也就是说,使用现实世界的数据,我们只能推断 X 与 y 之间的关系;但通过模拟数据,我们可以创建X 与 y 之间的关系——有了这个‘答案’,我们就可以测试我们的……

仿真数据,真实学习:功效分析

原文:towardsdatascience.com/simulated-data-real-learnings-power-analysis-652045eeae22?source=collection_archive---------8-----------------------#2024-03-26

第二部分 — 实验功效分析

Jarom HuletTowards Data Science Jarom Hulet

·发表于 Towards Data Science ·阅读时间 11 分钟 ·2024 年 3 月 26 日

--

图片由 Robert So 提供,来自 Pexels.com

引言

仿真是数据科学工具箱中的一个强大工具。阅读本文后,您将对如何使用仿真来估算实验设计的效能有一个很好的理解。这是一个多部分系列文章的第二部分,讨论了仿真在数据科学和机器学习中的应用。

以下是我们将要涵盖的内容:

  1. 功效分析概述

  2. 如何使用仿真计算功效 — 基于示例的方法

在本文中,我将简要介绍数据仿真的定义:

数据仿真是创造虚拟数据,模拟真实世界的属性。

在本系列的第一部分中,我将更广泛地讨论数据仿真的定义——您可以通过以下链接查看:

## 仿真数据,真实学习:第一部分

使用仿真测试机器学习方法

[towardsdatascience.com

功效分析概述

模拟数据,真实学习:情境分析

原文:towardsdatascience.com/simulated-data-real-learnings-scenario-analysis-02ee56ed8886?source=collection_archive---------6-----------------------#2024-04-04

第三部分 — 模拟情境

Jarom HuletTowards Data Science Jarom Hulet

·发布于 Towards Data Science ·9 分钟阅读·2024 年 4 月 4 日

--

图片来自 Pexels.com,由 pixabay 提供

引言

模拟是数据科学工具箱中的一项强大工具。在本文中,我们将讨论如何通过模拟可能的情境来帮助我们做出更好的决策和战略。我们将深入探讨的一个关键概念是如何利用机器学习模型和情境模拟来做出更好的决策。

本文的具体内容包括:

  1. 情境模拟用于优化

  2. 风险管理中的情境模拟

这是关于数据科学中模拟的多部分系列文章中的第三部分。第一篇文章介绍了如何使用模拟来测试机器学习方法,第二篇文章讲解了如何使用模拟来估算设计实验的效能。

## 模拟数据,真实学习:第一部分

使用模拟测试机器学习方法

towardsdatascience.com

仿真数据,真实学习:模拟系统

原文:towardsdatascience.com/simulated-data-real-learnings-simulating-systems-79374a9379fd?source=collection_archive---------8-----------------------#2024-04-16

第四部分——通过仿真理解系统

Jarom HuletTowards Data Science Jarom Hulet

·发布于 Towards Data Science ·13 分钟阅读·2024 年 4 月 16 日

--

来自 Pexels.com 的张凯逸

引言

仿真是数据科学工具箱中的一项强大工具。本文将探讨模拟系统如何帮助我们制定更好的策略并做出更好的决策。

本文的具体主题包括:

  1. 什么是系统仿真?

  2. 系统仿真优化

  3. 系统仿真风险评估

这是关于数据科学中仿真技术的多篇系列文章的第四篇。第一篇文章讲解了如何利用仿真来测试机器学习方法,第二篇文章讨论了如何使用仿真来估算设计实验的效力,而第三篇文章则探讨了我们如何通过模拟情景来制定策略。

以下是这些文章的链接:

## 仿真数据,真实学习:第一部分

使用仿真测试机器学习方法

towardsdatascience.com

救命!我们被 HECS 绑住了

原文:towardsdatascience.com/simulating-student-loan-maturity-in-australia-27366297c814?source=collection_archive---------11-----------------------#2024-05-01

《大学协议》建议的统计模拟

Murray GillinTowards Data Science Murray Gillin

·发表于Towards Data Science ·阅读时间 15 分钟·2024 年 5 月 1 日

--

引言

在澳大利亚,学生可以通过政府贷款——高等教育贷款计划(HELP)来支付高等教育费用。为了确保贷款的价值不被贬值,这些贷款每年会根据消费者价格指数进行调整。候选人只有在税后收入超过大约$51k 时才开始偿还贷款,并且有一个分级偿还率,当收入超过大约$151k 时,最高偿还比例为 10%。最近的贷款调整率创下 7.1%的历史新高,这是自 1990 年代以来最高的,这使得许多人更加关注学生债务是如何逐步增加的。

摄影:由Edwin Andrade拍摄,发布于Unsplash

我就是其中之一,离开学校时完成了荣誉学士学位,后来又在 COVID 期间通过硕士课程重新培训。在我的 20 多岁时,我的收入不足以达到强制性缴费的门槛,所以债务逐渐累积。获得硕士学位(约 4 万)后,债务水平进一步提高。虽然现在我正在采取措施清除债务并恢复那些非生产性扣除的部分,但可以想象,这对于许多毕业生来说是一个负担。

《大学协议审查》提出了关于债务偿还的以下建议性变更(建议 16)。

为了减少学生学习的长期财务成本,澳大利亚政府应使学生贡献更加公平,并更好地反映学生通过学习获得的终生收益,同时通过引入更公平、更简便的指数调整和还款安排,减少 HELP 贷款的负担。

这应包括:

A. 减少学生的贡献,以应对“就业准备毕业生”(JRG)计划的最重大影响,首先从人文学科、其他社会与文化、传播与人类运动学专业的学生开始,逐步过渡到基于预测的潜在终生收入的学生贡献系统。

B. 通过实施基于边际税率的 HELP 还款系统,减少低收入者的还款负担,并限制增加额外工作小时的反激励。

C. 通过改变 HELP 贷款的指数调整时间,减少还款时间,使得用于强制还款的金额在应用指数调整前能得到计算。

D. 确保 HELP 贷款的增长不会超过工资增长,通过将 HELP 的指数调整率设定为消费者价格指数(CPI)和工资价格指数(WPI)中的较低者。

E. 审查银行贷款实践,确保银行认识到 HELP 贷款不同于其他类型的贷款,且不会以不当方式限制人们的住房贷款借款能力。

让我们详细分析这些建议,特别是与学生贷款的指数调整和还款相关的部分,并将其框架化为统计问题。

B 点表明当前的还款率可能会对低收入者产生反激励作用。尽管这是一种同情的观点,但忽视了长期拖欠债务的影响。债务每年都会复利增长,与个人的还款能力无关。我们将分析还款率对清偿年限的影响,并审查其他国家如何对学生贷款进行指数调整或加收利息。

C 点是一个有趣的案例,乍看之下似乎公平,就像任何其他债务一样,扣款是从税后收入中进行的,应该在后续的指数调整前计入本金。我们可以进行相关模拟,并计算债务清偿年限在学生中的分布变化。

当前和提议的指数调整及还款影响

D 点是一个有趣的尝试,旨在通过将 HELP 贷款的指数调整率设定为 CPI 或 WPI 中较低的一个,来限制债务增长。我们可以进行一个简单的统计练习,测量过去 20 年中,CPI 低于 WPI 的次数。

为了回答这些问题,我们将尝试解答以下问题:

  • 学生是否会受益于在 CPI 和 WPI 之间转换,以最小化指数调整率?

  • 在三种情景下模拟学生贷款的轨迹:当前状态、上面提到的提议未来状态,以及对所有收入水平超过当前最低门槛的情况应用 10%的偿还率。这个练习将帮助我们评估不同情景下清偿年限分布是否存在差异。

学生们是否会从应用 CPI 和 WPI 中的最小值中受益?

这是旨在减少指数化对学生影响的关键建议之一。它在逻辑上是有缺陷的,因为薪资增长和水平与债务水平是独立的。然而,让我们暂时接受这一点,并评估是否存在合理的差异,这种差异将使债务增长放缓,相比单纯使用 CPI。相关数据已经来源并审查了WPICPI,感谢澳大利亚统计局,数据均在 CC BY 4.0 许可下发布。

wpi_df <- readr::read_csv("All sector WPI, quarterly and annual movement (%), seasonally adjusted (a).csv", 
    col_types = cols(`Quarterly (%)` = col_skip()), 
    skip = 1) %>% 
    rename(month_year = ...1,
           value = `Annual (%)`) %>% 
    drop_na() %>% 
    mutate(month_year = lubridate::my(month_year),
           rate = 'wpi')

cpi_df <- read_csv("All groups CPI, Australia, quarterly and annual movement (%).csv", 
    col_types = cols(`Change from previous quarter (%)` = col_skip()), 
    skip = 1) %>% 
    rename(month_year = ...1,
           value = `Annual change (%)`) %>% 
    drop_na() %>% 
    mutate(month_year = lubridate::my(month_year),
           rate = 'cpi')

rates_df <- bind_rows(cpi_df, wpi_df) %>% 
    filter(month_year >= '2014-03-01' & month_year <= '2023-12-01') %>% 
  mutate(rate = as_factor(rate))

rates_df %>% 
  ggplot(aes(month_year, value, color = rate)) +
    geom_point() +
    geom_line() +
    theme_ggdist() +
    scale_color_viridis_d(begin = 0.3, end = 0.7) +
    labs(x = 'Date', y = 'Index Value', color = 'Rate', title = 'Comparison of Annual CPI and WPI')

按季度比较年度 CPI 和 WPI(图片来源:作者)

我们可以看到,CPI 的方差大于 WPI,但随着时间推移,关于均值,两个基础分布之间是否存在实际差异?

我们为 10 年的历史数据拟合了一个贝叶斯模型,从后验分布的期望值中抽取样本,然后使用贝叶斯方法执行均值差异评估。

indices <- 
  brm(
    bf(value ~ rate + 0,
       sigma ~ rate + 0),
    data = rates_df,
    prior = c(prior(normal(2, 2), class = 'b')),
    family = gaussian, 
    iter = 2000, chains = 4, seed = 246, cores = 4, sample_prior = 'yes'
  )

new_df <- tibble(rate = c('wpi', 'cpi'))

new_df %>% 
  add_epred_draws(indices) %>% 
  compare_levels(.epred, rate, comparison = list(c('cpi', 'wpi'))) %>% 
  ggplot(aes(.epred, fill = after_stat(x > 0))) +
    stat_halfeye() +
    geom_vline(xintercept = 0, lty = 2) +
    theme_ggdist() +
    scale_fill_manual(values = c("gray80", "skyblue")) +
    labs(y = 'Density', x = 'Difference in Posterior Means', title = 'Difference in Posterior Means of WPI and CPI',
         subtitle = "80% of Density is Greater then 0/nApplying ROPE of 10%, Difference is Neglible", fill = 'Value Greater Then 0')

CPI 和 WPI 的后验均值分布差异(图片来源:作者)

基于 WPI 和 CPI 的后验分布,CPI 的均值比 WPI 的均值大 0.3 个基点。80%的后验分布差异大于 0,并且在 10%的 ROPE 范围内,我们可以认为这个差异是微不足道的。在 WPI 和 CPI 之间的切换在最好情况下会最小化通货膨胀的影响,但从长远来看,不会实质性地帮助学生克服这些债务。

模拟学生的指数化和偿还情景

让我们继续评估建议中的其他元素,主要是指数化时机和强制偿还率。

关键模拟假设概述

要模拟学生的结果,我们需要对关键变量有合理的先验知识。

为了模拟毕业生薪资,我们采用了相当宽泛的视角,使用 68,000 美元的毕业生中位数薪资,并应用了下面展示的对数正态分布。这样我们可以捕捉到大多数毕业生薪资集中在这一点附近,同时也考虑到某些行业可能提供的更高起薪。类似地,对于债务,我们使用了当前的平均学生债务,并应用了一个相对偏右的分布,以捕捉债务的广泛范围。

对于指数化率,我们简单地取了过去 10 年每季度的年度 CPI 平均值,并假设它遵循正态分布,如上所示。类似地,我们对薪水增长采取了保守且积极的看法,假设年均增长 3%,并遵循对数正态分布,使得毕业生因晋升或新工作机会能够更快地提升。

模拟变量分布(图片来自作者)

模拟变量假设(图片来自作者)

index <- 1:50000 #simulate 50000 students

year <- 0:19 #over 20 years

calculate_salary <- function(previous, basevalue, multiplier) {
  coalesce(basevalue, multiplier * previous)
}

set.seed(246)

base_df <- 
  crossing(index, year) %>% 
  group_by(index) %>% #salary and growth varies by individual
  mutate(salary_0 = rlnorm(1, mean = log(68000), sdlog = log(1.34)),
         debt_0 =  rlnorm(1, meanlog = 10.2, sdlog = 0.5)) %>% 
  group_by(year) %>% #indexation rate applies uniformly across all indices each year
  mutate(indexation_rate = rnorm(1, mean = 0.027, sd = 0.012),
         indexation_rate = round(indexation_rate, 3)) %>% 
  group_by(index, year) %>% 
  mutate(salary_growth = rlnorm(1, meanlog = -3.5, sdlog = 0.6) + 1,
         salary_growth = round(salary_growth, 3)) %>% 
  group_by(index) %>% 
  mutate(salary_0 = if_else(year > 0, NA, salary_0),
         salary_1 = accumulate2(salary_0, salary_growth[-1], calculate_salary),
         salary_1 = case_when(salary_1 < 18200 ~ salary_1 * (1-0), #calculate post-tax incomee
           salary_1 >= 18201 & salary_1 <= 45000 ~  salary_1 - 0 - (salary_1-18200)*(0.19),
           salary_1 >= 45001 & salary_1 <= 120000 ~ salary_1 - 5092 - (salary_1-45000)*(0.325),
           salary_1 >= 120001 & salary_1 <= 180000 ~ salary_1 - 29467 - (salary_1-120000)*(0.37),
           salary_1 >= 180001 ~ salary_1 - 51667 - (salary_1-180000)*(0.45)
         ))

上面的代码片段设置了模拟 50,000 名学生在 20 年内的情况。我们使用自定义函数calculate_salarypurrr::accumulate来迭代计算给定样本增长分布下的薪资增长,然后计算他们的税后收入。还需要注意的一点是,每个候选人从自己的增长分布中抽取数据,但指数化在所有学生中按相同的年增长率应用。

calculate_remaining_debt <- function(principal, payment, interest_rate) {
interest = principal * interest_rate
remaining_debt = principal + interest - payment
remaining_debt = ifelse(remaining_debt < 0, 0, remaining_debt)
remaining_debt
}

set.seed(246)

df <- base_df %>% mutate(
         repayment_rate = case_when(
           salary_1 < 51550 ~ 0.0, # Repayment Rates Post-Tax Income
           salary_1 >= 51550 & salary_1 <= 59518 ~ 0.01,
           salary_1 >= 59519 & salary_1 <= 63089 ~ 0.02,
           salary_1 >= 63090 & salary_1 <= 66875 ~ 0.025,
           salary_1 >= 66876 & salary_1 <= 70888 ~ 0.03,
           salary_1 >= 70889 & salary_1 <= 75140 ~ 0.035,
           salary_1 >= 75141 & salary_1 <= 79649 ~ 0.04,
           salary_1 >= 79650 & salary_1 <= 84429 ~ 0.045,
           salary_1 >= 84430 & salary_1 <= 89494 ~ 0.05,
           salary_1 >= 89495 & salary_1 <= 94865 ~ 0.055,
           salary_1 >= 94866 & salary_1 <= 100557 ~ 0.06,
           salary_1 >= 100558 & salary_1 <= 106590 ~ 0.065,
           salary_1 >= 106591 & salary_1 <= 112985 ~ 0.07,
           salary_1 >= 112986 & salary_1 <= 119764 ~ 0.075,
           salary_1 >= 119765 & salary_1 <= 126950 ~ 0.08,
           salary_1 >= 126951 & salary_1 <= 134568 ~ 0.085,
           salary_1 >= 134569 & salary_1 <= 142642 ~ 0.09,
           salary_1 >= 142643 & salary_1 <= 151200 ~ 0.095,
           salary_1 > 151201 ~ 0.1),
         repayment = salary_1 * repayment_rate,
         debt_1 = accumulate(2:n(), .init = first(debt_0),
~ calculate_remaining_debt(.x, repayment[.y], indexation_rate[.y])
),
         repayment = if_else(debt_1 == 0, 0, repayment),
         debt_paid = if_else(debt_1 == 0, 'y', 'n'))

df <- 
df %>% 
  group_by(index) %>% 
  mutate(clearance_year = if_else(lag(debt_1, default = first(debt_1)) > 0 & debt_1 == 0, 1, 0),
         clearance_cum = cumsum(clearance_year)) %>% 
  filter(clearance_cum == 0 | clearance_year == 1) %>% 
  select(1:11) %>%
  mutate(is_paid = if_else(debt_paid == 'y', 1, 0),
         debt = round(debt_1, 2),
         salary = round(salary_1, 2),
         scenario = 'INDEXATION PRE PAYMENT; PROGRESSIVE PAYMENT RATE',
         scenario_l = 'A',
         group = if_else(max(is_paid) == 1, 'paid', 'unpaid'))

# Repeat Again with Different Calculation of Debt, Indexation Post Repayment

calculate_remaining_debt <- function(principal, payment, interest_rate) {
remaining_debt = principal - payment + (principal-payment) * interest_rate
remaining_debt = ifelse(remaining_debt < 0, 0, remaining_debt)
remaining_debt 
}

df2 <- 
df2 %>% 
  group_by(index) %>% 
  mutate(clearance_year = if_else(lag(debt_1, default = first(debt_1)) > 0 & debt_1 == 0, 1, 0),
         clearance_cum = cumsum(clearance_year)) %>% 
  filter(clearance_cum == 0 | clearance_year == 1) %>% 
  select(1:11) %>%
  mutate(is_paid = if_else(debt_paid == 'y', 1, 0),
         debt = round(debt_1, 2),
         salary = round(salary_1, 2),
         scenario = 'INDEXATION AFTER PAYMENT; PROGRESSIVE PAYMENT RATE',
         scenario_l = 'B',
         group = if_else(max(is_paid) == 1, 'paid', 'unpaid'))

# Last Scenario Applies a Flat Rate of 10% Repayment Above Minimum Threshold

df3 <- base_df %>% 
  mutate(repayment_rate = case_when(
           salary_1 < 51550 ~ 0.0,
           salary_1 >= 51550 ~ 0.1), 
         repayment = salary_1 * repayment_rate,
         debt_1 = accumulate(2:n(), .init = first(debt_0),
~ calculate_remaining_debt(.x, repayment[.y], indexation_rate[.y])
),
         repayment = if_else(debt_1 == 0, 0, repayment),
         debt_paid = if_else(debt_1 == 0, 'y', 'n'))

df3 <- 
df3 %>% 
  group_by(index) %>% 
  mutate(clearance_year = if_else(lag(debt_1, default = first(debt_1)) > 0 & debt_1 == 0, 1, 0),
         clearance_cum = cumsum(clearance_year)) %>% 
  filter(clearance_cum == 0 | clearance_year == 1) %>% 
  select(1:11) %>%
  mutate(is_paid = if_else(debt_paid == 'y', 1, 0),
         debt = round(debt_1, 2),
         salary = round(salary_1, 2),
         scenario = 'INDEXATION PRE PAYMENT; FLAT PAYMENT RATE',
         scenario_l = 'C',
         group = if_else(max(is_paid) == 1, 'paid', 'unpaid'))

示例数据框(图片来自作者)

我们将这三种情景合并为一个单一视图,供后续分析使用。下面我们可视化了数据集中前 9 名学生的情况。

df4 <- bind_rows(df, df2, df3)

df4 %>% 
  filter(index <= 9) %>% 
  ggplot(aes(year, debt, color = scenario)) +
    geom_point() +
    geom_line() +
    facet_wrap(~index) +
    theme_ggdist() +
    scale_color_brewer(palette = "Dark2") +
    theme(legend.position = 'bottom', legend.direction = 'vertical') +
    labs(x = 'Year', y = 'Debt', title = 'Scenarios of First Nine Students')

让我们来解析一下,我们已经模拟了上述三种情景。我们模拟了 10%的还款率,这是新西兰对学生债务的做法,从大约新西兰元 25,000 开始。有些人可能会认为这相当苛刻,然而,新西兰不会收取利息,也不会应用指数化,除非毕业生离开该国工作。我认为这是一个合理的情景,值得进行分析。上面的可视化展示了数据集中前 9 名学生在每种情景下的债务轨迹——我们总共模拟了 50,000 名学生。

索引 4 是薪水和债务指数化如何条件独立的一个很好的例子。在当前状态下的前 14 年,债务没有得到任何偿还。其次,直到第 15 年,毕业生的税后薪水才超过了大约 51,000 的强制付款阈值,但在此期间债务增加了近 22,000,并且在 20 年的时间范围内依然无法清偿债务。

低的递增率意味着该学生可能永远无法偿还这笔债务,因为指数化将继续超过当前的还款速度。哪种结果更好:一个毕业生能够还清学生贷款,还是在职业生涯的余生中背负着这个负担,从他们的薪水中扣除低于指数化影响的增量?

我认为这突出了回顾中一个关键的遗漏点,我们在解决什么问题?在我看来,HELP 系统必须使毕业生尽快偿还债务。

债务清偿年数建模

现在我们已经有了包含所有学生模拟数据的数据集,可以评估各个情景下债务清偿所需的年份分布。

df4 %>% 
  filter(group == 'paid') %>% 
  group_by(index, scenario) %>%
  summarise(debt_clearance = max(year)) %>% 
  ggplot(aes(debt_clearance, fill = scenario)) +
    geom_histogram(binwidth = 1) +
    facet_grid(~scenario) +
    theme_ggdist() +
    scale_fill_brewer(palette = "Dark2", aesthetics = c('color', 'fill')) +
    theme(legend.position = 'none') +
    labs(x = 'Years to Debt Clearance', y = 'Count', title = 'Distribution of Years to Debt Clearance by Scenario')

按场景划分的债务清偿年限分布(图源:作者)

目前越来越清楚的是,改变指数化点之间几乎没有差异,然而设置一个比指数化率高得多的偿还率,可以使学生更早偿还债务。让我们完成一个贝叶斯方差分析(ANOVA),以了解后验均值年限的差异。鉴于我们处理的是计数数据,以下我们拟合了三种模型,改变了似然性和参数数量。

set.seed(246)

# Take Sample of Total Dataframe
count_df_sample <- df4 %>%
  filter(group == 'paid') %>% 
  group_by(index, scenario, scenario_l) %>%
  summarise(debt_clearance = max(year))  %>% 
  group_by(scenario_l) %>% 
  slice_sample(n = 4000)

# Poisson Likelihood
m1a <- brm(
  debt_clearance ~ scenario_l + 0,
  data = count_df_sample,
  family = poisson,
  prior = c(prior(gamma(9, 1), class = 'b', lb = 0)),
  chains = 4, iter = 2000, cores = 4, threads = threading(2)
) %>% 
  add_criterion(c('loo', 'waic'),  moment_match = T)

# Negative Binomial Likelihood w/ Pooling
m1b <- brm(
  debt_clearance ~ scenario_l + 0,
  data = count_df_sample,
  family = negbinomial,
  prior = c(prior(gamma(9, 1), class = 'b', lb = 0)),
  chains = 4, iter = 2000, cores = 4, threads = threading(2)
) %>% 
  add_criterion(c('loo', 'waic'),  moment_match = T)

# Establish Prior for Non-Pooling Negative Binomial 

prior <- get_prior(
  bf(debt_clearance ~ scenario_l + 0,
  shape  ~ scenario_l + 0),
  data = count_df_sample,
  family = negbinomial,
  prior = c(prior(gamma(9, 1), class = 'b', lb = 0))) %>% 
  as_tibble() %>% 
  mutate(prior = if_else(class == 'b' & dpar == 'shape' & coef == '', 'gamma(9, 1)',  prior),
         lb = if_else(class == 'b' & dpar == 'shape' & coef == '', '0',  lb),
         prior = if_else(class == 'b' & dpar == '' & coef == '', 'gamma(6, 1)', prior),
         lb = if_else(class == 'b' & dpar == '' & coef == '', '0', lb)) %>% 
  as.brmsprior()

# Negative Binomial Likelihood w/o Pooling

m1c <- brm(
  bf(debt_clearance ~ scenario_l + 0,
  shape  ~ scenario_l + 0),
  data = count_df_sample,
  family = negbinomial,
  prior = prior,
  chains = 4, iter = 2000, cores = 4, threads = threading(2)
) %>% 
  add_criterion(c('loo', 'waic'),  moment_match = T)

loo_compare(m1a, m1b, m1c) %>% print(simplify = F)

LOO 比较结果(图源:作者)

从我们创建的三个模型来看——没有合并的负二项模型显示出更好的外部样本预测能力。为了进行我们的方差分析(ANOVA),我们将使用这个模型。作为参考,情境 A 是预先指数化的渐进式,B 是事后指数化的渐进式,C 是预先指数化的固定税率。

new_df <- tibble(scenario_l = c('A', 'B', 'C'))

m1c %>% 
  tidybayes::epred_draws(new_df, ndraws = 4000, seed = 111) %>% 
  compare_levels(.epred, scenario_l) %>% 
  ggplot(aes(.epred, scenario_l, fill = scenario_l)) + 
    stat_dist_halfeye() +
    geom_vline(xintercept = 0, linetype = 'dashed') +
    theme_ggdist() +
    scale_fill_brewer(palette = "Dark2") +
    labs(x = 'Difference in Posterior Means', y = 'Scenario', title = 'Differences in Posterior Mean Between Each Scenario', fill = 'Scenario')

上述结果告诉我们两件事——首先,改变指数化的时间对 HELP 债务的到期时间几乎没有影响。其次,设置 10% 的固定偿还率将在平均上减少偿还债务的时间约 4 年。

它还表明,债务的时间越短,复利对债务价值的影响就越小,这意味着毕业生将能够重新获得本应支付的薪水部分,从而实现有效的税后收入增长。

结语

这项分析已经以完全透明和可重复的方式定量评估了大学协议审查中提出的建议。

我们的分析旨在提供一种替代现状的最佳情况。我们的模拟结果是乐观的,但并非不切实际,考虑了债务和薪资在 20 年间的增长。

首先,我们评估了长期来看,是否在 CPI 和 WPI 之间交换有意义。根据我们的建模,长期来看,在两种指数之间进行切换对计算指数化的影响几乎可以忽略不计,CPI 更具优势,平均差距为 0.3 个基点。

其次,我们根据上述三种情景,模拟了 50,000 名毕业生在 20 年内的债务和工资轨迹,调整了指数化点并增加了偿还率。然后我们使用这些数据评估了清偿债务的平均年数差异,并注意到改变指数化点不会产生实质性效果,而将偿还率增加到统一的 10%是一个可行的选项,可以让毕业生更快地偿还债务。我们可以通过我们的 DAG 来表达这些陈述,通过增加偿还,我们观察到债务水平下降得更快。此外,指数化的影响与工资无关,因此,任何基于收入水平减轻其影响的尝试都忽略了以下关系。

HELP 债务维护的因果图(作者提供的图片)

这变成了一个前期更多牺牲的案例,但随着时间的推移,避免了指数化对本金债务水平的复利效应。这一情形在一定程度上复制了新西兰的做法,采用一个平坦的、适用于更低最低门槛的广泛税率。

大学协议审查的作者有责任讨论其建议的影响。报告在没有验证或适当模拟的情况下做出了隐含的因果陈述。你不能在一句话中寻求缓解高等教育的财务压力,然后又不提供清晰的成本解决方案,帮助学生通过强制性和自愿性付款更及时地清偿债务。

联邦政府值得肯定地表示,HELP 债务正在审查中,但根据报告中的建议,并根据我们所做的模拟,我怀疑这些措施是否能对改善毕业生的财务状况产生实质性效果。

Sklearn 教程:模块 5

原文:towardsdatascience.com/sklean-tutorial-module-5-b30e08a4c746?source=collection_archive---------16-----------------------#2024-01-10

决策树其实很简单

Yoann MocquinTowards Data Science Yoann Mocquin

·发表于Towards Data Science ·阅读时间:14 分钟·2024 年 1 月 10 日

--

这是我 scikit-learn 教程系列中的第五篇文章。如果你还没读过前几篇,我强烈推荐你阅读前四篇;这样跟着教程走会容易得多。

Yoann Mocquin

Yoann Mocquin

Sklearn 教程

查看列表9 个故事

本模块介绍了决策树。正如我们将看到的,决策树是一种监督学习算法,它通过基于阈值/特征对递归地划分数据,创建一个像树一样的嵌套结构。树的叶子代表模型的预测结果。

个人免责声明:当我第一次开始学习 scikit-learn 教程时,我从未接触过决策树。最多我曾在某些地方听说过这个术语,但完全不知道它意味着什么。所以我对决策树感到好奇,并且担心它比我已知的其他模型,如线性回归模型和支持向量机,更复杂、更难理解。但结果证明,决策树其实简单得多,易于理解!而且它们非常强大!

图片来源:胡晨Unsplash

Python 中的切片:全面指南

原文:towardsdatascience.com/slicing-in-python-a-comprehensive-guide-a609c3bb877c?source=collection_archive---------7-----------------------#2024-06-05

PYTHON 编程

精通 Python 切片:从列表和元组到 NumPy 数组和 Pandas 数据框架,以及自定义实现

Marcin KozakTowards Data Science Marcin Kozak

·发表于Towards Data Science ·30 分钟阅读·2024 年 6 月 5 日

--

图片由freestocks提供,来源于Unsplash

Python 提供了多种数据容器,其中最常见的是列表。从更高的层次来看,它是一个将对象一个接一个地存储在容器中的序列。因此,我们需要一种机制来访问这样的容器中的特定元素——这个机制被称为索引。它的工作方式如下所示,且对列表和元组来说是完全相同的:

>>> objects = [
...     'Zen',
...     float('nan'),
...     25,
...     None,
...     ValueError,
...     ValueError(),
... ]
>>> objects[0]
'Zen'
>>> objects[1]
nan
>>> objects[-1]
ValueError()
>>> objects[-2]
<class 'ValueError'>

索引使得我们能够访问容器中的单个元素——然而,我们通常需要访问不止一个元素。实现这一目标的机制被称为slicing(切片)。

切片是 Python 中最强大且最方便的功能之一,它使得我们能够访问和操作序列的部分内容——如列表、元组、字符串、数组和数据框架,当然也包括自定义序列。在数据科学中,切片是你需要掌握的关键工具之一,因为……

建模缓慢变化维度(SCD)

原文:towardsdatascience.com/slowly-changing-dimensions-6a08dc0386ae?source=collection_archive---------11-----------------------#2024-05-03

深入探讨各种 SCD 类型以及如何在数据仓库中实施它们

Giorgos MyrianthousTowards Data Science Giorgos Myrianthous

·发表于 Towards Data Science ·13 分钟阅读·2024 年 5 月 3 日

--

图片来源:Pawel CzerwinskiUnsplash

在当今动态且竞争激烈的环境中,现代组织大量投资于其数据资产。这项投资确保了整个组织范围内的各个团队——从领导层、产品、工程、财务、市场营销到人力资源——都能做出 明智的 决策

因此,数据团队在帮助组织依赖数据驱动的决策过程中发挥着关键作用。然而,单单拥有一个强大且可扩展的数据平台是不够的;从存储的数据中提取 有价值的 洞察才是关键。这就是数据建模发挥作用的地方。

本质上,数据建模定义了数据如何存储组织访问,以便于提取洞察分析。数据建模的主要目标是确保数据需求和要求得到满足,从而有效支持业务和产品。

数据团队努力为组织提供解锁 其数据的全部潜力的能力,但通常会遇到一个与数据结构相关的大挑战,这影响了相关团队能否进行有意义的分析。这就是为什么建模维度是其中一个……

小型语言模型:在 PC 和树莓派上使用 38 亿 Phi-3 和 80 亿 Llama-3 模型

原文:towardsdatascience.com/small-language-models-using-3-8b-phi-3-and-8b-llama-3-models-on-a-pc-and-raspberry-pi-9ed70127fe61?source=collection_archive---------2-----------------------#2024-05-23

使用 LlamaCpp 和 ONNX 测试模型

Dmitrii EliuseevTowards Data Science Dmitrii Eliuseev

·发表于 Towards Data Science ·阅读时间 17 分钟·2024 年 5 月 23 日

--

图片来自 Jelleke Vanooteghem,Unsplash

如今,我们可以观察到在开发新的 AI 模型方面出现了有趣的变化。长期以来,人们一直知道较大的模型更“智能”,能够完成更复杂的任务。但它们的计算成本也更高。像微软、谷歌和三星这样的巨大设备制造商已经开始向客户推广新的 AI 功能,但显然,如果数百万用户在他们的手机或笔记本电脑上大规模使用 AI,计算云的成本可能会非常庞大。解决方案是什么?显而易见的方法是将模型运行在设备上,这样可以在延迟(无需网络连接,模型可以立即访问)、隐私(无需在云中处理用户响应)以及自然的计算成本方面带来优势。使用本地 AI 模型不仅对笔记本电脑和智能手机非常重要,对于自主机器人、智能家居助手和其他边缘设备也同样重要。

在撰写本文时,至少已有两个专门为设备端运行设计的模型宣布:

小而强大——小型语言模型的崛起

原文:towardsdatascience.com/small-mighty-rise-large-language-models-sml-llm-data-artificial-intelligence-gemma-mistral-future-d812938f2c2a?source=collection_archive---------4-----------------------#2024-05-21

为什么人工智能正在缩小以产生更大的影响

Josep FerrerTowards Data Science Josep Ferrer

·发表于Towards Data Science ·6 分钟阅读·2024 年 5 月 21 日

--

图片来自Guillaume de Germain于 Unsplash 发布。

我们的世界深受大型语言模型(LLM)发布的影响。它们一经推出,就迅速席卷了市场,GPT-3.5 在五天内就吸引了百万用户——这是它们初步成功的一个证明。

但蜜月期已经结束。

今天,人工智能的焦点是实用性、成本降低和效率优化。

因此,“越简单越好”的理念比以往任何时候都更加真实。

虽然最初较大的通用模型被优先考虑,但现在的焦点已经转向效率和成本效益。人工智能的未来正朝着更小、更专业化的模型发展。

因此,企业更倾向于寻求在特定领域表现出色的模型,而不是采用一刀切的方式。

本文探讨了小型语言模型(SML)崛起的原因,以及它们相较于大型语言模型(LLM)所提供的主要优势。

让我们稍微集中精力看看发生了什么,以及接下来会发生什么👇🏻

语言模型

如果你一直生活在山洞里——不过一定是一个非常深的山洞——让我们回顾一下什么是语言模型。

小型更智能

原文:towardsdatascience.com/smaller-is-smarter-89a9b3a5ad9e?source=collection_archive---------2-----------------------#2024-12-01

你真的需要顶级 LLM 的算力来获得莎士比亚风格的提拉米苏食谱吗?

Alexandre AllouinTowards Data Science Alexandre Allouin

·发表于Towards Data Science ·4 分钟阅读·2024 年 12 月 1 日

--

对大型语言模型(LLMs)对环境影响的关注日益增加。尽管关于 LLM 实际成本的详细信息可能难以获得,但我们可以尝试收集一些事实,以了解其规模。

由 ChatGPT-4o 生成

由于关于 ChatGPT-4 的综合数据尚不容易获取,我们可以以 Llama 3.1 405B 为例。这个 Meta 的开源模型可以说是迄今为止最“透明”的大型语言模型(LLM)。根据各种基准测试,Llama 3.1 405B 与 ChatGPT-4 相当,为理解这一范围内的 LLM 提供了一个合理的基础。

推理

运行此模型 32 位版本的硬件要求范围为 1,620 到 1,944 GB 的 GPU 内存,具体取决于来源(substratus, HuggingFace)。为了保守估算,我们使用较低的数字 1,620 GB。为了更直观地理解——尽管这是一个简化的类比——1,620 GB 的 GPU 内存大约相当于 100 台标准的 MacBook Pro(每台 16GB)的总内存。因此,当你向这些 LLM 询问莎士比亚风格的提拉米苏食谱时,需要 100 台 MacBook Pro 的算力才能给出答案。

训练

我正试图将这些数字转化为更具体的概念……虽然这还不包括训练成本,据估计,训练过程涉及大约 16,000 个 GPU,成本约为 6000 万美元(不包括硬件费用)——这是 Meta 的一项重大投资——整个过程大约耗时 80 天。在电力消耗方面,训练需要 11 GWh

在像法国这样的国家,每人年电力消耗大约为 2300 千瓦时。因此,11 GWh 大约相当于 4782 人一年的电力消耗。这一消耗释放了大约 5000 吨二氧化碳当量的温室气体(基于欧洲平均水平),尽管这个数字在不同国家训练模型时可能会轻松翻倍。

作为对比,燃烧 1 升柴油会产生 2.54 千克的二氧化碳。因此,在像法国这样的国家,训练 Llama 3.1 405B 的碳排放大约相当于燃烧约 200 万升柴油。这相当于大约 2800 万公里的汽车行驶。我认为这已经提供了足够的视角……而且我还没提到用于冷却 GPU 的水资源!

可持续性

很显然,人工智能仍处于起步阶段,我们可以预见,随着时间的推移,将会出现更为优化和可持续的解决方案。然而,在这场激烈的竞争中,OpenAI 的财务状况突显了其收入与运营开支之间的巨大差距,尤其是在推理成本方面。预计到 2024 年,公司将花费大约 40 亿美元用于由微软提供的推理工作负载处理能力,而其年收入预计在 35 亿到 45 亿美元之间。这意味着,仅推理成本几乎就等于——甚至超过——OpenAI 的总收入(deeplearning.ai)。

所有这些发生在一个背景下,专家们正在宣布人工智能模型的性能瓶颈(扩展范式)。与之前的飞跃相比,增加模型规模和 GPU 带来的回报显著减少,例如 GPT-4 相比于 GPT-3 取得的进展。Hugging Face 初创公司的研究员兼 AI 负责人Sasha Luccioni表示:“追求通用人工智能(AGI)一直是不现实的,‘越大越好’的人工智能方法最终注定会遇到限制——我认为这正是我们现在看到的现象。”

那现在呢?

但别误会我的意思——我不是在审判人工智能,因为我爱它!这项研究阶段绝对是人工智能发展中的正常阶段。然而,我认为我们需要在使用人工智能时运用常识:我们不能每次都拿火箭筒去打蚊子。人工智能必须变得可持续——不仅是为了保护我们的环境,还为了应对社会分裂的挑战。的确,由于高昂的成本和资源需求,若将全球南方地区甩在人工智能竞赛的后头,将是这场新智能革命中的一大失败。

那么,你真的需要 ChatGPT 的全部功能来处理你 RAG 管道中的最简单任务吗?你是否想控制运营成本?你想完全掌控你的管道流程吗?你是否担心自己的私人数据在网上流通?或者你只是对人工智能的影响保持警觉,并致力于其有意识的使用?

小型语言模型(SLM)可能是一个更聪明的选择!

小型语言模型(SLM)提供了一个值得探索的绝佳替代方案。它们可以在你的本地基础设施上运行,并且与人类智能结合时,可以提供巨大的价值。虽然 SLM 没有统一的定义——例如,2019 年,GPT-2 凭借其 15 亿参数被视为大型语言模型(LLM),但现在已经不再是这样——我指的是像 Mistral 7B、Llama-3.2 3B 或 Phi3.5 这样的模型。这些模型可以在“一台不错的电脑”上运行,从而大大减少碳足迹,并确保在本地安装时数据的机密性。虽然它们不如大型模型多才多艺,但在特定任务中明智地使用时,仍然可以提供显著的价值——同时在环保方面更具优势。

2024 年数据职业所需的软技能

原文:towardsdatascience.com/soft-skills-you-need-for-a-career-in-data-in-2024-e50c63a4b355?source=collection_archive---------4-----------------------#2024-04-20

现在你已经掌握了编程和数据可视化,接下来该做什么?

Rashi DesaiTowards Data Science Rashi Desai

·发表于Towards Data Science ·阅读时长 6 分钟·2024 年 4 月 20 日

--

图片由Andrew Palmer提供,来源于Unsplash

在快速发展的技术世界中,数据相关的职业在过去几年中强劲崛起,能够理解和转化数据的专业人士需求急剧增加。在过去几年里,组织看到数据职业数量和质量的指数级增长,同时对数据职业的兴趣也在急剧上升。

然而,今天的就业市场比以往任何时候都更加难以突破。由于疫情的延迟影响,许多公司冻结了招聘并裁员,这让数据行业的专业人士感到不安。依我之见,虽然你可能具备了正确的硬技能,但这远远不够。招聘经理最关心的问题是:你将如何创造独特且全面的价值。

当我在 2020 年开始我的数据职业生涯,担任百事可乐的数据科学实习生时,我有很多需要学习的东西。通过一年的实习,我学到了很多,放弃了许多,并逐渐意识到,软技能和硬技能一样重要(有时甚至更重要),比如编程和数据处理。

SOFTS:时间序列预测的最新创新

原文:towardsdatascience.com/softs-the-latest-innovation-in-time-series-forecasting-dbc82553dd76?source=collection_archive---------1-----------------------#2024-06-11

了解 SOFTS 的架构及其创新的 STAD 模块,并在 Python 中应用于预报项目。

Marco PeixeiroTowards Data Science Marco Peixeiro

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

--

图片来源:Vincent van Zalinge 通过 Unsplash

近年来,深度学习已成功应用于时间序列预测,其中新的架构不断设定了最先进性能的新标准。

一切始于 2020 年的N-BEATS,随后在 2022 年推出了NHITS。2023 年,PatchTSTTSMixer相继提出,它们依然位列顶级预测模型之中。

最近,我们发现了iTransformer,它进一步提升了深度学习预测模型的性能。

现在,我们介绍Series-cOre Fused Time Series 预报模型,简称SOFTS

该模型在 2024 年 4 月发表于文章SOFTS: Efficient Multivariate Time Series Forecasting with Series-Core Fusion,采用了一种集中式策略来学习不同序列之间的交互,从而在多元预测任务中取得了最先进的性能。

在本文中,我们将详细探讨 SOFTS 的架构,并发现一个新颖的STar Aggregate-Dispatch(STAD)模块,该模块负责学习时间序列之间的交互。接着,我们应用…

通过工件流水线解决 GPT 分支问题

原文:towardsdatascience.com/solve-the-gpt-branching-problem-with-an-artifact-pipeline-44024398c15f?source=collection_archive---------11-----------------------#2024-01-09

使用自定义 GPT 设计一个饮食规划应用

Andrew LucasTowards Data Science Andrew Lucas

·发布于Towards Data Science ·14 分钟阅读·2024 年 1 月 9 日

--

“分支问题”,由作者使用 ChatGPT 创建

“创建一个应用帮助我规划一周的饮食。”

想象一下,如果你可以要求 ChatGPT 实现一个完整的应用并提供下载链接。尽管目前的技术还不足以实现这一点,但 AI 已经能够自动化一些工作流程的部分内容,例如需求定义、任务规划,甚至是编码。

当提供正确的指示时,ChatGPT(或自定义 GPT)可以帮助递归地分解问题。当我们将问题划分为子问题时,我们需要跟踪所有生成的不同“分支”。这些分支会进一步分裂成更多的分支,形成一个复杂的树状结构。我称之为分支问题

我们将探讨几种方法,使用基于 GPT 的工具创建明确输出的工件,作为应用设计流程的一部分。最后,我将解释这如何帮助克服分支问题。

AI 免责声明:除了在我明确引用 ChatGPT 以阐述观点时,本故事的文字内容并未使用 AI 生成。

像人类一样思考

首先让我们思考一下人类是如何处理我的应用请求的。假设我在询问一个实际上有能力满足这个请求的人,比如一个软件公司项目经理。

最初的请求不够具体。一个人类项目经理会知道立即开始提问。

什么样的应用?

它可以是一个网站,一个移动应用,甚至是类似于 Slack 的机器人。在这种情况下,我更倾向于一个移动应用以及一个适配手机的网页应用,但我也可以接受其中任何一个。

我希望如何进行餐单规划?

事实证明,我已经思考了这一点。对于我来说,有一些食材是我偏好购买并使用的,所以我希望这个应用能利用 AI,根据我提供的食材清单生成食谱。我已经用 ChatGPT 做过这件事,所以我知道这是可行的。

我通过实验知道,ChatGPT 可以估算食材的卡路里和宏量营养素含量,所以我希望这能被包含在内。我还知道 ChatGPT 倾向于低估这些量,因此我希望有某种方式来处理这个问题,无论是通过手动审核(更安全)还是通过某种百分比自动调整这些值(更懒)。

当我向软件公司提出请求时,我早就已经考虑到了所有这些内容,但我不会把它们直接作为信息块呈现给他们。我会让他们在对话过程中逐步提取细节。这减少了我的认知负担,并且让公司的代表能在适当的速度下整合这些信息,记笔记或按照他们自己的接收流程进行。

这是人类自然的沟通方式,但 ChatGPT 并不知道我希望它问我澄清性问题。

ChatGPT 并不知道自己不知道什么

这是当我问“创建一个帮助我制定一周餐单的应用”时,GPT-4 的回答。为了简洁,回答已被缩短。

创建一个应用涉及几个关键步骤:

1. 定义应用的目的和功能

2. 设计用户界面(UI)

3. 选择开发方法

4. 开发应用

5. 测试应用

6. 启动并推广应用

7. 维护和更新

这是一个简化的概述。每个步骤都包含基于你具体需求和资源的详细过程和决策。

ChatGPT 的回答令人沮丧且无用。

我已经知道了所有这些。更糟糕的是,ChatGPT 会对每一点进行痛苦的详细解释,告诉我什么是开发一个应用。

如果我在这里停下来,可能会得出结论认为 AI 很笨,不懂得如何真正构建一个应用,但我会错的。

告诉它你想要的

显而易见的第一个解决方案就是直接告诉 ChatGPT 你希望它做什么。OpenAI 为我们提供了一个方便的功能叫做自定义指令,允许我们在与它开始对话之前,给 ChatGPT 额外的指令,以便它能够更好地理解我们的需求

这是Andrew Mayne(前Prompt Whisper)推荐的几条自定义指令:

由作者拍摄的 ChatGPT 自定义指令截图

Mayne 的指令简洁而有用,提供了三个有价值的功能:

  • 这些指令建议 ChatGPT 应该问你澄清性问题。也许更好的是将“应该”明确指出。

  • 它们要求 ChatGPT 提供多种澄清方式。这减少了您澄清想法时所需的认知负担,通过让您从列表中选择而非从零开始生成。

  • 问题按字母顺序组织,您只需回复字母及任何想要添加的内容即可。

拥有这些自定义指令后,ChatGPT 给出的回复更加有趣:

设计一个餐点规划应用是个好主意!为了根据您的需求量身定制,我有几个问题可以帮助引导开发过程:

1. 用户体验与界面:

A. 您偏好简洁和极简的设计,还是更鲜艳和互动的设计?

B. 应用是否应该包括诸如餐点照片等视觉元素,还是保持文本为主?

2. 功能性与特性:

A. 您希望应用根据饮食偏好(例如,素食、酮饮食、无麸质)推荐餐点吗?

B. 应用是否应该包括一个根据餐点计划更新的购物清单功能?

C. 您希望有一个功能来追踪每餐的营养信息吗?

D. 与智能家居设备或其他应用(如健身追踪器)的集成会有用吗?

3. 个性化与数据输入:

A. 应用是否允许您输入个人数据(如体重、身高、饮食限制)以进行个性化餐点规划?

B. 您希望能够手动输入或编辑餐点计划吗?

C. 是否希望有一个根据您的偏好随着时间变化学习的功能?

4. 附加功能:

A. 您希望有一个社区功能,例如与其他用户分享餐点计划或食谱吗?

B. 应用是否应该包含烹饪教学视频或食谱链接?

C. 是否希望有一个功能,用于规划特殊场合(如假期或聚会)的餐点?

随时回答这些问题,或者添加您认为对理想餐点规划应用至关重要的其他功能!

这样更有帮助。也许 ChatGPT 真的可以帮助构建应用!

不幸的是,Andrew Mayne 的三条指令不足以创建一个真正有用的工作流助手。

ChatGPT 的问题

在使用 ChatGPT 一段时间后,您会注意到一些问题:

  1. ChatGPT 会问很多问题,而您需要跟踪所有这些问题。最终,您将拥有一个分支树形结构的可能性,您需要以某种方式维护它。这就是分支问题

  2. 它所提的问题大多是随机的。两次对话可能会有完全不同的问题集,从而导致不同的应用设计。而且,您怎么知道什么时候停止回答问题呢?事实上,我们需要一种方法来找到最佳设计。

  3. 您的对话线程越长,ChatGPT 忘记自定义指令并恢复提供那些令人烦恼的模糊概述列表的可能性就越大。根据我的经验,ChatGPT 最适合在短时间内使用。

我们将看看解决这些问题的两种不同方法,特别是在开发餐点规划应用的背景下:

  • GPT Pilot 代理

  • OpenAI 的新自定义 GPTs

代理方法:GPT Pilot

GPT Pilot 项目通过创建多个具有明确角色和输出的 AI 代理,实现了一个概念验证的软件开发流程。

来自 GPT Pilot Github 页面(链接见上)的截图

它们有一个产品负责人角色,负责创建用户故事,一个架构师角色,决定技术需求,一个技术负责人角色,负责将工作拆分成开发任务,以及一个开发者角色,负责编写代码。

它们使用 OpenAI API 在这些角色中创建一个或多个代理。可以将“代理”看作是一个单独的 AI 实例。用户将通过一个工作流,协作开发 Web 应用程序,与每个代理合作。

首先,产品负责人代理向用户询问关于应用程序想法的澄清问题。然后,架构师代理建议使用特定的软件框架。技术负责人代理创建开发任务队列,开发者代理则依次处理每个任务。

这个策略看起来是朝着正确方向迈进的一步。AI 代理无法完全独立完成工作流程中的每个步骤,但它们可以通过引导用户通过预定义的流程,执行部分工作,然后要求用户提供更多信息,并最终批准或拒绝产物,从而显著加速开发过程。如果代理收到用户的拒绝,它将继续迭代。如果代理收到批准,则可以继续开发流程的下一步。

实际上,我发现 GPT Pilot 目前还不可用。我花了几分钟时间,尝试使用它设置一个包含 Python Flask 后端的 React.js 应用,而这是 ChatGPT 能轻松指导用户完成的任务。

GPT Pilot 代理经常会忘记它们正在运行的目录。总体而言,这次测试我花费了大约 1.50 美元的 OpenAI API 使用费用。虽然不多,但由于它经常且早期失败,我决定它尚不能与 ChatGPT 竞争。我们会给他们一些时间修复漏洞,并使工具更加灵活。

自定义 GPT 方法

与此同时,我们可以实现一个类似的策略——将任务分解成步骤——但是与其使用基于 API 的代理来执行每个步骤,不如使用自定义 GPTs。

使用自定义 GPTs 流程时,我们无需为使用付费——只需支付每月 20 美元的 ChatGPT Plus 订阅费用。

一个缺点是,自定义 GPT 有一个每个用户每 3 小时 50 次请求的使用限制

3 小时除以 50 次请求大约等于 每 4 分钟一次请求

我个人从未达到过这个限制,我认为只要不频繁浪费请求于模糊或不准确的提示,达到这一限制的可能性不大。我可以在不到 4 分钟的时间内将 ChatGPT 生成的代码集成到我的代码库中,但瓶颈在于搞清楚接下来应该向 ChatGPT 提出什么问题。通常,这一过程平均需要超过 4 分钟。

人类思考速度并不快……我们需要大量的咖啡休息时间。

示例:需求收集 GPT

创建一款餐单规划应用的第一步是定义应用程序需要执行的实际需求。有多种定义需求的方法,但敏捷开发方法建议从最终用户的角度来定义需求。我们称之为用户故事

下面是一些针对餐单规划应用的用户故事示例:

As a user, I want to create an account to access and personalize my experience.
As a user, I want to input a list of ingredients and receive meal recipe suggestions.
As a user, I want to add meals to a customizable weekly meal plan.
As a user, I want to adjust the number of servings for each meal in the plan.
As a user, I want to generate a grocery list based on my weekly meal plan.
As a user, I want to view a recipe with its title, description, ingredients, macronutrient stats, and total price.
As a user, I want to edit ingredient quantities and prices in a recipe.
As a user, I want to set dietary restrictions in my profile settings.
As a user, I want to save my favorite recipes and meal plans for future use.

注意: 从技术上讲,这些应该包括用户为什么要执行这些操作,但为了简便起见,我将其省略了。

我为需求收集创建了一个定制的 GPT,使用了以下指令:

You are an experienced software project manager who manages the entire process of creating software applications for clients from the client specifications to the development. You are talking to a client who wants your team to develop an application for them.

// 1\. DO be concise and to-the-point.
// 2\. DO ensure every word you say has a very specific purpose.
// 3\. DO NOT repeat yourself.
// 4\. DO NOT use pleasantries and formalities like "good morning" and "hello".
// 5\. DO focus on listening to the client.

GOAL: Gather requirements and create a collection of user stories for the application that will be provided to a software architect for system design.

THINK STEP BY STEP to perform the following steps:
1\. ASK the client for a description of their app.
2\. ITERATE through the following steps in a loop:
    * STEP 1: SUMMARIZE the app requirements so far.
    * STEP 2: ASK the client to either answer 3 clarifying questions about the app OR approve the the requirements summary as-is.
    * STEP 3: IF client has chosen to answer the clarifying questions rather than approving, LOOP to SUMMARIZE and ASK questions again.
3\. Once the requirements are approved, WRITE user stories into a file in MARKDOWN FORMAT. The file should contain bullet items ONLY.
4\. PROVIDE that file as a DOWNLOAD.

EXAMPLE USER STORIES for a to-do list app:
- As a user, I want to press a button to create a new to-do list.
- As a user, I want to enter a new todo list item using a text input.
- As a user, I want to delete any list item at any point.
- ...

Conduct the conversation as though you are talking to the client.

1. 提供人物角色

You are an experienced software project manager

在其默认角色“有帮助的助手”中,ChatGPT 给出的建议相当无聊。避免这种情况的方法是给定制 GPT 设定一个具体的人物角色。

2. 定义语气

// 1\. DO be concise and to-the-point.
// 2\. DO ensure every word you say has a very specific purpose.
// 3\. DO NOT repeat yourself.
// 4\. DO NOT use pleasantries and formalities like "good morning" and "hello".
// 5\. DO focus on listening to the client.

我基于实际的 ChatGPT 定制指令格式来设计了这个特别的提示格式,该格式几周前通过一个黑客方法可以访问。结合“//”字符、列表项和大写字母可以让模型更好地关注指令。

3. 提供目标

GOAL: Gather requirements and create a collection of user stories for the application that will be provided to a software architect for system design.

相当简单——只需告诉模型你想做什么。

4. 提供方法

THINK STEP BY STEP to perform the following steps:
1\. ASK the client for a description of their app.
2\. ITERATE through the following steps in a loop:
    * STEP 1: SUMMARIZE the app requirements so far.
    * STEP 2: ASK the client to either answer 3 clarifying questions about the app OR approve the the requirements summary as-is.
    * STEP 3: IF client has chosen to answer the clarifying questions rather than approving, LOOP to SUMMARIZE and ASK questions again.
3\. Once the requirements are approved, WRITE user stories into a file in MARKDOWN FORMAT. The file should contain bullet items ONLY.
4\. PROVIDE that file as a DOWNLOAD.

正如 OpenAI 的 Andrej Karpathy 所说,“最火的新编程语言是英语”

问题在于,英语并不擅长给出详细且精确的指令。这也是我们最初发明编程语言的原因!

在这个例子中,我设计了自己的伪代码来告诉模型如何生成用户故事。我很惊讶于模型能够遵循我给出的循环。这个方法为提示设计开辟了许多可能性。

5. 给出示例

EXAMPLE USER STORIES for a to-do list app:
- As a user, I want to press a button to create a new to-do list.
- As a user, I want to enter a new todo list item using a text input.
- As a user, I want to delete any list item at any point.

如果你希望你的 GPT 始终输出相同格式,提供示例!这是告诉它你期望输出哪种格式的最佳方法。

6. 采用人物角色

Conduct the conversation as though you are talking to the client.

GPT 应该直接进入人物角色,因此我在指令的最后加上了这一行,要求直接与客户对话。

工具化解决了分支问题

分支问题

自由形式的对话通常呈现分支树状结构,每一轮对话都会开启多个不同的可能性,指引对话走向不同的方向。通常我们从大局开始,然后逐步钻研细节。不幸的是,我们每次只能走一条路径,且在经历了多个分支后,很难回溯并探索其他路径。

例如,我必须决定如何开始阐述我为餐饮计划应用编写的用户故事。我可以选择为用户账户管理、构建食材清单、生成餐点创意或生成购物清单添加用户故事,但一次只能探索一个功能集。最终,我将需要回溯并定义其他功能。我将需要遍历整个功能树。

工件作为共享内存

我们可以通过构建工件来处理分支。

由于我正在使用 GPT 构建一个特定的工件——一个用户故事列表,因此更容易“返回”到我错过的先前分支。我可以随时查看列表中的任何主题,看看哪些需要进一步阐述,然后请求 GPT 添加更多用户故事。通过这种方式,工件作为我的外部记忆。

更重要的是,这种方法还迫使 GPT 在每次迭代时保持工件的最新版本,因为工件在每次更新并重复。当我完成详细说明某个特定功能(如餐点生成)后,GPT 会输出整个更新后的用户故事列表。我可以立即继续处理另一个话题,如购物清单生成,而无需重新向 GPT 解释整个应用程序。

该工件作为用户和自定义 GPT 之间的外部共享内存。

多级工件

在定义需求时,我将使用平面列表作为我的输出工件,但你也可以通过使用多级列表在工件中模拟树状结构。对于无序组件使用项目符号,对于顺序使用编号列表。

例如,如果我正在概述一个应用程序的软件模块,可能需要将设计拆分为通用模块,然后为每个高层模块添加几个子模块。例如,我可能想要一个“餐单计划”模块,其中包含“编辑餐单计划”和“生成购物清单”的子模块。这个例子有点牵强,但多级设计对于更复杂的项目是很有用的。

工件为管道提供数据

工件为你和模型提供共享内存,但它们也可以用于连接管道的不同部分。用户故事必须在确定技术需求之前定义。技术需求必须在设计用户界面或编写代码之前概述。

总结来说,不要只与 ChatGPT 交流。创建构建特定工件的自定义 GPT。将自定义 GPT 链接在一起,形成一个管道。

总有一天,人工智能将能够独立执行整个管道。你会向 ChatGPT 请求一个应用程序,它将通过Neuralink读取你的思维,并提供一个 zip 文件供你下载。

与此同时,使用 AI 自动化过程并执行工作流中繁琐的部分。这仍然是计算机最擅长的事情。

如果你对 AI 或智能理论感兴趣,可以查看我在 Substack 上的出版物,Agentic AI

用量子退火解决受限项目调度问题

原文:towardsdatascience.com/solving-a-constrained-project-scheduling-problem-with-quantum-annealing-d0640e657a3b?source=collection_archive---------10-----------------------#2024-08-20

用 D-Wave 的混合受限二次模型(CQM)解决资源受限项目调度问题(RCPSP)

Luis Fernando PÉREZ ARMAS, Ph.D.Towards Data Science Luis Fernando PÉREZ ARMAS 博士

·发表于Towards Data Science ·23 分钟阅读·2024 年 8 月 20 日

--

为什么那只狗没能通过量子力学课程?因为它无法理解“超爪重叠”(super-paws-ition)的概念。量子叠加是指一个量子系统可以同时处于多个状态,直到被测量为止,这时它会坍缩为其中一个可能的状态。(图片由 DALLE-3 生成)

我非常激动与您分享这篇文章,因为它与我当前的研究密切相关:利用量子计算优化项目调度。我对这个话题的热情源于我作为项目经理的背景以及我对量子计算如何帮助解决复杂优化问题的持续研究。在我的博士研究期间,我特别关注了今天的量子技术如何被用来解决项目管理领域特有的复杂调度问题。

我们正生活在一个令人难以置信的时代,量子计算机不再只是一个概念——它们已经真实存在并开始被使用。想象一下理查德·费曼会如何看待它们!然而,这些机器还未完全准备好。它们通常被称为 NISQ(噪声中等规模量子)设备,这一术语由量子计算先锋约翰·普雷斯基尔提出。这些机器仍然被视为我们未来期望看到的更先进量子计算机的早期版本。

目前,它们很小,只能使用少量量子位(qubits,量子信息位)。作为对比…

利用数学规划和列生成方法求解资源规划问题

原文:towardsdatascience.com/solving-a-resource-planning-problem-with-mathematical-programming-and-column-generation-07fc7dd21ca6?source=collection_archive---------2-----------------------#2024-06-05

使用 Python 和 Gurobipy 求解图着色问题的列生成方法

Luis Fernando PÉREZ ARMAS, Ph.D.Towards Data Science Luis Fernando PÉREZ ARMAS, Ph.D.

·发布于 Towards Data Science ·阅读时间:22 分钟·2024 年 6 月 5 日

--

毫无疑问,最好的资源规划者(图片由 DALLE-3 创建)

我曾在石油和天然气行业工作,我们面临的最棘手问题之一就是资源规划和预测。我和我的团队在这一方面常常遇到困难,经常是临时应对,不断扑灭火灾 🚒。我们仅仅是对局势做出反应,希望找到解决方案。但正如马克·沃尔伯格在电影中所说:“希望不是一种战术”。

“但即兴发挥只能带你走到下一个危机的边缘,永远不能替代提前考虑几步并规划到最后的思维。”

罗伯特·格林《权力的 48 条法则》

我一直觉得必须有更好的方法来解决这个问题,但当时我并不知道 🤔。事实上,获得解决这些问题的技能是我继续学习的主要动力之一。本文探讨了数学优化的一个具体应用,以及它如何能让我的工作当时变得更加轻松。我想,我写这篇文章是送给过去的自己🎁。

我曾在一家提供钻探和测量服务的公司工作。该公司提供工具……

使用 SOLID 在 Python 中解决网球重构挑战

原文:towardsdatascience.com/solving-a-tennis-refactoring-challenge-in-python-using-solid-f1282f85e7e0?source=collection_archive---------9-----------------------#2024-02-05

逐步展示如何使用 SOLID 原则解决重构挑战

Tomer GabayTowards Data Science Tomer Gabay

·发表于 Towards Data Science ·阅读时间 9 分钟 ·2024 年 2 月 5 日

--

图片来源:Lucas DaviesUnsplash

介绍

代码重构挑战在软件工程师中广为人知,但数据科学家对其了解较少,尽管数据科学家也能从练习这些挑战中获得很大收益。通过练习这些挑战,特别是在应用 SOLID 原则时,你能学到如何编写更好的代码;代码不仅模块化、质量高,而且面向对象。作为数据科学家,掌握 SOLID 原则能显著提升数据科学项目的质量和可管理性。这在团队中尤为重要,尤其是在那些大多数数据科学家起源于统计学家和数学家的团队,他们可能不像软件工程师那样熟悉编程基础。

网络上有许多重构挑战。也许最著名的是 Gilded Rose Kata。另一个有趣的重构挑战是 Tennis Refactoring Kata,我们将在本文中讨论它。

通常这些挑战被称为 kata。在“重构 kata”这一概念中,kata 一词借鉴自武术,指的是一种结构化的练习程序。在武术中,kata 是一系列动作和技巧的组合……

使用神经网络求解微分方程

原文:towardsdatascience.com/solving-differential-equations-with-neural-networks-4c6aa7b31c51?source=collection_archive---------2-----------------------#2024-02-06

神经网络如何成为解决微分方程的强大工具,无需使用训练数据

Rodrigo SilvaTowards Data Science Rodrigo Silva

·发表于Towards Data Science ·阅读时间:8 分钟·2024 年 2 月 6 日

--

图片由Linus Mimietz提供,来源于Unsplash

微分方程是自然科学中的重要工具,广泛应用于工程学、生物学、经济学,甚至社会科学。简而言之,它们告诉我们某一量如何随时间(或其他参数,通常我们关注时间变化)变化。我们可以理解某个种群、股价,甚至某个社会对于某些主题的意见如何随时间变化。

通常,解决微分方程的方法并不是解析性的(即没有“封闭式解”),我们必须依赖数值方法。然而,从计算的角度来看,数值方法可能会非常昂贵,更糟糕的是:积累的误差可能会非常大。

本文将展示神经网络如何成为解决微分方程的有力助手,并且我们如何借鉴物理启发式神经网络的概念来解决这样一个问题:我们能否使用机器学习的方法来求解微分方程?

一点点物理启发式神经网络的元素

在本节中,我将简要介绍物理启发式神经网络。我猜你已经了解“神经网络”部分,但是什么使它们受物理学启发呢?嗯,它们并不是直接受物理学启发,而是受(微分)方程的启发。

通常,神经网络被训练来发现模式,并弄清楚一组训练数据的行为。然而,当你训练一个神经网络以遵循你的训练数据的行为,并希望它能适应未见过的数据时,你的模型高度依赖于数据本身,而不是系统的基本性质。这听起来几乎像是一个哲学问题,但实际上比这更具实用性:如果你的数据来自海洋洋流的测量数据,那么这些洋流必须遵循描述洋流的物理方程。然而,请注意,你的神经网络对这些方程完全无知,只是在试图拟合数据点。

这就是物理信息化发挥作用的地方。如果除了学习如何拟合你的数据外,你的模型还学习如何拟合支配该系统的方程,那么你的神经网络的预测将更加精确,并且更好地泛化,这只是物理信息化模型的一些优势。

请注意,系统的控制方程不一定要涉及物理学,“物理信息化”仅仅是一种术语(而且这一技术通常由物理学家使用)。如果你的系统是一个城市的交通,并且你恰好拥有一个想让神经网络预测遵循的良好数学模型,那么物理信息化神经网络非常适合你。

我们如何将这些信息传递给模型呢?

希望我已经说服你,值得费心让模型意识到支配我们系统的基本方程。然而,我们如何做到这一点呢?有几种方法,但主要的一种是调整损失函数,使其包含一个额外的项,用于表示控制方程,除了通常与数据相关的部分。也就是说,损失函数 L 将由以下和构成:

这里,数据损失是通常的形式:均方差,或者是其他合适的损失函数形式;但方程部分才是最吸引人的部分。假设你的系统由以下微分方程支配:

我们如何将其纳入损失函数呢?嗯,由于训练神经网络的任务是最小化损失函数,我们希望最小化以下表达式:

所以,我们的方程相关损失函数最终变成了

也就是说,它是我们微分方程的均方差。如果我们能够最小化这个(也就是让这个项尽可能接近零),我们就自动满足了系统的控制方程。相当聪明,对吧?

现在,损失函数中的额外项 L_IC 需要被处理:它考虑了系统的初始条件。如果一个系统的初始条件未给出,那么该微分方程将有无穷多解。例如,从地面上抛出的球的轨迹与从 10 楼抛出的球轨迹由相同的微分方程决定;然而,我们知道这两个球的轨迹不会相同。这里变化的是系统的初始条件。我们的模型如何知道我们指的是哪些初始条件呢?此时,自然可以通过损失函数项来强制这一点!对于我们的微分方程,我们规定当 t = 0 时,y = 1。因此,我们希望最小化一个初始条件损失函数,表达式如下:

如果我们最小化这一项,就能自动满足系统的初始条件。现在,剩下需要理解的是如何利用这个方法来求解微分方程。

求解微分方程

如果一个神经网络既可以通过损失函数中的数据相关项进行训练(这通常是在经典架构中进行的),也可以通过数据和方程相关项同时训练(这就是我刚刚提到的物理信息神经网络),那么它一定也可以被训练来最小化仅仅方程相关的项。这正是我们将要做的!这里使用的唯一损失函数将是 L_equation。希望下面的图示能帮助说明我刚才说的内容:今天我们目标是右下角类型的模型,即我们的微分方程求解神经网络。

图 1:展示不同神经网络类型与其损失函数关系的图示。在本文中,我们的目标是右下角的模型。图片由作者提供。

代码实现

为了展示我们刚刚学到的理论,我将使用 Python 代码实现提出的解决方案,并使用 PyTorch 库进行机器学习。

首先要做的是创建一个神经网络架构:

import torch
import torch.nn as nn

class NeuralNet(nn.Module):
    def __init__(self, hidden_size, output_size=1,input_size=1):
        super(NeuralNet, self).__init__()
        self.l1 = nn.Linear(input_size, hidden_size)
        self.relu1 = nn.LeakyReLU()
        self.l2 = nn.Linear(hidden_size, hidden_size)
        self.relu2 = nn.LeakyReLU()
        self.l3 = nn.Linear(hidden_size, hidden_size)
        self.relu3 = nn.LeakyReLU()
        self.l4 = nn.Linear(hidden_size, output_size)

    def forward(self, x):
        out = self.l1(x)
        out = self.relu1(out)
        out = self.l2(out)
        out = self.relu2(out)
        out = self.l3(out)
        out = self.relu3(out)
        out = self.l4(out)
        return out

这只是一个简单的 MLP,使用 LeakyReLU 激活函数。然后,我将定义损失函数,在训练循环中稍后进行计算:

# Create the criterion that will be used for the DE part of the loss
criterion = nn.MSELoss()

# Define the loss function for the initial condition
def initial_condition_loss(y, target_value):
    return nn.MSELoss()(y, target_value)

现在,我们将创建一个时间数组,作为训练数据,并实例化模型,同时选择优化算法:

# Time vector that will be used as input of our NN
t_numpy = np.arange(0, 5+0.01, 0.01, dtype=np.float32)
t = torch.from_numpy(t_numpy).reshape(len(t_numpy), 1)
t.requires_grad_(True)

# Constant for the model
k = 1

# Instantiate one model with 50 neurons on the hidden layers
model = NeuralNet(hidden_size=50)

# Loss and optimizer
learning_rate = 8e-3
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)

# Number of epochs
num_epochs = int(1e4)

最后,让我们开始训练循环:

for epoch in range(num_epochs):

    # Randomly perturbing the training points to have a wider range of times
    epsilon = torch.normal(0,0.1, size=(len(t),1)).float()
    t_train = t + epsilon

    # Forward pass
    y_pred = model(t_train)

    # Calculate the derivative of the forward pass w.r.t. the input (t)
    dy_dt = torch.autograd.grad(y_pred, 
                                t_train, 
                                grad_outputs=torch.ones_like(y_pred), 
                                create_graph=True)[0]

    # Define the differential equation and calculate the loss
    loss_DE = criterion(dy_dt + k*y_pred, torch.zeros_like(dy_dt))

    # Define the initial condition loss
    loss_IC = initial_condition_loss(model(torch.tensor([[0.0]])), 
                                     torch.tensor([[1.0]]))

    loss = loss_DE + loss_IC

    # Backward pass and weight update
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

注意使用 torch.autograd.grad 函数自动对输出 y_pred 关于输入 t 进行求导,以计算损失函数。

结果

训练后,我们可以看到损失函数迅速收敛。图 2 显示了损失函数随训练轮数变化的图像,插图展示了损失函数下降最快的区域。

图 2:按历时(epochs)变化的损失函数。在插图中,我们可以看到收敛最为迅速的区域。图像来源:作者。

你可能已经注意到,这个神经网络并不是一个常规的神经网络。它没有训练数据(我们的训练数据是一个手工制作的时间戳向量,简单来说就是我们希望研究的时间域),因此它从系统中获得的所有信息都以损失函数的形式呈现。它唯一的目的就是在它被设计用来解决的时间域内求解微分方程。因此,要测试它,公正的方法是使用它训练时的时间域。图 3 显示了神经网络预测与理论答案(即解析解)之间的对比。

图 3:神经网络预测与微分方程解析解的预测对比。图像来源:作者。

我们可以看到两者之间的高度一致性,这对于神经网络来说非常好。

这种方法的一个警告是,它对于未来的时间不太适用。图 4 显示了如果我们将时间数据点向前滑动五步会发生什么,结果就是一片混乱。

图 4:神经网络和解析解对于未见数据点的预测。图像来源:作者。

因此,这里得到的教训是,这种方法是作为一个数值解法器,用于在时间域内解决微分方程,不能被当作常规神经网络用来对未见的、超出训练域的数据进行预测并期望它能很好地泛化。

结论

毕竟,剩下的一个问题是:

为什么要训练一个不能很好地泛化到未见数据的神经网络,并且更糟糕的是,它显然比解析解更差,因为它具有内在的统计误差?

首先,这里提供的示例是一个已知解析解的微分方程的例子。对于未知解,仍然必须使用数值方法。然而,正如所说,微分方程求解的数值方法通常会积累误差。这意味着如果你尝试在多个时间步长中求解方程,解的准确性会随着时间的推移而丧失。另一方面,神经网络解法器学习如何在每个训练历时中为所有数据点求解微分方程。

另一个原因是,神经网络擅长插值,因此,如果你想知道未见数据中函数的值(但这个“未见数据”必须位于你训练过的时间区间内),神经网络将迅速给出一个值,而经典的数值方法则无法迅速提供。

参考文献

[1] Marios Mattheakis 等,用于求解运动方程的哈密顿神经网络arXiv 预印本 arXiv:2001.11107v5,2022 年。

[2] Mario Dagrada, 介绍物理信息神经网络,2022 年。

我是如何通过回溯法解决 LinkedIn Queens 游戏的

原文:towardsdatascience.com/solving-linkedin-queens-game-cfeea7a26e86?source=collection_archive---------8-----------------------#2024-09-07

使用 OpenCV 自动检测拼图并重新绘制最终答案

Shanmukha RanganathTowards Data Science Shanmukha Ranganath

·发布于 Towards Data Science ·10 分钟阅读·2024 年 9 月 7 日

--

LinkedIn 最近推出了一个游戏功能,鼓励忙碌的专业人士抽出片刻时间,做一些既能激发思维又完全放松的事情。这些游戏提供了一个从工作中快速休息的机会,帮助你重新调整思维,以更专注的状态回到任务中。通过这些游戏,LinkedIn 旨在培养创造力,提升解决问题的能力,并重新点燃职场关系。

他们自己这样说:

游戏?在 LinkedIn 上?

是的,没错。

每年,我们都会研究世界上最好的工作场所。事实证明,深化和重新点燃职场关系的最佳方式之一,简单来说,就是一起玩乐。

所以,我们很高兴推出三款思维导向的游戏——Pinpoint、Queens 和 Crossclimb——它们能帮助你实现这一目标。

与你的联系建立竞争,激发对话,打破沉默。游戏促进关系,而关系是我们所做一切的核心。

这个功能最初引起了不同的反响,有人表示它偏离了 LinkedIn 的核心目标和初衷,然而后来的评价都变得非常积极。近期的...

解决 2023 年大语言模型推理问题

原文:towardsdatascience.com/solving-reasoning-problems-with-llms-in-2023-6643bdfd606d?source=collection_archive---------1-----------------------#2024-01-06

Zhaocheng ZhuTowards Data Science Zhaocheng Zhu

·发表于 Towards Data Science ·阅读时间 17 分钟·2024 年 1 月 6 日

--

现在是 2024 年初,ChatGPT 刚刚庆祝了一周年。对于大语言模型社区来说,一年是非常漫长的时间,这一年里发生了无数有趣的工作。让我们回顾一下这一年的进展,并讨论未来一年的话题。

智能体学院:大语言模型正在从教科书中提取知识并进行推理。图像由作者和 DALL·E 3 生成。

这篇文章由 Michael Galkin (英特尔 AI 实验室), Abulhair Saparov (纽约大学), Shibo Hao (加州大学圣地亚哥分校) 和 Yihong Chen (伦敦大学学院与 Meta AI 研究) 合著。文章中的许多见解是在与 Emily Xue (谷歌), Hanjun Dai (谷歌 DeepMind) 和 Bruno Ribeiro (普渡大学) 的富有成果的讨论中形成的。

目录

  1. 简介

  2. 工具使用

    1. 上下文学习使得使用更多工具成为可能

    2. 最常用的工具:代码解释器和检索工具

    3. 让大语言模型创建自己的工具

  3. 推理

    1. 规划

    2. 自我系列

    3. 评估与观察

  4. 2024 年需要解决的问题

简介

🔥 大语言模型(LLMs)必定是 2023 年最热门的话题。在上个月的 NeurIPS 会议上,社交活动中反复讨论的话题是:1)我们正在做什么关于 LLMs 的研究? 2)我的研究如何与 LLMs 结合? 3)从 XXX 转向 LLMs 的最佳策略是什么? 4)作为一个GPU 不足的团队,我们能做什么研究?原因是每个人都通过 X、Discord、Slack 和其他地方获得了 LLMs 的突破性新闻。

如果你查看 arXiv 上的语言模型论文,2023 年的数量从 2,837 篇跃升至 11,033 篇,这打破了 2019 到 2022 年间的线性趋势。过去一年中的论文大致可以分为三大类:1️⃣ 预训练和对齐;2️⃣ 工具使用和推理;3️⃣ 系统和服务。正如标题所示,本文将重点介绍 LLM 研究在工具使用和推理方面的进展。 我们挑选了大约 20 篇 👀 令人震惊的 👀 论文,并总结了它们的见解和影响。这篇文章绝不是对社区所有成就的全面总结。如果我们遗漏了任何话题,请随时评论。

作者和 ChatGPT 绘制的图表。

本文由两个主题组成:工具使用推理

  • 工具使用更多的是关于如何通过为大语言模型(LLMs)配备外部工具来解决推理问题,例如检索器、搜索引擎和代码解释器。虽然工具使用对于构建强大的人工智能并非必需(见下文 Yann 的分类),但当领域特定工具易于获取时,工具使用为许多应用提供了实际的解决方案。

  • 相比之下,推理则集中在利用 LLM 的内部推理能力来解决复杂问题。推理研究试图找出 LLMs 所具备能力的极限,并探索突破这一极限的方法。

这两者之间并没有严格的二分法,正如我们在本文接下来的部分中将看到的那样。

Yann LeCun 对检索与推理的分类。

工具使用

上下文学习使得使用更多工具成为可能

➡️ LLM 工具使用的一个限制是需要足够的人类标注。每当我们想教一个 LLM 使用工具时,我们需要足够的标注工具调用来对 LLM 进行微调。在 Meta 的Toolformer论文中,作者使用上下文学习创建了一个模型,能够为输入查询标注工具调用。然后,使用该模型在未标记的数据集上生成工具调用。虽然生成的调用可能远非完美,但可以通过执行工具并根据真实答案过滤输出,筛选出错误的调用。正确的调用被收集并用于微调模型。通过这种方式,我们可以基于常规数据集和仅仅 5 个额外标注来教会 Transformer 使用任何工具——这是任何工程师都能轻松完成的工作。

工具调用的自动注释。来源:Schick et al.

➡️ Lu et al. 提出了Chameleon 🦎,用于组合多步推理的工具。其核心思想是使用大型语言模型(LLM)将问题分解为一系列工具调用,然后为每个工具调用生成参数。这两个步骤都是通过少量示例提示来实现的。这个想法让人联想到 2016 年的神经模块网络(NMNs),它将问题分解为子任务,并为每个子任务学习一个模块。NMNs 的主要障碍是,它们在没有分解注释的情况下很难训练(见这项研究)。幸运的是,在预训练的 LLM 中,这不是问题。通过上下文学习,Chameleon 可以生成不同的工具调用组合来解决问题。一种类似的视觉推理方法在今年的 CVPR 上获得了最佳论文奖。

Chameleon 用于多步工具使用。来源:Lu et al.

➡️ 尽管与传统方法相比,上下文学习具有较高的效率,但它也面临某些限制,例如管理大量工具的难度。为了解决这个问题,Hao et al. 提出了ToolkenGPT,它通过为工具引入新的令牌嵌入(称为“toolkens”)来增强一个冻结的 LLM。该技术最初在多语言模型中用于适应新的语言。ToolkenGPT 允许在推理过程中像下一个词预测一样进行工具调用。它展示了处理超过 200 个工具的能力,同时具有成本效益,相较于 LoRA 微调,建立了一种新的效能与效率的平衡。类似的思路也已整合到多模态 LLM 中,用于机器人动作图像生成

ToolkenGPT 用于大规模工具使用。来源:Hao et al.

最常用的工具:代码解释器和检索器

如果你问我们哪些工具最广泛适用于推理任务,我们会说它们是代码解释器检索器。代码解释器可能是人类发明的最具表现力的逻辑与计算环境。检索器在 LLM 的参数化知识无法覆盖某些问题或已知假设的情况下,是一种很好的补充。让我们来看看这些工具是如何被 LLM 使用的。

➡️ 思维链(CoT)的一个常见失败是 LLM 无法执行算术运算。在程序辅助语言建模(PAL)思维程序(PoT)的提示中,作者通过程序提示代码语言模型来解决数学问题。可以在程序中插入标准的思维链文本作为注释。最终的答案是通过执行 Python 解释器生成的。这些方法背后的洞见是,代码解释器为各种计算提供了完美的工具,将失败案例减少到仅仅是推理错误。代码风格的提示也常用于规划任务中。

CoT 和 PAL 的比较。来源:Gao 和 Madaan 等

➡️ 检索器通常作为 LLM 的预处理工具,用于通过相关文档增强问题,通常称为检索增强生成(RAG)。然而,当涉及到多步骤问题解答时,仅凭问题本身就很难选择正确的文档。在Trivedi et al.提出的IRCoT中,作者将思维生成与知识检索交替进行。每当 LLM 生成一个思维句子时,IRCoT 就使用该句子从语料库中检索文档。这些文档被添加到提示中,以增强后续生成。即使是像BM25这样较弱的检索器,IRCoT 也在多个开放领域的问答基准测试中超越了一步 RAG。

交替使用 CoT 和知识检索的 IRCoT。来源:Trivedi 等

➡️ Yang 等提出了一种 RAG 在定理证明中的新颖应用。他们基于证明助手 Lean 构建了一个类似健身房的环境LeanDojo 🏯。Lean是一个互动编程环境,其中的编译器可以验证所写的证明是否证明了目标。它还包含了许多在标准库中已证明的定理,类似于 C++中的 STL。很酷的一点是,由于证明是通过将定理分解为已知前提构造的,因此定理证明可以从 RAG 中受益。给定一个定理,我们从标准库中检索相关的前提,然后请求 LLM 生成一个证明步骤。作者表明,RAG 需要的训练资源要少得多,并且在新颖的前提下具有更好的泛化能力。

在 Lean 中证明一个简单的逻辑定理。来源:Xena 项目

➡️ 最后,DSPyKhattab et al.提出了一种新的 LLM 编程方法,在该框架下,系统能够随着时间的推移自动优化提示,并结合提示技术(CoT, PoT)与检索。进一步地,DSPy 引入了提词器来优化提示并引导新的提示生成。很难用一段话来描述 DSPy——它不是普通的 RAG 技术,而是其进化版本。

让 LLM 创建自己的工具

工具使用有一个固有的限制:它依赖于特定任务所需工具的存在。在自然界中,工具使用并非人类的专属技能,许多其他动物也能够使用工具。然而,区别人类与其他动物的,是创造工具的能力。2023 年,我们看到了一些初步的研究,探索了 LLM 中工具制作的能力。

➡️ 在由Cai et al.提出的 LLM 作为工具制造者(LATM)中,作者提示 LLM 为给定任务编写 Python 函数形式的工具。这些工具随后在一些样本上进行验证,类似于工程师在 LeetCode 上解决问题的方式。一旦某些工具通过验证测试,它们会被 LLM 生成的文档字符串包装,以描述其使用方法。在测试时,LLM 会被提示将问题发送到手头的某个工具,并根据其使用方法执行该工具。LATM 在 BigBench 的广泛推理任务中,显著优于 CoT。

➡️ Voyager将工具制作的概念引入了 Minecraft 的世界,并取得了惊人的结果。Voyager 的核心思想是利用 LLM 根据现有的技能和世界状态提出任务。接着,LLM 被提示合成代码(即技能)来解决这些任务。技能基于环境反馈进行精炼,掌握的技能被保存到外部记忆中。由于新技能是在现有技能的基础上构建的,这大大降低了学习复杂技能的难度(例如,在 Minecraft 中制作钻石工具)。尽管学习技能库的概念可以追溯到DreamCoder,Voyager 展示了 GPT-4 在挑战性开放世界游戏中搜索技能的优势。请查看论文中的精彩演示

随着时间的推移,Minecraft 中的物品和技能被发现。来源:Wang et al.

➡️ 上述两篇工作都将工具设计为代码。事实上,工具也可以是自然语言。(不嫌弃的自我推销)在 Zhu et al. 的假设到理论 (HtT) 工作中,作者展示了我们可以利用 LLM 从标准的多步骤推理训练集诱导出一套文本规则库。其洞察是,在 LLM 为不同样本生成的所有规则中,发生频率较高并且导致正确答案的规则可能是正确的。然后,我们收集这些规则并将其附加到标准 CoT 提示中以进行推理并得出答案。HtT 的一个有趣方面是,它可以被看作是一种新的学习方式:我们不是学习模型参数,而是学习一套规则库,这与黑盒 LLM 配合得非常好。

HtT 学习多跳推理的文本规则。来源:Zhu et al.

推理

规划

CoT 风格推理的一个缺点是,LLM 必须贪婪地解码一个通向答案的路径。对于复杂的问题,如数学题或游戏,这就成了一个问题,因为没有试错就很难预测出一条路径。2023 年,社区在这个问题上取得了一些进展,推出了新的框架,使得 LLM 可以进行规划。

➡️ 如果我们将 CoT 概念化为“系统 1”推理——其特点是自动的、无意识的特性——那么就会出现一个问题:是否可以用 LLM 来复制更具意识的“系统 2”推理?这个问题在两种方法中得到了回应:推理通过规划 (RAP)思维树 (ToT)。这两者都使 LLM 能够通过可能的推理步骤进行推理,并根据特定的评估搜索最优推理链。RAP 还将 LLM 提示为一个“世界模型”,它预测在行动后的下一状态。这使得 LLM 可以在一个自我模拟的世界中操作,而不是与外部环境互动。现在这两个算法都可以在 LLM Reasoners 库中找到!

RAP 将 LLM 重新设计为一个代理和世界模型。来源:Hao et al.

Self 系列

Self 系列是一类技术,通过在 LLM 开发过程中用 LLM 预测替代人工努力。2023 年见证了这一领域的若干重要论文。让我们深入了解一些具有代表性的工作。

➡️ 许多人都有过 ChatGPT 在第一次尝试时未能提供期望输出的经历,而这有时可以通过指出其错误来修复。自我调试自我完善通过用机器反馈替代人类反馈来自动化这个过程。反馈来自于程序执行器或一个 LLM,它比较生成的内容与问题的解释。一项关键的观察是,自我完善的表现取决于反馈的质量,提供更好反馈的强大基础模型能带来更大的帮助。这类迭代完善方法在姿态估计蛋白质结构预测中也被证明非常有效,因为在单次运行中很难预测结构。

自我调试的示意图。来源:Chen et al.

➡️ 在Li and Qiu提出的记忆-思维(MoT)框架中,作者要求 LLM 在未标注数据集上生成 CoT 推理,并将其用于 RAG。你可能会问,考虑到生成的推理通常包含错误,这如何有用呢?关键的技巧是根据多数投票或熵最小化来过滤推理(Wan et al.中也使用了类似的思想来过滤推理)。一旦我们在未标注数据集上获得了良好的推理,我们会基于测试问题动态地检索少样本示例,这被证明比固定的少样本示例要好得多。MoT 可以解释为将一个参数化模型转化为一个非参数化模型,而无需额外的监督。

生成和回忆记忆的 MoT。来源:Li and Qiu

➡️ 超越 MoT,Yasunaga et al.提出了类比提示,它消除了在未标注数据集上倾倒推理的需求。类比提示要求 LLM 根据问题回忆相关的示例,从而从头生成动态的少样本示例。事实上,作者发现类比提示是大语言模型的一个涌现能力,类似于之前在开放领域问答中的研究。更大规模的 LLM 可以自我生成比标准 RAG 解决方案更好的示例。此外,这项工作为将多步生成合并为一个单一提示提供了一个很酷的技巧,使用 Markdown 语法——这对于预算紧张的提示工程师来说是一个天赐之物!💡

类比提示。来源:Yasunaga et al.

➡️ 自我优化和自我生成是 LLM 推理的极限吗?Yang et al.展示了 LLM 推理能力的更高级应用——基于生成的提示历史优化提示。这是对著名元学习论文《通过梯度下降学习学习》的一个酷炫重塑,但这里的所有步骤都是由 LLM 在文本上执行的。在每个步骤中,LLM 都会接收之前的解决方案和相应的性能指标提示,并尝试预测一个新的解决方案。值得注意的是,即使没有告诉 LLM 如何进行优化,LLM 也能逐步找到最大化指标的更好解决方案。也许这项工作让提示工程师离失业更近了一步?

由 LLM 优化的提示性能。来源:Yang et al.

🔁 可能是自学系列中最具启发性的👀工作是由Zelikman et al.提出的自我优化器(STOP) 。我们知道 LLM 是通过文本提示来引导的,接受文本作为输入并输出文本。虽然这些文本通常是分开的变量,但如果我们将它们建模为单一变量,会发生什么呢?在 STOP 中,作者从自我修改代码中汲取灵感,使用自我改进的提示来提升自身。

在 STOP 中改进自身的种子改进器。来源:Zelikman et al.

虽然种子提示并不比随机搜索算法更复杂,但有了强大的 LLM,人们可以发现许多先进的元启发式算法。有趣的是,GPT-4 发现了许多在其训练截止日期之后发布的提示策略,包括ToTParsel。看起来,LLM 为自己进行研究的那一天即将到来。朝着这个方向迈出的一步是Huang et al.的最新研究,表明 LLM 能够为常见基准测试甚至 Kaggle 挑战设计机器学习模型。

STOP 发现的算法。来源:Zelikman et al.

评估与观察

➡️ Kandpal 等人对 LLMs 的记忆能力进行了系统研究。他们向 LLM 提出了来自维基百科的事实性问题,并发现准确性与预训练文档中被提问实体的频率高度相关,无论模型的规模如何。通过推测这一趋势,作者估计,需要一个拥有 10¹⁸参数的模型才能在长尾实体上与人类表现相匹配——这远远超出了当前的 LLMs。因此,一个重要的启示是,对于与频繁知识相关的任务,应使用 LLM 推理,而对于与长尾知识相关的任务,可以考虑使用 RAG 或其他工具。

LLMs 几乎无法记住长尾知识。来源:Kandpal 等人

➡️ 随着社区尝试构建更大规模的混合数据集来训练 LLMs,一个担忧是 LLMs 可能无法真正学习推理,而只是简单地记住训练分布中的解决方案,就像人类在应试教育中一样。Wu 等人通过比较 GPT-4 在 11 个不同任务上的零样本 CoT 表现来回应这一担忧,每个任务都有默认设置和反事实设置。他们观察到,尽管 LLMs 在反事实设置中表现优于随机结果,但其表现始终低于默认设置。如何训练模型更多地关注推理而非记忆,仍然是一个悬而未决的问题。

GPT-4 在反事实变体上的表现不佳。来源:Wu 等人

➡️ Saparov 等人将一个合成数据集PrOntoQA扩展到 OOD 设置,以测试 LLMs 在控制深度、宽度、组合结构等方面的推理泛化能力。作者发现,CoT 能够推广到组合性和更长的证明。这与之前关于组合语义解析的结论相反,可能是因为推理只需要组合推理步骤,而语义解析还需要处理不断增长的输出。虽然 LLMs 能够使用大多数推理规则,但它们需要显式演示分情况证明反证法证明。在上下文学习和监督学习之间也存在一些违反直觉的定性差异。

关于推理的 OOD 泛化能力。来源:Saparov 等人

➡️ 关于 LLMs 中的参数化知识,Berglund 等人发现了一种他们称之为逆转诅咒的现象。也就是说,在闭卷问答中,被训练记住“ A 是 B”的 LLMs 并不知道“B 是 A”,尽管它们可以被提示进行演绎推理。这表明 LLMs 在其参数化知识中缺乏某些对称性,而赋予它们这种对称性对于更好的泛化至关重要。实际上,知识图谱领域一直是这一领域的领先者,像双重排列等变性关系旋转等工作便是例证。看看这些想法如何适应 LLMs 将会很有趣。

2024 年需要解决什么问题?

2023 年对于工具使用和推理来说是令人兴奋的一年,我们预计新的一年将更加精彩。让我们通过作者的预测来总结这篇文章。

Zhaocheng Zhu:

1️⃣ 使用 LLMs 进行推理仍然需要针对每个特定任务进行临时的工程努力。相比之下,一旦人类掌握了某项任务的技能,他们可以迅速将这些技能适应到类似的任务上,几乎不需要样本,甚至不需要样本(例如,从国际象棋到扑克牌)。如果我们能够创建跨任务泛化的 LLM 解决方案,这将节省大量的工程努力,并提升在低资源领域的表现。

2️⃣ 解决推理问题通常涉及大量的常识性知识,从数学、物理到像枚举法和反证法这样的策略,如果有的话。虽然 LLMs 可能从其训练数据中获得了这些知识,但我们对 LLMs 中参数化知识的精确控制仍然缺乏。我们希望看到关于 LLMs 知识表示的新研究,以及可以在 LLMs 中表达、注入或删除知识的技术。

Michael Galkin:

1️⃣ 在 2023 年,我们看到对Transformer 基础的 LLMs 可以学习什么基本原理的理解力度不断加大——我们是否真的可以期望 LLMs 能够解决任何任意的推理任务?一些著名的论文,比如信仰与命运关于长度泛化,表明 LLMs 的自回归特性可能不是处理复杂推理的最佳方式。在 2024 年,我预计会有更多的努力理解 LLMs 与算法的对齐。

2️⃣ 很可能在 2024 年,大多数开放和闭合的基础模型将是多模态的,支持视觉、文本、音频等多种输入。将其他模态融入推理是自然的下一步。

Abulhair Saparov:

1️⃣ 我预期会有更多的努力去寻找 LLMs 推理机制的更深层次理解。在执行推理任务时,它们使用什么算法?更准确地说,它们在多大程度上利用了会影响鲁棒性/泛化能力的捷径或启发式方法?

2️⃣ 与此相关,我预计研究人员将会取得进展,回答一个问题:是否增加 LLM 的规模和/或其训练将解决其在推理方面的局限性,或者这些局限性是否是根本性的,例如架构固有的问题。

Shibo Hao:

1️⃣ 在过去的一年里,LLM 推理研究的主要焦点集中在提示和监督微调上,像 STaRReflexionRAP 等方法,已经开始从强化学习(RL)中汲取灵感。然而,我们尚未见证一种突破性的方法,能够有效地利用强化学习提升 LLM 的推理能力,特别是与 RLHF 在对齐方面的进展相比。

2️⃣ 另一方面,未来语言可能成为强化学习系统中的主要表达媒介。其关键优势在于与传统的标量奖励/价值相比,语言蕴含着丰富的信息。一个能够通过强化学习自主提高推理能力的 LLM 代理(无需监督数据或提示工程),不仅令人兴奋,还可能意味着朝着通用人工智能(AGI)迈出了重要一步。

Yihong Chen:

1️⃣ 结构化与非结构化。我认为,大型语言模型(LLMs)将逐渐吞噬传统产品的市场份额,而这些传统产品大多依赖于大型数据库、规则和大量小型分类器。在这种情况下,我们所说的“LLM 推理”可能指的是我们期望能够有一种方法“X”,它可以弥合结构化世界(目前大多数产品数据所处的世界)与非结构化世界(大多数 LLM 所处的世界)之间的鸿沟。知识图谱在某种程度上代表了结构化世界,关于如何在知识图谱上进行有效推理的研究也很多,而 LLM 则代表了非结构化世界,尽管我们仍不清楚它们是如何进行推理的。它们各有优劣和局限性。我预计,结构化世界与非结构化世界之间的一个有效桥梁将能为产品提供更具务实性的解决方案。

2️⃣ 样本效率。正如 Zhaocheng 所提到的,目前 LLM 的推理在跨大量任务的泛化方面存在困难。我对是否能够简单地预训练一个能够用更少数据泛化的 LLM 感兴趣,类似于 跨多种语言的泛化所做的那样。

3️⃣ LLM 内部的推理。正如 Abulhair 和 Michael 所提到的,社区目前尚未清楚了解 LLM 是如何进行推理的,甚至是否真正在进行推理。我预计,未来会有更多的努力,去逆向工程 LLM 的推理过程,可能是通过 机械性方法 或其他可解释性方法。

Meme Time

延续Michael Galkin的传统,任何博客文章都不算完整,没有一个表情包。DALL·E 3 简直是表情包魔法师,如果它能正确拼写单词的话。猜猜每个面板上用的是什么提示?

一个 LLM 学到了什么,它能推理什么。图片来源:作者与 DALL·E 3。

阅读更多

如果这篇博客让你想了解更多关于 LLM 推理的内容,看看以下这些精彩的博客文章。

使用爬山算法解决经典的世界大赛下注问题

原文:towardsdatascience.com/solving-the-classic-betting-on-the-world-series-problem-using-hill-climbing-5e9766e1565d?source=collection_archive---------2-----------------------#2024-11-10

一个简单的爬山算法示例——解决一个在没有优化技术的帮助下很难解决的问题

W Brett KennedyTowards Data Science W Brett Kennedy

·发表于 Towards Data Science ·15 分钟阅读·2024 年 11 月 10 日

--

世界大赛下注是一个古老、有趣且富有挑战性的谜题。这也是一个很好的问题,可以用来展示一种叫做爬山算法的优化技术,我将在本文中详细介绍。

爬山算法是一种成熟且相对直接的优化技术。网上有许多其他的示例应用它,但我认为这个问题提供了一个有趣的应用案例,值得深入研究。

这个谜题可以在UC Davis的网页上看到。为了省去你查找的麻烦,我在这里重复一遍:

[E. Berlekamp] 赌注在世界大赛上。你是一个经纪人;你的工作是满足客户的要求,同时不让个人资本承担任何风险。你的客户希望在世界大赛的结果上下注 1,000 美元,这是一场棒球比赛,决出先赢得 4 场比赛的其中一个队伍。也就是说,客户在大赛开始前将 1,000 美元存入你这里。比赛结束时,如果他的队伍获胜,他需要从你这里获得 2,000 美元,如果他的队伍失败,则什么也得不到。没有针对整个世界大赛的市场。但你可以在每场比赛上分别下注,金额可以相等。你的策略是什么,以便在单场比赛中下注,从而实现客户要求的累计结果?

所以,有必要逐场进行投注(虽然也可以选择跳过某些比赛,只对这些比赛下注$0)。每场比赛后,我们将获得或失去正好是我们对该场比赛下注的金额。我们从客户提供的$1000 开始。如果我们的队伍赢得了整个系列赛,我们希望最终余额为$2000;如果他们输了,我们希望最终余额为$0。

如果你之前没有见过这个问题,并且希望尝试手动解决,现在是一个机会,在我们介绍如何通过编程解决之前。这个问题本身非常有趣,直接尝试解决它是值得的,之后再考虑使用爬坡算法。

接近问题的解决

对于这个问题,我假设暂时亏损是可以接受的。也就是说,在世界大赛期间,如果我们的余额降到零以下,这是可以的(我们是一家规模较大的经纪公司,可以撑过去),只要我们最终能够确保余额为$0 或$2000。然后,我们将$0 或$2000 返还给客户。

为这个问题提出一个大多数情况下有效的解决方案是相对简单的,但并不一定适用于每种情况。实际上,我在网上看到过一些关于这个谜题的描述,它们提供了一些解决方案的草图,但似乎并没有完全测试所有可能的胜负序列。

一种下注策略的示例是:$125、$250、$500、$125、$250、$500、$1000。在这个策略中,我们对第一场比赛下注$125,对第二场比赛下注$250,以此类推,直到比赛结束。例如,如果系列赛只进行五场比赛,我们下注的金额将是:$125、$250、$500、$125、$250。这个策略在大多数情况下有效,尽管不是对所有情况都适用。

考虑以下序列:1111,其中 0 表示团队 0 赢得一场比赛,1 表示团队 1 赢得一场比赛。在这个序列中,团队 1 赢得了四场比赛,因此赢得了系列赛。假设我们的队伍是团队 1,那么我们需要最终余额为$2000。

查看比赛、下注和每场比赛后所持有的金额,我们得到:

Game    Bet  Outcome   Money Held
----    ---   ----     ----------
Start    -     -         1000
1       125    1         1125
2       250    1         1375
3       500    1         1875
4       125    1         2000

也就是说,我们从$1000 开始。我们在第一场比赛下注$125。团队 1 赢得了那场比赛,所以我们赢得了$125,现在我们有$1125。接着我们在第二场比赛下注$250。团队 1 再次获胜,我们赢得$250,现在我们有$1375。接下来,我们对接下来的两场比赛分别下注$500 和$125。在这种情况下,我们最终正确地拥有$2000。

测试序列 0000(其中团队 0 在四场比赛中获胜):

Game    Bet  Outcome  Money Held
----    ---   ----    ----------
Start    -     -        1000
1       125    0         875
2       250    0         625
3       500    0         125
4       125    0           0

在这里,我们正确地(假设团队 0 赢得了系列赛)最终余额为$0。

测试序列 0101011(其中团队 1 在七场比赛中获胜):

Game    Bet  Outcome  Money Held
----    ---   ----    ----------
Start    -     -        1000
1       125    0         875
2       250    1        1125 
3       500    0         625
4       125    1         750
5       250    0         500 
6       500    1        1000
7      1000    1        2000

在这里,我们再次正确地最终拥有$2000。

然而,在序列 1001101 中,这个策略无法奏效:

Game    Bet  Outcome  Money Held
----    ---   ----    ----------
Start    -    -        1000
1       125   1        1125 
2       250   0         875
3       500   0         375
4       125   1         500
5       250   1         750
6       500   0         250
7      1000   1        1250

这里,尽管团队 1 赢得了系列赛(在 7 场比赛中赢得 4 场),但最终我们只有$1250,而不是$2000。

测试某个给定的策略

由于比赛序列可能有很多种,这使得手动测试变得非常困难(而且在测试许多可能的策略时也非常繁琐),所以接下来我们将开发一个函数来测试给定的策略是否正常工作:即它是否能正确地在队伍 1 赢得系列赛时最终保持至少$2000,在队伍 0 赢得系列赛时保持至少$0。

这个策略以一个七个数字的数组形式传入,表示在七场比赛中每场的投注金额。在只有四场、五场或六场比赛的系列赛中,策略数组中最后几项的值将不会被使用。上述策略可以表示为[125, 250, 500, 125, 250, 500, 1000]。

def evaluate_policy(policy, verbose=False):    
    if verbose: print(policy)
    total_violations = 0

    for i in range(int(math.pow(2, 7))):
        s = str(bin(i))[2:]
        s = '0'*(7-len(s)) + s  # Pad the string to ensure it covers 7 games
        if verbose: 
          print()
          print(s)

        money = 1000
        number_won = 0
        number_lost = 0
        winner = None

        for j in range(7):
            current_bet = policy[j]

            # Update the money
            if s[j] == '0':
                number_lost += 1
                money -= current_bet
            else:
                number_won += 1
                money += current_bet
            if verbose: print(f"Winner: {s[j]}, bet: {current_bet}, now have: {money}")

            # End the series if either team has won 4 games
            if number_won == 4:
                winner = 1
                break
            if number_lost == 4:
                winner = 0
                break

        if verbose: print("winner:", winner)
        if (winner == 0) and (money < 0):
            total_violations += (0 - money)
        if (winner == 1) and (money < 2000):
            total_violations += (2000 - money)

    return total_violations

这从创建每种可能的胜负序列的字符串表示开始。这样会创建一个由 2⁷(128)个字符串组成的集合,字符串从‘0000000’开始,然后是‘0000001’,依此类推,直到‘1111111’。其中一些是冗余的,因为某些序列会在七场比赛之前结束——一旦某个队伍赢得四场比赛。在实际生产中,我们可能会清理这些冗余序列以减少执行时间,但为了简化,我们仅仅遍历所有 2⁷种组合。这在后续有一些好处,因为它平等对待所有 2⁷(同样可能的)组合。

对于这些可能的序列,我们应用策略来确定每场比赛的投注,并持续跟踪所持有的资金。也就是说,我们遍历所有 2⁷种可能的胜负序列(直到某个队伍赢得四场比赛为止),对于每个序列,我们遍历序列中的每场比赛,逐场进行投注。

最终,如果队伍 0 赢得了系列赛,我们理想中的金额是$0;如果队伍 1 赢得了系列赛,我们理想中的金额是$2000,虽然如果超过这个金额也没有惩罚(或好处)。

如果我们在一场比赛序列结束时没有得到正确的资金数额,我们将确定短缺的金额;这就是该序列的成本。我们将所有可能的比赛序列中的短缺金额加起来,从而得到对该策略总体表现的评估。

要确定给定的策略是否正常工作,我们可以简单地调用此方法并传入给定的策略(以数组形式),然后检查它是否返回 0。返回值大于 0 表示存在一个或多个序列,其中经纪人最终剩余的资金过少。

山地攀登

我不会过多详细讨论山地攀登法,因为它已经被广泛理解并且在许多地方有详细文档,但我会快速描述其基本思想。山地攀登是一种优化技术。我们通常从生成问题的候选解决方案开始,然后在小的步骤中修改它,每一步都会得到更好的解决方案,直到我们最终达到最优解(或者被困在局部最优解中)。

为了解决这个问题,我们可以从任何可能的策略开始。例如,我们可以从[-1000, -1000, -1000, -1000, -1000, -1000, -1000]开始。这个特定的策略肯定效果不好——我们实际上会在所有七场比赛中对 Team 1 下注很多。但这没关系。爬山法是从任何地方开始,然后逐步朝更好的解决方案移动,因此即使从一个糟糕的解决方案开始,我们最终也理想地会达到一个强的解决方案。尽管如此,在某些情况下,我们可能无法达到最优解,有时需要(或至少有用)从不同的起点重新运行爬山算法。在这种情况下,从一个非常糟糕的初始策略开始也没问题。

在编写代码之前先手动操作这个难题,我们可能得出结论,一个策略比单纯的七个值的数组要复杂一些。那种策略形式完全基于是哪场比赛来决定每次下注的大小,而忽略了到目前为止的胜负记录。我们实际需要表示策略的是一个二维数组,如下所示:

[[-1000, -1000, -1000, -1000, -1000, -1000, -1000],
 [-1000, -1000, -1000, -1000, -1000, -1000, -1000],
 [-1000, -1000, -1000, -1000, -1000, -1000, -1000],
 [-1000, -1000, -1000, -1000, -1000, -1000, -1000]]

还有其他方法可以做到这一点,但正如我们下面所展示的,这种方法效果相当好。

这里,行表示到目前为止 Team 1 的获胜场数:0、1、2 或 3。列,如前所述,表示当前的比赛编号:1、2、3、4、5、6 或 7。

再次说明,在这个策略中,我们会在每场比赛中都对 Team 1 下注$1000,无论如何,因此几乎任何随机策略都可能稍微好一点。

这个策略有 4x7,或者说 28 个值。虽然其中一些是多余的,可以优化一下。我选择了简单而不是高效,但一般来说,在生产环境中我们会进行更多优化。在这种情况下,我们可以去除一些不可能的情况,比如在第 5、6 或 7 场比赛中有 0 场胜利(如果第 5 场比赛时 Team 1 没有胜利,Team 0 必须有 4 场胜利,从而结束系列赛)。28 个单元格中有 12 个实际上是无法达到的,其余的 16 个是相关的。

为了简单起见,在这个例子中没有使用,但实际上相关的字段如下,我已放置了-1000:

[[-1000, -1000, -1000, -1000,  n/a,   n/a,   n/a ],
 [ n/a,  -1000, -1000, -1000, -1000,  n/a,   n/a ],
 [ n/a,   n/a,  -1000, -1000, -1000, -1000,  n/a ],
 [ n/a,   n/a,   n/a,  -1000, -1000, -1000, -1000]]

标记为“n/a”的单元格不相关。例如,在第一场比赛中,不可能已经有 1、2 或 3 场胜利;那时只能有 0 场胜利。另一方面,到第四场比赛时,可能已经有 0、1、2 或 3 场胜利。

在编写任何代码之前,如果先手动操作一下,就能看到每个下注额很可能是$1000 的二分之一、四分之一、八分之一、十六分之一等等的倍数。虽然这不一定是最优解,但我将假设所有的下注都是$500、$250、$125、$62.50 或$31.25 的倍数,并且它们可能是$0。

不过,我假设永远没有反对 Team 1 下注的情况;虽然初始策略以负数下注开始,但生成新候选策略的过程只使用$0 到$1000 之间(包括$0 和$1000)的下注额。

因此,每个投注有 33 种可能的值(从$0 到$1000 的每个$31.25 的倍数)。考虑到完整的 28 个单元格,并假设投注是 31.25 的倍数,那么政策可能的组合数为 33²⁸。所以,测试所有可能的组合是不可行的。如果将其限制为使用的 16 个单元格,仍然有 33¹⁶种可能的组合。可能会有进一步的优化,但无论如何,要全面检查所有的组合,数量极其庞大,远远超出可行的范围。也就是说,直接用程序解决这个问题是可能的,但如果仅依赖于这里假设的条件,暴力破解法将是无法处理的。

因此,像爬山算法这样的优化技术在这里非常适用。通过从解决方案空间的一个随机位置开始(一个随机的政策,以 4x7 矩阵的形式表示),不断地(比喻地)向上移动(每次我们都朝着一个比之前稍微更好的解决方案前进),最终我们到达最高点,在这种情况下就是一个适用于世界系列投注问题的可行政策。

评估方法的更新

鉴于政策将以二维矩阵而非一维数组的形式表示,确定当前投注的代码将从以下内容变更:

current_bet = policy[j]

改为:

current_bet = policy[number_won][j]

也就是说,我们根据目前为止赢得的比赛数和当前比赛的编号来确定当前投注。否则,evaluate_policy()方法与上述相同。上述用于评估政策的代码实际上是大部分代码的核心。

寻找解决方案的代码

接下来,我们展示主要代码,它从一个随机政策开始,然后循环(最多 10,000 次),每次修改并(希望)改进此政策。每次循环,它生成当前最佳解的 10 个随机变体,选出其中最好的一个作为新的当前解决方案(如果没有更好的解,则保留当前解,并继续循环,直到找到更好的解)。

import numpy as np
import math
import copy

policy = [[-1000, -1000, -1000, -1000, -1000, -1000, -1000], 
          [-1000, -1000, -1000, -1000, -1000, -1000, -1000],
          [-1000, -1000, -1000, -1000, -1000, -1000, -1000],
          [-1000, -1000, -1000, -1000, -1000, -1000, -1000]]
best_policy = copy.deepcopy(policy)
best_policy_score = evaluate_policy(policy)
print("starting score:", best_policy_score)

for i in range(10_000):
    if i % 100 == 0: print(i)

    # Each iteration, generate 10 candidate solutions similar to the
    # current best solution and take the best of these (if any are better
    # than the current best).
    for j in range(10):
        policy_candidate = vary_policy(policy)
        policy_score = evaluate_policy(policy_candidate)
        if policy_score <= best_policy_score:
            best_policy_score = policy_score
            best_policy = policy_candidate
    policy = copy.deepcopy(best_policy)
    print(best_policy_score) 
    display(policy)
    if best_policy_score == 0:
        print(f"Breaking after {i} iterations")
        break

print()
print("FINAL")
print(best_policy_score)    
display(policy)

运行此代码时,主循环执行了 1,541 次才找到解决方案。每次迭代,它调用 vary_policy()(下面描述)十次来生成当前政策的十个变体。然后它调用 evaluate_policy()来评估每个变体。这个方法之前已经定义过,提供一个得分(以美元为单位),表示使用该政策时,代理商在 128 个世界系列实例的平均集下,能够容忍的亏损金额(我们可以将其除以 128,以获得任何单一世界系列的预期损失)。得分越低,结果越好。

初始解决方案的得分为 153,656.25,表现非常差,正如预期的那样。从那里开始,快速改善,迅速下降到约 100,000,然后是 70,000,50,000,以此类推。随着代码执行,打印出迄今为止找到的最佳政策,也会呈现越来越合理的政策。

生成随机变体的方法

以下代码生成当前政策的一个单一变体:

def vary_policy(policy):
    new_policy = copy.deepcopy(policy)
    num_change = np.random.randint(1, 10)
    for _ in range(num_change):    
        win_num = np.random.choice(4)
        game_num = np.random.choice(7)
        new_val = np.random.choice([x*31.25 for x in range(33)])
        new_policy[win_num][game_num] = new_val
    return new_policy

在这里,我们首先选择 4x7 策略中需要修改的单元格数量,范围在 1 到 10 之间。可以修改更少的单元格,这有助于在得分接近零时提升表现。也就是说,一旦我们拥有了一个强大的策略,我们可能希望减少对它的修改,而不是在过程初期进行大量修改,因为初期的解决方案往往较弱,更加注重搜索空间的探索。

然而,始终修改少量固定数量的单元格可能会导致陷入局部最优解(有时没有对策略进行修改,比如修改 1 或 2 个单元格后效果会更好,因此必须修改更多的单元格才能看到提升),并不总是能取得良好的效果。随机选择若干个单元格进行修改可以避免这种情况。不过,将这里的最大单元格数设置为 10 只是为了演示,并不是经过任何调优后的结果。

如果我们将自己限制在 4x7 矩阵中的 16 个相关单元格内进行修改,这段代码只需做一些小改动,简单地跳过对这些单元格的更新,并在显示矩阵时用一个特殊符号(相当于‘n/a’,如 np.NaN)标记它们,以便清晰显示。

结果

最终,算法能够找到以下策略。也就是说,在第一场比赛中,我们将没有胜利,因此会投注$312.50。在第二场比赛中,我们可能没有胜利,也可能有一次胜利,但无论哪种情况,我们的投注都是$312.50。在第三场比赛中,我们可能没有胜利,也可能有一次或两次胜利,因此投注额为$250、$375 或$250,依此类推,最多为七场比赛。如果我们进入第七场比赛,我们必须有 3 次胜利,并将在该场比赛中投注$1000。

[[312.5, 312.5, 250.0, 125.0, 718.75, 31.25, 281.25],
 [375.0, 312.5, 375.0, 375.0, 250.0, 312.5, 343.75],
 [437.5, 156.25, 250.0, 375.0, 500.0, 500.0, 781.25],
 [750.0, 718.75, 343.75, 125.0, 250.0, 500.0, 1000.0]]

我还制作了一个图表,展示了目前为止找到的最佳策略的得分如何随着 1,541 次迭代逐步下降(也就是变得更好——数值越小越好):

迭代过程中找到的最佳策略的得分,直到找到一个合适的解决方案(得分为 0)。

由于得分最初较大,这一变化有些难以察觉,因此我们再次绘制图表,跳过前 15 步:

迭代过程中找到的最佳策略的得分(跳过前 15 次迭代),直到找到一个合适的解决方案(得分为 0)。

我们可以看到,得分最初会迅速下降,即使在前 15 步之后,得分仍继续下降,随后进入一个长时间的小幅提升期,直到最终找到对当前策略的小幅调整,使得得分有所提升,然后再次出现下降,直到我们最终达到完美的 0 分(即对于任何可能的胜负序列,得分都短缺$0)。

相似类型的问题

我们在这里处理的问题是一个约束满足问题的例子,我们只是希望找到一个满足所有给定约束的解决方案(在这种情况下,我们将约束视为硬约束——对于任何可能有效的游戏序列,必须以$0 或$2000 结束)。

给定两个或更多完整的解决方案时,没有哪个解决方案比另一个更好;任何有效的解决方案都是好的,一旦我们找到了可行的策略,就可以停止。N 皇后问题和数独是这种类型问题的两个其他例子。

其他类型的问题可能具有最优性。例如,对于旅行商问题,任何访问每个城市一次的解决方案都是有效的,但每个解决方案的评分不同,有些解决方案优于其他解决方案。在这种类型的问题中,我们通常无法确定何时达到了最优解,我们通常只会尝试固定次数的迭代(或固定时间),或者直到我们找到了一个至少达到某个最低质量水平的解决方案。爬山法也可以用于这些类型的问题。

也可以制定一个问题,要求找到所有可行的解决方案,而不仅仅是一个。在世界大赛投注问题的情况下,找到一个可行的解决方案很简单,但找到所有解决方案会更加困难,需要进行穷举搜索(尽管可以优化,快速排除等价情况,或者在策略具有明显结果时提前停止评估)。

类似地,我们可以重新制定世界大赛投注问题,只要求一个好的解决方案,而不是完美的解决方案。例如,我们可以接受经纪人大部分时间没有盈亏,仅在其他情况下稍微亏损的解决方案。在这种情况下,爬山法仍然可以使用,但像随机搜索或网格搜索也是可行的——在固定次数的试验后,选择找到的最佳策略,这种方法在这种情况下可能足够有效。

在比世界大赛投注问题更困难的问题中,像我们这里使用的简单爬山法可能不够充分。例如,可能需要保持之前策略的记忆,或者包括一个叫做模拟退火的过程(在这种过程中,我们偶尔采取一个次优的步骤——一个可能实际质量低于当前解的步骤——以帮助突破局部最优解)。

对于更复杂的问题,可能更适合使用贝叶斯优化、进化算法、粒子群智能或其他更先进的方法。我希望在未来的文章中涵盖这些内容,但这个问题相对简单,直接的爬山法效果很好(尽管如前所述,可以很容易地优化使其效果更好)。

结论

这篇文章提供了一个简单的爬山法例子。这个问题相对直接,因此希望对于没有接触过爬山法的人来说足够简单,也可以作为一个很好的例子,即使你已经熟悉这种技术。

我认为有趣的是,尽管这个问题本可以通过其他方式解决,但像这里使用的优化技术可能是最简单且最有效的解决手段。虽然如果不使用这种方法,问题可能很难解决,但使用爬山法后,解决起来相当简单。

所有图片由作者提供

使用遗传算法解决旅行商问题

原文:towardsdatascience.com/solving-the-travelling-salesman-problem-using-a-genetic-algorithm-c3e87f37f1de?source=collection_archive---------0-----------------------#2024-08-25

使用 Python 的探索

James WilkinsTowards Data Science James Wilkins

·发布于数据科学前沿 ·阅读时间 15 分钟·2024 年 8 月 25 日

--

你可以在这里查看此项目的笔记本

图片由 DALL-E 生成,OpenAI

旅行商问题

旅行商问题(TSP)描述了一个情境,其中一名销售员希望访问多个城市,并在返回起点之前采取最短的路线。虽然它看起来很简单,但这个问题不仅没有已知的多项式时间解法,而且没有有效的方法来证明某个给定的解是最优的。

相反,我们通常使用启发式算法来给出一个近似解,这对于许多实际应用来说已经足够。在本文中,我们将探索使用遗传算法生成“良好”解的不同方法。有关旅行商问题(TSP)难点的更深入讨论,以及一些用于解决它的启发式方法的总结,请查看这篇文章

遗传算法

遗传算法(GA)是一种模仿生物进化过程的机器学习技术。它是一种引导的随机搜索算法,可以非常高效地探索庞大的解空间。遗传算法涉及构建一个“染色体”种群,这些染色体作为潜在解的候选…

关于将大型语言模型(LLM)应用落地的一些思考

原文:towardsdatascience.com/some-thoughts-on-operationalizing-llm-applications-aae3530821a8?source=collection_archive---------5-----------------------#2024-01-27

从开发 LLM 应用程序中获得的一些个人经验教训

Matthew HarrisTowards Data Science Matthew Harris

·发表于 Towards Data Science ·阅读时间 10 分钟·2024 年 1 月 27 日

--

来源:DALL·E 3 提示语为“Operationalizing LLMs, watercolor”

发布关于新型大型语言模型(LLM)技术和库的文章一直很有趣,但大多数时间我都在幕后为 LLM 解决方案的落地做工作。许多组织现在都在进行这方面的工作,所以我想分享一些我至今为止的经验和思考。

原型很容易……但生产环境,嗯,确实很难

为了展示大型语言模型(LLM)的一些惊人能力,快速做一个简单的演示看似很容易,但任何被要求将其展示给用户,并希望能产生明显影响的人很快就会意识到,需要做大量的工作来驯服这些模型。以下是大多数组织可能需要考虑的一些关键领域。

在推出使用大型语言模型(LLM)的应用程序之前,应该考虑的一些关键领域。

这个列表并不全面(另见 Kadour et al 2023),哪些方面适用于你的应用程序当然会有所不同,但即便只是解决安全性、性能和成本问题,都是一项艰巨的任务。

那么我们能为此做些什么呢?

不是所有的 LLM 应用都同样令人恐惧

目前有很多关于 LLM 安全使用的担忧,而且这也是完全合理的。由于 LLM 是基于人类产出的数据进行训练的,因此它们受到了许多人类状况的负面影响,而其回应方式的高度可信度又引发了新的安全问题。然而,风险并非在所有情况下都是相同的,有些应用比其他应用更为安全。例如,让 LLM 直接从其训练数据中提供答案,比起低级别技术使用 LLM 预测元数据,更容易产生幻觉和偏见。这是一个明显的区别,但对于那些准备构建 LLM 解决方案的人来说,值得考虑——从低风险的应用开始是显而易见的第一步,并且能减少启动所需的工作量。

LLM 的使用方式会影响它的风险性

未来保障,对抗炒作

我们生活在一个极为激动人心的时代,每周都有许多快速发展的 AI 技术问世,但这确实让制定路线图变得困难!在过去的一年里,几次发布了新的供应商功能、开源模型或 Python 包,这些都显著改变了技术格局。弄清楚使用哪些技术、框架和模型,以确保 LLM 应用能随着时间的推移保持价值,是一个挑战。如果在未来 6 个月内,它的功能会以原生免费或非常低的成本得到支持,那就没有意义去打造一个非常棒的应用。

另一个关键的考虑因素是要问 LLM 是否真的是解决问题的最佳工具。随着去年各种激动人心的消息,容易让人被冲动带走,什么都想用“LLM 来搞定”。和任何新技术一样,仅仅为了使用而使用通常是一个大错误,而随着 LLM 的热潮逐渐冷却,可能会发现我们炫酷的应用在实际使用中变得过时。

尽管如此,毫无疑问,LLM 可以提供一些令人难以置信的能力,因此,如果继续前进,以下是一些可能有用的建议……

采用“廉价 LLM 优先”策略

在网页设计中有一个概念叫做移动优先,即首先开发在功能较弱的手机和平板上能运行的网页应用,然后再考虑如何在更灵活的桌面浏览器上优化。这样做有时比反过来做更容易。类似的想法可以应用于 LLM 应用——在可能的情况下,尽量从一开始就使其与更便宜、更快速、成本更低的模型兼容,例如 GPT-3.5-turbo,而不是 GPT-4。这些模型的成本仅为前者的一小部分,而且通常会迫使设计过程朝向更加优雅的解决方案,将问题分解成更简单的部分,减少对昂贵且缓慢的模型的依赖。

当然,这并非总是可行的,而那些先进的 LLM 存在是有原因的,但许多关键功能可以通过较不强大的 LLM 来支持——如简单的意图分类、规划和记忆操作。也有可能,通过精心设计工作流,可以开辟不同的流,其中一些使用较不强大的 LLM,另一些则使用更强大的 LLM(我会在之后的博客文章中讨论这个话题)。

在未来,当那些更先进的 LLM 变得更便宜、更快速时,你可以将更基础的 LLM 替换掉,而你的应用程序可能会在几乎不需要额外努力的情况下奇迹般地改善!

避免使用本地 API,而应使用通用接口

使用通用接口是一种良好的软件工程方法。在 LLM 的情况下,这意味着使用一个服务或 Python 模块,它提供一个固定接口,可以与多个 LLM 提供商进行交互。一个很好的例子是langchain,它提供与多种 LLM 的集成。通过从一开始就使用 Langchain 与 LLM 进行通信,而不是使用本地 LLM API,我们可以在未来以最小的努力更换不同的模型。

另一个例子是使用autogen为代理提供支持,即使是使用OpenAI 助手也是如此。通过这种方式,当其他本地代理可用时,你的应用程序可以比围绕 OpenAI 的本地实现构建整个流程更容易进行调整。

代理还是链条?你可以同时使用两者!

LLM 开发中的一个常见模式是将工作流拆分为一系列条件步骤,使用诸如promptflow之类的框架。这些链条被明确定义,因此我们大致知道在我们的应用程序中会发生什么。它们是一个很好的起点,具有较高的透明度和可复现性。然而,它们不太适合处理边缘情况,这时自主 LLM 代理的组合可以很好地工作,因为它们能够迭代地朝着解决方案前进,并且从错误中恢复(大多数情况下)。这些代理的问题在于——至少目前——由于其迭代性质,它们可能会有点慢,因 LLM 令牌的使用而变得昂贵,而且有时会有点失控并且壮观地失败。不过,它们很可能是LLM 应用程序的未来,因此即使现在没有在应用程序中使用它们,做好准备也是一个好主意。通过将工作流构建为模块化链条,你实际上是在做这件事!工作流中的各个节点可以稍后替换为代理,在需要时提供两全其美的解决方案。

需要注意的是,这种方法存在一些局限性,LLM 响应的流式传输变得更加复杂,但根据你的用例,收益可能会超过这些挑战。

使用Promtpflow将 LLM 工作流中的各个步骤连接起来。这有几个优点,其中之一是未来可以用更先进的技术替换这些步骤。

你真的想让你的应用程序动态生成代码吗?

看到自动生成代理和 Open AI 助手生成代码并自动调试解决任务,真的让人惊叹,我觉得这就像是未来一样。这也开启了像“LLM 作为工具制造者”(LATM,Cai et al 2023)这样的惊人机会,在这种情况下,你的应用程序可以生成自己的工具。话虽如此,根据我个人的经验,到目前为止,代码生成还是有点“狂野”。是的,可以优化提示并实现验证框架,但即使生成的代码完美运行,当解决新任务时,它是否是正确的?我遇到过许多不正确的情况,而且通常很难发现——图表的刻度、在数组中加总错误的元素,或者从 API 中检索到略微错误的数据。我认为随着 LLM 和框架的进步,这种情况会有所改变,但目前来说,我会对让 LLM 在生产环境中动态生成代码非常谨慎,至少现在还是倾向于选择一些人工审核的环节。

从 LLM 增强型应用程序开始,而不是 LLM 优先的应用程序

当然,有很多用例绝对需要 LLM。但是为了更好地适应,选择那些 LLM 为过程增加价值,而不是成为过程的应用程序可能更为合理。想象一下,一个已经对用户有用的网页应用,呈现数据。这款应用可以通过实现 LLM 改进来提升其数据查找和总结的功能。通过稍微减少对 LLM 的依赖,应用程序也就能更少暴露于 LLM 性能带来的问题。显而易见,但在深入探索生成性 AI 之前,很容易忽视从小处着手。

别忘了……呃……对了,内存!

向 LLM 发送提示会产生费用,并可能导致用户在等待响应时体验较差。在许多情况下,提示与之前的请求相似或相同,因此能够记住过去的活动以供重用,而不必重新调用 LLM 是很有用的。一些很棒的工具包,如memgptGPTCache,使用文档嵌入的向量存储来持久化“记忆”。这与常见的RAG 文档检索技术相同,记忆实际上是分块的文档。稍微不同的是,像 memgpt 这样的框架做了一些巧妙的事情,利用 LLM 来自我管理记忆。

然而,你可能会发现,由于特定的使用场景,你需要某种形式的自定义记忆管理。在这种情况下,有时能够查看和操作记忆记录,而无需编写代码,是很有用的。一款强大的工具是pgvector,它将向量存储能力与 Postgres 关系型数据库结合起来,以便进行查询,使得理解与记忆相关的元数据变得容易。

测试,测试,再测试

最终,无论你的应用是否使用 LLM,它仍然是一个软件应用,因此可以从标准的工程技术中受益。一种显而易见的方法是采用测试驱动开发。这对于由供应商提供的 LLM 尤为重要,因为你需要控制这些 LLM 的性能可能随着时间的推移而变化,这是任何生产应用都需要量化的内容。现有几种验证框架,promptflow 提供了一些简单的验证工具,并且在Microsoft AI Studio 中原生支持。还有其他测试框架,关键是从一开始就使用某种框架,为验证打下坚实的基础。

尽管如此,需要注意的是,LLM 并不是确定性的,根据使用场景,每次提供的结果可能会有所不同。这对测试产生了有趣的影响,因为预期结果并非一成不变。例如,测试摘要任务是否按要求工作可能是一个挑战,因为每次的摘要可能略有不同。在这些情况下,通常有用的是使用另一个 LLM 来评估应用 LLM 的输出。可以应用一些度量标准,如 Groundedness、Relevance、Coherence、Fluency、GPT Similarity、ADA Similarity,具体见例如Azure AI studio 的实现

一旦你拥有了一套出色的测试,能够确认应用程序按预期工作,你可以将它们整合进 DevOps 管道,例如,在应用程序部署前通过 GitHub Actions 运行这些测试。

使用第三方工具,省去一些工作

当然,并没有一种方案适用于所有情况,但对于实施大型语言模型应用的小型组织来说,开发每个解决方案的各个方面可能是一项挑战。将精力集中在业务逻辑上,并与用户密切合作,同时使用企业工具来处理像大型语言模型安全性这样的领域,而不是自行开发这些工具,可能更为明智。例如,Azure AI Studio 提供了一些很棒的功能,可以一键执行大型语言模型的安全检查,并轻松部署到 API 端点,同时集成监控和安全功能。其他供应商,如谷歌也有类似的产品

当然,像这样的功能是有成本的,但考虑到开发它们是一项重大的工作,它可能非常值得。

Azure AI 内容安全工作室是云供应商解决方案的一个很好的示例,可以确保你的大型语言模型应用安全,而无需任何开发工作。

人类参与,始终如此

即使是最强大的大型语言模型,也远未完美,因此任何使用它们的应用程序都必须有人类参与,以确保一切按预期工作。为了实现这一点,所有与大型语言模型应用的互动都必须被记录,并且要有监控工具。这个要求与任何良好管理的生产应用程序并无不同,唯一的区别是需要新的监控方式来捕获性能和安全问题。

人类可以扮演的另一个关键角色是,当大型语言模型应用出错时进行修正和改进。如上所述,查看应用的内存可以提供帮助,尤其是当人类可以调整内存时,与大型语言模型协作,向最终用户提供最佳体验。将这些修改后的数据反馈到提示调优和大型语言模型微调中,可以成为改进应用程序的强大工具。

结论

上述想法并不是操作大型语言模型的详尽指南,可能并不适用于每个场景,但我希望它们对一些人有帮助。我们现在正处于一段令人兴奋的旅程中!

参考文献

大型语言模型的挑战与应用,Kaddour 等,2023

大型语言模型作为工具制造者,Cai 等,2023.

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

如果你喜欢这篇文章,请点赞,我会很高兴如果你关注我!你可以在这里找到更多文章 这里.

Sora — 直观且详尽的解释

原文:towardsdatascience.com/sora-intuitively-and-exhaustively-explained-a54f83ea9c21?source=collection_archive---------4-----------------------#2024-03-22

视频生成 | 多模态建模 | OpenAI

新时代的前沿视频生成

Daniel WarfieldTowards Data Science Daniel Warfield

·发布于 Towards Data Science ·阅读时间 18 分钟·2024 年 3 月 22 日

--

“Patchmaster” 由 Daniel Warfield 使用 MidJourney 创建。所有图片均为作者提供,除非另有说明。

在这篇文章中,我们将讨论 Sora,OpenAI 最新的前沿视频生成模型。我们将从描述 Sora 所依赖的基本机器学习技术开始,然后讨论关于 Sora 本身的信息,包括 OpenAI 的技术报告和相关的推测。阅读完这篇文章后,你将对 Sora(可能)是如何工作的有一个扎实的理解。

Sora 能够生成的视频质量示例。不幸的是,由于版权问题,我不能在这里包括实际的 Sora 生成视频。同时,由于 Medium 的文件大小限制,我也不能上传高质量的视频。因此,我将提供链接到 OpenAI 网站上的视频。此视频来自 这里,而实际的 Sora 视频可以在 这里 找到。

这对谁有用? 任何对生成式 AI 感兴趣的人。

这篇文章有多复杂? 这篇文章并不复杂,但有很多概念,可能会让经验较少的数据科学家感到有些吃力。

前提条件: 无需任何前提,但一些机器学习经验可能会有所帮助。如果你感到困惑,可以参考文中提到的相关文章,或文章末尾的推荐阅读。

定义 Sora

“化学直觉的火花”——以及 AlphaFold 3 中的重大局限!

原文:towardsdatascience.com/sparks-of-chemical-intuition-and-gross-limitations-in-alphafold-3-8487ba4dfb53?source=collection_archive---------6-----------------------#2024-06-05

在 DeepMind 发布其迄今为止最先进的生物分子结构预测模型的三周后,进行的观察。

LucianoSphere (Luciano Abriata, PhD)Towards Data Science LucianoSphere (Luciano Abriata, PhD)

·发表于Towards Data Science ·阅读时间:16 分钟·2024 年 6 月 5 日

--

图由作者绘制。

索引

- 介绍

  • AlphaFold 3 是什么,它与之前版本的区别 超越蛋白质

*社区黑客合作

-* 蛋白质存在的问题,包括 AlphaFold 2 没有遭遇的一些问题

  • AlphaFold 3(不了解的)脂质物理化学

  • 膜中的蛋白质

  • 蛋白质-核酸复合物

  • 蛋白质中的离子和金属位点** 蛋白质中的金属离子 - 讨论与结论 对于结构生物学家

对人工智能和数据科学爱好者的意义

介绍

就在三周前,AlphaFold 3 作为 DeepMind(以及与 Google 相关的另一家公司 Isomorphic Lab)最新的工具诞生,应用人工智能解决在原子级别理解生物学的问题。正如我在专门的文章中详细介绍的……

稀疏自编码器、加性决策树以及 AI 可解释性中的其他新兴话题

原文:towardsdatascience.com/sparse-autoencoders-additive-decision-trees-and-other-emerging-topics-in-ai-interpretability-9ab1eaf14d3e?source=collection_archive---------6-----------------------#2024-06-13

TDS 编辑Towards Data Science TDS 编辑

·发表于 Towards Data Science ·以 时事通讯 形式发送 ·阅读时间 4 分钟 ·2024 年 6 月 13 日

--

想要写你的第一篇 TDS 文章吗? 我们始终欢迎新作者的投稿

随着大规模语言模型(LLM)变得越来越庞大,AI 应用变得更加强大,理解其内部机制的探索变得更加困难且更加紧迫。关于黑箱模型风险的讨论并不是什么新鲜话题,但随着 AI 驱动工具的影响力不断扩大,幻觉现象和其他次优输出频繁出现在浏览器和用户界面中,这使得从业者(和最终用户)比以往任何时候都更加重要,必须抵制接受 AI 生成内容的表面价值。

我们的每周亮点栏目深入探讨了在广泛使用大规模语言模型时代,模型可解释性和解释性的问题。从对一篇有影响力的新论文的详细分析,到使用其他近期技术的实践实验,我们希望你能花时间探索这一日益重要的话题。

  • 手动深入探索 Anthropic 的稀疏自编码器在短短几周内,Anthropic 的“单义性扩展”论文吸引了 XAI 社区的广泛关注。Srijanie Dey 博士为任何对研究人员的主张和目标感兴趣的人提供了一份面向初学者的介绍,并展示了他们是如何提出“创新方法来理解神经网络中不同组件如何相互作用以及每个组件的角色”的。

  • 大型语言模型中的可解释特征对于“单义性扩展”论文的理论基础,若想获取一篇高水平、图文并茂的解析文章,我们强烈推荐Jeremi Nuer的 TDS 首篇文章——阅读之后,你将对研究人员的思路以及该研究对未来模型发展的意义有一个清晰的了解:“随着改进停滞,且大规模语言模型越来越难以扩展,如果我们希望在性能上实现下一次飞跃,真正理解它们的工作原理将变得至关重要。”

  • 人工智能可解释性的意义

    斯蒂芬妮·基尔默从特定模型及其带来的技术挑战中抽身,稍微“哲学化”了一些,在她关于可解释性局限性的文章中提出;她认为,试图揭示这些黑箱模型的努力可能永远无法实现完全的透明度,但仍然对机器学习研究人员和开发者而言非常重要,值得投入研究。

图片由乔安娜·科辛斯卡提供,来源于Unsplash

  • 加性决策树在他最近的研究中,W·布雷特·肯尼迪一直专注于可解释的预测模型,剖析其底层数学,并展示它们在实践中的工作原理。他对加性决策树的深入研究是对这种模型的有力且全面的介绍,展示了它如何旨在补充有限的可解释分类和回归模型选项。

  • 深度解析累计局部效应图(ALEs)与 Python为了丰富我们的内容,我们非常高兴地分享Conor O'Sullivan关于累计局部效应图(ALEs)的动手探索:一种较老但可靠的方法,即使在模型中存在多重共线性的情况下,也能提供清晰的解释。

本周有兴趣深入了解其他话题吗?从量化到宝可梦优化策略,我们都有涉及!

  • 在一个令人着迷的项目演示中,Parvathy Krishnan、Joaquim Gromicho 和 Kai Kaiser 展示了他们如何结合多个地理空间数据集和一些 Python 来优化选择医疗设施位置的过程。

  • 学习权重量化如何工作以及如何应用于实际的深度学习工作流程中——Chien Vu的教程既详尽又易懂。

  • 背包问题是一个经典的优化挑战;Maria Mouschoutzi 博士为此提出了一个有趣的新角度,展示了如何通过建模和 PuLP,一个 Python 优化框架来创建最强大的宝可梦团队。

  • 对于许多机器学习专业人士来说,从 RAG 系统中挤出最大价值仍然是一个重要的优先事项。Leonie Monigatti仔细分析了衡量上下文相关性的潜在解决方案。

  • 在作为技术巨头和高速成长初创公司数据领导者十多年后,Torsten Walbaum分享了他在一个基本问题上积累的见解:我们如何理解数据?

  • 数据分析师可能不常把自己当作程序员,但跨学科学习仍然有很大的空间—正如玛丽亚·曼苏罗娃在一篇数据聚焦的软件工程最佳实践总结中所展示的。

感谢你支持我们作者的工作!我们喜欢发布新作者的文章,所以如果你最近写了一篇有趣的项目教程、教程或关于我们核心主题的理论思考,不要犹豫,与我们分享

直到下一个变量,

TDS 团队

使用目标信息稀疏化知识图谱

原文:towardsdatascience.com/sparsifying-knowledge-graph-using-target-information-8fb1014cbb0f?source=collection_archive---------4-----------------------#2024-05-05

为监督任务稀疏化知识图谱,使用 PMI 去除无关的边;并通过医学数据的具体示例来说明

Sria LouisTowards Data Science Sria Louis

·发表于 Towards Data Science ·阅读时间:9 分钟·2024 年 5 月 5 日

--

“医学知识图谱” — 由 deepai.org 生成

以下是我在微软研究院工作时,在 Elad Yom-Tov 教授指导下进行的一项研究项目的简化片段。

前提要求:熟悉监督学习和对知识图谱有基本了解,包括其在特征工程中的应用。

TL;DR

你有一个监督学习任务,从高维二进制特征空间中预测目标变量y,并且你想利用一个知识图谱,其中每个节点代表一个特征。

问题在于知识图谱很密集。具体来说,有许多无关的边(插曲:维基百科过度链接危机)。

窍门:通过测量每条边的出现与目标变量之间的点互信息(PMI),评估每条边的相关性。去除相关性较低的边。完成!

现在,让我们进入具体细节。

首先,在第一章中,我们将探讨图稀疏化的需求。在第二章中,我们将通过医学领域的一个例子来具体说明。然后,在最后一章中,我们将深入讨论核心主题:使用 PMI 进行图稀疏化。祝阅读愉快!

第一章:需求

图和特别是知识图谱(KG)无处不在。随着图数据库基础设施的发展以及理论机器学习工具(如节点嵌入)的进步,这项技术正在成熟。

在现实世界中处理知识图谱时的一个关键挑战是——在我看来,这个挑战永远都存在——密度 即使是具有多项式复杂度的算法,也可能在密集图上失败。展望未来,如果硬件进步——未来的知识图谱将会在节点数量和边的数量上都有所增加。[此外,我预测并希望,在这场军备竞赛中,我们将逐步看到层次化的超图和其他怪异结构加入其中。]

知识图谱的优势在于它们能够在广泛的知识领域中持有复杂的连接,例如,所有医学知识或所有已知的天文物体,因此你可以利用它们来丰富更简单的特征,例如,如果你的特征非常稀疏,例如非常罕见的医学病症,它们可以通过图谱中的“邻接”信息进行丰富。

这种庞大规模带来了一个缺点:知识图谱通常过于庞大。除了计算复杂性外,这里还有另一个核心问题。知识图谱覆盖了领域中的一切,因此大部分信息与你的特定目标无关。换句话说,它们具有过度敏感性(即覆盖率/召回率)——因此特异性较低(即精确度)。你可能会说这是机器学习中的常见权衡;那么,图谱中的特殊性是什么呢?答案是,在典型的真实世界知识网络中,度分布是重尾的,存在巨大的“枢纽”,即具有极高度的节点,也就是拥有许多边的节点。你可能熟悉这种枢纽在社交网络和物理学中的效果(“六度分隔”)以及图论视角下的效果(随机图的直径)——在我们的上下文中,这意味着少数几个枢纽会显著减少图中大多数节点之间的距离,从而将图中的任何信号迅速扩散到整个图谱,抑制了任何可能与特定目标相关的更微妙的信息。

为了具体化,在下一章中,我们将描述我们处理的一个用例,然后,在第三章中,我们将描述一种通过删除无关边来克服密度问题的监督任务方法。可以直接跳到第三章。

第二章:具体示例

让我们考虑以下的监督任务。我们已简化细节,以专注于有趣的部分——PMI。

研究假设

我们可以根据一个人的医学维基百科浏览历史预测潜在的医学病症。例如,如果一个人浏览过有关头痛、吸烟、咳嗽和牙齿变色的文章,他们可能面临某种肺病的较高风险。再一次,这是一个简化的研究场景和一个假设性情境。

数据

  • 自变量以表格数据的形式给出,存储在设计矩阵 X 中。每个条目都有二进制值,0/1。

  • 每一行代表一个患者——即一个患者的维基百科阅读历史,作为二元变量——每个医学维基百科文章对应一个变量(这类似于 NLP 中的词袋编码)。

  • 每一列代表维基百科中的一个医学术语。

  • 例如,对于患者p和维基百科文章f(例如肺炎),X[i, f] = 1表示患者p曾访问过文章f。也就是说,“患者阅读了关于肺炎的文章”。显然,患者并不会查看与他们病情相关的具体维基百科文章,但我们从以往的研究中了解到,在某些情况下,他们会在线搜索与真实病情相关的症状。而这正是知识图谱变得非常有用的地方:“此人阅读了与图谱中某个与特定医学病症相关的邻域的症状信息”。

  • 我们将分别用mn表示行数(患者数)和列数(二元特征数)。

  • 目标(因变量)y是未来某医学病症的存在,例如严重的 COVID-19。

  • 此外,我们还拥有一个知识图谱(KG),即完整的医学维基百科,其中节点是医学术语的维基百科文章,边是超链接。请注意,这是一张有向图。

  • 为了简化起见,假设每个特征(列)在图谱中都表示为一个单一的节点。

使用知识图谱来丰富特征是一个广泛的话题,因此我们在这里不会详细讨论。然而,有一种值得注意的方法是节点嵌入,其中二元特征被转化为一个连续的、低维的向量空间。

现在我们可以利用这个医学示例场景来更好地理解稀疏化的需求:

  1. 还记得我们之前提到的中心节点吗?虽然一些中心节点对我们的目标可能有意义,但许多节点由与目标无关的高阶节点组成,比如“医学专业列表”或“医学症状列表”,这样的节点会在图谱中放大噪声,从而抑制关于某些特定稀有症状的重要信息。那么我们如何有效地识别这些中心节点,并找出它们的哪些边是无关的、可以安全移除的呢?

  2. 此外,可能需要利用相同的数据库来预测两个不同的目标变量,例如女性乳腺癌和男性前列腺癌,它们表现出不同的行为。在这种情况下,使用整个知识图谱可能并不具有优势,因为某些边与我们特定的目标标签可能无关。因此,可能需要排除与异性相关的边。那么我们如何系统地移除与特定目标变量无关的边呢?

第三章:PMI 技巧

我们的目标很明确:我们希望从知识图谱中去除那些与目标变量无关的边。虽然有多种数学定义可以描述相关性,但我们选择使用点互信息(PMI),因为它既简单又直观。

PMI 是信息论中的一个基本工具,接下来我们来讨论它:PMI 到底是什么?我们将首先概述其定义,然后力求建立更好的直观理解。

PMI 被描述为“自然语言处理(NLP)中最重要的概念之一”[参见 6.6]

PMI:点互信息

PMI 作为两个离散随机变量之间著名的互信息的点估计。给定两个随机变量XY的观察结果 x 和 y,我们定义:

点互信息(来自维基百科)

等式是贝叶斯定理的直接结果,为我们提供了关于 PMI 的不同视角,并且理想情况下,可以帮助我们建立直观理解:

如果 X 和 Y 是独立的,那么 p(x,y)=p(x)p(y)。因此,第一个项可以理解为以下两者之间的比率:

  • p(x, y) = 实际联合分布的点估计,带有依赖性,

  • p(x)p(y) = 假设两个变量独立时的联合分布。

看看最后一项,你可能会意识到 PMI 是在量化“在知道 y 的情况下,x 的概率如何变化”,反之亦然。

让我们做一个小练习,以便更好地理解 PMI:

  • 假设 1%的患者患有重症新冠,p(covid) = .01

  • 在曾经患有肺炎的患者中,有 4%的人患上了重症新冠。p(covid|pneumonia) = .04

  • 那么,给定肺炎的情况下,新冠的概率高于没有肺炎信息的情况,因此 PMI 很高。PMI(covid;pneumonia) = log(.04/.01) = 2。非常直观,对吧?

PMI 在其简单性中非常优美,但关于其特性、变化和应用还有许多可以探索的内容。一个值得注意的变体是归一化 PMI,它的范围在-1 到 1 之间。这个特性使得可以在多个随机变量对之间进行比较和筛选。请记住这一点——它稍后会变得非常有价值。

回到我们的任务

我们有一个大型稠密图,表示我们的二元特征之间的链接,并且我们有一个目标变量。我们如何智能地稀疏化这个图?

对于两个特征 v1 和 v2 之间的边 e,我们定义一个指示随机变量 x_e,当且仅当两个特征的值都是 1(True)时,x_e 的值为 1,表示这两个医学术语在患者中同时出现。现在,观察这条边和目标变量 y。我们提出了一个简单的问题:这条边与 y 相关吗?现在我们可以简单地通过 PMI 来回答!如果 PMI[x_e,y]接近零,则这条边与我们的目标无关,否则这条边包含一些相关信息

所以,最后总结,我们移除所有带有:

其中 alpha 是一个超参数,通过调节它,你可以控制图的稀疏性(与泛化误差之间的权衡,也就是过拟合的风险)。

三个警告 — 以及潜在的改进

警告 1) 特征空间通常表现出稀疏性,导致 PMI 的分子和分母都为零,因此我们最好不要删除这些边,因为我们对它们没有任何信息。

你可能会问:如果我们通常不删除边,难道我们真的在“稀疏化”图吗?答案在于枢纽节点。记得那些枢纽节点吗?它们实际上通常不会是零,因为它们是枢纽节点。

警告 2) 另一个值得思考的问题是:为什么要将边变量定义为“两个特征的值都是 1”?或者,我们也可以检查任一特征是否具有值为 1。因此,代替 y = x1 和 x2,我们可以考虑 y = x1 或 x2。这是一个有效的观点。这些不同的实现方式传达了你对领域理解的略微不同的叙述,并且可能适用于不同的数据集。我建议根据你的具体使用场景探索不同的版本。

警告 3) 即使概率不为零,在医学领域它们通常是非常非常小的,因此为了增加稳定性,我们可以定义条件 PMI:

条件 PMI

用通俗的语言说:我们在一个概率子空间中计算 PMI,其中第三个事件发生。

具体而言,在知识图谱中,请记住图是有向的。我们将使用 cPMI 来检查两个特征之间的边 e=(v1,v2) 是否相关,前提是第一个特征为正。

换句话说,如果 v1 从未出现,我们可以声明我们没有足够的信息来删除这条边,即使是为了删除它。

结论

现在,当我们知道什么是 PMI 时,我们就明白了,为了在知识图谱中删除不相关的边,我们可以检查每条边和目标变量之间的逐点互信息,并删除所有不相关的边。砰!🎤

随机对照试验中的空间挑战

原文:towardsdatascience.com/spatial-challenges-in-rcts-4a4d9ada5c59?source=collection_archive---------6-----------------------#2024-04-24

地点,地点,地点

Leonardo MaldonadoTowards Data Science Leonardo Maldonado

·发布于 Towards Data Science ·阅读时间 10 分钟 ·2024 年 4 月 24 日

--

图像由 AI 工具如 Photo Realistic GPT 和 Super Describe 生成。

随机对照试验(RCT)是研究因果关系、识别新治疗、干预措施和政策效果的标准方法。然而,其结果的可靠性和适用性可能会受到空间因素的显著影响(即与研究实施所在地理背景相关的特征)。理解并解决这些空间问题,尤其是当治疗应用于现实环境时,对于防止和减轻 RCT 结果中的潜在失真和偏差至关重要。那么,这些空间因素究竟是什么?它们如何扭曲 RCT 的结果?更重要的是,研究人员如何有效管理这些由空间引发的变化,以保持研究的完整性?

为什么空间因素很重要?

当我在随机对照试验(RCT)的背景下提到空间因素时,我指的是地理因素在这些研究中的作用,如果不加以考虑,可能会导致严重的误解。这些因素包括地点的气候、人口密度、文化习惯、卫生基础设施,甚至社会经济状况。

空间异质性可能导致随机对照试验(RCT)结果的显著变化,尤其是在…

空间索引:网格系统

原文:towardsdatascience.com/spatial-index-grid-systems-f02880fb147a?source=collection_archive---------9-----------------------#2024-06-12

空间索引中的网格系统,使用 GeoHash 和 Google S2

Adesh Nalpet AdimurthyTowards Data Science Adesh Nalpet Adimurthy

·发表于Towards Data Science ·12 分钟阅读·2024 年 6 月 12 日

--

本文是Stomping Grounds: 空间索引的续集,但如果你错过了第一部分也没关系——你依然能在这里找到很多新的见解。

3. Geohash

Geohash:由 Gustavo Niemeyer 于 2008 年发明,它将地理位置编码为一个短字符串,由字母和数字组成。它是一个分层的空间数据结构,通过使用 Z-order 曲线(第二部分)将空间划分为网格形状的桶。

3.1. Geohash — 直觉

地球是圆的,或者更准确地说是椭球体。地图投影是一组将地球表示为平面的变换。在地图投影中,来自地球表面的地点的坐标(纬度和经度)被转换为平面上的坐标。而 GeoHash 使用等距矩形投影

GeoHash 的核心就是巧妙地使用了 Z-order 曲线。将地图投影(矩形)分割成 2 个相等的矩形,每个矩形都由唯一的比特串标识。

空间索引:R 树

原文:towardsdatascience.com/spatial-index-r-trees-5ac6ad36ca20?source=collection_archive---------10-----------------------#2024-07-30

用于空间索引的数据驱动结构

Adesh Nalpet AdimurthyTowards Data Science Adesh Nalpet Adimurthy

·发表于Towards Data Science ·阅读时间 10 分钟·2024 年 7 月 30 日

--

如果你一直关注空间索引系列,它从多维索引的需求开始,并介绍了空间填充曲线,然后深入探讨了网格系统(GeoHash 和 Google S2)和镶嵌(Uber H3)。

在这篇文章中,我们将探讨R 树数据结构(数据驱动结构),它广泛用于存储多维数据,例如数据点、线段和矩形。

1. R 树和矩形

例如,考虑下面的大学布局计划。我们可以使用 R-Tree 数据结构对地图上的建筑物进行索引。

为此,我们可以在建筑物或一组建筑物周围放置矩形,然后对其进行索引。假设有一个更大的地图区域代表一个更大的部门,并且我们需要查询该部门内的所有建筑物。我们可以使用 R-Tree 来查找所有位于(部分或完全包含在)更大区域内的建筑物(查询矩形)。

在上图中,红色矩形表示查询矩形,用于询问...

空间索引:空间填充曲线

原文:towardsdatascience.com/spatial-index-space-filling-curves-e67baec2186a?source=collection_archive---------18-----------------------#2024-06-11

多维数据的空间索引与空间填充曲线

Adesh Nalpet AdimurthyTowards Data Science Adesh Nalpet Adimurthy

·发表于 Towards Data Science ·12 分钟阅读·2024 年 6 月 11 日

--

0. 概述

空间数据由于网络服务跟踪用户在何时何地进行操作而迅速增长(/正在增长)。大多数应用程序都会添加位置标签,并且通常允许用户在特定的地点和时间进行签到。这一增长主要归因于智能手机,它们充当了位置传感器,使得捕捉和分析此类数据变得前所未有的容易。

本文的目标是为多维索引的需求奠定基础,并深入探讨广泛应用于关系型和非关系型数据库中的空间填充曲线在空间索引中的使用。我们将分析每种类型的优缺点,并讨论当今最流行的索引类型。

空间索引分为两大类:空间驱动结构和数据驱动结构。数据驱动结构,如 R 树家族,是根据数据本身的分布量身定制的。空间驱动结构包括划分树(kd 树、四叉树)、空间填充曲线(Z-order、Hilbert)和网格系统(H3、S2、Geohash),每种…

空间索引:镶嵌

原文:towardsdatascience.com/spatial-index-tessellation-aca39463fe9f?source=collection_archive---------4-----------------------#2024-06-17

镶嵌在空间索引中的应用以及 Uber H3 如何工作

Adesh Nalpet AdimurthyTowards Data Science Adesh Nalpet Adimurthy

·发表于Towards Data Science ·15 分钟阅读·2024 年 6 月 17 日

--

本文是空间索引:网格系统的延续,我们将在其中为镶嵌奠定基础,并深入探讨Uber H3的细节。

0. 基础

镶嵌或铺砖是将空间覆盖/划分为更小的、不重叠的形状,这些形状完美地拼接在一起,没有间隙或重叠。在空间索引中,镶嵌用于将地球表面划分为可管理的单元,以便高效地存储数据、查询和分析。

为什么需要地理网格系统(镶嵌系统):现实世界充满了各种地理元素,既有自然的也有人工的,它们都没有遵循任何一致的结构。为了对这些元素进行地理算法或分析,我们需要一种更抽象的形式。

地图是一个很好的起点,也是最常见的抽象形式,大多数人都熟悉它。然而,地图仍然包含各种不一致性。这就需要一个网格系统,它将杂乱的地理空间转化为一个更加干净和结构化的数学空间,使得执行计算和查询变得更加容易。

Python 中的空间插值

原文:towardsdatascience.com/spatial-interpolation-in-python-0864abca6d48?source=collection_archive---------4-----------------------#2024-08-08

使用反距离加权法推断缺失的空间数据

米兰·贾诺索夫Towards Data Science 米兰·贾诺索夫

·发表于Towards Data Science ·阅读时间:5 分钟·2024 年 8 月 8 日

--

反距离加权法(IDW)是一种地统计学方法,用于根据周围已知值来推断特定位置的空间变量的未知值。IDW 的基本思想遵循托布勒的地理第一定律,内容为“所有事物相互关联,但近的事物比远的事物更相关”。也就是说,一个已知值的空间单元离一个未知值的空间单元越近,它对插值结果的影响就越大。

在这篇文章中,我们测试 IDW 方法,通过以非洲为例推断缺失的国家级人口密度水平。为此,我使用一张由 Natural Earth 提供的、经过人口估算并精心整理的世界地图(关于该数据的公共可用性请参考此处),然后人为删除若干数据点,这些数据点将通过 IDW 方法进行推断。最后,我将比较原始值与推断后的删除人口密度值。

所有图片均由作者创建。

数据准备

在这里,我将依赖 GeoPandas 的内置地图数据集“naturalearth_lowres”。这是由 Natural Earth 提供的全球地图,并由国家级别的...

说话,不要打字:探索与大型语言模型(LLMs)的语音交互

原文:towardsdatascience.com/speak-dont-type-exploring-voice-interaction-with-llms-part-1-732257710e9d?source=collection_archive---------4-----------------------#2024-04-23

为 LLM 应用增添语音模式

Youness MansarTowards Data Science Youness Mansar

·发布于Towards Data Science ·阅读时间 6 分钟·2024 年 4 月 23 日

--

图片由Quino Al拍摄,来源于Unsplash

许多 LLMs,特别是开源的 LLMs,通常仅限于处理文本,或偶尔处理带有图像的文本(大型多模态模型,LMMs)。但如果你想用声音与 LLM 进行沟通怎么办?得益于近年来强大的语音转文本开源技术的发展,这一目标变得可实现。

我们将探讨 Llama 3 与语音转文本模型的集成,所有操作都将在一个用户友好的界面中完成。这一融合使得通过语音与 LLM 进行(接近)实时的交流成为可能。我们的探索涉及选择 Llama 3 8B 作为 LLM,使用 Whisper 语音转文本模型,并利用 NiceGUI 的功能——这是一个在后端使用 FastAPI,前端使用 Vue3,并通过 socket.io 互联的框架。

阅读本文后,你将能够为 LLM 增添一种新的音频模式。这将允许你构建一个完整的端到端工作流和用户界面,使用语音来命令和提示 LLM,而不是打字。这一功能对于移动应用特别有用,因为在手机上打字可能不像在桌面上那么方便。此外,集成这一功能还可以增强你 LLM 应用的可访问性……

基于 DNN 的语音处理工具中的说话人隐私保护

原文:towardsdatascience.com/speakers-privacy-protection-in-dnn-based-speech-processing-tools-f446ad1d8871?source=collection_archive---------15-----------------------#2024-08-20

一种新颖的隐私保护语音处理方法,通过空间填充向量量化匿名化说话人属性

Mohammad Hassan ValiTowards Data Science Mohammad Hassan Vali

·发布于Towards Data Science ·阅读时间 9 分钟·2024 年 8 月 20 日

--

本文简要介绍了我们提出的隐私保护技术——Privacy-PORCUPINE [1],该技术在 2024 年 Interspeech 会议上发表。文章讨论了在 DNN 基础的语音处理工具瓶颈中使用普通向量量化时可能出现的隐私威胁。欲了解更多详情,请查看此链接下的论文。

图片由Dayne Topkin提供,来源于Unsplash

语音是人类与技术之间互动的便捷媒介,但证据表明它也使说话人面临隐私威胁。一个核心问题是,除了可能是私密的语言内容,语音还包含诸如说话人身份、年龄、健康状况、种族背景、性别和情绪等私密的附加信息。将这些敏感信息透露给听众可能使说话人面临价格抬高、追踪、骚扰、敲诈和身份盗窃等威胁。为了保护说话人,隐私保护语音处理旨在通过剥离下游任务不需要的私密信息来匿名化语音信号 [2]。

一个常见的隐私保护语音处理操作原理是通过一个信息瓶颈传递信息,瓶颈必须足够紧密,只允许所需的信息通过,并防止任何其他私人信息的传输。例如,可以通过自编码器实现这样的瓶颈,其中一个神经网络,称为编码器,将信息压缩到瓶颈中,另一个网络,解码器,重建所需的输出。瓶颈的信息传输速率可以绝对量化,但只有在量化的情况下才能量化,因此量化是任何隐私证明的必需组件[2]。下图展示了一个矢量量化变分自编码器(VQ-VAE)架构及其瓶颈。

图 1:矢量量化变分自编码器(VQ-VAE)的架构。(图片来源:作者)

正如隐私 ZEBRA 框架[3]中所指出的,我们需要从平均泄露私人信息的比特数以及最坏情况下泄露(比特数)的角度来表征隐私保护。矢量量化(VQ)[4]是一个恒定比特率量化器,因为它使用一个包含 K 个元素的码本对输入进行量化,且此码本的索引可以用 B=log2(K)比特表示。这个 B 等于私人信息泄露的平均比特数。就最坏情况的泄露而言,很明显,不同的码本元素以不同的频率使用(见下图 2.1)。这意味着,相对较小的发言人子集可能会被分配到特定的码本索引,以至于每当一个发言人被分配到该索引时,可能的发言人范围比其他码本索引的范围要小,且相应的私人信息泄露也较大(对于分配到该特定码本索引的发言人)。然而,据作者所知,这种泄露此前并未被量化,也没有现有的解决方案来补偿这种泄露的增加。因此,我们的主要目标是修改 VQ,使所有码本元素的出现概率相等,从而防止私人信息泄露(并因此改善最坏情况下的泄露)。

作为解决方案,我们在此采用了我们最近提出的向量量化修改方法,称为空间填充向量量化(SFVQ)[5],它将空间填充曲线引入到向量量化中。我们将 SFVQ 定义为向量量化的线性延续,使得后续的代码本元素通过一条线连接,在该分段连续线的任意点上可以映射输入(见图 2.2)。要了解更多关于 SFVQ 的内容,请参阅这篇文章

在我们的技术中,名为 Privacy PORCUPINE [1],我们提出了结合 SFVQ 的代码本重新采样方法,其中向量量化器沿着 SFVQ 的曲线重新采样,使得所有重新采样的代码本元素具有相等的出现概率(见图 2.3 和 2.4)。

图 2:1. 5 位向量量化在高斯分布(灰点)上的代码本向量(蓝点);Voronoi 区域用绿色表示 2. 同样的高斯分布上应用 5 位空间填充向量量化(SFVQ)(黑色曲线,代码本向量为蓝色) 3. 相同的 SFVQ 曲线及重新采样的代码本向量(红色交叉) 4. 相同的重新采样代码本向量及其 Voronoi 区域(绿色)。(图像由作者提供)

我们如何衡量泄露度?

图 2.1 展示了在输入较少的区域,Voronoi 区域较大。因此,这些较大的 Voronoi 区域包含较少的输入样本。类似地,较小的 Voronoi 区域包含较多的输入样本。这些差异是由于通过最小化均方误差(MSE)准则来优化代码本;较常见的输入通过较小的误差量化,从而最小化平均误差。我们的目标是确定输入不均匀分布到代码本条目时,如何影响私密信息的泄露。

我们通过衡量新的观察如何减少未知说话者可能身份的群体大小来度量泄露度。换句话说,假设我们已知说话者属于一个大小为 M 的群体。如果我们有一个观察结果,表明说话者被量化为索引 k,我们需要评估 M 中的有多少个说话者 L 会被量化到相同的索引。这个减少可以通过群体比例 L/M 来量化,表示泄露了 B_leak=log2(M/L)比特的信息。

在极端情况下,可能只有一个说话人被量化到某个特定的索引。这意味着在一个任意大的 M 中,只有一个说话人 L=1 被量化到该区间,尽管在实践中,我们只能对有限的 M 进行验证结果。尽管如此,从理论上讲,如果 M → ∞,那么信息泄漏也会发散,即 B_leak → ∞比特。我们所提出方法的主要目标是修改向量量化,以防止这种灾难性的泄漏。

码本重采样

在训练集上训练空间填充向量量化器(SFVQ)[5],该训练集包含了 M 个说话人的嵌入向量(图 2.2),我们将所有 M 个嵌入向量映射到已学习的曲线上。为了使用 K 个码本向量归一化出现频率,每个码本元素必须表示 M/K 个说话人嵌入。换句话说,每个 Voronoi 区域应包含 M/K 个说话人嵌入。考虑到这些映射到 SFVQ 曲线上的 M 个嵌入,我们从第一个码本元素(曲线的一端)开始,取前 M/K 个映射的嵌入,并计算这些向量的平均值。我们将这个平均向量定义为新的重采样码本向量(图 2.3 中的红色交叉点),代表第一块 M/K 个说话人嵌入。然后,我们继续类似的操作,直到计算出所有 K 个重采样码本向量,表示所有 K 块 M/K 个说话人嵌入。

实验

在我们的实验中,我们使用了Common Voice 语料库(版本 16.1)来获取大量的说话人数据。我们随机选择了 10,240 名说话人作为测试集,79,984 名说话人作为训练集。为了计算说话人嵌入,我们使用了 ECAPA-TDNN 的预训练说话人验证模型[6]。我们在训练集(说话人嵌入)上训练了向量量化(VQ)和空间填充向量量化(SFVQ)方法,采用不同的比特率,从 4 位到 10 位(16 到 1024 个码本向量)。

作为 VQ 和重采样 SFVQ 比较的示例,下面的图示展示了 VQ 和重采样 SFVQ 在 4 位信息(对应 16 个码本向量)情况下的出现频率。通过非正式的视觉检查,我们可以看到所提出方法的条目分布更加均匀,正如我们所期望的那样,但为了确认结果,我们需要进行适当的评估。为了比较 VQ 和重采样 SFVQ 得到的直方图,我们使用了不同的评估指标,这些指标将在下一节中讨论。

图 3:4 位量化情况下 VQ 和重采样 SFVQ 的码本出现概率。(图像来源:作者)

评估指标

假设我们有 K 个代码簿向量(区间)来量化 M 个讲者的样本。我们的目标是将样本均匀地分布到 K 个代码簿向量上,U(1,K),使得每个代码簿向量被使用 M/K 次。如果我们从均匀分布 U(1,K) 中抽取 M 个整数,就能获得直方图 h(k)。然后,如果我们计算 h(k) 各个区间的出现频率的直方图(即直方图的直方图),我们会发现新的直方图遵循二项分布 f(k),使得

其中随机变量 X 是每个区间内的出现次数,n 是实验次数(n=M),p 是每次实验的成功概率,p=1/K。在获得 VQ 和重采样 SFVQ 方法的代码簿索引出现次数的直方图 g(k)(图 3)之后,我们计算出 g(k) 各个区间内的出现频率直方图,记为 f_hat(k)。二项分布 f(k) 是理论最优分布,我们的观测值 f_hat(k) 应与其一致。因此,我们使用 Kullback-Leibler(KL)散度来评估观察分布与理想分布之间的距离。

通过获得出现频率的直方图 g(k),我们计算直方图的最小值除以总样本数(M=Σ g(k))作为最坏情况泄露。我们还计算了平均泄露,即出现频率的熵 g(k)/M。此外,我们使用稀疏度度量和标准差作为启发式度量,评估获得的直方图 g(k) 的均匀性。以下是评估公式:

结果

图 4 显示了 VQ 和重采样 SFVQ 在比特率函数下的性能。在每种情况下以及所有比特率下,所提出的方法(蓝线)低于 VQ(红线),这表明所提出的方法泄露的私人信息较少。需要提到的一个重要点是,正如预期的那样,重采样 SFVQ 使得平均泄露略高于 VQ,而重采样 SFVQ 和 VQ 的平均泄露都极其接近平均泄露的上限,在该上限下,出现频率的直方图是完全平坦的。

图 4:VQ 与重采样 SFVQ 在评估指标下的性能对比。(图片由作者提供)

结论

隐私保护语音处理变得越来越重要,因为语音技术的使用正在增加。通过将语音信号通过量化的信息瓶颈去除多余的私人信息,我们可以获得可证明的隐私保护。然而,这些保护依赖于量化水平的使用频率相等的假设。我们的理论分析和实验表明,使用最小均方误差(MSE)准则优化的向量量化通常不能提供均匀的发生频率。在最坏的情况下,即使量化器平均提供了充分的保护,某些说话人仍然可能被唯一识别出来。

我们提出了重采样 SFVQ 方法,以避免此类隐私威胁。因此,通过增加对较少出现输入的量化误差来实现隐私保护,而对较常见的输入则提高准确性(见图 2.4)。这与差分隐私理论[7]一致。

我们以说话人身份识别作为示例应用,尽管所提方法可以用于实现任何语音属性的可证明隐私泄露减少。总之,重采样 SFVQ 是一个通用的隐私保护语音处理工具。它提供了一种量化通过信息瓶颈传递信息量的方法,从而为具有可证明隐私保护的语音处理方法奠定基础。

GitHub 仓库

我们所提方法的实现代码及相关评估公开可用,链接如下:

[## GitHub - MHVali/Privacy-PORCUPINE

通过在 GitHub 上创建账户,参与 MHVali/Privacy-PORCUPINE 的开发。

github.com](https://github.com/MHVali/Privacy-PORCUPINE.git?source=post_page-----f446ad1d8871--------------------------------)

致谢

特别感谢我的博士项目导师Tom Bäckström 教授,他在这项工作中给予了我支持,并且是另一位贡献者。

参考文献

[1] M.H. Vali, T. Bäckström, “隐私 PORCUPINE:使用发生率归一化进行空间填充向量量化的说话人属性匿名化”,发表于Interspeech 会议论文集,2024 年。

[2] T. Bäckström, “语音技术中的隐私”,2024 年。[在线] 可在以下链接获取:arxiv.org/abs/2305.05227

[3] A. Nautsch 等, “隐私 ZEBRA:零证据生物识别评估”,发表于Interspeech 会议论文集,2020 年。

[4] M. H. Vali 和 T. Bäckström, “NSVQ:用于机器学习的向量量化中的噪声替代”,IEEE Access,2022 年。

[5] M.H. Vali, T. Bäckström, “使用空间填充曲线进行语音转换中的语音分析的可解释潜在空间”,发表于Interspeech 会议论文集,2023 年。

[6] B. Desplanques, J. Thienpondt, 和 K. Demuynck, “ECAPA-TDNN:在基于 TDNN 的说话人验证中强调通道注意力、传播与聚合,”发表于 Interspeech 会议论文集,2020 年。

[7] C. Dwork, “差分隐私:结果综述,”发表于

模型理论与应用国际会议

计算学。Springer,2008 年。

推测性解码:使用 Mixtral-8x7B 和 Gemma 加速推理

原文:towardsdatascience.com/speculative-decoding-for-faster-inference-with-mixtral-8x7b-and-gemma-f5b1487f5714?source=collection_archive---------4-----------------------#2024-03-08

使用量化模型以提高内存效率

Benjamin MarieTowards Data Science Benjamin Marie

·发表于 Towards Data Science ·阅读时间:7 分钟·2024 年 3 月 8 日

--

一只推测性思考的骆驼 — 由 DALL-E 生成

更大的语言模型通常能够提供更强的性能,但以降低推理速度为代价。例如,Llama 2 70B 在下游任务中的表现远超 Llama 2 7B,但其推理速度大约慢 10 倍。

许多解码超参数的技巧和调整可以加速非常大规模语言模型(LLM)的推理。尤其是推测性解码,在许多使用场景中都非常有效。

推测性解码使用一个小型 LLM 来生成 tokens,然后由一个更强大、更大的 LLM 进行验证,或在必要时进行修正。如果小型 LLM 足够准确,推测性解码可以显著加快推理速度。

在本文中,我首先解释了推测性解码的工作原理。然后,我展示了如何使用不同的模型对,包括 Gemma、Mixtral-8x7B、Llama 2 和 Pythia,所有模型均为量化版本,来运行推测性解码。我通过基准测试推理吞吐量和内存消耗,以突出显示哪些配置效果最佳。

推测性解码:使用两个 LLM 进行草稿和验证

使用 Python 和 AI 的语音转文本再转语音 — 一份如何实现的指南

原文:towardsdatascience.com/speech-to-text-to-speech-with-ai-using-python-a-how-to-guide-ee9b0b0ef082?source=collection_archive---------1-----------------------#2024-02-11

如何创建一个语音转文本再转语音程序

Naomi KrigerTowards Data Science Naomi Kriger

·发表于 Towards Data Science ·阅读时长 8 分钟·2024 年 2 月 11 日

--

图片Mariia Shalabaieva 提供,来源于 unsplash

自从我开始参加 GeekCon(是的,一个极客会议 🙂)已经整整十年了——这是一个为期一周末的黑客马拉松式活动,所有项目必须是无用的、纯粹为了好玩,今年有一个激动人心的变化:所有项目都必须包含某种形式的 AI。

我们小组的项目是一个语音转文本再转语音的游戏,工作原理如下:用户选择一个角色进行对话,然后通过语音表达他们想对角色说的任何话。这段语音输入被转录后发送给 ChatGPT,ChatGPT 会像角色一样进行回应。然后,使用语音合成技术将回应读出来。

现在游戏已经上线,带来了许多欢笑与乐趣,我编写了这份指南,帮助你自己创建一个类似的游戏。在整篇文章中,我们还将探索我们在黑客马拉松中所做的各种考虑和决策。

想查看完整代码吗?点击这里查看!

程序的流程

一旦服务器运行,用户将听到应用程序“讲话”,提示他们选择想要交谈的人物并开始与所选角色对话。每次想要大声说话时 —— 他们应按住键盘上的某个键并进行讲话。当他们讲话结束(并松开按键)时,录音将通过 [Whisper](https://platform.openai.com/docs/guides/speech-to-text/quickstart)OpenAI 的语音转文本模型)进行转录,转录结果将发送给 [ChatGPT](https://platform.openai.com/docs/guides/gpt/chat-completions-api) 以获取回复。回复将通过文本转语音库大声朗读,用户将听到回复内容。

实现

免责声明

注意:该项目是在 Windows 操作系统上开发的,并且包含了 pyttsx3 库,该库与 M1/M2 芯片不兼容。由于 pyttsx3 在 Mac 上不受支持,因此建议用户探索与 macOS 环境兼容的其他文本转语音库。

OpenAI 集成

我使用了两个 OpenAI 模型:Whisper 用于语音转文本转录,ChatGPT API 用于根据用户对所选人物的输入生成回复。尽管这样做会产生费用,但定价模型非常便宜,就我个人而言,我的账单至今还不到 $1。为了开始使用,我进行了 $5 的初始存款,到目前为止,我还没有用完这笔存款,而且这笔初始存款将持续到一年后才会过期。

我写这篇文章并未从 OpenAI 获得任何报酬或利益。

一旦你获得了 OpenAI API 密钥 —— 请将其设置为环境变量,在进行 API 调用时使用。确保不要将密钥推送到代码库或任何公共位置,也不要不安全地共享它。

语音转文本 —— 创建转录

语音转文本功能的实现是通过 Whisper 完成的,这是一种 OpenAI 模型。

以下是负责转录的函数代码片段:

async def get_transcript(audio_file_path: str, 
                         text_to_draw_while_waiting: str) -> Optional[str]:
    openai.api_key = os.environ.get("OPENAI_API_KEY")
    audio_file = open(audio_file_path, "rb")
    transcript = None

    async def transcribe_audio() -> None:
        nonlocal transcript
        try:
            response = openai.Audio.transcribe(
                model="whisper-1", file=audio_file, language="en")
            transcript = response.get("text")
        except Exception as e:
            print(e)

    draw_thread = Thread(target=print_text_while_waiting_for_transcription(
        text_to_draw_while_waiting))
    draw_thread.start()

    transcription_task = asyncio.create_task(transcribe_audio())
    await transcription_task

    if transcript is None:
        print("Transcription not available within the specified timeout.")

    return transcript

该函数被标记为异步(async),因为 API 调用可能需要一些时间才能返回响应,我们使用 await 等待它,以确保程序在收到响应之前不会继续执行。

正如你所看到的,get_transcript 函数还调用了 print_text_while_waiting_for_transcription 函数。为什么?因为获取转录是一个耗时的任务,我们希望让用户知道程序正在积极处理他们的请求,而不是卡住或无响应。因此,在用户等待下一步操作时,这段文字会逐渐打印出来。

使用 FuzzyWuzzy 进行文本比较的字符串匹配

将语音转录为文本后,我们要么直接使用它,要么尝试将其与现有字符串进行比较。

比较的使用案例包括:从预定义的选项列表中选择一个角色,决定是否继续游戏,以及在选择继续时——决定是选择新角色还是继续当前角色。

在这种情况下,我们希望将用户的语音输入转录与我们列表中的选项进行比较,因此我们决定使用FuzzyWuzzy库进行字符串匹配。

这使得我们可以从列表中选择最接近的选项,只要匹配分数超过预定的阈值。

这是我们功能的一段代码:

def detect_chosen_option_from_transcript(
        transcript: str, options: List[str]) -> str:
    best_match_score = 0
    best_match = ""

    for option in options:
        score = fuzz.token_set_ratio(transcript.lower(), option.lower())
        if score > best_match_score:
            best_match_score = score
            best_match = option

    if best_match_score >= 70:
        return best_match
    else:
        return ""

如果你想了解更多关于FuzzyWuzzy库及其功能的信息,可以阅读我写的这篇文章这里。

获取 ChatGPT 响应

一旦我们得到了转录内容,就可以将其发送给ChatGPT以获取回复。

对于每个ChatGPT请求,我们都会添加一个提示,要求它给出简短而有趣的回应。我们还告诉ChatGPT应该假装成哪个角色。

所以我们的功能如下所示:

def get_gpt_response(transcript: str, chosen_figure: str) -> str:
    system_instructions = get_system_instructions(chosen_figure)
    try:
        return make_openai_request(
            system_instructions=system_instructions, 
            user_question=transcript).choices[0].message["content"]
    except Exception as e:
        logging.error(f"could not get ChatGPT response. error: {str(e)}")
        raise e

系统指令如下所示:

def get_system_instructions(figure: str) -> str:
    return f"You provide funny and short answers. You are: {figure}"

文本转语音

对于文本转语音部分,我们选择了一个名为pyttsx3的 Python 库。这个选择不仅实现起来直接,而且还提供了若干额外的优势。它是免费的,提供两种语音选项——男性和女性——并允许你选择语速(每分钟单词数)。

当用户开始游戏时,他们从预定义的选项列表中选择一个角色。如果我们在列表中找不到匹配的内容,就会从“备选角色”列表中随机选择一个角色。在这两个列表中,每个角色都与一个性别关联,因此我们的文本转语音功能也会收到对应所选性别的语音 ID。

这是我们文本转语音功能的样子:

def text_to_speech(text: str, gender: str = Gender.FEMALE.value) -> None:
    engine = pyttsx3.init()

    engine.setProperty("rate", WORDS_PER_MINUTE_RATE)
    voices = engine.getProperty("voices")
    voice_id = voices[0].id if gender == "male" else voices[1].id
    engine.setProperty("voice", voice_id)

    engine.say(text)
    engine.runAndWait()

主要流程

现在我们已经大致完成了应用程序的所有部分,是时候深入了解游戏玩法了!主要流程如下所示。你可能会注意到一些我们尚未深入探讨的功能(例如choose_figureplay_round),但你可以通过查看仓库来探索完整代码。最终,大部分这些更高层次的功能都与我们上面提到的内部功能相联系。

这是主游戏流程的一段代码:

import asyncio

from src.handle_transcript import text_to_speech
from src.main_flow_helpers import choose_figure, start, play_round, \
    is_another_round

def farewell() -> None:
    farewell_message = "It was great having you here, " \
                       "hope to see you again soon!"
    print(f"\n{farewell_message}")
    text_to_speech(farewell_message)

async def get_round_settings(figure: str) -> dict:
    new_round_choice = await is_another_round()
    if new_round_choice == "new figure":
        return {"figure": "", "another_round": True}
    elif new_round_choice == "no":
        return {"figure": "", "another_round": False}
    elif new_round_choice == "yes":
        return {"figure": figure, "another_round": True}

async def main():
    start()
    another_round = True
    figure = ""

    while True:
        if not figure:
            figure = await choose_figure()

        while another_round:
            await play_round(chosen_figure=figure)
            user_choices = await get_round_settings(figure)
            figure, another_round = \
                user_choices.get("figure"), user_choices.get("another_round")
            if not figure:
                break

        if another_round is False:
            farewell()
            break

if __name__ == "__main__":
    asyncio.run(main())

未走的道路

我们有几个想法,在黑客松期间没有实现。这是因为在那个周末我们没有找到一个令我们满意的 API,或者由于时间限制无法开发某些功能。这些就是我们未选择的路径:

匹配响应语音与所选角色的“实际”语音

想象一下,如果用户选择与史瑞克、特朗普或奥普拉·温弗瑞对话。我们希望我们的文本到语音库或 API 能够用与所选人物匹配的声音来表达回应。然而,在黑客松期间,我们没有找到一个价格合理的库或 API 提供这个功能。如果你有建议,我们依然欢迎哦 =)

让用户与“自己”对话

另一个有趣的想法是提示用户提供他们自己的语音样本。我们将使用这个样本训练一个模型,然后让 ChatGPT 生成的所有回应用用户自己的声音朗读。在这种情况下,用户可以选择回应的语气(肯定和支持、讽刺、愤怒等),但声音会尽可能像用户自己的声音。然而,在黑客松的限制下,我们没能找到支持这种功能的 API。

为我们的应用添加前端

我们最初的计划是在应用程序中包含一个前端组件。然而,由于组内参与者人数的临时变动,我们决定优先开发后端。因此,当前应用程序仅在命令行界面(CLI)上运行,没有前端部分。

我们考虑的其他改进

当前最困扰我的是延迟。

流程中有几个相对较高延迟的组件,在我看来稍微影响了用户体验。例如:从提供音频输入到收到转录文本之间的时间,以及从用户按下按钮到系统实际开始录音之间的时间。所以,如果用户在按下按钮后立即开始讲话——由于这种延迟,至少会有一秒钟的音频没有被录制。

仓库链接与致谢

想看看整个项目吗?就在这里!

此外,温暖的致谢送给我的黑客松伙伴利奥尔·雅尔德尼,是他和我一起创造了这个游戏。

总结

在这篇文章中,我们学习了如何使用 Python 创建一个语音到文本再到语音的游戏,并将其与 AI 结合。我们使用了OpenAIWhisper模型进行语音识别,玩弄了FuzzyWuzzy库进行文本匹配,利用ChatGPT的对话魔力通过开发者 API 实现,并通过pyttsx3将其转换为语音。虽然OpenAI的服务(WhisperChatGPT开发者版)确实有一定的费用,但它还是很实惠的。

我们希望这篇指南对你有所启发,并激励你开始你的项目。

为编程和乐趣干杯!🚀

使用 BatchNorm 加速视觉变换器

原文:towardsdatascience.com/speeding-up-the-vision-transformer-with-batch-normalization-d37f13f20ae7?source=collection_archive---------7-----------------------#2024-08-06

介绍了如何在仅包含编码器的变换器架构中集成批量归一化,从而减少训练时间和推理时间。

Anindya Dey, PhDTowards Data Science Anindya Dey 博士

·发布于数据科学前沿·阅读时间 23 分钟·2024 年 8 月 6 日

--

图片来源:Jr Korpa,来自Unsplash

引言

基于变换器架构的引入,源自视觉变换器(ViT)的发现,已经在计算机视觉领域掀起了一场革命。对于广泛的应用,ViT 及其各种变种有效地挑战了卷积神经网络(CNN)作为最先进架构的地位(有关详细的比较研究,请参见这篇论文)。然而,尽管取得了这一成功,ViT 被认为需要显著更长的训练时间,并且在较小到中等输入数据规模下推理速度较慢。因此,研究可能导致更快训练和推理速度的视觉变换器修改方法是一个重要课题。

在一系列文章的第一篇中,我详细探讨了 ViT 的一种修改,这种修改将涉及用 Batch Normalization(BatchNorm)替换 Layer Normalization(LayerNorm)——即 transformers 默认的归一化技术。更具体地说,我将讨论这种模型的两个版本。正如我稍后将回顾的,ViT 是一个仅包含编码器的架构,其中 transformer 编码器由两个不同的模块组成——多头自注意力(MHSA)和前馈网络(FFN)。第一个模型将仅在前馈网络中实现 BatchNorm 层——这将被称为 ViTBNFFN(带有前馈网络中 BatchNorm 的视觉 Transformer)。第二个模型将用 BatchNorm 替换视觉 Transformer 中的 LayerNorm 所有地方——我将此模型称为 ViTBN(带有 BatchNorm 的视觉 Transformer)。因此,模型 ViTBNFFN 将同时涉及 LayerNorm(在 MHSA 中)和 BatchNorm(在 FFN 中),而 ViTBN 仅涉及 BatchNorm。

我将比较三个模型的性能——ViTBNFFN、ViTBN 和标准 ViT——在手写数字的 MNIST 数据集上的表现。更具体地说,我将比较以下指标——每个 epoch 的训练时间、每个 epoch 的测试/推理时间、训练损失和两个不同实验设置下模型的测试准确度。在第一个设置中,模型在固定的学习率和批量大小下进行比较。然后,在保持批量大小不变的情况下,使用不同的学习率值重复该实验。在第二个设置中,首先为每个模型找到最大化准确度的最佳学习率和批量大小,使用 贝叶斯优化 过程。然后,比较这些优化后的模型在上述指标上的表现。对于我们在下面详细介绍的合理架构选择,ViTBNFFN 和 ViTBN 模型在每个 epoch 的平均训练时间和平均推理时间上都提高了超过 60%,同时提供了与标准 ViT 相当(或更好的)准确度。此外,BatchNorm 模型相比 ViT 允许使用更大的学习率,而不会妥协模型的稳定性。这个发现与原论文中提到的 CNN 中使用 BatchNorm 的直觉一致,正如 Ioffe 和 Szegedy 所指出的。

你可以在 github 上的 repo 中获取这些文章中使用的代码,并进行尝试。告诉我你的想法!

目录

我从对 BatchNorm 及其 PyTorch 实现的简单介绍开始,然后简要回顾了 Vision Transformer。熟悉这些主题的读者可以跳到下一节,在那里我们描述了如何使用 PyTorch 实现 ViTBNFFN 和 ViTBN 模型。接下来,我使用 MLFlow 的跟踪功能设置简单的数值实验,训练并测试这些模型在 MNIST 数据集上的表现(没有任何图像增强),并将结果与标准 ViT 模型进行比较。贝叶斯优化是通过 Ax 平台 上的 BoTorch 优化引擎执行的。最后,我简要总结了结果并给出了一些结语。

批量归一化:定义与 PyTorch 实现

让我们简要回顾一下深度神经网络中 BatchNorm 的基本概念。这个想法最早在 Ioffe 和 Szegedy 的论文中提出,作为一种加速卷积神经网络训练的方法。假设 zᵃᵢ 表示深度神经网络中某一层的输入,其中 a 是批量索引,范围为 a=1,…, Nₛ,i 是特征索引,范围为 i=1,…, C。这里,Nₛ 是批量中的样本数量,C 是生成 zᵃᵢ 的层的维度。BatchNorm 操作通常包括以下步骤:

  1. 对于给定的特征 i,计算批量大小为 Nₛ 的均值和方差,即:

2. 对于给定的特征 i,使用上述计算的均值和方差对输入进行标准化,即定义(对于一个固定的小正数 ϵ):

3. 最后,对于每个特征 i,偏移并重新缩放标准化后的输入:

其中没有对索引 a 或 i 进行求和,并且参数(γᵃᵢ, βᵃᵢ)是可训练的。

另一方面,层归一化(LayerNorm)涉及对固定批量索引 a 计算特征索引的均值和方差,然后进行类似的标准化和缩放操作。

PyTorch 内置了一个 BatchNorm1d 类,用于对 2d 或 3d 输入执行批量归一化,具有以下规格:

代码块 1. PyTorch 中的 BatchNorm1d 类。

在一般的图像处理任务中,图像通常被分割成若干个较小的图块。输入 z 随后会有一个索引α(除了 a 和 i 的索引外),该索引标记序列中构成图像的特定图块。BatchNorm1d 类将输入的第一个索引视为批次索引,第二个索引作为特征索引,其中 num_features = C。因此,输入必须是一个形状为 Nₛ × C × N 的三维张量,其中 N 为图块数量。输出张量与输入的形状相同。PyTorch 还提供了一个 BatchNorm2d 类,可以处理四维输入。对于我们的任务,使用 BatchNorm1d 类就足够了。

PyTorch 中的 BatchNorm1d 类有一个额外的特性,我们需要讨论。如果设置 track_running_stats = True(这是默认设置),BatchNorm 层会在训练过程中保持其计算的均值和方差的运行估计(更多细节请见这里),这些估算值将在测试时用于归一化。如果设置 track_running_stats = False,则 BatchNorm 层不会保持运行估算值,而是使用批次统计量在测试时进行归一化。对于一般数据集,默认设置可能导致训练和测试的准确率显著不同,至少在前几个 epoch 内是这样的。然而,对于我使用的数据集,可以明确检查发现并非如此。因此,在使用 BatchNorm1d 类时,我保持默认设置。

标准 Vision Transformer:简要回顾

Vision Transformer(ViT)在论文一张图胜过 16 × 16 个词中被提出,主要用于图像分类任务。让我们从模型的简要回顾开始(有关 PyTorch 实现的详细信息,请见这里)。该编码器-仅 Transformer 模型的架构细节如图 1 所示,主要由三部分组成:嵌入层Transformer 编码器MLP 头部

图 1. Vision Transformer 架构。图片来源:一张图胜过 16×16 个词。

嵌入层将图像分解成多个 patch,并将每个 patch 映射为一个向量。嵌入层的组织方式如下。可以将 2d 图像看作一个实际的 3d 张量,形状为 H × W × c,其中 H、W 和 c 分别是图像的高度、宽度(单位为像素)和颜色通道数。在第一步中,图像被重新形状为一个形状为 N × dₚ的 2d 张量,使用大小为 p 的 patch,其中 N = (H/p) × (W/p)是 patch 的数量,dₚ = p² × c 是 patch 的维度。举一个具体的例子,考虑一个 28 × 28 的灰度图像。在这种情况下,H = W = 28,c = 1。如果选择 patch 大小 p = 7,则图像将被划分为 N = 4 × 4 = 16 个 patch,patch 维度为 dₚ = 49。

在下一步中,一个线性层将形状为 N × dₚ的张量映射到形状为 N × dₑ的张量,其中 dₑ被称为嵌入维度。然后,形状为 N × dₑ的张量通过在前面加上一个可学习的 dₑ维向量y₀,被提升为形状为(N+1) × dₑ的张量y。向量y₀表示CLS tokens在图像分类中的嵌入,下面我们将详细解释。接着,另一个形状为(N+1) × dₑ的张量yₑ被加到张量y中——该张量编码了图像的位置嵌入信息。可以选择使用一个可学习的yₑ,或者使用固定的 1d 正弦表示(更多细节请参见论文)。然后,形状为(N+1) × dₑ的张量z = y + yₑ被送入 Transformer 编码器。一般而言,图像还会被标记为一个批次索引。因此,嵌入层的输出是一个形状为 Nₛ × (N+1) × dₑ的 3d 张量。

如下图 2 所示,Transformer 编码器将形状为 Nₛ × (N+1) × dₑ的 3d 张量zᵢ作为输入,并输出一个相同形状的张量zₒ。然后,这个张量zₒ被送入 MLP 头进行最终的分类处理。令z⁰ₒ为形状为 Nₛ × dₑ的张量,对应于zₒ在第二维度上的第一个组件。这个张量是可学习张量y₀的“最终状态”,它在之前的步骤中将输入张量加到编码器之前。如果选择使用 CLS tokens 进行分类,MLP 头会从 Transformer 编码器的输出zₒ中提取z⁰ₒ,并将其映射到一个形状为 Nₛ × n 的张量,其中 n 是问题中的类别数。或者,也可以选择进行全局池化,通过计算给定特征的输出张量zₒ在(N+1)个 patch 上的平均值,从而得到一个形状为 Nₛ × dₑ的张量zᵐₒ。然后,MLP 头会将zᵐₒ映射到一个形状为 Nₛ × n 的 2d 张量。

图 2. Vision Transformer 中 Transformer 编码器的结构。图片来源:An Image is Worth 16x16 words。

现在让我们更详细地讨论变换器编码器的组成部分。如图 2 所示,它由 L 个变换器块组成,其中 L 通常被称为模型的 深度。每个变换器块又由一个多头自注意力(MHSA)模块和一个带有残差连接的 MLP 模块(也称为前馈网络)组成,如图所示。MLP 模块由两个隐藏层和一个位于中间的 GELU 激活层组成。第一个隐藏层前也有一个 LayerNorm 操作。

我们现在准备讨论模型 ViTBNFFN 和 ViTBN。

带有批量归一化的视觉变换器:ViTBNFFN 和 ViTBN

为了在 ViT 架构中实现批量归一化,我首先介绍一个专门针对我们任务的新 BatchNorm 类:

代码块 2. 实现批量归一化操作的 Batch_Norm 类,适用于 ViTBNFFN 和 ViTBN。

这个新类 Batch_Norm 使用了我之前讲解过的 BatchNorm1d(第 10 行)类。重要的修改出现在第 13-15 行。回想一下,输入到变换器编码器的张量形状是 Nₛ × (N+1) × dₑ。在编码器的某一层,输入是形状为 Nₛ × (N+1) × D 的 3D 张量,其中 D 是该层的特征数量。为了使用 BatchNorm1d 类,必须将这个张量重塑为 Nₛ × D × (N+1),正如我们之前所解释的那样。实现批量归一化后,需要将张量重新塑形回 Nₛ × (N+1) × D 的形状,以便其余架构保持不变。所有的重塑操作都是通过 einops 包中的 rearrange 函数来完成的。

现在可以以以下方式描述带有批量归一化的模型。首先,可以通过去除先前的 LayerNorm 操作并引入一个批量归一化层来修改 ViT 的变换器编码器中的前馈网络。我选择将批量归一化层插入到第一个隐藏层和 GELU 激活层之间。这就得到了模型 ViTBNFFN。新前馈网络的 PyTorch 实现如下所示:

代码块 3. 带有批量归一化的变换器编码器的 FeedForward(MLP)模块。

FeedForward 类的构造函数,代码在第 7-11 行,显而易见。批量归一化层由第 8 行中的 Batch_Norm 类实现。输入到前馈网络的张量形状为 Nₛ × (N+1) × dₑ。第一个线性层将其转换为形状为 Nₛ × (N+1) × D 的张量,其中 D= hidden_dim(代码中也称为 mlp_dimension)。因此,Batch_Norm 类的适当特征维度为 D。

接下来,可以将模型ViTBNFFN中的所有 LayerNorm 操作替换为由 Batch_Norm 类实现的 BatchNorm 操作。这就得到了ViTBN模型。与标准的 ViT 相比,我们在 ViTBNFFN/ViTBN 中进行了一些额外的调整。首先,我们通过引入一个额外的模型参数,加入了可学习的位置编码或固定的正弦位置编码的选项。类似于标准的 ViT,用户可以选择使用 CLS token 或全局池化的方式来进行最终分类。此外,我们将 MLP 头部替换为一个更简单的线性头部。通过这些更改,ViTBN 类的形式如下(ViTBNFFN 类也具有类似的形式):

代码块 4. ViTBN 类。

上述大部分代码不言自明,并且与标准的 ViT 类非常相似。首先,注意在第 23 到 28 行,我们已经在嵌入层中将 LayerNorm 替换为 BatchNorm。在Transformer类(表示 ViTBN 使用的 transformer 编码器)内部也进行了类似的替换(见第 44 行)。接下来,我们添加了一个新的超参数“pos_emb”,其取值为字符串‘pe1d’或‘learn’。在第一个情况下,使用固定的 1D 正弦位置嵌入,而在第二个情况下,使用可学习的位置嵌入。在前向传播函数中,第一个选项在第 62 到 66 行实现,而第二个选项在第 68 到 72 行实现。超参数“pool”的取值为字符串‘cls’或‘mean’,分别对应于最终分类时使用 CLS token 或全局池化。ViTBNFFN 类可以以类似的方式写出。

模型 ViTBN(类似地,ViTBNFFN)可以按如下方式使用:

代码块 5. 使用 ViTBN 处理 28 × 28 图像。

在这个特定的情况下,输入维度image_size = 28,这意味着 H = W = 28。patch_size = p = 7,意味着补丁的数量为 N = 16。由于颜色通道数量为 1,补丁的维度是 dₚ = p² = 49。分类问题中的类别数由num_classes给出。模型中的参数dim = 64 表示嵌入维度 dₑ。编码器中 transformer 块的数量由depth = L = 6 给出。参数headsdim_head分别对应自注意力头的数量以及编码器中 MHSA 模块中每个头的(共同)维度。参数mlp_dim是 MLP 或前馈模块的隐藏维度。参数dropout是 transformer 编码器的单个丢弃参数,出现在 MHSA 和 MLP 模块中,而emb_dropout是与嵌入层相关的丢弃参数。

实验 1:在固定超参数下比较模型

在介绍了包含 BatchNorm 的模型之后,我将开始第一个数值实验。众所周知,BatchNorm 能够加速深度神经网络的收敛,从而加速训练和推理过程。它还允许使用相对较大的学习率训练卷积神经网络,而不会引入不稳定性。此外,BatchNorm 预计能够作为正则化器,从而消除使用 dropout 的需求。本实验的主要动机是了解这些说法如何转化为具有 BatchNorm 的视觉变换器(Vision Transformer)。

对于本实验,我将使用 Code Block 5 中给出的 ViT 超参数,特别是选择 6 层的 transformer 编码器,即‘depth =6’。我们将使用 CLS 标记进行分类,这对应于设置pool = ‘cls’,并使用可学习的位置信息嵌入,这对应于设置pos_emb = ‘learn’。对于 ViTBNFFN 和 ViTBN,我将使用单层 transformer,因此我们有‘depth =1’,同时所有其他超参数保持与 ViT 相同。

实验涉及以下步骤:

  1. 对于给定的学习率,我将在 MNIST 手写图像数据集上训练 ViT、ViTBNFFN 和 ViTBN 模型,共进行 30 个周期。在这个阶段,我不会使用任何图像增强。每经过一个训练周期后,我将在验证数据上测试一次模型。

  2. 对于给定的模型和学习率,我将在每个周期中测量以下几个量:训练时间、训练损失、测试时间和测试准确度。对于固定的学习率,这将生成四个图表,每个图表绘制了这些量随周期变化的曲线,比较了三种模型的表现。这些图表可以用来比较模型的性能。特别地,我想比较标准 ViT 模型与包含 BatchNorm 的模型的训练和测试时间,以检查是否在某些情况下有显著的加速。

  3. 我将对三个代表性的学习率 l = 0.0005、0.005 和 0.01 执行步骤 1 和步骤 2 的操作,保持所有其他超参数不变。

在整个分析过程中,我将使用CrossEntropyLoss()作为损失函数,使用Adam优化器,并且在所有训练周期中,训练和测试的批量大小分别固定为 100 和 5000。我将把所有 dropout 参数设置为零,以进行本实验。为了简化问题,我也不考虑学习率衰减。

本实验已经通过MLFlow的跟踪功能进行。所有实验中的运行,我都使用了Google Colab上的NVIDIA L4 Tensor Core GPU

让我们首先讨论执行给定实验运行时使用的 MLFlow 模块的重要组成部分。第一个组成部分是train_model函数,用于根据给定的超参数选择进行模型的训练和测试:

代码块 6:数值实验的训练和测试模块。

函数train_model返回每个 epoch 的四个量——训练损失(cost_list),测试准确率(accuracy_list),训练时间(秒)(dur_list_train)和测试时间(秒)(dur_list_val)。第 19 到 32 行给出了该函数的训练模块,第 35 到 45 行给出了测试模块。请注意,该函数允许在每个训练 epoch 后进行一次模型测试。在我们代码的 Git 版本中,你还可以找到按类别划分的准确率,但为了简洁起见,这里我将跳过这一部分。

接下来,需要定义一个函数来下载 MNIST 数据集,将其拆分为训练集和验证集,并将图像转换为 torch 张量(不进行任何增强):

代码块 7:获取 MNIST 数据集。

现在我们准备编写 MLFlow 模块,模块的形式如下:

代码块 8:要执行的 MLFlow 实验模块。

让我们解释一些代码中的重要部分。

  1. 第 11 到 13 行分别指定了学习率、epoch 数量和损失函数。

  2. 第 16 到 33 行指定了训练和测试的各种细节。代码块 7 中的get_datesets()函数下载了 MNIST 数字的训练集和验证集,而在代码块 5 中定义的get_model()函数指定了模型。对于后者,我们设置了pool = ‘cls’pos_emb = ‘learn’。在第 20 行,定义了优化器,我们在第 21 到 24 行指定了训练和验证数据加载器以及各自的批大小。第 25 到 26 行指定了函数train_model的输出,我们在代码块 6 中定义了该函数——四个列表,每个列表包含n_epoch个条目。第 16 到 24 行指定了函数train_model的各种参数。

  3. 在第 37 到 40 行,指定了对于给定实验运行将记录的参数,在我们的实验中是学习参数和 epoch 数量。

  4. 第 44 到 52 行构成了代码中最重要的部分,在这里指定了需要记录的指标,也就是上面提到的四个列表。事实证明,默认情况下,函数mlflow.log_metrics()不会记录一个列表。换句话说,如果我们直接使用mlflow.log_metrics({generic_list}),那么实验将只记录最后一个 epoch 的输出。作为解决方法,我们使用 for 循环多次调用该函数,如下所示。

现在让我们深入分析实验结果,这些结果本质上总结在下面图 3 到图 5 的三组图表中。每个图表展示了一个包含四个图的集合,分别对应于每个时期的训练时间(左上角)、测试时间(右上角)、训练损失(左下角)和测试准确度(右下角),对于三种模型而言,这些图表都是在固定学习率下得到的。图 3、图 4 和图 5 分别对应于学习率 l=0.0005、l=0.005 和 l=0.01。为方便起见,我们定义一对比率:

其中 T(model|train) 和 T(model|test) 是我们实验中给定模型每个时期的平均训练时间和测试时间。这些比率为我们提供了一个粗略的度量,衡量了在集成 BatchNorm 后,Vision Transformer 加速的效果。我们将始终训练和测试模型相同的时期数——因此,可以分别通过上述比率定义每个时期的平均训练时间和测试时间的百分比提升:

我们从最小的学习率 l=0.0005 开始,该学习率对应于图 3。此时,标准 ViT 在较少的时期内就会收敛,相比其他模型,训练损失较低,准确度略高(~98.2%),见右下角图表,优于 ViTBNFFN (~97.8%) 和 ViTBN (~97.1%)。然而,ViT 的训练时间和测试时间相比 ViTBNFFN/ViTBN 高出超过 2 倍。从图表中可以读取出比率 rₜ 和 rᵥ:rₜ (ViTBNFFN) = 2.7, rᵥ (ViTBNFFN) = 2.6, rₜ (ViTBNFFN) = 2.5, rᵥ (ViTBN) = 2.5,其中 rₜ 和 rᵥ 如上所定义。因此,对于给定的学习率,BatchNorm 带来的速度提升在训练和推理中都非常显著——大约是 60%的提升。精确的百分比提升列在表 1 中。

图 3. 学习率 l = 0.0005 的图表。

在下一步中,我们将学习率增加到 l=0.005 并重复实验,得到图 4 中的一组图表。

图 4. 学习率 l=0.005 的图表。

对于学习率 l=0.005,标准 ViT 在更快收敛方面似乎没有任何优势。然而,与 ViTBNFFN/ViTBN 相比,ViT 的训练时间和测试时间再次较长。从图 3 和图 4 左上方的图形可视化比较来看,ViT 的训练时间显著增加,而 ViTBNFFN 和 ViTBN 的训练时间基本保持不变。这表明,在这种情况下训练时间的提升更为显著。另一方面,从图 3 和图 4 右上方的图形进行比较,可以看到测试速度的提升大致相同。rₜ和 rᵥ的比值可以从图 4 顶部的图形中读取:rₜ(ViTBNFFN)= 3.6,rᵥ(ViTBNFFN)=2.5,rₜ(ViTBN)= 3.5,rᵥ(ViTBN)= 2.5。显然,与较小学习率的情况相比,这里的 rₜ比值更大,而 rᵥ比值保持基本不变。这导致训练时间的提升百分比更高(约 70%),而推理时间的提升(约 60%)保持大致不变。

最后,让我们将学习率进一步提高到 l=0.01,并重复实验,得到图 5 中的一组图形。

图 5。学习率 l=0.01 的图形。

在这种情况下,ViT 在经过几个 epoch 后变得不稳定,正如图 5 中的训练损失图所示,训练损失从第 15 个 epoch 附近开始出现不收敛的行为。这一点也可以从测试精度图中得到验证,ViT 的精度在第 15 个 epoch 左右急剧下降。然而,ViTBNFFN 和 ViTBN 模型保持稳定,并且在 30 个 epoch 的训练结束时精度超过 97%。在这种情况下,ViT 的训练时间甚至更高,并且波动剧烈。对于 ViTBNFFN,训练时间有明显的增加,而对于 ViTBN,训练时间大致保持不变——请参见左上图。在训练比率 rₜ方面,我们得到 rₜ(ViTBNFFN)= 2.7 和 rₜ(ViTBN)= 4.3。第一个比率低于我们在之前的情况下得到的结果,这是由于 ViTBNFFN 的训练时间较长,抵消了 ViT 的训练时间增加。第二个比率显著更高,因为 ViTBN 的训练时间几乎没有变化。在这种情况下,测试比率 rᵥ——rᵥ(ViTBNFFN)= 2.6 和 rᵥ(ViTBN)= 2.7——显示出微小的增加。

训练时间和推理时间的提升——gₜ和 gᵥ在不同学习率下的总结见表 1。

表 1。ViTBNFFN 和 ViTBN 相对于 ViT 在每个 epoch 的训练和测试时间的百分比提升。

另外,直观地展示每个模型的训练时间如何随学习率变化也是很有趣的。这在图 6 中的三张图中得以展示,分别表示 ViT、ViTBNFFN 和 ViTBN。模型 _i 中的下标 i=1,2,3 分别对应给定模型的三种学习率 l= 0.0005、0.005 和 0.01。

图 6. 显示给定模型的每个周期训练时间如何随学习率变化的图形。下标 1、2、3 分别对应 l=0.0005、0.005 和 0.01。

显然,训练时间随学习率变化的差异对 ViT(上图)最为显著。另一方面,对于 ViTBN,在改变学习率时,训练时间大致保持不变(下图)。对于 ViTBNFFN,只有当学习率达到较大的值(~0.01)时,训练时间的变化才变得显著(中图)。

实验 2:比较优化后的模型

现在让我们设置一个实验,比较优化后的模型的性能。该过程将包括以下步骤:

  1. 首先执行贝叶斯优化,以确定每个模型的最佳超参数——学习率和批量大小。

  2. 给定三个优化模型,训练并测试每个模型 30 个周期,并使用 MLFlow 进行指标比较,如前所述——特别是每个周期的训练和测试/推理时间。

让我们从第一步开始。我们使用Ax平台上可用的 BoTorch 优化引擎。有关使用 BoTorch 进行优化程序的详细信息,请参阅Ax 文档。我们使用准确度作为优化指标,并将优化过程限制为 20 次迭代。我们还需要指定每种情况中进行搜索的超参数范围。我们之前的实验为我们提供了一些关于适当范围的见解。ViT 的学习率范围为[1e-5, 1e-3],而 ViTBNFFN 和 ViTBN 的学习率范围为[1e-5, 1e-2]。对于所有三个模型,批量大小范围为[20, 120]。模型的深度与实验 1 中的设置相同。优化过程的完整代码可以在 GitHub 仓库的optimization文件夹中的模块hypopt_train.py中找到。

该过程的结果是每个模型的一组优化超参数。我们在表 2 中总结了它们。

表 2. 使用 BoTorch 进行贝叶斯优化得到的每个模型的优化超参数。

对于每个模型,可以绘制准确率如何随迭代次数变化的图形。作为说明示例,我们在图 7 中展示了 ViTBNFFN 的收敛图。

图 7. ViTBNFFN 的收敛图。

现在可以进入步骤 2——我们使用优化的超参数训练和测试每个模型 30 个 epoch。30 个 epoch 的训练和测试指标比较总结在图 8 的四个图表中。

图 8。比较优化模型在 MNIST 数据集上训练和测试 30 个 epoch 的指标。

在 30 个 epoch 结束时,模型——ViT、ViTBNFFN 和 ViTBN——分别达到了 98.1%、97.6%和 97.8%的准确率。与 ViTBNFFN 和 ViTBN 相比,ViT 的收敛所需的 epoch 数较少。

从图 8 顶部的两幅图中可以明显看出,带有 BatchNorm 的模型在每个 epoch 的训练和推理速度上都有显著提升。对于 ViTBNFFN,rₜ和 rᵥ的比值可以从上述数据中计算得出:rₜ(ViTBNFFN)= 3.9,rᵥ(ViTBNFFN)= 2.6,而对于 ViTBN,则是 rₜ(ViTBN)= 3.5,rᵥ(ViTBN)= 2.5。表 3 总结了每个 epoch 的平均训练时间增益(gₜ)和平均推理时间增益(gᵥ)。

表 3。ViTBNFFN 和 ViTBN 相对于标准 ViT 在每个 epoch 的训练时间和推理时间的增益。

结果简要总结

现在让我们简要总结一下我们的研究:

  1. 在固定学习率下的训练和测试速度提升: 相较于 ViT,ViTBNFFN 和 ViTBN 的每个 epoch 的平均训练时间显著加快。无论在什么学习率范围内,增益 gₜ都大于~60%,但根据学习率和模型的不同,这一增益可能会有显著差异,具体请见表 1。对于每个 epoch 的平均测试时间,也有显著的提升(~60%),但对于这两个模型而言,随着学习率的变化,这一提升大致保持不变。

  2. 优化模型的训练和测试速度提升: 对于 ViTBNFFN 和 ViTBN 来说,每个 epoch 的平均训练时间的增益都超过 70%,而推理时间的增益则略高于 60%——gₜ和 gᵥ的具体值已总结在表 3 中。优化后的 ViT 模型比带有 BatchNorm 的模型收敛更快。

  3. BatchNorm 与较高学习率: 对于较小的学习率(~ 0.0005),所有三个模型都表现稳定,且 ViT 的收敛速度相比于 ViTBNFFN/ViTBN 更快。对于中等的学习率(~ 0.005),三个模型的收敛速度非常相似。对于较高的学习率(~ 0.01),ViT 变得不稳定,而 ViTBNFFN/ViTBN 模型保持稳定,并且其准确性与中等学习率的情况相当。因此,我们的研究结果验证了这样一个普遍预期:将 BatchNorm 集成到架构中,可以使用更高的学习率。

  4. 训练时间与学习率的变化: 对于 ViT,当增加学习率时,每个周期的平均训练时间会大幅增加,而对于 ViTBNFFN,这一增加要小得多。另一方面,对于 ViTBN,训练时间变化最小。换句话说,ViTBN 对学习率变化的训练时间最为稳定。

结语

在本文中,我介绍了两种在 ViT 类型架构中集成 BatchNorm 的模型——其中一个在前馈网络中部署 BatchNorm(ViTBNFFN),而另一个则在各处用 BatchNorm 替代 LayerNorm(ViTBN)。从上述数值实验中,我们得出了两个主要结论。首先,使用 BatchNorm 的模型可以在使用较少的变换器层的情况下,达到与标准 ViT 相同(或更高)的准确性。这反过来加快了训练时间和推理时间。对于 MNIST 数据集,在我考虑的学习率范围内,每个周期的训练和测试时间至少加快了 60%。其次,使用 BatchNorm 的模型允许在训练过程中使用更大的学习率,而不会使模型不稳定。

此外,在本文中,我专注于标准的 ViT 架构。然而,显然可以将讨论扩展到其他基于变换器的计算机视觉架构。Yao 等人已经研究了在 DeiT(数据高效图像变换器)和 Swin Transformer 中集成 BatchNorm。我建议读者参考这篇论文以获取更多细节。

感谢阅读!如果你已读完本文,请不要忘记留下评论!除非另有说明,本文中使用的所有图片和图表均由作者生成。

用 AI 为冰球增添趣味:利用计算机视觉进行球员追踪

原文:towardsdatascience.com/spicing-up-ice-hockey-with-ai-player-tracking-with-computer-vision-ce9ceec9122a?source=collection_archive---------0-----------------------#2024-07-09

使用 PyTorch、计算机视觉技术和卷积神经网络(CNN),我开发了一个模型,可以追踪球员、球队以及基本的表现统计数据

Raul Vizcarra ChirinosTowards Data Science Raul Vizcarra Chirinos

·发布于Towards Data Science ·30 分钟阅读·2024 年 7 月 9 日

--

现在,我不像以前那样频繁地打冰球了,但从小冰球就是我生活的一部分。最近,我有机会在利马举办的首届冰球锦标赛(3 对 3)中,协助裁判台并记录一些统计数据。此次活动得到了秘鲁滑冰曲棍球协会(APHL)的巨大支持,并且友谊联盟也亲切地参与其中。为了加入一些 AI 元素,我使用了PyTorch计算机视觉技术和卷积神经网络(CNN)来构建一个模型,追踪球员和球队,并收集一些基本的表现数据。

本文旨在成为设计和部署该模型的快速指南。尽管模型仍需进行一些微调,但我希望它能帮助任何人入门计算机视觉在体育中的应用。我还要特别感谢并感谢秘鲁滑冰曲棍球协会(APHL)允许我使用比赛的 40 秒视频样本进行此项目(你可以在项目的 GitHub 仓库找到视频输入样本)。

架构

在继续进行项目之前,我做了一些快速研究,找到一个可以作为基准的方向,以避免“重新发明轮子”。我发现,在使用计算机视觉追踪球员方面,有很多有趣的研究成果,尤其是在足球领域(这并不令人惊讶,足球是世界上最受欢迎的团队运动)。然而,我并没有找到很多冰球方面的资源。Roboflow 提供了一些有趣的预训练模型和数据集,用于训练自己的模型,但使用托管模型时出现了一些延迟问题,稍后我会进一步解释。最终,我借用了足球相关的资料来读取视频帧并获得单独的跟踪 ID,遵循了这个教程中解释的基本原理和跟踪方法(如果你有兴趣更好地理解一些基本的计算机视觉技术,我建议至少观看前一个半小时的教程)。

在覆盖了跟踪 ID 后,我开始构建自己的路径。在本文的过程中,我们将看到这个项目是如何从一个简单的物体检测任务发展成一个能够全面检测球员、球队并提供一些基本表现指标的模型的(示例剪辑从 01 到 08,作者自创)。

模型架构。作者自创

跟踪机制

跟踪机制是模型的核心。它确保视频中的每个检测到的物体都能被识别并分配一个唯一的标识符,保持该身份在每一帧中的一致性。跟踪机制的主要组成部分包括:

  1. YOLO(You Only Look Once): 它是一种强大的实时物体检测算法,最初在 2015 年的论文“You Only Look Once: Unified, Real-Time Object Detection”中提出。它以速度和在大约 80 个预训练类别中的通用性为特点(值得注意的是,它还可以在自定义数据集上进行训练,以检测特定物体)。对于我们的使用案例,我们将依赖 YOLOv8x,这是一种由 Ultralytics 基于之前版本的 YOLO 构建的计算机视觉模型。你可以在这里下载它。

  2. ByteTrack 跟踪器: 要理解 ByteTrack,我们必须先了解多目标跟踪(MOT,Multiple Object Tracking),它涉及在视频序列中追踪多个物体的运动,并将当前帧中检测到的物体与前一帧中的相应物体进行关联。为了实现这一目标,我们将使用 ByteTrack(2021 年在论文“ByteTrack: Multi-Object Tracking by Associating Every Detection Box”中提出)。为了实现 ByteTrack 跟踪器并为检测到的物体分配轨迹 ID,我们将依赖 Python 的 supervision 库。

  3. OpenCV: 是一个广泛应用于各种计算机视觉任务的 Python 库。对于我们的用例,我们将依赖OpenCV来可视化并标注视频帧中的边界框和每个检测物体的文本信息。

为了构建我们的追踪机制,我们将从以下两步开始:

  • 使用 ByteTrack 部署 YOLO 模型来检测物体(在我们的例子中是球员)并分配唯一的追踪 ID。

  • 初始化一个字典,将物体追踪信息存储在一个 pickle(pkl)文件中。这将极大地帮助我们避免每次运行代码时都需要逐帧执行视频物体检测过程,节省大量时间。

对于接下来的步骤,我们需要以下 Python 包:

pip install ultralytics
pip install supervision
pip install opencv-python

接下来,我们将指定我们的库和样本视频文件以及 pickle 文件的路径(如果存在;如果没有,代码会创建一个并将其保存在相同路径下):

#**********************************LIBRARIES*********************************#
from ultralytics import YOLO
import supervision as sv
import pickle
import os
import cv2

# INPUT-video file
video_path = 'D:/PYTHON/video_input.mp4'
# OUTPUT-Video File
output_video_path = 'D:/PYTHON/output_video.mp4'
# PICKLE FILE (IF AVAILABLE LOADS IT IF NOT, SAVES IT IN THIS PATH)
pickle_path = 'D:/PYTHON/stubs/track_stubs.pkl'

现在让我们继续定义我们的追踪机制(你可以在项目的 GitHub 仓库中找到视频输入示例)

#*********************************TRACKING MECHANISM**************************#
class HockeyAnalyzer:
    def __init__(self, model_path):
        self.model = YOLO(model_path) 
        self.tracker = sv.ByteTrack()

    def detect_frames(self, frames):
        batch_size = 20 
        detections = [] 
        for i in range(0, len(frames), batch_size):
            detections_batch = self.model.predict(frames[i:i+batch_size], conf=0.1)
            detections += detections_batch
        return detections

#********LOAD TRACKS FROM FILE OR DETECT OBJECTS-SAVES PICKLE FILE************#

    def get_object_tracks(self, frames, read_from_stub=False, stub_path=None):
        if read_from_stub and stub_path is not None and os.path.exists(stub_path):
            with open(stub_path, 'rb') as f:
                tracks = pickle.load(f)
            return tracks

        detections = self.detect_frames(frames)

        tracks = {"person": []}

        for frame_num, detection in enumerate(detections):
            cls_names = detection.names
            cls_names_inv = {v: k for k, v in cls_names.items()}

            # Tracking Mechanism
            detection_supervision = sv.Detections.from_ultralytics(detection)
            detection_with_tracks = self.tracker.update_with_detections(detection_supervision)
            tracks["person"].append({})

            for frame_detection in detection_with_tracks:
                bbox = frame_detection[0].tolist()
                cls_id = frame_detection[3]
                track_id = frame_detection[4]

                if cls_id == cls_names_inv.get('person', None):
                    tracks["person"][frame_num][track_id] = {"bbox": bbox}

            for frame_detection in detection_supervision:
                bbox = frame_detection[0].tolist()
                cls_id = frame_detection[3]

        if stub_path is not None:
            with open(stub_path, 'wb') as f:
                pickle.dump(tracks, f)

        return tracks

#***********************BOUNDING BOXES AND TRACK-IDs**************************#

    def draw_annotations(self, video_frames, tracks):
        output_video_frames = []
        for frame_num, frame in enumerate(video_frames):
            frame = frame.copy() 
            player_dict = tracks["person"][frame_num]

            # Draw Players
            for track_id, player in player_dict.items():
                color = player.get("team_color", (0, 0, 255))  
                bbox = player["bbox"]
                x1, y1, x2, y2 = map(int, bbox)         
            # Bounding boxes
                cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)
            # Track_id 
                cv2.putText(frame, str(track_id), (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, color, 2)

            output_video_frames.append(frame)

        return output_video_frames

该方法首先初始化 YOLO 模型和 ByteTrack 追踪器。接下来,每一帧以 20 帧为一批进行处理,使用 YOLO 模型检测并收集每批中的物体。如果 pickle 文件存在于路径中,它会从文件中预计算出追踪信息。如果 pickle 文件不存在(你是第一次运行代码或者删除了之前的 pickle 文件)get_object_tracks 将每个检测转换为 ByteTrack 所需的格式,用这些检测更新追踪器,并将追踪信息保存在指定路径的一个新 pickle 文件中。最后,代码会迭代每一帧,为每个检测到的物体绘制边界框和追踪 ID。

要执行追踪器并保存带有边界框和追踪 ID 的新输出视频,你可以使用以下代码:

#*************** EXECUTES TRACKING MECHANISM AND OUTPUT VIDEO****************#

# Read the video frames
video_frames = []
cap = cv2.VideoCapture(video_path)
while cap.isOpened():
    ret, frame = cap.read()
    if not ret:
        break
    video_frames.append(frame)
cap.release()

#********************* EXECUTE TRACKING METHOD WITH YOLO**********************#
tracker = HockeyAnalyzer('D:/PYTHON/yolov8x.pt')
tracks = tracker.get_object_tracks(video_frames, read_from_stub=True, stub_path=pickle_path)
annotated_frames = tracker.draw_annotations(video_frames, tracks)

#*********************** SAVES VIDEO FILE ************************************#
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
height, width, _ = annotated_frames[0].shape
out = cv2.VideoWriter(output_video_path, fourcc, 30, (width, height))

for frame in annotated_frames:
    out.write(frame)
out.release()

如果你的代码一切正常,你应该会得到一个类似于示例剪辑 01的视频输出。

示例剪辑 01:基础追踪机制(物体与追踪 ID)

提示 #01: 不要低估你的计算能力!第一次运行代码时,预计帧处理可能会花费一些时间,这取决于你的计算能力。对我来说,使用 CPU 配置处理大约需要 45 到 50 分钟(可以考虑使用 CUDA 作为选项)。尽管 YOLOv8x 追踪机制非常强大,但它需要相当大的计算资源(有时我的内存占用率达到了 99%,希望它没有崩溃!🙄)。如果你遇到 YOLO 的此版本问题,可以访问Ultralytics’ GitHub,那里有更轻量的模型来平衡准确性和计算能力。

冰球场

正如你从第一步中看到的,我们遇到了一些挑战。首先,正如预期的那样,模型捕捉到所有移动物体,包括球员、裁判,甚至冰场外的物体。其次,这些红色边界框会让跟踪球员变得有些不清晰,也不太适合展示。在这一部分中,我们将专注于将检测范围缩小到仅冰场内的物体。同时,我们会将底部的边界框替换为椭圆,以确保更清晰的可视性。

让我们先从使用矩形框切换到使用椭圆。为了实现这一点,我们只需在现有代码中的标签和边界框方法上方添加一个新的方法:

#************ Design of Ellipse for tracking players instead of Bounding boxes**************#

    def draw_ellipse(self, frame, bbox, color, track_id=None, team=None):
        y2 = int(bbox[3])
        x_center = (int(bbox[0]) + int(bbox[2])) // 2
        width = int(bbox[2]) - int(bbox[0])
        color = (255, 0, 0)
        text_color = (255, 255, 255)

        cv2.ellipse(
            frame,
            center=(x_center, y2),
            axes=(int(width) // 2, int(0.35 * width)),
            angle=0.0,
            startAngle=-45,
            endAngle=235,
            color=color,
            thickness=2,
            lineType=cv2.LINE_4
        )

        if track_id is not None:
            rectangle_width = 40
            rectangle_height = 20
            x1_rect = x_center - rectangle_width // 2
            x2_rect = x_center + rectangle_width // 2
            y1_rect = (y2 - rectangle_height // 2) + 15
            y2_rect = (y2 + rectangle_height // 2) + 15

            cv2.rectangle(frame,
                          (int(x1_rect), int(y1_rect)),
                          (int(x2_rect), int(y2_rect)),
                          color,
                          cv2.FILLED)

            x1_text = x1_rect + 12
            if track_id > 99:
                x1_text -= 10
            font_scale = 0.4
            cv2.putText(
                frame,
                f"{track_id}",
                (int(x1_text), int(y1_rect + 15)),
                cv2.FONT_HERSHEY_SIMPLEX,
                font_scale,
                text_color,
                thickness=2
            )

        return frame

我们还需要更新注释步骤,通过调用椭圆方法来替换边界框和 ID:

#***********************BOUNDING BOXES AND TRACK-IDs**************************#

    def draw_annotations(self, video_frames, tracks):
        output_video_frames = []
        for frame_num, frame in enumerate(video_frames):
            frame = frame.copy() 
            player_dict = tracks["person"][frame_num]

            # Draw Players
            for track_id, player in player_dict.items():
                bbox = player["bbox"]

            # Draw ellipse and tracking IDs
                self.draw_ellipse(frame, bbox, (0, 255, 0), track_id)

                x1, y1, x2, y2 = map(int, bbox)

            output_video_frames.append(frame)

        return output_video_frames

通过这些修改,你的视频输出应该看起来更整洁,如示例剪辑 02所示。

示例剪辑 02:用椭圆替代边界框

现在,为了处理冰场边界,我们需要对计算机视觉中的分辨率有一些基本了解。在我们的使用场景中,我们使用的是 720p(1280x720 像素)格式,这意味着我们处理的每一帧图像的尺寸为 1280 像素(宽度)乘 720 像素(高度)。

使用 720p(1280x720 像素)格式意味着什么? 这意味着图像由 1280 个水平像素和 720 个垂直像素组成。在这种格式下,坐标从图像的左上角(0, 0)开始,x 坐标随着向右移动而增加,y 坐标随着向下移动而增加。这些坐标用于标记图像中的特定区域,比如使用(x1, y1)表示左上角,使用(x2, y2)表示矩形框的右下角。理解这一点有助于我们测量距离和速度,并决定在视频中关注的分析区域。

也就是说,我们将使用以下代码开始标记帧边框为绿色线条:

#********************* Border Definition for Frame***********************
import cv2

video_path = 'D:/PYTHON/video_input.mp4'
cap = cv2.VideoCapture(video_path)

#**************Read, Define and Draw corners of the frame****************
ret, frame = cap.read()

bottom_left = (0, 720)
bottom_right = (1280, 720)
upper_left = (0, 0)
upper_right = (1280, 0)

cv2.line(frame, bottom_left, bottom_right, (0, 255, 0), 2)
cv2.line(frame, bottom_left, upper_left, (0, 255, 0), 2)
cv2.line(frame, bottom_right, upper_right, (0, 255, 0), 2)
cv2.line(frame, upper_left, upper_right, (0, 255, 0), 2)

#*******************Save the frame with marked corners*********************
output_image_path = 'rink_area_marked_VALIDATION.png'
cv2.imwrite(output_image_path, frame)
print("Rink area saved:", output_image_path)

结果应该是一个绿色矩形,如示例剪辑 03中(a)所示。但为了只追踪冰场内的移动物体,我们需要一个更像(b)中的边界。

图 03:冰场边界定义(作者自创)

得到(b)的正确边界就像一个反复试验的过程,你需要测试不同的坐标,直到找到最适合你模型的边界。最初,我的目标是完全匹配冰场边界。然而,跟踪系统在边缘附近存在困难。为了提高准确性,我稍微扩大了边界,以确保所有冰场内的跟踪物体都被捕捉到,同时排除场外的物体。最终的结果,如(b)所示,是我能得到的最好的结果(你仍然可以尝试更好的情况),由这些坐标定义:

  • 左下角: (-450, 710)

  • 右下角: (2030, 710)

  • 左上角: (352, 61)

  • 右上角: (948, 61)

最后,我们将定义两个额外的区域:白队和黄队的进攻区每个队伍的目标区域)。这将使我们能够收集每个队伍在其对手区域内的一些基本位置统计数据和压力指标。

图 04:进攻区(作者自创)

#**************YELLOW TEAM OFFENSIVE ZONE****************
Bottom Left Corner: (-450, 710)
Bottom Right Corner: (2030, 710)
Upper Left Corner: (200, 150)
Upper Right Corner: (1160, 150)

#**************WHITE TEAM OFFENSIVE ZONE****************
Bottom Left Corner: (180, 150)
Bottom Right Corner: (1100, 150)
Upper Left Corner: (352, 61)
Upper Right Corner: (900, 61)

我们现在先暂时搁置这些坐标,并在下一步中解释我们将如何对每个团队进行分类。然后,我们将把所有内容汇总到我们最初的跟踪方法中。

使用深度学习进行团队预测

自从 1943 年 Warren McCulloch 和 Walter Pitts 发表了《神经活动中固有思想的逻辑演算》一文以来,已经过去了 80 多年。这篇论文为早期的神经网络研究奠定了坚实的基础。后来,在 1957 年,一个简化的神经元数学模型(接收输入、对这些输入应用权重、对其求和并输出二进制结果)启发了Frank Rosenblatt 构建了 Mark I。这是第一个硬件实现,旨在展示感知机的概念,这是一种能够从数据中学习并进行二分类的神经网络模型。从那时起,让计算机像我们一样思考的追求就没有停歇。如果这是你第一次深入学习神经网络,或者你想要刷新并巩固你的知识,我推荐阅读Shreya Rao 的这系列文章,作为深度学习的一个很好的起点。此外,你还可以访问我收集的这系列故事(不同的贡献者),你可能会发现它们有用。

为什么选择卷积神经网络(CNN)? 说实话,这并不是我最初的选择。最初,我尝试使用LandingAI,一个适合云部署的用户友好平台,并且支持通过Python API 连接。然而,出现了延迟问题(需要处理超过 1,000 帧的数据)。即便是在Roboflow的预训练模型中,尽管它们提供了高质量的数据集和模型,仍然遇到了类似的延迟问题。意识到必须在本地运行后,我尝试了基于均方误差(MSE)的方法来分类队伍和裁判的球衣颜色。尽管这种方法看似是最终解决方案,但其准确性较低。经过几天的反复试验,我最终转向了 CNN。在众多深度学习方法中,CNN 非常适合进行物体检测,而 LSTM 或 RNN 更适用于像语言转录或翻译等序列数据。

在深入研究代码之前,让我们先了解一些关于其架构的基本概念:

  • 学习的样本数据集: 数据集已被分为三类:裁判客队(白色球衣的球员)和主队(黄色球衣的球员)。每一类的样本被分为两个子集:训练数据和验证数据。训练数据将在每次迭代(Epoch)中被 CNN 使用,用以“学习”多个层次中的模式。验证数据将在每次迭代结束时用来评估模型的表现,并衡量模型对新数据的泛化能力。创建样本数据集并不困难;我大约花了 30 到 40 分钟的时间,从视频中裁剪出每一类的样本图像并将它们整理到子目录中。我成功创建了一个约 90 张图像的样本数据集,你可以在项目的 GitHub 仓库中找到。

  • 模型是如何学习的? 输入数据会通过神经网络的每一层,每一层可以是一个或多个相互连接的层,用来进行预测。每一层都使用激活函数来处理数据,从而进行预测或对数据进行更改。这些层之间的每个连接都有一个权重,决定了一个层的输出对下一个层的影响程度。目标是找到这些权重的正确组合,以最小化预测结果的错误。通过一个叫做反向传播的过程和损失函数,模型会调整这些权重,以减少误差并提高准确性。这个过程会在所谓的Epoch(前向传播 + 反向传播)中重复进行,随着每个周期模型从错误中学习,它在预测上的表现也会逐渐变好。

  • 激活函数: 如前所述,激活函数在模型学习过程中扮演着重要角色。我选择了ReLU(修正线性单元),因为它在计算上非常高效,并能缓解所谓的消失梯度问题(即多层网络可能无法有效学习)。虽然 ReLU 工作得很好,其他函数sigmoidtanhswish也有其应用,具体取决于网络的复杂性。

  • 训练轮数(Epochs): 设置合适的训练轮数需要实验。你应该考虑数据集的复杂性、CNN 模型的架构和计算资源等因素。在大多数情况下,最好在每次迭代中监控模型的表现,并在改进变得微乎其微时停止训练,以避免过拟合。考虑到我小的训练数据集,我决定以 10 轮为基准开始。然而,在其他情况下,根据指标表现和验证结果,可能需要做出调整。

  • Adam(自适应矩估计): 最终目标是减少预测输出与真实输出之间的误差。如前所述,反向传播在此过程中起着关键作用,通过调整和更新神经网络权重来随着时间的推移改进预测。反向传播基于损失函数的梯度处理权重更新,而 Adam 算法通过动态调整学习率来进一步优化这一过程,从而逐步减少误差或损失函数。换句话说,它微调了模型学习的速度。

也就是说,为了运行我们的 CNN 模型,我们需要以下 Python 包:

pip install torch torchvision 
pip install matplotlib 
pip install scikit-learn

提示-02: 确保 PyTorch 正确安装。我所有的工具都在 Anaconda 环境中设置,当我安装 PyTorch 时,一开始看起来它似乎正确安装了。然而,在运行一些库时出现了一些问题。最初,我以为是代码的问题,但经过多次修改仍然没有成功,我只好重新安装 Anaconda,并在干净的环境中重新安装 PyTorch,问题就这样解决了!

接下来,我们将指定我们的库和样本数据集的路径:

# ************CONVOLUTIONAL NEURAL NETWORK-THREE CLASSES DETECTION**************************
# REFEREE
# WHITE TEAM (Team_away)
# YELLOW TEAM (Team_home)

import os
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import torchvision.transforms as transforms
import torchvision.datasets as datasets
from torch.utils.data import DataLoader
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
import matplotlib.pyplot as plt

#Training and Validation Datasets
#Download the teams_sample_dataset file from the project's GitHub repository
data_dir = 'D:/PYTHON/teams_sample_dataset' 

首先,我们将确保每张图片的大小一致(调整为 150x150 像素),然后将其转换为代码能够理解的格式(在 PyTorch 中,输入数据通常表示为 Tensor 对象)。最后,我们将调整颜色,以便模型更容易处理(归一化),并设置加载图片的程序。这些步骤共同帮助准备图片,并将它们整理好,使模型能够有效地从中学习,避免因数据格式导致的偏差。

#******************************Data transformation***********************************
transform = transforms.Compose([
    transforms.Resize((150, 150)),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])
])

# Load dataset
train_dataset = datasets.ImageFolder(os.path.join(data_dir, 'train'), transform=transform)
val_dataset = datasets.ImageFolder(os.path.join(data_dir, 'val'), transform=transform)

train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False)

接下来,我们将定义 CNN 的架构:

#********************************CNN Model Architecture**************************************
class CNNModel(nn.Module):
    def __init__(self):
        super(CNNModel, self).__init__()
        self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1)
        self.pool = nn.MaxPool2d(kernel_size=2, stride=2, padding=0)
        self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
        self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
        self.fc1 = nn.Linear(128 * 18 * 18, 512)
        self.dropout = nn.Dropout(0.5)
        self.fc2 = nn.Linear(512, 3)  #Three Classes (Referee, Team_away,Team_home)

    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = self.pool(F.relu(self.conv3(x)))
        x = x.view(-1, 128 * 18 * 18)
        x = F.relu(self.fc1(x))
        x = self.dropout(x)
        x = self.fc2(x)  
        return x

你会注意到我们的 CNN 模型有三层(conv1,conv2,conv3)。数据首先进入卷积层(conv),在这里应用了激活函数(ReLU)。该函数使得网络能够学习数据中的复杂模式和关系。接着,池化层被激活。什么是最大池化?它是一种减少图像大小的技术,同时保留重要特征,有助于高效训练并优化内存资源。这个过程在 conv1 到 conv3 之间重复进行。最后,数据通过全连接层(fc1,fc2)进行最终分类(或决策)。

下一步,我们初始化模型,配置类别交叉熵为损失函数(通常用于分类任务),并将 Adam 作为优化器。如前所述,我们将在 10 个周期内执行模型。

#********************************CNN TRAINING**********************************************

# Model-loss function-optimizer
model = CNNModel()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

#*********************************Training*************************************************
num_epochs = 10
train_losses, val_losses = [], []

for epoch in range(num_epochs):
    model.train()
    running_loss = 0.0
    for inputs, labels in train_loader:
        optimizer.zero_grad()
        outputs = model(inputs)
        labels = labels.type(torch.LongTensor)  
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        running_loss += loss.item()

    train_losses.append(running_loss / len(train_loader))

    model.eval()
    val_loss = 0.0
    all_labels = []
    all_preds = []
    with torch.no_grad():
        for inputs, labels in val_loader:
            outputs = model(inputs)
            labels = labels.type(torch.LongTensor)  
            loss = criterion(outputs, labels)
            val_loss += loss.item()
            _, preds = torch.max(outputs, 1)  
            all_labels.extend(labels.tolist())
            all_preds.extend(preds.tolist())

为了跟踪性能,我们将添加一些代码来跟踪训练进度,打印验证指标并绘制图表。最后,我们将模型保存为hockey_team_classifier.pth,保存在你选择的指定路径中。

#********************************METRICS & PERFORMANCE************************************

    val_losses.append(val_loss / len(val_loader))
    val_accuracy = accuracy_score(all_labels, all_preds)
    val_precision = precision_score(all_labels, all_preds, average='macro', zero_division=1)
    val_recall = recall_score(all_labels, all_preds, average='macro', zero_division=1)
    val_f1 = f1_score(all_labels, all_preds, average='macro', zero_division=1)

    print(f"Epoch [{epoch + 1}/{num_epochs}], "
          f"Loss: {train_losses[-1]:.4f}, "
          f"Val Loss: {val_losses[-1]:.4f}, "
          f"Val Acc: {val_accuracy:.2%}, "
          f"Val Precision: {val_precision:.4f}, "
          f"Val Recall: {val_recall:.4f}, "
          f"Val F1 Score: {val_f1:.4f}")

#*******************************SHOW METRICS & PERFORMANCE**********************************
plt.plot(train_losses, label='Train Loss')
plt.plot(val_losses, label='Validation Loss')
plt.legend()
plt.show()

# SAVE THE MODEL FOR THE GH_CV_track_teams CODE
torch.save(model.state_dict(), 'D:/PYTHON/hockey_team_classifier.pth')

此外,除了你的“pth”文件,在完成上述所有步骤后(你可以在项目的 GitHub 仓库中找到完整代码),你应该能看到如下输出(指标可能略有不同):

图 05:CNN 模型性能指标

#**************CNN PERFORMANCE ACROSS TRAINING EPOCHS************************

Epoch [1/10], Loss: 1.5346, Val Loss: 1.2339, Val Acc: 47.37%, Val Precision: 0.7172, Val Recall: 0.5641, Val F1 Score: 0.4167
Epoch [2/10], Loss: 1.1473, Val Loss: 1.1664, Val Acc: 55.26%, Val Precision: 0.6965, Val Recall: 0.6296, Val F1 Score: 0.4600
Epoch [3/10], Loss: 1.0139, Val Loss: 0.9512, Val Acc: 57.89%, Val Precision: 0.6054, Val Recall: 0.6054, Val F1 Score: 0.5909
Epoch [4/10], Loss: 0.8937, Val Loss: 0.8242, Val Acc: 60.53%, Val Precision: 0.7222, Val Recall: 0.5645, Val F1 Score: 0.5538
Epoch [5/10], Loss: 0.7936, Val Loss: 0.7177, Val Acc: 63.16%, Val Precision: 0.6667, Val Recall: 0.6309, Val F1 Score: 0.6419
Epoch [6/10], Loss: 0.6871, Val Loss: 0.7782, Val Acc: 68.42%, Val Precision: 0.6936, Val Recall: 0.7128, Val F1 Score: 0.6781
Epoch [7/10], Loss: 0.6276, Val Loss: 0.5684, Val Acc: 78.95%, Val Precision: 0.8449, Val Recall: 0.7523, Val F1 Score: 0.7589
Epoch [8/10], Loss: 0.4198, Val Loss: 0.5613, Val Acc: 86.84%, Val Precision: 0.8736, Val Recall: 0.8958, Val F1 Score: 0.8653
Epoch [9/10], Loss: 0.3959, Val Loss: 0.3824, Val Acc: 92.11%, Val Precision: 0.9333, Val Recall: 0.9213, Val F1 Score: 0.9243
Epoch [10/10], Loss: 0.2509, Val Loss: 0.2651, Val Acc: 97.37%, Val Precision: 0.9762, Val Recall: 0.9792, Val F1 Score: 0.9769 

完成 10 个训练周期后,CNN 模型的性能指标有所改善。最初,在第 1 个周期时,模型的训练损失为 1.5346,验证准确率为 47.37%。我们应如何理解这个初始点?

准确率是评估分类性能最常见的指标之一。在我们的案例中,它表示正确预测的类别占总类别的比例。然而,单靠高准确率并不能保证整体模型的表现;你仍然可能在某些类别上做出不好的预测(正如我在早期实验中所经历的那样)。关于训练损失,它衡量模型将输入数据映射到正确标签的效果。由于我们使用的是分类函数,交叉熵损失量化了预测的类别概率与实际标签之间的差异。像 1.5346 这样的初始值表示预测类别与实际类别之间存在显著差异;理想情况下,随着训练的进行,这个值应该趋近于 0。随着训练周期的进行,我们观察到训练损失显著下降,验证准确率提高。到最后一个训练周期时,训练损失和验证损失分别降到 0.2509 和 0.2651 的最低点。

为了测试我们的 CNN 模型,我们可以选择一部分球员图像并评估其预测能力。为了测试,你可以运行以下代码并使用 项目的 GitHub 仓库中的validation_dataset 文件夹

# *************TEST CNN MODEL WITH SAMPLE DATASET***************************

import os
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision.transforms as transforms
from PIL import Image

# SAMPLE DATASET FOR VALIDATION
test_dir = 'D:/PYTHON/validation_dataset'

# CNN MODEL FOR TEAM PREDICTIONS
class CNNModel(nn.Module):
    def __init__(self):
        super(CNNModel, self).__init__()
        self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1)
        self.pool = nn.MaxPool2d(kernel_size=2, stride=2, padding=0)
        self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
        self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
        self.fc1 = nn.Linear(128 * 18 * 18, 512)
        self.dropout = nn.Dropout(0.5)
        self.fc2 = nn.Linear(512, 3) 

    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = self.pool(F.relu(self.conv3(x)))
        x = x.view(-1, 128 * 18 * 18)
        x = F.relu(self.fc1(x))
        x = self.dropout(x)
        x = self.fc2(x)  
        return x

# CNN MODEL PREVIOUSLY SAVED
model = CNNModel()
model.load_state_dict(torch.load('D:/PYTHON/hockey_team_classifier.pth'))
model.eval()

transform = transforms.Compose([
    transforms.Resize((150, 150)),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])
])

#******************ITERATION ON SAMPLE IMAGES-ACCURACY TEST*****************************

class_names = ['team_referee', 'team_away', 'team_home']

def predict_image(image_path, model, transform):
# LOADS DATASET
    image = Image.open(image_path)
    image = transform(image).unsqueeze(0)  

# MAKES PREDICTIONS
    with torch.no_grad():
        output = model(image)
        _, predicted = torch.max(output, 1)  
        team = class_names[predicted.item()]
    return team

for image_name in os.listdir(test_dir):
    image_path = os.path.join(test_dir, image_name)
    if os.path.isfile(image_path):  
        predicted_team = predict_image(image_path, model, transform)
        print(f'Image {image_name}: The player belongs to {predicted_team}')

输出应如下所示:

 # *************CNN MODEL TEST - OUTPUT ***********************************#

Image Away_image04.jpg: The player belongs to team_away
Image Away_image12.jpg: The player belongs to team_away
Image Away_image14.jpg: The player belongs to team_away
Image Home_image07.jpg: The player belongs to team_home
Image Home_image13.jpg: The player belongs to team_home
Image Home_image16.jpg: The player belongs to team_home
Image Referee_image04.jpg: The player belongs to team_referee
Image Referee_image09.jpg: The player belongs to team_referee
Image Referee_image10.jpg: The player belongs to team_referee
Image Referee_image11.jpg: The player belongs to team_referee

如你所见,模型在识别队伍并排除裁判作为队员方面表现得相当不错。

小贴士 #03: 在 CNN 设计过程中我学到的一点是,增加复杂性并不总能提升性能。一开始,我尝试了更深的模型(更多的卷积层)和基于颜色的增强来提高球员球衣的识别率。然而,在我的小数据集上,我遇到了过拟合,而不是学习到可以泛化的特征(所有图像都被预测为白队球员或裁判)。正则化技术,如 dropout 和批量归一化也很重要;它们有助于在训练过程中施加约束,确保模型能够很好地泛化到新数据。少即是多,结果有时会更好😁。

将一切整合在一起

将所有内容整合在一起需要对我们之前描述的跟踪机制进行一些调整。下面是更新后的代码逐步解析。

首先,我们将设置所需的库和路径。请注意,现在已经指定了 pickle 文件和 CNN 模型的路径。这次,如果找不到路径中的 pickle 文件,代码将抛出一个错误。如果需要,使用之前的代码生成 pickle 文件,并使用此更新版本来执行视频分析:

 import cv2
import numpy as np
from ultralytics import YOLO
import pickle
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision.transforms as transforms
from PIL import Image

# MODEL INPUTS
model_path = 'D:/PYTHON/yolov8x.pt'
video_path = 'D:/PYTHON/video_input.mp4'
output_path = 'D:/PYTHON/output_video.mp4'
tracks_path = 'D:/PYTHON/stubs/track_stubs.pkl'
classifier_path = 'D:/PYTHON/hockey_team_classifier.pth'

接下来,我们将加载模型,指定冰场坐标,并启动每帧图像的批量检测过程,每次批量为 20 帧,和之前一样。请注意,目前我们只会使用冰场边界来聚焦分析范围。文章的最后几步中,当我们包含性能统计时,我们将使用进攻区坐标。

 #*************************** Loads models and rink coordinates********************#
class_names = ['Referee', 'Tm_white', 'Tm_yellow']

class HockeyAnalyzer:
    def __init__(self, model_path, classifier_path):
        self.model = YOLO(model_path)
        self.classifier = self.load_classifier(classifier_path)
        self.transform = transforms.Compose([
            transforms.Resize((150, 150)),
            transforms.ToTensor(),
            transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])
        ])
        self.rink_coordinates = np.array([[-450, 710], [2030, 710], [948, 61], [352, 61]])
        self.zone_white = [(180, 150), (1100, 150), (900, 61), (352, 61)]
        self.zone_yellow = [(-450, 710), (2030, 710), (1160, 150), (200, 150)]

#******************** Detect objects in each frame **********************************#
    def detect_frames(self, frames):
        batch_size = 20 
        detections = [] 
        for i in range(0, len(frames), batch_size):
            detections_batch = self.model.predict(frames[i:i+batch_size], conf=0.1)
            detections += detections_batch
        return detections

接下来,我们将添加预测每个球员队伍的过程:

#*********************** Loads CNN Model**********************************************#

    def load_classifier(self, classifier_path):
        model = CNNModel()
        model.load_state_dict(torch.load(classifier_path, map_location=torch.device('cpu')))
        model.eval()
        return model

    def predict_team(self, image):
        with torch.no_grad():
            output = self.classifier(image)
            _, predicted = torch.max(output, 1)
            predicted_index = predicted.item()
            team = class_names[predicted_index]
        return team

作为下一步,我们将添加之前描述的方法,从边界框转换为椭圆形:

#************ Ellipse for tracking players instead of Bounding boxes*******************#
    def draw_ellipse(self, frame, bbox, color, track_id=None, team=None):
        y2 = int(bbox[3])
        x_center = (int(bbox[0]) + int(bbox[2])) // 2
        width = int(bbox[2]) - int(bbox[0])

        if team == 'Referee':
            color = (0, 255, 255)
            text_color = (0, 0, 0)
        else:
            color = (255, 0, 0)
            text_color = (255, 255, 255)

        cv2.ellipse(
            frame,
            center=(x_center, y2),
            axes=(int(width) // 2, int(0.35 * width)),
            angle=0.0,
            startAngle=-45,
            endAngle=235,
            color=color,
            thickness=2,
            lineType=cv2.LINE_4
        )

        if track_id is not None:
            rectangle_width = 40
            rectangle_height = 20
            x1_rect = x_center - rectangle_width // 2
            x2_rect = x_center + rectangle_width // 2
            y1_rect = (y2 - rectangle_height // 2) + 15
            y2_rect = (y2 + rectangle_height // 2) + 15

            cv2.rectangle(frame,
                          (int(x1_rect), int(y1_rect)),
                          (int(x2_rect), int(y2_rect)),
                          color,
                          cv2.FILLED)

            x1_text = x1_rect + 12
            if track_id > 99:
                x1_text -= 10
            font_scale = 0.4
            cv2.putText(
                frame,
                f"{track_id}",
                (int(x1_text), int(y1_rect + 15)),
                cv2.FONT_HERSHEY_SIMPLEX,
                font_scale,
                text_color,
                thickness=2
            )

        return frame

现在,是时候添加分析器了,它包括读取 pickle 文件、将分析范围缩小到我们之前定义的冰场边界,并调用 CNN 模型以识别每个球员的队伍并添加标签。请注意,我们包括了一个特性,用不同的颜色标记裁判,并且改变他们椭圆形的颜色。代码的最后会将处理过的帧写入输出视频。

#******************* Loads Tracked Data (pickle file )**********************************#

    def analyze_video(self, video_path, output_path, tracks_path):
          with open(tracks_path, 'rb') as f:
              tracks = pickle.load(f)

          cap = cv2.VideoCapture(video_path)
          if not cap.isOpened():
              print("Error: Could not open video.")
              return

          fps = cap.get(cv2.CAP_PROP_FPS)
          frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
          frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))

          fourcc = cv2.VideoWriter_fourcc(*'XVID')
          out = cv2.VideoWriter(output_path, fourcc, fps, (frame_width, frame_height))

          frame_num = 0
          while cap.isOpened():
              ret, frame = cap.read()
              if not ret:
                  break

#***********Checks if the player falls within the rink area**********************************#
              mask = np.zeros(frame.shape[:2], dtype=np.uint8)
              cv2.fillConvexPoly(mask, self.rink_coordinates, 1)
              mask = mask.astype(bool)
              # Draw rink area
              #cv2.polylines(frame, [self.rink_coordinates], isClosed=True, color=(0, 255, 0), thickness=2)

              # Get tracks from frame
              player_dict = tracks["person"][frame_num]
              for track_id, player in player_dict.items():
                  bbox = player["bbox"]

              # Check if the player is within the Rink Area
                  x_center = int((bbox[0] + bbox[2]) / 2)
                  y_center = int((bbox[1] + bbox[3]) / 2)

                  if not mask[y_center, x_center]:
                      continue  

#**********************************Team Prediction********************************************#
                  x1, y1, x2, y2 = map(int, bbox)
                  cropped_image = frame[y1:y2, x1:x2]
                  cropped_pil_image = Image.fromarray(cv2.cvtColor(cropped_image, cv2.COLOR_BGR2RGB))
                  transformed_image = self.transform(cropped_pil_image).unsqueeze(0)
                  team = self.predict_team(transformed_image)

#************ Ellipse for tracked players and labels******************************************#
                  self.draw_ellipse(frame, bbox, (0, 255, 0), track_id, team)

                  font_scale = 1  
                  text_offset = -20  

                  if team == 'Referee':
                      rectangle_width = 60
                      rectangle_height = 25
                      x1_rect = x1
                      x2_rect = x1 + rectangle_width
                      y1_rect = y1 - 30
                      y2_rect = y1 - 5
                      # Different setup for Referee
                      cv2.rectangle(frame,
                                    (int(x1_rect), int(y1_rect)),
                                    (int(x2_rect), int(y2_rect)),
                                    (0, 0, 0),  
                                    cv2.FILLED)
                      text_color = (255, 255, 255)  
                  else:
                      if team == 'Tm_white':
                          text_color = (255, 215, 0)  # White Team: Blue labels
                      else:
                          text_color = (0, 255, 255)  # Yellow Team: Yellow labels

              # Draw Team labels
                  cv2.putText(
                      frame,
                      team,
                      (int(x1), int(y1) + text_offset), 
                      cv2.FONT_HERSHEY_PLAIN,            
                      font_scale,
                      text_color,
                      thickness=2
                  )

              # Write output video
              out.write(frame)
              frame_num += 1

          cap.release()
          out.release()

最后,我们添加 CNN 的架构(在 CNN 设计过程中定义)并执行冰球分析器:

 #**********************CNN Model Architecture ******************************#
class CNNModel(nn.Module):
    def __init__(self):
        super(CNNModel, self).__init__()
        self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1)
        self.pool = nn.MaxPool2d(kernel_size=2, stride=2, padding=0)
        self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
        self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
        self.fc1 = nn.Linear(128 * 18 * 18, 512)
        self.dropout = nn.Dropout(0.5)
        self.fc2 = nn.Linear(512, len(class_names))  

    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = self.pool(F.relu(self.conv3(x)))
        x = x.view(-1, 128 * 18 * 18)
        x = F.relu(self.fc1(x))
        x = self.dropout(x)
        x = self.fc2(x)
        return x

#*********Execute HockeyAnalyzer/classifier and Save Output************#
analyzer = HockeyAnalyzer(model_path, classifier_path)
analyzer.analyze_video(video_path, output_path, tracks_path)

运行所有步骤后,你的视频输出应该如下所示:

示例片段 06:跟踪球员和队伍

请注意,在这次更新中,物体检测仅限于冰球场内,且队伍和裁判已被区分开来。虽然 CNN 模型仍需微调,并且偶尔在一些球员身上会失去稳定性,但在整个视频中,它仍然大部分时间是可靠且准确的。

速度、距离和进攻压力

跟踪队伍和球员的能力为衡量表现开辟了令人兴奋的可能性,例如生成热图、分析速度和覆盖的距离、跟踪如区域进入或退出等动作,以及深入研究球员的详细指标。为了让我们能感受这一点,我们将添加三项表现指标:每个球员的平均速度、每支队伍滑行的距离,以及进攻压力以每支队伍在对方区域内所花费的距离占总距离的百分比来衡量)。我将把更详细的统计数据留给你们!

我们开始将冰场的坐标从基于像素的度量转换为近似米数。这一调整使我们能够以米为单位读取数据,而非像素。视频中看到的冰场的实际尺寸大约为 15mx30m(宽度为 15 米,高度为 30 米)。为了方便这一转换,我们引入了一种将像素坐标转换为米的方式。通过定义冰场的实际尺寸,并使用其角落的像素坐标(从左到右,从上到下),我们获得了转换因子。这些因子将支持我们估算米数和每秒米数速度的过程。(另一个有趣的技术是透视变换,你可以探索并应用它)

#*********************Loads models and rink coordinates*****************#
class_names = ['Referee', 'Tm_white', 'Tm_yellow']

class HockeyAnalyzer:
    def __init__(self, model_path, classifier_path):
        *
        *
        *
        *
        *
        *
        self.pixel_to_meter_conversion() #<------ Add this utility method

#***********Pixel-based measurements to meters***************************#
    def pixel_to_meter_conversion(self):
        #Rink real dimensions in meters
        rink_width_m = 15
        rink_height_m = 30

        #Pixel coordinates for rink dimensions
        left_pixel, right_pixel = self.rink_coordinates[0][0], self.rink_coordinates[1][0]
        top_pixel, bottom_pixel = self.rink_coordinates[2][1], self.rink_coordinates[0][1]

        #Conversion factors
        self.pixels_per_meter_x = (right_pixel - left_pixel) / rink_width_m
        self.pixels_per_meter_y = (bottom_pixel - top_pixel) / rink_height_m

    def convert_pixels_to_meters(self, distance_pixels):
        #Convert pixels to meters
        return distance_pixels / self.pixels_per_meter_x, distance_pixels / self.pixels_per_meter_y

我们现在准备好以每秒米数为单位添加每个球员的速度。为了做到这一点,我们需要进行三处修改。首先,在HockeyAnalyzer 类中初始化一个名为previous_positions的空字典,以帮助我们比较球员的当前和前一个位置。同样,我们还将创建一个team_stats结构来存储每支队伍的统计数据,以便进一步可视化。

接下来,我们将添加一个速度方法来估算球员的速度(以每秒像素数为单位),然后使用前面解释的转换因子将其转换为每秒米数。最后,在analyze_video 方法中,我们将调用新的速度方法,并将速度添加到每个追踪的对象(球员和裁判)中。这就是这些更改的效果:

#*********************Loads models and rink coordinates*****************#
class_names = ['Referee', 'Tm_white', 'Tm_yellow']

class HockeyAnalyzer:
    def __init__(self, model_path, classifier_path):
        *
        *
        *
        *
        *
        *
        *
        self.pixel_to_meter_conversion() 
        self.previous_positions = {} #<------ Add this.Initializes empty dictionary 
        self.team_stats = {
                    'Tm_white': {'distance': 0, 'speed': [], 'count': 0, 'offensive_pressure': 0},
                    'Tm_yellow': {'distance': 0, 'speed': [], 'count': 0, 'offensive_pressure': 0}
                } #<------ Add this.Initializes empty dictionary

#**************** Speed: meters per second********************************#
    def calculate_speed(self, track_id, x_center, y_center, fps):
        current_position = (x_center, y_center)
        if track_id in self.previous_positions:
            prev_position = self.previous_positions[track_id]
            distance_pixels = np.linalg.norm(np.array(current_position) - np.array(prev_position))
            distance_meters_x, distance_meters_y = self.convert_pixels_to_meters(distance_pixels)
            speed_meters_per_second = (distance_meters_x**2 + distance_meters_y**2)**0.5 * fps
        else:
            speed_meters_per_second = 0
        self.previous_positions[track_id] = current_position
        return speed_meters_per_second

#******************* Loads Tracked Data (pickle file )**********************************#

    def analyze_video(self, video_path, output_path, tracks_path):
          with open(tracks_path, 'rb') as f:
              tracks = pickle.load(f)

        *
        *
        *
        *
        *
        *
        *
        *
              # Draw Team label
                  cv2.putText(
                      frame,
                      team,
                      (int(x1), int(y1) + text_offset), 
                      cv2.FONT_HERSHEY_PLAIN,            
                      font_scale,
                      text_color,
                      thickness=2
                  )

#**************Add these lines of code --->:

                  speed = self.calculate_speed(track_id, x_center, y_center, fps)
                  # Speed label 
                  speed_font_scale = 0.8  
                  speed_y_position = int(y1) + 20
                  if speed_y_position > int(y1) - 5:
                      speed_y_position = int(y1) - 5

                  cv2.putText(
                      frame,
                      f"Speed: {speed:.2f} m/s",  
                      (int(x1), speed_y_position),  
                      cv2.FONT_HERSHEY_PLAIN,       
                      speed_font_scale,
                      text_color,
                      thickness=2
                  )

              # Write output video
              out.write(frame)
              frame_num += 1

          cap.release()
          out.release()

如果你在添加这些新代码时遇到问题,你可以随时访问项目的 GitHub 仓库,在这里你可以找到完整的集成代码。此时,你的视频输出应该如下所示(注意速度已添加到每个球员的标签上):

示例片段 07:跟踪球员和速度

最后,让我们添加一个统计板,跟踪每支队伍每个球员的平均速度,并显示其他数据,例如行进的距离和在对方区域内的进攻压力。

我们已经定义了进攻区域并将其集成到我们的代码中。现在,我们需要跟踪每个球员进入对方区域的次数。为此,我们将实现一个方法,使用射线投射算法。这个算法检查球员的位置是否在白队或黄队的进攻区域内。它通过从球员到目标区域画一条虚拟线来工作。如果这条线穿过一个边界,则表示球员在内部;如果穿过多个边界(在我们这个案例中是穿过四个边界中的两个),则表示球员在外部。代码接着扫描整个视频,确定每个跟踪物体的区域状态。

 #************ Locate player's position in Target Zone***********************#

    def is_inside_zone(self, position, zone):
          x, y = position
          n = len(zone)
          inside = False
          p1x, p1y = zone[0]
          for i in range(n + 1):
              p2x, p2y = zone[i % n]
              if y > min(p1y, p2y):
                  if y <= max(p1y, p2y):
                      if x <= max(p1x, p2x):
                          if p1y != p2y:
                              xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
                          if p1x == p2x or x <= xinters:
                              inside = not inside
              p1x, p1y = p2x, p2y
          return inside

现在,我们将通过添加一个方法来处理表现指标,该方法将在表格格式中为每支队伍显示球员平均速度总行进距离进攻压力(在对方区域内的时间百分比)。使用 OpenCV,我们将这些指标格式化为覆盖在视频上的表格,并将加入动态更新机制,以保持游戏过程中的实时统计。

#*******************************Performance metrics*********************************************#
    def draw_stats(self, frame):
         avg_speed_white = np.mean(self.team_stats['Tm_white']['speed']) if self.team_stats['Tm_white']['count'] > 0 else 0
         avg_speed_yellow = np.mean(self.team_stats['Tm_yellow']['speed']) if self.team_stats['Tm_yellow']['count'] > 0 else 0
         distance_white = self.team_stats['Tm_white']['distance']
         distance_yellow = self.team_stats['Tm_yellow']['distance']

         offensive_pressure_white = self.team_stats['Tm_white'].get('offensive_pressure', 0)
         offensive_pressure_yellow = self.team_stats['Tm_yellow'].get('offensive_pressure', 0)

         Pressure_ratio_W = offensive_pressure_white/distance_white   *100  if self.team_stats['Tm_white']['distance'] > 0 else 0
         Pressure_ratio_Y = offensive_pressure_yellow/distance_yellow *100  if self.team_stats['Tm_yellow']['distance'] > 0 else 0

         table = [
             ["", "Away_White", "Home_Yellow"],
             ["Average Speed\nPlayer", f"{avg_speed_white:.2f} m/s", f"{avg_speed_yellow:.2f} m/s"],
             ["Distance\nCovered", f"{distance_white:.2f} m", f"{distance_yellow:.2f} m"],
             ["Offensive\nPressure %", f"{Pressure_ratio_W:.2f} %", f"{Pressure_ratio_Y:.2f} %"],
         ]

         text_color = (0, 0, 0)  
         start_x, start_y = 10, 590  
         row_height = 30     # Manage Height between rows
         column_width = 150  # Manage Width  between rows
         font_scale = 1  

         def put_multiline_text(frame, text, position, font, font_scale, color, thickness, line_type, line_spacing=1.0):
             y0, dy = position[1], int(font_scale * 20 * line_spacing)  # Adjust line spacing here
             for i, line in enumerate(text.split('\n')):
                 y = y0 + i * dy
                 cv2.putText(frame, line, (position[0], y), font, font_scale, color, thickness, line_type)

         for i, row in enumerate(table):
             for j, text in enumerate(row):
                 if i in [1,2, 3]:  
                     put_multiline_text(
                         frame,
                         text,
                         (start_x + j * column_width, start_y + i * row_height),
                         cv2.FONT_HERSHEY_PLAIN,
                         font_scale,
                         text_color,
                         1,
                         cv2.LINE_AA,
                         line_spacing= 0.8 
                     )
                 else:
                     cv2.putText(
                         frame,
                         text,
                         (start_x + j * column_width, start_y + i * row_height),
                         cv2.FONT_HERSHEY_PLAIN,
                         font_scale,
                         text_color,
                         1,
                         cv2.LINE_AA,
                     )       

#****************** Track and update game stats****************************************#

    def update_team_stats(self, team, speed, distance, position):
        if team in self.team_stats:
            self.team_stats[team]['speed'].append(speed)
            self.team_stats[team]['distance'] += distance
            self.team_stats[team]['count'] += 1

            if team == 'Tm_white':
                if self.is_inside_zone(position, self.zone_white):
                    self.team_stats[team]['offensive_pressure'] += distance
            elif team == 'Tm_yellow':
                if self.is_inside_zone(position, self.zone_yellow):
                    self.team_stats[team]['offensive_pressure'] += distance

为了在视频中显示统计数据,我们需要调用analyze_video 方法,因此请确保在定义速度标签后、处理输出视频之前,添加这些额外的代码行:

*
*
*
*
*
*
*
#Speed label 
                  speed_font_scale = 0.8  
                  speed_y_position = int(y1) + 20
                  if speed_y_position > int(y1) - 5:
                      speed_y_position = int(y1) - 5

                  cv2.putText(
                      frame,
                      f"Speed: {speed:.2f} m/s",  
                      (int(x1), speed_y_position),  
                      cv2.FONT_HERSHEY_PLAIN,       
                      speed_font_scale,
                      text_color,
                      thickness=2
                  )
#**************Add these lines of code--->:

                  distance = speed / fps
                  position = (x_center, y_center)
                  self.update_team_stats(team, speed, distance, position)

              # Write output video
              out.write(frame)
              frame_num += 1

每个球员所覆盖的距离(单位:米)是通过将其速度(以米/秒为单位)除以帧率(以帧/秒为单位)来计算的。这个计算方法使我们能够估算每个球员在视频中每次帧变化之间移动的距离。如果一切顺利,最终的视频输出应该是这样的:

示例片段 08:最终输出

考虑事项与未来工作

该模型是使用计算机视觉追踪冰球比赛中球员的基本设置(或任何团队运动)。然而,还有许多精细调优可以改进,并且可以添加新功能。以下是我正在研究的一些想法,用于下一版本 2.0,你也可以考虑这些想法:

跟踪冰球的挑战: 根据相机的朝向和分辨率,跟踪冰球是具有挑战性的,因为它的尺寸相较于足球或篮球球来说较小。但如果你能够实现这一点,便能开启一些有趣的可能性来追踪表现,例如控球时间、进攻机会或射门数据。这同样适用于个别球员的表现;在冰球中,球员的换人频率远高于其他团队运动,因此,在一个时段内追踪每个球员的表现也是一种挑战。

计算资源,哦,为什么是计算! 我在一个 CPU 配置上运行了所有代码,但由于在设计过程中内存不足(有时导致蓝屏 😥),遇到了问题(建议使用 CUDA 设置)。我们的示例视频大约 40 秒长,最初为 5 MB,但在运行模型后,输出文件的大小增加到 34 MB。想象一下完整的 20 分钟比赛期间的大小。所以,在扩展时,你应该考虑计算资源和存储。

不要低估 MLOps: 要快速部署和扩展,我们需要高效的机器学习管道,支持频繁执行,并且可靠。这需要考虑持续集成-部署-训练方法。我们的用例是为特定场景构建的,但如果条件发生变化,比如摄像头方向或球衣颜色变化怎么办?为了扩展,我们必须采纳 CI/CD/CT 思维模式。

希望你对这个计算机视觉项目感兴趣,你可以在这个 GitHub 仓库访问完整的代码。如果你想支持该地区的冰球和冰球运动发展,可以关注APHL (我们总是需要您捐赠的二手设备,供年轻球员使用,并正在建设我们的第一个官方冰球场),全球范围内,也可以关注并支持Friendship League

我漏掉了什么吗? 欢迎提出建议。让我们继续交流!

posted @ 2025-01-09 18:55  绝不原创的飞龙  阅读(624)  评论(0)    收藏  举报