Linux 控制组的威力:容器如何控制其资源
使用 Linux 控制组优化容器资源分配
·发表于Towards Data Science ·阅读时间 8 分钟·2023 年 1 月 10 日
–
照片由Joshua Hoehne提供,来源于Unsplash
上一篇文章探讨了如何使用 Linux 命名空间在单一 Linux 系统内创建隔离环境。本文是我们深入了解容器如何工作的努力的一部分,旨在揭示其背后的机制。
## 容器:它们如何在幕后工作以及为何它们正在主导数据科学领域
初学者理解 Docker 魔法的指南
towardsdatascience.com
命名空间是我们旅程的第一步。我们看到如何创建一个PID
命名空间,创建一个其中运行的进程假设自己是唯一存在的世界,但如何对它们可以消耗的资源数量施加限制呢?这就要引入 Linux cgroups 了。
Linux 控制组,或称 cgroups,是一种强大的工具,用于管理和分配 Linux 系统中的资源。它们允许管理员限制进程或进程组使用的资源,确保关键系统服务始终能够获得其正常运行所需的资源。
但 cgroups 不仅对系统管理员有用——它们还为容器提供了一种控制自身资源的方法,使其在共享主机环境中更高效、更可靠地运行。
在本文中,我们将探讨在容器上下文中使用 cgroups 的好处,并展示如何在自己的环境中开始使用 cgroups。首先,我们将创建一个控制组,以限制在其上下文中运行的进程的内存消耗,然后在其下运行一个完整的命名空间。让我们深入了解吧!
学习速率 是一份面向那些对 MLOps 世界充满好奇的人的新闻通讯。MLOps 是一个广泛的领域,致力于以高效和可重现的方式将 ML 模型投入生产。容器在这个流程中发挥着关键作用。如果你想了解更多类似的主题,请点击这里订阅。每个月的第一个星期六,你将收到我关于最新 MLOps 新闻和文章的更新和见解!
什么是 Cgroups?
Linux 控制组或 cgroups 是一种内核特性,允许管理员将 CPU、内存和 I/O 带宽等资源分配给进程组。
Cgroups 提供了一种控制系统资源的使用量的方法。例如,管理员可以为与特定应用程序相关的进程组创建一个 cgroup(例如,运行在服务器上的 web 应用程序),然后设置这些进程允许使用的 CPU 和内存的限制。
Cgroups 对于各种目的都很有用,包括提高系统性能、隔离进程以提高安全性,以及简化在单个系统上管理多个应用程序。
在容器上下文中,cgroups 允许我们限制每个容器可以消耗的资源,以便我们的应用程序不会占用整个服务器,或者确保它拥有运行所需的资源。
例如,设置 Kubernetes 中 pod 的资源部分总是一个好主意,因为它帮助 Kubernetes 调度器决定将 pod 调度到哪个节点。这确保了我们的应用程序将拥有运行所需的一切。如果没有,应用程序甚至无法启动。
我们如何在实际中使用 cgroups?通过创建两个简单的示例来了解:首先,我们将创建一个简单的应用程序,并在特定的 cgroup 下运行它,然后在类似的上下文中运行一个完整的命名空间。
一个简单的示例
我们在 cgroups 世界的旅程从这里开始。首先,我们将创建一个简单的应用程序,并在内存限制的 cgroup 中运行它。然后,我们将在相同的上下文中创建一个 Linux 命名空间,并在该命名空间中运行相同的应用程序。
Memhog
为了使这个示例有效,我们需要控制应用程序使用的内存量。为此,我们将使用一个名为 memhog
的 Debian 软件包。
memhog
是一个简单的软件包,用于分配我们指定的内存以供测试(有关更多详细信息,请参见 这里 的手册页)。
第一步是安装 memhog
:
sudo apt update && sudo apt install numactl
测试你是否正确安装了 memhog
,让它分配 100 兆字节的内存:
memhog 100M
输出应该类似于以下内容:
..........
如果你得到了这个输出,那么你可以继续!让我们创建一个每 2 秒运行一次的 bash 脚本。因此,我们的应用程序是一个每两秒请求 100 兆字节的服务。创建一个新文件,命名为 memhog.sh
并将以下内容放入其中:
#!/bin/bash
while true; do memhog 100M; sleep 2; done
最后,使文件可执行:
sudo chmod +x memhog.sh
创建一个 Cgroup
要创建、管理和监控 cgroup,我们需要另一个名为 cgroup-tools
的软件包。所以,首先,你需要安装它:
sudo apt install cgroup-tools
现在我们在工具箱中有了这个,过程如下:
-
使用我们刚刚安装的软件包创建一个新的 cgroup
-
为我们想要控制的资源在这个特定的 cgroup 中设置一个限制
-
在这个 cgroup 下运行应用程序或命名空间
因此,让我们首先创建 cgroup。为此,请使用以下命令:
sudo cgcreate -g memory:memhog-limiter
这个命令创建了一个新的 cgroup(cgcreate
),类型为内存,并将其名称设置为 memhog-limiter
。这个命令实际上做的是在 /sys/fs/cgroup/memory
下创建了一个新目录,你可以通过运行 ls
来查看其内容:
ls -la /sys/fs/cgroup/memory/memhog-limiter/
drwxr-xr-x 2 root root 0 Jan 9 05:56 .
dr-xr-xr-x 8 root root 0 Jan 4 10:31 ..
-rw-r--r-- 1 root root 0 Jan 9 05:56 cgroup.clone_children
--w--w--w- 1 root root 0 Jan 9 05:56 cgroup.event_control
-rw-r--r-- 1 root root 0 Jan 9 05:56 cgroup.procs
-rw-r--r-- 1 root root 0 Jan 9 05:56 memory.failcnt
--w------- 1 root root 0 Jan 9 05:56 memory.force_empty
-rw-r--r-- 1 root root 0 Jan 9 05:56 memory.kmem.failcnt
-rw-r--r-- 1 root root 0 Jan 9 05:56 memory.kmem.limit_in_bytes
-rw-r--r-- 1 root root 0 Jan 9 05:56 memory.kmem.max_usage_in_bytes
-r--r--r-- 1 root root 0 Jan 9 05:56 memory.kmem.slabinfo
-rw-r--r-- 1 root root 0 Jan 9 05:56 memory.kmem.tcp.failcnt
-rw-r--r-- 1 root root 0 Jan 9 05:56 memory.kmem.tcp.limit_in_bytes
-rw-r--r-- 1 root root 0 Jan 9 05:56 memory.kmem.tcp.max_usage_in_bytes
-r--r--r-- 1 root root 0 Jan 9 05:56 memory.kmem.tcp.usage_in_bytes
-r--r--r-- 1 root root 0 Jan 9 05:56 memory.kmem.usage_in_bytes
-rw-r--r-- 1 root root 0 Jan 9 05:56 memory.limit_in_bytes
-rw-r--r-- 1 root root 0 Jan 9 05:56 memory.max_usage_in_bytes
-rw-r--r-- 1 root root 0 Jan 9 05:56 memory.move_charge_at_immigrate
-r--r--r-- 1 root root 0 Jan 9 05:56 memory.numa_stat
-rw-r--r-- 1 root root 0 Jan 9 05:56 memory.oom_control
---------- 1 root root 0 Jan 9 05:56 memory.pressure_level
-rw-r--r-- 1 root root 0 Jan 9 05:56 memory.soft_limit_in_bytes
-r--r--r-- 1 root root 0 Jan 9 05:56 memory.stat
-rw-r--r-- 1 root root 0 Jan 9 05:56 memory.swappiness
-r--r--r-- 1 root root 0 Jan 9 05:56 memory.usage_in_bytes
-rw-r--r-- 1 root root 0 Jan 9 05:56 memory.use_hierarchy
-rw-r--r-- 1 root root 0 Jan 9 05:56 notify_on_release
-rw-r--r-- 1 root root 0 Jan 9 05:56 tasks
(根据你的系统,目录的地点或结构可能会有所不同)
现在我们已经创建了 cgroup,让我们设置我们的限制。我们将设置 50 兆字节的限制,这意味着在这个上下文中运行的任何进程都不能超过这个限制。类似地,谈到进程组时,它们的总需求也不能超过这个限制。
要设置内存限制,请运行以下命令:
sudo cgset -r memory.limit_in_bytes=50M memhog-limiter
这个命令为 cgroup momhog-limiter
设置了 50 兆字节的内存限制。如果你查看我们之前看到的目录结构中的相应文件,你会看到正是这个(以字节为单位):
cat /sys/fs/cgroup/memory/memhog-limiter/memory.limit_in_bytes
52428800
我们准备在这个上下文中运行我们的应用程序和命名空间。
设定你的限制!
我们现在的状态如下:我们创建了一个每两秒分配 100 兆字节内存的服务。我们还创建了一个限制进程或一组进程可以消耗的内存为 50 兆字节的 cgroup。如果我们在这个上下文中尝试运行我们的服务,你期望会发生什么?
不再废话,要在这个上下文中执行服务,请运行下面的命令:
sudo cgexec -g memory:memhog-limiter ./memhogtest.sh
结果正如我们预期的那样:每次它尝试运行时,Linux 内核都会终止服务:
....../memhogtest.sh: line 2: 174662 Killed memhog 100M
....../memhogtest.sh: line 2: 174668 Killed memhog 100M
很好;这正是我们想看到的。但现在,我们如何在这个上下文中创建一个命名空间呢?这类似于容器的功能,因此如果我们实现了这一点,我们将朝着在没有 Docker 的情况下创建类似容器的环境的目标迈进,这也是本系列的终极目标。
在这个上下文中创建命名空间是相当简单的。以下命令可能对你来说很熟悉:
sudo cgexec -g memory:memhog-limiter unshare -fp --mount-proc
当然,我们在之前使用了第一部分来在memhog-limiter
cgroup 的上下文中运行服务(sudo cgexec -g memory:memhog-limiter
)。此外,我们在文章中看到了命令的第二部分,我们讨论了命名空间(unshare -fp — mount-proc
):这是我们用来创建新的PID
命名空间的命令。
因此,如果我们将所有内容结合起来,这个命令在我们的 cgroup 上下文中创建了一个新的PID
命名空间。要验证你是否在新的命名空间中,请运行以下命令:
# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 08:00 pts/0 00:00:00 -bash
root 12 1 0 08:00 pts/0 00:00:00 ps -ef
正如你所看到的,在你的新命名空间中,只有bash
作为PID
1 运行。因此,你现在启动的每个服务都将在我们的 cgroup 上下文中启动。让我们验证一下:
# ./memhogtest.sh
....../memhogtest.sh: line 2: 14 Killed memhog 100M
....../memhogtest.sh: line 2: 16 Killed memhog 100M
....../memhogtest.sh: line 2: 18 Killed memhog 100M
太好了!我们得到了和之前一样的输出。如果你想尝试一下,你可以减少memhog
尝试分配的内存量,让它正常工作。无论如何,祝贺你!你离创建自己的 Linux 容器而无需 Docker 更进一步了!
结论
总之,Linux cgroups 是管理和分配 Linux 系统资源的强大工具。它们允许管理员限制进程或进程组可以使用的资源量,确保重要的系统服务始终能够获得其正常运行所需的资源。
在容器的上下文中,cgroups 提供了一种让容器控制自身资源的方式,使它们能够在共享的主机环境中更高效、更可靠地运行。
这个故事探讨了如何在实践中使用 cgroups,并让我们离最终目标更近了一步:在 Linux 中创建类似容器的环境而无需 Docker。下一站?覆盖文件系统!
关于作者
我的名字是Dimitris Poulopoulos,我是为Arrikto工作的机器学习工程师。我为欧洲委员会、Eurostat、IMF、欧洲中央银行、OECD 和宜家等主要客户设计并实施了人工智能和软件解决方案。
如果你对阅读更多关于机器学习、深度学习、数据科学和数据运维的帖子感兴趣,可以在Medium、LinkedIn或在 Twitter 上关注@james2pl。
表达的观点完全是我个人的,并不代表我雇主的观点或意见。
OpenAI 的函数调用在数据管道中的力量:全面指南
利用 OpenAI 的函数调用功能改造数据管道:使用 PostgreSQL 和 FastAPI 实现电子邮件发送工作流
·发表于 Towards Data Science ·阅读时间 11 分钟·2023 年 6 月 22 日
–
介绍
AI 的激动人心的世界通过 OpenAI 最新的大型语言模型(LLMs)引入的函数调用能力又迈进了一步。这个新功能增强了人类与 AI 之间的互动,将其从简单的问答形式转变为更具动态性和主动性的对话。
那么这些函数调用能力到底是什么呢?本质上,它们允许 LLM 在对话中根据输入指令调用预定义的函数。这可以是任何操作,从发送电子邮件到根据对话的上下文从数据库中提取数据。
使用函数调用能力的好处和应用广泛。它显著提升了 AI 在各种应用中的动态效用,从客户服务到数据管道的构建。
想象一下一个客户服务 AI,它可以回答问题并执行诸如预约、向电子邮件地址发送信息或实时更新客户详情等操作。或者考虑一个数据管道,其中 LLM 代理可以根据命令获取、更新和处理数据。
在接下来的章节中,我们将进一步探讨这些应用,并提供一个逐步指南,介绍如何通过构建一个发送电子邮件的管道来利用这一新能力。
图 1:LLMs 正成为我们可以合作的智能代理(图片来源)
一如既往,代码可以在我的 Github 上找到。
理解新的函数调用能力并与 LangChain 进行比较
我们想要了解 OpenAI 新引入的函数调用功能在 AI 竞赛中带来了什么。为此,让我们深入了解它与市场上其他工具和框架(如 LangChain)的区别。我们在本系列的第一篇文章中已经介绍了 LangChain。它是一个用于开发 AI 驱动应用程序的流行框架。函数调用功能和 LangChain 带来了独特的优势和能力,旨在使 AI 更加可用、多样和动态。
我们已经知道,函数调用功能为 AI 应用程序增加了一层额外的互动性。它使模型能够在对话中调用预定义的函数,增强了 LLM 的活力和反应能力。这个新功能可能会简化将功能添加到 AI 应用程序的过程。开发者需要定义这些函数,然后模型可以根据上下文在对话中执行它们。这里的主要优势是与 OpenAI 模型的直接集成,这便于使用、快速设置,并且降低了熟悉 OpenAI 生态系统的开发者的学习曲线。
另一方面,LangChain 提供了一个全面且多功能的框架,旨在开发更复杂、数据感知和自主的 AI 应用程序。它允许语言模型与其环境互动,并根据高级指令做出决策。其模块提供了构建应用程序的抽象和标准接口,包括模型、提示、记忆、索引、链、代理和回调。
LangChain 的方法促进了构建应用程序,其中 LLM 可以在不同的交互中保持其状态,顺序调用和链式调用不同的实用程序,甚至与外部数据源互动。我们可以将这些视为增强的函数调用能力。因此,它对开发者特别有用,尤其是那些构建复杂、多步骤应用程序并利用语言模型的开发者。增加的复杂性带来的缺点是使用起来的学习曲线更陡峭。
用例 — 转换数据管道
在我看来,数据管道是 LLMs 新函数调用能力最令人兴奋的应用领域之一。数据管道是任何数据驱动组织中一个关键组件,它负责收集、处理和分发数据。通常,这些过程是静态的、预定义的,并且需要人工干预以进行任何更改或更新。这正是我们上面讨论的 LLM 动态行为创造机会的地方。
传统上,数据库查询需要特定的查询语言知识,如 SQL。借助 LLM 直接调用函数、服务和数据库的能力,用户可以通过对话检索数据,而不需要显式的查询表述。LLM 可以将用户的请求转换为数据库查询,获取数据,并以用户友好的格式实时返回。这一功能可以使组织内的不同角色都能平等地访问数据。
另一个可能发生变化的方面是数据转换。数据转换通常需要在分析之前进行单独的数据清理和处理步骤。LLM 可以通过根据用户的指示交互式地执行数据清理和操作任务来简化这一过程。此外,在对话中处理实时数据允许进行更多的探索性和迭代性数据分析。
第三个用例是数据监控。它涉及定期检查以确保数据管道中数据的准确性和一致性。借助 LLM,监控任务可以变得更加互动和高效。例如,LLM 可以在对话中提醒用户数据不一致,并立即采取纠正措施。
最后,LLM 还可以自动创建和分发数据报告。用户可以指示 LLM 根据特定标准生成报告,LLM 可以获取数据,创建报告,甚至将其发送给相关的接收者。
利用 OpenAI 函数调用能力构建电子邮件发送管道
我们的目标是创建一个发送电子邮件给用户的流程。虽然这听起来很简单,但这个过程的美在于由 LLM 控制的不同组件之间的相互作用。AI 模型不仅仅生成电子邮件正文;它还动态地与数据库互动以检索用户信息,撰写上下文适当的邮件,然后指示服务发送邮件。
我们的管道由三个主要组件组成:PostgreSQL、FastAPI 和 OpenAI LLM。我们使用 PostgreSQL 存储用户数据。这些数据包括用户名及其关联的电子邮件地址。它作为我们用户信息的真实来源。FastAPI 是一个现代、高性能的 Python Web 框架,用于构建 API。我们使用 FastAPI 服务来模拟发送电子邮件的过程。当服务接收到发送电子邮件的请求时,它会返回一个确认电子邮件已发送的响应。LLM 作为整个过程的协调者。它控制对话,根据上下文确定必要的操作,与 PostgreSQL 数据库互动以获取用户信息,撰写电子邮件内容,并指示 FastAPI 服务发送电子邮件。
实施 PostgreSQL 数据库
我们管道的第一个组件是 PostgreSQL 数据库,我们在其中存储用户数据。使用 Docker 设置 PostgreSQL 实例变得简单且可重复,Docker 是一个允许我们容器化和隔离数据库环境的平台。
你首先需要安装 Docker 来设置 PostgreSQL Docker 容器。安装后,你可以拉取 PostgreSQL 镜像并将其作为容器运行。我们将容器的端口 5432 映射到主机的端口 5432,以访问数据库。在生产环境中,请将密码设置为环境变量,不要像下面这样直接在命令中设置。我们这样做是为了加快我们的过程。
docker run --name user_db -e POSTGRES_PASSWORD=testpass -p 5432:5432 -d postgres
在我们的 PostgreSQL 实例运行后,我们现在可以创建一个数据库和一个表来存储我们的用户数据。我们将使用一个初始化脚本来创建一个 users
表,其中包含 username
和 email
列,并用一些虚拟数据填充它。这个脚本被放置在一个目录中,然后映射到容器中的 /docker-entrypoint-initdb.d
目录。PostgreSQL 在启动时执行此目录下的任何脚本。以下是脚本(user_init.sql
)的样子:
CREATE DATABASE user_database;
\c user_database;
CREATE TABLE users (
username VARCHAR(50),
email VARCHAR(50)
);
INSERT INTO users (username, email) VALUES
('user1', 'user1@example.com'),
('user2', 'user2@example.com'),
('user3', 'user3@example.com'),
...
('user10', 'user10@example.com');
LLM 能够理解 SQL 命令并用于查询 PostgreSQL 数据库。当 LLM 收到涉及获取用户数据的请求时,它可以生成一个 SQL 查询来从数据库中获取所需的数据。
例如,如果你要求 LLM 发送一封电子邮件给 user10
,LLM 可以生成如下查询:
SELECT email FROM users WHERE username=’user10';
这使它能够从 users
表中获取 user10
的电子邮件地址。LLM 然后可以使用这个电子邮件地址指示 FastAPI 服务发送电子邮件。
在下一部分,我们将指导你实现发送电子邮件的 FastAPI 服务。
创建 FastAPI 电子邮件服务
我们的第二个组件是 FastAPI 服务。该服务将模拟发送电子邮件的过程。这是一个直接的 API,接收包含收件人姓名、电子邮件和电子邮件正文的 POST 请求。它将返回一个确认电子邮件已发送的响应。我们将再次使用 Docker 确保我们的服务是隔离和可重复的。
首先,你需要安装 Docker(如果尚未安装)。然后,创建一个新的目录用于你的 FastAPI 服务并进入该目录。在这里,创建一个新的 Python 文件(例如,main.py
)并添加以下代码:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class User(BaseModel):
name: str
email: str
body: str
@app.post("/send_email")
async def send_email(user: User):
return {
"message": f"Email successfully sent to {user.name} with email {user.email}. Email body:\n\n{user.body}"
}
这段代码定义了一个 FastAPI 应用程序,包含一个单独的端点 /send_email/
。该端点接受 POST 请求,并期望一个包含收件人姓名、电子邮件和电子邮件正文的 JSON 数据。
接下来,在相同的目录中创建一个 Dockerfile,内容如下:
FROM python:3.9-slim-buster
WORKDIR /app
ADD . /app
RUN pip install --no-cache-dir fastapi uvicorn
EXPOSE 1000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "1000"]
这个 Dockerfile 指示 Docker 创建一个基于 python:3.9-slim-buster
镜像的镜像,这是一个理想的轻量级镜像,用于高效运行 Python 应用程序。然后,它将我们的 main.py
文件复制到镜像中的 /app/
目录下。
你可以使用以下命令构建 Docker 镜像:
docker build -t fastapi_email_service .
然后运行它:
docker run -d -p 1000:1000 fastapi_email_service
LLM 使用 POST 请求与 FastAPI 服务进行交互。当 LLM 决定发送电子邮件时,它生成对 send_email
函数的函数调用。此函数调用的参数包含名称、电子邮件和电子邮件正文。
函数调用由我们的 Python 脚本处理,该脚本提取函数参数,并用它们向我们的 FastAPI 服务发送 POST 请求。FastAPI 服务回应一个消息,指示电子邮件已成功发送。
现在,我们已经有了管道的所有组件。下一部分将把所有内容串联在一起,解释 LLM 如何协调 PostgreSQL 数据库和 FastAPI 服务之间的交互以发送电子邮件。
与 OpenAI LLM 集成
我们管道的最后一部分是 OpenAI LLM 集成。LLM 作为协调者,解释我们的命令,查询数据库以获取用户信息,并指示 FastAPI 服务发送电子邮件。
我们的脚本使用 OpenAI 的 API 进行基于聊天的完成。每个完成请求由一系列消息和可选的函数规格列表组成,模型可以调用这些函数。我们以用户消息开始对话,这为助手提供了提示。
这是我们用来向 API 发送请求的 chat_completion_request
函数:
@retry(wait=wait_random_exponential(min=1, max=40), stop=stop_after_attempt(3))
def chat_completion_request(messages, functions=None, model=GPT_MODEL):
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer " + openai.api_key,
}
json_data = {"model": model, "messages": messages}
if functions is not None:
json_data.update({"functions": functions})
response = requests.post(
"https://api.openai.com/v1/chat/completions",
headers=headers,
json=json_data,
)
return response
我们使用 Chat
类来管理对话历史。它有方法可以将新消息添加到历史记录中,并显示整个对话:
class Chat:
def __init__(self):
self.conversation_history = []
def add_prompt(self, role, content):
message = {"role": role, "content": content}
self.conversation_history.append(message)
def display_conversation(self):
for message in self.conversation_history:
print(f"{message['role']}: {message['content']}")
在我们的使用案例中,LLM 需要与我们的 PostgreSQL 数据库和 FastAPI 服务进行交互。我们定义这些函数并将其包含在我们的完成请求中。这是我们如何定义 sql_query_email
和 send_email
函数的:
functions = [
{
"name": "send_email",
"description": "Send a new email",
"parameters": {
"type": "object",
"properties": {
"to": {
"type": "string",
"description": "The destination email.",
},
"name": {
"type": "string",
"description": "The name of the person that will receive the email.",
},
"body": {
"type": "string",
"description": "The body of the email.",
},
},
"required": ["to", "name", "body"],
},
},
{
"name": "sql_query_email",
"description": "SQL query to get user emails",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The query to get users emails.",
},
},
"required": ["query"],
},
},
]
当我们发出完成请求时,LLM 以其预期的操作作出响应。如果响应包括函数调用,我们的脚本会执行该函数。例如,如果 LLM 决定调用 sql_query_email
函数,我们的脚本会从数据库中检索用户的电子邮件,然后将结果添加到对话历史记录中。当调用 send_email
函数时,我们的脚本会使用 FastAPI 服务发送电子邮件。
我们脚本的主循环检查 LLM 响应中的函数调用并相应地采取行动:
chat = Chat()
chat.add_prompt("user", "Send an email to user10 saying that he needs to pay the monthly subscription fee.")
result_query = ''
for i in range(2):
chat_response = chat_completion_request(
chat.conversation_history,
functions=functions
)
response_content = chat_response.json()['choices'][0]['message']
if 'function_call' in response_content:
if response_content['function_call']['name'] == 'send_email':
res = json.loads(response_content['function_call']['arguments'])
send_email(res['name'], res['to'], res['body'])
break
elif response_content['function_call']['name'] == 'sql_query_email':
result_query = query_db(json.loads(response_content['function_call']['arguments'])['query'])
chat.add_prompt('user', str(result_query))
else:
chat.add_prompt('assistant', response_content['content'])
当我们运行脚本时,我们得到以下输出:
{
"message": "Email successfully sent to User 10 with email user10@example.com.",
"Email body": "\n\nDear User 10, \n\nThis is a reminder that your monthly subscription fee is due. Please make the payment as soon as possible to ensure uninterrupted service. Thank you for your cooperation. \n\nBest regards, \nYour Subscription Service Team"
}
让我们解析一下我们得到这个输出的过程。我们的提示是 “给 user10 发送一封邮件,说明他需要支付每月订阅费。” 请注意,我们的消息中没有关于 user10
的电子邮件信息。LLM 识别到缺失的信息,并明白我们的函数 query_email
可以从数据库中获取该用户的电子邮件。在获取电子邮件后,它再次正确地完成了两件事:首先,它生成了电子邮件的正文,其次,它调用 send_email
函数来触发通过 FastAPI 邮件服务发送电子邮件。
结论
本文通过实施一个案例研究探讨了函数调用功能,其中 LLM 协调了一个涉及 PostgreSQL 数据库和 FastAPI 电子邮件服务的管道。LLM 成功地完成了从数据库中检索用户电子邮件并指示电子邮件服务发送个性化消息的任务,所有这些都是对单一提示的响应。
在 AI 模型中,函数调用的影响可能是巨大的,开启了自动化和简化流程的新可能性。数据管道可能会从静态和工程密集型的状态转变为动态实体,使得非技术用户能够通过自然语言快速获取最新数据。
关于我
连续创业者和 AI 领域的领军人物。我为企业开发 AI 产品,并投资于专注于 AI 的初创公司。
创始人 @ ZAAI | LinkedIn | X/Twitter
大语言模型编年史:探索 NLP 前沿
本文属于“大语言模型编年史:探索 NLP 前沿”系列文章的第一篇,这是一个新的每周系列,将探讨如何利用大型模型的力量来处理各种 NLP 任务。通过深入研究这些前沿技术,我们旨在赋能开发者、研究人员和爱好者,利用 NLP 的潜力,解锁新的可能性。
迄今为止发布的文章:
检索增强生成的力量:Base LLM 与 RAG LLMs 的比较,基于 Llama2
深入探讨使用 RAG 方法定制预训练 LLM 以适应特定使用案例,涉及 LangChain 和 Hugging Face 集成
·发表于 Towards Data Science ·阅读时间 12 分钟·2023 年 11 月 29 日
–
本文由 Rafael Guedes 共同撰写。
介绍
自 2022 年 11 月 ChatGPT 发布以来,大型语言模型(LLMs)因其理解和生成类似人类文本的能力,成为 AI 社区的热门话题,推动了自然语言处理(NLP)领域的边界。
LLMs 已被证明具有多样性,通过处理不同行业的不同使用案例,因为它们不局限于特定任务。它们可以适应多个领域,这使得它们对组织和研究社区具有吸引力。已经探索了许多使用 LLMs 的应用程序,例如内容生成、聊天机器人、代码生成、创意写作、虚拟助手等。
使 LLMs 非常吸引人的另一个特征是有开源选项。像 Meta 这样的公司将其预训练的 LLM(Llama2 🦙)在 Hugging Face 🤗 等存储库中提供。这些预训练的 LLM 对于每个公司的特定使用案例足够好吗?显然不够。
组织可以用自己的数据从头开始训练 LLM。但绝大多数组织(几乎所有组织)既没有所需的数据,也没有完成任务所需的计算能力。这需要拥有数万亿个标记的数据集,成千上万的 GPU,以及几个月的时间。另一个选择是使用预训练的 LLM,并将其调整为特定的使用案例。有两种主要的方法:微调和RAGs(检索增强生成)。
在本文中,我们将比较单独预训练的 Llama2 与集成在 RAG 系统中的预训练 LLama2 在回答关于 OpenAI 最新新闻的问题上的表现。我们将从解释 RAG 的工作原理及其子模块(检索器和生成器)的架构开始。最后,我们将逐步实现如何使用 LangChain 🦜️ 和 Hugging Face 构建一个适用于任何用例的 RAG 系统。
图 1:通过 RAG 方法,Llamas 变得越来越强大(图像来源:作者)
一如既往,代码可以在我们的Github上找到。
什么是检索增强生成(RAG)?
检索增强生成(RAG)是一种结合了检索器(如向量数据库或特征存储的非参数记忆)和生成器(如预训练的seq2seq变换器的参数记忆)技术的方法。它们用于提高 LLM [1] 的预测质量。它在推理时使用检索器,通过添加基于最相关文档的上下文/知识来构建更丰富的提示,以响应用户查询。
这种架构相对于传统 LLM 的优势是:
-
我们可以通过替换或添加更多文档/信息到非参数记忆中,轻松更新其知识。因此,它不需要重新训练模型。
-
它提供了对预测的可解释性,因为它允许用户检查哪些文档被检索以提供上下文,而这是我们从传统 LLM 中无法获得的。
-
它通过提供更准确和最新的信息,减少了*“幻觉”*的著名问题,这些信息通过检索器提供的文档获取。
图 2:检索增强生成(RAG)设置的示意图(图像来源:作者)
检索器——它是什么以及如何工作?
检索器的开发旨在解决问答(QA)问题,我们期望系统能够回答类似*“什么是检索增强生成?”*的问题。它通过访问包含有关主题信息的文档数据库来实现。
数据库通过将我们的文档拆分成等长的段落来填充,每个段落被表示为一系列标记。给定一个问题,系统需要遍历数据库,以找到可以更好回答问题的*“段落”*。
为了使这些系统在多个领域中有效工作,它们的数据库需要填充数百万或数十亿的文档。因此,为了能够遍历数据库寻找合适的段落,检索器需要在选择一组候选段落时非常高效。
密集段落检索器 (DPR) [2] 是作者在 [1] 中使用的检索器。它的目标是将数百万个段落索引到一个低维的连续空间,以高效地检索与特定问题最相关的前 k 个段落。
DPR 使用两个 密集编码器:
-
段落编码器将每个段落转换为一个 d 维向量,并使用 FAISS [3] 对它们进行索引。FAISS 是一个用于密集向量相似性搜索的开源库,可以应用于数十亿个向量。
-
问题编码器将输入问题转换为一个 d 维向量,然后使用 FAISS 检索与问题向量最接近的 k 个段落。向量之间的相似性可以通过它们之间的点积来计算。
-
DPR 使用的编码器架构是一个 BERT [4] 网络,它将输入转换为高维向量。然而,只要符合我们的用例,我们可以使用任何架构。
图 3:RAG 过程的概述,使用一个预训练的检索器,该检索器结合了查询编码器、文档索引和一个预训练生成器(seq2seq 模型)以预测自由文本形式的输出 (source)。
生成器 — 它是什么,如何工作?
生成器是一个 LLM,负责根据特定输入生成文本,通常被称为提示。
LLM 是主要由两种层组成的变换器模型 [5]:
-
全连接前馈网络 (FFN) 通过线性和非线性变换将一个嵌入向量映射到一个新的嵌入向量。
-
注意力层旨在选择哪些输入嵌入部分对当前任务更有用,产生一个新的嵌入向量。
BART [6] 是作者在 [1] 中为生成器选择的 LLM,它是一个序列到序列模型,具有以下架构 [7]:
-
编码器接收输入嵌入,并通过其六层(包括两个子层:多头自注意力机制和 FFN)生成一个 512 维的向量作为解码器的输出。
-
解码器遵循与编码器相同的逻辑,具有六层和两个子层,用于之前生成的输出。它还有一个额外的第三个子层,执行对编码器输出的多头注意力机制。
-
解码器输出接着传递到一个线性层,然后是一个 softmax 层,该层将预测下一个词的可能性。
如前一节所述,BART 不需要作为生成器使用。随着该领域的发展,特别是自 2022 年 11 月 chatGPT 发布以来,我们可以使用任何符合我们需求的架构。例如,可以使用开源方法如Llama2 或Falcon。
图 4:变压器的一般架构。它只在激活函数上与 BART 的架构不同,激活函数是 GeLUs 而不是 ReLUs (source)。
如何使用 LangChain 🦜️和 HuggingFace 🤗实现 RAG?
本节描述了如何使用 LangChain 创建您的 RAG。LangChain 是一个框架,用于轻松开发由 LLMs 驱动的应用程序,而 HuggingFace 是一个提供开源 LLMs 和数据集用于研究和商业用途的平台。
在我们的案例中,正如介绍中所述,我们创建了一个 RAG,其中生成器是一个 Llama2 模型,以便将其输出的质量与基础 Llama2 进行比较。我们将使 Llama2 回答问题*“OpenAI 的 CEO 发生了什么?”*。
该过程从加载 HuggingFace 的数据集新闻(cnn_dailymail — apache 2.0 许可证)开始,并通过 Luis 最近在 X/Twitter 上关于该主题的帖子,包括 CEO 辞职,补充了有关 OpenAI 的最新新闻。然后,我们通过创建一个文档列表(LangChain 期望的格式)来预处理它,以填充我们的向量数据库。
from langchain.docstore.document import Document
from langchain.document_loaders import HuggingFaceDatasetLoader
# Get some open ai news to add to the final dataset
openai_news = [
"2023-11-22 - Sam Altman returns to OpenAl as CEO with a new initial board of Bret Taylor (Chair), Larry Summers, and Adam D'Angelo.",
"2023-11-21 - Ilya and the board's decision to fire Sam from OpenAI caught everyone off guard, with no prior information shared.",
"2023-11-21 - In a swift response, Sam was welcomed into Microsoft by Satya Nadella himself.",
"2023-11-21 - Meanwhile, a staggering 500+ OpenAI employees made a bold move, confronting the board with a letter: either step down or they will defect to Sam's new team at Microsoft.",
"2023-11-21 - In a jaw-dropping twist, Ilya, integral to Sam's firing, also put his name on that very letter. Talk about an unexpected turn of events!",
"2023-11-20 - BREAKING: Sam Altman and Greg Brockman Join Microsoft, Emmett Shear Appointed CEO of OpenAI",
"2023-11-20 - Microsoft CEO Satya Nadella announced a major shift in their partnership with OpenAI. Sam Altman and Greg Brockman, key figures at OpenAI, are now joining Microsoft to lead a new AI research team. This move marks a significant collaboration and potential for AI advancements. Additionally, Emmett Shear, former CEO of Twitch, has been appointed as the new CEO of OpenAI, signaling a new chapter in AI leadership and innovation.",
"2023-11-20 - Leadership Shakeup at OpenAI - Sam Altman Steps Down!",
"2023-11-20 - Just a few days after presenting at OpenAI's DevDay, CEO Sam Altman has unexpectedly departed from the company, and Mira Murati, CTO of the company, steps in as Interim CEO. This is a huge surprise and speaks volumes about the dynamic shifts in tech leadership today.",
"""2023-11-20 - What's Happening at OpenAI?
- Sam Altman, the face of OpenAI, is leaving not just the CEO role but also the board of directors.
- Mira Murati, an integral part of OpenAI's journey and a tech visionary, is taking the helm as interim CEO.
- The board is now on a quest to find a permanent successor.""",
"2023-11-20 - The transition raises questions about the future direction of OpenAI, especially after the board's statement about losing confidence in Altman's leadership.",
"""2023-11-20 - With a board consisting of AI and tech experts like Ilya Sutskever, Adam D'Angelo, Tasha McCauley, and Helen Toner, OpenAI is poised to continue its mission. Can they do it without Sam?
- Greg Brockman, stepping down as chairman, will still play a crucial role, reporting to the new CEO."""
]
# load dataset with some news
loader = HuggingFaceDatasetLoader("cnn_dailymail", "highlights", name='3.0.0')
docs = loader.load()[:10000] # get a sample of news
# add openai news to our list of docs
docs.extend([
Document(page_content=x) for x in openai_news
])
接下来,我们准备为我们的 RAG 创建这两个模块。
检索器
在检索器中,我们有两个子模块:编码器和检索器。
编码器将段落转换为 d 维的嵌入向量。为此,我们从langchain.embeddings
导入HuggingFaceEmbeddings
,并选择我们想要使用的模型来创建嵌入。
在我们的案例中,我们选择了sentence-transformers/all-MiniLM-l6-v2
,因为它创建了 384 维的向量,具有良好的质量用于计算它们之间的相似度。它在内存使用上高效且快速。您可以在这里查看有关此模型及其他模型的更多详细信息。
from langchain.embeddings import HuggingFaceEmbeddings
encoder = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-l6-v2",
model_kwargs={"device": "cpu"},
)
检索器使用langchain.text_splitter
中的CharacterTextSplitter
将文档拆分为一定长度的段落。
在我们的案例中,我们选择了 1000 的长度。我们开始时选择了 100,如文献[1]中所述,但通过一些初步实验,我们发现 1000 在我们的使用案例中能取得更好的结果。
然后我们使用编码器将段落转换为嵌入。最后,我们可以将它们存储在如FAISS
的向量存储中。从这些存储中,我们可以稍后检索出与问题最相似的前k个文档。
from langchain.text_splitter import CharacterTextSplitter
from langchain.vectorstores import FAISS
# create passages
text_splitter = CharacterTextSplitter(
chunk_size=1000,
chunk_overlap=0,
)
passages = text_splitter.split_documents(<YOUR DOCUMENTS>)
# store passages in embedding format in FAISS
db = FAISS.from_documents(passages, encoder)
# retrieve the most similar document to your question
db.similarity_search(<YOUR QUESTION>, k=4)[0].page_content
生成器
如前所述,生成文本的 LLM 是 Llama2。它使用量化技术,这是一种降低权重表示精度的技术,以最小化使用模型所需的内存。请注意,由于不存在免费的午餐,我们在内存大小和准确性之间进行了权衡。
这个过程带来了如运行 LLM 时资源需求减少等优点,但也有如量化导致性能降低等缺点。
from langchain.llms import LlamaCpp
llm = LlamaCpp(
model_path="local/path/to/your/llama",
n_ctx=1024, # context length
temperature=0.7, # argument to control how much you want your LLM to follow your prompt
)
一旦我们拥有了 LLM,就该设置提示模板了。提示工程在与 LLM 交互时是相关的,因为它可以显著影响输出。
当我们找到一个能为使用案例生成所需输出的提示时,我们可以创建一个模板。LangChain 提供了一个简单的解决方案来创建提示模板。我们首先定义提示的结构,并根据用户的查询以字典格式添加动态变量。在我们的案例中,{context}
由检索器提供,用户的{question}
则由用户提供。
from langchain import PromptTemplate
TEMPLATE = """
Use the following pieces of context to answer the question at the end taking
in consideration the dates.
{context}
Question: {question}
Answer:
"""
# create prompt template
prompt = PromptTemplate(
template=TEMPLATE, input_variables=["context", "question"]
)
现在我们已经定义了我们的 LLM 和提示模板,我们从langchain.chains
中创建一个LLMChain
。它允许我们结合多个组件以创建一个连贯的应用程序。
在我们的案例中,我们可以创建一个链条,获取用户的问题和上下文,用提示模板格式化它,然后将格式化的响应传递给 LLM 以获得答案。
from langchain.chains import LLMChain
# define the LLMChain
query_llm = LLMChain(
llm=llm, prompt=prompt, llm_kwargs={"max_tokens": 5000}
)
# get answer from llm based on context and question
query_llm.run({"context": context, "question": question})
我们现在准备使用我们的模型,并将其与基础 Llama2 模型进行比较。
结果:基础 Llama2 vs RAG Llama2
“OpenAI 的 CEO 发生了什么事?”
考虑到问题,“OpenAI 的 CEO 发生了什么事?” 我们的目标是展示在不对 LLM 进行微调并使用 RAG 系统的情况下,我们可以通过相关信息轻松改进提示,使 LLM 在回答时更加准确和更新。相反,我们期望基础的 Llama2 提供准确的答案,但由于其训练数据未包含上周的信息,因此可能过时。
基础 Llama2:
OpenAI 是一个非营利的人工智能研究组织,总部位于加利福尼亚州旧金山,专注于开发友好的 AI 并确保其安全使用。OpenAI 的 CEO 是 Sam Altman,自 2010 年与 Elon Musk、Greg Brockman、Ilya Sutskever 和 Vince Conitzer 共同创立公司以来,他一直担任此职务。就最近的新闻而言,关于 CEO 或 OpenAI 本身没有重大进展,但已知 OpenAI 最近通过招聘额外的 15 名员工扩大了团队,以从事与 AI 相关的各种项目,包括新模型和机器学习工具的开发。
RAG Llama2:
Sam Altman 于 2023 年 11 月 20 日辞去了 OpenAI 首席执行官职务。他被 Mira Murati 取代,后者担任了临时首席执行官。然而,Sam Altman 在几天后以首席执行官身份回到公司,新的初始董事会成员包括 Bret Taylor(主席)、Larry Summers 和 Adam D’Angelo。
正如我们从上述示例中看到的,RAG Llama 能够提供带有更新信息的答案,而无需任何额外的微调过程,这种过程既昂贵又耗时。
结论
RAGs 为组织提供了比为每一个使用案例微调 LLMs 更快且更实惠的部署 LLMs 的可能性。
正如我们在使用案例中看到的,将关于 OpenAI 及其首席执行官上周丑闻的十二篇文档添加到我们从 HuggingFace 获取的 10k 新闻集合中就足够了。我们的检索器能够为我们的生成器创建足够的上下文,从而生成关于该主题的更准确且更新的答案。
在访问外部信息方面,RAGs 是一个很好的选择,因为它们通过在生成响应之前从知识来源中检索相关信息来增强 LLMs 的能力。然而,当涉及到调整 LLM 行为以适应特定的写作风格时,使用不常见的单词或表达式,结合使用可能更为合适。
关于我
连续创业者和 AI 领域的领导者。我开发 AI 产品以服务于企业,并投资于以 AI 为重点的初创公司。
创始人 @ ZAAI | LinkedIn | X/Twitter
参考文献
[1] Patrick Lewis, Ethan Perez, Aleksandra Piktus, Fabio Petroni, Vladimir Karpukhin, Naman Goyal, Heinrich Küttler, Mike Lewis, Wen-tau Yih, Tim Rocktäschel, Sebastian Riedel, Douwe Kiela. 适用于知识密集型 NLP 任务的检索增强生成。arXiv:2005.11401, 2021
[2] Vladimir Karpukhin, Barlas Oguz, Sewon Min, Ledell Wu, Sergey Edunov, Danqi Chen, 和 Wen-tau Yih. 用于开放域问答的密集通道检索。arXiv:2004.04906, 2020
[3] Jeff Johnson, Matthijs Douze, 和 Hervé Jégou. 基于 GPU 的亿规模相似性搜索。arXiv:1702.08734, 2017
[4] Jacob Devlin, Ming-Wei Chang, Kenton Lee, 和 Kristina Toutanova. 2019. BERT: 深度双向变换器的预训练用于语言理解。arXiv:1810.04805, 2019
[5] Michael R. Douglas. 大型语言模型。arXiv:2307.05782, 2023.
[6] Mike Lewis, Yinhan Liu, Naman Goyal, Marjan Ghazvininejad, Abdelrahman Mohamed, Omer Levy, Ves Stoyanov, Luke Zettlemoyer. BART: 用于自然语言生成、翻译和理解的去噪序列到序列预训练。arXiv:1910.13461, 2019
[7] Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N. Gomez, Lukasz Kaiser, Illia Polosukhin. 注意力机制是唯一需要的。arXiv:1706.03762, 2017.
点积在人工智能中的力量
原文:
towardsdatascience.com/the-power-of-the-dot-product-in-artificial-intelligence-c002331e1829
简单工具如何产生惊人的复杂性
·发表于Towards Data Science ·阅读时间 9 分钟·2023 年 5 月 15 日
–
将简单的事物放大以实现更复杂的目标是一个强大的理念,这一理念奠定了生命、计算机(基于Turing 机的简单性)以及深度学习技术(基于神经网络中神经元的理念)的基础。
更多往往不同,我们继续观察到随着最先进的架构不断扩展(像GPT这样的语言模型正在改变世界),这导致了更好的泛化和令人印象深刻的结果。
研究人员声称,大型语言模型展示了涌现能力,这些能力无需过多的架构创新,而(可以说)主要来自计算资源的增加和大规模重复简单操作(有关警告,请参见这篇文章)。
尽管训练深度学习模型需要社区多年积累的独创性和技术,但大多数深度学习方法的基本构建块仍然相当简单,仅由少量的数学操作组成。
也许最重要的是点积。在本文的其余部分,我想深入探讨点积的作用、它为何如此重要,以及为何将其规模化已实现令所有与之接触的人感到惊讶的人工智能水平。
人工智能作为点的产物(DALL-E 可能有点误解了我,但它仍然看起来很酷)。
想象一下你在像 Netflix 这样的公司工作,并被分配了选择电影以推荐给数百万不同用户的任务。每个用户都有自己独特的口味,而电影的选择有数百万部,因此这是一项相当艰巨的任务。
解决这个问题的一种方法是列出电影的几个不同属性,例如电影与特定类型(例如评估它是动作片还是情感戏剧)的对齐程度。还可以列出其他信息,例如喜剧内容、一些外部因素,如电影长度、是否由 Marvel 或 Disney 制作等。然后将所有电影的属性列表放入一个向量中。
了解用户口味的一个简单方法是直接查看该用户最喜欢的电影或最近观看的几部电影,这将产生第二个向量,从而给我们提供用户口味的一个概念。
然后可以通过计算新电影与用户最喜欢的电影的相似度来简单地推荐新电影,这些相似度是根据电影属性的向量空间中的某种相似度度量来计算的。
这就是点积,AI 最喜爱的相似度度量,登场的地方。
点积是一个简单的数学运算,用于测量两个向量之间的相似度。数学上,两个向量 x 和 y 的点积计算公式为:
x · y = Σ (x_i * y_i) for i = 1 to n
其中 x_i
和 y_i
分别是向量 x 和 y 的分量,n 是向量的维度。它也可以写作:
x · y = |x| * |y| * cos(θ)
其中θ是两个向量之间的角度,||表示它们的长度。在这种解释下,点积测量了 x 和 y 的对齐程度。如果两个向量平行,则点积对应于它们的长度。如果向量彼此垂直,则点积为零。
BenFrantzDale 在英文维基百科,CC BY-SA 3.0
点积通常与余弦相似度结合使用,后者通过向量的大小对点积进行标准化,提供一个对大小不变的相似度度量:
cosine_similarity(y, x) = (y · x) / (|y| * |x|)
从几何上讲,点积可以被视为一个向量在另一个向量上的投影。当你计算两个向量 x 和 y 的点积时,可以把它看作是将向量 x 投影到向量 y(或反之),然后将投影的长度乘以向量 y 的长度。
投影是一个重要概念,并且有一个很好的解释:它们可以被认为是向量所编码的不同特征的维度比较。
回到之前的例子,考虑两部电影,电影 A 和电影 B。为了简化起见,假设这两部电影通过两个不同的属性来表征:动作强度(AI)和情感深度(ED)(通常,这些向量当然可以大得多)。向量的第一个组件代表动作强度,第二个组件代表情感深度。假设这两个属性的测量范围是-5 到 5,其中-5 表示最不强烈或最不深刻,5 表示最强烈或最深刻。
一部电影,一部相对浅显的爆米花娱乐动作片,其动作强度为 4,情感深度为-3。它的向量表示为 A = [4, -3]。
电影 B 是一部 4 小时的黑白冥想片,关于塞尔维亚政府,从鸽子的视角看待。它在情感上相当要求,但没有很多动作场景,评分为 B = [-4, 4]。
我们可以通过点积来比较这两部电影,计算结果为
A · B = (4 * -4) + (-3 * 4)= -28
通过两向量的大小进行归一化,这得到了
-28/(sqrt(25)*sqrt(32))= -0.99。
正如预期的那样,这两部电影的相似度极低,因此它们的余弦(不)相似性得分接近-1。
那么,为什么这个简单的比较两个向量相似性的操作在 AI 中如此重要?
机器学习,也许更广泛地说,(人工)智能的核心,依赖于在庞大的规模上比较模式并衡量其相似性。
然而,提出用于比较的模式并非易事。
我在之前的示例中略过的一部分是如何为电影制定标签。动作强度和情感深度并不是从电影的真实数据中直接提取的,而是由人类评估得出的,他们进行复杂的认知(主观)任务——观看电影并将其转化为明显低维的表示,从中可以看出这些电影的差异。从电影的像素空间的角度来看(对于一部 4K 的 2 小时电影,这些像素大约是 1.4*10¹²),这是一种巨大的维度降低,其中包含个人的偏见和先验知识。
复杂非线性变换的概念是使点积强大的第二个重要因素。它将输入转换为在相应学习问题的背景下比较其相似性变得有意义的表示,例如分类任务或回归问题。
将非线性变换与点积相结合是神经网络核心功能的一部分。在这里,点积用于计算每个神经元中输入的加权和,这与非线性变换(激活函数,如 tanh 或 ReLU 函数,加上网络的学习权重)结合,构成了所有神经网络运行的基本机制。
这种组合在核函数的背景下变得更加清晰。
核函数使我们能够比较新变换空间中点的相似性。它通过计算在(通常是更高维度的)投影空间中变换后的数据点之间的点积来实现这一点。巧妙之处在于,“核技巧”允许在不显式计算变换的情况下进行这一计算,这可以节省大量资源,并在支持向量机(SVMs)中找到应用,结合核技巧,构成了近年来最强大和最具影响力的算法之一。
核机器结合了非线性变换,以找到一个特征空间,在这个空间中类别标签容易被分开。原作者:Alisneaky Vector: Zirguezi, CC BY-SA 4.0
这些基本见解可以与当前市场上的最热门模型连接起来:大型语言模型。
大型语言模型的一个标准任务是翻译两种语言之间的句子,比如在英语和德语之间:
“我正在阅读一篇关于点积在人工智能中重要性的文章。”
“Ich lese einen Artikel über die Bedeutung des Skalarproduktes für die KI。”
两个句子的含义大致相同,但它们的表示方式却显著不同。
翻译任务可以被表述为找到单词的非线性变换,这种变换对应于潜在语义空间中大致相同的位置,从而捕捉它们的“意义”。翻译的质量可以通过实现的相似度来衡量。
如果“测量相似性”还没有让你产生强烈的兴趣,那么我在这篇文章中的表现还不够好。
确实,点积在变压器模型的核心中出现,这些模型已经成为现代自然语言处理(NLP)以及许多其他机器学习任务的基础。
自注意力机制是变压器模型的一个关键组成部分。自注意力机制使模型能够权衡不同输入元素之间的重要性。这使得模型能够捕捉数据中的长程依赖关系和复杂关系。在自注意力机制中,点积被用来计算注意力得分和形成上下文感知的输入元素表示。
输入元素(通常是输入文本的嵌入/分词版本)首先被线性投影到三个不同的空间:查询(Q)、键(K)和值(V),使用各自的学习权重矩阵。这会为每个输入元素生成三组向量:查询向量、键向量和值向量。
点积随后用于计算每对查询向量和键向量之间的注意力分数(score_ij = q_i · k_j)。
这衡量了查询向量和键向量之间的相似性,决定了模型对每个输入元素相对于其他所有元素的关注程度。
在计算所有相似性分数后,分数会被缩放并通过一个相似性函数,然后可以用来计算上下文函数,该函数又是注意力分数和值的简单总和:(context_i = Σ (attention_ij * v_j))
通常选择的相似性函数是 softmax,可以看作是一种核函数,它是一种非线性变换,可以在元素之间进行比较,并估计哪些元素可能对预测有用。根据具体问题,也可以使用其他核函数。从更根本的角度来看,变换器可以被视为核机器(更准确地说是深度无限维非墨瑟二元核机器,如本文讨论)。
与其他示例一样,点积与输入和输出文本的非线性变换及投影结合,定义了自注意力机制。
我希望能够说服你,点积在人工智能中扮演了关键角色,尤其是在其最新的实例中。虽然在某些特定背景下,其他向量之间的距离/相似性度量(如欧几里得距离、切比雪夫距离、马哈拉诺比斯距离等)可能更为合适,但点积可能是最广泛使用的,并且构成了量化数据中相似性、关系和依赖性的基础构件。当与非线性变换结合时,点积处于各种算法和模型的核心,从支持向量机到神经网络,再到 Transformers 中的自注意力机制。
随着人工智能近年来取得的显著进展,我发现反思构成其基础的操作的简单性和适应性非常有趣。
有时看到这种策略的有效性可能有些令人害怕:在最近的播客中,Max Tegmark 评论了训练大型语言模型背后令人惊讶的简单性/愚蠢性(只需给它们大量文本,并使用自注意力的变换器预测下一个单词/句子)。在某些方面,智能可能比我们想象的更简单,正如大多数与 ChatGPT 互动过的人所确认的那样。
这具有重要的意义:计算机非常擅长以可靠和快速的方式执行一些非常简单的任务,并将其扩展到 Nth 级别。随着摩尔定律的承诺,我们将继续扩展这些模型。
这些是令人着迷且可能危险的时期,很可能点积将位于其中的核心。
应对 AI 风险的实际侧面
·
关注 在 Towards Data Science 发布 · 发送至 新闻通讯 · 3 分钟阅读 · 2023 年 9 月 14 日
–
在过去几年中众多激动人心的 AI 创新背后,我们发现了一系列已知和新兴的风险:算法偏见、隐私问题和版权侵犯等。这还不包括我们甚至尚未开始接触的宏观级社会问题,比如未来不久可能会有数百万个职位变得过时的机会。
数据和机器学习专业人士一直在努力提高对这些问题的认识,并提出可行的解决方案,旨在平衡技术进步与公平和负责任的实践。尽管现在可能还难以判断他们——以及我们所有人——在这一细微问题上的成功程度,但如果我们希望在我们的专业社区(及其他领域)中产生积极变化,了解这些讨论的轮廓仍然至关重要。
本周的亮点文章以清晰和务实的方式处理了关于人工智能的棘手问题——从监管到技术规范。不论你是对这一话题陌生还是已有一定了解,我们认为这些文章都值得你花时间阅读。
-
生成式人工智能的法律和伦理视角 对于生成式人工智能工具带来的复杂问题,奥利维亚·塔努维贾贾 的最新概述提供了易于理解的入门介绍:它提供了足够的细节以帮助你了解这一复杂话题,并提供了有用的资源,帮助你扩展对自己最关心领域的知识。
-
反对人工智能监管的理由毫无意义 欧洲联盟的人工智能法案常被誉为迄今为止最严肃的尝试来监管人工智能产品的开发和实施;阿德里安·布克 详细解读了其最显著的特征,反思了可能存在的不足,并倡导更多司法管辖区认真而主动地考虑类似的立法倡议。
图片由 mostafa meraji 提供,来自 Unsplash
-
下一步是负责任的人工智能。我们如何实现这一目标? 为了实际应用负责任和伦理的人工智能,埃尔多安·塔斯克森 提出了一个 6 步路线图,团队和组织可以根据自身需求进行调整。这提醒我们,个人从业者有能力主动影响实践和决策,从而在构建基于机器学习的产品过程中发挥作用。
-
OpenAI 的网络爬虫和 FTC 的失误围绕版权、艺术作品以及 LLMs 和图像生成模型的训练方式的争论从未如此激烈。Viggy Balagopalakrishnan通过关注 OpenAI 的最新新闻和 FTC(联邦贸易委员会)在监管资金充足的科技公司方面面临的挑战,提供了当前僵局的有用快照。
-
用护栏保护 LLMs
在微观地方控制 AI 工具的影响范围、领域和效果也很重要:例如,如果你正在进行大语言模型的集成,你肯定不希望它出现冒犯性语言或坚称幻觉是事实。Aparna Dhinakaran和 Hakan Tekgul 分享了一份实用指南,介绍了允许开发者对模型输出实施严格参数的开源工具。
想找其他主题的精彩读物?这些顶尖文章绝对不会让你失望:
-
如果你刚刚踏入环境数据科学,Caroline Arnold的易懂介绍是一个很好的起点。
-
机器学习如何从编码演变为嵌入?Mina Ghashami的解读既全面又适合初学者。
-
如果你想了解变压器的战略能力的有趣且启发性的教程,不要错过Charlie O’Neill关于构建井字棋模拟器的详细介绍。
-
为了帮助你解读职位描述及其实际内容在现实生活中的应用,Stephanie Kirmer概述了一些数据科学角色的关键类型。
-
想要深入了解数学?你绝对要收藏Gabriel de Longeaux关于两封信封悖论的详细探讨。
-
以实际操作的角度结束,我们强烈推荐Mariya Mansurova最近的使用 BERTopic 的逐步指南用于高级主题建模。
感谢您支持我们作者的工作!如果您喜欢在 TDS 上阅读的文章,请考虑成为 Medium 会员——这将解锁我们整个档案(以及 Medium 上的所有其他文章)。
对早期排序阶段的原则性方法
一种系统化的方法用于设计和评估推荐系统中的候选生成和早期排序阶段,并对核心指导原则进行深入分析。
·
关注 发表在 Towards Data Science ·9 分钟阅读·2023 年 12 月 6 日
–
众所周知,在推荐系统中,构建推荐有几个阶段:首先是候选生成,也常被称为检索,其次是一个或多个排序阶段。学术论文对早期阶段关注不多,但在实际应用中,这些阶段非常重要。如何衡量它们的质量也同样重要。
作者插图
候选生成通常是由不同来源的组合来组织的:
-
最受欢迎的项目,
-
类似于用户的历史记录,
-
在不同层次上结合之前的方法:例如,从用户的历史记录(或 ANN,或热门项目)中提取类别,然后从中选择流行的项目。
尽管每种方法本身可能不复杂,但整体组合却相当复杂,促使人们思考:如何优化它?要做到这一点,当然需要定义究竟需要优化什么,即应该使用什么指标来衡量候选生成的质量。
尽管我们的讨论集中在候选生成阶段,但值得注意的是,这些原则同样适用于所有早期排名阶段,因为它们也为后续阶段提供候选项。
有多种方法。有时质量根本没有被衡量(或仅凭目测),这一阶段也没有系统地优化。有时,会以某种方式衡量候选项的整体相关性。如果系统推荐一些异常内容,这也被认为是候选生成中的一个问题。有时,这种相关性甚至与最终阶段优化的内容进行对比。也就是说,候选项应该已经足够相关,无论如何衡量,最终排名将选择最具吸引力的(有吸引力的、可点击的等)。
有时,特别是在论文中,会使用HitRate@k、Recall@k、Precision@k、MRR、NDCG等指标,仅关注正向(相关)文档。如果用户随后与某个文档进行互动,则认为该文档是相关的。我更倾向于这种方法,但它存在显著的偏差问题,例如,用户往往会与系统推荐的项目进行更多互动。
有一段时间,我尝试制定一种不同的候选生成方法,并一直支持它。幸运的是,我不是唯一的支持者 — 这种方法已在各种系统中使用(例如,这篇关于扩展 Instagram Explore 推荐系统的文章中详细介绍)。然而,我不确定它是否可以称为行业标准 — 确实有一些主要系统没有使用它。
该方法基于以下原则:
早期阶段的主要目标是从最终排名的角度找到最佳文档。
简而言之,目标是找到最佳的文档。最佳的定义不是基于任何相关性,而是由当前最终排序器来决定。最终由排序器选择的候选项是好的,而其他的则不然。如果排序器发生变化(且这种变化可能很频繁),则质量评估也会随之变化。
(对于多阶段排名可能会有修改:可以使用最终排名器专门评估早期阶段的质量,或者使用下一阶段的排名器。也就是说,通过下一阶段但未通过最终阶段的候选项可以被视为负面或正面。我不确定哪种方法更好。)
尽管这种方法并不完美,我个人认为这是唯一可行的方法,意味着只有通过这种方法才能长期系统地改善所有阶段的质量,而不会遇到基本问题。至少,我不明白如何用其他方法实现这一点。
概述了这种方法的基本原则之后,现在让我们深入探讨其优缺点,首先从其潜在的缺点入手。
方法的缺点
-
整个候选生成过程,包括其质量如何衡量,开始显著依赖当前的排名方法。这增加了复杂性,在进行比较时考虑这一点是很重要的。当排名器发生变化时,早期阶段需要重新训练。
-
大多数情况下,系统最初是在不遵循这一原则的情况下构建的。将系统从另一种状态转变为遵循这一原则可能非常具有挑战性。特别是,如果系统的排名相当低(但由于各种技巧的存在,推荐结果是可接受的),那么遵循这一原则将不会改善系统,反而可能在当前显著恶化推荐结果。
-
这一原则假设排名器应在整个文档库中运行良好。否则,如果有质量低下的文档被错误地推荐,那么在试图取悦排名器时,候选生成过程最终会发现它们也被推荐出去。与排名器只操作一组已经相当不错的候选项相比,这使得排名器的训练变得更加复杂。
-
候选生成并不试图改善服务的端到端指标。根据这一原则可以改善它,但最终可能以失败告终。(然而,这恰好表明排名存在问题,比如错误的目标。)这增加了工作的复杂性:你不断改进,但最终却无法部署它。
-
对于产品规则的支持有限。这一原则规定,除了硬规则之外的所有规则应在最后阶段应用,早期阶段将会适应它们。这不仅涉及到技巧,还包括改进推荐系统各个方面的合理方法,如探索、多样性等。你必须提供多样化的候选项,因为排名器会选择它们。
从黑客到和谐:在推荐系统中构建产品规则
不要让启发式方法削弱你的机器学习,学会将它们结合起来
towardsdatascience.com
在探讨了局限性之后,让我们现在转向这种方法的优势。
这种方法的优点
-
这一原则基于分解。它为早期阶段提供了更清晰且可测量的目标,显著简化了系统。推荐的目标和损失的选择复杂性集中在排名阶段(这是无法避免的一个方面),而早期阶段主要集中在有效找到顶级候选者的功利任务上。因此,早期阶段仅作为加快排名过程的工具。
-
在这一原则中,没有根本性的限制。如果想象一个理想的推荐系统,什么也不会阻止它以这种方式构建。(对于其他方法,这一点不能成立——完美的推荐并不一定能够准确猜测用户将与之互动的内容!)随着排名的改善,这种简化的候选生成指标会越来越接近端到端指标。这类似于某些圈子里广为人知的迭代方法:‘改进指标——基于这些指标改进产品’。
-
不同的排名阶段彼此对齐;它们不会试图优化不同的内容。在系统中,如果不是这种情况,例如,如果你将候选者的总数翻倍,那么系统的整体质量可能不会提高,甚至可能会下降。例如,如果早期阶段优化某种相关性,那么额外的候选者可能会较少相关,从而整体相关性下降(尽管点击率可能会增加)。
-
由于分解的观点:早期阶段的衡量(因此优化)要容易得多。评估过程将在下节中讨论。训练基本上归结为提炼排名模型。(尽管有一些细微差别。例如,记录一些没有进入排名前列的候选者是比较好的。)
-
此外,对于训练和测量早期阶段,我们不再需要用户,这意味着不必在他们身上推出新方法。例如,可以使用爬虫技术,正如我们稍后将讨论的,通过向排名服务发送一系列使用新候选者的请求。
衡量候选生成
现在,让我们深入探讨文章中最实际的部分,讨论如何根据我们之前概述的原则在实践中测量候选生成(或排序的早期阶段)的质量。
首先,让我们检查一个简化但非常重要的情况:当排序完全基于一个最终模型的分数时。在这种情况下,我们可以简单地比较这个模型对于两组候选的平均分数。如果一种方法发现最终模型为其分配的预测高于另一种方法,那么第一种方法更好。
是否取整个输出的平均预测,仅取前几个位置,还是按位置递减(类似于IDCG——NDCG中的分母)似乎不是非常关键。可以根据个人喜好选择这些选项中的任何一个。
不过,需要考虑一个技术细节。如果这样的指标是离线测量的,需要能够在自定义候选项上运行排序(或整个推荐系统)。这可以通过在历史查询上进行模拟(离线重放——即试图回顾性地重现所有实体的所有信息)或通过抓取(如前所述,向推荐服务发送大量新查询,以便它使用感兴趣的候选生成方法)来完成。在这两种情况下,都会为相同查询的不同生成方法获得结果(最终模型的预测)。这对指标的敏感性是有益的。
然而,如果这个指标是在生产服务中在线测量的,那么所有计算都可以简单地基于模型的日志预测进行。这要简单得多,但灵活性较差,并且比较将跨越不同的查询。指标的敏感性降低(可能是因为某些方法恰好接收到更复杂的查询)。
现在让我们转到一般情况:最终排序不仅仅是某个模型的预测,还涉及大量其他逻辑、重排序、业务规则、随机化等。当你考虑如何在这种松散的表述中比较不同的候选集(什么是好,什么是坏)时,并不是很明显。
不过,我曾经设计了一种方法,结果非常简单有效。到目前为止,我还没有看到它在其他地方提到过。
方法如下。我们向候选源列表中添加一个特殊的源,该源生成随机候选(例如,均匀分布)。我们为这个源分配一个小的固定配额(例如,50 个候选)。然后我们观察推荐的文档中有多少比例最终来自这个源。如果我们的主要候选生成足够好,那么随机候选很少能胜过它,即很少进入前列。如果它很差,那么随机候选会经常超越它。
仅用于说明目的的合成数据
当然,我们假设添加随机候选者不会显著恶化系统:大多数候选者不会被推荐,而那些被推荐的候选者不会大幅降低用户体验,并且还会为用户和排名模型增加探索(它会在这些示例上进一步训练)。如果情况并非如此,那么首先需要‘修正排名’。😉
这种方法最酷的地方在于它不仅可以作为候选生成的度量标准,还可以作为整个系统健康状况的监控工具,包括最终排名。它检查候选生成与排名的一致性(是否为排名优化)。如果排名本身因某些原因下降,那么候选者也会变得不那么适合它。我们在实践中见证了这一点,当其中一个组件发生故障时,响应中随机候选者的比例增加了。
顺便提一下,这个特殊来源的随机性可以进行调整。如果你使用的不是均匀分布,而是与文档的受欢迎程度成比例的分布,它会成为更强的‘对抗性’参与者(这也可能增加敏感性)。然而,使用均匀抽样,可以提供一个关于我们的候选生成理想的查询比例的分析估计(即,即使我们将整个数据库添加到候选者中,结果也不会改变):
在这个公式中,N 代表数据库中的候选者总数,k 是使用的随机候选者数量,R 表示至少有一个随机候选者出现在输出中的查询比例。
结论
在这一探索过程中,我们专注于推荐系统中候选生成和早期排名阶段的一个具体原则。通过全面检查其优点和挑战,并提出实用的评估方法,我们强调了这一原则作为改进这些系统的强大工具的潜力。采纳这一原则不仅简化了推荐的复杂过程,还确保了效率和有效性。随着我们继续完善和应用这一原则,它将成为推动推荐系统领域发展的有希望的方向。
现代计算机科学家的原则
原文:
towardsdatascience.com/the-principles-of-a-modern-computer-scientist-8be9e7494e7e
未来将塑造人工智能的从业者可以从当前计算机科学家这一代的成就和不足中学到什么
·发布于 Towards Data Science ·阅读时间 2 分钟·2023 年 5 月 31 日
–
Demis Hassabis 于 2022 年 11 月在 Fundación Princesa de Asturias 演讲。照片由 Fabien Girardin 提供。
也许像你一样,在过去几年中,我参加了许多关于人工智能的过去、现在和未来的活动。AI 先锋和 DeepMind 联合创始人 Demis Hassabis 是其中一个活动的演讲者,地点在西班牙的奥维耶多。那时,我对他的辉煌职业生涯和与 AlphaFold 的迷人实验知之甚少。
我视 Demis Hassabis 为现代计算机科学家的典型代表。他不仅展现了创造力和想象力,还有逻辑、抽象思维和工程技能。当前主流的技术文化受制于“决定论思维”的驱动。愿景被极端化。未来感觉是二元的,乌托邦和反乌托邦的思考同时旋转。相比之下,Demis 谈论 AI 时有着细致的考量;他意识到其他学科的贡献,并能够清晰地表达他的工作在社会与技术的共同演变中所扮演的角色。我从这次活动中获得了启发。
从我这一代计算机科学家的成就和不足中,可能可以总结出一系列原则。年轻一代现在在学校里可以将这些“现代计算机科学家的原则”作为他们未来实践的蓝图。其中一些原则可能如下所示。
现代计算机科学家重视*:
想象力优于逻辑
逻辑和抽象思维是至关重要的,但它们可能将任何挑战限制为狭窄的有限解决方案。想象力是现代计算机科学家的一个关键成分,使其能够探索最广泛的可能性。
责任重于敏捷性
在科技领域,语言中的“冲刺”、“加速器”和“敏捷方法论”将优化和快速执行神圣化。相比之下,现代计算机科学家以识别他们工作潜在后果为荣。而这需要时间和耐心。
创造力重于过程
计算机科学的实践不仅仅限于遵循结构良好的迭代过程。现代计算机科学家从多个角度看待挑战。他们首先运用创造力思考。他们在安全的环境中构建实验和原型,以创建多条选择路径。
细微差别重于信念
现如今的世界奖励说服力。现代计算机科学家受到怀疑的驱动。他们知道如何倾听和挑战信念。他们的使命是将这些信念综合成细微的结论。
- 虽然右侧的项目也有价值,但现代计算机科学家更重视左侧的项目。当然,可能还有其他原则,这些原则不应被视为普遍适用的规则。
感谢Pelayo González Arbues带我参加此次活动,祝贺Irene Díaz主持了这些鼓舞人心的演讲和对话。
AI 编程工具的到来:产品工程团队将如何使用它们
作者使用 Midjourney 制作的图像
生成式 AI 将如何影响产品工程团队 — 第二部分
·发表于 Towards Data Science ·10 分钟阅读·2023 年 7 月 26 日
–
这是一个六部分系列的第二部分,探讨了针对开发者的生成式 AI 生产力工具(如 Github Copilot、ChatGPT 和 Amazon CodeWhisperer)如何影响整个产品工程团队的结构。
在 第一部分,我们探讨了:
-
产品工程的现状以及随着生成式 AI 工具的兴起,团队是否需要更少的人工工程师的可能性。
-
传统的 5:1 比例在技术团队中的应用:在行业中,通常每位产品经理对应大约五名工程师。
-
产品经理和工程师在当前产品开发过程中的角色,以及这些角色随着 AI 进步可能发生的变化。
-
过去的研究如何对哪些职业受到 AI 影响最小的预测产生了偏差,以及大型语言模型(LLMs)如何颠覆了这些预测,特别是对技术和创意行业的影响。
AI 编程工具的爆炸性增长
自动化几乎自软件工程存在以来就已经成为其一部分。埃里克·雷蒙德的 2003 年里程碑式论文,《Unix 编程艺术》 反思了 17 条软件工程师的设计原则,其中包括 生成规则:“避免手动编程;在可能的情况下,编写程序来编写程序”。
雷蒙德的建议即使在发布 20 年后仍然适用:
“人类在关注细节方面 notoriously 表现糟糕。因此,任何形式的手动编程都是延迟和错误的丰富来源。你的程序规范越简单和抽象,人类设计者弄对的可能性就越大。生成的代码(每一层)几乎总是比手动编写的更便宜、更可靠。”
自 Raymond 写下这些话以来,我们已经开发出了自动化测试工具,linters(自动检查我们编写的代码的工具)、开发环境的自动补全(像拼写检查器,但用于代码),甚至框架(如 React 和 Django),它们自动生成大量的基础代码,用于网站和移动应用等通用应用程序。在大多数情况下,开发者对自动化赞不绝口,同时始终默默相信我们的经验、技能和创意独特性会让我们难以被替代。麦肯锡及其同行也告诉我们我们会从 AI 自动化的魔爪中安全逃脱,这一点也没有帮助。
代码自动化领域最近的补充是一组工具,它们承诺与开发者平等地并肩工作,看起来非常有能力远超我们之前的想象。就像 Raymond 的第 17 条原则一样,这些工具可能会比人类更擅长编写复杂的软件。
我写作时,开发者辅助工具中的头号明星可能是Github Copilot,它毫不含糊地自称为配对程序员(即那个坐在你旁边共同编写代码的编程伙伴)。Copilot 于 2022 年 6 月发布给开发者,而它那更年轻、更迷人的兄弟,Copilot X,则在今年 3 月宣布。Github 声称 Copilot 能够提高开发者生产力,并且他们的研究报告显示该工具将完成工程任务的时间缩短了多达 55%,这些声明看起来完全合理。
Github(别忘了这是一家由微软拥有的公司,微软还拥有 传闻中的 45% 的 ChatGPT 制造商 OpenAI 股份)并不是唯一的选择。亚马逊 CodeWhisperer 去年也宣布推出,声称能够 提高开发者生产力 57%。像 Copilot 一样,CodeWhisperer 可以在工程师的开发环境(IDE)中访问,并能够纠正、评论、解释和编写代码。
IDE 很久以来已经能够提供代码自动补全,但有了 Copilot X,它可以在很少的上下文下编写整个代码段
在一篇关于 Github 团队如何开发和改进 Copilot 的引人入胜的文章中,John Berryman,Github 的高级研究员,解释了不仅仅是开发者面前的代码被用来提示 AI 模型。Berryman 解释道,
“关键在于我们不仅要提供给模型 GitHub Copilot 用户当前正在编辑的原始文件;相反,我们会在 IDE 内寻找额外的上下文信息,以提示模型更好的完成方案。”
正是这些开发工具可以利用的更广泛的上下文——IDE、开发者机器上的文件、git 中的代码库,甚至潜在的应用程序文档——使得这些工具如此强大。这种广泛的上下文降低了开发者“提示工程师”从 AI 中获取解决方案所需的技能水平。改进提示的示例可以从它们操作的环境内直接获得,也可以从 数十亿行 预先存在的代码中获得。这使得大型语言模型不仅能生成合适的输出,还能超越普通开发者的能力。
有了 Copilot X,代码界面本身不再是 AI 唯一可以使用的调色板,也不再仅限于自动生成代码。用户可以在开发环境中突出显示代码,并简单地询问代码的作用:
Copilot X 扩展了编码助手的范围,进入了类似 ChatGPT 的聊天环境
虽然 Copilot 和 Codewhisperer 是针对开发者优化的大型语言模型的具体实现,但即使是通用的 ChatGPT 作为工程伴侣也非常方便。
我已经使用了 GPT-3.5 和 GPT-4 模型好几个月,涉及各种任务,并继续对它的能力感到困惑——无论是代码还是其他多个学科。最近,我在重新使用生疏的 Python 技能来玩弄 OpenAI 的 API 时,似乎有些不厚道地没有向 ChatGPT 请求帮助。
正如你现在所预期的,通用聊天机器人能够给我一些完全合理的设置说明,这些说明反映了在线文档,但额外的好处是我可以通过对话逐步展开并扩展到其他话题(比如为什么 Python 在我每次安装时总是在 Windows 上无法正常工作)。
即便是通用的 ChatGPT 也非常擅长在技术领域提供各种帮助。
为了不被 Copilot 和 Codewhisperer 落在后头,OpenAI 最近宣布了他们自己增强代码功能的版本——一个带有代码解释器的 GPT-4 模型,它既可以编写也可以执行 Python 代码。这改进了 ChatGPT 在相当通用的模型中的代码补全实现,实际上允许它在聊天环境中直接编写和运行代码。
带有代码解释器扩展的 ChatGPT 不仅可以编写代码,还可以运行代码。
到目前为止,我花了很多时间讨论这个领域的三大玩家:Github、Amazon 和 OpenAI,但其他公司也不甘示弱。谷歌宣布了Google Cloud 的 Duet AI,这是一个类似 Copilot 和 Codewhisperer 的代码助手,同时还在其 Colab 中提供了AI 辅助机器学习工具(实现了 Codey,这是基于他们自己 PaLM 2 模型的一系列代码模型)。
在开发者特定工具之外,微软正在推出一系列 Copilot,包括 Windows 和Office(价格令人咋舌),Salesforce 最近似乎重新品牌了他们的每一个产品,在产品名称后添加了‘GPT’。谷歌也不甘落后,预计将把 Duet 添加到 Google Workplace,这将像 Office Copilot 一样帮助人们编写文档和创建稍微少些枯燥的演示文稿。
看起来无论你往哪里看,都有人在将资金投入到使人类更高效的 AI 模型中。表面上看,早期迹象表明这一切是有效的,我们应该为其影响做好准备。
产品工程团队将如何使用这些工具?
第一次我花大量时间用 ChatGPT 生成代码时,我实际上是在要求它生成一些用户故事。一位朋友让我帮忙编写一个应用程序,而我的专业编程日子现在已经远去,我需要一些帮助。
我的思考过程相当简单:“我现在不能再编程了,但我可以用开发人员可以理解的方式描述所需的内容,这样我们就可以聘请一个自由职业者来实际编写代码”。
我向 ChatGPT 解释了我希望它创建一些 行为驱动设计(BDD)故事。BDD 是一种相当普遍采用的开发方法,它作为客户(和产品经理)所需内容与工程师将编写的代码之间的中介。编写 BDD 故事是检验自己对问题理解的一个好方法,所以自然地,我让 ChatGPT 为我做了这件事。
ChatGPT 创建的一个 BDD 用户故事的示例——因为巫师与机器人。这就是全部。
重要的是要理解,BDD 故事并不是对将要执行的代码的近似,而是一个非常好的方法来了解交付的应用程序是否与产品经理真正要求的相符(并且希望,符合客户真正想要的)。
在要求这些用户故事之后,我意识到没有理由不能要求 ChatGPT 编写代表这些故事的代码。通过一点提示工程,我建议 ChatGPT 用 Javascript、HTML 和 CSS(构建简单网页应用程序的最基本代码)创建这个应用程序。两分钟后,我得到了一个应用程序的构建块,它代表了我们为其编写用户故事的功能基础。
一旦我让应用程序以其摇摇晃晃、刚孵化出的形式运行时,我意识到任何自尊心的开发人员都会基于代码和用户故事创建测试。因此,在与 ChatGPT 简要讨论了最适合 Javascript 的测试运行器后,我要求创建一组更技术性的、测试驱动开发的(TDD)测试,以确保应用程序符合我最初的 BDD 故事。
ChatGPT 很好地推荐了 Jasminne Javascript 测试运行器,并编写了测试。
整个过程最令人惊讶的是,完成一些开发人员最不喜欢的任务的轻松程度。对于大多数工程师来说,工作的美在于找到解决方案的创造力和代码成功运行的刺激。编写测试、创建文档、编写用户故事甚至引导基本代码是乏味的任务,从情感上讲,最好避免。
通过这个过程,我想起了 Simon Wardley 的一个发人深省的 推文线程。Simon 是 Wardley Maps 方法的创建者。Simon 的评论提出,你的测试套件应当表示和解释你产品实际包含的复杂且相互依赖的关系图,这也是你大量知识产权所在之处。
X : 编写一个商品规范的最佳方法是什么?
Me : 你的测试套件。
X : 啊?
Me : 每个新事物都从几个基本的测试开始,随着它的发展会增加更多测试,你的产品应该基于这些测试构建,最终这些测试应帮助定义商品。
Me : 你的业务、硬件和软件应该尽可能地将测试驱动开发融入其中。如何在复杂的环境中改变任何东西而不进行测试呢?地图上的每一条线都是一种关系,一个应该有测试的接口……
…这些测试定义了接口的操作,它们是你交给他人的规范,你用来检查产品的规范,你用来测试商品的规范,然后决定你希望进行的交易。这些测试应随着事物本身的演变而扩展和演变。
尽管测试的编写没有应用代码那样令人兴奋,并且常常被视为维护的负担,但良好的测试展示了应用程序的预期行为。更重要的是,测试不仅描述了某物应该如何表现,还展示了它确实能够一再如此表现。定义一个真正有价值的测试套件的范围和细节是产品和工程团队共同完成的协作工作的一部分。Simon 的推文激励我思考,也许,一个经过精心设计的测试开始了生成应用程序的未来。
从这个角度看,我们可以开始设想一个接受生成式 AI 工具的产品工程团队的未来。挖掘客户需求、理解潜在解决方案、分析为客户提供服务所创造的价值并优先排序工作将仍然像今天一样重要。产品管理无疑会从生成式 AI 工具中受益,但我怀疑对产品学科的影响会比对技术团队的影响小。
这让我们回到了那五名工程师。
在第三部分,你可以阅读到:
-
生成式 AI 工具如何可能颠覆长期以来的 5 名工程师对 1 名产品经理的比例。
-
像 Github Copilot 和 AWS Amplify Studio 这样的工具如何重塑产品开发,将工程师的重点从手动编码转向设计、架构和集成。
-
生成式 AI 工具如何帮助面临过时技术、轻松处理复杂移植和重构的团队。
-
AI 工具在移动和网页应用开发中的可能统一影响,减少重复劳动,弥合网页、Android 和 iOS 开发之间的技能差距。
-
编码自动化对初级开发者和工程进展的影响
本系列其他文章:
附注:如果你喜欢这些关于团队的文章,可以看看我的 Teamcraft 播客,在这里我和我的联合主持人安德鲁·麦克拉伦(Andrew Maclaren)与嘉宾讨论使团队运作的因素。
正确调用 ChatGPT API 的方法
如何可靠地调用 ChatGPT API 以构建稳健的应用程序
·
查看 发布于 Towards Data Science · 11 分钟阅读 · 2023 年 7 月 15 日
–
现在 LLM 随处可见,尤其是 ChatGPT。正在基于它构建大量应用程序,如果你还没尝试过,应该试试看。
由 Midjourney 创建。
在构建基于 ChatGPT 的应用程序时,通常需要进行多个并行调用。不幸的是,你不是唯一一个需要这样做的人。由于许多应用程序每天处理数百万个请求(顺便说一句,给他们的工程团队点赞),API 经常会返回“请求过多”的错误。因此,我们需要一种有效的方法来处理这种错误,同时进行多个并行调用。
在这个简短的 Python 教程中,我们将涵盖两个重要主题,以有效地调用 ChatGPT API:
-
并行执行多个调用
-
在调用失败时重试调用
1. 并行执行多个调用
执行调用的最简单方法是同步执行,也就是发送请求并等待响应到达以继续程序。我们可以简单地这样做:
import requests
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {OPENAI_API_KEY}"
}
response_json = requests.post("https://api.openai.com/v1/chat/completions", headers=headers, json={
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "ping"}],
"temperature": 0
}).json()
print(response_json["choices"][0]["message"]["content"])
Pong!
如果我们在一个简单的系统中工作,这样做就可以了,但是,如果我们希望并行地对 API 或其他资源(如数据库)执行多个调用,我们可以使用异步方式获得更快的响应。
异步执行任务将触发每个操作并等待它们并行完成,从而减少等待时间。
实现这一点的基本方法是创建不同的线程来处理每个请求,然而,有一种更好的方法可以做到这一点使用异步调用。
使用异步调用通常更高效,因为您可以指定应用程序应等待的确切位置,而在传统的线程处理中,系统将自动使线程等待,这可能是次优的。
下面我们展示了使用同步和异步调用之间的差异示例。
# Sync call
import time
def delay_print(msg):
print(msg, end=" ")
time.sleep(1)
def sync_print():
for i in range(10):
delay_print(i)
start_time = time.time()
sync_print()
print("\n", time.time() - start_time, "seconds.")
0 1 2 3 4 5 6 7 8 9
10.019574642181396 seconds.
#Async Call
import asyncio
async def delay_print_async(msg):
print(msg, end=" ")
await asyncio.sleep(1)
async def async_print():
asyncio.gather(*[delay_print_async(i) for i in range(10)])
start_time = time.time()
await async_print()
print("\n", time.time() - start_time, "seconds.")
0.0002448558807373047 seconds.
0 1 2 3 4 5 6 7 8 9
asyncio.gather
方法将触发所有传递给它的异步调用,并在它们准备就绪后返回它们的结果。
遗憾的是,使用requests
库进行异步调用是不可能的。您可以使用aiohttp
库来实现。下面是使用aiohttp
进行异步调用的示例。
import aiohttp
async def get_completion(content):
async with aiohttp.ClientSession() as session:
async with session.post("https://api.openai.com/v1/chat/completions", headers=headers, json={
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": content}],
"temperature": 0
}) as resp:
response_json = await resp.json()
return response_json["choices"][0]['message']["content"]
await get_completion("Ping")
Pong!
如前所述,要执行异步请求,我们需要使用asyncio.gather
方法。
async def get_completion_list(content_list):
return await asyncio.gather(*[get_completion(content) for content in content_list])
await get_completion_list(["ping", "pong"]*5)
['Pong!',
'Ping!',
'Pong!',
'Ping!',
'Pong!',
'Ping!',
'Pong!',
'Ping!',
'Pong!',
'Ping!']
虽然这种方法有效,但是这样做调用不理想,因为我们为每次调用重新创建会话对象。我们可以通过以下方式重复使用同一个会话对象来节省资源和时间:
async def get_completion(content, session):
async with session.post("https://api.openai.com/v1/chat/completions", headers=headers, json={
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": content}],
"temperature": 0
}) as resp:
response_json = await resp.json()
return response_json["choices"][0]['message']["content"]
async def get_completion_list(content_list):
async with aiohttp.ClientSession() as session:
return await asyncio.gather(*[get_completion(content, session) for content in content_list])
await get_completion_list(["ping", "pong"]*5)
简单吧?通过这种方式,您可以轻松执行多个调用。然而,一个问题是通常不建议通过这种方式执行无限次调用,因为这样可能会过载系统,并且会受到惩罚,导致在某段时间内无法执行额外的请求(相信我,您会)。因此,建议限制同时执行的调用数量是一个好主意。您可以通过asyncio.Semaphore
类轻松实现这一点。
Semaphore
类创建了一个上下文管理器,用于管理其上下文中当前正在执行的异步调用的数量。如果达到最大数量,它将阻塞直到某些调用完成。
async def get_completion(content, session, semaphore):
async with semaphore:
await asyncio.sleep(1)
async with session.post("https://api.openai.com/v1/chat/completions", headers=headers, json={
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": content}],
"temperature": 0
}) as resp:
response_json = await resp.json()
return response_json["choices"][0]['message']["content"]
async def get_completion_list(content_list, max_parallel_calls):
semaphore = asyncio.Semaphore(value=max_parallel_calls)
async with aiohttp.ClientSession() as session:
return await asyncio.gather(*[get_completion(content, session, semaphore) for content in content_list])
start_time = time.perf_counter()
completion_list = await get_completion_list(["ping", "pong"]*5, 100)
print("Time elapsed: ", time.perf_counter() - start_time, "seconds.")
print(completion_list)
Time elapsed: 1.8094507199984946 seconds.
['Pong!', 'Ping!', 'Pong!', 'Ping!', 'Pong!', 'Ping!', 'Pong!', 'Ping!', 'Pong!', 'Ping!']
这里的一个可选事项是报告调用进度如何。您可以通过创建一个小类来实现这一点,该类将保存进度并在所有调用之间共享。您可以像下面这样做:
class ProgressLog:
def __init__(self, total):
self.total = total
self.done = 0
def increment(self):
self.done = self.done + 1
def __repr__(self):
return f"Done runs {self.done}/{self.total}."
async def get_completion(content, session, semaphore, progress_log):
async with semaphore:
await asyncio.sleep(1)
async with session.post("https://api.openai.com/v1/chat/completions", headers=headers, json={
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": content}],
"temperature": 0
}) as resp:
response_json = await resp.json()
progress_log.increment()
print(progress_log)
return response_json["choices"][0]['message']["content"]
async def get_completion_list(content_list, max_parallel_calls):
semaphore = asyncio.Semaphore(value=max_parallel_calls)
progress_log = ProgressLog(len(content_list))
async with aiohttp.ClientSession() as session:
return await asyncio.gather(*[get_completion(content, session, semaphore, progress_log) for content in content_list])
start_time = time.perf_counter()
completion_list = await get_completion_list(["ping", "pong"]*5, 100)
print("Time elapsed: ", time.perf_counter() - start_time, "seconds.")
print(completion_list)
Done runs 1/10.
Done runs 2/10.
Done runs 3/10.
Done runs 4/10.
Done runs 5/10.
Done runs 6/10.
Done runs 7/10.
Done runs 8/10.
Done runs 9/10.
Done runs 10/10.
Time elapsed: 1.755018908999773 seconds.
['Pong!', 'Ping!', 'Pong!', 'Ping!', 'Pong!', 'Ping!', 'Pong!', 'Ping!', 'Pong!', 'Ping!']
关于如何执行多个异步请求,这一节就到此为止。通过这种方式,您可以执行多个异步调用,限制每次调用的数量,并报告进度。然而,仍然有一些问题需要处理。
发出的请求可能由于服务器过载、连接中断、请求不当等多种原因而失败。这些可能会生成异常或返回不可预测的响应,因此我们需要处理这些情况,并自动重试失败的调用。
2. 失败时重试调用
为了处理失败的调用,我们将使用tenacity
库。Tenacity 提供了函数装饰器,可以在函数调用生成异常时自动重试。
from tenacity import (
retry,
stop_after_attempt,
wait_random_exponential,
)
为了给我们的调用提供重试功能,我们需要添加@retry
装饰器。使用它而不添加额外参数将使函数在失败后立即并无限期地重试。这在某些情况下是不好的。
其中一个原因是我们的函数调用可能因服务器过载而失败,这使得在重新尝试之前等待一段时间是合理的。为了指示等待时间,我们将使用指数退避的方法,通过参数wait=wait_random_exponential(min=min_value, max=max_value)
。这将使等待时间随着函数失败的次数增加。
一个可选的操作是每次重试时记录消息。我们可以通过提供某个函数给参数before_sleep
来实现这一点。这里我们将使用print
函数,但更好的方法是使用logging
模块,并将logging.error
或logging.debug
函数传递给该参数。
为了演示,我们将生成随机异常。
import random
class ProgressLog:
def __init__(self, total):
self.total = total
self.done = 0
def increment(self):
self.done = self.done + 1
def __repr__(self):
return f"Done runs {self.done}/{self.total}."
@retry(wait=wait_random_exponential(min=1, max=60), before_sleep=print)
async def get_completion(content, session, semaphore, progress_log):
async with semaphore:
#await asyncio.sleep(1)
if random.random() < 0.2:
raise Exception("Random exception")
async with session.post("https://api.openai.com/v1/chat/completions", headers=headers, json={
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": content}],
"temperature": 0
}) as resp:
response_json = await resp.json()
progress_log.increment()
print(progress_log)
return response_json["choices"][0]['message']["content"]
async def get_completion_list(content_list, max_parallel_calls):
semaphore = asyncio.Semaphore(value=max_parallel_calls)
progress_log = ProgressLog(len(content_list))
async with aiohttp.ClientSession() as session:
return await asyncio.gather(*[get_completion(content, session, semaphore, progress_log) for content in content_list])
start_time = time.perf_counter()
completion_list = await get_completion_list(["ping", "pong"]*5, 100)
print("Time elapsed: ", time.perf_counter() - start_time, "seconds.")
print(completion_list)
<RetryCallState 133364377433616: attempt #1; slept for 0.74; last result: failed (Exception Random exception)>
<RetryCallState 133364377424496: attempt #1; slept for 0.79; last result: failed (Exception Random exception)>
Done runs 1/10.
Done runs 2/10.
Done runs 3/10.
Done runs 4/10.
Done runs 5/10.
Done runs 6/10.
Done runs 7/10.
Done runs 8/10.
Done runs 9/10.
Done runs 10/10.
Time elapsed: 1.1305301820011664 seconds.
['Pong!', 'Ping!', 'Pong!', 'Ping!', 'Pong!', 'Ping!', 'Pong!', 'Ping!', 'Pong!', 'Ping!']
这将使我们的函数在重试之前等待一段时间。然而,失败的原因可能是系统性的,例如服务器停机或错误的有效负载。在这种情况下,我们希望将重试次数限制在一个范围内。我们可以通过参数stop=stop_after_attempt(n)
来实现。
import random
class ProgressLog:
def __init__(self, total):
self.total = total
self.done = 0
def increment(self):
self.done = self.done + 1
def __repr__(self):
return f"Done runs {self.done}/{self.total}."
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(2), before_sleep=print)
async def get_completion(content, session, semaphore, progress_log):
async with semaphore:
#await asyncio.sleep(1)
if random.random() < 0.9:
raise Exception("Random exception")
async with session.post("https://api.openai.com/v1/chat/completions", headers=headers, json={
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": content}],
"temperature": 0
}) as resp:
response_json = await resp.json()
progress_log.increment()
print(progress_log)
return response_json["choices"][0]['message']["content"]
async def get_completion_list(content_list, max_parallel_calls):
semaphore = asyncio.Semaphore(value=max_parallel_calls)
progress_log = ProgressLog(len(content_list))
async with aiohttp.ClientSession() as session:
return await asyncio.gather(*[get_completion(content, session, semaphore, progress_log) for content in content_list])
start_time = time.perf_counter()
completion_list = await get_completion_list(["ping", "pong"]*5, 100)
print("Time elapsed: ", time.perf_counter() - start_time, "seconds.")
print(completion_list)
<RetryCallState 133364608660048: attempt #1; slept for 0.1; last result: failed (Exception Random exception)>
<RetryCallState 133364377435680: attempt #1; slept for 0.71; last result: failed (Exception Random exception)>
<RetryCallState 133364377421472: attempt #1; slept for 0.17; last result: failed (Exception Random exception)>
<RetryCallState 133364377424256: attempt #1; slept for 0.37; last result: failed (Exception Random exception)>
<RetryCallState 133364377430928: attempt #1; slept for 0.87; last result: failed (Exception Random exception)>
<RetryCallState 133364377420752: attempt #1; slept for 0.42; last result: failed (Exception Random exception)>
<RetryCallState 133364377422576: attempt #1; slept for 0.47; last result: failed (Exception Random exception)>
<RetryCallState 133364377431312: attempt #1; slept for 0.11; last result: failed (Exception Random exception)>
<RetryCallState 133364377425840: attempt #1; slept for 0.69; last result: failed (Exception Random exception)>
<RetryCallState 133364377424592: attempt #1; slept for 0.89; last result: failed (Exception Random exception)>
---------------------------------------------------------------------------
Exception Traceback (most recent call last)
/usr/local/lib/python3.10/dist-packages/tenacity/_asyncio.py in __call__(self, fn, *args, **kwargs)
49 try:
---> 50 result = await fn(*args, **kwargs)
51 except BaseException: # noqa: B902
5 frames
Exception: Random exception
The above exception was the direct cause of the following exception:
RetryError Traceback (most recent call last)
/usr/local/lib/python3.10/dist-packages/tenacity/__init__.py in iter(self, retry_state)
324 if self.reraise:
325 raise retry_exc.reraise()
--> 326 raise retry_exc from fut.exception()
327
328 if self.wait:
RetryError: RetryError[<Future at 0x794b5057a590 state=finished raised Exception>]
设置此参数后,一旦尝试次数达到最大值,将引发RetryError
。然而,可能有时我们希望继续运行而不生成异常,只是将None
值保存到调用返回中以便稍后处理。为此,我们可以使用回调函数retry_error_callback
在发生RetryError
错误时仅返回None
值:
import random
class ProgressLog:
def __init__(self, total):
self.total = total
self.done = 0
def increment(self):
self.done = self.done + 1
def __repr__(self):
return f"Done runs {self.done}/{self.total}."
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(2), before_sleep=print, retry_error_callback=lambda _: None)
async def get_completion(content, session, semaphore, progress_log):
async with semaphore:
#await asyncio.sleep(1)
if random.random() < 0.7:
raise Exception("Random exception")
async with session.post("https://api.openai.com/v1/chat/completions", headers=headers, json={
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": content}],
"temperature": 0
}) as resp:
response_json = await resp.json()
progress_log.increment()
print(progress_log)
return response_json["choices"][0]['message']["content"]
async def get_completion_list(content_list, max_parallel_calls):
semaphore = asyncio.Semaphore(value=max_parallel_calls)
progress_log = ProgressLog(len(content_list))
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(1)) as session:
return await asyncio.gather(*[get_completion(content, session, semaphore, progress_log) for content in content_list])
start_time = time.perf_counter()
completion_list = await get_completion_list(["ping", "pong"]*5, 100)
print("Time elapsed: ", time.perf_counter() - start_time, "seconds.")
print(completion_list)
<RetryCallState 133364377805024: attempt #1; slept for 0.22; last result: failed (Exception Random exception)>
<RetryCallState 133364377799456: attempt #1; slept for 0.53; last result: failed (Exception Random exception)>
<RetryCallState 133364377801328: attempt #1; slept for 0.24; last result: failed (Exception Random exception)>
<RetryCallState 133364377810208: attempt #1; slept for 0.38; last result: failed (Exception Random exception)>
<RetryCallState 133364377801616: attempt #1; slept for 0.54; last result: failed (Exception Random exception)>
<RetryCallState 133364377422096: attempt #1; slept for 0.59; last result: failed (Exception Random exception)>
<RetryCallState 133364377430592: attempt #1; slept for 0.07; last result: failed (Exception Random exception)>
<RetryCallState 133364377425648: attempt #1; slept for 0.05; last result: failed (Exception Random exception)>
Done runs 1/10.
Done runs 2/10.
Done runs 3/10.
Time elapsed: 2.6409040250000544 seconds.
['Pong!', 'Ping!', None, None, None, None, None, 'Ping!', None, None]
这样,None
值将被返回,而不是生成错误。
另一个尚未处理的问题是连接挂起问题。当我们执行请求时,由于某种原因,主机保持连接但既没有失败也没有返回任何内容,这种情况会发生。为了处理这种情况,我们需要设置超时,以便在调用在指定时间内未返回值时返回。为此,我们可以使用aiohttp
库中的timeout
参数,以及aiohttp.ClientTimeout
类。如果发生超时,将引发TimeoutError
,然后由tenacity
的retry
装饰器处理,并自动再次运行函数。
class ProgressLog:
def __init__(self, total):
self.total = total
self.done = 0
def increment(self):
self.done = self.done + 1
def __repr__(self):
return f"Done runs {self.done}/{self.total}."
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(20), before_sleep=print, retry_error_callback=lambda _: None)
async def get_completion(content, session, semaphore, progress_log):
async with semaphore:
async with session.post("https://api.openai.com/v1/chat/completions", headers=headers, json={
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": content}],
"temperature": 0
}) as resp:
response_json = await resp.json()
progress_log.increment()
print(progress_log)
return response_json["choices"][0]['message']["content"]
async def get_completion_list(content_list, max_parallel_calls):
semaphore = asyncio.Semaphore(value=max_parallel_calls)
progress_log = ProgressLog(len(content_list))
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(10)) as session:
return await asyncio.gather(*[get_completion(content, session, semaphore, progress_log) for content in content_list])
start_time = time.perf_counter()
completion_list = await get_completion_list(["ping", "pong"]*100, 100)
print("Time elapsed: ", time.perf_counter() - start_time, "seconds.")
<RetryCallState 133364375201936: attempt #1; slept for 0.57; last result: failed (TimeoutError )>
Time elapsed: 12.705538211999738 seconds.
太棒了!现在我们有了一种强大的方法来运行多个并行请求,发生失败时会自动重试,并在失败是系统性时返回 None
值。所以最终的代码如下:
import asyncio
import aiohttp
from tenacity import (
retry,
stop_after_attempt,
wait_random_exponential,
)
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {OPENAI_API_KEY}"
}
class ProgressLog:
def __init__(self, total):
self.total = total
self.done = 0
def increment(self):
self.done = self.done + 1
def __repr__(self):
return f"Done runs {self.done}/{self.total}."
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(20), before_sleep=print, retry_error_callback=lambda _: None)
async def get_completion(content, session, semaphore, progress_log):
async with semaphore:
async with session.post("https://api.openai.com/v1/chat/completions", headers=headers, json={
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": content}],
"temperature": 0
}) as resp:
response_json = await resp.json()
progress_log.increment()
print(progress_log)
return response_json["choices"][0]['message']["content"]
async def get_completion_list(content_list, max_parallel_calls, timeout):
semaphore = asyncio.Semaphore(value=max_parallel_calls)
progress_log = ProgressLog(len(content_list))
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(timeout)) as session:
return await asyncio.gather(*[get_completion(content, session, semaphore, progress_log) for content in content_list])
总结来说,我们实现了以下功能:
-
异步调用以减少等待时间。
-
记录异步调用的进度。
-
当调用失败时,自动触发重试。
-
如果失败是系统性的,则返回 None 值。
-
当调用超时且没有返回任何内容时,重试调用。
如果你有任何问题、发现错误或有改进的建议,请在下面留下评论!
数据科学的真正力量隐藏在明面上
原文:
towardsdatascience.com/the-real-power-of-data-science-is-hiding-in-plain-sight-700079f20d53
意见
如果数据科学家专注于方法而非影响力,他们可能会掩盖自己能够为组织带来的真正价值
·发布于 Towards Data Science ·5 min read·2023 年 1 月 18 日
–
数据科学的真正力量通常隐藏在不必要的复杂性背后。 来源:作者
数据科学行业内部的人与其他人之间正在形成障碍。
虽然数据科学的入门门槛众所周知;只有 2.4%的从业者拥有高等教育后的正式教育,超过一半拥有硕士或博士学位[1],但另一个障碍正在形成…
现在存在一个理解的障碍。
理解数据科学是什么、它能提供什么以及如何应用变得越来越困难。尽管这一直是一个复杂的行业,但最近的进展使其对外界变得晦涩难懂。
这个障碍随着时间的推移而增长,可能会阻碍人工智能应用的进展。近年来机器学习和人工智能的巨大进展如果无法跨行业、企业和组织应用,那也毫无意义。
本文探讨了这种差距是如何出现的,以及可以采取哪些措施来解决这一问题。
我们是如何来到这个位置的?
理解障碍的形成是由于三个并行发展的趋势:
1. 算法的复杂性正在指数级增长。
随着数据科学的发展,其基础算法也在不断成熟。为不同的应用场景创建了专门的模型,从图像识别到自然语言理解以及介于两者之间的各种架构都在被使用。
除了各种算法的爆炸性增长外,模型的规模和复杂性也随着时间的推移呈指数级增长,以实现更高的性能水平。
下图就是一个很好的例子。变压器模型,2018 年的全新架构,在短短几年内从包含几亿个参数的模型发展到超过 5000 亿个参数:
机器学习模型的复杂性在过去四年中呈指数级增长。来源:作者
这种复杂性导致了行业从业者在不断研究和理解新架构时付出了巨大的“开销”。
这也导致了从个人转向大型公司进行新模型开发的转变。
训练一个 5000 亿参数的模型所需的资源远远超过个人甚至小型组织的能力。只有大型国际公司才能在这种规模上运营。
2. 新思想的产生和分享速度在加快。
不足为奇的是,数据科学在研究领域是一个增长领域。然而,新思想在行业内的创造和分享速度持续加快。
下图显示了提交并被接受到领先的机器学习和神经科学会议 NeurIPS 的论文数量。
仅在过去五年中,就出现了三倍的增长。
在过去五年中,提交给 NeurIPS 的论文数量增加了三倍。来源:作者
此外,论文发表与提交会议之间的时间框架显著缩短[2]。创新的速度现在如此之快,以至于未提交的论文有可能在下一届年会上变得过时。
新论文的发布量、种类和速度现在远远超出了任何个人能够跟上的程度。需要做出选择,要么专注于特定领域,要么接受对行业的更广泛视角也是浅显的。
对于行业外的人来说,这使得理解当前发展状态以及这些状态在实际应用中的含义几乎是不可能的。
3. 随着数据科学家社区的成熟,它创造了进一步的理解障碍。
尽管上述前两点是新兴和快速增长行业的副作用,但第三个阻碍理解的领域完全是自我造成的。
数据科学社区充斥着术语和技术词汇,关注工具、方法和技术而非结果。
2022 年的谷歌搜索量始终远超用例搜索。来源:作者,数据来自谷歌趋势
这可能导致一种心态,即为不存在的问题创造解决方案;大量精力被耗费在攀登排行榜上,而不是将思想应用于现实世界。
行业语言也变得越来越技术化。这在更成熟的职业领域如法律和金融中反复出现;它有助于创建身份感和归属感。它还可以帮助区分在某一领域中有知识的人和没有知识的人。
然而,技术语言也是排他性和令人困惑的。对于一个建立在思想共享基础上的社区来说,看到人为障碍被建立起来是令人遗憾的;现有的障碍已经足够多了。
如何解决理解障碍的问题?
答案很简单:
-
简单性应被看重于复杂性。
-
应将重点放在实际成果上,而不是理论概念。
-
教育和参与行业之外的利益相关者应被视为任何数据科学角色中必不可少的一部分。
从根本上讲,需要将数据科学从作为独立实体的观点中转变过来。它应被视为企业和组织日常运营的一部分。
这应从根本上将重点从技术复杂性转移到实际影响和成果上。
参考文献
研究代理:应对基于大量文本语料库回答问题的挑战
我制作了一个自主 AI 研究代理,可以通过深度多跳推理能力回答困难问题
·
关注 发表在 Towards Data Science · 16 min 阅读 · 2023 年 8 月 29 日
–
作者提供的图片(使用 Photoshop 生成填充)
问题介绍
2021 年,我开始研究基于大量文本语料库回答问题的挑战。在预训练变换器普及之前,这个问题非常难以解决。
令我沮丧的是,我开始用《摩诃婆罗多》这一最复杂、最精细的故事之一进行实验。对于那些不熟悉这部作品的人来说,《摩诃婆罗多》是一部由 18 本书组成的总字数约为 180 万的作品。它是有史以来最长的诗歌,大约有 90,000 节。它的长度大约是《伊利亚特》和《奥德赛》总和的十倍。但《摩诃婆罗多》的震撼不仅在于长度,还有其广度。它在因果关系上高度非线性和复杂,拥有跨越七代的数千个角色,而这些角色中没有一个是完全善良或邪恶的。它对职责(业)、选择和人类存在,尤其是职责冲突和多重错误选择的冲突进行了深刻的哲学评论。《博伽梵歌》(印度教的核心哲学)也是《摩诃婆罗多》第六本书的一部分。
我将来自多个在线来源的《摩诃婆罗多》文本数据整理成了一个干净的数据集。然而,我找不到一种方法来对这些文本实施有意义的问答。
不到两年的时间,一切都发生了变化。
AI 和大型预训练变换器的快速进步正在深刻而根本地改变技术世界。我对这一点感到着迷,就像现在大多数技术人员一样。
几个月前,我带着对新兴的提示工程艺术的初步了解重新审视了这个问题。但这次我有了一个大致的想法,即创建一个可以处理任何复杂知识库的自主 研究代理。
《摩诃婆罗多》是最复杂的用例之一。然而,在每个知识领域,如法律、科学研究、教育、医疗等,每个项目都从对前人工作的深入研究开始。因此,这个问题是值得解决的。
研究代理
在这里,我将讨论一个可以解决多跳 KBQA 问题并具备深度推理能力的自主 AI 研究代理的设计和实施。我会分享一个包含研究代理初步实现的 Python 笔记本的 git 仓库。如果你对这部分内容感兴趣,请随时跳到本文后面的实施部分。
如果你对了解更多关于 AI 代理、‘基于知识的问题回答’(KBQA)、‘为什么’、‘什么’以及 AI 研究代理的设计演变感兴趣,请继续阅读。
为什么?
可能有人会问,为什么不直接使用 ChatGPT 接口提问。它已经在 2021 年之前生成的大量互联网数据上进行了训练,因此像《摩诃婆罗多》这样的文本语料库对它来说是已知的。
这是我的第一种方法。我问 ChatGPT 关于《摩诃婆罗多》的几个问题。我得到了对一些问题的良好回答。然而,大部分答案缺乏严谨性。这是可以预料的。GPT 是在通用数据集上训练的。它可以很好地理解和解释自然语言。它的推理能力也足够好。然而,它在任何特定领域都不是专家。所以,尽管它可能对《摩诃婆罗多》有一些了解,但可能不会给出深入研究的答案。有时 GPT 甚至可能没有任何答案。在这些情况下,它要么谦虚地拒绝回答问题,要么自信地编造答案(幻觉)。
实现 KBQA 的第二种最明显的方法是使用检索问答提示。这时,LangChain 显得极为有用。
检索问答
对于那些不熟悉 LangChain 库的人来说,它是将 LLMs(如 GPT)用于代码中的最佳方式之一。这里是使用 LangChain 实现 KBQA 的方法。
这个示例展示了在索引上进行问答。
总结一下,实现任何文档集合上的 KBQA 的步骤如下
-
将知识库拆分成文本块。
-
为每个文本块创建数值表示(嵌入)并将其保存到向量数据库中。
如果你的数据是静态的,第 1 步和第 2 步是一次性的工作。
-
使用用户的查询在这个数据库上进行语义搜索,并获取相关的文本块。
-
将这些文本块与用户的问题一起发送到 LLM,并要求其回答。
这是该过程的图示表示。
作者通过 draw.io 创建的图片
那么,为什么要继续呢?这似乎是一个已解决的问题!
不太对 🙁
这种方法适用于简单问题和简单的事实知识库。然而,对于需要更深层次、多跳推理的复杂知识库和更复杂的问题则不适用。多跳推理指的是通过多个逻辑或上下文推理步骤来得出结论或回答问题的过程。
此外,LLMs 在一次提示中处理文本的长度是有限的。你可以当然逐个发送文档,然后通过每次调用来‘细化’或‘缩减’答案。然而,这种方法不允许进行复杂的‘多跳’推理。在某些情况下,使用‘细化’或‘缩减’方法得到的结果比仅仅将所有文档塞进一个提示中要好,但差距不大。
对于复杂的知识库,用户的问题本身可能不足以找到所有相关文档,从而帮助 LLM 得出准确的答案。
例如:
阿周那是谁?
这是一个简单的问题,可以在有限的背景下回答。然而,以下问题:
为什么《摩诃婆罗多》战争发生了?
这是一个在整个文本语料库中分布的背景问题。问题本身对其背景信息了解有限。找到相关的文本片段并在此基础上进行推理可能效果不好。
那接下来呢?
AI 代理
这是 AI 出现后出现的最酷的概念之一。如果你不知道 AI 代理的概念,我迫不及待想向你解释,但我可能还是无法传达它的惊人之处。让我先用 ChatGPT 来解释一下。
AI 代理,也称为“代理”,是指能够自主感知其环境、做出决策并采取行动以实现特定目标的软件程序或系统。AI 代理旨在模拟类似人类的行为,用于问题解决和决策任务。它们在定义的环境中操作,并与该环境互动,以实现期望的结果。
简单来说,代理是一个接受问题、决定如何解决它并然后解决它的程序。代理会提供一组工具,如函数、方法、API 调用等。它可以按任何顺序选择使用这些工具。与传统软件不同,传统软件中解决问题所需的步骤顺序是预先编程的。当然,这是一个非常模糊的定义。但你现在应该能理解了。
这是我为我们的 KBQA 用例尝试的两种不同的代理。
ReAct 该代理使用 ‘ReAct’(推理与行动)风格的推理 来决定针对给定问题使用哪个工具。
这是 ReAct 代理的 langChain 实现:
本文档展示了如何使用代理来实现 ReAct 逻辑。
python.langchain.com](https://python.langchain.com/docs/modules/agents/agent_types/react?source=post_page-----4ef8e6f1b741--------------------------------)
我为代理提供了以下工具供其选择:
-
带有文档存储的检索 QA 链。
-
字符表词汇表搜索(我使用预训练模型创建了一个带有命名实体识别的词汇表)
-
维基百科搜索。
ReAct 代理并没有给我带来好的结果,并且大多数时候无法收敛到任何答案。它与 GPT 3.5 的兼容性不好。它可能在 GPT 4 上表现更好,而 GPT 4 比 GPT 3.5 贵 20 - 30 倍,因此这可能还不是一个选择。
即使它收敛了,我也无法获得好的结果。可能在创建“反应”提示方面更有经验的人会做得更好。
自我提问代理 这个代理根据原始问题提出后续问题,然后尝试找到中间答案。通过这些中间答案,它最终得出一个最终答案。这里是解释自我提问代理的文章
自我提问提示是一种从链式思维提示演变而来的方法。下面是一些实际的例子和…
这种方法给了我一些不错的结果。它在单跳推理的情况下效果很好,但即使这样,对于需要多跳的问提也会失败。
例如,问题:
谁杀了卡尔纳,为什么?
用这种方法相对容易回答。
问题
为什么阿周那杀了他的半兄卡尔纳?
要回答这个问题要困难得多。它要求 LLM 知道阿周那不知道卡尔纳是他的半兄弟的事实。LLM 无法知道它需要知道这个事实,无论是通过理解问题还是通过基于原始问题提出进一步的问题。
人类研究过程
再次引用 GPT
AI 代理被设计成在解决问题和决策任务中模仿类似人类的行为。
所以,我的下一个想法是研究人类如何研究,也就是元研究。我想象自己坐在图书馆里(大学的怀旧情怀),能够轻松获取所有与我的研究主题相关的书籍。我拿出一个笔记本和一支笔,开始记录我在研究一个主题时遵循的过程。
这是我得到的结果。
研究方法论:
在一页上记下原始查询。
-
我尝试通过阅读几本书来回答当前的问题。在过程中,我做了些笔记,并标记了一些我认为与当前问题最相关的摘录。
-
我在这些摘录中总能发现许多未知之处。我记录下这些未知,并写下更多问题以帮助我了解这些未知。
-
从这些问题中,我选择一个与原始问题最相关的问题。
-
我回到第一步
经过几次这样的迭代,我问自己是否有足够的信息来回答原始问题。
如果是,那么干得好!
如果不是,那就继续努力吧。
看啊!
最终,我知道该编写什么代码。我希望通过一些提示工程,这个过程能给我比之前尝试的其他方法更深刻的答案。
剧透警告……确实如此!😃
在开始编码之前,我在互联网上搜索了类似的想法。我发现了 BabyAGI。真是一个奇妙的世界!
这是描述 BabyAGI 的一个仓库
我意识到 BabyAGI 与上述研究过程之间有许多相似之处。因此,我怀着感激之情,从 BabyAGI 实现中使用的提示中获得了一些灵感。
研究代理— 实现
这里是使用惊人的 draw.io 将相同的过程转换为流程图
作者提供的图像(使用 draw.io 生成)
图表中的每个蓝色框都是对 LLM 的调用。
组件
-
QA 代理— 搜索答案和进一步的背景信息
这是一个简单的***‘stuff’*** 检索 QA 链,它使用了一个向量存储。在未来,这可以是一个使用向量存储、搜索 API 或维基百科 API、内容审核 API 和以往研究数据等工具的 AI 代理。这里的提示经过调整,以根据 1. 上下文(文档)和 2. 与原始问题的相关性来生成简洁的答案。
除了第一轮外,当前问题 总是步骤 2 中生成的中间问题,并在步骤 3 中选择。代理将中间答案附加到笔记中,并将最新的摘录(用于回答当前问题的文档)附加到书签中。最新的这些文档将在步骤 2 中使用。
-
问题生成器— 根据新的笔记提出更多问题
在这里,代理使用与当前问题匹配的最新向量搜索结果,并利用这些结果生成与原始问题相关的更多问题。它将这些问题附加到未回答的问题列表中。这里的提示经过调整,以确保新生成的问题不会与现有的问题列表重叠。
-
最相关问题挑选器— 挑选出与原始问题最相关的一个问题
这个提示从未回答的问题列表中挑选出一个与原始问题最相关的问题。这个问题将作为下一轮的当前问题。
在下一轮中,代理会在生成一组新的问题后,从未回答的问题列表中删除这个问题。
-
分析器— 我是否知道足够多?
我使用了一个max_iterations 参数来退出循环。现在这效果不错。然而,根据不断变化的上下文动态决定迭代次数或退出策略可能会更好。我将来会致力于开发一个可以自主完成这项任务的‘分析器’。
-
研究编译器— 编译研究
这是最终的提示。它利用研究过程中记录的笔记,得出对
原始问题
的详细最终答案
。
结果
研究代理在我尝试过的所有方法中是一个重大改进。它提供的答案比任何其他方法都更详细、准确。我玩了几个星期这个方法,对得到的答案的丰富性感到惊讶。
该代理在避免幻觉问题方面比任何以前的方法都有更大的进展。它会在初期的几次迭代中自动纠正产生的幻觉和事实错误。它在问题的深入处理过程中,结果会更为准确。
这是一个示例运行。
问题:为什么潘达瓦斯必须在森林中生活 12 年?
输出 —
如果你感兴趣,这是最终答案
Final Answer:
The Pandavas' decision to live in the forest for 12 years
was primarily due to the circumstances and events that
unfolded in their lives. After losing a game of dice to
The Kauravas and the Pandavas were forced into exile for
13 years, with the last year to be spent incognito.
During their exile, they faced numerous challenges
and hardships, including living in the forest.
Bhima contemplated killing all of
the Kurus, but his brother Yudhisthira always
pacified him, stating that it was not the opportune
time to fulfil their desires.
Thus, living in the forest allowed them to avoid
confrontations and maintain peace.
During their time in the forest, the Pandavas faced several difficulties.
One of the main challenges was Bhima's struggle to tolerate the ascetic
life. He contemplated killing all of the Kurus, but his brother
Yudhisthira always pacified him and advised against it.
Additionally, the Pandavas had to live in the forest and depend
on begging for their sustenance since their kingdom had been
plundered in gambling. They were distressed by the loss of their
kingdom and the insults to their queen Draupadi. The forest itself
was also full of dangers, adding to their hardships. Furthermore,
the Pandavas felt unable to fulfil the needs of certain pious Brahmanas
who stayed with them in the forest, as they did not possess anything
and were dependent on begging for their own survival.
Despite these challenges, the Pandavas made the most of their time
in the forest. They received knowledge from sages and saintly persons,
which helped them gain wisdom and spiritual insights. The great sage
Vyasa informed them that many generals had agreed to support their
cousin Duryodhana, including Karna, Shakuni, Bhurishravas, Sala,
Drona, and Bhishma. Vyasa also told Arjuna that if he were to fight
in a future war, he would need to obtain the divine astras of Lord
Indra and Lord Shiva.
In light of this information, Yudhisthira ordered Arjuna to go to the
Himalayan mountains to perform austerities to please Lord Shiva.
This was necessary for Arjuna to obtain the divine weapons that would
be crucial in the upcoming war. The Pandavas understood the importance
of being prepared and gathering the necessary resources to counter
the strength of their opponents. Therefore, Yudhisthira made the
decision to send Arjuna on this mission to seek Lord Shiva's blessings
and acquire the divine astras.
Overall, the Pandavas' exile in the forest for twelve years was a
result of the conditions imposed on them after losing the game
of dice. They faced various difficulties during their time in the
forest, but also gained valuable knowledge and prepared themselves
for the challenges that lay ahead.
这个答案相当详细。但代理的美妙之处不仅在于它准确回答了原始问题,还在于它进一步探索了与问题相关的故事。
在大多数情况下,我得到的答案充满了细节。每个这样的答案都激发了我进一步探究的好奇心。
该代理还会生成一组已回答的问题和未回答的问题,并在研究过程中记录下来。因此,每次运行后,它都会引导我提出更多的问题。在过去的几周里,我对《摩诃婆罗多》的了解比以前的多年都要多。
** Unanswered Questions **
'4\. How did the Pandavas receive knowledge from sages and saintly persons during their time in the forest?'
'5\. What were the specific austerities that Arjuna had to perform in the Himalayan mountains to please Lord Shiva?'
'6\. How did the Pandavas manage to hide from Duryodhana's spies for almost the entire thirteenth year of their exile?'
'8\. How did Bhima cope with the challenges of living as an ascetic in the forest? Did he face any particular difficulties or struggles during their time in exile?'
'9\. Can you provide more information about the generals who supported Duryodhana's cause? What were their roles and contributions in the Kurukshetra war?'
'10\. How did the Pandavas manage to maintain a peaceful life in the forest despite the challenges they faced?'
'11\. What were the specific teachings and knowledge that the Pandavas received from the sages and saintly persons during their time in the forest?'
'12\. Can you provide more information about the palace where the Pandavas lived for one full year before going to the forest?'
'13\. How did Lord Krishna's presence in the forest affect the Pandavas' experience during their exile?'
'14\. Can you provide more information about the dangers the Pandavas faced while living in the forest?'
'15\. What were the specific challenges and difficulties that Yudhisthira and his brothers faced in their daily lives as inhabitants of the forest?'
想象一下将相同的过程应用于其他知识领域,真是令人兴奋的想法!
代码
这里是实现研究代理的 Python 笔记本。
[## GitHub - rahulnyk/research_agent
通过在 GitHub 上创建一个帐户,来为 rahulnyk/research_agent 的开发做出贡献。
github.com](https://github.com/rahulnyk/research_agent?source=post_page-----4ef8e6f1b741--------------------------------)
《摩诃婆罗多》数据集的 Git 仓库
[## GitHub - rahulnyk/mahabharata: 从多个来源编纂的《摩诃婆罗多》文本,分割成若干部分…
《摩诃婆罗多》文本是从多个来源编纂的,分割成若干部分,并解析成带有元数据的 CSV 文件。命名实体…
github.com](https://github.com/rahulnyk/mahabharata?source=post_page-----4ef8e6f1b741--------------------------------)
接下来做什么?
当前实现是自主 AI 研究代理概念的一个简单版本。在代理的实现过程中,我对研究过程进行了几次修改。这是一次令人兴奋的旅程,但乐趣还未结束。以下是我目前正在进行的一些改进。
-
在公共链接上部署这个代理,并观察更多的使用模式。
-
用不同于《摩诃婆罗多》的源文档来使用该代理。
-
过程的第 1 步目前是一个简单的‘stuff’ QA 链,它使用了一个包含源文本语料库的向量存储。我正在努力将其替换为‘ReAct’代理,以便在研究过程中可以使用其他工具,如搜索 API、维基百科、审核 API 等。
-
- 我将每次运行生成的数据和元数据保存到一个
runs
向量存储中。我还将原始问题的嵌入保存到同一存储中。这帮助我跟踪代理的推理过程,并观察到其中出现的几个逻辑模式。这有助于调整 QA 代理,以跟随更紧密的推理路径。
- 我将每次运行生成的数据和元数据保存到一个
-
- 目前,研究代理在经过固定的迭代次数后存在。这对大多数问题来说效果很好。然而,根据不断变化的背景,动态决定迭代次数或退出策略可能更好。我会开发一个能够自主完成这一任务的“分析器”。
-
- 这个代理对大多数类型的问题效果很好,但对元问题除外。例如,如果我问“描述第五卷第三章中发生了什么”,代理会很难回答。在未来的版本中,我将加入一个带有‘ReAct’代理的自查询检索器,以处理此类情况。
-
- 到目前为止,我只尝试了 OpenAI GPT3.5 模型的代理。每次运行的费用约为 $0.02。我很快会尝试使用像 Llama 这样可以本地托管的较小模型。
-
在下一篇文章中,我计划写下在实施这些更新后的发现。更大的构想是创建一个顶尖的自主人工智能研究代理,能够在寻找深度研究的问题的答案方面表现出色。所以,请随时提出建议,如果可能,和我合作进一步完善。
-
希望你觉得这篇文章和分享的代码对你有帮助。
-
谢谢阅读。
-
希望你觉得这个人工智能研究代理既令人兴奋又有用。
-
我分享的笔记本只是创建一个自主人工智能研究代理这一更大构想的简单实现。为了使这个代理成为顶尖研究者,还有很多工作要做。
-
所以,请随时提出建议,如果可能,和我合作进一步完善。
-
谢谢阅读。
-
上述文章中使用的数据集的致谢以及许可证信息。
-
K. M. Ganguli 的完整翻译:可在公共领域获得。
-
- Laura Gibbs Tiny Tales:这是对《摩诃婆罗多》的重述,使用了两百个每个 100 字长的故事。我在这里使用她的作品是经过她的许可。
-
- Tilak 的 Kaggle 数据库:18 部《摩诃婆罗多》的文本格式数据用于 NLP。由Tilak以公共领域许可证共享。
失落的回归:用于预测的变换器
图片由 Aditya Vyas 提供,发布在 Unsplash
介绍一种新的变换器模型:PatchTST
·
关注 发表在 Towards Data Science · 5 min read · 2023 年 5 月 24 日
–
最近,基于变换器的方法被广泛采用。像 BERT 和 ChatGPT 这样的模型取得的显著成就鼓励了研究人员探索这种架构在各种领域的应用,包括时间序列预测。然而,香港中文大学和国际数字经济活动的研究人员最近的工作表明,为这一任务开发的实现效果不佳,并且在各种基准测试中可能被简单的线性模型击败[1]。
为此,普林斯顿大学和 IBM 的研究人员在他们的论文A Time Series is Worth 64 Words [2]中提出了 PatchTST(修补时间序列变换器)。在这篇论文中,Nie 等人介绍了两种关键机制,将变换器带回预测领域:
-
修补注意力:他们的注意力机制将时间序列的大部分作为标记进行处理,而不是逐点注意力
-
渠道独立性:时间序列中的不同目标序列被独立处理,使用不同的注意力权重。
在这篇文章中,我旨在总结这两种机制的工作原理,并讨论 Nie 等人[2]发现的结果的意义。
背景:变换器是否有效
在深入探讨 PatchTST 之前,我们需要首先了解 Zeng 等人发现的自注意力在预测领域中的问题。对于那些对详细总结感兴趣的人,我强烈建议阅读原始论文或我对他们工作的总结:
最近,长期时间序列预测(LTSF)任务中出现了许多基于变换器的解决方案……
arxiv.org](https://arxiv.org/abs/2205.13504?source=post_page-----24f6fec5bc30--------------------------------) ## 变换器是否输给了线性模型?
使用变换器进行长期预测可能不是最佳选择
towardsdatascience.com
总而言之,自注意力在应用于预测领域时存在一些关键问题。具体而言,以前的时间序列变换器使用逐点自注意力机制,其中每个时间戳被视为一个标记。然而,这存在两个主要问题。首先,这导致注意力机制对点的排列不变,如果你将点的位置颠倒,则观察到的注意力值相同。此外,单个时间戳本身包含的信息有限,其重要性来自于周围的时间戳。与语言处理中的情况类似,如果我们关注单个字符而不是单词。
这些问题在测试预测变换器时导致了一些有趣的结果:
-
变换器似乎非常容易对随机模式过拟合,因为向数据中添加噪声并没有显著降低变换器的性能。
-
更长的回溯期并没有帮助提高准确性,表明变换器未能捕捉到显著的时间模式。
这就是 PatchTST
为了应对变换器在这一领域的问题,Nie 等 [2] 引入了两个主要机制,使 PatchTST 区别于以往模型:通道独立性和分块。
在以往的工作中,所有目标时间序列会被连接到一个矩阵中,其中每一行是一个单独的序列,列是输入标记(每个时间戳一个)。这些输入标记随后会被投射到嵌入空间,这些嵌入会传递到一个单一的注意力层。
PatchTST 选择了通道独立性,每个系列独立传入变换器骨干网(图 1.b)。这意味着每个系列都有自己的一组注意力权重,使得模型可以更好地专注。该方法通常用于卷积网络,并且已被证明能显著提高网络的准确性 [2]。通道独立性还支持第二种机制:分块。
图 1:PatchTST 模型概览(图源自 Nie 等 2023 [2])
如前所述,关注单个时间步就像关注单个字符。你最终会失去顺序感(“dog”和“god”在字符级注意力下会有相同的注意力值),并且还会增加模型的内存使用。那么解决方案是什么?显然我们需要关注单词!
更准确地说,作者提议将每个输入时间序列拆分成固定长度的块 [2]。这些块随后作为输入标记传入主模型(块的长度即为标记大小) [2]。模型接着为每个块添加位置编码,并通过普通变换器 [3] 编码器处理。
这种方法自然允许 PatchTST 考虑到点级标记丢失的局部语义信息。此外,时间序列的分块显著减少了所需的输入标记数量,使得模型能够捕捉来自更长序列的信息,并显著减少了训练和预测所需的内存 [2]。此外,分块机制还使得对时间序列进行表示学习成为可能,使 PatchTST 成为一个更灵活的模型 [2]。
预测实验结果
图 2:基准上的 MSE 和 MAE 结果。第一名加粗,第二名带下划线。(图源自 Nie 等 2023)
在他们的论文中,作者测试了 PatchTST 的两种不同变体:一种是输入模型的 64 个 patch(因此得名),另一种是 42 个 patch。42 个 patch 的变体与其他模型具有相同的回溯窗口,因此可以视为对其他模型的公平比较。对于这两种变体,使用了长度为 16 的 patch 和步幅为 8 来构建输入令牌 [2]。如图 2 所示,PatchTST 变体在结果中占据主导地位,DLinear [1] 在极少数情况下获胜。平均而言,PatchTST/64 实现了 MSE 下降 21% 和 MAE 下降 16.7%。PatchTST/42 实现了 MSE 下降 20.2% 和 MAE 下降 16.4%。
结论
PatchTST 代表了 Transformer 架构在时间序列预测任务中的一个有前景的未来,特别是由于 patching 是一个简单有效的操作符,可以在未来模型中轻松实现。此外,PatchTST 的准确性成功表明,时间序列预测确实是一个复杂的任务,可能从捕捉复杂的非线性交互中获益。
资源和参考文献
-
PatchTST Github 仓库:
github.com/yuqinie98/PatchTST
-
PatchTST 的实现可以在 NeuralForecast 中找到:
nixtla.github.io/neuralforecast/
-
如果你对非 Transformer 的神经预测架构感兴趣,可以考虑阅读我之前的文章《神经基础分析网络》:
towardsdatascience.com/xai-for-forecasting-basis-expansion-17a16655b6e4
如果你对预测、深度学习和可解释人工智能感兴趣,可以通过关注我来支持我的写作!
参考文献
[1] A. Zeng, M. Chen, L. Zhang, Q. Xu. Transformers 对时间序列预测是否有效?(2022)。第三十七届 AAAI 人工智能会议。
[2] Y. Nie, N. H. Nguyen, P. Sinthong 和 J. Kalagnanam. 《一个时间序列值 64 个单词:使用 Transformers 的长期预测》(2023)。国际学习表征会议,2023。
[3] A. Vaswani, N. Shazeer, N. Parmar, J. Uszkoreit, L. Jones, A. N. Gomez, L. Kaiser, I. Polosukhin. 注意力即你所需(2017)。第 31 届神经信息处理系统会议。