TowardsDataScience-2023-博客中文翻译-四十七-
TowardsDataScience 2023 博客中文翻译(四十七)
什么是超几何分布
原文:
towardsdatascience.com/understanding-the-hypergeometric-distribution-e6540c7fec3c
解析数据科学中一个较不知名的分布
·发布于 Towards Data Science ·5 分钟阅读·2023 年 6 月 21 日
--

图片由 Roth Melinda 提供,Unsplash
背景
二项分布 是一个在数据科学内外都非常著名的分布。然而,你听说过它的较不知名的“亲戚”超几何分布 吗?如果没有,这篇文章将为你详细解释它是什么以及它为何对我们数据科学家有用。
补充视频。
直觉
超几何分布衡量在n次试验(样本)中成功的k的概率,没有替换,给定有关总体的一些信息。这与二项分布非常相似,但有一个关键区别是没有替换。因此,每次抽取/试验的成功(或结果)的概率会变化,而在二项分布中,成功(和失败)的概率是固定的。
一个易于理解的例子是确定从一副标准扑克牌中随机抽取 20 张牌中抽到全部 4 张国王的概率。如果我们抽到一张国王,那么抽到下一张国王的概率将与第一次不同,因为总体构成已经改变。因此,成功的概率是动态的。
理论
概率质量函数(PMF) 的超几何分布如下所示:

作者的 LaTeX 公式。
其中:
-
n 是试验的次数
-
k 是成功的数量。
-
N 是总体规模。
-
K 是总体中的成功总数。
-
X 是来自超几何分布的随机变量。
感兴趣的读者可以在这里找到 PMF 的推导。
类似括号的符号指的是二项系数:

作者提供的 LaTeX 公式。
阶乘 表明我们在处理组合和排列。你可以在我之前的博客中阅读更多内容:
关于组合和排列及其区别的简要描述。
towardsdatascience.com](/a-primer-on-combinations-and-permutations-d654aacea292?source=post_page-----e6540c7fec3c--------------------------------)
分布的均值由以下公式给出:

作者提供的 LaTeX 公式。
示例
让我们回到之前的例子,从一副普通的 52 张牌中随机抽取 20 张卡片,计算其中 4 张国王的情况。我们拥有的信息是:
-
N = 52:牌堆中的卡片数量。
-
n = 20:我们抽样的卡片数量。
-
k = 4:我们希望得到的国王数量(成功)。
-
K = 4:牌堆中的国王数量。
将这些数字代入 PMF:

作者提供的 LaTeX 公式。
因此,概率非常低。这是合理的,因为从一副牌中抽到一张国王的概率是 ~0.077 (1/13), 所以在样本更小的情况下,如我们上面所示,这个概率会进一步降低。
如果你想玩一些数字和不同的情境,我在这里链接了一个超几何分布计算器。
代码与图表
上述例子展示了超几何分布应用的有用示范。然而,我们可以通过将 PMF 绘制为成功数量 k 的函数来获得更全面的视角。
以下是一个 Python 绘制的图示,展示了在我们期望的国王数量 k 变化时的例子:
作者的 GitHub Gist。

作者在 Python 中生成的图表。
正如我们所见,从 20 张卡片样本中得到 5 张国王的概率为 0,因为牌堆中没有五张国王!我们得到的最可能的国王数量是 1。
现在让我们考虑一个新问题。随机抽取 30 张牌样本中,黑桃的超几何分布是多少?
作者提供的 GitHub Gist。

由作者用 Python 生成的图。
我们在 30 张牌的样本中得到 8 张黑桃的可能性最大。正如图示,很难在样本中得到没有黑桃的情况。
应用
超几何分布涉及到许多领域:
-
扑克牌中赢得一手的概率
-
投票人群分析
-
制造业中的质量控制
-
种群中的遗传变异
因此,超几何分布是你在数据科学职业生涯中最有可能遇到的内容,因此值得了解。
总结与进一步思考
在这篇文章中,我们讨论了超几何分布。这与二项分布非常相似,但成功的概率会变化,因为我们是在不放回的情况下抽样。这个分布在数据科学中非常强大,广泛应用于质量控制和赌博行业。因此,作为数据科学家,了解它非常值得。
完整代码可以在我的 GitHub 上找到:
[## Medium-Articles/Statistics/Distributions/hypergeometric.py at main · egorhowell/Medium-Articles
在我的中等博客/文章中使用的代码。通过创建账户来贡献 egorhowell/Medium-Articles 的开发…
另一个事项!
我有一份免费的通讯,Dishing the Data,我在其中每周分享成为更好数据科学家的技巧。没有“废话”或“点击诱饵”,只有来自实践数据科学家的纯粹可操作见解。
[## Dishing The Data | Egor Howell | Substack
如何成为更好的数据科学家。点击阅读由 Egor Howell 撰写的《Dishing The Data》,一份 Substack 出版物,内容包括…
newsletter.egorhowell.com](https://newsletter.egorhowell.com/?source=post_page-----e6540c7fec3c--------------------------------)
与我联系!
参考文献
-
关于超几何分布的更多信息:
brilliant.org/wiki/hypergeometric-distribution/
理解集成学习中多样性的重要性
原文:
towardsdatascience.com/understanding-the-importance-of-diversity-in-ensemble-learning-34fb58fd2ed0
多样性在提升集成性能中的作用
·发布于 Towards Data Science ·阅读时长 9 分钟·2023 年 1 月 2 日
--

集成学习是一种将多个模型结合以获得更好结果的机器学习技术。只有近年来,随着计算速度的提高,集成学习才成为应对机器学习领域困难问题的最有效技术之一。这种方法在大多数机器学习竞赛的获胜解决方案中使用,奖金高达 10 万美元。
我们如何选择最佳的集成成员,将它们组合成一个更强大的模型?在本文中,我们将探讨如何选择模型进行集成以及如何对它们进行聚合。
什么是多样性?
集成学习基于结合多个模型的概念,这些模型被称为弱学习者。这个名字来源于这样一个想法:个体集成成员不需要非常准确。只要它们比随机模型稍微好一点,结合它们就会有益。多样性是集成学习中的一个重要概念,指的是集成中的个体模型的预测应该尽可能地彼此不同。这是因为不同的模型可能会犯不同类型的错误。通过结合多样化模型的预测,我们可以减少整体的集成误差。
我们如何增加多样性?
多年来,开发了几种集成学习算法。每种算法都有独特的方式来实现多样性。这些方法包括:
在不同的训练数据子集上训练每个模型。当样本是有放回地抽取时,称为自助法。当抽样是无放回地进行时,称为粘贴法。这也有助于减少集成预测的方差,从而改善其泛化能力。自助法中的多样性理念在随机森林和极端随机树中得到了进一步的发展。前者通过选择每次分裂时树可用的随机特征数量来实现多样性,而后者通过进行随机分裂来降低树之间的相关性。
使用不同特征集训练每个模型。可以使用可用特征的不同组合或原始特征的不同转换来训练集成。这有助于捕捉数据的不同方面,从而提高性能。
使用不同类型的算法训练每个模型。这是投票和堆叠元模型中使用的方法。通过使用不同的算法,集成中的个别模型可以捕捉数据中的不同模式并产生不同类型的错误。

投票集成。包括多样化的集成成员是很重要的。[1]
然而,多样化的预测并不总是更好。
良好的和不良的多样性
在多数投票集成中,浪费 投票的概念可以用来理解集成的多样性。如果集成已经是正确的,分类器之间的低分歧表明已经有一些投票被浪费了,因为无论个别分类器的投票结果如何,都会做出相同的正确决策。这被称为良好的多样性,它测量了集成已经正确时的分歧。在这种情况下,更多的分歧意味着更少的浪费投票。相反,不良的多样性测量集成不正确时的分歧。在这种情况下,任何分歧都代表着浪费的投票,因为个别分类器没有为集成做出的正确决策做出贡献。为了最大化集成的效率,重要的是增加良好的多样性并减少不良的多样性,这可以通过减少浪费投票的数量来实现。
从准确性角度来看,另一种思考方式是这样的。例如,假设决策树模型在识别狗方面表现很好,但在识别猫时有困难。另一方面,逻辑回归模型在识别猫方面表现更好,但在识别狗时有困难。这些算法各有优缺点,会对数据产生不同类型的错误。通过结合这两个模型的预测,我们可以创建一个总体准确性高于任何单个模型的集成。这就是良好多样性的一个例子。我们可以再添加一个第三个模型,该模型在分类猫和狗方面都很差。第三个模型将增加集成的多样性,但不会带来任何好处。这被认为是差的多样性。[2]
我们如何测量多样性?
让我们称f₁, f₂, … fₙ为集成中不同模型的预测。
多样性度量有两种类型:成对和全局。成对度量需要对每一对fᵢ, fⱼ进行计算。最终,你会得到一个nxn**矩阵。全局度量是在整个预测矩阵上计算的,它们由单一值表示。以下列表绝非详尽,你可以在 Kuncheva 的论文中找到更多度量,参考文献中有列出。
在公式中,0 表示预测错误,1 表示预测正确。例如,N⁰¹表示第一个分类器正确而第二个错误的次数。
皮尔逊相关系数 计算集成中相似性的最简单方法是使用皮尔逊相关系数。如果两个模型的预测高度相关,这意味着它们非常相似。当ρᵢⱼ = 0 时获得最大多样性,而两个产生完全相同输出的分类器将有ρᵢⱼ = 1。即使在软投票集成中,当预测是概率时,也可以计算相关系数。
以下指标用于二元类别预测。
分歧 正如名字所示,它表示预测之间的分歧程度。通过将预测不同的次数除以总的预测次数来计算分歧。分歧值介于 0(没有预测不同)和 1(每个预测都不同)之间。

分歧度量。[2]
Yule 的 Q Yule 的 Q 值介于-1 和 1 之间。这个度量提供了重要的信息:正值表示模型在正确分类相同对象,而在不同对象上的错误将导致负 Q 值。0 的值表明预测是独立的。

Yule 的 Q。[2]
熵 以下度量是一个全局指标。它在整个预测矩阵上计算。熵基于这样一个思想:当不一致性达到最大时,一半的预测是零,而另一半是 1。在以下公式中,L 是集成模型的总成员数,l是正确分类实例 zⱼ的分类器总数。

二元预测的熵。[2]
Python 实现
def coefficients(preds):
A = np.asarray(preds[:, 0], dtype=bool)
B = np.asarray(preds[:, 1], dtype=bool)
a = np.sum(A * B) # A right, B right
b = np.sum(~A * B) # A wrong, B right
c = np.sum(A * ~B) # A right, B wrong
d = np.sum(~A * ~B) # A wrong, B wrong
return a, b, c, d
def disagreement(preds, i,j):
L = preds.shape[1]
a, b, c, d = coefficients(preds[:, [i, j]])
return float(b + c) / (a + b + c + d)
def paired_q(preds, i, j):
L = preds.shape[1]
# div = np.zeros((L * (L - 1)) // 2)
a, b, c, d = coefficients(preds[:, [i, j]])
return float(a * d - b * c) / ((a * d + b * c) + 10e-24)
def entropy(preds):
L = preds.shape[1]
tmp = np.sum(preds, axis=1)
tmp = np.minimum(tmp, L - tmp)
ent = np.mean((1.0 / (L - np.ceil(0.5 * L))) * tmp)
return ent
构建投票分类器
现在让我们比较两个集成模型,以评估其成员的多样性。在以下示例中,我使用了pycaML,它允许用几行代码训练和比较模型及集成模型。
我为第一个集成模型选择了 9 个模型,为第二个集成模型选择了 10 个模型。每个模型都使用默认参数进行训练。数据集可在UCI 存储库中获得。
模型比较 为了有一个基准来比较集成模型,下面的图展示了表现最好的 10 个模型。我们将重点关注 F1 得分。
from pmlb import fetch_data
from pycaML import ClassificationExperiment
name = 'diabetes'
df = fetch_data(name, return_X_y=False)
df.head()
df.to_csv(f'{name}.csv', index=False)
from pycaML import ClassificationExperiment
exp = ClassificationExperiment(name = name)
exp.load_data(f'{name}.csv', target='target')
exp.start()
exp.result.loc[:, exp.result.columns.str.contains('Test')].head(10)

在糖尿病数据集上表现最好的 10 个模型,由 pycaML 生成。图像由作者提供。
预测矩阵可以如下计算。最终,我们将得到一个 154x19 的矩阵,每一行是测试集中的一个实例,每一列是一个模型。得到矩阵后,我们可以将其拆分为两个集成模型进行比较。第一个包含奇数索引的模型,而第二个包含偶数索引的模型。
preds = pd.DataFrame()
for preds in exp.result.index:
preds[preds] = exp.predict(preds)
preds.shape
# (154, 19)
ensemble_1 = preds.iloc[:, 1::2]
ensemble_2 = preds.iloc[:, ::2]
def disagreement_matrix(preds):
res = np.zeros((preds.shape[1], preds.shape[1]))
for i in range(preds.shape[1]):
for j in range(i, preds.shape[1]):
res[i, j] = disagreement(preds, i, j)
res[j, i] = res[i, j]
return res
sns.heatmap(d, annot=True, cmap='coolwarm', vmin=0, vmax=1, cbar = False)
查看集成模型 1 的不一致矩阵,似乎 Perceptron 是与其他模型不一致最多的模型。另一方面,提升模型之间的不一致较少。

集成模型 1 的不一致矩阵。图像由作者提供。
集成模型 2 包含不同的模型。总体来看,不一致性相当低。与其他模型不一致最多的是 Passive Aggressive Classifier。

集成模型 2 的不一致矩阵。图像由作者提供。
由于集成模型 1 具有更高的熵,这意味着其多样性更高。

熵得分。图像由作者提供。
现在让我们通过投票来建立最终模型,这种元模型被称为硬投票。两个集成模型使用与之前相同的指标进行评估,即 F1 得分。
preds_voting = preds.mode(axis=1)[0]
print(f'F1 score for {model}: {f1_score(preds_voting, y_test, average="macro"):.3f})

模型评估后的结果。图像由作者提供。
最终,我们展示了更高的多样性(集成模型 1)导致了更好的集成模型。比较中找到的最佳模型是 Extra Trees,其 F1 得分为 0.71。最终模型的表现比在数据集上训练的最佳模型高出不少,达到了 0.764 的得分。
结论
集成学习是一种强大的机器学习技术,它涉及训练多个模型并结合它们的预测以实现更好的性能。我们发现,当集成中的模型多样性增加时,集成的性能也会提高。
这一发现突显了集成学习中多样性的价值。在构建集成模型时,考虑模型之间的多样性对于实现最佳性能至关重要。
因此,下次你在进行机器学习项目并考虑使用集成学习时,记得优先考虑模型之间的多样性。这可能会对模型的最终性能产生重要影响。
喜欢这篇文章吗?通过订阅我的通讯, 数据面试,每周获取数据科学面试问题。
此外,你还可以在 LinkedIn上找到我。
参考文献
[1] Géron, A. (2019). 实用机器学习与 Scikit-Learn、Keras 和 TensorFlow: 构建智能系统的概念、工具和技术(第 2 版)。O’Reilly.
[2] Kuncheva, L.I., Whitaker, C.J. 分类器集成中的多样性测量及其与集成准确性的关系。 机器学习 51, 181–207 (2003). doi.org/10.1023/A:1022859003006
理解推动产品的多层数据管理
产品领导者需要知道如何通过数据解决阻碍
·发表于 Towards Data Science ·阅读时间 11 分钟·2023 年 12 月 19 日
--

图片由 American Heritage Chocolate 提供,来源于 Unsplash
当我看到上面那层层叠叠的蛋糕时,我立刻想吃它,但我也意识到,这些层次与推动现代数据产品的数据管理层次之间有许多有用的相似之处。
就像每一层蛋糕都涉及巧克力一样,每一层数据管理也涉及数据。这一点并不意外。
但每一层都是独特的。蛋糕有黑巧克力、牛奶巧克力和白巧克力层。成功制作蛋糕的每一层需要不同的成分,正如成功执行我们数据管理流程的每一层也需要不同的成分一样。
因此,我们来到了本文的重点:实现产品愿景需要在每个开发层次上具备正确的成分。对于数据密集型产品来说,不理解实现产品所需的数据管理不同层次,就像不知道我们要烘焙的蛋糕的层次一样令人无助。
如果我们(产品领导者)仅仅理解最上层(白巧克力),当团队在其他层次遇到困难时,我们将难以解锁团队的进展。我们可能会失败地理解为什么我们一开始会被阻塞,并错过为团队设立成功的机会。
产品领导层负责协商路线图、交付物和时间表。当我们不理解成功实现产品愿景所需的内容时,我们就把产品的交付置于风险之中。
在此基础上,让我们深入探讨数据管理的不同层次,产品领导者应当了解这些层次,以及我们如何解锁在数据管理堆栈的任何层次上遇到摩擦的团队。
有关更多数据工程建议和反思 在 Substack 上找到我!并收听我的 YouTube 频道 以获取实际操作教程。
标记数据产品中的数据管理层次
这里是我在现代分析堆栈中遇到的层次,我与包括娱乐、能源、金融、零售、医疗保健和广告在内的多个行业的团队合作:
-
原始数据,通常非常详细且未经精炼/处理
-
衍生数据,由处理和转化原始数据生成
-
汇总数据,即原始和/或衍生数据的聚合
-
整理后的产品数据,结合了衍生和/或汇总的数据
在某些环境中,这些层次之间的界限可能会变得模糊。通常,数据产品生命周期开始时观察到的“原始数据”也包含了大量的软件工程。但是为了简化概念,我们将“原始数据”视为存在于最细粒度和原始格式的数据,这些数据对数据工程方面的操作是可访问的。
对产品领导者的快速热点评:数据 就是 你的产品

Cullan Smith 在 Unsplash 上的照片
我的整个职业生涯都在策略、设计、工程和解释分析产品。不是每个项目都成功,多年来我观察到,最容易失败的方式是将数据及其周围的流程/管道视为简单商品。
数据起初正是如此:一种商品。但通过业务逻辑、背景和产品/设计权衡的应用,你的数据变成了一个产品。这个产品对其他内部团队和外部应用程序都有价值。
这就是为什么我如此坚定地帮助产品领导者理解数据的不同层次。在现代技术环境中,我们需要了解原始数据如何转变为产品,以便有效地管理交付结果的各个方面。
产品领导者,请听我说:数据不再是你插入前端应用的简单、未过滤的资源。数据应在到达应用层之前,已经 预加载了产品价值。
原始数据:你需要了解的内容
原始数据的简单解释是,它是你的数据团队可以访问的最详细和最细粒度的信息。例如:
-
原始交易详情
-
原始客户信息
-
原始事件/仪器数据
-
来自第三方系统的混乱数据
在这层面,产品领导者应主要关注两件事:
-
我们是否拥有所需的所有原料?
-
原料是否高质量?
如果这两个问题的答案都是肯定的,那么我们就处于良好的状态,我们的工程师和分析师更接近于成功。
如果其中一个问题的答案是否定的,那么我们需要做一些工作来确保团队的成功。这通常需要负责:
-
与领导者和利益相关者设定期望。
-
在可用原料和产品愿景之间进行权衡。
-
与其他团队合作,规划引入新的(或改进现有的)原料的工作。
我们可能会在“原始数据质量”的话题中迷失,因此目前不妨沿用老话:垃圾进,垃圾出。即使在产品领导层的最高级别,倡导数据质量也应该是优先考虑的。
原始数据层对于非技术领导者来说可能有点抽象,并且在规划路线图和交付时间表时不常被考虑。具有讽刺意味的是,这一层的数据不足会大幅降低将创新理念推向市场的能力。当我们缺乏成功的关键原料时,想法往往难以转化为产品。
派生数据:将上下文应用于原始数据
想象一下你去餐厅点餐。如果服务员把一堆原始蔬菜扔到你桌子上,根部还沾着泥土,你会有什么感觉?当业务用户连接到“自助分析”时,接触到脏乱的原始数据,可能会有类似的感觉。

照片由Markus Spiske提供,来源于Unsplash
派生数据层是将原始数据提炼和转换为推动产品的燃料的地方。这涉及到清理数据,并且通常还涉及应用对下游过程有重大影响的业务逻辑和假设。确保在这一层实施的业务逻辑正确且一致是确保产品成功的重要部分。
这是产品领导者需要了解的关于派生数据层的内容:
-
存在提取、转换和加载(ETL)数据的过程。
-
你的团队负责其中的一些 ETL 过程。
-
你的团队可能依赖于其他团队拥有的 ETL 过程。
-
在这一层中业务逻辑和假设是不可避免的。
为产品领导者提炼出“所以怎样”:
-
在 ETL(提取、转换、加载)过程将原始数据转换为更具业务相关性的格式时,会应用业务逻辑和假设。
-
如果任何业务逻辑或假设是不完整或不正确的,这将对你的产品产生负面影响。
-
如果任何不完整/不正确的定义由其他团队负责,我们需要进行跨团队的外交以解决问题。
-
在可用原料和产品愿景之间做出权衡,是工程和产品之间的舞蹈。
如果你曾经看到过两个产品(或仪表板)在一个重要指标上不一致,那么根本原因很可能存在于数据管理的这一层。
表面上看,这似乎是一个纯粹的数据工程问题,但请记住:数据就是产品。这绝对是产品领导的关注点,产品领导可以通过帮助高级管理人员理解消除摩擦的价值主张,从而积极推动事情向前发展,为产品的生命线(数据)创造成功的条件。

照片由 Iván Díaz 提供,发布在 Unsplash 上
在这一层投资资源往往被高级产品领导视为纯粹的成本(投资回报率低或没有),这里的项目经常被“全新”功能开发所搁置。总是有压力去开拓新领域并推出创新产品。在这种对话中,进行有效沟通的关键在于为非技术领导者框定叙事,以便他们理解消除各种技术流程中的摩擦如何将产生的价值倍增。
驾驶一个漏气的轮胎会减慢行程,并可能造成进一步的损坏,导致更高的成本和错过的截止日期。投资于修复漏气的轮胎在帮助你的团队和产品到达目的地方面是价值的倍增器。这项投资还可能导致发现新的产品机会,因为你的团队能够灵活应变,并在开发有价值的功能时迅速行动。
汇总数据:既是福音也是诅咒
当我刚进入分析领域时,我很少看到分析师参与到衍生数据层。数据由独立的数据工程团队汇总并交付到汇总表格中,然后更接近业务背景的分析师会从那里开始。流程如下:
-
选择你喜欢的商业智能(BI)工具
-
寻找数据分析师或商业智能分析师
-
连接到包含汇总表格的数据库
-
根据需要应用 SQL 以连接和交付数据到 BI 工具
-
连接到汇总表格/视图并构建仪表板
这种模式今天仍然非常相关,如果你认识到这种模式,那么你可能会遇到这些问题:
-
“我们为什么无法获取 XYZ 指标的可见性?”
-
“我们为什么无法深入到特定交易/产品/客户?”
-
“我们如何从仪表板中获取更多可操作的洞察?”
-
“为什么我们的前两个仪表板显示不同的数字?”
这些问题驱使许多现代分析师深入工程堆栈,承担派生数据层的责任。随着数据生成量的增加,更多的工程流程正在开发以管理大规模数据。几十年前,我们有“IT”团队,这些团队中的一部分分叉成为数据工程,之后又分叉成数据平台工程、分析工程等。

照片由Jens Lelie提供,来源于Unsplash
角色仍在不断发展,但有一点很明确:数据团队的工作范围和责任正在扩大。而在管道中注入产品专业知识的时机和地点的重要性也在增加。
产品领导者需要了解以下内容,以跟上时代的步伐:
-
并非所有摘要都是相同的。
-
摘要表可能会轻易过度简化你所寻求的价值点。
-
没有产品意图的数据总结会导致洞察力浅薄。
-
产品领导者应意识到粒度与性能之间的反向关系,以及这如何影响产品权衡。
当数据工程师与业务脱节但负责为分析师构建摘要表时,挑战会在摘要层显现。当业务背景未融入数据工程工作中时,数据工程师通常会倾向于总结尽可能多的数据,以便商业智能工具(或其他下游系统)能够处理这些数据。分析师则使用这些数据,因为这是他们所拥有的,但他们难以深入挖掘以提取业务同事所需的信息。
多年来,我观察到团队在面对上述挑战时,会执行一个或多个以下操作:
-
引入更多商业敏锐的工程师(分析工程师),以弥合商业与工程之间的差距。
-
更少依赖试图在一个地方总结所有内容的通用非规范化表(宽广的覆盖面,浅显的洞察深度)。
-
更多依赖临时分析,虽然能深入数据,但不适用于通用的摘要表。
-
开发针对特定产品需求的目标摘要表,紧密结合产品目标。
上述第四点引出了数据管理产品领导者需要了解的最终层级:精心策划的产品数据。
精心策划的产品数据:一台运转良好的机器

照片由Tim Mossholder提供,来源于Unsplash
并非所有的摘要数据都准备好接受大舞台的考验。好的产品设计不是偶然的,好的数据架构也不是。
每个组织都有“自下而上”的数据管道。也就是说,管道从大量数据开始,经过转换和汇总,直到可被下游系统(如仪表板)使用。我们并不总是知道结果数据将如何使用,但我们知道在使用之前它需要被汇总。
这种方法对于各种工作仍然很有用,从自动化工作流到自助分析。然而,仅仅对汇总数据进行表面上的了解往往无法满足产品需求。它会留下大量未被利用的价值,难以从详细的数据中提取深层次的见解。
我们经常需要对详细数据应用特定的业务逻辑和假设,以生成能够为我们的产品提供有洞察力、针对性信息的数据源。
这需要一种“自上而下”的方法:
-
从产品创意开始,或从具有产品洞察力的工程师开始,他们能够看到数据中存在的产品潜力。
-
确定能够支持这些创意的数据。
-
协作(工程 + 产品)以发现利用现有资源可以实现的目标。
-
展示产品/技术权衡并定义最小可行产品数据层。
-
设计数据架构;从产品层的需求开始,向后工作,经过其他数据管理层。
这确实是一个过程。它需要能够与工程师沟通的产品人员,并且需要能够与产品沟通的工程师。理想情况下,它涉及具有产品和工程双重思维的高级工程师。数据专业人员在两者之间保持良好的平衡是非常宝贵的。投资于这种价值可以开辟创新的数据产品;不投资于这种价值则往往导致乏善可陈的“自下而上”的分析。
产品/工程的平衡非常重要。完全由工程人员组成的数据团队,如果没有产品意识,注定会产生提供了数据难以获取的部分值的庞大表格。完全由产品人员组成且缺乏技术意识的领导者可能会在错误的地方施加压力,困惑于为什么他们的团队无法让数据发挥作用。
成功的现代分析团队拥有产品愿景和技术技能,能够开发精心策划的产品层,能够从最细粒度的数据中开辟价值路径。实现这一目标的技能既包括工程,也包括产品。我称之为产品工程。
知道就是胜利的一半

由Greg Rakozy拍摄,来源于Unsplash
在这篇文章中,我们深入探讨了数据产品团队幕后发生的一个方面。还有很多内容需要讨论,因为成功地应对现实世界的产品交付涉及更多的工程,识别用户需求,搞清楚这些用户真正需要什么,并了解那些可能影响资源供应链的办公室政治,这些因素都可能决定团队的成功与否。
我的目标是让数据密集型产品团队的领导者考虑如何为他们的团队铺平成功的道路。当产品期望与工程现实不一致时,所有涉事人员都可能感到压力和沮丧。希望本文中讨论的话题能促使读者考虑数据工程期望和责任在其组织内部的变化。
让我们再一次提到影响数据密集型产品交付的数据管理层面。牢记这些要点可以使工程和产品之间的对话更加富有成效:
-
原始数据层
-
派生数据层
-
汇总数据层
-
精心策划的产品数据层
成功确定产品需求范围、在组织内协商优先级、与高级领导设定期望,并解决数据团队的问题,都是交付数据产品的重要因素。
提高对幕后各种数据管理层的认识将有助于你实现团队的成功,并提供更一致的结果。数据确实重要,也许与数据本身同样重要的是你开发的那些使数据成为推动产品的宝贵资源的过程。
了解时间序列趋势
原文:
towardsdatascience.com/understanding-time-series-trend-addfd9d7764e
确定性趋势与随机趋势,以及如何处理它们
·发表于Towards Data Science ·阅读时间 6 分钟·2023 年 3 月 14 日
--

图片由Ali Abdul Rahman拍摄,来源于Unsplash
检测和处理趋势是时间序列建模的关键步骤。
在本文中,我们将:
-
描述时间序列的趋势及其不同特征;
-
探索如何检测它;
-
讨论处理趋势的方法;
了解趋势
趋势作为时间序列的构建块
在任何给定时间,时间序列可以分解为三个部分:趋势、季节性和剩余部分。

时间序列的加法分解
趋势表示时间序列水平的长期变化。这种变化可以是向上(水平上升)或向下(水平下降)。如果变化在一个方向上是系统性的,那么趋势就是单调的。

美国 GDP 时间序列具有向上和单调的趋势。数据来源于参考文献[1]。作者提供的图片。
趋势作为非平稳性的原因
如果时间序列的统计属性不变,则该时间序列是平稳的。这包括时间序列的水平,在平稳条件下保持不变。
因此,当时间序列表现出趋势时,平稳性假设并未得到满足。建模非平稳时间序列具有挑战性。如果不处理,统计测试和预测可能会产生误导。这就是为什么在建模时间序列之前检测和处理趋势很重要。
对趋势的适当描述会影响建模决策。这进一步影响预测性能。
确定性趋势
趋势可以是确定性的,也可以是随机的。
确定性趋势可以用一个明确的数学函数建模。这意味着时间序列的长期行为是可预测的。任何偏离趋势线的情况只是暂时的。
在大多数情况下,确定性趋势是线性的,可以写成如下形式:

线性趋势的方程。系数b 是连续期间趋势的预期变化。系数 a 是截距。
但,趋势也可以遵循指数或多项式形式。

指数趋势方程。通过对两边取对数,这一趋势可以变为线性。
在经济学中,有几个时间序列的例子呈指数增长,例如 GDP:

美国 GDP 时间序列。原始趋势是指数的,但在对数变换后变为线性。数据来源于参考文献[1]。作者提供的图像。
具有确定性趋势的时间序列称为趋势平稳。这意味着去除趋势成分后,序列变得平稳。
线性趋势也可以通过将时间作为解释变量来建模。以下是你可以这样做的一个例子:
import numpy as np
import pandas as pd
from statsmodels.tsa.arima.model import ARIMA
# https://github.com/vcerqueira/blog/blob/main/data/gdp-countries.csv
series = pd.read_csv('data/gdp-countries.csv')['United States']
series.index = pd.date_range(start='12/31/1959', periods=len(series), freq='Y')
log_gdp = np.log(series)
linear_trend = np.arange(1, len(log_gdp) + 1)
model = ARIMA(endog=log_gdp, order=(1, 0, 0), exog=linear_trend)
result = model.fit()
随机趋势
随机趋势可以随机变化,这使得它们的行为难以预测。
随机游走是具有随机趋势的时间序列的一个例子:
rw = np.cumsum(np.random.choice([-1, 1], size=1000))

随机游走时间序列,其趋势突然和不可预测地变化。作者提供的图像。
随机趋势与单位根、积分和差分有关。
具有随机趋势的时间序列被称为差分平稳。这意味着通过差分操作可以使时间序列变得平稳。差分指的是取连续值之间的差异。
差分平稳时间序列也称为积分时间序列。例如,ARIMA(自回归积分滑动平均)模型包含一个特定的术语(I)用于积分时间序列。这个术语涉及应用差分步骤直到序列变得平稳。
最终,差分平稳或积分时间序列的特征是单位根。不深入数学细节,单位根是非平稳时间序列的一个特征。
预测含义
确定性和随机趋势对预测有不同的影响。
确定性趋势在整个时间范围内具有恒定的方差。在线性趋势的情况下,这意味着斜率不会改变。但现实世界中的时间序列展示了复杂的动态,趋势在长期内发生变化。因此,使用确定性趋势模型进行长期预测可能会导致性能较差。假设方差恒定会导致预测区间过窄,从而低估了不确定性。

随机游走的多个实现。作者提供的图像。
随机趋势假定随着时间变化。因此,时间序列的方差会随着时间增加。这使得随机趋势在长期预测中更具优势,因为它们提供了更合理的不确定性估计。
如何检测趋势
随机趋势可以通过单位根测试来检测。例如,扩展的迪基-福勒测试或 KPSS 测试。
扩展的迪基-福勒(ADF)测试
ADF 测试检查自回归模型是否包含单位根。测试的假设包括:
-
原假设:存在单位根(时间序列不是平稳的);
-
备择假设:不存在单位根。
这个测试在statsmodels中可用:
from statsmodels.tsa.stattools import adfuller
pvalue_adf = adfuller(x=log_gdp, regression='ct')[1]
print(pvalue_adf)
# 1.0
参数regression=‘ct’用于在模型中包括一个常数项和确定性趋势。正如你可以在文档中检查的那样,这个参数有四个可能的备选值:
-
c: 包括一个常数项(默认值);
-
ct: 常数项加线性趋势;
-
ctt: 常数项加线性和二次趋势;
-
n: 无常数或趋势。
选择应包括哪些项非常重要。错误的包含或排除某项可能会显著降低测试的效能。在我们的案例中,我们使用了ct选项,因为对数 GDP 系列显示出线性确定性趋势行为。
KPSS 测试
KPSS 测试也可以用来检测随机趋势。测试假设相对于 ADF 是相反的:
原假设:时间序列是趋势平稳的;
备择假设:存在单位根。
from statsmodels.tsa.stattools import kpss
pvalue_kpss = kpss(x=log_gdp, regression='ct')[1]
print(pvalue_kpss)
# 0.01
KPSS 拒绝了原假设,而 ADF 没有。因此,两种测试都表明存在单位根。请注意,一个时间序列可以具有包含确定性和随机成分的趋势。
那么,你如何处理单位根呢?
如何处理趋势
我们已经探讨了如何使用时间作为解释变量来考虑线性趋势。
处理趋势的另一种方法是差分。不是处理绝对值,而是对时间序列在连续期间的变化建模。
单次差分操作通常足以实现平稳性。然而,有时你需要进行多次差分。你可以使用 ADF 或 KPSS 来估计所需的差分步骤数。pmdarima库在ndiffs函数中封装了这个过程:
from pmdarima.arima import ndiffs
# how many differencing steps are needed for stationarity?
ndiffs(log_gdp, test='adf')
# 2
在这种情况下,对数 GDP 系列需要 2 次差分步骤才能实现平稳性:
diff_log_gdp = log_gdp.diff().diff()

对数 GDP 时间序列的二次差分。图像由作者提供。
关键要点
-
趋势是时间序列的一个关键组成部分;
-
展现趋势的时间序列是非平稳的,因为均值水平不是恒定的;
-
时间序列的趋势可以是确定性或随机的。确定性趋势用明确的数学函数建模,而随机趋势可以随机变化;
-
可以使用单位根检验如 ADF 或 KPSS 来检测随机趋势;
-
差分操作用于从时间序列中去除随机趋势。
感谢阅读,下次故事见!
参考文献
[1] 各种国家的 GDP 时间序列数据(许可证:CC0:公共领域)
[3] 随机游走——介绍及其性质 由 Ben Lambert 制作
了解我们失去的东西
我们如何应对大型语言模型中的灾难性遗忘
·
关注 发表在 Towards Data Science ·8 min read·2023 年 5 月 23 日
--
图 1:遗忘的共享体验。 图像由 OpenAI 开发的 DALL·E 生成。
遗忘是人类经历的固有部分。我们都会丢失钥匙,忘记一个熟悉的名字,或者记不起几天前晚餐吃了什么。但这种明显的记忆缺失不一定是缺陷。相反,它突显了一个复杂的认知机制,使我们的大脑能够优先排序、筛选和管理大量信息。遗忘,悖论般地,证明了我们学习和记忆的能力。
就像人们会遗忘一样,机器学习模型也会遗忘——特别是大型语言模型。这些模型通过调整内部参数以响应数据暴露来进行学习。然而,如果新数据与模型之前学到的内容相反,它可能会覆盖或削弱旧信息。即使是相符的数据也可能会干扰并调整本来很好的学习权重。这种现象被称为“灾难性遗忘”,是训练稳定和多才多艺的人工智能系统中的一个重大挑战。
LLM 中遗忘的机制
核心是,LLM 的记忆存在于其权重中。在神经网络中,每个权重本质上构成了网络高维权重空间中的一个维度。随着学习过程的展开,网络在这个空间中导航,依靠选择的梯度下降,旨在最小化损失函数。
这个损失函数通常是 LLM 分类任务中的交叉熵损失形式,它将模型的输出分布与目标分布进行比较。从数学角度看,对于目标分布 y 和模型输出 ŷ,交叉熵损失可以表示为:
在训练过程中,网络调整其权重以最小化这个损失。现在,决定一个权重应该改变多少的核心因素是学习率。在随机梯度下降更新规则中:
η 是学习率。然而,选择这个学习率可能很棘手,并且对灾难性遗忘有影响。如果 η 较高,模型具有很高的适应性,能够快速学习新任务,但可能会丧失先前的知识。较小的 η 能保持旧知识,但可能会影响新任务的学习。
此外,当我们意识到权重更新并不是独立时,复杂性就上升了。调整与某一特征相关的权重可能会无意中影响其他特征的表现,导致复杂的依赖关系网络。
我们还必须考虑训练过程中任务或数据的课程顺序。顺序引入任务可能会导致后续任务的主导地位,使模型偏向最新学习的任务,这是灾难性遗忘的直接表现。
应对灾难性遗忘的策略
我们希望我们的 LLM 记住的信息超出我们自身的记忆。因此,我们努力构建在内存使用上高效的系统,但不一定受限于我们的生物标准。在与 LLM 中的灾难性遗忘作斗争的过程中,研究人员开发了几种创新策略。最突出的三种策略包括弹性权重巩固、渐进神经网络和优化固定扩展层。每种技术都采用了独特的数学方法来缓解遗忘问题。
弹性权重巩固(EWC):记住每个权重的重要性
EWC 受到神经科学和贝叶斯推理的启发,其目标是量化每个权重对模型之前学习的任务的重要性。基本思想是,在遇到新数据时,对先前任务关键的权重应进行较少的调整。

图 2:EWC 示意参数空间, www.pnas.org/doi/full/10.1073/pnas.1611835114
在图 2 中,我们可以清楚地看到弹性权重巩固(Elastic Weight Consolidation)在训练任务 B 时防止灾难性遗忘所起的关键作用,而不会丧失我们从任务 A 中获得的知识。该图显示了参数空间,其中灰色区域表示任务 A 的最佳表现,奶油色区域表示任务 B 的良好表现。在我们学习任务 A 之后,我们的参数值标记为θ*A。
如果我们只集中于任务 B,并沿其梯度方向采取步骤(如蓝色箭头所示),我们将最小化任务 B 的损失,但可能会抹去我们对任务 A 的知识——这就是灾难性遗忘的问题。另一方面,如果我们用相同的系数约束所有权重(如绿色箭头所示),我们会施加一个严厉的限制,使我们保留任务 A 的记忆,但使得学习任务 B 变得困难。
这是 EWC 发挥作用的地方——它通过为任务 B(由红色箭头指示)找到一个不会对任务 A 的知识产生重大影响的解决方案,从而找到最佳平衡点。它通过具体确定每个权重相对于任务 A 的重要性来实现这一点。
EWC 在损失函数中引入了一个二次惩罚,限制重要权重的修改。这个惩罚项与当前权重值和初始权重值之间的差异的平方成正比,由重要性因子进行缩放。这个重要性因子通过费舍尔信息矩阵(Fisher Information Matrix)计算,作为权重对先前学习任务的意义的启发式指标。
在弹性权重巩固中,首先对神经网络进行任务 A 的训练,然后计算并保存费舍尔信息矩阵(Fisher Information Matrix)及学得的权重。在对网络进行任务 B 的训练时,EWC 修改损失函数以包括一个惩罚项,该惩罚项使用保存的 FIM 和权重计算,阻止对任务 A 关键权重的剧烈变化,从而在学习新任务与保留先前任务的知识之间取得平衡。惩罚的二次性质确保了从初始权重的较大偏差会产生更高的惩罚。通过对对先前任务贡献更大的权重赋予更大的惩罚,EWC 旨在在适应新信息的同时保留其学习的知识。
渐进神经网络(ProgNet):构建神经网络塔
ProgNets 引入了一种新架构,使网络在遇到新任务时能够扩展。它不是改变单个网络的权重,而是为每个任务添加一个新的网络(或列),将这些列堆叠起来,类似于建造一座塔。每个新列连接到所有以前添加的列,但反向不连接,从而保留旧列中的知识。
在 ProgNet 背后,每个任务由一个独立的列学习,输出是所有先前和当前列输入的函数。先前列的权重保持不变,防止任何灾难性遗忘,而新列的权重正常训练。

图 3 : 基于块的 ProgNet 模型, arxiv.org/abs/1606.04671
设想渐进神经网络为一组独立的处理单元,每个单元能够识别并利用与其任务相关的最重要输入。以图 3 为例,输出₃不仅与直接连接的隐藏层 h₂ 进行交互,还与之前列的 h₂ 层进行接口,通过其独特的横向参数修改这些层的输出。这个输出₃ 单元扫描并评估可用数据,战略性地忽略不必要的输入。例如,如果 h₂¹ 包含所有需要的信息,输出₃ 可能选择忽略其他信息。另一方面,如果 h₂² 和 h₂³ 包含有价值的信息,输出₃ 可能会优先关注这些,而忽略 h₂¹。这些侧向连接使网络能够有效管理跨任务的信息流,同时排除无关数据。
优化固定扩展层(OFELs):每个任务的新房间
OFELs 的概念就像为每个新家庭成员在房子里建一个新房间。在神经网络的背景下,OFELs 为 LLM 遇到的每个任务添加一个新层。这种层的扩展允许网络容纳新信息,而不会干扰已学到的内容。

图 4 : OFEL 图示, www.mdpi.com/2073-4425/10/7/553
OFELs 涉及到修改网络本身的架构。在这里,对于每个新任务,会在神经网络中添加一个新层,而不是重新训练整个网络。这种架构修改有助于在特定层中封装所需的知识,从而最小化对旧层已有权重的影响。
其中 g 是激活函数。OFELs 的架构设计使其能够加入一个专门用于新任务的新层,这意味着网络可以独立处理新输入(x_new)与旧输入(x_old)。本质上,虽然方程式呈现了架构中底层过程的全面视图,但在推理或预测新任务时,我们通常只会使用x_new而不需要x_old。
通过有选择地优化新层,OFELs 在获取与新任务相关的知识和保留之前学到的信息之间取得了微妙的平衡。这一精细的优化过程使模型能够适应新挑战,同时保持利用先前知识的能力,从而实现更强大和多样化的学习。
前瞻性学习
遗忘——无论是在人的大脑还是 LLMs 中——都是一个迷人的悖论。一方面,它可能成为持续学习和适应能力的障碍。另一方面,它是我们大脑和 AI 模型管理和优先处理信息的固有部分。应对灾难性遗忘的策略——弹性权重巩固、渐进神经网络和优化固定扩展层——提供了富有洞察力但又各不相同的方法,以保留大型语言模型的保留能力。它们各自提供独特的解决方案,反映了人工智能领域必须持续体现的机智和适应性。然而,至关重要的是要理解,灾难性遗忘的问题尚未完全解决;在这一领域仍有待开发的途径,需要深入探索、创新和创造力。
解决灾难性遗忘的挑战不仅推动我们朝着更高效的 AI 系统前进,而且朝着对学习和遗忘的更深入理解——这是人类和机器共同拥有的认知功能。因此,对于研究人员、科学家、从业者以及任何对智能工作原理感兴趣的人来说,参与这一持续对话变得至关重要。驯服灾难性遗忘现象的探索不仅仅是学术追求,而是一段承诺重新定义我们关系理解的旅程。
使用 Pytest 对 PySpark 代码进行单元测试
原文:
towardsdatascience.com/unit-testing-pyspark-code-using-pytest-b5ab2fd54415
·发表于Towards Data Science ·10 分钟阅读·2023 年 1 月 16 日
--

我非常喜欢单元测试。
阅读了两本书—— 《务实程序员》 和 《重构》 ——彻底改变了我对单元测试的看法。
“测试不是为了找出漏洞。
我们相信,测试的主要好处发生在你思考和编写测试的时候,而不是运行它们的时候。”
— 《务实程序员》,大卫·托马斯和安德鲁·亨特
我不再把测试视为在完成数据管道后需要完成的繁琐工作,而是将其视为一个强大的工具,用于改善代码设计,减少耦合,更快速地迭代,并与工作中的其他人建立信任。
然而,为数据应用程序编写良好的测试可能很困难。
与传统的软件应用程序具有相对明确的输入不同,生产环境中的数据应用程序依赖于大量且不断变化的输入数据。
在测试环境中准确地表示这些数据,以足够的细节覆盖所有边界情况,可能是非常具有挑战性的。有些人认为对数据管道进行单元测试没有什么意义,而是更关注数据验证技术。
我坚信在数据管道中同时实现单元测试和数据验证。单元测试不仅仅是为了找出漏洞,更是为了创建设计更好的代码,并与同事和最终用户建立信任。
如果你能养成编写测试的习惯,你将编写出设计更好的代码,节省长远的时间,并减少管道失败或在生产中给出不正确结果的痛苦。
单元测试 PySpark 代码的挑战
一个好的单元测试应具备以下特征:
-
专注。每个测试应测试一个单一的行为/功能。
-
快速。允许你快速迭代并获得反馈。
-
孤立。每个测试应该负责测试特定的功能,并且不依赖于外部因素以便成功运行。
-
简洁。创建测试不应包含大量的样板代码来模拟/创建复杂对象以便测试运行。
当涉及到专门为 PySpark 管道编写单元测试时,编写专注、快速、孤立和简洁的测试可能是一个挑战。
你可能会遇到一些障碍……
首先编写可测试的代码
PySpark 管道往往被编写成一个负责多个转换的巨型函数。
例如:
def main(spark, country):
"""Example function for processing data received from different countries"""
# fetch input data
...
# preprocess based on individual country requirements
if country == 'US':
# preprocessing for US
elif country == 'GB':
# preprocessing for UK
else:
# more preprocessing etc..
# join dataframes together
...
# run some calculations/aggregate
...
# save results
...
从这个角度考虑转换是合理的,并且在很多方面更容易理解和阅读。
但是,当你开始尝试为这个函数编写测试时,你会很快意识到,编写一个覆盖所有功能的测试是非常困难的。
这是因为函数高度耦合,并且函数可能有许多不同的路径。
即使你编写了一个测试来验证输入数据和输出数据是否符合预期。如果测试因任何原因失败,将很难理解长函数的哪个部分出现了问题。
相反,你应该将你的转换拆分为负责单一任务的可重用函数块。然后你可以为每个单独的函数(任务)编写单元测试。当这些单元测试都通过时,你可以更有信心地将所有函数组合在一起,得到最终管道的输出。
编写测试是一种良好的实践,并迫使你考虑设计原则。如果测试你的代码很困难,那么你可能需要重新考虑你的代码设计。
速度
Spark 被优化用于处理非常大的数据,计算也被优化以分布在多台机器上。
这在大型集群中表现得很好,但实际上在你可能用于单元测试的单台机器上会严重影响性能。
当你运行 PySpark 管道时,spark 会评估整个管道并计算一个优化的‘计划’以在分布式集群上执行计算。
计划带来了显著的开销。当你在分布式集群上处理数 TB 的数据时,这是合理的。但在单台机器上处理小数据集时,速度可能会出奇地慢。尤其是与 Pandas 的体验相比。
如果不优化你的 SparkSession 配置参数,你的单元测试将会运行得非常缓慢。
依赖 Spark Session
要在你的单元测试中运行 PySpark 代码,你需要一个 SparkSession。
如上所述,理想情况下,每个测试应与其他测试隔离,并且不需要复杂的外部对象。不幸的是,不能避免为单元测试启动 Spark 会话的要求。
创建 Spark 会话是编写 PySpark 管道单元测试时要克服的第一个障碍。
你应该如何为测试创建 SparkSession?
为每个测试初始化一个新的 Spark 会话会显著增加运行测试的时间,并给测试引入大量样板代码。
高效地创建和共享 SparkSession 对于保持测试性能在可接受的水平至关重要。
数据
你的测试将需要输入数据。
创建大数据管道的示例数据存在两个主要问题。
首先是大小。显然,你不能在生产中使用的完整数据集上运行测试。你必须使用一个更小的子集。
但是,通过使用一个小数据集,你会遇到第二个问题,即提供足够的测试数据来覆盖你想要处理的所有边界情况。
真正很难为测试模拟现实数据。虽然对此没有太多办法,但你可以使用更小、目标明确的数据集进行测试。
使用 Pytest 对 PySpark 代码进行单元测试的步骤
让我们通过一个使用PyTest为 PySpark 管道编写单元测试的示例来进行实践。
💻 完整代码可在此 GitHub 存储库 中获取
示例代码
这是一个处理银行交易的 PySpark 管道示例。在这个场景中,我们希望将原始交易分类为借记账户或信用账户交易,通过将其与一些参考数据连接起来。
每个交易记录都附带一个账户 ID。我们将使用这个账户 ID 连接到包含该账户 ID 是借记账户还是信用账户的信息的账户信息表。
import pyspark.sql.functions as F
from pyspark.sql import DataFrame
def classify_debit_credit_transactions(
transactionsDf: DataFrame, accountDf: DataFrame
) -> DataFrame:
"""Join transactions with account information and classify as debit/credit"""
# normalise strings
transactionsDf = transactionsDf.withColumn(
"transaction_information_cleaned",
F.regexp_replace(F.col("transaction_information"), r"[^A-Z0-9]+", ""),
)
# join on customer account using first 9 characters
transactions_accountsDf = transactionsDf.join(
accountDf,
on=F.substring(F.col("transaction_information_cleaned"), 1, 9)
== F.col("account_number"),
how="inner",
)
# classify transactions as from debit or credit account customers
credit_account_ids = ["100", "101", "102"]
debit_account_ids = ["200", "201", "202"]
transactions_accountsDf = transactions_accountsDf.withColumn(
"business_line",
F.when(F.col("business_line_id").isin(credit_account_ids), F.lit("credit"))
.when(F.col("business_line_id").isin(debit_account_ids), F.lit("debit"))
.otherwise(F.lit("other")),
)
return transactions_accountsDf
这个示例管道存在几个问题:
-
难以阅读。一个地方包含了大量复杂的逻辑。例如,正则表达式替换、按子字符串连接等。
-
难以测试。单个函数负责多个操作
-
难以重用。借记/信用分类是业务逻辑,无法轻松在项目中重用
步骤 1:将代码重构为更小的逻辑单元
让我们首先将代码重构为单独的函数,然后将这些函数组合成主classify_debit_credit_transactions函数。
然后,我们可以为每个单独的函数编写测试,以确保其按预期行为。
虽然这增加了代码的总体行数,但测试起来更容易,我们现在可以在项目的其他部分重用这些函数。
import pyspark.sql.functions as F
from pyspark.sql import DataFrame
def classify_debit_credit_transactions(
transactionsDf: DataFrame, accountsDf: DataFrame
) -> DataFrame:
"""Join transactions with account information and classify as debit/credit"""
transactionsDf = normalise_transaction_information(transactionsDf)
transactions_accountsDf = join_transactionsDf_to_accountsDf(
transactionsDf, accountsDf
)
transactions_accountsDf = apply_debit_credit_business_classification(
transactions_accountsDf
)
return transactions_accountsDf
def normalise_transaction_information(transactionsDf: DataFrame) -> DataFrame:
"""Remove special characters from transaction information"""
return transactionsDf.withColumn(
"transaction_information_cleaned",
F.regexp_replace(F.col("transaction_information"), r"[^A-Z0-9]+", ""),
)
def join_transactionsDf_to_accountsDf(
transactionsDf: DataFrame, accountsDf: DataFrame
) -> DataFrame:
"""Join transactions to accounts information"""
return transactionsDf.join(
accountsDf,
on=F.substring(F.col("transaction_information_cleaned"), 1, 9)
== F.col("account_number"),
how="inner",
)
def apply_debit_credit_business_classification(
transactions_accountsDf: DataFrame,
) -> DataFrame:
"""Classify transactions as coming from debit or credit account customers"""
CREDIT_ACCOUNT_IDS = ["101", "102", "103"]
DEBIT_ACCOUNT_IDS = ["202", "202", "203"]
return transactions_accountsDf.withColumn(
"business_line",
F.when(F.col("business_line_id").isin(CREDIT_ACCOUNT_IDS), F.lit("credit"))
.when(F.col("business_line_id").isin(DEBIT_ACCOUNT_IDS), F.lit("debit"))
.otherwise(F.lit("other")),
)
3. 使用 Fixtures 创建可重用的 SparkSession
在编写单元测试之前,我们需要创建一个可以在所有测试中重用的 SparkSession。
为此,我们在 conftest.py 文件中创建一个 PyTest fixture。
Pytest fixtures 是一次创建然后在多个测试中重复使用的对象。这对于像 SparkSession 这样的复杂对象特别有用,因为创建它们的开销很大。
# conftest.py
from pyspark.sql import SparkSession
import pytest
@pytest.fixture(scope="session")
def spark():
spark = (
SparkSession.builder.master("local[1]")
.appName("local-tests")
.config("spark.executor.cores", "1")
.config("spark.executor.instances", "1")
.config("spark.sql.shuffle.partitions", "1")
.config("spark.driver.bindAddress", "127.0.0.1")
.getOrCreate()
)
yield spark
spark.stop()
设置一些配置参数以优化 SparkSession,用于在单台机器上处理小数据进行测试是很重要的:
-
master = local[1]– 指定 spark 在一台本地机器上运行,并且使用一个线程 -
spark.executor.cores = 1– 设置核心数为一个 -
spark.executor.instances = 1- 设置执行器为一个 -
spark.sql.shuffle.partitions = 1- 设置最大分区数为 1 -
spark.driver.bindAddress = 127.0.0.1– (可选)显式指定驱动程序绑定地址。如果你的机器也与远程集群有活动连接,这将很有用。
这些配置参数本质上告诉 spark 你在单台机器上处理数据,并且 spark 不应该尝试分发计算。这将大大节省管道执行和计算本身的时间。
注意,建议使用 yield 而不是 return 来返回 spark session。有关更多信息,请阅读 PyTest 文档。使用 yield 还允许你在测试运行后执行任何清理操作(例如,删除本地临时目录、数据库或表等)。
4. 为代码创建单元测试
现在让我们为我们的代码编写一些测试。
我发现最有效的方式是用以下结构组织我的 PySpark 单元测试:
-
创建输入 DataFrame
-
使用我们要测试的函数创建输出 DataFrame
-
指定预期的输出值
-
比较结果
我还尽量确保测试覆盖正面测试用例和至少一个负面测试用例。
from src.data_processing import (
apply_debit_credit_business_classification,
classify_debit_credit_transactions,
join_transactionsDf_to_accountsDf,
normalise_transaction_information,
)
def test_classify_debit_credit_transactions(spark):
# create input test dataframes
transactionsDf = spark.createDataFrame(
data=[
("1", 1000.00, "123-456-789"),
("3", 3000.00, "222222222EUR"),
],
schema=["transaction_id", "amount", "transaction_information"],
)
accountsDf = spark.createDataFrame(
data=[
("123456789", "101"),
("222222222", "202"),
("000000000", "302"),
],
schema=["account_number", "business_line_id"],
)
# output dataframe after applying function
output = classify_debit_credit_transactions(transactionsDf, accountsDf)
# expected outputs in the target column
expected_classifications = ["credit", "debit"]
# assert results are as expected
assert output.count() == 2
assert [row.business_line for row in output.collect()] == expected_classifications
def test_normalise_transaction_information(spark):
data = ["123-456-789", "123456789", "123456789EUR", "TEXT*?WITH.*CHARACTERS"]
test_df = spark.createDataFrame(data, "string").toDF("transaction_information")
expected = ["123456789", "123456789", "123456789EUR", "TEXTWITHCHARACTERS"]
output = normalise_transaction_information(test_df)
assert [row.transaction_information_cleaned for row in output.collect()] == expected
def test_join_transactionsDf_to_accountsDf(spark):
data = ["123456789", "222222222EUR"]
transactionsDf = spark.createDataFrame(data, "string").toDF(
"transaction_information_cleaned"
)
data = [
"123456789", # match
"222222222", # match
"000000000", # no-match
]
accountsDf = spark.createDataFrame(data, "string").toDF("account_number")
output = join_transactionsDf_to_accountsDf(transactionsDf, accountsDf)
assert output.count() == 2
def test_apply_debit_credit_business_classification(spark):
data = [
"101", # credit
"202", # debit
"000", # other
]
df = spark.createDataFrame(data, "string").toDF("business_line_id")
output = apply_debit_credit_business_classification(df)
expected = ["credit", "debit", "other"]
assert [row.business_line for row in output.collect()] == expected
我们现在为 PySpark 管道中的每个组件都有了单元测试。
由于每个测试都重用相同的 SparkSession,运行多个测试的开销大大减少。
PySpark 代码单元测试的进一步提示
创建包含最少所需信息的测试 DataFrame
在创建测试数据的 DataFrame 时,只创建与转换相关的列。
你只需要创建函数所需的列的数据。你不需要所有可能存在于生产数据中的其他列。
这有助于编写简洁的函数,并且更具可读性,因为可以清楚地知道哪些列是函数所需的以及受函数影响的。如果你发现需要一个包含许多列的大 DataFrame 来执行转换,你可能一次尝试做得太多了。
这只是一个指导方针,你自己的用例可能需要更复杂的测试数据,但如果可能,保持数据小巧、简洁,并局限于测试范围。
记得调用一个“操作”以触发 PySpark 计算
PySpark 使用惰性计算。你需要在测试期间调用一个‘action’(例如 collect、count 等),以计算一个可以与预期输出进行比较的结果。
如果不需要的话,不要运行所有的 PySpark 测试
PySpark 测试通常比正常单元测试运行时间更长,因为有计算计划和执行的开销。
在开发过程中,利用 Pytest 的一些功能,例如 -k 标志来运行单个测试或仅运行单个文件中的测试。然后,仅在提交代码之前运行完整的测试套件。
保持单元测试的隔离性
注意不要在测试期间修改你的 spark 会话(例如,创建一个表,但之后不删除它)。
表格将在所有测试中持续存在,这可能会干扰预期行为。
尽量将数据的创建保持在使用它的地方附近。
你可以使用 Pytest fixtures 在多个测试之间共享数据框,甚至从 CSV 文件等中加载测试数据。
然而,根据我的经验,为每个单独的测试创建所需的数据更容易且更具可读性。
测试正面和负面结果
例如,在测试连接条件时,你应该包括一些不满足连接条件的数据。这有助于确保你既排除了错误的数据,又包含了正确的数据。
本博客最初发布在 engineeringfordatascience.com
如果你使用 Pytest 进行单元测试,可以查看我另一篇包含 Pytest 使用技巧的文章:
13 Tips for using PyTest [## 13 Tips for using PyTest
单元测试是软件开发中非常重要的技能。有一些很棒的 Python 库可以帮助我们……
单变量离散分布:易于理解的解释
数学和视觉上理解单变量离散分布
·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 10 月 20 日
--

图片来源 unDraw
你是否有这样的感觉?你想学习新的东西,但不知道从哪里开始。这就是我们在想要数学理解分布时的感受。是的,我们的教授向我们解释了所有这些分布,但只是用公式的数学方式!我们的教授没有用易于理解的视觉方式来解释。
这就是为什么我们写这篇文章来讲解最重要的单变量离散分布。我们想以数学和视觉的方式向你解释这些分布。我们的目标是让你理解数学和分布图之间的关系。此外,我们还为每个分布提供了一个例子。
作为数据科学家,了解分布如何工作是非常重要的。分布假设是一些机器学习算法的基础,并且对于解决统计问题至关重要,例如在保险行业中。
我们将讨论以下分布:
-
伯努利分布
-
二项分布
-
几何分布
-
泊松分布
-
均匀分布
让我们深入了解吧!
伯努利分布是什么?
如果一个随机变量 X 只有两个可能的结果(标记为 0 或 1),则存在一个参数为 p 的伯努利分布。X=1(成功)发生的概率是 p,而 X=0(失败)发生的概率是 1-p。
让我们来看一个例子。
示例:
X 代表一次掷硬币的结果,其中 X=1(正面)和 X=0(反面)。p 是硬币落在正面的概率。
接下来,你将看到伯努利分布的视觉和数学解释。首先,我们从数学开始!
数学描述:
伯努利分布具有以下要求:

随机变量 X 是伯努利分布的。概率 p 的值范围在 0 和 1 之间。
概率质量函数(PMF)是

公式显示,当 k=0 时,概率为 1-p。另一方面,当 k=1 时,概率为 p。
该分布的均值和方差是

现在,我们理解了数学原理。接下来,我们看一下视觉解释。
视觉解释:

伯努利分布的视觉解释(图像来源于作者)
在左侧的视觉图中,你可以看到随机变量 X 有两个可能的结果(k = 1 或 k = 0)。在右侧,你看到 k = 1(概率 p = 0.7)和 k=0(概率 p=1–0.7=0.3)的示例。
以下代码显示了如何生成右侧的图表:
import plotly.graph_objects as go
fig = go.Figure(data=[go.Bar(
x=[0, 1],
y=[0.3, 0.7],
width=[0.8, 0.8],
marker_color=['#165AA7', '#165AA7']
)],
layout = {
'xaxis': {'title': 'k', 'tick0': '0', 'dtick': '1'},
'yaxis': {'title': 'Probability', 'range': [0, 1]},
'title': 'p=0.7',
'barmode': 'relative',
}
)
fig.show()
伯努利分布 (n=1) 是二项分布的特例。所以现在让我们看看二项分布吧!
什么是二项分布?
如果一个随机变量 X 等于 n 个具有参数 p 的伯努利分布随机变量的总和,则存在一个具有参数 n 和 p 的二项分布:

X 代表在参数 p 的 n 次伯努利试验中的成功总数(1 的数量)。

具有二项分布的随机变量是 n 个伯努利随机变量的总和!
接下来,我们看一下这种分布的一个示例。
示例:
例如,X 代表 n 次抛硬币中的正面总数。得到正面的概率是 p。
下面,你可以看到二项分布的数学解释。
数学描述:
我们有以下要求:

随机变量 X 是二项分布的。概率 p 的值范围在 0 和 1 之间。n 的值是 1, 2, … 。
概率质量函数(PMF)是

该分布的均值和方差是

现在,我们知道了所有的公式。接下来,我们看看二项分布的视觉解释。
视觉解释:

二项分布的视觉解释(图像来源于作者)
在这个示例中,我们选择 n=6 和 p=0.5。我们可以用 PMF 计算概率(y 轴)的值。计算所有值后,我们得到右侧的图表。k=3(1 的数量)的概率是 31.25%。最高概率是 X=1 发生三次。
假设我们掷一枚硬币六次。那么,硬币正面出现三次的概率是 31.25%。
你可以在下面查看右侧图形的代码:
import plotly.graph_objects as go
fig = go.Figure(data=[go.Bar(
x=[0, 1, 2, 3, 4, 5, 6],
y=[0.015625, 0.09375, 0.234375, 0.3125, 0.234375, 0.09375, 0.015625],
width=[0.8, 0.8],
marker_color=['#165AA7', '#165AA7', '#165AA7', '#165AA7', '#165AA7', '#165AA7', '#165AA7']
)],
layout = {
'xaxis': {'title': 'k', 'tick0': '0', 'dtick': '1'},
'yaxis': {'title': 'Probability', 'range': [0, 0.35]},
'title': 'n=6, p=0.5',
'barmode': 'relative',
}
)
fig.show()
我们使用二项分布的 PMF 手动计算了 y 值。试着在代码中重现这些值。
什么是几何分布?
我们有一个具有参数p的伯努利试验序列。一个具有参数p的几何分布随机变量X表示在第一次成功之前发生的失败试验(0 的数量)。
示例:
你可以使用几何分布来确定设备和组件的寿命,即等待第一次故障或等待第一次成功。
下面,你可以看到几何分布的数学解释。
数学描述:
我们有以下要求:

随机变量X是几何分布的。概率p的值范围在 0 到 1 之间。
概率质量函数(PMF)为

该分布的均值和方差为

现在,我们知道了这个分布背后的数学原理。让我们更直观地看一下它。
视觉解释:

几何分布的视觉解释(图片由作者提供)
X表示失败的数量(0 的数量)。我们假设有一个电子组件,其故障概率为 50%(p=0.5)。对于第一个组件,错误概率为 50%。之后,概率会降低。
在下面,你可以看到右侧图形的代码:
import plotly.graph_objects as go
fig = go.Figure(data=[go.Bar(
x=[0, 1, 2, 3, 4, 5, 6, 7],
y=[0.5, 0.25, 0.125, 0.0625, 0.03125, 0.015625, 1/128, 1/256],
width=[0.8, 0.8],
marker_color=['#165AA7', '#165AA7', '#165AA7', '#165AA7', '#165AA7', '#165AA7', '#165AA7', '#165AA7']
)],
layout = {
'xaxis': {'title': 'k (number of 0s)', 'tick0': '0', 'dtick': '1'},
'yaxis': {'title': 'Probability', 'range': [0, 0.55]},
'title': 'p=0.5',
'barmode': 'relative',
}
)
fig.show()
我们使用 PMF 来计算图形的 y 值。试着自己动手做做看!
什么是泊松分布?
泊松分布是二项分布在试验次数n趋于无穷大且p趋于零时的极限情况。乘积np=λ保持不变。此外,参数λ是分布的均值和方差。λ表示成功事件的平均数量。
泊松分布对二项分布的近似规则:
n >= 50 和 p <= 0.05
泊松分布也被称为稀有事件分布。
示例:
例如,一家汽车保险公司需要计算一年内发生 1、2 或 3 起车祸的概率。
在下面,你可以看到泊松分布的数学解释。
数学描述:
泊松分布有以下要求:

随机变量X是具有参数λ的泊松分布。参数λ的值范围大于 0。
概率质量函数(PMF)为

该分布的均值和方差为

现在,我们知道了泊松分布的公式。是时候从视觉上看看它了。
视觉解释:

泊松分布的视觉解释(图片由作者提供)
我们假设一个保险公司想要知道一年内发生十次车祸的概率。此外,我们知道平均事故率为五。因此,我们可以查看红色图表,看到 k = 10 的概率非常小。这样,你就能得到所有的概率。
接下来,你可以看到右侧图表的代码。
import math
import plotly.express as px
df = pd.DataFrame(columns = ['Parameter', 'k', 'Probability'])
for _lambda in [1, 5, 10]:
for k in range(24):
poisson_formula = (pow(_lambda,k) * math.exp(-_lambda))/math.factorial(k)
df = df.append({'Parameter' : "λ=" + str(_lambda), 'k' : k, 'Probability' : poisson_formula}, ignore_index = True)
fig = px.line(df, x='k', y='Probability', color='Parameter', markers=True)
fig.show()
我们绘制了 λ=1、λ=5 和 λ=10 的图表。此外,我们使用上述 PMF 计算概率。最后,我们绘制了一个折线图,显示 k 值和概率。
什么是均匀分布?
随机变量 X 具有离散均匀分布(参数 a 和 b),可以以相等的概率取 a 到 b 之间的任何整数。在均匀分布中,所有结果的发生概率相同。
示例:
例如,掷一次骰子。随机变量 X 的可能值为:

每个数字的概率是相同的。
接下来,我们将介绍此分布背后的数学。
数学描述:
我们有以下要求:

概率质量函数(PMF)是

此分布的均值和方差为

现在,我们知道如何从数学上定义分布。让我们看看视觉解释。
视觉解释:

均匀分布的视觉解释(图片由作者提供)
在这个例子中,我们设置 a=1 和 b=6。我们可以看到所有 k 的值具有相同的概率。
接下来,你可以看到上面图表的代码。
import plotly.graph_objects as go
fig = go.Figure(data=[go.Bar(
x=[1, 2, 3, 4, 5, 6],
y=[1/6, 1/6, 1/6, 1/6, 1/6, 1/6],
width=[0.8, 0.8],
marker_color=['#165AA7', '#165AA7', '#165AA7', '#165AA7', '#165AA7', '#165AA7']
)],
layout = {
'xaxis': {'title': 'k', 'tick0': '0', 'dtick': '1'},
'yaxis': {'title': 'Probability', 'range': [0, 0.25]},
'title': 'a=1, b=6',
'barmode': 'relative',
}
)
fig.show()
我们使用均匀分布的概率质量函数(PMF)并绘制 x 轴和 y 轴的值。
结论
恭喜你,了解了最重要的单变量离散分布的工作原理。在每个解释中,我们首先给出了直观的示例。然后,我们介绍了分布背后的数学,并将这些数学内容放入程序代码中。程序代码为每个分布生成了一个图表。之后,我们基于图表和图形解释了分布的视觉效果。
希望你对单变量离散分布的理解比以前更好。感谢你的关注。
👉🏽 加入我们免费的每周 Magic AI 新闻通讯,获取最新的 AI 更新!
免费订阅 以在我们发布新故事时收到通知:
[## 每当 Janik 和 Patrick Tinz 发布内容时,您将收到一封电子邮件。
每当 Janik 和 Patrick Tinz 发布内容时,您将收到一封电子邮件。通过注册,如果您还没有创建 Medium 帐户,您将创建一个...
在我们的 关于页面 了解更多关于我们的信息。不要忘记关注我们在 X 上。非常感谢您的阅读。如果您喜欢这篇文章,随时分享。祝您度过愉快的一天!
使用 我们的链接 注册 Medium 会员,以阅读无限制的 Medium 故事。
释放隐藏模式:无监督机器学习文章推荐系统指南
构建一个基于无监督机器学习的文章推荐系统,并生成有助于推荐的特征和模式
·
关注 发表在 Towards Data Science ·8 分钟阅读·2023 年 3 月 7 日
[]
照片由 Salomé Watel 提供,图片来源于 Unsplash
最近有很多关于人工智能和机器学习令人难以置信的能力的讨论。随着我们看到机器学习应用的各种前沿领域,这一转变所产生的更高价值的可能性也在不断增长。像谷歌、微软和英伟达这样的公司正在推动人工智能和机器学习的边界,以利用技术进步建设一个伟大的社会。
现在,人工智能的炒作非常多,一个常被忽视的话题是无监督机器学习。我敢打赌,人们可能已经有机会享受一些最先进的流媒体服务,如Netflix和Amazon Videos,它们拥有最先进的推荐系统。然而,对于作为无监督机器学习一部分的推荐系统的惊人能力却鲜有讨论。
在本文中,我们将主要关注基于用户之前阅读的文章构建文章推荐系统,为他们提供积极的体验,并促使他们阅读更多类似类型的文章。这属于无监督机器学习,因为我们没有关于特定文章是否与其他文章相关的标签。相反,我们仅提供文本信息,我们必须在向用户推荐项目之前突出和可视化相似性。
最初,我们从数据开始,并在继续推荐文章的努力之前了解它们的类型。在浏览数据后,我们将可视化数据中的关键见解。最后,我们根据文章与所有文章列表之间的余弦相似度分数构建推荐系统以获取推荐。
阅读数据
第一步是读取数据。以下是读取数据的代码。读取数据后,我们将进行探索性数据分析。这里是数据的代码和表示。
注意: 数据集来自 CI&T DeskDrop 文章分享与阅读 | Kaggle,根据 数据库内容许可证 (DbCL) v1.0 — 开放数据公约:开放数据的法律工具 许可证

输入数据(作者提供的图片)
在我们的数据中,标题、文本和语言特征是最重要的。其他特征,如内容类型和作者用户代理,对于构建推荐系统的任务并不十分适用。我们将采取步骤去除这些特征,仅考虑标题、文本和语言等特征。请注意,图中未显示所有特征,因为这将成为一张大图。

数据信息(作者提供的图片)
我们获取数据的信息,并确定数据集中存在的非空值的数量。我们发现有些特征如 authorSessionID 和 authorUserAgent 包含超过50%的缺失值。我们将删除这些特征以及上面讨论的其他特征,专注于标题、文本和语言特征。
探索性数据分析(EDA)
现在让我们通过使用视觉效果和图表来探索数据集并理解它。第一步是探索数据中存在的文本内容类型。

内容类型的计数图(作者提供的图片)
从图中观察,大部分内容以HTML 链接的形式存在,用于推荐。这些 HTML 链接包含实际的文本数据和标题。我们在这里的重点是从文本中提取有意义的见解,而不是使用其他格式,如‘视频’和‘富媒体’格式。因此,我们可以从项目中删除这些类别。

语言的计数图(作者提供的图片)
我们的大部分文本和标题都是英文标题。由于我们只关注英语语言,我们可以采取措施删除其他类别,以提高我们的推荐模型效率。

标题词云(作者提供的图片)
词云提供了对我们文本语料库中最常出现的词汇的良好理解。词汇在语料库中的出现频率越高,词云图中的文本大小就越大。从图中可以看出,包含‘Google’、‘机器学习’和‘Apple’等词汇的标题在我们的标题空间中经常出现。

文本词云(作者提供的图片)
现在我们已经探索了标题并找出了最常见的词汇,是时候在我们的文章列表中探索文本本身,寻找有趣的趋势和模式了。大量文章包含‘数据’、‘用户’和‘时间’等词汇。

方差解释百分比(作者提供的图片)
将给定的一组标题转换为TFIDF格式并应用主成分分析,上述图表展示了每个组件解释的方差累计百分比。值得注意的是,随着组件数量的增加,解释的方差也在增加,因为更多的组件信息导致了更多的解释和信息。

方差解释百分比(作者提供的图片)
此图显示了 tfidf 组件对整个文本解释的方差,而不仅仅是考虑标题。这个图与之前的图大相径庭,因为只有少数主成分集能够解释数据方差的大部分。因此,这可以帮助降维,因为更少的特征能够解释大部分方差。

K-means 聚类图(图片来源:作者)
聚类是一种将相似的数据点分组在一起的技术,以寻找有趣的模式和共同点。通过这种方式,基于数据点所在的簇,可以给出推荐。
为了确定正确的簇数,使用k-means 聚类模型并遵循肘部法则来找到最佳簇数是很方便的。在我们的案例中,最佳值k 为 11,因为它符合肘部的形状。

PCA 与聚类 2D 图(图片来源:作者)
在执行 PCA 和基于最佳簇数的聚类后,到了可视化聚类结果的时刻。根据上述图,聚类效果相当不错,因为在簇中发现了模式。

PCA 与聚类 3D 图(图片来源:作者)
让我们确定3D 图的外观,以发现潜在的模式。可以看到,有很多空间可以分隔簇,因此可以指导我们的推荐系统根据之前阅读的文本给出好的建议。
还有其他降维技术,如TSNE和Kernal PCA。逐一了解它们以确定最佳聚类对于推荐系统将很有帮助。必须采取步骤可视化文本数据点并生成有趣的模式。

TSNE 与聚类 2D 图(图片来源:作者)
使用 TSNE 降维并以 11 个簇可视化表示,可以看到数据点分布得很好。因此,相似的文章在同一簇中的机会较小。结果,PCA 在聚类和确定最佳簇数方面表现良好。我们还将使用 3D 可视化来指导我们的思考和理解聚类机制。

TSNE 与聚类 3D 图(图片来源:作者)
在执行聚类任务后,可以看到许多文本的数据嵌入非常接近。因此,当数据点没有在各个方向上分布时,准确执行聚类可能具有挑战性。因此,我们可能会寻找替代的聚类方法和降维技术。

核 PCA 与聚类 2D 图(作者提供的图片)
核 PCA 是另一种用于维度缩减的流行方法。如所见,数据表示的分布良好,有助于确定和使用聚类的数量。总体而言,算法很好地减少了维度并分离了数据点。让我们也来看一下使用这种技术进行维度缩减后的3D 表示的聚类结果。

核 PCA 与聚类 3D 图(作者提供的图片)
在用核 PCA 绘制了 3D 生成的表示后,聚类效果相当好,点分布也相当均匀。因此,这种方法可以帮助基于聚类方法生成推荐。
比如,用户访问一个特定的网站并阅读了一段有趣的文本。在这一步之后,基于核主成分分析(PCA)表示的簇中的项目将推荐给用户。用户可能会觉得这些文章很吸引人,从而促进业务增长。
在执行了之前的步骤后,我们定义了一个函数,生成一组有用的特征,这些特征可以被各种推荐系统模型用于进行推荐。以下函数中使用的特征是重要的,能够很好地表示文本的类型及其内容和可读性。
我们将这个函数应用于数据框,并生成一组新的特征,这些特征被推荐系统所使用。最后,余弦相似度在确定感兴趣的文本与所有其他可能的文本和文章列表之间的距离时被考虑在内。
在根据生成的特征和核 PCA 表示获取当前文本的余弦差异后,将其与现有文章列表进行比较,以确定距离簇最小的文章。结果是,这些文章被推荐给用户,使他们的阅读体验变得引人入胜和有趣。
我们导入了各种用于测量余弦相似度和生成结构和内容相似的文章推荐的有用库。
结论
在阅读了这篇详尽的文章之后,你应该对推荐系统在文章上的具体实现细节有了较好的了解。进行维度缩减可以确保我们减少计算资源的消耗,同时在进行预测或提供推荐时减少异常值的影响。感谢你花时间阅读这篇文章。
以下是你可以联系我或查看我工作的方式。
GitHub:suhasmaddali (Suhas Maddali ) (github.com)
YouTube:https://www.youtube.com/channel/UCymdyoyJBC_i7QVfbrIs-4Q
LinkedIn:(1) Suhas Maddali,东北大学,数据科学 | LinkedIn
Medium: Suhas Maddali — Medium
Kaggle:Suhas Maddali | 贡献者 | Kaggle
释放 GPT-3 的力量:超级英雄描述的微调
GPT-3 微调的逐步指南
·
关注 发表在 Towards Data Science ·11 min 阅读·2023 年 2 月 18 日
--
自 2022 年底以来,OpenAI 推出的 ChatGPT 被许多人认为是人工智能的 iPhone 时刻。然而,OpenAI 的聊天机器人并不是第一个生成式 AI 文本机器学习模型,它跟随的是两年前推出的 GPT-3。
OpenAI 为我们提供了一个现成的 GPT-3 训练模型。此外,特定任务可以在较小的数据集上进行微调。例如,假设你想创建一个针对你公司特定的电子邮件回复生成器。首先,你必须收集大量有关你特定业务领域的数据,如客户电子邮件咨询和回复。然后,你可以使用这些数据来微调 GPT-3,以学习你公司特定的语言模式和短语。通过微调 GPT-3,可以创建一个高度定制和专业化的电子邮件回复生成器,专门针对特定业务领域使用的语言模式和词汇。
在这篇博客文章中,我将向你展示如何微调 GPT-3。我们将使用 python 代码进行操作,并且不假设对 GPT-3 有任何先验知识。
微调需要什么?
与当前在 Hugging Face 上提供的 GPT-2 模型不同(撰写本文时),我们没有直接访问 GPT-3 模型的权限。因此,你首先需要从 OpenAI 获取一个 API 密钥,并安装 Python 包 openai,可以通过 pip 快速完成。
获取 OpenAI 的 API 密钥:
-
创建一个账户,
-
点击‘创建新密钥’,然后
-
复制密钥。
关键是以‘sk-’开头的一长串字符。确保你保密!一旦你拥有了你的密钥,一个简单的获取密钥的方法是在终端中执行以下操作:(个人而言,为了简便,我将其放在我的.bachrc中):
export OPENAI_API_KEY=sk-t59pgejhtrff5(...)
使用 GPT-3 模型是有成本的。我们需要积分。撰写本文时,当你创建一个新账户时,你会获得免费的积分来尝试这个工具。我不知道这种情况是否会持续…
现在我们有了密钥和 Python 包,是时候考虑我们需要微调的数据了。首先,我们需要一个用于微调的示例文件,其中每个示例都是一个 prompt,后跟相应的 completion。
一个超级英雄描述生成工具

来自 DALL-E 2 的超级英雄
我们将为这个演示构建一个工具,以创建虚构超级英雄的描述。最终,工具将接收超级英雄的年龄、性别和能力,并自动生成超级英雄的描述。
在以下示例中,经过模型微调后,我们只需输入'40, woman, Healing ->',模型将自动生成一个描述。

这就是一切的关键!! 😃
创建一个合成的数据集用于微调
在某些情况下,你可能有一个数据集想要用于微调。但由于我没有现成的数据集,我们来看看如何直接从 GPT-3 创建一个合成数据集,用于超级英雄的描述。以下代码将给我一个包含 prompts 和相应 completions 的 CSV 文件。
import os
import openai
import pandas as pd
openai.api_key = os.getenv("OPENAI_API_KEY")
l_age = ['18', '20', '30', '40', '50', '60', '90']
l_gender = ['man', 'woman']
l_power = ['invisibility', 'read in the thoughts', 'turning lead into gold', 'immortality', 'telepathy', 'teleport', 'flight']
f_prompt = "Imagine a complete and detailed description of a {age}-year-old {gender} fictional character who has the superpower of {power}. Write out the entire description in a maximum of 100 words in great detail:"
f_sub_prompt = "{age}, {gender}, {power}"
df = pd.DataFrame()
for age in l_age:
for gender in l_gender:
for power in l_power:
for i in range(3): ## 3 times each
prompt = f_prompt.format(age=age, gender=gender, power=power)
sub_prompt = f_sub_prompt.format(age=age, gender=gender, power=power)
print(sub_prompt)
response = openai.Completion.create(
model="text-davinci-003",
prompt=prompt,
temperature=1,
max_tokens=500,
top_p=1,
frequency_penalty=0,
presence_penalty=0
)
finish_reason = response['choices'][0]['finish_reason']
response_txt = response['choices'][0]['text']
new_row = {
'age':age,
'gender':gender,
'power':power,
'prompt':prompt,
'sub_prompt':sub_prompt,
'response_txt':response_txt,
'finish_reason':finish_reason}
new_row = pd.DataFrame([new_row])
df = pd.concat([df, new_row], axis=0, ignore_index=True)
df.to_csv("out_openai_completion.csv")
让我们深入了解这段代码是如何工作的 🧐。
变量f_prompt包含以下句子,其中{age}、{gender}和{power}是缺失的。
想象一下一个详细描述的{age}岁{gender}虚构角色,他拥有{power}的超级能力。用最多 100 个字写出整个详细描述:
在代码的前三个for循环中,我们遍历不同的{age}、{gender}和{power}值。在每一步循环中,我们用不同的值替换 3 个缺失的变量。然后,我们使用openai.Completion.create函数请求 GPT 生成对我们提示的响应。
这个函数的最重要参数是
-
model:用于生成响应的模型。OpenAI 提供了四种标准 GPT-3 模型(
ada、babbage、curie或davinci),它们在规模和使用价格上有所不同。这里使用的是davinci——最大的模型。 -
prompt:我们希望 GPT-3 完成的提示。
-
temperature:温度是一个介于 0 和 1 之间的数字,控制输出的随机性。我们将温度设置为最大值,以便模型在生成响应时尽可能具有创造力。
-
max_tokens:定义响应的最大长度。
在这个脚本的最后,我们有一个存储在文件out_openai_completion.csv中的 Pandas 表格。这个表格中我们感兴趣的两个主要列是sub_prompt和response_txt。
-
sub_prompt例如会是‘18, man, invisibility’。它包含了三个用逗号分隔的被替换的值。
-
response_txt包含 GPT 模型的输出。
微调 GPT 模型
以下代码检索之前创建的文件out_openai_completion.csv,并使用openai对 GPT-3 模型进行微调。
import pandas as pd
import openai
import subprocess
df = pd.read_csv("out_openai_completion.csv")
prepared_data = df.loc[:,['sub_prompt','response_txt']]
prepared_data.rename(columns={'sub_prompt':'prompt', 'response_txt':'completion'}, inplace=True)
prepared_data.to_csv('prepared_data.csv',index=False)
## prepared_data.csv --> prepared_data_prepared.json
subprocess.run('openai tools fine_tunes.prepare_data --file prepared_data.csv --quiet'.split())
## Start fine-tuning
subprocess.run('openai api fine_tunes.create --training_file prepared_data_prepared.jsonl --model davinci --suffix "SuperHero"'.split())
让我们花时间来理解这段代码 🤓!
首先,文件out_openai_completion.csv的内容被加载到数据框df中。为了提醒一下,在我们的任务中,如果用户输入'40, female, healing',我们希望得到一个关于 40 岁女性角色拥有治愈能力的描述。为了进行微调,需要向 GPT 提供用户可能输入的示例以及相应的期望响应。在数据框df中,sub_prompt和response_txt列包含了输入示例和对应的期望响应。在上面的代码中,我们首先提取这两列,然后将其分别重命名为prompt和completion。结果数据框被存储在一个新的文件prepared_data.csv中,仅包含这两列。
这个文件prepared_data.csv的内容如下:
prompt,completion
"18, man, invisibility","He looks to be in his late teens, with dark eyes and unruly black hair which reach his shoulders. His build is athletic and strong, but not muscular. He often wears clothing that makes him blend in with his surroundings, in order to stay as inconspicuous as possible.He has the ability of invisibility, which he can use to become an invisible observer or to make himself or objects around him disappear from view. He has honed his skills to make himself undetectable, able to even make sounds vanish, allowing him to go unnoticed. His invisibility power is his greatest asset, and he uses it to protect those he cares about."
"18, man, invisibility","18 year-old Jackson is a powerful young man with the incredible power of invisibility. He stands at 6'2 and has a strong athletic frame with wavy brown hair, startling blue eyes and a warm and gentle smile. His strength and agility make him especially good at using his ability. He can become invisible in an instant, moving through walls and other obstacles without being detected. What's more, he can move objects with his mind and use his power to protect those he loves. His power is a blessing and a curse, as it can be abused to do harm. Jackson is a brave and noble person who uses his power to do good and make the world a better place."
"18, man, invisibility","Brandon is an 18-year-old of average build, standing roughly 5'10 with an inquisitive look. He has naturally wavy chestnut brown hair and bright blue eyes. His demeanor is usually quite serious, but he also has an easy and gentle smile. He has a natural gift of invisibility, which he uses to protect himself and his family from harm. He's an inquisitive and creative young man who uses his gift to explore the world, observe people, and uncover the truth. His consistent practice of mindfulness helps him remain unseen, even when his emotions are visible. His intelligence and persistent passion for truth drives him to help those in need."
"18, man, read in the thoughts","This 18-year-old man has a muscular stature and long, ash blonde hair. His bright blue eyes are full of knowledge and power, hinting at the strange ability he has - he can read minds. Now and then, strange sparks of electricity streak from his eyes when he concentrates hard enough. He is often calm and collected, but when provoked has the ability to blend his learning of your thoughts with his immense strength. His wit, intelligence and empathy are weapons that are often underestimated - yet, very lethal."
"18, man, read in the thoughts","He's an 18-year-old man with short, cropped dark hair and bright green eyes. His usual outfit consists of an olive green hoodie, blue jeans and purple trainers. He's relaxed, confident and always reads the atmosphere well. One of his great skills is read in the thoughts of others. By doing so, he is able to understand people more easily and relate to their concerns out of the blue. He always uses his superpower with the utmost dignity and respect, never using it to manipulate. He uses it to seek the truth, identify people's emotions and gain insight into their perspectives. He's the sort of person that can make anyone feel comfortable and is never afraid of a challenge. His superpower is merely an extension of his already charismatic disposition."
(...)
Python 的subprocess.run()函数将命令作为子进程运行。它通常用于执行外部程序,就像在终端中运行一样。
我们使用 subprocess.run() 执行‘openai tools fine_tunes.prepare_data’。这个函数接受输入文件 prepared_data.csv,检查数据是否正确,并生成一个名为 prepared_data_prepared.jsonl 的 JSONL 文件。JSONL 文件是一种将每个 JSON 对象存储在单独一行的格式。JSONL 文件包含一系列 JSON 对象,每个对象之间用换行符分隔。
注意,我们已添加选项“- - quiet”以自动接受‘openai tools fine_tunes.prepare_data’所做的所有建议。例如,它建议在所有提示的末尾添加‘-->’,并在每个响应的末尾添加一个END标记。
这个 JSONL 文件的前几行看起来像这样:
{"prompt":"18, man, invisibility ->","completion":" \n\nHe looks to be in his late teens, with dark eyes and unruly black hair which reach his shoulders. His build is athletic and strong, but not muscular. He often wears clothing that makes him blend in with his surroundings, in order to stay as inconspicuous as possible.\n\nHe has the ability of invisibility, which he can use to become an invisible observer or to make himself or objects around him disappear from view. He has honed his skills to make himself undetectable, able to even make sounds vanish, allowing him to go unnoticed. His invisibility power is his greatest asset, and he uses it to protect those he cares about. END"}
{"prompt":"18, man, invisibility ->","completion":" \n\n18 year-old Jackson is a powerful young man with the incredible power of invisibility. He stands at 6'2 and has a strong athletic frame with wavy brown hair, startling blue eyes and a warm and gentle smile. His strength and agility make him especially good at using his ability. He can become invisible in an instant, moving through walls and other obstacles without being detected. What's more, he can move objects with his mind and use his power to protect those he loves. His power is a blessing and a curse, as it can be abused to do harm. Jackson is a brave and noble person who uses his power to do good and make the world a better place. END"}
(...)
GPT-3 模型的微调实际上是在第二个 subprocess.run() 中实现的,其中执行了 openai api fine_tunes.create。在这个函数中,我们首先提供刚刚创建的 JSONL 文件的名称。然后你需要选择要微调的模型。OpenAI 提供了四个主要模型,具有不同的性能水平,适用于各种任务。Davinci 是最强大的模型,而 Ada 是最快的。Davinci 也是最昂贵的模型 😨。
由于我模型的目的是创建超级英雄的描述,因此我们给我的新模型添加了后缀“超级英雄”。
就这样😉 几分钟后,你将拥有一个可以使用的微调模型🌟。
现在是时候测试你的新模型了。
使用模型进行补全有不同的方法。主要通过 OpenAI 提供的 Playground 或者通过像 Python 这样的编程语言。
最简单的方法可能是使用 playground。
-
点击‘模型’并搜索带有后缀“超级英雄”的模型。
-
在‘停止序列’中添加标记‘END’。

现在是时候要求我们的模型进行新的预测了。我们将要求描述一个 18 岁的男性角色,他真的有一个不必要的能力😉 我们将要求描述一个拥有‘吃很多’能力的角色……看看会发生什么……😆

不错 😅
你想用 Python 来做吗?很简单!点击屏幕右上角的‘查看代码’。

在我们的案例中,在‘查看代码’中我们有:
import os
import openai
openai.api_key = os.getenv("OPENAI_API_KEY")
response = openai.Completion.create(
model="davinci:ft-klhm:superhero-2023-02-01-14-56-48",
prompt="18, Man, can eat a lot ->\n",
temperature=0.7,
max_tokens=256,
top_p=1,
frequency_penalty=0,
presence_penalty=0,
stop=["END"]
)
你只需要复制并粘贴即可👍。
总结
在这个博客中,我们已经了解了如何生成合成数据以优化我们的模型以及如何进行微调。我们使用了创建超级英雄的用例,但相同的方法也可以用于你可能拥有的任何用例。最重要的是要有足够的质量示例,包含提示和期望的响应。
感谢阅读!
如果你希望保持最新的出版物并增加这个博客的可见性,请考虑关注我。
释放 MLflow 的力量
原文:
towardsdatascience.com/unleashing-the-power-of-mlflow-36c17a693033
机器学习生命周期管理的快速导览
·发表于Towards Data Science ·6 分钟阅读·2023 年 5 月 30 日
--

图片由Stephen Dawson提供,来源于Unsplash
你是否曾经发现自己花了几个小时训练模型、调整超参数和选择特征,却意识到你已经有一组不错的参数,但却忘记跟踪或保存模型?我知道我有过,可能比我愿意承认的还要多。在你打开一个电子表格,开始记录模型使用的 alpha 值或邻居数量之前,我想向你介绍 MLflow。
MLflow 是一个多功能的开源平台,旨在管理端到端的机器学习生命周期,由 Databricks 开发。它为机器学习从业者、数据科学家和开发人员提供了一系列好处,使实验、重现性和 ML 模型的部署变得更加简便。所以,让我们来探索一下它能为你做些什么吧!
MLflow 的主要组件
在我们深入探讨使用 MLflow 的细节之前,了解 MLflow 是什么以及它在当今机器学习领域中为何是一个关键工具是至关重要的。
MLflow 帮助管理机器学习生命周期,包括实验、重现性和部署。它兼容任何(Python)机器学习库。它为最常见的库提供了现成的接口,使其具有高度的灵活性,满足你所有的开发需求。
MLflow 由四个主要组件组成:
-
MLflow Tracking:主要 API,记录和组织机器学习实验。它记录参数、指标和工件(如模型、笔记本和脚本),使你能够跟踪实验运行和结果。它配备了一个 UI,你可以通过 localhost 访问来查看、可视化和管理你的实验。
-
MLflow Projects:一种用于重复性和共享的代码打包格式。它定义了 ML 代码的标准结构,使你更容易理解、重用和与他人协作。
-
MLflow 模型:一种用于打包多种 ML 框架的标准格式,以及一个用于存储和共享模型的库。它简化了跨平台的模型部署。
-
MLflow 模型注册表:一个集中管理的模型库,具备模型谱系、版本控制、阶段转换和注释功能。它在需要与团队成员比较和组合模型的协作环境中特别有用。
MLflow 的优势在于其简化和优化 ML 生命周期的能力,使你能够轻松跟踪和比较实验、重现代码、管理模型,并相对容易地部署解决方案。
深入了解 MLflow Tracking:管理你的 ML 实验
让我们通过MLflow Tracking开始这次实践环节。API 和 UI 用于记录和管理你所有的 ML 实验。
你可以在简单的脚本中使用它,也可以将其扩展到大规模训练环境中。我将通过一个简单的代码片段,使用 Scikit-learn 中的 ElasticNet 回归来演示基本用法。
import mlflow
import mlflow.sklearn
from sklearn.linear_model import ElasticNet
from sklearn.metrics import mean_squared_error
import numpy as np
# Initiate a new MLflow run
with mlflow.start_run():
# Train and fit the model
model = ElasticNet(alpha=0.5, l1_ratio=0.5)
model.fit(X_train, y_train)
# Make predictions and calculate the RMSE
predictions = model.predict(X_test)
rmse = np.sqrt(mean_squared_error(y_test, predictions))
# Log parameters
mlflow.log_param("alpha", 0.5)
mlflow.log_param("l1_ratio", 0.5)
# Log metric
mlflow.log_metric("rmse", rmse)
# Log model (artifact)
mlflow.sklearn.log_model(model, "model")
运行此脚本后,你可以在 MLflow Tracking UI 中查看记录的运行详细信息。用户友好的界面允许你根据不同参数和指标过滤和排序运行,从而进行比较分析。

MLflow UI 示例
MLflow 模型注册表:简化你的模型管理
在开始使用 MLflow Tracking 后,你可能也想开始组织你的模型。这就是 MLflow 模型注册表的用武之地,它是一个与 MLflow Tracking 紧密集成的集中模型库。它是个人和团队审查、共享和协作 ML 模型的绝佳工具。
模型注册表简化了模型从实验到生产的过渡。它通过允许模型谱系跟踪、模型版本控制、阶段转换和模型注释来实现这一点。
你可以这样使用模型注册表:
- 从 MLflow Tracking 记录模型:
mlflow.sklearn.log_model(lr_model, "model")
2. 在注册表中注册已记录的模型:
result = mlflow.register_model(
"runs:/d16076a3ec534311817565e6527539c0/model",
"ElasticNetWineModel"
)
模型是通过其运行 ID 注册的(d16076a3ec534311817565e6527539c0在示例中)。
3. 列出所有注册的模型:
你可以通过列出所有注册的模型来检查你的模型是否已成功注册:
mlflow.search_runs()
4. 从注册表中加载模型:
你可以从注册表中加载模型进行预测或评分:
model_uri = "models:/ElasticNetWineModel/1"
model = mlflow.pyfunc.load_model(model_uri)
5. 在阶段之间转换模型版本:
模型注册表允许模型阶段的转换。你可以将模型从“无”状态转换为“暂存”、“生产”或“归档”阶段:
client = mlflow.tracking.MlflowClient()
client.transition_model_version_stage(
name="ElasticNetWineModel",
version=1,
stage="Production",
)
在这个例子中,我们将模型的版本 1 转移到“生产”阶段以供后续使用。
了解 MLflow 模型
MLflow 如何保存你的模型?
MLflow 模型提供了一种标准格式来打包你的机器学习模型,这些模型可以在各种下游工具中使用。例如,通过 REST API 进行实时服务,Spark 集群上的批量推断等。
MLflow 模型使用一种简单的约定来打包模型,每个模型都保存为一个目录,其中包含必要的文件和一个描述符文件,列出模型可以使用的几种“风味”。
一种风味定义了模型可以运行的特定格式或库。对于最重要的库有不同的风味。例如,TensorFlow 模型可以加载为 TensorFlow SavedModel 格式或作为一个 Python 函数应用于输入数据。
如果你有来自自定义库的模型或未内置的模型,你仍然可以在通用 Python 函数风味中使用它:
import mlflow.pyfunc
class ModelWrapper(mlflow.pyfunc.PythonModel):
def load_context(self, context):
# Initialization logic
pass
def predict(self, context, model_input):
# Prediction logic
pass
# Saving the model
mlflow.pyfunc.save_model(path="model_path", python_model=ModelWrapper())
当你准备好提供你的模型(例如用于 API)时,你可以使用mlflow models serve命令:
mlflow models serve -m models:/ElasticNetWineModel/1 -p 1234
此命令将在本地主机的端口1234上提供指定版本的模型(此处为1)。
探索 MLflow 项目:简化你的代码打包
如上所述,MLflow Projects 是一种以可重用和可复现的方式打包你的代码的格式。它主要帮助你在团队之间共享项目,并使其在不同平台上运行。每个项目仅仅是一个包含你的代码和名为MLproject的描述符文件的目录或库。
MLproject文件定义了项目的结构,包括依赖项、入口点和代码的参数。你的MLproject文件可能如下所示:
name: My_Project
conda_env: conda.yaml
entry_points:
main:
parameters:
alpha: float # no default value
l1_ratio: {type: float, default: 0.5} # default value
command: "python main.py {alpha} {l1_ratio}" # run script with params
在这个例子中,main.py是项目的入口点。alpha和l1_ratio是这个脚本的参数。conda.yaml文件列出了项目的 Python 依赖项。
你可以通过mlflow run命令运行项目的入口点:
mlflow run . -P alpha=0.42
该命令获取必要的依赖项,使用给定的和/或默认参数运行你的代码,并将结果记录到跟踪服务器。
总结
这就结束了我们对 MLflow 的快速浏览。
我们现在已经涵盖了 MLflow 的四个组件:MLflow Tracking、MLflow Projects、MLflow Models 和 MLflow Model Registry。这些工具结合在一起,为你提供了一个全面的平台,用于管理机器学习生命周期,促进更好的协作、代码重用和实验跟踪。
无论你是在单独工作还是在多个团队中工作,处理简单模型还是复杂的 ML 管道,我希望你发现 MLflow 是一个值得整合到你的工作流中的工具。
记住,我这里只是简单地触及了 MLflow 功能的表面。要深入了解高级功能、示例和使用方法,请查看官方 MLflow 文档或可能的后续帖子。
和往常一样,我也希望你学到了一些对你有帮助的东西。享受在你的机器学习项目中探索和利用 MLflow 的力量吧!
-Merlin
[## 使用我的推荐链接加入 Medium - Merlin Schäfer
阅读 Merlin Schäfer(以及 Medium 上成千上万其他作家的)每一个故事。你的会员费直接支持…
ms101196.medium.com](https://ms101196.medium.com/membership?source=post_page-----36c17a693033--------------------------------)
来源:
[1] https://mlflow.org/docs/latest/index.html
发掘数据科学家的提示工程潜力
原文:
towardsdatascience.com/unleashing-the-power-of-prompt-engineering-for-data-scientists-16b6d1f2bf85
如何以及为什么在数据工作中编写有效的提示
·发布于数据科学之路 ·18 分钟阅读·2023 年 6 月 7 日
--

图片由Gerd Altmann提供,来源于Pixabay
多亏了 GPT 模型,提示工程正成为数据科学中的一个重要领域。最初,我们看到世界各地许多好奇的人在测试 ChatGPT 以尝试欺骗它。然后,虽然这种趋势(终于!)结束了,但使用它来自动化无聊的任务或帮助处理一般任务的人数稳步增加。
开发人员和数据科学家从使用像 ChatGPT 这样的提示系统中受益良多。因此,在本文中,我们将概述提示工程及其如何为数据科学家编写高效提示。
我知道你从 ChatGPT 中受益良多,对吧?!但事实是,有时候我们作为数据科学家并不能完全从中获得我们想要的结果。所以,让我们看看如何通过一些简单的预防措施来提高我们的提示技能。
这篇文章的内容如下:
**Table of Contents:**
The importance of prompt engineering today
How prompt engineering can affect Data Scientists
Examples of effective prompts for Data Scientists
今天提示工程的重要性
过去 150 年的关键词可能是“自动化”。事实上,世界已经从手工制作的产品演变为生产线。虽然手工艺仍然(高度)有价值,但“批量生产”已成为与“自动化”相伴的词汇。
工作的机械化和自动化在不断增加,这种趋势渗透到了各个领域,不仅限于直接涉及生产商品的领域,例如制造业或农业。
如果我们以软件为例,第一个应该看到的就是自动化。当我大约三年前学习 Python 时,一位导师在审查我的第一个项目时告诉我:“Federico,开发软件意味着自动化事物!” 如果你在问,是的:我的第一个项目是一团糟(就像我们做的所有第一次的事情一样!)。
无论如何,真相是:人类进化的明确目标是:自动化事物。这可以与自动化无聊的事情或“艰苦的工作”相关。无论如何,关键在于朝着自动化的方向前进。
在这种情况下,提示工程只是最新的帮助我们自动化事物的工具。在“代码视角”下,这意味着自动化可自动化的事物:软件开发本质上是自动化,使用提示工程意味着进一步推动自动化。
事实上,即使在软件开发中也存在无聊的任务,即使,例如,我们已经创建了可以导入的类(但需要稍作修改)。
想一想:作为一个数据科学家,你每周开发多少个原型?你有多少时间来开发它们?
你创建一个原型,然后呢?项目的规格发生变化,客户改变主意,你的老板不满意……好吧,你自己说吧。
那么,为什么我们要花费大量精力在低价值但耗时的任务上,而不是自动化它们呢?在我看来,这里是提示工程的核心概念。
那么,让我们看看提示工程如何影响数据科学家,然后看看我们如何创建有用且高效的提示。
提示工程如何影响数据科学家
每项新技术都有其优点和缺点,提示工程也不例外。首先,让我们看看优点,然后是缺点。
数据科学家的提示工程:优点
-
更快的学习。如果你是数据科学领域(以及软件开发领域)初学者,你会发现像 ChatGPT 这样的工具非常有益,因为它就像是 24/7 随时可用的高级开发人员。不过,仍然不应将其视为万能的,主要因为它仍然会出现一些错误。如果你有兴趣,我写了一篇关于如何在 ChatGPT 时代有效开始编程的专门文章,点击这里。
-
更快的原型。在我看来,数据科学家工作中最重要的部分之一是原型开发。事实上,我们经常需要根据数据(通常是可用数据很少,且很脏的数据)快速给出回答。因此,原型可以给出客户需要的回答感,使我们有时间:a) 请求/获取更多数据,b) 请求/获取更多规格,c) 清洗数据,d) 进行必要的研究。
-
更快的调试和错误管理。我们必须诚实地说:在软件开发中,调试和错误管理更多的是一种诅咒而不是一种乐趣。对于机器学习/深度学习算法的软件开发也是如此。ChatGPT 是一个很好的调试和错误管理工具:通过正确的提示,它可以在几秒钟内发现错误和漏洞,节省大量时间和精力。只要提醒一下:由于 ChatGPT(以及类似工具)在云端工作,并且可能使用我们的提示来训练它们的算法,所以要记住不要写包含敏感信息的代码,因为在数据泄露的情况下可能会给你带来麻烦。
-
更快的研究。数据科学家的工作中一个重要部分就是进行研究。我们绝对需要大量的研究来解决问题,例如:特定库及其使用信息、与我们面临的问题领域知识相关的信息等等。好的提示通常对获取所需的信息非常有用。唯一需要记住的是,我们始终需要通过在互联网或书籍上深入验证输出的正确性。特别是对于代码,阅读文档始终很重要:否则,风险就是复制和粘贴代码而实际上不理解它。
数据科学家的提示工程:缺点
-
可能会失去工作。是的,我们必须说:AI 工具可能会使我们失去工作。这似乎是一种矛盾:市场对数据专业人员的需求在这些月里不断增加,但像 ChatGPT 这样的工具可能会取代我们。好吧,实话实说:这个可能性在目前还很遥远,因为 AI 工具需要专家的监督,就像我们在优点中讨论的那样。当然,你可以请求一些代码和数据分析,但如果你不知道如何使用这些代码,你怎么办?所以,是的:提示工程可能会导致一些数据专业人员失去工作,但这是一件几年后才会发生的事情,而不是几个月。
-
可能会忘记如何编程。这是一个实际问题。如果我们过于依赖提示工程而不是自己编写代码,我们可能会忘记如何编程。你知道的:编程是一个需要练习的过程,需要每天的练习。当然,就像骑自行车一样:你永远不会忘记如何骑。但你知道:过于依赖提示而不是编写代码会导致你的技能萎缩,因为你变得过于舒适。因此,使用像 ChatGPT 这样的工具,但不要仅仅依赖于这些工具:尽可能多地努力编写代码。因为我知道你喜欢编程,所以不要过多地依赖机器。
-
可能无法学习新知识。从事 IT 工作,尤其是在数据领域,令人兴奋的一点是新话题和技术几乎每天都在诞生。这也是我转行从事 IT 的原因之一:因为我喜欢不断学习新事物,我希望这被视为一件好事(是的,有些领域/公司认为自我提升不是好事)。但如果你只是依赖提示得到的答案并复制粘贴代码(或信息),你不会学到新知识。在代码方面,至少你需要请求解释,然后阅读文档。接着,如果可能的话,在不同的场景中尝试代码,以便你能熟悉你收到的代码。这也是我们在请求一般信息时需要采取的相同方法:始终通过阅读其他来源来深入了解(就像它应该总是那样:但让我们记住这一点!)。
-
可能出现“冗长的代码”。 代码能正常工作并不是保留它的好理由,你知道的。代码质量对未来的读者(可能是你)也很重要。有时,由于各种因素(通常是提示不正确),像 ChatGPT 这样的工具可能会给我们提供带有不必要部分的“冗长代码”。这就需要专业人士的帮助:不要盲目复制和粘贴。如果你复制和粘贴了它,在验证它有效后,尽量将其改进到最佳状态。
数据科学家的有效提示示例
现在,考虑到这些优缺点,让我们看看我们可以在 ChatGPT 中创建的一些有效提示,以改善和加速我们作为数据科学家的工作。
示例 1:原型
假设我们正在研究一个二分类示例。我们想用学习曲线比较三种 ML 模型,但我们实际上不记得使用的代码。此外,我们的代码库有点乱,我们不想浪费太多时间。
为了加快我们在这种情况下的工作,我们可以向 ChatGPT 提出如下提示:“用 Python 创建一个二分类问题,使用合成数据。使用标准化器对数据进行标准化,然后将数据拆分为训练集和测试集。然后,使用随机森林、KNN 和 SVM 分类器拟合训练集。最后,绘制 ROC 曲线以比较模型。” 这些是结果:
当然!我可以帮助你使用 Python 创建一个二分类问题,使用标准化器对数据进行标准化。然后,我们可以将数据拆分为训练集和测试集。最后,我们将使用三种分类器来拟合训练集:随机森林、K-最近邻(KNN)和支持向量机(SVM),并绘制 ROC 曲线以比较模型。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.svm import SVC
from sklearn.metrics import roc_curve, auc
# Generate synthetic data for binary classification
X, y = make_classification(n_samples=1000, n_features=20, n_informative=10,
n_redundant=10, random_state=42)
# Standardize the data using StandardScaler
scaler = StandardScaler()
X = scaler.fit_transform(X)
# Split the data into train and test sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2,
random_state=42)
# Fit the train set with classifiers
rf_classifier = RandomForestClassifier(random_state=42)
knn_classifier = KNeighborsClassifier()
svm_classifier = SVC(probability=True, random_state=42)
rf_classifier.fit(X_train, y_train)
knn_classifier.fit(X_train, y_train)
svm_classifier.fit(X_train, y_train)
# Generate predictions for the test set
rf_probs = rf_classifier.predict_proba(X_test)[:, 1]
knn_probs = knn_classifier.predict_proba(X_test)[:, 1]
svm_probs = svm_classifier.predict_proba(X_test)[:, 1]
# Calculate false positive rate, true positive rate,
# and area under the curve for ROC curve
rf_fpr, rf_tpr, _ = roc_curve(y_test, rf_probs)
rf_auc = auc(rf_fpr, rf_tpr)
knn_fpr, knn_tpr, _ = roc_curve(y_test, knn_probs)
knn_auc = auc(knn_fpr, knn_tpr)
svm_fpr, svm_tpr, _ = roc_curve(y_test, svm_probs)
svm_auc = auc(svm_fpr, svm_tpr)
# Plot the ROC curve
plt.figure()
plt.plot(rf_fpr, rf_tpr, label=f'Random Forest (AUC = {rf_auc:.2f})')
plt.plot(knn_fpr, knn_tpr, label=f'KNN (AUC = {knn_auc:.2f})')
plt.plot(svm_fpr, svm_tpr, label=f'SVM (AUC = {svm_auc:.2f})')
plt.plot([0, 1], [0, 1], 'k--')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver Operating Characteristic')
plt.legend(loc='lower right')
plt.show()
提供的代码使用 scikit-learn 的
make_classification函数生成一个合成的二分类问题。然后应用StandardScaler对数据进行标准化,并将数据拆分为训练集和测试集。然后在训练集上训练三个分类器:随机森林、K-近邻(KNN)和支持向量机(SVM)。为了评估,分类器使用
predict_proba方法为测试集生成概率。下一步涉及绘制接收者操作特征(ROC)曲线,以比较分类器的性能。使用
roc_curve函数计算假正例率(FPR)和真实正例率(TPR),使用auc函数计算曲线下面积(AUC)。最终,ROC 曲线使用 matplotlib 绘制,每个分类器的 AUC 显示在图例中。该曲线表示每个分类器在真实正例率和假正例率之间的权衡,允许对它们的性能进行可视化比较。
通过检查 ROC 曲线并比较 AUC 值,你可以评估随机森林、KNN 和 SVM 分类器在给定二分类问题上的相对性能。
现在,如果我们将这段代码复制并粘贴到 Jupyter Notebook 中,我们会得到:

上述代码生成的 ROC 曲线。图片由 Federico Trotta 提供。
我们得到了想要的结果,所以让我们分析一下这个提示:
-
它很具体。我指定了:a) 语言(Python),b) 问题类型(二分类),c) 需要遵循的过程(数据创建、数据标准化、数据拆分),和 d) 使用的 ML 模型。
-
它包含了所需的一切。没有额外的词语可能会造成混淆。提示直截了当。
-
它很简短。仅有 3 行。较长的提示可能导致错误的输出或使模型添加我们可能不需要的信息。如果你必须插入“长提示”,我的建议是将“大任务”分解成较小的任务,并输入不同的短提示,这些短提示可以是连续的。明确来说,上述提示可以分解成连续的对话,如:a) “使用 Python 创建一个二分类数据集,对数据进行标准化,并将其拆分为训练集和测试集”,b) “用随机森林、SVM 和 KNN 分类器拟合训练集,然后绘制 ROC 曲线”
示例 n°2:研究
假设我们知道多项式回归通常会过拟合,但我们想要证明,因为我们从未见过。提示可能是这样的:“我知道多项式回归模型通常会过拟合:你能展示一些东西吗,通过在 Python 中创建一些数据?”
结果如下:
当然!我可以生成一些随机数据并演示多项式回归如何导致过拟合。这是一个 Python 示例:
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
# Generate random data
np.random.seed(0)
X = np.linspace(-3, 3, 100)
y = 0.5 * X**2 + X + np.random.normal(0, 1, 100)
# Reshape the input data
X = X.reshape(-1, 1)
# Plot the original data
plt.scatter(X, y, color='b', label='Data')
# Fit polynomial regression models of different degrees
degrees = [1, 4, 15]
colors = ['r', 'g', 'm']
for degree, color in zip(degrees, colors):
# Create polynomial features
polynomial_features = PolynomialFeatures(degree=degree)
X_poly = polynomial_features.fit_transform(X)
# Fit the polynomial regression model
model = LinearRegression()
model.fit(X_poly, y)
# Predict the values
y_pred = model.predict(X_poly)
# Plot the fitted curve
plt.plot(X, y_pred, color=color, linewidth=2, label=f'Degree {degree}')
# Add labels and title to the plot
plt.xlabel('X')
plt.ylabel('y')
plt.title('Polynomial Regression - Overfitting Example')
plt.legend(loc='upper left')
# Display the plot
plt.show()
这段代码生成具有二次关系的随机数据点,并向 y 值添加一些噪声。然后,它将不同度数(在这个例子中为 1、4 和 15)的多项式回归模型拟合到数据上,并绘制出结果曲线。
你会观察到,随着多项式的度数增加,模型开始拟合数据中的噪声而不是潜在的趋势。这是过拟合的一个例子。线性模型(度数为 1)过于简单,无法捕捉潜在的二次关系,而高次多项式(度数为 15)试图精确拟合每一个数据点,从而导致曲线剧烈震荡。
需要注意的是,我提供的这个例子只是多项式回归中过拟合的简单说明。在实践中,你通常会使用更现实和复杂的数据集来评估和解决过拟合问题。
现在,让我们看看在 Jupyter Notebook 中得到什么:

下面的图显示了用上述代码创建的数据拟合差异。图片来自 Federico Trotta。
因此,这个图表显示了,给定一些二次分布的数据:
-
线性模型无法抓住路径。
-
4 度和 15 度的多项式“过于贴合路径”。
现在告诉我:这是否意味着这两个多项式过拟合了?
停下来思考一下。
是的和不是,但主要是“不是”。实际上,正如我们所知,过拟合发生在:
-
我们定义了评估数据的指标。
-
指标在训练集和测试集上都进行了计算,我们发现它们之间有很大的数值差异。
因此,这个提示不够令人满意,我们需要提出更多问题。例如,我们可以问:“好,那就用相同的数据和相同的多项式。然后,归一化数据,将数据集拆分为训练集和测试集,并计算两个集合的 R²”。
我们得到:
当然!这是一个更新版的代码,它归一化数据,将其分为训练集和测试集,并计算这两个集的 R²分数:
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.model_selection import train_test_split
from sklearn.metrics import r2_score
# Generate random data
np.random.seed(0)
X = np.linspace(-3, 3, 100)
y = 0.5 * X**2 + X + np.random.normal(0, 1, 100)
# Reshape the input data
X = X.reshape(-1, 1)
# Normalize the data
X_normalized = (X - np.mean(X)) / np.std(X)
y_normalized = (y - np.mean(y)) / np.std(y)
# Split the data into train and test sets
X_train, X_test, y_train, y_test = train_test_split(
X_normalized, y_normalized, test_size=0.2, random_state=0
)
# Plot the original data
plt.scatter(X_normalized, y_normalized, color='b', label='Data')
# Fit polynomial regression models of different degrees
degrees = [1, 4, 15]
colors = ['r', 'g', 'm']
for degree, color in zip(degrees, colors):
# Create polynomial features
polynomial_features = PolynomialFeatures(degree=degree)
X_poly_train = polynomial_features.fit_transform(X_train)
X_poly_test = polynomial_features.transform(X_test)
# Fit the polynomial regression model
model = LinearRegression()
model.fit(X_poly_train, y_train)
# Predict the values for train and test sets
y_pred_train = model.predict(X_poly_train)
y_pred_test = model.predict(X_poly_test)
# Calculate R² scores
r2_train = r2_score(y_train, y_pred_train)
r2_test = r2_score(y_test, y_pred_test)
# Plot the fitted curve
plt.plot(X_normalized, model.predict(polynomial_features.transform(X_normalized)), color=color, linewidth=2,
label=f'Degree {degree} (Train R²={r2_train:.2f}, Test R²={r2_test:.2f})')
# Add labels and title to the plot
plt.xlabel('X')
plt.ylabel('y')
plt.title('Polynomial Regression - Overfitting Example (Normalized)')
plt.legend(loc='upper left')
# Display the plot
plt.show()
在这个更新的代码中,数据首先通过减去均值并除以标准差进行归一化。然后,使用 scikit-learn 的
train_test_split()将数据拆分为训练集(80%的数据)和测试集(20%的数据)。多项式回归模型使用归一化的训练数据进行拟合,然后使用来自 scikit-learn 的
r2_score()计算训练集和测试集的 R²分数。这些 R²分数随后显示在每条拟合曲线的图例中。通过归一化数据并计算训练集和测试集上的 R²分数,你可以评估多项式回归模型的表现,并判断它们是否过拟合或对未见数据的泛化能力如何。
在这种情况下,我们得到:

下面的图显示了用上述代码创建的数据拟合差异。图片来自 Federico Trotta。
那么,我们得到了什么?嗯……我们看不到明显的过拟合迹象,因为测试集上的 4 度多项式 R² 稍微增加,而 15 度多项式的 R² 稍微下降。
这里的错误在于我们继续了开始的聊天。让我们记住,LLM 是神经网络,它们有过拟合的倾向。因此,有时候,如果你继续提示,你会得到相同的答案(或非常相似的东西),即使它返回了一些“对不起,我犯了个错误”的信息。
此外,我不需要任何图表。因此,在这种情况下,我们最好创建另一个聊天。现在让我们尝试更具体一些:“在 Python 中创建一个 2 度数据集。对数据进行标准化,并将其分割为训练集和测试集。用:线性回归、4 度多项式和 15 度多项式模型来拟合数据。打印每个模型在训练集和测试集上的 R 平方值。”
然后我们得到:
要在 Python 中创建一个 2 度的数据集,对其进行标准化,并将其分割为训练集和测试集,你可以使用
numpy和sklearn库。以下是你如何实现这一点的示例:
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.metrics import r2_score
# Create the dataset
X = np.random.rand(100, 1) # Independent variable
y = 3 * X**2 + np.random.randn(100, 1) # Dependent variable (quadratic relationship)
# Normalize the data
X_normalized = (X - np.mean(X)) / np.std(X)
y_normalized = (y - np.mean(y)) / np.std(y)
# Split the data into train and test sets
X_train, X_test, y_train, y_test = train_test_split(X_normalized, y_normalized, test_size=0.2, random_state=42)
# Fit the linear regression model
linear_regression = LinearRegression()
linear_regression.fit(X_train, y_train)
# Fit the 4-degree polynomial regression model
poly_features = PolynomialFeatures(degree=4)
X_poly_train = poly_features.fit_transform(X_train)
X_poly_test = poly_features.transform(X_test)
poly_regression = LinearRegression()
poly_regression.fit(X_poly_train, y_train)
# Fit the 15-degree polynomial regression model
poly_features = PolynomialFeatures(degree=15)
X_poly_train = poly_features.fit_transform(X_train)
X_poly_test = poly_features.transform(X_test)
poly_regression_15 = LinearRegression()
poly_regression_15.fit(X_poly_train, y_train)
# Calculate R-squared for train and test sets
linear_train_r2 = linear_regression.score(X_train, y_train)
linear_test_r2 = linear_regression.score(X_test, y_test)
poly_train_r2 = poly_regression.score(X_poly_train, y_train)
poly_test_r2 = poly_regression.score(X_poly_test, y_test)
poly_15_train_r2 = poly_regression_15.score(X_poly_train, y_train)
poly_15_test_r2 = poly_regression_15.score(X_poly_test, y_test)
# Print the results
print("Linear Regression R-squared (Train):", linear_train_r2)
print("Linear Regression R-squared (Test):", linear_test_r2)
print("4-Degree Polynomial Regression R-squared (Train):", poly_train_r2)
print("4-Degree Polynomial Regression R-squared (Test):", poly_test_r2)
print("15-Degree Polynomial Regression R-squared (Train):", poly_15_train_r2)
print("15-Degree Polynomial Regression R-squared (Test):", poly_15_test_r2)
在这个例子中,我们生成了一个具有独立变量
X和依赖变量y之间的二次关系的随机数据集。然后,使用 z-score 标准化对数据进行归一化。接下来,使用来自sklearn.model_selection模块的train_test_split将数据集分割为训练集(80%)和测试集(20%)。然后,我们拟合了三种不同的模型:线性回归、4 度多项式回归和 15 度多项式回归。对于多项式回归,我们使用来自
sklearn.preprocessing的PolynomialFeatures类来创建高达所需度数的多项式特征。使用归一化的训练数据来拟合模型。最后,我们使用各自模型的
score方法计算每个模型在训练集和测试集上的 R 平方值。R 平方值表示依赖变量中方差的比例,这部分方差可以通过
好的,我们得到了我们想要的结果,并附带了一些解释。现在,如果我们尝试一下,我们得到:
ValueError: X has 16 features, but LinearRegression
is expecting 5 features as input.
哎呀!正如我们所说,出现一些错误是很正常的。所以,在这种情况下,我们可以简单地继续聊天,例如:“我得到:ValueError: X 有 16 个特征,但 LinearRegression 期望输入 5 个特征。”
现在它返回:
对于错误表示歉意。似乎多项式特征转换出现了问题。要解决这个问题,我们需要确保在归一化的
X数据上应用多项式转换,而不是在原始X数据上。以下是修正后的代码:
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.metrics import r2_score
# Create the dataset
X = np.random.rand(100, 1) # Independent variable
y = 3 * X**2 + np.random.randn(100, 1) # Dependent variable (quadratic relationship)
# Normalize the data
X_normalized = (X - np.mean(X)) / np.std(X)
y_normalized = (y - np.mean(y)) / np.std(y)
# Split the data into train and test sets
X_train, X_test, y_train, y_test = train_test_split(X_normalized, y_normalized, test_size=0.2, random_state=42)
# Fit the linear regression model
linear_regression = LinearRegression()
linear_regression.fit(X_train, y_train)
# Fit the 4-degree polynomial regression model
poly_features = PolynomialFeatures(degree=4)
X_poly_train = poly_features.fit_transform(X_train)
X_poly_test = poly_features.transform(X_test)
poly_regression = LinearRegression()
poly_regression.fit(X_poly_train, y_train)
# Fit the 15-degree polynomial regression model
poly_features_15 = PolynomialFeatures(degree=15)
X_poly_train_15 = poly_features_15.fit_transform(X_train)
X_poly_test_15 = poly_features_15.transform(X_test)
poly_regression_15 = LinearRegression()
poly_regression_15.fit(X_poly_train_15, y_train)
# Calculate R-squared for train and test sets
linear_train_r2 = linear_regression.score(X_train, y_train)
linear_test_r2 = linear_regression.score(X_test, y_test)
poly_train_r2 = poly_regression.score(X_poly_train, y_train)
poly_test_r2 = poly_regression.score(X_poly_test, y_test)
poly_15_train_r2 = poly_regression_15.score(X_poly_train_15, y_train)
poly_15_test_r2 = poly_regression_15.score(X_poly_test_15, y_test)
# Print the results
print("Linear Regression R-squared (Train):", linear_train_r2)
print("Linear Regression R-squared (Test):", linear_test_r2)
print("4-Degree Polynomial Regression R-squared (Train):", poly_train_r2)
print("4-Degree Polynomial Regression R-squared (Test):", poly_test_r2)
print("15-Degree Polynomial Regression R-squared (Train):", poly_15_train_r2)
print("15-Degree Polynomial Regression R-squared (Test):", poly_15_test_r2)
现在代码应该可以正确工作,对归一化数据应用多项式转换,并相应地拟合模型。
现在,如果我们测试一下,我们得到:
Linear Regression R-squared (Train): 0.5287297254229243
Linear Regression R-squared (Test): 0.4627146369316367
4-Degree Polynomial Regression R-squared (Train): 0.5895312356444804
4-Degree Polynomial Regression R-squared (Test): 0.4601670183046247
15-Degree Polynomial Regression R-squared (Train): 0.6256705123172766
15-Degree Polynomial Regression R-squared (Test): 0.26972190687190367
在这里,我们可以看到 4 度模型的轻微过拟合和 15 度模型的明显过拟合。实际上,随着多项式的次数增加,它过拟合数据的概率也会增加。我们来考虑一下:我们有一个 2 度的数据分布,而我们正试图用 15 度的多项式来拟合它。因此,15 度的多项式很容易捕捉到数据中的每一个小的变化,从而导致过拟合。
结论
在这篇文章中,我们已经看到提示工程的重要性,它如何影响数据科学家,以及一些针对数据科学家的提示示例。
记住,提示应该简洁明了,我们始终需要验证答案。但正如我们所展示的,收益是巨大的。
免费 Python 电子书:
刚开始学习 Python 数据科学却感到困难?订阅我的通讯并获取免费的电子书:这将为您提供正确的学习路径,以便通过实践经验学习 Python 数据科学。
享受这个故事吗?通过我的推荐链接成为 Medium 会员,费用为 5$/月:我将获得一小笔佣金,而您无需支付额外费用。
## 通过我的推荐链接加入 Medium - Federico Trotta
阅读 Federico Trotta(以及 Medium 上成千上万其他作者)的每一个故事。您的会员费直接支持……
释放 Python Asyncio 队列的力量
原文:
towardsdatascience.com/unleashing-the-power-of-python-asyncios-queue-f76e3188f1c4
PYTHON CONCURRENCY
通过实际案例掌握使用 asyncio 的生产者-消费者模式
·发表于 Towards Data Science ·8 分钟阅读·2023 年 6 月 6 日
--

图片来源:由作者创建,Canva
在这篇文章中,我将轻松讲解 Python asyncio 中各种队列的 API 用法和应用场景。
在文章末尾,我将展示在经典购物场景中asyncio.Queue的实际用法。
介绍
为什么我们需要 asyncio.Queue
正如阅读过我之前文章的读者所知,我喜欢 asyncio,因为它几乎是并发编程的终极解决方案。
然而,在大规模、高度并发的项目中,大量不可控的并发任务等待将占用系统资源,导致性能差。
因此,有必要控制并发任务的数量。
为什么我们不能使用asyncio.Semaphore
在我之前关于同步原语的文章中,我介绍了使用Semaphore锁来控制同时运行的并发任务的数量。
## 在 Python Asyncio 中掌握同步原语:全面指南
asyncio.Lock、asyncio.Semaphore、asyncio.Event 和 asyncio.Condition 的最佳实践
[towardsdatascience.com
首先设置Semaphore锁的数量,获得锁的任务将会执行,而没有获得锁的任务则会等待。
然而,asyncio.Semaphore只能限制通过 IO 访问资源的任务的并发性;无法限制并发任务的数量。
因此,在这种情况下,asyncio.Semaphore不是一个完美的解决方案。
asyncio.Queue 就是这样的方式
使用asyncio.Queue,我们可以在程序启动时启动固定数量的并发任务,然后通过队列将待处理的数据传递给这些任务。
这就是著名的生产者-消费者模式。同时,像多进程队列一样,asyncio.Queue也用于在并发任务之间传递消息。
asyncio 队列的魔法世界
为什么asyncio.Queue会扮演如此重要的角色?实际上,我们在现实生活中也会遇到类似的问题:

超市中的人群。由作者创建,Canva
最典型的例子是大型购物超市。在这样的超市里,总是有很多顾客。
每个顾客购物完后需要结账。结账需要一些时间,这可能会导致拥堵。
更直接的解决方案是雇佣更多的收银员,最好每个顾客一个,以实现即时结账。
然而,这并不现实,因为这么多的收银员意味着老板需要承受巨大的成本压力(和资源消耗)。
因此,有人想出了一个好主意:让顾客排队,然后让几个收银员轮流结账。

超市中的顾客正在排队。照片由Lisanto 李奕良提供,来源于Unsplash
唯一的成本是顾客需要多等一会儿。同时,如果队列太长,经理可以选择临时增加几个收银员。这样,系统可以灵活扩展。
通过将顾客队列比作数据进入队列,将收银员比作并发任务,我们可以看到asyncio.Queue带来的好处:
-
它是生产者-消费者模式的良好实现。
-
它可以控制并发任务的数量。
-
这样可以让资源消耗变得可控,并且系统也可以灵活扩展。
生产者-消费者模式在 asyncio 中的冒险
什么是生产者-消费者模式
想象一下两种任务共享一个队列。任务 A 生成数据并将其放入队列,而任务 B 从队列中取出数据进行处理。
这就是生产者-消费者模式,其中任务 A 是生产者,任务 B 是消费者。
类比超市,顾客是生产者,收银员是消费者,顾客队列代表队列。
为什么使用生产者-消费者模式
在高并发程序中,生产者通常会快速生成数据,而消费者处理数据较慢。因此,生产者必须等待消费者完成处理后才能继续生产数据。
有时,消费者处理数据的速度较快,而生产者生成数据的速度较慢。这导致消费者在继续运行之前需要等待生产者生成数据。
为了平衡生产者和消费者,需要一个队列来存储生产者产生的数据。队列充当缓冲区,并将生产者和消费者解耦。

生产者-消费者模式图。图像由作者提供
使用 asyncio 的 Queue 实现生产者-消费者模式
现在,让我们使用 asyncio.Queue 实现之前提到的超市购物场景。
如代码所示,我们首先实现了 Customer 和 Product 类,分别表示需要结账的顾客和产品。Product 类有一个 checkout_time 属性,表示结账所需的时间。
之后,我们实现了一个 checkout_customer 方法作为消费者。
只要队列中有数据,该方法将继续循环。在每次迭代中,它使用 get 方法来获取一个 Customer 实例。
如果队列中没有数据,它将等待。
在检索到一条数据(在本例中是一个 Customer 实例)后,它会遍历 products 属性,并使用 asyncio.sleep 来模拟结账过程。
在处理完数据后,我们使用 queue.task_done() 来告诉队列数据已成功处理。
接下来,我们实现了 generate_customer 方法作为生产客户的工厂方法。
我们首先定义一个产品系列及每个产品所需的结账时间。然后,我们在每个顾客的购物车中放置 0 到 10 个产品。
此外,我们实现了 customer_generation 方法作为生产者。该方法定期生成几个客户实例并将其放入队列。如果队列已满,put 方法将会等待。
最后,我们使用 main 方法来初始化队列、生产者和消费者,并启动所有并发任务。

实现成功。图像由作者提供
正如预期的那样,实施成功。
引入 PriorityQueue
为什么使用 asyncio.PriorityQueue
前面提到的队列是一个 先进先出 (FIFO) 队列,其中第一个进入队列的项目是第一个被取出的。这在队列中的所有任务具有相同优先级时很合适。
然而,考虑以下情况:
假设有一个队列中有待处理的任务,每个任务都需要很长的处理时间。
错误日志或 VIP 用户访问是需要立即关注的高优先级任务。我们该怎么办?

这就是[asyncio.PriorityQueue](https://docs.python.org/3/library/asyncio-queue.html#priority-queue)发挥作用的地方。
简要描述asyncio.PriorityQueue的实现
与基于列表的 FIFO 队列不同,asyncio.PriorityQueue基于堆。它是通过二叉树结构构建的。
你可能对二叉搜索树比较熟悉,它确保最小的节点始终是最左边的节点。
然而,asyncio.PriorityQueue中的二叉树确保最小的节点始终在顶部,因此最高优先级的节点会被优先移除。

左侧是 PriorityQueue 使用的二叉树,右侧是二叉搜索树。 图片来源:作者
使用asyncio.PriorityQueue的实际例子
让我们通过一个实际存在的场景来说明asyncio.PriorityQueue的使用。
想象一下我们有一个订单服务 API。这个 API 需要时间来处理每个订单,但我们不能让用户等待太久。
所以当用户下订单时,API 首先将订单放入队列中,允许后台任务异步处理,同时立即向用户返回一条消息。
这个 API 接受两种类型的用户订单:普通用户和 VIP 用户。它必须确保 VIP 用户的订单以最高优先级处理。

VIP 订单以最高优先级处理。 图片来源:作者
为了保持读者的学习曲线较低,在这个例子中,我们将使用aiohttp来实现服务器。具体代码如下:
首先,我们定义一个枚举来标记两种类别:普通用户和 VIP 用户。
接下来,我们使用dataclass定义一个用户的订单,其中包含用户类型和订单处理时长。订单时长不考虑在优先级排序中。
然后我们定义消费者方法process_order_worker,它从队列中获取订单并模拟订单处理。
不要忘记使用queue.task_done()来告诉队列我们已经处理完订单。
接下来,我们使用aiohttp实现订单 API。这个 API 响应用户请求,生成一个订单对象,并将其放入asyncio.PriorityQueue。
它随后立即返回响应给用户,避免了用户的等待时间。
当程序启动时,我们使用create_order_queue来初始化队列和订单消费任务。
当程序结束时,我们使用destroy_order_queue来确保队列中的所有订单都被处理,并且后台任务正确关闭。
queue.join()将等待队列中的所有数据处理完成。asyncio.wait_for设置了 20 秒的超时,超时后将不再等待queue.join()完成。
我们可以使用 PyCharm 的 HTTP 请求来测试这个实现:

API 优先处理 VIP 用户的订单。图片来源:作者
正如你所见,两个高优先级的任务按预期处理了。完美!
结论
在本文中,我介绍了asyncio.Queue的使用及最佳实践。
当你需要控制程序的并发性时,我推荐使用asyncio.Queue来有效管理资源消耗。
我介绍了生产者-消费者模式及其优点:
-
在生产者和消费者之间平衡,最大化资源利用率。
-
解耦系统,允许生产者和消费者独立扩展。
最后,我展示了如何使用asyncio.PriorityQueue通过一个实际示例处理需要优先级的场景。
由于空间限制,我未能覆盖asyncio.Queue的所有方面。然而,我希望这篇文章提供了对基本概念和有用示例的扎实理解。
Python 的异步编程是一个强大的工具,使用asyncio.Queue的生产者-消费者模式是处理应用程序中的并发和优先级的多功能方法。
你可以通过我的文章列表获取更多关于 Python 并发的知识:

Python 并发
查看列表10 个故事!用 Aiomultiprocess 超强提升你的 Python Asyncio: 全面指南
[## 通过我的推荐链接加入 Medium - 彭倩
阅读彭倩的每一个故事(以及 Medium 上的其他成千上万位作者)。您的会员费直接支持彭倩……
本文最初发布于:www.dataleadsfuture.com/unleashing-the-power-of-python-asyncios-queue/
释放 Julia 超级类型的力量
原文:
towardsdatascience.com/unleashing-the-power-of-the-julia-supertype-bb369209efca
使用和操作抽象在 Julia 语言中做有趣的事情
·发布于 Towards Data Science ·阅读时间 7 分钟·2023 年 10 月 11 日
--

作者提供的图片
介绍
我发现不同编程语言探索中最令人着迷的事情之一是不同范式解决不同类型问题的方式。在编程中,现代编程语言可能会有一些预期的特性。这些特性的例子包括范围、多态性和抽象。一些范式在某些领域中的应用会更为突出,数据科学也不例外。这些特性在各种编程范式中通过多种不同的方法得以实现。当一个具有不典型范式的语言实现这些通用编程概念时,这个主题对我而言变得更加有趣。这引出了 Julia 编程语言。
Julia 编程语言是近年来编程世界中较为有趣的发展之一。编程世界对几种经过数十年打磨的选择性范式已经相当熟悉,这些范式创造了非常强大的意识形态方法来解决编程问题。虽然 Julia 采用了许多更通用的编程概念来完成工作,但该语言的范式本身极为独特,常常带来不同的做事方式。这被称为多重分发编程范式。当涉及到数据科学家所做的事情时,这种范式是一种非常有效的编程范式。
有了这种编程范式关系,Julia 会稍微调整其机制,以实现大多数程序员希望使用的更多通用编程概念。Julia 中有许多功能体现了这一点。所有这些概念都反馈到多重分发范式的强大能力中。对于有经验的程序员,我发现这可能是一个极其强大的范式!话虽如此,无论是什么范式,了解可用的工具以便充分利用它们是很重要的。一个非常适合 Julian 范式的技术示例是 Julian 的抽象实现。
抽象在数据科学中将是一个基本的技术。在数据科学中,观察值可能有很多不同的类型,像多态性这样的概念可能非常有效,因为它在不同结构的处理方式之间找到了共同点。
正如预期的那样,Julian 抽象是使用多重分发来实现的。然而,使用多重分发的方式有一些细微差别,这些细节很重要,因此事不宜迟,让我们来回顾一下 Julia 中抽象的方方面面!
超类型基础
为了开始创建我们自己的超类型,我们需要熟悉 abstract type 语法。abstract type 是一个定义松散的类型,仅存在于名称中——没有字段。通过这些名称的不同组织,我们可以将方法应用于我们类型层次结构的受限级别。一个很好的例子就是数字。
在 Julia 中,数字根据几个不同的实际类别进行组织。结果结构以层次结构的方式组织不同类型的数字,这被称为类型层次结构。在 Julia 中,这种类型层次结构始终以 Any 开始。Any 是一个抽象类型,每一个 Julia 类型都是其子类型。这个层次结构的数字部分从 Any 开始,接着是 Number,然后是 Real 数字——进一步细化为 Integers 及其他。

(图像由作者提供)
深入探讨 Julia 如何处理数字。
我们与 Julia 内部抽象交互的主要工具是子类型操作符<:。这是一种按位布尔操作符,意味着它根据两个操作数返回 true 或 false;按位指的是两个操作数,与一元操作相对,而布尔表示返回值的真/假性质。如果第一个操作数是第二个操作数的子类型,则该条件为真。在这个实例中,第一个操作数将是一个Type或抽象类型。第二个操作数将始终是抽象类型——因为事物只能是抽象类型的子类型。
# Everything <: Any!
Number <: Any
true
Int64 <: Number
true
Int64 <: AbstractString
false
这个操作符还在另外两个实例中使用。一个是多重调度,另一个是将类型指定为子类型。后者可以为超类型或常规类型完成。这是通过在定义后提供子类型操作符,然后跟上我们想要它下面的类型来完成的。
# we do not have to <: as Any, I just put this here to
# demonstrate how this is done.
abstract type AbstractExample <: Any end
struct Example <: AbstractExample
end
超类型调度
创建这些类型的层次结构是很好的,但更好的做法是与多重调度结合使用。通过多重调度,我们可以调度到我们想要的确切级别,从而创建仅涵盖特定类别的函数。在以下情况下,任何Float或Integer都将在此函数中以不同的方式处理:
remainder(x::Integer) = 0
remainder(x::AbstractFloat) = x - floor(x)
同样,如果这些函数的功能相同,我们可以上升一个层次并调度Real,然后创建一个函数。这是一个非常简单的实现,能够完美完成工作。
在这种情况下使用调度的一个细微差别是将调度子类型作为参数。例如,在某些情况下,我们可能会有一个看起来像这样的参数:
funcexamp(x::Vector{Number}) = begin
end
在这种情况下,我们希望任何包含数字的Vector都被调度到这个函数。然而,这不是我们编写的函数——相反,我们调度了一个Vector{Number} …
julia> myvec = [5, 10]
2-element Vector{Int64}:
5
10
julia> funcexamp(myvec)
ERROR: MethodError: no method matching funcexamp(::Vector{Int64})
Closest candidates are:
funcexamp(::Vector{Number})
@ Main REPL[1]:1
Stacktrace:
[1] top-level scope
@ REPL[3]:1
julia> myvec = Vector{Number}([5, 10])
2-element Vector{Number}:
5
10
julia> funcexamp(myvec)
这里的解决方案有点晦涩——我们以这种奇怪的方式使用子类型操作符调度参数。
funcexamp(x::Vector{<:Number}) = begin
end
奇怪的是,这是对这个操作符的一种一元用法——确实很奇怪,但它完成了工作,并且在其他方面逻辑上是有意义的。不过,这确实是一个需要注意的细微差别。
字段
当谈到 Julia 中的抽象时,字段也应该是讨论的一部分。简而言之,Julia 更倾向于不在字段中使用抽象类型。原因是,当类型的字段可以是几种不同类型时,Julia 的性能会受到严重影响。这对于多重调度来说也可能是一个非常不好的做法。利用通用函数构造器可能比使用通用结构更好。例如,以下结构可能不是一个好主意。
mutable struct Calculator
x::Real
y::AbstractString
end
在这种情况下,Real和AbstractString是模糊的字段类型。我们可以通过决定一个更好的字段或字段类型,或者使用一个参数来表示类型,来轻松解决这个问题。每当我们使用一个参数时,我们就为该参数可能的每种不同设置创建一个新类型。换句话说,Calculator{Float64}是与Calculator{Int64}不同的类型,而这两个类型的x字段将始终是该参数的类型。为了重新组织,我们在构造函数中添加一个参数,并将字段改为该参数。
mutable struct Calculator{T}
x::T
y::AbstractString
end
我们还可以对子类型进行显式化,明确我们期望这个字段是什么。
mutable struct Calculator{T <: Real}
x::T
y::AbstractString
end
至于y的情况,这可能是一个更好地使用不同类型的例子。在大多数情况下,AbstractString作为一个简单的String会更好。
mutable struct Calculator{T <: Real}
x::T
y::String
end
为了构造这个Calculator,现在需要提供这个参数。我们可以通过使用一个内部构造函数创建一个新的调度来改变这一点。
mutable struct Calculator{T <: Real}
x::T
y::String
function Calculator(x::Real, y::AbstractString)
new{typeof(x)}(x, string(y))
end
end
超类型内省
在这个关于超类型的概述中,我想指出的最后一点是能够内省超类型。虽然这显然不像 Julia 中的许多其他内省形式那样有用,但在弄清楚一些事情时这确实会很有帮助。最明显的这种内省形式在前面已经提到,即区分一个类型是否是一个抽象类型的子类型。
Int64 <: Integer
true
不幸的是,Julia 也没有提供很多超出这一点的选项。另一件可能有价值的事情是获取特定类型的层级的漂亮打印输出。这里有一个在 Julia 社区广泛流传的相当不错的函数可以做到这一点
function subtypetree(t, level=1, indent=4)
level == 1 && println(t)
for s in subtypes(t)
println(join(fill(" ", level * indent)) * string(s))
subtypetree(s, level+1, indent)
end
end
subtypetree (generic function with 3 methods)
我相信这个特定版本的原始来源来自于tk3369在 github 上的发布。
julia> subtypetree(Number)
Number
Complex
Real
AbstractFloat
BigFloat
Float16
Float32
Float64
AbstractIrrational
Irrational
Integer
Bool
Signed
BigInt
......
结论
抽象是现代高级编程语言中一个极其重要的工具。在多个地方创建相同的函数是繁琐的,程序员已经意识到这一点。许多语言有着截然不同的范式,因此实现这些编程概念的技术可能非常有趣。一个显著的例子就是 Julia 语言及其类型层级。
尽管 Julia 处理这个问题的方法有些不同寻常,但它通过极其强大的能力弥补了这种异常。通过这种抽象形式,我们可以具体到每个参数的精确类型,也可以模糊到允许Any通过我们的方法。总体而言,它确实非常强大,值得了解。感谢您的阅读!
用你的笔记本电脑释放数据科学的潜力
原文:
towardsdatascience.com/unlock-data-science-potential-with-your-laptop-64c64652e906

从零到英雄的笔记本电脑
·发表于 Towards Data Science ·6 分钟阅读·2023 年 11 月 7 日
--
作为数据科学家,你知道拥有一个可靠且高效的笔记本电脑是多么重要,它能够处理你项目所需的所有任务和工具。
无论你是处理大型数据集、复杂模型还是 BI 可视化,你都希望避免任何可能影响结果的技术问题或性能瓶颈。这就是为什么我认为为你的笔记本电脑做好成功准备是你数据科学之旅中的关键一步。
注意。本文主要针对 Windows 用户。 尽管如此,本文中的一些内容可能对 Linux 和 Mac 用户也有兴趣。
通过遵循我们的步骤和提示,你将能够将你的笔记本电脑转变为一个强大且高效的数据科学机器,能够应对你可能遇到的任何任务或挑战。
现在,让我们开始吧,为成功做好准备!
最重要的是——你不能没有的东西
1. 安装 Anaconda Prompt
令人惊讶的是,并不是每个数据科学家都在使用Anaconda。不要成为其中之一。
在安装库时,创建虚拟环境并避免破坏操作系统是至关重要的。对于每个项目,都要创建一个conda环境,并仅在该环境中安装相关库。
注意。确保你的 conda 环境中已安装pip。通常,当在新的 conda 环境中使用 pip 安装时,你实际上是在使用默认 conda 环境中的 pip,因此它不会使用新的环境。

资源。来自作者。
如果你无法从终端使用conda,且遇到以下问题,你基本上需要通过更新环境变量将 Anaconda 添加到你的路径中。

资源。来自作者。
更具体地说,要将 Anaconda 添加到你的环境变量中,你可以按照 stackoverflow 上给出的步骤进行操作。下面是根据 Anaconda 安装在我个人机器上的位置进行的编辑。

资源。来自作者。
我的建议是在安装过程中让 Anaconda 为你处理。当系统询问是否将 Anaconda3 添加到你的路径中时,点击是。
Anaconda 文档详细说明了如何卸载 Anaconda。
最后,请注意,Anaconda 虚拟环境有替代方案。其中之一是常见的 Python 虚拟环境 (virtualenv)。不过,我个人选择的是 conda。
要创建一个
virtualenv,可以调用virtualenv YOUR_ENV_NAME,其中*YOUR_ENV_NAME*是一个目录,用于放置新的虚拟环境及所有感兴趣的包。
这里最重要的是记住,创建一个包含所有相关包的独立环境对于你的项目是很重要的。例如,如果明天你接到一个新项目,不要急于进入项目,而是先设置一个包含所有必要包的专用环境。这样的环境可以通过多种方式与同事共享。在我的经验中,遵循这一过程也帮助我更快地将模型投入生产。
2. 在终端中获取 Linux 命令
要获取 Linux 命令,可以使用这个方法。你需要安装git,传统的安装方式。然后,对于像我这样的 Windows 用户,你需要按如下方式修改环境变量 [系统变量]:
-
安装git
-
将 C:\Program Files\Git\usr\bin 添加到你的路径变量中 [系统变量]
重要提示。 如果你在 Git 文件夹中看不到“usr”文件夹,那是因为它是隐藏的。即使你看不到它,也要按照上面和下面显示的准确名称添加该文件夹。
问题。由于某些原因,“ls”、“clear”等 Linux 命令能够正常工作,但过几周后会消失。有人能告诉我为什么我需要如此频繁地执行上述步骤并编辑环境变量吗?

资源。来自作者。
我个人无法在本地机器上编写代码,除非使用这些命令。我的最常用命令是:
-
ls/cd/mkdir
-
conda 相关命令 (conda create — name env_name python=3)
-
pip 相关命令 (pip install …)
-
docker 相关命令 (当使用 docker 时)
3. 获取一个 IDE
Visual Studio Code 或 PyCharm。
我曾经重度使用 PyCharm。最近我转向了 Visual Studio Code。两者都是很好的选择。
安装所有相关的扩展。例如,对于 VS code:
-
python 扩展
-
jupyter 扩展
-
github copilot 扩展

资源。来自作者。
现在构建干净的代码几乎是必不可少的,特别是在大型科技公司。因此,我建议你使用上述编辑器之一,这样你可以在保持代码整洁和有序的同时调试代码。
4. 了解如何在云端复制你的工作
这实际上并不是你笔记本电脑的一部分,但行业越来越多地使用云来进行各种项目。你可能会在一家利用 Microsoft Azure、Amazon AWS 或 Google 云的公司工作。很常见,你会被要求直接在这些平台上工作。因此,能够在云端复制你在本地所做的工作是一个很好的技能。
我知道在 Azure Machine Learning studio 中,创建你的 conda 环境非常简单,因此按照上述步骤将会使你向云端的过渡变得更加顺利。做一些功课以了解这与其他云数据科学平台的关系。另一方面,Azure Synapses 的工作方式与 pyspark 依赖的后台有所不同。
问题。你将如何优化从本地机器到云端的过渡?
更具异国情调
Windows Terminal
Linux 命令如 “ls”、 “cd” 等是非常有价值的。我在之前的 Surface 笔记本电脑上已经配置了这些命令。这一次,我在找到相关文档来进行设置时遇到了一些困难。
起初我以为 Windows Terminal 默认支持 Linux 命令。然而,事实并非如此。因此,你可以将 Windows Terminal 视为一个时尚的终端,你可以在其中拆分屏幕并选择多个终端,如命令提示符或 PowerShell。
根据我的经验,这是一个顶级的生产力功能,我通常会在一个终端中进行 git 工作,在另一个终端中运行和调试我的 Python 项目,等等。

资源。来自 en.wikipedia.org/wiki/Windows_Terminal。
“Windows Terminal 是一个 多标签 终端模拟器,由 Microsoft 开发,用于 Windows 10 及以后版本[4] 替代 Windows Console。[5] 它可以在单独的标签页中运行任何命令行应用。它预配置了运行 命令提示符、PowerShell、WSL、SSH 和 Azure Cloud Shell Connector。”[6][7] 来源于维基百科

深度学习的 GPU。
Cuda 通常是你在这里寻找的关键字。不过,对于 macOS,还有其他选项。
深度学习和需要大量计算能力的统计项目通常从 GPU 的运行中受益,因为它比 CPU 快得多。历史上,我在本地机器上运行 GPU 时遇到了许多问题,通常会寻找云解决方案,其中自然嵌入了 GPU,仅需一些 Python 工具,如 pytorch 或 tensorflow 来将操作推向 GPU。Azure ML、Google Colab 和其他机器学习的替代云解决方案都提供了 GPU 功能,无需安装各种软件。

照片由 BoliviaInteligente 提供,发布在 Unsplash
👋 还有最后一件事——想要联系吗?
我是微软的数据科学家,曾任教于 EPITA 巴黎。我拥有 8 项 AI 专利,并不断推动 AI 的前沿。
我希望你能成为我的前 1000 名粉丝之一。
另外,关注我的 LinkedIn.
发掘 Python 的全部潜力
原文:
towardsdatascience.com/unlock-the-full-potential-of-python-bc78a980168b
可读且高效的代码提示与技巧
·发布于 Towards Data Science ·阅读时间 9 分钟·2023 年 5 月 10 日
--

照片由 Maxwell Nelson 提供,刊登于 Unsplash
Python 是开发人员和数据科学家中最受欢迎的编程语言之一。开发人员使用该语言进行后台和前端开发。数据科学家和分析师使用 Python 进行数据分析。主要的机器学习库都可以在 Python 中使用。因此,Python 在数据科学家中非常受欢迎。
在这篇文章中,我们展示了一些 Python 的提示和技巧。这些提示和技巧将帮助你让 Python 代码更高效、更易读。你可以在下一个 Python 项目中直接使用这些技巧。我们将提示结构化成可以独立阅读的形式。阅读标题并决定哪个提示或技巧对你感兴趣!现在就开始吧!
使用 enumerate()
在 Python 中,你通常会对一个可迭代对象编写 for() 循环。因此,你不需要一个计数变量来访问元素。然而,有时确实需要一个计数器变量。有几种方法可以实现这样的 for() 循环。今天我们将展示两种变体。下面的示例代码展示了变体 A。
# variant A
company_list = ["Tesla", "Apple", "Block", "Palantir"]
i = 0
for company in company_list:
print(i, company)
i+=1
# Output:
# 0 Tesla
# 1 Apple
# 2 Block
# 3 Palantir
在这个示例中,我们创建了一个计数变量 i。我们在每次迭代时递增这个计数变量。这种实现容易出错。你必须记住在每次迭代时更新 i。
你可以通过在循环中使用 enumerate() 来避免这个错误。不要将可迭代对象直接插入 for() 循环中,而是将可迭代对象放入 enumerate() 的括号中。下面的示例展示了它是如何工作的:
# variant B
company_list = ["Tesla", "Apple", "Block", "Palantir"]
for count, company in enumerate(company_list, start=0):
print(count, company)
# Output
# 0 Tesla
# 1 Apple
# 2 Block
# 3 Palantir
enumerate() 函数还允许你设置计数器的起始值。该函数从可维护性、可读性和效率方面改进了你的代码。
使用 zip()
Python 的zip()函数创建一个迭代器,将两个或更多可迭代对象的元素结合起来。使用这个迭代器,你可以解决常见的编程问题。我们展示了如何用实际例子使用 Python 函数zip()。
你可以将zip()函数视作一个物理的拉链。这种类比将帮助你理解zip()函数。zip()函数的参数是可迭代对象,并返回一个迭代器。这个迭代器创建一系列的元组。元组包含了每个可迭代对象中的元素。zip()函数接受任何类型的可迭代对象(例如文件、列表、元组、字典等)。以下代码块展示了它如何处理两个列表作为参数。
list_a = [0,1,1]
list_b = [2,3,5]
zipped = zip(list_a, list_b)
list(zipped)
# Output
# [(0, 2), (1, 3), (1, 5)]
注意,zip()函数返回一个迭代器。我们需要使用list()来调用列表对象。在上面的示例中,迭代对象的长度是相同的。然而,也可能出现迭代对象长度不同的情况。我们现在将更详细地查看这种情况。
list(zip(range(7), range(42)))
# Output
# [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6)]
我们在上面的代码示例中看到使用了最短可迭代对象的长度。zip()函数忽略了较长可迭代对象中的剩余元素。从 Python 3.10 开始,zip()有了一个新的可选参数叫做 strict。这个参数的主要目的是提供一种安全的方式来处理不等长的可迭代对象。看看下面的代码示例。
list(zip(range(7), range(42), strict=True))
# Output
# ---------------------------------------------------------------------------
# ValueError Traceback (most recent call last)
# Cell In[2], line 1
# ----> 1 list(zip(range(7), range(42), strict=True))
#
# ValueError: zip() argument 2 is longer than argument 1
我们得到一个 ValueError,因为长度不匹配。当你需要确保函数仅接受相同长度的可迭代对象时,strict参数非常有用。
对多个可迭代对象进行循环是 Python zip()函数最常见的使用案例之一。zip()函数对迭代序列(例如列表、字典)非常有用。我们展示了如何使用zip()同时运行多个可迭代对象。我们还将经典方法与zip()方法进行比较。以下代码示例展示了遍历两个列表的经典方式。
from time import perf_counter
list_a = range(0, 10_000, 1)
list_b = range(10_000, 20_000, 1)
start = perf_counter()
for i in range(len(list_a)):
a = list_a[i]
b = list_b[i]
print(perf_counter()-start)
# Output
# 0.00775 s -> 7.75 ms
在这个例子中,我们迭代了两个列表list_a和list_b。在for()循环中,我们必须手动访问每个列表元素。这一过程增加了代码的运行时间。现在让我们来看看如何使用zip()函数改进代码。
from time import perf_counter
list_a = range(0, 10_000, 1)
list_b = range(10_000, 20_000, 1)
start = perf_counter()
for a,b in zip(list_a, list_b):
# do something
pass
print(perf_counter()-start)
# Output
# 0.00093 s -> 0.93 ms
在这个例子中,我们再次遍历列表list_a和list_b。现在我们可以直接在for()循环中访问列表的各个元素。其优点是代码更简短且运行时间更短。
不要对数字中的_感到惊讶。解决方案在最后一个提示中。
解包和打包函数参数
我们来看一个接收两个参数的 add 函数。在下面,你将看到经典的实现方式,和你在其他编程语言中看到的一样。
def add_function(a, b):
print(a + b)
foo = 2
bar = 5
add_function(foo, bar)
# Output
# 7
在这个例子中,我们将 5 和 2 相加。结果是 7。在 Python 中,我们还可以将参数列表传递给函数。这真的很酷!
# Unpacking
def add_function(a, b):
print(a + b)
foo1 = [2, 5]
foo2 = {'a':2, 'b':5}
add_function(*foo1)
add_function(**foo2)
# Output
# 7
# 7
在这个例子中,我们使用*来解包列表,以便将其所有元素传递出去。此外,我们使用**来解包字典。
我们还可以传递任意数量的参数。这被称为打包。
# Packing
def add_function(*args):
print(sum(args))
add_function(1, 1, 2, 3, 5)
# Output
# 12
上面的add_function()将所有参数打包到一个变量中。通过打包的变量,我们可以像使用普通元组一样处理它。args[0]和args[1]给你第一个和第二个参数。
解包和打包使你的代码更少出错且更清晰。它如此简单,你可以直接在下一个项目中使用它。
就地交换值
许多开发者使用临时变量来缓存。实际上有一种更简单更快速的方式,无需创建额外的变量。请看下面的代码。
# with additional tmp variable
a = 2
b = 4
tmp = a
a = b
b = tmp
print(a, b)
# Output
# 4 2
# without additional tmp variable
a = 2
b = 4
a, b = b, a
print(a, b)
# Output
# 4 2
你会发现这非常简单。然而,这种方法不仅限于变量。你也可以将其应用于列表。下面的代码展示了使用前几个斐波那契数的示例。
example_list = [1, 1, 2, 3, 5]
example_list[0], example_list[3] = example_list[3], example_list[0]
print(example_list)
# Output
# [3, 1, 2, 1, 5]
在这个例子中,我们重新排列了列表元素的顺序。往往是简单的东西让你的代码更干净。记住这个提示用于你的下一个 Python 项目。
避免使用字典的Get()方法引发异常
作为一名 Python 开发者,你已经多次见过KeyError异常。目标是防止意外的KeyError异常发生。以下代码展示了一个小例子中的异常。
dict={1: 2, 3: 4, 3: 5}
print(dict[8])
# Output
# ---------------------------------------------------------------------------
# KeyError Traceback (most recent call last)
# Cell In[30], line 2
# 1 dict={1: 2, 3: 4, 3: 5}
# ----> 2 print(dict[8])
#
# KeyError: 8
停止此错误的一个解决方案是使用.get()函数。我们来使用.get()。
dict={1: 2, 3: 4, 3: 5}
print(dict.get(8))
# Output
# None
现在,我们不会得到KeyError异常,因为我们使用了更安全的.get()方法。如果未找到键,返回值为 None(默认)。你还可以通过传递第二个参数来指定默认值。
dict={1: 2, 3: 4, 3: 5}
print(dict.get(8), 0)
# Output
# 0
在这个例子中,我们将默认返回值改为 0。.get()函数让你的代码更安全。
使用上下文管理器
Python 的上下文管理器是语言中一个强大的特性,用于管理资源、网络和数据库连接。诸如文件、网络或数据库连接等资源会在需要或不需要时自动设置和清理。上下文管理器在资源稀缺的项目或处理多个连接时非常有用。此外,上下文管理器还可以帮助处理错误。在 Python 中,with语句实现了上下文管理器。我们来看一个例子。
import json
with open('config/config_timescaleDB.json') as timescaleDB_file:
config_timescaleDB_dict = json.load(timescaleDB_file)
首先,我们来看语法。在with语句之后是上下文管理器表达式。这个表达式负责上下文管理器对象的设置。上下文管理器表达式的示例包括数据库连接或打开文件(如我们的例子)。as之后是一个可选变量,它接收上下文管理器的__enter__()方法的结果。在我们的例子中,这是 JSON 文件。接着是代码块。在代码块中,我们在上下文中执行代码。在我们的例子中,我们加载 JSON 文件并将内容保存到字典中。
在代码主体执行后,with 语句调用上下文管理器的 __exit__() 方法。__exit__() 方法执行清理操作(例如:关闭文件或释放资源)。实际上,with 语句至关重要,特别是在处理数据库连接时。使用上下文管理器可以确保数据库连接自动关闭。高效的资源管理变得可能。
使用上下文管理器时,许多手动工作被自动化处理。这导致代码中不是所有步骤都可见。因此,当你第一次查看代码时,很难理解。
上下文管理器应用于内存优化和清理资源管理,因为它们可以减少你的工作量。
处理大数字
你曾经在 Python 中处理过大数字吗?哪个数字更易读,1000000 还是 1,000,000?是第二个,对吧?不幸的是,第二种表示法在 Python 中不起作用。然而,在 Python 中,你可以使用 _ 代替 ,。我们来看一个例子。
# bad way
million = 1000000 # 1 million
thousand = 1000 # 1 thousand
total = million + thousand
print(f'{total:,}')
# Output
# 1,001,000
# smart way
million = 1_000_000 # 1 million
thousand = 1_000 # 1 thousand
total = million + thousand
print(f'{total:,}')
# Output
# 1,001,000
两种方法给出相同的结果。唯一的区别是大数字的表示方式。第二种方法更易读,避免了错误。
结论
在这篇文章中,我们学习了如何编写更清洁和更安全的代码。关键发现如下:
-
在循环中使用 enumerate() 函数:你可以获得当前迭代的计数和该迭代项的值。
-
在循环中使用 zip() 以迭代多个可迭代对象。
-
使用拆包和打包来减少错误并使代码更清晰。
-
避免使用临时变量进行就地值交换。
-
使用字典的 .get() 方法避免 KeyError 异常。
-
使用上下文管理器来管理资源。
-
使用 _ 来处理大数字,使你的代码更具可读性。
👉🏽 加入我们的免费每周魔法 AI 时事通讯,获取最新的 AI 更新!
免费订阅 以便在我们发布新故事时收到通知:
[## 订阅邮件以获取 Janik 和 Patrick Tinz 的最新发布。
每当 Janik 和 Patrick Tinz 发布时,你将收到一封电子邮件。通过注册,你将创建一个 Medium 账户,如果你还没有的话…
tinztwinspro.medium.com](https://tinztwinspro.medium.com/subscribe?source=post_page-----bc78a980168b--------------------------------)
了解更多关于我们的信息,请访问我们的 关于页面。不要忘记关注我们在 X。非常感谢你的阅读。如果你喜欢这篇文章,请随意分享。祝你有个美好的一天!
通过使用我们的链接注册 Medium 会员,即可阅读无限量的 Medium 故事。
解锁音频数据的潜力:使用 Whisper、WhisperX 和 PyAnnotate 进行高级转录和语音分段
利用最先进的语音识别和说话人归属技术简化音频分析
·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 4 月 17 日
--
引言
在我们快节奏的世界中,我们生成了大量的音频数据。想想你最喜欢的播客或工作中的会议电话。数据在原始形式中已经非常丰富,我们作为人类可以理解它。即便如此,我们仍然可以进一步处理,例如将其转换为书面格式以便后续搜索。
为了更好地理解当前任务,我们引入了两个概念。第一个是转录,即将口语转化为文本。第二个是我们在本文中探讨的语音分段。语音分段帮助我们为无结构的内容提供额外的结构。在这种情况下,我们关注的是将特定的语音片段归属于不同的说话人。
在上述背景下,我们通过使用不同的工具来解决这两个任务。我们使用了 Whisper,这是一个由 OpenAI 开发的通用语音识别模型。它在各种音频样本数据集上进行了训练,研究人员开发了它以执行多种任务。其次,我们使用了 PyAnnotate,这是一个用于说话人归属的库。最后,我们使用了 WhisperX,这是一个研究项目,旨在结合这两者,同时解决 Whisper 的一些局限性。

图 1:与机器对话(来源)。
本文属于“大型语言模型纪实:探索 NLP 前沿”系列文章,该系列每周更新,旨在探讨如何利用大型模型的力量来完成各种 NLP 任务。通过深入了解这些前沿技术,我们的目标是赋能开发者、研究人员和爱好者,充分发挥 NLP 的潜力,开启新的可能性。
目前已发布的文章:
一如既往,代码可在我的 Github 上获得。
Whisper: 一个通用的语音识别模型
Whisper 是一个通用的语音识别模型,在各种语音处理任务中表现非常好。它在多语言语音识别、语音翻译、口语语言识别和语音活动检测方面相当稳健。
Whisper 的核心是一个 Transformer 序列到序列模型。该模型将各种语音处理任务共同表示为由解码器预测的令牌序列。该模型通过使用特殊令牌作为任务说明符或分类目标,取代了传统语音处理管道的多个阶段。我们可以将其视为语音处理任务的元模型。
Whisper 有五种模型大小,针对边缘设备或大型计算机。它允许用户根据他们的使用情况和系统的容量选择合适的模型。请注意,一些仅支持英语的版本在处理英语用例时表现更好。
PyAnnotate: 说话人分离库
说话人分离是通过不同说话人识别和分割语音的过程。这项任务在分析呼叫中心的数据时非常有用,例如,当我们想要分开客户和代理的声音时。公司可以利用它来改善客户服务并确保公司政策的合规性。
PyAnnotate 是一个专门设计用于支持这项任务的 Python 库。该过程相对简单。它对数据进行预处理,允许我们从原始音频文件中提取特征。接下来,它根据提取的特征生成相似的语音段集群。最后,它将生成的集群分配给不同的说话人。
WhisperX: 带有语音活动检测和强制音素对齐的长音频转录
正如我们在前面部分看到的,Whisper 是一个大规模的、弱监督的模型,训练用于在语音处理领域执行多个任务。虽然它在不同领域甚至不同语言中表现良好,但在长音频转录时却有所不足。其限制在于训练过程使用了滑动窗口方法,这可能导致漂移甚至幻觉。此外,它在对齐转录与音频时间戳时也存在严重限制。在进行说话人分离时,这一点尤为重要。
为了应对这些限制,牛津大学的一个研究小组正在积极开发 WhisperX。Arxiv 预印本论文上个月发布。它使用语音活动检测(VAD),检测人声的存在与否,并对输入音频文件进行预分段。然后,它将这些片段裁剪并合并成大约 30 秒的窗口,通过定义在语音概率较低的区域(从语音模型得出的)上界限来实现。这一步还有一个额外的好处:允许使用批量转录与 Whisper 配合。这提高了性能,同时减少了我们上面讨论的漂移或幻觉的概率。最后一步称为强制对齐。WhisperX 使用音素模型将转录与音频对齐。基于音素的自动语音识别(ASR)识别语音的最小单位,例如“big”中的“g”元素。这种后处理操作将生成的转录与音频时间戳在单词级别对齐。
整合 WhisperX、Whisper 和 PyAnnotate
在本节中,我们整合了 WhisperX、Whisper 和 PyAnnotate 来创建我们自己的 ASR 系统。我们设计了我们的方法,以处理长形式的音频转录,同时能够分段语音并为每个片段分配特定的说话人。此外,它减少了幻觉的概率,提高了推理效率,并确保转录与音频之间的正确对齐。让我们构建一个流程来执行不同的任务。
我们从转录开始,将从音频文件中识别出的语音转换为书面文本。transcribe函数加载由model_name指定的 Whisper 模型并转录音频文件。它然后返回一个包含转录片段和语言代码的字典。OpenAI 设计的 Whisper 还具有语言检测功能,是一个多语言模型。
def transcribe(audio_file: str, model_name: str, device: str = "cpu") -> Dict[str, Any]:
"""
Transcribe an audio file using a speech-to-text model.
Args:
audio_file: Path to the audio file to transcribe.
model_name: Name of the model to use for transcription.
device: The device to use for inference (e.g., "cpu" or "cuda").
Returns:
A dictionary representing the transcript, including the segments, the language code, and the duration of the audio file.
"""
model = whisper.load_model(model_name, device)
result = model.transcribe(audio_file)
language_code = result["language"]
return {
"segments": result["segments"],
"language_code": language_code,
}
接下来,我们使用align_segments函数对齐转录片段。如前所述,这一步对准确的说话人分离至关重要,因为它确保每个片段对应到正确的说话人:
def align_segments(
segments: List[Dict[str, Any]],
language_code: str,
audio_file: str,
device: str = "cpu",
) -> Dict[str, Any]:
"""
Align the transcript segments using a pretrained alignment model.
Args:
segments: List of transcript segments to align.
language_code: Language code of the audio file.
audio_file: Path to the audio file containing the audio data.
device: The device to use for inference (e.g., "cpu" or "cuda").
Returns:
A dictionary representing the aligned transcript segments.
"""
model_a, metadata = load_align_model(language_code=language_code, device=device)
result_aligned = align(segments, model_a, metadata, audio_file, device)
return result_aligned
对齐转录片段后,我们可以进行说话人分离。我们使用diarize函数,它利用了 PyAnnotate 库:
def diarize(audio_file: str, hf_token: str) -> Dict[str, Any]:
"""
Perform speaker diarization on an audio file.
Args:
audio_file: Path to the audio file to diarize.
hf_token: Authentication token for accessing the Hugging Face API.
Returns:
A dictionary representing the diarized audio file, including the speaker embeddings and the number of speakers.
"""
diarization_pipeline = DiarizationPipeline(use_auth_token=hf_token)
diarization_result = diarization_pipeline(audio_file)
return diarization_result
在分离说话人后,我们使用assign_speakers函数为每个转录片段分配说话人。这是我们流程中的最后一步,完成了将原始音频文件转换为包含说话人信息的转录文本的过程:
def assign_speakers(
diarization_result: Dict[str, Any], aligned_segments: Dict[str, Any]
) -> List[Dict[str, Any]]:
"""
Assign speakers to each transcript segment based on the speaker diarization result.
Args:
diarization_result: Dictionary representing the diarized audio file, including the speaker embeddings and the number of speakers.
aligned_segments: Dictionary representing the aligned transcript segments.
Returns:
A list of dictionaries representing each segment of the transcript, including the start and end times, the
spoken text, and the speaker ID.
"""
result_segments, word_seg = assign_word_speakers(
diarization_result, aligned_segments["segments"]
)
results_segments_w_speakers: List[Dict[str, Any]] = []
for result_segment in result_segments:
results_segments_w_speakers.append(
{
"start": result_segment["start"],
"end": result_segment["end"],
"text": result_segment["text"],
"speaker": result_segment["speaker"],
}
)
return results_segments_w_speakers
最后,我们将所有步骤合并到一个transcribe_and_diarize函数中。该函数返回一个字典列表,表示每个转录片段,包括开始和结束时间、发言文本以及说话人标识符。请注意,你需要一个 Hugging Face API 令牌才能运行该流程。
def transcribe_and_diarize(
audio_file: str,
hf_token: str,
model_name: str,
device: str = "cpu",
) -> List[Dict[str, Any]]:
"""
Transcribe an audio file and perform speaker diarization to determine which words were spoken by each speaker.
Args:
audio_file: Path to the audio file to transcribe and diarize.
hf_token: Authentication token for accessing the Hugging Face API.
model_name: Name of the model to use for transcription.
device: The device to use for inference (e.g., "cpu" or "cuda").
Returns:
A list of dictionaries representing each segment of the transcript, including the start and end times, the
spoken text, and the speaker ID.
"""
transcript = transcribe(audio_file, model_name, device)
aligned_segments = align_segments(
transcript["segments"], transcript["language_code"], audio_file, device
)
diarization_result = diarize(audio_file, hf_token)
results_segments_w_speakers = assign_speakers(diarization_result, aligned_segments)
# Print the results in a user-friendly way
for i, segment in enumerate(results_segments_w_speakers):
print(f"Segment {i + 1}:")
print(f"Start time: {segment['start']:.2f}")
print(f"End time: {segment['end']:.2f}")
print(f"Speaker: {segment['speaker']}")
print(f"Transcript: {segment['text']}")
print("")
return results_segments_w_speakers
评估集成 ASR 系统的性能
让我们从测试我自己录制的一个短音频片段开始。视频中有两个发言者,我们需要识别它们。此外,注意到其中一个发言者在讲话中有多个犹豫,使得转录变得困难。我们将使用 Whisper 的base模型来评估其能力。为了更好的准确性,你可以使用medium或large模型。转录文本如下:
段落 1:
开始时间:0.95
结束时间:2.44
发言者:SPEAKER_01
转录文本:你在看什么电视节目?
段落 2:
开始时间:3.56
结束时间:5.40
发言者:SPEAKER_00
转录文本:目前我在看《我们这一天》。
段落 3:
开始时间:6.18
结束时间:6.93
发言者:SPEAKER_01
转录文本:内容是什么?
段落 4:
开始时间:8.30
结束时间:15.44
发言者:SPEAKER_00
转录文本:这是关于一个家庭在几代人中的生活。
段落 5:
开始时间:15.88
结束时间:21.42
发言者:SPEAKER_00
转录文本:你可以通过这个系列某种程度上体验他们的生活。
段落 6:
开始时间:22.34
结束时间:23.55
发言者:SPEAKER_01
转录文本:下一步会是什么?
段落 7:
开始时间:25.48
结束时间:28.81
发言者:SPEAKER_00
转录文本:也许是牛肉,我听说关于它的评价很好。
基础模型的执行时间:8.57 秒
基础模型的内存使用:3.67GB
我们的方法通过上述转录实现了其主要目标。首先,注意到转录是准确的,我们成功地忽略了说话中的犹豫。我们生成了语法正确的文本,这有助于可读性。段落被很好地分隔,并且与音频时间戳对齐。最后,发言者分离也得到了妥善执行,两个发言者被准确地分配到每个语音段落。
另一个重要方面是各种模型在长格式音频上的计算效率,特别是在 CPU 和 GPU 上运行推断时。我们选择了一个约 30 分钟的音频文件。以下是结果:

图 2:使用 CPU 和 GPU 的各种模型的执行时间(图片由作者提供)。
主要结论是这些模型非常庞大,需要更高效才能在大规模运行。对于 30 分钟的视频,我们在 CPU 上需要大约 70–75 分钟来完成转录,而在 GPU 上则大约需要 15 分钟。此外,请记住,我们需要大约 10GB 的 VRAM 来运行大型模型。考虑到这些模型仍处于研究阶段,我们应该预期这些结果。
结论
本文提供了使用最先进的语音识别和说话人分离技术分析音频数据的全面逐步指南。我们介绍了 Whisper、PyAnnotate 和 WhisperX,这些工具共同形成了一个强大的集成 ASR 系统——我们的方案在处理长格式音频转录时产生了有希望的结果。它还解决了 Whisper 在长格式音频转录中出现的主要限制问题,包括确保转录与音频对齐、准确分割语音以及将说话人归属到每个片段。
尽管如此,这些模型的计算效率仍然是一个挑战,尤其是对于长格式音频以及在有限硬件上运行推理时。即便如此,Whisper、WhisperX 和 PyAnnotate 的集成展示了这些工具在处理和分析音频数据方面的潜力,为各种行业和用例带来了新的应用可能性。
关于我
serial entrepreneur 和 AI 领域的领导者。我开发面向企业的 AI 产品,并投资于专注于 AI 的初创公司。
创始人 @ ZAAI | LinkedIn | X/Twitter
解锁因果推断的力量:数据科学家理解“后门调整”公式的指南
使用 Python 和 pgmpy 库的“后门调整”公式的完整工作示例
·发布于 Towards Data Science ·9 分钟阅读·2023 年 1 月 19 日
--

照片由 Roberto Huczek 提供,Unsplash
介绍
在概率理论中,查看数据集并根据其他变量的情况计算事件的概率是非常简单的。
例如:

即,销售的概率等于在产品已被搜索的情况下点击链接的概率。
然而,当数据中存在因果效应时,这种方法就会失效,这时因果推断就派上用场了。根据因果关系的模式,有多种方法可以选择,本文将重点介绍如何解锁“后门调整”公式的力量。
“后门标准”存在于 X 对 Y 的因果影响被第三个因素所“混淆”,该因素同时影响 X 和 Y 时 …

图片由作者提供
在这种情况下,公式 𝑝(𝑌|𝑋) 因为 Z 的混淆效应而不起作用,需要从 Pearlean 的“do”微积分中应用“后门调整”公式以获得正确结果:

这看起来非常复杂,尤其是对于那些刚接触“do”微积分的人,但实际上很容易理解和应用。
在本文结束时,你将理解如何在
Python和pgmpy中应用后门调整公式,它为何如此强大以及幕后发生了什么。
入门
要开始,我们需要一些标准导入和一个完全合成和虚构的测试数据集。
数据是从下面的 csv 文件中读取的,但如果你想了解如何生成合成二进制数据,以下文章提供了完整的解释 -
## 如何在少于 10 行代码中为任何因果推断项目生成合成数据
如果 99%的人接种疫苗,1%会出现反应,2%会感染疾病,如何生成具有代表性的合成数据?
## 如何在少于 10 行代码中为任何因果推断项目生成合成数据 
作者提供的图片
合成数据代表了一项虚构的药物试验结果,测试对象为 1000 人,所有人都有药物测试的医疗状况。例如,第一行代表一位男性(male=1),他服用了药物(drug=1)并康复了(recovery=1)。
问题
让我们先用一行 Python 代码快速查看数据中的传统概率,涉及康复和患者结果:
drug recovery
0 1 0.826
0 0.174
1 1 0.778
0 0.222
Name: recovery, dtype: float64
结果显示,77.8%的服药患者康复,但 82.6%未服药的患者康复。传统的概率方法显然表明药物有负面影响,药物试验应该结束。
然而,特征之间存在因果关系,这意味着需要应用因果推断和“do”操作符来建立真实效果。
因果解决方案
数据中的因果关系可以通过简单的有向无环图(DAG)进行可视化……

作者提供的图片
创建 DAG 的代码在本文中未列出,但完整的解释可以在这里找到:
如何用一行代码生成交互式有向无环图,以可视化和理解因果推断模型
因果图显示,虽然药物服用对恢复有因果影响,但事情并非如此简单。性别(男性)对“药物”和“恢复”都有因果影响,因为 …
-
相较于女性,更多的男性决定服用药物
-
男性的恢复率高于女性
因此,我们必须“去混淆” “男性”对药物服用的影响,以获得真实的影响。
理想的情况是我们回到过去,强迫所有人服用药物并测量影响。然后我们再次回到过去,这一次阻止所有人服用药物。我们只需比较这两个结果,就能得到答案!
然而,这种解决方案受到时间旅行的不可行性以及强迫或防止药物服用的伦理、道德和法律方面的影响。
不过,还是有前进的办法。那就是 Pearlean “do”计算,它提供了一个公式,将干预性的“do”转换为仅包含我们已知的观察数据的等效公式。
本文接下来将提供一种使用pgmpy库的简单因果解决方案,并提供一个手动执行所有计算的第二版本,以展示其工作原理。
解决方案 1:使用 Pgmpy 库
第一阶段是使用在有向无环图中定义的因果关系创建pgmpy因果模型。Pgmpy创建了一组条件概率表,描述因果关系,可以轻松地显示以了解发生了什么 …
以下代码将调用TabularCPD.__str__ 在pgmpy库中以显示条件概率表 …
… 但我已将它们以更直观和易于理解的格式展示在下面 …

作者提供的图像
下一阶段是对模型运行两次“do”操作,一次为 drug=1,再次为 drug=0。然后可以从第一次结果中减去第二次结果,以计算药物服用的整体影响,独立于“男性”并去混淆 …
If the drug is taken by everyone p(recovery)=0.8301
If the drug is not taken by anyone p(recovery)=0.7779
The improvement in recovery rate by everyone taking the drug is 5.2%
因此,pgmpy能够实现回到过去并重新运行药物试验的魔法。第一次重新运行强迫所有人服用药物,第二次阻止任何人服用药物,然后通过简单的减法得出答案,但pgmpy是如何实现这种魔法的?
解决方案 2:手动应用“do”计算
我们已经从 DAG 中得出结论,认为“药物”和“恢复”都受到“男性”的混淆,在因果推断中这种模式称为“后门”标准。
因此,任务是通过编写干预的数学公式来模拟干预(时间旅行的部分!),然后“调整”它,使其以我们可以观察的数据为表达。
“引言”部分的后门调整公式可以表示为以下药物试验数据的形式 -

从这里开始,计算药物的整体效果是直接的,结果如下 -
-
使用后门调整标准计算干预或“do-ing” Drug=1 的效果。
-
使用后门调整标准计算干预或“do-ing” Drug=0 的效果。
-
从第一部分的结果中减去第二部分的结果。
-
如果药物产生积极影响,则整体结果将是一个正数。
结果称为“平均因果效应”,可以表示如下 -

… 以及在药物示例中 …

使用后门调整公式替换左侧和右侧,得到如下结果

因此,我们需要通过将 Male=1 和 Male=0 的结果相加来解决减号左侧(即所有人都服用药物的干预),具体如下 -

这些概率可以直接从df_drug DataFrame中轻松计算,但它们已经在条件概率表中很好地总结了,因此可以立即替换如下 …

现在我们需要解决扩展 ACE 公式中减号右侧的内容(即防止所有人服用药物的干预) …

再次,我们可以直接从条件概率表中读取并替换概率 …

现在可以通过从第一个结果中减去第二个结果来计算平均因果效应 (ACE) …

因此,每个人服用药物的恢复率提高了 5.2%,这与使用pgmpy库计算出的结果完全一致!
结论
传统的概率方法在数据中存在因果关系时无法产生正确答案,需要因果技术来计算正确的结果。
本文使用了一个合成数据集,展示了服用药物对患者恢复的真实效果为 5.2%的正面影响,而传统的概率方法则建议 5%的负面影响。
应用因果推断技术需要一个“有向无环图”来定义因果关系,然后将其与数据集一起作为因果计算的输入,以展示真实效果。
pgmpy库完成了看似不可能的魔法,回到过去干预药物试验,不仅一次,而是两次,第一次强制每个人服用药物,然后阻止任何人服用药物。
然而,这不是魔法。它是通过应用 Pearlean “do” 计算中的“后门调整公式”来实现的,长期计算已被解释并通过将结果与
pgmpy库中的结果匹配来验证。
如果您喜欢这篇文章,您可以通过点击我的推荐链接以每月仅需$5 的价格获得对成千上万更多文章的无限访问(如果您通过此链接注册,我将获得一定比例的费用,而您无需额外支付任何费用)。
[## 通过我的推荐链接加入 Medium - Graham Harrison
阅读 Graham Harrison 的每一个故事(以及 Medium 上的其他成千上万的作家)。增强您对数据的知识…
grahamharrison-86487.medium.com
访问我的数据科学网站 — 数据博客。
深入阅读
如果您想了解更多关于pgmpy库的信息,请参见完整文档:pgmpy.org/index.html。
如果您想了解更多关于因果推断的信息,这本惊人的书籍是一个很好的起点:
[## 《为何之书:因果关系的新科学》(Penguin Science)
购买《为何之书:因果关系的新科学》(Penguin Science)01 由 Pearl, Judea, Mackenzie, Dana (ISBN…
而这本书提供了对一些关键概念的更深入和详细的探讨:
购买《统计中的因果推断:入门》1 由 Pearl, Judea, Glymour, Madelyn, Jewell, Nicholas P. (ISBN…
请注意:这些是 Amazon 推荐链接,如果您进行购买,作者将获得一定比例的提成。
解锁因果推断和前门调整的力量:数据科学家的深入指南
对因果推断前门调整的全面解释,包括所有 Python 源代码的示例
·发表于 Towards Data Science ·11 分钟阅读·2023 年 2 月 14 日
--

图片由 Evelyn Paris 提供,来源于 Unsplash
目标
在这篇文章结束时,你将理解因果推断的前门调整的魔力,它可以计算事件对结果的影响,即使存在其他未测量或甚至未知的因素影响两个变量,并且你将能够访问所有 Python 代码。
我在互联网和许多书籍中搜寻,试图找到一个在 Python 中完全工作的前门公式示例,但一无所获,因此,除非有我遗漏的资源,否则你即将阅读的内容确实是独一无二的…
介绍
在一篇最近的文章中,我探讨了“后门”调整公式的力量,来计算事件对结果的真实影响,即使存在“混淆”两个因素的可观察因素…
## 解锁因果推断的力量:数据科学家理解“后门”调整的指南…
使用 Python 和 pgmpy 库的“后门”调整公式的完整示例,包括完整…
towardsdatascience.com
目的是确定服用药物对患者恢复率的真实效果,而后门调整公式的魔力恢复了这一效果,即使“男性”掩盖了这一结果,因为 -
-
男性服用药物的比例高于女性
-
男性的恢复率高于女性

图片作者
在这个例子中,“男性”是一个“混杂因素”,但“男性”的值已包含在观察数据中,然后应用了后门公式来证明药物试验有积极效果。
但是如果“混杂因素”无法测量且未包含在数据中会怎样?
现实世界的例子
在 1950 年代,科学家们强烈相信吸烟导致呼吸道疾病,与此同时烟草公司设法提供了相反的“证据”,这场统计战持续不断。
这一证据的实质是烟草公司提出的假设,即遗传因素对吸烟者和发展呼吸道疾病的可能性负责。这对烟草公司来说是一个方便的假设,因为几乎无法测试。
这是关于相关因素之间因果联系的提议…

图片作者
一个因果推断解决方案
如果这是你唯一的数据,即来自未观察到的混杂因素到事件和结果的简单后门路径,那么什么也做不了;真实效果无法恢复。
然而,还有其他“模式”可以恢复效果,包括前门标准和工具变量。本文将全面解释这些模式中的第一个。
要满足前门标准,需要在事件和结果之间有一个中介,在吸烟的例子中可能是这样的 -

图片作者
即吸烟导致焦油,焦油导致呼吸道疾病,而不是直接的因果联系。
当这种模式存在时,可以使用“前门调整公式”来隔离和恢复事件(吸烟)对结果(呼吸道疾病)的影响,不受未观察到的混杂因素的影响,如 Judea Pearl 在《为什么的书》和《统计中的因果推断》中所提议的那样。
购买《为什么的书:因果关系的新科学》(企鹅科学)01,作者:Judea Pearl, Dana Mackenzie (ISBN…
购买《Causal Inference in Statistics: A Primer 1》由 Pearl, Judea, Glymour, Madelyn, Jewell, Nicholas P.(ISBN…)
排除未观察混杂因素的影响似乎很神奇,实际效果也确实令人惊叹,但如果你按照本文其余部分的步骤,你将能够通过几行 Python 代码将这一惊人技术添加到你的数据科学工具包中!
入门
我们首先需要一些测试数据。我使用我的BinaryDataGenerator类创建了一个合成数据集。如果你想要完整的源代码,请访问这篇文章 -
如何在少于 10 行代码中生成任何因果推断项目的合成数据 [## 如何在少于 10 行代码中生成任何因果推断项目的合成数据
如果 99%的人接种疫苗,1%有反应,2%得病,你如何生成具有代表性的合成数据?
图片由作者提供
数据的总结分析如下 -
-
样本中有 800 人。
-
50%的样本人口是吸烟者(400/800)
-
95%的吸烟者有焦油沉积物(380/400)
-
5%的非吸烟者有焦油沉积物(20/400)
-
15%有焦油的吸烟者有呼吸系统疾病(47/380)
-
10%没有焦油的吸烟者有呼吸系统疾病(2/20)
-
95%有焦油的吸烟者有呼吸系统疾病(19/20)
-
90%没有焦油的非吸烟者有呼吸系统疾病(342/380)
初次尝试:使用 Pgmpy 库
在我关于后门标准的文章中,我首先展示了一个使用pgmpy的简单解决方案。
鉴于在那个示例中应用后门标准如此简单,按同样的方式应用前门标准应该非常直接。这是应该能做到的代码……
预期结果是 4.5%(稍后会详细讨论!)但pgmpy崩溃,显示ValueError: Maximum Likelihood Estimator works only for models with all observed variables. Found latent variables: set().
经过大量研究并与开发人员沟通后,我得出的结论是,当存在未观察到的混杂因素时,pgmpy在应用“do”操作符(即进行干预)时不起作用,并且pgmpy无法应用前门调整公式。
更糟的是,DoWhy库在这种情况下也不起作用。
DoWhy可以处理在计算“平均处理效果”(ATE)时的未观察混杂因素,但当“do”操作符用于模拟干预时,它与pgmpy一样失败。
ATE 应用于连续变量,因此我们可以问DoWhy一个问题,如“如果二氧化碳排放增加 1 亿吨,对全球温度的因果效应是什么?”DoWhy将产生一个结果。
然而,当对离散的二元数据应用“do”干预,例如“在样本中每个人都吸烟的情况下,呼吸道疾病的概率是多少?”时,pgmpy或DoWhy无法进行计算,并且到目前为止我没有找到其他可以的库。
我的后门文章从pgmpy实现转向提供数学示例,以展示pgmpy在幕后所做的工作。在这篇文章中,理解数学是前提,以便我们可以在 Python 中构建自己的前门调整公式实现……
第二次尝试:手工计算
目标是通过模拟以下内容来计算平均因果效应(ACE) -
-
回到过去,执行一个干预,强制每个人都吸烟。
-
再次执行相同的时间旅行技巧,这次强制每个人都戒烟。
-
从第一个结果中减去第二个结果。
使用“do”操作符在数学上表达,这一惊人的成就看起来像这样 -

由于我们知道数据中存在一个未观察到的混杂因素和一个前门路径,所以我们需要用 Judea Pearl 提出的前门调整公式替换 ACE 公式的每一侧……

让我们从 ACE 公式的左侧开始,用前门调整公式替换,并使用数据中存在的变量代替 x、y 和 z。为了保持整洁,以下缩写将被使用:S = 吸烟,R = 呼吸,T = 焦油……

t 可以取值 {0, 1},s 可以取值 {0, 1},因此我们现在需要展开如下……

… 内部的 ∑𝑠 项可以进一步展开如下……

现在只需替换数据中的条件概率即可。在下一节中,将提供一个 Python 函数来计算数据中的任何条件概率,但现在需要的值如下……

替换这些条件概率得到……

所以……

… 如果你重新计算上面所有的步骤,得到𝑃(𝑅=1∣𝑑𝑜(𝑆=0))的答案是……

因此,整体的平均因果效应(ACE)是……

手动计算平均因果效应是相当费力的!幸运的是,现在前门调整公式的工作原理已完全理解,将所有这些转换为 Python 相对简单,以便可以针对任何特征为离散值的数据集完全自动化整个过程…
第三次尝试:一个可重复使用的 Python 函数
第三次尝试涉及构建一个可重复使用的 Python 函数,该函数实现了前一节中的数学,适用于任何简单的 DAG 和任何DataFrame,以便在理解数学后可以将其搁置一边。
该函数的实现需要使用条件概率,并且需要一个简单的 Python 函数来从任何DataFrame计算这些概率。
我将 calc_cond_prob 函数的细节留在了本文之外,以便专注于前门调整,但你可以从这篇文章中阅读完整的解释并下载源代码…
## 如何在 3 行代码中从任何 DataFrame 计算条件概率
学习编写一个简单的 Python 函数,使用诸如 p(exam=1 |… 的符号来计算条件概率
towardsdatascience.com
一旦你下载了 calc_cond_prob,就可以像这样轻松地从任何 DataFrame 计算条件概率…
𝑝(𝑟𝑒𝑠𝑝𝑖𝑟𝑎𝑡𝑜𝑟𝑦=0∣𝑠𝑚𝑜𝑘𝑖𝑛𝑔=0,𝑡𝑎𝑟=0)=0.1
… 或者可以明确指定 outcome / result 和 events 如下…
𝑝(𝑟𝑒𝑠𝑝𝑖𝑟𝑎𝑡𝑜𝑟𝑦=0∣𝑠𝑚𝑜𝑘𝑖𝑛𝑔=0,𝑡𝑎𝑟=0)=0.1
前一节解释了 Pearlean 前门调整公式背后的数学,并提供了一个完整的示例。
根据这些构建块(以及 calc_cod_prob 函数),可以开发一个 Python 函数,该函数将计算前门调整公式,用于包含以下特征的任何 DataFrame -
-
X — 处理
-
Y — 结果
-
Z — 中介
这是前门调整的完整源代码…
… 并且可以像这样调用该函数…
结论
首先讨论问题,如果吸烟对呼吸系统疾病的平均概率仅增加了 4.5%,这不会说服许多吸烟者戒烟。
然而,我们看到给定吸烟的呼吸系统疾病的单独概率 𝑃(𝑟𝑒𝑠𝑝𝑖𝑟𝑎𝑡𝑜𝑟𝑦=1∣𝑑𝑜(𝑠𝑚𝑜𝑘𝑖𝑛𝑔=1))=54.75%。
平均因果效应之所以如此低,是因为我们的虚构烟草公司使用了卑劣的手段,通过确保大量有呼吸系统疾病的非吸烟者被纳入样本,以掩盖真相,即吸烟确实会导致呼吸系统疾病。
即使数据中有这些噪音,即使我们接受一个不太可能的假设,即存在一个无法测量的遗传因素,使事件和结果都受到混淆,前门调整公式的魔力仍然揭示了吸烟与呼吸系统疾病之间的正向因果关系!
这个惊人的结果与我在其他数据科学技术中发现的任何东西都不同,它涉及到我的机器学习预测的客户总是问的最常见的问题,即 -
-
为什么会发生这种情况?
-
我应该做什么来改变结果并改善情况?
这些类型的“为什么?”问题使得应用前门调整以计算“干预”效果所需的知识、能力和理解成为数据科学工具包中不可或缺的补充。
不幸的是,目前可用的库,包括 pgmpy 和 DoWhy,在对包含未观察到的混淆变量和前门路径的离散数据集应用“do”操作时无法正常工作。
这是这些库功能上的一个巨大缺陷,经过长时间的搜索,包括在线和书籍中的实际示例,我找不到任何解决方案。
除非我遗漏了一些示例,使得这篇文章独一无二,我希望在前门调整开始吸引我时能够阅读这篇文章,而不是自己做所有这些研究。
不过这非常有趣,我真的希望你喜欢这个结果!
附加部分
既然如此,pgmpy 在这种情况下不起作用,并且在我的学习过程中已经走了这么远,我决定用 Python 编写一个前门调整公式的版本,以纠正这一遗漏。
仅仅是为了说明,我决定重构公式,使得 Python 实现更简洁,改动了这个 …

改成这样 ..

… 这是数学上等效的,就像是在说 —
4 x 3 x 1 x 2 x 2 = 4 x 1 x 2 x 2 x 3
注:有关这一等效性的完整解释,请参见 Pearl、Glymour 和 Jewell 的《统计学中的因果推断》,第 68 页(3.15)和第 69 页(3.16)。
回到解决方案,第一步是使用 pgmpy 类创建因果模型。需要注意的是:未观察到的混淆变量必须从 edges 列表中删除,因为这会导致 BayesianNetwork.fit() 方法因 ValueError 崩溃 …
一旦设置完成,前门公式可以在 Python 中实现如下 …
只是为了证明它的有效性,计算结果与手动计算以及之前直接在 DataFrame 上工作的 Python 函数完全一致 …
联系并保持联系 …
如果你喜欢这篇文章,你可以通过点击我的推荐链接以每月仅需$5 成为 Medium 会员,享受对成千上万篇文章的无限访问(如果你通过此链接注册,我将获得费用的一部分,对你没有额外费用)。
[## 使用我的推荐链接加入 Medium — Graham Harrison
阅读 Graham Harrison 的每个故事(以及 Medium 上成千上万其他作家的作品)。提升你对数据的知识…
grahamharrison-86487.medium.com](https://grahamharrison-86487.medium.com/membership?source=post_page-----8e7b8ba33421--------------------------------)
… 或通过 … 连接
订阅我的免费电子邮件,以便每当我发布新故事时你都会收到通知。
访问我的数据科学网站 — 数据博客。
解锁使用 Python、特征存储和 GCS 的高效批量预测管道的秘密
完整堆栈 7 步 MLOps 框架
课程 3:批量预测管道。使用 Poetry 打包 Python 模块
·发表于 Towards Data Science ·阅读时间 15 分钟·2023 年 5 月 12 日
--

由 Hassan Pasha 拍摄,来自 Unsplash
本教程代表了一个包含 7 节课程中的第三部分,将逐步指导你如何设计、实施和部署 ML 系统,并运用MLOps 的良好实践。在课程中,你将构建一个生产就绪的模型,以预测来自丹麦的不同消费者类型在接下来的 24 小时内的能源消耗水平。
在本课程结束时,你将理解使用批量服务架构设计、编码和部署 ML 系统的所有基础知识。
本课程面向中级/高级机器学习工程师,他们希望通过构建自己的端到端项目来提升技能。
现在,证书无处不在。构建高级端到端项目,并在之后展示出来,是获得专业工程师认可的最佳方式。
目录:
-
课程介绍
-
课程内容
-
数据来源
-
课程 3:批量预测管道。使用 Poetry 打包 Python 模块。
-
课程 3:代码
-
结论
-
参考资料
课程介绍
在这 7 节课程结束时,你将学会:
-
设计一个批量服务架构
-
使用 Hopsworks 作为特征存储
-
设计一个从 API 读取数据的特征工程管道
-
构建一个带有超参数调优的训练管道
-
使用 W&B 作为 ML 平台来跟踪你的实验、模型和元数据
-
实现一个批量预测管道
-
使用 Poetry 构建你自己的 Python 包
-
部署你自己的私有 PyPi 服务器
-
使用 Airflow 协调一切
-
使用预测来编写一个使用 FastAPI 和 Streamlit 的 Web 应用
-
使用 Docker 对代码进行容器化
-
使用 Great Expectations 确保数据验证和完整性
-
随时间监控预测的性能
-
将所有内容部署到 GCP
-
使用 GitHub Actions 构建 CI/CD 管道
如果这些听起来很多,不用担心。完成这门课程后,你会理解我之前说的所有内容。最重要的是,你将知道为什么我使用了这些工具以及它们如何作为一个系统协同工作。
如果你想从这门课程中获得最大收益, 我建议你访问包含所有课程代码的 GitHub 仓库 。这门课程的设计目的是快速阅读和复现文章中的代码。
在课程结束时,你将知道如何实现下面的图示。不要担心如果有些地方不太明白。我会详细解释所有内容。

你将在课程中构建的架构图 [作者提供的图片]。
在第 3 课结束时,你将学会如何实现和集成批量预测管道以及使用 Poetry 打包所有 Python 模块。
课程内容:
-
批量预测管道。使用 Poetry 打包 Python 模块。
-
使用 GE 进行数据验证以确保质量和完整性。模型性能持续监控。
如果你想全面掌握本节内容,我们建议你查看我们的上一篇课程,其中讲述了设计一个使用特征存储和 ML 平台的训练管道:
第二部分:训练管道。ML 平台。超参数调整。
towardsdatascience.com
数据来源
我们使用了一个免费的开放 API,提供丹麦所有能源消费者类型的每小时能源消耗值[1]。
它们提供了一个直观的界面,您可以轻松查询和可视化数据。您可以在这里访问数据 [1]。
数据有 4 个主要属性:
-
小时 UTC: 观察到数据点的 UTC 日期时间。
-
价格区域: 丹麦被分为两个价格区域:DK1 和 DK2——由大贝尔特分隔。DK1 位于大贝尔特以西,DK2 位于大贝尔特以东。
-
消费者类型: 消费者类型是工业代码 DE35,由丹麦能源公司拥有和维护。
-
总消耗: 总电力消耗(以千瓦时为单位)
注意: 观察数据有 15 天的延迟!但对于我们的演示用例,这不是问题,因为我们可以模拟实时中的相同步骤。

我们的网页应用程序的截图,显示了我们如何预测区域=1 和消费者类型=212 的能源消耗[作者提供的图片]。
数据点具有每小时分辨率。例如:“2023–04–15 21:00Z”,“2023–04–15 20:00Z”,“2023–04–15 19:00Z”等。
我们将数据建模为多个时间序列。每个独特的价格区域和消费者类型元组代表其独特的时间序列。
因此,我们将构建一个模型,独立预测每个时间序列的未来 24 小时能源消耗。
查看下面的视频,以更好地了解数据的样子 👇
课程和数据来源概览[作者提供的视频]。
第三部分:批量预测管道。使用 Poetry 打包 Python 模块。
第三部分的目标
本课程将教你如何构建批量预测管道。此外,它还将展示如何使用 Poetry 将我们在第 1、第 2 和第三部分中完成的所有管道代码打包成 Python PyPi 模块。👇
注意: 在下一节中,我们将把这些 Python 模块上传到我们自己的私有 PyPi 服务器,并从 Airflow 中安装它们。

最终架构图,其中第 3 课的组件以蓝色突出显示 [作者提供的图片]。
如果你记得第 1 课,模型可以通过以下方式进行部署:
-
批处理模式
-
请求-响应(例如,RESTful API 或 gRPC)
-
流式模式
-
嵌入式
本课程将以批处理模式部署模型。
我们将讨论在构建 web 应用程序时如何从批处理过渡到其他方法。你会发现这非常自然。
但是,如果你渴望比较批处理模式与请求-响应服务模式,可以查看我写的 5 分钟文章,解释如何使用请求-响应方法服务模型。
批处理模式下部署模型的主要步骤是什么,也就是构建批量预测管道的步骤?
步骤 1: 你将从特征库中以批处理模式加载特征。
步骤 2: 你将从模型注册表中加载训练好的模型(在我们的例子中,我们使用 Hopsworks 作为模型注册表)。
步骤 3: 你将预测未来 24 小时的能源消耗水平。
步骤 4: 你将把预测结果保存到 GCP 存储桶中。
之后,各种消费者将从 GCP 存储桶中读取预测并相应使用它们。在我们的案例中,我们使用 FastAPI 和 Streamlit 实现了一个仪表板。
通常,你的初始部署策略将是批处理模式。
为什么?
因为这样做,你不必关注延迟和吞吐量等限制。通过将预测保存到某些存储中,你可以快速使模型上线。
因此,批处理模式是部署模型的最简单和最快的方式,同时保持应用程序最终用户的良好体验。
当应用程序可以实时访问预测时,模型是在线的。
请注意,预测不是实时生成的,仅在实时访问(例如,从存储读取)中进行。
使用此方法的最大缺点是你的预测将有一定的滞后。例如,在我们的用例中,你生成并保存未来 24 小时的预测。假设经过 2 小时没有新的预测,现在你只有未来 22 小时的预测。
当你必须存储的预测数量是合理的时,你可以通过频繁生成预测来绕过这个问题。在我们的示例中,我们将每小时生成预测——我们的数据分辨率为 1 小时。因此,我们通过不断生成和存储新预测来解决延迟问题。
但这里出现了批量预测策略的第二个问题。假设预测集合很大。例如,你想预测 100 万用户的推荐,而数据库中有 1 亿个项目。那么,频繁计算预测将是非常昂贵的。
然后,你必须强烈考虑使用其他服务方法。
但这里有一个陷阱。
你的应用程序可能不会一开始就有 100 万用户和 1 亿条数据。这意味着你可以安全地从批处理模式架构开始,并在有必要时逐步转向其他方法。
这就是大多数人做的事情!
要了解如何转向其他方法,请查看这篇文章,了解Google Cloud 建议的 标准化 ML 架构。
理论概念与工具
GCS: GCS 代表 Google Cloud Storage,是 Google 在 GCP 中的存储解决方案。如果你更熟悉 AWS S3,它类似于此。
你可以向 GCS 写入任何文件。在我们的课程中,我们将 Pandas DataFrames 写入 parquet 文件。
GCS 与 Redis: 我们选择将预测结果写入 GCS 主要有 4 个原因:
-
易于设置
-
无需维护
-
访问免费层
-
我们还将使用 GCP 来部署代码。
Redis 是缓存预测结果以便后续由各种客户端访问的热门选择。
为什么?
由于你可以以低延迟访问数据,从而改善用户体验。
本来这是一个不错的选择,但我们想要简化事情。
此外,将预测结果写入 GCS 以便长期存储,并在 Redis 中缓存以供实时访问也是一种良好的做法。
Poetry: Poetry 是我最喜欢的 Python 虚拟环境管理工具。它类似于 Conda、venv 和 Pipenv。依我看,它更优,因为:
-
它提供了一个.lock文件,反映了所有子依赖项的版本。因此,复制代码非常简单和安全。
-
你可以直接使用 Poetry 快速构建你的模块。无需其他设置。
-
你可以使用 Poetry 快速将模块部署到 PiPy 服务器。无需其他设置,还有更多……
第 3 课:代码
注意: 所有安装说明都在仓库的 README 文件中。这里我们将直接跳到代码。
第 3 课中的所有代码都位于 batch-prediction-pipeline文件夹中。
batch-prediction-pipeline 文件夹下的文件结构如下:

显示批量预测管道文件夹结构的屏幕截图 [作者提供的图片]。
所有代码都位于 batch_prediction_pipeline 目录下(注意“_”而不是“-”)。
直接在你的 git 仓库中存储凭据是一个巨大的安全风险。这就是为什么你将使用.env文件来注入敏感信息。
.env.default是你必须配置的所有变量的示例。它还可以帮助存储那些不敏感的属性的默认值(例如,项目名称)。

.env.default 文件的截图[图片由作者提供]。
准备凭据
首先,你需要创建一个.env文件,在其中添加我们所有的凭据。
我已经在第 1 课中向你展示了如何设置你的.env文件。同时,我在第 1 课中解释了如何将.env文件中的变量从ML_PIPELINE_ROOT_DIR目录加载到SETTINGS Python 字典中,以便在代码中使用。
因此,如果你想复制我所做的,我强烈建议查看第 1 课。
如果你只是想轻松阅读,可以完全跳过“准备凭据”步骤。
在第 3 课中,你将使用两个服务:
Hopsworks (免费)
我们已经在第 1 课中向你展示了如何设置Hopsworks的凭据。请访问第 1 课中的“准备凭据”部分,在那里我们详细展示了如何设置 Hopsworks 的 API KEY。
GCP — Cloud Storage (免费)
在复制本课程时,你将坚持使用GCP — Cloud Storage 免费层。你可以在 GCP — Cloud Storage 中免费存储最多 5GB,这对我们的使用情况绰绰有余。
这个配置步骤会稍长一些,但我保证它并不复杂。顺便说一下,你将学习使用像 GCP 这样的云服务提供商的基础知识。
首先,访问 GCP 并创建一个名为“energy_consumption”(或其他任意名称)的项目。 随后,前往 GCP 项目的“Cloud Storage”部分,创建一个名为“hourly-batch-predictions”的非公开存储桶。** 选择任何区域,但请注意这一点—创建 GCP 存储桶的官方文档 [2]。
注意: 由于平台规则的不断变化,你可能需要选择不同的名称。这不是问题,只需根据你的需求命名,并在.env文件中进行更改:GOOGLE_CLOUD_PROJECT(我们的“energy_consumption”)和GOOGLE_CLOUD_BUCKET_NAME(我们的“hourly-batch-predictions”)。

GCP — Cloud Storage 视图的截图,你需要在其中创建你的桶 [图片由作者提供]。
现在你已经完成了所有 GCP 资源的创建。最后一步是创建一种方式,以便通过你的 Python 代码直接访问 GCP 桶的读写权限。
你可以通过 GCP 服务账户轻松做到这一点。我不想把整篇文章都挤占于 GCP 配置。因此,这份 GCP 官方文档展示了如何创建服务账户 [3]。
创建服务账户时,请注意一件事!
服务账户具有不同的角色。角色是一种配置服务账户权限的方式。
因此,你需要配置你的服务账户以拥有对你的“hourly-batch-predictions”桶的读写访问权限。
你可以通过在创建服务账户时选择“Storage Object Admin”角色来轻松做到这一点。
最后一步是找到一种方法,在你的 Python 代码中使用新创建的服务账户进行身份验证。
你可以通过访问你的服务账户并创建一个 JSON 密钥来轻松做到这一点。再次,这里是官方 GCP 文档,它会告诉你如何为你的服务账户创建 JSON 密钥 [4]。
再次,请记住一件事!
创建 JSON 密钥时,你将下载一个 JSON 文件。
下载 JSON 文件后,将其放在安全的地方,并转到你的.env文件。在那里,将GOOGLE_CLOUD_SERVICE_ACCOUNT_JSON_PATH的值更改为 JSON 文件的绝对路径。

.env.default 文件的截图 [图片由作者提供]。
注意: 记得将GOOGLE_CLOUD_PROJECT和GOOGLE_CLOUD_BUCKET_NAME变量更改为你的名称。
恭喜!你已经完成了 GCS — Cloud Storage 的配置。
现在你已经创建了一个 GCP 项目和桶。此外,你可以通过服务账户使用你的 Python 代码进行读写访问。你使用 JSON 文件的帮助登录到服务账户。
如果有什么问题,请在下面的评论中告诉我,或直接在LinkedIn上联系我。
批量预测管道 — 主功能
如你所见,主功能遵循批量预测管道的四个步骤:
-
从特征存储中以批处理模式加载数据。
-
从模型注册表中加载模型。
-
进行预测。
-
将预测结果保存到 GCS 桶中。
大部分功能都是日志行 😆
在这四个主要步骤中,你必须从之前步骤生成的元数据中加载所有参数,例如feature_view_version和model_version。
另外,你需要获得对 Hopsworks 特征存储的引用。
然后,你直接进入我们将在后面的教程中详细介绍的四个主要步骤 👇
步骤 1:以批处理模式从特征存储加载数据
这一步类似于我们在第 2 课中加载训练数据时所做的。
但这一次,我们不是从训练数据集中下载数据,而是直接请求一个日期时间范围内的数据批次,使用get_batch_data()方法。
这样做允许我们时间旅行到所需的日期时间范围,并请求所需的特征。这种方法使批量推理变得非常简单。
最后一步是按照sktime的预期准备 DataFrame 的索引,并将其分割为 X 和 y。
注意: 这是一个自回归过程:我们从过去的 y 值中学习以预测未来的 y 值(y = 能源消耗水平)。因此,我们将只使用 X 作为模型的输入。我们将仅将 y 用于可视化目的。
第二步:从模型注册表加载模型
从 Hopsworks 模型注册表加载模型非常简单。
下面的函数有一个参数是对 Hopsworks 项目的引用和我们要下载的模型版本。
使用这两个变量,你可以获得对模型注册表的引用。之后,通过模型的名称,你可以获得对模型本身的引用。在这种情况下,它是best_model。
最后,你下载工件/模型并将其加载到内存中。
这里的诀窍是你的模型是有版本控制的。因此,你总是知道你使用的是哪个模型。
注意: 我们使用在第 2 课中解释的训练管道上传了best_model到模型注册表。训练管道还提供了一个包含最新 model_version 的元数据字典。
第三步:预测未来 24 小时的能源消耗水平
Sktime使预测变得极其简单。下面代码片段中的关键行是“predictions = model.predict(X=X_forecast)”,它预测了未来 24 小时的能源消耗值。
模型训练时设置了 24 小时的预测时间范围。因此,它已经知道未来要预测多少个数据点。
另外,你需要准备外生变量X_forecast。在时间序列预测中,外生变量是你已经知道未来会发生的特征。例如,节假日。因此,基于你的训练数据 X,其中包含所有区域和消费者类型 ID,你可以通过将日期时间范围映射到预测范围来生成X_forecast变量。
第四步:将预测结果保存到存储桶
最后一个组件是将所有内容保存到 GCP 存储桶的函数。
这一步相对简单,难点在于配置你的存储桶和访问凭证。
我们获取对存储桶的引用,遍历 X、y 和预测,将它们作为 blob 写入存储桶。
注意: 除了预测结果外,我们还保存 X 和 y,以便将所有内容集中在一个地方,方便快速访问所有所需内容,并在网页应用中漂亮地呈现它们。
要获取桶的引用,你必须访问在教程开始时配置的设置。
如你所见,你创建了一个 GCS 客户端,并指定了项目名称和 JSON 凭据文件路径。之后,你可以快速获取你指定桶的引用。
将 blob 写入桶与写入普通文件非常相似。
你获取你要写入的 blob 的引用,并使用"with blob.open("wb") as f"打开资源。
注意你以二进制格式打开了 blob。
你将数据以 parquet 格式写入,因为它在存储大小和写入&读取性能之间是一个很好的折衷。
使用 Poetry 打包 Python 模块
Poetry使构建过程变得极其简单。
第一步显而易见,就是使用 Poetry 作为你的虚拟环境管理器。这意味着你已经拥有了"pyproject.toml"和"poetry.lock"文件——我们已经为你提供了这些文件。
现在,你只需进入与你的 Poetry 文件(上述文件)同一层级的项目目录(例如,进入你的batch-prediction-pipeline目录)并运行:
poetry build
这将创建一个包含你的包的dist文件夹作为wheel。现在你可以直接使用 wheel 文件安装你的包,或将其部署到 PyPi 服务器上。
要进行部署,请使用以下配置你的 PyPi 服务器凭据:
poetry config repositories.<my-pypi-server> <pypi server URL>
poetry config http-basic.<my-pypi-server> <username> <password>
最后,使用以下命令进行部署:
poetry publish -r <my-pypi-server>
就这样。我对 Poetry 如何简化这个过程感到惊讶。
否则,构建和部署你的 Python 包是一个繁琐且漫长的过程。
在第 4 课中,你将部署你的私人 PyPi 服务器,并使用我在上面展示的命令部署到目前为止你编写的所有代码。
结论
恭喜你!你完成了Full Stack 7-Steps MLOps Framework课程的第三课。
如果你已经走到这一步,你知道如何:
-
选择正确的架构
-
以批量模式从特征存储中访问数据
-
从模型注册中心下载你的模型
-
构建推理管道
-
将你的预测结果保存到 GCS
现在你了解了使用和实现批量预测架构的强大功能,你可以快速实时提供模型,同时为其他更高级的服务方法铺平道路。
查看第 4 课,了解如何托管你自己的私人 PyPi 服务器以及如何使用 Airflow 编排所有管道。
另外, 你可以在这里访问 GitHub 仓库。
💡 我的目标是帮助机器学习工程师在设计和生产化机器学习系统方面提升水平。关注我在 LinkedIn 或订阅我的 每周通讯以获取更多见解!
🔥 如果你喜欢阅读这样的文章并希望支持我的写作,可以考虑 成为 Medium 会员。通过使用 我的推荐链接,你可以在没有额外成本的情况下支持我,同时享受 Medium 丰富故事的无限制访问权限。
[## 使用我的推荐链接加入 Medium - Paul Iusztin]
🤖 加入以获取有关设计和构建生产就绪的机器学习系统的独家内容 🚀 解锁完整访问权限…
pauliusztin.medium.com](https://pauliusztin.medium.com/membership?source=post_page-----17a1462ca489--------------------------------)
参考资料
[1] 丹麦 API 的 DE35 行业代码能源消耗,丹麦能源数据服务
[2] 创建存储桶,GCP Cloud Storage 文档
[3] 创建服务账户,GCP IAM 文档
[4] 创建和删除服务账户密钥,GCP IAM 文档
如何在 3 行代码中从任何 DataFrame 计算条件概率
学会编写一个简单的 Python 函数,该函数将使用诸如 p(exam=1 | study=1) 的符号来计算条件概率。
·发表于 Towards Data Science ·阅读时间 6 分钟·2023 年 2 月 10 日
--

图片由 Dan Cristian Pădureț 提供,来源于 Unsplash
背景
随着我继续深入探讨因果推断,我达到了一个阶段,在这个阶段我需要构建使用复杂条件概率组合的公式,而代码变得难以阅读和维护。这促使我开发了一种简单的计算条件概率的方法。
在本文结束时,你将掌握一个简短的 Python 函数,该函数可以将条件概率直接应用于任何 pandas
DataFrame并返回结果。
回顾
条件概率是指在另一个事件已经发生的情况下,某个事件发生的概率。符号 𝑃(𝐴∣𝐵) 可以理解为事件 A 的概率(P),给定(|)事件 B 已经发生。
向前迈进一步,符号 𝑃(𝐴∣𝐵,𝐶) 意味着在事件 B 和事件 C 都已经发生的情况下,事件 A 的概率。
示例
我们都知道,如果公平的硬币被抛掷两次,得到两个正面的概率是 0.25,因为第一次事件(抛掷第一次硬币并得到正面)的概率是 0.5 乘以第二次事件(抛掷第二次硬币并得到正面)的概率,也是 0.5,即 0.5 x 0.5 = 0.25。
当两个事件完全独立时,简单的乘法法则适用,即第一个硬币抛掷的结果不会影响第二个事件,因此在执行这两个事件之前,两个正面朝上的概率是 0.25。
然而,并不是所有的事件都是独立的,这最好通过一个例子来解释 …
这是一个合成数据的表格,代表了一个虚构的 800 人样本,其中是否学习和是否通过考试都被观察和记录了 …

作者提供的图像
通过考试(exam=1)的概率如下 -

那么如果我们问以下问题会怎样:
其中一个测试对象被随机选中,结果发现他们学习了(study=1)。他们通过考试(exam=1)的概率是多少?
这些概率不是像抛硬币那样独立的,因为我们已经知道所选择的对象学习了。这个问题的答案可以表示如下 -

即,给定 study = 1,exam = 1 的概率是多少?
有两种方法可以计算这个结果:直观方法和概率方法。
直观方法
通过查看汇总表中 study = 1 的行,很容易看到有 400 个学习者(341 + 59) -

作者提供的图像
如果我们已经选择了一个学习者,他们必须来自这个样本中的这一部分。在这 400 人中,很容易看出 59 人通过了考试,因此 …

数学方法
计算条件概率的公式如下 -

注 - 符号 P(A|B)可以读作 - 条件概率或事件 A 在事件 B“给定”的情况下。
我个人发现这个方法不如第一个解决方案直观,因为我可以很容易地在DataFrame中可视化发生的情况,但这种方法确实有一个大优点。如果你只能访问概率而无法访问所有底层数据,那么“直观方法”就不适用了。
请考虑以下内容,这与汇总表相同,但使用的是概率而不是计数 …

作者提供的图像
现在 p(exam=1 | study=1)可以按如下方式计算 -

Python 函数解决方案
鉴于条件概率的解释,现在的目标是开发一个 Python 函数,可以消除所有的麻烦,并为任何 pandas DataFrame进行计算。
我们首先需要一些测试数据。我使用我的BinaryDataGenerator类创建了一个合成数据集。如果你想要完整的源代码,请访问这篇文章 …
## 如何在少于 10 行代码中生成任何因果推断项目的合成数据
如果 99% 的人接种了疫苗,1% 的人出现反应,2% 的人得病,你如何生成具有代表性的合成数据?
towardsdatascience.com 
图片来源:作者
版本 1:基本函数
使用承诺的“三行代码”实现的最基本版本的函数如下 -
0.1475
版本 2:基本函数加 LaTeX 输出
然而,我很快就厌倦了所有的 print 语句来显示结果,这导致了第二个版本,它可以选择性地输出计算的 LaTeX 表示...

版本 3:高级函数
到目前为止,我真的希望有一个最终版本,其中可以直接说 P(exam=1 | study=1) 并获得结果,但我也希望保留 outcome 和 events 作为可选参数,以便我可以选择使用哪种方法...

结论
条件概率在各种统计和机器学习解决方案中非常有用,但从 pandas DataFrame 中计算条件概率没有简单、可读和可重复的方法。
本文解释了什么是条件概率,给出了一个示例,并提供了一个 Python 函数的所有源代码,该函数可以接受任何 DataFrame 并计算和显示结果。
如果你喜欢这篇文章,你可以通过成为 Medium 会员,每月只需 $5 即可无限访问更多内容,通过 点击我的推荐链接(如果你通过此链接注册,我将获得一定比例的费用,且对你没有额外费用)。
[## 通过我的推荐链接加入 Medium - Graham Harrison
阅读 Graham Harrison 的每一个故事(以及 Medium 上的其他成千上万位作者的故事)。提升你的数据知识…
grahamharrison-86487.medium.com](https://grahamharrison-86487.medium.com/membership?source=post_page-----a232ca71e33c--------------------------------)
访问我的数据科学网站 — 数据博客。
解锁因果推断的秘密,通过有向无环图的硕士班
从基础到更高级方面的有向无环图的逐步解释
·发表于 Towards Data Science ·阅读时长 36 分钟·2023 年 4 月 6 日
--

Caleb Jones拍摄的照片,来自Unsplash
目标
在花费大量时间研究因果推断之后,我开始意识到自己对有向无环图(DAGs)的理解不够全面,这影响了我将其应用于解决实际问题的能力。
本文的目标是记录我的学习历程,并分享你需要了解的关于 DAG 的一切,以便将你对因果推断的理解提升到一个新的水平。
背景
我想从提出因果推断的定义开始—
因果推断是推理的过程,是应用从变量之间的因果关系中得出的结论,同时考虑潜在的混杂因素和偏差。
这确实很复杂,但它概括了关键点—
-
这是一种因果关系的研究。
-
关键在于得出可以应用于解决实际问题的结论。
-
任何偏差或“混杂”都必须考虑在内并进行补偿。
超越定义,有一句古老的格言是“相关性不等于因果性”,这引出了“那究竟是什么?”的问题
事实证明,因果关系不能仅从一组数据中推断或计算出来。数据需要扩展和补充额外的信息,以提出、可视化和表示因果关系,一种常见的方法是使用“有向无环图”。
一个简单的 DAG
从最基本的层面上看,DAG 确实非常简单。下面的例子表示服用药物“D”和恢复“R”之间的拟议关系,箭头表明服用药物对恢复有因果影响……

作者提供的图片
这个 DAG 说明了两个关键术语——“treatment”(处理)和“outcome”(结果)。
-
“treatment”(在这个例子中是药物)指的是被研究或操控的行动或干预,以确定其对结果的影响。
-
“outcome”(在这个例子中是恢复)指的是用来测量处理效果的变量。
在传统机器学习术语中,处理是自变量,结果是因变量。
当我第一次研究 DAG 时,我对术语感到困惑,因为“treatment”(处理)和“outcome”(结果)通常是医学术语,我曾怀疑 DAG 和因果推断是否仅限于医学领域。事实并非如此;因果推断可以应用于任何领域中的变量集合,我怀疑医学听起来的术语被借用是因为药物和治疗试验与因果推断有显著的重叠。
一个带有“混杂因素”的 DAG

作者提供的图片
这个例子增加了另一个因素——“G”或性别。箭头显示性别(G)对药物(D)和恢复(R)都有因果影响。解释如下——
-
更多的男性比女性决定服用药物,因此“性别”导致“药物”。
-
女性的自然恢复率比男性更高,因此“性别”导致“恢复”。
这使事情变得复杂。目标是确定服用药物对恢复的真实效果,但性别影响两者,因此仅仅观察试验中有多少人服用了药物并恢复,并不能提供准确的答案。
这种混合效应称为“混杂”,造成这种效应的变量称为“混杂因素”,必须“去混杂”以确定药物的真实效果……
随机对照试验、分层、条件控制、控制变量和协变量
为了计算 D 对 R 的真实效果,我们需要隔离并去除 G 的影响。有几种方法可以应用,包括以下几种……
随机对照试验
如果这是一个正在规划的未来试验,一种经过验证的方法是创建一个“随机对照试验”(RCT)。这将涉及将药物试验对象随机分配到一个接受药物的实验组和一个接受安慰剂的对照组中。(注意:重要的是不要告诉受试者他们被分配到哪个组)。
由于受试者被随机分配,现在不可能是性别在造成或影响谁接受药物。这有效地“抹去了” “G”和“D”之间的因果关系,这意味着药物对恢复的任何观察到的效果现在将与混杂效应无关 -

作者提供的图片
然而,如果研究基于历史试验,数据已经被记录,那么随机分配测试组和对照组就已经为时已晚,不能使用 RCT。
RCT 的另一个大挑战是,如果所研究的“治疗”是吸烟或肥胖,就不能随机分配到吸烟组或肥胖组,因此显然存在限制 RCT 适用性的道德和伦理界限。
幸运的是,还有其他方法可以应用于历史的观察数据,以缓解这些挑战,包括“分层”…
分层
在我们的例子中,如果受试者是 60%男性和 40%女性,则药物“D”对恢复“R”的影响可以如下隔离和计算 -
-
计算男性的恢复情况,并乘以 0.6(因为 60%是男性)。
-
计算女性的恢复情况,并乘以 0.4(因为 40%是女性)。
-
将这两个数字加在一起,这显示了药物对恢复的影响,与性别的影响无关。
不幸的是,如果有大量变量,分层可能变得非常复杂。例如,如果有 10 个变量,每个变量有 10 个可能的值,我们将已经达到 100 个层次,而如果一个或多个变量是连续的,排列将很快变得压倒性。
条件化
使用因果推断技术可以模拟现实世界中的随机对照试验对历史和观察数据的影响。
这听起来像魔法,但它使用了多年来由包括犹太·佩尔在内的专家们建立、定义和描述的成熟数学技术,佩尔在学术期刊和书籍中发表了他的发现,包括以下内容 -
-
《为何之书》
-
统计中的因果推断
这种 RCT 的“模拟”是通过应用所谓的反向调整来条件于所有的混杂因素即影响处理和结果的变量来实现的。
这是针对一个单一处理变量(X)和一个单一结果变量(Y),以及一个单一混杂因素(Z)的反向调整公式 -

作者提供的图片
关键点是,左侧描述了一个“干预”,例如“让每个人都服用药物”(do(X)),这在右侧被重新编写并用纯粹的观察数据来表示。
数学的详细解释超出了本文的范围,但如果你想查看一个完全的示例,请查看这个链接……
一个使用 Python 和 pgmpy 库的完整回门调整公式示例,包括完整的……
towardsdatascience.com
控制
控制是一种统计技术,指的是保持一个变量的值不变,以免它对试验结果产生不利影响。
例如,在一个研究维生素 D 对警觉性影响的试验中,试验设计者可能会决定给所有参与者提供关于饮食、进食时间、咖啡因摄入、屏幕时间、运动和酒精摄入的具体指示,以试图隔离真实效果。
目前文献中似乎存在一些健康的分歧。统计学家通常似乎会控制他们能控制的所有因素,即使在随机对照试验中也是如此。
因果推断的支持者,尤其是 Judea Pearl,似乎主张只控制(或条件化)那些具体干扰治疗效果对结果影响的变量。
我确实同意 Pearl 的观点,即在实验中控制所有因素可能并非必要,甚至可能对结果产生意想不到的负面影响。我的理由是,在随机对照试验中,诸如咖啡因摄入等外部因素由于参与者的随机选择而在各组内外会趋于平均化,我们又怎么能确定参与者确实遵循了指示呢?
此外,因果推断理论确实强烈主张识别一个最佳变量集进行条件设置,并相应地限制调整(更多内容稍后讨论)。
协变量
协变量的一个定义是影响响应变量但在研究中不感兴趣的变量。请考虑以下 DAG……

图片由作者提供
H 代表学习时间,是处理因素,E 代表考试成绩(结果),这是唯一感兴趣的变量。
然而,一位领域专家指出,先前的学生能力也会影响考试成绩,尽管它与研究无关,他们还是将其引入模型中。
在这种情况下,G 满足协变量的定义。
我个人观察到,协变量似乎是一个在统计和观察分析中使用的概念,因此受到统计学家的青睐,而因果推断的支持者可能倾向于认为,除非 G 是 H 和 E 的混杂因素,否则不需要将其引入模型中。
请注意,我包含了协变量的解释以求完整,因为在文献中经常提到协变量,并且理解协变量与混杂因素之间的区别是很重要的。
回顾
到目前为止,我们已经定义了因果推断,探索了 DAG 的目的,并解释了混杂因素、RCTs、分层、条件和控制。
RCTs、分层和控制都是与实际试验相关的统计技术。
条件化是一种因果推断技术,可以应用于历史性观察数据,以得出关于治疗对结果的影响的结论,即使原始调查的参与者并非随机选择。
这立即打开了机会之门,因为现在有很多历史性的观察数据集可以用来探索因果关系,这只是因果推断和有向无环图众多好处之一。
掌握了这些理解和术语后,我们现在准备探索 DAGs 的更复杂方面,这些方面在现有的书籍和在线文章中通常没有明确解释……
路径
路径是一个难以掌握的主题,因为这些概念和技术相互关联,使得学习的顺序变得困难。在下面的部分中,我将从高层次介绍路径,然后深入探讨分叉、链条和碰撞体的模式。
这引出了条件、阻断和解阻的讨论,只有在学习如何对整个 DAG 进行条件设置以消除混杂效应并得出因果结论之后,这些概念才有意义。
让我们从考虑以下有向无环图(DAG)开始……

图片由作者提供
这比药物、性别、恢复的例子要复杂,但一旦关键概念路径被解读出来,就会变得相当简单。我在文献中未能找到“路径”的明确定义,所以这是我的定义……
路径是一系列因果链路,在因果图中连接了处理和结果。
因此,在上述 DAG 中,一条单独的路径是 𝑋←𝑍1→𝑍3→𝑌。
起初这可能不直观,因为我们习惯于将自然路径从左到右流动地思考,比如 𝐴→𝐵→𝐶,但我希望你能暂时抛开这种直观的看法。在因果图中,箭头的方向并不重要,任何从 X 到 Y 的边的集合都构成了一条路径。
一旦完全理解了这一点,DAG 与扁平形式如 𝐷←𝐺→𝑅 的区别就会变得清晰。
有向无环图是描述一组因果关系的可视化,它包含多个路径。每当现有文献检查扁平形式时,它是在查看必须与其他路径一起存在以描述所有因果关系的单一路径。
为了说明这个解释,以下是示例 DAG 中所有“扁平化”路径的列表……
-
𝑋←𝑍1→𝑍3→𝑌
-
𝑋←𝑍1→𝑍3←𝑍2→𝑌
-
𝑋←𝑍3→𝑌
-
𝑋←𝑍3←𝑍2→𝑌
-
𝑋→𝑊←𝑌
相对于扁平视图的另一种可视化方法是突出显示 DAG 中每条路径的单独副本,如下例所示……

图片由作者提供
一旦这些关键点被解读,路径就不难理解。我常常想,现有书籍和文章的作者是否在他们的领域如此专家,以至于忽略了从初学者角度建立理解的感觉。
这样可视化 DAG 和路径确实帮助我建立了对下一阶段至关重要的理解……
交汇点、分叉、链条和碰撞器
在任何“扁平化”的路径中,将会有一系列表示感兴趣变量的节点,每个节点通过单向箭头或边连接到下一个节点,例如 𝐴→𝐵→𝐶。
任何有两个箭头的节点必须位于路径的中间而不是两端,这称为交汇点。
交汇点只能是三种模式之一——分叉、链条或碰撞器,这些将在下面的部分中探讨和解释。
分叉
分叉是熟悉的,因为药物(D)、康复(R)和性别(G)之间的关系遵循这一模式。以下是一个在 DAG 中可视化的示例以及一个扁平路径的示例……

图片由作者提供
- 𝑆←𝐴→𝑅
这个示例表示了儿童年龄(A)、鞋码(S)和阅读能力(R)之间的因果关系。它被称为“分叉”,因为路径从中间变量 A 处分叉成两个不同的方向。
因果推断的规则认为 S 到 R 通过 A 的路径是开放的,即关于 S 的信息可以通过混杂因子 A 传递到 R,除非我们在 A 上“条件化”,否则路径是封闭的。
这个规则让我困惑了很长时间,但通过查看一个实际的例子和一些数据,这个问题很容易解释。
以下合成数据集包含了 1000 个 S、A 和 R 的数据点……

图片由作者提供
合成数据集反映了 DAG 中描述的因果关系,因此我们可以预期年龄与鞋码之间以及年龄与阅读能力之间的相关性。以下是两个可视化这些关联的散点图……

图片由作者提供
确实,它们的相关性很强,r 值很高。但当我们查看鞋码与阅读能力之间的相关性时,会发生什么呢?虽然没有因果关系,但我们仍会看到相关性,因为年龄对这两个变量产生了混淆效应。
这显然使得可以从年龄预测阅读能力,在传统的预测机器学习算法中这可能有效,但没有因果关系,因为买大号鞋子不会让任何人更聪明,反之亦然。
下一步是理解在年龄上加条件会发生什么。加条件(和控制)实际上是将我们加条件的变量保持或冻结在一个单一值或狭窄范围或层次上。不同之处在于,加条件通过数学公式实现效果,而控制是在实际试验中隔离一个感兴趣的组。
因此,鞋码与年龄之间的关联可以通过考虑所有年龄段的值来进行无条件检查,或者通过选择一个单一年龄值来进行加条件检查,这可以很容易地可视化如下……

图片来源:作者
第一张图没有条件限制(即显示所有年龄段),我们可以看到 S 和 R 之间有很强的相关性。
第二张图的年龄条件限制在 8 岁儿童上。显然,在单一年龄段内,鞋码与阅读能力之间没有相关性,回归线平坦,r 值很低。
在未加条件的图表中,可以使用回归线选择一个鞋码并读取来预测阅读能力,这就是未阻塞一词所指的。
在加条件的图表中,无法使用回归线进行预测,因为该线是水平的,这就是阻塞一词所指的。
在分叉的中间节点上加条件会阻塞路径,而不加条件则保持路径开放。
在图表中添加了一个附加特性来帮助理解这种效应。在未加条件的图表中,单独的年龄已被着色,以便可以区分,并且每个年龄的回归线以浅灰色添加。
考虑到这些视觉特征,立即显而易见,对于任何单一年龄段,回归线是平坦的,相关性较低,但当考虑所有年龄段时,综合相关性较高。
这种现象被称为辛普森悖论,这是文献中使用的另一个术语,难以用语言解释,但用图示理解很容易。
链
链是两个节点之间通过中介的最简单模式。第一个变量对中介有因果影响,中介对最终变量有因果影响。
请注意,交汇点中的中间节点被称为中介节点,但链中的中间节点也可以称为中介节点,因为它调解了源节点(第一个)和目标节点(第三个)之间的变化。
这是一个链的示例,其中 D 代表药物使用,B 代表患者血压,R 代表患者恢复。因果箭头表示药物导致血压变化,而血压变化又导致恢复……

图片由作者提供
- 𝐷→𝐵→𝑅
对链进行条件限制的效果与对分叉进行条件限制完全相同,即条件限制会阻断路径,而不进行条件限制则会使路径畅通。
通过可视化示例来理解这一违反直觉的说法要容易得多,因此让我们从一个合成数据集开始,表示 DAG 中的因果关系……

图片由作者提供
如果我们查看药物与血压以及血压与恢复之间的散点图,我们会期待看到一种相关性……

图片由作者提供
第一个散点图显示,药物使用量越多,血压越低;第二个图则显示血压越低,恢复率越高,因此合成数据准确地表示了 DAG 中的因果关系。
现在让我们来看一下对中间变量(血压)进行条件限制的影响……

图片由作者提供
在没有条件限制的情况下,D 与 R 之间有很强的相关性,而当我们对 B 的单一值(或在连续变量的情况下,对一个狭窄的层次)进行条件限制时,相关性较低。
散点图显示,若在链中的中间节点不进行条件限制,则路径不会被阻断,而对其进行条件限制则会阻断路径。
如果这仍然有些违反直觉,可以考虑一下对特定血压值(例如 6.5)进行条件限制的情况,这意味着 B 被固定且不能改变。这就意味着 D 和 R 的变化不会很大,D 和 R 的任何变化都是由于数据中的误差项而非任何因果效应。
解决违反直觉的最终方法是查看未进行条件限制的散点图中点的颜色。每种颜色代表 B(血压)的固定值,就像在分叉例子中一样,很容易看到单一颜色的回归线是平的,但当所有数据点作为一个整体来看时,有很强的相关性。
对链中的中间节点进行条件限制会阻断并关闭路径,而不对其进行条件限制则使路径保持畅通。
一个值得关注的最后一点是,数据中的模式对于分叉和链条是相同的,这加强了因果关系不能仅仅通过数据确定的观点。实际上,需要一个 DAG 来补充数据,以揭示因果关系,这支持了因果关系倡导者的观点,而非纯粹统计学家的观点。
碰撞器
这个第三种也是最终的模式是碰撞器,它与分叉和链条有一些有趣的不同 …

作者提供的图像
- 𝑆→𝐵←𝐴
这个 DAG 表示了一个假设中的专注于体育的学院中的因果关系 -
-
体育能力(S)导致学生奖学金的授予(B)。
-
学术能力(A)也导致学生奖学金的授予(B)。
与其他模式一样,我们将从一组数据开始,以可视化和理解包含碰撞器的路径内部发生的情况 …

作者提供的图像
体育能力与奖学金、学术能力与奖学金的散点图如下 …

作者提供的图像
两种能力与学院给予奖学金的得分之间有相当强的相关性。现在让我们可视化留下注间变量(奖学金)不条件化或固定并条件化的影响 …

作者提供的图像
最大的惊讶是,留下一个碰撞器不进行条件化意味着路径是阻塞的,这可以通过考虑体育能力与学术能力之间的低相关性来证明。
在现实世界中,这并不令人惊讶。拥有高水平体育能力的人是不寻常的,而具有高学术能力的人也不常见,而在这两个领域都具有卓越能力的人非常稀有,这就是为什么相关性是平坦的。
然而,如果我们通过固定一个特定的奖学金得分来进行条件化,我们实际上解开了一个自然被阻塞的路径,这也是容易解释的。如果我们查看一个特定的奖学金得分,而这个人的体育能力较低,他们很可能具有较高的学术能力,反之亦然。
在碰撞器中的中间节点条件化会解开并打开路径,而不进行条件化则会使路径被阻塞和关闭。
回顾
回顾一下,DAG 将有多个路径连接处理节点和结果,每个路径可以通过突出显示 DAG 的副本或以“扁平化”形式 𝐴→𝐵→𝐶 来可视化。
在路径中,如果我们排除处理和结果节点,每个剩下的节点将有两个箭头,每个箭头连接到一个节点,这种 3 个节点和两个方向箭头的模式称为交点。
每个交点可以用仅有的 3 种模式之一连接——分叉、链条或碰撞器,了解每种模式的条件化效果(或不条件化)是很重要的。
直观地了解对每种模式进行条件化的效果是一个复杂的话题,但本质上只需要记住 3 条规则——
-
在分叉处对中间节点进行条件化会阻断路径,而不对其进行条件化则会使路径保持畅通。
-
在链中的中间节点进行条件化会阻断并关闭路径,而不对其进行条件化则会使路径保持畅通。
-
在碰撞器中对中间节点进行条件化会解除阻断并打开路径,而不对其进行条件化则会使路径保持阻断状态。
这些都是理解如何对复杂的 DAG 进行调整和条件化所需的基础知识,并了解其重要性,但在继续之前,还有一个话题需要探讨…
阻断和解除复杂路径的阻断
考虑以下路径…
𝑋←𝑍1←𝑍3←𝑍2→𝑌
X 是治疗,Y 是结果。其他节点中,Z1 和 Z3 是链,而 Z2 是分叉。问题是——“我们如何阻断这条路径?”
答案是,在任何路径中,无论它多长或多复杂,只要一个节点被阻断,那么整条路径就会被阻断。
这意味着,仅对 Z2 进行条件化会阻断路径,因为它是一个分叉,但仅对 Z1 或 Z2 进行条件化也会阻断路径,因为它们是链。实际上,任何组合都有效——{Z1}、{Z2}、{Z3}、{Z1, Z2}、{Z1, Z3}、{Z2, Z3}、{Z1, Z2, Z3}。
阻断路径的最佳调整集是包含单个节点的任何一个集合,即我们应该尽量少地进行条件化以阻断路径。
现在已经解释了如何阻断复杂路径,你一定会问——“为什么我要费心去阻断一条路径?”答案将在下一节中揭示,并展示有向无环图的全部威力。
回门调整
我们已经探讨了混杂变量如何导致治疗对结果的孤立效应丧失,因为混杂变量的效应与该孤立效应混合在一起。
我们还看到,对混杂变量进行条件化可以解决这个问题,使治疗的真实效应得以恢复…

由作者提供的图像
- 𝑋←𝑍→𝑌
在这个 DAG 中,如果我们对 Z 进行条件化,它会阻断该路径,从而达到预期效果。被突出显示的路径称为回门路径,而从 X 到 Y 的直接路径称为前门路径。
对回门路径的一个常识性定义是“从治疗到结果的任何包含分叉的路径”,但我找到了由 Judea Pearl 提出的更正式的定义…
回门路径是从 X 到 Y 的任何一条路径,只要它以指向 X 的箭头开始。 《为什么这本书》,Pearl 和 Mackensie,第 158 页
同样,任何不是回门路径的路径都是前门路径,因此…
前门路径是从 X 到 Y 的任何一条路径,只要它以指向 X 的箭头开始。
有效调整集
一个有效的调整集是任何一个节点集合,当对这些节点进行条件化时,可以去除混淆效应并隔离处理对结果的影响,在任何给定的有向无环图(DAG)中,这些有效的调整集可以是零个、一个或多个。
这里是 Judea Pearl 提出的定义有效调整集的一组规则……
-
我们阻断 X 和 Y 之间的所有虚假路径。
-
我们保持从 X 到 Y 的所有有向路径不变。
-
我们不创建新的虚假路径。
(Pearl, Glymour 和 Jewell,《统计中的因果推断》,第 61 页)
我认为这个定义不令人满意,因为“虚假”的含义对我来说不清楚,并且我也不喜欢“未扰动”,因为我认为有时需要扰动路径以产生期望的效果(稍后会详细说明)。
鉴于我对官方定义的不满,这里是我自己的提议……
一个有效的调整集是任何一个节点集合,当对这些节点进行条件化时,可以阻断并关闭处理和结果之间的所有后门路径,同时保留至少一条前门路径未被阻断且开放。
一个实例
为了测试我的定义并提供一个实例,我们回到之前的一个复杂有向无环图的例子……

图片由作者提供
-
𝑋←𝑍1→𝑍3→𝑌
-
𝑋←𝑍1→𝑍3←𝑍2→𝑌
-
𝑋←𝑍3→𝑌
-
𝑋←𝑍3←𝑍2→𝑌
-
𝑋→𝑊←𝑌
使用 Pearl 的定义,很容易看出前四条路径是后门路径,而最后一条路径是前门路径,只需查看指向或离开 X 的箭头方向即可。
为了找到有效的调整集,我们需要确定任何节点集合,当对这些节点进行条件化时,可以阻断并关闭处理和结果之间的所有后门路径,同时保留至少一条前门路径未被阻断且开放。
Z3 是一个明显的条件节点,因为它明显混淆了处理 X 和结果 Y。对 Z3 进行条件化可以阻断并且消除路径 3 的混淆。
到目前为止,一切顺利,但这是我长期无法理解的部分,而且现有文献也没有很好地解释这一点……
在路径 3 中,Z3 是一个分叉点,但在 M 形的路径 2 中,Z3 是一个碰撞点,我们知道对碰撞点进行条件化会解除阻断并开放路径。因此,我们已经阻断了 Z3 来关闭并阻断路径 3,但这样做的结果是我们无意中解除阻断并开放了路径 2!对 Z3 进行条件化解决了一个问题,但产生了另一个问题。
然而,这个问题很容易解决。除了对 Z3 进行条件化外,对 Z1 进行条件化也会阻断路径 2。逻辑如下—
-
Z3 必须保持条件化以阻断路径 3,但这样做会打开路径 2。
-
Z1 可以额外进行条件化,这样可以阻断路径 2,因为 Z1 是这条路径中的一个分叉点。
整个 DAG 的解决方案几乎完成,但还差一点……
当然,对 Z1 进行条件化是有效的,但我们也可以对 Z2 进行条件化,实际上对 Z1 和 Z2 都进行条件化也有效,因此 {Z1}、{Z2} 和 {Z1, Z2} 都是同样有效的调整集。
拼图的最后一部分是路径 5 是唯一的正向路径。我们之前的定义告诉我们,必须保留至少一条正向路径不被阻塞,但 W 是一个碰撞器,如果不进行条件化,它将阻塞并关闭路径 5。
因此,我们还必须对 W 进行条件化,以打开 X 到 Y 的正向路径。请注意,这也是我不同意使用“未扰动”一词的原因,因为在这个例子中,路径 5 必须被扰动以确保 X 到 Y 有一条开放的正向路径。
如果解释有些复杂,可视化会更容易。以下是 DAG 的所有有效调整集……

图片由作者提供
选择任意一个这三个调整集将正确调整和条件化 DAG,通过阻止并关闭所有 4 条反向路径,保留单一的正向路径不被阻塞。
调整集中的最终概念是最优调整集,即选择有效调整集中节点最少的一个。
在这个例子中,最优调整集可以是 {Z1, Z3, W} 或 {Z2, Z3, W}。第 3 个有效调整集 {Z1, Z2, Z3, W} 不是最优的,因为它涉及的调整数量多于最小数量。
为什么这很重要?
去混淆对于识别 X 和 Y 的孤立因果效应至关重要,同时去除了任何混淆变量的混合效应。
例如,我们想知道服用药物对恢复的真实影响,无论药物的性别偏好如何,或运动对学术表现的真实影响,无论潜在的体能如何,唯一的办法是构建 DAG,并在正确的节点集上进行条件化或控制。
我的观点是,识别最优调整集在条件化历史观测数据或控制现实世界试验中的因素时同样有效。
如果应调整的因素未被调整,结果将会错误;如果调整了过多因素,则数学计算将因条件设置而变得不必要地复杂,或实验和试验在实际中变得繁琐和笨重。
行话破解
在将所有内容汇总并应用于解决一些示例问题之前,还有一些因果术语需要解析……
d-分离
d-分离的一个正式定义如下:
如果一个路径 p 被一组条件节点 Z 阻塞,当且仅当
-
p 包含一个节点链 A -> B -> C 或叉 A <- B -> C,使得中间节点 B 在 Z 中(即被条件化),或
-
p 包含一个碰撞器 A -> B <- C,使得碰撞节点 B 不在 Z 中,且 B 的任何后代不在 Z 中。
统计中的因果推断,Pearl、Glymour 和 Jewell,第 46 页
语言最初难以解读,但结合上述对叉、链、碰撞器、条件等的解释和示例,可能更直接的定义是……
d-分离是应用一组条件节点到路径上,使其被阻塞的行为。
正式定义中唯一新的部分是且 B 的后代不在 Z 中。但这很容易解释。
考虑以下有向无环图……

作者提供的图片
在这个有向无环图 (DAG) 中,Z1 是路径 X -> Z1 <- Y 上的一个碰撞点,因此对 Z1 进行条件化会解锁并打开该路径,而不对 Z1 进行条件化则会使路径保持封闭。
且 B 的后代不在 Z 中 的陈述意味着,对 Z2 进行条件化(或不条件化)与对 Z1 进行操作的效果完全相同,打开路径 X -> Z1 <- Y 的有效节点集是 {Z1}、{Z2}。
这个额外的条件很难可视化和理顺,因此我采纳了这样的方式,鉴于描述作者的资历,并且也认识到它的使用可能是稀少的。碰撞点是三种模式中最不寻常的,而碰撞点的后代则更加不寻常。
但值得注意的是,文献中的许多示例确实包含了碰撞点的后代,因此要注意!
d-连接
d-连接 是 d-分离的对立面。我在文献中没有看到正式的定义,但逆转 d-分离的定义会得到以下结果 -
路径 p 只有在条件节点集 Z 的影响下才能被解锁,当且仅当
-
p 包含一个节点链 A -> B -> C 或者一个分叉 A <- B -> C,条件是中间节点 B 不在 Z 中(即,没有进行条件化),或者
-
p 包含一个碰撞点 A -> B <- C,使得碰撞节点 B 在 Z 中,或者 B 的后代在 Z 中。
更简单、非正式的定义可能是
d-连接是应用一组条件节点到路径上,使其被解锁的行为。
请注意,条件节点集可以是空集 {},实际上,除非路径包含碰撞点,否则将是空集。
关于 d-分离和 d-连接的说明
我的观点是,d-分离和 d-连接非常复杂,它们单独存在并没有实际意义。最终目标是确定一个有效的调整集或最佳调整集,以便对正确的节点集进行条件化,从而隔离处理对结果的影响。
我提供了一个解释,因为文献中使用了这些术语,因此理解它们的含义有助于阅读文献,但我的重点在于调整集。
无条件依赖、无条件独立、条件依赖和条件独立
这四个术语在文献中常被使用,通常没有明确的解释。这些是我的定义 -
-
无条件依赖: 特定路径自然是未被阻塞和开放的,没有任何条件化。
-
无条件独立: 特定路径自然是被阻塞和封闭的,没有任何条件化。
-
条件依赖性:一个特定的路径在进行条件化后是开放的,没有被阻塞。
-
条件独立性:一个特定的路径在进行条件化后被阻塞且关闭。
依赖性意味着结果依赖于治疗,即治疗值的变化会改变结果值。
独立性意味着结果不依赖于治疗,即治疗值的变化不会改变结果值。
外生变量与内生变量
文献中还有两个常用的术语,通过可视化容易理解。考虑以下 DAG……

作者提供的图像
浅蓝色节点是外生变量,深蓝色节点是内生变量。
外生变量的正式定义可能是:
模型外部的变量,即我们选择不解释它们是如何产生的变量,不论出于何种原因。
更简单地说,外生节点没有输入,只有输出,即它们没有任何原因,但它们会导致其他变量发生变化。
内生节点则相反,它们确实有内部“系统”中的输入或原因。
在文献中,常见的结构因果模型使用 U 来表示外生变量,V 来表示内生变量,如下所示……
-
V =
-
U =
未观察到的混杂因素
未观察到的混杂因素通常在因果图上表示为“U”节点,但不应与外生变量混淆。
未观察到的混杂因素是一个同时影响治疗和结果的变量,但未在分析中测量或包含该变量。
一个例子可能是我们怀疑性别影响了药物的摄取和效果,但在分析中未测量性别。
有几种因果技术可以用来考虑未观察到的混杂因素,但这些超出了本文的范围。
将所有内容结合起来,并用一些实际例子进行说明
现在已经涵盖了理解绝大多数有向无环图所需的所有技术,因此是时候利用这些知识来测试并分析一些 DAG 了。
这里是一种解决 DAG 和识别最佳调整集的方法……
-
研究 DAG 并识别治疗与结果之间的所有路径。
-
将路径分为后门路径(箭头指向治疗)和前门路径(箭头指向结果)。
-
前门路径(通常只有一条)不需要条件化,除非它包含碰撞点,在这种情况下将碰撞点添加到条件化节点集合中。
-
每条后门路径都需要通过选择路径上的节点中的叉或链来进行条件化。将这些节点添加到条件化节点集合中。
-
考虑你所选择的节点,以确保它们符合以下标准 — a. 所有的后门路径都已经被阻断和关闭。b. 至少有一条前门路径保持开放或未被阻断。
-
如果有多个调整集满足第 4 步的标准,则选择节点数量最少的一个。
通过遵循这种方法论,你将能够从任何有向无环图中识别出最优调整集。
以下示例应用了这种方法论,并为每个示例提供了解决方案和解释。
示例 1

图片来源:作者
- 𝑋→𝐴→𝑌
让我们从一个简单的开始。没有后门路径,且唯一的前门路径是 X -> A -> Y,这条路径未被阻断,因此不需要任何条件处理(即,最优调整集是空集{})。
示例 2

图片来源:作者
-
𝑋←𝐴→𝐵←𝐷→𝐸→𝑌
-
𝑋→𝐸→𝑌
这个需要多一些思考。有一条后门路径(路径 1)和一条前门路径(路径 2)。前门路径 2 没有被阻断,因此不需要条件处理以保持有效和开放。后门路径 1 在 B 处有一个碰撞点,因此自然被阻断,也不需要任何操作。
总结一下,所有的后门路径自然被阻断,前门路径自然被打开,因此最优调整集仍然是空集{}。
故事并没有就此结束。需要注意的是,节点 C 可以被条件处理,这不会造成任何伤害,因为它不影响任何东西。
同样,A 处的分叉也可以进行条件处理,这将进一步阻断后门路径 1,除了未条件处理的碰撞点 B 的阻断,再次使有向无环图得到正确调整。
这说明了调整或条件化有向无环图以获得“正确”结果的方式可能不止一种,并且统计学家和因果推断专家在决定如何行动时可能会有所不同。
Pearl 引用了一位领先的统计学家(匿名)的说法:“避免对某些观察到的协变量进行条件处理……是不科学的临时措施”(《为什么的书》,Pearl 和 Mackenzie,第 160 页),但通过这个例子可以反驳条件处理一切的粗暴方法。
对 B 或 C 进行条件处理会打开并解锁后门路径 1,而这需要被关闭(B 因为它是一个碰撞点,C 因为它是碰撞点的后代)。如果对 B 或 C 进行条件处理或控制,那么通过额外控制 A 或 E(或两者)可以重新阻断路径 1,但这可能导致复杂的数学问题(对于条件处理)或难以调节的现实试验(对于控制)。
我的偏好是使用历史和观察数据,优化调整集,简化数学计算并使用条件处理,但其他人可能有不同的看法。
示例 3

图片来源:作者
-
𝑋←𝐵→𝑌
-
𝑋→𝐴←𝐵→𝑌
-
𝑋→𝑌
路径 3 是一个开放的、未被阻断的前门路径,不需要任何条件处理。
有两个后门路径。路径 1 包含一个分叉点 B,必须对其进行条件调整。路径 2 包含一个碰撞点 A,因此路径 2 是无条件阻塞的。
最佳调整集是 {B},但需要注意的是,对 {A, B} 进行条件调整也会有效,因为尽管对 A 的条件调整会解锁碰撞点,但路径 2 会被对 B 的条件调整所阻塞。
统计解决方案可能是对 A 和 B 进行控制,但最佳因果解决方案是仅对 B 进行条件调整。
示例 4

图片由作者提供
- 𝑋→𝐴←𝑌
这个例子有点狡猾!它是示例 1 的变体,具有单一的前门路径,但这次 A 是一个碰撞点。
由于 A 是一个碰撞点,它将关闭并阻塞前门路径 1,因此完全有效的解决方案是对 A 进行条件调整。
然而,B 是 A 的后代,并且回顾上述 d-分离的定义,即…
-
p 包含节点链 A -> B -> C 或分叉 A <- B -> C,使得中间节点 B 在 Z 中(即被条件调整),或
-
p 包含碰撞点 A -> B <- C,使得碰撞节点 B 不在 Z 中,并且 B 的任何后代不在 Z 中。
… 对 B 的条件调整与对 A 的条件调整效果完全相同。
因此,有效的调整集是 {A}、{B} 和 {A, B},最佳调整集是 {A} 或 {B}。
示例 5

图片由作者提供
-
𝐺→𝑄→𝐻
-
𝐺→𝑄←𝐼→𝐻
-
𝐺→𝐻
在这个 DAG 中,节点代表以下变量 -
-
G : 性别(处理)
-
H : 招聘(结果)
-
Q : 资格
-
I : 收入
目标是捕捉性别(处理)对招聘(结果)的因果关系。此外,DAG 提出了性别对资格的因果影响和收入对资格及招聘的因果影响。
看起来非常复杂,但实际上很容易解决。没有箭头指向处理 G,因此没有后门路径。所有 3 条路径都是前门路径,均开放且未被阻塞,无需任何条件调整,因此最佳调整集是 {}。
注意,虽然有几个节点集可以进行条件调整以产生正确结果,但不采取任何行动是最佳选择。例如,对 I 进行条件调整会阻塞路径 2,但其他前门路径仍会保持开放。
这是因果推断视角选择不采取行动以保持数学简单的另一实例,而统计视角可能选择对某些变量进行控制。
示例 6

图片由作者提供
这个例子看起来更复杂,但实际上非常简单。浅蓝色节点是外生变量,即我们选择不解释其原因的变量。
这可能引发“为什么要包含这些变量?”的问题。在前面关于分叉的部分中,鞋码在模型中的定义如下(其中鞋码 = S 和年龄 = A) -
𝑆 = 2 + 2 + 𝜖
这个例子显示了年龄是外生变量——没有关于年龄的“原因”的解释,但年龄对模型至关重要,因为鞋码是从年龄派生的。
这也有助于说明外生变量和未观察到的混杂因素之间的区别。在因果模型中,我们会有外生变量的数据,但我们没有未观察到的混杂因素的数据。
在计算有效和最佳调整集时,未观察到的混杂因素应始终排除在考虑范围之外,因为我们没有关于它们的数据,因此无法对其进行条件化。
外生变量可以被纳入条件化集合。在年龄和鞋码的例子中,对年龄进行条件化是非常合理的,因为它干扰了鞋码和学术能力,而在这个例子中,条件化外生变量不会造成任何损害,但也不会是最佳选择,因为它们都没有出现在唯一的路径(路径 1)中。
那么,这个例子的最佳调整集是什么呢?
假设 Z 是处理变量,Y 是结果变量,则没有回门路径,只有 1 条前门路径。该路径被碰撞器 W 阻挡,可以通过对 W 进行条件化来开放和解锁它。
还应该注意,U 是碰撞器 W 的后代,因此对 U 进行条件化将与对 W 进行条件化有相同的效果。
因此,有 3 个有效调整集——{W}、{U} 和 {W, U},其中前两个是最佳的。
示例 7

作者提供的图像
-
𝑍→𝑊←𝑋→𝑌
-
𝑍←𝑇→𝑌
示例 7 是示例 6 的扩展。这次添加了一个回门路径,在处理 Z 和结果 Y 之间的 T 处有一个分叉。
必须对 T 进行条件化以阻断回门路径 2,因此有两个最佳调整集——{T, W} 和 {T, U}。
示例 8

作者提供的图像
-
𝑇←𝐴→𝐼
-
𝑇→𝐼
-
𝑇←𝐸→𝐼
-
𝑇←𝐵→𝐼
-
𝑇←𝑆→𝐼
-
𝑇←𝑈→𝐼
这个 DAG 中的节点表示以下变量…
-
T : 培训
-
I : 收入
-
A : 年龄
-
E : 教育
-
B : 福利
-
S : 单身
-
U : 大学学位
这个 DAG 表示培训课程 T 对收入 I 的因果影响,同时也表示年龄、教育、福利、单身(与已婚相比)和大学学位都对个人是否参加培训和收入造成了混杂。
有 5 条回门路径(路径 1、3、4、5 和 6)和 1 条前门路径(路径 2)。
前门路径是开放的和未被阻塞的,无需任何条件化处理。
所有的回门路径都有一个分叉,并且没有中间节点出现在多个回门路径上,因此所有的分叉都需要进行条件化处理。
最佳(也是唯一的)调整集是 {A, E, B, S, U},其中 U = 大学学位,不能与未观察到的混杂因素混淆。
示例 9

作者提供的图像
这个 DAG 中的节点表示以下变量…
-
A : 父母吸烟
-
B : 儿童哮喘
-
C : 潜在过敏体质(未观察到)
-
D : 父母哮喘
-
E : 慢性支气管炎
-
F : 性别
-
G : 社会经济状态
-
X : 吸烟
-
Y : 哮喘
这是一个极其复杂的 DAG。总共有 20 条路径可以通过 DAG 进行可视化,如下所示…

作者提供的图片
这里有一条正向路径(倒数第二行中的 X 到 Y 的直接路径),其他 19 条都是反向路径。
共有 64 种可能的节点组合可以成功条件化 DAG,例如{D}、{D, B}、{D, B, E}等,且每个可能的节点组合需要测试 20 条不同路径,总共 1,280 种排列组合。
准确地说,有两个有效的调整集——{A, B, E, F, G} 和 {A, B, D, E, F, G},其中第一个是最佳调整集…

作者提供的图片
人工检查所有 1,280 种排列组合并确保所有的反向路径都被阻塞且正向路径未被阻塞将是非常困难且耗时的。
你可能已经从所有 DAG 的图片和我处理过的例子中猜到,我开发了一个代码库,它为我完成了所有绘图、计算和路径查找工作。
本文记录了我在开发该库时所学到的一切,未来的文章将展示、探索和解释该代码库的工作原理以及如何用几行 Python 代码自动化你可能想知道的关于 DAG 的一切。
结论
对于任何坚持读完这样一篇长且详细的文章的人,我感谢你的坚持!我的大多数文章相对较短且集中,包含所有源代码,而这篇正好相反!
然而,这篇文章详细说明了我希望在开始我的因果推断之旅时了解的所有关于有向无环图的内容。
有许多文章和书籍,但在我看来,它们未能击中要点。它们通常直接跳入公式和方程,而忽略了需要通过示例和可视化来解释的基础知识和基本概念。
捕捉因果关系的 DAG 是因果推断中最重要的部分之一。如果没有对它们的全面理解,其余部分都没有意义,需要理解它们是什么、为什么存在以及它们如何帮助识别调整集,这是在应用反向(或正向)调整之前必须了解的关键问题。
联系并保持联系…
如果你喜欢这篇文章,你可以通过点击我的推荐链接以每月仅 5 美元成为 Medium 会员,获得对更多成千上万篇文章的无限访问(如果你通过此链接注册,我将获得部分费用,对你没有额外费用)。
## 通过我的推荐链接加入 Medium - Graham Harrison
阅读 Graham Harrison 的每个故事(以及 Medium 上的成千上万的其他作家)。提升你对数据的知识……
grahamharrison-86487.medium.com
……或通过以下方式连接……
访问我的数据科学网站——数据博客。
进一步阅读
如果你想了解更多关于因果推断的内容,这本精彩的书籍是一个很好的起点……
购买《为什么的书:因果关系的新科学(企鹅科学)》01 由 Pearl, Judea, Mackenzie, Dana (ISBN……
本书对一些关键概念进行了更深入和详细的探讨……
购买《统计中的因果推断:入门》 1 由 Pearl, Judea, Glymour, Madelyn, Jewell, Nicholas P. (ISBN……
注:这些是附属链接,如果你通过这些链接购买,我将获得一小部分佣金,对你没有额外费用。
通过强大的五步因果影响框架释放你作为商业分析师的全部潜力
因果推断可以帮助你成为一名商业分析师明星
·发表于 Towards Data Science ·阅读时长 7 分钟·2023 年 11 月 22 日
--
在商业环境中,领导层通常对决策或事件对 KPI 的影响感兴趣。 作为一名绩效分析师,我的大部分时间都在回答这样的问题:“{新闻、政府公告、特殊事件…} 对国家 X 的表现有何影响?”。直观地,如果我们知道新闻/公告/特殊事件从未发生过的情况下会发生什么,我们就能回答这个问题。
这就是因果推断的本质,一些非常有才华的人正在努力使因果推断框架对我们可用。

照片由 Andrew George 提供,来源于 Unsplash
Google Causal Impact 库就是其中之一。由 Google 开发,旨在帮助他们做出更好的营销预算决策,这个库可以帮助我们量化任何事件或干预对感兴趣的时间序列的影响。它听起来可能有些吓人,但其实非常直观。
作为商业分析师,我们应该在日常工作中利用这些工具;以下是你可以采取的 5 个简单步骤,以实施你的第一次因果影响分析。
步骤 1:安装和导入包
本指南将使用 Python。
我们将从安装 Google Causal Impact 包开始。
>pip install tfcausalimpact
你可以在 GitHub 上找到更多关于这个包的信息:github.com/WillianFuks/tfcausalimpact
运行因果影响分析时,你只需要 4 个包。
from causalimpact import CausalImpact
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
第 2 步:导入数据并定义前/后期
我们可以把因果影响框架看作是一个时间序列问题。
在特定日期,我们观察一个事件、新闻等,并跟踪我们的关注指标在该事件后相对于某些基准的变化。你可以将基准视为控制组。
要进行因果影响分析,我们需要以下内容:
-
研究事件的日期
-
我们受影响单位的关注变量的时间序列
-
我们受影响单位的关注变量的时间序列
-
未受事件影响的其他多个单位的关注变量的时间序列
对于我的示例,我使用了以下虚构场景:
-
事件: 2022 年 7 月 9 日在巴塞罗那举行的全球会议
-
关注变量:航空公司的乘客收入
-
受影响单位: 巴塞罗那
-
控制组: 其他欧洲市场
-
问题: 会议对巴塞罗那的乘客收入产生了什么影响?
在考虑时间范围时,应该尽量缩短后期的时间,而前期应该比后期长。
请注意,数据是人为生成的,仅用于说明。
#Define the dates
training_strat = "2022-03-10"
training_end = "2022-06-30"
treatment_start = "2022-07-01"
treatment_end = "2022-07-13"
#import the data- for my example i am using a CSV
data=pd.read_csv("example.csv", index_col='Date', parse_dates=True)

数据框的头部数据
第 3 步:创建控制组
此步骤是最关键的部分。
要估计任何事件或处理的影响,我们需要知道我们的关注单位(例如:巴塞罗那乘客收入)在处理和未处理情况下的情况。这是因果推断的主要问题。
我们永远无法在两种互斥的情况下观察我们的关注单位。解决方案是创建一个反事实场景。
你可以把反事实看作是一个假设场景,在这个场景中事件/处理并未发生。
我们的控制组将帮助我们创建这个场景。 要决定将哪些市场包括在控制组中,我们需要计算哪些市场具有预测我们关注市场(巴塞罗那)的预测能力。我们将使用相关性来建立这种预测能力。
此步骤仅需使用前期数据完成。
# get the training data only
df_training = data[data.index <= pd.to_datetime(training_end)]
在使用时间序列检查相关性时,我们需要数据是平稳的,意味着没有趋势或季节性成分,以避免发现虚假的相关性。
# Check for stationarity
from statsmodels.tsa.stattools import adfuller
test = adfuller(x = df_training['Barcelona'])[1]
print(test)
if test< 0.05:
print("Data is stationary")
else:
print("Time series is not stationary")

来自 adfuller 测试的结果
然而,大多数时间序列并非平稳,但我们可以使用称为差分的方法使其平稳。
# Differening calculates the difference or Pct Difference from the previous date
differencing = df_training.pct_change().dropna(thresh = 1,axis=1).dropna()
differencing.head()

每一行表示与前一天的百分比差异。
#Rest on the differenced data
test = adfuller(x = differencing['Barcelona'])[1]
print(test)
if test< 0.05:
print("Data is stationary")
else:
print("Time series is not stationary")

现在,我们可以使用这个新的时间序列来建立市场之间的相关性。
#Create correlation
market_cor= pd.Series(differencing.corr().abs()['Barcelona'])
#Create a list with markets with correlation coef >=0.3
markets_to_keep = list(market_cor[market_cor >=0.3].index)
#Keep only markets on the above list
final_data = data.drop(columns=[col for col in data if col not in markets_to_keep])

我们将用于对照组的市场
步骤 4:实施 CausalImpact
现在,我们准备实施 CausalImpact。
简而言之,CausalImpact 将利用我们的对照组来学习预测巴塞罗那在前期的乘客收入。模型将用此来预测一个没有会议发生的反事实后期情境。
实际发生的情况与反事实情境之间的差异即为会议的影响。
#Prepare Pre and Post periods
pre_period = [training_strat,training_end]
post_period = [treatment_start,treatment_end]
#Fitting CausalImpact
impact = CausalImpact(data=final_data,
pre_period=pre_period,
post_period=post_period)
步骤 5:解释结果与验证
Google CausalImpact 使得结果的可视化和总结变得非常简单。
你可以通过绘制影响图开始。
impact.plot()

print(impact.summary())
Posterior Inference {Causal Impact}
Average Cumulative
Actual 397631.92 5169215.0
Prediction (s.d.) 177248.77 (6914.43) 2304234.0 (89887.6)
95% CI [163527.02, 190631.09] [2125851.11, 2478204.1]
Absolute effect (s.d.) 220383.16 (6914.43) 2864981.0 (89887.6)
95% CI [207000.83, 234104.91] [2691010.9, 3043363.89]
Relative effect (s.d.) 124.34% (3.9%) 124.34% (3.9%)
95% CI [116.79%, 132.08%] [116.79%, 132.08%]
Posterior tail-area probability p: 0.0
Posterior prob. of a causal effect: 100.0%
For more details run the command: print(impact.summary('report'))
根据你测量的内容,你可能对平均影响或累计影响感兴趣。在我们的案例中,我们对会议期间的累计影响感兴趣。
根据分析,会议为巴塞罗那的乘客收入贡献了+280 万美元的增益。
为了更全面的总结,你可以使用以下命令。
print(impact.summary('report'))
Analysis report {CausalImpact}
During the post-intervention period, the response variable had
an average value of approx. 397631.92\. By contrast, in the absence of an
intervention, we would have expected an average response of 177248.77.
The 95% interval of this counterfactual prediction is [163527.02, 190631.09].
Subtracting this prediction from the observed response yields
an estimate of the causal effect the intervention had on the
response variable. This effect is 220383.16 with a 95% interval of
[207000.83, 234104.91]. For a discussion of the significance of this effect,
see below.
Summing up the individual data points during the post-intervention
period (which can only sometimes be meaningfully interpreted), the
response variable had an overall value of 5169215.0.
By contrast, had the intervention not taken place, we would have expected
a sum of 2304234.0\. The 95% interval of this prediction is [2125851.11, 2478204.1].
The above results are given in terms of absolute numbers. In relative
terms, the response variable showed an increase of +124.34%. The 95%
interval of this percentage is [116.79%, 132.08%].
This means that the positive effect observed during the intervention
period is statistically significant and unlikely to be due to random
fluctuations. It should be noted, however, that the question of whether
this increase also bears substantive significance can only be answered
by comparing the absolute effect (220383.16) to the original goal
of the underlying intervention.
The probability of obtaining this effect by chance is very small
(Bayesian one-sided tail-area probability p = 0.0).
This means the causal effect can be considered statistically
significant.
与机器学习不同,Causal Impact 没有准确度测量,这可能使得验证有点棘手。
然而,你可以做 3 件事来验证你的结果。
-
确保你在前期的置信区间不要过于宽泛——这可能表明你的对照组预测能力不足。
-
确保估计影响的置信区间不包含 0。
-
使用反驳测试,例如,如果你进行相同的分析但将事件日期更改为前期的任何一天,影响应为 0。
这是我在工作中最常做的分析之一;一旦你熟悉了这 5 步框架,你也可以使用它,并成为一名商业分析高手。
参考文献
[1]Brodersen, K. H., Gallusser, F., Koehler, J., Remy, N., & Scott, S. L. (2015). 使用贝叶斯结构时间序列模型推断因果影响。
[2]Molak, A., & Jaokar, A. (2023). 《Python 中的因果推断与发现:揭开现代因果机器学习的秘密,涵盖 DoWhy、EconML、PyTorch 等》 [平装本]。5 月 31 日。
[3]research.google/pubs/pub41854/Kay Brodersen 的《使用 CausalImpact 推断事件影响》
解锁数据访问:在没有 API 端点的情况下利用触发器
使用触发器填补数据拼图中的缺失部分
·发表在 Towards Data Science ·10 分钟阅读·2023 年 6 月 9 日
--

概述
你是否曾经遇到过这样的场景:你尝试通过 API 从一个事务系统(如电子商务系统)中提取一个关键数据点,却发现通过提供的端点无法访问所需的信息?如果是这样,请继续阅读,了解如何有效地利用触发器应对这一挑战。
在没有端点的情况下,我们可能会认为直接从事务表中查询数据是一种选择。 直接查询事务表绝对不是一个好主意,因为它可能会对事务系统的性能和稳定性产生重大影响,特别是当涉及到电子商务系统时。当你尝试从实时的电子商务系统中查询数据时,很可能会对用户体验产生不利影响(想象一下在亚马逊购物时需要等待 5 到 10 分钟才能检索到购物车!)。
除此之外,在事务系统的表上运行作业可能会干扰正在进行的事务。如果你考虑在数据仓库表中每天进行“截断-加载”操作,这个问题变得更加重要。此外,上述选项不足以成为可持续的解决方案,因为它不支持平滑的历史数据加载,假设事务系统中定期进行数据清除。
因此,自动化从事务系统中提取数据并将其无缝集成到数据仓库中,同时又不会对系统产生不利影响,就变得至关重要。在这种情况下,数据库触发器提供了一种有效的解决方案。但在我们深入探讨解决方案之前,这里是对触发器的简介。
触发器简介
数据库触发器
数据库触发器是一个经常被忽视的概念,自关系型数据库诞生以来就存在。数据库触发器是一个函数,每当在源表(在此情况下为事务表)中创建、更新(或甚至删除)记录时,都会触发该函数。
数据库触发器分为两种类型:DDL 触发器和 DML 触发器。
DDL 触发器在你希望获取数据库结构更改通知时设置。例如,当你希望在每次定义新模式时得到警报,或者在创建或删除新表时得到通知时,DDL 触发器非常有用。因此,命名为 DDL(数据定义语言)触发器。
DML 触发器在插入、删除或更新新记录时被触发。换句话说,你会在系统中发生数据操作更改时得到通知。一个重要的点是,数据库触发器可以被编程来不仅仅提醒你发生了变化,还可以执行如将数据移动到暂存表等操作。
专用触发器
现代云平台如 Azure 和 AWS 提供了作为其服务一部分的专用触发器。需要注意的是,专用触发器与数据库触发器并不相同。数据库触发器是特定于数据库管理系统(DBMS)的,并且在数据库内部运行,而专用触发器具有更广泛的应用范围。它们可以用于各种自动化任务、事件驱动的工作流,并且能够在云服务及其组件之间创建流畅的集成。
以下是 AWS 作为其云服务的一部分提供的一些专用触发器:
-
AWS Lambda 触发器:这些触发器帮助在指定事件发生时启动一个 lambda 函数。换句话说,你可以指定一个事件来触发 lambda 函数。事件可以是 AWS 内部的,也可以是外部的。内部事件可能与 AWS 服务相关,如 Amazon S3、Amazon DynamoDB 流或 Amazon Kinesis。外部事件可能来自 AWS 外部事务系统的数据库触发器或 IoT 事件。
-
Amazon S3 事件通知:这些触发器使你能够在 S3 存储桶被创建、修改或删除时获得通知。它们使用 AWS 的简单通知服务(SNS)来广播消息。
-
AWS Cloudwatch Events:如果你使用过独立的关系数据库,如 Microsoft SQL Server 和 SQL Server Management Studio (SSMS),你可能使用过 SQL Server Agent 来通知用户作业失败。Cloudwatch 特定于 AWS,不仅用于通知用户作业失败,还用于触发 Lambda 函数和响应事件。CloudWatch Event 和 Lambda Trigger 之间的重要区别在于,虽然 Lambda triggers 指的是 AWS Lambda 响应事件的能力,但 CloudWatch Events 是一个更广泛的事件管理服务,可以处理来自 Lambda 以外来源的事件。顺便提一下,虽然 SQL Server Agent 需要配置邮件服务器,但 Cloudwatch 没有这样的要求。
以下是 Azure 作为其云服务的一部分提供的一些专用触发器:
-
Blob Trigger——Azure blob 类似于 AWS 提供的 S3 buckets。类似于 Amazon S3 通知可以用来获取 S3 buckets 中变化的警报,blob triggers 可以用来获取 Azure blob 容器中变化的通知。
-
Azure Function Trigger——这些是 Azure 相当于 AWS Lambda Function Triggers 的功能。这些触发器可以用来响应 Azure 内部或外部事件来启动 Azure 函数,例如外部事务数据库触发器、HTTP 请求或 IoT 事件中心流。Azure 函数也可以基于预定义的时间表通过 Timer Trigger 启动。
现在我们已经看过了 AWS 和 Azure 提供的不同类型的数据库触发器和专用触发器,让我们回顾一下之前提到的使用案例,以刷新你的记忆。允许我提醒你一下我们之前提到的使用案例。
使用案例——你在事务系统的表中看到了一些你需要用于报告指标的数据点,但这些数据点没有通过你的事务系统的 API 端点提供。因此,你无法使用 Python 或 Java 编写脚本通过 API 获取这些数据点。你也不能在你的事务系统上直接查询,因为这可能会对其性能产生负面影响。
为了解决这个问题,我们使用数据库触发器和云服务提供的专用触发器的组合。以下是一个高级别的方法:

高级别方法(作者提供的图片)
前提条件: 识别你事务系统数据库中那些通过 API 端点无法访问的数据表。一旦识别出这些表,按照以下步骤操作——
步骤 1: 创建一个与事务表具有相同列的暂存表。确保从源事务表中没有任何额外的约束被复制过来。 这样可以尽可能减少对事务系统的影响。此外,还需添加一列来指示所执行的操作,例如插入、更新、删除。假设你的事务表的后端是 SQL Server,以下是需要创建的事务表和暂存表的示例。
-- Sample transactional table
CREATE TABLE Pricing_info (
ProductID INT PRIMARY KEY,
ProductName VARCHAR(50),
Quantity INT,
UnitPrice DECIMAL(10, 2),
OperationDate DATE
);
暂存表将是:
-- Create a Staging table without constraints
CREATE TABLE StagingTable_pricing (
ProductID INT,
ProductName VARCHAR(50),
Quantity INT,
UnitPrice DECIMAL(10, 2),
OperationDate DATE,
OperationType VARCHAR(10)
);
步骤 2: 直接在‘Pricing_info’表(主要事务表)上设置 DML 触发器。
触发器需要被编程,以便每当有新记录插入,或现有记录被更新或删除时,数据将被插入到暂存表中。使用暂存表的目的是避免对主要事务表施加不必要的压力。
以下是相同内容的示例。如下面所示,DML 触发器的两个最重要方面(实际上,任何数据库触发器)是触发事件和触发时间。触发事件指的是应该激活触发器的操作。在这种情况下,我们关注所有 DML 事件,即插入、删除和更新在事务表‘Pricing_info’中。触发时间指的是触发器是在事件发生之前还是之后执行活动。对于我们的用例,显然是‘After’事件触发器。我们创建三个触发器,每个 DML 事件一个。
以下是插入的触发器:
-- Create the trigger
CREATE TRIGGER TransactionTrigger_pricing_Insert
ON Pricing_info
--Trigger Event
AFTER INSERT
AS
BEGIN
-- Insert new records into the staging table
INSERT INTO StagingTable_pricing (ID, Column1, Column2, OperationType)
SELECT ID, Column1, Column2, 'INSERT'
FROM inserted
END;
接下来是更新的触发器:
-- Create the trigger
CREATE TRIGGER TransactionTrigger_pricing_update
ON Pricing_info
--Trigger Event
AFTER UPDATE
AS
BEGIN
-- Insert record in the staging table with the data that was updated
INSERT INTO StagingTable_pricing (ID, Column1, Column2, OperationType)
SELECT ID, Column1, Column2, 'UPDATE'
FROM inserted
END;
最后,我们创建删除的触发器:
-- Create the trigger
CREATE TRIGGER TransactionTrigger_pricing_Delete
ON Pricing_info
--Trigger Event
AFTER DELETE
AS
BEGIN
-- Insert record in the staging table with the data that was deleted
INSERT INTO StagingTable_pricing (ID, Column1, Column2, OperationType)
SELECT ID, Column1, Column2, 'DELETE'
FROM deleted
END;
步骤 3: 现在让我们进入设置专用触发器的部分。
步骤 3a。如果你的数据仓库托管在 AWS 中, 以下是可以实施的高级解决方案。

AWS 实现思路 — 使用 DML 触发器和 AWS Lambda(图片由作者提供)
在上述解决方案中,源是事务系统。我们在事务系统的数据库中设置一个数据库 DML 触发器。每当一个新记录进入事务数据库表时,触发器会将新数据插入到事务数据库中的暂存表中。根据计划(使用 AWS Cloudwatch 事件),Lambda 触发器会触发一个 Lambda 函数,将数据从暂存表抓取到数据仓库(Redshift)中的表中。让我们看看其中的步骤。
前提条件: 在数据仓库中创建一个表以保存事务信息。
步骤 3a.(i)创建一个 AWS Lambda 函数:编写 Lambda 函数的代码,该函数将从暂存表中获取记录,并将其插入到数据仓库表中,同时进行任何必要的计算。
步骤 3b. (ii) 创建 AWS Lambda Trigger — 使用 AWS Cloudwatch 服务安排 Lambda 触发器在夜间时间表中运行 Lambda 函数(建议在业务时间之外或事务系统活动较低的时间段运行 Lambda 函数)。
步骤 3c. (iii) 使用 EventBridge 设置事件映射 — 配置 Lambda 触发器为事件,以便触发器基于指定的事件条件启动。一个典型的场景是按计划触发 Lambda — 每天一次。
AWS 提供了有关设置 Lambda 函数的详细文档,因此不在本文的讨论范围内。
步骤 3b. 如果你的数据仓库托管在 Azure,我们可以使用 Azure Functions 和定时触发器或 Azure 提供的 Azure Function Trigger。

Azure 实现方案 — 使用 DML 触发器和 Azure Functions(作者提供的图片)
在这种情况下,使用定时触发器是一个好主意。当定时触发器激活时,它将运行 Azure Function,然后从暂存表中提取新记录/更新记录/删除记录。(注意:暂存表将具有一个额外的标志变量,指示记录是插入、更新还是删除。)
以下是要遵循的步骤:
步骤 3b. (i):创建 Azure Function:这类似于设置 AWS Lambda 函数。设置代码以从暂存表中提取记录并将其插入数据仓库表中,同时进行任何必要的计算。
步骤 3b. (ii):设置 Azure Function Trigger:使用 Azure Function 应用程序,设置一个定时触发器并指定计划和时间戳参数名称。
步骤 3b. (iii):使用 Azure Eventgrid 设置事件映射:配置触发器将事件数据映射到 Azure Function 中的适当参数。这使得触发器能够基于指定的事件条件启动。
处理历史数据加载
到目前为止,我们讨论的解决方案是针对新数据进入感兴趣的数据点。那么我们如何处理历史数据呢?
为此,一个选项是创建暂存表时执行‘CREATE TABLE AS SELECT’(SQL Server 中的 SELECT * INTO)。这将创建一个预先填充了事务表中当前所有数据的暂存表。其余步骤将保持不变(根据具体情况通过专用的 Azure 定时触发器/AWS Lambda 触发器)。
另一个选项是对事务表中的所有记录执行‘EMPTY UPDATE’。以下是基于当前示例/用例的空更新示例 —
UPDATE TABLE Pricing_info SET OperationDate=OperationDate
如你所见,没有任何值被更新。然而,从数据库的角度来看,所有记录都已更新,因此触发器将为所有更新触发。因此,所有数据都会到达暂存表。请注意,这不是推荐的方法,因为它可能由于生成的更新和撤销语句的数量而拖累交易系统。此外,整个更新操作期间事务表也将被锁定,其他进程无法使用,从而影响交易系统。如果你的事务表非常小,这种方法是可以使用的。
结语
在本文中,我们探索了触发器在捕获通过交易系统的标准 API 端点无法轻易获得的关键数据点方面的多样性。
这些有助于确保数据完整性,并消除对人工干预的需求。因此,它们也确保了关键报告指标的有效纳入。这里提供的解决方案是异步的,以确保交易系统不会受到任何负担。如果你的交易系统没有大量流量(或)没有直接被终端用户应用程序使用,那么它可以设置为同步过程。在这种情况下,lambda 或 Azure 函数需要将触发事件设置为交易数据库的暂存表。同时,还需要提供适当的数据库连接信息。
希望这篇文章对你有帮助。如果你有任何问题,请在评论中告诉我。祝学习愉快!
解锁数据建模成功:3 个必须拥有的上下文表
以及如何免费获取有价值的数据
·
关注 发表在 Towards Data Science ·6 分钟阅读·2023 年 7 月 6 日
--
照片 由 Tobias Fischer 提供,来自 Unsplash
数据建模对于分析团队来说可能是一项具有挑战性的任务。由于每个组织都有独特的业务实体,为每个表找到合适的结构和粒度成为了开放式问题。但不用担心!你需要的一些数据是简单的、免费的,并且占用存储空间极小。
当你的数据完全建模后,你可以看到以下好处:
-
查询生成较为简单,因此更易于阅读。
-
报告更具可扩展性,减少了硬编码的值。
-
你可能花费更少的时间去寻找正确的数据所在位置。
下面是可以简化你团队分析的 3 个通用表,你可以将它们以维度模型的形式导入到你的数据仓库中。
🗓️日期维度
时间序列报告
如果你曾经需要展示某个商业指标在特定时间点的情况,这几乎是一个必备的表。例如,你可能会被问到:
-
“FY23 的销售情况如何?”
-
你能每天展示客户流失情况吗?
管理层常常从时间序列的角度寻求洞察,提出类似“x 的增长或缩小情况如何?”的问题。日期维度可以根据不同的日期属性灵活分析各种指标。
大多数日期维度表可以通过在数据仓库中直接使用DDL语句和日期函数来创建。
在下面的示例中,我使用 BigQuery SQL 来完成这项任务:
CREATE OR REPLACE TABLE `your_project.your_dataset.date_dimension` AS
SELECT
full_date
, EXTRACT(MONTH FROM full_date) AS calendar_month_number
, EXTRACT(YEAR FROM full_date) AS calendar_year
, EXTRACT(QUARTER FROM full_date) AS calendar_quarter
, FORMAT_DATE('%B', full_date) AS calendar_month_name
, EXTRACT(DAYOFWEEK FROM full_date) AS week_name
, FORMAT_DATE('%A', full_date) AS day_name
, CASE
WHEN EXTRACT(DAYOFWEEK FROM full_date) BETWEEN 2 AND 6
THEN TRUE
ELSE FALSE
END AS day_is_weekday
, CASE
WHEN EXTRACT(DAYOFWEEK FROM full_date) = 1 THEN DATE_SUB(full_date, INTERVAL 2 DAY) -- Sunday
WHEN EXTRACT(DAYOFWEEK FROM full_date) = 2 THEN DATE_SUB(full_date, INTERVAL 3 DAY) -- Monday
ELSE DATE_SUB(full_date, INTERVAL 1 DAY)
END AS last_weekday
, EXTRACT(MONTH FROM DATE_ADD(full_date, INTERVAL 6 MONTH)) AS fiscal_month
, EXTRACT(YEAR FROM DATE_ADD(full_date, INTERVAL 6 MONTH)) AS fiscal_year
, EXTRACT(QUARTER FROM DATE_ADD(full_date, INTERVAL 6 MONTH)) AS fiscal_quarter
FROM UNNEST(GENERATE_DATE_ARRAY('2020-01-01', '2050-12-31', INTERVAL 1 DAY)) AS full_date

拆解一下:
-
我们从GENERATE_DATE_ARRAY函数开始,它返回一个你指定范围内的日期数组。然后我们使用 UNNEST 函数将数组的每个元素拆分成单独的行,就像在标准数据库表中一样。BigQuery 中的数组使用一行来显示多个值。
-
然后利用我们从展开数组生成的full_date列(表示日期格式为XXXX-MM-DD),我们可以使用许多 BigQuery 的EXTRACT函数将full_date的各个部分(如月份、日期、年份等)提取到不同的字段中。
-
FORMAT_DATE函数的作用类似于 EXTRACT,但提供了更多自定义日期值显示的选项。你可以使用Google 文档中描述的特殊格式元素来了解每个‘%’字符的意义。
-
我们还使用DATE_SUB函数,它只是从一个日期中减去一个值。这用于获取相关的财政年度,在这个示例中,财政年度将从每个日历年的七月开始。在这个函数中,我们指定一个数量(1 - 无限)和间隔(日、月、年等)。
🌎邮政编码维度
地理空间报告
如果你被要求创建热图可视化或进行一般的地理空间分析,邮政编码维度将对你的团队非常有用。这让你可以通过纬度和经度可视化元素,按县名、时区聚合,并附加人口数据以进行基准比较。
邮政编码维度是客户表的一个很好的补充表。通过将邮政编码字段作为连接键,你可以附加有意义的上下文数据到你的客户基础所在的位置及其背后的模式。
Opendatasoft 提供各种免费的数据集以及开源 API 连接器。适用于此用例的一个数据集是 US Zip Codes Points- United States of America 数据集。在此链接中,移到‘API’标签,配置 URL 以检索 JSON 数据。
几行 Python 代码后,我们可以输出如下 Pandas DataFrame:
import requests
import pandas as pd
url = 'https://data.opendatasoft.com/api/records/1.0/search/?dataset=georef-united-states-of-america-zc-point%40public&q=&facet=stusps_code&facet=ste_name&facet=coty_name&facet=cty_code&facet=zip'
response = requests.get(url)
zips = response.json()
pd.json_normalize(zips,record_path='records')

分解如下:
-
在这里,我使用 requests 库从‘url’变量中显示的 URL 检索数据,该 URL 在 opendatasoft 网站的 API 标签上生成。
-
使用 Pandas,我使用 json_normalize 函数将 JSON 数据转换为 Pandas DataFrame。
📈FX Rates 事实表
用于财务分析
拥有国际客户的组织通常需要将所有交易转换为基础货币以进行财务报告。为了理解外汇汇率波动如何影响收入,每日 FX 汇率数据流是回答这个问题的绝佳解决方案。
当与时间序列报告配合使用时,这也特别具有影响力,可以在特定销售时间附加汇率。在我构建显示客户收入随时间变化的仪表板的经验中,业务用户总是欣赏能够在日期轴上切换不同的外汇汇率值。FX Rates 表格可以让你完成所有这些操作。
Exchangerate.host 是另一个开源网站,允许你连接到每日 FX 汇率数据流。以下是如何检索数据的示例 — 更多信息请参见 他们的文档:
import requests
import pandas as pd
from datetime import date
#Retrieve the latest dates from the exchangerate api
url = 'https://api.exchangerate.host/latest?base=USD'
response = requests.get(url)
rates = response.json()
# Convert JSON list to a Pandas Dataframe & preview
rates_list = list(rates['rates'].items())
df_rates = pd.DataFrame(rates_list, columns=['currency', 'value'])
df_rates['cycle_date'] = date.today()
df_rates.head(10)

分解如下:
-
在这里,我们使用 requests 库从‘url’变量中指定的 URL 检索数据,数据形式为 Python 字典。请注意 — 我根据文档编辑了 URL,以指定基础货币为 USD。即,所有汇率将与其 USD 汇率相关。
-
接下来,我们将 Python 字典转换为列表,从‘rates’键中获取数据。
rates_list = list(rates['rates'].items())
- 然后,我们将列表转换为 Pandas DataFrame 并标记列标题:
df_rates = pd.DataFrame(rates_list, columns=['currency', 'value'])
- 最后,添加了一个名为 ‘cycle_date’ 的列,表示 ETL 周期日期,标示数据何时被摄取到数据仓库中。
结论
将公开可用的数据纳入数据仓库可以为分析团队提供即时价值,且付出最少的努力。这些表格以及任何适当建模的数据实体,消除了仅在像 Power BI 或 Tableau 这样的 BI 工具中存储嵌套业务逻辑的需求。它们提供了一个集中化的数据源,多个分析师可以参考并在报告中一致应用。这种数据建模的整体方法使团队能够轻松扩展报告,确保对源数据的透明度。通过利用这些类型的上下文表格,您的组织可以简化分析流程,消除报告中的不一致,并实现更高水平的数据驱动决策。
祝建模愉快!
解锁决策制定:人工智能桥接理论框架与技术进步
数据科学和人工智能如何帮助决策的简要概述
·发布于 Towards Data Science ·12 min read·2023 年 12 月 11 日
--

图片来源:Jake Melara 于 Unsplash
我们的生活充满了不断的决策和选择。由于我们的决策可能会带来相当大的经济和社会影响,决策研究从一开始就是高度跨学科的。自 20 世纪中叶以来,来自数学、社会学、心理学、经济学、政治学和计算机科学的科学家们一直在积极研究如何做出更好的决策。在这些领域中,最著名的成就包括预期效用理论、前景理论和博弈论。这些理论由数学模型驱动,但通常面临来自实际场景的经验测试的挑战。
随着计算能力的巨大提升和云技术的蓬勃发展,决策支持系统(DSS)与决策理论同步进步,以帮助人类做出决策,特别是在商业和组织环境中。一个典型的 DSS 包括可扩展的知识数据库,用于收集和存储大量信息,统计和分析算法,用于预测和投射,以及用户界面(包括图表和仪表板),供人类决策者可视化和互动决策过程。
然而,大多数人类决策是通过试错的增量学习得出的。迭代方法在处理新环境中的未知因素时特别有效。它需要探索新信息和评估错误以改进决策。值得注意的是,深度强化学习已经模拟了人类决策的试错性质,并在特定游戏中超越了人类玩家。
强化学习(RL)是从一开始就存在的机器学习领域之一。其重大突破发生在深度神经网络应用于模型之后。深度强化学习只是人工智能和深度学习革命化决策领域的一个例子。我们现在正处于一个人工智能为所有决策学科提供基础的时代,加快了对人类决策过程的理解,并进一步赋能人类做出数据驱动的理性决策。
决策过程与决策理论
决策是一个以选择行动方案为结束的过程。简单来说,这个过程有四个连续的阶段:评估、选项、评估和行动选择。每个阶段都依赖于前一个阶段的成功完成。
阶段 1:评估
第一个决策阶段是识别问题并提出决策。为了正确评估问题,我们应该理想地收集所有相关事实,识别当前状态与期望结果之间的差距,并最终确认是否需要做出决定。
问题评估对于理解环境、条件和可能的限制至关重要。收集到的信息越完整准确,接下来的决策阶段就会越顺利,确保做出更好的选择。跳过这个阶段可能会导致完全错误的决定,并带来不利的后果。文献和我们日常生活中的大量故事都教会我们一个教训:人们因为信息不完整和先前的无根据信念而匆忙做出错误决定。
阶段 2:选项
一旦问题得到确认并且所有前提信息已经收集完毕,下一阶段是制定不同的选项,然后做出最终决定。这一步涉及到基于以往知识和经验预测未来的结果。它包括发明、开发和分析决策备选方案,并在可能的情况下测试每个解决方案的可行性。
这个阶段需要技能和专业知识来做出准确的预测。可能需要工具或计算来制定选项。没有这些工具时,人脑使用想象力来基于记忆可视化未来的行动和潜在后果。时间因素也是这个阶段的一个因素:时间越久,准确预测的难度越大。
阶段 3:评估
面对多个不确定的选项时,大多数建立的决策理论都专注于如何评估这些替代方案,以及人类如何做出最终选择,基于每个选项应该有一个预期值的假设。在生物世界中,这个价值可能是减少饥饿、口渴或其他基本生存需求。在许多不同的情况下,这可能是奖励或惩罚。对金融或经济决策来说,最直接的价值是赚取或失去的金钱。
预期效用理论最初由瑞士数学家尼古拉斯·贝尔努利在 1713 年提出,后来由 20 世纪的其他经济学家和数学家完善。该理论提出,理性代理人应该选择具有最大预期效用的选项。预期效用是根据特定情况下某一行动可能达到的所有效用水平的加权平均值。实质上,效用反映了选择的预期值和风险。
让我们用一个简单的例子来看看预期效用理论如何运作。假设有两个情境,每个情境都有两个选项,我们让参与者选择每个情境中的最佳选项:
情境 1:
-
选项 1:100% 几率获得 450 美元
-
选项 2:50% 几率获得 1000 美元
情境 2:
-
选项 1:100% 几率损失 500 美元
-
选项 2:50% 几率损失 1100 美元
通过将概率与预期的美元价值相乘来推导每个选项的效用。根据预期效用理论,参与者应该在每个情境中选择效用最高的选项:情境 1 的第 2 个选项和情境 2 的第 1 个选项。
预期效用理论从经济学角度发展起来,以确保人们做出的选择与他们的目标一致。它还考虑了边际递减,即当金钱的价值增加时,效用的增加程度会减缓(例如,富人对额外的 100 美元的需求低于穷人)。
在 1979 年,心理学家丹尼尔·卡尼曼和阿莫斯·特沃斯基通过实验发现挑战了预期效用理论。他们的研究揭示了人们并不总是选择最大效用,而是根据价值和风险的相对性有倾向地选择更规避风险或寻求风险的选项。这些发现导致了前景理论的发展。
根据上述相同的例子,大多数参与者在情境 1 中选择选项 1,因为 450 美元是 100%确定的。在情境 2 中,大多数人选择了更具风险的选项 2,尽管它的效用值(550 美元)表示比另一个选项损失更多。换句话说,人们更愿意接受较小的效用收益以避免风险,但在面临损失时则愿意承担更多的风险。
因此,与期望效用理论中对收益和损失的对称最大效用计算相比,前景理论的曲线呈不对称的 S 形,负效用曲线明显比正部分陡峭(见下图)。简而言之,期望效用理论讲的是人们应该做什么以做出理性选择以获得最大效用。而前景理论解释了为什么人们往往由于偏好避免立即损失而不是追求长期收益,导致决策变得更差。

前景理论的曲线呈不对称的 S 形,因为人们对立即获得的收益有风险规避,而在面对损失时则会寻求额外的风险。图片来源于作者。
然而在现实生活中,期望值可能会变得模糊,因为它并不是我们人类思维的固有部分。价值可能依赖于幸福感或其他心理或社会因素。此外,在许多情况下,我们尚未确定每个结果的概率。决策理论的一般挑战在于,它们都具有仅适用于特定领域的假设,并且已经通过小样本进行测试。此外,期望值和概率不是静态的,而是依赖于许多因素,并可能相对于其他选项。决策者必须在每次这些因素或选项发生变化时重新评估效用。
第 4 阶段:行动选择
做出最终决定和选择行动方案有时与第 3 阶段不可分割。人类可以分配期望值,找到最大效用,并立即行动。然而,在许多其他情况下,最佳选项在评估后并不明显或不可用。我们常常遇到两种选择在补充或竞争的方式下提供相等的价值,并且选择一个与另一个相比成本似乎相当的情况。典型的困境可能使决策变得困难或使决策过程停滞不前。这时,人们会寻求他人(例如,家人或朋友)的帮助,或利用社会群体来决定。当一个特定问题有多个决策者时,博弈论已经发展起来以解决这种类型的决策困境(注:博弈论是社会决策中的一个重要领域,但超出了当前文章的范围)。
决策支持系统
深思熟虑的决策是人类智慧的一个重要功能。在面对新问题时,人类必须经历上述典型的四个决策阶段。然而,由于大脑容量的限制和潜在的偏见,我们的人脑往往难以合理化复杂的情况。自 20 世纪中叶以来,决策支持系统(DDS)应运而生,以扩展人类能力,帮助决策者做出更好的决策。DDS 的主要特点包括其知识系统和数据驱动的算法,用于进行预测和评估。
知识系统以扩展人类的记忆
我们需要了解世界以解决新问题。对相关事实的准确和完整的把握对于在决策的后续步骤中做出预测和预报至关重要。对于简单的生活问题,依靠我们对经验和知识的回忆可能已经足够。然而,对于更复杂的问题,我们的记忆并不是完全可信的,主要有两个原因:
首先,人类记忆并不是所有信息都能存储。人们只会关注他们注意到的方面,而忽略许多最初被认为不相关的细节。获取的短期记忆如果不被使用会逐渐衰退,只有一小部分被长期保存。有时,即使一个事实已经存储在大脑中,人们也可能在寻找它时遇到困难(例如,搜索问题)。
其次,我们的记忆会不断被修改和更新。我们的脑袋里没有单独的地方来存储我们经历的不同实例。它使用最初记录和处理信息的相同脑区来存储记忆。每当我们回忆或在脑海中重播时,相同的神经网络会被激活,突触的权重会进行调整。此外,由于神经网络的模式识别和联想性质,我们的大脑可以自动填补最初未被记录或观察到的细节。这解释了初始提问和在解决问题过程中隐性暗示如何扭曲人们的记忆以及影响他们的决策。
相反,计算机知识系统和数据库是忠实存储历史事实的工具,不会遗漏或扭曲。它们将原始事实和汇总信息进行有序存储,以便于访问和搜索。随着云计算的进步,各种类型的数据库可以存储不同种类的信息,并具有高扩展性、可靠性和性能。
先进的算法以提供更好的预测
对于深思熟虑的决策,人脑利用想象力来预测未来。尽管这非常强大,但我们的思维确实存在一些限制:
-
人类想象力的预测能力有限,因为它往往会重建并填补当前信息的空白。
-
许多预测需要非线性计算(例如,幂函数),这需要外部工具来帮助完成。
-
人脑使用工作记忆同时保持多个情境,以比较优缺点。工作记忆的容量有限,并且需要高度集中进行强度计算,这在长时间内不可持续。
相反,DDS(决策支持系统)利用数学、统计和机器学习算法,并借助强大的计算资源来帮助人类进行预测和预报。过去几十年中,众多算法和方法被开发、实施并成熟,这极大地帮助人类在各种行业和组织中做出更好的决策。
帮助人类做出理性决策
我们都知道情绪对我们的决策有着显著的影响,尤其是在选项评估过程中。例如,厌恶风险的情绪会导致偏见决策,这在前景理论中有所体现。人类的认知偏差和启发式也会导致主观评估和次优或错误选择。因此,数据分析可以通过利用效用理论及相关效用函数来缓解情绪的影响,克服潜在的偏见,使人们能够做出最佳选择。此外,DSS(决策支持系统)通常具有用户界面来呈现选项和重播情境,使决策过程更具互动性,帮助决策者做出无偏见的数据驱动决策。
未知的未知与强化学习
“…有已知的已知;我们知道我们知道的东西。我们也知道有已知的未知;也就是说,我们知道有些东西我们不知道。但也有未知的未知——那些我们不知道我们不知道的东西。”
– 唐纳德·拉姆斯菲尔德
决策的挑战在于未来的不确定性,尤其是与前美国国防部长唐纳德·拉姆斯菲尔德(2002 年新闻发布会上的名言)所提到的“未知的未知”相关。如果一切都是已知的(已知的已知),那么不再需要决策,因为结果已经确定。“已知的未知”通过选项的概率和风险来表示,这些概率和风险用于效用函数中,以识别具有最大效用的最佳选择。然而,“未知的未知”强调了无法预测或预报的结果的不确定性,它们只能在特定行动发生后被识别或发现。
试错法是处理未知未知问题的解决方案,人类实际上擅长这种方法。他们探索环境,做出最佳选择,并从结果中学习。这个过程不断迭代,直到他们能够做出完美的决策。这就是人类成为任何他们所做事情的高手的方式,包括决策。由于这种迭代学习模式,我们的大多数选择都是自动化的:一些是由自然完全固定的,如反射(例如,从火中撤回手,眨眼以避开强光),另一些是通过训练学习的结果(例如,赢得游戏,驾驶飞机)。
强化学习(RL)作为最古老的机器学习算法之一,使用与人类相同的试错方法进行学习。尽管 RL 最初在 1950 年代设计,但真正的革命发生在 2013 年,当时 Deep Mind 将深度神经网络应用于 RL,使得模型能够从零开始学习游戏,并最终在这些游戏中超越人类。最著名的例子是击败人类世界冠军的 AlphaGo。
深度强化学习模型具有与本文之前概述的类似决策过程。在模型中,代理进行观察、评估最佳路径、采取行动,并通过环境中的奖励获得反馈。随后,代理将其预期奖励与实际奖励进行比较,并利用差异来调整下一轮的行动。通过时间上的迭代调整,它将以最大化奖励的方式行动。值得注意的是,RL 使用“奖励”一词而非“效用”,因为奖励的计算与效用理论中的效用计算有所不同。

深度强化学习(Deep RL)将深度学习应用于典型的 RL 框架。图片来源:维基百科
深度强化学习(Deep RL)相较于决策支持系统(DSS)的优势在于,代理可以通过一系列传感器(如摄像头和触觉感应)自行探索环境和收集信息,并能够采取行动,如发送信号激活电机。换句话说,深度强化学习模仿了人类和动物的大脑,作为一个集成系统涵盖了从头到尾的决策过程,具备持续学习和改进的能力。
到目前为止,许多使用案例成功地利用了深度强化学习,包括游戏、自驾车、推荐系统、网络广告投放和交付、金融市场预测等。此外,人工智能和深度学习为在每个层级上连接碎片化的决策学科开辟了新的途径。以下是一些趋势,但肯定不是详尽无遗的:
-
研究人员使用深度学习算法来评估、测试和推进决策理论,基于大规模实验数据。
-
决策支持系统利用深度强化学习来替代或增强先前的算法,以进行更准确的预测和优化决策。
-
新的决策框架,如动态决策和鲁棒决策,已经出现,它们将传统的一次性繁重决策过程拆分为更小的迭代周期,并利用人工智能从以往的决策中学习并应对不确定性。
-
人工智能和深度神经网络已成为神经科学家研究人类决策机制和寻找预防人类做出如成瘾等错误决策方法的不可或缺的工具。
-
决策理论的进展和新的脑研究发现将帮助人工智能实现更自动化的决策。
结论
在不同的领域中,个人、组织和社会旨在做出与其目标和宗旨一致的最佳决策。鉴于决策在各种背景下的重要性,它已成为多个关键学科的研究对象。五位诺贝尔奖获得者因其在决策及相关的期望效用理论和前景理论上的重大贡献而获得认可。同时,数据科学家通过实施可扩展的决策支持和预测系统,成功地协助了人类决策者。深度强化学习在模拟甚至超越人类决策方面的最新突破标志着一个变革阶段,人工智能在整合决策学科、实现对人类决策过程的统一理解以及将各种决策系统提升到前所未有的自动化水平方面发挥了关键作用。
使用 Airflow 解锁 MLOps:ML 系统编排的全面指南
完整的 7 步 MLOps 框架
第四部分:私人 PyPi 服务器。用 Airflow 编排一切。
·发布于 Towards Data Science ·17 分钟阅读·2023 年 5 月 23 日
--

图片由 Hassan Pasha 提供,来源于 Unsplash
本教程代表7 节课程中的第四部分课,将一步一步指导你如何设计、实现和部署一个 ML 系统,并运用MLOps 好实践。在课程中,你将构建一个准备投入生产的模型,以预测丹麦多个消费者类型在接下来的 24 小时内的能源消耗水平。
在本课程结束时,你将理解如何使用批量服务架构设计、编码和部署 ML 系统的所有基础知识。
本课程针对中级/高级机器学习工程师,希望通过构建自己的端到端项目来提升技能。
如今,证书到处都是。构建可以展示的高级端到端项目是获得专业工程师认可的最佳方式。
目录:
-
课程介绍
-
课程内容
-
数据源
-
第四部分:私人 PyPi 服务器。用 Airflow 编排一切。
-
第四部分:代码
-
结论
-
参考资料
课程介绍
在这个 7 节课程结束时,你将知道如何:
-
设计批量服务架构
-
使用 Hopsworks 作为特征存储
-
设计一个从 API 读取数据的特征工程管道
-
构建一个包含超参数调整的训练管道
-
使用 W&B 作为 ML 平台跟踪你的实验、模型和元数据
-
实现一个批量预测管道
-
使用 Poetry 构建你自己的 Python 包
-
部署你自己的私人 PyPi 服务器
-
使用 Airflow 协调一切
-
使用预测来编写一个使用 FastAPI 和 Streamlit 的 web 应用程序
-
使用 Docker 对代码进行容器化
-
使用 Great Expectations 确保数据的验证和完整性
-
监控预测性能的变化情况
-
将所有内容部署到 GCP
-
使用 GitHub Actions 构建 CI/CD 管道
如果这听起来很多,不要担心。在你完成这门课程后,你将理解我之前说的所有内容。最重要的是,你将知道我为什么使用这些工具,以及它们如何作为一个系统协同工作。
如果你想从这门课程中获得最大的收益, 我建议你访问包含所有课程代码的 GitHub 仓库 。这门课程旨在让你快速阅读和复制文章中的代码。
到课程结束时,你将知道如何实现下图所示的内容。如果有什么不明白的地方,请不要担心。我会详细解释一切。

课程中你将构建的架构图 [作者提供的图片]。
到第四部分课结束时,你将知道如何托管你的 PyPi 仓库,并使用 Airflow 协调三个管道。你将学习如何调度管道以创建每小时的预测。
课程内容:
-
私有 PyPi 服务器。使用 Airflow 协调一切。
-
使用 GE 进行数据验证以确保质量和完整性。模型性能持续监控。
如果你想全面掌握这节课,我们建议你查看课程 1,课程 2 和课程 3,这些课程详细解释了你将在本文中协调的管道实现:
-
特征工程管道
-
训练管道
-
批量预测管道
数据源
我们使用了一个免费且开放的 API,该 API 提供了丹麦所有能源消费类型的每小时能源消耗值[1]。
它们提供了一个直观的界面,你可以轻松查询和可视化数据。你可以在这里访问数据 [1]。
数据具有 4 个主要属性:
-
小时 UTC: 数据点观察时的 UTC 日期时间。
-
价格区域: 丹麦被划分为两个价格区域:DK1 和 DK2——由大贝尔特海峡划分。DK1 位于大贝尔特海峡西侧,DK2 位于大贝尔特海峡东侧。
-
消费类型: 消费类型是由丹麦能源公司拥有和维护的行业代码 DE35。
-
总消耗: 总电力消耗(单位:千瓦时)
注意: 观察数据有 15 天的滞后!但对于我们的演示用例来说,这不是问题,因为我们可以模拟与实时相同的步骤。

我们的网络应用程序的屏幕截图,展示了我们如何预测区域 = 1 和消费类型 = 212 的能源消耗 [作者提供的图片]。
数据点具有每小时的分辨率。例如:“2023–04–15 21:00Z”,“2023–04–15 20:00Z”,“2023–04–15 19:00Z”等。
我们将数据建模为多个时间序列。每个唯一的价格区域和消费类型组合表示一个唯一的时间序列。
因此,我们将建立一个独立预测未来 24 小时每个时间序列的能源消耗的模型。
查看下面的视频,以更好地理解数据的样子 👇
课程与数据源概述 [作者提供的视频]。
课程 4:私人 PyPi 服务器。用 Airflow 协调一切。
第四部分课的目标
这节课将教你如何使用 Airflow 来协调你迄今为止实现的三个管道。
此外,要运行 Airflow 中的代码,你将学习如何托管你的 PiPy 仓库,并将管道部署为 3 个不同的 Python 模块。之后,你将直接从你的 PiPy 仓库中安装这些模块到 Airflow 中。

第 4 课组件用蓝色高亮的最终架构图 [作者提供的图片]。
通过使用 Airflow 编排所有内容,你将自动化整个过程。你不再需要手动运行 10 个不同的脚本,而只需点击一次“运行”按钮即可运行整个代码。
同时,以编程方式将所有步骤连接起来的程序更不容易出现错误。
为什么?
因为每个脚本都需要自己的配置。例如,批量预测管道需要特征视图版本(数据版本)和模型版本作为输入。
这些信息是从之前的脚本生成的元数据。当你手动运行一切时,容易复制错误的版本。但当你将所有内容封装在一个 DAG 中时,你只需构建一次,之后它将始终正常工作。
此外,通过使用 Airflow,你可以:
-
定期调度管道运行(你将每小时运行一次);
-
使用 Airflow 变量配置整个过程;
-
监控每个任务的日志。
这里是你将在 Airflow 中构建的概览 👇
理论概念与工具
Airflow: Airflow 是最受欢迎的编排工具之一。这个项目最初在 Airbnb 开发,但现在在 Apache 许可证下开源。这意味着你可以免费修改和托管它。Airflow 允许你构建、调度和监控 DAG。
DAG(有向无环图): DAG 是一种没有循环的图,这意味着逻辑流只能朝一个方向进行。
PyPi 注册表: PiPy 注册表是一个可以托管各种 Python 模块的服务器。当你运行“pip install <your_package>”时,pip 知道如何查看官方 PyPi 仓库中的你的包并安装它。托管自己的 PyPi 注册表的行为完全相同,但你必须配置 pip 以知道如何访问它。只有访问你 PyPi 服务器的人才能从中安装包。
第 4 课:代码
注意: 所有的安装说明都在仓库的 README 文件中。这里你将直接跳转到代码部分。
第 4 课中的所有代码都位于 airflow 文件夹下。
airflow 文件夹下的文件结构如下:

显示 airflow 文件夹结构的截图 [作者提供的图片]。
所有代码都位于dags 目录下。 每个 DAG 都有其自己的 Python 文件。
Docker 文件将帮助你快速托管 Airflow 和 PiPy 仓库。我会在后面详细解释。
直接将凭证存储在你的 git 仓库中是一个巨大的安全风险。这就是为什么你将使用.env文件来注入敏感信息。
.env.default是你必须配置的所有变量的示例。它也有助于存储不敏感的属性的默认值(例如,项目名称)。

.env.default 文件的截图 [作者提供的图片]。
准备凭证
由于第四部分课讨论了如何协调其他所有课程中的代码,如果你想重现代码,你需要检查如何设置第一部分、第二部分和第三部分中的 3 个管道。
这三节课将展示如何设置所有必要的工具和服务。还会展示如何创建并完成包含所有凭证的所需.env 文件。

.env.default 文件的截图 [作者提供的图片]。
唯一需要注意的是 👇
这次你需要将包含凭证的.env文件放置在airflow/dags文件夹下。
我们在 docker-compose.yaml 文件中为ML_PIPELINE_ROOT_DIR环境变量设置了默认值/opt/airflow/dags。因此,在 Airflow 内部运行管道时,它将默认从/opt/airflow/dags加载.env文件。
同时,请注意在/ airflow文件夹下还有另一个.env文件。这个文件不包含你的自定义凭证,但 Airflow 需要一些自定义配置。它的样子如下 👇

/airflow 文件夹中的.env 文件截图 [作者提供的图片]。
我在仓库的README.md中解释了如何完成这个.env文件。但作为附注,AIRFLOW_UID代表你计算机的用户 ID,而你知道ML_PIPELINE_ROOT_DIR是什么。
我只是想向你展示你可以在这里覆盖ML_PIPELINE_ROOT_DIR的默认值。请注意,这个路径将在 Docker 容器内使用,因此路径以/opt/开头。
# Move to the airflow directory.
cd airflow
# Make expected directories and environment variables
mkdir -p ./logs ./plugins
sudo chmod 777 ./logs ./plugins
# It will be used by Airflow to identify your user.
echo -e "AIRFLOW_UID=$(id -u)" > .env
# This shows where the project root directory is located.
echo "ML_PIPELINE_ROOT_DIR=/opt/airflow/dags" >> .env
设置私人 PyPi 服务器
你可以使用 这个仓库 容易地托管一个 PiPy 服务器。不过让我解释一下我们在设置中是如何做的。
第一步是创建一组凭证,这些凭证是你连接到 PyPi 服务器所需的。
# Install dependencies.
sudo apt install -y apache2-utils
pip install passlib
# Create the credentials under the energy-forecasting name.
mkdir ~/.htpasswd
htpasswd -sc ~/.htpasswd/htpasswd.txt energy-forecasting
PyPi 仓库将知道从 ~/.htpasswd/htpasswd.txt 文件中加载凭证。
现在,你将把新的私有 PyPi 仓库添加到 Poetry 中。要配置 Poetry,你需要指定服务器的 URL、服务器名称以及用于认证的用户名和密码(这些是你之前配置的):
poetry config repositories.my-pypi http://localhost
poetry config http-basic.my-pypi energy-forecasting <password>
在我们的示例中:
-
服务器名称: my-pypy
-
URL:
localhost -
用户名: energy-forecasting
-
密码:
检查你的 Poetry auth.toml 文件中的凭证设置是否正确:
cat ~/.config/pypoetry/auth.toml
你已经准备好了用户名和密码,这些将被你的 PyPi 仓库用于认证。同时,你也配置了 Poetry 以识别你的 PyPi 服务器。
现在,让我们看看如何运行 PyPi 服务器。
你将使用的 pyserver 代码 已经进行了 Docker 化。
为了简化,我们将 PyPi 服务器作为额外的服务添加到运行 Airflow 应用程序的 docker-compose.yaml 文件中。
为了更好地理解 docker-compose.yaml 文件,请查看 Airflow 官方文档 [2] 和 我们的 README.md。但请注意使用我们仓库中的 docker-compose.yaml 文件,因为我们修改了原始文件,正如下文所示。
滚动到 airflow/docker-compose.yaml 文件的底部,你会看到:
my-private-pypi:
image: pypiserver/pypiserver:latest
restart: always
ports:
- "80:8080"
volumes:
- ~/.htpasswd:/data/.htpasswd
command:
- run
- -P
- .htpasswd/htpasswd.txt
- --overwrite
这段代码使用了 PyPi 服务器的最新镜像,将服务器暴露在 80 端口,将包含你凭证的 ~/.htpasswd 文件夹作为卷加载,并用以下命令运行服务器:
run -P .htpasswd/htpasswd.txt --overwrite
-
"-P .htpasswd/htpasswd.txt" 明确告诉服务器使用哪些凭证。
-
"— overwrite" 表示如果部署了相同版本的新模块,它将覆盖上一个版本。
就这样!当你运行 Airflow 应用程序时,你会自动启动 PyPi 服务器。
注意: 在生产环境中,你可能会将 PyPi 服务器托管在与 Airflow 不同的服务器上。步骤是相同的,只是将所有内容添加到单个 docker-compose.yaml 文件中。在本教程中,我们希望一切运行起来更加简单。
自定义 Airflow Docker 文件
由于你必须在 Python 3.9 中运行所有代码,因此你需要继承默认的 apache/airflow:2.5.2 Airflow Docker 镜像,并添加一些额外的依赖项。
以下是 Docker 文件中的内容:
-
继承 apache/airflow:2.5.2
-
切换到 root 用户以安装系统依赖项
-
安装从私有 PyPi 服务器安装包所需的 Python 3.9 依赖项
-
切换回默认用户
因为我们切换了:
x-airflow-common:
&airflow-common
image: ${AIRFLOW_IMAGE_NAME:-apache/airflow:2.5.2}
到:
version: '3.8'
x-airflow-common:
&airflow-common
# image: ${AIRFLOW_IMAGE_NAME:-apache/airflow:2.5.2}
build: .
Docker 会知道在运行 docker-compose 时使用你的自定义镜像,而不是apache/airflow:2.5.2。
运行 Airflow
现在你了解了如何准备凭据以及 Docker 文件如何工作,前往./airflow目录并运行:
# Go to the ./airflow directory.
cd ./airflow
# Initialize the Airflow database
docker compose up airflow-init
# Start up all services
# Note: You should set up the private PyPi server credentials before running this command.
docker compose --env-file .env up --build -d
完成 Airflow 设置后,你可以使用默认凭据在127.0.0.1:8080访问 Airflow:
-
用户名: airflow
-
密码: airflow

Airflow 登录页面的截图 [作者提供的图片]。
将模块部署到私有 PyPi 服务器
记住你使用以下命令将新的 PyPi 服务器添加到 Poetry 中:
poetry config repositories.my-pypi http://localhost
poetry config http-basic.my-pypi energy-forecasting <password>
现在,使用my-pypi作为标识符,你可以快速将新包推送到你的 PyPi 仓库。
使用deploy/ml-pipeline.sh脚本,你可以仅使用 Poetry 构建和部署所有 3 个管道:
!/bin/bash
# Build and publish the feature-pipeline, training-pipeline, and batch-prediction-pipeline packages.
# This is done so that the pipelines can be run from the CLI.
# The pipelines are executed in the feature-pipeline, training-pipeline, and batch-prediction-pipeline
# directories, so we must change directories before building and publishing the packages.
# The my-pypi repository must be defined in the project's poetry.toml file.
cd feature-pipeline
poetry build
poetry publish -r my-pypi
cd ../training-pipeline
poetry build
poetry publish -r my-pypi
cd ../batch-prediction-pipeline
poetry build
poetry publish -r my-pypi
如你所见,我们迭代地进入 3 个管道的文件夹并运行:
poetry build
poetry publish -r my-pypi
Poetry 使用这两个命令在文件夹中查找pyproject.toml和poetry.lock文件,并知道如何构建包。
然后,根据生成的wheel文件,运行"poetry publish -r my-pypi",你将其推送到你的my-pipy仓库。
记住你将你的 PyPi 服务器标记为my-pipy。
完成了。你拥有了自己的 PyPi 仓库。
在接下来的章节中,我将向你展示如何从你的私有 PyPi 仓库中安装包。
注意: 你只使用 Poetry 来构建和部署模块。Airflow 将使用 pip 从你的 PyPi 仓库中安装这些模块。
定义 DAG 对象
你的 dag 定义在airflow/dags/ml_pipeline_dag.py文件中。
使用Airflow 2.0 的 API,你可以使用dag() Python 装饰器定义一个 DAG。
你的 dag 将在ml_pipeline()函数中定义,该函数在文件末尾调用。此外,Airflow 知道加载airflow/dags目录下定义的所有 DAG。
DAG 具有以下属性:
-
dag_id: DAG 的 ID
-
schedule: 它定义了 DAG 运行的频率
-
start_date: 根据给定的时间表,DAG 应该何时开始运行
-
catchup: 自动填补[start_date, 现在]之间的时间
-
tags: 标签 😄
-
max_active_runs: 此 DAG 可以并行运行的实例数
定义任务
下面的代码可能看起来很长,但一旦你理解了主要思路,它很容易阅读。
在 DAG 中,你定义了多个任务。一个任务是一个单独的逻辑单元/步骤,执行特定的操作。
任务的定义类似于 DAG:一个函数+一个装饰器。每个任务都有其函数和装饰器。
注意: 这是一个简单的提醒,我们使用的是 Airflow 2.0 的 API,而不是 1.0 的。
在我们的案例中,一个任务将代表一个主管道脚本。例如,特征工程管道将在一个单独的任务中运行。
你将使用 DAG 将所有脚本粘合在一个“程序”下,每个脚本与一个任务有 1:1 的对应关系。

"ml_pipeline"的视觉表示(请参见 YouTube 视频以获取更好的视图) [作者提供的图像]。
正如你在每个任务中看到的,你只需从其模块中导入并调用函数……并可能添加一些额外的日志。
定义任务的关键步骤在于task.virtualenv() Python 装饰器的参数中。
对于每个任务,这个特定的装饰器将创建一个不同的 Python 虚拟环境,在其中安装所有给定的需求。
注意: 172.17.0.1 是你私有 PyPi 仓库的 IP 地址。记住,你通过 docker-compose 在与 Airflow 相同的网络下托管你的 PyPi 仓库。172.17.0.1 是每个 Docker 容器在default Docker网络内可以访问的桥接 IP 地址。因此,Airflow 容器可以通过桥接 IP 地址访问 PyPi 服务器容器。
正如在requirements参数中所示,我们定义了以下内容:
-
"— trusted-host 172.17.0.1": 由于 PyPi 服务器没有用 HTTPS 保护,你必须明确表示你信任这个来源。
-
"— extra-index-url http://172.17.0.1": 告诉 Pip 在搜索 Python 包时也查看这个 PyPi 仓库。请注意,Pip 在搜索时仍会查看官方 PyPi 仓库。
-
"<your_python_packages>": 在上述两行之后,你可以添加任何 Python 包。但请注意,你已经安装了feature_pipeline、training_pipeline和batch_prediction_pipeline作为你用 Poetry 构建和部署的 Python 包。
其他参数并不是那么有趣,但让我来解释一下:
-
task_id=" <task_id>": 任务的唯一 ID。
-
python_version=" 3.9": 当我写这门课程时,Hopsworks 只支持 Python 3.9,所以我们必须强制使用这个版本的 Python。
-
multiple_outputs=True: 任务返回一个 Python 字典。
-
system_site_packages=True: 安装默认的系统包。
重要
注意,几乎每个任务都会返回一个包含信息的元数据字典,例如:
-
数据提取的日期范围,
-
特征组、特征视图等的版本。
-
sweep 的版本。
-
模型的版本等。
这些信息在任务之间传递是至关重要的。例如,create_feature_view 任务需要知道使用哪个版本的 feature_group 来创建下一个特征视图。此外,在运行 batch_predict 时,你需要知道要使用哪个版本的特征视图和模型来生成预测。
一个有趣的任务是 task.branch(task_id=" if_run_hyperparameter_tuning_branching"),它定义了一个是否运行超参数调整逻辑的 if-else 逻辑。
这种特殊类型的任务返回一个 task_ids 列表,这些任务将会被执行。例如,如果它返回 ["branch_run_hyperparameter_tuning"],则仅运行 task_id = branch_run_hyperparameter_tuning 的任务。
如下所示,定义了两个空的操作符(任务),并在 task.branch() 逻辑中使用了 task_ids。这是 Airflow 建议的一种常见模式,当在多个分支之间进行选择时,使用一组空操作符(无操作)。
将任务连接成一个 DAG
现在你已经定义了所有的任务,最后一步是将它们连接成一个 DAG。你必须执行这一步,以便 Airflow 知道每个任务的运行顺序。
基本上,这里你将定义逻辑图。
#1. 第一步是 确定你将用于配置 DAG 的变量集合,如 days_delay, days_export, feature_group_version, 等。你可以从 Airflow 的“Admin -> Variables”面板中访问这些变量。
请注意,你必须显式地使用蓝色加号按钮添加它们。

变量 Airflow 面板的截图 [作者提供的图片]。
#2. 第二步是 调用具有正确参数的任务。如你所见,由于 Airflow 2.0 API,这一步就像按特定顺序调用一系列 Python 函数一样。
注意: 如果一个函数的输出作为输入添加到另一个函数中,则图中的依赖关系会自动创建。
强调如何将每个管道元素的元数据传递到下一个元素是至关重要的。这样,我们强制执行以下脚本,以使用正确的数据和模型版本。
我还想强调以下这段代码:
feature_pipeline_metadata = run_feature_pipeline(
export_end_reference_datetime="{{ dag_run.logical_date }}",
)
“{{ dag_run.logical_date }}" 是 Airflow 注入的模板变量,反映了 DAG 运行时的逻辑日期,而不是当前日期。通过这样做,利用 Airflow 回填功能,你可以轻松地将其作为日期时间参考来回填给定时间窗口中的数据。现在你可以轻松地操作提取窗口的起始和结束点。
例如,如果你想运行 DAG 以回填 2023 年 5 月 10 日至 11 日的能源消耗预测,你将使用“2023 年 5 月 10 日 00:00 am”日期运行 Airflow 回填逻辑。
#3. 最后一步是使用 ">>" 操作符来强制执行特定的 DAG 结构。
"A >> B >> C" 意味着先运行 A,然后 B,再运行 C。
唯一稍微复杂一点的代码是这个:
>> if_run_hyperparameter_tuning_branch
>> [
if_run_hyperparameter_tuning_branch
>> Label("Run HPO")
>> branch_run_hyperparameter_tuning_operator
>> last_sweep_metadata
>> upload_best_model_step,
if_run_hyperparameter_tuning_branch
>> Label("Skip HPO")
>> branch_skip_hyperparameter_tuning_operator,
]
,其中根据branch运算符,DAG 将运行branch_run_hyperparameter_tuning_operator或branch_skip_hyperparameter_tuning_operator分支。
阅读有关 Airflow 中分支的更多信息这里 [3]。
以英语运行时,它将进行超参数优化或跳过,如下图所示——我知道图片比较小。查看视频以获取更清晰的视图。 👇

ml_pipeline DAG 截图 [作者提供的图片]。
就这样!你使用 Airflow 协调了所有 3 个管道。恭喜!
运行 ML Pipeline DAG
这一步很简单。
只需进入你的ml_pipeline DAG 并点击播放按钮。

ml_pipeline DAG 视图截图 [作者提供的图片]。
使用 Airflow 进行回填
查找你的airflow-webserver docker 容器 ID:
docker ps
在airflow-webserver容器内启动 shell 并运行airflow dags backfill,如下所示:
docker exec -it <container-id-of-airflow-webserver> sh
# In this example, you did a backfill between 2023/04/11 00:00:00 and 2023/04/13 23:59:59.
airflow dags backfill --start-date "2023/04/11 00:00:00" --end-date "2023/04/13 23:59:59" ml_pipeline
如果你想清除任务并重新运行它们,请运行以下命令:
docker exec -it <container-id-of-airflow-airflow-webserver> sh
airflow tasks clear --start-date "2023/04/11 00:00:00" --end-date "2023/04/13 23:59:59" ml_pipeline
结论
恭喜!你完成了第四课来自全栈 7 步 MLOps 框架课程。
如果你已经看到这里,你知道如何:
-
托管自己的 PyPi 服务器
-
使用 Poetry 构建和部署 Python 模块
-
使用 Airflow 协调多个管道
现在你已经理解了使用像 Airflow 这样的协调工具的强大功能,你可以构建强大的生产就绪管道,并快速调度、配置和监控。
查看第 5 课了解如何使用 Great Expectations 验证数据的完整性和质量。此外,你将了解如何在机器学习系统上实现监控组件。
此外, 你可以在这里访问 GitHub 仓库。
💡 我的目标是帮助机器学习工程师提升设计和生产机器学习系统的能力。关注我在LinkedIn或订阅我的每周通讯以获取更多见解!
🔥 如果你喜欢阅读这样的文章并希望支持我的写作,考虑成为 Medium 会员。通过使用我的推荐链接,你可以在不增加任何额外费用的情况下支持我,同时享受 Medium 丰富故事的无限访问权限。
[## 使用我的推荐链接加入 Medium - Paul Iusztin
🤖 加入以获取有关设计和构建生产就绪机器学习系统的独家内容 🚀 解锁全部访问权限…
参考资料
[1] 丹麦 API 每小时 DE35 行业代码的能耗,丹麦能源数据服务
[2] 在 Docker 中运行 Airflow,Airflow 文档
[3] Airflow 中的分支,Astronomer 上的 Airflow 文档
释放 JupyterLab 的潜力:发现你从未知道过的强大文本编辑器
在 JupyterLab 中通过一个优秀的文本编辑器释放你的编码效率和生产力
·发布在 Towards Data Science ·6 分钟阅读·2023 年 3 月 1 日
--

图片由 Luca Bravo 提供,发布在 Unsplash
本文是一个系列的第一部分。查看完整系列:第二部分、第三部分、第四部分。
JupyterLab 是一个开源的基于网页的交互式编码环境,许多人认为它并不是一个完整的 IDE。支持这一观点的众多原因之一是 JupyterLab 缺乏一个强大的文本编辑器。
全面性、让你保持专注的工具和教育是 2022 年 Project Jupyter 的主要主题
towardsdatascience.com
JupyterLab 允许用户创建和分享包含实时代码、方程式、可视化和叙述性文本的文档。它提供了一个灵活而强大的平台,用于科学计算、数据分析和机器学习工作流,同时支持用户在各种环境中运行代码,从本地机器到远程服务器和云服务。
然而,许多数据科学家和工程师仅仅将其用于实验。当需要将他们的工作投入生产时,他们会转向 IDE 来将所有部分整合起来。
现如今最受欢迎的 IDE 是 Microsoft Visual Studio Code。VS Code 等工具提供了一个强大的文本编辑器、一个调试器以及多个插件,使得使用 Git、Docker 或几乎任何编程语言变得非常轻松。但如果我告诉你,你可以在 JupyterLab 中获得类似的体验,你会怎么想?
在这一系列文章中,我们将会在 JupyterLab 中打造一个类似于 VC Code 的体验,并创建一个 Docker 镜像,这样我们就可以在任何地方使用它作为我们的自定义工作区。让我们开始吧!
这个故事的第二部分已经发布:
## Jupyter 已经有一个完美的文本编辑器:这就是你可以如何配置它
如何在 Jupyter 中获得类似 VS Code 的体验以及一个出色的文本编辑器
[towardsdatascience.com
Learning Rate 是一本针对对机器学习和 MLOps 领域感兴趣的人的新闻通讯。如果你想了解更多类似的主题,可以点击这里订阅。每个月的最后一个星期日,你将会收到我关于最新 MLOps 新闻和文章的更新和想法!
你为什么需要关心?
在我们开始之前,让我们先回答一个问题:你为什么需要关心这个问题?你已经有了 Visual Studio Code;为什么还要在 JupyterLab 中构建类似的体验?此外,VS Code 已经为 Jupyter Notebooks 提供了一个很好的编辑器。事实上,我通常觉得 VS Code 的 Notebooks UI 更好。
好吧,如果你本地编程,从不通过 ssh 连接到远程机器或使用不同的工作站来完成工作,那么你可能不需要这个。
然而,如今大多数人需要连接到远程服务器才能完成任务。例如,我使用 Kubeflow 来设计和运行我的 ML 实验和管道。Kubeflow 中的编码环境通常是一个 Jupyter 服务器。
那么,在这种情况下你会怎么做?拥有一个 VS Code 服务器可能是具有挑战性的。你要么需要付费订阅,要么学会接受code server(尽管是一个很棒的项目)的局限性。有没有替代方案,最好是开源的?
这个故事是为你准备的。在这一系列的最后,我们将在 JupyterLab 中建立一个强大的 Python 编码环境。将其扩展以支持更多编程语言将是微不足道的。
Jupyter 已经有了一个完美的文本编辑器
可能会让人惊讶的是,JupyterLab 已经有一个很棒的文本编辑器。为什么呢?因为 Jupyter 有一个终端模拟器;因此,我们可以安装 Neovim。然后,我们可以将 Neovim 配置为我们想要的样子和行为。
本系列文章的终极目标是安装 Neovim,并使其像 Python IDE 一样工作。相信我,到最后,你将拥有从 VS Code 迁移到通用文本编辑器所需的核心组件,一个可以带到任何地方的编辑器。
完成的体验大致如下:

图片来自作者
我们看到一个完整的 IDE 体验,包含文件浏览器、代码大纲、集成终端、代码检查和带有代码补全和文档弹出的 IntelliSense!我们甚至有像美丽的状态栏和根据文件内容显示图标等小特性。
在未来的文章中,我们将探索每一个隐藏的宝石,比如多行注释、拼写检查器、窗格导航以及以 VS Code 原生方式配置插件。首先,让我们安装 Neovim 并创建我们的初始配置。
在 JupyterLab 中安装 Neovim
在 Jupyter 中创建类似 VS Code 的体验的第一步是运行一个可以配置的 Jupyter 服务器。我们将使用上游的数据科学镜像,从 DockerHub 拉取。因此,准备好后,请运行以下命令:
docker run --name jupyter \
-p 8888:8888 \
--user root \
-e GRANT_SUDO=yes \
jupyter/datascience-notebook:latest
我们需要 sudo 权限,因为我们将安装各种 Debian 软件包和插件。我们传递的参数将授予默认用户(jovyan)无密码的 sudo 权限。有关更多信息,请参见 文档。
接下来,在 JupyterLab 中启动终端并安装最新版本的 Neovim:
wget https://github.com/neovim/neovim/releases/download/stable/nvim-linux64.deb && sudo dpkg -i nvim-linux64.deb
为了验证一切是否如预期那样工作,请运行 nvim。在撰写本文时,最新的稳定版本是 v0.8.3:

图片来自作者
学习 Vim
Neovim 基于 Vim,是一个强大且高度可定制的文本编辑器,主要用于类 Unix 操作系统。它设计为完全通过键盘使用,命令基于一系列按键和助记符缩写的组合。
那么,为什么选择 Neovim 而不是 Vim 呢?Neovim 是 Vim 的一个分支,这意味着它基于与 Vim 相同的核心代码,但具有额外的功能和改进。Neovim 的主要目标是提供更好的扩展性、更现代的开发实践和比 Vim 更可维护的代码库。
Neovim 相对于 Vim 有许多优点,但列举这些优点并不是本文的目标。不过,如果你知道如何在 Vim 中导航,你可以将这些知识转移到 Neovim 中。因此,让我们看看如何学习 Vim!
网上有很多资源;然而,在我看来,最好的方式是 vimtutor。首先,安装 Vim:
sudo apt install vim
然后,在终端中运行 vimtutor:

图片来自作者
vimtutor 有七个课程,涵盖了 Vim 的基本概念。完成这个互动教程将为你提供开始使用 Vim 和 Neovim 所需的所有工具。
在这篇文章中,我留给你们一句话:学习你的 Vim 绑定!如果你忘记了一些按键,不用担心;练习会让一切变得更简单。
在接下来的文章中,我们将开始配置 Neovim。我们将从配置编辑器的核心功能和外观开始,然后深入探讨更多专业插件,这些插件将把我们的编辑器转变为功能丰富的 Python IDE。
关于作者
我叫 Dimitris Poulopoulos,是一名在 Arrikto 工作的机器学习工程师。我为欧洲委员会、Eurostat、国际货币基金组织、欧洲中央银行、OECD 和宜家等主要客户设计和实施了 AI 和软件解决方案。
如果你有兴趣阅读更多关于机器学习、深度学习、数据科学和 DataOps 的文章,可以在 Medium、LinkedIn 或 Twitter 上关注 @james2pl。
表达的观点仅代表我个人,不代表我雇主的观点或意见。
解锁大数据的力量:图学习的迷人世界
利用深度学习将未开发的数据转变为长期竞争力的战略资产。
·
关注 发表在 Towards Data Science ·12 分钟阅读·2023 年 11 月 9 日
--

图片由 Nathan Anderson 提供,Unsplash 上的照片
大公司生成和收集大量数据,以一个例子来说,90% 的数据是在最近几年创建的。然而,73% 的这些数据仍然未被使用 [1]。然而,正如你可能知道的那样,数据是从事大数据工作的公司的金矿。
深度学习不断发展,今天的挑战是将这些新解决方案调整到特定目标,以突出表现并增强长期竞争力。
我以前的经理很有直觉,认为这两个事件可以结合在一起,共同促进访问、请求,最重要的是避免浪费时间和金钱。
为什么这些数据被闲置?
访问这些数据需要很长时间,权利验证,特别是内容检查在授予用户访问权限之前是必需的。

可视化数据未被使用的原因。(由 Bing Image Creator 生成)
有没有自动记录新数据的解决方案?
如果你对大型企业不太熟悉,也没关系——我也是。这样环境中的一个有趣概念是使用大数据,特别是HDFS(Hadoop 分布式文件系统),它是一个旨在整合公司所有数据的集群。在这庞大的数据池中,你可以找到结构化数据,而在这些结构化数据中,Hive 列被引用。这些列中的一些用于创建附加表,并可能作为各种数据集的来源。公司通过数据血缘保持表与表之间的信息。
这些列还具有各种特征(领域、类型、名称、日期、所有者等)。项目的目标是用业务数据来记录被称为物理数据的数据。
区分物理数据和业务数据:
简而言之,物理数据是表格中的列名,而业务数据则是该列的使用方式。
例如:名为 Friends 的表格包含列(角色、薪资、地址)。我们的物理数据是角色、薪资和地址。我们的业务数据例如,
-
对于“Character” -> 角色的名称
-
对于“Salary” -> 薪资金额
-
对于“Address” -> 人的地址
这些业务数据将帮助访问数据,因为你可以直接获得所需的信息。你会知道这是你项目所需的数据集,你寻找的信息 在这个表格中。所以你只需请求并找到你的幸福,提前去而不浪费时间和金钱。
“在我的最后一次实习中,我和我的团队 实习生,实现了一个大数据/图学习解决方案来记录这些数据。
这个想法是创建一个图来结构化我们的数据,并最终根据特征预测业务数据。换句话说,从存储在公司环境中的数据中,记录每个数据集以关联用途,将来减少搜索成本,并更具数据驱动。
我们有 830 个标签需要分类,但行数不多。希望图学习的力量能发挥作用。我让你阅读...“
文章目标: 本文旨在提供对大数据概念、图学习、使用的算法及结果的理解。它还涉及部署考虑事项以及如何成功开发模型。
为了帮助你理解我的过程,本文的提纲包括:
-
数据采集:为图创建获取必要数据
-
基于图的建模与 GSage
-
有效的部署策略
数据采集
正如我之前提到的,数据通常存储在 Hive 列中。如果你还不知道,这些数据存储在大型容器中。我们通过称为 ETL 的技术提取、转换和加载这些数据。
我需要什么类型的数据?
-
物理数据及其特征(领域、名称、数据类型)。
-
谱系(物理数据之间的关系,如果它们经历了共同的转换)。
-
将“某些与业务数据相关的物理数据”进行映射,然后“让”算法自行执行。
1. 特征/特性在我们存储数据时直接获得;它们在我们存储数据时是必需的。例如(取决于你的情况):

主要特征的示例,(由作者提供)
对于特征,基于经验,我们决定对三列使用特征哈希器。
特征哈希器: 机器学习中用于将高维度的分类数据(如文本或分类变量)转换为低维度数值表示的技术,以减少内存和计算需求,同时保留有意义的信息。
如果你有类似的模式,可以选择独热编码技术。如果你想交付你的模型,我建议使用特征哈希器。
2. 谱系稍微复杂一些,但并非不可理解。谱系就像物理数据的历史,我们大致了解已应用的转换以及数据存储在其他地方的位置。
想象一下你脑海中的大数据以及所有这些数据。在一些项目中,我们使用表格中的数据,并通过一个任务(Spark)进行转换。

从 Atlas 网站可视化的 Atlas 谱系,LINK
我们收集了所有物理数据的信息,以便在我们的图中创建连接,或者至少创建其中一个连接。
3. 映射是为我们的项目增值的基础。在这里,我们将业务数据与物理数据关联起来。这为算法提供了经过验证的信息,使其能够最终对新进来的数据进行分类。这个映射必须由了解公司流程的人完成,并且具备识别复杂模式的技能,而无需询问。
ML 建议,基于我自己的经验:
引用 Andrew NG 先生的话,在经典机器学习中,有一种叫做算法生命周期的东西。我们常常考虑算法,使其复杂,而不只是使用一个老式的线性回归(我尝试过;它不起作用)。在这个生命周期中,包括所有的预处理、建模和监控阶段……但最重要的是,数据聚焦。
这是我们经常犯的错误; 我们理所当然地开始进行数据分析。我们从数据集中得出结论,而有时却没有质疑其相关性。不要忘记数据聚焦,我的朋友们;它可以提升你的表现,甚至导致项目的改变 😃
回到我们的文章,获取数据后,我们最终可以创建我们的图。

我们数据集的分布图的绘图(使用networkx制作)。(由作者制作)
该图考虑了一批 2000 行的数据,因此在数据集和表格中有 2000 列。你可以在中心找到业务数据,而偏离中心的是物理数据。
在数学中,我们将图表示为 G,G(N, V, f)。N 代表节点,V 代表顶点(边),f 代表特征。假设这三者都是非空集合。
对于节点(我们在映射表中有业务数据 ID)以及物理数据,以便通过谱系追踪它们。
谈到谱系,它部分作为边,通过映射和 ID 链接我们已有的链接。我们必须通过使用Apache Atlas API的 ETL 过程来提取它。
你可以看到,在奠定基础之后,大数据问题可以变得容易理解,但实施起来却更具挑战性,尤其对于一名年轻的实习生来说……

“计算机上的忍者卡通”(由 Dall.E 3 生成)
基于图的建模与 GSage
图学习基础
本节将致力于解释 GSage 以及为何在数学和经验上都选择了它。
在这次实习之前,我不习惯使用图形。这就是为什么我购买了书籍[2],我在描述中包含了它,因为它大大帮助我理解了原理。
[## 图机器学习:通过应用机器学习技术将图数据提升到一个新的水平……
注意 /5。请查阅《图机器学习:通过应用机器学习技术将图数据提升到一个新的水平》……
原理很简单:当我们谈论图学习时,我们必然会讨论嵌入。在这个背景下,节点及其邻近关系在数学上被转换为减少原始数据集维度的系数,使计算更加高效。在降维过程中,解码器的一个关键原则是保持初始接近的节点之间的邻近关系。
另一个灵感来源是 Maxime Labonne [3],他对 GraphSages 和图卷积网络的解释展示了极佳的教学法,提供了清晰易懂的例子,使这些概念对希望深入了解的人变得更加易于理解。
GraphSage 的模型
如果这个术语对你来说不熟悉,请放心,几个月前我也在你的位置。像注意力网络和图卷积网络这样的架构曾让我经历了不少噩梦,更重要的是,它们让我夜不能寐。
为了节省你整天的时间,尤其是你的通勤时间,我将为你简化算法。
一旦你有了嵌入,这时候魔法才会发生。但你问一切是如何运作的?

基于《史酷比宇宙》的图解来解释 GSage(由作者制作)。
“你以交友闻名”这句话,你必须记住。
因为 GraphSAGE 的一个基本假设是同一邻域中的节点应该表现出类似的嵌入。为实现这一点,GraphSAGE 使用聚合函数,将邻域作为输入,并结合每个邻居的嵌入及特定权重。这就是为什么神秘公司嵌入会出现在 Scooby 的邻域中的原因。
本质上,它从邻域收集信息,权重可以是学习得出的,也可以是固定的,具体取决于损失函数。
当聚合器权重被学习时,GraphSAGE 的真正实力便会显现。此时,该架构可以利用节点的特征和邻域为未见节点生成嵌入,使其成为图基机器学习中各种应用的强大工具。

架构训练时间的差异,Maxime Labonne 的文章,链接
如你在此图中所见,当我们在 GraphSage 架构上使用相同的数据集时,训练时间会减少。GAT(图注意网络)和 GCN(图卷积网络)也是非常有趣的图架构。我真的鼓励你关注!
第一次计算时,我感到震惊,震惊于看到训练 1000 批次在数千行数据上仅需 25 秒。
我知道你现在对图学习感兴趣,并想了解更多,我的建议是阅读这位作者的内容。提供了很好的示例和建议。
使用 PyTorch Geometric 的 GraphSAGE 简介
[
作为 Medium 的读者,当我查看一篇新文章时,我会好奇阅读代码,对于你来说,我们可以在 PyTorch Geometric 中实现 GraphSAGE 架构,使用SAGEConv层。
让我们创建一个包含两个SAGEConv层的网络:
-
第一个使用了ReLU作为激活函数和一个dropout 层;
-
第二个直接输出节点嵌入。
在我们的多类分类任务中,我们选择使用交叉熵损失作为主要损失函数。这一选择是由于其适用于具有多个类别的分类问题。此外,我们还采用了强度为 0.0005 的 L2 正则化。
这种正则化技术有助于防止过拟合,通过对大参数值进行惩罚来促进模型的泛化。这是一种确保模型稳定性和预测准确性的全面方法。
import torch
from torch.nn import Linear, Dropout
from torch_geometric.nn import SAGEConv, GATv2Conv, GCNConv
import torch.nn.functional as F
class GraphSAGE(torch.nn.Module):
"""GraphSAGE"""
def __init__(self, dim_in, dim_h, dim_out):
super().__init__()
self.sage1 = SAGEConv(dim_in, dim_h)
self.sage2 = SAGEConv(dim_h, dim_out)#830 for my case
self.optimizer = torch.optim.Adam(self.parameters(),
lr=0.01,
weight_decay=5e-4)
def forward(self, x, edge_index):
h = self.sage1(x, edge_index).relu()
h = F.dropout(h, p=0.5, training=self.training)
h = self.sage2(h, edge_index)
return F.log_softmax(h, dim=1)
def fit(self, data, epochs):
criterion = torch.nn.CrossEntropyLoss()
optimizer = self.optimizer
self.train()
for epoch in range(epochs+1):
total_loss = 0
acc = 0
val_loss = 0
val_acc = 0
# Train on batches
for batch in train_loader:
optimizer.zero_grad()
out = self(batch.x, batch.edge_index)
loss = criterion(out[batch.train_mask], batch.y[batch.train_mask])
total_loss += loss
acc += accuracy(out[batch.train_mask].argmax(dim=1),
batch.y[batch.train_mask])
loss.backward()
optimizer.step()
# Validation
val_loss += criterion(out[batch.val_mask], batch.y[batch.val_mask])
val_acc += accuracy(out[batch.val_mask].argmax(dim=1),
batch.y[batch.val_mask])
# Print metrics every 10 epochs
if(epoch % 10 == 0):
print(f'Epoch {epoch:>3} | Train Loss: {total_loss/len(train_loader):.3f} '
f'| Train Acc: {acc/len(train_loader)*100:>6.2f}% | Val Loss: '
f'{val_loss/len(train_loader):.2f} | Val Acc: '
f'{val_acc/len(train_loader)*100:.2f}%')
def accuracy(pred_y, y):
"""Calculate accuracy."""
return ((pred_y == y).sum() / len(y)).item()
@torch.no_grad()
def test(model, data):
"""Evaluate the model on test set and print the accuracy score."""
model.eval()
out = model(data.x, data.edge_index)
acc = accuracy(out.argmax(dim=1)[data.test_mask], data.y[data.test_mask])
return acc
模型的部署:
在我们项目的开发和部署过程中,我们利用了三种关键技术,每种技术都有其独特而重要的作用:

来自Google的三个标志
Airflow: 为了高效管理和调度我们项目复杂的数据工作流,我们使用了 Airflow Orchestrator。Airflow 是一个广泛采用的任务调度工具,可以自动化流程,并确保我们的数据管道平稳且按时运行。
Mirantis: 我们项目的基础设施是在 Mirantis 云平台上构建和托管的。Mirantis 以提供强大、可扩展和可靠的云解决方案而闻名,为我们的部署提供了坚实的基础。
Jenkins: 为了简化我们的开发和部署流程,我们依赖 Jenkins,一个在持续集成和持续交付(CI/CD)领域中值得信赖的名字。Jenkins 自动化了项目的构建、测试和部署,确保了我们开发周期中的效率和可靠性。
此外,我们将我们的机器学习代码存储在公司的 Artifactory 中。 但什么是 Artifactory?
Artifactory: Artifactory 是一个集中式仓库管理器,用于存储、管理和分发各种工件,如代码、库和依赖项。它作为一个 安全 和有组织的存储空间,确保所有团队成员都可以轻松访问所需的资产。这使得 协作无缝并简化了应用程序和项目的部署,使其成为高效开发和部署工作流程的宝贵资产。
通过将我们的机器学习代码存储在 Artifactory 中,我们确保了我们的模型和数据 可以轻松支持通过 Jenkins 部署。
ET VOILA ! 解决方案已部署。
我谈了很多关于基础设施的内容,但对机器学习和我们取得的结果讲得不多。
结果 :
预测的可信度 :
对于每个物理数据,我们考虑了 2 个预测,因为模型性能的原因。
这怎么可能?
probabilities = torch.softmax(raw_output, dim = 1)
#torch.topk to get the top 3 probabilites and their indices for each prediction
topk_values, topk_indices = torch.topk(probabilities, k = 2, dim = 1)
首先,我使用了一个 softmax 函数来使输出可比,然后使用了一个名为 torch.topk 的函数。它返回给定 input 张量在指定维度上的 k 个最大元素。
那么,回到第一次预测,这里是我们训练后的分布。告诉你们,真棒!

模型输出的概率的绘图(来自 matplotlib),第一次预测(由作者制作)
训练/测试/验证中的准确率和损失。
我不会教你什么是机器学习中的准确率和损失,我假设你们都是专家……(如果不确定,可以询问 ChatGPT,没有羞耻感)。在训练中,通过不同的尺度,你可以看到曲线上的收敛,这很好,说明学习稳定。

准确率和损失的绘图(matplotlib)(由作者制作)
t-SNE :
t-SNE(t-分布随机邻域嵌入)是一种降维技术,用于通过在较低维空间中保留数据点之间的成对相似性来可视化和探索高维数据。
换句话说,想象一下训练前的随机分布 :

数据分布 训练前,(由作者制作)
记住我们正在做多分类,因此这是训练后的分布。特征的聚合似乎做得很满意。聚类形成,物理数据似乎已加入组,表明训练进行了良好。

数据分布 训练后,(由作者制作)
结论 :
我们的目标是基于物理数据预测业务数据(而且我们做到了)。我很高兴地通知你,该算法现在已投入生产,并正在为未来的用户进行接入。
虽然由于专有原因我不能提供完整的解决方案,但我相信你已经掌握了所有必要的细节,或者你完全有能力自行实施。
我最后的一条建议,我发誓,拥有一个出色的团队,不仅是那些工作出色的人,还有那些每天让你开心的人。
如果你有任何问题,请随时联系我。随时与我联系,我们可以详细讨论。
如果我没见到你,下午好,晚上好,晚安!
你掌握了吗?
正如钱德勒·宾可能会说:
“说谎总比进行复杂的讨论要好”
别忘了点赞和分享!
参考资料和资源
[1] Inc(2018),来自 Inc 的网络文章
[2] 图机器学习:通过应用机器学习技术和算法将图数据提升到一个新水平(2021),Claudio Stamile
[3] GraphSAGE,扩展图神经网络,(2021),Maxime Labonne
图片来源
-
Nathan Anderson的照片,来源于Unsplash
-
GraphSAGE 时间差,来自 Maxime Labonne 的文章,链接
-
Atlas Lineage 可视化,来自 Atlas 网站,链接
-
三个标志来自Google
媒体中面部模糊的力量解锁:全面探索与模型比较
各种人脸检测和模糊算法的比较
·
关注 发表在 Towards Data Science ·12 min read·2023 年 9 月 18 日
--
处理过的照片由 OSPAN ALI 提供,来源于 Unsplash
在当今数据驱动的世界中,确保个人隐私和匿名性至关重要。从保护个人身份到遵守严格的法规,如 GDPR,对各种媒体格式中人脸匿名化的高效可靠解决方案的需求从未如此迫切。
内容
-
介绍
-
人脸检测
-
Haar Cascade
-
MTCNN
-
YOLO
-
-
人脸模糊
-
高斯模糊
-
像素化
-
-
结果与讨论
-
实时性能
-
基于场景的评估
-
隐私
-
-
视频中的使用
-
Web 应用程序
-
结论
介绍
在这个项目中,我们探讨并比较了多种人脸模糊解决方案,并开发了一个允许轻松评估的 web 应用程序。让我们深入了解推动对这种系统需求的多样化应用:
-
保护隐私
-
导航法规环境:随着法规环境的迅速变化,全球各地的行业和地区正在实施更严格的规范,以保护个人身份。
-
训练数据保密性:机器学习模型依赖于多样化且准备充分的训练数据。然而,分享这些数据通常需要仔细的匿名化处理。
这个解决方案可以归结为两个基本组成部分:
-
人脸检测
-
人脸模糊技术
人脸检测
为了解决匿名化挑战,第一步是定位图像中存在人脸的区域。为此,我测试了三个图像检测模型。
Haar Cascade

图 1. 类似 Haar 的特征 (source — 原始论文)
Haar Cascade 是一种用于图像或视频中物体检测的机器学习方法,如人脸。它通过利用一组训练过的特征,称为‘类似 Haar 的特征’(图 1),这些特征是简单的矩形滤波器,集中于图像区域内像素强度的变化。这些特征可以捕捉边缘、角度及其他在脸部常见的特征。
训练过程包括向算法提供正面示例(包含人脸的图像)和负面示例(不包含人脸的图像)。算法通过调整特征的权重来学习区分这些示例。训练后,Haar Cascade 本质上成为一个分类器的层级结构,每个阶段逐步细化检测过程。
对于人脸检测,我使用了一个在正面人脸图像上训练的预训练 Haar Cascade 模型。
import cv2
face_cascade = cv2.CascadeClassifier('./configs/haarcascade_frontalface_default.xml')
def haar(image):
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30))
print(len(faces) + " total faces detected.")
for (x, y, w, h) in faces:
print(f"Face detected in the box {x} {y} {x+w} {y+h}")
MTCNN

图 2. MTCNN 中的人脸检测过程 (source — 原始论文)
MTCNN(多任务级联卷积网络)是一个复杂且高度准确的人脸检测算法,超越了 Haar Cascades 的能力。MTCNN 设计用于在具有多样化面部尺寸、方向和光照条件的场景中表现优异,利用一系列神经网络,每个网络都专门执行人脸检测过程中的特定任务。
-
第一阶段 — 提议生成:MTCNN 通过一个小型神经网络开始生成大量潜在人脸区域(边界框)的过程。
-
第二阶段 — 精细化:在第一阶段生成的候选区域在此步骤中进行筛选。第二个神经网络评估提议的边界框,并调整其位置,以更精确地对齐真实的人脸边界。这有助于提高准确性。
-
第三阶段 — 面部特征点:此阶段识别面部标志点,如眼角、鼻子和嘴巴。使用神经网络准确定位这些特征。
MTCNN 的级联架构使其能够在过程早期迅速剔除没有人脸的区域,将计算集中在更可能包含人脸的区域。它处理不同尺度(缩放级别)的人脸和旋转的能力使其相比 Haar Cascades 更适用于复杂场景。然而,其计算强度源于其基于神经网络的顺序处理方法。
在实施 MTCNN 时,我使用了 mtcnn 库。
import cv2
from mtcnn import MTCNN
detector = MTCNN()
def mtcnn_detector(image):
faces = detector.detect_faces(image)
print(len(faces) + " total faces detected.")
for face in faces:
x, y, w, h = face['box']
print(f"Face detected in the box {x} {y} {x+w} {y+h}")
YOLOv5

图 3. YOLO 物体检测过程 (source — 原始论文)
YOLO(You Only Look Once)是一种用于检测多种物体的算法,包括人脸。与其前身不同,YOLO 在通过神经网络的单次传递中进行检测,使其速度更快,更适合实时应用和视频。使用 YOLO 在媒体中检测人脸的过程可以分为四个部分:
-
图像网格划分:输入图像被划分为一个网格单元。每个单元负责预测位于其边界内的物体。对于每个单元,YOLO 预测边界框、物体概率和类别概率。
-
边界框预测:在每个单元内,YOLO 预测一个或多个边界框及其对应的概率。这些边界框代表潜在的物体位置。每个边界框由其中心坐标、宽度、高度以及物体存在于该边界框内的概率定义。
-
类别预测:对于每个边界框,YOLO 预测物体可能属于的各种类别的概率(例如,“人脸”,“汽车”,“狗”)。
-
非最大抑制(NMS):为了消除重复的边界框,YOLO 应用 NMS。这个过程通过评估边界框的概率和与其他框的重叠情况来丢弃冗余的边界框,只保留最有信心且没有重叠的框。
YOLO 的主要优势在于其速度。由于它通过神经网络进行一次前向传播来处理整个图像,因此比涉及滑动窗口或区域提议的算法快得多。然而,这种速度可能会在精度上有些妥协,尤其是在较小的物体或拥挤的场景中。
YOLO 可以通过在面部特定数据上进行训练并将输出类别修改为仅包括一个类别(‘face’)来适应面部检测。为此,我利用了基于 YOLOv5 构建的‘yoloface’库。
import cv2
from yoloface import face_analysis
face=face_analysis()
def yolo_face_detection(image):
img,box,conf=face.face_detection(image, model='tiny')
print(len(box) + " total faces detected.")
for i in range(len(box)):
x, y, h, w = box[i]
print(f"Face detected in the box {x} {y} {x+w} {y+h}")
面部模糊
在图像中识别出潜在面部的边界框后,下一步是对它们进行模糊处理以去除其身份信息。为此任务,我开发了两种实现方法。图 4 提供了一个演示用的参考图像。

图 4. 参考图像,来自Ethan Hoover的Unsplash
高斯模糊

图 5. 应用高斯模糊的模糊参考图像(图 4)
高斯模糊是一种图像处理技术,用于减少图像噪声和模糊细节。这在面部模糊领域尤为有用,因为它擦除了图像中那部分的具体信息。它通过计算每个像素周围邻域的像素值的平均值来完成这一操作。这个平均值以被模糊的像素为中心,并使用高斯分布计算,使得附近的像素权重更大,而远离的像素权重更小。结果是图像变得更柔和,高频噪声和细节减少。应用高斯模糊的结果如图 5 所示。
高斯模糊有三个参数:
-
要进行模糊处理的图像部分。
-
内核大小:用于模糊操作的矩阵。较大的内核大小会导致更强的模糊效果。
-
标准差:较高的值会增强模糊效果。
f = image[y:y + h, x:x + w]
blurred_face = cv2.GaussianBlur(f, (99, 99), 15) # You can adjust blur parameters
image[y:y + h, x:x + w] = blurred_face
像素化

图 6. 应用像素化的模糊参考图像(图 4)
像素化是一种图像处理技术,其中图像中的像素被替换为较大的单一颜色块。这种效果通过将图像划分为网格单元来实现,每个单元对应一组像素。然后,将单元中所有像素的颜色或强度作为该单元中所有像素颜色的平均值,这个平均值应用于单元中的所有像素。这一过程创建了简化的外观,减少了图像中细节的层次。像素化的结果如图 6 所示。正如你所观察到的,像素化显著地使得识别个人身份变得复杂。
像素化有一个主要参数,决定了多少组像素应该代表一个特定区域。例如,如果我们有一个包含面部的(10,10)图像区域,它将被替换为一个 10x10 的像素组。较小的数字会导致更大的模糊效果。
f = image[y:y + h, x:x + w]
f = cv2.resize(f, (10, 10), interpolation=cv2.INTER_NEAREST)
image[y:y + h, x:x + w] = cv2.resize(f, (w, h), interpolation=cv2.INTER_NEAREST)
结果与讨论
我将从两个角度评估不同的算法:实时性能分析和特定图像场景。
实时性能
使用相同的参考图像(见图 4),测量每个面部检测算法定位图像中面部边界框所需的时间。结果基于每个算法的 10 次测量的平均值。模糊算法所需的时间微不足道,将不在评估过程中考虑。

图 4. 每个算法检测面部所需的平均时间(秒)
可以观察到,由于 YOLOv5 通过神经网络的单次处理实现了最佳性能(速度)。相比之下,像 MTCNN 这样的方法需要通过多个神经网络的顺序遍历,这进一步使得算法并行化的过程变得复杂。
场景基础性能
为了评估上述算法的性能,除了参考图像(见图 4)外,我选择了几张在不同场景下测试算法的图像:
-
参考图像(见图 4)
-
紧密聚集的人群 —— 评估算法捕捉不同面部大小的能力,一些面部较近,一些较远(见图 8)
-
侧视面部 —— 测试算法检测未直接面向摄像头的面部的能力(见图 10)
-
翻转面部,180 度 —— 测试算法检测旋转 180 度的面部的能力(见图 11)
-
翻转面部,90 度 —— 测试算法检测旋转 90 度的面部的能力(见图 12)

图 8. Nicholas Green 的人群照片,来自 Unsplash

图 9. 多个面部由 Naassom Azevedo 提供,来源于 Unsplash

图 10. 侧面视图面部由 Kraken Images 提供,来源于 Unsplash

图 11. 图 4 的面部翻转 180 度。

图 12. 图 4 的面部翻转 90 度。
Haar Cascade
Haar Cascade 算法通常在匿名化面部方面表现良好,尽管有一些例外。它成功地检测了参考图像(图 4)和‘多个面部’场景(图 9)。在‘人群’场景(图 8)中,虽然处理得不错,但有些面部未完全检测到或较远。Haar Cascade 在面对不直接对着相机的面部(图 10)和旋转面部(图 11 和 12)时遇到挑战,未能完全识别面部。

图 13. Haar Cascade 的结果
MTCNN
MTCNN 实现的结果与 Haar Cascade 非常相似,具有相同的优缺点。此外,MTCNN 在检测图 9 中肤色较深的面部时存在困难。

图 14. MTCNN 的结果
YOLOv5
YOLOv5 与 Haar Cascade 和 MTCNN 的结果略有不同。它成功地检测到其中一个未直接对着相机的面部(图 10)以及旋转 180 度的面部(图 11)。然而,在‘人群’图像(图 8)中,它未能像前述算法那样有效地检测到较远的面部。

图 15. YOLOv5 的结果
隐私
在处理图像隐私挑战时,重要的是要考虑如何在保持图像自然外观的同时使面部无法识别。
高斯模糊
高斯模糊有效地模糊了图像中的面部区域(如图 5 所示)。然而,它的成功依赖于用于模糊效果的高斯分布参数。在图 5 中,面部特征仍然可辨识,这表明需要更高的标准差和卷积核大小以获得最佳结果。
像素化
像素化(如图 6 所示)由于其作为面部模糊方法的熟悉感,通常在视觉上对人眼更为愉悦,与高斯模糊相比。像素化中使用的像素数量在此上下文中起着关键作用,因为较小的像素数量使面部不那么可识别,但可能导致外观不那么自然。
总体来看,相较于高斯模糊算法,像素化方法更受青睐。这主要由于其熟悉性和上下文的自然性,在隐私和美学之间取得了平衡。
逆向工程
随着 AI 工具的兴起,预测逆向工程技术可能会去除模糊图像中的隐私过滤器变得尤为重要。然而,模糊面部的行为不可逆地用更一般化的面部细节替代了特定的面部细节。目前,AI 工具只能在提供清晰的参考图像时逆向工程模糊的面部。这与逆向工程的需求本身相矛盾,因为它假设了对个体身份的了解。因此,面部模糊作为保护隐私的一种有效且必要的手段,面对不断发展的 AI 能力仍然是必不可少的。
在视频中的应用
由于视频本质上是图像的序列,因此修改每个算法以对视频进行匿名化相对简单。然而,在这里,处理时间变得至关重要。对于一个 30 秒的视频,录制帧率为 60 帧每秒(每秒图像),算法需要处理 1800 帧。在这种情况下,像 MTCNN 这样的算法将不可行,尽管它们在某些场景下有所改进。因此,我决定使用 YOLO 模型来实现视频匿名化。
import cv2
from yoloface import face_analysis
face=face_analysis()
def yolo_face_detection_video(video_path, output_path, pixelate):
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
raise ValueError("Could not open video file")
# Get video properties
fps = int(cap.get(cv2.CAP_PROP_FPS))
frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
# Define the codec and create a VideoWriter object for the output video
fourcc = cv2.VideoWriter_fourcc(*'H264')
out = cv2.VideoWriter(output_path, fourcc, fps, (frame_width, frame_height))
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
tm = time.time()
img, box, conf = face.face_detection(frame_arr=frame, frame_status=True, model='tiny')
print(pixelate)
for i in range(len(box)):
x, y, h, w = box[i]
if pixelate:
f = img[y:y + h, x:x + w]
f = cv2.resize(f, (10, 10), interpolation=cv2.INTER_NEAREST)
img[y:y + h, x:x + w] = cv2.resize(f, (w, h), interpolation=cv2.INTER_NEAREST)
else:
blurred_face = cv2.GaussianBlur(img[y:y + h, x:x + w], (99, 99), 30) # You can adjust blur parameters
img[y:y + h, x:x + w] = blurred_face
print(time.time() - tm)
out.write(img)
cap.release()
out.release()
cv2.destroyAllWindows()
网络应用
为了对不同算法进行简化评估,我创建了一个网络应用程序,用户可以上传任何图像或视频,选择人脸检测和模糊算法,处理后将结果返回给用户。该实现使用 Flask 与 Python 进行后端开发,利用上述库以及 OpenCV,前端则使用 React.js 与模型进行用户交互。完整代码可在这个链接找到。
结论
在本帖的范围内,探索、比较和分析了包括 Haar Cascade、MTCNN 和 YOLOv5 在内的各种人脸检测算法。该项目还关注了图像模糊技术。
Haar Cascade 在某些场景下证明是一种高效的方法,通常表现出良好的时间性能。MTCNN 作为一种在各种条件下具有强大人脸检测能力的算法脱颖而出,尽管它在面对不典型的面部姿态时表现不佳。YOLOv5 凭借其实时人脸检测能力,在时间是关键因素(例如视频)的场景中成为了一个出色的选择,尽管在群体设置中的准确性略有下降。
所有算法和技术都集成到一个单一的网络应用中。该应用程序提供了对所有人脸检测和模糊方法的简便访问和利用,并能够使用模糊技术处理视频。
这篇文章是我在斯科普里的计算机科学与工程学院“数字图像处理”课程工作的总结。感谢阅读!
解锁线性回归中交互项的力量
原文:
towardsdatascience.com/unlocking-the-power-of-interaction-terms-in-linear-regression-ba30c2cf158f

图片由 Denys Nevozhai 提供,来自 Unsplash
了解如何通过包含交互项来使你的线性模型更灵活
·发表于 Towards Data Science ·10 分钟阅读·2023 年 5 月 18 日
--
线性回归是一个强大的统计工具,用于建模因变量与一个或多个自变量(特征)之间的关系。在回归分析中,一个重要但常被忽视的概念是交互项。简而言之,交互项使我们能够检查目标与自变量之间的关系是否会根据另一个自变量的值而变化。
交互项是回归分析中的一个关键组成部分,了解它们如何工作可以帮助从业者更好地训练模型并解释数据。尽管它们很重要,但交互项可能很难理解。
在这篇文章中,我们将提供一个关于线性回归中交互项的直观解释。
回归模型中的交互项是什么?
首先,让我们考虑更简单的情况,即没有交互项的线性模型。这样的模型假设每个特征或预测变量对因变量(目标)的影响与模型中的其他预测变量无关。
以下方程描述了具有两个特征的模型规格:

为了使解释更容易理解,我们使用一个例子。假设我们对使用两个特征建模房地产价格(y)感兴趣:其大小(X1)和一个布尔标志,指示公寓是否位于市中心(X2)。β0 是截距,β1 和 β2 是线性模型的系数,ε 是误差项(模型未解释的部分)。
收集数据并估计线性回归模型后,我们得到以下系数:

了解估计的系数,并且 X2 是一个布尔特征后,我们可以根据 X2 的值写出两个可能的情景。

如何解释这些?虽然在房地产的背景下这可能没有太大意义,但我们可以说,市中心一平方米的公寓价格为 310(截距的值),每增加一平方米的空间价格增加 20。另一种情况下,唯一的区别是截距减少了 10 个单位。下图展示了两条最佳拟合线。

市中心和市外房地产的回归线
正如我们所见,这些线是平行的,并且它们具有相同的斜率——X1 的系数,在这两种情况下都是相同的。
交互项表示共同影响
此时你可能会争辩说,市中心一平方米的公寓比郊区的一平方米公寓更贵。换句话说,这两个特征可能对房地产价格有共同的影响。
因此,我们认为不仅截距在这两种情况之间应该不同,而且线的斜率也应该不同。如何实现这一点?这正是交互项发挥作用的时候。交互项使模型的规格更具灵活性,并允许我们考虑这些模式。
交互项实际上是我们认为对目标有共同影响的两个特征的乘积。以下方程展示了模型的新规格:

再次假设我们已经估计了我们的模型,并且我们知道了系数。为了简化,我们保留了与之前示例相同的值。请记住,在现实生活中,它们很可能会有所不同。

一旦我们写出 X2(市中心或非市中心)的两个情景,我们可以立即看到两条线的斜率(X1 的系数)不同。正如我们假设的那样,市中心的额外一平方米现在比郊区更贵。
解释带有交互项的系数
向模型中添加交互项会改变所有系数的解释。在没有交互项的情况下,我们将系数解释为预测变量对因变量的唯一影响。
在我们的案例中,我们可以说β1是公寓面积对其价格的唯一影响。然而,加入交互项后,公寓面积的影响在不同的X2值下是不同的。换句话说,公寓面积对其价格的唯一影响不再局限于β1。
为了更好地理解每个系数所代表的含义,让我们再看一眼包含交互项的线性模型的原始规范。提醒一下,X2是一个布尔特征,表示某个公寓是否在市中心。

现在,我们可以按以下方式解释每个系数:
-
β0 — 市中心以外公寓的截距(或具有布尔特征X2值为 0 的任何组),
-
β1 — 市中心以外公寓的斜率(价格的影响),
-
β2 — 两组之间截距的差异,
-
β3 — 市中心公寓与市中心以外公寓之间斜率的差异。
例如,假设我们正在检验一个假设,即公寓面积对其价格的影响是均等的,无论公寓是否在市中心。然后,我们将估计包含交互项的线性回归,并检查β3是否显著不同于 0。
关于交互项的一些附加说明:
-
我们展示了双向交互项,但也可以有更高阶的交互项(例如,涉及 3 个特征)。
-
在我们的示例中,我们展示了数值特征(公寓面积)与布尔特征(公寓是否在市中心)的交互。然而,我们也可以创建两个数值特征的交互项。例如,我们可以创建公寓面积与房间数量的交互项。有关更多细节,请参考References部分提到的来源。
-
交互项可能在统计上显著,但主要效应可能不显著。此时,我们应遵循层次原则,即如果我们在模型中包含了交互项,也应包括主要效应,即使它们的影响在统计上不显著。
Python 实操示例
经过所有的理论介绍后,让我们看看如何在 Python 中向线性回归模型添加交互项。和往常一样,我们从导入所需的库开始。
import numpy as np
import pandas as pd
import statsmodels.api as sm
import statsmodels.formula.api as smf
# plotting
import seaborn as sns
import matplotlib.pyplot as plt
# settings
plt.style.use("seaborn-v0_8")
sns.set_palette("colorblind")
plt.rcParams["figure.figsize"] = (16, 8)
%config InlineBackend.figure_format = 'retina'
在我们的例子中,我们将使用statsmodels库来估计线性模型。对于数据集,我们将使用mtcars数据集。我非常确定,如果你曾经使用过 R,你对这个数据集已经很熟悉了。首先,我们加载数据集:
mtcars = sm.datasets.get_rdataset("mtcars", "datasets", cache=True)
print(mtcars.__doc__)
执行代码片段会打印数据集的详细描述。我们只展示相关部分——总体描述和列定义:
====== ===============
mtcars R Documentation
====== ===============
The data was extracted from the 1974 *Motor Trend* US magazine, and
comprises fuel consumption and 10 aspects of automobile design and
performance for 32 automobiles (1973–74 models).
A data frame with 32 observations on 11 (numeric) variables.
===== ==== ========================================
[, 1] mpg Miles/(US) gallon
[, 2] cyl Number of cylinders
[, 3] disp Displacement (cu.in.)
[, 4] hp Gross horsepower
[, 5] drat Rear axle ratio
[, 6] wt Weight (1000 lbs)
[, 7] qsec 1/4 mile time
[, 8] vs Engine (0 = V-shaped, 1 = straight)
[, 9] am Transmission (0 = automatic, 1 = manual)
[,10] gear Number of forward gears
[,11] carb Number of carburetors
===== ==== ========================================
然后,我们从加载的对象中提取实际数据集:
df = mtcars.data
df.head()

mtcars 数据集的预览
以我们的例子为例,假设我们要调查每加仑英里数(mpg)与两个特征的关系:重量(wt,连续变量)和传输类型(am,布尔值)。
首先,我们绘制数据以获取一些初步见解。
sns.lmplot(x="wt", y="mpg", hue="am", data=df, fit_reg=False)
plt.ylabel("Miles per Gallon")
plt.xlabel("Vehicle Weight");

每加仑英里数与车辆重量的散点图,按传输类型着色
仅通过目测图表,我们可以看出am变量的两个类别的回归线会有很大不同。为了比较,我们首先使用没有交互项的模型。
model_1 = smf.ols(formula="mpg ~ wt + am", data=df).fit()
model_1.summary()
以下表格展示了在没有交互项的情况下拟合线性回归的结果。

从摘要表中,我们可以看到am特征的系数在统计上不显著。利用我们已学到的系数解释方法,我们可以绘制am特征两个类别的最佳拟合线。
X = np.linspace(1, 6, num=20)
sns.lmplot(x="wt", y="mpg", hue="am", data=df, fit_reg=False)
plt.title("Best fit lines for from the model without interactions")
plt.ylabel("Miles per Gallon")
plt.xlabel("Vehicle Weight")
plt.plot(X, 37.3216 - 5.3528 * X, "blue")
plt.plot(X, (37.3216 - 0.0236) - 5.3528 * X, "orange");

两种传输类型的最佳拟合线
由于am特征的系数基本为零,这些线几乎重叠。
接下来,我们使用第二个模型,这次包括两个特征之间的交互项。下面展示了如何在statsmodels公式中添加交互项作为额外输入。
model_2 = smf.ols(formula="mpg ~ wt + am + wt:am", data=df).fit()
model_2.summary()
以下摘要表展示了包含交互项的线性回归拟合结果。

我们可以从摘要表中快速得出两个结论:
-
所有系数,包括交互项,都在统计上显著。
-
通过检查 R2(以及它的调整变体,因为模型中的特征数量不同),我们可以说含有交互项的模型拟合效果更好。
与之前的情况类似,我们绘制最佳拟合线。
X = np.linspace(1, 6, num=20)
sns.lmplot(x="wt", y="mpg", hue="am", data=df, fit_reg=False)
plt.title("Best fit lines for from the model with interactions")
plt.ylabel("Miles per Gallon")
plt.xlabel("Vehicle Weight")
plt.plot(X, 31.4161 - 3.7859 * X, "blue")
plt.plot(X, (31.4161 + 14.8784) + (-3.7859 - 5.2984) * X, "orange");

两种传输类型的最佳拟合线,包括交互项
我们可以立即看到自动传输和手动传输汽车的拟合线(无论是截距还是斜率)之间的差异。
附加信息: 我们还可以使用scikit-learn的PolynomialFeatures添加交互项。这个变换器不仅提供了添加任意阶交互项的可能性,还创建了多项式特征(例如,现有特征的平方值)。更多信息,请参见文档。
总结
在处理线性回归中的交互项时,有几点需要记住:
-
交互项允许我们检查目标与特征之间的关系是否会根据另一个特征的值而变化。
-
我们将交互项添加为原始特征的乘积。通过将这些新变量添加到回归模型中,我们可以测量它们与目标之间交互的影响。正确解释交互项的系数对于理解关系的方向和强度至关重要。
-
使用交互项可以使线性模型的规格更加灵活(不同的线条具有不同的斜率),这可以导致更好的数据拟合和更好的预测性能。
你可以在我的GitHub 仓库中找到本文使用的代码。像往常一样,任何建设性的反馈都是非常欢迎的。你可以通过Twitter或在评论中联系我。
喜欢这篇文章?成为 Medium 会员,继续通过无障碍阅读来学习。如果你使用 这个链接 成为会员,你将以无额外费用支持我。提前感谢,期待与你相见!
你可能还对以下内容感兴趣:
详细参考常用的回归评估指标及其在各种实际应用中的应用…
了解如何正确解释线性回归的结果——包括变量变换的情况
[## 关于线性回归你可能不知道的一件事 ## 线性回归系数的解释
如何训练一个具有多个输出的单一模型
towardsdatascience.com ## 极简主义者的实验跟踪指南
入门实验跟踪的最低限度指南
towardsdatascience.com
参考文献
-
rinterested.github.io/statistics/lm_interactions_output_interpretation.html -
janhove.github.io/analysis/2017/06/26/continuous-interactions -
Henderson 和 Velleman (1981),互动式构建多重回归模型。生物统计学,37,391–411。
-
mtcars数据集是 R 基础发行版的一部分,并且在 GNU 通用公共许可证 (GPL) 下发布
除非另有说明,否则所有图片均由作者提供。
最初发布于 NVIDIA 开发者博客 2023 年 4 月 26 日
解锁路线可视化的力量:3 种基本技巧
分析路线的必知技巧
·发布于 Towards Data Science ·4 分钟阅读·2023 年 1 月 29 日
--

图片由 José Martín Ramírez Carrasco 提供,来源于 Unsplash
路线可视化对交通规划、物流和供应链管理至关重要。在 AI 和数据科学领域,它被用于 ETA(预计到达时间)或车队优化等各种应用。
在博客中,我将介绍可视化路线的顶级技巧。为了说明博客内容,我将以波尔图市的出租车数据为例。出租车配有 IoT 设备,每 15 秒发出一次位置。
为了说明目的,我将使用与一次出租车旅行相关的数据。这些数据包含出租车在每个位置的速度和位置。

出租车轨迹数据(图片由作者提供)
现在让我们使用不同的技巧来可视化数据。
1. 路线折线图——基本但必备的技巧
路线折线图通常创建一个宽度为两像素的红色线条,显示路径或轨迹。

路线折线图(图片由作者提供)
这是一个简单但非常有效的技巧,用于可视化出租车的行驶路线,从 Palacio de Bolso 到 HF Tuela Porto。你还可以观察到,路线的一半沿着杜罗河,另一半在城市内部。
看到简单的表格数据转变为充满见解的可视化图表总是令人惊叹!
2. 路线热力图——炫酷的展示
让我们通过使用热图来使分析更加生动。这里展示的是相同的路线,但用红色和绿色点标记。红色点表示数据中有更多的定位点,相比之下绿色点则较少。

路线热图(图片由作者提供)
上图中带有红色和绿色点的卫星地图,看起来像是一部惊悚片的场景!
我们可以观察到定位点更多的地方。由于物联网传感器每 15 秒发射一次数据,这意味着出租车停了下来或速度较慢。通过进一步的分析,我们可以得出结论,出租车在起点和终点停靠,在转弯处速度较慢。
如你所见,与路线折线图相比,这提供了更多的信息。
3. 路线标记分析——在路线中加入数据分析
现在,让我们通过将路线分析与数据分析结合起来使其更加智能。数据中有一个速度列,急需被分析!让我们为这个列做一些公正的分析。
分析附加数据的有效方法之一是使用标记。下图展示了带有标记的路线,这些标记指示了最大速度点。

带有最大速度标记的路线分析(图片由作者提供)
除了最大速度之外,你可以引入其他任何数据科学技术。其中一个有趣的分析是异常值分析。这里展示的是带有标记的路线,标记指示了高值的异常点。

带有异常速度标记的路线分析(图片由作者提供)
也可以将其与箱线图进行比较。通常,在速度分析中,只有高值的异常值是有用的。

速度箱线图(图片由作者提供)
基于数据科学算法放置标记提升了路线分析。这里你看到的是速度的示例,但你可以在其他各种数据上引入标记,例如加速度或任何其他车辆传感器数据。
结论
路线可视化可以通过简单、花哨和智能的技术来完成。掌握所有这些技术对于有效解决交通规划、物流和供应链管理等关键用例非常有用。
数据源
用于本文的数据摘自以下数据集:archive.ics.uci.edu/ml/datasets/Taxi+Service+Trajectory+-+Prediction+Challenge,+ECML+PKDD+2015
可用于商业或非商业用途,引用时请使用以下说明
Moreira-Matias, L., Gama, J., Ferreira, M., Mendes-Moreira, J., Damas, L., “使用流数据预测出租车乘客需求”。发表于:IEEE 智能交通系统学报,第 14 卷,第 3 期,第 1393–1402 页,2013 年 9 月
请使用我的推荐链接加入 Medium。
[## 使用我的推荐链接加入 Medium - Pranay Dave
阅读 Pranay Dave 和数以千计的其他作者在 Medium 上的每一个故事。您的会员费直接支持……
请订阅以便每当我发布新故事时保持更新
[## 每当 Pranay Dave 发布新内容时,获取电子邮件。
每当 Pranay Dave 发布新内容时,您将收到一封电子邮件。通过注册,您将创建一个 Medium 账户(如果您还没有的话)……
额外资源
网站
您可以访问我的网站,这是一个无代码平台,从应用程序和业务用例的角度学习 AI 和数据科学。https://experiencedatascience.com
YouTube 频道
这是我 YouTube 频道的链接
www.youtube.com/c/DataScienceDemonstrated
利用 LLMs 解锁文本数据的力量
原文:
towardsdatascience.com/unlocking-the-power-of-text-data-with-llms-3ddcd063274a
数据科学实验室
学习如何使用 LLMs 处理文本数据:新手逐步指南
·发表于 Towards Data Science ·11 分钟阅读·2023 年 10 月 23 日
--

作者使用 Midjourney 生成的图像
客户评论、员工调查和社交媒体帖子在揭示人们对特定产品或服务的态度方面可能非常有力。然而,大多数数据分析师对这种类型的数据几乎不做任何处理。你问为什么? 从文本数据中生成见解并非易事,即使是最有经验的数据分析师也可能为此苦恼数天。
这就是大型语言模型(LLMs)派上用场的时候。它们可以帮助执行翻译、总结、情感分析等任务。但究竟什么是 LLM? 简而言之,你可以把 LLM 想象成一只鹦鹉。就像鹦鹉重复它在家听到的东西一样,LLM 模仿人类语言。一个关键的区别是,LLMs 已经在大量数据上进行过训练——远远超过了鹦鹉在笼子里能学到的东西!这就是 LLMs 能够生成连贯且与上下文相关的文本,而不是像鹦鹉那样偶尔说些无稽之谈的原因。🦜
在这篇文章中,我们将探讨 LLMs 如何工作,以及它们如何使数据分析师比以往更容易从文本数据中提取见解。目前有多种 LLMs 通过 API 提供,每种 LLM 具有不同的功能和价格。我们将使用 OpenAI API 中的 GPT-3。在撰写时,OpenAI 根据请求次数和生成的令牌数量收费。此次教程的总费用为$0.2。
现在开始深入探索吧!
目录
▹ 第 1 步:下载数据
▹ 第 2 步:读取数据
▹ 第 3 步:数据预处理
▹ 第 3a 步:处理 NaN 值
▹ 第 3b 步:为 GPT-3 转换文本
▹ 第 3c 步:计算令牌
▹ 第 4 步:设置 OpenAI 账户
▹ 第 5 步:使用 GPT-3
▹ 第 6 步:总结结果
先决条件
为了跟随本教程,你需要具备以下内容:
-
Python 的基础知识
-
Python 3 环境
-
OpenAI API 密钥(见第 4 步)
第一步:下载数据
我们将使用的数据集是 Kaggle 在 2017 年进行的一项行业调查,旨在揭示机器学习和数据科学的新趋势。在本教程中,我们将仅使用 freeformResponses csv 文件,该文件包含对 Kaggle 问题的开放式回答。

freeformResponses csv 文件的片段
第二步:读取数据
接下来,我们将把 csv 文件读取到数据框中,并重点关注“PersonalProjectsChallengeFreeForm”列。该列包含人们在使用公共数据集进行个人项目时面临的挑战。作为数据科学和机器学习平台,Kaggle 可以利用这些见解来改进其服务(例如,通过开发相关内容、教程和专门解决这些挑战的资源)。
# load library
import pandas as pd
# read file and create df
df = pd.read_csv('freeformResponses.csv', usecols = ['PersonalProjectsChallengeFreeForm'])
# check output
df.head()

输出
第三步:数据预处理
数据预处理涉及一系列步骤,以清理和准备数据以进行分析。GPT-3 可以处理相对干净和结构化的文本数据,而无需 extensive 预处理。然而,对于复杂或非标准的数据,可能需要一些额外的预处理,以确保在利用 GPT-3 时获得最佳结果。如果你的文本包含多种语言、拼写错误或领域特定术语,需要特别注意这一点。
第 3a 步:处理 NaN 值
我们将首先处理 NaN(非数字)值。NaN 值表示缺失或未定义的值,具有非常独特的属性,因此在早期使用 isna() 函数检测它们是很重要的。一旦识别出这些值,我们可以采取适当的措施有效地处理它们。
# count NaN values
df.isna().sum()

输出
有 13,214 个 NaN 值(占所有响应的 80%!),这意味着这些人没有回答问题。最简单的方法是使用 dropna() 函数删除所有包含 NaN 值的条目。然而,根据你的具体使用情况,你可能更愿意以其他方式处理 NaN 值,例如通过用特定值替换它们。
# drop NaN values
df = df.dropna()
# check output
df.head()

输出
为演示目的,我们将仅使用调查中的前 500 条(非空)响应。
# select first 500 rows
df = df.head(500)
第 3b 步:为 GPT-3 转换文本
接下来,我们将把文本数据转换为适合 GPT-3 的格式。我们将从“PersonalProjectsChallengeFreeForm”列中提取所有值,并将它们存储在“challenges”列表中。这一转换从使用 squeeze()函数开始,该函数将数据框转换为 pandas 系列。随后,tolist()函数将这个系列转换为列表。
# convert df into a series and then into a list
challenges = df.squeeze().tolist()
# check output
challenges[:5]

输出
在这个示例中,“challenges”是一个列表,其中每个元素代表原始调查的一个回应。我们将把这个文本作为输入提供给 GPT-3。
第 3c 步:计算 tokens
我们的文本几乎准备好用于 GPT-3 了。在我们继续之前,了解 GPT-3 如何处理文本是很重要的。最初,它执行分词,这涉及将文本拆分成称为tokens的更小单元。Tokens 是文本单元,例如句子、单词、数字,甚至标点符号。例如,短语“hello friend!”可以拆分为三个 tokens:“hello”、“friend”和“!”。

分词示例
在分词之后,GPT-3 继续进行编码,这意味着它将这些 tokens 转换为 token 数字。在我们的示例中,三个 tokens“hello”、“ friend”和“!”可以转换为三个 token 数字:“15339”、“4333”和“0”。

编码示例
通过确定文本中的 tokens 数量,我们将知道文本是否过长而无法被模型处理,以及 OpenAI API 调用的费用(API 调用的费用是根据输入中发送的 tokens 数量以及 GPT 返回的 tokens 数量来计费的)。
为此,我们将安装一个名为tiktoken的库,并导入必要的模块encoding_for_model。由于不同的 LLM 使用不同的方法对文本进行编码,我们需要指定我们将使用的模型,即“gpt-3.5-turbo-16k”。然后,我们将对每个句子进行分词和编码。
# install library
pip install tiktoken
# import library
from tiktoken import encoding_for_model
# define the model for encoding text, in this case, "gpt-3.5-turbo-16k"
enc = encoding_for_model("gpt-3.5-turbo-16k")
# create an empty list to store tokens
tokens = []
# loop through each sentence in the 'challenges' list
for sentence in challenges:
# encode the sentence using the specified model and append it to the 'tokens' list
tokens.append(enc.encode(sentence))
# check output
pd.DataFrame(data={'challenges':challenges, 'tokens':tokens}).head(3)

输出
最后一步是计算 tokens,这可以通过确定列表“num_tokens”的长度来完成。
# create an empty list to store the number of tokens
num_tokens = []
# iterate through the 'tokens' list, which is a list of lists
for item in tokens:
# nested loop to iterate through sublists within 'tokens'
for subitem in item:
# append the subitem (token) to the 'num_tokens' list
num_tokens.append(subitem)
# check output
len(num_tokens)

输出
要根据我们的输入估算总费用,我们可以参考定价文档。在我们的情况下,4629 个 tokens 将转换为 0.01 美元的费用。
第 4 步:设置 OpenAI 账户
我们的文本终于准备好用于 GPT-3 了(我们离好东西越来越近了!)。要使用 GPT-3,我们将使用 OpenAI API。确保你已经设置了 OpenAI 账户以访问 OpenAI API。如果你还没有账户,请按照下面的步骤创建一个。
首先,访问 OpenAI 网站,并点击页面右上角的“注册”按钮。填写表单,输入你的电子邮件地址,创建一个密码,并提供其他必要的信息。然后,点击“创建账户”按钮。请留意你的收件箱,你会收到一封确认邮件。点击邮件中的链接来验证你的账户。完成这些步骤后,你就可以登录了。
账户创建后,下一步是为其充值。请记住,在使用 API 时,你会为使用量付费。只需前往“管理账户”并找到“账单”标签。在那里,你可以添加你的支付卡详细信息,并指定你希望在账户中存入的初始金额。
最终的重要步骤是生成你的 API 密钥,它作为对 API 的私密访问密钥。你可以在“API 密钥”标签中创建它。请妥善保管此密钥,因为如果丢失无法恢复。不过,如果不幸丢失,你可以选择创建一个新的密钥。
第 5 步:使用 GPT-3
现在我们已经通过 OpenAI API 访问了 GPT-3,我们可以发送包含输入和 API 密钥的请求。作为回报,我们将获得包含 GPT-3 输出的响应。

使用 GPT-3 通过 OpenAI API
首先,我们将安装一个名为 openai 的库。然后,我们将设置 API 密钥以验证我们的请求。
# install library
pip install openai
# import library
import openai as ai
# replace 'your_api_key' with your actual API key
ai.api_key = 'your_api_key'
我们将文本发送给 GPT-3 并要求其总结主要话题,这些话题随后存储在“response”变量中。
💡 注意:这段代码是一个简化的示例,你可以根据具体需求调整用户消息和系统消息,以适应各种任务。
# get GPT-3 response
response = ai.ChatCompletion.create(
model = 'gpt-3.5-turbo-16k',
messages = [
{"role": "system", "content": "You're a helpful assistant. Your task is to analyze a set of reviews."},
{"role": "user", "content": f'''
Below is a set of reviews. Please, identify the main topics mentioned in these comments.
Return a list of 5 topics with description. Reviews:
{challenges}
'''
}
],
temperature = 0,
max_tokens = 6000
)
让我们一步步分析代码:
-
response = ai.ChatCompletion.create(:这一行发起一个请求到 GPT-3,并将响应赋值给变量“response”。 -
model = 'gpt-3.5-turbo-16k':这个参数指定使用哪个 GPT-3 模型。 -
messages = [ ... ]:这一部分定义了一系列消息,GPT-3 将为这些消息生成响应。每条消息都有一个角色(例如系统或用户)和内容。系统消息有助于设置 GPT-3 的行为。例如,我们可以说:“你是一个有用的助手。你的任务是分析一组评论”。而用户消息则提供指示以完成任务。例如,我们可以说:“下面是一组评论。请识别这些评论中提到的主要话题”。 -
temperature = 0:这个参数影响响应的随机性。你可以将其视为控制响应的创意性和不可预测性的方式。将其设置为 0 意味着你每次提问都会得到相同的输出,几乎像是坏掉的唱片。另一方面,将其设置为较高的值(例如 0.8)则意味着你将获得新的输出。 -
max_tokens = 6000:此参数指定了响应可以包含的最大令牌数。将其设置为 6000 可确保响应不会超过此长度。如果响应超出此限制,它将被截断。
在收到 GPT-3 的响应后,我们将返回内容(不包括任何额外的元信息)。
# show response
response['choices'][0]['message']['content']
GPT-3 返回了五个话题:
1. 数据清理和准备:许多评论提到清理和准备数据进行分析的挑战。这包括处理缺失值、格式问题、非结构化数据和数据整理的需求。
2. 数据质量和文档:一些评论强调了数据质量差的问题,包括缺乏文档、不正确的文档和不可靠的数据。还提到了数据完整性、准确性和可靠性的问题。
3. 寻找和访问相关数据集:许多评论者表达了在为他们的项目寻找合适数据集方面的困难。这包括找到符合特定要求的数据集的挑战、可用性不足、公共数据集的规模或相关性有限以及收集个人数据的需求。
4. 连接性和数据融合:一些评论提到与数据连接性和融合相关的挑战,例如整合来自不同来源的数据、处理不一致的格式和合并数据集。
5. 计算能力和可扩展性:一些评论提到与计算能力和可扩展性相关的挑战,特别是在处理大型数据集或在单台机器上处理数据时。
这些话题反映了个人在处理数据时面临的常见挑战,包括数据质量、数据准备、数据集可用性和技术限制的问题。
💡 注意:虽然 GPT-3 本身很强大,但通过用你的训练数据对模型进行微调,通常可以获得更好的结果。
第 6 步:总结结果
这些话题反映了个人在处理数据时面临的常见挑战,包括数据准备、数据质量、可靠性和可扩展性相关的问题。像 Kaggle 这样的公司可以利用这些见解来开发专门解决这些挑战的教育材料,从而为他们的社区提供宝贵的支持。
结论
在这篇文章中,我们探讨了大型语言模型(LLMs)在从文本数据中提取洞察力方面的巨大潜力。我们讨论了 LLMs 是如何工作的,以及它们如何成为处理文本数据的数据分析师的游戏规则改变者。现在你有了将这些概念应用于你自己文本分析任务的知识。
希望你发现这篇文章对你有帮助。如果你有任何问题或想法,我很乐意在评论中阅读它们!
解锁慢变维(SCD)的秘密:八种类型的全面视角
深入指南:何时以及如何使用 8 种类型的 SCD
·发布在Towards Data Science ·12 分钟阅读·2023 年 7 月 17 日
--

照片由Donald Tran拍摄,发布在Unsplash
如果你正在设计数据仓库,了解维度建模的方法论至关重要。这个方法论中最重要的概念之一是慢变维(SCD)。许多在线资源提供了关于慢变维的概述,但往往缺乏对使用哪种类型 SCD 的深入解释。
本文将讨论八种类型的 SCD,每种类型都有其独特的特点和应用场景。到最后,你将清楚地理解每种类型,并能在创建下一个维度建模中的 SCD 时进行区分。
为什么在维度建模中需要 SCD?
通常,OLTP 只存储近期数据。在执行更新语句时,当前数据会被替换,之前的数据将丢失,除非系统专门设计来保留历史数据,否则可能会增加延迟。
另一方面,OLAP 系统通常使用维度建模,它关注趋势并为业务提供洞察,通常更愿意保留历史记录。
SCD 的主要目标是追踪变化的历史。
在维度建模中,最常用的模式是星型模式。该模式围绕一个大的事实表展开,该事实表包含了您业务的各种度量字段。围绕事实表的维度表包含了允许对事实表中的数据进行操作和分析的字段。事实表通常有许多行,并且期望快速更新。另一方面,维度的变化很慢或永不变化。
替代键的重要性
事实表与维度表之间的连接是每个表中的键。这些通常是替代键,而不是 OLTP 系统中使用的 ID。替代键通常在数据仓库处理数据时生成,并在查找 OLTP ID 后确定。这对于某些类型的 SCD 来说至关重要,以避免在表上丢失主键,我们将在稍后的类型 2 中讨论这些问题。
为什么在维度建模中必须生成“替代”键,而不是重用原始业务 ID?使用替代键的一个好处是能够跟踪历史变化,同时避免键的重复。
让我们以一个名为“公司”的表作为 OLTP 系统中的示例。假设我们只有一家公司叫 Xesla,总部在加州帕洛阿尔托。然后 Xesla 决定迁往加州奥斯丁。然而,由于夏天湿度过高,Xesla 又回到了加州帕洛阿尔托。
如果我们查看 OLTP 端,它通常只保留当前的 location_id,而总部从加州搬迁到奥斯丁再到加州的过程是不可见的。

公司表的 OLTP 模式 | 作者提供的图片
在 OLTP 中直接执行分析查询有一定的劣势。对历史数据的忽视可能对业务不利。我们无法回答诸如“公司利润因总部搬迁而表现如何?”、“由于总部搬迁,员工流动率如何?”等问题。
在这种情况下,跟踪历史数据和替代键有助于保持行的唯一性和表的完整性。
我们引入了一个作为替代键的“键”列,这解决了多个问题。首先,它使我们能够跟踪公司总部的迁移历史。其次,它提供了一个唯一的主键,替代了业务 ID,消除了违反要求每行的主键唯一性的基础数据库规则的情况。

维度的替代键 | 作者提供的图片
我们仍需讨论维度表与事实表之间的关系及其使用。我们将在讨论每种 SCD 时解决这些问题。
SCD 类型 0 — 不允许更改
概述
SCD 是一种“特殊”类型的 SCD,不跟踪变化。本质上,类型 0 表示维度将保持不变。一旦条目进入 SCD 类型 0,它将不会改变。例如,我们可以有一个不会改变的日期维度。
这里一个令人困惑的点是日期维度将是固定的(SCD 类型 0),但我们对日期的解释可能会有所不同。例如,我们可能有与日历年不同的财政年度。我们可以设计我们的事实表,其中一个键是 calenderKey,另一个键是 fiscalCalenderKey。
实施
SCD 类型 0 也可以包括业务 ID,因为变化不会被跟踪,原始条目不能被覆盖。
何时考虑使用
对于固定维度,如日期和时间,SCD 类型 0 是最佳选择。这种类型最适合在您确定维度只需要添加新记录而没有未来更新时使用。
SCD 类型 1 — 更改覆盖
概述
SCD 类型 1 类似于许多 OLTP 设计,这些设计用新数据替换原始数据,使得跟踪单行的变化变得不可能。
与 SCD 类型 0 不同,类型 1 中更新维度行是可实现的。然而,类型 1 不保留历史记录。使用数据的用户可以假设维度的当前快照始终是最新的。
实施
SCD 类型 1 的实现可以是直接的:查找正确的 ID 并执行更新。
何时考虑使用
“我们是否总是希望保留历史记录?”
是否需要跟踪历史数据取决于您的业务用例。保留更多的数据点可以提供对数据集的更全面了解。将历史记录纳入您的维度建模设计可能是有利的。然而,不跟踪历史记录可以简化 ETL 管理。
有趣的是,前两种 SCD 类型无法跟踪任何变化。更强大的 SCD 类型从 SCD 类型 2 开始。
SCD 类型 2 — 使用新行跟踪历史记录
概述
SCD 类型 2 是一种强大的 SCD 类型,广泛用于跟踪维度建模中的变化。它具有快速显示变化历史记录的能力,非常实用。
当数据发生变化时,SCD 类型 2 会复制未更改的字段,这可能会重复并增加存储成本。为了更好地适应快速更新的维度,需要改进。
我们已经有了类似于我们在 Xelsa 示例中查看的模式的代理键。现在,我们可以添加有效的开始和结束日期,以指示该行处于活动状态的时间段。
在这种情况下,当前活动行的有效结束日期设置为未来的 12/31/9999,因此它不会(至少在较长时间内)过期。

SCD 类型 2 示例 | 作者提供的图片
实施
要执行 SCD 类型 2,我们需要执行两个任务:
-
添加代理键
-
添加一个当前行标识符,例如:开始/结束日期、版本号或当前标志。
在 ETL 方面,相比于 SCD 类型 1,实施过程更为复杂。此过程通常包括以下步骤:
-
用所有行的结束有效日期设置为 12/31/9999 来初始化维度表
-
当识别到更新时,查找 ID 以确定正确的行
-
按需要更新现有行的结束日期。
-
插入一行新的数据,ID 相同但替代键不同。使用前一行的结束日期 + 1 作为新行的开始日期,并将结束有效日期设置为 12/31/9999。

SCD 类型 2 实施步骤 | 图片由作者提供
何时考虑使用
关于 SCD,默认情况下通常选择类型 2 选项,因为它对消费者更简单。然而,如果一个维度频繁发生变化,可能会有更好的选择,因为重复未更改的字段多次可能会很昂贵。例如,我们有一个展平的维度,包含 50 个字段,一次字段更新可能导致其余 49 个字段再次重复。
SCD 类型 3—使用新字段跟踪历史

图片由 Super Snapper 在 Unsplash 提供
概述
我认为 SCD 类型 3 是一种“队列”选项。与其复制整个行并跟踪表的历史,SCD 类型 3 只跟踪选定字段的变化。由于类型 3 是在列级别进行的,保持有限数量的历史(通常是当前值和前一个值)是其实施方式。
继续以 Xesla 为例,假设我们只对跟踪状态和城市历史感兴趣。我们可以包括前一个状态和城市的列以跟踪历史。这里的历史仅限于最后一个,但我们可以通过额外的列追溯尽可能远的历史。

SCD 类型 3 示例 | 图片由作者提供
实施
SCD 类型 3 类似于一个大小有限的队列。在更新过程中,需要执行查找,前一个值将被丢弃,然后当前值将移到前一个值字段。
何时考虑使用
如果你希望跟踪一小部分列的历史,SCD 类型 3 是有利的。为了有效掌握业务的性质和变化频率,拥有坚实的知识基础是至关重要的。特别是在将 SCD 类型 3 扩展到新领域时,这一点尤为重要,因为这需要添加额外的列。
SCD 类型 4—使用单独的表(迷你维度)跟踪历史
概述
我们已经覆盖了通过 SCD 类型 2 在同一表中垂直跟踪历史和通过 SCD 类型 3 水平跟踪历史。SCD 类型 4 就像 Z 轴一样,我们在另一个表中跟踪历史。
一个表是当前视图表,另一个表是历史表。这与 CDC 的概念类似。当前表显示现在;历史表变更日志可以从头重建一切。

SCD 类型 4 | 作者图片
如果我们将这两个表合并,那就是 SCD 类型 2。那我们为什么要拆分当前视图和历史视图?主要原因是如果维度表更新迅速并最终达到数百万条记录,拆分当前视图可以帮助提高查询性能。保持当前视图相对较小可以提升最终用户的查询性能。
实现
关于数据更新,SCD 类型 2 涉及在同一个表中更新信息。然而,对于 SCD 类型 4,你必须使用两个独立的表。SCD 类型 4 的数据更新过程与 SCD 类型 2 遵循相同的模式。
使用时机
如果 SCD 类型 2 变得过大并开始影响查询速度,可能值得考虑切换到 SCD 类型 4。此外,如果你注意到维度表更新频繁,SCD 类型 4 可能是一个更好的选择,以准确跟踪完整的历史记录。
以下 SCD 类型 5、6、7 都是混合方法
SCD 类型 5 — 类型 4 小维度 + 类型 1 附加表
概述
SCD 类型 5 是对 SCD 类型 4 的一种增强,它融合了类型 1 的元素。它允许将一个维度的当前视图嵌入到另一个维度中。
SCD 类型 5 创建了一个雪花模式,并将当前视图保留为附加维度。然而,对于任何 BI 工具或展示层,将这两个维度合并为一个更好,以减少混淆。
例如,我们希望将公司的维度嵌入到员工表中,并链接到 employer_key。Current_Company_D 是当前视图。如在 SCD 类型 4 中讨论的那样,它通过 employer_key 作为外键引用。
如果需要进行更新,它将像 SCD 类型 1 一样工作。这意味着我们必须用新的当前公司 ID 参考覆盖 employer_key。为了避免与 SCD 类型 4 的历史数据混淆,最好将当前表前缀设置为 "Current_"。

SCD 类型 5 | 作者图片
实现
在 ETL 层维护 SCD 类型 5 可能是一项复杂的任务。这需要大量的工作来与 SCD 类型 4 和类型 1 结合。此外,确保适当的引用和准确的查找非常重要。
三个表必须一致并在更新时进行实现:主表、当前视图表和历史表。
使用时机
与其他 SCD 类型相比,SCD Type 5 通常讨论较少。管理 SCD Type 5 的 ETL 工作量很大。当当前视图的更新速度迅速增加时,单靠引用键变更很难跟踪所有变化,这时可以考虑使用 SCD Type 5。
SCD Type 6— 混合 SCD 1 + SCD 2 + SCD 3
概述
SCD Type 6 是将 SCD 1 + SCD 2 + SCD 3 混合而来的。这一过程初看可能比较冗长,但当你需要澄清未来潜在变化时,它非常适用。拉尔夫·金博尔(Ralph Kimball)说 SCD Type 6 是“一版本覆盖的不可预测变化”。
让我们继续使用示例来说明 SCD Type 6 的工作原理。

SCD Type 6 | 作者提供的图片
要将 SCD Type 6 拆分为各个类型,我们可以通过检查以下内容来实现:
-
SCD Type 1:我们为特定 ID 的所有行覆盖当前状态和当前城市。
-
SCD Type 2:当发生新变化时,我们创建第二行以跟踪演变和历史,并且翻转当前行标志和有效日期。
-
SCD Type 3:我们还添加了历史状态和历史城市字段,以便我们知道过去的值是什么。
实施
SCD Type 6 是 SCD Type 1 到 3 的组合。实施细节可以参考上述之前的实施方案。
使用时机
SCD Type 6 拥有丰富的上下文,并且可以相对快速地执行查询。由于它是一种混合的 SCD 方法,所有功能都包含在一个视图中,初看时可能会吸引你。
然而,SCD Type 6 增加了维度的行和列,导致增长速度远超用户的预期。在这些情况下,找到灵活性与丰富性之间的平衡至关重要,进行业务访谈和对齐也非常关键。
当你想展示信息的呈现方式并在一个视图中展示各种 SCD 类型时,SCD Type 6 是一个出色的 POC 选择。
SCD Type 7 — 通过逻辑拆分重新思考 Type 6
概述
SCD Type 7 是 SCD Type 6 的一种变体,也称为“Dual Type 1 and Type 2 Dimensions”。在 SCD Type 7 中,我们将一个 SCD Type 6 维度表分成两个不同的表:历史视图和当前视图。我们在事实表中使用双重外键来引用这些表。

SCD Type 7 | 作者提供的图片
使用 SCD Type 7 的一个优点是它允许我们为历史和当前表保持独立的键,保留各自的历史。这消除了将当前公司表绑定到与历史表相同的代理键的需要。
实施
基于当前视图,SCD Type 7 需要较少的 ETL 努力,因为它是从历史表中衍生出来的视图。一旦你有了 SCD Type 2 表,剩下的任务就是创建当前视图表并将键连接到事实表。
使用时机
使用 SCD 类型 7,用户可以快速访问当前视图和历史数据,而无需花费时间理解维度表的结构。与需要选择生效日期或当前行标志来查看现有数据的 SCD 类型 2 不同,SCD 类型 7 通过在事实表中推导出另一个关键字段来简化了这一过程。这为用户节省了时间和精力。
最终思考
理解慢变维(SCD)在数据仓库的维度建模中至关重要。虽然 SCD 类型 2 或类型 3 常用于跟踪历史,但其他类型的 SCD 也可以根据用户需求解决特定的问题。
我希望我对维度建模中的慢变维(SCD)的深入探讨能够对你在设计和构建数据仓库时有所帮助。
解锁用户激活与根本原因分析
逐步指南:如何进行结构化根本原因分析
·
关注 发布于 Towards Data Science ·6 分钟阅读·2023 年 3 月 20 日
--
在我之前的文章中,我讨论了如何为你的业务定义激活指标——那些你可以在短期内推动并在长期内产生影响的指标。
建立这些是迈向产品增长伟大旅程的第一步,一旦你识别出这些内容——这时有趣的部分才开始。你接下来的任务就是理解哪些方法和最有效的杠杆可以推动这些内容。
这就是根本原因分析可以帮助你的地方。RCA 是一种结构化的问题解决方法,可以提供对使用哪些杠杆以及它们对你期望结果的贡献程度的更好理解。它将帮助你深入了解当前的问题并识别其根本原因。
我特别喜欢这种分析方法,因为它位于定量和定性方法的交汇点。这使其成为一种高度跨职能和全面的研究,其中利用定性数据对于识别潜在原因和测试假设是必要的。
为了让这些内容更易于理解,我们将重用我在之前文章中使用的相同例子:
-
你经营一个健身应用程序。
-
你刚刚发现,上传锻炼视频的用户在下载应用程序后的 7 天内更有可能继续使用你的应用程序,而不是那些没有上传的用户。
-
你现在在思考如何才能最好地推动这个指标。

Matteo Grando 拍摄的照片,来源于 Unsplash
什么是 RCA
根本原因分析(RCA)是一种结构化的问题解决方法,它帮助你识别问题的根本原因,而不仅仅是处理其症状。它可以让你真正了解“为什么”——为什么事情会以这种方式发生。其输出可以有多种形式——但通常可以表示为“决策树”或“失败树”,这些形式易于理解,非常直观,商业利益相关者尤其喜欢。
Digressing a bit — this methodology can be applied to virtually any problem,
and in its simplest form, it doesn’t require any particular data knowledge.
If properly used, it can be a great tool to make you think about complex
systemic interactions and allow you to move away from a symptom to its cause.
它通常包括大约 5 个步骤:定义问题、收集数据、识别和评估不同的原因、识别根本原因以及制定解决方案计划。此外,它不仅允许你了解“为什么”,还可以让你对如何能在你的问题上取得多大进展有个粗略估计(从这些估计中你可以建立优先级模型、OKRs 等)。
常规的根本原因分析步骤
#1: 定义问题:根本原因分析的第一步是明确你想解决的问题。在我们的例子中(以及在此阶段),问题相当直接:“我们如何在用户注册我们的移动健身应用程序后的 7 天内增加上传锻炼的用户数量,以提高用户保留率?”
#2: 收集数据:下一步是收集有关问题的数据。这包括收集有关问题的信息,例如何时发生/发生了什么/谁受到影响/他们如何受到影响。重要的是要获得问题的定量和定性视角,并与受影响的人员和/或该主题的专家交谈。
#3: 识别和评估可能的原因:这才是真正有趣的部分!一旦你有了数据,下一步就是识别问题的可能原因。这包括与一些主题专家进行头脑风暴,并与用户交谈,以便能够全面了解可能导致问题的所有因素——包括非常明显的因素,也包括那些可能不那么明显的潜在原因。
在这一步中,你可以使用回归分析来获取一些灵感。当你完全不知道从哪里着手时,回归分析会很有用。它可以帮助你找到与问题高度相关的特征,然后从那里手动深入研究,以更好地理解问题。
在我们的例子中,假设我们发现某些国家或设备与激活的可能性高度负相关。在这种情况下,可能值得进一步探讨:
-
也许翻译存在一些问题?
-
也许在某些设备上存在显示错误?
-
等等。
Note that as always, correlation is not causation — this method can help you
find subsets of users worth looking into, but the results shouldn’t be taken
directly (i.e. without further checking) for your root cause analysis.
一旦你识别出所有可能的原因,并开始构建你的树形图,重要的是要评估这些原因并了解它们对问题的贡献程度。对于一些原因,这将很简单,因为你会有数据;而对于其他原因,则可能稍微复杂一些,你可能需要收集更多的数据和/或发挥一些想象力来生成一些现实的估计——但通过这些权重,当你开始撰写建议并决定调整哪些杠杆时,这将使事情变得更加简单。
#4: 迭代直到识别出根本原因:现在你已经识别出决策树的第一个节点,你可以重新迭代,直到找到实际的原因。作为一个经验法则,你应该尝试进行最多 5 次迭代(即“5 个为什么”技术)——这是一个很好的强迫性方法,挑战你真正思考第二/第三层次的效果,而不仅仅是明显的原因。
如果我们回到我们的例子,它可能看起来像这样:

针对健身应用激活率的根本原因分析示例(图像由作者提供)
#5: 制定行动计划:现在你已经对潜在根本原因有了清晰的了解,最后一步是制定一个行动计划来解决这些问题。我写了一篇关于“如何优先考虑进行哪些数据科学项目”的文章:这里的思路类似:你需要根据不同的参数来优先考虑要调整哪个杠杆,这些参数可以包括(但不限于):机会的大小、执行能力、对结果的信心、市场时间等。

数据项目的时间投资矩阵(图像由作者提供)
附赠:根本原因分析(RCA)和目标设定(例如 OKRs)
根本原因分析(RCA)不仅能让你更好地了解是什么驱动了问题,还能让你更好地理解如何解决它,以及你的修复可能会如何影响最终指标。
你在这里所做的工作,实际上也可以用于为你的公司设定目标/关键成果(OKRs):在上述示例中,你可以使用不同的权重(可能使用你用于优先级排序的参数,如对结果的信心进行折扣)来告知公司可以设定的不同目标。
最终,根本原因分析(RCA)是一个易于使用、漂亮的工具,可以帮助你的团队做出更好的决策。它不需要高水平的技术技能,但需要有组织性,并结合定量和定性数据。如果操作得当,它可以产生巨大的影响。
希望你喜欢阅读这篇文章!你有什么想分享的技巧吗?请在评论区告诉大家!
如果你想阅读更多我的文章,这里有一些你可能会喜欢的内容:
构建产品激活指标的简短指南
来自于一些构建了几个不成功的项目的人提供的清单
如果你对如何使用时间有合理的方法,你可以优化你产生的价值。
揭示 AI 对跨性别社区的有害影响
原文:
towardsdatascience.com/unmasking-ais-detrimental-effects-on-the-trans-community-d8f870949d79
性别识别软件的危险、不足的医学模型以及跨性别恐惧内容的放大
·发布于Towards Data Science ·阅读时间 8 分钟·2023 年 6 月 20 日
--

图片由Delia Giandeini在Unsplash提供
对 AI 风险的讨论常常集中在人工通用智能(AGI)和世界末日情景的假设危险上。机器人不会统治世界。然而,目前的 AI 确实带来了切实的风险,特别是对已经受到此技术影响的跨性别和性别非顺应社区。
我们将重点阐述对该社区的危险:
-
自动性别识别
-
医学模型的局限性
-
社交媒体上跨性别恐惧内容的放大
虽然跨性别社区感受到直接后果,但这些危险影响到我们所有人。它们传播仇恨,限制了多样性的丰富性,制约了我们全面表达自我的集体能力。我们必须理解作为技术专业人士的角色如何支持跨性别者,并创造一个更强大的社会。
我们已进入可以大规模部署 AI 的阶段,仅因为我们拥有大量数据和计算能力。令人担忧的是,AI 未能应对伦理挑战。
面部滤镜
我们将通过一个表面上看似不严重的例子来逐渐了解这些危险。如果你在社交媒体上,你知道什么是面部滤镜。它们使用机器学习来扭曲你的脸,使你看起来年老,甚至把你的狗变成迪士尼角色。大多数人会认为这些只是无害的娱乐。谈到性别时,情况可能会更复杂。不过,负面后果不应被夸大。
我只是一个盟友,不能代表跨性别者发言。即使在跨性别社区内部,性别转换滤镜的后果也是有争议的。它们可以让你探索你的性别认同和表达。然而,它们也可能强化性别刻板印象和排斥非二元性别者。一些人甚至利用这些滤镜来嘲弄过渡过程。
探索性别和与分配给你的性别不同的性别是好事,我鼓励这样做。你可能会发现自己意外的新东西,可能会对跨性别者更加友善。
在讨论这类技术时,应区分允许你选择性别的应用程序和那些试图预测性别的应用程序。例如,查看 Pixar 滤镜合集中的第一个视频。算法在用户没有传统男性或女性特征时会遇到困难。
这揭示了这些应用程序的问题——其底层技术基于一个假设:你可以预测某人的性别认同。这是一种伪科学。将这个假设应用到其他场景可能会有重大后果。
自动性别识别(AGR)
AGR 或性别识别软件是机器学习的一个分支,试图预测一个人的性别。这是通过分析面部特征、身体形状、衣物、声音模式或行为特征来实现的。然而,性别是复杂的,这些方面无法完全捕捉。尤其是对跨性别者来说更是如此。
对 4 个 AGR 系统的研究,如图 1所示,平均数据显示它们误判跨性别女性的比例为 12.7%,跨性别男性为 29.5%。相比之下,生理女性和男性的误判率为 1.7%和 2.4%[1]。这些系统还完全忽视了其他性别群体。

图 1:AGR 系统的准确性(来源:M. K. Scheuerman 等)
错误地确定跨性别者的性别是不尊重的。这也可能对心理健康产生严重影响。持续被称为你不认同的性别可能是既令人疲惫又令人气馁的。现在想象一个世界,这一切被自动化并融入我们的日常系统中。
你不必想得太多。 这些系统已经被部署了:
-
一家公司自动化了广告牌以向男性推广披萨,向女性推广沙拉。
-
柏林的公共交通运营商向女性旅客提供了 21%的折扣。
-
Giggle,“仅限女性”的社交网络应用程序拒绝接受跨性别女性。
-
变性 Uber 司机的账户被面部识别安全系统暂停了。
这些系统造成的伤害是众所周知的,以至于欧盟已被敦促禁止它们。
依赖于性别的医疗保健模型。
AGR 涉及机器学习,其中性别是目标变量。当我们将性别作为模型特征时也会出现问题。更具体地说,当我们不区分性别(出生时分配的性别)和性别(社会构建的角色)时。这一问题在医疗模型中普遍存在。
在医疗保健中,性别和性别认同常常被混淆。以至于提出了性别–性别认同医学这一术语[2]。事实上,关于跨性别和其他性别群体的数据收集非常有限。结果是,使用单一二元特征——男性/女性来训练模型,以出生时分配的性别作为性别和性别认同的代理[3]。
性别和性别认同是二元的、静态的且一致的错误假设在医疗系统中深深根植。
问题在于,许多诊断和治疗中,性别与性别认同之间的互动非常重要[4]。这对于 HIV 预防、生殖健康、荷尔蒙替代疗法和心理健康都适用。将性别与性别认同合并为一个变量,我们在医疗系统中忽视了跨性别者。结果是相比于顺性别者,跨性别者的护理水平较差。
在社交媒体上放大跨性别恐惧症内容。
到目前为止,我们关注的是更直接的影响。通过巩固性别期望和模型表现不佳,AI 可能导致跨性别者体验到负面影响。AI 也可能有间接影响,即影响他人对跨性别者的看法。
社交媒体推荐算法的唯一工作就是保持你在平台上。不幸的是,对你不属于的群体的愤怒,特别是,对激发参与的愤怒,[5]非常有效。还有担忧认为这些算法可能会强制执行已有的信念[6]。也就是只推荐与你过去互动过的内容类似的内容。
性别在现代社会规范和期望中占据核心地位。跨性别者的存在可能挑战这些规范。对于一些人来说,这种挑战带来了恐惧、愤怒以及不愿接受科学事实。这些都是容易导致增加参与度并创造反跨性别回音室的条件。
我们在Facebook上看到了这一点。用户在这里对影响跨性别者的问题有着偏颇和不准确的理解。如图 2所示,右倾页面上的跨性别问题帖子获得的互动几乎是其他页面的两倍。这些互动大多数出现在反跨性别网站发布的帖子上。

图 2:2020 年 10 月至 2021 年 9 月,各页面意识形态下的所有跨性别相关帖子在 Facebook 上的互动情况(图片来源:作者)(来源:媒体关注)
Facebook 并不是唯一存在问题的平台。在与反跨性别内容互动后,TikTok会把你带入极端主义、仇恨和暴力的兔子洞。我在 YouTube 短视频中被推荐反跨性别内容的经历促使我写下这篇文章,这也是其他人的共同经历。
这些平台上的内容试图推动错误的说法,即跨性别是一种意识形态或心理疾病。事实并非如此。它们还试图将辩论从基本人权问题转移到体育、卫生间和代词上。最隐蔽的做法是将追求平等重新框定为对儿童的攻击。
跨性别社区对儿童没有任何风险。然而,这些内容对儿童却构成了重大风险。在 2023 年,79 项反跨性别法案在美国通过。社交媒体被认为对这些政策变化有所贡献。反跨性别内容也导致了负面的社会变化。
82%的变性人曾考虑过自杀,40%的人尝试过自杀。导致这一数据的最重要因素是简短而普遍的日常侮辱或轻视[7]。这些行为被反变性内容正常化和推广。
人际微侵略对终生自杀尝试产生了独特且统计学上显著的影响。
基于这些后果,社交媒体平台在道义上有责任遏制这些内容。至少,应该将其标记为虚假和不科学。我们都应该拒绝变性恐惧。作为科技工作者,我们也应该利用我们独特的影响力。我们有能力反击这些趋势,并塑造那些伤害变性人的系统。
我们可以从了解 变性意味着什么 开始。我们可以推动包容性训练数据和更多样化的团队。我们还应该倡导旨在增加透明度、可解释性和人工监督的 AI 系统监管。在这样做时,我们不应被假设性的灾难情景分散注意力,而应关注 AI 的直接风险。
本文中的所有中等合作资金将捐赠给 TGEU。如果你想了解变性意味着什么或如何成为更好的盟友,请查看下面的视频。快乐的骄傲月 😃
变性的神经科学
你可以在 Mastodon | Twitter | YouTube | Newsletter 上找到我——免费注册以获得 Python SHAP 课程。
[## 通过我的推荐链接加入 Medium — Conor O’Sullivan
作为 Medium 会员,你的会员费的一部分会用于支持你阅读的作者,并且你可以完全访问每个故事……
conorosullyds.medium.com](https://conorosullyds.medium.com/membership?source=post_page-----d8f870949d79--------------------------------)
参考文献
[1] Scheuerman, M.K., Paul, J.M. 和 Brubaker, J.R., 2019。计算机如何看待性别:对商业面部分析服务中性别分类的评估。ACM 人机交互学报, 3(CSCW), 第 1-33 页 docs.wixstatic.com/ugd/eb2cd9_963fbde2284f4a72b33ea2ad295fa6d3.pdf
[2] Campesi, I., Montella, A., Seghieri, G. 和 Franconi, F., 2021. 个人护理需要性别和性别方法。Journal of Clinical Medicine, 10(20), 第 4770 页. www.ncbi.nlm.nih.gov/pmc/articles/PMC8541070/
[3] Chen, I.Y., Pierson, E., Rose, S., Joshi, S., Ferryman, K. 和 Ghassemi, M., 2021. 医疗中的伦理机器学习。年度生物医学数据科学回顾, 4, 第 123–144 页. www.ncbi.nlm.nih.gov/pmc/articles/PMC8362902/
[4] Albert, K. 和 Delano, M., 2022. 性别困扰:使用电子健康记录的机器学习中的性别/性别滑移、性别混淆和性别痴迷。Patterns, 3(8), 第 100534 页. www.sciencedirect.com/science/article/pii/S2666389922001313
[5] Rathje, S., Van Bavel, J.J. 和 Van Der Linden, S., 2021. 群体外敌意驱动社交媒体参与。国家科学院学报, 118(26), 第 e2024292118 页. www.pnas.org/doi/abs/10.1073/pnas.2024292118
[6] Jiang, R., Chiappa, S., Lattimore, T., György, A. 和 Kohli, P., 2019 年 1 月. 推荐系统中的退化反馈循环。在 2019 AAAI/ACM 人工智能、伦理与社会会议论文集 (第 383–390 页)。 arxiv.org/pdf/1902.10730.pdf
[7] Austin, A., Craig, S.L., D’Souza, S. 和 McInroy, L.B., 2022. 跨性别青少年的自杀倾向:阐明人际风险因素的作用。人际暴力杂志, 37(5–6), 第 NP2696-NP2718 页. pubmed.ncbi.nlm.nih.gov/32345113/
解开物理信息神经网络的设计模式:第二部分
通过集成学习和动态解空间扩展来增强 PINN 训练稳定性
·发表于 Towards Data Science ·7 分钟阅读·2023 年 5 月 19 日
--

图片由 Clay Banks 提供,Unsplash
欢迎来到本系列关于物理信息神经网络(PINN)设计模式的第二篇博客🙌 在这里,我们将探讨集成学习和动态解空间扩展技术,以增强 PINN 训练的稳定性和准确性。
展望未来,我们将从提出的策略试图解决的具体问题开始,然后详细介绍该策略、其实施方式以及为什么可能有效。之后,我们将看看哪些物理问题被用作基准,以及所提出方法的优缺点。最后,我们讨论替代方案和该方法的未来机会。
随着本系列的不断扩展,PINN 设计模式的集合变得更加丰富🙌 这里有一个预告,展示你将看到的内容:
PINN 设计模式 05:自动超参数调整
PINN 设计模式 07:使用 PINN 进行主动学习
让我们开始吧!
1. 论文概览:
-
标题:通过模型集成改进物理信息神经网络的训练
-
作者:K. Haitsiukevich, A. Ilin
-
机构:阿尔托大学
-
链接:arXiv
2. 设计模式
2.1 问题
物理信息神经网络(PINNs)提供了一种解决偏微分方程(PDEs)的有力途径,因其固有的灵活性、优雅性和对观测数据的无缝集成。尽管有这些优势,实际训练 PINNs 往往面临重大挑战:网络由于其复杂性和对超参数的敏感性,倾向于收敛到不正确的解决方案。
这个问题对 PINNs 应用的现状有着重要影响,因为它限制了它们能够产生的解决方案的准确性和可靠性。
现在的问题是:我们如何改进训练过程,以确保 PINNs 收敛到正确的解决方案?

PINN 工作流程。在实践中,PINNs 在训练过程中可能会收敛到错误的解决方案。一种有前景的解决方法是逐步扩展解区间,以解决这个问题。(图像由本博客作者提供)
2.2 解决方案
关键思想是逐步扩展解区间和利用集成学习。

残余点逐渐被纳入,以模拟解域扩展的行为。(图像由本博客作者提供)
在开始时(a),仅部署接近已知初始条件下采样点(实心蓝色矩形)的残余点(预先采样以填充整个解域)用于 PDE 损失计算。这启动了 PINN 训练,其中多个初始化权重不同的 PINN 模型进行训练,从而形成 PINN 的集成。在训练若干次迭代后(b),如果在这些位置的 PINN 集成预测的方差足够小,则将一些先前部署的残余点视为“伪点”。随后,解域可以扩展(c),如果新的残余点足够接近“伪点”,则将部署新的残余点。迭代继续进行(d),直到所有残余点都用于损失计算。
上述演练假设只知道初始条件。在其他情况下,如果观察数据在模拟域内可用,则解决区间也可以从那里扩展。
根据作者的实现,该策略引入了几个新的超参数,这些超参数决定了距离阈值以部署新的残差点,距离阈值以将部署的残差点转变为“伪点”,以及方差阈值以决定 PINN 集成是否一致。有关更多详细信息,请参阅原始论文。
2.3 为什么这个解决方案可能有效
解决方案区间的逐步扩展模拟了经典数值解算器的行为,这些解算器将解决方案从初始条件和边界条件“传播”到整个领域。因此,物理因果关系得到了尊重,这可能有助于更稳定的 PINN 训练。
通过利用 PINN 的集合,提出的算法将有更高的机会摆脱传统的单一 PINN 训练方法常见的错误解。这解释了为什么提出的算法能够达到更高的准确性和可靠性。
2.4 基准测试
论文对提出的策略在五个不同问题上的性能进行了广泛的基准测试,每个问题代表了一个独特的物理现象:
- 对流方程:这个方程模拟了输送现象,这在流体动力学、热传递和质量传递等领域中都是基础的。在这些场景中,理解能量、质量或动量等量的移动,主要由于差异性力量或梯度,是至关重要的。

- 反应系统:这一问题类别模拟了化学反应。从基础化学课程到复杂的生物工程过程,理解反应动力学和化学平衡可能是实验成功或工业过程成功与灾难性失败之间的区别。

- 反应扩散方程:这个方程模拟了反应与物质扩散的结合。这种问题在生物学等领域至关重要,它们描述了发育生物学中的模式形成过程,或在化学中模拟了不同物质在介质中的扩散和相互作用。

- 扩散方程(具有周期边界条件):这种方程对于理解热传导、流体流动和布朗运动等现象至关重要。周期边界条件意味着系统行为在时间或空间上重复,这是处理周期性或重复系统问题时常见的假设。

- 扩散方程(与上面展示的方程相同,但具有 Dirichlet 型边界条件):Dirichlet 型边界条件规定了解决方案在领域边界上必须取的值。

基准测试结果显示:
-
所提出的算法提供了稳定的训练,并对所有考虑的问题表现出竞争力;
-
所提出的算法通常稳定,对超参数选择的敏感性较小。
此外,论文建议以下做法可以帮助提高 PINN 的准确性:
-
将输入(包括空间输入 x 和时间输入 t)归一化到[-1, 1];
-
在通常的 Adam 优化后,使用 L-BFGS 对 PINN 训练进行微调。
2.5 优势与劣势
👍优势
-
能够稳定 PINN 的训练并取得竞争力的性能。
-
不需要预定义的间隔扩展计划(与“替代方案”部分中解释的时间自适应策略相对)。
-
时间和空间处理得同样。
-
足够灵活,允许在任意位置轻松融入测量值。
-
由于集成方法,预测的置信区间会自动获得。
👎劣势
-
相比于传统 PINN,计算开销较大(所有集成方法都存在这种情况)。
-
引入了额外的超参数(尽管所提出的解决方案对超参数值不敏感)。
2.6 替代方案
其他解决类似问题的方法是时间自适应技术系列,这些技术对当前的方法有很大影响。时间自适应技术将时间区间 [T₁, T₂] 划分为多个子区间,并通过单独的 PINN 依次在每个子区间上求解方程。一个子区间的解然后作为后续子区间的初始条件。
当前的算法继承了时间自适应技术的优点,即能够实现更准确和稳定的时间解传播。此外,它还消除了对预定义间隔扩展计划的要求。因此,当前的方法更灵活,可以在任意位置融入已知的测量值。
3 未来的潜在改进
有几种可能性可以进一步改进所提出的策略:
-
创建模型集成的高效方法(例如,dropout)。
-
将当前算法与其他 PINN 最佳实践结合(例如,在第一篇博客中讨论的智能采样残差点)。
4 收获
在这篇博客中,我们探讨了通过集成学习和动态解区间扩展来增强 PINN 训练的稳定性。这个话题非常相关,因为它直接影响 PINNs 的准确性。
以下是从论文中提出的设计模式中获得的收获:
-
[问题]:如何确保 PINNs 收敛到正确的解?
-
解决方案:1. 逐步扩展解区间,在整个领域内逐渐部署残差点。2. 集成学习,其中多个 PINN 使用不同的权重初始化。
-
[潜在好处]:1. 稳定 PINN 训练。2. PINN 预测的不确定性估计
如果你更喜欢视觉呈现,这里是我准备的 PINN 设计卡:

论文中提出的 PINN 设计模式。(图片由本博客作者提供)
我希望你觉得这个博客有用😃如果你想了解更多关于 PINN 设计模式的内容,可以随时查看这一系列中的其他文章:
-
PINN 设计模式 03:使用梯度提升的 PINN 训练
-
PINN 设计模式 07:与 PINN 的主动学习
期待在即将到来的博客中与您分享更多见解!
参考文献
[1] Haitsiukevich 等,使用模型集成改进物理信息神经网络的训练,arXiv,2022 年。
揭示物理信息神经网络的设计模式:第三部分
通过梯度提升训练超充 PINN 的性能
·发布于 Towards Data Science ·阅读时间 7 分钟·2023 年 5 月 25 日
--

照片由 Haithem Ferdi 提供,来源于 Unsplash
欢迎来到本系列的第三篇博客,在这里我们继续探索物理信息神经网络(PINN)的设计模式,进行令人兴奋的旅程。
在这篇博客中,我们将探讨使用梯度提升训练 PINNs,这是一种神经网络与梯度提升算法的激动人心的融合 🚀。
如往常一样,我将按照以下方式结构化这篇博客:
-
问题,提出的策略试图解决的具体问题;
-
解决方案,提出的策略的关键组件,它是如何实施的,以及它可能有效的原因;
-
基准,评估了哪些物理问题,以及相关的性能;
-
优点与缺点,提出的策略在什么条件下可能有效,同时也突出其潜在的局限性;
-
替代方案,为解决类似问题提出的其他方法,从而提供更广泛的潜在解决方案视角。
随着本系列的不断扩展,PINN 设计模式的集合变得更加丰富🙌 这里是对你即将迎接内容的预览:
PINN 设计模式 02: 动态解决方案区间扩展
PINN 设计模式 05:自动化超参数调整
PINN 设计模式 07:与 PINN 的主动学习
让我们深入了解一下!
1. 快速了解论文:
-
标题:用于物理信息神经网络的集成学习:一种梯度提升方法
-
作者:Z. Fang, S. Wang, P. Perdikaris
-
机构:宾夕法尼亚大学
-
链接:arXiv
2. 设计模式
2.1 问题
原始 PINNs 已知在模拟对输入的小变化非常敏感并需要高度准确性以准确捕捉其动态的物理过程时存在困难。这些物理系统的例子包括多尺度问题和奇异摄动问题,这些问题与流体动力学和气候建模等领域密切相关。

PINN 工作流程。原始 PINNs 在解决复杂 PDE 时通常面临挑战。解决此问题的一种有前景的方法是通过提升算法训练 PINNs。(图像由本博客作者提供)
2.2 解决方案
结果显示,其他机器学习算法也会遇到相同的问题,解决此问题的一种有前景的方法是采用“梯度提升”方法。因此,自然产生了一个问题:我们能否模拟梯度提升算法来训练 PINNs?论文对此给出了肯定的答案。
提升是一种通用的机器学习算法,可以简洁地表达为以下迭代形式:

在每一次提升轮次中,都会导出一个增量模型 hₘ(•) 并在上一次迭代的预测器 fₘ_₁(•) 上添加(以学习率 ρₘ 进行折扣),以便提高 fₘ(•) 的准确性。
现在,如果我们将 fₘ_₁(•)、fₘ(•) 和 hₘ(•) 替换为物理信息神经网络,我们可以实现通过提升算法训练 PINNs。下面是展示训练过程的示意图:

PINN 模型按顺序训练以迭代地最小化损失。只有标记为绿色的块是可训练的。损失是通常的 PINN 损失,即 PDE 损失、边界条件损失等。(图像由本博客作者提供)
在论文的实现中,加性 PINN 模型hₘ(•)的结构和超参数是预先确定的。这与原始的梯度提升算法不同,后者会利用梯度下降找到最佳的hₘ(•)形式。然而,作者声称使用预选的hₘ(•)仍然可以模拟提升算法的行为,但计算复杂性显著降低。
根据论文中进行的数值实验,通常 3~5 个 PINNs 足以提供令人满意的结果。对于设置学习率ρₘ,建议的做法是将初始ρ设置为 1,并且随着m的增加,ρ值按指数衰减。
2.3 解决方案为何可能有效
由于所提解决方案模拟了传统的“梯度提升”机制,因此自动继承了该方法提供的所有优点:通过顺序添加弱模型,每个新模型能够纠正前一模型所犯的错误,从而迭代地提高整体性能。这使得该方法在处理多尺度或奇异摄动问题等具有挑战性的问题时尤其有效。
同时,对于提升算法,即使每个提升阶段的组件模型相对“弱”,也仍然可以实现“强”的模型。这一特性使得整体 PINN 模型对超参数设置的敏感性降低。
2.4 基准测试
论文在四个不同的数学问题上对所提策略的性能进行了基准测试,每个问题都代表一个独特的数学挑战:
- 1D 奇异摄动问题:奇异摄动问题是一类特殊情况,其中方程中的某些项变得极小或极大,导致不同的行为模式,难以建模。这些问题通常出现在科学和工程的许多领域,如流体动力学、电路和控制系统。

ε = 1e-4。
- 2D 对流主导的扩散方程:这个方程模拟了对流效应(由于整体运动而产生的传输)远强于扩散效应(由于浓度梯度产生的传输)的物理现象。这些类型的问题出现在气象学(如风传播污染物)和海洋学(如洋流输送热量)等各种领域。

ε = 1e-3, Ω = (0, 1)²。
- 2D 对流主导的扩散问题(特征为弯曲流线和内部边界层):这是前述问题的一个更复杂的变体,其中流动模式弯曲,并且在问题领域内存在显著的边界层。这些复杂性需要更复杂的数值方法,使得问题更具现实世界挑战的代表性。

ε = 1e-4, Ω = (0, 1)², β = eˣ(sin(y), cos(y))。
- 2D 非线性反应-扩散方程(时间依赖):这个方程模拟了物质扩散与反应的结合,但它也具有非线性,并且随时间变化。这类问题在生物学和化学等领域很常见,在这些领域中,物质在介质中相互作用并扩散,且反应速率可能随时间变化。


Ω = (0, 2π),周期边界条件。
基准研究结果显示:
-
与传统的 PINNs 相比,所提出的算法在所有测试案例中显示了显著的准确性提升;
-
所提出的算法显示出鲁棒性,对超参数选择不太敏感。
2.5 优缺点
👍优点
-
相比于单一的 PINN,准确性显著提升。
-
对网络结构和排列的选择具有较强的鲁棒性。
-
调整超参数所需的努力较少。
-
灵活且可以轻松与其他 PINNs 技术集成。
👎缺点
-
不适用于解决具有导数爆炸的守恒定律(例如,无粘性 Burgers 方程、Sod 冲击管问题等),这是由于这些方程解对 PDE 损失的敏感性不足。
-
在可扩展性方面存在限制,因为它可能需要更多的计算资源和时间来顺序训练多个神经网络。
2.6 替代方案
由于这是第一篇将提升算法引入 PINN 领域的论文,目前尚无类似的工作。
尽管如此,在增强 PINN 建模具有挑战性的物理过程的能力方面,论文特别提到了 Krishnapriyan et al. 的工作。该策略是将时间域划分为子区间,PINNs 被逐步构建以模拟每个子区间(类似于 之前博客 中涵盖的想法)。
目前的论文在最后的基准案例研究(上述 2.4 节)中将 Krishnapriyan 的方法与新提出的方法进行了比较。结果表明,所提出的提升方法能够将误差降低 4 倍。
3 未来潜在改进
对所提出的策略的进一步改进包括研究神经网络的最佳顺序组合,与其他类型的神经网络架构混合并匹配梯度提升训练迭代,以及将 PINN 训练的其他最佳实践(例如,残差点生成)集成到梯度提升训练框架中。
4 主要收获
在这篇博客中,我们探讨了一种基于提升的集成学习的新型 PINN 训练范式。这个话题非常相关,因为它增强了 PINNs 处理像多尺度和奇异摄动问题这样具有挑战性问题的能力。
和往常一样,这里是本论文提出的设计模式的要点:
-
[问题]:如何提升 PINN 解决复杂问题的能力?
-
解决方案:梯度提升,其中多个“弱”PINN 按顺序训练,以迭代地提高整体性能。
-
[潜在的好处]:1. 能够解决简单 PINN 的复杂问题。2. 超参数调整的工作量减少
这是另一个 PINN 设计卡片:

论文中提出的 PINN 设计模式。(图像由本博客作者提供)
希望您觉得这篇博客有用😃如果您想了解更多关于 PINN 设计模式的内容,请随时查看本系列的其他帖子:
-
PINN 设计模式 07:利用 PINN 进行主动学习
期待在即将到来的博客中与您分享更多见解!
参考文献
[1] Fang 等,物理信息神经网络的集成学习:一种梯度提升方法,arXiv,2023。
[2] Krishnapriyan 等,物理信息神经网络中可能的失效模式的特征,arXiv,2021。
揭示物理信息神经网络的设计模式:第四部分
利用梯度增强学习提高 PINN 训练效率
·发表于 Towards Data Science ·7 min read·2023 年 5 月 29 日
--

图片由 Hassaan Qaiser 提供,来源于 Unsplash
欢迎来到本系列的第 4 篇博客,我们将继续激动人心的旅程,探索物理信息神经网络(PINN)的设计模式🙌
在这篇博客中,我们将探讨一篇提出了新的 PINN 变体的研究论文,这种变体被称为 梯度增强 PINN。更具体地,我们将研究 问题、解决方案、基准以及 优缺点,以提炼论文提出的设计模式。
随着本系列的不断扩展,PINN 设计模式的集合变得更加丰富🙌 这里是未来内容的一些预览:
PINN 设计模式 02: 动态解决方案区间扩展
PINN 设计模式 05: 自动超参数调优
PINN 设计模式 07:利用 PINN 进行主动学习
让我们开始吧!
1. 论文概览 🔍
-
标题:用于前向和逆向 PDE 问题的梯度增强物理信息神经网络
-
作者:J. Yu,L. Lu,X. Meng,G. E. Karniadakis
-
机构:圣马克学校(德克萨斯州)、宾夕法尼亚大学、布朗大学
-
链接:arXiv
2. 设计模式 🎨
2.1 问题 🎯
在实际应用中,通常观察到即使有许多训练点,传统 PINN 的准确性仍然有限,特别是在处理具有刚性解的挑战性 PDE 时。这一限制影响了 PINN 在解决各种前向和逆向 PDE 问题中的有效性,限制了其在高精度解决这些问题的能力。

PINN 工作流程。传统的 PINN 即使在有许多残差点的情况下通常也有限制精度。提升 PINN 准确性的一个有前途的方法是通过梯度增强学习算法来训练 PINN。(图片由本博客作者提供)
2.2 解决方案 💡
提升 PINN 准确性的一个有前途的方法是采用梯度增强学习方法来训练 PINN。
梯度增强学习在传统机器学习中已被证明是有用的[2]。如下面的插图所示,除了通常的输入-输出对(x,y),梯度增强学习还将已知的函数梯度值dy/dx作为额外的监督信号纳入。这种学习方法如果梯度信息可以低成本获得(例如,解析上可用、易于测量等),则可以有效。

常规学习(上图)只要求模型预测在 x 处与真实函数值 f(x)匹配;梯度增强学习(下图)额外要求模型预测在 x 处的导数与已知的梯度值 df(x)/dx 匹配。(图片改编自维基百科)
同样的理念也可以应用于 PINN 训练,如论文中所示。
以简单的二维拉普拉斯方程(∂²u/∂x² + ∂²u/∂y² = 0)为例,当使用 PINN 求解该方程时,我们会强制要求 PDE 残差f为零,其中f = ∂²u/∂x² + ∂²u/∂y²。f 本质上衡量预测是否符合控制方程,并构成训练 PINN 的整体损失函数中的 PDE 损失项。
在梯度增强 PINN(gPINN)中,我们还可以强制要求 PDE 残差的导数为零:

这样做的理由很简单:因为 f 在整个模拟域中为零,我们知道 f 的梯度也为零。因此,除了通常的 PDE 损失 f = 0 外,我们还会有两个额外的损失项。
2.3 为什么解决方案可能有效 🛠️
使 gPINN 有效的关键在于梯度提供了关于函数行为的额外信息。因此,它可以更有效地指导学习过程。这个特性在传统机器学习领域中已知,但当前论文显示相同的好处也可以用于 PINN 训练。
2.4 基准 ⏱️
论文考虑了总共 6 个不同的基准问题,包括 2 个前向问题,2 个反向问题,以及 2 个由标准 gPINN 和 gPINN+RAR 解决的带有刚性解的 PDE(基于残差的自适应细化用于采样残差点):
- 1D 泊松方程(前向问题,用标准 gPINN 解决):泊松方程是数学物理中一个基本的偏微分方程,描述了系统中物质的分布。

Dirichlet 边界条件:u(x=0) = 0,u(x=π) = π
- 扩散-反应方程(前向问题,用标准 gPINN 解决):该方程模型了物质的反应与扩散。这里的前向问题涉及在给定初始条件和反应速率的情况下预测物质的浓度。

D=1(扩散系数)

R 是化学反应

初始和边界条件
- Brinkman-Forchheimer 方程(反向问题,用标准 gPINN 解决,识别有效粘度 νₑ 和渗透率 K):该方程描述了在多孔介质中的流动,这在石油开采和地下水流动等领域很普遍。

边界条件:u(0) = u(1) = 0,H=1,ν=1e-3,ε=0.4,g=1
- 1D 扩散-反应系统(反向问题,用标准 gPINN 解决,识别空间依赖的反应速率 k(x)):类似于第二个问题,这也是一个扩散-反应方程。

扩散系数 λ = 0.01,f = sin(2πx)。除了用于预测 u 的网络外,还使用了一个单独的神经网络来近似 k。
- 1D Burgers 方程(前向问题,用 gPINN+RAR 解决):这是流体动力学中的一个基本方程,结合了非线性对流和扩散。

初始条件:u(x, 0) = -sin(πx),边界条件:u(-1, t) = u(1, t) = 0,ν=0.01/π
- Allen-Cahn 方程(前向问题,用 gPINN+RAR 解决):该方程模型了相分离过程,这在材料科学中至关重要。

初始条件:u(x, 0) = x² cos(πx),边界条件:u(-1, t) = u(1, t) = -1,D = 0.001
基准研究表明:
-
提出的梯度增强 PINN 学习(gPINN)在残差点更少的情况下实现了更高的准确性;
-
gPINN 结合先进的残差点采样方案(例如,RAR)在挑战性 PDE 问题中表现最佳。
2.5 优点与缺点 ⚡
优点 💪
-
不仅函数预测的准确性提高,函数导数预测的准确性也得到了改善。
-
收敛速度更快。
-
在训练点更少的情况下比传统 PINN 表现更好。
-
适用于前向问题和反向问题。
-
可以轻松与先进的残差点采样方案(参见上一篇博客)结合,以进一步提高性能,特别是在具有陡峭梯度的 PDE 解中。
缺点 📉
-
引入了新的加权参数来平衡整体 PINN 损失函数中的梯度损失项。
-
模型训练的复杂性增加,可能导致计算成本增加。
2.6 替代方案 🔀
由于这是第一篇将梯度增强学习范式引入 PINN 领域的论文,目前还没有其他类似的方法。论文中所有的比较都在普通 PINN、gPINN 及 gPINN+RAR 采样方案之间进行。
3 潜在的未来改进 🌟
还有几种可能性可以进一步改善提出的策略:
-
自动调整梯度损失项的权重。
-
改进残差点的选择以评估额外的梯度损失。当前论文使用相同的残差点来评估 PDE 残差和 PDE 残差的梯度。然而,如果两组残差点不相同,可能会获得更好的性能。
-
需要更高效的自动微分策略来计算高阶导数。
4 个要点 📝
在这篇博客中,我们探讨了通过梯度增强学习来提高 PINN 的准确性和训练效率。以下是论文中提出的设计模式的亮点:
-
[问题]:如何提高 PINNs 的准确性和训练效率?
-
解决方案:梯度增强学习,在 PINN 损失函数中不仅强制 PDE 残差为零,还强制其梯度为零。
-
[潜在好处]:1. 比简单 PINN 表现更佳,残差点更少。2. 不仅函数预测的准确性提高,函数导数预测的准确性也得到了改善。
像往常一样,我准备了一张 PINN 设计卡来总结要点:

论文中提出的 PINN 设计模式。(图像由本博客作者提供)
希望您觉得这篇博客有用!要了解更多关于 PINN 设计模式的内容,请随时查看本系列的其他文章:
-
PINN 设计模式 02:动态解决方案区间扩展
-
PINN 设计模式 03:带有梯度提升的 PINN 训练
-
PINN 设计模式 07:使用 PINN 进行主动学习
期待在接下来的博客中与您分享更多见解!
参考 📑
[1] Yu 等,基于梯度增强的物理信息神经网络用于正向和反向 PDE 问题,arXiv,2021 年。
[2] Laurent 等,梯度增强元模型的概述及应用,Arch Computat Methods Eng,2019 年。
揭示物理信息神经网络的设计模式: 第五部分
利用自动化超参数优化提升 PINN 效果
·发布于 Towards Data Science ·阅读时间 9 分钟·2023 年 6 月 4 日
--

图片来源:Drew Patrick Miller 在 Unsplash
欢迎来到本系列的第 5 篇博客,我们继续探索物理信息神经网络(PINN)的设计模式之旅🙌
你是否曾经想过,物理信息神经网络的最佳架构是否可以自动搜索?事实证明,有一种方法可以做到这一点,如我们今天要讨论的论文所示。
和往常一样,我们将从讨论当前问题开始,然后介绍提出的解决方案、基准测试过程以及所提技术的优缺点。博客最后将探讨一些潜在的未来机会。
如果你对本系列中涵盖的其他 PINN 设计模式感兴趣,可以在这里跟进:
PINN 设计模式 02: 动态解区间扩展
PINN 设计模式 07:使用 PINN 进行主动学习
让我们深入探讨!
1. 论文概览 🔍
-
标题:Auto-PINN:理解和优化物理信息神经网络架构
-
作者:Y. C. Wang, X. T. Han, C. Y. Chang, D. C. Zha, U. Braga-Neto, X. Hu
-
机构:德克萨斯农工大学,莱斯大学
-
链接:arXiv
2. 设计模式 🎨
2.1 问题 🎯
在物理信息神经网络(PINNs)的应用中,神经网络超参数,如网络深度、宽度、激活函数的选择等,都会对 PINN 的效率和准确性产生显著影响,这并不令人意外。
自然,人们会求助于AutoML(更具体地说,神经架构搜索)来自动识别最佳网络超参数。但在此之前,有两个问题需要解决:
-
如何有效地在广阔的搜索空间中导航?
-
如何定义合适的搜索目标?
后者的原因在于 PINN 通常被视为一个“无监督”问题:由于训练是通过最小化 ODE/PDE 残差来指导的,因此不需要标记数据。

PINN 工作流。PINN 的性能对网络结构非常敏感。解决这一问题的一个有前途的方法是利用 AutoML 进行自动超参数调优。(图片由本博客作者提供)
为了更好地理解这两个问题,作者进行了大量实验,以调查 PINN 性能对网络结构的敏感性。现在,让我们看看他们发现了什么。
2.2 解决方案 💡
论文提出的第一个观点是训练损失可以作为搜索目标的替代指标,因为它与 PINN 的最终预测准确性高度相关。这解决了为超参数搜索定义合适优化目标的问题。
第二个观点是无需同时优化所有网络超参数。相反,我们可以采用逐步解耦策略,例如,首先搜索最佳激活函数,然后固定激活函数的选择,寻找最佳网络宽度,再固定之前的决定并优化网络深度,依此类推。在他们的实验中,作者证明了这一策略非常有效。
有了这两个观点,我们来详细看看如何执行搜索。
首先,考虑哪些网络超参数?在论文中,推荐的搜索空间是:
-
宽度:每个隐藏层中的神经元数量。考虑的范围是[8, 512],步长为 4 或 8。
-
深度:隐藏层的数量。考虑的范围是[3, 10],步长为 1。
-
激活函数:Tanh、Sigmoid、ReLU,以及Swish。
-
变化点:使用 Adam 的周期占总训练周期的比例。考虑的值为[0.1, 0.2, 0.3, 0.4, 0.5]。在 PINN 中,通常的做法是首先使用 Adam 训练若干周期,然后切换到 L-BFGS 继续训练若干周期。此变化点超参数决定了切换的时机。
-
学习率:固定值为 1e-5,因为它对最终的架构搜索结果影响较小。
-
训练周期:固定值为 10000,因为它对最终的架构搜索结果影响较小。
其次,让我们详细审查提出的过程:
- 第一个搜索目标是激活函数。为此,我们采样宽度和深度参数空间,并计算不同激活函数下所有宽度-深度样本的损失。这些结果可以为我们提供哪个激活函数是主导的线索。一旦决定,我们将在后续步骤中固定激活函数。

第一步是识别主导的激活函数。(图片由博客作者提供)
- 第二个搜索目标是宽度。更具体地说,我们寻找几个 PINN 表现良好的宽度区间。

第二步是识别网络宽度的有前景区间。(图片由博客作者提供)
- 第三个搜索目标是深度。在这里,我们只考虑在上一步确定的最佳表现区间内变化的宽度,并希望找到 PINN 表现良好的最佳 K 个宽度-深度组合。

第三步是识别表现最好的宽度-深度组合的前 K 名。(图片由博客作者提供)
- 最终搜索目标是变化点。我们仅需为上一步识别的前 K 个配置中的每一个寻找最佳变化点。

最后一步是识别最佳变化点。(图片由博客作者提供)
这一搜索过程的结果是K 种不同的 PINN 结构。我们可以从这些 K 个候选中选择表现最好的一个,或简单地使用所有这些模型形成一个 K-ensemble PINN 模型。
注意,上述过程需要指定若干调优参数(如宽度区间的数量、K 的数量等),这将取决于可用的调优预算。
至于具体优化算法的使用,可以利用现成的 AutoML 库来完成任务。例如,论文中的作者使用了Tune package来执行超参数调优。
2.3 为什么解决方案可能有效 🛠️
通过解耦不同超参数的搜索,搜索空间的规模可以大大缩小。这不仅大大降低了搜索复杂性,还显著提高了为研究中的物理问题找到(近)最优网络架构的可能性。
此外,使用训练损失作为搜索目标既简单易行又令人期望。由于训练损失(主要由 PDE 残差损失构成)与推理过程中的 PINN 准确性高度相关(根据论文中进行的实验),识别出能提供最小训练损失的架构也可能会导致一个高预测准确度的模型。
2.4 基准测试 ⏱️
论文考虑了总共 7 个不同的基准问题。所有问题都是正向问题,PINN 用于求解 PDE。
-
具有 Dirichlet 边界条件的热方程。这类方程描述了给定区域内的热量或温度分布。
时间。

- 具有 Neumann 边界条件的热方程。

- 波动方程,描述了空间中振动的传播,如机械波和电磁波。这里考虑了 Dirichlet 和 Neumann 条件。

- Burgers 方程,已被用来模拟冲击流、燃烧室中的波动传播、交通流动等。

- 对流方程,描述了标量场在已知速度矢量场的作用下的运动。

- 对流方程,具有不同的边界条件。

- 反应方程,描述化学反应。

基准研究结果显示:
-
提出的 Auto-PINN 在各种 PDE 问题中表现稳定。
-
在大多数情况下,Auto-PINN 能够识别出具有最小误差值的神经网络架构。
-
使用 Auto-PINN 方法的搜索尝试次数较少。
2.5 优势和劣势 ⚡
优势 💪
-
显著降低了进行 PINN 应用的神经架构搜索的计算成本。
-
提高了识别适用于不同 PDE 问题的(近)最优神经网络架构的可能性。
劣势 📉
-
使用训练损失值作为搜索目标的有效性可能依赖于特定的 PDE 问题特征,因为基准测试仅针对特定的一组 PDE 进行。
-
数据采样策略影响 Auto-PINN 的性能。尽管论文讨论了不同数据采样策略的影响,但并未提供如何选择适合特定 PDE 问题的最佳策略的明确指南。这可能为使用 Auto-PINN 增添了另一层复杂性。
2.6 替代方案 🔀
传统的现成 AutoML 算法也可以用于解决物理信息神经网络(PINNs)的超参数优化问题。这些算法包括 随机搜索、遗传算法、贝叶斯优化 等。
与那些替代算法相比,新提出的 Auto-PINN 专门为 PINN 设计。这使得它成为优化 PINN 超参数的独特且有效的解决方案。
3 个潜在的未来改进 🌟
还有几个可能的方式来进一步改进所提议的策略:
- 结合更复杂的数据采样策略,例如自适应和基于残差的采样方法,以提高搜索精度和模型性能。
要了解更多关于如何优化残差点分布的信息,请查看 这篇博客 这是 PINN 设计模式系列中的一篇文章。
-
对搜索目标进行更多的基准测试,以评估训练损失值是否确实是各种类型 PDE 的良好代理。
-
纳入其他类型的神经网络。目前版本的 Auto-PINN 仅为多层感知机(MLP)架构设计。未来的工作可以探索卷积神经网络(CNNs)或递归神经网络(RNNs),这些可能增强 PINNs 解决更复杂 PDE 问题的能力。
-
Auto-PINN 中的迁移学习。例如,在某些类型的 PDE 问题上表现良好的架构可以作为类似类型 PDE 问题搜索过程的起点。这可能加快搜索过程并提高模型的性能。
4 个收获 📝
在这篇博客中,我们讨论了如何使用 Auto-PINN 方法有效调优 PINN 模型超参数。以下是论文中提出的设计模式的亮点:
-
[问题]: 如何自动调优 PINNs 的模型超参数?
-
[潜在好处]: 1. 更高效的搜索,显著降低计算成本。2. 提高识别不同类型 PDE 问题的(近似)最优神经网络超参数的可能性。
一如既往,我准备了一张 PINN 设计卡来总结收获:

论文中提出的 PINN 设计模式。(图像由该博客作者提供)
希望您觉得这篇博客对您有用!要了解更多关于 PINN 设计模式的内容,请随时查看本系列的其他文章:
-
PINN 设计模式 02:动态解空间区间扩展
-
PINN 设计模式 03:使用梯度提升的 PINN 训练
-
PINN 设计模式 07:使用 PINN 的主动学习
期待在即将发布的博客中与您分享更多见解!
参考 📑
[1] Wang et al., Auto-PINN: 理解和优化物理信息神经网络架构, arXiv, 2022.
揭开物理信息神经网络设计模式的面纱:第六部分
将因果关系引入 PINN 训练
·发布于Towards Data Science ·阅读时间 9 分钟·2023 年 6 月 13 日
--

照片由Delano Ramdas提供,来源于Unsplash
欢迎来到本系列的第六篇博客,我们将继续探索物理信息神经网络(PINN)的设计模式🙌
在这一集里,我们将讨论将因果关系引入物理信息神经网络训练的内容。正如我们今天将要查看的论文所建议的:尊重因果关系就是你所需的一切!
一如既往,让我们首先讨论当前的问题,然后转到建议的解决方案、评估程序以及所提方法的优缺点。最后,我们将通过探索潜在的机会来结束博客。
随着这一系列的不断扩展,PINN 设计模式的集合变得更加丰富🙌 这里是一些即将到来的内容的预览:
PINN 设计模式 01:优化残差点分布
PINN 设计模式 02:动态解区间扩展
PINN 设计模式 05:自动化超参数调整
PINN 设计模式 07:使用 PINN 的主动学习
让我们深入探讨!
1. 论文概述 🔍
2. 设计模式 🎨
2.1 问题 🎯
物理信息神经网络(PINNs)在结合观察数据和物理法则方面是一个重大进展。然而,在实践中,它们常常无法处理高度非线性、多尺度动态或混沌问题,并趋向于收敛到错误的解。
为什么会这样呢?
好吧,根本问题在于当前论文揭示的 PINN 公式中的因果性违背。
物理意义上的因果性意味着未来时刻的状态依赖于当前或过去时刻的状态。然而,在 PINN 训练中,这一原则可能不成立;这些网络可能隐含偏向于首先在未来状态下近似 PDE 解,然后才解决初始条件,实质上是“跳过”时间,从而违反因果性。
相比之下,传统的数值方法通过时间推进策略固有地保持因果性。例如,在时间上离散化 PDE 时,这些方法确保在近似时间 t + ∆t 时的解之前解决时间 t 时的解。因此,每个未来状态是依赖于已解决的过去状态,从而保持因果性原则。
对这个问题的理解引出了一个有趣的问题:我们如何纠正 PINN 中的因果性违背,使其符合基本物理法则?

PINN 工作流程。简单的 PINN 没有“因果性”嵌入其中。一种简单而有效的策略是动态加权不同时间实例的 PDE 残差损失。(图像由本博客作者提供)
2.2 解决方案 💡
这里的关键思想是重新制定 PINN 损失函数。
具体来说,我们可以引入动态加权方案,以考虑在不同时间位置评估的 PDE 残差损失的不同贡献。让我们通过插图来详细分析。
为了简化起见,我们假设在空间-时间域中的配点是均匀采样的,如下图所示:

总 PDE 残差损失是计算在所有配点上的,其梯度值用于驱动网络参数优化。(图像由本博客作者提供)
为了进行一步梯度下降,我们必须首先计算所有配点的累计 PDE 残差损失。具体做法是首先计算与在各个时间点采样的配点相关的损失,然后进行“简单的求和”以获得总损失。接下来的梯度下降步骤可以基于计算得到的总损失来优化 PINN 权重。
当然,配点的求和顺序不会影响总损失的计算;所有方法都会得到相同的结果。然而,按时间顺序分组损失计算的决定是有目的的,旨在强调‘时间性’的元素。这个概念对于理解提出的因果训练策略至关重要。
在这个过程中,评估在不同时间位置的 PDE 残差损失被视为同等重要。这意味着所有时间残差损失同时被最小化。
然而,这种方法存在风险,可能会导致 PINN 违反时间因果关系,因为它没有对在连续时间间隔最小化时间残差损失进行时间上的规范化。
那么,我们如何引导 PINN 在训练过程中遵循时间优先原则呢?
秘诀在于有选择地加权各个时间残差损失。例如,假设在当前迭代中,我们希望 PINN 专注于在时间点t₁处逼近解。那么,我们可以简单地在 Lᵣ(t₁)上加上更高的权重,这就是在t₁处的时间残差损失。这样,Lᵣ(t₁)将成为最终总损失中的主导成分,结果是优化算法将优先最小化 Lᵣ(t₁),这与我们首先在时间点t₁逼近解的目标一致。

通过在不同时间点分配时间残差损失的权重,我们可以引导优化器专注于在我们期望的时间点最小化损失。(图片由本博客作者提供)
在随后的迭代中,我们将注意力转向时间点t₂处的解。通过增加 Lᵣ(t₂)的权重,它现在成为总损失计算中的主要因素。因此,优化算法被引导去最小化 Lᵣ(t₂),从而提高了在t₂处解的预测准确性。

(图片由本博客作者提供)
从我们之前的演示可以看出,通过改变在不同时间点分配的时间残差损失权重,我们可以引导 PINN 在我们选择的时间点逼近解。
那么,这如何帮助将因果结构纳入 PINN 训练中呢?事实证明,我们可以设计一种因果训练算法(如论文中提出的),使得时间t的时间残差损失的权重 Lᵣ(t),只有在t之前的损失(Lᵣ(t-1), Lᵣ(t-2), 等)足够小时才显著。这有效地意味着神经网络仅在对先前步骤的近似准确度令人满意时才开始最小化 Lᵣ(t)。
为了确定权重,论文提出了一个简单的公式:权重ωᵢ设置为与所有之前时间实例的累计时间残差损失的大小成反向指数比例。这确保了当所有之前时间实例的累计损失较小时,权重ωᵢ才会活跃(即,具有足够大的值),即 PINN 已经能够准确地近似之前时间步的解。这就是时间因果关系在 PINN 训练中体现的方式。

(图片来自博客作者)
解释了所有组件后,我们可以将完整的因果训练算法拼凑如下:

论文中提出的因果训练算法的示意图。(图片来自博客作者)
在结束本节之前,有两个值得提及的备注:
-
论文建议使用ωᵢ的大小作为 PINN 训练的停止标准。具体来说,当所有ωᵢ的值都大于预定义的阈值δ时,训练可以认为完成。推荐的δ值为 0.99。
-
选择合适的ε值很重要。虽然可以通过传统的超参数调整来调整此值,但论文推荐了一种退火策略来调整ε。详细信息请参见原始论文(第三部分)。
2.3 为什么这个解决方案可能有效 🛠️
通过动态加权在不同时间实例评估的时间残差损失,提出的算法能够引导 PINN 训练首先在较早的时间近似 PDE 解,然后再尝试解决较晚时间的解。
这个属性有助于将时间因果关系明确地融入 PINN 训练中,并构成潜在更准确的物理系统模拟的关键因素。
2.4 基准 ⏱️
论文考虑了总共 3 个不同的基准方程。所有问题都是前向问题,其中 PINN 用于求解 PDE。
- 洛伦兹系统:这些方程出现在行星大气对流和不稳定性的研究中。洛伦兹系统对其初始条件具有强烈的敏感性,且对普通 PINN 来说是具有挑战性的。

- Kuramoto–Sivashinsky 方程:该方程描述了各种波动模式的动态,如火焰、化学反应和表面波。它被认为表现出丰富的时空混沌行为。

- Navier-Stokes 方程:这组偏微分方程描述了流体物质的运动,并构成了流体力学的基本方程。当前论文考虑了一个经典的二维衰减湍流示例,位于一个具有周期性边界条件的方形区域内。

基准研究表明:
-
与原始 PINN 训练方案相比,提出的因果训练算法能够实现 10 到 100 倍的准确性改进。
-
演示了配备因果训练算法的 PINNs 能够成功模拟高度非线性、多尺度和混沌系统。
2.5 优势与劣势 ⚡
优势 💪
-
尊重因果性原则,使 PINN 训练更加透明。
-
引入了显著的准确性改进,使其能够解决那些对 PINNs 仍然难以处理的问题。
-
提供了一个实用的定量标准,用于评估 PINNs 的训练收敛性。
-
与原始 PINN 训练策略相比,计算成本几乎可以忽略不计。唯一的额外成本是计算ωᵢ,这与自动微分操作相比几乎可以忽略。
劣势 📉
-
引入了新的超参数ε,该参数控制时间残差损失权重的调度。尽管作者提出了一种退火策略作为替代方案,以避免繁琐的超参数调优。
-
使 PINN 训练工作流程复杂化。应特别关注时间权重ωᵢ,因为它们现在是网络可训练参数(如层权重和偏差)的函数,计算ωᵢ的梯度不应反向传播。
2.6 替代方案 🔀
有几种替代方法试图解决与当前“因果训练算法”相同的问题:
-
自适应时间采样策略 (Wight et al.):该策略不是对不同时间点的配点进行加权,而是修改配点的采样密度。这种方法类似于将优化器的关注点转移到不同时间点的时间损失最小化上。
-
“时间推进”/“课程训练”策略(例如,Krishnapriyan et al.):通过在不同的时间窗口内顺序学习解决方案来尊重时间因果性。
然而,与那些替代方法相比,“因果训练算法”将时间因果性置于核心,更适应各种问题,且计算成本低。
3 潜在的未来改进 🌟
有几种可能性可以进一步改进提出的策略:
- 结合更复杂的数据采样策略,如自适应和基于残差的采样方法,以进一步提高训练效率和准确性。
欲了解如何优化残差点分布,请查看此博客中的 PINN 设计模式系列。
- 扩展到逆问题设置。当信息点源(即观测数据)可用时,如何确保因果关系将需要对目前提出的训练策略进行扩展。
4 个要点📝
在这篇博客中,我们探讨了如何通过重新制定训练目标将因果关系引入 PINN 训练。以下是论文中提出的设计模式的亮点:
-
[问题]:如何让 PINN 遵守支撑物理系统的因果关系原则?
-
解决方案:重新制定 PINN 训练目标,引入动态加权方案,以逐步将训练重点从早期时间步骤转移到后期时间步骤。
-
[潜在收益]:1. 显著提高 PINN 的准确性。2. 扩展 PINN 对复杂问题的适用性。
这是 PINN 设计卡,总结了主要收获:

论文中提出的 PINN 设计模式。(图片由此博客作者提供)
希望你觉得这篇博客有用!要了解更多关于 PINN 设计模式的信息,请随时查看此系列中的其他帖子:
-
PINN 设计模式 02:动态解决方案区间扩展
-
PINN 设计模式 03:带有梯度提升的 PINN 训练
-
PINN 设计模式 07:与 PINN 的主动学习
期待在即将到来的博客中与您分享更多见解!
参考📑
Wang 等人,尊重因果关系是训练物理信息神经网络所需的一切,arXiv,2022 年。
揭示物理信息神经网络的设计模式:第七部分
高效训练参数化 PINN 的主动学习
·发表于 Towards Data Science ·阅读时长 8 分钟·2023 年 7 月 25 日
--

图片来源于 Scott Graham 在 Unsplash
欢迎来到本系列的第 7 篇博客,我们将继续激动人心的探索物理信息神经网络(PINN)设计模式的旅程🙌
在这篇博客中,我们将详细探讨一篇将主动学习引入 PINN 的论文。和以往一样,我们将从设计模式的角度分析这篇论文:首先,我们会介绍目标问题,然后引入提出的方法。接下来,我们将讨论评估过程以及该方法的优缺点。最后,我们将通过探索未来的机会来总结博客内容。
随着系列的不断扩展,PINN 设计模式的集合也变得更加丰富!以下是一些即将呈现的内容预览:
PINN 设计模式 01: 优化残差点分布
PINN 设计模式 02: 动态解决方案区间扩展
PINN 设计模式 05: 自动超参数调优
让我们深入了解吧!
1. 论文概述 🔍
-
标题:主动训练物理信息神经网络以聚合和插值 Navier-Stokes 方程的参数化解
-
作者:C. A., Arthurs, A. P. King
-
机构:伦敦国王学院
-
链接:计算物理学期刊
2. 设计模式 🎨
2.1 问题 🎯
PINN 的主要用途之一是替代高保真度、耗时的数值模拟(例如,结构动力学的 FEM 模拟)。得益于已知控制微分方程强有力的正则化(表现为额外的损失项),PINN 的训练通常只需从少量模拟运行中收集的最少数据。
然而,这种理想的情况仅限于研究的问题不涉及变量参数的情况。在实际应用中,我们经常需要推断不同领域几何形状、模型参数(例如,材料属性)以及初始和/或边界条件的解。我们肯定不希望为每种不同的情况重新训练一个新的 PINN,因为这会非常低效且计算成本高。
解决这个挑战的一种策略是将普通 PINN 升级为参数化 PINN:本质上,变量参数被视为 PINN 的额外输入,如下图所示。一旦训练完成,我们可以使用参数化 PINN 在任何给定条件下进行即时预测。

图 1. 与仅接受空间/时间坐标的普通 PINN 相比,参数化 PINN 还将变量参数作为输入。(图片由本博客作者提供)
如预期的那样,更多的输入要求更多的训练数据。由于生成新训练数据意味着运行耗时的模拟,因此一个关键问题出现了:如何在达到期望的预测准确度的同时,最小化模拟运行次数(从而提高数据效率)以进行 PINN 训练?
2.2 解决方案 💡
回答这个问题的关键在于战略性地选择参数空间(即θ空间)中的点来运行模拟,而论文提出的解决方案是主动学习。

图 2. 主动学习工作流程。对于 PINN 训练,“标签”步骤对应于在参数空间中选择的样本位置运行数值模拟。(图片由本博客作者提供)
针对解决参数化 PDE 的应用,所提出的工作流程可以在下图中说明:

图 3. 论文中提出的主动学习流程示意图。在这里,θ₁和θ₂表示变量参数(在实际应用中,可能有更多这样的变量参数),而 u 表示我们尝试模拟的物理量(例如,在流体仿真中,u 可以是速度、压力等)(图片由本博客作者提供)
提出的主动学习流程从随机采样参数空间开始(在图示中,θ₁和θ₂是变量参数),并进行数值仿真以收集训练数据(即在不同空间/时间位置的u,即所建模的物理量)。
然后,我们继续进行常规的 PINN 训练,其中神经网络的预测不仅需要与收集的模拟结果u匹配,还需要满足主导的微分方程。
一旦训练收敛,我们将获得一个能够在仿真空间/时间域内准确预测u的 PINN 模型,前提是θ₁-θ₂值包含在训练数据集中。然而,到目前为止,对于未见过的θ₁-θ₂值组合,没有保证 PINN 也能提供准确的预测。
这就是为什么我们启动主动学习过程的原因。我们的目标是选择下一个θ₁-θ₂样本来进行仿真。理想情况下,我们希望这个新的θ₁-θ₂样本能够提供最多的信息,一旦将其相关的模拟结果附加到训练数据中,它可以带来 PINN 准确性的最大提升。
那么我们应该如何选择新的θ₁-θ₂样本呢?论文提出了一个简单的标准:首先,使用当前训练好的 PINN 预测参数空间中所有候选θ₁-θ₂样本的u(这些候选样本可以预先大量生成以均匀填充空间),然后计算不同候选θ₁-θ₂样本的相应 PDE 残差。最后,选择具有最高 PDE 残差的下一个θ₁-θ₂样本。
换句话说,论文建议使用PDE 残差作为 PINN 预测准确性的指标。由于当前训练的 PINN 在选定的θ₁-θ₂位置最难准确预测满足已知微分方程的u,因此在选定的θ₁-θ₂位置运行耗时的仿真,并将模拟结果附加到原始训练数据中是有意义的。一旦训练数据得到丰富,我们就开始另一轮 PINN 训练,从而完成一次主动学习的迭代。整个过程在计算的最大 PDE 残差降到预定义的阈值以下时终止。
2.3 为什么解决方案可能有效 🛠️
主动学习是一种成熟的技术,用于减少模型训练成本。它之所以有效,是因为它能够识别当前 PINN 预测不够满意的参数空间区域,然后相应地选择下一步要模拟的点,以最大化模型的改进。通过这种方式,主动学习可以显著提高数据效率,同时确保达到所需的预测准确性。
2.4 基准测试 ⏱️
与其他 PINN 论文不同,当前论文专注于解决连续 2D 管道域中的参数稳态不可压 Navier-Stokes 方程:

考虑了两个变量参数,包括流动边界条件,即流入速率,以及域形状参数。
论文展示了,通过提出的主动学习策略训练的 PINN 可以准确高效地预测给定流入速率和域形状值的流场,且这些值都在考虑的变动范围内。
该论文还展示了,使用提出的主动学习策略相比于在所考虑的参数空间内进行随机或均匀数据选择,需要的模拟次数要少得多,同时在流场预测中的误差也要小得多。
最后,论文展示了训练后的参数化 PINN 的一个实际应用:参数扫描。更具体地说,训练后的 PINN 可以用于搜索参数空间,并识别当流场满足给定条件时两个参数的值。
2.5 优势与劣势 ⚡
优点 💪
-
大幅减少训练具有所需预测准确性的参数PINN 模型所需的模拟数据。
-
训练后的 PINN 模型能够在参数空间的区域内提供极快的 PDE 解决方案近似值。
-
训练后的 PINN 具有高度灵活性,因为它可以在不需要全面重新训练的情况下适应不同的参数空间。这是可能的,因为主动学习允许模型随着新的模拟回合在扩展参数空间中被轻松更新。
-
不需要存储大量模拟结果,因为训练后的参数化 PINN 可以按需生成流场结果。由于典型的神经网络只消耗少量存储空间,因此潜在的数据压缩是显著的。
劣势 📉
-
提出的主动训练流程的有效性可能取决于初始数据。如果初始数据集不能充分代表系统的基本行为,模型可能难以识别潜在的改进区域,从而导致后续模拟的选择不理想。
-
此外,所提的方法使用 PDE 残差作为模型预测精度的指示。然而,对于模型预测精度与 PDE 残差不相关的情况,所提方法的有效性可能会被打折扣。
-
目前,本文仅考虑了两个变量参数。然而,随着问题维度的增加,主动学习可能变得计算昂贵。因此,所提方法的可扩展性仍需进一步研究。
2.6 替代方案 🔀
替代使用主动学习方案,样本(用于运行模拟以生成训练数据)也可以通过采用准随机采样方法生成。流行的准随机采样方法包括拉丁超立方体采样(LHS)、Sobol 序列和 Halton 序列。与简单随机采样方案相比,准随机采样方法具有更好的空间填充特性,这意味着需要更少的样本来均匀覆盖所研究的参数空间。
然而,与论文中提出的主动学习方法相比,准随机采样方案需要用户预先指定样本数量,因此在新模拟数据可用时,在丰富训练数据集方面的灵活性较差。然而,主动学习会引入额外的计算成本,因为需要估计模型的性能并确定最具信息量的点以进行下一次模拟运行。
3 个潜在的未来改进 🌟
有几个可能的方向可以进一步改进所提的策略:
-
利用更复杂的主动学习算法来自动选择训练数据点。这可能进一步提高预测的准确性,并减少训练所需的时间。
-
开发策略以允许在每次主动学习迭代中采样多个数据点。由于相应的模拟可以并行运行,因此整体训练效率可以大大提高。
-
将其他经过验证的最佳实践(如协同点采样、因果训练、集成学习等)与所提的主动学习方案整合,以进一步提升模型性能。
4 个要点 📝
在这篇博客中,我们探讨了如何使用主动学习进行数据高效的参数化 PINN 训练。以下是论文中提出的设计模式的亮点:
-
[问题]:如何用尽可能少的数据样本训练参数化 PINN。
-
解决方案:使用主动学习训练 PINN,在这种方法中,数据会被自适应地收集,以最大化 PINN 预测精度的提升。
-
[潜在好处]:1. 显著降低了参数化 PINN 的训练成本。2. 训练后的参数化 PINN 在参数空间的区域内提供了对 PDE 解的快速近似。
以下是总结要点的 PINN 设计卡:

论文中提出的 PINN 设计模式。(图片由本博客作者提供)
参考文献 📑
[1] Arthurs 等人,《物理信息神经网络的主动训练以聚合和插值 Navier-Stokes 方程的参数解》,计算物理学期刊,2021 年。
解密物理信息神经网络的设计模式:第 01 期
优化残差点分布以提升 PINN 训练效率和准确性
·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 5 月 15 日
--

近年来,物理信息神经网络(PINNs)作为一种卓越的方法出现,将神经网络的强大功能与基本物理定律的洞察力结合在一起。当我深入这个领域时,我经常感到被大量的研究论文和它们提出的各种技术所压倒。在这片信息的海洋中导航成为了一项挑战,特别是当我想找到最有效的解决方案来应对特定问题时。
我的个人经历激发了我开始这个博客系列的想法:我的想法是,在每篇博客文章中,我将集中于一篇或几篇研究论文,并将其贡献提炼成易于理解的见解。我希望这个博客系列能够作为一个结构化的地图,供 PINN 从业者依赖,以识别最适合当前挑战的技术,保持对最新进展的了解,并更自信地探索 PINNs 的世界。
那么,这个提炼过程应该是什么样的呢?就个人而言,我发现设计模式的概念是一个非常好的框架:
设计模式指的是经过测试并证明有效的可重用解决方案,用于常见问题。设计模式提供了解决这些问题的模板,可以根据需要适应不同的情况。它们作为最佳实践,捕捉了该领域专家的集体知识和经验。
因此,本博客系列将超越传统的论文评论。它将作为一个有组织的目录,涵盖:
-
问题,提出的策略试图解决的具体问题;
-
解决方案,提出策略的关键组成部分,如何实施以及为什么可能有效;
-
基准,评估了哪些物理问题,以及相关的性能;
-
优缺点,在何种条件下提出的策略可以有效,同时也突出其潜在的局限性;
-
替代方案,为解决类似问题而提出的其他方法,从而提供对潜在解决方案的更广泛视角。
我希望这种方法能引起你的共鸣,因为它真正反映了我对组织知识和使其易于获取的热情。事不宜迟,让我们通过探索第一篇 PINN 论文一起踏上这段激动人心的旅程,专注于为 PINN 训练创建更好的残差点。
随着这一系列的不断扩展,PINN 设计模式的集合变得更加丰富🙌 这里是对未来内容的简要预览:
PINN 设计模式 02:动态解空间扩展
PINN 设计模式 05:自动化超参数调优
PINN 设计模式 07:与 PINN 的主动学习
1. 论文概览:
-
标题:关于物理信息神经网络的非自适应和基于残差的自适应采样的综合研究
-
作者:C. Wu, M. Zhu, Q. Tan, Y. Kartha, L. Lu
-
机构:宾夕法尼亚大学、南加州大学、乔治亚理工学院
-
链接:arXiv
2. 设计模式
2.1 问题
物理信息神经网络(PINNs)通过显式地集成已知的物理过程的常微分方程或偏微分方程(ODEs/PDEs)提供了相对于传统神经网络的独特优势。PINNs 中这些控制方程的强制执行依赖于称为残差点的一组点。这些点在仿真领域内被战略性地选择,并将相应的网络输出代入控制方程以评估残差。残差指示了网络输出与基础物理过程的一致程度,因此作为一个关键的物理损失项来指导神经网络的训练过程。
显然,这些残差点的分布在影响 PINNs 训练过程的准确性和效率方面发挥着关键作用。然而,当前的方法通常涉及简单的均匀采样,这留有相当大的改进空间。

PINN 的示意图。虚线圈起来的部分,即残差点的分布,是本文主要解决的问题。(图像由本博客作者提供)
因此,一个紧迫的问题是:我们如何优化残差点的分布,以提高 PINNs 的准确性和训练效率?
2.2 解决方案
分配残差点的有前途的方法是采用自适应策略和精细化策略:
-
自适应策略意味着在每经过一定数量的训练迭代后,可以生成一批新的残差点来替换先前的残差点;
-
精细化策略意味着可以向现有残差点中添加额外的残差点,从而“精细化”残差点。
基于这两种基础策略,本文提出了两种新颖的采样方法:基于残差的自适应分布(RAD)和基于残差的自适应分布精细化(RAR-D):
- RAD: Residual-based Adaptive Distribution
关键思想是基于自定义的概率密度函数在空间域x上绘制新的残差样本。概率密度函数 P(x) 的设计使其与x处的 PDE 残差 ε(x) 成正比:

用于生成残差点的自定义概率密度函数。(来源于原始论文)
在这里,k 和 c 是两个超参数,分母中的期望项可以通过例如蒙特卡罗积分来近似。
RAD 方法总共有三个超参数:k、c 和重新采样的周期 N。尽管最佳的超参数值依赖于问题,但建议的默认值为 1、1 和 2000。
- RAR-D: Residual-based Adaptive Refinement with Distribution
从本质上讲,RAR-D 在提出的 RAD 方法基础上增加了精炼元素:经过一定的训练迭代后,RAR-D 不会完全用新的残差点替换旧的残差点,而是保留旧的残差点,并根据上述自定义概率密度函数绘制新的残差点。
对于 RAR-D,建议的默认值为 k 和 c 分别为 2 和 0。
2.3 为什么解决方案可能有效
关键在于设计的采样概率密度函数:该密度函数倾向于在 PDE 残差较大的区域放置更多的点,而在残差较小的区域放置较少的点。这种点的战略分布使得在残差较高的区域能够进行更详细的 PDE 分析,可能导致 PINN 预测精度的提高。此外,优化的分布还允许更高效地利用计算资源,从而减少了准确解决控制 PDE 所需的总点数。
2.4 基准测试
论文对两种提出的方法与其他八种采样策略进行了基准测试。他们在处理各种物理方程中的前向和逆向问题时对这些方法进行了评估:
- 一维扩散方程:这种方程描述了热传导、分子扩散以及其他类似的物理过程。该方程描述了物质密度如何随着时间的推移而扩散或扩展。

- Burgers’ 方程:该方程将非线性对流与扩散/粘度相结合。它被应用于包括流体力学、交通流、气体动力学等各种应用数学领域,有时还用于模拟冲击波。

- Allen-Cahn 方程:该方程广泛用于描述相分离过程,这是材料科学中的一个关键现象。它模拟了一个二元(两相)系统向最小能量配置的演变。

- 波动方程:该方程是描述波动的二阶线性偏微分方程——如声波、光波或水波——在介质中传播的方程。它在声学和电磁学领域中具有基础性作用。

- 扩散-反应方程(逆问题,标定反应速率 k(x)):该方程是扩散方程和反应系统的结合。

- Korteweg-de Vries 方程(逆问题,标定 λ₁ 和 λ₂):该方程描述了某些类型的波动传播,如浅水波。

比较研究表明:
-
RAD 策略始终表现最佳,使其成为一个强有力的默认策略。
-
如果计算成本是一个问题,RAR-D 可以作为一种强有力的替代方案,因为它提供了足够的准确性,并且计算需求低于 RAD。
-
RAD 和 RAR-D 在处理复杂 PDE 时表现特别有效。
-
当模拟的 PDE 具有光滑解时,RAD 和 RAR-D 的优势会减少。
2.5 优势与劣势
👍优点
-
动态改善基于 PDE 残差的残差点分布。
-
提高了 PINN 的准确性。
-
在较少的残差点下实现了与现有方法相当的准确性。
👎缺点
-
可能比其他非自适应均匀采样方法计算成本更高。然而,这是为了更高的准确性所付出的代价。
-
对于具有光滑解的 PDE,例如扩散方程、扩散反应方程等,一些简单的均匀采样方法可能会产生足够低的误差,使得提出的解决方案在这些情况下可能不太适用。
-
引入了两个新的超参数 k 和 c,需要调整,因为它们的最佳值依赖于问题。
2.6 替代方案
当前论文之前已经提出了其他方法:

论文中研究了总共 10 种采样方法。两种新提出的方法用红色突出显示。(图片由这篇博客作者提供)
在这些方法中,有两种方法对当前论文提出的方法影响深远:
-
基于残差的自适应细化 (Lu et al.),这是提出的 RAR-D 的一个特殊情况,其中 k 的值很大。
-
重要性采样 (Nabian et al.),这是通过设置 k=1 和 c=0 得到的 RAD 特殊情况。
3 潜在的未来改进
未来的改进可以在以下几个方向进行:
-
进一步优化残差点生成的概率密度函数,可能使用 元学习 技术。
-
利用更复杂的 主动学习 或 强化学习 技术来优化采样策略。
4 收获
在这篇博客中,我们探讨了优化 PINNs 中残差点分布的有前景的解决方案。这个主题高度相关,因为它直接影响 PINNs 的准确性和效率。论文中提出的设计模式的要点总结如下:
-
[问题]:如何分配残差点以评估物理损失?
-
解决方案:1. 自适应采样,即在高 PDE 残差区域采样更多。2. 细化采样,即在现有点上添加新的残差点。
-
[潜在好处]:1. 提高准确性。2. 需要较少的残差点。
这是我准备的 PINN 设计模式卡片:

本文提出的 PINN 设计模式。(图片来自本博客作者)
希望你觉得这个博客有用😃 如果你想了解更多关于 PINN 设计模式的内容,请随时查看系列中的其他文章。
-
PINN 设计模式 03:带有梯度提升的 PINN 训练
-
PINN 设计模式 07:与 PINN 的主动学习
参考文献
[1] Wu 等人,关于物理信息神经网络的非自适应和基于残差的自适应采样的综合研究,arXiv,2022 年。
揭开大数法则的面纱
原文:
towardsdatascience.com/unraveling-the-law-of-large-numbers-e36a3219acb2

大数法则的有趣之处在于它不仅仅是它所说明的内容,还有它所未说明的内容。
·发表于Towards Data Science ·阅读时间 15 分钟·2023 年 7 月 12 日
--
1966 年 8 月 24 日,一位才华横溢的剧作家汤姆·斯托帕德在苏格兰爱丁堡上演了一部戏剧。这部戏剧有一个奇特的标题,“罗生门与吉尔登斯特恩死了”。其主要角色,罗生斯坦和吉尔登斯特恩,是哈姆雷特(莎士比亚名著中的角色)的童年朋友。戏剧开场时,吉尔登斯特恩反复掷硬币,每次都是正面。每次结果使吉尔登斯特恩的钱包变轻,而罗生斯坦的钱包变重。随着正面的鼓点不断重复,吉尔登斯特恩感到担忧。他担心自己是否在暗中希望每次硬币都是正面,以此作为对某个早已遗忘的罪行的自我惩罚。或者时间是否在第一次掷硬币后停滞不前,他和罗生斯坦在经历着相同的结果。
斯托帕德出色地展示了概率法则如何融入我们的世界观、期望感和人类思维的基本结构中。当第 92 次掷硬币仍然是正面时,吉尔登斯特恩问道,他和罗生斯坦是否处于一个不自然的现实中,在那里概率法则不再适用。
吉尔登斯特恩的担忧当然是没有根据的。确实,连续 92 次都是正面的可能性小得不可思议。实际上,这个概率是一个小数点后跟 28 个零再跟 2。吉尔登斯特恩更有可能被一颗陨石砸中。
Guildenstern 只需在第二天再抛一次 92 次硬币,这个结果几乎肯定会与之前大相径庭。如果他每天都进行这个例程,他会发现大多数天里正面数量会与反面数量大致相等。
在连续出现 92 个正面的情况下,Guildenstern 得到了一个极度偏倚的样本。而他所目睹的是我们宇宙中的两种迷人行为——概率收敛 和 大数法则。
用简单的英语解释大数法则
大数法则,简称 LLN,有两个版本:弱版本和强版本。弱大数法则更直观、更易于理解,但也容易被误解。本文将讨论弱版本的内容,而强版本将在之后的文章中讨论。
弱大数法则关注的是样本均值与总体均值之间的关系。我将用简单的文字解释它的含义:
假设你从总体中随机抽取一个特定大小的样本,比如 100 个样本。记住样本大小这个术语。样本的大小是这个法则中的指挥者,是大拿。现在计算这个样本的均值并将其放置一旁。接下来,多次重复这一过程。你会得到一组不完美的样本均值。样本均值之所以不完美,是因为样本均值和真实总体均值之间总会存在误差。假设你能容忍一定的误差。如果你从这些均值中随机选择一个样本均值,有可能样本均值与总体均值之间的绝对差异会超过你的误差容忍度。
弱大数法则指出,样本均值与总体均值之间的误差落在你选择的容忍水平内的概率,随着样本量的增长,会趋近于完美的 1.0,也就是确定性,这种趋势在样本量无限增长或等于总体量时尤为明显。
无论你如何缩小容忍度水平,当你抽取越来越大规模的样本时,随机选择的样本均值超出这个容忍度的可能性将越来越小。
一个弱大数法则如何运作的现实世界示例
要了解弱大数法则的工作原理,我们将通过一个例子来演示。为此,请允许我带你到寒冷而阴郁的东北大西洋。
爱尔兰政府每天都会发布一个水温数据集,该数据集记录了从东北大西洋表面测得的水温。这份数据集包含了按纬度和经度索引的数十万条表面水温测量数据。例如,2023 年 6 月 21 日的数据如下:

很难想象八十万温度值的样子。所以让我们创建一个散点图来可视化这些数据。我在下面展示了这个图。图中空白的区域代表爱尔兰和英国。

东北大西洋海面温度的彩色散点图(图片来源:作者)(数据来源:数据集)
作为实践者,你永远无法接触到‘总体’。所以如果我将这组 80 万温度测量值称为‘总体’,你完全可以严厉批评我。但请稍等片刻。你很快会明白为什么将这些数据视为‘总体’对我们有帮助。
假设这些数据是……呃……总体。这个总体中 810219 个位置的平均海面温度是 17.25840 摄氏度。我们将这个值标记为总体均值μ。记住这个值,你将需要经常参考它。
现在假设你无法接触到这个 810219 个值的总体。相反,你只能接触到一个从该总体中抽取的 20 个随机位置的小样本。这是一个这样的随机样本:

大小为 20 的随机样本(图片来源:作者)
样本的平均温度是 16.9452414 摄氏度。这是我们的样本均值X_bar,其计算方式如下:
X_bar = (X1 + X2 + X3 + … + X20) / 20
你可以同样轻松地从同一总体中绘制第二个、第三个,甚至任意多个这样大小为 20 的随机样本。这里有一些随机样本供参考:

从总体中抽取的每个大小为 20 的随机样本(图片来源:作者)
关于随机样本究竟是什么的简要说明
在继续之前,我们先稍作绕道,了解一下随机样本究竟是什么。为了获得这个视角,我们将看一下赌场老丨虎丨机:

上面展示的老丨虎丨机包含三个槽位。每次你拉下机器的把手时,机器会从内部维护的图片总体中随机选择一张图片填充每个槽位,例如一系列水果图片。现在想象一个有 20 个槽位的老丨虎丨机,命名为X1 到X20。假设该机器被设计为从 810219 个温度测量值的总体中选择值。当你拉下把手时,20 个槽位中的每一个——X1 到X20——都会填充一个从 810219 个值的总体中随机选择的值。因此,X1 到 X20 是可以容纳总体中任何值的随机变量。它们一起形成了一个随机样本。换句话说,随机样本的每一个元素本身都是一个随机变量。
在老丨虎丨机的例子中,X1到X20具有一些有趣的性质:
-
X1 获得的值与X2 到X20 获得的值是独立的。同样适用于X2、X3,……,X20。因此,X1 到 X20是独立的随机变量。
-
由于X1、X2,……,X20可以容纳总体中的任何值,因此它们每一个的均值都是总体均值μ。使用期望值的符号 E(),我们可以将这个结果写为:
E(X1) = E(X2) = … = E(X20) = μ。
-
X1到X20具有相同的概率分布。
因此,X1、X2,……,X20是独立同分布(i.i.d.)随机变量。这些变量的均值是总体均值μ。对于任何特定的观测样本,X1、X2,……,X20的观测值均值即为样本均值X_bar。
…然后我们回到展示弱大数法则如何工作的部分
让我们计算这个 20 个元素样本的均值(记作X_bar),然后将其放置一旁。如果你再次拉下机器的把手,又会弹出另一个 20 元素的随机样本。继续计算它的均值并将其放置一旁。如果你重复这一过程一千次,你将计算出一千个样本均值。
这里是以这种方式计算出的 1000 个样本均值的表格。我们将其标记为X_bar_1 到X_bar_1000:

一个包含 1000 个样本均值的表格。每个均值是从一个 20 个样本的随机样本中计算得出的
现在仔细考虑以下陈述:
由于样本均值是从随机样本中计算得出的,样本均值本身也是一个随机变量。
此时,如果你智慧地点头并抚摸下巴,那确实是非常正确的。认识到样本均值是一个随机变量是统计学中最深刻的领悟之一。
请注意,上表中的每个样本均值与总体均值μ之间都有一定的距离。让我们绘制这些样本均值的直方图,以查看它们如何围绕μ分布:

样本均值的直方图(图片由作者提供)
大多数样本均值似乎接近于 17.25840 摄氏度的总体均值。而且有些均值与μ有相当大的距离。假设你对这个距离的容忍度是 0.25 摄氏度。如果你将手伸进这 1000 个样本均值的桶里,抓取任何一个在你手掌范围内的均值,并将其取出,那么你取出的均值超出这个容忍阈值的概率是多少?要估计这个概率,你必须计算那些在两个方向上与μ相距至少 0.25 度的样本均值的数量,并将这个数量除以 1000。在上表中,有 422 个这样的样本均值。因此,随机抽取一个这样的均值的概率是:
P(|X_bar — μ | ≥ 0.25) = 422/1000 = 0.422
让我们暂时搁置这个概率。
现在重复上述所有步骤,但这次使用样本大小为 100 而不是 20。所以你将做的是:从中抽取 1000 个大小为 100 的随机样本,计算每个样本的均值,存储所有这些均值,计算那些与μ相距至少 0.25 摄氏度的均值,并将此计数除以 1000。如果这听起来像是赫拉克勒斯的劳作,你没有听错。所以花点时间喘口气。当你都赶上来后,看看你为自己的劳作所获得的奖励。
下表包含了 1000 个随机样本的均值,每个样本的大小为 100:

1000 个样本均值的表格。每个均值是从一个大小为 100 的随机样本中计算得出的
在这 1000 个均值中,有 56 个均值与μ的偏差至少为 0.25 摄氏度。因此,你遇到这样的均值的概率仅为 56/1000 = 0.056。这个概率明显小于我们之前计算的当样本大小为 20 时的 0.422。
如果你用不同的样本大小逐步增加来重复这个实验,你将得到一个满是概率的表格。我已经为你做了这个练习,通过将样本大小从 10 逐步增加到 490。结果如下:

概率表。展示了当样本大小从 10 增加到 490 时的 P(|X_bar — μ | ≥ 0.25)(图片由作者提供)
此表中的每一行对应我从 810219 个温度测量值的总体中随机抽取的 1000 个不同样本。sample_size列提到每个这 1000 个样本的大小。一旦抽取,我计算了每个样本的均值,并统计了那些在两个方向上与μ相距至少 0.25 摄氏度的均值。num_exceeds_tolerance列提到这个计数。probability列是num_exceeds_tolerance / sample_size的比率。
注意到样本量增加时,这个计数迅速减小。相应的概率 P(|X_bar — μ | ≥ 0.25) 也是如此。样本量达到 320 时,概率已降至零。偶尔会小幅上升至 0.001,但那是因为我绘制了有限数量的样本。如果每次我绘制 10000 个样本而不是 1000 个,不仅偶尔的小幅上升会平滑掉,概率的衰减也会变得更平滑。
下图展示了 P(|X_bar — μ | ≥ 0.25) 随样本量的变化。它清晰地反映了随着样本量的增加,概率急剧下降至零。

P(|X_bar — μ | ≥ 0.25) 随样本量的变化(图像由作者提供)
如果将 0.25 摄氏度的容差换成不同的容差值——无论是更低还是更高的值——结果会如何?概率是否会无论你选择的容差水平如何而衰减?以下一系列图示说明了这个问题的答案。

概率 P(|X_bar — μ | ≥ ε) 会随着样本量的增加而衰减(至零)。这适用于所有 ε 的值(图像由作者提供)
无论你选择多么小的容差(ε),概率 P(|X_bar — μ | ≥ ε) 总是会随着样本量的增加而收敛至零。这就是弱大数法则的实际应用。
弱大数法则,正式陈述
弱大数法则的行为可以正式陈述如下:
假设 X1, X2, …, Xn 是 i.i.d. 随机变量,组成了一个大小为 n 的随机样本。假设 X_bar_n 是该样本的均值。还假设 E(X1) = E(X2) = … = E(Xn) = μ。则对于任何非负实数 ε,X_bar_n 距离 μ 至少 ε 的概率随着样本量的增加趋向于零。以下精美方程捕捉了这种行为:

弱大数法则(图像由作者提供)
在这一法则的三百年历史中,数学家们逐渐放宽了 X1 到 Xn 必须是独立同分布的要求,同时仍保留了该法则的精神。
“概率收敛”原则、“plim”符号以及用极少的话说出重要事情的艺术
使用概率作为运输手段逐渐收敛到某个值的特定方式称为 概率收敛。一般来说,概率收敛 原则可以陈述如下:

概率收敛(图像由作者提供)
在上述方程中,X_n 和 X 是随机变量。ε 是一个非负实数。上述方程表示,当 n 趋向于无穷大时,X_n 在概率上收敛于 X。注意,如果你将 X 替换为 μ,你会得到 WLLN 的方程。
在广袤的统计领域,你会不断遇到一个默默无闻的符号,叫做plim。它的发音是‘p lim’,或‘plim’(像‘plum’这个词,但‘i’发音),或概率极限。plim 是指一个度量如均值以概率收敛到一个特定值的简短说法。使用 plim,弱大数法则可以简洁地表述如下:

使用极少的墨水表达的弱大数法则(图片作者提供)
或者简单地表示为:

(图片作者提供)
符号的简洁性一点也不令人惊讶。数学家对简洁如蜜蜂对花蜜一样热衷。当涉及到传达深刻真理时,数学可能是最省墨水的领域。在这个追求效率的领域中,plim 占据了重要位置。你将很难找到像 plim 这样用更少的墨水或电子表达出的深刻概念。
但不要再挣扎了。如果 plim 的简洁美让你渴望更多,这里还有另一种,可能更高效的符号,它传达与 plim 相同的含义:

使用更少的墨水表达的弱自然数法则(图片作者提供)
揭穿一些关于弱大数法则的误解
在这篇文章的开头,我提到弱大数法则的显著性在于它所没有说的东西,甚至胜过它所说的。让我解释一下我的意思。弱大数法则常常被误解为样本量增加时,样本均值接近于总体均值或该思想的各种泛化。如我们所见,这种关于弱大数法则的观点与现实毫无关联。
实际上,让我们揭穿几个关于弱大数法则的误解。
神话 #1: 随着样本量的增加,样本均值趋向于总体均值。
这很可能是对弱大数法则的最常见误解。弱大数法则确实没有做出这样的断言。为了理解原因,请考虑以下情况:你成功地获得了一个非常大的样本。虽然你欣喜地欣赏着自己的成就,但你也应该问自己以下问题:仅仅因为你的样本很大,它就必须也平衡吗?是什么阻止自然用一个含有大量偏差的巨型样本来给你带来打击?答案是绝对没有。实际上,Guildenstern 的 92 个正面序列不就是这样吗?他的样本毕竟是完全随机的。如果大样本恰好有很大的偏差——虽然不太可能,但也不是不可能——那么尽管样本量很大,大偏差会把样本均值推到远离真实总体值的地方。相反,小样本可能会被证明是非常平衡的——再次强调,这虽然不太可能,但也不是不可能。关键是,随着样本量的增加,样本均值并不保证会忠实地向总体均值靠拢。自然并没有提供这样的绝对保证。自然提供的保证是,随着样本量的增加,样本均值落在总体均值的任何选定误差阈值内的概率会逐渐增加,换句话说,即 WLLN。
神话 #2:随着样本量的增加,样本的几乎所有特征——中位数、方差、标准差——都会收敛到相同的总体值。
这句话将两个神话打包成一个易于携带的包裹。首先,弱大数法则(LLN)假设的是概率上的收敛,而不是值上的收敛。其次,弱大数法则适用于样本均值的概率收敛,而不是任何其他统计量。弱大数法则不涉及中位数、方差或标准差等其他测量的收敛。
如何知道弱大数法则是否实际有效?
陈述弱大数法则(WLLN)以及用实际数据演示它的工作原理是一回事,但如何确保它始终有效呢?是否存在某些情况,会让它成为破坏者——样本均值无法以概率收敛到总体值的情况?要了解这些,我们必须证明 WLLN,并在此过程中精确定义它适用的条件。
恰巧的是,弱大数法则(WLLN)有一个美味的证明,其中一个关键要素是令人垂涎的切比雪夫不等式。如果这激起了你的兴趣,请关注我下一篇关于弱大数法则证明的文章。
再谈 Guildenstern
如果不安抚我们的朋友 Guildenstern 的担忧就离开是无礼的。让我们深入理解他所经历的结果是多么不可能。我们将使用伪随机生成器模拟 92 次不偏不倚的抛硬币。正面将被编码为 1,反面编码为 0。我们将记录 92 次结果的均值。均值是硬币正面朝上的次数的分数。例如,如果硬币正面朝上 40 次,反面朝上 52 次,均值为 40/92=0.43478。我们将重复进行 92 次抛硬币的动作一万次,得到一万次均值,并绘制其频率分布图。此分布的 X 轴代表均值,Y 轴表示观察到该值的次数。完成此练习后,我们得到以下类型的直方图:

10000 个样本的样本均值直方图(作者提供的图像)
我们看到大多数样本均值集中在 0.5 的总体均值附近。这是你最有可能观察到的结果。Guildenstern 的结果——连续 92 次正面——对应的均值是 92/92=1.0。这是一个极不可能的结果。在图中,你可以看到它在 10000 次实验中的出现频率基本为零。但与 Guildenstern 的担忧相反,这个结果并没有什么不自然的,概率法则依然以其惯有的果断性运作。Guildenstern 得到的完美 1.0 结果仅仅是在图的右尾的遥远区域潜伏,耐心等待一些不幸的抛硬币者,其唯一的错误就是不幸到令人难以想象。
参考文献和版权
数据集
从 DATA.GOV.IE 下载的 北东大西洋实时海面温度数据集,版权归 CC BY 4.0 所有。
图像
本文中的所有图像版权归 Sachin Date 所有,使用 CC-BY-NC-SA,除非图像下方提到不同的来源和版权。
感谢阅读!如果你喜欢这篇文章,请 关注我 以获取回归和时间序列分析的技巧、教程和编程建议。
揭示复杂性:一种使用噪声注入的流形学习的新方法
](https://medium.com/@jceweaver?source=post_page-----41251565fded--------------------------------)
](https://towardsdatascience.com/?source=post_page-----41251565fded--------------------------------) Jimmy Weaver
·
关注 发表在 Towards Data Science ·14 分钟阅读·Nov 17, 2023
--
在数据科学领域,高维数据既是挑战也是机遇。虽然它提供了大量的关系和模式,可以被塑造和转换,但如果不经过谨慎的清理和选择,它可能会变得令人不知所措,难以从中分析和得出结论:“维度的诅咒”。虽然直觉上你可能倾向于使用主成分分析将数据嵌入到一个较小的子空间,但这可能使你的数据问题变得更加复杂,非线性嵌入技术可能是更合适的选择。然而,在选择正确的非线性技术时需要注意,因为一次错误的转向可能导致过拟合或简单地不适合使用的嵌入。在本文中,我将借此机会讨论一种新颖的方法,来理解高维数据中的流形,使我们作为数据科学家能够基于我们复杂数据的潜在结构做出知情的定量决策。
我将首先介绍流形学习是什么,并概述四种流行的线性和非线性嵌入技术的高层但信息丰富的摘要。通过这些,我们将更深入地了解每种情况下所做的假设以及这些假设对有效嵌入的影响。我还将介绍一些 Python 示例,演示如何应用我的噪声注入分析方法来评估流形及其可能推断的类型。在本文结尾,您将彻底理解不同的流形学习技术,以及您可以采取的步骤来真正理解数据内部结构。

流形学习
在深入研究这些流形学习技术之前,重要的是准确理解什么是流形?在我们的背景下,流形是对我们高维空间结构的近似表示,该空间可能与附近数据点具有局部和/或全局关系。然而,有一个重要的警告,即我们事先并不知道我们 N 维空间内的真实结构,通常在嵌入数据时被迫对数据点之间的关系进行隐式假设。不同于数学中的流形学习(黎曼几何),在那里可以找到从一个空间到另一个空间的显式映射。
机器学习模型的成功,无论是在性能还是数据驱动洞察方面,都基本上取决于我们传递给它的数据。虽然传递更多信息可以使这些算法找到更复杂的关系和模式,但也会导致一系列问题,这些问题通常在“维度诅咒”一词下被泛化。
-
过拟合模型:随着数据维度的增加,后续的机器学习模型可能无法将数据中的真实关系泛化,结果是对噪声和异常值过拟合。
-
数据点之间的关系扩展:在大型复杂特征空间中,某些区域变得非常稀疏,很难建模,或者变得非常集中,关键信息被掩盖。
-
增加计算复杂性:大多数机器学习算法在特征数量增加时无法良好扩展,导致训练模型时增加计算时间或内存需求。
要克服这一问题,我们必须要么减少我们考虑的特征数量,要么将数据映射到一个低维空间,同时尽可能地保留关键信息。在下一节中,我们总结并探讨不同的技术(线性和非线性)。
主成分分析
主成分分析(PCA)可以说是将数据集嵌入或降低维度的最臭名昭著的方法,其解释性来源于其统计方法。在线上可以找到很多更深入探讨该算法的文章,但为了本文的目的,我以下列出了主要步骤。
PCA 的关键点在于,它通过假设一个线性流形并将数据映射到 N 个正交主成分上(这些主成分是原始特征的线性组合),试图保留所有数据点之间的关系。为此,首先对数据进行标准化,围绕均值中心化并相应缩放,以使所有变量的方差保持一致:

其中Xⱼ是所有特征j的原始特征空间X,μ和σ分别是Xⱼ的均值和标准差。算法然后计算标准化数据的协方差矩阵S。

表达每个变量与其他每个变量的相关性。PCA 然后执行协方差矩阵的特征值分解来确定特征值λᵢ和特征向量vᵢ。

由这些特征向量定义了一个矩阵W,按照特征值递减的顺序排列。转换数据的最终投影Y,就是Z和W的乘积。
总之,PCA 提供了一种发现数据内部结构的方法,以最大程度地保留和解释方差(即在最低维度上最大化信息)。每个特征值与方差的部分成比例,因此我们的矩阵W确保第一个投影的主成分包含最大的方差,每个后续的正交成分则包含稍少的一部分。
本地线性嵌入
在深入研究更高级的非线性方法之前,让我们从可能最简单易懂的方法开始,即局部线性嵌入(LLE)。本质上,LLE 假设给定的数据点及其邻居可以近似地表示并映射到流形上的线性切平面,使它们彼此之间成为线性组合。用于映射邻域集群到平面的权重被调整以最小化转换误差(见下文),并且此过程对每个数据点重复进行。因此,尽管数据点邻域内存在局部线性关系,但全局上会捕捉到非线性。
作为一名数据科学家,您需要定义最近邻数k,这需要仔细调整。有了这个值,第一个要解决的优化问题是将权重数组Wᵢⱼ定义为将每个数据点Xᵢ映射到切平面上的线性组合其邻居:

对于每个i,受以下约束:

确保保留局部几何,权重总和为 1。从而我们的嵌入Yᵢ就简单地是这些权重与原始空间Xᵢ的乘积,同时确保最小化以下嵌入成本函数

以上确保了较低维度表示在原始空间中最好地保留了局部权重。虽然这是捕捉非线性关系的一种优雅解决方案,但如果我们的k值未经适当调整或者我们的 N 维空间中存在稀疏部分,则可能会导致嵌入损坏。接下来,我们将探索其他使用局部和全局关系结合形成最终嵌入的非线性嵌入技术,在大多数情况下使它们更加稳健。
谱嵌入
谱嵌入(SE),也称为拉普拉斯特征映射嵌入,形成一个相似性图,将所有数据点连接在一起,然后根据点之间的谱相似性进行加权。通过这样做,SE 不仅保留了局部关系(如 LLE 所做的那样),而且连接的图确保全局关系也被考虑进去。依赖于图拉普拉斯中观察到的谱特性,使得这种算法能够发现其他技术可能无法识别的复杂非线性结构。
此算法的第一步是构建图:

其中Wᵢⱼ是节点i和j之间的边的宽度,σ是一个可定制的参数,用来控制邻域的宽度。从此可以得到图拉普拉斯矩阵L的定义为L=D-W,其中W是图的邻接矩阵,D是带以下条目的对角度矩阵:

通过对最终嵌入施加正交性和中心约束,该算法在拉普拉斯矩阵上执行特征值分解,以识别特征值和特征向量,从而相应地嵌入数据。
等距特征映射
最后一个将被介绍的非线性流形技术是等距特征映射(ISOMAP),这是一种强大的非线性嵌入方法,比前面提到的方法稍微具有更多定制性。算法假设可以通过连接的邻域图表示高维空间,其中节点之间的距离是测地距离。然后,该方法应用多维缩放(MDS)以找到数据的低维表示,使得节点之间的成对距离尽可能地保持不变。
在构建图形时,您可以选择对要考虑的最近邻数量和数据点到其邻居的相对欧几里得距离之一或两者施加限制。这些约束需要适当调整,例如,不能太大以至于形成捷径边(缺少关键结构信息),但也不能太小以至于无法创建连接的图。如果满足这些邻域条件,则在两个节点之间建立一条边。通过图形G,算法然后使用最短路径算法f(如 Dijkstra 或 Floyd)计算所有点对之间的测地距离Dᵢⱼ:

最后一步是将数据映射到子空间,这涉及将 MDS 应用于Dᵢⱼ。如果我们回顾 PCA 的概述,我们会在进行特征值分解之前评估协方差矩阵。MDS 稍有不同,通过计算与中心矩阵H相关的 Gram 矩阵B:


其中e是一个全为 1 的向量,n是数据点的数量。最终的嵌入来自于对应于最大特征值的d个特征向量:

噪声注入
到目前为止,我们已经讨论了将数据嵌入低维空间的线性和非线性方法,通过对潜在流形的某些假设。但是,我们如何知道哪种方法捕捉到了有用的信息,特别是当我们处理高维数据时无法可视化时。一种从定量角度评估任何嵌入技术性能的方法是使用我称之为噪声注入的方法。通过这种方法,我们将不同量的噪声应用于原始空间,并监控它对嵌入的影响。基本原理是,当原始空间中的噪声(失真)量增加时,会有一个点,任何流形学习算法都无法捕捉到真实的潜在结构。从观察嵌入如何响应不同量的噪声,可以很容易识别每种技术在建模数据集中的潜在结构时表现如何。下面是一个逐步总结,介绍如何进行这种分析,并提供两个 Python 示例来实现这个想法:
-
从原始数据集中生成带有高斯噪声的替代数据集,我们将为每个后续的替代数据集缩放噪声的方差。
-
使用各种流形学习技术嵌入这些数据集。
-
对于每种技术,使用 Procrustes 分析将注入噪声的嵌入与原始空间的嵌入(无合成附加噪声)进行比较,Procrustes 分析是一种流行的比较两种形状的统计方法。该评估方法将一个空间旋转、缩放和转换到另一个空间,以最小化每个数据点之间的平方差之和。这个差异是相似性的度量,将被分析以观察每种嵌入方法在噪声影响下的表现。
-
最终步骤是绘制 Procrustes 距离相对于合成附加噪声规模的变化,这使我们能够得出关于每种技术性能的结论。
让我们将上述步骤应用于经典的 S 曲线数据集:

import matplotlib.pyplot as plt
from sklearn import manifold, datasets
import numpy as np
from scipy.spatial import procrustes
from sklearn.decomposition import PCA
def compute_procrustes_distances(data, embedding_technique, max_noise, noise_step=0.05):
"""
Compute Procrustes distances for a range of Gaussian noise levels.
Parameters:
data (np.array): The original dataset.
embedding_technique (object): An instance of a manifold learning technique.
max_noise (float): Maximum level of noise to be added.
noise_step (float): Incremental step for noise addition.
Returns:
list: A list of Procrustes distances for each noise level.
"""
base_embedding = embedding_technique.fit_transform(data)
noise_levels = np.arange(0, max_noise, noise_step)
distances = []
for noise in noise_levels:
noisy_data = data + np.random.normal(-noise, noise, data.shape)
noisy_embedding = embedding_technique.fit_transform(noisy_data)
_, _, disparity = procrustes(base_embedding, noisy_embedding)
distances.append(disparity)
return distances
def plot_data(X, colour):
"""
Plot the dataset in 3D.
Parameters:
X (np.array): Data points.
colour (np.array): Colour mapping for the data points.
"""
fig = plt.figure(figsize=(30, 10))
ax = fig.add_subplot(111, projection='3d')
ax.scatter(X[:, 0], X[:, 1], X[:, 2], c=colour, cmap=plt.cm.Spectral)
ax.view_init(4, -72)
plt.show()
def plot_procrustes_distances(noise_range, distances, labels):
"""
Plot Procrustes distances for different embedding techniques.
Parameters:
noise_range (np.array): Range of noise levels.
distances (dict): Dictionary of distances for each embedding technique.
labels (list): List of labels for each embedding technique.
"""
plt.figure(figsize=(10, 6))
for label in labels:
plt.plot(noise_range, distances[label], label=label)
plt.xlabel('Range of noise')
plt.ylabel('Procrustes distance')
plt.title('Comparison of Embedding Techniques')
plt.legend()
plt.show()
# Generate and plot the S-curve dataset
X, colour = datasets.make_s_curve(1000, random_state=0)
plot_data(X, colour)
# Compute Procrustes distances for different embedding techniques
max_noise = 2
noise_range = np.arange(0, max_noise, 0.05)
embedding_techniques = {
"PCA": PCA(2),
"ISOMAP": manifold.Isomap(n_components=2),
"LLE": manifold.LocallyLinearEmbedding(n_neighbors=10, n_components=2),
"SE": manifold.SpectralEmbedding(n_components=2, n_neighbors=10)
}
distances = {label: compute_procrustes_distances(X, technique, max_noise) for label, technique in embedding_techniques.items()}
# Plot the computed Procrustes distances
plot_procrustes_distances(noise_range, distances, embedding_techniques.keys())

当然,在现实世界应用中,我们不会像这个虚拟示例那样知道潜在的真实结构,但现在我们假设我们不知道真实结构。从上面的图表中我们可以得出什么结论?从根本上说,一个好的趋势应该类似于 S 型曲线,其中最初我们会看到对少量噪声的抵抗,嵌入与原始空间非常相似,但会有一个临界点,当噪声破坏潜在结构时情况不再如此。此时,我们会预期 Procrustes 距离急剧增加,随后的嵌入将捕捉到噪声而几乎没有有意义的信息。考虑到这一点,以及上面的图表,我们可以总结如下:
PCA: 尽管 Procrustes 距离随着噪声增加而增加,但趋势相当线性,因此可能无法捕捉真实结构的充分信息。在不考虑其他趋势的情况下,这一点本身就强烈表明需要非线性嵌入。
LLE: 我们几乎看不到对即使是微小噪声的恢复能力,这可能是由于违反了局部线性假设。增加最近邻的数量(k)可能会减少这种嵌入的脆弱性,但这可能会以损失结果子空间中的细节(信息)为代价。
ISOMAP: 最初这个嵌入技术的表现看起来还不错,但随着噪声的增加,显而易见地缺乏信息捕捉(噪声水平后的线性趋势:0.25)。
SE: 在所有探索的方法中,SE 的表现最好,尽管它需要调整以获得最佳拟合。
总体而言,你可能会得出结论,显然我们的数据结构位于一个非线性流形上,并且 SE 和 ISOMAP 对这种非线性捕捉得相当好。考虑到这一点,以及 LLE 的糟糕表现,我们可以推测原始空间中流形的显著曲率。让我们通过一个不同的示例来探讨这一点:

# Generate and plot the S-curve dataset
X, color = datasets.make_swiss_roll(1000, random_state=0)
plot_data(X, color)
# Compute Procrustes distances for different embedding techniques
max_noise = 4
noise_range = np.arange(0, max_noise, 0.05)
embedding_techniques = {
"PCA": PCA(2),
"ISOMAP": manifold.Isomap(n_components=2),
"LLE": manifold.LocallyLinearEmbedding(n_neighbors=10, n_components=2),
"SE": manifold.SpectralEmbedding(n_components=2, n_neighbors=10)
}
distances = {label: compute_procrustes_distances(X, technique, max_noise) for label, technique in embedding_techniques.items()}
# Plot the computed Procrustes distances
plot_procrustes_distances(noise_range, distances, embedding_techniques.keys())

使用之前概述的相同函数,我们得到上述每种嵌入技术的 Procrustes 距离趋势。显然,这些数据中的结构是显著非线性的,因为我们观察到 PCA 很难捕捉底层结构。原始空间中持续的非线性也通过 LLE 的糟糕表现进一步强调,可能是由于邻域到切平面的映射不一致。同样,SE 和 ISOMAP 的表现良好,后者在结构被噪声打破后,趋于 1 的 Procrustes 距离的速度略快于前者。推测 SE 在所有嵌入中都捕捉到了一些噪声,这可能通过一些参数调整来纠正。调整这些算法可以改善嵌入数据的泛化和拟合,这里是一个对上述 ISOMAP 技术进行调整的示例:
import numpy as np
from scipy.spatial import procrustes
import matplotlib.pyplot as plt
from sklearn import manifold, datasets
def return_procrustes_distance(data, embedding_technique, max_noise, noise_step=0.05):
"""
Compute the Procrustes distance for different levels of noise.
Parameters:
data (array_like): The original data to be embedded.
embedding_technique (object): The embedding technique (e.g., PCA, SpectralEmbedding).
max_noise (float): The maximum level of noise to be added.
noise_step (float): The increment step for the noise level.
Returns:
list: A list of Procrustes distances for each noise level.
"""
embeddings = []
distances = []
noise_range = np.arange(0, max_noise, noise_step)
for noise_level in noise_range:
noisy_data = data + np.random.normal(0, noise_level, data.shape)
embedded_data = embedding_technique.fit_transform(noisy_data)
if not embeddings: # if embeddings list is empty
embeddings.append(embedded_data)
_, _, disparity = procrustes(embeddings[0], embedded_data)
distances.append(disparity)
return distances
# Generating the S-curve dataset
X, _ = datasets.make_swiss_roll(1000, random_state=0)
# Parameters
max_noise = 2
k_values = [5, 7, 9] # Different values of k for ISOMAP
# Computing and plotting Procrustes distances for each k value
noise_levels = np.arange(0, max_noise, 0.05)
plt.figure(figsize=(10, 6))
for k in k_values:
embedding = manifold.Isomap(n_components=2, n_neighbors=k)
procrustes_distances = return_procrustes_distance(X, embedding, max_noise)
plt.plot(noise_levels, procrustes_distances, label=f'ISOMAP (k={k})')
plt.xlabel('Noise Level')
plt.ylabel('Procrustes Distance')
plt.title('Procrustes Distance by Noise Level for Various k in ISOMAP')
plt.legend()
plt.show()

上述是一个非常通用的调整示例,你肯定会想要探索其他参数,但原则是通过简单地调整 k 并观察噪声的影响,我们可以开始看到算法的泛化效果更好。
结论
在本文中,我们探讨了一系列流形学习技术,并展示了如何通过噪声注入来更好地理解高维数据中的潜在结构。我们通过利用对每种算法如何工作的理解、每种算法所依据的假设,以及分析噪声对嵌入的影响来实现这一目标。有了这些见解,我们可以在将数据传递到后续的机器学习管道之前,更加明智地决定如何预处理或处理数据。这种方法也丰富了我们对结果嵌入的泛化理解,以及它可能如何响应噪声或潜在的未来数据漂移。
无论你计划将嵌入技术作为机器学习解决方案的一部分,还是希望扩展你的探索性数据分析过程,上述方法都将帮助你更深入地理解任何高维数据集中的隐藏结构。
除非另有说明,所有图像均由作者提供。
解决压缩轴的终极修复,逐步指导
使用 matplotlib 制作美丽的多尺度图表,只需三步即可完成。
·
阅读更多 发表在 Towards Data Science ·8 分钟阅读·2023 年 12 月 29 日
--
大幅度的异常值、细微特征和尖锐的尖峰常常让数据可视化变得令人沮丧。这三者都可能通过将图表组件压缩到过小的区域,使视觉细节变得难以辨识。
有时,通过简单地排除不受控的数据可以解决问题。当包含这些数据对于当前问题至关重要时,应用对数刻度可以重新调整轴的间距,从而更好地分隔低幅度数据。然而,这种方法也只能起到一定的作用。
在本文中,我们将看看另一种选择:缩放图,它通过面板提供感兴趣区域的放大视图来增强可视化。

我们将在本教程中构建的可视化。
缩放图通常作为主图的插图来排列,但也可以与原始图组合成网格。我们将深入探讨这两种方法。
本文提供了一个代码导向的教程,讲解如何使用matplotlib和outset库中的专业工具来构建缩放图。我们将构建一个关于德克萨斯州降雨数据的可视化,该数据由Evett 等人通过 USDA 提供。该数据集包括来自两个相邻地点的一整年雨量读数,间隔为 15 分钟。
短时间的降雨事件和最强降雨的极端强度使问题复杂化。将一个月的 Evett 等人的降雨数据放入一个简单的折线图中,揭示了我们面临的可视化问题。

我们还有不少工作要做来美化这个图表!在我们的可视化中,我们将重点恢复数据的三个特定组件。
-
第 72 天附近的小雨,
-
第 82 天附近的大暴雨,以及
-
整个月的轻度降水事件。
为了更好地展示这些细节,我们将为每个细节创建一个缩放面板。
计划已经制定好,接下来开始编写代码 👍
第零步:获取数据
通过开放科学框架获取雨量记录。
# ----- see appendix for package imports
df = pd.read_csv("https://osf.io/6mx3e/download") # download data
这里是数据的预览。
+------+-------------+--------------+--------------+------------+-----------+
| Year | Decimal DOY | NW dew/frost | SW dew/frost | NW precip | SW precip |
+------+-------------+--------------+--------------+------------+-----------+
| 2019 | 59.73958 | 0 | 0 | 0 | 0 |
| 2019 | 59.74999 | 0 | 0 | 0.06159032 | 0 |
| 2019 | 59.76041 | 0 | 0 | 0 | 0 |
| 2019 | 59.77083 | 0 | 0 | 0.05895544 | 0.0813772 |
| 2019 | 59.78124 | 0 | 0 | 0.05236824 | 0.0757349 |
+ ... + ... + ... + ... + ... + ... +
在继续之前,先做一些小的准备工作。
nwls = "NW Lysimeter\n(35.18817624°N, -102.09791°W)"
swls = "SW Lysimeter\n(35.18613985°N, -102.0979187°W)"
df[nwls], df[swls] = df["NW precip in mm"], df["SW precip in mm"]
# filter down to just data from March 2019
march_df = df[np.clip(df["Decimal DOY"], 59, 90) == df["Decimal DOY"]]
在上面的代码中,我们创建了更详细的列名,并将数据子集化为一个月份的数据
第一步:坐标轴网格
我们的第一个绘图步骤是初始化一个outset.OutsetGrid实例来管理我们放大图的网格。这个类类似于seaborn 的[FacetGrid](https://seaborn.pydata.org/generated/seaborn.FacetGrid.html),它通过基于分类变量将数据分布到坐标轴上,从而方便地构建标准的网格图。
OutsetGrid与FacetGrid不同,它不仅包括具有分面数据的坐标轴,还准备了一个包含所有数据的初始“源”坐标轴。此外,OutsetGrid包括工具来自动生成“标记”注释,显示放大图与原始图的对应关系。下面的示意图概述了OutsetGrid的绘图模型。

回到我们的示例,我们将通过提供我们希望放大的主要绘图区域的列表来构建一个OutsetGrid,该列表通过data参数传递。随后的参数提供样式和布局信息。
grid = otst.OutsetGrid( # initialize axes grid manager
data=[
# (x0, y0, x1, y1) regions to outset
(71.6, 0, 72.2, 2), # little shower around day 72
(59, 0, 90, 0.2), # all light precipitation events
(81.3, 0, 82.2, 16), # big rainstorm around day 82
],
x="Time", # axes label
y="Precipitation (mm)", # axes label
aspect=2, # make subplots wide
col_wrap=2, # wrap subplots into a 2x2 grid
# styling for zoom indicator annotations, discussed later
marqueeplot_kws={"frame_outer_pad": 0, "mark_glyph_kws": {"zorder": 11}},
marqueeplot_source_kws={"zorder": 10, "frame_face_kws": {"zorder": 10}},
)
这里我们为子图指定了宽于高的纵横比,并决定了我们想要的列数。

我们的轴网已经设置好,我们准备好进入下一步。
第 2 步:绘图内容
是时候在我们的轴上添加一些内容了。
我们可以使用区域图来共同可视化我们的雨量计读数。(对那些不熟悉的人,区域图就是填充到 x 轴的折线图。)应用透明效果将优雅地显示出测量仪一致的地方——以及不一致的地方。
我们可以利用 matplotlib 的 stackplot 来绘制重叠的区域图。虽然 stackplot 设计用于创建“堆叠”在彼此之上的区域图,但我们可以通过对绘图工具进行两次调用来获得重叠区域——每次调用一个测量仪。
为了在网格的所有四个轴上绘制相同的内容,我们将使用 OutsetGrid 的 broadcast 方法。该方法将绘图函数作为第一个参数,然后在每个轴上调用它,使用任何后续参数。
# draw semi-transparent filled lineplot on all axes for each lysimeter
for y, color in zip([nwls, swls], ["fuchsia", "aquamarine"]):
grid.broadcast(
plt.stackplot, # plotter
march_df["Decimal DOY"], # all kwargs below forwarded to plotter...
march_df[y],
colors=[color],
labels=[y],
lw=2,
edgecolor=color,
alpha=0.4, # set to 60% transparent (alpha 1.0 is non-transparent)
zorder=10,
)
为了与背景填充获得更好的对比,我们还将使用 broadcast 在堆叠图周围添加白色底层。
grid.broadcast(
plt.stackplot, # plotter
march_df["Decimal DOY"], # all kwargs below forwarded to plotter...
np.maximum(march_df["SW precip in mm"], march_df["NW precip in mm"]),
colors=["white"],
lw=20, # thick line width causes protrusion of white border
edgecolor="white",
zorder=9, # note lower zorder positions underlay below stackplots
)
以下是我们在进入下一阶段之前的图形样貌。

已经看起来不错——在此阶段我们可以看到放大显示在正确的轴上。
第 3 步:缩放指示器
现在是时候添加缩放指示框,也就是 outset 的“标牌”,以显示我们的辅助图的尺度与主图的尺度之间的关系。
# draw "marquee' zoom indicators showing correspondences between main plot
# and outset plots
grid.marqueeplot(equalize_aspect=False) # allow axes aspect ratios to vary
注意传递给 outset 绘图的 kwarg,以允许插图图形采用与主图不同的纵横比。这样,插图数据可以完全扩展,以利用所有可用的轴空间。

我们已经完成了大部分工作——此时只剩下几个最后的修整。
就这样!
我们最后要做的就是添加一个图例,并将数字 x 刻度更换为适当的时间戳。
grid.source_axes.legend( # add legend to primary axes
loc="upper left",
bbox_to_anchor=(0.02, 1.0), # legend positioning
frameon=True, # styling: turn on legend frame
)
# ----- see appendix for code to relabel axes ticks with timestamps
这样,图形就完成了。

就是这么简单,通过 3 个简单步骤完成了缩放图。
额外:想要插图而不是缩放?
我们可以通过重新排列放大网格轴的位置来创建插图。以下是使用 outset 库的 inset_outsets 工具的方法。
otst.inset_outsets(
grid,
insets=otst_util.layout_corner_insets(
3, # three insets
"NW", # arrange in upper-left corner
inset_margin_size=(0.02, 0), # allow closer to main axes bounds
inset_grid_size=(0.67, 0.9), # grow to take up available space
),
equalize_aspect=False,
)
sns.move_legend( # move legend centered above figure
grid.source_axes, "lower center", bbox_to_anchor=(0.5, 1.1), ncol=2
)
在这种情况下,我们还使用了 outset.util.layout_inset_axes 进行精细调整,以控制插图的大小和位置。

就这样,我们在左上角排列了三个缩放插图。
进一步的信息
你可以用 outset 做很多其他事情。

除了明确的缩放区域指定,outset 库还提供了类似于 seaborn 的数据导向 API,以推断包含数据框架分类子集的缩放插图。还提供了广泛的样式和布局自定义选项。
这是库画廊的一些亮点预览…

你可以在mmore500.com/outset的库文档中了解有关outset的更多信息。特别是,一定要查看快速入门指南。该项目在 GitHub 上的开源地址是mmore500/outset,— 不妨留下一个⭐️!
outset可以通过 pip 安装,命令为python3 -m pip install outset。
作者
本教程由我贡献,Matthew Andres Moreno。

我目前在密歇根大学担任博士后学者,我的工作得到了 Eric 和 Wendy Schmidt AI in Science 博士后奖学金的支持,这是 Schmidt Futures 项目的一部分。
我的职位在大学的生态与进化生物学系、复杂性研究中心和密歇根数据科学研究所之间分配。
在 Twitter 上可以找到我@MorenoMatthewA,在 GitHub 上可以找到我@mmore500。
声明: 我是outset库的作者。
引用
Evett, Steven R.; Marek, Gary W.; Copeland, Karen S.; Howell, Terry A. Sr.; Colaizzi, Paul D.; Brauer, David K.; Ruthardt, Brice B. (2023). 蒸发蒸腾、灌溉、露水/霜 — The Bushland, Texas 大豆数据集的水分平衡数据。Ag Data Commons. https://doi.org/10.15482/USDA.ADC/1528713. 访问时间 2023–12–26。
J. D. Hunter,“Matplotlib: A 2D Graphics Environment”,《计算科学与工程》,第 9 卷,第 3 期,90–95 页,2007 年 https://doi.org/10.1109/MCSE.2007.55
Marek, G. W., Evett, S. R., Colaizzi, P. D., & Brauer, D. K. (2021). 迟播短季大豆的初步作物系数:德克萨斯州高平原。《农业系统、地球科学与环境》,4(2)。 doi.org/10.1002/agg2.20177
用于统计计算的 Python 数据结构,McKinney,《第 9 届 Python 科学会议论文集》,第 445 卷,2010 年 https://doi.org/ 10.25080/Majora-92bf1922–00a
Matthew Andres Moreno. (2023). mmore500/outset. Zenodo. https://doi.org/10.5281/zenodo.10426106
Waskom, M. L.(2021)。seaborn: 统计数据可视化。《开源软件期刊》,6(60),3021, https://doi.org/10.21105/joss.03021.
附录
要安装此练习的依赖项,
python3 -m pip install \
matplotlib `# ==3.8.2`\
numpy `# ==1.26.2` \
outset `# ==0.1.6` \
opytional `# ==0.1.0` \
pandas `# ==2.1.3` \
seaborn `# ==0.13.0`
所有图像均为作者作品。
无监督数据剪枝:更少的数据学习更好
原文:
towardsdatascience.com/unsupervised-data-pruning-less-data-to-learn-better-30cd2bfbd855
基础模型 | 扩展法则 | 大型模型 | 数据剪枝
数据量的增加并不总是意味着模型更准确,那么如何选择数据呢?
·发表于Towards Data Science ·11 分钟阅读·2023 年 2 月 27 日
--

图片由作者使用DALL-E生成
扩展法则在不同的背景下(图像、文本、语言、语音等)都有观察到。增加参数数量是否真的是提高模型性能的唯一秘诀?如果不是,你实际可以做些什么?
什么是扩展法则,它为何成为问题?
近年来,我们看到模型中的参数数量飞跃式增长。所有大型公司都在不断推进创建更强大的模型。这导致了基准数据集误差的减少和意外行为的出现。但什么是扩展法则?
简而言之,扩展法则指出“测试误差通常会随着训练数据量、模型规模或计算量的增加而呈幂律下降。” 换句话说,要提高模型的性能,必须增加这三者中的任意一个:训练时的示例数量、参数数量或训练时长。
之前的技术研究表明,测试损失可能会随着训练数据集的增大而呈幂律下降。2017 年,Hestness 探讨了这一观点在不同的机器学习领域(机器翻译、语言建模、图像处理和语音识别)。
扩展法则随后在 OpenAI 的一篇文章中定义,他们展示了通过增加模型大小、数据集规模或训练计算量,性能得到了提升。

扩展法则由 OpenAI 展示(来源:这里)
他们正式写道:
模型性能最强烈地依赖于规模,这包括三个因素:模型参数数量 N(不包括嵌入)、数据集大小 D 和用于训练的计算量 C。在合理的范围内,性能对其他架构超参数(如深度与宽度)的依赖非常弱。— source
总结而言,他们断言性能与这三种因素中的每一种都有幂律关系。此外,如果增加参数数量 N,你也必须相应增加示例数量 D,否则会出现惩罚(过拟合)。N 和 D 之间存在一种关系,即如果将参数数量增加 8 倍,你必须将数据量增加 5 倍。
这一概念在 GPT-3 和后续模型(Google 的 LaMDA 达到了 5000 亿参数以上)中被推向极端。这些模型展示了惊人的能力,有些人猜测大幅增加参数数量将带来通用智能。那么会吗?
不。简而言之,神经网络 是模式匹配机器(或通用逼近器)。神经网络的作用是找到它们在训练集中见过的模式。一个更大并且经过更多数据训练的神经网络可能包含更多的模式并且可能识别更多的模式。那么无限数据?实际上,数据并不是无限的。
这种幂律扩展促使了对数据收集、计算和相关能源消耗的大量社会投资。然而,幂律扩展是极其微弱且不可持续的。— source
从 OpenAI 的文章中可以看出,需要一个数量级的数据、参数或计算才能将误差降低仅仅 3–2%。此外,为了扩展视觉变换器,需用到二十亿数据点才能在 ImageNet 上获得更多的准确性。

source (here)
总结一下,到目前为止我们见证了一种认为更多即更好的范式。但这真的是唯一的策略吗?
“我们能否通过一种好的选择训练样本的策略实现指数扩展?”
同时,可以说许多数据是冗余的。模型通常看到许多相似的示例。数据集通常通过随机从互联网下载成千上万的示例来获取。以往的研究表明,例如,可以按难度递增的顺序对训练集中的示例进行排序(从简单和冗余的示例到最困难的示例)。可以在保持性能的同时减少数据集中的示例数量(换句话说,简单和冗余的示例只消耗训练周期,而没有任何学习增益)。
关于这些概念的更多信息,如何高效地缩放视觉变换器,以及以模型为中心和以数据为中心的人工智能之间的区别:
为什么我们有巨大语言模型和小型视觉变换器?
Google ViT-22 为新的大型变换器铺平了道路,并将彻底改变计算机视觉
对你的数据集的关键分析
停止微调你的模型:你的模型已经很好,但数据不够好
towardsdatascience.com
无监督数据剪枝:你能在不知道标签的情况下去除无用的训练数据点吗?
之前的研究留下了不同的问题悬而未决。同时,是否也可以定义一个关于数据的误差幂律(在不牺牲性能的情况下指数减少示例数量)。其次,上述策略要求所收集的示例必须被标记(费时且昂贵)。因此,最终的策略应该是无监督的。
最近发表了一篇论文试图回答这些问题。该研究由 META、斯坦福大学和图宾根大学的合作进行:
广泛观察到的神经网络规模定律,其中误差作为训练集大小、模型大小或两者的幂次递减…
作者从扩展法则低效这一事实出发:幂律的指数接近零(这表明资源使用不充分)。此外,参数或数据量的增加对错误的减少影响很小。我们希望能够在不改变模型性能的情况下修剪数据集,即使数据集是未标注的(毕竟,标注数据集是最昂贵和耗时的操作之一)。怎么做?
作者在教师-学生设置的情况下探索了这种可能性。这是一种训练方法,其中你有一个已经用大量示例训练过的模型(尽管它是最广泛使用的情况之一,但不限于 CNN)。你向预训练的大模型(教师)提供一些数据,并使用教师的输出(教师提供类别概率作为输出,也称为软标签)训练较小的模型。
简而言之,作者使用了CIFAR-10作为数据集,从教师模型中获得概率,并使用教师的输出概率作为标签训练了学生模型几个时期。然后,他们计算了学生和教师输出之间的边际(这提供了模型学习示例难度的度量)。

图片来源:原始文章
第一个有趣的结果是,当训练集中的示例数量较多时,保留困难的示例而修剪(从而减少容易的示例)更好。当你在训练集中有少量示例时,修剪困难的示例更好。这可能看起来有悖直觉。作者指出,容易的示例提供了关于目标函数的粗略信息(基本上是数据集的一般模式)。相反,更困难的示例提供了可能在大数据集中被遗漏的细化信息。
直观地说,在有限的数据情况下,由于基础未得到充分捕捉,建模异常值是具有挑战性的;因此,保留容易的示例更为重要,以便模型能够达到适中的错误。然而,随着数据集的增大,容易的示例可以轻松学习,使得建模异常值成为根本挑战。来源:这里
换句话说,当数据量较少时,模型更适合学习关于数据模式的一般信息,而当数据量较大时,困难的示例有助于模型更好地理解类别之间的决策边界。
从信息论的角度来看,作者建议数据修剪可以增加从每个单独示例中获得的信息,因为无信息的示例会被过滤掉。
数据修剪提高了迁移学习

图片来自 Pixabay(来源:这里)
我们对扩展法则感兴趣的原因之一是我们对拥有基础模型感兴趣。一个 基础模型 是一个宽模型(例如变换器、视觉变换器等),它已使用大量未标记数据进行训练,然后可以用于不同的下游任务。
简而言之,我们在大量数据上训练一个非常宽的模型,然后通过迁移学习,将其应用于另一任务。例子有 BERT、GPT3 用于文本任务,以及 ResNet 用于计算机视觉任务(实际上,DALL-E 和 稳定扩散 也将预训练语言模型作为一个组件)。
训练基础模型极其昂贵,到目前为止,我们尝试增加其参数和用于训练的数据量。 然而,DeepMind 的 Chinchilla 及其后续研究表明,改善用于训练的数据可能会更有用。因此,这项研究的作者们想知道:数据修剪是否可以帮助迁移学习?
作者使用了一个预训练的 视觉变换器 (ViT),然后在 10%的 CIFAR-10 的修剪子集上进行了微调。这种方法比在整个 CIFAR-10 上微调 ViT 效果更好。此外,作者在不同的修剪子集(一个缩减版本)上预训练了 ResNet50,然后在 CIFAR-10 上进行了微调。结果显示,在修剪数据集上训练模型比使用整个 ImageNet 的效果更好。

图片来源:原始文章
因此,令人感兴趣的是,在上游任务中修剪预训练数据仍然可以在不同的下游任务中保持高性能。总体而言,这些结果展示了数据修剪在迁移学习中,无论是在预训练还是微调阶段的潜力。
[## SparseGPT: fewer parameters is better?
如何去除 1000 亿个参数并在一个 GPU 上愉快地进行推断
在大型数据集上扩展方法
之前的剪枝研究主要集中在小数据集上,但了解这些方法在大数据集上的表现也很重要。因此,作者在 ImageNet 上对各种先前的方法进行了基准测试,并评估了它们对模型性能的影响(他们选择了 8 种不同的方法)。
结果显示,这些指标仅保留了少量的困难样本,并且表现优于随机剪枝。但尽管它们在小数据集上表现良好,只有少数指标与在完整数据集上训练所获得的性能相匹配。此外,作者还指出:
我们发现所有的剪枝指标都会放大类别不平衡问题,从而导致性能下降。图 5 显示,许多数据剪枝指标在 ImageNet 上的扩展效果不好,而少数能够扩展的指标则需要大量的计算资源。此外,这些指标都需要标签,从而限制了它们对大规模基础模型进行剪枝的能力,这些基础模型通常是在大量未标记的数据集上训练的。因此,迫切需要简单、可扩展的自监督剪枝指标。来源:这里

图片来源:原文文章
作者提出了以下解决方案:
-
第一步是使用一个名为 SWaV 的预训练模型来提取数据集中每个样本的低维表示。
-
使用 k-means 聚类对样本的表示进行分组。
-
然后,使用余弦距离计算样本到簇中心的距离。如果一个样本离簇中心较近(因此作为表示也更接近其他样本),则它被认为是容易排序的样本;但如果它离中心较远,则被认为是难排序的样本。
-
最后,可以根据情况决定剪枝一部分容易或困难的样本。
我们发现,我们的自监督原型指标与最佳监督指标记忆法的性能相匹配或超过,直到仅保留 70%–80% 的数据,尽管我们的指标不使用标签,并且比许多之前提出的监督指标计算起来更简单、更便宜。来源:这里
结果与当前最先进的技术记忆法相符,后者需要标签且计算速度较慢。

图片来源:原文文章
结论
作者展示了数据剪枝如何影响与缩放法则一致的错误。此外,他们还展示了通过无监督学习可以获得一个 coreset(一个数据集的子集,允许一个模型在该子集上表现与在完整数据集上的表现相同)。该方法成本低、可扩展,并且不需要标签。
展望未来,作者建议这种方法仍然可以改进,并允许更激进的剪枝。这对大型基础模型的训练将极为有用。他们还建议:
如果这些高度剪枝的数据集版本可以用来训练大量不同的模型,可以考虑这些精心挑选的数据子集作为基础数据集,其中数据剪枝的初始计算成本可以在许多下游模型的训练效率提升中摊销,就像训练基础模型的初始计算成本在许多下游任务的微调效率提升中摊销一样。来源:这里
总之,减少数据集的规模在训练前可以节省时间和成本(减少标注工作)。更不用说,前瞻性地,如果你减少过度代表的群体,这将有助于在训练过程中对抗和/或识别偏差。
你们怎么看?你们尝试过剪枝你们的数据集吗?
如果你觉得这很有趣:
你可以查看我的其他文章,你也可以订阅以便在我发布文章时收到通知,你也可以在LinkedIn上联系我。
这是我的 GitHub 仓库的链接,我计划在这里收集与机器学习、人工智能以及更多相关的代码和资源。
[## GitHub - SalvatoreRa/tutorial: 机器学习、人工智能、数据科学的教程…]
机器学习、人工智能、数据科学的教程,包括数学解释和可重复使用的代码(使用 Python…)
或许你会对我最近的一篇文章感兴趣:
[## Microsoft BioGPT: 迈向生命科学领域的 ChatGPT?]
BioGPT 在不同的生物医学自然语言处理任务中达到了最先进的水平。
levelup.gitconnected.com [## 关于 ChatGPT 的所有信息]
了解已知的最新消息、其影响以及正在发生的变化。一篇文章包含所有内容。
无监督学习与涌现模式
无监督学习如何帮助我们检测相变和涌现现象?
·
关注 发布于 Towards Data Science · 6 分钟阅读 · 2023 年 4 月 26 日
--
William L. Stefanov 摄影,发表于 NASA-JSC 。(城市增长可以被理解为二阶相变和一种涌现现象)。
本文灵感来源于 Lei Wang [1] 的论文 Discovering Phase Transitions with Unsupervised Learning。
介绍
自然科学中最有趣的现象之一是相变或临界现象,通常可以在物质状态的过渡中看到,如固体、液体和气体。然而,这一现象超越了常规,连接了大脑、铁磁材料片和种群动态等方面[2,3]。然而,这些相变的涌现模式并不总是容易检测。因此,可以使用无监督学习技术来帮助完成这一任务。在本文中,我将使用主成分分析(PCA)作为玩具模型进行相变识别,这是一种用于临界现象分析的最简单模型之一,也就是 2D 伊辛模型。它由威尔赫尔姆·伦茨于 1920 年引入,以简化方式描述铁磁材料。
2D 伊辛模型相变的动态涌现现象。GIF 由作者提供。
相变与涌现现象
当系统经历相变时,会展示由微观结构组件相互作用产生的各种复杂宏观结构模式,从而诞生出涌现的模式。兰道在 1930 年代对这些现象进行了最简单的描述,他引入了φ这一阶参数概念,它可以是标量、向量甚至张量。阶参数提供有关系统的相位及其之间过渡的信息。相变的阶次可以定义为:
一阶相变
在一阶相变中,阶参数 φ 会在相变过程中出现不连续跳跃。
二阶相变
在二阶相变中,阶参数 φ 在相变过程中不会出现不连续跳跃;然而,它的导数会出现不连续跳跃。

第一阶段和第二阶段相变中阶参数的区别
阶数。图像由作者提供。
最简单的相变模型之一
2D 伊辛模型是描述铁磁材料的最简单模型之一,由一个自旋网格组成,自旋由离散变量描述,取两个值σᵢ = -1, 1,即自旋 “上” 和自旋 “下”。 这些自旋之间的相互作用由称为哈密顿量的函数给出,由系统的温度 T 控制,但我不会详细讨论。这个系统以在临界温度Tc 处经历二阶相变而著名,具有非常简单的描述。

2D 伊辛模型的示意图,其中每个网格节点为自旋,蓝色表示
按“下”按钮和“上”按钮的红色。图像由作者提供。
所以,对于T < Tc,温度是亚临界的,此时自旋几乎全部对齐。对于T > Tc,在超临界温度下,自旋几乎都是随机分布的。临界点的模型,即相变,展示了非常有趣的时空模式,称为突现模式,这些模式具有多种影响并在不同领域中进行研究。

2D Ising 模型的模拟展示了不同温度下的空间模式。突现现象出现在临界温度下。图像由作者提供。
2D Ising 模型的秩序参数是已知的,并由 Onsager 在 1944 年解析求解。解是简单的φ = ∑ᵢ σᵢ(即晶格中每个自旋值的总和),并且会通过无监督学习捕获,除了系统状态(即包含所有自旋值的矩阵)之外没有任何输入[1]。对于 2D Ising 模型,可以执行 PCA,甚至可能导致秩序参数的识别。
未知的相变和无监督学习
相变或秩序参数并不总是容易找到,因此某些无监督学习和降维技术可以帮助找到相变和临界点。这些技术的一个优点是它们不假设临界点的存在或局部性。主成分分析(PCA)是一种简单且常用的降维技术,广泛应用于各种物理系统中来帮助解决这一问题。
PCA 是一种通过主成分进行降维的技术,主成分是相互正交的方向,在这些方向上,数据方差随着接近第一个成分而单调增加。PCA 通过简单的线性变换找到主成分,即Y = XW。正交变换作用在列向量上W = (w₁,w₂,…,wₙ),其中wₗ表示配置空间中主成分的权重。它们由特征向量和特征值问题确定[1]。

其中XᵗX等同于协方差矩阵。我不会详细解释,但这里确实发生了很有趣的事情。
通过仅保留第一个主成分,PCA 是一种有效的降维方法,可以捕捉原始数据中的大部分线性变化,如下所示。可以看到,第一个成分表示数据扩展更广的轴。

这是一个二维数据的 PCA 组件的示例。图像由作者提供。
当应用于状态配置,即每个系统状态所在的多维空间时,PCA 会发现数据中最显著的线性变化。因此,该方法将对形成每个系统状态的基向量进行线性组合,以使方差单调增加。因此,在许多情况下,如果存在相变,则可以观察到它们。
寻找相变
对于二维伊辛模型,PCA 可以执行,甚至可以导致序参量的识别。
可以在不同温度下生成未关联的自旋配置,温度范围从 T/J = 1.6 到 2.9,假设在此范围内存在临界温度,并将每次模拟整合成一个 MxN** 矩阵,其中 M=900 是配置的总数,N 是晶格上的自旋数量。因此,矩阵的第一行的所有元素,(S₁ⱼ),将是模拟 1 在温度 T = 1.6 下的晶格自旋 σᵢ = -1,1,而矩阵的第二行,(S₂ⱼ),将是模拟 2 在温度 T = 1.62 下的晶格自旋 σᵢ = -1,1。

前两个 PCA 主成分,每个点都是模型的模拟
在二维伊辛模型在某一温度下的表现。可以看到超临界和亚临界状态的明显簇,而相变则分布在多个主成分上。图像由作者提供。
仅使用两个主成分,温度不同的簇的出现是显而易见的,超临界 T > Tc 和亚临界 T < Tc 情况下的簇特征明显,而接近相变的配置则分散。此时,其他监督算法可以分离这些簇。需要指出的是,使用其他主成分不会有显著贡献,因为模拟数据中的绝大多数方差都集中在前两个主成分中。

PCA 组件的累计解释方差。第一个主成分包含几乎所有的方差。Y 轴为对数刻度。图像由作者提供。
现在可以看到,PCA 的第一个主成分确实捕捉到了二维伊辛模型的序参量!因此,可以使用更先进的无监督学习方法来发现更复杂数据和模型的相变和涌现现象。

在二阶相变中,第一个主成分与序参量的比较。图像由作者提供。
结论
使用无监督学习和降维方法可以在不假设相变的位置和存在的情况下,甚至不需要系统状态数据之外的输入信息,检测模型的关键行为。这在没有解析解、具有复杂相变和涌现现象的更复杂模型中证明是非常有用的。
本文的笔记本可在 此处获取。
参考文献
[1] Lei Wang. (2016). 通过无监督学习发现相变. 物理评论 B, 94(19),195105. doi.org/10.1103/PhysRevB.94.195105
[2] Krkošek, M., & Drake, J. M. (2014). 关于鲑鱼种群动态中相变信号. 生物科学会议录, 281(1784), 20133221. doi.org/10.1098/rspb.2013.3221
[3] Steyn-Ross, M. L., Steyn-Ross, D. A., & Sleigh, J. W. (2004). 将全身麻醉建模为皮层中的一阶相变. 生物物理学与分子生物学进展, 85(2–3), 369–385. https://doi.org/10.1016/j.pbiomolbio.2004.02.001
无监督学习方法系列 — 探索 K-均值聚类
原文:
towardsdatascience.com/unsupervised-learning-method-series-exploring-k-means-clustering-d129fff3ab6a
让我们探索一种最著名的无监督学习方法之一,看看它如何利用距离将相似的实例映射在一起
·发表于 Towards Data Science ·阅读时间 13 分钟·2023 年 4 月 5 日
--

照片由 alexlanting @Unsplash.com 提供
无监督学习是一门神秘而有趣的艺术。虽然没有明确的标签可以预测,评估我们得到的解决方案可能会更困难,但无监督学习方法是理解数据结构和降低数据复杂性的极其有趣的技术。
与可视化和降维技术一起,聚类是无监督机器学习方法的重要组,它帮助我们将单个实例合并为更少的示例,同时丧失一些原始数据的信号。在这个无监督学习系列中,我们将首先介绍 k-均值 聚类,这是一种非常有趣和著名的基于距离的聚类方法。
K-均值算法
K-均值算法通过将每个观察映射到数据集中固定数量(k)的簇来工作。
让我们从一个示例开始,在这个示例中,我们将客户映射到一个 2 维图中,按 年龄 和 年收入:

年龄与年收入示例 — 作者提供的图片
如果我们需要对虚构商店的客户(每个客户是一个点)进行分组,在没有确定的标签系统来区分这些组的情况下,我们应该选择多少个不同的组?
为了回答这些问题,我们将首先进行一些实验。我们最初的假设是有 2 个不同的群体,我们需要将客户分配到这些群体中。
首先,我们将随机选择图上的两个点,这些点将作为 聚类中心点(代表我们组的中心)。这些点为方便识别而被标记为橙色:

聚类中心示例 — 作者图像
橙点的数量被认为是 k-means 的 k。我们的解决方案很糟糕。为什么?因为橙点未能代表潜在的数据。 我们有一个中心点(左下角),离数据太远了。我们怎么改进?
k-means 的第一步是将每个数据点分配给最近的 中心点。在我们的案例中,每个客户将被认为是我们在图中看到的橙点的一部分。
为了更容易理解,让我们从给其中一个点命名开始——客户 Steve!

Steve 与中心点 — 作者图像
Steve 有点困惑——他不知道应该加入哪个小组。他应该加入右下角的小组(由橙点表示)还是左上角的小组(由另一个橙点表示)?
让我们通过绘制 Steve 与每个小组之间的距离来帮助 Steve:

Steve 到第一组的距离 — 作者图像
表示这一距离的一个简单方法是计算欧几里得距离,即 Steve 与该组之间的距离,表示为下列公式:

如果我们将 P1 和 P2 替换为 Steve 和聚类中心点,我们得到以下计算:

Steve 到中心点 1 的距离是 22.36。那 Steve 到中心点 2 的距离呢?

Steve 到第二组的距离 — 作者图像
在这种情况下,距离是:

根据欧几里得距离,Steve 显然离第 1 组(或中心点 1)更近,因此他将被分配到该组——我们通过将代表 Steve 的点涂成红色来实现这一点:

将 Steve 分配到第一组 — 作者图像
如果我们对所有其他客户重复相同的过程,结果将是:

在上图中,我们将靠近中心点 1 的客户标记为红色(就像 Steve 一样),将靠近中心点 2 的客户标记为绿色。
我们的聚类解决方案完成了吗?不!
k-means 的下一阶段是重新计算中心点(橙点)。我们怎么做呢?我们只需计算分配给每个簇的点的平均值!
在这种情况下:
-
分配给中心点 1 的所有
Ages的平均值是 46.9。该组的所有Annual Income的平均值是 39.9。 -
质心 2 的所有
年龄的平均值为 37。这个组的所有年收入的平均值为 91。
坐标 (46.9, 39.9) 和 (37, 91) 将成为我们新的质心!让我们在 2-D 图中移动它们:

移动我们的质心 — 作者提供的图片
在图上显示新质心后,我们重新分配客户到聚类。Steve 和他的朋友们将需要重新分配!

重新设置聚类分配 — 作者提供的图片
我们再次计算每个数据点和质心之间的 欧几里得距离——完成后,我们将得到新的组别:

重新设置聚类分配 — 作者提供的图片
注意到我们第一次和第二次迭代之间发生了变化!让我们再次将迭代结果并排可视化。
迭代 1:

K-means 的第一次迭代 — 作者提供的图片
迭代 2:

K-means 的第二次迭代 — 作者提供的图片
一些客户在迭代之间从组 1 移动到组 2——即下面突出显示的紫色点:

移动客户 — 作者提供的图片
这是 k-means 聚类的核心主题,因为当没有点在迭代中更改聚类时,过程将停止。
在我们的案例中,两次迭代就足够了,因为没有客户会在下一次迭代中改变其组别——k-means 完成!
在执行聚类分组后,我们将把数据视为两个单独的数据点,由质心表示!

K-means 质心 — 作者提供的图片
这是一个非常重要的步骤——我们主动选择将数据点减少到仅 2 个。 这是数据方差的显著损失,也是聚类的核心思想之一。
我们如何评估这个解决方案? 一个想法是计算 簇内平方和,这是一个衡量每个数据点与其对应聚类之间距离的指标——从视觉上来看:

可视化 WCSS — 作者提供的图片
如果我们计算所有点与其相应质心之间的欧几里得距离,我们将得到一个大约 8850 的值——这个值表示了我们通过将客户视为两个聚类而丧失的信息。此外,我们还可以检查 簇间平方和 (bcss),它衡量所有质心之间的平均平方距离。
自然地,当我们添加一个新的质心时,WCSS 将会更低,因为点到其质心的距离减少:

用 3 个聚类可视化 WCSS — 作者提供的图片
拥有k-means直觉后,我们可以检查 Python 中sklearn的实现。此外,我们仍然不知道如何评估适当的簇数(k)——这是我们接下来会看到的内容!
Sklearn 实现
在这一部分,我们将使用航空公司客户满意度数据集,该数据集包含有关航空公司客户满意度的信息。每个观察值代表一个客户,变量包括客户的 demographic 信息、旅行类型(商务等)以及他们对航班各个方面的满意度评分。
这是数据前 5 行和 13 列的展示:
airline_data = pd.read_csv('/content/data/airline.csv')
airline_data.head(5)

航空公司数据预览——图像由作者提供
在我们的管道中进行预处理时,让我们移除一些不希望影响我们簇的列——为此,我将从可能的解决方案中移除所有分类列:
-
满意度;
-
性别;
-
客户类型;
-
类别;
-
旅行类型;
自然,这是一种我在数据管道中做出的选择,原因有两个:
-
我希望这篇博客文章的重点是解释 k-means,并避免构建一个更复杂的数据管道,这会使我们的注意力从这个目标上转移开。
-
对于分类变量,我们不希望有太多虚拟变量影响我们的簇。
随着我们在 k-means 解决方案中添加越来越多的二元(也称为虚拟)变量,这些变量在最终的簇距离中开始占据很大权重,即使在标准化之后,所以在向任何k-means解决方案中添加这类数据时要非常小心。
airline_data_filter = airline_data.drop([‘satisfaction’, ‘Gender’, ‘Customer Type’, ‘Class’, ‘Type of Travel’], axis=1)
我还注意到有 393 行的Arrival Delay in Minutes为 NA——k-means实现对此处理不是很好,所以我们需要做一些数据插补。
如果我们放大这些行,没有模式出现:
airline_data_filter.loc[airline_data_filter[‘Arrival Delay in Minutes’].isna()]

到达延误以分钟为单位为空的行——图像由作者提供
对于这些行,我将假设飞机到达的延误与起飞时的延误相同——使用np.where应用这个规则:
airline_data_filter['Arrival Delay in Minutes'] = np.where(
airline_data_filter['Arrival Delay in Minutes'].isna(),
airline_data_filter['Departure Delay in Minutes'],
airline_data_filter['Arrival Delay in Minutes']
)
规则很简单,我们依赖np.where来完成这个操作:
- 当
Arrival Delay in Minutes为na时,我们会将这一列设置为Departure Delay in Minutes,否则我们使用原始值。
预处理管道中的下一步是将所有变量标准化到一个共同的尺度。特别是在k-means中,距离是算法的关键部分,这一步骤可能对找到有意义的客户极其重要(尽管根据底层变量分布的行为和数字尺度的差异,未经标准化的测试也可能给出良好的结果)。
我将应用sklearn中的StandardScaler:
scaler = StandardScaler()
scaled_airline = scaler.fit_transform(airline_data_filter)
预处理完成——我们准备好拟合我们的k-means解决方案了!
但是..
我们选择多少个质心?
通常,在k-means解决方案中,我们会对不同的k值运行算法并评估每个解决方案的WCSS——这就是我们将要做的,使用来自sklearn的KMeans,并获取每个解决方案的wcss(存储在inertia_属性中):
from sklearn.cluster import KMeans
wcss = []
for k in range(1, 50):
print('Now on k {}'.format(k))
kmeans = KMeans(n_clusters=k, random_state=0).fit(scaled_airline)
wcss.append(kmeans.inertia_)
现在,我们可以可视化每个解决方案中WCSS的演变。有几种方法来选择合适的簇数——在这篇文章中,我们将使用肘部法则,它选择了使曲线下方变得不那么陡峭的簇数,因为这表示增加新簇不会显著降低WCSS:

WCSS 图 — 作者提供的图像
我们将选择 5 作为理想的簇数(请记住,选择肘部图中的点并不科学,实际上测试“肘部”附近的不同解决方案是个好主意):

WCSS 图 — 作者提供的图像
为了适配 5 个簇的解决方案,我们可以将该值传递给Kmeans实现中的参数:
kmeans_5 = KMeans(n_clusters=5, random_state=0).fit(scaled_airline)
现在,我们将根据此解决方案对过滤后的数据框中的每个客户进行簇预测——尽管我们应该在 scaled_data 上预测(因为它包含了解决方案拟合的相同尺度),实际上将预测结果添加到原始数据框中是个好主意,以便我们能够用有意义的尺度解释簇的均值:
airline_data['cluster_kmeans'] = kmeans.predict(scaled_airline)
我们如何分析这些簇?一个很好的想法是比较各簇中特征的均值:
airline_data_filter['cluster_kmeans'] = kmeans_5.predict(scaled_airline)
要解释cluster_kmeans,我们只需计算每个簇中所有变量的平均值:
airline_data_filter.groupby([‘cluster_kmeans’]).mean()

簇均值 — 作者提供的图像
例如,簇索引 1 似乎对他们的座位舒适度感到非常不满,因为平均而言,这组客户在调查中仅给了这个变量1.83分。虽然我们可以继续对所有变量进行这些比较,但在我们的聚类方案中仍然有很多维度(特征),这使得分析它们之间的差异变得更加困难。
为了从聚类方案中去除一些特征,我们可以应用的一些想法有:
-
去除或合并高度相关的变量。
-
进行主成分分析或其他降维技术。
为了保持文章简洁,我们来分析数值特征之间的相关矩阵:

数值特征的相关矩阵 — 作者提供的图像
从上面的相关矩阵中,我们可以识别出“在线登机”、“机上 Wi-Fi 服务”、“在线支持”和“在线预订的便捷性”似乎相关联。我将把这 4 个变量的平均值合并为一个称为在线和 Wi-Fi 满意度的变量:
online_cols = ['Online boarding', 'Inflight wifi service', 'Online support', 'Ease of Online booking']
airline_data_filter['Online and Wi-Fi Satisfaction'] = (
airline_data_filter[online_cols].mean(axis=1)
)
airline_data_filter.drop(columns=online_cols, inplace=True)
座椅舒适度和食品饮料也可以合并成一个“舒适与食品”变量:
comfort_food = ['Seat comfort', 'Food and drink']
airline_data_filter['Comfort & Food'] = (
airline_data_filter[comfort_food].mean(axis=1)
)
airline_data_filter.drop(columns=comfort_food, inplace=True)
最后,我将删除Arrival Delay in Minutes,因为它与Departure Delay in Minutes的相关性非常高:
airline_data_filter.drop(columns=['Arrival Delay in Minutes'], inplace=True)
此外,我们已经为这个数据集拟合了聚类解决方案,所以让我也删除这个变量:
airline_data_filter.drop(columns=[‘cluster_kmeans’], inplace=True)
我们只剩下 13 个特征了!K-means 解决方案可能也会受到维度灾难的影响(特别是在我们尝试解释聚类时),尝试减少数据集特征可能是以更简单的方式解释我们的聚类解决方案的好主意。
让我们查看基于包含较少特征的新数据集的肘部曲线:

特征较少的 KMeans 的 WCSS 图 — 作者提供的图像
在这种情况下,我将选择 6 个聚类作为解决方案。预测这些聚类,并将新的cluster再次添加到airline_data_filter数据集中:
kmeans_6 = KMeans(n_clusters=6, random_state=0).fit(scaled_airline)
airline_data_filter['cluster_kmeans'] = kmeans_6.predict(scaled_airline)
airline_data_filter.groupby(['cluster_kmeans']).mean()

聚类概况 — 数据集的前几列 — 作者提供的图像

聚类概况 — 数据集的其他列 — 作者提供的图像
在对我们的客户进行分析时,了解我们特征的平均值也是很重要的:

特征的全球平均值 — 作者提供的图像
基于每个变量的平均值比较,我们现在可以对我们的聚类进行一些分析!例如:
-
聚类索引 0 由一组在大多数变量上接近平均水平的客户组成。他们对飞行体验中的一些方面,如
机上和腿部空间服务、行李处理、值机服务和清洁度,似乎有些不满意。我们怎么知道的?因为他们在调查中对这些变量的平均评分为 2.8 分,比整体平均水平低 0.5 个百分点。 -
聚类索引 1 由非常满意的客户组成。这些客户给航空公司服务打分高于平均水平(对于这个客户群体,大多数变量的平均分数都在 4 星以上)。
-
另一方面,索引为 2 的聚类似乎非常不满意。这些客户给航空公司服务的评分低于平均水平。
-
索引为 3 的聚类包含长途旅行的客户,他们对航空公司服务的评分稍低于平均水平。这些客户似乎也更频繁地受到航班延误的影响。
-
索引为 4 的聚类的客户一般都很满意,除了三个变量:
Departure/Arrival Time Convenience、Gate Location和Comfort & Food。可能有一些额外的变量可以解释这些评分,例如这些客户可能是低成本旅行。 -
索引为 5 的集群包含年轻客户。非常有趣的是,他们对大多数变量的评分都为平均分,只有
Inflight Entertainment、Online & Wi-Fi Satisfaction和Comfort and Food除外。可能由于这些客户较年轻,他们对在线服务和娱乐的期望与航空公司未能满足的期望不同,这可能影响航空公司吸引年轻客户的能力。
如你所见,在 Python 中设置聚类解决方案非常简单。以下是你可以采取的一些下一步建议:
-
可视化集群内分类变量的分布。
-
检查这些集群如何与客户满意度相关。
-
构建有针对性的营销活动以提高客户满意度。例如,最后一个集群似乎对娱乐和在线服务感到失望——为什么不为年轻客户建立一个个性化的营销活动,以便在这些服务上提供更好的体验呢?
结论
就这些了!感谢你花时间阅读这篇博客文章。
我们没有讨论的一些内容是该算法的局限性。让我们用这个结论来讨论:
-
使用k-means 时需要记住的一点是算法对质心的初始位置非常敏感。这是因为如果初始质心选择不佳,算法可能会收敛到局部最小值而不是全局最小值。因此,通常建议多次运行算法,使用不同的初始质心,并选择提供最低总平方距离的解决方案。
-
k-means 的另一个局限性是它假设集群大致是球形的且大小相等。这意味着它可能在集群形状不规则或大小差异很大的数据集上效果不佳。在这种情况下,其他聚类算法可能更为合适,例如层次聚类或基于密度的聚类。
尽管有其局限性,k-means 仍然是一种流行且有效的聚类算法,已广泛应用于许多不同的场景。它相对容易实现,易于解释,并且能够高效处理大数据集。凭借其简单直观的方法,它是探索数据结构和识别可能不立即显现的模式的良好起点。
如果你想参加我的 Python 课程,欢迎随时加入 我的免费课程 (Python For Busy People — Python Introduction in 2 Hours) 或更长的 16 小时版本 (The Complete Python Bootcamp for Beginners)。我的 Python 课程适合初学者和中级开发人员,欢迎来参加我的课程!

Python 初学者课程 — 作者提供的图像
本文博客使用的数据集遵循 CC0: 公共领域 许可协议。
无监督学习系列——探索 DBScan
原文:
towardsdatascience.com/unsupervised-learning-series-exploring-dbscan-624c820290ce
在使用 Python 的 sklearn 时,了解著名的基于密度的聚类算法背后的理论
·发布在 Towards Data Science ·阅读时长 11 分钟·2023 年 11 月 21 日
--

图片来自 Kier in Sight Archives @Unsplash.com
聚类算法是数据科学领域最常用的解决方案之一,其中最受欢迎的算法被分为基于距离的方法和基于密度的方法。尽管经常被忽视,基于密度的聚类方法是k-means和层次聚类的有趣替代方案。
一些著名的基于密度的聚类技术包括DBScan(基于密度的空间聚类方法)或均值漂移,这两种算法使用数据点的质心来将观测值分组。
在这篇博客文章中,我们将探索 DBScan,一种特别适用于当你的数据包含以下特征时的聚类算法:
-
聚类具有不规则的形状。例如,非球形。
-
与其他方法相比,DBScan 不会对数据的底层分布做出任何假设。
-
你的数据集包含一些相关的离群值,这些离群值不应影响簇的质心映射。
如果这三句话让你感到困惑,不用担心!在这篇文章中,我们将一步一步实现DBScan方法,同时讨论上述话题。此外,我们还会查看著名的sklearn Python 实现!
此外,如果你想浏览我无监督学习系列的其他帖子,你可以查看:
-
K 均值
-
层次聚类
-
自组织映射
那么,让我们深入了解一下 DBScan 的工作原理吧!
拟合基于距离的聚类解决方案
在这本逐步操作手册中,我们将使用一个包含客户信息的玩具数据集。在这个例子中,我们将使用两个变量的聚类来便于理解。
假设我们经营一家商店,并且拥有关于客户的人口统计信息。我们希望根据他们的年收入和年龄进行一些营销活动,并且只希望运行 3 到 4 个不同的营销活动。
首先,我们通过散点图绘制这两个变量:

客户年龄与年收入 — 作者提供的图片
目前,假设某人的年收入为 120 美元是合理的(我们只是使用这个尺度来使年龄和收入之间的值相似,因为这样解释会更直观)。
如果你查看这些数据,你看到多少个不同的客户群体? 2 个?3 个?4 个?这是一个开放性问题,我们无法真正回答,因为没有标签或正确答案。
我们可以通过客户在图中的分布方式来争论我们有三种不同的群体:
-
图的左侧有一个散布的簇。
-
在中心处有一组 5 到 6 个客户。
-
在图的右侧有第三组簇。
不过,这只是一个猜测。也许 k 均值 可以帮助我们!让我们用 3 个簇拟合 k 均值算法:

带有 3 个质心的 K 均值解决方案 — 作者提供的图片
尽管这是一个有趣的解决方案,但似乎我们的聚类没有识别出一些模式,被离群点分散了。如果我们用 4 个质心进行拟合,结果会怎样?

带有 4 个质心的 K 均值解决方案
再次,k 均值解决方案被离群点驱散了。层次聚类会有帮助吗?让我们看看:

Ward 层次聚类 — 作者提供的图片
使用 Ward 方法,我们的结果类似于 K 均值 聚类(带有 3 个质心)。让我们改变层次聚类解决方案的连接方式,看看结果:

完整的层次聚类 — 作者提供的图片
哎呀,更糟了!更改连接方法也是一个糟糕的选择。
这个练习的核心是,某些数据集不适合基于距离的聚类。通常,人们急于使用这些类型的聚类模型,因为它们非常易于解释,但这些解决方案在我们的簇是球形且定义明确时效果更好。
解密 DBScan
现在我们已经看到基于距离的簇可能不适合某些类型的数据,让我们探讨为什么 DBScan 是不同的。
DBScan 从选择数据集中的一个随机点开始:

DBScan 步骤 1 — 作者提供的图像
选择数据点后,我们在该数据点周围绘制一个一定半径的圆。
围绕数据点构建的圆的半径是 DBScan 算法的一个超参数。
对于我们的例子,我们将绘制一个半径为 9 单位的圆:

DBScan 步骤 2 — 作者提供的图像
半径触及了多少个数据点?在这个例子中,我们的半径触及了 9 个数据点(包括数据点本身)。 这个数字与 DBScan 算法的另一个超参数有关:min_samples。
如果半径触及的点数等于或多于 min_samples 中的数值,我们将所选数据点视为核心点!
Min_samples是 DBScan 算法的另一个超参数。
被认为是核心点赋予数据点显著的权力:
-
数据点属于一个簇。
-
数据点能够将半径内的其他数据点召唤到簇中。
在测试数据点是否为核心点后,我们移动到边界内的另一个点:

移动到 DBScan 边界上的另一个点 — 作者提供的图像
我们刚刚移动到第一个数据点上方的那个数据点。接下来的问题自然是,这是否是一个核心点?
你知道如何进行这个测试!让我们开始在新数据点周围绘制一个边界:

在第二个数据点上绘制另一个边界 — 作者提供的图像
注意:这个圆的半径与我们之前看到的完全相同。
那么,这是否是一个核心点?不!因为这个数据点的半径仅触及 8 个数据点,少于我们定义的 min_samples 超参数。
我们称之为卫星数据点。 尽管它属于簇(因为它被另一个数据点“召唤”),但它没有权利将边界内的其他数据点召唤到簇中。 这是核心点和卫星点之间的主要区别。
到目前为止,我们已经知道了 DBScan 解决方案中的两种数据点:
-
核心点: 属于一个簇,并且有能力将其他点带入其中。
-
卫星点: 属于一个簇,但没有将其他数据点带入其中的能力。
我们认为当触及形成边界的卫星点时,簇才被定义。
在我们的解决方案中,以下所有数据点都被视为卫星点(使用我们的超参数),而它们边界内的所有点都属于一个簇:

卫星点前沿 —— 图片由作者提供
但仍然有一组数据点我们没有讨论 —— 那些没有被其他数据点调用的数据点怎么办?这些点肯定是核心点。它们也不能被视为卫星数据点,因为它们没有被任何核心点触及。
这些被称为离群点!这是dbscan算法的一个有用特性,它在解决方案中包含了离群点的识别。
例如,看看这里的这个数据点:

离群点识别 —— 图片由作者提供
我们也可以在这个数据点周围画一个半径:

离群点的半径 —— 图片由作者提供
注意,这个数据点与其他数据点完全孤立。此外,它的边界内没有足够的数据点来被视为核心数据点。
这使它成为离群点。数据点在 DBScan 中可以成为离群点的原因有两个:
-
它的边界内没有足够的数据点,也不是卫星点。
-
只有卫星点的边界能够到达它们。记住,卫星点无法将数据点引入自己的簇中。只有核心点承担这一责任。
DBScan 算法中的数据点被分类为核心、卫星和离群点
在将所有数据点分类为核心、卫星或离群点后,算法完成了!
DBScan 解决方案会在我们的年龄与年收入数据中找到哪些簇?让我们用以下超参数拟合一个 dbscan 解决方案(你还记得它们吗?)
-
Epsilon = 9
-
Min_Samples = 5

第一个 DBScan 解决方案 —— 图片由作者提供
紫色点不是“簇”。它们被解决方案视为离群点。
我们在这个解决方案中有一个利弊:
-
我们的 3 个簇是根据点的密度创建的,这可能是一个不错的解决方案。
-
不幸的是,这些超参数在这个解决方案中也过于严格,产生了太多离群点。
DbScan 对超参数的变化非常敏感
一个常见的技巧是进行一个DBSCan聚类,产生大量离群点,然后运行一个距离最小化算法来分配剩余的点(例如,最小欧氏距离到每个簇中心)。
另一个替代方案是调整我们的超参数。例如,我们可以提高EPS或减少min_samples:
-
EPS = 12
-
Min Samples = 4

第二个 DBScan 解决方案 —— 图片由作者提供
这个结果稍有不同,我们能够将更少的点视为异常值。尽管没有正确的解决方案,但使用不同的超参数分析聚类会给出不同的结果,因为该算法对eps和min_samples参数非常敏感。
既然我们获得了关于DBScan的新知识,那么接下来就将其应用于实际案例吧!
实际案例 — 美国的出租车站
我们将在此实际案例中使用的数据集基于Kaggle上的 Taxi Trip Duration 数据集,并在 Google BigQuery DataSets 上提供。为简化起见,我们将仅使用数据集的一个样本,以便在地图上绘制。
数据集包含大量信息,但我们感兴趣的是pickup_longitude和pickup_latitude。这两列包含乘客加入行程的 GPS 坐标。
我们的目标是让出租车公司知道他们应该在何处建立出租车站。考虑到乘客可以随时使用汽车,他们的目标是将站点建立在最合适的位置,尽量增加他们能从这些站点服务的潜在乘车次数。
要解决这个问题,让我们首先使用pandas加载数据:
import pandas as pd
taxi_data = pd.read_csv(‘/content/data/taxi_data_sample.csv’)
我们有一些数据点的 GPS 坐标错误。我们需要去除这些异常值:
taxi_data = taxi_data.loc[taxi_data.pickup_longitude > -100]
去除这些行后,让我们在二维地图上绘制我们的取车坐标。为了使以下代码正常工作,您需要安装并加载cartopy和contextily——cartopy在本地环境(例如使用conda)安装时可能会遇到一些问题——如果您遇到麻烦,请在 Google Colab 环境中安装该库:
!pip install cartopy
!apt-get -V -y -qq install python-cartopy python3-cartopy
!pip install contextily
安装依赖项后,我们可以使用以下命令在纽约绘制数据点:
import geopandas as gpd
import matplotlib.pyplot as plt
import contextily as ctx
import cartopy.crs as ccrs
def loadNewYorkMap():
# Load a GeoDataFrame with New York City boundaries
ny_shapefile = gpd.read_file(gpd.datasets.get_path('nybb'))
nyc = ny_shapefile[ny_shapefile['BoroName'] == 'Manhattan']
# Add new york city boundaries
ax = plt.axes(projection=ccrs.PlateCarree())
ax.add_geometries(nyc['geometry'], crs=ccrs.PlateCarree(), edgecolor='black', facecolor='none')
return ax
ax = loadNewYorkMap()
plt.scatter(
taxi_data.pickup_longitude,
taxi_data.pickup_latitude,
transform=ccrs.PlateCarree(),
color='red',
marker='o'
)
# Add a basemap using web tiles from OpenStreetMap
ctx.add_basemap(ax, crs=ccrs.PlateCarree(), source=ctx.providers.OpenStreetMap.Mapnik)
ax.set_xticks([])
ax.set_yticks([])
# Show the plot
plt.show()

纽约出租车接客 — 作者提供的图片
在拟合算法之前,我们知道dbscan是基于距离的。我们该怎么做?标准化!让我们将latitude和longitude标准化到相同的尺度。尽管缺乏标准化不会对这些数据造成太多问题,但做标准化仍然是一个好习惯,特别是如果您想将此用例应用于其他数据集:
scaled_taxi = StandardScaler().fit_transform(taxi_data[[‘pickup_latitude’, ‘pickup_longitude’]])
假设我们希望每个出租车站点服务至少 100 次行程,这与我们的dbscan参数如何匹配?
我们可以调整min_samples参数!让我们拟合第一个聚类解决方案:
from sklearn.cluster import DBSCAN
taxi_data[‘cluster_dbscan’] = DBSCAN(eps=0.25, min_samples=100).fit_predict(scaled_taxi)
我们选择了eps=0.25。让我们在地图上查看我们的解决方案!……但是我们应该绘制哪些坐标?一个有趣的解决方案是计算每个聚类坐标的平均值:
centroids_dbscan = taxi_data.groupby('cluster_dbscan')[['pickup_latitude', 'pickup_longitude']].mean()
此外,DBScan用值-1表示异常值。让我们排除这些值,因为它们不被视为“聚类”:
# Remove outliers
centroids_dbscan = centroids_dbscan.loc[0:]

纽约出租车接客聚类 — 作者提供的图片
非常酷!我们将设置 3 个站点:
-
曼哈顿的一篇。
-
约翰·肯尼迪国际机场的一个。
-
拉瓜迪亚机场的一个。
想查看哪些行程“理论上”由哪个站点服务?

纽约出租车接送聚类 — 作者提供的图像
很酷!紫色点被视为dbscan解决方案的离群点。似乎我们对DBScan解决方案中簇的定义有些严格。记得如何调整吗?降低eps或min_samples参数!让我们尝试一下:
taxi_data[‘cluster_dbscan_v2’] = DBSCAN(eps=0.1, min_samples=50).fit_predict(scaled_taxi)
检查我们新解决方案的质心并进行绘制:
centroids_solution_2 = taxi_data.groupby(‘cluster_dbscan_v2’)[[‘pickup_latitude’, ‘pickup_longitude’]].mean()
# Remove outliers
centroids_solution_2 = centroids_solution_2.loc[0:]

纽约出租车接送聚类 — 作者提供的图像
注意我们如何使用这些新参数设置更多站点。通过调整eps和min_samples,我们能够在数据中创建更小的簇:

纽约出租车接送聚类 — 作者提供的图像
继续调整eps和min_samples参数,亲自查看对聚类元素的影响!
结论
DBScan 是一种广泛使用的算法,在某些情况下非常酷,特别是当你的簇不是球形的且包含不规则模式时。
DBScan 已在基因表达聚类、客户分析或某些图像分割实例中得到应用。虽然k-means和层次聚类更为著名,但基于密度的聚类方法是你可以考虑的非常酷的选项,特别是对于你的无监督学习项目。
总之,这里是你应该记住的关于 DBScan 的要点:
-
它能够识别离群点。
-
它将点分为核心点、卫星点和离群点。
-
它使用数据点周围的半径(eps)来创建密度簇。
-
它使用最小样本数(min_samples 参数)来根据其功能(核心、卫星、其他)对数据点进行分类。
如果你喜欢这篇文章,确保阅读我在无监督学习系列中的其他文章:
-
K-Means
-
层次聚类
-
自组织映射
随时查看 YouTube 视频,我在我的频道 The Data Journey 中解释了 DBScan。
如果你想加入我的 Python 课程,请随时参加我的 完整的 Python 入门训练营。我的 Python 课程适合初学者/中级开发者,我很希望你能加入我的课程!
[本文使用的数据集受公共领域许可证保护]
无监督学习系列:探索层次聚类
原文:
towardsdatascience.com/unsupervised-learning-series-exploring-hierarchical-clustering-15d992467aa8
让我们探讨层次聚类是如何工作的,以及它如何基于成对距离构建簇。
·发布于 Towards Data Science ·阅读时间 11 分钟·2023 年 6 月 20 日
--

图片来源:Nathan Anderson @unsplash.com
在我上一篇无监督学习系列的文章中,我们探讨了最著名的聚类方法之一,K-means 聚类。在这篇文章中,我们将讨论另一种重要聚类技术背后的方法——层次聚类!
这种方法也基于距离(欧氏距离、曼哈顿距离等),并使用数据的层次表示来组合数据点。与k-means 相反,它不包含任何关于质心数量的超参数(如k),这些是数据科学家可以配置的。
大多数情况下,层次聚类可以分为两类:凝聚型聚类和分裂型聚类。在前者中,数据点被视为单一单位,并根据距离聚合到附近的数据点。在后者中,我们将所有数据点视为一个整体簇,并根据某些标准开始将其划分。由于凝聚型版本是最著名和广泛使用的(sklearn 的内置实现遵循这种协议),这是我们将在这篇文章中探讨的层次类型。
在这篇博客文章中,我们将分两步探讨凝聚型层次聚类:
-
首先,我们将逐步分析如何构建层次结构,使用凝聚型聚类中的平均方法(这是我们用来构建数据点层次结构的方法之一)。
-
然后,我们将看到如何使用 sklearn 的实现对实际数据集进行层次聚类的一些示例。这也是我们详细说明构建层次结构的其他方法(ward, minimum, 等)的地方。
让我们开始吧!
聚类示例 — 步骤详解
在我们的逐步示例中,我们将使用一个包含 5 个客户的虚拟数据集:

层次聚类示例 — 作者图片
设想一下我们经营一个有 5 个客户的商店,并希望根据他们的相似性对这些客户进行分组。我们有两个变量需要考虑:客户年龄和年收入。
我们聚类的第一步是计算所有数据点之间的成对距离。 我们通过将每个数据点表示为[x, y]格式来实现:
-
[60, 30]和[60, 55]之间的距离:25.0
-
[60, 30]和[30, 75]之间的距离:54.08
-
[60, 30]和[41, 100]之间的距离:72.53
-
[60, 30]和[38, 55]之间的距离:33.30
-
[60, 55]和[30, 75]之间的距离:36.06
-
[60, 55]和[41, 100]之间的距离:48.85
-
[60, 55]和[38, 55]之间的距离:22.0
-
[30, 75]和[41, 100]之间的距离:27.31
-
[30, 75]和[38, 55]之间的距离:21.54
-
[41, 100]和[38, 55]之间的距离:45.10
尽管我们可以使用任何类型的距离度量,但由于其简单性,我们将使用欧几里得距离。从我们之前计算的成对距离中,哪一个是最小的呢?
中年客户之间的距离,这些客户年收入不到 90k 美元 — 坐标为[30, 75]和[38, 55]!
回顾一下两个任意点 p1 和 p2 之间的欧几里得距离公式:

欧几里得距离公式 — 作者图片
让我们通过连接更近的两个客户,在二维图上可视化我们最小的距离:

连接两个最近的客户 — 作者图片
层次聚类的下一步是将这两个客户视为我们的第一个簇!

将最近的客户视为一个簇 — 作者图片
接下来,我们将再次计算数据点之间的距离。但这一次,我们将把我们分为一个簇的两个客户视为一个数据点。例如,考虑下面的红点,它位于两个数据点的中间:

将最近的客户视为一个簇 — 作者图片
总之,对于我们层次解决方案的下一次迭代,我们不会考虑原始数据点的坐标(表情符号),而是红点(这些数据点之间的平均值)。这就是计算平均连结方法距离的标准方式。
我们可以用来计算基于聚合数据点的距离的其他方法有:
-
最大(或完全连结):考虑与我们尝试聚合的点相关的集群中最远的数据点。
-
最小(或单一连结):考虑与我们尝试聚合的点相关的集群中最近的数据点。
-
Ward(或Ward连结):在下一次聚合中最小化集群中的方差。
让我在逐步解释中稍作停顿,深入探讨一下连结方法,因为这些方法在这种类型的聚类中至关重要。这里是一个关于层次聚类中不同连结方法的视觉示例,展示了合并的 3 个虚拟集群:

连结方法可视化
在sklearn实现中,我们将能够尝试一些这些连结方法,并在聚类结果中看到显著差异。
返回我们的示例,现在生成所有新数据点之间的距离 — 记住,从现在开始有两个集群被视为一个:

将最接近的客户视为一个集群 — 作者提供的图像
-
[60, 30]与[60, 55]之间的距离:25.0
-
[60, 30]与[34, 65]之间的距离:43.60
-
[60, 30]与[41, 100]之间的距离:72.53
-
[60, 55]与[34, 65]之间的距离:27.85
-
[60, 55]与[41, 100]之间的距离:48.85
-
[34, 65]与[41, 100]之间的距离:35.69
哪个距离最短? 是坐标[60, 30]和[60, 55]之间的路径:

将下一个最接近的客户视为一个集群 — 作者提供的图像
下一步自然是将这两个客户合并为一个集群:

创建下一个集群 — 作者提供的图像
在这个新的集群布局中,我们再次计算成对的距离! 请记住,我们总是将每个集群中数据点之间的平均值(根据我们选择的连结方法)作为距离计算的参考点:
-
[60, 42.5]与[34, 65]之间的距离:34.38
-
[60, 42.5]与[41, 100]之间的距离:60.56
-
[34, 65]与[41, 100]之间的距离:35.69
有趣的是,下一个需要聚合的数据点是两个集群,它们位于坐标[60, 42.5]和[34, 65]:

合并下一个集群 — 作者提供的图像
最终,我们通过将所有数据点聚合成一个大集群来完成算法:

将最终数据点合并到我们的簇中 — 作者提供的图片
考虑到这一点,我们到底在哪里停止呢?拥有一个包含所有数据点的大簇可能不是一个好主意,对吧?
要知道我们在哪里停止,我们可以使用一些启发式规则。但首先,我们需要熟悉另一种可视化我们刚刚完成的过程的方式 —— 树状图:

我们的层次聚类解决方案的树状图 — 作者提供的图片
在y 轴上,我们有刚刚计算的距离。在x 轴上,我们有每个数据点。从每个数据点爬升,我们到达一条水平线 —— 这条线的 y 轴值表示将连接边缘数据点的总距离。
记得我们最初将客户连接成一个簇吗?我们在 2D 图中的观察结果与树状图相符,因为这些正是使用水平线连接的第一个客户(从树状图底部爬升):

树状图中的第一条水平线 — 作者提供的图片
水平线代表我们刚刚完成的合并过程!自然地,树状图的末尾是一条连接所有数据点的大水平线。
既然我们已经熟悉了树状图,我们现在准备检查sklearn实现,并使用真实数据集来理解如何根据这种酷炫的聚类方法选择适当的簇数量!
Sklearn 实现
对于 sklearn 实现,我将使用这里提供的葡萄酒质量数据集。
wine_data = pd.read_csv('winequality-red.csv', sep=';')
wine_data.head(10)

葡萄酒质量数据集预览 — 作者提供的图片
这个数据集包含了关于葡萄酒(特别是红葡萄酒)的信息,如柠檬酸、氯化物或密度等特征。数据集的最后一列表示葡萄酒的质量,这是由一个评审小组进行的分类。
由于层次聚类处理的是距离,而我们将使用欧几里得距离,我们需要对数据进行标准化。 我们将从对数据应用 StandardScaler 开始:
from sklearn.preprocessing import StandardScaler
sc = StandardScaler()
wine_data_scaled = sc.fit_transform(wine_data)
使用我们标准化的数据集,我们可以拟合我们的第一个层次聚类解决方案!我们可以通过创建一个 AgglomerativeClustering 对象来访问层次聚类:
average_method = AgglomerativeClustering(n_clusters = None,
distance_threshold = 0,
linkage = 'average')
average_method.fit(wine_data_scaled)
让我详细说明我们在AgglomerativeClustering中使用的参数:
-
n_clusters=None用作获取所有簇的完整解决方案的方式(并且在这里我们可以生成完整的树状图)。 -
distance_threshold = 0必须在sklearn实现中设置,以生成完整的树状图。 -
linkage =average``是一个非常重要的超参数。记住,在理论实现中,我们描述了一种考虑新形成簇之间距离的方法。average是计算新距离时考虑每个新形成簇之间的平均点的方法。在sklearn实现中,我们还有三种其他方法,也就是我们所描述的:single,complete和ward。
在拟合模型后,接下来是绘制树状图。为此,我将使用sklearn 文档中提供的辅助函数:
from scipy.cluster.hierarchy import dendrogram
def plot_dendrogram(model, **kwargs):
# Create linkage matrix and then plot the dendrogram
# create the counts of samples under each node
counts = np.zeros(model.children_.shape[0])
n_samples = len(model.labels_)
for i, merge in enumerate(model.children_):
current_count = 0
for child_idx in merge:
if child_idx < n_samples:
current_count += 1 # leaf node
else:
current_count += counts[child_idx - n_samples]
counts[i] = current_count
linkage_matrix = np.column_stack(
[model.children_, model.distances_, counts]
).astype(float)
# Plot the corresponding dendrogram
dendrogram(linkage_matrix, **kwargs)
如果我们绘制我们的层次聚类解决方案:
plot_dendrogram(average_method, truncate_mode="level", p=20)
plt.title('Dendrogram of Hierarchical Clustering - Average Method')

平均方法的树状图 — 作者提供的图像
这个树状图不是很好,因为我们的观察似乎有点拥挤。有时,average,single和complete链接可能会导致奇怪的树状图,尤其是当数据中有强烈的离群值时。ward方法可能适合这种类型的数据,因此我们来测试一下这个方法:
ward_method = AgglomerativeClustering(n_clusters = None,
distance_threshold = 0,
linkage = 'ward')
ward_method.fit(wine_data_scaled)
plot_dendrogram(ward_method, truncate_mode="level", p=20)

Ward 方法的树状图 — 作者提供的图像
更好了!注意到这些簇似乎根据树状图得到了更好的定义。正如我们在帖子第一部分中描述的,ward 方法试图通过最小化新形成簇之间的内部方差来划分簇(online.stat.psu.edu/stat505/lesson/14/14.7)。目标是使得每次迭代中要聚合的簇最小化方差(数据点与新簇之间的距离)。
再次,通过更改linkage参数可以在AgglomerativeClustering函数中实现更换方法!
由于我们对ward方法的树状图效果满意,我们将使用该解决方案进行簇分析:

Ward 方法的树状图 — 作者提供的图像
你能猜到我们应该选择多少个簇吗?
根据距离,一个好的选择是将树状图在这个点切割,在这里每个簇似乎相对彼此远离:

Ward 方法的树状图,截止点在 30 — 作者提供的图像
我们的线穿过的垂直线数即为最终解的簇数。选择簇的数量并不是很“科学”,不同的簇数可能会得到不同的聚类解,这取决于业务解释。例如,在我们的案例中,稍微在树状图上方切割并减少最终解的簇数也可能是一种假设。
我们将坚持使用 7 个簇的解决方案,因此让我们将ward方法与这些n_clusters进行拟合:
ward_method_solution = AgglomerativeClustering(n_clusters = 7,
linkage = 'ward')
wine_data['cluster'] = ward_method_solution.fit_predict(wine_data_scaled)
由于我们希望基于原始变量解释我们的集群,我们将对标准化数据使用预测方法(距离基于标准化数据集)但将集群添加到原始数据集中。
让我们使用每个变量的均值来比较我们的集群,条件是 集群 变量:
wine_data.groupby([‘cluster’]).mean()

集群特征分析——作者图像
有趣的是,我们可以开始对数据进行一些见解——例如:
- 低质量的葡萄酒似乎有较高的
总二氧化硫含量——请注意最高平均质量集群和较低质量集群之间的差异:

集群 6 和 2 之间的二氧化硫——作者图像
如果我们比较这些集群中葡萄酒的 质量:

集群 6 和 2 之间的质量密度图——作者图像
显然,平均而言,集群 2 包含更高质量的葡萄酒。
另一个有趣的分析是对聚类数据均值进行相关矩阵分析:

集群均值的相关矩阵——作者图像
这为我们提供了一些可以探索的潜在线索(即使是对监督学习)。例如,在多维层面上,含有较高 硫酸盐 和 氯化物 的葡萄酒可能会被聚集在一起。另一个结论是,含有更高酒精度的葡萄酒往往与更高质量的葡萄酒相关。
结论
就是这样!感谢您花时间阅读这篇关于无监督学习的博客文章。我会继续在这个系列中添加更多无监督学习算法,以展示我们可以使用的不同类型的方法来了解数据的结构。
自然地,层次聚类有一些优缺点,我们可以讨论:
-
算法的一个大缺点是它可能需要过多的启发式方法来达到最终解决方案。可能需要结合树状图分析、基于距离的分析或轮廓系数方法来确定合理的集群数量。此外,必须避免将这些技术方法与一些业务知识结合,以避免陷入某种聚类陷阱。
-
从积极的一面来看,层次聚类方法非常可解释,有助于揭示数据中的隐藏结构。
-
另外,层次聚类不受中心点初始化问题的影响——这可能对某些数据集来说是一个优势。
层次聚类是一种非常著名的聚类方法,已应用于多种不同的应用场景:
-
客户细分;
-
异常值分析;
-
分析多维基因表达数据;
-
文档聚类;
这是一种非常酷的方法,数据科学家应该在他们的工具箱中拥有它。可以在下一个项目中尝试,并关注更多关于这个无监督学习系列的帖子!
如果你有兴趣参加我的 Python 课程,请随时加入 我的免费课程 (Python For Busy People — Python Introduction in 2 Hours) 或一个更长的 16 小时版本 (The Complete Python Bootcamp for Beginners)。我的 Python 课程适合初学者/中级开发人员,我非常希望你能来上我的课!
本文使用的数据集遵循 创意共享署名 4.0 国际 (CC BY 4.0) 许可协议,相关链接如下: archive.ics.uci.edu/dataset/186/wine+quality
无监督学习系列——探索自组织映射
原文:
towardsdatascience.com/unsupervised-learning-series-exploring-self-organizing-maps-fe2efde9f7a1
了解自组织映射的工作原理以及它们为什么是有用的无监督学习算法
·发表于 Towards Data Science ·阅读时间 16 分钟·2023 年 8 月 6 日
--

图片由 teckhonc @Unsplash.com
自组织映射(SOMs)是一种用于聚类的无监督神经网络类型 和 高维数据的可视化。SOMs 通过一种竞争学习算法进行训练,在这种算法中,网络中的节点(也称为神经元)竞争表示输入数据的权利。
SOM 架构由一个 2D 网格节点组成,其中每个节点都与一个权重向量相关,该向量表示 SOM 解决方案中的质心均值。节点以这样的方式组织,使得节点围绕相似的数据点组织,从而产生一个表示潜在数据的层。
SOMs 通常用于各种任务,如:
-
数据可视化
-
异常检测
-
特征提取
-
聚类
我们还可以将 SOMs 视为最简单的无监督学习神经网络版本!
虽然最初看起来有些困惑,但自组织映射(或称 Kohonen 映射,以其发明者命名)是一种有趣的算法类型,能够从数据中映射出潜在结构。它们可以描述如下:
-
一种单层无监督神经网络,没有反向传播。
-
一种受限的 k-means 解决方案,其中节点有能力影响其他节点的移动(在 k-means 的上下文中,节点被称为质心)。
在这篇博客文章中,我们将对 SOM 模型进行几个实验。之后,我们将把自组织映射应用于一个实际案例,在那里我们将能够看到算法的主要特性和缺陷。
理解 SOM 如何学习
要理解 SOMs 如何学习,我们从在 2 维中绘制一个玩具数据集开始。
我们将创建一个numpy数组,并随后绘制出来:
import numpy as np
X = np.array([[1, 2], [2, 1], [1, 3], [1, 2.5], [3.1, 5], [4, 10], [3.6, 5.4], [2, 3]])

玩具数据集绘制 — 作者提供的图像
这 8 个数据点使用 X 和 Y 轴进行排列,代表任意变量。
我们如何开始在这个数据集上训练自组织映射?SOM 的第一个重要特征是它是一个依赖于多个超参数的算法。 了解这些超参数的工作原理及其对学习过程的影响非常重要。
第一组超参数定义了 SOM 的大小,通常称为 网格。这个值类似于 k-means 的质心数量和神经网络层的 神经元数量。它提供了我们将用于减少数据点的元素数量。
在这个例子中,我们将构建一个简单的 2 by 2 网格。这意味着我们将有 4 个质心(神经元)来表示我们的完整数据集。
学习 1: 网格的大小是 SOM 的一个超参数
让我们绘制它们:
plt.scatter(X[:,0], X[:,1])
neurons = [[2,4], [2,8], [3,4], [3,8]]
for neuron in neurons:
plt.scatter(neuron[0], neuron[1], c='orange', s=400, edgecolor='black')

2 by 2 网格 — 作者提供的图像
在橙色中,我们可以看到我们的 网格 作为数据中的质心进行表示。我们还可以将这些数据点以“隐藏层”格式表示,仿佛我们在谈论一个神经网络,在这个网络中,每个神经元由 X 和 Y 特征的值(权重)表示:

2 by 2 网格的网络格式 — 作者提供的图像
黑线表示 x 轴的 权重(值),红色虚线表示 y 轴的 权重(值)。
学习 2: 我们可以将 SOM 的单元称为节点(神经网络术语)或质心(聚类术语)
SOM 训练的第 1 步:选择一个随机点,并计算到每个 神经元/质心 的欧几里得距离

SOM 训练的第一步 — 作者提供的图像
这个数据点将选择与 最佳匹配单元 (BMU) 距离最短的神经元。
学习 3: 最佳匹配单元是我们的数据点选择为最相似的单元
这个单元是将被拖向我们的数据点的神经元。
但是.. 是否只有这个节点会在此次训练迭代中移动?不!所有其他质心也会移动,根据它们与 BMU 的距离。这与 K-Means 有显著不同,在 K-Means 中,质心没有能力在算法的每次迭代中拖动其他质心。
BMU 和其他节点将被拖向数据点。这一移动将使用一个邻近函数,该函数将作为我们在推动 SOM 节点时应用的“力量”。
定义这种“邻近函数”的一种最常见方式是创建 基于距离的高斯方法。我们首先计算节点与 BMU 之间的距离:

节点与 BMU 之间的距离 — 作者图片
在这种情况下,我们使用的是欧氏距离,其中 distance = sqrt((x2-x1)² + (y2-y1)²)。
现在,我们需要将这个距离输入到高斯核中,以考虑 BMU 对其他节点的影响。
这个高斯近似将基于以下函数:

高斯核示例 — 作者图片
分子是神经元之间的欧氏距离,分母将标准差 (σ) 乘以 2。请记住,这个标准差是算法的另一个超参数!
让我们从视觉上来看这个问题。如果我们考虑较小的标准差,一个类似于这个的高斯核将“绘制”围绕BMU的以下圆圈:

BMU 周围的高斯核 — 作者图片
上面图像的阴影不透明度是节点将被拉向数据点的力量的代理。
如果我们选择更高的标准差,BMU 的影响会更大,更多的节点会受到影响:

BMU 周围的高斯核,大的标准差 — 作者图片
学习 4:标准差是我们可以在 SOM 训练中配置的超参数
我会将高斯公式的结果称为howclose — 例如,howclose 在我们 BMU 节点(记住它是由数据点选择的)和其他节点之间的值如下(使用 σ=1):

BMU 与每个数据点的 howclose — 作者图片
现在我们知道了我们将在每个节点上施加的力量,以将其拉向数据点。对于邻近节点,我们将施加最多力量的数据点是位于右下角的节点。相反,我们几乎不会移动位于右上角和左上角的节点。
我们知道了更新 SOM 时将使用的力量,但仍需要知道我们将如何移动每个节点。这可以通过更新公式来实现:

节点值更新公式 — 作者图片
其中:
-
wi,k 是节点 k 的变量 i 的当前坐标。
-
howclose 是我们之前计算的力量。
-
learningrate 是 SOM 算法的另一个超参数,类似于其他神经网络训练过程。
-
(xi-w i,k) 是我们将推动节点的方向。例如,在我们的示例中,所有节点都会被向下拖动,因为数据点位于每个节点下方(在 y 变量上)。
上述计算的结果将为我们提供 SOM 中现有节点的新坐标! 另一个重要细节:我们没有对 BMU 应用权重的计算(仅对邻近节点)。一种常见的方法是应用最大强度来拖动 BMU(高斯函数的输出=1)。
如果我们应用此逻辑,节点的新坐标是什么?计算结果见下表:

节点值更新摘要 — 图片由作者提供
让我们看看右下角节点的示例:
-
在x 轴(上表的第一行),此点的新坐标将是 2.53,而不是 3**。这是应用强度乘以学习率并将节点更新到 x 变量的数据点的结果。
-
在y 轴(上表的第四行),此点的新坐标将是 3.377,而不是 4。这是应用强度乘以学习率并将节点更新到 y 变量的数据点的结果。
此外,注意到右上角和左侧的节点移动微乎其微。为什么?因为howclose在将这些节点推向数据点时施加了较低的强度。
我们的 BMU 将如何移动?自然,它将被拖到数据点的顶部:

更新 BMU — 图片由作者提供
根据我们在更新值表中获得的坐标,让我们查看我们“新的”SOM:

SOM 中的新节点 — 图片由作者提供
紫色节点是更新后的“SOM”节点。我们的自组织映射图试图通过根据我们选择的随机点进行调整来模拟潜在的数据点。让我们看看三维视图中发生了什么:

SOM 中的新节点 — 3D 视图 — 图片由作者提供
注意,当我们将其与原始随机网络进行比较时,我们的 SOM 稍微移动了一下,新的黑色和虚线表示节点的新权重。

原始随机 SOM — 3D 视图 — 图片由作者提供
我们的 SOM 将对数据集中所有可用的数据点执行此过程(每次迭代时调整自身)。记住每一步:
-
计算 BMU(最佳匹配单元)使用距离度量。
-
计算 BMU 周围的核以了解如何将其他节点拉向数据点。
-
使用强度、学习率和方向的组合来拉动节点。
-
移动到下一个点并重复。
就像其他神经网络模型一样,SOM 中的纪元数是我们需要定义的另一个超参数。一个纪元包括对整个数据集的单次遍历。
学习 5:SOM 将继续调整自身,直到达到用户定义的纪元数。
在 Python 中,我们可以使用minisom包来训练自组织映射:
from minisom import MiniSom
som = MiniSom(
2, 2, 2, sigma=1,
learning_rate=1, neighborhood_function=’gaussian’, random_seed=42)
som.train(X, 100)
Minisom函数的参数如下:
-
前三个数字表示 SOM 的维度:x 轴上的节点数量,y 轴上的节点数量,以及变量的数量。
-
Sigma 包含起始标准差。
-
然后我们有算法的起始学习率(在这种情况下,设置为 1)。
-
neighborhood_function定义了我们的邻域的gaussian核。 -
random_seed确保我们的 SOM 结果可复现。
在对这个自组织映射(SOM)进行拟合和训练(如som.train函数中指定的 100 轮)后,节点会最终在哪里呢?让我们来看一下:
nodes = som.get_weights().reshape(4, 2)
plt.scatter(nodes[:,0], nodes[:,1], c=’purple’, s=400, edgecolor=’black’)
plt.show()

训练后的 SOM——作者图片
注意我们的 SOM 尽力模仿了底层数据点的特征!
尽管你可能认为这与k-means非常相似,但让我们看看在这个数据集上拟合 4 个质心的 k-means 解决方案的结果:

注意,在这个 k-means 解决方案中,右上角的节点被拉向了离群值。节点通过邻域函数连接在 SOM 中的这一事实,是算法的主要特征之一,防止节点被离群值拉离。
好的,很棒!我们已经对 SOM 有了很好的介绍,现在让我们看看 SOM 在实际数据中的应用!
将 SOM 应用于实际数据
在这部分博客文章中,我将使用一个包含 S&P 500 公司信息的数据集:
financial_valuation_data = pd.read_csv(‘/data/constituents-financials_csv.csv’)
这里是数据框的前 5 行:

financial_valuation_data 前 5 行——作者图片
在这个数据集中,我们可以看到以下列:
-
符号:公司的股票代码。
-
名称:公司的名称。
-
行业:公司的行业。
-
价格:公司当前的股价(截至 2014 年 12 月 31 日)。
-
价格/收益:股价与每股收益的比率。
-
每股收益:公司每股的收益金额。
-
52 周低点:过去一年中的最低股价。
-
52 周高点:过去一年中的最高股价。
-
市值:公司的总价值,等于每股价格乘以股份数量。
-
EBITDA:息税折旧摊销前利润。
-
价格/销售额:价格除以每股收入。
-
价格/账面价值:将价格除以公司的账面价值(资产-负债)的财务估值指标。
此外,我还会创建一个变量来模拟股价的波动性(尽管这不是股价波动性的理论公式,但它是一个简单的代理):
financial_valuation_data[‘Volatility’] = (
financial_valuation_data[’52 Week High’]-financial_valuation_data[’52 Week Low’]
)/financial_valuation_data[’52 Week High’]
很好!所以我们从子集化几个列开始,使我们的 SOM 更简单:
financial_valuation_data_subset = (
financial_valuation_data[[‘Price/Earnings’,’Dividend Yield’,’Earnings/Share’,’Price/Sales’, ‘Price/Book’, ‘Volatility’]]
)
你能根据上述子集确定我们的 SOM 将有多少维度吗?
6,我们刚提取的变量数量! 我们的玩具示例中的 SOM 只有两个维度(用于可视化)。这个新的 SOM 将更难以可视化,但幸运的是,还有一些图可以为我们提供训练模型的洞察。
我们在玩具示例中没有涉及的另一个步骤是标准化过程。记得自组织映射开始时计算随机数据点与每个节点之间的距离吗?因此(而且我们通常使用欧几里得距离),我们需要对数据进行标准化!我们将使用sklearn's StandardScaler来实现:
scaled_data = StandardScaler().fit_transform(financial_valuation_data_subset)
scaled_data = np.nan_to_num(scaled_data, nan=0)
我还会将一些nans填补为 0。由于我们数据集中只有少量nas,这不会造成太大影响。
数据集准备好后,我们可以尝试使用MiniSom包训练我们的SOM。让我们定义以下超参数:
-
一个 15 乘 15 的网格(225 个节点)——这是一个大型网络!
-
6 个特征(SOM 的维度)。
-
σ为 0.5。
-
学习率为 1。
-
高斯近似函数。
som = MiniSom(
15,
15,
6,
sigma=0.5,
learning_rate=1,
neighborhood_function='gaussian',
random_seed=42
)
你可能会想...我应该在 SOM 中使用多少次迭代?
一种很酷的可视化方式是分批训练 SOM 并计算quantization_error,这是一个检查每个数据点与其 BMU 之间平均距离的度量:
np.random.seed(42)
num_iterations = 1000
q_errors = []
for i in range(1, num_iterations):
som.train_batch(scaled_data, i)
q_errors.append(som.quantization_error(scaled_data))
这将告诉我们我们的 SOM 在复制底层数据点方面的效果如何。理想情况下,量化误差应该为零(几乎不可能,因为 SOM 的目标是减少数据集的维度,并且一定会涉及一些误差)。

SOM 训练过程 — 作者插图
有趣的是,在约 500 次迭代后,我们的量化误差相当稳定。这个图给了我们一个关于训练过程中可能需要的迭代次数的概述。
为了安全起见,我们将训练 SOM 1000 次,使用与之前相同的参数:
som = MiniSom(
15, 15, 6, sigma=0.5,
learning_rate=1, neighborhood_function=’gaussian’, random_seed=42)
som.train(scaled_data, 1000)
我们刚刚在这些数据上训练了我们的 SOM!处理完这些后,我们将查看我们的数据点如何与我们创建的地图相关。
第一行由以下数组表示:
scaled_data[0]

第一行数组 — 作者插图
有了训练好的模型,我们可以调用som.winner()函数来检索BMU(最近的 SOM 节点):
som.winner(scaled_data[0])
上述代码的输出是(2, 11) — 这意味着在 SOM 训练结束时,距离此数据点最近的节点坐标是 2,11(记住 Python 的索引从 0 开始):

我们第一个数据点的 BMU — 作者插图
上述 SOM 中的每个节点由一个包含 6 个值(维度或变量数量)的数组描述。继续这个过程,让我们看看第二个数据点的 BMU 是什么:
som.winner(scaled_data[1])

我们第二个数据点的 BMU — 作者插图
我们可以将这种行为推广到数据框中的所有数据点:
financial_valuation_data[‘winner_node’] = (
[som.winner(scaled_data[i]) for i in range(0, len(scaled_data))]
)
自然的后续问题是:“我们能否看到哪些节点附近有更多的数据点?” (这类似于 k-means 中最具代表性的簇)
financial_valuation_data.groupby(‘winner_node’).size().sort_values().tail(10)

根据数据点表示的前 10 个节点 — 作者提供的图像
另一个我们可以用训练好的 SOM 做的常见图是距离图。这个图显示了每个节点与其邻居之间的标准化平均距离:

SOM 距离图(U-Matrix)— 作者提供的图像
在这个距离图中,我们可以理解到有 7 或 8 个节点(红色的)与其他 SOM 节点相对较远。如果这些节点上附有数据点,可能有些特殊之处——例如:

SOM 中的远离节点 — 作者提供的图像
(
financial_valuation_data
.loc[financial_valuation_data[‘winner_node’]
.apply(lambda x: (x[0] == 4) & (x[1] == 0))]
)
通过筛选以此节点作为 BMU 的公司,我们获得了以下数据:

附属于 BMU 节点的公司 — 作者提供的图像
下一个问题是——为什么这些公司在这个节点上聚集在一起?可能是因为我们分析的 6 个维度中的一个在这个节点上有一个非常特殊的值。
我们可以通过查看每个节点的变量权重值来检查这一点:

每个节点的变量权重 — 作者提供的图像
这是另一种可视化训练好的 SOM 的有用图表,因为它显示了变量在 SOM 中的影响。
对于 L Brands 和 Philip Morris,它们的市净率值似乎非常高(> 1000)——这可以在节点的价格/账面价值影响中看到。这可能需要进一步的调查,特别是因为我们数据集的平均账面价值是 14。
让我们通过查看其他变量来进一步探讨 SOM 中的权重,例如检查具有异常市盈率的节点区域:

PE 比率在 SOM 节点上的权重
从红色区域中的高值节点开始:
(
financial_valuation_data
.loc[financial_valuation_data['winner_node']
.apply(lambda x: (x[0] >= 9) & (x[0] <= 11) & (x[1] == 1))]
)

市盈率高的公司 — 作者提供的图像
另一方面,让我们看看蓝色阴影区域,我们预计在这里会有市盈率较低的公司:
(
financial_valuation_data
.loc[financial_valuation_data[‘winner_node’]
.apply(lambda x: (x[0] >= 7) & (x[0] <= 8) & (x[1] == 12))]
)

市盈率低的公司 — 作者提供的图像
我们只有一家公司与这些节点相关联,并且市盈率非常低!有趣的是,具有此数据点的节点在 distance_map 中也被标记为远离其他节点。
拥有训练好的 SOM 可以帮助我们理解数据的潜在结构,特别是数据点如何围绕这些质心进行聚集,以及哪些变量与这些质心相关。另一个常见的做法是尝试理解不同类别如何围绕节点进行聚集。例如,我们能否在训练好的 SOM 上捕捉到任何行业趋势?
我们可以在每个节点内部绘制每个数据点的行业,以评估以下内容:

SOM 中按行业分类的数据点 — 作者提供的图片
每个方块代表一个 SOM 节点,在方块内部,我们绘制了附加到该节点的每个数据点。根据我们为网络选择的变量,我们能否识别出任何行业趋势?
(剧透:实际上有两个行业,我们的 SOM 可能能够很好地识别并将行业聚集在一起)。
让我们在上面的图中子集“房地产”和“公用事业”公司:

SOM 中的房地产和公用事业公司 — 作者提供的图片
看看我们的数据点在少数节点中如何紧密聚集!如果我们将其与“信息技术”行业进行比较:

SOM 中的信息技术公司 — 作者提供的图片
如果我们想在这些数据上进行一些监督模型(预测行业),这个 SOM 会为我们提供一些关于为什么我们会在使用的变量下预测“信息技术”类别时遇到困难的有益见解。
想了解更多示例,请查看很棒的JustGlowing(minisom 包的创作者)示例库。
结论
感谢您花时间阅读这篇帖子!
SOMs 是非常有趣的被忽视的算法,能够详细描述我们数据集的结构。它们可以帮助完成多种任务,例如检测异常值(在大型 SOMs 中)或构建对异常值具有鲁棒性的聚类算法(较小的 SOMs)。SOMs 在历史上已被用于解决各种问题,如 图像压缩、检测系统 和与 地球科学 相关的问题。
与 K-Means 和 层次聚类 相比,自组织映射在对异常值的鲁棒性方面通常更强,特别是在相同数量的聚类时(不要混淆这一点与大型 SOMs 能识别数据中的边界情况的事实)。
总之,你可以将它们用于各种数据科学任务,例如:
-
预处理步骤(使用大型 SOMs 识别异常值,理解特征之间的高相关性)。
-
理解你的特征预测类别的能力。
-
构建聚类算法。
如果你喜欢这篇文章,确保阅读我在无监督学习系列中的其他文章:
-
K-Means
-
层次聚类
我们在这篇文章中使用的数据集采用了开放数据共享许可,并且可以在 这里 获取。
无监督学习与 K-Means 聚类:从图像生成颜色调色板
这是关于无监督机器学习和 K-Means 算法的全面指南,包含了一个基于颜色对图像像素进行分组的聚类用例的演示
·发表于 Towards Data Science ·阅读时间 13 分钟·2023 年 4 月 14 日
--

Billy Huynh 的照片,来自 Unsplash
无监督学习是一种方法,在这种方法中,可以发现数据中的潜在模式,而无需向机器学习算法提供额外的信息(或标签/目标)。在本文中,我记录了最近在阅读一些图像处理文章时发现的一个相当酷的 K-Means 聚类算法应用,以及无监督聚类方法在机器学习中的介绍。
本文的关键要点:
1️⃣ 无监督机器学习:介绍、分类和应用
2️⃣ K-Means 聚类的全面理解
3️⃣ 使用 Scikit Learn Python 库进行逐步的 K-Means 聚类应用,以从给定图像生成颜色调色板
4️⃣ 使用 Pillow、Requests 和 Numpy 读取和处理图像
让我们首先深入了解无监督学习
机器学习(ML),一种通过给定数据来训练机器以吸收和学习的技术,通常可以分为监督学习、无监督学习和强化学习。

图片来源:作者
✅ 监督学习是通过提供带有元数据(在机器学习术语中称为标签)的样本来教机器学习的过程,以帮助识别过程。例如,在欺诈检测用例中,事务由分析师手动标记为欺诈或真实,然后用于训练机器学习模型。这个机器学习模型从欺诈事务的模式或行为示例中学习,并能够将学习应用到新的事务中,评估它是否符合用于学习的欺诈或真实事务的模式。然后,模型将新事务与欺诈或真实类别之一关联,根据识别的模式进行预测。通常,这些预测会与置信区间或概率得分相关,以指示模式与未见样本的匹配程度。较高的得分表示机器学习模型认为预测可能是正确的。
✅ 无监督学习也会发现样本中隐藏的这些模式。不同之处在于没有标签/类别。无监督技术非常适合于探索数据和理解那些从庞大的数据集中难以被人类识别的行为。它们广泛应用于文本分类(如新闻文章)、异常检测、卫星与空间图像处理、医学图像分析、客户细分和推荐引擎等。
无监督学习用于三个主要任务—— 聚类、关联和降维。
-
聚类是一种自动将样本分组的技术。
-
关联是一种用于发现数据集中特征之间关系的技术。
-
降维是一种用于减少数据集中高维特征数量的技术。
在本文中,我将重点关注聚类作为一种分组技术。
机器学习中的聚类
聚类可以用于多个应用,例如评估数据点之间的相似或不同、数据点在向量空间中的密集程度、提取主题等。主要有四种类型的聚类技术——

图片来源:作者
-
基于中心点的聚类是通过计算数据点到中心点的平均距离来识别数据点的组。可以有多个中心点,并且初始中心点会在多个迭代中进行优化。这种技术简单、高效且有效,但对初始超参数配置敏感。K 均值聚类是最受欢迎的属于这种技术的聚类算法。
-
基于密度的聚类 将高密度区域分组为簇。这对于空间数据处理非常有用。例如,这种技术可以用来定位 COVID-19 感染家庭的高密度区域、定位人口密集区或进行森林砍伐分析。DBSCAN(基于密度的空间聚类与噪声)是最常用于检测基于密度的簇的算法。 通常,这些算法会忽略异常值。
-
基于分布的聚类 用于检测数据中是否嵌入了特定的分布(例如高斯分布)。如果已知数据中嵌入了某种特定类型的分布,推荐使用这种技术。
-
层次聚类 用于检测数据中是否存在层次关系或分类法。这种技术可以进一步分为“自上而下”和“自下而上”方法。树状图 是一种数据可视化技术,通过创建分类地图来帮助解释层次聚类的结果。层次聚类的应用包括使用 DNA 序列理解和绘制生命科学领域的进化过程,以及在 Covid-19 期间跟踪感染簇,将其建模为区域并细化为子区域。
聚类的其他方法和定义包括划分/排他性聚类(每个数据点被分配到一个且只有一个簇)、重叠/非排他性聚类(一个数据点可以被分配到多个簇)、模糊或非模糊 方法以及部分(数据点可能没有被分配到任何簇)或完全聚类技术(所有数据点都有一个或多个簇分配)。
在下一部分,我将重点讲解 K 均值聚类技术、scikit-learn 实现以及该算法的优缺点。
K 均值聚类介绍
K 均值聚类是最受欢迎的基于中心点的聚类方法之一,其具有划分簇。簇的数量是预定义的,通常用 k 表示。所有数据点都被分配到这 k 个簇中的一个且仅一个。下面演示了在二维空间中(随机)数据点在经过 3 次迭代(如右侧所设定)后被划分成 4 组的过程。

由中东技术大学(METU)提供的 K 均值聚类模拟器 | 图片来源与模拟器链接:user.ceng.metu.edu.tr/~akifakkus/courses/ceng574/k-means/
从训练 K 均值模型获得的每个簇包含一个质心(在上图中由较大的圆圈标记)并且每个数据点被分配到距离最近的质心的簇中。簇的质心是该簇中所有数据点的均值。简而言之,K 均值算法通过迭代地最小化质心与每个数据点之间的距离,将数据点分成k个簇。通常,欧几里得距离用于测量质心与数据点之间的距离。
如果你熟悉机器学习中的成本函数,你会知道这个函数的评估估计了机器学习模型预测的误差,并且提升模型性能包括了最小化该函数的策略以实现最终效果。在聚类中,成本函数求和了各个簇的扭曲度。
就 K 均值聚类而言,扭曲度是所有点到其最近簇中心的平方距离(假设使用了欧几里得距离)。为了获得最佳簇,扭曲函数需要被最小化。

扭曲函数 | 图像来源:作者 | 参考文献:cs229.stanford.edu/notes2020spring/cs229-notes7a.pdf
K 均值算法最小化J关于c,保持µ固定(当数据点根据距离被(重新)分配到固定的质心时),然后最小化J关于µ,保持c固定(当质心在根据距离(重新)分配数据点之后(重新)更新时)。
K 均值的一个变体是 K-中位数,它依赖于从质心到数据点的曼哈顿距离。
K 均值算法:

图像来源:作者
步骤 1: 随机初始化每个k簇的质心
步骤 2: 将每个点分配给最近的质心,将数据点分组到初始的k簇中。
步骤 3: 通过计算每个k簇中所有点的平均值来重新计算质心。由于质心被重新计算并因此被更新,数据点随后也会被重新分配到最近的质心。
步骤 4: 重新计算质心并将数据点重新分配到更新后的质心(即重复步骤 3),直到点不再改变簇。
Scikit-Learn 的 K 均值实现:

图像来源:scikit-learn.org/stable/modules/generated/sklearn.cluster.KMeans.html
K 均值实现 在 Scikit-Learn 中的关键超参数包括:
-
n_clusters: 用户必须提供的簇的数量
-
init: 初始化质心的策略。尽管这些质心是随机选择的,为了加速收敛并获得最佳的聚类,可以应用像 K-means++ 技术这样的策略。否则,‘random’ 使用随机初始化的聚类。K-Means++ 随机选择一个质心,然后将剩余的 k−1 个质心放置在彼此尽可能远的位置。这里有一篇深入探讨 K-Means++ 的论文。
-
n_init: K-means 算法运行的次数,每次使用不同的质心种子。如果设置为 1,则对于稀疏数据,簇可能会出现高度不平衡。为了解决这个问题,n_init 次数的聚类算法被应用,并返回具有最佳惯性(用于聚类的性能评估指标)的簇分布。
-
max_iter: 质心可以被重新计算的最大次数。这对于处理时间较长的大数据集非常有用,但低的 max_iter 也可能导致在算法收敛之前被终止,从而导致亚最佳簇。
-
algorithm: 选择 Lloyd 或 Elkan。‘Lloyd’s algorithm’ 通常用于 K-Means。在维基百科中指出,这种算法(由 Stuart P. Lloyd, Bell Labs, 1957)用于在 欧几里得空间 的子集(即建立质心)中寻找均匀分布的点集,并将这些子集划分为形状良好且大小均匀的凸单元(分配和聚类与质心相关的数据点)。
现在,让我们来看看训练中可用的关键属性:
-
cluster_centers_ : 质心的坐标数组
-
labels_ : 每个数据点的标签
-
inertia_: 样本到其最近簇中心的平方距离之和,如果提供了样本权重,则按样本权重加权。这通常用于评估簇的形成情况。
注意,与监督学习方法不同,模型仅在训练数据上进行拟合。因此,fit、fit_transform 和 fit_predict 方法将只接受一个参数,即观察的数据集。
所有可用超参数、属性和方法的详细概述见此处。
✅ 为什么 K-Means 算法如此受欢迎?
-
简单、快速、高效且易于解释
-
对于大数据集,易于扩展
-
对于常见的相似性度量,如欧几里得距离、相关性和余弦相似性,收敛是有保证的
-
灵活且可以推广以适应各种簇大小、形状和密度
⚠️ 挑战:
-
手动设置簇的数量 (k)
-
质心容易受到异常值的影响
-
K-means 聚类算法主要适用于大致圆形的聚类,对于大小和密度变化较大的数据表现较差。然而,通过应用泛化策略,可以消除这一问题。
色彩调色板生成器
一个典型的聚类过程包括:
➡️ 准备数据 ➡️ 向量化数据(例如文本的相似性评分嵌入或图像的 RGB 值) ➡️ 运行所选择的聚类算法 ➡️ 解释结果 ➡️ 通过使用更新的数据或超参数重新训练模型来进行调整。
💡 在这个项目中,给定一张图片,目标是对像素 RGB 值进行聚类,应用聚类算法并获得代表调色板颜色的聚类中心。附加任务:对调色板颜色进行排序,并在 Python 的数据可视化库中使用它们。

图片来源:作者
让我们开始吧👍
步骤 1:获取图像数据
首先,我们需要一张图片并获取每个像素的 RGB 值。为了在屏幕上表示颜色,每个像素实际上由三个颜色组件组成:红色 (R)、绿色 (G) 和蓝色 (B)。这些通常被称为像素的 RGB 值。
一些注意事项:
🔴RGB 值 (255, 0, 0) 是一个红色像素
🟢RGB 值 (0, 255, 0) 是一个绿色像素
🔵RGB 值 (0, 0, 255) 是一个蓝色像素
⚪RGB 值 (255, 255, 255) 是一个白色像素
⚫RGB 值 (0, 0, 0) 是一个黑色像素
这里是如何使用 Pillow Python 库在 Python 中完成这项工作的。在 第 5 行,我使用 Image.open() 读取了图片,在 第 15 行,我将图片转换为 Numpy array 的 RGB 代码,最后在 第 18 行,将这些值放入 Pandas DataFrame 中。
图片来源:作者
你可以简单地使用 image_variable_name(在我的例子中是 img)来查看图像,或者使用 display(img) 或 img.show() 来显示图像。

图片来源:作者
RGB DataFrame 的结果如下:

图片来源:作者
此外,为了从 URL 读取图片,下面是我使用 requests 库来获取图片的实现。
图片来源:作者
接下来,按照之前的步骤获取 RGB 值并将其转换为 DataFrame 以继续处理。
步骤 2:使用 SkLearn 的 K-Means 实现进行聚类
现在我已经有了 RGB 值,我正在使用 scikit-learn 中的 K-Means 聚类算法。我使用的参数如下:
-
n_cluster:表示聚类数量的整数。我使用了 6。这个数量也表示调色板中的颜色数量,因为我们将选择聚类中心(~ RGB 值)来代表调色板中的颜色。
-
random_state:用于质心初始化的随机种子。我使用了 0。
-
init:质心初始化方法。我使用了 k-means++,它通过基于点对整体惯性的贡献的经验概率分布来选择初始簇质心,然后在每个采样步骤进行多次试验,以选择最佳的质心。
-
n_init:k-means 算法用不同的质心种子运行的次数。由于我选择了init: K-means++,n_init被自动设置为 1。
此外,我使用了max_iter (300)和algorithm(Lloyd)的默认值。
接下来,我使用了fit()方法将模型拟合到line 9中的 RGB DataFrame。
模型成功训练后,我访问了cluster_centers_属性,将其转换为整数,并放入list中。这个嵌套列表是调色板颜色的 RGB 值,其中每个元素都是一个包含 R、G 和 B 值的列表。
图像来源:作者
步骤 3(可选):重新排序簇质心,即你的调色板颜色
这是一个可选步骤,我根据其 HSV 表示中的值对颜色进行了排序,其中 H — 色调,S — 饱和度,V — 值。通过使用colorsys的 rgb_to_hsv()方法转换后,访问[h, s, v]列表的最后一个元素,我选择按“值”排序,该值描述了颜色的亮度或强度。它由 0 到 100(百分比)之间的整数表示,其中 0 是完全黑色,100 是最亮的,显示出最丰富的颜色。
图像来源:作者
这是一个总结,用于比较排序前后的调色板。

图像来源:作者
详细代码在笔记本中,但下面是我如何使用 Plotly 生成调色板的。
图像来源:作者
步骤 4:簇分析
如果你对图像中分组在一起的像素感到好奇,这里有一种可视化它们的方法。使用predict()方法预测数据中每个样本最接近的簇。由于我们不是在评估性能,而是在评估数据中的模式,所以可以在训练数据上执行此操作 —— 即未标记的训练数据或未见过的数据集,因此,我将其传递给了line 1中的predict()。然后,将预测的簇编号数组添加回数据集。
图像来源:作者
在line 3中,我使用了 lambda 函数来映射 RGB DataFrame 中每列的单个 R、G 和 B 值,并将 RGB 值转换为相应的 Hex 代码,因为使用Seaborn 的 color_palette()方法绘制起来更简单。下面是用于将 RGB 转换为 Hex 的函数。
图像来源:作者
最后,我将每个簇的可视化过程包裹在一个 for 循环中(第 4 到第 6 行),对于每个簇编号,从 0 到调色板大小/簇数(排除),使用 seaborn.color_palette() 方法在 第 6 行 显示相应的 Hex 代码(最多 10 个,如 [:10] 指定的)。
这是颜色调色板或簇中心,用于比较和关联簇:

图片来源:作者
下面是每个簇中的颜色。 请注意,调色板按之前显示的顺序排序,它们分别对应于簇 3、0、5、2、4 和 1。

图片来源:作者
第 5 步:使用流行的可视化库中的调色板
为此,我将 RGB 值转换为可以直接作为颜色列表传递到可视化方法中的 Hex 值列表。以下是代码:
图片来源:作者
- Seaborn
这是在 Seaborn 条形图上使用调色板的示例。在 Seaborn 中,我们可以像在 第 3 行 中那样设置调色板,或者直接传递 list_hex。
图片来源:作者

图片来源:作者
2. Plotly
这是在 Plotly 条形图上使用调色板的示例。我使用了 list_hex 作为参数 color_discrete_sequence,如 第 3 行 所示。
图片来源:作者
图片来源:作者
3. Matplotlib
这是在 Matplotlib 条形图上使用调色板的示例,其中我将 list_hex 作为参数 color 传递在 第 8 行。
图片来源:作者

图片来源:作者
希望你喜欢这个聚类练习 🙂。 这是完整的 笔记本 和下面的演示!
视频来源:作者
参考文献:
-
聚类算法 | Google
-
n_init 参数实际上做了什么? | StackOverFlow
-
如何实现 K-Means++ 算法? | StackOverFlow
-
讲座 2 — k-means 聚类问题 | 加州大学圣地亚哥分校
-
第 10 讲:k-means 聚类 | 耶鲁大学
🔍 寻找监督学习相关文章?
如果你进入了电气或电子工程领域(就像我曾经一样),你可能正在处理……
towardsdatascience.com ## 预测危险的地震突发 Part I:探索性数据分析、特征工程与不平衡数据集拆分
本文展示了探索性数据分析(EDA)、特征工程以及不平衡数据集的拆分策略……
towardsdatascience.com ## 预测危险的地震突发 Part II:训练与调优监督机器学习分类器和模型……
本文展示了使用二分类器预测危险的地震突发,调整模型超参数……
towardsdatascience.com ## 使用 NLP 和机器学习预测假新闻 | Scikit-Learn | GloVe | Keras | LSTM
towardsdatascience.com
感谢访问!
我的链接: Medium | LinkedIn | GitHub
无监督机器学习:探索一系列无需输出标签的模型
了解各种无监督机器学习模型的基本原理,以及它们如何在没有输出标签的情况下生成预测
·
关注 发表在 Towards Data Science · 12 分钟阅读 · 2023 年 5 月 25 日
--
照片由 Jacopo Maia 提供,来源于 Unsplash
在过去的十年中,机器学习和人工智能取得了巨大的进展。许多不同行业的公司都创建了更新、更强大的模型。大型语言模型(LLM)的引入和实施显著增加了人们对人工智能领域的关注。来自许多行业的公司正准备迎接机器学习和通用智能领域的巨大变革。
尽管这些机器学习方面的重大进展激励人们学习最新的人工智能技术,但往往存在一个问题——这个领域成功应用的高质量、标记数据的缺乏。为了让机器学习在监督下运作,对训练数据需要有标签。获得这些数据可能是相当具有挑战性的。因此,获取这些数据可能会很昂贵且费时,因为需要人工进行手动标记。此外,这个过程可能会导致标签不一致的问题。
那么,我们如何利用未标记的数据而不需要手动标记呢?这就是无监督机器学习的用武之地。顾名思义,这种方法认为在无监督机器学习模型的训练过程中可以使用没有输出标签的数据。这些模型会识别数据中固有的模式和趋势,并根据一组特定的属性或特征将其分组到不同的类别中。在将它们分组在一起之后,我们可以找出不同组之间的共同点,并利用这些信息将业务引导到正确的方向。

照片由Lucrezia Carnelos在Unsplash上提供引用。
举例来说,考虑一个希望向各种类型的客户销售产品并提供折扣的商店。然而,他们不确定谁可能会转换成他们的服务对象。如果客户对他们的广告没有积极反应,这将导致企业收入的损失。在这种情况下,可以使用无监督机器学习来执行客户细分,将客户分组成不同的类别。因此,公司可以调整其广告策略并针对特定的群体进行定向,确保成功转化购买产品。这只是无监督机器学习在商业上的简单应用。然而,在异常检测、文本挖掘、图像识别、降维和欺诈检测等领域,还有无数其他的应用。
既然已经看到无监督机器学习的有用性,现在是时候更深入地探索这些模型及其在不同场景中的潜在用例。需要注意的是,使用这些模型时总是有优缺点的,最佳选择在很大程度上取决于公司的具体用例和限制。
K-means 聚类
K-means 聚类是一种流行的无监督机器学习模型,用于将一组项分组到不同的类别中。在此模型中,K 是一个超参数,必须根据领域知识或通过标准技术(如肘部法则或轮廓分析)来选择。下面是通常用来将数据中的元素分组到 k 个簇中的步骤列表。
-
随机初始化一组数据点作为质心,基于 k 的值。例如,如果 k 的值是 10,则会随机选择 10 个数据点并将其标记为质心。
-
在此步骤之后,会计算每个数据点与这些质心之间的距离。距离最小的点会被分配到相应的质心。距离度量可以是欧几里得距离、余弦距离或曼哈顿距离。
-
这样,所有的数据点都属于在步骤 1 中随机分配的质心中的一个。
-
为了从每个簇中确定一个新的质心,需要计算每个簇的均值,并将其指定为新的质心。这个步骤会对所有形成的簇执行。
-
在为每个簇找到新的质心集后,步骤 2 到步骤 4 会重复进行,直到收敛。换句话说,寻找最佳簇的过程会停止,因为上一轮迭代的质心与当前迭代的质心之间没有太大差异。
尽管这种方法直观且具有很好的潜力,但它也有一些缺点,我们将在下文中讨论这些缺点以及优点。
优点
-
K-means 聚类因其可扩展性和处理大数据集的能力而闻名。
-
该算法的可解释性也很好,因为将数据点分配到簇的过程是直观的。
缺点
-
该算法对初始选择的质心集非常敏感。在每次运行时,我们可能会得到不同的初始质心集,这些初始质心会影响最终的聚类结果。
-
如果数据中存在一个或少数几个离群点,它们会对该算法的最终聚类结构产生影响。
-
最优的簇数量应该由程序员确定,而不是由算法自动寻找。这有时可能会耗时较长。
层次聚类
使用 k-means 聚类方法的一个缺点是,在生成聚类时需要事先确定聚类的总数。然而,这种方法往往耗时,并且导致结果不一致。然而,在层次聚类方法中,最佳的聚类数目可以在没有人工干预的情况下确定。我们将讨论层次聚类的两个子类别。
凝聚层次聚类: 这种方法专注于以自下而上的方式逐个构建聚类。最初,每个数据点被假定为一个单独的聚类。之后,将彼此最接近的聚类合并成一个合并的聚类。此步骤会一直进行,直到我们能够将所有数据点合并为一个最终的单一聚类。该方法可以帮助我们确定聚类形成的层次结构以及它们之间的相似性。树状图显示了这种树状结构,展示了所有聚类的形成方式。
分裂聚类: 这种方法遵循自上而下的方法,将所有数据点分配到一个单一的聚类中。我们根据数据点之间的距离将其分割成聚类。这些步骤一直进行,直到我们得到n个聚类,其中n表示选择用于聚类的数据点总数。n 的值较大将导致更多的聚类和更高的计算成本。通过树状图可以更容易地可视化结果,该图也显示了用于划分聚类的相似性。
优点
-
使用层次聚类方法时不需要指定聚类的数量。
-
这种方法更加直观,因为结构中有一个层次,解释起来更容易。
-
多次运行层次聚类方法会产生确定性的结果,而不会改变聚类模式。
缺点
-
运行此算法的时间复杂度相当高。如果我们有一个包含数百万条记录的大型数据集,寻找替代算法可能更好。
-
这种算法对数据中的异常值也很敏感,这可能会影响聚类的形成或生成方式。
-
用于拆分或合并聚类的距离度量选择可能会对最终的聚类结果产生重大影响。
基于密度的空间聚类算法(DBSCAN)
这是一种无监督的机器学习方法,通过相对密度来确定聚类。以下是 DBSCAN 方法中遵循的步骤列表。
-
选择初始参数: 选择一个尚未访问的任意起始数据点。设置你的参数:
eps(epsilon)是两个样本之间被认为在同一邻域中的最大距离,minPts是邻域内样本的最小数量,以使数据点符合核心点的条件。 -
确定核心点: 对于你的起始点,计算在
eps半径内的数据点数量以确定其密度。如果在此半径内至少有minPts,则将起始数据点标记为核心点;否则,标记为噪声(这可能会在以后更新)。 -
扩展簇: 对于每个核心点,如果它尚未被分配到簇中,则创建一个新簇。找到距离核心点
eps范围内的所有点(包括核心点本身)并将它们分配到同一个簇。如果这些点在距离eps范围内也有minPts,它们也是核心点,因此对这些点重复该过程。 -
分配边界点: 如果一个数据点在多个簇的
eps距离内,则将其分配给第一个遇到的核心点所在的簇。 -
迭代: 继续这个过程,直到所有点都被访问、分配到簇中或标记为噪声。这可能需要回到之前标记为噪声的点,检查它们是否在新找到的核心点的
eps半径内。 -
结束: 当所有点都被标记为核心点、边界点或噪声,并且已被适当地分配到簇中时,算法停止。
优点
-
不需要像 k-means 聚类方法那样指定簇的数量。
-
它对数据中的异常值更加鲁棒,因为它基于核心点和噪声点的概念。
-
它能够处理比上述讨论的算法更加任意形状的簇。
缺点
-
根据数据集的维度,需要仔细调整
minPoints和epsilon参数,这可能比较麻烦。 -
它不能用于对新数据样本进行预测,而只能对训练期间使用的数据点进行聚类。
-
运行 DBSCAN 模型多次可能不会总是产生相似的结果,因为它是非确定性的。
高斯混合模型(GMMs)
该方法基于期望最大化原理,将数据点根据其概率得分放入簇中。此外,这种方法假设数据点服从正态分布。为此,首先需要初始化均值、方差和混合权重。在此步骤之后,我们按照期望最大化方法确定数据点的簇。以下是使用高斯混合模型时采取的详细步骤解释。
-
第一步是初始化均值、方差和混合权重的集合。这可以随机完成,也可以借助 k-means 聚类方法完成。
-
在这一步之后,我们确定每个数据点属于这些 k 个簇中的任何一个的后验概率。这也称为期望阶段。
-
一旦确定了每个簇中的数据点,其组合均值、协方差和混合权重会被重新计算,以最大化期望。
-
步骤 2 和步骤 3 会重复进行,直到与期望最大化方法确定的簇相比,数据点在其他簇中的变化不再发生。
-
最终,我们得到的数据点被聚类在一起,假设每个数据点都符合正态分布。
优点
-
使用这种方法的一个优点是,它能够处理比 k-means 聚类方法更为复杂的椭圆形或球形簇。
-
这也导致了软聚类,这非常方便,因为我们可以知道模型在将数据点分配到每个簇时的置信度分数。
-
与其他模型如 k-means 相比,它们具有更高的灵活性,因为它们可以处理不同形状和大小的簇。
缺点
-
算法对最初选择的均值和协方差高度敏感。因此,较差的初始化可能会影响模型的性能。
-
确定混合模型的最佳数量可能很困难,通常通过试错法完成。
-
如果我们有一个大型数据集,由于这个原因,计算复杂度更高,模型可能会慢速收敛。
自编码器
这些模型能够将输入表示编码为较低维度的表示。它们不需要输出标签,而是只需要特征形式的输入数据。一旦生成了低维数据,也会用于解码,以获得原始数据。通过这种方式,我们能够使用编码版本重建原始数据。注意,解码输出与原始输入之间可能存在差异。这种错误也称为重建损失,在训练过程中进行优化。下面是自编码器工作原理的详细描述。
-
通过使用数据中的输入特征,进行编码以获得低维表示。通常,这种选择是为了更好地表示数据。
-
在这一步之后,编码后的信号被送入解码器,解码器能够重建原始输入。在此过程中,信号可能无法被完美重建。
-
因此,我们使用具有重建损失的优化。通过这种优化,调整权重使得重建的数据输出与输入数据紧密匹配。
-
更新每个权重时损失会减少。因此,我们得到的权重包含了将给定输入集转换为低维表示所需的信息,从而便于计算。
优点
-
与其他方法相比,它们更适合用于确定数据中存在的异常。
-
它们可以用来减少输入数据的维度,从而提高计算速度。
-
它们比其他模型更能有效地减少输入数据中的噪声。
缺点
-
当输入数据非常庞大时,运行多次训练来构建编码表示可能会非常耗费计算资源。
-
解释编码表示(隐藏表示)的含义可能会很困难,因为它们不直观。
主成分分析(PCA)
这是一种统计技术,它依赖于特征之间的协方差来确定低维空间。此外,该算法不像奇异值分解(SVD)那样需要输出训练标签来确定最佳特征。在这种方式下,我们得到的最终向量可以通过特征值和特征向量表示数据集的大部分方差。以下是执行主成分分析(PCA)的步骤列表。
-
在应用主成分分析(PCA)之前的初始步骤是对数据进行标准化。PCA 对数据的尺度非常敏感。由于机器学习的特性,我们通常输入的数据尺度各异。因此,在将这些信息提供给模型之前,对数据进行标准化是很重要的。
-
我们找到所有特征相互之间的协方差。通过这种方式,我们可以很好地理解数据中基于其他特征集的变化程度。
-
在执行这一步后,我们使用一组公式和方程来计算特征值和特征向量。为了做到这一点,我们应有一个突出重要信息的协方差矩阵。
-
在确定特征值和特征向量后,我们应按降序排列特征值,以确定每个特征相对于其他特征解释的方差。根据我们希望 PCA 分解的组件数量,我们将设置组件数量的值。
-
我们将对原始数据与特征向量进行矩阵乘法,以获得转换后的特征。这表示减少后的数据集,这样的训练和预测可能计算上更为高效。
优点
-
使用这种方法可以实现维度减少,这对于数据中特征数量非常多的场景是有用的。
-
它还可以帮助以能够在编码单元中可视化这些特征的方式减少维度。
-
当特征数量大于数据集的规模时,这可以减少过拟合。
缺点
-
由于主成分分析,解释存在的组件是困难的。原始特征更容易解释,因为它们代表了现实世界的数据,而主成分不能作为特征来解释,而是作为变换来理解。
-
对于缩放非常敏感,数据的不同尺度会极大地影响 PCA 的性能。
-
如果数据中存在离群值,它的表现可能不如其他无监督机器学习模型。
结论
探索无监督机器学习模型是一项引人入胜的工作。每个模型,如主成分分析,都具有独特的优点和局限性,这影响了我们解决问题的方法。关键在于根据数据和目标的具体情况来选择合适的工具。
通过深入了解这些模型,你可以为自己制定有效的解决方案并在数据科学领域产生有意义的影响。尽管面临挑战,但回报是显著的——将数据转化为有价值的洞察力并解决复杂问题的能力。
因此,继续学习、质疑和创新。每一步进步都有助于塑造技术的未来。感谢你将时间和好奇心投入到这一重要领域。机器学习的领域在等待你的贡献。
以下是你可以联系我或查看我工作的方式。
GitHub:suhasmaddali (Suhas Maddali ) (github.com)
YouTube:https://www.youtube.com/channel/UCymdyoyJBC_i7QVfbrIs-4Q
领英:(1) Suhas Maddali, Northeastern University, Data Science | LinkedIn
Kaggle:Suhas Maddali | Contributor | Kaggle
揭开 dropout 层的面纱:提升神经网络的必备工具
理解 dropout 层:通过 dropout 正则化提高神经网络训练效果并减少过拟合
·发布于Towards Data Science ·阅读时间 7 分钟·2023 年 5 月 19 日
--

dropout 层是构建神经网络时用于防止过拟合的一层。在这个过程中,使用一定的概率在不同的训练运行中排除个别节点,仿佛它们根本不属于网络结构。
但是,在深入了解这一层的细节之前,我们应首先了解神经网络是如何工作的以及为何会发生过拟合。
快速回顾:感知机是如何工作的?
感知机是一个受到人脑结构启发的数学模型。它由一个接收具有不同权重的数值输入的单一神经元组成。输入值与其权重相乘后加总,结果通过激活函数处理。在最简单的形式中,感知机基于激活函数产生二进制输出,如“是”或“否”。常用的激活函数是 sigmoid 函数,它将加权和映射到 0 和 1 之间的值。如果加权和超过某个阈值,输出将从 0 转变为 1。

感知机的基本结构 | 来源:作者
欲深入了解感知机的概念,欢迎参阅这篇文章:
揭开感知机的力量:深度学习的构建块
探索人工神经网络的基础及其实际应用
towardsdatascience.com
快速回顾:什么是过拟合?
过拟合发生在预测模型对训练数据过于特定时,学习了数据中的模式和噪声。这会导致对新数据的泛化能力差和预测不准确。深度神经网络特别容易过拟合,因为它们可以学习训练数据的统计噪声。然而,放弃复杂架构并不可取,因为它们能够学习复杂的关系。引入丢弃层有助于解决过拟合问题,通过提供平衡模型复杂性和泛化能力的解决方案。

泛化与过拟合的区别 | 来源: 作者
有关过拟合的更详细文章,请参考我们的相关文章:
[## 过拟合是什么? | 数据库营
过拟合是数据科学领域的术语,描述了模型对数据过度适应的特性……
丢弃层是如何工作的?
使用丢弃法,某些节点在训练运行中被设置为零,即从网络中移除。因此,它们对预测没有影响,并且在反向传播中也是如此。因此,每次运行中都会构建一个新的、略微修改的网络架构,网络学会在没有某些输入的情况下产生良好的预测。
在安装丢弃层时,还必须指定一个所谓的丢弃概率。这决定了层中多少节点将被设置为 0。如果我们有一个包含十个输入值的输入层,10%的丢弃概率意味着在每次训练中,一个随机输入将被设置为零。如果是隐藏层,则对隐藏节点应用相同的逻辑。因此,10%的丢弃概率意味着每次运行中 10%的节点将不会被使用。
最佳的概率也在很大程度上取决于层的类型。正如各种论文所发现的,对于输入层,接近 1 的丢弃概率是最佳的。而对于隐藏层,接近 50%的概率会产生更好的结果。
为什么丢弃层能防止过拟合?
在深度神经网络中,过拟合通常发生在不同层的某些神经元互相影响时。简单来说,这导致例如某些神经元纠正前一节点的错误,从而彼此依赖,或者只是传递前一层的好结果而没有重大变化。这会导致相对较差的泛化能力。
另一方面,通过使用 dropout 层,神经元不能再依赖于来自前一层或后一层的节点,因为它们无法假设这些节点在特定的训练过程中存在。这导致神经元实际上识别数据中更基本的结构,这些结构不依赖于个别神经元的存在。这些依赖关系在常规神经网络中实际上发生得相对频繁,因为这是快速减少损失函数并迅速接近模型目标的一种简单方法。
如前所述,dropout 也会稍微改变网络的架构。因此,训练出的模型是许多稍微不同模型的组合。我们已经从集成学习中熟悉这种方法,比如在随机森林中。这表明,许多相对相似的模型的集成通常比单个模型给出更好的结果。这种现象被称为“群体智慧”。
如何将 Dropout 构建到现有网络中?
实际上,dropout 层通常在全连接层之后使用,因为全连接层具有相对较多的参数,且所谓的“共适应性”,即神经元之间的依赖性,非常高。然而,从理论上讲,dropout 层也可以插入在任何层之后,但这可能导致更差的结果。
实际上,dropout 层只是插入在期望的层之后,然后使用前一层的神经元作为输入。根据概率值,这些神经元中的一些会被设置为零,然后传递到后续层。
在较大的神经网络中使用 dropout 层特别有用。这是因为具有许多层的架构比较小的网络更容易过拟合。当添加 dropout 层时,增加节点的数量也很重要。作为经验法则,节点的数量在引入 dropout 之前应除以 dropout 率。
推理过程中 dropout 会发生什么?
正如我们现在已经确定的那样,在训练过程中使用 dropout 层是避免过拟合的一个重要因素。然而,问题仍然是这个系统是否也在模型训练完成后用于对新数据进行预测时使用。
实际上,训练后 dropout 层将不再用于预测。这意味着所有神经元都将用于最终预测。然而,模型现在拥有比训练期间更多的神经元。因此,输出层的权重显著高于训练期间学到的权重。因此,权重会按照 dropout 率进行缩放,以确保模型仍能做出良好的预测。
如何在 Python 中使用 dropout 层?
对于 Python,已经有许多预定义的实现,你可以使用这些实现来应用 dropout 层。最著名的可能是 Keras 或 TensorFlow。你可以通过“tf.keras.layers”导入这些层类型:
然后,你需要传递参数,即一方面是输入向量的大小和 dropout 概率,选择这些参数时应考虑层的类型和网络结构。然后,可以通过在变量“data”中传递实际值来使用该层。还有一个参数“training”,它指定 dropout 层是否仅在训练中使用,而不在新值的预测,即推断中使用。
如果参数未被明确设置,dropout 层将仅在“model.fit()”即训练中处于活动状态,而在“model.predict()”即预测新值时不活动。
你应该记住的要点
-
Dropout 是一种神经网络层,它以定义的概率将神经元设置为零,即在训练运行中忽略它们。
-
通过这种方式,可以减少深度神经网络中的过拟合风险,因为神经元不会在彼此之间形成所谓的适应,而是识别数据中的更深层次结构。
-
Dropout 层可以用于输入层以及隐藏层。然而,研究表明,不同的 dropout 概率应根据层的类型来使用。
-
然而,一旦训练完成,dropout 层将不再用于预测。然而,为了使模型继续产生良好的结果,权重会根据 dropout 率进行缩放。
如果你喜欢我的工作,请 点击这里 订阅,或者查看我的网站 Data Basecamp!此外,Medium 允许你每月免费阅读 3 篇文章 。如果你希望 无限 访问我的文章和数千篇优秀文章,请点击我的推荐链接,会员费用为每月 $5 : medium.com/@niklas_lang/membership
理解反向传播算法在训练神经网络中的内部工作机制。
[towardsdatascience.com ## 从向量到张量:探索张量代数的数学原理
《张量在机器学习及其应用中的使用》
[towardsdatascience.com ## 机器学习中交叉验证的重要性
解释为什么机器学习需要交叉验证以及如何在 Python 中实现。
[towardsdatascience.com
揭示偏差调整的力量:在类别不平衡数据集中提升预测精度
·
关注 发表在 Towards Data Science ·11 分钟阅读·2023 年 8 月 13 日
--
解决类别不平衡问题对于数据科学中的准确预测至关重要。本文介绍了偏差调整,以提高模型在类别不平衡情况下的准确性。探索偏差调整如何优化预测并克服这一挑战。
介绍
在数据科学领域,有效管理不平衡数据集对于准确预测至关重要。不平衡的数据集以显著的类别差异为特征,可能导致模型偏向多数类,并对少数类的表现不佳,尤其是在诸如欺诈检测和疾病诊断等关键领域。
本文介绍了一种实用的解决方案,称为偏差调整。通过微调模型中的偏差项,它可以对抗类别不平衡,增强模型对多数类和少数类的准确预测能力。文章概述了针对二分类和多分类的算法,并探讨了它们的基本原理。值得注意的是,算法解释和基本原理部分严格建立了我的算法、过采样和调整类别权重之间的理论联系,从而加深了读者的理解。
为了证明其有效性和合理性,一项模拟研究考察了偏差调整与过采样之间的关系。此外,还通过实际应用展示了在信用卡欺诈检测中实现偏差调整的实施过程和实际效益。
偏差调整为在面对类别不平衡时优化预测建模结果提供了直接且有影响力的途径。本文提供了对偏差调整机制、原理及实际应用的全面理解,使其成为数据科学家在不平衡数据集中提升模型性能的重要工具。
算法
偏差调整算法引入了一种方法来解决二分类和多分类任务中的类别不平衡问题。通过在每个训练周期重新校准偏差项,该算法提高了模型处理不平衡数据集的能力。通过调整偏差项,该算法使模型对少数类实例更加敏感,从而提高分类准确性。
模型 f(x) 及其在预测中的作用
我们的偏差调整算法的核心概念是f(x) —— 这是指导我们处理类别不平衡问题的关键因素。f(x) 作为输入特征x和最终预测之间的连接。在二分类中,它作为一种映射,将输入转换为实际值,并与用于概率解释的 sigmoid 激活函数对齐。在多分类中,f(x) 变成一组函数,f_k(x),其中每个类别k都有自己的函数,与 softmax 激活函数同步工作。这种区别在我们的偏差调整算法中至关重要,我们使用f(x) 来调整偏差项并微调对类别不平衡的敏感性。
算法概述
算法的概念很简单:计算每个类别 k 的 f_k(x) 的平均值,并将该平均值表示为 δk。通过从 f_k(x) 中减去 δk,我们确保 f_k(x) − δk 的期望值对于每个类别 k 都变为 0。因此,模型预测每个类别发生的概率是相同的。虽然这提供了对算法原理的简要了解,但重要的是要注意,这种方法有理论和数学基础支撑,后续章节将进一步探讨。
二分类算法

作者创作
预测的利用:在进行预测时,应用算法中最后计算的 δ 值。该 δ 值反映了在训练过程中所做的累计调整,并作为预测时 sigmoid 激活函数中最终偏差项的基础。

多类别算法

作者创作
预测的利用:我们算法训练过程的最终结果是一个关键元素——最后计算的 δk 值。该 δk 值封装了在训练过程中精心调整的累计偏差项。它的重要性在于,它作为预测时 softmax 激活函数中最终偏差项的基础参数。

作者创作
算法解释与基本原理
从过采样到调整类别权重,从调整类别权重到新算法
在本节中,我们将深入探讨算法的解释和基本原理。我们的目标是阐明算法操作的机制和原理,提供关于其在分类任务中解决类别不平衡问题的有效性的见解。
损失函数与不平衡
我们从深入探讨算法的核心——损失函数开始。对于初步阐述,我们将研究未直接解决类别不平衡问题的损失函数。假设一个二分类问题,其中类别 1 占 90%的观察值,类别 0 占剩余的 10%。我们将类别 1 的观察值集合表示为 C1,将类别 0 的观察值集合表示为 C0,以此作为起点。
在未解决类别不平衡问题的情况下,损失函数的形式为:

作者创作
在模型估计过程中,我们努力最小化这个损失函数:

作者创作
缓解不平衡:过采样和调整类别权重
然而,我们工作的核心在于解决类别不平衡问题。为了克服这一挑战,我们探讨了过采样技术。虽然存在各种过采样方法——包括简单过采样、随机过采样、SMOTE 等——为了展示的清晰性,我们目前关注简单过采样,并稍微涉及随机过采样。
简单过采样: 我们工具箱中的一种基本方法是简单过采样,这是一种将少数类别的实例按八倍因子复制以匹配多数类别大小的技术。在我们的示例中,少数类别占 10%,多数类别占 90%,我们将少数类别的观察值复制八倍,从而有效地平衡类别分布。将复制的观察值集合记为 D0,这一步骤将我们的损失函数转化如下:

作者创建
这揭示了一个深刻的见解:简单过采样的核心原则与调整类别权重的概念无缝对应。将少数类别复制八倍有效地等同于将少数类别的权重增加到九倍。显著的是,过采样技术与权重调整的机制相似。
随机过采样: 对随机过采样的简单思考突显了一个相似的观察。随机过采样,与其更简单的对应方法类似,充当了观察权重随机调整的等效方法。
从调整类别权重到调整偏差
一个关键的启示强调了我们方法的核心:偏差调整、过采样和权重调整之间的本质等价性。这一洞察来源于
“Prentice 和 Pyke(1979)……已经表明,当模型包含每个类别的常数(截距)项时,这些常数项是唯一受 Y 的不平等选择概率影响的系数” Scott & Wild (1986) [2]。此外,Manski 和 Lerman(1977)在 softmax 设置中也显示了相同的结果 [1]。
揭示重要性: 将这一洞察转化为机器学习领域,常数(截距)项就是偏差项。这一基本观察揭示了,当我们重新校准类别权重或观察权重时,结果变化主要表现为对偏差项的调整。简而言之,偏差项是将我们的策略与解决类别不平衡问题的关键连接起来的枢纽。
统一视角
这种理解提供了一个直接的解释,说明我们的算法、过采样和权重调整在本质上是可互换和替代的。这种统一性简化了我们的方法,同时保持了其在缓解类别不平衡问题上的有效性。
模拟研究:通过过采样验证偏差项的影响
为了巩固我们的主张,即过采样主要影响偏置项,同时保持模型的功能核心不变,我们深入进行了一项针对性的模拟研究。我们的目标是实证演示过采样技术如何仅影响偏置项,而不改变模型的本质。
模拟设置
为了说明这一目的,我们关注一个简化的场景:具有单一特征的逻辑回归。我们的模型定义为:

作者创建
其中 1(.) 表示指示函数,x_i 从标准正态分布中抽取,e_i 服从逻辑斯蒂分布。在这种情况下,我们设定 f(x)=x。
运行模拟:
使用此设置,我们仔细检查了过采样技术对偏置项的影响,同时保持模型的核心不变。我们进行了三种过采样方法:简单过采样、SMOTE 和随机采样。每种方法都经过仔细应用,结果也被仔细记录。
下面的 Python 代码片段概述了模拟过程:
# Load Packages
import numpy as np
import statsmodels.api as sm
from imblearn.over_sampling import SMOTE, RandomOverSampler
# Set seed
np.random.seed(1)
# Create Simulation Datasets
x = np.random.normal(size = 10000)
y = (2.5 + x + np.random.logistic(size = 10000)) > 0
# Bias term is set to 2.5 and coefficient of x to 1
# The size of class 1 is 9005
print(sum(y == 1))
# The size of class 0 is 995
print(sum(y == 0))
# We want to match the size of class 0 to that of class 1
# Method 0 Don't do anything
x0 = x
y0 = y
method0 = sm.Logit(y0, sm.add_constant(x0)).fit()
print(method0.summary()) # 2.54 bias term and 0.97 x3 coefficient
# Method 1 Simple Oversampling
x1 = np.concatenate((x, np.repeat(x[y == 0], 8)))
y1 = np.concatenate((y, np.array([0] * (len(x1) - len(x)))))
method1 = sm.Logit(y1, sm.add_constant(x1)).fit()
print(method1.summary()) # 0.35 bias term and 0.98 x3 coefficient
# Method 2 SMOTE
smote = SMOTE(random_state = 1)
x2, y2 = smote.fit_resample(x[:, np.newaxis], y)
method2 = sm.Logit(y2, sm.add_constant(x2)).fit()
print(method2.summary()) # 0.35 bias term and 1 x3 coefficient
# Method 3 Random Sampling
random_sampler = RandomOverSampler(random_state=1)
x3, y3 = random_sampler.fit_resample(x[:, np.newaxis], y)
method3 = sm.Logit(y3, sm.add_constant(x3)).fit()
print(method3.summary()) # 0.35 bias term and 0.99 x3 coefficient
结果:

模拟结果;作者创建
主要观察
我们的模拟研究结果简洁地验证了我们的主张。尽管应用了各种过采样方法,但核心模型函数 f(x)=x 保持不变。关键见解在于模型组件在所有过采样技术中保持了显著的一致性。相反,偏置项表现出明显的变化,证实了我们的观点,即过采样主要影响偏置项,而不改变模型的基本结构。
加强核心概念
我们的模拟研究无疑强调了过采样、权重调整和偏置项修改之间的基本等价性。通过展示过采样仅改变偏置项,我们强化了这些策略作为对抗类别不平衡的可互换工具的原则。
将偏置调整算法应用于信用卡欺诈检测
为了展示我们偏置调整算法在解决类别不平衡问题上的有效性,我们使用了一个来自 Kaggle 竞赛 的真实数据集,专注于信用卡欺诈检测。在这种情况下,挑战在于预测一笔信用卡交易是否欺诈(标记为 1)或非欺诈(标记为 0),考虑到欺诈案件的固有稀少性。
我们首先加载必要的包并准备数据集:
import numpy as np
import pandas as pd
import tensorflow as tf
import tensorflow_addons as tfa
from sklearn.model_selection import train_test_split
from imblearn.over_sampling import SMOTE, RandomOverSampler
# Load and preprocess the dataset
df = pd.read_csv("/kaggle/input/playground-series-s3e4/train.csv")
y, x = df.Class, df[df.columns[1:-1]]
x = (x - x.min()) / (x.max() - x.min())
x_train, x_valid, y_train, y_valid = train_test_split(x, y, test_size=0.3, random_state=1)
batch_size = 256
train_dataset = tf.data.Dataset.from_tensor_slices((x_train, y_train)).shuffle(buffer_size=1024).batch(batch_size)
valid_dataset = tf.data.Dataset.from_tensor_slices((x_valid, y_valid)).batch(batch_size)
然后,我们定义了一个简单的深度学习模型用于二分类,并设置了优化器、损失函数和评估指标。我遵循了竞赛评估并选择了 AUC 作为评估指标。此外,模型故意被简化,因为本文的重点是展示如何实现偏差调整算法,而不是在预测中取得最佳成绩。
model = tf.keras.Sequential([
tf.keras.layers.Normalization(),
tf.keras.layers.Dense(32, activation='swish'),
tf.keras.layers.Dense(32, activation='swish'),
tf.keras.layers.Dense(1)
])
optimizer = tf.keras.optimizers.Adam()
loss = tf.keras.losses.BinaryCrossentropy()
val_metric = tf.keras.metrics.AUC()
在我们的偏差调整算法的核心中,训练和验证步骤中,我们细致地解决了类别不平衡问题。为了阐明这个过程,我们深入探讨了平衡模型预测的复杂机制。
使用累积 Delta 值的训练步骤
在训练步骤中,我们开始了提升模型对类别不平衡敏感性的旅程。在这里,我们计算并累积了两个不同集群的模型输出之和:delta0和delta1。这两个集群具有重要意义,分别代表了与类别 0 和类别 1 相关的预测值。
# Define Training Step function
@tf.function
def train_step(x, y):
delta0, delta1 = tf.constant(0, dtype = tf.float32), tf.constant(0, dtype = tf.float32)
with tf.GradientTape() as tape:
logits = model(x, training=True)
y_pred = tf.keras.activations.sigmoid(logits)
loss_value = loss(y, y_pred)
# Calculate new bias term for addressing imbalance class
if len(logits[y == 1]) == 0:
delta0 -= (tf.reduce_sum(logits[y == 0]))
elif len(logits[y == 0]) == 0:
delta1 -= (tf.reduce_sum(logits[y == 1]))
else:
delta0 -= (tf.reduce_sum(logits[y == 0]))
delta1 -= (tf.reduce_sum(logits[y == 1]))
grads = tape.gradient(loss_value, model.trainable_weights)
optimizer.apply_gradients(zip(grads, model.trainable_weights))
return loss_value, delta0, delta1
验证步骤:使用 Delta 解决不平衡问题
从训练过程中得出的归一化 Delta 值在验证步骤中发挥了核心作用。凭借这些精细化的类别不平衡指标,我们将模型的预测与真实的类别分布更准确地对齐。test_step函数将这些 Delta 值集成到自适应调整预测中,最终导致更精细的评估。
@tf.function
def test_step(x, y, delta):
logits = model(x, training=False)
y_pred = tf.keras.activations.sigmoid(logits + delta) # Adjust predictions with delta
val_metric.update_state(y, y_pred)
利用 Delta 值进行不平衡修正
随着训练的进行,我们收集了 encapsulated 在delta0和delta1集群总和中的宝贵见解。这些累积值成为我们模型预测中固有偏差的指示器。在每个训练周期结束时,我们执行了一个重要的转换。通过将累积的集群总和除以每个类别的相应观察数,我们得到归一化的 Delta 值。这种归一化作为关键的平衡器,概括了我们偏差调整方法的本质。
E = 1000
P = 10
B = len(train_dataset)
N_class0, N_class1 = sum(y_train == 0), sum(y_train == 1)
early_stopping_patience = 0
best_metric = 0
for epoch in range(E):
# init delta
delta0, delta1 = tf.constant(0, dtype = tf.float32), tf.constant(0, dtype = tf.float32)
print("\nStart of epoch %d" % (epoch,))
# Iterate over the batches of the dataset.
for step, (x_batch_train, y_batch_train) in enumerate(train_dataset):
loss_value, step_delta0, step_delta1 = train_step(x_batch_train, y_batch_train)
# Update delta
delta0 += step_delta0
delta1 += step_delta1
# Take average of all delta values
delta = (delta0/N_class0 + delta1/N_class1)/2
# Run a validation loop at the end of each epoch.
for x_batch_val, y_batch_val in valid_dataset:
test_step(x_batch_val, y_batch_val, delta)
val_auc = val_metric.result()
val_metric.reset_states()
print("Validation AUC: %.4f" % (float(val_auc),))
if val_auc > best_metric:
best_metric = val_auc
early_stopping_patience = 0
else:
early_stopping_patience += 1
if early_stopping_patience > P:
print("Reach Early Stopping Patience. Training Finished at Validation AUC: %.4f" % (float(best_metric),))
break;
结果
在我们应用于信用卡欺诈检测的过程中,我们算法的增强效果得到了体现。通过将偏差调整无缝集成到训练过程中,我们获得了令人印象深刻的 AUC 得分 0.77。这与未进行偏差调整时获得的 0.71 的 AUC 得分形成了鲜明对比。预测性能的显著改善证明了该算法在处理类别不平衡的复杂性方面的能力,为更准确和可靠的预测铺平了道路。
参考文献
[1] Manski, C. F., & Lerman, S. R. (1977). 从基于选择的样本中估计选择概率. Econometrica: Journal of the Econometric Society, 1977–1988.
[2] Scott, A. J., & Wild, C. J. (1986). 在病例对照或选择性抽样下拟合逻辑模型。《皇家统计学会 B 系列:统计方法》,48(2),170–182。
揭示推荐系统中的精确度@N 和召回率@N
原文:
towardsdatascience.com/unveiling-the-precision-n-and-recall-n-in-recommender-system-7a4c6b69d060
优化推荐系统:精确度和召回率使用案例的深入解读
·发表于Towards Data Science ·阅读时间 7 分钟·2023 年 6 月 29 日
--

讨论的主要话题是:
-
精确度和召回率概述
-
将精确度和召回率定义适配于推荐系统使用案例
-
二进制偏好转换的需求
-
实际召回率的问题
-
实际召回率的解决方案:Top-N 项目
-
一个说明性实现
-
代码实现
介绍
准确度指标是评估机器学习整体表现的有用指标,它代表数据集中正确分类实例的比例。准确度指标结合精确度和召回率等评估指标,用于更全面地理解模型的表现。
一般来说,精确度和召回率将预测的类别与测试集的实际类别进行比较,并计算正确预测与总预测数量的比例。
分类问题中的精确度和召回率
在监督分类问题中,目标值(y 值)必须是二进制的(尽管分类问题不限于多二进制值)。例如,良性或恶性,好或坏,垃圾邮件或非垃圾邮件。 这些二进制值允许应用精确度和召回率来衡量模型对预测值的分类正确性。
精确度衡量所有被预测为正的实例中正确预测的正实例(真正例)的比例

召回率衡量的是在数据集中所有实际正实例中,正确预测的正实例(真正例)的比例。

精确度和召回率提供了对模型性能的互补见解。精确度关注的是正预测的准确性,而召回率则关注模型找到所有正实例的能力。
将精确度和召回率定义适配到推荐使用案例
在构建推荐模型时,我们期望模型能够提供准确的项目推荐。分类准确率指标通过量化正确预测的实例占数据集中所有实例的百分比来衡量模型的性能。然而,在推荐系统的背景下,目标值通常由评分尺度表示,例如电影的 1-5 星评分。
因此,为了使精确度和召回率在推荐任务中适当地使用,有必要将评分尺度转换为二进制偏好。这通常通过将评分尺度转化为“相关”与“无关”项目的区分来实现。
二进制偏好转换的必要性
在这一阶段,确定一个明确的截止点非常重要。为此,我们假设任何评分高于 4 的项被认为是相关项,而评分低于 4 的项被认为是无关项(4 的截止值只是我选择的阈值,这个值可以根据每个使用案例的要求有所不同)。
在这一点上,这张插图可以帮助全面理解为何将偏好转换为二进制形式能够使推荐系统中的准确性指标如精确度和召回率变得可行。

作者插图
召回率不切实际的问题
然而,根据这篇文章 [1],
召回率在推荐系统中几乎总是难以测量。按纯粹的意义上讲,测量召回率需要知道每个项目是否相关;对于电影推荐系统,这将涉及要求许多用户观看所有 5000 部电影(在电影数据库中),以测量我们成功推荐每部电影给每位用户的程度—— Herlocker 等(2004)
为了简化上述陈述,例如,电影平台如 Netflix 不可能要求所有用户观看 5000 部电影来衡量推荐电影的准确性。由于每个用户评分的项目数量与数据集中项目的总数之间的巨大差异,召回率往往相对较低。
因此,上述问题的解决方案应集中于向用户推荐前 5 名/前 10 名的电影列表。考虑到向每个用户推荐数据库中所有可用电影既不现实也不可行。
不切实际的召回解决方案: Top-N 项目
在这里,Top-N 项目可以是一个适当的方式来利用精度和召回率指标进行推荐系统评估。它应预测用户评分的前 N 项。其中 N 是与前 N 推荐目标对应的整数。
这可以通过将数据集分为训练集和测试集来完成。训练集用于训练算法,然后在下一步中,测试集将用于预测前 N 项。为此,采用precision@N和recall@N指标。这种方法能够更相关和准确地评估系统在向用户推荐最相关项方面的表现。
Precision@N: 前 N 个推荐项中有多少对用户是相关的。精度 80%@10 表示模型提供了 10 个推荐电影,但只有 8 个实际上符合用户偏好
**Precision = (top-N 中推荐的相关项数量) / (推荐项总数)**
Recall@K: 数据集中相关项中有多少被系统推荐的前 N 项包含。召回率 60%@10 表示相关项的总数中有 60% 出现在前 N 个结果中。
**Recall = (top-N 中推荐的相关项数量) / (相关项总数)**
到此阶段,希望你理解了相关性和推荐定义(假设 4 星为分界线)

图片由作者提供
一个示例实现
如上所述,这里是需要的步骤:
-
设置阈值(如果 4 星被设定为分界线,则任何高于 4 的评分被视为相关项,任何低于 4 的评分被视为不相关项,推荐/不推荐也适用相同标准)
-
将评分数据转换为二进制偏好
-
为 Precision@N 和 Recall@N 设置 ’N’
-
计算单个 Precision@N 和 Recall@N

图片由作者提供
代码实现
如果你希望从零开始构建一个推荐系统,可以参考我之前的写作,你可以了解推荐系统背后的数学概念。然而,如果你对推荐任务已有充分了解,可以跳过这一部分。
矩阵分解背后的概念及其在 Python 中的实际实现
towardsdatascience.com
回到 daaa 代码!!
这里是计算 Precision@N 和 Recall@N 的 Python 代码片段。此外,你可以访问整个笔记本 这里。
输出结果如下:

输出片段
再次强调,由于模型推荐的相关项与每个用户评分的项数之间的巨大差异,这里的召回率值可能相对较低(尽管我们可以使用一个数据集,其中用户给出了相当多的评分,以应对接近 0 的较低召回率)。
此外,我想强调一个关于推荐系统中召回率的重要****注意事项,这也是 Herlocker 等人(2004 年)[1] 研究结果所支持的,如下所示:
recall@N 指标应仅在相同数据集上进行比较使用;不应将其解读为绝对度量
摘要
以上总结了我对 Precision@N 和 Recall@N 的全面解释。除此之外,我还在参考文献部分提供了一些对我的研究有显著贡献的有价值的论文和文章。
参考文献
[1] J.L. Herlocker, J.A. Konstan, L.G. Terveen, J.T. Riedl, 评估协同过滤推荐系统,ACM Trans Inform Syst, 22 (1) (2004), 第 5-53 页。可在此访问: grouplens.org/site-content/uploads/evaluating-TOIS-20041.pdf
矩阵分解背后的概念及其在 Python 中的实际应用
towardsdatascience.com [## Precision-N-and-Recall-N-Recommender-System/Precision_and_Recall_of_Recomender_Systems_Final.ipynb…
推荐系统中的 Precision@N 和 Recall@N。贡献于……
github.com](https://github.com/christienatashiaarchie/Precision-N-and-Recall-N-Recommender-System/blob/main/Precision_and_Recall_of_Recomender_Systems_Final.ipynb?source=post_page-----7a4c6b69d060--------------------------------) [## FAQ - Surprise 1 文档
在这里你会找到常见问题解答,以及一些其他的用例示例,这些示例不属于……
surprise.readthedocs.io [## 推荐系统中的召回率和精准率
详细说明与示例
提升您的数据可视化:4 个 Python 库以增强您的 Matplotlib 图表
使用这些易于使用的 Matplotlib 风格库改善您的数据可视化
·发表于 Towards Data Science ·阅读时间 10 分钟·2023 年 4 月 11 日
--

示例折线图,带有渐变填充,由 CyberPunk matplotlib 主题生成。图片由作者提供。
Matplotlib 是一个广泛使用的数据可视化 Python 库,通常在数据科学和 Python 学习的早期阶段就会遇到。然而,多年来,它因制作外观简单的图形而声名狼藉,而且使用起来可能会很尴尬,尤其是对于新手。
在我之前的几篇文章中,我展示了如何将 matplotlib 创建的基本图表变成 更具吸引力和美观。这从扩展绘图代码到仅添加几行代码不等。
在这篇文章中,我将与您分享四个 matplotlib 主题库,它们可以轻松地将您的 matplotlib 图形从乏味变得有趣。每个主题只需使用两行代码即可应用——一行导入语句和一行 matplotlib 使用语句。
尽管这些库非常适合创建时尚的图表,但您需要注意您的目标受众和人们可能存在的不同视觉问题,包括色盲。
让我们开始吧!
创建虚拟数据
在我们开始使用 matplotlib 样式之前,我们首先需要创建一些虚拟数据,以便我们有东西可以展示。
可以这样轻松完成:
import numpy as np
# Generate x values
x = np.linspace(0, 10, 20)
# Generate y values
y = np.sin(x)
y2 = np.cos(x)
CyberPunk 风格与 mplcyberpunk
Cyberpunk 是一种流行的科幻子流派,以其反乌托邦、技术先进的世界和反主流的态度而闻名。场景通常以未来主义风格描绘,具有霓虹灯和鲜艳的颜色。

图像由作者使用 Midjourney 生成。(一个繁忙的反乌托邦赛博朋克风格的未来城市街道,特色是有很多街头摊贩的地方。商店和区域周围有明亮的霓虹灯。阴雨和黑暗的氛围。逼真。— ar 3:2)
有时在制作海报或信息图时,你需要额外的亮点来吸引读者。这是我觉得 CyberPunk 风格发挥作用的时候。然而,你需要意识到,当为出版物制作图表时,它可能不会被视为专业,而且可能不适合有色盲问题的读者。
我们可以轻松地将这种样式应用到我们的图表中,使用CyberPunk 主题。

一个 CyberPunk 风格的 Matplotlib 图例。图像由 dhaitz 提供,来自 CyberPunk Readme。
要开始使用这个主题,我们可以这样安装:
pip install mplcyberpunk
要使用 CyberPunk 主题,我们只需调用plt.style.use()并传入参数cypberpunk。
为了让我们的点发光,我们需要调用make_scatter_glow()函数。这似乎是在每次调用plt.scatter()后都需要的。
import mplcyberpunk
plt.style.use('cyberpunk')
plt.figure(figsize = (8,8))
plt.scatter(x, y, marker = 'o')
mplcyberpunk.make_scatter_glow()
plt.scatter(x, y2, marker = 'o', c='lime')
mplcyberpunk.make_scatter_glow()
plt.xlabel('X-Axis')
plt.ylabel('Y-Axis')
plt.show()
当我们运行上述代码时,我们得到以下图表。

应用 CyberPunk 主题后的散点图。图像由作者提供。
我们也可以将 CyberPunk 主题应用于折线图。为了让线条发光,我们可以调用mplcyberpunk.make_lines_glow()。
plt.style.use('cyberpunk')
plt.figure(figsize = (8,8))
plt.plot(x, y, marker = 'o')
plt.plot(x, y2, marker = 'o', c='lime')
mplcyberpunk.make_lines_glow()
plt.xlabel('X-Axis')
plt.ylabel('Y-Axis')
plt.show()
运行时,返回的图表将显示类似霓虹灯的线条。

应用 CyberPunk 主题后的 Matplotlib 图表。图像由作者提供。
我们可以将上面的图表更进一步,加入线条与零点之间的渐变填充。
plt.style.use('cyberpunk')
plt.figure(figsize = (8,8))
plt.plot(x, y, marker = 'o')
plt.plot(x, y2, marker = 'o', c='lime')
mplcyberpunk.add_gradient_fill(alpha_gradientglow=0.5, gradient_start='zero')
plt.xlabel('X-Axis')
plt.ylabel('Y-Axis')
plt.show()
这会产生一个非常有趣的效果,如下所示。

应用 CyberPunk 主题后的 Matplotlib 图表,带有发光渐变填充。图像由作者提供。
CypberPunk 库还有几个其他选项,值得查看其仓库。
[## GitHub - dhaitz/mplcyberpunk: "Cyberpunk style" for matplotlib plots
在 matplotlib 基础上创建'cyberpunk'风格图表的 Python 包,只需额外增加 3 行代码。之后…
Matplotx
matplotx 库 提供了一种立即为 matplotlib 图形添加样式的简便方法。该库包含多个主题,可以轻松访问并应用于任何 matplotlib 图形。
在黑暗主题的 Jupyter Notebook 或 VSCode 中工作时,明亮的白色图形可能会令人感到刺眼。
为了减少这种影响,我们可以使图形采用黑暗主题。然而,这确实需要几行代码来正确设置样式。
Matplotx 使这个过程变得更容易,如下所示。由于它包含多个主题,我可以轻松找到一个与我的 VSCode 主题匹配的。
可以通过打开终端/命令提示符并运行以下命令将 Matplotx 安装到 Python 环境中。
pip install matplotx
安装库后,我们可以简单地使用 with 语句调用 plt.style.context 并传入 matplotx.styles。从这里,我们可以选择许多可用的主题之一。
在这个例子中,我选择了非常流行的 Dracula 主题
import matplotx
with plt.style.context(matplotx.styles.dracula):
plt.scatter(x, y, c=y2)
plt.colorbar(label='Y2')
plt.xlabel('X')
plt.ylabel('Y')
plt.show()
运行后,我们会得到以下结果

应用 matplotx 的 dracula 主题后的散点图。图像由作者提供。
matplotx 中有很多不同的样式:

来自 matplotx 的一些可用样式。图像来自 matplotx GitHub 仓库
让我们查看使用 Pitaya Smoothie 主题的折线图。
由于有多个子主题,我们需要使用方括号访问它们。在这个例子中,我们有一个黑暗主题,因此我们需要传入 dark 来访问它。
import matplotlib.pyplot as plt
import matplotx
with plt.style.context(matplotx.styles.pitaya_smoothie['dark']):
plt.plot(x, y, marker='o')
plt.plot(x, y2, marker='o')
plt.xlabel('X')
plt.ylabel('Y')
plt.show()
当我们运行上述代码时,我们会在所选主题中得到以下折线图。

应用 Pitaya Smoothie 黑暗主题后的 Matplotlib 折线图。图像由作者提供
还有许多浅色主题。例如,Pitaya Smoothie 具有一个,可以通过如下方式访问:
with plt.style.context(matplotx.styles.pitaya_smoothie['light']):

应用 Pitaya Smoothie 浅色主题后的 Matplotlib 折线图。图像由作者提供
如果你想了解更多关于这个库的信息,可以查看我之前的文章:
## 从乏味到惊艳:Matplotx 如何改善你的 Matplotlib 图表
简化使用 Matplotx 创建惊艳图表的过程
towardsdatascience.com
或者,如果你想获取主题并亲自尝试,可以查看下面的链接:
[## GitHub - nschloe/matplotx: 更多样式和有用的 Matplotlib 扩展
一些有用的 Matplotlib 扩展。这个包包括一些有用或美观的 Matplotlib 扩展。大多数…
github.com](https://github.com/nschloe/matplotx?source=post_page-----74361bc3b92e--------------------------------)
Quantum Black Styles
QuantumBlack Labs 是一家成立于 2012 年的公司,帮助其他公司利用数据做出更好的决策。他们使用先进的技术,如机器学习和人工智能,来分析跨医疗保健、金融和交通等多个行业的复杂数据集。
几年前,他们在GitHub上发布了自己的样式

应用 Quantum Black Labs 样式库到 matplotlib 图形的示例。图片由 Quantum Black Labs 创建。
要使用该主题,我们可以这样安装样式库:
pip install qbstyles
安装后,我们需要添加以下代码来导入它。
from qbstyles import mpl_style
如果你在 Jupyter Notebook 中工作,你需要在上述代码和下面的代码之间使用不同的单元格。
如果我们想使用深色主题,可以调用以下内容:
mpl_style(dark=True)
或者,如果我们想使用浅色主题:
mpl_style(dark=False)
然后我们可以开始编写创建散点图的代码
plt.scatter(x, y, c=y2)
plt.colorbar(label='Y2')
plt.xlabel('X')
plt.ylabel('Y')
plt.show()
根据你选择的是浅色还是深色主题,我们将得到以下其中一个图表。


应用 Quantum Black Labs 主题后的 Matplotlib 散点图(左侧为浅色模式,右侧为深色模式)。图片由作者提供
让我们看看深色主题在折线图上的效果。
mpl_style(dark=True)
plt.plot(x, y, marker='o')
plt.plot(x, y2, marker='o')
plt.xlabel('X')
plt.ylabel('Y')
plt.show()
当我们运行上述代码时,会得到以下图表。

应用 Quantum Labs Dark 主题后的 Matplotlib 折线图。图片由作者提供。
使用这个库来美化图表,虽然比 Matplotx 生成的图表稍显低调,但确实使它们感觉更专业,特别是浅色风格的图表。我完全不犹豫地将这些图表纳入专业演示或培训材料中。
你可以查看下面的链接来了解主题:
[## GitHub - quantumblacklabs/qbstyles: QuantumBlack Matplotlib styles
QB Styles 是一个 Python 包,包含浅色和深色的 matplotlib 样式。暗色样式 浅色样式 qbstyles 是一个 Python…
github.com](https://github.com/quantumblacklabs/qbstyles?source=post_page-----74361bc3b92e--------------------------------)
Science Plots
在撰写科学期刊或会议文章时,创建清晰、简单、易于解释的图形是至关重要的。某些期刊,如《自然》,要求使用特定的样式,而在其他情况下,有一个统一的样式可以防止出现难以解释的图形。
这就是SciencePlots 库发挥作用的地方。
它用于生成使用常见科学期刊样式的图表,使得创建图形变得更加容易。
这个库的一个优点是它使图形适合以黑白打印——这在研究人员中仍然是常见的做法。这将使得通过改变线条样式或确保在散点图中使用不同的形状来区分类别数据的线条变得容易。
如果你想探索更多SciencePlots中提供的样式,我建议你查看库的 GitHub 页面上的 Wiki 页面:
你现在无法执行该操作。你已经在另一个标签页或窗口中登录。你在另一个标签页或…
运行 SciencePlots 样式库需要在计算机上安装 LaTeX。你可以在这里找到有关 LaTeX 和如何安装的更多详细信息。
如果在 Google Colab 上运行,你可以在一个单元格中运行以下代码来安装 LaTeX。
!sudo apt-get install dvipng texlive-latex-extra texlive-fonts-recommended texlive-latex-recommended cm-super
!pip install SciencePlots
一旦设置好库和 LaTeX,你可以使用以下代码创建你的第一个科学图表。
import scienceplots
with plt.style.context(['science', 'high-vis']):
plt.figure(figsize = (6,6))
plt.plot(x, y, marker='o', label='Line 1')
plt.plot(x, y2, marker='o', label='Line 2')
plt.xlabel('X')
plt.ylabel('Y')
plt.legend()
plt.show()
当我们运行上述代码时,我们得到如下图表,适合包含在期刊出版物中。它简单且易于区分不同的线条。

应用 SciencePlots 主题到线性图上,提供高可见度,适合包含在科学期刊中。图片由作者提供。
让我们看看 IEEE 风格是什么样的。我们可以通过修改以下代码来创建一个。
with plt.style.context(['science', 'ieee']):
与上面略有不同,颜色也有所改变,但仍然是一个非常漂亮的科学期刊图表。

应用 IEEE 样式的 SciencePlots 主题到线性图上。图片由作者提供。
多年来,我为会议和期刊撰写了大量技术论文。这些论文中包含了很多图表,这些图表通常是在 Excel 中创建的,或者用几行 Python 绘图代码生成的。这可能很耗时,尤其是在调整尺寸、颜色和线条样式时。
未来,在我所有的论文中,我将以 SciencePlots 主题作为起点。
如果你有兴趣尝试,你可以从以下链接获取 SciencePlots 主题:
GitHub - garrettj403/SciencePlots: Matplotlib 科学绘图样式
警告:从版本 2.0.0 开始,你需要在设置样式之前添加import scienceplots(…
摘要
在本文中,我们介绍了四个非常有用的 matplotlib python 库,这些库可以将您的无聊的 matplotlib 图形提升到一个新的水平。每个库都为您的图表提供了独特的样式。
尽管这些风格提供了很好的创意可视化,但您需要考虑您的受众,尤其是那些有色盲和其他眼部问题的人群。
请记住,创建出色的数据可视化的关键在于使您的信息易于理解,并以最佳方式讲述您的故事。
但是,有时候,玩弄数据并全力以赴地使用赛博朋克主题也是不错的选择。
感谢阅读。在离开之前,您一定要订阅我的内容,并将我的文章发送到您的收件箱。 您可以在这里进行订阅!或者,您可以 注册我的新闻通讯 以免费获得额外的内容,直接发送到您的收件箱。
其次,通过注册会员,您可以获得完整的 Medium 体验,并支持其他成千上万的作家和我。每月仅需 $5,您可以完全访问所有精彩的 Medium 文章,并且有机会通过您的写作赚取收入。
如果您使用 我的链接, 您将直接支持我,并且费用不会增加。如果您这样做,非常感谢您的支持。
在机器学习系统中维护数据质量
原文:
towardsdatascience.com/upholding-data-quality-in-machine-learning-systems-d77a7d06f02e
数据 | 机器学习 | 质量保证
对机器学习中未被看到的基石的推荐
·发表于 Towards Data Science ·阅读时长 4 分钟·2023 年 6 月 29 日
--

图片来源于 Battlecreek Coffee Roasters 在 Unsplash
在炫目的机器学习(ML)世界中,很容易沉迷于制定复杂算法、引人入胜的可视化和令人印象深刻的预测模型的刺激。
然而,就像建筑物的耐久性不仅仅取决于其可见结构,还取决于其隐藏的基础一样,机器学习系统的有效性也依赖于一个常被忽视但至关重要的方面:数据质量。
上游数据质量保证的必要性
想象你的机器学习训练和推理流程是蒸汽火车的旅程。
维护火车本身——机器学习系统——的健康至关重要,但如果轨道受到损害呢?
如果提供给系统的数据质量没有得到上游保证,那就像是受损的轨道——你的火车迟早会出轨,尤其是在大规模运行时。
因此,从一开始,就必须从源头监控数据质量。
就像火车检查员在旅程前检查轨道一样,我们必须在数据的起点仔细审查数据。
这可以通过一个称为‘数据合同’的概念来实现。
数据合同在维护数据质量中的作用
想象一下被邀请参加一个聚餐会,每位客人都带来一道菜。
如果没有任何协调,你可能会遇到一个完全由甜点组成的盛宴!
类似地,在数据的广阔领域中,数据生产者和消费者之间必须达成一致(即数据合同),以确保产生的数据符合特定的质量标准。
这个合同本质上是一个蓝图,包含了一个非详尽的元数据列表,例如:
-
模式定义:数据结构的详细信息,如字段、数据类型等。
-
模式版本:确保在更改或改进时的一致性。
-
服务水平协议(SLA)元数据:SLA 规范以管理期望。
-
语义:澄清数据的含义和解释。
-
血统:记录数据从起源到目的地的旅程。
让我们通过一个执行数据合同的架构更好地理解这一点。
数据合同的实际应用:一个示例架构
想象一个制造装配线,其中每个工人都知道他们的角色和需要达到的标准。
现在,让我们将这一概念应用于我们的数据架构。
-
模式变更首先在版本控制中进行,一旦获得批准,就会在数据生产应用程序、数据库和中央数据合同注册处中实施。这是理想的开始数据合同执行的地方——在数据生产的阶段。进一步下游的任何验证步骤都作为保护措施,以防止低质量数据渗入系统。
-
数据在生成后会被推送到一些消息系统,如 Kafka 主题。这可能包括应用服务直接发出的事件或用于变更数据捕获(CDC)流的原始数据主题。
-
现在,想象一下 Flink 应用程序作为警惕的守门员,从原始数据流中消费数据,并将其与合同注册处中的模式进行验证。
-
不符合合同的数据——类似于生产线上的废品——会被导向死信主题。
-
已验证的数据被批准用于已验证数据主题,就像经过质量批准的商品准备包装和运输。
-
已验证的数据随后被发送到对象存储进行另一轮验证,作为双重检查机制。
-
按计划,对象存储中的数据会进行额外的服务水平协议(SLA)验证。通过这些审查后,数据被推送到数据仓库,在那里进行转换和建模以供分析用途。
-
从这里开始,建模和策划后的数据走两条路径。它被发送到特征商店系统进行进一步的特征工程,而实时特征则直接从已验证数据主题中获取。请注意,由于难以针对 SLA 进行检查,确保此阶段的数据质量可能具有挑战性。
-
这些高质量数据随后被用于机器学习训练管道。
-
同样的数据用于特征服务中的推理。
记住,机器学习系统也容易受到数据相关问题的影响,如数据漂移和概念漂移。
虽然这些被认为是“静默失败”,并且可以进行监控,但它们通常不包括在数据合同中。
我们将在后续文章中深入探讨数据漂移的话题。
总结评论
机器学习系统的隐藏力量在于推动它们的数据的无形完整性。
数据质量虽然不光彩,却在机器学习项目的成功中扮演着关键角色。
数据合同的概念确保了这一重要方面不会被忽视。
记住,这不仅仅是关于建设最快的火车或最令人印象深刻的车站,维持轨道的质量同样重要。
无论你的机器学习系统多么复杂,没有高质量的数据,其旅程将充满中断和潜在的脱轨。
请记住,并确保在你的机器学习工作中给予数据质量应有的重视。
毕竟,最令人兴奋的机器学习进展不仅建立在革命性的算法之上,还依赖于可靠的高质量数据。
你喜欢这篇文章吗?每月 $5,你可以成为会员,解锁 Medium 上的无限访问权限。你将直接支持我以及你在 Medium 上的其他喜爱作者。非常感谢!
## 使用我的推荐链接加入 Medium - David Farrugia
作为 Medium 的会员,你的部分会员费将转给你阅读的作者,并且你可以全面访问每个故事……
想要联系我吗?
我很乐意听听你对这个话题的看法,或是任何有关 AI 和数据的内容。
如果你希望联系我,请发邮件至 davidfarrugia53@gmail.com。
提升建模 — 数据科学家优化信用卡续卡活动指南
应用因果机器学习来缩减活动目标受众 — 第一部分/2
·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 7 月 13 日
--

由作者使用 Bing Image Creator 生成
第一部分: 缩减信用卡续卡活动的目标受众
第二部分: 在信用卡返现活动中识别最佳客户旅程
作为一个新兴的数据科学家,我的学术背景教会我将准确性视为成功项目的标志。而行业则关注的是短期和长期的盈利和节省成本。这篇文章是关于投资回报率(ROI)的课程——商业行为的圣杯。
大部分促销活动针对的是客户的细分群体,而非直接面对个人。例如,付费搜索、展示广告、付费社交等。而直接面向消费者(D2C)的活动则是直接针对个人客户。这些包括直邮、电子邮件、短信或甚至推送通知。银行和金融科技领域的企业能够开展大规模的 D2C 活动,因为每个人都有应用程序。但如今,这些企业希望在促销支出上提高效率(怎么做?)。
理解问题
有了这些背景信息,让我们来谈谈信用卡发行商 Flex,它提供第一年免费——即没有年费。从第二年开始,它会收取全额年费。在过去的 3 年里,他们观察到年保留率较低,仅有 30% 的持卡人在第一年后继续使用这张卡。Flex 决定对特定客户进行续约优惠试验,以继续扩大其客户基础。问题是——如果不小心,这种策略可能会很昂贵。
作为数据科学家,我们的任务是从 500 万名即将续约的客户中,准备出最小的目标客户群体以进行这些优惠。
提升建模简要说明
多年来,数据科学家一直致力于建立响应模型,以预测客户对直接营销活动的回应可能性。对于较新的企业,这可能有效,但随着品牌的发展,他们的问题也在不断演变。
响应模型无法解决的问题有:
-
客户在接触到活动时回应的可能性会增加多少?
-
我们如何优先考虑那些面临流失风险的客户?他们是谁?
-
是否有客户可能对促销信息产生负面回应?他们是谁?
-
我们如何减少活动中的目标客户数量而不影响增量收入?
介绍提升建模。这是一种机器学习技术,它预测处理对个人购买行为的增量影响,而不仅仅是行为的可能性。通过这种方式,你可以针对那些最有可能受到你活动影响的客户,避免在那些不容易被影响的人身上浪费资源。这将提升活动的投资回报率和客户满意度。

图片由作者提供
你可能以前见过这种客户分类。确定的客户 对你的品牌或产品有强烈的好感,无论如何都会购买。无可救药的客户 对你的产品没有需求。促销活动不太可能改变这两类客户。潜在的客户 是那些如果没有促销会购买的客户。真正的说服型客户 是那些只有在市场推广的情况下才会购买的客户。 他们能提升活动的投资回报率。
在这项任务中,我们首先要识别出说服型客户。其次,为他们找到最合适的优惠。
准备信用卡客户数据集
我们有一个包含 500 万客户的数据集,这些客户的服务期限为 10 个月,这意味着他们有 2 个月的时间进行续约。这是模拟客户数据,你可以通过这个 Python 代码自己创建。

图片由作者提供
我们在这里需要做一些探索性数据分析(EDA),我使用了ydata-profiling(以前称为 Pandas Profiling)工具生成了一个交互式报告。

作者提供的图片

作者提供的图片
我们有 20 个客户变量——包括定性(如年龄、收入层次)和定量(交易次数、分类消费)。其中一些变量的相关性相当高。
试点——深入了解之前的续订活动
Flex 已经在 5 万客户中进行了试点活动,消息内容如下。
我们很高兴地通知您,您的信用卡有资格享受特别续订优惠。在有限的时间内,您可以以仅$49 的降低年费续订您的信用卡,比常规费用节省高达 50%。此优惠仅限于像您这样的忠实客户,前提是您已使用我们的信用卡超过一年。
根据客户在第二年支付的金额,有 3 种优惠——30%、50%或 70%。从活动中得出的结论是,接受处理的客户群体的保留率为 55%,相比支付全额年费的对照组提高了 25%(55 减去 30)。这被称为平均处理效应(ATE)。

作者提供的图片
我们有活动结果,这些数据可以用来优化下一次活动。为此,我们必须计算每个客户的条件平均处理效应(CATE)——这是客户级别效果的高大上名称。
注意——试点活动是对推广或营销策略进行的小规模测试,目的是在大规模推出之前评估策略的有效性、可行性和成本,并识别和解决任何问题或挑战。试点活动可以帮助优化营销计划,提高投资回报率,并减少失败的风险。
倾向得分匹配——获得一个代表性的对照组
倾向得分匹配(PSM)旨在基于客户的观察特征匹配具有类似接受处理概率的客户。PSM 可以帮助减少观察性研究中因混杂变量造成的偏倚,其中无法随机分配处理。它包括为每个客户估计倾向得分,即在给定协变量的情况下接受处理的条件概率,然后匹配具有类似得分的处理和未处理客户。
由于在试点活动中有 3 种不同的处理方法,我将使用 PSM 来为每个处理组近似一个相同的对照组。例如——对照组中的一组客户(支付了全年的费用)与接受了年费 x 30%处理的客户类似。类似地,对于年费 x 50%和年费 x 70%的组。这样可以消除实验设置中的任何混杂变量,从而能够确定每个处理组的真实提升。
通常,倾向评分是使用简单的逻辑回归模型计算的。我还推荐如psmpy等包,这些包可以很好地完成这项工作,并处理类不平衡问题。
特征选择——导致提升增加的因素
经过倾向评分匹配后,我们有 3 对数据集——
(Control₃₀, Treatment₃₀)
(Control₅₀, Treatment₅₀)
(Control₇₀, Treatment₇₀)
我使用这些数据对构建了 3 个模型,每个处理组一个,使用CausalML库中的 X-learner 算法。SHAP 值可以用来检查哪些特征与提升相关。

图片来源:作者
评估 X-learner 模型
我们构建了 3 个 Qini 曲线,在这些曲线中,我们可以看到从最高到最低 CATE 的客户加入目标后的累计提升。这类似于传统机器学习中的 ROC 曲线。较低的线是随机分配到处理/对照组的提升。这里我们报告提升曲线下的面积或 Qini 分数——分数越高越好。

图片来源:作者
正如预期的那样,年费 x 30%处理具有最高的 Qini 分数。现在模型已经准备好,我们可以在新数据上应用它们。
预测样本外提升——设计下一次活动
我们转向 500 万名待续订的客户。我们可以选择提供年费 x 30%、年费 x 50%或年费 x 70%。或者什么都不提供——全额年费。通过三个 X-learner,我预测每个处理的 CATE。具有最大 CATE 的处理将是最佳处理。如果所有处理的 CATE 相似(相互之间在±10%以内),则我们选择年费 x 70%的处理(当然,我们希望获得更高的收入)。如果最大 CATE 为负数,则我们不对该客户进行营销(他们是沉睡的狗)。

图片来源:作者
这是我们最佳的分配方案。大约有 50 万名客户不建议接受处理。

图片来源:作者
在这种类型的表示(见下图)中,我们根据 CATE 将客户分为分位数。分位数 1 具有最高的 CATE,分位数 10 具有最低的 CATE。如果我们给所有客户一种处理方式,我们可以看到较低的分位数会更早地跌到 0 以下。因此,我们将坚持在下一次活动中使用最佳处理。

图片由作者提供
Qini 曲线告诉我们,我们期望从运行这个活动中获得相当大的提升。在曲线上没有明显的截止点或拐点来分离说服者。

图片由作者提供
增量响应——我们应该针对哪些客户?
下一次活动的平均提升预计为 0.052。那些提升高于平均水平的分位数是可以针对的客户。但为了节俭进行本次活动,我们将只选择前 20%,称之为说服者。那些提升为负的分位数是沉睡的狗。其余的要么是确定的事情,要么是失落的原因。
在这个重新设计的最佳处理图表中,更容易可视化说服者。在这种情况下,它们是前五分之一的分位数。

图片由作者提供
我们无法向业务团队报告提升,因此让我们将其转换为增量 ROI 和收入规模。对于分位数d,增量 ROI 为

收入是来自分位数的续订费用总额。活动成本是 Flex 自身承担的续订费用部分。我们看到,只有对前 7 个分位数或前 70%的客户提供折扣才是有利可图的。

图片由作者提供
前 20%,即说服者,预计将从这 500 万客户的续订中带来 80%的总收入。 这在商业中经常被观察到,他们称之为帕累托原理。这样的条形图也可以用来构建 CLV(客户生命周期价值),以了解活动的长期投资回报率。

图片由作者提供
所以为了回答这个问题——我们应该针对谁?就是说服者,大约有 100 万客户。我们如何个性化他们的优惠?我们使用具有最高条件平均处理效应的最佳处理。
结论
通过这种方式,提升建模识别出将为活动带来最多增量 ROI 的客户,并据此进行目标定位。通过这样做,提升建模优化了活动的投资回报率并减少了浪费开支。
我希望你发现这篇文章有用且信息丰富,并且你会在下一次的推广或营销活动中尝试提升建模。
提升模型通常应用于 D2C(直接面向消费者)活动,如直邮、电子邮件、短信或应用通知。如果你想优化付费媒体,请前往我关于 市场营销组合建模的系列文章。
感谢阅读!😄
成本优化中的提升建模
如何调整 CATE 以考虑与治疗相关的成本
·
关注 发表在 Towards Data Science ·13 分钟阅读·2023 年 3 月 10 日
--
照片由 Remy Gieling 提供,来源于 Unsplash
吸引顾客回到你的生意中是困难的。在竞争激烈的行业激励下,品牌们花费大量资金希望顾客再次光顾你的店铺。一种说服顾客回到公司中的方法是通过与他们互动,展示不同的广告以期望新的转化。
有时这些广告有效,有时客户需要更多的激励。这时,我们可以向客户提供折扣,以便他们在与品牌互动时感受到额外的价值,并放弃我们平等成本的广告方法。这些折扣的明显挑战是品牌在交易中会损失价值。这是我们将重点关注的问题:我们如何知道向客户发送哪个(如果有的话)折扣,以增加他们的转化概率?
一些读者可能注意到这个问题接近于典型的 Uplift 建模场景。我们对一些观察到的处理进行比较,选择具有最大条件平均处理效应(CATE)的处理。当我们没有同时观察到处理和对照时,我们估计反事实(即未发生的情况)。为了适应 Uplift 框架,我们可以将之前的问题重述为:我们如何调整 CATE 以考虑每种处理相关的成本?
这是一个数据科学家需要解决的问题。我们可以帮助经验丰富的广告商筛选广告和折扣的集合,并确定每个客户应看到的内容,基于先前观察到的互动数据。基于这些观察数据,我们可以帮助企业或品牌决定在所述情景中与客户互动的最佳策略。
我们从对 Uplift 建模和 Meta Learners 的快速介绍开始,学习它们各自的定义及如何解决平等成本问题。接着我们介绍净值 CATE,并展示我们需要对 Meta Learners 进行的细微修改以考虑成本。除了 Meta Learner 的调整,我们还将探讨 CausalML 中的一种解决方案——反事实价值估计器(CVE),以及这种方法如何解决净值问题。最后,我们将查看一些实验并讨论在生产环境中如何实际应用。
Uplift 建模的快速概览
为了确保我们在 Uplift 建模方面达成一致,我在这里提供了一个简要概述。Uplift 建模是因果推断下的一个框架,专注于确定最佳处理方案。与传统的统计学习技术相比,Uplift 建模的优势在于我们估计反事实效应,即未发生情景的结果。通过这种估计,我们可以预测未接受的处理的效果,从而回答“如果我们做了 X 会发生什么?”的问题。
这种治疗组和对照组之间差异的度量被称为条件平均治疗效应(CATE)。为了形式化这一概念,我将转向一些方程。我们将受试者的结果表示为 Y,将治疗表示为 t,将治疗标识符表示为 j,其中 j=0 是对照组。在我们的案例中,Y 是一个二元变量,指示用户是否转化。我们将差异条件化在一个包含受试者信息的列向量 x 上,其中最重要的可能是购买行为。使用这种符号,我们得到了下面的公式。
CATE 公式
现在我们有了一个期望的值,我们需要一种方法来估计这个值。在 Uplift Modelling 的背景下,最流行的方法是通过使用 Meta Learners。Meta Learners 利用我们熟悉的统计模型(即逻辑回归、线性回归、XGBoost 等),但将问题重新格式化,以学习解决 CATE 的方法。
从本质上讲,Meta Learners 试图学习每个治疗的伪效应,并将其学习围绕该估计进行。伪效应通过比较每个治疗的统计学习模型估计值与观察值和估计值之间的差异来学习。在 Meta Learner 工作流程结束时,我们输出每个治疗的 CATE。
对于本文来说,了解 CATE 是重要的,而学习如何估计 CATE 的方法则不那么重要。这种传统的提升建模解释来估计 CATE 非常快速,对于实际应用这种方法并不全面。我建议阅读这篇文章以获得良好的介绍,而对于因果推断相关的全面介绍,我建议阅读《勇敢而真实的因果推断》。
扩展 CATE 以捕捉净值
如上所示,我们的 CATE 值仅捕捉了转化概率。在这里,我们转变视角,考虑一个 CATE,用于考虑转化的总价值以及激活转化所用治疗的成本。
为了考虑价值,我们需要引入 Zhao 和 Harinen [1] 的一些新符号。我们引入 v 作为交易的预期值,s 作为转化成本(即激活时的治疗/折扣成本),以及 c 作为展示成本(即向消费者展示治疗/折扣的成本)。再次,t 代表一个治疗,j 代表具体的治疗,其中 j=0 是对照组的特殊情况。下面我们可以看到我们如何使用这些值来更新 CATE 公式,以考虑总净值。
净值 CATE 方程
这个公式为我们在考虑具有相关成本的治疗应用时提供了一些新的灵活性。我们可以看到,当我们比较净值 CATE 时,我们有考虑定价的治疗效果。
使用 X-Learner 考虑净值
基于我们上述的净值 CATE 概念,扩展 X-Learner 以处理这种新的 CATE 基础是相当简单的。在使用 X-Learner 时,一个主要步骤是学习给定预测和真实情况的治疗伪效应。为此,我们为每个治疗拟合一个响应模型(表示为 mu),并计算治疗组与对照组之间的差异。对于我们没有的值(反事实),我们使用训练好的响应模型,通过数据集中每个个体的一组特征进行估计。标准伪效应如下所示:
没有净值的伪效应。
如上所述,我们希望在伪效应中捕捉净值。我们可以使用赵和哈里宁[1]提出的方法,以我们对 CATE 所做的相同修改来实现这一点。如果你注意到,伪效应是 CATE 的估计,即给定关于受试者的一组特征,治疗效果比其他治疗好多少。通过对期望值做出相同的修改,我们可以将净值伪效应重新整理为:
带有净值的伪效应。
通过 CATE 建模净值的一个良好结果是,这也是我们对 X-Learner 所需的唯一调整。由于 CATE 已经是一个连续变量,因此需要训练的后续模型可以处理回归任务。
使用反事实价值估计器考虑净值
还有一些其他方法可以在比较中考虑净值。我们在这里考虑的一种方法是由 Uber 维护的 Python 库 CausalML 中实现的解决方案。他们的解决方案,即反事实价值估计器(CVE),在计算净值 CATE 的相同理念上进行了创新,但在计算发生的位置上采取了略微不同的方法。
CVE 是一个后建模优化器,它从几个模型中获取输入,以估计净值 CATE。第一个使用的模型是转化概率模型。该模型用于预测受试者在给定其特征和接受的治疗情况下的转化概率。
下一个为 CVE 训练的模型是任何能够预测 CATE 的学习器。CATE 与转化概率相结合,以确定在治疗场景下相对于反事实的收敛概率。这种计算如下所示:
CVE 中的 CATE 方程。方程取自[2]。
针对 CVE 训练的下一个模型是预期转化价值预测器。根据场景,这个模型可能不是必需的。如果你可以轻松地用历史支出代替用户将要支出的金额,那么这是一个可行的选项。然而,如果你有关于用户如何与品牌互动或者他们在下一个交易中可能花费多少的信息,那么你可以通过回归问题来建模这一点。
到此为止,我们已经拥有了所有预测值,我们需要使用上述描述的净值 CATE 来优化哪个治疗可能会带来最大的净值回报。有关这种方法的更多信息,你可以查看[2]中提供的信息。稍后在文章中,我们将通过代码进一步探讨这个概念。
示例
在这里,我们将通过一个示例来说明如何应用到目前为止讨论的方法。我们将使用一些来自 CausalML 的辅助函数,并且还会调整他们的一个笔记本来进行示例演示。我们还将按他们在笔记本中所做的那样进行评估。要查看他们的演示,请访问这个链接。
我们将使用的度量标准是潜在收益启发式方法,用于评估如果我们在之前的数据批次中采用这种治疗分配策略,我们的平均收益会是多少。在保留数据中,我们匹配所有治疗等于观察到的治疗的情况。当这些相等时,我们计算这些个体的平均值。为了进一步说明,我在下面写了一些假设性的 SQL 代码,展示了如何计算这个值,列名对应于我们在整篇文章中讨论的变量。
SELECT AVG((expected_value - conversion_cost) * conversion - impression_cost)
FROM preds_and_ground_truth
WHERE predicted_treatment = ground_truth_treatment;
首先,我们需要创建我们的数据。在这个示例中,我们将使用两种治疗和一个对照组。我设置了 positive_class_proportion=0.1,这代表了 10% 的转化率。这个数字可能会根据你的情况有所不同,所以如果你在模拟,请确保相应地选择这个值。
df, X_names = make_uplift_classification(
n_samples=5000,
treatment_name=["control", "treatment1", "treatment2"],
positive_class_proportion=0.1,
)
接下来我们要做的是创建与成本相关的函数。第一个函数是期望值,我将其创建为一个无关特征的函数。这个特征对转化没有影响,因此它测试我们在进行优化时计算预期支出的能力。
df['expected_value'] = np.abs(df['x6_irrelevant']) * 20 + np.random.normal(0, 5)
现在我们将使用来自 CausalML 的辅助函数创建所有的成本信息。我们将创建我们的转化成本数组 cc_array、展示成本数组 ic_array,并获取条件(我们的治疗)。转化价值数组就是我们上面创建的期望值。
# Put costs into dicts
conversion_cost_dict = {"control": 0, "treatment1": 2.5, "treatment2": 10}
impression_cost_dict = {"control": 0, "treatment1": 0, "treatment2": 0.02}
# Use a helper function to put treatment costs to array
cc_array, ic_array, conditions = get_treatment_costs(
treatment=df["treatment_group_key"],
control_name="control",
cc_dict=conversion_cost_dict,
ic_dict=impression_cost_dict,
)
# Put the conversion value into an array
conversion_value_array = df['expected_value'].to_numpy()
接下来我们可以创建实际的价值数组。这是按照上述期望的相同公式计算的交易价值。
actual_value = get_actual_value(
treatment=df["treatment_group_key"],
observed_outcome=df["conversion"],
conversion_value=conversion_value_array,
conditions=conditions,
conversion_cost=cc_array,
impression_cost=ic_array,
)
随机策略
我们首先要查看的策略是将治疗随机分配给不同的对象。这可能看起来像这样:
test_actual_value = actual_value.loc[test_idx]
random_treatments = pd.Series(
np.random.choice(conditions, test_idx.shape[0]), index=test_idx
)
test_treatments = df.loc[test_idx, "treatment_group_key"]
random_allocation_value = test_actual_value[test_treatments == random_treatments]
最佳治疗策略
下一个策略是采取具有最高平均处理效果(ATE)的处理。这完全不考虑受试者的背景。
best_ate = df_train.groupby("treatment_group_key")["conversion"].mean().idxmax()
actual_is_best_ate = df_test["treatment_group_key"] == best_ate
best_ate_value = actual_value.loc[test_idx][actual_is_best_ate]
最佳可能
最佳可能政策是一个我们可以用来评估模型比较的标准。这个模型只考虑那些我们没有损失价值的情况。当受试者在对照组中或在我们提供的两个处理方案中有一个发生了转化时,就是这种情况。
test_value = actual_value.loc[test_idx]
best_value = test_value[test_value >= 0]
X Learner
在这里,我们将使用一个没有成本优化的普通 X Learner。我在这里使用的 X Learner 是我自己实现的,如果你想试验一下,我在下面的链接中包括了我的仓库链接。
xm = XLearner()
encoder = {"control": 0, "treatment1": 1, "treatment2": 2}
X = df.loc[train_idx, X_names].to_numpy()
y = df.loc[train_idx, "conversion"].to_numpy()
T = np.array([encoder[x] for x in df.loc[train_idx, "treatment_group_key"]])
xm.fit(X, y, T)
要根据 X-Learner 获得最佳处理,我们可以获取预测的 CATE 值,并通过对数据框进行 argmax 操作来选择最大值的处理。
X_test = df.loc[test_idx, X_names].to_numpy()
xm_pred = xm.predict(X_test).drop(0, axis=1)
xm_best = xm_pred.idxmax(axis=1)
xm_best = [conditions[idx] for idx in xm_best]
actual_is_xm_best = df_test["treatment_group_key"] == xm_best
xm_value = actual_value.loc[test_idx][actual_is_xm_best]
反事实价值估计器
要使用 CausalML 的 CVE,我们需要首先训练几个模型。第一个模型是转化分类器。这只是一个简单的分类问题。我们使用分类器来预测在给定处理暴露和我们可能知道的其他信息的情况下转化的概率。
proba_model = lgb.LGBMClassifier()
W_dummies = pd.get_dummies(df["treatment_group_key"])
XW = np.c_[df[X_names], W_dummies]
proba_model.fit(XW[train_idx], df_train["conversion"])
y_proba = proba_model.predict_proba(XW[test_idx])[:, 1]
接下来我们需要训练的模型是预测客人转化期望值的模型。这是另一个简单的问题,这次是回归问题。
expected_value_model = lgb.LGBMRegressor()
expected_value_model.fit(XW[train_idx], df_train['expected_value'])
pred_conv_value = expected_value_model.predict(XW[test_idx])
我们为这个模型使用的另一个值是预测的 CATE 值。在前一步中我们拟合了一个 X-Learner,它为我们预测了 CATE。现在我们可以使用 CVE 来优化我们的行动。
cve = CounterfactualValueEstimator(
treatment=df_test["treatment_group_key"],
control_name="control",
treatment_names=conditions[1:], # idx 0 is control
y_proba=y_proba,
cate=xm_pred,
value=pred_conv_value,
conversion_cost=cc_array[test_idx],
impression_cost=ic_array[test_idx],
)
CVE 是一个非参数优化器。这意味着我们在使用 CVE 时不会学习任何权重。相反,我们利用已经学到的值并在预测行动时优化它们以应对外部成本。下面是如何从 CVE 获得最佳行动的示例。
cve_best_idx = cve.predict_best()
cve_best = [conditions[idx] for idx in cve_best_idx]
actual_is_cve_best = df.loc[test_idx, "treatment_group_key"] == cve_best
cve_value = actual_value.loc[test_idx][actual_is_cve_best]
净值优化 X-Learner
接下来我们将再次查看 X-Learner,但这次考虑的是净值 CATE 而不是普通的 CATE。这是之前提到的那个 X-Learner,来自我的仓库。如果你想试验一下,请查看下面链接的仓库。
nvex = XLearner(ic_lookup=ic_lookup, cc_lookup=cc_lookup)
X = df.loc[train_idx, X_names].to_numpy()
y = df.loc[train_idx, "conversion"].to_numpy()
T = np.array([encoder[x] for x in df.loc[train_idx, "treatment_group_key"]])
value = df.loc[train_idx, "expected_value"].to_numpy()
nvex.fit(X, y, T, value)
结果比较
下面我们可以看到每个策略在测试集上的平均值结果。正如我们预期的那样,优化了处理分布中的值的方法优于那些没有考虑值的方法。随机分配和最佳处理作为良好的基准测量,但没有提供足够的性能,使其成为具有竞争力的简单方法。X-Learner 比简单方法有了很大改善,但表现不如考虑净值的方法。最佳表现来自于净值优化(NVO)X-Learner 和 CVE。这是因为这些方法针对的是净值,这是我们衡量它们的标准。

测试集每个受试者的平均值。
要在正式的活动中衡量成功,我建议采用稍微复杂一点的方法,遵循回测范式。对于那些不熟悉的人,回测涉及在历史数据上测试算法,使用截止日期上的持出集。假设你有 90 天的数据。对策略/算法进行回测审查将包括在前 45 天进行训练,并在接下来的 45 天进行测试,然后通过一些固定的天数增量来增加训练集,并重复训练和验证问题。在这里,我们可以使用相同的方法进行测试,看看算法在历史增量上的表现。
数据收集和验证的考虑
在执行这样的活动时,你的模型只有在你的数据质量上才会有用。随机数据很昂贵,可能不适合你全部的数据,但拥有一些随机数据还是很重要的。在分配处理时,确保收集一些随机分配的子样本。这些数据最好用于验证目的,以确保你训练的算法没有学习到由于处理分配不平衡而产生的趋势。
对于那些担心在观察数据上训练的人,有几种自然的方法可以考虑。在 X-Learner 中,我们学习一个用于处理分配的倾向模型。当考虑处理效果时,我们学习了个体被分配到该处理组的可能性加权平均值。欲了解更多信息,我建议查看[1]中的公式 (10)、(11) 和 (12)。
观察数据的不平衡也可以在转换模型和回归模型中加以考虑。通过测量转换和预期价值模型中处理组的准确性,我们可以确保数据不会偏向任何一组。如果结果确实出现偏差,可以使用许多采样技术来解决这个问题(这是一个简单的解决方法的好例子)。
结论
在这里,我们介绍了在分配处理时如何优化价值。我们讨论了如何处理这个问题,包括对 Uplift Modelling 的快速介绍,并介绍了 ATE 和 CATE。然后,我们修改了 CATE 的计算方法,将价值、转换成本和印象成本包含在期望值中,以学习净值 CATE。接下来,我们查看了 CausalML 提供的现有解决方案,即 Counterfactual Value Estimator,并了解了它如何考虑净值 CATE。最后,我们通过 CausalML 的笔记本,并与我们的净值优化 X-Learner 进行了扩展。
你可以在这里访问我的仓库 这里!
你可以从 CausalML 访问原始笔记本 这里!
除非另有说明,所有图片均属于作者。
[1] Zhao, Z., & Harinen, T.. (2019). 多重处理的提升建模与成本优化。
[2] Huigang Chen, Totte Harinen, Jeong-Yoon Lee, Mike Yung, & Zhenyu Zhao. (2020). CausalML: 用于因果机器学习的 Python 包。
城市可达性 — 如何及时到达除颤器
原文:
towardsdatascience.com/urban-accessibility-how-to-reach-defibrillators-on-time-c865d9194448

作者提供的图片。
在这篇文章中,我将早期的城市可达性或步行性研究与有关公共除颤器设备位置的开源数据结合起来。此外,我还结合了全球人口数据和 Uber 的 H3 网格系统,以估算布达佩斯和维也纳范围内的人口中可以合理到达任何设备的比例。
·发布于 走向数据科学 ·11 分钟阅读·2023 年 10 月 1 日
--
城市可达性的根源,或称为步行性,在于一种基于图的计算方法,该方法测量欧几里得距离(将其转化为步行分钟,假设速度恒定且没有交通堵塞和障碍物)。这种分析的结果可以告诉我们,从城市内的每一个位置到达特定类型设施的难易程度。更准确地说,从城市道路网络中的每一个节点出发,但由于大量的道路交叉,这种近似通常是可以忽略的。
在当前的案例研究中,我专注于一种特定类型的兴趣点(POI):除颤器设备的位置。虽然奥地利政府的开放数据门户分享了有关这一点的官方记录,但在匈牙利,我只能获得一个覆盖率不到一半的众包数据集——希望未来该数据集在绝对规模和数据覆盖范围上都能增长。
在我的文章的第一部分,我将为每个城市创建可达性地图,展示在 15km/h 的跑步速度下,达到最近的除颤器单元所需的时间,范围为 2.5 公里。然后,我将使用 Uber 的 H3 库将城市划分为六边形网格,以计算每个网格单元的平均除颤器可达时间。我还根据我之前的文章估算每个六边形单元的人口水平。最后,我将这些数据结合起来,计算作为可达性(跑步)时间函数的人口比例。
作为免责声明,我想强调我不是经过专业培训的医学专家——我也不打算对除颤器设备相较于其他生命支持手段的重要性表态。然而,基于常识和城市规划原则,我假设获取这些设备越容易越好。
1. 数据来源
一如既往,我喜欢从探索我使用的数据类型开始。首先,我将收集我研究城市的行政边界——匈牙利布达佩斯和奥地利维也纳。
然后,基于我之前关于如何处理栅格化人口数据的文章,我从 WorldPop 中心添加了城市级别的人口信息。最后,我结合了维也纳官方政府数据和我自己通过网络抓取的相同数据,尽管数据来源繁杂且固有不完整,适用于布达佩斯。
1.1. 行政边界
首先,我使用OpenStreetMap和OSMNx库查询布达佩斯和维也纳的行政边界:
import osmnx as ox # version: 1.0.1
import matplotlib.pyplot as plt # version: 3.7.1
admin = {}
cities = ['Budapest', 'Vienna']
f, ax = plt.subplots(1,2, figsize = (15,5))
# visualize the admin boundaries
for idx, city in enumerate(cities):
admin[city] = ox.geocode_to_gdf(city)
admin[city].plot(ax=ax[idx],color='none',edgecolor= 'k', linewidth = 2) ax[idx].set_title(city, fontsize = 16)
这段代码块的结果:

图 1. 布达佩斯和维也纳的行政边界。图片由作者提供。
1.2. 人口数据
其次,按照这篇文章中的步骤,我为这两个城市创建了矢量数据格式的人口网格,基于 WorldPop 在线人口数据库。为了避免重复步骤,我直接读取了包含这些城市人口信息的过程输出文件。
此外,为了让效果更加美观,我使用 Matplotlib 和 ChatGPT 的快速脚本创建了 2022 年颜色“Very Peri”的色彩图。
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap
very_peri = '#8C6BF3'
second_color = '#6BAB55'
colors = [second_color, very_peri ]
n_bins = 100
cmap_name = "VeryPeri"
colormap = LinearSegmentedColormap.from_list(cmap_name, colors, N=n_bins)
import geopandas as gpd # version: 0.9.0
demographics = {}
f, ax = plt.subplots(1,2, figsize = (15,5))
for idx, city in enumerate(cities):
demographics[city] = gpd.read_file(city.lower() + \
'_population_grid.geojson')[['population', 'geometry']]
admin[city].plot(ax=ax[idx], color = 'none', edgecolor = 'k', \
linewidth = 3)
demographics[city].plot(column = 'population', cmap = colormap, \
ax=ax[idx], alpha = 0.9, markersize = 0.25)
ax[idx].set_title(city)
ax[idx].set_title('Population density\n in ' + city, fontsize = 16)
ax[idx].axis('off')
这段代码块的结果:

图 2. 基于 WordPop 数据的人口密度地图。图片由作者提供。
1.3. 除颤器位置
第三,我收集了两个城市中可用除颤器的位置信息。
对于维也纳,我从奥地利政府官方开放数据门户下载了包含 1044 个单元点位置的数据集:
尽管布达佩斯/匈牙利没有这样的官方开放数据门户,但匈牙利国家心脏基金会运营一个众包网站,运营商可以更新他们的除颤器位置。他们的全国数据库包含 677 个单位;然而,他们的免责声明称他们知道至少有一千个单位在该国运营——并等待其所有者上传。通过一个简单的网络爬虫,我下载了每个 677 个注册单位的位置,并将数据集筛选到布达佩斯,结果得到 148 个单位的数据集。
# parse the data for each city
gdf_units= {}
gdf_units['Vienna'] = gpd.read_file('DEFIBRILLATOROGD')
gdf_units['Budapest'] = gpd.read_file('budapest_defibrillator.geojson')
for city in cities:
gdf_units[city] = gpd.overlay(gdf_units[city], admin[city])
# visualize the units
f, ax = plt.subplots(1,2, figsize = (15,5))
for idx, city in enumerate(cities):
admin[city].plot(ax=ax[idx],color='none',edgecolor= 'k', linewidth = 3)
gdf_units[city].plot( ax=ax[idx], alpha = 0.9, color = very_peri, \
markersize = 6.0)
ax[idx].set_title('Locations of defibrillator\ndevices in ' + city, \
fontsize = 16)
ax[idx].axis('off')
这个代码块的结果:

图 3. 布达佩斯和维也纳的除颤器位置。图片由作者提供。
2. 可达性计算
接下来,我总结了 Nick Jones 于 2018 年撰写的这篇出色的文章,讲述如何计算步行可达性:
import os
import pandana # version: 0.6
import pandas as pd # version: 1.4.2
import numpy as np # version: 1.22.4
from shapely.geometry import Point # version: 1.7.1
from pandana.loaders import osm
def get_city_accessibility(admin, POIs):
# walkability parameters
walkingspeed_kmh = 15
walkingspeed_mm = walkingspeed_kmh * 1000 / 60
distance = 2500
# bounding box as a list of llcrnrlat, llcrnrlng, urcrnrlat, urcrnrlng
minx, miny, maxx, maxy = admin.bounds.T[0].to_list()
bbox = [miny, minx, maxy, maxx]
# setting the input params, going for the nearest POI
num_pois = 1
num_categories = 1
bbox_string = '_'.join([str(x) for x in bbox])
net_filename = 'data/network_{}.h5'.format(bbox_string)
if not os.path.exists('data'): os.makedirs('data')
# precomputing nework distances
if os.path.isfile(net_filename):
# if a street network file already exists, just load the dataset from that
network = pandana.network.Network.from_hdf5(net_filename)
method = 'loaded from HDF5'
else:
# otherwise, query the OSM API for the street network within the specified bounding box
network = osm.pdna_network_from_bbox(bbox[0], bbox[1], bbox[2], bbox[3])
method = 'downloaded from OSM'
# identify nodes that are connected to fewer than some threshold of other nodes within a given distance
lcn = network.low_connectivity_nodes(impedance=1000, count=10, imp_name='distance')
network.save_hdf5(net_filename, rm_nodes=lcn) #remove low-connectivity nodes and save to h5
# precomputes the range queries (the reachable nodes within this maximum distance)
# so, as long as you use a smaller distance, cached results will be used
network.precompute(distance + 1)
# compute accessibilities on POIs
pois = POIs.copy()
pois['lon'] = pois.geometry.apply(lambda g: g.x)
pois['lat'] = pois.geometry.apply(lambda g: g.y)
pois = pois.drop(columns = ['geometry'])
network.init_pois(num_categories=num_categories, max_dist=distance, max_pois=num_pois)
network.set_pois(category='all', x_col=pois['lon'], y_col=pois['lat'])
# searches for the n nearest amenities (of all types) to each node in the network
all_access = network.nearest_pois(distance=distance, category='all', num_pois=num_pois)
# transform the results into a geodataframe
nodes = network.nodes_df
nodes_acc = nodes.merge(all_access[[1]], left_index = True, right_index = True).rename(columns = {1 : 'distance'})
nodes_acc['time'] = nodes_acc.distance / walkingspeed_mm
xs = list(nodes_acc.x)
ys = list(nodes_acc.y)
nodes_acc['geometry'] = [Point(xs[i], ys[i]) for i in range(len(xs))]
nodes_acc = gpd.GeoDataFrame(nodes_acc)
nodes_acc = gpd.overlay(nodes_acc, admin)
nodes_acc[['time', 'geometry']].to_file(city + '_accessibility.geojson', driver = 'GeoJSON')
return nodes_acc[['time', 'geometry']]
accessibilities = {}
for city in cities:
accessibilities[city] = get_city_accessibility(admin[city], gdf_units[city])
for city in cities:
print('Number of road network nodes in ' + \
city + ': ' + str(len(accessibilities[city])))
这个代码块输出布达佩斯(116,056)和维也纳(148,212)的道路网络节点数量。
现在可视化可达性地图:
for city in cities:
f, ax = plt.subplots(1,1,figsize=(15,8))
admin[city].plot(ax=ax, color = 'k', edgecolor = 'k', linewidth = 3)
accessibilities[city].plot(column = 'time', cmap = 'RdYlGn_r', \
legend = True, ax = ax, markersize = 2, alpha = 0.5)
ax.set_title('Defibrillator accessibility in minutes\n' + city, \
pad = 40, fontsize = 24)
ax.axis('off')
这个代码块输出以下图形:

图 4. 布达佩斯的除颤器可达性(分钟)。图片由作者提供。

图 5. 维也纳的除颤器可达性(分钟)。图片由作者提供。
3. 映射到 H3 网格单元
目前,我已经有了人口和可达性数据;我只需要将它们结合起来。唯一的难点是它们的空间单位不同:
-
可达性被测量并附加到每个城市道路网络中的节点上
-
人口数据来源于光栅网格,现在由每个光栅网格的质心的 POI 描述
尽管重新处理原始光栅网格可能是一个选项,出于更明显的普遍性(并添加一点我的个人风格),我现在将这两种点数据集映射到Uber 的 H3 网格系统,对于那些尚未使用过它的人,现在知道它是一个优雅、高效的空间索引系统,使用六边形瓷砖即可。更多阅读,请点击此链接!
3.1. 创建 H3 单元
首先,编写一个函数,将城市分割成任意分辨率的六边形:
import geopandas as gpd
import h3 # version: 3.7.3
from shapely.geometry import Polygon # version: 1.7.1
import numpy as np
def split_admin_boundary_to_hexagons(admin_gdf, resolution):
coords = list(admin_gdf.geometry.to_list()[0].exterior.coords)
admin_geojson = {"type": "Polygon", "coordinates": [coords]}
hexagons = h3.polyfill(admin_geojson, resolution, \
geo_json_conformant=True)
hexagon_geometries = {hex_id : Polygon(h3.h3_to_geo_boundary(hex_id, \
geo_json=True)) for hex_id in hexagons}
return gpd.GeoDataFrame(hexagon_geometries.items(), columns = ['hex_id', 'geometry'])
resolution = 8
hexagons_gdf = split_admin_boundary_to_hexagons(admin[city], resolution)
hexagons_gdf.plot()
这个代码块的结果:

图 6. 维也纳 H3 六边形分割,分辨率为 8。图片由作者提供。
现在,查看几个不同的分辨率:
for resolution in [7,8,9]:
admin_h3 = {}
for city in cities:
admin_h3[city] = split_admin_boundary_to_hexagons(admin[city], resolution)
f, ax = plt.subplots(1,2, figsize = (15,5))
for idx, city in enumerate(cities):
admin[city].plot(ax=ax[idx], color = 'none', edgecolor = 'k', \
linewidth = 3)
admin_h3[city].plot( ax=ax[idx], alpha = 0.8, edgecolor = 'k', \
color = 'none')
ax[idx].set_title(city + ' (resolution = '+str(resolution)+')', \
fontsize = 14)
ax[idx].axis('off')
这个代码块的结果:

图 7. 不同分辨率下布达佩斯和维也纳的 H3 六边形分割。图片由作者提供。
让我们保持分辨率 9!
3.2. 将值映射到 h3 单元
现在,我已经将我们的两个城市放在一个六边形网格格式中。接下来,我将根据每个点几何体落入的网格单元,将人口和可达性数据映射到六边形单元格中。为此,GeoPandasa 的 sjoin 函数做了很好的空间连接,是一个不错的选择。
此外,由于每个城市中有超过 100k 个道路网络节点和数千个人口网格重心,因此每个六边形网格单元中很可能会映射多个 POI。因此,需要进行聚合。由于人口是一个累加量,我将通过将其相加来聚合同一六边形内的人口水平。然而,可达性不是广泛的,所以我将计算每个瓷砖的平均除颤器可达性时间。
demographics_h3 = {}
accessibility_h3 = {}
for city in cities:
# do the spatial join, aggregate on the population level of each \
# hexagon, and then map these population values to the grid ids
demographics_dict = gpd.sjoin(admin_h3[city], demographics[city]).groupby(by = 'hex_id').sum('population').to_dict()['population']
demographics_h3[city] = admin_h3[city].copy()
demographics_h3[city]['population'] = demographics_h3[city].hex_id.map(demographics_dict)
# do the spatial join, aggregate on the population level by averaging
# accessiblity times within each hexagon, and then map these time score # to the grid ids
accessibility_dict = gpd.sjoin(admin_h3[city], accessibilities[city]).groupby(by = 'hex_id').mean('time').to_dict()['time']
accessibility_h3[city] = admin_h3[city].copy()
accessibility_h3[city]['time'] = \
accessibility_h3[city].hex_id.map(accessibility_dict)
# now show the results
f, ax = plt.subplots(2,1,figsize = (15,15))
demographics_h3[city].plot(column = 'population', legend = True, \
cmap = colormap, ax=ax[0], alpha = 0.9, markersize = 0.25)
accessibility_h3[city].plot(column = 'time', cmap = 'RdYlGn_r', \
legend = True, ax = ax[1])
ax[0].set_title('Population level\n in ' + city, fontsize = 16)
ax[1].set_title('Defibrillator reachability time\n in ' + city, \
fontsize = 16)
for ax_i in ax: ax_i.axis('off')
这个代码块的结果是以下图形:

图 8. 布达佩斯的城市特征。图片由作者提供。

图 9. 维也纳的城市特征。图片由作者提供。
4. 计算人口可达性
在这最后一步,我将估计在一定时间内从最近的除颤器单元可达的人口比例。在这里,我仍然基于相对快速的 15 公里/小时跑步速度和 2.5 公里距离限制。
从技术角度来看,我合并了 H3 级别的人口和可达性时间数据框,然后对时间维度进行简单的阈值处理,对人口维度进行求和。
f, ax = plt.subplots(1,2, figsize = (15,5))
for idx, city in enumerate(cities):
total_pop = demographics_h3[city].population.sum()
merged = demographics_h3[city].merge(accessibility_h3[city].drop(columns =\
['geometry']), left_on = 'hex_id', right_on = 'hex_id')
time_thresholds = range(10)
population_reached = [100*merged[merged.time<limit].population.sum()/total_pop for limit in time_thresholds]
ax[idx].plot(time_thresholds, population_reached, linewidth = 3, \
color = very_peri)
ax[idx].set_xlabel('Reachability time (min)', fontsize = 14, \
labelpad = 12)
ax[idx].set_ylabel('Fraction of population reached (%)', fontsize = 14, labelpad = 12)
ax[idx].set_xlim([0,10])
ax[idx].set_ylim([0,100])
ax[idx].set_title('Fraction of population vs defibrillator\naccessibility in ' + city, pad = 20, fontsize = 16)
这个代码块的结果是以下图形:

图 10. 以奔跑速度从最近的除颤器单元可到达的人口比例。图片由作者提供。
5. 结论
在解释这些结果时,我想强调,一方面,除颤器可达性可能与心脏病发作的生存率没有直接关系;评估这种影响超出了我的专业范围和这个项目的范围。此外,用于布达佩斯的数据明显不完整且来源拥挤,而非官方的奥地利数据源。
在免责声明之后,我们看到了什么?一方面,我们看到在布达佩斯,约 75–80% 的人口可以在 10 分钟内到达一个设备,而在维也纳,我们已经能在 6–7 分钟内几乎实现完全覆盖。此外,我们需要仔细阅读这些时间值:如果我们遇到不幸事件,需要在每一分钟可能都是生死攸关的情况下,到达设备,取出它,返回(使旅行时间是可达时间的两倍),安装等。
从开发的角度来看,结论是确保我们拥有完整的数据,然后使用可达性和人口图,将它们结合起来,进行分析,并在部署新设备和新位置时构建,以最大化有效覆盖的人口。
城市韧性:空间公平
使用空间数据科学对人口进行建模 + 分析地拉那的教育公平。
·
关注 发表在 Towards Data Science ·5 min read·2023 年 5 月 30 日
--
图片由 Gledisa Golikja 提供,来源于 Unsplash
你好!
这是城市韧性项目的第二部分 (第一部分在这里),专注于地拉那的人口趋势!在第一部分中,我们研究了幂律分布,并建立了空间马尔可夫模型以理解人口随时间的变化。在第二部分中,我想更深入地探讨这些预测,并了解它们对地拉那特定社区的意义。让我们开始吧!
空间马尔可夫模型(续)
上一次,我使用了地拉那开放数据的统计信息(数据 许可: 创作共用署名)来获得这个空间马尔可夫模型矩阵:

空间马尔可夫矩阵结果(图片由作者提供)
让我们来看看这些结果在特定社区中的含义。截至 2021 年,城市中人口最多的区域是 5 区、2 区、7 区、4 区和 11 区,紧随其后的是卡沙尔,这是一座位于地拉那市区之外的市镇,拥有许多新开发项目。这里是一个快速的可视化:
卡沙尔是一个有趣的郊区增长例子,像可口可乐、沃达丰、Top Channel 以及一些小型企业都在这里设立了店铺。2009 年,其人口仅为 20829,但截至 2021 年,已几乎增长到 58664 人。这些增长非常迅速的地区也正是最需要可持续解决方案的地方:卡沙尔每天增长约 11 人,且中位年龄为 33 岁(来源)。
其他高人口地区在过去 12 年中也经历了增长:
有趣的是,这些区域彼此相邻:这加强了这样一种直觉,即周围社区发生的趋势可能也会影响到该社区的特征。
一些例子
让我们稍微关注一下 5 区。其直接邻近的区域是 7 区、10 区和 2 区,人口分别为 77124、27637 和 83827。根据空间马尔可夫结果,考虑到这些邻近区域,5 区有约90%的机会保持在最高人口区间。它也有约5%的机会降到第一个和第二个人口区间。
10 区是地拉那的另一个社区,涵盖了城市广场、商业区(Blloku/The Block)以及一些最繁忙的街道。2021 年其人口为 27637,其邻近区域的人口在 77000 到 87000 之间。根据马尔可夫结果,它有约93%的机会保持在当前的人口区间。
第#2 步:学校数据与空间公平
在可持续发展方面,城市应该致力于为所有社区中的居民提供高质量的资源。地理资源可用性的概念也称为空间公平:在一个致力于为所有公民提供类似机会的城市中,这意味着人们将平等地获得公共空间、清洁环境和如学校等机构的访问机会。
在这个背景下,我想探讨学校的分布作为空间公平性的一个标志。地拉那的所有儿童是否都能接受到可及的、高质量的教育?是否存在劣势区域?有哪些学校的趋势和模式?为此,我将使用地拉那中小学(统称为“9-vjecare”)的数据(链接,该数据拥有创作共用署名许可)。以下是地拉那各行政区学校密度的可视化图:

地拉那各区域的学校密度(图片由作者提供)
这是相同的可视化图,但只关注于 11 个城市区域:

地拉那 11 个城市区域的学校密度(图片由作者提供)
一眼看去,似乎密度最高的区域实际上是那些位于 11 个主要行政区之外的地方。也就是说,像 Shengjergj、Zall Bastar 和 Peze 这样的地方成为了前 3 名。这对那些就读于这些学校的孩子意味着什么?这是否意味着他们上学时的安全性或可靠性更高?
这里是从 Kashar 的一所学校“Sadik Stavileci”出发的步行网络可视化。该图展示了如果以每小时 4.5 公里的速度步行,能在 5、10 或 15 分钟内从学校到达的范围。

从 Kashar 学校步行距离的等时线图(图片由作者提供)
如你所见,孩子们在几分钟内能够覆盖的距离可能并不大。然而,这个工具在规划建筑项目时很有用,以确保一个地方对预期使用者易于到达。步行到学校和从学校回家的合理时间是多少?我们如何改善交通或骑行等服务,以确保孩子们能够安全上学?作为一个起点,计算地拉那所有学校的等时线并与步行距离内的孩子数量进行比较将是很有趣的。
侧边栏:我使用了 OSMnx,这是一款结合了 OpenStreetMaps 数据和网络指标的网络分析包来制作这些图表。这里是进行这个操作的源笔记本(isochrones)。
衡量不平等:空间自相关
为了衡量空间分布中的不平等,我们可以使用一些其他指标。空间自相关就是其中之一,它包括计算 Moran’s I(我们在第一部分中为人口统计量做了这个)。这是为了检验学校在地拉那是否均匀分布的原假设。测试的结果是0.186(p 值为 0.111)。
PySAL 还为我们提供了两种可视化自相关的方法:Moran 图和Moran’s I在原假设下的分布。

Moran 图 + 实证分布(图像来源:作者)
Moran 图显示了学校数量与滞后学校数量(通过将学校数量与空间权重矩阵相乘得到)之间的关系。定性分析中,我们将图表解释为当数据点表现出高相关性时,显示正的空间自相关。另一方面,分布是实证的:它是通过模拟一系列随机分布学校数量的地图,然后计算每张地图的 Moran’s I 得到的。(蓝线:分布的均值,红线:在地拉那数据中观察到的统计值)
📔 结论 + 笔记本
这就结束了该项目的第二部分!总体而言,我认为使用空间数据科学工具是相对未被探索的,特别是在阿尔巴尼亚的背景下,但绝对非常有用。这个项目可以通过更多细化的数据(如学校示例)来丰富。在此之前,这里是更新后的 笔记本。
感谢阅读!
城市韧性:地震韧性,案例研究 [第一部分]
量化地震韧性:幂律分布、自组织城市和可持续人口动态
·
关注 发布于Towards Data Science ·6 分钟阅读·2023 年 4 月 17 日
--
图片由Michael Silkesjöö拍摄,来源于Unsplash
你好,欢迎!
设置和简介
我一直在思考城市规划中的各种问题,其中一个引起我兴趣的主要问题是:“是什么让一个城市具有韧性?” 为什么有些城市能够应对并克服各种挑战,而另一些城市却挣扎得更多?或者,为什么城市中的不同区域(例如街区)在韧性方面存在超局部差异?这个多部分的故事探讨了以阿尔巴尼亚首都地拉那为背景的城市韧性。
对这个问题已经有很多候选答案:CDC 使用社会脆弱性指数(SVI)来衡量一个普查区对挑战的脆弱性,包括人为和自然挑战。SVI 包括如贫困、缺乏交通工具和拥挤的住房等因素,以生成该区域的最终排名。这个指数可以用来解决诸如“决定需要多少紧急救援人员”以及“识别需要紧急避难所的区域”等问题。
在这个故事中,我想的不仅仅关注韧性的“灾害管理”方面。相反,我们来看 OECD 的定义:韧性城市是指具有吸收、恢复和为未来冲击(经济、环境、社会和制度)做好准备能力的城市。 对于地拉那来说,特别重要的是在了解人口变化和自 1990 年代以来的快速城市化背景下的韧性。快速城市化会给城市的土地和服务资源带来很大压力,韧性城市确保居民能够公平地获得这些资源。这些动态在过去 30 年中是如何发展的?
在这个故事中,我将使用来自地拉那市开放数据门户的人口统计数据(数据+许可:Creative Commons Attribution)以及PySAL用于空间分析,seaborn和pandas用于数据可视化。让我们开始吧!
幂律
城市规划者通过观察城市形态来尝试捕捉韧性城市的概念。一个主要的例子是尺度层级或幂律。幂律是 x 和 y 之间的一种关系,可以用以下公式和形状建模:

幂律分布(作者图片)
结果表明,很多关系可以用幂律来描述:姓氏的频率(一些姓氏非常流行,因此出现的频率非常高)、能源消耗(一些地理区域消耗了大部分能源)。尤其在城市规划中,规模等级/幂律出现在地铁站分布、住宅密度甚至职位数量中。
数据可视化
我们来看看地拉那在 2009 年、2015 年和 2021 年这 3 年的数据中的人口分布:

人口计数与排名的行政区域(作者提供的图片)
这些计数似乎略微偏离幂律,至少在定性上是这样。还有更多的测试可以进行来验证幂律,例如 Kolmogorov-Smirnov 测试。然而,我们需要做一系列假设才能实际进行这些测试(如值之间相互独立)。现在,让我们更详细地查看这些邻域。
这些偏差背后的原因可能是什么?原因可能有很多:某些区域的突然增长、向外迁移、邻域内部的移动等。这里是地拉那 24 个区域在 2009 年到 2020 年期间的人口地图:

多年来的地拉那人口(作者提供的图片)
注意到在城市边缘区域的逐步增长:在共产主义垮台后的时期,很多人迁移到地拉那的边界地区,通常在旧的征用土地上建房,最终成为地拉那经济生活的一个重要部分(更多信息见这里)。
那些构成城市地拉那的核心 11 个区域呢?这里是筛选出来的地图:

多年来的城市地拉那(作者提供的图片)
空间马尔可夫模型
为了更深入地分析这些人口动态,我们需要建立能够通过时间表达行为的模型。马尔可夫模型对于此非常有用,它允许我们对离散状态的转移和稳定概率进行建模。在这种设置下,我们可以为区域的人口计数指定4个(可定制)桶:
- 🪣 桶 1: [0, 5690] ,桶 2: [5691, 12018],桶 3: [12019, 50184],桶 4: 50185+
在每一个时刻,我们可以处于这 4 个状态中的任何一个,且我们处于状态 i的机会只取决于之前的状态 (i — 1),这也称为马尔可夫性质。因此,我们可以得到一个邻域从例如桶 1 的人口数转变为桶 2 的人口数的转移概率。
然而,在这个具体的案例研究中,我们的区域很可能表现出一定程度的空间依赖性。例如,如果一个区域的人口密度很高,那么它周围的区域也很可能人口密度很高。为了考虑这一点,我们关注空间马尔可夫模型。这些模型要求我们计算一个描述这种空间依赖性的权重矩阵W。不过,首先,让我们使用莫兰指数检查邻里是否确实存在空间依赖性:

莫兰指数(图片由作者提供)
莫兰指数检查全局空间自相关:我们期望类似值的区域彼此靠近,而不同值的区域彼此远离。从本质上讲,它是用于衡量我们需要考虑的空间依赖性的度量。如上图所示,地拉那的行政区域及其人口数量表现出强烈的空间自相关,超出了上限和下限。
现在让我们看看如何计算考虑这种依赖关系的空间权重矩阵 W。有几种方法:对于这个项目,我选择了皇后连通性,它将邻近的多边形建模为那些共享至少一个边或顶点的多边形,并输出 W 矩阵。
W 矩阵是 22x22 的矩阵,每个值(i, j)中为 1,表示第 i 行的多边形与第 j 列的一个顶点或边相交。现在我们准备将年度人口数据和 W 矩阵输入到空间马尔可夫实例中。
空间马尔可夫结果
请注意,我们将获得 4 个独立的矩阵:每个多边形邻居可以位于一个分类中。以下是结果的一览:

空间马尔可夫模型矩阵(图片由作者提供)
有一些有趣的模式:
-
如果一个“人口 < 5690”的区域被其他“人口 < 5690”的区域包围,它保持在该分类中的概率为 0.925,但如果它的邻居是“人口在[5691, 12018]”之间,则该概率降至 0.684,因此第一个区域的人口更有可能增加。
-
类似地,一个“人口[5691, 12018]”的区域如果其邻居在最低分类中更可能保持这样(0.931),但如果其邻居也在同一分类中,则可能性较低(0.812)。
你可以使用其余的数值得出类似的结论。重要的是,每个区域更可能保持在同一分类中,而不是发生变化。这也有逻辑上的意义:在相对较短的时间内,一个地方更可能保持其当前特征,而不是经历剧烈变化。
下一步 + 代码
为什么这些结果有用?正如我们所见,观察可持续增长和衰退是城市韧性的重要组成部分。这些工具,包括测量幂律拟合或空间马尔可夫模型,使我们能够更准确地理解这些动态。在这个故事中,我介绍了如何使用其中一些方法,并查看了一些有趣的结果,包括哪些区域在给定的前几年数据中预期会增长或衰退。
下一次,我将更详细地探讨城市设计。与此同时,这里是代码笔记本供参考。感谢阅读!
使用 ChatGPT 查询你的 Neo4j 数据库
一种简单的方法让 ChatGPT 学习你的图谱结构,并帮助你构建 Cypher 查询
·
关注 发表在 Towards Data Science ·8 分钟阅读·2023 年 1 月 31 日
--
ChatGPT 已经席卷了整个世界。几乎不可能不被各种关于它的信息轰炸。我想,AI 生成内容泛滥只是时间问题。
我一直在使用 ChatGPT,并尝试在空闲时间生成 Cypher 查询。我发现了一篇利用 ChatGPT 帮助建模图谱并在 Neo4j 中创建一些基本查询的博客文章。因此,我决定记录一下使用 ChatGPT 作为 question2cypher 或 english2cypher 引擎的经验。
Neo4j Aura 设置
我们将使用 Neo4j Aura,它提供了一个带有预填充数据的 Neo4j 数据库的免费云实例,进行这次实验。登录后,点击 New Instance 并选择 Graph based Recommendations 数据集。

选择 Graph Based Recommendations 数据集。图片由作者提供。
在数据库实例启动后,点击 Query 按钮以打开 Neo4j Browser。
ChatGPT 设置
接下来,如果你还没有设置 ChatGPT,你需要进行设置。打开 ChatGPT 网站 并按照注册说明进行操作。完成后,你应该能看到 ChatGPT 用户界面。

ChatGPT 用户界面。图片由作者提供。
定义图 schema
ChatGPT 对使用 Cypher 和属性图模型有一定经验。然而,它并不了解你的图的 schema。因此,为了避免 ChatGPT 猜测并产生不准确的节点标签和属性,提供相关信息是至关重要的。
我认为告知 ChatGPT 节点标签和关系类型的最佳方式是使用以下 APOC 过程。
CALL apoc.meta.stats
YIELD labels, relTypes
结果

Apoc schema 结果。图片由作者提供。
假设你在 Neo4j Browser 中运行了上述 APOC schema 过程。在这种情况下,你可以简单地复制响应的文本版本并将其粘贴到 ChatGPT 中,并附上额外的信息,即这是 Neo4j 数据库的 schema 定义。
我有一个具有以下 schema 的 Neo4j 数据库:
—APOC schema 过程的结果—

图片由作者提供。
我得到以下响应,但不同的执行可能会有显著不同的反应。接下来,你需要提供关于节点和关系属性的信息,因为这些在 APOC schema 过程中并不可用。我们可以使用以下 Cypher 查询来提取所有属性键及其节点和关系类型。
MATCH (n)
UNWIND keys(n) AS key
RETURN labels(n)[0] AS label, collect(distinct key) AS propertyKeys
UNION
MATCH ()-[r]->()
UNWIND keys(r) AS key
RETURN type(r) AS label, collect(distinct key) AS propertyKeys
你可以通过告知 ChatGPT 你正在提供有关节点和关系类型的信息来将这些信息输入 ChatGPT。

提供节点和关系属性键。图片由作者提供。
使用 ChatGPT 生成 Cypher 查询
现在我们可以继续测试 ChatGPT 基于各种提示生成 Cypher 查询的效果。我们需要告知 ChatGPT 它将生成 Cypher 查询。我发现需要在初始提示中说明的是,ChatGPT 不应对来自先前提示的查询做任何假设,否则它会尝试将来自不相关的先前提示的信息合并到新生成的 Cypher 查询中。你也可以选择省略查询解释。
现在你将根据提示生成 Cypher 查询。除非明确指定,否则不要发布任何解释。同时也不要对上下文做任何假设。只查看给定提示的上下文,不要从之前的提示中假设任何其他内容。
我已经明确要求它不要在之前的提示中做任何假设,因为最初它假设了我的电影偏好,因为我经常询问基努·里维斯和喜剧。生成的查询在你要求它忘记或不忘记时似乎大致相同。唯一的区别是当我要求推荐时,它假设我喜欢基努·里维斯和喜剧,因此采用了这一点。此外,图谱模式的遗忘在这两种情况下都会发生,无论你是否要求它忘记之前的提示。
我们可以从简单的开始。

图片由作者提供。
提供的查询按预期工作。我们现在可以尝试一些更复杂的。

图片由作者提供。
再次,查询按预期工作。ChatGPT 在基本的图形模式匹配和聚合方面相当出色。它大多数时候也能选择正确的节点属性。然而,它在所谓的存在性查询上存在问题,这类查询可能会返回 true(如果模式存在)或 false(如果模式不匹配)。

图片由作者提供。
它似乎更喜欢MATCH子句,并且尚未掌握OPTIONAL MATCH或其他允许空值的方法。然而,在一些帮助下,它能够完成任务。

图片由作者提供。
唯一的问题是它忘记了基努·里维斯。
所以它只是检查一个演员是否出现在《黑客帝国》中。

图片由作者提供。
感觉就像是和一个有很多创造力但注意力几乎为零的人进行橡皮鸭编程,因为查询再次忘记了OPTIONAL MATCH。也许 ChatGPT 只是有无法治愈的注意力缺陷综合症。如果你希望它生成允许空值或 0 值的查询,它绝对需要一些微调。
接下来,我们可以检查 ChatGPT 的创造力。我们可以让它找到最受欢迎的喜剧,而不指定“受欢迎”的定义。

图片由作者提供。
生成的查询非常出色。ChatGPT 可能可以从许多电影推荐教程中学习。此外,它正确地假设了imdbRating属性包含某种流行度评分值。
我们可以要求它在数据库中包括用户评分。

图片由作者提供。
起初,我感到印象深刻。尽管它没有将 IMDB 评分和用户评分结合起来,但它为用户评分提供了一个不错的开始。然而,它忘记了流派是一个独立的节点,而不是节点属性。

图片由作者提供。
尽管修正后的 Cypher 查询现在正确地将类型识别为单独的节点,但由于某种原因,它决定将流行度定义为 IMDB 投票数。因此,即使你获得一个有效的 Cypher 查询,也不能保证下次会得到相同的 Cypher 语句,尽管它声称一致性很重要。
让我们尝试另一个对 Cypher 创意有要求的提示。

作者提供的图片。
尽管它抱怨无法提供好的推荐,但它仍然提供了一个好的起点。让我们看看是否可以尝试改进推荐。

作者提供的图片。
似乎 ChatGPT 有一些关于使用共同演员的平均电影评分的想法,然后提供与基努·里维斯在评分最高的电影中共同出演的演员作为推荐。
唯一的问题是上述查询无效。再次出现了 ChatGPT 虚构的节点属性imdb_rating而不是imdbRating。此外,它在第四行中收集电影评分,然后尝试对其取平均值。不幸的是,Cypher 不允许使用 avg() 操作符对数组进行平均。因此,我们需要去掉 collect 或使用 APOC。

作者提供的图片。
现在,ChatGPT 的 ADHD 决定我们应该继续使用它最初的基本推荐。然而,如果你使用的是 Neo4j v5,还会遇到另一个问题。在 v5 中,几种 Cypher 语法已被弃用,而 ChatGPT 对此一无所知,需要用最新的 Cypher 语法进行训练。在上述示例中,size() 操作符在 Neo4j v5 中不能再用于计算图模式。
最后,我想尝试一下 ChatGPT 是否能作为图数据科学家。

作者提供的图片。
默认情况下,ChatGPT 想要使用已弃用的图算法库。将其修正为使用图数据科学库可以稍微改善查询,但效果不完全。GDS 库自 2.0 版本以来不支持匿名图。因此,你需要单独投影一个内存图,然后在其上运行 PageRank。

作者提供的图片。
这是一些好的想法,但语法仍然无法使用。不过,它可以作为一个好的起点。
结论
ChatGPT 展现出作为 Neo4j 数据库查询接口的巨大潜力。它在基本的图匹配和聚合方面表现良好。不幸的是,它有时会忘记确切的属性名称或图模式,但在一点帮助下,它能够记住。
但是,当你想生成复杂的查询时,该查询是否有效的可能性是相等的。调试复杂查询感觉像是与一个注意力短暂的 ADHD 人士进行橡皮鸭编程。虽然它可能解决你明确提到的 bug,但也可能改变其他变量或引入新的 bug。ChatGPT 的一致性不是它的强项。此外,它没有接受 Neo4j v5 或图数据科学库中使用的更新 Cypher 语法的训练。因此,需要额外的微调才能正确。
总的来说,我感觉现阶段的 ChatGPT 可以用来生成简单的查询和模式。对于更复杂的任务,比如提供推荐,它可以用来提示 Cypher 语法或如何解决问题,但绝对不应被依赖来提供有效的查询。我很期待看到基于更新数据训练的新版 ChatGPT 的表现。我也可能尝试微调 ChatGPT 或 GPT-3,看看效果如何。
p.s. 文章由 黄思行 撰写,关于 使用 GPT-3 生成 Cypher 查询**.
使用类来生成信号
类 == 更好的代码可读性
·
关注 发表在 Towards Data Science ·7 分钟阅读·2023 年 2 月 1 日
--
在处理信号数据时,你可能需要编写多个函数和方法来实现你想要应用于信号数据的处理过程。然而,有时在没有先前了解这些数据的情况下,跟踪你对信号应用的方法的质量可能会很困难。因此,生成具有已知参数的信号数据可以帮助你更好地理解你的程序和方法。生成信号,特别是正弦信号,在信号处理研究中非常常见。通常,它用于测试或展示不同的信号处理技术,如离散傅里叶变换、小波分析等。

照片由 Markus Spiske 提供,来源于 Unsplash
在这篇文章中,我们将学习如何使用类构建一个信号生成器,以便生成正弦信号以进一步理解信号处理方法。
让我们从一个简单的代码开始,它生成一个正弦信号。
# Import the required package
import numpy as np
import matplotlib.pyplot as plt
# Generate the x-axis (from 0 to 1 with step of 1/200)
x = np.linspace(0,20,200)
# Generate a sinusoidal 1D Array
y = np.sin(x)
# Plot the result
plt.plot(x, y, 'b')
plt.xlabel('x')
plt.ylabel('y')
plt.title('Sinusoidal Signal')
plt.show()

使用 Numpy 包生成信号。[作者提供的图片]
为什么选择类?
从上面的代码来看,我们似乎可以在不使用类的情况下生成信号。那么,为什么类是一个好的选择呢?[1]
-
使用类时数据更可控,因为你可以构建你的对象和方法,这些方法可以用于控制和获取信号,而不需要像上述那样重复编码。
-
使用类可以带来更多功能。类允许你将变量和函数定义为属性。
-
提高代码的可读性和灵活性。使用类后,你的代码更容易理解和使用。
-
类在构建包中起着重要作用。如果你经常处理信号数据,你可能会考虑构建一个专门的包,帮助你更好、更快地进行信号数据分析工作。
“Think DSP” 是一个专门为数字信号处理构建的优秀示例。艾伦·道尼将他在信号处理方面的思考和经验融入了这个包,从生成信号到分析谱图等。
类,简单的开始
定义一个类类似于定义函数。首先,你需要为你的类取一个适当的名字。虽然没有命名类的硬性规定,但最好简短且与其功能相关。同时,这也是一种常见的做法。让我们为我们的信号生成器定义一个类。
# Define a class
class Signal:
# Statement 1
# Statement 2
在类内部可以定义不同的对象。这些对象可以是变量或函数,可以作为类的方法或属性传递。我们信号生成器类中的变量可能是信号的频率和幅度。让我们还定义一个打印这些变量的函数。
# Signal Generator Class
class Signal:
"""
Signal Generator
"""
# Define the frequency
f = 10 # in Hz
# Define the amplitude
a = 2
# Print the variables
def vars(self):
print('frequency=', self.f)
print('amplitude=', self.a)
在上面的代码中,“self” 代表类的实例,它用于访问类的属性,并将属性与给定的参数绑定 [2]。你可以使用除了 “self” 之外的任何词作为名称,因为该名称对 Python 没有特殊含义。然而,请注意,如果不遵循这一惯例,你的代码可能会对其他 Python 程序员的可读性降低 [1]。关于上面的代码,还有一点是定义类之后的语句。这是描述类功能的文档。将其实现到你的类中很重要,因为它使你的代码更具可读性。类的用户可以使用 Python 中的 help 函数访问文档。
下面的代码表示如何访问类的变量或函数。
# Define a signal object
signal = Signal()
# Print the frequency of the signal
print(signal.f)
# Calling vars to print the Signal class variables
signal.vars()
10
frequency= 10
amplitude= 2
类的特殊方法
到目前为止,上述类并没有展示太多灵活性。但幸运的是,Python 提供了可以用来使类更具可控性的特殊方法。我们将讨论的最重要的方法是 init() 方法。它用于实例化类并将其参数自定义为特定的初始状态。
有了这一点,调用类对象将自动调用 init 方法并实例化所有在 init 方法中实现的变量和函数 [1]。下面的代码是在之前的代码基础上修改的,通过定义 init 方法来完成。
# Signal Generator class
class Signal:
"""
Signal Generator
"""
# Initialize the class object
def __init__(self, f, a):
self.frequency = f
self.amplitude = a
# Print the varibles
def vars(self):
print('frequency=', self.frequency)
print('amplitude=', self.amplitude)
现在,如果我们尝试定义一个 Signal 对象,我们需要传递信号的频率和幅度。
# Define a signal object
signal = Signal(f=10, a=2)
# Print the frequency of the signal
print(signal.frequency)
# Calling vars to print the Signal class variables
signal.vars()
10
frequency= 10
amplitude= 2
现在我们理解了如何定义类及其对象以及特殊的初始化方法。是时候构建我们从一开始就想要的信号生成器类了。首先,我们需要指出在 init() 方法中实现的参数。正弦波的一般形式可以用以下方程表示 [3]:
y(t)=A.sin(2πf+ϕ)
其中:
-
A: 信号的幅度
-
f : 信号的频率 [Hz]
-
ϕ: 信号的相位
应该将两个额外的参数添加到我们的变量中:
-
信号的 duration,以秒为单位
-
采样率,每秒样本数
# Building a class Signal for better use.
class Signal:
"""
Generate sinusoidal signals with specific ampltiudes, frequencies, duration,
sampling rate, and phase.
Example:
signal = Signal(amplitude=10, sampling_rate=2000.0)
sine = signal.sine()
cosine = signal.cosine()
"""
def __init__(self, amplitude=1, frequency=10, duration=1, sampling_rate=100.0, phase=0):
"""
Initialize the Signal class.
Args:
amplitude (float): The amplitude of the signal
frequency (int): The frequency of the signal Hz
duration (float): The duration of the signal in second
sampling_rate (float): The sampling per second of the signal
phase (float): The phase of the signal in radians
Additional parameters,which are required to generate the signal, are
calculated and defined to be initialized here too:
time_step (float): 1.0/sampling_rate
time_axis (np.array): Generate the time axis from the duration and
the time_step of the signal. The time axis is
for better representation of the signal.
"""
self.amplitude = amplitude
self.frequency = frequency
self.duration = duration
self.sampling_rate = sampling_rate
self.phase = phase
self.time_step = 1.0/self.sampling_rate
self.time_axis = np.arange(0, self.duration, self.time_step)
# Generate sine wave
def sine(self):
"""
Method of Signal
Returns:
np.array of sine wave using the pre-defined variables (amplitude,
frequency, time_axis, and phase)
"""
return self.amplitude*np.sin(2*np.pi*self.frequency*self.time_axis+self.phase)
# Generate cosine wave
def cosine(self):
"""
Method of Signal
Returns:
np.array of cosine wave using the pre-defined variables (amplitude,
frequency, time_axis, and phase)
"""
return self.amplitude*np.cos(2*np.pi*self.frequency*self.time_axis+self.phase)
让我们使用我们的 Signal 类
假设我们要生成一个由三个正弦信号之和组成的信号。这三个信号的频率分别是 (20, 2, 7)Hz。这三个信号的幅度分别是 (2, 6, 1)。信号的采样率是 1000。最后,信号的持续时间是 3 秒。我们保持信号的相位为 0。
# Define the first signal, 20Hz and amplitude of 2
s1 = Signal(amplitude=2, frequency=20, sampling_rate=1000.0, duration=3)
sine1 = s1.sine()
# Define the second signal, 2Hz and amplitude of 6
s2 = Signal(amplitude=6, frequency=2, sampling_rate=1000.0, duration=3)
sine2 = s2.sine()
# Define the second signal, 7Hz and amplitude of 1
s3 = Signal(amplitude=1, frequency=7, sampling_rate=1000.0, duration=3)
sine3 = s2.sine()
# Our signal is the sum of the three signals
signal = sine1 + sine2 + sine3
# Plot the signal
plt.plot(s1.time_axis, signal, 'r')
plt.xlabel('Time [sec]')
plt.ylabel('Amplitude')
plt.title('This signal is generated using Signal class')
plt.show()

使用我们的 Signal 类生成信号的示例。[作者提供的图片]
结论
-
我们指出了一些使用类时代码的有用属性,例如,更好的数据控制、更强的代码功能,以及更好的代码可读性和灵活性。
-
我们已经逐步学习了如何在 Python 中构建一个类。我们从定义类及如何将变量或函数定义为对象开始,然后将其作为方法或属性与类一起传递。
-
我们学习了特殊方法 init()及其如何用于实例化类,并将参数定制为特定的初始状态,这使得用户能够更好地控制数据。
-
我们构建了最终的类作为一个信号生成器,您可以用它来获取具有特定频率、采样率、振幅和持续时间的正弦信号。
参考文献
[1] Python 文档,类,类的初步介绍。 [访问时间:2023 年 1 月 28 日]
[2] GeeksforGeeks,类,类中的 self。 [访问时间:2023 年 1 月 28 日]
[3] Kong, Q., Siauw, T., & Bayen, A. (2020). 《Python 编程与数值方法:工程师和科学家的指南》,傅里叶变换,波的基础。学术出版社。
使用深度学习生成奇幻名字:从零构建语言模型
一个语言模型能否发明独特的奇幻角色名字?让我们从零开始构建它
·发表于 Towards Data Science ·阅读时间 11 分钟·2023 年 9 月 22 日
--

来源:pixabay.com
要真正掌握 语言模型 (LM) 的复杂性并熟悉其基本原理,唯一的方法就是卷起袖子开始编写代码。在本文中,我展示了一个从零开始完全构建的 递归神经网络 (RNN),没有使用任何深度学习库。
Tensorflow、Keras、Pytorch 使得构建深层复杂的神经网络变得轻而易举。这无疑是机器学习从业者的一大优势,但这种方法也有一个巨大的缺点,那就是这些网络的运行机制往往不够清晰,因为它们是在“引擎盖下”发生的。
这就是为什么今天我们将进行一个鼓舞人心的练习,仅使用 Numpy Python 库来构建一个语言模型!
理解递归神经网络和语言模型
标准全连接神经网络不适用于像文本生成这样的 自然语言处理 (NLP) 任务。主要原因如下:
-
对于 NLP 任务,输入和输出可能采用不同的形式和维度。
-
标准神经网络不会同时使用网络不同步骤中学到的特征。
在 AI 应用于 NLP 领域的主要突破无疑是递归神经网络 (RNN)。
RNN 是一类特别适合 NLP 任务和文本生成的人工神经网络。它们的有效性在于能够捕捉数据中的序列依赖关系。人类语言深深依赖于考虑上下文并将句子的第一个词与最后一个词联系起来。考虑这些句子:
-
他说:“西奥多·罗斯福曾是美国总统。”
-
他说:“泰迪熊正在打折!”
单词 “Teddy” 在这两个句子中具有完全不同的含义。我们人类通过考虑上下文和句子另一部分的词语,很容易理解这一点。令人惊讶的是,RNN 也能做到这一点!
RNN 架构
RNN 的架构相当简单。它们由一系列顺序单元组成,每个单元接收一个词 x(或一个字符)作为输入,输出一个词/字符 y,并将激活值 a 传递给下一个单元。

RNN 流程图。图片由作者提供。
RNN 单元内部发生的事情更有趣。步骤如下:
-
前一个单元的激活值乘以一些权重 W_aa
-
输入 x 乘以一些权重 W_ax
-
前面步骤的结果会相加,并与偏置 b_a 一起处理
-
应用像双曲正切这样的激活函数来计算激活值,然后传递给下一个单元
-
激活值乘以一些权重 W_ya 并与偏置 b_y 相加
-
最后,对结果向量应用 softmax 函数,并返回输出 y_hat

RNN 单元流程图。图片由作者提供。
总结一下,激活和输出的公式如下:


我无法在这里提供 RNN 和语言模型的全面理论介绍。有关内容,请参见本文末尾列出的资源。
现在我们来深入了解实际的 Python 代码。本文将详细解释代码中最重要的部分。为了使文章简洁,一些直观的部分被省略。整个带注释的代码可以在我的 GitHub 仓库 中获取:
[## articles/names-generator-RNN at main · andreoniriccardo/articles
通过在 GitHub 上创建账户来为 andreoniriccardo/articles 开发做贡献。
数据准备
我们的目标是教会语言模型发明新颖的奇幻角色名称。因此,我们需要为语言模型提供一个实际奇幻名称的数据库。语言模型将从中获得训练和灵感。
由于这个 维基百科页面,我们可以轻松访问提到的所有角色的列表,这些角色出现在 指环王 或 霍比特人 书中。使用 BeautifulSoup 和 Regex 库,以下代码将收集数据:
from bs4 import BeautifulSoup
import requests
import re
url = "https://en.wikipedia.org/wiki/List_of_Middle-earth_characters"
# Get the contents of the webpage in text format
data = requests.get(url).text
soup = BeautifulSoup(data,"html.parser")
# Parse the HTML code to isolate rows containing character names
names = soup.find_all('li')
# Apply a second filter to isolate actual names
names_2 = []
for i in names:
if """<li><a href="/wiki/""" in str(i):
names_2.append(str(i))
# Retrieve final character names applying cleaning conditions
lotr_names = []
for i in names_2:
candidate_name = re.findall(r'"(.*?)"', i)[1]
conditions = 'Category:' not in candidate_name and 'Middle-earth ' not in candidate_name and 'Tolkien' not in candidate_name and 'Lord of the Rings' not in candidate_name and 'The Hobbit' not in candidate_name
if conditions:
lotr_names.append(candidate_name.replace(' (Middle-earth)',''))
关于 Regex 函数的解释,我为你提供了这份详尽的 指南。
此时,我们的数据集将如下所示:

我的目标是通过替换不需要的字符如 ä、é、î 来简化我们的词汇。
replace_dict = {'a':['â','ä','á'],
'e':['ê','ë','é'],
'i':['î','í'],
'o':['ô','ö','ó'],
'u':['ú','û',],
' ':['-']
}
for new_char in replace_dict.keys():
for old_char in replace_dict[new_char]:
lotr_names = lotr_names.replace(old_char, new_char)
最终,我们的词汇由 27 个字符组成:
[‘x’,‘j’,‘f’,‘t’,‘c’,‘b’,‘o’,‘l’,‘y’,‘w’,‘i’,‘e’,‘g’,‘m’,‘k’,‘d’,‘v’,‘n’,‘u’,‘a’,‘z’,‘r’,‘\n’,‘s’,‘ ‘,‘p’,‘h’]
请注意,特殊字符 ‘\n’ 作为名称终止符,表示生成的名称应何时终止。
数据集已经准备好,我们现在可以专注于建模递归神经网络。
构建语言模型
在本节中,我展示了语言模型的 Python 实现。整个代码可以分为两个部分:
-
正向传播
-
Backprop
在接下来的部分中,我将展示实际训练模型的代码以及如何生成奇幻角色名称。
正向传播
如上所示,RNN 是由多个单元组成的网络。在 Python 中建模单个 RNN 单元然后通过多个单元集成其输出是有利的。
模拟单个 RNN 单元的代码如下:
def RNN_forward_prop_step(parameters, a_prev, x):
W_aa = parameters['W_aa']
W_ax = parameters['W_ax']
W_ya = parameters['W_ya']
b_y = parameters['b_y']
b = parameters['b']
# Compute hidden state
a_next = np.tanh(np.dot(W_ax, x) + np.dot(W_aa, a_prev) + b)
# Compute log probabilities for next character
p_t = softmax(np.dot(W_ya, a_next) + b_y)
return a_next, p_t
如你所见,这一点也不复杂。我们只是将模型的参数作为输入,并将其与单元的输入和先前的激活函数相乘。最后,我们应用 softmax 函数返回一个表示输出字符概率的向量。
下一步是遍历多个 RNN 单元。这正是 RNN_roward_prop() 函数的作用。
def RNN_forward_prop(X, Y, a0, parameters, vocab_size):
x = {}
a = {}
y_hat = {}
a[-1] = np.copy(a0)
loss= 0
# Iterate for the T timesteps
for t in range(len(X)):
# One-hot representation of the t-th character
x[t] = np.zeros((vocab_size, 1))
if X[t] != None:
x[t][X[t]] = 1
# Run one timestep of forward prop
a[t], y_hat[t] = RNN_forward_prop_step(parameters, a[t-1], x[t])
# Update loss function
loss -= np.log(y_hat[t][Y[t],0])
cache = (y_hat, a, x)
return loss, cache
它调用之前的 RNN_roward_pro_step() 函数 T 次,其中 T 是输入词的字符数。最后,返回损失函数和输出结果。
Backprop
Backprop,即反向传播,是调整网络权重以逐渐接近期望输出的过程,即减少模型损失函数。这是通过应用梯度下降来完成的。
详细说明梯度下降或其他可行优化算法的公式超出了本文的范围。我将推荐这篇文章,它正好涉及如何选择适合你深度网络的优化算法。
由于神经网络的开发过程是迭代性的,我们需要利用每一个可能的便捷方法……
towardsdatascience.com
我用来建模反向传播流的策略与前向传播相同:编码单个 RNN 单元的反向传播并多次迭代。
def RNN_back_prop_step(d_y, grads, parameters, x, a, a_prev):
grads['dW_ya'] += np.dot(d_y, a.T)
grads['db_y'] += d_y
da = np.dot(parameters['W_ya'].T, d_y) + grads['da_next']
da_raw = (1 - a * a) * da
grads['db'] += da_raw
grads['dW_ax'] += np.dot(daraw, x.T)
grads['dW_aa'] += np.dot(daraw, a_prev.T)
grads['da_next'] = np.dot(parameters['W_aa'].T, daraw)
return grads
上述函数接受前向传播的输出和之前的梯度作为输入,并计算更新后的梯度。
RNN_back_prop_function()函数的迭代通过以下代码行执行:
def RNN_back_prop(X, Y, parameters, cache):
# Initialize gradients as an empty dictionary
grads = {}
# Retrieve from cache and parameters
(y_hat, a, x) = cache
W_aa = parameters['W_aa']
W_ax = parameters['W_ax']
W_ya = parameters['W_ya']
b_y = parameters['b_y']
b = parameters['b']
# Initialize gradients
grads['dW_ax'], grads['dW_aa'], grads['dW_ya'] = np.zeros_like(W_ax), np.zeros_like(W_aa), np.zeros_like(W_ya)
grads['db'], grads['db_y'] = np.zeros_like(b), np.zeros_like(b_y)
grads['da_next'] = np.zeros_like(a[0])
# Backpropagate through timesteps
for t in reversed(range(len(X))):
dy = np.copy(y_hat[t])
dy[Y[t]] -= 1
grads = RNN_back_prop_step(dy, grads, parameters, x[t], a[t], a[t-1])
return grads, a
训练语言模型
此时,所有模型的组件都已设置好并准备执行。我写了以下代码来整合我们上面看到的函数。
def RNN_optimization(X, Y, a_prev, parameters, alpha, vocab_size):
# 1\. Forward propagation
loss_now, cache = RNN_forward_prop(X, Y, a_prev, parameters, vocab_size)
# 2\. Backward propagation
grads, a = RNN_back_prop(X, Y, parameters, cache)
# 3\. Clip gradients
grads = clip_grads(grads, 10)
# 4\. Update parameters
parameters = update_parameters(parameters, grads, alpha)
return loss_now, parameters, a[len(X)-1]
实际的魔法发生在以下代码片段中。这是整个算法的主要部分,一旦调用此函数,语言模型将被训练。
def train_model(data, n_a=50, max_iter = 100000):
# Get the list of characters
chars = list(set(data))
# Get the dictionary size (number of characters)
vocab_size = len(chars)
# Get encoding and decoding dictionaries
chars_to_encoding = encode_chars(chars)
encoding_to_chars = decode_chars(chars)
# Get dataset as a list of names and strip, then shuffle the dataset
data = data.split('\n')
data = [x.strip() for x in data]
np.random.shuffle(data)
# Define n_x, n_y parameters
n_x, n_y = vocab_size, vocab_size
# Initialize the hidden state
# a_prev = initialize_hidden_state(n_a)
a_prev = np.zeros((n_a, 1))
# Initialize the parameters
parameters = initialize_parameters(n_a, n_x, n_y)
# for k in parameters.keys():
# print('{}: tipo {}, Datatype {}'.format(k, type(parameters[k]), parameters[k].dtype))
# Get current loss function value
loss_now = get_initial_loss(vocab_size, len(data))
# Perform max_iter iteration to train the model's parameters
for iter in range(max_iter):
# print(iter)
# Get the index of the name to pick
name_idx = iter % len(data)
example = data[name_idx]
# Convert encoded and decoded example into a list
example_chars = [char for char in example]
example_encoded = [chars_to_encoding[char] for char in example]
# Create training input X. The value None is used to consider the first input character
# as a vector of zeros
X = [None] + example_encoded
# Create the label vector Y by appending the '\n' encoding to the end of the vector
Y = example_encoded + [chars_to_encoding['\n']]
# Perform one step of the optimization cycle:
# 1\. Forward propagation
# 2\. Backward propagation
# 3\. Gradient clipping
# 4\. Parameters update
loss_tmp, parameters, a_prev = RNN_optimization(X, Y, a_prev, parameters, alpha=0.01, vocab_size=vocab_size)
# for k in parameters.keys():
# print('{}: tipo {}, Datatype {}'.format(k, type(parameters[k]), parameters[k].dtype))
loss_now = smooth(loss_now, loss_tmp)
return parameters
结果是一个能够模仿托尔金创作过程的模型,轻松生成独特的角色名字。
生成奇幻角色名字
为了从我们训练的语言模型中采样新的字符名字,我开发了两个函数。
sample()函数接受网络的参数和将字符映射到数字的词汇表作为输入。其思想是多次应用前向传播步骤,直到返回‘\n’特殊字符,或者达到生成字符的上限(在此设置为 50)。
最终,它返回一个索引列表,用于编码生成的奇幻角色名字。
def sample(parameters, chars_to_encoding):
W_aa = parameters['W_aa']
W_ax = parameters['W_ax']
W_ya = parameters['W_ya']
b_y = parameters['b_y']
b = parameters['b']
vocab_size = b_y.shape[0]
n_a = W_aa.shape[1]
x = np.zeros((vocab_size,))
a_prev = np.zeros((n_a,))
indices = []
idx = -1
counter = 0
newline_character = chars_to_encoding['\n']
while (idx != newline_character and counter != 50):
a = np.tanh(np.dot(W_ax,x)+np.dot(W_aa,a_prev)+np.ravel(b))
z = np.dot(W_ya,a) + np.ravel(b_y)
y = softmax(z)
idx = np.random.choice(list(chars_to_encoding.values()), p=np.ravel(y))
indices.append(idx)
x = np.zeros((vocab_size,))
x[idx] = 1
a_prev = a
counter +=1
if (counter == 50):
indices.append(chars_to_encoding['\n'])
return indices
为了以人类可读的形式打印生成的奇幻名字,我们需要调用get_sample()函数,它接受先前生成的索引列表和将索引映射到字符的解码字典作为输入。
def get_sample(sample_ix, encoding_to_chars):
txt = ''.join(encoding_to_chars[ix] for ix in sample_ix)
txt = txt[0].upper() + txt[1:]
return txt
一切就绪后,你现在可以欣赏一些原创的奇幻角色名字,如下所示:

图片由作者提供。
评估模型性能
在训练过程的初期,模型仍然无法有效模仿托尔金的风格。如果我们在这个阶段采样随机名字,我们会得到纯粹的胡言乱语,如下所示:
-
在第 2000 次迭代和损失 27.96 时的“Orvnnvfufufiiubx”
-
在第 4000 次迭代和损失 26.43 时的“Aotvux”
我们可以清楚地看到这些名字不符合标志性的中土世界模式和声音。然而,通过让模型在数据集特征上学习更长时间,我们开始逐渐获得更可信的生成名字:
-
“Furun I” 在第 14000 次迭代和损失 21.53
-
“Flutto Balger” 在第 16000 次迭代和损失 21.11
最后,经过训练的模型似乎能够模拟角色的名字风格。
我所描述的改进在定量上也可以通过以下方式可视化。
通过绘制迭代过程中的损失函数,我们可以清楚地看到 优化算法 如何逐步调整权重和偏差到正确的方向。

图片由作者提供。
损失函数的振荡行为是因为我们在每次迭代步骤中使用了一个单一的训练示例来训练模型。使用较大的批次将会导致更平滑的损失曲线。
结论
总之,为幻想名称生成构建语言模型给我们带来了几个见解。
我们发现 RNN 和语言模型能够识别数据中的序列依赖性,使它们成为自然语言处理以及涉及文本的所有任务的关键元素。
此外,我不能再强调实践方法在任何数据科学相关主题中获得经验的重要性。学习理论是基础,但只提供了你所需的一半知识。
最后,我想再次强调我们刚刚创建的工具的灵活性。我推荐用不同的名字集进行训练。尝试使用迪士尼角色名字或典型的宠物名字作为输入数据集,并与我分享你生成的内容!
如果你喜欢这个故事,可以关注我,以便及时了解我的最新项目和文章!
这是我的一些过去项目:
## 使用 Scikit-Learn 的集成学习:一个友好的介绍
像 XGBoost 或随机森林这样的集成学习算法是 Kaggle 比赛中表现最好的模型之一……
towardsdatascience.com ## 欧洲旅行优化:遗传算法和 Google Maps API 解决旅行推销员问题
使用遗传算法和 Google Maps API 探索欧洲 50 个最受欢迎城市的魅力,解锁高效的……
towardsdatascience.com ## 数据科学的诞生:历史上的首次假设检验与 Python 见解
深入了解每位数据科学家需要知道的 Python 驱动的见解
towardsdatascience.com
资源
将 Delta Lake 用作下游应用程序的主数据管理(MDM)源
在本文中,我们将尝试了解如何利用 Delta Lake 更改提要的输出来为下游应用程序提供数据
·发布于 Towards Data Science ·9 min read·2023 年 2 月 5 日
--

图片由 Satheesh Sankaran 提供,来自 Pixabay
根据 ACID 规则,隔离理论指出“任何事务的中间状态不应影响其他事务”。几乎每个现代数据库都是为了遵循这一规则而构建的。不幸的是,直到最近,同样的规则在大数据领域还未能有效实施。这是什么原因呢?
现代分布式处理框架,如 Hadoop MapReduce 和 Apache Spark,以批处理的方式进行计算。计算完成后,会生成一组输出文件,每个文件存储一系列记录。通常,分区和减少器的数量会影响生成的输出文件数量。但也存在一些问题:
-
轻微的记录级更改或新增记录(CDC)会迫使你每次都重新计算整个批处理——这是一种巨大的计算周期浪费,影响了成本和时间。
-
下游消费者在批处理重新计算期间可能会看到不一致的数据

作者提供的图片
Delta Lake 框架为 Spark 计算添加了事务管理的概念。通过支持 ACID 事务、模式强制、索引、版本控制和数据修剪,Delta Lake 旨在提高数据的可靠性、质量和性能。
简单来说,即使添加或更改了一些 CDC 记录,也无需重新计算整个批次。相反,它提供了插入、更新、删除或合并数据的功能。Delta Lake 通过选择包含已更改数据的文件,将其读入内存,然后将结果写入新文件来工作。

作者提供的图片
Delta Lake 被广泛用作实现现代数据湖屋架构的基础。它的核心功能使其非常适合将来自不同来源和不同模式的数据集合并到一个通常被称为“单一真实来源”的公共数据层中。这个“单一真实来源”即 MDM 层,用于支持所有的分析工作负载,包括 BI、数据科学、机器学习和人工智能。
在本文中,我们将尝试进一步拓展对 Delta Lake 的理解。如果 Delta Lake 可以作为汇来合并来自不同来源的数据,那我们为什么不能将其用作源来捕获下游消费者的变更数据(CDC)呢?如果我们能使用奖章架构来实现这一点,那就更好了。奖章架构可以用来将来源系统中的 CDC 数据合并到数据湖屋的青铜层、白银层和黄金层。
更好的是,我们可以以流的方式捕获变更并将其发布到下游消费者。让我们探讨一个用例:
-
酒店价格在一天内会发生多次变化
-
一家电子商务公司专注于追踪全球最新的酒店价格,并在其网站门户上展示这些信息,以便客户可以根据实时数据进行预订。

作者提供的图片
在此示例中,我们将使用生产者程序从三个 API 来源读取实时价格信息。生产者将以 JSON 格式将数据作为事件发送到 Amazon Kinesis。然后,我们将在 Databricks notebook 中使用结构化流来读取这些事件。最后,CDC 事件被传输到关系数据库。这个关系数据库被电子商务门户用来展示不断变化的酒店价格。
执行运行 CDF Notebook 的前置步骤
上述示例的代码可以在以下位置找到:
[## blogs/cdc-source at master · mkukreja1/blogs
目前无法执行该操作。您已在另一个标签或窗口中登录。您已在另一个标签或窗口中注销…
要运行此代码,你需要拥有有效的 AWS 和 Databricks 帐户。在 Databricks 中运行笔记本之前,需要在 AWS 上完成一些前提步骤:
-
使用下面的链接获取 AWS 访问密钥。访问密钥(访问密钥 ID 和秘密访问密钥)将作为 Databricks 笔记本访问 AWS 服务(如 Amazon Kinesis)的凭证。
console.aws.amazon.com/iam/home?region=us-east-1#/security_credentials -
在连接到 AWS 门户时,点击 AWS 云终端菜单。然后运行下面的命令以创建本文所需的前提 AWS 资源:
$ git clone <LINK>
$ cd blogs/cdc-source
$ sh pre-req

作者提供的图片
- 启动将从 API 读取事件并将其发送到 Amazon Kinesis 的生产者。
$ nohup python3 hotel-producer.py &

作者提供的图片
- 保持 AWS 云终端会话运行。从这里开始,生产者将每 5 分钟将事件发送到 Amazon Kinesis。
Delta Lake 更改馈送正在运行中!
现在我们已经在 AWS 上创建了前提资源,我们准备好将 CDC 作为源笔记本进行运行。Databricks 笔记本中的代码将从 Amazon Kinesis 读取事件,将更改合并到铜层,然后进行清理并将结果合并到银层。所有这些都将在流式处理的方式中完成,最后,结果(更改数据馈送)将同步到外部关系数据库表。在此时,你需要登录到你的 Databricks 帐户。
准备 Delta Lake 作为更改数据馈送源笔记本环境
将 delta-as-cdc-source-notebook.ipynb 笔记本导入到你的 Databricks 工作区。要运行笔记本,你需要用前一节中获取的值替换三个变量(awsAccessKeyId、awsSecretKey 和 rdsConnectString)。

作者提供的图片
在铜层中创建 Delta 表
我们将从 Amazon Kinesis 读取事件。默认情况下,Databricks 运行时包括结构化流的 Kinesis 连接器。你可能已经注意到,我们在流的有效载荷中发送了 JSON 事件。在下面的示例中,我们使用结构化流读取事件,将架构应用于 JSON,提取其中的值,最后将结果保存为铜层中的 Delta 表。我们选择 Amazon S3 作为所有 Delta 表的存储位置。

作者提供的图片
注意到铜层中的数据是事件数据的原始表示,因此我们遵循匹配原始流形式的架构。
在更改数据流中策划数据
从这里开始,铜层表将根据从 Kinesis 流读取的数据不断添加新分区。在铜层中选择时间戳作为分区列是一个好习惯。这有助于轻松识别从源读取的事件的时间顺序,并在未来需要重放事件时发挥重要作用。
在下一步中,我们将进行一些转换以整理数据,例如将 Unix 纪元时间转换为日期、更改数据类型和拆分字段。

作者提供的图片
我们现在准备将数据合并到银层中。但是在此之前,我们需要了解 CDC 在结构化流中的工作原理。更重要的是,Delta 层的变更日志流如何发布到下游消费者?
理解 Delta 作为变更流的流程
参考下面的示例,我们可以了解使用 Delta Lake 作为数据源的变更数据流动。在结构化流处理数据时,数据会被处理成微批次。实施过程中包括将变更数据同时写入多个表,这也称为幂等写入,如下所示:
-
银层表(hotels_silver),其中每个微批次中的记录要么作为新记录插入,要么合并到现有记录中。每次变更都会创建 Delta 表的新版本。
-
一个变更日志表(change_log)存储键和batchId。将该表中的数据视为时间上的不可变更日志。
在下面的示例中,铜层流显示了Marriott 酒店在纽约的两条记录(在下图中突出显示)。注意这两条记录随时间变化的价格差异。时间上,当第一条记录从 Kinesis 读取时,其时间戳=022–02–16T21:06:57 被分配为batchId=2。现在,如果我们使用 change_log 表中的键将记录与 hotels_silver 表中的记录连接起来,我们可以重建该行并将其作为 CDC 记录发送给下游消费者。在下面的示例中,相同的记录在不同的时间间隔内被发送了两次。

作者提供的图片
第二次变更记录的时间戳=022–02–16T21:07:41 被分配为batchId=3并发送到下游。下游消费者可以接收 CDC 并保持其状态与持续变化保持同步。
将 Delta 实现为变更数据流
了解数据流动后,让我们深入探讨实际实现。下面的函数在微批次级别运行。对于每个微批次,该函数会对银层以及变更记录表执行幂等写入。

该函数通过 foreachBatch() 操作调用,该操作允许对流查询的输出进行任意操作和写入逻辑。在下面的代码中,我们同时对两个表执行了经过整理的数据流的幂等写入。

作者提供的图片
在幂等写入进行的同时,对于每个新的微批次,变更数据会与银层表连接,以重建 CDC 记录。

作者提供的图片
然后,可以将重建的 CDC 记录同步到下游。在下面的例子中,我们将 CDC 记录发送到关系型数据存储。

作者提供的图片
关系型数据存储接收不可变的 CDC 记录流,并执行去重逻辑,以便在其应用程序中显示最新的记录等效项。让我们在下面的部分检查这如何发生。
在下游消费者中检查酒店价格
现在我们已经将 CDC 流推送到下游消费者(在我们的例子中是关系型 MySQL 数据库),让我们查询几个记录以查看记录如何演变。来自 Databricks 笔记本的 CDC 记录流正在不断推送到hotelcdcprices表。但该表包含所有记录,包括随时间变化的记录。因此,创建了一个视图来根据时间戳对变更行进行排序。

作者提供的图片
此视图显示了任何酒店在任何给定时间的最新价格等效项。此视图可供 Web 应用程序使用,以在门户上显示最新价格。

作者提供的图片
变更数据馈送的典型用例是什么?
下面是一些常见的用例,可以从使用 Delta 表作为合并来自不同来源的 CDC 数据并将其下传给消费者中获益:
以流式方式读取变更数据馈送并合并到银层
从流数据源捕获 CDC 并将微批次连续合并到银层。
在黄金层中执行聚合而无需重新计算整个批次
仅使用银层中的变更数据,在黄金层中汇总相应的行,无需重新计算整个批次。
透明地将 Delta 表中的变更传输给下游消费者
轻松将变更传输到下游消费者,如关系型数据库和应用程序。
总结来说,使用 Delta 表中的变更数据馈送功能,不仅可以简化 CDC 数据的收集和合并过程,还可以将变更数据下传到关系型数据库、No-SQL 数据库以及其他应用程序。这些下游应用可以有效地利用这些 CDC 数据来满足任何必要的目的。
我希望这篇文章对你有所帮助。Delta Lake 和 Change Data Feed 是由 Datafence Cloud Academy 提供的 AWS 大数据分析课程的一部分。课程由我本人在周末在线授课。
更频繁地使用频率分析
原文:
towardsdatascience.com/use-frequency-more-frequently-14715714de38
一本从基础到高级的频率分析手册:探索一个在数据科学中广泛被低估的重要工具
·发布于 Towards Data Science ·阅读时间 20 分钟·2023 年 5 月 18 日
--
频率分析在众多领域中极其有用。从音频到机械系统,再到自然语言处理和无监督学习。对许多科学家和工程师来说,它是一个重要的工具,但对许多数据科学家和开发者来说,它几乎不为人知。如果你不了解频率分析,不用担心,你刚刚找到了一本手册。

图片由 Daniel Warfield 使用 p5.js 制作。本文档中的所有图片均使用 p5.js 或 Python 的 Matplotlib 库创建,除非另有说明。
这对谁有用? 任何处理几乎所有信号、传感器、图像或 AI/ML 模型的人。
这篇文章有多高级? 这篇文章适合初学者,包含的示例也会引起即使是最高级的频率分析用户的兴趣。无论你的技能水平如何,你都能从这篇文章中获得一些有用的东西。
你将从这篇文章中获得什么? 对波动和频率的概念性和数学性理解,对如何在 Python 中应用这些概念的实际理解,一些常见的使用案例以及一些更高级的使用案例。
注意: 为了帮助你快速浏览,我将子章节标记为基础、中级和高级。这是一篇长文,旨在让从零开始的读者掌握相关知识。然而,如果你已经在频率领域有了教育或经验,你可以跳过中级部分,直接阅读高级主题。
我还设置了链接,方便你点击导航到目录以及从目录返回
目录
点击链接以导航到特定部分
1) 频率领域
1.1) 频率领域的基础(基础)
1.2) 频率领域的细节(中级)
1.3) Python 中的简单示例(中级)
2) 频域的常见应用 2.1) 去趋势和信号处理(中级)
2.2) 振动分析(高级)
3) 频域的高级应用
3.1) 数据增强(高级)
3.2) 嵌入和聚类(高级)
3.3) 压缩(中级)
4) 数据科学家的概念要点 5) 总结
1) 频域
1.1) 频域基础(基础)
(返回目录)
首先,什么是 领域?假设你想理解温度随时间的变化。仅仅阅读这句话,你可能会想象出这样的图表:

当你考虑某一时间段内的温度时,你可能会想象的场景
也许你想象时间从左到右进展,更高的温度对应更高的垂直点。恭喜你,你已经将数据映射到了一个二维时间 领域 中。换句话说,你把在某些时间记录的温度读数映射到一个时间为一轴、值为另一轴的空间中。
我们还有其他方式来表示温度与时间的数据。如你所见,这些数据具有“周期性”特征,意味着它来回振荡。许多数据以这种方式表现:声音、心跳的 ECG 数据、加速度计等运动传感器,甚至图像。在某种程度上,很多事物的数据都是周期性来回变化的。
“如果你想发现宇宙的秘密,就要从能量、频率和振动的角度思考。” ― 尼古拉·特斯拉
我可以通过曲折的方式到达这一点,但一图胜千言。本质上,我们可以将温度图表拆解成一系列简单的波,这些波具有不同的频率和振幅(频率指波动的快慢,振幅指波动的高低),然后用这些波来描述数据。

所有不同频率和振幅的波组成了我们原始的波。你可能会注意到,有一条波比另外两条更为微妙,几乎在原始波中看不出来。找到这些隐藏的信息是频域分析的一大好处。
这些波是通过傅里叶变换提取的,傅里叶变换将我们的原始波从时间域映射到频域。频域中不是值对时间,而是振幅对频率。

每个提取的波都有一个频率和振幅。如果我们将频率绘制在 x 轴上,将振幅绘制在 y 轴上,我们就绘制了所谓的频谱图
总结来说,傅里叶变换将数据(通常,但不总是在时间域中)映射到频域中。频域描述了所有的波形,具有不同的频率和振幅,这些波形加在一起可以重建原始波形。

原始波形在时间域中的表现,以及在频域中的频率内容。这两者描述的是相同的信号
1.2) 频域的细节(中级)
(返回目录)
sin 函数是三角形的对边与直角三角形的斜边的比值,针对某个角度。

θ(theta) 是一个直角三角形的角度,a 是 θ 对边的长度,c 是斜边的长度
sin 波是当你绘制 a/c 对于不同的 θ(不同的角度)时得到的结果,并且它在几乎所有的科学学科中作为最基本的波形使用。

正弦函数、直角三角形和正弦波之间的关系
通常 sin(θ) 被展开为 Asin(ωθ+ϕ)*。
ω(omega) 代表频率(ω 的值越大,正弦波的振荡速度越快)
ϕ(phi) 代表相位(改变 ϕ 会将波形向右或向左移动)
A 影响函数的缩放,定义了振幅(振荡的大小)。

“A” 控制振幅(高度), “omega” 控制频率(振荡速度), “phi” 控制相位(左右移动)
当我解释频域时,我展示了一个简化的表示,其中水平轴是频率,垂直轴是振幅。实际上,频域不是二维的,而是三维的:一个维度表示频率,一个维度表示振幅,另一个维度表示相位。对于更高维度的信号(如图像),谱图可以具有更高的维度。

传统的振幅与频率谱图(左)与更具描述性的振幅、频率和相位图。
当将信号转换到频域(例如使用像 scipy 这样的库)时,你会得到一个虚数列表。
[1.13-1.56j, 2.34+2.6j, 7.4,-3.98j, ...]
如果你不熟悉虚数,不用担心。你可以将这些列表想象成点,其中列表的索引对应于频率,而复数虚数分别代表振幅和相位的元组。
[(1.13, 1.56), (2.34, 2.6), (7.4, -3.98), ...]
我还没有谈论这些数字的单位。由于单位本质上是对所有数据的线性变换,它们在数据科学的角度上常常可以忽略。然而,如果你将来使用频域,你可能会遇到像赫兹(Hz)、周期(T)等频域特有的概念。你将在示例中看到这些单位的详细说明。
如果你想更多地了解单位及如何作为数据科学家处理它们,我有一篇关于此的文章在这里
1.3) Python 中的简单示例(中级)
(返回目录)
在这个示例中,我们加载了一个小号音乐片段,将其转换到频域,绘制频谱图,并使用频谱图来理解原始信号。
首先,我们将加载和绘制声音数据,这是一个随时间变化的幅度数据。该数据用于控制扬声器振膜的位置,振膜的振动产生声音。
"""
Loading a sample waveform, and plotting it in the time domain
"""
#importing dependencies
import matplotlib.pyplot as plt #for plotting
from scipy.io import wavfile #for reading audio file
import numpy as np #for general numerical processing
#reading a .wav file containing audio data.
#This is stereo data, so there's a left and right audio audio channel
samplerate, data = wavfile.read('trumpet_snippet.wav')
#creating wide figure
plt.figure(figsize=(18,6))
#defining number of samples we will explore
N = 3000
#calculating time of each sample
x = np.linspace(start = 0, stop = N/samplerate, num = N)
#plotting channel 0
plt.subplot(2, 1, 1)
plt.plot(x,data[:N,0])
#plotting channel 1
plt.subplot(2, 1, 2)
plt.plot(x,data[:N,1])
#rendering
plt.show()

这是一个立体声小号音乐片段的左声道和右声道的时间域图。X 轴表示时间(秒),Y 轴表示信号的幅度,控制扬声器振膜的位置,从而产生声音。(原始小号数据来自 storyblocks.com)
让我们将这些波形转换到频域
"""
Converting the sample waveform to the frequency domain, and plotting it
This is basically directly from the scipy documentation
https://docs.scipy.org/doc/scipy/tutorial/fft.html
"""
#importing dependencies
from scipy.fft import fft, fftfreq #for computing frequency information
#calculating the period, which is the amount of time between samples
T = 1/samplerate
#defining the number of samples to be used in the frequency calculation
N = 3000
#calculating the amplitudes and frequencies using fft
yf0 = fft(data[:N,0])
yf1 = fft(data[:N,1])
xf = fftfreq(N, T)[:N//2]
#creating wide figure
plt.figure(figsize=(18,6))
#plotting only frequency and amplitude for the 1st channel
plt.subplot(2, 1, 1)
plt.plot(xf, 2.0/N * np.abs(yf0[0:N//2]))
plt.xlim([0, 6000])
#plotting only frequency and amplitude for the 2st channel
plt.subplot(2, 1, 2)
plt.plot(xf, 2.0/N * np.abs(yf1[0:N//2]))
plt.xlim([0, 6000])
plt.show()

之前加载的小号音频的频域表示。X 轴是频率(以赫兹为单位,即每秒振荡次数),Y 轴是信号的幅度。
仅通过可视化这个图表,可以得到一些见解。
-
两个信号包含非常相似的频率内容,这很有意义,因为它们都来自同一录音。通常,立体声录音是用两个独立的麦克风同时录制的。
-
主频率大约为 523Hz,对应于 C5 音符。
-
有大量的共鸣,这可以看作是基频整数倍的频率处的尖峰。这种特性在使乐器声音良好时至关重要,是由于乐器的各个部分在不同频率下共鸣,而这些共鸣是由主要振动引起的。
-
这是一个非常清晰的声音,音峰没有被大量无关的频率内容弄得模糊不清。
-
这是一个有机的声音。有些频率内容与基频无关。这可以被认为是乐器的音色,使其听起来像小号,而不是其他乐器演奏相同的音符。
在第二部分中,我们将探讨频域在时间序列信号处理中的常见用途。在第三部分中,我们将探讨更高级的话题。
2) 频域的常见用途
2.1) 去趋势化和信号处理(中级)
(返回目录)
假设你有一个电气系统,并且你想了解在一天时间内该系统的逐分钟电压变化。你设置一个电压计,捕捉并绘制电压随时间的变化信息。
假设在这个例子中,我们只关心逐分钟数据的图表,并且我们将频率过高的波视为噪声,将频率过低的波视为我们想忽略的趋势。

我们不关心几个小时内发生的长期趋势。我们感兴趣的是逐分钟数据(由作者合成生成的原始数据)

我们关心的是这个时间范围内的趋势

我们不关心振荡过快的波,这些被视为信号中的噪声
所以,在这个例子中,我们只关心观察振荡频率低于每秒一次,并且高于每 5 分钟一次的内容。我们可以将数据转换到频域,去除所有不感兴趣的频率,然后转换回时间域,以便仅包括我们感兴趣的趋势来可视化波形。
首先,让我们观察未更改的频域:
"""
Plotting the entire frequency domain spectrogram for the mock electrical data
"""
#load electrical data, which is a numpy list of values taken at 1000Hz sampling frequency
x, y = load_electrical_data()
samplerate = 1000
N = len(y)
#calculating the period, which is the amount of time between samples
T = 1/samplerate
#calculating the amplitudes and frequencies using fft
yf = fft(y)
xf = fftfreq(N, T)[:N//2]
#creating wide figure
plt.figure(figsize=(18,6))
#plotting only frequency and amplitude for the 1st channel
plt.plot(xf, 2.0/N * np.abs(yf[0:N//2]))
#marking units of the two axis
plt.xlabel('fq (Frequency in Hz)')
plt.ylabel('V (Volts)')
#setting the vertical axis as logorithmic, for better visualization
plt.gca().set_yscale('log')
#rendering
plt.show()

这是我们正在分析的电气系统的完整未过滤频谱图
我们可以将所有我们不感兴趣的频率内容设置为零。通常你会使用像巴特沃斯滤波器这样的特殊滤波器来完成这个任务,但我们会保持简单。
"""
converting the data to the frequency domain, and filtering out
unwanted frequencies
"""
#defining low frequency cutoff
lowfq = 1/(5*60)
#defining high frequency cutoff
highfq = 1
#calculating the amplitudes and frequencies, preserving all information
#so the inverse fft can work
yf = fft(y)
xf = fftfreq(N, T)
#applying naiive filter, which will likely create some artifacts, but will
#filter out the data we don't want
yf[np.abs(xf) < lowfq] = 0
yf[np.abs(xf) > highfq] = 0
#creating wide figure
plt.figure(figsize=(18,6))
#plotting only frequency and amplitude
plt.plot(xf[:N//2], 2.0/N * np.abs(yf[0:N//2]))
#marking units of the two axis
plt.xlabel('fq (Frequency in Hz)')
plt.ylabel('V (Volts)')
#setting the vertical axis as logorithmic, for better visualization
plt.gca().set_yscale('log')
#zooming into the frequency range we care about
plt.xlim([-0.1, 1.1])
#rendering
plt.show()

我们所隔离的频域图,其它频率信息都设置为零
现在我们可以进行逆快速傅里叶变换,以重构我们关心的数据波形
"""
Reconstructing the wave with the filtered frequency information
"""
#importing dependencies
from scipy.fft import ifft #for computing the inverse fourier transform
#computing the inverse fourier transform
y_filt = ifft(yf)
#creating wide figure
plt.figure(figsize=(18,6))
#plotting
plt.plot(x,y_filt)
#defining x and y axis
plt.xlabel('t (seconds)')
plt.ylabel('V (volts)')
#looking at a few minutes of data, not looking at
#the beginning or end of the data to avoid filtration artifacts
plt.xlim([60*2,60*10])
#rendering
plt.show()

几分钟的数据,启用了我们的滤波器。我们已经去除了过高频率的内容,并通过去除过低频率的内容将波形集中在 0 附近。
就这样。我们成功地去除了我们不关心的高频信息,并通过去除低频趋势将我们关心的数据集中在零附近。我们现在可以使用这逐分钟数据来深入了解我们正在测量的电气系统。
2.2) 振动分析(高级)
(返回目录)
我在之前的例子中已经介绍了振动分析,以分析声波的形式。在这个例子中,我将讨论分析物理系统中的振动,比如工厂中的电机。
预测某些电机何时需要维护可能是困难的。通常,像对齐不当这样简单的问题会发展成更严重的问题,例如完全的发动机故障。我们可以使用周期性收集的频率记录来帮助我们了解电机是否以不同的方式运行;从而让我们在问题变成更大的问题之前对发动机进行诊断。

在发动机经历轻微故障的期间采集的振动数据。在时间域中,几乎不可能看到故障发生的时间。(原数据由作者合成生成)
为了分析这些数据,我们将计算并呈现所谓的梅尔频谱图。梅尔频谱图就像普通的频谱图,但不是计算整个波形的频率内容,而是从信号中提取小的滚动窗口中的频率内容。这使我们能够绘制频率内容随时间变化的情况。
"""
plotting a mel-spectrogram of motor vibration to diagnose the point of failure
note: if you don't want to use librosa, you can construct a mel-spectrogram
easily using scipy's fft function across a rolling window, allowing for more
granular calculation, and matplotlib's imshow function for more granular
rendering
"""
#importing dependencies
import librosa #for calculating the mel-spectrogram
import librosa.display #for plotting the mel spectrogram
#loading the motor data
y = load_motor_data()
samplerate = 1000 #in Hz
#calculating the mel spectrogram, as per the librosa documentation
D = np.abs(librosa.stft(y))**2
S = librosa.feature.melspectrogram(S=D, sr=samplerate)
#creating wide figure
fig = plt.figure(figsize=(18,6))
#plotting the mel spectrogram
ax = fig.subplots()
S_dB = librosa.power_to_db(S, ref=np.max)
img = librosa.display.specshow(S_dB, x_axis='time',
y_axis='mel', sr=samplerate,
fmax=8000, ax=ax)
fig.colorbar(img, ax=ax, format='%+2.0f dB')
ax.set(title='Mel-frequency spectrogram')
#rendering
plt.show()

电机数据的梅尔频谱图。与二维频谱图不同,梅尔频谱图是三维的:垂直轴是振荡频率,x 轴是时间(在这种情况下是时间的百分比),颜色代表幅度,以分贝为单位。注意在时间 0.2 时,电机的频率内容突然发生变化。
在梅尔频谱图中,每个垂直切片代表一个时间区域,高频内容显示在图上方,低频内容显示在图下方。很容易看出在时间 0.2(数据的 20%处),频率内容发生了剧烈变化。在这一点上,一个平衡重物变松,导致发动机失衡。在此时进行维护可能会避免未来的过度磨损。
一种简单而有效地应用这一原理的方法是通过定期振动读数。工人将一个加速度计通过磁铁固定在电机的机体上,并每月记录一次或两次频率内容。这些振动数据窗口随后被转换到频率域,在那里提取某些关键特征。一个常见的频率域提取特征是功率谱密度,实际上是某些频率区域下频率域曲线的面积。提取的特征可以绘制在几周的记录上,并用作整体电机健康状况的代理。
3) 频率域的高级应用
3.1) 数据增强(高级)
(返回目录)
数据增强是从真实数据中创建假数据的过程。一个典型的例子是图像分类,以增强一个用于分类图像是否是狗还是猫的数据集。

图像增强的示例,其中单张图像可以用来生成多个图像供机器学习模型学习。由 Affinity Designer 2 创建,图片来自 storyblocks.com
增强可以是一个极其强大的工具,但如果你没有图像怎么办?如果你有声音、运动、温度或其他信号呢?如何合理地增强这些类型的数据?在时间域中,增强策略更像是正则化策略:在这里添加一点噪声,在那里上下移动数据。它们向数据中添加随机信息,这可能有用,但实际上并不会生成新的示例。
我们可以借鉴音乐制作场景中的一个概念:波形表。波形表的思想是将两个波形转换到频域,在频域中进行插值,然后将插值转换回时间域。我并不是指混合,将一个信号叠加在另一个信号上,而是创建一个完全新的波形,其中包含两个(或更多)其他波形的频率内容。
让我们假设我们正在尝试构建一个模型来检测音频片段中是否有人在说话。我们有一堆包含人们说话的音频样本,还有一堆没有人说话的样本,且都在各种不同的情况下。这些数据需要有人带着不同的麦克风去捕捉声音,然后手动标记数据是否包含有人说话,且涉及各种情况。假设模型必须非常稳健和准确,而录制足够的数据以达到所需的性能水平在财务上是不可行的。
理论上,使人类语音听起来像这样的是频率内容。将一个谈话片段和另一个谈话片段的频率内容混合起来,应该仍然听起来像有人在说话。我们可以使用波形表来构造这些人工波形,从而免费生成更多数据(除了数据科学家的薪水和云端昂贵的计算资源)。
"""
loading and plotting two waveforms recorded in two seperate environments,
both including people talking
"""
#loading two waveforms
samplerate, y1 = wavfile.read('crowd.wav')
_, y2 = wavfile.read('citycenter.wav')
#creating x axis for both waveforms
N = 1000000
x1 = np.linspace(start = 0, stop = N/samplerate, num = N)
x2 = np.linspace(start = 0, stop = N/samplerate, num = N)
#creating wide figure
plt.figure(figsize=(18,6))
offset = 1000000
#plotting waveform 1
plt.subplot(2, 1, 1)
plt.plot(x1,y1[offset:offset+N])
plt.xlabel('t (seconds)')
plt.ylabel('A (db)')
#plotting waveform 2
plt.subplot(2, 1, 2)
plt.plot(x2, y2[offset:offset+N])
plt.xlabel('t (seconds)')
plt.ylabel('A (db)')
#rendering
plt.show()

两个波形,均明显包含人们说话的声音。(原始数据来自 storyblocks.com)
我们可以将这两个波形转换到频域,并创建几个在这两个波形之间的频率表示。
"""
Converting both waves to the frequency domain, constructing a wave table, and rendering the wave table
"""
#calculating the frequency content for both waves.
#Only analyzing 1 of the 2 stereo channels
fq1 = fft(y1[offset:offset+N,0])
fq2 = fft(y2[offset:offset+N,0])
#defining frequency axis
T = 1/samplerate
xf = fftfreq(N, T)
#creating wide figure
fig = plt.figure(figsize=(18,6))
ax = fig.add_subplot(111, projection='3d')
#plotting source waves
plt.plot(xf[:N//2], np.array([1]*(N//2)), 2.0/N * np.abs(fq1[0:N//2]))
plt.plot(xf[:N//2], np.array([0]*(N//2)), 2.0/N * np.abs(fq2[0:N//2]))
fq_interp = []
#creating interpolations
for per in np.linspace(0.1,0.9,9):
thisfq = (fq1*per) + (fq2*(1-per))
fq_interp.append((per, thisfq))
#plotting interpolation
plt.plot(xf[:N//2], np.array([per]*(N//2)), 2.0/N * np.abs(thisfq[0:N//2]))
plt.show()

原始波形(在极端位置)和中间波形的频谱图。请注意,图表显示的频谱图为频率与振幅,但插值也在相位上进行。
现在我们可以对所有这些插值的频域计算逆快速傅里叶变换,并提取我们的波形表。
"""
Computing the inverse fft on the frequency content, and constructing the final table of waves
"""
#creating wide figure
fig = plt.figure(figsize=(18,6))
ax = fig.add_subplot(111, projection='3d')
plt.plot(x, np.array([1]*len(x)), y1[offset:offset+N,0])
plt.plot(x, np.array([0]*len(x)), y2[offset:offset+N,0])
#creating interpolations
for per, interp in fq_interp:
waveform = ifft(interp)
plt.plot(x, np.array([per]*len(x)), waveform)
plt.show()

最终的波形表。极端的波形是源波,而中间的波形是在频域中的插值。
就这样。从 2 个讨论波形,我们现在有了 10 个讨论波形。数据增强可能是一个棘手的任务,因为你可能会创建不实际反映你试图模拟的数据的内容。当使用类似的增强策略时,你可以使用更接近源波形的增强(80% 一个波形,20% 另一个)。这些比接近中心的波形(50%,50%)更可能是现实的。
3.2) 嵌入和聚类(高级)
(返回目录)
对于这个示例,我们将使用情感分析模型的输出,基于客户情感随时间的变化对不同产品进行聚类。假设我们运营一家有评论的商店,这些评论在正面和负面之间波动。我们注意到一些评论彼此相关。我们想要找到具有相似情感分析趋势的产品,以便将它们分组在一起并进一步理解。
首先,让我们看看我们的数据:
"""
loading 1000 average sentiment scores over the course of a year,
and plotting the first 10 of them
"""
#loading sentiment data
sentiments = load_sentiments()
#creating wide figure
plt.figure(figsize=(18,6))
#plotting first 10 sentiments
for i in range(10):
plt.plot(sentiments[i])
#rendering
plt.xlabel('days')
plt.ylabel('sentiment (low to high)')
plt.show()

前 10 个情感示例(数据由作者合成生成)
如你所见,我们有许多用户情感的示例,按天平均。我们可以去除非常低频的内容,这将去除非常长期的平均趋势(如平均值),同时去除非常高频的内容,这些是噪声,不太可能产生有用的聚类。
"""
Converting to the frequency domain, removing very low and high frequency content, and plotting the results
We do this, so we can visually understand the frequency content which we deem important, before we begin clustering.
"""
#importing dependencies
from scipy.fft import fft, ifft #for computing frequency information
#creating wide figure
plt.figure(figsize=(18,6))
#defining the low frequency and high frequency cutoffs
#because lowfq is so low, it effectively only cuts of the wave
#with a frequency of zero, which controls the vertical offset of the data
lowfq = 0.0001
highfq = 0.05
#plotting first 10 sentiments
for i in range(10):
#getting signal
sig = sentiments[i]
#calculating the frequency domain
yf = fft(sig)
T = 1
N = len(sig)
xf = fftfreq(N, T)
#applying naiive filter
yf[np.abs(xf) < lowfq] = 0
yf[np.abs(xf) > highfq] = 0
#converting back to the time domain, and plotting
y = ifft(yf)
plt.plot(y)
#rendering
plt.show()

最终,我们将会在频域中对数据进行聚类。我们生成这个图表只是为了确认我们是否保留了我们关心的内容类型:既不过于低频,也不过于高频。
现在我们完成了时间域的处理,将开始在频域中构建我们的聚类。让我们看看我们筛选后的频域图。

用于构建上述波形的频率内容
我们的聚类操作的输入将是一个振幅列表,每个振幅对应一个特定的频率。我们可以将这些数据输入到我们的聚类算法中,但还有一个额外的步骤可以带来显著的改进。想象一下,我们试图聚类四个简单的正弦波,其频域内容如下:

四个正弦波的频域表示,用于演示目的
你会期望左边的波形聚集在一起,右边的波形也紧密聚集在一起。然而,描述这些数据的向量看起来是这样的:
[0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0]
[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0]
[0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0]
[0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0]
从 t-SNE 的角度来看,这些波形都是彼此正交的,因为它们没有沿相似的轴共享任何值。我们可以通过使频域“模糊”来解决这个问题;我们可以对这些数据应用移动平均,使频率内容与邻近区域融合。

我们的样本数据,应用了双向指数移动平均,导致相似的频率内容互相渗透。
这些数据更有可能产生良好的聚类结果,因为相似的频率内容尖峰更容易互相渗透。让我们将这一概念应用到我们的情感数据样本图中:
"""
Converting data to the frequency domain, and applying an exponential
moving average in both directions. This is the data we will be clustering.
"""
#converting data to frequencies, and filtering out content
wfs = [np.abs(fft(y)[0:N//2][1:20]) for y in sentiments[:10]]
#loading sample data into a pandas dataframe
df = pd.DataFrame(wfs).T
#applying an exponential moving average in both directions, and adding them
df_plt = df.iloc[::-1].ewm(span=3, adjust=False).mean().iloc[::-1]
df_plt = df.ewm(span=3, adjust=False).mean().add(df_plt)
#creating wide figure
plt.figure(figsize=(18,6))
plt.plot(df_plt)

经过滤波的频率数据,用于聚类。请记住,可以对这种一般方法进行许多更改。可以使用不同的高频和低频,可以使用不同范围的指数平均,可以对频率域进行归一化,以使相对幅度相似,等等。
由于我们的处理步骤,这些数据更有可能创建我们实际关心的数据簇。现在我们可以将所有这些整合在一起,创建我们的最终簇:
"""
Converting all the sentiment waveforms to the frequency domain,
applying filtration, and embedding in 2d with TSNE
"""
#importing dependencies
from sklearn.manifold import TSNE
#converting data to frequencies, and filtering out content, for al product sentiment waveforms
wfs = [np.abs(fft(y)[0:N//2][1:20]) for y in sentiments]
#loading sample data into a pandas dataframe
df = pd.DataFrame(wfs).T
#applying an exponential moving average in both directions, and adding them
df_plt = df.iloc[::-1].ewm(span=3, adjust=False).mean().iloc[::-1]
df_plt = df.ewm(span=3, adjust=False).mean().add(df_plt)
#creating wide figure
plt.figure(figsize=(18,6))
#embedding the data
embedding = TSNE(n_components=2 ,init='random', perplexity=20).fit_transform(df_plt.values.T)
#plotting
plt.scatter(embedding[:,0],embedding[:,1])

t-SNE 图,展示了所有用户情感产品评论的过滤频率域。
就这样!自然地,对于实际应用,生成此图表后还需要做大量工作。这些数据云很可能需要被探索,并可能需要标记,还需要进一步调整关键参数以获得更多洞察。不过在这个例子中,我们使用频率域对时间序列数据应用了聚类算法,使我们能够看到哪些情感以相似的方式波动。这种类型的分析可以为网站内的产品推荐提供信息。
3.3) 压缩(中级)
(返回目录)
信号包含大量数据。在每秒采样 96,000 次的情况下,几个小时的录音会生成巨大的音频文件。这些原始录音对于高质量音频处理非常有用,但当你完成工作并想要将样本发送给朋友时,你愿意为了速度和大小牺牲一些音频质量。你可以降采样到一个点(每秒发送更少的样本),但是,这会限制你可以发送的频率的最大音调(如果你每秒只发送 200 个样本,就不能发送高于 100 Hz 的任何频率)。相反,你可以将样本转换到频率域,将相似频率压缩在一起,然后发送频率域和采样率。接收者可以通过从频率域到时间域的变换来重建压缩后的音频。这使你能够发送任意高的频率,而无需发送任意大的数据量。例如,mp3 文件之所以比 .wav 文件小得多,是因为它们在编码中显著使用了傅里叶变换。
4) 数据科学家的概念性收获
(返回目录)
直接使用频率分析作为工具对于解决某些问题可能至关重要,正如我们在之前的例子中所看到的。经常被忽视的是将频率域作为一个概念的使用。作为数据科学家,可能很难理解自相似建模策略,如递归网络和卷积网络,尤其是在解决具体且微妙的问题时。有时,将这些问题视为准频率域提取可能更为有用。
例如,卷积网络使用在数据上传播的波形(卷积)。结果随后被池化,降低了数据的分辨率,然后应用进一步的波形。你可以将卷积视为提取不同频率的信息,通常是从高频信息到低频信息。记住这一点可以对步幅、内核大小和其他超参数有更直观的理解。
5) 总结
(返回目录)
在这篇文章中,我们涵盖了频率域,它如何与信号和正弦波相关,并看到了一些频率域表示的例子。我们看到如何将时间序列信号转换为频率域,反之亦然,并看到通过转换到频率域,如何解决几类问题。
关注更多内容!
在未来的帖子中,我将描述频率域如何应用于更高维度的信号,如图像和视频,以及如何在机器学习/数据科学应用中发挥巨大作用。我还将描述几个机器学习领域的标志性论文,重点关注实用和直观的解释。
署名: 本文档中的所有图片均由丹尼尔·沃菲尔德(Daniel Warfield)创作。只要你引用本文或danielwarfield.dev,你可以将本文中的任何图片用于个人非商业目的。
附言——加入我在 RoundtableML 的行列
RoundtableML 是一个充满活力的社区,雄心勃勃且积极进取的个人聚集在一起,协作并推动机器学习和人工智能应用的边界,以安全和负责任的方式。如果你渴望扩展机器学习知识,参与开放研究,深入科学论文,并在小型亲密小组中进行机器学习项目——这里就是你的理想之地!
你可以通过这个 Discord 邀请加入。
使用 GPT 模型生成用于训练机器学习模型的文本数据
Python 中的逐步指南
·发表在 Towards Data Science ·9 min read·2023 年 7 月 12 日
--

图片由 Claudio Schwarz 提供,来源于 Unsplash
动机
数据对于构建机器学习模型至关重要,但文本数据的收集却面临以下困难:
-
开源文本数据集是有限的。隐私规则和商业机密通常限制了特权数据的分发。此外,公开的数据集可能不具备商业使用的许可证,或者更重要的是,可能不具有相关的上下文。例如,IMDB 的电影评论可能对分析客户对银行产品的情感没有意义。
-
机器学习模型通常需要大量的训练数据才能有效运作。公司,特别是初创公司,可能需要大量时间来收集可靠的文本数据。此外,这些数据可能没有针对特定机器学习任务标注响应变量。例如,一家公司可能收集了客户的投诉原文,但可能没有详细了解这些投诉的主题或情感。
我们如何克服上述限制,以可扩展且具有成本效益的方式生成适合目的的文本数据?鉴于近期在大型语言模型和生成型人工智能方面的进展,本文*提供了一个关于通过调用 OpenAI 的 GPT 模型套件 来生成合成文本数据的教程。
以演示为例,我们来探讨一个为保险公司生成客户投诉数据的用例。通过丰富的文本数据来训练语言模型,该用例是公司可能通过在自然语言理解任务中表现更好,例如将投诉分类到主题中或评分投诉者情感,从而实现更好的客户结果。
*这篇文章 100% 无 ChatGPT。
前提条件:设置 OpenAI API 密钥
为了调用 GPT 模型,只需注册 OpenAI 账户并在 用户设置 下访问 API 密钥。请务必保持此密钥的私密。
请注意,根据使用情况,访问 GPT 模型会有一定的 费用,尽管对我来说这并不重要(准备此教程的费用不到 $0.08 美元)。
第一步:加载所需的 Python 包
#!pip install openai if you haven't already, make sure you install
# and upgrade to the latest version
import os
import openai
import pandas as pd
import numpy as np
第二步:生成单个客户投诉
我们首先使用 text-davinci-003 模型在 GPT-3.5 模型套件下生成一个客户投诉数据点。
openai.api_key = "INSERT YOUR API KEY STRING HERE"
prompt_text = "You are a customer of a life insurance company. \
\n \nYou are extremely dissatisfied of the time it takes to assess your life insurance claim. It has been a horrible customer experience.\
\n \nWhat would you say in a complaint you are going to raise against the life insurance company?"
complaint = openai.Completion.create(
engine = "text-davinci-003",
prompt = prompt_text,
temperature = 0.6,
top_p = 1,
max_tokens = 1000,
frequency_penalty = 0,
presence_penalty = 0
)
print(complaint.choices[0].text)
请注意,关于上述代码:
-
你需要在代码的第一行插入你的私有 API 密钥字符串。
-
prompt_text 帮助 GPT 模型理解其角色,从而通过回应提示问题生成客户投诉。
-
代码中引用的其他参数(temperature, top_n, max_tokens, frequency_penalty 和 presence_penalty)在 这一部分 的文章中有更详细的解释。
上述代码生成的客户投诉是:
我写信是为了表达对我所购买的寿险公司极度的不满。评估我的寿险索赔所花费的时间不可接受,对我的客户体验造成了严重影响。我觉得我等待如此长时间才能收到对我索赔的回复是不可接受的。我敦促公司立即采取行动解决这一问题,并确保所有客户未来都能收到及时且令人满意的回应。
初看起来这似乎是合理且连贯的。
第三步:大规模生成客户投诉
你可能会争辩说,你可以通过在 ChatGPT 中输入 prompt_text 来简单地复制第二步。如果你只是想生成有限数量的数据点,你确实是对的。然而,对于大规模文本数据生成,手动在 ChatGPT 前端重复此操作完全不可行。那么,我们如何自动化这个任务(最终扩展生成客户投诉的操作)呢?答案在于对第二步代码的一个小调整。
由于 GPT 模型的设计以及 temperature 参数的性质决定了生成文本的创造力和多样性,因此第 2 步中每次运行代码都会生成不同的客户投诉。鉴于此,我们只需要设置一个循环来运行第 2 步中的代码 n 次,并存储每次运行的输出结果。
为了演示,下面的代码创建了一个循环,用于生成 n = 3 个投诉,并将输出存储在数据框中:
prompt_text = "You are a customer of a life insurance company. \
\n \nYou are extremely dissatisfied of the time it takes to assess your life insurance claim. It has been a horrible customer experience.\
\n \nWhat would you say in a complaint you are going to raise against the life insurance company?"
text_gen = []
for i in range(0, 3):
completion = openai.Completion.create(
engine = "text-davinci-003",
prompt = prompt_text,
max_tokens = 120,
temperature = 0.6,
#top_p = 1,
frequency_penalty = 0,
presence_penalty = 0
)
text_gen.append(completion.choices[0]['text'])
print('Generating complaints number %i'%(i))
text_gen
以下代码片段展示了生成的 3 条客户投诉。不用说,参数 n 可以设置为你选择的任意数字。

图像 1:生成的客户投诉。图片作者提供。
更高级的用例
零样本与少样本训练在 GPT 模型中的比较
根据上述用例向 GPT 模型提供独立的文本提示被视为零样本训练。通过零样本训练生成的文本数据可能会对特定任务略显通用。
在我们已经拥有有限但有意义的训练数据,并希望生成额外的训练数据以与现有数据相似的情况下,我们可以将 prompt_text 输入指向现有数据。这提供了 GPT 模型少样本训练。
例如,假设我们希望对 GPT 模型进行少样本训练,以生成一些类似于 IMDB 电影评论数据集的文本(假设你已将其存储在 df_imdb 变量中):
text_gen = []
for i in range(len(df_imdb)):
prompt_text = f"The following are movie reviews with a positive sentiment. REVIEW: {df_imdb[i]} REVIEW:"
review = openai.Completion.create(
engine = "text-davinci-003",
prompt = prompt_text,
temperature = 0.6,
top_p = 1,
max_tokens = 1000,
frequency_penalty = 0,
presence_penalty = 0
)
text_gen.append(review.choices[0].text)
请注意,这段代码还隐式地为生成的文本数据创建了标签。在这个实例中,我们正在生成‘正面’评论。具有‘负面’或‘中立’标签的文本数据也可以以类似的方式生成。这使我们能够获得带标签的数据集,而无需任何人工努力!
此外,这段代码展示了 GPT 模型能够通过识别文本提示中的模式生成数据,而无需通过问题进行引导。在这个实例中,它被训练以在给出“REVIEW:”提示后生成电影评论。
为了提高与现有数据的相似性,我们甚至可以用现有数据中的多个数据点进行少样本训练。可以通过更新prompt_text输入来轻松完成:
prompt_text =
f"The following are movie reviews with a positive sentiment. REVIEW: {df_imdb[i]} REVIEW: {df_imdb[i-1]} REVIEW:"
其他 GPT 模型
到目前为止,text-davinci-003 GPT 模型已被用来演示本文中的用例。其他 GPT 模型 也可以通过 engine 参数按需调用。例如,某些 GPT 模型如 gpt-3.5-turbo 比其他模型更强大,但在 Python 中可能需要不同的调用方式,因为它们接受的是‘对话’作为输入,而不是文本字符串。
以下代码展示了调用 gpt-3.5-turbo 模型生成投诉数据。
dialogue = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "My life insurance company won't let me reinstate my policy after a couple of dishonours?"},
{"role": "assistant", "content": "I'm sorry to hear that, is there anything I can help you with."},
{"role": "user", "content": "Yes, you can draft me a complaint directed at the life insurance company, in no more than 100 words"}
]
text_gen_chat = []
for j in range(0, 3):
res = openai.ChatCompletion.create(
model = "gpt-3.5-turbo",
messages = dialogue,
temperature = 0.6
)
text_gen_chat.append(res.choices[0].message.content)
print('Generating complaints data number %i'%(j))
text_gen_chat
输出结果:

图像 2:生成的客户投诉。图片作者提供。
风险
虽然上述用例展示了 GPT 模型(或更广义的大型语言模型)的即刻实施,但鉴于这些模型相对较新,用户可能需要对将这些模型与实际数据和工作方式整合所固有的已知和未知风险保持谨慎。特别是在生成用于训练机器学习模型的文本数据时,戴上我的风险和治理帽子,以下是我认为在实际实施技术时的关键(已知)风险:
-
关于通过 OpenAI 的 API 端点提供某些类型的输入给 GPT 模型,可能存在隐私问题。这在公司利用专有数据生成增强文本数据时尤其相关(如上文的高级用例讨论),特别是当输入内容如包含个人信息的客户投诉时。一种潜在的缓解措施是,在预处理步骤中去标识个人信息,以确保投诉数据以匿名方式使用。此外,使用这种技术的公司需要制定监管措施和政策,管理私人和敏感信息的使用。
-
对于某些类型的用例,GPT 模型生成的文本数据可能会引入偏见。例如,由于 GPT 模型是基于大量互联网上公开文本的语料库进行预训练的,因此在生成正面和负面客户反馈的任务中,它可能天然倾向于后者(即客户投诉),假设这些内容在互联网上更为突出。此外,生成的文本可能没有‘细致调整’到适应公司提供的特定产品特性,因为相关文本可能在互联网上不可用。这最终表现为在高效文本数据生成与数据可用性之间的权衡。
-
关于时效性,尽管预计 GPT 模型将继续获得更新,但在撰写本文时,大多数模型的训练数据基于截至 2021 年 9 月的互联网数据(即将近 2 年前)。
与大语言模型相关的其他已知风险,如幻觉,在此背景下较不相关。
结论
本文提供了一种实际的(和生成的)方法来解决获取用于训练机器学习模型的文本数据的一些实际约束。
退一步说,正确关注和处理风险的情况下,本文展示了用户如何从‘后端’访问 OpenAI 的 GPT 模型。这使用户能够解锁利用大型语言模型支持 ChatGPT 的商业机会,ChatGPT 最初是为个别临时用例设计的。
你是这些有关机器学习主题的实用教程的粉丝吗?随着我乘着人工智能/机器学习的浪潮,我喜欢用全面的语言编写和分享逐步指南和操作教程,并附有可直接运行的代码。如果你想访问我所有的文章(以及 Medium 上其他从业者/作者的文章),你可以通过 这个链接 进行注册!
使用 LangChain 的输出解析器与 ChatGPT 配合以获得结构化输出
原文:
towardsdatascience.com/use-langchains-output-parser-with-chatgpt-for-structured-outputs-cf536f692685
用一个示例用例进行解释。
·发表于 Towards Data Science ·6 分钟阅读·2023 年 6 月 6 日
--

图片由 Dmitry Ratushny 提供,来源于 Unsplash
ChatGPT 和许多其他 LLM 在创建基于 LLM 的应用程序方面引领了潮流。这些模型在处理文本输入和根据你的查询生成文本输出方面非常强大。然而,它们并不是作为开发框架而设计的。
LangChain 是一个开源开发框架,用于使用大型语言模型 (LLMs) 的应用程序。它提供了以组件形式的抽象,帮助以更高效或程序化的方式使用 LLM。
这些组件包括:
-
模型:ChatGPT 或其他 LLM
-
提示:提示模板和输出解析器
-
索引:摄取外部数据,如文档加载器和向量存储
-
链:组合组件以创建端到端的用例。一个简单链的示例可以是 提示 + LLM + 输出解析器
-
代理:使 LLM 使用外部工具
LangChain 的主要思想是将多个组件串联在一起,以扩展 LLM 的功能并创建更实用的工具或应用程序。

(图像由作者提供)
LangChain 的开发者不断以非常快速的速度添加新功能。这改变了我们与 LLM 交互的方式。
在本文中,我们将通过一个示例用例演示如何使用输出解析器和提示模板帮助从 LLM 中获取更结构化的输出。
我们将首先使用仅一个提示模板和 LLM 进行示例。然后,我们将添加一个输出解析器来做同样的示例。
提示模板 + LLM
提示模板和 LLM 是你可以使用 LangChain 创建的最简单的链。
使用提示模板相比于手动自定义带有 f-strings 的提示有许多优势。它允许在适用时重用提示。此外,LangChain 提供了可直接使用的模板,适用于查询数据库等常见任务。
我们将使用 OpenAI 的 ChatGPT 作为我们的 LLM,因此我们需要设置一个 API 密钥。
import os
import openai
from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv())
openai.api_key = os.environ['OPENAI_API_KEY']
为了使这段代码正常工作并设置 API 密钥,你需要创建一个名为 OPENAI_API_KEY 的环境变量,该变量保存你从 API Keys 菜单中获得的 API 密钥。
让我们开始创建一个模型。ChatOpenAI 是 LangChain 对 ChatGPT API 端点的抽象。
from langchain.chat_models import ChatOpenAI
chat = ChatOpenAI(temperature=0.0)
默认情况下,LangChain 创建的聊天模型的温度值为 0.7。temperature 参数用于调整输出的随机性。像 0.7 这样的较高值会使输出更加随机,而像 0.2 这样的较低值则会使输出更加集中和确定。我们可以在创建模型实例时设置其值。
下一步是创建提示模板。我们将创建一个用于从产品评论中提取信息的模板。
review_template = """\
For the following review, extract the following information:
recommended: Does the buyer recommend the product? \
Answer True if yes, False if not or unknown.
delivery_days: How many days did it take for the product \
to arrive? If this information is not found, output -1.
setup: Extract any sentences about the setup of the product.
Format the output as JSON with the following keys:
recommended
delivery_days
setup
review: {review}
"""
from langchain.prompts import ChatPromptTemplate
prompt_template = ChatPromptTemplate.from_template(review_template)
上面的代码片段从给定的提示字符串中创建了一个提示模板。评论作为输入变量保存,可以通过 input_variables 属性进行检查:
prompt_template.input_variables
# output
['review']
现在我们可以使用这个模板和产品评价创建一个实际的提示。
product_review = """
I got this product to plug my internet based phone for work from home (Avaya desktop phone).
It works! It arrived in 5 days, which was earlier than the estimated delivery date.
The setup was EXTREMELY easy. At completion, I plugged the phone into the
extender's ethernet port and made a few phone calls which all worked perfectly with
complete clarity. VERY happy with this purchase since a cordless headset is
around $250 (which I would have needed since the phone had to be at the ethernet
port on the wall). I recommend this product!
"""
messages = prompt_template.format_messages(review=product_review)
messages 是一个 Python 列表,包含实际的提示。我们可以通过 messages[0].content 查看提示,它会输出以下提示:
对于以下评论,提取以下信息:
recommended: 买家是否推荐该产品。如果是,则回答 True;如果不是或未知,则回答 False。
delivery_days: 产品到达用了多少天?如果没有找到此信息,输出 -1。
setup: 提取关于产品设置的任何句子。
将输出格式化为 JSON,键包括:recommended delivery_days setup
review: 我购买了这个产品来连接我的互联网电话用于在家工作(Avaya 桌面电话)。它有效!它在 5 天内到达,比预计的交货日期还要早。设置非常简单。完成后,我将电话插入扩展器的以太网端口,并进行了几次电话,所有电话都完全清晰地正常工作。对这次购买非常满意,因为一个无线耳机大约 $250(我本来需要的,因为电话必须放在墙上的以太网端口上)。我推荐这个产品!
我们已经准备好了模型和提示。下一步是使用提示查询模型:
# chat is the model and messages is the prompt
response = chat(messages)
print(response.content)
# output
{
"recommended": true,
"delivery_days": 5,
"setup": "The setup was EXTREMELY easy."
}
尽管 response 看起来像 JSON,但它是一个字符串,这使得解析变得困难。
type(response.content)
# output
str
我们现在将学习如何将输出解析器与提示模板一起使用,以便更容易解析输出。
提示模板 + LLM + 输出解析器
通过使用 format_instructions 将输出解析器添加到提示中。让我们逐步了解这个过程。
第一步是导入所需的模块,并为每个需要提取的信息定义一个ResponseSchema:
from langchain.output_parsers import ResponseSchema
from langchain.output_parsers import StructuredOutputParser
recommendation_schema = ResponseSchema(
name="recommended",
description="Does the buyer recommend the product? \
Answer True if yes, False if not or unknown."
)
delivery_days_schema = ResponseSchema(
name="delivery_days",
description="How many days did it take for the product to arrive? \
If this information is not found,output -1."
)
setup_schema = ResponseSchema(
name="setup",
description="Extract any sentences about the setup of the product."
)
response_schemas = [
recommendation_schema,
delivery_days_schema,
setup_schema
]
下一步是创建输出解析器并使用这些模式来格式化指令:
output_parser = StructuredOutputParser.from_response_schemas(response_schemas)
format_instructions = output_parser.get_format_instructions()
我们将像之前一样创建提示模板。在创建实际的提示时,我们将传入format_instructions参数:
prompt_template = ChatPromptTemplate.from_template(template=review_template)
messages = prompt_template.format_messages(
review=product_review,
format_instructions=format_instructions
)
让我们使用我们新的提示来查询模型。
response = chat(messages)
output_dict = output_parser.parse(response.content)
print(output_dict)
# output
{'recommended': 'True', 'delivery_days': '5', 'setup': 'The setup was EXTREMELY easy.'}
我们使用了parse方法来解析输出。output_dict的类型是字典,比字符串更容易解析。我们可以使用get方法提取特定的信息。
output_dict.get("delivery_days")
# output
5
最后的话
你可能会争辩说,我们可以使用内置的json模块将字符串解析为 JSON 文件,并且使用loads方法是一个简单的过程。你说得对!这比创建一个输出解析器并将其实现到提示模板中要简单。
然而,还有一些更复杂的情况,其中输出解析器简化了这个过程,而这种简化是内置的json模块无法做到的。此外,在处理包含不同类型模块的较长链时,输出解析器还提供了额外的好处。
你可以成为 Medium 会员 以解锁我所有的写作内容,并且可以访问 Medium 上的其他内容。如果你已经是会员了,别忘了 订阅 ,如果你希望在我发布新文章时收到电子邮件通知。
感谢阅读。如果你有任何反馈,请告诉我。
使用 PyGWalker 提升你的 Jupyter Notebook EDA 体验
原文:
towardsdatascience.com/use-pygwalker-to-enhance-your-jupyter-notebook-eda-experience-d7a5ee358be9
PyGWalker 库简介,便于数据可视化
·发表于 Towards Data Science ·阅读时间 8 分钟·2023 年 3 月 1 日
--

PyGWalker 在同一视图中显示多个图表。图片由作者提供。
快速高效地创建有效且引人注目的数据可视化是数据科学工作流程的关键部分。有多种选项可以实现这一点,从商业软件如 Tableau 到免费的替代方案如专用的 Python 库。生成图表所需的技能和时间因选项而异。
多年来,已经开发了几种 Python 库来简化数据探索过程。事实上,这些库如此简单,以至于你只需 3–5 行代码即可开始使用。
最近出现在 EDA 场景中的一个库是 PyGWalker。
PyGWalker (Python 绑定的 Graphic Walker) 是一个 Python 库,能够帮助加速数据分析和可视化工作流程,直接在 Jupyter notebook 中进行。它通过提供类似于流行数据分析软件 Tableau 的界面来利用交互性的强大功能。

使用井孔数据在 PygWalker 中创建散点图。图片由作者提供。
使用这种类型的界面,我们可以将变量拖放到特定部分,快速创建图表、筛选图表并理解数据。
你可以使用下面的链接访问 PyGWalker 的 GitHub 存储库。
## GitHub - Kanaries/pygwalker: PyGWalker: 将你的 pandas 数据框转换为 Tableau 风格的用户界面…
PyGWalker 可以简化你的 Jupyter Notebook 数据分析和数据可视化工作流程,通过将 pandas 数据框转换为 Tableau 风格的界面来实现。
本文将探讨 PyGWalker 的一些功能,使用的是我最喜欢的井筒数据集之一(详细信息见文章末尾)。
在撰写本文时,PygWalker 的版本是 0.1.4.6,其中一些功能可能已在此版本之后进行了更新。
视频教程
如果你想查看 PyGWalker 的实际操作,可以在我的 YouTube 频道 上找到该教程的视频版本。
在 Jupyter Lab 中安装和运行 PyGWalker
要开始使用 PyGWalker,我们需要先安装它。我们可以使用 pip install pygwalker 或 conda install pygwalker(如果你使用 Anaconda)。
安装 PyGWalker 库后,我们可以打开 Jupyter Notebook,然后导入 PyGWalker 和 pandas 库,后者将用于从 CSV 文件中加载数据。
import pandas as pd
import pygwalker as pyg
在这些导入之后,下一步是加载我们将用于本教程的数据。我们可以通过调用 pandas 中熟悉的 pd.read_csv() 函数来加载数据,然后传入我们的 CSV 文件。
df = pd.read_csv('Data/Xeek_Well_15-9-15.csv')
现在是运行 PyGWalker 的时候了,我们可以使用以下简单的调用来实现。
pyg.walk(df)
一旦单元格运行完毕,我们将得到一个非常好的界面,显示数据集中可用的变量。变量将根据其类型进行分组。

PyGWalker 用户界面直接在 Jupyter Notebook 中。图片由作者提供。
使用 PyGWalker 构建散点图
我们将创建的第一个图是 RHOB 和 NPHI 的简单散点图——这是岩石物理学中常用的图。
在此之前,我们需要在工具栏上关闭聚合。这将允许我们绘制实际的数据值,而不是任何形式的聚合。

关闭聚合选项将允许绘制实际的数据值。图片由作者提供。
现在我们可以从左侧的字段列表中选择我们想要绘制的变量。根据你想将变量放在 x 轴还是 y 轴,可以点击并拖动到相应的轴框中。
你还会注意到字段列表中的项目有不同的图标。蓝色文档图标代表分类数据,紫色井号代表数值数据。
在这个例子中,我将 NPHI 放在 x 轴上,将 RHOB 放在 y 轴上。

使用井筒数据在 PygWalker 中创建散点图。图片由作者提供。
更改 PyGWalker 图表大小
当图表出现时,它可能看起来很小。然而,我们可以很容易地增加图表的大小。方法是通过菜单将布局模式从自动更改为固定。
一旦选项更改,我们可以通过点击出现在图周围的蓝色边框来更改大小,或者点击“布局模式”按钮旁边的齿轮图标并调整滑块。

在 PygWalker 中更改图形大小。图片来自作者。
向 PyGWalker 散点图添加额外的变量
我们还可以向图中添加更多变量,以帮助我们更好地理解数据。这些额外的变量可以是分类的也可以是数值的,我们可以使用它们来添加颜色、不透明度、大小和形状。
在下面的示例中,我添加了 LITH 变量,这将根据不同的岩性给数据点上色。然后我们可以将鼠标悬停在散点图中的任何一点上,查看其值。

将分类变量应用于 PyGWalker 创建的散点图。图片来自作者。
如果我们改用数字变量,我们将在图的侧边看到一个颜色条。这个轴的值范围可以通过应用过滤器来更改——我们很快就会看到如何操作。

将数字变量应用于 PyGWalker 创建的散点图。图片来自作者。
缩放和移动 PygWalker 散点图
如果我们想要更改比例或放大数据的某个部分,我们首先需要点击工具栏上的“自动调整大小”按钮,然后可以使用鼠标滚轮进行放大或缩小。
然后,我们可以通过使用左键点击、按住该按钮并拖动光标在图中移动。

在 PyGWalker 中缩放和移动散点图。图片来自作者。
如果能像在 Plotly 图表中那样,通过点击轴或轴的角来手动更改图上的刻度,将是一个很好的功能。
按类别过滤数据
我们还可以使用我们的变量过滤数据。
当我们使用分类数据进行过滤时,我们点击并拖动我们想要的变量到过滤器区域,然后取消选择我们不想看到的类别。

在 PygWalker 中应用分类过滤器。图片来自作者。
使用数字变量进行过滤时,我们会得到一个滑块,可以控制最小值和最大值范围。看起来我们不能手动编辑这些值,这将是一个很好的功能。
一个不错的功能是,我们可以通过将另一个变量添加到过滤框中并设置范围或选择我们想要的类别,来应用多个过滤器。

使用 PygWalker 对数据应用数值过滤器。图片来自作者。
使用 PyGWalker 创建折线图
PyGWalker 中有几种不同的图表类型。
当添加数据且标记类型设置为自动时,库将尝试为你的数据找到最佳图表。然而,这可能并不总是最合适的图表类型。
你可以通过点击工具栏上的标记类型按钮,然后选择你想要的类型来更改图表类型。
下面的示例展示了如何用两个变量创建折线图。

在 PyGWalker 中创建折线图。图片由作者提供。
查看原始数据
PyGWalker 提供了一种方便的方式来查看数据框中的原始数据,并在需要时更改数据类型。如果某列被错误识别为不正确的数据类型,你可以快速更改它。
如果数据视图中能够进行更多操作,例如过滤数据或对列应用颜色比例尺,那就太好了,因为有时这有助于揭示数据中的问题。

PyGWalker 中的原始数据视图。图片由作者提供。
摘要
PyGWalker 提供了我在 Jupyter notebook 中遇到的最互动、外观最好的 EDA 库体验之一。该界面为非编码者或初学者提供了一种立即开始创建图表的简便方法。
你应该在下一个项目中尝试一下。查看我下面的文章,如果你想了解其他强大的 Python EDA 库。
## 5 个强大的 Python 库,你需要了解的 EDA 工具
利用 Python 的力量探索和理解你的数据
towardsdatascience.com
本文使用的数据集是 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 并行下载多个文件(或 URL)
在更短时间内获取更多数据
·
关注 发布于 Towards Data Science ·5 min read·2023 年 9 月 8 日
--
图片来源于 Wesley Tingey 在 Unsplash
我们生活在一个大数据的世界里。大数据通常以多个小数据集(即由多个文件组成的大数据集)的形式组织。获取这些数据通常会因为下载(或获取负担)而令人沮丧。幸运的是,通过一些代码,可以自动化并加快文件下载和获取的过程。
自动化文件下载可以节省大量时间。有几种方法可以使用 Python 自动化文件下载。下载文件的最简单方法是使用一个简单的 Python 循环来遍历 URL 列表。这种串行方法适用于少量小文件,但如果你要下载很多文件或大文件,你会希望使用并行方法来最大化计算资源。
使用并行文件下载例程,你可以更好地利用计算机资源来同时下载多个文件,从而节省时间。本教程演示了如何在 Python 中开发通用文件下载函数,并应用于串行和并行方式下载多个文件。本教程中的代码仅使用 Python 标准库中提供的模块,因此无需安装其他模块。
导入模块
对于这个示例,我们只需要 requests 和 multiprocessing Python 模块来并行下载文件。requests 和 multiprocessing 模块都可以从 Python 标准库中获取,因此你不需要进行任何安装。
我们还将导入 time 模块来跟踪下载单个文件所需的时间,并比较串行和并行下载例程之间的性能。time 模块也是 Python 标准库的一部分。
import requests import time from multiprocessing import cpu_count from multiprocessing.pool import ThreadPool
定义 URL 和文件名
我将使用 gridMET NetCDF 文件来演示如何在 Python 中并行下载文件,这些文件包含了美国的每日降水数据。
在这里,我在列表中指定了四个文件的 URL。在其他应用中,你可以通过编程方式生成一个要下载的文件列表。
urls = ['https://www.northwestknowledge.net/metdata/data/pr_1979.nc', 'https://www.northwestknowledge.net/metdata/data/pr_1980.nc', 'https://www.northwestknowledge.net/metdata/data/pr_1981.nc', 'https://www.northwestknowledge.net/metdata/data/pr_1982.nc']
每个 URL 必须与其下载位置相关联。在这里,我将文件下载到 Windows 的‘Downloads’目录。我在列表中硬编码了文件名,以便简化和透明。根据你的应用程序,你可能想编写代码来解析输入的 URL 并将其下载到特定目录。
fns = [r'C:\Users\konrad\Downloads\pr_1979.nc', r'C:\Users\konrad\Downloads\pr_1980.nc', r'C:\Users\konrad\Downloads\pr_1981.nc', r'C:\Users\konrad\Downloads\pr_1982.nc']
多进程要求并行函数只接受一个参数(有一些解决方法,但我们在这里不讨论)。为了下载一个文件,我们需要传递两个参数,一个是 URL,另一个是文件名。因此,我们将 urls 和 fns 列表打包成一个元组列表。列表中的每个元组将包含两个元素;一个 URL 和该 URL 的下载文件名。这样我们就可以传递一个包含两个信息的单一参数(元组)。
inputs = zip(urls, fns)
下载 URL 的函数
现在我们已经指定了下载的 URL 以及它们关联的文件名,我们需要一个函数来下载这些 URL(download_url)。
我们将向 download_url 传递一个参数(arg)。这个参数将是一个可迭代对象(列表或元组),其中第一个元素是要下载的 URL(url),第二个元素是文件名(fn)。这些元素被分配给变量(url 和 fn)以提高可读性。
现在创建一个 try 语句,其中在文件创建后检索 URL 并将其写入文件。当文件写入时,将返回 URL 和下载时间。如果发生异常,则会打印一条消息。
download_url 函数是我们代码的核心。它完成实际的下载和文件创建工作。我们现在可以使用这个函数来串行(使用循环)和并行下载文件。让我们看一下这些示例。
def download_url(args):
t0 = time.time()
url, fn = args[0], args[1]
try:
r = requests.get(url)
with open(fn, 'wb') as f:
f.write(r.content)
return(url, time.time() - t0)
except Exception as e:
print('Exception in download_url():', e)
使用 Python 循环下载多个文件
要将 URL 列表下载到相关文件,请遍历我们创建的可迭代对象(inputs),将每个元素传递给 download_url。每次下载完成后,我们将打印下载的 URL 及下载所需的时间。
下载所有 URL 的总时间将在所有下载完成后打印。
t0 = time.time()
for i in inputs:
result = download_url(i)
print('url:', result[0], 'time:', result[1])
print('Total time:', time.time() - t0)
输出:
url: https://www.northwestknowledge.net/metdata/data/pr_1979.nc time: 16.381176710128784
url: https://www.northwestknowledge.net/metdata/data/pr_1980.nc time: 11.475878953933716
url: https://www.northwestknowledge.net/metdata/data/pr_1981.nc time: 13.059367179870605
url: https://www.northwestknowledge.net/metdata/data/pr_1982.nc time: 12.232381582260132
Total time: 53.15849542617798
下载单个文件的时间在 11 到 16 秒之间。总下载时间稍少于一分钟。你的下载时间将根据你特定的网络连接有所不同。
让我们将这种串行(循环)方法与下面的并行方法进行比较。
使用 Python 并行下载多个文件
首先,创建一个函数(download_parallel)来处理并行下载。该函数(download_parallel)将接受一个参数,一个包含 URL 和相关文件名的可迭代对象(我们之前创建的 inputs 变量)。
接下来,获取可用于处理的 CPU 数量。这将决定并行运行的线程数量。
现在使用 multiprocessing ThreadPool 将 inputs 映射到 download_url 函数。在这里我们使用 ThreadPool 的 imap_unordered 方法,并将 download_url 函数及其输入参数(inputs 变量)传递给它。imap_unordered 方法将同时运行指定线程数量的 download_url(即并行下载)。
因此,如果我们有四个文件和四个线程,则所有文件可以同时下载,而不是等待一个下载完成后再开始下一个。这可以节省大量处理时间。
在 download_parallel 函数的最后部分,将打印下载的 URL 及下载每个 URL 所需的时间。
def download_parallel(args):
cpus = cpu_count()
results = ThreadPool(cpus - 1).imap_unordered(download_url, args)
for result in results:
print('url:', result[0], 'time (s):', result[1])
一旦定义了 inputs 和 download_parallel,就可以用一行代码并行下载文件。
download_parallel(inputs)
输出:
url: https://www.northwestknowledge.net/metdata/data/pr_1980.nc time (s): 14.641696214675903
url: https://www.northwestknowledge.net/metdata/data/pr_1981.nc time (s): 14.789752960205078
url: https://www.northwestknowledge.net/metdata/data/pr_1979.nc time (s): 15.052601337432861
url: https://www.northwestknowledge.net/metdata/data/pr_1982.nc time (s): 23.287317752838135
Total time: 23.32273244857788
请注意,这种方法下载每个单独文件所需的时间更长。这可能是由于网络速度变化,或将下载映射到各自线程所需的开销所致。尽管单独文件下载时间较长,但并行方法使总下载时间减少了 50%。
你可以看到并行处理如何大大减少多个文件的处理时间。随着文件数量的增加,使用并行下载方法可以节省更多时间。
结论
在你的开发和分析流程中自动化文件下载可以节省大量时间。正如本教程所示,实现并行下载例程可以大大减少文件获取时间,特别是当你需要下载许多文件或大文件时。
最初发布于 https://opensourceoptions.com.
SMOTE 和其他选项:处理不平衡数据的综合指南
原文:
towardsdatascience.com/use-smote-with-caution-3fa015ba3bc5
关于何时使用和何时不使用合成数据来解决类别不平衡问题的指南
·发布于 Towards Data Science ·10 分钟阅读·2023 年 1 月 3 日
--

如果你是一名机器学习从业者,你可能会更频繁地遇到类别不平衡问题。类别不平衡发生在数据集中类别分布不均的情况。例如,假设我们正在处理流失问题。在这个特定场景下,我们的少数类和多数类分别是客户流失和客户继续留在当前服务提供商。但是,如果你深入探索这个问题,你会发现流失类别的客户较少,因为客户流失是一个对业务有利但对模型不利的不频繁事件。因此,如果我们将这个数据集输入模型,它将比少数类别(流失场景)更准确地学习到多数类别(非流失场景)。这就是我们问题的开始。
如何处理机器学习中的类别不平衡
最明显的答案是由于模型在训练过程中与少数类的交互较少,我们可以通过向模型中添加更多少数类数据来改善这一点。但是怎么做呢?我们有几种方法:
-
收集更多的少数类数据——这是一个理论上简单但在实践中不可行的解决方案。因为在满足业务实际需求的同时很难做到这一点,例如,我们可能需要改变逻辑以将更多客户转入流失类别。
-
随机过采样——我们可以复制少数类数据,直到得到合理的类别分布。这可能导致模型学习到不准确的流失场景。简单来说,它会过度学习一些小的事件模式。
-
随机欠采样 — 我们可以通过从多数类中删除样本来平衡数据集。然而,这会从数据集中删除一些信号。此外,如果我们的数据集严重不平衡(少数样本少于 1%),我们可能需要从数据集中删除大量的多数类样本,以使其更平衡。
-
我们可以生成合成数据 — 我们将在本文中更深入地探讨这一点。
生成合成数据以重新平衡数据集
其基本思想是生成与少数类中现有样本类似的更多少数类样本。但与重复少数类实例多次不同,这将基于我们拥有的数据集生成新的少数类实例。为此,通常使用 SMOTE(合成少数过采样技术)方法。但也有许多替代方法,如……
-
ADASYN(自适应合成采样)
-
Tomek Links:该技术从多数类中移除与少数类样本非常接近的样本。这个想法是移除那些容易被误分类为少数类的多数类简单案例。
-
近邻缺失:该技术从特征空间中选择与少数类样本最接近的多数类样本,并将其从数据集中移除。这个想法类似于 Tomek Links,但它移除的是最有可能被误分类为少数类的样本,而不是多数类中的简单案例。
让我们对 SMOTE 进行一些实验。
首先,导入数据集。这里我使用的是Wine Quality dataset,你可以通过 这个 链接访问数据集。我们来加载数据集并绘制类别分布。
import pandas as pd
import numpy as np
#read the dataset
data = np.load('x20data.npz')
df = pd.DataFrame(data['data'])
df['target'] = data['label']
df['target'] = df['target'].replace(-1,0)
#lets plot class distribution
df['target']\
.value_counts(normalize=True)\
.plot\
.bar(figsize=(10,6),alpha=0.6)
plt.xlabel("Label")
plt.ylabel("Distribution %")
plt.yticks(list(np.linspace(0,1,11)))
plt.grid(True)
plt.show()

图片由作者提供
现在我们可以使用 imblearn 库对数据集进行 SMOTE。在下面的代码中,我们将对数据集进行 SMOTE 处理,并绘制原始数据集和重采样数据集的图。
import matplotlib.pyplot as plt
from sklearn.manifold import TSNE
from imblearn.over_sampling import SMOTE
# Set the font family for matplotlib
plt.rcParams['font.family'] = 'Verdana'
# Split your data into features and labels
X = df.drop('target', axis=1)
y = df['target']
# Define the SMOTE resampling method
smote = SMOTE(random_state=42)
# Resample the data using SMOTE
X_resampled, y_resampled = smote.fit_resample(X, y)
# Use TSNE for dimensionalilty reduction
X_tsne = TSNE(n_components=2).fit_transform(X)
X_resampled_tsne = TSNE(n_components=2).fit_transform(X_resampled)
# Get the class ratios
original_class_ratio = np.bincount(y) / len(y)
resampled_class_ratio = np.bincount(y_resampled) / len(y_resampled)
# Create subplots
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6))
# Plot the original data
ax1.scatter(X_tsne[:, 0], X_tsne[:, 1], c=y, cmap='rainbow',alpha=0.4)
ax1.set_title(f'Original Data\nClass Ratio: {original_class_ratio[1] * 100:.1f} : {original_class_ratio[0] * 100:.1f}')
# Plot the resampled data
ax2.scatter(X_resampled_tsne[:, 0], X_resampled_tsne[:, 1], c=y_resampled, cmap='rainbow',alpha=0.4)
ax2.set_title(f'Resampled Data\nClass Ratio: {resampled_class_ratio[1] * 100:.0f} : {resampled_class_ratio[0] * 100:.0f}')
plt.show()

图片由作者提供
如图所示,我们将 220 个原始少数事件转化为 2152 个,大约增加了 9 倍。这就是问题所在。让我们关注我们特定的流失问题。
-
生成这种虚假客户数据可能会导致模型学习到在现实世界中不存在的模式。
-
在大多数情况下,我们的数据集存在质量问题。因此,数据集中加入噪声的可能性很高。使用嘈杂数据生成新数据是个坏主意。
面对这些潜在的问题,我们有如下问题。
我们要将这些模型部署到生产环境中吗?
如果这是一个流失或欺诈检测问题,我不会将其部署到生产环境中,因为这些客户数据在应用 SMOTE 后可能变得更加嘈杂。但上述问题的答案很大程度上取决于我们所处理的数据和业务问题。一般来说,当数据嘈杂且问题复杂时,依赖 SMOTE 并不是一个好主意。
让我们使用过采样数据构建分类器并评估模型。
from sklearn.metrics import confusion_matrix, roc_auc_score, roc_curve, ConfusionMatrixDisplay
def plot_confusion_matrix_and_roc(model,df):
"""Helper function for plotting model performances"""
# Make predictions on the holdout set
X,y = df.drop('target',axis=1), df['target']
y_pred = model.predict(X)
# Create the confusion matrix
cm = confusion_matrix(y, y_pred)
# Calculate the AUC ROC score
auc = roc_auc_score(y, y_pred)
# Get the FPR and TPR for the ROC curve
fpr, tpr, thresholds = roc_curve(y, y_pred)
# Create a figure with two subplots
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 5))
# Plot the confusion matrix
ax1.matshow(cm, cmap='Blues')
ax1.set_title('Confusion Matrix')
ax1.set_xlabel('Predicted')
ax1.set_ylabel('Actual')
# Add labels to the confusion matrix
# for i in range(cm.shape[0]):
# for j in range(cm.shape[1]):
# ax1.text(j, i, f'{cm[i, j]:n}', ha='center', va='center')
# Add labels for the TP, FP, TN, and FN cells
ax1.text(0, 0, f'TN: {cm[0, 0]:n}', ha='center', va='center', color='w')
ax1.text(0, 1, f'FN: {cm[1, 0]:n}', ha='center', va='center', color='k')
ax1.text(1, 0, f'FP: {cm[0, 1]:n}', ha='center', va='center', color='k')
ax1.text(1, 1, f'TP: {cm[1, 1]:n}', ha='center', va='center', color='k')
# Plot the ROC curve
ax2.plot(fpr, tpr, label='AUC ROC = %0.2f' % auc)
ax2.plot([0, 1], [0, 1], 'k--')
ax2.set_title('ROC Curve')
ax2.set_xlabel('FPR')
ax2.set_ylabel('TPR')
ax2.legend()
plt.show()
#train the model
from xgboost import XGBClassifier
model = XGBClassifier()
model.fit(X_resampled,y_resampled)
plot_confusion_matrix_and_roc(model,df_holdout)

作者提供的图片
现在是时候尝试其他处理类别不平衡的方法了。下面我将解释一些我用来解决不平衡问题的方法。
-
使用类别权重
-
更改评估指标
-
通过对模型进行错误分析来创建更多特征
-
使用无监督算法检测数据集中的聚类。
让我们深入探讨这些方法。
使用类别权重
在对具有类别不平衡的数据集进行模型训练时,损失函数可能会被多数类主导,因为它的实例更多。这可能导致模型更多地关注多数类而不是少数类。类别权重的主要思想是根据每个样本的类别分配权重。在训练过程中,它会对少数类给予更多的权重。这意味着模型在训练过程中会更多关注少数类,以期提高该类别的表现。
提前使用类别权重可以通过对少数类分配更高的权重来平衡损失函数,使其对损失函数的影响更大。这可以帮助模型更好地学习少数类的特征并提高其表现。
从数学上讲,类别权重通常通过将每个样本的损失乘以其类别的权重来融入损失函数。例如,假设我们有一个包含两个类别(0 和 1)的数据集以及以下类别权重:
class_weights = {0: 1, 1: 10}
二分类模型的损失函数可能定义如下:

作者提供的图片
要将类别权重融入这个损失函数,我们可以按如下方式修改它:

作者提供的图片
现在,当模型训练完成后,每个样本的损失将乘以其类别的权重。这将使模型更加关注少数类,因为其样本将显著影响损失函数。
大多数主要机器学习模型接受 sample_weight 参数。以下是如何使用 XGBoost 库来做到这一点的示例。
# this will compute sample weight for us
from sklearn.utils import compute_sample_weight
#coputing class weights, this will be (num_samples_in_y,)
sample_weights = compute_sample_weight(class_weight='balanced',y=y)
model = XGBClassifier()
# just plug sample weight parameter into your model
model.fit(X,y,sample_weight=sample_weights)
需要注意的是,如果权重过高,样本权重可能会导致过拟合。通常,尝试一系列权重并查看哪个在验证集上表现最佳是个好主意。
另外,我们也可以使用 scale_pos_weight 参数在 XGBoost 中实现。它会给出类似的结果。
让我们快速绘制上述模型性能图。

作者提供的图片
如果你检查上述两种情况的混淆矩阵。你会注意到,在过采样场景中,高假阳性比较多。我们使用类别权重减少了假阳性预测,同时也减少了真正的阳性。因此,我们必须根据实际业务需求调整我们的方法。
更改评估指标
大多数库将准确率作为分类任务的默认评估指标。对于平衡问题,这没问题。但对于不平衡问题,这将导致模型仅猜测主要类别而不会学习任何潜在的信号。
例如,假设我们有 97 个非流失客户和三个流失客户。构建一个模型并使用准确率作为评估指标,通过盲目预测每 100 个样本为非流失类别,可以达到 97%的准确率。作为简单的解决方法,我们可以将评估指标更改为其他不同的指标。精确度、召回率、F1 分数和平衡准确率是处理不平衡分类任务的几个最佳选择。
通过对模型进行错误分析来创建更多特征
当你的模型表现不佳时,我们可以使用错误分析来寻找不同的数据段,发现不同的性能水平。以之前的流失示例为例,我们可以找到模型的错误与客户收入区间的关系,并识别模型表现好的收入段和表现差的收入段。类似地,我们可以利用这些信息创建错误分析报告。在错误分析报告之后,我们可以确定模型可以用来区分流失者与非流失者的新特征。
例如,如果你知道低收入用户因为“XYZ”原因而流失,你可以添加这个特征(如果模型中还没有的话)。否则,你可以利用这个潜在特征进行特征工程,例如对“XYZ”特征进行分箱处理。
使用无监督算法检测数据集中的聚类
一种强大且流行的方法是分段。如果我们知道一些特征可以用于将数据分隔成不同的子群体,我们可以利用这些特征进行聚类模型。在聚类后,你会注意到不同组之间存在各种类别不平衡的情况。有几种可能的场景,例如,
-
你可能会发现只有一个类别的子群体。验证了这种客户行为后,我们可以进一步跳过对这个特定子群体的建模。这将减少整个数据集的不平衡。
-
你可能会发现与之前的数据集相比,分布较为平衡且易于建模。
-
或者,你可能会发现具有高度不平衡类别分布的子群体。但这与原始分布相比并不差。这是因为这种不平衡发生在相似的数据点中,并且即使分布不平衡,也可以是一个强烈的信号。例如,如果我们正在处理一个疾病预测模型,将人们根据年龄分组是个好主意。如果整体类别不平衡为 2%,则按年龄分组将为不同的子群体产生不同的类别分布。在较高年龄组中,这种分布将更加平衡。在中年和年轻年龄组中,这将高度不平衡。由于我们单独对每个段进行建模,模型可以很好地泛化到该特定段。
结论
本文旨在展示除了合成数据生成之外的处理类别不平衡的替代方法。值得注意的是,一些方法在很大程度上依赖于数据、问题类型以及你所处理的领域。尝试几种不同的方法,并选择最适合你问题的方法通常是一个好主意。
请查找上述数据集的引用和许可证信息。
1. 引用:Lemaître, G., Nogueira, F., Aridas, C. K., & Oliveira, D. V. R. (2016). 用于基准测试的不平衡数据集 [Data set]. Zenodo. doi.org/10.5281/zenodo.61452
2. 数据集许可证: Open Data Commons Open Database License v1.0
感谢阅读。














浙公网安备 33010602011771号