TowardsDataScience-2023-博客中文翻译-九-
TowardsDataScience 2023 博客中文翻译(九)
使用 Apache Spark 在 PB 规模上构建数据湖
原文:
towardsdatascience.com/building-a-data-lake-on-pb-scale-with-apache-spark-1622d7073d46
我们在 Emplifi 如何处理大数据
·发布于 Towards Data Science ·15 分钟阅读·2023 年 1 月 26 日
--

图片由Victor Hanacek在picjumbo提供
在职业上,我在 Emplifi(前身为 Socialbakers)公司的数据工程团队中度过了过去四年,其中一个我参与的最大项目是构建一个分布式数据存储系统,目前该系统存储了近一 PB 的数据,目的是为数据分析师和研究人员提供可以高效分析和研究的数据表。正如你可以想象的那样——构建和维护这样一个数据湖并非易事,因为数据不仅频繁变化,而且其架构随着时间的推移而演变,拥有几十个甚至几百个具有不同嵌套层级的字段。
在这篇文章中,我想分享我在这段旅程中的经验和亮点,主要集中在技术层面。
我们每天处理的数据来源于社交网络,如 Facebook、Twitter、Instagram、YouTube、LinkedIn 或 TikTok。处理的数据集主要是这些网络上的公共档案和发布的帖子。一小部分数据也来自内部系统。我们的存储系统建立在 S3 AWS 上,我们称之为数据湖,因为我们在这里以原始格式以及预处理、处理和聚合后的形式存储数据。原始数据主要以压缩的 JSON 文件形式存在,而处理和聚合的数据集则以 Apache Parquet 格式存储,并以(Hive metastore)表格形式向用户公开。
数据流
数据经过公司基础设施的多个点,首先,它通过公共和在某些情况下也通过私有 API 从社交网络下载。接下来,它到达 DynamoDB,这是 AWS 上的一个键值分布式数据库服务,我们称之为主数据库。DynamoDB 擅长处理记录的频繁更新,这对我们的用例非常有用,因为数据来自社交网络,其中每条记录(如 post)随着时间的推移而演变——它收集互动并频繁变化。每次在 DynamoDB 中的更新也会在我们数据湖的stage层保存到 S3。这是 Apache Spark 中一系列步骤的起点,这些步骤将生成一致的表,数据分析师和研究人员可以使用标准分析工具高效访问。接下来的章节,我将更详细地描述这些步骤。
数据的结构
传入到stage的数据是以压缩 JSON 文件保存的连续记录流。每条记录有两个子结构——oldImage和newImage,其中第一个是记录更新前的状态,而后者包含更新后的数据。每条记录(post)可能在一天内被多次下载自社交网络,因此这个流中包含重复项,即每个id唯一标识的 post 可能出现多次。我们需要只考虑每个id的最新version来正确更新最终表。这里可以看到数据结构的一个简单示例:
{
'newImage': {
'id': 1,
'version': 100,
'created_time': '2023-01-01: 10:00:00',
'interactions': 50,
MANY OTHER FIELDS...
}
'oldImage': {
'id': 1,
'version': 101,
'created_time': '2023-01-01: 10:00:00',
'interactions': 51,
MANY OTHER FIELDS...
}
}
数据湖——三层抽象
我们的数据湖构建在 S3(AWS 上的分布式对象存储)上,并有三层抽象,我们称之为stage、target和mart。在stage层,我们保存主要来自 DynamoDB 的原始数据,但也来自其他数据库,如 Elasticsearch、Postgres、MongoDB,以及各种内部 API。这些数据主要以压缩 JSON 格式到达,有时也以 CSV 格式到达。这些数据集每天进行预处理,并以 Apache Parquet 格式保存于stage中。
这些 Parquet 文件接下来会被处理,并用于更新target中的表,更新可以是每日一次,也有些情况下——数据新鲜度不那么关键——每周一次。与stage数据不同,target表由我们的数据用户直接访问。
对于一些数据结构不太友好的表,我们创建新的表来转换数据,以便于查询,并将其保存到mart层——我们不在target层这样做,因为这里我们希望保持表作为数据来源系统的镜像,以避免用户混淆,例如,DynamoDB 中的数据与target中的数据不同。

数据湖的结构(图片由作者制作)
对我们来说,使表格在查询时高效是一个重要优先事项。数据分析师不得不等待几分钟才能完成查询是非常痛苦的。我们在两个层面上以不同的成功程度实现了这一点:
-
在 target 中:我们的手脚会有些束缚,因为我们保证数据与原始数据库中的结构相同。然而,我们可以通过自定义文件组织来实现相当大的成果,例如使用分区、桶和排序等技术。有关更多细节,请参见下一节。
-
在 mart 中:我们可以做得更多,简化嵌套结构,并根据对表执行的典型查询进行预连接或预聚合。
阶段
stage 是数据最终暴露给用户的入口点。这里传入的原始数据每天都会经过以下 3 个处理步骤:
-
模式演变
-
数据清理
-
数据去重
模式演变——阶段层
来自数据的结构随时间发生变化并不罕见——数据集中可能会添加新字段,甚至某些字段的数据类型可能会改变。如果没有一个健全的模式演变步骤,这将是一个相当大的问题。Apache Spark 允许你通过提供模式来读取数据,或者让 Spark 推断它(使用所谓的 schema-on-read)。
通常,你对模式应该是什么有一些期望(理想情况下与已经存在的 target 表的模式相同),但是将此模式提供给 Spark 会导致所有新字段可能因为某些原因在数据的新增量中缺失。此外,如果某个字段的数据类型发生变化,Spark 将会将其读取为 NULL(如果这两种数据类型不兼容),或者会丢弃这些行,或者根据指定的 模式 完全失败(另见我的另一篇 文章 关于 JSON 模式演变)。另一方面,让 Spark 推断模式似乎更合理,但也有一些缺点和需要考虑的点:
-
时间戳将被推断为字符串(除了 Spark 3.0 版本,它将被推断为时间戳)
-
map 类型被推断为结构体
-
如果没有其他逻辑,所有新字段可能会传播到最终表中,这可能是不希望的——有人应该确认新字段是否可以暴露给用户(也许新字段只是由于某些错误产生的)
-
如果没有其他逻辑,如果数据类型与 target 表中的类型不同,将增量合并到 target 表中将会失败
我们解决这个问题的方法是使用我们内部开发的架构比较工具。我们为所有数据集的架构建立了版本控制系统(架构注册表)。在处理过程中,我们首先让 Spark 推断架构,然后将其与版本化的架构进行比较。它会检测所有新字段并在一些通知渠道中报告。如果数据类型发生变化,算法会决定这些新类型是否可以安全地转换为版本化类型而不会丢失数据。基于这种比较,我们创建了一个新的架构,并使用这个新架构重新读取数据,将变化的字段转换为版本化字段。
唯一的问题发生在新数据类型无法安全转换时,此时我们会使用新的数据类型,但在通知渠道中报告错误,因为这意味着在目标层的后续作业将会因为这个问题而失败。在这种情况下,我们需要手动调查为什么会发生这种情况,是否只是由于某些错误导致的单次更改,或者是数据结构中的永久性更改,这意味着我们需要相应地调整最终表格。

我们的架构注册表提供了 API 和 Web 用户界面。在这里,你可以看到来自 Web UI 的截图,其中比较了一个数据源的两个架构版本。版本 1.0.6 中存在而版本 1.0.5 中不存在的新字段以绿色突出显示。
数据清洗
在使用演化后的架构读取数据到 DataFrame 并转换类型后,我们应用一系列过滤器,排除某些关键字段(如id、version、created_time等)中具有NULL值的记录。这些字段对于数据的正确去重和在目标层的最终表格中的正确合并是必要的。我们将这些记录称为损坏记录,并将其保存在一个单独的文件夹中,以便调查为什么缺少这些值。
数据去重
如前所述,stage中的数据可能包含多个具有相同id的记录版本。这是因为个别记录(主要是来自社交网络的帖子)不断变化,我们一天内从社交网络下载这些记录多次。
stage层的处理作业在清晨运行,目标是获取前一天的增量(新数据),并为将数据合并到最终表格的目标作业做准备。在架构演化和数据清洗之后,我们还需要进行去重,即对于每个id,我们将仅保留最新版本:
increment_df = (
spark.read
.format('json')
.option('path', input_path)
.schema(evolved_schema)
.load()
.select('newImage.*')
)
w = Window().partitionBy('id').orderBy(desc('version'))
result_df = (
increment_df
.withColumn('r', row_number().over(w))
.filter(col('r') == 1)
.drop('r')
)
从代码中可以看出,我们仅从newImage中选择数据,并使用窗口函数row_number()进行去重(有关在 Spark 中使用窗口函数的详细信息,请参见我另一篇文章)。
目标
目标层的目标是创建与 DynamoDB、Elasticsearch 和其他内部数据库镜像的表,这些表可以通过标准分析工具直接访问,如 SQL、Spark 中的 DataFrame API 或 Pandas,以及 Python 数据科学生态系统中的所有库都可以在其上使用。请注意,通过直接访问原始数据库实现这一点将非常困难。
从技术上讲,在目标中,我们从阶段获取 parquet 文件,并将其插入到现有表中。我们还希望为分析师提供准备好的分析查询表,因此在创建表的布局时我们投入了一些精力。表本身只是对文件的某种抽象,我们再次使用 Apache Parquet 格式。
表布局
我们使用写时复制的概念,这意味着在每次更新时,整个表和所有文件都会被覆盖。这是相当昂贵的,特别是对于大型表,然而,它允许我们保持文件的紧凑和组织,以满足高效读取的要求。
表示社交网络帖子的数据表按时间维度进行分区,典型的分区列是created_year和created_month,这两个列都是从帖子的created_time派生出来的。这种分区加快了数据的读取,因为分析师通常对一些近期数据感兴趣。因此,处理引擎如 Spark 或 Presto 确保只扫描查询的分区,而跳过其他分区。这些表还按列profile_id进行分桶,因为它可以与表示个人资料的表在此列上连接。对该列进行聚合和连接的查询不会引起 Spark 中的数据洗牌,这样可以提高此类查询的总体效率。此外,如果查询包含对profile_id列的过滤条件,则可以进行分桶修剪(有关分桶的更多信息,请参见我另一篇文章)。
合并增量
target层中的处理作业在stage作业后清晨运行,其目标是将前一天的增量合并到target表中。我们将增量和表都读取到 DataFrames 中,将它们联合,并使用窗口函数row_number — 类似于我们在stage中所做的 — 选择将保存到新快照中的最新记录版本。因此,如果特定的id已经在表中,它将被来自增量的具有更大version的记录所替代:
stage_df = (
spark.read
.format('parquet')
.schema(input_schema)
.option(input_path)
.load()
)
target_df = spark.table(table_name)
w = Window().partitionBy('id').orderBy(desc('version'))
result_df = (
stage_df.unionByName(target_df)
.withColumn('r', row_number().over(w))
.filter(col('r') == 1)
.drop('r')
)
然而,如果stage_df和target_df的模式不同,这种方法将不起作用,因此我们需要再次应用模式演变步骤。
模式演变 — 目标层
同样地,在stage中我们对模式进行了演变,我们也需要在target中进行相同的操作。要成功将增量合并到target表中,两个表的模式必须相同。然而,如果增量中添加了新字段,这种情况可能不会成立。
我们使用一种称为input_schema的模式来控制这些新字段是否已添加到表中。这个input_schema也有版本,如果我们想将新字段推广到表中,我们会创建一个包含这些新字段的新版本的input_schema。这是一个手动步骤,考虑到应该有人确认这些新字段可以暴露给数据用户,这似乎是合理的。
接下来,我们还需要将表的模式更改为与增量的模式相同。Spark SQL 提供了一种使用ALTER TABLE table_name ADD COLUMNS向表中添加新列的方法,但它没有提供添加嵌套字段的方法。
因此,我们将这一功能自行实现到我们的框架中。在这种情况下,target作业会在一个临时位置创建一个具有新模式的空表。之后,我们删除原始表,并将空表指向原始表的数据位置。这样,我们将获得一个具有相同数据但模式已修改的新表:
(
spark.createDataFrame([], new_schema)
.write
.mode('overwrite')
.option('path', table_location_temp)
.saveAsTable(table_name_temp)
)
spark.sql('ALTER TABLE table_name_temp SET LOCATION table_location')
spark.sql('MSCK REPAIR TABLE table_name_temp')
spark.sql('DROP TABLE table_name')
spark.sql('ALTER TABLE table_name_temp RENAME TO table_name')
原子写入
从我们开发数据湖的初期开始,最大的挑战之一就是确保原子写入到位。问题是,当简单地覆盖一个表时...
(
df.write
.mode('overwrite')
.format('parquet')
.option('path', output_path)
.saveAsTable(table_name)
)
…不是原子的。如果 Spark 作业因任何原因在写入过程中失败,表可能会停止存在,或者 S3 前缀可能会开始包含部分写入的数据。在我们的框架中,我们通过始终将表保存在不同的位置并以新名称table_name_temp保存来实现原子性,在写入成功后,我们交换表名称,因此新的快照只有在成功写入后才会对用户可用。如果作业失败,我们不会交换名称,而是从头开始在新位置重新启动过程:
output_path = posixpath.join(output_path, str(int(time.time())))
(
df.write
.mode('overwrite')
.format('parquet')
.option('path', output_path)
.saveAsTable(table_name_tmp)
)
spark.sql('DROP TABLE IF EXISTS table_name')
spark.sql('ALTER TABLE table_name_tmp RENAME TO table_name')
我们保存数据的路径包含写入时的时间戳,并且我们始终保留最近的几个快照,这使得所谓的时间旅行成为可能——如果我们发现新创建的表因某些原因(由于某些错误)而损坏,我们可以将其指向任何之前的快照。
数据质量
目标 表中的数据的一致性和质量至关重要,因为这些表直接暴露给数据用户。这可以通过在每次更新后检查表格来实现,以确保数据没有由于代码中的某些错误、原始数据库的错误导出或数据在经过转换步骤后到达最终表时可能发生的其他问题而被破坏。
我们使用的数据质量框架是Great Expectations,它可以与 Spark 集成。对于每个数据集,我们可以定义一组期望,这些期望会在创建表的新快照后进行验证。如果一些关键期望未能满足,我们可以将表“时间旅行”到之前的快照,并详细调查失败的期望。
Mart
尽管从目标 表中访问数据相当高效,但对于某些表来说,这并不是很用户友好。这是自然的,因为,如前所述,我们保持目标 表中的数据结构与数据在原始数据库中存储的结构相同,而这些数据库不一定是为分析目的存储的。这主要涉及嵌套数据结构中的列,例如数组或结构体。有时,使用 SQL 的人们在查询中转换这些字段可能太繁琐,因此我们创建了从目标 表派生的附加表,以便在这些表中转换数据结构,从而更容易进行查询。有时这些表也可以与其他表连接,并且它们是为某些特定用户群体量身定制的。这些派生表被保存在湖泊的mart 层中。mart 的另一个目的是保留由分析师计算的一些汇总和结果表。
环境、作业、协调
在stage、target 和mart 层中的所有处理都是用 PySpark 实现的,并且在 Python 中实现了处理模式演变的自定义逻辑。代码在Databricks平台上运行,对于每个数据源,我们在stage 层和target 层都有一个作业,对于某些数据源,还有mart 层的作业。所有作业都使用 Apache Airflow 技术进行协调,这使我们能够定义作业之间的依赖关系。
去重、模式演变、原子数据保存等核心逻辑都在类和模块中实现,并编译成一个轮子(wheel),这个轮子被导入到任务中,以便基于软件工程的最佳实践复用代码。
湖中的所有步骤基本上都是自动化的,仅有少数几个与引入新数据源相关的手动步骤。在这种情况下,我们只需在配置文件中添加一些配置参数,并将新数据源的模式添加到模式注册表中。Git 管道会在 Databricks 中创建作业,并通过 Airflow 按照预定的调度启动它们。其他手动步骤与监控和审批新字段以及对无法安全转换为版本化类型的更改数据类型进行调试相关。
如前所述,数据用户可以使用 SQL 或 Spark 中的 DataFrame API 访问目标和数据集市表,这些 API 可以方便地与 Pandas 集成,从而实现与整个 Python 数据科学和机器学习库生态系统的集成。喜欢使用 SQL 的分析师可以使用Querybook——一个用户友好的笔记本界面——它连接到 Presto 引擎,而喜欢使用 Spark 的科学家则可以在 Databricks 的笔记本环境中访问表格。

我们的数据湖用户(分析师和研究人员)可以在 Spark 上使用数据科学和机器学习库,或者使用 Presto 引擎用 SQL 查询数据。他们可以选择在 Databricks 和 Querybook 中工作,这两个平台都提供了具有用户友好界面的笔记本环境,适用于各种使用场景。
致谢
构建和维护数据湖是整个数据工程团队的协作工作。我想感谢所有同事,不仅感谢在数据湖项目中进行的有益且富有成效的合作,感谢我们在过去四年中提出的大量有趣的想法和意见,还要感谢对本文的审阅和有益的评论。
结论
在处理大数据的公司中,拥有一个强大且可靠的数据湖似乎是必需的。传统的数据仓库概念已不再适用,因为数据量庞大,并且来自具有复杂结构的不同数据库和系统。然而,在实现这样的存储系统时,仍然有几个挑战需要应对。
我们在关系数据库中认为理所当然的 ACID 事务在此已不再那么明显,必须处理存储系统中的低级概念,如文件组织。通过一些额外的努力,可以恢复 ACID——我们在原子性方面达到了这一点。另一种方法是使用更先进的表格格式,如 Delta 或 Iceberg,它们提供 ACID。
数据模式的变化,无论是由于导出过程中出现错误还是实体真的发生了变化,都是一个相当棘手的问题,如果不加以谨慎处理,可能会导致数据丢失。我们在内部开发了一个工具,我们考虑将来将其开源。这个工具允许我们对所有数据源进行模式版本控制,以便查看其模式如何演变的历史。它还允许我们比较模式,执行各种操作,并轻松处理模式演变。
维护数据湖需要与数据用户(通常是分析师、研究人员和数据科学家)进行持续讨论。了解对表执行了什么样的查询,使我们能够通过在分区、桶化甚至排序方面使用自定义布局来进行高度优化。
构建分子属性预测的图卷积网络
人工智能
制作分子图和开发一个基于 PyTorch 的简单 GCN 的教程
·
关注 发表在 Towards Data Science ·17 分钟阅读·2023 年 12 月 23 日
--
照片由 BoliviaInteligente 提供,来源于 Unsplash
人工智能在全球范围内引起了轰动。每周都会出现新的模型、工具和应用程序,承诺推动人类努力的边界。开放源代码工具的可用性使得用户能够在少量代码中训练和使用复杂的机器学习模型,真正实现了人工智能的民主化;同时,尽管许多这些现成的模型可能提供了出色的预测能力,但它们作为黑箱模型的使用可能会剥夺了对人工智能深入理解的好奇学生。特别是在自然科学中,这种理解尤为重要,因为知道一个模型准确是不够的——还必须了解它与其他物理理论的联系、其局限性以及它对其他系统的普遍适用性。在本文中,我们将通过化学的视角探讨一种特定的机器学习模型——图卷积网络。这并不是一个数学严格的探讨;相反,我们将尝试将网络的特征与传统自然科学模型进行比较,并思考它为何表现如此出色。
1. 对图形和图神经网络的需求
在化学或物理学中,模型通常是一个连续函数,比如 y=f(x₁, x₂, x₃, …, xₙ),其中 x₁, x₂, x₃, …, xₙ 是输入,y 是输出。这样的模型的一个例子是决定两个点电荷 q₁ 和 q₂ 之间的静电相互作用(或力)的方程,这两个点电荷在相对介电常数为 εᵣ 的介质中,相隔距离为 r,通常称为库仑定律。

图 1:库仑方程作为点电荷之间静电相互作用的模型(图像来源:作者)
如果我们不知道这种关系,但假设有多个数据点,每个数据点包括点电荷之间的相互作用(输出)和相应的输入,我们可以拟合一个人工神经网络来预测任何给定点电荷在指定介质中的任何给定分离下的相互作用。在这个问题的情况下,虽然忽略了一些重要的警告,但创建一个数据驱动的物理问题模型是相对简单的。
现在考虑从分子的结构预测某一特定性质的问题,比如在水中的溶解度。首先,没有明显的输入集来描述一个分子。你可以使用各种特征,如键长、键角、不同类型元素的数量、环的数量等等。然而,没有保证任何这样的任意集合对所有分子都有效。
其次,与点电荷的例子不同,输入可能不一定存在于连续空间中。例如,我们可以将甲醇、乙醇和丙醇视为一组链长逐渐增加的分子;然而,它们之间并不存在任何概念——链长是一个离散参数,没有办法在甲醇和乙醇之间进行插值以得到其他分子。拥有一个连续的输入空间对于计算模型的导数是至关重要的,这些导数随后可以用于优化所选属性。
为了克服这些问题,已经提出了各种编码分子的方法。其中一种方法是使用 SMILES 和 SELFIES 等方案进行文本表示。这种表示方法有大量文献资料,我推荐感兴趣的读者阅读这篇有用的综述。第二种方法涉及将分子表示为图形。虽然每种方法都有其优点和缺点,但图形表示对化学更直观。
图是由节点通过边连接组成的数学结构,边表示节点之间的关系。分子自然适应这种结构——原子成为节点,键成为边。图中的每个节点由一个向量表示,该向量编码了相应原子的属性。通常,一位编码方案就足够了(更多内容见下一节)。这些向量可以堆叠起来形成一个节点矩阵。节点之间的关系——由边表示——可以通过一个方形的邻接矩阵来划分,其中每个元素aᵢⱼ 取值为 1 或 0,取决于两个节点i 和 j 是否由边连接。对角线上的元素设置为 1,表示自连接,这使得矩阵适合卷积(如你将在下一节看到的)。可以开发更复杂的图形表示,其中边的属性也在一个单独的矩阵中进行一位编码,但我们将这些留待另一篇文章。这些节点和邻接矩阵将作为我们模型的输入。

图 2:将乙酰胺分子表示为图形,节点的原子序号通过一位编码表示(图片来源:作者)
通常,人工神经网络模型接受的是一维输入向量。对于多维输入,比如图像,开发了一类叫做卷积神经网络的模型。在我们的情况下,我们有二维矩阵作为输入,因此需要一个修改过的网络来接受这些输入。图神经网络是为了处理这样的节点和邻接矩阵而开发的,它们将这些矩阵转换为适当的一维向量,这些向量可以通过普通的人工神经网络的隐藏层来生成输出。图神经网络有许多类型,比如图卷积网络、消息传递网络、图注意力网络等等,它们主要在于节点和边之间交换信息的函数上有所不同。由于图卷积网络相对简单,我们将更详细地了解它们。
2. 图卷积和池化层
考虑你输入的初始状态。节点矩阵表示了每个原子的独热编码。为了简化起见,我们考虑原子序数的独热编码,其中原子序数为n的原子在nᵗʰ索引处有一个 1,其余位置都是 0。邻接矩阵表示节点之间的连接。在当前状态下,节点矩阵不能作为人工神经网络的输入,原因有以下几点:(1) 它是二维的,(2) 它不是排列不变的,(3) 它不是唯一的。这里的排列不变性意味着无论你如何排列节点,输入应该保持不变;目前,相同的分子可以由相同节点矩阵的多个排列表示(假设邻接矩阵也有适当的排列)。这是一个问题,因为网络会将不同的排列视为不同的输入,而它们应该被视为相同的。
对于前两个问题,有一个简单的解决方案——池化。如果节点矩阵沿列维度进行池化,那么它将被减少到一个排列不变的一维向量。通常,这种池化是简单的均值池化,这意味着最终池化后的向量包含节点矩阵中每一列的均值。然而,这仍然无法解决第三个问题——池化两个异构体的节点矩阵,例如正戊烷和新戊烷,将产生相同的池化向量。
为了使最终的池化向量具有唯一性,我们需要在节点矩阵中加入一些邻居信息。以同分异构体为例,虽然它们的化学式相同,但它们的结构却不同。加入邻居信息的一个简单方法是对每个节点及其邻居进行某种操作,例如求和。这可以表示为节点矩阵与邻接矩阵的乘法(试着在纸上计算:邻接矩阵与节点矩阵的乘积生成一个更新后的节点矩阵,其中每个节点向量等于它自身与邻居节点向量的和)。通常,通过用对角度矩阵的逆进行预乘,对每个节点的度(或邻居数量)进行归一化,从而使这一和值成为邻居的均值。最后,这个乘积会被一个权重矩阵后乘,以使这个操作具有参数化特性。这个完整的操作称为图卷积。图 3 显示了一种直观而简单的图卷积形式。一个数学上更严格且数值上更稳定的形式可以在Thomas Kipf 和 Max Welling 的研究中找到,该研究对邻接矩阵进行了修改的归一化。卷积和池化操作的组合也可以解释为一种非线性的经验群体贡献方法。

图 3:用于乙酰胺分子的图卷积(作者提供的图片)
图卷积网络的最终结构如下——首先,为给定的分子计算节点和邻接矩阵。然后对这些矩阵应用多次图卷积,并进行池化以生成一个包含所有分子信息的单一向量。随后,这个向量通过标准人工神经网络的隐藏层产生输出。隐藏层、池化层和卷积层的权重通过对基于回归的损失函数(如均方误差)应用反向传播同时确定。
3. 代码实现
在讨论了与图卷积网络相关的所有关键概念之后,我们准备开始使用 PyTorch 构建一个网络。虽然存在一个名为 PyTorch Geometric 的灵活且高性能的 GNN 框架,但我们不会使用它,因为我们的目标是深入了解其内部机制并发展我们的理解。
本教程分为四个主要部分——(1)使用 RDKit 自动创建图形,(2)将图形打包成 PyTorch 数据集,(3)构建图卷积网络架构,以及(4)训练网络。完整的代码以及安装和导入所需包的说明可以在文章末尾提供的 GitHub 仓库中找到链接。
3.1. 使用 RDKit 创建图形
RDKit 是一个化学信息学库,允许高通量访问小分子的性质。我们将需要它来完成两个任务——获取分子中每个原子的原子序数以进行节点矩阵的独热编码,并获取邻接矩阵。我们假设分子是通过其 SMILES 字符串提供的(这对于大多数化学信息学数据来说是正确的)。此外,为了确保所有分子的节点和邻接矩阵的大小一致——默认情况下它们的大小不一致,因为它们的大小依赖于分子中的原子数——我们用 0 填充这些矩阵。最后,我们将对上面提出的卷积进行小修改——我们将邻接矩阵中的“1”替换为相应的键长的倒数。这样,网络将获得更多关于分子几何的信息,并且还会根据邻居的键长来加权每个节点周围的卷积。
class Graph:
def __init__(
self, molecule_smiles: str,
node_vec_len: int,
max_atoms: int = None
):
# Store properties
self.smiles = molecule_smiles
self.node_vec_len = node_vec_len
self.max_atoms = max_atoms
# Call helper function to convert SMILES to RDKit mol
self.smiles_to_mol()
# If valid mol is created, generate a graph of the mol
if self.mol is not None:
self.smiles_to_graph()
def smiles_to_mol(self):
# Use MolFromSmiles from RDKit to get molecule object
mol = Chem.MolFromSmiles(self.smiles)
# If a valid mol is not returned, set mol as None and exit
if mol is None:
self.mol = None
return
# Add hydrogens to molecule
self.mol = Chem.AddHs(mol)
def smiles_to_graph(self):
# Get list of atoms in molecule
atoms = self.mol.GetAtoms()
# If max_atoms is not provided, max_atoms is equal to maximum number
# of atoms in this molecule.
if self.max_atoms is None:
n_atoms = len(list(atoms))
else:
n_atoms = self.max_atoms
# Create empty node matrix
node_mat = np.zeros((n_atoms, self.node_vec_len))
# Iterate over atoms and add to node matrix
for atom in atoms:
# Get atom index and atomic number
atom_index = atom.GetIdx()
atom_no = atom.GetAtomicNum()
# Assign to node matrix
node_mat[atom_index, atom_no] = 1
# Get adjacency matrix using RDKit
adj_mat = rdmolops.GetAdjacencyMatrix(self.mol)
self.std_adj_mat = np.copy(adj_mat)
# Get distance matrix using RDKit
dist_mat = molDG.GetMoleculeBoundsMatrix(self.mol)
dist_mat[dist_mat == 0.] = 1
# Get modified adjacency matrix with inverse bond lengths
adj_mat = adj_mat * (1 / dist_mat)
# Pad the adjacency matrix with 0s
dim_add = n_atoms - adj_mat.shape[0]
adj_mat = np.pad(
adj_mat, pad_width=((0, dim_add), (0, dim_add)), mode="constant"
)
# Add an identity matrix to adjacency matrix
# This will make an atom its own neighbor
adj_mat = adj_mat + np.eye(n_atoms)
# Save both matrices
self.node_mat = node_mat
self.adj_mat = adj_mat
3.2. 在 Dataset 中打包图
PyTorch 提供了一个便捷的Dataset类来存储和访问各种数据。我们将使用它来存储每个分子的节点和邻接矩阵及输出。请注意,使用这个Dataset接口来处理数据不是强制性的;不过,使用这个抽象会使后续步骤更加简单。我们需要为继承自Dataset类的GraphData类定义两个主要方法:一个是len方法来获取数据集的大小,另一个是getitem方法来获取给定索引的输入和输出。
class GraphData(Dataset):
def __init__(self, dataset_path: str, node_vec_len: int, max_atoms: int):
# Save attributes
self.node_vec_len = node_vec_len
self.max_atoms = max_atoms
# Open dataset file
df = pd.read_csv(dataset_path)
# Create lists
self.indices = df.index.to_list()
self.smiles = df["smiles"].to_list()
self.outputs = df["measured log solubility in mols per litre"].to_list()
def __len__(self):
return len(self.indices)
def __getitem__(self, i: int):
# Get smile
smile = self.smiles[i]
# Create MolGraph object using the Graph abstraction
mol = Graph(smile, self.node_vec_len, self.max_atoms)
# Get node and adjacency matrices
node_mat = torch.Tensor(mol.node_mat)
adj_mat = torch.Tensor(mol.adj_mat)
# Get output
output = torch.Tensor([self.outputs[i]])
return (node_mat, adj_mat), output, smile
由于我们已经定义了自己定制的节点和邻接矩阵、输出以及 SMILES 字符串的返回方式,我们需要定义一个自定义函数来整理数据,即将数据打包成一个批次,然后传递给网络。通过传递数据批次而不是单个数据点,并使用小批量梯度下降来训练神经网络,可以在准确性和计算效率之间取得微妙的平衡。我们将在下面定义的整理函数本质上会收集所有数据对象,将它们按类别分层,堆叠在列表中,转换为 PyTorch 张量,并重新组合这些张量,以便以与我们的GraphData类相同的方式返回它们。
def collate_graph_dataset(dataset: Dataset):
# Create empty lists of node and adjacency matrices, outputs, and smiles
node_mats = []
adj_mats = []
outputs = []
smiles = []
# Iterate over list and assign each component to the correct list
for i in range(len(dataset)):
(node_mat,adj_mat), output, smile = dataset[i]
node_mats.append(node_mat)
adj_mats.append(adj_mat)
outputs.append(output)
smiles.append(smile)
# Create tensors
node_mats_tensor = torch.cat(node_mats, dim=0)
adj_mats_tensor = torch.cat(adj_mats, dim=0)
outputs_tensor = torch.stack(outputs, dim=0)
# Return tensors
return (node_mats_tensor, adj_mats_tensor), outputs_tensor, smiles
3.3. 构建图卷积网络架构
完成数据处理部分的代码后,我们现在转向构建模型本身。为了清晰起见,我们将构建自己的卷积层和池化层,但你们中更高级的开发者可以轻松地用 PyTorch Geometric 模块中更复杂的预定义层替换这些层。ConvolutionLayer本质上做三件事——(1)从邻接矩阵计算逆对角度矩阵,(2)对四个矩阵(D⁻¹ANW)进行乘法运算,以及(3)对层输出应用非线性激活函数。与其他 PyTorch 类一样,我们将从已经定义了forward方法等方法的Module基类继承。
class ConvolutionLayer(nn.Module):
def __init__(self, node_in_len: int, node_out_len: int):
# Call constructor of base class
super().__init__()
# Create linear layer for node matrix
self.conv_linear = nn.Linear(node_in_len, node_out_len)
# Create activation function
self.conv_activation = nn.LeakyReLU()
def forward(self, node_mat, adj_mat):
# Calculate number of neighbors
n_neighbors = adj_mat.sum(dim=-1, keepdims=True)
# Create identity tensor
self.idx_mat = torch.eye(
adj_mat.shape[-2], adj_mat.shape[-1], device=n_neighbors.device
)
# Add new (batch) dimension and expand
idx_mat = self.idx_mat.unsqueeze(0).expand(*adj_mat.shape)
# Get inverse degree matrix
inv_degree_mat = torch.mul(idx_mat, 1 / n_neighbors)
# Perform matrix multiplication: D^(-1)AN
node_fea = torch.bmm(inv_degree_mat, adj_mat)
node_fea = torch.bmm(node_fea, node_mat)
# Perform linear transformation to node features
# (multiplication with W)
node_fea = self.conv_linear(node_fea)
# Apply activation
node_fea = self.conv_activation(node_fea)
return node_fea
接下来,我们构造PoolingLayer。该层只执行一个操作,即沿第二维度(节点数量)计算均值。
class PoolingLayer(nn.Module):
def __init__(self):
# Call constructor of base class
super().__init__()
def forward(self, node_fea):
# Pool the node matrix
pooled_node_fea = node_fea.mean(dim=1)
return pooled_node_fea
最后,我们将定义一个ChemGCN类,包含卷积层、池化层和隐藏层的定义。通常,这个类应该有一个构造函数来定义这些层的结构和顺序,以及一个forward方法,接受输入(在我们的情况下是节点和邻接矩阵)并生成输出。我们将对所有层的输出应用LeakyReLU激活函数。此外,我们还将使用 dropout 来减少过拟合。
class ChemGCN(nn.Module):
def __init__(
self,
node_vec_len: int,
node_fea_len: int,
hidden_fea_len: int,
n_conv: int,
n_hidden: int,
n_outputs: int,
p_dropout: float = 0.0,
):
# Call constructor of base class
super().__init__()
# Define layers
# Initial transformation from node matrix to node features
self.init_transform = nn.Linear(node_vec_len, node_fea_len)
# Convolution layers
self.conv_layers = nn.ModuleList(
[
ConvolutionLayer(
node_in_len=node_fea_len,
node_out_len=node_fea_len,
)
for i in range(n_conv)
]
)
# Pool convolution outputs
self.pooling = PoolingLayer()
pooled_node_fea_len = node_fea_len
# Pooling activation
self.pooling_activation = nn.LeakyReLU()
# From pooled vector to hidden layers
self.pooled_to_hidden = nn.Linear(pooled_node_fea_len, hidden_fea_len)
# Hidden layer
self.hidden_layer = nn.Linear(hidden_fea_len, hidden_fea_len)
# Hidden layer activation function
self.hidden_activation = nn.LeakyReLU()
# Hidden layer dropout
self.dropout = nn.Dropout(p=p_dropout)
# If hidden layers more than 1, add more hidden layers
self.n_hidden = n_hidden
if self.n_hidden > 1:
self.hidden_layers = nn.ModuleList(
[self.hidden_layer for _ in range(n_hidden - 1)]
)
self.hidden_activation_layers = nn.ModuleList(
[self.hidden_activation for _ in range(n_hidden - 1)]
)
self.hidden_dropout_layers = nn.ModuleList(
[self.dropout for _ in range(n_hidden - 1)]
)
# Final layer going to the output
self.hidden_to_output = nn.Linear(hidden_fea_len, n_outputs)
def forward(self, node_mat, adj_mat):
# Perform initial transform on node_mat
node_fea = self.init_transform(node_mat)
# Perform convolutions
for conv in self.conv_layers:
node_fea = conv(node_fea, adj_mat)
# Perform pooling
pooled_node_fea = self.pooling(node_fea)
pooled_node_fea = self.pooling_activation(pooled_node_fea)
# First hidden layer
hidden_node_fea = self.pooled_to_hidden(pooled_node_fea)
hidden_node_fea = self.hidden_activation(hidden_node_fea)
hidden_node_fea = self.dropout(hidden_node_fea)
# Subsequent hidden layers
if self.n_hidden > 1:
for i in range(self.n_hidden - 1):
hidden_node_fea = self.hidden_layersi
hidden_node_fea = self.hidden_activation_layersi
hidden_node_fea = self.hidden_dropout_layersi
# Output
out = self.hidden_to_output(hidden_node_fea)
return out
3.4. 网络训练
我们已经构建了训练模型和进行预测所需的工具。在这一部分,我们将编写辅助函数来训练和测试我们的模型,并编写脚本以运行生成图表、构建网络和训练模型的工作流程。
首先,我们定义一个Standardizer类来标准化我们的输出。神经网络更喜欢处理相对较小且相互之间变化不大的数字。标准化有助于达到这一点。
class Standardizer:
def __init__(self, X):
self.mean = torch.mean(X)
self.std = torch.std(X)
def standardize(self, X):
Z = (X - self.mean) / (self.std)
return Z
def restore(self, Z):
X = self.mean + Z * self.std
return X
def state(self):
return {"mean": self.mean, "std": self.std}
def load(self, state):
self.mean = state["mean"]
self.std = state["std"]
其次,我们定义一个函数来执行每个 epoch 的以下步骤:
-
从数据加载器中解包输入和输出,并将其传输到 GPU(如果可用)。
-
通过网络传递输入并获得预测结果。
-
计算预测值与输出之间的均方误差。
-
执行反向传播并更新网络的权重。
-
对其他批次重复上述步骤。
该函数返回批量平均损失和均值绝对误差,可用于绘制损失曲线。一个类似的没有反向传播的函数用于测试模型。
def train_model(
epoch,
model,
training_dataloader,
optimizer,
loss_fn,
standardizer,
use_GPU,
max_atoms,
node_vec_len,
):
# Create variables to store losses and error
avg_loss = 0
avg_mae = 0
count = 0
# Switch model to train mode
model.train()
# Go over each batch in the dataloader
for i, dataset in enumerate(training_dataloader):
# Unpack data
node_mat = dataset[0][0]
adj_mat = dataset[0][1]
output = dataset[1]
# Reshape inputs
first_dim = int((torch.numel(node_mat)) / (max_atoms * node_vec_len))
node_mat = node_mat.reshape(first_dim, max_atoms, node_vec_len)
adj_mat = adj_mat.reshape(first_dim, max_atoms, max_atoms)
# Standardize output
output_std = standardizer.standardize(output)
# Package inputs and outputs; check if GPU is enabled
if use_GPU:
nn_input = (node_mat.cuda(), adj_mat.cuda())
nn_output = output_std.cuda()
else:
nn_input = (node_mat, adj_mat)
nn_output = output_std
# Compute output from network
nn_prediction = model(*nn_input)
# Calculate loss
loss = loss_fn(nn_output, nn_prediction)
avg_loss += loss
# Calculate MAE
prediction = standardizer.restore(nn_prediction.detach().cpu())
mae = mean_absolute_error(output, prediction)
avg_mae += mae
# Set zero gradients for all tensors
optimizer.zero_grad()
# Do backward prop
loss.backward()
# Update optimizer parameters
optimizer.step()
# Increase count
count += 1
# Calculate avg loss and MAE
avg_loss = avg_loss / count
avg_mae = avg_mae / count
# Print stats
print(
"Epoch: [{0}]\tTraining Loss: [{1:.2f}]\tTraining MAE: [{2:.2f}]"\
.format(
epoch, avg_loss, avg_mae
)
)
# Return loss and MAE
return avg_loss, avg_mae
最后,我们编写整体工作流程。这个脚本将调用我们之前定义的所有内容。
#### Fix seeds
np.random.seed(0)
torch.manual_seed(0)
use_GPU = torch.cuda.is_available()
#### Inputs
max_atoms = 200
node_vec_len = 60
train_size = 0.7
batch_size = 32
hidden_nodes = 60
n_conv_layers = 4
n_hidden_layers = 2
learning_rate = 0.01
n_epochs = 50
#### Start by creating dataset
main_path = Path(__file__).resolve().parent
data_path = main_path / "data" / "solubility_data.csv"
dataset = GraphData(dataset_path=data_path, max_atoms=max_atoms,
node_vec_len=node_vec_len)
#### Split data into training and test sets
# Get train and test sizes
dataset_indices = np.arange(0, len(dataset), 1)
train_size = int(np.round(train_size * len(dataset)))
test_size = len(dataset) - train_size
# Randomly sample train and test indices
train_indices = np.random.choice(dataset_indices, size=train_size,
replace=False)
test_indices = np.array(list(set(dataset_indices) - set(train_indices)))
# Create dataoaders
train_sampler = SubsetRandomSampler(train_indices)
test_sampler = SubsetRandomSampler(test_indices)
train_loader = DataLoader(dataset, batch_size=batch_size,
sampler=train_sampler,
collate_fn=collate_graph_dataset)
test_loader = DataLoader(dataset, batch_size=batch_size,
sampler=test_sampler,
collate_fn=collate_graph_dataset)
#### Initialize model, standardizer, optimizer, and loss function
# Model
model = ChemGCN(node_vec_len=node_vec_len, node_fea_len=hidden_nodes,
hidden_fea_len=hidden_nodes, n_conv=n_conv_layers,
n_hidden=n_hidden_layers, n_outputs=1, p_dropout=0.1)
# Transfer to GPU if needed
if use_GPU:
model.cuda()
# Standardizer
outputs = [dataset[i][1] for i in range(len(dataset))]
standardizer = Standardizer(torch.Tensor(outputs))
# Optimizer
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
# Loss function
loss_fn = torch.nn.MSELoss()
#### Train the model
loss = []
mae = []
epoch = []
for i in range(n_epochs):
epoch_loss, epoch_mae = train_model(
i,
model,
train_loader,
optimizer,
loss_fn,
standardizer,
use_GPU,
max_atoms,
node_vec_len,
)
loss.append(epoch_loss)
mae.append(epoch_mae)
epoch.append(i)
#### Test the model
# Call test model function
test_loss, test_mae = test_model(model, test_loader, loss_fn, standardizer,
use_GPU, max_atoms, node_vec_len)
#### Print final results
print(f"Training Loss: {loss[-1]:.2f}")
print(f"Training MAE: {mae[-1]:.2f}")
print(f"Test Loss: {test_loss:.2f}")
print(f"Test MAE: {test_mae:.2f}")
就这样!运行这个脚本应该会输出训练和测试的损失和错误。
4. 结果
具有给定架构和超参数的网络在开源的DeepChem 库上训练,该库包含约 1000 种小分子的水溶性。下图显示了一个特定训练-测试划分的训练损失曲线和测试集的对比图。训练集和测试集上的平均绝对误差分别为 0.59 和 0.58(以 log mol/l 为单位),低于线性模型的 0.69 log mol/l(基于数据集中的预测)。神经网络表现优于线性回归模型并不令人意外;尽管如此,这种粗略的比较使我们确信模型的预测是合理的。此外,我们仅通过在图中包含基本的结构描述符——原子序数和键长——来实现这一点,让卷积和池化函数建立这些描述符之间更复杂的关系,从而得出最准确的分子性质预测。

图 4:测试集的训练损失曲线(左)和对比图(右)(图像由作者提供)
5. 最后的说明
这绝不是解决所选问题的最终模型。改进模型的方式有很多,包括:
-
优化超参数
-
使用早停策略找到具有最低验证损失的模型
-
使用更复杂的卷积和池化函数
-
收集更多数据
尽管如此,本教程的目标是通过一个简单的例子阐述化学领域图卷积网络的基础知识。在掌握了基础知识后,你在 GCN 模型构建之旅中的可能性是无限的。
仓库和有用的参考资料
-
完整的代码(包括创建图形的脚本)提供在GitHub 仓库中。安装所需模块的说明也提供在那里。用于训练模型的数据集来自开源的DeepChem 库,该库在 MIT 许可下(允许商业使用)。仓库中的原始数据集文件名为 delaney_processed.csv。
-
关于图卷积网络的研究文章。本文介绍的卷积函数是本文中给出的函数的简化和更直观的形式。
-
关于消息传递神经网络的研究文章。这些是更为通用和富有表现力的图神经网络。可以证明,图卷积网络是具有特定类型消息函数的消息传递神经网络。
-
关于分子深度学习的在线书籍。这是一个极好的资源,可以帮助你学习化学深度学习的基础知识,并通过动手编码练习应用所学。
如果你有任何问题、评论或建议,请随时通过电子邮件联系我或通过X 联系我。
使用 Streamlit 构建 LAS 文件数据探索应用
原文:
towardsdatascience.com/building-a-las-file-data-explorer-app-with-streamlit-347289e0d000
使用 Python 和 Streamlit 探索 Log ASCII Standard 文件
·发布于 Towards Data Science ·14 min 阅读·2023 年 2 月 3 日
--

照片由 Carlos Muza 提供,Unsplash 上的
LAS 文件是石油和天然气行业中传输和存储井日志和/或岩石物理数据的标准且简单的方式。该格式在 80 年代末和 90 年代初由 加拿大井日志学会 开发,旨在标准化和组织数字日志信息。LAS 文件本质上是结构化的 ASCII 文件,包含多个部分,其中有关于井及其数据的信息;因此,它们可以在典型的文本编辑器中轻松查看,如记事本或 TextEdit。
Streamlit 是我最喜欢的 Python 库之一,用于创建快速且易于使用的仪表板或交互式工具。如果你想创建一个应用程序,使你或最终用户无需担心代码,它也非常棒。因此,在本文中,我们将深入了解如何使用 Streamlit 构建 LAS 文件的数据探索应用。
如果你想查看完整的应用演示,请查看下面的短视频。
或者在 GitHub 上探索源代码:
[## GitHub - andymcdgeo/las_explorer: LAS Explorer 是一个 Streamlit 网络应用,允许你理解…
LAS Explorer 是一个 Streamlit 网络应用,允许你理解 LAS 文件的内容。还包括…
github.com](https://github.com/andymcdgeo/las_explorer?source=post_page-----347289e0d000--------------------------------)
如果你想了解如何在 Python 中处理 LAS 文件,以下文章可能会引起你的兴趣:
安装和设置 Streamlit
我们应用的第一部分将涉及导入所需的库和模块。
这些是:
导入这些库后,我们可以在最后添加一行代码,将页面宽度设置为全页,并更改浏览器窗口中的应用标题。
import streamlit as st
import lasio
import pandas as pd
from io import StringIO
# Plotly imports
from plotly.subplots import make_subplots
import plotly.graph_objects as go
import plotly.express as px
st.set_page_config(layout="wide", page_title='LAS Explorer v.0.1')
为了检查 Streamlit 是否正常工作,我们可以在终端中运行以下命令:
streamlit run app.py
这将打开一个浏览器窗口,显示一个空白的 Streamlit 应用。

空白的 Streamlit 应用。图片来源:作者。
使用 st.file_uploader 加载 LAS 文件
我们要添加到应用中的第一段代码是调用 st.sidebar。这将创建一个位于应用左侧的列,我们将用它来存储我们的导航菜单和文件上传小部件。
st.sidebar.write('# LAS Data Explorer')
st.sidebar.write('To begin using the app, load your LAS file using the file upload option below.')
我们可以使用 st.sidebar.write 添加一些消息和说明给最终用户。在这个示例中,我们将保持相对简单,提供应用名称和如何开始的消息。
一旦侧边栏到位,我们可以开始实现文件上传器的代码部分。
las_file=None
uploadedfile = st.sidebar.file_uploader(' ', type=['.las'])
las_file, well_data = load_data(uploadedfile)
if las_file:
st.sidebar.success('File Uploaded Successfully')
st.sidebar.write(f'<b>Well Name</b>: {las_file.well.WELL.value}',
unsafe_allow_html=True)
为此,我们需要调用 st.file_uploader。我们还将限制文件类型为 .las 文件。为了更实用,我们可能还希望包含大写版本的扩展名。
接下来,我们将调用 load data 函数,稍后我们将详细介绍。该函数将设置为返回 las_file 作为一个 lasio las 文件对象,以及 well_data 作为包含井日志测量数据的数据框。
随后,我们将检查是否有 las 文件。如果设置为 None,则不会发生任何事情;然而,如果文件通过 load_data 函数成功加载,则它不会是 None,因此会执行下面的代码。
if 函数中的代码本质上显示了一个彩色标注,后跟 las 文件的井名称。
在运行 Streamlit 应用之前,我们需要创建 load_data 函数。这将允许我们读取数据并生成 lasio las 文件对象和 pandas 数据框。
@st.cache
def load_data(uploaded_file):
if uploaded_file is not None:
try:
bytes_data = uploaded_file.read()
str_io = StringIO(bytes_data.decode('Windows-1252'))
las_file = lasio.read(str_io)
well_data = las_file.df()
well_data['DEPTH'] = well_data.index
except UnicodeDecodeError as e:
st.error(f"error loading log.las: {e}")
else:
las_file = None
well_data = None
return las_file, well_data
当我们运行 Streamlit LAS 数据浏览器应用时,我们将看到左侧的侧边栏以及文件上传小部件。

添加侧边栏到 LAS 文件数据浏览器 Streamlit 应用后。图片由作者提供。
然后我们可以点击浏览文件并搜索一个 las 文件。
一旦该文件被加载,我们将看到绿色的提示,表示文件加载成功,接着是文件中包含的井名。

成功读取 LAS 文件与 LAS 数据浏览器 Streamlit 应用。图片由作者提供。
向 Streamlit 应用中添加主页
当有人第一次启动 LAS 数据浏览器应用时,展示应用的名称和简要描述会很好。
st.title('LAS Data Explorer - Version 0.2.0')
st.write('''LAS Data Explorer is a tool designed using Python and
Streamlit to help you view and gain an understanding of the contents
of a LAS file.''')
st.write('\n')
当我们重新运行应用时,现在将看到我们的主页。这可以扩展以包括额外的说明、有关应用的详细信息以及如果出现问题如何联系。

创建主页后的 LAS 数据浏览器 Streamlit 应用。图片由作者提供。
在构建 Streamlit 应用时,最好将代码拆分成函数,并在适当的时间调用它们。这使得代码更具模块化,更易于导航。
对于我们的主页,我们将上述代码放入一个名为 home() 的函数中。
def home():
st.title('LAS Data Explorer - Version 0.2.0')
st.write('''LAS Data Explorer is a tool designed using Python and
Streamlit to help you view and gain an understanding of the contents
of a LAS file.''')
st.write('\n')
添加导航单选按钮
在构建 Streamlit 应用时,很容易陷入一个不断添加部分的陷阱,结果是生成一个很长的可滚动网页。
使 Streamlit 应用更具可导航性的一种方法是添加导航菜单。这允许你将内容拆分到多个页面上。
实现这一点的一种方法是使用一系列单选按钮,这些按钮在切换时将更改主界面上显示的内容。
首先,我们需要为导航部分指定一个标题,然后我们必须调用 st.sidebar.radio 并传入一个我们希望用户能够导航到的页面列表。
# Sidebar Navigation
st.sidebar.title('Navigation')
options = st.sidebar.radio('Select a page:',
['Home', 'Header Information', 'Data Information',
'Data Visualisation', 'Missing Data Visualisation'])
当我们运行应用时,我们将看到现在有一个由单选按钮表示的导航菜单。

添加了单选按钮导航菜单后的 LAS 数据浏览器。图片由作者提供。
目前,如果点击按钮,什么也不会发生。
我们需要告诉 Streamlit 在进行选择时该做什么。
通过创建如下的 if/elif 语句来实现。当选择了一个选项时,将调用一个特定的函数。
例如,如果用户选择了主页,则会显示先前创建的主页函数。
if options == 'Home':
home()
elif options == 'Header Information':
header.header(las_file)
elif options == 'Data Information':
raw_data(las_file, well_data)
elif options == 'Data Visualisation':
plot(las_file, well_data)
elif options == 'Missing Data Visualisation':
missing(las_file, well_data)
让我们开始实现其他部分,以便开始显示一些内容。
从 LAS 文件中检索井头信息
在每个 las 文件中,顶部有一个包含有关井的信息的部分。这包括井名、国家、操作员等。

Volve 田野的 LAS 文件头示例。图片由作者提供。
为了读取这些信息,我们将创建一个名为header的新函数,然后遍历头部中的每一行。
为了防止用户点击头信息单选按钮时出现错误,我们需要检查在加载过程中是否已经创建了 las 文件对象。否则,我们将向用户展示错误。
然后,对于每个头项,我们将显示描述名称(item.descr)、助记符(item.mnemonic)和相关值(item.value)。
def header(las_file):
st.title('LAS File Header Info')
if not las_file:
st.warning('No file has been uploaded')
else:
for item in las_file.well:
st.write(f"<b>{item.descr.capitalize()} ({item.mnemonic}):</b> {item.value}",
unsafe_allow_html=True)
当应用程序重新运行,并从导航菜单中选择头信息页面时,我们现在会看到相关的井信息。

来自 LAS 文件的井日志头信息。图片由作者提供。
检索井日志测量信息
在成功读取头信息后,我们接下来要查看 las 文件中包含了哪些井日志测量。
为此,我们将创建一个简单的函数,名为raw_data,它将:
-
遍历 las 文件中的每个测量,写出它的助记符、单位和描述
-
提供测量总数的统计
-
使用 pandas 的
describe方法为每个测量创建一个统计摘要表 -
创建一个包含所有原始值的数据表
对于一个单一的函数来说,这个工作量很大,可能需要整理一下,但对于这个简单的应用程序,我们将把它们保持在一起。
def raw_data(las_file, well_data):
st.title('LAS File Data Info')
if not las_file:
st.warning('No file has been uploaded')
else:
st.write('**Curve Information**')
for count, curve in enumerate(las_file.curves):
st.write(f" {curve.mnemonic} ({curve.unit}): {curve.descr}",
unsafe_allow_html=True)
st.write(f"<b>There are a total of: {count+1} curves present within this file</b>",
unsafe_allow_html=True)
st.write('<b>Curve Statistics</b>', unsafe_allow_html=True)
st.write(well_data.describe())
st.write('<b>Raw Data Values</b>', unsafe_allow_html=True)
st.dataframe(data=well_data)
当 Streamlit 应用重新运行时,我们将看到所有与井日志测量相关的信息。
首先,我们有井测量信息和相关统计数据。

LAS 井日志测量信息。图片由作者提供。
然后是原始数据值。

LAS 井日志测量信息。图片由作者提供。
使用 Plotly 在 Streamlit 中可视化井日志数据
与任何数据集一样,仅通过分析原始数字很难掌握数据的外观。为了进一步深入,我们可以使用交互式图表。
这些将使最终用户更容易更好地理解数据。
以下代码在 Streamlit 页面上生成多个图表。所有内容都包含在一个函数中,以便在这个应用程序中使用。请记住,每个函数代表 LAS 数据探索器应用中的一个页面。
为了避免使用多个页面,下面的代码将为三种不同的图生成三个展开器:折线图、直方图和散点图(在岩石物理学中也称为交叉图)。
def plot(las_file, well_data):
st.title('LAS File Visualisation')
if not las_file:
st.warning('No file has been uploaded')
else:
columns = list(well_data.columns)
st.write('Expand one of the following to visualise your well data.')
st.write("""Each plot can be interacted with. To change the scales of a plot/track, click on the left hand or right hand side of the scale and change the value as required.""")
with st.expander('Log Plot'):
curves = st.multiselect('Select Curves To Plot', columns)
if len(curves) <= 1:
st.warning('Please select at least 2 curves.')
else:
curve_index = 1
fig = make_subplots(rows=1, cols= len(curves), subplot_titles=curves, shared_yaxes=True)
for curve in curves:
fig.add_trace(go.Scatter(x=well_data[curve], y=well_data['DEPTH']), row=1, col=curve_index)
curve_index+=1
fig.update_layout(height=1000, showlegend=False, yaxis={'title':'DEPTH','autorange':'reversed'})
fig.layout.template='seaborn'
st.plotly_chart(fig, use_container_width=True)
with st.expander('Histograms'):
col1_h, col2_h = st.columns(2)
col1_h.header('Options')
hist_curve = col1_h.selectbox('Select a Curve', columns)
log_option = col1_h.radio('Select Linear or Logarithmic Scale', ('Linear', 'Logarithmic'))
hist_col = col1_h.color_picker('Select Histogram Colour')
st.write('Color is'+hist_col)
if log_option == 'Linear':
log_bool = False
elif log_option == 'Logarithmic':
log_bool = True
histogram = px.histogram(well_data, x=hist_curve, log_x=log_bool)
histogram.update_traces(marker_color=hist_col)
histogram.layout.template='seaborn'
col2_h.plotly_chart(histogram, use_container_width=True)
with st.expander('Crossplot'):
col1, col2 = st.columns(2)
col1.write('Options')
xplot_x = col1.selectbox('X-Axis', columns)
xplot_y = col1.selectbox('Y-Axis', columns)
xplot_col = col1.selectbox('Colour By', columns)
xplot_x_log = col1.radio('X Axis - Linear or Logarithmic', ('Linear', 'Logarithmic'))
xplot_y_log = col1.radio('Y Axis - Linear or Logarithmic', ('Linear', 'Logarithmic'))
if xplot_x_log == 'Linear':
xplot_x_bool = False
elif xplot_x_log == 'Logarithmic':
xplot_x_bool = True
if xplot_y_log == 'Linear':
xplot_y_bool = False
elif xplot_y_log == 'Logarithmic':
xplot_y_bool = True
col2.write('Crossplot')
xplot = px.scatter(well_data, x=xplot_x, y=xplot_y, color=xplot_col, log_x=xplot_x_bool, log_y=xplot_y_bool)
xplot.layout.template='seaborn'
col2.plotly_chart(xplot, use_container_width=True)
一旦上述代码实现后,我们可以看到 LAS 文件可视化页面,包含三个可展开的框。

在地球科学和岩石物理学中,我们经常在折线图上绘制数据——通常称为日志图。y 轴通常表示井眼深度,而 x 轴表示我们希望可视化的数据。这使我们可以轻松地可视化这些测量数据随深度的趋势和模式。
在日志图部分,我们可以从数据框中选择特定列,并在交互式 Plotly 图表中显示它们。

使用 Plotly 创建的井日志图,并显示在 LAS 数据探索者 Streamlit 应用中。图片由作者提供。
直方图显示数据分布,并允许我们在一个小而简洁的图表中包含大量数据。
在直方图部分,我们有一些基本选项。我们可以从数据框中选择一列进行显示,并决定是否以线性或对数方式显示。
最后,我们可以使用 Streamlit 的颜色选择器。这允许你为直方图选择颜色,可以增强你在演示和报告中的可视化效果。

使用 Plotly 在 LAS 数据探索者 Streamlit 应用中创建的直方图。图片由作者提供。
散点图(交叉图)通常在岩石物理学和数据科学中用于比较两个变量。 从这种图表中,我们可以了解两个变量之间是否存在关系以及这种关系的强度。
在数据可视化页面的交叉图部分,我们可以选择 x 轴和 y 轴变量,以及一个第三变量,用于数据的颜色编码。
最后,我们可以将 x 轴和 y 轴设置为线性刻度或对数刻度。

使用 Plotly 在 LAS 数据探索者 Streamlit 应用中创建的散点图/交叉图。图片由作者提供。
识别井日志测量中的缺失数据
缺失数据是我们在处理数据集时面临的最常见的数据质量问题之一。它可能因多种原因而缺失,从传感器故障到不当和可能粗心的数据管理。
在处理数据集时,识别缺失数据并理解数据缺失的根本原因是至关重要的。对数据缺失原因的正确理解是开发务实解决方案的关键,尤其是许多机器学习算法无法处理缺失值。
在 Python 中,我们可以使用 pandas 的 describe 函数提供的文本数据摘要。虽然这很有用,但在图表中可视化缺失数据值通常更有帮助。这使我们能够轻松识别可能在基于文本的摘要中不明显的模式和关系。
为了创建数据完整性的交互式图表,我们可以利用 Plotly 库。下面的代码设置了 LAS 数据浏览器应用中的缺失数据可视化页面。
首先,我们检查是否有有效的 LAS 文件;如果有,我们开始创建页面并添加一些说明文本。
接下来,我们为用户提供一个选项,以选择数据框中的所有数据或选择特定列。在这旁边,我们允许用户更改图表中条形的颜色。
然后,我们继续根据用户选择绘制数据。
def missing(las_file, well_data):
st.title('LAS File Missing Data')
if not las_file:
st.warning('No file has been uploaded')
else:
st.write("""The following plot can be used to identify the depth range of each of the logging curves.
To zoom in, click and drag on one of the tracks with the left mouse button.
To zoom back out double click on the plot.""")
data_nan = well_data.notnull().astype('int')
# Need to setup an empty list for len check to work
curves = []
columns = list(well_data.columns)
columns.pop(-1) #pop off depth
col1_md, col2_md= st.columns(2)
selection = col1_md.radio('Select all data or custom selection', ('All Data', 'Custom Selection'))
fill_color_md = col2_md.color_picker('Select Fill Colour', '#9D0000')
if selection == 'All Data':
curves = columns
else:
curves = st.multiselect('Select Curves To Plot', columns)
if len(curves) <= 1:
st.warning('Please select at least 2 curves.')
else:
curve_index = 1
fig = make_subplots(rows=1, cols= len(curves), subplot_titles=curves, shared_yaxes=True, horizontal_spacing=0.02)
for curve in curves:
fig.add_trace(go.Scatter(x=data_nan[curve], y=well_data['DEPTH'],
fill='tozerox',line=dict(width=0), fillcolor=fill_color_md), row=1, col=curve_index)
fig.update_xaxes(range=[0, 1], visible=False)
fig.update_xaxes(range=[0, 1], visible=False)
curve_index+=1
fig.update_layout(height=700, showlegend=False, yaxis={'title':'DEPTH','autorange':'reversed'})
# rotate all the subtitles of 90 degrees
for annotation in fig['layout']['annotations']:
annotation['textangle']=-90
fig.layout.template='seaborn'
st.plotly_chart(fig, use_container_width=True)
当我们访问 LAS 数据浏览器的这一页时,我们会看到一个互动的 Plotly 图表,如下所示。如果用户选择了“所有数据”,则所有列都会显示出来。

使用 Streamlit 在 Plotly 图表中显示 pandas 数据框的所有列。图片由作者提供。
如果用户选择了“自定义选择”,则他们可以直接从数据框中选择列。

使用 Streamlit 多选框从数据框中选择列,并在 Plotly 图表中显示它们。图片由作者提供。
如果你想查看使用 Python 识别缺失值的其他方法,请查看下面的文章:
- 使用 missingno Python 库识别和可视化机器学习前的缺失数据
摘要
在本文中,我们展示了如何使用 Streamlit 和 Python 构建一个用于探索 LAS 文件的应用程序。虽然这是一个基础应用,但它可以作为查看原始 LAS 文件的一种有用替代方案。还可以添加更多功能来编辑文件或将其转换为其他标准格式。可能性无穷无尽!
本教程中使用的数据
本教程中使用的数据是 Equinor 于 2018 年发布的 Volve 数据集的一个子集。数据集的完整详细信息,包括许可证,可以在下面的链接中找到。
Equinor 已正式提供了一整套来自北海油田的数据,用于研究、学习等……
Volve 数据许可证基于 CC BY 4.0 许可证。许可证协议的完整详细信息可以在这里找到:
感谢阅读。在离开之前,你应该肯定地订阅我的内容,并将我的文章发送到你的收件箱。 你可以在这里做到这一点!另外,你还可以 注册我的通讯 以便免费获取额外的内容。
其次,你可以通过注册会员,获得完整的 Medium 体验,支持我和其他成千上万的作家。每月只需花费 5 美元,你即可全面访问所有精彩的 Medium 文章,还能有机会通过写作赚钱。
如果你使用 我的链接, 你将直接通过你的费用的一部分支持我,而且不会额外花费你更多。如果你这样做了,非常感谢你的支持。
用 Hamilton 在 13 分钟内构建一个可维护且模块化的 LLM 应用堆栈
LLM 应用是数据流,使用专门设计的工具来表达它们
·
关注 发表在 Towards Data Science ·13 分钟阅读·2023 年 7 月 13 日
--
LLM 堆栈。使用合适的工具,如 Hamilton,可以确保你的堆栈不会变得难以维护和管理。图片来源于 pixabay。
此文章与 Thierry Jean 合作撰写,最初发布于 此处。
在这篇文章中,我们将分享如何使用Hamilton这一开源框架来编写模块化和可维护的代码,以支持你的大型语言模型(LLM)应用程序堆栈。Hamilton 非常适合描述任何类型的数据流,这正是你在构建 LLM 驱动应用程序时所做的。通过 Hamilton,你可以获得强大的软件维护人机工程学,同时还能轻松地交换和评估应用程序组件的不同提供者/实现。免责声明:我是 Hamilton 包的作者之一。
我们将演示的示例将镜像你用于填充向量数据库的典型 LLM 应用程序工作流程。具体来说,我们将涵盖从网络中提取数据、创建文本嵌入(向量)并将其推送到向量存储中。

堆栈概述。作者提供的图像。
LLM 应用程序数据流
首先,让我们描述一下典型的 LLM 数据流的组成。应用程序将接收一个小的数据输入(例如文本、命令),并在更大的上下文中进行操作(例如聊天记录、文档、状态)。这些数据将通过不同的服务(LLM、向量数据库、文档存储等)进行操作、生成新的数据工件,并返回最终结果。大多数用例会在迭代不同输入的过程中重复这一流程多次。
一些常见的操作包括:
-
将文本转换为嵌入
-
存储 / 搜索 / 检索嵌入
-
查找嵌入的最近邻
-
检索用于嵌入的文本
-
确定传递到提示中的上下文
-
使用相关文本中的上下文提示模型
-
将结果发送到其他服务(API、数据库等)
-
…
-
并将它们串联起来!
现在,让我们在生产环境中深入探讨上述内容,假设用户对你的应用程序的输出不满意,并且你想找到问题的根源。你的应用程序记录了提示和结果。你的代码允许你找出操作的顺序。然而,你不知道问题出在哪里,系统产生了不理想的输出……为了解决这个问题,我们认为跟踪数据工件及生成它们的代码是关键,这样你才能快速调试类似的情况。
由于许多操作是非确定性的,这增加了你的 LLM 应用程序数据流的复杂性,这意味着你不能重新运行或逆向工程操作以重现中间结果。例如,即使你拥有相同的输入和配置,生成文本或图像响应的 API 调用可能也是不可重复的(你可以通过如temperature这样的选项缓解部分问题)。这也扩展到某些向量数据库操作,如“查找最近”——其结果取决于数据库中当前存储的对象。在生产环境中,快照数据库状态以使调用可重复几乎是不现实的。
基于这些原因,采用灵活的工具以创建稳健的数据流很重要,这样可以让你:
-
轻松地插入各种组件。
-
了解组件之间如何连接。
-
添加和定制常见的生产需求,如缓存、验证和可观察性。
-
根据你的需求调整流结构,而不需要强大的工程技能
-
插件集成到传统的数据处理和机器学习生态系统中。
在这篇文章中,我们将概述 Hamilton 如何满足第 1、2 和 4 点。有关第 3 和 5 点的信息,请参阅我们的文档。
当前的 LLM 应用程序开发工具
LLM 领域仍处于起步阶段,使用模式和工具正在快速演变。虽然 LLM 框架可以让你入门,但当前的选项并未经过生产环境测试;据我们了解,目前没有成熟的科技公司在生产中使用当前流行的 LLM 框架。
别误解我们的意思,有些工具确实非常适合快速建立概念验证!然而,我们认为它们在两个特定领域存在不足:
1. 如何建模 LLM 应用程序的数据流。 我们强烈认为“动作”的数据流建模更适合用函数来表示,而不是通过面向对象的类和生命周期。函数更容易推理、测试和更改。面向对象的类可能变得相当晦涩,并带来更多的思维负担。
当出现错误时,面向对象的框架需要你深入到对象的源代码中以理解它。而使用 Hamilton 函数时,清晰的依赖关系谱能告诉你在哪里查找,并帮助你推理发生了什么(更多信息请见下文)!
2. 定制/扩展。 不幸的是,一旦你超出框架提供的“简单”功能,你需要强大的软件工程技能来修改当前框架。如果这不是一个选项,这意味着你可能会在特定的自定义业务逻辑上脱离框架,这可能会导致你维护更多的代码面积,而不是如果你一开始就不使用框架的话。
关于这两点的更多信息,我们推荐你查看这些讨论线程(hacker news, reddit),其中有用户详细讨论。
虽然 Hamilton 并不是当前 LLM 框架的完整替代品(例如,没有“代理”组件),但它确实拥有满足 LLM 应用程序需求的所有构建模块,并且两者可以协同工作。如果你想要一种干净、清晰且可定制的方式来编写生产代码、集成多个 LLM 技术栈组件,并对你的应用程序进行观察,那么让我们继续进入接下来的几个部分吧!
使用 Hamilton 构建
Hamilton 是一个声明式微框架,用于在 Python 中描述数据流。它不是一个新框架(已有 3.5 年以上历史),并且在生产建模数据和机器学习数据流中使用多年。它的优势在于以一种直观易创建和维护的方式表达数据和计算流(类似于 DBT 对 SQL 的作用),这非常适合支持建模 LLM 应用程序的数据和计算需求。

Hamilton 范式的示意图。与其使用过程性赋值,不如将其建模为一个函数。函数名称是你可以获得的“输出”,而函数输入参数声明了计算所需的依赖关系。图片由作者提供。
Hamilton 的基础知识很简单,而且可以通过多种方式扩展;你不必了解 Hamilton 就能从这篇文章中获得价值,但如果你感兴趣,可以查看:
-
tryhamilton.dev – 在浏览器中的互动教程!
-
在 5 分钟内使用 Hamilton 进行 Pandas 数据转换
进入我们的示例
为了帮助建立一些心理背景,想象一下。你是一个小型数据团队,负责创建一个 LLM 应用程序,与组织的文档进行“聊天”。你认为评估候选架构在功能、性能配置、许可证、基础设施要求和成本方面是很重要的。最终,你知道你组织的主要关注点是提供最相关的结果和良好的用户体验。评估这些的最佳方法是构建一个原型,测试不同的技术栈,并比较它们的特性和输出。然后,当你过渡到生产环境时,你会希望确保系统能够轻松维护和检查,以始终提供优质的用户体验。
有鉴于此,在这个示例中,我们将实现 LLM 应用程序的一部分,特别是数据摄取步骤,用于索引知识库,其中我们将文本转换为嵌入并存储在向量数据库中。我们使用几种不同的服务/技术以模块化的方式实现这一点。广泛的步骤包括:
-
从 HuggingFace Hub 加载 SQuAD 数据集。你可以将其替换为你的预处理文档的语料库。
-
使用 Cohere API、OpenAI API 或 SentenceTransformer 库 嵌入文本条目。
如果你需要了解更多关于嵌入和搜索的信息,我们推荐以下链接:
在我们讲解这个示例时,考虑以下几点会对你有帮助:
-
将我们展示的内容与当前做的事情进行比较。 看到 Hamilton 如何使你能够策划和结构化一个项目,而无需明确的 LLM 重点框架。
-
项目和应用结构。 了解 Hamilton 如何强制执行一种结构,使你能够构建和维护模块化堆栈。
-
迭代中的信心和项目的持久性。 结合上述两点,Hamilton 使你能够更轻松地维护生产中的 LLM 应用程序,无论它的作者是谁。
让我们从一个可视化开始,以便你能对我们谈论的内容有一个概览:

Hamilton DAG 可视化 Pinecone + 句子变换器堆栈。图片由作者提供。
当使用 pinecone 和句子变换器时,LLM 应用程序的数据流将如下所示。借助 Hamilton,了解事物的连接就像在 Hamilton 驱动程序对象上调用display_all_functions()一样简单。
模块化代码
让我们解释一下使用 Hamilton 实现模块化代码的两种主要方式,以我们的示例为背景。
@config.when
Hamilton 关注可读性。虽然没有解释@config.when的作用,你可能已经可以判断这是一个条件语句,且仅在满足条件时包括。下面你将找到使用 OpenAI 和 Cohere API 将文本转换为嵌入的实现。
Hamilton 将识别两个函数作为替代实现,因为@config.when装饰器和相同的函数名称embeddings位于双下划线(__cohere、__openai)之前。它们的函数签名不必完全相同,这意味着采纳不同实现是简单且清晰的。
embedding_module.py
对于这个项目,将所有嵌入服务实现放在同一个文件中并使用@config.when装饰器是合理的,因为每个服务只有 3 个函数。然而,随着项目复杂性的增长,函数也可以移动到单独的模块中,并采用下一节的模块化模式。另一个要点是这些函数都是独立可单元测试的。如果你有特定的需求,将其封装到函数中并进行测试是很简单的。
更换 Python 模块
下面你将看到 Pinecone 和 Weaviate 的向量数据库操作实现。请注意,这些代码片段来自pinecone_module.py和weaviate_module.py,并观察函数签名的相似之处和不同之处。
pinecone_module.py 和 weaviate_module.py
使用 Hamilton 时,数据流通过函数名称和函数输入参数连接在一起。因此,通过共享类似操作的函数名称,这两个模块可以轻松互换。由于 LanceDB、Pinecone 和 Weaviate 实现分别存在于不同的模块中,这减少了每个文件的依赖数量,使文件更短,从而提高了可读性和可维护性。每个实现的逻辑都清晰地封装在这些命名函数中,因此针对每个模块进行单元测试是直接可行的。分离的模块强化了它们不应同时加载的概念。当发现多个相同名称的函数时,Hamilton 驱动程序实际上会抛出错误,这有助于加强这一概念。
驱动程序的影响
运行 Hamilton 代码的关键部分是Driver对象,它在run.py中找到。排除 CLI 和一些参数解析的代码,我们得到:
run.py 的代码片段
Hamilton 驱动程序负责协调执行,并且是你通过它操控数据流的工具,通过上述代码片段中看到的三种机制实现了模块化:
-
Driver 配置。 这是一个字典,驱动程序在实例化时接收该字典,包含应该保持不变的信息,例如使用哪个 API 或嵌入服务 API 密钥。这与可以传递 JSON 或字符串的命令平面(例如 Docker 容器、Airflow、Metaflow 等)很好地集成。具体来说,这里是我们指定要更换哪个嵌入 API 的地方。
-
驱动程序模块。 驱动程序可以接收任意数量的独立 Python 模块来构建数据流。在这里,
vector_db_module可以替换为我们连接的所需向量数据库实现。还可以通过 importlib 动态导入模块,这在开发与生产环境中可能很有用,同时也能实现通过配置驱动的方式来改变数据流实现。 -
驱动程序执行。
final_vars参数决定了应该返回什么输出。你无需重构代码来改变想要获得的输出。举个例子,如果你想调试数据流中的某些内容,可以通过将函数的名称添加到final_vars来请求任何函数的输出。例如,如果你有一些中间输出需要调试,可以很容易地请求它,或者完全在那个点停止执行。请注意,驱动程序在调用execute()时可以接收inputs和overrides值;在上面的代码中,class_name是一个执行时的input,指示我们要创建的嵌入对象以及将其存储在向量数据库中的位置。
模块化总结
在 Hamilton 中,使组件可互换的关键是:
-
定义具有相同名称的函数,然后,
-
使用
@config.when对它们进行注解,并通过传递给驱动程序的配置选择使用哪一个,或者, -
将它们放在不同的 Python 模块中,并将所需的模块传递给驱动程序。
所以我们刚刚展示了如何使用 Hamilton 插件、交换和调用各种 LLM 组件。我们无需解释什么是面向对象的层次结构,也不要求你具备广泛的软件工程经验(我们希望如此!)。为了实现这一点,我们只需匹配函数名称及其输出类型。因此,我们认为这种编写和模块化代码的方式比当前 LLM 框架所允许的更加可访问。
Hamilton 代码的实际应用
为了支持我们的主张,这里有一些我们观察到的将 Hamilton 代码应用于 LLM 工作流的实际影响:
CI/CD
模块/@config.when 的可互换性也意味着在 CI 系统中的集成测试非常容易思考,因为你可以根据需要灵活地交换或隔离数据流的部分。
协作
-
Hamilton 实现的模块化可以轻松跨团队边界镜像。函数名称及其输出类型成为合同,确保可以进行有针对性的更改并对更改充满信心,还可以通过 Hamilton 的 可视化和血统功能 了解下游依赖关系(就像我们看到的初始可视化一样)。例如,如何与向量数据库交互并进行消费就非常清晰。
-
代码更改更易于审查,因为流程由声明式函数定义。更改是自包含的;由于没有面向对象的层次结构需要学习,只需修改一个函数。任何“自定义”的内容都被 Hamilton 默认支持。
调试
当 Hamilton 出现错误时,很清楚它映射到的代码是什么,并且由于函数的定义,你知道它在数据流中的位置。
以使用 cohere 的 embeddings 函数为简单示例。如果发生超时或解析响应时出错,将清楚地映射到这段代码,并且通过函数定义你会知道它在流程中的位置。
@config.when(embedding_service="cohere")
def embeddings__cohere(
embedding_provider: cohere.Client,
text_contents: list[str],
model_name: str = "embed-english-light-v2.0",
) -> list[np.ndarray]:
"""Convert text to vector representations (embeddings) using Cohere Embed API
reference: https://docs.cohere.com/reference/embed
"""
response = embedding_provider.embed(
texts=text_contents,
model=model_name,
truncate="END",
)
return [np.asarray(embedding) for embedding in response.embeddings]

可视化显示embeddings在数据流中的位置。图像由作者提供。
创建模块化 LLM 堆栈的技巧
在结束之前,这里有一些想法来指导你构建应用程序。某些决策可能没有明显的最佳选择,但正确的模块化方法将使你能够随着需求的变化高效迭代。
-
在编写任何代码之前,绘制你的工作流的 DAG。这为定义通用步骤和接口奠定了基础,这些步骤和接口不是特定于服务的。
-
确定可以交换的步骤。通过有目的地设置配置点,你将减少投机泛化的风险。具体来说,这将导致具有较少参数、默认值且按主题模块分组的函数。
-
将数据流的部分切分成依赖较少的模块(如有相关)。这将导致更短的 Python 文件,减少包依赖,提高可读性和可维护性。Hamilton 对此不在意,可以从多个模块构建其 DAG。
结论与未来方向
感谢你阅读到这里。我们相信 Hamilton 在帮助每个人表达他们的数据流方面有一定作用,而 LLM 应用程序只是其中一个用例!总结我们在这篇文章中的信息,可以归纳为:
-
将 LLM 应用程序视为数据流是有用的,因此非常适合使用 Hamilton。
-
面向对象的 LLM 框架可能不透明且难以扩展和维护以满足生产需求。相反,应该使用 Hamilton 简单的声明式风格编写自己的集成。这样可以提高代码的透明度和可维护性,具有清晰的可测试函数、明确的运行时错误映射到函数的方式,以及内置的数据流可视化。
-
使用 Hamilton 所规定的模块化将使协作更高效,并为你提供必要的灵活性,以便按照该领域的进展速度修改和更改 LLM 工作流。
现在邀请你在这里玩转、尝试和修改完整的示例。这里有一个README文件会解释如何运行命令和开始使用。否则,我们正在思考以下内容来提升 Hamilton + LLM 应用体验:
-
代理。 我们能否为代理提供与常规 Hamilton 数据流相同的可视性?
-
并行化。 我们如何简化在文档列表上运行数据流的表达方式。请参见这个进行中的 PR了解我们的意思。
-
缓存和可观察性的插件。 目前已经可以在 Hamilton 上实现自定义的缓存和可观察性解决方案。我们正在致力于为常见组件提供更多的标准选项,例如 redis。
-
用户贡献的数据流部分。 我们看到可以在特定 LLM 应用用例上标准化常见名称的可能性。在这种情况下,我们可以开始聚合 Hamilton 数据流,并允许人们根据自己的需求下载。
我们想听听你的意见!
如果你对这些内容感到兴奋,或有强烈的看法,欢迎访问我们的 Slack 频道或在这里留下评论!一些可以帮助你的资源:
📣 加入我们的Slack社区 — 我们很乐意帮助解答你可能遇到的问题或帮助你入门。
⭐️ 在GitHub上给我们点赞
📝 如果你发现了问题,请给我们留下一个issue
你可能感兴趣的其他 Hamilton 文章:
-
tryhamilton.dev – 一个在浏览器中进行交互式教程的平台!
帮助初创公司创始人找到最佳孵化器:一个端到端的项目。
一个自由职业项目的演示,使用 Python、Pinecone、FastAPI、Pydantic 和 Docker 提出最佳孵化器的建议
·发布在 Towards Data Science ·15 min 阅读·2023 年 11 月 26 日
--
Harness,一个致力于帮助创始人创业的初创公司,找到我开发了一个帮助其社区找到最合适孵化器的工具:匹配工具。
在本文中,我们将介绍这个项目的不同阶段,从解决方案设计到交付。

Rames Quinerie 在 Unsplash 上的照片
背景
该公司及其联合创始人希望创建一个工具,使他们的初创公司创始人社区能够找到全球最佳的孵化器和加速器。
为了实现这一目标,他们手动从孵化器网站收集数据,包括位置、各种要求、资金机会等详细信息。此外,他们还利用了一个活跃的创始人社区。
利用孵化器和其社区的数据,他们需要找到一种方法来检索基于初创公司信息的前 k 名孵化器。
挑战接受。
解决方案设计
概述
乍一看,这个项目看起来像是一个推荐系统,比如 Netflix 或 Amazon 用于向用户推荐最佳的系列或产品。通过用户行为,如点击、评论或点赞,公司可以预测并推荐最合适的产品。
然而,在这种特定情况下,我们缺乏关于创始人偏好的任何先前数据。因此,在这种情况下构建推荐系统是不可行的。
另一种方法可以涉及将孵化器和初创企业数据嵌入到向量空间中进行相似性搜索。简而言之,这种方法涉及测量向量之间的距离,以确定最接近给定初创企业的孵化器。
但这种方法在这种情况下有很多缺陷。
孵化器具有我所称的硬标准,这些因素可能导致任何不符合要求的初创企业被立即拒绝。这可能包括如果孵化器要求混合或面对面的出席,位置不在同一城市,或缺乏资金。
那些硬标准会使嵌入(数据的向量表示)在这种情况下不是一个好的方法。例如,一个孵化器可能完全匹配一个初创企业,但如果申请未开放,则不应向创始人推荐这个孵化器。
这些硬标准的存在使得在整个数据集上使用嵌入不适合这种情况。例如,即使一个孵化器与初创企业完美对接,如果当前没有开放申请,也不适合向创始人推荐。
最后,即使大多数特征可以转化为数值(融资金额,接受的前期融资金额,初创企业收入预期)或分类(国家,出席要求,MVP 准备好),某些特征由于其多样性却无法分类:
-
融资工具: 赠款,140k$,股权(SAFE),…
-
行业重点: 医疗科技,人工智能,金融科技,…
此外,这些特征必须在匹配工具中考虑,但可能不会被视为硬标准。例如,创始人可能会选择一个专注于健康科技的孵化器,并且仍然愿意接受一个生物技术初创企业。
混合方法
为了解决这些问题,我们来考虑最佳的两全其美的方案。
如果某些孵化器的硬 标准会导致不匹配,可以考虑根据初创企业的信息筛选这些孵化器。经过缩小潜在匹配的列表后,我们可以使用剩余的软标准进行相似性搜索,将其转化为统一的文本并嵌入到向量中。
好消息是:Pinecone 向其向量数据库提供了这一功能!
[## 向量搜索中的缺失 WHERE 子句 | Pinecone]
向量相似性搜索使得庞大的数据集可以在几分之一秒内进行检索。然而,尽管其卓越的表现和…
www.pinecone.io](https://www.pinecone.io/learn/vector-search-filtering/?source=post_page-----bd65c41175bd--------------------------------)
项目路径现在已经明确:
-
孵化器的数据需要预处理以便过滤硬标准和相似性搜索软标准。然后将数据存储在 Pinecone 向量数据库中。
-
过滤对象必须根据 Pinecone Python 库构建。此外,它还需要保持灵活,以便客户可以轻松修改标准而无需修改算法。
-
软标准需要统一,并转换为嵌入格式,使用适当的嵌入模型。
-
数据是关键,我们需要为启动信息实现数据验证步骤,也需要为upserting新的孵化器数据到向量数据库中进行验证。我们将使用Pydantic。
-
该算法将作为API在docker 容器中提供。我们将使用 FastAPI 并创建一个 Dockerfile,以确保代码在任何环境下都能正常工作。
-
额外说明:单元测试和集成测试将被设置,以便任何人可以以 CI/CD 方式修改代码。
所有这些点都与利益相关者讨论过并被接受了。
我们准备出发了!
数据预处理
我收到了孵化器的解析信息在一个电子表格中。乍一看,数据相当混乱:手动提取没有明确的过程,字符串而不是布尔值,同一特征内的一致性缺乏,……
需要做大量的工作来使数据可用。

相同特征的不同日期“格式”
关于数据集中空值,每个特征都是独立处理的。
例如,出勤要求可能是面对面、混合或远程。在这种情况下,缺少此特征的孵化器被认为是要求面对面出勤。
另一个例子是启动公司的注册:注册或未注册。与其选择这两个类别中的一个,不如添加第三个类别作为默认值:无论如何。这将在过滤阶段有用,不仅选择主要类别之一,还选择所有未明确说明的孵化器。我们将在过滤部分讨论这个问题。
最终,我们将软标准转化为一个单一的提示以嵌入。为此,我们简单地使用了一个提示模板。如果在项目后期需要添加新特性,只需更新该提示即可。
# config.py
class Templates:
embedding_template = """Industries accepted:
{industry_focus}
Funding vehicle:
{funding_vehicle}"""
Templates.embedding_template.format(
industry_focus=industry_focus,
funding_vehicle=funding_vehicle
)
一旦孵化器数据经过预处理,就会导出到Pinecone 向量数据库中。
使用孵化器数据构建向量数据库
Pinecone 提供了一个易于使用的 Python SDK,用于插入、修改和查询向量数据库中的数据。
在我们的案例中,我们需要upsert(插入或更新)一个表示软标准的向量,此外还有硬标准。
根据 Pinecone,数据应遵循以下格式:
# List[(id, vector, metadata)]
[
("A", [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], {"genre": "comedy", "year": 2020}),
("B", [0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2], {"genre": "documentary", "year": 2019}),
("C", [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], {"genre": "comedy", "year": 2019}),
("D", [0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4], {"genre": "drama"}),
("E", [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5], {"genre": "drama"})
]
嵌入
有许多模型,无论是开源的还是非开源的,可以将文本嵌入到向量表示中。在这种情况下,我们将使用sentence-bert,一个旨在利用开源嵌入模型的 Python 库。你可以查看我之前的文章,其中描述了它的工作原理:
随着大型语言模型推动的 AI 最新趋势和 ChatGPT(OpenAI)的成功,企业已经…
这个库的简洁性使其成为构建第一个匹配工具版本的良好选择。
# pip install -U sentence-transformers
from sentence_transformers import SentenceTransformer
class SentenceTransformersEmbedding:
"""Embedding using the SentenceTransformers library (https://www.sbert.net)"""
def __init__(
self,
model_name: str = "all-MiniLM-L6-v2"
) -> None:
self.model = SentenceTransformer(model_name)
def get_embeddding(self, texts: Union[str, List[str]]) -> List:
# We need to return a list instead of an array for Pinecone
return self.model.encode(texts).tolist()
准备并导出孵化器数据。
要将新的孵化器数据插入到向量数据库中,我们按照 Pinecone 文档中介绍的方式准备数据。
def prepare_from_payload(self, incubators: List[Incubator]) -> List[Tuple[str, List[float], Mapping[str, Any]]]:
"""Prepare payload containing incubators data to export to Pinecone vector database.
Args:
incubators (List[Incubator]): List of Incubator containing the incubator information that will be sent to Pinecone.
Returns:
List[Tuple[str, List[float], Mapping[str, Any]]]: Prepared data for Pinecone. Check official documentation (https://docs.pinecone.io/docs/metadata-filtering#inserting-metadata-into-an-index).
"""
data = []
for incubator in incubators:
metadata = {key: value for key, value in incubator.model_dump(exclude={"incubator_id"}).items()}
additional_information_text = Templates.embedding_template.format(incubator.industry_focus, incubator.funding_vehicle)
embedding = self.embedding_generator.get_embeddding(additional_information_text)
incubator_data = (incubator.incubator_id, embedding, metadata)
data.append(incubator_data)
return data
正如你在代码中看到的,我们使用 Pydantic 的BaseModel创建了一个Incubators对象。
from pydantic import BaseModel
from datetime import date
class Incubator(BaseModel):
incubator_id: str
name: str
application_open: int = 1
next_deadline: date = date.max
funding_amount: int = 0 # Maximal amount the incubator can fund
attendance_requirement: Literal["in-person", "remote", "hybrid"] = "in-person"
incorporation: Literal["incorporated", "unincorporated"] = "regardless"
minimum_cofounders: int = 0
minimum_employees: int = 0
previous_funding_accepted: int = 1
...
class Incubators(BaseModel):
incubators: List[Incubator]
这个BaseModel类有两个主要好处。它不仅确保数据符合我们算法和查询的正确格式,而且还定义了孵化器数据的默认模式。
print(Incubator(
incubator_id="id",
name="incubator_on_fire",
industry_focus="Health tech",
funding_vehicle="Grant"
))
# Output
{
'id': 'id'
'name': 'incubator_on_fire',
'application_open': 1,
'next_deadline': datetime.date(9999, 12, 31),
'funding_amount': 0,
'attendance_requirement': 'in-person',
'incorporation': 'regardless',
'minimum_cofounders': 0,
'minimum_employees': 0,
'woman_founders': 0,
'student_founders': 0,
'industry_focus': 'Health tech',
'funding_vehicle': 'Grant'
...
}
孵化器数据随后使用 Pinecone Python 库导出到向量数据库。为了让其他开发人员能够在应用程序的整体架构中实现这段代码,我们使用了 FastAPI:
import os
from fastapi import FastAPI, HTTPException
from app.models import Incubators
from features import FeatureEngine
from embedding import SentenceTransformersEmbedding
PINECONE_API_KEY = os.getenv("PINECONE_API_KEY")
ENVIRONMENT = os.getenv("ENVIRONMENT")
app = FastAPI()
@app.post("/upsert")
def upsert(incubators: Incubators):
try:
embedding_generator = SentenceTransformersEmbedding()
feature_engine = FeatureEngine(embedding_generator=embedding_generator)
data = feature_engine.prepare_from_payload(incubators=incubators.incubators)
vectors = [pinecone.Vector(id=id, values=values, metadata=metadata) for id, values, metadata in data]
pinecone.init(api_key=PINECONE_API_KEY, environment=ENVIRONMENT)
index = pinecone.Index(index_name=VectorDatabaseConfig.index_name)
index.upsert(vectors=vectors)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
数据导出后,我们能够开始使用初创公司信息查询向量数据库。
构建匹配算法
该算法在两个步骤中执行 top-k 孵化器的检索:
-
过滤掉不相关的孵化器,
-
使用嵌入向量执行相似性搜索。
我们还需要确保算法足够灵活,以便在项目后期添加或更改任何数据而不触及算法的核心。
那么如何做到这一点呢?
这是我想到的解决方案:
Pinecone 使用与 MongoDB 相同的语言来过滤数据库[source]。它看起来是这样的:
import pinecone
pinecone.init(api_key=PINECONE_API_KEY, environment=ENVIRONMENT)
index = pinecone.Index("example-index")
index.query(
vector=[0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1],
filter={
"genre": {"$eq": "documentary"},
"year": 2019
},
top_k=5,
include_metadata=True
)
过滤映射也可以更为复杂:
# $in statement
{
"genre": { "$in": ["comedy", "documentary", "drama"] }
}
# Multi criteria
{
"genre": { "$eq": "drama" },
"year": { "$gte": 2020 }
}
# $or statement
{
"$or": [{ "genre": { "$eq": "drama" } }, { "year": { "$gte": 2020 } }]
}
通过在查询中实现初创公司信息,我们能够检索出符合要求的孵化器:$gte — 大于, $eq — 等于, 等等…
但有些情况更为复杂。
例如,位置和出席要求是配对使用的。如果一个孵化器只接受混合或面对面,那么初创公司逻辑上应该位于与孵化器相同的城市/国家。但匹配工具也应该展示所有接受远程的孵化器,无论初创公司位于何处。
另一个示例:假设初创公司由 女性创始人 领导,或者初创公司已经构建了 MVP。因此,具有此陈述为真的初创公司应被提议孵化器,该孵化器仅接受女性创始人,或要求 MVP,此外还包括所有其他孵化器。
正如这些示例所示,标准可以分为不同的“模板”称为 Criterion。这些标准模板将用于构建 filter_object,这是 Pinecone/MongoDB 使用的过滤映射。
使用 Python 类,它看起来是这样的:
class Criterion(ABC):
"""Incubators criterion template used to build the filter object.
Each subclass of this class is a specific rule case used incubators and start-ups data.
Args:
name (str): incubators metadata name as it is in the vectordatabase.
"""
def __init__(
self,
name: str,
) -> None:
self.name = name
class NormalCriterion(Criterion):
"""Basic rule for creating to filter data based on this criterion.
It takes this form:
```python
criterion.name = {criterion.condition_type: payload[criterion.startup_correspondance]}
```py
With `payload` the start-up information.
Example:
```python
max_funding_amount = {$gte: 10000}
```py
This will filter all incubators with a maximal funding capacity greater than 10000.
Args:
condition_type (str): comparison element like "$eq" (equal), "$lte" (lower than or equal), "$gt" (greater than)
The complete list is available on the pinecone documentaton (https://docs.pinecone.io/docs/metadata-filtering#metadata-query-language).
startup_correspondance (str): start-up correspondance from the payload
"""
def __init__(
self,
name: str,
condition_type: str,
startup_correspondance: str
) -> None:
self.condition_type = condition_type
self.startup_correspondance = startup_correspondance
super().__init__(name=name)
父类对象 Criterion 用于构建多个子类,表示每种情况。如果我们以上面介绍的 女性创始人/MVP 情况为例:
class InclusiveCriterion(Criterion):
"""If condition validated, considers all.
Example:
Being women founders should match women-founders-only incubators, but also the other incubators.
Same for MVP, Ready_to_pay, Students founders, etc...
```
if woman_founders_startup (False) != condition (True):
{"woman_founders_incubator": {"$eq": woman_founders_startup_value (false)}}
参数:
condition_type (str): 比较元素,如 "$eq"(等于),"$lte"(小于或等于),"$gt"(大于)
完整列表可在 pinecone 文档中找到 (https://docs.pinecone.io/docs/metadata-filtering#metadata-query-language)。
startup_correspondance (str): 从 payload 中的初创公司对应(见 matching_tool/app/models.py)
condition (bool): 如果条件得到验证,考虑标准
"""
def __init__(
self,
name: str,
condition_type: str,
startup_correspondance: str,
condition: bool
) -> None:
self.condition_type = condition_type
self.startup_correspondance = startup_correspondance
self.condition = condition
super().__init__(name)
```py
Those `Criterion` classes are used along their respective method to build the `filter_object` :
def normal_case(
payload: Mapping,
criterion: NormalCriterion,
filter_object: Dict
) -> Dict:
"""最简单的情况:取启动值(资金额,之前的资助等)并在 vectordatabase 中按此过滤
condition_type($eq, $lte, $gte, $gt, ...)
参数:
payload (Mapping): 启动信息
criterion (NormalCriterion): 普通标准
filter_object (Dict): 在 vectordatabase 查询期间的元数据过滤器
返回:
Dict:
```pypython
{metadata_name: {condition_type: startup_value}}
```
"""
filter_object[criterion.name] = {
criterion.condition_type: payload[criterion.startup_correspondance]
}
return filter_object
def inclusive_case(
payload: Mapping,
criterion: InclusiveCriterion,
filter_object: Dict
) -> Dict:
"""包容性案例:为包容性案例准备过滤器:女性创始人,学生创始人,MVP,其他费用...
如果条件满足(初创公司中的女性创始人 == 1),因此不要考虑过滤标准 => 获取所有(仅接受女性的孵化器和其他所有孵化器)
否则:只考虑没有女性创始人的孵化器 => {women_founders: {"$eq: 0}}
参数:
payload (Mapping): 启动信息
criterion (NormalCriterion): 普通标准
filter_object (Dict): 在 vectordatabase 查询期间的元数据过滤器
"""
if payload[criterion.startup_correspondance] != criterion.condition:
filter_object[criterion.name] = {criterion.condition_type: payload[criterion.startup_correspondance]}
return filter_object
All these `Criterion` classes are stored inside another class object we call `Criteria` . This class acts as a repository of all the criteria to consider for filtering the database and can be easily modified to add or remove any criterion.
class Criteria:
"""使用 Criterion 模板进行过滤。
使用适当的 Criterion 模板添加或删除任何条件。
"""
country = DependendantCriterion(
name="country",
condition_type="$eq",
startup_correspondance="country"
)
city = DependendantCriterion(
name="city",
condition_type="$eq",
startup_correspondance="city"
)
attendance_requirement = ConditionalCriterion(
name="attendance_requirement",
condition=["remote"],
true_criteria=[],
else_criteria=[country, city]
)
minimum_cofounders = NormalCriterion(
name="minimum_cofounders",
condition_type="$lte",
startup_correspondance="n_cofounders"
)
working_product_requirement = InclusiveCriterion(
name="working_product_requirement",
condition_type="$eq",
startup_correspondance="working_product",
condition=True
)
woman_founders = InclusiveCriterion(
name="woman_founders",
condition_type="$eq",
startup_correspondance="woman_founders",
condition=True
)
...
Once all the criteria are added to the `Criteria` object, we iterate over it and build the `filter_object` based on the start-up information. For each `Criterion` case, we add a filter element to the `filter_object` .
class Matcher:
"从向量数据库中检索与初创公司信息匹配的孵化器。"
def __init__(
self,
index: Index,
criteria: Criteria = Criteria(),
embedder: Embedding = SentenceTransformersEmbedding(),
) -> None:
"""
参数:
index (Index): 向量数据库索引 / 表
criteria (Criteria, optional): 孵化器元数据以进行搜索。默认为 Criteria()。
embedder (Embedding, optional): 嵌入方法,用于将文本转换为向量表示
语义搜索。默认为 SentenceTransformersEmbedding()。
"""
self.index = index
self.criteria = criteria
self.embedder = embedder
def _get_filter(
self,
payload: Dict[str, Any],
) -> Mapping[str, Any]:
"""构建用于在 Pinecone 上过滤元数据的字典。
过滤对象应遵循以下格式。有关更多信息,请查看官方 Pinecone 文档:
https://docs.pinecone.io/docs/metadata-filtering
参数:
payload (Dict[str, Any]): 初创公司信息
返回:
Mapping[str, Any]: 过滤对象
```pybash
filter={
'application_open': 1,
'$or': [{'attendance_requirement': {'$in': ['remote']}}, {'country': {'$eq': 'estonia'}, 'city': {'$eq': 'tallinn'}}],
'funding_amount': {'$gte': 12000},
'other_costs': {'$eq': 0},
'previous_funding_accepted': {'$eq': 1},
'working_product_requirement': {'$eq': 0}
}
```
"""
# 初始过滤器
filter_object = {"application_open": 1}
criteria = self.criteria.get_criteria()
for criterion in criteria:
if isinstance(criterion, NormalCriterion):
if check_correspondance_in_payload(payload, criterion):
filter_object = normal_case(
payload=payload,
criterion=criterion,
filter_object=filter_object,
)
if isinstance(criterion, InclusiveCriterion):
if check_correspondance_in_payload(payload, criterion):
filter_object = inclusive_case(
payload=payload,
criterion=criterion,
filter_object=filter_object,
)
if isinstance(criterion, ConditionalCriterion):
if check_dependencies(payload, conditional_criterion=criterion):
filter_object = conditional_case(
payload=payload,
criterion=criterion,
filter_object=filter_object,
)
if isinstance(criterion, DefaultCriterion):
if check_correspondance_in_payload(payload, criterion):
filter_object = default_case(
payload=payload,
criterion=criterion,
filter_object=filter_object,
)
return filter_object
As you can see in the code, we built four different `Criterion` templates to consider many cases: `NormalCriterion` , `InclusiveCriterion` , `ConditionalCriterion` , and `DefaultCriterion` .
In the future of the project, more categories can be added without changing the algorithm core, making it **customizable**.
Once the `filter_object` is created with the `_get_filter()` method, the vector database can be queried with the Pinecone `index.query()` method:
matches = self.index.query(
vector=embedding,
filter=filter_object,
include_metadata=True,
top_k=top_k
)
The matching tool algorithm is created. We then served it through an API endpoint using FastAPI and Pydantic.
@app.post("/match")
def search(payload: StartUp, top_k: int = 5) -> Mapping:
LOGGER.info("开始匹配。")
try:
payload = preprocess_payload(dict(payload))
pinecone.init(api_key=PINECONE_API_KEY, environment=ENVIRONMENT)
index = pinecone.Index(index_name=VectorDatabaseConfig.index_name)
matching_tool = Matcher(index=index)
matches = matching_tool.match(payload=payload, top_k=top_k)
return matches
except Exception as e:
LOGGER.error(f"{str(e)}")
raise HTTPException(status_code=500, detail=str(e))
As `Incubator` built with Pydantic, we created the object `Startup` object to ensure the start-up data comes in the right format:
class StartUp(BaseModel):
country: Optional[str] = None
city: Optional[str] = None
funding_amount: Optional[int] = None
n_cofounders: Optional[int] = None
n_employees: Optional[int] = None
woman_founders: Optional[bool] = None
industry_focus: str = ""
funding_vehicle: str = ""
...
An advantage of using Pydantic with FastAPI is that the API payload (here the start-up information) doesn’t have to be complete. For example, if there is missing information, Pydantic will automatically replace it with its default value, or not consider it at all in the algorithm (defined by the `None` statement).
The core of the API is now set up. We can now make the code ready for shipment using Docker and CI/CD with Pytest.
# Delivering the API
## Integration test with Pytest
During the development of the code, unitests and integration tests were created to ensure no modifications would break the algorithm.
Furthermore, creating the test algorithms not only provides a CI/CD process but also gives my client indications about how the code is supposed to work.
To build an integration test with FastAPI, we used the `TestClient` provided within the library. It uses the `httpx` library instead of `requests` making a call to the API.
The data used as validation of the code is stored in an external JSON file `data/integration_test_data.json`
integration_test.py
pip install httpx
from fastapi.testclient import TestClient
URL = "/match"
client = TestClient(app)
DATA_PATH = Path(os.path.realpath(file)).parent / "data/integration_test_data.json"
with open(DATA_PATH, 'r') as data:
DATA = json.load(data)
def test_match():
for test in DATA["match_tests"]:
response = client.post(URL, json=test["payload"])
assert response.status_code == 200
payload: Dict = json.loads(response.content)
match_ids = [match["incubator_id"] for match in payload.values()]
for expected_id in test["expected"]:
assert expected_id in match_ids

Run Pytest on all “test” scripts
Once all tests passed, we created the **Dockerfile** to containerize the code.
## Docker
To create a Docker container, we simply create a Dockerfile within the repository:
FROM python:3.9
WORKDIR /src
ENV PYTHONPATH=/src
COPY requirements.txt requirements.txt
COPY matching_tool/ .
RUN pip install -r requirements.txt
EXPOSE 8001
CMD ["uvicorn", "app.api:app", "--host", "0.0.0.0", "--port", "8001"]

Structure of the repository
Here’s what each line does:
* `FROM` import the docker image from the hub with all the basic elements required to run Python 3.9 in this case.
* `WORKDIR` specifies the location of the code within the container
* `ENV PYTHONPATH = /src` specifies which directory Python has to look into to import internal modules.
* `COPY` copies the files in the attributed directory.
* `RUN` is triggered during the Docker image creation, and before the Docker container build. This way, `pip install -r requirements.txt` only runs once.
* `EXPOSE` exposes a container port of our choice, here’s the port 8001\. The API port should match the container port.
* `CMD ['uvicorn”, “app.api.app”, “ — host”, “0.0.0.0”, “ — port 8001]`runs the FastAPI API. It is important here to indicate the host as `0.0.0.0` to enable calls from outside the container.
We then created the Docker image by running in the CLI:
docker build -t matching-tool:latest -f Dockerfile .
Finally, to run the container, one has just to write:
docker run -p 8001:8001 --name matching-tool matching-tool
一旦容器运行,任何人都可以通过端口 8001 调用 API。也可以将 Docker 容器部署到任何云提供商,**使匹配工具立即生效**。
项目已准备好交付。
# 结论
在这篇文章中,我分享了我为一家美国初创公司进行的实际项目。
根据我所提供的数据,以及与利益相关者的多次迭代,我开发了一个工具,帮助初创企业创始人找到最适合他们需求的孵化器。我逐步解释了我所遵循的过程和解决此问题的不同策略。
下一步将是将此算法嵌入到整体应用中,并开始收集用户数据。这将启动任何机器学习功能所需的**飞轮**。确实,从这些代表用户偏好的数据中,将能够构建一个会随时间学习的推荐系统,并为当前和未来的创始人提供最佳输出。
与[Harness](https://www.joinharness.com/)在这个项目中合作非常愉快。我祝愿他们一切顺利。他们知道未来有合作的机会可以随时联系我。
如果你喜欢这篇文章,[**欢迎订阅我的新闻通讯**](https://medium.com/@jeremyarancio/subscribe)**。我分享有关 NLP、MLOps 和创业的内容。**
你可以通过[Linkedin](https://www.linkedin.com/in/jeremy-arancio/)联系我,或者查看我的[Github](https://github.com/JeremyArancio)。
如果你是企业并希望将机器学习应用到你的产品中,你也可以[**预约通话**](https://topmate.io/jeremyarancio/555697)。
再见,祝编码愉快!
# 使用 Streamlit 构建 Medium 统计跟踪器
> 原文:[`towardsdatascience.com/building-a-medium-stats-tracker-with-streamlit-dfe75f69b8fc`](https://towardsdatascience.com/building-a-medium-stats-tracker-with-streamlit-dfe75f69b8fc)
## 使用 Python Streamlit 库跟踪和监控 Medium 统计数据
[](https://andymcdonaldgeo.medium.com/?source=post_page-----dfe75f69b8fc--------------------------------)[](https://towardsdatascience.com/?source=post_page-----dfe75f69b8fc--------------------------------) [Andy McDonald](https://andymcdonaldgeo.medium.com/?source=post_page-----dfe75f69b8fc--------------------------------)
·发表于 [Towards Data Science](https://towardsdatascience.com/?source=post_page-----dfe75f69b8fc--------------------------------) ·9 分钟阅读·2023 年 2 月 27 日
--

照片由 [Markus Winkler](https://unsplash.com/es/@markuswinkler?utm_source=medium&utm_medium=referral) 提供,来自 [Unsplash](https://unsplash.com/?utm_source=medium&utm_medium=referral)
我已经在 Medium 上写作了一段时间,最近开始使用 Excel 跟踪我的统计数据。但最近,我在考虑使用 Streamlit 构建一个应用,以提供更好的体验。所以我想,为什么不试试,并在构建的过程中写一篇文章呢?
在本文中,我们将介绍构建一个简单的 Streamlit 仪表板的过程,并提供一种方法,让最终用户通过表单输入他们的最新统计数据。到文章末尾,我们将拥有一个可以进一步构建并在以后变得更加完善的基本仪表板。

用于查看和分析 Medium 统计数据的 Streamlit 仪表板。图片由作者提供
如果你刚刚开始使用 Streamlit,你可能想查看我下面的文章。
[](/getting-started-with-streamlit-web-based-applications-626095135cb8?source=post_page-----dfe75f69b8fc--------------------------------) ## 入门 Streamlit 网络应用
### 温和地介绍创建 Streamlit 网络应用
towardsdatascience.com [](/getting-started-with-streamlit-5-functions-you-need-to-know-when-starting-out-b35ed7d872b9?source=post_page-----dfe75f69b8fc--------------------------------) ## 入门 Streamlit:开始时需要知道的 5 个函数
### 利用这 5 个函数简化你的 Streamlit 学习
[towardsdatascience.com
# 导入库并设置 CSV 文件
第一步是导入我们的关键 Python 库:[streamlit](https://streamlit.io/)、[pandas](https://pandas.pydata.org/)和[plotly_express](https://plotly.com/)。接着,我们需要将应用程序配置为默认的宽屏显示,同时添加应用程序的标题。
```py
import streamlit as st
import pandas as pd
import plotly_express as px
st.set_page_config(layout='wide')
st.header('Medium Stats Dashboard')
接下来,我们需要在与应用程序相同的目录中创建一个新的 CSV 文件,并将其命名为medium_stats。
我们需要打开文件并填写四列。我们可以通过代码完成此操作,但这是一种快速且简单的方式来设置文件。

Streamlit 中的 Medium Stats 仪表板的 CSV 起始文件。图像由作者提供。
从 Streamlit 应用程序向 CSV 文件中写入统计数据的功能
我们将设置的第一个功能是允许用户在仪表板中输入他们的统计数据并更新 CSV 文件。
def update_stats_csv(date, followers, email_subs, ref_members):
with open('medium_stats.csv', 'a+') as f:
f.write(f'\n{date},{followers},{email_subs},{ref_members}')
这段代码将打开 CSV 文件,并在其上追加一行,其中每个统计数据由逗号分隔。
接下来,我们需要创建一个简单的表单,允许用户输入统计数据。这是通过 Streamlit 中的st.form功能实现的。然后我们可以使用 with 语句,开始添加我们希望在仪表板中跟踪的每个关键统计数据的输入框数量。
我们还会将其添加到侧边栏中,以免主显示区域显得杂乱。
with st.sidebar.form('Enter the latest stats from your Medium dashboard'):
date = st.date_input('Date')
follower_num = st.number_input('Followers', step=1)
email_subs = st.number_input('Email Subscribers', step=1)
ref_members = st.number_input('Referred Members', step=1)
submit_stats = st.form_submit_button('Write Stats to CSV')
if submit_stats:
update_stats_csv(date, follower_num, email_subs, ref_members)
st.info(f'Stats for {date} have been added to the csv file')
当我们重新运行 Streamlit 应用时,应该会在侧边栏看到以下表单。

Streamlit 侧边栏中的 Medium Stats 录入表单。图像由作者提供。
当我们选择一个日期并添加几个数字时,我们可以点击Write Stats to CSV按钮,将值提交到 CSV 文件中。我们还会看到一个弹出提示,表示统计数据已被添加到文件中。

添加一个月统计数据后的 Streamlit Medium Stats 录入表单。图像由作者提供。
如果我们返回到 CSV 文件中,将会看到文件中新增了一行,我们刚刚添加的统计数据就在其中。

从 Streamlit 应用程序写入统计数据后的 CSV 文件。图像由作者提供。
很好!我们确认可以直接将统计数据添加到 CSV 文件中。现在我们可以继续加载 CSV 文件,并开始创建所有月份的图表。
读取包含所有统计数据的 CSV 文件
在 Streamlit 应用的主要部分,我们将添加一个展开器。当点击并展开它时,我们将能够在 pandas 数据框中查看 CSV 文件中的原始数据。
df = pd.read_csv('medium_stats.csv')
with st.expander('View Raw Data'):
st.dataframe(df)
如果我们返回到 Streamlit 应用中,我们将看到展开窗口,并查看 CSV 文件中的所有统计数据。

使用 Streamlit 的 st.expander 查看来自统计 CSV 文件的原始数据。图片由作者提供。
创建 Plotly 图表以查看 Medium 统计数据
现在我们已经成功将所有统计数据加载到 Streamlit 中,我们可以开始制作一些互动图表,以帮助我们理解趋势。对于这个应用程序,我们将使用 Plotly Express 创建图表。
使用起来非常简单,并且只需很少的代码就能获得强大且互动的图表。
我们将创建的第一个图表是每月关注者数量的线图。
fig_followers = px.line(df, x=df.date, y='followers', title='Followers')
st.plotly_chart(fig_followers, use_container_width=True)
当我们回到 Streamlit 应用程序时,我们现在可以看到我们的图表。

Streamlit 中的基础 Plotly Express 图表。图片由作者提供。
我们的文件中只有一个数据点,因此在线图中没有可查看的内容。为了解决这个问题,我们需要在 CSV 文件中添加更多数据。
在下面的示例中,我添加了从八月到二月底的多个月份。

添加多个日期后的 medium_stats CSV 文件。图片由作者提供。
现在我们可以回到 Streamlit 应用程序,查看 Plotly Express 线图。

Plotly Express 线图显示 Medium 关注者随时间的变化。图片由作者提供。
在我们的数据集中,我们跟踪了另外两个统计数据:电子邮件订阅者数量和推荐成员数量。我们将这两个数据并排显示在两个柱状图中。
为此,我们首先需要使用 st.columns() 函数创建两列,然后使用 px.bar 来设置这两个柱状图。
为了确保图表填满每一列,我们需要包括 use_container_width 参数并将其设置为 True。
plot_col1, plot_col2 = st.columns(2)
fig_subscribers = px.bar(df, x=df.date, y='email_subs', title='Email Subscribers')
plot_col1.plotly_chart(fig_subscribers, use_container_width=True)
fig_subscribers = px.bar(df, x=df.date, y='referred_members', title='Referred Members')
plot_col2.plotly_chart(fig_subscribers, use_container_width=True)
我们的应用程序现在有了两个额外的柱状图,并且我们可以很好地概览 Medium 统计数据随时间的变化。

Medium 统计仪表板,包括跟踪关注者数、电子邮件订阅者和推荐成员的图表。图片由作者提供。
随着我们开始向 CSV 文件中添加更多日期,我们可能需要考虑引入日期过滤器或按年分组数据,尤其是当我们在平台上待了很长时间时。
将最新月份的指标添加到 Streamlit 应用程序中
拥有互动图表是很棒的,然而,如果我们真的想知道当前月份的关注者数或电子邮件订阅者数,我们必须查看图表并尝试解决这个问题。我们需要为每个图表进行这项工作。
一种替代且更快捷的方法是使用 Streamlit 的指标将最新月份的统计数据作为突出数字显示在仪表板上。
如果我们假设数据框中的最后一行是最新月份,我们可以使用 pandas 的 iloc 函数提取每一列的最后值。
def get_latest_monthly_metrics(df):
# Assumes the last row is the latest entry
df['date'] = pd.to_datetime(df['date'])
df['month'] = df['date'].dt.month_name()
latest_month = df['month'].iloc[-1]
latest_follower_count = df['followers'].iloc[-1]
latest_sub_count = df['email_subs'].iloc[-1]
latest_refs_count = df['referred_members'].iloc[-1]
return latest_month, latest_follower_count, latest_sub_count, latest_refs_count
我们可以通过确保数据始终按日期排序来使这段代码更健壮。
下一步是通过调用 get_latest_monthly_metrics() 函数来获取最新统计数据,然后将这些统计数据传递给 Streamlit 的 metric 调用,如下所示:
month, followers, subs, refs = get_latest_monthly_metrics(df)
st.write(f'### Medium Stats for {month}')
col1, col2, col3 = st.columns(3)
col1.metric('Followers', followers)
col2.metric('Email Subscribers', subs)
col3.metric('Referred Members', refs)
当我们回到应用时,我们现在可以在应用顶部显著地看到最新统计数据。

在包含 Streamlit 指标后的 Medium Stats 仪表盘。图片由作者提供。
Streamlit 指标函数的一个优点是我们可以包含每个指标的变化量。
为了准备我们的数据,我们可以创建一个函数,为每个统计数据的 dataframe 添加一个差异列,如下所示:
def calculate_differences(df):
for x in ['followers', 'email_subs', 'referred_members']:
df[f'{x}_increase'] = df[x].diff()
return df
然后我们需要更新 get_latest_monthly_metrics() 函数,以考虑新的列。我们将返回每个最新的统计数据作为包含两个值的列表:一个是实际的统计数据值,另一个是变化量或增量。
def get_latest_monthly_metrics(df):
# Assumes the last row is the latest entry
df['date'] = pd.to_datetime(df['date'])
df['month'] = df['date'].dt.month_name()
latest_month = df['month'].iloc[-1]
latest_follower_count = [df['followers'].iloc[-1],
df['followers_increase'].iloc[-1]]
latest_sub_count = [df['email_subs'].iloc[-1],
df['email_subs_increase'].iloc[-1]]
latest_refs_count = [df['referred_members'].iloc[-1],
df['referred_members_increase'].iloc[-1]]
return latest_month, latest_follower_count, latest_sub_count, latest_refs_count
最后,我们更新了对这个函数创建的变量的调用,以便引用每个列表中的正确值:
col1, col2, col3 = st.columns(3)
col1.metric('Followers', followers[0], delta=round(followers[1]))
col2.metric('Email Subscribers', subs[0], delta=round(subs[1]))
col3.metric('Referred Members', refs[0], delta=round(refs[1]))
当我们回到 Streamlit 应用时,我们现在将拥有每个统计数据的增量。

Medium Stats 仪表盘,正确显示了过去一个月和当前月之间的差异。图片由作者提供。
就这样——一个基本的 Streamlit 仪表盘,用于可视化我们 Medium 账户的关键统计数据。
Medium Stats Streamlit 应用的完整代码
如果你想复制这个仪表盘并进行一些操作,这里是应用的完整代码。
import streamlit as st
import pandas as pd
import plotly_express as px
st.set_page_config(layout='wide')
st.header('Medium Stats Dashboard')
#Main Functions
def update_stats_csv(date, followers, email_subs, ref_members):
with open('medium_stats.csv', 'a+') as f:
f.write(f'\n{date},{followers},{email_subs},{ref_members}')
def calculate_differences(df):
for x in ['followers', 'email_subs', 'referred_members']:
df[f'{x}_increase'] = df[x].diff()
return df
def get_latest_monthly_metrics(df):
# Assumes the last row is the latest entry
df['date'] = pd.to_datetime(df['date'])
df['month'] = df['date'].dt.month_name()
latest_month = df['month'].iloc[-1]
latest_follower_count = [df['followers'].iloc[-1],
df['followers_increase'].iloc[-1]]
latest_sub_count = [df['email_subs'].iloc[-1],
df['email_subs_increase'].iloc[-1]]
latest_refs_count = [df['referred_members'].iloc[-1],
df['referred_members_increase'].iloc[-1]]
return latest_month, latest_follower_count, latest_sub_count, latest_refs_count
#Sidebar
with st.sidebar.form('Enter the latest stats from your Medium dashboard'):
date = st.date_input('Date')
follower_num = st.number_input('Followers', step=1)
email_subs = st.number_input('Email Subscribers', step=1)
ref_members = st.number_input('Referred Members', step=1)
submit_stats = st.form_submit_button('Write Stats to CSV')
if submit_stats:
update_stats_csv(date, follower_num, email_subs, ref_members)
st.info(f'Stats for {date} have been added to the csv file')
# Main page
df = pd.read_csv('medium_stats.csv')
with st.expander('View Raw Data'):
st.dataframe(df)
df = calculate_differences(df)
month, followers, subs, refs = get_latest_monthly_metrics(df)
st.write(f'### Medium Stats for {month}')
col1, col2, col3 = st.columns(3)
col1.metric('Followers', followers[0], delta=round(followers[1]))
col2.metric('Email Subscribers', subs[0], delta=round(subs[1]))
col3.metric('Referred Members', refs[0], delta=round(refs[1]))
fig_followers = px.line(df, x=df.date, y='followers', title='Followers')
st.plotly_chart(fig_followers, use_container_width=True)
plot_col1, plot_col2 = st.columns(2)
fig_subscribers = px.bar(df, x=df.date, y='email_subs', title='Email Subscribers')
plot_col1.plotly_chart(fig_subscribers, use_container_width=True)
fig_subscribers = px.bar(df, x=df.date, y='referred_members', title='Referred Members')
plot_col2.plotly_chart(fig_subscribers, use_container_width=True)
总结
在本文中,我们看到如何利用我们的 Medium 统计数据来创建直接在 Streamlit 内部的交互式图表。这是一个基本的应用,但通过样式和更多功能(如包括 Medium 会员收入),可以变得更好。
通过 Medium API 自动更新数据是非常好的。然而,我不相信所有的统计数据都可以通过 API 访问。
感谢阅读。在你离开之前,你应该订阅我的内容,将我的文章发送到你的收件箱。 你可以在这里做到这一点!或者,你可以 注册我的新闻通讯 以免费获取额外的内容。
其次,你可以通过注册会员来获得完整的 Medium 体验,支持我和成千上万的其他作家。这仅需每月$5,你将可以访问所有精彩的 Medium 文章,还可以通过写作赚取收入。如果你通过 我的链接注册,你将通过你的费用的一部分直接支持我,而且不会额外增加费用。如果你这样做了,非常感谢你的支持!
构建一个问答 PDF 聊天机器人
LangChain + OpenAI + Panel + HuggingFace
·
关注 发表在 Towards Data Science ·6 分钟阅读·2023 年 4 月 9 日
--
让我们来构建一个用于回答外部 PDF 文件问题的聊天机器人。通过 5 个简单的步骤,你应该能够构建一个像这样的问答 PDF 聊天机器人:

😊 想试用这个应用吗?我已经在 Hugging Face 上托管了这个应用:sophiamyang-panel-pdf-qa.hf.space/LangChain_QA_Panel_App
💻 你可以在这里找到我所有的代码:
huggingface.co/spaces/sophiamyang/Panel_PDF_QA/tree/main
📒 你可以在这里找到我的笔记本文件:
huggingface.co/spaces/sophiamyang/Panel_PDF_QA/blob/main/LangChain_QA_Panel_App.ipynb
好的,开始构建这个问答 PDF 聊天机器人吧!
第一步:设置
- 安装所需的包
!pip install langchain openai chromadb pypdf panel notebook
- 导入所需的包
import os
from langchain.chains import RetrievalQA
from langchain.llms import OpenAI
from langchain.document_loaders import TextLoader
from langchain.document_loaders import PyPDFLoader
from langchain.indexes import VectorstoreIndexCreator
from langchain.text_splitter import CharacterTextSplitter
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma
import panel as pn
- 定义 OpenAI API 密钥:在 OpenAI 创建一个账户并生成 API 密钥
platform.openai.com/account。请注意,OpenAI API 不是免费的。你需要在这里设置账单信息才能使用 OpenAI API。或者,你可以使用 HuggingFace Hub 或其他地方的模型。查看我之前的 博客文章 和 视频 了解如何使用其他模型。
import os
os.environ["OPENAI_API_KEY"] = "COPY AND PASTE YOUR API KEY HERE"
第一步:定义 Panel 小部件
为了制作这个应用程序,我们将需要以下小部件:
-
file_input用于上传文件 -
openaikey输入你的 OpenAI 密钥 -
widgets结合了所有其他小部件:prompt用于输入问题文本,run_button用于点击并运行模型,select_k用于选择相关块的数量,以及select_chain_type用于选择使用的链类型。
file_input = pn.widgets.FileInput(width=300)
openaikey = pn.widgets.PasswordInput(
value="", placeholder="Enter your OpenAI API Key here...", width=300
)
prompt = pn.widgets.TextEditor(
value="", placeholder="Enter your questions here...", height=160, toolbar=False
)
run_button = pn.widgets.Button(name="Run!")
select_k = pn.widgets.IntSlider(
name="Number of relevant chunks", start=1, end=5, step=1, value=2
)
select_chain_type = pn.widgets.RadioButtonGroup(
name='Chain type',
options=['stuff', 'map_reduce', "refine", "map_rerank"]
)
widgets = pn.Row(
pn.Column(prompt, run_button, margin=5),
pn.Card(
"Chain type:",
pn.Column(select_chain_type, select_k),
title="Advanced settings", margin=10
), width=600
)

第二步:定义问答函数
我们将使用 LangChain 和 OpenAI 进行问答。在 LangChain 中至少有四种方式进行问答。查看我之前的博客文章和视频,了解 LangChain 中的四种问答方式。
在这个函数中,我们使用了 RetrievalQA 来检索相关的文本块,然后仅将这些文本块传递给语言模型。我们定义了四个参数:—
-
file是你感兴趣的输入 PDF 文件 -
query是你的问题 -
chain_type让你将链类型定义为四种选项之一:“stuff”、“map reduce”、“refine”、“map_rerank”。查看我之前的博客文章以了解每种链类型。 -
k定义了相关文本块的数量
def qa(file, query, chain_type, k):
# load document
loader = PyPDFLoader(file)
documents = loader.load()
# split the documents into chunks
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
texts = text_splitter.split_documents(documents)
# select which embeddings we want to use
embeddings = OpenAIEmbeddings()
# create the vectorestore to use as the index
db = Chroma.from_documents(texts, embeddings)
# expose this index in a retriever interface
retriever = db.as_retriever(search_type="similarity", search_kwargs={"k": k})
# create a chain to answer questions
qa = RetrievalQA.from_chain_type(
llm=OpenAI(), chain_type=chain_type, retriever=retriever, return_source_documents=True)
result = qa({"query": query})
print(result['result'])
return result
下面是一个使用此函数的示例:

第三步:将输出显示为 Panel 对象
上述函数返回语言模型的查询、结果和源文档。为了在 Panel 应用中显示结果和源文档,我们需要将它们转换为 Panel 对象。在 qa_result 函数中,我将查询(prompt_text)、结果(result[“result”])和源文档(result["source_documents"])追加到一个名为 convos 的列表中,然后将其转换为 Panel 对象 pn.Column(*convos)。
convos = [] # store all panel objects in a list
def qa_result(_):
os.environ["OPENAI_API_KEY"] = openaikey.value
# save pdf file to a temp file
if file_input.value is not None:
file_input.save("/.cache/temp.pdf")
prompt_text = prompt.value
if prompt_text:
result = qa(file="/.cache/temp.pdf", query=prompt_text, chain_type=select_chain_type.value, k=select_k.value)
convos.extend([
pn.Row(
pn.panel("\U0001F60A", width=10),
prompt_text,
width=600
),
pn.Row(
pn.panel("\U0001F916", width=10),
pn.Column(
result["result"],
"Relevant source text:",
pn.pane.Markdown('\n--------------------------------------------------------------------\n'.join(doc.page_content for doc in result["source_documents"]))
)
)
])
#return convos
return pn.Column(*convos, margin=15, width=575, min_height=400)
第四步:运行按钮执行函数
接下来,我想让 qa_result 函数具有交互性,也就是说,当我点击运行按钮时,我希望运行这个函数。这在 Panel 中非常简单。我们使用 pn.bind 将 qa_result 函数绑定到 run_button 小部件。请注意,在 qa_result 函数中,我们传入了一个参数 _,实际上在函数中并未使用。这只是一个占位符,让 pn.bind 知道我们可以将一个小部件绑定到这个函数上。
qa_interactive = pn.panel(
pn.bind(qa_result, run_button),
loading_indicator=True,
)
然后我想将输出格式化到一个框中,因此我使用了 pn.WidgetBox:
output = pn.WidgetBox('*Output will show up here:*', qa_interactive, width=630, scroll=True)

第 5 步:定义布局
现在我们可以使用 pn.Column 将所有小部件和输出组合成一列。你可以运行 panel serve LangChain_QA_Panel_App.ipynb 来服务这个应用程序。现在你应该有一个可以运行的应用程序!
# layout
pn.Column(
pn.pane.Markdown("""
## \U0001F60A! Question Answering with your PDF file
Step 1: Upload a PDF file \n
Step 2: Enter your OpenAI API key. This costs $$. You will need to set up billing info at [OpenAI](https://platform.openai.com/account). \n
Step 3: Type your question at the bottom and click "Run" \n
"""),
pn.Row(file_input,openaikey),
output,
widgets
).servable()
最终步骤:托管到 Hugging Face Space
如你所见,在我的视频开始时,我在 Hugging Face 上托管了我的应用:sophiamyang-panel-pdf-qa.hf.space/LangChain_QA_Panel_App
查看我之前的 博客文章 和 视频,了解如何在 Hugging Face 上托管 Panel 应用。
这是我为这个应用程序所做的:
-
我上传了我的 Jupyter Notebook 文件
-
我创建了一个 Dockerfile。这个应用程序的特别之处在于,它为 Chroma 向量数据库创建了一个 .chroma 目录。因此,我们需要创建这个目录并赋予它权限:
RUN mkdir .chroma和RUN chomd 777 .chroma。 -
我在一个
[requirements.txt](https://huggingface.co/spaces/sophiamyang/Panel_PDF_QA/blob/main/requirements.txt)文件 中包含了所有所需的包
结论
希望在这篇文章结束时,你能了解如何使用 LangChain、OpenAI 和 Panel 构建一个问答 PDF 聊天机器人,并知道如何将应用程序部署到 Hugging Face Space。
致谢:
感谢 Jim Bednar 和 Philipp Rudiger 的指导和反馈!

图片由 Volodymyr Hryshchenko 提供,来源于 Unsplash
. . .
由 Sophia Yang 于 2023 年 4 月 8 日发布
Sophia Yang 是一位高级数据科学家。通过 LinkedIn、Twitter 和 YouTube 联系我,并加入 DS/ML 书友会 ❤️
使用机器学习构建推荐系统
原文:
towardsdatascience.com/building-a-recommender-system-using-machine-learning-2eefba9a692e
Kaggle 蓝图
在 Python 中使用共访矩阵和 GBDT 排名模型的“候选项重排序”方法
·发表于Towards Data Science ·阅读时长 6 分钟·2023 年 3 月 1 日
--

“一个绝佳的选择,女士!我们的汉堡非常适合配菜和饮料。我可以推荐一些选项吗?”(作者提供的图像)
欢迎来到新文章系列的第一版,称为“Kaggle 蓝图”,在这里我们将分析Kaggle比赛的顶级解决方案,以便提取可以应用于我们自己数据科学项目的经验教训。
本期内容将回顾“OTTO — 多目标推荐系统”比赛中的技术和方法,该比赛于 2023 年 1 月底结束。
问题陈述:多目标推荐系统
“OTTO — 多目标推荐系统”比赛的目标是构建基于大量隐式用户数据的多目标推荐系统(RecSys)。
具体来说,在电子商务的使用案例中,竞争者们处理了以下细节:
-
多目标:点击、购物车添加和订单
-
大型数据集:超过 2 亿个事件,涉及约 180 万项
-
隐式用户数据:用户会话中的先前事件
基于真实世界电子商务会话构建推荐系统
如何处理大量项目的推荐系统
这次比赛的主要挑战之一是选择项目的数量众多。将所有可用的信息输入复杂模型需要大量计算资源。
因此,大多数竞争者遵循的通用基线是两阶段候选生成/重新排序技术[3]:
-
阶段:候选生成——这一步将每个用户的潜在推荐(候选项)数量从数百万减少到大约 50 到 200 个[2]。为了处理数据量,通常使用简单模型。
-
阶段:重新排序——你可以在这一步使用更复杂的模型,例如机器学习(ML)模型。一旦你对减少后的候选项进行排名,你可以选择排名最高的项目作为推荐。

两阶段推荐候选生成/重新排序技术(图像来源于作者,灵感来自[3])
第一阶段:使用共访矩阵生成候选项
两阶段方法的第一步是将潜在推荐(候选项)的数量从数百万减少到大约 50 到 200 个[2]。为了处理大量项目,第一阶段的模型应保持简单[5]。
你可以选择和结合不同的策略来减少项目数量[3]:
-
通过用户历史
-
通过流行度——这种策略也可以作为一个强有力的基线[5]
-
通过基于共访矩阵的共现
生成候选项的最直接方法是利用用户历史:如果用户查看过某个项目,他们很可能也会购买它。
然而,如果用户查看的项目数(例如五个项目)少于我们希望为每个用户生成的候选项数量(例如 50 到 200),我们可以通过项目流行度或共现来填充候选项列表[7]。由于基于流行度的选择较为直接,我们将在本节中重点关注基于共现的候选项生成。
通过共现生成的候选项可以通过共访矩阵进行处理:如果user_1购买了item_a,并且在不久后购买了item_b,我们会存储这些信息[6, 7]。

推荐系统的用户购买行为的最小示例(图像来源于作者)
- 对于每个项目,计算在指定时间框架内每个其他项目的出现次数。

共访矩阵的最小示例(图像来源于作者)
2. 对于每个项目,找到在该项目之后最常访问的 50 到 200 个项目。
如上图所示,共访矩阵不一定是对称的。例如,购买了汉堡的人也很可能会购买饮料——但相反的情况可能不成立。
你还可以根据接近度为共访矩阵分配权重。例如,同一会话中一起购买的项目可能比用户在不同购物会话中购买的项目有更高的权重。
共访矩阵类似于通过计数进行的矩阵分解 [6]。矩阵分解是推荐系统的常用技术。具体来说,它是一种协同过滤方法,用于发现项目和用户之间的关系。
矩阵分解推荐系统的步骤
towardsdatascience.com
阶段 2:使用 GBDT 模型重新排序
第二步是重新排序。虽然你可以通过手工规则 [1] 达到良好的效果,但理论上使用 ML 模型应该更有效 [5]。
你可以使用不同的梯度提升决策树(GBDT)排名器,如 XGBRanker 或 LGBMRanker [2, 3, 4]。
训练数据和特征工程的准备
GBDT 排名模型的训练数据应包含以下列类别 [2]:
-
来自候选生成的用户和项目对 — 数据框的基础将是第一阶段生成的候选列表。对于每个用户,你应该得到
N_CANDIDATES个项目,因此起点应该是形状为(N_USERS * N_CANDIDATES, 2)的数据框。 -
用户特征 — 计数、聚合特征、比例特征等。
-
项目特征 — 计数、聚合特征、比例特征等。
-
用户-项目特征(可选)— 你可以创建用户-项目互动特征,如‘点击的项目’
-
标签 — 对于每个用户-项目对,合并标签(例如,‘购买’或‘未购买’)。
生成的训练数据框应如下所示。

用于训练推荐系统 GDBT 排名模型的训练数据结构(作者提供的图片)
GBDT 排名模型
此步骤旨在训练 GBDT 排名模型以选择 top_N 推荐。
GBDT 排名器将接受三个输入:
-
X_train,X_val:包含FEATURES的训练和验证数据框 -
y_train,y_val:包含LABELS的训练和验证数据框 -
group:注意FEATURES不包含user,item列 [2]。因此,模型需要知道在何种组内对项目进行排名:group = [N_CANDIDATES] * (len(train_df) // N_CANDIDATES)
以下是使用 XGBRanker [2] 的示例代码。
import xgboost as xgb
dtrain = xgb.DMatrix(X_train,
y_train,
group = group)
# Define model
xgb_params = {'objective' : 'rank:pairwise'}
# Train
model = xgb.train(xgb_params,
dtrain = dtrain,
num_boost_round = 1000)
以下是带有LGBMRanker的示例代码[4]:
from lightgbm.sklearn import LGBMRanker
# Define model
ranker = LGBMRanker(
objective="lambdarank",
metric="ndcg",
n_estimators=1000)
# Train
model = ranker.fit(X_train,
y_train,
group = group)
GBDT 排名模型将对指定组内的项目进行排序。要检索top_N推荐项,您只需按用户对输出进行分组,并按项目的排名排序即可。
总结
从回顾 Kagglers 在“OTTO——多目标推荐系统”比赛过程中创建的学习资源中,还可以学到更多的课程。对于这种类型的问题陈述,还有许多不同的解决方案。
在本文中,我们集中讨论了在许多竞争者中流行的一般方法:通过协同访问矩阵进行候选生成,以减少潜在推荐项目的数量,然后进行 GBDT 重排序。
享受这个故事了吗?
免费订阅 以在我发布新故事时获得通知。
[## 当 Leonie Monigatti 发布新内容时,获取电子邮件通知。
当 Leonie Monigatti 发布新内容时,您将收到电子邮件通知。通过注册,您将创建一个 Medium 账户(如果您还没有的话)……
medium.com](https://medium.com/@iamleonie/subscribe?source=post_page-----2eefba9a692e--------------------------------)
在 LinkedIn、Twitter和 Kaggle上找到我!
参考文献
[1] Chris Deotte (2022). “候选重排序模型——[LB 0.575]”在 Kaggle 笔记本中。(访问日期:2023 年 2 月 26 日)
[2] Chris Deotte (2022). “如何构建 GBT 排序模型”在 Kaggle 讨论中。(访问日期:2023 年 2 月 21 日)
[3] Ravi Shah (2022). “大型数据集的推荐系统”在 Kaggle 讨论中。(访问日期:2023 年 2 月 21 日)
[4] Radek Osmulski (2022). “[polars] 概念验证:LGBM Ranker”在 Kaggle 笔记本中。(访问日期:2023 年 2 月 26 日)
[5] Radek Osmulski (2022). “在 Kaggle 上的 OTTO 比赛介绍(RecSys)”在 YouTube 上。(访问日期:2023 年 2 月 21 日)
[6] Radek Osmulski (2022). “协同访问矩阵究竟是什么?”在 Kaggle 讨论中。(访问日期:2023 年 2 月 21 日)
[7] Vladimir Slaykovskiy (2022). “协同访问矩阵”在 Kaggle 笔记本中。(访问日期:2023 年 2 月 21 日)
使用开源工具和 Databricks 构建单一客户视图
一个可扩展的数据质量和记录链接工作流,使客户数据科学成为可能
·
关注 发表在 Towards Data Science ·11 分钟阅读·2023 年 11 月 6 日
--
介绍
单一客户视图是对客户与业务所有互动的全面数据表示。它将来自多个来源的数据,如购买记录、网站互动、社交媒体、电子邮件咨询、反馈和其他数据源,整合为每个独特客户的单一记录[1]。一旦实现单一客户视图,就能为精准营销、提高客户保留率和最大化客户生命周期价值提供许多机会[2]。然而,实现并维护单一客户视图可能会面临挑战,包括数据质量限制、多种第三方主数据管理(MDM)工具的许可成本,以及处理个人数据时的客户隐私和合规性考虑,这些都使得构建单一客户视图变得复杂。本文提供了一种构建单一客户视图的数据质量和记录链接管道的方法,考虑了这些挑战,并提供了使用基于 Spark 的云计算平台 Databricks 的低成本开源方法。
解决方案概述
下图总结了一种使用 Databricks 生成单一客户视图的开源方法。在一系列连接在工作流中的 Databricks 笔记本中,我们可以借助 Great Expectations 库实现数据分析和验证检查,解决任何数据质量问题,并确保在清洗和合规笔记本中遵守数据保护和保留政策,使用 Splink 库进行客户数据表之间或内部的概率记录链接,最终应用黄金记录逻辑并连接所有客户记录,以去重并链接不同的客户数据集,从而生成单一客户视图。

Databricks 中开源数据质量和记录链接工作流的概述。图像作者提供
所有转换都在使用 Pyspark/Spark SQL 编写的查询中实现,或利用任何开源组件的 Spark 后端,从而利用 Databricks 的并行计算能力来扩展各阶段所需的处理,以适应可能的大规模客户数据集。通过使用 Lakehouse 架构来存储所有中间表和最终输出,我们可以从对所有数据操作的审计日志中受益,如果需要遵守合规性要求,可以永久删除客户数据,同时保留用于分析目的的匿名信息,并享受 Delta 表格式的性能和可靠性[3]。数据从源系统(例如,Dynamics 365 或 Salesforce 等客户关系管理(CRM)工具)进入流程,并以单一客户视图的形式输出,准备用于分析或操作用例,例如客户画像仪表板、推荐引擎等机器学习应用,以及通过电子邮件、短信或邮寄的自动化营销和追加销售。
使用 Great Expectations 进行数据分析和验证
Great Expectations 是一个开源的 Python 库 [4],它允许你以编程方式为数据管道创建自动化测试,确保数据的有效性、完整性以及一致的模式。用户可以为数据中的字段定义期望字典,然后将数据与这些期望进行测试。
# contactid
custom_expectation_suite.add_expectation(
ExpectationConfiguration(
expectation_type = "expect_column_values_to_not_be_null",
kwargs = {'column':'contactid'},
meta = {'reason':'contactid is the unique ID for D365 contacts,
this should not be null'})
)
使用 Great Expectations 定义期望的示例
该库提供了多种现成的期望,如测试空值、唯一值和数值范围;如果标准集没有提供所需的期望,用户还可以创建自定义期望。Great Expectations 支持各种数据源,包括数据库、CSV 文件,以及 Pandas 或 Pyspark 数据框,并在使用 Databricks 时可以利用 Spark 后端。运行验证测试后,用户将获得一个基于网页的数据验证报告,报告中突出显示了哪些字段通过了期望套件,哪些字段未通过,并说明了原因,如下例所示。

使用 Great Expectations 生成的数据验证报告 docs.greatexpectations.io/docs/
Great Expectations 还提供数据分析功能,使用户能够深入了解其数据集,例如总结统计信息(均值、中位数和标准差)、完整性和唯一性,并提供一套可视化工具,帮助用户探索数据并识别潜在问题。这些洞察可能会突出需要在期望套件中添加额外期望或向数据管理员发送警报的需求,如果客户数据的后续迭代超出了预期值。
在构建单一客户视图的背景下,Great Expectations 可以帮助你了解客户数据集的组成和质量,并在数据传递到管道的后续步骤或交给下游应用程序之前验证输出。Great Expectations 可以用来在第一次处理客户数据集时初步发现数据质量问题,并且在生产环境中可以与 CI/CD 工具集成,以便将数据集的单元测试作为部署管道的一部分。
清洗和合规性
清洗和合规阶段涉及解决分析和验证中突出的数据质量问题,并确保存储的客户数据符合数据保留的任何限制(例如 GDPR [5])。在 Databricks 中,可以使用 Pyspark/Spark SQL 查询,通过选择、过滤或映射字段或值来提供所需的数据清洗操作。
客户数据集包含个人身份信息(姓名、电子邮件地址、电话号码和邮政地址),这些信息对于从多个来源链接客户数据至关重要。每个客户数据集还将包含与每个客户相关的属性,例如交易日志、偏好设置、指示会员资格/状态的标志和指示与他们沟通时间的时间戳。为了准备单一客户视图的数据,这些数据集可能需要汇总,以便将任何时间序列或事件日志数据集总结成每个实体的单行表,这些表随后可以在概率匹配阶段进行连接和/或去重/链接。
数据保留策略也可以在此阶段程序化实施,并且可以处理特定用户删除请求的代码。Delta Lake 提供了优化,使得数据湖中的点删除更加高效,使用数据跳过 [6]。如果个人身份信息对于客户数据的后续使用不是必要的,它可以在此阶段被永久匿名化,或者被伪匿名化/拆分为具有不同权限级别的敏感和非敏感表,从而限制对原始客户数据的访问 [3]。
%sql
DELETE FROM data WHERE email = 'customer@exampledomain.com';
VACUUM data;
Delta 表中客户数据的点删除示例
使用 Splink 的概率记录匹配
客户数据集通常包含每个唯一客户的重复记录;客户可能手动输入他们的详细信息,导致拼写错误,随着时间的推移,客户可能更换地址或更改账户信息,造成多个记录中只有一个是有效且最新的,并且当客户在多个数据集中出现时,这些表之间显然会有多个记录需要进行链接。这个去重和记录链接的问题可以通过使用 Splink [7]来解决,Splink 是一个开源的概率记录匹配算法,基于 Spark 实现,由英国司法部团队创建,用于协助司法应用中的记录匹配。

需要去重和链接的客户数据集示例 github.com/moj-analytical-services/splink
Splink 使用贝叶斯算法将多个字段中的模糊匹配分数组合成总体匹配概率,使用户能够使用这些匹配概率阈值标记客户记录在表格之间的链接或表格内给定个体的重复记录。用户可以定义一套需要计算的模糊匹配分数,例如 Jaccard 相似性和 Levenshtein 距离,应用于各种个人数据字段,如姓名、电子邮件、电话号码和地址,以及阻断规则,这些规则通过将模糊匹配比较限制在其他字段上有精确匹配的记录,从而降低匹配过程的计算复杂性。使用期望最大化训练概率记录链接模型,并用来生成预测阻断规则中每对比较的匹配概率。此预测阶段的输出表示一个图数据结构,其中所有记录的配对比较作为节点,用边连接,边表示它们的配对匹配概率,这个图可以通过连接组件算法被解析成匹配概率大于用户定义阈值的高度连接相似记录的簇。

典型 Splink 去重工作流概述。 github.com/moj-analytical-services/splink 经作者许可编辑。
尽管 Splink 需要用户做出大量决策以成功进行记录匹配,尤其是在选择接受或拒绝跨匹配概率范围的匹配的置信水平时,但作为一个开源项目,Splink 不需要商业记录匹配工具所需的许可费用,只要选择合适的匹配规则和阻断规则,并适当设置匹配概率阈值,它就能很好地工作。为了评估 Splink 提供的链接质量,数据管理员理想情况下应该手动验证一组随机样本的匹配质量,以建立对输出结果的信心,并根据数据最终应用的关键性来权衡假阳性和假阴性率。

通过检查不同匹配概率下的随机样本对的匹配准确性。 github.com/moj-analytical-services/splink 经作者许可编辑。
黄金记录逻辑
一旦识别出重复记录或表之间的关联记录,构建单一客户视图的最后阶段是实施黄金记录逻辑,将每个唯一客户的所有记录统一为一行,并定义一组字段以包含所有相关的客户数据用于后续使用[8]。决定哪些字段和哪些客户信息副本作为黄金记录继续使用,将取决于数据的使用方式、记录实践的知识和相关性标准。在下面的示例中,在 Splink 中去重的客户数据集上应用了自定义的黄金记录逻辑(在此处,唯一客户已分配了唯一的“cluster_id”),选择每个客户的最新和最完整的重复记录。其他应用可能会看到多个记录和字段,来自多个表,合并成一个复合记录。此外,可能会在相同的数据上应用多个黄金记录定义,并用于不同的目的(例如,与非会员相比,会员可能会应用不同的黄金记录逻辑)。通常,给定的营销用例会需要特定的客户数据子集,并且可能从同一数据集中派生出多个营销用例;对于这些营销用例中的每一个,可以在此阶段实施不同的标志,以便在后续选择时方便使用。
# count nulls
df_nulls_counted = df.withColumn('numNulls', sum(df[col].isNull().cast('int') for col in df.columns)*-1)
# flag most complete
df_most_complete = df_nulls_counted.withColumn("row_number",f.row_number()\
.over(Window.partitionBy(df_nulls_counted.cluster_id)\
.orderBy(df_nulls_counted.numNulls.desc()))).cache()\
.withColumn('most_complete', f.when(f.col("row_number")==1, 1).otherwise(0)).drop("row_number")
# flag most recent
df_most_complete_most_recent = df_most_complete.withColumn("row_number",f.row_number()\
.over(Window.partitionBy(df_most_complete.cluster_id)\
.orderBy(df_most_complete.createdon_timestamp.desc()))).cache()\
.withColumn('most_recent', f.when(f.col("row_number")==1, 1).otherwise(0)).drop("row_number")
# order by number of nulls
df_golden = df_most_complete_most_recent.withColumn("row_number",f.row_number()\
.over(Window.partitionBy(df_most_complete_most_recent.cluster_id)\
.orderBy(*[f.desc(c) for c in ["numNulls","createdon_timestamp"]]))).cache()\
.withColumn('golden_record', f.when(f.col("row_number")==1, 1).otherwise(0)).drop("row_number")
# add splink duplicate flag
df_golden = df_golden.select('*', f.count('cluster_id')\
.over(Window.partitionBy('cluster_id')).alias('dupeCount'))\
.withColumn('splink_duplicate', f.when(f.col('dupeCount') > 1, 1).otherwise(0))
使用 Pyspark 窗口函数和二进制标志实现的示例黄金记录逻辑
集成与应用案例
如下所示的 Azure 架构可以用于单一客户视图的云部署,作为客户分析和数据科学用例的平台 [9]。使用像 Azure 这样的云平台,可以以具有成本效益的方式进行扩展,同时简化与存储和使用客户数据相关的数据保护和合规方面。Azure 还提供了一套 ETL 和数据科学组件来实施解决方案。各种客户数据源,如 CRM 和销售点系统,可以通过 Azure 中的编排工具,如 Synapse Analytics 和 Data Factory 进行批量加载,Event Hubs 和 Delta Live Tables 用于流数据源,将数据存储在数据湖存储账户中。使用 Databricks lakehouse 架构,使得将多种客户数据类型合并到一个共同的存储账户中变得更加容易,并根据奖牌模式结构化后续转换,包括一个原始铜区、一个具有定义模式的银色单一客户视图区和一个用于任何分析视图或数据科学输出的金色区域,这些区域可以进行维度建模,以便于下游用例,如 CRM 系统中的数据操作用于营销活动、客户分析仪表板帮助你定量理解客户基础,以及基于客户数据构建的其他应用程序,如流失模型或推荐引擎。

Azure 上专注于单一客户视图的分析和数据科学平台的云架构。 https://learn.microsoft.com/en-us/azure/architecture/solution-ideas/articles/azure-databricks-modern-analytics-architecture 经作者许可编辑。
通过在云中存储和处理客户数据,你将部分客户数据安全的责任委托给云服务提供商(如微软),并可以在可能的情况下使用 Azure 环境的私有网络和更高的加密标准,以确保客户数据的安全。通过配置访问控制,可以限制对客户数据的访问,因此,虽然企业内部的广泛受众可以查看匿名化或聚合的客户数据,但原始客户数据的访问可以限制在需要用于营销目的或解决方案维护的业务人员中,从而最大限度地减少泄漏风险。
一旦整理好,单一客户视图就会开启许多数据科学应用的门路,比如按人口统计进行客户画像分析、客户生命周期价值分析或近期-频率-货币价值分析、市场篮子分析(购买模式和共同购买商品组的倾向分析),以及用于流失预测的 ML 建模,从而进行及时的电子邮件干预,如折扣和优惠,以提高客户保留率和推荐引擎,匹配客户和他们在客户生命周期中的特定时间点最有可能购买的产品。单一客户视图的其他分析应用包括汇总客户数据的仪表板,以理解购买趋势和季节性,以及对客户反馈的文本分析,以了解业务可以改进服务的领域;从 CRM 系统的操作角度来看,单一客户视图也是非常有用的,因为它将提高营销活动和其他干预措施的效率,特别是在联系客户的成本较高的情况下,如邮寄营销或冷拨电话/SMS 消息,通过减少对重复客户或过时地址/电话号码的联系。
结论
-
使用开源工具、Databricks 和其他 Azure 组件,可以建立一种具有成本效益且可扩展的单一客户视图,并以安全且合规的方式将其部署到云端。
-
提议的解决方案是高代码的,需要领域专业知识,并且涉及数据管理员在清洗和处理不同客户数据集时的决策。
-
然而,这种解决方案相较于商业产品也有许多优势,如较低的许可费用和运行成本、解决方案的可定制性,以及与其他云组件的便捷集成,以部署分析和数据科学用例。
-
单一客户视图开启了多种具有影响力的数据科学和分析用例的大门,这些用例可以帮助企业更有效地进行市场营销,理解客户并提供更好的服务。
感谢阅读,如果你有兴趣讨论或进一步阅读,请联系我或查看下面的一些参考资料。
www.linkedin.com/in/robert-constable-38b80b151/
参考文献
[2] www.experian.co.uk/assets/about-us/white-papers/single-customer-view-whitepaper.pdf
[3] www.databricks.com/blog/2022/03/23/implementing-the-gdpr-right-to-be-forgotten-in-delta-lake.html
[4] docs.greatexpectations.io/docs/
[5] gdpr-info.eu/
[6] docs.databricks.com/en/security/privacy/gdpr-delta.html
[7] github.com/moj-analytical-services/splink
[8] www.informatica.com/blogs/golden-record.html
使用 LangChain、Google Maps API 和 Gradio 构建智能旅行行程建议器(第一部分)
了解如何构建一个可能激发你下一次公路旅行灵感的应用程序
·
关注 发表在 Towards Data Science ·13 分钟阅读·2023 年 9 月 26 日
--
本文是一个三部分系列的第一部分,我们将使用 OpenAI 和 Google APIs 构建一个旅行行程建议器应用程序,并在 Gradio 生成的简单 UI 中展示它。在这一部分,我们首先讨论了这个项目的提示工程。只想查看代码?请点击 这里.
1. 动机
自 2022 年底 ChatGPT 发布以来,对大型语言模型(LLMs)及其在面向消费者的产品(如聊天机器人和搜索引擎)中的应用兴趣激增。不足一年,我们已经可以访问大量来自Hugging Face的开源 LLM、Lamini等模型托管服务以及 OpenAI 和 PaLM 等付费 API。看到这一领域的发展如此迅速,新的工具和开发范式似乎每几周就会出现,既令人兴奋又有些不知所措。
在这里,我们将仅仅采样这些工具中的一小部分,构建一个有用的应用程序,帮助我们进行旅行规划。在计划度假时,得到曾经去过那里的人建议往往很不错,看到这些建议在地图上展示更好。在没有这种建议的情况下,我有时会浏览 Google 地图,在我想要访问的一般区域内随意选择一些看起来有趣的地方。也许这个过程很有趣,但它效率低且可能会遗漏一些东西。拥有一个能够根据几个高层次偏好提供大量建议的工具岂不是很好吗?
这正是我们尝试构建的系统:一个能够根据一些高层次的偏好提供旅行行程建议的系统,例如“我有 3 天时间探索旧金山,并且喜欢艺术博物馆”。Google 搜索的生成 AI 功能和 ChatGPT 已经可以为类似的查询提供创造性的结果,但我们希望更进一步,生成一个包含旅行时间和美观地图的实际行程,帮助用户定位。

这就是我们将构建的系统:一个生成旅行建议的系统,配有基本地图,显示由 LLM 提供的路线和中途点。
目标更多是熟悉构建此类服务所需的工具,而不是实际部署应用程序,但在此过程中我们将了解一些关于提示工程、与 LangChain 协调的 LLM、使用 Google Maps API 提取方向以及使用 leafmap 和 gradio 显示结果的知识。令人惊叹的是,这些工具能够如此快速地为此类系统构建一个 POC,但真正的挑战总是出现在评估和边缘情况管理上。我们将构建的工具远非完美,如果有人有兴趣进一步帮助我开发,那将是非常棒的。
2. 提示策略
该项目将使用 OpenAI 和 Google PaLM API。你可以通过在这里和这里注册账户来获取 API 密钥。撰写时,Google API 的通用可用性有限,并且有等待名单,但获取访问权限通常只需几天时间。
使用 [dotenv](https://pypi.org/project/python-dotenv/) 是避免将 API 密钥复制粘贴到开发环境中的一种简单方法。在创建了一个包含以下行的 .env 文件之后
OPENAI_API_KEY = {your open ai key}
GOOGLE_PALM_API_KEY = {your google palm api key}
我们可以使用这个函数来加载变量,以便 LangChain 等下游使用。
from dotenv import load_dotenv
from pathlib import Path
def load_secets():
load_dotenv()
env_path = Path(".") / ".env"
load_dotenv(dotenv_path=env_path)
open_ai_key = os.getenv("OPENAI_API_KEY")
google_palm_key = os.getenv("GOOGLE_PALM_API_KEY")
return {
"OPENAI_API_KEY": open_ai_key,
"GOOGLE_PALM_API_KEY": google_palm_key,
}
那么,我们应该如何设计旅行代理服务的提示呢?用户将可以输入他们想要的任何文本,因此我们首先要能够确定他们的查询是否有效。我们肯定要标记任何包含有害内容的查询,例如具有恶意意图的行程请求。
我们还想过滤掉与旅行无关的问题——毫无疑问,大语言模型可以回答这些问题,但这些问题超出了本项目的范围。最后,我们还希望识别出不合理的请求,例如 “我想飞到月球” 或 “我想进行从纽约到东京的三天公路旅行”。对于这样的不合理请求,如果模型能够解释为何不合理并提出有帮助的修改建议,那将是很好的。
一旦请求被验证,我们可以继续提供建议的行程,理想情况下应该包含路线点的具体地址,以便可以将其发送到诸如 Google Maps 的映射或导航 API。
行程应该是人类可读的,包含足够的细节,使用户能够将其作为独立建议使用。像 ChatGPT 这样的庞大、经过指令调优的大语言模型似乎在提供这样的回应方面表现出色,但我们需要确保路线点地址以一致的方式提取。
因此,这里有三个不同的阶段:
-
验证查询
-
生成行程
-
以 Google Maps API 可以理解的格式提取路线点
可能设计一个能够一次完成所有三项工作的提示,但为了便于调试,我们将其拆分为三个大语言模型调用,每个部分一个。
幸运的是,LangChain 的 [PydanticOutputParser](https://python.langchain.com/docs/modules/model_io/output_parsers/pydantic) 确实可以提供帮助,通过提供一组预设的提示,鼓励大语言模型以符合输出模式的方式格式化其回应。
3. 验证提示
让我们看看验证提示,我们可以将其包装在一个模板类中,以便更容易包含和迭代不同的版本。
from langchain.prompts.chat import (
ChatPromptTemplate,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate,
)
from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
class Validation(BaseModel):
plan_is_valid: str = Field(
description="This field is 'yes' if the plan is feasible, 'no' otherwise"
)
updated_request: str = Field(description="Your update to the plan")
class ValidationTemplate(object):
def __init__(self):
self.system_template = """
You are a travel agent who helps users make exciting travel plans.
The user's request will be denoted by four hashtags. Determine if the user's
request is reasonable and achievable within the constraints they set.
A valid request should contain the following:
- A start and end location
- A trip duration that is reasonable given the start and end location
- Some other details, like the user's interests and/or preferred mode of transport
Any request that contains potentially harmful activities is not valid, regardless of what
other details are provided.
If the request is not valid, set
plan_is_valid = 0 and use your travel expertise to update the request to make it valid,
keeping your revised request shorter than 100 words.
If the request seems reasonable, then set plan_is_valid = 1 and
don't revise the request.
{format_instructions}
"""
self.human_template = """
####{query}####
"""
self.parser = PydanticOutputParser(pydantic_object=Validation)
self.system_message_prompt = SystemMessagePromptTemplate.from_template(
self.system_template,
partial_variables={
"format_instructions": self.parser.get_format_instructions()
},
)
self.human_message_prompt = HumanMessagePromptTemplate.from_template(
self.human_template, input_variables=["query"]
)
self.chat_prompt = ChatPromptTemplate.from_messages(
[self.system_message_prompt, self.human_message_prompt]
)
我们的 Validation 类包含查询的输出模式定义,这将是一个包含两个键 plan_is_valid 和 updated_request 的 JSON 对象。在 ValidationTemplate 中,我们使用 LangChain 的有用模板类来构造提示,并创建一个带有 PydanticOutputParser 的解析器对象。这将 Pydantic 代码转换为可以与查询一起传递给 LLM 的一组指令。然后,我们可以在系统模板中引用这些格式指令。每次调用 API 时,我们希望 system_message_prompt 和 human_message_prompt 都发送给 LLM,这就是为什么我们将它们打包在 chat_prompt 中。
由于这实际上并不是一个聊天机器人应用程序(虽然可以将其制作成一个!),我们可以将系统和人工模板放在同一个字符串中,以获得相同的响应。
现在,我们可以创建一个使用 LangChain 调用 LLM API 的 Agent 类。这里我们使用 ChatOpenAI,但如果你更喜欢,也可以用 GooglePalm 替代。
请注意,我们这里还使用了 Langchain 的 LLMChain 和 SequentialChain,尽管我们只是进行了一次 LLM 调用。这可能有些过度,但如果未来我们想添加另一个调用(例如,在验证链运行之前调用 OpenAI moderation API),这可能会有帮助。
import openai
import logging
import time
# for Palm
from langchain.llms import GooglePalm
# for OpenAI
from langchain.chat_models import ChatOpenAI
from langchain.chains import LLMChain, SequentialChain
logging.basicConfig(level=logging.INFO)
class Agent(object):
def __init__(
self,
open_ai_api_key,
model="gpt-3.5-turbo",
temperature=0,
debug=True,
):
self.logger = logging.getLogger(__name__)
self.logger.setLevel(logging.INFO)
self._openai_key = open_ai_api_key
self.chat_model = ChatOpenAI(model=model, temperature=temperature, openai_api_key=self._openai_key)
self.validation_prompt = ValidationTemplate()
self.validation_chain = self._set_up_validation_chain(debug)
def _set_up_validation_chain(self, debug=True):
# make validation agent chain
validation_agent = LLMChain(
llm=self.chat_model,
prompt=self.validation_prompt.chat_prompt,
output_parser=self.validation_prompt.parser,
output_key="validation_output",
verbose=debug,
)
# add to sequential chain
overall_chain = SequentialChain(
chains=[validation_agent],
input_variables=["query", "format_instructions"],
output_variables=["validation_output"],
verbose=debug,
)
return overall_chain
def validate_travel(self, query):
self.logger.info("Validating query")
t1 = time.time()
self.logger.info(
"Calling validation (model is {}) on user input".format(
self.chat_model.model_name
)
)
validation_result = self.validation_chain(
{
"query": query,
"format_instructions": self.validation_prompt.parser.get_format_instructions(),
}
)
validation_test = validation_result["validation_output"].dict()
t2 = time.time()
self.logger.info("Time to validate request: {}".format(round(t2 - t1, 2)))
return validation_test
要运行示例,我们可以尝试以下代码。设置 debug=True 将激活 LangChain 的调试模式,该模式会打印查询文本在通过各种 LangChain 类时的进展情况。
secrets = load_secets()
travel_agent = Agent(open_ai_api_key=secrets[OPENAI_API_KEY],debug=True)
query = """
I want to do a 5 day roadtrip from Cape Town to Pretoria in South Africa.
I want to visit remote locations with mountain views
"""
travel_agent.validate_travel(query)
这个查询似乎合理,因此我们得到如下结果
INFO:__main__:Validating query
INFO:__main__:Calling validation (model is gpt-3.5-turbo) on user input
INFO:__main__:Time to validate request: 1.08
{'plan_is_valid': 'yes', 'updated_request': ''}
现在我们通过将查询更改为不那么合理的内容来进行测试,例如
query = """
I want to walk from Cape Town to Pretoria in South Africa.
I want to visit remote locations with mountain views
"""
响应时间较长,因为 ChatGPT 正在尝试提供有关查询为何无效的解释,因此生成了更多的标记。
INFO:__main__:Validating query
INFO:__main__:Calling validation (model is gpt-3.5-turbo) on user input
INFO:__main__:Time to validate request: 4.12
{'plan_is_valid': 'no',
'updated_request': 'Walking from Cape Town to Pretoria in South Africa is not ...' a
4. 行程提示
如果查询有效,它可以传递到下一个阶段,即行程提示。在这里,我们希望模型返回详细的建议旅行计划,形式应为包含途经地址和有关每个地点活动建议的项目符号列表。这实际上是项目的主要“生成”部分,有很多方法可以设计查询以获得良好的结果。我们的 ItineraryTemplate 看起来像这样
class ItineraryTemplate(object):
def __init__(self):
self.system_template = """
You are a travel agent who helps users make exciting travel plans.
The user's request will be denoted by four hashtags. Convert the
user's request into a detailed itinerary describing the places
they should visit and the things they should do.
Try to include the specific address of each location.
Remember to take the user's preferences and timeframe into account,
and give them an itinerary that would be fun and doable given their constraints.
Return the itinerary as a bulleted list with clear start and end locations.
Be sure to mention the type of transit for the trip.
If specific start and end locations are not given, choose ones that you think are suitable and give specific addresses.
Your output must be the list and nothing else.
"""
self.human_template = """
####{query}####
"""
self.system_message_prompt = SystemMessagePromptTemplate.from_template(
self.system_template,
)
self.human_message_prompt = HumanMessagePromptTemplate.from_template(
self.human_template, input_variables=["query"]
)
self.chat_prompt = ChatPromptTemplate.from_messages(
[self.system_message_prompt, self.human_message_prompt]
)
请注意,这里不需要 Pydantic 解析器,因为我们希望输出是一个字符串,而不是 JSON 对象。
要使用这个,我们可以向 Agent 类添加一个新的 LLMChain,如下所示
travel_agent = LLMChain(
llm=self.chat_model,
prompt=self.itinerary_prompt.chat_prompt,
verbose=debug,
output_key="agent_suggestion",
)
我们在实例化 chat_model 时没有设置 max_tokens 参数,这允许模型决定其输出的长度。特别是对于 GPT4,这可能使响应时间相当长(有时超过 30 秒)。有趣的是,PaLM 的响应时间明显较短。
5. 航点提取提示
使用行程提示可能会给我们一个很好的航点列表,可能是这样的。
- Day 1:
- Start in Berkeley, CA
- Drive to Redwood National and State Parks, CA (1111 Second St, Crescent City, CA 95531)
- Explore the beautiful redwood forests and enjoy nature
- Drive to Eureka, CA (531 2nd St, Eureka, CA 95501)
- Enjoy the local cuisine and explore the charming city
- Overnight in Eureka, CA
- Day 2:
- Start in Eureka, CA
- Drive to Crater Lake National Park, OR (Crater Lake National Park, OR 97604)
- Marvel at the stunning blue lake and hike the scenic trails
- Drive to Bend, OR (Bend, OR 97701)
- Indulge in the local food scene and explore the vibrant city
- Overnight in Bend, OR
- Day 3:
- Start in Bend, OR
- Drive to Mount Rainier National Park, WA (55210 238th Ave E, Ashford, WA 98304)
- Enjoy the breathtaking views of the mountain and hike the trails
- Drive to Tacoma, WA (Tacoma, WA 98402)
- Sample the delicious food options and explore the city's attractions
- Overnight in Tacoma, WA
- Day 4:
- Start in Tacoma, WA
- Drive to Olympic National Park, WA (3002 Mount Angeles Rd, Port Angeles, WA 98362)
- Explore the diverse ecosystems of the park and take in the natural beauty
- Drive to Seattle, WA (Seattle, WA 98101)
- Experience the vibrant food scene and visit popular attractions
- Overnight in Seattle, WA
- Day 5:
- Start in Seattle, WA
- Explore more of the city's attractions and enjoy the local cuisine
- End the trip in Seattle, WA
现在我们需要提取航点的地址,以便可以进行下一步操作,即将它们绘制在地图上,并调用 Google Maps 方向 API 以获取它们之间的路线。
为此,我们将进行另一次 LLM 调用,并再次使用PydanticOutputParser以确保我们的输出格式正确。为了理解这里的格式,简要考虑一下我们在项目的下一阶段(第二部分中介绍)要做的事情是有用的。我们将调用Google Maps Python API,其形式如下。
import googlemaps
gmaps = googlemaps.Client(key=google_maps_api_key)
directions_result = gmaps.directions(
start,
end,
waypoints=waypoints,
mode=transit_type,
units="metric",
optimize_waypoints=True,
traffic_model="best_guess",
departure_time=start_time,
)
其中,start 和 end 是地址字符串,waypoints 是要访问的中间地址列表。
我们请求的航点提取提示的模式因此如下所示。
class Trip(BaseModel):
start: str = Field(description="start location of trip")
end: str = Field(description="end location of trip")
waypoints: List[str] = Field(description="list of waypoints")
transit: str = Field(description="mode of transportation")
这将使我们能够将 LLM 调用的输出插入到方向调用中。
对于这个提示,我发现添加一个一次性示例真的有助于模型符合期望的输出。对较小的开源 LLM 进行微调以使用这些 ChatGPT/PaLM 的结果提取航点列表可能是一个有趣的衍生项目。
class MappingTemplate(object):
def __init__(self):
self.system_template = """
You an agent who converts detailed travel plans into a simple list of locations.
The itinerary will be denoted by four hashtags. Convert it into
list of places that they should visit. Try to include the specific address of each location.
Your output should always contain the start and end point of the trip, and may also include a list
of waypoints. It should also include a mode of transit. The number of waypoints cannot exceed 20.
If you can't infer the mode of transit, make a best guess given the trip location.
For example:
####
Itinerary for a 2-day driving trip within London:
- Day 1:
- Start at Buckingham Palace (The Mall, London SW1A 1AA)
- Visit the Tower of London (Tower Hill, London EC3N 4AB)
- Explore the British Museum (Great Russell St, Bloomsbury, London WC1B 3DG)
- Enjoy shopping at Oxford Street (Oxford St, London W1C 1JN)
- End the day at Covent Garden (Covent Garden, London WC2E 8RF)
- Day 2:
- Start at Westminster Abbey (20 Deans Yd, Westminster, London SW1P 3PA)
- Visit the Churchill War Rooms (Clive Steps, King Charles St, London SW1A 2AQ)
- Explore the Natural History Museum (Cromwell Rd, Kensington, London SW7 5BD)
- End the trip at the Tower Bridge (Tower Bridge Rd, London SE1 2UP)
#####
Output:
Start: Buckingham Palace, The Mall, London SW1A 1AA
End: Tower Bridge, Tower Bridge Rd, London SE1 2UP
Waypoints: ["Tower of London, Tower Hill, London EC3N 4AB", "British Museum, Great Russell St, Bloomsbury, London WC1B 3DG", "Oxford St, London W1C 1JN", "Covent Garden, London WC2E 8RF","Westminster, London SW1A 0AA", "St. James's Park, London", "Natural History Museum, Cromwell Rd, Kensington, London SW7 5BD"]
Transit: driving
Transit can be only one of the following options: "driving", "train", "bus" or "flight".
{format_instructions}
"""
self.human_template = """
####{agent_suggestion}####
"""
self.parser = PydanticOutputParser(pydantic_object=Trip)
self.system_message_prompt = SystemMessagePromptTemplate.from_template(
self.system_template,
partial_variables={
"format_instructions": self.parser.get_format_instructions()
},
)
self.human_message_prompt = HumanMessagePromptTemplate.from_template(
self.human_template, input_variables=["agent_suggestion"]
)
self.chat_prompt = ChatPromptTemplate.from_messages(
[self.system_message_prompt, self.human_message_prompt]
)
现在,让我们向Agent类添加一个新方法,该方法可以使用 SequentialChain 顺序调用ItineraryTemplate和MappingTemplate来调用 LLM。
def _set_up_agent_chain(self, debug=True):
# set up LLMChain to get the itinerary as a string
travel_agent = LLMChain(
llm=self.chat_model,
prompt=self.itinerary_prompt.chat_prompt,
verbose=debug,
output_key="agent_suggestion",
)
# set up LLMChain to extract the waypoints as a JSON object
parser = LLMChain(
llm=self.chat_model,
prompt=self.mapping_prompt.chat_prompt,
output_parser=self.mapping_prompt.parser,
verbose=debug,
output_key="mapping_list",
)
# overall chain allows us to call the travel_agent and parser in
# sequence, with labelled outputs.
overall_chain = SequentialChain(
chains=[travel_agent, parser],
input_variables=["query", "format_instructions"],
output_variables=["agent_suggestion", "mapping_list"],
verbose=debug,
)
return overall_chain
要进行这些调用,我们可以使用以下代码。
agent_chain = travel_agent._set_up_agent_chain()
mapping_prompt = MappingTemplate()
agent_result = agent_chain(
{
"query": query,
"format_instructions": mapping_prompt.parser.get_format_instructions(),
}
)
trip_suggestion = agent_result["agent_suggestion"]
waypoints_dict = agent_result["mapping_list"].dict()
waypoints_dict中的地址应该已经足够格式化以便与 Google Maps 一起使用,但它们也可以进行地理编码,以减少调用方向 API 时出现错误的可能性。航点字典应该类似于这样。
{
'start': 'Berkeley, CA',
'end': 'Seattle, WA',
'waypoints': [
'Redwood National and State Parks, 1111 Second St, Crescent City, CA 95531',
'Crater Lake National Park, Crater Lake National Park, OR 97604',
'Mount Rainier National Park, 55210 238th Ave E, Ashford, WA 98304',
'Olympic National Park, 3002 Mount Angeles Rd, Port Angeles, WA 98362'
],
'transit': 'driving'
}
6. 将所有内容整合在一起
我们现在能够使用 LLM 验证旅行查询,生成详细的行程并提取航点作为可以传递下游的 JSON 对象。你会看到在代码中,几乎所有这些功能都由Agent类处理,该类在TravelMapperBase中实例化并如下使用。
travel_agent = Agent(
open_ai_api_key=openai_api_key,
google_palm_api_key=google_palm_api_key,
debug=verbose,
)
itinerary, list_of_places, validation = travel_agent.suggest_travel(query)
使用 LangChain 使得替换使用的 LLM 变得非常简单。对于 PALM,我们只需声明。
from langchain.llms import GooglePalm
Agent.chat_model = GooglePalm(
model_name="models/text-bison-001",
temperature=0,
google_api_key=google_palm_api_key,
)
对于 OpenAI,我们可以使用上述部分中描述的ChatOpenAI或OpenAI。
现在,我们准备进入下一阶段:我们如何将地点列表转换为一组方向,并在地图上绘制它们以供用户查看?这将在本三部分系列的第二部分中介绍。
感谢阅读!请随时在这里探索完整的代码库 github.com/rmartinshort/travel_mapper。任何改进建议或功能扩展将不胜感激!
使用 LangChain、Google Maps API 和 Gradio 构建智能旅行路线建议器(第二部分)
学习如何构建一个可能激发你下次公路旅行灵感的应用程序
·
关注 发布于 Towards Data Science ·11 分钟阅读·2023 年 9 月 26 日
--
这篇文章是三部分系列中的第二部分,我们使用 OpenAI 和 Google API 构建了一个旅行路线建议应用程序,并通过 gradio 生成的简单 UI 展示。在这一部分,我们讨论了如何使用 Google Maps API 和 folium 从一组途经点生成交互式路线地图。只想看看代码?在 这里。
1. 第一部分回顾
在第一部分的三部分系列中,我们使用 LangChain 和提示工程构建了一个系统,该系统顺序调用 LLM API——无论是谷歌的 PaLM 还是 OpenAI 的 ChatGPT——将用户的查询转换为旅行行程和格式化良好的地址列表。现在是时候看看如何将这些地址列表转换为带有路线标记的旅行路线了。为此,我们主要将使用通过googlemaps包提供的 Google Maps API。我们还将使用folium进行绘图。让我们开始吧!
2. 准备进行 API 调用
要生成 Google Maps 的 API 密钥,你首先需要在 Google Cloud 上创建一个账户。他们提供90 天免费试用期,之后你将按使用的 API 服务支付费用,类似于你在 OpenAI 上的操作。完成后,你可以创建一个项目(我的项目叫 LLMMapper),并导航到 Google Cloud 网站上的 Google Maps Platform 部分。从那里,你应该能访问“密钥与凭据”菜单以生成 API 密钥。你还应该查看“API 和服务”菜单,探索 Google Maps Platform 提供的众多服务。在这个项目中,我们只会使用方向和地理编码服务。我们将对每个途经点进行地理编码,然后查找它们之间的路线。

截图显示了如何导航到 Google Maps Platform 网站的密钥和凭据菜单。在这里你将生成一个 API 密钥。
现在,可以将 Google Maps API 密钥添加到我们之前设置的 .env 文件中
OPENAI_API_KEY = {your open ai key}
GOOGLE_PALM_API_KEY = {your google palm api key}
GOOGLE_MAPS_API_KEY = {your google maps api key here}
要测试这是否有效,请使用第一部分中描述的方法从 .env 文件加载机密。然后我们可以尝试如下进行地理编码调用
import googlemaps
def convert_to_coords(input_address):
return self.gmaps.geocode(input_address)
secrets = load_secets()
gmaps = googlemaps.Client(key=secrets["GOOGLE_MAPS_API_KEY"])
example_coords = convert_to_coords("The Washington Moment, DC")
谷歌地图能够将提供的字符串与实际地点的地址和详细信息匹配,并应返回如下列表
[{'address_components': [{'long_name': '2',
'short_name': '2',
'types': ['street_number']},
{'long_name': '15th Street Northwest',
'short_name': '15th St NW',
'types': ['route']},
{'long_name': 'Washington',
'short_name': 'Washington',
'types': ['locality', 'political']},
{'long_name': 'District of Columbia',
'short_name': 'DC',
'types': ['administrative_area_level_1', 'political']},
{'long_name': 'United States',
'short_name': 'US',
'types': ['country', 'political']},
{'long_name': '20024', 'short_name': '20024', 'types': ['postal_code']}],
'formatted_address': '2 15th St NW, Washington, DC 20024, USA',
'geometry': {'location': {'lat': 38.8894838, 'lng': -77.0352791},
'location_type': 'ROOFTOP',
'viewport': {'northeast': {'lat': 38.89080313029149,
'lng': -77.0338224697085},
'southwest': {'lat': 38.8881051697085, 'lng': -77.0365204302915}}},
'partial_match': True,
'place_id': 'ChIJfy4MvqG3t4kRuL_QjoJGc-k',
'plus_code': {'compound_code': 'VXQ7+QV Washington, DC',
'global_code': '87C4VXQ7+QV'},
'types': ['establishment',
'landmark',
'point_of_interest',
'tourist_attraction']}]
这非常强大!虽然请求有些模糊,但谷歌地图服务准确地匹配到了一个精确的地址,并提供了坐标以及其他可能对开发者有用的本地信息。我们只需要使用formatted_address和place_id字段即可。
3. 构建路线
地理编码对于我们的旅行地图应用程序很重要,因为地理编码 API 似乎在处理模糊或部分完成的地址时比方向 API 更加熟练。无法保证来自 LLM 调用的地址包含足够的信息,以便方向 API 能提供良好的响应,因此首先进行地理编码步骤可以减少错误的可能性。
让我们首先对起点、终点和中间途经点列表调用地理编码器,并将结果存储在字典中
def build_mapping_dict(start, end, waypoints):
mapping_dict = {}
mapping_dict["start"] = self.convert_to_coords(start)[0]
mapping_dict["end"] = self.convert_to_coords(end)[0]
if waypoints:
for i, waypoint in enumerate(waypoints):
mapping_dict["waypoint_{}".format(i)] = convert_to_coords(
waypoint
)[0
return mapping_dict
现在,我们可以利用方向 API 获取包含途经点的从起点到终点的路线
def build_directions_and_route(
mapping_dict, start_time=None, transit_type=None, verbose=True
):
if not start_time:
start_time = datetime.now()
if not transit_type:
transit_type = "driving"
# later we replace this with place_id, which is more efficient
waypoints = [
mapping_dict[x]["formatted_address"]
for x in mapping_dict.keys()
if "waypoint" in x
]
start = mapping_dict["start"]["formatted_address"]
end = mapping_dict["end"]["formatted_address"]
directions_result = gmaps.directions(
start,
end,
waypoints=waypoints,
mode=transit_type,
units="metric",
optimize_waypoints=True,
traffic_model="best_guess",
departure_time=start_time,
)
return directions_result
指南 API 的完整文档在这里,并且可以指定许多不同的选项。注意我们指定了路线的起点和终点,以及途经点的列表,并选择了optimize_waypoints=True,这样 Google Maps 就知道可以调整途经点的顺序以减少总旅行时间。我们还可以指定交通类型,默认为driving,除非另有设置。请回忆一下在第一部分中我们让 LLM 返回交通类型及其行程建议,因此理论上我们也可以在这里利用这一点。
从方向 API 调用返回的字典具有以下键
['bounds',
'copyrights',
'legs',
'overview_polyline',
'summary',
'warnings',
'waypoint_order'
]
在这些信息中,legs 和 overview_polyline 对我们最有用。legs 是一个路线段的列表,每个元素看起来像这样
['distance',
'duration',
'end_address',
'end_location',
'start_address',
'start_location',
'steps',
'traffic_speed_entry',
'via_waypoint'
]
每个 leg 进一步细分为 steps,这是逐步指示和其关联的路线段的集合。这是一个包含以下键的字典列表
['distance',
'duration',
'end_location',
'html_instructions',
'polyline',
'start_location',
'travel_mode'
]
polyline 键是存储实际路线信息的地方。每个 polyline 是一系列坐标的编码表示,Google Maps 生成这些编码作为将长列表的经纬度值压缩成一个字符串的方法。它们是编码字符串,看起来像
“e|peFt_ejVjwHalBzaHqrAxeEoBplBdyCzpDif@njJwaJvcHijJcIabHfiFyqMvkFooHhtE}mMxwJgqK”
你可以在这里阅读更多内容,但幸运的是,我们可以使用decode_polyline工具将它们转换回坐标。例如
from googlemaps.convert import decode_polyline
overall_route = decode_polyline(
directions_result[0]["overview_polyline"]["points"]
)
route_coords = [(float(p["lat"]),float(p["lng"])) for p in overall_route]
这将提供沿路线的经纬度点列表。
这就是我们绘制一个简单地图的所有信息,显示途经点及其连接的正确驾驶路径。我们可以使用 overview_polyline 作为起点,尽管我们稍后会看到,这可能会在高缩放级别的地图上导致分辨率问题。
假设我们从以下查询开始:
“我想从旧金山到拉斯维加斯进行为期 5 天的公路旅行。我想沿着 HW1 参观漂亮的沿海城镇,然后在南加州欣赏山景”
我们的 LLM 调用提取了一个途经点字典,我们运行了build_mapping_dict和build_directions_and_route以从 Google Maps 获得我们的方向结果
我们可以这样提取途经点
marker_points = []
nlegs = len(directions_result[0]["legs"])
for i, leg in enumerate(directions_result[0]["legs"]):
start, start_address = leg["start_location"], leg["start_address"]
end, end_address = leg["end_location"], leg["end_address"]
start_loc = (float(start["lat"]),float(start["lng"]))
end_loc = (float(end["lat"]),float(end["lng"]))
marker_points.append((start_loc,start_address))
if i == nlegs-1:
marker_points.append((end_loc,end_address))
现在,使用 folium 和 branca,我们可以绘制一个漂亮的交互式地图,这个地图应该会出现在 Colab 或 Jupyter Notebook 中
import folium
from branca.element import Figure
figure = Figure(height=500, width=1000)
# decode the route
overall_route = decode_polyline(
directions_result[0]["overview_polyline"]["points"]
)
route_coords = [(float(p["lat"]),float(p["lng"])) for p in overall_route]
# set the map center to be at the start location of the route
map_start_loc = [overall_route[0]["lat"],overall_route[0]["lng"]]
map = folium.Map(
location=map_start_loc,
tiles="Stamen Terrain",
zoom_start=9
)
figure.add_child(map)
# Add the waypoints as red markers
for location, address in marker_points:
folium.Marker(
location=location,
popup=address,
tooltip="<strong>Click for address</strong>",
icon=folium.Icon(color="red", icon="info-sign"),
).add_to(map)
# Add the route as a blue line
f_group = folium.FeatureGroup("Route overview")
folium.vector_layers.PolyLine(
route_coords,
popup="<b>Overall route</b>",
tooltip="This is a tooltip where we can add distance and duration",
color="blue",
weight=2,
).add_to(f_group)
f_group.add_to(map)
当运行此代码时,Folium 将生成一个交互式地图,我们可以探索并点击每一个途经点。

从 Google Maps API 调用结果生成的交互式地图
4. 优化路线
上述方法中,我们通过一个包含航点列表的单次调用来获取 Google Maps 方向 API,然后绘制 overview_polyline,作为概念验证效果很好,但仍存在一些问题:
-
在调用 Google Maps 时,使用
place_id来指定起点、终点和航点名称比使用formatted_address更有效。幸运的是,我们在地理编码调用的结果中获得了place_id,因此我们应该使用它。 -
单次 API 调用中可以请求的航点数量限制为 25(有关详细信息,请参见
developers.google.com/maps/documentation/directions/get-directions)。如果我们从 LLM 获得的行程中有超过 25 个停靠点,我们需要向 Google Maps 发出更多调用,然后合并响应。 -
overview_polyline在放大时分辨率有限,可能是因为它沿线的点数经过了大规模地图视图的优化。这对于一个概念验证来说不是主要问题,但如果能对路线分辨率进行更多控制,使其在高缩放级别下也能保持良好外观,那就更好了。方向 API 为我们提供了更细致的路段折线,我们可以利用这些信息。 -
在地图上,将路线拆分为单独的路段并允许用户查看与每个路段相关的距离和旅行时间是很好的功能。同样,Google Maps 提供了这些信息,因此我们应该加以利用。

overview_polyline 的分辨率有限。在这里,我们已经缩放到圣巴巴拉,但尚不清楚我们应该走哪些道路。
问题 1 可以通过修改 build_directions_and_route 来使用 mapping_dict 中的 place_id 而不是 formatted_address 来轻松解决。问题 2 稍微复杂一些,需要将初始航点拆分成一些最大长度的块,从每个块中创建起点、终点和子列表,然后在这些块上运行 build_mapping_dict 和 build_directions_and_route。最终结果可以在最后合并。
问题 3 和 4 可以通过使用 Google Maps 返回的每段路程的单独步骤折线来解决。我们只需遍历这两个级别,解码相关的折线,然后构建一个新的字典。这也使我们能够提取距离和持续时间值,这些值被分配给每个解码的路段,然后用于绘图。
def get_route(directions_result):
waypoints = {}
for leg_number, leg in enumerate(directions_result[0]["legs"]):
leg_route = {}
distance, duration = leg["distance"]["text"], leg["duration"]["text"]
leg_route["distance"] = distance
leg_route["duration"] = duration
leg_route_points = []
for step in leg["steps"]:
decoded_points = decode_polyline(step["polyline"]["points"])
for p in decoded_points:
leg_route_points.append(f'{p["lat"]},{p["lng"]}')
leg_route["route"] = leg_route_points
waypoints[leg_number] = leg_route
return waypoints
现在的问题是 leg_route_points 列表可能会变得非常长,当我们在地图上绘制这些点时,可能会导致 folium 崩溃或运行非常缓慢。解决方案是沿路线采样这些点,以确保有足够的点以便进行良好的可视化,但又不至于让地图加载困难。
一种简单且安全的方法是计算总路线应包含的点数(例如 5000 个点),然后确定每段路线应包含的点的比例,并均匀地从每段中采样相应数量的点。请注意,我们需要确保每段至少包含一个点,以便它能够显示在地图上。
以下函数将执行此采样,输入一个来自上面get_route函数的waypoints字典。
def sample_route_with_legs(route, distance_per_point_in_km=0.25)):
all_distances = sum([float(route[i]["distance"].split(" ")[0]) for i in route])
# Total points in the sample
npoints = int(np.ceil(all_distances / distance_per_point_in_km))
# Total points per leg
points_per_leg = [len(v["route"]) for k, v in route.items()]
total_points = sum(points_per_leg)
# get number of total points that need to be represented on each leg
number_per_leg = [
max(1, np.round(npoints * (x / total_points), 0)) for x in points_per_leg
]
sampled_points = {}
for leg_id, route_info in route.items():
total_points = int(points_per_leg[leg_id])
total_sampled_points = int(number_per_leg[leg_id])
step_size = int(max(total_points // total_sampled_points, 1.0))
route_sampled = [
route_info["route"][idx] for idx in range(0, total_points, step_size)
]
distance = route_info["distance"]
duration = route_info["duration"]
sampled_points[leg_id] = {
"route": [
(float(x.split(",")[0]), float(x.split(",")[1]))
for x in route_sampled
],
"duration": duration,
"distance": distance,
}
return sampled_points
在这里我们指定了我们想要的点间距——每 250 米一个点——然后相应地选择点的数量。我们还可以考虑从路线长度估算所需的点间距,但这种方法在第一次尝试中似乎效果相当好,在地图上的中等高的缩放级别下提供了可接受的分辨率。
现在我们已经将路线拆分为具有合理样本点数量的段落,我们可以继续将它们绘制在地图上,并使用以下代码对每一段进行标注。
for leg_id, route_points in sampled_points.items():
leg_distance = route_points["distance"]
leg_duration = route_points["duration"]
f_group = folium.FeatureGroup("Leg {}".format(leg_id))
folium.vector_layers.PolyLine(
route_points["route"],
popup="<b>Route segment {}</b>".format(leg_id),
tooltip="Distance: {}, Duration: {}".format(leg_distance, leg_duration),
color="blue",
weight=2,
).add_to(f_group)
# assumes the map has already been generated
f_group.add_to(map)

这是一个标注并注释过的路线段示例,以便它能够出现在地图上。
5. 整合所有内容
在代码库中,以上提到的所有方法都被打包在两个类中。第一个是RouteFinder,它接受Agent的结构化输出(见第一部分),并生成采样路线。第二个是RouteMapper,它接收采样路线并绘制一个 folium 地图,可以保存为 html 文件。
由于我们几乎总是希望在请求路线时生成地图,RouteFinder的generate_route方法处理这两个任务。
class RouteFinder:
MAX_WAYPOINTS_API_CALL = 25
def __init__(self, google_maps_api_key):
self.logger = logging.getLogger(__name__)
self.logger.setLevel(logging.INFO)
self.mapper = RouteMapper()
self.gmaps = googlemaps.Client(key=google_maps_api_key)
def generate_route(self, list_of_places, itinerary, include_map=True):
self.logger.info("# " * 20)
self.logger.info("PROPOSED ITINERARY")
self.logger.info("# " * 20)
self.logger.info(itinerary)
t1 = time.time()
directions, sampled_route, mapping_dict = self.build_route_segments(
list_of_places
)
t2 = time.time()
self.logger.info("Time to build route : {}".format((round(t2 - t1, 2))))
if include_map:
t1 = time.time()
self.mapper.add_list_of_places(list_of_places)
self.mapper.generate_route_map(directions, sampled_route)
t2 = time.time()
self.logger.info("Time to generate map : {}".format((round(t2 - t1, 2))))
return directions, sampled_route, mapping_dict
回想一下在第一部分中我们构建了一个名为Agent的类,该类处理 LLM 调用。现在我们还有了RouteFinder,我们可以将它们组合到整个旅行映射器项目的基础类中。
class TravelMapperBase(object):
def __init__(
self, openai_api_key, google_palm_api_key, google_maps_key, verbose=False
):
self.travel_agent = Agent(
open_ai_api_key=openai_api_key,
google_palm_api_key=google_palm_api_key,
debug=verbose,
)
self.route_finder = RouteFinder(google_maps_api_key=google_maps_key)
def parse(self, query, make_map=True):
itinerary, list_of_places, validation = self.travel_agent.suggest_travel(query)
directions, sampled_route, mapping_dict = self.route_finder.generate_route(
list_of_places=list_of_places, itinerary=itinerary, include_map=make_map
)
这可以通过以下查询运行,这也是在test_without_gradio脚本中给出的示例。
from travel_mapper.TravelMapper import load_secrets, assert_secrets
from travel_mapper.TravelMapper import TravelMapperBase
def test(query=None):
secrets = load_secrets()
assert_secrets(secrets)
if not query:
query = """
I want to do 2 week trip from Berkeley CA to New York City.
I want to visit national parks and cities with good food.
I want use a rental car and drive for no more than 5 hours on any given day.
"""
mapper = TravelMapperBase(
openai_api_key=secrets["OPENAI_API_KEY"],
google_maps_key=secrets["GOOGLE_MAPS_API_KEY"],
google_palm_api_key=secrets["GOOGLE_PALM_API_KEY"],
)
mapper.parse(query, make_map=True)
就路线和地图生成而言,我们现在已经完成了!但是我们如何将所有这些代码打包成一个易于实验的漂亮 UI 呢?这将会在本系列的第三部分中讲解。
感谢阅读!请随时在这里探索完整的代码库github.com/rmartinshort/travel_mapper。任何改进建议或功能扩展都会非常感谢!
使用 LangChain、Google Maps API 和 Gradio 构建智能旅行行程建议器(第三部分)
了解如何构建一个可能激发你下一次公路旅行灵感的应用
·
关注 发表在 Towards Data Science ·6 分钟阅读·2023 年 9 月 26 日
--
本文是一个三部分系列中的最后一篇,我们使用 OpenAI 和 Google API 构建了一个旅行行程建议应用,并通过 Gradio 生成的简单 UI 展示它。在这一部分,我们讨论如何构建该 UI,将我们在第一部分和第二部分中构建的 Agent 和 RouteFinder 模块组合在一起。只想查看代码?请在这里找到。
1. 第二部分回顾
在这个三部分系列的第二部分,我们构建了一个系统,该系统从一系列 LLM 调用中获取解析后的路标列表(第一部分),并使用 Google Maps API 和 Folium 生成它们之间的路线,并将其绘制在交互式地图上。回顾一下,我们在这个项目中的目标是构建一个应用程序,允许用户轻松输入旅行请求,例如“从柏林到苏黎世的四天行程,我想尝试大量当地啤酒和食物”,并返回详细的行程安排和地图供他们探索。感谢第一部分和第二部分,我们已经组装了所有组件,现在只需将它们组合在一个使使用变得简单的 UI 中即可。
2. 将地图连接到 Gradio
gradio是一个出色的库,用于快速构建能够展示机器学习模型的交互式应用程序。它有一个gradio.Plot组件,旨在与 Matplotlib、Bokeh 和 Plotly 配合使用(详细信息这里)。然而,我们在第二部分生成的地图是使用 Folium 制作的。虽然可以使用这些其他库重新制作这些地图,但幸运的是,我们不需要这么做。相反,我们可以使用leafmap包,它允许我们重用已有的 Folium 代码,并强制输出 Gradio 可以理解的 HTML。详细信息可以在这里找到。
让我们来看一个简单的例子,了解它是如何工作的。首先,我们将创建一个函数,从中输出所需格式的 HTML。
import leafmap.foliumap as leafmap
import folium
import gradio as gr
def generate_map(center_coordinates, zoom_level):
coords = center_coordinates.split(",")
lat, lon = float(coords[0]), float(coords[1])
map = leafmap.Map(location=(lat,lon), tiles="Stamen Terrain", zoom_start=zoom_level)
return map.to_gradio()
在这里,函数generate_map接收一个格式为“lat,lon”的坐标字符串和一个 Folium 地图的缩放级别。它生成地图并将其转换为 Gradio 可以读取的格式。
接下来,让我们构建一个非常简单的 Gradio 界面来展示我们的地图。
demo = gr.Blocks()
with demo:
gr.Markdown("## Generate a map")
with gr.Row():
with gr.Column():
# first col is for buttons
coordinates_input = gr.Textbox(value="",label="Your center coordines",lines=1)
zoom_level_input = gr.Dropdown(choices=[1,2,3,4,5,6,7,8,9],label="choose zoom level")
map_button = gr.Button("Generate map")
with gr.Column():
# second col is for the map
map_output = gr.HTML(label="Travel map")
map_button.click(generate_map, inputs=[coordinates_input,zoom_level_input], outputs=[map_output])
# run this in a notebook to display the UI
demo.queue().launch(debug=True)
在这里,我们利用Blocks API,它为我们提供了设置应用程序 UI 的灵活性。我们创建了一行组件,分为两列。第一列包含三个元素:一个文本框供用户输入所需的中心坐标,一个下拉菜单选择缩放级别,以及一个名为“生成地图”的按钮,用户需要点击这个按钮。
在第二列,我们有map_output,这是一个gradio.HTML()组件,用于显示地图的 HTML。
然后,我们需要做的就是定义点击 map_button 时发生的事情。当发生这种情况时,我们将运行 generate_map 函数,传入从 coordinates_input 和 zoom_input 中选择的值。结果将被发送到 map_output 变量。
运行此代码会产生以下用户界面

使用 leafmap 和 gradio 生成的基本映射用户界面
这确实不复杂也不精美,但它包含了使用 gradio 构建映射工具的基本元素。
3. 为我们的旅行代理商创建一个简单的用户界面
让我们首先看看 gradio 应用程序在我们检查代码之前的一些功能。不过要注意,gradio 提供了大量组件来创建复杂且美观的用户界面,而这个旅行地图用户界面仍然处于 POC 阶段。

旅行地图应用最终版本中所有组件的描述
我们的应用程序本质上有两列。第一列包含一个文本框,供用户输入查询,一个单选按钮组,允许我们在模型之间切换,以及一个显示验证检查输出的文本框。
第二列包含由 leafmap.folium 生成的地图和一个显示 LLM 调用完整文本行程输出的文本框。“生成地图” 按钮在底部,截图中不可见。
多亏了 gradio 在后台完成的所有工作,所有这些代码都非常简洁。
import gradio as gr
from travel_mapper.TravelMapper import TravelMapperForUI, load_secrets, assert_secrets
from travel_mapper.user_interface.utils import generate_generic_leafmap
from travel_mapper.user_interface.constants import EXAMPLE_QUERY
def main():
# load the AP keys
secrets = load_secrets()
assert_secrets(secrets)
# set up travel mapper (see part 2)
travel_mapper = TravelMapperForUI(
openai_api_key=secrets["OPENAI_API_KEY"],
google_maps_key=secrets["GOOGLE_MAPS_API_KEY"],
google_palm_api_key=secrets["GOOGLE_PALM_API_KEY"],
)
# build the UI in gradio
app = gr.Blocks()
# make a generic map to display when the app first loads
generic_map = generate_generic_leafmap()
with app:
gr.Markdown("## Generate travel suggestions")
# make multple tabs
with gr.Tabs():
# make the first tab
with gr.TabItem("Generate with map"):
# make rows 1 within tab 1
with gr.Row():
# make column 1 within row 1
with gr.Column():
text_input_map = gr.Textbox(
EXAMPLE_QUERY, label="Travel query", lines=4
)
radio_map = gr.Radio(
value="gpt-3.5-turbo",
choices=["gpt-3.5-turbo", "gpt-4", "models/text-bison-001"],
label="models",
)
query_validation_text = gr.Textbox(
label="Query validation information", lines=2
)
# make column 2 within row 1
with gr.Column():
# place where the map will appear
map_output = gr.HTML(generic_map, label="Travel map")
# place where the suggested trip will appear
itinerary_output = gr.Textbox(
value="Your itinerary will appear here",
label="Itinerary suggestion",
lines=3,
)
# generate button
map_button = gr.Button("Generate")
# make the second tab
with gr.TabItem("Generate without map"):
# make the first row within the second tab
with gr.Row():
# make the first column within the first row
with gr.Column():
text_input_no_map = gr.Textbox(
value=EXAMPLE_QUERY, label="Travel query", lines=3
)
radio_no_map = gr.Radio(
value="gpt-3.5-turbo",
choices=["gpt-3.5-turbo", "gpt-4", "models/text-bison-001"],
label="Model choices",
)
query_validation_no_map = gr.Textbox(
label="Query validation information", lines=2
)
# make the second column within the first row
with gr.Column():
text_output_no_map = gr.Textbox(
value="Your itinerary will appear here",
label="Itinerary suggestion",
lines=3,
)
# generate button
text_button = gr.Button("Generate")
# instructions for what happens whrn the buttons are clicked
# note use of the "generate_with_leafmap" method here.
map_button.click(
travel_mapper.generate_with_leafmap,
inputs=[text_input_map, radio_map],
outputs=[map_output, itinerary_output, query_validation_text],
)
text_button.click(
travel_mapper.generate_without_leafmap,
inputs=[text_input_no_map, radio_no_map],
outputs=[text_output_no_map, query_validation_no_map],
)
# run the app
app.launch()
4. 创建包
从 github 上查看存储库可以看出,旅行地图代码是通过 cookiecutter 的标准模板构建的,但模板中的一些重要部分尚未填充。理想情况下,我们会包括单元测试和集成测试,并完成存储库设置,以便使用持续集成/持续交付(CI/CD)概念。如果项目在这个 POC 阶段之后进一步发展,这些方面将在未来添加。
代码可以通过几种方式在本地运行。如果我们将上述 main 函数放入一个名为 driver.py 的脚本中,我们应该能够从 travel_mapper 项目的顶层从终端运行它。如果包成功运行,终端中应出现类似以下的消息
Running on local URL: http://127.0.0.1:7860
将此网址复制粘贴到网络浏览器中应该会显示出在本地运行的 gradio 应用。当然,如果我们真的想将其部署到网络上(我不推荐,因为 API 调用的费用),需要更多的步骤,但这超出了这些文章的范围。
驱动程序也可以从名为 run.sh 的 bash 脚本中运行,该脚本可以在代码库的 user_interface 模块中找到。
# Run the UI
# run this from the top level directory of the travel mapper project
export PYTHONPATH=$PYTHONPATH:$(pwd)
echo "Starting travel mapper UI"
$(pwd)/travel_mapper/user_interface/driver.py
从项目的顶层运行时,这也会正确设置PYTHONPATH,确保项目特定的导入语句始终被识别。
这就是本系列的全部内容,感谢你一直看到最后!请随时在这里探索完整的代码库 github.com/rmartinshort/travel_mapper。任何改进建议或功能扩展的意见都非常受欢迎!
使用 Redshift Serverless 和 Kinesis 构建流数据管道
面向初学者的完整教程
·发表于Towards Data Science ·阅读时间 9 分钟·2023 年 10 月 6 日
--

图片由Sebastian Pandelache拍摄,来源于Unsplash
在本文中,我将讨论最受欢迎的数据管道设计模式之一——事件流。除了其他好处,它还支持超快的数据分析,我们可以创建实时更新结果的报告仪表盘。我将演示如何通过构建一个使用 AWS Kinesis 和 Redshift 的流数据管道来实现这一点,并且可以通过几次点击使用基础设施即代码进行部署。我们将使用 AWS CloudFormation 来描述我们的数据平台架构并简化部署过程。
想象一下,作为数据工程师,你的任务是创建一个将服务器事件流与数据仓库解决方案(Redshift)连接起来的数据管道,以便转换数据并创建分析仪表盘。

管道基础设施。图片来源:作者。
什么是数据管道?
它是一个数据处理步骤的序列。由于这些阶段之间的逻辑数据流连接,每个阶段生成一个输出,作为下一个阶段的输入。
我之前在这篇文章中写过相关内容:
选择合适的架构及示例
towardsdatascience.com
例如,事件数据可以由后端的源创建,使用 Kinesis Firehose 或 Kafka 流构建事件流。然后它可以馈送到多个不同的消费者或目的地。
流式处理是企业数据的“必备”解决方案,因其流数据处理能力。它能够实现实时数据分析。
在我们的用例场景中,我们可以设置一个ELT 流式数据管道到 AWS Redshift。AWS Firehose 流可以提供这种无缝集成,当数据流被直接上传到数据仓库表时。然后,数据可以被转换以使用 AWS Quicksight 作为 BI 工具来创建报告,例如。

添加了 BI 组件。图片来源于作者。
本教程假设学习者熟悉 AWS CLI 并且具有基本的 Python 知识。
工作流程
1. 首先,我们将使用 AWS CloudFormation 创建 Kinesis 数据流。
2. 我们将使用 AWS Lambda 向此事件流发送示例数据事件。
3. 最后,我们将配置 AWS Redshift 集群并测试我们的流式管道。
创建 AWS Kinesis 数据流
AWS Kinesis Data Streams 是一个 Amazon Kinesis 实时数据流解决方案。它提供了出色的可扩展性和耐用性,数据流可以被任何消费者访问。
我们可以使用 CloudFormation 模板来创建它。下面的命令行脚本将触发 AWS CLI 命令进行部署:
KINESIS_STACK=YourRedshiftDataStream
ENV=staging
aws \
cloudformation deploy \
--template-file kinesis-data-stream.yaml \
--stack-name $KINESIS_STACK \
--capabilities CAPABILITY_IAM \
--parameter-overrides \
"Environment"=$ENV
并且模板 kinesis-data-stream.yaml 将如下所示:
AWSTemplateFormatVersion: 2010-09-09
Description: >
Firehose resources relating to statistics generation.
Repository - https://github.com/your_repository.
Parameters:
Environment:
AllowedValues:
- staging
- production
Description: Target environment
Type: String
Default: 'staging'
Resources:
MyKinesisStream:
Type: AWS::Kinesis::Stream
Properties:
Name: !Sub 'your-data-stream-${Environment}'
RetentionPeriodHours: 24
StreamModeDetails:
StreamMode: ON_DEMAND
# ShardCount: 1
Tags:
-
Key: Environment
Value: Production
非常简单。如果一切顺利,我们将看到我们的 Kinesis 流被部署:

流已创建。图片来源于作者。
2. 创建 AWS Lambda 函数以模拟事件流
现在我们希望将一些事件发送到我们的 Kinesis 数据流。为此,我们可以创建一个无服务器应用程序,例如 AWS Lambda。我们将使用boto3库(AWS 的 Python SDK)来构建一个数据连接器与 AWS Kinesis 进行数据源连接。

本地运行应用以模拟事件流。图片来源于作者。
我们的应用程序文件夹结构可以如下所示:
.
├── app.py
├── config
│ └── staging.yaml
├── env.json
└── requirements.txt
我们的app.py必须能够向 Kinesis 数据流发送事件:
# Make sure boto3 is installed locally, i.e. pip install boto3
import json
import random
import boto3
kinesis_client = boto3.client('kinesis', region_name='eu-west-1')
# Constants:
STREAM_NAME = "your-data-stream-staging"
def lambda_handler(event, context):
processed = 0
print(STREAM_NAME)
try:
print('Trying to send events to Kinesis...')
for i in range(0, 5):
data = get_data()
print(i, " : ", data)
kinesis_client.put_record(
StreamName=STREAM_NAME,
Data=json.dumps(data),
PartitionKey="partitionkey")
processed += 1
except Exception as e:
print(e)
message = 'Successfully processed {} events.'.format(processed)
return {
'statusCode': 200,
'body': { 'lambdaResult': message }
}
我们希望添加一个帮助函数来生成一些随机事件数据。例如:
# Helpers:
def get_data():
return {
'event_time': datetime.now().isoformat(),
'event_name': random.choice(['JOIN', 'LEAVE', 'OPEN_CHAT', 'SUBSCRIBE', 'SEND_MESSAGE']),
'user': round(random.random() * 100)}
我们可以使用python-lambda-local库本地运行和测试 AWS Lambda,方法如下:
pip install python-lambda-local
cd stack
python-lambda-local -e events_connector/env.json -f lambda_handler events_connector/app.py event.json --timeout 10000
# -e is for environment variables if you choose to use them.
# event.json - sample JSON event to invoke our Lambda with.
env.json 只是一个事件负载,用于本地运行 Lambda。
config/staging.yaml 可以包含我们应用程序未来可能需要的任何环境特定设置。例如:
# staging.yaml
Kinesis:
DataStreamNsme: your-data-stream-staging
如果你需要使用requirements.txt,它可以如下所示:
requests==2.28.1
pyyaml==6.0
boto3==boto3-1.26.90
python-lambda-local==0.1.13
在你的命令行中运行这个:
cd stack
pip install -r events_connector/requirements.txt
这种方法很有用,因为我们可能希望将无服务器应用程序部署到云中并进行调度。我们可以使用 CloudFormation 模板来实现这一点。我之前在这里写过:
## Infrastructure as Code for Beginners
使用这些模板像专业人士一样部署数据管道
当我们使用 CloudFormation 模板时,应用程序可以通过类似的 shell 脚本进行部署:
PROFILE=your-aws-profile
STACK_NAME=YourStackNameLive
LAMBDA_BUCKET=your-lambdas-bucket.aws # Make sure it exists
date
TIME=`date +"%Y%m%d%H%M%S"`
base=${PWD##*/}
zp=$base".zip"
echo $zp
rm -f $zp
pip install --target ./package -r requirements.txt
cd package
zip -r ../${base}.zip .
cd $OLDPWD
zip -r $zp ./events_connector -x __pycache__
aws --profile $PROFILE s3 cp ./${base}.zip s3://${LAMBDA_BUCKET}/events_connector/${base}${TIME}.zip
aws --profile $PROFILE \
cloudformation deploy \
--template-file stack.yaml \
--stack-name $STACK_NAME \
--capabilities CAPABILITY_IAM \
--parameter-overrides \
"StackPackageS3Key"="events_connector/${base}${TIME}.zip" \
"Environment"="staging" \
"Testing"="false"
这是一个灵活的设置,允许我们创建强大的 CI/CD 管道。我记得我在下面的帖子中创建了一个。
Continuous Integration and Deployment for Data Platforms
数据工程师和 ML Ops 的 CI/CD
[towardsdatascience.com
创建 Redshift Serverless 资源
现在我们需要为我们的流数据管道创建 Redshift Serverless 集群。我们可以手动或使用 CloudFormation 模板配置 Redshift Workgroup、创建 Namespace 和其他所需资源。
Redshift Serverless 仅仅是一个数据仓库解决方案。它可以执行任何规模的分析工作负载,无需数据仓库基础设施管理。Redshift 运行迅速,并能在几秒钟内从巨量数据中生成洞察。它会自动扩展,为即使是最苛刻的应用程序提供快速性能。

例子视图显示了我们应用程序的事件。图像来源于作者。
在我们的案例中,我们可以使用 CloudFormation 模板定义来部署 Redshift 资源。
AWSTemplateFormatVersion: 2010-09-09
Parameters:
DatabaseName:
Description: The name of the first database in the Amazon Redshift Serverless environment.
Type: String
Default: dev
MaxLength: 127
AllowedPattern: '[a-zA-Z][a-zA-Z_0-9+.@-]*'
AdminUsername:
Description: The administrator's user name for Redshift Serverless Namespace being created.
Type: String
Default: admin
AllowedPattern: '[a-zA-Z][a-zA-Z_0-9+.@-]*'
AdminUserPassword:
Description: The password associated with admin user.
Type: String
NoEcho: 'true'
Default: Admin123
MinLength: 8
MaxLength: 64
# AllowedPattern: '^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[^\x00-\x20\x22\x27\x2f\x40\x5c\x7f-\uffff]+'
NamespaceName:
Description: A unique identifier that defines the Namespace.
Default: rswg
Type: String
MinLength: 3
MaxLength: 64
AllowedPattern: '^[a-z0-9-]+$'
WorkgroupName:
Description: A unique identifier that defines the Workspace.
Default: redshiftworkgroup
Type: String
MinLength: 3
MaxLength: 64
AllowedPattern: '^[a-z0-9-]*$'
BaseRPU:
Description: Base RPU for Redshift Serverless Workgroup.
Type: Number
MinValue: 8
MaxValue: 512
Default: 8
AllowedValues: [8,16,32,40,48,56,64,72,80,88,96,104,112,120,128,136,144,152,160,168,176,184,192,200,208,216,224,232,240,248,256,264,272,280,288,296,304,312,320,328,336,344,352,360,368,376,384,392,400,408,416,424,432,440,448,456,464,472,480,488,496,504,512]
PubliclyAccessible:
Description: Redshift Serverless instance to be publicly accessible.
Type: String
Default: true
AllowedValues:
- true
- false
SubnetId:
Description: You must have at least three subnets, and they must span across three Availability Zones
Type: List<AWS::EC2::Subnet::Id>
SecurityGroupIds:
Description: The list of SecurityGroupIds in your Virtual Private Cloud (VPC).
Type: List<AWS::EC2::SecurityGroup::Id>
LogExportsList:
Description: Provide comma seperate values from list "userlog","connectionlog","useractivitylog". E.g userlog,connectionlog,useractivitylog. If left blank, LogExport is turned off.
Type: CommaDelimitedList
Default: userlog,connectionlog,useractivitylog
EnhancedVpcRouting:
Description: The value that specifies whether to enable enhanced virtual private cloud (VPC) routing, which forces Amazon Redshift Serverless to route traffic through your VPC.
Type: String
AllowedValues:
- true
- false
Default: false
Metadata:
'AWS::CloudFormation::Interface':
ParameterGroups:
- Label:
default: Namespace parameters
Parameters:
- NamespaceName
- DatabaseName
- AdminUsername
- AdminUserPassword
- IAMRole
- LogExportsList
- Label:
default: Workgroup parameters
Parameters:
- WorkgroupName
- BaseRPU
- PubliclyAccessible
- SubnetId
- SecurityGroupIds
- EnhancedVpcRouting
ParameterLabels:
DatabaseName:
default: "Database Name"
AdminUsername:
default: "Admin User Name"
AdminUserPassword:
default: "Admin User Password"
NamespaceName:
default: "Namespace"
WorkgroupName:
default: "Workgroup"
BaseRPU:
default: "Base RPU"
PubliclyAccessible:
default: "Publicly accessible"
SubnetId:
default: "Subnet Ids (Select 3 Subnet Ids spanning 3 AZs)"
SecurityGroupIds:
default: "Security Group Id"
IAMRole:
default: "Associate IAM Role"
EnhancedVpcRouting:
default: "Enhanced VPC Routing"
LogExportsList:
default: "Log Export List"
Resources:
RedshiftAccessRole:
Type: AWS::IAM::Role
Properties:
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSGlueServiceRole
- arn:aws:iam::aws:policy/AmazonRedshiftAllCommandsFullAccess
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
-
Effect: Allow
Principal:
Service:
- redshift.amazonaws.com
Action:
- sts:AssumeRole
RedshiftRolePolicy:
Type: AWS::IAM::Policy
Properties:
PolicyName: RedshiftRolePolicy
PolicyDocument:
Version: 2012-10-17
Statement:
-
Effect: Allow
Action: s3:ListAllMyBuckets
Resource: arn:aws:s3:::*
-
Effect: Allow
Action:
- 's3:Get*'
- 's3:List*'
Resource: '*'
-
Effect: Allow
Action: cloudwatch:*
Resource: "*"
-
Effect: Allow
Action: kinesis:*
Resource: "*"
Roles:
- !Ref RedshiftAccessRole
RedshiftServerlessNamespace:
DependsOn: RedshiftAccessRole
Type: 'AWS::RedshiftServerless::Namespace'
Properties:
AdminUsername:
Ref: AdminUsername
AdminUserPassword:
Ref: AdminUserPassword
DbName:
Ref: DatabaseName
NamespaceName:
Ref: NamespaceName
IamRoles:
- !GetAtt [ RedshiftAccessRole, Arn ]
LogExports:
Ref: LogExportsList
RedshiftServerlessWorkgroup:
Type: 'AWS::RedshiftServerless::Workgroup'
Properties:
WorkgroupName:
Ref: WorkgroupName
NamespaceName:
Ref: NamespaceName
BaseCapacity:
Ref: BaseRPU
PubliclyAccessible:
Ref: PubliclyAccessible
SubnetIds:
Ref: SubnetId
SecurityGroupIds:
Ref: SecurityGroupIds
EnhancedVpcRouting:
Ref: EnhancedVpcRouting
DependsOn:
- RedshiftServerlessNamespace
Outputs:
ServerlessNamespace:
Description: Name of the namespace
Value: !Ref NamespaceName
ServerlessWorkgroup:
Description: Name of the workgroup
Value: !Ref WorkgroupName
所以如果我们在命令行中运行这段代码,它将部署这个堆栈:
STACK=YourRedshiftServerless
SUBNETID=subnet-1,subnet-2,subnet-3
SECURITYGROUPIDS=sg-your-security-group
aws \
cloudformation deploy \
--template-file redshift-serverless.yaml \
--stack-name $STACK \
--capabilities CAPABILITY_IAM \
--parameter-overrides \
"SubnetId"=$SUBNETID \
"SecurityGroupIds"=$SECURITYGROUPIDS
通常,我们希望在私有子网中部署数据库。然而,在开发的早期阶段,你可能希望从开发机器直接访问 Redshift。
这不推荐用于生产环境,但在这种开发情况下,你可以先将 Redshift 放入我们的
defaultVPC 子网。现在,当所有所需的管道资源成功配置后,我们可以连接我们的 Kinesis 流和 Redshift 数据仓库。
然后我们可以使用 SQL 语句在 Redshift 中创建 kinesis_data 模式:
CREATE EXTERNAL SCHEMA kinesis_data
FROM KINESIS
IAM_ROLE 'arn:aws:iam::123456789:role/rs3-RedshiftAccessRole-1TU31HQNXM0EK';
;
CREATE MATERIALIZED VIEW "your-stream-view" AUTO REFRESH YES AS
SELECT approximate_arrival_timestamp,
partition_key,
shard_id,
sequence_number,
refresh_time,
JSON_PARSE(kinesis_data) as payload
FROM kinesis_data."your-data-stream-staging";
;
这段 SQL 的第一部分将设置 AWS Kinesis 作为数据源。第二部分将创建一个包含我们应用程序事件数据的视图。
确保创建一个具有 AmazonRedshiftAllCommandsFullAccess AWS 管理策略的 AWS Redshift 角色。
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "s3:ListAllMyBuckets",
"Resource": "arn:aws:s3:::*",
"Effect": "Allow"
},
{
"Action": [
"s3:Get*",
"s3:List*"
],
"Resource": "*",
"Effect": "Allow"
},
{
"Action": "cloudwatch:*",
"Resource": "*",
"Effect": "Allow"
},
{
"Action": "kinesis:*",
"Resource": "*",
"Effect": "Allow"
}
]
}
就这样。一切准备好运行应用程序以模拟事件数据流。这些事件会立即出现在我们刚刚创建的 Redshift 视图中:

应用程序在本地运行。图像来源于作者。

示例视图显示了来自我们应用程序的事件。图片由作者提供。
结论
我们创建了一个简单而可靠的流数据管道,从使用 AWS Lambda 创建的无服务器应用程序到 AWS Redshift 数据仓库,在那里数据实时转化和摄取。它能够轻松捕获、处理和存储任何规模的数据流。对于任何机器学习(ML)管道都非常适用,其中模型用于检查数据并预测推理端点,因为数据流向其目标。
我们使用基础设施即代码来部署数据管道资源。这是部署不同数据环境中资源的首选方法。
推荐阅读
/continuous-integration-and-deployment-for-data-platforms-817bf1b6bed1?source=post_page-----04e09d7e85b2--------------------------------) [## 数据平台的持续集成和部署
数据工程师和 ML 运维的 CI/CD
[Towards Data Science /data-platform-architecture-types-f255ac6e0b7?source=post_page-----04e09d7e85b2--------------------------------) [## 数据平台架构类型
它能多大程度上满足你的业务需求?选择的困境。
[Towards Data Science [`levelup.gitconnected.com/infrastructure-as-code-for-beginners-a4e36c805316?source=post_page-----04e09d7e85b2--------------------------------) [## 初学者的基础设施即代码
使用这些模板像专业人士一样部署数据管道
从头开始构建树状 Parzen 估计器(有点像)
传统超参数调整方法的替代方案
·
关注 发布于 Towards Data Science · 9 分钟阅读 · 2023 年 4 月 4 日
--
2 维超参数调整的 TPE 可视化。图片由 Alexander Elvers via Wikipedia Commons 提供。
机器学习模型如何适应数据由一组称为超参数的初始条件决定。超参数有助于限制模型的学习行为,以便它(希望)能够很好地拟合数据并在合理的时间内完成。找到最佳的超参数集(通常称为“调优”)是建模任务中最重要且耗时的部分之一。历史上,超参数调优的方法涉及对超参数组合网格进行暴力搜索或随机搜索,分别称为网格搜索和随机搜索。尽管受欢迎,网格搜索和随机搜索方法缺乏收敛到一个合理的超参数集的方法——也就是说,它们完全是试错法。本文将探讨一种相对较新的超参数调优方法——树结构帕尔岑估计器(TPE)——并通过逐步实现的 Python 代码深入了解其功能。

图 1:网格搜索和随机搜索技术的动画。
TPE 是一种贝叶斯优化算法。这意味着它允许我们从一些关于最佳模型超参数的初始信念开始,并在学习不同超参数如何影响模型性能时,以有原则的方式更新这些信念。这已经比网格搜索和随机搜索有了显著的改进!我们可以通过尝试更多导致良好模型的超参数组合,而不是通过试错法来确定最佳超参数集。
TPE 得名于两个主要思想:1. 使用帕尔岑估计来建模我们对最佳超参数的信念(稍后会详细介绍)和 2. 使用一种称为后验推断图的树状数据结构来优化算法运行时间。在这个例子中,我们将忽略“树结构”部分,因为它与超参数调优本身无关。此外,我们不会深入讨论贝叶斯统计、期望改进等内容。这里的目标是对 TPE 及其工作原理有一个高层次的概念理解。有关这些主题的更深入讨论,请参阅 J. Bergstra 和同事关于 TPE 的原始论文 [1]。
设置示例
为了构建我们的 TPE 实现,我们需要一个玩具示例来操作。假设我们想通过一些随机生成的数据找到最佳拟合线。在这个例子中,我们有两个超参数需要调优——线的斜率 m 和截距 b。
import numpy as np
#Generate some data
np.random.seed(1)
x = np.linspace(0, 100, 1000)
m = np.random.randint(0, 100)
b = np.random.randint(-5000, 5000)
y = m*x + b + np.random.randn(1000)*700

图 2:用于 TPE 的随机生成线性数据。
由于 TPE 是一种优化算法,我们还需要一些指标来优化。我们将使用均方根误差(RMSE)。让我们定义函数 rmse 来计算这个指标,如下所示:
def rmse(m, b):
'''
Consumes coeffiecients for our linear model and returns RMSE.
m (float): line slope
m (float): line intercept
y (np.array): ground truth for model prediction
'''
preds = m*x + b
return np.sqrt(((preds - y)**2).sum()/len(preds))
我们需要的最后一件事是一些关于最佳超参数的初步信念。假设我们认为最佳拟合线的斜率在 (10,100) 区间上是一个均匀随机变量,而截距也在 (-6000, -3000) 区间上是一个均匀随机变量。这些分布称为先验分布。它们是均匀随机变量等同于说我们认为最佳超参数的真实值在各自的区间内有相同的可能性。我们将实现一个类来封装这些变量,并使用它们来定义我们的初始搜索空间。
class UniformDist:
'''
Class encapsulates behavior for a uniform distribution.
'''
def __init__(self, min_, max_):
'''
Initializes our distribution with provided bounds
'''
self.min = min_
self.max = max_
def sample(self, n_samples):
'''
Returns samples from our distribution
'''
return np.random.uniform(self.min, self.max, n_samples)
#Define hyperparameter search space
search_space = {'m':UniformDist(10,100), 'b':UniformDist(-6000,-3000)}
完成所有这些设置后,我们可以继续编写算法本身。
步骤 1:随机探索
TPE 的第一步是从我们的先验分布中随机采样超参数集合。这个过程给了我们一个关于哪些搜索空间区域能够生成良好模型的初步估计。函数 sample_priors 消耗我们的初始搜索空间和要从中抽取的随机样本数。然后它使用我们的目标函数 rmse 评估生成的模型,并返回一个包含每次试验的斜率、截距和 RMSE 的 Pandas 数据框。
import pandas as pd
def sample_priors(space, n_samples):
'''
Consumes search space defined by priors and returns
n_samples.
'''
seed = np.array([space[hp].sample(n_samples) for hp in space])
#Calculate rmse for each slope intercept pair in the seed
seed_rmse = np.array([rmse(m, b) for m, b in seed.T])
#Concatenate and convert to dataframe
data = np.stack([seed[0], seed[1], seed_rmse]).T
trials = pd.DataFrame(data, columns=['m', 'b', 'rmse'])
return trials

图 3:30 次迭代的随机采样超参数结果。每个‘x’表示一个超参数样本。
步骤 2:划分搜索空间和 Parzen 估计
在从我们的先验分布生成一些初始样本后,我们现在使用分位数阈值 γ 将超参数搜索空间分成两个部分,其中 γ 在 0 和 1 之间。我们可以任意选择 γ=0.2。那些产生模型表现位于我们目前创建的所有模型的前 20% 的超参数组合被归入“良好”分布 l(x)。所有其他超参数组合则归入“差”分布 g(x)。

图 4:经过 30 轮随机选择超参数后的 l(x) 和 g(x) 分布的 KDE。颜色较深的区域表示密度较高。
结果表明,下一组要测试的最佳超参数组合由 g(x)/l(x) 的最大值给出(如果您想查看推导过程,请参见 [1])。这在直观上是合理的。我们希望超参数在我们的“好”分布 l(x) 下很可能,而在我们的“坏”分布 g(x) 下不太可能。我们可以使用 Parzen 估计器建模 g(x) 和 l(x),这就是“TPE”中的“PE”来自哪里。Parzen 估计的粗略思路即核密度估计(或 KDE)是我们将对一系列正态分布取平均,每个分布以属于 g(x) 或 l(x) 的观察值为中心。结果分布在我们的搜索空间中样本接近的区域具有高密度,而在样本远离的区域具有低密度。为了执行 Parzen 估计,我们将使用 SKLearn 库中的 KernelDensity 对象。函数 segment_distributions 消耗我们的试验数据框和我们的阈值 γ,并返回 l(x) 和 g(x) 的 Parzen 估计器。结果分布在图 4 中可视化。
from sklearn.neighbors import KernelDensity
def segment_distributions(trials, gamma):
'''
Splits samples into l(x) and g(x) distributions based on our
quantile cutoff gamma (using rmse as criteria).
Returns a kerned density estimator (KDE) for l(x) and g(x),
respectively.
'''
cut = np.quantile(trials['rmse'], gamma)
l_x = trials[trials['rmse']<cut][['m','b']]
g_x = trials[~trials.isin(l_x)][['m','b']].dropna()
l_kde = KernelDensity(kernel='gaussian', bandwidth=5.0)
g_kde = KernelDensity(kernel='gaussian', bandwidth=5.0)
l_kde.fit(l_x)
g_kde.fit(g_x)
return l_kde, g_kde
第 3 步:确定要测试的下一个“最佳”超参数
如第 2 步所述,下一组要测试的最佳超参数集最大化 g(x)/l(x)。我们可以通过以下方式确定这一组超参数。首先,我们从 l(x) 中抽取 N 个随机样本。然后,对于每一个样本,我们评估其相对于 l(x) 和 g(x) 的对数似然,选择最大化 g(x)/l(x) 的样本作为下一组要测试的超参数组合。我们决定使用的 SKLearn KernelDensity 实现使得这一计算非常简单。
def choose_next_hps(l_kde, g_kde, n_samples):
'''
Consumes KDE's for l(x) and g(x), samples n_samples from
l(x) and evaluates each sample with respect to g(x)/l(x).
The sample which maximizes this quantity is returned as the
next set of hyperparameters to test.
'''
samples = l_kde.sample(n_samples)
l_score = l_kde.score_samples(samples)
g_score = g_kde.score_samples(samples)
hps = samples[np.argmax(g_score/l_score)]
return hps
现在汇总
我们需要做的就是将之前讨论的所有组件串联起来,得到 TPE 的实现!我们需要做出的决定是我们想要进行多少轮随机探索,算法要完成多少次迭代,以及我们的截止阈值 γ 将是多少(是的,即使是 TPE 也有超参数)。在选择这些数量时,有几个方面需要考虑。
-
如果在开始 TPE 之前,您的先验没有捕捉到“最佳”超参数集,那么算法可能会很难收敛。
-
你进行的随机探索轮次越多,对 g(x) 和 l(x) 的初步近似就会越准确,这可能会改善
tpe的结果。 -
γ 的值越高,最终落在 l(x) 中的样本就会越少。仅有少量样本用于估计 l(x) 可能会导致
tpe选择不佳的超参数。
def tpe(space, n_seed, n_total, gamma):
'''
Consumes a hyperparameter search space, number of iterations for seeding
and total number of iterations and performs Bayesian Optimization. TPE
can be sensitive to choice of quantile cutoff, which we control with gamma.
'''
#Seed priors
trials = sample_priors(space, n_seed)
for i in range(n_seed, n_total):
#Segment trials into l and g distributions
l_kde, g_kde = segment_distributions(trials, gamma)
#Determine next pair of hyperparameters to test
hps = choose_next_hps(l_kde, g_kde, 100)
#Evaluate with rmse and add to trials
result = np.concatenate([hps, [rmse(hps[0], hps[1])]])
trials = trials.append(
{col:result[i] for i, col in enumerate(trials.columns)},
ignore_index=True
)
return trials
结果
要对我们之前创建的合成数据执行 TPE,我们运行如下:
#Define hyperparameter search space
np.random.seed(1)
search_space = {'m':UniformDist(10,100), 'b':UniformDist(-6000,-3000)}
df = tpe(search_space,
n_seed=30,
n_total=200,
gamma=.2)
就这些!我们现在可以分析我们的结果。为了简洁起见,生成以下可视化所用的代码将不予展示。然而,源代码可以在此项目的 GitHub 仓库中找到。
首先,让我们将 TPE 的最佳超参数设置与回归求解器得到的实际最佳斜率和截距进行比较。

图 5:使用 TPE 和线性回归获得的超参数的最佳拟合线。
从图 5 中可以看出,使用 TPE 我们能够密切接近我们线性回归模型的最佳超参数设置。我们不期望 TPE 能超越回归求解器,因为线性回归有一个封闭形式的解。然而,在无限次的试验中,我们期望它会收敛到类似的解。
TPE 是一种优化算法,因此我们不仅关心是否能够找到一组不错的超参数。我们还需要检查在 200 次迭代中我们的目标函数是否降低。图 6 展示了在初始 30 个随机样本之后,我们的 TPE 实现有一个明确的(或大致上)趋势来最小化我们的目标函数。

图 6:所有 TPE 试验中的 RMSE。注意左侧和右侧图中 y 轴刻度的差异。红点表示具有最低 RMSE 的试验。
到目前为止一切看起来都很棒!我们最后要检查的是我们关于超参数“最佳”分布的信念在 200 次迭代中的变化。

图 7:我们 TPE 算法在 200 次迭代中的 l(x)的 Parzen 估计。
如图 7 所示,我们从一个非常宽泛的 l(x)分布开始,迅速收敛到接近最终结果的分布。图 6 和图 7 清楚地说明了 TPE 的三个简单步骤如何组合成一个能够以复杂但直观的方式探索搜索空间的算法。
结论
超参数调优是建模过程中的关键部分。尽管网格搜索和随机搜索方法易于实现,但 TPE 作为替代方案提供了一个更有原则的超参数调优方法,从概念上也比较简单。许多 Python 库都很好的实现了 TPE,包括Hyperopt(由[1]的作者创建并维护)和Optuna。无论是像我们的玩具示例一样简单,还是像神经网络超参数调优一样复杂,TPE 都是一个多功能、有效且简单的技术,近年来在数据科学和机器学习领域越来越受欢迎。下次当你在调整模型超参数时,或许可以跳过网格搜索。
参考文献
- Bergstra, J., Bardenet, R., Bengio, Y., & Kégl, B., 超参数优化算法 (2011),神经信息处理系统进展,24。
除非另有说明,否则所有图像均由作者提供。
用整体思维模型构建 AI 产品
一个由跨学科团队进行 AI 产品构思、规划和定义的工具
·
关注 发表在 数据科学前沿 · 23 分钟阅读 · 2023 年 9 月 3 日
--
注:本文是名为“剖析 AI 应用”的系列文章中的第一篇,介绍了 AI 系统的心理模型。该模型作为跨学科 AI 和产品团队讨论、规划和定义 AI 产品的工具,以及与业务部门对齐的工具,旨在汇集产品经理、用户体验设计师、数据科学家、工程师和其他团队成员的观点。在本文中,我介绍了心理模型。后续文章将展示如何将其应用于特定的 AI 产品和功能:
-
通过对话访问数据创建信息优势
-
通过大型语言模型重新定义对话 AI
我将在未来的文章中分析一系列其他 AI 应用,并且也在准备我的书 创建智能产品 ,该书专为产品构建者介绍 AI。关注我,保持最新动态!
公司常常认为,将 AI 纳入其产品中只需聘请 AI 专家,让他们施展技术魔法即可。这种方法直接导致了集成谬误:即便这些专家和工程师生产出卓越的模型和算法,它们的输出往往停留在游乐场、沙箱和演示阶段,永远无法真正成为产品的完整组成部分。多年来,我见证了许多数据科学家和工程师的挫败感,他们的技术出色的 AI 实现并未成为用户面对的产品。相反,它们被赋予了前沿实验的光荣地位,使内部利益相关者产生了骑乘 AI 浪潮的印象。现在,自从 2022 年 ChatGPT 发布以来,AI 的普及无处不在,公司再也无法将 AI 作为展示其技术水平的“灯塔”功能。
为什么集成 AI 如此困难?原因有几个:
-
团队常常专注于 AI 系统的单一方面。这甚至导致了数据中心、模型中心和人机中心等不同阵营的出现。虽然每个阵营都为研究提供了激动人心的视角,但实际产品需要将数据、模型和人机交互结合成一个连贯的系统。
-
AI 开发是一个高度协作的企业。在传统的软件开发中,你面对的是后端和前端组件的相对清晰的二分法。在 AI 中,你不仅需要为你的团队增加更多的角色和技能,还需要确保不同方之间的紧密合作。你的 AI 系统的不同组件将以紧密的方式相互作用。例如,如果你正在开发一个虚拟助手,你的用户体验设计师必须了解提示工程以创建自然的用户流程。你的数据标注员需要了解你的品牌和虚拟助手的“性格特征”,以创建一致且符合你定位的训练数据,而你的产品经理需要掌握并审视数据管道的架构,以确保它符合用户的治理关注。
-
在构建 AI 时,公司经常低估设计的重要性。虽然 AI 从后端开始,但良好的设计对于在生产中展现其价值是不可或缺的。AI 设计突破了传统 UX 的界限。你提供的大部分功能在界面上并不直接可见,而是“隐藏”在模型中,你需要教育和引导用户以最大化这些好处。此外,现代基础模型可能会产生有毒、错误和有害的输出,因此你需要设置额外的保护措施来减少这些风险。所有这些可能需要你团队掌握新的技能,如提示工程和对话设计。有时,这也意味着做一些违反直觉的事情,比如低估价值以管理用户期望,以及增加摩擦以赋予用户更多控制和透明度。
-
AI 热潮带来了压力。许多公司在没有经过客户和市场需求验证的情况下急于实施。偶尔提及 AI 热词可以帮助你在市场上定位自己为一个进步和创新的企业,但从长远来看,你需要通过真正的机会来支撑你的热词和实验。这可以通过业务和技术之间的紧密协调来实现,基于市场机会和技术潜力的明确映射。
在这篇文章中,我们将构建一个集成了这些不同方面的 AI 系统的思维模型(参见图 1)。它鼓励构建者从整体上进行思考,清晰理解他们的目标产品,并在过程中根据新的见解和输入进行更新。该模型可以作为一种工具来简化协作,协调 AI 团队内外的多样化视角,并基于共同愿景构建成功的产品。它不仅适用于新的 AI 驱动的产品,也适用于整合到现有产品中的 AI 功能。

图 1:AI 系统的思维模型
以下部分将简要描述每个组件,重点关注那些特定于人工智能产品的部分。我们将从商业视角开始——市场机会和价值——然后深入探讨用户体验和技术。为了说明这一模型,我们将使用生成营销内容的副驾驶作为例子。
1. 机会
随着你现在可以用人工智能做的所有酷炫事情,你可能迫不及待地想动手开始构建。然而,为了构建出用户需要和喜爱的东西,你应该以市场机会为基础来支持你的开发。在理想的情况下,机会来自于客户告知我们他们需要或想要什么。[1] 这些可以是未满足的需求、痛点或愿望。你可以在现有的客户反馈中寻找这些信息,例如产品评论和销售及成功团队的笔记。此外,不要忘记你自己也可能是你产品的潜在用户——如果你正在解决一个你自己经历过的问题,这种信息优势就是额外的优势。除此之外,你还可以使用调查和访谈等工具进行主动的客户研究。
比如,我不需要看得太远就能看到创业公司以及大型公司在内容营销方面的痛苦。我自己也经历过——随着竞争的加剧,通过个体、定期且(!)高质量的内容来建立思想领导力变得越来越重要,以便于区分。而且,对于一个小而忙碌的团队来说,总会有比写一周博客帖子更重要的事情。同时,我也经常遇到我网络中的人,他们在建立一致的内容营销常规方面苦苦挣扎。这些“本地”的、可能有偏见的观察可以通过超出个人网络的调查来验证,从而确认解决方案的更广泛市场。
现实世界略显模糊,客户并不会总是主动向你提出新的、精心构思的机会。相反,如果你张开你的天线,机会将从多个方向向你靠近,例如:
-
市场定位:人工智能很时髦——对于已经建立的企业,它可以用来增强企业作为创新、高科技、未来-proof 等的形象。例如,它可以将现有的营销公司提升为一个人工智能驱动的服务,并使其与竞争对手区分开来。然而,不要为了人工智能而做人工智能。市场定位的技巧需要谨慎应用,并与其他机会结合使用——否则,你有失去信誉的风险。
-
竞争对手:当你的竞争对手采取行动时,他们可能已经完成了基础研究和验证。过一段时间看看他们——他们的发展是否成功?利用这些信息来优化你自己的解决方案,采纳成功的部分,纠正错误。例如,假设你观察到一个竞争对手提供完全自动化生成营销内容的服务。用户点击一个“大红按钮”,AI 就会继续撰写和发布内容。经过一些研究,你了解到用户犹豫使用这个产品,因为他们希望对过程保持更多的控制,并将自己的专业知识和个性融入写作中。毕竟,写作也涉及自我表达和个体创造力。这是你前进的时机,推出一个功能丰富、可配置的多功能工具,让用户能够在需要时将自己融入到内容创建过程中。
-
法规:技术颠覆和全球化等宏观趋势迫使监管者收紧要求。法规带来压力,并且是防弹的机会来源。例如,假设有一项法规要求每个人都必须明确标识 AI 生成的内容。那些已经使用 AI 内容生成工具的公司将会消失于内部讨论中,考虑是否接受这一要求。许多公司会因为想要保持真实的思想领导形象而选择回避,而不是生产明显的 AI 生成的模板。假设你聪明地选择了一个增强型解决方案,让用户有足够的控制权,从而能够保持文本的正式“作者”身份。当新的限制被引入时,你是免疫的,可以迅速前进以利用这一法规,而那些使用完全自动化解决方案的竞争对手则需要时间来恢复。
-
使能技术:新兴技术和现有技术的重大突破,例如 2022–23 年的生成式 AI 浪潮,可以开辟新的做事方式,或将现有应用提升到一个新水平。假设你过去十年一直经营一家传统的营销公司。现在,你可以开始将 AI 技巧和解决方案引入你的业务,提高员工的效率,利用现有资源服务更多客户,并增加利润。你是在现有专业知识、声誉和(希望是良好意图的)客户基础的基础上进行的,因此引入 AI 增强功能可能比新进入者要顺利和风险更小。
最后,在现代产品世界中,机会往往不那么明确和正式,可以通过实验直接验证,从而加快你的开发。因此,在以产品驱动的增长中,团队成员可以提出自己的假设,而不必严格依赖数据驱动的论证。这些假设可以以逐步的方式制定,例如修改提示或更改一些 UX 元素的本地布局,这使得它们易于实施、部署和测试。通过消除对每个新建议提供先验数据的压力,这种方法利用了所有团队成员的直觉和想象力,同时强化了对建议的直接验证。假设你的内容生成运行顺利,但你听到越来越多关于 AI 透明性和可解释性不足的投诉。你决定实施额外的透明度级别,并向用户展示用于生成内容的具体文档。你的团队对一组用户进行了功能测试,发现他们很高兴使用该功能来追溯原始信息来源。因此,你决定将其建立在核心产品中,以提高使用率和满意度。
2. 价值
要理解和传达你 AI 产品或功能的价值,你首先需要将其映射到一个用例——它将解决的具体业务问题——并找出 ROI(投资回报率)。这迫使你将思维从技术层面转移到解决方案的用户侧利益上。ROI 可以沿着不同的维度进行衡量。对于 AI,一些维度包括:
-
提高效率:AI 可以成为个人、团队和整个公司生产力的助推器。例如,对于内容生成,你可能会发现,现在你可以在 1-2 小时内完成原本需要 4-5 小时的博客文章[2],并将节省下来的时间用于其他任务。效率的提升通常伴随着成本的节约,因为完成相同工作所需的人力减少。因此,在商业环境中,这种好处对用户和领导层都很有吸引力。
-
更个性化的体验:例如,你的内容生成工具可以让用户设置他们公司的一些参数,如品牌属性、术语、产品优势等。此外,它还可以跟踪特定作者的编辑内容,并随着时间的推移,将其生成适应该用户独特的写作风格。
-
趣味和愉悦:在这里,我们探讨了产品使用的情感层面,也就是 Don Norman 所称的“本能”层面。像游戏和增强现实这样的产品在 B2C 领域存在着整个类别。那么 B2B 呢——你不会认为 B2B 产品存在于一个无菌的专业真空中?实际上,这个类别可以产生比 B2C 更强烈的情感反应。例如,写作可以被视为一种令人满意的自我表达行为,或者作为与写作障碍和其他问题的内在斗争。考虑一下你的产品如何增强任务的积极情感,同时缓解甚至转化其痛苦的方面。
-
便利性:你的用户需要做什么才能利用 AI 的魔力?设想将你的内容生成助手集成到流行的协作工具中,如 MS Office、Google Docs 和 Notion。用户将能够在不离开其数字“家”的舒适环境下访问你产品的智能和效率。因此,你最小化了用户体验产品价值和持续使用的努力,从而提升了用户获取和采纳率。
一些 AI 的好处——例如效率——可以直接量化 ROI。对于像便利性和愉悦感这样不那么具体的收益,你需要考虑代理指标,如用户满意度。记住,从终端用户价值的角度思考,不仅能缩小用户与产品之间的差距。作为一种意外的副作用,它可以减少你公共沟通中的技术细节。这将防止你无意中吸引不必要的竞争。
最后,你应该尽早考虑的一个基本价值方面是可持续性。你的解决方案如何影响社会和环境?在我们的例子中,自动化或增强的内容生成可能会取代和消除大规模的人类工作负荷。你可能不希望被称为某个整个职业类别的“杀手”——毕竟,这不仅会引发伦理问题,还会招致威胁你工作的用户的抵制。考虑一下你如何解决这些担忧。例如,你可以教育用户如何有效利用他们的新空闲时间来设计更复杂的营销策略。这些可以为你提供一个可防御的护城河,即使其他竞争者赶上了自动化内容生成。
3. 数据
对于任何类型的 AI 和机器学习,你需要收集和准备数据,以使其反映现实生活中的输入,并为你的模型提供足够的学习信号。如今,我们看到一种数据中心 AI 的趋势——这种 AI 方法远离对模型的无休止调试和优化,专注于修复输入到这些模型中的数据中的众多问题。当你开始时,有不同的方法可以获得一个合适的数据集:
-
你可以使用现有的数据集。这可以是标准的机器学习数据集,也可以是具有不同初始目的的数据集,经过适配以适合你的任务。有一些数据集经典,例如用于情感分析的IMDB 电影评论数据集和用于手写字符识别的MNIST 数据集。还有更多异国情调和令人兴奋的替代方案,如Catching Illegal Fishing和Dog Breed Identification,以及 Kaggle 等数据中心的无数用户策划的数据集。找到一个专门为你的特定任务而制作且完全满足你需求的数据集的机会相当小,在大多数情况下,你还需要使用其他方法来丰富你的数据。
-
你可以手动注释或创建数据以产生正确的学习信号。手动数据注释——例如,对文本进行情感评分——曾是早期机器学习中的常用方法。最近,它作为 ChatGPT 秘密武器的主要成分重新引起了关注。大量的人工努力被投入到创建和排序模型的响应,以反映人类的偏好。这种技术也被称为从人类反馈中进行强化学习(RLHF)。如果你拥有必要的资源,可以用它们来创建高质量的数据,以应对更具体的任务,比如生成营销内容。注释可以在内部完成,也可以使用外部供应商或诸如 Amazon Mechanical Turk 这样的众包服务。无论如何,大多数公司都不愿意花费大量资源进行 RLHF 数据的手动创建,并且会考虑一些自动化数据创建的技巧。
-
因此,你可以使用数据增强来向现有的数据集添加更多示例。对于情感分析等简单任务,你可以在文本中引入一些额外的噪声,改变几个词汇等。对于更开放的生成任务,目前对使用大型模型(例如基础模型)进行自动化训练数据生成充满热情。一旦你确定了增强数据的最佳方法,就可以轻松地将其扩展到所需的数据集大小。
在创建数据时,你面临质量和数量之间的权衡。你可以手动标注较少的数据,保证其高质量,或者将预算花在开发自动数据增强的黑客技术和窍门上,这会引入额外的噪声。如果你选择手动标注,可以在内部进行,并形成注重细节和质量的文化,或者将工作众包给匿名人员。众包通常质量较低,因此你可能需要标注更多数据以补偿噪声。如何找到理想的平衡?这里没有现成的解决方案——最终,你将通过不断的训练和数据增强来找到理想的数据组成。一般来说,在预训练模型时,它需要从头开始获取知识,这只能通过大量数据来实现。另一方面,如果你想对一个已经存在的大模型进行微调并进行最后的专业化处理,你可能会更重视质量而非数量。在这种情况下,使用详细指南对小数据集进行受控手动标注可能是最佳解决方案。
最终,你可以获得的最有价值的数据来自生产。当你的应用上线时,你应该设置数据收集机制,以收集用户输入、AI 输出,以及(如果可能的话)额外的学习信号,如用户评估。这将创造数据网络效应,其中单个用户从你模型的大规模集体学习中受益。具体来说,重用生产数据进行微调将使你的模型尽可能接近用户期望的“真实情况”。这会导致更高的用户满意度、更频繁的使用和参与,从而获得更多高质量的数据——这是一个被称为“数据飞轮”的良性循环。

图 2:数据飞轮
4. 智能
数据是你的模型将学习的原材料,希望你能编制一个有代表性、高质量的数据集。现在,你的 AI 系统的实际智能——其从现有数据中学习并推广到新数据的能力——存在于机器学习算法和模型中,以及这些算法和模型可以调用的任何附加工具和插件中。在核心 AI 模型方面,你可以使用三种主要选项:
-
提示现有模型。 GPT 系列的高级大语言模型(如 ChatGPT 和 GPT-4),以及来自 Anthropic 和 AI21 Labs 等其他提供商的模型,都可以通过 API 进行推理。通过提示,你可以直接与这些模型对话,在提示中包含完成任务所需的所有领域和任务特定的信息。这可以包括要使用的具体内容、类似任务的示例(少量示例提示)以及模型需要遵循的指令。例如,如果你的用户想生成一篇关于新产品特性的博客文章,你可以要求他们提供一些关于该特性的核心信息,如其优点和用例、使用方法、发布日期等。然后,你的产品将这些信息填入精心设计的提示模板中,并要求 LLM 生成文本。提示非常适合快速启动预训练模型。然而,随着时间的推移,通过提示构建的护城河会迅速变薄——在中期,你需要一个更具防御性的模型策略来维持你的竞争优势。
-
微调预训练模型。 这种方法使 AI 在过去几年中变得非常受欢迎。随着越来越多的预训练模型的出现,以及 Huggingface 等门户提供模型仓库和标准代码来处理这些模型,微调正在成为尝试和实施的首选方法。当你使用预训练模型时,你可以从别人已经在模型的数据、训练和评估上所做的投资中受益,这些模型已经“了解”了很多关于语言和世界的知识。你只需使用任务特定的数据集对模型进行微调,这个数据集可以远小于最初用于预训练的数据集。例如,对于营销内容生成,你可以收集一组在参与度方面表现良好的博客文章,并对这些文章的指令进行逆向工程。从这些数据中,你的模型将学习成功文章的结构、流程和风格。微调是在使用开源模型时的最佳选择,但像 OpenAI 和 Cohere 这样的 LLM API 提供商也越来越多地提供微调功能。特别是在开源轨道上,你仍需考虑模型选择、训练和部署较大模型的成本开销,以及模型的维护和更新计划。
-
从零开始训练你的机器学习模型。 一般来说,这种方法适用于较简单但高度特定的问题,对于这些问题,你有特定的知识或合适的数据集。内容生成不完全符合这一类别——它需要高级语言能力来起步,这些能力只能在经过大量数据的训练后获得。较简单的问题,如特定类型文本的情感分析,通常可以用像逻辑回归这样的既有机器学习方法来解决,这些方法的计算成本低于复杂的深度学习方法。当然,也存在诸如特定领域的概念提取这样的适度复杂问题,你可能会考虑从头训练一个深度神经网络。
除了训练之外,评估对于成功使用机器学习至关重要。合适的评估指标和方法不仅对 AI 功能的自信发布很重要,而且将作为进一步优化的明确目标,并作为内部讨论和决策的共同基础。虽然技术指标如精确度、召回率和准确率可以提供一个良好的起点,但最终,你会想寻找反映 AI 为用户提供的实际价值的指标。
5. 用户体验
AI 产品的用户体验是一个引人入胜的主题——毕竟,用户对与 AI“合作”寄予了很高的期望,但也有对 AI 超越并可能超越他们智能的担忧。这种人机合作的设计需要一个深思熟虑且明智的发现和设计过程。一个关键的考虑因素是你希望你的产品提供多少自动化——请注意,完全自动化远不是理想解决方案。以下图示展示了自动化的连续体:

图 3:AI 系统的自动化连续体
让我们来看一下这些层级:
-
在第一个阶段,人类做所有的工作,且没有自动化。尽管对 AI 的炒作很多,但现代公司中大多数知识密集型任务仍在这一层面进行,这为自动化提供了巨大的机会。例如,那个抵制 AI 驱动工具并坚信写作是一项高度手动且特有的工艺的内容撰写者就在这里工作。
-
在辅助人工智能的第二阶段,用户对任务执行拥有完全的控制权,并且大部分工作是手动完成的,但 AI 工具帮助他们节省时间并弥补不足之处。例如,当在紧迫的截止日期内撰写博客文章时,使用 Grammarly 或类似工具进行最终语言检查可以成为一个受欢迎的节省时间的方法。它可以消除手动修订,这需要大量宝贵的时间和注意力,且可能仍然存在错误和遗漏——毕竟,人非圣贤。
-
增强智能是指人工智能作为一个合作伙伴,增强人类的智能,从而利用两个世界的优势。与辅助人工智能相比,机器在您的过程中可以说更多,涵盖更多的责任,例如构思、生成和编辑草稿,以及最终的语言检查。用户仍需参与工作,做出决策,并完成部分任务。用户界面应清楚地指示人类和人工智能之间的劳动分配,突出错误潜在性,并提供对其执行步骤的透明度。简而言之,“增强”体验通过迭代和精炼引导用户达到期望的结果。
-
最后,我们有完全自动化——这是对人工智能爱好者、哲学家和专家来说的一个有趣想法,但通常不是现实产品的最佳选择。完全自动化意味着您提供一个“红色大按钮”,启动过程并将完全控制交给机器。一旦人工智能完成,用户将面对最终输出,要么接受,要么放弃。中间发生的任何事情他们无法影响。完全自动化是设计方法的重要元素,如环境智能和冷静技术,应用于智能家电、语音助手等。然而,目前,LLM 和其他基础模型远未能捕捉和处理它们需要的丰富上下文信息,以实现无缝和可靠的自动化操作。可以想象,完全自动化的用户体验选项相当有限,因为几乎没有互动。成功的主要责任落在您的技术同事肩上,他们需要确保输出质量异常高。
AI 产品在设计时需要特殊对待。标准的图形界面是确定性的,允许你预见用户可能采取的所有路径。相比之下,大型 AI 模型是概率性的和不确定的——它们展示了一系列惊人的能力,但也存在诸如有毒、错误和有害输出的风险。从外部来看,你的 AI 界面可能看起来很简单,因为产品的大部分能力存在于模型中,并且对用户不可直接见到。例如,一个 LLM 可以解释提示、生成文本、搜索信息、总结内容、采用特定的风格和术语、执行指令等。即使你的 UI 是一个简单的聊天或提示界面,也不要让这种潜力被忽视——为了引导用户成功,你需要明确和现实。让用户了解你的 AI 模型的能力和局限性,允许他们轻松发现和修正 AI 犯的错误,并教会他们迭代自己以获得最佳输出的方法。通过强调信任、透明度和用户教育,你可以让你的用户与 AI 进行合作。虽然深入探讨 AI 设计这一新兴学科超出了本文的范围,但我强烈建议你从其他 AI 公司以及设计的其他领域,如人机交互,寻找灵感。你将很快识别出一系列重复的设计模式,如自动补全、提示建议和 AI 通知,这些都可以整合到你的界面中,以充分利用你的数据和模型。
此外,为了提供一个真正出色的设计,你可能需要为你的团队添加新的设计技能。例如,如果你正在构建一个用于优化营销内容的聊天应用程序,你将需要与负责对话流程和聊天机器人“个性”的对话设计师合作。如果你正在开发一个需要彻底教育和引导用户通过各种选项的增强型产品,一个内容设计师可以帮助你建立合适的信息架构,并为你的用户添加适当的引导和提示。
最后,对意外保持开放态度。AI 设计可能会让你重新思考关于用户体验的原始观念。例如,许多 UX 设计师和产品经理被训练去最小化延迟和摩擦,以平滑用户体验。然而,在 AI 产品中,你可以暂停这种斗争,利用这两者的优势。延迟和等待时间对于教育用户非常有用,例如通过解释 AI 当前正在做什么以及指示他们可能的下一步。中断,例如对话和通知弹窗,可以引入摩擦,以加强人机合作关系,并增加用户的透明度和控制感。
6. 非功能需求
除了数据、算法和用户体验(UX)之外,还需要考虑所谓的非功能需求(NFRs),如准确性、延迟、可扩展性、可靠性和数据治理,这些都确保用户确实能够获得预期的价值。NFRs 的概念源自软件开发,但在人工智能领域尚未得到系统性考虑。这些需求通常是在用户研究、构思、开发和人工智能能力运营过程中,随时出现时才会被采纳。
你应该尽早理解和定义你的非功能需求,因为不同的非功能需求将在你旅程的不同阶段显现。例如,隐私需要从数据选择的初始步骤开始考虑。准确性在生产阶段最为敏感,当用户开始在线使用你的系统时,可能会用意外的输入使系统不堪重负。可扩展性是一个战略考虑因素,当你的业务扩大用户和/或请求数量或提供的功能范围时,这一考虑因素会变得重要。
在非功能需求方面,你不可能全部满足。以下是一些你需要平衡的典型权衡:
-
提高准确性的首要方法之一是使用更大的模型,这会影响延迟。
-
使用“原样”的生产数据进行进一步优化可能对学习最有利,但可能会违反你的隐私和匿名规则。
-
更具可扩展性的模型是通用型的,这会影响它们在公司或用户特定任务上的准确性。
如何优先考虑不同的需求将取决于可用的计算资源、你的用户体验概念(包括自动化的程度)以及由人工智能支持的决策的影响。
关键要点
-
从结果倒推:不要假设技术本身就能完成任务;你需要一个明确的路线图来将人工智能集成到面向用户的产品中,并教育用户了解其优点、风险和局限性。
-
市场对齐:优先考虑市场机会和客户需求来指导人工智能的发展。不要因为炒作而急于实施人工智能,尤其是在没有市场验证的情况下。
-
用户价值:定义、量化并传达人工智能产品在效率、个性化、便利性及其他价值维度方面的价值。
-
数据质量:专注于数据质量和相关性,以有效地训练人工智能模型。尝试使用小规模、高质量的数据进行微调,而使用较大的数据集进行从头训练。
-
启动数据飞轮:你最有价值的数据来自生产。确保你有机制记录这些数据,并通过用户反馈等额外学习信号来丰富数据。
-
智能性:为你的用例选择合适的复杂性和防御性(提示、微调、从零开始训练),并仔细评估其性能。随着时间的推移,你将获得更多的专业知识和信心,并可以考虑切换到更高级的模型策略。
-
以用户为中心的设计:在设计 AI 产品时要考虑用户需求和情感,平衡自动化与用户控制。注意概率 AI 模型的“不可预测性”,并引导用户与之协作并从中受益。
-
人类与 AI 伙伴关系的深思熟虑设计:通过强调信任、透明度和用户教育,你可以让用户与 AI 进行协作。
-
非功能性需求:在开发过程中考虑准确性、延迟、可扩展性和可靠性等因素,并尽早评估这些因素之间的权衡。
-
协作:促进 AI 专家、设计师、产品经理和其他团队成员之间的密切合作,以利用跨学科的智慧,成功整合你的 AI。
参考文献
[1] Teresa Torres (2021). 《持续发现习惯:发现创造客户价值和商业价值的产品》。
[2] Orbit Media (2022). 《新的博客统计数据:2022 年什么内容策略有效?我们采访了 1016 位博客作者》。
[3] Don Norman (2013). 《日常物品的设计》。
[4] Google, Gartner and Motista (2013). 《从推广到情感:将 B2B 客户与品牌连接起来》。
注:所有图片均由作者提供。
为企业构建人工智能战略
原文:
towardsdatascience.com/building-ai-strategies-for-businesses-7b2e900399b7
通过沃德利地图制定人工智能战略的艺术
·发表于Towards Data Science ·阅读时间 13 分钟·2023 年 6 月 6 日
--

图片由作者提供:通过 Midjourney 生成
背景——在一家成熟银行主导人工智能战略
在创办 Data-Centric Solutions 之前,我负责了一家成熟银行的人工智能战略的开发、执行和实施。尽管在这种规模的交付上相对较为青涩,但我还是充满热情地接受了这份工作。消除了最初的冒名顶替综合症,我安慰自己,即使最坏的情况也不过是犯几个错误、吸取教训并实现专业成长。
人工智能战略显然比我最初认为的要复杂得多。在我的职业生涯中,我有幸参与了成功和不成功的战略。我希望分享一些我获得的宝贵见解,并希望这能为他人提供一个基础。
灵感来源于沃德利地图
我最初是通过一位值得信赖的导师推荐发现了沃德利地图的。对于那些不熟悉的人,沃德利地图是一种系统映射的方法。顾名思义,这种方法是由西蒙·沃德利开发的。
起初,我有些怀疑,但随着深入了解,我越来越认识到它的价值。我发现这些不仅是阐述战略的绝佳方式,而且是不可或缺的工具。
让我信服的是沃德利的棋类比喻。作为一个狂热的国际象棋爱好者,我在能抽出时间时都会下棋,这个比喻让我深感触动。国际象棋虽然比商业简单得多,但仍是一种战略游戏。沃德利强调,能够在国际象棋中制定和实施战略是因为存在一张地图,即棋盘本身!
如果你愿意,你可以阅读 Wardley 的 国际象棋类比,像我一样,你可能会被说服去追求这种方法!
什么构成了一个图?
根据 Wardley,以下六个组件对于任何图来说都是有用的:
-
图是可视化的,这在棋盘上无可否认。
-
图是上下文的。我们承认棋盘与国际象棋游戏有关。
-
图使位置的理解成为可能。我们安排棋盘,以便理解棋子的摆放。
-
图参考一个锚点。锚点是板上固有的,根据板在玩家和棋子之间的位置为我们提供方向感。
-
图允许运动的理解。棋盘的设计模式让我们理解运动。
-
图包含组件。在棋类比中,棋子就是组件。
理解 Wardley 图
在我们深入探讨我创建的策略图之前,我想让读者对 Wardley 图有一个基本的直觉。
简要定义
Wardley 图应被解读为对业务价值链或服务的战略可视化,突出其组件的成熟度和演变。这有助于理解依赖关系,识别机会,并做出关于在哪里集中创新、资源和开发工作的明智决策。

图 1:作者提供的图像 — 空白 Wardley 图,客户是锚点,因此位于图的顶部
让我们详细说明 Wardley 图的属性:
锚点:在我们所有的图中,锚点是客户。图上所有组件的位置都是相对于客户的。
组件:组件代表为满足客户需求而进行的技术、实践或活动。关联的组件以某种方式相互依赖,主要通过使能。资本、数据和风险可以在关联组件之间流动。
在 AI 策略的目的下,我定义了以下组件:
-
智能应用:提供给客户的 AI 驱动应用。我保持了这一点的通用性,但读者可以想象任何类型的 AI 驱动应用。这是价值链中最可见的部分,因此在图中最接近客户。
-
机器学习操作(MLOps):使机器学习/AI 模型在生产中得到管理的实践、人员和技术。要了解更详细的解释,阅读此文。
-
研究与开发(R&D):发现和原型新 AI/ML 模型的实践。这些活动通常在实验室或“沙坑”环境中进行。
-
数据管理:数据管理涉及以高效、安全的方式组织、存储、检索和维护数据,以确保用户能够访问、可靠和及时获取数据。
-
基础设施:指所有其他组件所建立的技术 IT 基础设施。
可见性:组件的位置由 y 轴决定,代表沿价值链的可见性。组件距离客户越近,它的可见性就越高。
进化:运动受限于 x 轴,代表地图上给定组件的进化阶段(成熟度)。组件会因外部因素而随着时间发展。
任何组件都有四个进化阶段:
-
创生:创建新颖、独特的组件或技术。
-
定制构建: 组件被更好地理解,但仍需针对每个使用场景进行定制。
-
产品:组件变得标准化,转变为广泛分发的产品或服务。
-
实用性:组件高度商品化,作为以效率和成本降低为重点的公用服务提供。
现在我们对地图有了基本了解,让我们一起探索 AI 的格局吧!
注意:这是对 Wardley 映射的简化理解,但足以理解我为本文创建的地图。如果你想了解更多,我建议你阅读完整的博客。
阶段 0 的 AI:恐龙时代
我被招聘到一个阶段 0 的公司来构建 AI 战略。我们拥有大量的遗留基础设施,有限的现代技术栈访问权限,以及很少的内部专家来构建智能应用。我怀疑许多读者可以与阶段 0 相关联。

图 2 — 作者提供的图像:阶段 0 的 AI
请花一些时间理解这个阶段 0 公司的地图。
基础设施:有大量的遗留本地技术基础设施。这些是几十年前由第三方构建的产品,大多数现在已不再得到支持,只有少数几个非常昂贵的专家可以维护。没有人真正理解它们,人们也不愿意干预这些系统,以免引发重大问题。它们几乎被 IT 部门保护,以防止“崩溃”。这些遗留基础设施是可靠的、静态的,显然不利于创新。
数据管理:数据管理系统建立在遗留基础设施之上。几代分析师和工程师构建了定制的数据管道、表视图、仪表盘和报告层,且大多未有文档记录。我们的本地数据仓库被戏称为“数据沼泽”。数据几乎像是无序的西部,一旦有人更改了管道而未通知任何人,你建立的仪表盘可能会随机失败。沼泽中的数据是不可信的。
我偶尔听到有人把自由格式文本字段或 CSV 文件称为杂乱数据。相比之下,我在这里描述的杂乱程度,类似于石油泄漏;自由格式文本输入就像洒出的牛奶。
Phase 0 AI 战略
我们意识到研发、MLOps 和智能应用不能超越数据管理的演变。为了启动 AI 战略,我们首先需要解决这个问题。
作为一个数据科学职能,我们需要对原型所需的数据有更多的控制。我们利用了一款现成的分析工具,允许我们连接到源源不断的数据来源。有限的存储和计算意味着我们需要利用统计方法来适当地抽样数据源。使用开源分析工具进行了数据质量检查。有效地说,我们为研发创建了一个定制的数据管理解决方案。
研发开始时是发现阶段,我们花时间与客户交谈,试图了解智能应用如何能给他们带来好处。我们制定了框架来评估可行性与价值,以便为我们提供一种优先考虑开发计划的方法。
凭借我们临时的数据管理方法和数据实验室,我们设法开发了一些原型应用。然而,从 AI 战略的角度来看,我们遇到了瓶颈。我们已经用尽了所有的途径,下一步的逻辑是从 Phase 0 发展。
基础设施与数据管理的演变
我希望你暂时设想一下你是一个 Phase 0 业务的首席技术官 (CTO)。外部技术的发展会如何影响你的业务?

图 3 — 作者图像 — 红线表示过渡到未来状态,蓝线表示资本流动,黑色矩形表示对变化的阻力(惯性)
点 1 — 第一个明显的点涉及技术基础设施,它已经被云服务提供商商品化。计算和存储可以作为一种公用事业获得。
点 2 — 因此,数据管理的工具和方法也在不断演变。撰写时,存在流行的云原生数据湖,可以满足广泛的数据管理需求。尽管这还未完全商品化。
考虑到这一点,一个精明的 CTO 可以将他们的 Phase 0 业务的未来状态表示如图 3 所示。
作为 CTO,关注竞争对手,你应该能够预见到更小、更灵活的公司可能已经是云原生的。这将使它们能够投资于研发活动和 MLOps,因为它们的数据管理和基础设施的演变使这成为可能,并且可能具有盈利潜力。该地图将使你能够预测竞争对手如何通过比你更快地开发智能应用来抢占你的市场份额。你可以论证投资于基础设施和数据管理的重要性。
你可能还会预见到可能会有一些对演变的抵制,无论是由于内部摩擦还是之前对自己定制解决方案的成功。在我的阶段 0 经历中,数据沼泽确实受到了一些人的喜爱。有政治力量会拖慢迁移到更高效的数据管理方法的进程。
图 3 中展示的地图帮助你看到,为了推进你的 AI 战略,你应该优先考虑技术基础设施和数据管理的演变。这正是我在阶段 0 银行经历的情况,该战略从实验性 AI 转向了技术基础设施和数据管理方法的成熟。
在阶段 1 业务中的 AI:技术精通、混乱和热情
我有幸参与了阶段 1 银行的战略。我觉得这是大多数刚刚从博士或硕士课程中毕业的数据科学家设想自己能到达的地方。这家银行已经将其技术基础设施外包给了云服务提供商。他们还利用了“产品化”的基于云的数据管理解决方案。
没有公司是完美的,但老实说,这与阶段 0 的银行相差甚远,对我个人来说非常有趣。公司里有数百名数据科学家在探索机器学习和人工智能的新领域。我已经记不清在创新会议上看到的令人惊叹的场景了。然而,前方也有一些大问题。

图 4——作者提供的图像——阶段 1 公司快照
AI,只是昂贵的科学实验?
我记得曾经与创新团队的高级成员一起开会。我们每个人都在挠头,试图弄清楚如何将这些精彩的模型投入生产。这些原型都很出色,但说实话,它们对客户没有任何价值。我们不如说是在做昂贵的科学实验。此时,我希望你能想象自己是 CTO 或创新负责人。你会如何解决生产问题?
Wardley 地图:AI 的演变——阶段 2
在撰写本文时,云服务提供商、扩展公司和初创公司一直在解决将人工智能投入生产的问题。一些提供商承诺处理部署机器学习模型所涉及的所有复杂性,而另一些提供商则承诺处理从部署到监控再到重新训练的整个模型生命周期。

图 5 — 作者提供的图像 — AI 阶段 2 的进化,MLOps 被产品化并作为服务由第三方提供。
考虑到这一点,阶段 1 银行的 CTO 理解了进化的必要性。CTO 选择与云服务提供商合作,构建产品化的 MLOps 服务。这将真正帮助他们在竞争中脱颖而出,因为没有多少建立的银行在大规模交付智能应用。但这不仅仅是构建新技术那么简单。
从战略角度来看,经营阶段 2 的业务需要以完全不同于阶段 1 的方式组织团队。整个业务所需的人才类型也大相径庭。让我们稍微绕一点路来理解这一点。
简要绕行至团队态度
Wardley 绘图给我们提供了态度的概念。态度帮助我们概念化团队在不同进化阶段的工作方式。这里是三种态度的简要概述:
-
先驱者:先驱者是富有创意的个体,他们探索未知领域,通过核心研究使未来的成功成为可能,尽管面临频繁的失败。
-
定居者:定居者是实用的远见者,他们通过应用研究和差异化,将原型转化为有利可图的产品,弥合可能与实际之间的差距。
-
城镇规划者:城镇规划者是效率专家,他们将产品工业化,通过工业研究最大化规模经济,使创新对大众变得可及、可靠且经济实惠。
领导阶段 2 业务
在第一阶段,银行有两种类型的员工:先驱者和城镇规划者。先驱者是进行研发和建立酷炫原型的数据科学家。城镇规划者则管理 IT 基础设施。在我与这家银行合作期间,讨论的重点是如何整合这两个阵营。我们的 Wardley 图会如何建议我们解决这个问题?

图 6 — 作者提供的图像 — 领导阶段 2 业务,态度已绘制。关键在左下角
引入定居者
在构建智能应用的背景下,定居者实际上就是你的机器学习和数据工程师。他们的激励是将研究成果转化为价值。作为关注外部因素的尽责 CTO,我们可以看到从我们定制的 MLOps 平台转向更标准化的方法的必要性。
更标准化的方法需要从先驱者转变为定居者。当我们理解这一点后,招聘方法也调整以引进定居者。许多公司犯的错误是让他们的研发数据科学家变成定居者。成功的过渡需要两者,而这些态度往往不会存在于同一个人身上。
阶段 3 的 AI:在 AI 应用中开拓未知领域
图 7 描绘了从第 2 阶段到第 3 阶段业务的演变。作为首席技术官,我们可以利用这种映射方法来预测变化,并重新调整我们的战略。
第 1 点 — 随着 MLOps 整个领域变得像公用事业一样。你可以想象一个情况,其中模型被第三方 API 消耗。
部署:与其让企业开发自己的基础设施来部署机器学习模型,MLOps 实用服务提供商将提供无缝、可扩展的部署解决方案。这可能包括实时预测、批量评分或将模型嵌入应用程序中。
监控:机器学习模型需要对其性能随时间的漂移或输入数据的变化进行监控。作为服务的一部分,提供商将跟踪这些指标,识别问题,并在模型需要调整或重新训练时通知企业。
重新训练:随着新数据的出现或模型性能随时间下降,模型可能需要重新训练。MLOps 实用服务提供商可以处理这一过程,在新数据上重新训练模型,并在对业务操作造成最小干扰的情况下更新部署的模型。
第 2 点 — 数据管理作为公用事业将数据处理视为一种服务,类似于水或电。公司订阅这种服务,可能包括数据存储、安全、处理和分析。这使企业能够专注于其核心竞争力,而公用事业提供商则确保数据管理的质量,提供可扩展性、成本效益和可靠性。

图 7 — 作者提供的图像 — 第 3 阶段
先锋的角色再探
第 3 点——在第 3 阶段,先锋(数据科学家)的角色将再次成为焦点。然而,这次他们将拥有更好的装备。他们对 AI 未知领域的探索将不再受到同时开发强大操作流程的需要的限制。相反,他们将能够自由创造和创新。由于潜在的高回报,资本将流入这一新的潜在价值领域。
新市场和专业知识
第 4 点——随着 AI 应用的普及,并开始更深刻地影响社会的各个方面,AI 伦理的重要性也将增加。确保开发的 AI 应用是公平、透明和尊重隐私将变得至关重要。组织需要将 AI 伦理嵌入其创新过程,从设计和开发阶段一直到部署和监控。我们甚至可能看到围绕 AI 伦理和安全创建新的客户需求和市场。
最终思考
随着 MLOps 成为一种实用工具,AI 领域可能会经历显著的变革。AI 的民主化,加上对创新 AI 应用开发的关注以及 AI 伦理的重要性日益增长,预示着未来 AI 将越来越多地融入我们的日常生活。展望未来,企业的任务很明确:通过培养创新导向的思维方式、建立强大的先锋和开拓者团队,并关注不断发展的 AI 伦理领域,为实用 MLOps 时代做好准备。
最后,我要感谢 Simon Wardley 与世界分享这一方法。
感谢阅读。
关注我在 LinkedIn
订阅 Medium 以获取更多我的见解:
[## 通过我的推荐链接加入 Medium - John Adeojo
我分享数据科学项目、经验和专业知识,以帮助您在旅程中前进。您可以通过…
如果您有兴趣将 AI 或数据科学整合到您的业务运营中,我们邀请您预约一次免费的初步咨询:
发现我们在帮助企业实现雄心勃勃目标方面的专业知识,提供免费咨询。我们的数据科学家和…
www.data-centric-solutions.com
构建一个 AI 驱动的语言学习应用:从两个 AI 聊天中学习
创建一个双聊天机器人语言学习应用的逐步教程,使用 Langchain、OpenAI、gTTS 和 Streamlit
·发表于 Towards Data Science ·25 分钟阅读·2023 年 6 月 26 日
--

DALL-E 提示:两个友好的机器人相互交谈。
当我第一次开始学习一门新语言时,我喜欢购买那些“对话练习”书籍。我发现这些书籍非常有用,因为它们帮助我理解了语言的运作——不仅仅是语法和词汇,还有人们在日常生活中如何实际使用它。
现在随着大型语言模型(LLMs)的兴起,我想到一个问题:我是否可以以一种更互动、动态和可扩展的形式来复制这些语言学习书籍?我能否利用 LLM 创建一个生成新鲜、按需对话的工具,为语言学习者提供帮助?
这个想法激发了我今天想与大家分享的项目——一个由人工智能驱动的语言学习应用程序,在这个应用中,学习者可以观察并学习两个 AI 聊天机器人进行的用户定义的对话或辩论。
关于所使用的技术栈,我使用了 Langchain、OpenAI API、gTTS 和 Streamlit 来创建这个应用,用户可以定义角色、场景或辩论主题,让 AI 生成内容。
开发的语言学习应用程序演示。(视频由作者提供)
如果你对它的工作原理感到好奇,那就跟随我一步一步了解构建这个互动双聊天机器人系统的过程 🗺️📍🚶♀️。
你可以在这里💻找到完整的源代码。在这篇博客中,我们还将介绍关键的代码片段以解释这些想法。
既然如此,我们开始吧!
内容目录
· 1. 项目概述
· 2. 前置条件
∘ 2.1 LangChain
∘ 2.2 ConversationChain
· 3. 项目设计
∘ 3.1 开发单一聊天机器人
∘ 3.2 开发双聊天机器人系统
· 4. 使用 Streamlit 的应用界面设计
· 5. 学习成果与未来扩展
· 6. 结论
1. 项目概述
如前所述,我们的目标是创建一个由两个对话式 AI 或聊天机器人驱动的独特语言学习应用程序。这个应用程序的创新之处在于让这些聊天机器人相互互动,创造出逼真的目标语言对话。用户可以观察这些 AI 驱动的对话,将其作为语言学习资源,并理解所选语言的实际使用。
在我们的应用程序中,用户应具备根据自身需求定制学习体验的灵活性。他们可以调整多个设置,包括目标语言、学习模式、会话时长和熟练程度。
目标语言 🔤
用户可以选择他们希望学习的语言。这个选择将指导聊天机器人在互动过程中使用的语言。目前,我已经支持了英语——‘en’,德语——‘de’,西班牙语——‘es’,和法语——‘fr’,但只要 GPT 模型对这些语言有足够的知识,添加更多语言是微不足道的。
学习模式 📖
这个设置允许用户选择聊天机器人之间的对话风格。在“对话”模式下,用户可以定义角色(例如,顾客和服务员)和动作(点餐和接单),并指定一个场景(在餐厅),聊天机器人将模拟一个逼真的对话。在“辩论”模式下,用户被提示输入一个辩论话题(我们是否应该采用核能?)。然后,聊天机器人会围绕提供的话题进行激烈的辩论。
应用程序的界面应该是响应式的,并根据用户选择的学习模式动态调整,以提供无缝的用户体验。
会话时长 ⏰
会话时长设置让用户控制每次聊天机器人对话或辩论的持续时间。这意味着他们可以进行简短的快速对话,或根据个人喜好进行更长时间、更深入的讨论。
熟练程度 🏆
这个设置将聊天机器人的对话复杂性量身定制为用户的语言能力水平。初学者可能更喜欢简单的对话,而更高级的学习者则可以处理复杂的辩论或讨论。
一旦用户指定了这些设置,他们就可以启动会话,观看 AI 聊天机器人如何根据用户的偏好进行动态和互动的对话。我们的整体工作流程可以如下所示:

工作流程概述。用户指定的设置将用于配置提示,这些提示将提供给聊天机器人以生成对话。获得的脚本(连同用户设置)将用于填充应用程序界面。(图片由作者提供)
2. 前提条件
在我们深入开发应用程序之前,让我们熟悉一下我们将使用的工具。在这一部分,我们将简要介绍 LangChain 库,特别是 ConversationChain 模块,它作为我们应用程序的核心部分。
2.1 LangChain
构建一个由大型语言模型(LLMs)驱动的应用程序涉及许多复杂性。你需要通过 API 调用与语言模型提供者进行接口连接,将这些模型与各种数据源连接,处理用户互动的历史记录,并设计执行复杂任务的管道。这正是 LangChain 库发挥作用的地方。
LangChain是一个旨在简化基于大语言模型(LLM)应用程序开发的框架。它提供了一系列组件,解决了上述常见痛点。无论是管理与语言模型提供者的互动,组织数据连接,维护历史互动的记忆,还是定义复杂的任务管道,LangChain 都能应对自如。
LangChain 介绍的一个关键概念是“Chain”。本质上,链允许我们将多个组件组合在一起,以创建一个统一的应用程序。例如,LangChain 中的一个基本链类型是 LLMChain。它创建了一个管道,首先使用用户提供的输入键值格式化提示模板,然后将格式化的指令传递给 LLM,最后返回 LLM 的输出。
LangChain 主持各种链类型,包括 RetrievalQAChain,用于对文档进行问答,SummarizationChain,用于总结多个文档,以及我们今天的重点,即 ConversationChain。
2.2 ConversationChain
ConversationChain 用于通过提供一个消息交换和存储对话历史的框架来促进互动对话。以下是一个示例代码片段,以说明其用法:
from langchain.chains import ConversationChain
# Create conversation chain
conversation = ConversationChain(memory, prompt, llm)
# Run conversation chain
conversation.predict(input="Hi there!")
# Obtain the LLM response: "Hello! How can I assist you today?"
# We can keep calling conversation chain
conversation.predict(input="I'm doing well! Just having a conversation with an AI.")
# Obtain the LLM response: "That sounds like fun! I'm happy to chat with you. Is there anything specific you'd like to talk about?"
在这个示例中,ConversationChain 接受三个输入,memory,一个持有互动历史的 LangChain 组件;prompt,输入到 LLM 的内容;以及 llm,核心的大型语言模型(例如,GPT-3.5-Turbo 等)。
一旦 ConversationChain 对象被实例化,我们只需使用用户输入调用 conversation.predict() 即可获得 LLM 的响应。ConversationChain 的便利之处在于,我们实际上可以多次调用 conversation.predict(),它会自动在后台记录消息历史。
在下一部分,我们将利用 ConversationChain 的强大功能创建我们的聊天机器人,并深入探讨记忆、提示模板和 LLM 的定义与使用。
如果你想了解更多关于 LangChain 的信息,可以查看他们的官方文档。此外,这个YouTube 播放列表也提供了全面的实践介绍。
3. 项目设计
既然我们对要构建的内容和工具有了明确的了解,就该动手编写代码了!在这一部分,我们将专注于创建双聊天机器人交互的细节。首先,我们将探讨单一聊天机器人的类定义,然后扩展这一定义以创建一个双聊天机器人类,使我们的两个聊天机器人能够互相互动。我们将把使用 Streamlit 设计应用界面的工作留到第四部分。
3.1 开发一个单一的聊天机器人
在这一小节中,我们将一起开发一个单一的聊天机器人,随后将其集成到双聊天机器人系统中。让我们从整体的类设计开始,然后将注意力转向提示工程。
🏗️ 类设计
我们的聊天机器人类应该能够管理一个单独的聊天机器人。这包括实例化一个以用户指定的 LLM 作为基础的聊天机器人,根据用户的意图提供指令,并支持交互式的多轮对话。考虑到这一点,让我们开始编码吧。
首先,导入必要的库:
import os
import openai
from langchain.prompts import (
ChatPromptTemplate,
MessagesPlaceholder,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate
)
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain
from langchain.chains import ConversationChain
from langchain.chat_models import ChatOpenAI
from langchain.memory import ConversationBufferMemory
接下来,我们定义类构造函数:
class Chatbot:
"""Class definition for a single chatbot with memory, created with LangChain."""
def __init__(self, engine):
"""Select backbone large language model, as well as instantiate
the memory for creating language chain in LangChain.
"""
# Instantiate llm
if engine == 'OpenAI':
# Reminder: need to set up openAI API key
# (e.g., via environment variable OPENAI_API_KEY)
self.llm = ChatOpenAI(
model_name="gpt-3.5-turbo",
temperature=0.7
)
else:
raise KeyError("Currently unsupported chat model type!")
# Instantiate memory
self.memory = ConversationBufferMemory(return_messages=True)
目前,你只能选择使用原生的 OpenAI API。然而,添加更多后端 LLM 是很简单的,因为 LangChain 支持多种类型(例如 Azure OpenAI 端点、Anthropic 聊天模型、Google Vertex AI 上的 PaLM API 等)。
除了 LLM,我们还需要实例化另一个重要的组件——记忆,它跟踪对话历史。在这里,我们使用ConversationBufferMemory来实现这一目的,它仅仅是在聊天机器人的当前输入前添加最后几条输入/输出。这是 LangChain 提供的最简单的记忆类型,对于我们当前的目的已经足够。
要全面了解其他类型的记忆,请参考官方文档。
继续,我们需要一个类方法,允许我们给聊天机器人下达指令并与其对话。这就是self.instruct()的作用:
def instruct(self, role, oppo_role, language, scenario,
session_length, proficiency_level,
learning_mode, starter=False):
"""Determine the context of chatbot interaction.
"""
# Define language settings
self.role = role
self.oppo_role = oppo_role
self.language = language
self.scenario = scenario
self.session_length = session_length
self.proficiency_level = proficiency_level
self.learning_mode = learning_mode
self.starter = starter
# Define prompt template
prompt = ChatPromptTemplate.from_messages([
SystemMessagePromptTemplate.from_template(self._specify_system_message()),
MessagesPlaceholder(variable_name="history"),
HumanMessagePromptTemplate.from_template("{input}")
])
# Create conversation chain
self.conversation = ConversationChain(memory=self.memory, prompt=prompt,
llm=self.llm, verbose=False)
- 我们定义了一些设置,以便用户可以自定义他们的学习体验。
除了在“第一部分 项目概述”中提到的内容外,我们还有四个新的属性:
self.role/self.oppo_role: 这个属性的形式是一个字典,用于记录角色名称及其对应的动作。例如:
self.role = {'name': 'Customer', 'action': 'ordering food'}
self.oppo_role表示与当前聊天机器人进行对话的另一个聊天机器人所扮演的角色。这是重要的,因为当前聊天机器人需要了解它正在与谁沟通,以提供必要的上下文信息。
self.scenario 为对话设置了场景。对于“对话”学习模式,self.scenario 代表对话发生的地方。
正在发生;对于“辩论”模式,self.scenario 代表辩论的话题。
最后,self.starter 只是一个布尔标志,用于指示当前聊天机器人是否会发起对话。
- 我们为聊天机器人构建提示。
在 OpenAI 中,一个聊天模型通常接受一系列消息作为输入,并返回模型生成的消息作为输出。LangChain 支持 SystemMessage、AIMessage 和 HumanMessage:SystemMessage 帮助设置聊天机器人的行为,AIMessage 存储以前聊天机器人的响应,HumanMessage 提供聊天机器人需要回应的请求或评论。
LangChain 方便地提供了 PromptTemplate 来简化提示生成和处理。对于聊天机器人应用,我们需要为所有三种消息类型指定 PromptTemplate。最关键的部分是设置 SystemMessage,它控制聊天机器人的行为。我们有一个单独的方法 self._specify_system_message() 来处理这个问题,稍后我们会详细讨论。
- 最后,我们把所有部分整合在一起,构建一个
ConversationChain。
🖋️ 提示设计
我们现在的重点是指导聊天机器人根据用户的需求参与对话。为此,我们有 self._specify_system_message() 方法。该方法的签名如下所示:
def _specify_system_message(self):
"""Specify the behavior of the chatbot, which consists of the following
aspects:
- general context: conducting conversation/debate under given scenario
- the language spoken
- purpose of the simulated conversation/debate
- language complexity requirement
- exchange length requirement
- other nuance constraints
Outputs:
--------
prompt: instructions for the chatbot.
"""
本质上,这个方法编译一个字符串,然后将其输入到 SystemMessagePromptTemplate.from_template() 以指示聊天机器人,如上面 self.instruct() 方法的定义所示。我们将在接下来的部分中分析这个“长字符串”,以了解每个语言学习需求如何融入提示中。
1️⃣ 会话长度
会话长度通过直接指定在一个会话中可以发生的最大交换次数来控制。这些数字目前是硬编码的。
# Determine the number of exchanges between two bots
exchange_counts_dict = {
'Short': {'Conversation': 8, 'Debate': 4},
'Long': {'Conversation': 16, 'Debate': 8}
}
exchange_counts = exchange_counts_dict[self.session_length][self.learning_mode]
2️⃣ 聊天机器人在一次交换中可以说的句子数量
除了限制允许的总交换次数外,限制聊天机器人在一次交换中可以说多少也是有益的,或者等效地,限制句子数量。
在我的实验中,通常不需要在“对话”模式中限制这个,因为聊天机器人模拟真实对话,往往会在合理的长度内发言。然而,在“辩论”模式下,需要施加限制,否则聊天机器人可能会继续发言,最终生成一篇“文章” 😆。
类似于限制会话长度,限制演讲长度的数字也是硬编码的,并且与用户在目标语言中的熟练程度相对应:
# Determine number of sentences in one debate round
argument_num_dict = {
'Beginner': 4,
'Intermediate': 6,
'Advanced': 8
}
3️⃣ 确定演讲复杂度
在这里,我们调整聊天机器人可以使用的语言复杂度级别:
if self.proficiency_level == 'Beginner':
lang_requirement = """use as basic and simple vocabulary and
sentence structures as possible. Must avoid idioms, slang,
and complex grammatical constructs."""
elif self.proficiency_level == 'Intermediate':
lang_requirement = """use a wider range of vocabulary and a variety of sentence structures.
You can include some idioms and colloquial expressions,
but avoid highly technical language or complex literary expressions."""
elif self.proficiency_level == 'Advanced':
lang_requirement = """use sophisticated vocabulary, complex sentence structures, idioms,
colloquial expressions, and technical language where appropriate."""
else:
raise KeyError('Currently unsupported proficiency level!')
4️⃣ 把所有内容整合起来!
以下是不同学习模式下的指令示例:
# Compile bot instructions
if self.learning_mode == 'Conversation':
prompt = f"""You are an AI that is good at role-playing.
You are simulating a typical conversation happened {self.scenario}.
In this scenario, you are playing as a {self.role['name']} {self.role['action']}, speaking to a
{self.oppo_role['name']} {self.oppo_role['action']}.
Your conversation should only be conducted in {self.language}. Do not translate.
This simulated {self.learning_mode} is designed for {self.language} language learners to learn real-life
conversations in {self.language}. You should assume the learners' proficiency level in
{self.language} is {self.proficiency_level}. Therefore, you should {lang_requirement}.
You should finish the conversation within {exchange_counts} exchanges with the {self.oppo_role['name']}.
Make your conversation with {self.oppo_role['name']} natural and typical in the considered scenario in
{self.language} cultural."""
elif self.learning_mode == 'Debate':
prompt = f"""You are an AI that is good at debating.
You are now engaged in a debate with the following topic: {self.scenario}.
In this debate, you are taking on the role of a {self.role['name']}.
Always remember your stances in the debate.
Your debate should only be conducted in {self.language}. Do not translate.
This simulated debate is designed for {self.language} language learners to
learn {self.language}. You should assume the learners' proficiency level in {self.language}
is {self.proficiency_level}. Therefore, you should {lang_requirement}.
You will exchange opinions with another AI (who plays the {self.oppo_role['name']} role)
{exchange_counts} times.
Everytime you speak, you can only speak no more than
{argument_num_dict[self.proficiency_level]} sentences."""
else:
raise KeyError('Currently unsupported learning mode!')
5️⃣ 谁先发言?
最后,我们指示聊天机器人是否应该先发言或等待对方 AI 的回应:
# Give bot instructions
if self.starter:
# In case the current bot is the first one to speak
prompt += f"You are leading the {self.learning_mode}. \n"
else:
# In case the current bot is the second one to speak
prompt += f"Wait for the {self.oppo_role['name']}'s statement."
现在我们已经完成了提示设计🎉 简要总结一下,这就是我们迄今为止开发的内容:

单一聊天机器人类。(图片由作者提供)
3.2 开发双聊天机器人系统
现在我们来到了令人兴奋的部分!在这一小节中,我们将开发一个双聊天机器人类,让两个聊天机器人相互互动💬💬
🏗️ 类设计
由于之前开发的单一聊天机器人类,我们可以轻松地在类构造函数中实例化两个聊天机器人:
class DualChatbot:
"""Class definition for dual-chatbots interaction system,
created with LangChain."""
def __init__(self, engine, role_dict, language, scenario, proficiency_level,
learning_mode, session_length):
# Instantiate two chatbots
self.engine = engine
self.proficiency_level = proficiency_level
self.language = language
self.chatbots = role_dict
for k in role_dict.keys():
self.chatbots[k].update({'chatbot': Chatbot(engine)})
# Assigning roles for two chatbots
self.chatbots['role1']['chatbot'].instruct(role=self.chatbots['role1'],
oppo_role=self.chatbots['role2'],
language=language, scenario=scenario,
session_length=session_length,
proficiency_level=proficiency_level,
learning_mode=learning_mode, starter=True)
self.chatbots['role2']['chatbot'].instruct(role=self.chatbots['role2'],
oppo_role=self.chatbots['role1'],
language=language, scenario=scenario,
session_length=session_length,
proficiency_level=proficiency_level,
learning_mode=learning_mode, starter=False)
# Add session length
self.session_length = session_length
# Prepare conversation
self._reset_conversation_history()
self.chatbots是一个字典,用于存储与两个机器人相关的信息:
# For "conversation" mode
self.chatbots= {
'role1': {'name': 'Customer',
'action': 'ordering food',
'chatbot': Chatbot()},
'role2': {'name': 'Waitstaff',
'action': 'taking the order',
'chatbot': Chatbot()}
}
# For "debate" mode
self.chatbots= {
'role1': {'name': 'Proponent',
'chatbot': Chatbot()},
'role2': {'name': 'Opponent',
'chatbot': Chatbot()}
}
self._reset_conversation_history用于启动一个新的对话历史并提供初始指令给聊天机器人:
def _reset_conversation_history(self):
"""Reset the conversation history.
"""
# Placeholder for conversation history
self.conversation_history = []
# Inputs for two chatbots
self.input1 = "Start the conversation."
self.input2 = ""
为了促进两个聊天机器人之间的互动,我们使用了self.step()方法。此方法允许两个机器人之间进行一轮互动:
def step(self):
"""Make one exchange round between two chatbots.
"""
# Chatbot1 speaks
output1 = self.chatbots['role1']['chatbot'].conversation.predict(input=self.input1)
self.conversation_history.append({"bot": self.chatbots['role1']['name'], "text": output1})
# Pass output of chatbot1 as input to chatbot2
self.input2 = output1
# Chatbot2 speaks
output2 = self.chatbots['role2']['chatbot'].conversation.predict(input=self.input2)
self.conversation_history.append({"bot": self.chatbots['role2']['name'], "text": output2})
# Pass output of chatbot2 as input to chatbot1
self.input1 = output2
# Translate responses
translate1 = self.translate(output1)
translate2 = self.translate(output2)
return output1, output2, translate1, translate2
请注意,我们嵌入了一个名为self.translate()的方法。此方法的目的是将脚本翻译成英语。此功能对于语言学习者可能很有用,因为他们可以理解在目标语言中生成的对话的含义。
为实现翻译功能,我们可以使用基本的LLMChain,它需要一个后台 LLM 模型和一个指令提示:
def translate(self, message):
"""Translate the generated script into English.
"""
if self.language == 'English':
# No translation performed
translation = 'Translation: ' + message
else:
# Instantiate translator
if self.engine == 'OpenAI':
# Reminder: need to set up openAI API key
# (e.g., via environment variable OPENAI_API_KEY)
self.translator = ChatOpenAI(
model_name="gpt-3.5-turbo",
temperature=0.7
)
else:
raise KeyError("Currently unsupported translation model type!")
# Specify instruction
instruction = """Translate the following sentence from {src_lang}
(source language) to {trg_lang} (target language).
Here is the sentence in source language: \n
{src_input}."""
prompt = PromptTemplate(
input_variables=["src_lang", "trg_lang", "src_input"],
template=instruction,
)
# Create a language chain
translator_chain = LLMChain(llm=self.translator, prompt=prompt)
translation = translator_chain.predict(src_lang=self.language,
trg_lang="English",
src_input=message)
return translation
最后,语言学习者能够获得生成对话脚本的关键语言学习点的总结可能是有益的,无论是关键词汇、语法点还是功能短语。为此,我们可以包含一个self.summary()方法:
def summary(self, script):
"""Distill key language learning points from the generated scripts.
"""
# Instantiate summary bot
if self.engine == 'OpenAI':
# Reminder: need to set up openAI API key
# (e.g., via environment variable OPENAI_API_KEY)
self.summary_bot = ChatOpenAI(
model_name="gpt-3.5-turbo",
temperature=0.7
)
else:
raise KeyError("Currently unsupported summary model type!")
# Specify instruction
instruction = """The following text is a simulated conversation in
{src_lang}. The goal of this text is to aid {src_lang} learners to learn
real-life usage of {src_lang}. Therefore, your task is to summarize the key
learning points based on the given text. Specifically, you should summarize
the key vocabulary, grammar points, and function phrases that could be important
for students learning {src_lang}. Your summary should be conducted in English, but
use examples from the text in the original language where appropriate.
Remember your target students have a proficiency level of
{proficiency} in {src_lang}. You summarization must match with their
proficiency level.
The conversation is: \n
{script}."""
prompt = PromptTemplate(
input_variables=["src_lang", "proficiency", "script"],
template=instruction,
)
# Create a language chain
summary_chain = LLMChain(llm=self.summary_bot, prompt=prompt)
summary = summary_chain.predict(src_lang=self.language,
proficiency=self.proficiency_level,
script=script)
return summary
类似于self.translate()方法,我们使用了一个基本的LLMChain来执行所需的任务。请注意,我们明确要求语言模型根据用户的熟练程度总结关键的语言学习点。
有了这一点,我们已经完成了双聊天机器人类的开发🥂 简要总结一下,这就是我们迄今为止开发的内容:

单一聊天机器人 & 双聊天机器人类。(图片由作者提供)
4. 使用 Streamlit 进行应用程序界面设计
我们现在准备开发用户界面🖥️ 对于这个项目,我们将使用 Streamlit 库来构建前端。
如果你不熟悉,Streamlit 是一个开源 Python 库,用于创建专注于数据科学和机器学习的互动式 Web 应用程序。它通过提供易于使用的 API、即时更新的实时代码重载、用于用户输入的互动小部件、对数据可视化库的支持以及包含丰富媒体的能力,简化了构建和部署应用程序的过程。
让我们从一个新的 Python 脚本 app.py 开始,并导入必要的库:
import streamlit as st
from streamlit_chat import message
from chatbot import DualChatbot
import time
from gtts import gTTS
from io import BytesIO
除了主要的streamlit库,我们还导入了streamlit_chat库,这是一个由社区构建的 Streamlit 组件,专门用于创建聊天机器人用户界面。我们之前开发的DualChatbot类存储在chatbot.py文件中,因此也需要导入该文件。最后,我们导入gTTS,即Google Text-to-Speech,以为这个项目中的机器人生成的对话脚本添加音频。
在配置 Streamlit 界面之前,让我们首先定义语言学习设置:
# Define the language learning settings
LANGUAGES = ['English', 'German', 'Spanish', 'French']
SESSION_LENGTHS = ['Short', 'Long']
PROFICIENCY_LEVELS = ['Beginner', 'Intermediate', 'Advanced']
MAX_EXCHANGE_COUNTS = {
'Short': {'Conversation': 8, 'Debate': 4},
'Long': {'Conversation': 16, 'Debate': 8}
}
AUDIO_SPEECH = {
'English': 'en',
'German': 'de',
'Spanish': 'es',
'French': 'fr'
}
AVATAR_SEED = [123, 42]
# Define backbone llm
engine = 'OpenAI'
AVATAR_SEED用于为不同的聊天机器人生成不同的头像图标。
我们开始设置用户界面的基本布局,并建立供用户选择的选项:
# Set the title of the app
st.title('Language Learning App 🌍📖🎓')
# Set the description of the app
st.markdown("""
This app generates conversation or debate scripts to aid in language learning 🎯
Choose your desired settings and press 'Generate' to start 🚀
""")
# Add a selectbox for learning mode
learning_mode = st.sidebar.selectbox('Learning Mode 📖', ('Conversation', 'Debate'))
if learning_mode == 'Conversation':
role1 = st.sidebar.text_input('Role 1 🎭')
action1 = st.sidebar.text_input('Action 1 🗣️')
role2 = st.sidebar.text_input('Role 2 🎭')
action2 = st.sidebar.text_input('Action 2 🗣️')
scenario = st.sidebar.text_input('Scenario 🎥')
time_delay = 2
# Configure role dictionary
role_dict = {
'role1': {'name': role1, 'action': action1},
'role2': {'name': role2, 'action': action2}
}
else:
scenario = st.sidebar.text_input('Debate Topic 💬')
# Configure role dictionary
role_dict = {
'role1': {'name': 'Proponent'},
'role2': {'name': 'Opponent'}
}
time_delay = 5
language = st.sidebar.selectbox('Target Language 🔤', LANGUAGES)
session_length = st.sidebar.selectbox('Session Length ⏰', SESSION_LENGTHS)
proficiency_level = st.sidebar.selectbox('Proficiency Level 🏆', PROFICIENCY_LEVELS)
注意引入了time_delay变量。它用于指定显示两个连续消息之间的等待时间。如果此延迟设置为零,则两个聊天机器人之间生成的交换将迅速出现在应用程序中(仅受限于 OpenAI 的响应时间)。然而,为了用户体验,在下一次交换出现之前,允许用户有足够的时间阅读生成的消息可能是有益的。
接下来,我们初始化 Streamlit 会话状态以存储用户特定的会话数据:
if "bot1_mesg" not in st.session_state:
st.session_state["bot1_mesg"] = []
if "bot2_mesg" not in st.session_state:
st.session_state["bot2_mesg"] = []
if 'batch_flag' not in st.session_state:
st.session_state["batch_flag"] = False
if 'translate_flag' not in st.session_state:
st.session_state["translate_flag"] = False
if 'audio_flag' not in st.session_state:
st.session_state["audio_flag"] = False
if 'message_counter' not in st.session_state:
st.session_state["message_counter"] = 0
在这里我们回答两个问题:
1️⃣ 首先,我们为什么需要“session_state”?
在 Streamlit 中,每次用户与应用程序交互时,Streamlit 会从头到尾重新运行整个脚本,更新应用程序的输出。然而,Streamlit 的这种反应性特征在你想要维护用户特定数据或在不同交互或页面之间保留状态时可能会成为挑战。由于 Streamlit 在每次用户交互时都会重新加载脚本,常规 Python 变量会丢失其值,应用程序将重置为初始状态。
这就是session_state发挥作用的地方。Streamlit 中的会话状态提供了一种存储和检索数据的方式,这些数据在用户会话期间会持久存在,即使应用程序被重新加载或用户在不同组件或页面之间导航时。它允许你保持状态信息并为每个用户保留应用程序的上下文。
2️⃣ 其次,session_state中存储了哪些变量?
“bot1_mesg”是一个列表,其中每个元素都是一个字典,包含第一台聊天机器人说的话。它具有以下键:“role”、“content”和“translation”。同样的定义适用于“bot2_mesg”。
“batch_flag” 是一个布尔标志,用于指示对话交流是否一次性显示或有时间延迟。在当前设计中,当两个聊天机器人之间的对话首次生成时,它们的聊天将会有时间延迟地出现。之后,用户可能希望查看生成对话的翻译或添加音频,存储的对话消息(在“bot1_mesg”和“bot2_mesg”中)将一次性显示。这是有利的,因为我们不需要再次调用 OpenAI API,从而减少成本和延迟。
“translate_flag” 和 “audio_flag” 用于指示翻译和/或音频是否会显示在原始对话旁边。
“message_counter” 是一个计数器,每当一个来自聊天机器人的消息显示时,它会加一。这个想法是将消息 ID 与此计数器关联,因为 Streamlit 要求每个 UI 组件必须有唯一的 ID。
现在我们可以引入让两个聊天机器人互动并生成对话的逻辑:
if 'dual_chatbots' not in st.session_state:
if st.sidebar.button('Generate'):
# Add flag to indicate if this is the first time running the script
st.session_state["first_time_exec"] = True
with conversation_container:
if learning_mode == 'Conversation':
st.write(f"""#### The following conversation happens between
{role1} and {role2} {scenario} 🎭""")
else:
st.write(f"""#### Debate 💬: {scenario}""")
# Instantiate dual-chatbot system
dual_chatbots = DualChatbot(engine, role_dict, language, scenario,
proficiency_level, learning_mode, session_length)
st.session_state['dual_chatbots'] = dual_chatbots
# Start exchanges
for _ in range(MAX_EXCHANGE_COUNTS[session_length][learning_mode]):
output1, output2, translate1, translate2 = dual_chatbots.step()
mesg_1 = {"role": dual_chatbots.chatbots['role1']['name'],
"content": output1, "translation": translate1}
mesg_2 = {"role": dual_chatbots.chatbots['role2']['name'],
"content": output2, "translation": translate2}
new_count = show_messages(mesg_1, mesg_2,
st.session_state["message_counter"],
time_delay=time_delay, batch=False,
audio=False, translation=False)
st.session_state["message_counter"] = new_count
# Update session state
st.session_state.bot1_mesg.append(mesg_1)
st.session_state.bot2_mesg.append(mesg_2)
当第一次运行脚本时,会话状态中将没有“dual_chatbots”键(因为双聊天机器人尚未创建)。因此,当用户点击侧边栏上的“Generate”按钮时,上述代码片段将被执行。两个聊天机器人将往返聊天给定次数,所有对话消息都记录在会话状态中。show_message() 函数是一个辅助函数,旨在成为唯一的接口来样式化消息显示。我们将在本节末尾再次回到它。
现在,如果用户与应用互动并更改一些设置,Streamlit 将从头开始重新运行整个脚本。由于我们已经生成了所需的对话脚本,因此无需再次调用 OpenAI API。相反,我们可以简单地检索存储的信息:
if 'dual_chatbots' in st.session_state:
# Show translation
if translate_col.button('Translate to English'):
st.session_state['translate_flag'] = True
st.session_state['batch_flag'] = True
# Show original text
if original_col.button('Show original'):
st.session_state['translate_flag'] = False
st.session_state['batch_flag'] = True
# Append audio
if audio_col.button('Play audio'):
st.session_state['audio_flag'] = True
st.session_state['batch_flag'] = True
# Retrieve generated conversation & chatbots
mesg1_list = st.session_state.bot1_mesg
mesg2_list = st.session_state.bot2_mesg
dual_chatbots = st.session_state['dual_chatbots']
# Control message appearance
if st.session_state["first_time_exec"]:
st.session_state['first_time_exec'] = False
else:
# Show complete message
with conversation_container:
if learning_mode == 'Conversation':
st.write(f"""#### {role1} and {role2} {scenario} 🎭""")
else:
st.write(f"""#### Debate 💬: {scenario}""")
for mesg_1, mesg_2 in zip(mesg1_list, mesg2_list):
new_count = show_messages(mesg_1, mesg_2,
st.session_state["message_counter"],
time_delay=time_delay,
batch=st.session_state['batch_flag'],
audio=st.session_state['audio_flag'],
translation=st.session_state['translate_flag'])
st.session_state["message_counter"] = new_count
请注意会话状态中还有一个名为“first_time_exec”的标志。这个标志用于指示原始生成的脚本是否已经在应用中显示。如果我们去掉这个检查,应用第一次运行时相同的消息将会出现两次。
剩下的唯一任务是在 UI 中加入关键学习点的总结。为此,我们可以使用 st.expander。在 Streamlit 中,st.expander 对于我们希望以简洁形式呈现的大量内容或信息很有用,最初隐藏视图。当用户点击扩展器时,内容将展开或折叠,从而显示或隐藏额外的细节。
# Create summary for key learning points
summary_expander = st.expander('Key Learning Points')
scripts = []
for mesg_1, mesg_2 in zip(mesg1_list, mesg2_list):
for i, mesg in enumerate([mesg_1, mesg_2]):
scripts.append(mesg['role'] + ': ' + mesg['content'])
# Compile summary
if "summary" not in st.session_state:
summary = dual_chatbots.summary(scripts)
st.session_state["summary"] = summary
else:
summary = st.session_state["summary"]
with summary_expander:
st.markdown(f"**Here is the learning summary:**")
st.write(summary)
由于关键学习点的总结也是通过调用 OpenAI API 生成的,我们可以将生成的总结保存到会话状态中,以便如果脚本第二次运行时可以检索这些内容。
最后,让我们用辅助函数 show_message 完成 Streamlit UI 设计:
def show_messages(mesg_1, mesg_2, message_counter,
time_delay, batch=False, audio=False,
translation=False):
"""Display conversation exchanges. This helper function supports
displaying original texts, translated texts, and audio speech.
Output:
-------
message_counter: updated counter for ID key
"""
for i, mesg in enumerate([mesg_1, mesg_2]):
# Show original exchange ()
message(f"{mesg['content']}", is_user=i==1, avatar_style="bottts",
seed=AVATAR_SEED[i],
key=message_counter)
message_counter += 1
# Mimic time interval between conversations
# (this time delay only appears when generating
# the conversation script for the first time)
if not batch:
time.sleep(time_delay)
# Show translated exchange
if translation:
message(f"{mesg['translation']}", is_user=i==1, avatar_style="bottts",
seed=AVATAR_SEED[i],
key=message_counter)
message_counter += 1
# Append autio to the exchange
if audio:
tts = gTTS(text=mesg['content'], lang=AUDIO_SPEECH[language])
sound_file = BytesIO()
tts.write_to_fp(sound_file)
st.audio(sound_file)
return message_counter
有几个要点需要进一步解释:
1️⃣ message() 对象
这部分属于 streamlit_chat 库,用于显示消息。在最简单的形式下,我们有:
import streamlit as st
from streamlit_chat import message
message("Hellp, I am a Chatbot, how may I help you?")
message("Hey, what's a chatbot", is_user=True)

(图片来自 streamlit_chat GitHub 仓库)
其中参数 is_user 决定消息是左对齐还是右对齐。在我们的 show_message 代码片段中,我们还指定了 avatar_style 和 seed 来设置两个聊天机器人的头像图标。key 参数仅用于为每条消息分配唯一的 ID,这是 Streamlit 所要求的。
2️⃣ 语音合成
在这里,我们使用 gTTS 库基于生成的脚本创建目标语言的音频语音。这个库使用起来非常简单,但它有一个限制:你只能使用一种声音。在生成音频对象后,我们可以使用 st.audio 为应用中的每条消息创建一个音频播放器。
太棒了!我们现在已经完成了 UI 设计 😃 在终端中输入以下命令:
streamlit run app.py
你应该能在浏览器中看到应用,并能够与其互动。干得好!

开发的语言学习应用界面。(作者提供的图片)
5. 学习与未来扩展
在结束之前,我想与你分享一些来自这个项目的关键学习和未来扩展的潜在方向。
1️⃣ 如何结束对话?
如果想要正确实现,这个问题实际上比看起来要复杂得多。理想情况下,我们希望对话自然结束。然而,在我的一些实验中,我注意到聊天机器人在对话结束时会不断互相说“谢谢”或“再见”,这不必要地延长了对话。一些可能的解决方案包括:
-
交换轮次的硬限制:这可能是最简单的解决方案,也是我们在这个项目中采用的解决方案。然而,它可能并不总是理想的,因为它可能导致对话被过早终止。作为解决方法,我们已经在
SystemMessage中指示机器人在设定的交换轮次内完成对话。 -
使用“信号词”:聊天机器人可以被编程以在认为对话自然结束时说出特定的“信号词”(例如,“对话结束”)。然后可以实现逻辑来检测这些“信号词”并相应地结束循环。
-
对话的后处理:一旦聊天机器人生成了对话,可以部署另一个 LLM 作为“编辑器”来修剪对话。这可能是一个有效的方法。然而,其缺点可能包括设计额外的提示、调用 OpenAI API 可能产生的额外费用以及增加的延迟。
2️⃣ 如何控制语言复杂性?
根据我的经验,开发的聊天机器人似乎在遵循语言复杂度的指示方面存在困难:有时即使熟练度设定为“初学者”,也会出现“中级”语言使用。这可能是因为当前的提示设计不足以明确区分不同复杂度级别之间的细微差别。
解决这个问题有几种方法:首先,我们可以进行上下文学习。也就是说,我们提供示例给聊天机器人,并展示我们期望在不同复杂度级别中使用的语言。另一种方法与我们之前讨论的类似:我们可以使用另一个 LLM 来调整对话的复杂性。实质上,这个额外的 LLM 可以利用生成的脚本作为起点,并重写一个新的脚本,以匹配用户期望的熟练程度。
3️⃣ 更好的文本到语音库?
当前项目仅使用了简单的 gTTS 库来合成语音,还有改进的空间。更先进的库提供了多语言支持、多说话者支持以及更自然的语音。比如:pyttsx3、Amazon Polly、IBM Watson TTS、Microsoft Azure Cognitive Services TTS、Coqui.ai-TTS,以及 Meta 最近发布的Voicebox。
4️⃣ 更多不同场景的测试?
由于时间限制,我只测试了几个场景,以确定聊天机器人是否能够生成有意义的对话。这些测试发现了我最初提示设计中的问题,提供了改进的机会。额外的场景测试可能会揭示被忽视的领域,并提出改进提示的方法。我已编制了一份全面的列表 ,包括典型的“对话”场景和“辩论”话题。随意尝试这些场景,并评估当前提示设计的表现。
5️⃣ 包含其他形式的生成型 AI?
这个项目主要探索了文本到文本(聊天机器人)和文本到语音的生成型 AI 技术。我们可以通过利用其他形式的生成型 AI,如文本到图像或文本到视频,进一步提升用户体验。
-
文本到图像:对于每个用户输入的场景,我们可以使用文本到图像模型来创建相应的图像。将这些图像与生成的对话一起展示,可以提供视觉上下文并增强语言学习的参与感。像StableDiffusion、Midjourney和DALL-E这样的模型可以用于此目的。
-
文本转视频:为了让应用更具多媒体功能,我们可以根据输入场景生成视频。像RunwayML这样的工具可以帮助实现这一点。此外,我们甚至可以尝试创建数字人来呈现对话,如果执行得当,这可能会大大提升用户体验。Synthesia可能是一个合适的工具。
6️⃣ 更多语言学习设置?
目前,我们的应用主要集中于“对话”和“辩论”学习模式。然而,增长潜力巨大。例如,我们可以引入其他学习模式,如“讲故事”和“文化学习”。此外,我们可以扩展聊天机器人的互动,以适应更多专业和技术场景。这些可能包括会议、谈判,或销售与市场、法律、工程等领域。这样的功能可能对那些希望提升专业语言能力的语言学习者有帮助。
6. 结论
哇,多么精彩的旅程!非常感谢你一直陪伴我 😃 从设计提示到创建聊天机器人,我们确实覆盖了很多领域。使用 LangChain 和 Streamlit,我们构建了一个功能齐全的双聊天机器人系统,可以用于语言学习,不错吧!
如果你觉得我的内容有用,可以通过这里买杯咖啡给我🤗 非常感谢你的支持!
建立一个能够识别我手写字的人工智能 — 第一部分
原文:
towardsdatascience.com/building-an-ai-to-recognize-my-handwriting-part-i-7bef0d3cdc46
理论(和实践)的出发点
·发表于Towards Data Science ·12 分钟阅读·2023 年 4 月 2 日
--

图片由作者提供
大约十年前,受到 Tim Ferriss 和其他自助书籍作者的启发,我开始定期在笔记本(纸质的那种)上手写。我现在已经填满了 10 到 15 本笔记本。把它们数字化岂不是很好吗?
本系列文章记录了我开发一个能够将我的手写笔记转换为正式纯文本文件的人工智能的过程。根据我的手写字的样子,这将确实非常具有挑战性。
这第一篇文章,第一部分,将涵盖基础知识。我将首先进一步解释我的动机,然后介绍理论框架,说明如何可以解决这个问题。
我将简要介绍卷积神经网络(CNNs),这是一种专门为图像识别设计的人工神经网络。之后,我将尝试一种自动化的方式来“识别”我的手写字,目的是避免手动标注和注释我的输入数据。
接下来的部分将更加实用,并实际使用 CNNs 与训练数据。但我认为这第一部分作为必要的基础是有用的,即使它以令人失望的结局告终。
像我许多的文章一样,我会在进展中发布,处理尚未知道的挑战,带领读者一起体验这个过程。
根据我收到的关于“数据科学家转量化交易”系列的反馈,我将将笔记本和文件上传到 GitHub以便更好地跟随。它们可以在这里找到(GitHub 链接),并将定期更新。
我希望你阅读这些内容时能像我创作时那样享受。随时欢迎联系我获取更多想法或问题,或者对这篇文章进行评论。
最好,
乔纳斯
我的动机:一堆个人笔记本
这十年来,我一直在做一种叫做“日记记录”的事情。与传统的“亲爱的日记”式写作不同,这种记录的目的是为了整理对各种事情的思考,而不是每天总结你的一天细节,这些细节通常在下周就变得无关紧要。比如:你现在的状况,你可能去哪里,以及最近你面临的挑战。或者是一般值得注意的经历。对我来说,它有双重目的:今天能够反思以及将思维过程归档以备未来参考。
无论如何,我喜欢这样做,多年来我完成了大约 15 本笔记本。对我来说,它们非常宝贵,因为它们记录了我的生活,涵盖了担忧和挑战,以及亮点和改变人生的时刻。我想象着在一两十年后翻阅这些笔记本,充满了怀旧之情。因此,我想保留它们!
目前它们散落在德国的两个不同地方,这增加了在意外火灾中丢失一半的可能性。当然,更可能的威胁是时间,我们的老敌人。光盘在 25 年后会停止工作,而只有上帝知道我的纸质笔记能存活多久。我曾经找到过一些我在学校时写的旧文字。它们已经相当模糊了。
我考虑过将这些内容数字化一段时间了。肯定有服务提供商!然而,由于内容极其个人化,我不想与第三方分享。
作为一名专业的数据科学家,我知道可以建立和训练我自己的图像识别和 OCR 模型的可能性。我理论上知道该怎么做。但我从未做过。
此外,我知道有很多模型已经在手写文字上预训练过,并且我可以针对自己的数据集进行调整。当然,在这种情况下,局部运行它们不会被视为第三方。然而,再次根据我自己的笔迹判断,我很确定目前没有任何系统“足够智能”到能理解我写的内容。
长话短说,这就是我的动力。我的目标很简单:通过使用 AI 数字化我多年的工作,同时获取构建 OCR 系统的实际经验。
我不想阅读太多关于其他人如何处理这个问题或“你应该做什么”的内容,因为这会妨碍我的学习体验。我预期会在某个时候失败,面临另一个需要解决的挑战。重复。
处理流程概述和使用的技术
现在我的动机已经明确,是时候考虑我们实际需要做什么了。简单来说,我期望有一个由三部分组成的流程:预处理、主要处理(CNN)和后处理。

作者提供的图像
预处理
我需要我的手写图像数据,并以一种提高系统速度和性能指标的方式进行处理。我可以测试和实验不同的方式,例如不同的图像大小或通道数。我将主要使用OpenCV 和 Numpy来预处理数据。第二部分将介绍LabelImg,这是我用来创建标注输入数据的开源工具。
主要处理
这是魔法发生的地方,在这里,输入图像以浮点数矩阵的形式被转换为一个预测属于某一类别的概率向量。简而言之,输出结果可能类似于“image_1 是‘Test’,概率为 90%,‘Toast’,概率为 10%”。
我将使用各种卷积神经网络(CNNs),这是进行图像识别的首选架构。如果你对 CNNs 不太熟悉,我将在本文后面简要描述它们的主要属性。我不会使用任何预训练网络,依据我之前的动机,并假设世界上没有人有像我一样难看的手写字。
不过,我不会完全从头开始。我将定义并使用TensorFlow和Keras来处理神经网络,以及它们所有可用的类和辅助函数。
后处理
此时我们将有一个训练好的 CNN,能够或多或少地识别我的手写字。然而,这一输出可能需要进一步清理和处理,使用各种 NLP 方法。
例子:系统可能预测一个词为“Tost”,但这个词在德语中并不存在。流程中的另一个模型可能基于相似性将其纠正为“Test”。整个后处理部分在本文及下篇文章中不会涉及,因为我仍然离知道如何实现这一点很远。
关于我期望构建的三部分系统已经讲了很多。由于卷积神经网络(CNNs)在第一部分和第二部分中占据核心地位,我将以一种非常非科学、务实的方式简要介绍卷积神经网络(CNNs)。这个总结主要取自于Aurélien Géron 的《动手机器学习:使用 Scikit-Learn、Keras 和 TensorFlow(第 2 版)》,这是我最喜欢的实用机器学习入门书籍。
卷积神经网络(CNNs)简短介绍
如果你大致了解什么是人工神经网络(ANNs)及其在分类任务中的工作原理,你将能够很容易地理解接下来的内容。你会在本节末尾找到关于 CNN 的简短资源列表。
CNN 建立在 1950 年代末期关于我们大脑如何处理视觉输入的研究基础上。这些研究导致了1981 年诺贝尔奖。研究人员的主要发现是:许多神经元只对视觉场中小区域的视觉刺激做出反应。这些领域相互重叠,拼凑成我们所见的整体。此外,两个神经元可能共享相同的感受野,但一个只对水平线做出反应,另一个则对垂直线做出反应。
根据这些发现,计算机科学家开始创建专门为图像识别任务设计的神经网络。1998 年,Yann LeCun(现为 Meta 的首席 AI 科学家)创建了用于识别手写字符的LeNet-5架构。
那么,CNN 与其他(深度)神经网络有什么不同?本质上,CNN 与标准 ANN 类似,其中高级别的神经元从低级别的神经元输出中获取输入,以检测数据中的各种模式,随着数据在网络中传播,从简单到复杂。然而,CNN 有两个特殊的构建块,称为卷积层和池化层。
这些构建块的动机很容易理解。以一个相对较小的图像 100px x 100px 为例。这意味着有 10,000 个输入(如果是彩色图像则为 30,000)。对于一个具有 1,000 个神经元的全连接标准 ANN,这将转化为 10,000,000 个连接需要适配。卷积层和池化层只允许部分连接这些层。此外,CNN 能够识别图像中出现的各种模式,而标准 ANN 则在处理偏移图像时会遇到困难。使用这些构建块有很多优点。现在了解更多关于它们的内容。
卷积层
根据上述研究人员的想法,卷积层中的神经元仅与其感受野中的像素相连(如果这是第一层),而不是与整个图像相连。在后续层中,神经元仅与小矩形中的输出相连。这是一个简单的想法,但仅用语言很难描述。下面的图像可能会有所帮助。

由 Aphex34 — 自作,CC BY-SA 4.0,commons.wikimedia.org/w/index.php?curid=45679374
这个感受野矩形现在滑动(称为步幅)在图像上,每次输出一个值,从而在下一层减少图像的大小。结果是过滤器,也称为卷积。

作者提供的图像
在训练过程中,CNN 将学习最有用的滤波器,如水平线与垂直线,这些可以被可视化。这些滤波器可能一开始非常简单,但随着数据在网络中进展,它们会被组合成越来越复杂的滤波器。应用于实际图像的滤波器被称为特征图(Feature Maps)(即滤波器和叠加图像的点积),突出显示激活滤波器的像素。

作者提供的图像
池化层
为了进一步减少计算负担,所谓的池化层用于将输入的信息聚合到一个减少的输出中,作为下一个(卷积)层的输入。这个过程也被称为子采样。像卷积层一样,池化层具有有限的感受野,并在输入图像上滑动。然而,步幅通常设置为使得感受野不重叠。
例如,如今常用的最大池化层(Max Pooling Layers)输出其视野内所有神经元的最大值,从而只保留最相关的像素信息。下图可视化了这一点。

由 Aphex34 创作,CC BY-SA 4.0,commons.wikimedia.org/w/index.php?curid=45673581
池化层可能非常具有破坏性,因为它们丢弃了 80-90%的信息。然而,它们也为数据添加了一些不变性,从而减少了对特定但不可泛化的细节的过拟合风险。
输出层
池化层通常跟随卷积层,这种模式重复几次,直到输出被展平(从矩阵到向量),以便得到与预期类别一样多的最终输出。例如,如果我们想要分类 0 到 9 的手写数字,我们会在最后使用一个具有 10 个输出的全连接层,以及一个 softmax 激活函数,如果我们想要预测图像属于十种可能性中的每一种的概率。
在输入“8”的图像之后的最终输出可能是向量[0.2,0,0,0,0,0,0.1,0,0.7,0],这意味着模型预测“8”的可能性最大,但也可能是“0”有 20%以及“6”有 10%的概率。根据训练图像,这些手写数字可能非常接近。
就这样,你所需要了解的全部内容就绪。CNNs 是一种用于图像识别的特殊人工神经网络。它们使用卷积层学习图像的简单和复杂模式,并使用池化层减少计算负担和过拟合的风险。
当然,CNNs(卷积神经网络)还有更多的内容,它们的输入数据的视觉性质使得它们在视觉上更容易理解。这里有一些进一步的基础资源,帮助你理解 CNNs 的工作原理。
维基百科:en.wikipedia.org/wiki/Convolutional_neural_network
Josh Starmer 的(幽默但 nonetheless 高度信息性的)QuestStats: www.youtube.com/watch?v=HGwBXDKFk9I
如果你仍然觉得不够,3Blue1Brown 的视频深入探讨了卷积是什么以及它们如何在各种应用中使用,例如图像处理(从 8:22 开始)。
开始使用我自己的手写文字
在概述了我们需要的技术及其理论工作方式后,现在是时候变得更实际,即开始编码了。你可以在 我的代码和文件这里 查看。
我开始时在纸上写了一封小信,拍了一张照片,并使用 OpenCV 在我的电脑上读取它。别在意它是德语的。电脑也不会在意(目前还不)。

图片由作者提供
先暂时忽略我的实际任务,我在寻找一种自动检测纸张和墨水的方法,即检测文本。我的目标是找到一种以自动化的方式裁剪出单词的方法,而暂时忽略每个单词的实际含义。
我使用了所谓的行文本分割,通过调整我在这个 GitHub 笔记本 中找到的代码。简单来说,这个过程旨在首先识别文本行,然后遍历这些行以识别单词。
它开始于对图像进行二值化,使每个像素可以是 0(黑色)或 1(白色)。类似于池化层的作用,下一步是膨胀,在这个步骤中,一个核(即接收域)在图像上滑动,将图像像素替换为该领域中的最大值,然后继续滑动。结果是图像的“生长”部分,因此命名为膨胀。
同样,与通过阅读理解相比,直接看到实际发生的情况更容易。左侧图像显示了我的信件在二值化后的样子,右侧则是经过膨胀处理后的样子。膨胀的效果类似于我们使用文本标记器时的效果。

图片由作者提供
从这一步到线检测只是一个小步骤。我将使用 OpenCV 的 findContours 函数。下方的蓝色框应该标识出我文本所写的行。正如你所见,这在某些部分效果更好。通过循环处理这些行以标记单个单词会生成带有黄色框的图像。

这看起来还不错,对吧?然而,它给了我 346 个识别出的单词(而实际上只有 58 个)。这种方法导致了重复,在我的图像中出现了相同的识别区域。即使在去除这些明显的重复之后,我仍然剩下 94 个单词。这些是单词的重叠部分,如下例所示。

图片由作者提供
现在我有了表示单词的信件片段,但大多数时候,我需要大量手动浏览样本以删除无意义的部分。此外,我还需要创建某种查找表,说明 image_00001.jpg 代表“ich”,而 image_00002.jpg 代表“Dokumente”。
总体而言,在效率方面并没有取得太大进展。我们可能更聪明了,但在实际应用方面尚未取得更大进展。
第一部分的总结与结论
我知道,我在这个系列的第一篇文章中以一个难题作为结尾。很抱歉!但通过将这个失败重新解读为挑战,我们将为更加乐观的第二部分奠定基础,我保证!
这段时间并没有浪费。我介绍了我们将用于此任务的核心技术——卷积神经网络(CNNs),它是如何工作的,以及这种网络架构与您之前可能了解的标准架构有何不同。了解你为何进入某个领域,而不仅仅是如何进入,是很重要的。因此,我概述了我的情况和动机。
目前的主要教训是: 如果我们想训练一个 CNN,我们需要正确标记的输入数据,不能依赖于自动生成这些数据的方法,例如使用稀释技术的行词分割方法在二值化输入图像上。
在第二部分中,我将开始训练 CNN 以识别上述信件中的单词。我会介绍 LabelImg,这是一款我用来标注的开源工具,以创建正确标记的我的手写训练数据集。
我们将看看这种非传统的 CNN 与 LeNet-5 和 VGGNet 等更著名的架构相比效果如何,以及通过调整数据预处理的参数,我们是否能提高准确性。
希望你读得愉快,并会继续关注。我已经可以说,第二部分也将以一个难题,即另一个挑战告终。好的一面是,将会有第三部分来(希望)解决它。
祝一切顺利,
乔纳斯
使用信号处理思想构建锻炼次数计数器
使用零交叉方法设计特定类别的计数器
·
关注 发表在 Towards Data Science ·7 分钟阅读·2023 年 1 月 17 日
--
照片由 Karsten Winegeart 在 Unsplash.com 提供
在这篇博客文章中,我讨论了一种非常新颖和独特的方法来构建实时锻炼次数计数器,这种方法在姿态估计的基础上应用了信号处理的思想。这种方法可以轻松地适应于构建其他类别的计数器。
从进行健身活动时计数某种运动的次数,到测量生物事件(如心跳和脉搏计数),重复计数有着无数的应用,近年来在这一领域的研究获得了大量关注。在这篇博客中,我讨论了使用信号处理领域的一些简单概念来构建一个快速而准确的重复计数器的方法。
我将首先迅速覆盖基础知识,然后介绍基于信号的重复计数公式。该方法的实现可以在这里找到。让我们从信号处理领域讨论一些可以用来构建重复计数器的概念开始。
零交叉
数学函数或波形穿越轴的交点的参考点(交点不一定是 0)。该术语通常用于电子学中,指的是周期性电压和电流中的没有信号的点。

图:零交叉。图片由作者提供。
峰值检测
在信号或波形中检测峰值(或位置),即观察到的突然偏离(你会看到尖峰)正常行为。检测这种偏差的技术是通过计算z-score,它捕捉信号的均值和标准差来计算偏差。

图:z-score 公式。图片由作者提供。
-
在信号的任何位置,z-score 算法本质上计算前一个数据点窗口的滞后平均值和滞后标准差。
-
通过计算范围 滞后平均值 +/- (阈值 * 滞后标准差); 来识别信号中的峰值;如果当前点的值超出了范围,则认为它是异常的一部分。
算法的更多细节和数学内容可以在这里找到。
重复计数 <> 信号处理
读者自然会产生一个问题,即零交叉和峰值检测算法如何用于重复计数。我们来看看:
假设
认为一个人/物体由一组关键点(关注点)组成。例如,这些关键点可以是肩膀、臀部等人体关节。
为了简化,我将问题限制为运动重复计数,因为人体关键点的可获得性较高(这一思路可以很容易地扩展到其他关键点易于获得的对象)。我们可以使用开源姿态估计模型来计算身体关键点的空间位置。我在这篇博客中使用了 Tensorflow 的Movenet姿态估计模型进行说明。这个模型相当快速和准确。
我们假设任何重复的运动,例如锻炼,都可以看作是关键点或函数(度量)在关键点上的一组正弦波形。这些度量包括不同身体关键点组合之间的角度和距离。
算法
基本思想是在实时移动的时间窗口中检测这些信号度量的零交叉点。
使用零交叉点进行重复计数是一个两阶段的过程:
阶段 1:参考计算
这个阶段是给定锻炼的一次性活动。我们首先使用参考视频找到一个零交叉线,也就是参考线(对于锻炼重复计数器,它可以是教练的视频)。大多数步骤将在重复计数阶段中使用。
a) 我们使用 Movenet 姿势估计模型实时观察人体关键点。请考虑以下参考:

图:使用 Movenet 模型进行身体关键点估计。教练正在做跳跃杰克。GIF 由作者提供。
b) 然后,我们使用不同身体关键点的组合计算度量。度量可以是关键点之间的距离或角度。一些度量示例:左肩到左掌的距离(欧几里得/y 轴)、左肩处的夹角等。
这个想法是使用能够覆盖广泛运动范围的度量。我通常更喜欢面对前置摄像头进行锻炼;因此,选择欧几里得距离和 y 轴距离度量即可。如果您希望为侧面锻炼建立重复计数器,您可能需要考虑 x 轴距离。我还通过肩膀到肩膀的距离来规范化度量,以便重复计数不会受到摄像头距离的影响。
c) 帧级姿势估计会导致身体关键点的抖动,从而导致计算度量的抖动。我们使用低通滤波器使度量平滑,并去除度量距离和角度中的抖动,这使得参考计算和重复计数更加准确。有关该技术的更多详细信息,请参见这里。确保在计算度量之前身体关键点在框架内。
d) 接下来,我们筛选出静止的度量信号。我们计算这些信号的标准差,并去除低于固定阈值的信号。如果没有度量被筛选出去,我们使用标准差最大的前 3 个度量。对于锻炼重复计数器,我们考虑总共18个度量。对于上述参考和标准差阈值为0.4的情况,我们最终得到8个对重复性贡献最大的度量。

图:度量波形。图片由作者提供。
e) 最后,我们将所有剩余的非平稳度量在时间上相加,并计算使用总和信号的均值作为参考线。我们将这些度量和参考线(均值)的 ID 保存到一个配置字典中,以便在重复计数时使用。教练视频的参考线:

图:参考视频的整体信号波形。图片来源:作者。
仔细查看参考视频可以看出,总共有 6 个重复。这些重复实际上对应于上面整体信号中观察到的峰值。
阶段 2:重复计数
这一阶段的大部分步骤与参考计算阶段是相同的。
a) 给定一个测试视频,我们首先实时计算关键点和归一化度量(与前一步相同)。
b) 我们使用参考计算阶段的配置字典来确定此练习所需的非平稳度量,然后将这些度量在时间上相加,创建一个综合的整体信号。
c) 我们实时创建一个固定大小的移动窗口,并检查它与参考线,即零交叉线的交集。对于任何重复的动作,通常有两种状态,一种是运动的上升状态,另一种是下降状态,任意一种都是正常状态。
因此,对于一个重复动作,整体信号波形与参考线有两个交点。第一个交点表明该人已经达到动作的上升状态,第二个交点则表明该人回到正常状态,这个重复动作完成了。
就这样!
结果
让我们查看这种方法在测试视频上的表现。

图:使用零交叉技术进行的重复计数。GIF 来源:作者。
结果看起来不错,对吧?😎。这是上述测试视频的整体信号波形。直观地看,四个峰值对应于四个重复。

图:测试视频的整体信号波形。图片来源:作者。
方法的优点
-
这个算法相当快速和准确。
-
这个想法是直观的,实现起来很简单。
-
这种方法在生产环境中易于集成。
方法的缺点
-
不是一个通用的重复计数器(但这个想法可以适用于其他类别)。
-
需要使用每个练习/类别的参考视频来计算零交叉线,因此难以扩展到大量的练习数据。
-
在背景噪声较大的地方,姿态估计可能效果不好,从而导致重复计数结果不佳。
-
对于那些整体信号由于不同度量最终相互平均而变得平坦的练习,这可能不起作用。
在这篇博客中,我强调了使用零交叉点来计数重复次数的想法;然而,另一种技术——峰值检测,我们在开始时简要讨论过,也可以用来实时检测重复次数。
有用的参考资料
[1]. 使用低通滤波器使姿态估计更有效
[2]. 鲁棒的峰值检测算法(使用 z-分数)
[4]. 该方法的实现可以在这里找到;请阅读库中的说明以获取使用方法。
希望你能理解如何使用信号处理技术来构建一个特定类别的重复计数器。我很想了解阅读这篇博客的人的反馈。我很乐意回答有关上述概念的任何疑问/问题。欢迎反馈。你可以通过 Linkedin 联系我。
谢谢!
利用 GPTs 构建物理信息神经网络领域的专家 GPT
一个定制的副驾驶,用于简化 PINN 研究和开发
·发表于Towards Data Science·14 分钟阅读·2023 年 11 月 18 日
--

定制 GPT 的标志。(由 DALL·E 3 生成)
在最近的 OpenAI DevDay 上,最令人兴奋的发布之一就是 GPTs。基本上,GPTs 是任何人都可以创建的用于特定目的的 ChatGPT 定制版本。配置一个可用的 GPT 的过程无需编程,只需通过聊天即可。因此,自发布以来,社区已经创建了各种 GPT,以帮助用户提高生产力,增添生活乐趣。
作为物理信息神经网络(PINN)领域的从业者,我经常使用 ChatGPT(GPT-4)来帮助我理解复杂的技术概念,调试在实现模型时遇到的问题,并提出新颖的研究想法或工程解决方案。尽管非常有用,我发现 ChatGPT 在提供超出其一般 PINN 知识的量身定制答案时常常显得力不从心。虽然我可以调整我的提示以纳入更多的上下文信息,但这是一项相当耗时的工作,有时会迅速消耗我的耐心。
现在可以轻松定制 ChatGPT,我想到一个想法:为什么不开发一个定制的 GPT,作为 PINN 专家🦸♀️,从我精心挑选的资源中汲取知识,并努力以量身定制的方式回答关于 PINN 的问题呢?
所以,在这篇博客文章中,让我们看看如何使这一目标成为现实!我们将首先介绍构建我们目标 GPT 的过程,提供指令设计和知识库的详细信息。然后,我们将进行一些演示,看看如何与新创建的 GPT 进行最佳互动。最后,我们将探讨未来发展的机会。
这个想法与你产生共鸣吗?让我们开始吧🗺️📍🚶♀️
这是我物理启发机器学习系列中的另一篇博客。其他包括:
物理启发神经网络:应用中心指南
揭开物理启发神经网络的设计模式
通过 PINN 和符号回归发现微分方程
通过物理启发的 DeepONet 进行操作学习
通过物理启发的 DeepONet 解决逆问题
随时查看它们!
内容目录
· 1. 如何构建?
∘ 知识
∘ 说明
· 2. 如何互动?
· 3. 如何扩展
· 4. 总结
1. 如何构建?
如前所述,构建定制化 GPT 非常简单:这只是将我们的想法传达给 GPT Builder 的问题。

GPT Builder 环境。
为了开发我们的目标 GPT,我们需要特别关注知识和说明部分。让我们看看如何正确设置它们。
知识
本节允许我们向 GPT 附加文件。稍后,当 GPT 响应用户的问题时,它可能会从上传的文件中检索相关信息,并基于这些检索到的信息来回答问题。实质上,这些附加的文件作为知识库为定制化 GPT 提供支持,并促进 RAG(检索增强生成)模式。
为了为 GPT 提供正确的知识,我将我关于PINN 设计模式的系列技术博客上传到了 GPT Builder。以前,我在 Towards Data Science 上定期发表文章,提炼出可重用的解决方案,这些方案经过测试,已被证明对 PINN 实现中的常见问题有效。每篇文章都详细解释了一种设计模式,包括具体问题、相应解决方案的技术性、基准、优缺点以及替代方案。可以在这里找到一篇示例博客。
我保持这一系列文章的初衷是创建一个结构化的地图,供 PINN 从业者依赖,以便识别最适合特定挑战的技术,并跟上最新进展。现在我可以进一步推进,将系列文章整合到 GPT 的知识库中,使用户可以通过聊天直接学习 PINN 中的最佳实践。
指令
一旦 GPT 的知识更新完成,我们需要专注于制定正确的指令。指令部分允许我们输入命令来控制自定义 GPT 的行为。如果你有使用 Open API 的经验,指令就像是 Chat Completions API 中的“SystemMessage”。
对于我们当前的任务,我们希望创建一个在 PINN 领域的专家 GPT,并能够有效地帮助用户应对 PINN 研究和开发中的复杂性。我们将这些想法解释给 GPT 构建者,系统将自动生成一份指令草稿。
制定正确的指令通常是一个迭代过程,需要多轮解释思想 ==> 更新指令 ==> 测试 GPT 行为 ==> 完善思想。经过一段时间的调整,我得到了以下 GPT:

生成的 GPT。(作者提供的图像)
指令(由 GPT 生成):
我是 PINN 设计模式专家,专注于物理信息神经网络,重点是系列技术文章中概述的设计模式,包括集成学习、主动学习、超参数调整、梯度增强学习、因果训练、协同点采样等主题。每篇文章讨论一个特定的设计模式,这些模式在研究论文中展示,涵盖以下方面:
来源:提出设计模式的原始研究论文。
问题:设计模式中的策略旨在解决的具体问题。
解决方案:策略的关键组成部分、其实施方式以及其有效性的理由。
基准:使用设计模式评估的偏微分方程及其相关性能。
优势与劣势:策略有效的条件及其局限性。
替代方案:其他类似问题的解决方法,提供对潜在解决方案的更广泛视角。
潜在的未来改进:关于推进设计模式的建议。
我利用这种结构化的方法来帮助用户在多个方面,包括分析设计模式、评估解决方案、指导研究和开发,并提供量身定制的建议和解释以支持您的工作。
我的目标是促进对物理信息神经网络的全面而细致的理解,帮助用户在这一动态发展领域中导航并取得成功。
在我的回应中,我将整合来自综合数据库的物理信息神经网络知识和见解,确保用户体验流畅自然,而不会明确引用信息来源。然而,当用户明确询问信息来源时,我会指出哪个原始研究论文提出了相关的设计模式或策略。这种方法保持了沟通的专业性和流畅性,专注于根据物理信息神经网络的专业知识提供准确和相关的上下文信息。
这是一个非常全面的指令,涵盖了角色规范、知识库的描述,以及如何在与用户互动时使用该知识库。此外,我还为 GPT 启用了网页浏览功能,以便在必要时可以在线查找信息。
这里有一个附带说明:在开发自己的 GPT 并将其发布到公众时,你可能需要考虑隐私和网络安全问题。例如,具有不当目的的用户可能利用提示注入攻击来揭示原始的 GPT 配置指令,这些指令你可能不希望公开。在这些情况下,请记得添加额外的指令以防范这些攻击。
好了,现在我们已经构建了所需的 GPT,我们可以保存更改并将其发布到公众(我会在本文末尾分享 GPT 链接)。是时候测试一下并看看如何与之互动了。
2. 如何互动?
凭借对 PINN 最佳实践的特定知识,以及在预训练期间获得的一般 PINN 知识,新创建的 GPT 可以在多种不同方式上成为用户的有用助手。接下来,让我们看看一些具体的例子。
- 提出具体的实施问题。
问:什么是 PINN 中的配点,我应该如何对其进行采样?
配点是 PINN 中的一个关键概念,以正确的方式进行采样对成功实现 PINN 至关重要。让我们看看定制的 GPT 如何回答这个问题:

定制 GPT 生成的回应。 (图片由作者提供)
GPT 首先基于其对 PINN 的一般知识解释了配点的定义,然后提供了几种具体的、先进的采样策略(例如,RAD、RAR-D)的详细描述。这可能会很有帮助,因为它为从业者提供了快速的指引,便于深入探讨。它还表明,定制的 GPT 不仅能够从知识库中提取相关信息,而且知道何时这样做。
现在,如果我们向标准 ChatGPT(GPT-4)提出相同的问题,回答将如下所示:

标准 ChatGPT(GPT-4)生成的回应。 (图片由作者提供)
我们可以看到标准的 ChatGPT 只能提供有关采样策略的一般信息。尽管它还建议将“自适应采样”作为潜在的策略之一,但其描述非常高层次,没有提供更多细节,例如如何调整采样,或如何测量网络性能等。此外,最后一个建议,即“结合数据与物理”,则相当无用,因为使用 PINN 的整个理念是将数据与物理结合。
- 请求编写原型代码
问:你能用 Python 编写一个原型代码来演示 RAD 算法的工作流程吗?
继续之前的对话,我们可以进一步要求定制的 GPT 生成一些实现其建议的特定算法的原型代码,例如 RAD 算法。以下是回复:

定制 GPT 生成的回复。(图像来源:作者)
不错!定制的 GPT 能够基于博客中的描述快速勾勒出一些原型代码。这对从业者了解算法的内部工作原理并实现快速原型设计,以查看该算法是否适合当前问题非常有用。
- 请求最佳实践总结
问:你能总结一下那些可以有效提高 PINN 训练稳定性的方法吗?
这是从业者可能感兴趣的另一个常见点,即寻求与他们工作相关的量身定制的总结或概述。以下是定制 GPT 的回复:

定制 GPT 生成的回复。(图像来源:作者)
我们可以看到定制的 GPT 能够生成广泛但具体的技术总结,从集成学习到超参数调优。回答与博客中所指出的内容非常一致,并反映了最先进的技术。同时,注意到博客系列还讨论了其他技术,如主动学习。然而,定制的 GPT 认为这些技术与“提高 PINN 训练的稳定性”并不十分相关,因此没有在回答中提及。总体而言,这是一个对从业者快速了解全貌的有用功能。
另一方面,如果我们问标准的 ChatGPT(GPT-4)相同的问题,得到的回答如下:

标准 ChatGPT(GPT-4)生成的回复。为了节省空间,回复被截断。(图像来源:作者)
这里仅显示了完整响应的一部分以节省空间,但标准 GPT 的回答主要围绕神经网络训练的一般最佳实践,例如正则化、归一化与缩放、学习率调度、迁移学习和预训练等。这进一步显示了定制 GPT 具备针对我们需求的知识的好处。
- 询问特定概念
问:因果训练在 PINN 训练中解决了什么问题?
对于新手从业者,GPT 的常见使用方式可能是更好地理解他们在研究论文中遇到的特定 PINN 概念。在这种情况下,我们要求 GPT 解释 因果训练 的含义,这是 2022 年引入到 PINN 的一个相当新的概念。以下是我们获得的响应:

定制 GPT 生成的响应。(作者提供的图片)
在我提供给 GPT 的博客系列中,有一篇深入探讨了 PINN 训练中的因果关系问题。本质上,定制 GPT 抓住了这一点,并很好地总结了方法的核心思想。
- 寻找原始研究论文
问:最初提出将因果训练引入 PINN 的论文是哪一篇?
如果某个具体技术对用户的问题确实很有趣,用户自然会想深入了解并阅读原始研究论文。因此,定制 GPT 的一个好特性是提供指向提出该技术的原始研究论文的指针。以下是获得的响应:

在这种情况下,定制 GPT 正确地指出了研究的来源。有趣的是,标准 GPT 则给出了完全错误的答案:

我在网上搜索过,没有找到由 Paris Perdikaris 撰写的《Causality-Inspired Neural Networks for Physics-Informed Machine Learning》这篇论文。这就是大型语言模型的幻觉!
- 调试实际实施中的问题
问:当我在训练物理信息神经网络时,我注意到尽管训练损失在减少,但神经网络对物理量的预测始终不准确,尤其是在早期时间步骤。这可能是什么原因?
帮助调试实际实施问题可能是定制 GPT 最受从业者欢迎的功能之一。在这里,我们向定制 GPT 提出一个实际问题,并评估它是否能揭示可能的根本原因。获得的响应如下:

定制 GPT 生成的响应片段。(作者提供的图片)
我们看到在众多可行的理由中,“时间因果关系的违反”排在首位。相反,如果我们向标准 GPT 提出同样的问题,我们的结果会有所不同:

标准 GPT 生成的响应片段。(图像来源于作者)
我们看到建议的根本原因相当普遍,也适用于其他类型的神经网络,这些网络不一定是以物理为基础的。
定制 GPT 列出“时间因果性违反”作为首要原因的原因有两个:
-
我的 PINN 系列博客中的每一篇都有一个部分专门描述特定技术旨在解决的问题。因此,该博客系列实际上是“症状”与“治疗”对的集合。
-
一篇博客深入探讨了 PINN 中的因果训练概念。配备了这一知识库的定制 GPT 将“症状”(问题)与相应的“治疗”(将因果关系纳入训练)相匹配。
通过利用 PINN 中的症状与治疗的定制知识,定制 GPT 更有可能揭示具体实际实施问题的根本原因。
- 激发新的研究想法
问:将主动学习应用于 PINN 的未来研究机会是什么?
最后,使用定制 GPT 的另一个用途是为新的研究方向和想法提供灵感。在这里,我们希望探索将主动学习技术与 PINN 相结合的可能性,我们的定制 GPT 提供了一个长长的潜在机会清单(为了节省一些空间,仅列出了机会标题):
========== * 以下列表由 GPT 生成 * ============
-
主动学习中的算法改进
-
高维问题的可扩展性
-
与其他机器学习技术的集成
-
实时与在线学习
-
不确定性量化
-
特定应用的主动学习策略
-
高效计算策略
-
逆问题中的主动学习
-
鲁棒性与泛化能力
-
与实验设计的集成
========== * 上述列表由 GPT 生成 * ============
对于其中一些方向,我已经看到最近的工作。对于其他方向,我不了解任何具体工作,它们可能非常有趣且富有成果!作为附注,GPT 提出的前两大方向绝对不是它的“原创”想法。它们实际上在我讨论主动学习应用于 PINN 的博客的“潜在未来改进”部分中明确提到。这表明定制 GPT 可以无缝地将其通用知识与检索到的特定信息结合起来,形成回答。
3. 如何扩展
我们已经讨论了专家 GPT 如何帮助从业者在 PINN 领域中导航。在这一部分,让我们换个角度,讨论一些未来可能的扩展方向。
-
更新知识库:PINN 是一个充满活力的研究领域,时不时会宣布新的进展。因此,为了使专家 GPT 保持“专家”地位,定期更新其知识库并告知其关于 PINN 的最新进展至关重要。
-
增加知识库的文档多样性:目前,知识库仅包含有关 PINN 最佳实践的文档。此外,我们可以添加其他类型的文档来丰富多样性。例如,添加 PINN 领域的综述论文可以使 GPT 为用户描绘更好的全貌;另外,添加面向应用的研究论文可以使 PINN 了解现实案例,从而更好地帮助用户完成任务。
-
针对特定 PINN 库的编码助手:进行 PINN 研究的软件正在迅速发展。因此,跟上库中的最新进展对于从业者来说变得越来越困难。一个有前途的提高生产力的方法是设计一个 GPT 作为代码副驾驶,软件文档作为知识库。通过这种方式,编码助手 GPT 可以通过自然语言交流为用户的问题提供量身定制的编码解决方案。
-
增强定制 GPT 与行动:我们尚未触及的定制 GPT 的另一项功能是所谓的“行动”。本质上,这些“行动”类似于“插件”,它们赋予 GPT 在 ChatGPT 之外采取行动的能力。一项近期的工作[1]探讨了利用 LLM 自动调用 API 来支持 PINN 软件以帮助用户完成任务的可能性。对于创建专家 GPT 的情况,我们也可以让专家 GPT 访问专业 PINN 软件的 API,以便它可以以自主的方式执行不同类型的分析。这可以大大降低从业者利用 PINN 的门槛,加速 PINN 技术在各种现实问题中的应用。
4. 总结
在本博客中,我们探讨了创建一个专注于 PINN 并可以帮助从业者日常工作的定制 GPT 的想法(使用 ChatGPT 最新的 GPT 功能)。我们解释了创建这个定制 GPT 的过程,并展示了它可能比标准 GPT 更有利的一些场景。这些场景包括:
-
询问具体实施问题。
-
请求编写原型代码
-
请求最佳实践总结
-
询问具体概念
-
寻找原始研究论文
-
调试实际实施中的问题
-
激发新的研究思路
最后,我们简要讨论了从用户体验角度进一步改进这个定制 GPT 的可能方式。
你可以在这里访问创建的 GPT:chat.openai.com/g/g-Z4YUXe3Bf-pinn-design-pattern-specialist
希望你觉得这个博客有用🤗如果你对学习更多关于 PINN 的内容感兴趣,请查看我之前的博客:
-
解开物理信息神经网络设计模式的奥秘
-
使用 PINN 和符号回归发现微分方程
-
通过物理信息 DeepONet 进行算子学习
-
通过物理信息 DeepONet 解决逆问题
或者,如果你想查看更多大型语言模型的应用:
-
开发用于研究论文消化的自主双聊机器人系统
-
当 AutoML 遇上大型语言模型
期待与你分享更多令人兴奋的项目。敬请关注!
参考文献
[1] Kumar 等人,《MyCrunchGPT: 基于 chatGPT 的科学机器学习框架》,arXiv,2023 年。
在 Panel 中构建交互式 ML 仪表板
原文:
towardsdatascience.com/building-an-interactive-ml-dashboard-in-panel-d3e344ea7126
·发表于 Towards Data Science ·阅读时长 6 分钟·2023 年 6 月 6 日
--
作者:Andrew Huang、Sophia Yang、Philipp Rudiger

图像分类应用演示。
HoloViz Panel 是一个多功能的 Python 库,能够让开发人员和数据科学家轻松构建交互式可视化。无论你是在进行机器学习项目、开发 web 应用,还是设计数据仪表板,Panel 提供了一套强大的工具和功能,以增强你的数据探索和展示能力。在这篇博客文章中,我们将深入探讨 HoloViz Panel 的激动人心的功能,探索它如何彻底改变你的数据可视化工作流程,并演示如何用大约 100 行代码制作这样的应用。
尝试应用并查看代码:
充分利用 ML/AI 的力量
ML/AI 已成为数据分析和决策过程中的重要组成部分。借助 Panel,你可以将 ML 模型和结果无缝集成到你的可视化中。在这篇博客文章中,我们将探讨如何使用 OpenAI CLIP 模型进行图像分类任务。
CLIP 在大量的图像-文本对数据集上进行预训练,使其能够理解图像和相应的文本描述,并适用于各种下游任务,如图像分类。
我们使用了两个与 ML 相关的函数来执行图像分类任务。第一个函数load_processor_model使我们能够从 Hugging Face 加载预训练的 CLIP 模型。第二个函数get_similarity_score计算图像与提供的类别标签列表之间的相似度。
@pn.cache
def load_processor_model(
processor_name: str, model_name: str
) -> Tuple[CLIPProcessor, CLIPModel]:
processor = CLIPProcessor.from_pretrained(processor_name)
model = CLIPModel.from_pretrained(model_name)
return processor, model
def get_similarity_scores(class_items: List[str], image: Image) -> List[float]:
processor, model = load_processor_model(
"openai/clip-vit-base-patch32", "openai/clip-vit-base-patch32"
)
inputs = processor(
text=class_items,
images=[image],
return_tensors="pt", # pytorch tensors
)
outputs = model(**inputs)
logits_per_image = outputs.logits_per_image
class_likelihoods = logits_per_image.softmax(dim=1).detach().numpy()
return class_likelihoods[0]py
为交互性绑定小部件
Panel 的一个关键优势是其将小部件绑定到函数的能力。这种功能为用户提供了一个直观的界面,以便操作底层数据并通过交互获得更深入的见解。
Python 函数
在我们的示例中,我们有一个process_input函数,它将从图像分类模型获得的相似度分数格式化为具有良好 UI 的 Panel 对象。实际函数利用了 async;如果你不熟悉 async,不用担心!我们将在后面的部分解释它,但请注意 async不是使用 Panel 的要求——Panel 只是支持它!
async def process_inputs(class_names: List[str], image_url: str):
"""
High level function that takes in the user inputs and returns the
classification results as panel objects.
"""
...
yield results
面板小部件
我们使用了两个小部件来与此函数进行交互。
-
image_url是一个 TextInput 小部件,允许输入任何字符串作为图像 URL。 -
class_names是另一个 TextInput 小部件,它接受模型分类的可能类名。
image_url = pn.widgets.TextInput(
name="Image URL to classify",
value=pn.bind(random_url, randomize_url),
)
class_names = pn.widgets.TextInput(
name="Comma separated class names",
placeholder="Enter possible class names, e.g. cat, dog",
value="cat, dog, parrot",
)
根据process_inputs函数签名,它接受两个参数:class_names和image_url。我们可以使用pn.bind将每个参数/关键字参数绑定到一个小部件,如下所示:
interactive_result = pn.panel(
pn.bind(process_inputs, image_url=image_url, class_names=class_names),
height=600,
)
-
第一个位置参数是函数名。
-
关键字参数在匹配函数签名后,组件的值会绑定到函数的关键字参数上。
为了澄清,如果小部件命名为image_url_input而不是image_url,则调用将是:
pn.bind(process_inputs, image_url=image_url_input, ...)
添加模板设计样式
应用程序和仪表板的美观在吸引观众方面发挥了关键作用。Panel 使你能够根据流行的设计(如 Material 或 Fast)为你的可视化添加样式,使你能够创建视觉上吸引人且专业的界面。
在这个示例中,我们使用了一个bootstrap模板,我们可以控制在多个区域(如title和main)显示的内容,并可以为各种组件指定尺寸和颜色:
pn.extension(design="bootstrap", sizing_mode="stretch_width")
我们还将Progress条设计设置为Material。
row_bar = pn.indicators.Progress(
...
design=pn.theme.Material,
)
注意,你也可以使用styles和stylesheets!
计算密集型任务的缓存
一些数据处理任务可能计算成本高,导致性能缓慢。Panel 提供了缓存机制,使你可以存储昂贵计算的结果,并在需要时重用它们,从而显著提高应用程序的响应速度。
在我们的示例中,我们使用 pn.cache 装饰器缓存了 load_processor_model 的输出。这意味着我们不需要多次下载和加载模型。这一步骤将使你的应用程序感觉更加响应迅速!
附加说明:为了进一步响应性,还可以参考 defer_loading 和 加载指示器。
@pn.cache
def load_processor_model(
processor_name: str, model_name: str
) -> Tuple[CLIPProcessor, CLIPModel]:
processor = CLIPProcessor.from_pretrained(processor_name)
model = CLIPModel.from_pretrained(model_name)
return processor, model
使用 JavaScript 实现功能桥接
虽然 Panel 提供了丰富的互动功能,但你可能偶尔需要通过 JavaScript 实现额外的功能。将 JavaScript 代码与 Panel 可视化集成非常简单,从而扩展其功能。通过弥合 Python 和 JavaScript 之间的差距,你可以创建更高级的可视化效果,添加超出 Panel 原生功能范围的互动元素。
在我们应用程序的底部,你可能会看到一组图标,代表 Panel 的社交媒体账户,包括 LinkedIn 和 Twitter。当你点击这些图标中的任何一个时,你会被自动重定向到相应的社交媒体个人资料。这个无缝的点击和重定向功能是通过 Panel 的 JavaScript 集成 js_on_click 方法实现的:
footer_row = pn.Row(pn.Spacer(), align="center")
for icon, url in ICON_URLS.items():
href_button = pn.widgets.Button(icon=icon, width=35, height=35)
href_button.js_on_click(code=f"window.open('{url}')")
footer_row.append(href_button)
footer_row.append(pn.Spacer())
理解同步与异步支持
异步编程因其高效处理并发任务的能力而受到欢迎。我们将讨论同步与异步执行的差异,并探索 Panel 对异步操作的支持。理解这些概念将使你能够在 Panel 中利用异步功能,提高应用程序的性能和响应性。
使用 async 使你的函数可以在单线程内进行协作多任务处理,并允许 IO 任务在后台进行。例如,当我们从互联网获取一张随机图片时,我们不知道需要等待多久,并且我们不希望在等待时停止程序。异步实现了并发执行,使我们在等待时可以执行其他任务,确保应用程序的响应性。记得添加相应的 await。
async def open_image_url(image_url: str) -> Image:
async with aiohttp.ClientSession() as session:
async with session.get(image_url) as resp:
return Image.open(io.BytesIO(await resp.read()))
如果你对异步不熟悉,也可以将其改写为同步!异步不是使用 Panel 的必需条件!
def open_image_url(image_url: str) -> Image:
with requests.get(image_url) as resp:
return Image.open(io.BytesIO(resp.read()))
其他尝试的想法
在这里,我们仅探讨了一个想法;你可以尝试更多:
-
互动文本生成:利用 Hugging Face 强大的语言模型,如 GPT 或 Transformer,生成互动文本。结合 Panel 的小部件绑定功能与 Hugging Face 模型,创建动态界面,让用户输入提示或调整参数以生成自定义文本输出。
-
情感分析和文本分类:使用 Hugging Face 的预训练情感分析或文本分类模型构建互动仪表盘。使用 Panel,用户可以输入文本样本,展示预测的情感或类别概率,并通过互动可视化探索模型预测。
-
语言翻译:利用 Hugging Face 的翻译模型创建互动语言翻译界面。使用 Panel,用户可以输入一种语言的文本并可视化翻译后的输出,方便实验和探索翻译质量。
-
命名实体识别(NER):将 Hugging Face 的 NER 模型与 Panel 结合,构建互动 NER 可视化。用户可以输入文本并可视化识别的实体,突出实体范围,并通过直观界面探索模型预测。
-
聊天机器人和对话 AI:使用 Hugging Face 的对话模型,你可以创建互动聊天机器人或对话代理。Panel 使用户能够与聊天机器人进行互动对话,展示响应,并通过互动小部件自定义聊天机器人的行为。
-
模型微调和评估:使用 Panel 创建互动界面,以微调和评估 Hugging Face 模型。用户可以输入自定义训练数据,调整超参数,展示训练进展,并通过互动可视化评估模型性能。
-
模型比较和基准测试:使用 Panel 构建互动界面,以比较和基准测试不同 Hugging Face 模型在特定 NLP 任务中的表现。用户可以输入样本数据,比较模型预测,展示性能指标,并探索不同模型之间的权衡。
查看我们的应用程序画廊以获取其他创意!祝实验愉快!
加入我们的社区
Panel 社区充满活力和支持,经验丰富的开发者和数据科学家乐于帮助和分享他们的知识。加入我们并与我们联系:
最初发布于 https://blog.holoviz.org。
作者 Andrew Huang、Sophia Yang、Philipp Rudiger
Sophia Yang 是高级数据科学家。通过 LinkedIn、Twitter 和 YouTube 与我联系,并加入 DS/ML 书籍俱乐部 ❤️
建立分析成熟的组织(AMO)
一些简单的框架可以帮助确定您组织的分析需求以及如何使其更具数据驱动性
·
关注 发表在 Towards Data Science ·13 分钟阅读·2023 年 10 月 30 日
--
了解您组织的分析成熟度可以为您作为数据专业人士提供强大的优势。它将使您的“非分析”决策更为明智(从“项目优先级”到“如何展示您的发现”),并帮助您制定长期目标。这确实是一种优势——并非所有数据专业人士都愿意退一步设计长期目标(更少的人能实现这些长期目标)。
本文分为 3 部分:
-
第一部分:理解分析成熟度
-
第二部分:移动阶段
-
第三部分:“成熟”组织是什么
让我们深入探讨!
了解分析成熟度
任何组织(团队、产品、公司等)在某个时间点都会处于特定的分析成熟度阶段。就像人类“爬行、走路然后跑步”一样,组织也经历相同的阶段。这是自然界的定律之一:一切都经历创作、发展和成熟。
有一些有趣的框架¹与分析成熟度相关,具有不同的组件和重点。从个人经验来看,我发现通过以下 4 个组件来看待一个组织是最有用和可操作的:
-
其需求:Robert D. Peng 和 Elizabeth Matsui 在“数据的艺术”中写道,主要有 6 种类型的问题:描述性问题、探索性问题、推论性问题、预测性问题、因果性问题和机制性问题。你被问到的问题类型通常是组织成熟度水平的一个很好的指标——一个低成熟度的组织通常对描述性和探索性数据研究感兴趣,而一个高级成熟度的组织会提出更多预测性和因果性问题。
-
其人员:另一个分析成熟度的关键组件是人员,包括能力和资源。取决于组织拥有多少数据资源以及他们的能力有多强。
-
其工具和流程:是否有标准化的工具供数据专业人员使用?我们是否有标准化的流程(例如,优先级、模板等)供数据团队使用?
-
其文化:在决策过程中直觉与数据的分配比例如何?
根据你所在组织在这些组件上的评分,它将落入这三个阶段之一:
-
启动阶段: 在这个阶段,组织需要基本的报告来了解已经发生了什么(“事后诸葛亮”)。没有中央数据团队,可能甚至没有数据分析师——数据研究由一些数据敏锐的操作人员在他们的 9 到 5 工作之外完成。也没有工具,没有流程,没有关于查看特定现象时应使用什么视角的明确一致意见。这导致了很多噪音(例如,不同团队有不同的流失定义,导致后续意见不一致)。在文化方面,尽管每个人都同意数据应该指导决策过程,但由于数据不足(或对数据的不信任),很多决策都是通过“有依据的直觉”来做出的。
-
发展阶段: 组织对市场及其应该跟踪的一些关键指标有了良好的可见性。现在,它需要理解为什么事情以某种方式发展(“洞察”)。团队开始得到数据专业人士的支持(无论是嵌入在团队内部还是在集中式数据团队中)。数据基础设施正从 Google Spreadsheets 缓慢转变为更为强大的工具。为了分类和优先处理所有的数据请求,首批数据专业人士建立了基本的优先级原则和票务系统(即 Google Form)。各团队开始采用共同的视角,因此数据在决策中越来越被依赖。非数据专业人士在数据问题上变得更聪明,通过工具,非数据专业人士可以自己查看数据。
-
成熟阶段: 组织理解了为什么事情以某种方式发展,并现在可以预测和影响未来的变化(“前瞻性”)。集中式数据团队开始形成,充当主动的思想合作伙伴(与之前阶段的“被动支持”相比)。工具、流程和指标正在标准化。数据在每个决策过程中都被期待。

了解分析成熟度(图片由作者提供)
上面的图像是对现实生活的简化。实际上,组织在每个组件上的评分可能非常不同——但你可以大致理解其要点。这个框架的美妙之处在于:
-
它为你提供了一种结构化的方式来发现阻碍组织分析成长的关键因素。
-
它可以帮助你精准定位组织在其旅程中的位置——以及下一步是什么。
这正是你在知道如何使用这个框架时获得强大优势的原因:它提供了一种方式来理解你当前的位置和你可能达到的目标,并诊断为何尚未达到目标。你的任务则是“仅仅”制定一个策略来去除障碍——这正是我们在接下来要探讨的内容。
阶段推进
理查德·鲁梅特在《好战略,坏战略》中写道:“战略工作的核心始终是相同的:发现情况中的关键因素,并设计协调和聚焦行动的方法以应对这些因素”。
当你想提升组织的分析成熟度时,这同样适用:你需要找出帮助你迈向下一步的关键因素,并设计一个计划来实现目标。我们上面看到的框架——将分析成熟度分解为 4 个组件:组织的需求、数据资源、流程与工具以及数据文化——可以帮助你找出组织中的差距——但找出差距只是工作中的 20%。让我们讨论剩下的 80%。
好战略,坏战略框架
我喜欢理查德·鲁梅尔特的书,我认为它提供了一个出色的框架来思考这个问题。他解释说,一个好的战略有三个要素:
-
诊断:框架中最重要的部分是诊断——它是你整个逻辑方法的基础。你的诊断应当使你能够了解当前的情况,同时也能了解组织存在的原因和“为什么”。
-
一些指导原则:从这些诊断中,你可以得出一些指导原则——一旦你开始提升分析成熟度,这些原则将帮助你使决策过程更简单,并帮助你在时间上保持正确的轨道。
-
遵循上述的连贯行动计划:凭借你的诊断和指导原则,你的主要任务是决定你希望在什么时间范围内达到什么目标,以及你将如何实现这一目标。

良好战略与差劲战略框架(图源:作者)
从诊断开始
“表述清楚的问题已解决一半”——约翰·杜威
思路是理解当前情况及其背后的真正“原因”。你不想解决症状——你的目标是深入根本原因,并修复需要修复的地方。
这里有一些进行良好诊断的技巧:
-
从我们之前看到的 4 个维度出发:需求/人员/工具与流程/文化,使用这些视角评估你的组织,并找出每个领域的根本原因。
-
获取当前痛点和解决方案的数据:
-
采访人员:了解人们,他们的工作,他们的决策过程,以及他们如何在日常工作中使用数据。
-
影子观察:类似地,影子观察可以是深入了解他们日常工作的好方法,并且可以发现仅通过采访无法获得的见解。
-
发送调查问卷:根据你的组织规模,发送调查问卷可以帮助你获取更多的定量数据。附加好处:它还可以让你开始跟踪组织对“分析”的感受,并给你一个可以后续报告的基准。
-
做“文献回顾”,既包括内部(回顾之前的工作,了解人们如何尝试解决之前的问题,成功与否,以及原因),也包括外部(网上有很多免费的内容,你考虑的问题很可能已经被记录和讨论过(无论是在 HBR 上的一篇好文章,还是在一个晦涩的分析爱好者论坛上)。获取他人对不同问题解决方案的看法总是非常有帮助的)。
-
练习 5 个“为什么”:每次你发现新的见解时,问自己为什么。你需要从整体的角度来看待问题,理解组织所处状况的关键原因。请注意,这不是一件容易的任务,尤其是当你在公司待了很长时间,并且习惯了现状时。
推导指导政策
“每个人都有计划,直到他们被打了一拳” —— 迈克·泰森
诊断将揭示一些模式,这些模式应该能让你推导出指导原则。这些指导原则在几种不同的情况下会很有用:
-
在定义你的行动计划时:将这些视为高速公路上的“护栏”:它们将帮助你始终保持正轨,确保你诊断的问题能够得到解决
-
当面临你未预料到的情况时:你可以利用不同的原则来促进和指导你的决策——这将为你带来无比的安心
-
在进行权衡或对利益相关者说“不”时:说“不”总是很复杂——但这是良好战略的核心。通过明确你的原则并让利益相关者同意它们,拒绝他们的要求会更容易被他们接受。
指导原则中最难的部分是坚持这些原则——就像生活中一样。
制定行动计划
这个行动计划需要一致性和连贯性,并覆盖分析成熟度的不同组件。
如何制定行动计划:
-
找到你所支持组织中的主题专家,并与他们一起制定计划:
-
向他们介绍你的诊断和指导原则,并与他们一起头脑风暴下一步该做什么以及时间框架。
-
如果你在一个快速发展的组织中,考虑优化选择性——为你提供时间来推动组织的成熟度,同时也能应对“火警演习”或时间敏感的问题
-
超越你的组织思考:如果你支持的是更大公司的某一部分,还需要考虑如何与你的其他分析职能进行互动,并将这些添加到你的计划中
-
设置成功标准:每当有定性工作进行时,不要忘记设置成功标准。就像其他工作一样,你应该能够在完成后判断这是否成功。因此,设置一个二元成功标准,以便告诉你完成得如何。认真思考一下——确保标准能够准确代表你要解决的问题。
-
设置报告流程和时间表:完成工作很重要,但如果没有人知道或使用你所建立的内容,你是否真的在创造价值?设置一个适当的报告流程将使你能够一次实现多个目标:
-
让你的工作获得更大受众的可见性,并促进合作机会
-
为你的新分析产品提供市场推广策略(因为你有一个展示新仪表板和报告的场所)
-
确保领导层的支持:没有领导的支持,你无法围绕数据建立文化。向他们展示计划,并获得他们的支持,以确保顺利实现你的目标
成功的公式
FS 新闻通讯 之前分享了一个小小的想法:
“成功的秘诀:
-
勇于开始。
-
专注的纪律。
-
有解决问题的信心。
-
有耐心去了解进展并不总是显而易见。
-
即使在糟糕的日子里,也要有坚持下去的毅力。
最终——这就是全部的核心。你需要有勇气开始讨论你组织的分析成熟度及其应该达到的水平,具备制定行动计划的纪律(同时处理即时的紧急问题),有信心找到合适的解决方案,尽管可能会有反对意见,还需要有耐心和坚持不懈地向前推进。
希望你能达到最终目标:建立一个分析成熟的组织。
最终目标:分析成熟组织(AMO)。
我一直在谈论 AMO(分析成熟组织),我们也见证了如何发展它——但我从未具体说明什么是分析成熟的组织,以及它为何如此出色。所以这是第三部分——提供了分析成熟组织与众不同的具体例子!
一个 AMO 是一个了解其市场复杂动态的组织,以及哪些活动可以影响它的组织。
分析成熟的组织清楚地了解其活动(“输入指标”)如何驱动短期结果(“输出指标”),而这些短期结果又进一步驱动长期成果(“成果指标”)。
- 例如:一个分析成熟的市场营销组织将知道发送促销邮件(输入:发送邮件的数量)对推动新注册用户(输出:注册用户的数量)的影响,以及这些注册用户在未来转化为付费用户(成果:付费用户数量)的程度。他们会使用不同的比率(注册 vs 发送)并对其不同的活动进行基准测试,帮助他们提高技能。
成熟的组织还将对影响其主要指标的关键因素有清晰的理解。他们可以无缝地进行根本原因分析,以了解这些主要指标的演变,并采取纠正措施。
- 例如:一个销售组织将能够根据可能存在的困难或有利机会,确定优先考虑哪些渠道和客户群体。他们已经完善了调查过程——到达能够自动化它的程度,此时,一个算法可以直接将正确的洞察信息呈现给正确的人。
数据需求已经转向更多的“复杂”问题——例如机会规模、因果影响跟踪等。更困难的问题——需要深入的领域专业知识以及先进的统计方法。
- 例如:一个分析成熟的人力资源组织会希望开始研究如何驱动员工留存和/或成功——为此,它将开始进行因果影响分析,以提取预测成功的关键因素。
一个分析成熟的组织是一个少数专业数据团队协作的组织。
-
一个分析成熟组织的整体框架依赖于干净的数据——这就是为什么在一个分析成熟的组织中,你有数据工程师正在创建管道、数据集和数据库,并且承诺遵守非常严格的规则和“服务级别协议”(SLA),以便下游团队(如数据科学或商业智能)可以轻松使用这些数据。
-
你还有产品经理,与数据工程师一起工作,确保构建正确的数据库来解决组织最紧迫的痛点,并建立工具以改善数据可发现性(即使在一个非常成熟的组织中,这始终是一个复杂的话题)。
-
你有数据科学家,他们消耗所有这些数据并将其转化为更深入的见解,以供产品和业务用户使用——使组织能够做出更好的决策。他们通常是一个相当核心的团队,其工作会影响上游和下游团队的路线图(即,他们的需求将影响数据工程团队的路线图,而他们的发现通常会影响其他分析团队的工作)。
-
最后,你有业务/数据/财务分析师,他们支持战略决策和日常运营。
举一个大型零售商的具体例子:
-
数据工程师将构建合适的管道,以确保我们拥有每日数据库,其中包括商店名称、位置、库存、每件商品的销售数量等。
-
数据科学家将使用这些数据库进行“市场篮子分析”——以揭示哪些商品最常一起购买。
-
业务分析师将这些发现转化为如何在不同的商店中实施的计划。他们将建立指标来跟踪“实施情况”(并可能为不同商店设定 OKR)。
一个分析成熟的组织是一个拥有强大工具和标准化流程的组织——这使得不同团队能够更快、更高质量地得出洞察。
-
在一个 AMO 中,已经实施了强大的数据治理流程,使人们更容易使用数据。分析师不必花费数小时逐一检查每个数据源——他们可以信任几个经过认证的数据库和指标,这大大节省了他们宝贵的时间。
-
已经建立(或实施)了多个工具来标准化典型的数据研究——这减少了个人贡献者出错的可能性,并使更多人能够获得所需的洞察。
-
例如:你不再需要为 A/B 测试进行统计测试,而是有一个工具,你只需输入数据,它会自动为你完成这些操作。
-
同样,从项目管理的角度来看——研究的常规“步骤”已经被映射、正式化和标准化(从优先级决策过程到研究的内部市场推广)。得益于这些正式化的流程,组织更容易理解谁在做什么,如何与不同的数据团队进行协作。
最终,一个分析成熟的组织是一个每个人都具备数据敏锐性的组织。
-
知识管理已经成为优先事项(而不仅仅是事后的思考),因此人们发现很容易找到资源和支持来回答他们的数据请求。
-
还有一些令人鼓舞的、经验丰富的“数据领袖”已经开始组织一个内部的“数据爱好者”生态系统(更多内容请参见下一篇文章!)
-
内部培训是可用的,并且在提升人们的技能——无论他们在数据旅程中的哪个阶段。
-
数据论坛是“酷”的地方——这里是精彩对话和重大决策的场所。数据团队被视为“思想伙伴”,在关键决策时会被召集到讨论桌前。每一个决策都是以数据为基础,甚至是由数据驱动的。
总结来说,你有一个运转良好的系统。一切都设定得如此,以便数据团队可以专注于生成高质量的洞见,数据使用的门槛也被降低,使得有兴趣的个人可以开始提取洞见并改善他们的日常工作。这是一种乌托邦。
本文已转载至 Analytics Explained,这是一个我总结了在各种分析角色中所学的知识(从新加坡的初创公司到旧金山的大型科技公司),并回答读者关于分析、增长和职业的问题的通讯。
¹: 分析成熟度模型概述 由 Karol Król 和 Dariusz Zdonek 撰写。
利用维度建模构建更好的数据仓库:数据工程师指南
数据仓库维度建模设计 101
·发表于Towards Data Science ·阅读时间 9 分钟·2023 年 5 月 8 日
--

图片由Erin Doering提供,来源于Unsplash
关于数据密集型应用的系统设计,通常有两个选项:优化写入或优化读取。
没有一种数据库设计既适合于写入也优化读取。就像所有系统设计视角一样,没有绝对对错的解决方案,只有优缺点。作为从事数据模型设计的数据专业人士,角色中的一个关键部分是识别用例,并进一步确定应应用哪种设计原则。
数据仓库历史上一直作为向最终用户提供数据的层,它是将数据转化为洞察的最后一公里。Ralph Kimball 开发了一种著名的建模设计技术,称为维度建模。他的《数据仓库工具包:维度建模的权威指南,第 3 版》是维度建模的最关键书籍。
尽管大数据和云计算技术解放了我们,使用更多的计算能力和更便宜的存储,但新手或经验丰富的数据工程师却忽视了数据仓库建模设计。越来越少的人关注慢变化维度(SCD)、代理键、表粒度等概念。
数据模型设计对于建立任何数据仓库系统的基础至关重要。我希望引起社区对以下内容的关注——《通过维度建模构建更好的数据仓库:数据工程师指南》。
为什么你应该关心 OLTP 与 OLAP
如果你对数据库有所了解,两个缩写词应该不会陌生:OLTP 和 OLAP。
OLTP 代表在线事务处理,以规范化的设计理念而闻名。在早期,存储是昂贵的。规范化的核心思想是减少重复并提高写入效率。
例如,我们可以有一个二维电子表格表。我们有产品及其子类型。爱丽丝和约翰都以 $1.99 购买了 Honeycrisp 苹果。对于不同的客户,相同属性的相同苹果重复出现——我们有重复。
当我们需要执行更新/插入/删除操作时,情况会变得更糟。例如,我们需要添加苹果来源于哪个农场,因此我们必须返回到这个表中并重复录入数千次。

OLTP 规范化以去除重复 | 图片来源:作者
为了解决这种重复并提高效率,我们可以通过规范化来实现——创建一个单独的产品表,并将所有相关字段移动到该表中。在原始表中,仅保留一个标识符以引用新创建的表中的行。
这节省了存储空间,并减少了在需要执行更新/插入/删除操作时接触的行数。
那么 OLTP 方面的难点是什么?它不适用于分析模式查询。
分析模式查询专注于使用选择/过滤/聚合在读取数据时的查询效率。 对这种查询模式缺乏支持导致了 OLAP。
OLAP 代表在线分析处理,以非规范化的设计理念而闻名。非规范化的核心思想是减少复杂的业务逻辑和大型表连接(后面我们称之为事实到事实表)在运行时的需求。
数据的准备和编写通常发生在 ETL 过程的前期,这通常需要更长的时间来执行和将业务逻辑预计算到最终表中。
回到我们的苹果数据示例,我们之前共享的电子表格是分析设计的一个优秀例子。我们避免了电子表格之间的联接,并且可以直接进行选择/过滤。
大多数时候,数据工程的目标是为终端用户提供 OLAP 数据仓库。与尽量避免重复的 OLTP 不同,OLAP 的设计原则是——冗余(反规范化)并不坏。我们牺牲了存储成本,但在运行时获得了更快的查询速度。
成功维度模型设计的 4 个步骤
数据仓库工具包:维度建模的权威指南,第 3 版 是维度建模最关键的书籍。该书涵盖了成功构建数据仓库的四个关键步骤:
1. 选择业务流程
在我的数据工程职业生涯中,我学到的最重要的事情就是找到并坚持业务用例。你可以拥有所有高端工具、毫秒级延迟的实时处理和强大的分布式计算查询引擎。但没有用例就像空中楼阁。
无论你的设计多么周到,如果你也是终端用户(通常不太可能对于数据工程来说),咨询终端用户始终是明智的第一步。我们可以采访终端用户,了解他们如何利用领域知识解析和理解数据,他们当前的报告是如何生成的,以及了解他们在过程中遇到的痛点。
Kimball 的企业数据仓库总线架构也是一种优秀的技术,用于桥接业务使用流程和标准维度。目标是评估在事实表中共享的大小。利用总线矩阵技术可以简化后续阶段的维度设计决策,并帮助你可视化整体结构。

数据仓库总线矩阵 | 图片由作者提供
2. 声明粒度
粒度是什么?它指的是表中保留的信息的最低级别。
我们如何知道需要什么粒度?我们需要与用户咨询以做出这样的决定。通常,提供的信息越详细,表格中的灵活性就越高。仅有聚合数据是不可能获得详细的行级数据的。例如,我们可以从每日聚合中获得每周销售数据,但很难将每周销售数据轻松拆解为每日数据。
聚合视图在查询时更快,但需要更多细节。另一方面,更低的粒度可以在后期聚合以提供相同的见解,但在运行时可能需要额外的时间。与终端用户协商并帮助他们理解利弊和潜在的未来需求是至关重要的。
我建议将粒度设定得尽可能低,因为更详细的数据可以在后续聚合,但反之则不可。如果需要更高粒度的事实表,可以使用现有的事实表来构建。
3. 确定维度
一旦确定了粒度,接下来应该确定维度。维度是事实表的基石。
维度用于使事实表对业务更具意义。通常,维度是名词,如日期、类别、产品等。它们的作用是切片和切块。最终用户可以使用维度来关注特定数据区域或汇总多个字段以观察潜在模式。例如,在我们之前的例子中,前 3 种苹果是什么?
维度表通常包含的数据比事实表少得多。如果你熟悉分布式计算或曾在 Spark 物理计划中工作过,那么维度表通常适合广播连接,这意味着这个数据集足够小,可以发送到每个节点以提高连接效率。
缓慢变化维度(SCD)是核心维度设计概念。虽然有超过六种 SCD 类型,但最关键的是 SCD 类型 2,因为它在行业中的广泛应用。
如果维度值中的一行已被修改,如何在你的维度表设计中跟踪这种变化?
这就是 SCD 类型 2 的魅力。我们会添加另一行,使用相同的业务键。新添加的行包括未更改的字段和修改过的字段。为了知道哪一行是当前的,哪一行是过去的,我们会为特定行的有效期添加开始和结束日期时间。例如,如果供应商从 CA 迁移到 IL,我们仍然可以在缓慢变化维度中追踪。

缓慢变化维度(SCD)类型 2 | 作者图片
Suppier_Code — ABC 通常是从 OLTP 系统提供的业务键。为了保持一个主键,以便以后与事实表连接,我们需要创建一个代理键 — Suppkier_Key——在这个维度中是唯一的。
4. 确定事实
事实表是维度建模的“灵魂”。事实表保留了核心业务数据。大多数事实表的行仅为数字,例如总销售量、交易费用和利润。
事实表有多种类型。你可以构建基于事务的事实表或快照事实表,如每月销售数据。
事实表的核心思想是跟踪给定时期的数据变化。例如,假设 Alice 购买了 Honeycrisp 苹果。然后第二天,她因为发现苹果里有虫子而退货。
-
如果我们采用事务基础视图,我们将记录两行。第一行是$2.99 的利润,第二行是-$2.99(假设没有额外操作成本),因为她退货了。
-
如果我们对每月的基本视图进行快照,Alice 的购买不会影响业务,因为 Alice 的净利润为 0。
事实表是金字塔的顶端。它需要所有基础工作准备好。因此,在开始设计事实表之前,拥有一个坚实的基础是至关重要的。
我应该选择星型模式还是雪花模式?
星型模式和雪花模式的选择对于熟悉 OLTP 设计并试图进行一定程度规范化的数据仓库来说可能更直接。
在大多数情况下,星型模式应该是首选。原因有几个:
-
我们希望避免额外的联接,这会降低查询性能。
-
维度之间的关系越多,管理和维护数据完整性就越复杂。
-
最终用户通常对数据模式不够熟悉,数据仓库的设计应该简单且用户友好。
-
存储额外数据的额外成本是微乎其微的。
我们考虑雪花模式的一个例外情况是为了节省成本,或者如果维度本身频繁更新,需要添加规范化以减少需要处理的字段数量。然而,首要目标是减少联接,以减少最终用户的查询时间。
为什么不将所有内容保存在单个表中?
另一种选择是将所有字段存储在一个表中,而不是分开存储事实表和维度表。
我们可以选择类似电子表格的单表,并且通过像Parquet这样的列式存储,查询可能变得更快。
需要考虑的关键标准是——维度表也会在多个事实表中共享。这样可以更容易地管理,因为维度在多个地方被引用。
关于联接呢?维度表和事实表的联接是否缓慢?我们之前提到过“维度通常比事实表包含的数据少得多。”对于分布式计算,最有效的方法是将较小的数据集发送到每个节点,以避免大量数据的传输。在这种情况下,事实表的联接通常是最小的,由工程师评估查询性能和数据管理工作之间的权衡。
最后的思考
维度建模及其原则在行业中已经存在超过 40 年。它已被证明是一种典型的 OLAP 用例设计模式。尽管大数据时代带来了比以往更强大的分布式计算引擎,但 OLAP 的基本设计原则不应被忽视。正确设计数据项目始终至关重要,而不是盲目投入资源。
我希望这篇文章能够引起更多数据工程师的关注,促使他们在追逐新工具之前重新考虑数据仓库设计。
Building Better ML Systems — Chapter 1: Every Project Must Start with a Plan
关于机器学习项目生命周期、设计文档、商业价值和需求。关于从小处开始和快速失败。
·
关注 发布于 Towards Data Science ·9 min read·Apr 20, 2023
--
图片由 charlesdeluvio 提供,来源于 Unsplash
很多数据科学家和机器学习工程师在大学毕业后,对他们的日常工作有一种错误的认知——他们期望其工作与他们的学习相似:
尝试最新的前沿算法在固定的相对干净的数据集上,并选择在准确性方面表现最好的算法(期望)。
你不需要:
-
思考商业价值和永无止境的需求列表。
-
(最有可能) 收集、标记和清理数据集。在某些情况下,训练/验证/测试划分已经为你完成了。
-
彻底评估你的模型,检查偏见,并进行 A/B 测试。
-
将模型部署到成千上万(或百万)用户,并确保它 99.9%的时间保持运行。
-
监控模型,捕捉任何准确率下降,并在需要时重新训练模型。
-
在部署上一个版本后立即收集新数据,并开始着手开发一个新的、更好的模型。
是的,你不需要在研究/学习项目期间考虑这些。但在实际项目中,这变得至关重要。
研究和实际项目之间的主要区别是:
在现实生活中,许多用户以各种可想象和不可想象的方式使用你的模型,并期望它始终快速、准确且公平地工作,没有偏见。用户的行为不断变化,可能会发生疫情和战争,而你的公司则试图通过提供用户想要的东西来赚取利润,并通过以其他人从未尝试或成功过的方式应用机器学习来建立竞争优势(现实)。
在本系列中,你将了解到,构建更好的机器学习系统需要将其视为一个系统——对每个组件及其关系给予足够的关注。
本教程将对数据科学家、机器学习工程师、团队和技术负责人(或那些希望成为技术负责人的人)有所帮助。虽然本系列不会全面覆盖所有内容,但它将帮助你打下机器学习系统设计的坚实基础,弥补任何不足,并让你探索不太熟悉的话题。在过程中,我会提供许多优秀文章、论文和书籍的链接。
事不宜迟,让我们开始吧!
每个项目都必须以计划开始。
下面是机器学习项目生命周期。让自己放松。首先,你需要理解任务并确定需要做什么。接着,你收集、标记和清理数据。然后,你进入建模阶段。之后,你评估模型并选择最佳模型。最后,你部署模型并监控其性能。

图 1. 机器学习项目生命周期。图源作者。
这就是结局吗?不,这仅仅是开始。
在监控过程中,你可能会发现模型在某些用户子集上的表现不好,或者准确率随着时间的推移而下降,因此你重新开始:理解问题 -> 获取数据 -> 建模和评估 -> 部署。
或者在模型评估过程中,你可能会发现模型还不够好,无法部署,因此你将重新开始:了解哪些地方不工作以及如何改进 -> 收集更多数据 -> 进行更多建模 -> 评估(希望)这次获得更好的结果。
(如果这是你第一次了解机器学习项目生命周期,我建议你查看 Anton Morgunov 的文章:机器学习项目的生命周期:有哪些阶段?)
因此,有两件重要的事情需要理解:
-
构建机器学习系统是一个迭代过程,直到模型从生产中移除为止,这一过程将永无止境。(无休止的工作)
-
图 1 提供了机器学习系统开发的简化版本,但实际上,你并不会顺利地从一个阶段过渡到另一个阶段。在每个阶段,可能会出现问题(通常会出现),这可能会让你退回一个或多个步骤,甚至让你重新开始。(欢迎来到现实世界)

图 2. 现实 机器学习项目的生命周期。图像由作者提供。
那些有工程背景的人可能会想:机器学习项目和传统软件开发有什么区别?测试、构建和发布在哪里?谢谢你的提问。
事实上,机器学习项目是软件工程项目的一个子集。所以你在软件工程中想到的所有最佳实践在机器学习项目中都非常受欢迎。话虽如此,让我向你介绍一个真正现实的软件项目生命周期,其中包含机器学习组件:

图 3. 真实 现实 的带有机器学习组件的软件项目生命周期。图像由作者提供。
为了控制这种混乱,每个项目都必须从计划开始。
(阅读 MLOps: Machine Learning Life Cycle 由 Satish Chandra Gupta 撰写,以了解更多关于机器学习软件开发生命周期的内容。)
在花费数千美元进行数据标注和数周进行机器学习模型开发之前,你需要做四件事。我们称之为“编程前”阶段。因此,现在关闭你的 PyCharm 吧,你只需要一个 Google 文档、大脑和 Zoom。
1. 估算机器学习项目的商业价值。
任何商业公司的目标都是赚更多的钱或提供更好的客户体验……为了赚更多的钱。牢记这个简单的原理,向你的老板、高层管理人员和利益相关者证明当前的机器学习项目是一个值得投资的项目。
理想情况下,您需要提供一些关于机器学习模型如何增加公司收入、用户参与度或减少请求处理时间等方面的大致数据。在这里可以发挥创造力,关闭完美主义,并且不要犹豫向财务和市场部门的同事寻求帮助。
(请记住,后续这些指标将用于评估项目,因此在承诺交付内容时务必要实际可行。)
2. 收集需求。
一旦没有人对机器学习模型的必要性表示怀疑,就开始收集需求。
每个领域都是特定的,每个项目都是独特的,因此没有一份详尽的需求清单可供参考。因此,请相信您的经验,并与同事合作。
这里有一个有用的提示:列出一些通用问题(我将在下面分享我的),然后直接提问。开始对话,随着讨论的进行,更多与项目相关的问题自然会浮现。
-
我们有多少数据?我们将如何标记它?
-
模型的延迟应该是多少?
-
模型将部署在云端还是本地?实例规格是什么?
-
是否有关于数据隐私和模型可解释性的要求?
如果一个任务可以通过机器学习解决,这并不意味着一定要这样做。 在这一点上,我建议您重新考虑是否采用纯软件工程方法或基本的规则驱动方法可能更适合。以下是可以帮助您做出决策的一些文章:
-
何时使用机器学习,亚马逊
-
四种情况下不适合使用机器学习,作者 Svenja Szillat
没有数据就没有机器学习。这似乎是显而易见的,但不幸的是,在我的职业生涯中,我见过太多公司犯同样的错误:他们想要人工智能,但他们的数据集很小,缺少重要的特征或者数据不干净。Monica Rogati 在文章“AI 需求层次”中提到,AI 应被视为需求金字塔的顶端,而数据收集、存储和清理则是其基础。

图 4. AI 需求层次。修改自 Monica Rogati 在“AI 需求层次”中的图像。
3. 从小开始,快速失败。
即使您的目标是创建一个每天为数百万用户提供服务的机器学习系统,从一个小得多的项目开始也是明智的选择:
-
PoC(概念验证)。从数据存储中手动检索数据,在 Jupyter Notebook 中快速迭代几种算法,最后,证明(或拒绝)假设:您可以在您拥有的数据上训练一个具有令人满意准确度的机器学习模型。在 PoC 阶段,您还将了解到部署和扩展模型所需的内容。
-
MVP(最小可行产品)。假设 PoC 阶段成功,现在你正在创建一个仅包含主要功能并发布给用户的产品。在机器学习项目中,这意味着将模型推出给一部分用户并评估它是否带来了预期的商业价值。
一旦你意识到一个想法行不通——可以心无旁骛地放弃它并转向下一个。这在你还没有花费多年工作或数十万美元时要容易得多。将失败的成本保持在较低水平是项目成功的关键因素。
(要深入了解这个话题,请阅读 POC 与 MVP:选择构建优秀产品 作者:Dmitry Chekalin。)
4. 编写设计文档。
软件工程中的设计文档是对软件系统架构的描述——其整体结构、各个组件及其相互之间的互动。它可以采取任意形式和结构,可以是正式的或非正式的,高级别的或详细的(这由团队决定)。在软件开发的实施阶段,设计文档作为开发人员遵循的蓝图。
这是软件工程中的最佳实践,正如我之前提到的,所有的软件工程最佳实践在 ML 项目中都受到高度欢迎。
我个人喜爱设计文档的原因如下:
-
编写触发思考过程。 编写设计文档就像在高层次上实现项目——你实际上不编写代码,但仍然对数据、算法和基础设施做出决策。你考虑所有场景并评估权衡,这意味着你将通过避免死胡同在未来节省时间和金钱。
-
设计文档简化了团队内部的同步和协作。 文档在团队成员之间共享,以便他们可以审阅,熟悉系统设计,并在需要时发起讨论。没有人被忽视,每个人都被鼓励参与。
如果你准备开始编写设计文档,以下是 Eugene Yan 提出的机器学习系统模板。可以随意修改并根据项目需求进行调整。
如果你想了解更多关于设计文档的概念,可以查看这些文章:
-
如何为机器学习系统编写设计文档 作者:Eugene Yan
-
Google 的设计文档 作者:Malte Ubl
结论
在这一章中,我们了解到每个项目都必须以计划开始,因为机器学习系统过于复杂,无法以临时的方式实施。我们回顾了机器学习项目的生命周期,讨论了为什么以及如何估算项目的商业价值,如何收集需求,然后冷静地重新评估是否真的需要机器学习。我们学习了如何通过“PoC”和“MVP”等概念从小处开始并快速失败。最后,我们谈到了在规划阶段设计文档的重要性。
在接下来的文章中,你将了解数据收集和标注、模型开发、实验跟踪、在线和离线评估、部署、监控、再训练以及更多内容——所有这些将帮助你构建更好的机器学习系统。
下一章已经发布:
关于数据中心 AI、训练数据、数据标注和清洗、合成数据以及一点数据工程和…
towardsdatascience.com
《构建更好的机器学习系统》—— 第二章:驯服数据混乱
关于数据中心的人工智能、训练数据、数据标注和清洗、合成数据,以及一些数据工程和 ETL。
·
关注 发表在 Towards Data Science ·12 分钟阅读·2023 年 5 月 24 日
--
照片由 charlesdeluvio 拍摄,来自 Unsplash
构建机器学习系统远不仅仅是迭代酷炫的最先进算法。
研究或学习项目以演示结束。在商业项目中,模型会发布给成千上万,甚至是百万用户,他们以各种可想象和不可想象的方式使用你的模型,并期望其始终快速、准确、公正地工作。一旦预测错误,可能会导致人员伤亡、数百万美元的损失,或严重损害公司的声誉。
在这个系列中,我们讨论了构建良好的机器学习系统所需解决的重要主题:商业价值和需求、数据收集和标注、模型开发、实验跟踪、在线和离线评估、部署、监控、再训练以及更多。
在上一章中,我们了解到每个项目都必须从计划开始,因为机器学习系统过于复杂,不能随意实现。我们回顾了机器学习项目的生命周期,讨论了为什么以及如何估计项目的商业价值,如何收集需求,然后冷静地重新评估是否真的需要机器学习。我们学习了如何通过“PoC”和“MVP”等概念从小处着手,快速失败。最后,我们谈到了规划阶段设计文档的重要性。
本章完全关于数据。我们将深入探讨机器学习系统中数据的各个方面——数据中心 AI、训练数据、数据标注和清洗、合成数据,以及一些数据工程和 ETL。本帖是系列中最长的一篇,但原因正当:数据科学家的大部分工作时间都投入在数据上。
所以让故事开始吧。
数据中心 AI
提高模型准确性的两种方法是:
-
收集更多数据或清理现有数据,同时保持模型不变。
-
使用更高级的算法或调整当前模型的超参数,同时保持数据集不变。
第一种方法被称为数据中心,第二种方法是模型中心。现在机器学习社区倾向于数据中心 AI;许多研究人员和从业者已经得出结论,改进数据比改进算法更能显著提高模型的准确性。你听过无数次的“垃圾进,垃圾出”这一说法正在重新焕发光彩。
这是 DeepLearning.AI 和 Landing AI 创始人 Andrew Ng 的观点:
“与其专注于代码,公司应该专注于开发系统化的工程实践,以可靠、高效、系统的方式改进数据。换句话说,公司需要从以模型为中心的方法转向以数据为中心的方法。”
建立优秀 AI 产品的公司也采用数据中心方法。前特斯拉 AI 总监安德烈·卡帕西(Andrey Karpathy)分享了他在特斯拉的大部分时间都投入在数据上。

图片。优秀的 AI 公司更关注数据而非算法。
来源:“构建软件 2.0 堆栈”作者:Andrej Karpathy*。
数据驱动的 AI 已经变得如此流行,以至于它最近演变成了一个研究改进数据集技术的独立学科。为了与 ML 社区保持一致,我强烈建议你参加 MIT 提供的这个优秀的免费课程:数据驱动 AI 介绍。
数据管道
一切都是数据。系统生成的日志、银行交易、网站数据、用户输入数据和客户数据只是你们业务可能涉及的一些例子。
到达的数据通常是混乱的、无结构的和脏的。它来自多个数据源,这可能会很棘手;有时它是加密的,或者可能缺少某些片段。数据可以是字节流、文本文件、表格、图像、语音和视频录音的形式;它可以是二进制的或人类可读的。
在数据科学家和机器学习工程师可以利用这些数据之前,数据需要经过处理、转换、清理、聚合和存储。
数据管道是一种组织数据流的方法。
ETL(提取-转换-加载)是一个广泛用于数据分析和机器学习的数据管道示例。在 ETL 中,数据以以下方式组织:
-
首先,你需要确定要收集哪些数据以及从哪些来源收集。
-
接下来,你合并这些数据源,将数据转换为所需格式,解决不一致性,并修复错误。
-
之后,你设计数据存储,并将处理和清理后的数据存储在那里。
-
最后,你自动化整个过程,使其无需人工干预即可运行。数据管道应周期性地或在特定事件发生时自动触发。
要深入了解 ETL,请查看 NIX United 的文章 什么是 ETL 过程:概述、工具和最佳实践。

图像。ETL 管道。 图像由 NIX United 提供。
这是一种高层次的数据管道概述。这个话题要广泛得多,更加复杂,因此越来越多的公司正在雇用数据工程师来处理数据存储和管道,同时让数据科学家和机器学习工程师专注于数据分析和建模。
如果你对数据工程师的技能集感到好奇,可以阅读 现代数据工程师路线图 由 datastack.tv 提供。我很高兴看到这个领域内专业角色的兴起,也很高兴数据科学家不再需要了解所有的东西了。这真是太让人松了一口气!
在我们深入讨论训练数据和标签之前,还有一件重要的事情:
如果数据管道设置得当,即使没有先进的机器学习,你的公司也能从数据中受益。 所以在采用机器学习之前,公司通常会从报告、指标和基础分析开始。
训练数据
为了训练“猫与狗”分类器,你需要向模型展示大量的猫图片,同时说“这是一只猫”,并展示大量的狗图片,同时说“这是一只狗”。不提供任何规则或解释,让模型决定如何进行预测。从数学上讲,这意味着模型调整其参数,直到输入与训练数据的预期输出匹配。
模型基于训练数据建立对世界的理解,假设训练数据代表了现实世界,并且正确地代表了它。 这就是为什么训练数据的质量非常重要。
-
“猫与狗”模型无法预测品种或分类其他动物,因为这些信息在训练集中不存在。
-
如果标签中存在错误,某些猫被标记为狗,反之亦然,模型会感到困惑,无法达到高准确率。非随机错误对模型可能极具破坏性。例如,如果所有吉娃娃都被标记为猫,模型将学习将吉娃娃预测为猫。
-
现实世界的数据包含偏见。例如,女性薪资更低。所以,如果你训练一个模型来预测公司员工的薪资,模型可能会预测女性的薪资较低,因为这正是数据中所体现的,并且模型假设应该是这样。
-
如果某些类别或分段在训练数据中表现不足或缺失,模型将无法很好地学习这些类别,并会产生不正确的预测。
训练数据应该是相关的、一致的、具有代表性的和全面的。这些术语的含义在 Amal Joby 的文章什么是训练数据?它在机器学习中的作用中解释得很好。
在我们都一致同意在高质量数据上训练模型至关重要之后,让我分享一些实用的技巧。
在收集训练数据之前,了解业务任务,然后将其框定为机器学习问题:应预测什么,以及从什么输入中预测。几乎任何业务任务都可以根据要求和限制以不同方式表示。在进行计算机视觉项目时,我通常在目标检测、分割和分类之间进行选择,并决定类别数量。
训练数据必须与模型在生产中‘看到’的数据非常相似。 从理论上讲,模型可以推广到未见过的数据,但在实践中,这种推广能力是相当有限的。例如,如果你为室内环境训练一个计算机视觉模型,它在户外效果会很差。类似地,在推特上训练的情感模型对于分析经典文学文本片段也不会有效。我个人经历过计算机视觉模型在面对较小的差异时,如光照、肤色、天气条件和压缩方法的微小变化,难以推广的情况。为了克服训练数据和生产数据之间的差异,一种流行的方法是使用生产中的最新数据作为训练数据集。

图像。训练数据和生产(测试)数据之间的不匹配示例。来源: Google Research Blog。
一个小而干净的数据集比一个大但脏的数据集更好。 对于大多数项目而言,数据标注是一个瓶颈。数据标注是一个极其复杂、缓慢且昂贵的过程(下一部分将专门讨论)。拥有一个巨大的干净数据集是只有庞大的科技公司才能负担得起的奢侈品。其他所有公司都必须在规模和质量之间做出选择,你应该始终选择质量,特别是对于用于评估模型的数据集。
没有人能真正确定需要多少数据。 这取决于预测的现实世界现象的复杂性、训练数据的变异性以及所需的模型准确性。找到这个答案的唯一方法是通过反复试验。因此……
分块获取数据。 从一个小数据集开始,标注它,训练一个模型,检查准确性,分析错误,并计划下一轮的数据收集和标注。
训练数据不是静态的。 正如你从前一章中回忆的那样,你将在研究阶段和模型已投入生产时多次训练和重新训练模型。每次新的迭代和模型更新时,都需要一个新的训练数据集。没有休息的机会,记住了吗? 😃
数据标注
目前,大多数生产中的机器学习模型是监督学习。这意味着需要标注数据来训练和评估模型。即使在无监督学习的情况下,模型从未标注的数据中学习模式和结构,仍然需要标注数据来评估模型的准确性;否则,你怎么知道它是否足够好以用于生产?
标签有两种类型:人工标签和自然标签。
一些机器学习任务涉及预测未来。例如,预测股票价格、客户流失、到达时间、欺诈交易和推荐。一旦未来到来,我们就知道真实的标签。这些标签被称为自然标签,我们只需在它们到来时进行收集。
在计算机视觉和自然语言处理(NLP)中,我们不是预测未来,而是对图像和文本进行分类、分析和检索信息。这就是为什么我们无法获得自然标签,必须严重依赖人工标签。
人工数据标注是一个极其复杂、缓慢且昂贵的过程。 不要把它当作机器学习项目中的一个任务来考虑;最好将其视为一个独立的数据注释项目,具有自己的范围、预算、时间线、团队、工具和关键绩效指标(KPI)。

图像。数据注释项目的阶段。来源: 管理数据注释项目的最佳实践
如果你与数据注释紧密合作,我推荐你查看 Tina Tseng 等人关于数据注释项目管理最佳实践的 30 页报告。对于较短版本和我自己的见解,请继续阅读这篇文章。
首先要决定的是:谁来标注数据? 需要考虑三个选项:众包、供应商和内部标注团队。我记得大约五年前,围绕像亚马逊机械土耳其这样的众包工具的兴奋。然而,很快发现众包标注只适用于需要最少或不需要员工培训的非常简单的任务。因此,大多数公司在供应商和内部标注团队之间进行选择。初创公司通常倾向于选择供应商,因为这提供了一个更简单的起点,而大型 AI 公司则建立自己的标注团队,以控制过程并实现更高的标注质量。举例来说,特斯拉有 1000 名全职员工在其人工数据标注团队中。仅仅是个例子。
创建指导方针并根据它们培训标注者。 指导方针是提供应标注内容及其方式的解释和视觉示例的文件。然后,将指导方针转化为标注者在进行实际标注任务之前必须完成的培训材料。如果你与供应商合作,请确保他们的员工培训过程设置得当。
现实世界的数据是模糊和混乱的,因此允许标注者说:“我不知道如何标注这个样本。” 然后,收集这些混淆样本,并用它们来改进指导方针。
标注工具很重要。标注员通常按小时计费,因此帮助他们更快、更准确地标注会节省你很多钱。在大规模上,标注员每小时标注 100 个样本与 300 个样本的差别尤其明显。所以明智选择,并关注以下几点:
-
标注单个样本所需的时间。一些工具专门为 NLP 任务开发;完全不同的工具用于 2D 或 3D 计算机视觉。
-
是否支持 AI 驱动的标注。这是你想要使用的功能。该工具可能通过用户点击对象来预测分割掩码,或者允许你部署自己的模型来协助标注过程。
-
它与您的基础设施的契合程度。标注工具将集成到数据管道中。一旦数据到达,它会自动采样并发送给标注员。他们标注数据,标签会自动存储在数据库中。一些工具可能比其他工具更适合你的基础设施,考虑一下这一点。
开源标注工具列表在这里,这里有一个不错的比较介绍了一些免费的和付费的工具。
估算成本和时间线。 你会惊讶于数据标注的缓慢和昂贵(我就是)。因此,最好做好准备(并提前准备你的经理)。
这里是大致估算成本和时间的公式:
-
标注时间(工时)= 标注一个样本的时间(小时) * 数据集大小(样本数量) + 预留的训练和错误修正时间(工时)
-
标注时间(工作天数)= 标注时间(工时) / 员工人数 / 8 小时
-
成本(\()= 标注员的小时费率(\)) * 标注时间(工时)
无论你多么努力,数据标签不可避免地会出现错误。 人类会犯错,他们可能会分心或误解任务。因此,检查标签的质量是必需的。当然,你为此选择的算法或工具也必须集成到数据管道中。我会一再强调:一切必须自动化。
一个这样的工具是Cleanlab。它由麻省理工学院毕业生开发,最近获得了很大的人气。Cleanlab 使用统计方法和机器学习算法改进图像、文本和表格数据的标签(有关它能做什么的示例,请查看Cleanlab 博客)。
关于数据标注的最后一点,我推荐这篇 Synced 的深刻文章——数据标注:AI 突破背后的十亿美元生意。标题已经很自解释了,文章确实值得一读。
合成数据
将所有上述数据标注的挑战,添加数据隐私问题以及现实世界数据中的严重类别不平衡,你就会明白为什么合成数据变得越来越受欢迎。
合成数据通常是使用一些游戏引擎、生成对抗网络的组合,或许再加上一点魔法来生成的。在自动驾驶汽车行业中,合成数据已经变得至关重要。查看一下NVIDIA和Tesla已经在做的事情。
一旦合成数据生成设置好,就可以相对快速且经济地获得大量多样化的数据集,并且具有极高的标签准确性。即使合成数据看起来不完美,它仍然可以用于模型的预训练。
如果你对扩展这一主题的知识感兴趣,这里有一个很好的资源:什么是合成数据? 由 NVIDIA 提供。
结论
在这一章中,我们讨论了行业中的新趋势——以数据为中心的人工智能,这是一种构建机器学习系统的方法,认为干净的数据比先进的机器学习算法更重要。我们涉及了数据管道,旨在组织混乱和非结构化的数据流,以便这些数据可以用于分析。我们了解到,训练数据应该是相关的、均匀的、具有代表性的和全面的,因为模型是基于这些数据建立对世界的理解的。我们回顾了两种类型的标签——人工标签和自然标签——并讨论了获取人工标签的复杂、缓慢且昂贵的过程,以及使这一过程不那么痛苦的最佳实践。最后,我们讨论了真实数据和人工标注的替代方案:合成数据。
在接下来的帖子中,你将学习到模型开发、实验跟踪、在线和离线评估、部署、监控、再训练等多个方面——这些都将帮助你构建更好的机器学习系统。
下一章已经可以阅读:
关于基准、实验跟踪、适当的测试集和指标。关于让算法发挥作用。
towardsdatascience.com
构建更好的机器学习系统 — 第三章:建模。让乐趣开始
关于基线、实验跟踪、适当的测试集和指标。关于让算法发挥作用。
·
关注 发布于 Towards Data Science ·15 min read·2023 年 8 月 25 日
--
你好,很高兴再次见到你。我非常欣赏你想成为更好专业人士、做得更好以及构建更好机器学习系统的愿望。你很棒,继续保持!
在这个系列中,我尽力帮助你掌握设计和构建机器学习系统的艺术、科学和(有时的)魔法。在这里,我们讨论业务价值和需求、数据收集与标记、模型开发、实验跟踪、在线和离线评估、部署、监控、再训练,以及更多内容。
这是第三章,专注于模型开发。一个机器学习算法只是机器学习系统的一小部分。 没有精心设计的系统,即使是完美准确的算法也无法服务于客户,也不会为公司带来利润。在这篇文章中,我将从一个不同的角度展示:如何选择、开发和评估算法,同时考虑算法的主要目标是为业务带来价值。最终,无论你是用线性回归还是最先进的神经网络解决业务问题,都不重要。

一个机器学习算法(中间的黑盒)只是机器学习系统的一小部分。 图片来源
在我们继续之前,让我们快速回顾一下我们已经学到的内容。
第一章 讲的是规划。我们了解到每个项目必须从计划开始,因为机器学习系统过于复杂,不能以临时的方式实施。我们回顾了机器学习项目生命周期,讨论了估算项目业务价值的原因和方法,如何收集需求,然后以冷静的头脑重新评估机器学习是否真的必要。我们学习了如何从小处着手,并利用“PoC”和“MVP”等概念快速失败。最后,我们谈到了在规划阶段设计文档的重要性。
第二章 讲的是数据。我们讨论了行业中的新趋势——数据中心人工智能,这是一种将干净的数据视为比先进的机器学习算法更重要的构建机器学习系统的方法。我们介绍了数据管道,这些管道旨在组织混乱和非结构化数据的流动,以便数据可以用于分析。我们了解到,训练数据应该是相关的、一致的、具有代表性的和全面的,因为模型基于这些数据建立对世界的理解。我们回顾了两种标签——人工标签和自然标签——并探讨了获取人工标签的复杂、缓慢和昂贵的过程,并讨论了使这一过程更少痛苦的最佳实践。最后,我们讨论了真实数据和人工标记的替代品:合成数据。
如果你不小心错过了之前的帖子,我建议你在继续之前阅读它们。我会在这里等你。
现在,让我们开始享受乐趣吧。
如何选择一个 ML 算法
没有一种算法适用于所有问题。你需要尝试几种方法,真正了解你的数据和领域,直到找到有效的方法。
思考、头脑风暴、与同事讨论、询问 ChatGPT,然后写下你打算尝试的三种方法:1) 一种非常简单的方法;2) 一种非常流行的方法;3) 一种新颖且有创意的方法。
-
一种非常简单的方法。算法中引入的每一种复杂性都必须是有理由的。从一个简单的方法开始(甚至可能是非 ML 的),评估它,并将其作为基线与其他所有模型进行比较。
-
一种非常流行的方法。如果你看到、听到或读到许多人使用特定算法解决相同的业务任务 — 确保将其添加到你的实验列表中。利用集体智慧!我对流行的方法总是充满期望,在大多数情况下,它们效果很好。
-
一种新颖且有创意的方法。尽管试试看。如果你通过击败典型的流行方法建立了竞争优势,你的老板和公司会很高兴。
提醒:不要重复发明轮子。有成百上千的开源库和代码库,已经实现了你可能想到的大多数算法、数据采样策略或训练循环。不要自己编写 K-means 聚类算法 — 使用scikit-learn中的现成实现。不要从头开始编写 ResNet50 — 使用PyTorch中的现成实现。在实现最新的论文之前,检查一下PapersWithCode,我敢打赌有人已经做过了。
做研究和发明新东西令人兴奋。从零开始实现算法,理解每一行代码,确实很有吸引力。然而,研究通常适合大学和大型科技公司。对于初创公司来说,每一美元都很重要,因此他们根本无法投资成功几率很低的东西(研究通常涉及 100 次尝试和 1 次成功)。
小心“最先进技术”。假设你正在使用YOLOv7进行目标检测,然后你听说YOLOv8已经发布,预计会更好。这是否意味着你需要升级所有的生产管道以支持 YOLOv8?不一定。
在大多数情况下,这种“更好”意味着在静态基准数据集上提升 1-2%,例如COCO。由于你的数据和业务问题在各个方面都不同,模型在你数据上的准确性可能更好、略好,甚至更差。此外,从本系列的第二章中,你应该记住:提升数据质量比改善算法能显著提高模型准确性。想办法清理训练数据——你会看到准确率提高 5-10%。
如何开发机器学习算法
首先,获取一个基准。基准是你将要与之竞争的模型。基准有两个逻辑选择:
-
从生产环境中获取一个现有的模型(如果你有的话)。我们想要改进现有的模型,这就是为什么我们需要与之进行比较。
-
一个非常简单且易于部署的模型。如果业务任务可以通过简单的方法解决,何必费劲训练复杂的模型?花几天时间寻找并实现一个简单的解决方案。
现在开始实验。你设计所有实验以在基准之上进行改进。找到一个有前途的算法?很好,评估并与基准进行比较。你的模型更好?恭喜你,它现在是你的新基准,考虑进行实验以进一步改进它。

算法开发是一个迭代的过程。图像由作者提供
算法开发是一个迭代的过程。你要么找到一个足够好的算法投入生产,要么耗尽时间。两种情况都是可能的。
自然地,你尝试的大多数想法都会失败。所以不要为此沮丧,也不要把它当成个人问题。我们都是这样工作的:找到一个好点子,试一试,发现这个点子实际上很糟糕,然后想出一个新的、希望这次好的点子,再试一试,发现它也不起作用,再找一个新点子,……
我的建议是:给单一想法的努力设定时间框架。如果你不能在 N 天内(提前选择你的 N 值)使这个想法奏效,就结束它并转向另一个想法。如果你真的想要成功,你需要经历许多不同的想法,因为,正如我之前所说,大多数你尝试的想法都会失败。
真正真正地了解你的数据。 可视化样本和标签,绘制特征分布,确保你理解特征的含义,探索每个类别的样本,了解数据收集策略,阅读提供给标注员的数据标注说明……训练自己预测模型应该预测的内容。如果你想创建一个好的算法,开始像算法一样思考(我不是在开玩笑)。所有这些都将帮助你发现数据中的问题、调试模型,并提出实验想法。
将数据划分为训练集、验证集和测试集。在训练集上训练,在验证集上选择超参数,在测试集上评估。确保这些划分之间没有重叠或数据泄漏。有关更多信息,请查看这篇文章:《机器学习中的训练、验证和测试划分》 作者 Jacob Solawetz。
方法:使用开源模型,先用默认参数运行,然后进行超参数调整。使用来自机器学习库的算法,例如scikit-learn、PyTorch、OpenCV,或者来自 GitHub 上拥有大量 stars、良好 readme 和允许商业使用的许可证的仓库。用默认超参数在你的数据上训练并进行评估。算法的默认超参数是为了在基准数据集(ImageNet, COCO)上最大化准确性,因此在大多数情况下,它们不适合你的数据和任务。详细了解每个超参数的含义及其对训练/推理的影响,以便进行超参数优化。典型的超参数优化方法包括Grad Student Descent、随机/网格/贝叶斯搜索和进化算法。永远不要在进行超参数优化之前就声称算法不起作用。欲了解更多信息,请查看 Pier Paolo Ippolito 的这篇文章:《超参数优化》。
更加深入地处理你的数据:进行特征工程和数据增强。 特征工程指的是转换现有特征和创建新特征。特征工程是一项关键技能,因此我向你推荐两篇可以帮助你掌握这项技能的优秀文章:
-
《机器学习特征工程的基本技术》 作者 Emre Rençberoğlu
-
《高级特征工程和预处理的 4 个技巧》 作者 Maarten Grootendorst
数据增强是一种从现有数据中创建新训练样本的技术,使得模型在训练过程中“看到”更多样本。增加训练集是提高模型准确性的最简单方法,因此你应该在可能的情况下始终进行数据增强。例如,在计算机视觉领域,几乎没有人会在没有基本图像增强(如旋转、缩放、裁剪、翻转等)的情况下训练模型。有关更多细节,请查看我的文章:《计算机视觉数据增强完全指南》。
如果你对自然语言处理中的数据增强方法感到好奇,可以阅读 Shahul ES 的《NLP 中的数据增强:Kaggle 大师的最佳实践》。
迁移学习是你的朋友。零样本学习是你最好的朋友。 迁移学习是一种流行的提高模型准确性的技术。实际上,这意味着你使用某个数据集上预训练的模型,继续用你的数据进行训练(“转移知识”)。即使你的数据与 COCO 或 ImageNet 数据集的图片看起来相差甚远,来自这些数据集的权重仍然可以改善你的模型。
零样本学习是一种无需训练即可在你的数据上工作的算法。怎么做到的?通常,它是一个在巨大样本数据集上预训练的模型。你的数据可能类似于这个模型已经训练过的内容;模型已经“见过”了大量样本,因此能够很好地泛化到新数据上。零样本学习听起来可能像是一个梦想,但确实有一些超级模型存在:Segment Anything、大多数词嵌入模型,ChatGPT。

为你的方便准备的模型开发清单。图片来源于作者
关于模型开发还有很多要说的,但我们需要总结,以留出时间讨论实验跟踪和评估的话题。如果你仍然渴望知识,可以查看 Andrej Karpathy 的这篇精彩文章:训练神经网络的配方。
实验跟踪
实验跟踪是将实验信息保存到某个仪表板或文件中的过程,以便你将来可以查看。这就像软件开发中的日志记录。训练和测试数据集的链接、超参数、git 哈希、测试数据上的指标——这些都是你可能跟踪的内容示例。
你必须跟踪你运行的所有实验。如果出于某种原因你的团队没有这样做,立即安排一个团队会议讨论这件事的重要性。你会感谢我的 😃
那么,我们为什么要进行实验跟踪呢?
-
为了比较不同实验之间的差异。当你开发模型时,你会训练和评估许多不同的算法,尝试不同的数据预处理技术,使用不同的超参数,采用各种创造性的技巧。最终,你希望看到你尝试了什么,哪些有效,哪些获得了最佳准确率。也许以后你会想回到某个实验,重新审视其结果。模型开发可能会持续几周甚至几个月,因此如果没有适当的实验跟踪,你会忘记你做了什么,并且不得不重新做实验。
-
为了重现实验。如果你不能重现它,那就不算数。检查一下:你能回到你最成功的实验,重新运行它并获得相同的准确率吗?如果答案是“不能”,可能是因为你没有对代码和数据进行版本控制,没有保存所有超参数,或者没有设置随机种子。
随机种子的设置重要性在 Cecelia Shao 的文章中解释得很好:在 ML 实验中正确设置随机种子。并不像你想象的那么简单。
-
调试实验。有时实验无法正常工作:算法不收敛,预测结果异常,准确率接近随机。如果没有保存实验信息,几乎不可能理解失败的原因。保存超参数列表、样本和数据增强的可视化、损失图等可能会给你一些线索,帮助你找到问题所在。
既然你已经相信实验追踪的重要性,我们来谈谈如何实际操作。
有很多免费的和付费的实验追踪工具,你可以选择适合你要求和预算的工具。可能最受欢迎的是Weights&Biases;我用过很多次,它很好。有关其他工具的评论,请查看15 个最佳 ML 实验追踪和管理工具由 Patrycja Jenkner 撰写。
机器学习实验由数据、代码和超参数组成。确保你使用版本控制工具管理代码,如 Github 或 Gitlab,并在开发过程中提交所有更改。能够恢复到旧的代码版本以重新运行旧的实验是很重要的。对数据进行版本控制。最简单和最流行的方法是为每个数据集的新版本创建一个新文件夹或文件(最好是在云存储上,例如Amazon S3或Google Cloud Storage)。有些人使用一个叫做Data Version Control (DVC)的工具。

ML 实验由数据、代码和超参数组成。图片由作者提供
你究竟应该追踪什么呢?嗯,追踪你能追踪的所有内容并非坏主意 😃 大多数时候,除非实验失败且失败非常严重,否则你不会用到所有这些信息。
以下是你可能需要考虑追踪的事项列表:
-
提交的 Git 哈希值
-
训练、验证和测试数据集的链接
-
超参数及其随时间的变化(模型结构、学习率、批量大小、数据增强等)
-
训练和验证集上的损失图
-
训练和验证集上的指标图
-
测试集上的指标
-
带标签的训练样本可视化(包括和不包括应用的数据增强)
-
测试集上的错误可视化
-
环境(操作系统、CUDA 版本、软件包版本、环境变量)
-
训练速度、内存使用、CPU/GPU 利用率
一次性设置实验跟踪,享受其永久的好处。
模型评估
在将模型部署到生产环境之前,必须彻底评估模型。这种评估称为“离线”评估。相比之下,“在线”评估则是检查已经在生产环境中运行的模型。在线评估将在本系列的下一章中讨论,今天我们只关注离线评估。
要进行离线评估,我们需要一个指标和一个数据集。
模型在测试数据集上进行评估,这是你在训练和调整超参数时留出的数据集。假设 1)测试集足够大且极其干净;2)模型从未见过测试数据;3)测试数据代表生产数据。如果其中一个假设被违反,评估就会不正确,存在获得过于乐观的指标并部署差模型的高风险。
在小规模测试集上进行评估可能会偶然得到一个好的指标。对脏数据的评估不会展示模型的真实表现。虽然训练集中出现错误更具包容性(你可以在干净标签、脏标签甚至无标签上进行训练),但测试集中出现错误可能是有害的。重要提示:无监督模型也需要标记的测试集。否则,你如何知道你的模型是否足够好?
确保你的模型没有“见过”测试数据。始终过滤重复项,以避免相同样本出现在训练集和测试集中。不要随机拆分数据,改用基于时间或用户的拆分。基于时间的拆分意味着将较旧的数据放入训练集,将较新的数据放入测试集。基于用户的拆分意味着将同一用户的所有数据放在同一拆分中。要非常小心数据泄漏,更多详细信息请参阅 Prerna Singh 的 《机器学习中的数据泄漏:如何检测并减少风险》。
指标是一个假定与模型真实表现相关的数字:数字越高,模型越好。你可以选择一个或几个指标。例如,分类任务的典型指标有准确率、精确率、召回率和 F1 分数。选择一些简单且理想情况下可解释的指标,以便非技术经理和客户也能理解。
以下是 Shervin Minaee 关于各种任务和领域指标的优秀文章:
-
20 个流行的机器学习指标。第一部分:分类和回归评估指标
-
20 个热门机器学习指标。第二部分:排序和统计指标
使用基于切片的指标并评估你能想到的每个数据片段(除非你想陷入像“Zoom 的虚拟背景功能不适用于黑人面孔”这样的丑闻)。例如,面部检测系统必须分别评估不同种族、性别和年龄的人群。电子商务模型值得评估桌面与移动端、不同国家和浏览器的表现。仔细检查每个片段是否在测试集中得到充分代表。基于切片的指标也有助于解决类别不平衡问题:分别查看每个类别的精确度和召回率比总的精确度/召回率更有意义。
避免丑闻的另一种方法(这次是“银行 ABC 的新信用评分系统歧视未婚女性”)是使用行为测试。一篇很棒的论文,超越准确性:使用 CheckList 对 NLP 模型进行行为测试,建议除了数值指标外,还使用最低功能性、不变性和方向期望测试。尽管这篇论文专注于自然语言处理,这些测试类型也可以很容易地应用于表格数据和图像。
在“银行 ABC 的新信用评分系统歧视未婚女性”的例子中,不变性行为测试可能会有很大帮助。保持所有特征不变,但改变婚姻状况和性别,并检查模型预测是否发生变化。如果你看到预测有显著差异(而应当是“不变”的),可能说明你的模型在训练数据中吸收了偏见;这需要修正,例如,通过完全移除模型输入中的敏感(易引起歧视的)特征。
最后,可视化错误。找到测试集中模型出错的样本;可视化这些样本并分析为何会发生这些错误。这是因为测试集仍然很脏吗?训练集中是否有足够相似的样本?模型错误是否存在某种模式?这种分析有助于发现测试集中可能的标注错误和训练过程中的漏洞,并提出进一步提高模型性能的想法。

方便使用的模型评估检查清单。图片由作者提供
结论
在本章中,我们学会了如何在开发模型时考虑到,机器学习算法只是机器学习系统的一部分。模型开发从创建一个简单的基准模型开始,并通过迭代改进不断推进。我们提出了最有效的方式:利用开源模型并围绕它进行实验,而不是重新发明轮子或陷入研究的泥潭。我们讨论了“最先进”算法的陷阱和数据增强及迁移学习的好处。我们一致认为实验跟踪的重要性,并学习了如何设置它。最后,我们讨论了离线评估——指标选择、适当的测试集、基于切片的评估和行为测试。
我们快到达目标了,只剩下最后一章。在下一篇(最后一篇)文章中,你将学习关于部署、监控、在线评估和再训练的内容——这些是帮助你构建更好机器学习系统的最后一块知识。
最终章将很快发布。订阅以保持关注。
构建更好的 ML 系统——第四章:模型部署及其发展
关于部署、监控、数据分布漂移、模型更新和生产环境中的测试。
·
关注 发表在 Towards Data Science ·13 分钟阅读·2023 年 9 月 28 日
--
部署模型并在生产环境中支持它们更多是工程问题,而非机器学习问题。
当一个 ML 项目接近生产阶段时,越来越多的人参与其中:后端工程师、前端工程师、数据工程师、DevOps、基础设施工程师……
他们选择数据存储,介绍工作流和管道,将服务集成到后端和 UI 代码库中,自动化发布,进行备份和回滚,决定计算实例,设置监控和警报……如今,几乎没有人期望数据科学家/机器学习工程师能做到这一切。即使在一个小型初创公司中,人们也在某种程度上有所专门化。
“数据科学家/机器学习工程师为什么需要了解生产环境?”——你可能会问。
这是我的回答:
拥有一个在生产环境中的模型并不意味着我们已经完成了所有与机器学习相关的任务。哈!远远没有。现在是时候面对一整套新的挑战了:如何在生产环境中评估你的模型并监控其准确性是否仍然令人满意,如何检测数据分布的变化并应对这些变化,多久重新训练一次模型,以及如何确保新训练的模型更好。这里有一些方法,我们将对此进行详细讨论。
在这篇文章中,我有意只专注于机器学习话题,并省略了许多工程概念或以高层次的方式进行介绍——以便使不同经验水平的人都能简单易懂。
这是“构建更好的机器学习系统”系列的最终篇。本系列旨在帮助你掌握设计和构建机器学习系统的艺术、科学和(有时的)魔法。在之前的章节中,我们已经讨论了项目规划和商业价值(第一章);数据收集、标注和验证(第二章);模型开发、实验跟踪和离线评估……(第三章)。如果你错过了之前的帖子,我建议你在阅读这篇之前或之后看看它们。
部署
在将模型部署到生产环境时,有两个重要的问题需要问:
-
模型是否应该实时返回预测结果?
-
模型是否可以部署到云端?
第一个问题迫使我们在实时推断与批量推断之间做出选择,第二个问题则在云计算与边缘计算之间做出选择。
实时与批量推断
实时推断是一种直接且直观的与模型互动的方式:你提供一个输入,它返回一个预测。这种方法在需要立即获取预测时使用。例如,银行可能会使用实时推断来验证一笔交易是否存在欺诈行为,然后再最终确认它。
批处理推断则更便宜且更容易实现。之前收集的输入会一次性处理。批处理推断用于评估(在静态测试数据集上运行时)、临时活动(如选择客户进行电子邮件营销活动)或在不需要立即预测的情况下。批处理推断也可以是实时推断的成本或速度优化:你提前计算预测并在请求时返回它们。

实时推断与批处理推断。图片来源于作者
实时推断比批处理推断要具有更大的挑战性和成本。这是因为模型必须始终在线并以低延迟返回预测。它需要一个聪明的基础设施和监控设置,这可能甚至在同一公司内部的不同项目中也会有所不同。因此,如果立即获得预测对业务并不关键——那么坚持使用批处理推断会更好。
然而,对于许多公司来说,实时推断在准确性和收入方面确实有所不同。这对搜索引擎、推荐系统和广告点击预测都是如此,因此投资于实时推断基础设施是非常值得的。
关于实时推断与批处理推断的更多详细信息,请查看这些帖子:
-
在生产环境中部署机器学习模型 作者: Microsoft
-
批处理推断与在线推断 作者: Luigi Patruno
云计算与边缘计算
在云计算中,数据通常通过互联网传输并在集中式服务器上处理。另一方面,在边缘计算中,数据在生成数据的设备上处理,每个设备以去中心化的方式处理其自身的数据。边缘设备的例子包括手机、笔记本电脑和汽车。

云计算与边缘计算。图片来源于作者
像 Netflix 和 YouTube 这样的流媒体服务通常在云端运行其推荐系统。它们的应用程序和网站将用户数据发送到数据服务器以获得推荐。云计算相对容易设置,你可以几乎无限制地扩展计算资源(或至少在经济上合理的情况下)。然而,云基础设施高度依赖稳定的互联网连接,敏感的用户数据不应通过互联网传输。
边缘计算的发展是为了克服云计算的局限性,并且能够在云计算无法工作的地方进行工作。自动驾驶引擎运行在汽车上,因此即使没有稳定的互联网连接也能快速工作。智能手机认证系统(如 iPhone 的 FaceID)运行在智能手机上,因为通过互联网传输敏感用户数据不是一个好主意,用户确实需要在没有互联网连接的情况下解锁手机。然而,为了使边缘计算可行,边缘设备需要足够强大,或者模型必须足够轻量和快速。这催生了模型压缩方法,如低秩近似、知识蒸馏、剪枝和量化。如果你想了解更多关于模型压缩的信息,这里是一个很好的起点:超棒的机器学习模型压缩。
若要深入了解边缘计算和云计算,请阅读以下文章:
-
边缘计算与云计算有什么区别? 作者:NVIDIA
-
边缘计算与云计算:主要区别 作者:Mounika Narang
轻松部署与演示
“生产是一个连续体。对于一些团队来说,生产意味着从笔记本结果生成漂亮的图表来展示给业务团队。而对于其他团队来说,生产意味着确保你的模型每天能为数百万用户持续运行。” Chip Huyen, 为什么数据科学家不必了解 Kubernetes
将模型部署以服务数百万用户是一个大团队的任务,因此作为数据科学家/机器学习工程师,你不会被单独留下。
然而,有时你确实需要单独进行部署。也许你正在进行一个个人或学习项目,并希望创建一个演示。也许你是公司里的第一位数据科学家/机器学习工程师,你需要在公司决定扩展数据科学团队之前带来一些业务价值。也许你的所有同事都忙于各自的任务,因此你在考虑是否自己进行部署而不等待支持。你不是第一个,也绝对不会是最后一个面对这些挑战的人,且有解决方案可以帮助你。
要部署一个模型,你需要一个运行模型的服务器(实例),一个与模型通信的 API(发送输入,获取预测),以及(可选的)一个用户界面,用于接收用户输入并展示预测结果。
Google Colab 是 Jupyter Notebook 的升级版。它是一个很棒的工具,可以创建你可以分享的演示。它不需要用户进行任何特定的安装,提供免费的 GPU 服务器来运行代码,并且你可以轻松定制它以接受用户的任何输入(文本文件、图像、视频)。它在学生和机器学习研究人员中非常受欢迎(这是 DeepMind 研究人员如何使用它的)。如果你对了解更多关于 Google Colab 的信息感兴趣,请从 这里 开始。
FastAPI 是一个用于构建 Python API 的框架。你可能听说过 Flask,FastAPI 类似,但代码更简单,更专注于 API,速度更快。有关更多详细信息,请查看 官方文档。有关实际示例,请阅读 Goku Mohandas 的《模型服务的 API》。
Streamlit 是一个易于创建 Web 应用程序的工具。它很简单,我真的这么认为。应用程序看起来很漂亮且互动性强——有图像、图表、输入窗口、按钮、滑块等。Streamlit 提供 Community Cloud,你可以免费发布应用程序。要开始使用,请参阅 官方教程。
云平台。Google 和 Amazon 在使部署过程无缝且易于访问方面做得很好。它们提供付费的端到端解决方案,用于训练和部署模型(存储、计算实例、API、监控工具、工作流等)。这些解决方案易于上手,并且功能广泛以支持特定需求,因此许多公司选择使用云服务提供商构建其生产基础设施。
如果你想了解更多内容,请查看以下资源:
-
以基本上免费的方式大规模部署你的副项目 由 Alex Olivier 提供
-
将模型部署到端点 由 Google 提供
监控
像所有生产中的软件系统一样,机器学习系统必须进行监控。这有助于快速检测和定位错误,防止系统发生灾难性故障。
从技术上讲,监控意味着收集日志,从中计算指标,将这些指标显示在类似于 Grafana 的仪表板上,并设置警报以在指标超出预期范围时提醒。
应该监控哪些指标? 由于 ML 系统是软件系统的一个子类,因此可以从操作指标开始。示例包括机器的 CPU/GPU 利用率、内存和磁盘空间;发送到应用程序的请求数量和响应延迟、错误率;网络连接性。要深入了解操作指标的监控,请查看 Justin Ellingwood 的文章指标、监控与警报介绍。
操作指标关注机器、网络和应用程序的健康状态,而与 ML 相关的指标检查模型准确性和输入一致性。
准确性是我们最关心的事情。这意味着模型可能仍然会返回预测,但这些预测可能完全不准确,你直到模型被评估时才会意识到。如果你有幸在一个自然标签快速可用的领域工作(如推荐系统),只需收集这些标签,并持续评估模型。然而,在许多领域,标签可能需要很长时间才能到达,或者根本不会出现。在这种情况下,监控一些可能间接指示准确性下降的指标是有益的。
为什么模型准确率会下降?最普遍的原因是生产数据已从训练/测试数据中漂移。在计算机视觉领域,你可以直观地看到数据已经漂移:图像变得更暗或更亮,或分辨率发生变化,或现在室内图像比室外图像更多。
要自动检测数据漂移(也称为“数据分布变化”),需要持续监控模型的输入和输出。模型的输入应该与训练期间使用的一致;对于表格数据,这意味着列名以及特征的均值和方差必须相同。监控模型预测的分布也是有价值的。例如,在分类任务中,你可以跟踪每个类别预测的比例。如果发生了显著变化——比如一个模型以前将 5%的实例分类为 A 类,现在将 20%分类为 A 类——这就是一个明确的信号,表明确实发生了某些事情。要了解更多关于数据漂移的内容,请查看 Chip Huyen 的这篇精彩文章:数据分布变化与监控。
关于监控还有很多要说的内容,但我们必须继续前进。如果你觉得需要更多信息,可以查看这些文章:
-
监控机器学习系统 作者:Goku Mohandas
-
关于如何监控生产环境中模型的综合指南 作者:Stephen Oladele
模型更新
如果你将模型部署到生产环境中并不加以更新,它的准确性会随着时间的推移而降低。 在大多数情况下,这可以通过数据分布的变化来解释。输入数据可能会改变格式。用户行为不断变化,没有有效的理由。流行病、危机和战争可能会突然发生,打破之前有效的所有规则和假设。“变化是唯一的不变。”- 赫拉克利特。
这就是为什么生产模型必须定期更新的原因。更新分为两种类型:模型更新和数据更新。在模型更新中,更改算法或训练策略。模型更新不需要定期进行,通常是根据具体情况进行的——当业务任务发生变化、发现错误或团队有时间进行研究时。相比之下,数据更新是在新数据上训练相同的算法。定期的数据更新是任何 ML 系统的必备条件。
定期数据更新的前提是建立一个能够支持自动数据流、模型训练、评估和部署的基础设施。
关键在于,数据更新应尽量减少人工干预。人工工作应主要用于数据标注(同时确保数据流向和来自标注团队的流动完全自动化),可能需要做出最终部署决策,并解决在训练和部署阶段可能出现的任何问题。
一旦基础设施建立起来,更新的频率只是你需要在配置文件中调整的一个值。模型应该多频繁地用更新的数据进行更新? 答案是:尽可能频繁且经济合理。如果增加更新频率带来的价值大于成本——绝对应该增加。然而,在某些情况下,即使训练每小时进行一次可能会非常有利,也可能不可行。例如,如果模型依赖于人工标注,这个过程可能会成为瓶颈。
从头开始训练还是仅在新数据上进行微调? 这不是一个二元的决策,而是两者的结合。频繁地对模型进行微调是合理的,因为它比从头开始训练更具成本效益且更快。然而,有时从头开始训练也是必要的。理解微调主要是对成本和时间的优化至关重要。通常,公司最初会采用从头开始训练的直接方法,随着项目的扩展和发展,逐渐纳入微调。
要了解有关模型更新的更多信息,请查看此帖子:
重训练,还是不重训练?让我们分析一下 ML 模型更新 由 Emeli Dral 等人编写。
测试生产环境
在将模型部署到生产环境之前,必须对其进行彻底评估。我们已经在上一篇文章中讨论了生产前(离线)评估(请参见“模型评估”部分)。然而,直到你将模型部署到生产环境,你永远不知道它在生产环境中的表现如何。这促生了生产环境中的测试,也称为在线评估。
在生产环境中测试并不意味着草率地将你可靠的旧模型替换为新训练的模型,然后焦急地等待第一次预测,随时准备在出现轻微问题时回滚。绝不要这样做。有更聪明、更安全的策略来在生产环境中测试你的模型,而不会冒着失去资金或客户的风险。
A/B 测试是业界最流行的方法。通过这种方法,流量在现有模型和新模型之间以某种比例随机划分。现有模型和新模型对真实用户进行预测,预测结果被保存并随后仔细检查。比较的不仅仅是模型准确性,还可以比较一些与业务相关的指标,如转化率或收入,这些有时可能与准确性负相关。
A/B 测试高度依赖于统计假设检验。如果你想了解更多,可以查看这篇文章:A/B 测试:统计检验的完整指南 作者:弗朗切斯科·卡萨列尼奥。有关 A/B 测试的工程实现,请查看在线 AB 测试模式。
Shadow 部署是测试模型的最安全方式。其理念是将所有流量发送到现有模型,并以通常的方式将其预测返回给最终用户,同时也将所有流量发送到新的(Shadow)模型。Shadow 模型的预测不会被使用,仅仅是存储以备未来分析。

A/B 测试与 Shadow 部署。图片由作者提供
Canary 发布。你可以将其视为“动态”A/B 测试。新的模型与现有模型并行部署。一开始,只有一小部分流量(例如 1%)会发送到新模型,其余 99% 仍由现有模型提供服务。如果新模型的表现足够好,其流量份额会逐渐增加并再次评估,再次增加并评估,直到所有流量都由新模型提供服务。如果在某个阶段,新模型的表现不佳,它会被从生产环境中移除,所有流量将重新指向现有模型。
这里是进一步解释的文章:
Shadow 部署与 ML 模型的 Canary 发布 作者:巴尔托什·米库尔斯基。
结论
在这一章中,我们了解了一系列新的挑战,这些挑战在模型部署到生产环境后会出现。模型的运营和与 ML 相关的指标必须持续监控,以便快速检测和修复可能出现的错误。模型必须定期在更新的数据上重新训练,因为其准确性会随着时间的推移而降低,主要是由于数据分布的变化。我们讨论了在部署模型之前需要做出的高层次决策——实时推理与批量推理以及云计算与边缘计算,每种都有其自身的优点和限制。我们介绍了用于简单部署和演示的工具,当你必须单独完成时可以使用。我们了解到,模型必须在生产环境中进行评估,除了对静态数据集的离线评估外。你永远不知道模型在生产环境中的表现如何,直到你实际发布它。这一问题催生了“安全”和受控的生产测试——A/B 测试、影子部署和金丝雀发布。
这也是“打造更好的 ML 系统”系列的最后一章。如果你从一开始就跟随我,你现在应该知道,ML 系统远不只是一个复杂的算法。我真的希望这个系列对你有所帮助,拓宽了你的视野,并教会了你如何构建更好的 ML 系统。
感谢你的阅读!
如果你错过了前面的章节,这里是完整的列表:
## 打造更好的 ML 系统。第一章:每个项目都必须从计划开始
关于 ML 项目生命周期、设计文档、业务价值和需求。关于从小做起和快速失败。
关于以数据为中心的 AI、训练数据、数据标注和清洗、合成数据,以及一些数据工程和…
关于基准线、实验跟踪、适当的测试集和指标。关于让算法发挥作用。
因果推断的构建模块——使用 LEGO 的 DAG 方法
原文:
towardsdatascience.com/building-blocks-of-causal-inference-a-daggy-approach-using-lego-cac1372348f3
使用 DAG 和贝叶斯回归的因果推断简介
·发布于 Towards Data Science ·阅读时间 9 分钟·2023 年 2 月 4 日
--
因果推断是一个迷人的话题。因果模型旨在创建对变量关系的机械理解。最近我读了 理查德·麦克艾勒斯的《统计思维》,他那富有表现力且易于理解的写作改变了我对回归和统计分析以及生活的思考方式。
本文旨在探讨使用有向无环图(DAG)和 brms (Buerkner) 的因果建模。

图片由 Markus Spiske 提供,来源于 Unsplash
我发现了一篇由 Ziegler 等人撰写的精彩文章,旨在教导小学生多元线性回归模型的教学法。我将使用的数据集取自这篇文章,并在 CC BY 4.0 许可下提供。
摘要 我们提出了一项创新活动,利用关于 LEGO 积木的数据帮助学生自我发现多个…
加载包和数据
library(tidyverse)
library(tidybayes)
library(brms)
library(ggdag)
library(dagitty)
train <- read_csv('lego.population.csv')
train_parse <-
train %>%
drop_na(Weight, Pieces) %>%
mutate(Weight = as.numeric(str_sub(Weight, 1, 3)),
Price = as.numeric(str_remove(Price, '[^[:alnum:]]')),
Amazon_Price = as.numeric(str_remove(Amazon_Price, '[^[:alnum:]]')),
Price = coalesce(Price, Amazon_Price)) %>%
drop_na(Price, Weight) %>%
mutate(Weight_c = Weight/max(Weight),
Price_c = Price/max(Price),
Pieces_c = Pieces/max(Pieces)) %>%
drop_na(Weight, Pieces, Price)
我们进行了一些基本的数据清理,然后对变量进行最小/最大缩放,使它们在相同的尺度上,这样我们后续对贝塔后验均值的解释会更容易得多。
探索性数据分析
使用 GGally:ggpairs,我们生成了下面的变量对图。
GGally::ggpairs(train_parse %>%
select(Weight_c, Pieces_c, Price_c),
aes(alpha = 1/3)) +
theme_minimal() +
labs(title = 'Pairplot for Weight, Pieces and Price')

使用最小-最大缩放变量的变量对图(图由作者提供)
没有特别有趣的内容需要注意,除了所有三个变量之间强正线性关系的存在。但正确的因果模型是什么?增加价格是否也会增加件数?我认为不是这样。
因果推断
使用加载的数据集,我们将创建一个关于价格的因果理解。这个数据集在因果关系上概念上很容易理解。
数以千计的乐高套装存在,从基本的儿童 Duplo 到像千年隼或死亡星这样的包含数千件的巨大套装。价格从$10–20 起步,对于限量版模型套装则高达数千美元。我们能否为给定套装的特征创建一个合理的因果模型?
DAGs
在下文中,我们使用 daggity 包设置我们提议的 DAG,配以一些绘图坐标,然后使用 ggdag 绘制下面的图,描述了权重和件数如何影响乐高套装价格的所有不同路径。
dag_coords <-
tibble(name = c("Pcs", 'W', 'Pr'),
x = c(1, 2, 2),
y = c(2, 2, 1))
dagify(Pr ~ Pcs,
Pr ~ W,
W ~ Pcs,
coords = dag_coords) %>%
ggplot(aes(x = x, y = y, xend = xend, yend = yend)) +
geom_dag_point(color = 'dark red', alpha = 1/4, size = 20) +
geom_dag_edges(edge_color = 'dark red') +
geom_dag_text(color = 'dark red') +
scale_x_continuous(NULL, breaks = NULL, expand = c(.1, .1)) +
scale_y_continuous(NULL, breaks = NULL, expand = c(.1, .1)) +
theme_bw() +
theme(panel.grid = element_blank())
我们的任务现在是测试这些 DAG 的影响,然后最终确定一个最佳描述这些变量对价格影响的因果模型。首先,我们可以合理地说,件数与重量和价格正相关,即套装的件数越多,重量和价格也会更高。类似地,我们也可以推理出,较大、较重的套装应该价格更高。

权重、件数和价格的完整 DAG(图由作者提供)
对件数进行价格回归
自然的起点是将价格与件数进行回归,Pcs → Pr。贝叶斯分析的美妙之处在于能够在查看数据之前提供先验分布。集合价格的分布由正态分布描述,其中均值由拦截项和件数的梯度项的线性项描述。拦截项由一个相对宽泛的高斯分布描述,其均值为$20,标准差为$6。实质上,这描述了没有任何件数的乐高套装的基础价格分布。beta 项描述了每件的价值增加。谷歌搜索表明乐高件的平均价格为 11 美分,因此这是一个良好的起点,我们将使标准差足够宽泛。

Pcs → Pr 先验公式(图由作者提供)
mP_Pr <- brm(Price ~ 1 + Pieces,
family = gaussian,
prior = c(prior(exponential(1), class = sigma),
prior(normal(20, 6), class = Intercept),
prior(normal(0.11, 0.04), class = b)
),
data = train_parse,
warmup = 1000, iter = 2000, chains = 4, cores = 4, seed = 246) %>%
add_criterion(criterion = c('loo'))
summary(mP_Pr)

Pcs → Pr 的摘要输出(图由作者提供)
在接触数据后,我们的先验并没有偏离太远,截距项的后验均值代表了一个零件数为零的 Lego 套件的平均价格,每增加一个部件,套件的价值增加 8 分。后验分布在各自的尺度上具有相当离散的误差项,因此基于数据模型对这些值相当自信。
将价格回归于重量
类似于上述情况,设置一些合理但宽泛的先验,这些先验将被数据所淹没。我们假设与之前相同的截距先验,重量的 beta 项也很宽泛,均值为 $50/kg,标准差为 $15。

W → Pr 先验公式(图片由作者提供)
mW_Pr <- brm(Price ~ 1 + Weight,
family = gaussian,
prior = c(prior(exponential(5), class = sigma),
prior(normal(20, 6), class = Intercept),
prior(normal(50, 15), class = b)), )
data = train_parse,
warmup = 1000, iter = 2000, chains = 4, cores = 4, seed = 246)

W → Pr 的总结输出(图片由作者提供)
这次截距项低于之前,后验均值为 $5.77,重量的梯度项后验均值为 $57.90/kg。注意这个模型更为自信,因为误差值比回归价格于部件数时更为离散。
将价格回归于部件数和重量
现在情况变得更加有趣了——我们为价格创建了两个贝叶斯回归模型,分别使用部件数和重量。这两个模型看起来都相当不错,但哪个是更好的因果模型呢?
遇到碰撞器,其中部件数和重量彼此独立(Pcs || W)。鉴于我们对变量的机械知识,我们已经可以将这种形式排除,但让我们开发模型来支持这一点。


碰撞器 DAG 和 W → Pr ← Pcs 的公式(图片由作者提供)
# The Colllider
mWP_Pr <- brm(
Price ~ 1 + Pieces + Weight,
family = gaussian,
prior = c(prior(exponential(5), class = sigma),
prior(normal(20, 6), class = Intercept),
prior(normal(50, 15), class = b, coef = Weight),
prior(normal(0.11, 0.04), class = b, coef = Pieces)
),
data = train_parse,
warmup = 1000, iter = 4000, chains = 4, cores = 4, seed = 246)

W → Pr ← Pcs 的输出总结(图片由作者提供)
我们立刻注意到,截距的系数几乎没有变化——而且两个预测变量的系数都有所下降,这并不令人惊讶,因为它们的相关性非常强,这是多重共线性的一个例子。这足以证明碰撞器作为合适的因果模型是不成立的,违反了两个变量之间的独立性假设。下面我们将重量和部件数的后验分布一起可视化为散点图,以展示多重共线性,存在某种共享的方差轴。在右侧,我们还看到回归于重量与回归于重量和部件数一起的重量项的后验分布几乎没有差异。
# LHS: Plot of Posterior Draws Weight vs. Pieces
as_draws_df(mWP_Pr) %>%
ggplot(aes(x = b_Pieces_c, y=b_Weight_c)) +
geom_point(alpha = 0.3) +
labs(y = 'Weight', x = 'Pieces', title = 'Weight and Pieces Display Multicollinearity and Not Indpendant', subtitle = 'How Can Price per Piece Increase as Price per Unit Weight Decreases?') +
theme_minimal()
# RHS: Comparison of Posterior Draws for Weight Across W → Pr ← Pcs & W → Pr
tidy_draws(mWP_Pr) %>%
select(b_Pieces_c, b_Weight_c) %>%
transmute(Pieces_Weight = b_Pieces_c + b_Weight_c,
Weight = tidy_draws(mW_Pr) %>% select(b_Weight_c) %>% as_vector()) %>%
pivot_longer(1:2, names_to = 'Variable', values_to = 'posterior_samples') %>%
ggplot(aes(posterior_samples, fill = Variable)) +
geom_density(alpha = 1/3) +
theme_minimal() +
labs(title = 'Addition of Pieces and Weight is Nearly Equivalent to Weight Alone',
subtitle = 'Variables Min-Max Scaled',
y = 'Density',
x = 'Posterior Distribution')
#NB We've used models regenerated using min-max scaled variables.


重量和部件数显示了多重共线性,变量经过最小-最大缩放(图片由作者提供)
下一个 DAG 被称为管道,其中件数在条件为重量时与价格独立(或 Pcs || Pr | W)。换句话说,一旦知道一组的重量,知道件数对我们理解价格没有进一步的价值。

管道 Pcs → W → Pr(图像由作者提供)
以下我们将管道建立为贝叶斯模型,使用我们之前示例中的先验。
# The Pipe
pr_model <- bf(Price ~ 1 + Weight)
w_model <- bf(Weight ~ 1 + Pieces)
mWP_Pr2 <- brm(pr_model + w_model + set_rescor(F),
prior = c(prior(exponential(5), class = sigma, resp = Price),
prior(exponential(5), class = sigma, resp = Weight),
prior(normal(50, 15), class = b, coef = Weight, resp = Price),
prior(normal(0.11, 0.04), class = b, coef = Pieces, resp = Weight)),
family = gaussian,
data = train_parse,
warmup = 1000, iter = 4000, chains = 4, cores = 4, seed = 246)

Pcs → W → Pr 的总结输出(图像由作者提供)
由于我们在重量上回归,价格的截距保持不变,其后验均值实际上与我们的第一个模型 W → Pr 相同。重量截距的后验均值可能代表了在添加任何件数之前的包装重量。件数的回归系数相对较小(0.0014kg/1.4g 每件)。
让我们用一个反事实模型来测试隐含的条件独立性。我们建立新的数据,改变件数的同时保持重量恒定为 1 公斤。
# Counterfactual Plot Pieces on Price, Holding Weight Consant
nd <- tibble(Pieces = seq(from = 0, to = 5000, length.out = 50),
Weight = 1)
predict(mWP_Pr2,
resp = c('Price'),
newdata = nd) %>%
as_tibble() %>%
bind_cols(nd) %>%
ggplot(aes(x = Pieces, y = Estimate, ymin = Q2.5, ymax = Q97.5)) +
geom_smooth(stat = 'identity', alpha = 1/5, size = 1/4) +
labs(y = 'Counterfactual Price', x = 'Manipulated Pieces')

价格的影响因素图(图像由作者提供)
由于价格的影响是通过重量间接体现的,因此在重量恒定的情况下,价格将保持不变,无论件数的多少。这支持了隐含的条件独立性,即在条件为重量时,价格与件数是独立的(或 Pcs || Pr | W)。换句话说,一旦知道一组的重量,知道件数对我们理解价格没有进一步的信息增量。
总结评论
在本文中,我们展示了使用贝叶斯回归的因果建模的易于理解的入门介绍。我们开发了一个因果模型,用于分析物理变量(件数和重量)对价格的影响,并发现我们可以合理地认为,将价格回归到重量上是一个不错的因果模型。
我想借此机会感谢理查德·麦克埃尔瑞斯特(Richard McElreath)对因果建模的精彩著作和讲座,激发了我改变思维方式,采用更加严谨的贝叶斯方法来处理工作和生活。
谢谢。我希望你阅读这篇文章的乐趣与我写作时的乐趣一样。如果你还不是 Medium 的会员——使用我的推荐链接,定期获取我和其他优秀 Medium 作者的新出版物更新。
阅读穆雷·吉林在 Medium 上的文章。亚马逊澳大利亚的业务风险分析师 | 热情的数据分析师和机器学习专家…
在 Python 中构建理解管道
原文:
towardsdatascience.com/building-comprehension-pipelines-in-python-ec68dce53d03
PYTHON 编程
理解管道是构建管道的一种 Python 特有的理念
·发表于Towards Data Science ·阅读时间 12 分钟·2023 年 2 月 17 日
--

理解管道能直接带你到达目标。图片来源于安妮卡·胡辛加在Unsplash
生成器管道提供了一种 Pythonic 的方式来创建软件管道,即操作链,其中每个操作(除了第一个)都将前一个操作的输出作为其输入:
这篇文章提出了一种优雅的方式来构建生成器管道
towardsdatascience.com
它们使你能够应用转换编程,如托马斯和亨特在他们伟大的书籍程序员的修炼中所描述的那样
来自维基百科,百科全书《程序员的修炼:从学徒到大师》是一本关于计算机的书…
en.wikipedia.org](https://en.wikipedia.org/wiki/The_Pragmatic_Programmer?source=post_page-----ec68dce53d03--------------------------------)
Python 中的典型生成器管道在管道的每一步都使用生成器。换句话说,管道的每一步都构建为一个生成器。Thomas 和 Hunt 讨论了通过管道操作符实现的管道,这在许多编程语言中都可以使用。虽然 Python 没有内置的管道操作符,但由于某些操作符可以在类定义中重载,因此可以很容易地创建它。我们可以在这个 [Pipe](https://github.com/JulienPalard/Pipe) Python 包中看到这一点,该包使用了|操作符。
在上述文章中,我展示了使用生成器来创建管道的每一步可能引入的视觉混乱,从而降低代码的可读性。此外,当管道的每次运行都很快时,这种方法的性能较差。因此,我提出了一种替代的、比经典生成器管道更具可读性的高效构建生成器管道的方法。该方法将函数组合与生成器结合使用。未来,我将向您展示如何使用管道操作符创建管道。
尽管如此,正如我在本文中所写的那样,生成器表达式是推导的一个特例。
学习列表推导(listcomps)、集合推导(setcomps)、字典推导等的复杂性。
[towardsdatascience.com
那么,为什么我们要把自己局限于生成器管道呢?为什么不考虑列表推导管道、字典推导管道或集合推导管道呢?
这个问题困扰了我一段时间,但我把它当作烦人的苍蝇在头顶嗡嗡作响,无法让我安宁,不停地嗡嗡嗡……最终,我放弃了拒绝这个想法,并决定至少提出讨论。大约两个月前,在一次愉快的冬季散步中,我带着我的两只狗走在森林里。一个美丽的冬天,雪、霜、强风——我和狗狗们在树林里,走在我最喜欢的小路上,思考(好吧,是我,不是狗)如何使用其他推导来构建管道,而不仅仅是生成器表达式。
本文是这次探讨的结果。我提出了一种将生成器管道推广到我称之为推导管道的概念,其中生成器管道只是一个特例。
生成器管道示例
为了保持一致性和清晰性,我将使用在上一篇文章中用到的相同生成器管道示例。请注意,我稍微修改了一些类型注解。生成器管道如下¹:
import math
from typing import Generator
# Type aliases
Number = int | float
PowerType = int | float
PipelineItems = Iterable[Number]
def double(x: Number) -> Number:
return x**2
def power(x: Number, n: PowerType) -> Number:
return x**n
def add(x: Number, y: Number) -> Number:
return x + y
def calculate(x: Number) -> Number:
x = power(x, 0.5)
x = double(x)
x = add(x, 12)
x = power(x, 2)
x = add(x, math.pi**0.5)
x = round(x, 2)
x = add(x, 75)
return x
def get_generator_pipeline(
items: PipelineItems,
) -> Generator[Number, None, None]:
"""Create generator pipeline applying calculate() to each item."""
return (calculate(x_i) for x_i in items)
get_generator_pipeline()函数——之前称为get_pipeline()——返回一个生成器管道;也就是说,一个生成器,它懒惰地(按需)计算items可迭代对象的后续元素的管道。我们可以用任何我们想要的方式评估生成器,例如,使用list():
>>> items = [1.12, 2.05, 1.122, -0.220002, 7.0036]
>>> pipeline = get_generator_pipeline(items)
>>> list(pipeline)
如果我们想得到一个列表,那么使用生成器表达式就没有意义——对应的列表推导式会更好!那么,为什么一开始就不使用列表推导管道而使用生成器管道呢?
什么时候使用生成器管道?什么时候不使用?
生成器管道有一个重要的优势,这与生成器的主要优势相同:延迟评估。当items可迭代对象很大,或者管道的步骤生成大型对象时,生成器管道将帮助我们避免内存不足的问题。
但是,如果items可迭代对象很短且输出可迭代对象占用的内存不多,那我们为何要担心内存问题呢?更何况,我们知道生成器表达式可能比对应的列表推导式要慢——唯一的例外是,当项的数量(或大小)大到无法在内存中全部保留和处理时。
考虑以下这个与上面示例相当不同的例子:
-
我们有
paths,一个包含文件路径的列表。 -
在管道的每一步中,从一个路径读取一个文本文件,并对文本进行处理。
-
因此,返回的是处理后的文本的可迭代对象。
在这种情况下,输出可迭代对象将远大于输入可迭代对象。因此,当路径数量庞大时,生成器管道在这里效果最好——因为返回一个长文本的长列表将会非常低效,甚至可能无法实现。我们需要管道返回一个生成器,因此需要生成器管道。
现在,想象另一个管道。我们有相同的路径可迭代对象,但我们对文本的处理不同了。之前,我们返回长文本。现在,我们只需要每个文本中是否包含“Python”这个词的信息;因此,对于每个文本,我们只需要一个布尔值。对于路径列表,我们将得到一个长度相同的布尔值列表。生成器管道在这里的优势是什么?没有。
更重要的是,仅返回布尔值几乎没有意义:很难将特定的值与相应的文本联系起来。因此,最好返回一个以路径为键,布尔值为值的字典。这将使管道基于字典。或者,我们可以返回一个包含文本中“Python”单词的路径列表;然而,这种输出会忽略其他路径,从而丢失部分信息——有时我们可能需要这些信息。
建立理解管道
我们终于来到了本文的主题:理解管道以及如何构建它们。上述示例展示了我们如何决定是否需要生成器管道或其他类型的管道。除了生成器管道,我们还可以构建
-
列表理解管道,即 listcomp 管道
-
集合理解管道,即 setcomp 管道
-
字典理解管道,即 dictcomp 管道
接下来,我将使用这三种其他类型的理解管道重写使用calculate()函数的生成器管道。为了完整性,我将重复生成器管道的代码。我还会改变typing的导入,因为这次我们需要比之前更多的类型,之前我们只创建了一个生成器管道。
from typing import Dict, Generator, Iterable, List, Set
def get_generator_pipeline(
items: PipelineItems,
) -> Generator[Number, None, None]:
"""Create generator pipeline applying calculate() to each item."""
return (calculate(x_i) for x_i in items)
def get_listcomp_pipeline(items: PipelineItems) -> List[Number]:
"""Create listcomp pipeline applying calculate() to each item."""
return [calculate(x_i) for x_i in items]
def get_setcomp_pipeline(items: PipelineItems) -> Set[Number]:
"""Create setcomp pipeline applying calculate() to each item."""
return {calculate(x_i) for x_i in items}
def get_dictcomp_pipeline(items: PipelineItems) -> Dict[Number, Number]:
"""Create dictcomp pipeline using calculate() for items.
Items are dict keys with calculate(item) being
the corresponding value.
"""
return {x_i: calculate(x_i) for x_i in items}
def get_dictcomp_pipeline_str(items: PipelineItems) -> Dict[str, Number]:
"""Create dictcomp pipeline using calculate() for items.
str(item) are dict keys with calculate(item) being
the corresponding value.
"""
return {str(x_i): calculate(x_i) for x_i in items}
我使用了特定版本的字典管道,但请注意,我们也可以构建其他版本,具体取决于我们希望将什么用作字典的键。稍后我们将看到一个示例。
接下来,我们将总结这些基本类型的管道,包括生成器管道。请记住,它们在输出上有所不同,因为它们都可以接受相同类型的输入——任何可迭代对象都可以。
生成器管道
-
以任何可迭代对象(
items)作为输入。 -
返回一个生成器作为管道。
-
可以使用任何形式和复杂度的生成器表达式(例如,它可以包含多个级别的
if过滤器和for循环)。 -
可以按需求值。
列表理解管道,也称为 listcomp 管道
-
以任何可迭代对象(
items)作为输入。 -
运行列表理解作为管道,因此返回一个列表。
-
可以使用任何复杂度的列表理解。
-
是贪婪求值的。
集合理解管道,也称为 setcomp 管道
-
以任何可迭代对象(
items)作为输入。 -
运行集合理解作为管道,因此返回一个集合。
-
可以使用任何复杂度的集合理解。
-
由于最终输出是一个集合,它将包含唯一的结果。因此,如果可迭代对象中的两个或更多项返回相同的输出,它们的重复实例将被跳过。
-
是贪婪求值的。
字典理解管道,也称为 dictcomp 管道
-
以任何可迭代对象(
items)作为输入。 -
运行字典理解作为管道,因此返回一个字典。
-
可以使用任何复杂度的字典理解。
-
对于特定的
item,返回一个键值对;键不必是item——它可以是从item或其任何处理结果中得到的任何东西。 -
由于管道返回一个字典,你应该使用唯一的键;否则,相同键的结果将被覆盖,最后一个键值对将被保留。
-
是贪婪求值的。
示例
在这里,让我们看看上述管道的行为。我们将对以下可迭代对象进行操作:
>>> items = (1, 1.0, 10, 50.03, 100)
我以 doctests 的形式展示这些示例。你可以在以下Towards Data Science文章中阅读更多关于这个用于文档测试的出色 Python 内置包的内容:
## Python 文档测试使用 doctest:简单的方法
doctest 允许进行文档、单元和集成测试,以及测试驱动开发。
[towardsdatascience.com
在文章末尾的附录中,你会找到这个练习的完整脚本。
生成器管道
>>> gen_pipeline = get_generator_pipeline(items)
>>> gen_pipeline # doctest: +ELLIPSIS
<generator object get_generator_pipeline.<locals>.<genexpr> at 0x7...>
正如预期的那样,生成器管道返回一个生成器。因此,目前我们无法看到输出,要做到这一点,我们需要评估其值。如何做取决于管道和解决的问题。在这里,我们将使用一个简单的 for 循环:
>>> for i in gen_pipeline:
... print(i)
245.77
245.77
560.77
3924.49
12620.77
请记住,即使 gen_pipeline 是使用生成器管道创建的,它仍然是一个普通的生成器。作为生成器,在评估之后(我们在上面的 for 循环中做了),它是空的。它仍然存在,但你不能再用它来查看输出:
>>> gen_pipeline # doctest: +ELLIPSIS
<generator object get_generator_pipeline.<locals>.<genexpr> at 0x7...>
>>> next(gen_pipeline)
Traceback (most recent call last):
...
StopIteration
Listcomp 管道
>>> list_pipeline = get_listcomp_pipeline(items)
>>> list_pipeline
[245.77, 245.77, 560.77, 3924.49, 12620.77]
Listcomp 管道会贪婪地评估管道,因此在创建时就可以看到结果。你可以根据需要多次这样做,这与上面的生成器管道不同。
像之前一样,我们可以看到前两个值完全相同。这是可以预期的,因为 items 的前两个元素是相同的……或者不是?第一个是整数 1,而第二个是浮点数 1.0。理论上,这些 不是 相同的对象,因为它们的类型不同。然而,Python 将它们视为相等:
>>> 1 == 1.0
True
那么,setcomp 和 dictcomp 管道会有什么表现呢?我们将在下文中看到。
Setcomp 管道
>>> set_pipeline = get_setcomp_pipeline(items)
>>> set_pipeline
{560.77, 3924.49, 245.77, 12620.77}
哈!注意虽然 items 包含五个元素,但上述输出仅包含四个。这并不意外——正如我们之前看到的,Python 将 1 和 1.0 视为相等,因此对这两个值评估 calculate(x) 的结果是相同的。由于它们是相同的,结果集合中只包含一个输出值,即 245.77。
使用集合和 setcomp 管道时请记住这一点。因此,当你希望实现这种行为时——换句话说,当你希望只保留唯一结果时——请使用 setcomp 管道。
Dictcomp 管道
>>> dict_pipeline = get_dictcomp_pipeline(items)
{1: 245.77, 10: 560.77, 50.03: 3924.49, 100: 12620.77}
与集合一样,我们得到了结果字典的四个元素。如你所见,当你希望在字典中使用 1 和 1.0 作为键时,它们会合并成一个键,在我们的例子中是 1。如果这正是你想要的结果,那么你完成了。
如果你需要它们两个怎么办?你可以创建字符串键。例如,Python 是否将 str(1) 和 str(1.0) 视为不同的?让我们来看看:
>>> str(1) != str(1.0)
True
是的,确实如此!我们需要重新定义管道函数,然后:
def get_dictcomp_pipeline_str(items: PipelineItems) -> Dict[str, Number]:
"""Create dictcomp pipeline using calculate() for items.
str(item) are dict keys with calculate(item) being
the corresponding value.
"""
return {str(x_i): calculate(x_i) for x_i in items}
让我们看看这个新的 dictcomp 管道如何运作:
>>> dict_str_pipeline = get_dictcomp_pipeline_str(items)
>>> dict_str_pipeline
{'1': 245.77, '1.0': 245.77, '10': 560.77, '50.03': 3924.49, '100': 12620.77}
结果字典包含五个元素,正如我们所期望的那样。
结论
在这篇文章中,我提出了将生成器管道的一般化扩展到理解管道。虽然生成器表达式通常用于创建管道,但生成的生成器管道是理解管道的一个特例。当你创建一个管道时,请考虑哪种类型的管道最能代表你的需求——并使用它。不必因为“生成器管道”这个术语在 Python 社区中很常见而仅仅坚持使用生成器管道。你可以自由使用任何适合你目标的方法。
在这篇文章中,我们使用了简单的示例。我这样做是有目的的:这种简洁性帮助我们集中关注本文的主要话题——理解管道。未来,我计划展示更多代表现实生活场景的高级示例。
请注意,创建最终理解的函数——在我们的示例中,get_generator_pipeline()、get_listcomp_pipeline()、get_setcomp_pipeline()、get_dictcomp_pipeline()和get_dictcomp_pipeline_str()——仅创建管道的最后一步。然而,实际的管道隐藏在这些函数调用的函数中;在我们的例子中,这是calculate()函数。我们暂时回到这个函数:
def calculate(x: Number) -> Number:
x = power(x, .5)
x = double(x)
x = add(x, 12)
x = power(x, 2)
x = add(x, math.pi**.5)
x = round(x, 2)
x = add(x, 75)
return x
你明白我的意思吗?我们的管道由函数power()、double()、add()、再次power()、再次add()、round()和再次add()组成。这里应用了所有的管道步骤,创建输出的函数只是以适合你需求的方式调用这个函数。
记住,如果基本函数(前面句子中列出的前四个函数)不符合你的需求,你可以创建一个新函数,就像我们在定义get_dictcomp_pipeline_str()函数时所做的那样。这个例子表明,我们不受限于理解管道的基础版本:只要正确,你可以做任何你想做的事情。
脚注
¹ 如果你使用的是低于 3.10 版本的 Python,代码将无法运行。这是因为typing的联合运算符|可以替代Union,它是在 Python 3.10 中添加的。因此,如果你使用的是旧版本 Python,请替换这两行:
Number = int | float
PowerType = int | float
使用这三行:
Number = Union[int, float]
PowerType = Union[int, float]
这将有效。
附录
以下是本文中使用的脚本的完整代码。如上脚注所述,在旧版本的 Python 中,你可能需要将int | float替换为Union[int, float],当然在此之前需要从typing中导入Union。
import math
from typing import Dict, Generator, Iterable, List, Set
Number = int | float
PowerType = int | float
PipelineItems = Iterable[Number]
def double(x: Number) -> Number:
return x**2
def power(x: Number, n: PowerType) -> Number:
return x**n
def add(x: Number, y: Number) -> Number:
return x + y
def calculate(x: Number) -> Number:
x = power(x, 0.5)
x = double(x)
x = add(x, 12)
x = power(x, 2)
x = add(x, math.pi**0.5)
x = round(x, 2)
x = add(x, 75)
return x
def get_generator_pipeline(
items: PipelineItems,
) -> Generator[Number, None, None]:
"""Create generator pipeline applying calculate() to each item."""
return (calculate(x_i) for x_i in items)
def get_listcomp_pipeline(items: PipelineItems) -> List[Number]:
"""Create listcomp pipeline applying calculate() to each item."""
return [calculate(x_i) for x_i in items]
def get_setcomp_pipeline(items: PipelineItems) -> Set[Number]:
"""Create setcomp pipeline applying calculate() to each item."""
return {calculate(x_i) for x_i in items}
def get_dictcomp_pipeline(items: PipelineItems) -> Dict[Number, Number]:
"""Create dictcomp pipeline using calculate() for items.
Items are dict keys with calculate(item) being the corresponding value.
"""
return {x_i: calculate(x_i) for x_i in items}
def get_dictcomp_pipeline_str(items: PipelineItems) -> Dict[str, Number]:
"""Create dictcomp pipeline using calculate() for items.
str(item) are dict keys with calculate(item) being
the corresponding value.
"""
return {str(x_i): calculate(x_i) for x_i in items}
if __name__ == "__main__":
items = (1, 1.0, 10, 50.03, 100)
gen_pipeline = get_generator_pipeline(items)
list_pipeline = get_listcomp_pipeline(items)
set_pipeline = get_setcomp_pipeline(items)
dict_pipeline = get_dictcomp_pipeline(items)
dict_str_pipeline = get_dictcomp_pipeline_str(items)
# Generator pipeline
# Note that we need to evaluate it to see the output,
# hence the for loop.
print(gen_pipeline)
for i in gen_pipeline:
print(i)
# Listcomp pipeline
print(list_pipeline)
# Setcomp pipeline
print(set_pipeline)
# Dictcomp pipeline
print(dict_pipeline)
# Dictcomp pipeline with strings as keys
print(dict_str_pipeline)
在亚马逊网络服务上构建生成型 AI 应用——我的第一次经历
48 小时黑客马拉松:Amazon Bedrock & SageMaker
·发表于 Towards Data Science ·阅读时间 11 分钟·2023 年 9 月 22 日
--

图片由以色列·安德拉德提供 (Unsplash)
大公司对生成型 AI 该如何处理还不完全确定,但他们想要做一些事情。
一些人正在通过内部黑客马拉松探索这项技术。
更新:我现在在 YouTube 上发布分析教程。
作为澳大利亚“四大”银行之一的工程师和数据科学家,在过去一个月里,我被卷入了三次这些令人兴奋的活动中。
为什么是黑客马拉松?
他们作为一种绝佳方式,让公司的知识工作者——无论是技术还是非技术人员——头脑风暴生成型 AI 应用场景,测试市场上可用的 AI 工具,并快速制作一些工作原型供决策者审查。
这对你有什么好处?
-
脱离日常工作。(老板,这只是开玩笑!)
-
提升你的创业和创新技能。
-
生成型 AI 是一种颠覆性技术。赶上潮流,否则被抛在后面。
-
在不同的云服务供应商处获得实践经验,例如 AWS 与 Microsoft。
最后一项内容很重要。
不是每天都有公司付钱让你探索最新的企业生成型 AI 工具。
在这篇文章中,我想与我的分析师、工程师和数据科学家同行分享我玩转亚马逊网络服务(AWS) AI 工具栈的经验。

大型模型预示着机器学习的范式转变。作者提供的图片
AWS 的友善团队为我们黑客马拉松的参与者提供了以下资源:
-
AWS SageMaker JumpStart 用于访问文本和图像基础模型;
-
在 AWS Bedrock 上的Guru 来调整我们基于公司数据的聊天机器人;
-
Streamlit 用于快速部署概念验证应用来推销我们的产品。
让我们深入了解吧!
AWS 生成 AI 堆栈
生成 AI 需要大量的数据集和巨大的计算能力来训练这些数据上的巨大神经网络,这使得公共云成为一个理想的平台选择。
主要的公共云服务提供商,如亚马逊、谷歌和微软,正在激烈争夺开发者、企业和研究人员,通过提供训练和微调生成式 AI 模型的能力,并消费预训练的基础模型-即服务。

一系列后端的神奇技术支持生成 AI 产品。图片来源:作者
微软由于与OpenAI 的幸运合作,领先于其竞争对手——这是一项真正的战略智慧。
OpenAI 的旗舰产品 ChatGPT 在 2023 年的飞速崛起震惊了谷歌。CEO Sundai Pichai 认识到这对谷歌现有商业模式构成了生死威胁,发出了红色警报,呼吁全员动员,将追赶作为首要任务。
与此同时,亚马逊稍晚进入这个领域,但正在迅速追赶。

主要公共云服务提供商的 AI 堆栈。图片来源:Ivana Tilca (合理使用)
AWS 在生成 AI 的投资主要集中在三个领域:
-
AWS Bedrock 是一个完全托管的平台,旨在通过 API 轻松消费基础模型。亚马逊与AI21Labs、Anthropic 和Stability AI等生成 AI 初创公司合作,提供通过亚马逊Bedrock API 访问的文本和图像基础模型。Bedrock 被定位为微软Azure OpenAI平台的竞争对手——被认为是公共云中最成熟和可靠的生成 AI 平台。
-
AWS SageMaker 是一个提供用户访问、定制和部署机器学习模型的环境。亚马逊最近扩展了对基础模型的支持,允许用户利用和微调一些最受欢迎的开源模型。特别地,通过官方合作伙伴关系,它提供了对Hugging Face 模型广泛目录的轻松访问。
-
Amazon Titan,是由亚马逊内部研究人员精心打造的一系列基础模型。预计TITAN将整合各种亚马逊服务的模型,如Alexa、CodeWhisperer、Polly、Rekognition和其他 AI 服务。
背景介绍完毕,我将深入探讨我们在黑客马拉松中使用 AWS 工具的经验。
AWS SageMaker JumpStart
简而言之,SageMaker使我们能够在 AWS 上部署基础模型,并通过 API 在Jupyter Notebooks中访问这些模型。
在黑客马拉松开始时,我们获得了访问AWS Workshop Studio的凭证,这是亚马逊用于培训和运行多天 AWS 研讨会的沙箱云环境。
实际上,我们的Workshop Studio环境配备了一个完整的 GenAI 课程,但考虑到我们只有 48 小时可以操作,AWS 的讲师为我们提供了一个快速的速成课程,帮助我们访问构建工作 GenAI 产品所需的基本内容。
SageMaker配备了一项名为JumpStart的功能,它正是这样做的:通过提供预训练的基础模型来快速启动 AI 和 ML 项目,使项目能够迅速起步。对于那些希望尽快开始而无需重新发明轮子的人来说,这是一个福音——考虑到从头训练大型模型(LM)所需的技术和计算工作,这一提议极具价值。
AWS 教会我们如何使用JumpStart访问一些 AWS 预训练的文本到文本基础模型,通过一种数据科学家熟悉的界面:Jupyter Notebooks。这一切都直接集成在SageMaker中。
我们获得了包含所有所需代码的笔记本,以使用文本到文本模型,包括亚马逊的Titan和 Meta 的Llama。
我们只需插入 AWS 提供的 API 密钥。很简单!

作者提供的图像
另一种受欢迎的 GenAI“类型”是文本到图像模型,例如 OpenAI 的DALL-E、Midjourney Inc 的Midjourney和 Stability AI 的开源Stable Diffusion。
再次通过熟悉的Jupyter Notebooks界面,我们迅速开始玩弄开源的图像合成 GenAI 模型,特别是稳定扩散模型。

作者提供的图像
快速说明: Stable Diffusion是 Stability AI 推出的产品,而“稳定扩散模型”实际上是一个市场营销术语,指的是将潜在扩散模型(解决了 GAN 的缺陷)与称为 CLIP 嵌入的技术相结合的模型。DALL-E V2、Midjourney V5 和Stable Diffusion 都使用稳定扩散模型,这些模型目前是图像合成的最前沿技术。
有关文本到文本和文本到图像模型的技术深入,请参阅我的文章 这里 和 这里。
总的来说,SageMaker 使我们能够快速利用 GenAI,因为…
-
AWS 已经为我们在 SageMaker JumpStart 上部署了 6 到 7 个 LMs;
-
它们可以通过熟悉的 Jupyter Notebooks 访问;
-
我们被提供了立即使用这些模型所需的 Python 代码和 API 密钥。
这意味着那些技术背景不特别强的分析师可以很快开始使用 GenAI,并将更多宝贵的黑客马拉松时间用于用例构思、构建和推介。
Streamlit & Cloud9
Streamlit.io 是一个开源的前端应用程序。
对于黑客马拉松参与者而言,Streamlit 是一种快速创建可以与我们构建的 Gen-AI 模型互动的可视化应用的好方法。
将我们的模型与Streamlit连接的最简单方法是使用亚马逊在线 IDE AWS Cloud9 进行编码,并将其部署到 AWS,更确切地说是 Amazon EC2。
为了揭开术语的面纱,Cloud9 是亚马逊自己提供的基于云的开发环境,而 EC2 (Elastic Compute Cloud) 是 AWS 颇受欢迎的云环境,个人和企业可以在其中部署应用,应用可以即时扩展到数百万用户。

图片由作者提供
在黑客马拉松中,我们学习了如何使用 Streamlit 指南 这里 快速部署 Gen-AI 应用,这些指南是由我们的一位 AWS 教练编写的。
这使我们能够快速为我们的文本到文本模型创建一个用户友好的移动前端。
应用用户可以安全地上传公司文件,如政策文件,这些文件随后会调整底层 GenAI 模型,使其能够立即模拟经验丰富的公司员工的专业知识。
就像那些从 The Matrix 中瞬间下载的内容一样。

图片由作者提供
这种调整 Gen-AI 模型的方法被称为 RAG 方法,即…
-
获取公司的企业知识库;
-
增强基础模型与这些公司特定的数据;
-
生成现在为公司量身定制的人类般的响应。
RAG 通过公司自己的数据增强基础模型的训练,从而获得最新和最可靠的信息。
企业拥抱 RAG,因为它目前在解决 AI 幻觉 方面排名前列,这对必须确保 AI 向重要客户和员工提供可靠信息的大公司来说可能会带来重大挑战。
亚马逊 Bedrock 上的 Guru Chatbot
AWS 团队还让我们访问了 Guru,这是一个集成到亚马逊 Bedrock 中的 ChatGPT 风格的机器人。
Guru 具有处理上传文档的能力,通过 RAG 方法进行 AI 调优,成为公司的专家,并根据上传的文档回应用户查询。
例如,我上传了我们公司详细的 2022 年年度股东大会报告,这是一份厚达 308 页的文件,我绝对没有时间阅读 或者甚至适当地浏览。相反,我把它交给了 Guru,它轻松地消化了报告,并迅速为我提供了一个简明的总结。

图片来源:作者
亚马逊 Bedrock 的优势在于其管理方式,通过 API 便捷地访问预训练的基础模型。
选择“我希望哪个 AI”来总结我上传的文档非常简单。
我尝试了所有的模型:亚马逊 Titan、Meta 的 Llama 以及其他的,看到它们的反应非常有趣。
例如,‘更大的’模型听起来更好——更“像人类”——但延迟较高。确实,选择使用哪个 AI 模型并不总是倾向于选择最大和最强大的大语言模型;一个调优良好的、小型模型,能够提供亚秒级响应,往往更为合适。
在模型复杂性和用户体验之间存在微妙的平衡。
简而言之,Guru 是将公司数据与 GenAI 聊天机器人集成的最简单方法。不需要 Jupyter Notebooks,也无需构建或训练模型,一切——包括应用程序前端——都已部署并准备好使用。
结果
那么我们这些谦虚的黑客马拉松参与者表现如何呢?
在那 48 小时内,我们构建了什么,并向我们专注的首席技术官展示了?
我们四人团队模拟了一个可以发送 个性化营销优惠 的 AI。
在数字时代,大规模提供超个性化的产品和服务是数据驱动型组织的 圣杯。这需要大量的数据,提供“360 度视角”的客户信息,以及一个足够强大的数据和高级分析平台,以实时处理和行动这些见解。
我们利用亚马逊的 SageMaker JumpStart 预训练基础模型,将一些虚拟客户见解转化为个性化邮件,其中还包括专门为每位客户量身定制的 AI 生成的图片。目标是建立情感联系,推动销售,将体验从平凡普通提升到独特非凡。
忘掉那些千篇一律的“用我们来再融资你的抵押贷款!”广告,广告中有典型的白色栅栏、狗和 2.4 个孩子。在这里,我们希望传达一个能打动客户独特情况的提案,配有 AI 生成的完美房子。
等等,这不是有点让人毛骨悚然吗?
另外,如果我们获得的见解错误,最终冒犯了客户怎么办?
确实,事情可能会很糟糕。我们的 AI 会足够准确、公平和安全吗?我们拥有可信的 AI 吗?
然后就是隐私、安全和关于 GenAI真正提供价值的考虑。
这就是为什么我们将虚拟客户见解输入到我们的 AI 中,而不是 实际生产客户数据。
我们绝对不允许将敏感客户数据在生产环境中发送给公共云服务提供商,除非有一系列的安全控制。因此使用了虚拟数据。
我们很快意识到,GenAI 在我们的用例中的价值并不在于生成客户见解。那是我们的高级分析平台最擅长的——数据处理。GenAI 的超级能力在于以人性化的方式传达这些见解。
最终的话
生成性 AI 是新的,数据驱动的组织极其渴望将这一颠覆性技术融入他们的业务中,不知怎么的。
黑客马拉松是公司一举多得的好方法:
-
挖掘最佳用例;
-
评估合适的云服务供应商;
-
识别创新员工。
我在亚马逊 GenAI 技术栈上的经验教会我,Gen-AI 时代的创新全在于迅速组装合适的现成 AI 工具以创造价值。
我们获得了实际操作经验,辨别 GenAI 在可信的方式下真正增加价值的地方,以及如何在保持安全性和合规性的同时无缝集成这项技术到公司基础设施中——这些考虑是采纳的成败之关键。
各个参与者都在 Amazon Bedrock 或 SageMaker 中快速拼凑出一些粗糙的东西,尽快向客户展示,并进行迭代。
他们说,快速失败并从失败中前进。
猜猜怎么着:我即将参加一个微软黑客马拉松,我一定会及时更新进展。真的很期待玩玩Azure OpenAI!
在Twitter和 YouTube 这里、这里和这里找到我。
我受欢迎的 AI、ML 和数据科学文章
-
AI 与机器学习:快速入门 — 这里
-
机器学习与机制建模 — 这里
-
数据科学:现代数据科学家的新技能 — 这里
-
生成性人工智能:大公司如何争相采用 — 这里
-
ChatGPT 与 GPT-4:OpenAI 如何赢得自然语言理解的战争 — 这里
-
生成性 AI 艺术:DALL-E、Midjourney 和 Stable Diffusion 解析 — 这里
-
超越 ChatGPT:寻找真正的智能机器 — 这里
-
现代企业数据战略解析 — 这里
-
从数据仓库和数据湖到数据网格 — 这里
-
从数据湖到数据网格:最新架构指南 — 这里
-
Azure Synapse Analytics 实战:7 个用例解析 — 这里
-
云计算基础知识:如何利用云服务助力您的业务 — 这里
-
数据仓库与数据建模 — 快速速成课程 — 这里
-
数据产品:为分析构建坚实基础 — 这里
-
数据民主化:5 种‘数据普及’战略 — 这里
-
数据治理:分析师常见的 5 个痛点 — 这里
-
数据讲故事的力量 — 卖故事,而非数据 — 这里
-
数据分析入门:谷歌方法 — 这里
-
Power BI — 从数据建模到令人惊艳的报告 — 这里
-
回归分析:使用 Python 预测房价 — 这里
-
分类:使用 Python 预测员工流失 — 这里
-
Python Jupyter Notebooks 与 Dataiku DSS —— 在这里
-
受欢迎的机器学习性能指标解释 —— 在这里
-
在 AWS 上构建 GenAI —— 我的第一次体验 —— 在这里
-
COVID-19 的数学建模与机器学习 —— 在这里
-
工作的未来:在 AI 时代你的职业安全吗 —— 在这里
在 Python 中构建互动数据可视化:Plotly 入门
探索用于数据分析和机器学习的互动可视化的强大功能
·发布在Towards Data Science ·阅读时间 6 分钟·2023 年 7 月 19 日
--

数据可视化是数据专业人员最重要的任务之一。它实际上帮助我们理解数据,并提出更多问题以便进一步调查。
但是数据可视化不仅仅是我们在探索性数据分析阶段必须完成的任务。我们还可能需要展示数据,通常是向观众展示,以帮助他们得出结论。
在 Python 中,我们通常使用matplotlib和seaborn作为绘图库。
然而,有时我们可能需要一些互动的可视化。在某些情况下,为了更好地理解数据;在其他情况下,为了更好地展示我们的解决方案。
在这篇文章中,我们将讨论plotly,这是一个用于创建互动可视化的 Python 库。
什么是 Plotly?
正如我们可以在他们的网站上阅读到的:
Plotly 的 Python 绘图库可以创建互动的、出版质量的图表。包括如何制作折线图、散点图、面积图、条形图、误差条、箱线图、直方图、热图、子图、多轴图、极坐标图和气泡图的示例。
Plotly.py 是免费且开源的,你可以查看源代码、报告问题或在 GitHub 上贡献。
所以,Plotly 是一个免费的开源 Python 库,用于制作互动可视化。
正如我们在他们的网站上看到的,它为我们提供了创建不同领域图表的可能性:AI/ML、统计、科学、金融等。
由于我们对机器学习和数据科学感兴趣,我们将展示一些与这些领域相关的图表以及如何在 Python 中创建它们。
最后,要安装它,我们需要输入:
$ pip install plotly
1. 互动气泡图
Plotly 的一个有趣且有用的功能是可以创建互动气泡图。
在气泡图的情况下,气泡有时会相交,使数据难以读取。如果图表是互动的,我们就可以更轻松地读取数据。
让我们看一个例子:
import plotly.express as px
import pandas as pd
import numpy as np
# Generate random data
np.random.seed(42)
n = 50
x = np.random.rand(n)
y = np.random.rand(n)
z = np.random.rand(n) * 100 # Third variable for bubble size
# Create a DataFrame
data = pd.DataFrame({'X': x, 'Y': y, 'Z': z})
# Create the scatter plot with bubble size with Plotly
fig = px.scatter(data, x='X', y='Y', size='Z',
title='Interactive Scatter Plot with Bubble Plot')
# Add labels to the bubbles
fig.update_traces(textposition='top center', textfont=dict(size=11))
# Update layout properties
fig.update_layout(
xaxis_title='X-axis',
yaxis_title='Y-axis',
showlegend=False
)
# Display the interactive plot
fig.show()
然后我们得到:

我们编写了互动气泡图。图片由Federico Trotta提供。
因此,我们使用NumPy创建了一些数据,并将它们存储在一个Pandas数据框中。然后,我们通过方法px.scatter()创建了互动图,方法从数据框中检索数据,并指定标题(与Matplotlib不同,后者将标题插入图表创建方法之外)。
2. 互动相关矩阵
我有时面临的任务之一是正确可视化相关矩阵。事实上,当我们有大量数据时,这些矩阵有时难以阅读和可视化。
解决问题的一种方法是使用 Plotly 创建互动可视化。
为了这个目的,让我们创建一个具有 10 列的 Pandas 数据框,并使用 Plotly 创建一个互动相关矩阵:
import pandas as pd
import numpy as np
import plotly.figure_factory as ff
# Create random data
np.random.seed(42)
data = np.random.rand(100, 10)
# Create DataFrame
columns = ['Column' + str(i+1) for i in range(10)]
df = pd.DataFrame(data, columns=columns)
# Round values to 2 decimals
correlation_matrix = df.corr().round(2)
# Create interactive correlation matrix with Plotly
figure = ff.create_annotated_heatmap(
z=correlation_matrix.values,
x=list(correlation_matrix.columns),
y=list(correlation_matrix.index),
colorscale='Viridis',
showscale=True
)
# Set axis labels
figure.update_layout(
title='Correlation Matrix',
xaxis=dict(title='Columns'),
yaxis=dict(title='Columns')
)
# Display the interactive correlation matrix
figure.show()
然后我们得到:

我们创建的互动相关矩阵。图片由Federico Trotta提供。
因此,我们可以通过方法ff.create_annotated_map()以非常简单的方式创建互动相关矩阵。
3. 互动 ML 图表
在机器学习中,我们有时需要图形化地比较数量。在这些情况下,有时阅读我们的图表会很困难。
典型的情况是 ROC/AUC 曲线,其中我们比较不同 ML 模型的性能。事实上,有时这些曲线会相交,我们无法正确地可视化它们。
为了改进我们的可视化,我们可以使用 Plotly 创建一个 ROC/AUC 曲线,如下所示:
import pandas as pd
import numpy as np
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.svm import SVC
from sklearn.metrics import roc_curve, auc
import plotly.graph_objects as go
# Create synthetic binary classification data
X, y = make_classification(n_samples=1000, n_features=20, random_state=42)
# Scale the data using StandardScaler
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# Split the data into train and test sets
X_train, X_test, y_train, y_test = train_test_split(X_scaled, y, test_size=0.2, random_state=42)
# Initialize the models
knn = KNeighborsClassifier()
rf = RandomForestClassifier()
dt = DecisionTreeClassifier()
svm = SVC(probability=True)
# Fit the models on the train set
knn.fit(X_train, y_train)
rf.fit(X_train, y_train)
dt.fit(X_train, y_train)
svm.fit(X_train, y_train)
# Predict probabilities on the test set
knn_probs = knn.predict_proba(X_test)[:, 1]
rf_probs = rf.predict_proba(X_test)[:, 1]
dt_probs = dt.predict_proba(X_test)[:, 1]
svm_probs = svm.predict_proba(X_test)[:, 1]
# Calculate the false positive rate (FPR) and true positive rate (TPR) for ROC curve
knn_fpr, knn_tpr, _ = roc_curve(y_test, knn_probs)
rf_fpr, rf_tpr, _ = roc_curve(y_test, rf_probs)
dt_fpr, dt_tpr, _ = roc_curve(y_test, dt_probs)
svm_fpr, svm_tpr, _ = roc_curve(y_test, svm_probs)
# Calculate the AUC (Area Under the Curve) for ROC curve
knn_auc = auc(knn_fpr, knn_tpr)
rf_auc = auc(rf_fpr, rf_tpr)
dt_auc = auc(dt_fpr, dt_tpr)
svm_auc = auc(svm_fpr, svm_tpr)
# Create an interactive AUC/ROC curve using Plotly
fig = go.Figure()
fig.add_trace(go.Scatter(x=knn_fpr, y=knn_tpr, name='KNN (AUC = {:.2f})'.format(knn_auc)))
fig.add_trace(go.Scatter(x=rf_fpr, y=rf_tpr, name='Random Forest (AUC = {:.2f})'.format(rf_auc)))
fig.add_trace(go.Scatter(x=dt_fpr, y=dt_tpr, name='Decision Tree (AUC = {:.2f})'.format(dt_auc)))
fig.add_trace(go.Scatter(x=svm_fpr, y=svm_tpr, name='SVM (AUC = {:.2f})'.format(svm_auc)))
fig.update_layout(title='AUC/ROC Curve',
xaxis=dict(title='False Positive Rate'),
yaxis=dict(title='True Positive Rate'),
legend=dict(x=0.7, y=0.2))
# Show plot
fig.show()
然后我们得到:

我们创建的 AUC/ROC 曲线。图片由Federico Trotta提供。
因此,我们通过方法add.trace(go.Scatter)为我们使用的每个 ML 模型(KNN、SVM、决策树和随机森林)创建了散点图。
这样,可以轻松显示曲线自交点的详细信息和区域值。
结论
在这篇文章中,我们简要介绍了 Plotly,并展示了如何利用它在数据分析和机器学习中实现更好的可视化。
正如我们所看到的,这是一个低代码库,确实帮助我们更好地可视化数据,从而改善结果。

Federico Trotta
嗨,我是费德里科·特罗塔,一名自由职业技术写作人员。
想和我合作吗?联系我。
使用 Python 构建互动数据可视化 — 叙述的艺术
简介指南
Seaborn、Bokeh、Plotly 和 Dash,有效地传达数据洞察
·发表于 Towards Data Science ·阅读时间 16 分钟·2023 年 6 月 4 日
--

掌握叙述的艺术对数据科学家很重要,但对数据分析师尤为关键。
与那些不熟悉数据的人分享数据洞察和亮点,特别是那些可能没有技术背景的人,是数据分析过程中的重要部分。
如果他们不了解你所说的内容或没有被吸引,他们不在乎你是否在数据清洗和转换方面表现最好。
因此,可视化是最终叙述的一部分,它可以说是展示任何事实的最佳方式——这就是为什么每个人都在使用它们。
此外,像 Tableau 或 Power BI 这样的工具因其能够轻松创建互动仪表板而日益受到欢迎。商业人士通常会被带有图表和颜色的酷炫仪表板所震撼,因此他们已开始将其作为招聘要求。
今天,我将与大家分享一些 Python 选项,用于为那些不能或不喜欢/不想使用上述特定数据可视化工具的人创建互动式可视化。
在完成所有分析之后,你将不再需要放弃 Python——你还可以用它来以视觉形式分享你的见解。
更具体地说,我将讨论四个库:Seaborn、Bokeh、Plotly 和 Dash。
因为深入探讨所有这些内容可能会导致帖子极其冗长,所以我会尽量简洁,并重点展示它们能做些什么。在未来的帖子中,我可能会更详细地介绍其中一些(或所有),但会逐一进行。
今天的目录:
-
首先是数据
-
Seaborn
-
Bokeh
-
Plotly
-
Dash
-
讲述故事
你可以在这篇文章末尾的 资源 部分找到整个代码的链接。
那么我们开始吧。
1. 首先是数据
如果我们没有故事可讲,那我们就无法讲故事,对吧?
由于我喜欢营养和健康领域,我将使用来自世界卫生组织 (WHO) 的公开数据。具体来说是 成人超重的流行率,BMI >= 25(年龄标准化估计)(%)[1]。
我们的目标是分析数据并查看我们能发现什么。它包含了从 1975 年到 2016 年按性别、国家和年份划分的流行率值。
免责声明:结果不会让你惊讶,因为这是一个广泛讨论的话题。但我们今天不是来发现新东西的。
我将使用笔记本来简化操作,但请记住,我们即将创建的图表也可以嵌入到网站中——这取决于我们使用的库——这才是真正的好处所在。
我稍微清理了一下数据,使其简单并为接下来的步骤做好准备。这里是代码片段,以便你可以按照相同的步骤操作:
import pandas as pd
# Load the data
df = pd.read_csv("data.csv")
# Remove missing data and keep only useful columns
df = df.loc[
df['Value'] != "No data",
['ParentLocation', 'Location', 'Period', 'Dim1', 'Value']
]
# Rename columns
df.columns = ['ParentLocation', 'Location', 'Period', 'Gender', 'Prevalence']
# Some values in Prevalence contain ranges (like '13.4 [8.7 – 18.9]'), so we just keep the average
df['Prevalence'] = df['Prevalence'].apply(
lambda x: float(x.split('[')[0]) if '' in x else x
)
这就是它的样子:
![
前 5 行 DataFrame — 作者提供的图片
我们准备开始绘图了。
2. Seaborn
我相信你已经熟悉 Seaborn 了。它是与 Matplotlib 一起用于绘图的最常用库之一。你可能不知道的是,我们也可以用它来做动态可视化,而不仅仅是静态的。
但让我介绍一下,以防有人不熟悉。
用他们自己的话来说:“Seaborn 是一个基于 matplotlib 的 Python 数据可视化库。它提供了一个高级接口,用于绘制吸引人且信息丰富的统计图表。”[2]
所以这就像是 Matplotlib 的改进版。
我将先使用一个简单的 relplot() 来绘制不同变量之间的关系:
import seaborn as sns
# Apply the style
sns.set_style("ticks")
sns.despine()
# Prepare df for this graph
grouped_df = df.groupby(
['ParentLocation', 'Gender', 'Period']
)['Prevalence'].mean().reset_index()
# Create visualization
sns.relplot(
data=grouped_df[
grouped_df['Gender'] != 'Both sexes'
],
kind="line",
x="Period",
y="Prevalence",
col="Gender",
hue="ParentLocation",
palette="Paired"
)

流行率-时间-性别关系 — 作者提供的图片
这里的可视化本身并不是重点。我们将在这篇文章的最后一部分通过模拟讲故事来提取一些见解。
我们得到的是静态图表。如果不通过更改代码,我们无法改变任何内容,这使得图表是静态的。如果我们想在 ParentLocation 基础上进行比较,那是可以的,但我们无法在国家级别上进行详细分析。这样做会导致同一张令人困惑的图表中出现大量线条。
这不是我们来这里的目的。
但 Seaborn 不允许我们实现交互式仪表板……不过,我们可以使用 ipywidgets 来提高互动性。
如果下一段代码在你的系统上不起作用,那是因为你需要通过在终端中运行以下命令启用小部件扩展
jupyter nbextension enable --py --sys-prefix widgetsnbextension
现在我们将绘制相同的 relplot(),但这次我们希望以可交互的方式比较国家。以下是如何操作:
import ipywidgets as widgets
countries = sorted(pd.unique(df['Location']))
# Creates Dropdown Widget
def create_dd(desc, i=0):
dd = widgets.Dropdown(
options=countries,
value=countries[i],
description=desc
)
return dd
# Creates the relplot
def draw_relplot(country1, country2):
sns.relplot(
data=df[
(df['Location'].isin([country1, country2]))
& (df['Gender'] != 'Both sexes')
],
kind="line",
x="Period",
y="Prevalence",
col="Gender",
hue="Location",
palette="Paired"
)
# Generate the final widget
dd1 = create_dd('Country 1', 0)
dd2 = create_dd('Country 2', 1)
ui = widgets.HBox([dd1, dd2])
# Create the interactive plot and display
out = widgets.interactive_output(
draw_relplot,
{'country1': dd1, 'country2': dd2}
)
display(ui, out)
这次较长。但我们所做的只是将一些代码包装到两个函数中 — 一个用于创建下拉菜单,另一个用于创建图表本身 — 然后通过使用 HBox 小部件来创建 UI,它是一个包装器,将传递的小部件分组。然后我们使用 interactive_output 函数来创建输出并显示所有内容。
结果:

按性别的国家比较 — 图片由作者提供
我们在这里添加了一些互动性和动态性!我们已经可以一次比较两个国家,并查看它们的超重患病率差异。
情况变得更好了。
然而,这种方法有一个限制。图表不可交互,我们只是动态地显示数据。这就是我们想要的吗?也许在某些时候可以,但今天不行。
介绍我们的下一个新朋友……
3. Bokeh
使用官方声明:
“只需几行 Python 代码,Bokeh 就能让你创建在 web 浏览器中显示的交互式 JavaScript 驱动的可视化。”[3]
Bokeh 是一个更加复杂且完整的交互图形和仪表板选项。我们实际上可以用 Bokeh 创建类似 Tableau 的仪表板。
缺点是这不是最用户友好的选项。即使安装对于初学者在 Jupyter Notebook 中使用也可能相当痛苦。
默认情况下,我们创建的图表和仪表板会呈现在 HTML 页面或文件中,但我们可以通过导入并调用 output_notebook() 函数将它们内联渲染在笔记本中。
让我们尝试用 Bokeh 复制我们用 Seaborn 创建的第一个图表,生成一个可交互的图形:
from bokeh.plotting import figure
from bokeh.io import show, output_notebook
from bokeh.layouts import row
from bokeh.palettes import Paired
def prepare_figure(gender):
l = figure(
title=f"Gender = {gender}",
x_axis_label='Period',
y_axis_label='Prevalence',
width=475,
outer_width=475,
height=500,
)
for i, loc in enumerate(pd.unique(grouped_df['ParentLocation'])):
l.line(
grouped_df[
(grouped_df['Gender'] == gender)
& (grouped_df['ParentLocation']==loc)
]['Period'],
grouped_df[
(grouped_df['Gender'] == gender)
& (grouped_df['ParentLocation']==loc)
]['Prevalence'],
legend_label=loc,
line_width=2,
color=Paired[12][i]
)
l.legend.location = 'top_left'
l.legend.click_policy="mute"
l.legend.label_text_font_size='8px'
l.legend.background_fill_alpha = 0.4
return l
# Render the figure inline
output_notebook()
# Create the first figure and input the data
l1=prepare_figure('Male')
# Create the second figure and input the data
l2=prepare_figure('Female')
p = row(l1, l2)
# Show the plot
show(p)

按性别比较欧洲超重患病率,从 2000 年到 2016 年 — 图片由作者提供
太棒了!我们的第一个交互式数据可视化。但这仍然远未达到仪表板的概念。
此外,我们目前还没有选择显示数据的能力。我们只能显示/隐藏一些线条,并在图形中移动和放大/缩小。很酷,但我们需要更多。
尽管 Bokeh 内置了自己的小部件 — 我们可以创建下拉菜单等 — 我还是先到此为止。
Bokeh 绝对不适合初学者,我不想让任何人感到困惑。在我看来,与接下来的两个选项相比,它是一个较差的替代方案。
就像浪费时间和金钱讨论大米对你的好处一样,而实际上你可以专注于更健康的选择,如糙米,甚至其他蔬菜,如西兰花。
现在进入重点部分。
4. Plotly
再次引用 Plotly 的文档:
Plotly Express 是
plotly库的内置部分,是创建大多数常见图形的推荐起点。[…] 这些函数的 API 经过精心设计,尽可能一致且易于学习,使得在数据探索会话中,从散点图切换到条形图、直方图或旭日图都变得容易。[4]
Plotly 是一个令人惊叹的工具,我一再使用它,我可以保证它正如他们所说的那样:易于学习,且从一个图表切换到另一个图表非常简单。
然而,我们今天不会多谈 Plotly Express,因为我们要深入一点。我们将使用 Plotly 的图形对象来完成任务。
我们将直接进入允许我们创建国家比较图表的代码,并包含两个下拉框:
from plotly.subplots import make_subplots
import plotly.graph_objects as go
# Define variables
colors = ['#a6cee3', '#1f78b4']
# Define widgets (using previous function)
dd1 = create_dd('Country 1', 0)
dd2 = create_dd('Country 2', 1)
# Create figure and traces
def create_figure(country1, countr
fig = make_subplots(
shared_xaxes=True,
shared_yaxes=True,
rows=1,
cols=2,
vertical_spacing = 0,
subplot_titles=("Gender = Female", "Gender = Male"),
)
for j, gender in enumerate(['Female', 'Male']):
for i, loc in enumerate([country1, country2]):
fig.add_trace(
go.Scatter(
x=df[
(df['Gender'] == gender)
& (df['Location'] == loc)
]['Period'],
y=df[
(df['Gender'] == gender)
& (df['Location'] == loc)
]['Prevalence'],
name=loc,
line=go.scatter.Line(color=colors[i]),
hovertemplate=None,
showlegend=False if j==0 else True
),
row=1,
col=j+1
)
# Prettify
fig.update_xaxes(showspikes=True, spikemode="across")
fig.update_layout(
hovermode="x",
template='simple_white'
)
return fig
fig = create_figure(countries[0], countries[1])
# Create the Figure Widget
g = go.FigureWidget(
data = fig,
layout=go.Layout(
barmode='overlay'
)
)
# Handle what to do when the DD value changes
def response(change):
dfs = []
for gender in ['Female', 'Male']:
for loc in [dd1.value, dd2.value]:
dfs.append(
df[(df['Gender'] == gender)
& (df['Location'] == loc)]
)
x = [temp_df['Period'] for temp_df in dfs]
y = [temp_df['Prevalence'] for temp_df in dfs]
with g.batch_update():
for i in range(len(g.data)):
g.data[i].x = x[i]
g.data[i].y = y[i]
g.data[i].name = dd1.value if i%2 == 0 else dd2.value
g.layout.barmode = 'overlay'
g.layout.xaxis.title = 'Period'
g.layout.yaxis.title = 'Prevalence'
dd1.observe(response, names="value")
dd2.observe(response, names="value")
container = widgets.HBox([dd1, dd2])
widgets.VBox([container, g])
这可能看起来很长,但主要是因为我已经将其格式化为无需水平滚动的形式。如果你仔细观察,你会发现我们实际上做的事情很少!
我们在重用之前片段的部分,例如 create_dd() 函数。是的,我们再次使用 ipywidgets,但它们与 Plotly 的集成非常顺畅。所有操作都通过他们的 observe() 方法和我们编写的 response() 函数来处理,每当任一下拉框的值发生变化时,都会执行这个函数。
这是视觉效果:

使用 plotly 进行数据交互 — 作者提供的图片
这很简单!同样,使用 Bokeh 实现相同的效果是可能的,但在 Plotly 中学习如何做要快得多。无论哪种方式,结果都很棒。
进入我们的最后一个工具……
Dash
Dash 本身不是一个绘图库。它是一个用于生成仪表盘的出色框架。最近,它在构建互动式基于 Web 的仪表盘方面的受欢迎程度不断上升。
剧透:Dash 是由 Plotly 的开发者创建的。所以这里我们仍然会使用 Plotly,但现在与 Dash 框架结合起来,看看我们可以用它们创建什么。
那么,我们如何使用 Dash 构建相同的图表呢?与我之前所做的将整个代码放在这里不同,我想将其分成几个部分,以便更好地理解。
正如我在开始时所说的,我会写单独的帖子来深入探讨这些框架。所以如果你感兴趣,请考虑关注我。
我们将从导入开始。请注意,我们将重用已经见过的代码,所以我不会重新导入像 Plotly 这样的框架。
# Install dash and jupyter_dash
!pip install dash
!pip install jupyter-dash
# Import
import dash_core_components as dcc
from dash import html
from jupyter_dash import JupyterDash
请注意,我导入了 JupyterDash,这是因为我正在使用 Jupyter Notebook。如果你使用的是普通脚本,只需将最后一行替换为 from dash import Dash。
接下来我们要做的是创建应用:
app = JupyterDash(__name__)
我希望你对 HTML 有所了解,因为我们现在要使用Dash的html模块做一点这方面的工作。我计划创建一个简单的 3 列 1 行网格,列宽分别为 15%、60%和 25%。第一列将用于下拉框,第二列用于图表,第三列将为空。
第一列将由一个包含标题和另一个包含下拉框的 div 容器组成。以下代码实现了它:
html.Div([
html.H1('Countries'),
html.Div(
[dcc.Dropdown(
id='country1_dropdown',
options=df['Location'].unique().tolist(),
value='',
placeholder='Select a country'
),
dcc.Dropdown(
id='country2_dropdown',
options=df['Location'].unique().tolist(),
value='',
placeholder='Select a country'
)]
)
]
)
一开始可能会显得有些混乱,但其实非常简单。每个 HTML 元素都有其子元素在列表参数中(在这种情况下,<div>包裹着一个<h1>和另一个<div>,而两个下拉框在内层的<div>中)。
现在让我们创建图表代码:
html.Div([
dcc.Graph(
id='chart'
)
])
这个比较简单。它只是一个包裹着 ID 为 chart 的图表的<div>容器。
然而,如果没有应用样式,仪表盘是不可接受的。我们怎么给这些元素添加一些 CSS 呢?
很简单,每个元素都有一个可选的style参数,我们可以用来添加我们的 CSS。下面是最后一段代码如何加上样式:
html.Div([
dcc.Graph(
id='chart',
style={}
)
], style={
'grid-column-start': 'second',
'grid-column-end': 'third',
}
)
我已经听到你说:“当然,我们知道如何创建这两个元素。但我们实际怎么创建布局呢?”很简单,只需将它们全部放入一个<div>容器中,并将其分配给app.layout:
app.layout = html.Div([
# Dropdown menu
html.Div([
html.H1('Countries'),
html.Div(
[dcc.Dropdown(
id='country1_dropdown',
options=df['Location'].unique().tolist(),
value='',
placeholder='Select a country',
style={'margin-bottom': '10px', 'max-width': '200px'}
),
dcc.Dropdown(
id='country2_dropdown',
options=df['Location'].unique().tolist(),
value='',
placeholder='Select a country',
style={'max-width': '200px'}
)]
)
], style={
'grid-column-start' : 'first',
'grid-column-end' : 'second',
'padding': '2%',
'justify-self': 'center'
}),
# Plot
html.Div([
dcc.Graph(
id='chart',
style={}
)
], style={
'grid-column-start' : 'second',
'grid-column-end' : 'third',
})
], style={
'width': '100vw'
'display': 'inline-grid',
'grid-template-columns': '[first] 15% [second] 60% [third] 25%',
'grid-template-rows': '[row] 100%',
'grid-gap': '1rem',
'align-items': 'right',
})
如果你尝试运行这个,你会看到空图表。我们可以通过回调向它们添加一些数据。这些回调有输入和输出依赖关系,用于更新显示的数据,并达到我们期望的交互效果。
你接下来会看到一个叫做update_chart()的函数,它基本上是我们在 Plotly 部分中使用的create_chart()函数的复制粘贴。
colors = ['#a6cee3', '#1f78b4']
@app.callback(
dash.dependencies.Output('chart', 'figure'),
dash.dependencies.Input('country1_dropdown', 'value'),
dash.dependencies.Input('country2_dropdown', 'value')
)
def update_chart(country1, country2):
# Create figure and traces
fig = make_subplots(
shared_xaxes=True,
shared_yaxes=True,
rows=1,
cols=2,
vertical_spacing = 0,
subplot_titles=("Gender = Female", "Gender = Male"),
)
for j, gender in enumerate(['Female', 'Male']):
for i, loc in enumerate([country1, country2]):
fig.add_trace(
go.Scatter(
x=df[
(df['Gender'] == gender)
& (df['Location'] == loc)
]['Period'],
y=df[
(df['Gender'] == gender)
& (df['Location'] == loc)
]['Prevalence'],
name=loc,
line=go.scatter.Line(color=colors[i]),
hovertemplate=None,
showlegend=False if j==0 else True
),
row=1,
col=j+1
)
# Prettify
fig.update_xaxes(showspikes=True, spikemode="across")
fig.update_layout(
hovermode="x",
template='simple_white',
)
return fig
所以这里的新内容是函数声明上方的部分。我们说回调依赖于两个输入——country1 和 country2——输出是一个id='chart'的图表。
最后,我们运行应用:
# Run app
if __name__ == '__main__':
app.run_server()
然后导航到http://127.0.0.1:8050或服务器运行的任何 IP,像我这里一样玩一玩:

使用 Dash 和 Plotly 与数据交互 — 图片由作者提供
看看它是多么流畅。我们可以结合 HTML 和 Python 图表的力量,创建漂亮的仪表盘,这都要归功于 Dash。
6. 讲述故事
这一切都是比较基础的。我们没有建立一个仪表盘,也没有讲述一个故事。我们只是看了一些让图表互动的工具。
在这一部分,我将分享一个示例仪表盘,任何人都可以用来向利益相关者讲述故事。它可能不是最漂亮或最完整的,但足以展示 Dash 的能力,并说服你开始使用它来进行未来的可视化。
免责声明:请爱上 Dash 和 Plotly 提供的实用功能,而不是我实现的设计。我的目标是展示 Dash 的能力,而不是试图说服你超重是全球真正的问题。

我为这个项目构建的简单仪表盘 — 图片由作者提供
现在来探讨一些见解:
一般基础
许多科学家普遍认为肥胖是一种流行病。它是否符合流行病的实际定义并不重要。重要的是它的流行率以及它对我们健康的负面影响。
超重是肥胖之前的阶段。因此,超重也很重要,因为如果不加以控制,可能会导致未来的肥胖。
如果我们检查 1975 年的世界地图与 2016 年的世界地图,可以清楚地看到后者的颜色更加鲜艳。这并不好,因为颜色越黄,超重流行率越高。所以,整体而言,全球的超重流行率有了很大增加。
全球化、技术和科学的进步以及许多其他因素使大多数国家的食品变得丰富。或者至少,我们可以猜测食品获取在总体上有所增加。这与日益增加的久坐生活方式以及过度消费不健康食品结合起来,导致了超重流行率的巨大增加。
哦,随着超重流行率的增加,肥胖率也随之增加。
顶级国家
我们看到瑙鲁或帕劳等国家的流行率极高(~88.7%)。我们尚未确认数据,但一些报告确实坚持解决这些国家的肥胖问题(而且,数据应来自世界卫生组织,应该是可靠的)。
现在聚焦于瑙鲁,“从 1980 年代开始,瑙鲁人过上了久坐的生活方式,饮食不健康,导致了太平洋地区最糟糕的健康状况”。[5]
此外,“瑙鲁约 90%的土地面积覆盖着磷酸盐矿床,大部分已被条带开采且不可耕种。这导致瑙鲁人依赖从澳大利亚和新西兰等大洋洲国家进口的含糖量和脂肪含量都很高的加工食品” [6]
我们可以说瑙鲁是一个异常值,但有几个国家处于类似的位置。至少可以说,这令人担忧。
时间差异
一个国家的流行病率是否增加了很多可能取决于几个因素,所有因素都应独立对待。
然而,我对新加坡感到着迷。它们在 41 年里几乎没有变化。几个西欧国家也在增幅最小的榜单上。我相信,如果我们寻找的话,可以提取出一个模式。
在另一端,博茨瓦纳在 1975 年和 2016 年之间的流行率大幅上升。然而,在 2016 年,它们仍然远未达到顶端。这可能是因为近年来它们的食品获取增加,但博茨瓦纳仍然是博茨瓦纳。
性别
在大多数国家,男性历史上通常比女性更容易超重。我们通常看到男性的超重情况比女性更多。
我们可以在这里尝试提出不同的假设,但有一点是不可否认的:女性在历史上比男性更关心自己的外貌,可能是由于社会压力。我们生活在一个性别歧视的世界中,这只是其中一些后果。
最近我们才看到像巴西这样的国家,女性的超重率已经超过了男性。这并不是值得高兴的事,但我们可以把它看作是一种女性主义行为吗?可能不能。
但如果我们进行概括,模式仍然很清晰。
最终结论
残酷的事实是,超重的普遍性——因此,肥胖——随着时间的推移在增加。而且它影响每个人:所有性别,所有国家。没有一个是安全的。
这真的算是流行病吗?有人可能这么认为。
令人担忧的是,在大多数情况下,超重是由于不良习惯、营养不良、久坐生活方式和睡眠不好所致。加上整体热量摄入已经持续多年增加且似乎没有平稳下来。
所有这些都依赖于个人,但大多数人显然没有采取任何行动。
已经证明肥胖会导致死亡:它与心脏病、中风、糖尿病、癌症等相关……而我们依然放任其发展。
我们必须停止这种情况,但我们只能照顾好自己。所以务必做出明智的选择。
正如 Jerzy Gregorek 所说:
“艰难的选择,轻松的生活。轻松的选择,艰难的生活。”
结束
感谢你读完它!
这是我写过的最长的一篇文章,但我像孩子玩玩具一样享受了它。我希望你也一样。
本文的目的是分享我用来创建互动可视化的一些工具,并通过我创建的仪表板进行一种简短而非正式的讲述,以从我们看到的数据中提取一些见解和假设。
在资源部分,你将看到一个链接,检查用于此帖子的所有代码![7]
**Thanks for reading the post!**
I really hope you enjoyed it and found it insightful.
Follow me and subscribe to my mailing list for more
content like this one, it helps a lot!
**@polmarin**
如果你想进一步支持我,请考虑通过下面的链接订阅 Medium 的会员:这不会让你多花一分钱,但会帮助我完成这个过程。
[## 通过我的推荐链接加入 Medium - Pol Marin
阅读 Pol Marin 的每一篇故事(以及 Medium 上成千上万其他作者的故事)。你的会员费用直接支持 Pol……
资源
[1] 成年人超重的流行率,BMI >= 25(年龄标准化估计)(%) — 世界卫生组织
[2] seaborn: 统计数据可视化
[3] Bokeh
[4] Python 中的 Plotly express — Plotly
[5] 西谷高明(2012 年 5 月 27 日)。“瑙鲁:一个被肥胖和糖尿病困扰的岛屿”。朝日新闻。
[6] “我在如此小的岛屿上见过太多的葬礼”:瑙鲁的惊人故事,这个世界上 2 型糖尿病发病率最高的小岛国
[7] 互动可视化仓库 — GitHub
使用 pyvis 构建互动网络图
原文:
towardsdatascience.com/building-interactive-network-graphs-using-pyvis-5b8e6e25cf64
学习如何让你的网络图栩栩如生
·发布在 Towards Data Science ·阅读时间 7 分钟·2023 年 3 月 6 日
--

在我之前关于创建网络图的文章中,我展示了如何使用 NetworkX 包构建一个图。NetworkX 的主要问题是生成的图是静态的。一旦图被绘制出来,用户就无法与之互动(例如重新排列节点等)。如果用户可以与图进行互动,网络图会更直观(也更有趣!)。这也是本文的主要关注点。
学习如何使用 NetworkX 包可视化复杂网络
towardsdatascience.com
在这篇文章中,我将展示如何使用 pyvis 包创建一个互动网络图。
pyvis包是流行的 visJS JavaScript 库的一个封装,它使你可以轻松地在 Python 中生成可视化网络图。
安装 pyvis
要安装 pyvis 包,请使用 pip 命令:
!pip install pyvis
创建网络
首先,使用 pyvis 中的 Network 类创建一个新图:
from pyvis.network import Network
net = Network(
notebook=True,
)
要在 Jupyter Notebook 中显示图形,将 notebook 参数设置为 True。上述代码片段创建了一个无向图。
添加节点
现在你可以向图中添加节点了:
net.add_node("Singapore")
net.add_node("San Francisco")
net.add_node("Tokyo")
net.add_nodes(["Riga", "Copenhagen"],
color=['lightgreen', 'yellow'])
add_node() 函数添加一个节点,而 add_nodes() 函数则可以向图中添加多个节点。你还可以为这两个函数设置可选的 color 参数,以设置节点的颜色。
要显示图形,请调用show()函数并为输出指定名称:
net.show('mygraph.html')
节点现在应该显示出来:

所有图片由作者提供
添加边
在图中添加了节点后,现在可以添加边来连接这些节点:
net.add_edge("Singapore","San Francisco")
net.add_edge("San Francisco","Tokyo")
net.add_edges(
[
("Riga","Copenhagen"),
("Copenhagen","Singapore"),
("Singapore","Tokyo"),
("Riga","San Francisco"),
("San Francisco","Singapore"),
]
)
net.show('mygraph.html')
add_edge()函数添加一个连接两个节点的边,而add_edges()函数接受一个包含多个节点的元组列表。
图现在应该显示连接各个节点的边。试着拖动每个节点,看看它们在释放后是如何被拉回的:
所有视频由作者提供
有向图
如果你想要一个有向图,你应该在Network类中设置directed参数:
net = Network(
notebook=True,
directed=True
)
如果你修改了之前的代码并重新运行所有代码片段,你现在应该能看到一个有向图:

修改图的物理效果
如果你点击并拖动图中的节点,你会注意到节点会四处弹跳。当你放开鼠标时,节点会弹回到原来的位置。所有这些行为非常像由弹簧(边)束缚的真实球体(节点)。你可以使用repulsion()函数自定义图形背后的物理效果(它们如何弹回、弹簧的阻尼等)。以下语句显示了repulsion()函数中所有参数的默认值:
net.repulsion(
node_distance=100,
central_gravity=0.2,
spring_length=200,
spring_strength=0.05,
damping=0.09,
)
这里是各种参数的用途:
-
node_distance— 这是排斥力的影响范围。 -
central_gravity— 使整个网络被吸引到中心的引力。 -
spring_length— 边的静态长度。 -
spring_strength— 边的弹簧强度。 -
damping— 一个从 0 到 1 的值,表示前一次物理模拟迭代中的速度有多少会延续到下一次迭代。
理解各种参数的最佳方法是尝试一下。以下示例设置了spring_length和damping参数:
net.repulsion(
spring_length=400,
damping=0.01,
)
net.show('mygraph.html')
这是图的样子:

以下视频展示了拖动和释放节点时图的行为:
你还可以使用show_buttons()函数显示 UI 以动态改变图的物理效果:
net.show_buttons(filter_='physics')
net.show('mygraph.html')

filter_参数可以选择以下选项之一:
show_buttons(filter_=['nodes', 'edges', 'physics'])
如果你想显示所有过滤器,将其设置为True:
net.show_buttons(filter_= True)
我将留给你尝试这些过滤器的效果以及它们的工作方式。可视化航班延误数据集
现在你已经熟悉了使用pyvis包的基础知识,我们将使用它来可视化2015 年航班延误数据集中的各种机场之间的航班。
2015 年航班延误数据集 (airports.csv)。来源:
www.kaggle.com/datasets/usdot/flight-delays。许可 — CC0: 公共领域
首先,将flights.csv文件加载到 Pandas DataFrame 中。由于这个 CSV 文件很大,我将只加载执行工作所需的三列:
import pandas as pd
df = pd.read_csv('flights.csv',
usecols = ["ORIGIN_AIRPORT", "DESTINATION_AIRPORT","YEAR"])
一旦数据框加载完成,我将继续统计从一个机场到另一个机场的航班数量:
df_between_airports = df.groupby(by=["ORIGIN_AIRPORT", "DESTINATION_AIRPORT"]).count()
df_between_airports = df_between_airports['YEAR'].rename('COUNT').reset_index()
df_between_airports = df_between_airports.query('ORIGIN_AIRPORT.str.len() <= 3 & DESTINATION_AIRPORT.str.len() <= 3')
df_between_airports = df_between_airports.sort_values(by="COUNT", ascending=False)
df_between_airports
结果输出如图所示:

由于航班组合超过 4500 种,我们只选择前 130 种组合:
top = 130
df_between_airports = df_between_airports.head(top)
df_between_airports['COUNT'] = df_between_airports['COUNT'] / 5000
df_between_airports
注意,我将COUNT列中的值除以 5000,因为稍后我将使用COUNT列中的值作为连接两个机场的边的线宽。因此,值需要缩小到一个较小的范围。前 130 种组合现在如图所示:

接下来,我将汇总每个机场的起始航班(记住之前的计数已经标准化):
node_sizes = df_between_airports.groupby('ORIGIN_AIRPORT').COUNT.agg(sum)
node_sizes
每个机场起始航班的数量将作为节点的大小:
从一个机场起飞的航班越多,节点越大。
ORIGIN_AIRPORT
ANC 1.2766
ATL 20.2544
BOS 6.3382
BWI 1.1674
CLT 1.2614
DAL 1.2524
DCA 4.0138
DEN 11.5638
DFW 5.5244
EWR 2.0252
FLL 2.5436
HNL 5.1544
HOU 1.2592
JAX 1.0192
JFK 6.1684
KOA 1.2694
LAS 6.8754
LAX 21.0822
LGA 7.3132
LIH 1.1710
MCO 2.7096
MIA 2.2936
MSP 2.3608
MSY 1.1186
OAK 1.2562
OGG 1.6626
ORD 12.6836
PHL 2.3876
PHX 7.2886
SAN 2.4130
SEA 7.3736
SFO 12.2998
SJC 1.2678
SLC 3.4424
SMF 1.1148
TPA 1.4166
Name: COUNT, dtype: float64
绘制图表
你现在可以绘制图表:
from pyvis.network import Network
net = Network(
notebook = True,
directed = True, # directed graph
bgcolor = "black", # background color of graph
font_color = "yellow", # use yellow for node labels
cdn_resources = 'in_line', # make sure Jupyter notebook can display correctly
height = "1000px", # height of chart
width = "100%", # fill the entire width
)
# get all the nodes from the two columns
nodes = list(set([*df_between_airports['ORIGIN_AIRPORT'],
*df_between_airports['DESTINATION_AIRPORT']
]))
# extract the size of each airport
values = [node_sizes[node] for node in nodes]
# extract the edges between airports
edges = df_between_airports.values.tolist()
# use this if you don't need to set the width of the edges
# edges = df_between_airports.iloc[:,:2].values.tolist()
# add the nodes, the value is to set the size of the nodes
net.add_nodes(nodes, value = values)
# add the edges
net.add_edges(edges)
net.show('flights.html')
图表如下所示:

让我们稍微放大一下:

你可以看到ATL和LAX的起始航班最多(它们是两个最大节点)。你可以通过将这两个节点的颜色更改为红色来突出显示它们。为此,你可以使用nodes属性遍历所有节点,并检查value键的值。如果值大于 20,则使用color键将节点颜色设置为红色:
...
...
# add the edges
net.add_edges(edges)
# color the nodes red if their count is more than 20
for n in net.nodes:
if n['value'] > 20:
n['color'] = 'red'
net.show('flights.html')
ATL和LAX节点现在显示为红色:

如果你喜欢阅读我的文章,并且它对你的职业/学习有所帮助,请考虑注册成为 Medium 会员。每月 5 美元,能够无限访问 Medium 上的所有文章(包括我的)。如果你使用以下链接注册,我将赚取一小部分佣金(对你没有额外费用)。你的支持意味着我可以投入更多时间撰写类似的文章。
[## 使用我的推荐链接加入 Medium — Wei-Meng Lee
阅读 Wei-Meng Lee 的每个故事(以及 Medium 上其他成千上万的作者)。你的会员费直接支持…
摘要
在这篇文章中,你学习了如何使用pyvis包创建交互式网络图。pyvis包最有趣的地方在于它能够让你的网络图生动起来。交互式网络图非常适合用于社交网络、公司结构或其他你希望可视化实体之间关系的网络。享受使用pyvis的乐趣,并告诉我你使用它处理的数据类型!
使用 OPL 堆栈构建 LLMs 驱动的应用程序
OPL:OpenAI、Pinecone 和 Langchain 用于知识驱动的 AI 助手
·
关注 发表在 Towards Data Science ·12 min 阅读·2023 年 4 月 3 日
--
Midjourney 提示:一个女孩用多个积木块建造乐高桥梁
我记得一个月前,Eugene Yan 在 LinkedIn 上发布了一项 投票:
你是否感到因为没有参与 LLMs/生成型 AI 而错失良机?
大多数人回答了“是”。考虑到 chatGPT 引发的广泛关注以及现在 gpt-4 的发布,很容易理解为什么会这样。人们形容大型语言模型(LLMs)的崛起就像 iPhone 的时刻。然而,我认为真的没有必要感到 FOMO。考虑一下:错过了开发 iPhones 的机会并不排除创造创新 iPhone 应用程序的巨大潜力。LLMs 也是如此。我们刚刚进入了一个新时代,现在正是利用 LLMs 整合构建强大应用程序的绝佳时机。
在这篇文章中,我将涵盖以下主题:
-
什么是 OPL 技术栈?
-
如何使用 OPL 构建具有领域知识的 chatGPT?(包含代码演示的关键组件)
-
生产考虑因素
-
常见误解
1. 什么是 OPL 技术栈?

作者创建的图像
OPL 代表 OpenAI、Pinecone 和 Langchain, 它已逐渐成为克服 LLMs 两个局限性的行业解决方案:
-
LLMs 幻觉: chatGPT 有时会提供过度自信的错误回答。一个潜在的原因是这些语言模型被训练得非常有效地预测下一个词,或者更准确地说是下一个标记。给定输入文本,chatGPT 会返回高概率的词,这并不意味着 chatGPT 具有推理能力。
-
知识更新不够及时: chatGPT 的训练数据仅限于 2021 年 9 月之前的互联网数据。因此,如果你的问题涉及最近的趋势或话题,它可能会产生不太理想的回答。
常见的解决方案是在 LLMs 上添加知识库,并使用 Langchain 作为构建流水线的框架。每项技术的关键组件可以总结如下:
-
OpenAI:
-
提供对强大 LLMs 如 chatGPT 和 gpt-4 的 API 访问
-
提供将文本转换为嵌入的模型。
-
-
Pinecone:提供嵌入向量存储、语义相似度比较和快速检索。
-
Langchain:它包含 6 个模块(
Models、Prompts、Indexes、Memory、Chains和Agents)。-
Models提供了灵活的嵌入模型、聊天模型和 LLMs,包括但不限于 OpenAI 的产品。你还可以使用 Hugging Face 上的其他模型,如 BLOOM 和 FLAN-T5。 -
Memory: 有多种方式可以让聊天机器人记住过去的对话记录。根据我的经验,实体记忆效果好且高效。 -
Chains: 如果你是 Langchain 的新手,Chains 是一个很好的起点。它遵循类似流水线的结构来处理用户输入,选择 LLM 模型,应用 Prompt 模板,并从知识库中搜索相关上下文。
-
接下来,我将介绍我使用 OPL 技术栈构建的应用程序。
2. 如何使用 OPL 构建具有领域知识的 chatGPT?(包含代码演示的关键组件)
我构建的应用程序称为chatOutside,它有两个主要部分:
-
chatGPT:让你直接与 chatGPT 聊天,格式类似于问答应用,每次接收一个输入和输出。
-
chatOutside:让你与具有户外活动及趋势专业知识的 chatGPT 版本进行对话。格式更类似于聊天机器人的风格,所有消息在对话过程中都会被记录。我还包含了一个提供源链接的部分,这可以增强用户信心,并且总是有用的。
如你所见,如果你问同样的问题:“2023 年最好的跑鞋是什么?我的预算在$200 左右。”chatGPT 会说“作为一个 AI 语言模型,我无法访问未来的信息。”而 chatOutside 会为你提供更及时的答案,并附上源链接。

开发过程涉及三个主要步骤:
-
第一步:在 Pinecone 中构建外部知识库
-
第二步:使用 Langchain 进行问答服务
-
第三步:在 Streamlit 中构建我们的应用程序
每个步骤的实施细节将在下面讨论。
步骤 1: 在 Pinecone 中构建外部知识库
- 步骤 1.1: 我连接到我们的外部目录数据库,并选择了在 2022 年 1 月 1 日至 2023 年 3 月 29 日之间发布的文章。这为我们提供了大约 20,000 条记录。

外部的示例数据预览
接下来,我们需要进行两个数据转换。
- 步骤 1.2: 将上述数据框转换为字典列表,以确保数据可以正确地插入到 Pinecone 中。
# Convert dataframe to a list of dict for Pinecone data upsert
data = df_item.to_dict('records')
-
步骤 1.3: 使用 Langchain 的
RecursiveCharacterTextSplitter将content拆分成更小的块。将文档拆分为更小的块有两个好处:-
一篇典型文章可能超过 1000 个字符,这非常长。想象一下我们想检索前 3 篇文章作为提示给 chatGPT,我们很容易就会超过 4000 个字元限制。
-
更小的块提供更相关的信息,从而为 chatGPT 提供更好的提示上下文。
-
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=400,
chunk_overlap=20,
length_function=tiktoken_len,
separators=["\n\n", "\n", " ", ""]
)
分割后,每条记录的内容被拆分成多个部分,每个部分少于 400 个字元。

将内容拆分为多个块
值得注意的是,使用的文本分割器称为RecursiveCharacterTextSplitter,这是 Langchain 的创建者 Harrison Chase 推荐使用的。基本思路是首先按段落拆分,然后按句子拆分,重叠(20 字元)。这有助于保留来自周围句子的有意义信息和上下文。
- 步骤 1.4: 将数据插入 Pinecone。下面的代码改编自 James Briggs 的精彩 教程。
import pinecone
from langchain.embeddings.openai import OpenAIEmbeddings
# 0\. Initialize Pinecone Client
with open('./credentials.yml', 'r') as file:
cre = yaml.safe_load(file)
# pinecone API
pinecone_api_key = cre['pinecone']['apikey']
pinecone.init(api_key=pinecone_api_key, environment="us-west1-gcp")
# 1\. Create a new index
index_name = 'outside-chatgpt'
# 2\. Use OpenAI's ada-002 as embedding model
model_name = 'text-embedding-ada-002'
embed = OpenAIEmbeddings(
document_model_name=model_name,
query_model_name=model_name,
openai_api_key=OPENAI_API_KEY
)
embed_dimension = 1536
# 3\. check if index already exists (it shouldn't if this is first time)
if index_name not in pinecone.list_indexes():
# if does not exist, create index
pinecone.create_index(
name=index_name,
metric='cosine',
dimension=embed_dimension
)
# 3\. Connect to index
index = pinecone.Index(index_name)
我们批量上传并嵌入所有文章,这花费了大约 20 分钟来插入 2 万条记录。请根据你的环境调整 tqdm 导入(你不需要同时导入两个!)
# If using terminal
from tqdm.auto import tqdm
# If using in Jupyter notebook
from tqdm.autonotebook import tqdm
from uuid import uuid4
batch_limit = 100
texts = []
metadatas = []
for i, record in enumerate(tqdm(data)):
# 1\. Get metadata fields for this record
metadata = {
'item_uuid': str(record['id']),
'source': record['url'],
'title': record['title']
}
# 2\. Create chunks from the record text
record_texts = text_splitter.split_text(record['content'])
# 3\. Create individual metadata dicts for each chunk
record_metadatas = [{
"chunk": j, "text": text, **metadata
} for j, text in enumerate(record_texts)]
# 4\. Append these to current batches
texts.extend(record_texts)
metadatas.extend(record_metadatas)
# 5\. Special case: if we have reached the batch_limit we can add texts
if len(texts) >= batch_limit:
ids = [str(uuid4()) for _ in range(len(texts))]
embeds = embed.embed_documents(texts)
index.upsert(vectors=zip(ids, embeds, metadatas))
texts = []
metadatas = []
在将 Outside 文章数据插入后,我们可以通过使用 index.describe_index_stats() 检查我们的 Pinecone 索引。需要注意的统计数据之一是 index_fullness,在我们的案例中为 0.2。这意味着 Pinecone pod 已满 20%,暗示一个 p1 pod 大约可以存储 10 万篇文章。

在将数据插入 Pinecone 后
步骤 2:使用 Langchain 进行问答服务
注意:Langchain 最近更新非常快,下面代码使用的版本是 *0.0.118* 。

OPL 堆栈中的数据流
上面的草图说明了推理阶段数据流动的方式:
-
用户提问:“2023 年最好的跑鞋是什么?”
-
问题使用
ada-002模型转换为嵌入。 -
用户问题嵌入与 Pinecone 中存储的所有向量通过
similarity_search函数进行比较,该函数检索最有可能回答问题的前三个文本块。 -
Langchain 然后将前三个文本块作为
context,与用户问题一起传递给 gpt-3.5(ChatCompletion)生成答案。
所有这些都可以通过不到 30 行代码实现:
from langchain.vectorstores import Pinecone
from langchain.chains import VectorDBQAWithSourcesChain
from langchain.embeddings.openai import OpenAIEmbeddings
# 1\. Specify Pinecone as Vectorstore
# =======================================
# 1.1 get pinecone index name
index = pinecone.Index(index_name) #'outside-chatgpt'
# 1.2 specify embedding model
model_name = 'text-embedding-ada-002'
embed = OpenAIEmbeddings(
document_model_name=model_name,
query_model_name=model_name,
openai_api_key=OPENAI_API_KEY
)
# 1.3 provides text_field
text_field = "text"
vectorstore = Pinecone(
index, embed.embed_query, text_field
)
# 2\. Wrap the chain as a function
qa_with_sources = VectorDBQAWithSourcesChain.from_chain_type(
llm=llm,
chain_type="stuff",
vectorstore=vectorstore
)
现在我们可以通过提出一个与徒步旅行相关的问题进行测试:“你能推荐一些加州湾区带水景的高级徒步旅行路线吗?”

Langchain VectorDBQA 带来源
步骤 3:在 Streamlit 中构建我们的应用
在 Jupyter notebook 中验证逻辑有效后,我们可以将所有内容整合在一起,并使用 streamlit 构建前端。在我们的 streamlit 应用中,有两个 python 文件:
-
app.py:前端的主要 python 文件,驱动应用 -
utils.py:由app.py调用的支持函数
这就是我的 utils.py 看起来的样子:
import pinecone
import streamlit as st
from langchain.chains import VectorDBQAWithSourcesChain
from langchain.chat_models import ChatOpenAI
from langchain.vectorstores import Pinecone
from langchain.embeddings.openai import OpenAIEmbeddings
# ------OpenAI: LLM---------------
OPENAI_API_KEY = st.secrets["OPENAI_KEY"]
llm = ChatOpenAI(
openai_api_key=OPENAI_API_KEY,
model_name='gpt-3.5-turbo',
temperature=0.0
)
# ------OpenAI: Embed model-------------
model_name = 'text-embedding-ada-002'
embed = OpenAIEmbeddings(
document_model_name=model_name,
query_model_name=model_name,
openai_api_key=OPENAI_API_KEY
)
# --- Pinecone ------
pinecone_api_key = st.secrets["PINECONE_API_KEY"]
pinecone.init(api_key=pinecone_api_key, environment="us-west1-gcp")
index_name = "outside-chatgpt"
index = pinecone.Index(index_name)
text_field = "text"
vectorstore = Pinecone(index, embed.embed_query, text_field)
# ======= Langchain ChatDBQA with source chain =======
def qa_with_sources(query):
qa = VectorDBQAWithSourcesChain.from_chain_type(
llm=llm,
chain_type="stuff",
vectorstore=vectorstore
)
response = qa(query)
return response
最后,这就是我的 app.py 看起来的样子:
import os
import openai
from PIL import Image
from streamlit_chat import message
from utils import *
openai.api_key = st.secrets["OPENAI_KEY"]
# For Langchain
os.environ["OPENAI_API_KEY"] = openai.api_key
# ==== Section 1: Streamlit Settings ======
with st.sidebar:
st.markdown("# Welcome to chatOutside 🙌")
st.markdown(
"**chatOutside** allows you to talk to version of **chatGPT** \n"
"that has access to latest Outside content! \n"
)
st.markdown(
"Unlike chatGPT, chatOutside can't make stuff up\n"
"and will answer from Outside knowledge base. \n"
)
st.markdown("👩🏫 Developer: Wen Yang")
st.markdown("---")
st.markdown("# Under The Hood 🎩 🐇")
st.markdown("How to Prevent Large Language Model (LLM) hallucination?")
st.markdown("- **Pinecone**: vector database for Outside knowledge")
st.markdown("- **Langchain**: to remember the context of the conversation")
# Homepage title
st.title("chatOutside: Outside + ChatGPT")
# Hero Image
image = Image.open('VideoBkg_08.jpg')
st.image(image, caption='Get Outside!')
st.header("chatGPT 🤖")
# ====== Section 2: ChatGPT only ======
def chatgpt(prompt):
res = openai.ChatCompletion.create(
model='gpt-3.5-turbo',
messages=[
{"role": "system",
"content": "You are a friendly and helpful assistant. "
"Answer the question as truthfully as possible. "
"If unsure, say you don't know."},
{"role": "user", "content": prompt},
],
temperature=0,
)["choices"][0]["message"]["content"]
return res
input_gpt = st.text_input(label='Chat here! 💬')
output_gpt = st.text_area(label="Answered by chatGPT:",
value=chatgpt(input_gpt), height=200)
# ========= End of Section 2 ===========
# ========== Section 3: chatOutside ============================
st.header("chatOutside 🏕️")
def chatoutside(query):
# start chat with chatOutside
try:
response = qa_with_sources(query)
answer = response['answer']
source = response['sources']
except Exception as e:
print("I'm afraid your question failed! This is the error: ")
print(e)
return None
if len(answer) > 0:
return answer, source
else:
return None
# ============================================================
# ========== Section 4\. Display ChatOutside in chatbot style ===========
if 'generated' not in st.session_state:
st.session_state['generated'] = []
if 'past' not in st.session_state:
st.session_state['past'] = []
if 'source' not in st.session_state:
st.session_state['source'] = []
def clear_text():
st.session_state["input"] = ""
# We will get the user's input by calling the get_text function
def get_text():
input_text = st.text_input('Chat here! 💬', key="input")
return input_text
user_input = get_text()
if user_input:
# source contain urls from Outside
output, source = chatoutside(user_input)
# store the output
st.session_state.past.append(user_input)
st.session_state.generated.append(output)
st.session_state.source.append(source)
# Display source urls
st.write(source)
if st.session_state['generated']:
for i in range(len(st.session_state['generated'])-1, -1, -1):
message(st.session_state["generated"][i], key=str(i))
message(st.session_state['past'][i], is_user=True,
avatar_style="big-ears", key=str(i) + '_user')
3. 生产考虑
好了,够了!
应用实际上已经很不错了。但是如果我们想进入生产环境,还有一些额外的事项需要考虑:
-
在 Pinecone 中摄取新的和更新的数据:我们对文章数据进行了单次批量插入。实际上,每天都有新的文章添加到我们的网站中,而且一些字段可能会更新已经摄取到 Pinecone 的数据。这不是机器学习的问题,而是对媒体公司而言常常存在的问题:如何保持每个服务中的数据更新。潜在的解决方案是设置一个定时任务来定期执行插入和更新任务。有关如何并行发送插入操作的指令,如果我们可以使用 Django 和 Celery 的异步任务,这可能会非常有用。
-
Pinecone pod 存储的限制:当前应用使用的是 p1 pod,能够存储最多 1M 个 768 维向量,或者如果我们使用 OpenAI 的
ada-002嵌入模型(其维度为 1536),则大约能存储 50 万个向量。 -
流式处理功能以提高响应速度:为了减少用户感知的延迟,可能有助于向应用添加流式处理功能。这将模拟 chatGPT 按 token 返回生成的输出,而不是一次性显示整个响应。虽然这种功能在使用 LangChain 函数的 REST API 中有效,但对于我们来说,因为我们使用 GraphQL 而不是 REST,这带来了独特的挑战。
4. 常见误解和问题
-
chatGPT 记住了截至 2021 年 9 月的互联网数据,并且基于记忆检索答案。
- 这并不是它的工作方式。训练之后,chatGPT 从内存中删除数据,并使用其 1750 亿个参数(权重)来预测最可能的 token(文本)。它不会基于记忆检索答案。这就是为什么如果你只是复制 chatGPT 生成的答案,你不太可能找到来自互联网的任何来源。
-
我们可以训练/微调/提示工程 chatGPT。
- 训练和微调大型语言模型意味着实际更改模型参数。你需要访问实际的模型文件,并根据你的具体使用情况指导模型。在大多数情况下,我们不会训练或微调 chatGPT。我们所需要的只是提示工程:为 chatGPT 提供额外的上下文,并让它根据这些上下文回答问题。
-
token 和词有什么区别?
-** Token 是一个词片段。100 个 token 大约等于 75 个词。例如,“Unbelievable” 是一个词,但包含 3 个 token(un, belie, able)。**
-
4000-token 限制意味着什么?
-** OpenAI gpt-3.5 的 token 限制为 4096,用于结合用户输入、上下文和响应。当使用 Langchain 的内存时,(用户问题 + 上下文 + 内存 + chatGPT 响应)使用的总词数需要少于 3000 个词(4000 个 token)。**
- gpt-4 有更高的 token 限制,但也贵了 20 倍!(gpt-3.5:$0.002/1000 tokens;gpt-4:$0.045/1000 tokens,假设 500 个用于提示和 500 个用于完成)。
-
我是否必须使用像 Pinecone 这样的向量存储?
-** 不,Pinecone 不是唯一的向量存储选项。其他向量存储选项包括 Chroma、FAISS、Redis 等。此外,你并不总是需要向量存储。例如,如果你想为特定网站构建一个问答系统,你可以爬取网页并参考这个 openai-cookbook-recipe。
告别的话
感谢你阅读这篇长文!如果你对使用 Langchain 有任何问题或建议,请随时联系我。
此外,我将参加 LLM Bootcamp,希望能学习到更多关于将 LLMs 应用到生产中的最佳实践。如果你对这个话题感兴趣,请关注我未来的帖子!😃
为企业建立机器学习操作
原文:
towardsdatascience.com/building-machine-learning-operations-for-businesses-6d0bfbbf2139
支持你的 AI 策略的有效 MLOps 蓝图
·发布于 Towards Data Science ·11 分钟阅读·2023 年 6 月 20 日
--

图片来源:由作者生成,使用 Midjourney
背景 — 探索 MLOps
在我的职业生涯中,我发现成功的 AI 策略的关键在于能够将机器学习模型部署到生产中,从而大规模释放其商业潜力。然而,这不是一件容易的事——它涉及各种技术、团队的整合,并且往往需要组织内部的文化转变,这个系统被称为 MLOps。
然而,没有一种通用的 MLOps 策略。在本文中,我提供了一个灵活的 MLOps 蓝图,可以作为起点或用来微调你当前的工作流程。尽管 MLOps 的旅程可能复杂,我强烈建议将其视为将 AI 融入业务的一个不可或缺的初步步骤,而不是一个次要考虑因素。
MLOps 超越了技术

图片来源:成功 MLOps 策略的组成部分
在深入技术细节之前,我想分享一些我观察到的各种 MLOps 策略中的(非技术性)见解。MLOps 不仅仅是技术——它依赖于三个关键组成部分:投资、文化和技术。那些从一开始就考虑这三方面的公司往往在策略上更成功。我见过的一个常见错误是企业优先投资解决方案,而忽视了必要的文化变革。这种疏忽可能严重破坏你的策略,可能浪费资金并削弱高层管理人员或投资者的信心。
文化
向任何业务引入新文化绝非易事,需要全体员工的全力支持。我常见的一个常见陷阱是,当企业突然用新的、光鲜的工具替代旧工具时,往往忽视了文化变革。这种做法可能引发不满,导致这些工具被忽视或误用。
相反,成功管理文化变革的公司涉及终端用户参与制定 MLOps 策略,并赋予他们推动责任。此外,他们还提供了必要的支持和培训,以提升用户技能,激励他们参与这些举措。
解决方案可能在技术上确实更优,但如果不推动文化变革,它就有可能无效。毕竟,是人们操作技术,而不是技术操作人们。
技术
为了简明起见,我将技术定义为技术基础设施和数据管理服务的组合。
有效的 MLOps 策略建立在成熟的数据生态系统之上。通过利用数据管理工具,数据科学家应能够以安全且符合监管要求的方式访问数据进行模型开发。
从技术基础设施的角度来看,我们应当赋能数据科学家和机器学习工程师,提供所需的硬件和软件,以便促进 AI 产品的开发和交付。对许多公司而言,利用云基础设施是实现这一目标的关键。
投资
在 MLOps 中没有捷径,特别是在投资方面。高效的 MLOps 策略应优先考虑对人力和技术的投资。我遇到的一个常见问题是,由于预算限制,客户往往倾向于围绕单一数据科学家构建 MLOps 策略。在这种情况下,我通常建议重新评估,或至少调整预期。
从一开始,就必须明确你的创新投入的范围及其持续时间。实际上,如果你希望人工智能成为你运营的基础并带来相关好处,持续投资是至关重要的。
有关开发 AI 策略的观点,您可能希望阅读我关于使用 Wardley 图绘制 AI 策略的文章:
通过 Wardley 图绘制 AI 策略的艺术
[towardsdatascience.com
MLOps 的高级蓝图
现在我们已经打下了基础,我们将深入探讨一些 MLOps 的技术组件。为了帮助可视化,我设计了一个流程图,说明了各个过程之间的关系。虚线表示数据流动,实线表示从一个活动到另一个活动的过渡。

作者提供的图像:高层次 MLOps 工作流程
模型开发实验室
模型开发过程本质上是不可预测和反复的。未能认识到这一点的公司将难以建立有效的 AI 策略。实际上,模型开发往往是工作流程中最混乱的部分,充满了实验、重复和频繁的失败。这些元素在探索新解决方案时是必不可少的;创新正是在这里诞生的。那么,数据科学家需要什么?实验、创新和合作的自由。
现在有一种普遍的看法,即数据科学家在编写代码时应遵循软件工程最佳实践。虽然我不反对这种观点,但每件事都有其时间和场所。我不认为模型开发实验室必然是这种实践的场所。与其试图平息这种混乱,我们应该将其视为工作流程的必要部分,并寻求利用能够帮助我们管理混乱的工具 — 一个有效的模型开发实验室应能提供这些。让我们探讨一些潜在的组件。
实验与原型开发 — Jupyter Labs
Jupyter Labs 提供了一个多功能的集成开发环境(IDE),适用于初步模型和概念验证的创建。它提供了对笔记本、脚本和命令行接口的访问,这些都是数据科学家熟悉的功能。
作为一个开源工具,Jupyter Labs 以其与 Python 和 R 的无缝集成,涵盖了当代数据科学模型开发任务的大部分。大多数数据科学工作负载可以在实验室 IDE 中进行。
环境管理 — Anaconda

作者提供的图像:Anaconda 虚拟环境及模型共享的示意图
有效的环境管理可以简化后续的 MLOps 工作流程步骤,重点关注安全访问开源库和再现开发环境。Anaconda,作为一个包管理器,允许数据科学家创建虚拟环境,并使用其简单的命令行接口(CLI)安装模型开发所需的库和包。
Anaconda 还提供了代码库镜像,评估开源包的安全商业使用,尽管需要考虑第三方管理的相关风险。使用虚拟环境在管理实验阶段至关重要,本质上为特定实验提供了一个封闭的空间来容纳所有包和依赖项。
版本控制与协作 — GitHub Desktop
协作是成功的模型开发实验室的重要组成部分,利用 GitHub Desktop 是促进这一点的有效方式。数据科学家可以通过 GitHub Desktop 为每个实验室创建一个仓库。每个仓库存储模型开发的笔记本或脚本,以及一个 environment.yml 文件,该文件指示 Anaconda 如何在另一台机器上重现笔记本开发时的环境。
Jupyter Labs、Anaconda 和 GitHub 这三种实验室组件的结合为数据科学家提供了一个安全的空间进行实验、创新和协作。
#An example environment.yml file replicating a conda environment
name: myenv
channels:
- conda-forge
dependencies:
- python=3.9
- pandas
- scikit-learn
- seaborn
模型管道开发
在与处于 MLOps 成熟初期的客户讨论时,似乎存在这样一种观点:数据科学家开发模型后,将其“交给”机器学习工程师进行“生产化”。这种做法不可行,而且很可能是最快丢失机器学习工程师的方式。没有人愿意处理他人的混乱代码,坦率地说,期望工程师处理这些代码是不公平的。
相反,组织需要培养一种文化,使数据科学家负责在数据实验室中开发模型,然后将其正式化为端到端模型管道。这是为什么:
-
数据科学家对他们的模型了解得比任何人都要多。让他们负责创建模型管道将提高效率。
-
你在每个开发阶段建立一种软件工程最佳实践的文化。
-
机器学习工程师可以专注于能够增加价值的工作,如资源配置、扩展和自动化,而不是重构他人的笔记本。
构建端到端的管道初看起来可能会令人畏惧,但幸运的是,有针对数据科学家的工具可以帮助他们实现这一目标。
模型管道构建 — Kedro
Kedro 是一个由 麦肯锡量子黑客 提供的开源 Python 框架,用于帮助数据科学家构建模型管道。
# Standard template for Kedro projetcs
{{ cookiecutter.repo_name }} # Parent directory of the template
├── conf # Project configuration files
├── data # Local project data
├── docs # Project documentation
├── notebooks # Project related Jupyter notebooks
├── README.md # Project README
├── setup.cfg # Configuration options for tools
└── src # Project source code
└── {{ cookiecutter.python_package }}
├── __init.py__
├── pipelines
├── pipeline_registry.py
├── __main__.py
└── settings.py
├── requirements.txt
├── setup.py
└── tests
Kedro 提供了一个用于构建端到端模型管道的标准模板,结合了软件工程最佳实践。其背后的理念是鼓励数据科学家构建模块化、可重复和易维护的代码。一旦数据科学家完成了 Kedro 工作流,他们实际上构建了一个可以更容易地部署到生产环境的系统。以下是总体概念:
-
项目模板:Kedro 提供了一个标准且易于使用的项目模板,提升了结构性、协作性和效率。
-
数据目录:Kedro 中的数据目录是项目可以使用的所有数据源的注册表。它提供了一种简单的方法来定义数据的存储方式和位置。
数据工程目录由 Kedro 项目定义,详见 docs.kedro.org/en/0.18.1/faq/faq.html
-
管道: Kedro 将数据处理结构化为一个依赖任务的管道,强制执行清晰的代码结构,并可视化数据流和依赖关系。
-
节点: 在 Kedro 中,节点是一个 Python 函数的包装器,指定该函数的输入和输出,作为 Kedro 管道的构建块。
-
配置: Kedro 管理不同环境(开发、生产等)的配置,而不需要将任何配置硬编码到代码中。
-
输入/输出: 在 Kedro 中,I/O 操作与实际计算相抽象,这增加了代码的可测试性和模块化,并简化了不同数据源之间的切换。
-
模块化和可重用性: Kedro 提倡模块化编码风格, resulting in reusable, maintainable and testable code.
-
测试: Kedro 集成了 Python 中的测试框架 PyTest,使得编写管道测试变得简单。
-
版本控制: Kedro 支持数据和代码的版本控制,使得能够重现管道的任何先前状态。
-
日志记录: Kedro 提供了标准化的日志记录系统,用于跟踪事件和变更。
-
钩子和插件: Kedro 支持钩子和插件,根据项目需求扩展框架功能。
-
与其他工具的集成: Kedro 可以与 Jupyter notebook、Dask、Apache Spark 等各种工具集成,以促进数据科学工作流的不同方面。
所有 Kedro 项目都遵循这一基本模板。对数据科学团队实施这一标准将实现可重复性和可维护性。
若要了解 Kedro 框架的更全面概述,请访问以下资源:
-
Kedro 文档: link
-
数据工程中分层思维的重要性: link
注册与存储 — 数据版本控制 (DVC)
注册和存储是机器学习可重复性的基础,任何希望融入机器学习的企业都应牢记这一点。机器学习模型本质上由代码、数据、模型制品和环境组成 — 这些都必须可追溯,以实现可重复性。
DVC 是一个为模型和数据提供版本控制和跟踪的工具。虽然 GitHub 可能是一个替代方案,但它在存储大对象的能力上有限,对大量数据集或模型构成问题。DVC 实质上扩展了 Git,提供相同的版本控制能力,同时支持在本地或云端的 DVC 仓库中存储更大的数据集和模型。
在商业环境中,将代码版本控制在 Git 仓库中有明显的安全优势,同时将实际的模型制品和数据单独存储在受控环境中。
记住,随着关于 AI 商业使用的法规日益严格,模型的可重复性将变得越来越重要。可重复性有助于审计。
模型管道部署 — Docker

作者提供的图片:推理管道的 Docker 部署示意图。请注意,这种方法也可以应用于模型监控和再训练管道
部署不仅仅是一个单一任务,而是一个精心设计的工具、活动和过程的融合;Docker 将这些都联系在一起,用于模型部署。对于具有众多依赖项的复杂 ML 应用程序,Docker 通过将应用程序与其环境封装在一起,确保了在任何机器上的一致性。
这个过程始于一个 Dockerfile;然后,Docker 使用其命令构建一个镜像,这是一个适用于任何启用 Docker 的机器的预打包模型管道。与 Kedro 的管道功能结合使用,Docker 可以高效地部署模型再训练和推理管道,确保 ML 工作流各个阶段的可重复性。
模型监控与再训练管道 — MLflow
随着时间的推移,机器学习模型的性能会受到退化的影响,这可能是由于概念漂移或数据漂移。我们希望能够监控模型性能何时开始下降,并在必要时重新训练它们。MLflow 通过其跟踪 API 提供了这种能力。跟踪 API 应该被纳入数据科学家构建的模型训练和推理管道中。虽然我在模型监控和再训练管道中指定了 MLflow 进行跟踪,但跟踪也可以在模型开发实验室进行,特别是用于实验跟踪。
推理端点
鉴于推理管道已经被封装到 Dockerfile 中,我们可以在任何地方创建该管道的 Docker 镜像,以用作任何应用程序的 API 端点。根据使用情况,我们将不得不决定在哪里部署 Docker 镜像。不过,这超出了本文的范围。
角色与职责
在 MLOps 中分配明确的角色和责任对其成功至关重要。MLOps 的多面性跨越了多个学科,因此需要清晰的角色划分。这确保了每项任务的高效执行。此外,它还促进了责任的明确,从而更快地解决问题。最后,明确的委派减少了混乱和重叠,使团队更高效,并有助于维持和谐的工作环境。这就像一台运行良好的机器,每个齿轮都发挥着完美的作用。
数据科学家
-
角色:数据科学家在 MLOps 策略中的主要功能是专注于模型开发。这包括初步实验、原型设计和为经过验证的模型设置建模管道。
-
职责:数据科学家确保模型遵循机器学习最佳实践,并与业务案例对齐。除了实验室任务,他们还与业务利益相关者沟通,识别有影响的解决方案。他们对数据实验室负责,一个主数据科学家应该设定操作节奏和实验室设置的最佳实践。
机器学习工程师
-
角色:ML 工程师负责监督 MLOps 的技术基础设施,探索创新解决方案,与数据科学家共同制定策略,并提升流程效率。
-
职责:他们确保技术基础设施的功能,监控组件的性能以控制成本,并确认生产模型满足所需规模的需求。
数据治理专业人士
-
角色:数据治理专业人士维护安全性和数据隐私政策,在 MLOps 框架内安全传输数据方面发挥关键作用。
-
职责:虽然数据治理是每个人的责任,这些专业人士会制定政策并通过定期检查和审计确保合规。他们跟进法规,确保所有数据使用者的合规。
结论
在 MLOps 领域的导航是一项需要深思熟虑的规划、正确的技术与人才组合,以及支持变革和学习的组织文化的任务。
这个过程可能看起来很复杂,但通过采用精心设计的蓝图,并将 MLOps 视为一个整体的、迭代的过程而不是一次性的项目,你可以从你的 AI 策略中获得巨大的价值。然而,请记住,没有一种方法适用于所有情况。量体裁衣,调整你的策略以适应具体需求,并保持灵活应对变化的环境,这一点至关重要。
在 LinkedIn 上关注我
订阅 Medium 以获取更多我的见解:
[## 使用我的推荐链接加入 Medium — John Adeojo
我分享数据科学项目、经验和专业知识,以帮助你在旅途中取得成功。你可以通过…
如果你有兴趣将 AI 或数据科学融入到你的业务运营中,我们邀请你预约一次免费的初步咨询:
通过免费的咨询发现我们在帮助企业实现雄心目标方面的专业知识。我们的数据科学家和…
www.data-centric-solutions.com
为我儿子打造的 AI 漫画视频生成器
原文:
towardsdatascience.com/building-owly-an-ai-comic-story-generator-for-my-son-c99fb695d83b
利用在 Amazon SageMaker JumpStart 上精细调整的 Stable Diffusion 2.1,我开发了一种名为 Owly 的 AI 技术,能够制作带有音乐的个性化漫画视频,以我儿子的玩具作为主角。
·发表于 Towards Data Science ·24 分钟阅读·2023 年 4 月 11 日
--

Owly AI 漫画故事讲述者 [AI 生成图像]
每天晚上,与我 4 岁的儿子 Dexie 分享睡前故事已经成为一种珍贵的例行公事,他非常喜欢这些故事。他的书籍收藏相当丰富,但他特别着迷于我从零开始编写的故事。以这种方式创作故事也让我能够融入我希望他学习的道德价值观,这些在商店购买的书籍中可能很难找到。随着时间的推移,我磨练了编写个性化叙事的技巧,点燃了他的想象力——从有裂缝的龙到寻找陪伴的孤独天灯。最近,我编造了像 Slow-Mo Man 和 Fart-Man 这样的虚构超级英雄故事,这些故事已经成为他的最爱。
尽管这段时间对我来说充满了乐趣,但在经过半年的每晚讲故事后,我的创意储备正在经受考验。为了让他持续获得新鲜而有趣的故事而不使自己精疲力竭,我需要一个更可持续的解决方案——一种能够自动生成引人入胜的故事的 AI 技术!我给她起了个名字叫 Owly,以他最喜欢的鸟类——猫头鹰来命名。
Pookie 和通往魔法森林的秘密门——由 AI 漫画生成器生成。
概念
当我开始组装我的愿望清单时,它很快膨胀起来,这种膨胀源于我对测试现代技术前沿的渴望。普通的文本故事是不够的——我设想了一个 AI 制作一个完整的漫画,最多可有 10 个面板。为了增加 Dexie 的兴奋感,我计划使用他熟悉和喜爱的角色,如 Zelda 和 Mario,甚至可以加入他的玩具。坦率地说,个性化的角度源于对漫画条纹的视觉一致性的需求,我稍后会深入讨论。但别急,这还不是全部——我还希望 AI 朗读故事,并配上合适的音乐来营造气氛。完成这个项目对我来说既有趣又具有挑战性,而 Dexie 将会享受到一场量身定制的互动故事盛宴。

Dexie 的玩具作为漫画故事的主要角色 [作者提供的图片]
脑力风暴
为了征服上述要求,我意识到需要组装五个奇妙的模块:
-
故事脚本生成器,编写多段故事,每段故事将被转换为漫画条纹部分。此外,它会推荐一种音乐风格,以从我的库中挑选合适的曲调。为了实现这一目标,我请来了强大的 OpenAI GPT3.5 大型语言模型 (LLM)。
-
漫画图像生成器,为每个故事片段生成图像。Stable Diffusion 2.1 与 Amazon SageMaker JumpStart、SageMaker Studio 和 Batch Transform 联手,将这一切变为现实。
-
文本转语音模块,将书面故事转换为音频叙述。Amazon Polly 的神经引擎跃然而出,提供了救援。
-
视频制作器,将漫画条纹、音频叙述和音乐编织成一个自播放的杰作。MoviePy 是这个节目的明星。
-
最终,控制器将这四个模块的宏伟交响曲协调起来,建立在强大的 AWS Batch 基础上。
游戏计划?让故事脚本生成器编织一个 7-10 段的叙述,每段转变为漫画条纹部分。然后,漫画图像生成器为每个片段生成图像,而文本转语音模块则制作音频叙述。根据故事生成器的推荐选择一段悦耳的音乐。最后,视频制作器将图像、音频叙述和音乐结合在一起,创造一个异想天开的视频。Dexie 将会在这个独一无二的互动故事冒险中获得极大的享受!
漫画图像生成器
在深入探讨故事脚本生成器之前,让我们首先探索图像生成模块,以便为任何关于图像生成过程的参考提供背景。有许多文本到图像的 AI 模型可供选择,但我选择了 Stable Diffusion 2.1 模型,因为它在使用 Amazon SageMaker 和更广泛的 AWS 生态系统进行构建、微调和部署方面非常受欢迎且容易。
Amazon SageMaker Studio 是一个集成开发环境(IDE),提供了一个统一的基于 Web 的界面,处理所有机器学习(ML)任务,简化数据准备、模型构建、训练和部署。这提高了数据科学团队的生产力,最多可提高 10 倍。在 SageMaker Studio 中,用户可以无缝上传数据、创建笔记本、训练和调整模型、调整实验、与团队协作,并将模型部署到生产环境中。
Amazon SageMaker JumpStart 是 SageMaker Studio 中的一个宝贵功能,提供了大量广泛使用的预训练 AI 模型。一些模型,包括 Stable Diffusion 2.1 base,可以使用你自己的训练集进行微调,并附带一个示例 Jupyter Notebook。这使得你可以快速高效地对模型进行实验。

在 Amazon SageMaker JumpStart 上启动 Stable Diffusion 2.1 Notebook [图片来源:作者]
我导航到 Stable Diffusion 2.1 base 视图模型页面,并通过点击 Open Notebook 按钮启动了 Jupyter Notebook。

Stable Diffusion 2.1 Base 模型卡 [图片来源:作者]
几秒钟内,Amazon SageMaker Studio 提供了示例笔记本,其中包含所有必要的代码,用于从 JumpStart 加载文本到图像模型、部署模型,甚至为个性化图像生成进行微调。

Amazon SageMaker Studio IDE [图片来源:作者]
有许多文本到图像的模型可用,许多模型由其创建者为特定风格量身定制。利用 JumpStart API,我筛选并列出了所有文本到图像的模型,使用 filter_value “task == txt2img” 并在下拉菜单中展示以便于选择。
from ipywidgets import Dropdown
from sagemaker.jumpstart.notebook_utils import list_jumpstart_models
# Retrieves all Text-to-Image generation models.
filter_value = "task == txt2img"
txt2img_models = list_jumpstart_models(filter=filter_value)
# display the model-ids in a dropdown to select a model for inference.
model_dropdown = Dropdown(
options=txt2img_models,
value="model-txt2img-stabilityai-stable-diffusion-v2-1-base",
description="Select a model",
style={"description_width": "initial"},
layout={"width": "max-content"},
)
display(model_dropdown)
# Or just hard code the model id and version=*.
# Eg. if we want the latest 2.1 base model
self._model_id, self._model_version = (
"model-txt2img-stabilityai-stable-diffusion-v2-1-base",
"*",
)
我需要的模型是 model-txt2img-stabilityai-stable-diffusion-v2–1-base,它允许微调。

大量文本到图像的模型 [图片来源:作者]
在不到 5 分钟的时间内,利用提供的代码,我将模型部署到了运行 g4dn.2xlarge GPU 实例的 SageMaker 端点。我迅速从我的文本提示中生成了第一张图片,你可以在下面查看展示效果。

我的图像生成器创建了一张海龟在水下游泳的图像 [图片来源:作者]
Amazon SageMaker Studio 简化了我的实验和原型制作过程,使我能够迅速尝试各种图像生成提示,并通过文件浏览器和预览窗口直接在 IDE 中查看生成的图像。此外,我可以在 IDE 中上传图像,利用内置终端启动 AWS CLI 将图像上传到 S3 存储桶或从中下载图像,并执行 SageMaker 批量转换作业以对模型生成大量图像进行大规模测试。
故事脚本生成器
这个模块的任务非常简单:根据故事主题和角色名称制作故事脚本。使用 GPT3.5 API 生成关于特定主题的故事非常简单。
openai.api_key = self._api_key
prompt = "Write me a 1000-word story about Bob the penguin who wants to travel to Europe to see famous landmarks"
response = openai.Completion.create(
model="text-davinci-003",
prompt=prompt,
temperature=0.7,
max_tokens=2089,
top_p=1,
frequency_penalty=0,
presence_penalty=0
)
例如,使用提示“写一篇关于企鹅 Bob 想要去欧洲看著名地标的 1000 字故事。他发现自己的勇气和好奇心让他体验了许多令人兴奋的事情。” GPT3.5 会轻松地编写出引人入胜的故事,就像由专业讲故事的人撰写的一样,下面就是一个类似的例子。
企鹅 Bob 一直梦想着去欧洲旅行,参观著名的地标。他听朋友们讲过巴黎的埃菲尔铁塔、罗马的斗兽场和伦敦的大本钟。他对南极洲的日常生活感到厌倦,渴望冒险。
一天,Bob 决定勇敢地开始规划他的旅行。他花了几个小时研究最佳的旅行路线和最实惠的住宿。经过仔细考虑,他决定从巴黎开始他的旅程。
船程漫长而疲惫,但他对终于到达欧洲感到兴奋。他登记入住酒店后,立即出发去看埃菲尔铁塔。当他走在巴黎的街道上时,他感到前所未有的惊奇和兴奋。
在接下来的几天里,他参观了像卢浮宫、巴黎圣母院和凡尔赛宫这样的著名地标。他尝试了新的食物,结识了新朋友,每一次经历都为他的冒险增添了色彩。
故事本身很棒,但为了将其转化为漫画,我需要将故事分成若干部分,并为每一部分创建一幅图像。最合逻辑的方法是将每个段落转换为一个部分。然而,正如你所见,从这些段落生成的图像呈现出一些重大挑战。

我们的企鹅 Bob 被描绘成不同角色 [AI 生成的图像]
-
角色混乱出现了!每幅漫画中,Bob 都被描绘成完全不同的角色。在第一幅中,他是一个企鹅玩偶;在第二幅中,他是一个人的腿;在第三幅中,他是一个穿着西装的绅士;在第四幅中,他是一个穿蓝衬衫的男人。这种情况发生是因为只有第一段提到了“企鹅 Bob”,而第二段仅称他为“Bob”,其余段落则用“他”来指代。鉴于这些稀少的信息,难怪图像生成器将 Bob 以多种不同的形象呈现。
-
这些场景也缺乏焦点。第一幅漫画展示了一个企鹅玩偶坐在桌子上,而不是一个居住在南极的企鹅梦想着欧洲冒险。在第二幅漫画中也出现了类似的问题,展示了某个人的腿高高飞过城市。看来图像生成器将“一天,鲍勃决定冒险”理解成了从飞机跳伞,而该段落的重点应该是鲍勃计划他的欧洲之行。长段落中有多个焦点往往会使图像生成器感到困惑,导致图像脱离上下文。
为了解决场景缺乏焦点的问题并提高角色的一致性,我对提示进行了细化,要求每段包含一个简洁的一句话场景描述,格式为[],以便可以程序化提取。这使我能够提供详细的示例,并指导图像生成器创建更集中和准确的图像。
对于每个部分,请详细描述场景,并始终在一段中包含位置的句子,格式为[a photo of character in the location]、[a photo of character in front of an object]、[a photo of character next to an object]、[a photo of a location]。
使用更新后的提示,这里是生成的故事。
[鲍勃企鹅在南极的照片]
鲍勃企鹅是一只快乐和好奇的企鹅,生活在南极。他对在那里与朋友和家人一起的生活感到满足。但有一天,他决定冒险,探索冰冷大陆之外的世界。他听说了许多美丽而异国情调的地方,并希望亲自体验。
[鲍勃企鹅在读书的照片]
鲍勃企鹅开始研究世界,查看地图并阅读不同国家和文化的信息。他特别被欧洲吸引,那里的许多著名地标和景点让他向往。他决定欧洲是他想要访问的地方,于是开始计划他的旅行。
[鲍勃企鹅在游轮上的照片]
他开始了漫长的船程。他感到兴奋,迫不及待地想要到达那里,并且决心去欧洲。在几周的旅行后,他终于到达了目的地。
[鲍勃企鹅在埃菲尔铁塔的照片]
鲍勃企鹅开始探索欧洲,对他访问的各种不同地方感到惊叹。他去了巴黎的埃菲尔铁塔、罗马的斗兽场和爱尔兰的莫赫悬崖。他去的每个地方都让他充满了敬畏和喜悦。
正如你所观察到的,生成的场景描述要集中得多。它们提到单一场景、位置和/或正在进行的活动,通常以角色的名字开头。这些简洁的提示对于我的图像生成器效果显著,如下所示的改进图像就是证明。

我们的 Bob 企鹅的更一致外观 [AI 生成的图像]
Bob 企鹅已成功回归,但他在每个漫画条幅中依然呈现出全新的形象。由于图像生成过程将每张图像视为独立的,而且没有提供有关 Bob 的颜色、大小或企鹅类型的信息,因此一致性仍然难以实现。
我之前考虑过将详细的角色描述作为故事生成的一部分,以保持图像中的角色一致性。然而,这种方法由于两个原因被证明是不切实际的:
-
有时,很难在不 resorting 到大量文本的情况下详细描述一个角色。虽然企鹅的种类可能不多,但考虑到鸟类的一般情况——有着无数形状、颜色和品种如葵花鹦鹉、鹦鹉、金丝雀、鹈鹕和猫头鹰,这项任务变得令人生畏。
-
生成的角色不总是符合提示中提供的描述。例如,一个描述绿色鹦鹉带红色喙的提示可能会生成一只绿色鹦鹉带黄色喙的图像。
所以,尽管我们尽了最大努力,我们的企鹅朋友 Bob 仍然经历着某种身份危机。
为图像生成器添加个性化
我们解决企鹅困境的方法在于为 Stable Diffusion 模型提供一个关于我们企鹅角色应有外观的视觉提示,以影响图像生成过程并保持生成图像的一致性。在 Stable Diffusion 的世界中,这一过程被称为微调,你需要提供一些(通常是 5 到 15 张)包含相同对象的图像以及描述它的句子。这些图像将被称为训练图像。
结果是,这种个性化不仅仅是解决方案,还是我漫画生成器的一个非常酷的功能。现在,我可以将 Dexie 的许多玩具作为故事中的主要角色,例如他节日的圣诞企鹅,为 Bob 企鹅注入新生命,使其对我年轻但坚韧的观众更加个性化和易于关联。因此,一致性的追求变成了为定制故事的胜利!

Dexie 的玩具现在是 Bob 企鹅 [作者提供的图像]
在我令人兴奋的实验日子里,我发现了一些智慧的结晶可以分享,以在微调模型时实现最佳结果,减少过拟合的可能性:
-
保持训练图像中的背景多样化。这样,模型就不会将背景与对象混淆,防止在生成图像中出现不必要的背景配角。
-
从不同角度捕捉目标对象。这有助于提供更多视觉信息,使模型能够生成具有更大角度范围的对象,从而更好地匹配场景。
-
混合特写镜头和全身镜头。这确保了模型不会假设特定姿势是必要的,从而为生成的对象与场景的和谐提供了更多灵活性。
为了执行稳定扩散模型的精细调整,我启动了一个 SageMaker Estimator 训练作业,使用 Amazon SageMaker Python SDK 在 ml.g5.2xlarge GPU 实例上,并将训练过程定向到 S3 桶中的训练图像集合。生成的精细调整模型文件将保存在 s3_output_location 中。仅需几行代码,魔力便开始显现!
# [Optional] Override default hyperparameters with custom values
hyperparams["max_steps"] = 400
hyperparams["with_prior_preservation"] = False
hyperparams["train_text_encoder"] = False
training_job_name = name_from_base(f"stable-diffusion-{self._model_id}-transfer-learning")
# Create SageMaker Estimator instance
sd_estimator = Estimator(
role=self._aws_role,
image_uri=image_uri,
source_dir=source_uri,
model_uri=model_uri,
entry_point="transfer_learning.py", # Entry-point file in source_dir and present in train_source_uri.
instance_count=self._training_instance_count,
instance_type=self._training_instance_type,
max_run=360000,
hyperparameters=hyperparams,
output_path=s3_output_location,
base_job_name=training_job_name,
sagemaker_session=session,
)
# Launch a SageMaker Training job by passing s3 path of the training data
sd_estimator.fit({"training": training_dataset_s3_path}, logs=True)
为准备训练集,确保它包含以下文件:
-
一系列名为 instance_image_x.jpg 的图像,其中 x 是从 1 到 N 的数字。在这种情况下,N 代表图像的数量,理想情况下应大于 10。
-
一个 dataset_info.json 文件,其中包含一个必需字段,称为 instance_prompt。该字段应提供对象的详细描述,并在对象名称前加上唯一标识符。例如,“Bob 企鹅的照片”,其中‘Bob’充当唯一标识符。通过使用该标识符,你可以引导精细调整后的模型生成标准企鹅(称为“企鹅”)或来自训练集中的企鹅(称为“Bob 企鹅”)。一些来源建议使用像 sks 或 xyz 这样的唯一名称,但我发现这样做并非必要。
dataset_info.json 文件还可以包含一个可选字段,称为 class_prompt,它提供对象的一般描述,而没有唯一标识符(例如,“一只企鹅的照片”)。此字段仅在 prior_preservation 参数设置为 True 时使用;否则,将被忽略。我将在下面的高级精细调整部分进一步讨论。
{"instance_prompt": "a photo of bob penguin",
"class_prompt": "a photo of a penguin"
}
经过几次使用 Dexie 的玩具进行测试后,图像生成器产生了一些真正令人印象深刻的结果。它将 Dexie 的袋鼠磁性积木创作栩栩如生地呈现出来,使其跃入虚拟世界。生成器还巧妙地描绘了他心爱的淋浴乌龟玩具在水下游泳的场景,周围环绕着色彩斑斓的鱼群。图像生成器确实捕捉到了 Dexie 玩耍时最喜欢的魔力!

Dexie 的玩具栩栩如生 [AI 生成的图像]
针对精细调整的稳定扩散模型进行批量转换
由于我需要为每个漫画条生成一百多张图像,因此部署 SageMaker 端点(可以将其视为 Rest API)并一次生成一张图像并不是最有效的方法。因此,我选择对我的模型进行批量转换,向其提供包含生成图像提示的 S3 桶中的文本文件。
我会提供更多关于这个过程的细节,因为我最初对它感到困惑,我希望我的解释能为你节省一些时间。你需要为每个图像提示准备一个文本文件,文件中包含以下 JSON 内容:{“prompt”: “a photo of Bob the penguin in Antarctica”}。虽然似乎有一种方法可以使用 MultiRecord 策略将多个输入合并到一个文件中,但我未能弄清楚它的工作原理。
我遇到的另一个挑战是对我的微调模型执行批量转换。你不能使用 Estimator.transformer() 返回的转换器对象执行批量转换,这通常在我之前的项目中有效。相反,你需要先创建一个 SageMaker 模型对象,将微调模型的 S3 位置指定为 model_data。之后,你可以使用这个模型对象创建转换器对象。
def _get_model_uris(self, model_id, model_version, scope):
# Retrieve the inference docker container uri
image_uri = image_uris.retrieve(
region=None,
framework=None, # automatically inferred from model_id
image_scope=scope,
model_id=model_id,
model_version=model_version,
instance_type=self._inference_instance_type,
)
# Retrieve the inference script uri. This includes scripts for model loading, inference handling etc.
source_uri = script_uris.retrieve(
model_id=model_id, model_version=model_version, script_scope=scope
)
if scope == "training":
# Retrieve the pre-trained model tarball to further fine-tune
model_uri = model_uris.retrieve(
model_id=model_id, model_version=model_version, model_scope=scope
)
else:
model_uri = None
return image_uri, source_uri, model_uri
image_uri, source_uri, model_uri = self._get_model_uris(self._model_id, self._model_version, "inference")
# Get model artifact location by estimator.model_data, or give an S3 key directly
model_artifact_s3_location = f"s3://{self._bucket}/output-model/{job_id}/{training_job_name}/output/model.tar.gz"
env = {
"MMS_MAX_RESPONSE_SIZE": "20000000",
}
# Create model from saved model artifact
sm_model = model.Model(
model_data=model_artifact_s3_location,
role=self._aws_role,
entry_point="inference.py", # entry point file in source_dir and present in deploy_source_uri
image_uri=image_uri,
source_dir=source_uri,
env=env
)
transformer = sm_model.transformer(instance_count=self._inference_instance_count, instance_type=self._inference_instance_type,
output_path=f"s3://{self._bucket}/processing/{job_id}/output-images",
accept='application/json')
transformer.transform(data=f"s3://{self._bucket}/processing/{job_id}/batch_transform_input/",
content_type='application/json')
就这样,我的定制图像生成器已经准备好了!
高级 Stable Diffusion 模型微调
虽然这对我的漫画生成器项目并非必需,但我想提到一些涉及 max_steps、prior_reservation 和 train_text_encoder 超参数的高级微调技术,以防它们对你的项目有用。
Stable Diffusion 模型的微调由于你提供的训练图像数量与基础模型使用的图像数量之间的巨大差异,非常容易过拟合。例如,你可能只提供了 10 张 Bob the penguin 的图像,而基础模型的训练集包含了数千张企鹅图像。图像数量更多会降低过拟合的可能性,并减少目标对象与其他元素之间的错误关联。
当将 prior_reservation 设置为 True 时,Stable Diffusion 使用提供的 class_prompt 生成默认数量的 x 张图像(通常为 100),并在微调过程中将这些图像与 instance_images 结合起来。或者,你可以通过将这些图像放置在 class_data_dir 子文件夹中来手动提供这些图像。根据我的经验,prior_preservation 在对 Stable Diffusion 进行微调以生成真人面孔时通常至关重要。当使用 prior_reservation 时,确保提供一个提到最合适的通用名称或与角色相似的常见对象的 class_prompt。对于 Bob the penguin,这个对象显然是一只企鹅,所以你的类提示应该是 “a photo of a penguin”。这种技术也可以用来生成两个角色之间的混合体,稍后我会讨论。
另一个对高级微调有帮助的参数是 train_text_encoder。将其设置为 True 以启用在微调过程中对文本编码器的训练。结果模型将更好地理解更复杂的提示,并更准确地生成真人面孔。
根据你具体的使用案例,不同的超参数值可能会产生更好的结果。此外,你需要调整 max_steps 参数来控制所需的微调步骤数。请记住,较大的训练集可能会导致过拟合。
文本转语音和视频生成
通过利用 Amazon Polly 的神经文本转语音(NTTS)功能,我能够为故事的每个段落创建音频叙述。音频叙述的质量非常出色,因为它听起来非常自然和像人类发声,使其成为理想的讲故事者。
为了适应年轻观众,如 Dexie,我采用了 SSML 格式,并利用了
self._pollyClient = boto3.Session(
region_name=aws_region).client('polly')
ftext = f"<speak><prosody rate=\"90%\">{text}</prosody></speak>"
response = self._pollyClient.synthesize_speech(VoiceId=self._speaker,
OutputFormat='mp3',
Engine='neural',
Text=ftext,
TextType='ssml')
with open(mp3_path, 'wb') as file:
file.write(response['AudioStream'].read())
file.close()
在所有辛勤工作之后,我使用了 MoviePy —— 一个非常棒的 Python 框架 —— 神奇地将所有照片、音频叙述和音乐转变成一个令人惊叹的 mp4 视频。说到音乐,我让我的技术选择完美的配乐来匹配视频的氛围。怎么做的呢?好吧,我只是修改了我的故事脚本生成器,使用一些巧妙的提示从预定列表中返回一种音乐风格。这有多酷?
在故事开始时,请从以下列表中建议匹配故事的歌曲风格,并用<>标出。歌曲风格列表包括 action、calm、dramatic、epic、happy 和 touching。
一旦选择了音乐风格,下一步是从相关文件夹中随机挑选一个 MP3 曲目,该文件夹包含了一些 MP3 文件。这有助于为最终产品增添一点不可预测性和兴奋感。
控制模块
为了协调整个系统,我需要一个以 Python 脚本形式存在的控制模块,它能够无缝地运行每个模块。当然,我还需要一个计算环境来执行这个脚本。我有两个选择 — 第一个是我偏好的选择 — 使用 AWS Lambda 的无服务器架构。这涉及使用多个 AWS Lambda,配合 SQS。第一个 Lambda 作为公共 API,使用 API Gateway 作为入口点。这个 API 会接收训练图像的 URL 和故事主题文本,并对数据进行预处理,将其放入 SQS 队列中。另一个 Lambda 会从主题中提取数据并进行数据准备 —— 比如图像调整大小、创建 dataset_info.json,并触发下一个 Lambda 调用 Amazon SageMaker Jumpstart 来准备 Stable Diffusion 模型,并执行 SageMaker 训练作业以微调模型。唷,这真是一口气说完。最后,Amazon EventBridge 将作为事件总线,用于检测训练作业的完成,并触发下一个 Lambda 使用微调后的模型执行 SageMaker Batch Transform 生成图像。
然而,这个选项不可行,因为 AWS Lambda 函数的最大存储限制为 10GB。在对 SageMaker 模型执行批处理转换时,SageMaker Python SDK 会在本地 /tmp 临时下载并提取 model.tar.gzip 文件,然后将其发送到运行批处理转换的管理系统。不幸的是,我的模型压缩后为 5GB,因此 SageMaker Python SDK 抛出一个错误,说“磁盘空间不足。”对于大多数模型尺寸较小的使用场景,这将是最佳和最清洁的解决方案。
因此,我不得不选择第二个选项——AWS Batch。它运行良好,但成本稍高,因为 AWS Batch 计算实例必须在整个过程中运行——即使在对模型进行微调和执行批处理转换时,这些操作都是在 SageMaker 内的独立计算环境中执行的。我本可以将过程拆分为多个 AWS Batch 实例,并通过 Amazon EventBridge 和 SQS 将它们连接起来,就像我之前使用无服务器方法一样。但由于 AWS Batch 启动时间较长(约 5 分钟),这会给整体过程增加过多的延迟。因此,我选择了集成的 AWS Batch 选项。

Owly 系统架构
欣赏 Owly 壮丽的架构图吧!我们的冒险从通过 AWS 控制台启动 AWS Batch 开始,为其配备充满训练图像的 S3 文件夹、一个引人入胜的故事主题和一个令人愉快的角色,这些都通过 AWS Batch 环境变量提供。
# Basic settings
JOB_ID = "penguin-images" # key to S3 folder containing the training images
STORY_TOPIC = "bob the penguin who wants to travel to Europe"
STORY_CHARACTER = "bob the penguin"
# Advanced settings
TRAIN_TEXT_ENCODER = False
PRIOR_RESERVATION = False
MAX_STEPS = 400
NUM_IMAGE_VARIATIONS = 5
AWS Batch 立即启动,从 JOB_ID 指定的 S3 文件夹中检索训练图像,将其调整为 768x768,并在将其放入暂存 S3 桶之前创建 dataset_info.json 文件。
接下来,我们调用 OpenAI GPT3.5 模型 API 来编写一个引人入胜的故事,并与所选主题和角色相协调的补充歌曲风格。然后我们召唤 Amazon SageMaker JumpStart 来释放强大的 Stable Diffusion 2.1 基础模型。使用该模型,我们启动 SageMaker 训练作业,对其进行微调以适应我们精心挑选的训练图像。经过短短 30 分钟的间歇后,我们为每个故事段落制作图像提示,以文本文件的形式,然后将其放入 S3 桶中,作为图像生成盛宴的输入。Amazon SageMaker Batch Transform 被用来在短短 5 分钟内批量生成这些图像。
完成后,我们借助 Amazon Polly 为故事中的每个段落制作音频讲解,仅需 30 秒即可将其保存为 mp3 文件。然后我们从按歌曲风格分类的库中随机挑选一个 mp3 音乐文件,基于我们巧妙的故事生成器的选择。
最终的工作将生成的图像、音频讲述 mp3 文件和音乐.mp3 文件巧妙地编织成一个视频幻灯片,借助 MoviePy 完成。为了增加优雅感,我们添加了平滑的过渡和肯·伯恩斯效果。最后的杰作,完成的视频,随后被上传到输出 S3 存储桶,等待你的急切下载!
测试并添加一些增强功能
我必须说,我对结果感到相当满意!故事脚本生成器真的是超乎预期地表现出色。几乎每一个制作的故事脚本不仅写得很好,而且充满了积极的道德观,展示了大型语言模型(LLM)的令人惊叹的能力。至于图像生成,嘛,这就有点参差不齐了。
根据我之前描述的所有改进,一五分之一的故事可以直接用于最终视频。其余的四分之一,通常有一到两张图像存在常见问题。
-
首先,我们仍然遇到不一致的角色。有时模型会生成一个与训练集中原始角色略有不同的角色,通常选择现实主义版本而不是玩具版本。但不要担心!在文本提示中添加期望的照片风格,如“一个卡通风格的海龟雷克斯在海里游泳”,有助于缓解这个问题。然而,这确实需要人工干预,因为某些角色可能需要现实主义风格。
-
然后就是缺失身体部位的奇特情况了。偶尔,我们生成的角色会出现四肢或头部缺失的情况。哎呀!为了解决这个问题,我们在稳定扩散模型中添加了负面提示,比如“缺失四肢,缺失头部”,以鼓励生成避免这些奇特属性的图像。

雷克斯海龟的不同风格(右下角的图像为现实主义风格,右上角的图像为混合风格,其余为玩具风格)和缺少头部(左上角的图像)[AI 生成的图像]
- 当处理对象之间不常见的互动时,会出现奇异的图像。生成角色在特定位置的图像通常会得到令人满意的结果。然而,当涉及到描绘角色与其他对象互动时,尤其是以不寻常的方式,结果往往不尽如人意。例如,尝试描绘刺猬汤姆挤奶牛时,会产生刺猬与奶牛的奇特混合体。同时,制作汤姆·刺猬拿着花束的图像则会变成一个人同时抓着刺猬和花束。不幸的是,我尚未制定出解决这一问题的策略,这使我得出结论,这是当前图像生成技术的局限。如果你试图生成的图像中的对象或活动非常不寻常,模型缺乏先验知识,因为训练数据中从未出现过这样的场景或活动。

从“汤姆刺猬在挤奶牛”提示生成的混合体(顶部图像)是刺猬和奶牛的混合。 从“汤姆刺猬拿着花”生成的图像(底部左侧图像)则是一个人同时抱着刺猬和花束 [AI 生成的图像]
最终,为了提高故事生成的成功率,我巧妙地调整了我的故事生成器,使其每段生成三个不同的场景。此外,对于每个场景,我指示我的图像生成器创建五个图像变体。通过这种方法,我增加了从十五个可用图像中获得至少一个优质图像的可能性。拥有三种不同的提示变体也有助于生成完全独特的场景,尤其是当一个场景过于稀有或复杂以至于无法创建时。以下是我更新后的故事生成提示。
"Write me a {max_words} words story about a given character and a topic.\nPlease break the story down into " \
"seven to ten short sections with 30 maximum words per section. For each section please describe the scene in " \
"details and always include the location in one sentence within [] with the following format " \
"[a photo of character in the location], [a photo of character in front of an object], " \
"[a photo of character next to an object], [a photo of a location]. Please provide three different variations " \
"of the scene details separated by |\\nAt the start of the story please suggest song style from the following " \
"list only which matches the story and put it within <>. Song style list are action, calm, dramatic, epic, " \
"happy and touching."
唯一的额外成本是在图像生成步骤完成后进行一点手动干预,我会挑选每个场景的最佳图像,然后继续进行漫画生成过程。尽管有这一小小的不便,我现在在制作精彩漫画方面的成功率达到了 9/10!
上演时刻
随着 Owly 系统的全面组装,我决定在一个美好的星期六下午对这项技术奇迹进行测试。我从他的玩具收藏中生成了一些故事,准备利用我购买的巧妙便携式投影仪来提升 Dexie 的睡前故事体验。那天晚上,当我看到 Dexie 的脸上绽放出兴奋的光芒和眼睛睁大的神情,漫画在他的卧室墙上播放时,我知道我的努力都值得了。

Dexie 正在看着他卧室墙上的漫画 [图片来源于作者]
最棒的是,现在我只需不到两分钟就能利用我已经拍摄的玩具角色照片创作一个新故事。此外,我可以无缝地将我希望他从每个故事中学到的宝贵道德融入其中,比如不与陌生人交谈、勇敢冒险或对他人友善和乐于助人。以下是这个神奇系统生成的一些令人愉快的故事。
《超级刺猬汤姆拯救他的城市免受龙的威胁》 — 由 AI 漫画生成器生成。
《勇敢的企鹅鲍勃:欧洲历险记》 — 由 AI 漫画生成器生成。
一些有趣的实验
作为一个好奇的发明家,我忍不住尝试图像生成模块,以推动 Stable Diffusion 的边界,并将两个角色合并成一个宏伟的混合体。我用科瓦兹·奥克诺特的图像对模型进行了微调,但我加了一点变化,把塞尔达设定为既独特又经典的角色名。设置prior_preservation为 True,我确保 Stable Diffusion 会“奥克诺特化”塞尔达,同时保持她的独特本质。
我巧妙地利用了适度的max_step为 400,刚好能够保留塞尔达的原始魅力,同时避免她被科瓦兹·奥克诺特的无法抗拒的魅力完全吞噬。请看塞尔达与科瓦兹的辉煌融合,合而为一!
Dexie 兴奋不已地见证了他最喜欢的两个角色在他的睡前故事中大展拳脚。他踏上了激动人心的冒险,打击外星生物并寻找隐藏的宝藏!
不幸的是,为了保护知识产权,我不能展示生成的图像。
生成型 AI,特别是大型语言模型(LLMs),将长期存在,并成为不仅仅是软件开发,还包括许多其他行业的强大工具。我在一些项目中亲身体验了 LLMs 的真正力量。就在去年,我构建了一个名为 Ellie 的机器人泰迪熊,能够移动头部并像真正的人类一样进行对话。虽然这项技术无疑强大,但重要的是要谨慎使用,以确保生成结果的安全性和质量,因为它可能是一把双刃剑。
各位,这就是全部了!希望你们觉得这个博客有趣。如果是的话,请给我多多点赞。随时在LinkedIn与我联系,或查看我在Medium 个人主页上的其他 AI 项目。敬请关注,我将在接下来的几周内分享完整的源代码!
最后,我要感谢来自 AWS 的Mike Chambers,他帮助我排查了微调后的 Stable Diffusion 模型批量转换代码的问题。
从基础构建 PCA
原文:
towardsdatascience.com/building-pca-from-the-ground-up-434ac88b03ef
通过一步步的推导,超级提升你对主成分分析的理解
·发表于数据科学前沿 ·12 分钟阅读·2023 年 8 月 7 日
--

热气球。图像由作者提供。
主成分分析(PCA)是一种常用于降维的旧技术。尽管在数据科学家中这是一个广为人知的话题,但 PCA 的推导通常被忽视,留下了关于数据本质和微积分、统计学以及线性代数之间关系的宝贵见解。
在本文中,我们将通过思想实验推导 PCA,从二维开始,扩展到任意维度。随着我们逐步推进每一项推导,我们将看到看似不同的数学分支之间的和谐互动,最终达到优雅的坐标变换。这一推导将揭示 PCA 的机制,并揭示数学概念的迷人相互联系。让我们开始这段启发性的 PCA 探索之旅。
在二维中热身
作为生活在三维世界中的人类,我们通常理解二维概念,这也是本文的起点。从二维开始将简化我们的首次思想实验,并使我们更好地理解问题的本质。
理论
我们有一个数据集,长得像这样(请注意,每个特征应该被缩放到均值为 0 和方差为 1):

(1) 相关数据。图像由作者提供。
我们立即注意到这些数据位于由x1和x2描述的坐标系中,这些变量是相关的。我们的目标是找到一个由数据的协方差结构决定的新坐标系。 特别是,坐标系中的第一个基向量应解释将原始数据投影到其上的大部分方差。
我们的首要任务是找到一个向量,使得当我们将原始数据投影到这个向量上时,保留最大量的方差。换句话说,理想的向量指向方差最大化的方向,正如数据所定义的那样。
这个向量可以通过它与 x 轴逆时针方向所成的角度来定义:

(2) 通过旋转向量来寻找最大方差的方向。图像由作者提供。
在上面的动画中,我们通过与 x 轴的角度来定义一个向量,我们可以看到向量在 0 到 180 度之间的每个角度指向的方向。从视觉上看,我们可以看到一个接近 45 度的θ值指向数据方差的最大方向。
为了重新阐述我们的目标,我们希望找到一个角度,使得向量指向方差最大化的方向。从数学上讲,我们希望找到一个最大化这个方程的θ:

(3) 最佳的θ将最大化这个目标函数。图像由作者提供。
在这里,N是数据中的观察次数。我们将每个观察值投影到由[cos(θ) sin(θ)]定义的轴上,这是一个由角度θ定义的单位向量,并对结果进行平方。
当我们改变θ时,这个方程给出了当数据投影到由θ定义的轴上的方差。让我们计算方程中的点积并重写这个表达式,使其更易于处理:

(4) 重写后的方差方程。图像由作者提供。
幸运的是,这是一个凸函数,我们可以通过计算它的导数并将其设为 0 来最大化它。我们还需要计算方差方程的二阶导数,以确定我们是否找到了最小值或最大值。var(θ)的一阶和二阶导数如下所示:

(5) var(θ)的一阶和二阶导数。图像由作者提供。
接下来,我们可以将一阶导数设为 0,并重排方程以隔离θ:

(6) 将 var(θ)的一阶导数设为 0 并重排。图像由作者提供。
最后,利用常见的三角恒等式和一些代数,我们得到了一个使θ最小化或最大化var(θ)的封闭形式解:

(7) 计算θ的方程,用于找到最大或最小方差的方向。图像由作者提供。
值得注意的是,这个方程是我们在二维空间进行 PCA 所需的全部。二阶导数将告诉我们θ是否对应于局部最小值或最大值。由于只有另一个主成分,它必须通过将θ偏移 90 度来定义。因此,两个主成分角度是:

(8) 定义二维主成分的角度。图像由作者提供。
如前所述,我们可以使用var(θ)的二阶导数来确定哪个θ属于主成分 1(最大方差的方向),哪个θ属于主成分 2(最小方差的方向)。另外,我们也可以将两个θs代入var(θ)中,看看哪个结果更高。
一旦我们知道哪个θ对应于每个主成分,我们就将每一个代入二维单位向量的三角函数方程([cos(θ) sin(θ)])。具体来说,我们做如下操作:

(9) 从最大化或最小化 var(θ) 的 θs 确定两个主成分。图像由作者提供。
就是这样——pc1和pc2是主成分向量。通过思考主成分分析的目标,我们能够在二维空间中从零开始推导出主成分。为了验证这个结果是否正确,让我们编写一些 Python 代码来实现我们的策略。
代码
第一个函数找到从方差方程的导数中推导出的主角度之一。由于这是一个封闭形式的方程,实现起来非常简单:
import numpy as np
def find_principal_angle(x1: np.ndarray, x2: np.ndarray) -> float:
"""
Find the angle corresponding to one of the principal components
in two dimensions.
Parameters
----------
x1 : numpy.ndarray
First input vector with shape (n,).
x2 : numpy.ndarray
Second input vector with shape (n,).
Returns
-------
float
The principal angle in radians.
"""
cov = -2 * np.sum(x1 * x2)
var_diff = np.sum(x2**2 - x1**2)
return 0.5 * np.arctan(cov / var_diff)
根据方差方程的性质,find_principal_angle() 恢复一个主角度,另一个主角度则相差 90 度。为了确定 find_principal_angle() 返回的是哪个主角度,我们可以使用方差方程的二阶导数或 Hessian 矩阵:
def compute_pca_cost_hessian(x1: np.ndarray,
x2: np.ndarray,
theta: float) -> float:
"""
Compute the Hessian of the cost function for Principal Component
Analysis (PCA) in two dimensions.
Parameters
----------
x1 : numpy.ndarray
First input vector with shape (n,).
x2 : numpy.ndarray
Second input vector with shape (n,).
theta : float
An angle in radians for which the cost function is evaluated.
Returns
-------
float
The Hessian of the PCA cost function evaluated at the given theta.
"""
return np.sum(
2 * (x2**2 - x1**2) * np.cos(2 * theta) -
4 * x1 * x2 * np.sin(2 * theta)
)
这个函数的逻辑直接来源于图 (5)。我们需要的最后一个函数是从 find_principal_angle() 和 compute_pca_cost_hessian() 确定两个主成分:
def find_principal_components_2d(x1: np.ndarray, x2: np.ndarray) -> tuple:
"""
Find the principal components of a two-dimensional dataset.
Parameters
----------
x1 : numpy.ndarray
First input vector with shape (n,).
x2 : numpy.ndarray
Second input vector with shape (n,).
Returns
-------
tuple
A tuple containing the two principal components represented as
numpy arrays.
"""
theta0 = find_principal_angle(x1, x2)
theta0_hessian = compute_pca_cost_hessian(x1, x2, theta0)
if theta0_hessian > 0:
pc1 = np.array([np.cos(theta0 + (np.pi / 2)),
np.sin(theta0 + (np.pi / 2))])
pc2 = np.array([np.cos(theta0), np.sin(theta0)])
else:
pc1 = np.array([np.cos(theta0), np.sin(theta0)])
pc2 = np.array([np.cos(theta0 + (np.pi / 2)),
np.sin(theta0 + (np.pi / 2))])
return pc1, pc2
在 find_principal_components_2d() 中,theta0 是其中一个主角度,theta0_hessian 是方差方程的二阶导数。因为 theta0 是方差方程的极值点,theta0_hessian 告诉我们 theta0 是最小值还是最大值。特别地,如果 theta0_hessian 为正,则 theta0 必定是最小值,对应于第二主成分。否则,如果 theta0_hessian 为负,theta0 则是最大值,对应于第一主成分。
为了验证find_principal_components_2d()是否做了我们想要的事情,让我们找到一个二维数据集的主成分,并将其与scikit-learn 中的 PCA 实现的结果进行比较。以下是代码:
import numpy as np
from sklearn.decomposition import PCA
# Generate two random correlated arrays
rng = np.random.default_rng(seed=80)
x1 = rng.normal(size=1000)
x2 = x1 + rng.normal(size=len(x1))
# Normalize the data
x1 = (x1 - x1.mean()) / x1.std()
x2 = (x2 - x2.mean()) / x2.std()
# Find the principal components using the 2D logic
pc1, pc2 = find_principal_components_2d(x1, x2)
# Find the principal components using sklearn PCA
model = PCA(n_components=2)
model.fit(np.array([x1, x2]).T)
pc1_sklearn = model.components_[0, :]
pc2_sklearn = model.components_[1, :]
print(f"Derived PC1: {pc1}")
print(f"Sklearn PC1: {pc1_sklearn} \n")
print(f"Derived PC2: {pc2}")
print(f"Sklearn PC2: {pc2_sklearn}")
"""
Derived PC1: [0.70710678 0.70710678]
Sklearn PC1: [0.70710678 0.70710678]
Derived PC2: [ 0.70710678 -0.70710678]
Sklearn PC2: [ 0.70710678 -0.70710678]
"""
# Visualize the results
fig, ax = plt.subplots(figsize=(10, 6))
ax.scatter(x1, x2, alpha=0.5)
ax.quiver(0, 0, pc1[0], pc1[1], scale_units='xy', scale=1, color='red')
ax.quiver(0, 0, pc2[0], pc2[1], scale_units='xy', scale=1, color='red', label="Custom PCs")
ax.quiver(0, 0, pc1_sklearn[0], pc1_sklearn[1], scale_units='xy', scale=1, color='green')
ax.quiver(0, 0, pc2_sklearn[0], pc2_sklearn[1], scale_units='xy', scale=1, color='green', label="Sklearn PCs")
ax.set_xlabel("$x_1$")
ax.set_ylabel("$x_2$")
ax.set_title("Custom 2D PCA vs Sklearn PCA")
ax.legend()
这段代码创建了二维相关数据,进行了标准化,并使用我们的自定义逻辑和scikit-learn PCA 实现找到了主成分。我们可以看到,我们得到了与 scikit-learn 完全相同的结果。得到的主成分如下:

(10) 我们的二维 PCA 方法与 scikit-learn 实现完全匹配。图像来源:作者。
如预期的那样,两组主成分完全重叠。然而,我们方法的问题是它无法推广到二维以外的情况。在下一节中,我们将推导出任意维度的类似结果。
推导一般结果
在二维中,我们利用方差定义、微积分和一点三角学推导出了一个封闭形式的方程来找到主成分。不幸的是,这种方法对于超过二维的数据集很快会变得不可行。为此,我们必须依赖于数学中最强大的分支——线性代数来进行计算。
理论
PCA 在更高维度的推导在许多方面类似于二维情况。根本的区别在于,我们必须利用一些线性代数工具来帮助我们考虑主成分与数据原点可能形成的所有角度。
首先,假设我们的数据集有d个特征和n个观察值。如前所述,每个特征应缩放到均值为 0 且方差为 1。我们将数据排列成如下矩阵:

(11) PCA 的数据矩阵。图像来源:作者。
如前所述,我们的目标是找到一个单位向量,p,它指向数据方差最大方向。为此,我们首先需要陈述两个有用的事实。第一个是我们数据的协方差矩阵,在更高维度中的方差的类似物,可以通过将 X 与其转置相乘找到:

(12) 计算 X 的协方差矩阵的方程。图像来源:作者。
尽管这很有用,我们并不关心数据本身的方差。我们想知道数据在投影到新轴上的方差。 为此,我们回顾一个关于标量量的方差的结果。即,当标量乘以常数时,得到的方差是该常数的平方乘以原始方差:

(13) 乘以常数的标量方差。图片由作者提供。
类似地,当我们用数据矩阵乘以一个向量(即,当我们将一个向量投影到矩阵上)时,得到的方差可以按如下方式计算:

(14) 向量投影到数据矩阵上的方差。图片由作者提供。
这给了我们定义 PCA 问题所需的一切。也就是说,我们想找到向量p(主成分),使下列方程最大化:

(15) 在d维度下,PCA 问题的陈述是找到向量p,使其在投影到数据时的方差最大。图片由作者提供。
在 d 维度下,PCA 问题的陈述是找到向量p,使其在投影到数据时的方差最大。我们还规定p 是一个单位向量,这使得这是一个约束优化问题。因此,就像在二维情况下一样,我们需要使用微积分。
为了解决这个问题,我们可以利用 拉格朗日乘子,它允许我们在满足指定约束的同时最小化目标函数。这个问题的拉格朗日表达式如下:

(16) PCA 拉格朗日表达式。图片由作者提供。
为了优化这个问题,我们对其求导并将其设为 0:

(17) PCA 拉格朗日表达式的导数。图片由作者提供。
一些重新排列给出了以下表达式——这是线性代数中一个非常熟悉的结果:

(18) PCA 特征方程。图片由作者提供。
这告诉我们什么?令人惊讶的是,最佳主成分 (p) 乘以数据的协方差矩阵 (S),实际上就是那个相同的 p 乘以一个标量 (λ)。换句话说,p 必须是协方差矩阵的** 特征向量**,我们可以通过计算 S 的特征值分解来找到 p。我们还观察到:

当数据投影到p上时,数据的方差等于p的特征值。图片由作者提供。
当数据投影到p上时,数据的方差等于对应于p.的特征值。我们还知道,S 最多有 d 个特征值/特征向量,特征向量是正交的,特征值从大到小排序。这意味着,通过对 S 进行特征值分解,我们可以恢复所有的主成分:

(20) 所有的 d 个主成分都来自于 S 的特征值分解。图片由作者提供。
这是一种统计学、微积分和线性代数的美丽交汇,给我们提供了一种优雅的方法来找到高维数据集的主成分。真是一个美丽的结果!
代码
如果我们使用 NumPy,PCA 的一般形式实际上比我们推导出的二维版本更容易编码。以下是使用三维数据集的 PCA 示例:
import numpy as np
from sklearn.decomposition import PCA
# Generate three random correlated arrays
rng = np.random.default_rng(seed=3)
x1 = rng.normal(size=1000)
x2 = x1 + rng.normal(size=len(x1))
x3 = x1 + x2 + rng.normal(size=len(x1))
# Create the data matrix (X)
X = np.array([x1, x2, x3]).T
# Normalize X
X = (X - X.mean(axis=0)) / X.std(axis=0)
# Compute the covariance matrix of X (S)
S = np.cov(X.T)
# Compute the eigenvalue decomposition of S
variances, pcs_derived = np.linalg.eig(S)
# Sort the pcs according to their variance
sorted_idx = np.argsort(variances)[::-1]
pcs_derived = pcs_derived.T[sorted_idx, :]
# Find the pcs using sklearn PCA
model = PCA(n_components=3)
model.fit(X)
pcs_sklearn = model.components_
# Compute the element-wise difference between the pcs
pc_diff = np.round(pcs_derived - pcs_sklearn, 10)
print("Derived Principal Components: \n", pcs_derived)
print("Sklearn Principal Components: \n", pcs_derived)
print("Difference: \n", pc_diff)
"""
Derived Principal Components:
[[-0.56530668 -0.57124206 -0.59507215]
[-0.74248305 0.66666719 0.06537407]
[-0.35937066 -0.47878738 0.80100897]]
Sklearn Principal Components:
[[-0.56530668 -0.57124206 -0.59507215]
[-0.74248305 0.66666719 0.06537407]
[-0.35937066 -0.47878738 0.80100897]]
Difference:
[[ 0\. 0\. 0.]
[ 0\. 0\. -0.]
[ 0\. 0\. 0.]]
"""
正如我们所见,scikit-learn 的 PCA 实现给出的主成分正是协方差矩阵的特征向量。
最后的思考
主成分分析(PCA)是一种强大的技术,广泛应用于数据科学和机器学习中,用于减少高维数据集的维度,同时保留最重要的信息。本文探讨了 PCA 的基础原理,首先在二维情况下进行了分析,并使用线性代数将其扩展到任意维度。
在二维情况下,我们通过最大化数据投影到新轴上的方差,推导出了一个闭式解来找到主成分。我们使用了三角学和微积分来确定定义主成分的角度。随后,我们在 Python 中实现了我们的策略,并通过将结果与 scikit-learn 的 PCA 实现进行比较来验证其正确性。
在扩展到任意维度时,我们利用线性代数找到了一种通用的解决方案。通过将 PCA 表达为特征值问题,我们展示了主成分是协方差矩阵的特征向量。相应的特征值表示数据在投影到每个主成分上的方差。
正如我们在数学中常常看到的,那些看似无关的概念,经过不同数学家在不同时间段的发展,最终会汇聚成一个美丽的结果。PCA 是这种复杂交汇的一个典型例子,还有许多类似的例子值得探索!
成为会员: https://harrisonfhoffman.medium.com/membership
请给我买杯咖啡: https://www.buymeacoffee.com/HarrisonfhU
参考文献
-
Ali Ghodsi, Lec 1: 主成分分析 —
www.youtube.com/watch?v=L-pQtGm3VS8&t=3129s -
独立成分分析 2 —
www.youtube.com/watch?v=olKgmOuAvrc
在 Apache Airflow 中构建管道 - 初学者指南
原文:
towardsdatascience.com/building-pipelines-in-apache-airflow-for-beginners-58f87a1512d5
一个快速简单的演示,展示如何在 Airflow 上运行 DAG
·发表于 Towards Data Science ·阅读时间 9 分钟 ·2023 年 3 月 15 日
--

图片由 Kelly Sikkema 提供,Unsplash
Apache Airflow 在数据科学和数据工程领域非常受欢迎。它拥有许多功能,使用户能够以编程方式创建、管理和监控复杂的工作流。
然而,该平台的功能范围可能会无意中成为初学者的负担。新用户在浏览 Apache Airflow 的文档和教程时,可能会被新术语、工具和概念淹没。
为了提供一个更易于理解的入门介绍,我们提供了一个 Apache Airflow 演示的简化版本,该演示涉及编码和运行 Airflow 管道。
术语
熟悉以下 Airflow 术语后,将更容易跟随演示。
- DAG: DAG,意为有向无环图,是 Airflow 用来表示工作流的工具。DAG 由表示任务的节点和表示任务之间关系的箭头组成。

示例 DAG(作者创建)
2. 任务: 任务是 DAG 中的一个工作单元。DAG 中的任务可以通过依赖关系连接在一起,确保按照特定顺序执行。
3. 操作符: 操作符是用于实例化 DAG 中任务的工具。Airflow 提供了大量操作符,能够执行基本的工作,如执行 Python 代码、bash 命令和 SQL 查询。
4. 元数据数据库: 元数据数据库存储用户创建和运行的 Airflow 管道的元数据。
5. Webserver: Webserver 是一个方便的用户界面,使用户能够运行、监视和调试管道。
6. 调度器: 调度器负责监控 DAG 并在其依赖关系满足时运行任务。
演示
演示的目标是使用 Python 创建一个 DAG 并在 Airflow 上运行。由于我们优先考虑简洁性,我们将创建一个仅包含两个任务的 DAG。
第一个任务运行一个名为 pull_jokes 的 Python 函数,它从 官方笑话 API 获取随机笑话并将其存储在文本文件中。
第二个任务运行一个 bash 命令,打印一句话:
echo "The random jokes have been pulled"
最后,我们希望第二个任务(即 bash 命令)在第一个任务(即 Python 函数)之后立即执行。如果我们可视化这个 DAG,它会像这样:

演示 DAG(由作者创建)
很简单,对吧?我们开始吧。
第一部分 — 设置 Airflow
在开始创建任何 DAG 之前,我们需要在机器上设置 Airflow,这将需要使用命令行界面。
有几种方法可以安装 Apache Airflow,如 文档 中所述。在这里,我们将使用 PyPI 方式进行安装。
- 激活 Windows 子系统 Linux(适用于 Windows 用户)
如果你使用的是 Windows,你需要激活 Windows 子系统 Linux(WSL),这将需要先安装 Ubuntu。
要激活 WSL,请在 PowerShell 中输入 wsl 命令。
2. 创建虚拟环境
接下来,创建我们将工作的虚拟环境。输入以下命令以安装 python-venv。
sudo apt install python3-venv
对于演示,我们将创建一个名为“airflow_venv”的虚拟环境并激活它。
python -m venv airflow_venv
source airflow_venv/bin/activate

激活虚拟环境(由作者创建)
3. 选择 Airflow 的主目录(可选)
重要的是要知道 Airflow 项目将存储在哪里,因为创建的工作流应存储和配置在这里。
默认情况下,Airflow 的主目录将是 ~/airflow 目录。然而,用户可以使用以下命令更改 Airflow 的主目录:
export AIRFLOW_HOME=<directory>
对于案例研究,项目将存储在 ~/airflowhome 目录中。
4. 安装 Apache Airflow
确认主目录后,使用以下命令通过 pip 安装 Apache Airflow:
pip install apache-airflow
此安装将在主目录中创建一个具有以下结构的项目:
└── airflowhome/
├── airflow.cfg
├── logs/
│ └── ...
└── webserver_config.py

Airflow 主目录(由作者创建)
名为 airflow.cfg 的文件包含了所有 Airflow 的配置。它包含一个名为 dags_folder 的参数,显示所有创建的 DAG 必须 存放的文件夹路径。
通过输入以下命令,你可以查看当前分配的目录:
vim airflow.cfg

代码输出(由作者创建)
如输出所示,所有用于此演示的 airflow DAG 必须位于airflowhome目录中的dags子目录中。
因此,我们将在同一目录中创建一个名为dags的文件夹,其中将包含随后创建的管道。
mkdir dags
注意:您也可以更改 airflow.cfg 文件中分配给
*dags_folder*参数的路径,选择您喜欢的路径。
项目结构应更新为如下:
└── airflowhome/
├── airflow.cfg
├── logs/
│ └── ...
├── dags/
└── webserver_config.py
5. 初始化元数据数据库
接下来,使用以下命令初始化数据库:
airflow db init
初始化数据库后,airflow 主目录中现在应该有一个名为airflow.db的文件。
└── airflowhome/
├── airflow.cfg
├── airflow.db
├── logs/
│ └── ...
├── dags/
└── webserver_config.py

Airflow 主目录(作者创建)
6. 创建用户。
需要一个用户账户来访问网络服务器,因此我们可以使用以下命令创建一个。
airflow users create --username <username> \
--password <password> \
--firstname <firstname> \
--lastname <lastname> \
--role <role> \
--email <email>
对于案例研究,我们将创建一个管理员账户。

创建用户(作者创建)
一旦创建了用户账户,可以使用以下命令进行验证:
airflow users list

代码输出(作者创建)
7. 启动 Web 服务器和调度器
可以通过一行命令打开 Airflow web 服务器。
airflow webserver -p 8080
注意:默认端口为 8080,但您可以选择不同的端口。
在新的终端中,使用以下命令启动调度器:
airflow scheduler
9. 登录到 Web 服务器
启动 web 服务器后,在网页浏览器中访问“localhost:8080/”(或您选择的任何端口),您将被定向到登录页面,在那里输入您新创建的用户账户的登录凭据。

登录页面(作者创建)
输入详细信息后,您将被定向到主页。

Web 服务器(作者创建)
目前,只会有 Airflow 提供的示例 DAG,但一旦创建了您的 DAG,它们也会显示在 web 服务器上。
第二部分 — 创建 DAG
既然我们已经设置好了 Airflow,我们可以使用 Python 构建 DAG。此演示中创建的 DAG 将被称为“pulling_jokes_dag”。
以下名为pull_jokes.py的 Python 脚本创建了一个 DAG 实例及其任务:
这是一个小代码片段,但有很多内容。让我们一步步详细解读。
- 创建 DAG 实例
首先,我们创建一个 DAG 实例,在其中确定 DAG 的配置。
一个 DAG 实例必须为两个参数分配值:dag_id和start_date。dag_id参数是 DAG 的唯一标识符,而start_date参数是 DAG 计划开始的日期。
为了简化操作,我们只会指定几个参数。schedule 参数定义 DAG 应运行的规则。end_date 参数表示 DAG 运行应停止的时间。catchup 参数表示调度程序是否应为自上次数据间隔以来未运行的任何数据间隔启动 DAG 运行。tag 参数为 DAG 分配标签,这将使其在 UI 中更易于找到。
对于那些对 DAG 实例所有可用参数感兴趣的人,可以访问 Airflow 的 文档。
2. 创建第一个任务
创建 DAG 实例后,我们可以创建第一个任务,该任务运行 pull_jokes 函数,使用 Python Operator。
task_id 参数是任务的唯一标识符,而 python_callable 参数包含应执行的函数。
3. 创建第二个任务
接下来,我们创建第二个任务,该任务执行 bash 命令,使用Bash Operator。
再次地,task_id 参数是任务的唯一标识符,而 bash_command 参数包含应执行的 bash 脚本。
4. 建立依赖关系
创建任务后,需要设置依赖关系以确定任务的执行顺序。
我们可以使用 >> 操作符将任务 2 设置为在任务 1 之后 执行。
第三部分 — 在 Airflow 上运行 DAG
一旦创建了 DAG 的 Python 脚本,下一步是让 DAG 在 Airflow 上运行。
提醒一下,DAG 脚本 必须 放置在 airflow.cfg 文件中指定的位置。用于此案例研究的 pull_jokes.py 文件应位于此位置。

DAGs 目录(作者创建)
将 pull_jokes.py 文件移动到正确的位置后,项目目录结构应如下所示:
└── airflowhome/
├── airflow.cfg
├── airflow.db
├── dags/
│ └── pull_jokes.py
├── logs/
│ └── ...
└── webserver_config.py
现在,Airflow 应该可以访问 DAG。要确认这一点,只需输入以下命令:
airflow dags list
我们现在应该能够在 Web 服务器中查看新添加的 DAG。使用以下命令启动 Web 服务器:
airflow webserver -p 8080
在一个单独的终端中,启动调度程序
airflow scheduler
你应该能够在 UI 中看到名为“pulling_jokes_dag”的 DAG。

Web 服务器中的 DAG(作者创建)
专业提示:虽然这是可选的,但最好为你的 DAG 分配标签。这将使其在 Web 服务器中更易于查找。
从技术上讲,DAG 可以仅通过命令行界面进行管理,但 Web 服务器中的功能使得访问和监控创建的管道变得更加容易。
一些功能包括但不限于:
- DAG 以网格形式的详细说明:

网格视图(作者创建)
- DAG 的图形形式分析

图形视图(作者创建)
- DAG 的底层 Python 代码

代码(作者创建)
- 审计日志

审计日志(作者创建)
一旦 Airflow 访问到 DAG,它将根据提供的时间表执行 DAG 运行。
也可以通过点击网页服务器右上角的“播放按钮”手动运行 DAG。

运行 DAG(作者创建)
或者,可以使用 CLI 手动触发 DAG:
airflow dags trigger <dag_id>
就这样,我们的 DAG 在 Airflow 上运行起来了!
结论

照片由Prateek Katyal提供,来源于Unsplash。
希望这对那些希望亲身体验 Apache Airflow 的人来说是一个有用的入门指南。
这个演示绝不是 Airflow 功能和能力的全面展示(我们只是初步了解),但它应该能帮助人们入门,这通常是最困难的部分。
在构建并运行第一个 DAG 后,你将建立一个坚实的基础,这将使你更容易设计和运行更复杂的工作流,体验更加顺畅。
祝你在数据科学的努力中好运!
使用深度学习构建强大的推荐系统
原文:
towardsdatascience.com/building-powerful-recommender-systems-with-deep-learning-d8a919c52119

作者插图
使用 PyTorch 库 TorchRec 的逐步实现
·发布在 Towards Data Science ·阅读时长 7 分钟·2023 年 7 月 3 日
--
在正确的时间向客户推荐合适的产品是各行各业普遍面临的挑战。例如,银行家不断寻找向现有或潜在客户推荐高度相关服务的机会。零售商努力推荐符合客户口味的吸引人产品。类似地,社交网络旨在构建引人入胜的动态,以促进用户采纳。
尽管这是一个被广泛研究的用例,但由于问题的独特性,取得令人满意的性能结果仍然困难重重。主要原因包括存在大量分类数据,通常会导致稀缺问题,以及用例的计算方面,带来扩展性问题。直到最近,推荐模型才开始利用神经网络。
在这种背景下,Meta 开发并公开了一个深度学习推荐模型(DRLM)。该模型因结合了协同过滤和预测分析的原理,并适用于大规模生产而特别出色。
目标
本文的目标是通过使用 PyTorch 库 TorchRec 的逐步实现,帮助你有效解决自己的推荐系统问题。
阅读本文后,你将理解:
-
DLRM 模型如何工作?
-
DLRM 模型有什么独特之处,使其强大而具有可扩展性?
-
你如何从头到尾实现自己的推荐系统?
本文需要对推荐系统问题有一般性的了解,并熟悉 pytorch 库。本文所述的实验使用了库 TorchRec 和 PyTorch。你可以在 GitHub 上找到代码 这里 。
[## GitHub - linafaik08/recommender_systems_dlrm
通过在 GitHub 上创建账户来为 linafaik08/recommender_systems_dlrm 的开发做出贡献。
1. 解码 DLRM 模型
让我们首先深入了解 DLRM 模型的复杂性,并探索其基本原理和机制。
1.1. 模型设计概述
为了提供一个更具体的例子,我们来考虑一个在线零售商希望为每个访问其网站的客户创建个性化推荐的场景。
为了实现这一点,零售商可以训练一个模型,该模型预测客户购买特定产品的概率。该模型根据各种因素为每个客户的每个产品分配一个分数。推荐流通过对这些分数进行排序来构建。
在这种情况下,模型可以从涵盖每个客户和产品一系列信息的历史数据中学习。这包括诸如客户年龄和产品价格等数值变量,以及产品类型、颜色等类别特征。
DLRM 模型的优势在于:它具有利用数值变量和类别变量的非凡能力,即使在处理大量独特类别时也是如此。这使得模型能够全面分析和理解特征之间的复杂关系。要理解原因,我们来看一下图 1 中的架构模型。

图 1 — DLRM 模型架构,由作者绘制,灵感来自 [5]
类别特征
DLRM 为每个类别特征学习一个嵌入表,并使用这些表将这些变量映射到稠密表示。因此,每个类别特征都表示为相同长度的向量。
数值特征
DLRM 通过一个称为底部 MLP 的多层感知器(MLP)处理数值特征。该 MLP 的输出维度与之前的嵌入向量相同。
成对交互
DLRM 计算所有嵌入向量与处理过的数值特征之间的点积。这使得模型能够包括二阶特征交互。
连接与最终输出
DLRM 将这些点积与处理过的数值特征进行拼接,并将结果输入到另一个 MLP 中,称为顶部 MLP。最终的概率通过将此 MLP 的输出传递到 sigmoid 函数获得。
1.2. 模型实现
尽管模型在理论上具有良好的潜力,但其实际实施仍然存在计算难题。
通常,推荐系统涉及处理大量数据。特别是使用 DLRM 模型会引入非常多的参数,超过了常见深度学习模型。因此,这增加了其实现所需的计算需求。
-
DLRM 中大多数参数可归因于嵌入,因为它们由多个表组成,每个表都需要大量内存。这使得 DLRM 在内存容量和带宽方面都具有较高的计算需求。
-
尽管 MLP 参数的内存占用较小,但仍然需要大量计算资源。
为了缓解内存瓶颈,DLRM 依赖于一种独特的模型并行(用于嵌入)和数据并行(用于 MLP)的组合。
2. 从概念到实现:构建自定义推荐系统的逐步指南
本节提供了从头到尾实现自己推荐系统的详细逐步指南。
2.1. 数据转换和批处理构建
第一步是将数据转换为张量并将其组织成批次,以输入到模型中。
为了说明这个过程,让我们以这个数据框为例。

对于稀疏特征,我们需要将值拼接成一个向量并计算长度。这可以使用 KeyedJaggedTensor.from_lengths_sync 函数来完成,该函数接受两个元素作为输入。以下是 Python 脚本的一个示例:
values = sample[cols_sparse].sum(axis=0).sum(axis=0)
values = torch.tensor(values).to(device)
# values = tensor([1, 0, 2, 0, 2, 2, 0, 2, 0, 1, 0, 1, 2, 0], device='cuda:0')
lengths = torch.tensor(
pd.concat([sample[feat].apply(lambda x: len(x)) for feat in cols_sparse],
axis=0).values,
dtype=torch.int32
).to(self.device)
# lengths = tensor([1, 1, 1, 1, 1, 2, 3, 2, 2, 0], device='cuda:0', dtype=torch.int32)
sparse_features = KeyedJaggedTensor.from_lengths_sync(
keys=cols_sparse,
values=values,
lengths=lengths
)
对于密集特征和标签,过程更为简单。这是 Python 脚本的一个示例:
dense_features = torch.tensor(sample[cols_dense].values, dtype=torch.float32).to(device)
labels = torch.tensor(sample[col_label].values, dtype=torch.int32).to(device)
通过使用前一步骤的输出,可以构建一个批次。这是 Python 脚本的一个示例:
batch = Batch(
dense_features=dense_features,
sparse_features=sparse_features,
labels=labels,
).to(device)
要获取更全面的实现,可以参考 GitHub batch.py 文件及相关 GitHub 仓库。
2.2. 模型初始化和优化设置
下一步涉及初始化模型,如以下 Python 代码所示:
# Initialize the model and set up optimization
# Define the dimensionality of the embeddings used in the model
embedding_dim = 10
# Calculate the number of embeddings per feature
num_embeddings_per_feature = {c: len(v) for c, v in map_sparse.items()}
# Define the layer sizes for the dense architecture
dense_arch_layer_sizes = [512, 256, embedding_dim]
# Define the layer sizes for the overall architecture
over_arch_layer_sizes = [512, 512, 256, 1]
# Specify whether to use Adagrad optimizer or SGD optimizer
adagrad = False
# Set the epsilon value for Adagrad optimizer
eps = 1e-8
# Set the learning rate for optimization
learning_rate = 0.01
# Create a list of EmbeddingBagConfig objects for each sparse feature
eb_configs = [
EmbeddingBagConfig(
name=f"t_{feature_name}",
embedding_dim=embedding_dim,
num_embeddings=num_embeddings_per_feature[feature_name + '_enc'],
feature_names=[feature_name + '_enc'],
)
for feature_idx, feature_name in enumerate(cols_sparse)
]
# Initialize the DLRM model with the embedding bag collection and architecture specifications
dlrm_model = DLRM(
embedding_bag_collection=EmbeddingBagCollection(
tables=eb_configs, device=device
),
dense_in_features=len(cols_dense),
dense_arch_layer_sizes=dense_arch_layer_sizes,
over_arch_layer_sizes=over_arch_layer_sizes,
dense_device=device,
)
# Create a DLRMTrain instance for handling training operations
train_model = DLRMTrain(dlrm_model).to(device)
# Choose the appropriate optimizer class for the embedding parameters
embedding_optimizer = torch.optim.Adagrad if adagrad else torch.optim.SGD
# Set the optimizer keyword arguments
optimizer_kwargs = {"lr": learning_rate}
if adagrad:
optimizer_kwargs["eps"] = eps
# Apply the optimizer to the sparse architecture parameters
apply_optimizer_in_backward(
optimizer_class=embedding_optimizer,
params=train_model.model.sparse_arch.parameters(),
optimizer_kwargs=optimizer_kwargs,
)
# Initialize the dense optimizer with the appropriate parameters
dense_optimizer = KeyedOptimizerWrapper(
dict(in_backward_optimizer_filter(train_model.named_parameters())),
optimizer_with_params(adagrad, learning_rate, eps),
)
# Create a CombinedOptimizer instance to handle optimization
optimizer = CombinedOptimizer([dense_optimizer])
然后可以使用以下代码训练和评估模型:
loss, (loss2, logits, labels) = train_model(batch)
要获取更全面的实现,可以参考 GitHub model.py 文件及相关 GitHub 仓库。
关键要点
✔ DLRM 模型提供了一种有效结合数值和类别特征的引人注目的方法,使用嵌入技术,使得模型能够捕捉复杂的模式和关系。
✔ 尽管其架构需要相当的计算资源,但其实现结合了模型并行和数据并行的独特组合,使得模型在生产环境中具有可扩展性。
✔ 然而,由于数据的有限性,该模型的性能尚未在各种真实世界的数据集上进行广泛测试。这引发了对其在实际场景中有效性的担忧。
✔ 此外,该模型需要调整大量参数,这进一步使得过程复杂化。
✔ 考虑到这一点,像 LGBM 这样更简单的模型可能提供类似的性能,并且具有更简单的实现、调优和长期维护,而不会有相同的计算开销。
参考文献
[1] M Naumov 等,用于个性化和推荐系统的深度学习推荐模型,2019 年 5 月
[2] Facebook 团队的 DLRM 模型初步实现的 Github 仓库,开放源代码
[3] DLRM:一种先进的开源深度学习推荐模型,Meta AI 博客,2019 年 7 月
[4] 现代生产推荐系统的 Pytorch 库,torchec
[5] Vinh Nguyen, Tomasz Grel 和 Mengdi Huang,优化 NVIDIA GPU 上的深度学习推荐模型,2020 年 6 月
使用 LangChain 和 LLMs 的业务分析
原文:
towardsdatascience.com/business-analytics-with-langchain-and-llms-c8e902446073
生成式 AI
一个关于用人类语言查询 SQL 数据库的分步教程
·发表于 数据科学前沿 ·7 分钟阅读·2023 年 12 月 19 日
--

图片由作者提供(通过 Midjourney 生成)
许多企业在其数据库中存储了大量专有数据。如果有一个能够理解人类语言并查询这些数据库的虚拟代理,这将为这些企业打开巨大的机会。想想客户服务聊天机器人,它们就是一个常见的例子。这些代理可以接受客户请求,向数据库询问信息,并向客户提供他们所需的内容。
这些代理的好处不仅限于外部客户互动。许多企业主或公司中的人员,即使是在科技公司,也可能不知道 SQL 或类似的语言,但他们仍然需要向数据库查询信息。这就是像 LangChain 这样的框架派上用场的地方。这些框架使得创建这些有用的代理/应用程序变得容易。那些可以与人类对话,同时与数据库、API 等进行交互的代理。
LLM 支持的应用程序
LangChain 是一个开源框架,用于构建使用大型语言模型(LLMs)的互动应用程序。这是一个帮助 LLMs 连接其他信息来源,并使其能够与周围世界对话的工具。在这些框架中,一个重要的概念是链(Chain)。让我们来看看这个概念。
什么是链(Chains)?
链接是这个框架中的高级工具,它们将 LLM 与其他工具结合起来,以执行更复杂的任务。具体来说,链是使用 LLM 序列和其他工具(如 SQL 数据库、API 调用、bash 操作符或数学计算器)来完成复杂工作的接口。例如,我们的应用程序接收来自用户的输入并将其传递给我们的 LLM 模型;然后,LLM 调用一个 API。API 对 LLM 进行响应,LLM 使用响应执行另一个任务,如此循环往复。正如你所看到的,这是一系列输入和输出的链条,其中在许多部分,我们有 LLM 模型处理情况。
开始动手实践
现在是时候开始动手编写一个简单的 LLM 支持的应用程序了。对于这个应用程序,我们将制作一个简单的问答代理,它接收我们的问题并查询 SQL 数据库以为我们找到答案。
设置 PostgreSQL 示例数据库
我们使用了来自 postgresqltutorial.com 的 DVD Rental Sample Database(许可证信息)。要在你的本地系统上安装数据库,你需要安装 PostgreSQL。在终端中简单地输入 *psql* 看看它是否运行。
然后你应该按照 这里 的说明进行操作。这里我将快速带你完成数据库安装过程。
在你的终端中通过以下命令启动 PostgreSQL:
psql -U <USERNAME>
你必须将 <USERNAME> 替换为你的实际用户名。系统会询问你的密码。输入你的密码后,你将进入你的 Postgres 数据库。
首先,我们应该创建一个数据库以加载所有的表。
postgres=# CREATE DATABASE dvdrental;
创建数据库后,你可以通过输入 *\list* 命令来检查它,你应该会在返回的列表中看到你的 dvdrental 数据库。然后简单地退出 Postgres(通过 \q)。
现在我们需要下载表和数据。你可以在这里下载所有内容的 tar 文件:下载 DVD Rental Sample Database
进入你下载 tar 文件的文件夹,使用 pg_restore,我们可以将表加载到我们的 Postgres 数据库中。
pg_restore -U postgres -d dvdrental dvdrental.tar
在继续之前,让我们检查表是否已加载。通过 psql -U <USERNAME> 启动 Postgres 并输入你的密码。
然后输入以下命令以列出 dvdrental 数据库中的表。
postgres=# \c dvdrental
dvdrental=# \dt
你应该看到一个如下所示的表列表:
List of relations
Schema | Name | Type | Owner
--------+---------------+-------+----------
public | actor | table | postgres
public | address | table | postgres
public | category | table | postgres
public | city | table | postgres
public | country | table | postgres
public | customer | table | postgres
public | film | table | postgres
public | film_actor | table | postgres
public | film_category | table | postgres
public | inventory | table | postgres
public | language | table | postgres
public | payment | table | postgres
public | rental | table | postgres
public | staff | table | postgres
public | store | table | postgres
设置 .env 文件
在构建示例数据库之后,我们需要创建一个 .env 文件。我们使用此文件来存储我们的秘密、密钥以及环境变量。当然,我们可以将所有这些信息放在代码中,但即使是小项目也必须遵循最佳工程实践。意外的 git push 可能会将我们的 API 密钥和秘密暴露给公众。以下是我们的 .env 文件:
OPENAI_API_KEY = 'YOUR-OPENAI-API-KEY'
PSQL_HOST = 'localhost'
PSQL_PORT = 'YOUR-POSTGRES-PORT'
PSQL_USERNAME = 'YOUR-POSTGRES-USERNAME'
PSQL_PASSWORD = 'YOUR-POSTGRES-PASSWORD'
PSQL_DB = 'dvdrental'
正如你所见,你需要获得自己的 OpenAI API 密钥进行测试。你可以按照 这里 的说明从 Open AI 获取 API 密钥。
创建一个 LangChain 应用程序
在将 .env 文件保存到你的项目文件夹后,我们可以开始实际的代码。首先,我们将所需的库导入到 Python 代码中。
pip install langchain langchain-experimental openai psycopg2
对于本教程,我建议使用 Jupyter Notebook 逐步测试它。
我们需要在开始时导入所需的库。
from os import environ
from dotenv import load_dotenv
from langchain.chat_models import ChatOpenAI
from langchain.utilities import SQLDatabase
from langchain_experimental.sql import SQLDatabaseChain
load_dotenv()
使用 load_dotenv(),我们加载了在 .env 文件中定义的环境变量。现在,我们可以安全地在代码中访问它们。
对于我们的链,我们需要一个具有聊天功能的语言模型。为简单起见,我们选择了来自 OpenAI 的 gpt-3.5-turbo,该模型可以通过 API 访问。你可以使用任何其他公共或私有模型。
# Instantiating Our LLM
OPENAI_API_KEY = environ.get("OPENAI_API_KEY")
llm = ChatOpenAI(model_name='gpt-3.5-turbo', temperature=0, openai_api_key=OPENAI_API_KEY)
除了模型,我们还需要一个 SQL 数据库连接。该连接使我们的链能够对数据库进行查询并获取结果。如前所述,我们使用的是 PostgreSQL 数据库,但根据 LangChain 的说法,任何具有 JDBC 连接的 SQL 引擎都应该容易使用(例如 MySQL、Presto、Databricks)。
# Database Setup
PSQL_HOST = environ.get("PSQL_HOST")
PSQL_PORT = environ.get("PSQL_PORT")
PSQL_USERNAME = environ.get("PSQL_USERNAME")
PSQL_PASSWORD = environ.get("PSQL_PASSWORD")
PSQL_DB = environ.get("PSQL_DB")
psql_uri = f"postgresql+psycopg2://{PSQL_USERNAME}:{PSQL_PASSWORD}@{PSQL_HOST}:{PSQL_PORT}/{PSQL_DB}"
db = SQLDatabase.from_uri(psql_uri, sample_rows_in_table_info=3)
db.get_usable_table_names()
如果你对我们在 SQL 数据库连接中使用的 sample_rows_in_table_info 参数感到好奇,Rajkumar 等人在他们的论文中(arxiv.org/abs/2204.00498)展示了从表中包含几行样本数据可以提高模型在创建模式下查询数据的性能。在 LangChain 中,你可以简单地设置 sample_rows_in_table_info,并确定从每个表中提取的样本行数,这些样本行将被附加到每个表的描述中。
为了测试 SQL 数据库连接,我使用 db.get_usable_table_names() 打印了可用表的列表。它也应该返回以下表的列表给你。
['actor',
'address',
'category',
'city',
'country',
'customer',
'film',
'film_actor',
'film_category',
'inventory',
'language',
'payment',
'rental',
'staff',
'store']
下一步是最重要的步骤。借助我们的 LLM 模型和 SQL 数据库连接,我们现在应该能够实例化我们的链。在展示实例化之前,让我们先对链有更多了解。
现在,我们正在使用来自 LangChain 的 SQL 链来获取人类问题,将其转换为 SQL 查询,在数据库上运行它,并检索结果。
# Definining Our SQL Dabatabe Chain
db_chain = SQLDatabaseChain.from_llm(llm, db, verbose=True)
最后,我们使用 run() 方法将输入/问题传递给链,并获取最终响应。
# Define a Function To Run the Chain with Our Query
def query(input: str) -> str:
response = db_chain.run(input)
return response
现在是测试代码的时候了。我们从一个简单的查询开始,该查询只需查询一个表。
# Example 1:
query("What is the average length of films released in 2006")
该链接收我们的自然语言查询,并首先将其转换为 SQL 查询。然后,使用 SQL 数据库连接,它在数据库上运行 SQL 查询。返回的响应为 LLM 提供了上下文,并与原始人类查询一起触发了响应。在这种情况下,你可以看到最终的响应:
> Entering new SQLDatabaseChain chain...
What is the average length of films released in 2006
SQLQuery:SELECT AVG(length) FROM film WHERE release_year = 2006
SQLResult: [(Decimal('115.2720000000000000'),)]
Answer:The average length of films released in 2006 is 115.272 minutes.
> Finished chain.
**'The average length of films released in 2006 is 115.272 minutes.'**
首先,由于 verbose=True(我们在其中定义了 db_chain),我们可以获得更多关于构建的 SQL 查询和返回的 SQL 结果的信息。如果你关闭 verbose,你只能看到最终结果(用粗体标记)。最终答案正确显示了 2006 年上映的电影的平均时长是 115.272 分钟(你可以在 Postgres 上使用你自己的 SQL 查询进行验证)。
第二个问题稍微复杂一些,需要连接三个表。
# Example 2:
query("Which actor played in movies with total longer duration? And how much was the duration?")
这个问题询问的是在电影总时长超过其他任何演员的演员。这个问题需要连接三个表:actor、film_actor 和 film。以下是链响应:
> Entering new SQLDatabaseChain chain...
Which actor played in movies with total longer duration? And how much was the duration?
SQLQuery:SELECT a.first_name, a.last_name, SUM(f.length) AS total_duration
FROM actor a
JOIN film_actor fa ON a.actor_id = fa.actor_id
JOIN film f ON fa.film_id = f.film_id
GROUP BY a.actor_id
ORDER BY total_duration DESC
LIMIT 1;
SQLResult: [('Mary', 'Keitel', 4962)]
Answer:Mary Keitel played in movies with a total duration of 4962 minutes.
> Finished chain.
**'Mary Keitel played in movies with a total duration of 4962 minutes.'**
如你所见,我们超简单的 LangChain 应用成功地理解了这些表之间的关系,并构建了 SQL 查询。
总结
在本文中,我们介绍了一款强大的开源工具,叫做 LangChain,它使我们能够构建基于 LLM 的应用程序。
然后我们使用 SQLDatabaseChain 构建了一个应用程序,该应用程序根据用户的问题查询 SQL 数据库并返回结果。
这个简单的问答应用可以扩展成一个更复杂的业务分析助手,供企业内部的任何人每天使用,从专有数据中获取最新的洞察。
但什么是 LDA?使用类似于 3Blue1Brown 的数学动画来解释。
·发布于Towards Data Science ·13 分钟阅读·2023 年 6 月 5 日
--
让你的同事们印象深刻 😉
你是否曾经为解释数学概念而苦恼过?3Blue1Brown是我最喜欢的 YouTube 频道,它提供了很酷的数学和统计主题的视频和动画。直到我观看了这个频道上的可视化解释,我才真正理解了二项分布🤷♀️
如果你能够了解这些动画是如何制作的,以便你能创建类似的动画来解释数据科学概念,并让你的队友和经理们印象深刻,那岂不是很棒吗?🤩😍

LDA 生成过程。作者制作的 Gif。
好吧,你真幸运,因为你可以使用Manim来实现这一点!😍它是一个免费的开源项目,允许我们使用 Python 创建数学动画,最初由Grant Sanderson编写,现在由 Manim 社区维护。
在这篇文章中,我们将学习如何在 Manim 中创建一些基本元素,并应用我们所学的内容来动画展示如何生成文档,使用的是 Latent Dirichlet Allocation (LDA)。如果你希望理解 LDA 的工作原理,我有一篇文章可以帮助你。在这篇文章中,我们将假定你已经理解了 LDA 的工作原理。好了,让我们开始吧。
什么是 Manim

Manim 是一个 Python 库,它允许用户创建精确的数学动画,类似于3Blue1Brown的视频。Manim 由以下主要组件构建,并且需要较新的 Python 版本(3.7–3.10)才能运行:
Manim 构建模块

本质上,manim 将三个概念结合起来以生成数学动画: Mobject, Animation, 和 Scene。
Mobject 是 Mathematical Object 的缩写。它们是所有 manim 动画的基本构建块。每个从 Mobjects 派生的类代表一个可以在屏幕上动画化和显示的对象,例如形状、方程或图表。
manim 的核心是 Animation,这是在两个 Mobjects 之间进行插值的过程。它定义了可以应用于 Mobjects 的变换和运动,例如 旋转 或 淡出。
Scene 类是你动画的画布。每个 Mobject 都必须被 [添加](https://docs.manim.community/en/stable/reference/manim.scene.scene.Scene.html#manim.scene.scene.Scene.add) 到一个场景中才能显示,每个动画也必须通过场景 [播放](https://docs.manim.community/en/stable/reference/manim.scene.scene.Scene.html#manim.scene.scene.Scene.play)。
设置环境
你可以通过运行以下命令创建一个新的环境并使用 conda 安装 Manim:
conda create -n my-manim-environment
conda activate my-manim-environment
conda install -c conda-forge manim
最好通过 conda 安装 manim,因为 conda 会处理所有的依赖项,如 ffmpeg、pycairo 等,你不需要担心安装额外的依赖项。LaTeX 是一个可选的依赖项,不会通过 conda 安装。为了使用 Manim 的 LaTeX 接口,例如渲染公式,你可以通过运行以下命令来为 macOS 安装 LaTeX:
brew install --cask mactex-no-gui
在 Ubuntu 上,你可以运行
sudo apt install texlive
现在,你应该已经成功安装了 Manim。很好,我们开始吧。
使用 Manim 创建基本元素
在这一部分,我们将学习如何使用 Manim 创建基本的 Mobjects,比如箭头、曲线和条形图。这将为我们在下一节中创建 LDA 生成过程的动画做好准备。
创建基本元素 - 一个箭头
首先,让我们导入 manim 库的所有内容:
from manim import *
在 Manim 中,脚本动画的代码通常写在construct()方法内,该方法属于Scene类。在这个方法内,你可以创建对象,将它们显示在屏幕上,并对其进行动画处理。例如,你可以使用Arrow类创建两个箭头,并使用shift()方法将它们排列在一起,shift()方法用于调整Mobject的位置。最后,我们将使用Create类来初始化对象的动画,并通过调用play()方法将动画添加到场景中:
from manim import *
from manim.mobject.geometry.tips import ArrowSquareTip
class ArrowExample(Scene):
def construct(self):
arrow_1 = Arrow(
start=RIGHT, end=LEFT, color=BLUE
) # Specifies the starting point of the arrow as the right side.
arrow_2 = Arrow(
start=RIGHT, end=LEFT, color=BLUE, tip_shape=ArrowSquareTip
).shift(
DOWN
) # Shifts the position of the arrow downward
self.play(Create(arrow_1))
self.play(Create(arrow_2))
将上面的脚本保存到Arrows.py文件中。现在,在你存储这个文件的项目文件夹中,运行以下命令以生成脚本的视频(“ -p”选项允许你在动画渲染后显示视频)
manim -p manim.py Arrows.py

创建基本元素 - 条形图
我们还可以使用 Manim 中的BarChart类来创建条形图。在下面的示例中,我使用以下配置创建了一个条形图:
from manim import *
class BarChartExample(Scene):
def construct(self):
# Create the initial bar chart
barchart1 = BarChart(
values=[0.1, 0.2, 0.3],
y_range=[0, 0.6, 0.2],
bar_names=["food", "animals", "sport"],
y_length=6,
x_length=10,
x_axis_config={"font_size": 36}
)
self.play(Create(barchart1))

但仅仅创建条形图并不有趣。让我们看看能否将初始条形图转变为最终的条形图。为此,我们将使用强大的[animate](https://docs.manim.community/en/stable/reference/manim.mobject.mobject.Mobject.html#manim.mobject.mobject.Mobject.animate)方法。
.animate
animate()是所有 Mobjects 的一个属性,允许你展示对象随时间变化的情况,如在动画过程中改变对象的位置、颜色、大小或形状。在下面的示例中,我们将为场景中已经存在的第一个条形图创建动画,并使用animate.become()将其转换为第二个条形图:
from manim import *
class BarChartExample(Scene):
def construct(self):
# Create the initial bar chart
barchart1 = BarChart(
values=[0.1, 0.2, 0.3],
y_range=[0, 0.6, 0.2],
bar_names=["food", "animals", "sport"],
y_length=6,
x_length=10,
x_axis_config={"font_size": 36}
)
# Create the second bar chart
barchart2 = BarChart(
values=[0.4, 0.3, 0.3],
bar_names=["food", "animals", "sport"],
y_length=6,
x_length=10,
x_axis_config={"font_size": 36}
)
self.play(Create(barchart1)) # draw the first bar chart
# Animate the first bar chart to become the second one
self.play(barchart1.animate.become(barchart2), run_time=1.5)
self.wait(2)

创建基本元素 - 曲线
在 Manim 中,Axes是一个用于创建坐标系统可视化表示的类,可以在其中添加图形、点、线等数学对象。在这个例子中,我们将使用Axes类来绘制正弦函数。首先,使用一些特定配置创建Axes对象。接下来,我们将使用axes.get_axis_labels()方法为正弦曲线获取名称。标签通过add()方法添加到axes中,最后,VGroup类用于将axes和sin_curve分组并一起移动。生成的动画将显示正弦函数,如下所示:
from manim import *
class SinFunctionPlot(Scene):
def construct(self):
axes = Axes(
x_range=[-10, 10, 1], # x-axis range and step size
y_range=[-1.5, 1.5, 1], # y-axis range and step size
x_length=10,
axis_config={"color": BLUE},
x_axis_config={
"numbers_to_include": np.arange(-10, 10, 2),
"numbers_with_elongated_ticks": np.arange(-10, 10, 2),
},
)
axes_labels = axes.get_axis_labels(Tex("sin(x)"))
# add labels to the axes object
axes.add(axes_labels)
# create the sin function
sin_curve = axes.plot(lambda x: np.sin(x), color=RED)
plot = VGroup(axes, sin_curve)
# Incrementally show the plot
self.play(Create(plot))
self.wait(2)

使用 Manim 创建 LDA 动画
很棒,到现在你已经学会了如何在 Manim 中创建一些元素,如箭头、曲线和条形图。那我们将所学应用到 LDA 模型生成文档的动画中如何呢?
基本上,LDA 生成过程包含两个组件:主题分布和词汇分布。因此,我们将通过动画展示文档是如何通过 LDA 生成的,具体表现为词汇和主题是如何被选择的。
LDA 使用Dirichlet 和多项式分布。然而,为了能够在二维中可视化数据,我们将考虑一个更简单的示例,其中我们的文档只包含两个主题:食物 🍕 和动物 🐖。这使我们可以使用 Beta 分布而不是 Dirichlet 分布来可视化主题分布。
创建主题分布
Beta 分布的概率密度曲线 (PDF)
我们将从创建 Beta 分布的概率密度曲线开始。如何绘制这条曲线?没错!我们将首先设置一个Axes对象,以便在其上绘制 PDF。我将使用以下一些预定义的配置:
ax = Axes(
x_range=[0, 1],
y_range=[0, 3],
tips=False,
x_length=config["frame_width"] / 3,
y_length=config["frame_height"] * 0.6,
).add_coordinates()
在设置好 Axes 对象后,通过调用get_x_axis_label()和ax.get_y_axis_label()方法为 x 轴和 y 轴添加标签会很不错。为了书写标签,我们将使用Tex类提供的 LaTeX 语法。经过一些美化处理,标签就准备好了:
x_label = ax.get_x_axis_label(
Tex("probability").scale(0.7), edge=DOWN, direction=ORIGIN
)
y_label = ax.get_y_axis_label(
Tex("probability density").scale(0.7).rotate(90 * DEGREES),
edge=LEFT,
direction=LEFT,
buff=0.2,
)
ax.add(x_label, y_label)

很好,我们现在将通过使用类中的参数𝛼和𝛽创建一个 beta 分布对象来创建 PDF 曲线,并将该曲线添加到 Axes 对象中:
alpha = 2
beta = 5
num_samples = 1
def pdf(x):
return dist.pdf(x)
dist = scipy.stats.beta(self.alpha, self.beta)
pdf_curve = ax.plot(pdf, x_range=[0, 1], color=BLUE_C)
# add the PDF curve to the ax object.
ax.add(pdf_curve)

很棒!每个好的图表都有一个标题,让我们使用Tex类为 PDF 曲线添加一个标题:
title_pdf = (
Tex("Dirichlet Distribution's PDF")
.set_color_by_gradient(*bar_kwargs["bar_colors"])
.set_fill(color=WHITE, opacity=0.3)
.set_stroke(width=1.2)
.set(width=7)
.to_corner(UL)
)
为了显示 PDF 曲线使用了 alpha 和 beta 参数,我想通过创建两个Tex对象并使用.next_to()方法将其显示在轴旁边来将其包含在动画中。以下代码块将完成这项工作:
a_tex = Tex(r"$\alpha = 2$", color=BLUE).scale(0.7)
b_tex = Tex(r"$\beta = 5$", color=BLUE).scale(0.7)
ax.add(a_tex, b_tex)
a_tex.next_to(ax, direction=UP + LEFT, aligned_edge=RIGHT)
b_tex.next_to(a_tex, DOWN)
最后,我们将使用Write和Create类来在屏幕上写入 alpha、beta 值,并绘制轴和 PDF 曲线:
self.play(Write(a_tex), Write(b_tex))
self.play(Create(ax))
self.play(Create(pdf_curve), run_time=2)

好的。接下来要做的是动画演示 LDA 过程中如何创建主题分布。我们的文档包含 2 个主题:食物 🍕 和动物 🐖。我们将创建一个函数来返回一个BarChart对象,显示每个主题在文档中出现的概率。Beta 分布以 alpha 和 beta 作为参数,额外的样式和自定义选项可以通过kwargs传递:
alpha = 2
beta = 5
def get_barchart(self, alpha, beta, width, height, zeros=True, **kwargs):
num_samples = 1
if zeros is True:
data = [0, 0]
else:
dist = scipy.stats.beta(alpha, beta)
prob_0 = dist.rvs(num_samples)[0]
prob_1 = 1 - prob_0
data = [prob_0, prob_1]
bar_chart = BarChart(values=data, x_length=width, y_length=height, **kwargs)
return bar_chart
bar_kwargs = {
"x_length": config["frame_width"] / 2.5,
"y_length": config["frame_height"] - 3.25,
"bar_colors": [RED, GREEN, BLUE, YELLOW],
"y_range" : [0, 0.7, 0.15],
}
接下来,我们将通过初始化 BarChart 创建一个 manim 条形图对象bar,并将条形图添加到屏幕的右下角。接下来,我们希望通过创建显示标签的 Tex 对象来标记每个条形,并使用.next_to()方法将标签放置在相应条形的下方。最后,我们使用Create类绘制并显示条形图的创建过程:
bar = BarChart([0.7, 0.3], **bar_kwargs)
bar.to_edge(DOWN + RIGHT)
tex_animals = Tex("animals")
tex_animals.next_to(bar.bars[0], DOWN, buff=0.1)
tex_food = Tex("food")
tex_food.next_to(bar.bars[1], DOWN, buff=0.1)
self.play(Create(bar))
self.play(Write(tex_food), Write(tex_animals))

为了模拟从 Beta 分布中进行多次重采样,我们将创建循环,在每次迭代中生成一个新样本,计算相应的概率,并使用change_bar_values方法相应地更新条形图的值:
# resample 10 times
for i in range(10):
topic_distribution = scipy.stats.beta(self.alpha, self.beta)
probability = new_dist.rvs(self.num_samples)[0]
new_data = [probability, 1 - probability]
if i == 0:
dot = Dot(ax.coords_to_point(probability, 0))
self.play(Create(dot), bar.animate.change_bar_values(new_data))
else:
self.play(
dot.animate.move_to(ax.coords_to_point(probability, 0)),
bar.animate.change_bar_values(new_data),
)
self.wait(1)
将所有内容结合起来,我们就有了以下动画:

如果你看到一个 PDF 曲线和柱状图被绘制的动画,恭喜你!你刚刚从头开始编写了你的第一个 Manim 场景,用来显示 LDA 主题分布。
创建词汇分布
太好了,我们有了主题分布。然而,一个主题还应该包含属于它的词汇。假设我们的词汇表包括以下十个词:
{🍌,🥝,🍋,🍓,🍅,🐥,🐖,🐑,🐊,🦓}
并且每个文档将仅包含 5 个词。为了可视化 LDA 的生成过程,我们将分别显示每个主题的主题分布和词汇分布,两个单独的柱状图。主题分布将与我们之前做的相同:
topic_distribution = BarChart(
values=[0,0],
bar_names=['animals', 'food'],
y_axis_config={"font_size": 36},
y_length=3,
x_length=5,
y_range=[0, 0.6, 0.1],
)
不幸的是,对于词汇分布,我们不能将表情符号列表传递给 bar_names,这必须一个个手动添加到正确的位置。我们将为此创建一个辅助函数:
words = [
'banana',
'kiwi',
'lemon',
'strawberry',
'tomato',
'chicken',
'pig',
'sheep',
'crocodile',
'zebra'
]
def add_svg_xticks(bar_chart):
"""Adds svg symbols instead of standard x-ticks to a BarChart."""
for i, word in enumerate(words):
symbol = SVGMobject(f"icons/{word}.svg", width=0.4)
symbol.next_to(bar_chart.bars[i], DOWN, buff=0.3, aligned_edge=DOWN)
bar_chart.add(symbol)
在这里,我们使用 symbol = SVGMobject(f”icons/{word}.svg”) 从磁盘加载一个 SVG 符号,并使用 .next_to() 方法将其移动到对应的柱子正下方。BarChart 包含 10 根柱子(因为我们的词汇表中有 10 个词汇),并且第“i”根柱子可以通过 bar_chart.bars[i] 来引用。

为了说明主题和词汇的随机选择,我们在场景中添加两个箭头:
arrow_topic = Line(ORIGIN, DOWN * 0.8).add_tip().set_color(BLUE)
arrow_word = Line(ORIGIN, DOWN * 0.8).add_tip().set_color(BLUE)
这将绘制一条小的垂直线,.add_tip() 将箭头头部添加到线条上。这些箭头将出现、消失并移动,以说明在随机选择过程中选择了哪个主题和哪个词汇。
我们通过从 Dirichlet 分布中采样来开始动画,以获取每个主题的词汇分布:
topic_p = scipy.stats.dirichlet(self.alphas_topics).rvs(1)[0]
word_p1 = scipy.stats.dirichlet(self.alphas_words_topic1).rvs(1)[0]
word_p2 = scipy.stats.dirichlet(self.alphas_words_topic2).rvs(1)[0]
word_p = np.concatenate((word_p1, word_p2))
并通过让每根柱子从零变换到其初始值来初始化动画:
self.play(
topic_distribution.animate.change_bar_values(topic_p),
word_distribution.animate.change_bar_values(word_p),
)

接下来,我们从主题分布中随机采样一个主题(这是通过多项分布完成的),并使用箭头可视化选择。这可以通过在选择的第一个主题/词汇时将其淡入场景来实现:
# move
arrow_topic.next_to(topic_distribution[0][topic], UP, buff=0.1)
arrow_word.next_to(word_distribution[0][word], UP, buff=0.1)
# animate
self.play(FadeIn(arrow_topic))
self.play(FadeIn(arrow_word))

或者通过播放一个运动动画,当选择了新的主题/词汇时:
# animate move
self.play(arrow_topic.animate.next_to(topic_distribution[0][topic], UP, buff=0.1))
self.play(arrow_word.animate.next_to(word_distribution[0][word], UP, buff=0.1))

现在选择了主题和词汇,我们可以最终开始编写我们的文档。我们复制所选的词汇,并将该词汇动画化地移动到我们正在生成的文档中:
word_symbol = create_word_token(word)
word_symbol.next_to(
word_distribution[0][word], DOWN, buff=0.3, aligned_edge=DOWN
)
topic_symbol = create_topic_symbol(row, topic)
self.play(FadeIn(topic_symbol))
self.play(word_symbol.animate.move_to(topic_symbol.get_center()))
self.play(FadeOut(topic_symbol))

我们重复选择主题和词汇的过程,直到达到我们希望的文档大小,在我们的例子中是 5 个词。要过渡到下一个文档,我们使箭头渐隐:
self.play(FadeOut(arrow_topic, arrow_word))
并创建一个新行以编写新文档:
new_row = VGroup(Tex(f"doc {doc_index}: "))
new_row.next_to(row, DOWN, buff=0.1, aligned_edge=LEFT)
self.add(new_row)
row = new_row

并重新采样词汇/主题分布:
topic_prob = dirichlet(self.alphas_topics).rvs(1)[0]
word_prob = np.concatenate(
[
dirichlet(self.alphas_words_topic1).rvs(1)[0],
dirichlet(self.alphas_words_topic2).rvs(1)[0],
],
)
self.play(
topic_distribution.animate.change_bar_values(topic_prob),
word_distribution.animate.change_bar_values(word_prob),
)

并对任意多个文档重复整个过程。在下面的示例中,我们创建了 3 个文档,每个文档包含 5 个词。

总结
恭喜!你刚刚在 Manim 中创建了一个 LDA 生成过程。在这篇文章中,我们学习了:
- Manim 的三个主要构建模块:
-
Mobjects:可以在屏幕上显示的数学对象,例如
Circle、Square、Axes等。 -
定义可以应用于 Mobjects 的转换和移动的动画。
-
场景:场景是你动画的画布,显示 Mobjects。
2. 创建基本的 Manim 对象,例如箭头、条形图和曲线
3. 动画化生成文档的过程,使用 LDA。
Manim 提供了许多超出本文范围的功能。最好的学习方式是通过实践,因此我鼓励你进一步探索并尝试自己的示例。我也很期待看到你的动画!
参考资料
-
Manim 文档:Manim Community v0.17.3
-
如何使用 Python 创建类似 3Blue1Brown 的数学动画 | 作者 Khuyen Tran | Towards Data Science
代码
产生所有动画的完整代码可以在 这里 找到。
BYOL —对比自监督学习的替代方法
原文:
towardsdatascience.com/byol-the-alternative-to-contrastive-self-supervised-learning-5d0a26983d7c
🚀Sascha 的论文俱乐部
Bootstrap Your Own Latent: A New Approach to Self-Supervised Learning 由 J. Grill 等人
·发表于 Towards Data Science ·10 分钟阅读·2023 年 9 月 7 日
--
在今天的论文分析中,我们将深入探讨关于 BYOL (Bootstrap Your Own Latent) 的论文。它提供了一个对比自监督学习技术的替代方案,能够去除对大量负样本和庞大批量大小的需求。此外,它在理解今天最先进的基础模型(如 DINO 系列,包括 DINOv2)的道路上具有里程碑式的意义。
尽管对比自监督学习框架仍然有些直观,但 BYOL 起初可能会让人感到困惑和不安。因此,这是一个很好的论文来一起分析。让我们深入了解它,揭示其核心思想吧!

图片由 Sascha Kirch 创作,来源于 出版物
论文: Bootstrap your own latent: A new approach to self-supervised Learning 由 Jean-Bastien Grill 等人,2020 年 6 月 13 日
资源: GitHub
类别: 相似性学习、表征学习、计算机视觉、基础模型
其他讲解:
[CLIP] — [GLIP] — [Segment Anything] — [Depth Anything] — [DINO] — [DDPM]
大纲
-
背景与概述
-
声称的贡献
-
方法
-
实验
-
结论
-
进一步阅读与资源
背景与概述
BYOL 属于通过相似性学习进行的自监督表示学习。自监督意味着没有提供明确的真实标签,但可以从未标记的数据中构建监督信号。表示学习意味着模型学习将输入编码到一个维度较低且语义丰富的表示空间中。最后,在相似性学习中,相似的特征在潜在表示空间中被映射得相互接近,而不相似的特征则被映射得更远。这些表示在许多深度学习任务中至关重要,这些任务利用这些表示来生成新数据、执行分类、分割或单目深度估计等。
许多成功的方法,如 CLIP、GLIP、MoCo或SimCLR使用了对比学习的方法。在对比学习中,最大化匹配数据对的得分,同时最小化不匹配数据的得分。这一过程严重依赖于训练期间提供的批量大小和负样本数量。这种依赖性使得数据收集和训练变得更加具有挑战性。
BYOL 的目标是:
-
摆脱对对比学习所需的负样本和大批量大小的需求。
-
减少对特定领域数据增强的依赖,使其适用于其他领域,如语言或图像。
在论文中提到的许多参考文献中,BYOL 突出了它与平均教师、动量编码器和引导潜在预测 (PBL)的相似性。
声称的贡献(根据作者的说法)
-
BYOL(Bootstrap your own latent)的介绍,这是一种自监督表示学习方法,不需要负对(如对比学习中的)。
-
BYOL 表示法被证明优于当时的最新技术水平(论文发布时)。
-
与对比学习法相比,BYOL 显示出对批量大小和使用的图像增强方法更具弹性。

Sascha Kirch 的论文解析
查看列表7 个故事!“DDPM — 去噪扩散概率模型” 论文插图,由 Sascha Kirch 绘制

方法
既然我们已经了解了 BYOL 声称要解决的问题,让我们尝试理解如何实现这一目标。首先,让我们观察图 1 中呈现的架构。

图 1:框架架构。 图像来源 + Sascha Kirch 的注释。
BYOL 由两个网络组成:在线网络和目标网络。在线网络由三个子模块组成,即编码器、投影器和预测器。目标网络由两个子模块组成,即编码器和投影器。两个网络的编码器和预测器具有完全相同的架构,它们仅在模型权重上有所不同。在线网络在训练过程中进行优化,而目标网络则通过在线网络和自身的指数移动平均来更新其权重。
编码器 —— 编码器由一个 ResNet 卷积神经网络组成。它将输入图像转换为潜在表示。
投影器 —— 通过多层感知器网络(MLP)将潜在空间从 4096 维空间投影到 256 维空间。我猜测投影器对框架的工作并不关键,但 256 只是表示学习领域中常用的方便输出维度。
预测器 —— 旨在从在线网络的投影潜在空间中预测目标网络的投影潜在空间。避免表示崩溃至关重要。
在训练过程中,对输入图像应用两个不同且随机选择的增强来构建该图像的两个不同视图。一个视图被输入到在线模型中,另一个视图被输入到目标模型中。这些增强包括但不限于:调整大小、翻转、裁剪、颜色扭曲、灰度转换、高斯模糊和饱和度。训练目标是最小化两个网络输出之间的平方 L2 距离。训练后,最终只保留在线网络的编码器作为最终模型!
就这样。简单,对吧?😜 好吧,看完论文后我的表情更像这样:😵 虽然将框架的处理过程分解为其关键组件相对直接,但获得直觉确实花费了我不少时间。
在我们尝试理解为什么 BYOL 实际有效之前,让我们首先简化呈现的方程并揭示它们的奥秘。
数学揭秘
在大致了解了 BYOL 的架构及其训练方式后,让我们仔细看看这些方程。我不得不说,论文中呈现的数学部分比实际需要的要复杂得多。在某些情况下,它的展示方式过于复杂,而在其他情况下,它在清晰度上滞后,留下了解释的空间,造成混淆。
我会专注于那些我认为理解发生了什么的方程。我们从精确倒序分析这些方程开始,为什么不呢?😜
首先,让我们谈谈训练期间模型参数的更新。回顾一下,我们有两个模型:在线模型和目标模型。在线模型通过使用 LARS 优化器优化损失函数来更新。

方程 1:在线网络的权重更新。来源 + Sascha Kirch 的注释
上面的方程简单地说:“通过调用优化器函数对当前参数、这些参数相对于损失函数的梯度以及学习率 eta 进行更新,来更新模型的参数 theta”。
另一方面,目标模型不是通过优化来更新,而是通过从在线模型中复制权重,并对复制的更新权重和目标网络的当前权重应用指数移动平均:

方程 2:目标网络的权重更新。来源 + Sascha Kirch 的注释
上面的方程简单地说:“通过计算当前权重 xi 和在线模型的更新权重的指数移动平均来更新模型的参数 xi”。Tau 遵循余弦调度,以减少整个训练过程中在线模型的贡献。
现在让我们看看用于更新在线模型的损失函数。它被定义为两个其他损失函数的和。这些损失函数具有相同的方程,稍后我们将看到,但在网络的两个不同输入上计算。回忆一下图 1.,从图像 x 生成了两个不同的视角(即 v 和 v'),通过应用不同的增强。一个视角输入到在线模型中,另一个输入到目标模型中。在训练期间,计算损失之前执行两个前向传递,其中网络的输入会交换。输入到在线模型的图像会输入到目标模型中,反之亦然。

方程 3:BYOL 的损失函数。 来源 + Sascha Kirch 的注释
个体前向传递的损失是在线模型和目标模型的 L2 归一化输出的平方 L2 距离。让我们分解论文中的相应方程:

方程 4:个体损失函数。 来源 + Sascha Kirch 的注释
注:论文中提到这是均方误差,实际上并不正确。L2 距离没有除以其元素数量。我猜他们将其与计算所有批次的均值混淆了。
BYOL 的直觉
现在,我们已经理解了框架和方程的核心信息,让我们尝试获得一些直觉。我会呈现作者的观点,然后我会尝试加入一些我自己的直觉,虽然知道这可能不准确 🤡。
BYOL 如何学习其表示? — 模型被鼓励生成其两个输入的相同潜在表示,这两个输入代表了同一对象/场景的不同视角。无论图像是模糊的、黑白的还是翻转的,猫仍然是猫。事实上,我认为强大的数据增强在这里至关重要。它基本上告诉模型:“看,这些是同一事物的不同变体,所以忽略这些变体,当提取对象/场景的表示时,将它们视为相等!”。
为什么表示没有崩溃? — 回顾之前我们提到,BYOL 属于相似性学习的范畴。网络不会最简单地将所有内容映射到潜在空间的同一点以实现最高相似度吗?实际上,这是相似性学习中的一个主要困难,称为“崩溃解决方案”。对比学习方法通过为每个匹配提供许多负样本来解决这个问题,以将相似特征映射到潜在空间中的彼此接近,同时将不相似的特征映射到更远的位置。BYOL 通过在在线网络和目标网络之间引入不对称性及其预测子模块,并通过基于指数移动平均的更新规则来解决这个问题,以确保在整个训练过程中预测器的近似最优。
[## 获取 Sascha Kirch 发布的新邮件通知 🚀
获取 Sascha Kirch 发布的新邮件通知 🚀 想要了解更多深度学习知识或保持最新动态…
medium.com](https://medium.com/@SaschaKirch/subscribe?source=post_page-----5d0a26983d7c--------------------------------)
实验
BYOL 的作者展示了实验和消融研究,以证明他们方法的有效性。
批量大小的消融研究
从对比表示学习方法(例如,CLIP 和 GLIP)我们知道,在训练过程中,批量大小有很大的依赖性。例如,CLIP 是在批量大小为 32,768 的情况下训练的,这在考虑到它是一个多模态语言-图像模型时显得非常疯狂。
作者声称,由于 BYOL 不需要负样本,它对较低批量大小的敏感度较低,他们通过图 2 所示的实验进行了验证。

图 2:批量大小的影响。图片来源 + Sascha Kirch的注释。
可惜,这对于我的私人笔记本电脑来说可能仍然太大了 😅
图像增强的鲁棒性消融研究
SimCLR论文表明,对比视觉方法对图像增强的选择非常敏感,特别是那些影响颜色直方图的增强。虽然相同图像的裁剪部分共享类似的颜色直方图,但负样本对的裁剪部分则不然。模型在训练过程中可能会走捷径,专注于颜色直方图的差异,而不是语义特征。
作者们声称,BYOL 对图像增强选择的鲁棒性更强,因为在线和目标网络的更新方式。虽然这一假设得到了实验的支持,但仍然存在较强的依赖性,因此性能有所下降。

图 3:对图像增强的鲁棒性。图片来源 + Sascha Kirch的注释。
在 ImageNet 上的线性评估
在表示学习领域,一个重要的特征是模型将语义丰富的特征投射到潜在空间的能力,以便对相似特征进行聚类,并将不同的特征分开。一个常见的测试是冻结模型(对于 BYOL,只冻结在线模型的编码器),并在表示的基础上训练线性分类器。
BYOL 的线性评估已在 ImageNet 上进行,并与许多其他模型进行了比较,超越了当时的最新技术。
你会在许多论文中发现 ResNet-50 编码器与其他 ResNet 变体的区别。只是 ResNet-50 已经成为评估性能的标准网络。

表 1:在 ImageNet 上的线性评估。来源
半监督微调用于分类
表示学习中的另一个典型实验设置是模型在特定下游任务和数据集上进行微调时的表现。
表 2 展示了在分类任务上对 BYOL 进行微调时使用 1%或 10%的整个 ImageNet 训练集的指标。

表 2:在 ImageNet 上的半监督训练。来源
迁移到其他视觉任务
作者们还展示了他们在语义分割任务和单目深度估计任务(计算机视觉的两个重要领域)中迁移学习 BYOL 的实验。
与之前的方法相比,差异微乎其微,但我想这里的关键信息是,“我们有一种不同的方法,效果同样出色。”

表 3:迁移到其他视觉任务。来源
结论
BYOL 提出了一种自监督表示学习的替代方法。通过实现两个进行相似性学习的网络,BYOL 可以在没有对比学习方法所需的负样本的情况下进行训练。为了避免崩溃解,目标网络通过 EMA 从在线网络中更新,并在在线网络之上构建了一个额外的预测子模块。
进一步阅读与资源
如果你已经读到这里:恭喜🎉并感谢😉!既然你似乎对这个话题很感兴趣,这里有一些进一步的资源:
以下是基于 BYOL 的论文列表:
这里是我关于对比学习方法的两篇文章:CLIP 和 GLIP,用于自监督表示学习:
论文总结——从自然语言监督中学习可迁移的视觉模型
towardsdatascience.com ## GLIP: 将语言-图像预训练引入目标检测
论文总结:基础语言-图像预训练
towardsdatascience.com
字节对编码初学者指南
原文:
towardsdatascience.com/byte-pair-encoding-for-beginners-708d4472c0c7
一份通俗易懂的 BPE 标记器指南
·发布于Towards Data Science ·6 分钟阅读·2023 年 10 月 10 日
--

图片由作者提供
在这篇文章中,我们将深入了解一种最著名的标记化算法,即字节对编码(BPE)。它被广泛应用于许多最先进的大型语言模型中,如 BERT 家族、BART 和 GPT 家族。
让我们开始吧。
字节对编码(BPE)
字节对编码(BPE)是一种基于语料库的子词标记化算法。它是基于语料库的,因为它使用训练语料库来学习频繁的字符(或符号)并将其合并成一个符号。它也是一种子词标记器,因为它将文本分解为比(或等于)单词更小的单元。
下面的图像展示了句子“ it is raining”的子词标记化。请注意,虽然“it”和“is”是完整的单词标记;“rain”和“ing”是“raining”的子词。

BPE 算法有两个主要部分:标记学习器、标记分割器。
1- 标记学习器:它处理一组文本并创建一个包含标记的词汇表。这些语料库作为训练语料库。

标记学习器处理一组文本并建立一个词汇表 —— 图片由作者提供
2- 标记分割器:它将一段文本(如句子)分割成标记。该文本是测试数据。我们利用从前一步中获得的学习,在这一步中对测试数据进行标记化。

标记分割器将句子转换为其标记 —— 图片由作者提供
值得一提的是,
“字节对编码(BPE)(Gage,1994)是一种旧的数据压缩技术,它通过迭代替换序列中最频繁的字节对,用一个未使用的字节。”[1]
我们知道的当前 BPE 算法用于分词,适应了这个算法,但不是合并频繁的字节对,而是合并频繁的字符(或字符序列)。
算法的完整伪代码如下。让我们按照伪代码一步一步地学习这个算法。

BPE 伪代码 — 图像来源 [1]
第一阶段如下图红框中突出显示:我们通过查看我们拥有的文档语料库来创建一个词汇表,按字符拆分它们,并将每个字符及其出现频率添加到词汇表中。
我们看到我们的文档语料库包含* [low, lower, newest, widest],每个的频率不同。我们使用*来表示单词的结束。

第一步 — 图像来源 [1] 由作者修改
在第二步中,对于num_merge=10次,我们依次调用三个函数:
-
get_stats():此函数返回一个字典(称为 pairs),其中包含词汇表中每对字符及其出现频率。
-
max():此函数获取 get_stats()返回的字符对,并返回出现频率最高的对。
-
merge_vocab():将字符对视为一个单位,将每个字符对的所有出现替换为一个单独的单位。它返回更新后的词汇表。
这三个函数一起获取语料库中最常见的字符对,将它们合并为一个符号并更新词汇表。我们将此过程重复 num_merges=10 次。

第二步 — 图像来源 [1] 由作者修改
让我们通过上述示例进行演练。我们的语料库如下:

语料库示例 — 图像由作者提供
我们通过将所有单词拆分为字符来构建词汇表。在单词末尾添加 _ 以表示单词的结束,然后将其拆分为字符。在伪代码中,他们使用符号表示单词的结束,但在这里我们使用—。

语料库中的词汇表 — 图像由作者提供
接下来,我们需要计算语料库中每个字符的频率。为了更好地表示字符频率的语料库,我们将其表示如下:

语料库表示 — 图像由作者提供
注意“l o w — 5”表示在“low-”这个词的上下文中,l, o, w 和 — 每个字符都重复了 5 次。
接下来,我们找出哪个字符对最常相邻? 我们看到“e”和“r”最常相邻,共出现 8 次。因此,我们将它们合并为一个符号“er”,将其添加到词汇表中,并用新符号更新语料库表示。

合并“e”和“r”为“er” — 图像由作者提供
我们重复这个过程:接下来哪两个符号最常紧挨在一起? 我们发现“w”和“er”最常紧挨在一起出现 8 次。因此,我们将它们合并为一个符号“wer”*。

合并“w”和“er”为“wer” — 图片来自作者
我们重复这个过程:接下来最常一起出现的两个符号是“wer”和“—”。我们将它们合并为“wer-”并更新语料库:

合并“wer”和“—”为“wer-” — 图片来自作者
频率最高的两个符号是“l”和“o”,它们出现了 7 次。我们将它们合并为“lo”并更新词汇表。

合并“l”和“o”为“lo” — 图片来自作者
到目前为止,我们已经进行了 4 次合并。我们将继续将符号合并,直到达到 10 次合并。之后,我们的词汇表就准备好了,可以使用 BPE 的分词算法对测试数据进行分词。这就是分词算法的工作原理:
BPE 分词器算法: 在测试数据上运行每个从训练数据中学习到的合并。这些已学习的合并是我们之前添加到词汇表中的符号。按我们从训练数据中学到的顺序逐一在测试数据上运行它们。
注意,字符对的频率在这里并不重要,也不需要额外的学习。只需扫描测试数据以查找已学得的词汇条目,并拆分序列。
BPE Tokens 的属性
BPE 分词器的工作方式会导致生成的 tokens 常常是频繁出现的词汇和频繁出现的子词。频繁出现的子词通常是像est和er这样的词素。
词素是具有意义的最小语言单位。它们提供关于语言的意义和语法的线索。
需要注意的是,词汇表的大小可以通过控制合并操作的数量来配置。较小的词汇表会导致更频繁的子词单元。
总结
总之,字节对编码(BPE)算法在现代自然语言处理(NLP)中发挥了关键作用,通过高效地将文本拆分为子词单元。其 token 学习器建立了一个子词词汇表,而 token 分词器使用这个词汇表进行文本分词。尽管存在一些缺点,BPE 在各种 NLP 任务中已经证明了其高效性,并且仍然是许多当前大型语言模型(如 GPT-3、BERT 和 RoBERTa)的基础组件。
如果你有任何问题或建议,请随时与我联系:
电子邮件:mina.ghashami@gmail.com
LinkedIn: www.linkedin.com/in/minaghashami/
参考文献
GitHub Actions 中的缓存
原文:
towardsdatascience.com/caching-in-github-actions-7ff11b6c1874
加速您的 CI/CD 管道
·发表于Towards Data Science ·7 分钟阅读·2023 年 11 月 21 日
--
在这篇文章中,我们将探讨如何缓存GitHub Actions。GitHub Actions 是 GitHub 提供的一个平台,允许自动化工作流,通常用于 CI/CD(持续集成/交付)管道——例如,当想要合并新的 PR 时自动运行单元测试。由于这些管道运行频繁,且执行时间可能显著增长,因此考虑节省时间是很有意义的——缓存动作输出就是一种方法。

照片由Possessed Photography拍摄,发表于Unsplash
在这篇文章中,我们将讨论缓存。我觉得官方文档简洁且未解答一些问题——因此我在这里想多做一些解释。我们首先简要介绍 GitHub Actions 及其缓存的工作原理,然后通过两个示例进行演示:第一个示例遵循创建素数的原始玩具示例,而第二个示例更现实——我们缓存一个完整的 Python 环境。
GitHub Actions 简介
在上一篇文章中,我详细介绍了这一主题——因此这里我们仅简要概述此内容,并且我建议参阅链接中的文章获取详细信息。总的来说,GitHub Actions 允许自动化工作流,通常用于 CI/CD 管道,例如运行单元测试、检查风格指南等。在接收到某些触发事件后,运行器(可以由 GitHub 托管或自定义)会接收由不同步骤组成的任务。让我们用上一篇文章中的一个示例进行演示:
name: Sample Workflow
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
sample_job:
runs-on: ubuntu-20.04
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Set up Python 3.10.0
uses: actions/setup-python@v3
with:
python-version: "3.10.0"
- name: Echo 1
run: echo "Echo 1"
- name: Echo 2
run: |
echo "Echo 2a"
echo "Echo 2b"
在这里,我们定义了一个名为“Sample Workflow”的工作流,并将代码推送和打开新 PR 设置为事件触发器。工作流包括一个在“ubuntu-20.04”上运行的单个作业——这是一个运行所述 Ubuntu 版本的免费 Git 实例。该作业有不同的步骤,包括检出代码库、设置 Python 和输出不同的消息。
为了运行此工作流,我们需要将其放置到.github/workflows文件夹中。一旦放置并推送到 Github,这个工作流将会在定义的事件触发器上自动运行——我们可以方便地查看定义工作流的状态和输出,例如,如下所示:

作者提供的图片
缓存操作
在奠定基础之后,让我们继续讨论缓存。通过缓存操作,我们可以定义一个缓存步骤。借用我们即将到来的玩具示例,这可以如下所示:
- name: Cache Primes
id: cache-primes
uses: actions/cache@v3
with:
path: prime-numbers
key: ${{runner.os}}-primes
缓存由一个键和一个路径唯一标识:如果这两者都存在/填写,缓存将被加载——否则生成。当工作流第一次运行(或某些依赖项发生更改)并生成缓存时,指定在path下的文件夹内容将上传到某个 Github 存储区。这样,缓存就与之前运行的运行器无关,并且始终可用(特别是,你不需要自己的运行器来持久化缓存——可以使用公开的 Github 运行器)。
现在,在接下来的步骤中,我们可以检查缓存是否可用(缓存命中),并跳过它(通常是生成缓存内容的步骤):
- name: Generate Prime Numbers
if: steps.cache-primes.outputs.cache-hit != 'true'
run: ./generate_primes.sh
缓存生成素数
话虽如此,让我们给出第一个完整的示例:通过 Github Actions,我们生成前N个素数,并缓存此输出。请注意,这一做法受到原始文档的启发,尽管在我看来更为完整。
这个示例由两个 bash 脚本组成。通过第一个脚本generate_primes.sh,我们生成前N个素数并将它们写入prime-numbers/generate_primes.sh:
#!/bin/bash
N=10 # Number of prime numbers to generate
file_path="prime-numbers/primes.txt" # Path where to store the primes
# Remove existing file if it exists
rm -f "$file_path"
# Function to check if a number is prime
is_prime() {
num=$1
for ((i=2; i*i<=num; i++)); do
if ((num % i == 0)); then
return 1
fi
done
return 0
}
# Create directory for prime numbers if it doesn't exist
mkdir -p "$(dirname "$file_path")"
echo "Generating prime numbers ..."
count=0
number=2
while [ $count -lt $N ]; do
if is_prime $number; then
echo $number >> prime-numbers/primes.txt
((count++))
fi
((number++))
done
另一个脚本primes.sh读取此文件,并简单地打印存储的素数:
#!/bin/bash
# Read and print prime numbers from the file
if [ -f prime-numbers/primes.txt ]; then
echo "Prime numbers:"
cat prime-numbers/primes.txt
else
echo "File prime-numbers/primes.txt not found."
fi
假设我们想找很多素数,并且这需要一段时间,这与缓存这个过程密切相关,这正是我们用prime_workflow.yml做的:
name: Caching Primes
on: push
jobs:
build:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- name: Cache Primes
id: cache-primes
uses: actions/cache@v3
with:
path: prime-numbers
key: ${{ runner.os }}-primes
- name: Generate Prime Numbers
if: steps.cache-primes.outputs.cache-hit != 'true'
run: ./generate_primes.sh
- name: Use Prime Numbers
run: ./primes.sh
我们检出代码库,在第 2 步调用缓存操作:键由运行器操作系统和后缀“-primes”组成,缓存路径是第一个脚本将结果文件转储到的文件夹。
然后,我们要求工作流生成素数(即运行generate_primes.sh)——如果没有缓存命中,例如,当第一次执行此脚本时。
最后,我们在脚本primes.sh中使用生成的或缓存的素数。查看此工作流的第二次运行,我们观察到确实跳过了“生成”步骤:

作者提供的图片
你还可以在Github上找到完整的示例。
缓存 Poetry 环境
现在,让我们来看一个稍微现实一点的示例:强烈建议任何 Python 项目都捆绑一个环境,以确保所有开发者使用相同的包和版本。在 Github Actions 运行器上,仓库通常是全新检出的,这意味着每次都必须重新安装设置的环境。因此,我们在这里展示如何缓存这个环境——即与其下载和安装包,不如将完整的环境缓存并从缓存中下载。特别是,此示例将展示如何使用poetry,这是我管理环境时更倾向使用的工具。
该项目中的示例文件main.py如下所示:
import matplotlib.pyplot as plt
import numpy as np
def plot():
x = np.linspace(0, 10, 50)
y = np.sin(x)
plt.plot(x, y)
plt.savefig("plot.png")
if __name__ == "__main__":
plot()
即,我们使用matplotlib绘制简单的正弦曲线,因此需要这个和 numpy。
因此,我们的pyproject.toml文件包含以下内容(这里,我假设你对 poetry 有基本了解——否则可以参考链接的帖子):
[tool.poetry]
name = "myproject"
version = "0.1.0"
description = "…"
authors = ["hermanmichaels <hrmnmichaels@gmail.com>"]
[tool.poetry.dependencies]
python = "3.10"
matplotlib = "3.5.1"
mypy = "0.910"
numpy = "1.22.3"
black = "22.3.0"
正如我们所见,我们安装了所需的包——以及一些每个 Python 项目都不应错过的有用工具。
然后,设置环境(包括缓存)的相应 Github Actions 工作流如下所示:
name: Caching Env
on: push
jobs:
build:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.10.0
uses: actions/setup-python@v3
with:
python-version: "3.10.0"
- name: Install poetry
run: curl -sSL https://install.python-poetry.org | python3 -
- name: Cache Env
id: cache-env
uses: actions/cache@v3
with:
path: ~/.cache/pypoetry
key: ${{ runner.os }}-env
- name: Install poetry dependencies
if: steps.cache-env.outputs.cache-hit != 'true'
run: poetry install
我们首先安装 Python 和 poetry。在最后一步,我们运行poetry install,它将所有所需的包安装到 poetry 环境中。
本帖的关键点是倒数第二步:在此步骤中,我们定义了一个目标路径为~/.cache/pypoetry的缓存——这是 poetry 环境默认存储的地方。因此,如果再次运行此工作流,并且此键/文件夹组合存在于缓存中,我们将跳过 poetry 安装——而是从云缓存中下载完整的环境。
此示例也可以在相同的演示仓库中找到。
注意:这其中存在权衡。在没有缓存的版本中,我们下载所有包并安装它们。在使用缓存的版本中,我们不需要安装任何东西,而是需要从缓存中下载所有已安装的包。哪个更快应取决于各种因素,如带宽、包的大小和安装时长。如果你有实际的数据点,或者你更喜欢如何处理,请在评论中告诉我!
缓存管理
最后,关于监控和管理缓存的一点说明:当你在 Github 上打开你的仓库,并导航到“Actions / Caches”时,我们会看到如下图像:

作者提供的图像
这个仓库中使用的所有缓存都被展示出来,例如,指明了它们的大小,我们可以在需要时将其移除。
结论
这结束了我们对 Github Actions 缓存的介绍。这是一个非常有用的功能,因为 CI/CD 流水线通常让开发者等待不耐烦——通过缓存,我们可以减少这些流程的运行时间,并减少对整个系统的负载。
在对主题进行一般介绍后,我们通过两个具体示例展示了如何应用缓存:我们从缓存生成的素数的玩具示例开始,然后展示了如何缓存一个诗歌环境。这个Github 仓库包含了所有示例代码。
感谢阅读!
召唤所有函数

图片由作者使用 Dall-E 创建
基准测试 OpenAI 的函数调用与解释
·
关注 发表在 Towards Data Science ·9 分钟阅读·2023 年 12 月 7 日
--
感谢 Roger Yang 对这篇文章的贡献
对第三方大型语言模型(LLMs)的可观察性主要通过基准测试和评估来实现,因为像 Anthropic 的 Claude、OpenAI 的 GPT 模型和 Google 的 PaLM 2 这样的模型是专有的。在这篇博客文章中,我们将 OpenAI 的 GPT 模型的函数调用和解释与各种性能指标进行基准测试。我们特别关注 GPT 模型和 OpenAI 功能在正确分类幻觉和相关响应方面的表现。下面的结果展示了不同 LLM 应用系统在速度和性能之间的权衡,以及如何利用这些带有解释的结果进行数据标注、LLM 辅助评估和质量检查。我们使用的实验框架也在下面提供,以便从业者可以在默认分类模板上进行迭代和改进。
OpenAI 函数调用和解释
使用 OpenAI 函数调用,你可以描述函数给各种 GPT 模型,这些函数可以用来输出包含调用这些函数的参数的 JSON 对象。函数调用本质上作为一种工具或代理,用于以给定格式记录响应,以可靠地将 OpenAI GPT 模型的能力与外部工具和 API 连接。关于解释,由于许多情况下很难理解 LLM 为何以特定方式响应,这些解释旨在促使 LLM 解释输出是否正确。然后,你可以附上一个输出标签(‘相关’或‘无关’)和 LLM 的解释。下面是一个带有解释的‘相关’评估示例,用于调试 LLM 响应。

图片由作者提供
在下面的“结果与权衡”部分,提供了两个用例的比较表格:预测相关性和预测幻觉(分别为表 1 和表 2)。每个表格比较了 GPT-4-turbo、GPT-4、GPT-3.5 和 GPT-3.5-turbo 在特定提示指令和 LLM 属性下的各种分类指标(准确性、精确度、召回率和F1 分数)的表现。
测试的四个基础模型在这些情况下使用上述指标(除了中位处理时间)进行评估。
-
无 _function_calling & 无 _explanations
-
有 _function_calling & 无 _explanations
-
有 _function_calling & 有 _explanations
-
无 _function_calling & 有 _explanations
因此,LLM 中的提示模板保持不变(即普通的提示完成),例如without_function_calling & without_explanations。具有功能调用但无解释的样本要求 LLM 将其答案放在仅接受枚举作为输入的 JSON 对象中,而具有功能调用和解释的样本要求 LLM 在同一个 JSON 对象中提供解释(详见Google colab notebook)。
基准数据集和评估指标
正确识别 LLM 输出中的幻觉和相关响应是我们客户当前实施 LLM 应用中最大的两个痛点。优化 LLM 评估系统对于幻觉意味着正确识别所有虚构响应,同时跟踪事实输出。对于利用搜索和推荐作为其用户体验的一部分的用例,与用户满意度相关的最重要因素是结果的速度和相关性。为了评估 LLM 系统在相关和无关输出上的性能,您应该了解关键指标:
-
精确度和召回率:检索到的信息有多相关?
-
准确性:回应在语境上的准确度有多高?
-
延迟:系统提供响应需要多长时间?
-
用户反馈:结果的相关性和响应时间如何影响用户体验?
结果与权衡
这里是关于基准测试 LLM 系统在无关和相关预测上的结果:

表 1 作者提供
这里是基准测试 LLM 系统在幻觉和事实预测上的结果:

表 2 作者提供
在分析结果之前,你可以在这个Google colab notebook中自行复现这些结果。请注意,通常情况下,由于 LLM 的非确定性特性,你无法完全复现这些表中的数字,但对于这个 notebook,我们已经添加了一个种子以保证每次采样结果都相同。此外,我们还添加了分层采样,使得二进制类别确实是完全 50/50。请注意,运行此 notebook 需要消耗计算资源与您的 OpenAI API 密钥相关。 默认采样数量设置为 2,但如果您希望复制这篇博文中的结果,可以将数字更改为 100。
中等处理时间
为了清晰起见,这些比较(使用了 100 个样本)是在 Google Colab 上使用标准 OpenAI API 帐户和密钥进行的。因此,虽然在不同的设置下运行时延迟值不太可能完全准确,但最慢和最快的模型将会被重现。
此外,在评估中使用解释可能需要花费 3 到 20 倍的时间来编译(这与函数调用无关)。
关于整体相关性的模型预测能力
- 延迟:GPT-3.5-instruct > GPT-3.5-turbo > GPT-4-turbo > GPT-4
关于模型预测能力对幻觉的表现
- 延迟:GPT-3.5-instruct > GPT-3.5-turbo ~ GPT-4-turbo > GPT-4
带有函数调用的 GPT 模型的延迟通常略高于不带函数调用的 LLM,但请注意一些警告。首先,延迟是从 OpenAI 返回的 HTTP 头信息中提取的,因此根据你的帐户和请求方式,延迟值可能会有所变化,因为这些值是由 OpenAI 内部计算的。函数调用的权衡也取决于你的用例。例如,若没有函数调用,你需要通过提供示例和详细描述来精确指定输出的结构。然而,如果你的用例是 结构化数据提取,那么直接使用 OpenAI 函数调用 API 是最简单的。
总的来说,带有函数调用的 LLM 的表现与不使用函数调用而使用普通提示完成的 LLM 相当。你是否决定使用 OpenAI 函数调用 API 而不是提示工程,应该取决于你的用例和输出的复杂性。
GPT 模型性能比较
关于整体相关性的模型预测能力:
- 性能:GPT-4 ~ GPT-4-turbo ~ GPT-3.5-turbo >>> GPT-3.5-instruct
关于模型预测能力对幻觉的表现:
- 性能:GPT-4 ~ GPT-4-turbo > GPT-3.5-turbo > GPT-3.5-instruct
有趣的是,在这两种用例中,使用解释并不总是能提高性能。更多内容见下文。
评估指标
如果你在决定使用哪个 LLM 来预测相关性,你可以选择 GPT-4、GPT-4-turbo 或 GPT-3.5-turbo。
GPT-4-turbo 能够准确识别输出的相关性,但在召回所有 50 个示例上有所牺牲,实际上,即使使用解释,其召回率也不比抛硬币好。
GPT-3.5-turbo 也面临相同的权衡,尽管它的延迟较低且准确性较低。从这些结果来看,GPT-4 拥有最高的 F1 分数(精确率和召回率的调和平均值)和总体最佳表现,同时运行时间与 GPT4-turbo 相当。
GPT-3.5-instruct 预测所有内容都是相关的,因此不是一个可行的预测相关性的 LLM。有趣的是,当使用解释时,预测性能显著提高,尽管它仍然低于其他 LLM。同时,GPT-3.5-instruct 无法使用 OpenAI 函数调用 API,并且可能会在 2024 年初被弃用。
如果你正在决定选择哪个 LLM 来预测虚假信息,你可以使用 GPT-4、GPT-4-turbo 或 GPT-3.5-turbo。
结果显示,在精确度、准确性、召回率和 F1 分数方面,GPT-4 在识别虚假和事实输出的正确率上比 GPT-4-turbo 高出约 3%。
尽管 GPT-4 和 GPT-4-turbo 的表现略高于 GPT-3.5-turbo(请注意,得出小差距不代表噪声的结论时应使用更多样本),如果你计划使用解释,可能值得使用 GPT-3.5-turbo。
用于预测虚假和事实的解释在 GPT-3.5-turbo 上的返回速度比 GPT-4 和 GPT-4-turbo 快三倍以上,然而,当比较 GPT-4 模型预测虚假信息的召回率时,两个 GPT-3.5 模型的召回率有所下降。
讨论与影响
在决定使用哪个 LLM 进行应用时,需要进行一系列实验和迭代。同样,在决定是否将 LLM 用作评估器时,也需要进行基准测试和实验。基本上,这两种方法是基准 LLM 的主要方法:LLM 模型评估(评估基础模型)和通过可观察性进行 LLM 系统评估。

图片由作者提供 | 在单一基础模型上评估两种不同的提示模板。我们正在测试相同的数据集,并观察它们的精确度和召回率等指标如何对比。
最终,当决定一个 LLM 是否适合你的用例作为性能评估器时,你需要考虑系统的延迟以及相关预测指标的性能。在本文中,我们总结了这些模型的开箱即用表现——没有增加提高速度和性能的技术。请记住,开箱即用时,这些 LLM 都使用零样本模板,因此没有将示例添加到 LLM 提示模板中以改善模型输出。由于这些数据作为基准,团队可以通过提示工程、提示模板(和存储库)、代理、微调以及像 RAG 和 HyDE 这样的搜索和检索应用来提高 LLM 系统性能。
潜在的未来工作:数据标注的解释
通过这种基准测试,我们发现了一些有趣的示例,其中提供解释会改变预测标签。以下示例在没有使用解释时预测为“relevant”,即使真实标签也是“relevant”。由于即使是“黄金数据集”也可能存在标注错误(尤其是在更主观的任务中),LLM 提供的充分解释可能足以编辑真实标签。这可以视为 LLM 辅助的评估或质量检查。
以下是来自维基数据集的一个示例,用于相关性说明。请注意,‘D’列是数据集提供的真实标签,‘E’列是没有调用函数和解释的预测标签,而‘F’列是创建的预测标签(未调用函数),并带有‘G’列中的解释。因此,‘E’列和‘F’与‘G’列是两个独立 LLM 调用的响应。F&G 是从同一次调用中生成的,如下图 1 所示。

图 1(作者提供):脚本截图(代码见 colab)。在这里,标签和解释一起返回,但我们要求首先提供解释(见提示)。
下表展示了当我们拥有真实标签为‘relevant’,LLM 预测标签在没有调用函数的情况下为‘relevant’,但当 LLM 被要求首先提供解释时,标签变为‘irrelevant’的情况。与我们遇到的几个类似示例一样,LLM 对将检索到的答案标记为‘irrelevant’提出了有效的论据。虽然我们许多人经常考虑罗马帝国,但我们可以同意,对于“罗马帝国持续了多久?”的多段回答既不简洁也不够相关,不足以引起最终用户的积极反馈。LLM 辅助的评估有很多可能性,包括为需要数据标记的公司节省成本和时间。这以及解释所提供的可见性,以及 LLM 返回其参考文献(用于预测的文档),都是 LLM 可观测性领域的重大进展。

作者示例
结论
希望这篇文章能为那些希望更好地理解并平衡新 LLM 应用系统的性能与速度权衡的团队提供一个良好的开端。正如往常一样,生成式 AI 和 LLMOps 领域正在迅速发展,因此观察这些发现和领域如何随时间变化将是很有趣的。
有问题吗?请在此处或在LinkedIn、X或Slack与我联系!
如何使用 Llama2 和 LangChain 构建本地聊天机器人
原文:
towardsdatascience.com/can-a-llama-2-powered-chatbot-be-trained-on-a-cpu-ce9ec6ebe3c2
使用 Python 的概述和实现
·发表于 Towards Data Science ·6 分钟阅读·2023 年 10 月 12 日
--

图片来源:Adi Goldstein 在 Unsplash
目录
∘ 介绍
∘ 案例研究
∘ 第 1 步 — 创建向量存储
∘ 第 2 步—创建 QA 链
∘ 第 3 步 — 创建用户界面
∘ 评估聊天机器人
∘ 结果
∘ 最终判决
∘ 参考文献
介绍
本地模型的出现受到希望建立自己定制 LLM 应用程序的企业的欢迎。这些模型使开发者能够构建可以离线运行并符合隐私和安全要求的解决方案。
这些大规模语言模型(LLMs)最初非常庞大,主要服务于那些拥有资金和资源来配置 GPU 和在大量数据上训练模型的企业。
然而,本地 LLM 现在有了更小的尺寸,这引发了一个问题:普通 CPU 用户是否可以利用这些相同的工具和技术?
这个问题值得考虑,因为用户可以从构建自己的个人本地聊天机器人中获得很多好处,这些机器人可以离线执行任务。
在这里,我们通过在 CPU 上使用 Meta 的 Llama2 构建一个封闭源代码的聊天机器人来探讨这种可能性,并评估其作为个人可靠工具的表现。
案例研究
为了测试在个人电脑上离线运行本地聊天机器人的可行性,让我们进行一个案例研究。
目标是使用量化版本的 Meta 的 Llama2(7B 参数)构建一个聊天机器人。该模型将用于构建一个 LangChain 应用程序,促进响应生成,并可以通过用户界面访问,让人们与应用程序进行互动。

聊天机器人图示(由作者创建)
该聊天机器人将使用两个 PDF 文档进行训练(两个文档均可通过 arXiv API 访问):
为了提供背景,这个机器人将在具有以下规格的计算机上进行训练:
-
操作系统:Windows 10
-
处理器:Intel i7
-
RAM: 8GB
注:跟随本案例研究需要具备 LangChain 框架和 Streamlit 库的基础知识
第 1 步 — 创建向量存储
首先,我们创建向量存储,它将存储来自文档的嵌入数据,并促进与用户查询相关文档的检索。

创建向量存储(由作者创建)
为此,数据必须转换为文本块。这通过使用 PyPDFLoader 加载 PDF 文档,并将文本分割成 500 个字符的块来完成。
接下来,使用来自 HuggingFace 的句子变换器将文本块转换为嵌入。参数中指定设备为“cpu”是很重要的。
在创建了文本块并加载了嵌入模型后,我们可以创建向量存储。对于这个案例研究,我们使用 Facebook AI 相似性搜索(FAISS)。
这个向量存储将被保存在本地,以备将来使用。
第 2 步—创建 QA 链
接下来,我们需要加载检索 QA 链,它从向量存储中检索相关文档,并使用它们来回答用户的查询。

QA 链(由作者创建)
QA 链需要三个组件:量化的 Llama 2 模型、FAISS 向量存储和一个提示模板。
首先,我们下载量化的 Llama2 模型,该模型可在HuggingFace 仓库中找到。对于这个案例研究,模型通过名为“llama-2-7b-chat.ggmlv3.q2_K.bin”的文件进行下载,占用内存 2.87 GB。
然后使用 CTransformers 加载模型,这是一个用于绑定在 C 中实现的变换器模型的 Python 库。由于我们想要一个生成响应的目标型聊天机器人,因此将temperature设置为 0。
接下来,我们加载先前创建的向量存储。
之后,我们定义提示模板。此步骤是可选的,但由于我们希望与研究论文进行互动,我们需要优先考虑准确性,这可以通过提示中的指令来实现。幻想回答是非常不受欢迎的,因此主要指令是对无法通过提供的 PDF 文档回答的问题回应“我不知道”。
有了这些元素,我们可以创建 QA 链,该链将使用加载的量化 Llama2 模型、向量存储和提示模板生成对用户查询的响应。
最后,我们创建执行响应生成的函数。
第 3 步——创建用户界面
LangChain 应用所需的核心元素已经构建完成,因此我们可以转向为聊天机器人构建用户界面。
Streamlit 库适合这个任务,因为它包含了针对聊天机器人应用的特性。
以下代码将先前构建的函数整合到用户界面中(要查看完整的源代码,请访问 GitHub 仓库)。
Streamlit 应用使用如下:
streamlit run app.py
完成了!我们的个人闭源聊天机器人已成功运行!

聊天机器人用户界面(由作者创建)
评估聊天机器人
我们有了聊天机器人,让我们用 3 个不同的问题来评估其表现:
- 计算机视觉在运动分析中的好处是什么?

聊天机器人响应(由作者创建)
2. 给我一个包含计算机视觉的运动项目列表。

聊天机器人响应(由作者创建)
3. 用什么算法来追踪运动员?

聊天机器人响应(由作者创建)
结果
总的来说,机器人似乎能够返回令人满意的响应,而不包含未经请求的信息。
然而,从对“用什么算法来追踪运动员?”这个问题的回答中,可以明显看出一个限制,即答案在句子中途被截断。这可以归因于量化版 Llama2 模型的有限上下文窗口(即令牌数量)。聊天机器人无法正确回答需要大量令牌的问题。
此外,一个在响应中未传达的限制是时间。虽然机器人对问题的回答适当,但在这台 CPU 上生成响应平均需要超过一分钟。由于运行时间如此长,使用此工具作为替代手动搜索文档集合中的内容的理由并不充分。
最后,这整个过程消耗了我计算机大量内存,使其他应用程序无法使用。
最终裁决

图片由Arek Socha提供,来自Pixabay
既然案例研究已经结束,让我们重新审视最初的问题:我们能否在 CPU 上构建 LLM 驱动的应用程序?
答案是:是的……但我们可能不应该。
本案例研究的积极之处在于量化后的 Llama2 模型容易下载并融入应用中,并且聊天机器人能够生成高质量的回应。
然而,有限的令牌、较长的运行时间和高内存使用的组合使得在 CPU 上训练闭源聊天机器人的前景变得不可行。
当然,这一结论是基于进行案例研究所用设备的限制,因此,具有更高处理能力和存储空间的计算机可能会产生更有希望的结果。此外,未来会有更小的 LLM 与更大的上下文窗口公开,使得闭源聊天机器人在 CPU 上构建和使用变得更加容易。
如果你有兴趣了解这个应用在你的设备上表现如何,随时可以查看以下代码库中的代码:
anair123/Llama2-Powered-QA-Chatbot-For-Research-Papers (github.com)
感谢阅读!
参考文献
-
Naika, B. T., Hashmi, M. F., & Bokde, N. D. (n.d.). 《计算机视觉在体育中的综合评述:开放问题、未来趋势和研究方向》。Arxiv.
arxiv.org/pdf/2203.02281 -
Zhao, Z., Chai, W., Hao, S., Hu, W., Wang, G., Cao, S., Song, M., Hwang, J.-N., & Wang, G. (n.d.). 《深度学习在体育应用中的调查:感知、理解与决策》。Arxiv.
arxiv.org/pdf/2307.03353.pdf
人工智能能克服人类的确认偏差吗?
原文:
towardsdatascience.com/can-ai-overcome-humans-confirmation-bias-57bee0bc5c8c
人工智能如何补充人类智慧的不足,并作为人类认知偏差的对冲力量
·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 11 月 11 日
--

从诺贝尔奖得主丹尼尔·卡尼曼的《思考,快与慢》一书中,我们知道人类的大脑在其应有的功能上远非完美。除了情绪冲动、顽固的成瘾以及自人类文明诞生以来的道德挣扎外,卡尼曼在他的书中全面描述了我们认知偏差的细微差别。更令人沮丧的是,许多偏差渗透到我们生活的方方面面、组织和社会中。鉴于此,我们现在面临两个关键问题。首先,我们如何识别我们决策中的这种偏差的现实?其次,我们如何在决策过程中弥补或防止这些偏差?在本文中,我将从数据科学的角度出发,通过关注最普遍的偏差之一——确认偏差,来回答这两个问题。随着机器学习和人工智能的进步,在它们的帮助下,我们现在看到了识别、克服和防止这些偏差的曙光。
什么是确认偏差?
确认偏差是指解释和寻找信息以确认或支持现有信念或先前结论的倾向。由于确认偏差,人们倾向于通过片面地测试想法、关注支持/积极证据而忽视可能反驳自己观点的替代/矛盾事实来确认自己的信念或假设。确认偏差是无意的,与故意欺骗相对。这种偏差在许多领域的人类生活和社会中具有广泛的影响,包括科学研究、医学、政治、执法、社会心理学和组织决策。
英国心理学家彼得·凯瑟特·瓦森在 1960 年代首次系统性地提出并研究了确认偏差。在他的实验中,瓦森给参与者一个 4 卡片谜题,也称为 Wason 选择任务。这个谜题可以有任何变体,但结果高度可重复。让我们来看一个例子:
桌子上放着四张卡片,每张卡片的一面有一个数字,另一面有一个颜色。规则是如果一张卡片的一个面显示偶数,则另一面应该是蓝色。现在,桌子上你看到四张卡片:3、8、蓝色和红色。你必须翻转哪张卡片来测试规则是否成立?

Wason 选择任务的一个例子:必须翻转哪个卡片来测试规则“如果一张卡片的一个面显示偶数,则另一面是蓝色”? 图片来源:维基百科
每个人都知道需要翻转 8 号卡;有些人可能选择 8 号和蓝色卡。正确的答案是翻转 8 号卡和红色卡,而大多数人会遗漏翻转红色卡。如果你翻转 3 号卡,无论另一面是蓝色还是非蓝色都与规则无关。同样地,如果你翻转蓝色卡发现另一面是奇数,这也不重要,因为规则规定另一面可能是偶数。另一方面,如果你翻转红色卡发现另一面是偶数,那么你就证明了规则被违反。
令人惊讶的是,参与者在各种形式的测试中表现一直很差。人们关注规则的积极支持(另一面是蓝色),但忽视了可能会使规则无效的信息(另一面不是蓝色)。
Wason 选择任务的规则都很简单,具备逻辑条件和结果:如果 P,则 Q。要完全验证规则是否有效,需要确认下面列出的两个标准:
-
如果 P,则 Q
-
如果不是 Q,则不是 P
平均而言,只有 10%的 Wason 选择任务参与者完全正确地选择了第二种选项,这显示了人类大脑自动关注积极证据来确认结论,但很难检查可能反驳规则的证据。
有趣的是,如果我们在难题中加入社会背景,大多数人能够很快回答正确,主要涉及权限或职责。一个流行的例子是这样的:规则是如果你未满 21 岁,你不能喝酒。假设有四张卡片——啤酒、28、可乐、15。你必须翻转哪张卡片以测试规则是否正确?大多数人会凭直觉快速给出正确答案:翻转啤酒卡以查看另一面是否标注年龄超过 21 岁,并翻转 15 卡以确认另一面是否列出了含酒精的饮料。
什么导致确认偏差?
Wason 选择任务的结果意味着什么?科学家们仍在研究哪些神经回路和机制可以解释确认偏差。但我们可以推导出两点:
-
人脑不是通过符号和标记来解决这种逻辑问题的逻辑运算器。
-
当人们有社会情境中的规则的先前经验时,偏差可以通过社会背景克服。
参考生物大脑和人工学习中的神经网络学习机制(见文章 “从生物学习到人工神经网络:接下来是什么?”),确认偏差可能是神经网络在模式识别中工作的副产品。对于现有的信念或规则,神经网络通过加强输入前提条件的相关神经连接来进行学习。类似的证据会激活相同的网络,得出相同的结论,同时加强神经连接。相反,要验证对立面,网络需要通过不同的输入数据(非 P)进行单独训练,以得出不同的结论(非 Q)。换句话说,网络可能涉及不同的神经连接来进行独立学习。由于建立另一个神经输出以理解对立效果的障碍和努力,人脑倾向于现有的脑电路。
当人们遵守社会规则时,他们知道如果不遵守会受到惩罚或付出一定的代价。这种对立场景被认为是被纳入大脑的电路中,这解释了为什么在解决社会背景中的难题时,人类没有困难看到另一面。换句话说,大脑首先通过实证数据学习了双方的场景。
然而,还有另一种方法可以防止确认偏差,那就是使用超越我们本能大脑能力的工具。这些通常被称为心理工具。对于 Wason 选择任务,这个工具就是简单的逻辑:
-
如果 P,那么 Q
-
如果不是 Q,那么不是 P
假设我们将每个规则的前提(P)和结果(Q)插入上述两个场景;无论是否与社会背景相关,我们将 100%正确地解开难题。换句话说,使用正负逻辑这样简单的思维工具可以帮助我们清晰地思考,而不会凭直觉忽视对立面。
然而,在现实世界中,实验室之外的规则更复杂和隐含。数据科学和 AI 可以通过利用相同的原则和策略来帮助人类克服确认偏差。
数据科学和 AI 如何克服确认偏差?
鉴于生物神经网络和人工神经网络之间的学习相似性,我们不希望 AI 一开始就重复人类的偏见。虽然人类克服确认偏差很困难,但 AI 和机器学习有以下优势来克服这些偏差:
-
模型和算法以预设的方式解释训练数据。因此,它们不会像人类那样有解释或偏好某些问题而忽略相反的事实。
-
数据科学家和工程师在 AI 方面的协作工作使其更加客观,与每个人观点中的人类偏见形成对比。
-
对于 AI 来说,增加额外的统计和逻辑工具与流程以防止偏差的发生是很灵活的。
另一方面,由于 AI 和机器学习依赖于人为策划的训练数据,因此需要额外的预防措施以防止将偏差引入模型。因此,我们应关注三个领域来克服确认偏差:训练数据、模型测试和监控,以及结果的可解释性。
- 确保训练数据不带有偏见
自数据科学开始以来,我们的口号之一就是做出“数据驱动的决策”。但正如我们所知,数据可能是不完整的、垃圾的或完全错误的,这是一大危险,可能导致 AI 做出糟糕或有偏见的决策。使训练数据正确且完整,包括确认和否认的事实,是消除确认偏差的前提。
例如,假设我们构建一个模型来预测订阅者的增长。除了寻找与订阅者相关的特征外,是否探索过与非订阅者相关的特征?是否有些特征同时对订阅和非订阅有所贡献?训练数据是否限制或偏离了我们的预测和决策?
确保训练数据中所有方面的事实得到均等代表是确保 AI 不继承人类偏差的关键步骤之一。由于 AI 模型依赖于人类收集和整理的数据,而人类倾向于有确认偏差,因此在设计训练模型时,确保数据包含正面和负面场景是必须的,以确保模型没有偏差。然而,这通常涉及不同的数据来源,并需要多种数据收集和整理方法。在某些情况下,如果对立数据不存在或收集成本较高,数据合成可能是模拟对比场景的解决方案。
2. 通过彻底测试和验证来防止偏差
机器学习和人工智能已经具备自动测试过程来验证模型。然而,验证的目的通常围绕预测的可重复性、确保模型泛化能力而不出现过拟合,以及从统计分布中移除异常值。防止确认偏差需要额外步骤来验证训练集和模型的行为及输出。例如,模型能否同时确认和反驳一个假设?由于负面案例样本较少,是否存在任何后果或异常?是否由于人类干预认为不重要而导致某些特征被低估?
确认偏差的识别不是一次性的任务。在不断变化的世界中,新的负面或矛盾的事实可能会意外出现。最近的例外情况可能在未来成为新的常态。数据整理中的例外处理程序应定期检查,以确定是否删除了实际的相反案例。此外,应该定期进行审计和测试,以确保在模型发布后没有引入偏差。
3. 演示“思考”过程
从人类经验来看,我们的思维过程和心理模型对做出正确的决定至关重要。我们应该评估和理解 AI 模型如何得出决定或结论。AI 的一个明显优势是它可以让许多数据科学家和工程师同时合作评估其决策过程,而人类只能在个体的思维中单独进行。
然而,神经网络和深度学习模型以难以解释而闻名。鉴于此,可能需要推理过程和混合方法来理解一个决定或结论是否存在偏差:
-
彻底了解训练数据的来源、处理方式以及如何用于模型。
-
使用特定过程和可用库(例如,LIME — 局部可解释模型无关解释,SHAP — Shapley 加性解释)提高模型的可解释性。
-
利用可视化(例如图表)来展示不仅仅是最终结果,还包括从源数据到训练和模型执行的全过程,例如训练数据的质量、支持每个决策的参数实例、输出类别随时间的一致性、过程中的任何异常或离群点等。通过这种方式,数据工程师和数据科学家更容易识别模型可能在哪个步骤出现偏差,以及还需要哪些数据或训练。
结论
在历史上,人类一直擅长发明工具以克服自身的局限性和约束。鉴于人类和 AI 智能之间的相似性和差异性,我们应关注 AI 如何补充人类智能的不足,并防止人类的认知偏差。虽然人类难以克服这些偏差,但数据科学和 AI 可以帮助我们识别并最小化这些偏差,同时使过程更加透明。尽管本文重点讨论了确认偏差,但类似的原则和方法也可以应用于解决其他认知偏差。
AI 真的能帮你通过面试吗?
原文:
towardsdatascience.com/can-ai-really-help-you-at-passing-interviews-22bce4a57a2
思考、提示和一些建议
·发布在 Towards Data Science ·9 分钟阅读·2023 年 8 月 5 日
--

Andertoons 图像许可证
最近,我听到了一些面试官的担忧,他们担心候选人可能会利用 AI 来通过技术面试。担忧在于使用像 ChatGPT 或 Bard 这样的 LLM 技术,候选人可能会“作弊”并在技术面试中解决问题。
我认为你应该利用 AI来通过面试,但不是你最初所期待的那种方式。
通过面试最重要的因素是充分准备,而准备的最佳方式就是练习。
一些公司会收取数千美元,通过模拟面试提供反馈,并指出需要改进的地方。这为那些无法负担这种服务的人创造了巨大的机会差距。
随着新型 LLM 技术的出现,问题在于 AI 是否在平衡竞争的条件,是否可以作为练习模拟面试的导师。首先,让我们深入探讨一下利用 AI 的错误方式。
错误的方式
首先,让我们试着解决这个有效的问题。在面试中,你是否应该使用 LLM 服务(可以是 ChatGPT、Bard——随便你)来尝试获得技术问题的答案?技术是可行的,但真正的问题是你作为候选人是否真的应该这样使用?
在我看来,答案是不。这样做会在短期和长期内伤害到你。让我们来看一下不这样做的一些原因:
你会被抓住
面试很有压力,大多数情况下,面试官会察觉到你在分屏操作、试图搜索答案并说出你不完全理解的内容。
我在我的职业生涯中进行过超过 1000 次面试,在一些情况下,候选人试图进行分屏操作,同时在与我交谈时寻找答案。这一点非常明显。
我怎么知道他们在寻找答案?他们不会注意我的暗示,会突然停顿,然后从不同的角度开始说话。
面试非常有压力。额外的压力和担心自己会被抓住会显著降低你的智商。
最可能的结果是你会失败面试。我见过几次这种情况。你可能非常聪明且勤奋,但一旦作弊,你就不会成为团队中的文化补充——任何团队。
面对面做不到的事
在 COVID 之前,大多数面试都是面对面的。大科技公司会为现场面试安排航班。随着 COVID 的出现,这一点有所变化,但现在随着返回办公室,越来越多的面试将会面对面进行。在这一点上,如果你被邀请到现场,使用 LLMs 在面试中解决问题几乎是不可能的。
游戏的错误角度
即使你成功获得了工作,你在新职位上很可能会遇到困难。你可以试图说服自己,只要进入岗位,就能在过程中解决一切,但你会给自己带来很多压力,因为你心里知道你在面试中作弊了。
这就像是以谎言开始一段关系——这很可能不会有好的结局。
更好地利用 AI 的方法
看起来 LLMs 在聊天互动方面非常擅长,这正是名字中的意思:大型语言模型。你很可能已经尝试过某个版本的 ChatGPT 或 Bard。如果没有,我鼓励你尝试一下。
从到目前为止大家做的实验来看,LLMs 在正确的提示方式下可能会成为非常好的导师。辅导和获得专业公司提供的服务来指导或教你学习技能是相当昂贵的,但现在借助 AI,事情可能会变得免费。
现在你可以使用 LLMs、今天的 ChatGPT 和 Bards 来完善你的技能,提升面试表现。
来自 AI 行业的一些技巧和窍门
在 LLMs 的帮助下,出现了一种新的“学科”,即所谓的提示工程,这是“明确你要求你的 AI LLM 模型提供什么”的聪明名称,以便你获得所需的答案。
目前在行业中关于提示工程有一些陷阱,你可以利用这些陷阱来获得更好的结果。是的,AI 确实会出现幻觉,这是一种巧妙的说法,指的是偏离正轨并给出错误的答案。在这里,良好的提示工程能够减少幻觉的可能性。我会尝试引用不同的文献,以便你看到我并不是在凭空捏造这些内容,你也可以深入研究,否则,你可以直接使用这些提示。
上下文,上下文,上下文
你可能听到过无数次,但上下文很重要。LLMs 也是如此。你为 LLM 引擎设置的上下文越好,你获得的答案就会越好。
逐步方法更好
同样适用于面试。面试官希望看到你为达到特定结果所采取的步骤。如果你在提示中要求提供逐步答案,你会更有可能得到不出现幻觉的模型,并获得更准确的结果。
这在行业中也被称为[思维链推理](https://ai.googleblog.com/2022/05/language-models-perform-reasoning-via.html)(零样本 CoT)。你可以通过在提示的末尾附加一个“让我们一步一步思考”来实现这一点。
LLMs 擅长自我检查
出于某种原因,即使 LLM 出现幻觉并且你询问模型根据你的初始问题答案是否正确,它通常也会知道答案错误并给出正确答案。如果它们擅长检查自己的答案,它们也擅长检查你的答案。
LLMs 不擅长数学
LLMs在基本算术上表现不佳因此尽量避免这些主题。
除非你想通过微积分和基本算术的面试,否则这应该没问题。
但这些技术细节说够了,让我们看看如何将其付诸实践并融入一些之前的行业经验。随意实验你的文本提示。
模拟面试脚本和应用提示技巧
让我们看看如何将你的 LLM 聊天伙伴转变为面试官,带你进行模拟演示。下面是模拟面试过程中主要部分的详细介绍。
闲聊
这是为了让候选人放松并描述过程。我们在这里不需要任何提示,或者你可以和你的 LLM 进行关于天气的友好聊天,随你选择。
目前没有提示。
向候选人展示问题
在这里我们可以使用以下提示来生成一些可能的 Java 面试问题:
LLM prompt: “You are an interviewer with a FAANG tech company.
What is a list of algorithmic problems to choose from in order to assess if a candidate is a good technical fit.
The candidate’s preferred language is Java.”
ChatGPT 示例:

作者制作的图片
不要忘记,提供背景信息是有帮助的。通常,问题分为算法或系统设计阶段,你可以通过改变提示中的术语来选择训练哪种类型。
现在你可以选择你的问题,假设我们选择 回文算法问题。
LLM prompt: “Please present the palindrome problem to the candidate.”
ChatGPT 示例:

作者制作的图片
与候选人一起讨论解决方案,就像在面试中一样
在这里你需要打开一个 Google 文档(在远程面试中,你很可能会有一个与面试官共享的书写解决方案的介质)。你可以开始起草你的解决方案和代码,并大声思考,就像在面试中一样。
如果你需要更多提示,可以请求它们
LLM prompt: "Can you give additional hints and examples without giving the solution?"
时间到
当时间到时,评估解决方案并给予候选人宝贵的反馈。完成你认为有效的解决方案后,检查它是否有效。在下面的提示之后,将代码复制并粘贴到 LLM 提示中。不要忘记应用之前的技巧并请求逐步分析。
LLM prompt: “"Is this a good solution for the Palindrome problem that the candidate has provided? Let's go through it step by step."
我们有以下问题的解决方案,其中我们 故意添加了 2 个错误 来看看引擎是否会发现它们:
-
将正确的 length(cleanedInput) -1 改为 length(cleanedInput) -2
-
将正确的 right — 改为 right ++
public class Palindrome {
public static boolean isPalindrome(String input) {
String cleanedInput = removeSpacesAndPunctuation(input);
cleanedInput = convertToLowercase(cleanedInput);
int left = 0;
int right = cleanedInput.length() - 2;
while (left < right) {
if (cleanedInput.charAt(left) != cleanedInput.charAt(right)) {
return false; // Characters don't match, not a palindrome
}
left++;
right++;
}
return true; // All characters matched, it's a palindrome
}
public static String removeSpacesAndPunctuation(String input) {
StringBuilder cleanedInput = new StringBuilder();
for (char c : input.toCharArray()) {
if (Character.isLetterOrDigit(c)) {
cleanedInput.append(c);
}
}
return cleanedInput.toString();
}
public static String convertToLowercase(String input) {
return input.toLowerCase();
}
public static void main(String[] args) {
String input = "A man, a plan, a canal: Panama";
System.out.println(isPalindrome(input)); // Output: true
}
}
输出:

作者制作的图片
很高兴我们还得到了一些关于我们犯的错误的指点。
记住,减少 LLM 幻觉机会的一种方法是利用逐步分析方法或 CoT 通过请求逐步分析。
从回文示例中学习后,你可以继续研究另一个问题。
你还可以让 LLM 检查之前提供的答案,以确保它是正确的,并逐步进行分析。
LLM prompt: Are you sure that you have provided a correct assessment? Let’s go through it step by step.
LLMs 和模拟面试的局限性
即使应用了展示的技巧和窍门,LLMs 仍可能会产生幻觉并认为错误的解决方案看起来不错。最好的办法是进行事实核查。将代码运行在模拟器中,以查看它是否输出正确的答案。
使用 LLMs 的一种方法是提出面试中会问到的问题。作为一个很好的起点,你可以使用 LLM 模型解决问题,并通过运行实际代码或在线搜索解决方案来进行事实核查。
可能有帮助的工具
如果你不想自己实验 LLM 聊天引擎,你可以利用现有的 语音和聊天工具,这些工具可以让你在面试中更自如。
Google 提供了一些很酷的面试准备工具:interview-warmup
使用 AI 进行学习,而不是“游戏”
面试的目的在于评估你是否具备必要的技能以及是否与公司文化相匹配。操控系统不会让你与许多公司的文化契合,也很可能不会帮你获得工作。
你可以利用人工智能获得竞争优势,通过更快、更聪明地学习以及准备面试。这样你将提升你的价值,并能够通过任何面试,获得你想要的职位。
AI 能解决你的问题吗?
识别 AI 适用项目想法的三个简单启发式方法
·
关注 发表在 Towards Data Science · 6 分钟阅读 · 2023 年 11 月 27 日
--
图片由 TheDigitalArtist 提供,来源于 pixabay。
在一个旨在将 AI 能力融入其产品和服务的产品组织中,总是面临着让非 AI 领域专家也能参与到 AI 进程中的挑战。虽然不是每个人都需要成为 AI 专家,但必须尽可能多地让人们贡献想法,并探索利用 AI 的力量将公司推向更高水平。这一点尤其适用于领域专家和产品人员,他们对其产品和服务试图解决的问题了如指掌,并且知道问题的关键所在。
我学到的一个普遍存在的挑战是基本问题“我们可以用 AI 解决哪些问题?”。当非专家提出这个问题时,答案竟然很难给出。因此,我设计了三个启发式问题,你可以在面对一个问题时使用这些问题,思考“这个问题可以用 AI 解决吗?”。如果你能对这三个问题都回答“是”,你可能会发现自己有机会启动一个 AI 项目。
问题 1:你能说出来吗?
你可以把 AI 想象成一个回答问题的预言者。你需要问自己的是:
你能用文字表达你希望得到解答的问题吗?
这当然是一个适用于任何你想做的事情的测试。如果你想做某件事,但无法明确表达你想做的是什么,你可能并不真正知道自己想要什么。启动一个 AI 项目也不例外。
向 AI 提问的示例问题可能包括:
-
这张图片中有一只狗吗?
-
明天的天气会是什么?
-
下周的乐透号码是什么?
这些都是可以提问的良好问题。但并不是所有的问题都能得到回答,因此我们需要另一个测试。
问题 2:它存在吗?
我们可以把预言者看作一个将问题映射到答案的函数:

预言者函数将问题映射到答案。
左侧的圆圈包含所有问题,而右侧的圆圈包含所有答案。预言者是将问题发送给答案的函数。接下来要问自己的是:
这个函数存在吗?
这可能看起来很奇怪,而且问题还会变得更加奇怪:你应该从形而上学的层面来问这个问题——理论上这个函数是否有可能存在?让我们举几个例子:

可能的预言者函数及其存在。
我们都见过 AI 回答“图片中的狗”这个问题,所以我们知道这个函数是存在的。我们也见过天气预报,所以我们知道在某种程度上可以预测明天的天气。但没有办法预测下周的乐透号码。原因在于,乐透被操控的目的正是为了使这个函数不存在。这是不可能的。这就是我所说的“在形而上学层面上”。
为什么这很重要?因为机器学习(这就是我们如何制作 AI)是通过从示例中学习来尝试近似函数。

描绘的预言者函数以及它基于 AI 的近似。
如果我们有很多关于函数(即预言者)应如何行为的示例,我们可以尝试学习这种行为,并尽可能地模拟它。但你只能近似一个存在的函数。
诚然,这些都比较抽象,因此我建议将这个启发式方法替换为以下的元启发式方法:
一个知识丰富的人能做这件事吗?
从形而上学的角度来看,假设拥有世界上所有的信息和无限的时间,人类能否回答这个问题?显然,人类在识别图片中的狗方面做得相当好。人类确实开发了天气预报,并且也进行天气预报。但是,我们无法预测下周的乐透号码。
如果你已经走到这一步,回答了两次“是”,那么你有 1)一个明确的问题,以及 2)你知道至少在理论上,这个问题是可以回答的。但还有一个问题需要解决:
问题 3:背景信息是否可用?
这个问题稍微有点技术性。关键在于,预言者函数通常需要比单纯的问题更多的信息来找到答案。有知识的人在担任预言者时,可能需要额外的信息来做出决策或提供答案。这就是我所说的背景信息。

预言者函数和背景信息一起工作。背景信息通常包含超出问题本身的信息。
例如,天气预报的预言者需要知道当前的气象条件以及几天前的条件才能做出预测。这些信息不包含在短语“明天的天气会是什么?”中。
另一方面,在狗和猫的图片中,背景信息就在图片中,不需要额外的背景信息。
之所以这很重要,是因为当我们训练 AI 时,AI 会被问到如下类型的问题:

AI 训练问题。图片由 brgfx 提供,来源于 Freepik
然后,AI 会在收到真实答案之前做出猜测,并希望随着时间的推移,AI 能够学会区分猫和狗。但要实现这一点,差异必须是可用的,以便 AI 能学会识别这些差异。在图片的情况下,这很直接——你只需要确保图片的质量足够高,以使区分成为可能。在天气预报的情况下,变得更复杂——你实际上需要做出明智的决定,以确定进行天气预测所需的信息。这是一个最好由领域专家回答的问题,因此你可能需要寻求帮助,以获得一个好的答案。
但关键是:如果没有足够的信息供有知识的人回答这个问题,那么 AI 学会如何回答这个问题的希望也很小。你需要这些背景信息。
结论
总结一下,如果你希望测试你的 AI 项目想法,以确定这是否是一个可以通过 AI 解决的问题,你可以尝试回答以下三个问题:
1. 你能把你的问题用文字表达出来吗?
2. 有知识的人能完成这项工作吗?
3. 背景信息是否可用?
如果你能对这三个问题都回答“是”,那么你已经准备好继续前进了。可能仍然会有障碍需要克服,也许最终会发现太困难了。但那是另一个帖子的主题。
祝好运!
诚挚的问候
丹尼尔·巴克伦德
LLM 能否取代金融科技经理?开发无 GPU AI 工具进行企业分析的综合指南
实践教程
开发你自己的零成本 LLM 包装器,以在本地解锁企业上下文
·发表于Towards Data Science ·9 分钟阅读·2023 年 12 月 20 日
--
“在孤独中,心灵获得力量,学会依赖自己” | 劳伦斯·斯特恩

图片由Daniel Eliashevskyi提供,来源于Unsplash
过去不到一年,GPT 星尘 ✨几乎涵盖了全球的各个领域。越来越多的专家,无论来自哪个领域,都渴望利用大型语言模型(LLM)来优化他们的工作流程。显然,企业界也不能缺席这一新趋势的探索。未来承诺着前所未有的可能性,但这些都伴随着适当的…成本。
本项目的范围是演示如何利用 LLM 的端到端解决方案,以减轻隐私和成本问题。我们将使用LLMWare,一个用于工业级企业 LLM 应用开发的开源框架,检索增强生成(RAG)方法[1],以及BLING——一组新推出的开源小模型,完全依赖 CPU 运行。
概念
在成功预测 Jrue Holiday 的 🏀 转会 到密尔沃基雄鹿后,Data Corp 开始了一个新项目:协助一家金融科技中小企业优化其决策过程。也就是说,构建一个工具来处理数百万份专有文档,查询先进的 GPT 类模型,并为经理提供简洁、优化的信息。这一切很好,但有两个主要陷阱:
-
安全性: 查询商业 LLM 模型(即 GPT-4)本质上意味着通过互联网共享专有信息(那那些数百万份文档怎么办?)。数据泄露无疑会损害公司的完整性。
-
成本: 像上面提到的自动化工具将提高经理们的生产力,但天下没有免费的午餐。预计的每日查询可能达到数百次,考虑到‘GPU-饥渴’的 LLM,累积的成本可能很容易失控。
上述限制促使我选择了一个棘手的替代方案:
如何开发一个能够处理专有知识并利用 LLM 模型的定制工具,但能够在本地(内部部署)运行,几乎不花费成本?
为了更好地传达结果,做出了几个假设:
1: 公司专注于资产管理子领域,因此我们将查询相关话题:例如资产criticality。
2: 为了简化起见,我们将使用少量文档(3)来代表公司的专有来源。doc_1 部分描述了术语“criticality”,doc_2 包含了“critical”的词条,但意义不相关,doc_3 完全不相关。
为了完成任务,我们必须提取有关主题术语“credibility”的最佳上下文。然后,为了验证,我们将直接将其与 OpenAI 的 ChatGPT 的相应答案进行比较。
操作模式
-
熟悉关键概念,如 RAG 和 BLING 模型的应用。
-
环境设置和测试以运行代码。
-
工具开发,包括向量数据库初始化、嵌入模型选择、针对有效 RAG 的语义查询。
-
基准测试结果与 OpenAI 模型;与 GPT-3.5-turbo 输出进行比较。
1. 关键概念
在深入实施之前,熟悉基础知识是必要的。
嵌入
原始文档的文本必须转化为向量表示,这对执行相关性搜索至关重要。简而言之,这种元素使得机器学习模型能够找到它们之间的相似性,从而更好地理解原始数据(即单词)之间的关系。这种转换是通过使用嵌入模型[2]来完成的。
RAG
检索增强生成(RAG)是自然语言处理(NLP)领域的一个综合方法,以双重方式运作:
-
检索相关文档中的信息
-
生成基于这些信息的响应。
其实现涉及到用户希望从基础模型之外检索数据的情况,然后将其添加到上下文中以增强他们的提示[3]。

工作流图片由作者提供
如上所示,用户查询触发了知识库的相关上下文检索——这是 RAG 模型的工作。然后,这些上下文增强了原始提示,现已增强的提示则输入到基础 LLM 模型中。
BLING 模型
这完全是一个便捷的开源小型模型(1B-3B 参数),经过优化以实现 RAG(检索增强生成)实施,旨在运行在基于 CPU 的基础设施上。它们由 HuggingFace 开发,针对知识密集型行业(即金融、法律、监管等),其实现涉及必须严格保护的敏感数据——这实际上是我们的企业案例!
现在我们了解了工具背后的基本技术细节,让我们开始编程吧!
2. 环境设置
在这一部分,我们将建立运行代码所需的环境。在命令行界面(CLI)中,只需运行以下代码片段:
-
安装transformers —— 一个开源的预训练模型工具包,能够在多个模态上执行任务。它提供了快速下载和使用模型的 API。
-
安装llmware框架 —— 一个企业级基于 LLM 的开发框架,提供工具和微调模型,包括检索库。
install.py
docker_compose.bash
- 导入库
import.py
为了确认设置,你可以按照LLMWare的快速入门指南,或选择直接运行以下代码(现在,让它完全运行 - 我们稍后会解释一切):
test_query.py
输出,包括查询结果,表明你已经成功设置了机器。让我们继续下一部分吧!
3. 工具开发
ℹ️ For your reference ℹ️
This code has been executed on a MacBook Pro 2,3 GHz Quad-Core i5
with 8BG RAM, which is anything but powerful for our days, yet managed
to perform well.
现在,让我们进入有趣的部分——动手实践吧!
步骤 1 - 向量数据库
首先,我们必须创建一个数据库来存储我们的企业文本。这些信息在多维空间中表示为向量(嵌入),能够存储它们的数据库属于向量数据库的范畴[4]。
Milvus 是我们将要使用的开源解决方案,它将帮助我们对公司的文档执行语义查询。我们只需创建一个文件夹并将这些文档移动到那里。然后只需复制以下代码片段中的 samples_path:
vector_db.py
第二步 - 嵌入模型
正如预期的那样,嵌入将被定位……由于我们将工具应用于资产管理领域,我们可以选择一个相关的嵌入模型。希望 HuggingFace 提供了一个专门构建的模型,名为:[industry-bert-asset-management](https://huggingface.co/llmware/industry-bert-asset-management-v0.1)。这是一个基于 BERT 的行业领域 Transformer,专为资产管理行业中的嵌入设计。
embeddings.py
第三步 - 语义查询
接下来,我们构造我们的语义查询,并将其传递通过向量数据库,要求返回 2 个结果。为了简化起见(参见假设 #1),要执行的查询是:“什么被定义为关键性?”
semantic_search.py
第四步 - BLING 模型
获取源数据后,我们将把它传递给选择的 BLING 模型。
首先,我们设置一个变量 embedded_text 来存储从 query_res 列表中的项拼接得到的最终文本。接下来,我们从 LLMWare 实例化一个 Prompt 对象(prompter),以克服严格的提示结构。然后,我们检查所有相关的 HuggingFace models 以实现最佳性能。
rag.py
为了更清楚,我在此描述了一个精简版本的输出,包括模型-回答对:
> Loading Model: llmware/bling-1b-0.1...
LLM Response: The product of the consequence of failure and likelihood of failure ratings provides the overall
criticality score for a given asset. The higher the score, the greater risk
> Loading Model: llmware/bling-1.4b-0.1...
LLM Response: The product of the consequence of failure and likelihood of failure ratings provides assertions
for the overall criticality score.
> Loading Model: llmware/bling-falcon-1b-0.1...
LLM Response: Criticality scores are the product of the consequence of failure and likelihood of failure ratings.
Criticality scores provide an informed prioritization process that not only identifies the highest risk assets,
but also allows for the comparison of risk reduction options.
> Loading Model: llmware/bling-cerebras-1.3b-0.1...
LLM Response: Criticality score (risk score) for a given asset.
> Loading Model: llmware/bling-sheared-llama-1.3b-0.1...
LLM Response: Criticality score
> Loading Model: llmware/bling-sheared-llama-2.7b-0.1...
LLM Response: Criticality is the product of consequence of failure and likelihood of failure ratings, which provides the
overall risk score for a given asset.
> Loading Model: llmware/bling-red-pajamas-3b-0.1...
LLM Response: The product of the consequence of failure and likelihood of failure ratings provides the overall criticality
score for a given asset.
注意:由于模型的随机性质,您的结果可能会有所不同。
4. 验证
起初,明智的做法是展示涉及的文档对“关键性”术语的说明。根据假设 #2,只有 doc_1 是相关的,并且说……
Criticality is the measure of risk associated with an asset.
Knowing which assets are more critical than others can aid in determining:
- how to prioritize the spending of limited funds;
- where to deploy limited personnel resources;
- how to manage an individual asset or collection
- capital improvement planning decisions.
To identify which assets are critical, two questions are important:
- How likely is the asset to fail (likelihood or probability of failure)?
- What are the consequences if the asset does fail (consequence or impact of failure)?
CRITICALITY SCORE
The product of the consequence of failure and likelihood of failure ratings provides
the overall criticality score (risk score) for a given asset. The higher the score, the greater risk.
鉴于此,很容易得出结论,除了 *llmware/bling-falcon-1b-0.1*,其他模型的表现均不尽如人意,该模型包含了数学评估(即产品)和优先级语义:
> Loading Model: llmware/bling-falcon-1b-0.1...
LLM Response: Criticality scores are the product of the consequence of failure and likelihood of failure ratings.
Criticality scores provide an informed prioritization process that not only identifies the highest risk assets,
but also allows for the comparison of risk reduction options.
与 GPT-3.5-turbo 的基准测试
在设置好我们的 OpenAI 密钥后,我们可以使用任何所需的模型(在我们的例子中是 GPT-3.5)和添加 query_results 的 prompter。这样,我们就可以准备好用源和查询字符串对 LLM 进行查询。
validate_gpt.py
In the context of an asset management, criticality is defined by the
importance of an asset to the organization's operations, financial well-being,
and strategic objectives. It considers the asset's impact on production,
revenue generation, regulatory compliance, and overall business continuity.
Critical assets are prioritized for strategic planning, resource allocation,
and risk mitigation efforts.
答案非常好,与我们工具的输出相比,推导结果令人非常满意。特别是,尽管我们的实现缺少了对资产重要性和资源的简要参考,但它成功地包含了数学评估(也就是产品)和优先级语义,这些都源于专有文档。换句话说,这意味着我们那只有 1B 参数、在相当普通的笔记本电脑上本地运行的简陋模型,竟然能够与一个 20B 参数的原始 LLM 进行竞争!
结论
再一次,这是一次愉快的旅程……从零成本工具开始,利用最先进的开源框架如 LLMWare,我们轻松开发了一个隐私优先的 AI 工具用于上下文分析。
尽管我们使用了甚至是1/20 GPT-3.5-turbo 模型的小型 LLM,且完全不使用 GPU,但输出结果还是非常出色!我们敢于声称,我们的工具成功地从公司文档中提取了最重要的信息,并“有意识地”将其与 LLM 的响应相结合。
但最重要的是,这次尝试为克服与 GPU 相关的成本和隐私问题奠定了基础,尤其是在处理第三方商业 LLM 解决方案时。如果有任何事情是理所当然的,那就是企业——即使是小企业——可以从类似的实现中获得显著的好处。通过在本地运行,可能还使用专有 GPU 以获得额外的提升,它们可以优化其运营并赶上这一新的 LLM 热潮。

图片由Andy Holmes提供,刊登在Unsplash上
感谢阅读,祝您圣诞快乐!如果有任何问题,请随时在下面留言或通过𝕏 / LinkedIn与我联系。无论如何……
享受假期,克隆repo,并招聘下一个…… #LLM 😉
参考文献
[2] www.cloudflare.com/en-gb/learning/ai/what-are-embeddings/
[3] Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks, P. Lewis et al, 2020, arXiv:2005.11401
[4] www.pinecone.io/learn/vector-database/
Chat GPT 能下国际象棋吗?
原文:
towardsdatascience.com/can-chat-gpt-play-chess-4c44210d43e4
使用多策略 AI 和深度强化学习击败 GPT3.5 的国际象棋比赛
·发表于 Towards Data Science ·阅读时间 5 分钟·2023 年 12 月 18 日
--

作者提供的图片:使用 DALLE-3 生成的机器人下国际象棋
2021 年,我开发了一个人工智能模型,它使用多种策略和深度强化学习,旨在成为一个真实且专业的国际象棋玩家,我将该 AI 机器人与许多不同的机器人进行了对抗。你可以在下面的文章中获得更多关于这段旅程的细节:
使用机器学习创建国际象棋冠军
towardsdatascience.com
2023 年,我使用相同的人工智能来测试 ChatGPT 是否可以下国际象棋,以及它在真实比赛中的表现如何。
在开始之前,首先要问 ChatGPT 是否可以下国际象棋。ChatGPT 说它可以下棋,但不是一个完美的玩家(这是否是它输掉比赛的借口?)。

ChatGPT 的回答 — 作者提供的图片
好的,我的 AI 正在使用白方,让我们开始以经典的 e4 开局!

ChatGPT 的回答 — 作者提供的图片
在开局的几步中,我们可以看到 ChatGPT 理解开局,并对每一步给出逻辑分析。目前比赛进展均衡。


ChatGPT 的回答和国际象棋棋步 — 作者提供的图片
在 8 步之后,我的 AI 模型在 castling 后犯了一个错误,它将兵移动到 h3 后,选择了将主教移动到 f4,而不是用骑士占据 d4,这在这个位置上是最佳的移动。
这个错误给了 ChatGPT 巨大的优势,但是……

棋步 — 图片作者提供
ChatGPT 进行了一些无意义的分析,声称我的 AI 模型将其骑士固定在了女王上,并决定将一个兵移动到 a6,而不是吃掉 c3 上的骑士,而这才是这个位置的最佳移动。
看起来 ChatGPT 无法跟踪棋盘及其所有棋子,它将这场比赛与训练数据集中其他比赛混淆了。

ChatGPT 的回答 — 图片作者提供
ChatGPT 没有把握住这个机会,我的 AI 决定走 Qe2,然后 ChatGPT 进行了另一个无意义的分析,称我将皇后移动以连接我的车并支持 d4,但我甚至没有 d4 上的兵。这个奇怪的分析证实了它没有跟踪任何棋盘位置。


ChatGPT 的回答和棋步 — 图片作者提供
即使有一些错误,ChatGPT 仍然有优势,我曾一度认为我的 AI 会输掉这场比赛。
然后……ChatGPT 决定走 Ba6,这是一个错误,导致整场比赛失利,并将优势转回到我的 AI。

棋步 — 图片作者提供
经过一些无意义的分析后,ChatGPT 完全丧失了对棋盘位置的跟踪,并进入了一个非法移动的无限循环。以下是第一次非法移动的演示:


ChatGPT 的回答和棋步 — 图片作者提供
进入这个无限循环后,我无法再继续下棋,因为 ChatGPT 在第 15 步后无法找到任何合法的走法,游戏最终以 ChatGPT 认输结束。

ChatGPT 的回答 — 图片作者提供
ChessAI 获胜!因为它比 ChatGPT 更清楚自己在做什么!

棋步 — 图片作者提供
结论
总结,ChatGPT 3.5 无法进行棋局对弈,这个大型语言模型无法跟踪棋盘并检查合法/非法的走法。ChatGPT 使用训练数据集来推断前一个对局中的下一步,但它混淆了这些对局,并没有分析实际的棋盘位置以及如何下这一位置。我们可以假设 ChatGPT 没有足够的游戏知识来做出合理的决定,即使它在开始时声称它已接受了棋规、信息和策略的训练。
国际象棋是一个具有高状态可能性和强规则的顺序决策问题,而 GPT 3.5 尚未能学会这些规则,RL 模型或搜索模型在这种类型的问题上仍然表现更好。目前我的 ChessAI 模型,一个国际象棋专家模型,仍然表现更好!
在下一篇文章中,我将与相同的 AI 进行对局,但这次对战的是 GPT 4.0。GPT 4.0 比 3.5 更强吗?它能进行完整的对局吗?让我们在下一篇文章中揭晓!
如果你想评估整场比赛:
FEN: r1bqkbnr/pp2pppp/2np4/2p5/2B1P3/5N2/PPPP1PPP/RNBQK2R w KQkq — 0 4
PGN: 1. e4 c5 2. Bc4 Nc6 3. Nf3 d6 4. Nc3 Nf6 5. O-O e6 6. h3 Be7 7. d4 cxd4 8. Bf4 a6 9. Qe2 b5 10. Nxb5 axb5 11. Bxb5 Ba6 12. Bxa6 Rxa6 13. Qxa6 Qb6 14. Qd3 Qb7 15. b4 Ne5
任何问题或建议可以通过 LinkedIn 联系我:www.linkedin.com/in/octavio-b-santiago/
ChatGPT 能与领域特定情感分析机器学习模型竞争吗?
一个使用 ChatGPT 与领域特定模型的动手比较
·
关注 发表在 Towards Data Science ·15 分钟阅读·2023 年 4 月 25 日
--
照片由 K. Mitch Hodge 提供,来源于 Unsplash
ChatGPT 是一个 GPT (生成式 预训练 变换器) 机器学习(ML)工具,令世界惊叹。它令人惊叹的能力让普通用户、专业人士、研究人员甚至其自身的创造者 都感到印象深刻。此外,它作为一个为通用任务训练的机器学习模型,在特定领域情况下表现出色,这一点令人印象深刻。作为一名研究人员,我对它进行情感分析(SA)的能力很感兴趣。
SA 是一种非常广泛的自然语言处理(NLP)技术。它有多个应用,因此可以用于多个领域(例如,金融、娱乐、心理学)。然而,一些领域采用特定的术语和行话(例如,金融)。因此,通用领域的机器学习模型是否可以与特定领域的 模型一样强大,仍然是一个NLP 中的开放研究问题。
如果你问 ChatGPT 这个研究问题——也就是本文的标题——它会给你一个谦逊的回答(继续试试吧)。但是,哦,我亲爱的读者,我通常不会给你剧透,但你完全无法想象这个 ChatGPT 回答有多么出人意料地谦虚……
作为 AI 研究员、行业专家和爱好者,我习惯于对通用领域的 NLP 机器学习工具(例如,GloVe)进行微调,以用于特定领域的任务。这是因为大多数领域很少能找到一个开箱即用的解决方案,能够在没有一些微调的情况下表现得足够好。我将展示如何不再是这种情况。
在这篇文章中,我通过讨论以下主题来比较 ChatGPT 与一个特定领域的机器学习模型:
-
SemEval 2017 任务 5 —— 一个特定领域的挑战
-
使用 ChatGPT API 给数据集打标签的代码示例
-
比较的裁决和结果及其可重复性细节
-
结论与结果讨论
-
额外内容:如何在实际场景中进行这种比较
注释 1:这只是一个简单的实践实验,稍微揭示了这个主题, 而非 详尽的科学调查。
注释 2:除非另有说明,所有图片均由作者提供。
1. SemEval 2017 任务 5 —— 一个特定领域的挑战
SemEval (语义评估) 是一个著名的 NLP 研讨会,研究团队在情感分析、文本相似性和问答任务中进行科学竞争。组织者提供由标注员(领域专家)和语言学家创建的文本数据和金标准数据集,以评估每项任务的最先进解决方案。
特别是,SemEval 的2017 年任务 5要求研究人员对金融微博和新闻标题进行情感分析,评分范围从 -1(最负面)到 1(最正面)。我们将使用那一年 SemEval 的金标准数据集来测试 ChatGPT 在特定领域任务中的表现。子任务 2 数据集(新闻标题)包含两个句子集(每个最多 30 个单词):训练集(1,142 个句子)和测试集(491 个句子)。
考虑到这些数据集,下面展示了情感评分和文本句子的分布情况。下图显示了训练集和测试集中都有双峰分布。此外,图表还显示数据集中正面句子多于负面句子。这将在评估部分提供有用的信息。

SemEval 2017 任务 5 子任务 2(新闻标题)数据分布情感评分,考虑到训练集(左 — 1,142 个句子)和测试集(右 — 491 个句子)。
对于这个子任务,获胜的研究团队(即在测试集上排名最佳的团队)将他们的机器学习架构命名为Fortia-FBK。受这一竞赛发现的启发,我和一些同事撰写了研究文章(评估基于回归的金融文本情感分析技术),在其中我们实现了 Fortia-FBK 的版本,并评估了改进该架构的方法。
此外,我们还调查了使这一架构成为获胜架构的因素。因此,我们的实现(代码在这里)将用于与 ChatGPT 进行比较。所采用的架构(CNN+GloVe+Vader)如下所示。

针对金融新闻领域的特定领域情感分析机器学习模型。架构是为研究文章“评估基于回归的金融文本情感分析技术”开发的。来源: 作者硕士论文(Lima Paiva, F.C.,《将情感分析融入强化学习以实现智能交易》)。
2. 使用 ChatGPT API 对数据集进行标注
关于使用 ChatGPT API 的讨论已在 Medium 上的数据合成中进行过。此外,你还可以在ChatGPT API 代码示例部分找到情感标注示例(请注意,使用该 API 并非免费)。在这个代码示例中,请考虑 SemEval 2017 任务的金标准数据集,你可以在这里获取。
然后,为了同时标注多个句子,使用如下代码,其中我准备了一个完整的提示,包含来自数据框的句子,黄金标准数据集中的要标注的句子以及情感所指的目标公司。
def prepare_long_prompt(df):
initial_txt = "Classify the sentiment in these sentences between brackets regarding only the company specified in double-quotes. The response should be in one line with format company name in normal case followed by upper cased sentiment category in sequence separated by a semicolon:\n\n"
prompt = "\"" + df['company'] + "\"" + " [" + df['title'] + ")]"
return initial_txt + '\n'.join(prompt.tolist())
然后,调用text-davinci-003 引擎(GPT-3 版本)的 API。在这里,我对代码进行了一些调整,以考虑提示加回答的最大字符数,总共最多 4097 个字符。
def call_chatgpt_api(prompt):
# getting the maxium amount of tokens allowed to the response, based on the
# api Max of 4097, and considering the length of the prompt text
prompt_length = len(prompt)
max_tokens = 4097 - prompt_length
# this rule of dividing by 10 is just a empirical estimation and is not a precise rule
if max_tokens < (prompt_length / 10):
raise ValueError(f'Max allowed token for response is dangerously low {max_tokens} and might not be enough, try reducing the prompt size')
response = openai.Completion.create(
model="text-davinci-003",
prompt=prompt,
temperature=0,
max_tokens=max_tokens,
top_p=1,
frequency_penalty=0,
presence_penalty=0
)
return response.choices[0]['text']
long_prompt = prepare_long_prompt(df)
call_chatgpt_api(long_prompt)
最终,对黄金标准数据集中 1633 个(训练 + 测试集)句子进行处理,你会得到以下 ChatGPT API 标签的结果。

使用 ChatGPT API 标注情感的 SemEval 2017 任务 5 子任务 2(新闻头条)黄金标准数据集示例。
2.1. ChatGPT 及其 API 在规模化时的问题
与其他 API 一样,有一些典型要求
-
请求速率限制需要进行节流调整
-
请求限制为 25000 tokens(即子词单元或字节对编码)
-
每个请求的最大长度为 4096 tokens(包括提示 + 响应)
-
成本为\(0.0200 / 1K tokens(注:我在所有操作后从未花费超过 U\) 2)
然而,这些只是处理大多数 API 时的典型要求。此外,记住在这个领域特定的问题中,每个句子都有一个目标实体(即公司)用于情感分析。因此,我不得不进行一些调整,直到设计出一个使得能够一次标注多个句子的情感并且后续处理结果变得简单的提示模式。此外,还有其他限制影响了我之前展示的提示和代码。具体来说,我发现使用这个文本 API 处理多个句子(>1000)时存在问题。
-
可重复性: ChatGPT 的情感评估在提示中做出很小的修改(例如,添加或删除句子中的逗号或句号)后可能会发生显著变化。
-
一致性: 如果你没有明确指定模式响应,ChatGPT 会变得富有创意(即使你选择了非常低的随机性参数),这使得处理结果变得困难。此外,即使你指定了模式,它也可能输出不一致的格式。
-
不匹配: 尽管它可以非常准确地识别你在句子中想要评估情感的目标实体(例如公司),但在大规模处理时,它可能会混淆结果。例如,假设你传递了 10 个包含目标公司的句子。如果这些公司出现在其他句子中或重复出现,ChatGPT 可能会混淆目标和句子情感,改变情感标签的顺序或提供少于 10 个标签。
-
偏见: 目前,ChatGPT 偏见问题是众所周知的。而且有改进这个问题的想法。然而,在那之前,请注意你正在学习使用一个有偏见的 API。
所有这些问题都意味着需要一个学习曲线来正确使用(有偏见的)API。它需要一些微调才能得到我需要的结果。有时我不得不进行许多尝试,直到以最小的一致性达到了期望的结果。
在理想情况下,你应该一次发送尽可能多的句子,原因有二。首先,你希望尽快获得标签。其次,提示作为令牌计算成本,因此请求越少,成本越低。然而,每次请求有 4096 个令牌的限制。此外,考虑到我提到的问题,另一个显著的 API 限制存在。一次传递太多句子增加了不匹配和不一致的机会。因此,你需要不断增加和减少句子的数量,直到找到一致性和成本的最佳平衡点。如果不这样做,你会在后处理结果阶段遭遇问题。
总之,如果你需要处理成千上万的句子,先从几句开始,并且最多用 10 个提示来检查响应的可靠性。然后,逐渐增加数量以验证容量和质量,直到找到适合你的任务的最佳提示和比例。
3. 判决和比较结果
3.1. 比较的细节
在其 GPT-3 版本中,ChatGPT 不能使用数值为文本句子赋予情感(无论我尝试了多少次)。然而,专家在这个特定的黄金标准数据集中为句子情感赋予了数值评分。
因此,为了进行有效的比较,我必须:
-
将数据集评分分类为 正面、中性 或 负面 标签。
-
对领域特定的机器学习模型生成的评分做相同的操作。
-
定义可能的阈值范围(步长为 0.001),以确定一个类别的开始和结束。然后,根据阈值 TH,超过 +TH 的评分被视为 正面 情感,低于 -TH 为 负面,介于两者之间的是 中性。
-
遍历阈值范围,并在每个点评估两个模型的准确性。
-
按照集合(即训练或测试)来调查它们的表现,因为领域特定的模型在训练集上可能具有不公平的优势。
步骤 3 的代码如下。完整的比较代码在这里。
def get_df_plot(df, th_sequence):
temp_list = []
for th in th_sequence:
converted_gold_arr = np.where((df['sentiment'] <= th) & (df['sentiment'] >= -th), 0, np.sign(df['sentiment']))
converted_model_arr = np.where((df['cnn-pred-sent'] <= th) & (df['cnn-pred-sent'] >= -th), 0, np.sign(df['cnn-pred-sent']))
df['sent_cat_value'] = converted_gold_arr.astype(np.int64)
df['cnn_pred_sent_cat_value'] = converted_model_arr.astype(np.int64)
corr_gold_chatgpt = df['chatgpt_sent_value'].corr(df['sent_cat_value'])
corr_gold_cnn = df['chatgpt_sent_value'].corr(df['cnn_pred_sent_cat_value'])
acc_gold_chatgpt = (df['chatgpt_sent_value']==df['sent_cat_value']).mean()
acc_gold_cnn = (df['chatgpt_sent_value']==df['cnn_pred_sent_cat_value']).mean()
temp_list.append([th, corr_gold_chatgpt, corr_gold_cnn, acc_gold_chatgpt, acc_gold_cnn])
return pd.DataFrame(data=temp_list, columns=['th', 'corr_gold_chatgpt', 'corr_gold_cnn', 'acc_gold_chatgpt', 'acc_gold_cnn'])
th_sequence = np.arange(0, 1.000001, 0.001)
df_plot = get_df_plot(df.copy(), th_sequence)
3.2. 判决:是的,ChatGPT 不仅可以赢,而且可以超越竞争对手
最终结果显示在下面的图表中,该图表展示了当调整阈值(x 轴)时,两个模型在对数字黄金标准数据集进行分类时的准确性(y 轴)变化。此外,训练集和测试集分别位于左右两侧。

比较 ChatGPT 和领域特定机器学习模型,分别考虑训练集(左侧)和测试集(右侧)。这项评估评估了两个模型在对数字黄金标准数据集进行分类时,准确性(y 轴)如何随阈值(x 轴)的变化而变化。
首先,我必须诚实。我没想到会得到如此惊人的结果。因此,为了对 ChatGPT 公平,我复制了原始 SemEval 2017 竞赛设置,其中领域特定的机器学习模型将使用训练集进行构建。然后,实际排名和比较仅会在测试集上进行。
然而,即使在训练集中,在最有利的情况下——ChatGPT 的阈值为 0.066,而领域特定模型的阈值为 0.014——领域特定的机器学习模型的准确率最多比 ChatGPT 的最佳准确率低 2 个百分点(0.73 对 0.75)。此外,ChatGPT 在训练集和测试集中所有阈值下的准确率均优于领域特定模型。
有趣的是,两个模型的最佳阈值(0.038 和 0.037)在测试集中接近。在这个阈值下,ChatGPT 的准确率比领域特定模型高出 11 个百分点(0.66 对 0.77)。此外,ChatGPT 在阈值变化中的一致性明显优于领域特定模型。因此,可以看出 ChatGPT 的准确率下降得要少得多。
总的来说,ChatGPT 在准确性方面远远超过了领域特定的机器学习模型。另一方面,ChatGPT 还可以根据特定任务进行微调。因此,想象一下 ChatGPT 会变得多么优秀。
3.3. 调查 ChatGPT 的情感标注
我一直打算进行更深入的调查,通过分析 ChatGPT 不准确的例子,并将其与领域特定模型进行比较。然而,由于 ChatGPT 的表现远超预期,我转而只调查它未能正确判断情感的案例。
起初,我进行了一次类似的评估,但这次使用了完整的黄金标准数据集。接着,我选择了将黄金标准数值转换为正面、中性和负面标签的阈值(0.016),以获得 ChatGPT 的最佳准确率(0.75)。然后,我制作了一个混淆矩阵。图表如下。

左侧是一个线图,用于评估 ChatGPT 的准确性(y 轴)如何随阈值(x 轴)的变化而变化,以便对数字 Gold-Standard 完整数据集进行分类。右侧是正面、中性和负面标签的混淆矩阵,给出了最大 ChatGPT 性能的阈值为 0.016。此外,混淆矩阵包含了根据转换标签的 ChatGPT 的命中率和遗漏率的百分比。
请回忆一下我在前一节中展示的数据句子分布,其中正面句子的分数比负面句子的分数要高。在混淆矩阵中,观察到考虑到 0.016 的阈值,正面句子有 922 个(56.39%),负面句子有 649 个(39.69%),中性句子有 64 个(3.91%)。
此外,请注意 ChatGPT 在中性标签上的准确性较低。这是可以预期的,因为这些标签更容易受到阈值限制的影响。有趣的是,ChatGPT 倾向于将大多数这些中性句子分类为正面句子。然而,由于中性句子较少,这一现象可能与数据集中较高的正面情感分数有关。
另一方面,在考虑其他标签时,ChatGPT 显示出识别正面类别的能力比负面类别高出 6 个百分点(78.52%对 72.11%)。在这种情况下,我不确定这是否与每个评分范围内句子的数量有关。首先,因为每种类别的句子数量更多。其次,请观察 ChatGPT 将标签错误分类到相反方向(正面到负面或反之)的次数。再次,ChatGPT 在负面类别上的错误更多,而负面类别的句子数量较少。因此,ChatGPT 似乎在处理负面句子时比处理正面句子时更有困难。
3.4. 一些特定案例及与人类专家的比较
我选择了一些在 Gold-Standard(人类评分)和 ChatGPT 之间具有最明显特征的句子。然后,我使用了之前确定的相同阈值将数值评分转换为情感标签(0.016)。此外,ChatGPT 已经被报道表现优于人类。因此,我调查了这些差异,并给出了我的裁决,即人类还是 ChatGPT 更精确。
此外,自 2018 年以来,美国机构DARPA 已经深入研究了让 AI 决策具备解释性的重要性。值得注意的是,ChatGPT 具备这种能力:它可以解释其决策。这一能力帮助我做出了裁决。下表展示了这一检查结果。

表格显示了 Gold-Standard 标签(通过使用 0.016 阈值从人类专家分数转换而来)与 ChatGPT 之间存在不匹配的句子示例。此外,我给出了我认为最好的裁决。
从我支持人类专家的案例开始。在句子 3 和 4 的情况下,ChatGPT 应该意识到,获得回款和公司联盟通常被认为在金融领域是有利的。然而,在句子 7 的情况下,我要求它解释其决定,ChatGPT 的回复是:
对 Barclays 的积极情感通过“创纪录”一词传达,这意味着公司在成功解决与监管机构的法律问题上取得了显著成就。
对于这个句子,ChatGPT 没有理解到,尽管达成创纪录的交易通常是好的,但 SEC 是一个监管机构。因此,与 SEC 达成创纪录的交易意味着 Barclays 和 Credit Suisse 必须支付创纪录的罚款。
继续讨论句子 5 和 8,这些都是非常艰难的判断。让我明确,人类在评估中是正确的。然而,ChatGPT 确实无法猜到这些。在句子 5 中,需要了解当时的情况才能理解这个句子代表了一个好的结果。对于句子 8,需要知道油价下跌与特定目标公司的股票价格下跌相关。
然后,对于句子 6,最中性的句子可以获得零情感分数,ChatGPT 解释了其决定如下:
这个句子是积极的,因为它宣布了新的投资银行首席运营官的任命,对公司来说是个好消息。
然而,这个回答很普通,没有很有洞察力,也没有解释为什么 ChatGPT 认为这位特定高管的任命是好的。因此,我在这种情况下同意了人类专家的观点。
有趣的是,我在句子 1、2、9 和 10 中对 ChatGPT 的评判是有利的。此外,仔细观察,人类专家应该更多关注目标公司或整体信息。这在句子 1 中尤其明显,专家们应当认识到虽然对 Glencore 的情感是积极的,但目标公司是 Barclays,而不是撰写报告的公司。在这方面,ChatGPT 在识别情感目标和意义方面做得更好。
4. 结论和结果讨论
从下表可以看出,实现这样的表现需要大量的财务和人力资源。

比较模型的各个方面,例如参数数量、使用的词嵌入大小、成本、构建所需的研究人员数量、测试集中的最佳准确性,以及其决策是否可解释。
在这方面,尽管 ChatGPT 优于领域特定模型,但最终比较仍需要对 ChatGPT 进行领域特定任务的微调。这将有助于确定微调的性能提升是否超过了投入的成本。
此外,文本模型中最重要的因素之一是词嵌入的大小。自 SemEval 2017 版以来,这项技术已有所发展。因此,这部分的一些更新可能会显著提高领域特定模型的结果。
另一方面,随着生成文本模型和大型语言模型(LLMs)的流行,一些开源版本可以帮助组装一个有趣的未来对比。此外,像 ChatGPT 这样的 LLM 能够解释其决策是一个杰出的、可能出乎意料的成就,这可能会彻底改变这一领域。
5. 附加:如何在应用场景中进行这种对比
在不同领域的情感分析是一个独立的科学工作。然而,在合适的场景中应用情感分析的结果可能是另一个科学问题。此外,考虑到我们处理的是金融领域的句子,尝试将情感特征添加到应用智能系统中会比较方便。这正是一些研究人员一直在做的事情,我也在尝试这样做。
在 2021 年,我和一些同事发表了一篇关于如何在应用场景中使用情感分析的研究文章。在这篇文章中——在第二届 ACM 国际金融人工智能会议(ICAIF’21)上呈现——我们提出了一种将市场情绪纳入强化学习架构的高效方法。该架构的实现源代码在这里可以获得,其整体设计的一部分如下所示。

一个将市场情绪纳入应用场景的强化学习架构的示例架构部分。来源: 《智能交易系统:一种情感感知的强化学习方法》。第二届 ACM 国际金融人工智能会议(ICAIF ‘21)论文集。 Lima Paiva, F. C.; Felizardo, L. K.; Bianchi, R. A. d. C. B.; Costa, A. H. R.
该架构旨在处理像 Gold-Standard 数据集中那样的数值情感分数。然而,也有技术(例如,Bullishnex 指数)可以将由 ChatGPT 生成的类别情感转换为适当的数值。应用这种转换使得可以在这样的架构中使用 ChatGPT 标记的情感。此外,这也是在这种情况下可以做的一种示例,并且是我计划在未来分析中做的事情。
5.1. 我研究领域的其他文章(NLP,RL)
-
利马·帕伊瓦,F. C.; 弗利扎多,L. K.; 比安基,R. A. d. C. B.; 科斯塔,A. H. R. 智能交易系统:一种情感感知的强化学习方法。第二届 ACM 国际金融人工智能会议(ICAIF ‘21)论文集。
-
弗利扎多,L. K.; 利马·帕伊瓦,F. C.; 德维塔·格雷夫斯,C.; 松本,E. Y.; 科斯塔,A. H. R.; 德尔-莫拉尔-埃尔南德斯,E.; 布兰迪马特,P. 超越算法交易强化学习系统:对加密货币市场的监督方法。应用专家系统(2022 年),第 202 卷,第 117259 页。
-
弗利扎多,L. K.; 利马·帕伊瓦,F. C.; 科斯塔,A. H. R.; 德尔-莫拉尔-埃尔南德斯,E. 应用于交易系统的强化学习:一项调查. arXiv,2022 年。
使用的资源
主要参考文献
-
卡杰赫·纳西尔图西,A.,阿赫博佐尔吉,S.,英·瓦赫,T.,和吴,D. C. L. 市场预测中的文本挖掘:一项系统综述。应用专家系统(2014 年),41(16):7653–7670。
-
洛赫兰,T. 和 麦克唐纳,B. 何为负债?文本分析、词典和 10-K 报告。金融杂志(2011 年),66(1):35–65。
-
汉密尔顿,W. L.,克拉克,K.,列斯科维奇,J.,和朱拉夫斯基,D. 从未标记语料库中诱导领域特定情感词典。 2016 年自然语言处理经验方法会议论文集,页 595–605。
-
科尔蒂斯,K.; 弗雷塔斯,A.; 道德特,T.; 赫尔利曼,M.; 扎鲁克,M.; 汉施胡,S.; 戴维斯,B. SemEval-2017 任务 5:金融微博和新闻的细粒度情感分析。第 11 届语义评价国际研讨会(SemEval-2017)论文集。
-
戴维斯,B.,科尔蒂斯,K.,瓦西留,L.,孔皮斯,A.,麦克德莫特,R.,和汉施胡,S. 由 X-Scores 驱动的社交情感指数。ALLDATA,第二届国际大数据、小数据、关联数据和开放数据会议(2016 年)。
-
Ferreira, Taynan; 利马·帕伊瓦,F. C.; 西尔瓦,罗伯托·达; 保拉,安赫尔·德; 科斯塔,安娜; 库尼亚斯卡,卡洛斯。评估基于回归的金融文本情感分析技术。第 16 届国家人工智能与计算智能会议(ENIAC),2019 年。
联系方式
ChatGPT 能推荐带有机器学习的电影吗
在推荐的背景下测试 ChatGPT 的极限的有趣旅程
·
关注 发表在 Towards Data Science ·6 min read·2023 年 4 月 17 日
--
照片由Tech Daily提供,来源于Unsplash
最近我花了一些时间与我们亲爱的 AI 统治者 ChatGPT(开个玩笑!)一起探讨这个模型并推动其极限。我测试了电影推荐的用例。你可以在这里找到视频演示。
由数十亿参数驱动的单体 LLM,经过 RLHF 微调,彻底改变了我们对 AGI 的认知。ChatGPT 的崛起,GPT-3.5 和 GPT-4 证明了语言模型的能力和技能在过去几个月中扩展了多少。ChatGPT 在发布后仅两个月内达到 1 亿用户 是 AI 取得惊人进步的见证。
使用 ChatGPT 的电影推荐
许多人正在以创意方式使用 ChatGPT,从 从零开始制作 Flappy Bird 到 构建网站。跟随这一趋势,我决定查看 ChatGPT 是否可以根据数据集计算一个未见过的电影的用户评分。我首先要求 ChatGPT 生成一个数据集。

响应迅速,并生成了如上下文中解释的数据集。

d
我将会要求 ChatGPT 进行以下操作,
预测 Jack 对电影《复仇者联盟》的评分
我的希望是 ChatGPT 使用协作过滤方法来完成此任务。可以首先创建一个评分矩阵,利用评分矩阵计算与 Jack 的用户相似度。最后,

请注意,我在计算评分时忽略了对《复仇者联盟》的评分为 0 的用户。以下的 Excel 表展示了这些计算。我们正在寻找的最终答案是 9。

接下来,我提出了如下问题。

看起来 ChatGPT 认为这应该是一个数据点,当前在数据集中缺失。我还尝试了 “一步步思考” 的技巧,但这并没有让 ChatGPT 进展太远。
接下来,我尝试使用 思路链推理 来阐述计算最终结果所需遵循的方法。

成功了!这次,ChatGPT 能够按照计划执行,生成中间结果并计算最终答案。

但稍等一下!最终结果是错误的。
问题 1:由于任务复杂性,ChatGPT 可能在数学上失败了
看起来 ChatGPT 得出的最终结果是错的。如果你把最后一步第 2 行的方程式复制到计算器中,你会得到 9,而不是 8.95。此外,余弦距离也毫不意外地错误。但是,考虑到 ChatGPT 只是一个语言模型,它能做到这一点仍然令人印象深刻。让我们给予它怀疑的余地,尝试找出 ChatGPT 出错的地方。

不幸的是,ChatGPT 无法解决这个问题。这是新响应的一部分内容。

我无法让 ChatGPT 纠正错误。但它一直承认自己犯了错误,这有点矛盾。这引出了第二个问题。
问题 #2:ChatGPT 过于奉承
ChatGPT 非常过于奉承,每次你指出它错了,它都会认为自己错了。有趣的是,即使它手头上有正确的解决方案,它也会认为自己错了 😅。

在这里,[0, 10, 0, 8] 是实际向量。但是 ChatGPT 认为这是错误的,并产生了其他幻觉,以摆脱它当前的困境。这几乎像是 Bing 聊天是 ChatGPT 的邪恶兄弟。
经过一番对话后,我想测试一下 ChatGPT 的记忆/注意力跨度。所以我问,

ChatGPT 说,

哦哦!如果你回顾 ChatGPT 的第一个有意义的回应,评分矩阵已经发生了变化。这就进入了大型语言模型(LLMs)中最棘手的问题之一。
问题 #3:ChatGPT 会产生幻觉
ChatGPT 的出现激励了科学界,引发了围绕 ChatGPT 的各种理念;从提高生产力到征服世界。一个观点是 ChatGPT 作为计算机程序的范式转变。在历史上,我们所熟知和喜爱的计算机程序是一组确定性的特定指令,通过这些指令我们可以达到预期的结果。ChatGPT 就像一个计算机程序,但它使用户可以使用自然语言进行交流,而不是用语法化的指令。
然而,在计算机程序中,如果一个变量脱离了上下文,那就是一个明显的错误。但对于 LLMs,它们只是凭空想象一些东西来填补空白。这在某些情况下可能是致命的。想象一下你尝试用 ChatGPT 解决账单错误,而 ChatGPT 却产生了一个登录错误。这将是一个非常混乱的用户体验。
你可以在下面找到我冒险的录像。
新的前沿
即使 ChatGPT 存在一些问题,这也不是世界末日!我仍然对 ChatGPT 比预训练的 GPT-3 更加出色感到印象深刻。所以这些模型只会变得更好。
我们已经有了GPT-4 的发布和等待列表。技术报告已经显示出令人瞩目的性能提升。例如,在小学数学问题上,GPT-3.5 达到了 57.1%,而 GPT-4 则设定了 92% 的标准。此外,GPT-4 在事实检索能力和比 ChatGPT 更少的幻觉方面表现得更好。
如果你对从定性角度比较 GPT-4 和 ChatGPT 感兴趣,我推荐这个视频。
另一个进展是最近推出的能够使用自然语言进行推荐的模型。这个模型被称为 P5,并在与最先进的模型对比时表现出色。例如,P5 在顺序推荐上超越了 Bert4Rec 和 SASRec。
结论
ChatGPT 当然也不是没有缺陷。例如,ChatGPT 在简单的算术操作上失败,表现出谄媚行为,并在这个过程中产生了幻觉。但这只是开始。ChatGPT 的继任者 GPT-4 在许多方面都展现了显著的改进。此外,研究人员正在寻找利用自然语言的新方法来解决如推荐系统等新问题。
除非另有说明,所有图片均为作者提供
ChatGPT 能解决背包问题吗?
用 ChatGPT 解决运筹学(OR)问题
·
关注 发表在 Towards Data Science ·10 分钟阅读·2023 年 9 月 21 日
--
图片由 Jakob Owens 提供,来自 Unsplash
自从 ChatGPT 出现以来,我一直在思考 ChatGPT 将如何影响优化和运筹学(OR)领域。关于 ChatGPT 通过高中和本科水平的数学考试的新闻,引发了我对它在解决 OR 问题方面表现的兴趣。在本文中,我将使用一个经典的 OR 问题——0/1 背包问题来进行一些测试。
背包问题
在优化和决策难题的领域,背包问题长期以来一直是经典的基准问题。它挑战我们选择一组物品,每个物品都有自己的重量和价值,以最大化总价值,同时保持在给定的重量限制内。动态规划是一种我们通常用来解决 0/1 胶囊问题的有效算法。
与 ChatGPT 的对话
我从一个很小的例子开始,并要求 ChatGPT 解决它。
我:考虑一个背包问题,其中背包的容量是 10lbs。我有 4 个物品可以选择,它们的价值分别为 $10、$40、$30、$50,重量分别为 5lbs、4lbs、6lbs、3lbs。你能为我解决这个问题吗?
ChatGPT:当然!背包问题是一个经典的优化问题,目标是最大化选择物品的总价值,同时满足所选物品的总重量不超过背包的容量。
在你的情况下,你有 4 个物品及其相应的价值和重量:
项目 1:价值 = $10,重量 = 5lbs 项目 2:价值 = $40,重量 = 4lbs 项目 3:价值 = $30,重量 = 6lbs 项目 4:价值 = $50,重量 = 3lbs
背包容量 = 10lbs
要解决这个问题,你可以使用动态规划方法。创建一个二维表格,其中行表示物品,列表示剩余的背包容量(从 0lbs 到 10lbs)。通过迭代填充表格,以找到每个物品组合和剩余容量的最大值。
以下是表格填充过程:
*容量 (lbs) 0 1 2 3 4 5 6 7 8 9 10
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
项目 1 (5lbs) 0 0 0 0 0 10 10 10 10 10 10
项目 2 (4lbs) 0 0 0 0 40 40 40 40 50 50 50
项目 3 (6lbs) 0 0 0 0 40 40 30 70 70 70 70
项目 4 (3lbs) 0 0 0 50 50 50 50 70 70 70 80*
表格显示了每个物品组合和剩余容量的最大值。最终单元格(右下角)具有 $80 的值,这是在 10lbs 的背包容量下可以获得的最大值。
要找到应该选择的物品,你可以从最后一个单元格回溯到第一个单元格,同时考虑导致最大值的决策。在这种情况下,你会选择项目 2 和项目 4,它们的总值为 $40 + $50 = $90,总重量为 4lbs + 3lbs = 7lbs,这在 10lbs 的背包容量范围内。
因此,最佳解决方案是选择项目 2 和项目 4,这样你将得到总值 $90。
显然,我们注意到 ChatGPT 能够选择正确的方法来解决这个问题,但 ChatGPT 给出的最终解答与其推理过程不一致。具体来说,动态规划(DP)表显示 $80(右下角单元格)是最佳目标值,但 ChatGPT 得出的结论是 $90 是最终答案。
为了检查哪个是正确的解决方案,我编写了一段 Python 脚本来执行 DP 算法。
import numpy as np
v = [10, 40, 30, 50]
w = [5, 4, 6, 3]
c = 10
dp = np.zeros((5,11))
for i in range(1,5):
for j in range(1,11):
if j < w[i-1]:
dp[i,j] = dp[i-1,j]
else:
dp[i,j] = max(dp[i-1,j],dp[i-1,j-w[i-1]]+v[i-1])
dp[1:,1:]
打印出的 DP 表如下。

这确认了 ChatGPT 得出的结论是正确的,但 ChatGPT 给出的 DP 表是错误的。然后我继续让它纠正 DP 表。
我:我认为你的解决方案是正确的,但表格有些错误。
ChatGPT:我为任何困惑道歉。让我为你纠正表格。以下是修正后的背包问题动态规划表:
容量 (磅) 0 1 2 3 4 5 6 7 8 9 10
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
物品 1 (5 磅) 0 0 0 0 0 10 10 10 10 10 10
物品 2 (4 磅) 0 0 0 0 40 40 40 40 40 50 50
物品 3 (6 磅) 0 0 0 0 40 40 40 40 40 50 70
物品 4 (3 磅) 0 0 0 50 50 50 90 90 90 90 90*
现在,表格是正确的。右下角单元格中的值是$90,这表示在 10 磅的背包容量下可以获得的最大值。如前所述,最优解是选择物品 2 和物品 4,这将给你总价值$90。
有趣的是,ChatGPT 能够对表格进行一些修正,使右下角单元格与$90 对齐。然而,如果我们更仔细地查看表格,表格的最后一行仍然不正确。第 4 行第 7 列的单元格(对应于背包容量为 6 磅)应该是$50 而不是$90。一个容量为 6 磅的背包无法选择物品 2 和物品 4 来达到$90,因为物品 2 和物品 4 的总重量为 7 磅,超出了背包的容量。
我随后要求它编写一些代码来解决这个问题。
我:你能写一段 Python 代码来解决这个问题吗?
ChatGPT:当然可以!你可以使用动态规划在 Python 中解决 0/1 背包问题。以下是解决你提供的特定实例的 Python 代码片段:
def knapsack(values, weights, capacity):
n = len(values)
# Initialize a table to store the maximum values for different capacities
dp = [[0] * (capacity + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for w in range(capacity + 1):
# If the current item's weight is more than the current capacity, skip it
if weights[i - 1] > w:
dp[i][w] = dp[i - 1][w]
else:
# Choose the maximum between including and excluding the current item
dp[i][w] = max(dp[i - 1][w], dp[i - 1][w - weights[i - 1]] + values[i - 1])
# Backtrack to find the selected items
selected_items = []
i, w = n, capacity
while i > 0 and w > 0:
if dp[i][w] != dp[i - 1][w]:
selected_items.append(i - 1)
w -= weights[i - 1]
i -= 1
return dp[n][capacity], selected_items
# Item values and weights
values = [10, 40, 30, 50]
weights = [5, 4, 6, 3]
knapsack_capacity = 10
max_value, selected_items = knapsack(values, weights, knapsack_capacity)
print(f"Maximum value: ${max_value}")
print("Selected items:")
for i in selected_items:
print(f"Item {i + 1} (Value: ${values[i]}, Weight: {weights[i]} lbs)")
当你运行这段代码时,它将输出最大值和应放入背包的选定物品。在你的特定情况下,它会正确选择物品 2 和物品 4,这些物品的总价值为$90,总重量为 7 磅,在背包容量为 10 磅的范围内。
这段代码清晰正确地打印出了最优解。由这段代码计算出的 DP 表也正确。
然后我进一步测试了一个更大规模的实例,一个背包容量为 500 磅且有 10 个物品可供选择的背包问题。每个物品的价值和重量都是使用以下代码随机生成的。
import numpy as np
np.random.seed(0)
w = np.random.normal(10, 3, 100)
w = [int(x) if x>= 1 else 1 for x in w]
v = np.random.normal(100, 30, 100)
v = [int(x) if x>= 10 else 10 for x in v]
dp = np.zeros((101,501))
for i in range(1,101):
for j in range(1,501):
if j < w[i-1]:
dp[i,j] = dp[i-1,j]
else:
dp[i,j] = max(dp[i-1,j],dp[i-1,j-w[i-1]]+v[i-1])
dp[1:,1:]
我复制并粘贴了生成的值和重量,并要求 ChatGPT 解决它。
我:考虑一个背包问题,其中背包的容量为 500 磅。我有 100 个物品可供选择,其值由列表 [156, 59, 61, 129, 64, 158, 87, 77, 157, 144, 156, 127, 74, 157, 91, 124, 128, 95, 118, 127, 111, 67, 108, 139, 79, 95, 86, 155, 120, 112, 76, 116, 79, 100, 80, 120, 117, 93, 111, 67, 55, 113, 105, 119, 171, 128, 72, 133, 60, 86, 97, 151, 77, 75, 97, 80, 133, 67, 65, 86, 85, 157, 128, 102, 63, 125, 69, 53, 135, 109, 127, 109, 125, 80, 68, 120, 75, 79, 86, 100, 89, 58, 80, 33, 118, 51, 66, 101, 77, 146, 61, 108, 98, 64, 115, 94, 123, 124, 164, 140] 和重量由列表 [15, 11, 12, 16, 15, 7, 12, 9, 9, 11, 10, 14, 12, 10, 11, 11, 14, 9, 10, 7, 2, 11, 12, 7, 16, 5, 10, 9, 14, 14, 10, 11, 7, 4, 8, 10, 13, 13, 8, 9, 6, 5, 4, 15, 8, 8, 6, 12, 5, 9, 7, 11, 8, 6, 9, 11, 10, 10, 8, 8, 7, 8, 7, 4, 10, 8, 5, 11, 7, 10, 12, 10, 13, 6, 11, 7, 7, 8, 9, 10, 6, 12, 11, 5, 14, 15, 13, 9, 6, 13, 8, 13, 10, 12, 11, 12, 10, 15, 10, 11] 分别给出。你能为我解决这个问题吗?
ChatGPT:解决 100 个物品、容量为 500 磅的背包问题是一个计算密集型任务。然而,我可以提供给你用于解决此问题的 Python 代码,使用动态规划方法,但请注意,这可能需要一些时间来运行。
这里是使用提供的值和重量来解决背包问题的 Python 代码:
def knapsack(values, weights, capacity):
n = len(values)
dp = [[0] * (capacity + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for w in range(capacity + 1):
if weights[i - 1] > w:
dp[i][w] = dp[i - 1][w]
else:
dp[i][w] = max(dp[i - 1][w], dp[i - 1][w - weights[i - 1]] + values[i - 1])
selected_items = []
i, w = n, capacity
while i > 0 and w > 0:
if dp[i][w] != dp[i - 1][w]:
selected_items.append(i - 1)
w -= weights[i - 1]
i -= 1
return dp[n][capacity], selected_items
# Item values and weights
values = [
156, 59, 61, 129, 64, 158, 87, 77, 157, 144, 156, 127, 74, 157, 91, 124,
128, 95, 118, 127, 111, 67, 108, 139, 79, 95, 86, 155, 120, 112, 76, 116,
79, 100, 80, 120, 117, 93, 111, 67, 55, 113, 105, 119, 171, 128, 72, 133,
60, 86, 97, 151, 77, 75, 97, 80, 133, 67, 65, 86, 85, 157, 128, 102, 63,
125, 69, 53, 135, 109, 127, 109, 125, 80, 68, 120, 75, 79, 86, 100, 89, 58,
80, 33, 118, 51, 66, 101, 77, 146, 61, 108, 98, 64, 115, 94, 123, 124, 164,
140
]
weights = [
15, 11, 12, 16, 15, 7, 12, 9, 9, 11, 10, 14, 12, 10, 11, 11, 14, 9, 10, 7,
2, 11, 12, 7, 16, 5, 10, 9, 14, 14, 10, 11, 7, 4, 8, 10, 13, 13, 8, 9, 6, 5,
4, 15, 8, 8, 6, 12, 5, 9, 7, 11, 8, 6, 9, 11, 10, 10, 8, 8, 7, 8, 7, 4, 10,
8, 5, 11, 7, 10, 12, 10, 13, 6, 11, 7, 7, 8, 9, 10, 6, 12, 11, 5, 14, 15, 13,
9, 6, 13, 8, 13, 10, 12, 11, 12, 10, 15, 10, 11
]
knapsack_capacity = 500
max_value, selected_items = knapsack(values, weights, knapsack_capacity)
print(f"Maximum value: {max_value}")
print("Selected items:")
for i in selected_items:
print(f"Item {i + 1} (Value: {values[i]}, Weight: {weights[i]})")
运行这段代码将为你提供可以获得的最大值和所选物品的列表。请耐心等待,因为可能需要一些时间来执行。
这一次,它没有直接解决问题并给出答案,而是给了我一段 Python 代码来运行。代码在解决这个问题时表现很好。
结论
这是我第一次尝试理解 ChatGPT 解决复杂数学问题的能力,比如经典的 OR 问题——背包问题。对于小规模的背包问题,它似乎能够给出最佳解决方案的答案。然而,答案的质量并不总是有保障的。在推理过程中或最终答案中可能会出现错误。对于大规模实例,ChatGPT 倾向于避免直接给出最佳解,而是提供一段代码供运行。代码通常编写得很好,可以得到正确的解决方案。因此,在使用 ChatGPT 解决背包问题时,不要过于依赖它给出的最佳解直接答案,而是应运行它提供的代码进行双重检查。
ChatGPT 能写出比数据分析师更好的 SQL 吗?
原文:
towardsdatascience.com/can-chatgpt-write-better-sql-than-a-data-analyst-f079518efab2
·发表于Towards Data Science ·6 分钟阅读·2023 年 1 月 5 日
--

我尝试了ChatGPT,这是 GPT-3 语言模型的一个变体,专门设计用于在对话上下文中生成类似人类的文本。当然,就像大多数人一样,我也好奇:人工智能能做我的工作吗?它能做得比我好吗?
我有 2 年的数据分析师和分析工程师工作经验。根据BBC Science Focus,ChatGPT 已吸收了 570 GB 的数据。那么谁写的 SQL 更好呢?
来玩吧!
这个游戏将基于 3 个 LeetCode SQL 挑战(一个简单,两个中等)。我会先写出每个解决方案,然后将练习发给 ChatGPT,看看哪个解决方案效果最好。
我将提供每个挑战的链接,供你也尝试击败 ChatGPT。
挑战 1(简单)
这个挑战叫做客户下的最大订单数量。

作者截图
这是我写的查询:
WITH layer_1 AS (
SELECT
customer_number, COUNT(DISTINCT order_number) AS order_number
FROM orders
GROUP BY customer_number
)
SELECT customer_number
FROM layer_1
ORDER BY order_number DESC
LIMIT 1
它通过了,并且运行时间正确:

作者截图
现在让我们看看 ChatGPT 在这方面的表现。
这是 ChatGPT 的回答:

作者截图
ChatGPT 甚至解释了它所做的工作。我觉得这个查询的可读性不高——这也是我喜欢公共表表达式的原因之一——但让我们看看它的表现如何。

令人印象深刻的是,它有效,但速度比我的结果慢。尽管我很高兴能做得比 ChatGPT 更好,但我希望能知道如何改进这个查询。
挑战 2(中等)
下一个挑战叫做 树节点。

作者截图
我写的第一个查询是这个:
# Write your MySQL query statement below
WITH l1 AS (
SELECT
t.id,
c.id AS c_id,
t.p_id
FROM Tree t
LEFT JOIN Tree c
ON c.p_id = t.id
),
l2 AS (
SELECT
id,
COUNT(DISTINCT c_id) AS nb_childrens,
COUNT(DISTINCT p_id) AS nb_parents
FROM l1
GROUP BY id
)
SELECT id,
CASE
WHEN nb_childrens >0 AND nb_parents >0 THEN "Inner"
WHEN nb_childrens > 0 THEN "Root"
ELSE "Leaf"
END AS type
FROM l2
我在提交之前运行了它,它得到了错误的结果…

作者截图
结果我没有足够注意示例,特别是第二个:

作者截图
当一个节点既是叶子又是根节点时,它应该被输出为根节点。我将 CASE WHEN 的顺序改为:
CASE
WHEN nb_childrens >0 AND nb_parents >0 THEN "Inner"
WHEN nb_parents > 0 THEN "Leaf"
ELSE "Root"
END
而这次,它通过了!
我的查询得到了平均结果:

作者截图
现在是 ChatGPT 玩游戏的时候了!
ChatGPT 阅读了所有示例,并且没有在既是根节点又是叶节点的节点上犯我的错误:

作者截图
但它仍然给了我一个错误的答案:

作者截图
ChatGPT 没有按每个 id 返回一行。
所以我决定给 ChatGPT 一个提示:

作者截图
而且它能够纠正它!

作者截图
我们在这个挑战中都犯了一个错误,但都能纠正它(尽管我不得不给 ChatGPT 提示)。在运行时间方面我有稍微更好的结果,所以得分给我,但差距很小!
挑战 3(中等)
最后的挑战叫做 资本收益/亏损。

作者截图
这是我写的查询:
SELECT
stock_name,
SUM(
CASE
WHEN operation = "Buy" THEN -1*price
ELSE price
END
)
AS capital_gain_loss
FROM Stocks
GROUP BY stock_name

作者截图
它通过了,但运行时间相当差,超过 90% 的玩家在这方面表现得比我好。
让我们看看 ChatGPT 是否其中之一。

作者截图
让我们尝试这个解决方案:

作者截图
再次,它得到了错误的结果。ChatGPT 将一个买入与每一个未来的卖出连接,而不仅仅是对应的一个,因此它的解决方案只在只有一对买入/卖出操作时有效。
我试图告诉 ChatGPT 纠正它的错误,而没有给它任何提示:

作者截图
不幸的是,它给了我完全相同的查询。所以我抱怨了:

作者截图

作者截图
这一次,ChatGPT 和我使用了类似的结构,查询通过了。然而,我不确定 ChatGPT 是否真的理解了它一开始错在哪里。

作者截图
ChatGPT 的运行时间稍微比我差。
结果
Challenge ChatGPT 🤖 Data Analyst 👩💻 Winner
----------- --------------- --------------------- ---------------------
1 ✅ (22%) ✅ (62%) Data Analyst 👩💻
2 ❌ ✅ (36%) ❌ ✅ (54%) Data Analyst 👩💻
3 ❌ ❌ ✅ (8%) ✅ (5%) Data Analyst 👩💻
我认为可以公平地说,我在这次 SQL 挑战中“战胜”了 ChatGPT。我仍然对它的能力感到印象深刻,并惊讶于它能够纠正自己的错误!ChatGPT 在速度上绝对超过我;它能在几秒钟内写出有效的 SQL 语法,而我需要几分钟。
但它仍然有 50%的错误。即使它成功完成了每一个挑战,我也不会担心我的工作。利益相关者从不会向分析师提出如此明确的请求和输出示例。他们带来的是业务问题,我们必须考虑如何利用现有数据来找到最佳答案。ChatGPT 能做到这一点吗?
我问了 ChatGPT 对这个问题的看法:

截图作者
这就总结完了!
希望你喜欢这篇文章!如果喜欢的话,请关注我,获取更多关于 SQL 和数据分析的内容。
数据科学能找到大脚怪吗?
追踪世界上最难捉摸的神秘生物之一的分析方法
·
关注 发表在 Towards Data Science ·14 min read·2023 年 5 月 24 日
--
图片由 Jon Sailer 提供,来源于 Unsplash
看着大脚怪研究人员在夜晚追逐——有时是字面意义上的——间接证据、声音和半隐约的阴影,通过森林寻找,确实让电视节目变得精彩纷呈。
我喜欢在闲暇时间看一些电视节目,而一档名为“探险大脚怪”的节目在许多午休时光中给我带来了乐趣。然而,这档节目并不是普通的神秘生物纪录片,因为它声称使用“数据算法”来首先确定一个目标搜索区域,然后派遣人类研究人员去追踪这个毛茸茸的对象。
显然,当我听到这种情况时,我的耳朵就会竖起来,除了打包过夜行李外,我开始思考我能做些什么来帮助搜索。所以开始吧,“帮助”!
虽然这将是一篇相当轻松的文章,我将查看一些数据,包括:
-
使用 Pandas Profiling 进行探索性数据分析
-
传统方式的探索性数据分析
-
使用 OPTICS 进行聚类
数据
数据来自 Timothy Renner¹,他从 Bigfoot Field Researchers Organization²那里获得了这些观察结果。
数据快速预览:

图片由作者提供
有很多有趣的字段,例如:
-
位置特征如目击地点的州、县以及经纬度。
-
包含温度、云量和降水量的天气特征,但包括温度范围、降水细节和风速。
-
像月相这样神秘的信息。
-
对导致目击事件的完整文本描述,以及目击事件本身的详细信息。
使用 Pandas-Profiling 进行探索性数据分析
Pandas-Profiling 是探索数据的绝佳工具。只需几行代码,你就能获得一个非常有用的交互式报告,概述数据的重要特征。让我们看看它的实际效果。
# create the report
from pandas_profiling import ProfileReport
profile = ProfileReport(df)
profile.to_file(os.path.join(folder,'bfro_reports_eda.html'))
我们可以更深入地查看classification特征:

图片由作者提供
对一个重要特征的详细拆解——稍后会有更多内容。
我们还可以可视化互动——这个视觉图解释了风速和气压之间的关系:

图片由作者提供
Pandas-Profiling 还包括帮助我们通过各种相关性测量来理解数据的功能:

图片由作者提供
当然,没有对缺失情况的总结,EDA 是不完整的。这里我们有几个很棒的展示——首先是按列的缺失情况图表。

图片由作者提供
… 以及缺失值之间的相关性:

图片由作者提供
这么多有用的信息,付出如此少的努力。
传统方式的探索性数据分析
有时我们需要动手来更好地理解数据。让我们这样做,并在过程中讨论。
一个变化趋势
我对大脚怪目击事件随时间变化的趋势感兴趣。在我们继续之前,让我们快速看一下数据库中包含的报告类型。
总结 BFRO 网站³,A 类报告涉及在可以更有信心排除其他动物误解或误认的情况下的明确目击。
在任何情况下未能清晰观察到目标的目击被认为是B 类目击。这包括远距离的目击和光线条件差的目击。
C 类报告是那些高潜在不准确的报告。大多数间接报告或源头无法追溯的故事都属于这一类别。
重要的是,B 类报告并不被认为比 A 类报告更不可信或不重要——BFRO 认为这两类报告都足够可信,可以展示给公众。然而,C 类报告被保存在 BFRO 档案中,但很少公开列出;1958 年前的已发布或本地记录的事件和在非小报报纸或杂志中提到的目击事件是例外。
让我们来看看随时间变化的年度目击,并将 A 类和 B 类目击分开。

作者提供的图像
随着时间的推移,整体呈现出强烈的下降趋势,同时,A 类和 B 类目击的比例保持相对一致。总体下降趋势的驱动因素可能难以确定:
-
改变的社会规范可能会阻止目击者报告潜在的目击——在今天,多少人相信“大脚怪”确实存在?
-
类似地,有多少人知道“大脚怪实地研究办公室”和它的数据库?在这些人中,有多少人愿意花时间完成提交?我敢打赌,不会太多,并且这个数字可能会随着时间的推移而减少。
-
数据库中报告越来越少可能有实际原因。一个主要原因是报告提交的便捷性:在一个优化为便利的数字化世界中,你会花多长时间填写详细的表单并提供准确的信息?
回到数据。值得记住的是,我们在这里讨论的是绝对数量。理想情况下,我们应该考虑一些曝光度的度量,以便获得更好的洞察:一种能指示看到“大脚怪”可能性的特征。例如,将年目击次数表示为进入森林区域的年均外出次数的一个比例,会是一个更有趣的度量。
目前,目击的绝对数量是人们外出的频率的直接函数,可能不可靠:看到更多的“大脚怪”可能是由于更多的人外出,而不是“大脚怪”数量的变化或人们发现神秘生物的能力的变化。
季节性组合
在这样的时间序列数据中,我们可以预期在规律的间隔内会有一些变化;这被称为季节性⁴。
任何大脚怪的季节性行为自然会在数据中产生季节性影响。例如,冬季的冬眠将导致冬季观察数量减少。像雪暴、降雨以及日光时数等季节性因素将影响我们实际看到大脚怪的能力。观察季节性的最大驱动因素可能是季节对潜在观察者的影响。目击事件可能会与夏季露营、季节性狩猎和浪漫的秋季森林散步有关。
现在我们可能没有足够的数据来可视化每月的季节性变化,以免噪声掩盖任何效果,所以我们将查看整体的目击分布情况。

作者提供的图片
我们看到大多数目击事件发生在夏季。我们应该预期这一点,因为良好的天气和充足的日光使人们能够更远、更广泛地进入户外。作为一个喜欢温暖毯子和热茶的人,我并不感到惊讶于最少的目击事件发生在最寒冷和最黑暗的月份。也许大脚怪对茶和毯子有相同的看法。
我们看到这种季节性趋势在时间上是一致的:

作者提供的图片
不幸的是,我们没有太多关于观察时间的数据,所以无法深入探讨。我想象一下,天黑后情况会变得有点疯狂——这在电视上总是会发生的。
到目前为止,我们的探索性数据分析(EDA)帮助我们了解了报告给 BFRO 的大脚怪目击数量随着时间的推移而下降,并且绝大多数目击事件发生在夏季和秋季。这些见解主要与每个报告的时间元素相关——接下来我们将关注位置。
在北美的哪里呢?

照片由 Tabea Schimpf 提供,发布在 Unsplash
让我们使用纬度和经度坐标对来了解大脚怪活动的区域。Folium⁵ 是一个用于创建地图可视化的优秀 Python 包。
使用 Folium 进行互动可视化
首先是热力图,高活动区域以红色和黄色突出显示:
import folium
from folium.plugins import HeatMap,MiniMap
# data for plotting
filter_map = (df['latitude'].notna()) & (df['longitude'].notna())
df_map = df.loc[filter_map,['latitude','longitude','observed']].copy()
# create the map
heat_map = folium.Map(
location = [42,-97.37],
tiles = 'OpenStreetMap',
zoom_start = 4,
control_scale = True
)
# add the heat to the map
HeatMap(
data = df_map[['latitude','longitude','observed']],
min_opacity = 0.1
).add_to(heat_map)
# add mini map
heat_map.add_child(MiniMap(position = 'bottomleft',toggle_display = True))

作者提供的图片
像所有优秀的科幻电影一样,活动似乎仅限于美国(主要是下 48 州)。
华盛顿州和俄勒冈州似乎是活动的绝对重灾区;而俄亥俄州和佛罗里达州代表了最活跃的东部区域。中部地区似乎活动非常少。
我的地理知识不是很好,但我知道华盛顿州和部分俄勒冈州林木繁茂,而佛罗里达州的大部分地区野性十足(既是大沼泽地⁶ 的家园,也是佛罗里达人⁷ 的家乡)。
如果我猜的话,我会说大脚怪可能更喜欢森林或沼泽栖息地,因此华盛顿和佛罗里达似乎是合理的观察结果。到目前为止,都是合情合理的。接下来我们来探讨一些更科学的内容——通过聚类数据来识别大脚怪的热点区域。
使用 OPTICS 进行聚类
聚类是一项旨在将数据集划分为子集的工作,每个子集中的观察结果在某种程度上是相似的。
在这种情况下,我们将使用纬度和经度来识别簇——或者说大脚怪活动的热点。我们将根据各种期望的特性选择我们的聚类算法:
-
该算法需要能够反映坐标对所隐含的物理距离——即,它必须能够使用 haversine 度量。
-
我们需要该算法来过滤掉“噪声”:我们预期一些目击事件是随机的,因此应该落在簇之外。算法需要能够筛选出这些点并将其排除在簇之外。
-
我不知道有多少个大脚怪热点;做出先验假设可能会使分析有偏差。因此,算法应该能够自行确定“最佳”簇数——即它应该是无监督的。
-
理想情况下,我们还可以指定最小簇大小——即热点中的最小目击次数——以便将该簇视为高活动区域。
排序点以识别聚类结构(OPTICS)是一种基于密度的聚类算法,能够提取具有不同密度和形状的簇⁸。它的性质与 DBSCAN 算法类似,但设计上旨在克服在不同密度数据中检测有意义簇的问题⁹。
scikit-learn 文档¹⁰对这些概念做了很好的总结,假设你对 DBSCAN 已经有所了解:
OPTICS 算法与 DBSCAN 算法有许多相似之处,可以被认为是 DBSCAN 的推广¹⁰。
DBSCAN 和 OPTICS 之间的关键区别在于,OPTICS 算法构建了一个可达性图,这个图为每个样本分配了一个
可达性 _距离,以及一个簇内的排序 _属性;这两个属性在模型拟合时分配,并用于确定簇的成员资格¹⁰。OPTICS 生成的可达性距离允许在单个数据集中提取具有可变密度的簇¹⁰。
如果你不熟悉 DBSCAN,我在这里详细说明了它的工作原理(并解释了 haversine 距离):
演示如何结合自定义度量和贝叶斯优化来调整基于密度的空间聚类…
towardsdatascience.com
我们将编写一个函数来帮助我们进行聚类。这里没有什么太复杂的:
-
该函数将接受我们的(过滤后的)数据集、最小聚类大小的参数和季节的参数。
-
该函数根据
season进行一些过滤。 -
我们初始化了 OPTICS 聚类,并进行聚类(记得转换为弧度,因为我们使用的是 haversine 距离度量)。在这种情况下,我们只改变了聚类中的最小样本数,即
min_cluster_size。 -
该函数返回一个包含结果聚类质心的数据集。我们通过计算聚类中所有点的平均纬度和平均经度来粗略计算质心,记得不要包括聚类
-1,因为这是 OPTICS 分配给“噪声”观察的标签。
# function to do clustering
def get_cluster(data,cluster_size = 20,season = None):
# get data
if season is not None:
d = data.loc[data['season'] == season,:].copy()
else:
d = data.copy()
# cluster
cluster = OPTICS(
min_cluster_size = cluster_size,
metric = 'haversine',
algorithm = 'ball_tree',
n_jobs = -1
)
cluster.fit(
np.radians([x for x in zip(d['latitude'],d['longitude'])])
)
# get cluster centroids
d['cluster'] = pd.Series(cluster.labels_)
fields = ['latitude','longitude']
d_agg = d.loc[d['cluster'] >= 0,:].groupby('cluster')[fields].mean()
return d_agg
我们差不多准备好运行聚类了。在开始之前,让我们做一些粗略的计算,以帮助设置最小聚类大小。
我将集中关注过去 20 年的数据,并考虑如果某个区域有持续的活动报告,则将其视为热点。如果不按季节进行分层,我认为每年一次报告是一个好的起点——即将cluster_size设置为 20,season = None。如果我们要考虑季节性因素,则可以将其调整为反映我们上面调查的季节性分布。
这意味着:
-
在“总体”数据集上进行的任何聚类都将具有最小聚类大小为 20。
-
夏季、秋季、冬季和春季的最小聚类大小分别为 7、6、3 和 3。这反映了每个季节中观察到的 37%、30%、16%和 15%的数据(分别基于上面的 EDA)。
最后,是时候进行聚类和可视化了!首先是“总体”聚类结果。

图片来源:作者
在华盛顿州和波特兰地区,我们看到了一些聚类,正如热图所示。有趣的是,俄亥俄州并没有很多热点,佛罗里达州则没有。
让我们看看季节性聚类的样子:

图片来源:作者
有趣!
看起来在美国西部,大脚怪主要在夏季(红点)和秋季(橙点)出现。中央地区似乎有一些春季热点(绿色)。如果你住在美国东部,你甚至可以在冬季(蓝点)看到大脚怪。
如果我们稍微放大一点,可以集中在你最有可能全年遇到大脚怪的区域:

图片来源:作者
现在我们将忽略城市化程度较高的地区似乎吸引大脚怪的事实,转而识别大脚怪经常出现的地标。
词云
我们的数据包含一个名为location_details的字段。不用猜也知道这是什么。
词云将允许我们利用这个字段来了解“大脚怪”目击中最常见的地点和地标,而无需绘制频率或比例——令人耳目一新!
词云只是一个包含一组单词的图像,每个单词的大小表示其重要性或频率。词云是非常简单的展示方式,可以用最少的努力提供大量的见解。让我们使用wordcloud Python 包来生成一个词云。
from wordcloud import WordCloud
# get text
text = df.loc[:,'location_details'].str.cat(sep = ' ').lower()
# cloud
wordcloud = WordCloud(scale = 5,background_color = 'white').generate(text)
# show
plt.figure(figsize = (20,7.5))
plt.imshow(wordcloud, interpolation = 'bilinear')
plt.axis('off')
plt.show()
我们需要做的唯一数据处理是将描述列转换为一个单一的文本字段,并将所有字母转换为小写。这可以通过cat和lower字符串方法轻松完成。wordcloud则处理了大部分其他工作——生成一个外观出色的词云:

图片来源:作者
看起来大多数目击事件涉及某种高速公路或湖泊。显然山脉、森林和林地并不多。
总结
在这篇文章中我们涵盖了相当多的内容。让我们总结一下。
数据探索
我们看到如何使用 Pandas-Profiling 来减少许多手动的探索性数据分析任务,但也看到有时如果我们想更详细地理解数据中的某些元素,动手实践是必要的。
下次我可能会专注于在运行分析之前进行一些轻量级的预处理,因为报告最终将日期字段视为字符串而不是日期时间。这是一个误解——主题上是一致的。
我确实想知道运行时间和报告的解释性如何随着更广泛、高维的数据框的扩展而变化:更多的总结、交互和相关性计算无疑会带来计算成本;更大的相关性可视化可能会使解释变得更加困难。
可视化
Folium 使我们能够相当轻松地创建出色的交互式可视化,创建了热图并绘制了各种 OPTICS 聚类运行的结果。
虽然我在这里没有演示,但 Folium 也可以轻松创建和可视化聚类。如果我再做一次这种分析——或类似的分析——在进行更正式的聚类工作之前利用这个功能可能是有利的。
使用 OPTICS 进行聚类
尽管我没有明确展示,但我过滤掉了所有缺少纬度或经度的目击事件。在没有对坐标对的缺失进行任何分析的情况下,我不能确定这是否引入了任何偏差,如果我们在进行严肃分析时需要检查这一点。
这实际上引出了数据中的一个重要细节——坐标对的准确性。我想象一下,当你刚刚发现一个大脚怪时获取精确的 GPS 读数可能是具有挑战性的,因此很多经纬度读数可能只是参考地标的最佳估计。我们可能无法纠正这一点,因为任何形式的干预都可能引入偏差。这种位置“误差”实际上比你想象的更常见——例如,交通事故的位置可能是从最近的安全地点获取的。
现在很明显,聚类需要一些改进,尤其是因为一些聚类中心落在了相当城市化的区域。我们不仅要在 OPTICS 中调整min_cluster_size,还需要对聚类区域进行控制,以便得到一些“可搜索”的区域。我不知道 OPTICS 算法中是否有允许我们控制聚类区域的参数,因此我们需要引入某种形式的后处理——类似的内容在我上面链接的文章中有介绍。
如果你读到这里,谢谢你的阅读!希望你享受阅读的过程,就像我享受写作一样。
最后的一句话
尽管这篇文章的重点是使用数据科学的方法来寻找一个神秘生物,但请以我写作时的心态来看待它——这是一个有趣的分析。我认为大脚怪存在并在某个地方徘徊吗?不。我认为如果科学家们真的发现了一个新的动物物种,那将会很惊人。科学万岁!
布拉德
参考资料和资源
-
大脚怪目击数据集 — timothyrenner | data.world,根据公共领域许可证提供。
-
GitHub — python-visualization/folium: Python 数据. Leaflet.js 地图。
语言模型能否自制工具?
原文:
towardsdatascience.com/can-language-models-make-their-own-tools-cbc7c3777d22
LaTM、CREATOR 以及其他 LLM 工具使用的闭环框架……
·发布于Towards Data Science ·阅读时间 16 分钟·2023 年 9 月 17 日
--

(照片由Todd Quackenbush提供,来源于Unsplash)
在最近的综述中,我们探讨了通过外部工具增强大型语言模型(LLMs)的实用性。这些模型可以被教导以多种方式利用工具。然而,我们应该意识到,现有的工具跟随 LLMs 仅利用了一小部分潜在工具[3],而我们希望用 LLMs 解决的问题范围几乎是无穷无尽的! 考虑到这一点,显而易见,这种范式是有限制的——我们总是能找到需要尚不存在的工具的情景。在本综述中,我们将探讨旨在解决这一问题的最新研究,通过赋予 LLMs 创造自身工具的能力来解决这一问题。这种方法与人类生活有趣地类比,因为制造工具的能力导致了重大的技术进步。现在,我们探讨类似技术对 LLMs 进化的影响。
“根据人类进化里程碑的经验教训,一个关键的转折点是人类获得了制造自身工具以应对新兴挑战的能力。我们开始初步探索将这一进化概念应用于 LLMs 领域。” — 引自 [1]

(来源于 [1, 2])
背景
在进一步了解工具制造的 LLMs 之前,我们需要刷新一些背景概念。我们在最近的综述中已经覆盖了许多这些概念,但我们现在会简要地再次讨论它们,以使我们对最新出版物的讨论更加全面和易于理解。
为什么我们应该使用工具?

(来自 [3, 8, 9])
在之前的概述中,我们已经了解了几种不同类型的工具,这些工具可以与 LLM 集成以改善其性能,例如:
-
基础工具(计算器、搜索引擎等) [链接]
-
深度学习模型 API [链接]
通过让 LLM 访问某些工具,我们可以轻松解决这些模型存在的限制,例如缺乏最新信息、无法进行简单的算术运算、产生虚假信息或在长链推理中出错。

(来自 [3])
工具提供上下文。 例如,如果 LLM 被问到关于最近几周的流行文化事件的问题,由于知识截止日期,它不太可能提供准确的答案。在某些情况下,LLM 可能会产生看似可信的错误答案,并误导用户提供不正确的信息 — 这是一个重大问题,因为许多(非技术性)LLM 用户如 ChatGPT 使用这些模型就像使用搜索引擎一样! 为了解决这个问题,我们可以提供一个工具,允许 LLM 执行搜索查询并从互联网上获取最新信息作为额外上下文;见上文。这样,LLM 可以记住更少的信息,而依赖上下文学习,通过工具提供的最新信息得出准确的最终答案。
“通过赋能 LLM 使用工具,我们可以获得更广泛和不断变化的知识库,并完成复杂的计算任务。” — 来自 [3]
在这次概述中,我们将看到一个有趣的工具作为基准使用,即 Wolfram ChatGPT 插件。ChatGPT 的插件生态系统通过 API 将 LLM 与外部工具集成。基本上,我们向 ChatGPT 提供 API 的描述,模型通过提示方法学习如何使用该工具(即调用其 API);更多详细信息 这里。要了解更多关于 Wolfram 插件的信息(它非常有用!),请查看精彩概述 这里。
本概述。 存在许多不同类型或类别的工具,但我们将重点关注一种特定类型的工具 — 那些实际上由 LLM 创建的工具。通常,这些工具被格式化为独立的 Python 函数,完成对 LLM 有用的某些任务或子任务。工具通过直接提示支持代码的 LLM 生成所需的功能而创建。通过允许 LLM 创建自己的工具,解决问题的系统不再受限于固定的工具集合。我们可以随着时间的推移识别所需的功能,并使 LLM 能够自动创建任何有用的工具!
提示技术

语言模型以统一的格式解决许多不同的任务(来自 [10])
语言模型的通用文本到文本结构极为强大,因为它允许我们通过 i) 将问题格式化为文本提示和 ii) 从模型返回的文本中提取相关输出信息来解决许多不同的任务。然而,使用语言模型通常没有这么简单。我们提供给模型的提示的措辞和结构可以极大地改变其效果——提示工程至关重要!
最近,我们讨论了许多实用技巧和技术,以通过提示工程充分利用 LLM。
-
实用提示工程 [link]
-
高级提示工程 [link]
然而,有两种特别相关的提示技术——思维链(CoT) [6] 和思维程序(PoT) [7] 提示。这两种技术都旨在提高语言模型可靠解决复杂推理任务的能力。

(来自 [6])
思维链。 长期以来,LLM 因无法解决基于推理的任务而受到批评。尽管这一问题已通过最近的模型变体得到缓解,但诸如 CoT 提示等技术仍能引发这些模型更好的推理能力。这怎么可能? 我们只需要向 LLM 提供将基于推理的问题分解为逐步解决的示例(即问题解决的理由或“思维链”)。这些示例直接插入 LLM 的提示中。然后,模型可以利用其上下文学习能力,在解决用户提出的问题时生成类似的理由。有趣的是,生成这样的理由会大幅提高 LLM 在基于推理的任务上的表现。

(来自 [11, 12, 13])
除了普通的 CoT 提示外,还有许多变体被提出;见上文。通过将难题分解成更小的部分并逐步解决,让 LLM 解决复杂问题的理念极为强大。然而,我们可以通过几种不同的方式来实现这一点(其中一些实际上比 CoT 提示更简单)——CoT 提示并不总是我们最佳的选择。
思维程序。 尽管思维 CoT 提示变体效果良好,但它们未能建模像迭代这样的概念,并且存在组成性差距,这意味着 LLM 可能正确解决了问题的每一步,但最终答案仍然可能不正确。为了缓解这些问题,最近的研究探索了程序辅助的语言模型。这些技术类似于 CoT 提示,但我们使用代码支持的 LLM(例如,Codex [14])生成包含代码和自然语言组件的混合问题解决方案——基本上是带有信息性注释的程序。然后,我们可以使用外部解释器执行 LLM 创建的程序,以得出最终答案!

(来源:[7])
PoT 提示的基本思想是某些想法和概念在程序中建模起来更容易。与其让 LLM 既解决子任务又从这些解决方案中生成最终答案,我们可以将部分过程委托给更可靠的系统。即,程序可以更容易地建模和解决数学方程式,执行迭代等,从而减少 LLM 的组成性差距。
工具使用的转折点
“与其让 LLM 作为工具的用户,不如让它们成为工具的创造者,以更高的准确性和灵活性解决问题。” — 来源于[2]
此时,我们可能应该确信工具是对现有语言模型的有用补充。但是,当我们将可用工具的范围扩展到 LLM 可以创建的任何工具时,会有什么可能性? 简而言之,这种方法形成了一个闭环框架,在这个框架中,LLM 被赋予了随意改进自身功能的能力。正如我们接下来会看到的,现有模型在制作自己的工具方面出乎意料地有能力,这使得它们能够随着时间的推移动态适应解决新的、困难的问题。
解耦工具制作和工具使用
我们知道,使用外部工具可以极大地提高 LLM 的解决问题能力[3]。然而,该领域的先前工作假设所需的工具已经存在并可供 LLM 使用。因此,这种方法依赖于人工制作和策划一套全面解决任何任务所需功能的工具。但是,如果 LLM 需要一个工具,而这个工具不在它的工具箱里怎么办? 现有的工具跟随方法对此类问题没有解决方案!
作为替代方法,[2]中的作者提出了一种“闭环”框架,该框架利用 LLM 自身即时构建所需工具。这个框架称为 LLMs 作为工具制造者(LaTM),它允许 LLM 不断生成解决不同复杂推理任务所需的工具;见下文。

(来源:[1])
两阶段工具方法。 LaTM 使用两阶段框架:
-
工具制作: 为给定任务制作工具(即 Python 函数)
-
工具使用: 使用现有工具来解决任务
值得注意的是,我们不必在这两个阶段使用相同的 LLM。例如,我们可以将更强大的模型(例如,GPT-4)应用于工具制作,而使用轻量且成本效益更高的模型(例如,GPT-3.5-turbo)来进行工具使用。鉴于工具制作通常需要相对于工具使用更强大的 LLM,这种方法使 LaTM 在实践中能够节省成本。我们只在工具制作阶段使用最昂贵和强大的模型,每个工具执行一次并不断重复使用于问题解决!

(来自 [1])
工具制作过程的目标是从少量任务解决示例中生成一个通用且可重用的工具 —— 以 Python 函数的形式实现。在 [2] 中,这一目标通过首先通过少量示例学习“提议”一个工具来实现。我们提供多个期望行为的演示,并提示 LLM 生成一个程序,该程序再现这些演示的输出。如果生成的程序没有错误,LaTM 使用 LLM 生成几个单元测试(即基于提供的演示)并执行这些测试以确认它们通过。最后,工具被“包装”,或通过展示其用法的提示提供;见上文。
尽管工具制作复杂且需要强大的 LLM 才能成功,但工具使用可以通过更具成本效益的模型来实现 —— 我们只是使用现有工具来解决任务! 我们可以提示 LLM 通过工具制作过程中创建的包装工具来使用工具,这些工具提供将任务转换为相关函数调用的演示。在这里,LaTM 依赖 LLM 的上下文学习能力来确定每个工具的正确使用方法;见下文。

(来自 [1])
值得注意的是,使用较小的模型进行工具使用意味着我们只在 LaTM 流程的短暂部分中使用更强大的模型 —— 工具只创建一次,解决更多问题时可以持续重复使用。创建和使用 LaTM 工具的过程可能看起来有些复杂,但实际上相当简单。下图中提供了一个创建和使用工具来解决逻辑推理问题的端到端示例。

(来自 [1])
添加调度器。 除了工具创建和使用之外,[2]中的作者还提出了一个调度模块,用于处理 LaTM 中新工具的即时创建和使用;详见下文。简而言之,调度器是一个用于确定传入任务是否应该创建新工具或仅使用现有工具的 LLM。通过使用这个额外的模块,我们可以轻松识别现有工具无法处理的新任务,并创建所需的任何工具,使 LaTM 能够更好地处理新任务顺序到达的流式场景。有趣的是,[2]中的作者展示了一个基于 GPT-3.5 的调度器可以以 ~95% 的准确率识别正确的工具使用方式——或是否需要新工具。

(来源于[1])
这有效吗? LaTM 在 BigBench 提供的一小部分复杂推理任务上进行评估,其中 GPT-4 被用作工具创建者,而 GPT-3.5-turbo 被用作工具使用者。某种程度上,GPT-4 能够在大多数测试案例中创建出可行且有用的工具并不令人意外;详见下文。能力较弱的模型(例如 GPT-3.5-turbo)可以用于处理较简单的问题的工具制作,但在更复杂的领域中需要 GPT-4。进一步地,我们发现工具制作需要更长的上下文长度,因为 LaTM 在生成工具时使用了全部历史(即到目前为止生成的所有工具的示例),以提高可靠性。

(来源于[1])
当 LaTM 的性能与像链式思维提示 [4]这样的技术进行比较时,我们发现所提出的方法使现有的 LLM 能力大大增强!通过使用生成的工具,像 GPT-3.5-turbo 这样的模型可以与 GPT-4 进行类似的表现,并且远超 CoT 提示的性能;详见下文。此外,我们还发现,使用更轻量的模型作为工具使用者更为可取,甚至在某些情况下优于使用像 GPT-4 这样的强大模型。

(来源于[1])
LaTM 是一个有趣的闭环框架,使 LLM 能够生成自己的工具。由于它仅在解决问题过程中的一小部分使用大型、昂贵的 LLM(例如 GPT-4),LaTM 是一种提高 LLM 性能的成本效益高的方法。我们可以以较小的模型和更低的成本,几乎匹配 GPT-4 在复杂推理任务中的表现。
纠正工具创建中的错误

(来源于[2])
在使用大型语言模型(LLMs)创建自己工具的想法的基础上,[2]的作者提出了一种新颖的框架,该框架 i) 通过文档和代码创建来使用 LLMs 创建相关工具,ii) 采用一种更简单的方法来规划如何使用工具解决问题,iii) 为工具使用过程添加了一个补充的错误处理机制,以使整个系统更具鲁棒性和准确性。 resulting technique, 被称为 CREATOR [2],与[2]中的研究并行探索。 这两篇出版物的目标是通过使所需工具的创建成为可能,从而创建更智能和适应性强的系统以解决复杂问题。

(来自 [2])
问题解决方法。 CREATOR 通过一个四步过程来处理工具的创建和使用(参见上面的插图):
-
创建:通过文档和代码实现来创建工具。
-
决策:决定何时以及如何使用现有工具来解决问题。
-
执行:执行将工具应用于解决问题的程序。
-
修正:根据执行结果修改工具和决策。
之前的工作中没有修正步骤。这个组件作为一个自动化的错误处理机制,提高了系统的鲁棒性。由于[2]中的 LLMs(以及相关出版物[1]中的 LLMs)使用代码作为创建工具的媒介,我们可以轻松地检测和修正创建或使用工具时出现的错误(例如,通过堆栈跟踪或类似的方式)。

(来自 [2])
在[2]中,工具创建遵循一种上下文学习方法,该方法提供详细的说明和少量示例以指导 LLM 生成正确的工具。工具创建有两个主要组件:
-
文档:概述有关工具的相关信息(例如,功能、目的、签名等)。
-
实现:在代码中实现工具(参见上文)。
类似于[1],[2]中创建的工具被封装在一个函数或方法(在 Python 中)中,可以由 LLM 调用。

(来自 [2])
在决策阶段,LLM 会考虑所有工具的文档,并确定使用哪些工具以及如何使用它们来解决当前的问题。在制定了问题解决计划后,我们可以:
-
格式化每个工具的输入。
-
执行每个工具以获取相关输出。
-
对工具输出执行任何需要的操作以推导出答案。
如果工具执行导致生成任何错误,我们可以简单地记录这些信息,并再次迭代四步过程,将错误作为修正现有工具的额外输入;参见上文。否则,我们可以使用生成的信息提取用户问题的最终答案。
“我们的研究揭示了利用 LLMs 作为工具创建者可以促进知识转移,且 LLMs 表现出不同的工具创建能力,从而使其能够灵活应对各种情况。” — 来自 [2]
改进的数学推理。 在 [2] 中提出的系统在数学(和表格)推理数据集 MATH 和 TabMWP [4, 5] 上进行了评估。在所有实验中,ChatGPT(即 GPT-3.5-turbo)作为基础模型,因其代码生成和令人印象深刻的推理能力。CREATOR 与基线方法进行比较,如标准 CoT 提示 [6]、PoT 提示 [7] 和 Wolfram alpha ChatGPT 插件。当所有方法都使用 ChatGPT 作为基础模型时,我们发现 CREATOR(结合 CoT 提示)比基线方法具有更高的整体准确性,并且成功执行率也有所提高(即系统提供了有效格式的答案)。

(来自 [2])
除了这些评估,作者在 [2] 中提出了一个新的 Creation 挑战数据集,尝试通过在没有现有工具或代码包的新场景中测试问题解决能力来评估 LLM 的工具创建能力。在这一基准测试中,CREATOR 稍微超越了现有的基线。然而,当 LLM 获得关于应创建工具的实用性的文本提示时,这种性能改进会更大;见下文。

(来自 [2])
除了在总体上匹配或超越基线性能的能力之外,CREATOR 框架在问题变得越来越困难时表现得更加稳定,而基线方法往往会恶化。CREATOR 在某些问题类别上也会出现类似的恶化,但该框架似乎更加适应并能够处理复杂问题;见下文。

(来自 [2])
收获
在本概述中,我们探讨了一种更具动态性和灵活性的工具使用方法,与 LLMs 结合。我们不再制定一组固定的工具供 LLM 使用,而是可以让 LLM 根据需要创建所需的工具。通过这种方法,我们不再遇到由于没有访问所需工具而导致的问题。我们可能最初会怀疑这种策略是否成功(即 LLMs 是否足够强大以创建自己的工具?),但最近的研究[1, 2]告诉我们,像 GPT-4 这样的最先进 LLMs 完全有能力创建独立的 Python 函数形式的工具,只要在工具创建中采取纠错措施。这些工具可以被使用和重复使用(即使是由较弱的 LLMs),以解决各种复杂问题。
“工具制造使 LLM 能够不断生成可以应用于不同请求的工具,以便未来的请求在解决任务时可以调用相应的 API。” — 来自 [1]
使用 LLMs(大语言模型)来创建工具是很棒的,但这与已经将 LLMs 与各种现有工具整合的并行努力有何关系? 这还有待观察。然而,我个人认为,最佳系统将会使用现有技术的混合体。已经存在许多有用的工具,并且这些工具每天都在与流行的 LLMs 集成(例如,参见 ChatGPT 插件商店)。因此,单靠 LLMs 来创建自己的工具并不合适。相反,我们可以利用现有的工具,同时也给 LLMs 所需的技能,以创造它们所缺乏的任何工具。随着时间的推移,LLMs 可用的工具套件将继续演变,使基于 AI 的问题解决系统变得更加有效。
与我联系!
非常感谢你阅读这篇文章。我是 Cameron R. Wolfe,Rebuy的 AI 总监。我研究深度学习的经验和理论基础。如果你喜欢这个概述,请订阅我的 Deep (Learning) Focus newsletter,在这里我通过从基础到高级的相关主题概述来帮助读者理解 AI 研究。你也可以在 X 和 LinkedIn 上关注我,或者查看我在 medium 上的 其他文章!
参考文献
[1] 蔡天乐等. “大型语言模型作为工具制造者。” arXiv 预印本 arXiv:2305.17126(2023 年)。
[2] 钱成等. “CREATOR: 通过工具创建解开大型语言模型的抽象与具体推理。” arXiv 预印本 arXiv:2305.14318(2023 年)。
[3] Schick, Timo, 等. “Toolformer: 语言模型可以自学使用工具。” arXiv 预印本 arXiv:2302.04761(2023 年)。
[4] Lu, Pan, 等. “通过策略梯度进行动态提示学习,以应对半结构化数学推理。” arXiv 预印本 arXiv:2209.14610(2022 年)。
[5] Hendrycks, Dan, 等. “通过数学数据集衡量数学问题解决能力。” arXiv 预印本 arXiv:2103.03874(2021 年)。
[6] Wei, Jason, 等. “思维链提示在大型语言模型中引发推理。” arXiv 预印本 arXiv:2201.11903(2022 年)。
[7] 陈文华等. “思想提示程序:解开数值推理任务中的计算与推理。” arXiv 预印本 arXiv:2211.12588(2022 年)。
[8] Shen, Yongliang, 等. “Hugginggpt: 用 chatgpt 和 huggingface 的朋友解决 AI 任务。” arXiv 预印本 arXiv:2303.17580(2023 年)。
[9] Patil, Shishir G., 等. “Gorilla: 连接大量 API 的大型语言模型。” arXiv 预印本 arXiv:2305.15334(2023 年)。
[10] 拉菲尔,科林 等。“通过统一的文本到文本转换器探索迁移学习的极限。” 机器学习研究杂志 21.1 (2020): 5485–5551。
[11] 小岛武史 等。“大型语言模型是零样本推理器。” arXiv 预印本 arXiv:2205.11916 (2022)。
[12] 王学智 等。“自我一致性提高了语言模型中的思维链推理能力。” arXiv 预印本 arXiv:2203.11171 (2022)。
[13] 周,丹尼 等。“最少到最多提示方法在大型语言模型中的复杂推理能力。” arXiv 预印本 arXiv:2205.10625 (2022)。
[14] 陈马克 等。“评估训练有代码的大型语言模型。” arXiv 预印本 arXiv:2107.03374 (2021)。
LLM 能否取代数据分析师?构建一个 LLM 驱动的分析师
第一部分:赋能 ChatGPT 工具
·
关注 发表在 Towards Data Science ·19 分钟阅读·2023 年 12 月 11 日
--
图片来自 DALL-E 3
我认为在过去的一年里,我们每个人至少都有过一次怀疑 ChatGPT 是否(或者更准确地说何时)能够取代你的角色。我也不例外。
我们有一个共同的观点,即生成型 AI 的最新突破将对我们的个人生活和工作产生重大影响。然而,目前尚未明确我们角色的变化会如何发展。
花大量时间思考不同的未来情景及其概率可能很吸引人,但我建议一种完全不同的方法——自己动手构建原型。首先,这相当具有挑战性和趣味性。其次,这将帮助我们以更结构化的方式来看待我们的工作。第三,这将给我们提供一个实践最前沿方法——LLM 代理的机会。
在这篇文章中,我们将从简单开始,学习 LLM 如何利用工具并执行直接任务。但在接下来的文章中,我们将深入探讨不同的方法和 LLM 代理的最佳实践。
那么,旅程开始吧。
什么是数据分析?
在深入了解 LLM 之前,我们先定义什么是分析,以及我们作为分析师要做哪些任务。
我的座右铭是分析团队的目标是帮助产品团队在可用时间内基于数据做出正确的决策。这是一个很好的使命,但为了定义 LLM 驱动的分析师的范围,我们应该进一步分解分析工作。
我喜欢 Gartner 提出的框架。它识别了四种不同的数据和分析技术:
-
描述性分析回答诸如“发生了什么?”的问题。例如,12 月份的收入是多少?这种方法包括报告任务和使用 BI 工具。
-
诊断分析更进一步,提出诸如“为什么会发生这种情况?”的问题。例如,为什么收入比去年减少了 10%?这种技术需要对数据进行更深入的挖掘和切分。
-
预测分析允许我们回答诸如“会发生什么?”的问题。这种方法的两个基石是预测(预测业务正常情况下的未来)和模拟(建模不同可能的结果)。
-
规范性分析影响最终决策。常见的问题包括“我们应该关注什么?”或“我们如何能将量提高 10%?”。
通常,公司会逐步经历这些阶段。如果你的公司尚未掌握描述性分析(例如没有数据仓库、BI 工具或指标定义),那么几乎不可能开始看预测和不同的情景分析。因此,这个框架也可以展示公司的数据成熟度。
同样,当分析师从初级成长为高级时,她通常会经历这些阶段,从明确的报告任务开始,逐步发展到模糊的战略问题。因此,这个框架在个人层面也具有相关性。
如果我们回到 LLM 驱动的分析师,我们应该专注于描述性分析和报告任务。最好从基础开始。因此,我们将重点学习 LLM,以了解有关数据的基本问题。
我们已经为第一个原型定义了重点。因此,我们准备进入技术问题,讨论 LLM 代理和工具的概念。
LLM 代理和工具
当我们之前使用 LLM 时(例如,为了做主题建模这里),我们在代码中自己描述了确切的步骤。例如,让我们看看下面的链条。首先,我们要求模型确定客户评论的情感。然后,根据情感,从评论中提取提到的优点或缺点。

作者插图
在这个例子中,我们明确地定义了 LLM 的行为,并且 LLM 很好地解决了这个任务。然而,如果我们构建更高层次和模糊的东西,比如一个由 LLM 驱动的分析师,这种方法就不起作用了。
如果你曾经作为分析师或与分析师合作过至少一天,你会知道分析师会收到各种不同的问题,从基础问题(如“昨天我们网站上有多少客户?”或“你能为我们明天的董事会会议做个图表吗?”)到非常高层次的问题(例如,“主要的客户痛点是什么?”或“我们应该接下来进入哪个市场?”)。不用说,描述所有可能的场景是不现实的。
然而,有一种方法可能对我们有所帮助——代理。代理的核心思想是将 LLM 用作推理引擎,可以选择下一步做什么,以及何时向客户返回最终答案。这听起来非常接近我们的行为:我们接到任务,定义所需工具,使用它们,然后在准备好时返回最终答案。
与代理相关的基本概念(我已经在上面提到过)是工具。工具是 LLM 可以调用以获取缺失信息的功能(例如,执行 SQL、使用计算器或调用搜索引擎)。工具至关重要,因为它们使你能够将 LLM 提升到一个新的水平,并与世界互动。在本文中,我们将主要关注作为工具的 OpenAI 函数。
OpenAI 已经对模型进行了微调,以便能够处理函数:
-
你可以将包含描述的函数列表传递给模型;
-
如果与你的查询相关,模型会返回一个函数调用——函数名称和调用它的输入参数。
你可以在文档中找到更多信息和支持函数的模型的最新列表。
使用 LLM 的函数有两个显著的用例:
-
标记和提取——在这些情况下,函数用于确保模型的输出格式。你将得到一个结构化的函数调用,而不是通常的内容输出。
-
工具和路由——这是一个更有趣的用例,允许你创建一个代理。
让我们从提取的更直接的用例开始,以了解如何使用 OpenAI 函数。
用例 #1: 标记与提取
你可能会想知道标记和提取之间有什么区别。这些术语很接近。唯一的区别在于模型是提取文本中呈现的信息还是标记文本提供新信息(即定义语言或情感)。

作者插图
由于我们决定专注于描述性分析和报告任务,让我们使用这种方法来结构化传入的数据请求,并提取以下组件:度量标准、维度、筛选条件、时间段和期望输出。

作者插图
这将是一个提取示例,因为我们只需要文本中存在的信息。
OpenAI Completion API 基础示例
首先,我们需要定义函数。OpenAI 期望一个作为 JSON 的函数描述。这个 JSON 会传递给 LLM,所以我们需要告诉它所有的上下文:这个函数的作用是什么以及如何使用它。
这是一个函数 JSON 的示例。我们已经指定了:
-
函数本身的
name和description, -
每个参数的
type和description, -
函数所需的输入参数列表。
extraction_functions = [
{
"name": "extract_information",
"description": "extracts information",
"parameters": {
"type": "object",
"properties": {
"metric": {
"type": "string",
"description": "main metric we need to calculate, for example, 'number of users' or 'number of sessions'",
},
"filters": {
"type": "string",
"description": "filters to apply to the calculation (do not include filters on dates here)",
},
"dimensions": {
"type": "string",
"description": "parameters to split your metric by",
},
"period_start": {
"type": "string",
"description": "the start day of the period for a report",
},
"period_end": {
"type": "string",
"description": "the end day of the period for a report",
},
"output_type": {
"type": "string",
"description": "the desired output",
"enum": ["number", "visualisation"]
}
},
"required": ["metric"],
},
}
]
在这个用例中没有必要实现函数本身,因为我们不会使用它。我们只是以结构化的方式作为函数调用获得 LLM 响应。
现在,我们可以使用标准的 OpenAI Chat Completion API 来调用函数。我们传递给 API 调用:
-
模型——我使用了最新的 ChatGPT 3.5 Turbo,它可以与函数一起工作,
-
消息列表——一个系统消息以设置上下文和一个用户请求,
-
我们之前定义的函数列表。
import openai
messages = [
{
"role": "system",
"content": "Extract the relevant information from the provided request."
},
{
"role": "user",
"content": "How did number of iOS users change over time?"
}
]
response = openai.ChatCompletion.create(
model = "gpt-3.5-turbo-1106",
messages = messages,
functions = extraction_functions
)
print(response)
结果是我们得到了以下 JSON。
{
"id": "chatcmpl-8TqGWvGAXZ7L43gYjPyxsWdOTD2n2",
"object": "chat.completion",
"created": 1702123112,
"model": "gpt-3.5-turbo-1106",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"function_call": {
"name": "extract_information",
"arguments": "{\"metric\":\"number of users\",\"filters\":\"platform='iOS'\",\"dimensions\":\"date\",\"period_start\":\"2021-01-01\",\"period_end\":\"2021-12-31\",\"output_type\":\"visualisation\"}"
}
},
"finish_reason": "function_call"
}
],
"usage": {
"prompt_tokens": 159,
"completion_tokens": 53,
"total_tokens": 212
},
"system_fingerprint": "fp_eeff13170a"
}
记住,函数和函数调用会计入令牌限制并产生费用。
模型返回了一个函数调用而不是常见的响应:我们可以看到content是空的,而finish_reason等于function_call。在响应中,还有函数调用的输入参数:
-
metric = "number of users", -
filters = "platform = 'iOS'", -
dimensions = "date", -
period_start = "2021-01-01", -
period_start = "2021-12-31", -
output_type = "visualisation"。
模型做得很好。唯一的问题是它凭空假设了时间段。我们可以通过向系统消息添加更明确的指导来解决这个问题,例如,"从提供的请求中提取相关信息。仅提取初始请求中呈现的信息;不要添加任何其他内容。如果缺少内容,返回部分信息。"
默认情况下,模型决定是否独立使用函数(function_call = 'auto')。我们可以要求它每次返回特定的函数调用,或者根本不使用函数。
# always calling extract_information function
response = openai.ChatCompletion.create(
model = "gpt-3.5-turbo-1106",
messages = messages,
functions = extraction_functions,
function_call = {"name": "extract_information"}
)
# no function calls
response = openai.ChatCompletion.create(
model = "gpt-3.5-turbo-1106",
messages = messages,
functions = extraction_functions,
function_call = "none"
)
我们得到了第一个使用 LLM 函数的工作程序。这很棒。然而,在 JSON 中描述函数并不是很方便。让我们讨论如何简化这个过程。
使用 Pydantic 定义函数
为了更方便地定义函数,我们可以利用Pydantic。Pydantic 是最受欢迎的数据验证 Python 库。
我们已经使用Pydantic 定义了 LangChain 输出解析器。
首先,我们需要创建一个继承自BaseModel类的类,并定义所有字段(我们函数的参数)。
from pydantic import BaseModel, Field
from typing import Optional
class RequestStructure(BaseModel):
"""extracts information"""
metric: str = Field(description = "main metric we need to calculate, for example, 'number of users' or 'number of sessions'")
filters: Optional[str] = Field(description = "filters to apply to the calculation (do not include filters on dates here)")
dimensions: Optional[str] = Field(description = "parameters to split your metric by")
period_start: Optional[str] = Field(description = "the start day of the period for a report")
period_end: Optional[str] = Field(description = "the end day of the period for a report")
output_type: Optional[str] = Field(description = "the desired output", enum = ["number", "visualisation"])
然后,我们可以使用 LangChain 将 Pydantic 类转换为 OpenAI 函数。
from langchain.utils.openai_functions import convert_pydantic_to_openai_function
extract_info_function = convert_pydantic_to_openai_function(RequestStructure,
name = 'extract_information')
LangChain 验证我们提供的类。例如,它确保函数描述被指定,因为 LLM 需要它来使用这个工具。
结果是,我们得到了相同的 JSON 以传递给 LLM,但现在我们将其表示为 Pydantic 类。
{'name': 'extract_information',
'description': 'extracts information',
'parameters': {'title': 'RequestStructure',
'description': 'extracts information',
'type': 'object',
'properties': {'metric': {'title': 'Metric',
'description': "main metric we need to calculate, for example, 'number of users' or 'number of sessions'",
'type': 'string'},
'filters': {'title': 'Filters',
'description': 'filters to apply to the calculation (do not include filters on dates here)',
'type': 'string'},
'dimensions': {'title': 'Dimensions',
'description': 'parameters to split your metric by',
'type': 'string'},
'period_start': {'title': 'Period Start',
'description': 'the start day of the period for a report',
'type': 'string'},
'period_end': {'title': 'Period End',
'description': 'the end day of the period for a report',
'type': 'string'},
'output_type': {'title': 'Output Type',
'description': 'the desired output',
'enum': ['number', 'visualisation'],
'type': 'string'}},
'required': ['metric']}}
现在,我们可以在调用 OpenAI 时使用它。让我们从 OpenAI API 切换到 LangChain,使我们的 API 调用更加模块化。
定义 LangChain 链
让我们定义一个链,以从请求中提取所需的信息。我们将使用 LangChain,因为它是 LLM 最受欢迎的框架。如果你之前没有使用过它,我推荐你在我之前的一篇文章中学习一些基础知识。
我们的链很简单。它由一个 Open AI 模型和一个包含一个变量request(用户消息)的提示组成。
我们还使用了bind函数将functions参数传递给模型。bind函数允许我们为模型指定常量参数,这些参数不是输入的一部分(例如,functions或temperature)。
from langchain.prompts import ChatPromptTemplate
from langchain.chat_models import ChatOpenAI
model = ChatOpenAI(temperature=0.1, model = 'gpt-3.5-turbo-1106')\
.bind(functions = [extract_info_function])
prompt = ChatPromptTemplate.from_messages([
("system", "Extract the relevant information from the provided request. \
Extract ONLY the information presented in the initial request. \
Don't add anything else. \
Return partial information if something is missing."),
("human", "{request}")
])
extraction_chain = prompt | model
现在是尝试我们的函数的时候了。我们需要使用 invoke 方法并传递一个request。
extraction_chain.invoke({'request': "How many customers visited our site on iOS in April 2023 from different countries?"})
在输出中,我们得到了没有任何内容但有一个函数调用的AIMessage。
AIMessage(
content='',
additional_kwargs={
'function_call': {
'name': 'extract_information',
'arguments': '''{
"metric":"number of customers", "filters":"device = 'iOS'",
"dimensions":"country", "period_start":"2023-04-01",
"period_end":"2023-04-30", "output_type":"number"}
'''}
}
)
所以,我们已经学习了如何在 LangChain 中使用 OpenAI 函数来获得结构化输出。现在,让我们转到更有趣的用例——工具和路由。
用例 #2:工具与路由
现在是使用工具并赋予我们的模型外部能力的时候了。在这种方法中,模型是推理引擎,它们可以决定使用哪些工具以及何时使用(这称为路由)。
LangChain 有一个工具的概念——代理可以用来与世界互动的接口。工具可以是函数、LangChain 链或甚至其他代理。
我们可以使用format_tool_to_openai_function轻松将工具转换为 OpenAI 函数,并不断将functions参数传递给 LLM。
定义自定义工具
让我们教会我们的 LLM 驱动的分析师计算两个指标之间的差异。我们知道 LLM 在数学上可能会出错,因此我们希望要求模型使用计算器,而不是依靠自己计算。
要定义一个工具,我们需要创建一个函数并使用@tool装饰器。
from langchain.agents import tool
@tool
def percentage_difference(metric1: float, metric2: float) -> float:
"""Calculates the percentage difference between metrics"""
return (metric2 - metric1)/metric1*100
现在,这个函数有name和description参数,这些参数将传递给 LLMs。
print(percentage_difference.name)
# percentage_difference.name
print(percentage_difference.args)
# {'metric1': {'title': 'Metric1', 'type': 'number'},
# 'metric2': {'title': 'Metric2', 'type': 'number'}}
print(percentage_difference.description)
# 'percentage_difference(metric1: float, metric2: float) -> float - Calculates the percentage difference between metrics'
这些参数将用于创建 OpenAI 函数规范。让我们将我们的工具转换为 OpenAI 函数。
from langchain.tools.render import format_tool_to_openai_function
print(format_tool_to_openai_function(percentage_difference))
我们得到了以下的 JSON 作为结果。它概述了结构,但缺少字段描述。
{'name': 'percentage_difference',
'description': 'percentage_difference(metric1: float, metric2: float) -> float - Calculates the percentage difference between metrics',
'parameters': {'title': 'percentage_differenceSchemaSchema',
'type': 'object',
'properties': {'metric1': {'title': 'Metric1', 'type': 'number'},
'metric2': {'title': 'Metric2', 'type': 'number'}},
'required': ['metric1', 'metric2']}
}
我们可以使用 Pydantic 来指定参数的模式。
class Metrics(BaseModel):
metric1: float = Field(description="Base metric value to calculate the difference")
metric2: float = Field(description="New metric value that we compare with the baseline")
@tool(args_schema=Metrics)
def percentage_difference(metric1: float, metric2: float) -> float:
"""Calculates the percentage difference between metrics"""
return (metric2 - metric1)/metric1*100
现在,如果我们将新版本转换为 OpenAI 函数规范,它将包括参数描述。这样更好,因为我们可以与模型共享所有需要的上下文。
{'name': 'percentage_difference',
'description': 'percentage_difference(metric1: float, metric2: float) -> float - Calculates the percentage difference between metrics',
'parameters': {'title': 'Metrics',
'type': 'object',
'properties': {'metric1': {'title': 'Metric1',
'description': 'Base metric value to calculate the difference',
'type': 'number'},
'metric2': {'title': 'Metric2',
'description': 'New metric value that we compare with the baseline',
'type': 'number'}},
'required': ['metric1', 'metric2']}}
所以,我们已经定义了 LLM 可以使用的工具。让我们在实践中尝试一下。
实践中使用工具
让我们定义一个链,并将我们的工具传递给函数。然后,我们可以在用户请求上测试它。
model = ChatOpenAI(temperature=0.1, model = 'gpt-3.5-turbo-1106')\
.bind(functions = [format_tool_to_openai_function(percentage_difference)])
prompt = ChatPromptTemplate.from_messages([
("system", "You are a product analyst willing to help your product team. You are very strict to the point and accurate. You use only facts, not inventing information."),
("user", "{request}")
])
analyst_chain = prompt | model
analyst_chain.invoke({'request': "In April we had 100 users and in May only 95\. What is difference in percent?"})
我们得到了一个带有正确参数的函数调用,所以它正在工作。
AIMessage(content='', additional_kwargs={
'function_call': {
'name': 'percentage_difference',
'arguments': '{"metric1":100,"metric2":95}'}
}
)
为了更方便地处理输出,我们可以使用OpenAIFunctionsAgentOutputParser。让我们将它添加到我们的链中。
from langchain.agents.output_parsers import OpenAIFunctionsAgentOutputParser
analyst_chain = prompt | model | OpenAIFunctionsAgentOutputParser()
result = analyst_chain.invoke({'request': "There were 100 users in April and 110 users in May. How did the number of users changed?"})
现在,我们以更结构化的方式获得了输出,我们可以轻松地检索工具的参数,像result.tool_input。
AgentActionMessageLog(
tool='percentage_difference',
tool_input={'metric1': 100, 'metric2': 110},
log="\nInvoking: `percentage_difference` with `{'metric1': 100, 'metric2': 110}`\n\n\n",
message_log=[AIMessage(content='', additional_kwargs={'function_call': {'name': 'percentage_difference', 'arguments': '{"metric1":100,"metric2":110}'}})]
)
所以,我们可以像 LLM 请求的那样执行函数。
observation = percentage_difference(result.tool_input)
print(observation)
# 10
如果我们想从模型中获取最终答案,我们需要将函数执行结果传递回去。为此,我们需要定义一个消息列表来传递给模型观察结果。
from langchain.prompts import MessagesPlaceholder
model = ChatOpenAI(temperature=0.1, model = 'gpt-3.5-turbo-1106')\
.bind(functions = [format_tool_to_openai_function(percentage_difference)])
prompt = ChatPromptTemplate.from_messages([
("system", "You are a product analyst willing to help your product team. You are very strict to the point and accurate. You use only facts, not inventing information."),
("user", "{request}"),
MessagesPlaceholder(variable_name="observations")
])
analyst_chain = prompt | model | OpenAIFunctionsAgentOutputParser()
result1 = analyst_chain.invoke({
'request': "There were 100 users in April and 110 users in May. How did the number of users changed?",
"observations": []
})
observation = percentage_difference(result1.tool_input)
print(observation)
# 10
然后,我们需要将观察结果添加到我们的observations变量中。我们可以使用format_to_openai_functions函数以模型期望的方式格式化结果。
from langchain.agents.format_scratchpad import format_to_openai_functions
format_to_openai_functions([(result1, observation), ])
结果,我们得到了 LLM 能够理解的消息。
[AIMessage(content='', additional_kwargs={'function_call': {'name': 'percentage_difference',
'arguments': '{"metric1":100,"metric2":110}'}}),
FunctionMessage(content='10.0', name='percentage_difference')]
让我们再调用一次链,将函数执行结果作为观察结果传递。
result2 = analyst_chain.invoke({
'request': "There were 100 users in April and 110 users in May. How did the number of users changed?",
"observations": format_to_openai_functions([(result1, observation)])
})
现在,我们得到了来自模型的最终结果,听起来很合理。
AgentFinish(
return_values={'output': 'The number of users increased by 10%.'},
log='The number of users increased by 10%.'
)
如果我们使用的是原始 OpenAI Chat Completion API,我们可以只添加另一条消息,角色设置为 tool。你可以在这里找到详细的示例。
如果我们开启调试,我们可以看到传递给 OpenAI API 的确切提示。
System: You are a product analyst willing to help your product team. You are very strict to the point and accurate. You use only facts, not inventing information.
Human: There were 100 users in April and 110 users in May. How did the number of users changed?
AI: {'name': 'percentage_difference', 'arguments': '{"metric1":100,"metric2":110}'}
Function: 10.0
要开启 LangChain 调试,执行以下代码并调用你的链,看看在幕后发生了什么。
import langchain
langchain.debug = True
我们尝试使用了一个工具,但让我们扩展我们的工具包,看看 LLM 如何处理它。
路由:使用多个工具
让我们在分析工具包中再添加几个工具:
-
获取每月活跃用户
-
使用 Wikipedia。
首先,让我们定义一个虚拟函数来计算按月份和城市过滤的观众。我们将再次使用 Pydantic 来指定函数的输入参数。
import datetime
import random
class Filters(BaseModel):
month: str = Field(description="Month of customer's activity in the format %Y-%m-%d")
city: Optional[str] = Field(description="City of residence for customers (by default no filter)",
enum = ["London", "Berlin", "Amsterdam", "Paris"])
@tool(args_schema=Filters)
def get_monthly_active_users(month: str, city: str = None) -> int:
"""Returns number of active customers for the specified month"""
dt = datetime.datetime.strptime(month, '%Y-%m-%d')
total = dt.year + 10*dt.month
if city is None:
return total
else:
return int(total*random.random())
然后,让我们使用the wikipedia Python 包来允许模型查询 Wikipedia。
import wikipedia
class Wikipedia(BaseModel):
term: str = Field(description="Term to search for")
@tool(args_schema=Wikipedia)
def get_summary(term: str) -> str:
"""Returns basic knowledge about the given term provided by Wikipedia"""
return wikipedia.summary(term)
让我们定义一个包含模型目前知道的所有函数的字典。这个字典将帮助我们以后进行路由。
toolkit = {
'percentage_difference': percentage_difference,
'get_monthly_active_users': get_monthly_active_users,
'get_summary': get_summary
}
analyst_functions = [format_tool_to_openai_function(f)
for f in toolkit.values()]
我对我们之前的设置做了一些更改:
-
我稍微调整了系统提示,强制 LLM 在需要基本知识时咨询 Wikipedia。
-
我已经将模型更改为 GPT 4,因为它更适合处理需要推理的任务。
from langchain.prompts import MessagesPlaceholder
model = ChatOpenAI(temperature=0.1, model = 'gpt-4-1106-preview')\
.bind(functions = analyst_functions)
prompt = ChatPromptTemplate.from_messages([
("system", "You are a product analyst willing to help your product team. You are very strict to the point and accurate. \
You use only information provided in the initial request. \
If you need to determine some information i.e. what is the name of the capital, you can use Wikipedia."),
("user", "{request}"),
MessagesPlaceholder(variable_name="observations")
])
analyst_chain = prompt | model | OpenAIFunctionsAgentOutputParser()
我们可以调用我们的链条中的所有函数。让我们从一个非常直接的查询开始。
result1 = analyst_chain.invoke({
'request': "How many users were in April 2023 from Berlin?",
"observations": []
})
print(result1)
我们得到的结果是对 get_monthly_active_users 的函数调用,输入参数为 {'month': '2023–04–01', 'city': 'Berlin'},这看起来是正确的。模型能够找到正确的工具并解决任务。
让我们尝试将任务变得更复杂一些。
result1 = analyst_chain.invoke({
'request': "How did the number of users from the capital of Germany\
change between April and May 2023?",
"observations": []
})
让我们停下来想一想我们希望模型如何推理。显然,模型没有足够的信息来直接回答,所以它需要进行一系列函数调用:
-
调用 Wikipedia 获取德国的首都
-
调用
get_monthly_active_users函数两次,以获取 4 月和 5 月的 MAU -
调用
percentage_difference以计算指标之间的差异。
这看起来相当复杂。让我们看看 ChatGPT 是否能够处理这个问题。
在第一次调用中,LLM 返回了一个对 Wikipedia 的函数调用,参数为 {'term': 'capital of Germany'}。到目前为止,它遵循了我们的计划。
让我们提供观察结果,看看接下来的步骤会是什么。
observation1 = toolkitresult1.tool
print(observation1)
# The capital of Germany is the city state of Berlin. It is the seat of
# the President of Germany, whose official residence is Schloss Bellevue.
# The Bundesrat ("federal council") is the representation of the Federal States
# (Bundesländer) of Germany and has its seat at the former Prussian Herrenhaus
# (House of Lords). Though most of the ministries are seated in Berlin,
# some of them, as well as some minor departments, are seated in Bonn,
# the former capital of West Germany.
# Although Berlin is officially the capital of the Federal Republic of Germany,
# 8,000 out of the 18,000 total officials employed at the federal bureaucracy
# still work in Bonn, about 600 km (370 mi) away from Berlin.
# source: https://en.wikipedia.org/wiki/Capital_of_Germany
result2 = analyst_chain.invoke({
'request': "How did the number of users from the capital of Germany change between April and May 2023?",
"observations": format_to_openai_functions([(result1, observation1)])
})
模型想要执行 get_monthly_active_users,参数为 {'month': '2023–04–01', 'city': 'Berlin'}。让我们执行它,并再次将信息返回给模型。
observation2 = toolkitresult2.tool
print(observation2)
# 168
result3 = analyst_chain.invoke({
'request': "How did the number of users from the capital of Germany change between April and May 2023?",
"observations": format_to_openai_functions([(result1, observation1), (result2, observation2)])
})
然后,模型请求再次调用 get_monthly_active_users,参数为 {'month': '2023–05–01', 'city': 'Berlin'}。到目前为止,它表现得非常出色。让我们跟随它的逻辑。
observation3 = toolkitresult3.tool
print(observation3)
# 1046
result4 = analyst_chain.invoke({
'request': "How did the number of users from the capital of Germany change between April and May 2023?",
"observations": format_to_openai_functions(
[(result1, observation1), (result2, observation2),
(result3, observation3)])
})
随后的结果是一个对 percentage_difference 的函数调用,参数为 {'metric1': 168, 'metric2': 1046}。让我们计算观察结果并再次调用我们的链条。希望这将是最后一步。
observation4 = toolkitresult4.tool
print(observation4)
# 523.27
result5 = analyst_chain.invoke({
'request': "How did the number of users from the capital of Germany change between April and May 2023?",
"observations": format_to_openai_functions(
[(result1, observation1), (result2, observation2),
(result3, observation3), (result4, observation4)])
})
最终,我们从模型中得到以下响应:德国首都柏林的用户数量在 2023 年 4 月到 5 月间增加了约 523.27%。
这是针对这个问题的 LLM 调用的完整方案。

作者插图
在上述示例中,我们手动逐一触发了后续调用,但这可以很容易地自动化。
这是一个很棒的结果,我们能够看到 LLM 如何进行推理并利用多种工具。模型用了 5 步达成结果,但它遵循了我们最初制定的计划,因此这是一个相当合乎逻辑的路径。然而,如果你计划将 LLM 用于生产环境,请记住,它可能会犯错误,并引入评估和质量保证流程。
你可以在 GitHub 上找到完整的代码。
总结
本文教会了我们如何利用 OpenAI 函数为 LLMs 提供外部工具。我们已经考察了两个使用案例:提取以获得结构化输出和路由以使用外部信息回答问题。最终结果让我感到鼓舞,因为 LLM 能够使用三种不同的工具回答相当复杂的问题。
回到最初的问题,LLMs 是否能取代数据分析师。我们当前的原型仍然很基础,远未达到初级分析师的能力,但这只是开始。敬请关注!我们将深入探讨 LLM 代理的不同方法。下次,我们将尝试创建一个能够访问数据库并回答基本问题的代理。
参考
本文的灵感来源于 DeepLearning.AI 的“LangChain 中的函数、工具和代理”课程。
LLMs 能否替代数据分析师?使用 SQL 获取答案
第二部分:深入探讨 LLM 代理
·
关注 发表在 Towards Data Science ·31 分钟阅读·2023 年 12 月 22 日
--
图片由 DALL-E 3 提供
在 上一篇文章 中,我们开始构建一个由 LLM 驱动的分析师。我们决定专注于描述性分析和报告任务,因为这些任务是分析师最常见的。大多数分析师的职业生涯从这些任务开始,大多数公司也从报告和 BI 工具开始构建分析功能。
我们的第一个原型可以使用现成的工具来回答与定义指标相关的问题,如下面的示例所示。

作者插图
下一步是教我们的 LLM 驱动分析师获取任何指标。分析师通常使用 SQL 来获取数据。因此,对 LLM 分析师来说,最有用的技能是与 SQL 数据库互动。
我们已经讨论了 OpenAI 功能,并了解了 LLM 如何使用工具与世界进行集成。在这篇文章中,我想专注于 LLM 代理,并详细讨论它们。我们将学习如何使用 LangChain 构建代理,并尝试不同类型的代理。
设置数据库
首先,让我们设置一个我们将要交互的数据库。我选择的是 ClickHouse。ClickHouse 是一个开源的列式 SQL 数据库管理系统,适用于在线分析处理(OLAP)。它是大数据和分析任务的良好选择。
如果你想了解更多关于 ClickHouse 的内容,请查看我的文章。不过,你可以使用任何数据库。你只需调整获取数据的函数代码即可。
安装 ClickHouse 只需一行代码。初始命令会执行 ClickHouse 团队提供的脚本,以下载适合你平台的二进制文件。然后,你需要启动一个服务器,就完成了。
curl https://clickhouse.com/ | sh # downloads appropriate binary file
./clickhouse server # starts clickhouse server
你可以通过 HTTP API 访问 ClickHouse。默认情况下,它监听 8123 端口。
CH_HOST = 'http://localhost:8123' # default address
def get_clickhouse_data(query, host = CH_HOST, connection_timeout = 1500):
r = requests.post(host, params = {'query': query},
timeout = connection_timeout)
return r.text
我通常检查 r.status_code = 200 以确保请求已成功完成,否则会引发错误。然而,我们将把此函数的结果传递给 LLM。因此,无论输出 DB 返回什么,都没关系,无论是否为错误,LLM 都能够妥善处理。
我为这个例子生成了合成数据。如果你想了解更多关于数据模拟的内容,可以在这里找到代码。我使用了保留曲线来为客户建模会话,考虑了账户创建以来的天数和每周的季节性。这可能现在有些复杂,因为我们还不太使用数据。但我希望在未来的原型中,我们能利用 LLM 从这些数据中获得一些见解。
我们只需要几个表来表示基本的电子商务产品数据模型。我们将处理用户列表(ecommerce.users)及其会话(ecommerce.sessions)。
让我们来看一下 ecommerce.sessions 表。

作者截图
在这里,你可以看到我们为用户提供了哪些功能。

作者截图
现在,我们有了可以使用的数据,准备继续深入讨论 LLM 代理的细节。
代理概述
LLM 代理的核心思想是将 LLM 作为推理引擎来定义要执行的操作集合。在经典方法中,我们硬编码了一系列操作,但使用代理时,我们给模型提供工具和任务,让它决定如何实现这些任务。
关于 LLM 代理的最基础论文之一是“ReAct: Synergizing Reasoning and Acting in Language Models”,作者是 Shunyu Yao 等。ReAct(Reasoning + Acting)方法建议结合:
-
帮助制定计划并在出现例外情况时更新计划的推理,
-
允许模型利用外部工具或从外部来源获取数据的操作。
这种方法在不同任务上的表现更好。下面是来自论文的一个示例。

来自Yao 等人的论文的示例
实际上,这就是人类智能的工作方式:我们将内在的推理与任务导向的行动结合起来。假设你需要做晚餐。你将使用推理来制定计划(“客人将在 30 分钟内到达,我只有时间做意大利面”),调整计划(“本变成了素食者,我应该为他订购一些食物”)或决定委派任务(相当于外部工具,“意大利面没了,我需要问我的伴侣去买”)。同时,你会使用行动来使用一些工具(询问伴侣帮助或使用搅拌机)或获取一些信息(查找互联网了解煮意大利面所需的时间,以使其达到“ al dente”)。所以,使用类似的方法对 LLM 是合理的,因为它对人类有效(人类无疑是 AGI)。
现在,自 ReAct 以来,LLM 代理有许多不同的方法。它们在用于设置模型推理的提示、如何定义工具、输出格式、处理中间步骤的记忆等方面有所不同。
一些最受欢迎的方法包括:
-
OpenAI 函数,
-
AutoGPT,
-
BabyAGI,
-
计划并执行的代理。
我们稍后会使用这些方法来完成我们的任务,并查看它们的工作原理及其差异。
从零开始构建代理
让我们开始构建一个代理。我们将从零开始,以了解其内部工作原理。然后,如果你不需要任何自定义,我们将使用 LangChain 的工具来加快原型制作。
LLM 代理的核心组件包括:
-
引导模型推理的提示。
-
模型可以使用的工具。
-
记忆 — 一个将先前迭代传递给模型的机制。
对于 LLM 代理的第一个版本,我们将使用 OpenAI 函数作为构建代理的框架。
定义工具
让我们开始定义我们机器人的工具。让我们考虑一下我们的 LLM 驱动分析师可能需要哪些信息来回答问题:
-
表格中的列表 — 我们可以将其放入系统提示中,以便模型了解我们拥有的数据,并且每次都不需要执行工具。
-
表格的列列表,以便模型可以理解数据模式,
-
表格中列的前几个值,以便模型可以查找过滤器的值,
-
SQL 查询执行的结果,以便获取实际数据。
要在 LangChain 中定义工具,我们需要对函数使用 @tool 装饰器。我们将使用 Pydantic 来指定每个函数的参数模式,以便模型知道传递给函数的内容。
我们在 上一篇文章 中详细讨论了工具和 OpenAI 功能。所以如果你需要复习这个主题,请不要犹豫去阅读它。
下面的代码定义了三个工具:execute_sql、get_table_columns 和 get_table_column_distr。
from langchain.agents import tool
from pydantic import BaseModel, Field
from typing import Optional
class SQLQuery(BaseModel):
query: str = Field(description="SQL query to execute")
@tool(args_schema = SQLQuery)
def execute_sql(query: str) -> str:
"""Returns the result of SQL query execution"""
return get_clickhouse_data(query)
class SQLTable(BaseModel):
database: str = Field(description="Database name")
table: str = Field(description="Table name")
@tool(args_schema = SQLTable)
def get_table_columns(database: str, table: str) -> str:
"""Returns list of table column names and types in JSON"""
q = '''
select name, type
from system.columns
where database = '{database}'
and table = '{table}'
format TabSeparatedWithNames
'''.format(database = database, table = table)
return str(get_clickhouse_df(q).to_dict('records'))
class SQLTableColumn(BaseModel):
database: str = Field(description="Database name")
table: str = Field(description="Table name")
column: str = Field(description="Column name")
n: Optional[int] = Field(description="Number of rows, default limit 10")
@tool(args_schema = SQLTableColumn)
def get_table_column_distr(database: str, table: str, column: str, n:int = 10) -> str:
"""Returns top n values for the column in JSON"""
q = '''
select {column}, count(1) as count
from {database}.{table}
group by 1
order by 2 desc
limit {n}
format TabSeparatedWithNames
'''.format(database = database, table = table, column = column, n = n)
return str(list(get_clickhouse_df(q)[column].values))
值得注意的是,上面的代码使用了 Pydantic v1。在 2023 年 6 月,Pydantic 发布了 v2,这与 v1 不兼容。所以如果你看到验证错误,请检查你的版本。你可以在 文档 中找到有关 Pydantic 兼容性的更多详细信息。
我们将使用 OpenAI 的功能,需要转换我们的工具。此外,我已经将工具包保存在一个字典中。在执行工具以获取观察结果时,这将非常方便。
from langchain.tools.render import format_tool_to_openai_function
# converting tools into OpenAI functions
sql_functions = list(map(format_tool_to_openai_function,
[execute_sql, get_table_columns, get_table_column_distr]))
# saving tools into a dictionary for the future
sql_tools = {
'execute_sql': execute_sql,
'get_table_columns': get_table_columns,
'get_table_column_distr': get_table_column_distr
}
定义一个链
我们已经为模型创建了工具。现在,我们需要定义代理链。我们将使用最新的 GPT 4 Turbo,它也经过了微调以便与这些功能一起使用。让我们初始化一个聊天模型。
from langchain.chat_models import ChatOpenAI
llm = ChatOpenAI(temperature=0.1, model = 'gpt-4-1106-preview')\
.bind(functions = sql_functions)
下一步是定义一个由系统消息和用户问题组成的提示。我们还需要一个 MessagesPlaceholder 来设置模型将要处理的观察列表的位置。
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
system_message = '''
You are working as a product analyst for the e-commerce company.
Your work is very important, since your product team makes decisions based on the data you provide. So, you are extremely accurate with the numbers you provided.
If you're not sure about the details of the request, you don't provide the answer and ask follow-up questions to have a clear understanding.
You are very helpful and try your best to answer the questions.
All the data is stored in SQL Database. Here is the list of tables (in the format <database>.<table>) with descriptions:
- ecommerce.users - information about the customers, one row - one customer
- ecommerce.sessions - information about the sessions customers made on our web site, one row - one session
'''
analyst_prompt = ChatPromptTemplate.from_messages(
[
("system", system_message),
("user", "{question}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
]
)
正如我们讨论过的,我已经将数据库中的表列表添加到提示中,以便模型对我们的数据有至少一些了解。
我们拥有所有构建块,并准备好设置代理链。输入参数包括用户消息和中间步骤(之前的消息、函数调用和观察)。我们使用 format_to_openai_function_messages 将输入参数传递到提示中,以转换成期望的格式。然后,我们将所有内容传递给 LLM,最后使用输出解析器 OpenAIFunctionsAgentOutputParser 以方便操作。
from langchain.agents.format_scratchpad import format_to_openai_function_messages
from langchain.agents.output_parsers import OpenAIFunctionsAgentOutputParser
analyst_agent = (
{
"question": lambda x: x["question"],
"agent_scratchpad": lambda x: format_to_openai_function_messages(x["intermediate_steps"]),
}
| analyst_prompt
| llm
| OpenAIFunctionsAgentOutputParser()
)
我们已经定义了我们的主要代理链。让我们尝试调用它。我传递了一个空列表,因为开始时没有中间步骤。
analyst_agent.invoke({"question": "How many active customers from the United Kingdom do we have?",
"intermediate_steps": []})
# AgentActionMessageLog(
# tool='execute_sql',
# tool_input={'query': "SELECT COUNT(DISTINCT user_id) AS active_customers_uk FROM ecommerce.sessions WHERE country = 'United Kingdom' AND active = TRUE"},
# log='\nInvoking: `execute_sql` with `{\'query\': "SELECT COUNT(DISTINCT user_id) AS active_customers_uk FROM ecommerce.sessions WHERE country = \'United Kingdom\' AND active = TRUE"}`\n\n\n',
# message_log=[AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{"query":"SELECT COUNT(DISTINCT user_id) AS active_customers_uk FROM ecommerce.sessions WHERE country = \'United Kingdom\' AND active = TRUE"}', 'name': 'execute_sql'}})]
# )
我们得到了一个 AgentActionMessageLog 对象,这意味着模型想要调用 execute_sql 函数。当模型准备好将最终答案返回给用户时,它会返回 AgentFinish 对象。
如果我们查看 tool_input,可以看到模型想要执行以下查询。
SELECT COUNT(DISTINCT user_id) AS active_customers_uk
FROM ecommerce.sessions
WHERE country = 'United Kingdom' AND active = TRUE
查询看起来很好,但使用了错误的列名:active 而不是 is_active。有趣的是,LLM 是否能够从这个错误中恢复并返回结果。
我们可以一步一步地手动执行这些步骤,但自动化会更方便。
-
如果返回
AgentActionMessageLog对象,我们需要调用一个工具,将观察结果添加到agent_scratchpad中,并再次调用链。 -
如果我们得到了
AgentFinish对象,我们可以终止执行,因为我们已经得到了最终答案。
我还会在十次迭代后添加一个断点,以避免潜在的无限循环。
from langchain_core.agents import AgentFinish
# setting initial parameters
question = "How many active customers from the United Kingdom do we have?"
intermediate_steps = []
num_iters = 0
while True:
# breaking if there were more than 10 iterations
if num_iters >= 10:
break
# invoking the agent chain
output = analyst_agent.invoke(
{
"question": question,
"intermediate_steps": intermediate_steps,
}
)
num_iters += 1
# returning the final result if we got the AgentFinish object
if isinstance(output, AgentFinish):
model_output = output.return_values["output"]
break
# calling tool and adding observation to the scratchpad otherwise
else:
print(f'Executing tool: {output.tool}, arguments: {output.tool_input}')
observation = sql_toolsoutput.tool
print(f'Observation: {observation}')
print()
intermediate_steps.append((output, observation))
print('Model output:', model_output)
我在输出中添加了一些工具使用的日志,以查看执行情况。此外,你可以随时使用 LangChain 调试模式查看所有调用。
执行结果如下。
Executing tool: execute_sql, arguments: {'query': "SELECT COUNT(*) AS active_customers_uk FROM ecommerce.users WHERE country = 'United Kingdom' AND active = TRUE"}
Observation: Code: 47\. DB::Exception: Missing columns: 'active'
while processing query: 'SELECT count() AS active_customers_uk
FROM ecommerce.users WHERE (country = 'United Kingdom') AND (active = true)',
required columns: 'country' 'active', maybe you meant: 'country'.
(UNKNOWN_IDENTIFIER) (version 23.12.1.414 (official build))
Executing tool: get_table_columns, arguments: {'database': 'ecommerce', 'table': 'users'}
Observation: [{'name': 'user_id', 'type': 'UInt64'}, {'name': 'country', 'type': 'String'},
{'name': 'is_active', 'type': 'UInt8'}, {'name': 'age', 'type': 'UInt64'}]
Executing tool: execute_sql, arguments: {'query': "SELECT COUNT(*) AS active_customers_uk FROM ecommerce.users WHERE country = 'United Kingdom' AND is_active = 1"}
Observation: 111469
Model output: We have 111,469 active customers from the United Kingdom.
注意:不能保证代理不会对你的数据库执行 DML 操作。因此,如果在生产环境中使用,请确保 LLM 要么没有更改数据的权限,要么你的工具实现不允许这样做。
所以,模型尝试执行 SQL,但收到错误提示,没有 active 列。然后,它决定查看表的 schema,随后相应地修正了查询,并获得了结果。
这是相当不错的性能。我自己也是如此。我通常会先尝试回忆或猜测列名,只有在第一次尝试失败时才会查看文档。
但在大多数情况下,我们不需要自己编写执行代码。我们可以使用 LangChain 的 AgentExecutor 类。查看文档以了解该类的所有可能的 参数。
你只有在想要自定义某些内容时,才需要编写自己的执行器。例如,添加一些条件以终止执行或逻辑来使用工具。
你可以在下面找到使用 AgentExecutor 的相同代码。
from langchain.agents import AgentExecutor
analyst_agent_executor = AgentExecutor(
agent=analyst_agent,
tools=[execute_sql, get_table_columns, get_table_column_distr],
verbose=True,
max_iterations=10, # early stopping criteria
early_stopping_method='generate',
# to ask model to generate the final answer after stopping
)
analyst_agent_executor.invoke(
{"question": "How many active customers from the United Kingdom do we have?"}
)
结果是我们得到了一个易于追踪的输出,且结果相同。你可以注意到 LangChain 对代理输出的格式化非常方便。

图片由作者提供
我们从头开始构建了 LLM 代理。现在,我们了解它是如何工作的,并知道如何自定义它。然而,LangChain 提供了一个高级函数 initialize_agent,可以仅通过一次调用完成。你可以在 文档 中找到所有详细信息。
from langchain.agents import AgentType, Tool, initialize_agent
from langchain.schema import SystemMessage
agent_kwargs = {
"system_message": SystemMessage(content=system_message)
}
analyst_agent_openai = initialize_agent(
llm = ChatOpenAI(temperature=0.1, model = 'gpt-4-1106-preview'),
agent = AgentType.OPENAI_FUNCTIONS,
tools = [execute_sql, get_table_columns, get_table_column_distr],
agent_kwargs = agent_kwargs,
verbose = True,
max_iterations = 10,
early_stopping_method = 'generate'
)
请注意,我们传递了 ChatOpenAI 模型,但未绑定任何函数。我们单独传递了工具,因此不需要将它们链接到模型。
不同的代理类型
我们从头开始构建了一个基于 OpenAI 函数的 LLM 代理。然而,还有很多其他方法。因此,我们也来尝试一下这些方法。
我们将查看 ReAct 方法(来自我们之前讨论的论文中的初始版本)以及 LangChain 提供的几种实验方法:Plan-and-execute、BabyAGI 和 AutoGPT。
ReAct 代理
让我们从查看 ReAct 代理开始。通过当前的实现,我们可以轻松地更改代理类型,并尝试论文中描述的 ReAct 方法。
最通用的 ReAct 实现是零样本 ReAct。它不适用于我们,因为它只支持单一字符串作为输入。我们的工具需要多个参数,因此我们需要使用结构化输入 ReAct。
我们可以利用使用模块化框架的优势:我们只需更改一个参数agent = AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION,就可以了。
system_message = system_message + '''\nYou have access to the following tools:'''
agent_kwargs = {
"prefix": system_message
}
analyst_agent_react = initialize_agent(
llm = ChatOpenAI(temperature=0.1, model = 'gpt-4-1106-preview'),
agent = AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION,
tools = [execute_sql, get_table_columns, get_table_column_distr],
agent_kwargs = agent_kwargs,
verbose = True,
max_iterations = 10,
early_stopping_method = 'generate'
)
你可能会想知道如何找到可以为代理指定的参数。不幸的是,这些没有文档说明,因此我们需要深入源码来理解它。让我们一步步来讨论。
-
我们可以看到
analyst_agent_react是AgentExecutor类的一个对象。 -
这个类有一个代理字段。在我们的例子中,它是
StructuredChatAgent类的一个对象。这个类依赖于指定的代理类型。 -
让我们找到一个
StructuredChatAgent类的实现并看看它是如何工作的。在这种情况下,LangChain 创建 了一个由前缀、工具描述、格式化的指令和后缀组成的提示。 -
你可以在代码中找到你可以作为
agent_kwargs传递的所有参数的完整列表。 -
所以,我们可以从这里覆盖默认的
PREFIX值,并将其作为agent_kwargs中的prefix传递。如果你感兴趣,你可以在这里阅读默认的 ReAct 提示,并考虑如何根据你的任务对其进行调整。
如果你感兴趣,你可以使用以下调用查看最终提示。
for message in analyst_agent_react.agent.llm_chain.prompt.messages:
print(message.prompt.template)
让我们调用我们的方法,看看结果。
analyst_agent_react.run("How many active customers from the United Kingdom do we have?")
我们可以注意到模型采用了稍微不同的推理框架。模型开始时写下思考(推理),然后转到动作(函数调用)和观察(函数调用的结果)。然后,迭代重复。最后,模型返回动作 = 最终答案。
> Entering new AgentExecutor chain...
Thought: To answer this question, I need to define what is meant by
"active customers" and then query the database for users from
the United Kingdom who meet this criteria. I will first need to know
the structure of the `ecommerce.users` table to understand what columns
are available that could help identify active customers and their location.
Action:
{
"动作": "获取表列",
"动作输入": {
"数据库": "电子商务",
"表": "用户"
}
}
Observation: [{'name': 'user_id', 'type': 'UInt64'}, {'name': 'country', 'type': 'String'}, {'name': 'is_active', 'type': 'UInt8'}, {'name': 'age', 'type': 'UInt64'}]
Thought:The `ecommerce.users` table contains a column named `is_active`
which likely indicates whether a customer is active or not, and a `country`
column which can be used to filter users by their location. Since we
are interested in active customers from the United Kingdom, we can use
these two columns to query the database.
Action:
{
"动作": "执行 SQL",
"动作输入": {
"查询": "SELECT COUNT(*) AS active_customers_uk FROM ecommerce.users WHERE country = 'United Kingdom' AND is_active = 1"
}
}
Observation: 111469
Thought:I have the information needed to respond to the user's query.
Action:
{
"动作": "最终答案",
"动作输入": "来自英国的活跃客户有 111,469 个。"
}
> Finished chain.
'There are 111,469 active customers from the United Kingdom.'
尽管模型采用了不同的路径(从理解表格模式开始,然后执行 SQL),但它得出了相同的结果。
现在,让我们继续实验方法。在 LangChain 中,有实验性的代理类型。它们尚不建议用于生产。然而,尝试使用它们并查看它们的工作效果会很有趣。
计划与执行代理
下面的代码基于来自 LangChain 厨房书的 示例 。
该代理遵循“计划与执行”方法,与我们之前查看的“行动”代理不同。这一方法受 BabyAGI 框架和 论文《计划与解决提示》 的启发。
这种方法的特点是代理首先尝试规划下一步,然后执行它们。
这种方法有两个组成部分:
-
规划器 — 一个常规的大型语言模型,其主要目标是进行推理和规划,
-
执行者 — 行动代理,是一个拥有可以用来执行的工具集合的 LLM。
这种方法的优势在于你有一个分离:一个模型专注于规划(推理),另一个模型专注于执行(行动)。它更具模块化,可能你可以使用针对特定任务微调的小型且便宜的模型。然而,这种方法也会产生更多的 LLM 调用,因此如果我们使用 ChatGPT,会更昂贵。
让我们初始化规划器和执行者。
from langchain_experimental.plan_and_execute import PlanAndExecute, load_agent_executor, load_chat_planner
model = ChatOpenAI(temperature=0.1, model = 'gpt-4-1106-preview')
planner = load_chat_planner(model)
executor = load_agent_executor(model,
tools = [execute_sql, get_table_columns, get_table_column_distr],
verbose=True)
目前没有办法为执行者指定自定义提示,因为你不能将其传递给 该函数。然而,我们可以破解提示,将我们的初始系统消息添加到默认提示的开头,以提供一些关于任务的背景。
免责声明:覆盖对象字段是一种不好的做法,因为我们可能会绕过一些提示验证。我们现在这样做只是为了实验这种方法。这样的解决方案不适用于生产。
executor.chain.agent.llm_chain.prompt.messages[0].prompt.template = system_message + '\n' + \
executor.chain.agent.llm_chain.prompt.messages[0].prompt.template
现在,是时候定义一个代理并执行我们之前询问的相同查询了。
analyst_agent_plan_and_execute = PlanAndExecute(
planner=planner,
executor=executor
)
analyst_agent_plan_and_execute.run("How many active customers from the United Kingdom do we have?")
调用返回了一个错误:RateLimitError: Error code: 429 — {'error': {'message': '请求超出 gpt-4–1106-preview 组织内基于令牌使用的每分钟限制:限制 150000,要求 235832。', 'type': 'tokens_usage_based', 'param': None, 'code': 'rate_limit_exceeded'}}。看起来模型尝试向 OpenAI 发送了过多的令牌。
让我们通过查看模型的输出(可以在下面找到)来尝试理解发生了什么:
-
首先,模型决定查看
ecommerce.users和ecommerce.sessions列,以确定“活跃”客户的标准。 -
它意识到需要在
ecommerce.users表中使用is_active。然而,模型决定还应该使用会话数据来定义客户的活动。 -
然后,模型深入挖掘试图定义
ecommerce.sessions中近期活动的标准。它查看了action_date、session_duration和revenue的分布情况。 -
最终,它将活跃客户定义为在过去 30 天内有过会话的客户,并且会话时长和收入高于某些阈值,而忽略了可以直接使用
is_active。模型计划查看数据以定义这些阈值。 -
最终错误是由于尝试加载所有客户的 user_ids,因为 ChatGPT 的令牌太多。
尽管有很多文本,我鼓励你阅读一下,看看模型是如何思考问题的。
> Entering new AgentExecutor chain...
Action:
{
"action": "Final Answer",
"action_input": "有关客户的数据存储在 SQL 数据库中,特别是在名为'ecommerce.users'的表中。"
}
> Finished chain.
> Entering new AgentExecutor chain...
Thought: To determine the criteria for an "active" customer, I need
to understand what data is available in the 'ecommerce.users' and
'ecommerce.sessions' tables. This will help me identify which columns
could be used to define an "active" customer based on recent purchases,
account activity, or other engagement metrics.
Action:
{
"action": "get_table_columns",
"action_input": {
"database": "ecommerce",
"table": "users"
}
}
Observation: [{'name': 'user_id', 'type': 'UInt64'}, {'name': 'country', 'type': 'String'}, {'name': 'is_active', 'type': 'UInt8'}, {'name': 'age', 'type': 'UInt64'}]
Thought:The 'ecommerce.users' table contains columns for 'user_id', 'country',
'is_active', and 'age'. The 'is_active' column seems to directly indicate
whether a user is considered active or not. However, to understand
the criteria behind this designation, we need to see if there are
any engagement metrics in the 'ecommerce.sessions' table that could
correlate with the 'is_active' status.
Action:
{
"action": "get_table_columns",
"action_input": {
"database": "ecommerce",
"table": "sessions"
}
}
Observation: [{'name': 'user_id', 'type': 'UInt64'}, {'name': 'session_id', 'type': 'UInt64'}, {'name': 'action_date', 'type': 'Date'}, {'name': 'session_duration', 'type': 'UInt64'}, {'name': 'os', 'type': 'String'}, {'name': 'browser', 'type': 'String'}, {'name': 'is_fraud', 'type': 'UInt8'}, {'name': 'revenue', 'type': 'Float32'}]
Thought:The 'ecommerce.sessions' table includes columns such as 'user_id',
'session_id', 'action_date', 'session_duration', 'os', 'browser', 'is_fraud',
and 'revenue'. The 'action_date' could be used to determine recent activity,
and 'session_duration' might indicate engagement level. 'Revenue' could
be used to identify recent purchases. To define an "active" customer,
we could consider users with recent 'action_date', longer 'session_duration',
and positive 'revenue'. However, to set specific criteria, we might need
to understand the distribution of these values, such as
the most recent 'action_date' or average 'session_duration'.
Let's first check the distribution of 'action_date' to see how recent
the sessions are.
Action:
{
"action": "get_table_column_distr",
"action_input": {
"database": "ecommerce",
"table": "sessions",
"column": "action_date",
"n": 10
}
}
Observation: ['2023-12-25', '2023-12-26', '2023-12-27', '2023-12-18', '2023-12-28', '2023-12-21', '2023-12-20', '2023-12-04', '2023-12-19', '2023-12-14']
Thought:The distribution of 'action_date' shows recent dates, with
the latest being '2023-12-28'. This suggests that the data includes
very recent sessions. To define an "active" customer, we could consider
a timeframe such as the last 30 days. However, before finalizing
this criterion, it would be beneficial to also understand
the distribution of 'session_duration' and 'revenue' to see if there
are common thresholds that could further define an "active" customer.
Let's check the distribution of 'session_duration' next.
Action:
{
"action": "get_table_column_distr",
"action_input": {
"database": "ecommerce",
"table": "sessions",
"column": "session_duration",
"n": 10
}
}
Observation: [772, 860, 887, 678, 405, 671, 670, 707, 982, 907]
Thought:The distribution of 'session_duration' shows a range of values,
with durations varying from 405 to 982 seconds. These values could
be used to set a threshold for engagement level, but without context,
it's difficult to determine what would be considered a high or
low session duration. To get a better understanding, we should look
at the distribution of 'revenue' to see if there's a common revenue
threshold that could be used in conjunction with 'action_date' and
'session_duration' to define an "active" customer. Let's check
the distribution of 'revenue' next.
Action:
{
"action": "get_table_column_distr",
"action_input": {
"database": "ecommerce",
"table": "sessions",
"column": "revenue",
"n": 10
}
}
Observation: [0.0, 1556.9, 7803.2, 4936.5, 619.6, 9947.7, 646.2, 3251.1, 2860.9, 4376.5]
Thought:The distribution of 'revenue' shows a range of values including zero,
which could indicate sessions without purchases, and values up to 9947.7,
which suggests significant purchases. To define an "active" customer,
we could consider those with non-zero revenue within the last 30 days
and with a session duration above a certain threshold. However, to set
a precise threshold for 'session_duration' and 'revenue', we would need
to analyze the data further, possibly calculating averages or percentiles.
For now, we can propose a preliminary definition of an "active" customer
as one who has had a session within the last 30 days, with a session duration
and revenue above certain thresholds to be determined.
Action:
{
"action": "Final Answer",
"action_input": "根据'ecommerce.users'和'ecommerce.sessions'表中可用的数据,可以初步定义一个'活跃'客户为在过去 30 天内有过会话的客户,其会话时长和收入高于某些阈值。'users'表中的'is_active'列可能已经反映了这种定义或类似定义,但需要进一步分析以设定'session_duration'和'revenue'的具体阈值。这些阈值可以通过计算数据分布的平均值或百分位数来确定。"
}
> Finished chain.
> Entering new AgentExecutor chain...
Action:
{
"action": "get_table_columns",
"action_input": {
"database": "ecommerce",
"table": "users"
}
}
Observation: [{'name': 'user_id', 'type': 'UInt64'}, {'name': 'country', 'type': 'String'}, {'name': 'is_active', 'type': 'UInt8'}, {'name': 'age', 'type': 'UInt64'}]
Thought:The 'ecommerce.users' table contains a 'country' column which
can be used to filter the customer records based on the location
being the United Kingdom. I will write and execute an SQL query
to retrieve the user IDs of customers located in the United Kingdom.
Action:
{
"action": "execute_sql",
"action_input": {
"query": "SELECT user_id FROM ecommerce.users WHERE country = 'United Kingdom'"
}
}
Observation:
1000001
1000011
1000021
1000029
1000044
... <many more lines...>
这是一个很好的例子,说明代理在问题上过于复杂化并进入了过多细节。人类分析师也会不时犯这样的错误。因此,看到 LLM 行为中的类似模式是很有趣的。
如果我们反思一下如何潜在地解决这个问题,有几种方法:
-
首先,我们可以防止在尝试从数据库获取过多数据时出现问题,如果
execute_sql函数的输出超过 1K 行,则返回错误。 -
另一个我会考虑的是允许 LLM 提出后续问题,并指示它不要做出假设。
让我们继续研究启发当前方法的 BabyAGI 方法。
带工具的 BabyAGI 代理
下面的代码基于 示例 来自 LangChain 的食谱。
与之前的方法类似,我们的另一个实验方法 BabyAGI 尝试先计划然后执行。
这种方法使用检索,因此我们需要设置向量存储和嵌入模型。我使用开源且轻量级的 Chroma 进行存储和 OpenAI 嵌入。
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import Chroma
embedding = OpenAIEmbeddings()
persist_directory = 'vector_store'
vectordb = Chroma(
persist_directory=persist_directory,
embedding_function=embedding
)
检索允许模型长期存储所有结果,并提取和传递仅最相关的结果。如果你想了解更多关于检索的内容,请阅读 我关于 RAG 的文章(检索增强生成)。
首先,我们将创建一个待办事项链,稍后将用作我们执行器的工具。
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
todo_prompt_message = '''
You are a planner who is an expert at coming up with a todo list for
a given objective. Come up with a todo list for this objective: {objective}
'''
todo_prompt = PromptTemplate.from_template(todo_prompt_message)
todo_chain = LLMChain(llm=OpenAI(temperature=0.1,
model = 'gpt-4-1106-preview'), prompt=todo_prompt)
然后,我们将创建一个指定工具和提示的代理。
from langchain.agents import AgentExecutor, Tool, ZeroShotAgent
from langchain.prompts import PromptTemplate
tools = [
execute_sql,
get_table_columns,
get_table_column_distr,
Tool(
name="TODO",
func=todo_chain.run,
description="useful for when you need to come up with todo lists. Input: an objective to create a todo list for. Output: a todo list for that objective. Please be very clear what the objective is!",
)
]
prefix = """
You are an AI who performs one task based on the following objective: {objective}. Take into account these previously completed tasks: {context}.
You are asked questions related to analytics for e-commerce product.
Your work is very important, since your product team makes decisions based on the data you provide. So, you are extremely accurate with the numbers you provided.
If you're not sure about the details of the request, you don't provide the answer and ask follow-up questions to have a clear understanding.
You are very helpful and try your best to answer the questions.
All the data is stored in SQL Database. Here is the list of tables (in the format <database>.<table>) with descriptions:
- ecommerce.users - information about the customers, one row - one customer
- ecommerce.sessions - information about the sessions customers made on our web site, one row - one session
"""
suffix = """Question: {task}
{agent_scratchpad}"""
prompt = ZeroShotAgent.create_prompt(
tools,
prefix=prefix,
suffix=suffix,
input_variables=["objective", "task", "context", "agent_scratchpad"],
)
llm = OpenAI(temperature=0.1)
llm_chain = LLMChain(llm=llm, prompt=prompt)
tool_names = [tool.name for tool in tools]
analyst_agent_babyagi = ZeroShotAgent(llm_chain=llm_chain, allowed_tools=tool_names)
analyst_agent_babyagi_executor = AgentExecutor.from_agent_and_tools(
agent=analyst_agent_babyagi, tools=tools, verbose=True
)
最后一步是定义 BabyAGI 执行器并运行它。
from langchain_experimental.autonomous_agents import BabyAGI
baby_agi = BabyAGI.from_llm(
llm=llm,
vectorstore=vectordb,
task_execution_chain=analyst_agent_babyagi_executor,
verbose=True,
max_iterations=10
)
baby_agi("Find, how many active customers from the United Kingdom we have.")
再次,模型未能返回结果,因为它无法遵循工具的输入模式。
而且,令人惊讶的是,模型决定不使用待办事项功能来创建待办列表,而是直接跳入 SQL 查询。然而,第一个查询不正确。模型尝试恢复并调用get_table_columns函数以获取列名,但未能遵循模式。
让我们查看日志。
*****TASK LIST*****
1: Make a todo list
*****NEXT TASK*****
1: Make a todo list
> Entering new AgentExecutor chain...
Thought: I need to find out how many active customers from the United Kingdom
we have
Action: execute_sql
Action Input: SELECT COUNT(*) FROM ecommerce.users WHERE country = 'UK' AND active = 1
Observation: Code: 47\. DB::Exception: Missing columns: 'active' while processing query:
'SELECT count() FROM ecommerce.users WHERE (country = 'UK') AND (active = 1)',
required columns: 'country' 'active', maybe you meant: 'country'.
(UNKNOWN_IDENTIFIER) (version 23.12.1.414 (official build))
Thought: I need to get the columns of the ecommerce.users table
Action: get_table_columns
Action Input: ecommerce.users
所以,我们看到另一个问题,这在未使用 OpenAI 函数的代理中非常常见——它们无法遵循结构。
带工具的 AutoGPT 代理
下面的代码基于 示例 来自 LangChain 的食谱。
让我们看看另一种实验方法——使用 LangChain 框架实现的 AutoGPT。
再次,我们需要为中间步骤设置向量存储。
embedding = OpenAIEmbeddings()
from langchain.vectorstores import Chroma
persist_directory = 'autogpt'
vectordb = Chroma(
persist_directory=persist_directory,
embedding_function=embedding
)
在这种情况下,我们再次无法向模型指定任何提示。让我们尝试在没有任何具体指导的情况下使用它。但我们将添加 get_tables 工具,以便模型可以查看所有可用的表。我希望这能帮助模型编写正确的 SQL 查询。
@tool()
def get_tables() -> str:
"""Returns list of tables in the format <database>.<table>"""
return ['ecommerce.users', 'ecommerce.sessions']
让我们创建一个 AutoGPT 代理。这只需一个函数调用即可完成。然后,我们执行它,看看它是如何工作的。
from langchain_experimental.autonomous_agents import AutoGPT
analyst_agent_autogpt = AutoGPT.from_llm_and_tools(
ai_name="Harry",
ai_role="Assistant",
tools= [execute_sql, get_table_columns,
get_table_column_distr, get_tables],
llm=ChatOpenAI(temperature=0.1, model = 'gpt-4-1106-preview'),
memory=vectordb.as_retriever(),
)
analyst_agent_autogpt.chain.verbose = True
analyst_agent_autogpt.run(["Find how many active customers from the United Kingdom we have."])
模型能够给出正确的答案:“来自英国的活跃客户数量是 111,469。”
阅读提示是有趣的,因为我们使用了默认提示。你可以通过 analyst_agent_autogpt.chain.prompt 访问它。
System: You are Harry, Assistant
Your decisions must always be made independently without seeking user
assistance.
Play to your strengths as an LLM and pursue simple strategies with
no legal complications.
If you have completed all your tasks, make sure to use the "finish" command.
GOALS:
1\. Find how many active customers from the United Kingdom we have.
Constraints:
1\. ~4000 word limit for short term memory. Your short term memory is short,
so immediately save important information to files.
2\. If you are unsure how you previously did something or want to recall
past events, thinking about similar events will help you remember.
3\. No user assistance
4\. Exclusively use the commands listed in double quotes e.g. "command name"
Commands:
1\. execute_sql: execute_sql(query: str) -> str - Returns the result of SQL query execution, args json schema: {"query": {"title": "Query", "description": "SQL query to execute", "type": "string"}}
2\. get_table_columns: get_table_columns(database: str, table: str) -> str - Returns list of table column names and types in JSON, args json schema: {"database": {"title": "Database", "description": "Database name", "type": "string"}, "table": {"title": "Table", "description": "Table name", "type": "string"}}
3\. get_table_column_distr: get_table_column_distr(database: str, table: str, column: str, n: int = 10) -> str - Returns top n values for the column in JSON, args json schema: {"database": {"title": "Database", "description": "Database name", "type": "string"}, "table": {"title": "Table", "description": "Table name", "type": "string"}, "column": {"title": "Column", "description": "Column name", "type": "string"}, "n": {"title": "N", "description": "Number of rows, default limit 10", "type": "integer"}}
4\. get_tables: get_tables() -> str - Returns list of tables in the format <database>.<table>, args json schema: {}
5\. finish: use this to signal that you have finished all your objectives, args: "response": "final response to let people know you have finished your objectives"
Resources:
1\. Internet access for searches and information gathering.
2\. Long Term memory management.
3\. GPT-3.5 powered Agents for delegation of simple tasks.
4\. File output.
Performance Evaluation:
1\. Continuously review and analyze your actions to ensure you are
performing to the best of your abilities.
2\. Constructively self-criticize your big-picture behavior constantly.
3\. Reflect on past decisions and strategies to refine your approach.
4\. Every command has a cost, so be smart and efficient. Aim to complete
tasks in the least number of steps.
You should only respond in JSON format as described below
Response Format:
{
"thoughts": {
"text": "thought",
"reasoning": "reasoning",
"plan": "- short bulleted\n- list that conveys\n- long-term plan",
"criticism": "constructive self-criticism",
"speak": "thoughts summary to say to user"
},
"command": {
"name": "command name",
"args": {
"arg name": "value"
}
}
}
Ensure the response can be parsed by Python json.loads
System: The current time and date is Sat Dec 16 18:39:49 2023
System: This reminds you of these events from your past:
[]
所以,我们可以看到这个模型通过向量存储具有长期记忆。此外,它还具备全面的推理,包括思考、理由、计划、批评和总结。
让我们看看模型的推理(我只过滤了模型的回答)。所以,它遵循了与之前成功的代理相同的方法。
{
"thoughts": {
"text": "To find the number of active customers from the United Kingdom, I need to identify the relevant database and table that contains customer information. Once I have that, I can look for columns that might indicate customer activity and country. The first step is to list all the available tables.",
"reasoning": "Understanding the structure of the database is essential before I can execute a query to find the active customers. Listing tables is a logical first step to identify where customer data might be stored.",
"plan": "- Use the get_tables command to list all available tables.\n- Identify the table that likely contains customer information.\n- Use get_table_columns to understand the structure of the customer table.\n- Execute an SQL query to count active UK customers.",
"criticism": "I need to ensure that I'm efficient with the commands I use, aiming to gather the necessary information in as few steps as possible.",
"speak": "I am now going to list all the available tables to find where customer information is stored."
},
"command": {
"name": "get_tables",
"args": {}
}
}
{
"thoughts": {
"text": "The 'ecommerce.users' table is likely to contain information about the customers. To find the number of active customers from the United Kingdom, I need to understand the structure of this table, specifically looking for columns that might indicate whether a customer is active and their country.",
"reasoning": "Knowing the structure of the 'users' table will help me determine which columns to use in an SQL query to count active UK customers.",
"plan": "- Use the get_table_columns command to get the structure of the 'ecommerce.users' table.\n- Identify columns that indicate customer activity and country.\n- Formulate an SQL query to count active UK customers based on the identified columns.",
"criticism": "I must continue to be efficient with my commands, ensuring that each step brings me closer to the goal without unnecessary actions.",
"speak": "I will now retrieve the structure of the 'ecommerce.users' table to identify the relevant columns for active customers and their country."
},
"command": {
"name": "get_table_columns",
"args": {
"database": "ecommerce",
"table": "users"
}
}
}
{
"thoughts": {
"text": "With the structure of the 'ecommerce.users' table known, I can see that there are columns for 'country' and 'is_active', which are necessary to identify active customers from the United Kingdom.",
"reasoning": "The 'country' column will allow me to filter customers by their country, and the 'is_active' column will allow me to filter customers by their activity status. With this information, I can construct an SQL query to count the number of active customers from the UK.",
"plan": "- Execute an SQL query using the 'execute_sql' command to count the number of active customers from the United Kingdom.\n- The query will count rows where 'country' is 'United Kingdom' and 'is_active' is true (1).",
"criticism": "I need to ensure that the SQL query is correctly formulated to retrieve the exact information needed without any errors.",
"speak": "I am now ready to execute an SQL query to count the number of active customers from the United Kingdom."
},
"command": {
"name": "execute_sql",
"args": {
"query": "SELECT COUNT(*) FROM ecommerce.users WHERE country = 'United Kingdom' AND is_active = 1"
}
}
}
{
"thoughts": {
"text": "I have successfully executed the SQL query and obtained the number of active customers from the United Kingdom, which is 111,469.",
"reasoning": "The goal was to find the number of active customers from the UK, and the SQL query has provided this information.",
"plan": "- Since the goal has been achieved, the next step is to use the 'finish' command to signal that I have completed all my objectives.",
"criticism": "There is no need for further action or commands as the objective has been met.",
"speak": "I have found the number of active customers from the United Kingdom and am ready to finish the task."
},
"command": {
"name": "finish",
"args": {
"response": "The number of active customers from the United Kingdom is 111,469."
}
}
}
总体来说,AutoGPT 看起来是一种可行的方法。然而,我仍然相信,目前构建代理的最可靠方法是通过 OpenAI 的函数。
我们需要从头开始构建一切吗?
我们在构建集成了 SQL 数据库的代理时度过了一段美好的学习时光。然而,我必须提到 LangChain 有自己实现的 SQL 代理——SQLDatabaseChain。
这种方法使用 SQL Alchemy 与数据库进行交互。因此,我们需要安装clickhouse-sqlalchemy包以连接到 ClickHouse。
pip install clickhouse-sqlalchemy
我们可以建立与数据库的连接并初始化工具包。
uri = 'clickhouse+native://localhost/ecommerce'
db = SQLDatabase.from_uri(uri)
toolkit = SQLDatabaseToolkit(db=db, llm=OpenAI(temperature=0))
工具包是与某个主题相关的有用工具的集合。你可以在文档中找到许多示例。
我们可以看到工具包中拥有的工具列表。这里有用于进行 SQL 查询或获取与数据库相关信息的工具。
toolkit.get_tools()
然后,我们可以快速创建并运行一个基于 OpenAI 函数的代理。
agent_executor = create_sql_agent(
llm=ChatOpenAI(temperature=0.1, model = 'gpt-4-1106-preview'),
toolkit=toolkit,
verbose=True,
agent_type=AgentType.OPENAI_FUNCTIONS
)
agent_executor.run("How many active customers from the United Kingdom do we have?")
我们在这边没有多大麻烦就得到了正确的答案。
> Entering new AgentExecutor chain...
Invoking: `sql_db_list_tables` with ``
sessions, users
Invoking: `sql_db_schema` with `users`
CREATE TABLE users (
user_id UInt64,
country String,
is_active UInt8,
age UInt64
) ENGINE = Log
/*
3 rows from users table:
user_id country is_active age
1000001 United Kingdom 0 70
1000002 France 1 87
1000003 France 1 88
*/
Invoking: `sql_db_query` with `SELECT COUNT(*) FROM users WHERE country = 'United Kingdom' AND is_active = 1`
[(111469,)]We have 111,469 active customers from the United Kingdom.
> Finished chain.
'We have 111,469 active customers from the United Kingdom.'
我们可以使用langchain.debug = True来查看使用了什么提示。
System: You are an agent designed to interact with a SQL database.
Given an input question, create a syntactically correct clickhouse query
to run, then look at the results of the query and return the answer.
Unless the user specifies a specific number of examples they wish to obtain,
always limit your query to at most 10 results.
You can order the results by a relevant column to return the most interesting
examples in the database.
Never query for all the columns from a specific table, only ask for
the relevant columns given the question.
You have access to tools for interacting with the database.
Only use the below tools. Only use the information returned
by the below tools to construct your final answer.
You MUST double check your query before executing it. If you get
an error while executing a query, rewrite the query and try again.
DO NOT make any DML statements (INSERT, UPDATE, DELETE, DROP etc.)
to the database.
If the question does not seem related to the database, just return
"I don't know" as the answer.
Human: How many active customers from the United Kingdom do we have?
AI: I should look at the tables in the database to see what I can query.
Then I should query the schema of the most relevant tables.
因此,我们有了一个相当方便且有效的 SQL 分析实现。如果你不需要任何自定义更改,你可以直接使用 LangChain 的实现。
此外,你可以稍作调整,例如,通过将提示传递给create_sql_agent函数(文档)。
摘要
今天,我们学习了如何创建不同类型的代理。我们实现了一个完全从零开始的 LLM 驱动代理,可以处理 SQL 数据库。然后,我们利用高层次的 LangChain 工具,通过几个函数调用实现了相同的结果。
所以,现在我们的 LLM 驱动分析师可以使用你的数据库中的数据并回答问题。这是一个重大改进。我们可以将 SQL 数据库代理作为工具添加到我们的 LLM 驱动分析师中。这将是我们的第一个技能。
代理现在可以回答与数据相关的问题并自行工作。然而,分析工作的基石是合作。因此,在接下来的文章中,我们将添加记忆功能和学习代理,以便提出后续问题。敬请关注!
非常感谢你阅读这篇文章。我希望它对你有所启发。如果你有任何后续问题或评论,请在评论区留言。



















浙公网安备 33010602011771号