TowardsDataScience-2023-博客中文翻译-三十五-
TowardsDataScience 2023 博客中文翻译(三十五)
产品实验中的陷阱
原文:
towardsdatascience.com/pitfalls-in-product-experimentation-145d12bb139f

图片来自 Freepik (www.freepik.com)
常见的禁忌事项往往被忽视,这些在产品实验中导致结果不佳且不可靠
·发布于 Towards Data Science ·8 分钟阅读·2023 年 1 月 17 日
--
我们都知道产品实验很重要,其好处已经被许多组织证明,它使得基于数据的产品、功能和流程决策成为可能。谷歌测试了 40 种蓝色,而正确的蓝色使其收入增加了 2 亿美元。Booking.com 已经承认 组织的扩展和转型是通过在这里进行的众多测试和实验得以实现的。
然而,产品实验和其他统计测试或实验一样,容易出现陷阱。这些陷阱可能是设计和/或执行上的缺陷,在整个过程中可能被隐藏或未被察觉。数据团队——产品数据分析师/数据科学家——有责任保障实验执行和分析的可靠性。因此,了解常见陷阱及其处理方式非常重要,因为这些陷阱可能误导分析结果和结论。
如果实验没有被正确配置和分析,可能会导致结果不佳且不可靠,违背了实验最初的目的——即测试处理方法并评估其影响。
配置陷阱
在查看统计策略和分析之前,确保整体实验的规划和设计是正确的至关重要。尽管这些看起来很基础,但很有可能被忽视(再说一次,因为它非常基础),如果处理不当,最终会使我们错过实验。
-
优化错误指标。 指标选择决定了处理变更是否被推广的整体决策。作为经验法则,实验的指标理想上应该是与业务相关且受处理影响/可移动的。 (1) 如果这个指标上升/下降,你会感到高兴吗? (2) 假设你是接受了处理的用户,你会做或不做会影响指标的活动吗?
-
未充分利用变体潜力。 在理论世界中,A/B 测试(或分割测试)是一个常用术语。它是比较两个版本以找出哪个表现更好。在实际世界中,这可以进一步扩展到两个以上的版本(A/B/n 测试)或测试变量组合(多变量测试)。拥有更多的变体对于最大化资源利用和获取最佳决策选项的可能性非常有帮助。它们带来了一些副统计效应(例如样本量需求增加;家庭误差率),但仍然值得探索。
-
重叠实验。 在组织内可能会同时进行许多实验。当这些不同的实验在相似的特征上运行时,可能会出现问题,因为它们可能会相互干扰——影响到重叠用户子集上的相同指标。实验的指标增加可能实际上不是单纯来自于处理本身,而是来自于重叠实验中的另一个处理。全组织协调(从实验时间到目标分配)可以帮助最小化这个问题。
-
直接全面推广。 可能会很诱人立即将实验全面推广,以最小化所需时间并尽快获得结果。然而,实验变更仍然是“产品发布”,中间可能会出现问题。建议以分阶段推广的方式进行实验,以降低这些发布的风险。

图片由 pch.vector 提供于 Freepik (www.freepik.com)
拥有一个产品实验*台可以成为预防这些陷阱的潜在解决方案,确保实施标准化的指标和最佳实践。
统计陷阱
产品实验是持续测试假设以改进您的产品的过程。假设测试本身本质上是一种统计推断,因此需要遵循统计原则以正确进行产品实验。
根据产品的背景和使用情况,实验可能在统计上更复杂,需要注意一些额外的措施。以下是一些常见的问题。
实验“窥探”
在进行实验时,直接在部署后很快查看结果并得出(过早的)结论是非常诱人的,特别是当结果看起来良好或与我们的假设一致时。这被称为实验“窥探”问题。
实验“窥探”发生在结果在达到适当的样本大小之前被错误地宣布*。即使初始结果显示统计显著性,这种推断也可能是纯粹出于偶然**,如果在达到适当样本大小之前做出的推断,则是有缺陷的。
解决这个问题的理想方法是在测试开始时确认样本大小,并在达到该样本值之前推迟任何结论。然而,在某些情况下,达到足够的样本大小可能需要很长时间,并且变得不实际。在这种情况下,可以探索的一种技术是顺序测试,其中最终样本大小根据我们在测试过程中观察到的数据而动态调整。因此,如果我们在开始时观察到更极端的结果,测试可以提前结束。

由Hexandcube拍摄,图片来源于Unsplash
没有设定正确的零假设
在产品实验中,我们设定一个零假设进行测试——即是否被拒绝——并给予处理。一个常见的经典零假设是在分析的数据集(对照组与处理组)之间没有兴趣变量的差异。这被称为优越性测试,其中我们期望处理组和对照组之间有一些优越的差异——期待处理组在兴趣变量(例如均值、比例)上有正向变化,以便继续实施处理。
这方面的一个替代方法是非劣性检验,在这种检验中,只要测试变体没有比对照组差得太多,我们就有理由实施该变体。该检验中的原假设是“变体中的兴趣变量比对照组差 X%或更多”。在此检验中,只要表现不如对照组的变体仍在“可接受范围”内,我们可以继续实施该处理。

优越性与非劣性检验的示意图(图示作者)
非劣性检验对于可能带来一些负面影响的变更(即测试移除某个功能对预订转化的影响)或检查实验中的次要指标是否可以接受降低到某个阈值以提高主要指标,都是有用的。
污染
常用的假设检验——z 检验和 t 检验——假设数据是从一个独立的正态分布中抽样的。假设大多数情况下,通过确保随机化和非重复分配可以很容易地满足这一条件,但在某些情况下可能会比较棘手。
例如,关于在按需配送应用中实验配送定价。虽然处理是针对选定的用户,但由于配送车队在区域内共享(而不是按客户分配),也可能对非处理组产生影响。这被称为污染或网络效应,其中实验的不同处理相互干扰。
一种常见的解决方案是使用“切换实验”。在这种情况下,实验中的所有用户将接触到相同的体验,随机化发生在时间间隔和区域(或其他可以隔离处理效应的粒度)上。然后,感兴趣的指标将跨时间间隔进行*均。
多重比较问题
多重比较问题是统计学中的一个著名问题。当同时考虑一组统计推断或推断基于观察值选择的参数子集时,就会发生这种问题。
例如,我们正在对电子商务*台的新 UI 页面(处理组)与旧 UI 页面(对照组)进行实验。除了主要测试预订转化的影响外,我们还检查了许多其他(不太相关的)指标,如搜索框点击次数、每个类别的点击次数、会话时长、优惠券使用率等。随着比较的属性增多,仅由于随机 抽样误差 ,处理组和对照组在至少一个属性上出现差异的可能性也越来越大。
为了在统计上控制这个问题,可以采用一些方法,例如 邦费罗尼校正,它降低了判断结果显著性所需的 p 值阈值。

图片由 Clay Banks 提供,来源于 Unsplash。
提升到一个新层次
上述常见陷阱之外,产品实验结果可能仍然不够理想和可靠。在分析实验结果时,还需考虑和牢记一些警示点。
-
新颖效应。当产品中引入一些变化时,用户通常会对这些变化感到好奇,从而推动业务指标的变化。然而,这种效应是暂时的,因为兴趣可能会随着变化变得不再新颖而逐渐趋于正常。考虑到这一点,在实验中建立“烧入期”通常是一个好主意,并忽略实验初期收集的数据。
-
考虑季节性。某些产品/功能的使用可能有一定的季节性生命周期,这可能会影响实验。例如,娱乐网站在周末的流量可能会显著高于工作日。在进行产品实验时,我们可以尽量覆盖周末和工作日,以获得治疗影响的全面估计。
没有一种完美简单的方法来进行产品实验,因为它因人群和治疗背景的不同而有所变化。此外,并非所有现实世界的影响都可以轻易量化。但仍然,以统计学正确的方式进行的产品实验可以帮助为业务决策提供科学依据。这可以与用户研究和产品/业务领域专家的洞察(如你作为 PM 或数据分析师)相辅相成。
摆脱电子表格的束缚
原文:
towardsdatascience.com/pivot-away-from-spreadsheets-e2c1639f4e2b
Excel 不是唯一的数据透视表工具
·发布于 Towards Data Science ·8 分钟阅读·2023 年 4 月 13 日
--

“但是数据透视表!”:许多 Excel 爱好者在听到我赞扬pandas(以及不可避免的建议他们放弃使用电子表格进行数据分析)时的常见反应。
认为数据透视表是电子表格的唯一属性的信念,可能阻碍了许多人完全转向使用 Python 进行数据分析。
当然,这完全不是真的——正如我们今天将深入探讨的那样:
-
pandas中的基本数据透视表 -
使用定制函数和计算字段在数据透视表中
让我们开始吧——我们将从模拟一些数据开始。
数据
我们将从模拟一些数据开始。这里的工作量不大,因为我们模拟了关于两种不同产品的销售数据:
import pandas as pd
import numpy as np
# product A
d_a = pd.DataFrame(
{
'Month':pd.date_range(
start = pd.to_datetime('01-01-2012',format = '%d-%m-%Y'),
end = pd.to_datetime('31-12-2022',format = '%d-%m-%Y'),
freq = 'MS',
),
'Quotes':np.random.randint(
low = 1_000_000,
high = 2_500_000,
size = 132
),
'Numbers':np.random.randint(
low = 300_000,
high = 500_000,
size = 132
),
'Amounts':np.random.randint(
low = 750_000,
high = 1_250_000,
size = 132
),
'Product':'A'
}
)
d_a['Store'] = np.where(
d_a['Quotes'] / d_a['Quotes'].max() < 0.3,
'Uptown',
np.where(
d_a['Quotes'] / d_a['Quotes'].max() < 0.6,
'Downtown',
'Central'
)
)
# product B
d_b = pd.DataFrame(
{
'Month':pd.date_range(
start = pd.to_datetime('01-01-2012',format = '%d-%m-%Y'),
end = pd.to_datetime('31-12-2022',format = '%d-%m-%Y'),
freq = 'MS'
),
'Quotes':np.random.randint(
low = 100_000,
high = 800_000,
size = 132
),
'Numbers':np.random.randint(
low = 10_000,
high = 95_000,
size = 132
),
'Amounts':np.random.randint(
low = 450_000,
high = 750_000,
size = 132
)
}
)
d_b['Product'] = np.where(
d_a['Quotes'] / d_a['Quotes'].max() < 0.5,
'B',
'C'
)
d_b['Store'] = np.where(
d_a['Quotes'] / d_a['Quotes'].max() < 0.3,
'Uptown',
np.where(
d_a['Quotes'] / d_a['Quotes'].max() < 0.6,
'Downtown',
'Outer'
)
)
df = pd.concat([d_a,d_b],axis = 0)
df = df.sample(frac = 1).copy().reset_index(drop = True)
… 这给出:

作者提供的图片
看起来很公*。让我们从数据透视表的基础开始。
基本数据透视表
数据透视表的前提相当简单:一些数量在各种数据段中进行汇总(或计算)。这些结果可以以表格形式表示,其中适当行和列交叉点的值是段结果。
例如,让我们看看每个商店每种产品的总报价数量。
在 Excel 中,我们可能会这样创建数据透视表查询:

作者提供的图片
… 这将给我们一个基本的表格,如下所示:

作者提供的图片
这里的空白条目表示特定段没有数据——例如,中央商店的产品 C 没有报价。
在 pandas 中,这很容易复制,使用 pivot_table¹ 函数:
# basic pivot table
pd.pivot_table(
data = df,
index = 'Store',
columns = 'Product',
values = 'Quotes',
aggfunc = 'sum'
)
输出呈现方式略有不同,缺失的数据更加突出:

作者提供的图片
让我们更详细地查看一下代码。
index、columns 和 values 参数类似于 Excel 透视表查询中的“列”、“行”和“值”字段。也就是说,我们提供给 index 的列将形成透视表的行。同样,我们通过 columns 提供的列将形成透视表中的列。最后但同样重要的是,我们通过 values 提供的内容将是我们计算和汇总的数量,并将填充表格单元格。
稍有不同的是,我们如何定义应用于表格数据的函数。在 Excel 中,我们通常会在“值”窗格中点击每个数量,从“值字段设置”窗口中选择一个合适的函数。

作者提供的图片
在 Python 中,这会简单一些,因为我们只需通过函数的名称来引用它们(稍后会详细介绍)。
那么,如何扩展表格以涵盖更多数量呢?例如,如果我们想计算每个产品和商店组合的最小报价和销售数量,这样做:
# include two quantities
pd.pivot_table(
data = df,
index = 'Store',
columns = 'Product',
values = ['Quotes','Numbers'],
aggfunc = ['min'],
margins = True,
margins_name = 'Combined'
)

作者提供的图片
啊,但这里有些不同——我们包括了每行和每列的统计信息(并将其命名为“Combined”)。这很容易实现,因为margins 控制是否包含这些总计,margins_name 允许我们为这些表格条目提供一个有意义的名称。
定制函数和计算字段
很多分析——包括探索性分析——可以使用相同的简单函数完成:总和、*均值、最小值、最大值(标准差和方差也可以)。这些函数非常普遍,以至于它们已经内置于 pandas 中,我们可以在调用 pivot_table 时通过名称引用它们。
然而,有些情况下我们可能需要使用自定义函数。例如,计算变异系数绝不是一个“标准”函数,但我们可以通过首先定义它,然后通过aggfunc将其输入到透视表调用中来轻松实现。
# function for coefficient of variation (cov)
def cov(x):
return x.std() / x.mean()
# pivot table with cov
pd.pivot_table(
data = df,
index = 'Store',
columns = 'Product',
values = ['Quotes'],
aggfunc = [cov]
)
结果是:

作者提供的图片
Excel 透视表的一个更有用的功能是能够创建所谓的计算字段——透视表中的一个字段,该字段来源于透视表中的其他字段。pandas 对透视表的实现仍然可以提供这些功能,但需要多一点工作,并回到基本原理。
作为一个实际示例,假设我们对每个产品在每个商店的转换率感兴趣。数学上这很简单:
-
计算每个产品和商店的实际销售总数(数据中的“数量”)。
-
计算每个产品和商店的销售报价总数(“报价”)。
-
转换率为 (1) / (2)。
要将其作为透视表生成,我们可以按如下方式编码:
# total sales numbers
num = pd.pivot_table(
data = df,
index = 'Store',
columns = 'Product',
values = 'Numbers',
aggfunc = 'sum',
margins = True,
margins_name = 'Combined'
)
# total sales quotes
den = pd.pivot_table(
data = df,
index = 'Store',
columns = 'Product',
values = 'Quotes',
aggfunc = 'sum',
margins = True,
margins_name = 'Combined'
)
# conversion rate
num / den
结果如下:

图片由作者提供
当然,这作为单独展示是可以的,但为了完整性,我们也可以将其与包含“原始”数据的透视表合并。
小贴士和技巧
作为额外内容,给出一些提示和技巧,然后我们就结束了。
创意定制函数
我们稍微谈了一下使用定制函数的事例。我展示了一个我们需要自定义函数来进行分析的案例,但你可以用更具创意的方式使用自定义函数。
作为示例,我们使用内置的 sum 函数来计算总报价和销售。比如这样:

图片由作者提供
现在,尽管我相信形式之前是功能,但我不喜欢“总和”作为伪标题漂浮的方式。如果我要展示这个表格,我会考虑编写一个新的函数,名字适合展示——例如“总计”——它做的事情和 sum 一样,但结果更具描述性。
因此定义新的函数并传入它:
# custom function with nicer name
def Total(x):
return x.sum()
# pivot table
pd.pivot_table(
data = df,
index = 'Store',
columns = 'Product',
values = ['Quotes','Numbers'],
aggfunc = [Total],
margins = True,
margins_name = 'Combined'
)
给我们提供:

图片由作者提供
虽然表格还未完全准备好展示,但它看起来确实更好了。
格式化透视表
我在这里用了不少 p 字眼:通常情况下,简单的透视表会出现在幻灯片包中。
在此之前,我们可能想对表格进行一些格式化——例如以适当的方式处理缺失值,并使数字更易读。
这通过一些 DataFrame 样式设置非常简单。以下是我之前做的一个示例:

图片由作者提供
通过在 pivot_table 调用的末尾添加一点 .style.format(’{:,.0f}’,na_rep=’-’) 来完成。
对 DataFrame 的展示有大量的自定义选项。实际上,我已经为此写了一篇文章:
简单的格式化技巧让你的 pandas DataFrame 准备好展示
towardsdatascience.com
总结
我们很快覆盖了相当多的内容。我们已经了解了 pandas 中透视表的基础知识,尝试了使用定制函数,并对计算字段的工作示例进行了尝试。
在结束之前,几句想法。
Excel 仍然有几个优点。其中之一是熟悉度;无论多么简单,尝试全新的东西可能会有挑战。
用户使用的点击和拖拽操作是一个优势——在 Python 中没有这样的操作。
然而,如果这个操作必须重复多次,点击和拖拽的优势可能会在长期内被抵消:将这些操作转化为硬编码会节省时间和精力。
是的,这可以通过 VBA 或录制宏来完成,但说实话——无论 VBA 社区多么庞大和有帮助,它仍然是一个难以学习(且不太受欢迎)的语言。
Excel 在某些更微妙的功能和操作方面比 pandas 更为出色。我不是说在 pandas 中无法做到这些,但当你处理日期、过滤数据透视表和创建计算字段时,Excel 可以让你的生活变得更轻松。
如果你发现自己经常重复创建相同的数据透视表,也许你应该考虑转向 Python。我已经这样做了,并且没有回头——在这种情况下,草地真的更绿。
如果你读到了这里——感谢你的阅读! 如果你喜欢我的闲谈并希望查看更多内容,请查看我的其他故事。我写的内容涵盖了从图表和图形到神经网络的各种主题。
参考资料
Pandas 中的透视表及使用 Python 处理多重索引数据的实用示例。
原文:
towardsdatascience.com/pivot-tables-in-pandas-with-hands-on-examples-in-python-9f29a48796f2
了解如何对 Pandas DataFrame 进行透视,并获得有意义的洞察。
·发布在 Towards Data Science ·阅读时间 11 分钟·2023 年 2 月 9 日
--

图片来源 Елена from Pexels
透视表是一种数据操作工具,用于重新排列表格,有时还会聚合值以便于分析。
在这篇文章中,我们将探讨 Pandas 的 pivot_table 函数以及如何使用它提供的各种参数。我们将使用来自 Kaggle 的实际数据集来说明何时以及如何使用 pivot_table 函数。
透视表的优点
-
你可以通过一个或多个列对数据进行分组,然后使用各种统计数据(如均值、总和和计数)来总结这些值。
-
它具有易于使用的语法,可以直观地进行简单到复杂的数据转换。
透视表语法
pandas.pivot_table(data,
values=None,
index=None,
columns=None,
aggfunc='mean',
fill_value=None,
margins=False,
dropna=True,
margins_name='All',
observed=False,
sort=True)
必需参数:
data: 数据源,格式为 pandas DataFrame
以下两个参数中至少需要一个来分组数据。
-
index: 按行分组数据的列。 -
columns: 按列分组数据的列。
可选参数:
-
values: 使用aggfunc函数进行聚合的列。 -
aggfunc: 用于聚合值的函数。 -
fill_value: 用于替换缺失值的值。 -
dropna: 是否删除仅包含 NaN 值的整行或整列。 -
margins: 是否包含行和列的小计。 -
margins_name: 行和列小计的标签名称。 -
observed: 仅显示分类分组器的观察值。 -
sort: 是否对结果的行索引进行排序。
返回值:
pivot_table 函数返回一个 DataFrame,其中包含使用传递给它的参数汇总的数据。
易于理解的类比 — 简而言之,透视表语法表示对于每一个‘index’,返回‘values’列的‘aggfunc’,按‘columns’进一步分组。

Pandas 透视表由作者提供的插图
在下面的演示中,我们将使用这个类比来理解操作。
数据
在本文中,我们将使用来自 Kaggle 的酒店预订数据集,该数据集使用署名 4.0 国际 (CC BY 4.0)许可证。我们将在这个笔记本中对数据集进行探索和清理,下面是我们将用来演示 Pandas 中透视表的 DataFrame 的一个示例。

作者提供的示例 DataFrame
逐个参数探索
‘Index’ 参数(默认值:index = None)
这是用于对数据进行分组的列。
pd.pivot_table(data, index='market')
让我们使用前面提到的类比来查看上面的代码。对于每个market,我们想要所有数值列的mean(values参数默认为所有可以处理默认aggfunc=’mean’参数且不在‘index’参数中的列)。

图片由作者提供
这会产生一个按‘market’分组的 DataFrame。每个单元格中的值是按市场组计算的该列的*均值(例如,客人数量或房价)。
‘Columns’ 参数(默认值:columns = None)
这也是在列级别对数据进行分组。
pd.pivot_table(data, columns='market')
相同的类比适用。对于每个‘market’,返回数值的‘mean’。这些组按列显示,并返回一个转置的 DataFrame。

图片由作者提供
‘Values’ 参数(默认值:values = None)
这些是将出现在表格单元格中的值。聚合函数应用于这些值。
如前所述,我们必须传递data和至少index或columns参数来对数据进行分组。如果values参数没有明确指定,则会从数据中推断出支持aggfunc参数的列。
pd.pivot_table(data,
index='meal_plan',
values = 'room_price').head()
上面的代码表示对于每个‘meal_plan’组,返回‘room_price’值的‘mean’。

图片由作者提供
‘Aggfunc’ 参数(默认值:aggfunc = ‘mean’)
这是每组的值汇总(或选择返回值)的函数。
下表包含常用的aggfunc选项,以及它们是否仅适用于数值函数或同时适用于数值和分类函数。
数值列:
*‘mean’ — 每组的*均值
‘sum’ — 每组的总和
‘median’ — 每组的中位数
‘std’ — 标准差
‘var’ — 每组的方差
‘mad’ — *均绝对偏差
‘prod’ — 每组的值的乘积*
数字和分类列
*‘count’ — 每组的行数
‘min’ — 每组的最小值
‘max’ — 最大值
‘first’ — 每组的第一行值
‘last’ — 最后一行的值
‘nunique’ — 每组的唯一值数量*
pd.pivot_table(data,
index='market',
values = 'room_type',
aggfunc = 'nunique')
在上面的代码中,对于每个 ‘market’ 组,返回 unique ‘room_types’ 的数量。

**aggfunc** 字典 — 我们还可以有 {values:’aggfunc’} 字典对,其中我们为不同的 values 列传递不同的函数。
pd.pivot_table(data,
index = 'market',
aggfunc = {'room_type': 'count',
'lead_time':'max',
'room_price': 'mean'})
在这里,对于每个 ‘market’ 组,返回 ‘room_types’ 的 count、‘lead_time’ 的 maximum 和 ‘price’ 的 average。

作者提供的图像
**df.reset_index()** — 上述示例中生成的结果按 index 参数分组,使其成为新的 index。你可以使用 df.reset_index() 将此列重置为常规列,同时添加一个包含从 0 开始的 RangeIndex。

作者提供的图像
理解透视表中的多重索引列头
在查看其他 pivot_table 参数之前,我们先来看看从 pivot_table 生成的 多重索引列,以及如何去除这些层级。

DataFrame 多重索引示意图,作者提供
当透视表具有以下条件时,会出现多重索引列:
*一个 ‘index’ 参数
AND
多个 ‘columns’ 参数
AND/OR
多个 ‘values’ 参数
AND/OR
多个 ‘aggfunc’ 参数*
~ **index** 和多个 **columns**
pd.pivot_table(data,
index = 'market',
columns = ['status','year'],
values = 'room_price',
aggfunc = 'mean',
fill_value=0).head()
在上面的代码中,我们按 ‘market’ 对数据进行分组,然后返回按 status 和 year 进一步分组的 average ‘room price’。

作者提供的图像
注意,在 ‘columns’ 列表中的每一列,表中会按照从最外层(0)到最内层(1)的顺序添加一个列级别(层次结构)。
~ **index** 和多个 **values**
pd.pivot_table(data,
index = 'market',
columns = 'status',
values = ['nights','month'],
aggfunc = 'mean',
fill_value=0).head()
在这里,我们按 ‘market’ 对数据进行分组,然后返回 ‘nights‘ 和 ‘month’ 值的 mean,按 ‘status’ 分组。

作者提供的图像
values 层级位于 columns 之上,默认情况下,名称是排序的。(sort=False 参数目前仅适用于行索引)。与 columns 参数不同,所有名称都保持在一个层级中,无论列表的长度如何。
~ **index** 和多个 **aggfunc** 选项
pd.pivot_table(data,
index = 'market',
columns = 'status',
values = 'guests',
aggfunc = ['mean','max'],
fill_value=0).head()
在这里,按 ‘market’ 分组数据,然后返回 ‘guests’ 列的 mean 和 maximum 值,并进一步按 ‘status’ 分组。

图片来源:作者
aggfunc 层次处于最高层次。类似于 values,所有 aggfunc 选项都保留在一个层次中。
~ **index** 和多个 **columns** 、 **values** 以及 **aggfunc** 选项
pd.pivot_table(data,
index = 'market',
columns = ['status','year'],
values = ['lead_time','nights'],
aggfunc = ['median','max'],
fill_value=0)
在这里,对于每个 ‘market’,返回 ‘lead_time’ 和 ‘mights’ 列的 median 和 maximum 值,并进一步按 ‘status’ 和 ‘year’ 分组。

图片来源:作者
注意,我们现在有四个层次;两个层次用于 columns 参数,以及每个 values 和 aggfunc 参数各一个层次。
移除多重索引层次
你会发现,当存在列的层次结构时,df.reset_index() 将不会按预期工作。

df.reset_index 的多重索引示意图由作者提供
这里有两种处理多重索引的方法
- 合并/合并层次

图片来源:作者
使用上述的数据透视表,下面有两种合并层次的方法。
- 使用
***map***和***join***函数
df.columns.map('_'.join)
#Results
['month_Canceled', 'month_Not Canceled',
'nights_Canceled','nights_Not Canceled']
在 map 函数中,调用 join 函数并传递分隔符符号,在我们的例子中是下划线。这将返回一个连接的列层次的列表,然后你可以将其分配给数据框的列名,如下所示。
df.columns = df.columns.map('_'.join)

合并后的多重索引列标题
你还可以提供其他符号或字符作为分隔符,例如空字符串 (''.join)、点 ('.'.join),甚至一个词 ('per'.join)。注意,层次是从最上层(层次 0)向最内层(层次 -1)组合的。
- 使用列表推导式——当你想在合并过程中重新排序名称时,这种方法很有用。
df.columns = [f'{i}_{j}' for i,j in df.columns]
上面的代码结果与上一节相同的数据框。该代码表示对于每一列,i 为最上层,j 为其下一个层次,按照这种顺序返回这两个层次(i 然后 j),并用下划线分隔。
现在,假设你希望在合并后对名称进行不同的排序。我们将使用下面的数据框。

图片来源:作者
你可以使用列表推导式合并像 level1_level0_level2 这样的名称。在这里,使用三个变量名来表示层次 (i,j,k),然后提供所需的格式进行返回,例如 (f’{j}_{i}_{k}’)。
df.columns = [f'{j} {i} {k}' for i,j,k in df.columns]
在上面的代码中,我们使用空格来分隔名称。

合并后的列名由作者提供
2. 删除一些层次
有时,这些层次结构并没有实际用处。

图片来源:作者
要删除上述数据透视表的前两个层次,使用 df.columns.droplevel(level=position)。
df.columns = df.columns.droplevel(level = [0,1])
df

丢弃多级索引列的 DataFrame
删除‘列级别’名称 — 一旦你移除或合并了级别,你可以使用 df.reset_index()。
df.reset_index()

图片由作者提供
但现在,原始列名(传递给 columns 参数的名称)仍然出现在重置的 DataFrame 中。请参见上面的 ‘year’,它作为新索引的名称出现在错误的位置。我们可以使用 df.rename_axis() 来移除这个名称。
df2 = df.rename_axis(columns=None)
df2.reset_index()

图片由作者提供
‘Fill_value’ 参数(默认值:fill_value = None)
这是替代缺失值的值。
当没有 index-columns 组合用于你正在聚合的 values 时,会出现缺失值或 NaNs(Not a number)。

图片由作者提供
在上面的 DataFrame(左侧),区号 1112 没有 agent ‘b’,因此在 1112 和 ‘b’ 的组合值的位置上出现 NaN。
在这里,我们可以使用 fill_value=0 将 NaNs 替换为 0。

图片由作者提供
你也可以使用字符串值,如下面的代码中,我们将一个空字符串传递给 fill_value 参数。

图片由作者提供
‘dropna’ 参数(默认值:dropna = True)
当为 True 时,此参数丢弃仅包含缺失值的行或列。
例如,使用下面的 DataFrame,假设我们有一个新的 agent c 尚未分配区号,以及一个新的 area code 1113 没有价格和大小。

图片由作者提供
如果我们希望它们出现在透视表中,而不是原始表中存在的内容,我们可以按照以下步骤进行:
- 将
agent和area_code特征转换为 分类数据类型 并提供所有可能的类别。
df['area_code'] = pd.Categorical(
df['area_code'],
categories=['1111',
'1112',
'1113'])
df['agent'] = pd.Categorical(
df['agent'],
categories=['a', 'b', 'c'])
- 使用
dropna=False透视数据,以便不丢弃仅包含缺失值的列和行。你也可以在这里使用fill_value。
pd.pivot_table(df,
index = 'area_code',
columns = 'agent',
values = 'price',
aggfunc = 'mean',
dropna = False)

图片由作者提供
‘Margins’ 参数(默认值:margins = False)
这显示了透视表中每一行和每一列的聚合值。margin 参数在透视表的结果为数值(来自 aggfunc 函数如 sum、count、nunique 等)时有效,并且相同的 aggfunc 函数用于计算 margin 值。
例如,在下面的 pivot_table 中,我们按 ‘area_code’ 分组数据,并返回 ‘price’ 的 sum,进一步按 ‘agent’ 分组。我们还希望 margins 值,在这种情况下,返回的是行和列值的 sum。
pd.pivot_table(df,
index = 'area_code',
columns = 'agent',
values = 'price',
aggfunc = 'sum',
dropna = False,
fill_value = 0,
margins = True)

图片由作者提供
‘Margins_name’ 参数(默认值:margins_name=’All’)
这会重命名边际名称,并且仅在margins设置为 True 时有效。
在下面的代码中,我们将边际名称更改为‘sum_total’。

图片作者提供
‘Observed’ 参数(默认值:observed = False)
当你有像之前那样的分类数据类型时,有时透视表会显示只包含零值的行和列。这种情况即使在dropna=True时也会发生。

图片作者提供
当你设置observed=True时,只显示‘observed’分组。这在从大型数据集中创建透视表时节省了内存和计算能力。

图片作者提供
‘Sort’ 参数(默认值:sort = True)
这会对行和列索引进行排序。
pd.pivot_table(df,
index = 'area_code',
aggfunc = 'first')

图片作者提供
当sort=False时,结果行索引按原始顺序出现,但列标题保持排序状态。
pd.pivot_table(df,
index = ['area_code','agent'],
aggfunc = 'first',
sort = False)

图片作者提供
结论
在这篇文章中,我们探讨了如何使用 Pandas 透视表及其参数。我们还了解到,index和columns参数的所有唯一组合的values会通过aggfunc函数聚合成一个值。
我们接着看了如何通过传递一个index和多个columns、values或aggfunc参数来生成分层列(多级索引)。后来,我们合并并删除了多级索引级别。
透视表对总结数据集很重要。你可以使用这个总结来绘制图表,或为演示格式化和美化表格。
可以在GitHub 上的这个位置找到本文使用的笔记本和文件。
希望你喜欢这篇文章。要在我发布新文章时接收更多类似内容,请点击这里订阅。如果你还不是 Medium 成员并希望支持我作为写作者,请访问这个链接,我将获得一小部分佣金。感谢阅读!
使用 Python 实现、解决和可视化旅行推销员问题
将数学优化模型翻译为 Python,进行优化,并可视化解决方案,以便快速获得建模错误的反馈
·
关注 发表在Towards Data Science ·24 分钟阅读·2023 年 5 月 9 日
--
由 DALL·E 3 生成的图像,作者的提示是:“城市中的位置网络和连接它们的最佳路线”
👁️ 这是涵盖项目“Python 中的智能决策支持系统”的系列文章中的第 3 篇。 我鼓励你查看,以获得整个项目的一般概述。如果你只是对如何在 Python 中实现 TSP 模型感兴趣,你仍然来对地方了:这篇文章是自包含的,我将带你完成所有步骤——安装依赖项、分析、代码、结果解释和模型调试。
本文继续从sprint 2结束的地方接着讲解。在这里,我们将上一篇文章中制定的数学模型实现到 Python 中,使用Pyomo,并遵循良好实践。然后,模型经过优化,解决方案被可视化和分析。为了教学目的,我们发现初始模型公式不完整,因此我展示了如何从第一性原理推导出修正公式所需的约束。这些新约束被添加到 Pyomo 模型中,新解决方案再次被分析和验证。
目录
1. 使用 Pyomo 在 Python 中实现模型
-
1.1. 安装依赖项
-
1.2. 数学变成代码
2. 解决和验证模型
-
2.1. 解决模型
-
2.2. 结果可视化
-
2.3. 分析结果
3. 修正公式
-
3.1. 激励思想
-
3.2. 将激励思想表达为逻辑蕴涵
-
3.3. 将逻辑蕴涵公式化为线性约束
-
3.4. 推导“大 M”的适当值
4. 实现和验证新公式
-
4.1. 扩展 Pyomo 模型
-
4.2. 绘制更新模型的解决方案
5. 结论(用于下一个冲刺)
📖 如果你没有阅读过上一篇文章,也不用担心。数学公式在这里也阐述(但没有推导),每个模型组件都与其代码实现相邻。如果你不理解这些内容的来源或含义,请阅读“冲刺 2”的文章,如果你想了解更多关于问题陈述和动机的背景,请阅读“冲刺 1”的文章。
一种以概念为先,数学为后的建模方法,用于解决运筹学中最著名的路径问题
[towardsdatascience.com
1. 实现使用 Pyomo 的 Python 模型
使用 Python 是因为它是数据科学中的顶级语言,而Pyomo则是处理大规模模型的最佳(开源)库之一。
在本节中,我将逐一讲解在公式中定义的每个模型组件,并解释它是如何转换为 Pyomo 代码的。我尽量不留下任何空白,但如果你觉得还有问题,请在评论中提问。
免责声明:目标读者预计对 Pyomo 甚至建模都是新手,因此为了降低他们的认知负担, 简洁明了的实现优先于编程最佳实践*。目前的目标是教授优化建模,而非软件工程。代码会在未来迭代中逐步改进,随着这一概念验证演变成更复杂的 MVP。
1.1. 安装依赖项
对于着急的人
安装(或确保已经安装)库pyomo、networkx和pandas,以及包glpk。
📝 包
glpk包含GLPK 求解器,这是我们用来优化创建模型的外部求解器。Pyomo 用于创建问题模型并将其传递给 GLPK,GLPK 将运行算法来执行优化过程。然后,GLPK 将解决方案返回给 Pyomo 模型对象,这些解决方案被存储为模型属性,我们可以在不离开 Python 的情况下方便地使用它们。
推荐的安装 GLPK 的方法是通过conda,这样 Pyomo 可以轻松找到 GLPK 求解器。要一次性安装所有依赖项,请运行:
conda install -y -c conda-forge pyomo=6.5.0 pandas networkx glpk
对于有组织的人
我建议创建一个单独的虚拟环境,在其中安装所有需要的库以跟随本系列的文章。复制这段文本
name: ttp # traveling tourist problem
channels:
- conda-forge
dependencies:
- python=3.9
- pyomo=6.5.0
- pandas
- networkx
- glpk # external solver used to optimize models
- jupyterlab # comment this line if you won't use Jupyter Lab as IDE
并将其保存在名为environment.yml的 YAML 文件中。在相同位置打开 Anaconda 提示符并运行命令
conda env create --file environment.yml
几分钟后,环境将创建完毕,所有依赖项都已安装。运行conda activate ttp以“进入”环境,启动 Jupyter Lab(在终端中运行jupyter lab),然后开始编码吧!
1.2. 数学变成代码
首先,确保 Pyomo 可以找到 GLPK 求解器
### ===== Code block 3.1 ===== ###
import pandas as pd
import pyomo.environ as pyo
import pyomo.version
import networkx as nx
solver = pyo.SolverFactory("glpk")
solver_available = solver.available(exception_flag=False)
print(f"Solver '{solver.name}' available: {solver_available}")
if solver_available:
print(f"Solver version: {solver.version()}")
print("pyomo version:", pyomo.version.__version__)
print("networkx version:", nx.__version__)
Solver 'glpk' available: True
Solver version: (5, 0, 0, 0)
pyomo version: 6.5
networkx version: 2.8.4
⛔ 如果你收到消息
'glpk' available: False,说明求解器安装不正确。请尝试以下方法解决问题:
重新仔细执行安装步骤
在基础(默认)环境中运行
conda install -y -c conda-forge glpk尝试安装适合你的不同求解器
然后读取距离数据的 CSV 文件
### ===== Code block 3.2 ===== ###
path_data = (
"https://raw.githubusercontent.com/carlosjuribe/"
"traveling-tourist-problem/main/"
"Paris_sites_spherical_distance_matrix.csv"
)
df_distances = pd.read_csv(path_data, index_col="site")
df_distances

现在我们进入[敏捷运筹学工作流程]的“阶段 4”,如下图所示的绿色块:

图 1. 运筹学问题解决的极简工作流程。第 4 阶段:计算机 模型(作者提供的图片)
任务是将之前创建的数学模型实现成代码,完全按照数学上定义的方式。
👁️ 如果这样可以使模型实现更容易,我们可以创建任意数量的 Python 对象,但在编写代码时,我们不允许以任何方式修改底层模型。这会导致数学模型和计算机模型不同步,从而使后续的模型调试变得相当困难。
我们实例化一个空的 Pyomo 模型,在其中将模型组件作为属性存储:
model = pyo.ConcreteModel("TSP")
1.2.1. 集合
为了创建站点集合𝕊 = {卢浮宫,埃菲尔铁塔,…,酒店},我们从数据框的索引中提取它们的名称,并用它来创建一个名为sites的 Pyomo Set:
### ===== Code block 3.3 ===== ###
list_of_sites = df_distances.index.tolist()
model.sites = pyo.Set(initialize=list_of_sites,
domain=pyo.Any,
doc="set of all sites to be visited (𝕊)")
创建派生集

表达式 3.1. 旅行的可能弧的派生集(站点到站点的轨迹)。
我们将过滤器𝑖 ≠ 𝑗 存储在构造规则(Python 函数_rule_domain_arcs)中,并在初始化Set时将此规则传递给filter关键字。请注意,这个过滤器将应用于站点的笛卡尔积(𝕊 × 𝕊),并会筛选出那些不符合规则的笛卡尔积成员。
### ===== Code block 3.4 ===== ###
def _rule_domain_arcs(model, i, j):
""" All possible arcs connecting the sites (𝔸) """
# only create pair (i, j) if site i and site j are different
return (i, j) if i != j else None
rule = _rule_domain_arcs
model.valid_arcs = pyo.Set(
initialize=model.sites * model.sites, # 𝕊 × 𝕊
filter=rule, doc=rule.__doc__)
1.2.2. 参数
参数
𝐷ᵢⱼ ≔ 站点𝑖和站点𝑗之间的距离
是通过构造函数pyo.Param创建的,该构造函数将第一个(位置)参数域𝔸(model.valid_arcs)传递给它,并将关键字参数initialize设置为另一个构造规则_rule_distance_between_sites,该规则针对每对(𝑖, 𝑗)∈ 𝔸进行评估。在每次评估中,距离的数值从数据框df_distances中提取,并在内部与对(𝑖, 𝑗)相关联:
### ===== Code block 3.5 ===== ###
def _rule_distance_between_sites(model, i, j):
""" Distance between site i and site j (𝐷𝑖𝑗) """
return df_distances.at[i, j] # fetch the distance from dataframe
rule = _rule_distance_between_sites
model.distance_ij = pyo.Param(model.valid_arcs,
initialize=rule,
doc=rule.__doc__)
1.2.3. 决策变量
由于𝛿ᵢⱼ具有与𝐷ᵢⱼ相同的“索引域”,因此构建此组件的方式非常相似,只是这里不需要构造规则。

表达式 3.2. 二进制决策变量
model.delta_ij = pyo.Var(model.valid_arcs, within=pyo.Binary,
doc="Whether to go from site i to site j (𝛿𝑖𝑗)")
pyo.Var的第一个位置参数保留用于其索引集𝔸,变量的“类型”通过关键字参数within指定。这里的“变量类型”指的是变量可以取的值范围。在这里,𝛿ᵢⱼ的范围仅为 0 和 1,所以它是二进制类型。从数学上讲,我们会写作𝛿ᵢⱼ ∈ {0, 1},但我们可以在创建变量时通过设置within=pyo.Binary直接在 Pyomo 中表示这一点,而无需创建单独的约束。
1.2.4. 目标函数

表达式 3.3. 需要最小化的目标函数:总巡回距离
要构造目标函数,我们可以将表达式“存储”在一个函数中,该函数将用作构造规则。此函数只接受一个参数,即模型,用于提取构建表达式所需的任何模型组件。
### ===== Code block 3.6 ===== ###
def _rule_total_distance_traveled(model):
""" total distance traveled """
return pyo.summation(model.distance_ij, model.delta_ij)
rule = _rule_total_distance_traveled
model.obj_total_distance = pyo.Objective(rule=rule,
sense=pyo.minimize,
doc=rule.__doc__)
观察数学表达式与函数的返回语句之间的*行关系。我们通过sense关键字指定这是一个最小化问题。
1.2.5. 约束
如果你记得之前的文章,一个方便的方式来强制每个站点仅被访问一次是强制每个站点同时被“进入”一次和“退出”一次。
每个站点仅被访问一次

表达式 3.4. 约束集,强制每个站点仅被“进入”一次。
def _rule_site_is_entered_once(model, j):
""" each site j must be visited from exactly one other site """
return sum(model.delta_ij[i, j] for i in model.sites if i != j) == 1
rule = _rule_site_is_entered_once
model.constr_each_site_is_entered_once = pyo.Constraint(
model.sites,
rule=rule,
doc=rule.__doc__)
每个站点仅被退出一次

表达式 3.5. 约束集,强制每个站点仅被“退出”一次。
def _rule_site_is_exited_once(model, i):
""" each site i must departure to exactly one other site """
return sum(model.delta_ij[i, j] for j in model.sites if j != i) == 1
rule = _rule_site_is_exited_once
model.constr_each_site_is_exited_once = pyo.Constraint(
model.sites,
rule=rule,
doc=rule.__doc__)
1.2.6. 模型的最终检查
模型实现已完成。为了查看模型的整体情况,我们应该执行model.pprint(),并稍微浏览一下,以查看是否遗漏了一些声明或犯了一些错误。
为了了解模型的规模,我们打印它包含的变量和约束的数量:
def print_model_info(model):
print(f"Name: {model.name}",
f"Num variables: {model.nvariables()}",
f"Num constraints: {model.nconstraints()}", sep="\n- ")
print_model_info(model)
#[Out]:
# Name: TSP
# - Num variables: 72
# - Num constraints: 18
拥有少于 100 个约束或变量,这个问题的规模较小,求解器会相对较快地优化它。
2. 解决和验证模型
2.1. 解决模型
AOR 流程图中的下一步是优化模型并检查解决方案:
res = solver.solve(model) # optimize the model
print(f"Optimal solution found: {pyo.check_optimal_termination(res)}")
# [Out]: Optimal solution found: True
好消息!求解器找到了这个问题的最优解!让我们检查一下,以便知道到巴黎时该跟随什么路线!
为了进行快速检查,我们可以运行model.delta_ij.pprint(),这将打印所有𝛿ᵢⱼ变量的(最优)值,值为 0 或 1:

图 3.1. 模型打印的决策变量值摘录(图片来源:作者)
仅通过查看选择的弧线很难可视化一个巡回路线,更不用说分析它以验证我们是否正确地制定了模型。
要真正理解解决方案,我们需要对其进行可视化。
2.2. 结果可视化
一张图片胜过千言万语
由于我们处理的是节点和弧,可视化解决方案的最简单方法是将其绘制成图形。请记住,这只是一个概念验证,因此快速、有效的反馈优于美观。更具洞察力的可视化可以等到我们有一个可行的 MVP 后再做。现在,让我们编写一些辅助函数以高效地绘制解决方案。
函数extract_solution_as_arcs接收一个已解决的 Pyomo 模型,并从解决方案中提取“选定的弧”。接下来,函数plot_arcs_as_graph将活跃弧的列表存储在 Graph 对象中以便于分析,并绘制该图,使得酒店是唯一的红色节点,作为参考。最后,函数plot_solution_as_graph调用上述两个函数,以图形的形式展示给定模型的解决方案。
### ===== Code block 3.7 ===== ###
def extract_solution_as_arcs(model):
""" Extract list of active (selected) arcs from the solved model,
by keeping only the arcs whose binary variables are 1 """
active_arcs = [(i, j)
for i, j in model.valid_arcs
if model.delta_ij[i, j].value == 1]
return active_arcs
def plot_arcs_as_graph(tour_as_arcs):
""" Take in a list of tuples representing arcs, convert it
to a networkx graph and draw it
"""
G = nx.DiGraph()
G.add_edges_from(tour_as_arcs) # store solution as graph
node_colors = ['red' if node == 'hotel' else 'skyblue'
for node in G.nodes()]
nx.draw(G, node_color=node_colors, with_labels=True, font_size=6,
node_shape='o', arrowsize=5, style='solid')
def plot_solution_as_graph(model):
""" Plot the solution of the given model as a graph """
print(f"Total distance: {model.obj_total_distance()}")
active_arcs = extract_solution_as_arcs(model)
plot_arcs_as_graph(active_arcs)
现在我们可以看到解决方案的真实情况:
plot_solution_as_graph(model)

图 3.2. 第一个模型的解决方案,显示了不希望出现的子旅行,而不是单一旅行路线。(图像由作者提供)
嗯,这显然是不符合我们预期的! 没有单一的旅行路线遍历所有站点并返回酒店。确实,所有站点都被访问了,但只是作为小的、不连通的站点簇的一部分。技术上讲,我们指定的约束条件确实被严格遵守:每个站点只进入一次,离开一次,但总体结果不是我们所期望的单一旅行路线,而是一组子旅行。这意味着我们在上一篇文章中的假设.) 是错误的,因此模型中缺少了某些东西,以编码“解决方案中不允许子旅行”的要求。
2.3. 分析结果
出了什么问题?
当一个模型的解决方案不合逻辑时,唯一的可能解释是: 模型是错误的¹。
求解器给出了模型的真实最优解,但我们给出的模型并不符合我们要解决的问题。现在的任务是找出原因,以及我们哪里犯了错误。回顾起来,显而易见的候选因素是我们在前一篇文章“4.4. 创建约束”部分最后两段中做出的可疑假设,即我们设计数学模型时的假设。我们(现在知道是错误的)假设从两个约束条件自然会形成一个单一的旅行路线。但正如我们刚刚可视化的那样,并非如此。为什么?
错误的根源在于我所称的“未言明的常识”:我们对世界的知识如此明显,以至于我们忘记在模型中指定它。
我们隐含地知道在访问站点时传送是不可能的,但我们没有明确告知模型。这就是为什么我们观察到那些小的子旅行,连接了一些站点,但不是全部。模型“认为”从一个站点传送到另一个站点是可以的,只要一旦到达某个站点,就会退出一次并进入一次(请再次查看图 3.2)。我们之所以看到子旅行,仅仅是因为我们告诉模型最小化旅行的距离,而正好传送有助于节省距离。
因此,我们需要防止这些子旅行的形成以获得一个现实的解。我们需要设计一些新的约束条件,以“告知模型”子旅行是被禁止的,或者说,解必须是一个单一的旅行。我们选择后者,并从第一原则出发,推导出一组直观的约束条件,并能很好地完成任务。
3. 修正模型
参考[敏捷运筹研究工作流程],我们现在处于模型重新制定阶段。模型的重新制定可能涉及改进或修复。我们的目的是修复它。
我们知道我们想要什么:强制解为一个单一的旅行,从我们的初始站点,即酒店,开始和结束。挑战在于如何将这一要求编码为一组线性约束。下面是一个想法,源于旅行的性质。
3.1. 激励思想
我们有𝑁个站点需要“遍历”,包括酒店。由于我们从酒店开始,这意味着还有 𝑁 − 1 个站点需要访问。如果我们跟踪访问这些站点的“时间顺序”,使得第一个目的地(酒店之后)标记为 1,第二个目的地标记为 2,以此类推,那么在返回酒店之前的最后一个目的地将被标记为 𝑁 − 1。如果我们将这些用于跟踪访问顺序的数字称为“等级”,那么在旅行中出现的模式是任何站点(除了酒店)的等级总是比前一个站点的等级高 1 个单位。如果我们能够制定一组约束条件,使得任何可行解都符合这种模式,那么可以说,我们在模型中引入了一个“时间顺序”的要求。事实证明我们确实可以做到这一点。
3.2. 将激励思想表达为逻辑蕴涵
💡 这是我们希望任何可行解都满足的“模式”:
任何站点(除了酒店)的等级必须总是比其在旅行中前面的站点的等级高 1 个单位
我们可以将这个模式重新表达为一个逻辑蕴涵,如下:“站点𝑗的等级必须比站点𝑖的等级高 1 个单位当且仅当 𝑗 紧接着 𝑖 被访问,对于所有不包括酒店𝐻的弧 (𝑖, 𝑗)”。这个措辞在数学上表示为:

表达式 3.6. 排名变量的逻辑含义:每访问一个新地点,排名增加 1。
其中 𝑟ᵢ 是我们需要跟踪(尚未确定的)访问顺序的新变量。为了将它们与决策变量区分开来,我们称它们为“排名变量”。右侧的意思是“对于所有属于所有地点集合(不包括酒店)的 𝑖 和 𝑗”。为了符号方便,我们定义新的集合 𝕊* 来存储除酒店之外的所有地点(用 𝐻 表示):

表达式 3.7. 所有感兴趣地点的集合:所有地点,除酒店外。
这允许我们简洁地定义排名变量为:

表达式 3.8. 排名变量的定义,仅定义在感兴趣的地点。
👁️ 酒店 不能有相关的排名变量 因为 酒店将同时是任何旅行的起点和终点,这一条件 会违反模式 “旅行中排名变量不断增加”。这样,每个地点的排名总是随着每个新弧的添加而增加,从而确保* 禁止封闭环路,除非环路闭合在唯一一个没有排名变量的地点:酒店。
𝑟ᵢ 的界限源自其描述:排名从 1 开始,并单调增加,直到所有 𝕊* 中的地点都被访问,最终为 | 𝕊* |(非酒店地点集合的大小)。此外,我们允许它们取任何正实数值:

表达式 3.9. 排名变量的范围和界限
3.3. 将逻辑含义表述为线性约束
现在的挑战是将这种逻辑含义转化为一组线性约束。幸运的是,线性不等式也可以用来强制逻辑含义,不仅仅是有限资源限制。
一种方法是通过所谓的Big-M 方法,其包括声明一个约束,使得当您关心的条件满足时,约束有效(激活),当您关心的条件不满足时,约束变得冗余(失效)。该技术称为“大 M”是因为它使用一个常数值 𝑀,该值足够大,以至于当出现在约束中时,使约束在任何情况下都变得冗余。当 𝑀 不出现在约束中时,约束在期望的含义上是“有效的”。
但是什么决定了约束是否“有效”? 简短的回答是约束所应用的决策变量的值。让我们看看它是如何工作的。
希望的含义是仅当 𝛿ᵢⱼ = 1 时 𝑟ⱼ = 𝑟ᵢ + 1。我们可以用 𝛿ᵢⱼ 替代表达式中的 1,得到

当𝛿ᵢⱼ = 1 时,这是我们希望保持的关系,但当𝛿ᵢⱼ = 0 时则不是。为了“纠正”𝛿ᵢⱼ = 0 的情况,我们添加一个冗余项 𝑅𝑇 ,其功能是仅在 𝛿ᵢⱼ = 0 时“解除约束”。因此,这个冗余项必须包括变量𝛿ᵢⱼ,因为它依赖于它。

在这个上下文中,“解除约束”意味着“使其冗余”,因为冗余约束在模型中与不存在的约束效果相同。
让我们来深入探讨如何推导RT的表达式。𝑅𝑇(𝛿ᵢⱼ)的表达式需要满足以下这些属性:

表达式 3.10. 冗余项必须满足的属性以确保有效的冗余。
为了满足点(1),我们需要𝑅𝑇(𝛿ᵢⱼ = 1) = 0,因此𝑅𝑇的表达式必须包含乘数(1 − 𝛿ᵢⱼ),因为当𝛿ᵢⱼ = 1 时它变为 0。这种形式使𝑅𝑇在𝛿ᵢⱼ = 1 时“消失”,或在𝛿ᵢⱼ = 0 时“减少”为一个常数(我们称之为M)。因此,冗余项的一个候选项是

表达式 3.11. 定义用于选择性使某些约束变为冗余的“冗余项”。
其中𝑀应该从问题数据中确定(稍后会详细说明)。
为了满足所有可能的𝑖和𝑗的点(2),我们需要将表达式(3.11)中的等式变成不等式(=变为≥),并找到一个足够大的常数 𝑀 (绝对值)以确保无论 𝑟ⱼ 和 𝑟ᵢ 取什么值,约束始终得到满足。这就是“大 M”中的“大”的来源。
一旦我们找到这样的足够大的常数 𝑀,我们的“逻辑暗示”约束将采取以下形式

表达式 3.12. 对于“一个站点的等级必须高于其前一个站点”的暗示的约束。
将这些约束引入模型中,将会强制解为单一巡回。但如果我们不首先指定一个好的M值,约束将不会产生期望的效果。
3.4. 推导出“巨大 M”的适当值
由于目标是使𝑅𝑇(𝛿ᵢⱼ = 0) = 𝑀,我们可以通过在表达式(3.12)中设置𝛿ᵢⱼ = 0 来推导出适当的 𝑀 值:

表达式 3.13. 𝑀的最小值的推导。
为了使𝑟ⱼ − 𝑟ᵢ ≥ 𝑀对所有非酒店站点 𝑖,𝑗 都满足,我们需要 𝑟ⱼ − 𝑟ᵢ 的下界也大于𝑀。 𝑟ⱼ − 𝑟ᵢ的下界(LB)是𝑟ⱼ − 𝑟ᵢ可以取的最小值,可以通过以下方式获得

其中𝑟ᵐⁱⁿ是可能的最低等级,𝑟ᵐᵃˣ是可能的最高等级。因此,为了使表达式(3.13)中的不等式(1)对所有站点的所有等级都成立,以下不等式也必须成立:

多亏了这个不等式,我们知道为了使大 M 方法有效,𝑀 必须取的最小值是

表达式 3.14. 大 M 的下界。
那么 𝑟ᵐⁱⁿ 和 𝑟ᵐᵃˣ 的值是多少?根据我们的约定,我们给第一个访问的站点排了第 1 名,这当然是最小排名(即,𝑟ᵐⁱⁿ = 1)。由于排名在每个访问的站点中增加 1 单位,旅行中的最后一个非酒店站点将具有最大排名,等于所有非酒店站点的数量。由于非酒店站点的数量可能会变化,我们需要一个通用表达式。如果你还记得,我们定义了集合 𝕊* 包含所有非酒店站点,因此我们要找的数字是集合 𝕊* 的大小(即,元素的数量),用数学符号表示为 | 𝕊* |。因此,我们推导出 𝑟ᵐⁱⁿ = 1 和 𝑟ᵐᵃˣ = | 𝕊* |。代入表达式(3.14),我们最终得到了一个适当的 M 值:

表达式 3.15. 从问题数据中推导出的“大 M”值。
由于 𝕊* 总是会有超过 2 个需要访问的站点(否则就没有 决策 问题),在这个模型中,“大 M”值总是负的“大”值。
📝 理论值与计算值
理论上,我们可以选择比这里推导出的 𝑀 值更“负”的值——甚至可以编造 巨大的 数字以确保它们足够大,避免这个计算——但 这不是好的实践。 如果 𝑀 变得过大(绝对值),可能会在求解器的算法中产生 性能问题 ,或者,在最坏的情况下,甚至使求解器** 将不可行的解视为可行解**。这就是为什么推荐的做法是从问题数据中推导出 紧凑但足够大的值 来表示 𝑀。
既然我们已经推导出适当的“大 M”值,我们将把它存储在新的模型参数中,以便于重复使用。有了这个,子旅行排除约束集已准备好,其“完整形式”为

表达式 3.16. 子旅行排除约束。
为了保持视角的准确性,请注意,这实际上是原始 逻辑蕴涵在表达式(3.6)中的“约束等价物”:

恭喜!我们终于有了一组可以添加到模型中的约束。提出这些约束是困难的部分。现在,让我们验证将它们添加到模型中是否真的能消除子旅行。
4. 实现和验证新公式
4.1. 使用新公式扩展 Pyomo 模型
修订和纠正模型需要添加一些额外的集合、参数、变量和约束。让我们将这些新的模型组件按照最初制定阶段的顺序添加到 Pyomo 模型中。
4.1.1. 集合和参数
- 感兴趣的站点,𝕊*: 所有站点的集合 不包括酒店:

表达式 3.17。感兴趣的站点集合的定义(即,非酒店站点)
Pyomo Set 对象具有与 Python 集合兼容的操作,因此我们可以直接计算 Pyomo 集合与 Python 集合之间的差异:
model.sites_except_hotel = pyo.Set(
initialize=model.sites - {'hotel'},
domain=model.sites,
doc="Sites of interest, i.e., all sites except the hotel (𝕊*)"
)
- 大 M,子回路消除约束的新参数:

model.M = pyo.Param(initialize=1 - len(model.sites_except_hotel),
doc="big M to make some constraints redundant")
4.1.2. 辅助变量
- 排序变量,rᵢ: 用于跟踪站点访问顺序:

model.rank_i = pyo.Var(
model.sites_except_hotel, # i ∈ 𝕊* (index)
within=pyo.NonNegativeReals, # rᵢ ∈ ℝ₊ (domain)
bounds=(1, len(model.sites_except_hotel)), # 1 ≤ rᵢ ≤ |𝕊*|
doc="Rank of each site to track visit order"
)
在评论中,你可以看到完整的数学定义中变量的元素是如何很好地映射到Pyomo 变量声明函数 pyo.Var 的参数上的。我希望这有助于你理解在开始构建 Pyomo 模型之前拥有一个良好定义的数学模型的价值。实现过程将自然流畅,错误也会减少。
4.1.3. 约束
- 解必须是一个从酒店开始并结束的单一巡回:

def _rule_path_is_single_tour(model, i, j):
""" For each pair of non-hotel sites (i, j),
if site j is visited from site i, the rank of j must be
strictly greater than the rank of i. """
if i == j: # if sites coincide, skip creating a constraint
return pyo.Constraint.Skip
r_i = model.rank_i[i]
r_j = model.rank_i[j]
delta_ij = model.delta_ij[i, j]
return r_j >= r_i + delta_ij + (1 - delta_ij) * model.M
# cross product of non-hotel sites, to index the constraint
non_hotel_site_pairs = model.sites_except_hotel * model.sites_except_hotel
rule = _rule_path_is_single_tour
model.constr_path_is_single_tour = pyo.Constraint(
non_hotel_site_pairs,
rule=rule,
doc=rule.__doc__)
Pyomo 模型已更新。增加子回路消除约束后,它增长了多少?
print_model_info(model)
# [Out]:
# Name: TSP
# - Num variables: 80
# - Num constraints: 74
我们已经从 72 个变量增加到 80 个变量,从 18 个约束增加到 74 个约束。显然,这种表述在约束方面比变量更多,因为它使我们之前的约束数量增加了四倍。这就是我们通常为使模型更“现实”而付出的代价,因为现实性通常意味着——如果数据不变——限制可允许的解的数量。
和往常一样,我们可以使用model.pprint()检查模型结构。虽然随着模型组件数量的增加,这种“打印”迅速失去价值,但它仍然可以让我们快速了解模型的构成和规模。
4.2. 绘制更新模型的解
让我们解决更新后的模型并绘制新解。祈祷好运。
res = solver.solve(model) # optimize the model
solution_found = pyo.check_optimal_termination(res)
print(f"Optimal solution found: {solution_found}")
# [Out]: Optimal solution found: True
if solution_found:
plot_solution_as_graph(model)

现在我们说到点子了!这正是我们在新增约束后期待的结果:没有形成子回路,这使得解路径现在变成了单一巡回。
请注意,显然,这个解模型的目标值现在是 14.9 公里,而不是不准确的 5.7 公里,我们从不完整的模型中得到的。
👁️ 图形绘制不是地图上的巡回
请注意,这张图片仅仅是图表的一种可能绘图,而不是地理轨迹。你看到的圆圈和链接并不对应于地理空间中的实际路径(如果我们没有在创建过程中使用任何地理信息,它怎么可能对应呢?)。你可以通过多次运行
plot_solution_as_graph(model)来验证这一点:每次运行时,节点的位置都会不同。图表是抽象的数学结构,通过“链接”连接“点”,表示任意实体之间的关系。我们在这里使用图表来研究解决方案的有效性,而不是可视化巴黎的真实旅行。我们在[这篇文章]中做到了这一点。
5. 结论(或规划下一个冲刺)
通过对解决方案的最终验证,我们得出结论,这个更新版本的模型可以解决任何实例的旅行推销员问题,因此我们可以认为它是一个成功的概念验证(POC)。
💡 逐步演进解决方案,每次冲刺一个
这个 POC 尚未解决我们原始的复杂旅游问题,但它确实解决了我们作为第一个基石提出的最小有价值问题。因此,它确实使我们可以证明(即基于证据)更接*复杂问题的有价值解决方案。有了一个最小的有效示例,我们可以更好地评估在我们希望的方向上需要做什么,以及在解决方案的更成熟版本达到之前可以临时简化掉的内容。在任何时候都有一些有用的东西,我们将开发一个越来越有价值的系统,直到我们满足。这就是通往有效解决方案的“敏捷之路”的本质。
这种方法的有效性已经得到验证,我们必须扩展和改进它,以便逐渐涵盖我们原始问题的更多特征,每次迭代都提供逐步有价值的解决方案。在这个 POC 中,我们专注于基本模型的设计和制定,因此我们不得不假设一组固定的站点及其距离矩阵作为给定条件。当然,这具有局限性,下一步应该是有一个能够接受任意数量站点的模型。为此,我们需要一种方法,根据站点列表及其地理坐标自动生成距离矩阵。这就是我们下一个冲刺的目标。
5.1. 接下来做什么
在下一篇文章(第 4 次冲刺)中,我们将创建一个从任何站点列表中自动生成距离矩阵的类。这一功能与我们刚刚在这里构建的模型结合起来,将使我们能够快速地解决不同输入的多个模型,并进行比较。此外,以这种方式对解决方案进行概括,将使我们在未来进行一些敏感性和情景分析时更加轻松。此外,随着我们将这个概念验证升级为“MVP 状态”,我们将开始使用面向对象的代码来保持良好的组织,并为扩展做好准备。不要浪费时间,直接跳到这里:
从地理坐标估算任何一对站点之间的距离,作为解决问题的一个起点……
towardsdatascience.com
脚注
- 实际上,还有一个导致结果错误的原因:模型所依赖的数据也可能是错误的,不只是模型公式。但是,从某种意义上说,如果你把“模型”视为“模型实例”,即,一个具体的模型与具体的数据,那么如果数据错误,模型自然也会错误,这就是我声明中的意思。↩
感谢阅读,下次见!📈😊
随时关注我,向我提问,在评论中给我反馈,或在LinkedIn上联系我。
如何预测玩家流失,借助 ChatGPT 的一些帮助
使用低代码机器学习*台的数据科学 | Actable AI
使用低代码*台分析数据和训练模型以预测玩家流失
·发表于 Towards Data Science ·阅读时长 22 分钟·2023 年 6 月 20 日
--

图片由 Tima Miroshnichenko 在 Pexels 提供
介绍
在游戏世界中,公司不仅致力于吸引玩家,还希望尽可能长时间地保留他们,特别是在依赖游戏内微交易的免费游戏中。这些微交易通常涉及购买游戏内货币,使玩家能够获取用于进阶或定制的物品,同时为游戏的开发提供资金。监控 流失率 是至关重要的,因为高流失率意味着收入显著损失,这反过来会导致开发者和管理者的压力增加。
本文探讨了基于从移动应用程序获取的数据的真实世界数据集的使用,特别关注用户所玩过的关卡。利用 机器学习,这已成为技术领域的一个重要部分,并构成了人工智能 (AI) 的基础,企业可以从他们的数据中提取有价值的洞察。
然而,构建机器学习模型通常需要编码和数据科学专业知识,这使得许多个人和缺乏资源的小公司无法接触到此技术,因为他们没有足够的资金来聘请数据科学家或强大的硬件来处理复杂的算法。
为了应对这些挑战,低代码和无代码机器学习*台应运而生,旨在简化机器学习和数据科学过程,从而减少对广泛编码知识的需求。这样的*台包括Einblick、KNIME、Dataiku、Alteryx和Akkio。
本文使用一个低代码机器学习*台来训练一个能够预测用户是否会停止玩游戏的模型。此外,还探讨了结果解释和可以用来提高模型性能的技术。
本文的其余部分组织如下:
-
*台
-
数据集
-
探索性数据分析
-
训练分类模型
-
提高模型性能
-
创建新特征
-
训练一个新的(希望得到改进的)分类模型
-
生产中的模型部署
-
结论
*台
完整披露——在撰写本文时,我是一名在Actable AI工作的数据科学家,因此本文中将使用该*台。我还参与了 ML 库中新功能的实施和维护,因此我很好奇这个*台在实际问题中的表现。
该*台提供了一个 Web 应用程序,包含多个流行的机器学习方法,适用于传统的分类、回归和分割应用。还提供了一些不那么常见的工具,如时间序列预测、情感分析和因果推断。缺失的数据也可以被填补,数据集的统计信息可以被计算(例如特征之间的相关性、方差分析(ANOVA)等),同时数据可以通过条形图、直方图和词云等工具进行可视化。
还有一个Google Sheets 插件,可以在电子表格中直接进行分析和模型训练。不过,请注意,这个插件中可能不包含最新的功能。

Actable AI 的 Google Sheets 插件。图像由作者提供。
核心库是开源的,托管在GitHub上,由几个知名且值得信赖的框架组成,例如AutoGluon和scikit-learn,这些框架也是开源且免费提供的。这与其他相关*台类似,这些*台也利用了现有的开源解决方案。
然而,这引出了一个问题:如果大多数工具已经可用且免费,那么你为何还要使用这些*台?
主要原因是这些工具需要编程语言如 Python 的知识,因此任何可能对编程不熟悉的人可能会发现难以或无法使用。因此,这些*台旨在以图形用户界面(GUI)的形式提供所有功能,而不是一组编程命令。
更有经验的专业人士也可能通过易于使用的图形界面节省时间,这种界面可能还会提供可用工具和技术的信息描述。一些*台还可能呈现你可能不熟悉的工具,或在处理数据时提供可能有用的警告(如数据泄露的存在,即模型访问了在生产环境中不可用的特征)。
使用这些*台的另一个原因是提供了运行模型的硬件。因此,无需购买和维护自己的计算机和组件,如图形处理单元(GPU)。
数据集
数据集由使用该*台的游戏公司提供,可以在这里查看,并附有CC BY-SA-4 许可证,允许在提供适当信用的情况下进行共享和改编。共有 789,879 行(样本),这相当可观,应有助于减少如模型过拟合等效果。
数据集包含了一个人在移动应用中玩过的每个等级的信息。例如,包含了游戏时间、玩家是否赢得了等级、等级编号等信息。
用户 ID 也已包括在内,但已被匿名化,以免透露原始玩家的身份。一些字段也已被删除。然而,这应该为检验本文章中考虑的 ML *台提供的工具是否在预测玩家是否会流失方面有用提供了一个坚实的基础。
每个特征的意义如下:
-
Churn:如果玩家在两周以上没有玩游戏,则为‘1’,否则为‘0’ -
ServerTime:等级播放时服务器的时间戳 -
EndType:等级结束的原因(如果玩家赢得游戏则主要为‘Win’,如果玩家输掉游戏则为‘Lose’) -
LevelType:等级类型 -
Level:等级编号 -
SubLevel:子等级编号 -
Variant:等级变体 -
Levelversion:等级版本 -
NextCar:未使用(包括在内是为了观察*台如何处理只有一个标签的特征,后文将讨论) -
AddMoves:可用的额外移动 -
DoubleMana:未使用(包括在内是为了观察*台如何处理只有一个标签的特征,后文将讨论) -
StartMoves: 关卡开始时可用的移动次数 -
ExtraMoves: 购买的额外移动次数 -
UsedMoves: 玩家使用的移动次数 -
UsedChangeCar: 未使用(包括在内是为了观察*台如何处理只有一个标签的特征,稍后将讨论) -
WatchedVideo: 是否观看了视频,以获得额外的移动机会 -
BuyMoreMoves: 玩家购买更多移动的次数 -
PlayTime: 玩这个关卡的时间 -
Scores: 玩家获得的分数 -
UsedCoins: 在关卡中使用的总金币 -
MaxLevel: 玩家达到的最高关卡 -
Platform: 设备类型 -
UserID: 玩家 ID -
RollingLosses: 玩家连续失败的次数
探索性数据分析
训练前的第一步是通过探索性数据分析(EDA)了解数据。EDA 是一种数据分析方法,涉及总结、可视化和理解数据集的主要特征。其目标是深入了解数据,识别任何模式、趋势、异常或存在的问题(例如缺失值),这些都可以帮助指导所使用的特征和模型。
让我们首先查看关卡结束的主要原因:

数据集中某些列的统计信息。图片来源:作者。
在上图中,我们可以看到关卡结束的主要原因(由 EndType 表示)是玩家输掉了游戏(63.6%),而获胜的玩家占 35.2%。我们还可以看到,UsedChangeCar 列似乎没有用,因为它在所有行中包含相同的值。
一个非常重要的观察是,我们的目标值高度不*衡,在前 10,000 行中只有 63 个样本(即 0.6%的数据)具有流失值 1(即玩家已流失)。这一点需要记住,因为我们的模型可能很容易被偏向仅预测 Churn 的值为 0。原因是模型可以在一些指标上取得非常好的值;在这种情况下,如果使用一个简单的模型选择最常见的类别,它将 99.4%的时间是正确的!我邀请你阅读 Baptiste Rocca 和Jason Brownlee的两篇精彩文章,以了解更多相关信息。
不幸的是,Actable AI 目前尚不提供处理不*衡数据的方法,例如通过合成少数类过采样技术 (SMOTE)或使用类权重或不同的采样策略。这意味着在选择优化指标时需要特别小心。如上所述,准确率可能不是最佳选择,因为即使某一类样本从未被正确标记,高准确率也可以实现。
另一种有用的分析方法是特征之间的相关性,特别是预测特征与目标特征之间的相关性。这可以使用‘相关性分析’工具完成,结果可以直接在 Actable AI *台上 查看:

正/负图表。图片由作者提供。
在上面的图表中,蓝色条形图表示当值为 1 时,特征与 Churn 的正相关,而橙色条形图表示负相关。需要注意的是,相关性介于 -1 和 1 之间,其中正值表示两个特征趋向于相同方向变化(例如,两者都增加或都减少),而负相关仅表示一个特征增加或减少时,另一个特征则相反。因此,相关性的大小(忽略负号)可能是最重要的注意点。
有一些结论,例如,失去一关的玩家更容易流失(最上面的蓝色条形图),而赢得一关的玩家往往继续游戏(第三个橙色条形图)。但是,也需要注意的是,这些值相当低,表明这些特征与目标的相关性较弱。这意味着可能需要进行 特征工程,通过现有特征创建新的特征,以捕捉更显著的信息,从而使模型能够进行更准确的预测。特征工程将在本文后续部分详细讨论。
然而,在创建新特征之前,值得查看仅使用数据集中原始特征可以实现什么样的性能。因此,下一步可能会更令人兴奋——训练一个模型以查看可以获得什么样的性能。
训练分类模型
由于我们希望预测用户是否会停止游戏,这是一个 分类 问题,需要选择多个标签中的一个。在我们的情况下,问题涉及分配两个标签中的一个(‘1’对应于‘Churn’,‘0’对应于‘No Churn’),这进一步使其成为一个 二分类 问题。
这个过程主要通过 AutoGluon 库完成,该库自动训练多个模型,然后选择性能最佳的模型。这避免了手动训练单个模型并比较其性能的麻烦。
在 Actable AI *台上需要设置多个参数,以下是我的选择:

在 Actable AI 网络应用中选择的选项。图片由作者提供。
也可以选择用于模型优化的指标。我使用了接收者操作特征曲线下的面积 (AUC ROC) ,因为它对之前讨论的类别不*衡问题的敏感性要小得多。数值范围从 0 到 1(后者为完美分数)。
一段时间后,结果被生成并显示,也可以在这里查看。计算了一些不同的指标,这不仅是良好的实践,而且如果我们真正想了解我们的模型的话,几乎是必要的,因为每个指标关注模型性能的某些方面。
显示的第一个指标是优化指标,其值为 0.675:

评估指标。图片由作者提供。
这不是很好,但请记住,在我们的 EDA 过程中,特征与目标的相关性相当弱,因此性能不佳并不令人惊讶。
这一结果还突出了理解结果的重要性;我们通常会对 0.997(即 99.7%)的准确率感到非常满意。然而,这主要是由于数据集的不*衡性质,如前所述,因此不应过于重视。同时,像精确度和召回率这样的分数默认基于 0.5 的阈值,这可能不是最适合我们应用的阈值。
ROC和精确度-召回率曲线也显示出来,这再次清晰地表明性能略显不足:


训练模型的 ROC 曲线(左)和精确度-召回率曲线(右)。图片由作者提供。
这些曲线还对确定我们在最终应用中可以使用的阈值非常有用。例如,如果希望最小化假阳性数量,则可以选择一个模型获得更高精确度的阈值,并检查相应的召回率。
最佳模型获得的每个特征的重要性也可以查看,这可能是更有趣的结果之一。这是使用AutoGluon 的排列重要性计算的。P 值也显示出来,以确定结果的可靠性:

特征重要性表。图片由作者提供。
也许并不意外,最重要的特征是EndType(显示导致等级结束的原因,如胜利或失败),其次是MaxLevel(用户玩过的最高等级,较高的数字表示玩家在游戏中非常活跃)。
另一方面,UsedMoves(玩家执行的动作次数)几乎没有用,而StartMoves(玩家可用的动作次数)实际上可能会损害性能。这也很有道理,因为单独的动作次数和可用动作次数并没有提供太多信息;它们之间的比较可能会更加有用。
我们还可以查看每个类别(在这种情况下是 1 或 0)的估计概率,这些概率用于推导预测类别(默认情况下,概率最高的类别被指定为预测类别):

包含原始值、Shapley 值和预测值的表格。图片来源于作者。
可解释的 AI 正变得越来越重要,以理解模型行为,这也是像Shapley 值这样的工具越来越受欢迎的原因。这些值表示特征对预测类别概率的贡献。例如,在第一行中,我们可以看到 RollingLosses 值为 36 会降低该玩家的预测类别(类别 0,即该人将继续玩游戏)的概率。
相反,这意味着其他类别(类别 1,即玩家流失的概率)增加了。这是有道理的,因为较高的RollingLosses值表示玩家连续失去了很多等级,因此更可能由于挫折而停止玩游戏。另一方面,较低的RollingLosses值通常会提高负类别的概率(即玩家不会停止玩游戏)。
如前所述,训练并评估了多个模型,然后选择最佳模型。值得注意的是,在这种情况下,最佳模型是LightGBM,它也是最快的之一。

关于训练模型的信息。图片来源于作者。
提高模型性能
现在,我们可以尝试提高模型性能。也许最简单的方法之一是选择“优化质量”选项,看看我们能走多远。这个选项配置了几个通常能提高性能的参数,但可能会导致训练时间变慢。以下是获得的结果(你也可以在这里查看):

使用“优化质量”选项时的评估指标。图像由作者提供。
再次关注 ROC AUC 指标,性能从 0.675 提高到 0.709。这对于如此简单的更改来说是相当不错的提升,尽管仍远未理想。我们还能做些什么来进一步提高性能?
创建新特征
如前所述,我们可以通过特征工程来做到这一点。这涉及从现有特征中创建新特征,这些新特征能够捕捉更强的模式,并与待预测变量有更高的相关性。
在我们的例子中,数据集中特征的范围相当狭窄,因为这些值只涉及到一个记录(即用户玩过的一个关卡的信息)。因此,通过总结一段时间内的记录,获得更全球性的视角可能会非常有用。这样,模型将了解用户的历史趋势。
例如,我们可以确定玩家使用了多少额外的动作,从而提供一个难度的衡量指标;如果需要的额外动作很少,那么关卡可能太简单;另一方面,较高的数字可能意味着关卡太难。
还可以通过检查玩家在过去几天内的游戏时间,来检查用户是否沉浸并投入于游戏。如果玩家玩得很少,这可能意味着他们失去了兴趣,可能很快就会停止游戏。
有用的特征在不同领域之间有所不同,因此重要的是要尝试找到与当前任务相关的信息。例如,你可以查找和阅读研究论文、案例研究和文章,或寻求在该领域有经验的公司或专业人士的建议,他们对最常见的特征、它们之间的关系、潜在的陷阱以及最有可能有用的新特征都有深入了解。这些方法有助于减少试错过程,加快特征工程过程。
考虑到*期在大型语言模型(LLMs)方面的进展(例如,你可能听说过ChatGPT……),以及考虑到特征工程对经验不足的用户可能有些令人生畏,我很好奇 LLMs 是否可以在提供创建特征的想法上有所帮助。我正是这样做的,结果如下:

当询问关于如何创建新特征以更准确地预测玩家流失时,ChatGPT 的回答实际上是非常有用的。图像由作者提供。
ChatGPT 的回答实际上相当不错,并且也指出了上述讨论的许多基于时间的特征。当然,请记住,如果所需的信息不可用,我们可能无法实现所有建议的功能。此外,众所周知,它容易产生幻觉,因此可能无法提供完全准确的答案。
我们可以从 ChatGPT 获得更相关的回答,例如通过指定我们正在使用的功能或使用提示,但这超出了本文的范围,留给读者作为练习。不过,LLMs 可以被视为一个起始步骤,尽管仍强烈建议从论文、专业人士等处寻求更可靠的信息。
在 Actable AI *台上,可以使用相当知名的SQL编程语言创建新功能。对于不太熟悉 SQL 的用户,利用ChatGPT自动生成查询的方法可能会很有帮助。然而,根据我的有限实验,这种方法的可靠性可能会有些不稳定。
为确保输出结果的准确计算,建议手动检查部分结果以验证是否正确计算了预期的输出。这可以通过在 SQL Lab 中运行查询后查看显示的表格轻松完成,SQL Lab 是 Actable AI 用来编写和运行 SQL 代码的界面。
以下是我用来生成新列的 SQL 代码,如果你想创建其他功能,这些代码可以帮助你入门:
SELECT
*,
SUM("PlayTime") OVER UserLevelWindow AS "time_spent_on_level",
(a."Max_Level" - a."Min_Level") AS "levels_completed_in_last_7_days",
COALESCE(CAST("total_wins_in_last_14_days" AS DECIMAL)/NULLIF("total_losses_in_last_14_days", 0), 0.0) AS "win_to_lose_ratio_in_last_14_days",
COALESCE(SUM("UsedCoins") OVER User1DayWindow, 0) AS "UsedCoins_in_last_1_days",
COALESCE(SUM("UsedCoins") OVER User7DayWindow, 0) AS "UsedCoins_in_last_7_days",
COALESCE(SUM("UsedCoins") OVER User14DayWindow, 0) AS "UsedCoins_in_last_14_days",
COALESCE(SUM("ExtraMoves") OVER User1DayWindow, 0) AS "ExtraMoves_in_last_1_days",
COALESCE(SUM("ExtraMoves") OVER User7DayWindow, 0) AS "ExtraMoves_in_last_7_days",
COALESCE(SUM("ExtraMoves") OVER User14DayWindow, 0) AS "ExtraMoves_in_last_14_days",
AVG("RollingLosses") OVER User7DayWindow AS "RollingLosses_mean_last_7_days",
AVG("MaxLevel") OVER PastWindow AS "MaxLevel_mean"
FROM (
SELECT
*,
MAX("Level") OVER User7DayWindow AS "Max_Level",
MIN("Level") OVER User7DayWindow AS "Min_Level",
SUM(CASE WHEN "EndType" = 'Lose' THEN 1 ELSE 0 END) OVER User14DayWindow AS "total_losses_in_last_14_days",
SUM(CASE WHEN "EndType" = 'Win' THEN 1 ELSE 0 END) OVER User14DayWindow AS "total_wins_in_last_14_days",
SUM("PlayTime") OVER User7DayWindow AS "PlayTime_cumul_7_days",
SUM("RollingLosses") OVER User7DayWindow AS "RollingLosses_cumul_7_days",
SUM("PlayTime") OVER UserPastWindow AS "PlayTime_cumul"
FROM "game_data_levels"
WINDOW
User7DayWindow AS (
PARTITION BY "UserID"
ORDER BY "ServerTime"
RANGE BETWEEN INTERVAL '7' DAY PRECEDING AND CURRENT ROW
),
User14DayWindow AS (
PARTITION BY "UserID"
ORDER BY "ServerTime"
RANGE BETWEEN INTERVAL '14' DAY PRECEDING AND CURRENT ROW
),
UserPastWindow AS (
PARTITION BY "UserID"
ORDER BY "ServerTime"
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
)
) AS a
WINDOW
UserLevelWindow AS (
PARTITION BY "UserID", "Level"
ORDER BY "ServerTime"
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
),
PastWindow AS (
ORDER BY "ServerTime"
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
),
User1DayWindow AS (
PARTITION BY "UserID"
ORDER BY "ServerTime"
RANGE BETWEEN INTERVAL '1' DAY PRECEDING AND CURRENT ROW
),
User7DayWindow AS (
PARTITION BY "UserID"
ORDER BY "ServerTime"
RANGE BETWEEN INTERVAL '7' DAY PRECEDING AND CURRENT ROW
),
User14DayWindow AS (
PARTITION BY "UserID"
ORDER BY "ServerTime"
RANGE BETWEEN INTERVAL '14' DAY PRECEDING AND CURRENT ROW
)
ORDER BY "ServerTime";
在这段代码中,“窗口”被创建以定义需要考虑的时间范围,如过去一天、过去一周或过去两周。落在该范围内的记录将用于功能计算,这主要是为了提供一些关于玩家在游戏中的历程的历史背景。功能的完整列表如下:
-
time_spend_on_level:用户在某一关卡上花费的时间。这反映了关卡的难度。 -
levels_completed_in_last_7_days:用户在过去 7 天内完成的关卡数量(1 周)。这反映了关卡的难度、毅力和游戏沉浸感。 -
total_wins_in_last_14_days:用户在过去 14 天内赢得关卡的总次数 -
total_losses_in_last_14_days:用户在过去 14 天内失败的关卡总次数 -
win_to_lose_ratio_in_last_14_days:过去 14 天内获胜次数与失败次数的比例(total_wins_in_last_14_days/total_losses_in_last_14_days) -
UsedCoins_in_last_1_days:前一天使用的金币数量。这反映了游戏难度水*和玩家花费游戏货币的意愿。 -
UsedCoins_in_last_7_days:过去 7 天内使用的金币数量(1 周) -
UsedCoins_in_last_14_days:过去 14 天(2 周)内使用的硬币数量 -
ExtraMoves_in_last_1_days:用户在前一天内使用的额外移动次数。表示了等级的困难程度。 -
ExtraMoves_in_last_7_days:用户在过去 7 天(1 周)内使用的额外移动次数 -
ExtraMoves_in_last_14_days:用户在过去 14 天(2 周)内使用的额外移动次数 -
RollingLosses_mean_last_7_days:用户在过去 7 天(1 周)内的累计损失*均数。表示了等级的困难程度。 -
MaxLevel_mean:所有用户达到的最高等级的*均值。 -
Max_Level:玩家在过去 7 天(1 周)内达到的最高等级。结合MaxLevel_mean,可以指示玩家相对于其他玩家的进展。 -
Min_Level:用户在过去 7 天(1 周)内玩的最低等级 -
PlayTime_cumul_7_days:用户在过去 7 天(1 周)内玩的总时间。表示玩家在游戏中的沉浸度。 -
PlayTime_cumul:用户玩的总时间(自第一个可用记录以来) -
RollingLosses_cumul_7_days:过去 7 天(1 周)内的累计滚动损失总数。这表示了困难程度。
计算某一行新特征的值时,重要的是只使用过去的记录。换句话说,必须避免使用未来的观测数据,因为在生产环境中模型显然无法访问任何未来的值。
一旦对创建的特征感到满意,我们可以将表格保存为新的数据集,并运行一个新的模型,应该(希望)达到更好的性能。
训练一个新的(希望有所改进的)分类模型
现在是时候看看新列是否有用。我们可以重复之前的步骤,唯一的不同是现在使用包含额外特征的新数据集。使用相同的设置以确保与原始模型的公*比较,结果如下(也可以在这里查看):

使用新列的评估指标。图像由作者提供。
ROC AUC 值为 0.918,相比原始值 0.675 有了显著改善。它甚至比为质量优化的模型(0.709)还要好!这证明了理解数据和创建能够提供更丰富信息的新特征的重要性。
现在,了解哪些新特征实际最有用会很有趣;我们可以再次查看特征重要性表:

新模型的特征重要性表。图像由作者提供。
看起来过去两周的总损失数相当重要,这很有意义,因为一个玩家输掉游戏的次数越多,他们变得沮丧并停止游戏的可能性就越大。
所有用户的*均最大水*似乎也很重要,这再次是有意义的,因为它可以用来确定一个玩家与大多数其他玩家的差距——远高于*均水*表明一个玩家对游戏非常投入,而远低于*均水*的值可能表明玩家的动机还不够强烈。
这些只是我们可以创建的一些简单特性。还有其他特性可以创建,可能进一步提高性能。我将把这留给读者作为练习,看看还可以创建哪些其他特性。
在相同时间限制下,训练一个优化质量的模型并没有提高性能。然而,这也许可以理解,因为使用的特性更多,因此可能需要更多时间进行优化。如这里所示,将时间限制增加到 6 小时确实将性能提升到 0.923(以 AUC 为标准):

使用新特性并优化质量时的评估指标结果。图像由作者提供。
还应该注意到一些指标,如精准率和召回率,仍然相当差。这是因为假设了 0.5 的分类阈值,而这可能并非最佳。尽管可以通过点击曲线来更改阈值,AUC 是阈值无关的,可以提供更全面的性能图像。如前所述,AUC 在训练不*衡数据集时作为优化指标尤为有用,如此处所示。
经过训练的模型的 AUC 性能总结如下:
┌─────────────────────────────────────────────────────────┬───────────┐
│ Model │ AUC (ROC) │
├─────────────────────────────────────────────────────────┼───────────┤
│ [Original features](https://app.actable.ai/r/C3FLuD7jpJY2NaV2VLdKDg) │ 0.675 │
│ [Original features + optim. for quality](https://app.actable.ai/r/ze61cu96uA3ZKApZgUCztA) │ 0.709 │
│ [Engineered features](https://app.actable.ai/r/GDfT0fXaMmYO-5cmt-XxZA) │ 0.918 │
│ [Engineered features + optim. for quality + longer time](https://app.actable.ai/r/G8frdBHCjbl9twVSTKuP8w) │ 0.923 │
└─────────────────────────────────────────────────────────┴───────────┘
生产环境中的模型部署
如果我们不能在新数据上实际使用一个好的模型,那么它也没有什么用处。机器学习*台可能提供在训练模型的基础上生成未来未见数据预测的能力。例如,Actable AI *台允许使用一个API,使得模型可以在*台外的数据上使用,比如导出模型或插入原始值以获得即时预测。
然而,定期在未来数据上测试模型是至关重要的,以确定其是否仍按预期运行。确实,可能需要用更新的数据重新训练模型。这是因为特征(例如特征分布)可能随时间变化,从而影响模型的准确性。
例如,一家公司可能会引入一项新政策,从而影响客户行为(无论是积极的还是消极的),但如果模型无法访问反映新变化的特征,则可能无法考虑到这一新政策。如果出现这种剧烈变化但没有可以告知模型的特征,那么可以考虑使用两个模型:一个用于训练和处理旧数据,另一个用于训练和处理新数据。这将确保模型专门适用于具有不同特征的数据,而这些特征可能很难被单一模型捕捉。
结论
在本文中,使用了包含用户在移动应用中每个级别信息的真实数据集,训练了一个分类模型,该模型可以预测玩家是否会在两周内停止玩游戏。
整个处理流程都被考虑在内,从探索性数据分析(EDA)到模型训练,再到特征工程。提供了关于结果解释和如何改进的讨论,将值从 0.675 提高到 0.923(其中 1.0 为最大值)。
新创建的特征相对简单,当然还存在许多可以考虑的特征。此外,还可以考虑像特征归一化和标准化这样的技术。一些有用的资源可以在这里和这里找到。
关于 Actable AI *台,当然我可能有些偏见,但我确实认为它有助于简化一些数据科学家和机器学习专家需要完成的繁琐过程,具备以下期望的方面:
-
核心 ML 库是开源的,因此任何具有良好编程知识的人都可以验证其安全性。任何了解 Python 的人也可以使用它。
-
对于那些不了解 Python 或不熟悉编程的人来说,图形用户界面(GUI)提供了一种使用多种分析和可视化的方式,操作起来相对简单。
-
开始使用该*台并不困难(它不会用过多的技术信息让用户感到困惑,从而劝退知识较少的人)。
-
免费层允许对公开的数据集进行分析
-
提供了大量工具(除了本文讨论的分类问题之外)
尽管如此,仍然存在一些缺点,并且有几个方面可以改进,例如:
-
免费层不允许在私有数据上运行 ML 模型
-
用户界面看起来有些过时
-
一些可视化可能不够清晰,有时难以解释。
-
应用程序有时响应较慢
-
不支持不*衡数据
-
仍然需要一些数据科学和机器学习的知识才能从*台中获得最大的收益(虽然其他*台可能也是如此)
在未来的文章中,我将考虑使用其他*台,以确定它们的优缺点,从而确定哪些使用场景最适合每个*台。
直到那时,希望这篇文章对你来说是一次有趣的阅读!请随时留下你的反馈或问题!
你对这篇文章有什么想法吗?请随时在 LinkedIn 上发留言、评论,或直接给我发消息!
此外,请确保 关注 我,以确保在未来文章发布时能够及时收到通知。

图片来源于 Pixabay
作者在撰写本文时是 Actable AI 的数据科学家。
玩转苹果健康数据
原文:
towardsdatascience.com/playing-with-apple-health-data-be426bcff85e
数据科学项目
对血糖数据进行分类和时间序列预测
·发表于 Towards Data Science ·阅读时间 12 分钟·2023 年 2 月 27 日
--

今天,我们将深入探索苹果的世界。
我是一名糖尿病患者,佩戴一款持续血糖监测仪,简称 CGM。它每几分钟记录一次我的血糖,并将结果传送到我的手机上。方便的是,我可以与医生分享我的数据,我们一起讨论特定的趋势和模式,并做出真正基于数据的决策。
自然地,我对应用程序收集的数据感兴趣。由于 Dexcom G6(我的 CGM 品牌)没有 CSV 导出功能,我通过将数据同步到苹果健康来绕过这个限制。问题是苹果健康不允许用户过滤导出的数据。因此,生成的导出文件是一个巨大的文件。如果你用了手机至少一年,导出的文件大到几乎无法打开。差点儿!
我接受了这个挑战:找到读取文件并进行处理的方法。
Python,救援!
规划
我们将像处理一个优秀的数据科学项目一样来应对这个挑战。首先,我们将使用 PAPEM-DM 框架来生成有见地的图形和一个干净的 CSV 文件。
这里有两个主要障碍:
-
文件格式为 XML。
-
这个文件非常大,以至于在线的 XML 阅读器和转换器都无法打开它。
-
我们不知道架构是什么。
这是我们的方案:
-
将 XML 文件读取到数据框中
-
将数据缩减为仅包含 Dexcom G6 数据
-
稍微清理一下数据框
-
将数据框导出为 CSV
-
进行特征工程
-
进行探索性数据分析(制作漂亮的图表)
-
预测血糖水*是高、正常还是低
-
进行一些时间序列预测
从 XML 到 CSV 的 Apple Health 数据采集
是时候启动 Jupyter notebook 并获取我们的数据了!
首先,让我们导入一些 Python 包。
# setting the random seed for reproducibility
import random
random.seed(493)
# for reading the xml file
import xml.etree.ElementTree as ET
# for manipulating dataframes
import pandas as pd
import numpy as np
# for working with timestamps
import datetime as dt
from dateutil.parser import parse
# for modeling
from sklearn import preprocessing
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from imblearn.over_sampling import SMOTE
from sklearn.metrics import confusion_matrix
from sklearn.metrics import classification_report
from sklearn.metrics import roc_auc_score
from sklearn.metrics import roc_curve
from sklearn import metrics
# for time-series forecasting
import matplotlib.pyplot as plt
from prophet import Prophet
# for visualizations
%matplotlib inline
import matplotlib.pyplot as plt
import seaborn as sns
sns.set(style="whitegrid")
# to print out all the outputs
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
# set display options
pd.set_option('display.max_columns', None)
pd.set_option('display.max_rows', None)
pd.set_option('display.max_colwidth', None)
第二步,让我们创建元素树对象 [1]。
# create element tree object
tree = ET.parse('export.xml')
# extract the attributes for every health record
root = tree.getroot()
record_list = [x.attrib for x in root.iter('Record')]
第三步,让我们将元素树对象转换为数据框 [1]。
# create the dataframe
df = pd.DataFrame(record_list)
在下面,我们可以看到类型列中的奇怪名称。

截图由 Ednalyn C. De Dios 提供
让我们解决这个问题!
# shorter observation names
df['type'] = df['type'].str.replace('HKQuantityTypeIdentifier', '')
df['type'] = df['type'].str.replace('HKCategoryTypeIdentifier', '')
来看看:

截图由 Ednalyn C. De Dios 提供
我们的数据框很大!

截图由 Ednalyn C. De Dios 提供
让我们搜索我们想要的内容。

截图由 Ednalyn C. De Dios 提供
让我们进行筛选。
df['type'].value_counts()
然后,通过仅保留必要的列和行,进一步缩减我们的数据框大小。
df1 = df.loc[df['type'] == 'BloodGlucose']
df1 = df1[['startDate', 'value']]
很好!

截图由 Ednalyn C. De Dios 提供
现在,让我们做一点清理。下面,我们将 startDate 列的数据类型更改为 datetime,并将值列强制为数字类型。最后,我们用 1 填充那些未必测量到任何值的记录的空白字段。
df1["startDate"] = pd.to_datetime(df1["startDate"])
# value is numeric, NaN if fails
df1['value'] = pd.to_numeric(df1['value'], errors='coerce')
# some records do not measure anything, just count occurences
# filling with 1.0 (= one time) makes it easier to aggregate
df1['value'] = df1['value'].fillna(1.0)
最后,是时候将我们的数据框导出为 CSV 文件。我们将其命名为 “blood_glucose.csv”。
df1.to_csv('blood_glucose.csv', index=False)
从 Dexcom G6 的 Apple Health 数据的准备
现在,让我们准备进行一些数据整理。
首先,为了清晰起见,让我们将 ‘startDate’ 重命名为 ‘timestamp’。然后,我们将从 timestamp 列中提取时间组件和属性。
# rename date column
df2 = df1.rename(columns={'startDate':'timestamp'})
# extract time components from the timestamp column
df2['year'] = df2.timestamp.dt.year
df2['month'] = df2.timestamp.dt.month
df2['day'] = df2.timestamp.dt.day
df2['hour'] = df2.timestamp.dt.hour
# extract time attributes from the timestamp column
df2['day_of_year'] = df2.timestamp.dt.dayofyear
df2['week_of_year'] = df2.timestamp.dt.weekofyear
df2['day_of_week'] = df2.timestamp.dt.dayofweek
我们还将从星期几列中提取星期几特征,并根据小时列推导出一天中的时间段。
# extract weekday characteristic from the day of the week column
df2['weekday'] = np.where(df2['day_of_week'] < 5, True, False)
# extract part of the day from the hour column
def get_day_period(x):
if x in range(6,12):
return 'Morning'
elif x in range(12,18):
return 'Afternoon'
elif x in range(18,23):
return 'Evening'
else:
return 'Late night'
df2['part_of_day'] = df2['hour'].apply(get_day_period)
接下来,我们将对记录的血糖水*进行分类,判断其是高、正常还是低。
# extract blood glucose level from the value column
def get_status(row):
if row['value'] < 70:
val = "Low"
elif row['value'] <= 180:
val = "Normal"
else:
val = "High"
return val
df2['status'] = df2.apply(get_status, axis=1)
查看值计数,我们可以看到状态类别之间存在巨大的不*衡。

截图由 Ednalyn C. De Dios 提供
我决定放弃低状态,并创建另一列来指示读取值是否为高。
# creates a new column and designates a row as either high or low
df2['high'] = np.where(df2['status'] != 'High', '0', '1').astype('int32')
现在,我们准备删除 timestamp 和 status 列,因为我们不再需要它们。
df2 = df2.drop(columns=['status', 'timestamp'])
让我们现在看看我们的数据框。

截图由 Ednalyn C. De Dios 提供
Dexcom G6 的 Apple Health 数据的探索性数据分析
我们即将进行一些可视化,所以让我们设置图形大小和布局。
plt.rcParams["figure.figsize"] = [10, 6]
plt.rcParams["figure.autolayout"] = True
在深入探讨之前,让我们检查一下我们正在处理的年份。让我们执行 value_counts()。
show_values(df2, ['year'])
不过我们可以看到年份的顺序混乱,所以让我们手动创建一个正确的年份顺序列表。
years = [2020, 2021, 2022, 2023]
接下来是令人兴奋的部分。让我们绘制一些小提琴图!下面我们定义一个函数,根据一天中的时间、血糖值以及是否为周末来创建一些小提琴图。以下是实现这一切的代码:
def show_violin(df, year):
ax = sns.violinplot(x="part_of_day",
y="value",
hue="weekday",
split=True,
data=df[df['year'] == year],
order=['Morning', 'Afternoon', 'Evening', 'Late night'])
ax.set_title('Distribution of Blood Glucose Value per Part of the Day ' + str(year), fontsize=16)
# Set label for x-axis
plt.ylabel( "Blood Glucose" , size = 12 )
plt.xlabel( "Part of the Day" , size = 12 )
plt.legend(title='Weekday?', loc='upper right')
plt.show()
以及用于根据年份创建单独的小提琴图的代码片段:
for year in years:
show_violin(df2, year)
结果是:

截图由 Ednalyn C. De Dios 提供
美丽!
但如果我们想将所有年份的数据进行比较呢?让我们再次做同样的事情,只是将图表叠加在一起,看看它们的效果。在代码中,这转化为在函数中省略 plt.show() 这一行。这是代码:
def show_violin_no_legend(df, year):
ax = sns.violinplot(x="part_of_day",
y="value",
hue="weekday",
split=True,
data=df[df['year'] == year],
order=['Morning', 'Afternoon', 'Evening', 'Late night'])
ax.set_title('Distribution of Blood Glucose Value per Part of the Day Over the Years', fontsize=16)
# Set label for x-axis
plt.ylabel( "Blood Glucose" , size = 12 )
plt.xlabel( "Part of the Day" , size = 12 )
ax.get_legend().remove()
结果是:

截图由 Ednalyn C. De Dios 提供
完美!
让我们为自己鼓掌。我们成功地将许多维度压缩到了一个漂亮的图表中!
探索性数据分析没有唯一正确的方法。因此,我们将跳过其余常见的分析方法(如分布、时间序列图等),直接进入建模阶段。
建模 — 逻辑回归
首先,让我们丢弃值列,因为我们将使用高列作为目标变量。
df3 = df2.drop(columns=['value'])
由于我们将进行分类任务以预测读数是否会很高,所以让我们为我们的分类变量创建虚拟变量。
categorical_vars = ['month', 'weekday', 'part_of_day']
for var in categorical_vars:
cat_list='var'+'_'+var
cat_list = pd.get_dummies(df3[var], prefix=var)
dfx=df3.join(cat_list)
df3=dfx
data_vars=df3.columns.values.tolist()
to_keep=[i for i in data_vars if i not in categorical_vars]
df4=df3[to_keep]
df4.columns.values
让我们检查一下数据框是否存在类别不*衡:
show_values(df4, ['high'])
如上所示,高值的数量远远少于非高值。这可能会导致我们的模型不准确,因此我们将使用 SMOTE 来解决这个问题。SMOTE(合成少数类过采样技术)是一种用于解决数据集不*衡问题的机器学习技术,它通过在现有少数类数据点之间进行插值来创建合成的数据点。它通过随机选择一个少数类数据点并找到其 k 个最*邻来实现。然后随机选择这些邻居中的一个,并通过它们之间的插值创建一个新的数据点。
新的数据点是通过选择连接少数类数据点及其选择的邻居的线段上的一个随机点来创建的。这个过程会重复进行,直到少数类与多数类*衡为止。
结果是一个*衡的数据集,这使得机器学习算法可以更有效地进行训练。此外,通过创建合成的数据点,SMOTE 确保分类器不会偏向于多数类别,从而提高在少数类别上的表现。
这是从 Susan Li 的关于逻辑回归的文章中借用的 SMOTE 实现。[2]
X = df4.loc[:, df4.columns != 'high']
y = df4.loc[:, df4.columns == 'high']
os = SMOTE(random_state=493)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=493)
columns = X_train.columns
os_data_X,os_data_y=os.fit_resample(X_train, y_train)
os_data_X = pd.DataFrame(data=os_data_X,columns=columns )
os_data_y= pd.DataFrame(data=os_data_y,columns=['high'])
# we can check the numbers of our data
print("length of oversampled data is ",len(os_data_X))
print("Number of no subscription in oversampled data",len(os_data_y[os_data_y['high']==0]))
print("Number of subscription",len(os_data_y[os_data_y['high']==1]))
print("Proportion of no subscription data in oversampled data is ",len(os_data_y[os_data_y['high']==0])/len(os_data_X))
print("Proportion of subscription data in oversampled data is ",len(os_data_y[os_data_y['high']==1])/len(os_data_X))
接下来,我们将通过运用常识和直觉来进行特征选择,以决定使用哪些列来预测高血糖。
cols=['hour',
'month_1',
'month_2',
'month_3',
'month_4',
'month_5',
'month_6',
'month_7',
'month_8',
'month_9',
'month_10',
'month_11',
'month_12',
'weekday_False',
'weekday_True',
'part_of_day_Afternoon',
'part_of_day_Evening',
'part_of_day_Late night',
'part_of_day_Morning']
然后,我们将把数据框分成自变量和因变量,即 X 和 y。
X=os_data_X[cols]
y=os_data_y['high']
然后,我们将进行训练-测试-划分,并创建一个逻辑回归对象,接着用我们的数据进行拟合。
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=493)
logreg = LogisticRegression(solver="liblinear")
logreg.fit(X_train, y_train)
终于,我们准备进行预测了!
y_pred = logreg.predict(X_test)
print('Accuracy of logistic regression classifier on test set: {:.2f}'.format(logreg.score(X_test, y_test)))
我们还将添加混淆矩阵和其他指标来评估模型的表现。
confusion_matrix = confusion_matrix(y_test, y_pred)
print(confusion_matrix)
结果是:

截图由 Ednalyn C. De Dios 提供
由于我们进行的是二分类,我们将检查 ROC(接收者操作特征)和 AUC(曲线下面积)。
ROC 曲线是一个图表,显示了二分类器的真实正例率(灵敏度)和假正例率(1 — 特异性)之间的权衡,随着分类阈值的变化。换句话说,它显示了分类器在不同置信水*下区分正例和负例的能力。
ROC 曲线是通过绘制真实正例率与假正例率的关系图创建的。AUC 是 ROC 曲线下的面积,范围从 0 到 1。AUC 为 1 表示分类器可以完美区分正例和负例,而 AUC 为 0.5 表示分类器的表现不比随机猜测好。

截图由 Ednalyn C. De Dios 提供
用通俗的话来说,ROC AUC 提供了一种衡量二分类器在正确区分正例和负例的能力的方法,无论使用什么阈值。高 AUC 分数表示分类器表现良好,而低分数表示表现较差。它是评估和比较不同分类器性能的有价值工具,尤其是在数据集不*衡或假正例和假负例的成本不同的情况下。
时间序列预测 — Prophet
在我们的最后一次尝试中,我们将使用 Prophet 进行时间序列预测。Prophet 是一个由 Facebook 开发的时间序列预测工具,旨在用户友好和灵活。在使用 Prophet 进行时间序列预测时,需要注意以下几点:
-
数据应该符合特定格式:Prophet 需要时间序列数据的特定格式,包含一个名为‘ds’的列用于日期/时间,一个名为‘y’的列用于要预测的值。
-
需要处理异常值:异常值可能会显著影响预测的准确性,因此必须识别和妥善处理这些异常值。
-
需要考虑季节性和趋势成分:Prophet 旨在捕捉时间序列数据中的季节性和趋势成分,因此如果数据中存在这些成分,必须将其包含在模型中。
-
应包括节假日和事件:Prophet 允许你包含可能影响时间序列数据的节假日和事件,如国家假日或营销活动。
-
可以包含多个时间序列:Prophet 可以处理具有不同季节性和趋势的多个时间序列,允许你同时建模和预测它们。
-
需要考虑预测的不确定性:Prophet 为其预测提供了不确定性区间,在根据预测做决策时需要考虑这些不确定性区间。
-
超参数需要调整:Prophet 具有多个可以调整的超参数,以提高预测的准确性,例如用于捕捉季节性的傅里叶项的数量。
总体而言,Prophet 可以成为一个强大的时间序列预测工具,但必须仔细考虑数据和模型设置,以确保准确和有用的预测。因此,我们将导入之前保存的 csv 文件,专注于我们的数据框架,避免所有的转换。
df5 = pd.read_csv('blood_glucose.csv', parse_dates=True, infer_datetime_format=True)
接下来,我们将把列重命名为 ‘ds’ 和 ‘y’,因为 Prophet 需要这样的格式。
df5 = df5.rename(columns={'startDate':'ds', 'value':'y'})
我们还将确保 ‘ds’ 列是 datetime 格式的。通过 tx_localize(None),我们也在去除时间戳中的时区部分,因为 Prophet 不喜欢这样。
df5["ds"] = pd.to_datetime(df5["ds"])
df5['ds'] = df5['ds'].dt.tz_localize(None)
然后,我们将重新采样我们的数据,以考虑每日读数的*均值。
df6=df5.set_index('ds').resample('D').agg(y=('y', 'mean'))
df6 = df6.reset_index()
我们准备好了:
model = Prophet()
model.fit(df6)
df6_forecast = model.make_future_dataframe(periods=12, freq='MS')
df6_forecast = model.predict(df6_forecast)
plt.figure(figsize=(18, 6))
model.plot(df5_forecast, xlabel = 'Timestamp', ylabel = 'Glucose')
plt.title('Blood Glucose')

截图由 Ednalyn C. De Dios 提供
以及组件:
model.plot_components(df6_forecast)

截图由 Ednalyn C. De Dios 提供
结论
我们已经从导入 Apple Health 数据,转换它以发现见解,预测高血糖,并预测血糖水*,走了很长一段路。
在这个过程中,我们学习了如何使用 datetime 对象,制作小提琴图,利用 SMOTE *衡不*衡的数据集,使用逻辑回归进行分类,并使用 Prophet 进行预测。
接下来,我们将处理清洗过的数据,并使用 Power BI 创建一个仪表盘。
敬请关注!
感谢你的光临和阅读我的文章。希望这个逐步的讲解对你有所帮助!
如果你想了解更多关于我从懒散者到数据科学家的旅程,请查看下面的文章:
我没有学位的情况下进入数据科学的历程。
towardsdatascience.com
参考文献:
-
[2]
towardsdatascience.com/building-a-logistic-regression-in-python-step-by-step-becd4d56c9c8
请:在数据科学中不要再抛硬币了
原文:
towardsdatascience.com/please-no-more-flipping-coins-in-data-science-f21e893d4fbd
为什么数据科学需要工程化统计。
·发表于 Towards Data Science ·阅读时长 6 分钟·2023 年 4 月 6 日
--

图片由 Keith Johnston 提供,来自 Pixabay
前几天,我读到一个我关注的数据科学频道的讨论主题,内容是:“我抛 8 枚不同的硬币,第 8 枚硬币出现反面的概率是多少?”
你知道吗?这让我想起了我在学校时为什么讨厌统计学,尽管我的统计课是“轻量级”的。
幸运的是,我开始学习数据科学时已经在工作。当时,我有机会重新学习统计,并专注于一个特定领域:这让我爱上了“应用统计”。或者,正如我称之为的:“数据科学统计”。
所以,在这篇文章中,我想告诉你为什么我认为我们在谈论数据科学时需要对统计进行工程化,将抛硬币留给和朋友喝酒时的闲聊。
虽然依赖“更多学术例子”并没有什么坏处,但事实上,有些人就是无法忍受它们,更喜欢“领域示例”,因为这能帮助他们更好地理解话题。我就是这样的人。
我知道:这可能是一篇有争议的文章,但我希望你会喜欢,另外……请在评论中谈谈这个话题!
为什么我在学校讨厌统计学
我在“科学高中”(类似于“理科高中”)和机械工程中学习过几节“轻量级”统计课程。但你知道吗?这些方法很相似。
在意大利,除了比萨饼,我们还因非常理论化的研究而闻名。相信我:这是真的!
我不知道你如何,但对我们来说,解决统计练习时,以下问题可能是典型的:
-
“翻开一副扑克牌。你抽到红心 A 的概率是多少?”
-
“你有 100 个球在一个袋子里。30 个是黑色的,70 个是白色的。你拿出一个,发现是黑色的。那么下一个球是白色的概率是多少?”
而且这个列表可以继续扩展……事实上,每次我听到这些问题时,我的(心理)回答是:“谁在乎?”。
是的,让我们大声说出来:谁在乎?谁想成为一个可以预测下一个球颜色的魔术师?嗯,我不想……
事实是,当我的大脑听到这样的问题时,它就像关机了一样,甚至不愿意听完整句话,因为它知道它不在乎。它就是不喜欢这些挑战。
关键在于:这些挑战并不具挑战性。嗯,至少对我来说是这样的。
但还有更多。似乎统计学中唯一有趣的部分就是概率。所以,如果我能再次和我的一位统计学教授交谈,我会说:“嘿!我们都知道赌博是不道德的,因为赢的概率极低。但统计学不仅仅是概率,概率可以远远超出这个范畴!”。
我是如何爱上统计学的
作为一名工程师,我喜欢非常实用,喜欢具体的例子。
当我开始学习数据科学时,我在一家工业领域的公司担任过程工程师,当我发现自己需要了解统计学时,我对自己说:“好吧,看看这是不是一个合适的时机”。
我问的第一个问题是:“对于在特定装配线生产的特定产品,数据科学能否帮助我找到在特定制造阶段表现最好的操作员?”。好吧,这是一个非常具有挑战性的问题。实际上,我最终以此为题开发了我的学士论文,创建了一个用于工业过程的异常检测算法。
数据科学是一门实用的科学
他们总是告诉我:“数学是一门精确的科学”。这意味着数学可以用方法论的严谨性来表达,其现象是可测量、可重复的,并且可以用分析的方式客观表达,从而通过数学表达式预测在其范围内的现象结果。
对我来说,这意味着一件简单的事:数学在天上,意味着它与现实接触较少。
请不要误解我的意思。我们的世界依靠数学运作,但依靠的是应用数学!
我的意思是,我把数学看作是法律,把科学和工程看作是需要应用法律的法官。这适用于任何科学,这就是为什么我说数据科学中的数学应该是工程化的。
你没听说过数据科学中最重要的是什么吗?那就是领域知识!不是数学,不是统计学,不是编程:是领域知识!这也说明了我们之前说的:数据科学是一门实践科学,我们需要将数学和统计学应用于实际领域案例。
这就是为什么我不在乎抛硬币的概率。
我喜欢听到:“如果我的机器在接下来的 12 个月内以这些条件工作,什么时候是维护的最佳时机?”
这就是机器学习的力量,它不过是将数学和统计学应用于现实世界。
抛硬币没有什么不好……
请不要误解我的意思:我对计算抛硬币概率和其他类似问题没有异议。
但也许,我们应该理解很多人并不在意,因为他们需要(并喜欢)解决更具挑战性的问题。
我记得曾和大学同学们一起度过晚上,讨论一些深奥的,确实是理论性的数学话题。似乎我们的头脑在讨论和共同学习分析和代数等复杂话题时得到了扩展。
所以,即使是纯数学也有其美。但在某个时刻,我确实需要把脚踏实地,尝试用实际案例来应用我的学识,原因很简单:这帮助我更好地理解理论和公式。
所以,如果抛硬币帮助你更好地理解概率和统计,那对你来说是件好事,但这并不是每个人的情况。
实用统计学和概率如何帮助学生更好地理解数据科学
有很多研究表明,实践方法有助于更好地理解数学。这里有一本有趣的书可能会对你有所帮助。
问题是:一种方法什么时候才算是“足够实用”?计算抛硬币的概率不是“足够实用”吗?
好吧,解决的问题应该既实用又有趣……
所以,如果你正在学习数据科学并需要掌握统计学,如果你不喜欢抛硬币问题或类似问题,你应该努力挑战自己。
如果你有工作,尝试将数据科学应用到其中;这就是我所做的,并且效果很好。例如,假设你在工业领域工作并能分析生产数据。选择一个产品,开始问自己:
-
制造它所需的*均时间是多少?
-
是否有某个时间点,操作员的表现最好(例如,早晨)?
-
工作日生产废料的概率是多少?
如此等等。尽量具体一点,你会发现你无法停止提出这样的问题并找到解决方案。
所以,尽量坚持你的行业(或者如果你没有工作,选择一个你喜欢的行业),提出正确的问题,这些问题最终会让你爱上(实际的)统计学。
结论
正如我在开头所说,这可能是一篇有争议的文章,但我希望你能明白我的观点。
这个想法是激励人们在讨论数据科学时给出真实的例子。我知道:即使是在计算第 8 枚硬币的正反面概率时也有一种美;事实上,有人说:“这是普通的硬币,还是说是加重的?”。
公*的观点!但再次问,你感兴趣吗?
好吧,抱歉,我不能。如果你对解决这些统计问题不感兴趣,但确实希望在数据科学领域发展职业:请不要放弃。自己创造例子来挑战自己:你会在几天内看到结果。
并且……在评论中告诉我你的想法!
-
订阅 我的 Substack 通讯 以获取更多数据科学内容。
-
喜欢这篇文章?通过 我的推荐链接加入 Medium:以 5 美元/月(无额外费用)解锁 Medium 上的所有内容。
-
觉得有用吗? 请请我喝杯咖啡 Ko-fi。
更多我的内容:
在用 Python 学习和实践数据科学一年的经验分享
towardsdatascience.com towardsdatascience.com ## 如何在全职工作(或学习)时学习数据科学
在全职工作的同时学习和实践数据科学一年多后的经验分享
towardsdatascience.com towardsdatascience.com ## Python 类简明指南:面向对象编程的权威指南
通过这门全面的课程参考资料提升你的 Python 技能
towardsdatascience.com
请停止错误绘制神经网络
优质图表的案例
·
关注 发表在 Towards Data Science ·12 分钟阅读·2023 年 3 月 21 日
--
图片由作者提供,改编自 tikz.net/neural_networks/ (CC BY-SA 4.0)
作者:亚伦·马斯特和多龙·伯格曼
如果你是数百万尝试学习神经网络的人之一,那么你可能见过类似上述的图像。
这个图表只有一个问题:那就是毫无意义。
这意味着困惑、不完整,且可能是错误的。这个图表受到了某个著名在线深度学习课程中的图表的启发,排除了所有的偏差系数,并将数据展示成好像它是一个函数或节点。它“可能”错误地显示了输入。我们之所以说“可能”,是因为即使其中一位我们获得了完成这类课程的证书,还是很难确定它试图展示的内容。¹
其他神经网络图表有不同的缺陷。以下是一个受某个来自山景城——基于广告公司的 TensorFlow 课程图示启发的插图:

图片由作者提供,改编自之前的图片。
这个图比第一个图更清晰地展示了输入,这是一个进步。但它也做了其他奇怪的事情:它按名称展示了偏差,但没有用视觉图示显示它,还将数量以不同于它们使用或创建的顺序展示出来。
为什么?
很难猜测这些图表是如何形成的。第一个图在表面上看起来类似于流网络图表,这些图表在图论中使用。但它违反了这种流图的核心规则,即流入节点的量等于流出节点的量(这里没有适用的例外)。第二个图看起来可能起初是第一个图,但随后被编辑成展示参数和数据,结果顺序错乱。² 这些图表都没有视觉上展示偏差(我们看到的大多数图表也没有),但这种选择并没有节省多少空间,如下所示。
我们没有挑选这些图表。对“神经网络图表”的互联网图片搜索显示,上述图表是常态,而非例外。
糟糕的图表对学生不好
上述图表如果仅在经验丰富的专业人士之间使用,可能会还不错。但遗憾的是,它们被用于教学目的,给无辜的机器学习学生带来了困扰。
遇到这种奇怪情况的学习者必须做出书面或心理笔记,例如“这里有偏差,但他们没有显示出来”或“他们画在圆圈内部的东西实际上是处理的输出,而这个圆圈两张幻灯片前就显示过”或“输入实际上并不像图中显示的那样工作。” 上述提到的著名(且通常很优秀的)课程包含讲师耐心地多次重复某个网络的实际工作方式与图示方式不符的讲座。在课程的第三周,他勇敢地尝试折衷,交替使用特殊的、准确的图示来展示节点内部发生的情况,以及更典型的图示来展示其他内容。(如果你想看到那些更好的节点图示,可以参考这篇博客文章。)
一种更好的方法
学习神经网络不应该是解码误导性图表的练习。 我们提出了一种建设性的、新颖的方法来教授和学习神经网络:使用好的图表。我们希望图表能够简洁而忠实地表示数学内容——如费曼图、文氏图、数字滤波器图和电路图中所示。
制作 GOOD 图
那么,我们究竟提出了什么?让我们从基础开始。神经网络涉及许多简单的操作,这些操作已经在电气工程师使用了几十年的流程图中有表示。具体来说,我们可以描绘数据复制、数据相乘、数据相加和将数据输入函数以输出数据。然后,我们可以将这些符号的缩略版本组装成一个准确的整体,我们将其称为一般目标可观察描绘图,简称 GOOD 图。(抱歉,反缩写者。)
让我们看看这些构建块。首先,这里是如何展示来自单一数据源的三个数据副本的。很直观。

这里是显示缩放输入的方法。它就是一个三角形。

三角形表示输入值x₁进入它后被某个数w₁缩放,产生结果w₁乘x₁。例如,w₁可以是 0.5 或 1.2。稍后如果我们将这个三角形移到图的右端(与箭头合并)并将其缩小,会更方便,所以我们就这样画吧。

好吧,我们承认:这只是一个带实心三角形尖端的箭头。关键是,三角形尖端乘以箭头上的数据。
接下来,这是一种显示将两个或更多东西加在一起的方法。我们称和为z。也很简单。

现在,我们用一些标准符号展示了加法和乘法。但如果我们有一个更通用的函数,它接受一个输入并产生一个输出呢?更具体地说,当我们制作神经网络时,我们会使用一个激活函数,它通常是 Sigmoid 或 ReLU。无论哪种方式,都不成问题;我们只需将其表示为一个框。例如,假设我们函数的输入叫做z,函数z的结果叫做g(z),产生一个输出a。它看起来是这样的:

可选地,我们可以指出g(z)具有给定的输入—输出特性,这可以放在函数框附*。这是一个包括 ReLU 的g(z)图以及函数框的图。在实际应用中,通常只有少数几种激活函数,因此在图层附*注明函数名称(例如 ReLU)也足够了。

或者,我们可以进一步缩略,因为在典型的神经网络中将有许多,通常是相同的激活函数。我们建议在函数框内使用单个风格化的脚本字母来表示特定激活,例如R表示 ReLU:

同样,Sigmoid 可以用风格化的S表示,其他函数可以用另一指定字母表示。
在继续之前,让我们注意一个简单但关键的事实:我们将数据(及其传输方向)表示为箭头,将操作(如乘法、加法、一般函数)表示为形状(如三角形、圆形、方形)。这是电气工程流程图中的标准做法。 但由于某些原因,也许是受到早期计算机科学研究的启发,该研究物理上将记忆和操作结合在一起,当绘制神经网络时,这一惯例被忽视或甚至被颠倒。³ 尽管如此,这一区分仍然很重要,因为我们确实训练函数参数,但我们不训练数据,每个数据实例都是不可变的。
好的,回到我们的故事。由于我们很快会构建神经网络图,因此需要描绘大量的“求和然后函数”处理。为了使我们的图示更加紧凑,我们将创建一个缩略符号来结合这两个功能。首先,让我们将它们绘制在一起,假设g(z)是 ReLU。

既然我们要缩略符号,那么我们来看一下它们放得非常*时的样子。我们还将从图中删除内部变量和函数符号,并添加一些虚线以提示建议的形状:

基于此,我们引入一个新的总结符号来表示“求和然后函数”:

它特殊的形状提醒我们它的作用。它也故意看起来与其他符号不同,以帮助我们记住它是特别的。⁴
一个好事
现在,让我们通过一个简单的逻辑回归示例,将上述所有图示操作整合在一起。我们的示例从二维输入开始,将每个输入维度的值乘以一个唯一的常数,然后将结果与常数b(我们称之为偏置)相加,并将和通过 Sigmoid 激活函数。由于稍后会讲解的原因,我们将偏置表示为 1 乘以值b。为了完整性(并作前瞻),我们为所有这些值指定名称,并在图示中显示出来。输入为x₁和x₂,乘法因子包括权重w₁和w₂以及偏置b。加权输入和偏置的总和为z,函数g(z)的输出为 a。

关于图表左下角显示的那个数字“1”。这个数字 1 不是输入值,但通过显示这个数字而不仅仅是输入,我们明确了这些值中的每一个都乘以一个参与求和的参数。这样我们可以在同一个图表中展示* w(输入权重)和值 b(偏置)。不好的图表通常省略偏置,但好的图表不会。特别是在网络可能故意省略偏置的情况下,省略偏置尤其危险;如果未显示偏置,观众就不得不猜测*它是否是网络的一部分。因此,请在图表中故意包括或排除偏置。
现在通过使用上面定义的“求和然后函数”符号来稍微清理一下。我们还在图表下方显示了变量名称。请注意,我们用“求和然后函数”符号中的* S*来表示 Sigmoid 函数。

这看起来很简单。这是件好事。
现在,让我们构建一些更有趣的东西:一个具有三个单位的隐藏层(使用 ReLU 激活)和一个使用 Sigmoid 激活的输出层的实际神经网络(NN)。 (如果你不熟悉,隐藏层是除了输入层或输出层以外的任何层。)请注意,这是上面 Mountain View 网络图中使用的相同架构。在这种情况下,每个输入维度和输入层偏置连接到隐藏层中的每个节点,然后隐藏层输出(加上偏置值)连接到输出节点。每个函数的输出仍然称为a,但我们使用带括号的上标和下标分别表示我们从哪个层和节点输出。类似地,我们使用带括号的上标来表示* w和 b*值指向的层。使用之前示例中的风格,它看起来像这样:

现在我们有了进展。此时,我们还可以看到每层的* W和 b的维度由输入的维度和每层的节点数指定。让我们通过不单独标记每个 w和 b*值来清理上述图表。

本文所有图片均由作者提供。
哒哒!我们有了一个很好的神经网络图表,同时也是好的。可学习的参数在图表上(作为三角形)和图表下方的总结中都显示出来,而数据直接在图表上以标记的箭头显示。网络的架构和激活函数,通常称为超参数,可以通过检查图表本身的布局和节点来查看。
一切都很棒
让我们考虑一下好的图表的好处,不考虑坏图表:
-
很容易看出每层的操作顺序。先进行乘法运算,然后是求和,然后是激活函数。
-
容易将(不可变的)数据流经网络与属于网络的(可训练的)参数区分开来。
-
容易看出每层的w矩阵和b向量的维度。对于一个有N节点的层,很明显我们需要b的形状为[N,1]。对于一个在有M节点(或输入)的层之后的有N节点的层,很明显w的形状为[N,M]。 (然而,仍然需要记住形状是[输出,输入]而不是[输入,输出]。)
-
相关的,我们可以看到权重和偏差确切存在的位置,即层与层之间。传统上,它们被命名为属于它们输出到的层,但使用优秀图示的学生会被提醒,这只是一个命名惯例。
让我们还来回顾一下优秀图示与差的图示的区别:
-
它们显示了每一层的偏差。它们不会遗漏偏差。
-
它们将数据视为数据,将函数视为函数。它们不会混淆两者。
-
它们显示了数据何时被复制并发送到函数。它们不会跳过这一步。
-
它们按照正确的顺序显示所有步骤。它们不会错误地重新排序或遗漏步骤。
-
它们相对简洁明了。好吧,不好的那一些稍显简洁。
做一些好事
本文详细讨论了差的图示存在的问题,并为优秀图示提供了理由。但如果你是机器学习讲师,我们鼓励你直接开始使用优秀图示,而不需要额外的宣传。优秀图示比其他选项更具自解释性。反正你在课程中会讲解神经网络,所以在那个时候引入优秀图示是个好主意。
当然,为了服务你的学生,展示一些差的图示也是个好主意。了解外界是如何绘制图示的,即使它们毫无意义,也很重要。我们认为,从准确的东西中学习,然后考虑混乱的东西,比反过来要容易得多。
更多精彩内容在前面
这篇文章完成了系列中的第一篇,如果它受到欢迎的话。特别是,我们关注的是简化网络图示,它们紧凑地表示了上面展示的全连接网络类型,并且也有改进的空间。卷积网络图示值得单独讨论。我们也在开发一个软件包,用于自动绘制优秀图示。
致谢
作者感谢 Jeremy Schiff 和 Mikiko Bazeley 对本篇文章的协助。
参考文献和附注
-
基于其他层,可能第一个图示正在将输入传递给非*凡的激活函数,从中得到的值可能与输入不同。但在相关课程中没有这样工作的例子,因此将这样的图示作为备忘单上唯一的全连接图示是不合适的。或者,第一个层中显示的“a”值可能与输入相同,在这种情况下,激活函数是恒等函数,这会引起*凡且不必要的处理。无论哪种情况,图示都是模糊的,因此不佳。
-
前两个图示中的任意一个看起来都可能是更好、更旧的图示的一个不幸缩略,例如 Duda、Hart 和 Stork 第二版《模式分类》第六章中的图示(其中一位我们还保留着 2002 年 CS229 时的纸质版)。那本书将激活函数显示为圆形单元(比将其输出放在单元内要好),并正确地显示输出在制作副本并分离到下一层之前离开单元。(虽然它也奇怪地显示了输入和偏置。)
-
如果你的学习已经进展到包括卷积网络(CNs),你会发现 CN 图示通常将数据显示为块,将处理过程显示为标注的箭头。不要担心。现在,只需记住数据和处理过程之间有本质的区别,对于全连接神经网络,一个好的(或优秀的)图示会清晰地表明它们的不同。
-
对于那些将“和再函数”符号视为反向操作的与门的逻辑爱好者,请记住,与门是不可逆的。因此,这个新符号必然有其他含义,我们在这里定义。
请使用流工作负载来评估向量数据库性能
为何静态工作负载不足以满足需求,以及通过比较 HNSWLIB 和 DiskANN 使用流工作负载学到了什么
·
跟随 在 Towards Data Science ·9 min 阅读·Dec 1, 2023
--
图片由 DALLE-3 提供
向量数据库是为了高维向量检索而构建的。如今,许多向量是由深度神经网络生成的嵌入,如GPTs和CLIP,用于表示数据点,如文本片段、图像或音频轨道。嵌入被用于许多应用,如搜索引擎、推荐系统和聊天机器人。你可以在向量数据库中索引嵌入,数据库使用*似最*邻(ANN)索引,通过如余弦或欧几里得等距离函数支持快速检索最邻*的对象。对于 100 万向量的索引,延迟为 2 到 10 毫秒,并且随着索引大小呈亚线性扩展(即O(log n))。
在这篇文章中,我指出了我们当前评估 ANN 索引方式的几个问题,并建议了一种新的评估类型。本文重点关注嵌入向量的 ANN 索引,这是一个最*受到广泛关注的领域:像Pinecone、Zilliz、Qdrant和Weaviate这样的向量数据库初创公司提供嵌入索引和检索作为其核心服务。
1. 静态负载基准测试是不够的。
评估 ANN 索引的标准方法是使用静态负载基准测试,它由固定数据集和固定查询集组成。

静态负载基准测试。图片来源于作者。
静态负载基准测试首先从固定数据集中构建 ANN 索引,然后以不同参数设置多次运行固定查询集,并在每个最小准确度水*下测量可达到的最高查询吞吐量。对每个 ANN 索引执行相同的程序后,基准测试会生成如下图所示的图表:

图源自ANN Benchmarks (11/25/2023)。MIT 许可。
上图比较了使用静态负载名为 glove-100-angular 的不同 ANN 索引,该负载包含词语的嵌入。
这种评估方法由ann-benchmarks项目推广,该项目始于 5 年前。许多向量数据库现在在其技术博客中使用这种方法来测量其性能。请参见Qdrant 基准和Timescale 基准。
静态负载基准测试是有用的,因为结果易于理解,并允许我们通过相同的图表比较不同索引算法之间的准确度和查询性能权衡。
除非这是对 ANN 索引的完整评估,否则你不应仅仅根据这一点选择 ANN 索引。它过分强调了召回准确性和查询性能,而忽略了其他重要方面,如索引性能和内存使用。
索引性能应当被反映出来。
索引吞吐量衡量了 ANN 索引接受新数据点的速度。与查询吞吐量类似,它通常与召回准确性呈反比。例如,下面的图展示了 HNSWLIB 和 DiskANN Vamana 索引的索引吞吐量与召回之间的关系。

HNSWLIB 和 DiskANN Vamana 在 OpenAI 1M 上的召回率与索引吞吐量。图片由作者提供。
这张图与之前的 Recall-QPS 图类似,但从这张图中你可以看到更高的召回率也是索引性能的权衡。如果我对索引和查询性能都感兴趣,这张图仍然不够充分,因为它没有显示出召回性能权衡的边界,因为静态基准测试将索引和查询负载分开。
许多 ANN 索引支持批量索引 API,这比逐点索引(即,一次添加一个向量)优化得多,并且可以创建更准确的索引。例如,基于簇的 ANN 索引在批量中构建所有向量的簇。静态工作负载基准将索引和查询负载分开,因此它鼓励批量索引,这可能不切实际。
另一个与索引相关的问题是产品量化(PQ)。许多 ANN 索引使用 PQ 或其他形式的向量压缩来加速计算。静态工作负载基准允许 ANN 索引在查询阶段开始之前构建优化的压缩代码本,但这样的最佳代码本在实践中可能无法实现。
内存使用很重要。
大多数流行的 ANN 索引都是内存中的,这意味着它们的主要数据结构保持在易失性存储(DRAM)中以供查询。因此,衡量内存效率及其与性能和召回准确性的权衡是很重要的。例如,在这篇研究论文中,作者测量了 HNSW 在 10 亿点下的内存使用为 490 GB,而NSG为 303 GB,但在召回和查询性能方面,HNSW 仅略微优于 NSG。进行 ANN 索引基准测试时,这种权衡应当被重点考虑。
然而,仅用静态基准很难获得内存效率的现实图景,原因有几个。首先,ANN 索引算法可以创建一个读取优化的索引,这样虽然索引很紧凑,但会影响随后的索引性能。其次,工作负载只捕捉纯粹的索引或纯粹的查询,而不是两者的混合,这在实际场景中,如问答引擎或聊天机器人,数据不断到达时更为常见。
数据分布随时间变化。
在静态基准中,数据和查询集保持不变。这并不现实,因为数据和查询是由最终用户的兴趣驱动的,而这些兴趣会随着时间变化。如果数据和查询集始终固定,那么最佳的索引就是一个记住每个查询结果的缓存。最*,ANN 索引的研究(例如,FreshDiskANN)开始测量分布外查询性能——这是一个正确的进步。
删除的情况如何?
删除 API 已经成为 ANN 索引的标准,但没有静态基准在测量这一点。能够处理删除是重要的,因为新兴的 AI 相关应用场景如聊天机器人将 ANN 索引作为类似于在线事务处理(OLTP)数据库的操作存储,因为数据不断被添加和修改。
2. 流式工作负载提供了更多信息。
如果一个 ANN 索引支持以下 API:
-
插入(ID, vector)
-
查询(vector)
-
删除(ID)
如果使用场景不仅仅是静态数据和查询(比如,所有场景?),那么流式工作负载基准可以让你更深入地了解 ANN 索引的特性以及它们在特定使用场景中的表现。
流式工作负载基准包括两个流:一个数据流,对应一系列的插入和删除 API 调用;另一个查询流,对应一系列的查询 API 调用。它可以通过实际的流式系统如 Kafka 实现,或者更简单地使用运行手册,其中包含指向数据集和查询集的序列,类似于静态基准中使用的那些。

使用运行手册的简单流式工作负载基准。图片由作者提供。
上面的图示展示了NeurIPS 23' 大型 ANN 基准中使用的流式工作负载基准。具体来说,运行手册中的每一步对应一批向量,因此操作可以并行执行。这种方法具有以下优点:
-
灵活性: 工作负载模式和数据分布变化可以被建模为不同的流式工作负载,然后编译成不同的运行手册。
-
现实性: 索引和查询交替进行,因此 ANN 索引必须适应未来的插入。此外,内存配置更准确地反映了实际工作负载。
-
简单分析: 性能可以通过整体吞吐量来描述,而不是索引与查询吞吐量,因此可以轻松地可视化召回率与性能之间的权衡。
-
完整性: 插入和删除操作也进行了评估。
在这篇博客文章中,我深入探讨了上述(4)点,并展示了我通过流式工作负载基准测试发现的新见解。
比较召回稳定性:HNSW 与 Vamana
在运行流式工作负载基准测试时,我们收集的一个重要指标是每个查询操作的召回率。我们可以通过查看不同索引设置(参数、算法等)的召回稳定性随时间的变化来进行比较,并决定哪个索引适用于特定的使用场景。
我测量了DiskANN的 Vamana 和各种 HNSW 实现的召回稳定性,测试是在NeurIPS 23' Big ANN Benchmarks中定义的流式工作负载下进行的。
关于 Vamana 和 HNSW 的一些背景:它们都是图形 ANN 索引,并且特别擅长处理嵌入。在图形 ANN 索引中,每个向量是一个节点,查询作为图形遍历执行。有向边被选择性地构造,以限制内存使用,同时保证从任何节点到任何节点的快速遍历。在原地删除期间,对于每个即将到来的邻居节点,图形 ANN 索引执行边修复以维护有向图结构。
我们使用的第一个 HNSW 实现是基于HNSWLIB的,增加了使用名为[repairConnectionsForUpdate](https://github.com/nmslib/hnswlib/blob/359b2ba87358224963986f709e593d799064ace6/hnswlib/hnswalg.h#L987)的修复算法实现的删除 API,该算法已是 HNSWLIB 源代码的一部分。其思想是对需要修复的节点执行“重新插入”,并在所有层级上更新其出度邻居。下图展示了 Vamana 和 HNSW 的召回率随时间的变化。

DiskANN 的 Vamana 与基于 HNSWLIB 的 HNSW 实现的召回稳定性。删除 API 调用标记为“X”。图片由作者提供。
请注意,我将 Vamana 的最大度参数设置为 40(R = 40),将 HNSW 的基础层最大度也设置为 40(M = 20, M0 = 40)。因此,它们应使用大致相同的内存。
从该图中可以明显看出,删除对召回率有不利影响,因为召回率在连续删除过程中单调下降。相比之下,HNSW 受删除的影响远大于 Vamana。
我们使用的第二个 HNSW 实现将 HNSWLIB 的边修复算法替换为 Vamana 的,这两者差别很大。Vamana 的边修复算法的思想是将每个被删除节点的入邻居连接到被删除节点的出邻居,同时应用修剪步骤以保持最大度约束。在这种情况下,我们使用 HNSW 原始的修剪算法。它由 HNSWLIB 在一个名为[getNeighborsByHeuristic2](https://github.com/nmslib/hnswlib/blob/359b2ba87358224963986f709e593d799064ace6/hnswlib/hnswalg.h#L382C16-L382C16)的函数中实现。

回顾 DiskANN 的 Vamana 和一个使用 Vamana 边修复算法处理删除的 HNSW 实现。图片由作者提供。
在所有参数保持不变的情况下,将 HNSWLIB 的边修复算法更改为 Vamana 的,HNSW 的召回稳定性立即得到了改善。
让我们多做一点工作,将 HNSW 的边修剪算法改为 Vamana 的。现在 HNSW 索引与 Vamana 的几乎相同,唯一的不同是它有多个层级。我们称这个索引为“多层 Vamana”。

回顾 DiskANN 的 Vamana 和一个使用 Vamana 边修复和修剪算法的 HNSW 实现——“多层 Vamana”。图片由作者提供。
你可以看到 HNSW 的召回率现在略高于 Vamana 的,同时使用了类似的内存。我还没有在任何研究论文中发现这一观察结果。此外,虽然图中没有性能数据,但我注意到在切换到 Vamana 的修剪算法时性能显著下降。
总之,通过使用流式工作负载基准测试,我能够发现有关不同边修复和修剪算法的新信息。一个合乎逻辑的下一步是研究这些算法的性能影响,我可以通过流式工作负载基准测试来实现这一点。
3. 结论
总结一下,在这篇博客文章中,我指出了静态工作负载基准测试不足以真实评估 ANN 索引,并描述了我认为更好的替代方案——流式工作负载基准测试。我还使用了一个特定的流式工作负载来揭示 HNSW 和 Vamana 索引之间的新对比。感谢NeurIPS 23' Big ANN Benchmarks团队!他们已经开源了我在这篇博客文章中使用的流式工作负载。
我们需要一个用于向量数据库的 TPC-C 和 TPC-H。
在基准测试方面仍有很多工作要做。ANN 索引是向量数据库的核心特征,它们已经筹集了超过 3.5 亿美元。然而,其中许多仍在使用一种过时的方法来衡量性能,这种方法已经不能反映实际使用场景。数据库系统在 90 年代和 2000 年代初经历了类似的阶段。随后,像 TPC-C 和 TPC-H 这样的标准基准被开发出来,并且至今仍在使用。我们应该为向量数据库也制定类似的标准。
在框架之外绘制 — 使用 Python 替代矩形图表的 8 种圆形图表
使用 Python 绘制圆形图表
·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 4 月 11 日
--

图片由 Daniel Roe 提供,来自 Unsplash
在矩形区域内绘制图表,如典型的条形图,是数据可视化中的常见做法。使用这些图表的优点是可以最大限度地利用空间。
顺便提一下,将过多的矩形图表紧密放置在一起,比如创建仪表盘,可能会使结果显得不美观或过于密集。
由于矩形图表并不是唯一的选择,一些圆形替代方案可以取而代之。使用不同的图表样式可能会产生美观的效果。


一个可以作为矩形图表替代方案的圆形图表示例。图片来源于作者。
在继续之前,我想澄清一下,这篇文章并不反对使用矩形图表。主要目的是呈现一些想法。这样,读者可以决定哪些最适合他们的用途。
开始吧。
替代图表
在这篇文章中,将介绍三种类型的矩形图表及其八种替代方案:
1. 堆积条形图的替代方案。
-
1 比例面积(又称嵌套形状)
-
2 饼图
-
3 圆环图
2. 树图的替代方案。
-
4 日晕图(又称径向树图)
3. 条形图的替代方案。
-
5 圆形条形图(又称赛车赛道图)
-
6 径向条形图
-
7 雷达图
-
8 圆形打包图
1. 堆积条形图的替代方案。
首个矩形图表是堆积条形图。这个图表对于表达相对于总量的比例贡献很有用。首先,我们将创建一个简单的堆积条形图。然后将解释替代选项。
使用 numpy.random 创建一个模拟数据集。如果你已经有其他数据集可以尝试,可以跳到可视化部分。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
np.random.seed(42)
v1 = np.random.randint(5,45,50)
v2 = np.random.randint(10,60,50)
v3 = np.random.randint(30,80,50)
v4 = np.random.randint(90,100,50)
df = pd.DataFrame(zip(v1,v2,v3,v4),
columns=['v1','v2','v3','v4'])
df = df.melt()
df.head()

创建一个颜色列表和字典以供后续使用。以下代码中的颜色可以修改。
v_list = ['v1', 'v2', 'v3', 'v4']
c_code = ['#F77F00','#56C596','#2A639B','#F6D55C']
dict_color = dict(zip(v_list,c_code))
下一步是按 DataFrame 分组,计算百分比列,并添加颜色列。
df_gb = df.groupby(by='variable').sum().reset_index()
df_gb['pct']= [round(i,1) for i in df_gb['value']*100/df_gb['value'].sum()]
df_gb['color']= c_code
df_gb

绘制堆积条形图,以便后续与其他选项进行比较。
groups = ['V']
v1 = [df_gb.pct[0]]
v2 = [df_gb.pct[1]]
v3 = [df_gb.pct[2]]
v4 = [df_gb.pct[3]]
values = np.array([v1, v2, v3, v4])
sns.set_style('darkgrid')
fig, ax = plt.subplots(figsize=(1,6))
for i,c in zip(range(values.shape[0]), df_gb.color):
ax.bar(groups,values[i], bottom=np.sum(values[:i], axis=0), color=c)
plt.show()

堆积条形图。图像由作者提供。
#1 比例面积(也称为嵌套形状)
如前所述,堆积条形图的概念是展示比例贡献。因此,我们可以使用比例面积来展示相对于总量的贡献,应用相同的观点。
fig, ax = plt.subplots(figsize=(8,8))
ax.scatter(0, 0)
cir = plt.Circle((0, 0), 1, color='lightgray', fill=True)
ax.add_patch(cir)
df_gb.sort_values(by=['pct'], ascending=False, inplace=True)
for i,c,l in zip(df_gb.pct,df_gb.color,df_gb.variable):
data = np.array(i)
r = np.sqrt(data/100)
cir_p = plt.Circle((0, r-1), r, color=c, fill=True)
ax.add_patch(cir_p)
ax.axis('off')
kw = dict(color="white", va="center", ha="center")
kw.update(dict(fontsize=19))
ax.text(0, (2*r)-1.08, l+' '+str(i)+'%', **kw)
ax.set_aspect('equal')
plt.xlim([-1, 1])
plt.xlim([-1, 1])
plt.show()

作为堆积条形图的替代选项的比例面积。图像由作者提供。
一个缺点是,这种替代方案比堆积条形图需要更多的空间。如果绘图区域很重要,这是一个限制。另一个需要考虑的是将条形图替换为 x 轴上的时间线。使用多个嵌套形状不方便显示随时间变化的情况。
结果可以单独表达并结合为一个照片拼贴,这是制作信息图的一个别致的想法。
for i,c,l in zip(df_gb.pct,df_gb.color,df_gb.variable):
data = np.array(i)
r = np.sqrt(data/100)
fig, ax = plt.subplots(figsize=(8,8))
ax.scatter(0, 0)
cir = plt.Circle((0, 0), 1, color='lightgray', fill=True)
cir_p = plt.Circle((0, r-1), r, color=c, fill=True)
ax.add_patch(cir)
ax.add_patch(cir_p)
ax.axis('off')
kw = dict(color="white", va="center", ha="center")
kw.update(dict(fontsize=32))
ax.text(0, r-1, l+'\n'+str(i)+'%', **kw)
ax.set_aspect('equal')
plt.xlim([-1, 1])
plt.xlim([-1, 1])
plt.show()
哇!!

作为堆积条形图的替代选项的分离比例面积。图像由作者提供。
如果你想创建一个照片拼贴,你可以查看我的文章:'9 个用 Python 制作的比条形图更引人注目的可视化 (link)。'
#2 饼图
饼图是展示数据组成百分比的简单选项。使用饼图作为替代方案有与比例面积相同的缺点,即需要更多的空间。因此,如果绘图区域不重要,这个选项很简单易创建。
import plotly.express as px
fig = px.pie(df_gb, values='value', names='variable',
color='variable', color_discrete_map=dict_color)
fig.update_layout(width=600, height=600, margin=dict(l=0, r=0, b=0, t=0))
fig.show()

作为堆积条形图替代选项的饼图。图像由作者提供。
#3 圆环图
基本上,圆环图是一个带有空洞的饼图。这个中心的空白似乎有一些好处,因为一些来源声称它有助于读者的视线集中在弧形上,并促进叙述。此外,它还可以用于添加一些注释或信息。
import plotly.express as px
fig = px.pie(df_gb, values='value', names='variable',
color='variable', color_discrete_map=dict_color,
hole=0.6)
fig.update_layout(width=600, height=600, margin=dict(l=0, r=0, b=0, t=0))
fig.show()

作为堆积条形图替代选项的圆环图。图像由作者提供。
2. 替代树图的选项。
从技术上讲,treemap 是一种有效展示层级数据的方法。它通过一组嵌套矩形表达数据。treemap 图使用了全部绘图区域。如果你需要一些空白空间,这可能不是一个好选择。
为了展示 treemap,我将通过向每个变量添加子组来修改数据集。下面的代码展示了如何添加子组列并绘制图表。之后,我们将看到 treemap 的替代方案。
df['sub']=[i+'.1' if j%2 else i+'.2' for i,j in zip(df.variable, df.value)]
df_wsub=df.groupby(by=['variable','sub']).mean().reset_index()
df_wsub

import plotly.express as px
fig = px.treemap(df_wsub, path=['variable', 'sub'], values='value',
color='variable',color_discrete_map=dict_color)
#fig.update_traces(root_color="lightgrey")
fig.update_layout(width=800, height=600, margin=dict(l=0, r=0, b=0, t=0))
fig.show()

treemap 图。图片来自作者。
#4 Sunburst 图(也称为径向 treemap)
treemap 的完美替代品是 sunburst 图。该图也称为径向 treemap,因为它实际上是一个圆形区域的 treemap。我们可以使用 Plotly 和几行代码创建 sunburst 图,如下所示。
import plotly.express as px
fig = px.sunburst(df_wsub, path=['variable', 'sub'], values='value',
color='variable',color_discrete_map=dict_color)
fig.update_layout(width=800, height=600, margin=dict(l=0, r=0, b=0, t=0))
fig.show()
哒哒!!

Sunburst 图作为 treemap 图的替代方案。图片来自作者。
与 treemap 相比,你可以看到这个图表在圆圈周围有更多的空白空间。
3. 条形图的替代方案。
尽管条形图是一种常用的标准图表,但绘制过多的条形图会使结果变得枯燥。通过改变视角,矩形条形图可以用一些等效的形式替代。
作为示例,我将制作一个模拟数据集并使用它绘制一个简单的条形图。之后,我们将讨论一些替代方案。
np.random.seed(42)
v1 = np.random.randint(5,50,50)
v2 = np.random.randint(5,50,50)
v3 = np.random.randint(20,60,50)
v4 = np.random.randint(20,60,50)
v5 = np.random.randint(90,100,50)
v6 = np.random.randint(10,50,50)
v7 = np.random.randint(10,50,50)
v8 = np.random.randint(25,60,50)
v9 = np.random.randint(25,60,50)
df = pd.DataFrame(zip(v1,v2,v3,v4,v5,v6,v7,v8,v9),
columns=['v1','v2','v3','v4','v5','v6','v7','v8','v9'])
df = df.melt()
#extract color palette
pal = list(sns.color_palette(palette='Spectral', n_colors=9).as_hex())
df_gb = df.groupby(by='variable').sum().reset_index()
df_gb['pct']= [round(i,1) for i in df_gb['value']*100/df_gb['value'].sum()]
df_gb['color']= pal
df_gb

绘制条形图。
sns.set_style('darkgrid')
plt.figure(figsize=(9,5))
plt.bar(x=df_gb.variable, height=df_gb.pct,color=df_gb.color)
plt.show()

条形图。图片来自作者。
#5 循环条形图(也称为赛道图)
第一个替代方案是修改条形图的方向,使其围绕中心循环。阅读图表时,条形图似乎在竞赛完成圆圈。因此,这可以产生引人注目的效果。
请注意,循环条形图存在条形图比例不均的缺点。即使它们具有相同的值,靠*中心的条形图总是比外面的短。
可以通过标记每个条形图来处理问题,以向用户显示实际值。
import math
plt.gcf().set_size_inches(8,8)
sns.set_style('darkgrid')
#set max value
max_val = 26
ax = plt.subplot(projection='polar')
for i,p,l in zip(range(len(df_gb)),pal,df_gb.variable):
ax.barh(i, df_gb['pct'][i]*2*np.pi/max_val, label=l, color=p)
#set the subplot
ax.set_theta_zero_location('N')
ax.set_theta_direction(1)
ax.set_rlabel_position(0)
ax.set_thetagrids([], labels=[])
labels = [i+' '+str(j)+'%' for i,j in zip(df_gb.variable, df_gb.pct)]
ax.set_rgrids(range(len(df_gb)), labels= labels)
#set the projection
ax = plt.subplot(projection='polar')
plt.legend().set_visible(False)
plt.show()

循环条形图作为条形图的替代方案。图片来自作者。
#6 径向条形图
除了循环,条形图还可以通过从中心向外移动来表达。与循环条形图相比,这种图没有每个条形图比例的问题。这是使结果看起来更吸引人的另一种方法。
顺便说一下,不在彼此旁边的条形图难以比较。问题也可以通过标记来解决,以方便比较。
plt.figure(figsize=(8,8))
ax = plt.subplot(111, polar=True)
plt.axis()
#set min and max value
ax.axis(ymin=0, ymax=25)
#set heights and width
heights = df_gb['pct']
width = 2*np.pi / len(df_gb)
#set index and angle
indexes = list(range(1, len(df_gb)+1))
angles = [element * width for element in indexes]
bars = ax.bar(x=angles, height=heights, width=width, bottom=0,
linewidth=1, edgecolor="white", color=pal)
labelPadding = 2
labels = [i+' '+str(j)+'%' for i,j in zip(df_gb.variable, df_gb.pct)]
for bar, angle, height, label in zip(bars, angles, heights, labels):
rotation = np.rad2deg(angle)
alignment = ""
#deal with alignment
if angle >= np.pi/2 and angle < 3*np.pi/2:
alignment = "right"
rotation = rotation + 180
else:
alignment = "left"
ax.text(x=angle, y=0 + bar.get_height() + labelPadding,
s=label, ha=alignment, va='center', rotation=rotation,
rotation_mode="anchor")
ax.set_thetagrids([], labels=[])
plt.show()

径向条形图作为条形图的替代方案。图片来自作者。
#7 雷达图
在这种情况下,使用雷达图可能是一个奇怪的想法,因为这个图表通常用于处理多变量数据。然而,通过改变视角,雷达图可以作为一种替代方式,用于显示分类数据及其在轴上的值。
import plotly.express as px
import pandas as pd
fig = px.line_polar(df_gb, r='pct', theta='variable',
line_close=True, color_discrete_sequence=[c_code[1]])
fig.update_polars(radialaxis_range=[0,25])
fig.update_layout(width=600, height=600, margin=dict(l=10,r=10,b=10,t=20))
fig.update_traces(fill='toself')
fig.show()

作为条形图替代的雷达图。图片由作者提供。
从技术上讲,雷达图和径向条形图返回的是相同的概念结果。雷达图用线条来比较值,而不是在中心周围显示条形。
#8 圆形打包
最后,我们可以将矩形条形图转换为多个圆圈,并将它们组合成一个圆形打包。这是另一种创建信息图表的想法。
请考虑即使圆形可以标记信息,过多的圆形也可能导致图表难以阅读。
从使用 circlify 库计算每个圆圈的位置开始。
import circlify
# compute circle positions:
circles = circlify.circlify(df_gb['pct'].tolist(),
show_enclosure=False,
target_enclosure=circlify.Circle(x=0, y=0))
绘制圆形打包图。
df_gb.sort_values(by='pct', ascending=True, inplace=True)
fig, ax = plt.subplots(figsize=(8,8), facecolor='white')
ax.axis('off')
lim = max(max(abs(c.x)+c.r, abs(c.y)+c.r,) for c in circles)
plt.xlim(-lim, lim)
plt.ylim(-lim, lim)
# print circles
for circle,v,p,color in zip(circles,df_gb.variable,df_gb.pct,df_gb.color):
x, y, r = circle
ax.add_patch(plt.Circle((x, y), r, alpha=0.9,color = color))
plt.annotate(v+'\n'+str(p)+'%',(x,y),size=12,va='center',ha='center')
plt.xticks([])
plt.yticks([])
plt.show()

作为条形图替代的圆形打包。图片由作者提供。
主要收获
本文没有展示任何新内容,而是提出了用圆形图形替代矩形图表的想法。相反,这些圆形图表也可以用矩形图表替代。
选择合适的图表取决于许多因素,如受众、媒体等。主要的想法是根据使用情况改变图表,以使数据可视化更具吸引力。
显然,除了本文提到的图表和想法,还有更多其他的。如果你有任何意见,请随时分享。我很乐意看到。
感谢阅读
这些是我认为你可能会感兴趣的数据可视化文章:
-
9 种使用 Python 显示比例的可视化方法,而不是饼图 (link)
-
9 种使用 Python 的可视化方法,比条形图更引人注目 (link)
-
8 种使用 Python 处理多时间序列数据的可视化方法 (link)
-
7 种使用 Python 表达排名变化的可视化方法 (link)
-
Battle Royale — 比较 7 个用于交互式金融图表的 Python 库 (link)
参考
- 饼图和甜甜圈图模板:饼图和甜甜圈图。 Moqups. (n.d.). 取自
moqups.com/templates/charts-graphs/pie-donut-chart/
使用 Python 和 Plotly Express 绘制流数据
原文:
towardsdatascience.com/plot-streaming-data-with-python-and-plotly-express-e7aea8d9c441
实时跟踪国际空间站
·发布于 Towards Data Science ·8 分钟阅读·2023 年 3 月 26 日
--

国际空间站(图片来自 NASA 图片库)
流数据 指的是实时数据,它不断地从源头流向目标。它包括音频、视频、文本或由社交媒体*台、传感器和服务器等来源生成的数值数据。数据传输以稳定的流形式进行,没有固定的开始或结束。流数据在医疗保健、金融和运输等领域非常重要,并且它是 物联网 (IoT) 的关键组成部分。
处理流数据的能力是数据科学家的重要技能。在这个 快速成功数据科学 项目中,我们将使用流数据来跟踪国际空间站(ISS)绕地球轨道运行。我们将使用 Python 和 Plotly Express 在 Jupyter Notebook 中进行编码。
国际空间站遥测
遥测 是 原位 收集和自动传输远程传感器数据到接收设备以进行监测。虽然有众多国际空间站的遥测数据来源,我们将使用 WTIA REST API(WTIA 代表 国际空间站在哪里?)。
这个 API 由 Bill Shupp 编写,包含了比典型的国际空间站跟踪/通知网站提供的更多功能。文章末尾,我将列出一些额外的国际空间站遥测和跟踪来源,以备你想尝试或将我们的结果与他们的结果进行比较。
Plotly Express 库
Plotly Express 是 Plotly 图形库的内置部分。作为 Plotly 的简化、高级版本,它是创建大多数常见图形的推荐起点。
Plotly Express 包含 30 多个用于一次性创建整个图形的函数,这些函数的 API 设计得非常一致且易于学习。这使得在数据探索过程中可以轻松切换图形类型。
虽然 Plotly Express 易于使用且能创建美观的交互图,但它们的自定义程度不如使用 Plotly 或 matplotlib 等低级库生成的图表。像往常一样,你必须为了易用性而放弃一些控制权。
安装 Plotly 可以访问 Plotly Express。你可以使用pip安装它:
pip install plotly==5.13.1
或 conda:
conda install -c plotly plotly=5.13.1
请注意,版本号会随着时间变化,因此请务必检查 Plotly 的文档,其中还包括对经典 Jupyter Notebook 和 JupyterLab 的支持(如有需要)。此外,你可以在这里找到 Plotly Express 的PyPi页面。
过程
数据流并不是真正连续的。网站更新的频率是某个频率,比如每秒一次。同样,我们不能比数据生成的速度更快地获取数据,也不应该尝试尽可能快地获取。如果你频繁地 ping 一个网站,它可能会认为受到攻击并阻止你的访问。因此,在可能的情况下,我们应该以每 5 或 10 秒的礼貌速率请求数据。
在本项目中,我们将逐步处理流数据。以下是工作流程:
-
从网站获取数据。
-
将其放入 pandas DataFrame 中。
-
使用 Plotly Express 绘制图表。
-
清除图表。
-
重复。
导入库和分配常量
除了 Plotly Express 外,我们还需要time模块来控制访问流数据的频率;requests 库用于从 URL 获取数据;pandas 库用于准备数据以供绘图;以及IPython.display,用于在发布更新的 ISS 位置之前清除图表。
我们将使用名为ISS_URL的常量来存储 WTIA REST API 的 URL。由于流数据永无止境,我们将分配另一个常量ORBIT_TIME_SECS,帮助我们确定何时结束程序。该常量代表一个完整的 ISS 绕地球运行的时间(约 92 分钟)。
import time
import requests
import pandas as pd
import plotly.express as px
from IPython.display import clear_output
ISS_URL = 'https://api.wheretheiss.at/v1/satellites/25544'
ORBIT_TIME_SECS = 5_520 # Time required for ~1 complete orbit of ISS.
获取流数据
以下get_iss_telemetry函数从 URL 获取流数据,将其加载为 pandas DataFrame,删除不必要的列,并返回 DataFrame。每次调用此函数时,它会记录 ISS 的单个瞬间的遥测数据。稍后,我们将在循环中调用此函数,以随时间跟踪 ISS。当然,为了使这一切正常工作,你需要一个活跃的互联网连接。
def get_iss_telemetry(url):
"""Return DataFrame of current ISS telemetry from URL."""
response = requests.get(url)
if response.status_code != 200:
raise Exception(f"Failed to fetch ISS position from {url}. \
Status code: {response.status_code}")
data = response.json()
telemetry = pd.DataFrame(data, index=[0])
telemetry = telemetry.drop(['id', 'footprint', 'daynum',
'solar_lat', 'solar_lon'], axis=1)
return telemetry
首先从网站获取遥测数据。requests 库抽象化了 Python 中 HTTP(超文本传输协议)请求的复杂性,使其更加简单和人性化。get()方法检索 URL 并将输出分配给一个response变量,该变量引用了网页返回的Response对象。此对象的text 属性包含可读的网页文本字符串。
下一步是检查response对象的 HTTP 状态码。状态码 200 表示客户端已请求服务器上的文档,服务器已应允。虽然 200 是最常见的成功响应,但你可能还会看到 201、202、205 或 206。
网站的响应格式为 JSON(JavaScript 对象表示法),因此我们使用.json()方法将文本字符串加载为 Python 字典,名为data。以下是输出示例:
{'name': 'iss', 'id': 25544, 'latitude': 38.880080494467, 'longitude': -67.403143848187, 'altitude': 420.39039975637, 'velocity': 27592.859206857, 'visibility': 'daylight', 'footprint': 4509.4491061861, 'timestamp': 1679690648, 'daynum': 2460028.3639815, 'solar_lat': 1.5714095683049, 'solar_lon': 230.52865473588, 'units': 'kilometers'}
接下来,我们将data字典转换为名为telemetry的 pandas DataFrame。请注意,index=[0]参数可以避免“避免没有索引的标量值”错误。由于我们不需要诸如“id”或“solar_lat”之类的信息,我们将在返回 DataFrame 之前删除这些列。

最终的“遥测”DataFrame(图像来源于作者)
绘制国际空间站
最后一步是定义一个函数,用于绘制给定数量轨道的遥测数据。这个函数将使用for循环调用之前的函数。最终的图形将包括一个标记表示国际空间站,并且有一条线记录其 地面轨迹随时间变化。
由于我们将在二维地图上绘制一个球体,这条圆形路径将表现为一个 S 形的正弦曲线。这是一个有趣的视频,解释了原因。

当扁*化时,圆形轨道呈现为正弦波形(图像来源于作者)。
def track_iss(url, num_orbits=2, interval=10):
"""
Plot current ISS location from URL and record and plot its track.
Arguments:
url = ISS telemetry URL -> string
num_orbits = Number of ISS orbits to plot -> integer
interval = Wait period (seconds) between calls to URL -> integer
"""
num_pulls = int(ORBIT_TIME_SECS * num_orbits / interval)
latitudes = []
longitudes = []
for _ in range(num_pulls):
df = get_iss_telemetry(url).round(2)
latitudes.append(df['latitude'].iloc[0])
longitudes.append(df['longitude'].iloc[0])
clear_output(wait=True)
fig = px.scatter_geo(df,
lat='latitude',
lon='longitude',
color='visibility',
color_discrete_map = {'daylight': 'red',
'visible': 'orange',
'eclipsed': 'black'},
hover_data=['timestamp', 'altitude',
'velocity', 'units'])
fig.add_trace(px.line_geo(lat=latitudes, lon=longitudes).data[0])
fig.update_traces(marker=dict(symbol='x-open', size=10),
line_color='blue')
fig.update_layout(width=700, height=500,
title='International Space Station Tracking')
fig.show()
time.sleep(interval)
track_iss函数接受一个 URL、要绘制的轨道数量和一个暂停执行的时间间隔(秒)作为参数。这个暂停是为了防止网站被过多请求淹没。最后两个参数使用关键字,这意味着它们将被视为默认值。
num_pulls变量指的是我们调用get_iss_telemetry()函数并从网站“拉取”数据的次数。这个数字基于轨道时间、轨道数量和暂停间隔。
为了在地图上绘制国际空间站的轨迹,我们需要将每次提取的经纬度对存储在一个列表中。因此,我们在开始循环之前初始化每个属性的空列表。
要绘制遥测数据,我们遍历num_pulls变量,首先调用get_iss_telemetry()函数,将结果四舍五入到小数点后两位。然后,我们将纬度和经度结果附加到我们的列表中,并清除屏幕,以便每次循环都以新的图形开始。
现在来生成图形。首先,我们调用px.scatter_geo()方法在地球地图上绘制点。输入直观易懂。我们的 ISS 标记的颜色由 DataFrame 的“visibility”列决定。输出包括“daylight”、“eclipsed”和“visible”。后者指的是 ISS 仍在反射阳光,从而在夜空中可见。要将这些结果转换为颜色,我们将color_discrete_map参数传递一个字典。
为了支持“悬停窗口”弹出,我们将hover_data参数传递为我们想要查看的数据列名列表。要查看完整的 DataFrame,我们应传递df.columns,而不是列表。
在 Plotly 中,图形由一个或多个轨迹组成,每个轨迹是一个绘图元素,例如散点图、折线图或条形图。px.line_geo()方法返回一个包含单个轨迹对象的图形对象,该对象表示用给定的纬度和经度绘制的线。我们使用其数据属性(.data[0])来添加该对象。索引为“0”,因为只有一个轨迹。
添加轨迹后,我们需要使用update_traces()方法更新图中的轨迹。我们的 ISS 标记参数使用字典指定。我们使用开放的“X”作为标记符号。对于折线图,我们指定颜色为蓝色。
为了完成图形,我们调用update_layout()方法并传入宽度、高度和标题。然后我们调用show()方法来显示结果。
循环通过调用time模块的sleep()方法并传入interval变量来结束。在这种情况下,它将使循环暂停 10 秒。尽管 WTIA REST API 的请求速率被限制为每秒一次,但没有必要贪婪!
剩下的就是调用我们的函数:
track_iss(ISS_URL)

显示约 2 圈 ISS 轨道的 ISS 跟踪器(图片作者提供)
结果
尽管代码量微不足道,但这个图表拥有很多功能。
如果将光标悬停在标记上,弹出窗口将显示诸如站点的高度、速度(以公里/小时为单位)、可见性等遥测数据。

Plotly Express 图形与激活的 ISS 标记悬停窗口(图片作者提供)
你可以使用鼠标滚轮或工具栏进行缩放。你可以截图、*移和重置视图。ISS 标记的颜色会根据 ISS 的照明情况而变化。

当 ISS 标记的位置对应于白天时,标记会变成红色(图片作者提供)
最后,我们通过增量处理流式 ISS 数据。尽管它以每秒一赫兹的频率流式传输,我们还是每十秒拉取一次数据,以便对源 API 更为友好。
更多跟踪器
你还可以通过Open-Notify-API访问 ISS 的遥测数据。
正如使用 Python 一样,总有多种方法可以完成任务。以下是一些追踪空间站的替代方法:
-
使用 Plotly Express 和正射投影(地球)。
-
使用 Plotly Express 并计算空间站的速度。
-
使用一个ISS 图标进行追踪,并包含船员信息。
-
使用Raspberry Pi追踪空间站。
谢谢!
感谢阅读,确保关注我以获取未来更多快速成功数据科学项目。
如何在 Matplotlib 中绘制垂直和水*线
原文:
towardsdatascience.com/plot-vertical-horizontal-lines-matplotlib-34cf7fe86ce
使用 matplotlib 在 Python 图表中绘制水*线和垂直线
·发表于 Towards Data Science ·阅读时间 5 分钟·2023 年 1 月 9 日
--

图片来源:Kelly Sikkema 在 Unsplash
Matplotlib 是用于绘制静态或交互式可视化的最常用库之一。在处理图表时,一个常见的任务是需要在图表中的特定位置绘制线条。例如,你可能希望绘制一条水*线或垂直线,以标记阈值或突出显示某个数据点。
在本教程中,我们将演示如何使用 matplotlib 函数在现有图表中绘制垂直和水*线。我们还将讨论在将线条添加到图表时应考虑的一些选项和注意事项。
首先,我们创建一个基础图表来可视化正弦函数——一个广为人知的三角函数——我们将在本文中引用它以演示如何绘制附加的线条。
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 20, 100)
y = np.sin(x)
fig, ax = plt.subplots()
ax.plot(x, y)
plt.show()
这是输出图表(希望它对你来说很熟悉!):

使用 matplotlib 来可视化正弦函数(trigonometric function)——来源:作者
绘制一条水*线
现在,为了在坐标轴上绘制一条水*线,我们可以使用[matplotlib.pyplot.axhline()](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.axhline.html)函数,该函数接受以下三个参数:
-
y:这是水*线将在 y 轴上定位的精确数据点。 -
xmin:这是一个浮点数,取值范围在 0 到 1 之间,表示相对于 x 轴的起始点。例如,如果设置为0.5,则水*线将从图的中间位置开始,位于指定的y位置。值为0表示图的最左边,而1对应于图的最右边。 -
xmax:类似地,这是一个浮点数参数,范围在 0 到 1 之间,表示绘制的水*线的终点。值为0表示图的最左边,而1对应于图的最右边。
在下面的示例中,我们在0.75的 y 轴点添加了一条水*线,起始和结束位置分别为指定的xmin和xmax值:
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 20, 100)
y = np.sin(x)
fig, ax = plt.subplots()
ax.plot(x, y)
ax.axhline(y=0.75, color='r')
plt.show()
生成的图应如下所示:

带有水*线的正弦图 — 来源:作者
如果我们想绘制一条不限制于xmin和xmax位置的线(它们默认为0和1),我们可以简单地重新排列函数调用为
ax.axhline(y=0.75, color='r')

没有 xmin 和 xmax 的水*线 — 来源:作者
绘制垂直线
同样,要在坐标轴上绘制垂直线,我们需要调用[matplotlib.pyplot.axvline()](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.axvline.html)函数,该函数需要以下三个参数:
-
x:这是水*线将在 y 轴上定位的精确数据点。 -
ymin:这是一个浮点数,取值范围在 0 到 1 之间,表示相对于 y 轴的起始点。值为0表示图的底部,而1对应于图的顶部。 -
ymax:类似地,这是一个浮点数参数,范围在 0 到 1 之间,表示绘制的垂直线的终点。值为0表示图的底部,而1对应于图的顶部。
现在假设我们想在现有图上绘制一条垂直线,该垂直线将在7.5值处穿过 x 轴。
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 20, 100)
y = np.sin(x)
fig, ax = plt.subplots()
ax.plot(x, y)
ax.axvline(x=7.5, color='y')
plt.show()
axvline()函数将在指定点创建一条垂直线,从图的顶部到最底部(因为我们没有指定ymin和ymax):

使用axvline绘制的垂直线 — 来源:作者
结论
总之,Python 中的 Matplotlib 库通过使用axhline()和axvline()函数分别允许在图表中创建水*和垂直线。
这些函数接受关于 x 或 y 轴上线的位置的参数,以及有关线的起始和结束点的可选参数。通过使用这些函数,可以很容易地突出显示特定数据点或在 Python 中的图表和图形中添加阈值。
成为会员 并阅读 Medium 上的每一个故事。你的会员费用直接支持我和你阅读的其他作者。你还将全面访问 Medium 上的所有故事。
通过我的推荐链接加入 Medium — Giorgos Myrianthous
了解何时以及如何在对数刻度上可视化数据
展示如何使用 matplotlib 和 Python 将图例放置在图表之外
如何在 Python 中使用 Matplotlib 绘制对数轴
作为 Medium 会员,你的一部分会员费用会分给你阅读的作者,并且你可以全面访问所有故事……
了解如何将 matplotlib 图形和图表保存为图像文件
Plotly 和 Pandas:结合力量实现有效的数据可视化
一份受《数据讲故事》启发的快速指南
·
关注 发表在 Towards Data Science ·13 min read·2023 年 7 月 10 日
--
“我的数据可视化技能很糟糕。我的观众似乎对我的工作不感兴趣,更糟的是,他们没有被说服。”
曾经有很多人遇到过这个问题。除非你有天赋或之前上过设计课程,否则制作视觉美观且对观众直观的图表可能非常具有挑战性和耗时。
当时我想到的是:我希望在制作图表时更加用心,以便它能直观地传达信息给我的观众。我的意思是,不要过度消耗他们的脑力和时间来理解发生了什么。
我曾经认为从 Matplotlib 切换到 Seaborn,再到 Plotly 可以解决美学问题。事实上,我错了。可视化不仅仅是关于美学的。以下是我尝试复制的两个可视化图表,灵感来自《数据讲故事》¹由 Cole Nussbaumer Knaflic 编写,这些图表真正激励我改变了我的可视化方法。它们看起来干净、优雅且有目的。我们将在本文中尝试复制这些图表!

作者提供的图像
这篇文章的要点是。如果你在寻找关于优秀可视化背后概念的深入解释,查看《数据讲故事》¹,每一页都是值得你花时间阅读的宝贵内容。如果你在寻找工具特定的实用建议,你来对地方了。Cole 在她的书开头提到,她提供的建议是普遍适用的,与工具无关,尽管她承认她在书中的示例是使用 Excel 创建的。有些人,包括我自己,出于许多原因并不喜欢 Excel 和拖放工具。有些人更喜欢使用 Python、R 及其他一些编程语言来创建可视化。如果你属于这一类,并且将 Python 作为主要工具,那么这篇文章就是为你准备的。
目录
-
链式调用—Pandas 绘图
-
水*条形图
-
折线图
-
附录:数字图表
链式调用—Pandas 绘图
如果你对使用 Pandas 进行数据整理颇有经验,你可能会遇到或甚至自己采用“链式调用”的概念。简而言之,链式调用可以让你的代码更具可读性、更易于调试,并且更适合生产。以下是我所指的一个简单示例。你不必逐行阅读,只需快速浏览即可理解“链式调用”的概念。每一步都清晰易懂,代码组织良好,没有不必要的中间变量。
(epl_10seasons
.rename(columns=lambda df_: df_.strip())
.rename(columns=lambda df_: re.sub('\W+|[!,*)@#%(&$_?.^]', '_', df_))
.pipe(lambda df_: df_.astype({column: 'int8' for column in (df_.select_dtypes("integer").columns.tolist())}))
.pipe(lambda df_: df_.astype({column: 'category' for column in (df_.select_dtypes("object").columns.tolist()[:-1])}))
.assign(match_date=lambda df_: pd.to_datetime(df_.match_date, infer_datetime_format=True))
.assign(home_team=lambda df_: np.where((df_.home_team == "Arsenal"), "The Gunners", df_.home_team),
away_team=lambda df_: np.where((df_.away_team == "Arsenal"), "The Gunners", df_.away_team),
month=lambda df_: df_.match_date.dt.month_name())
.query('home_team == "The Gunners"')
)
这很好,但你是否知道你可以继续链式调用以创建基本的可视化图表?Pandas Plot 默认使用 Matplotlib 后端来实现这一点。让我们看看它是如何工作的,并复制 Cole 在她的书中创建的一些示例。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.graph_objects as go
%matplotlib inline
pd.options.plotting.backend = 'plotly'
df = pd.DataFrame({"concerns": ["Engine power is less than expected",
"Tires make excessive noise while driving",
"Engine makes abnormal/excessive noise",
"Seat material concerns",
"Excessive wind noise",
"Hesitation or delay when shifting",
"Bluetooth system has poor sound quality",
"Steering system/wheel has too much play",
"Bluetooth system is difficult to use",
"Front seat audio/entertainment/navigation controls"
],
"concerns per 1,000": [12.9, 12.3, 11.6, 11.6, 11.0, 10.3, 10.0, 8.8, 8.6, 8.2],},
index=list(range(0,10,1)))
我们有一个看起来像这样的 DataFrame。

作者提供的图像
(df
.plot
.barh()
)
这是生成基本可视化图表的最快方法。通过将**.plot**属性和**.line**方法直接链式调用到 DataFrame,我们获得了下面的图表。

图片由作者提供
如果你认为上面的图表没有通过美学检查,请暂时按捺住你的反应和判断。确实,它看起来至少有些难看。让我们改进一下,让它更好。诀窍是,将 Pandas 绘图后端从 Matplotlib 切换到 Plotly,这样即将揭示的魔法就会显现。
pd.options.plotting.backend = 'plotly'
你可能会问,“为什么要换成 Plotly?难道 Matplotlib 不也能做到同样的事情吗?”好吧,这就是区别所在。
如果我们在 Pandas 中使用 Matplotlib 后端,它返回一个 Axes 对象,尝试使用内置的**type()**方法自行验证。这很好,因为 axes 对象允许我们访问方法以进一步修改图表。查看这份文档²以了解对 Axes 对象执行的可能方法。我们来选择一个快速演示一下。
(df
.plot
.barh()
.set_xlabel("concerns per 1,000")
)

图片由作者提供
我们成功地将 x 轴标签设置为“每千个关注点”,但这样做的结果是,我们返回了一个Text 对象,失去了珍贵的 Axis 对象,而这个对象让我们可以访问进一步修改图表的宝贵方法。真可惜!
这里是绕过上述限制的另一种方法,
(df
.plot
.barh(xlabel="Concerns per 1,000", ylabel="Concerns", title="Top 10 design concerns")
)

图片由作者提供
然而,由于 Pandas 实现的限制,我们仍然无法进行广泛的修改,具体请参考这里。
另一方面,Plotly 不返回一个 Axes 对象。它返回一个**go.Figure**对象。这里的区别在于,负责更新图表的方法也会返回一个**go.Figure**对象,这样你可以继续链式调用方法,以进一步更新图表。我们来试试吧!
顺便说一下,如果你想知道我如何获得下面的方法和参数组合,它们都可以在官方文档中找到,在这里³。
这里是一些重要的方法,帮助你入门——**.update_traces**,**.add_traces**,**.update_layout**,**.update_xaxes**,**.update_yaxes**,**.add_annotation**,**.update_annotations**。
横向条形图
让我们为下面的可视化定义一组颜色调色板。
GRAY1, GRAY2, GRAY3 = '#231F20', '#414040', '#555655'
GRAY4, GRAY5, GRAY6 = '#646369', '#76787B', '#828282'
GRAY7, GRAY8, GRAY9, GRAY10 = '#929497', '#A6A6A5', '#BFBEBE', '#FFFFFF'
BLUE1, BLUE2, BLUE3, BLUE4, BLUE5 = '#25436C', '#174A7E', '#4A81BF', '#94B2D7', '#94AFC5'
BLUE6, BLUE7 = '#92CDDD', '#2E869D'
RED1, RED2, RED3 = '#B14D4A', '#C3514E', '#E6BAB7'
GREEN1, GREEN2 = '#0C8040', '#9ABB59'
ORANGE1, ORANGE2, ORANGE3 = '#F36721', '#F79747', '#FAC090'
gray_palette = [GRAY1, GRAY2, GRAY3, GRAY4, GRAY5, GRAY6, GRAY7, GRAY8, GRAY9, GRAY10]
blue_palette = [BLUE1, BLUE2, BLUE3, BLUE4, BLUE5, BLUE6, BLUE7]
red_palette = [RED1, RED2, RED3]
green_palette = [GREEN1, GREEN2]
orange_palette = [ORANGE1, ORANGE2, ORANGE3]
sns.set_style("darkgrid")
sns.set_palette(gray_palette)
sns.palplot(sns.color_palette())

图片由作者提供
在这里,我们希望通过定义不同的颜色来突出显示等于或高于 10% 的关注点。
color = np.array(['rgb(255,255,255)']*df.shape[0])
color[df
.set_index("concerns", drop=True)
.iloc[::-1]
["concerns per 1,000"]>=10] = red_palette[0]
color[df
.set_index("concerns", drop=True)
.iloc[::-1]
["concerns per 1,000"]<10] = gray_palette[4]
然后我们从 DataFrame 创建图表。
(df
.set_index("concerns", drop=True)
.iloc[::-1]
.plot
.barh()
.update_traces(marker=dict(color=color.tolist()))
)

图片由作者提供
更新布局将得到如下结果。在这里,我们指定模板,添加标题和边距,并指定图形对象的大小。让我们暂时对注释进行评论。
(df
.set_index("concerns", drop=True)
.iloc[::-1]
.plot
.barh()
.update_traces(marker=dict(color=color.tolist()))
.update_layout(template="plotly_white",
title=dict(text="<b>Top 10 design concerns</b> <br><sup><i>concerns per 1,000</i></sup>",
font_size=30,
font_color=gray_palette[4]),
margin=dict(l=50,
r=50,
b=50,
t=100,
pad=20),
width=1000,
height=800,
showlegend=False,
#annotations=annotations
)
)

作者提供的图片
更新 x 和 y 轴属性将得到如下结果。
(df
.set_index("concerns", drop=True)
.iloc[::-1]
.plot
.barh()
.update_traces(marker=dict(color=color.tolist()))
.update_layout(template="plotly_white",
title=dict(text="<b>Top 10 design concerns</b> <br><sup><i>concerns per 1,000</i></sup>",
font_size=30,
font_color=gray_palette[4]),
margin=dict(l=50,
r=50,
b=50,
t=100,
pad=20),
width=1000,
height=800,
showlegend=False,
#annotations=annotations
)
.update_xaxes(title_standoff=10,
showgrid=False,
visible=False,
tickfont=dict(
family='Arial',
size=16,
color=gray_palette[4],),
title="")
.update_yaxes(title_standoff=10,
tickfont=dict(
family='Arial',
size=16,
color=gray_palette[4],),
title="")
)

作者提供的图片
最后但同样重要的是,我们将添加一些注释到图表中。在这里,我们有一些注释——为水*条形图和脚注添加数据标签。让我们一起做这个。首先,我们在一个单独的单元格中定义注释。
annotations = []
y_s = np.round(df["concerns per 1,000"], decimals=2)
# Adding data labels
for yd, xd in zip(y_s, df.concerns):
# labeling the bar net worth
annotations.append(dict(xref='x1',
yref='y1',
y=xd, x=yd - 1,
text=str(yd) + '%',
font=dict(family='Arial', size=16,
color=gray_palette[-1]),
showarrow=False))
# Adding Source Annotations
annotations.append(dict(xref='paper',
yref='paper',
x=-0.72,
y=-0.050,
text='Source: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco'
'<br>laboris nisi ut aliquip ex ea commodo consequat.',
font=dict(family='Arial', size=10, color=gray_palette[4]),
showarrow=False,
align='left'))
(df
.set_index("concerns", drop=True)
.iloc[::-1]
.plot
.barh()
.update_traces(marker=dict(color=color.tolist()))
.update_layout(template="plotly_white",
title=dict(text="<b>Top 10 design concerns</b> <br><sup><i>concerns per 1,000</i></sup>",
font_size=30,
font_color=gray_palette[4]),
margin=dict(l=50,
r=50,
b=50,
t=100,
pad=20),
width=1000,
height=800,
showlegend=False,
annotations=annotations
)
.update_xaxes(title_standoff=10,
showgrid=False,
visible=False,
tickfont=dict(
family='Arial',
size=16,
color=gray_palette[4],),
title="")
.update_yaxes(title_standoff=10,
tickfont=dict(
family='Arial',
size=16,
color=gray_palette[4],),
title="")
)

作者提供的图片
相对于最初的默认版本,这是不是一个更好的图表?让我们继续探索另一种流行图表——折线图。
请注意,下面的示例比上面的更复杂。然而,思路保持不变。
折线图
让我们快速查看默认的 Matplotlib 折线图绘图后端。
pd.options.plotting.backend = 'matplotlib'
df = pd.DataFrame({"Received": [160,184,241,149,180,161,132,202,160,139,149,177],
"Processed":[160,184,237,148,181,150,123,156,126,104,124,140]},
index=['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'])
(df
.plot
.line()
);

作者提供的图片
让我们将绘图后端切换到 Plotly!
pd.options.plotting.backend = 'plotly'
(df
.plot(x=df.index,
y=df.Received,
labels=dict(index="", value="Number of tickets"),)
)
在将 Pandas 绘图后端切换到 Plotly 后,上面的代码给出了以下结果。在这里,我们首先绘制已接收系列。

作者提供的图片
让我们通过进一步链接上面的方法来更新线条属性。在这里,我们修改颜色、宽度,并在数据点上放置标记。
(df
.plot(x=df.index,
y=df.Received,
labels=dict(index="", value="Number of tickets"),)
.update_traces(go.Scatter(mode='lines+markers+text',
line={"color": gray_palette[4], "width":4},
marker=dict(size=12)),)
)

作者提供的图片
让我们将处理过的系列添加到图表中!
(df
.plot(x=df.index,
y=df.Received,
labels=dict(index="", value="Number of tickets"),)
.update_traces(go.Scatter(mode='lines+markers+text',
line={"color": gray_palette[4], "width":4},
marker=dict(size=12)),)
.add_traces(go.Scatter(x=df.index, #Add Processed col
y=df.Processed,
mode="lines+markers+text",
line={"color": blue_palette[0], "width":4},
marker=dict(size=12)))
)

作者提供的图片
让我们在 5 月的索引处添加一条垂直线,以显示两条线开始分歧的点。
(df
.plot(x=df.index,
y=df.Received,
labels=dict(index="", value="Number of tickets"),)
.update_traces(go.Scatter(mode='lines+markers+text',
line={"color": gray_palette[4], "width":4},
marker=dict(size=12)),)
.add_traces(go.Scatter(x=df.index, #Add Processed col
y=df.Processed,
mode="lines+markers+text",
line={"color": blue_palette[0], "width":4},
marker=dict(size=12)))
.add_traces(go.Scatter(x=["May", "May"], #Add vline
y=[0,230],
fill="toself",
mode="lines",
line_width=0.5,
line_color= gray_palette[4]))
)

作者提供的图片
接下来,让我们通过将背景更改为白色,添加标题、边距和其他一些元素来更新整体布局。关于注释,我们暂时进行评论。
(df
.plot(x=df.index,
y=df.Received,
labels=dict(index="", value="Number of tickets"),)
.update_traces(go.Scatter(mode='lines+markers+text',
line={"color": gray_palette[4], "width":4},
marker=dict(size=12)),)
.add_traces(go.Scatter(x=df.index, #Add Processed col
y=df.Processed,
mode="lines+markers+text",
line={"color": blue_palette[0], "width":4},
marker=dict(size=12)))
.add_traces(go.Scatter(x=["May", "May"], #Add vline
y=[0,230],
fill="toself",
mode="lines",
line_width=0.5,
line_color= gray_palette[4]))
.update_layout(template="plotly_white",
title=dict(text="<b>Please approve the hire of 2 FTEs</b> <br><sup>to backfill those who quit in the past year</sup> <br>Ticket volume over time <br><br><br>",
font_size=30,),
margin=dict(l=50,
r=50,
b=100,
t=200,),
width=900,
height=700,
yaxis_range=[0, 300],
showlegend=False,
#annotations=right_annotations,
)
)

作者提供的图片
接下来,我们将对x 和 y 轴进行更新。
(df
.plot(x=df.index,
y=df.Received,
labels=dict(index="", value="Number of tickets"),)
.update_traces(go.Scatter(mode='lines+markers+text',
line={"color": gray_palette[4], "width":4},
marker=dict(size=12)),)
.add_traces(go.Scatter(x=df.index, #Add Processed col
y=df.Processed,
mode="lines+markers+text",
line={"color": blue_palette[0], "width":4},
marker=dict(size=12)))
.add_traces(go.Scatter(x=["May", "May"], #Add vline
y=[0,230],
fill="toself",
mode="lines",
line_width=0.5,
line_color= gray_palette[4]))
.update_layout(template="plotly_white",
title=dict(text="<b>Please approve the hire of 2 FTEs</b> <br><sup>to backfill those who quit in the past year</sup> <br>Ticket volume over time <br><br><br>",
font_size=30,),
margin=dict(l=50,
r=50,
b=100,
t=200,),
width=900,
height=700,
yaxis_range=[0, 300],
showlegend=False,
#annotations=right_annotations,
)
.update_xaxes(dict(range=[0, 12],
showline=True,
showgrid=False,
linecolor=gray_palette[4],
linewidth=2,
ticks='',
tickfont=dict(
family='Arial',
size=13,
color=gray_palette[4],
),
))
.update_yaxes(dict(showline=True,
showticklabels=True,
showgrid=False,
ticks='outside',
linecolor=gray_palette[4],
linewidth=2,
tickfont=dict(
family='Arial',
size=13,
color=gray_palette[4],
),
title_text="Number of tickets"
))
)

作者提供的图片
最后但同样重要的是,我们将添加一些注释到图表中。在这里,我们有一些注释——为折线图添加标签(已接收,已处理),以及为散点添加标签,这可能会有些复杂。让我们一起做这个。首先,我们在一个单独的单元格中定义注释。
y_data = df.to_numpy()
colors = [gray_palette[3], blue_palette[0]]
labels = df.columns.to_list()
right_annotations = []
# Adding labels to line
for y_trace, label, color in zip(y_data[-1], labels, colors):
right_annotations.append(dict(xref='paper',
x=0.95,
y=y_trace,
xanchor='left',
yanchor='middle',
text=label,
font=dict(family='Arial',size=16,color=color),
showarrow=False))
# Adding labels to scatter point
scatter_annotations = []
y_received = [each for each in df.Received]
y_processed = [float(each) for each in df.Processed]
x_index = [each for each in df.index]
y_r = np.round(y_received)
y_p = np.rint(y_processed)
for ydn, yd, xd in zip(y_r[-5:], y_p[-5:], x_index[-5:]):
scatter_annotations.append(dict(xref='x2 domain',
yref='y2 domain',
y=ydn,
x=xd,
text='{:,}'.format(ydn),
font=dict(family='Arial',size=16,color=gray_palette[4]),
showarrow=False,
xanchor='center',
yanchor='bottom',
))
scatter_annotations.append(dict(xref='x2 domain',
yref='y2 domain',
y=yd,
x=xd,
text='{:,}'.format(yd),
font=dict(family='Arial',size=16,color=blue_palette[0]),
showarrow=False,
xanchor='center',
yanchor='top',
))
在定义注释后,我们只需将注释变量放入链式方法中,如下所示。
(df
.plot(x=df.index,
y=df.Received,
labels=dict(index="", value="Number of tickets"),)
.update_traces(go.Scatter(mode='lines+markers+text',
line={"color": gray_palette[4], "width":4},
marker=dict(size=12)),)
.add_traces(go.Scatter(x=df.index, #Add Processed col
y=df.Processed,
mode="lines+markers+text",
line={"color": blue_palette[0], "width":4},
marker=dict(size=12)))
.add_traces(go.Scatter(x=["May", "May"], #Add vline
y=[0,230],
fill="toself",
mode="lines",
line_width=0.5,
line_color= gray_palette[4]))
.update_layout(template="plotly_white",
title=dict(text="<b>Please approve the hire of 2 FTEs</b> <br><sup>to backfill those who quit in the past year</sup> <br>Ticket volume over time <br><br><br>",
font_size=30,),
margin=dict(l=50,
r=50,
b=100,
t=200,),
width=900,
height=700,
yaxis_range=[0, 300],
showlegend=False,
annotations=right_annotations,
)
.update_layout(annotations=scatter_annotations * 2)
.update_xaxes(dict(range=[0, 12],
showline=True,
showgrid=False,
linecolor=gray_palette[4],
linewidth=2,
ticks='',
tickfont=dict(
family='Arial',
size=13,
color=gray_palette[4],
),
))
.update_yaxes(dict(showline=True,
showticklabels=True,
showgrid=False,
ticks='outside',
linecolor=gray_palette[4],
linewidth=2,
tickfont=dict(
family='Arial',
size=13,
color=gray_palette[4],
),
title_text="Number of tickets"
))
.add_annotation(dict(text="<b>2 employees quit in May.</b> We nearly kept up with incoming volume <br>in the following two months, but fell behind with the increase in Aug <br>and haven't been able to catch up since.",
font_size=18,
align="left",
x=7.5,
y=265,
showarrow=False))
.add_annotation(dict(xref='paper',
yref='paper',
x=0.5,
y=-0.15,
text='Source: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco'
'<br>laboris nisi ut aliquip ex ea commodo consequat.',
font=dict(family='Arial',
size=10,
color='rgb(150,150,150)'),
showarrow=False,
align='left'))
.update_annotations(yshift=0)
.show()
)

作者提供的图片
附加:数字图表
恭喜你阅读到文章的这一部分!这是一个额外的图表,供你参考!我们在这里创建了一个图表,以美观的方式呈现一个数字。简而言之,这就是我所指的。

图片来源:作者
由于这不是 DataFrame 的结果,我们可以从头开始创建一个空的 **go.Figure** 对象,然后逐步添加注释。最后,通过相应地更新布局来结束。
(go
.Figure() # Create an empty figure
.add_annotation(
x=0.0,
y=1,
text='Pilot program was a success',
showarrow=False,
font={'size': 36, 'color': 'white'},
bgcolor=gray_palette[-3],
bordercolor='gray',
borderwidth=0,
xref='paper',
yref='paper',
xanchor='left',
yanchor='top',
align='left',
ax=0,
ay=-10
)
.add_annotation(
x=-1.0, # X-coordinate of the text position
y=3.0, # Y-coordinate of the text position
text="After the pilot program,", # The text content
showarrow=False, # Hide the arrow
font=dict(size=20,
color=blue_palette[1]), # Customize the font size
xanchor='left',
yanchor='top',
align='left',
)
.add_annotation(
x=-1.0, # X-coordinate of the text position
y=1.6, # Y-coordinate of the text position
text="<b>68%</b>", # The text content
showarrow=False, # Hide the arrow
font=dict(size=160,
color=blue_palette[1]), # Customize the font size
xanchor='left',
align='left',
)
.add_annotation(
x=-1.0, # X-coordinate of the text position
y=0.2, # Y-coordinate of the text position
text="<b>of kids expressed interest towards science,</b>", # The text content
showarrow=False, # Hide the arrow
font=dict(size=20,
color=blue_palette[1]), # Customize the font size
xanchor='left',
align='left',
)
.add_annotation(
x=-1.0, # X-coordinate of the text position
y=-0.2, # Y-coordinate of the text position
text="compared to 44% going into the program.", # The text content
showarrow=False, # Hide the arrow
font=dict(size=20,
color=gray_palette[-3]), # Customize the font size
xanchor='left',
align='left',
)
.add_annotation(
x=-1.0, # X-coordinate of the text position
y=-0.7, # Y-coordinate of the text position
text='Based on survey of 100 students conducted '
'before and after pilot program '
'(100% response rate on both surveys).', # The text content
showarrow=False, # Hide the arrow
font=dict(size=10.5,
color=gray_palette[-3]), # Customize the font size
xanchor='left',
align='left',
)
.update_layout(
xaxis=dict(visible=False), # Hide x-axis
yaxis=dict(visible=False), # Hide y-axis
margin=dict(l=0,
r=0,
b=0,
t=0,
pad=0),
font=dict(size=26,
color=gray_palette[-3]), # Customize the font size
paper_bgcolor='rgba(0,0,0,0)',
plot_bgcolor='rgba(0,0,0,0)'
)
.show()
)
后记
就这些了!关键是逐步更新和完善你的图表,直到获得理想的结果。当然,每种技术都有其局限性。如果你的图表变得过于复杂,可能有必要参考 Plotly Express,甚至使用 Plotly Graph Objects 从头开始构建。刚开始采用这种技术可能会显得困难和陌生,但坚持练习,你很快就能创建出有意义且美观的可视化!
如果你从这篇文章中获得了一些有用的内容,请考虑在 Medium 上给我一个 关注。很简单,每周一篇文章,让自己保持更新,走在前沿!
与我联系!
参考文献
-
《用数据讲故事》由 Cole Nussbaumer Knaflic 编著。
www.storytellingwithdata.com/books -
Matplotlib Axes API.
matplotlib.org/stable/api/axes_api.html -
Plotly 图形库。
plotly.com/python/reference/
Plotly 和 Python:为岩心物理和地质数据创建互动热图
可视化地下钻井记录测量中的地理空间变化
·发表于 Towards Data Science ·7 分钟阅读·2023 年 6 月 16 日
--

Plotly 热图用于探索挪威大陆架上钻井记录测量的地理空间变化。图片由作者提供。
解释地下数据需要了解地质和岩心物理数据在一个区域内的变化。这通常涉及处理分散在区域内的钻井记录测量和解释属性,这导致了估算这些测量之间的值的挑战。
我们可以通过一种称为克里金法(kriging)的地统计学方法来估算值(或填补空白)。这种方法在观察到的测量数据之间进行估算和外推,并预测未测量位置的值。
在我之前的文章中,我们重点介绍了如何使用 pykrige 和 matplotlib 来绘制和可视化挪威大陆架的地质变化。本文将进一步推进这些可视化,并使这些图表变得互动。
## 利用 pykrige 和 matplotlib 进行地质变化的空间可视化
从钻井记录测量探索空间地质变化
towardsdatascience.com
Pykrige 使用快速回顾
在使用 Plotly 之前,我们将快速回顾一下在上一篇文章中使用的代码,以便你跟上进度。
第一步是导入我们所需的库。在这种情况下,我们需要 pandas 来加载我们的 csv 数据,pykrige 来进行数据点之间的插值,以及 numpy 来进行一些数学操作。
import pandas as pd
from pykrige import OrdinaryKriging
import numpy as np
df = pd.read_csv('Data/Xeek Force 2020/Xeek_2020_Balder_DTC_AVG.csv')
一旦数据加载完成,我们可以通过调用 pykrige 的 OrdinaryKriging 方法来进行克里金过程。
在这个调用中,我们传入我们的 x 和 y 数据,代表数据的纬度和经度。我们还需要传入我们想要外推的变量。在这种情况下,我们使用的是 Balder Formation 的*均声波压缩慢度(DTC)值。
一旦模型生成后,我们可以将其应用于覆盖井位的自定义纬度和经度范围。
OK = OrdinaryKriging(x=df['LON'],
y=df['LAT'],
z=df['DTC_MEAN'],
variogram_model='exponential',
verbose=True, enable_plotting=True,
coordinates_type='geographic')
grid_lat = np.arange(57.5, 62, 0.01, dtype='float64')
grid_long = np.arange(1.5, 4.5, 0.01,dtype='float64')
zstar, ss = OK.execute('grid', grid_long, grid_lat)
zstar
然后我们将两个位置数组 grid_lat 和 grid_long 以及我们的网格数据传递给 matplotlib 的 imshow 图。这样会生成类似于下图的图形。

使用 pykrige 运行普通克里金(Ordinary Kriging)的数据的 Matplotlib imshow 图表。图片由作者提供。
尽管我们返回的图形讲述了数据趋势的故事,但很难识别具体的井位以及测量点之间的任何值。
使用 Plotly 使我们的图表具有交互性
一种立即改变这一点的方法是使用 Plotly 库。Plotly 是一个出色的库,用于创建高度交互的图表,且易于组装。
Plotly 提供了两种主要的绘图方式:Plotly Express 和 Plotly Graph Objects。
Plotly Express 提供了一个用于 Plotly 的高级接口,并利用简单的语法创建强大的交互式图表。然而,自定义图表的某些方面可能需要大量工作,并且可能比较困难。这时,库中的 Graph Objects 部分就派上用场了。它提供了一个低级接口,可以完全控制你的图形;然而,这也意味着组装图形会稍微复杂一些。
在这个示例中,我们将使用 Graph Objects,可以按照以下方式导入:
import plotly.graph_objects as go
接下来,我们可以使用 numpy 的 linspace 函数来定义我们的 x 和 y 数组。
这将创建两个与我们之前创建的数据网格大小相同的数组。
我们还将创建两个用于经度和纬度的列表。这些值超出了数据的经度和纬度值,并且允许我们在数据点的边缘周围有填充。
longitude = [1.5, 4.5]
latitude = [57.5, 62]
x = np.linspace(longitude[0], longitude[1], zstar.shape[1])
y = np.linspace(latitude[0], latitude[1], zstar.shape[0])
使用 matplotlib 时,我们可以使用 imshow 来显示这类数据。
尽管 Plotly 也有imshow图,但我在写作时(据我所知)无法控制图形的范围。这意味着我们不能指定坐标轴的起始点值。
因此,为了展示我们的数据网格,我们可以切换到使用Plotly 的热图。
热图根据每个数据单元格的值为其着色。你可以在我的关于 Seaborn 的文章中了解更多关于热图的信息。
我们可以使用以下代码来创建 Plotly Graph Objects 热图。
fig = go.Figure()
fig.add_trace(go.Heatmap(z=zstar, x=x, y=y))
fig.update_xaxes(range=(longitude[0], longitude[1]))
fig.update_yaxes(range=(latitude[0], latitude[1]))
fig.update_layout(autosize=False, width=800, height=800)
fig.show()
首先,我们创建一个图形对象,然后添加一个轨迹。这个轨迹包含我们的x和y位置数据,以及由克里金插值法生成的网格(zstar)。
我们还会将图形的大小设置为 800 x 800,这样在 Jupyter 笔记本中就能得到足够大的图表进行操作。
运行上述代码后,我们得到带有所有数据值的热图,且坐标轴显示在正确的范围内。

Plotly Graph Objects 热图显示了挪威大陆架上我们声波压缩缓慢度测量的变化。图片由作者提供。
这个图表的优点是我们可以悬停在任何点上查看值。此外,Plotly 允许我们放大某些部分以进行更仔细的观察。
尽管上述图表很好,但我们缺少额外的信息,这些信息可以帮助读者,例如井的位置以及坐标轴标签。
为了添加我们的井位置,我们需要添加第二个轨迹。这次使用go.scatter()并传入数据框中的纬度和经度值。我们还可以通过添加一个字典来控制这些点的显示。在这个示例中,我们将它们设置为黑色。
fig = go.Figure()
fig.add_trace(go.Heatmap(z=zstar, x=x, y=y))
# Add Well Locations
fig.add_trace(go.Scatter(x=df['LON'], y=df['LAT'],
mode='markers', marker=dict(color='black')))
fig.update_xaxes(range=(longitude[0], longitude[1]))
fig.update_yaxes(range=(latitude[0], latitude[1]))
fig.update_layout(autosize=False, width=800, height=800)
fig.show()

Plotly Graph Objects 热图显示了测量位置(井)以及我们声波压缩缓慢度在挪威大陆架上的变化。图片由作者提供。
现在,我们可以看到我们井的位置;然而,如果我们悬停在标记上,得到的只是纬度和经度值。这在某种程度上是有用的;不过,知道标记表示哪个井以及该井测量的 DTC 值会更好。
为了解决这个问题,我们可以在数据框中直接创建一个新的列作为悬停文本。如果我们以后要在其他图表中使用这个信息,这会很有用。
fig = go.Figure()
fig.add_trace(go.Heatmap(z=zstar, x=x, y=y,
colorbar=dict(title='DTC (us/ft)',
title_font=dict(size=18))))
df['hover_text'] = df.apply(lambda row: f"""<b>{row['WELL']}</b>
<br>Latitude: {row['LAT']}
<br>Longitude: {row['LON']}
<br>Log Value: {round(row['DTC_MEAN'], 2)}""",
axis=1)
fig.add_trace(go.Scatter(x=df['LON'], y=df['LAT'],
mode='markers', marker=dict(color='black'),
name='Wells', text=df['hover_text'], hoverinfo='text', showlegend=True))
fig.update_xaxes(range=(longitude[0], longitude[1]),
title='Longitude')
fig.update_yaxes(range=(latitude[0], latitude[1]),
title='Latitude')
fig.update_layout(autosize=False, width=800, height=800,
legend=dict(x=1, y=0, xanchor='auto', yanchor='auto',
bgcolor='rgba(255, 255, 255, 0.5)'))
fig.show()
当我们运行上述代码时,我们得到以下图表。

Plotly Graph Objects 热图增加了互动性。这个图表显示了测量位置(井)以及我们声波压缩缓慢度的变化。图片由作者提供。
现在,当我们将鼠标悬停在任何一个井上时,会显示井名,接着是纬度和经度,以及记录值。在这种情况下,我们展示的是声学压缩慢度。
总结
在这个简短的教程中,我们已经看到如何超越简单和静态的 matplotlib 图形来展示我们的测量变化。Plotly 提供的额外功能和互动性使其成为可视化井记录测量的地理空间变化的绝佳选择。
额外的互动性让我们能够识别每个点所代表的井的具体信息、该位置的测量值,并解释那些没有直接测量值的网格的数值。
使用的数据集
本文使用的数据集是 Xeek 和 FORCE 2020 机器学习竞赛训练数据集的一个子集(Bormann et al., 2020)。该数据集在挪威政府的 NOLD 2.0 许可证下发布,详细信息可在此处找到:挪威开放政府数据许可证 (NLOD) 2.0。完整数据集可以通过这里访问。
数据集的完整参考资料是:
Bormann, Peter, Aursand, Peder, Dilib, Fahad, Manral, Surrender, & Dischington, Peter. (2020). FORCE 2020 井记录和岩相数据集,用于机器学习竞赛 [数据集]。Zenodo. doi.org/10.5281/zenodo.4351156
感谢阅读。在你离开之前,你一定要订阅我的内容,并在你的邮箱中获取我的文章。 你可以在这里做到这一点!
其次,你可以通过订阅会员获得完整的 Medium 体验,并支持我和其他数千名作者。只需每月$5,你就能全面访问所有精彩的 Medium 文章,还能通过写作赚取收入。
如果你使用 我的链接, 你将直接用你的费用支持我,并且这不会增加你的费用。如果你这样做了,非常感谢你的支持。
在 Python 中绘制和弦图
原文:
towardsdatascience.com/plotting-chord-diagrams-in-python-72fd71b3eef0
如何使用 Holoviews 绘制和弦图以显示各种数据属性之间的关系
·发表于 Towards Data Science ·阅读时间 7 分钟·2023 年 2 月 15 日
--

由作者提供的图片
目前谈到数据可视化时,通常会想到一些常见的图表类型——条形图、饼图、直方图等。然而,还有一种非常有趣但很少讨论的图表类型——和弦图。那么,什么是和弦图呢?
和弦图表示多个实体(称为节点)之间的流动或连接。使用和弦图,你可以轻松地可视化数据集中各个数据点之间的连接或关系。以航班延误数据集为例,它包含了从一个机场到另一个机场的详细航班信息。如果你想可视化各机场之间的关系,和弦图(见本文开头的图示)是展示这些信息的绝佳方式。
在这篇文章中,我将向你展示如何使用一个名为HoloViews的第三方库绘制和弦图。
所有图片均由作者提供,除非另有说明。
安装 Holoviews
HoloViews 是一个开源的 Python 库,旨在使数据分析和可视化变得无缝且简单。HoloViews 依赖于另外两个 Python 库——pyviz 和 bokeh。因此,安装 HoloViews 的最佳方法是使用以下命令:
conda install -c pyviz holoviews bokeh
使用航班延误数据集
像往常一样,我最喜欢用来说明概念的数据集是 2015 年的航班延误数据集。你可以从以下网址下载数据集:www.kaggle.com/datasets/usdot/flight-delays。
许可协议: CC0: 公共领域
在本文中,所有代码示例将使用 Jupyter Notebook 运行。
首先,使用 Pandas 加载flights.csv文件:
import pandas as pd
df = pd.read_csv('flights.csv')
df
这是数据框:

对于本文,我们需要两个特定的列:
-
ORIGIN_AIRPORT
-
DESTINATION_AIRPORT
我们感兴趣的是查看起点和终点机场之间的关系。
查找起点和终点机场之间的关系
下一步是找出起点和终点机场的唯一组合。你可以使用groupby()函数,然后使用count()函数:
df_between_airports = df.groupby(by=["ORIGIN_AIRPORT", "DESTINATION_AIRPORT"]).count()
df_between_airports
输出是一个多索引数据框:

显然,你只需要一个非索引列。因此,让我们提取YEAR列并将其重命名为COUNT,然后重置索引:
df_between_airports = df_between_airports['YEAR'].rename('COUNT').reset_index()
df_between_airports
现在输出包含从一个机场到另一个机场的航班数量:

注意到一些机场代码是 5 位数字(例如 10135、10397 等)。这些实际上是 FAA 的机场 ID,用作 IATA 代码(如 XNA、SFO、SLC 等)的替代值。理想情况下,我们应将所有这些 5 位数的机场 ID 替换为实际的 IATA 代码,但为了简化本文,我们将删除它们:
df_between_airports = df_between_airports.query(
'ORIGIN_AIRPORT.str.len() <= 3 & DESTINATION_AIRPORT.str.len() <= 3')
df_between_airports
现在你对我们尝试实现的目标有了更清晰的了解。例如,从 ABE 到 ATL 共有 898 个航班,从 ABE 到 DTW 共有 711 个航班,等等:

观察到总共有 4693 个组合,生成这么多组合的和弦图会非常混乱。因此,让我们按降序排列它们:
df_between_airports = df_between_airports.sort_values(by="COUNT",
ascending=False)
df_between_airports

接下来提取前 40 个组合:
top = 40
df_between_airports.head(top)['ORIGIN_AIRPORT'].unique()
在前 40 个组合中,共有 18 个起点机场:
array(['SFO', 'LAX', 'JFK', 'LAS', 'LGA', 'ORD', 'OGG', 'HNL', 'ATL',
'MCO', 'DFW', 'SEA', 'BOS', 'DCA', 'FLL', 'PHX', 'DEN', 'TPA'],
dtype=object)
展示和弦图
我们现在准备展示和弦图。首先,导入holoviews并指定bokeh作为扩展:
import holoviews as hv
hv.extension('bokeh')
HoloViews 使用%%opts单元魔法来修改单元的执行方式,以显示其输出。你使用Chord类来展示和弦图:
%%opts Chord [height=500 width=500 title="Flights between airports" ]
chord = hv.Chord(df_between_airports.head(top))
chord
以下输出显示了前 40 个航班组合之间的关系:

和弦图上的每个圆圈(称为节点)代表一个机场。要查看机场之间的关系,请将鼠标悬停在一个圆圈上:

上图显示了从DFW(达拉斯/沃思堡国际机场)起飞的航班。显然,无法查看目的地机场。因此,让我们获取起点和终点机场的列表,然后使用它来创建一个hv.Dataset对象:
# get the top count of flights between airports
df_between_airports = df_between_airports.head(top)
# find all the unique origin and destination airports
airports = list(set(df_between_airports["ORIGIN_AIRPORT"].unique().tolist() +
df_between_airports["DESTINATION_AIRPORT"].unique().tolist()))
airports_dataset = hv.Dataset(pd.DataFrame(airports, columns=["Airport"]))
要显示每个节点的机场名称,请在%%opts单元魔法中设置labels属性,并将airports_dataset变量传递给Chord类初始化器:
%%opts Chord [height=500 width=500 title="Flights between airports" labels="Airport"]
chord = hv.Chord((df_between_airports, airports_dataset))
chord
请注意,
df_between_airports和airports_dataset变量被包装为一个元组。
弦图现在在每个节点上显示了机场代码:

当你现在悬停在 DFW 上时,你可以清楚地看到 DFW 的航班飞往 ORD(奥黑尔国际机场)和 ATL(哈茨菲尔德-杰克逊亚特兰大国际机场):

如果你点击DFW节点,其余的航班路径将被灰色化:

为弦图应用颜色
弦图支持 Bokeh 调色板。你可以在 docs.bokeh.org/en/latest/docs/reference/palettes.html 查看调色板颜色列表。
Bokeh 调色板提供了一系列用于颜色映射的调色板。
这里有两个常用的 Bokeh 调色板:

来源: docs.bokeh.org/en/latest/docs/reference/palettes.html
现在让我们通过添加另一个 %%opts 单元魔法语句来为弦图应用颜色:
%%opts Chord [height=500 width=500 title="Flights between airports" labels="Airport"]
%%opts Chord (node_color="Airport" node_cmap="Category20" edge_color="ORIGIN_AIRPORT" edge_cmap='Category20')
chord = hv.Chord((df_between_airports, airports_dataset))
chord
node_cmap 指示应用于节点的调色板,而 edge_color 指示应用于弦图边缘的调色板:

这是另一个应用了不同调色板的示例:
%%opts Chord [height=500 width=500 title="Flights between airports" labels="Airport"]
%%opts Chord (node_color="Airport" node_cmap="Category20" edge_color="ORIGIN_AIRPORT" edge_cmap='Bokeh')
chord = hv.Chord((df_between_airports, airports_dataset))
chord

确认弦图中的关系
在本文的最终部分,我只想确保弦图显示了正确的信息。选择JFK表明航班飞往LAX和SFO:

要确认这一点,请使用以下语句:
df_between_airports.query('ORIGIN_AIRPORT == "JFK"')
输出确认了弦图中的答案:

在弦图中选择ORD显示航班飞往:
-
LGA
-
LAX
-
DFW
-
SFO

以下语句确认了我们所看到的:
df_between_airports.query('ORIGIN_AIRPORT == "ORD"')

如果你喜欢阅读我的文章,并且这些文章对你的职业/学习有帮助,请考虑成为 Medium 会员。会员费用为每月 $5,并且可以无限制地访问 Medium 上的所有文章(包括我的文章)。如果你通过以下链接注册,我将赚取少量佣金(不会增加你的额外费用)。你的支持意味着我将能够花更多时间撰写像这样的文章。
[## 通过我的推荐链接加入 Medium - Wei-Meng Lee
阅读 Wei-Meng Lee 的每个故事(以及 Medium 上成千上万的其他作者)。你的会员费用直接支持…
weimenglee.medium.com](https://weimenglee.medium.com/membership?source=post_page-----72fd71b3eef0--------------------------------)
总结
现在你已经了解了如何使用 HoloViews 创建弦图,你会在什么时候使用它呢?你应该在以下场景中使用弦图:
-
当你想用简单的表示方式展示大数据集之间的相互连接时。
-
当你想要创建视觉上引人注目的、具有美感的表示时。
-
当你需要查找和比较数据组之间的相互关系时。
尽情玩转弦图,并记得告诉我你在实际项目中是如何使用它的!
使用 Python 绘制网络图
原文:
towardsdatascience.com/plotting-network-graphs-using-python-bc62f0d93b3f
了解如何使用 NetworkX 包来可视化复杂网络
·发表于 Towards Data Science ·阅读时间 8 分钟·2023 年 3 月 6 日
--

图片由 Alina Grubnyak 在 Unsplash 提供
网络图是一种可视化形式,使你能够可视化和分析实体之间的关系。例如,下图展示了 2013 年夏季一个月内维基百科编辑对各种维基百科语言版本的贡献。

来源: en.wikipedia.org/wiki/Graph_theory#/media/File:Wikipedia_multilingual_network_graph_July_2013.svg
从网络图中,你可以得出一些观察结论:
-
英语 (en) 是主要的语言,所有其他语言都翻译成英语;同时,许多英语材料也被翻译成其他语言
-
中文 (zh) 被翻译成日语 (ja),但反向翻译则不然
-
中文和日语材料都被翻译成英语,反之亦然
在这篇文章中,我将向你展示如何使用NetworkX包绘制网络图的基础知识。
安装 NetworkX
要安装 NetworkX 包,请使用pip命令:
!pip install networkx
一些图的术语
在开始绘制你的网络图之前,了解一些基本的网络图术语是很有用的。
下图展示了一个有向图(也称为有向图;图中的边有方向,如箭头所示):

所有图片均由作者提供
-
节点是图中的基本单元。节点也通常被称为顶点。
-
边是图中两个节点之间的连接。
与此同时,无向图没有节点之间的方向(因此没有箭头),而且边是双向的。
创建图形
现在开始创建一个网络图。我们将逐步进行。
首先,创建一个networkx.classes.graph.Graph对象:
import networkx as nx
G = nx.Graph()
print(G)
# Graph with 0 nodes and 0 edges
nx.Graph()类创建一个无向图。如果你想创建一个有向图,请使用nx.DiGraph(directed=True),它返回一个networkx.classes.digraph.DiGraph对象。
我们将在本文后面讨论有向图。
添加节点
创建图形(G)后,你现在需要向其中添加一些节点:
G.add_node("Singapore")
G.add_node("San Francisco")
G.add_node("Tokyo")
print(G)
# Graph with 3 nodes and 0 edges
上述代码片段向图形添加了三个节点,但尚未定义边。除了使用add_node()函数添加单个节点外,你还可以使用add_nodes_from()函数一次性添加多个节点:
G.add_nodes_from(["Riga", "Copenhagen"])
print(G)
# Graph with 5 nodes and 0 edges
目前你的图形有五个节点。
添加边
定义了节点后,你现在可以添加边以连接它们:
G.add_edge("Singapore","San Francisco")
G.add_edge("San Francisco","Tokyo")
G.add_edges_from(
[
("Riga","Copenhagen"),
("Copenhagen","Singapore"),
("Singapore","Tokyo"),
("Riga","San Francisco"),
("San Francisco","Singapore"),
]
)
print(G)
# Graph with 5 nodes and 6 edges
与节点一样,你可以使用add_edge()函数添加单独的边,或者使用add_edges_from()函数添加多个边(只需提供表示每条边的元组列表)。
绘制图形
在图形中添加了节点和边后,你现在可以使用draw()函数来可视化图形:
nx.draw(G)
你应该会看到类似这样的结果:

请注意,每次调用
draw()函数时,你都会得到不同的图形。
这是同一图形的另一种变体:

显示标签
显然,没有标签的图形不是很有用(如果完全可用的话!)。所以让我们绘制一个带有节点标签的图:
nx.draw(G, with_labels = True)
上述
draw()函数与with_labels参数等同于调用以下函数:
-
nx.draw_networkx_nodes()— 绘制图形中的所有节点 -
nx.draw_networkx_labels()— 在每个节点上绘制标签 -
nx.draw_networkx_edges()— 绘制连接节点的边
那么使用
draw()函数与其他函数相比有什么优势呢?实际上,使用各个函数绘图可以让你自定义单个节点、标签和边的外观。
现在你可以看到每个节点的标签:

使用布局
记住draw()函数每次绘制图形时都会使用不同的布局?好吧,你可以指定你想要用于图形的布局。这里是一个例子:
pos = nx.circular_layout(G)
nx.draw(G, pos, with_labels = True)
所有节点现在将以圆形方式排列:

另外,你也可以通过nx.draw_circular()函数(而不是nx.draw()函数)使用圆形布局绘制图形:
nx.draw_circular(G, with_labels = True)
你可以尝试其他布局:
-
nx.draw_kamada_kawai(G, with_labels = True) -
nx.draw_planar(G, with_labels = True) -
nx.draw_random(G, with_labels = True) -
nx.draw_spectral(G, with_labels = True) -
nx.draw_spring(G, with_labels = True) -
nx.draw_shell(G, with_labels = True)
标记边
在给节点标记的情况下,您可能还想标记边。您可以通过 nx.draw_networkx_edge_labels() 函数来实现。
pos = nx.circular_layout(G)
nx.draw(G, pos, with_labels = True)
nx.draw_networkx_edge_labels(
G,
pos,
edge_labels={
("Singapore","Tokyo"): '2 flights daily',
("San Francisco","Singapore"): '5 flights daily',
},
font_color='red'
)
上述代码片段标记了三个节点的两条边:

有向图
到目前为止,我们的图是无向的。在某些情况下,绘制有向图可能会很有用。例如,在我们的示例中,边可能表示两个城市之间的航班。使用有向图可以让我们直观地检查哪些航班从一个城市到另一个城市。以下代码片段显示了我们示例现在被绘制为有向图:
import networkx as nx
#---directed graph---
G = nx.DiGraph(directed=True)
# add nodes
G.add_node("Singapore")
G.add_node("San Francisco")
G.add_node("Tokyo")
G.add_nodes_from(["Riga", "Copenhagen"])
# add edges
G.add_edge("Singapore","San Francisco")
G.add_edge("San Francisco","Tokyo")
G.add_edges_from(
[
("Riga","Copenhagen"),
("Copenhagen","Singapore"),
("Singapore","Tokyo"),
("Riga","San Francisco"),
("San Francisco","Singapore"),
]
)
# set layout
pos = nx.circular_layout(G)
# draw graph
nx.draw(G, pos, with_labels = True)
# draw edge labels
nx.draw_networkx_edge_labels(
G, pos,
edge_labels={
("Singapore","Tokyo"): '2 flights daily',
("San Francisco","Singapore"): '5 flights daily',
},
font_color='red'
)
现在您可以观察到从新加坡到旧金山的航班,反之亦然。另一方面,从里加到旧金山有航班,但反向则没有。

定制节点
默认情况下,节点是蓝色的,大小相当小。您可以通过将字典传递给 draw() 函数来定制节点以及边的颜色:
options = {
'node_color': 'yellow', # color of node
'node_size': 3500, # size of node
'width': 1, # line width of edges
'arrowstyle': '-|>', # array style for directed graph
'arrowsize': 18, # size of arrow
'edge_color':'blue', # edge color
}
nx.draw(G, pos, with_labels = True, arrows=True, **options)
节点现在是黄色的(并且更大),边是蓝色的:

给节点添加轮廓
如果您想为节点添加轮廓,需要使用 matplotlib 手动完成。以下代码片段将图形大小设置为 10 英寸 x 10 英寸(宽度 x 高度),然后使用 set_edgecolor() 函数为每个节点绘制黑色轮廓:
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 10))
pos = nx.circular_layout(G)
options = {
'node_color': 'yellow',
'node_size': 8500,
'width': 1,
'arrowstyle': '-|>',
'arrowsize': 18,
}
nx.draw(G, pos, with_labels = True, arrows=True, **options)
ax = plt.gca()
ax.collections[0].set_edgecolor("#000000")
每个节点现在都有一个黑色轮廓:

如果您没有设置图形大小,您的图表可能看起来像这样:

设置节点颜色
要为每个节点指定不同的颜色,您可以指定一个颜色调色板,例如来自 bokeh,并将颜色设置在传递给 draw() 函数的字典中的 node_color 键中:
from networkx import *
import matplotlib.pyplot as plt
from bokeh.palettes import Spectral
plt.figure(figsize=(8, 8))
pos = nx.circular_layout(G)
options = {
'node_color': Spectral[5], # first 5 colors from the Spectral palette
'node_size': 8500,
'width': 1,
'arrowstyle': '-|>',
'arrowsize': 18,
}
nx.draw(G, pos=pos, with_labels = True, arrows=True, **options)
ax = plt.gca()
ax.collections[0].set_edgecolor("#000000")
现在图表将为每个节点使用不同的颜色:

如果您想指定自己的颜色,可以在字典中手动设置它们:
options = {
'node_color': ['yellow','magenta','lightblue','lightgreen','pink'],
'node_size': 8500,
'width': 1,
'arrowstyle': '-|>',
'arrowsize': 18,
}
相关文章
了解如何让您的网络图栩栩如生
towardsdatascience.com ## 使用 Basemap 和 mplleaflet 可视化地理空间网络图
了解如何在地图上绘制网络图
towardsdatascience.com
如果你喜欢阅读我的文章,并且这些文章对你的职业/学习有帮助,请考虑注册成为 Medium 会员。每月$5,注册后可以无限访问 Medium 上所有文章(包括我的)。如果你通过以下链接注册,我将获得一小笔佣金(不会增加你的额外费用)。你的支持意味着我能花更多时间撰写类似的文章。
[## 使用我的推荐链接加入 Medium - Wei-Meng Lee
阅读 Wei-Meng Lee 的每一个故事(以及 Medium 上其他成千上万位作者的故事)。你的会员费将直接支持…
weimenglee.medium.com](https://weimenglee.medium.com/membership?source=post_page-----bc62f0d93b3f--------------------------------)
摘要
在这篇文章中,我探讨了网络图的基础知识以及它如何帮助你可视化数据集中不同实体之间的关系。本文重点介绍了如何使用 NetworkX 包来绘制图形。在我的下一篇文章中,我将利用航班延误数据集来可视化不同机场之间的联系。敬请关注!
在 Python 中绘制维恩图
原文:
towardsdatascience.com/plotting-venn-diagrams-in-python-6c55e0d78e57
学习如何使用维恩图展示两个或更多数据集之间的关系
·发表于 Towards Data Science ·阅读时间 8 分钟·2023 年 2 月 24 日
--

图片由 Dustin Humes 提供,Unsplash
在数据可视化中,我们生成的大多数图表属于以下一种或多种类型:
-
条形图
-
饼图
-
折线图
-
直方图
-
时间序列
然而,有一种图表不常用,那就是 维恩图。维恩图 是另一种被低估的可视化形式。它实际上是一种非常有用的可视化形式,可以让你检查两个不同数据集之间的关系。例如,以下维恩图展示了两个生物集的关系——集合 A(左圈;有两条腿的生物)和 B(右圈;会飞的生物)。重叠区域包含那些既有两条腿又能飞的生物:

来源: en.wikipedia.org/wiki/Venn_diagram#/media/File:Venn_diagram_example.png
在本文中,我将向你展示如何从样本数据集中绘制一个维恩图。我还会展示如何自定义维恩图以修改其外观和感觉。
所以让我们开始吧!
安装 matplotlib-venn 包
使用 pip 命令安装 matplotlib-venn 包:
!pip install matplotlib-venn
数据集
对于数据集,我创建了一个虚构的数据文件,名为 purchases.csv,内容如下:
custid,product
1,Mac mini
17,Mac mini
1,Mac Studio
2,MacBook Pro 13
3,Mac Studio
18,Mac mini
2,MacBook Pro 13
5,Mac Studio
7,Mac Studio
6,MacBook Pro 13
4,MacBook Pro 13
8,Mac mini
9,Mac mini
5,Mac mini
6,Mac mini
19,Mac mini
8,Mac Studio
2,Mac mini
2,Mac Studio
20,MacBook Pro 13
该文件包含了不同客户通过 custid 识别的三款 Mac 产品(Mac mini、Mac Studio 和 MacBook Pro 13)的购买记录。
下一步是将其加载到 Pandas DataFrame 对象中:
import pandas as pd
df = pd.read_csv('purchases.csv')
df

所有图片由作者提供
绘制 Venn 图
要绘制一个 2 圆 Venn 图,有几种方法。最简单的方法是向venn2()函数(在matplotlib_venn包中)提供两个集合的值,它会自动为你绘制 Venn 图。
让我们使用这种方法来绘制一个 2 圆 Venn 图,显示购买 Mac mini、Mac Studio 及两者的数量。
首先,我会找到所有购买了 Mac mini 的客户,并提取custid作为一个集合:
mac_mini = set(df.query('product == "Mac mini"')['custid'])
mac_mini
以下custid集合是那些购买了 Mac mini 的:
{1, 2, 5, 6, 8, 9, 17, 18, 19}
同样,我会提取所有购买了 Mac Studio 的custid:
mac_studio = set(df.query('product == "Mac Studio"')['custid'])
mac_studio
这些是购买了 Mac Studio 的客户:
{1, 2, 3, 5, 7, 8}
现在我们准备绘制 2 圆 Venn 图:
from matplotlib_venn import venn2
venn2([mac_mini, mac_studio],
('Mac mini','Mac Studio'))

注意,你需要提供要显示在 Venn 图上的标签。如果不提供,默认标签是 A 和 B,这可能会有点误导或混淆。
如果你想要一个 3 圆 Venn 图,只需调用venn3()函数:
from matplotlib_venn import venn3
macbookpro_13 = set(df.query('product == "MacBook Pro 13"')['custid'])
macbookpro_13 # {2, 4, 6, 20}
venn3([mac_mini, mac_studio, macbookpro_13],
('Mac mini','Mac Studio','MacBook Pro 13'))

我非常喜欢这种方法,因为我不需要手动计算有多少人只购买了 Mac mini,有多少人购买了 Mac Studio,有多少人购买了 Mac mini 和 Mac Studio 等。
备选方法 1
第二种方法是手动传递数值到venn2()或venn3()函数中。对于venn2()函数,格式是:venn2(subsets = (*Ab*,*aB*,*AB*)),其中:
-
Ab表示集合 A 中但不在集合 B 中的项目数量
-
aB表示集合 B 中但不在集合 A 中的项目数量
-
AB表示同时包含在集合 A 和 B 中的项目数量
让我们使用数据框计算Ab、aB和AB的值。首先,找到购买了 Mac mini 但没有购买 Mac Studio 的人:
# calculate Ab
mac_mini_exclude_mac_studio = mac_mini - mac_studio
display(mac_mini_exclude_mac_studio)
# {6, 9, 17, 18, 19}
然后,找到购买了 Mac Studio 但没有购买 Mac mini 的人:
# calculate aB
mac_studio_exclude_mac_mini = mac_studio - mac_mini
display(mac_studio_exclude_mac_mini)
# {3, 7}
最后,找到购买了Mac mini和Mac Studio的所有人:
# calculate AB
mac_mini_and_mac_studio = mac_studio.intersection(mac_mini)
display(mac_mini_and_mac_studio)
# {1, 2, 5, 8}
计算了Ab、aB和AB的值后,你只需计算每个集合中的项目数,并将它们传递给venn2()函数:
venn2(subsets = (
len(mac_mini_exclude_mac_studio), # Ab
len(mac_studio_exclude_mac_mini), # aB
len(mac_mini_and_mac_studio) # AB
),
set_labels = ('Mac mini','Mac Studio')
)
不足为奇,结果与我们之前得到的相同:

备选方法 2
下一种方法是二进制方法。你可以用字典代替传递值作为元组。对于 2 圆 Venn 图,你以以下格式传递二进制值:
-
Ab — “
10” -
aB— “
01" -
AB— “
11”
对于 3 圆 Venn 图,二进制值如下:
-
Abc — “
100” -
ABc — “
110” -
ABC — “
111” -
aBC— “
011" -
abC — “
001” -
AbC— “
101” -
aBc — “
010”
以下代码片段绘制了你之前绘制的相同 2 圆 Venn 图:
venn2(subsets = {
'10': len(mac_mini_exclude_mac_studio), # Ab
'01': len(mac_studio_exclude_mac_mini), # aB
'11': len(mac_mini_and_mac_studio) # AB
},
set_labels = ('Mac mini','Mac Studio'),
)
自定义 Venn 图
由于生成的 Venn 图是使用 matplotlib 创建的,因此它可以像使用 matplotlib 创建的任何图表一样自定义。
设置透明度
你可以使用 alpha 参数设置圆圈的透明度:
v2 = venn2(subsets = {
'10': len(mac_mini_exclude_mac_studio),
'01': len(mac_studio_exclude_mac_mini),
'11': len(mac_mini_and_mac_studio)
},
set_labels = ('Mac mini','Mac Studio'),
alpha = 0.8,
)
下面是将 alpha 参数设置为 0.8 的图表效果。如果你想要更浅的色调,可以将其设置为更低的值,如 0.1 或 0.2:

设置颜色
你可以使用 set_colors 参数指定圆圈的单独颜色:
v2 = venn2(subsets = {
'10': len(mac_mini_exclude_mac_studio),
'01': len(mac_studio_exclude_mac_mini),
'11': len(mac_mini_and_mac_studio)
},
set_labels = ('Mac mini','Mac Studio'),
alpha = 0.8,
set_colors=('lightblue', 'yellow')
)

设置线条样式
要绘制圆圈的轮廓,使用 venn2_circles() 函数(用于 2 圆韦恩图)与 venn2() 函数结合使用。以下代码片段展示了如何绘制带有 -- 虚线和 5 线宽的轮廓:
from matplotlib_venn import venn2_circles
c = venn2_circles(subsets = {
'10': len(mac_mini_exclude_mac_studio),
'01': len(mac_studio_exclude_mac_mini),
'11': len(mac_mini_and_mac_studio)
},
linestyle='--',
linewidth=5,
)
你可以参考
matplotlib.org/3.1.0/gallery/lines_bars_and_markers/linestyles.html了解支持的线条样式列表。
这是更新后的韦恩图:

设置字体大小
韦恩图上显示有两种类型的标签:
-
标签 — 圆圈外的文本
-
子集标签 — 圆圈内的文本
以下代码片段设置了两种类型标签的字体大小:
for text in v2.set_labels: # the text outside the circle
text.set_fontsize(20);
for text in v2.subset_labels: # the text inside the circle
text.set_fontsize(15)

自定义线条样式
你还可以在 venn2_circles() 函数之外以编程方式设置轮廓的样式和线宽:
c[0].set_lw(3.0) # customize left outline
c[0].set_ls('-.')
c[1].set_lw(2.0) # customize right circle
c[1].set_ls('--')

设置图表标题
由于这是 matplotlib,你显然可以为图形设置标题:
import matplotlib.pyplot as plt
plt.title('Customers distribution for Mac Mac and Mac Studio')

设置子集标签
如果你想自定义单独标签的外观,可以使用 get_label_by_id() 函数并传入单个圆圈的二进制值来引用标签,并设置它们的显示文本和颜色:
for text in v2.set_labels: # the text outside the circle
text.set_fontsize(20);
for text in v2.subset_labels: # the text inside the circle
text.set_fontsize(12)
text = 'Mac mini\n'
for i in mac_mini_exclude_mac_studio:
text += f'{i}\n'
v2.get_label_by_id('10').set_text(text) # Mac mini
text = 'Mac Studio\n'
for i in mac_studio_exclude_mac_mini:
text += f'{i}\n'
v2.get_label_by_id('01').set_text(text) # Mac Studio
text = 'Mac mini &\n Mac Studio\n'
for i in mac_mini_and_mac_studio: # Mac mini and Mac Studio
text += f'{i}\n'
v2.get_label_by_id('11').set_text(text)
v2.get_label_by_id('11').set_color('red')

如果你喜欢阅读我的文章,并且这些文章对你的职业/学习有帮助,请考虑成为 Medium 会员。每月 $5,你可以无限访问 Medium 上的所有文章(包括我的)。如果你使用以下链接注册,我将获得少量佣金(对你没有额外费用)。你的支持意味着我将能花更多时间写这样的文章。
[## 通过我的推荐链接加入 Medium - Wei-Meng Lee
阅读 Wei-Meng Lee 的每一个故事(以及 Medium 上成千上万的其他作家)。你的会员费直接支持…
weimenglee.medium.com](https://weimenglee.medium.com/membership?source=post_page-----6c55e0d78e57--------------------------------)
总结
就这样完成了!你学会了如何使用示例数据框绘制简单的 2 圈和 3 圈维恩图,并且了解了可以对图形进行的各种自定义。绘制维恩图很简单,更具挑战性的是整理你的数据,以便将其传递给 API 进行绘图。无论如何,希望你玩得开心!
POCS 基于的聚类算法解释
每个数据有不同的重要性
·
关注 发布于 Towards Data Science ·5 分钟阅读·2023 年 3 月 24 日
--
图片来源于 Kier in Sight 在 Unsplash
聚类分析(或 聚类)是一种数据分析技术,它探索并将一组向量(或数据点)分组,使得同一簇中的向量彼此间的相似性大于与其他簇中的向量。聚类算法在数据分析、模式识别和图像处理等众多应用中被广泛使用。
本文回顾了一种基于凸集投影(POCS)方法的新聚类算法,称为 POCS 基础的聚类算法。原始论文在IWIS2022中介绍,源代码也已发布在Github上。
凸集
凸集定义为一个数据点的集合,其中连接集合中任意两个点 x1 和 x2 的线段完全包含在该集合中。根据这种凸集的定义,空集∅、单集合、线段、超*面和欧几里得球被认为是凸集。数据点也被认为是一个凸集,因为它是一个单集合(一个只有一个元素的集合)。这指向了 POCS 概念可以应用于数据点聚类的新路径。
投影到凸集(POCS)
让我简要回顾一下 POCS 的概念(不涉及方程)。POCS 的方法大致可以分为两种形式:交替 和 并行。
交替 POCS
从数据空间中的任意一点开始,将该点交替投影到两个(或更多)相交的凸集上,将会收敛到这些集合交集中的一点。下面显示了一个图形说明。

作者提供的图片。
当凸集不相交时,交替投影将收敛到依赖于投影顺序的贪婪极限循环。

作者提供的图片。
并行 POCS
与交替形式不同,POCS 的并行形式同时将数据点投影到所有凸集上,每个投影都有一个重要性权重。对于两个非空交集的凸集,与交替版本类似,并行投影收敛到这些集合的交集中的一点。

作者提供的图片。
在凸集不相交的情况下,投影将收敛到一个最小化解决方案。POCS 基础的聚类算法的主要思想源于这一特性。

作者提供的图片。
欲了解 POCS 的更多细节,请访问原始论文和/或一些其他推荐的论文(可用 pdf 文件):
POCS 基础的聚类算法
利用并行 POCS 方法的收敛性质,作者提出了一种非常简单但有效(在某种程度上)的聚类算法。该算法的操作与经典的 K-Means 算法类似,但处理每个数据点的方式有所不同,即 K-Means 算法对每个数据点赋予相同的权重,而 POCS-based 聚类算法则对每个数据点赋予不同的权重,权重与数据点到簇原型的距离成正比。就这样!
下面显示了算法的伪代码:

图像来自论文。
实验结果
作者检查了 POCS-based 聚类算法在一些公共基准数据集上的性能,这些数据集来自网站Clustering basic benchmark。这些数据集的描述总结在下表中。

图像来自论文。
在论文中,作者比较了 POCS-based 聚类算法与其他传统聚类方法(包括 K-Means 和 Fuzzy C-Means 算法)的性能。关于执行时间和聚类误差的评估总结在以下表格中。

图像来自论文。

图像来自论文。
可视化的聚类结果也在以下图中展示。

图像来自论文。
欲了解更多细节,可以在原始论文这里查看。
示例代码
让我们在一个非常简单的数据集上试用这个算法。为了简单起见,可以使用以下命令安装发布的算法包:
pip install pocs-based-clustering
首先,我们导入几个必要的包,并创建一个以 10 个簇为中心的 5000 个数据点的简单数据集:
# Import packages
import time
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs
from pocs_based_clustering.tools import clustering
# Generate a simple dataset
num_clusters = 10
X, y = make_blobs(n_samples=5000, centers=num_clusters, \
cluster_std=0.5, random_state=0)
plt.figure(figsize=(8,8))
plt.scatter(X[:, 0], X[:, 1], s=50)
plt.show()

图像由作者提供。
现在,使用内置函数进行聚类并显示结果:
# POSC-based Clustering Algorithm
centroids, labels = clustering(X, num_clusters, 100)
# Display results
plt.figure(figsize=(8,8))
plt.scatter(X[:, 0], X[:, 1], c=labels, s=50, cmap='viridis')
plt.scatter(centroids[:, 0], centroids[:, 1], s=100, c='red')
plt.show()

图像由作者提供。
结论
在这篇文章中,我简要回顾了一种基于凸集投影方法(POCS)的简单而有效的聚类技术,称为 POCS-based Clustering Algorithm。该算法利用 POCS 的收敛性质应用于聚类任务,并在一定程度上实现了可行的改进。该算法的有效性已在一些基准数据集上得到验证。原始论文可以在arXiv(预印本)或IEEE Xplore(发表论文)找到。代码也已发布在Github上。
我很高兴欢迎你来到我的 Facebook 页面,分享有关机器学习的内容:深入机器学习。你还可以在这里找到我发布的其他重要内容:
-
EDN-GTM
-
MetaFormer
-
Darkeras
-
EFPN: 扩展特征金字塔网络
-
数据增强
-
数据蒸馏
-
以及其他内容在我的页面上。
感谢你的时间!
2022 年 EMNLP 上的诗歌、花卉和龙
·
关注 发表在 Towards Data Science ·8 min 阅读·2023 年 1 月 2 日
--
“诗歌、花卉、地牢与龙的结合,数字艺术 — ar 3:2 — v 4”,Midjourney
EMNLP 会议是自然语言处理领域一个备受推崇的活动,研究人员汇聚一堂,分享和讨论该领域的最新发现。今年的会议于 12 月 7 日至 12 月 11 日在阿布扎比举行。在会议上展示的众多论文中,我想特别提到三篇给我留下深刻印象的论文。这些论文可能不是最实用或最知名的,但我认为它们值得一提。两篇论文以海报的形式展示,第三篇是完整的演讲。我最喜欢的三篇论文中的一篇是 PoeLM。
PoeLM: 一种用于无监督诗歌生成的节奏和韵律可控语言模型
-
组织: 巴斯克大学、Meta AI、哥本哈根大学
-
代码:
github.com/aitorormazabal/poetry_generation,不过这里只有数据集创建。 -
主要思想: 通过控制代码生成西班牙语和巴斯克语正式诗歌,使用在非诗歌文本上训练的语言模型。
动机
现代语言模型能写诗吗?当然能。你可以快速用ChatGPT测试一下。挑战在于强加特定的约束,比如固定的音节数或特定的韵律或节奏方案。
我们如何强迫语言模型生成正式的诗歌?一种方法是修改解码算法,这在现代语言模型中很复杂,因为它们处理的是子词,既不是词也不是音节。本文描述了另一种方法。要使其有效,你需要一个常规的文本语料库和一个能够分析音节和韵律的系统。
训练语言模型

来自论文的图,提出的方法。
你需要做的步骤是:
-
获取一个常规的非诗歌语料库,并将其拆分成短语。
-
将文本分成 N 个短语块,其中 N 是随机采样的。
-
使用结构描述符(=前缀)增强组,以包含每个短语的音节数和韵尾。
-
用将结构描述符视为普通标记的经典变换器语言模型进行训练。

来自论文的图。正式诗歌及其相关的结构描述符。
上图中的结构描述符是
<PREF>
<LEN:11><END:echo>
<LEN:11><END:ura>
<LEN:11><END:ura>
<LEN:11><END:echo>
</PREF>
这个描述符意味着四行;每行有 11 个音节;首尾两行以“echo”结尾,第 2 和第 3 行以“ura”结尾。模型将学习如何使用这些代码,因为使用这些提示生成文本比没有提示要容易。
生成
-
选择韵律方案和音节数。
-
生成一个结构描述符。作者通过从训练语料库中五种最常见的韵律声音中独立采样每种韵律声音来完成这个任务。
-
提供诗歌的第一行(可选)
-
使用训练过的语言模型生成大量诗歌候选。
-
过滤掉所有不符合韵律方案或包含错误音节数的候选。
-
使用训练过的语言模型(没有结构描述符)按一般流畅度重新排序剩余的候选,并输出得分最高的那个。
它的效果如何?

来自论文的表格。系统 S1 在人工评估中排名高于 S2 的次数百分比。
第 5 步的过滤率对于西班牙语诗歌为 30.9%,对于巴斯克语诗歌为 23.4%。37.3%的人更喜欢自动生成的诗歌而非知名诗人的诗歌,比较的是第一行相同的诗歌。
你能在你的语言中做到这一点吗?
使用描述的算法需要可靠的音节划分和押韵检测过程。虽然某些语言可能已有这样的程序,但其他语言可能具有更复杂的特征,如节奏,需要考虑。在这些情况下,可以修改结构描述符以包括额外的组件。
为什么这对我很重要?
六年前,丹尼尔·阿纳斯捷耶夫和我开发了一个俄罗斯诗歌生成系统,rupo。这是一个基于 LSTM 的语言模型,具有一些独特的特征:它从右到左预测文本,分别使用单词的标准形式及其语法特征,并且基于有限状态接受器。自那时以来,自然语言处理技术取得了显著进展,使得今天创建类似系统可能更加容易。
画一朵花:自然语言中的处理和基础抽象
-
论文: Lachmy 等人, 2022
-
机构: 巴伊兰大学, AI2
-
代码:
github.com/OnlpLab/Hexagons,但目前还没有基准,只有数据集本身。 -
主要思想: 创建一个基于六边形网格的指令式模式绘制的自然语言基础抽象基准。

论文中的图,展示自然语言中的抽象层次
动机
我们知道大型语言模型无法正确计算或执行简便的估算。即使是简单的空间推理任务也是个问题(不过思维链有所帮助)。但是抽象呢?当你命令你假设的 AI 助手,“订三个披萨,一个 BBQ,一个意大利辣香肠,一个玛格丽塔,前两个大,最后一个中,下午 5 点”,它应该能够理解你。这不仅仅涉及省略号,还有条件、迭代、功能分解、递归和其他机制。
为了衡量模型 grasp 抽象概念的程度,我们可以将其 grounding 在各种 虚拟 世界中。在这种情况下,作者使用了一个 10x18 瓷砖和八种颜色的六边形棋盘作为 grounding 抽象的基础。
数据集
本研究的数据集是通过众包方式收集的。虽然作者提供了起始图像,但众包工作者也通过绘制额外的模式进行了贡献。注释过程分为两个阶段:第一阶段,一组注释人员根据图像编写说明;第二阶段,另一组人员根据说明尝试重建图像。任何不一致或分歧都通过人工检查解决。最终数据集包含 175 张独特的图像、620 组说明和 4177 个说明步骤。

论文中的图,画廊样本。
实验
测试了两种模型:分类模型和生成模型。分类模型使用了 DeBERTa 来预测每个瓦片的状态。生成模型则使用了 T5 来生成一系列动作。模型在各种设置下进行了测试,这些设置在历史记录和当前棋盘信息的量上有所不同:无历史记录、一个前一步、完整历史记录、预测棋盘和神谕棋盘。结果表明,这些模型的表现明显低于人类,甚至在拥有神谕棋盘和完整历史记录的情况下,也只能处理最基本的抽象。

论文中的表格。两种模型在测试集上的结果,基于动作的指标。

论文中的表格。数据集评估,人类表现。
为什么这很重要?
这是对自然语言模型面临的挑战的一个很好的视觉展示。这个基准使得可以迅速识别这些模型中缺乏哪些抽象机制。我怀疑基于代码的模型会在这个任务中表现更好,并且我对测试这一假设很感兴趣。
龙与地下城作为人工智能对话挑战
-
机构:宾夕法尼亚大学,谷歌研究
-
代码:尚未发布,应该在这里
-
主要思想:基于 D&D 对话创建一个对话系统挑战,任务是在游戏中生成下一个对话回合,并预测游戏的状态,给定对话历史。

“robots playing D&D, digital art, futuristic — ar 3:2 — v 4”,Midjourney
动机
龙与地下城(Dungeons & Dragons)是一款奇幻桌面角色扮演游戏。角色们在奇幻的环境中展开冒险。地下城主作为游戏的裁判和讲述者,同时维护冒险发生的环境,并扮演游戏世界的居民,也称为非玩家角色(NPCs)。角色们组成一个小队,与环境中的居民和彼此互动。他们一起解决难题,参与战斗,探索并收集宝藏和知识。在这个过程中,角色们获得经验值,逐渐升级并变得越来越强大,经过一系列的游戏会话。— 维基百科
许多自然语言处理数据集都高度专业化,专注于特定任务。龙与地下城(D&D)是一项需要所有参与者高度语言理解的人类活动。它涉及一系列技能,如文本生成、知识库查找、多方对话、目标设定、常识推理、意图检测、状态跟踪和问题回答,使其成为评估 NLP 模型能力的理想测试*台。
AI 在 D&D 中的其他应用包括 角色照片生成 和当然还有著名的 AI Dungeon。
数据集

论文中的图示。D&D Beyond 论坛中 3 回合的示例。
作者从 D&D Beyond 论坛上抓取了 Play-By-Post 数据,在这个论坛上,人们通过轮流在论坛上发帖来描述他们的行动。这不是 D&D 会话的唯一可能来源。例如,CRD3 数据集使用了 Critical Role 节目的转录稿。

论文中的表格,数据集统计信息。
使用基于规则的启发式方法通过正则表达式和命名实体识别(NER)从文本中提取游戏状态信息。此外,在启发式方法无法提取信息的情况下,还使用了用于文本的 CNN 分类器。数据集不仅包括角色内文本,还包括角色外帖子。
实验
LaMDA,谷歌类似于 GPT-3 的大型语言模型,用于处理两个任务:游戏状态跟踪和回应生成。作者尝试了模型的各种微调变体,包括使用当前或前几个回合的状态作为控制特征。为了评估模型的表现,招募了六名对幻想题材感兴趣并具有 D&D 经验的专业评估员,其中包括三名曾担任地下城主的人员,进行手动评估。

论文中的表格。系统和人工编写的金牌回应的*均人类评估者评分。
评估结果显示领域适应是有益的,但控制特征的影响可能更清晰。然而,这些特征使得模型能够在游戏中担任特定角色,这可能使其成为实际 D&D 游戏中地下城主或玩家的有价值替代品。

论文中的表格。GST 的*均准确率与多数类基线相比。
游戏状态跟踪任务的结果本可以更好。模型接收了所有以前对话回合及其对应的状态变量,以及当前回合的文本,并期望输出当前回合的正确状态变量。模型的联合准确率为 58%。这些结果表明,仅使用大型语言模型不足以完成此任务,可能需要进一步修改以提高性能。
结论
总结上述研究和发现突出了持续存在的挑战和改进领域。必须考虑非主流论文的价值,因为它们可能提供独特的见解和方法,这些可能在急于跟上更广为人知的作品时被忽视。
Python 依赖管理:你应该选择哪个工具?
原文:
towardsdatascience.com/poetry-a-better-way-to-manage-python-dependencies-bd7b5f1eab25
Poetry、Pip 和 Conda 的深入比较
·发表于 Towards Data Science ·10 分钟阅读·2023 年 6 月 13 日
--

作者提供的图片
最初发表于 https://mathdatasimplified.com 于 2023 年 6 月 13 日。
动机
随着数据科学项目的扩展,依赖项的数量也会增加。为了保持项目环境的可重现性和可维护性,使用高效的依赖管理工具非常重要。
因此,我决定比较三种流行的依赖管理工具:Pip、Conda 和 Poetry。经过仔细评估,我相信 Poetry 在有效性和性能方面超越了其他两个选项。
在本文中,我们将深入探讨 Poetry 的优势,并突出其与 Pip 和 Conda 的主要区别。
可用的包
拥有广泛的包选择使开发人员更容易找到最适合其需求的特定包和版本。
Conda
一些包,例如 “snscrape”,无法通过 conda 安装。此外,某些版本,例如 Pandas 2.0,可能无法通过 Conda 安装。
虽然你可以在 conda 虚拟环境中使用 pip 解决包的限制,但 conda 无法跟踪用 pip 安装的依赖项,这使得依赖管理变得具有挑战性。
$ conda list
# packages in environment at /Users/khuyentran/miniconda3/envs/test-conda:
#
# Name Version Build Channel$ conda list # packages in environment at /Users/khuyentran/miniconda3/envs/test-conda: # # Name Version Build Channel
Pip
Pip 可以从 Python 包索引(PyPI)和其他仓库中安装任何包。
Poetry
Poetry 还允许从 Python 包索引(PyPI)和其他仓库中安装包。
依赖项数量
减少环境中的依赖项数量简化了开发过程。
Conda
Conda 提供了完整的环境隔离,管理 Python 包和系统级别的依赖项。这可能导致与其他包管理器相比,包的大小更大,在安装和分发过程中可能会消耗更多的存储空间。
$ conda install pandas
$ conda list
# packages in environment at /Users/khuyentran/miniconda3/envs/test-conda:
#
# Name Version Build Channel
blas 1.0 openblas
bottleneck 1.3.5 py311ha0d4635_0
bzip2 1.0.8 h620ffc9_4
ca-certificates 2023.05.30 hca03da5_0
libcxx 14.0.6 h848a8c0_0
libffi 3.4.4 hca03da5_0
libgfortran 5.0.0 11_3_0_hca03da5_28
libgfortran5 11.3.0 h009349e_28
libopenblas 0.3.21 h269037a_0
llvm-openmp 14.0.6 hc6e5704_0
ncurses 6.4 h313beb8_0
numexpr 2.8.4 py311h6dc990b_1
numpy 1.24.3 py311hb57d4eb_0
numpy-base 1.24.3 py311h1d85a46_0
openssl 3.0.8 h1a28f6b_0
pandas 1.5.3 py311h6956b77_0
pip 23.0.1 py311hca03da5_0
python 3.11.3 hb885b13_1
python-dateutil 2.8.2 pyhd3eb1b0_0
pytz 2022.7 py311hca03da5_0
readline 8.2 h1a28f6b_0
setuptools 67.8.0 py311hca03da5_0
six 1.16.0 pyhd3eb1b0_1
sqlite 3.41.2 h80987f9_0
tk 8.6.12 hb8d0fd4_0
tzdata 2023c h04d1e81_0
wheel 0.38.4 py311hca03da5_0
xz 5.4.2 h80987f9_0
zlib 1.2.13 h5a0b063_0
Pip
Pip 只安装包所需的依赖项。
$ pip install pandas
$ pip list
Package Version
--------------- -------
numpy 1.24.3
pandas 2.0.2
pip 22.3.1
python-dateutil 2.8.2
pytz 2023.3
setuptools 65.5.0
six 1.16.0
tzdata 2023.3
Poetry
Poetry 也只安装包所需的依赖项。
$ poetry add pandas
$ poetry show
numpy 1.24.3 Fundamental package for array computing in Python
pandas 2.0.2 Powerful data structures for data analysis, time...
python-dateutil 2.8.2 Extensions to the standard Python datetime module
pytz 2023.3 World timezone definitions, modern and historical
six 1.16.0 Python 2 and 3 compatibility utilities
tzdata 2023.3 Provider of IANA time zone data
卸载包
卸载包及其依赖项可以释放磁盘空间,防止不必要的杂乱,并优化存储资源的使用。
Pip
Pip 仅移除指定的包,而不是它的依赖项,这可能导致随着时间的推移积累未使用的依赖项。这可能导致存储空间使用增加以及潜在的冲突。
$ pip install pandas
$ pip uninstall pandas
$ pip list
Package Version
--------------- -------
numpy 1.24.3
pip 22.0.4
python-dateutil 2.8.2
pytz 2023.3
setuptools 56.0.0
six 1.16.0
tzdata 2023.3
Conda
Conda 移除包及其依赖项。
$ conda install -c conda pandas
$ conda uninstall -c conda pandas
Collecting package metadata (repodata.json): done
Solving environment: done
## Package Plan ##
environment location: /Users/khuyentran/miniconda3/envs/test-conda
removed specs:
- pandas
The following packages will be REMOVED:
blas-1.0-openblas
bottleneck-1.3.5-py311ha0d4635_0
libcxx-14.0.6-h848a8c0_0
libgfortran-5.0.0-11_3_0_hca03da5_28
libgfortran5-11.3.0-h009349e_28
libopenblas-0.3.21-h269037a_0
llvm-openmp-14.0.6-hc6e5704_0
numexpr-2.8.4-py311h6dc990b_1
numpy-1.24.3-py311hb57d4eb_0
numpy-base-1.24.3-py311h1d85a46_0
pandas-1.5.3-py311h6956b77_0
python-dateutil-2.8.2-pyhd3eb1b0_0
pytz-2022.7-py311hca03da5_0
six-1.16.0-pyhd3eb1b0_1
Proceed ([y]/n)?
Preparing transaction: done
Verifying transaction: done
Executing transaction: donePoetry
Poetry
Poetry 也会移除包及其依赖项。
$ poetry add pandas
$ poetry remove pandas
• Removing numpy (1.24.3)
• Removing pandas (2.0.2)
• Removing python-dateutil (2.8.2)
• Removing pytz (2023.3)
• Removing six (1.16.0)
• Removing tzdata (2023.3)
依赖文件
依赖文件通过指定所需包的确切版本或版本范围,确保软件项目环境的可重现性。
这有助于在不同系统或不同时间点重新创建相同的环境,确保开发者之间使用相同的依赖项进行协作。
Conda
要在 Conda 环境中保存依赖项,你需要手动将它们写入文件。environment.yml 文件中指定的版本范围可能会导致安装不同的版本,可能在重现环境时引入兼容性问题。
假设我们已经安装了 pandas 版本 1.5.3 作为示例。这里是一个示例 environment.yml 文件,指定了依赖项:
# environment.yml
name: test-conda
channels:
- defaults
dependencies:
- python=3.8
- pandas>=1.5
如果新用户尝试在 pandas 的最新版本为 2.0 时重现环境,则会安装 pandas 2.0。
# Create and activate a virtual environment
$ conda env create -n env
$ conda activate env
# List packages in the current environment
$ conda list
...
pandas 2.0
如果代码库依赖于 pandas 版本 1.5.3 特有的语法或行为,而在版本 2.0 中语法发生了变化,那么使用 pandas 2.0 运行代码可能会引入错误。
Pip
相同的问题也可能发生在 pip 上。
# requirements.txt
pandas>=1.5
# Create and activate a virtual environment
$ python3 -m venv venv
$ source venv/bin/activate
# Install dependencies
$ pip install -r requirements.txt
# List packages
$ pip list
Package Version
---------- -------
pandas 2.0
...
你可以通过在 requirements.txt 文件中冻结版本来固定版本:
$ pip freeze > requirements.txt
# requirements.txt
numpy==1.24.3
pandas==1.5.3
python-dateutil==2.8.2
pytz==2023.3
six==1.16.0
然而,这使得代码环境的灵活性降低,并且在长期维护中可能变得更加困难。任何对依赖项的更改都需要手动修改 requirements.txt 文件,这可能既耗时又容易出错。
Poetry
Poetry 在安装包时会自动更新 pyproject.toml 文件。
在以下示例中,“pandas” 包被添加了版本约束 ¹.5。这种灵活的版本管理方法确保了你的项目可以适应更新的版本而无需手动调整。
$ poetry add 'pandas=¹.5'
# pyproject.toml
[tool.poetry.dependencies]
python = "³.8"
pandas = "¹.5"
poetry.lock 文件存储了每个包及其依赖项的精确版本号。
# poetry.lock
...
[[package]]
name = "pandas"
version = "1.5.3"
description = "Powerful data structures for data analysis, time series, and statistics"
category = "main"
optional = false
python-versions = ">=3.8"
[package.dependencies]
numpy = [
{version = ">=1.20.3", markers = "python_version < \"3.10\""},
{version = ">=1.21.0", markers = "python_version >= \"3.10\""},
{version = ">=1.23.2", markers = "python_version >= \"3.11\""},
]
python-dateutil = ">=2.8.2"
pytz = ">=2020.1"
tzdata = ">=2022.1"
...
这确保了安装的包的一致性,即使包的 pyproject.toml 文件中指定了版本范围。在这里,我们可以看到安装了 pandas 1.5.3 而不是 pandas 2.0
$ poetry install
$ poetry show pandas
name : pandas
version : 1.5.3
description : Powerful data structures for data analysis, time series, and statistics
dependencies
- numpy >=1.20.3
- numpy >=1.21.0
- numpy >=1.23.2
- python-dateutil >=2.8.1
- pytz >=2020.1
开发和生产的独立依赖项
通过分离依赖项,你可以清楚地区分开发用途所需的包(例如测试框架和代码质量工具)与生产环境所需的包(通常包括核心依赖项)。
Conda
Conda 本身不支持为不同环境分别设置依赖项,但可以通过创建两个环境文件来解决这个问题:一个用于开发环境,另一个用于生产环境。开发文件包含生产和开发依赖项。
# environment.yml
name: test-conda
channels:
- defaults
dependencies:
# Production packages
- numpy
- pandas
# environment-dev.yml
name: test-conda-dev
channels:
- defaults
dependencies:
# Production packages
- numpy
- pandas
# Development packages
- pytest
- pre-commit
Pip
Pip 也不直接支持分开的依赖项,但可以使用类似的方法通过分开的需求文件来实现。
# requirements.txt
numpy
pandas
# requirements-dev.txt
-r requirements.txt
pytest
pre-commit
# Install prod
$ pip install -r requirements.txt
# Install both dev and prod
$ pip install -r requirements-dev.txt
Poetry
Poetry 通过支持一个文件中的分组来简化依赖项管理。这使你可以在一个地方跟踪所有依赖项。
$ poetry add numpy pandas
$ poetry add --group dev pytest pre-commit
# pyproject.toml
[tool.poetry.dependencies]
python = "³.8"
pandas = "².0"
numpy = "¹.24.3"
[tool.poetry.group.dev.dependencies]
pytest = "⁷.3.2"
pre-commit = "³.3.2"
要仅安装生产依赖项:
$ poetry install --only main
要安装开发和生产依赖项:
$ poetry install
更新环境
更新依赖项对于从 bug 修复、性能改进和新包版本中引入的新功能中受益是至关重要的。
Conda
Conda 允许你只更新指定的包。
$ conda install -c conda pandas
$ conda install -c anaconda scikit-learn
# New versions available
$ conda update pandas
$ conda update scikit-learn
之后,你需要手动更新 environment.yaml 文件,以保持与更新的依赖项同步。
$ conda env export > environment.yml
Pip
Pip 也只允许你更新指定的包,并要求你手动更新 requirements.txt 文件。
$ pip install -U pandas
$ pip freeze > requirements.txt
Poetry
使用 Poetry,你可以使用 update 命令来升级 pyproject.toml 文件中指定的所有包。此操作会自动更新 poetry.lock 文件,确保包规范和锁定文件之间的一致性。
$ poetry add pandas scikit-learn
# New verisons available
poetry update
Updating dependencies
Resolving dependencies... (0.3s)
Writing lock file
Package operations: 0 installs, 2 updates, 0 removals
• Updating pandas (2.0.0 -> 2.0.2)
• Updating scikit-learn (1.2.0 -> 1.2.2)
依赖解析
依赖冲突发生在项目所需的包或库具有冲突的版本或不兼容的依赖项时。正确解决冲突对于避免错误、运行时问题或项目失败至关重要。
Pip
pip 顺序地安装包,这意味着它会按照指定的顺序逐个安装每个包。这种顺序方式有时会导致当包具有不兼容的依赖项或版本要求时发生冲突。
例如,假设你首先安装 pandas==2.0.2,它需要 numpy>=1.20.3。后来,你使用 pip 安装 numpy==1.20.2。尽管这会导致依赖冲突,但 pip 会继续更新 numpy 的版本。
$ pip install pandas==2.0.2
$ pip install numpy==1.22.2
Collecting numpy=1.20.2
Attempting uninstall: numpy
Found existing installation: numpy 1.24.3
Uninstalling numpy-1.24.3:
Successfully uninstalled numpy-1.24.3
ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
pandas 2.0.2 requires numpy>=1.20.3; python_version < "3.10", but you have numpy 1.20.2 which is incompatible.
Successfully installed numpy-1.20.2
Conda
Conda 使用 SAT 求解器来探索所有包版本和依赖项的组合,以找到兼容的集合。
例如,如果一个现有包对其依赖项有特定的约束(例如,statsmodels==0.13.2 需要 numpy>=1.21.2,<2.0a0),而你要安装的包不符合该要求(例如,numpy<1.21.2),conda 不会立即引发错误。相反,它会仔细搜索所有所需包及其依赖项的兼容版本,只有在找不到合适的解决方案时才报告错误。
$ conda install 'statsmodels==0.13.2'
$ conda search 'statsmodels==0.13.2' --info
dependencies:
- numpy >=1.21.2,<2.0a0
- packaging >=21.3
- pandas >=1.0
- patsy >=0.5.2
- python >=3.9,<3.10.0a0
- scipy >=1.3
$ conda install 'numpy<1.21.2'
...
Package ca-certificates conflicts for:
python=3.8 -> openssl[version='>=1.1.1t,<1.1.2a'] -> ca-certificates
openssl -> ca-certificates
ca-certificates
cryptography -> openssl[version='>1.1.0,<3.1.0'] -> ca-certificates
Package idna conflicts for:
requests -> urllib3[version='>=1.21.1,<1.27'] -> idna[version='>=2.0.0']
requests -> idna[version='>=2.5,<3|>=2.5,<4']
idna
pooch -> requests -> idna[version='>=2.5,<3|>=2.5,<4']
urllib3 -> idna[version='>=2.0.0']
Package numexpr conflicts for:
statsmodels==0.13.2 -> pandas[version='>=1.0'] -> numexpr[version='>=2.7.0|>=2.7.1|>=2.7.3']
numexpr
pandas==1.5.3 -> numexpr[version='>=2.7.3']
Package patsy conflicts for:
statsmodels==0.13.2 -> patsy[version='>=0.5.2']
patsy
Package chardet conflicts for:
requests -> chardet[version='>=3.0.2,<4|>=3.0.2,<5']
pooch -> requests -> chardet[version='>=3.0.2,<4|>=3.0.2,<5']
Package python-dateutil conflicts for:
statsmodels==0.13.2 -> pandas[version='>=1.0'] -> python-dateutil[version='>=2.7.3|>=2.8.1']
python-dateutil
pandas==1.5.3 -> python-dateutil[version='>=2.8.1']
Package setuptools conflicts for:
numexpr -> setuptools
pip -> setuptools
wheel -> setuptools
setuptools
python=3.8 -> pip -> setuptools
pandas==1.5.3 -> numexpr[version='>=2.7.3'] -> setuptools
Package brotlipy conflicts for:
urllib3 -> brotlipy[version='>=0.6.0']
brotlipy
requests -> urllib3[version='>=1.21.1,<1.27'] -> brotlipy[version='>=0.6.0']
Package pytz conflicts for:
pytz
pandas==1.5.3 -> pytz[version='>=2020.1']
statsmodels==0.13.2 -> pandas[version='>=1.0'] -> pytz[version='>=2017.3|>=2020.1']
尽管这种方法提高了找到解决方案的机会,但在处理广泛环境时可能会计算密集。
诗歌
通过专注于项目的直接依赖,Poetry 的确定性解析器缩小了搜索范围,使得解析过程更为高效。它评估指定的约束条件,如版本范围或特定版本,并立即识别任何冲突。
$ poetry add 'seaborn==0.12.2'
$ poetry add 'matplotlib<3.1'
Because poetry shell depends on seaborn (0.12.2) which depends on matplotlib (>=3.1,<3.6.1 || >3.6.1), matplotlib is required.
So, because poetry shell depends on matplotlib (<3.1), version solving failed.
这种即时反馈有助于防止潜在问题升级,并允许开发者在开发过程早期解决问题。例如,在以下代码中,我们可以放宽对 seaborn 的要求,以便安装特定版本的 matplotlib:
poetry add 'seaborn<=0.12.2' 'matplotlib<3.1'
Package operations: 1 install, 2 updates, 4 removals
• Removing contourpy (1.0.7)
• Removing fonttools (4.40.0)
• Removing packaging (23.1)
• Removing pillow (9.5.0)
• Updating matplotlib (3.7.1 -> 3.0.3)
• Installing scipy (1.9.3)
• Updating seaborn (0.12.2 -> 0.11.2)
结论
总结而言,Poetry 相比 pip 和 conda 提供了几个优势:
-
广泛的软件包选择: Poetry 提供对 PyPI 上广泛软件包的访问,允许你利用多样的生态系统来支持你的项目。
-
高效的依赖管理: Poetry 只安装指定软件包所需的必要依赖,减少了环境中多余的软件包数量。
-
简化的软件包移除: Poetry 简化了软件包及其相关依赖的移除,使得维护一个干净高效的项目环境变得更加容易。
-
依赖解决: Poetry 的确定性解析器高效地解决依赖关系,及时识别和处理任何不一致或冲突。
虽然 Poetry 可能需要一些额外的时间和精力来让你的团队成员学习和适应,但长期来看,使用像 Poetry 这样的工具可以节省时间和精力。
我喜欢写关于数据科学概念的文章,并玩弄各种数据科学工具。你可以通过以下方式跟进我的最新帖子:
-
订阅我在Data Science Simplified上的新闻通讯。
R 中的泊松回归
原文:
towardsdatascience.com/poisson-regression-in-r-957752266a34
R 统计系列
·发布于 Towards Data Science ·6 分钟阅读·2023 年 2 月 10 日
--

图片由 Michael Dziedzic 提供,来源于 Unsplash
介绍
回归分析是一个广泛的领域。我们可以根据数据类型进行多种回归分析。我们在之前的文章中详细介绍了逻辑回归。在本文中,我将介绍泊松回归,并用 R 实现一个示例。
简要背景
线性回归可以用于数值数据,而逻辑回归则用于分类数据。我们可以对二分变量执行简单的二元逻辑回归,也可以进行多重逻辑回归。根据需求,我们可以选择部分比例赔率模型或广义回归模型。但在很多情况下,我们需要处理计数数据。例如,博物馆的访客数量可以通过调查收集,为了对这个计数响应变量建模,我们需要使用泊松回归。其他类型的例子包括医院的就诊次数或学生在特定时间内参加的数学课程数量。
泊松分布
一个计数响应变量的泊松分布表示为:

在这里,x = 计数变量,λ = 事件的*均数。在泊松分布中,事件的*均数等于该变量的方差。因此,λ = 方差(x)
数据集
作为此案例研究的数据来源,我们将使用UCI 机器学习库中的成人数据集。根据数据集,约 30000 人应根据他们的种族、教育、职业、性别、薪水、每周工作小时数以及每月收入等人口统计特征进行识别。

修改后的成人数据集
我们将使用以下变量来建模“vissci”变量,该变量表示在过去一年中访问科学或技术博物馆的次数
-
教育:数值型和连续型
-
婚姻状况:二元变量(0 表示未婚,1 表示已婚)
-
性别:二元变量(0 表示女性,1 表示男性)
-
家庭收入:二元变量(0 表示*均水*或低于*均水*,1 表示高于*均水*)
-
全职工作:二元变量(0 表示兼职,1 表示全职工作)
R 中的实现
实施过程与广义回归模型非常相似。在这里,我们将使用带有泊松家族的 glm()命令。在上面的代码片段中,我们定义了两个模型
model1:一个单预测变量模型。在这里,我们希望使用教育年限这一单一预测变量来建模 vissci。
model2:一个多预测变量模型。在这里,我们希望使用所有预测变量来建模 vissci。
结果的解释
模型 1 的摘要如下所示。

模型 1 摘要
它提供了类似的偏差残差统计数据,这些数据与线性回归模型非常相似,其中偏差是通过测量线性拟合线的偏差来计算的。回归模型的系数如下所示。由于这里只有教育变量,因此这里只显示一个系数。系数估计值为 0.13486,这意味着教育每增加一个单位,预期的科学博物馆访问次数的对数增加了 0.13486 倍。
还有一个术语叫做事件率比(IRR),它是用于测量独立变量每增加一个单位的发生率。这个 IRR 值可以通过系数的指数值获得。

模型 1 参数的 IRR
在这里,educ 的 IRR 值为 1.14437,这意味着教育变量每增加一个单位,预期的科学博物馆访问次数增加 14.437%。相关的 p 值为<0.05,这表明 educ 是一个重要的参数,可以用来预测科学博物馆的访问次数。
可以对多个独立变量进行类似的研究。接下来,我们希望包括剩余的变量来建模预期的科学博物馆访问次数,并确定它们是否有显著影响。模型 2 的摘要如下所示。

模型 2 摘要
乍一看,我们可以看到婚姻和全职工作状态不是确定科学博物馆访问次数的显著变量。对于性别,我们将 0 视为女性,将 1 视为男性。系数为 0.33612,这意味着性别增加一个单位(即女性到男性)时,预期到科学博物馆的访问次数的对数增加了 0.13486。对于家庭收入,估计值为 0.57499。

模型 2 参数的 IRR
观察 IRR 值,我们可以说性别在确定我们的因变量中起着重要作用。在性别变量增加一个单位(即女性到男性)的情况下,预期到科学博物馆的访问次数增加了 39.951%。这意味着男性比女性更可能访问科学博物馆。家庭收入的重要性更大。家庭收入变量增加一个单位(即低于*均水*到高于*均水*)的情况下,预期到科学博物馆的访问次数增加了 77.711%。这也意味着高收入家庭访问科学博物馆的次数多于低于*均收入的家庭。此外,如前所述,婚姻状况和全职工作状态在这里并不显著。
主要发现
教育年限、性别和家庭收入状态对于确定预期到科学博物馆的访问次数至关重要。
-
教育年限增加一个单位时,预期到科学博物馆的访问次数增加了 14.437%。
-
性别变量增加一个单位(即女性到男性)时,预期到科学博物馆的访问次数增加 39.951%。男性比女性更频繁地访问科学博物馆。
-
如果家庭收入增加到高于*均水*,预期到科学博物馆的访问次数增加 77.711%。
-
婚姻和工作状态在确定预期到科学博物馆的访问次数方面并不显著。
结论
我们已经涵盖了泊松分布的基本概念,并在 R 中实现了泊松回归模型。我们从 UCI 数据库中获取了一个数据集,并明确指出了一些预测变量对我们期望的因变量的影响。这种研究对于理解我们社会中的细微歧视是重要的。泊松回归在多个工程研究中也至关重要。
数据集的感谢
[## 通过我的推荐链接加入 Medium - Md Sohel Mahmood]
阅读 Md Sohel Mahmood 的每一个故事(以及 Medium 上成千上万的其他作者的故事)。您的会员费直接…
mdsohel-mahmood.medium.com [## 每当 Md Sohel Mahmood 发布文章时,您将收到电子邮件。
每当 Md Sohel Mahmood 发布文章时,您将收到电子邮件。通过注册,如果您尚未创建 Medium 账户,将会自动创建一个…
将你的分析团队定位到正确的项目上
“好的足球运动员会跟随足球——最好的足球运动员已经在球落地时就在那里”
·
关注 发表在 Towards Data Science · 5 分钟阅读 · 2023 年 12 月 23 日
--
你将要参与的项目对你团队的成功(以及你自己的成功)有着很大的影响。在某种程度上,这也是你每月/每季度/每天(取决于你的优先级排序流程)必须做出的最重要的决定之一。
然而——我从未真正遇到过适合我需求的分析世界的思维模型,用于帮助决定该参与哪些项目。因此,这就是我们将在本文中尝试的练习——制定一个思维模型来帮助做出更好的优先级决策。

项目优先级排序表(图片来源于作者)
任何项目都有其相关的风险。
正如我们在上一篇文章中讨论的那样,有多个宏观元素可以影响你的研究:数据可用性、技能水*、时间框架、组织准备情况和政治环境。
这些因素中的每一个都可能在你的研究中产生一些风险。如果你没有正确的数据,你很可能无法得到正确的答案。如果你没有合适的技能,或者你当前的技能与需要掌握的技能之间的差距太大,或者你的组织没有准备好实施任何结果,或者可能会有一些政治障碍阻碍实施——你的项目失败的风险就会更高。
尽管有许多不同和复杂的因素在起作用,但在你这边,你可以用简单的方法对潜在项目进行评分——使用 1-5 分评分系统(或 0-100%),基于你克服上述任何障碍的能力的信心。
影响有不同的规模和形式。
理解项目的影响力需要你正确理解你的分析项目将在实际中如何使用。在某种程度上,这是一种很好的强制因素,用来正确理解“需要完成的工作”以及你的工作如何被操作化。
在进行此项工作时,有几个方面需要考虑:
-
重要的是要考虑“第二层次”的影响。有些数据项目将会有“自身”的影响。其他的则会使其他人能够产生更大的影响(例如:仪表板)。还有一些将解锁之前“锁定”的数据分析(例如:数据管道)。
-
“价值的深度”并不是唯一需要考虑的标准——“宽度”也很重要。例如,构建一个仪表板可能会产生少量的价值,但面向大量用户——总的来说,这实际上是很多价值。
-
影响需要根据工作的时效性和需求来考虑。如果你的行业正在快速发展,或者公司战略有早期变化的迹象——在你的影响计算中考虑这些因素是很重要的。
同样地,使用 5 分制评分系统可以是对不同项目进行排序并理解哪些项目可能产生最高价值的简单方法。
你的时间有限。
时间是另一个重要的强制因素——因为要准确计算完成研究所需的时间,你必须对项目的范围有清晰的理解。这不仅仅是关于数据项目本身,而是关于所有能够使项目成功的因素:
-
确保在项目开始前,每个人对目标和交付物达成一致。
-
实际执行项目。
-
使其易于理解。
-
将其传达给你的受众。
-
在项目结束后达成对结果和行动项的共识。
请注意,项目进行到一半时总会有一个意外的挑战。这就是任何数据项目的魅力,它就像《福瑞斯特·冈普》中的巧克力盒——你永远不知道你会得到什么。考虑到这些意外的“惊喜”是很重要的,最好预留稍多的时间。如果你对所需时间毫无头绪,以下是一些建议:
-
与过去类似的项目进行基准比较
-
使用 群体智慧 并询问其他同行他们认为这样的项目需要多少时间。
-
进行一个 “思想实验”(但是真正的实验,而不是仅仅思考 2 分钟)。花些时间想象整个过程以及你需要经历的所有步骤来完成项目。从你的视觉化练习中,评估一下从开始到完成所需的时间。
综合考虑
现在你已经有了一个清晰的了解:
-
每个项目所需的时间
-
这些项目将带来的影响
-
每个项目相关的风险
你可以将所有这些信息综合起来,定义你的“投资理论”。
基本上,就像风险投资基金投资于初创公司一样——你决定将比金钱更宝贵的东西投资到哪里:你的时间。你可以选择(在合理范围内)如何*衡团队的投资组合:你对什么风险水*感到舒适?你是想选择几个冒险项目还是众多简单/经过验证的项目?你是否想加倍投入那些已经在过去显示出成功影响的项目?你决定。
我的个人投资理论:我总是尽量每季度选择一两个低信心/高回报的项目,同时还有众多较小/更容易/有保证价值的项目。
简而言之
把自己当作数据项目领域的风险投资家。你做出的每一个选择都是一种投资——不是金钱的投资,而是更宝贵的东西:你的时间和精力。
你面临着所有这些潜在项目争夺你的注意力,每个项目都有其自身的风险和回报。就像风险投资家一样,你需要选择那些承诺能带来最佳回报的项目。这意味着有时需要选择那些低信心、高回报的长期项目,这些项目可能会真正带来收益。其他时候,则是积累那些较小的成功,确保稳定的价值和进展。
最终,你负责定义自己的投资组合——并且主动承担这一活动非常重要,因为如果你不这样做,别人会。
使用 Python 和 Linux 的后量子密码学
初学者指南
·
关注 发表在 Towards Data Science ·9 分钟阅读·2023 年 8 月 15 日
--
图片由 Jean-Louis Paulin 提供,来源于 Unsplash
如果我们相信爱德华·斯诺登,密码学是“对抗监视的唯一真正保护” [1]。然而,量子技术的进步可能会危及这一保障。我们的文章讨论了量子计算为什么对数据安全构成威胁以及如何应对。我们不是进行纯理论分析,而是通过使用 Python、C 和 Linux 的代码示例来展开讨论。
量子基础
当谷歌科学家在 2019 年报告首次实现量子supremacy时,引发了极大的兴奋。量子计算可能对加密产生重大影响。要理解这个问题,我们需要讨论一些基本概念。
与经典计算机不同,量子计算机的算法不依赖于位(bits),而是依赖于量子位(qubits)。一个位只能取状态 0 或 1。当我们多次测量一个位时,总是得到相同的结果。量子位则不同。尽管听起来很奇怪,但一个量子位可以同时取 0 和 1 的值。当我们重复测量时,只能得到 0 或 1 的某种概率。在量子位的初始状态下,测量为 0 的概率通常是百分之百。然而,通过叠加,不同的概率分布可以生成。这些原因源于量子力学,遵循与“正常”生活不同的规律。
量子计算机的主要优势在于其概率特性。经典计算机在我们需要可靠的单一结果时表现出色,而量子计算机则擅长处理概率和组合问题。当我们对处于叠加状态的量子位执行操作时,它同时作用于值 0 和 1。随着量子位数量的增加,量子计算机相对于经典计算机的优势也会增加。一个具有三量子位的量子计算机可以同时处理最多八个值(2³):即二进制数 000、001、010、011、100、101、110 和 111。
科学文献一致认为,量子计算机将有助于解决以前看似难以处理的问题。然而,目前没有理想的量子计算机。当前一代量子计算机被称为噪声中等规模量子(NISQ)。这种机器处理能力有限,对错误很敏感。现代设备提供最多几百个量子位。一个例子是 IBM 在 2022 年推出的 433 量子位Osprey芯片。现在,该公司计划到 2033 年开发一台具有 100,000 量子位的机器。
我们的文章解释了为什么这一进展对数据安全构成威胁。通过代码示例,我们展示了量子计算机如何破解某些加密方法,并讨论了应对策略。源代码可以在GitHub上找到。它是在 Kali Linux 2023.2 下使用 Python 3.10 的 Anaconda 开发的。
加密与素因数
在加密消息时,相对简单的方法是应用对称算法。这种方法使用相同的密钥进行明文的加密和密文的解密。这个方法的主要挑战是安全地交换发件人和收件人之间的密钥。一旦私钥被第三方知道,他们就有机会拦截并解密消息。
非对称密码学似乎是解决这个问题的方案。像RSA这样的算法使用不同的密钥进行加密和解密。在这里,加密是使用一个或多个公开密钥,收件人将其提供给所有人。解密时,收件人使用仅自己知道的私钥。这样,发件人可以在没有风险的情况下获得公开密钥,因为它本身并不保密。只有收件人的私钥必须被保护。但是,当潜在攻击者知道公开密钥时,如何加固这样的程序?为此,非对称算法依赖于像质因数分解这样的数学问题。
质因数分解通过示例最好地理解。在 Python 中,我们可以使用库SymPy的factorint函数来确定某个整数的质因数。
>>> import sympy
>>> sympy.factorint(10)
{2: 1, 5: 1}
>>> 2**1 * 5**1
10
>>> sympy.factorint(1000)
{2: 3, 5: 3}
>>> 2**3 * 5**3
1000
>>> sympy.factorint(55557)
{3: 2, 6173: 1}
>>> 3**2 * 6173**1
55557
>>>
上述控制台输出说明了每个自然数都可以表示为质数的乘积。这些被称为质因数。回想一下学校的日子,质数只能被 1 和自身整除。例如,数字 10 可以用术语 10=2¹ * 5¹表示。因此,10 的质因数是 2 和 5。类似地,数字 55557 可以用方程 55557=3² * 6173¹表示。所以,55557 的质因数是 3 和 6173。找到给定整数的质因数的过程称为质因数分解。
对于经典计算机,质因数分解对于小数字来说很简单,但对于大整数则变得越来越困难。每增加一个数字都会大幅增加可能组合的总和。超过某个点后,经典计算机几乎无法确定质因数。例如,考虑以下来自 RSA 因数分解挑战的数字(RSA-260),该挑战于 2007 年结束。在撰写时,它尚未被因数分解。
#!/usr/bin/env python
import sympy
rsa_260 = 22112825529529666435281085255026230927612089502470015394413748319128822941402001986512729726569746599085900330031400051170742204560859276357953757185954298838958709229238491006703034124620545784566413664540684214361293017694020846391065875914794251435144458199
print("Start factoring...")
factors = sympy.factorint(rsa_260)
# Will probably not be reached
print(factors)
像 RSA 这样的非对称算法利用质因数分解和类似问题的计算难度来确保加密。不幸的是,量子世界遵循自己的规律。
量子算法
关于密码学,有两个量子算法尤其值得关注。Shor 算法提供了一种高效的质因数分解方法。在大型量子设备上运行时,它理论上可以破解像 RSA 这样的非对称加密方法。从实际角度来看,这种情况仍在未来。一篇 2023 年的《自然》文章提到至少需要 1,000,000 个量子比特。撇开硬件不谈,找到能够在大型量子计算机上可靠扩展的算法实现也很困难。IBM 的框架Qiskit曾尝试实现这一功能,但在版本 0.22.0 时弃用了。不过,网上可以找到 Shor 算法的实验性实现。
Grover 算法对对称加密构成威胁。也称为量子搜索算法,它为对给定函数的输入进行无结构搜索提供了加速。量子计算机可以利用它加速对对称加密信息的暴力攻击。然而,与 Shor 算法不同的是,所提供的加速不是指数级的。简单来说,这意味着增加加密密钥的长度会使搜索变得极其昂贵。例如,对 128 位密钥进行暴力攻击需要最多 2¹²⁸ 次迭代。假设 Grover 的搜索将这个数字减少到 2⁶⁴,那么将密钥长度加倍到 256 位会再次增加到 2¹²⁸ 次迭代。这为可能的解决方案打开了大门。
对称加密解决方案
在某些条件下,对称加密是一种现成的、简单的方法来应对量子算法。原因在于 Grover 的搜索并不会指数级扩展,而 Shor 算法只威胁到非对称方法。根据当前的知识,高度复杂的对称算法可以被视为量子抗性。现在,美国国家标准与技术研究所(NIST)以及德国联邦信息安全局(BSI)都将AES-256纳入这一类别[2][3]。AES 是高级加密标准(Advanced Encryption Standard)的缩写,而数字 256 代表密钥的位长。在 Linux 下,AES-256 由 GNU 隐私保护工具 (GnuPG) 实现。下面的 shell 脚本展示了如何使用 AES-256 对文件进行加密和解密。
# Encrypt
gpg --output encrypted.gpg --symmetric --cipher-algo AES256 plain.txt
# Decrypt
gpg --output decrypted.txt --decrypt encrypted.gpg
上述脚本加密了文件“plain.txt”的内容,将密文写入“encrypted.gpg”文档,再次解密它,最后将输出保存到文件“decrypted.txt”。在加密之前,GnuPG 会要求输入密码短语以生成私钥。出于安全原因,选择一个强密码短语并保密至关重要。GnuPG 可能会缓存密码短语,并在解密时不再询问。要清除缓存,可以执行以下 shell 命令。
gpg-connect-agent reloadagent /bye
将 GnuPG 集成到 Python 中使用 subprocess 模块相对简单。下面的代码片段展示了使用 AES-256 的加密原型实现。
#!/usr/bin/env python
import subprocess
import getpass
# Read passphrase
passphrase = getpass.getpass("Passphrase:")
passphrase2 = getpass.getpass("Passphrase:")
if passphrase != passphrase2:
raise ValueError("Passphrases not identical!")
# Perform encryption
print("Encrypting...")
args = [
"gpg",
"--batch",
"--passphrase-fd", "0",
"--output", "encrypted.gpg",
"--symmetric",
"--yes",
"--cipher-algo", "AES256",
"plain.txt",
]
result = subprocess.run(
args, input=passphrase.encode(),
capture_output=True)
if result.returncode != 0:
raise ValueError(result.stderr)
为了获取密码短语,上述脚本使用 getpass 模块。确认后,密码短语通过标准输入传递给 GnuPG。这由参数 passphrase-fd 0 指示。或者,密码短语可以作为字符串或通过文件通过命令行参数发送给 GnuPG。然而,由于这些参数对其他用户可见,因此这两种选项在原型中被拒绝了。另一种更安全的方式是使用 GPG-Agent。选择哪种选项取决于所需的安全级别。包括加密和解密在内的概念验证可以在 这里 找到。作为 GnuPG 的替代方案,还有其他 AES-256 实现。在这里选择一个可信的来源至关重要。
非对称解决方法
寻找非对称解决方案时,NIST 后量子密码学标准化 计划 是一个很好的起点。自 2016 年以来,它评估了多个抗量子算法的候选者。获胜者之一是 Kyber。该系统实现了一种所谓的安全密钥封装机制。与其他算法类似,Kyber 依赖于一个难以解决的问题来保护两个方之间的密钥交换。与素因数分解不同,它基于一个称为“带错误学习”的问题。Kyber 提供的保护级别取决于密钥长度。例如,Kyber-1024 旨在提供“与 AES-256 大致相当”的安全级别 [4]。
用 C 语言编写的 Kyber 参考实现可在 GitHub 上找到。在 Linux 下,我们可以通过执行以下 shell 命令来克隆和构建框架。安装需要一些先决条件,这些条件在项目的 README 中有记录。
git clone https://github.com/pq-crystals/kyber.git
cd kyber/ref && make
将参考实现集成到 Python 中有几种方法。其中一种是编写一个 C 程序并调用它。下面的 C 函数使用 Kyber 在两个虚构的实体 Alice 和 Bob 之间进行密钥交换。完整源代码请参见 这里。
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include "kem.h"
#include "randombytes.h"
void round_trip(void) {
uint8_t pk[CRYPTO_PUBLICKEYBYTES];
uint8_t sk[CRYPTO_SECRETKEYBYTES];
uint8_t ct[CRYPTO_CIPHERTEXTBYTES];
uint8_t key_a[CRYPTO_BYTES];
uint8_t key_b[CRYPTO_BYTES];
//Alice generates a public key
crypto_kem_keypair(pk, sk);
print_key("Alice' public key", pk);
//Bob derives a secret key and creates a response
crypto_kem_enc(ct, key_b, pk);
print_key("Bob's shared key", key_b);
print_key("Bob's response key", ct);
//Alice uses Bobs response to get her shared key
crypto_kem_dec(key_a, ct, sk);
print_key("Alice' shared key", key_a);
}
不深入细节,可以看出 Kyber 使用了多个公钥和私钥。在上述示例中,Alice 生成了一个公钥 (pk) 和一个私钥 (sk)。接下来,Bob 使用公钥 (pk) 推导出共享密钥 (key_b) 和响应密钥 (ct)。后者被返回给 Alice。最后,Alice 使用响应密钥 (ct) 和她的私钥 (sk) 生成共享密钥的实例 (key_a)。只要双方保持私钥和共享密钥的机密性,该算法就能提供保护。在运行程序时,我们会得到类似于下面的文本输出。
Alice' public key: F0476B9B5867DD226588..
Bob's shared key: ADC41F30B665B1487A51..
Bob's response key: 9329C7951AF80028F42E..
Alice' shared key: ADC41F30B665B1487A51..
为了在 Python 中调用 C 函数,我们可以使用 subprocess 模块。或者,可以构建一个共享库,并使用 ctypes 模块进行调用。第二种方法在下面的 Python 脚本中实现。在加载从 Kyber C 代码生成的共享库后,过程 round_trip 会像其他 Python 函数一样被调用。
#!/usr/bin/env python
import os
import ctypes
# Load shared library
libname = f"{os.getcwd()}/execute_round_trip1024.so"
clib = ctypes.CDLL(libname, mode=1)
print("Shared lib loaded successfully:")
print(clib)
# Call round trip function
print("Executing round trip:")
clib.round_trip()
除了 Kyber 的参考实现外,其他提供商也实现了该算法。例如,开源项目 Botan 和 Open Quantum Safe。
结论
我们的分析显示,量子技术仍处于早期阶段。但我们不应低估它对加密和其他密码学方法(如签名)构成的威胁。颠覆性创新随时可能推动发展。攻击者现在可以存储消息,稍后再解密。因此,应立即采取安全措施。尤其是,因为有可用的解决方法。正确使用时,像 AES-256 这样的对称算法被认为是量子抗性的。此外,像 Kyber 这样的非对称解决方案也在进展中。使用哪些替代方案取决于应用场景。遵循零信任模型,组合多种方法能提供最佳保护。这样,量子威胁可能会像 Y2K 问题一样,成为一种自我实现的预言。
关于作者
Christian Koch 是 BWI GmbH 的企业架构师,并且是纽伦堡技术学院 Georg Simon Ohm 的讲师。
Lucie Kogelheide 是 BWI GmbH 的后量子密码学技术主管,负责启动公司向量子安全密码学的迁移过程。
Raphael Lorenz 是 Lorenz Systems 的创始人兼首席信息安全官,专注于整体安全解决方案。
参考文献
-
Snowden, Edward: 永久记录。Macmillan, 2019。
-
国家标准与技术研究院: NIST 后量子密码学:常见问题. 2023 年 6 月 29 日。访问日期:2023 年 8 月 2 日。
-
联邦信息安全办公室 (BSI): 量子安全密码学——基础知识、当前进展和建议 (PDF)。2021 年 10 月。访问日期:2023 年 8 月 2 日。
-
CRYSTALS — 代数格加密套件:Kyber Home. 2020 年 12 月。访问时间:2023 年 8 月 2 日。
免责声明
请注意,信息安全是一个关键话题,作者对发布的内容不提供任何担保。
数据驱动讲故事中的上下文力量
原文:
towardsdatascience.com/power-of-context-in-data-driven-storytelling-b4dc48a402e
要么正确开始,要么立刻放弃
·发表于 Towards Data Science ·阅读时间 12 分钟·2023 年 9 月 5 日
--
什么是数据驱动讲故事,它为何如此重要?
数据驱动讲故事是一种通过叙述和可视化来传达数据信息的方式。其目标是吸引观众,并帮助他们更好地理解主要结论和趋势。
数据可视化有助于吸引观众。仅仅展示原始事实或信息是不够的。以一种能引起观众注意并且令人愉悦的方式展示数据是很重要的。
叙事 就是故事本身。叙事位于数据驱动故事的核心,帮助创造一个连贯的、有意义的信息。
上下文是数据驱动讲故事中的一个关键组成部分,在我看来,占据了故事成功的 80%。叙事和视觉元素占据了剩下的 20%。
今天,我们拥有丰富的数据、工具和方法。然而,即使有了这些资源,将它们付诸实践仍然是相当具有挑战性的。讲故事可以弥合这个差距。今天公司的决策者、董事会成员、总监和经理们,不应担心数据清理、分析技术或具体工具。对他们而言,重要的是解释、建议及其潜在影响。他们希望能够提出问题、提供反馈并理解答案。他们都需要数据驱动的讲故事!
但如果我们不认识到上下文,数据讲故事必然会失败!
故事的上下文有三个基本维度:情境、功能和数据。
本质上,上下文 指的是故事双方(即讲述者和观众)都需要理解的背景信息。它回答三个主要问题:谁,什么,和 如何?
谁?
这关乎于确定故事的受众是谁,并理解讲述者与听众之间的关系。
什么?
这关乎于故事的主要主题以及你希望受众在听完后做出什么决定或行动。
怎么做?
这关乎于选择正确的数据和分析工具,并决定如何将发现与受众分享。
如果我们没有做好这些功课,会发生什么?
哦,很多。我给你举几个例子。
切斯托霍瓦防守
切斯托霍瓦是波兰南部的一座城市,拥有一座可以追溯到 15 世纪的著名大教堂。它有一幅受人尊敬的圣母像,对许多波兰人来说极为神圣。这座城市还因其在 1655 年战胜强大的瑞典军队而闻名,被誉为勇气和抵抗的经典例证。

切斯托霍瓦防守,source。
当波兰足球队面对强大的对手并必须防守自己的球门(这种情况经常发生)时,我们波兰人开玩笑地称之为“防守切斯托霍瓦”。我们希望他们能像 17 世纪的防御者那样获胜,但这通常只是愿望罢了。
不幸的是,当我们不认识或不关心上下文时,这也是我们故事中相当常见的情形。大多数关于数据的批评(无论是否有效)都源于模糊性或不一致性。当数据不清晰或自相矛盾时,人们更容易质疑其有效性。而且,事实证明,捍卫我们的立场会更加困难。
回到我的切斯托霍瓦例子,那里(只有)一个对手。假设有更多的对手。两个、三个、更多,所有的对手!这时我们就陷入了
问题的交火

问题的交火。来源:由作者在 DALL-E 2 生成的图像
当有影响力或权威人士提出疑虑或担忧时,这种轰炸会显著加剧。一旦种下了怀疑的种子,其他人可能会迅速跟进,提出问题或放大初步的担忧。
这里的危险有两方面。首先,这些疑虑和担忧很容易遮蔽我们的故事和支持分析。其次,当面临无情的提问,特别是如果没有准备好,就有可能显得不够可信或知识浅薄,即使数据和故事最初是扎实且经过良好研究的。
我们自己也可能会对这种混乱作出贡献。尤其是如果我们在某些时候,可能无意中做了一些事,这会...
牵连某人
这很经典。确实,不太好。

来源:由作者在 DALL-E 2 生成的图像
在展示数据驱动的洞察时,有可能无意中让观众中的某个人处于不舒服的境地,特别是当数据揭示敏感或意外的结果时。这可能导致紧张局势并打乱讨论的整体流程。这种不适可能源于挑战现有信念的数据,暗示意外的后果,或突显某些特定领域或个人。
这些情况可能会偏离主要的数据洞察,并破坏预期的叙事。个体可能会感到被针对或防御,特别是当数据似乎在批评他们的工作或决策时。此外,其他观众成员可能会小心翼翼,因为他们担心后续的数据点可能会突显他们的领域。以同理心和策略来展示数据至关重要,以维持建设性的环境。
好的。现在我们知道了可能出现的问题。让我们看看如何避免或摆脱困难情况。
情境背景
准备的威力
在制作以数据为驱动的演示或沟通时,设定合适的背景并了解受众的动态至关重要,无论他们是同事、上级还是下属。预测可能的异议并确保数据的清晰性非常关键。与利益相关者进行预先的数据验证会议尤其重要,特别是在使用不熟悉的工具或数据集时。决定是进行详细的演示还是简短的沟通(如电子邮件)更为适宜。为沟通设定明确的目标,并始终准备好主要信息,以应对有限的注意力跨度。
考虑进行利益相关者访谈、调查、直接观察和焦点小组,以获得全面的理解。技术上的不确定性?咨询领域专家。社交媒体也可以提供公众情绪和潜在受众观点的见解。
动力是关键
在我一次关于讲故事的演示中,我构建了一个非凡的结构(在我主观看来)。我们都同意生活中的故事能够引起情感共鸣,对吧?我们认同英雄,憎恨邪恶角色,经历恐惧和快乐。这些都是情感。但是如何在企业生活中激发这些情感呢?嗯,通过动机。你看到了连接了吗?
企业生活中的动机实际上有两个方面:
-
我们让利益相关者高兴或
-
我们让他们不高兴。
当他们看到良好的结果、项目进展或战略的首次积极效果时,他们会感到高兴。如果他们看到相反的情况,他们会感到不高兴。或者他们可能会感到受到威胁:一个邪恶角色。

来源:作者在 DALL-E 2 生成的图片
在叙事中,三个常见的对手塑造了叙事的框架。第一个是竞争。这种外部挑战迫使企业或个人进行创新并保持领先。接着是全球不确定性。后 COVID 时代的持续担忧和 2022 年乌克兰战争的影响示例了此类事件如何造成广泛的忧虑。这些不可预测的情况通常需要对计划或策略进行调整。最后是内部斗争。像过时的技术这样的问题看似微不足道,但随着时间的推移可能会导致更严重的问题,从而使应对外部挑战变得更加困难。这些元素共同构成了我们故事中的核心障碍,为克服逆境和成长奠定了基础。
情景分析可以像一个“假如”游戏,帮助企业在面对坏角色时进行导航。对于竞争,它帮助我们预测对手的下一步动作并做好准备。它帮助我们思考不同的结果,并为像 COVID 或冲突这样的意外事件提前制定计划(至少在某种程度上)。对于我们的问题,比如旧技术,它展示了修复这些问题的好处或忽视它们的风险。这是一个帮助企业为不同情况做好准备的工具。
应对挑战情境
“切斯托霍瓦防御”陷阱强调了预先采取行动的必要性。数据必须以透明且连贯的方式呈现。不一致或模糊的可视化可能会损害数据的完整性,导致怀疑。强大的数据管理实践和基于数据科学原则的精确可视化技术对增强数据的可信度至关重要。必须精心准备,预见潜在挑战,并保持一致的数据驱动叙事。数据熟练的同事的审查可以发现分析中的潜在陷阱或偏见。在展示可能敏感或有争议的发现时,调整对观众反应的敏感性至关重要。迅速回应关切,提供数据支持的澄清,并在话题变得有争议时提议后续讨论。在动态的数据科学领域,营造一个开放的、基于证据的沟通环境可以确保每个人都感受到被重视,并做出数据驱动的决策。
功能性背景
上下文的功能性方面,尤其是在考虑数据和数据科学时,本质上更具“实际性”。这一务实维度强调将原始数据转化为可操作的洞察,这些洞察与利益相关者产生共鸣,并对业务决策产生直接影响。虽然更广泛的视角提供了总体概述,但功能性上下文确保数据驱动的叙事不仅仅是信息性的,而是真正有影响力、有意义的,并且与业务目标紧密对齐。这是一个不可忽视的元素。
功能性上下文是关于使数据对现实世界决策有用和相关。 它将原始数据转化为可以帮助业务的洞察。情境上下文,另一方面,是关于设定场景。 它回答了谁应该关心数据,主要信息是什么,以及如何分享。将功能性上下文视为数据故事的“实质”,而情境上下文则是故事发生的“背景”。这两者都很重要,以确保数据故事既清晰又有影响力。
数据素养
一个重要的方面是数据素养,它有助于保持功能性上下文的连贯性。
数据素养就像是能够阅读和理解数字背后的深层故事。它是关于真正理解数字所传达的信息,并确保我们分享真实且重要的信息。这项技能有助于将复杂的数据转化为人们可以轻松联系的简单故事。
当你具备数据素养时,你可以识别趋势、关系或异常值,并确保数据质量良好。这使得你用数据讲述的故事既有趣又值得信赖。简而言之,就是将数字转化为每个人都能理解并相信的清晰故事。
相关性
相关性是引人入胜的叙事的基石,尤其是如果根植于数据科学的话。这是关于确保你展示的问题、概念或机会直接影响业务。虽然不一定需要是有形的,但效果越显著,你的故事就会越有影响力。
在制定叙事时,确定与你的利益相关者产生共鸣的关键绩效指标(KPIs)和关键结果指标(KRIs)至关重要。理解这些通常并不困难。也许你的组织使用*衡计分卡,这是一个突出目标如何贯穿整个公司的工具,每个目标都有相关的指标。或者,即使没有确切的数字,了解影响某人激励措施的指标也可以增强你故事的影响力。
理解你公司的价值创造链至关重要。这将数据素养与相关性连接起来。例如,你可以向利益相关者展示特定的数据驱动方法如何提升客户旅程的效率。请看下面的图示。 教育你的听众关于特定数据分析工具在优化关键业务流程中的好处是一项值得优先考虑的宝贵投资。

来源:由作者基于[1]的灵感制作的图像。
情节要点
故事的结构和流程赋予其形式,但情节要点使其独特。这些是塑造每个场景的关键细节。它们让观众能够对呈现的数据的重要性做出自己的评估。它们帮助观众可视化预期的背景。以下是九个需要考虑的关键情节要点:
-
趋势变化: 观察趋势是否在上升或下降以及其进展。例如,即使在投资了安全措施后,生产线上的事故数量可能增加。
-
依赖性: 显示两个事物之间的关系。例如,更高的净推荐值(NPS)可能与更多的客户留存相关联。
-
交集: 这涉及一个变量超越另一个变量。它可以是正面的,比如初创企业的收入超过成本;也可以是负面的,比如一个产品的销售低于竞争对手。
-
预测: 预测未来。例如,一个国家的人口可能因移民和其他人口变化而发生的变化。
-
比较: 指出两个或多个项目之间的相似性或差异性。这可以是比较一台旧机器的效率与我们考虑购买的新机器。这在商业故事中经常使用。
-
深入分析: 将一般数据拆分为详细的细分。你可能在仪表板上看到整体区域结果,然后深入到子区域或单个商店。
-
聚合(缩小视图): 与深入分析相反。例如,我们将一个商店的结果与区域或全国*均水*进行比较。
-
聚类分析: 揭示数据集中存在的集中或分布情况。显著的聚类可能表明一个机会或问题。例如,一个诊所中最贵的病人可能都居住在特定工厂附*。
-
离群值: 这些是从其他数据点中突出的数据点。离群值可能表示问题或机会,具体取决于背景。例如,某个特定产品的购买频率可能远高于其类别中的其他产品[2]。

来源:由作者基于[2]制作的图像。
数据背景
数据背景指的是附加在原始数据旁的额外信息或“元数据”,以使数据更易于理解和有价值。 这些元数据本质上就像一张路线图,提供重要细节,比如谁收集了数据、何时何地收集的以及收集数据的初衷。了解背景有助于更准确地解读数据并进行有效的决策。

带有背景的元数据 😃。图片由作者提供。
在企业环境中,管理这些元数据需要各个角色的合作:
-
数据生产者:创建数据的个人或系统。他们负责确保在数据生成或收集时,元数据的准确性和完整性。
-
数据使用者:依赖数据进行各种任务的最终用户,如分析、报告或决策。他们需要元数据来正确解读数据,并信任其有效性。
-
元数据管理者:专门负责管理元数据的人员或系统。他们确保元数据以有序的方式存储,并且数据使用者能够方便地访问这些信息。
正确管理元数据的重要性不容忽视,尤其是在一个日益数据驱动的世界中。没有这些背景信息,数据很容易被误解或误用,导致错误的结论和不良的决策。
技术进步促使了更具动态性的元数据管理方法,包括“主动元数据管理”。在这种方法中,元数据不仅仅是静态地存储,而是不断更新和在不同系统间同步。这使得数据的无缝集成和交叉引用成为可能,提供了更全面和最新的视图。主动元数据管理在数据不断更新且需要快速、准确的解读以进行实时决策的环境中至关重要 [3]。
引用数据来源
关于背景还有最后一点值得提及,那就是引用数据来源。当你在工作中使用数据时,就像从别人那里借书一样。正如你会感谢借书的人,你也应该给予提供数据的人员应有的信用。这不仅是对他人的尊重,还帮助其他人追溯原始数据。如果他们想查看数据本身,这是双赢的:数据的提供者得到认可,使用数据的人可以展示他们的工作基于可靠的信息。为了给予信用,提到数据的创造者、数据生成时间以及数据的名称 [4]。在我看来,适当地给予信用尤其在我们之前讨论的前三种情况中可能会起到救命作用。如果数据来源于知名或可信的地方,那就更好了。
结论
用数据讲故事不仅仅是关于数字。 这涉及到用视觉效果和好的故事使这些数字易于理解。关键是背景: 了解情况,使数据有用,并了解数据来源。想象一下讲解一场著名的战役:如果你没有正确设定背景,人们就无法理解。讲好一个故事,你需要了解你的听众,选择合适的主题,并准备回答任何问题。这也是与人们的情感连接,尤其是在商业中。擅长数据意味着你的故事既令人兴奋又可信。 了解数据对公司为何重要以及发现数据中的有趣点使故事更丰富。在充满数据的世界中,适当的背景至关重要。理解数据的来源和其可信度对做出明智决策至关重要。
作者的主观评估
你喜欢这篇文章吗?考虑订阅以获取我发布的新故事通知,或关注我。
参考文献
[1] 艾薇·刘,数据策略中的连接点,2022 年 1 月 2 日
[2] 布伦特·戴克斯,《有效的数据讲述》,Wiley,2019
[3] 彼得·克罗克,增强数据背景的指南:谁,什么,何时,哪里,为什么,以及如何, 2023 年 8 月 11 日
[4] 哥伦比亚大学,引用数据来源——为什么这样做是好的,以及如何做到?
实用的预算优化方法在营销组合建模中的应用
如何使用饱和曲线和统计模型优化媒体组合
·
关注 发表在 Towards Data Science · 9 分钟阅读 · 2023 年 2 月 28 日
--
图片由 Joel Filipe 提供,来源于 Unsplash
市场营销组合建模(MMM)是一种数据驱动的方法,用于识别和分析业务结果(如销售或收入)的关键驱动因素,通过检验各种因素对响应的影响。MMM 的目标是提供有关如何优化营销活动(包括广告、定价和促销)以改善业务表现的见解。在所有影响业务结果的因素中,营销贡献(例如各种媒体渠道的广告支出)被认为对响应有直接和可衡量的影响。通过分析不同媒体渠道广告支出的效果,MMM 可以提供有价值的见解,帮助确定哪些渠道最有效于增加销售或收入,以及哪些渠道可能需要优化或淘汰,以最大化营销投资回报。
对 MMM 的简短介绍
市场营销组合建模(MMM)是一个多步骤的过程,涉及一系列独特的步骤,这些步骤由正在分析的营销效果驱动。首先,将媒体渠道的系数限制为正值,以考虑广告活动的正面效应。
如何在 Python 中使用 RPy2 接口拟合 SciPy 线性回归并调用 R 岭回归
[towardsdatascience.com
其次,应用广告库存转换,以捕捉广告对消费者行为的滞后和衰减影响。
实验先验、数据归一化,并将贝叶斯建模与 Robyn(Facebook 的开源 MMM)进行比较…
[towardsdatascience.com
第三,广告支出与相应业务结果之间的关系不是线性的,而是遵循递减效应法则。在大多数 MMM 解决方案中,建模者通常采用线性回归来训练模型,这带来了两个主要挑战。首先,建模者必须应用饱和转换步骤来建立媒体活动变量与响应变量之间的非线性关系。其次,建模者必须制定关于适用于每个媒体渠道的可能转换函数的假设。然而,更复杂的机器学习模型可能在不应用饱和转换的情况下捕捉非线性关系。
捕捉非线性广告饱和度和递减回报,而无需显式转换媒体变量
使用*滑样条建模营销组合 [## 使用机器学习方法改善营销组合建模
使用基于树的集成方法构建 MMM 模型,并使用 SHAP(Shapley 加性解释)解释媒体渠道表现
最后一步是通过估计系数以及广告库存和饱和函数的参数来构建营销组合模型。
预算优化
饱和曲线和经过训练的模型都可以用于营销组合建模,以优化预算支出。使用饱和曲线的优势包括:
-
简化可视化支出对结果的影响
-
不再需要基础模型,因此预算优化程序简化,仅需饱和转换的参数
其中一个缺点是饱和曲线基于历史数据,可能无法始终准确预测未来支出的响应。
使用训练模型进行预算优化的优势在于,模型使用媒体活动与其他变量(包括趋势和季节性)之间的复杂关系,可以更好地捕捉递减回报。
数据
我继续使用Robyn提供的 MIT 许可证数据集进行实际示例,并按照相同的数据准备步骤应用 Prophet 来分解趋势、季节性和假期。
数据集包括 208 周的收入(从 2015 年 11 月 23 日到 2019 年 11 月 11 日),包含:
-
5 个媒体支出渠道:tv_S, ooh_S, print_S, facebook_S, search_S
-
2 个也包含曝光信息(印象,点击)的媒体渠道:facebook_I, search_clicks_P(本文未使用)
-
无支出的有机媒体:新闻通讯
-
控制变量:事件,假期,竞争对手销售 (competitor_sales_B)
建模
我构建了一个完整的工作 MMM 流程,可以在现实生活中用于分析媒体支出对响应变量的影响,包含以下组件:
-
广告库存转换具有无限衰减率(0 < α < 1)
-
饱和 Hill 变换 具有两个参数:斜率/形状参数(控制曲线的陡峭度(s > 0))和半饱和点(0 < k ≤ 1)
-
来自 scikit-learn 的岭回归
关于系数的说明
在 scikit-learn 中,岭回归不提供设置部分系数为正值的选项。然而,一个可能的解决方法是,如果某些媒体系数为负值,则拒绝 optuna 解决方案。这可以通过返回一个非常大的值来实现,表明负系数是不可接受的,必须排除。
关于饱和变换的说明
Hill 饱和函数假设输入变量在 0 到 1 的范围内,这意味着在应用变换之前必须对输入变量进行归一化。这一点很重要,因为 Hill 函数假设输入变量的最大值为 1。
然而,可以通过使用以下方程式将半饱和参数缩放到支出范围,从而将 Hill 变换应用于未归一化的数据:
half_saturation_unscaled = half_saturation * (spend_max - spend_min) + spend_min
其中 half_saturation 是在 0 和 1 之间的原始半饱和参数,spend_min 和 spend_max 分别表示最小和最大支出值。
完整的变换函数如下:
class HillSaturation(BaseEstimator, TransformerMixin):
def __init__(self, slope_s, half_saturation_k):
self.slope_s = slope_s
self.half_saturation_k = half_saturation_k
def fit(self, X, y=None):
return self
def transform(self, X: np.ndarray, x_point = None):
self.half_saturation_k_transformed = self.half_saturation_k * (np.max(X) - np.min(X)) + np.min(X)
if x_point is None:
return (1 + self.half_saturation_k_transformed**self.slope_s / X**self.slope_s)**-1
#calculate y at x_point
return (1 + self.half_saturation_k_transformed**self.slope_s / x_point**self.slope_s)**-1
使用饱和曲线进行预算优化
一旦模型训练完成,我们可以使用通过 Hill 饱和变换生成的响应曲线来可视化媒体支出对响应变量的影响。下图展示了五个媒体渠道的响应曲线,描述了每个渠道的支出(按周)与 208 周期间响应之间的关系。

作者提供的图片
使用饱和曲线优化预算涉及确定每个媒体渠道的最佳支出,以在保持总预算固定的情况下实现最高的整体响应。
为了启动优化,通常使用特定时间段的*均支出作为基准。然后,优化器使用每个渠道的预算,这些预算可以在预定的最小和最大限度(边界)内波动,以进行受限优化。
以下代码片段演示了如何使用minimize函数(来自scipy.optimize包)实现预算优化。然而,值得注意的是,也可以使用其他优化包,如nlopt或nevergrad。
optimization_percentage = 0.2
media_channel_average_spend = result["model_data"][media_channels].mean(axis=0).values
lower_bound = media_channel_average_spend * np.ones(len(media_channels))*(1-optimization_percentage)
upper_bound = media_channel_average_spend * np.ones(len(media_channels))*(1+optimization_percentage)
boundaries = optimize.Bounds(lb=lower_bound, ub=upper_bound)
def budget_constraint(media_spend, budget):
return np.sum(media_spend) - budget
def saturation_objective_function(coefficients,
hill_slopes,
hill_half_saturations,
media_min_max_dictionary,
media_inputs):
responses = []
for i in range(len(coefficients)):
coef = coefficients[i]
hill_slope = hill_slopes[i]
hill_half_saturation = hill_half_saturations[i]
min_max = np.array(media_min_max_dictionary[i])
media_input = media_inputs[i]
hill_saturation = HillSaturation(slope_s = hill_slope, half_saturation_k=hill_half_saturation).transform(X = min_max, x_point = media_input)
response = coef * hill_saturation
responses.append(response)
responses = np.array(responses)
responses_total = np.sum(responses)
return -responses_total
partial_saturation_objective_function = partial(saturation_objective_function,
media_coefficients,
media_hill_slopes,
media_hill_half_saturations,
media_min_max)
max_iterations = 100
solver_func_tolerance = 1.0e-10
solution = optimize.minimize(
fun=partial_saturation_objective_function,
x0=media_channel_average_spend,
bounds=boundaries,
method="SLSQP",
jac="3-point",
options={
"maxiter": max_iterations,
"disp": True,
"ftol": solver_func_tolerance,
},
constraints={
"type": "eq",
"fun": budget_constraint,
"args": (np.sum(media_channel_average_spend), )
})
一些重要点:
-
fun — 需要最小化的目标函数。在这种情况下,它接受以下参数:
媒体系数 — 每个媒体渠道的岭回归系数,与相应的饱和度水*相乘,以估计每个媒体渠道的响应水*。
斜率和半饱和度 — Hill 变换的两个参数,用于每个媒体渠道的支出最小值和最大值,以正确估计给定媒体支出的响应水*。
目标函数遍历所有媒体渠道,并根据每个媒体渠道的个体响应水*的总和计算总响应。为了在优化函数中最大化响应,我们需要将其转换为最小化问题。因此,我们获得总响应的负值,并将其作为优化函数的目标。
-
method = SLSQP — 顺序最小二乘规划(SLSQP)算法是一种流行的约束优化问题方法,通常用于优化市场营销组合建模中的预算分配。
-
x0 — 初始猜测。一个大小为(n,)的实数数组,其中
n是独立变量的数量。在这种情况下,x0 对应于媒体渠道的*均支出,即每个渠道的*均支出数组。 -
bounds — 指每个渠道的媒体支出范围。
-
约束条件 — SLSQP 的约束条件被定义为字典列表,其中
budget_constraint是一个确保媒体支出总和等于固定预算的函数:np.sum(media_channel_average_spend)。
优化过程完成后,我们可以为每个媒体渠道生成响应曲线,并比较优化前后的支出分配,以评估优化过程的影响。

图片由作者提供
使用训练模型进行预算优化
使用训练模型优化预算的过程与之前的方法非常相似,并且可以应用于有饱和度变换和没有饱和度变换的模型。这种方法为优化营销组合提供了更大的灵活性,允许在包括未来在内的各种时间周期中进行优化。
以下代码突出了当前方法和之前方法之间的差异:
每个渠道的*均支出乘以期望的优化周期。
optimization_period = result["model_data"].shape[0]
print(f"optimization period: {optimization_period}")
optimization_percentage = 0.2
media_channel_average_spend = optimization_period * result["model_data"][media_channels].mean(axis=0).values
lower_bound = media_channel_average_spend * np.ones(len(media_channels))*(1-optimization_percentage)
upper_bound = media_channel_average_spend * np.ones(len(media_channels))*(1+optimization_percentage)
boundaries = optimize.Bounds(lb=lower_bound, ub=upper_bound)
我们可以将优化结果解释为“在特定时间区间内,每个渠道的适当支出量”。
目标函数还需要两个额外参数:*optimization_period*和*additional_inputs*— 所有其他用于模型训练并在所选时间周期内可用的变量,如趋势、季节性、控制变量:
def model_based_objective_function(model,
optimization_period,
model_features,
additional_inputs,
hill_slopes,
hill_half_saturations,
media_min_max_ranges,
media_channels,
media_inputs):
media_channel_period_average_spend = media_inputs/optimization_period
#transform original spend into hill transformed
transformed_media_spends = []
for index, media_channel in enumerate(media_channels):
hill_slope = hill_slopes[media_channel]
hill_half_saturation = hill_half_saturations[media_channel]
min_max_spend = media_min_max_ranges[index]
media_period_spend_average = media_channel_period_average_spend[index]
transformed_spend = HillSaturation(slope_s = hill_slope, half_saturation_k=hill_half_saturation).transform(np.array(min_max_spend), x_point = media_period_spend_average)
transformed_media_spends.append(transformed_spend)
transformed_media_spends = np.array(transformed_media_spends)
#replicate average perio spends into all optimization period
replicated_media_spends = np.tile(transformed_media_spends, optimization_period).reshape((-1, len(transformed_media_spends)))
#add _hill to the media channels
media_channels_input = [media_channel + "_hill" for media_channel in media_channels]
media_channels_df = pd.DataFrame(replicated_media_spends, columns = media_channels_input)
#prepare data for predictions
new_data = pd.concat([additional_inputs, media_channels_df], axis = 1)[model_features]
predictions = model.predict(X = new_data)
total_sum = predictions.sum()
return -total_sum
目标函数通过*media_inputs*参数接收在时间周期内受约束的媒体支出。我们假设这些媒体支出在时间周期的所有周内均匀分布。因此,我们首先将*media_inputs*除以时间周期以获得*均支出,然后使用np.tile进行复制。接着,我们将非媒体变量与媒体支出进行拼接,并在时间区间内的每一周使用model.predict(X=new_data)来预测响应。最后,我们计算总响应作为每周响应的总和,并返回总响应的负值以进行最小化。
结论
在营销组合建模中优化预算支出很重要,因为它使营销人员能够以最有效的方式分配资源,最大化营销效果,并实现业务目标。
我展示了两种使用饱和曲线和训练模型优化营销组合的实际方法。
有关详细实现,请参阅我的Github repo上的完整代码。
感谢阅读!
使用 Python 进行时间序列异常检测的实用指南
原文:
towardsdatascience.com/practical-guide-for-anomaly-detection-in-time-series-with-python-d4847d6c099f
一篇关于使用 Python 和 sklearn 检测时间序列数据异常值的实践文章
·发表于 Towards Data Science ·阅读时长 13 分钟·2023 年 3 月 16 日
--

图片由 Will Myers 提供,来源于 Unsplash
异常检测是一个任务,我们希望识别出明显偏离数据大多数部分的稀有事件。
时间序列中的异常检测有广泛的实际应用,从制造业到医疗保健。异常值表示意外事件,它们可能由生产故障或系统缺陷引起。例如,如果我们监控一个网站的访客数量,数量降到 0,可能意味着服务器出现故障。
在进行预测建模之前,检测时间序列数据中的异常值也很有用。许多预测模型是自回归的,这意味着它们会考虑过去的值来进行预测。过去的异常值肯定会影响模型,因此去除这些异常值可能是一个好的主意,以获得更合理的预测。
在本文中,我们将介绍三种不同的异常检测技术,并在 Python 中实现它们。
-
*均绝对偏差(MAD)
-
隔离森林
-
局部离群因子(LOF)
第一个方法是基线方法,如果系列满足某些假设,它可以很好地工作。其他两种方法是机器学习方法。
使用我的 免费时间序列备忘单 学习最新的时间序列分析技术!获取统计学和深度学习技术的实现,全部使用 Python 和 TensorFlow!
让我们开始吧!
时间序列中的异常检测任务类型
时间序列数据中的异常检测任务主要有两种类型:
-
基于点的异常检测
-
基于模式的异常检测
在第一种类型中,我们希望找到被认为异常的单个时间点。例如,一次欺诈交易就是一个点状异常。
第二种类型关注于寻找作为离群点的子序列。一个例子可能是一个股票在许多小时或几天内以异常水*交易。
在本文中,我们将只关注基于点的异常检测,这意味着我们的离群点是在时间上的孤立点。
场景:AWS 云上的 CPU 利用率
我们在一个监控 AWS 云中 EC2 实例 CPU 利用率的数据集上应用不同的异常检测技术。这是实际数据,每 5 分钟记录一次,从 2014 年 2 月 14 日 14:30 开始。数据集包含 4032 个数据点。它通过Numenta Anomaly Benchmark (NAB)在 AGPL-3.0 许可证下提供。
本文所用的特定数据集可以在这里找到,相关标签在这里。完整源代码可在GitHub上找到。
在开始之前,我们需要格式化数据,以便将每个值标记为离群点或内点。
df = pd.read_csv('data/ec2_cpu_utilization.csv')
# The labels are listed in the NAB repository for each dataset
anomalies_timestamp = [
"2014-02-26 22:05:00",
"2014-02-27 17:15:00"
]
# Ensure the timestamp column is an actual timestamp
df['timestamp'] = pd.to_datetime(df['timestamp'])
现在,离群点被标记为-1,而内点被标记为 1。这与 scikit-learn 中的异常检测算法输出一致。
df['is_anomaly'] = 1
for each in anomalies_timestamp:
df.loc[df['timestamp'] == each, 'is_anomaly'] = -1

格式化的数据包括时间戳、值和标签,以确定其是否为离群点(-1)或内点(1)。图像由作者提供。
到目前为止,我们拥有一个格式正确的数据集,包括时间戳、值和标签,以指示值是否为离群点(-1)或内点(1)。
现在,让我们绘制数据以可视化异常。
anomaly_df = df.loc[df['is_anomaly'] == -1]
inlier_df = df.loc[df['is_anomaly'] == 1]
fig, ax = plt.subplots()
ax.scatter(inlier_df.index, inlier_df['value'], color='blue', s=3, label='Inlier')
ax.scatter(anomaly_df.index, anomaly_df['value'], color='red', label='Anomaly')
ax.set_xlabel('Time')
ax.set_ylabel('CPU usage')
ax.legend(loc=2)
fig.autofmt_xdate()
plt.tight_layout()

监控 EC2 实例上的 CPU 使用情况。两个红点表示异常点,而其他蓝点被认为是正常的。图像由作者提供。
从上面的图中,我们可以看到我们的数据仅包含两个离群点,如红色点所示。
这显示了异常检测的挑战性!由于这些事件很少,我们很少有机会从中学习。在这种情况下,只有 2 个点是离群点,占数据的 0.05%。这也使得模型评估更具挑战性。一个方法基本上只有两次正确的机会,而有 4030 次错误的机会。
鉴于以上所有内容,让我们应用一些时间序列异常检测技术,从*均绝对偏差开始。
*均绝对偏差(MAD)
如果我们的数据是正态分布的,我们可以合理地说,尾部的每个数据点都可以被视为离群点。
为了识别它们,我们可以使用 Z 分数,这是一个以标准差为单位的均值测量。如果 Z 分数为 0,则值等于均值。通常,我们设置 Z 分数阈值为 3 或 3.5,以指示一个值是否是异常值。
现在,回想一下 Z 分数的计算方法。

Z 分数的公式。作者提供的图像。
其中 mu 是样本的均值,sigma 是标准差。基本上,如果 Z 分数很大,意味着该值远离均值,接*分布尾部的一端,这也可能表示它是一个异常值。

带有 Z 分数的正态分布。我们可以看到,当 Z 分数为 3 时,我们达到了分布的尾部,因此我们可以说,超过该阈值的数据是异常值。作者提供的图像。
从上图中,我们可以直观地看到经典的 Z 分数阈值 3,用于确定一个值是否是异常值。如黑色虚线所示,Z 分数为 3 时,我们达到了正态分布的尾部。因此,任何 Z 分数大于 3(或小于 -3,如果我们不处理绝对值的话)都可以标记为异常值。
现在,这在我们假设有一个完全正态分布的情况下效果很好,但异常值的存在必然会影响均值,从而影响 Z 分数。因此,我们将注意力转向中位绝对偏差或 MAD。
强健 Z 分数方法
为了避免异常值对 Z 分数的影响,改用中位数,这在存在异常值时是更强健的指标。
中位绝对偏差或 MAD 定义为:

MAD 是值与样本中位数之间绝对差值的中位数。作者提供的图像。
基本上,MAD 是样本值与样本中位数之间绝对差值的中位数。然后,我们可以使用以下公式计算强健 Z 分数:

强健 Z 分数公式。请注意,0.6745 是 MAD 收敛的标准正态分布的第 75 个百分位数。作者提供的图像。
在这里,强健 Z 分数计算方法是:取值与样本中位数之间的差值,乘以 0.6745,然后除以 MAD。请注意,0.6745 代表标准正态分布的第 75 个百分位数。
为什么是 0.6745?(可选阅读)
与传统的 Z 分数不同,强健 Z 分数使用中位绝对偏差,这通常小于标准差。因此,为了获得类似 Z 分数的值,我们必须进行缩放。
在没有异常值的正态分布中,MAD 大约是标准差的 2/3(精确来说是 0.6745)。因此,由于我们是除以 MAD,我们乘以 0.6745 以回到正态 Z 分数的尺度。
稳健 Z 分数方法在两个重要假设下效果最佳:
-
数据接*正态分布
-
MAD 不等于 0(当超过 50%的数据具有相同值时会发生)
第二点很有趣,因为如果是这种情况,那么任何不等于中位数的值都会被标记为异常值,无论阈值如何,因为稳健 Z 分数将会非常大。
鉴于此,让我们将此方法应用到我们的场景中。
应用 MAD 进行异常值检测
首先,我们需要检查数据的分布情况。
import seaborn as sns
sns.kdeplot(df['value']);
plt.grid(False)
plt.axvline(0.134, 0, 1, c='black', ls='--')
plt.tight_layout()

我们的数据分布情况。我们已经看到数据不符合正态分布!更糟的是,许多数据点正好落在中位数(黑色虚线)上,这意味着 MAD 要么是 0,要么非常接* 0。图片由作者提供。
从上图中,我们可以看到两个问题。首先,数据接*正态分布。其次,黑色虚线表示样本的中位数,它正好位于分布的峰值上。这意味着许多数据点等于中位数,意味着我们处于 MAD 可能为 0 或非常接* 0 的情况。
尽管如此,我们还是继续应用该方法,以便了解如何使用它。
下一步是计算样本的 MAD 和中位数,以计算稳健 Z 分数。scipy包包含了 MAD 公式的实现。
from scipy.stats import median_abs_deviation
mad = median_abs_deviation(df['value'])
median = np.median(df['value'])
然后,我们简单地编写一个函数来计算稳健 Z 分数,并创建一个新列来存储分数。
def compute_robust_z_score(x):
return .6745*(x-median)/mad
df['z-score'] = df['value'].apply(compute_robust_z_score)
注意,我们得到了 0.002 的 MAD,这确实接* 0,意味着这个基准可能表现不佳。
完成后,我们决定一个阈值来标记异常值。典型的阈值是 3 或 3.5。在这种情况下,任何稳健 Z 分数大于 3.5(右尾)或小于-3.5(左尾)的值将被标记为异常值。
df['baseline'] = 1
df.loc[df['z-score'] >= 3.5, 'baseline'] = -1 # Right-end tail
df.loc[df['z-score'] <=-3.5, 'baseline'] = -1 # Left-hand tail
最后,我们可以绘制混淆矩阵,看看我们的基准是否正确识别了异常值和正常值。
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
cm = confusion_matrix(df['is_anomaly'], df['baseline'], labels=[1, -1])
disp_cm = ConfusionMatrixDisplay(cm, display_labels=[1, -1])
disp_cm.plot();
plt.grid(False)
plt.tight_layout()

基准异常值检测方法的混淆矩阵。显然,许多正常值被标记为异常值,这在预期之中,因为我们的数据没有符合 MAD 方法的假设。图片由作者提供。
毫不意外地,我们看到基准方法表现不佳,因为 1066 个正常值被标记为异常值。这再次是预期中的情况,因为我们的数据没有符合该方法的假设,且 MAD 非常接* 0。尽管如此,我还是想介绍这种方法的实现,以防在其他场景中对你有用。
尽管结果令人失望,但当假设对你的数据集成立时,这种方法仍然有效,现在你知道在有意义的情况下如何应用它。
现在,让我们转到机器学习方法,首先从隔离森林开始。
隔离森林
孤立森林算法是一种基于树的算法,通常用于异常检测。
算法首先随机选择一个属性,并在该属性的最大值和最小值之间随机选择一个分裂值。这个分区过程会重复多次,直到算法隔离了数据集中的每个点。
然后,这个算法的直觉是,离群点隔离所需的分区会比正常点少,如下图所示。

隔离一个内点。注意在点被隔离之前,数据必须经过多次分区。图像由 Sai Borrelli 提供 — 维基百科

隔离一个离群点。现在,我们看到隔离它所需的分区较少。因此,它很可能是一个异常值。图像由 Sai Borrelli 提供 — 维基百科
在上面的两个图中,我们可以看到在隔离内点和离群点时,分区的数量如何不同。在顶部图中,隔离一个内点需要很多分裂。在底部图中,隔离点所需的分裂较少。因此,它很可能是一个异常值。
所以我们看到在孤立森林中,如果隔离数据点的路径很短,那么它就是一个异常值!
应用孤立森林
首先,让我们将数据分成训练集和测试集。这样,我们可以评估模型是否能够在未见数据上标记异常值。这有时被称为新颖性检测,而不是异常检测。
train = df[:3550]
test = df[3550:]
然后,我们可以训练我们的孤立森林算法。在这里,我们需要指定一个污染水*,这只是训练数据中离群点的比例。在这个例子中,我们的训练集只有一个离群点。
from sklearn.ensemble import IsolationForest
# Only one outlier in the training set
contamination = 1/len(train)
iso_forest = IsolationForest(contamination=contamination, random_state=42)
X_train = train['value'].values.reshape(-1,1)
iso_forest.fit(X_train)
训练完成后,我们可以生成预测。
preds_iso_forest = iso_forest.predict(test['value'].values.reshape(-1,1))
再次,我们可以绘制混淆矩阵以查看模型的表现。
cm = confusion_matrix(test['is_anomaly'], preds_iso_forest, labels=[1, -1])
disp_cm = ConfusionMatrixDisplay(cm, display_labels=[1, -1])
disp_cm.plot();
plt.grid(False)
plt.tight_layout()

孤立森林算法的混淆矩阵。在这里,我们可以看到算法没有标记任何异常值。它还错误地将一个异常值标记为正常点。图像由作者提供。
从上面的图中,我们注意到算法无法标记新的异常值。它还将一个异常值标记为正常点。
再次,这是一项令人失望的结果,但我们还有一种方法需要介绍,即局部离群因子。
局部离群因子
直观上,局部离群因子(LOF)通过比较点的局部密度与其邻居的局部密度来工作。如果点和邻居的密度相似,那么该点就是一个内点。然而,如果点的密度远小于邻居的密度,那么它一定是一个离群点,因为较低的密度意味着该点更孤立。
当然,我们需要设置要查看的邻居数量,scikit-learn 的默认参数是 20,这在大多数情况下效果很好。
一旦设置了邻居的数量,我们就计算可达距离。仅用文字和图片解释这有点复杂,但我会尽力说明。

可视化可达距离,图像由作者提供。
假设我们正在研究点 A,并且我们将邻居数量设置为 3(k=3)。在保持点 A 在中间的情况下画一个圆圈,就会得到你在上图中看到的黑色虚线圆圈。点 B、C 和 D 是离 A 最*的三个邻居,而点 E 在这种情况下太远,因此被忽略。
现在,可达距离被定义为:

可达距离方程。图像由作者提供。
换句话说,从 A 到 B 的可达距离是 B 的 k-距离和 A 到 B 的实际距离之间的较大值。
B 的 k-距离只是从点 B 到其第三个最*邻的距离。这就是为什么在上面的图中,我们画了一个以 B 为中心的蓝色虚线圆圈,以体现从 B 到 C 的距离是 B 的 k-距离。
一旦计算了 A 的所有 k 个最*邻的可达距离,将计算局部可达密度。这仅仅是可达距离的*均值的倒数。
直观上,可达密度告诉我们到达邻*点需要走多远。如果密度大,则点彼此靠*,我们不需要走太远。
最后,局部异常因子仅仅是局部可达密度的比率。在上面的图中,我们将 k 设置为 3,因此我们会有三个比率需要进行*均。这允许我们将点的局部密度与其邻居进行比较。
如前所述,如果该因子接* 1 或小于 1,则为正常点。如果大于 1,则为异常值。
当然,这种方法也有缺点,因为大于 1 的值并不是一个完美的阈值。例如,LOF 为 1.1 可能意味着某个数据集中的异常值,但对另一个数据集则不适用。
应用局部异常因子方法
使用scikit-learn应用局部异常因子方法是直接的。我们使用与隔离森林相同的训练/测试拆分,以便获得可比较的结果。
from sklearn.neighbors import LocalOutlierFactor
lof = LocalOutlierFactor(contamination=contamination, novelty=True)
lof.fit(X_train)
然后,我们可以生成预测以标记测试集中的潜在异常值。
preds_lof = lof.predict(test['value'].values.reshape(-1,1))
最后,我们绘制混淆矩阵来评估性能。
cm = confusion_matrix(test['is_anomaly'], preds_lof, labels=[1, -1])
disp_cm = ConfusionMatrixDisplay(cm, display_labels=[1, -1])
disp_cm.plot();

局部异常因子的混淆矩阵。我们看到该算法成功识别了测试集中唯一的异常值。图像由作者提供。
在上面的图中,我们可以看到 LOF 方法能够标记测试集中唯一的异常值,并且正确地将其他每个点标记为正常点。
和往常一样,这并不意味着局部异常因子比孤立森林方法更好。这只是表示在这个特定情况下,局部异常因子效果更好。
结论
在本文中,我们探讨了三种不同的时间序列数据异常检测方法。
首先,我们探讨了一种使用*均绝对偏差(MAD)的强健 Z-score。这在数据呈正态分布且 MAD 不为 0 时效果良好。
然后,我们看了孤立森林方法,这是一种机器学习算法,它确定数据集需要多少次分割才能孤立一个点。如果需要的分割次数很少,那么这个点就是一个异常点。如果需要很多分割,那么这个点很可能是内点。
最后,我们看了局部异常因子(LOF)方法,这是一种无监督学习方法,它将一个点的局部密度与其邻居的密度进行比较。基本上,如果一个点的密度相对于其邻居较小,这意味着它是一个孤立点,很可能是异常点。
希望你喜欢这篇文章,并学到了新知识!
想要掌握时间序列预测吗?查看Python 中的应用时间序列预测,这是唯一涵盖统计学、深度学习和最先进模型的 100% Python 课程。
干杯 🍻
支持我
享受我的工作吗?通过请我喝咖啡来支持我,这是一种简单的方式来鼓励我,而我也可以享受一杯咖啡!如果你愿意,点击下面的按钮 👇
实用介绍 Transformer 模型:BERT

图片来源于 Alex Padurariu 在 Unsplash
动手教程
动手教程:如何使用 BERT 构建你的第一个情感分析模型
·
关注 发表在 Towards Data Science ·7 分钟阅读·2023 年 7 月 17 日
--
前言:本文总结了关于给定主题的信息。它不应被视为原创研究。本文中包含的信息和代码可能受到我过去从各种在线文章、研究论文、书籍和开源代码中阅读或看到的内容的影响。
目录
-
BERT 简介
-
预训练和微调
-
实践操作:使用 BERT 进行情感分析
-
解释结果
-
结束语
在 NLP 中,变换器模型架构是一项革命性的进展,大大增强了理解和生成文本信息的能力。
在本教程中,我们将深入探讨 BERT,这是一种著名的基于变换器的模型,并提供一个实际的示例来微调基础 BERT 模型以进行情感分析。
BERT 介绍
BERT 由 Google 研究人员于 2018 年推出,是一种强大的语言模型,使用变换器架构。与早期的 LSTM 和 GRU 等单向或顺序双向模型架构相比,BERT 同时考虑过去和未来的上下文。这得益于创新的“注意力机制”,它使模型在生成表示时能够权衡句子中单词的重要性。
BERT 模型在以下两个 NLP 任务上进行了预训练:
-
掩码语言模型(MLM)
-
下一句预测(NSP)
并且通常被用作各种下游 NLP 任务的基础模型,例如我们在本教程中将涵盖的情感分析。
预训练和微调
BERT 的强大之处在于它的两步过程:
-
预训练是 BERT 在大量数据上进行训练的阶段。因此,它学会了预测句子中的掩码词(MLM 任务)和预测一个句子是否跟随另一个句子(NSP 任务)。这一阶段的输出是一个具有通用“语言理解”能力的预训练 NLP 模型。
-
微调是在特定任务上进一步训练预训练的 BERT 模型。模型以预训练的参数进行初始化,并在下游任务上对整个模型进行训练,使 BERT 能够根据当前任务的具体情况微调其语言理解能力。
实践操作:使用 BERT 进行情感分析
完整代码可以作为 GitHub 上的 Jupyter Notebook获得
在这个动手练习中,我们将使用 IMDB 电影评论数据集[4] (许可证:Apache 2.0)来训练情感分析模型,该数据集标记了评论是积极还是消极。我们还将使用 Hugging Face 的 transformers 库加载模型。
让我们加载所有库
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.metrics import confusion_matrix, roc_curve, auc
from datasets import load_dataset
from transformers import AutoTokenizer, AutoModelForSequenceClassification, TrainingArguments, Trainer
# Variables to set the number of epochs and samples
num_epochs = 10
num_samples = 100 # set this to -1 to use all data
首先,我们需要加载数据集和模型分词器。
# Step 1: Load dataset and model tokenizer
dataset = load_dataset('imdb')
tokenizer = AutoTokenizer.from_pretrained('bert-base-uncased')
接下来,我们将创建一个图表,以查看积极和消极类别的分布。
# Data Exploration
train_df = pd.DataFrame(dataset["train"])
sns.countplot(x='label', data=train_df)
plt.title('Class distribution')
plt.show()

图 1. 训练数据集的类别分布
接下来,我们通过对文本进行分词来预处理我们的数据集。我们使用 BERT 的分词器,将文本转换为与 BERT 词汇表对应的标记。
# Step 2: Preprocess the dataset
def tokenize_function(examples):
return tokenizer(examples["text"], padding="max_length", truncation=True)
tokenized_datasets = dataset.map(tokenize_function, batched=True)

之后,我们准备训练和评估数据集。请记住,如果你想使用所有数据,可以将num_samples变量设置为-1。
if num_samples == -1:
small_train_dataset = tokenized_datasets["train"].shuffle(seed=42)
small_eval_dataset = tokenized_datasets["test"].shuffle(seed=42)
else:
small_train_dataset = tokenized_datasets["train"].shuffle(seed=42).select(range(num_samples))
small_eval_dataset = tokenized_datasets["test"].shuffle(seed=42).select(range(num_samples))
接下来,我们加载预训练的 BERT 模型。我们将使用AutoModelForSequenceClassification类,这是一种针对分类任务设计的 BERT 模型。
在本教程中,我们使用‘bert-base-uncased’版本的 BERT,它是基于小写英文文本进行训练的。
# Step 3: Load pre-trained model
model = AutoModelForSequenceClassification.from_pretrained('bert-base-uncased', num_labels=2)
现在,我们准备定义我们的训练参数并创建一个Trainer实例来训练我们的模型。
# Step 4: Define training arguments
training_args = TrainingArguments("test_trainer", evaluation_strategy="epoch", no_cuda=True, num_train_epochs=num_epochs)
# Step 5: Create Trainer instance and train
trainer = Trainer(
model=model, args=training_args, train_dataset=small_train_dataset, eval_dataset=small_eval_dataset
)
trainer.train()
结果解释
训练完模型后,让我们来评估一下。我们将计算混淆矩阵和 ROC 曲线,以了解我们的模型表现如何。
# Step 6: Evaluation
predictions = trainer.predict(small_eval_dataset)
# Confusion matrix
cm = confusion_matrix(small_eval_dataset['label'], predictions.predictions.argmax(-1))
sns.heatmap(cm, annot=True, fmt='d')
plt.title('Confusion Matrix')
plt.show()
# ROC Curve
fpr, tpr, _ = roc_curve(small_eval_dataset['label'], predictions.predictions[:, 1])
roc_auc = auc(fpr, tpr)
plt.figure(figsize=(1.618 * 5, 5))
plt.plot(fpr, tpr, color='darkorange', lw=2, label='ROC curve (area = %0.2f)' % roc_auc)
plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver operating characteristic')
plt.legend(loc="lower right")
plt.show()

图 2. 混淆矩阵

图 3. ROC 曲线
混淆矩阵详细地分解了我们的预测与实际标签的匹配情况,而 ROC 曲线则展示了在不同阈值设置下,真正例率(敏感度)与假正例率(1 — 特异度)之间的权衡。
最后,为了查看我们的模型的实际效果,让我们用它来推断样本文本的情感。
# Step 7: Inference on a new sample
sample_text = "This is a fantastic movie. I really enjoyed it."
sample_inputs = tokenizer(sample_text, padding="max_length", truncation=True, max_length=512, return_tensors="pt")
# Move inputs to device (if GPU available)
sample_inputs.to(training_args.device)
# Make prediction
predictions = model(**sample_inputs)
predicted_class = predictions.logits.argmax(-1).item()
if predicted_class == 1:
print("Positive sentiment")
else:
print("Negative sentiment")

结语
通过对 IMDb 电影评论的情感分析示例进行讲解,我希望你能清晰地理解如何将 BERT 应用于现实世界的自然语言处理问题。我在这里包含的 Python 代码可以进行调整和扩展,以应对不同的任务和数据集,为更复杂和准确的语言模型铺*道路。
参考文献
[1] Devlin, J., Chang, M. W., Lee, K., & Toutanova, K. (2018). BERT:用于语言理解的深度双向变换器的预训练。arXiv 预印本 arXiv:1810.04805
[2] Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., … & Polosukhin, I. (2017). 注意力机制即一切。在《神经信息处理系统进展》中(第 5998–6008 页)。
[3] Wolf, T., Debut, L., Sanh, V., Chaumond, J., Delangue, C., Moi, A., … & Rush, A. M. (2019). Huggingface 的变换器:最先进的自然语言处理。ArXiv, abs/1910.03771。
[4] Lhoest, Q., Villanova del Moral, A., Jernite, Y., Thakur, A., von Platen, P., Patil, S., Chaumond, J., Drame, M., Plu, J., Tunstall, L., Davison, J., Šaško, M., Chhablani, G., Malik, B., Brandeis, S., Le Scao, T., Sanh, V., Xu, C., Patry, N., McMillan-Major, A., Schmid, P., Gugger, S., Delangue, C., Matussière, T., Debut, L., Bekman, S., Cistac, P., Goehringer, T., Mustar, V., Lagunas, F., Rush, A., & Wolf, T. (2021). 数据集:自然语言处理的社区库。收录于 2021 年自然语言处理实证方法会议:系统演示 (第 175–184 页)。在线和多米尼加共和国蓬塔卡纳:计算语言学协会。取自 aclanthology.org/2021.emnlp-demo.21
感谢阅读。如果您有任何反馈,请随时通过评论此帖子、在 LinkedIn上给我留言,或发送电子邮件 (smhkapadia[at]gmail.com)
如果您喜欢这篇文章,请访问我的其他文章
逐步指南:为任何领域微调预训练的 NLP 模型
towardsdatascience.com [## 自然语言处理的演变
语言模型发展的历史视角
medium.com](https://medium.com/aimonks/the-evolution-of-natural-language-processing-56ce27916e10?source=post_page-----4715ed0deede--------------------------------) ## Python 中的推荐系统:LightFM
使用 LightFM 在 Python 中构建推荐系统的逐步指南
towardsdatascience.com ## 评估主题模型:潜在狄利克雷分配 (LDA)
构建可解释的主题模型的逐步指南
towardsdatascience.com
使用 Azure ML 实现实用的 MLOps

图片由 Luca Bravo 提供,来源于 Unsplash
使用 Azure ML CLI(v2) 和 github actions 自动化 ML 流程
·
关注 发表在 Towards Data Science · 10 分钟阅读 · 2023 年 2 月 20 日
--
介绍
机器学习模型像我们日常使用的软件产品一样,影响着我们与世界的互动。正如 DevOps 对于无缝的 CI/CD 是必要的,MLOps 也变得至关重要,以不断构建最新的模型并利用其预测结果。
在本文中,我们将使用 Azure ML CLI(v2) 和 Github Actions 构建端到端的 MLOps。本文希望能成为你下一个 MLOps 项目的起点!
本文将帮助你模拟以下场景:
数据经常发生漂移并通过 API 提供。因此需要在规定的频率下重新训练模型并重新部署到在线端点。
通过以下步骤实现每周的定时任务:
-
使用 API 下载数据并将其注册为 Azure 数据集。
-
计算管理及触发 Azure ML Studio 中的模型训练任务。
-
注册在最新任务中创建的模型。
-
将新模型部署到在线端点。
在 Azure ML 中自动化 ML 流水线有两个选项:
1. Azure DevOps
2. Github Actions
你可以在这里阅读相关内容。我选择了 GitHub Actions 因为它的使用方便。
关于 MLOps 的快速回顾
MLOps 的需求及实现步骤:
-
通过一致性和变更跟踪进行质量控制:
a. 整个项目的初始设置 — IDE、工作区、权限
b. 环境版本控制
c. 数据版本控制
d. 代码版本控制
e. 其他组件的版本控制
-
模型的快速实验:
a. 跟踪模型超参数
b. 跟踪不同数据切片上的模型指标、偏差、公*性和可解释性
c. 维护 ML 流水线中变更部分之间的链接
-
无缝的模型部署和全面的模型监控:
a. 快速将模型部署到生产环境
b. 分阶段推出、蓝绿或其他部署策略
c. 跟踪模型效果以触发重新训练
d. 跟踪数据漂移以触发重新训练
范围
我将整个 ML 项目结构化为三个不同的流水线,以实现上述 MLOps 目标:
-
构建流水线
-
训练流水线
-
部署流水线
在讨论这些流水线后,我们将深入探讨代码实现。
1. 构建流水线
1.1 初始设置
步骤 1:设置 Azure ML Studio:
我们将使用 MS Azure 进行数据版本控制、模型训练和部署。请按照以下步骤创建你的 Azure ML 工作区:
-
登录到你的 Azure 帐户,或从这里获取免费的 Azure 订阅。
-
创建资源组(参考)。
-
创建 ML 工作区 - 访问 ml.azure.com,点击 创建工作区 并按照屏幕上的指示操作。
当你的 Azure ML 工作区创建完成后,你应该能看到如下界面:

通过 ml.azure.com 访问 Azure ML 工作区
你暂时无法看到这些任务,不用担心!
步骤 2:将 GitHub Actions 与 Azure ML Studio 链接:
在 GitHub 中初始化一个仓库并转到设置 > 秘密和变量 > Actions > 新建仓库秘密
打开一个新的浏览器标签页以创建一个服务主体以访问你的 ML 工作区。(ref)
将生成的 JSON 保存为 AZURE_CREDENTIALS 并存储在你的仓库秘密中。

仓库中的 GitHub Actions 密钥
第 3 步:生成个人访问令牌 (PAT)
使用普通的 GITHUB_TOKEN,你无法编辑工作流文件(.github/workflows 中的文件),你需要添加 PAT。你需要编辑工作流文件以自动化版本控制训练运行、模型版本等。
在 GitHub 中,从设置 > 开发者设置 > 个人访问令牌 生成 PAT。

用于编辑工作流的个人访问令牌
将 PAT 保存到你的仓库秘密中,并设置以下仓库权限:

PAT 的仓库权限
1.2 环境版本控制
需要两个环境:
-
训练环境 — 用于模型训练依赖
-
部署环境 — 用于模型服务依赖
对于较小的项目,你也可以将它们保存在相同的环境中。
1.3 数据版本控制
在我们的例子中,数据每周下载一次,并在 Azure 数据存储中注册为数据集。数据版本控制对于跟踪模型的血统非常重要。在我们的例子中,这是通过jobs/data_download.save_to_data_upload(…)
1.4 代码版本控制
代码版本控制通过 GitHub 完成。
1.5 其他组件的版本控制
你还需要版本控制 runs, models 和其他组件。为了自动化它们的命名,你需要在之前的 cron 作业中编辑它们。在我们的例子中,这是通过 jobs/update_training_yamls.py 完成的。
1.6 自动化测试
为了自动测试 Python 文件,我们可以使用 pytest 并跟踪代码覆盖率。
2. 训练管道
2.1 模型超参数跟踪
在我们的例子中,我们使用了 mlflow for pytorch 来记录以下详细信息:
指标:

每次训练任务中捕获的指标
运行时模型参数:

每次运行时版本控制的参数
2.2 在不同数据切片上跟踪模型指标、偏差、公*性和解释性
由于我使用了股票数据来模拟不断变化的数据,我跳过了这部分。然而,在大多数机器学习用例中,这些是判断模型性能的重要指标。
2.3 维护机器学习管道中变化部分之间的链接
Azure ML studio 自动链接所有内容。
训练模型时的示例血统跟踪:

训练任务快照
同样,数据和模型也会自动版本化并跟踪它们的历史。
3. 部署流水线
3.1 快速模型部署到生产环境
最初创建端点并部署第一个模型需要一些时间。之后,新注册的模型可以在更短的时间内用于更新端点。
3.2 分阶段推出、蓝绿部署或不同的部署策略
可以使用不同的部署策略。在我们的案例中,我们覆盖了之前部署的模型。然而,通过 Azure ML,蓝绿部署在部署过程中可以非常轻松地实现。
3.3 追踪模型效果以触发重新训练
模型部署后,我们需要跟踪模型对已知标签的性能。这将帮助我们识别模型在哪些数据层次上表现不佳。这将帮助我们确定是否需要收集更多数据或在下一次模型重新训练期间采取其他措施。
3.4 追踪数据漂移以触发重新训练
对于模型重新训练,有两种思路:
1. 基于计划
2. 基于漂移
两种选项各有优缺点。在这个例子中,我遵循基于计划的模型重新训练。因此,我没有实现任何数据漂移监控。然而,Azure 提供了监控数据漂移的工具。
代码
作为工程师,从头开始编写代码时会变得更加清晰。所以,尽情使用吧!
代码库: coderkol95/MLOps_stock_prediction
项目文件夹结构:

文件夹和文件的简要信息:
.github/workflows/
控制流水线的 yml 文件放在这里。这些文件通过 cron 作业触发数据下载/上传、模型训练、注册和部署。
data_pipeline.yml:频率 - 每周一凌晨 1:01
- 下载股票数据并更新 yml 文件
通过 API 下载数据到 csv 文件,并更新 data-upload.yml 文件,添加数据集标签、版本和路径。
- 编辑 yaml 文件
更新 yml 文件中其他组件的版本,如 job_name、模型版本等,这些将在运行期间使用。
- 将文件推送到 github
将更新的 yml 文件和下载的 csv 文件推送到代码库
- 上传到 azure
在 Azure 数据存储中注册数据集
name: data upload to azure
env:
ticker: WIPRO.NS
start: 366
end: 1
on:
schedule:
- cron: "1 1 * * 1"
jobs:
datawork:
runs-on: ubuntu-latest
steps:
- name: checkout repository
uses: actions/checkout@v2
with:
token: ${{ secrets.PAT }}
repository: 'coderkol95/MLOps_stock_prediction'
- name: setup python 3.9
uses: actions/setup-python@v4
with:
python-version: "3.9"
- name: install python packages
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: download ticker data and update yml file
run: python data_download.py --ticker $ticker --start $start --end $end
id: data
working-directory: jobs
- name: edit yaml files
run: python update_training_yamls.py
working-directory: jobs
- name: push files to github
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add -A
git commit -m "Ticker data for $ticker downloaded and YAML file updated." || exit 0
git push @github.com/${GITHUB_REPOSITORY}.git">https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git HEAD
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: azure login
uses: azure/login@v1
with:
creds: ${{secrets.AZURE_CREDENTIALS}}
- name: setup
run: bash setup.sh
working-directory: cli
continue-on-error: true
- name: upload to azure
run: az ml data create -f jobs/data_upload.yml
model_pipeline.yml:频率 - 每周一凌晨 2:01
- 训练作业
在最新的数据集上计算创建和模型训练。
数据集准备和模型训练通过 pytorch lightning 完成。所有日志记录通过 MLFlow 进行。有关代码的详细信息,请参阅我的 代码库。
- 注册作业
从最新运行中注册模型。
- 删除计算
训练完成后计算删除。
name: training and registering model
env:
job_name: ga-run-10
compute_name: computer456
registered_model_name: GA_model
on:
schedule:
- cron: "1 2 * * 1"
jobs:
train-job:
runs-on: ubuntu-latest
steps:
- name: check out repo
uses: actions/checkout@v2
- name: azure login
uses: azure/login@v1
with:
creds: ${{secrets.AZURE_CREDENTIALS}}
- name: setup
run: bash setup.sh
working-directory: cli
continue-on-error: true
- name: create-compute
run: az ml compute create --name $compute_name --size STANDARD_DS11_v2 --min-instances 1 --max-instances 2 --type AmlCompute
- name: train-job
working-directory: jobs
run: az ml job create --file train.yml --debug --stream # --stream causes the step to go on, as long as the model trains.
# If training is expected to take a long time, registration can be scheduled in a separate cron job, triggered later.
register-job:
needs: [train-job]
runs-on: ubuntu-latest
steps:
- name: check out repo
uses: actions/checkout@v2
- name: azure login
uses: azure/login@v1
with:
creds: ${{secrets.AZURE_CREDENTIALS}}
- name: setup
run: bash setup.sh
working-directory: cli
continue-on-error: true
- name: register-model
run: az ml model create
--name $registered_model_name
--version 10
--path azureml://jobs/ga-run-10/outputs/artifacts/paths/outputs/
--type custom_model
delete-compute:
needs: [train-job]
runs-on: ubuntu-latest
steps:
- name: check out repo
uses: actions/checkout@v2
- name: azure login
uses: azure/login@v1
with:
creds: ${{secrets.AZURE_CREDENTIALS}}
- name: setup
run: bash setup.sh
working-directory: cli
continue-on-error: true
- name: delete-step
run: az ml compute delete --name $compute_name --yes
deployment_pipeline.yml:频率 - 每周一凌晨 3:01
-
端点和部署创建(如果是第一次)
-
使用最新模型更新在线部署(如下所示)
name: model deployment
on:
schedule:
- cron: "1 3 * * 1"
jobs:
# compare-job:
# Compare if the model is good enough
# Profile the model
# If it is good enough, proceed to next step
deployment-job:
runs-on: ubuntu-latest
steps:
- name: check out repo
uses: actions/checkout@v2
- name: azure login
uses: azure/login@v1
with:
creds: ${{secrets.AZURE_CREDENTIALS}}
- name: setup
run: bash setup.sh
working-directory: cli
continue-on-error: true
# Commenting out as endpoint creation is only needed during the first run
# - name: create-endpoint
# run: az ml online-endpoint create --name ga-deployment
- name: deployment-step
run: az ml online-deployment update -f deploy.yml #--all-traffic # First time it'll be az ml ... create --all-traffic
working-directory: jobs
我将 cron 作业的执行时间间隔设置为 1 小时,因为它在 1 小时内完成,你可以根据需要设置更长的间隔。你也可以设置标志来捕捉作业完成状态。
cli/
setup.sh: 配置运行代码的虚拟机,适用于 Azure ML。
GROUP="RG"
LOCATION="eastus"
WORKSPACE="AzureMLWorkspace"
az configure --defaults group=$GROUP workspace=$WORKSPACE location=$LOCATION
az extension remove -n ml
az extension add -n ml
jobs/
这里保存了特定于 Azure 的 YAML 文件以及用于单个管道步骤执行的 Python 脚本。
data_download.py get_ticker_data(…)
通过 API 调用下载数据并保存到 CSV 文件。我使用 Yahoo Finance API 下载了数据。
get_dataset_tags(…)
版本控制数据并添加标签。

在上传到 Azure 之前生成的数据集标签。
save_to_data_upload(…)
将数据集规格写入 Azure yml 文件,以便上传到 Azure 数据存储。
对于代码,你可以参考我的代码库。
data_upload.yml
这个 yml 文件由jobs/data_download.save_to_data_upload(…) 更新。这个 yml 文件将数据集上传到 Azure。
$schema: https://azuremlschemas.azureedge.net/latest/data.schema.json
type: uri_file
name: 'WIPRO'
description: Stock data for WIPRO.NS during 2022-02-14:2023-02-13 in 1d interval.
path: '../data/WIPRO.NS.csv'
tags: {'Length': 249, 'Start': '2022-02-14', 'End': '2023-02-13', 'Median': 413.7, 'SD': 69.09}
version: 20230215
deploy.yml
这个 yml 文件在每次 cron 作业前由jobs/update_training_yamls.py 更新。
$schema: https://azuremlschemas.azureedge.net/latest/managedOnlineDeployment.schema.json
name: green
endpoint_name: ga-deployment
model: azureml:GA_model:10
code_configuration:
code: ../jobs
scoring_script: deployment.py
environment: azureml:stock-pricing:5
instance_type: Standard_DS1_v2
instance_count: 1
deployment.py
在端点使用的脚本来生成在线预测。有关代码,你可以参考我的代码库。
init()
初始化模型和 PyTorch Lightning 使用的数据模块。
run()
用于提供来自模型的在线预测。
train.py
使用 PyTorch Lightning 和 MLflow 的模型训练脚本。有关代码,你可以参考我的代码库。
train.yml 这个 yml 文件在每次 cron 作业前由jobs/update_training_yamls.py 更新。
$schema: https://azuremlschemas.azureedge.net/latest/commandJob.schema.json
name: ga-run-10
tags:
modeltype: pytorch
code: ../jobs
command: >-
python train.py
--data ${{inputs.data}}
inputs:
data:
type: uri_file
path: azureml:WIPRO@latest
environment: azureml:stock-pricing:4
compute: azureml:computer456
display_name: stock
experiment_name: ga_train_job
description: Training job via Github actions
update_training_yamls.py
更新在每次运行前需要更新的多个组件的版本,如运行 ID、待注册的模型版本、待部署的模型。有关代码,你可以参考我的代码库。
结论
我希望你现在对如何使用 MS Azure 实现自动化端到端的 MLOps 项目有了了解。有关详细的代码实现,你可以参考我的代码库。
实用的提示工程
原文:
towardsdatascience.com/practical-prompt-engineering-74e96130abc4
成功提示 LLMs 的技巧和窍门…
·发表于Towards Data Science ·15 分钟阅读·2023 年 7 月 30 日
--

(照片由Jan Kahánek提供,来源于Unsplash)
由于其文本到文本的格式,大型语言模型(LLMs)能够用一个模型解决各种任务。这种能力最初通过像GPT-2和GPT-3这样的模型的零样本和少样本学习得到了展示[5, 6]。然而,当经过微调以符合人类的偏好和指示时,LLMs 变得更具吸引力,推动了流行的生成应用,如编码助手、信息寻求对话代理和基于聊天的搜索体验。
由于其能够实现的应用,LLMs 在研究界和大众文化中迅速崛起。在这一过程中,我们还见证了一个新的、互补的领域的出现:提示工程。从高层次看,LLMs 通过 i) 以文本(即提示)作为输入和 ii) 生成文本输出,从中提取有用信息(例如,分类、摘要、翻译等)。这种方法的灵活性非常有利。然而,我们必须确定如何正确构造输入提示,以便 LLM 能够生成所需的输出。
提示工程是一门实证科学,研究不同的提示策略如何优化 LLM 性能。尽管存在各种方法,我们将通过本概述构建对提示的一般机制的理解,以及一些基本的(但极其有效的!)提示技术,如零样本/少样本学习和指令提示。在此过程中,我们将学习实用技巧,并获得可立即采纳的要点,以成为更有效的提示工程师和 LLM 从业者。

(作者创建)
理解大型语言模型(LLMs)。 由于本文重点讨论提示技术,这里不会解释语言模型的历史或机制。为了更好地理解语言模型(这是深入理解提示的一个重要前提),我写了一些概述,供大家参考。以下是这些概述的列表(按重要性排序):
提示一览

语言模型可以使用其通用的文本到文本格式解决各种任务(来自 [1])
鉴于当前对 LLM 的热度,我们可能会问自己:LLM 的基本优势是什么,使它们如此强大? 尽管这个问题没有单一的答案(例如,模型规模,大规模预训练数据,人类反馈 等),LLM 的一个主要优势是其通用的文本到文本格式。这些模型擅长 下一个词预测,因此通过正确调整和利用这一技能可以解决许多不同的任务!
要解决一个任务,我们需要做的就是 i) 向模型提供包含相关信息的文本输入,并 ii) 从模型返回的文本中提取输出。这样统一的方法可以用于翻译、总结、问答、分类等。然而,事情并不(完全)那么简单。即,提供给 LLM 的提示(即输入文本)的措辞和结构可以显著影响模型的准确性。换句话说,提示工程非常重要。
什么是提示工程?
“提示工程是一个相对较新的学科,旨在开发和优化提示,以高效使用 LMs 处理各种应用和研究主题。” — 来自 [2]
由于正确制作提示内容对与 LLM 获得有用结果至关重要,提示工程在最*几个月引起了很多关注。然而,这是一门经验科学——发现最佳提示通常是基于启发式的,需要实验。我们可以通过 跟踪和版本管理我们的提示,测试不同的想法,来发现更好的提示。

用指令来提示 LLM(由作者创建)
提示的组成部分。 提示可以创建的选项有很多种。然而,大多数提示由相同的几个(可选)组件组成:
-
输入数据:这是 LLM 预计处理的实际数据(例如,要翻译或分类的句子,要总结的文档等)。
-
示例:展示 LLM 正确行为的最佳方法之一是提供几个具体的输入-输出对作为提示。
-
指令:我们可以通过指令在提示中用文字描述要做的事情,而不是展示具体的正确行为示例;见上文。
-
指标:以固定且可预测的结构向 LLM 提供输入是有帮助的,因此我们可以通过使用指标来分隔提示的不同部分;见下文。
-
上下文:除了上述描述的组件,我们可能还希望以某种方式向 LLM 提供额外的“上下文”或信息。

指标可以用来以多种方式结构化提示(由作者创建)
一般提示。 提示工程的细节根据所使用的模型和我们试图解决的任务差异很大。然而,有一些普遍接受的提示工程原则是有帮助的,要牢记这些原则 [1, 3]。
-
从简单开始:从简单的提示开始,然后逐步修改提示,同时跟踪经验结果。
-
直接:如果我们希望 LLM 匹配特定的风格或格式,我们应当明确而直接地说明。准确地说出你想要什么可以传达信息。
-
具体性:模糊性是每个提示工程师的敌人。我们应当使提示详细且具体,但不要过度提供过长的输入(即,提示的长度有 限制!)。
-
示例是强大的:如果描述我们想要的内容很困难,提供几个不同输入的正确输出或行为的具体示例可能会很有用。

语言模型的上下文窗口可视化(由作者创建)
上下文窗口。 当我们考虑不同的提示技巧和方法时,我们需要记住我们只能在提示中包含有限的数量的信息。所有 LLM 都有一个预定义的上下文窗口,设定了可以一次处理的总令牌数(即文本序列中的单词或子词)的限制。上下文窗口的大小在模型之间有所不同,但目前有强烈的推动力来增加上下文窗口的大小。例如,GPT-4 的上下文窗口为 32K 令牌,比 OpenAI 之前的任何模型都要 大 4 倍。
常见的提示技术

零样本和少样本学习的出现(来自 [4, 5, 6])
尽管 LLM 由于像ChatGPT这样的热门模型最*经历了爆炸性增长,但提示技术已经存在了相当一段时间。最初,像GPT [4]这样的模型经过微调以解决下游任务。随着GPT-2 [5]的提出,我们看到研究人员开始使用零样本学习来用单一的基础模型解决多个下游任务。最后,GPT-3 向我们展示了随着模型规模的增大,语言模型在少样本学习方面变得非常出色。在本节中,我们将深入探讨这些思想,以更好地了解零样本和少样本学习的工作原理,并提供一些更复杂的提示技术的细节。
零样本学习

(来自 [6])
零样本学习的基本思想相当简单。我们只需将任务描述和相关的输入数据提供给 LLM,并让它生成结果;见上文。由于观察到的大量预训练数据,LLM 通常非常有能力以这种方式解决任务。也就是说,它们可以利用其知识库来解决(相对)大量的任务;请见下文示例(由GPT-3.5生成)。

零样本学习与 GPT-3.5(作者创建)
零样本学习被像 GPT-2 这样的模型广泛探索,并在某些情况下表现良好。然而,如果零样本学习无法解决我们的任务,我们应该怎么做? 在许多情况下,我们可以通过提供更具体和明确的信息来大幅提高 LLM 的性能。特别是,我们可以开始在提示中添加期望输出的示例,让模型能够复制从提示中看到的数据模式。
少样本学习
除了任务描述之外,我们还可以通过高质量的输入输出示例来增强我们的提示。这种技术形成了少样本学习的基础,少样本学习试图通过提供明确的正确行为示例来提高大型语言模型(LLM)的性能。如果使用得当并应用于正确的模型,少样本学习非常有效,这一点通过如GPT-3 [6]等 LLM 的突破性能力得到了证明;请见下文。

(来自 [3])
然而,学习如何正确利用大型语言模型的少量示例学习能力可能很复杂。我们应该在提示中包含哪些示例?是否有正确的提示结构方式?提示的变化是否会显著影响大型语言模型?
大多数大型语言模型对提示的构造方式很敏感,这使得提示工程既困难又重要。尽管像 GPT-4 这样的最新模型似乎对提示中的小扰动不那么敏感 [2],但研究社区 [7] 给我们提供了一些有关如何正确使用少量示例学习的提示,这些提示仍然有助于理解:
-
示例的排序很重要,打乱少量示例的顺序可能会显著改变大型语言模型的表现。增加更多少量示例并不能解决这个问题。
-
少量示例中的标签分布很重要,应与实际的数据分布相匹配。令人惊讶的是,标签的正确性并不是那么重要。
-
大型语言模型倾向于重复最后一个少量示例(即*期效应)。
-
包含在提示中的示例应该是多样化且随机排序的。
最佳数据采样。 选择多样化、随机排序且与测试示例相关的示例是最好的。除此之外,还进行了大量研究,以确定如何为提示选择最佳示例。例如,少量学习样本可以通过多样性选择 [8]、基于不确定性的选择 [9],甚至是根据与测试示例的相似性进行选择 [10]。

(来源 [3])
少量学习 vs. 微调。 在继续之前,我想解决一个显著的 混淆点。少量学习不是微调。 少量学习是在提示中向大型语言模型提供示例,这些示例可以作为生成正确输出的相关上下文。这个过程被称为“上下文学习”;见上文。模型的参数不会通过少量学习进行修改。相比之下,微调明确地训练模型(即通过反向传播更新其权重)在选定的数据集上。
指令提示

使用调整过指令的语言模型作为编码助手(来源 [15])
少量学习非常强大,但它有一个显著的缺点:示例消耗大量的标记。由于大型语言模型的上下文窗口是有限的,我们可能需要探索不会消耗太多标记的提示方法。例如,我们能否通过文字解释正确的行为给大型语言模型? 简短的回答是可以!这种技术,即将书面指令作为提示的一部分,包括在内,被称为指令提示,它在特定类型的大型语言模型上表现最佳。
指令调优与对齐。 最*,语言模型的发展主要集中在提高指令跟随能力上。预训练的 LLM 在开箱即用时并不擅长遵循指令。然而,教会这些模型如何遵循指令能使它们更好地完成用户的需求(即,改善人类对齐)。遵循指令的 LLM 支持多种有用的应用,从信息检索对话代理(例如,ChatGPT)到编码助手(例如,Codex[13]);见下文。

(见[13, 14])
正如在之前的帖子中广泛讨论的那样,创建 LLM 的第一步是使用语言建模目标在大规模的未标记文本语料库上进行预训练。在此过程中,模型获取信息并学会准确地进行下一个词预测。然而,模型的输出并不总是有趣、引人入胜或有帮助,并且模型通常难以遵循复杂的指令。为了鼓励这种行为,我们需要超越基本的预训练。
创建遵循指令的 LLM。 有几种不同的方法可以教会 LLM 如何遵循指令。例如,我们可以进行指令调优[12],或者在包含指令的对话示例上微调 LLM。一些显著的模型采用了这种方法,比如LLaMA(及其变体)[15]、所有 FLAN 模型[12]、OPT-IML[16]等。或者,我们可以使用由监督微调(SFT)和来自人类反馈的强化学习(RLHF)组成的三步法;见下文。这种方法已经创造出了令人惊叹的模型,如 ChatGPT、GPT-4、Sparrow[17]等。

基于人类反馈对 LLM 进行对齐(见[13])
制定有用的指令。 如果我们能够访问到一个已经训练来遵循指令的 LLM,我们可以通过给模型提供有用和信息丰富的指令来完成很多工作。以下是一些使用指令提示的关键技巧和想法:
-
就像我们提示的其余部分一样,指令应该是具体和详细的。
-
我们应避免在提示中告诉 LLM 不要做某事。相反,我们应该专注于告诉 LLM 要做什么。
-
使用具有清晰指示的输入结构来识别提示中的指令是非常有帮助的;见下文。

指令提示的不同格式(由作者创建)
角色提示。 另一种与指令提示相关的有趣提示技术是角色提示,它给模型分配一个“角色”或人格。这个角色在提示中通过一个文本片段被分配,例如:
-
你是一位著名且杰出的数学家。
-
你是一名医生。
-
你是一个音乐专家。
有趣的是,最*的 LLM 能够在对话中很好地承担和维持这些角色[18];见下文。

角色提示与 LaMDA(来自[18])
更进一步,角色提示不仅仅是一个有趣的技巧。给 LLM 提供一个角色实际上可以提高性能(例如,将 GPT-3 角色提示为“杰出的数学家”可以提高性能 在基于算术的问题上)。然而,角色提示仅在某些情况下能提高性能。
“当给 AI 分配一个角色时,我们给了它一些背景信息。这个背景帮助 AI 更好地理解问题。理解问题越好,AI 通常会给出更好的答案。” — 来自 learnprompting.org
现实世界中的指令提示。 用指令提示 LLMs 是一个非常强大的工具,我们可以用它来处理各种应用。为了理解如何利用这一技术,我们可以看看最*发布的ChatGPT 插件,其中包括一个开源的信息检索 API。在这个 API 内部,提供了两个特定的模块用于从文档中提取元数据和过滤个人身份信息(PII)。有趣的是,这些服务完全基于 LLM,并使用如下所示的提示。

用于从 ChatGPT 信息检索 API 中提取元数据和检测个人身份信息(由作者创建)
在这些提示中,LLM 获得了有关如何执行期望任务的具体和详细指令。指令的一些显著方面包括:
-
期望的输出格式(无论是 json 还是 true/false)明确说明。
-
指令使用结构化格式(即,以项目符号分隔的列表)来描述重要信息。
-
LLM 的任务(即识别 PII 或提取元数据)在提示中明确说明。
-
有趣的是,这些提示多次告诉模型不要做什么,这通常是不被建议的。
鉴于 LLMs 的局限性,信任 LLM 准确执行如 PII 检测等关键任务可能不是最佳选择。不过,这种方法展示了指令提示的巨大潜力。与其编写整个程序或服务,我们或许可以通过编写一个提示来快速解决许多任务。
重点
“为聊天机器人角色编写一个真正出色的提示是一个极具杠杆效应的技能,并且是用一点自然语言编程的早期示例” — Sam Altman
如果我们从这次概述中学到的其他东西,我们应该知道构造正确的提示(即提示工程)是成功利用 LLMs 的关键部分。由于语言模型的文本到文本结构,它们非常通用,可以用来解决各种任务。然而,我们必须为这些模型提供详细和适当的上下文,以便它们表现良好。尽管最佳提示技术因模型和任务而异,但有许多高层次的要点可以帮助我们最大化成功的机会。
从零到少样本学习。 由于其广泛的预训练(以及如今的微调)数据集,大型语言模型(LLMs)包含大量信息,并能够开箱即用地解决各种任务。为此,我们仅需向模型提供任务描述和相关输入数据,然后模型会生成正确的输出。然而,由于提供给模型的上下文有限,零样本学习的效果有限。为了提升零样本学习的表现,我们应该通过在提示中插入示例来利用少样本学习。
指令跟随 LLMs。 尽管表现良好,少样本学习通常会消耗大量的 tokens,这在大多数 LLMs 的上下文窗口有限的情况下是个问题。为了应对这一点,我们可以采用一种指令提示的方法,提供 LLM 期望行为的精确文本描述,而不是通过正确输出的具体示例来捕捉这种行为。指令提示是强大的,但它需要一种经过特定形式微调的 LLM(例如,通过指令调优或 RLHF)才能良好工作。预训练的 LLMs 在开箱即用时并不擅长跟随指令。
技巧和窍门。 提示工程有各种技巧和最佳实践可以采纳。通常,这些技巧会随着每次新模型发布而变化(例如,GPT-4 在处理非结构化提示方面比之前的模型 [2] 更加出色),但一些原则已经适用了一段时间。首先,我们应该始终从简单的提示开始,然后逐渐增加复杂性。在发展我们的提示时,我们应当尽量具体和详细,同时避免过于冗长(由于有限的上下文窗口)。最后,为了真正最大化 LLM 的性能,我们通常需要利用少样本学习、指令提示或 更复杂的方法。
结束语
非常感谢阅读这篇文章。我是 Cameron R. Wolfe, Rebuy 的 AI 总监。我研究深度学习的实证和理论基础。你也可以查看我在 medium 上的 其他文章!如果你喜欢这篇文章,请关注我的 twitter 或订阅我的 Deep (Learning) Focus 时事通讯,在这里我通过对流行论文的易懂概述,帮助读者深入理解 AI 研究中的主题。
参考文献
[1] Raffel, Colin 等。“使用统一的文本到文本变换器探索迁移学习的极限。” 机器学习研究杂志 21.1 (2020):5485–5551。
[2] Saravia, Elvis 等。“提示工程指南”, github.com/dair-ai/Prompt-Engineering-Guide (2022)。
[3] Weng, Lilian. (2023 年 3 月)。提示工程。Lil’Log。 lilianweng.github.io/posts/2023-03-15-prompt-engineering/.
[4] Radford, Alec 等。“通过生成性预训练提高语言理解。” (2018)。
[5] Radford, Alec 等。“语言模型是无监督的多任务学习者。”
[6] Brown, Tom 等。“语言模型是少样本学习者。” 神经信息处理系统进展 33 (2020):1877–1901。
[7] Tony Z. Zhao, Eric Wallace, Shi Feng, Dan Klein 和 Sameer Singh. 2021 年。使用前的校准:提高语言模型的少样本性能。ICML。
[8] Su, Hongjin 等。“选择性标注使语言模型成为更好的少样本学习者。” arXiv 预印本 arXiv:2209.01975 (2022)。
[9] Diao, Shizhe 等。“使用思维链的主动提示用于大型语言模型。” arXiv 预印本 arXiv:2302.12246 (2023)。
[10] Liu, Jiachang 等。“什么样的上下文示例对 GPT-$3 $? 有效。” arXiv 预印本 arXiv:2101.06804 (2021)。
[11] Wei, Jason 等。“思维链提示在大型语言模型中引发推理。” arXiv 预印本 arXiv:2201.11903 (2022)。
[12] Wei, Jason, 等. “微调语言模型是零样本学习者。” arXiv 预印本 arXiv:2109.01652 (2021)。
[13] Chen, Mark, 等. “评估训练于代码上的大型语言模型。” arXiv 预印本 arXiv:2107.03374 (2021)。
[14] Ouyang, Long, 等. “通过人类反馈训练语言模型以遵循指令。” 神经信息处理系统进展 35 (2022): 27730–27744。
[15] Touvron, Hugo, 等. “Llama:开放且高效的基础语言模型。” arXiv 预印本 arXiv:2302.13971 (2023)。
[16] Iyer, Srinivasan, 等. “OPT-IML:通过广义化视角扩展语言模型指令元学习。” arXiv 预印本 arXiv:2212.12017 (2022)。
[17] Glaese, Amelia, 等. “通过针对性的人工判断改善对话代理的对齐。” arXiv 预印本 arXiv:2209.14375 (2022)。
[18] Thoppilan, Romal, 等. “Lamda:对话应用的语言模型。” arXiv 预印本 arXiv:2201.08239 (2022)。
实用 Python:spaCy 在 NLP 中的应用
原文:
towardsdatascience.com/practical-python-spacy-for-nlp-b9d626cf53ed
高效的 Python 编程
自然语言处理入门指南
·发表于 Towards Data Science ·12 分钟阅读·2023 年 1 月 9 日
--
spaCy Python 库是一个流行的自然语言处理(NLP)工具。它旨在帮助开发者构建处理和“理解”大量文本的应用程序。spaCy 配备了先进的分词、解析和实体识别功能。它还支持多种流行语言。spaCy 在运行时速度快、效率高,使其成为构建生产级 NLP 应用的良好选择。spaCy 的一个重要部分是其创建和使用特定 NLP 任务的自定义模型的能力,例如命名实体识别或词性标注。开发者可以使用特定于其应用程序的数据进行微调,以满足特定用例的需求。
目录
· 概述
· NLP 和 spaCy 介绍
· 安装和设置 spaCy
· 使用 spaCy 的基本 NLP:分词和词性标注
· 高级 NLP 与 spaCy:NER 和依存句法分析
· 在 spaCy 中处理大型语料库和自定义模型
· 高级 spaCy 技巧:文本分类和词向量
· spaCy 实践
∘ spaCy 和深度学习
∘ spaCy 的功能示例
· 总结:进一步资源和下一步
· 联系
概述
我们介绍了使用 spaCy 进行 NLP 的基础知识,包括分词和词性标注,以及更高级的主题,如命名实体识别和依存句法分析。指南还涉及了处理大型数据集、自定义模型以及文本分类和词向量等高级技巧。最后,它还提供了一些进一步资源的建议以及希望继续学习 spaCy 的 NLP 的下一步。

博客的词云。作者通过www.wordclouds.com/生成。
NLP 和 spaCy 简介
自然语言处理(NLP)是计算机科学和人工智能的一个领域,致力于使计算机能够理解和处理人类语言(图 2)。NLP 有许多应用,包括机器翻译、文本分类、情感分析和聊天机器人开发。

图 2. NLP 是一个交叉 AI、计算机科学和语言学的话题。视觉效果由作者创建。
在 NLP(自然语言处理)领域中,最受欢迎的 Python 库之一是 spaCy:这是一个开源库,旨在帮助开发者构建能够以速度和效率处理大量文本的应用程序,使其成为构建生产级 NLP 应用程序的不错选择。
安装和设置 spaCy
要安装和设置 spaCy,你必须在计算机上安装 Python 和 pip,即 Python 包管理器。如果你还需要获取 Python 和 pip,你可以从官方网站下载和安装它们。
www.python.org/?source=post_page-----b9d626cf53ed-------------------------------- [## 欢迎访问 Python.org
Python 编程语言的官方网站
一旦你安装了 Python 和 pip,你可以使用 pip 来安装 spaCy。首先,打开终端或命令提示符并输入以下命令:
pip install spacy
这将安装 spaCy 的最新版本及其所有依赖项。
安装后,你必须下载一个 spaCy 的预训练语言模型。spaCy 包括几种语言的模型,包括英语、德语、法语和西班牙语。例如,要下载英语模型,你需要输入以下命令:
python -m spacy download en_core_web_sm
这将下载小型英语模型,其中包括基本的 NLP 功能,如分词、词性标注和依存解析。如果你需要更多高级功能,你还可以下载包含额外功能的大型模型,如命名实体识别和词向量。
基本的 NLP 与 spaCy:分词和词性标注
NLP 中最基本的任务之一是分词,即将文本字符串拆分成单独的标记(词汇和标点符号)。分词在许多 NLP 管道中至关重要,因为它使我们能够将特定的单词和标点符号作为离散的单元进行处理。
spaCy 通过其nlp对象和Doc类使得分词变得容易。要使用 spaCy 对文本字符串进行分词,你可以将字符串传递给nlp对象:
import spacy
nlp = spacy.load("en_core_web_sm")
doc = nlp("This is a sentence.")
返回的Doc对象包含对象的标记,你可以通过text访问。例如,你可以像这样迭代这些标记:
for token in doc:
print(token.text)
这将输出输入字符串中的单个标记:
This
is
a
sentence
除了分词之外,另一个常见的 NLP 任务是词性标注(POS tagging),即标记每个词语的对应词性。词性标注通常用于帮助消除词义的歧义,并识别句子的基本语法结构。
spaCy 使得通过 Token 对象的 pos_ 属性执行词性标注变得简单。例如,你可以遍历 Doc 对象中的词语,并像这样打印出它们的词性标签:
for token in doc:
print(token.text, token.pos_)
这将输出词语及其词性标签:
This DET
is VERB
a DET
sentence NOUN
spaCy 包含了大量的词性标签,这些标签在所有支持的语言中是一致的。你可以在 spaCy 文档中找到完整的词性标签列表。
在下一节中,我们将探讨如何使用 spaCy 执行更高级的 NLP 任务,如命名实体识别和依存句法分析。
使用 spaCy 进行高级 NLP:命名实体识别和依存句法分析

作者生成的图像。
在前一节中,我们介绍了使用 spaCy 的分词和词性标注的基础知识。在这一节中,我们将探讨两个可以使用 spaCy 执行的更高级的 NLP 任务:命名实体识别和依存句法分析。
命名实体识别(NER)识别并分类文本中的命名实体,如人名、组织名和地点名。NER 通常用于从非结构化文本中提取结构化信息,并且可以成为信息提取和实体链接等任务的宝贵工具。
spaCy 包含了一个预训练的 NER 模型,可以识别和分类文本中的命名实体。要使用 NER 模型,你可以遍历 Doc 对象中的实体,并打印出它们的文本和标签:
for ent in doc.ents:
print(ent.text, ent.label_)

spaCy 包含几个预定义的实体类型,如 PERSON、ORG 和 GPE(地缘政治实体)。你可以在 spaCy 文档中找到完整的实体类型列表。
依存句法分析是分析句子的语法结构并确定单词之间的依赖关系。依存句法分析常用于句子的主要主语和宾语,并且可以成为总结和问答等任务的有用工具。
spaCy 包含一个依存句法分析器,可以用来分析句子的语法结构。要使用依存句法分析器,你可以遍历 Doc 对象中的词语,并打印出词语文本、依存标签和头词:
for token in doc:
print(token.text, token.dep_, token.head.text)
这将输出词语、它们的依存标签以及依存关系的头词:
This nsubj is
is ROOT is
a attr a
sentence dobj is
spaCy 包含几个依存标签,这些标签描述了词语与其头词之间的语法关系。你可以在 spaCy 文档中找到完整的依存标签列表。
在下一节中,我们将探讨如何使用 spaCy 处理大数据集并为特定的 NLP 任务构建自定义模型。
在 spaCy 中处理大规模语料库和自定义模型
在前面的章节中,我们涵盖了使用 spaCy 进行分词、词性标注、命名实体识别和依存解析的基础知识。本节将探讨两个更高级的话题:处理大数据集和构建自定义模型。
在处理大数据集时,将整个数据集加载到内存中通常是不切实际的。幸运的是,spaCy 提供了几种工具和技术来以流式方式处理大数据集。
一种方法是使用 spaCy 的 nlp.pipe 方法,它允许你在小批量中处理大数据集。nlp.pipe 方法接受一个文本文档的可迭代对象,并返回处理后的 Doc 对象。例如,你可以像这样使用 nlp.pipe 处理大数据集:
import spacy
nlp = spacy.load("en_core_web_sm")
with open("large_dataset.txt") as f:
for doc in nlp.pipe(f):
# Process the doc here
print(doc)
nlp.pipe 方法旨在高效且节省内存,因此非常适合处理大数据集。
除了处理大数据集外,spaCy 还允许你为特定的自然语言处理任务构建自定义模型。例如,你可以使用 spaCy 的 train 方法在标记数据集上微调一个预训练的模型。这在构建适用于特定领域或语言的模型时非常有用。
要使用 spaCy 训练自定义模型,你必须准备一个训练数据集和配置文件。训练数据集应该是 (text, annotations) 元组的列表,其中 text 是一段文本,annotations 是命名实体注释的字典。配置文件应该指定模型类型、训练数据和配置,以及训练模型的输出目录。
准备好训练数据和配置文件后,你可以使用 spacy.train 方法来训练模型:
import spacy
nlp = spacy.blank("en") # Create a blank English model
# Load the training data and configuration
train_data = [("This is a text.", {"entities": [(4, 10, "ENTITY")]})]
config = {"iterations": 100}
# Train the model
nlp.train(train_data, config)
# Save the trained model to a directory
nlp.to_disk("/model_output_dir")
这让你了解了如何使用 spaCy 处理大数据集和构建自定义模型。接下来的章节将介绍一些高级的 spaCy 技巧,如文本分类和词向量。
高级 spaCy 技巧:文本分类和词向量
高级 spaCy 技巧:文本分类和词向量
在前面的章节中,我们涵盖了使用 spaCy 进行自然语言处理任务的基础知识,如分词、词性标注、命名实体识别和依存解析。本节将探讨两种可以使用 spaCy 执行的更高级的技术:文本分类和词向量。
文本分类是根据文本内容为文本分配标签。文本分类是自然语言处理中的常见任务,可用于各种应用,如情感分析和垃圾邮件检测。
spaCy 包含了几个实用的结构,可用于进行文本分类。一个方法是使用 spaCy 内置的Thinc库的线性模型训练支持。要使用 spaCy 训练文本分类器,你必须准备一个训练数据集和一个配置文件。训练数据集应为(text, label)元组的列表,其中text是文本字符串,label是目标标签。配置文件应指定模型类型、训练数据和配置,以及训练模型的输出目录。
一旦你准备好训练数据配置文件,就可以使用spacy.train方法来训练分类器:
import spacy
nlp = spacy.blank("en") # Create a blank English model
# Load the training data and configuration
train_data = [("This is a positive text.", {"cats": {"POSITIVE": 1}}),
("This is a negative text.", {"cats": {"NEGATIVE": 1}})]
词向量是词的数值表示,能够捕捉数据集中词的意义和关系。词向量常用于自然语言处理(NLP),用于表示词之间的相似性,对于文档分类和机器翻译等任务非常有用。
spaCy 包含许多词向量模型,可用于计算数据集的词向量。要在 spaCy 中使用词向量模型,你需要从 spaCy 网站下载一个预训练模型。
一般而言,spaCy 的所有管道包遵循[lang]_[name]的命名约定。对于 spaCy 的...
例如,要下载en_vectors_web_lg模型,你可以使用以下命令:
python -m spacy download en_core_web_lg
一旦你下载了词向量模型对象,可以使用它来计算数据集的词向量。例如,.vector访问一个标记的词向量:
import spacy
nlp = spacy.load("en_core_web_lg")
doc = nlp("This is a sentence.")
for token in doc:
print(token.text, '\n', token.vector)
这将输出标记及其词向量:
This [ 1.6849 1.9826 -0.77743 ... -2.9454 -0.83337 ]
is [ 1.4750e+00 6.0078e+00 1.1205e+00 ... -8.5967e-01 9.7466e+00]
a [ -9.3629 9.2761 -7.2708 ... -6.816 3.5737 ]
sentence [-2.7653e+00 -7.9512e-01 ... 1.0124e+00 1.7035e-01]
. [-0.076454 -4.6896 -4.0431 ... -0.52699 -1.3622 ]
每个向量都是一个固定大小的嵌入。
print(len(token.vector))
# 300
spaCy 和深度学习
spaCy 的一个关键特性是它能够使用卷积神经网络(CNNs)执行深度学习任务。这包括命名实体识别(NER)、词性标注(POS tagging)和依赖解析。
要使用 spaCy 的深度学习特性,你需要安装带有en_core_web_md模型的软件包,其中包括 CNNs。安装后,你可以通过调用 spaCy 对象的方法(如doc和span对象)来访问 CNNs 和其他高级特性。
再次,通过终端下载模型:
python -m spacy download en_core_web_md
现在,使用 spaCy 的深度学习特性执行 NER:
import spacy
nlp = spacy.load("en_core_web_md")
doc = nlp("Apple is looking at buying U.K. startup for $1 billion")
for ent in doc.ents:
print(ent.text, ent.label_)
这将输出以下内容:
Apple ORG
U.K. GPE
$1 billion MONEY
正如你所见,spaCy 的深度学习特性使得用几行代码就能轻松执行高级 NLP 任务。
spaCy 的功能示例
这里是如何使用 spaCy 的多个不同特性来执行高级自然语言处理任务的示例:
import spacy
# Load the spaCy model with the "en_core_web_md" model
nlp = spacy.load("en_core_web_md")
# Process a text
doc = nlp("Apple is looking at buying U.K. startup for $1 billion. The startup specializes in machine learning and artificial intelligence.")
# Iterate over the named entities in the document
for ent in doc.ents:
# Print the text and label of the entity
print(ent.text, ent.label_)
# Extract the first noun phrase in the text
first_noun_phrase = doc[0:2]
print(first_noun_phrase.text)
# Get the part-of-speech tags for the tokens in the text
pos_tags = [token.pos_ for token in doc]
print(pos_tags)
# Extract the dependencies of the tokens in the text
dependencies = [(token.text, token.dep_, token.head.text) for token in doc]
print(dependencies)
# Get the lemma of the first token in the text
first_token_lemma = doc[0].lemma_
print(first_token_lemma)
输出:
Apple ORG
U.K. GPE
$1 billion MONEY
Apple
['PROPN', 'AUX', 'VERB', 'VERB', 'ADP', 'VERB', 'ADP', 'NOUN', 'NUM', 'NOUN']
[('Apple', 'nsubj', 'is'), ('is', 'aux', 'looking'), ('looking', 'ROOT', 'looking'), ('at', 'prep', 'looking'), ('buying', 'pcomp', 'at'), ('U.K.', 'compound', 'startup'), ('startup', 'dobj', 'buying'), ('for', 'prep', 'buying'), ('$1', 'nummod', 'billion'), ('billion', 'pobj', 'for')]
apple
本示例演示了如何使用 spaCy 的多个不同功能,包括命名实体识别、名词短语提取、词性标注、依存解析和词形还原。
总结:进一步的资源和下一步
本教程涵盖了在 Python 中使用 spaCy 进行自然语言处理的基础知识。随后,我们查看了诸如分词、词性标注、命名实体识别、依存解析等日常 NLP 任务,以及更高级的技术,如文本分类和词向量。
如果你想继续学习有关 spaCy 的 NLP,许多资源可以利用。spaCy 文档是一个很好的起点,其中包括有关库的 API 和功能的详细信息。
spaCy 的核心数据结构是类和对象。Language 类用于处理文本…
你还可以在 spaCy 网站上找到许多教程和示例。
spaCy 与 64 位 CPython 3.6+ 兼容,并且可以在 Unix/Linux、macOS/OS X 和 Windows 上运行。最新的 spaCy 版本…
除了 spaCy 文档,还有许多其他资源可以学习 NLP。以下是一些建议:
- Coursera 上的自然语言处理课程:
## 最佳自然语言处理(NLP)课程与认证 [2023] | Coursera
自然语言处理(NLP)是人工智能(AI)领域的一个分支,专注于使计算机能够…
- Steven Bird、Ewan Klein 和 Edward Loper 合著的《Python 自然语言处理》一书:
[## NLTK 书籍
Steven Bird、Ewan Klein 和 Edward Loper 这本 NLTK 书籍的版本“为 Python 3 和 NLTK 3 进行了更新”。第一…
- Dan Jurafsky 和 James H. Martin 合著的《言语与语言处理》一书
我们列出了许多对改进工作提出了大量精彩建议和修复的优秀人士。
这个教程对你有帮助,并为你使用 spaCy 进行 Python NLP 奠定了坚实的基础。祝你的 NLP 项目好运!
未来的博客将深入探讨在 spaCy 中实现的深度学习机制以及各种可视化 NLP 结果的技术。
如果你有任何进一步的问题或需要更多帮助,请告诉我。
联系方式
想要联系?关注罗宾逊博士在LinkedIn、Twitter、Facebook和Instagram。访问我的主页获取论文、博客、邮件订阅等更多信息!
研究员和企业家问候!作为一名研究员,罗宾逊博士提出并运用了先进的 AI 来理解……
实用的探索性数据分析改进技巧
使 EDA 更简单(和更美观)的简短指南
·
关注 发表在 Towards Data Science · 11 min read · 2023 年 8 月 11 日
--
图片由 Sam Dan Truong 提供,来自 Unsplash
介绍
探索性数据分析(EDA)是使用任何机器学习模型之前的必经步骤。EDA 过程需要数据分析师和数据科学家的专注和耐心:在从分析的数据中获得有意义的洞察之前,通常需要花费大量时间积极使用一个或多个可视化库。
在这篇文章中,我将根据个人经验分享一些如何简化 EDA 过程并提高效率的技巧。特别是,我将给出三条在对抗 EDA 过程中学到的重要建议:
-
使用最适合你任务的非*凡图表;
-
充分利用可视化库的功能;
-
寻找更快的方法来完成相同的任务。
注:为了在这篇文章中创建信息图,我们将使用 Kaggle 上的风电生成数据[2]。我们开始吧!
提示 1:不要害怕使用复杂的图表。
当我在处理与风能分析和预测相关的研究论文时,我学会了如何应用这个技巧[1]。在这个项目的 EDA 过程中,我遇到了需要创建一个总结矩阵的问题,这个矩阵能反映风参数之间的所有关系,以便找出它们之间最强的相互影响。第一个想到的想法是建立一个我在许多数据科学/数据分析项目中看到的‘老牌’相关矩阵。
如你所知,相关矩阵用于量化和总结变量之间的线性关系。在以下代码片段中,corrcoef函数用于风电生成数据的特征列。这里我还应用了 Seaborn 的heatmap函数,将相关矩阵数组绘制为热图:
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import numpy as np
# read data
data = pd.read_csv('T1.csv')
print(data)
# rename columns to make their titles shorter
data.rename(columns={'LV ActivePower (kW)':'P',
'Wind Speed (m/s)':'Ws',
'Theoretical_Power_Curve (KWh)':'Power_curve',
'Wind Direction (°)': 'Wa'},inplace=True)
cols = ['P', 'Ws', 'Power_curve', 'Wa']
# build the matrix
correlation_matrix = np.corrcoef(data[cols].values.T)
hm = sns.heatmap(correlation_matrix,
cbar=True, annot=True, square=True, fmt='.3f',
annot_kws={'size': 15},
cmap='Blues',
yticklabels=['P', 'Ws', 'Power_curve', 'Wa'],
xticklabels=['P', 'Ws', 'Power_curve', 'Wa'])
# save the figure
plt.savefig('image.png', dpi=600, bbox_inches='tight')
plt.show()

相关矩阵的示例。图像由作者提供。
分析结果图表后,可以得出风速和有功功率之间有强相关性,但我认为许多人会同意我的观点,即使用这种可视化方式解释结果并不容易,因为这里只展示了数字。
一个良好的替代相关矩阵的方案是散点图矩阵,它允许你在一个地方可视化数据集中不同特征之间的成对相关性。在这种情况下,应该使用 sns.pairplot:
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
# read data
data = pd.read_csv('T1.csv')
print(data)
# rename columns to make their titles shorter
data.rename(columns={'LV ActivePower (kW)':'P',
'Wind Speed (m/s)':'Ws',
'Theoretical_Power_Curve (KWh)':'Power_curve',
'Wind Direction (°)': 'Wa'},inplace=True)
cols = ['P', 'Ws', 'Power_curve', 'Wa']
# build the matrix
sns.pairplot(data[cols], height=2.5)
plt.tight_layout()
# save the figure
plt.savefig('image2.png', dpi=600, bbox_inches='tight')
plt.show()

散点图矩阵的示例。图像由作者提供。
通过查看散点图矩阵,可以快速观察数据的分布情况以及是否包含异常值。然而,这种图表的主要缺点与成对绘制数据造成的重复有关。
最后,我决定将上述图表合并为一张,其中左下部分包含所选参数的散点图,右上部分包含不同大小和颜色的气泡:较大的圆圈表示所研究的参数具有更强的线性相关性。矩阵的对角线将显示每个特征的分布:这里的窄峰值表明该参数变化不大,而其他特征则变化较大。
构建此总结矩阵的代码如下。这里的图包括三部分——fig.map_lower、fig.map_diag、fig.map_upper:
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
# read data
data = pd.read_csv('T1.csv')
print(data)
# rename columns to make their titles shorter
data.rename(columns={'LV ActivePower (kW)':'P',
'Wind Speed (m/s)':'Ws',
'Theoretical_Power_Curve (KWh)':'Power_curve',
'Wind Direction (°)': 'Wa'},inplace=True)
cols = ['P', 'Ws', 'Power_curve', 'Wa']
# buid the matrix
def correlation_dots(*args, **kwargs):
corr_r = args[0].corr(args[1], 'pearson')
ax = plt.gca()
ax.set_axis_off()
marker_size = abs(corr_r) * 3000
ax.scatter([.5], [.5], marker_size,
[corr_r], alpha=0.5,
cmap = 'Blues',
vmin = -1, vmax = 1,
transform = ax.transAxes)
font_size = abs(corr_r) * 40 + 5
sns.set(style = 'white', font_scale = 1.6)
fig = sns.PairGrid(data, aspect = 1.4, diag_sharey = False)
fig.map_lower(sns.regplot)
fig.map_diag(sns.histplot)
fig.map_upper(correlation_dots)
# save the figure
plt.savefig('image3.jpg', dpi = 600, bbox_inches = 'tight')
plt.show()

总结矩阵的示例。图片由作者提供。
总结矩阵结合了之前研究的两种图表的优点——它的下半部分(左侧)模拟散点图矩阵,上半部分(右侧)图形化地反映了相关矩阵的数值结果。
提示 2:充分利用可视化库的功能
我时常需要向同事和客户展示 EDA 的结果,因此可视化是我在这项任务中重要的助手。我总是尽量在图表中添加各种元素,如箭头和注释,以使其更具吸引力和可读性。
让我们回到前面讨论的风电项目的 EDA 实施案例。当谈到风能时,功率曲线是最重要的参数之一。风力涡轮机(或整个风电场)的功率曲线是一个图表,展示了在不同风速下生成的电力。需要注意的是,涡轮机在低风速下不会运行。它们的启动与切入速度相关,通常在 2.5–5 m/s 的范围内。风速在 12 到 15 m/s 之间时,达到额定功率。最后,每台涡轮机都有一个上限风速,当风速超过此限值时,涡轮机将停止发电,直到风速降回到操作范围内。
研究的数据集包括理论功率曲线(这是来自制造商的典型曲线,没有任何离群点)和实际曲线(如果我们绘制风力与风速的关系,后者通常包含许多超出理想理论形状的点,这可能是由于涡轮机故障、不正确的 SCADA 测量或计划外维护所致)。
现在我们将创建一张图像,展示两种类型的功率曲线——首先是没有任何额外项(除了图例)的:
import pandas as pd
import matplotlib.pyplot as plt
# read data
data = pd.read_csv('T1.csv')
print(data)
# rename columns to make their titles shorter
data.rename(columns={'LV ActivePower (kW)':'P',
'Wind Speed (m/s)':'Ws',
'Theoretical_Power_Curve (KWh)':'Power_curve',
'Wind Direction (°)': 'Wa'},inplace=True)
# build the plot
plt.scatter(data['Ws'], data['P'], color='steelblue', marker='+', label='actual')
plt.scatter(data['Ws'], data['Power_curve'], color='black', label='theoretical')
plt.xlabel('Wind Speed')
plt.ylabel('Power')
plt.legend(loc='best')
# save the figure
plt.savefig('image4.png', dpi=600, bbox_inches='tight')
plt.show()

一张‘静默’的风力功率曲线图。图片由作者提供。
如你所见,这张图需要解释,因为它没有包含任何额外的细节。
但如果我们添加线条来突出显示图表的三个主要区域——标记了切入、额定和切出速度的区域,以及一个箭头注释来展示一个离群点会怎样呢?
让我们看看在这种情况下图表会是什么样子:
import pandas as pd
import matplotlib.pyplot as plt
# read data
data = pd.read_csv('T1.csv')
print(data)
# rename columns to make their titles shorter
data.rename(columns={'LV ActivePower (kW)':'P',
'Wind Speed (m/s)':'Ws',
'Theoretical_Power_Curve (KWh)':'Power_curve',
'Wind Direction (°)': 'Wa'},inplace=True)
# build the plot
plt.scatter(data['Ws'], data['P'], color='steelblue', marker='+', label='actual')
plt.scatter(data['Ws'], data['Power_curve'], color='black', label='theoretical')
# add vertical lines, text notes and arrow
plt.vlines(x=3.05, ymin=10, ymax=350, lw=3, color='black')
plt.text(1.1, 355, r"cut-in", fontsize=15)
plt.vlines(x=12.5, ymin=3000, ymax=3500, lw=3, color='black')
plt.text(13.5, 2850, r"nominal", fontsize=15)
plt.vlines(x=24.5, ymin=3080, ymax=3550, lw=3, color='black')
plt.text(21.5, 2900, r"cut-out", fontsize=15)
plt.annotate('outlier!', xy=(18.4,1805), xytext=(21.5,2050),
arrowprops={'color':'red'})
plt.xlabel('Wind Speed')
plt.ylabel('Power')
plt.legend(loc='best')
# save the figure
plt.savefig('image4_2.png', dpi=600, bbox_inches='tight')
plt.show()

一张‘多话’的风力曲线图。图像由作者提供。
提示 3:总是找到更快的方法来完成相同的工作
在分析风数据时,我们通常希望获得有关风能潜力的全面信息。因此,除了风能的动态,还需要一个图表显示风速如何依赖于风向。
为了展示风力的变化,可以使用以下代码:
import pandas as pd
import matplotlib.pyplot as plt
# read data
data = pd.read_csv('T1.csv')
print(data)
# rename columns to make their titles shorter
data.rename(columns={'LV ActivePower (kW)':'P',
'Wind Speed (m/s)':'Ws',
'Theoretical_Power_Curve (KWh)':'Power_curve',
'Wind Direction (°)': 'Wa'},inplace=True)
# resample 10-min data into hourly time measurements
data['Date/Time'] = pd.to_datetime(data['Date/Time'])
fig = plt.figure(figsize=(10,8))
group_data = (data.set_index('Date/Time')).resample('H')['P'].sum()
# plot wind power dynamics
group_data.plot(kind='line')
plt.ylabel('Power')
plt.xlabel('Date/Time')
plt.title('Power generation (resampled to 1 hour)')
# save the figure
plt.savefig('wind_power.png', dpi=600, bbox_inches='tight')
plt.show()
下面是结果图:

风力的动态。图像由作者提供。
如人们可能注意到的那样,风力动力学的轮廓具有相当复杂、不规则的形状。
风玫瑰,或称极坐标玫瑰图,是一种特殊的图表,用于表示气象数据的分布,通常是按方向划分的风速 [3]。matplotlib库中有一个简单的模块windrose,可以轻松构建这种可视化图表,例如:
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
from windrose import WindroseAxes
# read data
data = pd.read_csv('T1.csv')
print(data)
# rename columns to make their titles shorter
data.rename(columns={'LV ActivePower (kW)':'P',
'Wind Speed (m/s)':'Ws',
'Theoretical_Power_Curve (KWh)':'Power_curve',
'Wind Direction (°)': 'Wa'},inplace=True)
wd = data['Wa']
ws = data['Ws']
# plot normalized wind rose in a form of a stacked histogram
ax = WindroseAxes.from_ax()
ax.bar(wd, ws, normed=True, opening=0.8, edgecolor='white')
ax.set_legend()
# save the figure
plt.savefig('windrose.png', dpi = 600, bbox_inches = 'tight')
plt.show()

基于现有数据获得的风玫瑰图。图像由作者提供。
看着风玫瑰图,可以注意到有两个主要的风向——东北和西南。
但是如何将这两张图片合并为一张呢?最明显的选择是使用add_subplot。尽管由于windrose库的特殊性,这不是一个简单的任务:
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
from windrose import WindroseAxes
# read data
data = pd.read_csv('T1.csv')
print(data)
# rename columns to make their titles shorter
data.rename(columns={'LV ActivePower (kW)':'P',
'Wind Speed (m/s)':'Ws',
'Theoretical_Power_Curve (KWh)':'Power_curve',
'Wind Direction (°)': 'Wa'},inplace=True)
data['Date/Time'] = pd.to_datetime(data['Date/Time'])
fig = plt.figure(figsize=(10,8))
# plot both plots as subplots
ax1 = fig.add_subplot(211)
group_data = (data.set_index('Date/Time')).resample('H')['P'].sum()
group_data.plot(kind='line')
ax1.set_ylabel('Power')
ax1.set_xlabel('Date/Time')
ax1.set_title('Power generation (resampled to 1 hour)')
ax2 = fig.add_subplot(212, projection='windrose')
wd = data['Wa']
ws = data['Ws']
ax = WindroseAxes.from_ax()
ax2.bar(wd, ws, normed=True, opening=0.8, edgecolor='white')
ax2.set_legend()
# save the figure
plt.savefig('image5.png', dpi=600, bbox_inches='tight')
plt.show()
在这种情况下,结果如下所示:

一张风力动力学和风玫瑰图的合成图。图像由作者提供。
这里的主要缺点是两个子图的大小不同,因此风玫瑰图周围有很多空白区域。
为了简化操作,我建议采取不同的方法,使用Python Imaging Library(PIL) [4],仅需 11 (!) 行代码:
import numpy as np
import PIL
from PIL import Image
# list images that needs to be merged
list_im = ['wind_power.png','windrose.png']
imgs = [PIL.Image.open(i) for i in list_im]
# resize all images to match the smallest
min_shape = sorted([(np.sum(i.size), i.size) for i in imgs])[0][1]
# for a vertical stacking - we use vstack
images_comb = np.vstack((np.asarray(i.resize(min_shape)) for i in imgs))
images_comb = PIL.Image.fromarray(imgs_comb)
# save the figure
imgages_comb.save('image5_2.png', dpi=(600,600))
这里的输出看起来更美观,因为两张图片大小相同,代码选择了最小的一张,并将其他图片调整为匹配:

一张使用 PIL 获得的风力动力学和风玫瑰图。图像由作者提供。
顺便提一下,在使用PIL时,也可以使用水*堆叠——例如,让我们比较和对比一个‘沉默’的和一个‘多话’的功率曲线图:
import numpy as np
import PIL
from PIL import Image
list_im = ['image4.png','image4_2.png']
imgs = [PIL.Image.open(i) for i in list_im]
# pick the image which is the smallest, and resize the others to match it (can be arbitrary image shape here)
min_shape = sorted([(np.sum(i.size), i.size) for i in imgs])[0][1]
imgs_comb = np.hstack((np.asarray(i.resize(min_shape)) for i in imgs))
### save that beautiful picture
imgs_comb = PIL.Image.fromarray(imgs_comb)
imgs_comb.save('image4_merged.png', dpi=(600,600))

比较和对比两个功率曲线图。图像由作者提供。
结论
在这篇文章中,我与您分享了三个使 EDA 过程更轻松的技巧。希望这些建议对您有所帮助,并且您也能将它们应用到自己的数据任务中。
这些提示完美地匹配了我在进行 EDA 时总是尝试应用的公式:定制 → 列出 → 优化。
好吧,你可能会问,这到底为什么重要?我可以说,实际上这很重要,因为:
-
根据你当前面临的具体需求定制图表是非常重要的。例如,不要创建大量的信息图表,想想如何将几个图表合并成一个,就像我们在创建总结矩阵时一样,这样可以结合散点图和相关图表的优点。
-
你所有的图表都应该能自我说明。因此,你需要知道如何在图表上列出重要信息,使其详细且易于阅读。比较一下“沉默”和“健谈”的功率曲线之间的差异。
-
最后,每个数据专家都应该学习如何优化 EDA 过程以提高效率(让生活更轻松)。如果你需要将两张图像合并为一张,并不一定要一直使用
add_subplot选项。
还有什么呢?我可以肯定地说,EDA 是处理数据时非常有创意和有趣的一步(更不用说它也非常重要)。
让你的信息图表像钻石一样闪耀,不要忘记享受这个过程!
参考列表
-
论文《数据驱动的风能分析与预测应用:“La Haute Borne”风电场的案例》。
doi.org/10.1016/j.dche.2022.100048 -
风力发电数据:
www.kaggle.com/datasets/bhavikjikadara/wind-power-generated-data?resource=download -
关于 windrose 库的教程:
windrose.readthedocs.io/en/latest/index.html
感谢阅读!
“实际”构建图像分类器
以实际方式构建图像分类器(基于真实故事)
·
关注 发表在 Towards Data Science ·10 min read·2023 年 1 月 17 日
--
图片由 Elena Mozhvilo 提供,发布在 Unsplash
大多数从业者首先通过图像分类学习深度学习。如果你上过编程课程,那么可以将这视为他们开始时给你的“hello world”练习。尽管许多专家认为图像分类问题已经解决(这可能有一定的道理),但从这类问题中你仍然可以学到很多细微之处和技巧。话虽如此,在接下来的几个版本中,我将通过讲故事的形式分享我如何构建、评估和分析这些模型。起初,我考虑将所有内容整合到一篇文章中。但是为了让你免于阅读我对托尔斯泰《战争与和*》的回应,我将其分解成若干部分。注意: 本文中使用的所有代码都可以在这里找到。
那么你认为你能进行分类吗?
为了激励这个问题,让我们假设你被聘为 Petpoo Inc 的计算机视觉(CV)工程师,这是一家宠物洗发水公司。现在你可能会问,为什么一家宠物洗发水公司需要一名计算机视觉工程师。更重要的是,为什么他们会叫自己 Petpoo?我将在下面回答第一个问题,但老实说,我不知道第二个问题的答案。
在你的第一天,你的经理见到你并带你熟悉你的项目。
“嗨 INSERT_YOUR_NAME_HERE!我们很高兴你能加入我们。Petpoo 正在扩大规模,我们客户最希望的功能之一是为他们的宠物推荐定制洗发水。我们的客户对社交媒体非常熟悉,希望他们的宠物明星在分享的短视频和自拍中拥有最佳造型。他们也没有时间填写冗长的问卷,因为他们忙着教他们的宠物拍出完美的鸭脸。所以我们正在开发一种基于照片识别宠物的解决方案。一旦我们知道了宠物的品种,我们就可以从我们的产品线中推荐适合的定制洗发水,让宠物和它的主人都开心。”
“由于我们大多数客户拥有猫或狗,我们希望你能构建一个图像分类器,以识别宠物是猫还是狗。作为开始,咱们先保持在这个层次。根据我们上线后的反馈,我们可以为解决方案添加更多功能。祝好运,我们都寄望于你。”
你问:“我们有些什么数据用于原型设计?”
“好问题。你可以从这个数据集开始。”他回复道。
“我们将如何评估性能?”你问道。
“对于原型,使用你最好的判断。”他说完便离开了。
好的,你需要构建一个图像分类器来区分猫和狗。你有一个数据集可以开始使用,还需要弄清楚如何评估原型。
你决定先查看数据。毕竟,如果数据不好,再多的机器学习洗发水也无济于事。
初看起来,这些数据处理得当,你可以立即开始使用。总共有* 7500 张图像,因此应该足够用于微调。数据集包含了特定宠物品种的分类标签,所以你需要对其进行修改,使每张图像都与猫或狗标签相关联。好的,写一个简单的函数来完成这个任务,我们就准备好了。

示例图像和标签:图像由作者创建
所以,猫的图像将有标签“True”,而狗的图像将有标签“False”。
在开始实验之前,你将数据分为训练集和验证集。你足够聪明地知道,简单性总是优于复杂性。因此,作为第一步,你选择了一个知名的深度学习模型——ResNet-18 作为基线。然后,你关闭了所有高级设置,比如调度器、数据增强、预训练权重和正则化,并通过它传递一批数据。当你检查模型的预测时,你看到的是这样的结果:
(TensorBase([0.0056, 0.9944]), TensorBase(1.))
很好,概率加起来为 1。这意味着基本设置是正确的。感到自信,你训练模型几个轮次,并观察发生了什么。损失似乎在减少,但对于这样一个简单的问题和如此干净的数据集,准确率却让人失望。

10 轮训练后的结果(未使用迁移学习):图像由作者创建

训练曲线:图像由作者创建
为了留下良好的第一印象,你可视化了被错误分类的示例,并绘制了混淆矩阵。

分类结果:图像由作者创建

错误分类示例:图像由作者创建
嗯,模型似乎将大量的狗误认为是猫。奇怪,非常奇怪。
你推测这是因为训练示例不足以从头开始训练。因此,你决定使用一种在你的职业生涯中表现良好的技术——迁移学习。
在用预训练权重初始化 Resnet-18 后,你重复了相同的实验,保持其他一切不变。这次的结果看起来很棒。几乎没有错误,训练曲线也非常好。

10 轮训练后的结果(使用迁移学习):图像由作者创建

使用迁移学习的训练曲线:图像由作者创建
你重复之前的可视化,发现只有少量的狗被误认为是猫。

混淆矩阵看起来好多了!图像由作者创建

错误分类示例:图像由作者创建
高兴的是你的新工作顺利开始,你保存了所有工作并在傍晚回家。
很久以后。凌晨 1:30……你被惊醒,汗珠从额头上滑落。
你在设置实验时错过了一个非常重要的细节。准确率可能根本不是这个问题的好指标。
为什么?
第二天,你急忙赶到工作岗位,检查你的直觉是否正确。在检查样本图像并计算数据集的总大小时,由于急于获得结果,你忘记了检查猫和狗的图像数量。数据集最初设计用于分类不同品种的狗和猫。它可能为了这个任务而进行了*衡(每个类别有相似数量的样本)。
你从未检查猫和狗的图像数量是否相似。你在键盘上急速打字,键盘的“哒哒”声后随之而来的沉寂证实了你的怀疑。
猫:2400 张,占总数据的 32.4%
狗:4990 张,占总数据的 67.6%
这解释了为什么你训练的第一个模型效果很差。数据不*衡使模型混淆。数据集中狗的数量是猫的两倍多。模型无法收集到足够的信息来区分猫和狗,因为它看到的大多数图像都是狗。但是为什么你训练的第二个模型几乎完美无瑕?因为你用训练了 120 多万张图像的权重初始化了它,其中许多是狗和猫的图像。
问题解决了!没问题吧?其实不然。迁移学习(你在第二个模型中做的)并不总是有效。当预训练数据集(模型最初训练时的数据)和目标数据集(你正在训练的数据)之间存在领域不匹配时,迁移学习可能会失败。你很幸运,因为预训练数据集与你正在使用的宠物数据集有相似的分布(呼~)。
下面是下一个错误——默认将准确率作为指标。将混淆矩阵并排查看。左侧是你的原始实验,右侧是迁移学习实验。供参考,行是实际情况。第一行对应实际的狗图像,第二行对应实际的猫图像。第一列代表模型认为是狗的图像,第二列代表模型认为是猫的图像。

混淆矩阵并排对比:由作者创建的图像
如果你查看了原始的准确率数字,你会看到在没有迁移学习的情况下准确率为 86%,而在进行迁移学习后准确率为 99.7%。还不错,对吧?
然而,在左侧的混淆矩阵中,你可以看到 44 只狗被模型分类为猫,162 只猫被分类为狗。这是一个你会因盲目依赖准确率而错过的大问题。这是为什么?
想象一下一个最坏的情况,如果你只有 90 张狗的图片和 10 张猫的图片,并且没有预训练的权重。如果你把所有 100 张图片的答案都猜为狗,你的准确率将是 90%。你也会得到一个完全无用的模型。
因此,在另一种情况下,使用准确率作为指标将隐藏模型中的这一根本性缺陷,直到为时已晚。我的意思是,如果哈里斯·皮尔顿这位喜爱小狗的社交名人使用了这个模型,发现它无用,然后在她的社交媒体渠道上谴责了这个产品,那该怎么办?
当你感谢幸运星并整合迁移学习的结果时,你的经理过来问道。“你好啊!你的工作进展如何?”,他问。“还不错,我想我有一些成果。”,你紧张地回答。你向他展示了你的迁移学习成果以及 0.003 的误差率。他感到惊讶。“我们多久可以为此制作一个演示?”,他问。“我可以在本周开始着手准备”,你回答。
“我们的营销团队与顶级网红哈里斯·皮尔顿有联系,我们希望他们尝试一下这个并给我们反馈。”他咧嘴一笑。“嗯……好的”,你吞了吞口水,回忆起你之前的失误。“好的,我们目标是下周末前。快点!”,他说着拍了拍你的肩膀,愉快地走开了。
笔在纸上划过的涂鸦伴随着沉默的停顿。即使你正在设计应用程序,你的思绪却游离在别处。我应该用什么来评估模型?我该如何*衡这个不稳定的数据集?为什么我会遗漏这些基本步骤?
在接下来的几天里,你构建了一个简单的应用程序界面,以测试模型并查看它在真实世界图像上的表现。毕竟,如果你无法将模型用于实际应用,训练它有什么意义呢?
对初步原型感到满意后,你将注意力转向脑海中那些迫切的问题。翻开一本参考教材,你浏览了 F1 分数、ROC、精准度和召回率,所有这些想法涌上心头。你重新运行了实验,但除了测量准确率外,你还测量了精准度和召回率。结果现在更加有意义——在没有迁移学习的模型中,精准度和召回率较低,而准确率掩盖了这些缺陷。另一方面,迁移学习模型的精准度和召回率都非常高。

没有迁移学习——一般的精准度和召回率:由作者创建的图像

有了迁移学习——出色的精准度和召回率:由作者创建的图像
你决定写一个检查清单,说明如何训练模型并评估它——以备你或团队中的其他人在未来需要参考。
但在你开始之前,你的经理过来了。你向他展示了正在开发中的应用,他非常高兴。“这真棒!我们的营销团队与一些影响者进行了交谈,他们希望这个应用能更具个性化——按品种推荐。我试图说服他们,但他们已经承诺在下周末影响者来总部时准备好。你认为到那时能做好吗?我知道这将需要一些艰苦的工程工作。”他说得很歉意。“细粒度分类?到下周末?这要求很高。我可以找个人和我一起工作吗?”你对突然变化的要求和缩短的时间表感到惊讶。“嗯……”他停顿了一下……
(本系列的结论将在 2023 年某个时候发布——仍在前期制作中)
这是我给你的问题:
你会如何处理这个新任务?考虑到这些限制,什么是最佳方法?你需要注意什么?
没有正确的答案,只有权衡。请给我发个消息或在此帖子中留下评论,分享你的想法。
数据集详情:
牛津宠物数据集 — 该数据集可用于商业/研究目的下载,受 知识共享署名-相同方式共享 4.0 国际许可协议 保护。版权归图像的原始所有者所有。
预训练上下文是你所需的一切
原文:
towardsdatascience.com/pre-training-context-is-all-you-need-f457ffa8a358
现代变换模型背后的驱动力在很大程度上来源于其相关数据,从而允许强大的上下文学习能力。
·发布于 Towards Data Science ·6 分钟阅读·2023 年 11 月 27 日
--
生成型人工智能及其流行的变换模型如今随处可见,每小时都有新模型发布(见人工智能的膨胀)。在这个迅速发展的人工智能领域,这些模型可能带来的价值似乎是无穷无尽的。像chatGPT这样的“大型语言模型”(LLM)已经成为每位工程师资源的组成部分,作家们用它们来支持他们的文章,设计师们则用计算机视觉模型的结果来创建初步的视觉效果或寻找灵感。
如果这不是魔法,那究竟是什么驱动了这些令人印象深刻的变换模型?
然而,即使成就和实用性都很出色,生成型人工智能提升了生产力,但重要的是要记住,现代机器学习模型(如 LLM 或 Vision Transformers)并没有进行任何魔法(类似于 ML 或统计模型本身从未有过神奇的事实)。尽管模型的卓越能力可能被视为类似魔法,一些领域专家甚至谈到模型的幻觉,但每个模型的基础仍然只是数学和统计概率(虽然有时复杂,但仍是数学)。这引出了一个根本性的问题:如果这不是魔法,那究竟是什么驱动了这些令人印象深刻的变换模型?

图 1:展示了 ChatGPT(使用 GPT4)将其“先进技术”和“广泛训练”视为主要性能驱动因素。
每个模型的基础是数据
与任何模型(统计模型或机器学习模型)一样,训练数据对模型的最终性能影响最大。如果没有大量高质量的数据来反映你希望模型学习的关系,那么就没有可以训练的内容,最终的模型表现会很差(著名的 GIGO 原则:垃圾进垃圾出)。这一数据建模的基本原则多年来没有改变。每个革命性的新型变换模型背后首先只有一个因素:数据。是数据的数量、质量和上下文决定了模型的后续表现。*期研究(见下文)通过展示最新的生成性人工智能模型在提供的上下文属于训练分布时能够很好地进行泛化,而在异质分布学习中表现不佳来支持这一点。
同质分布与异质分布学习
重要的是要记住,模型不过是一个巨大的网络、树或关系图。机器学习模型基本上学习的是如何将给定的输入转换为所需的输出(见图 2)。

图 2:一个超级简单的神经网络结构图,它根据天气和其他上下文预测人流量。在左侧是训练过程中的输入(特征),而右侧是输出(目标)。在它们之间可以有多个转换(层),这些层学习复杂的输入输出关系。
当模型被训练时(换句话说:当这些关系被更新时),输入的上下文和输出的信息量将决定模型的擅长领域。类似于人类能够很好地回答母语中的问题,机器学习模型在处理它们见过的数据时表现较好。这被称为同质分布学习。如果在训练过程中,模型接收到大量丰富的上下文,它可以依赖于这种获得的知识,最终的预测结果会显示出准确的表现。
然而,异质分布学习描述的是模型需要基于它未曾见过的上下文进行预测的情况。你可以想象一个从未学过挪威语的人突然回应用挪威语提出的问题。请查看图 3 以了解同质分布和异质分布学习的概况。

图 3:展示了同质分布(左)与异质分布(右)学习。左侧的模型在面对不属于原始训练数据的新的上下文(在这种情况下是“政治”)时表现较差,而右侧的模型在处理未见过的上下文时表现良好。机器学习模型通常属于左侧类别,并且在异质分布学习中表现不佳。
现代 LLMs 和其他 ML 模型的出色性能来源于原始训练数据中的大量上下文。由于模型的广泛预训练,能够处理的内分布学习的问题范围非常广泛。这使得模型能够回答各种问题,这可能对用户而言看起来像是魔法或人类级智能,但实际上并非如此。同样,模型的错误或意外回答也并不是真正的幻觉,它基本上突出了原始训练数据中的上下文缺口,从而导致分布外学习。总的来说,机器学习模型在其分布外学习能力上非常有限,需要对基础模型进行广泛训练。
语言模型中的预训练力量
在谷歌 DeepMind 成员最*的一篇论文中,作者进一步支持了现代 LLMs 的上下文学习性能主要源自其预训练分布的观点。论文“Pretraining Data Mixtures Enable Narrow Model Selection Capabilities in Transformer Models”由Steve Yadlowsky、Lyric Doshi和Nilesh Tripuraneni(2023)撰写,专注于现代变换器模型如何获得其令人印象深刻的上下文学习能力(即应对任何提供的上下文的能力)。
变换器模型,特别是大型语言模型(LLMs),具有在上下文中学习的卓越能力…
这些发现非常有启发性。当变换器模型在覆盖广泛上下文的数据上进行预训练时,它们在学习新的任务时表现出令人印象深刻的能力,这些任务都属于预训练上下文。这种能力接*最优,展示了在训练分布内的显著泛化和适应能力。然而,当这些模型遇到超出其预训练领域的上下文时,性能受到限制且会出现失败。这突显了在分布外上下文中的减少泛化能力和明显限制。
视觉变换器:规模案例研究
在另一项研究中(也是由谷歌 DeepMind 于 2023 年进行)题为:“ConvNets Match Vision Transformers at Scale”的论文中,作者Samuel L. Smith、Andrew Brock、Leonard Berrada和Soham De挑战了一个在计算机视觉领域广泛存在的观念,即在大规模下,现代的视觉变换器模型超越了传统的模型,如卷积神经网络(CNNs)。该研究使用类似的计算预算训练了 CNNs 和视觉变换器,并比较了它们的性能。
## ConvNets 在规模上匹敌视觉 Transformer
许多研究人员认为,卷积神经网络(ConvNets)在小型或中型数据集上表现良好,但在更大的数据集上不具竞争力……
结果表明,预训练所使用的计算预算与随后的性能之间存在规模法则。在 ImageNet 上进行微调后,预训练的 CNN 在相当的预算下达到了与视觉 Transformer 相匹配的性能。
摘要
这两项研究共同描绘了现代 Transformer 模型令人印象深刻的性能。首先,性能不仅仅受到模型架构的驱动,更主要是由预训练的量驱动。其次,当预训练的上下文涵盖广泛时,生成的模型也会展示出广泛的上下文学习能力。
这些研究强调了一个关键原则:训练数据的量、质量和上下文是任何基础机器学习模型最关键的部分。在不知道预训练涵盖的上下文的情况下,很难事先确定模型在哪些领域表现良好。基准测试可以帮助指示潜在的上下文限制。这些测试并不展示模型的一般性能,它们主要展示了哪些上下文曾经成为模型训练分布的一部分。
总之,随着人工智能时代的到来和数据科学家及工程师在机器学习模型开发中的人数增加,预训练涵盖广泛上下文的重要性变得越来越明显;在许多方面,它不仅是过程的一部分,而且是你所需要的全部。
所有图像,除非另有说明,均由作者提供。
请查看 我的个人主页,关注我,或 订阅我的邮件列表 ,如果你想了解我写的内容或希望及时获得新故事的更新。
精准聚类简化版:kscorer 的自动选择最佳 K-means 聚类指南
kscorer 简化了聚类过程,通过先进的评分和并行化提供了实用的数据分析方法
·
关注 发表在 Towards Data Science · 7 分钟阅读·2023 年 11 月 10 日
--
由 DALL-E-2 根据作者的描述制作
无监督机器学习,尤其是聚类,在数据科学中是一项具有挑战性的任务。它对广泛的实际商业分析项目至关重要。聚类可以独立运行,但它也是复杂数据处理管道中的一个有价值的组成部分,这些管道提升了其他算法的效率。例如,聚类在开发推荐系统时起着关键作用。
好吧,Scikit-Learn 著名地提供了各种经过验证的聚类算法。尽管如此,其中大多数都是参数化的,需要设置簇数,这是聚类中最重要的挑战之一。
通常,使用迭代方法来决定最佳的簇数。这意味着你多次进行聚类,每次使用不同的簇数,并评估相应的结果。虽然这种技术很有用,但也有其局限性。
yellowbrick 包是一个常用工具,可以轻松识别最佳簇数。然而,它也有一些缺点。一个显著的缺点是评估多个指标时可能会出现冲突的结果,以及在图表上识别肘部的挑战。
此外,数据集的大小也是一个问题,不论使用何种包。当处理大数据集时,资源消耗困难可能会妨碍你有效地迭代多个簇。如果是这种情况,可以考虑探索诸如MiniBatchKMeans等技术,它可以实现并行聚类。
但对你的聚类程序进行高级优化可能需要一些鲜为人知的技术,稍后会详细介绍。你还将了解kscorer 包,它简化了这些技术,提供了一种更强大和高效的方法来确定最佳簇数。
这些技术毫无疑问是:
-
降维。 在应用聚类算法之前,对数据进行主成分分析(PCA)可能会有益。这将减少数据干扰,并导致更可靠的聚类过程。
-
余弦相似度。 有一种简单的方法可以在 K-means 中使用(*似)余弦距离,即通过对数据进行欧几里得归一化。这样你就不需要预先计算距离矩阵,比如在进行凝聚聚类时。
-
多指标手头。 为了找到最佳簇数,应该依赖多指标评估,而不是仅仅依靠单一指标。
-
数据采样。 为了解决资源消耗问题并改进聚类结果,可以从数据中获取随机样本以执行聚类操作并评估指标。通过多次迭代的*均得分可以减少随机性的影响,从而产生更一致的结果。
下面展示了这个工作流程。

图片由作者提供
幸运的是,无需从头开始构建整个管道,因为 kscorer 包中已经有现成的实现。
现在,让我们深入探讨一下
我曾在一次会议演讲中听到一位数据科学家说:“基本上,只要你知道自己在做什么,你可以做任何你想做的事情。” © Alex2006
在聚类之前建议对数据进行缩放,以确保所有特征处于同一水*,避免因特征的大小而主导。标准化(以均值为中心并按标准差缩放)或最小-最大缩放(将值缩放到指定范围)是常用的缩放技术。
值得注意的是,特征缩放的重要性在这里得到了完美的说明,它不仅限于 KNeighbors 模型,还适用于各种数据科学方法。通过 z-score 归一化对特征进行标准化可以确保所有特征在相同的尺度上,防止任何特征因其大小而主导模型调整。这个缩放过程可能会显著影响模型的性能,与使用未缩放数据时进行的模型调整相比,可能会导致不同的模型调整。
此外,K-means 聚类与主成分分析(PCA)之间存在基本联系,这在丁和赫的论文 “K-means Clustering via Principal Component Analysis”中进行了探讨。尽管最初这两种技术的目的不同,但最终它们都旨在有效地表示数据,同时最小化重构误差。PCA 旨在将数据向量表示为减少数量的特征向量的组合。而 K-means 聚类则旨在将数据向量表示为簇中心向量的组合。这两种方法都力求最小化均方重构误差。
在应用 PCA 后,由于可能出现的计算问题(有些值可能接*零,而其他值可能非常大),我们需要再次对数据进行缩放。这完全有意义,因为我们在 PCA 后已经失去了对初始特征的跟踪,因此数据将无法解释。
另一个可能不为人知的有趣相关性是余弦相似度与欧几里得距离之间的关系。理解这种关系在这些度量间接互换使用时至关重要。这些知识在将传统的 K-means 聚类算法转换为球面 K-means 聚类算法时具有实际应用,其中余弦相似度是聚类数据的关键指标。如前所述,我们可以通过对数据应用欧几里得归一化来“建立”余弦相似度与欧几里得距离之间的联系。
在缺乏真实簇标签的情况下,聚类模型的评估必须依赖于内在度量,而kscorer 包提供了一套全面的指标来评估聚类质量。这些指标提供了有关识别簇之间分离程度的有价值见解:
-
轮廓系数。它通过计算数据点到其不属于的最*簇的*均距离与每个数据点的簇内*均距离之间的差异来量化簇的分离程度。结果经过标准化,并表示为两者之间的比例,值越高表示簇分离越优越。
-
Calinski-Harabasz 指数。它计算簇间散度与簇内散度的比率。Calinski-Harabasz 测试得分越高,表示聚类性能越好,簇定义越清晰。
-
Davies-Bouldin 指数。它衡量簇间离散度与簇内离散度的比率,值越低表示聚类性能越优越,簇的区分度越高。
-
邓恩指数。它通过比较簇间距离(任意两个簇质心之间的最小距离)与簇内距离(簇内任意两点之间的最大距离)来评估簇的质量。邓恩指数越高,表示簇定义越清晰。
包中使用的指标的 Python 计算方法如下:
- 贝叶斯信息准则 (BIC)。BIC 作为一个额外的、在某种程度上是独立的度量。虽然 K-means 没有提供直接的概率模型,但 BIC 可以帮助估计应用 K-means 模型后的数据分布。这种方法提供了对簇质量更全面的评估。
所有指标都经过标准化,确保较高的评分始终表示定义明确的聚类。这种彻底的评估对于识别数据集中最佳聚类数至关重要。
为了克服记忆限制,并迅速执行数据预处理和评分操作以进行 K-means 聚类,kscorer 包利用 N 个随机数据样本。这种方法确保了无缝执行,并能适应不同大小和结构的数据集。类似于交叉验证技术,它能够保持稳健的结果,即使每次迭代只关注数据的一个有限子集。
使用 kscorer 动手操作
因此,我们有一些数据用于聚类。请注意,我们在此场景中假设不知道确切的聚类数。
接下来,我们将把数据集分成训练集和测试集,并拟合一个模型来检测最佳聚类数。该模型将自动在 3 到 15 之间搜索最佳聚类数。这可以轻松实现,如下所示:
完成拟合过程后,我们可以查看所有应用指标的标准化评分。这将帮助我们确定适合我们数据的最佳聚类数。当查看图表时,你会注意到一些聚类被突出显示,并带有相应的评分。这些标记点对应于所有指标的*均评分中的局部最大值,因此代表了选择最佳聚类数的最佳选项。
现在,我们可以评估我们新的聚类标签与真实标签的匹配程度。请确保这种选项在实际商业场景中通常是不可用的😉
在聚类中,你可以尝试对之前未见过的数据进行聚类。但请注意,这并不是一个典型的聚类任务。一种不同且通常更有用的策略是使用聚类标签作为目标来构建分类器。这将使得将聚类标签分配给新数据变得更容易。
最后,提供一个新的互动视角来观察我们的数据。
因此,这就是我们如何利用kscorer 包深入研究 K-means 聚类,该包简化了寻找最佳聚类数的过程。由于其复杂的指标和并行处理,它已被证明是一个实用的数据分析工具。
使用 Python 预测人类表现的极限
原文:
towardsdatascience.com/predict-the-limits-of-human-performance-with-python-50cc824d2539
使用 SciPy 建模指数衰减
·发表于 Towards Data Science ·10 分钟阅读·2023 年 5 月 1 日
--

一幅表现奥运跑者冲过终点线的生动油画,描绘成星云的爆炸(由 DALL-E2 制作)
人类会不会跑得比法拉利快?当然不会。人类表现本质上是有限的,许多因素限制了我们的速度,包括血液输送氧气的速度和肌肉的反应速度。除非我们进行重大基因工程,否则我们现在的速度就是我们能达到的极限。
那么我们怎么知道这一点呢?事实上,人类表现,像许多其他特质一样,遵循一个钟形曲线分布[1]。这意味着大多数人都在曲线的峰值附*的*均范围内,只有少数人特别慢或特别快。当我们离峰值越来越远时,具有这种表现水*的个体数量会以指数方式下降。在短跑的情况下,这意味着最快的短跑运动员已经达到了曲线的*坦、收缩部分。因此,取得显著的速度提升将变得越来越困难。

2005 年以来记录的 100 米试验时间图,低于 9.93 秒[2]。(参考麦考密克工程学院(2016))
这并不是说没有改进的空间。训练、营养、设备和其他因素可以帮助个人在其基因限制范围内提高表现。然而,重要的是要认识到,我们不能从根本上改变我们的身体极限,尤其是当监管机构继续限制在田径比赛中使用先进的生物技术时。
由于人类表现是有限的,我们可以预测许多运动项目的未来结果[3]。例如,100 米短跑世界纪录的减少,像许多其他自然现象一样,似乎遵循指数衰减的模式[4]。因此,我们可以用指数方程来建模:

指数方程(图片由作者提供)
在这个方程中,y表示对世界纪录短跑时间的预测(以秒为单位);x表示自首次记录设立以来的年数;a、b和c表示曲线拟合参数:
-
a是尺度因子或幅度。它决定了指数函数的垂直伸展或压缩。 -
b是衰减常数。它表示当x增加时,函数衰减的速度。 -
c是垂直位移的量。它决定了水*渐*线的y值,即当x趋*于无穷大时,指数函数接*的一条水*线。
这三个参数是通过将指数函数拟合到一组数据点来解决的,使用的是曲线拟合算法。通过最小化y的预测值与每个x值处实际y值之间差异的*方和,来确定最适合数据的值。
当然,如果你不是数学家,找到正确的参数可能会有些困难。幸运的是,对于我们其他人来说,有一个 Python 库可以让曲线优化变得非常简单。
SciPy 库
开源的SciPy 库在 NumPy 的基础上扩展,提供了物理常数、转换因子和用于数学、科学及工程的数值例程[5]。这些包括用于曲线拟合的优化例程,这正是我们项目所需要的。
使用 conda 安装 SciPy:
conda install scipy
使用 pip 安装:
pip install scipy
代码
该项目的 Python 代码是在 JupyterLab 中编写的。如果你想下载笔记本,可以在这个Gist找到。
导入库并设置 RC 参数
以下单元导入库并为 matplotlib 图形设置运行配置参数。提前设置这些参数不是严格必要的,但在绘制多个图形时可以减少后续代码量。
import warnings
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import scipy.optimize
# Suppress warnings for using np.exp() on large values during optimization:
warnings.filterwarnings('ignore')
# Set default run configuration for plots:
plt.rcParams['figure.figsize'] = (6, 4)
plt.rc('font', size=12)
plt.rc('axes', titlesize=14)
plt.rc('axes', labelsize=12)
plt.rc('xtick', labelsize=11)
plt.rc('ytick', labelsize=11)
plt.rc('legend', fontsize=11)
加载数据
世界纪录不经常被打破,因此匹配的数量少于二十个。我们将使用topend sports 网站上的纪录列表,并将其作为字典[6]输入。然后我们将字典转换为 pandas DataFrame,以便于使用。我们的大部分分析将使用纪录(以秒为单位)与自首次记录以来的年数,因此我们将添加一个表示年数的列。
# Input men's 100 m world records in seconds.
# If two records were set in the same year, list only the latest (lowest):
records = {2009: 9.58, 2008: 9.69, 2007: 9.74, 2005: 9.77, 2002: 9.78,
1999: 9.79, 1996: 9.84, 1994: 9.85, 1991: 9.86, 1988: 9.92,
1983: 9.93, 1968: 9.95, 1960: 10, 1956: 10.1, 1936: 10.2,
1930: 10.3, 1921: 10.4, 1912: 10.6}
# Turn dictionary into a DataFrame:
df = pd.DataFrame(records.items(), columns=['year', 'time'])
df['years'] = df['year'] - 1912 # Years since first record.
df = df.sort_values('year').reset_index(drop=True)
display(df)

男子 100 米世界纪录数据框(作者提供的图片)
绘制世界纪录
尽早查看数据总是一个好主意,因此让我们从数据框中制作一个茎状图。这应该可以轻松地查看数据中的趋势和异常值。
# Graph the world records:
plt.stem(df.year, df.time)
plt.title("Men's 100 m Sprint World Records")
plt.ylabel("Time (secs)")
plt.ylim(9.5, 10.8)
plt.grid(True);

按年份绘制的男子 100 米世界纪录茎状图(作者提供的图片)
正如你可能对指数衰减的预期,纪录时间最初下降得相当快,但随后开始趋于*稳,就像飞机接*跑道一样。虽然现代运动员拥有优化的训练、营养和设备,但随着接*人类表现极限,进步变得越来越困难。或者并非如此?
看一下右侧的最后两个数据点。它们看起来像是从悬崖上掉下来的。这不是一个温和的曲线*稳着陆。这很奇怪。这就是乌塞恩·博尔特。

牙买加跑者蒙太奇,由 DALL-E2 生成(提示:一幅戏剧性的油画,描绘一位穿着黄色衬衫和绿色短裤的牙买加奥运跑者,冲过终点线的画面如星云爆炸)
乌塞恩·博尔特的疯狂故事
乌塞恩·博尔特是一位牙买加跑者,拥有“世界上最快的人”称号[6][7]。2008 年,他在男子 100 米短跑中以 9.69 秒的时间赢得了奥运金牌。这创造了新的世界纪录,尽管他在庆祝时提前放慢了速度(你可以在这里观看)。
一年后,博尔特保持专注,以 9.58 秒的时间和 44.72 公里/小时(27.79 英里/小时)的最高速度越过终点线。这个纪录比生物统计学家基于当时数学模型的预期早了几十年。
今天,多亏了博尔特,对于 100 米的最终时间预测充满了谦逊和不确定性。在讨论的数字中,包括相对较高的 9.44 秒和非常低的9.27 [8][9]。
为了评估博尔特对 100 米冲刺预测的影响,我们来做一些自己的预测。这些预测将完全基于以前的世界纪录,而不是所有职业比赛的结果。我们将首先预测没有博尔特的情况,然后重复这个过程有博尔特的情况。由于我们会做多次预测,因此我们将首先编写用于创建和优化指数函数的函数。
定义指数衰减的函数
第一个函数将接受x值(自第一个记录以来的年数)和曲线拟合参数a、b、c,并返回预测的y值(时间)。第二个函数将接受第一个函数作为参数,以及x和y数据,并使用 SciPy 的[optimize.curve_fit()](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html#scipy.optimize.curve_fit)方法自动选择最佳拟合参数。我们将p0参数设置为None,这意味着我们让方法决定y的最佳最小值和最大值,而不是提供猜测。
def expo(x, a, b, c):
"""Return y values for exponential decay curve."""
return a * np.exp(-b * x) + c
def optimize_curve_fit(a_func, x, y):
"""Return optimized parameters for curve fit."""
params, covar = scipy.optimize.curve_fit(a_func, x, y, p0=None)
return params
优化曲线拟合参数
在调用我们的函数之前,我们需要构建包含博尔特时间(_all后缀)和不包含博尔特时间(_nB后缀)的世界纪录数据集。我们将这些数据集连同我们的expo()函数一起传递给optimize_curve_fit()函数。该函数返回优化后的a、b和c拟合参数作为 NumPy 数组。
# Generate datasets with and without Bolt's times (nB = No Bolt):
x_all, y_all = df.years, df.time
x_nB, y_nB = x_all[:-2], y_all[:-2]
# Find optimized parameters for fitting the curve to the points:
params_nB = optimize_curve_fit(expo, x_nB, y_nB)
params_all = optimize_curve_fit(expo, x_all, y_all)
print(f"Parameters without Bolt (a, b, c) = {params_nB}")
print(f" Parameters with Bolt (a, b, c) = {params_all}")
Parameters without Bolt (a, b, c) = [0.98795896 0.01631187 9.57391395]
Parameters with Bolt (a, b, c) = [1.34836526 0.00941746 9.18654695]
绘制结果
为了绘制我们的预测曲线,我们将把我们的指数衰减函数(expo())传递给 matplotlib 的plot()方法,并使用优化后的拟合参数。
# Plot exponential curves for data with and without Bolt's times:
plt.plot(x_all, y_all, '.', label='measured data', c='k')
plt.plot(x_nB, expo(x_nB, *params_nB),
'-', label='fitted without Bolt')
plt.plot(x_all, expo(x_all, *params_all), '--',
label='fitted with Bolt', c='red')
plt.title("Men's 100 m World Record")
plt.xlabel('Years Since First Record (1912)')
plt.ylabel('Times (s)')
plt.grid(True)
plt.legend(framealpha=1);

两条拟合世界纪录数据的指数曲线(作者提供的图像)
哇。尤塞恩·博尔特真的是超出了曲线。这是因为,从物理上讲,他是一个异常值。尽管比大多数短跑运动员高且步幅更长,但他仍能保持类似的步频,这意味着他可以用更少的步伐覆盖相同的距离。这可能是因为他结合了较矮短跑运动员的快肌纤维和较高个子人的身体机械优势[8]。
一个有效的问题是他是否打破了曲线,还是仅仅加快了我们沿曲线的进展。要调查这一点,我们需要将两条曲线外推到未来。
预测未来表现
以下代码首先将指数曲线外推 570 年(从 1912 年首次记录前 20 年到 550 年后)。绘制曲线后,标记博尔特的数据点,以便我们可以看到它们在未来与曲线交点的位置。最后,打印每条曲线预测的最小时间。请注意,这些值与曲线拟合练习中的c参数相同。
# Extrapolate exponential curves to predict future performance:
x_extrap = np.arange(-20, 550)
y_nB_extrap = expo(x_extrap, *params_nB) # Without Bolt.
y_B_extrap = expo(x_extrap, *params_all) # With Bolt.
# Create a plot of the world record times and the extrapolated curves.
fig, ax = plt.subplots()
ax.plot(x_all, y_all, '.', label='data', c='k')
ax.plot(x_extrap, y_nB_extrap, '-', label='fitted without Bolt')
ax.plot(x_extrap, y_B_extrap, '--', c='red', label='fitted with Bolt')
ax.set(title="Men's 100 m World Record Extrapolated",
xlabel='Years Since First Record (1912)',
ylabel='Time (s)',
yticks=np.arange(9.0, 11.0, 0.2))
ax.grid(True)
ax.legend(framealpha=1)
# Add a dotted horizontal line for each of Bolt's world record times.
bolt_times = {2009: 9.58, 2008: 9.69}
for year, time in bolt_times.items():
ax.axhline(time, ls=':', linewidth=1.3, color='red')
ax.text(0, time + 0.01, f"Bolt {year}", color='red',
horizontalalignment='left', size=9)
# Define function and inverse function to permit a secondary x-axis for year:
axis_transform = lambda x_extrap: x_extrap + 1912
axis_inverse = lambda x_extrap: x_extrap - 1912
ax2 = ax.secondary_xaxis('top', functions=(axis_transform, axis_inverse))
print(f"\nMinimum predicted time without Bolt data = {min(y_nB_extrap):.2f} sec.")
print(f"Minimum predicted time with Bolt data = {min(y_B_extrap):.2f} sec.\n")
Minimum predicted time without Bolt data = 9.57 sec.
Minimum predicted time with Bolt data = 9.19 sec.

外推到未来的两条曲线(作者提供的图像)
从技术上讲,这两条曲线都允许博尔特当前的 9.58 秒记录。如果我们假设红色曲线包含博尔特的数据,并且提供了有效的预测,那么博尔特的成就就提前了数十年。
红色曲线预测,100 米冲刺的终极人类极限为 9.19 秒,并将在约 400 年内达到。虽然 9.19 秒确实很快,但这并不与其他已发布的预测值相悖,如9.27、9.26和9.09秒 [9][10][11]。
虽然 400 年是很长时间,但一些研究人员认为博尔特的当前纪录将再保持约230 年 [2]!无论如何,我们的 9.57 秒和 9.19 秒的值都是合理的,有很大可能夹住终极值。数学(以及 Python)真是太神奇了!
摘要
许多自然现象,如放射性衰变、岩石断裂和人口增长,可以使用指数方程、幂律和逻辑斯蒂函数等工具进行数学建模。除了匹配现有数据,这些模型还可以预测未来行为。在这个快速成功数据科学项目中,我们使用了指数方程来预测男子 100 米赛跑的终极跑步时间。
拟合曲线到数据需要操作多个参数。目标是最小化实际数据点和预测数据点之间的误差。Python 的 SciPy 库包括自动化这一过程的函数,使曲线拟合对所有人都易于实现。
来源
[1] 正态分布。 (2023 年 4 月 16 日)。在维基百科。 en.wikipedia.org/wiki/Normal_distribution。
[2] 西北大学麦考密克工程学院:打破博尔特 100 米纪录需要多长时间?Luis Amaral 教授计算了几率 (2016)。
[3] Little, Brown and Company, 公式:成功的普遍法则 作者 Albert-László Barabási (2018)。
[4] 指数衰减。 (2023 年 3 月 11 日)。在维基百科。
[5] SciPy: https://scipy.org/ (2023)。
[6] Robert Wood, “100 米世界纪录。” Topend Sports 网站,2008,www.topendsports.com/sport/athletics/record-100m.htm, 访问日期 2023 年 5 月 1 日。
[7] 尤赛恩·博尔特。 (2023 年 4 月 15 日)。在维基百科。
[8] Wired: 博尔特飞速无比,但远未接*人类极限 作者 Alexis Madrigal (2008)。
[9] Runner’s World: 终极 100 米时间:9.27 秒? 作者:安比·伯富特 (2014)。
[10] Idea & Issac: Femto Essays: 男子 100 米世界纪录挑战简单曲线拟合 作者:田端达夫 (2008)。
[11] Idea & Issac: Femto Essays: 博尔特的世界纪录再次改变了经验预测 作者:田端达夫 (2009)。
谢谢!
感谢阅读,并确保关注我,以获取未来更多的快速成功数据科学项目。
使用卷积网络预测结核分枝杆菌的药物耐药性 — 论文评审
·
关注 发表在 Towards Data Science ·6 分钟阅读·2023 年 3 月 20 日
--
神经网络可以提高对病原体药物耐药性的预测能力
在这篇文章中,我将回顾一篇关于医学研究与建模及机器学习之间接口的最新论文。论文Green, A.G., Yoon, C.H., Chen, M.L. 等. 卷积神经网络突显与抗微生物耐药性相关的结核分枝杆菌突变. Nat Commun 13, 3817 (2022). https://doi.org/10.1038/s41467-022-31236-0描述了两种培训神经网络模型以预测给定 M. tuberculosis (MTB)菌株对 13 种抗生素的耐药性的 Approaches。这种建模技术的优点是能够生成一个显著性图,突显出对预测影响最大的特征,从而解决了一些关于模型可解释性的担忧。
问题陈述
结核病(TB)是导致全球感染性病原体死亡的主要原因。其病原体 M. tuberculosis(或 MTB)正逐渐对抗生素产生耐药性——这一过程对公共卫生构成威胁。虽然对每个患者的 MTB 分离株进行一系列抗生素的耐药性实证测试可能是最准确的方法,但可能需要几周才能完成,并且无法及时治疗。分离株的分子诊断只需数小时或数天,但仅关注基因组序列中的特定位点。因此,学习表型(药物耐药性)与病原体基因型(被诊断位点的结构)之间依赖关系的机器学习模型可能提供所需的解决方案。

图片由Julia Koblitz拍摄,发布于Unsplash
单药和多药模型
作者描述了两种建模方法:第一种名为 SD-CNN(单药 CNN),训练 13 个不同的 CNN,每个 CNN 预测对不同药物的耐药性。第二种名为 MD-CNN(多药 CNN),同时预测对 13 种药物的耐药性。这种建模技术背后的见解是关于多任务学习的开创性研究(Caruana, R. 多任务学习. Mach. Learn. 28, 41–75 (1997)),该研究表明,与直觉相反,训练 CNN 同时执行不同任务,确实可以提高其在每个单独任务上的表现,前提是这些任务是相关的。这个结果的解释是,一个任务生成的特征对其他任务的表现是有利的(例如,通过路标检测的辅助任务训练自动驾驶汽车转向模型)。多任务学习在遗传学研究中的优势由Dobrescu, A., Giuffrida, M. V. & Tsaftaris, S. A. 以更少做更多:一种植物表型的多任务深度学习方法. Front. Plant Sci. 11, 141 (2020)证明。
模型输入
用于训练的数据是 10,201 个 M. tuberculosis 病原体分离株,这些分离株在 13 种抗生素上进行了耐药性测试。MD-CNN 的输入是一个 5x18x10,291 的数组,其中 5 是 4 种核苷酸的独热编码(腺嘌呤、胸腺嘧啶、鸟嘌呤、胞嘧啶和一个缺口字符),18 是 locus 索引(作者使用 18 个与药物耐药性相关的 loci),10,291 是最长 locus 的长度。locus(复数—loci)是染色体上的一个特定固定位置,其中存在特定基因或基因序列。locus 由其起始索引和结束索引定义,从约定的起点计算核苷酸。不同的 loci 具有不同的长度。
每个 13 个 SD-CNN 模型的输入包含 18 个 loci 中的一个子集,这些 loci 对该药物的抗性有已知影响。
模型输出
MD-CNN 模型的输出是一个 13 元素的向量(按抗结核药物索引),每个元素包含该菌株对该药物的抗性信心的 sigmoid 结果。SD-CNN 模型返回一个对应于该药物抗性信心的单一 sigmoid 值。
模型架构
该模型是一个 CNN,由 2 个 1-D 卷积和最大池化块组成,之后是 3 个全连接层。描述见图 1。

图 1 — MD-CNN 架构。Conv 1a 和 1b 的卷积核尺寸为 5x12 和 1x12。Conv 2a 和 2b 的卷积核尺寸为 1x3。最大池化层的形状为 1x3。所有步幅均为 1x1。所有激活函数均为 ReLU,输出层除外,输出层使用 sigmoid。每层的输出维度在图形表示下方给出。SD-CNN 模型与此图的不同之处在于它们的 locus 维度不是 18,输出维度是 1。图像由作者提供。
结果
SD-CNN 和 MD-CNN 模型在相互之间及与两个先前模型:Reg+L2 和 SOTA 模型 WDNN(Chen, M. L. 等. 超越多药耐药性:利用稀有变异通过机器和统计学习模型进行结核分枝杆菌耐药性预测。EBioMedicine 43, 356–369 (2019)*)的基准测试。基准测试使用了对训练集的 5 折交叉验证。
测试显示,MD-CNN 的表现与 WDNN(当前的 SOTA 模型,使用布尔编码已知突变的基因组序列作为输入。它被设计为多层感知器的组合,即不使用卷积)相当。MD-CNN 的*均 AUC 在一线药物上为 0.948(WDNN 为 0.960),在二线药物上为 0.912(WDNN 为 0.924)。SD-CNN 的准确率略低,两组药物的 AUC 均为 0.888。MD-CNN 和 SD-CNN 展示了对新数据的泛化能力,在一个单独收集的 12,848 个样本的测试集上取得了大致相同的 AUC)。— 有关模型的图形比较,请参见 原始论文。
作者指出,MD-CNN 模型的敏感性高于 SD-CNN 模型(即药物耐药性的漏检率较低),而 SD-CNN 模型的特异性更高(即错误将样本分类为耐药药物的比例较低)。换句话说——MD-CNN 不那么保守,倾向于将更多案例分类为‘耐药’。
分析 SD-CNN 的性能时,作者检查了假阴性案例。在检查数据时,他们观察到具有相同模型输入的样本在某些情况下对同一种药物具有耐药性,而在其他情况下则对该药物敏感(即它们的实际分类不同)。这使得作者假设,SD-CNN 模型未包含的位点中的突变可能是耐药性的原因。
可解释性和显著性映射
作者使用DeepLIFT(Avanti Shrikumar, Peyton Greenside, and Anshul Kundaje. 2017. 通过传播激活差异来学习重要特征。发表于第 34 届国际机器学习大会 — 第 70 卷(ICML’17)。JMLR.org, 3145–3153。),这是一种计算输入特征对输出贡献的方法,来解释模型的预测。通过在计算机中变更基因型输入(模拟输入)并将结果与‘参考结果’进行比较,作者发现了以前未知的变异对 MTB 药物耐药性的影响。
对模型架构的几点思考
作为一名机器学习工程师和研究员,大部分关注点在计算机视觉领域,我从阅读这篇论文和相关背景材料中学到了很多。显然,神经网络在医学和生物学领域的建模技术方面具有巨大潜力。在将此模型中使用的技术与我自己的经验进行比较时,我想到了一些如果我在研究的第二阶段工作时会感兴趣尝试的东西:
-
Gap encoding — 四种核苷酸以 1-hot 编码表示,并额外增加一个表示间隙的元素。我很好奇如果将间隙表示改为[0, 0, 0, 0],结果是否会有所改善。
-
特征深度——此处呈现的架构在整个模型中使用单一特征。我对计算机视觉的直觉让我对特征多样化的可能性感到好奇。正如在计算机视觉中,训练过程可能会收敛到图像中的单一位置具有多种特征,如‘圆度’,‘金属性’,‘*滑度’,我猜在基因组序列中也可能是这样。
-
填充类型——作者在其卷积层中使用‘valid’填充,而不是计算机视觉中常用的‘same’填充。这会随着序列在层之间传递逐渐缩短序列。‘Same’填充保持序列的空间大小,允许序列边缘附*的结构即使在模型的后期阶段仍然保持一些效果。它还允许诸如将来自不同阶段的层的输出连接起来等操作。
-
注意机制——(Vaswani 等人,Attention Is All You Need, 2017, NIPS)——注意块在发现序列中远程标记之间的微妙关系(例如,NLP 中的句子不同部分)时非常有用,当一个标记的值可能对另一个标记的值的解释产生重大影响时,它们尤其相关。看看添加注意块是否能改善结果,如果能——则使用它来追溯基因组中的区域间隐藏关系会很有趣。
预测高急诊室使用率
原文:
towardsdatascience.com/predicting-high-emergency-room-visit-rates-5fff6a8950f4
使用 Python 分析健康的社会决定因素(SDOH)
·发布于数据科学前沿 ·10 分钟阅读·2023 年 5 月 23 日
--

照片由国家癌症研究所拍摄,来自 Unsplash
本项目的目标是利用来自AHRQ的按县划分的社会决定因素(SDoH)数据,查找特定变量与县急诊就诊率之间的关系。最终,我希望开发一个与高急诊率相关的顶级特征的预测模型。我决定查看 2019 年和 2020 年的数据(2018 年数据不可用)。该数据集经过AHRQ的明确许可使用。
这个逐步教程介绍了我加载、清理、分析和建模数据的过程。
加载数据
第一步是加载两个数据文件并检查其形状。

作者的代码
由于两个数据框的列数不同,我将导入数据字典并提取相同的列。
我根据列名(内连接)合并了数据字典,以获取最终的公共列列表。一旦获得列,我选择了每个数据框中这些列的子集,并将它们与轴=0 进行连接,以垂直添加它们。我的 df_final 包含了 2019 年和 2020 年数据中的公共列。
dictionary2019=pd.read_csv('Data/datadictionary2019.csv', encoding= "ISO-8859–1")
dictionary2020=pd.read_csv('Data/datadictionary.csv', encoding= "ISO-8859–1")
commoncolumns=dictionary2020.merge(dictionary2019, how='inner', left_on='name', right_on='name')['name'].values.tolist()
dfa=df2019[commoncolumns]
dfb=df2020[commoncolumns]
df_final=pd.concat([dfa, dfb], axis=0)
df_final
关于数据
该数据有 674 列,因此我们需要尝试缩小需要查看的特征。让我们从我感兴趣的变量——急诊访问率开始。
数据集包括每 1,000 名男性医疗保险(双重和非双重)受益人的急诊科访问次数。然而,数据提供了单独的男性和女性比率,因此我将创建一个加权*均的整体急诊率。
为此,我将比率分别乘以男性的百分比和女性的百分比,然后将这些值相加。
男性 ED 比率:597.1129158207091
女性 ED 比率:639.9023742580443
import numpy as np
#create an average overall ED rate by weighting the male and female rates by their percentage of the population and adding
df_final['Malerate']=(df_final['ACS_PCT_MALE']*df_final['MMD_ED_VISITS_M_RATE'])/100
df_final['Femalerate']=(df_final['ACS_PCT_FEMALE']*df_final['MMD_ED_VISITS_F_RATE'])/100
df_final['EDrate']=df_final['Malerate']+df_final['Femalerate']
#print the mean ED rate to use as our baseline for Good and Bad outcomes
数据清理

查看数据框后,我们可以看到一些县的数据缺失。为了解决这个问题,我删除了某些列,并用均值插补了其他列。
对于这个项目,我们将删除 ED 比率计算中缺失值的行,因为这意味着他们没有相关的急诊访问数据。
然后,我们将找到数据集的 80 百分位数,作为‘高 ED 比率’的截断点。这将成为我未来预测的结果变量。整体 ED 比率将用于相关性和探索性数据分析。
#create cutoff value for high EDrate
cutoff=np.percentile(df_final['EDrate'], 80)
#if ED rate is greater than the 50th percentile then flag as high or else 0 for low
df_final['HighED']=np.where(df_final['EDrate']>cutoff, 1, 0)
为了处理一些缺失数据,我开始时删除了任何缺失值超过 10%的列。然后,我得到了剩余列中有缺失值的列表,如下所示。
# drop columns with >10% missing
df_final.dropna(thresh=0.90*len(df_final),axis=1, inplace=True)
#list columns remaining with missing values
df_final.isnull().sum().to_frame(name='counts').query('counts > 0').sort_values(by='counts', ascending=False)
我们先用均值对所有浮点型列进行简单插补。首先,我们需要将训练集和测试集分开:
from sklearn.impute import SimpleImputer
from sklearn.model_selection import train_test_split
columns=df_final.loc[:, df_final.dtypes == float].columns.values
X_train, X_test, y_train, y_test = train_test_split( df_final.index, df_final['HighED'], stratify=df_final['HighED'], test_size=0.25, random_state=42)
df_train=df_final.iloc[X_train]
df_test=df_final.iloc[X_test]
imp = SimpleImputer( strategy='mean')
df_impute_train=df_train[columns].copy(deep=True)
df_impute_test=df_test[columns].copy(deep=True)
df_impute_train[:]=imp.fit_transform(df_train[columns])
df_impute_test[:]=imp.fit_transform(df_test[columns])
df_impute_train
接下来,我们将选择所有浮点型列,以便我们可以运行每个特征与目标特征之间的相关性分析。根据相关性,我们将设定阈值,并保留那些与 ED 比率显著正相关或负相关的列。
#print positive correlations over .2 (small-moderate effect size)
def positivecorrelations(threshold=.2):
columns=df_impute_train.columns.values
positivecolumns=[]
for col in columns:
if df_impute_train['EDrate'].corr(df_impute_train[col])>threshold:
positivecolumns.append(col)
return positivecolumns
poscols=(positivecorrelations())
#print negative correlations less than -.2 (small-moderate effect size)
def negativecorrelations(threshold=-.2):
columns=df_impute_train.columns.values
negativecolumns=[]
for col in columns:
if df_impute_train['EDrate'].corr(df_impute_train[col])<threshold:
negativecolumns.append(col)
return negativecolumns
negcols=(negativecorrelations())
我制作了一个最终的列列表,如下所示:
#make a final list of significant columns
sigcols=poscols+negcols
print(sigcols)
len(sigcols)
我们最终得到了 140 列。在打印列的列表后,我意识到我仍然需要进一步清理 -
我们要确保不包括任何包含 ED 的变量,因此我们会通过以下代码过滤掉所有这些列,以及我们计算出的女性比率和男性比率。
stringVal = "ED"
sigcols.remove('Femalerate')
sigcols.remove('Malerate')
finalcols=[x for x in sigcols if stringVal not in x]
len(finalcols)
这使我们剩下了 140 列。我们将数据集缩小到 112 列。但现在我查看列的列表时,看到我们还应排除任何包含 _IP(住院)、_PA(急性后期)和 _EM(E&M)的列。我们也不关心每月的最低和最高温度,因此我会删除这些列。
stringVal = "_IP"
finalcols=[x for x in finalcols if stringVal not in x]
stringVal2="TEMP_"
finalcols=[x for x in finalcols if stringVal2 not in x]
stringVal3="_PA"
stringVal4="_EM"
finalcols=[x for x in finalcols if stringVal3 not in x]
finalcols=[x for x in finalcols if stringVal4 not in x]
len(finalcols)
#result is 77
基于对输出的另一轮仔细检查,我发现有些特征测量的是非常相似的事物(即整体估计百分比与年龄 X-Y)。如果整体值存在,就删除那些指定年龄范围的列。此外,PQI 在所有不同的人群子集中的重复,因此我们将采用加权*均来找出整体比率,就像我们之前用 ED 比率一样。
finalcols=[x for x in finalcols if x not in ('ACS_PCT_PRIVATE_SELF_BELOW64', 'ACS_PCT_PRIVATE_SELF','SAIPE_PCT_POV_0_17', 'ACS_PCT_PRIVATE_ANY_BELOW64', 'SAIPE_PCT_POV_5_17', 'NEPHTN_HEATIND_90', 'NEPHTN_HEATIND_95', 'NEPHTN_HEATIND_100')]
df_impute_train.loc[:, 'MalePQI']=(df_impute_train['ACS_PCT_MALE']*df_impute_train['MMD_OVERALL_PQI_M_RATE'])/100
df_impute_train.loc[:, 'FemalePQI']=(df_impute_train['ACS_PCT_FEMALE']*df_impute_train['MMD_OVERALL_PQI_F_RATE'])/100
df_impute_train.loc[:, 'PQI']=df_impute_train['MalePQI']+df_impute_train['FemalePQI']
df_impute_train['PQI'].describe()
df_impute_test.loc[:, 'MalePQI']=(df_impute_test['ACS_PCT_MALE']*df_impute_test['MMD_OVERALL_PQI_M_RATE'])/100
df_impute_test.loc[:, 'FemalePQI']=(df_impute_test['ACS_PCT_FEMALE']*df_impute_test['MMD_OVERALL_PQI_F_RATE'])/100
df_impute_test.loc[:, 'PQI']=df_impute_test['MalePQI']+df_impute_test['FemalePQI']
df_impute_test['PQI'].describe()
rate="_RATE"
finalcols=[x for x in finalcols if rate not in x]
race="ACS_PCT_BLACK_"
finalcols=[x for x in finalcols if race not in x]
dictionary2020[['name', 'label']][dictionary2020['name'].isin(finalcols)]
我还从数据字典中提取了所有列的完整标签,并审查了它们,以确保所有列在我对医疗分析的背景知识下都是实用的。

最终结果得到 78 个可以使用的列。接下来,我想快速可视化这些变量与 EDrate 之间的关系,于是写了一个小循环来创建散点图:
import matplotlib.pyplot as plt
def create_scatterplots(var='EDrate'):
for col in finalcols:
plt.scatter(df_impute_train[col], df_impute_train[var])
plt.title(("{} vs {}".format(col, var)))
plt.show()
create_scatterplots()

本分析的下一部分涉及开发几个预测模型。在这个项目中,我使用了逻辑回归、支持向量机、随机森林分类器和 XGBoost 分类器。我们首先测试的是逻辑回归和 SVM,因此需要对数据进行缩放。
我选择了 MinMaxScaler 来尝试减少异常值的影响。
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
X_train=df_impute_train[finalcols].copy(deep=True)
X_test=df_impute_test[finalcols].copy(deep=True)
y_train=df_train['HighED']
y_test=df_test['HighED']
scaler.fit(X_train)
X_train_scaled=scaler.transform(X_train)
X_test_scaled=scaler.transform(X_test)
逻辑回归
如果你对分类问题不熟悉,可以查看逻辑回归介绍这篇文章,由Ayush Pant撰写。对于这次逻辑回归,我决定设置 class_weight=’balanced’,因为这是一个不*衡的分类问题:
from sklearn.linear_model import LogisticRegression
model = LogisticRegression(solver='liblinear', random_state=0, class_weight='balanced').fit(X_train_scaled, y_train)
model.score(X_test_scaled, y_test)
下方的混淆矩阵显示了 TP、TN、FP 和 FN。我们还打印了分类报告。
from sklearn.metrics import classification_report, confusion_matrix
import matplotlib.pyplot as plt
cm = confusion_matrix(y_test, model.predict(X_test_scaled))
fig, ax = plt.subplots(figsize=(8, 8))
ax.imshow(cm, cmap='summer', alpha=0.3)
ax.grid(False)
ax.xaxis.set(ticks=(0, 1), ticklabels=('Predicted LowED', 'Predicted HighED'))
ax.yaxis.set(ticks=(0, 1), ticklabels=('Actual LowED', 'Actual HighED'))
ax.set_ylim(1.5, -0.5)
for i in range(2):
for j in range(2):
ax.text(j, i, cm[i, j], ha='center', va='center', color='black')
plt.show()


支持向量机
我尝试的下一个模型是支持向量机。如果这是一个你不熟悉的话题,我推荐查看Ajay Yadav的文章支持向量机(SVM)
在我的项目中,我对类别权重进行了调整,以观察哪些设置能在模型结果方面取得*衡。
from sklearn.svm import SVC
clf=SVC(class_weight='balanced')
clf.fit(X_train_scaled, y_train)
clf.score(X_test_scaled, y_test)
cm = confusion_matrix(y_test, clf.predict(X_test_scaled))
fig, ax = plt.subplots(figsize=(8, 8))
ax.imshow(cm, cmap='summer', alpha=0.3)
ax.grid(False)
ax.xaxis.set(ticks=(0, 1), ticklabels=('Predicted LowED', 'Predicted HighED'))
ax.yaxis.set(ticks=(0, 1), ticklabels=('Actual LowED', 'Actual HighED'))
ax.set_ylim(1.5, -0.5)
for i in range(2):
for j in range(2):
ax.text(j, i, cm[i, j], ha='center', va='center', color='black')
plt.show()
print(classification_report(y_test, clf.predict(X_test_scaled)))

接下来要测试的模型是随机森林分类器和 XGBoost。由于基于树的模型不需要缩放,我将使用原始的 X 数据来进行这两项测试。
随机森林分类器
在你开始运行随机森林分类器之前,可能需要阅读一篇背景文章,例如Tony Yiu的文章理解随机森林。一旦你对概念有了良好的理解,可以尝试运行下面的简单示例代码:
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import make_classification
clf = RandomForestClassifier( max_features=None, random_state=0, class_weight='balanced')
clf.fit(X_train, y_train)
clf.score(X_test, y_test)
cm = confusion_matrix(y_test, clf.predict(X_test))
fig, ax = plt.subplots(figsize=(8, 8))
ax.imshow(cm, cmap='summer', alpha=0.3)
ax.grid(False)
ax.xaxis.set(ticks=(0, 1), ticklabels=('Predicted LowED', 'Predicted HighED'))
ax.yaxis.set(ticks=(0, 1), ticklabels=('Actual LowED', 'Actual HighED'))
ax.set_ylim(1.5, -0.5)
for i in range(2):
for j in range(2):
ax.text(j, i, cm[i, j], ha='center', va='center', color='black')
plt.show()
print(classification_report(y_test, clf.predict(X_test)))

到目前为止,我们的随机森林分类报告如下:

凭借 94%的准确率,这个模型表现得相当不错,但我对假阴性率感到担忧。让我们看看 XGBoost 是否能做得更好。
XGBoost
对 XGBoost 不熟悉?查看George Seif的文章——XGBoost 初学者指南,在运行下面的代码之前熟悉一下提升树。
import xgboost as xgb
# Init classifier
xgb_cl = xgb.XGBClassifier(random_state=0)
# Fit
xgb_cl.fit(X_train, y_train)
# Predict
preds = xgb_cl.predict(X_test)
cm = confusion_matrix(y_test, xgb_cl.predict(X_test))
fig, ax = plt.subplots(figsize=(8, 8))
ax.imshow(cm)
ax.grid(False)
ax.xaxis.set(ticks=(0, 1), ticklabels=('Predicted LowED', 'Predicted HighED'))
ax.yaxis.set(ticks=(0, 1), ticklabels=('Actual LowED', 'Actual HighED'))
ax.set_ylim(1.5, -0.5)
for i in range(2):
for j in range(2):
ax.text(j, i, cm[i, j], ha='center', va='center', color='red')
plt.show()
print(classification_report(y_test, xgb_cl.predict(X_test)))


看起来随机森林分类器是明显的赢家,具有更高的准确性、精确度、召回率和 f1。
让我们来看看一些特征重要性值:
features = df_impute_test.columns.values
importances = clf.feature_importances_
indices = np.argsort(importances)
plt.figure(figsize=(8, 20))
plt.title('Feature Importances')
plt.barh(range(len(indices)), importances[indices], color='b', align='center')
plt.yticks(range(len(indices)), [features[i] for i in indices])
plt.xlabel('Relative Importance')
plt.show()

import shap
explainer = shap.TreeExplainer(clf)
shap_values = explainer.shap_values(X_test)
shap.summary_plot(shap_values, X_test, plot_type="bar")

主要发现
- 最重要的特征是黑人女性的比例
— 这表明少数群体在急性医疗方面面临更大的困难,任何歧视或健康不*等问题应立即解决。
- 残疾退伍军人的比例也是一个重要特征
— 这表明残疾和退伍军人身份导致了急诊科的高使用率,需要解决潜在的风险因素
- SHAP 值显示 PQI 和过去 12 个月领取食物券/SNAP 的家庭比例有显著影响
— 这间接地将社会经济地位与健康不良结果联系起来,表明如果医疗提供者能够在这一领域提供帮助,患者可能不会那么频繁地去急诊科。
推荐和未来方向
基于这些发现,医疗保健领域的数据专业人员可能考虑开发预测模型,以隔离与健康不良结果相关的社会决定因素特征。
希望目标医疗程序可以解决诸如由于种族、残疾或社会经济地位而产生的歧视等社会决定健康需求。通过为高风险个体或高风险区域提供更密集的护理和支持,医疗保健分析师可以致力于在非急性(非急诊)环境中更好地治疗这些患者。
这可以通过社区健康设施、家庭护理机构以及与健康保险公司合作的其他伙伴来实现。
结论
在这篇文章中,我介绍了一个与社会决定健康相关的公共数据集。通过分析 2019 年和 2020 年的数据,我得出了几个预测模型。该模型的目标是根据社会决定健康因素预测一个县是否会有高急诊科使用率。我的分析中最好的模型是随机森林分类器。我们回顾了驱动模型的核心特征,并解释了对医疗保健分析师的影响。
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
查看 我的 GitHub 上的整个笔记本
如果你是 Medium 的新用户,喜欢这样的故事,可以在这里注册。
互联网引用: 社会决定健康数据库。内容最后审查于 2022 年 11 月。医疗保健研究与质量局,罗克维尔,MD。www.ahrq.gov/sdoh/data-analytics/sdoh-data.html
使用 GPT-3 预测人道主义数据集的元数据
·
关注 发布于 Towards Data Science ·19 分钟阅读·2023 年 1 月 18 日
--
快速响应人道主义灾难,更好的是,能够预测这些灾难,可以挽救生命[1]。数据是关键,不仅仅是拥有大量数据,而是清洁的数据并且理解良好[2],以便创建对实际情况的清晰视图。在许多情况下,这些关键数据被存储在数百个小型电子表格中,因此在进行数据整合时可能非常耗时,并且在发生人道主义事件时,随着新数据的不断涌入,维护这些数据也很困难。自动化数据发现过程可能会加快响应速度并改善受影响人员的结果。
简化发现的一个方法是确保表格数据有描述每列的元数据。这可以帮助将数据集链接在一起,例如知道一个地雷位置表中的列指定了经度和纬度,这与另一个表中定位野战医院的列类似。列名并不总能明显显示它们可能包含的数据,这些数据可能以多种语言呈现,并遵循不同的标准。在理想的情况下,这种元数据是与数据一起提供的,但正如我们下面将看到的那样,这通常不是情况。手动处理这项工作可能非常庞大。
在这篇文章中,我将探讨我们如何通过使用OpenAI 的 GPT-3 大型语言模型来预测人道主义数据集的元数据属性,从而帮助自动化这个过程,并改进以往工作的表现。
人道主义数据交换(HDX)
人道主义数据交换(HDX)是一个极好的*台,旨在通过以标准化的方式将人道主义数据集合在一起,解决这些问题。截至我写这篇文章时,全球共有 20,403 个数据集,涵盖了广泛的领域和文件类型。这些数据集中的 CSV 和 Excel 文件产生了大约 148,000 个不同的表格,数据量非常庞大!

人道主义数据交换(HDX)*台上的文件类型。有关数据如何汇总的信息,请参见这个笔记本。
人道主义交换语言(HXL)
HDX *台的一个优点是它鼓励数据拥有者使用人道主义交换语言(HXL)格式来标记他们的数据。这些元数据使得将数据结合起来并以有意义的方式使用变得更容易,从而在时间紧迫时加快处理速度。
HXL 标签有两种形式,一种是设置在数据集级别的标签,另一种是应用于表格数据中列的字段级别标签。后者看起来像这样:

第二行有 HXL 标签的表格示例[#HXL Standards examples]
注意列标题下方的第二行,那些是 HXL 标签。它们由前缀为‘#’的标签(例如‘#adm1’)和某些情况下的属性(例如‘+name’)组成。
挑战在于这些字段级标签并不总是设置在 HDX 数据集上,这使得使用那里的数据变得更加困难。查看肯尼亚的 CSV 和 Excel 数据,大多数表格似乎缺少列 HXL 标签。

分析人道主义数据交换 (HDX)*台上肯尼亚的数据文件,查看哪些文件有 HXL 列标签。有关如何整理数据的细节,请参见这个笔记本。
如果我们能填补那些空白并为尚未拥有标签的列填充 HXL 标签,那不是很好吗?
微软已经在使用fastText 嵌入预测 HXL 标签方面做了一些非常出色的工作,请参见这个笔记本和相应的论文 [3]。作者在预测标签方面达到了 95% 的准确率,预测属性方面达到了 92%,表现非常出色。
不过,我想知道我们是否可以使用另一种技术,现在有了一些新技术……
GPT-3
正如我在上一篇文章中提到的,去年对生成性 AI 似乎真的非常关注。这个故事的明星之一是 Open AI 的GPT-3 大型语言模型(LLM),它有一些非常惊人的能力。重要的是,它可以经过微调来学习语言的特殊应用模式,如计算机代码。
所以我想到 HXL 标签只是另一种语言‘特殊情况’,可能可以通过一些 HXL 标签示例对 GPT-3 进行微调,然后查看它是否能对新数据进行预测。
从 HDX 获取一些训练数据
首先,值得澄清一下 HDX 数据集、资源和表格的层次结构。‘数据集’可以包含一组‘资源’,这些资源是文件。数据集有自己的页面,比如这个,提供了很多关于历史、上传者和数据集级别标签的有用信息。

HDX *台上的一个 HDX 数据集示例
上面的示例有两个 CSV 文件资源,如果选择更多 > 在 HDX 上预览,可以显示 HXL 标签。

一个 HDX *台上的示例资源
这是一个超级酷的*台!
我们将下载像上面这样的资源进行分析。HDX 提供了一个 用于与其 API 交互的 Python 库,可以通过……进行安装。
pip install hdx-python-api
然后你需要设置连接。由于我们仅下载开放数据集,因此不需要设置 API 密钥……
from hdx.utilities.easy_logging import setup_logging
from hdx.api.configuration import Configuration
from hdx.data.dataset import Dataset
setup_logging()
Configuration.create(hdx_site="prod", user_agent="my_agent_name", hdx_read_only=True)
经过一些实验,我写了一个小的包装器来下载每个数据集的资源(文件)。它支持 CSV、TSV、XLS 和 XLSX 文件类型,这些类型应该包括足够的表格用于我们的模型微调。它还保存数据集和资源的 HDX JSON 元数据以及每个文件。
def is_supported_filetype(format):
"""
Checks if the file format is currently supported for extracting meta data.
Parameters
----------
format : str
The file format to check.
Returns
-------
bool
True if the file format is supported, False otherwise.
"""
matches = ["CSV", "XLSX", "XLS", "TSV"]
if any(x in format for x in matches):
return True
else:
return False
def download_data(datasets, output_folder):
"""
Downloads data from HDX. Will save dataset and resource meta data for each file
Parameters
----------
datasets : pandas.DataFrame
A dataframe containing the datasets to download.
output_folder : str
The folder to download the data to.
"""
if not os.path.exists(output_folder):
os.mkdir(output_folder)
for index, row in datasets.iterrows():
dataset = Dataset.read_from_hdx(row["id"])
resources = dataset.get_resources()
for resource in resources:
dir = f"./{output_folder}/{row['name']}_{row['id']}"
print(
f"Downloading {row['name']} - {resource['name']} - {resource['format']}"
)
resource["dataset_name"] = row["name"]
if not os.path.exists(dir):
dump_hdx_meta_file(dataset, dir, "dataset.json")
try:
dir = f'{dir}/{get_safe_name(resource["name"])}_{get_safe_name(resource["id"])}'
if not os.path.exists(dir):
dump_hdx_meta_file(resource, dir, "resource.json")
if is_supported_filetype(resource["format"]):
url, path = resource.download(dir)
else:
print(
f"*** Skipping file as it is not a supported filetype *** {resource['name']}"
)
else:
print(f"Skipping {dir} as it already exists")
except Exception as e:
traceback.print_exc()
sys.exit()
print("Done")
上述内容有点啰嗦,因为我想能够重新启动下载,并让过程继续从中断的地方开始。此外,API 似乎偶尔会出现错误,可能是由于我的互联网连接,所以里面有一些 Try/Except。通常我不喜欢 Try/Except,但目标是创建一个训练数据集,所以我不介意缺少一些资源,只要我有一个代表性的样本来训练 GPT-3。
使用搜索 HDX API,我们搜索‘HXL’以寻找可能具有 HXL 标签的数据集,然后下载这些文件……
datasets_hxl = pd.DataFrame(Dataset.search_in_hdx("HXL"))
download_data(datasets_hxl, output_folder)
这可能需要一段时间(几个小时),所以不妨喝杯好茶!
根据我能发现的,列 HXL 标签在 HDX 资源元数据中未列出,因此要提取这些标签,我们必须分析下载的文件。经过一些实验,我写了一些辅助函数……
def check_hdx_header(first_row):
"""
This function checks if the first row of a csv file likely an HDX header.
"""
matches = ["#meta", "#country", "#data", "#loc", "#geo"]
if any(x in first_row for x in matches):
return True
else:
return False
def set_meta_data_fields(data, file, dataset, resource, sheet, type):
"""
This function create a data frame with meta data about the data, as well as a snippet of its
first nrows.
Parameters:
data: a dataframe
file: the name of the data file
dataset: the dataset JSON object from HDX
resource: the resource JSON object from HDX
sheet: the sheet name if the data was a tab in a sheet
type: the type of file, CSV, XLSX, etc.
Returns:
dict: a dictionary with metadata about the dataframe
"""
nrows = 10
# Data preview to only include values
data = data.dropna(axis=1, how="all")
cols = str(list(data.columns))
if data.shape[0] > 0:
first_row = str(list(data.iloc[0]))
has_hxl_header = check_hdx_header(first_row)
num_rows = int(data.shape[0])
num_cols = int(data.shape[1])
first_nrows = data.head(nrows)
else:
first_row = "No data"
has_hxl_header = "No data"
num_rows = 0
num_cols = 0
first_nrows = None
dict = {}
dict["resource_id"] = resource["id"]
dict["resource_name"] = resource["name"]
dict["resource_format"] = resource["format"]
dict["dataset_id"] = dataset["id"]
dict["dataset_name"] = dataset["name"]
dict["dataset_org_title"] = dataset["organization"]["title"]
dict["dataset_last_modified"] = dataset["last_modified"]
dict["dataset_tags"] = dataset["tags"]
dict["dataset_groups"] = dataset["groups"]
dict["dataset_total_res_downloads"] = dataset["total_res_downloads"]
dict["dataset_pageviews_last_14_days"] = dataset["pageviews_last_14_days"]
dict["file"] = file
dict["type"] = type
dict["dataset"] = dataset
dict["sheet"] = sheet
dict["resource"] = resource
dict["num_rows"] = num_rows
dict["num_cols"] = num_cols
dict["columns"] = cols
dict["first_row"] = first_row
dict["has_hxl_header"] = has_hxl_header
dict["first_nrows"] = first_nrows
return dict
def extract_data_details(f, dataset, resource, nrows, data_details):
"""
Reads saved CVS and XLSX HDX files and extracts headers, HDX tags and sample data.
For XLSX files, it extracts data from all sheets.
Parameters
----------
f : str
The file name
dataset : str
The dataset name
resource : str
The resource name
nrows : int
The number of rows to read
data_details : list
The list of data details
Returns
-------
data_details : list
The list of data details
"""
if f.endswith(".xlsx") or f.endswith(".xls"):
print(f"Loading xslx file {f} ...")
try:
sheet_to_df_map = pd.read_excel(f, sheet_name=None)
except Exception:
print("An exception occurred trying to read the file {f}")
return data_details
for sheet in sheet_to_df_map:
data = sheet_to_df_map[sheet]
data_details.append(
set_meta_data_fields(data, f, dataset, resource, sheet, "xlsx")
)
elif f.endswith(".csv"):
print(f"Loading csv file {f}")
# Detect encoding
with open(f, "rb") as rawdata:
r = chardet.detect(rawdata.read(100000))
try:
data = pd.read_csv(f, encoding=r["encoding"], encoding_errors="ignore")
except Exception:
print("An exception occurred trying to read the file {f}")
return data_details
data_details.append(set_meta_data_fields(data, f, dataset, resource, "", "csv"))
else:
type = f.split(".")[-1]
print(f"Type {type} for {f}")
data = pd.DataFrame()
data_details.append(set_meta_data_fields(data, f, dataset, resource, "", type))
return data_details
# Loop through downloaded folders
def extract_all_data_details(startpath, data_details):
"""
Extracts all data details for downloaded HDX files in a given directory.
Parameters
----------
startpath : str
The path to the directory containing all datasets.
data_details : list
Results
Returns
-------
data_details : pandas.DataFrame
Results, to which new meta data was appended.
See function set_meta_data_fields for columns
"""
for d in os.listdir(startpath):
d = f"{startpath}/{d}"
with open(f"{d}/dataset.json") as f:
dataset = json.load(f)
for r in os.listdir(d):
if "dataset.json" not in r:
with open(f"{d}/{r}/resource.json") as f:
resource = json.load(f)
for f in os.listdir(f"{d}/{r}"):
file = str(f"{d}/{r}/{f}")
if ".json" not in file:
data_details = extract_data_details(
file, dataset, resource, 5, data_details
)
data_details = pd.DataFrame(data_details)
return data_details
现在我们可以在之前下载的数据文件上运行……
hxl_resources_data_details = extract_all_data_details(f"./data/hxl_datasets/", [])
print(hxl_resources_data_details.shape)
(25695, 22)
这个数据框包含 25,695 行,用于在 HDX 上搜索‘HXL’时扫描 CSV 和 Excel 文件找到的每个表格数据集,包含数据预览、列名称和在某些情况下的 HXL 标签。
训练/测试拆分
通常,我会简单地使用 Scikit learn 的 train_test_split 来处理要用于模型的数据。然而,在这样做时,我注意到同一个数据集中的重复资源(文件)可能会出现在训练集和测试集中。例如,一个组织可能会提供多个机场的文件,每个文件的格式和 HXL 标签完全相同。如果我们生成一个提示数据框然后分割,这些机场将同时出现在训练集和测试集中,这无法很好地反映我们的需求,即预测全新的数据集的 HXL 标签。
为了解决这个问题,我采取了以下措施:
-
将 HDX ‘数据集’拆分为训练集/测试集(请记住,一个数据集可能包含多个资源文件)
-
使用每个,我创建了资源的数据框,每行一个数据文件。
-
然后,使用这些训练/测试资源数据框,我创建了训练/测试数据框,每列一个行。这些是 GPT-3 微调所需的提示。
创建 GPT-3 微调提示
为了微调 GPT-3,我们需要提供一个 JSONL 格式的提示和响应训练文件。我决定使用(i)列名;(ii)该列的一个数据样本。补全将是 HXL 标签和属性。
这里是一个例子……
{"prompt": " 'scheduled_service' | \"['1', '1', '0', '0', '0', '0', '0', '0']\"", "completion": " #status+scheduled"}
GPT-3 的格式非常特殊,花了一段时间才调整好!像在补全开始处添加空格这样的设置是 OpenAI 推荐的。
def get_hxl_tags_list(resources):
"""
Build a list of the HXL tags found in a dataframe of HDX resources.
Parameters
----------
resources : pandas dataframe
A dataframe of HDX resources
Returns
-------
hxl_tags : list
A list of HXL tags.
"""
hxl_tags = []
for row, d in resources.iterrows():
if d["has_hxl_header"] == True:
fr = d["first_row"].replace(" ", "")
for c in fr.split(","):
fr = re.sub("\[|\]|\"|\'","", c)
hdxs = fr.split("+")
for h in hdxs:
if h not in hxl_tags and len(h) > 0:
hxl_tags.append(h.lower())
hxl_tags = list(set(hxl_tags))
hxl_tags.remove('nan')
return hxl_tags
def get_prompt(col_name, data):
"""
Builds the prompt for GPT-3 for predicting HXL tags and attributes
Parameters
----------
col_name : str
Column name
data : list
A list of sample data for the column
Returns
-------
prompt : string
A prompt for GPT-3.
"""
ld = len(data) - 1
col_data = json.dumps(str(list(data.iloc[1:ld])))
prompt = f" {col_name} | {col_data}".lower()
return prompt
def create_training_set(resources):
"""
Builds a jsonl training data file for GPT-3 where each row is a prompt for a column HXL tag.
It will only output prompts where the sample data for the column didn't contain nans.
Parameters
----------
resources : pandas dataframe
A dataframe of HDX resources
Returns
-------
train_data : list
A list of prompts and completions for the HXL tag autocomplete feature.
"""
train_data = []
for row, d in resources.iterrows():
if d["has_hxl_header"] == True:
cols = d["columns"][1:-1].split(",")
hdxs = d["first_row"][1:-1].split(",")
data = d["first_nrows"]
has_hxl_header = d["has_hxl_header"]
if len(cols) == len(hdxs) and len(cols) > 1:
ld = len(data) - 1
for i in range(0, len(cols)):
if i < len(hdxs):
hdx = re.sub("'|\"", "", hdxs[i])
# Only include is has HXL tags and good sample data in column
if has_hxl_header == True and hdx != np.nan:
prompt = get_prompt(cols[i], data.iloc[:,i])
if 'nan' not in hdx and 'nan, nan' not in prompt:
p = {
"prompt": prompt,
"completion": f" {hdx}",
}
train_data.append(p)
return train_data
你会注意到我在上面排除了数据中存在 NaNs 的提示。我认为我们应该从良好的数据样本开始,但这是需要在未来重新审视的内容。
我们现在可以生成一个训练数据集,并将其保存为 GPT-3 的文件……
# Create training set
X_train = create_training_set(X_train_resources)
print(f"Training records: {len(X_train)}")
train_file = "fine_tune_openai_train.jsonl"
with open(train_file, "w") as f:
for p in X_train:
json.dump(p, f)
f.write("\n")
print("Done")
这就是训练数据的样子……
{"prompt": " 'Country ISO3' | \"['COD', 'COD', 'COD', 'COD', 'COD', 'COD', 'COD', 'COD']\"", "completion": " #country+code"}
{"prompt": " 'Year' | \"['2010', '2005', '2000', '1995', '1990', '1985', '1980', '1975']\"", "completion": " #date+year"}
{"prompt": " 'Indicator Name' | \"['Barro-Lee: Percentage of female population age 15-19 with no education', 'Barro-Lee: Percentage of female population age 15-19 with no education', 'Barro-Lee: Percentage of female population age 15-19 with no education', 'Barro-Lee: Percentage of female population age 15-19 with no education', 'Barro-Lee: Percentage of female population age 15-19 with no education', 'Barro-Lee: Percentage of female population age 15-19 with no education', 'Barro-Lee: Percentage of female population age 15-19 with no education', 'Barro-Lee: Percentage of female population age 15-19 with no education']\"", "completion": " #indicator+name"}
{"prompt": " 'Indicator Code' | \"['BAR.NOED.1519.FE.ZS', 'BAR.NOED.1519.FE.ZS', 'BAR.NOED.1519.FE.ZS', 'BAR.NOED.1519.FE.ZS', 'BAR.NOED.1519.FE.ZS', 'BAR.NOED.1519.FE.ZS', 'BAR.NOED.1519.FE.ZS', 'BAR.NOED.1519.FE.ZS']\"", "completion": " #indicator+code"}
{"prompt": " 'Value' | \"['48.1', '51.79', '52.1', '43.62', '35.44', '38.02', '43.47', '49.08']\"", "completion": " #indicator+value+num"}
{"prompt": " 'Country ISO3' | \"['COD', 'COD', 'COD', 'COD', 'COD', 'COD', 'COD', 'COD']\"", "completion": " #country+code"}
{"prompt": " 'Year' | \"['2015', '2014', '2013', '2012', '2011', '2010', '2009', '2008']\"", "completion": " #date+year"}
这个训练数据集中有 139,503 行,每列一行,来自我们从 HDX 下载的表格数据,专门用于那些列中有 HXL 标签的情况。
生成 OpenAI API 密钥
在我们能做任何事情之前,你需要先注册一个 OpenAI 账户。完成后,你应该有$18 的免费积分。如果使用少量数据,这应该足够,但在这次分析和几次模型训练中,我累计了 $50 的账单,因此你可能需要将信用卡绑定到你的账户上。
一旦你有了账户,你可以生成 API 密钥。我选择将其保存到本地文件并在代码中引用,但OpenAI Python 库也支持使用环境变量。
微调 GPT-3
好了,现在是令人兴奋的部分!有了我们精美的训练数据,我们可以按如下方式微调 GPT-3……
import openai
from openai import cli
# Open AI API key should be put into this file
openai.api_key_path = "./api_key.txt"
print("Uploading training file ...")
training_id = cli.FineTune._get_or_upload(train_file, True)
# validation_id = cli.FineTune._get_or_upload(validation_file_name, True)
print("Fine-tuning model ...")
create_args = {
"training_file": training_id,
# "validation_file": test_file,
"model": "ada",
}
# https://beta.openai.com/docs/api-reference/fine-tunes/create
resp = openai.FineTune.create(**create_args)
job_id = resp["id"]
status = resp["status"]
print(f"Fine-tunning model with jobID: {job_id}.")
在上面,我们将微调模型提交给 OpenAI,然后可以查看状态……
result = openai.FineTune.retrieve(id=job_id)
print(result['status'])
我选择保持简单,但你也可以将其提交给 OpenAI,并通过此处显示的流来监控状态。
一旦状态显示为‘成功’,你现在可以获得一个模型 ID 用于预测(补全)……
result = openai.FineTune.retrieve(id=job_id)
model = result["fine_tuned_model"]
用我们微调过的 GPT-3 模型预测 HXL 标签
我们现在有了一个模型,来看看它能做什么!
要调用 GPT-3,你可以使用Open AI Python 库的 ‘create’ 方法。查看文档了解你可以调整的参数是值得的。
def create_prediction_dataset_from_resources(resources):
"""
Generate a list of model column-level prompts from a list of resources (tables).
It will only output prompts where the sample data for the column didn't contain nans.
Parameters
----------
resources : list
A list of dictionaries containing the resource name, columns, first_row, and first_nrows.
Returns
-------
prediction_data : list
A list of dictionaries containing GPT-3 prompts (one per column in resource table)
"""
prediction_data = []
for index, d in resources.iterrows():
cols = d["columns"][1:-1].split(",")
hdxs = d["first_row"][1:-1].split(",")
data = d["first_nrows"]
has_hxl_header = d["has_hxl_header"]
if len(cols) == len(hdxs) and len(cols) > 1:
ld = len(data) - 1
# Loop through columns
for i in range(0, len(cols)):
if i < len(hdxs) and i < data.shape[1]:
prompt = get_prompt(cols[i], data.iloc[:,i])
# Skip any prompts with at least two nan values in sample data
if 'nan, nan' not in prompt:
r = {
"prompt": prompt
}
# If we were called with HXL tags (ie for test set), populate 'expected'
if has_hxl_header == True:
hdx = re.sub("'|\"| ", "", hdxs[i])
# Row has HXL tags, but this particular column doesn't have tags
if hdx == 'nan':
continue
else:
r["expected"]= hdx
prediction_data.append(r)
return prediction_data
def make_gpt3_prediction(prompt, model, temperature=0.99, max_tokens=13):
"""
Wrapper to call GPT-3 to make a prediction (completion) on a single prompt.
Parameters
----------
prompt : str
Prompt to use for prediction
model : str
GPT-3 model to use
temperature : float
Temperature to use for sampling
max_tokens : int
Maximum number of tokens to use for sampling
Returns
-------
result : dict
Dictionary with prompt, predicted, and
log probabilities of each completed token
"""
result = {}
result["prompt"] = prompt
model_result = openai.Completion.create(
engine=model,
prompt=prompt,
temperature=temperature,
max_tokens=max_tokens,
top_p=1,
frequency_penalty=0,
presence_penalty=0,
stop=["\n"],
logprobs=1
)
result["predicted"] = model_result["choices"][0]["text"].replace(" ","")
result["logprobs"] = model_result['choices'][0]['logprobs']['top_logprobs']
return result
def make_gpt3_predictions(
sample_size, prediction_data, model, temperature=0.99, max_tokens=13, logprob_cutoff=-0.01
):
"""
Wrapper to call GPT-3 to make predictions on test file for sample_size samples.
Parameters
----------
sample_size : int
Number of predictions to make from test file
prediction_data : list
List of dictionaries with prompts
model : str
GPT-3 model to use
postprocess : bool
Whether to postprocess the predictions
temperature : float
Temperature to use for sampling
max_tokens : int
Maximum number of tokens to use for sampling
prob_cutoff : float
Logprob cutoff for filtering out low probability tokens
Returns
-------
results : list
List of dictionaries with prompt, predicted, predicted_post_processed
"""
results = []
prediction_data = sample(prediction_data, sample_size)
for i in range(0, sample_size):
prompt = prediction_data[i]["prompt"]
res = make_gpt3_prediction(
prompt, model, temperature, max_tokens
)
# Filter out low logprob predictions
pred = ""
seen_tokens = []
for w in res["logprobs"]:
token = list(w.keys())[0]
prob = w[token]
if prob > logprob_cutoff and token not in seen_tokens:
pred += token
if '+' not in token:
seen_tokens.append(token)
else:
break
pred = re.sub(r" |\+$|\+v_$", "", pred)
r = {
"prompt": prompt,
"predicted": res["predicted"],
"predicted_log_prob_cutoff": pred,
#"logprobs": res["logprobs"]
}
# For test sets we have expected values, add back for performance reporting
if "expected" in prediction_data[i]:
r['expected'] = prediction_data[i]['expected'].replace(' ', '')
results.append(r)
return results
我们使用以下方式调用,限制为 500 个提示……
# Generate the prompts we want GPT-3 to complete
print("Building model input ...")
prediction_data = create_prediction_dataset_from_resources(X_test_resources)
# How many predictions to try from the test set
sample_size = 500
# Make the predictions
print("Making GPT-3 predictions (completions) ...")
results = make_gpt3_predictions(
sample_size, prediction_data, model, temperature=0.99, max_tokens=20, logprob_cutoff=-0.001
)
这产生了以下结果……
def output_prediction_metrics(results, prediction_field="predicted_post_processed"):
"""
Prints out model performance report if provided results in the format:
[
{
'prompt': ' \'ISO3\' | "[\'RWA\', \'RWA\', \'RWA\', \'RWA\', \'RWA\', \'RWA\', \'RWA\', \'RWA\']"',
'predicted': ' #country+code+iso3+v_iso3+',
'predicted_post_processed': '#country+code',
'expected': '#country+code'
},
... etc ...
]
Parameters
----------
results : list
See above for format
prediction_field : str
Field name of element with prediction. Handy for comparing raw and post-processed predictions.
"""
y_test = []
y_pred = []
y_justtag_test = []
y_justtag_pred = []
for r in results:
if "expected" not in r:
print("Provided results do not contain expected values.")
sys.exit()
y_pred.append(r[prediction_field])
y_test.append(r["expected"])
expected_tag = r["expected"].split("+")[0]
predicted_tag = r[prediction_field].split("+")[0]
y_justtag_test.append(expected_tag)
y_justtag_pred.append(predicted_tag)
print(f"GPT-3 results for {prediction_field}, {len(results)} predictions ...")
print("\nJust HXL tags ...\n")
print(f"Accuracy: {round(accuracy_score(y_justtag_test, y_justtag_pred),2)}")
print(
f"Precision: {round(precision_score(y_justtag_test, y_justtag_pred, average='weighted', zero_division=0),2)}"
)
print(
f"Recall: {round(recall_score(y_justtag_test, y_justtag_pred, average='weighted', zero_division=0),2)}"
)
print(
f"F1: {round(f1_score(y_justtag_test, y_justtag_pred, average='weighted', zero_division=0),2)}"
)
print(f"\nTags and attributes with {prediction_field} ...\n")
print(f"Accuracy: {round(accuracy_score(y_test, y_pred),2)}")
print(
f"Precision: {round(precision_score(y_test, y_pred, average='weighted', zero_division=0),2)}"
)
print(
f"Recall: {round(recall_score(y_test, y_pred, average='weighted', zero_division=0),2)}"
)
print(
f"F1: {round(f1_score(y_test, y_pred, average='weighted', zero_division=0),2)}"
)
return
output_prediction_metrics(results, prediction_field="predicted")
GPT-3 results for predicted, 500 predictions ...
Just HXL tags ...
Accuracy: 0.99
Precision: 0.99
Recall: 0.99
F1: 0.99
Tags and attributes with predicted ...
Accuracy: 0.0
Precision: 0.0
Recall: 0.0
F1: 0.0
嗯!?那,嗯……糟糕。仅预测 HXL 标签效果非常好,但预测标签和属性的效果就差很多。
让我们看看一些失败的预测……
{
"prompt": " 'gho (code)' | \"['mort_100', 'mort_100', 'mort_100', 'mort_100', 'mort_100', 'mort_100', 'mort_100', 'mort_100']\"",
"predicted": "#indicator+code+v_hor_funder_",
"expected": "#indicator+code"
}
{
"prompt": " 'region (code)' | \"['afr', 'afr', 'afr', 'afr', 'afr', 'afr', 'afr', 'afr']\"",
"predicted": "#region+code+v_reliefweb+f",
"expected": "#region+code"
}
{
"prompt": " 'dataid' | \"['310633', '310634', '310635', '310636', '310629', '310631', '310630', '511344']\"",
"predicted": "#meta+id+fts_internal_view_all",
"expected": "#meta+id"
}
{
"prompt": " 'gho (url)' | \"['https://www.who.int/data/gho/indicator-metadata-registry/imr-details/5580', 'https://www.who.int/data/gho/indicator-metadata-registry/imr-details/5580']\"",
"predicted": "#indicator+url+name+has_more_",
"expected": "#indicator+url"
}
{
"prompt": " 'year (display)' | \"['2014', '2014', '2014', '2014', '2014', '2014', '2014', '2014']\"",
"predicted": "#date+year+name+tariff+for+",
"expected": "#date+year"
}
{
"prompt": " 'byvariablelabel' | \"[nan]\"",
"predicted": "#indicator+label+code+placeholder+Hubble",
"expected": "#indicator+label"
}
{
"prompt": " 'gho (code)' | \"['ntd_bejelstatus', 'ntd_pintastatus', 'ntd_yawsend', 'ntd_leishcend', 'ntd_leishvend', 'ntd_leishcnum_im', 'ntd_leishcnum_im', 'ntd_leishcnum_im']\"",
"predicted": "#indicator+code+v_ind+olk_ind",
"expected": "#indicator+code"
}
{
"prompt": " 'enddate' | \"['2002-12-31', '2003-12-31', '2004-12-31', '2005-12-31', '2006-12-31', '2007-12-31', '2008-12-31', '2009-12-31']\"",
"predicted": "#date+enddate+enddate+usd+",
"expected": "#date+end"
}
{
"prompt": " 'endyear' | \"['2013', '2013', '2013', '2013', '2013', '2013', '2013', '2013']\"",
"predicted": "#date+year+endyear+end_of_",
"expected": "#date+year+end"
}
{
"prompt": " 'country (code)' | \"['dnk', 'dnk', 'dnk', 'dnk', 'dnk', 'dnk', 'dnk', 'dnk']\"",
"predicted": "#country+code+v_iso2+v_",
"expected": "#country+code"
}
有趣的是。似乎模型几乎完美地完成并捕捉了正确的标签和属性,然后在末尾添加了一些额外的属性。例如……
"predicted": "#country+code+v_iso2+v_",
"expected": "#country+code"
让我们看看期望的标签和属性在预测的前半部分出现的频率……
passes = 0
fails = 0
for r in results:
if r["predicted"].startswith(r["expected"]):
passes += 1
else:
fails += 1
#print(json.dumps(r, indent=4, sort_keys=False))
print(f" Out of {passes + fails} predictions, the expected tags and attributes where in the predicted tags and attributes {round(100*passes/(passes+fails),1)}% of the time.")
Out of 500 predictions, the expected tags and attributes where in the predicted tags and attributes 99.0% of the time.
在 500 次预测中,期望的标签和属性99%的时间都出现在预测的标签和属性中。换句话说,期望的值通常是大多数预测的首部分。
所以 GPT-3 在预测标签和属性方面具有很高的准确性,但在末尾添加了额外的属性。
那么,如何排除那些额外的标记呢?
嗯,结果证明 GPT-3 返回了每个标记的对数概率。如上所述,我们还计算了一个预测,假设我们在对数概率高于某个截止值时停止完成标记……
# Filter out low logprob predictions
pred = ""
seen_tokens = []
for w in res["logprobs"]:
token = list(w.keys())[0]
prob = w[token]
if prob > logprob_cutoff and token not in seen_tokens:
pred += token
if '+' not in token:
seen_tokens.append(token)
else:
break
pred = re.sub(r" |\+$|\+v_$", "", pred)
让我们看看在假设截止值为 -0.001 的情况下表现如何……
output_prediction_metrics(results, prediction_field="predicted_log_prob_cutoff")
Just HXL tags ...
Accuracy: 0.99
Precision: 1.0
Recall: 0.99
F1: 0.99
Tags and attributes with predicted_log_prob_cutoff ...
Accuracy: 0.94
Precision: 0.99
Recall: 0.94
F1: 0.95
这还不错,标签和属性的准确率为 0.94。既然我们知道正确的标签和属性在预测中出现的概率为 99%,我们应该通过调整对数概率截止值和进行一些后处理来做得更好。
结论与未来工作
以上是对 GPT-3 在预测元数据,特别是人道主义数据集上的 HXL 标签方面应用的快速分析。它在这一任务上的表现非常好,并且在类似的元数据预测任务中有很大的潜力。
当然,还需要更多的工作来完善方法,例如:
-
尝试其他模型(我上面使用的是 'ada')以查看是否能改善性能(尽管这会增加成本)。
-
模型超参数调整。对数概率截止值可能非常重要。
-
可能需要更多的提示工程,比如在表格中包含列列表,以提供更好的上下文,以及在两行标题表格上的覆盖列。
-
更多的预处理。这篇文章的处理不多,盲目地使用从 CSV 文件中提取的表格,因此数据可能有些混乱。
也就是说,我觉得使用 GPT-3 来预测数据集上的元数据有很大的潜力。
敬请关注更多更新!
参考文献
[1] Mark Lowcock, 人道主义事务副秘书长和紧急救援协调员,Anticipation saves lives: How data and innovative financing can help improve the world’s response to humanitarian crises (2019)
[2] Sarah Telford, Opinion: Humanitarian world is full of data myths. Here are the most popular (2018)
[3] Vinitra Swamy 等人,人道主义数据的机器学习:使用 HXL 标准进行标签预测(2019 年)
用于此分析的笔记本可以在这里找到。
预测 NBA 薪资的机器学习方法
使用 Python 构建机器学习模型,以预测 NBA 薪资并分析最有影响力的变量
·
关注 发表在 Towards Data Science · 9 分钟阅读 · 2023 年 8 月 24 日
--
(照片由 Emanuel Ekström 提供,来源于 Unsplash)
NBA 作为体育界最盈利和竞争激烈的联赛之一脱颖而出。在过去几年里,NBA 球员的薪资呈上升趋势,但每一个令人惊叹的扣篮和三分球背后都隐藏着决定这些薪资的复杂因素。
从球员表现和球队成功到市场需求和代言交易,许多变量都会影响结果。谁没想过为什么他们的球队会在表现不佳的球员身上花这么多钱,或对成功的交易感到惊讶?
在本文中,我们利用 Python 的机器学习能力来预测 NBA 薪资,并揭示对球员收入影响最大的关键因素。
所有使用的代码和数据都可以在GitHub上找到。
理解问题
在深入探讨问题之前,了解联赛薪资系统的基本原理至关重要。当一名球员在市场上待签合同时,他被称为自由球员(FA),这是本项目中将频繁出现的术语。
NBA 在一套复杂的规则和规定下运营,旨在保持球队之间的竞争*衡。这个系统的核心有两个关键概念:薪资上限和奢侈税。
薪资上限作为支出限制,限制了球队在一个赛季中可以花费的球员薪资总额。上限由联赛收入决定,每年更新一次,以确保球队在合理的财务框架内运营。它还旨在防止大市场球队显著超支,促进球队之间的公*竞争。
薪资上限在球员之间的分配可以有所不同,顶级球员有最高薪资,而新秀和老将则有最低薪资。
然而,超越薪资上限并不罕见,尤其是对那些希望组建争冠阵容的球队。当一支球队超过薪资上限时,它进入了奢侈税的范畴。奢侈税对超支的球队施加处罚,抑制球队过度支出,同时为联赛提供额外收入。
还有许多其他规则作为例外,例如中层例外(MLE)和交易例外,允许球队进行战略性的阵容调整,但对于这个项目来说,了解薪资上限和奢侈税就足够了。

NBA 薪资上限从 1984 年到 2023 年的演变(图片来源:作者)
由于薪资上限的持续增长,选择的方式将是使用薪资上限的百分比作为目标,而不是薪资金额本身。这个决定旨在融入薪资上限的演变特性,确保结果不会受到时间变化的影响,并在评估历史赛季时仍然适用。然而,需要注意的是,这并不完美,仅仅是一个*似值。
数据
对于这个项目,目标是使用仅来自上一个赛季的数据来预测球员签订新合同的薪资。
使用的个人统计数据包括:
-
每场比赛的*均统计数据
-
总统计数据
-
高级统计数据
-
个体变量:年龄、位置
-
与薪资相关的变量:上一赛季的薪资、上一赛季和当前赛季的最大薪资上限以及该薪资的上限百分比。

2022–23 赛季的薪资分布(图片由作者提供)
由于我们不知道球员将签约的球队,只包括了个体特征。
总体而言,这项研究为每名球员提供了78 个特征加上目标。
大部分数据通过BRScraper获得,这是我最*创建的Python 包,可以轻松抓取和访问来自Basketball Reference的篮球数据,包括 NBA、G 联盟和其他国际联赛。遵守了所有关于不对网站造成伤害或阻碍其性能的指南。
数据处理
一个值得考虑的有趣方面是选择球员来训练模型。最初,我选择了所有可用的球员,但大多数球员可能已经有合同,在这种情况下,薪资值不会发生剧烈变化。
例如,假设一名球员签订了为期 4 年的 2000 万美元合同。他每年大约获得 500 万美元(虽然很少每年的薪资完全相同,通常薪资在 500 万美元左右会有一定的递增)。然而,当自由球员签订新合同时,薪资值可能会发生更大幅度的变化。
这意味着用所有可用球员训练模型可能会整体上表现更好(毕竟,大多数球员的薪资接*最后一个!),但在评估仅自由球员时,表现会显著变差。
由于目标是预测签订新合同的球员的薪资,因此数据中仅应包含这种类型的球员,以便模型能更好地理解这些球员之间的模式。
关注的赛季是即将到来的 2023–24 赛季,但会使用2020–21 赛季及之后的数据来增加样本量,这得益于目标的选择。由于缺乏自由球员的数据,未使用较早的赛季。
这使得在选择的三个赛季中有426 名球员,其中 84 名是 2023–24 赛季的自由球员。
建模
训练-测试拆分的设计是为了确保 2023–24 赛季的所有自由球员仅包含在测试集中,保持了大约 70/30 的拆分比例。
最初使用了几种回归模型:
-
支持向量机(SVM)
-
弹性网
-
随机森林
-
AdaBoost
-
梯度提升
-
轻量级梯度提升机(LGBM)
通过均方根误差(RMSE)和决定系数(R²)评估了每种方法的表现。
你可以在我之前的文章中找到每个指标的公式和解释,使用机器学习预测 NBA MVP。
构建一个机器学习模型来预测 NBA MVP 并分析最有影响力的变量。
towardsdatascience.com
结果
查看整个数据集中的所有赛季,获得了以下结果:

各种模型中获得的 RMSE 和 R²值(图像由作者提供)
模型整体表现良好,其中随机森林和梯度提升取得了最低的 RMSE 和最高的 R²,而AdaBoost在使用的模型中表现最差。
变量分析
一种有效的可视化模型预测关键变量的方法是通过SHAP 值,这是一种提供合理解释每个特征如何影响模型预测的技术。
关于 SHAP 及其图表解读的更深入解释可以在使用机器学习预测 NBA MVP中找到。

与随机森林模型相关的 SHAP 图表(图像由作者提供)
我们可以从这个图表中得出一些重要结论:
-
每场比赛的分钟数 (MP) 和 得分 (PTS) 每场比赛 和 总数 是三个最有影响力的特征。
-
上赛季薪资 (Salary S-1) 和 该薪资的薪资占比 (% Cap S-1) 也非常有影响力,分别排在第 4 位和第 5 位。
-
先进统计数据 在最重要的特征中并不占主导地位,只有两个出现了在列表中,WS (胜利贡献值) 和 VORP (替代球员价值)。
这是一项非常令人惊讶的结果,因为与MVP 项目不同,在该项目中,先进的统计数据主导了 SHAP 的最终结果,球员薪资似乎与常见统计数据如分钟、得分和首发场次有更大的关系。
这令人惊讶,因为大多数先进统计数据的设计初衷正是为了更好地评估球员的表现。PER (球员效率评级) 在前 20 名中缺席(排名第 43)尤为引人注目。
这提出了一个可能性,即在薪资谈判过程中,总经理可能遵循了一种相对简单的方法,可能忽视了更广泛的表现评估指标。
也许问题并没有那么复杂! 简化来看,打得时间最长、得分最多的球员赚得更多!
附加结果
聚焦于今年的自由球员,并将他们的预测与实际薪水进行比较:

2023–24 赛季随机森林模型的主要结果(单位:百万)(图片由作者提供)
在顶部,我们有五名被低估的球员(收入低于他们应得的),中间是五名估值正确的球员,底部是五名被高估的球员(收入高于他们应得的)。值得注意的是,这些评估仅基于模型的输出。
从顶部开始,前 MVP拉塞尔·威斯布鲁克是模型中最被低估的球员,我认为这是事实,因为他与快船签下了约每年 400 万美元的合同。埃里克·戈登、梅森·普拉姆利和马利克·比斯利也处于类似的情况,他们在获得良好表现的同时收入非常少。达安吉洛·拉塞尔也出现在这五人榜单中,尽管他的年薪为 1700 万美元,这表明他应该赚得更多。
值得注意的是,这些球员都签约了竞争性的球队(快船、太阳、雄鹿和湖人)。这是一种已知行为,球员们选择减少薪水以有机会为能够赢得冠军的球队效力。
在中间,塔雷安·普林斯、奥兰多·罗宾逊、凯文·诺克斯和德里克·罗斯的薪水都较低,看似足够。卡里斯·勒弗特年薪 1500 万美元,但也确实值这个价钱。
在底部,弗雷德·范弗利特被评为最被高估的球员。火箭队作为一支重建中的球队,在他的三年合同上投入了 1.285 亿美元,这是一项引人注目的举动。他们还签下了迪龙·布鲁克斯,合同金额高于预期。
克里斯·米德尔顿在这个夏天签下了大合同。尽管雄鹿队是一个竞争者队伍,但他们属于非主要市场,无法承受失去其中一名最佳球员的风险。德雷蒙德·格林和卡梅隆·约翰逊在各自的球队中也有类似的情况。
结论
预测体育结果始终充满挑战。从目标选择到球员筛选,这个项目证明比预期要复杂。然而,结果证明其实相当简单,取得的结果非常令人满意!
当然,还有多种改进的方法,其中之一是使用特征选择或降维技术来减少特征空间,从而减少方差。
此外,访问之前赛季的自由球员数据也会使样本数量增加。然而,目前似乎没有公开获取这些数据的途径。
许多其他外部变量也会影响此问题。例如,毫无疑问,如果某种方式能够知道球队,像去年种子、季后赛结果和已经使用的薪资百分比这样的变量可能会非常有用。然而,保持镜像实际自由球员场景的方式,即球队未知,可能会得出更贴*球员“真实价值”的结果,不管签约球队的背景如何。
本项目的主要前提之一是仅使用上一赛季的数据来预测下一赛季的薪水。加入旧赛季的统计数据确实可能会提高结果,因为球员的历史表现可以提供有价值的见解。然而,这些数据的广泛性质将需要精心的特征选择来管理其复杂性和高维度。
再次说明,所有使用的代码和数据都可以在GitHub上找到。

(照片由Marius Christensen拍摄,来源于Unsplash)
我始终可以通过我的渠道联系到(LinkedIn 和 GitHub)。
感谢您的关注!👏
加布里埃尔·斯佩兰扎·帕斯托雷洛
预测星巴克奖励计划的成功
原文:
towardsdatascience.com/predicting-success-of-a-reward-program-at-starbucks-b32b77dcf9b8
初学者友好 — 从头到尾逐步解释完整项目
·发布在 Towards Data Science ·9 分钟阅读·2023 年 6 月 20 日
--

照片由 Robert Linder 在 Unsplash 拍摄
项目概述
该项目专注于识别能够有效吸引星巴克现有客户并吸引新客户的奖励计划优惠。
星巴克是一家数据驱动的公司,通过利用包含客户信息、特别优惠和交易数据的数据集,致力于全面了解其客户。
为了开发一个能够确定奖励计划成功的模型,我将项目分为三个阶段:
-
检查和清理 Udacity 提供的数据。
-
创建一个结合所有相关信息的数据集。
-
构建和评估三个分类模型的性能以预测特定人员的奖励计划的成功或失败。
问题陈述
对市场营销活动进行重大投资是一个复杂的决策,需要获得各种利益相关者的批准、财务资源和时间。因此,拥有一个能够对特定目标群体发起特定优惠是否值得的预测模型,对于任何公司来说都是一个战略资产。
为了创建这个模型,我们将使用监督学习技术进行二分类。
模型的结果将指示该优惠是否预计会有效。
数据集探索与整理
Udacity 提供了三个 JSON 格式的数据集:portfolio、profile 和 transcript。每个数据集都有不同的用途,并为我们的分析提供了宝贵的信息。
Portfolio 数据集
该数据集提供了有关星巴克当前有效优惠的信息。
-
id(字符串)——优惠 id
-
offer_type(字符串)——优惠类型,即 BOGO、折扣、信息性
-
difficulty(整数)——完成优惠所需的最低花费
-
reward(整数)——完成优惠后给予的奖励
-
duration(整数)——优惠开放的时间,以天为单位
-
channels(字符串列表)

组合数据集(原始)——图像作者:Erdem Isbilen
portfolio数据集中有十行和六列。这是一个简单的数据集,没有缺失、空值或重复值。
‘channels’,‘id’,‘offer_type’列是分类变量,而‘difficulty’,‘duration’,‘reward’是整数。
请参见我对数据集所做的修改:
-
对‘channels’和‘offer_type’进行独热编码
-
将‘id’更改为‘offer_id’

组合数据集(经过数据清理)——图像作者:Erdem Isbilen
个人资料数据集
个人资料数据集包含有关星巴克客户的人口统计信息。
-
age(整数)——客户的年龄
-
became_member_on(整数)——客户创建应用账户的日期
-
gender(字符串)——客户的性别(注意有些条目包含‘O’代表其他,而不是 M 或 F)
-
id(字符串)——客户 id
-
income(浮点数)——客户的收入

个人资料数据集(原始)——图像作者:Erdem Isbilen
这个数据集中有 17000 行(数据集中唯一的人员数量)和 5 列,有 2175 个空值(出现在gender和income列中)。由于这些行的年龄值也为 118,我从数据集中删除了所有 2175 行。
请参见我对数据集所做的修改:
-
删除 2175 行缺失值(也包括年龄值为 118 的行)
-
将‘id’更改为‘customer_id’
-
‘become_member_on’字符串转日期
-
创建‘year_joined’和‘membership_days’列
-
对‘gender’进行独热编码
-
创建‘age_group’以将客户分类为青少年、年轻成人、成年人、老年人
-
创建‘income_range’以将客户分类为普通、高于*均水*、高收入
-
创建‘member_type’以将客户分类为新会员、常规会员、忠实会员

个人资料数据集(经过数据清理)——图像作者:Erdem Isbilen
可以看到,加入该计划的人数在 2013 年至 2017 年之间呈上升趋势,2017 年是最佳年份。50%的会员年龄在 42 至 66 岁之间。

年龄、收入、加入年份列的直方图——图像作者:Erdem Isbilen
如下所示,在低收入和中等收入区男性人口超过女性人口,而在高收入区女性人口超过男性人口。

不同性别群体收入的直方图图表— 图片来源(作者)Erdem Isbilen
在考虑性别时,数据集存在一些偏差,因为男性人口数量超过女性人口,而且其他类别的人数较少。具体来说,数据集中有 8484 名男性、6129 名女性和仅 212 名其他类别的人。

性别的直方图图表— 图片来源(作者)Erdem Isbilen
记录集数据
记录集数据捕捉了客户与优惠活动的互动。
-
event (str) — 记录描述(即交易、优惠接收、优惠查看等)
-
person (str) — 客户 ID
-
time (int) — 从测试开始的时间,以小时为单位。数据从时间 t=0 开始
-
value — (字符串字典) — 根据记录是优惠 ID 还是交易金额

记录集数据(原始)— 图片来源(作者)Erdem Isbilen
如果记录集中的事件对应于三种可能的优惠状态之一(查看、接收或完成),则值列包含优惠的id。除了优惠 ID 之外,如果事件状态为‘offer completed’,还会有reward 值。
但是,如果事件是交易,则值列将仅显示交易金额。
请查看我对数据集所做的修改:
-
将‘value’展开为‘offer_id’、‘amount’、‘rewards’ 新列。
-
通过将时间(小时)转换为天数来创建‘time_in_days’。
-
将‘person’更改为‘customer_id’
-
将记录集数据拆分为两个子数据集:offer_tr(优惠数据)和transaction_tr(交易数据)

offer_tr 数据集(数据处理后)— 图片来源(作者)Erdem Isbilen
构建模型
一旦数据集被清理并完成必要的修改,我们需要将它们合并为一个单一的数据集。然后我们将创建一个名为offer_successful的新列。该列将指示某个优惠是否对特定客户成功。这将使我们能够建立可以预测特定类型客户对某个优惠是否成功的模型。
客户必须查看和完成优惠活动在允许的时间范围内,才能将该优惠视为成功。
在开发模型时遇到的困难是数据集中没有特定的列指明某个客户的报价是否成功。因此,我开发了一个辅助函数,通过考虑已完成和查看的报价数据以及这些事件之间的时间范围来计算目标值。
成功和失败的报价数量分别为 35136 和 31365。这意味着我们在考虑目标时有一个*衡的数据集。因此,我们在选择模型时没有任何限制(这些限制通常与不*衡的数据集有关)。
为了创建一个可以预测报价是否成功的模型,我们需要在最终数据集上训练一个模型。
由于这是一个二分类问题,我们将使用三种不同的监督学习算法。
-
逻辑回归
-
随机森林
-
梯度提升
我将使用 sklearn 的默认设置开始构建模型,以了解不同模型的准确性水*。
然后,我将使用RandomizedSearchCV进行 12 次迭代,以优化模型的超参数,因为它比GridSearchCV计算开销更小。RandomizedSearchCV通过从指定分布中随机抽样超参数值来工作。
这些模型中的每一个都在训练数据集上进行训练,并在测试数据集上进行评估,以避免过拟合,并查看模型在未见数据上的表现。
指标和结果
我将使用混淆矩阵和下面的指标来评估模型的性能。
我将特别关注精确度,因为研究的主要目的是尽可能准确地定义正类。
-
准确率: 准确率是评估分类模型准确性最常用的指标。它通过将正确预测的数量除以预测总数来计算。
-
精确度: 精确度是衡量模型在预测正类时准确性的指标。它通过将真正例的数量除以真正例的数量加上假正例的数量来计算。
-
召回率: 召回率是衡量模型在预测正类时完整性的指标。它通过将真正例的数量除以真正例的数量加上假负例的数量来计算。
-
F1 分数: F1 分数是精确度和召回率的加权*均值。它通过将 2 * (精确度 * 召回率)除以(精确度 + 召回率)来计算。
超参数微调前的初始准确率指标:

表格形式的模型表现 — 图片作者(Erdem Isbilen)
超参数微调后的结果:

表格形式的模型表现 - 图片作者(Erdem Isbilen)
尽管所有模型提供了类似的性能指标,但随机森林的准确性略微更高。我们发现超参数的微调对模型性能的影响很小。这可能是因为我们为调优选择的自定义参数比默认参数更差。因此,我们可能需要考虑选择不同的参数集来微调模型。
论证
由于随机森林和梯度提升可以更好地处理异常值和高维数据,因此它们在我们的案例中表现优于逻辑回归。
结论
在这个项目中,我分析了星巴克提供的数据集,并开发了一个可以预测顾客是否会完成优惠的模型。
我对每个数据集进行了探索性分析,并可视化数据以获得全面的理解。这包括检查数据集的各个方面。随后,我进行了预处理部分,这是最耗时和具有挑战性的任务。数据集复杂,包含的数据需要应用数据整理、工程和预处理技术,以获得最终的清洁版本。
最后一步是开发几个二元分类模型以进行预测。模型构建完成后,使用测试数据评估模型的性能。结论是这三种模型提供了类似的性能结果。
改进
尽管这些模型提供了一个良好的起点,但精度为 66%的结果仍有改进的空间。
对于这个项目的一个有趣改进是创建多个监督学习模型,并将它们组合成一个自定义集成模型。通过组合多个监督学习模型来创建集成模型可以带来错误补偿的优势。通过利用不同模型的优势并弥补它们的弱点,集成模型实现了更好的泛化性能,从而提高了准确性和鲁棒性。
为了提高我们奖励计划预测的准确性,我们可以考虑将不同类型的奖励分开,并为每个计划开发单独的模型。通过这种方法,我们可以根据每个奖励计划的具体特征和目标量身定制我们的建模技术,从而实现更精确的预测和更好的结果。这种方法还可以帮助我们识别每个奖励计划中的任何独特趋势或模式,从而使计划设计和实施更加有效。
另一种优化奖励计划效果的方法是识别并排除那些无论奖励计划如何都购买的个人。通过这样做,我们可以将资源集中在那些更有可能积极影响计划结果的人身上,从而最大化我们从计划中获得的利益。
反思
我发现这个顶点项目是一个非常愉快的经历,让我能够提升数据预处理和建模的技能。数据预处理步骤是最耗时且令人畏惧的任务。然而,数据集本身很有启发性,这激励我继续努力工作。
使用 XGBoost 预测水泵的功能性
原文:
towardsdatascience.com/predicting-the-functionality-of-water-pumps-with-xgboost-8768b07ac7bb
一个从“数据挖掘水表”竞赛中获得灵感的端到端机器学习项目
·发表于 Towards Data Science ·10 分钟阅读·2023 年 6 月 1 日
--

图片由 Kelly 拍摄:www.pexels.com/photo/close-up-of-a-child-s-hands-catching-water-from-the-spout-of-a-water-pump-3030281/
目录
∘ 介绍
∘ 目标
∘ 工具/框架
∘ 探索性数据分析
∘ 特征工程
∘ 创建训练和测试数据集
∘ 确定评估指标
∘ 创建基线模型
∘ 数据建模方法
∘ 超参数调优方法
∘ XGBoost 模型
∘ CatBoost 模型
∘ LightGBM 模型
∘ 选择最佳模型
∘ 模型解释
∘ 模型部署
∘ 限制
∘ 结论
∘ 参考文献
介绍
注:此项目灵感来自 DrivenData 主办的Pump it Up: 数据挖掘水表竞赛。
坦桑尼亚目前面临严重的水危机,28%的居民缺乏安全用水。一种可行的解决办法是确保全国安装的水泵保持功能正常。
利用 Taarifa 提供的数据,这些数据来自坦桑尼亚水务部,有机会利用机器学习来检测不再功能或需要维修的水泵。
目标
本项目的目标是训练和部署一个机器学习模型,以预测水泵是否功能正常、无法运作或正常但需要维修。
工具/框架
本项目需要使用各种工具和框架。
便于数据分析和建模的脚本都使用 Python 编写。
数据预处理和特征工程使用 Pandas 和 Scikit Learn 模块完成。数据建模则结合了 Scikit Learn 和其他机器学习库进行。
最终模型被集成在一个使用 Streamlit 库构建的 Web 应用程序中。该应用程序随后通过 Heroku 进行部署。
为了更全面地了解项目的依赖关系,请访问GitHub 仓库。
探索性数据分析
进行探索性数据分析(EDA)将揭示数据集的组成、数据应经历的过程以及应该考虑的机器学习算法。
提供的数据包括 59400 个数据点和 41 个特征,其中包括目标标签。
41 个特征如下:

代码输出(作者创建)
注意:有关这些特征的详细信息,请访问竞赛问题描述。
status_group特征将作为项目的目标标签。它揭示了水泵是否正常运作、无法运作,或正常但需要维修。
如代码输出所示,数据主要由类别特征组成。
此外,许多特征报告了类似的信息。例如,latitude、longitude、region和region_code特征都显示了水泵的位置。包含所有这些特征是多余的,甚至可能会影响模型的性能。
此外,数据集中有几个特征存在缺失值。

缺失值(作者创建)
最终,目标标签中的值分布表明数据是不*衡的,其中“功能需要维修”类别的样本较少。

目标标签(作者创建)
特征工程
从 EDA 的结果可以看出,许多特征在建模之前需要移除或修改。
- 移除不相关的列
id、recorded_by和wpt_name特征已被移除,因为它们对目标标签没有影响。
2. 移除冗余列
包含冗余信息的特征也应当被移除。这些特征包括:subvillage、latitude、longitude、region_code、district_code、lga、ward、scheme_name、extraction_type、extraction_type_group、payment、water_quality、quantity、source、source_type、waterpoint_type 和 management。
3. 创建“年龄”特征
construction_year 和 date_recorded 特征与水泵的状态无关。然而,通过使用这两个特征,我们可以推导出水泵的“年龄”(即从建设开始的年数),以了解它们有多旧。
2. 移除弱预测变量
最后,应移除与目标标签关系不够强的预测变量。
数值特征与目标标签之间的关系通过 ANOVA 进行评估。以下代码片段创建了一个图表,显示了每个特征的 p 值。

代码输出(作者创建)
类别特征与目标标签之间的关系通过卡方独立性检验进行评估。以下代码片段创建了一个图表,显示了每个特征的 p 值。

代码输出(作者创建)
在测试的数值特征和类别特征中,只有 num_private 特征因其高 p 值而被移除。
在特征选择过程后,数据集从 41 个特征缩减为 18 个特征。
创建训练集和测试集
原始数据集被分割为训练集和测试集,并采用分层抽样,以确保目标标签中的各组在每个划分中有相同的代表性。
确定评估指标
数据已为建模做好准备,但首先需要确定最适合该项目的评估指标。
为此,我们需要考虑最终用户的优先级。

作者创建
机器学习解决方案应通过检测非功能性或需要修理的水泵来提高获取清洁水的可及性。该解决方案还应通过正确识别不需要修理或更换的水泵来限制资金和资源的浪费。
值得注意的是,错误预测是非常不受欢迎的。
未能正确识别需要修理或更换的水泵(即假阴性)将减少清洁水的获取。依赖这些水泵的居民将无法用于农业和卫生目的,生活水*将下降。此外,建造水泵的政府和/或组织将失去声誉。
另一方面,未能正确识别功能正常的水泵(即假阳性)也是一种不理想的结果。这将导致将有限的资金和资源浪费在不需要修理或更换的水泵上。
鉴于假阳性和假阴性的巨大成本,机器学习模型应考虑精确度和召回率指标。然而,由于假阴性似乎更具后果,因此应更加重视提高召回率。
因此,用于项目的评估指标是 f2 分数指标,该指标考虑了精确度和召回率,但对召回率给予更大权重。

F2-score 公式
创建基线模型
基线将有助于为机器学习模型的结果提供背景。本项目将利用两个基线模型作为参考:一个哑分类器和一个逻辑回归。
哑分类器将始终对水泵的功能进行随机预测。
在对类别特征和缺失数据进行编码和填补后,将训练一个具有默认超参数的逻辑回归模型。
逻辑回归的作用是展示一个简单模型在现有数据上的表现。如果逻辑回归的表现不如哑分类器,则会表明数据存在问题。

代码输出(作者创建)
如输出所示,逻辑回归的 f2 分数明显高于哑分类器,这表明数据有足够的信号。
数据建模方法
构建模型的过程可以在以下流程图中呈现:

数据建模流程图(作者创建)
将用于项目的三种模型包括Catboost 分类器、LGBM 分类器和XGBoost 分类器。所有这些分类器都包含集成学习,非常适合处理不*衡的数据。此外,它们支持类别特征和/或缺失数据。
对于这些模型中的每一个,都确定了最佳超参数集。然后使用这些超参数训练模型,并用测试集进行评估。
一旦每个模型都经过测试,最终将选择最佳模型(即 f-2 分数最高的模型)。该模型将用于网络应用程序。
超参数调整方法
超参数调整方法本身包含许多关键技术,因此值得通过另一个流程图进行详细说明。

超参数调优流程图(作者创建)
超参数调优将使用 Optuna 库执行。
该过程包括创建一个 Optuna 研究。在每个研究中,分类器使用 100 个超参数集进行训练和评估。每个超参数集都通过分层交叉验证进行评估,该方法将训练数据分成多个折叠,每个折叠用来训练一个模型。
每个超参数集将通过训练模型的*均 f-2 分数来衡量。产生最高 f-2 分数的超参数集将被认为是最佳超参数集。
XGBoost 模型
为了展示对分类器进行的数据建模和超参数调优,以下代码片段展示了 XGBoost 的训练和评估过程。
首先,运行 Optuna 研究以找到 XGBoost 分类器的最佳超参数。
该研究将确定最佳超参数组合。这些超参数随后用于训练 XGBoost 分类器,然后在测试集上进行评估。

代码输出(作者创建)
CatBoost 模型
使用 XGBoost 分类器的程序,对 CatBoost 分类器进行训练并在测试集上进行评估(有关整个代码库,请访问 GitHub 仓库)。

代码输出(作者创建)
LightGBM 模型
使用 XGBoost 分类器的程序,对 LightGBM 分类器进行训练并在测试集上进行评估(有关整个代码库,请访问 GitHub 仓库)。

代码输出(作者创建)
选择最佳模型
所有模型的性能记录在下表中。

每个模型的性能(作者创建)
由于 XGBoost 分类器产生了最高的 f-2 分数(≈0.80),因此被认为是最佳模型。
模型解释
XGBoost 模型的性能可以通过分类报告和混淆矩阵进行背景说明,这些报告将预测值与实际值进行比较。

分类报告(作者创建)

混淆矩阵(作者创建)
如分类报告和混淆矩阵所示,功能性和非功能性水泵的精度和召回率相对较高。然而,该模型在功能正常但需要修理的水泵上的表现不佳。
模型部署
现在建模过程已完成,该模型应部署到一个可供最终用户访问的 Web 应用程序中。
该网页应用程序是使用 Streamlit 库构建的,文件名为 app.py。该文件的底层代码如下所示:
当使用 streamlit run app.py 命令运行时,应用程序应如下所示:

Streamlit 应用程序(由作者创建)
该应用程序包含一个侧边栏,用户可以在其中输入感兴趣的水泵参数。在点击“预测水泵状态”后,XGBoost 模型将预测具有选定特征的水泵是否功能正常、功能失效或功能正常但需要维修。结果会在应用程序中输出。

进行预测(由作者创建)
该网络应用程序也已通过 Heroku 托管,因此你可以通过点击下面的链接访问它:
[## Streamlit
预测水泵功能的 Streamlit 应用程序
water-pump-functionality-app.herokuapp.com](https://water-pump-functionality-app.herokuapp.com/?source=post_page-----8768b07ac7bb--------------------------------)
限制
尽管该项目已经产生了一个功能性网络应用程序,但它仍然存在值得注意的某些限制。
1. 没有现成的解决方案作为参考
尽管所提出的解决方案确实使用户能够确定坦桑尼亚水泵的功能,但由于没有现成的解决方案可以作为参考,因此很难向客户推介。因此,很难确定此模型能节省多少资金以及它能在多大程度上改善水的可及性。
2. 约束条件知识有限
项目在假设下进行,即假阴性(即将非功能水泵识别为功能正常)比假阳性(即将功能正常的水泵识别为非功能)更不可取。然而,只有在修理和更换水泵的资金和资源没有重大限制的情况下,这一假设才成立。
不幸的是,没有对这些约束条件的清晰理解,就无法确定最适合的机器学习模型评估指标。
3. 缺乏领域知识
数据集中在分类特征中存在许多独特的值。然而,DrivenData 没有提供这些值代表的含义。因此,该项目缺乏基于证据的分类特征处理策略。
结论

图片由 Alexas_Fotos 提供,来源于 Unsplash
总体而言,该项目旨在利用 Taarifa 收集的数据训练一个预测水泵功能的机器学习模型,并将其纳入具有商业价值的应用程序中。
要访问整个代码库,请访问 GitHub 仓库:
[## GitHub - anair123/Detecting-Faulty-Water-Pumps-With-Machine-Learning
目前无法执行该操作。您在另一个标签或窗口中已登录。您在另一个标签或…
感谢您的阅读!
参考文献
Bull, P., Slavitt, I., & Lipstein, G. (2016 年 6 月 24 日). 利用群众的力量来提高社会部门的数据科学能力。arXiv.org. arxiv.org/abs/1606.07781
什么是泊松分布
对最著名概率分布之一的概述
·发表于 Towards Data Science ·阅读时间 4 分钟·2023 年 6 月 6 日
--

图片由Anne Nygård拍摄,来源于Unsplash
背景
泊松分布 是一种普遍的离散概率分布。它由西门·丹尼斯·泊松于 19 世纪初首次发布,并且已经在许多行业中找到应用,包括保险、流行病学和电子商务。因此,数据科学家需要了解这一重要概念。在这篇文章中,我们将深入探讨这一分布的复杂性,并提供实际世界的例子。
补充视频。
直观
泊松分布的核心概念是量化一个事件在给定时间间隔内发生特定次数的概率。
作为一个例子,假设我们有一个零售店,每小时*均接待 20 位顾客。利用泊松分布,我们可以计算该店在一个小时内接待特定数量顾客的概率,例如 10、15 或 30 位顾客。
理论
泊松分布的概率质量函数 (PMF) 为:

公式由作者以 LaTeX 编写。
其中:
-
e: 欧拉数(约 2.73)
-
k: 发生次数(≥ 0)
-
X: 离散随机变量(≥ 0)
-
λ: 预期发生次数(≥ 0)
泊松分布的参数是λ,它表示发生次数的均值,E(X) = λ,以及分布的方差,VAR(X) = λ。有关均值和方差的推导,请参见这里。
值得注意的是,泊松分布实际上是从二项分布派生出来的。虽然我们在本文中不会详细讨论其推导过程,但有兴趣的读者可以在这里找到相关信息。
泊松分布的条件:
-
事件的数量, k,是独立发生的 (泊松过程)
-
事件在时间间隔内随机发生
-
事件的预期数量是固定的
-
在时间间隔的任何点获得事件的概率是相等的
示例与图表
回到我们之前的商店示例,其中每小时的*均顾客数为 20。商店在一个小时内接待 10 名顾客的概率是多少?
所以,我们得到的是:
-
λ = 20
-
k = 10
将这些值代入 PMF 公式:

Equation by author in LaTeX.
如我们所见,这个概率非常低。为了更好地理解顾客访问的分布情况,我们可以绘制整个 PMF:
GitHub Gist by author.

Plot generated by author in Python.
正如观察到的,顾客访问的分布几乎呈钟形曲线,最可能的顾客数量为 20。这是因为 20 是预期的数量。为了进一步了解,我们可以探索一些均值为 10 或 30 的情景,并绘制相应的分布:
GitHub Gist by author.

Plot generated by author in Python.
因此,当均值变小的时候,分布中的大部分概率质量会向左移动。这种移动是预期的,因为均值率代表了顾客访问的预期速率。因此,顾客人数更可能接*均值。
总结与进一步思考
泊松分布是数据科学和统计学中广泛使用且著名的概率分布。它基于给定的均值率来建模事件以特定速率发生的概率。泊松分布在遗传学、保险、欺诈检测等多个行业中都有应用。
如果你想查看本文中使用的完整代码,它可以在我的 GitHub 上找到:
[## Medium-Articles/poisson.py at main · egorhowell/Medium-Articles
我在我的中等博客/文章中使用的代码。通过创建帐户贡献于 egorhowell/Medium-Articles 的开发…
另一件事!
我有一个免费的通讯,Dishing the Data,在这里我分享每周的提示,帮助你成为更好的数据科学家。没有“虚 fluff”或“点击诱饵”,只有来自实际数据科学家的纯粹可操作的见解。
[## Dishing The Data | Egor Howell | Substack
如何成为更好的数据科学家。点击阅读由 Egor Howell 发表的 Substack 文章《Dishing The Data》,…
newsletter.egorhowell.com](https://newsletter.egorhowell.com/?source=post_page-----5afd4d70b1d7--------------------------------)
与我联系!
参考文献和进一步阅读
- 更多的泊松分布示例:
brilliant.org/wiki/poisson-distribution/
各种逻辑回归模型的预测(第一部分)
原文:
towardsdatascience.com/prediction-in-various-logistic-regression-models-2543281cd55a
R 系列统计
·发表于Towards Data Science ·阅读时间 8 分钟·2023 年 4 月 16 日
--

图片由Jen Theodore拍摄,来源于Unsplash
介绍
在之前的几篇文章中,我们已经涵盖了各种类型的逻辑回归模型。这些模型的目标是尽可能准确地预测未来的数据点以及中间数据点。在本文中,我们将讨论如何在 R 中进行预测分析,包括简单和多重逻辑回归,使用二分类和有序数据。
数据集
成人数据集将作为我们研究的一部分案例研究。该数据集中收集的数据包括超过 30,000 名个体的详细信息。数据包括每个人的种族、教育背景、职业、性别、工资、每周工作小时数、持有的工作数量以及他们的收入水*。

成人数据集来自 UCI 机器学习库
数据集简述:
-
Bachelors:1 表示该人拥有学士学位,0 表示该人没有学士学位。
-
Income_greater_than_50k_code:1 表示家庭总收入超过 50,000 美元,0 表示家庭总收入低于 50,000 美元。
-
Marital_status_code:1 表示该人已婚,0 表示该人未婚或离婚。
-
Race_code:1 表示非白人,2 表示白人。
用于二分类数据的简单逻辑回归预测
我们将通过上述数据集来识别两个可以用于预测二元收入结果的变量,这些收入结果可以大于 $59K 或小于 $50K,利用教育水*和婚姻状况变量。 该研究提出了以下问题:
教育水*对收入的影响是什么?
要进行预测分析,首先需要安装ggpredict库。第一个命令将提供二元“学士学位”变量的预测概率。我们知道Bachelors变量可以有两个值:0 和 1。R 将为家庭收入(也是一个二元变量)提供大于 $50k 的概率。

第一个输出提供了预测收入概率的表格数据。在这里,如果学士学位 = 1,则家庭收入大于 $50k 的概率变为 0.47,而如果没有学士学位,该概率降至 0.16。 这告诉我们教育对家庭收入有重要影响。
多重逻辑回归预测二元数据
使用上述数据集,我们将采用两个预测变量:教育水*和婚姻状况,以预测二元收入结果,这些结果可以大于 $50K 或小于 $50k。这里的研究问题是:
教育水*和婚姻状况对收入的综合影响是什么?
使用二元数据进行多重逻辑回归的实现与下面的简单逻辑回归非常相似。
在这里,我们希望将婚姻状况作为另一个预测变量来预测家庭收入。使用类似的ggpredict命令,我们得到以下结果。

对于第二个有 2 个预测变量的模型,概率数据已经为另一个变量的均值进行了调整。这里我们有第二个预测变量为Marital_status_code,其均值为 0.47。 这告诉我们,数据集中有 47% 的人已婚,而 53% 的人要么未婚要么离婚。保持该值不变,如果一个人有学士学位,则家庭收入大于 $50k 的概率为 0.44。如果没有,则概率降至 0.17。
简单逻辑回归预测有序数据
有时我们可能会有多于 2 个响应水*的结果变量,且该变量是有序的。我们数据集中家庭收入变量只有两个结果水*,但如果响应变量有超过 2 个结果,则可以遵循相同的方法。
回归模型的目的是对以下数据集中的问题提供定量解释:
教育水*、性别和种族对收入的个体影响是什么?
为了定义一个响应变量为有序的逻辑回归模型,我们可以使用clm()命令来自ordinal包。首先,我们需要将预测变量和响应变量转换为因子。这里响应变量有两个以上的类别,通常这个模型被称为比例奇数(PO)模型。
我们将使用相同的数据集来进行 PO 模型的预测。预测时,我们也将使用来自ggeffects库的ggpredict()命令。
首先预测教育代码为 5、10 和 13 的家庭收入是否大于$50k,这些代码分别代表 9 年级、高中毕业和博士学位。这里响应水* 1 表示家庭收入低于$50k 的群体,响应水* 2 表示家庭收入高于$50k 的群体。如果个人拥有 9 年级教育,那么家庭收入低于$50k 的概率是 0.98,家庭收入高于$50k 的概率是 0.02。如果个人拥有博士学位,家庭收入低于$50k 的概率是 0.35,家庭收入高于$50k 的概率是 0.65。因此,教育水*越高,家庭收入一般也越高。

如果我们使用“学士学位”作为预测变量进行相同的预测,我们观察到没有学士学位的个体家庭收入低于$50k 的概率是 0.84,家庭收入高于$50k 的概率是 0.16。

以“教育年限”作为预测变量,我们可以得出教育年限越长,获得高收入的概率越高。如果个体有 16 年的教育,家庭收入高于$50k 的概率是 0.69。值得注意的是,所有响应水*的概率总和等于 1。

多重逻辑回归在有序数据中的预测
在有序逻辑回归中,预测变量可以是有序的、二元的或连续的,而响应变量是有序的。
考虑预测收入的例子,其中教育的等级只有两个响应级别。例如,要进行回归分析,我们可以从 1 年级到博士学位分配有序的数字。也可以使用二进制变量来预测收入。例如,我们可以将 1 分配给拥有学士学位的人,将 0 分配给没有学士学位的人。这有点像一个有两个级别的有序变量。最后,我们还可以使用连续变量如教育年限来预测收入。在这里,我们尝试定量回答以下问题。
教育水*、性别和种族对收入的综合影响是什么?
在第一个模型中,我们使用了所有三个预测变量来预测收入,结果如下所示。

收入水*分为两种(收入超过 50000 美元和收入低于 50000 美元),因此也有两个响应级别(如上图所示的红色)。在第二列中,你会发现每个教育水*的预测概率。教育水*为 3(5-6 年级)时,收入低于 50000 美元的概率为 0.99,而教育水*为 13(博士学位)时,收入低于 50000 美元的概率为 0.36。根据第二个响应级别的预测结果,可以得出相同的结论。因此,很明显家庭收入与教育水*有正相关关系。较高的教育水*与家庭收入的提高相关联。与往常一样,这些结果使用了其他两个变量Gender_code和Race_code的均值进行调整。
当在第二个ggpredict命令中包含性别时,我们得到以下结果。

现在我们有了性别和收入水*的 2 个层次,因此我们有了 4 个表格。如果一个人的性别代码为 1(女性),并且他们拥有博士学位(教育代码 13),那么他们预期收入超过 50000 美元的概率为 0.42;而如果这个人的性别代码为 2(男性),且他拥有博士学位,那么预期收入超过 50000 美元的概率为 0.74。换句话说,这表明女性在相同教育水*下没有得到与男性相等的薪酬。与往常一样,该模型只使用了Race_code的均值进行调整。
结论
在这篇文章中,我们对二项和序数逻辑回归模型进行了预测分析,使用了单一和多个预测变量。我们涵盖了这四个模型中ggpredict()命令的使用,并且定量讨论了结果。作为提醒,模型的整体表现将取决于数据清理的程度。存在不必要的数据、重复的数据或错误的数据会导致模型结果误导。
数据集致谢
Dua, D. 和 Graff, C. (2019). UCI 机器学习库 [http://archive.ics.uci.edu/ml]. 加州欧文:加州大学信息与计算机科学学院。
感谢阅读。
各种逻辑回归模型的预测(第二部分)
原文:
towardsdatascience.com/prediction-in-various-logistic-regression-models-part-2-f8994e306a4c
R 语言统计系列
·发布在Towards Data Science ·阅读时长 8 分钟·2023 年 4 月 27 日
--

Vladimir Fedotov拍摄,发布在Unsplash
介绍
我们已经涵盖了二元和有序数据的逻辑回归模型,并演示了如何在 R 语言中实现该模型。此外,使用 R 库的预测分析也在早期文章中讨论过。我们已经看到单一和多个预测变量对响应变量的影响,并对其进行了量化。我们采用了二元和有序响应变量,展示了如何处理不同类型的数据。在本文中,我们将探讨四种额外的逻辑回归模型预测分析,即广义有序回归模型、部分比例奇数模型、多项逻辑回归模型和泊松回归模型。
数据集
我们的研究将使用相同的UCI 机器学习库的成人数据集作为案例研究。该数据集中收集了超过 30000 名个体的人口统计数据。数据包括每个人的种族、教育、工作、性别、工资、持有的工作数量、每周工作小时数和收入。为了更好地理解,以下是所考虑的变量。
-
教育:数值型和连续型。个人的健康状况可以受到教育的重大影响。
-
婚姻状态:二元(0 代表未婚,1 代表已婚)。这个变量的影响可能会很小,但它仍然被纳入了分析中。
-
性别:二元(0 代表女性,1 代表男性)。它也有可能影响较小,但这值得关注。
-
家庭收入:二元(0 代表*均或低于*均,1 代表高于*均)。健康状况可能受到影响。
-
健康状态:有序(1 代表差,2 代表一般,3 代表良好,4 代表优秀)
广义有序回归模型中的预测
考虑我们收集了数百个个体的数据。数据中包括有关个人的教育、年龄、婚姻状态、健康状态、性别、家庭收入和全职就业状态的信息。教育、性别、婚姻状态和家庭收入将作为回归模型中的预测变量。除了教育外,预测变量都是二元的,这意味着它们的值要么是 0,要么是 1。教育是一个连续变量,表示个人受教育的年限。以下变量被考虑用于此次回归分析。
-
教育年限
-
婚姻状态
-
性别
-
家庭收入
-
健康状态
如果我们执行有序逻辑回归并保持比例奇数假设,那么每个预测变量的系数值将为 1。假设家庭收入的系数为‘x’,这意味着每单位家庭收入增加(在这种情况下从 0 到 1),健康状态的更高类别的对数概率或对数赔率将增加‘x’。因此,我们可以得出关于此模型的以下结论。
-
如果家庭收入增加到高于*均水*,从差健康状态变为*均健康状态的对数赔率是‘x’。
-
如果家庭收入增加到高于*均水*,从一般健康状态变为良好健康状态的对数赔率是‘x’。
-
如果家庭收入增加到高于*均水*,从良好健康状态变为优秀健康状态的对数赔率是‘x’。
比例奇数模型的特点是所有结果水*上的对数赔率相同。现实数据常常违反这一假设,因此我们不能使用比例奇数模型。如前所述,解决这一非比例奇数问题的两个可能解决方案是采用广义有序模型或部分比例奇数模型。
-
广义有序回归模型 -> 所有预测变量的所有级别的效果可能会有所不同
-
部分比例奇数模型 -> 所有/部分预测变量的某些级别的效果允许变化
我们已经在早期文章中实现了广义方法和部分比例奇数方法的模型。
R 语言中的统计系列
[towardsdatascience.com ## 部分比例奇数模型在 R 中
R 统计系列
[towardsdatascience.com
现在我们将使用这些模型实现预测过程。

在这里,我们可以看到提供的教育值下,不同健康状态的累计预测概率。我们知道我们的健康状态有四个独特的值。
如果个人有 15 年的教育,
-
健康一般及以上的累计概率是 96%
-
健康良好及以上的累计概率是 77%
-
健康优秀的累计概率是 24%
如果个人只有 5 年的教育,
-
健康一般及以上的累计概率是 81%
-
健康良好及以上的累计概率是 41%
-
健康优秀的累计概率是 8%
因此,很明显,教育年限在决定个人健康状态方面起着重要作用。如果我们只想获得预测概率,可以执行以下命令。
ggpredict(model1, terms = “educ[5,10,15]”,ci=NA)

如果个人有 15 年的教育,
-
健康不良的概率是 4%
-
健康一般的概率是 20%
-
健康良好的概率是 52%
-
健康优秀的概率是 24%
如果个人只有 5 年的教育,
-
健康不良的概率是 19%
-
健康一般的概率是 40%
-
健康良好的概率是 33%
-
健康优秀的概率是 8%
显然,教育年限增加了拥有更好健康的概率。所有这些值都已针对婚姻状况、性别和全职工作状态的均值进行了调整。
部分比例奇数模型中的预测
在部分比例奇数模型中,我们可以选择希望不同结果水*的效应变化的预测变量。我们可以首先确定哪些预测变量违反了比例奇数假设,然后将这些变量放在parallel = FALSE ~ 命令之后。这里,我们将婚姻状况和家庭收入作为违反假设的预测变量。

如果个人有 15 年的教育,
-
健康不良的概率是 4%
-
健康一般的概率是 20%
-
健康良好的概率是 52%
-
健康优秀的概率是 24%
如果个人只有 5 年的教育,
-
健康状况差的概率为 17%
-
健康状况一般的概率为 41%
-
健康良好的概率为 35%
-
健康状况优秀的概率为 7%
累积概率也可以使用之前描述的方法计算。
多项回归模型中的预测
我们在以下文章中介绍了多项逻辑回归分析。
R 中的统计系列
[towardsdatascience.com
多项回归是一种统计方法,用于估计个体落入特定类别的可能性,相对于基准类别,利用对数几率或对数几率比的方法。实质上,当名义响应变量有多个结果时,它作为二项分布的扩展来工作。作为多项回归的一部分,我们需要定义一个参考类别,模型将基于参考类别确定各种二项分布参数。
在以下代码中,我们定义了健康状态的第一个级别作为参考水*,我们将基于这个参考水*比较多个二项回归模型。
我们的预测方法得出了以下结果。

如果个体接受了 15 年的教育,
-
健康状况差的概率为 4%
-
健康状况一般的概率为 19%
-
健康状况良好的概率为 52%
-
健康状况优秀的概率为 25%
再次,这些预测的概率是在保持其他预测变量均值的情况下计算的。在多项逻辑回归中,响应变量应为名义变量。然而,这里的响应被转换为序数变量以使用ggpredict()命令。
泊松回归模型中的预测
有时我们需要处理涉及计数的数据。为了对计数响应变量建模,例如博物馆访问次数,我们需要使用泊松回归。到医院的访问次数或特定学生群体修读的数学课程数量也可以作为示例。我们在以下文章中介绍了泊松回归。
R 中的统计系列
[towardsdatascience.com
我们将使用相同的数据集,预测从教育年限、性别、婚姻状况、全职工作状态和家庭收入中得出的科学博物馆访问次数。代码块如下所示。
使用相同的ggpredict()命令,我们获得了不同教育年限以及不同性别的以下结果。

-
如果个体是女性(性别=0)且教育年限为 15 年,则预测的科学博物馆访问次数为 0.44。
-
如果个体是男性(性别=1)且教育年限为 15 年,则预测的科学博物馆访问次数为 0.62。
-
这表明女性访问科学博物馆的频率低于男性。结论已针对婚姻状况、全职工作状况和家庭收入的均值进行调整。
结论
在这篇文章中,我们覆盖了四种不同类型回归模型的预测分析。部分比例奇数模型可以视为广义序数回归模型的一个子集,因为 PPO 模型允许只有少数预测变量在不同层次上变化。多项式回归模型适用于类别没有顺序的名义响应变量。最后,泊松回归模型适合预测计数变量。我们展示了在所有四种回归模型中使用ggpredict()函数以及结果的解释。
数据集致谢
Dua, D. 和 Graff, C. (2019). UCI 机器学习库 [http://archive.ics.uci.edu/ml]。加州欧文:加州大学信息与计算机科学学院(CC BY 4.0)
感谢阅读。
[## 使用我的推荐链接加入 Medium - Md Sohel Mahmood
阅读 Md Sohel Mahmood(以及 Medium 上的成千上万其他作者)的每一个故事。您的会员费直接...
mdsohel-mahmood.medium.com](https://mdsohel-mahmood.medium.com/membership?source=post_page-----f8994e306a4c--------------------------------) [## 每当 Md Sohel Mahmood 发布文章时,您将收到电子邮件。
每当 Md Sohel Mahmood 发布文章时,您将收到电子邮件。通过注册,您将创建一个 Medium 账户(如果您还没有的话)...
mdsohel-mahmood.medium.com](https://mdsohel-mahmood.medium.com/subscribe?source=post_page-----f8994e306a4c--------------------------------)
预测性能漂移:另一面的硬币
我们知道原因,接下来讨论类型
·
关注 发表在 Towards Data Science ·7 min 阅读·2023 年 2 月 2 日
--
预测性能漂移的两个方面(作者提供的图片)
机器学习的世界发展如此迅速,在不到二十年的时间里,我们已经进入了下一个阶段。模型已经建立,现在我们需要知道它们是否在短期、中期和长期内提供准确的预测。构建模型时,我们口袋里有很多方法、理论方法、思潮、范式和数字工具。现在,我们希望更好地理解预测性能随时间变化的风险。
有人可能认为,模型的预测性能是由测试集的规定质量来保证的,测试集是一组在模型训练过程中未参与的样本,但在那时进行测量或观察。现实是,尽管模型可能在基于该测试集的情况下以某种(希望非常令人满意的)预测性能交付,但随时间推移的观测将不会完全具备与训练集和测试集相同的属性或趋势。请记住,模型是机器,而不是思考实体。如果它们看到一些新的、未曾训练过的东西,它们无法对这些新事物进行批判。我们,人类,可以。
预测性能漂移:A 面
我们对预测性能的思考方式涉及将影响模型的变异分布分开。有输入特征的分布P(X)、标签的分布P(Y),以及给定输入的标签的条件分布P(Y|X)。这些组件中的任何变化都是模型在预测中产生更多不准确结果的潜在原因。考虑到这一点,如果我们监控这些组件并且它们中的某些部分发生变化,那么就需要进行深入检查,看看发生了什么,并在必要时修复。
预测性能漂移:B 面
虽然之前的内容无可争议,但我们也可以从预测性能漂移的另一个维度来观察:漂移的类型。当我们观察预测性能时,可以区分不同类型的漂移。那这样做有什么用呢?实际上,这不仅能检测到某些变化,还能更容易地知道如何修复这些变化。
漂移的类型大致可以分为四种:偏差、斜率、方差和非线性。虽然这些名称可能更具体地指向线性回归模型,但它们也适用于一般的机器学习模型。这些类型的更通用名称可以是常量偏移、相对于预测边界的旋转、与边界收敛的分散以及边界形状的变化。我们可能会同意,后者的名称较长且书写和讨论起来比较费时,因此我们坚持使用前者的名称。在这样做的同时,也不要将任何类型与模型的性质混淆。任何四种类型的漂移都可能发生在任何类型的模型中。

回归模型的漂移类型(作者提供的图片)
通过检查不同类型的漂移,不仅可以检测到某些变化,还能更容易地知道如何修复这些变化。
B 面:四种类型
偏差漂移指的是恒定的偏移。在回归模型中,如果我们观察到我们的预测值与观察值之间存在恒定差异,则发生了偏差漂移。在分类模型中,如果每个类别的特征有恒定的偏移,这也可能是特定于类别的。这种漂移可能仅仅是由于总体均值的变化,而特征之间及其对目标变量的影响没有其他变化。由于是简单的变化,可以通过重新调整模型的偏差或样本中的均值来轻松修复。
斜率漂移发生在回归函数或决策边界相对于中心的旋转时。这种漂移在涉及从不同角度拍摄但本质上相同的图像的学习任务中非常常见。根据旋转的程度以及旋转是否是分类任务特定的,斜率漂移可以通过简单调整旋转或完全重新训练模型来修复。

线性分类器的漂移类型示例(图片作者提供)
方差漂移,顾名思义,是指离散度的增加。如果离散度推动样本越过任务边界,它将对预测性能造成损害。在回归的情况下,方差漂移表现为更大的残差。在分类的情况下,这种漂移可能是每个类别中原始特征离散度的扩展。同样,这种漂移也可能是特定于类别的。这种漂移可能由不同原因造成,它可能对应于靠*决策边界的更大采样或输入特征的不均匀偏移。如果是由于采样问题,这个问题可以通过使用更能代表当前采样的更新数据重新计算模型来解决。

非线性分类器的漂移类型示例(图片作者提供)
最后一种漂移称为非线性漂移。虽然从展示的示例中看起来非常具体和系统化,这种漂移可能包含前面提到的三种类型的所有组合。著名的教学案例涉及使用 2 个输入特征来分隔 2 个类别。在二维空间中,这两个类别可能通过一个线性函数来分隔,但 2 个点的简单旋转会引发非线性变化。如果这些类别不再能通过一条直线来分隔,那么问题就变成了非线性问题。正如其他情况一样,对于分类任务,这种漂移也可能是特定于类别的,如上面线性分类的示例所示。由于这是最复杂的漂移类型,可能在模型的预测质量中观察到的非线性模式可能总是需要对模型进行重新训练。

从线性到非线性分类的示例(图片由作者提供)
何时预测性能会退化?
漂移并不总是有问题。让我们稍微深入一下。X的中心发生变化而Y的中心没有变化,或者反之,将导致偏差漂移。然而,如果X和Y都按照模型函数的要求移动,那么预测性能将不会漂移,因为模型将继续做出准确的预测。谈到斜率时,虽然回归任务中旋转的空间较小,但在分类任务中,只要这种旋转不与决策边界重叠,类别可以旋转而不影响性能。这可能是一个非常理论性的事实,在实践中的发生率较小,但事情就是这样。
方差漂移对于分类问题可能比斜率漂移更现实。只要方差的扩散没有与决策边界重叠,模型就能处理方差漂移。当数据点开始穿越决策边界时,预测性能会受到影响。回归模型也是如此,尽管与斜率漂移的情况类似,对于方差扩展的容忍度非常小,必须考虑预测性能的退化。
非线性漂移是最不允许的。这些变化可能总是会有问题。模型可以对许多与之前漂移相对应的变化保持鲁棒性,但它们是为了处理特定任务和特定形状而设计的。这个形状是我们创建模型以预测的概念的表现,如果这个形状开始改变,那么整个概念可能也在改变。因此,在非线性变化不是问题的情况下很难思考。
让我们继续抛硬币
因此,我们现在有了一种新的视角来思考模型预测性能的退化。我们不仅配备了原因,还配备了类型,当我们监控模型的预测质量时,可以继续抛硬币。这样,我们不仅能获得什么和何时发生了变化的信息,还能了解为什么发生变化以及如何发生变化。
我们是否最终会形成“谁”和“哪里”的定义?
准备应对气候变化与 AI 助手
通过对话简化复杂数据
·
关注 发表在 Towards Data Science ·13 分钟阅读·2023 年 11 月 26 日
--
使用 OpenAI 的 ChatGPT 和 Dall-E-3 生成的图像
摘要
在这篇文章中,我们探讨了如何使用来自 Probable Futures API 和新的 OpenAI Assistants API 创建一个对话式 AI 代理。这个 AI 代理能够回答有关气候如何影响特定地点的问题,并且还能进行基本的数据分析。AI 助手非常适合这样的任务,为非技术用户提供了展示复杂数据的有希望的途径。
我最*和一个邻居聊起了气候变化可能对我们产生的影响,以及如何最好地准备房屋以应对极端天气事件。有一些很棒的网站提供了与此相关的地图信息,但我想知道是否有时候人们只是想问像“我的家会如何受到气候变化的影响?”和“我可以怎么做?”这样的问题,并获得一个简明的总结和如何准备的建议。所以我决定探索一下最*几周发布的一些 AI 工具。
OpenAI 的助手 API
由大型语言模型如 GPT-4 驱动的 AI 代理正在成为人们通过对话与文档和数据互动的一种方式。这些代理解读人们的提问,调用 API 和数据库以获取数据,生成并运行代码进行分析,然后将结果呈现给用户。像 langchain 和 autogen 这样出色的框架正在引领潮流,提供了轻松实现代理的模式。最*,OpenAI 也加入了这个行列,推出了 GPTs 作为一种无代码的创建代理的方法,我在 这篇文章 中探索了这一点。这些设计得非常好,为更广泛的受众打开了大门,但它们确实有一些限制。它们需要一个带有 openapi.json 规范的 API,这意味着它们当前不支持 graphql 等标准。它们也不支持注册功能的能力,这对于无代码解决方案来说是可以预期的,但可能限制了它们的能力。
介绍 OpenAI 最*的另一个发布——助手 API。
助手 API(测试版)是一种编程方式来配置 OpenAI 助手,支持功能、网页浏览和从上传文档中检索知识。与 GPTs 相比,功能是一个很大的区别,因为这些功能使得与外部数据源的互动变得更加复杂。功能是指大型语言模型(LLMs)如 GPT-4 被告知某些用户输入应导致调用代码函数。LLM 会生成一个 JSON 格式的响应,包含调用函数所需的确切参数,然后可以用于本地执行。要详细了解它们如何与 OpenAI 一起工作,请参见 这里。
气候变化综合 API——Probable Futures
为了能够创建一个 AI 代理来帮助准备应对气候变化,我们需要一个好的气候变化数据来源和一个 API 来提取这些信息。任何这样的资源都必须采用严格的方法来结合气候模式(GCM)预测。
幸运的是,Probable Futures的团队做得非常出色!

Probable Futures提供了与气候变化预测相关的各种资源
Probable Futures 是“一个非营利气候素养倡议,提供在线实用工具、故事和资源,面向所有人,无论身在何处”,他们提供了一系列基于 CORDEX-CORE 框架的地图和数据,这是对来自 REMO2015 和 REGCM4 区域气候模型的气候模型输出的标准化。[附注:我与 Probable Futures 没有关联]
重要的是,他们提供了一个GraphQL API来访问这些数据,我在请求 API 密钥后可以访问。
根据文档我创建了函数,并将其保存在文件assistant_tools.py中…
pf_api_url = "https://graphql.probablefutures.org"
pf_token_audience = "https://graphql.probablefutures.com"
pf_token_url = "https://probablefutures.us.auth0.com/oauth/token"
def get_pf_token():
client_id = os.getenv("CLIENT_ID")
client_secret = os.getenv("CLIENT_SECRET")
response = requests.post(
pf_token_url,
json={
"client_id": client_id,
"client_secret": client_secret,
"audience": pf_token_audience,
"grant_type": "client_credentials",
},
)
access_token = response.json()["access_token"]
return access_token
def get_pf_data(address, country, warming_scenario="1.5"):
variables = {}
location = f"""
country: "{country}"
address: "{address}"
"""
query = (
"""
mutation {
getDatasetStatistics(input: { """
+ location
+ """ \
warmingScenario: \"""" + warming_scenario + """\"
}) {
datasetStatisticsResponses{
datasetId
midValue
name
unit
warmingScenario
latitude
longitude
info
}
}
}
"""
)
print(query)
access_token = get_pf_token()
url = pf_api_url + "/graphql"
headers = {"Authorization": "Bearer " + access_token}
response = requests.post(
url, json={"query": query, "variables": variables}, headers=headers
)
return str(response.json())
我有意排除了datasetId以检索所有指标,这样 AI 代理就能拥有广泛的信息供其使用。
该 API 非常强大,既可以接受城镇和城市,也可以接受完整地址。例如…
get_pf_data(address="New Delhi", country="India", warming_scenario="1.5")
返回包含位置气候变化信息的 JSON 记录…
{'data': {'getDatasetStatistics': {'datasetStatisticsResponses': [{'datasetId': 40601, 'midValue': '17.0', 'name': 'Change in total annual precipitation', 'unit': 'mm', 'warmingScenario': '1.5', 'latitude': 28.6, 'longitude': 77.2, 'info': {}}, {'datasetId': 40616, 'midValue': '14.0', 'name': 'Change in wettest 90 days', 'unit': 'mm', 'warmingScenario': '1.5', 'latitude': 28.6, 'longitude': 77.2, 'info': {}}, {'datasetId': 40607, 'midValue': '19.0', 'name': 'Change in dry hot days', 'unit': 'days', 'warmingScenario': '1.5', 'latitude': 28.6, 'longitude': 77.2, 'info': {}}, {'datasetId': 40614, 'midValue': '0.0', 'name': 'Change in snowy days', 'unit': 'days', 'warmingScenario': '1.5', 'latitude': 28.6, 'longitude': 77.2, 'info': {}}, {'datasetId': 40612, 'midValue': '2.0', 'name': 'Change in frequency of “1-in-100-year” storm', 'unit': 'x as frequent', 'warmingScenario': '1.5', 'latitude': 28.6, 'longitude': 77.2, 'info': {}}, {'datasetId': 40101, 'midValue': '28.0', 'name': 'Average temperature', 'unit': '°C', 'warmingScenario': '1.5', 'latitude': 28.6, 'longitude': 77.2, 'info': {}}, {'datasetId': 40901, 'midValue': '4.0', 'name': 'Climate zones', 'unit': 'class', 'warmingScenario': '1.5', 'latitude': 28.6, 'longitude': 77.2, 'info': {'climateZoneName': 'Dry semi-arid (or steppe) hot'}}, {'datasetId': 40613, 'midValue': '49.0', 'name': 'Change in precipitation “1-in-100-year” storm', 'unit': 'mm', 'warmingScenario': '1.5', 'latitude': 28.6, 'longitude': 77.2, 'info': {}}, {'datasetId': 40701, 'midValue': '7.0', 'name': 'Likelihood of year-plus extreme drought', 'unit': '%', 'warmingScenario': '1.5', 'latitude': 28.6, 'longitude': 77.2, 'info': {}}, {'datasetId': 40702, 'midValue': '30.0', 'name': 'Likelihood of year-plus drought', 'unit': '%', 'warmingScenario': '1.5', 'latitude': 28.6, 'longitude': 77.2, 'info': {}}, {'datasetId': 40704, 'midValue': '5.0', 'name': 'Change in wildfire danger days', 'unit': 'days', 'warmingScenario': '1.5', 'latitude': 28.6, 'longitude': 77.2, 'info': {}}, {'datasetId': 40703, 'midValue': '-0.2', 'name': 'Change in water balance', 'unit': 'z-score', 'warmingScenario': '1.5', 'latitude': 28.6, 'longitude': 77.2, 'info': {}}, {'datasetId': 40201, 'midValue': '21.0', 'name': 'Average nighttime temperature', 'unit': '°C', 'warmingScenario': '1.5', 'latitude': 28.6, 'longitude': 77.2, 'info': {}}, {'datasetId': 40205, 'midValue': '0.0', 'name': 'Freezing days', 'unit': 'days', 'warmingScenario': '1.5', 'latitude': 28.6, 'longitude': 77.2, 'info': {}}, {'datasetId': 40301, 'midValue': '71.0', 'name': 'Days above 26°C (78°F) wet-bulb', 'unit': 'days', 'warmingScenario': '1.5', 'latitude': 28.6, 'longitude': 77.2, 'info': {}}, {'datasetId': 40302, 'midValue': '24.0', 'name': 'Days above 28°C (82°F) wet-bulb', 'unit': 'days', 'warmingScenario': '1.5', 'latitude': 28.6, 'longitude': 77.2, 'info': {}}, {'datasetId': 40303, 'midValue': '2.0', 'name': 'Days above 30°C (86°F) wet-bulb', 'unit': 'days', 'warmingScenario': '1.5', 'latitude': 28.6, 'longitude': 77.2, 'info': {}}, {'datasetId': 40102, 'midValue': '35.0', 'name': 'Average daytime temperature', 'unit': '°C', 'warmingScenario': '1.5', 'latitude': 28.6, 'longitude': 77.2, 'info': {}}, {'datasetId': 40103, 'midValue': '49.0', 'name': '10 hottest days', 'unit': '°C', 'warmingScenario': '1.5', 'latitude': 28.6, 'longitude': 77.2, 'info': {}}, {'datasetId': 40104, 'midValue': '228.0', 'name': 'Days above 32°C (90°F)', 'unit': 'days', 'warmingScenario': '1.5', 'latitude': 28.6, 'longitude': 77.2, 'info': {}}, {'datasetId': 40105, 'midValue': '187.0', 'name': 'Days above 35°C (95°F)', 'unit': 'days', 'warmingScenario': '1.5', 'latitude': 28.6, 'longitude': 77.2, 'info': {}}, {'datasetId': 40106, 'midValue': '145.0', 'name': 'Days above 38°C (100°F)', 'unit': 'days', 'warmingScenario': '1.5', 'latitude': 28.6, 'longitude': 77.2, 'info': {}}, {'datasetId': 40202, 'midValue': '0.0', 'name': 'Frost nights', 'unit': 'nights', 'warmingScenario': '1.5', 'latitude': 28.6, 'longitude': 77.2, 'info': {}}, {'datasetId': 40304, 'midValue': '0.0', 'name': 'Days above 32°C (90°F) wet-bulb', 'unit': 'days', 'warmingScenario': '1.5', 'latitude': 28.6, 'longitude': 77.2, 'info': {}}, {'datasetId': 40305, 'midValue': '29.0', 'name': '10 hottest wet-bulb days', 'unit': '°C', 'warmingScenario': '1.5', 'latitude': 28.6, 'longitude': 77.2, 'info': {}}, {'datasetId': 40203, 'midValue': '207.0', 'name': 'Nights above 20°C (68°F)', 'unit': 'nights', 'warmingScenario': '1.5', 'latitude': 28.6, 'longitude': 77.2, 'info': {}}, {'datasetId': 40204, 'midValue': '147.0', 'name': 'Nights above 25°C (77°F)', 'unit': 'nights', 'warmingScenario': '1.5', 'latitude': 28.6, 'longitude': 77.2, 'info': {}}]}}}
创建一个 OpenAI 助手
接下来,我们需要使用测试版 API 构建 AI 助手。在文档和非常有用的OpenAI Cookbook中有一些很好的资源。然而,由于这是新推出的测试版,信息还不是很多,所以有时需要一些试错过程。
首先,我们需要配置助手可以使用的工具,比如获取气候变化数据的功能。参考文档…
get_pf_data_schema = {
"name": "get_pf_data",
"parameters": {
"type": "object",
"properties": {
"address": {
"type": "string",
"description": ("The address of the location to get data for"),
},
"country": {
"type": "string",
"description": ("The country of location to get data for"),
},
"warming_scenario": {
"type": "string",
"enum": ["1.0", "1.5", "2.0", "2.5", "3.0"],
"description": ("The warming scenario to get data for. Default is 1.5"),
}
},
"required": ["address", "country"],
},
"description": """
This is the API call to the probable futures API to get predicted climate change indicators for a location
""",
}
你会注意到我们为每个参数提供了文本描述。根据实验,这似乎是助手在填充参数时使用的,所以要尽可能清晰,并记录任何特性,以便 LLM 能够调整。从这些定义工具…
tools = [
{
"type": "function",
"function": get_pf_data_schema,
}
{"type": "code_interpreter"},
]
你会注意到我保留了 code_interpretor,赋予助手运行数据分析所需代码的能力。
接下来,我们需要指定一组用户指令(系统提示)。这些指令在将助手的性能调整到我们的任务上时至关重要。通过一些快速实验,我得出了这一组…
instructions = """
"Hello, Climate Change Assistant. You help people understand how climate change will affect their homes"
"You will use Probable Futures Data to predict climate change indicators for a location"
"You will summarize perfectly the returned data"
"You will also provide links to local resources and websites to help the user prepare for the predicted climate change"
"If you don't have enough address information, request it"
"You default to warming scenario of 1.5 if not specified, but ask if the user wants to try others after presenting results"
"Group results into categories"
"Always link to the probable futures website for the location using URL and replacing LATITUDE and LONGITUDE with location values: https://probablefutures.org/maps/?selected_map=days_above_32c&map_version=latest&volume=heat&warming_scenario=1.5&map_projection=mercator#9.2/LATITUDE/LONGITUDE"
"GENERATE OUTPUT THAT IS CLEAR AND EASY TO UNDERSTAND FOR A NON-TECHNICAL USER"
"""
你可以看到我添加了指令,要求助手提供如网站等资源,帮助用户为气候变化做准备。这有点‘开放’,对于生产环境的助手,我们可能希望有更严格的筛选。
一件现在可能的奇妙事情是,我们还可以就一般的语气提供指令,在上面的例子中请求输出对非技术用户清晰。显然,所有这些需要一些系统化的提示工程,但有趣的是,我们现在在某种程度上通过劝说来‘编程’。😊
好的,现在我们有了工具和指令,让我们创建助手……
import os
from openai import AsyncOpenAI
import asyncio
from dotenv import load_dotenv
import sys
load_dotenv()
api_key = os.environ.get("OPENAI_API_KEY")
assistant_id = os.environ.get("ASSISTANT_ID")
model = os.environ.get("MODEL")
client = AsyncOpenAI(api_key=api_key)
name = "Climate Change Assistant"
try:
my_assistant = await client.beta.assistants.retrieve(assistant_id)
print("Updating existing assistant ...")
assistant = await client.beta.assistants.update(
assistant_id,
name=name,
instructions=instructions,
tools=tools,
model=model,
)
except:
print("Creating assistant ...")
assistant = await client.beta.assistants.create(
name=name,
instructions=instructions,
tools=tools,
model=model,
)
print(assistant)
print("Now save the DI in your .env file")
上述假设我们在.env文件中定义了密钥和代理 ID。你会注意到代码首先检查是否存在代理,使用.env文件中的ASSISTANT_ID并进行更新,否则它会创建一个全新的代理,生成的 ID 必须复制到.env文件中。否则,我会创建大量的助手!
一旦创建了助手,它将在OpenAI 用户界面上可见,可以在Playground中进行测试。由于大多数功能调用相关的开发和调试实际上涉及调用代码,我发现 Playground 对这次分析并不是特别有用,但它设计得很好,可能在其他工作中会有用。
对于这次分析,我决定使用新的GPT-4-Turbo模型,将model设置为“gpt-4–1106-preview”。
创建用户界面
我们想创建一个完整的聊天机器人,因此我从这个chainlit 食谱示例开始,稍微调整了一下,将代理代码分离到一个专用文件中,并通过……
import assistant_tools as at
Chainlit 非常简洁,用户界面易于设置,你可以在这里找到应用的代码。
尝试我们的气候变化助手 AI 代理
把这些放在一起——见代码这里——我们用简单的chainlit run app.py启动代理……

让我们询问一个位置……

注意到上述我故意拼写错误了 Mombasa。
然后代理开始工作,调用 API 并处理 JSON 响应(这大约花了 20 秒)……

根据我们的指令,它最终完成了……

但这是否正确?
让我们调用 API 并查看输出……
get_pf_data(address="Mombassa", country="Kenya", warming_scenario="1.5")
这会查询 API……
mutation {
getDatasetStatistics(input: {
country: "Kenya"
address: "Mombassa"
warmingScenario: "1.5"
}) {
datasetStatisticsResponses{
datasetId
midValue
name
unit
warmingScenario
latitude
longitude
info
}
}
}
这给出了以下内容(缩略显示)……
{
"data": {
"getDatasetStatistics": {
"datasetStatisticsResponses": [
{
"datasetId": 40601,
"midValue": "30.0",
"name": "Change in total annual precipitation",
"unit": "mm",
"warmingScenario": "1.5",
"latitude": -4,
"longitude": 39.6,
"info": {}
},
{
"datasetId": 40616,
"midValue": "70.0",
"name": "Change in wettest 90 days",
"unit": "mm",
"warmingScenario": "1.5",
"latitude": -4,
"longitude": 39.6,
"info": {}
},
{
"datasetId": 40607,
"midValue": "21.0",
"name": "Change in dry hot days",
"unit": "days",
"warmingScenario": "1.5",
"latitude": -4,
"longitude": 39.6,
"info": {}
},
{
"datasetId": 40614,
"midValue": "0.0",
"name": "Change in snowy days",
"unit": "days",
"warmingScenario": "1.5",
"latitude": -4,
"longitude": 39.6,
"info": {}
},
{
"datasetId": 40612,
"midValue": "1.0",
"name": "Change in frequency of \u201c1-in-100-year\u201d storm",
"unit": "x as frequent",
"warmingScenario": "1.5",
"latitude": -4,
"longitude": 39.6,
"info": {}
},
.... etc
}
]
}
}
}
进行抽查时,似乎代理完美地捕捉了它们,并向用户提供了准确的总结。
通过指令改善可用性
AI 代理可以通过一些指令来改进其信息呈现方式。
其中一项指令是始终生成一个返回到 Probable Futures 网站的地图可视化链接,点击后会转到正确的位置……

代理始终生成一个 URL,将用户带到正确的地图可视化上,以便在可能的未来网站上进行查询。
另一个指令要求代理始终提示用户尝试其他变暖情景。默认情况下,代理会生成预测全球温度升高 1.5 摄氏度的结果,但我们允许用户探索其他——有些令人沮丧的——情景。
分析任务
既然我们给了 AI 代理代码解释器技能,它应该能够执行 Python 代码来进行基本的数据分析。让我们尝试一下。
首先我询问了气候变化如何影响伦敦和纽约,代理提供了总结。然后我问了……

结果是代理使用代码解释器生成并运行 Python 代码以创建一个图表……

AI 代理能够使用从 API 提取的气候变化数据进行基本的数据分析任务。
不错!
结论与未来工作
通过使用 Probable Futures API 和 OpenAI 助手,我们能够创建一个对话界面,展示人们如何能够询问有关气候变化的问题并获得如何准备的建议。代理能够进行 API 调用以及一些基本的数据分析。这提供了另一种气候意识的渠道,这可能对一些非技术用户更具吸引力。
我们当然可以开发一个聊天机器人来确定意图/实体,并编写代码处理 API,但这需要更多工作,并且在 API 发生更改或添加新 API 时需要重新审视。此外,大型语言模型代理在解释用户输入和总结方面表现良好,并且可以运行代码和进行基本数据分析。我们的特定用例似乎特别适合 AI 代理,因为任务范围有限。
不过也有一些挑战,这个技术有点慢(查询完成大约需要 20-30 秒)。此外,LLM 令牌费用未在本文中分析,可能会很高。
也就是说,OpenAI Assistants API 仍处于测试阶段。此外,代理没有经过任何调整,因此通过进一步的工作,额外的常用功能、性能和成本可能会针对这一令人兴奋的新技术进行优化。
参考文献
本文基于 Probable Futures 提供的数据和其他内容,Probable Futures 是 SouthCoast Community Foundation 的一个项目,部分数据可能由 Woodwell Climate Research Center, Inc.或 The Coordinated Regional Climate Downscaling Experiment (CORDEX)提供
这个分析的代码可以在这里找到。
你可以在这里找到更多我的文章。
用故事地图互动展示空间数据 #2
原文:
towardsdatascience.com/presenting-spatial-data-interactively-with-story-maps-2-17363598208f
通过互动地图讲述故事和洞察
·发表于 Towards Data Science ·7 分钟阅读·2023 年 8 月 22 日
--

由 Sutan.co.uk 定制的故事地图(作者,2023)
演示链接:
由 Sutan.co.uk 提供的雅加达通勤故事。我们如何在雅加达通勤?使用 HTML、CSS 和 JavaScript 进行呈现…
jakarta.sutan.co.uk](https://jakarta.sutan.co.uk/?source=post_page-----17363598208f--------------------------------)
仓库链接:
[## GitHub — sutanmufti/story-maps
通过在 GitHub 上创建账户来为 sutanmufti/story-maps 的开发做贡献。
github.com](https://github.com/sutanmufti/story-maps?source=post_page-----17363598208f--------------------------------)
相关文章:
通过响应式地图讲述故事/洞察。用地图讲故事!
towardsdatascience.com
空间数据科学与制图
作为空间数据专家和数据科学家,我的工作是从(地理)空间数据中生成洞察。自然,最终的结果是将这些洞察可视化,以便人们理解这些洞察是什么。这需要沟通技巧。虽然普通的数据可视化围绕图表和图形展开,但空间数据可视化则围绕地图展开。制图师,负责制作地图的人员,本质上就是完成这项工作的人。
然而,我感觉“制图学”和“数据科学”之间的界限正在消退或融合。90 年代由 ESRI(环境系统研究所)推动的 GIS 的显著性,将信息技术整合到了制图学领域。这意味着地图不再是手工绘制的;相反,地图是基于你提供的表格绘制的。这是一个强大且新颖的范式!每个表格都可以通过一系列规则转换为地图。
“我们对制图学的定义将其描述为‘地图制作的艺术、科学和技术’,这一定义展示了这一学科的广度。” — 英国制图学会
什么使空间数据分析不同于数据分析?空间数据分析具有我称之为“空间属性”的东西;一个几何列。了解更多关于空间数据结构的信息,请参阅我的其他文章:
空间数据表达指南
towardsdatascience.com
从静态地图到互动地图
在互联网出现之前,空间数据分析的输出是静态地图。然而,随着 IT 技术的进步,我们现代的互联网浏览器能够处理互动性。这利用了 Javascript,一种使网站具有互动性的编程语言。这也惠及了制图界;我们可以将地图呈现为互动网页。看看 Google Maps 或 Openstreetmap。
好消息是,在这个开源和现代环境中,每个人都可以访问构建这种互动地图的工具。
本文
我想分享如何制作带有互动地图的空间数据可视化。更确切地说,它是一个故事地图;一个与段落相关的逐页地图。我希望这能激励人们生成更多互动地图,并作为点燃新颖空间数据可视化创意的灯塔。
我不会深入讲解代码。然而,我会讨论使事物互动的基本理念,并突出代码片段。你可以在 GitHub 仓库 这里 查看代码。
请注意,我将讨论的范围限制在使用 Javascript 处理的互动性。我不会讨论使用 CSS 的样式和使用 HTML 的布局。

雅加达故事地图(来源:作者,2023)
引入 Javascript 的互动性
虽然 Python 主要用于 GIS、数据科学和空间数据科学,但我们使用 HTML、CSS 和 Javascript 开发网站。要了解更多关于这些语言及其在空间数据科学中角色的信息,请阅读我的其他文章。
Spatial Data Science: Javascript & Python
从数据工程到全栈应用,你需要掌握的语言。入门以及精选学习资源……
towardsdatascience.com
你正在阅读的这篇文章的亮点是充分利用 JavaScript。处理用户输入或使我们的网站发生变化并活跃使用 JavaScript。
使用地图绘制库
我们不从零开始编码;使用模块/库是不可避免的。在地图绘制的情况下,经典的地图绘制库是 Leaflet(如前面文章中的链接所讨论的)。在上面的演示链接中,我使用 leaflet 来显示和动画化地图。
## Leaflet - an open-source JavaScript library for interactive maps
Leaflet 是一个现代的、轻量级的开源 JavaScript 库,用于移动友好的交互式地图。
请注意,这并不是唯一的地图绘制库,例如,Openlayers、Google Maps Javascript API、ArcGIS Javascript API、Mapbox 等!但我喜欢 leaflet,因为它重量轻,非常简单,并且最有可能解决 80%的问题。此外,它们的文档也很直接,并附有示例。
技术理念
在跳入代码高亮之前,我想先讨论一下代码试图实现的思想。代码只是实现目的的工具;我们必须先了解目的,然后再进行编码。
每页地图
故事地图由包含相关地图的页面组成,以说明页面内容。每个页面通常具有不同的地图。这意味着当激活页面时(发出事件),会调用一个函数。这是人们在学习 JavaScript 时通常会学到的内容。

页面(来源:作者,2023 年)
这是每个页面按钮的最简单伪代码
const pageButtonOne = document.querySelector('#pagebutton')
pageButtonOne.addEventListener('click',handlePageOne)
function handlePageOne(){
// remove the map layers
// adds the relevant layers
// zoom to the view
}
这就是处理事件(例如用户点击)的方式;每个按钮都有一个事件处理函数。Leaflet 中这样代码的一个简单示例可以在以下页面找到(演示链接):

页面响应性演示(来源:作者,2023 年)
## story-maps/pagebuttonexample.html at main · sutanmufti/story-maps
通过在 GitHub 上创建账户,为 sutanmufti/story-maps 的开发做出贡献。
实际的 Javascript 代码在以下代码块中:
let map = L.map('map').setView([51.505, -0.09], 13);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap'
}).addTo(map);
const pageone = document.getElementById('pageone')
const pagetwo = document.getElementById('pagetwo')
pageone.addEventListener('click', ()=>{
map.flyTo([51.510, -0.08], 10);
// add or remove layers
})
pagetwo.addEventListener('click', ()=>{
map.flyTo([52.545, -0.091], 8);
// add or remove layers,
// or other animation functions that reponse to the interactions
})
上述代码捕获了“点击”事件并运行相应的箭头函数(这是一个 JavaScript 的特性)。
处理地理空间数据
一张地图由数据图层组成。每个“图层”是一个包含几何列的表。这就是我们显示地理空间数据的方式。我还有另一篇文章讨论空间数据结构。
空间数据科学:数据结构
一个阐述空间数据的指南
towardsdatascience.com
我用 QGIS 处理、分析、绘制和操作地理空间数据。它是一个免费开源软件,旨在向每个人提供 GIS(地理信息系统)访问权限。作为免费软件,它竟然如此强大,实在令人印象深刻!

QGIS 用户界面(来源:作者,2023)
你可以用 QGIS 做什么?你可以进行空间数据分析和生成地图图层。它可以连接到如 Postgresql 这样的企业数据库,并管理系统中的空间数据。然后,进行空间 SQL 并根据 SQL 分析生成图层。
空间数据科学:空间查询
用 SQL 回答每一个“哪里”问题;附示例
towardsdatascience.com
这些分析(无论是来自 QGIS 工具箱还是空间 SQL 分析)会导出为适当的格式;最有可能是 GeoJSON 格式。这是一种符合 GeoJSON 标准的 JSON 格式,用于标准化地理空间数据序列化。这确保了数据的互换属性,允许 ogr2ogr 或 Leaflet 等多种软件处理空间数据。
使用 Python 扩展
当你想确保可重复性时,你可以使用 Python 控制台并生成你的脚本。QGIS 的 API 使用起来非常直接,还有一个 cook book。

Python 控制台(QGIS)
将 GeoJSON 与 Leaflet 集成
这些输出然后被添加到 leaflet 中,如下代码所示。对每个图层都要这样做。
async function loadGeojsonToMap(){
// using http request to obtain the geojson file from qgis
const resp = await fetch('./data.geojson')
const geojsonfeature = await resp.json()
const mapLayer = L.geoJSON(geojsonFeature)
mapLayer.addTo(map) // map was instanciated in the previous code block
return mapLayer
}
const mapLayer = await loadGeojsonToMap();
结论
制图是视觉艺术和数据科学的产物。计算机技术的进步使得制图变得互动。这样的互动,如果设计得当,可以生成丰富插图的叙事,允许读者进行探索。首先,我们使用 QGIS 或其他 GIS 软件分析并生成图层。然后,我们通过 Leaflet(HTML、Javascript 和 CSS)提供分析结果。最棒的是,这完全免费,每个人都可以做到。你也可以做到这一点。
如果你喜欢空间和网络数据科学内容,请关注我。我撰写与城市规划、基础设施发展和交通背景相关的高级空间数据分析内容。
使用网络地图展示空间数据
原文:
towardsdatascience.com/presenting-spatial-data-with-web-maps-4069c01e26ac
深入探讨地图图块、基础地图、地图图层和矢量数据
·发布于 Towards Data Science ·15 分钟阅读·2023 年 8 月 15 日
--

爱尔兰西部历史泥炭沼泽地图,Corine 土地覆盖数据 2000
github.com/mmc1718/webmap-ireland
🗺 内容
-
简介
- 网络地图的工作原理
-
准备基础地图
-
创建矢量图块集
-
识别所需的更改
-
修改图块集
-
-
样式化基础地图 - 创建 JSON 样式文件
- 提供样式化的地图图块
-
创建网络地图 - 加载基础地图
- 在地图上叠加数据
制作地图的方式有很多种。我们可以使用桌面 GIS 软件,如 QGIS 或 ArcGIS,使用 Web 框架,如 Leaflet 或 Mapbox GL JS,或者使用传统的墨水和纸张来完成。
网络地图是展示空间数据的绝佳选择,因为它们易于分享且具有互动性。现在有许多工具可以使创建网络地图的过程变得简单,同时让我们完全控制地图的各个元素。
我将介绍使用开源软件和开放数据源创建您自己的地图的完整过程,包括如何使用OpenStreetMap和Natural Earth数据创建自定义矢量基础地图,使用Tileserver提供图块,将基础地图加载到网页上,并使用Maplibre在地图上叠加空间数据。
作为示例,我将创建一个展示十年期间泥炭沼泽损失的爱尔兰泥炭沼泽地图。我已准备好来自 Corine 土地覆盖清单的数据,使其可以在网络地图上显示。最终结果将是一个在浏览器中可查看的美观的爱尔兰沼泽地图。
你可以在这里查看已部署的最终地图版本: marymcguire.dev/ireland-bog-map
这个项目的代码库可以在这里找到:
[## GitHub - mmc1718/webmap-ireland: 显示爱尔兰泥炭沼泽的网络地图
显示爱尔兰泥炭沼泽的网络地图。通过创建账户来为 mmc1718/webmap-ireland 的发展做贡献…
github.com](https://github.com/mmc1718/webmap-ireland?source=post_page-----4069c01e26ac--------------------------------)
Docker 将是这个项目的主要依赖,因此如果你还没有安装,你需要先安装它才能继续。假设你具有一些基本的 GIS 概念知识,基本的终端使用、读取 JSON 文件的能力,并能够跟随使用 JavaScript 和 HTML 的简单示例。
网络地图如何工作
数字地图由两个主要部分组成:基础地图和数据层。如果你还不熟悉网络地图,它们与此无异。



都柏林的三个网络地图视图,地图数据版权归 OpenStreetMap 所有
数据层通常是制作地图的根本原因。基础地图为我们提供了一个基础地图,以便我们在其上叠加数据。样式设计对基础地图很重要,以支持数据层而不分散注意力,并且对数据层也很重要,使数据可读并传达关于其价值的附加信息(较暗或较亮的颜色,或较大或较小的符号传达不同的意义)。
通常,网络基础地图以一组单独的地图瓦片的形式出现,这些瓦片拼接在一起形成一张地图。

图示摘自《空间数据基础设施的 Web 地图瓦片服务:管理与优化》,该章节在 CC BY 许可证下发布,详细信息请参见www.intechopen.com/chapters/38302#F1
重要的是,这些地图瓦片被分类为不同的缩放级别,这意味着完整的地图瓦片集实际上包括了许多版本的地图——每个缩放级别一个。当你在网络地图(或“滑动地图”)上放大或缩小时,地图特征会变得更加详细或不那么详细,有些特征只有在达到某些缩放级别时才会出现。在后台,页面会加载与当前查看的缩放级别和地图部分相对应的新瓦片。
将这些瓦片拼凑在一起,就形成了一个瓦片金字塔。我们将其视为金字塔,因为缩放级别越高,覆盖整个区域所需的瓦片就越多。Maptiler 提供了一个很好的可视化图示,你可以在开始使用网络地图时参考。
在处理地理空间数据时,你可能听说过矢量数据和栅格数据之间的区别。地图切片也不例外;单个切片可以是矢量切片,通常是 .pbf 文件,也可以是栅格 (.png) 文件。
我将创建一个存储在 Mapbox 的 MBTiles 格式中的矢量切片集作为基础地图。我更喜欢矢量,因为它们提供了更多的灵活性。
准备基础地图
创建矢量切片集
第一步是创建基础地图切片集。有许多现成的基础地图可用,但我希望地图将重点放在爱尔兰岛上,排除大不列颠。这并不常见,因此我将制作自己的基础地图。
我使用Planetiler来创建矢量切片基础地图。Planetiler 是一个创建矢量地图切片集的工具,速度非常快。甚至还有一个 Docker 镜像,这使得使用 Planetiler 生成完整的切片集变得简单。Planetiler 使用 OpenStreetMap 数据来创建地图切片,而对我来说,爱尔兰和北爱尔兰的数据提取已经可用。
运行以下命令将下载所需的数据,并使用 OpenMapTiles 架构作为默认值生成切片集:
docker run --rm -v "$(pwd)/data":/data ghcr.io/onthegomap/planetiler:latest --download --area=ireland-and-northern-ireland --minzoom=4
注意 minzoom 参数设置为 4。这是因为我们的地图仅关注爱尔兰,所以不需要在爱尔兰过于小的非常低的缩放级别生成切片。缩放级别 4 是我们需要的最低级别,设置限制将节省切片生成时间。
Planetiler 完成的时间将根据你的硬件而有所不同,但可能需要一个小时或更长时间。让进程运行,等待的同时做些其他事情。
一旦切片完成,我们就可以查看它们。我使用Tileserver来完成这项工作。同样,我们可以使用 Docker 来运行此步骤,从而节省时间。确保在与切片相同的目录中运行此命令,或者使用 mbtiles 的完整路径运行。
docker run --rm -it -v $(pwd):/data -p 8080:8080 maptiler/tileserver-gl --mbtiles=./ireland_and_northern_ireland.mbtiles
我们的切片现在可以在 http://localhost:8080 上查看。
默认情况下,Tileserver 允许我们查看原始切片数据或使用基本样式的样式化切片。这对于检查我们的切片是否符合预期并识别任何问题非常有用。
即使原始数据没有样式,功能也会根据它们所在的数据层进行颜色编码,并且旁边有一个图例显示详细信息。这在样式化时需要特别注意。
确定所需的更改
查看切片数据揭示了切片本身存在的一个直接问题。

使用 Tileserver GL 查看未样式化的矢量切片集
尽管我们仅使用了爱尔兰和北爱尔兰的数据来生成地图,但大不列颠的轮廓仍然可见。这是因为 Planetiler 生成了填充水域的海洋多边形,使得陆地部分为空。我们看到的是大不列颠岛应在的位置的海洋空洞——如果你点击岛屿,你会发现没有特征,连陆地都没有。
我们该如何处理切割海洋效果?我们无法填补空洞,但对我们来说重要的是,我们可以对背景和陆地边界进行样式设置。通常这会通过给陆地周围的海洋设置样式来完成,以使背景填充陆地。我们需要反过来做,为爱尔兰岛包括一个陆地多边形,以对背景进行样式设置和对比。
在创建样式文件之前,需要做一些更改。首先,我们需要用额外的特征来补充数据——爱尔兰的陆地面积。
切片集还有一个稍微不那么明显的问题,但在放大时会看到。

注意到县界(粉色)在缩放级别 4 之后消失。在缩放级别 1-4 之间,Planetiler OpenMapTiles 模式使用 Natural Earth 的数据而非 OSM 来生成地球的低分辨率地图。缩放级别 4 之后,使用的是 OpenStreetMap 的数据。由于我们从缩放级别 4 开始生成切片,这一点很明显。
由于 Planetiler OpenMapTiles 模式适用于整个星球,不可避免地在国家之间存在一些不一致性。爱尔兰县界被从 OSM 数据中过滤掉就是这些不一致性之一。我们的地图特别是关于爱尔兰的,因此我们希望把它做对。
除了不一致性外,县界在爱尔兰地图上是一个重要特征,我们希望在所有缩放级别中都包括它。
简单的解决方案是从缩放级别 4 开始,将 Natural Earth 的县界添加回切片集中。
修改切片集
Tippecanoe 是一个非常有用的工具,用于处理 MBTiles 切片集。它允许我们从 GeoJSON 数据创建 mbtiles,并将 MBtiles 文件合并在一起。我们只需要所需的额外数据(GeoJSON 格式),就可以将所需的特征合并到切片集中。
[## GitHub - felt/tippecanoe: 从大型 GeoJSON 特征集合中构建矢量切片集。
从大型 GeoJSON 特征集合中构建矢量切片集。- GitHub - felt/tippecanoe: 从大型 GeoJSON 特征集合中构建矢量切片集…
为了获取表示爱尔兰的多边形——或更可能是多多边形,感谢其岛屿——我转向 OpenStreetMap,特别是Overpass Turbo。不立即清楚我们需要哪个特征(爱尔兰岛?爱尔兰和北爱尔兰?),因此需要一些探索。
通过在openstreetmap.org上使用‘查询特征’工具,我能够找到两个关系,当它们结合时,结果正是我所需要的特征。通过检查标签,我可以创建一个查询以供 Overpass 使用。
使用Overpass Turbo获取两个特征如下所示:

Overpass Turbo 的截图,显示了爱尔兰和阿尔斯特的关系
查询:
[out:json][timeout:25];
(
relation["ref"="IE0"];
relation["boundary"="historic"]["name"="Ulster"];
);
out body;
>;
out skel qt;
导出按钮允许我们将结果保存为 GeoJSON 文件。剩下的就是合并这两个特征,以便得到一个 MultiPolygon。
县界线可以从Natural Earth 网站下载。实际上,Planetiler 已经为你下载了 Natural Earth 数据的副本,但由于我们只需要国家边界,单独下载此提取文件会更简单。文件名为Admin 1 — States, Provinces,下载链接如下:
你可以在这个项目的GeoJSON 文件示例笔记本中找到准备两个 GeoJSON 文件的例子。
一旦 GeoJSON 文件准备好后,你可以从 GitHub 克隆Tippecanoe repo,并使用 Docker 构建镜像或从源代码安装(请参阅 README 获取说明)。我用于将数据合并到我的 tileset 中的命令如下:
# convert the files to mbtiles
tippecanoe -o ireland_outline.mbtiles -f ireland.geojson
tippecanoe -o ireland_counties.mbtiles -f counties.geojson
# merge the mbtiles into the main tileset
tile-join -o "ireland_1.mbtiles" --name="OpenMapTiles" -pk -f ireland_and_northern_ireland.mbtiles ireland_outline.mbtiles
tile-join -o "ireland_final.mbtiles" --name="OpenMapTiles" -pk -f ireland_1.mbtiles ireland_counties.mbtiles
使用 Tileserver 预览合并新数据后的新图块时,我们注意到了一些变化。
提示:运行 Tileserver 时,请确保使用的是正确的 mbtiles 文件,即你使用 Tippecanoe 创建的文件。运行 Tileserver 时使用参数--mbtiles来指定文件。

未样式化的矢量数据视图,包括爱尔兰和县界层
图例提供了一种快速验证我们新图层是否存在的方法。我们可以在这里看到两个新图层;‘ireland’和‘counties’。
样式化基础地图
创建 JSON 样式文件
现在我们已经准备好基础 tileset,准备开始样式化。当制作样式时,我选择突出自然特征如山脉,减少对如高速公路等人造基础设施的关注,以保持沼泽主题。
如果你对地图样式不熟悉,它们是包含有关如何样式化切片集中矢量数据的信息的 JSON 文档。 Maputnik 是一个样式编辑器,我们可以用来创建和编辑样式,它允许我们在工作时预览样式和原始数据。它是 Mapbox 样式编辑器的一个开源替代品,可以使用公开的 Docker 镜像运行。
[## GitHub - maputnik/editor: 一个开源的视觉编辑器,用于'Mapbox Style Specification'
一个开源的视觉编辑器,用于'Mapbox Style Specification' - GitHub - maputnik/editor: 一个开源的视觉…
github.com](https://github.com/maputnik/editor?source=post_page-----4069c01e26ac--------------------------------)
一旦 Maputnik 打开,打开一个空白样式。
首先,我们需要将其连接到我们的切片集。这是通过‘数据源’选项卡完成的。你需要复制你的 TileJSON 文件的链接 —— 使用 Tileserver 可以查看 —— 并将其粘贴到 #OpenMapTiles 活跃的数据源中。TileJSON 文件是一个包含有关你的切片集信息的 JSON 元数据文件,包括源 URL。

Maputnik 截图,显示了 Sources 窗口
如果一切正常,你应该能够将视图从‘地图’切换到‘检查’,并在屏幕上看到你的数据。数据将类似于使用 Tileserver 查看的原始数据。在你创建了一些样式图层之前,你的‘地图’视图将只显示一个空白屏幕。

在 Maputnik 中切换地图和检查视图
指定特征样式的方式是通过样式图层完成的。这些图层可以在 Maputnik 的侧边面板中编辑,并将被转换为我们最终样式 JSON 文件中的‘layers’字段中的 JSON 对象。点击‘添加图层’按钮开始操作。
第一个创建的图层通常是背景。之后,根据需要添加图层以定位不同的特征。注意图层的顺序会有所不同 —— 特征将根据样式顺序显示在彼此的上方或下方。
样式图层影响哪些特征取决于其源(特征所在的数据层,例如运输或土地使用)以及我们设置的过滤器。你可以在创建图层时指定这一点。
例如,我们可以过滤边界样式图层,以仅影响边界源图层中的陆地边界,通过使用 maritime 标签忽略海洋边界:

在 Maputnik 中查看的县界样式选项
在 JSON 中,这个字段最终变成如下:
"filter": [
"all",
["!=", "maritime", 1],
[">", "admin_level", 2],
["<", "admin_level", 8]
],
在底部的侧边面板中,你可以看到原始 JSON 的样子。
请注意,多个样式表可以影响相同的特征。这使得能够渲染相同特征的不同版本,例如一个点特征可以同时显示为标签和图像。
使用 Maputnik 创建的样式遵循 Mapbox 样式规范,因此如果有疑问,你可以 查看文档。
你可以在 这里 查看我最终的样式。
在线有大量关于制图的提示信息。Esri 发布了 一份在线资源列表,这是一个很好的起点。
请注意,我为样式添加了一个额外的远程字体和精灵(徽标或图像)源。这可以在 Maputnik 的源窗口中完成,我们在这里添加了我们的图块源。我使用的源是 https://orangemug.github.io/font-glyphs/glyphs/{fontstack}/{range}.pbf 用于字体和 https://openmaptiles.github.io/osm-bright-gl-style/sprite 用于精灵。(查看 Orange Mug 的自定义字体库 github.com/orangemug/font-glyphs)
提供样式地图图块
一旦你的样式准备好,从 Maputnik 下载它并保存在与你的 MBTiles 图块集相同的目录中。我们需要为 Tileserver 创建一个配置文件,以便在提供图块时它可以找到你的自定义样式。
创建一个名为 config.json 的新文件,并确保其包含如下内容。
{
"options": {
"paths": {
"root": "",
"fonts": "/usr/src/app/node_modules/tileserver-gl-styles/fonts",
"styles": "styles",
"mbtiles": ""
}
},
"styles": {
"natural": {
"style": "ireland.json",
"tilejson": {
"bounds": [
-16.325684,
47.960502,
-5.053711,
56.872996
]
}
}
},
"data": {
"v3": {
"mbtiles": "ireland.mbtiles"
}
}
}
这个配置文件将加载一个名为 ireland.mbtiles 的 mbtiles 文件(使用在选项 -> 路径 -> mbtiles 下定义的路径)。Tileserver 将查找在样式目录(选项 -> 路径 -> styles)中名为 ireland.json 的样式文件来提供服务。请注意,字体、样式和 mbtiles 的路径假定是相对于根目录的。
在包含新配置文件的目录中重新启动 Tileserver 将使我们新的样式在 localhost:8080 上可供查看。
提示:如果你希望你的图块在 localhost 之外的地方可用,你需要在配置文件中包含‘domains’。详细信息请参见 Tileserver 文档。
创建 Web 地图
加载基础地图
我们已经有了基础地图和样式。现在剩下的就是将数据叠加到地图上。我有两个关于爱尔兰沼泽的数据文件。我准备数据的步骤可以在 GitHub 上查看。
我使用 Maplibre 来创建我的地图。Maplibre 是 Mapbox GL JS 的开源替代品,非常相似。我们可以直接在 HTML 文件中添加 JavaScript 脚本,或者使用 JavaScript 文件并导入它。
按照Maplibre 文档中的示例,首先创建一个 HTML 文件。包含一个<script>标签,标签内的代码将使用新创建的自定义底图创建地图实例。
<div id="map"></div>
<script>
const map = new maplibregl.Map({
container: "map",
// stylesheet location
style: "http://localhost:8080/styles/natural/style.json",
center: [-8, 53], // starting position [lng, lat]
zoom: 7, // starting zoom
minZoom: 6,
maxBounds: [
[-17.7, 46.6],
[2.7, 58.6],
],
});
</script>
注意,我在‘style’字段中包含了由 Tileserver 提供的样式文件链接。
设置边界意味着用户将无法将地图*移到定义的边界框坐标之外,这很有用,因为我们的数据仅限于地图上的一个小区域。默认情况下,可以*移整个世界,这在我们的情况下并不是我们想要的。可以使用这个方便的工具找到自定义边界:norbertrenner.de/osm/bbox.html。
要查看我们的页面,我们需要一个 Web 服务器。我使用VSCode 的 Live Server 扩展来快速查看开发中的网页。它可以免费安装,并且意味着你只需点击一下按钮即可提供 Web 应用。加载页面将显示我们的底图:

作者样式的爱尔兰底图
在地图上叠加数据
接下来,我们将数据图层添加到地图中。同样,Maplibre 网站提供了许多示例,涵盖了创建地图时可能遇到的各种场景。
要加载 GeoJSON 文件,我们首先需要将文件路径添加为源。定义文件源后,我们可以将具有样式选项的图层添加到地图上。这一切都在一个函数中完成,该函数在地图的“加载”事件触发时运行(这就是map.on(“load" …)的意思)。我将新函数命名为setUpMap并将其作为第二个参数传递给map.on(“load")。
function setUpMap() {
map.addSource("peat_bogs", {
type: "geojson",
data: "data/ireland_peat_bogs_2012.geojson",
});
map.addSource("lost_bogs", {
type: "geojson",
data: "data/peat_bog_loss.geojson",
// required for smooth rendering while zooming
tolerance: 0,
});
map.addLayer({
id: "peat_bogs",
type: "fill",
source: "peat_bogs",
paint: {
"fill-color": "#92a94a",
"fill-opacity": 0.4,
},
});
map.addLayer({
id: "lost_bogs",
type: "fill",
source: "lost_bogs",
paint: {
"fill-color": "#744333",
"fill-opacity": 0.4,
},
})
};
map.on("load", setUpMap);
每个图层使用不同的颜色进行样式设置,并且稍微透明,以便基础地图中的地点标签和细节能够显示出来。我使用了这个方便的工具来选择和比较颜色。
我还使用了一个名为 maplibre-gl-legend 的插件,插件可以在这里找到:github.com/watergis/maplibre-gl-legend。以下代码定义了图例:
const legendInfo = {
lost_bogs: "Lost Peat Bog (Derived from Corine Land Cover (CLC) 2000, Version 2020_20u1)",
peat_bogs: "Peat Bog (Corine Land Cover (CLC) 2012, Version 2020_20u1)",
};
map.addControl(
new MaplibreLegendControl.MaplibreLegendControl(legendInfo, {
showDefault: true,
showCheckbox: true,
onlyRendered: false,
reverseOrder: true,
title: "Peat Bog loss in Ireland Between 2000 and 2012",
}),
"top-right"
);j
图例还提供了一种方式,使我能够为数据源包含署名,而无需在其他地方添加这一内容。
查看最终结果,我们可以看到一个完整的沼泽地地图,附带互动图例:

爱尔兰 2000–2012 年间泥炭沼泽损失的地图,附带图例
感谢阅读!如果你觉得这篇文章有用,可以考虑点赞、关注作者或在 GitHub 上给项目加星👏
贝叶斯深度学习入门
原文:
towardsdatascience.com/primer-on-bayesian-deep-learning-d06e0601c2ae
概率深度学习
·发表于 Towards Data Science ·阅读时间 8 分钟·2023 年 2 月 1 日
--
介绍
本文属于“概率深度学习”系列。该系列每周涵盖概率方法在深度学习中的应用。主要目标是扩展深度学习模型以量化不确定性,即了解它们不知道什么。
贝叶斯深度学习是一个新兴领域,它将深度学习的表达能力和表征能力与贝叶斯方法的不确定性建模能力相结合。这两种范式的整合提供了一个原则性框架,用于解决深度学习中的各种挑战,如过拟合、权重不确定性和模型比较。
在本文中,我们提供了对贝叶斯深度学习的全面介绍,涵盖其基础、方法论和最新进展。我们的目标是以清晰易懂的方式展示基本概念和思想,使其成为研究人员和实践者的理想资源,特别是那些对该领域较新的人员。
迄今为止已发布的文章:
-
从头开始在 TensorFlow Probability 中进行最大似然估计
-
从头开始在 TensorFlow 中进行概率线性回归
-
确定性与概率深度学习
-
从头开始使用 TensorFlow 实现朴素贝叶斯
-
贝叶斯深度学习

图 1:今日格言:层层深入 (source)
确定性、概率性和贝叶斯深度学习
深度学习在计算机视觉、自然语言处理和游戏等多个应用领域取得了显著成功。尽管如此,传统的深度学习模型本质上是确定性的,并且在其预测中提供了有限的不确定性量化能力。为了解决这个问题,贝叶斯深度学习和概率深度学习作为重要范式出现,允许将不确定性融入深度学习模型中。
贝叶斯深度学习和概率深度学习代表了将不确定性融入深度学习模型的重要范式。这些方法相比传统的确定性深度学习具有若干优势,包括能够提供不确定性估计以及在存在数据分布外数据时进行稳健推理的能力。
在贝叶斯深度学习中,模型参数被视为随机变量,并对其施加先验分布。这个先验代表了关于模型参数的先验知识,例如它们的期望值或分布形状。然后,通过贝叶斯推理更新参数的后验分布,使用数据形成一个后验分布,这代表了我们在数据给定下对参数的更新信念。这将导致模型参数的分布以及模型预测中的不确定性度量。
概率深度学习则将数据生成过程建模为概率函数。给定一个输入,模型预测一个输出分布,从而允许量化预测中的不确定性。这种方法在输出空间复杂的问题中尤为有用,例如图像生成或语音合成。在这些情况下,将数据生成过程建模为概率函数可以捕捉数据中的复杂模式,并生成高质量的输出。
贝叶斯深度学习和概率深度学习都是重要且活跃的研究领域,最*在贝叶斯神经网络、变分推断和深度生成模型等技术方面取得了进展。尽管这些方法具有潜在的好处,但由于高维模型和深度学习中通常使用的大量数据所带来的计算和统计困难,这些方法仍然具有挑战性。尽管如此,*年来该领域已经取得了显著进展,贝叶斯深度学习和概率深度学习的未来研究充满了兴奋和潜力。
贝叶斯学习:概述
在这篇文章中,我们考虑了用于统计推断的贝叶斯框架,其中我们用符号𝑃表示概率密度。
贝叶斯方法提供了一种统一的统计推断方法,我们的目标是计算给定一些观察数据的模型参数分布。在神经网络的背景下,这对于估计权重参数的不确定性尤为有用,因为我们可以计算给定训练数据的这些参数的后验分布。这是通过应用贝叶斯定理实现的,该定理指出:

其中:
-
𝐷:观察数据,以𝑥和𝑦值对表示,例如 𝐷=
-
𝑤:模型权重的值
-
𝑃(𝑤):先验密度,表示在观察到数据之前对模型权重分布的初始信念
-
𝑃(𝐷|𝑤):给定权重𝑤时观察到数据的似然
-
𝑃(𝑤|𝐷):模型权重的后验密度,通过贝叶斯定理将观察到的数据和先验信念结合计算得出。
归一化项 ∫𝑃(𝐷|𝑤′)𝑃(𝑤′)d𝑤′=𝑃(𝐷) 是与𝑤无关的,用于归一化后验密度。
贝叶斯定理使我们能够将观察到的数据与我们的先验信念结合,以获得模型参数的后验分布,提供它们不确定性的概率表示。在神经网络背景下实施贝叶斯学习可能很具挑战性,因为计算归一化常数的复杂性。为了克服这一点,使用了各种*似方法,例如变分贝叶斯。这些方法旨在有效地估计神经网络权重的后验分布。
变分贝叶斯推断
在变分贝叶斯方法中,我们使用第二个函数来*似后验分布,这个函数称为变分后验。这个函数由一组参数来表征,记作𝜃,可以通过优化来学习。关键思想是选择一个变分后验的函数形式,使其能够有效地优化,以尽可能接*真实的后验分布。
一种常见的评估*似质量的方法是使用变分后验和真实后验之间的 Kullback-Leibler (KL) 散度。KL 散度是衡量两个概率分布之间差异的指标,当两个分布相等时,其值为 0。
给定观察数据 𝐷,变分后验 𝑞(𝑤|𝜃) 和真实后验 𝑃(𝑤|𝐷) 之间的 KL 散度定义为

当将观察数据视为常量时,我们可以将这个表达式简化为仅依赖于 𝜃 和 𝐷 的函数:

第一项测量变分分布和先验分布之间的差异,第二项是变分后验下的期望负对数似然。
这个函数,称为变分下界,可以针对 𝜃 进行优化,以获得对真实后验的最佳*似。优化可以使用基于梯度的方法进行,使变分贝叶斯能够扩展到大型复杂模型。
贝叶斯神经网络的反向传播方案
在这一部分,我们描述了一种用于贝叶斯神经网络的反向传播算法,允许我们将权重不确定性纳入模型。我们的方法涉及引入一个密度为 𝑞(𝑤|𝜃) 的变分后验,其中 𝑤 是网络中的一个权重,𝜃 是一组可训练的参数。这个后验表示了我们对真实权重后验的*似,真实权重后验由观察数据和我们对权重的先验信念(通过先验密度 𝑃(𝑤) 编码)决定。
我们的目标是更新变分后验,使其准确地逼*真实的权重后验,这一目标通过证据下界(ELBO)来量化。为此,我们最小化变分后验和先验之间的 ELBO,公式如下:

其中 𝐷 是训练数据。然而,这个表达式涉及对权重 𝑤 的积分,这可能在计算上不可行。为了解决这个难题,我们将 ELBO 重写为期望:

为了进行反向传播,我们需要对 𝐿(𝜃|𝐷) 关于 𝜃 进行求导。然而,这很具有挑战性,因为期望的底层分布依赖于 𝜃。我们通过重参数化技巧克服了这个问题,这使得我们能够通过期望进行求导。
重参数化技巧:贝叶斯神经网络中随机梯度优化的关键技术
重新参数化技巧是一种数学技术,用于优化具有随机参数的模型。其目标是将依赖变量𝑤转换为具有固定分布且与模型参数𝜃无关的随机变量𝜖。这种转换使得可以独立于𝜃计算𝑓(𝑤;𝜇,𝜎)对𝑞(𝑤|𝜇,𝜎)的期望,从而实现模型的高效优化。
例如,考虑一个高斯分布𝑞(𝑤|𝜇,𝜎),其中𝜃=(𝜇,𝜎)。通过变量变换𝑤=𝜇+𝜎𝜖,其中𝜖∼𝑁(0,1),我们得到

然后可以轻松计算关于𝜇和𝜎的导数,并通过蒙特卡罗采样来估计期望。
重新参数化技巧是贝叶斯神经网络中高效优化的关键工具,并且在现代深度学习研究中得到了广泛应用。它使得可以对具有随机参数的模型进行基于梯度的优化,从而实现高效的训练和推断。
最终连接
贝叶斯深度学习提供了一种将不确定性融入深度学习模型的框架。通过将神经网络权重视为随机变量,我们可以捕捉到内在不确定性和认识不确定性,从而提供更稳健和可靠的预测。变分贝叶斯推断提供了一种学习网络权重后验分布参数的方法,这些参数可以通过反向传播方案进行优化。
重新参数化技巧(reparameterization trick),这是贝叶斯神经网络中用于随机梯度优化的关键技术,使我们能够通过将随机变量转换为可微分的形式来有效地估计梯度。这一技巧使得可以使用标准的基于梯度的优化算法来学习网络权重后验分布的参数。
需要注意的是,尽管内在不确定性(aleatoric uncertainty)考虑了数据中的固有噪声,但认识不确定性(epistemic uncertainty)则捕捉了我们对基础模型知识的不了解。在贝叶斯深度学习中,认识不确定性通过对网络权重施加先验分布并通过变分推断过程用观察数据更新它来建模。这导致了一个更灵活和信息量更大的模型,因为网络可以适应新数据并融入有关基础关系的额外信息。
结论
总之,贝叶斯深度学习提供了一种将不确定性融入深度神经网络预测的框架。通过将网络权重视为随机变量并建模其后验分布,我们可以获得比传统确定性深度学习方法更稳健和信息量更大的模型。
重参数化技巧与基于梯度的优化算法相结合,使得深度模型中的可扩展贝叶斯推断成为可能。因此,贝叶斯深度学习受到越来越多的关注,并在包括主动学习、分布外检测和不确定性感知强化学习等各种应用中取得了成功。这个领域仍然相对较新,仍有很大的探索和创新空间。
在未来,我们期望贝叶斯深度学习在人工智能和机器学习的发展中扮演越来越重要的角色。
关于我
连续创业者和 AI 领域的领导者。我为企业开发 AI 产品,并投资于专注于 AI 的初创公司。
创始人 @ ZAAI | LinkedIn | X/Twitter
参考资料
[1] — Coursera: Deep Learning Specialization
[2] — Coursera: TensorFlow 2 for Deep Learning 专业化课程
隐私政策
最后更新于 2024 年 9 月
我们收集、存储和使用个人信息的方法
信息贡献者提供给我们的
·发表于 Towards Data Science ·1 分钟阅读·2023 年 3 月 7 日
--

图片由 Annie Spratt 拍摄,来源于 Unsplash
我们是一个利用 Medium *台将数据专业人士汇聚在一起的出版物,旨在分享和扩展他们对数据科学、机器学习及相关主题的知识。
随着 TDS 多年来的不断发展,我们的工作流程和编辑实践也发生了变化。我们希望向贡献者明确这些变化如何可能影响他们与我们分享的个人信息,因此我们制定了一份隐私政策,详细阐述了我们的方法。
您可以在此处找到最新版本:隐私政策 — 2024 年 9 月。
请注意,Medium 自身有其隐私政策,您可能还想查看一下 — 您可以在这里找到它。
如果您有任何问题,您可以在此页面找到所有联系信息。您还可以在这里浏览我们隐私政策的旧版本:2023 年 3 月、2022 年 8 月、2020 年 7 月。
私有 GPT:在企业数据上微调 LLM
原文:
towardsdatascience.com/private-gpt-fine-tune-llm-on-enterprise-data-7e663d808e6a
用数据做一些酷炫的事情
·发表于数据科学前沿 ·阅读时间 9 分钟·2023 年 7 月 5 日
--

照片由Robynne Hu提供,来源于Unsplash
介绍
在大数据和先进人工智能的时代,语言模型已成为能够处理和生成类人文本的强大工具。像 ChatGPT 这样的大型语言模型是能够在许多主题上进行对话的通用机器人。然而,LLM 也可以在特定领域的数据上进行微调,使它们在特定领域的企业问题上更加准确和切题。
许多行业和应用将需要经过微调的 LLM。原因包括:
-
从在特定数据上训练的聊天机器人中获得更好的性能
-
像 ChatGPT 这样的 OpenAI 模型是一个黑箱,公司可能会犹豫通过 API 分享他们的机密数据。
-
ChatGPT API 的费用对于大型应用程序可能会很高。
微调 LLM 的挑战在于该过程尚不明确,而且训练一个没有优化的十亿参数模型所需的计算资源可能非常昂贵。
幸运的是,很多关于训练技术的研究已经完成,这使我们现在能够在较小的 GPU 上微调 LLM。
在本博客中,我们将介绍一些用于微调 LLM 的技术。我们将使用 Colab GPU 在金融数据上训练Falcon 7B 模型!这里使用的技术是通用的,可以应用于其他更大的模型,如 MPT-7B 和 MPT-30B。
在深度学习分析中,我们过去 6 年一直在构建定制的机器学习模型。如果你对为你的应用程序微调一个 LLM 感兴趣,联系我们。
QLoRA
QLoRA,即“量化低秩适配”,呈现了一种结合量化和低秩适配的方法,以实现 AI 模型的高效微调。以下将详细解释这两个术语。
QLoRA 减少了微调 LLM 所需的内存,同时在性能上与标准的 16 位模型微调模型没有任何下降。这种方法使得 70 亿参数的模型可以在 16GB GPU 上微调,330 亿参数的模型可以在单个 24GB GPU 上微调,以及 650 亿参数的模型可以在单个 46GB GPU 上微调。
这意味着大多数公司现在可以以较小的成本拥有微调后的 LLM 或本地模型。
QLoRA 由两种技术组成:
-
LoRA — 低秩适配
-
量化
LoRA
LoRA,即低秩适配器(LoRA),是小型可训练参数集,注入到 Transformer 架构的每一层中进行微调。在微调过程中,原始模型权重被冻结且未更新,这些注入的权重在微调时会更新。这大大减少了下游任务的可训练参数数量。在随机梯度下降期间,梯度通过被冻结的预训练模型权重传递到适配器。因此,只有这些具有小内存占用的适配器在训练过程中被更新。
量化
接下来,我们将讨论量化。要理解量化,首先需要了解通常用于存储模型权重的不同数据类型。
-
FP32 — 完整精度权重,占用 4 字节内存
-
FP16/BF16 — 半精度权重,占用 2 字节内存
-
INT8 — 数据类型包括一个 8 位表示,可以存储 2⁸ 个不同的值(在 [0, 255] 或 [-128, 127] 的范围内用于有符号整数)
-
FP8 和 FP4,分别代表浮点 8 位和 4 位精度。它们是小型浮点值家族的一部分。
使用量化,模型需要的内存比存储原始模型所需的内存要小得多。例如,一个 8 位量化模型只需原始模型大小的 1/4。
那么,如何将原始的 32 位权重存储在像 INT8 或 FP8 这样的 8 位数据类型中呢?
量化意味着将值从一种数据类型“舍入”到另一种数据类型。它通过将较大的值压缩到具有较少位数的数据类型中来工作,但会有少量的精度损失。
在 QLoRA 的情况下,模型权重存储在 4 位浮点数据类型中。但为了尽量减少总体精度损失,所有矩阵运算都在 16 位浮点中执行。计算完成后,权重会被存储/量化回来,然后再存储。
总结一下,QLORA 使用一种存储数据类型(通常是 4 位 NormalFloat)和一种计算数据类型(16 位 BrainFloat)。存储数据类型在进行前向和后向传递时被反量化为计算数据类型。权重仅在需要时才被解压,因此在训练和推理过程中内存使用保持低位。更多详细信息可以在这里找到。[4]
那么 QLoRA 微调的准确性影响如何?
QLoRA 调优在各种实验中显示出与 16 位微调方法相匹配的效果。此外,使用 QLoRA 微调 LLaMA 模型的 Guanaco 模型在OpenAssistant 数据集(OASST1)上是最先进的聊天机器人系统,并且在 Vicuna 基准测试中接* ChatGPT。
HuggingFace 对微调的支持
HuggingFace 发布了几个可以轻松微调 LLM 的库。
这些包括:
-
PEFT 库:HuggingFace 发布了一个关于参数高效微调(PEFT)的库,该库支持 LORA。
-
量化支持——许多模型可以使用 bitsandbytes 模块以 8 位和 4 位精度加载。以 4 位加载模型的基本方法是调用 from_pretrained 方法时传递参数 load_in_4bit=True。
-
Accelerate 库——Accelerate 库具有许多功能,能够轻松减少模型的内存需求
-
监督微调训练器——SFT 训练器是大规模 LLM 的监督微调训练类。
现在我们结合所有技术在金融数据集上训练 Falcon 7B 模型。
在 Alpaca Finance 数据集上训练 Falcon 7B
我们成功地在 Google Colab 上微调了最*发布的 Falcon-7B 模型,使用Alpaca-Finance数据集。这个数据集包含了大约 70K 的金融数据点。这是一个开源数据,存储在 HuggingFace 数据集中心,并可以直接从中心加载。
数据是金融问题及其答案。数据快照如下:

Alpaca Finance 数据集。图片来源:作者
这个微调的代码可以在这里查看。
首先,我们从 HuggingFace 加载一个预训练的 Falcon-7B 模型。我们使用 AutoModelForCausalLM,通常用于加载像 GPT 这样的自回归语言模型。注意,我们将存储类型设置为 4 位,将计算类型设置为 FP-16。

加载 Falcon 7B 模型的代码片段。图片来源:作者
现在,我们将创建 Adapters(记住!额外的层添加到 Transformers 模块中,将保存我们的微调权重)。我们将根据建议,将它们添加到线性层以及 Transformers 模块的查询值对中,以获得最佳的微调模型精度。下面可以在 target_modules 参数中注意到这一点。

PEFT 配置。图片由作者提供
最后,我们将整合一切,同时初始化 SFTTrainer 类。我们还会提供我们的数据、适当的令牌、格式化函数和 max_seq_length。
模型训练大约需要一个小时。我使用了 WandB 进行日志记录。更多细节请查看 Colab Notebook 这里。
训练损失在 100 步后相当低。

训练损失 — 财务 Alpaca 数据集。图片由作者提供
使用 Alpaca Finance 数据集进行推理
Colab Notebook 还包含推理代码。我发现模型在回应问题时表现良好。对于我关于退休所需收入的问题,它有一个较长的生成。它涵盖了一些关于 401K 和其他养老金计划的经验法则。它强调了了解退休收入以给出答案的必要性,最后建议我咨询财务规划师!
响应不如 chatGPT 那样干净,但很高兴看到一个 7B 模型可以相对容易地训练和运行。
### Instruction:
how do I figure out how much money I need to retire?
### Response:
There are a lot of assumptions embedded in any formula, so it's hard to say for
sure what you'll need per month, but some ideas to get started would be:
around 80k per year if you have a very high net worth or 180k per year if you
have a very low net worth. It sounds like your income is well above this number
, especially if you have a lot of assets. To really know though, you'll need
to do a full financial analysis. There are several factors that aren't
included in this equation, the most important being what kind of retirement
plan you have. If you have a 401k, the rule of thumb is ~$60k per year will
cover a typical retirement. If you have a pension, the rule of thumb is ~$10k
per year will cover a pension. These are rough guidelines, and there are lots
of other factors that go into it, such as whether you have another source of
income, what kind of investments you have, etc. A financial planner should
be able to help you with this. I will say that $150k per year is pretty high
even if you have a lot of assets; the average American family makes less than
$45k per year. If you don't have a retirement plan or your plan just isn't
enough to support you in retirement, then yes, you should consider
supplementing your income. However, before you make that decision,
you need to be honest with yourself and ask if you live beyond the basic necessities.
If you have the same quality of life that you had when you were making
half of what you are now, then yeah, you might want to look at
我尝试的另一个问题是关于投资组合多样化的。再次,来自一个小模型的响应质量让我感到惊讶!
### Instruction:
Why should portfolios be diversified?
### Response:
Because the performance of different assets is not identical. Even if they are
correctly classified as "equities" or "bonds", different stocks will behave
differently under different economic conditions. And even if you have the
same general view of the market (eg, it's overvalued), you will do better
if you own a broad basket of stocks rather than one that is too focused on
either the rising or falling side. Also, different markets interact with each
other, and crises in one affect others. If everyone rushes into safe havens
in a panic, for instance, then those safe havens will rise and you'll lose
money. Conversely, if everyone thinks a bubble is about to burst, then you
want to be buying up the shares before everyone else catches on and they
start declining. A single individual or small group can be right that the
stock market is overvalued, but if everyone thinks that way, it's time to
sell - and the sale will be greater the earlier you join it. Also, some
investments inherently involve more risk than others. Gambling stocks, for
instance, would probably be a bad idea during an industrywide gambling slump.
And some forms of investment, such as short
结论
在自定义数据集上微调 LLMs 变得更加容易,这可以让人们使用自己的“私有 GPT”模型。这些自定义模型可以在商业 GPU 上本地托管,并具有类似 ChatGPT 的界面。
深度学习分析是一个值得信赖的定制机器学习模型提供商,适用于多种使用场景。我们拥有全球客户基础,与组织合作开发符合其独特需求的定制解决方案。如果您希望利用语言模型(LLMs)的力量进行应用,欢迎与我们联系。请通过 info@deeplearninganalytics.org 与我们的团队联系,探索更多可能性。我们期待进一步讨论您的项目。
参考文献
[1] huggingface.co/blog/falcon
[2] huggingface.co/blog/hf-bitsandbytes-integration
[5] www.promptengineering.org/exploring-qloras-potential-for-accessibility-and-innovation/
[6] blog.gopenai.com/paper-review-qlora-efficient-finetuning-of-quantized-llms-a3c857cd0cca
[7] QLoRA 论文 -: arxiv.org/abs/2305.14314
[8] LoRA 论文 -: arxiv.org/abs/2106.09685
[9] huggingface.co/docs/trl/main/en/sft_trainer
专业级 GPU 系统 vs 消费级 GPU 系统用于深度学习
原文:
towardsdatascience.com/pro-gpu-system-vs-consumer-gpu-system-for-deep-learning-a62bec69f557
硬件
为什么你可能会考虑使用专业级 GPU
·发表于 Towards Data Science ·阅读时间 21 分钟·2023 年 4 月 19 日
--

本文将使用的专业工作站。图片来源于 Exxact Corporation ,授权给 Michael Clayton
在训练神经网络,特别是深度神经网络时,系统中拥有一个 GPU(或显卡)几乎是必需的。与 CPU 相比,即使是相当普通的 GPU,其训练速度的差异也是天壤之别。
……但你什么时候可能会考虑跳入专业级而非消费级 GPU 的领域?训练和推理速度有很大的差别吗?还是说其他因素使得转向专业级 GPU 更具吸引力?
介绍
本文的目的是让你了解作为普通消费者(或刚开始从事机器学习/深度学习)的 GPU 与高端系统中使用的 GPU 之间的主要区别。这些系统可能用于开发和/或推理高级深度学习模型。
除了作为一个有趣的练习来理解前沿专业设备和消费级硬件在纯处理速度上的区别外,它还将突出一些消费级 GPU 及其相关系统在处理前沿深度学习模型时存在的其他限制。
你提到的“专业”或“消费级”GPU 指的是哪些 GPU?
“现实世界”的差异将在文章的其余部分中讨论,但如果你想要一个明确的技术区分,包括示例显卡和规格,那么这一部分应该能涵盖。
正如我在之前的文章中详细说明的那样,NVIDIA 是目前深度学习和神经网络 GPU 的唯一明智选择。这主要是由于它们与 TensorFlow 和 PyTorch 等*台的更全面集成。
因此,在制造商的规格方面区分专业级和消费级显卡是相对直接的。
从以下页面中了解当前的 NVIDIA 消费级显卡批次:
探索 NVIDIA GeForce 显卡。RTX 40 系列、RTX 30 系列、RTX 20 系列和 GTX 16 系列。
…以及专业级 GPU:
了解 3D 艺术家、建筑师和产品设计师如何利用 NVIDIA RTX ™ 和 Omniverse ™ 的强大功能…
还有一些 GPU,主要用于数据中心,超越了上述的专业级显卡。A100 是一个很好的例子:
NVIDIA EGX ™ *台包括优化的软件,提供基础设施上的加速计算…
你还可以了解 NVIDIA 认证的专业数据中心和工作站系统中使用的系统规格和 GPU:
[## NVIDIA 认证系统
NVIDIA 认证系统程序汇集了业界最完整的加速工作负载性能集合…
计划
我通常认为实际演示(或实验)是说明一个观点的最佳方式,而不仅仅依赖于制造商提供的规格和统计数据。
有鉴于此,虽然文章将讨论相关统计数据,但它还将直接比较三种不同的 GPU(专业级和消费级),在相同的深度学习模型上进行不同层次的比较。
这应该有助于突出在考虑是否需要专业级 GPU 时哪些因素重要,哪些因素不重要。
GPU 规格 — 概述

对于这项实验,将会有三种不同的显卡,但有四个比较级别:
-
NVIDIA RTX 1070(基础)
-
NVIDIA Tesla T4(中端)
-
NVIDIA RTX 6000 Ada(高端)
-
2 x NVIDIA RTX 6000 Ada(双高端!)
那么,这些不同的显卡在原始规格方面如何比较?

不同显卡的比较 — 表格由作者提供
注意: 我在上面的表格中包含了 RTX 4090,因为它是当前消费级显卡的巅峰,可能是与 RTX 6000 Ada 最直接的比较对象。我将在整篇文章中参考 4090 作为比较点,尽管它不会出现在基准测试中。
如果上面的表格只是一些没有意义的数字,那么我推荐我的上一篇文章,它介绍了一些术语:
加快训练速度,迭代更快
towardsdatascience.com
专业系统

从各个角度展示了本文使用的专业工作站。图片由 Exxact Corporation 提供,授权给 Michael Clayton
制作这样的文章的一个问题是,你需要访问专业级系统,因此主要障碍之一就是……成本。
幸运的是,有些公司会提供设备试用机会,以便你可以查看它是否满足你的需求。在这个特定的案例中,Exxact 足够友好地允许远程访问他们的一台设备,以便我可以进行所需的比较。
…工作站的价值大约为 25,000 美元
为了强调这些系统可能的成本,我估计我所获得访问权限的工作站价值大约为25,000 美元。如果你想更深刻(或者更惊讶?)了解这些内容,可以查看 配置器,看看实际能达到什么水*。
顺便提一下,如果您正在认真考虑这一水*的硬件,您也可以申请远程“试用”:
[## NVIDIA H100 试用 | Exxact
使用最新 NVIDIA 技术试用您的应用程序,注册以远程访问我们设备齐全的 GPU 服务器……
对于感兴趣的人,这些是“专业”系统的完整规格:

专业系统的规格 — 作者提供的表格
注意: 您可以参考本文中包含两张金色 GPU 的黑色计算机机箱的任何图片,因为这些是上述系统的实际照片。
有趣的是,拥有高端系统不仅仅是将您能找到的最佳显卡装入当前系统中。其他组件也需要升级。系统 RAM、主板、CPU、冷却系统以及电源,当然还有电力。
竞争者

本文中将用作基准的 NVIDIA GeForce GTX 1070 FTW — 作者提供的图片
NVIDIA GeForce GTX 1070
在底部的是 GTX 1070,这对于大多数人来说很容易获得,但仍然显著 比 CPU 更快。它还具有 8GB 的相当数量的 GPU RAM。一个不错的简单消费级基准。
NVIDIA Tesla T4
Tesla T4 可能是一个奇怪的补充,但有几个原因。
首先要注意的是,Tesla T4 实际上是一款专业显卡,只是几代之前的产品。
从处理速度来看,它大致相当于 RTX 2070,但其 GPU RAM 是 16GB 的双倍。这额外的 RAM 使其在此测试中处于中端范围。目前一代的消费级显卡通常有这种范围的 RAM(RTX 4070 [12GB] 和 RTX 4080 [16GB]),因此它在 GPU RAM 方面代表了消费级显卡。
最终原因是您可以在 Colab 免费访问这些显卡。这意味着任何阅读本文的人都可以亲自动手运行代码以查看效果!
专业 GPU — NVIDIA RTX 6000 Ada

这张图片展示了在本文中使用的专业工作站中安装的两张 NVIDIA RTX 6000 Ada 显卡。图片来源于 Exxact Corporation ,由 Michael Clayton 授权
毋庸置疑,RTX 6000 Ada 无论在规格还是价格上都是一款令人印象深刻的显卡。MSRP 为 6800 美元,绝对不是便宜的显卡。那么,如果你可以用仅仅 1599 美元(MSRP)购买 RTX 4090,为什么还要买一张(或者更多!?)RTX 6000 Ada 呢?
RTX 4090 的 RAM 只有 RTX 6000 Ada 的一半,且功耗比 RTX 6000 Ada 高出 50%
我在桌子上放了一张 RTX 4090 来尝试回答这个问题。这有助于展示消费者显卡和专业显卡之间最明显的两个区别(至少从规格上来看):
-
可用的 GPU RAM 数量
-
使用中的最大功耗
RTX 4090 的 RAM 只有一半,且 功耗高出 50% 比 RTX 6000 Ada。这绝非偶然,随着文章的深入将会显现出原因。
此外,考虑到 RTX 4090 更高的功耗,值得注意的是 RTX 6000 Ada 的速度仍然快了大约10%。
这些额外的 RAM 和降低的功耗真的有区别吗?希望比较能在文章后面帮助回答这个问题。
还有其他不太明显的优点吗?
是的,获得专业级显卡确实有一些额外的好处。
可靠性

图片来源于 WikiImages,来自 Pixabay
NVIDIA RTX 专业显卡经过广泛的专业应用认证,经过领先的独立软件供应商(ISVs)和工作站制造商的测试,并由全球支持专家团队提供支持。
从本质上讲,这意味着显卡在软件(驱动程序)和硬件层面上可能更可靠、更抗崩溃,如果遇到问题,还有一个广泛的专业网络可以解决问题。这些因素在企业应用中显然非常重要,因为时间就是金钱。
想象一下运行一个复杂的深度学习模型几天,然后由于崩溃或错误丢失结果。接着还需要花费更多的时间来解决问题。真是不妙!
这种安心感是否成为额外的理由来支付更多的费用?这真的取决于你的优先级和规模...
规模

照片由 Daniele Levis Pelusi 在 Unsplash 上提供
如果你在设计一个具有最佳 GPU 处理能力的计算机系统,那么可能需要多个 GPU。显然,系统中可容纳的 GPU 数量主要取决于主板插槽的数量以及机箱内的物理空间限制。
然而,还有其他直接与 GPU 相关的限制因素,这也是消费级 GPU 和专业 GPU 在设计上开始偏离的地方。
考虑到专业主板可能有四个双槽 GPU 的插槽(如本文中的专业系统)。理论上,你可以将 4 x RTX 6000 Ada GPU 装入系统中没有任何问题。然而,你只能在同一主板上装入 2 x RTX 4090。为什么?因为 4090 是三槽显卡(约 61mm 厚),而 6000 是双槽显卡(约 40mm 厚)。
消费级 GPU 的设计并没有考虑到相同的限制(即高密度构建),因此随着规模的扩大,它们的实用性开始下降。
冷却
接着讨论可能的尺寸问题……即使消费级显卡采用相同的双槽设计,还有其他问题。
专业级 GPU 通常配备有冷却系统(吹风机类型),这些系统设计用于从前到后通过显卡抽取空气,并且有封闭的罩子将空气直接排出机箱(即没有热空气在机箱内部循环)。这允许专业 GPU 紧密堆叠在机箱内,同时仍能高效地进行自我冷却,对机箱内其他组件或 GPU 的影响最小。

两张显卡,一张采用‘吹风机’冷却系统,另一张采用更常见的风扇冷却系统。图片由Nana Dua拍摄,发布在Pexels上。注释由作者提供。
消费级 GPU 通常使用从上/下方的风扇冷却。这不可避免地意味着 GPU 的热空气在机箱内会有一定程度的循环,因此需要优秀的机箱通风。
然而,在多个 GPU 的机箱中,其他显卡的紧密接*会使风扇冷却变得非常无效,并且不可避免地导致 GPU 和其他接*组件的温度不理想。
总的来说,专业显卡设计为紧凑高效地打包到系统中,同时保持冷却和自我封闭。
准确性

图片由Ricardo Arce拍摄,发布在Unsplash上。
这实际上与深度学习并不特别相关,但专业级 GPU 通常具有 ECC(错误更正码)RAM。在需要高精度(即,位翻转的潜在随机错误低)的处理过程中,这将非常有用。
然而,深度学习模型有时会调优为较少的数值精度(半精度 8 位计算),因此这对正在运行的计算可能并不会造成实际问题。
尽管如果这些随机位翻转导致你的模型崩溃,那么这也可能值得考虑。
深度学习模型

图片来源:Pixabay
对于深度学习模型,我希望它既先进又领先行业,并对 GPU 具有较高要求。同时,它也必须在难度上具有可扩展性,因为测试中的 GPU 具有广泛的能力范围。
一个专业级模型,适用于专业级显卡
要使模型符合行业标准,就排除了从头开始构建模型的可能性,因此在此比较中,将利用迁移学习使用现有的、经过验证的模型。
大数据
为确保输入数据的重量,分析将基于图像,特别是图像分类。
可扩展性
最终标准是可扩展性,有一组特定的模型完全符合这一标准……
EfficientNet
EfficientNet 由一系列图像分类模型(B0 到 B7)组成。每个模型都变得越来越复杂(也更准确)。随着你在模型系列中的进展,它也有不同的预期输入形状,从而增加了数据输入大小。

不同 EfficientNet 模型的比较——数据来源于EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks——表格由作者提供
这有两个方面的效果:
-
随着你在不同的 EfficientNet 模型中进展,模型参数会增加(即,对 GPU 处理要求更复杂、更高的模型)。
-
需要处理的原始数据量也会增加(从 224x224 像素到 600x600 像素不等)。
最终,这为加载 GPU 提供了广泛的可能性,包括处理速度和 GPU RAM 需求。
数据
本文中使用的数据集是一组图像,描绘了游戏石头剪子布中手势的三种可能组合。数据¹。

来自数据集的四个示例,分为三种不同的类别。合成图像由作者提供。
每张图片为 PNG 格式,尺寸为 300(宽)像素 x 200(高)像素,全彩色。
原始数据集总共包含 2188 张图片,但为了这篇文章使用了一个较小的选择,共包含 2136 张图片(每个类别 712 张)。从原始数据集中略微减少总图片数,是为了*衡类别。
本文使用的*衡数据集可以在这里找到:
## notebooks/datasets/rock_paper_scissors at main · thetestspecimen/notebooks
这些数据集是原始“石头剪子布”数据集的一个选择,详细信息请见参考部分…
测试

如前所述,EfficientNet 提供了各种不同的级别,因此为了测试,以下将在每个 GPU 上运行:
-
EfficientNet B0(简单)
-
EfficientNet B3(中等)
-
EfficientNet B7(高强度)
这将测试显卡的速度能力,由于每个模型的总体参数不同,也会测试内存需求的范围,因为输入图像大小也会有所不同。
EfficientNet 模型将解锁所有层,并允许进行学习。
三个最终模型:
EfficientNetB0
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_layer (InputLayer) [(None, 224, 224, 3)] 0
data_augmentation (Sequenti (None, 224, 224, 3) 0
al)
efficientnetb0 (Functional) (None, None, None, 1280) 4049571
global_avg_pool_layer (Glob (None, 1280) 0
alAveragePooling2D)
output_layer (Dense) (None, 3) 3843
=================================================================
Total params: 4,053,414
Trainable params: 4,011,391
Non-trainable params: 42,023
_________________________________________________________________
EfficientNetB3
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_layer (InputLayer) [(None, 300, 300, 3)] 0
data_augmentation (Sequenti (None, 300, 300, 3) 0
al)
efficientnetb3 (Functional) (None, None, None, 1536) 10783535
global_avg_pool_layer (Glob (None, 1536) 0
alAveragePooling2D)
output_layer (Dense) (None, 3) 4611
=================================================================
Total params: 10,788,146
Trainable params: 10,700,843
Non-trainable params: 87,303
_________________________________________________________________
EfficientNetB7
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_layer (InputLayer) [(None, 600, 600, 3)] 0
data_augmentation (Sequenti (None, 600, 600, 3) 0
al)
efficientnetb7 (Functional) (None, None, None, 2560) 64097687
global_avg_pool_layer (Glob (None, 2560) 0
alAveragePooling2D)
output_layer (Dense) (None, 3) 7683
=================================================================
Total params: 64,105,370
Trainable params: 63,794,643
Non-trainable params: 310,727
_________________________________________________________________
速度测试
GPU 的速度将通过其完成一个周期的速度来评估。
更具体地说,每个显卡上将运行至少两个周期,第二个周期将用于判断处理速度。第一个周期通常会有额外的加载时间,因此不适合作为一般执行时间的参考。
第一个周期的运行时间仅供参考。
GPU 内存测试
为了测试 GPU 内存的极限,每个显卡的批量大小和每个 EfficientNet 模型(即 B0、B3 或 B7)已调整到尽可能接*该显卡的极限(即尽可能填满 GPU 内存)。
实际的峰值 GPU 内存使用情况也会披露,以便进行比较。
代码

一如既往,我将所有的 Python 脚本(GTX 1070 和 RTX 6000 Ada)以及笔记本(Tesla T4)都提供在 GitHub 上:
[## notebooks/pro-vs-consumer-graphics-card at main · thetestspecimen/notebooks
你现在无法执行该操作。你在另一个标签或窗口中登录了。你在另一个标签中登出了…
如果你愿意,也可以直接在 Colab 上访问 Tesla T4 的笔记本:
EfficientNetB0:
EfficientNetB3:
EfficientNetB7:
结果

照片由 Pixabay
EfficientNet B0

EfficientNet B3

EfficientNet B7

注意: 在第一轮训练中,我列出了括号中的秒数。这是第一轮和第二轮训练之间的时间差。
讨论 — 执行速度

图片由 Arek Socha 提供,来自 Pixabay
第一个要查看的项是执行速度。
对于这个特定的数据集,EfficientNet B0 对任何显卡都没有造成太大的挑战,所有显卡都在几秒钟内完成了一轮训练。
然而,需要记住的是,本文章使用的数据集很小,实际上,两块 RTX 6000 Ada 显卡在执行速度上大约比 GTX 1070(和 Tesla T4)快17 倍。对于 EfficientNet B3(快 8 倍)和 B7(快 11 倍),情况也基本相同。
区别在于,当将其视为执行时间时,这种速度的减慢会随着模型的复杂性增加而变得更具障碍。
例如,在这个非常小的数据集上,使用 GTX 1070 执行一个周期大约需要 15 分钟。与一对 RTX 6000 Ada 的 1 分钟多一点相比。
…情况更糟。
扩展规模
让我们现实一点。没有模型会在一个周期内收敛。对于像 EfficientNet 这样的模型,四百可能是一个更合理的数字。
这将是使用像 GTX 1070 这样的 GPU 需要4 天的差距,而在双 RTX 6000 Ada 设置下仅需几个小时(准确地说是 6.5 小时)。然后考虑到实际数据集不只有 2188 张图片,它可能有数百万张(作为参考,ImageNet有刚刚超过1400 万张图片)。
行业进展
另一个需要记住的是行业进展。EfficientNet 已经有几年历史了,情况已经发生了变化。
举个小例子,NoisyStudent在标准 EfficientNets 的基础上增加了一种叫做 EfficientNet-L2 的变体,并表示:
由于模型大小较大,EfficientNet-L2 的训练时间大约是 EfficientNet-B7 的五倍。
…所以如果你需要保持在前沿,速度确实很重要。
那么这对专业图形卡与消费者图形卡意味着什么呢?
事实是,如果你仅仅看执行速度,专业和消费者 GPU 之间几乎没有区别,如果你对比相同条件下的产品。RTX 4090 的速度几乎和 RTX 6000 Ada 一样。
RTX 4090 的速度几乎和 RTX 6000 Ada 一样。
目前为止,这个小实验仅仅说明了速度非常重要,因为行业标准模型的复杂性发展很快。老一代图形卡已经明显较慢。要跟上,就需要至少保持在硬件的前沿。
…规模在回答这个问题时确实很重要。
…但随着进展速度的加快(只需看看 GTP-3 和 GTP-4 的迅速发展),如果你想保持在前沿,即使是 RTX 4090 或 RTX 6000 Ada 级别的单个 GPU 也可能不够。如果是这样的话,专业级图形卡在构建系统时优越的散热、较低的功耗和更紧凑的尺寸就是一个显著的优势。
本质上,规模在回答这个问题时非常重要。
然而,速度只是一个方面。现在让我们转到 GPU RAM,这里情况会有些更有趣…
讨论 — GPU RAM
GPU RAM 在某些情况下是一个重要的考虑因素,甚至可能是是否可以使用某些模型或数据集的实际限制因素。
让我们看看一对 RTX 6000 Ada 的全面表现:

两张 RTX 6000 Ada GPU 正在运行深度学习模型。图片由作者提供
你可能会注意到上面的图片中两个 GPU 的 GPU RAM 都达到了 100%。然而,这并不是真实的使用情况:
默认情况下,TensorFlow 会映射几乎所有 GPU 的 GPU 内存(受
[*CUDA_VISIBLE_DEVICES*](https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#env-vars))到进程中。这是为了通过减少内存碎片来更有效地利用设备上相对宝贵的 GPU 内存资源。
限制
绝对的限制被 GTX 1070(它有 8GB GPU RAM)所凸显,它只能以批量大小为 1 运行 EfficientNet B7(即它每次只能处理 1 张图片,然后更新模型参数并将下一张图片加载到 GPU RAM 中)。
这会引发两个问题:
-
由于频繁的参数更新和更频繁地将新数据加载到 GPU RAM 中(即更大的批量大小本质上更快),你会失去执行速度。
-
如果输入图像尺寸再大一点,模型将无法运行,因为它无法将单张图像放入 GPU RAM 中。
即使是 Tesla T4,它拥有不算差的 16GB GPU 内存,在 EfficientNet B7 上也只能处理批量大小为 2 的任务。
如前所述,16GB 的 GPU RAM 是大多数当前一代消费级 GPU 的良好代表,只有 RTX 4090 拥有 24GB。因此,如果你处理的是内存密集型原始数据,这对消费级 GPU 来说是一个相当显著的缺点。
此时,为什么所有专业 GPU 相比于消费级 GPU 拥有如此大量的 RAM 突然变得清晰。正如执行速度讨论中提到的,EfficientNet 已经不再处于前沿,因此今天的现实可能比这篇文章中的测试所述还要苛刻。
系统密度
关于 GPU RAM 的另一个考虑因素是系统密度。
例如,我可以访问的系统有一个可以容纳 4 张双高显卡的主板(我也见过最多可安装 8 张显卡的系统)。这意味着如果你的系统对 GPU RAM 有优先需求,那么专业 GPU 就是不二选择:
4 x RTX 6000 Ada = 192GB GPU RAM 和 1200W 功耗
4 x RTX 4090 = 96GB GPU RAM 和 1800W 功耗
(……正如我在文章前面提到的,RTX 4090 是一款三槽 GPU,所以这甚至不切实际。实际上,只有两张 RTX 4090 显卡才能实际适配,但为了方便比较,我们假设它是可行的。)
这不是一个小差异。要匹配 RTX 6000 Ada 系统的 GPU RAM,你将需要两个分别消耗至少 三倍功率 的系统。
要匹配 RTX 6000 Ada 系统的 RAM,你将需要两个分别消耗至少三倍功率的系统。
别忘了,因为你需要两个独立的系统,你还需要额外支付 CPU、电源、主板、冷却、机箱等费用。
关于系统 RAM 的一个附注…

你是否注意到在专业系统中,CPU 上下各有 8 根 64GB 的系统 RAM?图片来自 Exxact Corporation ,由 Michael Clayton 授权使用
还值得指出的是,重要的并不仅仅是 GPU RAM。随着 GPU RAM 的增加,你需要同步增加系统 RAM。
你可能会注意到,在 Tesla T4 的 Jupyter notebooks 中,我已注释掉了以下优化:
train_data = train_data.cache().prefetch(buffer_size=tf.data.AUTOTUNE)
val_data = val_data.cache().prefetch(buffer_size=tf.data.AUTOTUNE)
这是因为,对于 EfficientNet B7,如果启用这些设置,训练将崩溃。
为什么?
因为“.cache()”优化将数据保留在系统内存中,以便有效地传递给 GPU,而 Colab 实例只有 12GB 的系统内存。这还不够,即使 GPU RAM 峰值为 9.9GB:
这个 [.cache()] 会将一些操作(如文件打开和数据读取)从每个训练周期中省略。
然而,专业系统有 8 根 64GB 的系统 RAM,总计 512GB 的系统 RAM。所以即使两张 RTX 6000 Ada GPU 合计有 96GB 的 GPU RAM,系统 RAM 仍有足够的余量来处理大量缓存。
结论

图片来自 Exxact Corporation ,由 Michael Clayton 授权使用
那么,专业级显卡在深度学习中是否优于消费级显卡?
如果钱不是问题,那么是的,它们更好。
这是否意味着你应该放弃考虑用于深度学习的消费级显卡?
不,并不是。
这完全取决于具体的需求,通常情况下,更是规模。
大型数据集
如果你知道你的工作负载将是内存密集型的(例如大型语言模型、图像或视频分析),那么同一代和处理速度的专业显卡通常拥有大约两倍的 GPU RAM。
这完全取决于具体的需求和规模。
这是一个显著的优势,特别是考虑到与消费级显卡相比,实现这一点不需要额外增加能源需求。
较小的数据集
如果你的 RAM 需求不高,那么问题就更复杂了,涉及到可靠性、兼容性、支持、能源消耗,以及额外 10% 的速度是否值得那显著的价格上涨。
规模
如果你打算投资重大基础设施,那么可靠性、能源消耗和系统密度可能从低优先级变为相当重要的考虑因素。这些都是专业 GPU 擅长的领域。
相反,如果你需要一个较小的系统,并且高 GPU 内存需求并不重要,那么考虑消费者级别的显卡可能会有利。与大规模相关的因素,如可靠性和能源消耗将变得不那么重要,系统密度也不会成为问题。
最终结论
总的来说,这是一个*衡的过程,但如果我必须选择两个因素来总结选择消费者级 GPU 与专业级 GPU 之间最重要的因素,那就是:
-
GPU 内存
-
系统规模
如果你有较高的 GPU 内存需求,或需要配备多个 GPU 的大型系统,那么你需要一个专业级的 GPU/多个 GPU。
否则,大多数情况下,消费者级别可能会是一个更好的选择。
如果你觉得这篇文章有趣或有用,记得关注我,或订阅我的通讯以获取更多类似的内容。
如果你还没有,可以考虑订阅 Medium。你的会员费不仅直接支持我,还支持你阅读的其他作者。你还将获得 Medium 上每一篇文章的完全无限制访问权限。
使用我的推荐链接注册将为我带来少量佣金,但不会影响你的会员资格,所以如果你选择这样做,谢谢你。
[## 通过我的推荐链接加入 Medium - Mike Clayton
阅读 Mike Clayton 的每一篇文章(以及 Medium 上的其他成千上万的作者)。你的会员费直接支持…
medium.com](https://medium.com/@maclayton/membership?source=post_page-----a62bec69f557--------------------------------)
参考文献
[1] Julien de la Bruère-Terreault, 石头剪子布图像 (2018), Kaggle, 许可协议:CC BY-SA 4.0
使用回归进行二元事件的概率预测
原文:
towardsdatascience.com/probabilistic-forecasting-of-binary-events-using-regression-4f8a8022ec37
使用累积分布函数预测极端值的概率
·发布于Towards Data Science ·阅读时间 5 分钟·2023 年 3 月 8 日
--

图片由Silas Baisch提供,Unsplash上的照片
在本文中,我们将探讨时间序列中二元事件的概率预测。目标是预测时间序列超过临界阈值的概率。
你将学习如何(以及为什么)使用回归模型来计算二元概率。
介绍
首先,为什么你会使用回归来计算二元概率而不是分类器?
二元事件的概率预测通常被视为分类问题。但回归方法可能更为合适,原因有两个:
-
对点预测和事件概率的兴趣;
-
超过阈值的变动。
对点预测和事件概率的兴趣
有时你可能想要预测未来观测值的数值以及相关事件的概率。
例如,在预测海洋波高的情况下。海洋波是一个有前景的清洁能源来源。短期点预测对于估算从这一来源可以生产多少能源非常重要。
但是,大浪可能会损坏波能转换器——这些设备将波能转换为电力。因此,预测波高超过临界阈值的概率也很重要。
因此,在预测海洋波高的情况下,最好使用一个模型来计算两种类型的预测。
超过阈值的变动
时间序列中的二元事件通常由超越——当时间序列超过预定义阈值来定义。
在某些情况下,最合适的阈值可能会根据不同的因素或风险情况而改变。因此,用户可能会对估计不同阈值的超越概率感兴趣。
分类模型在训练过程中固定了阈值,在推断过程中不能更改。而回归模型则独立于阈值构建。因此,在推断过程中,你可以同时计算多个阈值的事件概率。
如何从预测中计算二元概率

照片由Jamie O’Sullivan拍摄,来源于Unsplash
那么,你如何使用回归模型来估计二元事件的概率呢?
让我们继续讨论关于预测海洋波浪高度的示例。
数据集
我们将使用从爱尔兰海岸上的一个智能浮标收集的时间序列[1]。
import pandas as pd
START_DATE = '2022-01-01'
URL = f'https://erddap.marine.ie/erddap/tabledap/IWaveBNetwork.csv?time%2CSignificantWaveHeight&time%3E={START_DATE}T00%3A00%3A00Z&station_id=%22AMETS%20Berth%20B%20Wave%20Buoy%22'
# reading data directly from erdap
data = pd.read_csv(URL, skiprows=[1], parse_dates=['time'])
# setting time to index and getting the target series
series = data.set_index('time')['SignificantWaveHeight']
# transforming data to hourly and from centimeters to meters
series_hourly = series.resample('H').mean() / 100

海洋波浪*均高度的每小时时间序列。水*虚线表示一个关键安全阈值。图像由作者提供。
超越概率预测
我们的目标是预测一个大波的概率,我们将其定义为超过 6 米的波浪。这个问题是超越概率预测的一个特定实例。
在上一篇文章中,我们探讨了超越概率预测背后的主要挑战。通常,这个问题通过两种方法之一来解决:
-
一个概率二元分类器;
-
一个预测集成。概率是根据预测超过阈值的模型比例计算的。
在这里,你将了解第三种方法。这种方法基于一个预测模型,但不一定是一个集成模型。像 ARIMA 这样的模型也可以。
使用累积分布函数
假设预测模型做出了一个“y”的预测。然后,假设这个预测遵循均值为“y”的正态分布。当然,分布的选择取决于输入数据。在这里,我们为了简化,选择正态分布。在*稳性条件下,标准差(“s”)可以使用训练数据来估计。
在我们的示例中,“y”是模型预测的波浪高度。“s”是训练数据中波浪高度的标准差。
我们通过累积分布函数(CDF)得到二元概率预测。
什么是累积分布函数(CDF)?
当在值 x 上进行评估时,CDF 表示随机变量取小于或等于 x 的值的概率。我们可以取补充概率(1 减去该概率)来获得随机变量超越 x 的概率。

标准正态分布的累积分布函数。图片由作者提供。
在我们的案例中,x 是表示超越的感兴趣的阈值。
这里是如何使用 Python 进行这项操作的一个片段:
import numpy as np
from scipy.stats import norm
# a random series from the uniform dist.
z = np.random.standard_normal(1000)
# estimating the standard dev.
s = z.std()
# fixing the exceedance threshold
# this is a domain dependent parameter
threshold = 1
# prediction for a given instant
yhat = 0.8
# probability that the actual value exceeds threshold
exceedance_prob = 1 - norm.cdf(threshold, loc=yhat, scale=s)
预测大波浪
让我们看看如何使用 CDF 来估计大波浪的概率。
首先,我们使用自回归构建预测模型。
# using past 24 lags to forecast the next value
N_LAGS, HORIZON = 24, 1
# the threshold for large waves is 6 meters
THRESHOLD = 6
# train test split
train, test = train_test_split(series_hourly, test_size=0.2, shuffle=False)
# transforming the time series into a tabular format
X_train, Y_train = time_delay_embedding(train, n_lags=N_LAGS, horizon=HORIZON, return_Xy=True)
X_test, Y_test = time_delay_embedding(test, n_lags=N_LAGS, horizon=HORIZON, return_Xy=True)
# training a random forest
regression = RandomForestRegressor()
regression.fit(X_train, Y_train)
# getting point forecasts
point_forecasts = regression.predict(X_test)
然后,我们可以使用 CDF 将点预测转化为超越概率。
import numpy as np
from scipy.stats import norm
std = Y_train.std()
exceedance_prob = np.asarray([1 - norm.cdf(THRESHOLD, loc=x_, scale=std)
for x_ in point_forecasts])
该模型能够有效地检测到大波浪的发生:

在最*的一篇论文中,我将这种方法与分类器和集成方法进行了比较。基于 CDF 的方法带来了更好的预测。你可以在参考文献[2]中查看详细信息。实验的代码也可以在Github上找到。
关键要点
-
从预测模型中获取二值概率是理想的,尤其是在点预测也有用或超越阈值发生变化的问题中;
-
你可以使用预测模型通过 CDF 获得二值超越概率;
-
使用 CDF 是一种与使用分类器或回归集成相竞争的替代方案。
感谢阅读,下次故事见!
相关文章
参考文献
[1] 爱尔兰波浪浮标 (许可证:知识共享署名 4.0)
[2] Vitor Cerqueira 和 Luis Torgo。“通过回归预测显著波高的超越概率。” arXiv 预印本 arXiv:2206.09821 (2022)。
使用 TensorFlow 进行概率逻辑回归
原文:
towardsdatascience.com/probabilistic-logistic-regression-with-tensorflow-73e18f0ddc48
概率深度学习
·发表于 数据科学前沿 ·阅读时间 9 分钟·2023 年 1 月 25 日
--
介绍
本文属于“概率深度学习”系列。该系列每周介绍深度学习中的概率方法。主要目标是扩展深度学习模型以量化不确定性,即了解它们不知道的内容。
在这篇文章中,我们将介绍概率逻辑回归的概念,这是一种强大的技术,允许在预测过程中纳入不确定性。我们将探讨这种方法如何在数据噪声大或模型过拟合的情况下提供更稳健和准确的预测。此外,通过在模型参数上引入先验分布,我们可以对模型进行正则化,防止过拟合。这种方法是进入贝叶斯深度学习激动人心的世界的绝佳起点。
已发布的文章:
-
从零开始的最大似然估计在 TensorFlow Probability 中
-
从零开始的 TensorFlow 概率线性回归
-
确定性与概率深度学习
-
从头开始使用 TensorFlow 实现朴素贝叶斯
-
使用 TensorFlow 进行概率逻辑回归

图 1:今天的座右铭:直线可以分隔更多的事物,比我们想象的要多 (source)
与往常一样,代码可以在我的 GitHub 上找到。
初步工作
在我们系列文章的前一篇中,我们从头开始构建了朴素贝叶斯算法,并使用它根据选择的特征对葡萄酒样本进行分类。这一次,我们将使用概率逻辑回归方法。由于我们已经采用了端到端的方法,我将跳过大部分探索性数据分析部分和类先验分布定义。
唯一需要注意的是我们为此模型选择的特征有所不同。

图 2:根据酒精和色调的目标样本分布。
我们将使用色调和类黄酮作为自变量。注意这些特征在分隔目标变量方面比酒精和色调更有效。

图 3:根据类黄酮和色调的目标样本分布。
概率逻辑回归
逻辑回归是一种广泛使用的二元分类统计方法,用于建模二元响应变量与一个或多个预测变量之间的关系。逻辑回归可以用来建模二元结果的概率作为预测变量的函数。传统的逻辑回归模型是一个确定性模型,假设预测变量与响应变量之间的关系是固定且已知的。然而,在许多实际应用中,预测变量与响应变量之间的真实关系是不确定的,因此使用概率方法更为合适。
概率逻辑回归模型通过概率框架建模预测变量与二元响应变量之间的关系,并能够考虑数据和模型参数的不确定性。这是通过对模型参数施加概率分布来实现的,而不是假设固定值。通过这种方式,与传统逻辑回归模型相比,概率逻辑回归模型可以提供更准确的预测和更好的不确定性量化。
最流行的逻辑回归概率模型之一是贝叶斯逻辑回归模型。这些模型基于贝叶斯定理,该定理指出,给定数据的模型参数的后验概率与数据给定参数的似然和参数的先验概率的乘积成正比。通常,贝叶斯逻辑回归模型使用共轭先验分布来对模型参数进行建模,这允许对后验分布进行封闭形式的解法。这使得计算响应变量给定预测变量的概率成为可能,这被称为后验预测分布。
似然
在本节中,我们介绍了一种在逻辑回归的概率方法中计算类别条件密度的方法。我们的方法基于均值的最大似然估计,其由以下公式给出

其中 𝑋(𝑛)𝑖 是第 n 个样本的第 i 个特征,𝑌(𝑛) 是第 n 个样本的目标标签,𝑘 是类别标签,𝛿(𝑌(𝑛)=𝑦𝑘) 是一个指示函数,如果 𝑌(𝑛)=𝑦𝑘 则等于 1,否则为 0。
为了估计标准差 𝜎𝑖,我们将通过从数据中学习这些参数来实现,而不是使用封闭形式的解法。我们通过实现自定义训练循环来实现这一点,该循环通过最小化数据的*均每个示例负对数似然来优化标准差的值。
我们的函数根据上述方程计算类别条件高斯分布的均值 𝜇𝑖𝑘。然后,它使用 MultivariateNormalDiag 创建一个多变量高斯分布对象,将均值设置为 𝜇𝑖𝑘,将尺度设置为 TensorFlow 变量。
该函数运行一个自定义训练循环,循环次数为指定的纪元,其中计算*均每个示例的负对数似然。接下来,梯度被传播,尺度变量相应地更新。在每次迭代中,尺度变量的值和损失都会被保存。
它返回一个包含三个对象的元组:损失值、每次迭代的尺度变量和最终学习到的批量 MultivariateNormalDiag 分布对象。
def train(x, y, scales, optimiser, epochs):
estimated_scales = []
n_classes = np.unique(y).shape[0]
n_features = x.shape[1]
counts = np.zeros(n_classes)
mean_cond_class = []
std_feature_given_class = []
for c_k in range(n_classes):
mean_cond_class.append(np.mean(x[np.squeeze(y==c_k)], axis=0))
mean_cond_class = np.asarray(mean_cond_class, dtype=np.float32)
x_c = np.concatenate((x,y.reshape(-1,1)), axis=1)
mv_normal_diag = tfd.MultivariateNormalDiag(loc=mean_cond_class,scale_diag=scales)
x = np.expand_dims(x , 1).astype('float32')
for i in range(epochs):
with tf.GradientTape() as tape:
tape.watch(mv_normal_diag.trainable_variables)
predictions = - mv_normal_diag.log_prob(x)
p1 = tf.reduce_sum(predictions[np.squeeze(y==0)][:,0])
p2 = tf.reduce_sum(predictions[np.squeeze(y==1)][:,1])
loss = p1 + p2
grads = tape.gradient(loss, mv_normal_diag.trainable_variables)
opt.apply_gradients(zip(grads, mv_normal_diag.trainable_variables))
estimated_scales.append(mv_normal_diag.trainable_variables[0].numpy())
print('Step {:03d}: Loss: {:.3f}: Scale1: {:.3f}: Scale2: {:.3f}'.format(i, loss, mv_normal_diag.trainable_variables[0].numpy()[0], mv_normal_diag.trainable_variables[0].numpy()[1]))
estimated_scales = np.asarray(estimated_scales)
return estimated_scales, mv_normal_diag
让我们创建要训练的变量。
scales = tf.Variable([1., 1.], name='scales')
opt = tf.keras.optimizers.Adam(learning_rate=0.01)
epochs = 100
我们现在准备开始训练过程。
scales_arr, class_conditionals_binary = train(x_train, y_train, scales, opt, epochs)
-----
Step 000: Loss: 290.708: Scale1: 0.990: Scale2: 0.990
Step 001: Loss: 288.457: Scale1: 0.980: Scale2: 0.980
Step 002: Loss: 286.196: Scale1: 0.970: Scale2: 0.970
Step 003: Loss: 283.924: Scale1: 0.960: Scale2: 0.960
Step 004: Loss: 281.641: Scale1: 0.950: Scale2: 0.950
Step 005: Loss: 279.348: Scale1: 0.940: Scale2: 0.940
[...]
最后,我们可以检查模型如何区分我们的葡萄酒类别。

图 4:类别条件密度轮廓。
使用我们在前一篇文章中定义的函数,我们可以为测试集生成预测。在上面的图中,我们可以看到类别被很好地分开,因此我们从模型中获得了良好的准确率。
predictions = predict(prior_binary, class_conditionals_binary, x_test)
accuracy = accuracy_score(y_test, predictions)
print("Test accuracy: {:.4f}".format(accuracy))
---------
Test accuracy: 0.92
为了定量评估我们的概率逻辑回归模型的性能,我们绘制了决策区域。这些区域由分隔两个类别的边界定义,为模型区分类别的能力提供了见解。我们的分析表明,模型能够有效地分隔数据集中的两个类别,这从视觉上明显的区域可以看出。然而,需要注意的是,决策边界被限制为线性,这是逻辑回归模型的假设。
plt.figure(figsize=(9, 5))
plot_data(x_train, y_train)
x0_min, x_0_max = x_train[:, 0].min()-0.5, x_train[:, 0].max()+0.5
x1_min, x_1_max = x_train[:, 1].min()-0.5, x_train[:, 1].max()+0.5
contour_plot((x0_min, x0_max), (x1_min, x1_max),
lambda x: predict(prior_binary, class_conditionals_binary, x),
1, label_colors, levels=[-0.5, 0.5, 1.5],
num_points=200)
plt.title("Training set with decision regions")
plt.show()

图 5:类别条件决策区域。
逻辑回归中的缺失链接
在本节中,我们将上述类别条件密度的定义与逻辑回归联系起来。我们展示了预测分布 𝑃(𝑌=𝑦0|𝑋) 可以写作

其中 𝑃(𝑋|𝑌=𝑦0) 和 𝑃(𝑋|𝑌=𝑦1) 是类别条件密度,𝑃(𝑌=𝑦0) 和 𝑃(𝑌=𝑦1) 是类别先验。
这个方程可以重新排列为 𝑃(𝑌=𝑦0|𝑋)=𝜎(𝑎),其中

是 sigmoid 函数,以及

是对数赔率。
通过我们额外的建模假设,即共享协方差矩阵 Σ,可以使用高斯概率密度函数显示 𝑎 实际上是 𝑋 的线性函数,

其中

这个线性函数 𝑎=𝑤𝑇𝑋+𝑤0 解释了逻辑回归的决策边界为何是线性的。可以看出,参数 𝑤 和 𝑤0 是类别条件密度 𝑃(𝑋|𝑌=𝑦0) 和 𝑃(𝑋|𝑌=𝑦1) 以及类别先验 𝑃(𝑌=𝑦0) 和 𝑃(𝑌=𝑦1) 的函数。这些参数通常通过最大似然估计,如我们在前面章节中所做的那样。
生成逻辑回归模型
在本节中,我们使用前面章节中推导出的方程来直接参数化生成逻辑回归模型的输出伯努利分布。具体来说,我们使用先验分布和类别条件分布来计算权重和偏置项 𝑤 和 𝑤0。
为了实现这一点,我们编写了一个新函数,该函数以先验分布和类别条件分布作为输入。该函数使用这些分布的参数来计算权重和偏置项,根据前面章节中推导出的方程。
函数的输入是对两个类别的先验分布和类别条件分布。
然后,函数使用这些输入来计算权重和偏置项,如下所示

该函数返回𝑤和𝑤0,这可以用来直接参数化生成逻辑回归模型的输出伯努利分布。这允许对模型参数及其与先验和类条件分布的关系有更直接和透明的理解。
def get_logistic_regression_params(prior, class_conditionals):
cov = class_conditionals.covariance()[0]
cov_inv = tf.linalg.inv(cov)
mu0 = class_conditionals.parameters['loc'][0]
mu1 = class_conditionals.parameters['loc'][1]
w = np.matmul(cov_inv,(mu0-mu1))
w0 = - 0.5 * (np.matmul(tf.transpose(mu0), np.matmul(cov_inv, mu0)))\
+ 0.5 * (np.matmul(tf.transpose(mu1), np.matmul(cov_inv, mu1)))\
+ np.log(prior.parameters['probs'][0] / prior.parameters['probs'][1])
return w, w0
w, w0 = get_logistic_regression_params(prior_binary, class_conditionals_binary)
我们现在可以使用这些参数制作轮廓图,以显示我们逻辑回归模型的预测分布。
fig, ax = plt.subplots(1, 1, figsize=(9, 5))
plot_data(x_train, y_train, alpha=0.35)
x0_min, x0_max = x_train[:, 0].min()-0.5, x_train[:, 0].max()+0.5
x1_min, x1_max = x_train[:, 1].min()-0.5, x_train[:, 1].max()+0.5
X0, X1 = get_meshgrid((x0_min, x0_max), (x1_min, x1_max))
logits = np.dot(np.array([X0.ravel(), X1.ravel()]).T, w) + w0
Z = tf.math.sigmoid(logits)
lr_contour = ax.contour(X0, X1, np.array(Z).T.reshape(*X0.shape), levels=10)
ax.clabel(lr_contour, inline=True, fontsize=10)
contour_plot((x0_min, x0_max), (x1_min, x1_max),
lambda x: predict(prior_binary, class_conditionals_binary, x),
1, label_colors, levels=[-0.5, 0.5, 1.5],
num_points=200)
plt.title("Training set with prediction contours")
plt.show()

图 6: 我们逻辑回归模型预测分布的密度轮廓。
我们的方法是否完全是贝叶斯的?
上述方法可以被视为一种贝叶斯推断形式,因为它涉及通过先验分布引入关于模型参数的先验知识,并通过类条件分布使用观测数据更新这些知识。这是贝叶斯推断的一个关键方面,旨在将关于模型参数的先验知识和不确定性融入推断过程。
在贝叶斯推断中,目标是计算给定观测数据下模型参数的后验分布。上述方法可以看作是一种*似贝叶斯推断,因为它涉及使用类条件密度和先验分布的最大似然估计来计算模型的权重和偏置。同时,该方法通过共享协方差矩阵来引入不确定性,这作为正则化项。
值得注意的是,上述方法并非完全贝叶斯,因为它没有提供模型参数的后验分布的封闭形式。相反,它使用基于最大似然估计的*似。
结论
在这篇文章中,我们提出了一种概率方法来处理预测过程中的内在不确定性。通过在模型参数上引入先验分布,我们的方法对模型进行正则化,防止过拟合。我们展示了如何使用 TensorFlow Probability 实现该方法以及如何分析其结果。
值得注意的是,尽管我们的方法包含了贝叶斯原则,但它并不是完全贝叶斯的方法,因为我们没有模型参数的完整后验分布。然而,考虑到预测过程中的内在不确定性,已经使我们对预测过程更有信心。
保持联系: LinkedIn
参考文献和材料
[1] — 葡萄酒数据集
[2] — Coursera: 深度学习专项课程
[3] — Coursera: TensorFlow 2 深度学习专项课程
[4] — TensorFlow 概率指南和教程
[5] — TensorFlow 博客中的 TensorFlow 概率帖子
概率机器学习与分位数匹配:一个 Python 示例
原文:
towardsdatascience.com/probabilistic-ml-with-quantile-matching-an-example-with-python-c367eee85f18
一种将分位数回归预测转化为概率分布的鲜为人知的技术。
·发表于 Towards Data Science ·阅读时长 8 分钟·2023 年 9 月 4 日
--

“分位数匹配”,由Giulia Roggia。已获许可使用。
-
分位数回归
-
分位数匹配
-
Python 示例:预测糖尿病进展
-
结论
当我们训练回归模型时,我们获得的是点预测。然而,在实际应用中,我们通常对估计每个预测值的不确定性感兴趣。为实现这一目标,我们假设我们试图预测的值是一个随机变量,目标是估计其分布。
目前有许多方法可以估计预测的不确定性,如方差估计、贝叶斯方法、保形预测等。分位数回归是这些著名方法之一。
分位数回归
分位数回归包括为每个感兴趣的分位数估计一个模型。这可以通过使用一种不对称的损失函数来实现,这种损失函数称为pinball 损失。分位数回归简单易懂,并且在高效的库中如LightGBM中很容易获得。然而,分位数回归也存在一些问题:
-
没有保证分位数的顺序是正确的。例如,你对 50%分位数的预测可能会比 60%分位数的预测要大,这显然是不合理的。
-
为了获得整个分布的估计,你需要训练许多模型。例如,如果你需要每个百分位点的估计,你必须训练 99 个模型。
这就是分位数匹配如何提供帮助的。
分位数匹配
分位数匹配的目标是给定一组分位数估计来拟合分布函数。我们可以将此视为回归问题,因此曲线不必完全符合分位数。相反,它应该“尽可能接*”,同时保持使其成为分布函数的特性。
具体来说,我们感兴趣的是估计逆累积分布函数:给定一个概率alpha,我们想知道P(X<v)=alpha的值是什么,其中P代表概率,X是我们尝试预测的随机变量。
在以下示例中,我们提供了 3 种适配这种分布的替代方案。
Python 示例:预测糖尿病进展

图片由Towfiqu barbhuiya提供,来源于Unsplash
在本节中,我们展示了一个应用于糖尿病数据集的分位数匹配示例,该数据集可在Sklearn中获得:
对每个 n = 442 糖尿病患者,获得了十个基线变量,包括年龄、性别、体重指数、*均血压和六项血清测量值,以及感兴趣的响应,即基线后一年病情进展的定量测量。
让我们首先导入所需的库:
from abc import ABC, abstractmethod
import numpy as np
import pandas as pd
import plotly.graph_objects as go
from lightgbm import LGBMRegressor
from scipy import optimize, stats
from scipy.interpolate import PchipInterpolator
from sklearn.datasets import load_diabetes
分位数匹配方法
我们定义了三种替代方案来从一组分位数中估计逆累积分布函数:
-
拟合正态分布
-
拟合“半正态”分布:由两个不同标准差的正态分布组成的分布,一个在中位数以下,一个在中位数以上(与半正态分布的绝对值不同,后者也被称为半正态)。
-
三次插值:使用三次样条来估计*滑的递增曲线。
注意这三种方法逐渐更灵活。第一种方法将输出限制为遵循正态分布。第二种方法允许存在不对称性,这在实际世界的例子中很常见,例如预测价格回报。第三种方法对基础分布没有任何假设,例如,它允许多模态。
为了实现这些方法,我们使用了一个易于扩展的设计模式:
-
一个定义匹配器类接口的基础抽象类
-
一组实现不同算法的具体类
-
一个返回所需方法类的工厂
首先,我们定义基类和工厂。为了简单起见,我们建立一个接口来一次拟合和预测一个样本:
class QuantileMatcherBase(ABC):
@abstractmethod
def fit_one(self, alphas, quant_values):
pass
@abstractmethod
def predict_one(self, alphas):
pass
def quantile_matcher_factory(match, **kwargs) -> QuantileMatcherBase:
matcher_map = {
"normal": QuantileMatcherNormCurvFit,
"half_normal": QuantileMatcherHalfNormCurvFit,
"cubic_interpolation": QuantileMatcherCubicInterpolation,
}
if match not in matcher_map:
raise ValueError(f"Unknown matcher {match}")
return matcher_mapmatch
然后,我们可以继续进行具体的实现。首先是常规分布:我们将问题框定为一个非线性优化问题,其中我们需要估计参数以最小化拟合曲线与观察值之间的*方差。
class QuantileMatcherNormCurvFit(QuantileMatcherBase):
"""Normal distribution quantile matcher."""
def __init__(self):
self.params = None
def fit_one(self, alphas, quant_values):
self.params, _ = optimize.curve_fit(
lambda x, mu, sigma: stats.norm.isf(x, mu, sigma),
alphas,
1 - quant_values,
)
def predict_one(self, alphas):
return 1 - stats.norm.isf(alphas, *self.params)
对于半常规分布,我们重复使用上面定义的类:一次用于中位数以下的值,一次用于中位数以上的值。
class QuantileMatcherHalfNormCurvFit(QuantileMatcherBase):
"""Half-Normal distribution quantile matcher."""
def __init__(self):
self.below = QuantileMatcherNormCurvFit()
self.above = QuantileMatcherNormCurvFit()
def fit_one(self, alphas, quant_values):
self.below.fit_one(alphas[alphas<=0.5],quant_values[alphas<=0.5])
self.above.fit_one(alphas[alphas>=0.5],quant_values[alphas>=0.5])
# trick to ensure same median
mu = (self.below.params[0] + self.above.params[0]) / 2
self.below.params[0] = mu
self.above.params[0] = mu
def predict_one(self, alphas):
pred = self.above.predict_one(alphas)
pred_below = self.below.predict_one(alphas)
pred[alphas<0.5] = pred_below[alphas<0.5]
return pred
请注意,在 fit_one 方法中,我们应用了一个小技巧,以确保两个分布具有相同的中位数。
三次插值的实现很简单:
class QuantileMatcherCubicInterpolation(QuantileMatcherBase):
"""Increasing cubic interpolation quantile matcher."""
def __init__(self):
self.params = None
def fit_one(self, alphas, quant_values):
self.interp = PchipInterpolator(alphas, quant_values)
def predict_one(self, alphas):
return self.interp(alphas)
量化回归包装器
我们定义一个类,用于拟合一些 Lightgbm 模型,并进行量化回归,以适应预定义的量化集合。我们实现了一个方法 predict_raw 来获取每个模型的原始预测,以及一个方法 predict_cdf 来使用之前定义的 QuantileMatcher 类在量化网格上获取(反向)累积分布函数。
class ProbLGBMRegressor:
_forbidden_keys = (
"objective",
"objective_type",
"app",
"application",
"loss",
"alpha",
)
def __init__(
self,
alphas=np.array([0.01, 0.1, 0.25, 0.5, 0.75, 0.9, 0.99]),
**lgbm_args
):
self.alphas = alphas
for key in self._forbidden_keys:
if key in lgbm_args:
raise ValueError(f"{key} parameter is not allowed.")
self._models = {}
for alpha in self.alphas:
self._models[alpha] = LGBMRegressor(
objective="quantile", alpha=alpha, **lgbm_args
)
def fit(self, x, y):
for alpha in self.alphas:
self._models[alpha].fit(x, y)
def predict_raw(self, x):
return pd.DataFrame(
{alpha: model.predict(x) for alpha, model in self._models.items()}
)
def predict_cdf(
self,
x,
inference_alphas=np.linspace(0.001, 0.999, 999),
match="normal_curve_fit",
**matcher_params,
):
# Compute predictions for the limited set of quantiles.
raw_preds = self.predict_raw(x)
# Estimate the cumulative distribution for each sample.
matcher = quantile_matcher_factory(match, **matcher_params)
predictions = []
for _, row in raw_preds.iterrows():
matcher.fit_one(self.alphas, row.values)
preds = matcher.predict_one(inference_alphas)
predictions.append(preds)
return pd.DataFrame(predictions, columns=inference_alphas)
糖尿病数据集:拟合和预测
现在我们可以加载糖尿病数据集,并使用上面定义的类来训练模型并预测目标值的分布。
x,y = load_diabetes(return_X_y=True, as_frame=True)
# Fit a regressor
prob_lgbm = ProbLGBMRegressor()
prob_lgbm.fit(x,y)
# Predict the distributions with all methods
predicted_cdf = {}
for match in ["normal","half_normal","cubic_interpolation"]:
predicted_cdf[match] = prob_lgbm.predict_cdf(x, match=match)
# For visualization purposes, we predict also the "raw" values
predicted_raw = prob_lgbm.predict_raw(x)
预测的图形分析
为了了解我们的模型是什么样的,我们可以绘制几个样本的预测分布。让我们定义一个帮助函数来绘制预测的累积分布函数。
def get_fig_cumulative_distribution_function(predicted_cdf, predicted_raw, idx):
# Small artifact to ensure same range in figures
max_limit = max([pred.iloc[idx, -1] for pred in predicted_cdf.values()]) + 5
min_limit = max([pred.iloc[idx, 0] for pred in predicted_cdf.values()]) - 5
# Create traces for each distribution.
trace = []
for match, pred_cdf in predicted_cdf.items():
x = [min_limit] + list(pred_cdf.iloc[idx].values) + [max_limit]
y = [0] + list(pred_cdf.columns) + [1]
trace.append(go.Scatter(x=x, y=y, mode="lines", name=match.title()))
# Add trace for raw quantile predictions.
trace.append(
go.Scatter(
x=predicted_raw.iloc[idx],
y=predicted_raw.columns,
mode="markers",
name="Raw Predictions",
marker={"size": 10},
)
)
# Create the figure
fig = go.Figure(trace)
fig.update_layout(
title="Cumulative Distribution Functions",
yaxis_title="alpha",
xaxis_title="quantile",
)
# Set x-axis limits
fig.update_xaxes(range=(min_limit, max_limit))
return fig
这里是通过预测数据集中第一个样本获得的图表:

索引 0 的预测累积分布。图片由作者提供。
我们可以看到三种方法产生了不同的曲线。虽然常规分布和半常规分布非常接*且*滑,但三次插值则较为不规则,完美拟合了所有“原始”预测。
虽然评估其量化值很方便,但从累积分布的角度分析分布的全球行为可能比较困难。为了获得更好的视图,我们可以通过使用 有限差分法 来估计相应的概率分布。让我们定义一个帮助函数来完成这项工作:
def get_fig_probability_distribution_function(predicted_cdf, idx):
trace = []
for match, pred_cdf in predicted_cdf.items():
quantiles = pred_cdf.iloc[idx].values
icdf_values = pred_cdf.columns.values
# Estimate the PDF using finite differences
diff_icdf = np.diff(icdf_values)
diff_quantiles = np.diff(quantiles)
pdf_est = diff_icdf / diff_quantiles
# Create a Plotly figure for the estimated PDF
trace.append(
go.Scatter(
x=quantiles[:-1],
y=pdf_est,
mode="lines",
fill="tozeroy",
name=match,
)
)
fig = go.Figure(data=trace)
# Add labels and title
fig.update_layout(
xaxis_title="Quantiles",
yaxis_title="Estimated PDF",
title="Estimated Probability Density Function",
)
return fig
通过对数据集中第一个样本应用上述函数,我们得到以下图表:

索引 0 的预测概率分布。图片由作者提供。
让我们再看几个样本的累积分布和概率分布。
这些是我们对第二个样本得到的结果:

索引 1 的预测累积分布。图片由作者提供。

索引 1 的预测概率分布。图片由作者提供。
这些是我们对第三个样本得到的结果:

索引 2 的预测累计分布。图像由作者提供。

索引 2 的预测累计分布。图像由作者提供。
我们可以看到,正常分布和半正态分布并不一致,这表明真实的潜在分布存在不对称性。
我们还注意到,三次插值给出了多模态且常常极端的结果。这是因为插值不受特定形式的约束,并且在拟合接*的点时往往具有高导数。这些结果可能不切实际,*滑技术可能有助于缓解这个问题。
结论
从初步来看,半正态分布似乎是最佳选择,因为它提供了现实的分布,同时能够建模不对称行为。然而,选择匹配算法的最佳方式是交叉验证预测并评估相关指标,例如预测区间的宽度及其准确性(90%的区间应在大约 90%的时间内包含目标)。
如开头所述,这种技术并不很受欢迎,我还没有机会在实际场景中使用它。因此,如果你在项目中使用了它,请告诉我!
本示例中使用的完整代码可在 此处 获取。
喜欢这篇文章? 查看我的其他文章 并关注我以获取更多内容! 点击这里 阅读无限制文章并在不增加你额外成本的情况下支持我 ❤️
主成分分析的概率视角
原文:
towardsdatascience.com/probabilistic-view-of-principal-component-analysis-9c1bbb3f167
潜在变量、期望最大化与变分推断
·发表于 Towards Data Science ·9 分钟阅读·2023 年 7 月 12 日
--

寻找隐藏变量(图片来源:作者)
在数据科学和机器学习中,主要使用的降维技术之一是主成分分析(PCA)。之前,我们已经讨论过将 PCA 应用于 管道与 支持向量机 的一些例子,在这里我们将从概率的角度来看 PCA,以提供对数据结构的更全面和稳健的理解。概率 PCA(PPCA)的一个最大优点是它能够处理数据集中缺失的值,这是经典 PCA 无法做到的。 由于我们将讨论潜在变量模型和期望最大化算法,你还可以查看 这篇详细的文章。
你可以从这篇文章中学到什么?
-
PCA 简短介绍。
-
PPCA 的数学构建块。
-
期望最大化(EM)算法或变分推断?用于参数估计时应该使用哪一个?
-
使用 TensorFlow Probability 在玩具数据集上实现 PPCA。
让我们深入探讨一下吧!
1. 奇异值分解(SVD)和 PCA:
线性代数中一个重要的概念是SVD,它是一种对实数或复数矩阵进行分解的技术,例如,一个矩阵(假设为A)可以被分解为:

方程 1:矩阵 A 的 SVD。
其中 U,Vᵀ 是正交矩阵(转置等于逆),而 Σ 将是一个对角矩阵。A 不需要是方阵,例如它是一个 N×D 矩阵,因此我们可以将其视为我们的数据矩阵,其中 N 个实例和 D 个特征。U,V 分别是方阵 (N×N) 和 (D×D),Σ 将是一个 N×D 矩阵,其中 D×D 的子集是对角的,其余条目为零。
我们也知道特征值分解。给定一个可以对角化的方阵 (B) 可以分解为:

方程 2:矩阵的特征值分解
其中 Q 是一个方形的 N×N 矩阵,其第 i 列是 B 的特征向量 q_i,而 Λ 是对角矩阵,其对角元素是对应的特征值。
让我们尝试通过乘以Aᵀ来修改方程 (1)。

方程 3:乘以 A 的转置。
在这里,AᵀA 将是一个方阵,即使 A 起初不需要是(可以是 m×n)。Σ Σᵀ 是一个对角矩阵,而 V 是一个正交矩阵。现在,这基本上是矩阵 AᵀA 的特征分解。这里的特征值是方程 (1) 中 A 的奇异值的*方。
对于正半定矩阵,SVD 和特征分解是等效的。PCA 最终归结为协方差矩阵的特征分解。找到最大特征值和相应的特征向量基本上可以视为找到最大方差的方向。
给定 D 维数据(特征),完全的特征分解将是昂贵的 ∼O(D³),但现在如果我们选择一些潜在空间维度 M(<D),则计算会便宜 ∼O(MD²)。
2. PPCA 的构建块:
2.1. 假设:
PPCA 是一个潜在变量模型 (LVM),我们之前在 详细讨论过 包括期望最大化 (EM) 算法。LVM 提供了数据的低维表示。假设我们的数据 (x) 是 N×D 维的,具有 D 个特征;那么 PCA 的 LVM 寻求一个 M 维的潜在变量向量 z,它可以用来生成观察变量 (x),并且它们通过线性关系相互关联:

方程 2.1:PPCA 的生成过程;x 条件于潜在变量 z。
上述方程中的噪声 ϵ 是一个 D 维向量,具有零均值高斯分布和 σ²I 的协方差;

方程 2. 2:噪声被建模为具有零均值和协方差 σ² 的正态分布。
由于我们知道潜在空间是 M 维的,这使得我们的 W 向量是 D×M 维的。假设潜在变量 z 具有零均值、单位协方差的高斯分布:

Eq. 2.3:潜在变量的先验是均值为零、协方差为单位的正态分布。
上述两个方程导致条件分布 x 在 z 下的如下:

Eq. 2.4:根据前两个方程,我们得到条件分布 x。
这是另一个均值为 Wz(我们可以设定 μ = 0)和协方差为 σ² 的正态分布。
上述方程应让我们想起正态分布的一个基本属性:即,如果 x 服从多元正态分布 x∼N(μ, Σ),则 x 的任何线性变换也是多元正态分布 y = Ax + b ∼ N(Aμ+b, AΣAᵀ)。
给定联合分布,边际分布也将是高斯分布:

Eq. 2.5:数据分布也服从正态分布。
由于我们希望确定参数 W、μ、σ,我们可以通过 MLE 或 EM 算法来解决这个问题。这里我们将关注 EM 方法,然后是变分推断。两种方法在 Bishop 的书中都有详细描述。Bishop 认为,随着数据维度的增加,通过迭代的 EM 步骤,我们可能在计算上比 MLE 获得优势。这主要涉及协方差矩阵的计算成本,其中 D 维数据协方差矩阵的评估需要 O(ND²),N是数据点的数量。
2.2. PPCA 的 EM 步骤:
在讨论了EM 算法 参考于高斯混合模型之后,这里我将其参考于 PPCA 进行描述。EM 算法的步骤如下:
-
在期望步骤中,我们计算完整数据对数似然相对于潜在变量的后验分布 (p(z|x)) 的期望,使用的是“旧”参数。
-
最大化该数据对数似然函数将得到“新”的参数,这些参数将被插入到第 1 步中。
由于数据点是独立的,完全数据似然将是:

Eq. 2.6:包括观察变量和潜在变量在内的完全数据似然。
E 步的主要目标是计算上述表达式的期望。在这里,我们需要使用方程 3 和 4 中 p(x|z) 和 p(z)。推导在 Bishop 的书中给出,但重要的是推导需要计算 E[z_n]、E[z_n zᵀ_n],这些可以通过后验分布 p(z|x) 推导得出。
一旦 E 步完成,M 步则涉及最大化相对于参数 W、σ² 的期望对数似然。
变分推断、EM 和 ELBO:
上述 EM 步骤依赖于一个关键假设,即后验分布p(z|x)是可处理的(这是方程 2.6 中的 E 步所必需的)。如果不是这样呢? 如果后验没有任何解析表达式?这就是变分推断的基础。
我们现在借助变分方法。主要思想是我们尝试找到一个分布q(z),使其尽可能接*后验分布p(z|x)。这个*似分布可以有自己的变分参数:q(z|θ),我们尝试找到使q接*感兴趣后验的参数设置。q(z)应该相对简单,更易于推断。为了衡量两个分布q(z)和p(z|x)*的接*程度,常用的度量是 Kullback-Leibler (KL) 散度。变分推断中的 KL 散度自然引入了证据下界(ELBO):

方程 2.7:q(z)和后验p(z|x)的接*程度,ELBO 加在一起给我们数据的可能性。
其中 ELBO (q) 被定义为:

方程 2.8:ELBO 的定义
对于推导,你可以查看参考中的笔记本或其他可用的讲义。
由于 KL 散度是非负的,log p(x)* ≥ ELBO(q)。所以我们在变分推断(VI)中所做的就是最大化 ELBO。
我们还可以很容易地看到 VI 和传统 EM 算法之间的联系;当q(x)==p(z|x)*时,KL 散度项消失。
由于我们现在已经完成了 PPCA 的概述,我们将使用 TensorFlow Probability 来实现这一点,并使用 ELBO 的定义尝试最大化它,这反过来等同于最大化数据的可能性 log p(x)。
使用 TensorFlow Probability 实现 PPCA:
为了通过变分推断实现一个简单的 PPCA 示例,我将遵循 TensorFlow Probability 应用中的原始示例。
实现这一点时,我们将假设我们知道噪声的标准差(σ,我选择了 3.0),如方程(2.2)所定义,并且我们对w进行先验设定,并尝试通过变分推断来估计。让我们定义联合分布:
在这里我们使用了JointDistributionCoroutineAutoBatched,这是JointDistributionCoroutine的“自动批处理”版本。它根据输入参数的形状自动应用批处理语义,从而允许更灵活地处理批处理维度。我们可以将批处理或非批处理参数传递给联合分布,它会自动处理批处理语义。我们从这个联合分布中采样后,使用tf_model.sample()在第 51 行中,我们绘制观察数据(x)的分布(2D):

图 1:数据的观测分布,给定上述代码定义的联合分布。
顺便提一下,由于这些数据点是在你运行代码时随机采样的,你可能不会得到完全相似的点。
我们尝试认为后验p(W, Z|X)可以用一个由θ参数化的更简单的分布q(W, Z)来*似。在 VI 中,我们的目标是最小化q(W, Z)和p(W, Z|X)之间的 KL 散度,这从方程(2.8)来看,则是最大化证据下界(ELBO)。
在这种情况下的 ELBO:

我们通过 VI 尝试最小化的 ELBO。
为了最小化 ELBO,我们将首先定义类似于定义联合分布的方式的替代分布,并使用tfp.vi方法将替代后验拟合到目标(未归一化)对数密度。
在获得替代分布后,我们可以使用它来采样新的数据点,结果如下图所示:

图 2:采样的分布与原始分布非常相似。
结论:
我们已经探讨了 PPCA 背后的数学原理,并使用 TensorFlow 通过一个简单的玩具数据集测试了我们的理解。通过使用替代后验生成新样本的可能性,使我们能够在数据中填补缺失值、生成新样本等,这在标准 PCA 中是不可能的。许多实际数据集展示了复杂的关系、噪声污染和不完整的观测,这使得经典 PCA 的效果不佳。通过考虑不确定性、处理缺失数据,并提供概率建模框架,PPCA 为数据分析、模式识别和机器学习开辟了新的途径。如果下次你打算使用 PCA 来处理你的数据集,但你知道观测可能会有噪声且存在缺失数据,为什么不尝试 PPCA 呢?
概率 PCA 还与因子分析(FA)密切相关,后者是一种线性高斯潜变量模型。FA 与 PPCA 之间的主要区别在于,在方程 2.2 中描述噪声分布时,PPCA 假设协方差是各向同性的,而 FA 中它是对角矩阵。我会在下面留下更多参考文献,以便你可以根据这篇文章进行进一步探索。
参考文献:
[1]《概率主成分分析》;M. Tipping, C. Bishop;J.R. Statist. Soc. B (1999)。
[2]《模式识别与机器学习》;C. Bishop;第十二章:连续潜变量。
[3]《连续潜变量模型》;多伦多大学;讲义。
[5] 我的笔记本链接。GitHub
数据分析师实际生活中的概率面试问题
将概率面试问题与数据分析师的日常任务联系起来
·
关注 发表在 Towards Data Science · 5 分钟阅读 · 2023 年 10 月 22 日
--
如果你申请数据分析师或数据科学家的职位,在面试中你会经常遇到概率问题。但问题是:有些人确信这些问题与实际工作关系不大。像“为什么我们要计算掷骰子 5 次都掷出 6 的概率?”这样的问题经常出现。在这篇文章中,我将分享一些真实的例子来解释为什么理解概率比你想象的更重要。为此,我们来看看一些面试任务,并了解它们在现实世界中的应用。
Q1. 你连续抛掷 10 次硬币,所有硬币都是正面朝上的概率是多少?
想象你是一个食品配送服务的数据分析师。每次订单完成后,客户可以评分食物的质量。团队的主要目标是提供顶级服务,如果餐厅收到差评,你需要检查。所以,关键问题是——多少条差评应该触发对餐厅的检查?
有时候,一个餐厅偶尔会收到一些不太好的反馈,这并不是他们的错。如果一个餐厅处理了 1000 个订单,他们可能会因为偶然原因收到几条差评。
这样考虑:大约 5%的订单偶然会收到负面评价。然后,每个餐厅的差评数量遵循二项分布Bin(n, p),其中“n”是订单数量,“p”是负面评价的可能性(在我们这里是 5%)。
所以,如果一个餐厅有 100 个订单,他们收到至少 7 条差评的概率大约是 23.4%,而收到至少 10 条差评的概率则小得多,只有 2.8%。你可以通过计算器这里来检查,参数是n=100、x=10、p=0.05,别忘了选择选项x>=X。

作者提供的图片。
结论是:如果你将检查阈值设定为 100 个订单中的 7 条差评,你可能会过于频繁地检查餐厅,这意味着你会增加额外成本,并对餐厅施加更多压力。
Q2. 你从 52 张标准扑克牌中抽取 10 次。抽到没有红色牌的概率是多少?
现在,想象你处在电子商务网站的世界里。你和你的团队刚刚引入了一种新的支付方式,你想知道客户使用这个新功能的频率。但问题是——由于一个小 bug,大约 2%的新支付请求会失败。换句话说,客户在 98%的会话中看到这个新支付选项。为了弄清楚客户选择这种支付方式的频率,你想关注那些始终可以使用它的用户。但这就有点棘手了。
设想一个只有一个会话的用户——你以 2%的概率将他们排除在分析之外。现在,考虑一个有 25 个会话的用户。对于他们来说,至少一个会话中没有该功能的机会是 1–0.98²⁵ = 39.7%。所以,你可能会无意中遗漏一些最忠诚的客户,这可能会扭曲你的分析。

图片由作者提供。
Q3. 如果你掷骰子三次,得到两个连续的三的概率是多少?
想象你在一家像 Uber 这样的打车公司工作。在某些国家,人们仍然用现金支付车费,这对司机来说可能是个麻烦。他们需要携带零钱,处理现金交易等等。
你的团队担心如果司机连续接到三个现金订单,他们可能会感到沮丧并且零钱用完。因此,你考虑在这种情况下限制现金订单。但在此之前,你想了解这种情况发生的频率。
假设每个司机每天的*均行程数是 10,其中 10%的行程以现金支付。
因此,得到三个连续现金订单的概率是 0.10.10.1 = 0.001. 但它可以是第 1、第 2、第 3 次订单;第 2、第 3、第 4 次订单,等等。这意味着连续三个现金订单的机会仅为 80.10.1*0.1 = 0.008. 看起来相当低,你可能要考虑暂时不实现这个功能。

Q4, 一项 HIV 测试的准确率是 99%(双向)。只有 0.3%的人口是 HIV 阳性。如果一个随机人检测结果为阳性,这个人 HIV 阳性的概率是多少?
原文文章见这里。
你在银行或信用行业,建立模型来预测客户是否会归还贷款。总体来说,85%的贷款通常会被偿还。在你最新的模型中,对于那些还款的客户,预测正确率为 92%。然而,当客户没有还款时,预测的正确率仅为 60%。现在,你有一个担忧:如果你的模型表示客户不会还款,那么他们实际还款的真实概率是多少?
首先,让我们计算模型预测“客户不会还款”的可能性。这涉及两个部分:
-
从不会还款的客户那里得到这种预测的概率:0.6*(1–0.85) = 0.09
-
从会还款的客户那里得到这种预测的概率:(1–0.92)*0.85 = 0.068
-
如果我们的模型认为客户不会还款,那么客户实际还款的概率是:0.068/(0.068+0.09) = 0.43

因此,如果你认为客户不会归还贷款,实际上他们有相当高的概率会归还。
这篇文章的全部意义是什么?它强调了理解概率和组合数学对数据科学家和分析师至关重要。在你的日常生活中,你会遇到需要掌握概率的情况,否则你可能会得出错误的结论。然而,从雇主的角度来看,面试问题应更具实际性,以帮助未来的分析师认识到这些知识在工作中的实际应用。
感谢你花时间阅读这篇文章。我非常希望听到你的想法,请随时分享你的评论或问题。
探讨最小样本量公式:推导与应用
A/B 测试中的样本量公式简明指南
·
关注 发表在 Towards Data Science · 18 分钟阅读 · 2023 年 2 月 1 日
--
太长;不读
本文回答了两个围绕 A/B 测试中最小样本量计算的重要“如何做”问题:
1) 如何推导最小样本量公式 𝜨?
公式背后的核心思想是反转假设检验中的 p 值计算,特别关注于统计功效,即在原假设确实为假的情况下拒绝原假设的概率。

图 1:最小样本量计算公式的直观解释
2) 如何在现实场景中使用公式计算最小样本量?
我们使用历史数据,将其汇总到正确的实验单位,来计算我们感兴趣指标的标准差以及历史*均值作为基线水*。然后,我们手动确定所需的显著性水*𝛼、统计功效1-𝛽,以及最重要的,期望在处理组和对照组之间的最小可检测效果𝑑𝑚𝑖𝑛。
这些是样本量计算所需的四个输入。值得注意的是,如果我们想检测到更微妙的增量提升(小𝑑𝑚𝑖𝑛),则在更强的统计能力和更长的实验周期之间存在权衡。因此,在设计 A/B 实验时,我们通常会计算在不同𝑑𝑚𝑖𝑛水*下的多个样本量版本,以量化这些权衡。
目录
-
我们为什么要关心
-
设置
-
I. 推导最小样本量(简单公式/无𝛽 𝛽)
• 关键思想
• 公式推导
• 示例:分析芝加哥两个社区之间的下载速度差异
-
II. 推导最小样本量(标准公式/带𝛽) • 关键思想 • 公式推导
• 示例:“揭开”Evan 的 A/B 样本量计算器的神秘面纱
-
实际问题
• 如何从我们的通用公式推导经验法则样本量?
• 如何确定𝑑𝑚𝑖𝑛?
• 如果两个组的方差和样本量不相等怎么办?
• 如何使用历史数据估算标准差𝜎?
• 我们需要多少历史数据?
• 我们应该如何准备这些数据?
-
总结
为什么我们需要关心?
在 2022/2023 年,当网上有大量现成的样本量计算器(例如,Evan Miller’s Calculator、Optimizely)可用,以及大多数科技公司都有自己建立的实验*台和工具时,深入探讨公式背后的原理似乎没有必要。然而,了解这些原理仍然很重要,因为:
-
A/B 测试样本量计算是产品数据科学家****面试中最常见的问题之一,我们需要不仅能够背诵公式,还要能解释其原理。
-
在在线随机 A/B 测试和离线对照试验中,现实世界的业务背景可能很复杂,因此我们需要了解公式的来龙去脉,以正确选择或调整这些工具并避免谬误。
-
此外,我们在假设检验和功效分析中建立“统计直觉”至关重要,因为由于样本量不足,常常会遇到无显著性检验结果。我们当然希望提高对可能的第二类错误(忽视真正有效的处理策略)的敏感度,并准备估算下一步所需的样本量。这不仅仅涉及技术公司中的典型数据科学工作,还涉及学术界和日常生活中的研究问题——“女性员工的薪酬是否低于男性?”、“财富更高的社区是否比欠发达社区有更好的互联网连接和下载速度?”等。我们不仅要问自己“我们从数据中看到的是否具有统计学意义?”,还要问“我们是否有足够的样本?如果没有,我们还需要多少数据或未来需要发送多少份调查问卷?”
关于这个主题,有许多优秀的入门文章和资源,但大多数仅仅“呈现”公式。站在这些前人的肩膀上,我希望在这里进一步深入探讨直觉和推导,回答“这些公式为何如此定义”的问题。此外,我还将讨论使用这些公式的一些实际问题。
设置
测试
本文中样本量计算的讨论将主要基于两个样本单侧 z 检验。
原因在于,在 A/B 测试场景中,测试统计量通常是样本均值(例如实验期间每用户的*均 XXX,其中该期间特定用户的所有行为被视为一个样本,而该指标只是所有用户的样本均值统计量),并且通常根据中心极限定理呈正态分布。这里倾向于使用单侧检验,因为我们通常对处理效果有明确的方向性信念(即处理组应表现得比对照组更好或更差)。
符号和概念
-
𝜨: 最小所需样本量。
-
𝑝: p 值。
-
𝛼: 显著性水*,或称为第一类错误。
-
𝛽: 第二类错误。1-𝛽表示统计功效。
-
X̄1/X̄2/X̄d: 第 1 组(对照组)、第 2 组(处理组)和两个随机均值变量之间差异的样本均值。
-
x̄1/x̄2/x̄d: 第 1 组(对照组)、第 2 组(处理组)和两个均值统计量之间差异的样本均值。
-
z_(1-𝛼): 切割顶部𝛼(%)的 z 分数,或标准正态分布的临界值。例如,在单侧检验中,z_(1–5%)=z_(95%)≈1.65; z_(1–2.5%)=z_(97.5%)≈1.96; z_(2.5%)≈-1.96。**
-
𝜎:每组观察值的标准差。我们假设两个组的标准差相同(我们还将讨论当方差不相等时如何调整公式)。
-
𝑑𝑚𝑖𝑛:最小可检测效应,即在给定样本量 𝜨 的情况下,可以检测到的统计显著差异水*。例如,如果对照组的转换率为 10%,且我们预期处理能够将转换率从 10% 实际提高到 15%(请注意这是我们的预期),则(绝对)𝑑𝑚𝑖𝑛 为 15%-10%=5%。直观地说,如果我们预期的处理效应非常小(即我们对处理策略没有信心),我们将需要更多的样本来将其与自然变异区分开,因为真实效应可能非常微弱。如果最小可检测效应较大,它应该相当显著,因此少量样本就足以告诉我们两组之间的差异是否实际具有统计学意义。
目标
给定 I 型错误𝛼、II 型错误𝛽、标准差𝜎和最小可检测效应𝑑𝑚𝑖𝑛,我们希望估计最小所需样本量 𝜨,以便我们有足够的统计功效来检测到至少与𝑑𝑚𝑖𝑛一样大的差异。
换句话说,样本量 𝜨(仅)保证我们能检测到至少𝑑𝑚𝑖𝑛的处理效应或组间差异。如果实际的处理效应实际上小于𝑑𝑚𝑖𝑛,我们可能无法将这种微小的改进与自然变异区分开。
I. 推导最小样本量(天真的公式/不考虑𝛽)
虽然统计功效1-𝛽,或者当实际上存在差异时检测差异的概率,是估计样本量的关键因素,但当我开始学习 A/B 测试时,我发现很难理解。因此,在本文中,我们首先从这种“天真的”最小样本量估计版本开始,仅考虑显著性水*𝛼,然后在下一节中逐步进入“正式”的推导,其中引入统计功效的概念。
关键思想
计算最小所需样本量的关键思想就是假设检验的逆过程,或者换句话说,询问样本量 “就像 p 值已知为显著性水* 𝛼”。
给定两个样本,我们可以基于样本均值、标准差和N计算 p 值。用图 2的话来说,我们希望样本均值差异统计量X̄d 落在红色区域内,因为这是原假设 H0 的拒绝区域。

图 2:假设检验中的拒绝区域和临界值(作者提供)
现在,由于显著性水* 𝛼 本质上是 p 值的 阈值,如果我们知道历史数据中的 标准差 𝜎 并指定 𝛼 的水*以及我们希望检测的两组之间的预期差异 𝑑𝑚𝑖𝑛,我们可以计算 N。这种公式下所需的最小样本量 N 确保我们有足够的“显著性”来检测两组之间的 𝑑𝑚𝑖𝑛 量的差异。
公式推导
基于上述思想,我们可以概述以下高级公式,我将围绕这个逻辑逐步讨论 N 的推导:

公式 1: 朴素最小样本量公式的高级概念(作者提供)
首先,我想明确我们的假设和测试统计量。零假设是组 1(对照组)和组 2(处理组)的均值统计量之间没有统计学差异。备择假设则表明我们的处理组‘优于’对照组,表明这是一个单侧检验。

公式 2: 两样本单侧 T 检验/Z 检验中的 H0 和 H1(作者提供)
我们关注的是组 1 和组 2 的均值统计量的差异:

公式 3: 双样本 T 检验/Z 检验的测试统计量(作者提供)
在进一步探讨之前,让我们先探讨分布性质。我们不需要对两组 𝛸1、𝛸2 做任何分布假设,因为根据中心极限定理,它们的 样本均值统计量 以及推导出的 样本均值差异统计量 遵循正态分布。

公式 4: 关键随机变量和测试统计量的分布性质(作者提供)
记住,p 值表示在零假设为真的情况下,随机变量获得一个值与当前样本统计量一样极端或更极端的概率。因此,p 值和 z 统计量可以通过以下公式计算:

公式 5: 两样本单侧 Z 检验的 p 值公式(作者提供)
因此,如果我们进行假设检验,当我们的 p 值小于 𝛼 时,我们将以 𝛼 显著性水*拒绝零假设,或者样本 z 统计量大于临界 z 值:

公式 6: 当 H0 被拒绝时的两样本单侧 T 检验/Z 检验的假设检验标准(作者提供)
太棒了!现在我们已经得出了给定两个样本组的假设检验最终公式。你可能已经注意到分母中有𝜨。尽管很简单,计算所需的最小样本量的方法是(1)将x̄d视为预定的最小可检测效应(MDE 或 𝑑𝑚𝑖𝑛);(2)假设原假设成立(μ2-μ1=0),以及(3)通过反转不等式来求𝜨。
-
要进行假设检验,我们通过计算组 1 和组 2 之间的样本均值差异来计算x̄d。
-
要计算最小样本量,我们用预期的最小可检测效应𝑑𝑚𝑖𝑛替换基于样本的x̄d,这代表了来自组 1(对照组)和组 2(处理组)的预期差异,或者在产品分析语言中,就是我们期望治疗策略带来的潜在增量效果。
因此,给定𝑑𝑚𝑖𝑛、𝛼和𝜎的𝜨的推导如下:

公式 7:给定𝑑𝑚𝑖𝑛、𝛼和𝜎的𝜨的推导(作者)
示例:分析芝加哥两个社区之间的下载速度差异
假设你是一个研究助理,与你的教授一起工作。团队当前的研究是调查互联网不*等,这通过下载速度,在服务不足和富裕社区之间来衡量,你的教授坚信/假设存在这种不*等,因为基础设施、服务提供商等方面存在差异。
两个关注的地理社区是林肯公园(芝加哥北区,最富裕的社区之一)和南岸(芝加哥南区,通常被认为是服务不足的社区)。团队已经对每个社区的 10 个随机选择的家庭进行了调查,教授让你进行数据分析以验证他的假设。
表 1:初步小样本(N=10)(合成数据,作者编造)
你意识到t 检验可能是检查林肯公园和南岸之间每个家庭*均下载速度是否存在统计显著差异的一个好方法。然而,在整理数据后,你发现结果是不具有统计显著性,你认为这是因为样本量太小,实际差异被自然变异所掩盖。
接下来出现的问题是:你建议团队收集多少更多的样本? 我们需要知道三个数字: 𝛼、𝜎 和 𝑑𝑚𝑖𝑛来根据公式 7 计算所需的最小样本量。
我们通常指定 5% 的显著性水*,并且在假设方差齐性的情况下,标准差可以用组 1 的样本标准差进行*似。对于𝑑𝑚𝑖𝑛,我们需要与教授沟通,借用领域知识并确定组 1(南岸)和组 2(林肯公园)之间的预期差异。目前,假设教授建议这两个社区之间家庭下载速度的预期差异或最小可检测提升(%)应为 1%。因此,我们得到:

公式 8: 𝛼、𝜎 和 𝑑𝑚𝑖𝑛 在下载速度示例中的规格(作者提供)
插入这些数字后,我们能够计算出所需的最低样本量,为 77。这个数字意味着我们应该调查 67 户家庭(我们已经有 10 户),而这个更大的样本(N=77)将使我们能够检测到 1% 或更大的下载互联网速度差异,前提是这两个社区之间确实存在这样的差异。

公式 9: 计算给定 𝑑𝑚𝑖𝑛、𝛼 和 𝜎 的 𝜨 的示例(作者提供)
完整的 Python 实现可以在我的 GitHub Gist 上找到,或嵌入的笔记本如下:
II. 推导最低样本量(标准公式/与 𝛽)
关键思想
在科技公司设计和启动 A/B 测试时,我们通常不仅关心获得显著的 p 值以拒绝原假设,更重要的是在原假设确实错误时拒绝原假设,这表明我们的处理确实有效,而不是第一类错误。因此,我们所需的样本量需要有足够的统计功效 (1-𝛽) 来检测这种处理效应。**图形上,如图 3所示,我们最‘渴望’的是两件事:
-
样本统计量(X̄d)超出了临界值,因为这是 H0 的拒绝区域。
-
样本统计量(X̄d)落在 分布 Ha 中的蓝色阴影区域,因为蓝色区域代表了替代分布,表示处理组和对照组之间的总体差异不为零。
这是计算所需最低样本量的关键思想。

图 3: 假设检验中的拒绝区域、临界值、统计功效(作者提供)
从上述提到的朴素版本到正式表示的公式转换,反映了统计功效 1-𝛽 的考虑,如下所示。随着由 1-𝛽 指定的临界值添加到分子中,所需的样本量 增加,这很有意义,因为我们在推断中“购买”了更多的效率。

公式 10:样本大小估计中的从简单公式到标准公式(作者)
另一种看法是,A/B 测试所需的最小样本大小是上述简单版本的扩展,因为现在包含/控制了第二类错误𝛽。计算最小样本大小的逻辑仍然是假设检验的反向,只是我们需要考虑两种分布 H0 和 H1,并纳入第一类错误/显著性水*𝛼和第二类错误𝛽。
公式推导
因此,我们通过设定拒绝错误 H0 的概率小于预设的统计功效 (1-𝛽) 来建立我们的高阶公式,如下所示。
请注意,我们现在是在以 H1 作为统计量来源的实际分布进行条件化,而不是 H0,后者假设处理组和对照组之间没有差异。

公式 11:正式最小样本大小推导背后的高阶思路(作者)
计算最小所需样本大小需要4 个输入:
-
我们需要指定显著性水*𝛼
-
我们需要指定的统计功效1-𝛽
-
标准差𝜎,我们可以从历史数据中计算得出
-
期望的处理效应大小,即我们想要检测的两个组之间的差异𝑑𝑚𝑖𝑛 (μ2-μ1)。
回顾一下,样本均值差异统计量遵循正态分布,其均值为μ2-μ1,标准差为2𝜎/N的*方根。

公式 12:关键随机变量和检验统计量的分布特性(作者)
上述高阶公式的推导包括两个步骤。首先,我们使用𝛼表示x_crit,即当假设 H0 为真时拒绝原假设的临界值。其次,我们使用𝑑𝑚𝑖𝑛、𝛼和𝛽求解最小样本大小𝜨。

公式 13:给定𝑑𝑚𝑖𝑛、𝛼、𝛽和𝜎的𝜨推导(作者)
示例:“揭示”埃文的神奇 A/B 样本大小计算器
这个“简单”但优雅的 A/B 样本量计算工具,如图 4所示,是由 Evan Miller 创建并发布的。它在 Udacity 和 Google 的优秀 A/B 测试入门课程系列中被提及,因此成为了许多数据科学学生,包括我在内的首选 A/B 测试工具。虽然这些样本量计算工具非常方便使用,但人们可能会担心,并不会完全信任它们,因为通常没有具体解释输出背后的逻辑。因此,我希望提供一个简要说明,以帮助揭开帷幕,展示这些计算器是如何工作的。

图 4:Evan Miller 的样本量计算器
首先,Evan 的 A/B 测试样本量计算器专门针对z-比例检验,其中关键指标是比例(如果你对不同类型的统计测试感兴趣,我认为我写的另一篇文章 如何选择适合不同 A/B 指标的统计测试 会对你有帮助)。
值得注意的是,在估计比例指标的样本量时,我们不需要输入标准差。 这是因为像转化率这样的比例指标可以被视为所有用户转化状态的聚合。而且因为用户要么转化,要么未转化,用统计学语言来说,这是一个随机伯努利事件,记作Bernoulli(p)。因此,转化率的分析方差估计为𝑝∗(1-𝑝)。这一良好性质意味着我们“不需要”输入标准差来计算最小样本量,因为我们总是可以使用样本*均转化率𝑝来估计真实的分析标准差,而𝑝就是基线转化率,如 Evan 的计算器输入框中所述。
牢记这一重要概念,让我们深入了解这个工具(图 4)。该工具需要四个输入:
-
基线转化率 𝑝:假设当前转化水*为 20%。
-
最小可检测效应(MDE):这里的定义可能有些棘手,但概念很简单。有“绝对”MDE 和“相对”MDE。5%的绝对 MDE 表示我们计算的样本量只能检测 15%到 25%之间的范围,而相对 MDE 定义了可检测的范围为 20%(1–5%)和 20%(1+5%)。例如,如果我们的产品经理认为推出这个新产品功能会将转化率从 20%提高到 25%,那么这意味着我们应该将𝑑𝑚𝑖𝑛设置为 5%,或者将绝对 MDE 设置为 5%,或者将相对 MDE 设置为 25%,因为 20%*1/4=5%。
-
统计功效 1-𝛽
-
显著性水* 𝛼
将其整理成我们目前建立的公式:

公式 14:给定𝑑𝑚𝑖𝑛、𝛼、𝛽和𝜎(双侧)在 A/B 工具示例中的计算𝜨的说明(作者提供)。手动计算的样本量(1003)与 A/B 测试工具生成的结果(1030)略有不同,这可能是精度舍入差异造成的。

公式 14:给定𝑑𝑚𝑖𝑛、𝛼、𝛽和𝜎(双侧)在 A/B 工具示例中的计算𝜨的说明(作者提供)
说话便宜:
作者
实际问题
我认为公式 13是最小所需样本量的一般形式。以下实际问题部分讨论了对更具体情况的几个重要扩展。例如,当方差不等时,当指标是比例时,双侧测试使用时与经验法则公式的关系。
如何从我们的通用公式推导出经验法则样本量?
许多资源提到一个方便的公式,N等于16*𝜎2/𝑑𝑚𝑖𝑛。如果我们进行双侧测试并指定𝛼 = 0.05、𝛽 = 0.2,我们可以将两个 z 分数*方和乘以 2 的结果四舍五入到 16。有关更多详细信息,请参见《可信的在线对照实验》第 17.6 方程或Belle(2008)。

公式 15:从一般形式到经验法则形式(作者提供)
如何确定𝑑𝑚𝑖𝑛?
我个人认为,𝑑𝑚𝑖𝑛或最小可检测效应(MDE)应通过PM 和 DS 之间的密切讨论来确定。这是一个需要仔细沟通和商业直觉的艰难决定:
-
DS 可以提供与以下内容相关的见解:(1)类似历史实验的 MDE;(2)关键指标的波动幅度;(3)A/B 测试的可用流量/样本量。
-
PM 可以提供与以下内容相关的见解:(1)潜在处理效应的预期大小;(2)我们所拥有的信心量;(3)此测试处理的重要性/范围;(4)此测试处理的紧迫性/时间表。
可能需要反复沟通,因为更高统计功效与更长实验周期之间存在权衡。假设处理效应很微弱(即𝑑𝑚𝑖𝑛应设定为较小),且需要更高的统计功效来检测。在这种情况下,我们必须积累更多样本,因此在日常流量通常不变的情况下,预计需要更长时间。
尽管确定𝑑𝑚𝑖𝑛是棘手的,我建议将不同𝑑𝑚𝑖𝑛及其对应的样本量组织成表格,如表 2(原始 Google 表格可以通过 这里 )。我们数据科学家首先选择几个可能的 MDEs并计算相应的样本量以及所需天数。MDEs 可以随时轻松调整,表格为我们提供了一个直接的对比示例,使我们能够与产品经理就上述权衡进行沟通:

表 2:计算不同 MDEs(𝑑𝑚𝑖𝑛)下所需最小样本量的示例表(作者提供)
如果两组的方差和样本量不相等呢?
在观察性研究中,两组之间有不同方差是非常常见的。当同质方差假设不成立时,只需做一些微不足道的调整,即在调整公式时用处理组方差和对照组方差的总和替代 2*𝜎2。

公式 16:在方差不等情况下调整样本量公式(作者提供)
更重要的是,我们可以进一步释放隐含的假设,即处理组和对照组之间的流量是相等的。
正如你可能已经注意到的,以上展示的正态分布样本均值差异统计量的尺度是我们在网上帖子中经常看到的所谓汇总标准差的人口版本:

公式 17:汇总标准差公式(作者提供)
如何使用历史数据估计标准差𝜎?
在 A/B 测试场景中,为了评估潜在的流量及所需时间,样本量计算通常在实验开始前进行。计算的(唯一)资源是历史数据。因此,问题是我们需要多少历史数据,如何准备这些数据?
我们需要多少历史数据?
建议使用过去数据的大致相同时间长度作为预期的 A/B 测试周期。此外,需要注意的是,我们必须确保历史数据的范围与处理策略一致。否则,外部有效性无法保证。例如,如果产品经理想测试扩大“下一步”按钮的效果,并希望在两周内收集有关转化率是否增加的实验结果,那么我们需要收集过去两周内曾(应当)接触过该按钮的合格用户的转化率数据。
我们应如何准备这些数据?
除了选择正确的时间范围和应用适当的过滤器到历史数据中,我们还要确保数据以正确的方式进行聚合,以便分析单元与随机化单元相匹配。换句话说,如果流量是在用户层面随机分配的,而用于度量的源数据通常是用户行为层面的,我们需要按每个用户进行分组,并聚合他们的行为以计算度量,然后计算历史样本均值和标准差,这将用于计算所需的样本大小。
摘要
在这篇文章中,我们探讨了最小样本大小公式的推导,并广泛讨论了使用该公式的实际问题。你已学到:
-
如何推导 A/B 测试中的最小样本大小公式
-
如何理解许多在线样本大小计算器以及其他版本的最小样本大小公式。
-
如何使用这个公式来计算样本大小,通过将历史数据准备成正确的格式,与产品经理讨论以确定𝑑𝑚𝑖𝑛
这篇文章特别受到<可信的在线受控实验>(作者:Diane Tang, Ron Kohavi, 和 Ya Xu)、Emma Ding 的 YouTube 视频“如何估算 A/B 测试中的样本大小”以及我在 TikTok 工作中的经验的启发和基础。
参考文献
-
Glen, S. 统计中的样本大小(如何找到):Excel,Cochran 公式,常见技巧。StatisticsHowTo.com.
-
Kohavi, R., Tang, D., & Xu, Y. (2020). 可信的在线受控实验:A/B 测试的实用指南。在可信的在线受控实验:A/B 测试的实用指南(第 I 页)。剑桥:剑桥大学出版社。
-
Singh, A. S., & Masuku, M. B. (2014). 采样技术与应用统计研究中的样本量确定:概述。国际经济、商业与管理期刊, 2(11), 1–22.
-
Van Belle, G. (2008). 样本量。统计学经验法则, 27–51.
-
Yamane, T. (1973). 统计学:入门分析-3.
使用广义加性模型(GAMs)生成见解
学习如何解释广义加性模型(GAMs)并从数据中提取有用的见解
·
关注 发表在 Towards Data Science ·10 分钟阅读·2023 年 1 月 30 日
--
今天我们将学习如何使用广义加性模型来预测 2011 年至 2012 年间华盛顿特区的自行车租赁数量。该数据集由共享单车公司 Capital Bikeshare 提供。共享单车系统是一种新一代服务,允许用户在方便的位置取车和还车,从而促进零排放的交通,具有对交通、环境和健康问题的积极影响。
GAMs 是什么?
“广义加性模型是一个广义线性模型,其线性预测变量涉及协变量*滑函数的总和”(Wood, 2017)。
GAMs(广义加性模型)通过添加协变量的估计权重来与线性回归相同。最大的区别在于,这些权重代表的是灵活的关系,而不仅仅是线性的关系,我们使用链接函数来建模目标变量。
我们可以将这种模型用于不同的应用:使用泊松分布建模疾病的传播,基于数值/分类变量使用二项分布(逻辑回归)预测患者是否有某种疾病,使用纬度和经度数据研究物种在某区域的空间行为。GAMs 是一个多功能的框架,可以应用于几乎任何领域。
在我们的背景下,我们可以创建一个模型来解释由于时间、湿度和温度的变化,租赁自行车的数量是如何变化的。此外,租赁自行车的数量表现得像一个正态分布。
我们可以正式写出我们的模型:

示例 GAM 的数学表达式
函数 f1、f2 和 f3 允许我们建模目标变量与解释变量之间的灵活关系。最终,这些关系的估计权重之和就是预测/估计的租赁自行车数量。在训练模型之前,我们将描述数据集,并开始进行一些探索性数据分析,以决定使用哪些解释变量。
加载库
# Basic wrangling functions
library("dplyr")
# Beautiful plots and data exploration
library("ggplot2")
# Comparing variables and data exploration
library("GGally")
# Library to fit gams
library("mgcv")
# Modern library for visualizing and assessing gams
library("gratia")
数据集
我们从 Interpretable Machine Learning 书籍的 GitHub 仓库下载数据集,这些数据已被预处理,并且准备好供我们使用。让我们解释一下每个变量:
-
season: 年中的季节。
-
holiday: 那天是否为假日。
-
workingday: 这一天是否为工作日,本质上是周末与否。
-
weathersit: 那天的天气情况,有三种类别。
-
temp: 温度,以摄氏度计。
-
hum: 相对湿度百分比。
-
windspeed: 风速,以 km/h 计。
-
days_since_2011: 一个时间步长变量,用于考虑时间的流逝。
-
cnt: 租赁的自行车数量。
-
weekday: 一周中的天
我们将使用下面的函数并总结感兴趣的变量。
get.bike.data = function(){
url = "https://raw.githubusercontent.com/christophM/interpretable-ml-book/master/data/bike.csv"
# Download file and save it as bikes.csv in our current folder
file = download.file(url, "bikes.csv")
# Read the file and return it as a data frame
bike = read.csv('bikes.csv', stringsAsFactors = T) %>% data.frame()
}
# Relevant variables
variables.of.interest = c('season','holiday', 'workingday', 'weathersit', 'temp', 'hum', 'windspeed', 'days_since_2011', "cnt", "weekday")
# Read data and extract variables of interest
bikes = get.bike.data() %>% dplyr::select(variables.of.interest)
# Summarise data
summary(bikes)

summary 函数的输出
我们巧妙地用一个函数总结了我们的数据,我们拥有每天的分类和数值数据。我们的数据集包含 730 条记录,大约是两年的数据。
数据探索
让我们开始分析数值变量,以检查它们是否对租赁自行车数量有影响。
# Compare each variable against the other
ggpairs(bikes %>% select(c(temp, hum, windspeed, cnt))) +
labs(subtitle = "Numeric variable exploration") +
my_theme()

图 1. 检查变量相关性
我们使用了ggpairs函数来创建这个图形,在这里我们可以可视化变量之间的相互影响并发现模式。总体而言,我们可以看到温度对自行车租赁数量有正面影响(见左下角的图)。让我们进一步探讨温度对其他解释天气情况和日期的分类变量的影响。

图 2. 按季节划分的天气情况及其对租赁自行车数量的影响
无论季节如何,只要天气好,租赁自行车的数量就会比天气差时高。例如,在冬季有三种天气情况,但总体而言,当天气不好时,租赁自行车的数量较低。此外,与冬季相比,夏季的租赁自行车数量显著较高。也许我们应该考虑天气情况对模型的影响。

图 3. 温度在不同季节和天气情况中的影响
左图确认了我们的发现,表明良好的天气显著有助于增加租赁自行车的数量。另一方面,右图显示温度对较冷的季节如秋冬有正面影响。有趣的是,当温度接* 30 摄氏度时,夏季出现了轻微的负面趋势,表明一些骑行者不愿意在高温下骑行。
训练广义加法模型(GAM)?
在 R 中训练/拟合广义加法模型(GAM),我们将使用mgcv库。它是一个功能强大且维护良好的库。
M = gam(cnt ~ season + weathersit + s(days_since_2011, bs ="cr", k = 70) +
s(temp, bs = "cr", by = season, k = 15), data = bikes, )
我们的模型将预测租赁自行车数量,并考虑以下因素:
-
weathersit: 每种天气情况将估算一个不同的截距。什么是截距?截距是指在其他所有变量设为零的情况下,基于给定天气情况的*均租赁自行车数量。
-
s(days_since_2011, bs = “cr”, k = 70): 这个术语表示我们将使用三次回归样条(*滑函数)来估算租赁自行车数量随时间变化的情况。k值是函数的阶数,决定了它的灵活性。稍后会变得清晰。
-
s(temp, by = season, k = 15): 这个术语估算了在不同季节中温度对租赁自行车数量的影响。由于 k 值较小,它不如上述术语灵活。
-
season: 每个季节都有不同的截距,因为我们使用了一个估算季节温度的术语。
所有这些项的总和将导致我们模型的预测结果。
总结和检查我们的模型
# Summarize model
summary(M)

图 4. 模型摘要
首先,图 4. 的模型摘要告诉我们目标变量的分布和链接函数。在我们的案例中:高斯(正态)和恒等函数(变量未改变)。我们有参数系数或截距,为每个季节和每种天气情况计算了一个截距(也称为参数效应)。在下一部分中,展示了我们*滑函数的有效自由度。这些值告诉我们关系的灵活性(本质上是它们与线性关系的差异,线性关系的 edf=1)。最后,我们有解释的偏差,在我们的案例中是 88%。我们的模型解释了 88%的数据。
# Checking k-value and edf
k.check(M)

图 5. 检查模型的灵活性
图 5. 中的下一个模型检查与*滑函数的秩有关。例如,第一个解释自行车数量与时间关系的*滑函数被赋予了 70 的秩。其中一个秩用于计算截距,其余的用于建模关系。edf 告诉我们所使用函数使用了多少可用的灵活性(69 个秩)。作为一个好的经验法则,我们希望 k 大于 edf,k-index 大于 1。我们还希望 p 值较大,而不是较小,这通常是情况。在我们的案例中,我们未能获得后两者,但我们已给函数足够的灵活性,如 edf 所示。
解释*滑效应
本节将展示*滑函数如何建模解释变量与租赁自行车数量之间的复杂关系。以下图形可以使用以下函数创建:
# Plot smooth and parametric effects
draw(M, parametric = TRUE)

图 6. 时间与租赁自行车之间的非线性关系
需要注意的一点是,所有的部分效应图都以均值为中心,阴影区域表示 95%的置信区间。这意味着 y 轴上显示的增减反映了租赁自行车的*均预测值。例如,可以通过陈述在前半段时间内自行车数量低于*均值来解释图 6.。我们还可以看到,由于所使用函数的 30 个有效自由度,关系是相当灵活的。

图 7. 温度对秋季租赁自行车的影响
图 7 中温度的影响没有很大的灵活性,但我们可以清晰地看到一些“波动”。 一个重要方面是 x 轴上的 rug 图,它显示了数据点的数量。当没有数据点时,置信区间会显著增大,因为我们不确定关系。我们可以通过说在秋季,温度的升高会增加租赁自行车的数量来解释此图。更准确地说,当温度约为 5 摄氏度时,预测的自行车数量比*均值低——1200(低 1200)。

图 8. 春季温度对租赁自行车的影响
在春季,温度的变化方式不同,预测的自行车数量在温度从 4 摄氏度升高到 20 摄氏度时急剧上升,但之后预测的自行车数量开始下降。我们需要仔细考虑较高温度值的的不确定性,rug 图表明数据点不多。如果有更多数据,这部分图表可能会显示*滑曲线?

图 9. 夏季温度对租赁自行车的影响
夏季非常有趣,因为我们看到在温暖的日子里数量下降。在图 9 中,我们可以看到温度超过 30 摄氏度对预测的租赁自行车数量有负面影响,低于预测*均值约——2500。这可能是因为骑行者不愿意在非常热的日子里骑车。

图 10. 冬季温度对租赁自行车的影响
最后,在冬季,随着温度的升高,预测的租赁自行车数量增加。注意图 10 显示了一个几乎线性的关系,与模型总结中的 1.24 edf 相符。虽然它的灵活性不如其他*滑函数,但按季节建模温度似乎是合适的。我们可以通过说在冬季 15 摄氏度时,预测的自行车数量大致与*均值相同来解释此图。
接下来我们将解释估计的截距或参数效应。
解释参数效应

图 11. 一年四季对租赁自行车的影响
图 11. 显示了带有 95% 置信区间的估计参数效应。我们可以通过陈述春季预测的自行车数量比秋季低 1489 辆来解读此图。我们对这一陈述充满信心,因为置信区间不包括零(我们的比较点)。如果预测也包括秋季的预测数据,春季的效应将不会显著,对吗?我们可以对冬季对预测自行车数量的影响做出相同的陈述。另一方面,夏季的效应不显著,因为其置信区间相当大并且包括零。这表明,在夏季,我们的模型预测的租用自行车数量也可以在秋季预测。我们可以从中提取一个见解:租用自行车数量的增加不是因为夏季,而是因为温度逐渐变暖。

图 12. 天气情况对租用自行车的影响
最后,图 12. 告诉我们,当天气雾霾或下雨/下雪/暴风雨时,预测自行车数量的变化是显著的(因为置信区间不包括零)。在这种情况下,效应是负面的,因为我们可以说,在雾霾天气的日子里,预测的租用自行车数量比良好天气的日子少 690 辆。
结论
我们训练了一个广义加性模型,以预测华盛顿特区的租用自行车数量,基于时间变化、季节中的温度变化和整体天气情况。我们学习了如何读取模型摘要并检查有效自由度以获得正确的拟合。最后,我们解释了模型的*滑效应和参数效应,以理解是什么驱动了租用自行车的数量,并获得了以下见解:
-
骑自行车的人在夏季气温过高时(高于约 25 摄氏度)通常会避免租用自行车。
-
租车数量的上升发生在天气温和、气温开始变暖的季节(秋季和冬季)。
-
我们对雨雪是负面影响租用自行车数量的显著因素充满信心。
总之,广义加性模型是一种强大的机器学习框架,具有很好的解释性,可以用于从数据中提取见解。将噪声转化为句子。
除非另有说明,所有图片均由作者提供。
参考文献
Wood, S.N., 2017. 广义加性模型:R 语言入门. 第二版。Chapman and hall/CRC.
代码
可以在我的 GitHub 仓库中找到:
github.com/alvarofps/Producing-Insights-with-GAMs
产品经理必须决定:功能还是用户设计
原文:
towardsdatascience.com/product-managers-must-decide-features-or-user-design-e3a14a27859
观点
产品经理可以优化用户设计或功能开发。不能两者兼顾。
·发表于 Towards Data Science ·阅读时间 7 分钟·2023 年 1 月 18 日
--

图片由 Austin Distel 提供,来自 Unsplash
市场的二分化似乎在许多应用中都是一个普遍的主题。无论是在新闻报道中的左右之分,对数据科学家的需求,还是折扣与奢侈零售之间的分歧;稳定状态通常会定格在极端位置。当涉及到软件开发时,我注意到的两个极端是优化功能开发与优化用户设计。如果你的产品处于两者之间,你可能不会成功。

作者提供的图片
为什么只能选择一个?
之所以趋向极端,是因为这两个开发目标有着正交的路径。由于项目管理固有的约束(见下文),开发高质量的软件产品有一定的局限性。一个团队在给定的时间框架内只能优先处理有限数量的任务。这些约束,再加上你的定价和销售策略,将迫使开发在这两个极端之间做出选择。

作者提供的图片
让我们假设一个早期阶段的科技初创公司正在决定构建 B2B 项目管理软件的最佳开发路径。他们有 10 名员工负责在 12 个月内发布软件的 beta 版本。对于这个软件产品的最早决策之一是选择它是一个 web 应用程序、移动应用程序、两者之一,还是两者都有(即桌面 vs. 手机)。这一决定影响了技术栈,从而产生巨大的级联效应。如果你只追求 web 应用程序,但想要整合多个外部软件,这将需要更复杂的工程工作。这将减少优化用户设计的时间。另一方面,如果你希望有一个 web 应用程序和一个本地 iOS 移动应用程序,这通常需要两个 不同 的前端框架。由于前端工作量基本上翻倍,这减少了添加功能的时间。
在另一个工作流中,正在讨论最佳定价和销售策略。软件产品的复杂性,通常与功能数量相关,直接影响最佳销售策略。产品越复杂,推动以产品为主导的增长策略就越困难,这种策略通常强调自助获取用户。复杂的软件产品通常需要客户演示,定价更高,并且可能受益于第三方部署合作伙伴。优化用户设计而非功能的简单产品更容易实现以产品为主导的增长策略,因为用户会快速学会使用这些产品。虽然其中一些决策可能会被延迟,但在开发过程中,你的决策会更倾向于在功能开发与用户设计之间做出权衡。
根据个人经验,我曾积极参与选择某组织的企业实验室信息管理系统(LIMS)。这涉及了一个漫长的信息收集过程,我们研究了 LIMS 选项,接收了一些供应商的演示,并采访了用户以了解他们的需求。在所有这些工作之后,我们的决策最终选择了具有最多功能的 LIMS 供应商和具有最佳用户设计的供应商。也许了解这个框架可以节省一些时间。
为什么市场奖励极端?
在 Startup=Growth 中,Paul Graham 说道:“如果你有这样的想法,但增长不够快,竞争对手会更快地增长。在具有网络效应的业务中,增长过慢尤其危险,而最好的初创公司通常在某种程度上具备这种效应。”
我相信,由于网络效应和指数增长,软件产品市场趋向于两个极端。优化功能或用户设计将导致用户采纳的增长速度比选择两者组合更快,因为增长不是线性的。

作者插图
功能优化的产品将满足更多样化团队的需求,并增加未来需求的选择性,因为它比竞争对手有更多的功能。而简单的产品可能只适用于特定的利基市场,复杂的产品则可能满足多个部门类型或行业的需求。此外,开发更多功能通常需要依赖外部软件集成。团队的多样性,加上外部软件集成,将使产品接触到更大的潜在客户市场。每个新用户接触到产品都是免费的广告,推动增长循环。
用户设计优化的产品有几个固有优势。当与自助式免费增值模式(即产品驱动增长)结合时,简洁性将转换更多潜在客户,因为产品更容易学习。简洁性减少了用户获取过程中的摩擦。在已经使用产品的组织中,直观的产品将受益于增加的用户采纳。用户喜欢易于使用的产品。以简洁性为核心,用户池将增长,因为它也包括了较少技术用户。回想当我的技术挑战型父母准备购买他们的第一部智能手机时,我立刻推荐了 iPhone,因为史蒂夫·乔布斯对优化用户设计情有独钟。
何时应该专注于功能与用户设计?
开发策略应保持灵活,并尽可能保留选择权。当需要选择路径时,有许多因素我会考虑。
人才
您的员工在什么技术上最为擅长?如果您想开发一个本地移动应用,通常需要使用某些专门的语言。如果您想要一个具有无限外部集成功能的网页应用,那么您可能需要更多的全栈或后端工程师。您组织内的才能和/或您能负担得起的才能可能会决定您的策略。
目标消费者
您的目标消费者是谁?您是针对技术精通的千禧一代消费者,还是您的用户技术能力较差?如果您正在打造面向消费者的品牌,那么美学和简洁性可能会比 B2B 产品更为重要。
竞争
您是该行业的先行者,还是已经有几个直接和间接竞争者?竞争较少会给您更多选择,而竞争增加会促使您成为领导者的对立面以实现增长。
销售和定价策略
你打算如何获取和转化用户?你打算向用户收取多少费用?以产品为导向的增长战略对于更复杂的软件产品来说会更具挑战性,但也不是不可能的。Gitlab 前增长总监 Hila Qu 在一篇*期文章中深入探讨了这两种策略之间的权衡。
什么时候不重要?
分发能力 对于这一优化理论来说,至少有一个关键的例外——微软。当微软进入你的行业时,你可以把特性和用户设计扔到一边。看看 Teams 和 Slack 的对比。Slack 在几乎所有方面都是客观上更好的软件产品。然而,这些都无关紧要,因为 Microsoft Office/365 在组织中的嵌入程度如此之深,Teams 成为了大多数组织的预算考虑选择。分发能力让组织能够打开发展手册,选择自己的发展路径。
AI/ML 尽管大多数情况下特性开发和用户设计改进会被迫分开进行,但也有一些例外。推荐系统是许多产品中的常见 AI/ML 应用,我认为这是一个复杂的特性扩展,可以改善用户设计。一个大家熟悉的例子是 Twitter,当你选择关注某个人时,它会触发额外推荐的其他人关注。这个过程使得发现你更可能喜欢的其他账户变得更容易,这是一种用户体验的改进。然而,别搞错了,将 AI/ML 融入应用程序的后台可不是一件容易的事。

Twitter 推荐关注示例
结论
- 虽然有一些例外,但我注意到市场往往更青睐那些针对特性或用户设计进行了优化的软件产品——而不是两者兼顾。如果增长是主要目标,那么确定优化哪一条路径应当是优先考虑的事项。两者兼顾只会将用户转向那些正确优化的竞争对手。毕竟,独角兽之所以稀有是有原因的。
- 数据通才
使用无服务器容器服务将机器学习模型生产化
如何使用 Azure 容器应用程序创建无服务器容器化推理端点以用于你的机器学习模型
·发表于 Towards Data Science ·7 分钟阅读·2023 年 1 月 9 日
--

照片由 Jan Canty 提供,发布在 Unsplash 上。
介绍
无服务器容器架构是一种构建和运行容器化应用程序和服务的方法,无需管理底层基础设施。在这种架构中,容器用于打包和部署应用程序,这些容器在云服务提供商提供的完全托管环境中运行。
云服务提供商负责运行容器所需的基础设施,如硬件和操作系统。开发者无需担心设置或维护这些基础设施,可以专注于编写代码和构建应用程序。容器通常在集群中运行,云服务提供商会根据需求自动扩展容器的数量。这使得应用程序能够处理流量波动,而无需手动干预。无服务器架构可能比传统架构更具成本效益,因为用户仅需为实际使用的资源付费,而不是为可能未完全利用的固定计算能力付费。
一些无服务器容器服务的例子包括 Azure Functions、Azure Container Apps、AWS Lambda 和 Google Cloud Functions。本文将演示如何利用 Azure Container Apps,这是一个完全托管的无服务器容器服务,用于大规模构建和部署应用程序,用于生产化机器学习模型。Azure Container Apps 的常见用途包括部署 API 端点、托管后台处理应用程序、处理事件驱动的处理和运行微服务 [2]。
这些步骤将帮助我们训练并部署一个 scikit-learn 模型到 Azure Container Apps。
-
在本地训练模型
-
使用 FastAPI 创建推断 API
-
对应用程序进行 Docker 化
-
部署到 Azure Container Apps
0. 设置
这是以下示例使用的设置。
开发环境
-
Visual Studio Code
-
Azure CLI
-
Python 3.8
-
Docker
-
Python 包:有关
requirements.txt的信息,请参阅第三部分
项目结构
项目文件夹结构如下:
FastAPI-Azure-Container-Apps
├─ fastapp
│ └─ app.py
├─ model
│ ├─ heart-disease.joblib
│ └─ train.py
├─ data
│ └─ heart-disease.csv
├─ Dockerfile
├─ requirements.txt
└─ .dockerignore
数据集
UCI 心脏病数据集 [3] 是一个公共数据集,包含关于被诊断为心脏病的患者的数据。它包括各种患者特征,如年龄、性别、血压和胆固醇水*。1 和 0 值在 condition 列中分别表示心脏病的存在与否。
1. 在本地训练模型
文件:train.py
为了演示目的,我们将使用仅 5 个特征来训练一个梯度提升分类器。
import pathlib
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import GradientBoostingClassifier
from joblib import dump
print ('Load Data')
df = pd.read_csv(pathlib.Path('data/heart-disease.csv'))
y = df.pop('condition')
X = df.loc[:, ['age', 'sex', 'cp', 'trestbps', 'chol']].to_numpy()
print ('Train-Test Split')
X_train, X_test, y_train, y_test = train_test_split(X,y, test_size = 0.2)
print ('Train Model')
gbc = GradientBoostingClassifier()
gbc.fit(X_train, y_train)
print ('Save Model')
dump(gbc, pathlib.Path('model/heart-disease.joblib'))
使用 CLI 运行训练:
python model/train.py
2. 使用 FastAPI 创建预测端点
文件:app.py
from fastapi import FastAPI
from pydantic import BaseModel
import numpy as np
from joblib import load
import pathlib
app = FastAPI(title = 'Heart Disesase Prediction', version = '0.1.0')
model = load(pathlib.Path('model/heart-disease.joblib'))
class ModelData(BaseModel):
age:int=30
sex:int=0
cp:int=2
trestbps:int=115
chol:int=0
class ResponseData(BaseModel):
prediction_result:float=0.1
@app.post('/predict', response_model = ResponseData)
def predict(data:ModelData):
input_data = np.array([v for k,v in data.dict().items()]).reshape(1,-1)
prediction_result = model.predict_proba(input_data)[:,-1]
return {'prediction_result':prediction_result}
在 app.py 文件中我们
-
定义
app,这是 FastAPI 的一个实例。 -
加载训练好的模型。
-
定义 API 接受的输入数据格式(
ModelData) -
定义 API 响应格式(
ResponseData) -
定义
/predict路由,当对该路由发出 POST 请求时,将触发predict函数。 -
predict函数接收来自 POST 请求的输入数据,进行预测并返回患者患有心脏病的概率。
此时我们可以在本地测试 FastAPI 应用程序。--reload 标志有助于加速开发过程。FastAPI 会在检测到代码更改时自动重新加载,这意味着开发人员不需要手动重启 FastAPI 来测试代码更改。
# CLI
uvicorn fastapp.app:app --reload
你将在终端上看到以下消息:
INFO: Uvicorn running on <http://127.0.0.1:8000> (Press CTRL+C to quit)
给定的 URL 会将我们带到 Swagger UI,我们可以在这里测试 API。
3. 使用 Docker 对应用程序进行容器化
创建一个 **requirements.txt** 文件
requirements.txt 文件包含所有所需的 python 包。
fastapi>=0.68.0,<0.69.0
pydantic>=1.8.0,<2.0.0
uvicorn>=0.15.0,<0.16.0
numpy == 1.19.5
scikit-learn==0.23.2
joblib==1.1.0
nest_asyncio == 1.5.5
创建一个 **.dockerignore** 文件
.dockerignore 文件的目的是避免复制那些不用于推断的文件,例如训练脚本和数据。
data/
model/train.py
创建一个 **Dockerfile** 文件
FROM python:3.8
WORKDIR /app
COPY . /app
RUN pip install -r requirements.txt
CMD ["uvicorn", "fastapp.app:app", "--host", "0.0.0.0", "--port", "80"]
这是 Dockerfile 的简要描述:
-
使用
python:3.8作为基础镜像 -
创建一个名为
/app的工作目录 -
将项目文件夹中的所有文件复制到工作目录,除了
.dockerignore文件中列出的文件或子目录。 -
安装
requirements.txt中列出的 Python 包 -
CMD在启动 Docker 容器时在 80 端口运行 FastAPI 应用。与本地测试不同,在这里运行 uvicorn 时不包括 --reload 标志。虽然 reload 标志对加快开发过程有帮助,但在生产环境中并不需要。
构建 Docker 镜像
docker build . -t heart-disease-app
启动 Docker 容器
我们将容器中 FastAPI 运行的 80 端口映射到 Docker 主机上的 8080 端口。
docker run -p 8080:80 -it heart-disease-app
测试应用
此时,我们可以通过访问以下 URL 再次通过 Swagger UI 测试应用:[127.0.0.1:8080/docs](http://127.0.0.1:8080/docs)
4. 部署到 Azure 容器应用
在本节中,我们将把 Docker 镜像推送到 Azure 容器注册表,然后在 Azure 容器应用中部署 Docker 容器。
要将容器化应用部署到 Azure 容器应用,我们需要满足以下先决条件:
-
Azure 资源组
-
Azure 容器注册表
-
Azure 容器应用环境
以下命令将在命令行中执行。
创建资源组
资源组是用于支持应用的 Azure 服务的逻辑分组。我们在 eastus 区域创建了一个 heartdisease_rg 资源组。所有后续资源将分配给 heartdisease_rg。
az group create --location eastus --name heartdisease_rg
创建 Azure 容器注册表
Azure 容器注册表(ACR)是一个用于存储和管理容器镜像的仓库集合。我们在 heartdisease_rg 资源组下创建了一个名为 heartdisease 的容器注册表,并选择了 Basic SKU 定价计划。
az acr create --name heartdisease --resource-group heartdisease_rg --sku Basic
一旦容器注册表被配置,我们可以使用以下命令检查 ACR 登录服务器
az acr list --resource-group heartdisease_rg
上述命令返回一个包含登录服务器的长字符串。请注意登录服务器的详细信息,因为我们将在下一步中使用它。
...
"location": "eastus",
"loginServer": "heartdisease.azurecr.io",
"name": "heartdisease"
...
标记 Docker 镜像
要将本地 Docker 镜像推送到 ACR,Docker 镜像 heart-disease-app 被标记为 {login server}/{docker image name}/{version} 格式。
docker tag heart-disease-app heartdisease.azurecr.io/heart-disease-app:v0.1.0
登录 ACR
确保在推送镜像到 ACR 之前已登录。
az acr login -n heartdisease
将 Docker 镜像推送到 ACR
Docker push 是一个将本地 Docker 镜像上传到容器注册表的命令。这将使 Docker 镜像可供 Azure 容器应用使用。
docker push heartdisease.azurecr.io/heart-disease-app:v0.1.0
成功推送 Docker 镜像后,镜像将在 ACR 的 UI 中显示。

图片由作者提供。
创建 Azure 容器应用环境
在创建 Azure 容器应用之前,我们定义了容器应用将运行的 heartdiseaseenv 环境。
az containerapp env create --name heartdiseaseenv --resource-group heartdisease_rg --location eastus
创建 Azure 容器应用
在这一步中,我们创建了 heart-disease-container-app Azure 容器应用,使用的是前一步中创建的 heartdiseaseenv 环境。我们还定义了应使用的 Docker 镜像:heartdisease.azurecr.io/heart-disease-app:v0.1.0 和容器注册表的登录服务器:heartdisease.azurecr.io。ingress 设置为 external,因为此 API 旨在公开发布到互联网。
az containerapp create --name heart-disease-container-app --resource-group heartdisease_rg --environment heartdiseaseenv --image heartdisease.azurecr.io/heart-disease-app:v0.1.0 --target-port 80 --ingress external --query properties.configuration.ingress.fqdn --registry-identity system --registry-server heartdisease.azurecr.io
如果 az containerapp create 命令成功,它将返回一个访问应用程序的 URL。
Container app created. Access your app at <https://heart-disease-container-app.nicehill-f0509673.eastus.azurecontainerapps.io/>
测试应用程序
我们可以使用 Swagger UI、curl 或 python 请求来测试应用程序。要访问 Swagger UI,只需在给定 URL 的末尾添加 docs:[heart-disease-container-app.nicehill-f0509673.eastus.azurecontainerapps.io/docs](https://heart-disease-container-app.nicehill-f0509673.eastus.azurecontainerapps.io/docs.).
使用 CURL,命令如下:
curl -X 'POST' \\
'<https://heart-disease-container-app.nicehill-f0509673.eastus.azurecontainerapps.io/predict>' \\
-H 'accept: application/json' \\
-H 'Content-Type: application/json' \\
-d '{
"age": 64,
"sex": 1,
"cp": 3,
"trestbps": 120,
"chol": 267
}'
我们也可以通过以下方式使用 Python 的请求库向预测端点 https://heart-disease-container-app.nicehill-f0509673.eastus.azurecontainerapps.io/predict 发送 POST 请求:
import requests
response = requests.post(url = '<https://heart-disease-container-app.nicehill-f0509673.eastus.azurecontainerapps.io/predict>',
json = {"age": 64,
"sex": 1,
"cp": 3,
"trestbps": 120,
"chol": 267
)
print (response.json())
# {'prediction_result': 0.8298846604381431}
结论
在本文中,我们讨论了使用无服务器容器化机器学习推理端点的优势,并演示了如何使用 FastAPI 创建 API 端点,用 Docker 容器化并使用 Azure 容器应用程序部署容器化应用程序的示例。
加入 medium 阅读更多类似的文章!
参考文献
[1] 无服务器计算和应用 | Microsoft Azure
[2] Azure 容器应用概述 | Microsoft Learn
[3] 心脏病数据集来自 UCI 机器学习库。根据 CC BY 4.0 许可协议授权。
工作效率技巧、数据职业见解及其他*期必读内容
·
关注 发表在 Towards Data Science · 作为 时事通讯 发送 · 阅读需 4 分钟 · 2023 年 10 月 26 日
--
数据科学是一个快速发展的领域,新工具不断出现,工作流程不断演变,职业路径也在迅速变化——有时仅在几周之内。
我们最受读者关注和讨论的文章反映了这些趋势,读者纷纷涌向那些由数据和机器学习专业人士撰写的优秀文章,这些专业人士根据他们的实际经验分享了深刻的见解。为了确保你不会错过我们最好的文章,我们很高兴分享一些过去一个月的亮点故事。这些文章涵盖了从编程到 LLM,再到数据讲述的广泛内容,但都专注于可操作的、第一手的建议。请欣赏!
-
编程曾经很难,直到我学会了这两件事 如何从“有志编程者”转变为能够实际竞争优秀编程职位的人?Natassha Selvaraj 的热门文章探讨了培养成长心态和建立日常编程习惯的实际方法。
-
6 种扼杀数据科学生产力的坏习惯 Donato Riccio 指出,提高生产力不仅仅是关于学习和做更多的事情;避免或打破那些对你的工作有害的习惯同样重要。Donato 关注的这些习惯尤其与数据科学家的日常工作流程密切相关。
-
忘掉 RAG,未来是 RAG-Fusion 检索增强生成已经成为优化大型语言模型的常见方法,但它也存在重大缺陷。Adrian H. Raudaschl 提出了 RAG-Fusion,这是一种经过修改的技术,通过将互反排名融合和生成查询纳入过程来解决这些挑战。

图片由 engin akyurt 提供,来源于 Unsplash
-
介绍 KeyLLM — 使用 LLM 进行关键词提取 在提高 LLM 效率的话题上,Maarten Grootendorst 最*分享了 KeyLLM 的发布消息,这是他对 KeyBERT 包的扩展,旨在大规模进行关键词提取。他随后通过一个基于开源 Mistral 7B 模型的示例为我们演示了其使用方法。
-
如何成为数据工程师如果你是一个初级 IT 从业者或中级软件工程师,想要职业转型,💡迈克·沙霍米罗夫的实践指南是一个很好的资源来探索数据工程角色的转变。
-
在远程工作的时代培养新的数据科学家远程和混合工作模式的转变对早期职业数据科学家产生了什么影响?斯蒂芬妮·基尔默提供了对雇主和员工在这一(相对)新领域面临挑战的深思,及他们可以采取什么措施以确保下一代数据专业人士仍能从经验丰富的前辈那里获益。
-
TimesNet:时间序列预测的最新进展跟上时间序列分析领域的最新前沿研究:马尔科·佩谢罗的最新解释聚焦于今年早些时候发布的论文中揭示的 TimesNet。这个模型利用基于 CNN 的架构在不同任务中实现了最先进的结果,“使其成为时间序列分析的基础模型的优秀候选者。”
-
公司今天可以实施的 5 种生成 AI 用例有时存在热议,有时存在实际价值——而在生成 AI 工具方面,商业领导者很难分辨两者的不同。巴尔·摩西为此提供了解救,概述了五个有前景的用例,展示了生成 AI 方法在公司中可能进行实验的意义。
-
Excel 中的互动仪表板如果你在寻找新的创意方式来以引人入胜和易于访问的方式展示你的数据,为什么不尝试一下 Excel?杰克·邱的逐步教程解释了如何充分利用“在‘非技术’世界中最广泛使用的数据工程和分析软件”来创建时尚的互动仪表板。
-
战略数据分析(第一部分) 在她最*推出的系列中,Viyaleta Apgar 提供了一个结构化、详细的概述,介绍了数据分析师需要回答的问题以及他们可以使用的各种有效方法。如果你还没有阅读过,我们推荐从头开始:第一部分概述了数据分析师可能处理的四种基本问题类型。(或者也可以跳过到第二部分,该部分侧重于描述性问题。)
我们最新的一批新作者
每个月,我们都很高兴看到一批新的作者加入 TDS,他们各自以独特的声音、知识和经验与我们的社区分享。如果你在寻找新的作家来探索和关注,只需浏览我们最新加入的作者的作品,包括 Daniel Warfield、Satwiki De、Samuel Montgomery、Alexander Nikitin、Aman Steinberg、Hamed Seyed-allaei、Matheus Cammarosano Hidalgo、Malte Bleeker、Christopher Karg、Akif Mustafa、Gabriel Moreira、Jake Teo、Ilia Teimouri、Jeremie Charlet、Ed Izaguirre、Silvia Onofrei、Markus Stadi、Kairo Morton、Josu Diaz de Arcaya、Deepsha Menghani、Jon Flynn、Lennart Langouche、Guillaume Colley、Angjelin Hila、Emmanouil Karystinaios、Sofia Rosa、Anthony Alcaraz、Kseniia Baidina、Kenneth Ball和 Nicholaus Lawson。
感谢您支持我们作者的工作!如果您喜欢在 TDS 上阅读的文章,可以考虑成为 Medium 会员——这将解锁我们所有的档案(以及 Medium 上的其他所有帖子)。
直到下一个 Variable,
TDS 编辑部
使用 cProfile 对 Python 代码进行性能分析
原文:
towardsdatascience.com/profiling-python-code-with-cprofile-328ae152fdfc
在这篇文章中,我们将探索如何使用 cProfile 模块对 Python 代码进行性能分析
·发表于 Towards Data Science ·阅读时长 6 分钟·2023 年 2 月 10 日
--

图片由 Arnold Francisca 提供,来源于 Unsplash
目录
-
介绍
-
什么是代码性能分析?
-
cProfile 基本使用
-
从终端使用 cProfile 对 Python 代码进行性能分析
-
使用 cProfile 对 Python 代码进行性能分析
-
使用 cProfile 在 Python 中对函数进行性能分析
-
导出 cProfile 数据
-
结论
介绍
现在程序员可以在几天内编写成千上万行代码。新程序和应用程序的复杂性不断演变,代码库包含多个函数,其中一些可能会减慢整个程序的性能。
专注于代码性能分析,特别是对你的 Python 代码进行性能分析以识别性能瓶颈,可以显著提高软件的性能,并改善用户体验。
在本教程中,我们将使用 Python 内置的 cProfile 模块,该模块提供 Python 程序的确定性性能分析。
什么是代码性能分析?
代码性能分析是分析程序性能的过程,特别是分析代码性能以识别潜在的瓶颈。
确定代码中运行缓慢的部分并优化这些代码可以显著提高软件的性能,减少内存使用和资源消耗。
在 Python 中,cProfile 性能分析工具可以跟踪 Python 脚本的执行时间和内存使用情况,帮助识别运行缓慢和高资源消耗的代码部分。
cProfile 基本使用
这是一个使用 cProfile 进行性能分析和解释输出的快速示例:
import cProfile
import re
cProfile.run("re.compile('Python')")
你应该得到:
Python
4 function calls in 0.000 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.000 0.000 :1()
1 0.000 0.000 0.000 0.000 {built-in method builtins.exec}
1 0.000 0.000 0.000 0.000 {built-in method builtins.print}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}Now let’s interpret the output:
现在让我们来解释输出结果:
-
ncalls — 调用次数
-
tottime — 在给定函数中花费的总时间
-
percall — tottime 和 ncalls 的比率
-
cumtime — 当前函数及其子函数的累计时间
-
percall — cumtime 和原始调用的比率
-
filename — 每个函数的数据
从终端使用 cProfile 对 Python 代码进行性能分析
在开始 Python 代码性能分析之前,我们需要一些示例 Python 代码来进行测试。
让我们创建一个简单的脚本,它将“Python Programming”打印 5 次,并将其命名为main.py:
i=0
for i in range(5):
print('Python Programming')
i+=1
一旦你运行它,你应该得到:
Python Programming
Python Programming
Python Programming
Python Programming
Python Programming
使用 cProfile 性能分析 Python 代码最简单的方法之一是从终端运行 cProfile。
打开终端,导航到你的 Python 脚本 (main.py) 所在的文件夹,并运行:
python -m cProfile main.py
你应该得到:
Python Programming
Python Programming
Python Programming
Python Programming
Python Programming
8 function calls in 0.001 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.001 0.001 main.py:1()
1 0.000 0.000 0.001 0.001 {built-in method builtins.exec}
5 0.001 0.000 0.001 0.000 {built-in method builtins.print}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
使用 Python 的 cProfile 对 Python 代码进行性能分析
使用 cProfile 直接在 Python 脚本中分析 Python 代码是另一种方法。
你需要将 cProfile 模块导入到 Python 环境中,并显式调用性能分析函数,同时将 Python 代码作为字符串传递给该函数作为参数:
import cProfile
cProfile.run("print('Python Programming')")
你应该得到:
Python Programming
4 function calls in 0.000 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.000 0.000 :1()
1 0.000 0.000 0.000 0.000 {built-in method builtins.exec}
1 0.000 0.000 0.000 0.000 {built-in method builtins.print}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
在这个示例中,我们将代码简化为仅打印“一行‘Python Programming’”,因为如你所见,将代码作为字符串传递给cProfile.run()并不是最方便的选项。
应该有更好的方法,对吧?当然!在下一节中,我们将探讨如何使用函数和cProfile对 Python 代码进行性能分析。
使用 Python 的 cProfile 对 Python 函数进行性能分析
让我们重用前面部分的 Python 代码,现在将其放入函数my_func()中:
def my_func():
i=0
for i in range(5):
print('Python Programming')
i+=1
现在我们可以通过将该函数作为参数传递给cProfile.run()来轻松地对其进行性能分析:
import cProfile
def my_func():
i=0
for i in range(5):
print('Python Programming')
i+=1
cProfile.run('my_func()')
你应该得到:
Python Programming
Python Programming
Python Programming
Python Programming
Python Programming
9 function calls in 0.001 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.001 0.001 :1()
1 0.000 0.000 0.001 0.001 main.py:3(my_func)
1 0.000 0.000 0.001 0.001 {built-in method builtins.exec}
5 0.001 0.000 0.001 0.000 {built-in method builtins.print}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
导出 cProfile 数据
在前面的部分中,我们对 Python 代码进行了性能分析,结果在终端中打印出来。
那我们可以提取并保存分析数据吗?
是的!使用内置的pstats模块和cProfile,我们可以将分析结果提取并保存到一个简单的.txt 文件中。
请注意,profile.run() 功能定义如下:
profile.run(command, filename=None, sort=- 1)
当 filename 设置为 None 时,它会自动打印出性能分析报告。
现在,让我们将 filename 设置为一些示例文件名,如‘results’,并运行上一节的代码:
import cProfile
def my_func():
i=0
for i in range(5):
print('Python Programming')
i+=1
cProfile.run('my_func()', 'results')
你会注意到终端中的输出仅仅是函数应该生成的内容,但没有性能分析报告:
Python Programming
Python Programming
Python Programming
Python Programming
Python Programming
现在,你会在项目目录中创建一个名为‘results’的新文件:

图片由作者提供
我们有了包含分析报告的文件,但我们还不能打开它。现在我们需要使用pstats模块将其转换为.txt 文件,然后才能访问报告。
这段额外的代码将把‘results’文件转换为‘results.txt’:
import pstats
with open('results.txt', 'w') as file:
profile = pstats.Stats('results', stream=file)
profile.print_stats()
file.close()
你应该在目录中看到一个新文件:

作者提供的图片
现在我们已经成功创建了一个包含分析报告的.txt 文件。它应包含以下数据:

作者提供的图片
提取 cProfile 数据的完整代码
import cProfile
import pstats
def my_func():
i=0
for i in range(5):
print('Python Programming')
i+=1
cProfile.run('my_func()', 'results')
with open('results.txt', 'w') as file:
profile = pstats.Stats('results', stream=file)
profile.print_stats()
file.close()
结论
在这篇文章中,我们探讨了如何使用cProfile模块来分析 Python 代码。
代码分析帮助识别代码中的瓶颈,并帮助理解哪些部分的代码应该优化以提高整体性能。
如果您有任何问题或对某些编辑有建议,请随时在下面留言,并查看我的更多Python 编程教程。
最初发表于 https://pyshark.com 2023 年 2 月 10 日。
程序辅助语言模型
原文:
towardsdatascience.com/program-aided-language-models-93d226c7d9a0
大型语言模型(LLMs)可以编写代码,但如果它们能执行程序呢?
·发表于 Towards Data Science ·18 分钟阅读·2023 年 8 月 22 日
--

(照片由 Florian Olivo 提供于 Unsplash)
尽管大型语言模型(LLMs)被用于多种应用,但它们通常在解决基于推理的任务时遇到困难。随着链式思维和从少到多提示等提示技术的出现,这一问题显著减轻了。从高层次来看,这些技术通过在模型的提示中提供问题解决的例子来鼓励 LLMs 进行推理行为。然后,模型可以学会输出这些推理过程,并逐步解决潜在的问题。值得注意的是,这是一种仅依赖提示的方法,无需微调,显示了 LLMs 在给定足够上下文的提示时具备推理能力。
尽管链式思维提示等技术效果显著,但 LLM 预计需要生成问题解决的思维链和最终答案。有趣的是,这种方法可能导致一些特殊的失败情况,其中 LLM 可能生成准确的解决问题的推理,但仍会给出错误的答案。通常,这些错误是由于简单的错误(例如,计算错误)造成的。为了解决这个问题,最*的研究探讨了一种程序化的方法,鼓励 LLM 生成包含自然语言和代码组件的思维链。然后,LLM 可以通过外部解释器运行这些代码,以获得所需的输出。
为了理解这种方法为什么有用,我们应该注意到许多 LLM 难以解决的问题(例如,算术错误、无法评估复杂表达式等)可以在程序内部轻松表达和解决。因此,使用链式思维风格的提示在具有编码能力的 LLM(例如,Codex)上,可以将 LLM 的优势与任意 Python 程序的计算能力相结合!更具体地说,LLM 可以被鼓励生成包含自然语言和代码组件的问题解决理由,生成一个可以由外部解释器运行的脚本,以计算问题的最终输出。我们将在本概述中探讨这种方法,这对 LLM 在解决推理任务中的准确性和可靠性大有裨益。

(来自 [1, 2])
背景信息

预训练一个大语言模型(LLM)
尽管现代大语言模型具有令人难以置信的能力,但这些模型都基于一个简单的预训练程序,该程序对大量未标记的文本数据执行下一个词预测。虽然我们可以调整这个过程的细节(例如,数据的类型或混合),但大多数 LLM 的基本预训练方法保持不变。我们只需* i)* 从预训练语料库中采样一些文本,* ii)* 教会模型准确预测语料库中的下一个词/标记。就是这样! 这个简单而深刻的方法为现代语言建模奠定了基础。
但……还有一些从多年的研究中学到的技巧和经验,使我们能够让语言模型变得像ChatGPT或GPT-4一样强大。大多数模型使用相同的仅解码器架构,但仅仅通过预训练无法创建高性能的语言模型。我们需要:
-
足够的规模(即,大模型和预训练数据集)。
-
通过监督微调(SFT)和来自人类反馈的强化学习(RLHF)进行行为调整[11, 12]。
-
[可选] 领域专业化(即,在特定类型的数据上微调模型,例如代码或对话)。
如果我们正确执行所有这些步骤,我们可以创建一个强大的基础模型,它能够通过文本提示解决各种任务。值得注意的是,大多数语言模型的知识和信息都是通过预训练获得的(见“训练过程”部分 这里),但在预训练之后进行的这些额外的精细化步骤使 LLMs 变得更具可引导性和更有趣;见下文。

(来自 [11])
LLMs 在什么方面表现不佳? 语言模型在各种不同的应用中取得了令人印象深刻的表现,但它们并不完美。这些模型有已知的局限性,例如:
-
添加大数的困难
-
无法评估/解决复杂方程
-
对迭代过程的推理困难
例如,如果我们给大型语言模型(LLM)提供一个关于Fibonacci 数列的描述,然后要求它计算第 100 个数字,那么它很可能会失败!为什么会这样? 好吧,我们知道 LLMs 在进行算术运算时表现不佳,而解决 Fibonacci 数列(除非模型使用暴力记忆)需要在两个数字之间进行多次迭代加法。如果模型在每次迭代中有 95%的概率正确执行加法,那么第 100 个 Fibonacci 数正确的概率不到 1%!
快速免责声明。 最*发布的 GPT-4 使得关于 LLM 局限性的声明变得更加困难。例如,GPT-4 完全能够解决第 100 个 Fibonacci 数,甚至可以在最小提示努力下评估一些(相对)复杂的方程;见下文。

(来自 ChatGPT Plus)
鉴于此,对 LLM 能力的任何声明都需要保持一定的怀疑态度。这个领域迅速发展,模型每天都变得越来越强大和令人印象深刻(字面意义上)。
教授 LLMs 如何编码
如上所述,创建高性能 LLM 的一个(可选)部分是领域专业化。在预训练之后,LLMs 相当通用,仅能完成单一任务——下一个标记预测。如果我们想要一个在某个特定领域专业化或擅长执行特定任务(例如,信息检索对话或编写剧本)的 LLM,我们需要在大量展示该任务正确行为的数据上进行微调。这个技术的一个最成功的应用,特别是与此概述相关,是创建可以编写代码的语言模型。

(来自 [4])
类似于如何从互联网下载大量文本数据用于预训练语言模型,我们可以从公共来源(例如 GitHub)下载大量代码用于训练 LLMs,这使得编码成为专门化 LLMs 的一个特别完美的应用。例如,Codex [4]就是一个显著的模型,它使用从互联网下载的未标记文本数据和代码的组合进行训练。给定一个 Python 文档字符串,Codex 的任务是生成一个有效的 Python 函数,以执行文档字符串中概述的任务;见上文。

(来自 [4])
Codex 在人工策划的编码任务上表现极佳(见上文),甚至被用来驱动GitHub Copilot编码助手,揭示了 LLMs 不仅仅可以应用于自然语言!我们也可以将它们应用于许多其他具有类似结构的问题。在这种情况下,我们使用进一步的语言模型预训练来适应预训练 LLM 到新领域。值得注意的是,Codex 能够生成代码和自然语言输出,使其成为一个特别多用途且有用的 LLM。而且,创建这种领域特定的模型相对简单——我们只需要大量的代码进行训练。
思维链 (CoT) 提示
超越之前概述的限制,LLMs 最初因无法解决推理任务而受到批评。然而,该领域的研究带来了突破性的技术,如CoT 提示 [3],使 LLMs 能够相当准确地解决基于推理的任务。CoT 提示的理念很简单。我们只需使用少量样本学习来教 LLM 如何输出详细解释其答案的解决方案——适用于任何推理任务;见下文。这种方法极其实用,因为我们只需要生成少量解决方案示例来包含在提示中,而之前的工作则编纂了整个数据集用于微调。

(来自 [3])
与教 LLM 如何编码不同,我们通过 CoT 提示发现,这些模型能够在无需任何微调的情况下解决推理任务!相反,我们只需采用一种更好的提示方法来“解锁”LLM 解决复杂推理任务的能力。
“大型预训练语言模型具备内置的推理能力,但它们需要特定的提示才能释放其威力。” — 来自 [13]
鉴于我们在之前的概述中已经了解了很多关于 CoT 提示及其许多变体,我不会在这里深入探讨这个概念。然而,有一个显著的方面我们应该注意——语言模型被期望同时i) 生成思维链和ii) 从这个思维链中提取最终答案。尽管 CoT 提示是有效的,但我们可能会开始怀疑:依赖语言模型准确解决这两个步骤是否真的是一个好主意?
在语言模型中解耦推理和计算
我们知道,语言模型(在正确的提示方法下)能够提供准确的问题解决理由或详细的输出解释。然而,生成正确的理由并不意味着语言模型会正确解决问题!如果语言模型在给出最终答案时出现一个小的算术错误怎么办? 由于语言模型的基本局限性,像 CoT 提示这样的技术通常会遇到令人沮丧的失败案例,其中模型生成了准确的理由,但输出了错误的最终答案。这类错误通常被称为语言模型的组合性差距。
“我们衡量模型可以正确回答所有子问题但未能生成整体解决方案的频率,这一比率称为组合性差距。” —— 引自 [16]
在本节中,我们将探讨最*的研究,这些研究尝试通过利用已在代码上进行训练的语言模型的独特技能(例如,Codex [4])来编写连贯且功能性强的程序来解决这个问题。我们可以依靠语言模型生成问题解决的理由。但是,我们不是要求语言模型给出实际的答案,而是提示模型生成一个与理由相关的程序,这个程序在使用单独的代码解释器执行时,可以生成最终答案。因此,我们的理由变成了代码和语言的混合体——基本上是一个带有说明性评论的 Python 脚本!
程序辅助语言模型(PaL)

(来自 [1])
在 [1] 中,作者提出了一种受 CoT 启发的技术,称为程序辅助语言模型(PaL),该技术使用语言模型将基于推理的问题分解为逐步的问题解决理由。然而,这种理由包含了自然语言和(基于 Python 的)编程组件。生成这种混合理由后,我们可以通过 Python 解释器执行程序化部分来解决问题。这种方法的目标是消除语言模型生成正确推理链但仍产生错误最终答案的情况。
“这弥合了链式思维方法中的一个重要差距,即推理链可能是正确的,但产生了错误的答案。” —— 引自 [1]
使用 PaL,我们可以利用 LLM 生成解决问题的推理,但计算最终解决方案的过程(即,模型通常在这一部分挣扎的地方!)被委托给代码解释器,从而消除了算术或逻辑错误的潜在可能性。因此,LLM 只需学习如何生成解决问题的推理——解决方案是程序化得出的。我们可以通过少量学习教导 LLM 生成这种混合推理。然而,为了实现这一点,我们需要一个在自然语言和代码上都经过预训练的 LLM(例如,Codex [4])。
理解 PaL。 从高层次来看,PaL 采用的方法与 CoT 提示非常相似。我们使用一种少量提示的方法,提供几个将问题分解为相关推理的示例。CoT 和 PaL 之间的主要区别在于,PaL 使用的推理是由交错的自然语言和程序语句组成的;见下文。

(来自 [1])
PaL 中的每一步推理过程都附加了程序语句。然后,当这些程序语句被综合时,它们可以通过单独的 Python 解释器执行,以生成最终答案(即,通过单次、事后的执行完成)。PaL 正在通过少量学习教导 LLM 生成一个逐步解决所需问题的程序。有趣的是,[1]中的作者鼓励 LLM 通过利用 Python 注释语法(即 # 字符)生成基于自然语言的中间步骤,这使得语言组件能够插入到生成的程序中。换句话说,我们正在教导 LLM 通过逐步的程序和信息性注释来解决推理任务!

(来自 [1])
与 CoT 提示不同,PaL 使用的少量示例不包含最终解决方案。相反,示例仅仅是交错了自然语言语句的程序(没有其他东西!)。最终解决方案的生成委托给 Python 解释器,因此 LLM 不需要学习如何执行这一步骤;见上文。
更进一步,[1]中的作者观察到,为程序中使用的变量提供有意义的名称是有益的。这一发现表明,PaL 提出的推理过程是一种真正的混合方法,它融合了语言和程序组件。在编程和语言模式之间形成符号链接是重要的;见下文。

(来自 [1])
这效果好吗? PaL 在各种符号、数学和算法推理任务中进行了评估,结果显示它能够减轻许多与 CoT 提示相关的常见问题。所提出的方法与标准的 少量学习(在 [1] 中称为“直接”提示)以及 CoT 提示进行了比较。在数学推理任务中,PaL 与 Codex [4] 相结合,轻松超越了之前的提示方法,适用于各种不同的模型。值得注意的是,PaL 甚至超越了 Minerva [5],这是一种专门针对大量定量推理数据进行微调的 LLM;见下文。

(来自 [1])
从上表中,我们还应注意到,使用 Codex 的 PaL 在 GSM8K 上达到了最先进的性能,超越了 PaLM-540B(即更大的模型!)的 CoT 性能 15% 的绝对 top-1 准确率。有趣的是,[1] 中的作者指出,GSM8K 主要集中在较小数字的数学词题上(即,50% 的数字在 0-8 之间),并提出了 GSM-Hard——这是一个包含更大数字的数据集版本。在更困难的数据集上,PaL 相较于 PaLM 的 CoT 提示在绝对 top-1 准确率上提高了* 40%,揭示了程序辅助提示对于需要复杂算术运算的大数字问题更具优势。

(来自 [1])
在符号和算法推理任务中,PaL 再次提供了显著的好处;见上文。实际上,PaL 在这个类别中的五个数据集中接*完全解决四个,达到了 >90% 的准确率。此外,PaL 似乎随着问题复杂性的增加而保持一致的表现;见下文。在这里,我们可以看到,大量数字或推理任务中的更多对象所带来的复杂性在程序处理上很简单,尽管直接用 LLM 处理这样的复杂性可能会引发问题。

(来自 [1])
思维程序(PoT)提示
如前所述,CoT 提示的推理过程有两个不同的步骤:
-
生成基于语言(或程序)的解决方案 rationale
-
根据这一 rationale 计算最终答案
LLM 擅长执行上述第一步,但可能在计算最终答案时遇到困难。通常,这个问题是由于算术错误或无法评估复杂表达式所致。简单来说,LLM 在处理复杂的数字任务时会遇到困难。在 [2] 中,作者旨在利用一种称为思维程序(PoT)提示的代码增强提示方法来缓解这个问题,并使 LLM 能够准确地解决复杂的数字任务。
“在 PoT 中,计算可以委托给程序解释器,该解释器用于执行生成的程序,从而将复杂的计算与推理和语言理解解耦。” — 摘自 [2]
正如我们所猜测的那样,PoT 提示与 PaL 非常相似。这两种技术都使用代码增强的提示技术来解决复杂的推理任务,并将推理过程的必要部分委托给代码解释器。更具体地说,PoT 提示利用基于代码的 LLM 的少量学习(例如,Codex [4])生成包含自然语言声明和代码(用 Python 编写)的混合推理。然后,将输出的代码部分卸载到解释器进行评估,从而将推理和计算解耦。

(摘自 [2])
相比之下,CoT 提示直接在 LLM 上进行推理和计算。这是一个问题,因为 LLM 在以下方面存在困难:
-
执行基本的算术运算(特别是大数运算)
-
评估复杂的数学表达式(例如,多项式或微分方程)
-
解决需要迭代的问题
这些问题通过上图展示,其中一个使用 CoT 提示的 LLM 无法评估一个简单的立方方程或在斐波那契序列的迭代计算中进行推理。幸运的是,我们可以用程序轻松解决这些问题!例如,我们可以使用 for 循环计算斐波那契序列,而立方方程可以轻松地用 Python 语法表示。然后,我们可以运行这个程序来生成正确的输出,从而消除对 LLM 的不必要依赖。

(摘自 [2])
PoT 的详细信息。 类似于 PaL,PoT 提示生成的解决问题的推理包含语言和代码组件。LLM 通过一系列包含问题对及相关“思维程序”(即包含解释计算的多步骤程序和自然语言声明)的少量示例来学习生成这种推理;见上文。

使用 SymPy 的符号数学编程示例
与 PaL 不同,PoT 编写的代码依赖于一个名为SymPy的符号数学库。这个包允许用户定义数学“符号”,然后将它们组合在一起形成复杂的表达式。为了评估这些表达式,我们可以将它们传递到 SymPy 的solve函数中;见上文。更多细节,请查看这里的教程。尽管使用了符号数学,PoT 提示与尝试直接生成数学方程的 LLM 不同,之前的工作表明这非常困难[3]。这是因为 PoT 提示:
-
通过一个多步骤、基于理由的过程生成符号方程。
-
将符号变量与语义上有意义的名称关联起来。
与 PaL 类似,[2]中的作者指出,为程序中的变量分配有意义的名称确实对 LLM 的性能产生了可测量的影响。
结果。 PoT 提示在多个数学文字问题和金融问答数据集(例如,FinQA [8]和 ConvFinQA [9])上使用 Codex [4]和 GPT-3 [7]进行了评估。使用少量学习和 CoT 提示(包括一个可以访问外部计算器的 CoT 提示变体)的多个不同 LLM 被用作基准。如下表所示,PoT 提示在所有情况下都显著优于基准,这强调了将推理与计算分离的价值。

(来自[2])
有趣的是,[2]中的作者还发现零-shot PoT 提示(即类似于零-shot CoT 提示 [10])效果非常好。即使没有为 LLM 策划几个程序融入的理由,我们也可以通过 PoT 提示在数值任务中实现合理的性能。此外,作者对使用 PoT 提示提出了一个有趣的实际注意事项。为了避免生成完全基于语言的理由(即一个带有所有注释的程序),他们不得不手动抑制#符号的概率。虽然这是一个小细节,但值得记住——我们不希望生成的程序仅仅是注释!此外,这也表明,使这种技术在实践中有效往往是脆弱和困难的。
我们能做得更好吗?

(来自[14])
PaL 和 PoT 在大多数实验中采用了贪婪解码策略,这意味着 LLM 会通过迭代选择下一个概率最高的 token 来生成输出序列。然而,我们可以使用各种更好的解码策略!一个值得注意的(且超级简单的)策略是自一致性[14]。该技术使用相同的 LLM 和提示来为一个问题生成多个不同的输出。然后,通过对生成的所有输出进行多数投票来得出最终答案;见上文。

(来自 [2])
当将自一致性应用于 PoT 提示时,我们会看到立即且显著的好处!如上所示,带有自一致性的 PoT 在几乎所有考虑的数据集中都达到了新的最先进性能。同样,PaL [1] 也受益于自一致性的使用,甚至用于探索更复杂的解码/提示策略,如最少到最多提示 [15](即 CoT 提示的一种变体,显式地逐步解决推理任务)。与这种更复杂的提示风格结合时,PaL 变得更加有效;见下文。

(来自 [1])
尽管 PaL 和 PoT 表现得相当好,我们可以通过对其提示技术进行一些易于实现的补充来使其更上一层楼。这些发现激发了进一步的实验。也许我们可以通过利用其他有用的技术,如提示集成,来获得额外的性能提升。
主要结论
尽管 LLM 本身非常有用,但在本综述中我们看到,当 LLM 能够访问有用的工具时,它们可以变得更加出色。特别是,我们了解到将 LLM 连接到外部代码解释器对于推理任务的性能极其有利。然而,为了使其效果良好,我们需要访问能够编写代码的 LLM。以下是一些主要结论。
为什么会有效? PaL 和 PoT 的有效性源于 LLM 能够生成准确的解决问题的理由,但往往在简单任务如算术和迭代中遇到困难。幸运的是,这些概念可以轻松地在程序中建模,使得将 LLM 连接到外部代码解释器成为一种直观且强大的解决推理问题的技术。简而言之,我们通过依赖 LLM 擅长的领域,并将剩余的解决问题的组件委托给能够更可靠地生成解决方案的代码解释器,获得了很多收益。
我们应该如何解决 LLM 的弱点? 正如这篇文章简要提到的那样,许多已知的 LLM 缺点正随着更强大的模型(如 GPT-4)的发布而得到解决。然而,我们在这份概述中看到,解决这些问题的替代方法可能更加可靠。特别是,依靠外部代码解释器可以解决由于 LLM 在解决基于推理的任务时所遇到的局限性问题。赋予模型执行代码的能力无疑扩大了其能力范围,这激发我们思考其他可能对 LLM 有用的 工具。
将思想表达为程序。 这项工作真正突显了程序可以被解释为表达个人思想的结构化语言这一事实。与自然语言相比,编程语言的约束更多,这使得它们能够轻松表达迭代、建模复杂方程等。然而,程序的形式化性质也限制了表达能力——用自然语言写诗要比在 Python 脚本中写诗容易得多(假设没有调用 GPT-4 API)! 在我看来,考虑自然语言和代码之间的差异是相当有趣的。我们在这里看到,将它们结合在一起可以发挥两者的优势。
结束语
非常感谢你阅读这篇文章。我是 Cameron R. Wolfe,Rebuy 的人工智能总监。我研究深度学习的实证和理论基础。你还可以查看我在 medium 上的 其他写作!如果你喜欢这篇文章,请在 twitter 上关注我,或者订阅我的 Deep (Learning) Focus newsletter,在这里我通过对流行论文的易懂概述帮助读者建立对人工智能研究主题的更深入理解。
参考文献
[1] Gao, Luyu 等. “PAL:程序辅助语言模型。” arXiv 预印本 arXiv:2211.10435 (2022)。
[2] Chen, Wenhu 等. “思维程序提示:将计算与推理解耦以进行数值推理任务。” arXiv 预印本 arXiv:2211.12588 (2022)。
[3] Wei, Jason 等. “思维链提示引发大语言模型的推理。” arXiv 预印本 arXiv:2201.11903 (2022)。
[4] Chen, Mark 等. “评估训练有素的代码大型语言模型。” arXiv 预印本 arXiv:2107.03374 (2021)。
[5] Lewkowycz, Aitor 等. “用语言模型解决定量推理问题。” arXiv 预印本 arXiv:2206.14858 (2022)。
[6] Chen, Wenhu. “大型语言模型是少量(1)次表格推理者。” arXiv 预印本 arXiv:2210.06710 (2022)。
[7] Brown, Tom 等. “语言模型是少量学习者。” 神经信息处理系统进展 33 (2020): 1877–1901。
[8] 陈志宇 等. “Finqa:一个关于金融数据的数字推理数据集。” arXiv 预印本 arXiv:2109.00122 (2021)。
[9] 陈志宇 等. “Convfinqa:探索对话金融问答中的数字推理链。” arXiv 预印本 arXiv:2210.03849 (2022)。
[10] 小岛武志 等. “大型语言模型是零-shot 推理器。” arXiv 预印本 arXiv:2205.11916 (2022)。
[11] 欧阳龙 等. “通过人工反馈训练语言模型以遵循指令。” 神经信息处理系统进展 35 (2022): 27730–27744。
[12] 托皮兰·罗马尔 等. “Lamda:用于对话应用的语言模型。” arXiv 预印本 arXiv:2201.08239 (2022)。
[13] 李一飞 等. “关于提升语言模型推理能力的进展。” arXiv 预印本 arXiv:2206.02336 (2022)。
[14] 王学智 等. “自我一致性提高语言模型中的思维链推理。” arXiv 预印本 arXiv:2203.11171 (2022)。
[15] 周登嵘 等. “从最少到最多的提示使大型语言模型能够进行复杂推理。” arXiv 预印本 arXiv:2205.10625 (2022)。
[16] 普雷斯·奥菲尔 等. “测量并缩小语言模型中的组合性差距。” arXiv 预印本 arXiv:2210.03350 (2022)。
通过 Go 和 Metal Shading Language 编程苹果 GPU
研究 Go、Cgo、Metal Shading Language、Metal Performance Shaders,以及对矩阵乘法的不同方法进行基准测试
·
关注 发表在 Towards Data Science ·12 分钟阅读·2023 年 12 月 4 日
--
图片由 Etienne Martin 在 Unsplash 上拍摄
下面我将描述如何使用 cgo 在 Go 和原生 C 之间进行接口,如何将其用于与 Apple 的 Metal Performance Shaders 框架的 Objective-C 绑定进行接口,如何与用 Metal 着色语言 编写的 自定义 GPU 代码(着色器)进行接口,最后将所有这些与手写的和 OpenBLAS 的基于 Go 的矩阵乘法操作进行基准测试。这是为我的 M2 MacBook 编写的。
源代码的布局,在 GitHub 上可用,如下所示:

高级源代码、库和设备布局
这量很大,所以我将其分解为这些部分,或者可以直接跳到 基准测试。
-
GPU 和浮点并行性
-
Metal GPU 基础
-
Metal 着色语言
-
Objective-C 绑定
-
Metal Performance Shaders 框架
-
Go 和 cgo
-
Go 实现基线和 OpenBLAS
-
结果
GPU 和浮点并行性
我假设大多数人此时直观上已经熟悉 GPU 在某些计算任务中的强大性能,尤其是一些支持机器学习的任务。直到我开始尝试 Metal,我才亲身理解它们比 CPU 强大 多少。
GPU 设计上极其高效于大规模并行浮点运算,这要求高内存带宽。我的 MacBook M2 有 8 个 CPU 核心和 8 个 GPU 核心,但为了对比,Nvidia RTX 4090 拥有 16384 核心,而 H100 拥有 16896 CUDA 核心及数百个额外的专用张量核心。GPU 通常支持 SIMD 处理,使其能够在多个数据点上同时执行相同的指令。
除了图形处理外,矩阵乘法和线性代数任务一般都受益于这种并发,得益于其高度可并行的算法。这反过来支持了核心机器学习工作负载,如训练和推断 [1] [2]。
CUDA 可能是最著名的 GPU 编程*台,专门针对 Nvidia 硬件。也有数学框架可用于 OpenGL。像 TensorFlow 和 PyTorch 这样的框架可以与 GPU 硬件 轻松集成,且透明度相当高。这篇文章 对将支持 Metal 的 GPU 框架集成到 spaCy NLP 库 中的性能提升做了有趣的分析。
Metal GPU 基础知识
直接编程 GPU 计算并不像编写设备 CPU 代码那样简单。当使用 Apple 的 Metal 框架时,执行 GPU 上代码的大致操作步骤如下:
-
寻找适当的 GPU 设备
-
创建一个用于执行命令的队列(即 MTLCommandQueue)
-
将数据数组的指针封装到结构化缓冲区中;如果数据是可执行代码,则使用 管道状态,否则使用 常规缓冲区。Apple 的 GPU 使用 统一内存空间,这意味着我们不需要实际 复制 任何数据到特定于 GPU 的物理内存中。
-
提交命令缓冲区以进行执行,并等待结果或在完成时设置事件处理程序
-
从响应缓冲区中提取字节,并使用 CPU 程序代码在本地格式化
原始 GPU 编程使用异步模型。
Metal 着色语言
Metal 着色语言 是 C++14 的一种衍生语言,可用于编写自定义逻辑(称为“着色器”),以在兼容 Metal 的 GPU 上运行。一般来说,如果可能的话,使用 MPS 框架(稍后讨论)来实现等效功能可能更好——它通常针对常见的 GPU 对齐用例(如矩阵乘法或 神经网络)进行了高度优化。
MSL 代码的调试相当困难。你可以通过 Xcode 使用着色器调试器,但如果你想在没有 Xcode 的情况下检查或打印中间值,你需要将数据写入响应调试缓冲区,并在你的 C++或 Objective-C 包装器中解析这些原语。
MSL 函数通过kernel标识公开为公共接口。Metal 框架传递当前调用线程上下文或线程组的 ID,这些 ID 可以用来确保非重叠写入。线程可以通过三维 ID 系统表示;这个线程空间的维度在包装器代码中配置。
以下是原始矩阵乘法算法的实现,结合了一些循环展开,令人惊讶地显著提高了性能。这只是为了比较;通常,MPS 的MPSMatrixMultiplication功能会更合适。
kernel void matrix_multiply_naive(
device const MatrixParams *params,
constant float *A,
constant float *B,
device float *C,
// Indicates the thread's unique position within the entire grid of
// threads being executed. The uint2 type is a 2D coordinate, with
// fields x and y representing its indices on each axis.
// This parameter is not directly provided from the calling code,
// but provided by the Metal framework
uint2 gid [[thread_position_in_grid]]
) {
if (gid.x >= params->a_rows || gid.y >= params->b_cols) {
return; // This thread is out of matrix dimensionality range, do nothing
}
float sum = 0.0;
int k;
// Loop unrolling; improves performance by a notable margin
for (k = 0; k <= params->a_cols - 4; k += 4) {
sum += A[gid.x * params->a_cols + k]
* B[k * params->b_cols + gid.y];
sum += A[gid.x * params->a_cols + k + 1]
* B[(k + 1) * params->b_cols + gid.y];
sum += A[gid.x * params->a_cols + k + 2]
* B[(k + 2) * params->b_cols + gid.y];
sum += A[gid.x * params->a_cols + k + 3]
* B[(k + 3) * params->b_cols + gid.y];
}
// Handle any remaining elements
for (; k < params->a_cols; ++k) {
sum += A[gid.x * params->a_cols + k] * B[k * params->b_cols + gid.y];
}
C[gid.x * params->b_cols + gid.y] = sum;
}
我还在 MSL 中实现了一个原始转置函数以供比较。给定一个转置矩阵,这是对上述逻辑的一个微不足道的调整,其内部循环遍历 B 的行而不是列:
// Loop unrolling; improves performance by a notable margin
for (k = 0; k <= params->a_cols - 4; k += 4) {
sum += A[gid.x * params->a_cols + k]
* B[gid.y * params->b_cols + k]; // Note this is gid.y * cols plus k
sum += A[gid.x * params->a_cols + k + 1]
* B[gid.y * params->b_cols + k + 1];
sum += A[gid.x * params->a_cols + k + 2]
* B[gid.y * params->b_cols + k + 2];
sum += A[gid.x * params->a_cols + k + 3]
* B[gid.y * params->b_cols + k + 3];
}
// Handle any remaining elements
for (; k < params->a_cols; ++k) {
sum += A[gid.x * params->a_cols + k] * B[gid.y * params->b_cols + k];
}
我在早期的博客文章中讨论了这种方法,这是一种相当简单的方法,可以提高原始算法的标量性能,至少在 CPU 上是如此。更多内容稍后会讨论。
Objective-C 绑定
Metal 框架提供了从 Metal 源代码编译库的能力。一旦文件内容被加载,绑定代码会按名称查找内核函数,并初始化一个新的MTLComputePipelineState,表示编译后的函数代码。
id<MTLDevice> device = MTLCreateSystemDefaultDevice();
// Compile and initialize a new library located at the provided source path.
MTLCompileOptions *compileOptions = [MTLCompileOptions new];
compileOptions.languageVersion = MTLLanguageVersion3_0;
// Wrap input source path string
NSString *ss = [NSString stringWithUTF8String:source_path];
// Initialize new library containing compiled shader functions
id<MTLLibrary> lib = [device newLibraryWithSource:ss
options:compileOptions
error:&error];
// Create a representation of the naive multiplication public shader function in
// the Metal library created above
id<MTLFunction> naiveFunction =
[lib newFunctionWithName:@"matrix_multiply_naive"];
// Create the new compute pipeline state
id<MTLComputePipelineState> pipelineStateNaive = [device newComputePipelineStateWithFunction:naiveFunction
error:&error];
为了实际调用原生 Metal 代码,线程配置需要设置,并初始化 GPU 缓冲区。
[computeEncoder setComputePipelineState:pipelineStateNaive];
MTLSize threadsPerGrid = MTLSizeMake(params->a_cols, params->a_rows, 1);
// Calculate a threadgroup size.
// https://developer.apple.com/documentation/metal/calculating_threadgroup_and_grid_sizes?language=objc
NSUInteger w = pipelineStateNaive.threadExecutionWidth;
NSUInteger h = pipelineStateNaive.maxTotalThreadsPerThreadgroup / w;
MTLSize threadsPerThreadgroup = MTLSizeMake(w, h, 1);
// Encode kernel function inputs
[computeEncoder setBytes:params length:16 atIndex:0];
[computeEncoder setBuffer:bufferA offset:0 atIndex:1];
[computeEncoder setBuffer:bufferB offset:0 atIndex:2];
[computeEncoder setBuffer:bufferC offset:0 atIndex:3];
// Encode the compute command.
[computeEncoder dispatchThreads:threadsPerGrid
threadsPerThreadgroup:threadsPerThreadgroup];
// End the compute pass.
[computeEncoder endEncoding];
// Execute the command.
[commandBuffer commit];
这内容比较多,我在这里阐明一下关系:

Objective-C 包装器中的概念、类型和硬件的高级布局
Metal 性能着色器框架
MPS 框架是苹果公司提供的高性能库,用于其Metal GPU 系列。它提供从图像任务到神经网络支持的功能。
API 主要通过 Swift 或 Objective-C 提供,尽管也有一个 Metal-cpp 库可供使用。
MPSMatrixMultiplication API 相对容易使用。与上述 MSL 代码一样,MPS 命令仍需编码到 MTLCommandBuffer 中,并异步提交执行。
// Define Matrix "descriptions", accounting for matrix dimensionality and byte size
MPSMatrixDescriptor *descriptorA = [MPSMatrixDescriptor matrixDescriptorWithDimensions:a_rows
columns:a_cols
rowBytes:a_cols * sizeof(float)
dataType:MPSDataTypeFloat32];
MPSMatrixDescriptor *descriptorB = [MPSMatrixDescriptor matrixDescriptorWithDimensions:b_rows
columns:b_cols
rowBytes:b_cols * sizeof(float)
dataType:MPSDataTypeFloat32];
// Output matrix
MPSMatrixDescriptor *descriptorC = [MPSMatrixDescriptor matrixDescriptorWithDimensions:a_rows
columns:b_cols
rowBytes:b_cols * sizeof(float)
dataType:MPSDataTypeFloat32];
// Initialize matrix representations using above descriptions and matrix buffers
MPSMatrix *matrixA = [[MPSMatrix alloc] initWithBuffer:bufferA descriptor:descriptorA];
MPSMatrix *matrixB = [[MPSMatrix alloc] initWithBuffer:bufferB descriptor:descriptorB];
MPSMatrix *matrixC = [[MPSMatrix alloc] initWithBuffer:bufferC descriptor:descriptorC];
// Creates the multiplication instance
MPSMatrixMultiplication *matrixMultiplication = [[MPSMatrixMultiplication alloc] initWithDevice:device
resultRows:a_rows
resultColumns:b_cols
interiorColumns:a_cols];
// Encodes the multiplication command into the command buffer for the GPU
id<MTLCommandBuffer> commandBuffer = [commandQueue commandBuffer];
[matrixMultiplication encodeToCommandBuffer:commandBuffer
leftMatrix:matrixA
rightMatrix:matrixB
resultMatrix:matrixC];
Go 和 cgo
我不特别喜欢使用 Objective-C,这个程序的重点是运行源自 Go 程序的 GPU 代码。
Cgo 是一种 Go 语言功能,允许 Go 编译器理解与本地 C 代码相关的注释中的编译指令。它支持一种 外部函数接口。
指令配置有点脆弱,但任何紧接着 import “C” 行的注释(称为“前言”)在编译引用的 C 代码时将被解释为头文件导入或编译参数。例如:
/*
#cgo LDFLAGS: -framework Foundation -framework CoreGraphics -framework Metal -framework MetalPerformanceShaders -L/opt/homebrew/opt/openblas/lib -lopenblas
#include <stdlib.h>
#include "metal.h"
*/
import "C"
-
通过命令行
LDFLAGS传递链接标志给链接器 -
使用标准头文件
stdlib.h编译 C 代码 -
使用本地项目头文件
metal.h编译 C 代码
需要一些反复试验才能找到适用于 MacOS 的正确链接器标志。
-
Foundation:基础库 -
CoreGraphics:在 MacOS 上与 GPU 接口时必需 -
Metal:用于 Metal 的库和语言支持,包括 MSL -
MetalPerformanceShaders:上述讨论的 MPS 库
事实证明,Apple 在其 Accelerate 框架中捆绑了一个 BLAS 实现,因此除了通过 brew 安装 OpenBLAS 外,还需要在链接时提供库的位置:
-L/opt/homebrew/opt/openblas/lib -lopenblas
go:embed 指令允许 Go 程序在编译时包含文件,这在我们希望将 MSL 源文件(mm.metal)的内容传递给 Metal 框架时是 非常有用的,如上所述,用于编译。
//go:embed mm.metal
var source string
// Compile the shader source code and initialize pipelines. The metalSource
// param contains the contents of an embedded Metal Shading Language file.
func Compile (metalSource string) {
// Wrap string in a C string
src := C.CString(metalSource)
// Free the above string after command queue is initialized
defer C.free(unsafe.Pointer(src))
// Compile the source, initialize pipelines and command queue
C.initializePipelineAndCommandQueue(src)
}
上述对 C 的引用是通过 cgo 与 C API 接口,例如:
// Calls initializeMTLBuffers from Obj-C bindings
C.initializeMTLBuffers(
a_data, // Input opaque pointer for A
b_data, // Input opaque pointer for B
C.int(4), // Converts 4 into C integer type
C.int(a.Size()),
C.int(b.Size()),
C.int(a.Rows * b.Cols))
params := MatrixParams{
a_rows: int32(a.Rows),
a_cols: int32(a.Cols),
b_rows: int32(b.Rows),
b_cols: int32(b.Cols),
}
// Return an unsafe pointer to this MatrixParams struct, cast to
// the native C representation defined in the shared header file
return (*C.MatrixParams)(unsafe.Pointer(¶ms));
注意,这意味着 C 是一个保留关键字,不能用作变量名。
Go 实现基线和 OpenBLAS
我想将基于 GPU 的矩阵乘法性能与更高级的实现(如 Gonum library)以及直观的手写(且相对低效)实现进行比较。
我在 Go 中实现了几种不同的算法,包括这个并行转置的简单算法,它将乘法工作天真地划分到 N 个 goroutine 中:
func (a Matrix[T]) TransposeMultParallel(b *Matrix[T]) *Matrix[T] {
if a.Cols != b.Rows {
panic("matrices are the wrong size for multiplication")
}
c_data := make([]T, a.Rows*b.Cols)
t := b.Transpose()
var wg sync.WaitGroup
for i := 0; i < a.Rows; i++ {
wg.Add(1) // Add a count to the WaitGroup for the new goroutine
go func(i int) { // Kick off goroutine
defer wg.Done() // Decrease the count when the goroutine completes
ptr := i * b.Cols
for j := 0; j < b.Cols; j++ {
var sum T = 0.0
for k := 0; k < a.Cols; k++ {
sum += a.At(i, k) * t.At(j, k)
}
c_data[ptr+j] = sum
}
}(i)
}
wg.Wait() // Wait for all goroutines to complete
return InitMatrixWithData(a.Rows, b.Cols, c_data)
}
Gonum BLAS是一个纯 Go 库,它实现了 BLAS 接口。然而,它也可以配置为将代数运算转发到本地代码 BLAS 实现,例如通过netlib的OpenBLAS。
我上面展示了如何配置cgo以正确链接到 MacOS 上的 OpenBLAS 安装。在应用程序代码中,可以直接设置首选的 BLAS 实现。从基准测试代码:
// Convert primitive arrays into gonum dense matrix types
gonum_a := mat.NewDense(a_rows, a_cols, a64_data)
gonum_b := mat.NewDense(b_rows, b_cols, b64_data)
gonum_c := mat.NewDense(a_rows, b_cols, nil)
gonum_d := mat.NewDense(a_rows, b_cols, nil)
// Configure Gonum to use Gonum-default Go implementation
blas64.Use(gonum.Implementation{})
// Run a multiplication using Gonum BLAS impl
start = time.Now()
gonum_c.Mul(gonum_a, gonum_b)
bdata.TimeGonumNative(start)
// Configure Gonum to use Netlib which forwards operations to a
// native C-code BLAS implementation (OpenBLAS in our case)
blas64.Use(netlib.Implementation{})
// Run a multiplication using OpenBLAS impl through Gonum API
start = time.Now()
gonum_d.Mul(gonum_a, gonum_b)
bdata.TimeGonumOpenBLAS(start)
结果
我的基准测试代码运行了几次以下矩阵乘法实现的试验,并报告了每次乘法两个逐渐增大的方阵所花费的*均时间:
- Naive multiplication, in Go
- Transposed naive multiplication, in Go
- Goroutine-parallelized transposed naive multiplication, in Go
- Gonum pure Go-based BLAS multiplication
- Gonum-wrapped OpenBLAS multiplication, written in C
- Hand-implemented naive multiplication, in MSL, on GPU
- Hand-implemented transposed naive multiplication, in MSL, on GPU
- Metal Performance Shaders framework, called from Objective-C, on GPU
基准测试输出如下(浮点数为毫秒):
2023-12-01 11:12:51.644 go-mm[75818:22427382] Using default device Apple M2
elements naive transpose transpose_parallel metal_naive metal_transpose mps gonum openblas
160000 196.00 201.00 42.00 8.00 9.67 0.33 4.67 6.00
250000 381.33 387.67 80.67 11.00 11.67 0.00 8.33 21.00
360000 801.00 789.33 159.33 19.00 16.33 0.00 14.33 4.67
490000 1228.00 1075.00 411.00 23.67 24.33 1.00 26.67 16.33
...
一些快速绘图通过matplotlib

所有方法的性能图
正如预期,我手写的 Go 实现相对失控。实际上,其他方法速度如此之快,以至于在图中无法区分它们。以下是这次运行的 GPU 使用滑动直方图

活动监视器 GPU 历史可视化 — 所有方法(Y 轴为使用百分比)
你可以看到 GPU 并不是特别忙碌,因为时间主要花在了 CPU 操作上。以下是另一轮测试,排除了最慢的三种乘法技术:

排除我手写的 Go 变体的各种方法性能图
大约 16M 元素(4k x 4k),Gonum开始下降。可以清楚地看到,基于 GPU 的和OpenBLAS操作优于纯 Go 实现。仅看基于 GPU 的方法:

仅在 GPU 上运行的矩阵乘法操作性能图
这里有几个有趣的笔记:
-
Metal Performance Shaders 库的速度惊人
-
天真的方法和转置天真的方法之间没有实际性能差异
对于第二点:这与上述 Go 基础的实现对比性能特性不同。结果表明,对 CPU 有利的缓存访问模式在 GPU 上效果不同,尤其是它们的 SIMD 组(或 warps)访问内存的方式。见 GPU 利用率以便比较:

活动监视器 GPU 历史可视化 — 仅 GPU 操作
现在仅查看OpenBLAS和MPS — 这两种最快的方法:

OpenBLAS 与 Apple 的 Metal Performance Shaders MPSMatrixMultiplication API 性能对比图
在大约 35M 元素时,OpenBLAS 实现开始下降,而 MPS 则保持稳定。这里的差异相当显著,后者完成相同的 35M 元素矩阵乘法操作的时间少于 15%。可以合理地假设,随着矩阵规模的增长,这种差异会继续扩大。
当然,这两种方法之间可能存在算法差异,因此这不是一个公*的 CPU 与 GPU 比较。如果我绘制我两个手工编码实现的性能差异图,它看起来是这样的:

我的 MSL 编写的矩阵乘法代码与 Go 编写的代码的性能比率图
这意味着,基于 MSL 的简单实现完成 5M 元素的乘法操作仅需我 Go 实现的 1%时间,而这种比率似乎随着时间的推移对 GPU 更有利。
在三维中编程:我迈出的量子计算第一步
原文:
towardsdatascience.com/programming-in-3d-my-first-steps-into-quantum-computing-566b9b93929d
教程
多维计算的世界
·发表于Towards Data Science ·9 分钟阅读·2023 年 2 月 22 日
--

来源:Stable Diffusion。
我记得我的第一次量子体验
我在 2018 年格雷斯·霍珀(GHC18)大会上发言。我刚刚参观了人满为患、令人应接不暇的展位博览会,并且正在寻找一个可以坐一会儿的地方。
结果证明,我坐下来参加了 IBM Quantum 关于使用Qiskit的入门演讲,这是一种开源量子计算框架。
透过镜子观察
这个演讲是对量子比特和叠加态概念的基本介绍。
我对什么是叠加态毫无头绪,但我发现这次演讲绝对引人入胜。
想象一下,计算机上的比特能够同时表示 0 和 1 这两个值的想法。
这怎么可能呢?
尽管这最初让我对量子计算建立了基础,但它也让我对在完全不同的层次上编程的想法开了眼界。
这感觉就像在三维空间中编程。
在量子世界中编程
在经典计算机上,比特以 0 或 1 的值表示。这是一种排他条件。
一个比特可以持有 0 或 1 的值。
然而,它不可能同时是 0 和 1。
另一方面,量子计算机上的量子比特可以同时持有 0 和 1 的值(或介于两者之间的浮动值)。
想一想这个问题
如果我们在经典计算机上有两个比特,我们可以在一个 CPU 周期内表示以下一个值:00、01、10、11。
然而,量子计算机上的两个量子位可以同时表示这些相同的值。这意味着你可以以二次或指数级的速度处理计算!
类似于两个量子位可以同时表示四种不同的值,三个量子位可以表示八个值。同样,四个量子位可以表示十六个值。
虽然经典计算机可以处理每 n 位的 n 个计算,但量子计算机可以处理惊人的 2^n 个计算每 n 个量子位!
只需 50 个量子位,你就可以在一个 CPU 周期内处理 2⁵⁰个计算。
这相当于在程序的单次运行中处理超过 140,000 GB 的数据。
在更高维度中思考
由于量子计算可以同时处理每个量子位的多个状态,因此编写程序时你开始以完全不同的方式思考。
与其编写一个使用单一比特的程序并期望它在特定计算中得到 0 或 1 的结果,你必须开始考虑一系列的百分比。
所以,一个量子位可能在一半时间里为 0,另一半时间里为 1。
这听起来很混乱,我最初也感到困惑——实际上,到现在仍然感到困惑!
然而,让我们考虑一个有趣的示例来理解编写经典程序与编写量子计算机程序之间的核心区别。
我说你好,你说再见
假设我们想编写一个程序,根据传入的参数值为 1 或 3,输出“你好”或“再见”。
如果函数接收到输入 1,我们输出单词“你好”。
如果函数接收到输入 3,我们输出单词“再见”。
这两个值在二进制中相差仅一位(1 = 01,3 = 11)。我们将第一个、最低有效位固定为 1。这两个值作为量子计算机的一个方便示例。
让我们看看如何用它来编写一个普通的经典程序。
def greeting(input):
if input == 1:
print('Hello')
elif input == 3:
print('Goodbye')
greeting(1)
greeting(3)
你好
再见
这就是在二维层面上编程。
我们有一个可以理解的函数,它接受一个数字输入。我们使用条件语句来检查值是否为 1 或 3。然后相应地输出消息。
每次执行此函数时,我们都知道根据输入输出的结果是什么。
让我们看看在量子计算机上这是什么样子的。
在 3D 中编程
我们可以使用量子计算框架 Qiskit 来创建我们的程序,如下例所示。
# Create a quantum circuit with 2 qubits.
qc = QuantumCircuit(2)
# Initialize the simulator.
simulator = Aer.get_backend('aer_simulator')
# Invert the first qubit (q0) to a value of 1.
qc.x(0)
# Place the second qubit (q1) into superposition for a value of 0 AND 1.
qc.h(1)
# Measure the result.
qc.measure_all()

生成 2 比特数字 01(1)或 11(3)的量子计算程序。第一个量子位(q0)设置为 1。第二个量子位(q1)处于 0 和 1 的叠加态。来源:作者,由 Qiskit 生成。
上面的程序使用了两个量子比特。就像我们的经典程序使用输入 1 或 3 来输出消息一样,我们可以通过使用两个量子比特在量子计算机上表示相同的两个值。
由于二进制中两个值 01 (1) 和 11 (3) 都需要最不重要的位为 1,我们通过反转第一个量子比特(使用 X-gate)来使其持有值 1。
现在,这里来到了惊人的部分
我们的量子程序可以同时持有两个不同的值!
当最不重要的量子比特 (q0) 固定为 1 时,最重要的量子比特 (q1) 将同时持有 0 和 1 的值,从而有效地表示整数 1 或 3。如果这个量子比特最终测量为 0,则结果二进制为 01,从而得到整数 1。如果量子比特测量为 1,则结果二进制为 11,得到整数 3。
两个量子比特几乎可以同时表示数字 1 和 3!
让我们运行程序,看看输出结果。
# Execute the circuit.
job = execute(qc, simulator)
result = job.result()
counts = result.get_counts()
量子计算机输出两个量子比特测量为 11 或 01 的次数。在这两种情况下,请注意只有最重要的量子比特(即左侧的那个)在变化。
这是我们放置于叠加态的量子比特 (q1) —— 这就是感觉像在 3D 中编程的地方!
我们不再考虑单一值。而是,我们现在必须考虑 多个 值。具体来说,我们必须同时考虑 1 和 3 作为输入到我们的问候程序中。

量子计算机上 1 或 3 的结果分布。来源:作者。
一半时间我们会收到 1 作为函数输入,而另一半时间我们会收到 3。
我们的函数输出的消息直接与第二个量子比特 (q1) 处于 0 或 1 状态的机会相关。
运行量子版本
理解量子程序会返回多个值的想法需要大量思考。
由于我们将返回多个值,我们需要更改问候程序以考虑这两种情况。
def qgreeting(counts):
if counts['01'] > 0:
print('Hello')
if counts['11'] > 0:
print('Goodbye')
qgreeting(counts)
你好
再见
上面的程序略微修改了我们原来的经典实现,即我们现在检查量子电路测量结果中是否有任何计数大于零。如果有,我们输出结果。
请注意,经典版和量子版问候函数之间的重要区别:
-
经典版需要调用 greeting() 函数两次以输出两条消息。
-
量子版本只需要调用一次 qgreeting() 即可输出相同的消息。
叠加问候
到目前为止,我们已经修改了问候程序以检查量子比特测量的计数。这使我们能够从一次调用中输出两个消息,这有助于我们理解程序中多个结果的量子性质。
然而,由于量子比特确实同时表示多个值,与其展示两个单独的问候语,也许我们可以将两个问候语叠加在一起!
def qgreeting(counts):
message = ''
# Define our messages.
hello = 'Hello'
goodbye = 'Goodbye'
# Get the total number of measurements counts.
total = counts['01'] + counts['11']
# Calculate the percentage from the counts for "hello" and "goodbye".
percent_hello = counts['01'] / total
percent_goodbye = counts['11'] / total
# Calculate the number of letters to use from "hello" and "goodbye".
hello_chars = math.ceil(len(hello) * percent_hello)
goodbye_chars = math.ceil(len(goodbye) * percent_goodbye)
# Combine the two greetings into a single message.
for i in range(hello_chars):
message += hello[i]
for i in range(goodbye_chars):
message += goodbye[i]
# The result is magic!
print(message)
qgreeting(counts)
HelGood
哇——我们将两个消息(“你好”和“再见”)叠加在一起,并将它们合并为一个问候语(“HelGood”)。
从每个问候语输出的字母数量来自我们量子程序中每个结果 01(1)和 11(3)的测量计数。
由于我们的量子电路有 50/50 的机会得到任何结果,因此我们将每个问候语的前一半组合在一起也就不足为奇。
好吧,也许这也不完全正确
将两个问候语合并在一起确实可以体现量子程序同时表示的多重值,但仍然有些令人困惑。
相反,让我们将多个量子态解析为一个单一的答案。
展*量子
正如我们所见,量子qgreeting() 使用量子程序测量结果中的百分比计数来输出消息。
由于我们对每个结果都有测量计数,让我们只取数量最多的计数,并将其作为输入传递给我们原始的经典问候方法。
如果量子程序的结果是 {‘11’: 529, ‘01’: 495},我们将使用二进制结果 11,其等于 3,因为它的计数最多。
多数规则
让我们使用这种方法,通过选择数量最多的值并将其作为输入传递给我们的经典程序来运行我们原始的经典问候方法。
for i in range(6):
# Execute the circuit.
job = execute(qc, simulator)
result = job.result()
counts = result.get_counts()
# Find the most frequent hit count.
key = max(counts, key=counts.get)
# Since the quantum computer returns a binary string (one bit for each qubit), we need to convert it to an integer.
num = int(key, 2)
# Display "Hello" or "Goodbye".
greeting(num)
你好
再见
再见
再见
你好
再见
这似乎更为现实。
我们现在选择数量最多的计数,并使用该消息作为输出。这实际上被称为在计算基态下测量。
注意,每次执行量子电路时输出的消息都不同。这是由于我们放置在叠加态中的单个量子比特(在最重要的量子比特上)。
我们不再拥有一个完全确定的程序,可以对输出结果有确定性。相反,我们的程序运行在量子宇宙中!
量子计算能力和复杂性
我们可以在量子计算程序中几乎同时表示多个值,这真是令人着迷。
然而,当我们开始考虑程序中的 3、4、10、20 或 100 个量子比特时,很容易看到这变得多么复杂。
如果只有一个量子比特被置于叠加态(即有 50/50 的随机概率结果为 0 或 1),理解起来可能不会太困难。毕竟,其他量子比特将保持最初分配的固定值。只有处于叠加态的量子比特会拥有多个可能值。然而,当一个或多个量子比特处于叠加态时,情况变得越来越复杂——但同时也变得越来越强大!
实际上,与经典计算机相比,叠加态是量子程序在性能上获得二次方和指数级提升的核心。由于量子比特可以在单个 CPU 周期内表示多个状态,因此它们能够处理比经典计算机更多的比特排列。
现在,我希望你能逐渐明白,编程量子计算机就像在编程 3D 一样。简单来说,这与经典编程是完全不同的层次!
这听起来有趣吗?
我希望这次对量子计算的初步探索能像激发了我的兴趣一样,激发你的兴趣。
我确实相信,正是这种高维思维使量子计算如此引人入胜,特别是对于那些希望在全新层次上提升技能的程序员来说。
你可以在这里下载问候程序的完整代码示例。
关于作者
如果你喜欢这篇文章,请考虑在Medium、Twitter以及我的网站上关注我,以便获取我未来的帖子和研究成果的通知。
促销预测:零售巨头的案例研究
原文:
towardsdatascience.com/promotion-forecasting-case-study-with-a-retail-giant-4d8dcbae6b5c
了解一个使用机器学习、NLP 和领域专长的需求预测算法如何在短短一年内在国家范围内实现令人印象深刻的 18%缺货和短缺减少。
·发表于 Towards Data Science ·阅读时间 7 分钟·2023 年 12 月 30 日
--

来源:图像由 Dall-E 根据作者的提示创建。
引言:面临的挑战
在零售领域,全球领先的奥尚面临着一个关键挑战:掌握促销预测的艺术。这个故事讲述了我们如何利用机器学习、自然语言处理(NLP)和深厚的领域知识取得突破——在短短一年内将缺货和库存过剩减少了 18%。
场景设定
在促销活动进行的每个商店中,准确预测这些促销商品的需求是一个关键挑战。目标是明确但复杂的:准确对齐供应与不断变化的客户需求,以避免库存过剩并确保客户满意度。
在奥尚零售国际的数据预测团队中,我开始了一个使命。我们的目标是制定一个在不同国家之间适应性强的预测模型,尽可能减少更改。这个为奥尚乌克兰开发的模型后来也应用于罗马尼亚和法国,成为他们促销战略的重要组成部分。
预测挑战
我需要为奥尚乌克兰的 22 家超市提供每日的预测。这些预测分为常规商品和生鲜商品,并延伸至 55 天。目标是预测整个促销活动中店铺和 SKU 级别的需求。
数据和粒度
在商店中预测促销期间的销售量可能非常具有挑战性,尤其是当涉及到各种产品和不可预测的销售模式时。这种方法测试了我们的极限。
我们的策略围绕现有的促销定价、展示和日期展开,因为大约三分之一的产品缺乏历史促销数据。我们的建模工作集中于利用在售产品和促销产品的销售数据。我们专门为促销设计了特征,确保每一个方面都被捕捉到。
关于特征工程
两个模型都受益于各种特征,包括常见的时间特征,如星期几、周、月等。结合销售汇总、产品和商店属性以及促销中的复杂目标编码。
最重要的特征当然是我们包括的促销内容:促销属性如机制、促销折扣率、促销期间的产品展示、上次促销前的周数、折扣金额、促销持续时间,在日常层面上最重要的特征之一是促销动量,这表明第 4 天对于 7 天促销特别突出。
当然,我们还包括了大量的历史促销汇总,一些动量,静态和动态汇总。这些特征使我们能够有效地捕捉零售需求的复杂动态。
双模型策略
我选择使用机器学习算法而不是统计模型,甚至传统线性模型,是因为可以一次性建模和预测广泛范围的产品,并且机器学习特别擅长利用促销、事件、价格等外部因素。
选择 LightGBM 至关重要,因为它的效率和管理非线性复杂特征的能力。直接预测方法以及全特征方法,加上量身定制的交叉验证策略,帮助我们管理不稳定的促销模式并保持稳定性。
这些交叉验证策略截然不同:
-
第一个模型受益于减少窗口方法,以减轻旧促销的历史效应,因为消费者的促销行为变化迅速。
-
第二个模型使用了扩展窗口方法
第一个模型:我们的强大基线
我基于广泛的探索性数据分析(EDA)和观察到的过去促销的*均值是一个强有力的估算器,构建了一个特征丰富的树模型算法。该算法完全由促销销售和属性(以及用于表征产品、商店、促销、事件等的分类特征)驱动。
为了更好地理解模型的结构,可以将其视为‘特征全面预测模型’,这一术语在我的文章(它是最*聚合方法部分的衍生)中详细解释。
该模型旨在根据过去的促销销售数据和属性预测特定商店和 SKU 的未来促销总销售额。我们使用了基于树的算法,根据过去的促销销售、价格和属性数据以及未来促销属性数据来预测未来。
为了改进我们的第一个模型,我们使用了‘镜像 SKU’方法。这涉及到使用现有数据创建每个新产品的镜像,并重建所有必要的特征以进行准确的预测。
第二种模型:处理极端情况

来源:作者提供的图片
第二种模型使用‘直接模型预测’的方法,更具动态性。每天,产品和商店都有其自身的特征,算法会根据产品是否会促销或在后备架上来预测次日的销售情况。
预测基于多个因素,包括倒数第 n 天的销售情况、倒数第 n 天的促销*均值,以及最*四个星期三促销的均值等。特征经过调整,以利用低粒度来捕捉更准确的预测,包括季节性、事件、趋势、促销动量或价格动量等。此模型适用于有足够销售历史的 SKU。
选择正确的指标
我们与需求规划师合作,定义了评估的范围。这包括整个操作在 SKU 和 STORE 级别的聚合需求,排除了在整个 14 天内没有销售的操作。
我们使用了如 WMAPE(加权*均百分比误差)、偏差和在特定阈值下的准确操作数量等指标,结合了总过剩库存和与规划师预测相比的总缺货数量等业务指标。这些指标通过对整年数据的详细回测计算得出,以验证我们的算法在任何时间框架内的稳健性,并考虑了趋势和季节性。
数据预处理
促销对所有利益相关者来说是一个复杂的数据处理过程,因此数据准备至关重要。这包括:从促销目录数据集中重建促销,利用 EDA 确定实际的促销开始日期,并填补有促销但没有销售的数据空白。我们还优化了促销机制,并解决了如促销价格高于最新货架价格等定价异常问题,从而提高了模型的准确性。我通过纠正促销日期的不准确性和解决数据预处理中通过 EDA 揭示的各种短缺来“微调”数据。此步骤对于使我们的预测与实际销售对齐至关重要。
一个显著的发现是促销开始时的实际短缺,没有销售。我们假设这是一个配送问题,销售加速后的缺货是由于规划者对单位的指挥不足。要找到这些短缺,简单的方法是追踪高轮换产品的无销售情况,观察它们的最新销售,验证同一天、同品牌和同系列内是否有促销活动对需求造成了侵蚀,如果这些条件都满足,那么产品因指挥不足而缺货的可能性很高。
另一个有意义的短缺是促销结束时的缺货,对于中高轮换产品,存在累计无销售的模式,或者促销势头中断并下降直到无销售,当我们发现这些情况时,可以对缺货潜力进行假设。如何轻松找到这种情况?反向操作,找到促销中的最后一天无销售或急剧下降的日期,并回顾尝试找到势头变化,与另一家商店的相同产品促销进行比较以确认你的偏见。
解决有限历史数据的问题
对于具有稀疏促销历史的新产品,我开发了“镜像 SKU”方法,显著提高了预测准确性。该方法量体裁衣,旨在透明,并遵循了这一启发式规则:
对于每个没有历史销售记录的 SKU(我们的目标)……
-
根据产品销售属性生成高质量的 SKU 候选,选择最优子集作为 Brand X 子系列。
-
计算候选与目标之间的产品描述距离(使用 NLP 算法如 TFIDF 和余弦距离)。
-
计算目标和候选之间的促销价格距离作为绝对差异
-
通过最小化两个指标对 2.和 3.的推荐进行排名。
-
提取 3 个镜像 SKU。
将该方法与我们上面定义的第一个模型的交叉学习相结合,导致新产品预测的指标提高了 25/30%。
超越实施
在模型部署后,我们提供了详细的销售分析和置信区间。这些见解赋予了需求规划师更多的数据驱动决策能力。
与利益相关者密切合作对于将模型适应零售复杂性和定制化至具体需求至关重要。他们的见解对于改进算法以实际应用于零售环境中极为宝贵。
影响
我们的模型不仅超越了传统的预测方法,还在需求规划师的预测上提高了 15%。这使得每年为规划师节省了超过 30,000 小时,并且在全国范围内减少了 Auchan 乌克兰的 18%库存过剩和短缺,从而带来了 100,000 美元的利润。
结论
这段旅程突显了数据科学在零售预测中的变革力量。这证明了有针对性、数据驱动的策略如何带来实质性的运营改进和效率。




















浙公网安备 33010602011771号