TowardsDataScience 2023 博客中文翻译(一百二十三)

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

分类器集成:Voting Classifier

原文:towardsdatascience.com/ensemble-of-classifiers-voting-classifier-ef7f6a5b7795

结合多种不同模型以获得更好的预测

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Saptashwa Bhattacharyya

·发布于 Towards Data Science ·阅读时间 7 分钟·2023 年 8 月 17 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

使用 Voting Classifier 的决策边界(图片来源:作者;代码见参考文献)

在机器学习的上下文中,Ensemble 指的是一个有限数量的机器学习模型的集合(可能包括 ANN),这些模型是为同一任务训练的。通常,这些模型是独立训练的,然后将它们的预测结果结合起来。

当不同模型的预测结果不一致时,有时使用集成模型进行分类比任何单个分类器更为有用。在这里,我们希望结合不同的分类器创建一个集成模型,然后使用这个集成模型进行预测任务。本文将讨论什么内容?

  • 使用 Sklearn 的 VotingClassifier 来构建一个集成模型。

  • 什么是 VotingClassifier 中的硬投票和软投票?

  • 使用 VotingClassifier 检查单个模型的性能。

  • 最后,使用 GridSearchCV + VotingClassifier 来寻找单个模型的最佳参数。

让我们开始吧!

数据准备:

为了查看 VotingClassifier 的实际应用示例,我使用了心脏病预测数据集(开放数据库许可下提供)。这里的任务是对患者是否可能有心脏病进行二分类预测。数据集包含 10 个属性,包括年龄、性别、静息血压等,数据来源于 900 多名患者。让我们检查一些不同参数的分布情况。我们检查‘ClassLabel’的计数(1 表示心脏病,0 表示健康),即健康和患病人群随性别的变化情况。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1:参与者性别对 ClassLabel 分布的影响。(图片来源:作者;代码见参考文献)。

总的来说,我们看到男性的生病比例相较于女性要高。我们还可以检查个别特征,例如胆固醇和静息血压分布,见下文,我们发现生病患者的胆固醇和静息血压均较高,尤其是女性。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2:两种不同类别(健康和生病)下男性和女性的胆固醇和静息血压分布(图片来源:作者)。

对于数值特征,也可以使用箱线图查看分布,其中箱体表示数据中心 50% 的范围。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3:通过箱线图展示的一些数值参数分布(图片来源:作者)。

我们在数据集上发现了一些问题;有相当多的数据点胆固醇和静息血压为零。虽然确实有可能存在非常低的胆固醇,但很多数据点(患者)都为零,这是否有些过于偶然?不过,由于我们的主要目标是实现 VotingClassifier,因此我们将这些数据点保留原样,并继续进行下一阶段。

训练-测试与分类:

在一些初步分析后,我们准备数据集以训练和测试不同的分类器,并比较它们的性能。首先,我们将数据分成训练集和测试集,分别对数值列进行标准化(以防止数据泄漏),并初始化 3 种不同的分类器——支持向量机、逻辑回归 和 AdaBoost,后者是一种集成学习方法,特别属于提升(Boosting)方法(另一种被称为‘袋装(Bagging)’)。接下来是使用训练集训练这些分类器,并检查测试集的预测结果。我们检查了 3 种不同的性能指标——精度、召回率和 F1 分数。以下是代码块:

检查心脏病数据集上各个分类器的性能。

以下是得分:

>>> SVC, LogReg, AdaBoost Precisions: 0.9096562 0.8915564 0.8825301
>>> SVC, LogReg, AdaBoost Recalls: 0.9086956 0.8913043 0.8826086
>>> SVC, LogReg, AdaBoost F1-scores: 0.90831418 0.8910015 0.8825525

我们看到,F1 分数的平均值约为 ~89%。还可以检查 AdaBoost 分类器的特征重要性,胆固醇显然是主要的重要特征之一。下面是图示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4:特征重要性图:仅考虑 AdaBoost 分类器,胆固醇的重要性最高(图片来源:作者)。

AdaBoost 分类器的混淆矩阵如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5:AdaBoost 分类器的混淆矩阵。

VotingClassifier 用于形成集成模型:

VotingClassifier 结合了不同的机器学习分类器,并使用投票规则(‘软’或‘硬’)来预测类别标签。

当模型的性能几乎相同时,它平衡了各个模型的个体弱点;在这里,我们将结合之前使用的单独分类器来构建集成模型。但什么是软投票和硬投票呢?

多数 (‘硬’) 投票: 对于特定样本,预测的类别标签是代表每个单独分类器预测的类别标签的多数(众数)。下面的表格展示了一个具有 3 个类别的分类任务示例

+-------------+---------+---------+--------+
| Classifier  | Class1  | Class2  | Class3 |
+-------------+---------+---------+--------+
| SVC         |    0.2  |    0.3  |    0.5 |
| LogReg      |    0.3  |    0.4  |    0.3 |
| AdaBoost    |    0.1  |    0.3  |    0.6 |
+-------------+---------+---------+--------+

在这里,SVC 和 AdaBoost 都预测 Class3 作为输出标签,在硬投票方案下,这将被选中。

软投票: 软投票返回预测概率之和的最大类别标签;也可以为参与创建集成的分类器分配一个权重数组。下面是一个示例代码块

svc_classifier = SVC(probability=True)
logreg_classifier = LogisticRegression()
adaboost_classifier = AdaBoostClassifier()

estimators = [('svm', svc_classifier), ('logreg', logreg_classifier), 
              ('adaboost', adaboost_classifier)]
voting_classifier_soft = VotingClassifier(estimators=estimators, 
                                          voting='soft', 
                                          weights=[1, 1, 1]) 

再次查看下面的表格,以权重 [1, 1, 1] 为例,即所有分类器的权重相等

+ — — — — — — — + — — — — — + — — — — -+ — — — — -+
| Classifier    | Class1    | Class2   | Class3   |
+ — — — — — — — + — — — — — + — — — — -+ — — — — -+
| SVC           | w1 x 0.2  | w1 x 0.3 | w1 x 0.5 |
| LogReg        | w2 x 0.3  | w2 x 0.4 | w2 x 0.3 |
| AdaBoost      | w3 x 0.1  | w3 x 0.3 | w1 x 0.6 |
| Weighted Avg. | 0.20      | 0.33     | 0.47     |
+ — — — — — — — + — — — — — + — — — — -+ — — — — -+

在‘软’投票方案下,这里将选择 Class3。有关 VotingClassifier 的更多信息,您还可以查看 官方 Sklearn 指南。现在,让我们构建一个 VotingClassifier,并使用这个集成模型在测试集上进行训练和预测,使用‘软’和‘硬’方案。

使用 VotingClassifier 确实可以看到相较于单个估算器,性能指标有所改善。同样,我们也可以绘制混淆矩阵以进一步验证结果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6:使用 VotingClassifier 和‘硬’投票方案获得的混淆矩阵。(图像由作者提供)。

VotingClassifier 和 GridSearchCV:

也可以使用 GridSearchCV 调整 VotingClassifier 中各个估算器的参数(‘超参数’)。GridSearch 会穷尽所有参数组合,并使用交叉验证来找到最佳超参数。我们之前已经通过 这里 介绍了使用支持向量机、主成分分析和 GridSearchCV 构建分析管道的示例。我们展示了在 VotingClassifier 中获取最佳参数是相当简单的,下面是一个示例代码块:

对于参数空间(在第 18 行),我们仔细研究了 SVM 的 ‘C’ 和 ‘gamma’ 参数(使用径向基函数核),这些参数我们之前已经详细讨论过 之前。对于逻辑回归,我们检查了正则化强度参数的多个逆值;最后,对于使用决策树作为基估算器的 AdaBoost 分类器,我们检查了一些可能的估算器数量值,即 Boosting 终止的值。

结论: 在这篇文章中,通过 VotingClassifier,我们讨论了机器学习中的一个重要概念——集成技术。我们使用个体估计器构建一个集成,这些估计器的预测结果通过‘Hard’或‘Soft’投票方案结合,用于示例分类任务。通常认为,结合几个不同的估计器可能会胜过最佳估计器的表现,但集成有助于减少选择表现不佳的分类器的风险[1]。

根据手头的任务,你可以考虑使用 VotingClassifier 集成技术,而不是从多个分类器中选择最佳的。

保持坚强!干杯!!

参考文献:

[1] “决策中的集成系统”;R. Polikar,IEEExplore,DOI: 10.1109/MCAS.2006.1688199

[2] Sklearn VotingClassifier: 用户指南

[3] “基于集成的分类器”;L. Rokach,人工智能评论 33,1–39 (2010)。 Springer

[4] 代码和笔记本:GitHub

[5] 心力衰竭预测数据;在开放数据库许可 (ODbl) 下提供;链接

通过单元测试确保模型的可靠性

原文:towardsdatascience.com/ensure-model-reliability-with-unit-testing-589292e6f0e2

在开发过程中尽早发现错误

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Renato Boemer

·发表于 Towards Data Science ·4 min read·2023 年 1 月 25 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

照片由 Diana PolekhinaUnsplash 上发布

你是否曾经去过数据科学家或机器学习工程师的面试,并被问到如何设置单元测试?如果你不知道我在说什么,或者只是想复习一下单元测试的基础知识,那么这篇文章适合你。

单元测试是任何软件开发过程中不可或缺的一部分,对于机器学习模型尤其重要。单元测试是一种软件测试方法,其中软件应用程序的单个单元或组件在隔离的环境中进行测试。在机器学习的背景下,单元测试用于确保模型的各个组件,如特征预处理或模型评估指标,按照预期工作。

作为一个学习数据科学和机器学习的人,你可能会想知道为什么单元测试对机器学习模型如此重要。答案很简单:

单元测试有助于在开发过程中尽早发现错误。

从长远来看,这可以节省大量时间和精力,因为早期发现的错误通常比后期发现的错误更容易和更快速地修复。此外,单元测试可以帮助确保机器学习模型的健壮性和可靠性,这对于模型预测可能会带来实际后果的应用程序至关重要。

示例与代码

我将使用 Python 演示如何实现单元测试,为了这个示例,我使用了一个简单的线性回归模型。通常,线性回归是我们在训练营中学习的最简单的模型。

首先,我们需要安装必要的包。我们将使用 Python 内置的unittest 包。所以,在脚本的开头,像这样开始:

import unittest

接下来,我们将定义一个用于单元测试的类。该类应该继承自unittest.TestCase,每个测试方法的命名应以test_开头。如果你不熟悉 Python 类,请在评论中告诉我!

class TestLinearRegression(unittest.TestCase):
    def test_fit(self):
        # test code for fitting the model
        pass

    def test_predict(self):
        # test code for making predictions with the model
        pass

    def test_score(self):
        # test code for evaluating the model's performance
        pass

在每个测试方法中,我们可以使用各种断言方法来检查模型的输出是否符合预期。例如,在test_fit方法中,我们可以检查模型的系数是否在真实系数的某个容差范围内。在test_predict方法中,我们可以检查模型的预测值是否在真实值的某个容差范围内。而在test_score方法中,我们可以检查模型的性能指标是否高于某个阈值。以下是上述代码的演变:

class TestLinearRegression(unittest.TestCase):
    def test_fit(self):
        # test code for fitting the model
        self.assertEqual(model.coef_, true_coef, delta=1e-3)

    def test_predict(self):
        # test code for making predictions with the model
        self.assertEqual(predictions, true_values, delta=1e-3)

    def test_score(self):
        # test code for evaluating the model's performance
        self.assertGreater(score, 0.9)

请记住,在上面的示例中,我使用了unittest.TestCase类中的assertEqualassertGreater方法。这只是unittest包中众多断言方法中的两个示例。还有用于检查是否引发异常、值是否为真或假等断言方法。

获取结果

要运行单元测试,我们可以使用unittest.main()方法。这将自动发现并运行测试类中定义的所有测试方法。我通常通过在命令行(也称为终端或 Shell)中输入带有-m unittest标志的 Python 文件来运行测试。例如:

python test_linear_regression.py -m unittest

理想情况下,单元测试应该在实际代码之前编写。这被称为测试驱动开发(TDD),它是软件开发中广泛接受的实践。通过先编写测试,你可以确保你编写的代码将满足需求并通过测试。

最后,你的单元测试将返回结果,例如运行的测试数量、通过的测试数量以及发生的任何错误或失败。然后,你应该分析这些结果,并确定测试用例是否如预期那样通过,或是否需要解决任何问题。

进一步探索

充分利用单元测试的最佳方法之一是确保测试用例覆盖尽可能多的场景。这包括正面和负面的场景。例如,一个测试用例应该检查模型的预测值是否在真实值的某个容差范围内,但它也应该检查模型的预测值是否不在真实值的某个容差范围内。

你还可以使用像 Jenkins 或 CircleCI 这样的持续集成服务来自动化运行测试的过程,并在网络界面上获取结果。

结论

单元测试是机器学习开发过程中至关重要的一部分。它有助于及早发现错误,确保模型的稳健性和可靠性,并在长远中节省时间和精力。通过使用 Python 和 unittest 包,你可以轻松为机器学习模型实现单元测试,并充分利用它。记得实践测试驱动开发,并在编写测试用例时覆盖尽可能多的场景。

想要全面访问 Medium 文章并支持我的工作吗?请使用下面的链接订阅:

[## 使用我的推荐链接加入 Medium - Renato Boemer

阅读 Renato Boemer 的每一个故事(以及 Medium 上其他成千上万的作家的故事)。你的会员费直接支持…

boemer.medium.com](https://boemer.medium.com/membership?source=post_page-----589292e6f0e2--------------------------------)

你可能想要阅读:

## 如何成功设置 A/B 测试

帮助非数据专业人士在没有数据科学家的情况下自信地进行测试

towardsdatascience.com

确保在 Scikit-learn 管道中正确使用变换器

原文:towardsdatascience.com/ensuring-correct-use-of-transformers-in-scikit-learn-pipelines-393566db7bfa

机器学习项目中的有效数据处理

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 彭乾

·发表在 数据科学前沿 ·11 分钟阅读·2023 年 12 月 20 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

确保在 Scikit-learn 管道中正确使用变换器。图片由作者提供

本文将解释如何在 Scikit-Learn (sklearn) 项目中正确使用 PipelineTransformers,以加速和重用我们的模型训练过程。

这部分内容补充和澄清了有关 Pipeline 示例和一些常见误解的官方文档。

我希望阅读完这篇文章后,你能够更好地利用 Pipeline 这个优秀的设计来完成你的机器学习任务。

介绍

世界各地的中餐馆都有一道著名的菜肴叫做“左宗棠鸡”,我想知道你是否尝试过。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

左宗棠鸡。标准化烹饪过程的典范。照片来源:作者创作,Canva

“左宗棠鸡”的一个特点是每块鸡肉都被厨师处理成相同的大小。这确保了:

  1. 所有鸡块都腌制相同的时间。

  2. 在烹饪过程中,每块鸡肉都达到相同的熟度。

  3. 使用筷子时,均匀的大小使得捡起食材更加容易。

这种预处理包括清洗、切割和腌制食材。如果鸡块切割得比平时大,即使炒的时间相同,风味也会发生显著变化。

因此,在准备开设餐厅时,我们必须考虑标准化这些过程和配方,以确保每一道“左宗棠鸡”都有一致的口味和质地。这就是餐厅成功的关键。

回到机器学习的世界,Scikit-Learn 还提供了这样的标准化流程,称为 Pipeline。它们将数据预处理和模型训练过程固化为标准化的工作流程,使机器学习项目更易于维护和重用。

在本文中,我们将探讨如何在 Scikit-Learn 的 Pipeline 中正确使用变换器,确保我们的数据准备得像精致餐点的配料一样完美。

为什么使用变换器

什么是变换器

在 Scikit-Learn 中,变换器主要分为两类:数据缩放和特征降维。

以一组房屋数据为例,其中包括位置、面积和卧室数量等特征。

如果不将这些特征标准化到相同的尺度,模型可能会因区域(通常是较大的数值)中的微小波动而忽略位置的显著影响(通常是分类数据)。

这就像用过多的胡椒粉掩盖了草药的细腻味道。

正确使用变换器

通常,数据缩放是通过标准化来完成的,公式为:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

训练数据标准化的公式。图片作者提供

其中 train_meantrain_std 是从训练数据中提取的变量。

在 Scikit-Learn 中,训练数据和测试数据都是使用 [train_test_split](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html?ref=dataleadsfuture.com#sklearn.model_selection.train_test_split) 方法从原始数据集中获得的。

在缩放测试数据时,使用相同的 train_meantrain_std

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在缩放测试数据时使用相同的 train_mean 和 train_std 变量。图片作者提供

这里出现了一个问题:为什么使用训练数据来生成这些变量?

让我们看看一个简单的数据集,其中训练数据为:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

一个简单的训练数据集。图片作者提供

标准化后,训练数据变为:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

缩放后的简单数据集。图片作者提供

显然,在缩放后,大于 0 的特征标记为 1,这意味着在缩放之前,大于 10 的特征标记为 1。

现在让我们看看测试数据:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

尚未分类的测试数据。图片作者提供

如果我们使用从测试数据分布中生成的 test_meantest_std,而不考虑训练数据,则结果变为:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

使用测试数据生成变量时的错误演示。图片作者提供

显然,这个预测结果是没有意义的。但是假设我们使用 train_meantrain_std 来处理数据,并结合模型预测;我们来看看会发生什么:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

使用训练数据的变量,我们获得了正确的结果。图片由作者提供

如我们所见,只有通过对训练数据生成的变量进行数据预处理,才能确保模型的预测符合预期。

在 Scikit-Learn 中使用变换器

在 Scikit-Learn 中使用变换器非常简单。

我们可以使用make_classification生成一组模拟数据,然后用train_test_split将其拆分为训练集和测试集。

from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split

X, y = make_classification(n_samples=100, n_features=2, 
                           n_classes=2, n_redundant=0, 
                           n_informative=2, n_clusters_per_class=1, 
                           random_state=42)

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

让我们看看数据的分布:

import matplotlib.pyplot as plt

plt.scatter(X_train[:, 0], X_train[:, 1], color='red', marker='o')
plt.scatter(X_test[:, 0], X_test[:, 1], color='green', marker='s')
plt.xlabel('feature_idx_0')
plt.ylabel('feature_idx_1')
plt.tight_layout()
plt.show()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

缩放前的数据分布。图片由作者提供

在这里,我们使用StandardScaler来缩放特征。

首先,初始化StandardScaler,然后用训练数据fit它:

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
scaler.fit(X_train)

接下来,我们可以使用拟合的变换器transform训练数据的特征:

X_train_std = scaler.transform(X_train)

当然,我们也可以使用fit_transform一次性拟合并转换训练数据:

X_train_std = scaler.fit_transform(X_train)

然后我们只需转换测试数据,无需再次拟合:

X_test_std = scaler.transform(X_test)

转换后,数据的分布保持不变,唯一的变化是规模:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

缩放后的数据分布。图片由作者提供

除了使用像[StandardScaler](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.StandardScaler.html?ref=dataleadsfuture.com#sklearn.preprocessing.StandardScaler)[MinMaxScaler](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.MinMaxScaler.html?ref=dataleadsfuture.com#sklearn.preprocessing.MinMaxScaler)这样的工具进行数据缩放外,我们还可以使用PCASelectKBest等进行维度减少。为了简洁起见,我不在这里深入探讨这些内容,但欢迎查阅官方文档获取更多信息。

在管道中使用变换器

为什么使用管道

如前所述,在机器学习任务中,我们通常需要使用各种变换器进行数据缩放和特征维度减少,然后再训练模型。

这带来了几个挑战:

  • 代码复杂性:每次使用变换器时,我们必须经历初始化、fit_transformtransform步骤。转换过程中漏掉一个步骤可能会破坏整个训练过程。

  • 数据泄露:如我们所讨论的,对于每个变换器,我们用训练数据拟合,然后转换训练数据和测试数据。我们必须避免测试数据的分布泄漏到训练数据中。

  • 代码复用性:一个机器学习模型不仅包括用于预测的训练好的估计器,还包括数据预处理步骤。因此,一个包括变换器和估计器的机器学习任务应该是原子化和不可分割的。

  • 超参数调优:在设置好机器学习步骤后,我们需要调整超参数,以找到 Transformer 参数值的最佳组合。

Scikit-Learn 引入了 Pipeline 模块来解决这些问题。

什么是 Pipeline

Pipeline 是 Scikit-Learn 中实现责任链设计模式的一个模块。

创建 Pipeline 时,我们使用 steps 参数将多个 Transformers 链接在一起进行初始化:

from sklearn.pipeline import Pipeline
from sklearn.decomposition import PCA
from sklearn.ensemble import RandomForestClassifier

pipeline = Pipeline(steps=[('scaler', StandardScaler()),
                           ('pca', PCA(n_components=2, random_state=42)),
                           ('estimator', RandomForestClassifier(n_estimators=3, max_depth=5))])

官方文档指出,最后一个 Transformer 必须是一个 Estimator。

如果你不需要指定每个 Transformer 的名称,你可以使用 make_pipeline 简化 Pipeline 的创建:

from sklearn.pipeline import make_pipeline

pipeline_2 = make_pipeline(StandardScaler(),
                           PCA(n_components=2, random_state=42),
                           RandomForestClassifier(n_estimators=3, max_depth=5))

从源代码理解 Pipeline 的机制

我们已经提到,在使用每个 Transformer 时,不要让测试数据变量泄露到训练数据中的重要性。

当每个数据预处理步骤是独立时,这个原则相对容易确保。

但如果我们使用 Pipeline 整合这些步骤会怎么样?

如果我们查看官方文档,会发现它只是对整个数据集使用 fit 方法,而没有解释如何分别处理训练数据和测试数据。

牢记这个问题,我深入研究了 Pipeline 的源代码以寻找答案。

阅读源代码表明,虽然 Pipeline 实现了 fitfit_transformpredict 方法,但它们的工作方式与普通的 Transformers 不同。

以以下 Pipeline 创建过程为例:

from sklearn.pipeline import Pipeline
from sklearn.decomposition import PCA
from sklearn.ensemble import RandomForestClassifier

pipeline = Pipeline(steps=[('scaler', StandardScaler()),
                           ('pca', PCA(n_components=2, random_state=42)),
                           ('estimator', RandomForestClassifier(n_estimators=3, max_depth=5))])

内部实现可以通过以下图示表示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

调用 fitpredict 方法时的内部实现。图片由作者提供

正如你所见,当我们调用 fit 方法时,Pipeline 首先将 Transformers 与 Estimator 分开。

对于每个 Transformer,Pipeline 会检查是否有 fit_transform 方法;如果有,它会调用该方法;否则,它会调用 fit

对于 Estimator,它直接调用 fit

对于 predict 方法,Pipeline 将 Transformers 与 Estimator 分开。

Pipeline 按顺序调用每个 Transformer 的 transform 方法,然后是 Estimator 的 predict 方法。

因此,在使用 Pipeline 时,我们仍然需要拆分训练数据和测试数据。然后我们只需对训练数据调用 fit,对测试数据调用 predict

在将 Pipeline 与 GridSearchCV 进行超参数调优时,有一种特殊情况:你不需要手动拆分训练数据和测试数据。我将在最佳实践部分详细解释这一点。

实际应用中使用 Transformers 和 Pipeline 的最佳实践

既然我们已经讨论了 Transformers 和 Pipeline 的工作原理,是时候履行标题中的承诺,讨论在实际项目中将 Transformers 与 Pipeline 结合的最佳实践。

将 Pipeline 与 GridSearchCV 结合用于超参数调优

在机器学习项目中,选择合适的数据集处理和算法是一方面。在调试初步步骤之后,是时候进行参数优化了。

使用 GridSearchCVRandomizedSearchCV,你可以尝试不同的参数以找到最佳适配:

import time

from sklearn.model_selection import GridSearchCV

pipeline = Pipeline(steps=[('scaler', StandardScaler()),
                           ('pca', PCA()),
                           ('estimator', RandomForestClassifier())])
param_grid = {'pca__n_components': [2, 'mle'],
              'estimator__n_estimators': [3, 5, 7],
              'estimator__max_depth': [3, 5]}

start = time.perf_counter()
clf = GridSearchCV(pipeline, param_grid=param_grid, cv=5, n_jobs=4)
clf.fit(X, y)

# It takes 2.39 seconds to finish the search on my laptop.
print(f"It takes {time.perf_counter() - start} seconds to finish the search.")

但在机器学习中,超参数调优不仅限于 Estimator 参数;它还涉及 Transformer 参数的组合。

将所有步骤与 Pipeline 集成,允许对每个元素进行不同参数组合的超参数调优。

请注意,在超参数调优期间,我们不再需要手动分割训练数据和测试数据。GridSearchCV 会使用 [StratifiedKFold](https://scikit-learn.org/stable/modules/cross_validation.html?ref=dataleadsfuture.com#stratified-k-fold) 将数据分为训练集和验证集,该方法实现了 k-fold 交叉验证机制。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

StratifiedKFold 迭代过程将训练数据和测试数据分开。图片来源:作者

我们还可以设置交叉验证的折数,并选择使用多少个工作线程。调优过程在下图中展示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

GridSearchCV 超参数调优的内部实现。图片来源:作者

由于空间限制,我不会在此详细介绍 GridSearchCVRandomizedSearchCV。如果你感兴趣,我可以在下次撰写另一篇文章来解释它们。

使用 memory 参数缓存 Transformer 输出

当然,使用 GridSearchCV 进行超参数调优可能会很慢,但这没关系,Pipeline 提供了缓存机制,通过缓存中间步骤的结果来加速调优效率。

在初始化 Pipeline 时,你可以传入 memory 参数,这将缓存每个转换器第一次调用 fittransform 后的结果。

如果随后的 fittransform 调用使用相同的参数,这在超参数调优期间很可能发生,这些步骤将直接从缓存中读取结果,而不是重新计算,从而显著提高了重复运行相同 Transformer 时的效率。

memory 参数可以接受以下值:

  • 默认值为 None:不使用缓存。

  • 字符串:提供存储缓存结果的路径。

  • joblib.Memory 对象:允许更细粒度的控制,例如配置缓存的存储后端。

接下来,让我们使用之前的 GridSearchCV 示例,这次将 memory 添加到 Pipeline 中,看看能提高多少速度:

pipeline_m = Pipeline(steps=[('scaler', StandardScaler()),
                           ('pca', PCA()),
                           ('estimator', RandomForestClassifier())],
                      memory='./cache')
start = time.perf_counter()
clf_m = GridSearchCV(pipeline_m, param_grid=param_grid, cv=5, n_jobs=4)
clf_m.fit(X, y)

# It takes 0.22 seconds to finish the search with memory parameter.
print(f"It takes {time.perf_counter() - start} seconds to finish the search with memory.")

如图所示,使用缓存后,调优过程仅需 0.2 秒,相较于之前的 2.4 秒大幅提升了速度。

如何调试 Scikit-Learn Pipeline

在将 Transformers 集成到 Pipeline 中后,整个预处理和转换过程变成了一个黑箱。很难理解当前的处理步骤。

幸运的是,我们可以通过在 Pipeline 中添加日志记录来解决这个问题。

我们需要创建自定义转换器,以在数据转换的每一步添加日志记录。

这里是一个使用 Python 标准日志库添加日志记录的示例:

首先,你需要配置一个记录器:

import logging

from sklearn.base import BaseEstimator, TransformerMixin

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger()

接下来,你可以创建一个自定义 Transformer,并在其方法中添加日志记录:

class LoggingTransformer(BaseEstimator, TransformerMixin):
    def __init__(self, transformer):
        self.transformer = transformer
        self.real_name = self.transformer.__class__.__name__

    def fit(self, X, y=None):
        logging.info(f"Begin fit: {self.real_name}")
        self.transformer.fit(X, y)
        logging.info(f"End fit: {self.real_name}")
        return self

    def fit_transform(self, X, y=None):
        logging.info(f"Begin fit_transform: {self.real_name}")
        X_fit_transformed = self.transformer.fit_transform(X, y)
        logging.info(f"End fit_transform: {self.real_name}")
        return X_fit_transformed

    def transform(self, X):
        logging.info(f"Begin transform: {self.real_name}")
        X_transformed = self.transformer.transform(X)
        logging.info(f"End transform: {self.real_name}")
        return X_transformed

然后,你可以在创建 Pipeline 时使用这个 LoggingTransformer

pipeline_logging = Pipeline(steps=[('scaler', LoggingTransformer(StandardScaler())),
                             ('pca', LoggingTransformer(PCA(n_components=2))),
                             ('estimator', RandomForestClassifier(n_estimators=5, max_depth=3))])
pipeline_logging.fit(X_train, y_train)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

添加 LoggingTransformer 后的效果。图像由作者提供

当你使用 pipeline.fit 时,它会依次调用每个步骤的 fittransform 方法,并记录相应的信息。

在 Scikit-Learn Pipeline 中使用 passthrough

在 Pipeline 中,可以将某个步骤设置为 'passthrough',这意味着对于这个特定的步骤,输入数据将不加改变地传递到下一个步骤。

当你希望有选择性地启用/禁用复杂管道中的某些步骤时,这一点很有用。

参考上面的代码示例,我们知道在使用 DecisionTreeRandomForest 时,标准化数据是多余的,因此我们可以使用 passthrough 跳过这一步。

下面是一个示例:

param_grid = {'scaler': ['passthrough'],
              'pca__n_components': [2, 'mle'],
              'estimator__n_estimators': [3, 5, 7],
              'estimator__max_depth': [3, 5]}
clf = GridSearchCV(pipeline, param_grid=param_grid, cv=5, n_jobs=4)
clf.fit(X, y)

重用 Pipeline

经过一番波折,我们终于得到了一个表现良好的机器学习模型。

现在,你可能考虑如何重用这个模型,与同事共享,或将其部署到生产环境中。

然而,模型训练的结果不仅包括模型本身,还包括各种数据处理步骤,这些步骤都需要保存。

使用 joblib 和 Pipeline,我们可以保存整个训练过程以备后用。以下代码提供了一个简单的示例:

from joblib import dump, load

# save pipeline
dump(pipeline, 'model_pipeline.joblib')

# load pipeline
loaded_pipeline = load('model_pipeline.joblib')

# predict with loaded pipeline
loaded_predictions = loaded_pipeline.predict(X_test)

结论

官方 Scikit-Learn 文档 是我见过的最好的文档之一。通过学习其内容,你可以掌握机器学习应用的基础知识。

然而,在实际项目中使用 Scikit-Learn 时,我们经常遇到官方文档可能未涵盖的各种细节。

如何正确结合 Transformers 和 Pipeline 就是一个这样的案例。

在这篇文章中,我介绍了使用 Transformers 的原因和一些典型应用场景。

然后,我从源代码层面解释了 Pipeline 的工作原理,并完成了在训练和测试数据集上应用的合理用例。

最后,对于实际机器学习项目的每个阶段,我介绍了结合 Transformers 和 Pipeline 的最佳实践,基于我的工作经验。

希望这篇文章能对你有所帮助。如果你有任何问题,请给我留言,我会尽力回答。

感谢阅读我的故事。

你可以订阅以获取我最新的数据科学故事。

如果你有任何问题,可以在LinkedInTwitter(X)上找到我。

这篇文章最初发表在数据引领未来上。

确保可信的 ML 系统,通过数据验证和实时监控

原文:towardsdatascience.com/ensuring-trustworthy-ml-systems-with-data-validation-and-real-time-monitoring-89ab079f4360

完整的 7 步骤 MLOps 框架

课程 5:使用 GE 进行质量和完整性的数据验证。模型性能持续监控。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Paul Iusztin

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由Hassan Pasha拍摄,Unsplash

本教程代表7 节课中的第五部分,将逐步指导你如何设计、实施和部署 ML 系统,使用MLOps 良好实践。在课程中,你将构建一个生产就绪的模型,用于预测未来 24 小时内来自丹麦的多种消费类型的能源消耗水平。

到课程结束时,你将理解如何使用批处理服务架构来设计、编码和部署 ML 系统的所有基本原理。

本课程针对中级/高级机器学习工程师,希望通过构建自己的端到端项目提升技能。

如今,证书随处可见。构建先进的端到端项目,并在之后展示出来,是获得专业工程师认可的最佳方式。

目录:

  • 课程介绍

  • 课程内容

  • 数据源

  • 课程 5:使用 GE 进行质量和完整性的数据验证。模型性能持续监控。

  • 课程 5:代码

  • 结论

  • 参考文献

课程介绍

在这 7 节课的课程结束时,你将知道如何:

  • 设计批处理服务架构

  • 使用 Hopsworks 作为特征存储

  • 设计一个从 API 读取数据的特征工程管道

  • 构建一个具有超参数调优的训练管道

  • 使用 W&B 作为 ML 平台来跟踪你的实验、模型和元数据

  • 实现批处理预测管道

  • 使用 Poetry 构建自己的 Python 包

  • 部署你自己的私有 PyPi 服务器

  • 使用 Airflow 协调一切

  • 使用预测来开发基于 FastAPI 和 Streamlit 的 Web 应用

  • 使用 Docker 将代码容器化

  • 使用 Great Expectations 确保数据验证和完整性

  • 监控预测的性能随时间变化

  • 将一切部署到 GCP

  • 使用 GitHub Actions 构建 CI/CD 流水线

如果这听起来很多,不用担心。在完成这门课程后,你将理解我之前所说的一切。最重要的是,你将知道为什么使用这些工具以及它们如何作为一个系统协同工作。

如果你想充分利用这门课程, 我建议你访问包含所有课程代码的 GitHub 仓库 。这门课程旨在快速阅读并复制文章中的代码。

到课程结束时,你将知道如何实现下面的图示。如果有些内容你不理解,别担心。我会详细解释一切。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

课程中将构建的架构图 [图由作者提供]。

第 5 课结束时,你将知道如何使用 Great Expectations 验证数据的完整性和质量。同时,你还会了解如何在 ML 系统上实现监控组件。

课程内容:

  1. 批量服务。特征存储。特征工程流水线。

  2. 训练流水线。ML 平台。超参数调整。

  3. 批量预测流水线。使用 Poetry 打包 Python 模块。

  4. 私有 PyPi 服务器。使用 Airflow 协调一切。

  5. 使用 GE 进行数据质量和完整性验证。模型性能持续监控。

  6. 使用 FastAPI 和 Streamlit 消费和可视化模型预测。将一切容器化。

  7. 将所有 ML 组件部署到 GCP。使用 GitHub Actions 构建 CI/CD 流水线。

  8. [额外] 探秘‘不完美’ML 项目——经验与见解

要了解更多背景信息,请查看第 3 课,这将教你如何使用批处理架构和特征存储构建推理管道。

此外,第 4 课将展示如何使用 Airflow 来协调所有的管道。

这节课将利用上述观点,并假设你已经理解了这些观点。

数据源

我们使用了一个免费且开放的 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 小时的能源消耗。

查看下面的视频以更好地理解数据的样子 👇

课程和数据源概述 [作者视频]。

第 5 课:使用 GE 进行数据质量和完整性的验证。模型性能持续监控。

第 5 课的目标

目前,机器学习流程已经实施并协调好了。这意味着我们完成了吗?

还不完全……

最后一步,将使你从一个优秀的工程师成长为一个杰出的工程师,就是添加一个组件,让你能够快速诊断生产系统中发生的情况。

在第 5 课中,你将主要学习两个不同的主题,这些主题服务于一个共同目标:确保你的生产系统正常工作。

1. 数据验证: 在将数据导入特征存储之前,检查 FE 管道生成的数据是否正常。

2. 模型监控: 持续计算反映你生产模型性能的各种指标。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

最终架构图,其中第 5 课的组件用蓝色突出显示 [作者提供的图片]。

我将在理论概念与工具部分详细介绍。但简要概述一下,为了持续监控你的模型性能,你将使用你之前的预测与新收集的实际情况来计算所需的指标,在你的情况下是 MAPE。

例如,你预测 6 月 1 日的 24 小时能耗值。最初,你没有数据来计算指标。但是,在 12 小时后,你可以收集实际能耗。因此,你可以在获取实际情况后计算最后 12 小时的所需指标。

1 小时后,你可以计算另一个数据点的指标,依此类推……

这是我们将在本教程中采用的策略。

理论概念与工具

数据验证: 数据验证是确保数据质量和完整性的过程。我是什么意思?

当你自动从不同来源(在我们的案例中是 API)收集数据时,你需要一种方法来持续验证你刚刚提取的数据是否遵循系统期望的一组规则。

例如,你期望能耗值是:

  • 类型为浮点数,

  • 非空,

  • ≥0。

当你开发 ML 管道时,API 仅返回符合这些条款的值,数据人员称之为“数据契约”。

但是,当你让你的系统在生产环境中运行 1 个月、1 年、2 年等时,你将永远不知道哪些数据源可能发生变化,而这些变化你没有控制权。

因此,你需要一种方法来不断检查这些特征,以便在将数据导入特征存储之前。

注意: 要了解如何将这一概念扩展到非结构化数据,如图像,你可以查看我的 掌握数据完整性以清理你的计算机视觉数据集 文章。

Great Expectations(简称 GE): GE 是一个流行的工具,可以轻松进行数据验证并报告结果。Hopsworks 支持 GE。你可以将 GE 验证套件添加到 Hopsworks,并选择当新数据插入时以及验证步骤失败时的处理方式 — 了解更多关于 GE + Hopsworks [2]。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

GE 数据验证运行的截图 [作者提供的图片]。

实际情况类型: 当你的模型在生产环境中运行时,你可以在三种不同的场景中访问你的实际情况:

  1. 实时: 理想的情况是你可以轻松访问目标。例如,当你推荐一个广告,消费者要么点击它,要么不点击。

  2. 延迟: 最终,你会访问实际情况。但不幸的是,那时反应时间可能已太晚,无法及时做出适当的反应。

  3. 无: 你无法自动收集任何 GT。通常在这种情况下,如果需要实际数据,你必须聘请人工注释员。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

真实数据/目标/实际数据类型 [作者提供的图像]。

在我们的案例中,我们介于 #1 和 #2 之间。GT 不完全是实时的,但延迟只有 1 小时。

延迟 1 小时是否合适很大程度上取决于业务背景,但假设在你的情况下,这是可以的。

由于我们认为 1 小时的延迟对于我们的用例是可以的,我们很幸运:我们实时(近实时)访问 GT。

这意味着我们可以使用 MAPE 等指标来实时(近实时)监控模型的性能。

在情景 2 或 3 中,我们需要使用数据和概念漂移作为代理指标来计算时间上的性能信号。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

显示观察结果和预测重叠的截图。如你所见,GT 在最新的 24 小时预测中不可用 [作者提供的图像]。

ML 监控: ML 监控是确保你的生产系统随时间正常运行的过程。同时,它为你提供了一种机制,可以主动调整系统,例如及时重新训练模型或使其适应环境中的新变化。

在我们的案例中,我们将持续计算 MAPE 指标。因此,如果误差突然激增,你可以创建警报来通知你,或自动触发超参数优化步骤以将模型配置适应新环境。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

显示所有时间序列计算出的平均 MAPE 指标的截图 [作者提供的图像]。

第 5 课:代码

你可以在这里访问 GitHub 仓库。

注意: 所有的安装说明都在仓库的 README 文件中。在这里,你将直接跳到代码部分。

第 5 课中的代码位于以下位置:

使用 Docker,你可以快速在 Airflow 中托管所有内容,这样你就不必浪费大量时间进行设置。

直接将凭证存储在你的 git 仓库中是一个巨大的安全风险。这就是为什么你将使用 .env 文件来注入敏感信息。

.env.default 是你必须配置的所有变量的示例。它也有助于存储不敏感的属性的默认值(例如项目名称)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

.env.default 文件的截图 [作者提供的图像]。

准备凭证

我不想过多重复。你已经在之前课程的 “准备凭证” 中有逐步说明。

幸运的是,在这篇文章中,你无需准备之前课程中的额外凭证。

检查**“准备凭证”** Lesson 4 是一个很好的起点,展示了如何准备所有凭证和工具。另请检查 GitHub 仓库 以获取更多信息。

这将展示如何在 .env 文件中完成所有凭证的配置。

现在,让我们开始编码 🔥

数据验证

使用 GE + Hopsworks 进行数据验证的概述 [作者视频]。

GE 套件定义在 feature-pipeline/feature_pipeline/etc/validation.py 文件中。

在下面的代码中,你定义了一个 GE ExpectationSuite,名为 energy_consumption_suite

使用 ExpectationConfiguration 类,你可以添加各种验证测试。在以下示例中,添加了 2 个测试:

  1. 检查表的列是否与给定的有序列表匹配。

  2. 检查列的长度是否为 4。

简单而强大 🔥

现在,让我们来看看完整的验证套件 👇

使用 GE,你将检查 Pandas DataFrame 的以下特征:

  1. 列的名称应为:[“datetime_utc,”… “energy_consumption”]。

  2. DF 应有 4 列。

  3. 列 “datetime_utc” 应该所有值都不为空。

  4. 列 “area” 期望仅有值为 0、1 或 2。

  5. 列 “area” 应为 int8 类型。

  6. 列 “consumer_type” 期望仅有值为 111、…

  7. 列 “consumer_type” 应为 int32 类型。

  8. 列 “energy_consumption” 应该有值 ≥ 0。

  9. 列 “energy_consumption” 应为 float64 类型。

  10. 列 “energy_consumption” 应该所有值都不为空。

如你所见,质量检查主要归结为:

  1. 检查表的模式。

  2. 检查列的类型。

  3. 检查列的值(对离散特征和连续特征使用不同的逻辑)。

  4. 检查空值。

你将把这个验证套件附加到 FE 流水线的 to_feature_store() 加载函数中,文件为 feature-pipeline/feature_pipeline/etl/load.py

现在,Hopsworks 每次将新 DataFrame 插入特征组时都会运行给定的 GE 验证套件。

如果验证套件失败,你可以选择拒绝新数据或触发警报以采取手动措施。

ML 监控

课程内构建的 ML 监控仪表板概述 [作者视频]。

当涉及到 ML 监控时,最困难的部分不是代码本身,而是如何选择监控你的 ML 模型。

请注意,像 EvidentlyArize 这样的工具通常用于 ML 监控。但在这种情况下,我想保持简单,不再添加另一个工具。

但概念仍然是相同的,这一点最为关键。

在下面的代码片段中,我们做了以下事情:

  1. 从 GCP 存储桶加载了预测数据。所有预测数据都在批量预测步骤中聚合在 predictions_monitoring.parquet 文件中。

  2. 准备了预测 DataFrame 的结构。

  3. 连接到 Hopsworks 特征存储。

  4. 查询了特征存储中位于最小和最大预测时间边界的数据。这就是你的 GT。你想根据预测的时间窗口获取所有可用的数据。

  5. 准备了 GT DataFrame 的结构。

  6. 合并两个 DataFrame。

  7. 在 GT 可用的地方,计算 MAPE 指标。为了简化,你将计算所有时间序列的 MAPE 指标的聚合值。

  8. 将结果写回到 GCP 存储桶,由前端加载和显示。

上面定义的函数将在 Airflow DAG 中作为自己的任务运行。每次 ML 管道运行时,它都会被调用。因此,每小时,它将查找预测和 GT 之间的新匹配项,计算 MAPE 指标并将其上传到 GCS。

阅读 第 6 课 了解如何使用 Streamlit 和 FastAPI 以美观的 UI 显示来自 GCP 存储桶的结果。

结论

恭喜你!你完成了 第五课 来自 全栈 7 步 MLOps 框架 课程。这意味着你接近于了解如何使用 MLOps 好实践构建一个端到端的 ML 系统。

在本课程中,你学会了如何:

  • 使用 GE 构建一个数据验证套件来测试你的数据质量和完整性,

  • 理解为什么 ML 监控至关重要,

  • 构建你自己的 ML 监控系统,以实时跟踪模型性能。

现在你理解了掌控数据和 ML 系统的力量,你可以安稳地睡觉,知道一切运转良好,如果有问题,你也可以快速诊断。

查看第 6 课以了解如何使用你的预测和来自 GCP 存储桶的监控指标,利用 FastAPI 和 Streamlit 构建一个 Web 应用。

此外, 你可以在这里访问 GitHub 仓库

💡 我的目标是帮助机器学习工程师在设计和生产化 ML 系统方面提升技能。关注我在 LinkedIn 或订阅我的 每周通讯 获取更多见解!

🔥 如果你喜欢阅读类似的文章并希望支持我的写作,考虑 成为 Medium 会员。通过使用 我的推荐链接,你可以在没有额外费用的情况下支持我,同时享受 Medium 丰富故事的无限访问。

[## 使用我的推荐链接加入 Medium - Paul Iusztin

🤖 加入以获取有关设计和构建生产就绪 ML 系统的独家内容 🚀 解锁完整访问…

pauliusztin.medium.com

参考资料

[1] 丹麦 API 的 DE35 行业代码能耗丹麦能源数据服务

[2] 企业 AI 数据验证:在 Hopsworks 中使用 Great Expectations(2022),Hopsworks 博客

娱乐数据科学:流媒体与影院

原文:towardsdatascience.com/entertainment-data-science-streaming-vs-theatrical-af948b69a8f7?source=collection_archive---------6-----------------------#2023-12-08

不同之处与相似之处

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Danny Kim, PhD

·

关注 发表在 Towards Data Science ·9 分钟阅读·2023 年 12 月 8 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

照片由 Krists Luhaers 拍摄,发布在 Unsplash

在我在Towards Data Science上的娱乐数据科学的新前沿文章中,我提到数据科学如何应用于内容生命周期的各个阶段,从绿灯到制作再到发布。虽然很容易理解数据科学应用在决定哪些剧本应该获得绿灯和优化制作成本之间可能会有所不同,但即使在那些乍一看似乎相对相似的背景下,也可能存在显著差异。

大约一年半前,我在一家大型电影公司开始了新工作。由于之前的工作是在流媒体技术方面,我预期情况会多多少少相似,只是这次我将专注于电影数据,而不是电视和电影数据。使用数据预测受欢迎程度,情况会有多大不同呢?

哎呀,我真的是毫无头绪。

业务完全不同。问题不同,利益相关者不同,数据不同,等等。因此,我想写这篇文章有两个目标。第一个明显的目标是向有志于娱乐数据领域的初级专业人士展示数据科学工作在影院和流媒体背景下的差异。但我想这种动态在许多不同的行业中也可能会显现——你认为你会做大致相同的预测 Y 使用 X 的工作,却发现 X 和 Y 的实际情况完全不同——所以第二个更广泛的目标是让各个领域的数据专业人士了解到,即使两个职位在表面上看起来功能上类似,它们在深入探究数据和业务问题后也可能在各种方面截然不同。

基于此,以下是我从流媒体娱乐数据科学转到影院娱乐数据科学后的关键观察。我略过了一些显而易见的“显而易见”点(哦,原来没有影院电视剧发布,真是意外),但我触及了一些主要趋势。当然,这些并不是一些绝对真理的陈述;根据公司、团队领导等情况,你的体验可能会有所不同。此外,虽然数据科学可以在娱乐内容生命周期的早期阶段发挥作用,如我上面提到的,这篇文章来源于我在接近发布的下游流程中的经验。如果我有时显得有些模糊,那是故意的,因为我不想泄露任何秘密调料 😉

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由Joshua Sortino提供,发布在Unsplash上。

数据范围

最明显的区别在于数据的范围。在戏剧数据科学中,主要的分析单位是电影,也许是电影-国家,而在特定年份里,每个国家上映的电影数量有限!这与流媒体数据科学相对,在流媒体中,你通常可以获得来自历史的个人-内容-时间级别的数据,导致数据集大得多(也需要不同的工具来处理它们)。

这并不是说你从未在戏剧方面处理更大、更详细的数据集;这些数据集通常与标题或标题的某个元素相关,你常常以某种方式处理它们,以生成与标题相关的见解。但底线是,由于标题空间默认较小,因此数据范围也较小。我希望有一天,我们可以在戏剧领域获得更多像流媒体领域那样的细粒度、个体层面的消费见解,但至少现在情况不是这样。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Michael Marais 的照片,来源于 Unsplash

无* 历史数据

在流媒体中,绝大多数情况下(流媒体独占发布的明显例外),你将拥有大量历史数据可供使用。标题在票房上赚了多少钱?它发布时的社交媒体热度如何?这个标题在《烂番茄》上的表现如何?

在戏剧领域,你无法享受这种奢侈。确实,你可以在一定程度上依赖特定组件的历史数据,无论是演员、剧组、类型还是某种组合,但即便如此,这些数据点通常也不会像字面历史数据那样与特定标题清晰关联。此外,这种比较可能充满主观性问题和外部干扰因素;如何决定哪些标题真正可以相互比较?营销和营销活动的差异在公众对标题间相似性的认知中扮演了什么角色?

是的,系列和特许经营在某种程度上是这个规则的半例外,但过度依赖续集效应和相似性假设很容易适得其反。是的,在许多情况下,前作的表现可以较好地估计其继任者的表现,但系列可能会随着时间的推移失去动力或因超出认知范围(新角色/情节和与过去标题的弱联系)而无法识别,以至于早期标题的表现对预测新标题的表现可能毫无意义。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

照片由Scott Graham提供,来源于Unsplash

非常特别的数据

来自由技术人员管理的数据团队的初创流媒体技术领域,我花了大量时间研究哪些数据集可能对我们的需求有用。在这一过程中,我发现了许多适合各种需求的冷门数据集,并调查了我们如何能够以低成本收集那些供应商以高价出售的数据(例如,我们如何在不支付昂贵许可费用的情况下获取 Google 搜索数据?)。

在戏剧领域,标准和惯例似乎更为成熟。某些消费者和社交媒体数据集或数据集类型在行业内几乎被所有人使用。例如,虽然社交聆听可能作为一个显而易见的当代数据来源,但存在一些主要的成熟供应商提供详细的预发布和发布后消费者数据,这些供应商中的一些已经存在了几十年。这些是许多不在戏剧领域的人可能从未听说过的数据集,但当你身处其中时,它们是你谈论的所有内容。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

照片由Marten Bjork提供,来源于Unsplash

无窗口(或单一窗口)

在流媒体领域,可用窗口——以及在某种程度上,可用性的性质——是分析中的一个重要因素。这些窗口可能与各种内容(例如,标题是否与圣诞节有关,而窗口是圣诞节窗口?)和市场级因素(例如,标题是否在首页显著展示?)的因素相互作用。

正如你所想象的那样,这些顾虑在戏剧数据科学中较少存在——或者更准确地说,除非你在做与“我们是否应该制作这个标题?”或“我们什么时候应该发布这个标题?”相关的上游建模,否则在你参与之前,关于窗口因素的所有问题已经以(可能的)发布日期的形式为你决定好了。你只需担心一个窗口(除非你需要担心错开的发布日期,那是完全不同的情况),而上层决策者已经决定了发布日期。现在你需要在这个窗口的背景下尽力提供所有有用的见解。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

照片由Christian Joudrey提供,来源于Unsplash

更加重视业务

在我从事流媒体工作的期间,我们很容易将标题和观众视作数字,因为我们拥有成千上万的数据,但这种哲学也反映在方法论上。我们不仅会生成总结性统计数据,还会将所有内容转化为某种向量嵌入(即一系列在某些人类不可观测的维度上有意义的数字),即使这可能会牺牲可解释性;说“内容维度 2 是模型中最重要的变量”并不真正有意义。

流媒体服务更像科技公司运营,而戏剧化方面则更接近传统的制片公司业务。在戏剧化方面,除了数字之外,更注重业务本身。数据的存在不仅仅是为了数字本身,而是为了提供可操作的见解给组织中的各种利益相关者,其中许多人既不是数据科学家,也不在日常工作中使用数据。做出准确的预测很重要,但同样重要的是可解释性,而不是仅仅为了减少模型误差的微小百分比而急于放弃可解释性。因此,我觉得自己在所做的工作中与业务和观众之间有了更多的联系。

关键要点和结论

在我讨论流媒体到戏剧化转变的过程中,我涉及了各种主题,但潜在的主题包括询问那些在同一行业中从事两个松散类似的工作的人的相关问题。因此,总结一下,在你认为你的下一份工作将会大致与上一份工作相似之前,根据上述提到的流媒体与戏剧化数据科学之间的差异,这里有一些值得更深入思考的问题,以帮助你思考你上一份工作和下一份工作之间的相似性和差异:

  • 数据范围: 数据的单位是什么?数据添加的频率和每次的单位数量是多少?因此,数据集有多大,需要什么工具来处理这样一个数据集?

  • 历史数据的可用性: 如果有的话,提供了哪些类型的历史数据?可用的历史数据是否直接适用,还是需要某种聚合、插补或相似性分析?

  • 数据来源: 使用了哪些数据来源?这些数据来源是更一般性相关的,还是非常特定于某些上下文的?是否有实验新数据来源或搁置现有数据来源的空间?大家都在使用哪些既定的、传统的数据集?

  • 时间要素: 你必须回答的特定问题的相关时间窗口是什么?如何决定?是单一的还是多个的,是固定的还是不断变化的?在工作中如何考虑时间和相关因素(例如季节性、假期等)?某个特定的时间窗口对业务是否比其他时间窗口更重要?

  • 业务重点: 受众是谁?鉴于此,需要在准确性和可解释性之间找到什么样的平衡?这又如何影响你认为有用的特征?业务的节奏如何推动工作的节奏?

显然,我被聘为目前的职位是因为我的技能集与职位职责相关,并且我所做的与我之前做的类似——但与流媒体方面相比,剧院方面的数据科学在某种程度上既相似又不同。如我上文所述,数据不同,流程不同,期望不同。如果你希望进入激动人心的娱乐数据科学领域,或考虑转到相似但不同的工作领域,我希望你能发现这篇文章对你有用!

在撰写时,丹尼·金(宾夕法尼亚大学博士;《福布斯》30 岁以下 30 人名单 2022)是索尼影业娱乐电影集团市场分析与洞察团队的高级数据科学家。丹尼之前在 Whip Media 和派拉蒙影业工作过,他是宾夕法尼亚大学和南加州大学安纳伯格传播学院、沃顿商学院和南加州大学电影艺术学院的校友。

实体解析:识别嘈杂数据中的真实世界实体

原文:towardsdatascience.com/entity-resolution-identifying-real-world-entities-in-noisy-data-3e8c59f4f41c?source=collection_archive---------3-----------------------#2023-09-21

基本理论和 Python 实现

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Tomonori Masui

·

关注 发表在 Towards Data Science ·19 分钟阅读·2023 年 9 月 21 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者使用 Midjourney 生成

在当今数据驱动的世界中,组织常常面临多样且不一致的数据来源的挑战。实体解析,也称为记录链接或去重,帮助识别和合并在数据集内或跨数据集没有共享唯一标识符的重复或相关记录。准确的实体解析提高了数据质量,增强了决策制定,并提供了有价值的见解。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

实体解析在不一致的数据源中识别相同的现实世界实体(图像由作者提供)

实体解析适用于各种行业。例如,CRM 系统通过解析重复的客户记录来整合客户信息、改善服务和实现精准营销。电子商务平台使用实体解析来合并产品列表,提升搜索功能、推荐系统和客户体验。

在这篇文章中,我们将探讨使用基准数据集的基本实体解析方法的技术细节。

目录

  • 实体解析概述

  • 基准数据集

  • 阻断

  • 块处理

  • 实体匹配

  • 聚类

  • 集群评估

实体解析概述

标准的实体解析(ER)框架包括几个步骤:阻断、块处理、实体匹配和聚类。

1. 阻断:这是实体解析的第一步,旨在通过将数据集划分为较小、可管理的块来减少搜索空间,从而识别相同的实体。这些块包含共享类似属性的记录,使后续的比较更加高效。

2. 块处理:此步骤通过丢弃两种不必要的比较来精化块,从而最小化比较的数量:冗余比较,即在多个块中重复出现的比较,以及无用比较,即涉及不太可能匹配的记录的比较。

3. 实体匹配:这一阶段专注于比较块中的记录,以根据记录的相似性寻找匹配。可以使用各种相似性度量和匹配算法来将记录对分类为匹配或不匹配。

4. 聚类:聚类涉及根据记录的相似性将匹配的记录分组到集群中。创建的集群可以用于获取实体的综合视图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

实体解析工作流程(图像由作者提供)

基准数据集

在接下来的章节中,我们将深入探讨实体解析过程中的每个步骤的更多细节,并使用基准数据集进行 Python 实现。

该数据集来源于莱比锡大学数据库组,并获得了创作共用许可证,来源于实际的MusicBrainz数据库中有关歌曲的记录,但经过DAPO 数据污染工具故意进行了修改。该工具向数据集中注入了重复项和错误,导致数据集中包含了 50%原始记录的重复项,覆盖了两个到五个来源。这些重复项具有较高的损坏程度,作为评估 ER 和聚类方法有效性的严格测试。

我们可以使用以下代码加载数据。

import requests
from io import BytesIO
import pandas as pd

url = "https://raw.githubusercontent.com/tomonori-masui/entity-resolution/main/data/musicbrainz_200k.csv"
res = requests.get(url)
df = pd.read_csv(BytesIO(res.content))

一些示例记录如下所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

每条记录代表一首歌曲,具有诸如艺术家、标题、专辑、年份等属性(你可以在这个链接中找到字段描述)。CID是集群 ID,具有相同CID的记录是重复的(在上面的示例中,所有三条记录代表同一首歌曲)。我们的目标是在这个嘈杂的数据集中识别这些重复项。

为了简化工作,我们只关注英文歌曲。下面的代码识别出具有英文歌曲的集群 ID 的记录。

english_cids = df[
    df.language.str.lower().str.contains("^en|^eg", na=False)
].CID.unique()

df = df[df.CID.isin(english_cids)].reset_index(drop=True)

我们还对一些字符串字段进行预处理,以获得标准化的值。

for col in ["title", "artist", "album"]:
    df[col] = (
        df[col]
        .str.lower()
        .replace("[^a-z0-9]", " ", regex=True)  # replacing special characters with a space
        .replace(" +", " ", regex=True)         # removing consecutive spaces
        .str.strip()                            # removing leading and tailing spaces
    )

df.loc[df.number.notna(), "number"] = (
    df[df.number.notna()]
    .number.replace("[⁰-9]", "", regex=True)              # removing non-digits
    .apply(lambda x: str(int(x)) if len(x) > 0 else None)  # removing leading zeros
)

请注意,这个基准数据集是一个单一的数据集,如果你有多个数据来源需要解决实体问题,你需要标准化它们的数据模式,并将这些多个数据源整合成一个统一的数据集,然后再继续后续步骤。

阻断

阻断是实体解析的第一步,它根据某些属性将相似记录分组。通过这样做,过程将搜索范围缩小到仅考虑每个块内的比较,而不是检查数据集中的所有可能记录对。这显著减少了比较的数量,加快了 ER 过程。由于跳过了许多比较,这可能会导致错过真实匹配。因此,阻断应该在效率和准确性之间取得良好的平衡。在本节中,我们将探索三种不同的阻断方法(标准阻断、标记阻断和排序邻域),以找到最佳的平衡点。

标准阻断

最直接的块处理技术是根据特定属性将数据集划分为块。例如,在我们的数据集中,可以根据 ArtistTitle 字段创建块。这种方法直观且易于实现,但其有效性对噪声非常敏感,因为重复项的阻塞键有一点点不同就会把它们放在不同的块中。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在艺术家字段上的标准块示例(图像由作者提供)

我们可以使用以下函数获得标准块。字典 blocks 将存储阻塞键(key)及其对应的已阻塞记录的索引(idx)。

from collections import defaultdict

def standard_blocking(field_values: pd.Series) -> dict[str, list]:

    blocks = defaultdict(list)
    for idx, key in enumerate(field_values):
        if key is not None:
            blocks[key].append(idx)

    return blocks

在以下代码中,我们使用 titleartistalbum 字段创建三个独立的标准块。

sb_title = standard_blocking(df.title)
sb_artist = standard_blocking(df.artist)
sb_album = standard_blocking(df.album)

令牌块

令牌块处理的重点是将属性值分解(即令牌化)为更小的单位,称为令牌,然后使用这些令牌创建用于比较的块。令牌通常是从文本中提取的单个单词或小的 n-gram(长度为n的子字符串)。令牌块为每个不同的令牌值创建一个块,而不考虑相关属性:如果两个记录在其任何属性中共享一个令牌,它们将位于同一个块中。这产生了高召回率,因为冗余(即单个记录可以属于多个块),代价是低精确度。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

令牌块的示例(图像由作者提供)

以下函数基于单词令牌生成令牌块。请注意,我们排除了停用词(例如“a”、“the”、“is”等)中的令牌。

from nltk.tokenize import word_tokenize

def token_blocking(df: pd.DataFrame, stop_words: set) -> dict[str, list]:

    blocks = defaultdict(list)

    for i, row in enumerate(df.itertuples()):

        # concatenate columns and tokenize
        string = " ".join([str(value) for value in row if not pd.isna(value)])
        tokens = set(
            [word for word in word_tokenize(string) if word not in stop_words]
        )

        # create blocks
        for token in tokens:
            blocks[token].append(i)

    return blocks

由于我们知道哪些字段与创建块相关,我们仅使用特定字段(titleartistalbum)来执行令牌块处理:

import string
from nltk.corpus import stopwords

columns = ['title', 'artist', 'album']
stop_words = set(stopwords.words('english') + list(string.punctuation))
token_blocks = token_blocking(df[columns], stop_words)

排序邻域

排序邻域按特定字段的值进行字母顺序排序。一个固定大小的窗口在排序后的记录上滑动,窗口内的所有可能对被标识为比较的候选对。请注意,它直接生成一对对的列表,而不是块。虽然这种方法有效处理了阻塞字段中的噪声,但选择较小的窗口会牺牲召回率以提高精度,而较大的窗口具有更高的召回率但精度较低。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

带有窗口大小为 3 的排序邻域示例(图像由作者提供)

以下代码执行窗口大小为 3 的排序邻域,使用 titleartistalbum 字段作为排序键。

def sorted_neighborhood(
    df: pd.DataFrame, keys: list, window_size: int = 3
) -> np.ndarray:

    sorted_indices = (
        df[keys].dropna(how="all").sort_values(keys).index.tolist()
    )
    pairs = []
    for window_end in range(1, len(sorted_indices)):
        window_start = max(0, window_end - window_size)
        for i in range(window_start, window_end):
            pairs.append([sorted_indices[i], sorted_indices[window_end]])

    return np.array(pairs)

columns = ['title', 'artist', 'album']
sn_pairs = sorted_neighborhood(df, columns)

我们将在接下来的两个部分中进行块处理和实体匹配后,比较这三种方法的性能。

块处理

这一步旨在提高块的精度,同时保持可比较的召回率水平。相关技术包括在输入块集合B内减少不必要和冗余的比较,从而生成一个具有改进精度的新块集合B'。我们将在本节探讨一些主要的块处理技术。

块清理

块清理设置块大小的上限,并清除大小超过限制的块。它假设过大的块由冗余比较主导,这意味着这些块中包含的重复项更可能出现在其他较小的块中。

下面的代码按预定的限制值(此处设为 1000 条记录)清除块。它还过滤掉只有一条记录的块,因为这些块不生成可比较的对。我们在前一节的三个标准块和标记块上执行此purge_blocks函数。

def purge_blocks(
    blocks: dict[str, list], purging_threshold: int = 1000
) -> dict[str, list]:

    blocks_purged = {
        key: indices
        for key, indices in blocks.items()
        if len(indices) < purging_threshold and len(indices) > 1
    }

    return blocks_purged

token_blocks = purge_blocks(token_blocks)
sb_title = purge_blocks(sb_title)
sb_artist = purge_blocks(sb_artist)
sb_album = purge_blocks(sb_album)

元块

元块转换输入块集合为图(或邻接矩阵),其中每个节点对应一条记录,边连接每对在块中共同出现的记录。边权重表示跨块对出现频率:权重越高,匹配可能性越大。低权重的边被剪枝,因为它们可能代表多余的比较。因此,对于每个保留的边,生成一个新块,导致精细化的块集合(或作为每个精细化块仅有一对记录的对列表)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

元块示例(作者提供的图片)

我们仅对标记块执行元块,因为它们在块之间有很多重叠。下面的代码首先从标记块中创建一对对的列表,然后将其转换为邻接矩阵。

import itertools
from scipy.sparse import csr_matrix

def get_pairs_from_blocks(blocks: dict[str, list]) -> list[list]:
    return [
        pair
        for indices in blocks.values()
        for pair in list(itertools.combinations(indices, 2))
    ]

def get_adjacency_matrix_from_pairs(
    pairs: list[list], matrix_shape: tuple[int, int]
) -> csr_matrix:

    idx1 = [pair[0] for pair in pairs]
    idx2 = [pair[1] for pair in pairs]
    ones = np.ones(len(idx1))

    return csr_matrix(
        (ones, (idx1, idx2)), shape=matrix_shape, dtype=np.int8
    )

pairs = get_pairs_from_blocks(token_blocks)
adj_matrix = get_adjacency_matrix_from_pairs(pairs, (len(df), len(df)))

接下来,我们根据边权重在邻接矩阵中剪枝。在这里,我们剪枝所有边权重为 1 的边,即仅在单个块中出现的对被修剪。

def prune_edges(
    adj_matrix: csr_matrix,
    edge_weight_threshold: float,
) -> csr_matrix:

    adj_matrix_pruned = adj_matrix >= edge_weight_threshold

    return adj_matrix_pruned

adj_matrix = prune_edges(adj_matrix, edge_weight_threshold=2)

接着,我们从经过剪枝的邻接矩阵中获取对。

def get_pairs_from_adj_matrix(adjacency_matrix: csr_matrix) -> np.ndarray:
    return np.array(adjacency_matrix.nonzero()).T

tb_pairs = get_pairs_from_adj_matrix(adj_matrix)

块的并集

对于标准块,我们获取三个独立块的并集。首先,我们将块转换为邻接矩阵列表。

adj_matrix_list = []
for blocks in [sb_title, sb_artist, sb_album]:
    pairs = get_pairs_from_blocks(blocks)
    adj_matrix_list.append(
        get_adjacency_matrix_from_pairs(pairs, (len(df), len(df)))
    )

接着,我们从矩阵的并集和其中的候选对中获取结果。

def get_union_of_adj_matrices(adj_matrix_list: list) -> csr_matrix:

    adj_matrix = csr_matrix(adj_matrix_list[0].shape)
    for matrix in adj_matrix_list:
        adj_matrix += matrix

    return adj_matrix

adj_matrix_union = get_union_of_adj_matrices(adj_matrix_list)
sb_pairs = get_pairs_from_adj_matrix(adj_matrix_union)

下表总结了三种不同块处理方法生成的最终候选对数量。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们将通过查看下一节中的匹配结果来确定哪个最适合我们的数据。

实体匹配

在这一步骤中,我们从上一步生成的候选对中识别匹配对。虽然有多种方法可以找到匹配,但一种简单直接的方法可以如下概述:

  1. 在每个属性上测量相似度

    你可以使用任何相似性度量,如余弦相似度、杰卡德相似度或莱文斯坦距离相似度,根据你的数据或具体要求的适用性。在计算某些度量的相似性之前,文本字段可能需要进行分词。

  2. 计算整体相似性

    此步骤将每个属性的相似性组合成一个整体的相似性分数,可以通过应用手动定义的规则或利用在标记数据上训练的机器学习模型来实现。

  3. 确定匹配

    对整体相似性分数应用相似性阈值以找到匹配

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

实体匹配示例(图片由作者提供)

以下函数 get_field_similarity_scores 处理上述第 1 步。如果 sim_type 设置为 “fuzzy”,则计算 余弦相似度;否则,进行精确匹配。余弦相似度是在字符级 3-grams 上计算的,这些 3-grams 通过使用来自 scikit-learn 的 [CountVectorizer](https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.CountVectorizer.html) 模块从输入字符串中向量化。我们计算 titleartistalbum 字段的余弦相似度,同时对 number 字段进行精确匹配。

from sklearn.preprocessing import normalize
from sklearn.feature_extraction.text import CountVectorizer

def get_field_similarity_scores(
    df: pd.DataFrame, pairs: np.ndarray, field_config: dict[str, str]
) -> dict[str, np.ndarray]:
    """
    Measuring similarity by field. It is either cosine similarity
    (if sim_type == 'fuzzy') or exact match 0/1 (if sim_type == 'exact'). 
    Attribute's similarity scores are stored in field_score dictionary 
    with the field name as key.
    """

    field_scores = {}

    for field, sim_type in field_config.items():
        if sim_type == "fuzzy":
            field_scores[field] = cosine_similarities(
                df[field].fillna(""), pairs
            )
        else:
            field_scores[field] = exact_matches(df[field], pairs)

    return field_scores

def cosine_similarities(
    field_values: pd.Series, pairs: np.ndarray
) -> np.ndarray:
    """
    Computing cosine similarities on pairs
    """

    token_matrix_1, token_matrix_2 = get_token_matrix_pair(
        field_values, pairs
    )
    cos_sim = cosine_similarities_on_pair_matrices(
        token_matrix_1, token_matrix_2
    )

    return cos_sim

def get_token_matrix_pair(
    field_values: pd.Series, pairs: np.ndarray,
) -> tuple[csr_matrix, csr_matrix]:
    """
    Converting pairs into matrices of token counts (matrix of records 
    by tokens filled with token counts). 
    """

    all_idx = np.unique(pairs)
    vectorizer = CountVectorizer(analyzer="char", ngram_range=(3, 3))
    vectorizer.fit(field_values.loc[all_idx])
    token_matrix_1 = vectorizer.transform(field_values.loc[pairs[:, 0]])
    token_matrix_2 = vectorizer.transform(field_values.loc[pairs[:, 1]])

    return token_matrix_1, token_matrix_2

def cosine_similarities_on_pair_matrices(
    token_matrix_1: csr_matrix, token_matrix_2: csr_matrix
) -> np.ndarray:
    """
    Computing cosine similarities on pair of token count matrices.
    It normalizes each record (axis=1) first, then computes dot product
    for each pair of records.
    """

    token_matrix_1 = normalize(token_matrix_1, axis=1)
    token_matrix_2 = normalize(token_matrix_2, axis=1)
    cos_sim = np.asarray(
        token_matrix_1.multiply(token_matrix_2).sum(axis=1)
    ).flatten()

    return cos_sim

def exact_matches(
    field_values: pd.Series, pairs: np.ndarray
) -> np.ndarray:
    """
    Performing exact matches on pairs
    """

    arr1 = field_values.loc[pairs[:, 0]].values
    arr2 = field_values.loc[pairs[:, 1]].values

    return ((arr1 == arr2) & (~pd.isna(arr1)) & (~pd.isna(arr2))).astype(int)

field_config = {
    # <field>: <sim_type>
    "title": "fuzzy",
    "artist": "fuzzy",
    "album": "fuzzy",
    "number": "exact",
}

field_scores_sb = get_field_similarity_scores(df, sb_pairs, field_config)

基于规则的匹配

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

基于规则的匹配(图片由作者提供)

在计算特定领域的相似性分数后,我们希望将它们组合成一个整体的相似性分数,如上述第 2 步所述。我们在这里采用了一个非常简单的方法:仅计算属性分数的平均值,随后应用一个分数阈值来识别匹配(第 3 步)。下面的阈值值已在这里调整过,但你可能需要通过查看一些匹配/不匹配的示例来调整它,当你处理自己的数据集时。

def calc_overall_scores(field_scores: dict[str, np.ndarray]) -> np.ndarray:
    return np.array(list(field_scores.values())).mean(axis=0)

def find_matches(scores: np.ndarray, threshold: float) -> np.ndarray:
    return scores >= threshold

scores_sb = calc_overall_scores(field_scores_sb)
is_matched_sb = find_matches(scores_sb, threshold=0.64)

上面的代码对标准块中的对进行了匹配。此外,我们将此匹配过程扩展到来自 token block 和 sorted neighborhood 的对,允许我们比较它们的表现。下面的代码在表格中总结了比较结果。

from IPython.display import display
from collections import Counter

def show_results(
    is_matched_list: list[np.ndarray],
    blocking_approach_name_list: list[str],
):

    result = pd.DataFrame(
        [Counter(is_matched).values() for is_matched in is_matched_list],
        columns=["Unmatch", "Match"],
    )
    result["Blocking Approach"] = blocking_approach_name_list
    result["Matching Rate"] = result.Match / (
        result.Match + result.Unmatch
    )
    result["Matching Rate"] = result["Matching Rate"].map("{:.1%}".format)
    result["Match"] = result["Match"].map("{:,}".format)
    result["Unmatch"] = result["Unmatch"].map("{:,}".format)

    display(
        result[["Blocking Approach", "Match", "Unmatch", "Matching Rate"]]
    )

is_matched_list = [is_matched_sb, is_matched_tb, is_matched_sn]
blocking_approach_name_list = [
    "Standard Blocking",
    "Token Blocking",
    "Sorted Neighborhood",
]
show_results(is_matched_list, blocking_approach_name_list)

下面是输出结果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如表中所示,Token Blocking 产生了最多的匹配数,而 Sorted Neighborhood 的匹配率最高。由于 Token Blocking 可能错过的匹配最少,我们将继续使用这种方法的结果。值得注意的是,我们的小数据集并未显示出可扩展性问题。然而,对于较大的数据集,其中 Token Blocking 可能不可行,你可能需要考虑其他更具可扩展性的方法。

机器学习匹配

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

机器学习匹配(图片由作者提供)

如果您有标记数据或手动标记的样本对(作为匹配或非匹配),您可以训练一个机器学习模型来预测匹配的对。由于我们的数据具有集群标签CID,我们将这些转换为对的匹配标签(匹配/非匹配),并训练一个机器学习模型,随后与前一节中执行的基于规则的方法进行性能比较。

以下代码生成模型输入X和相应的目标变量y。同一集群CID内的对被标记为匹配(y = 1),而不同集群内的对被标记为非匹配(y = 0)。

def get_x_y(
    field_scores: dict[str, np.ndarray],
    pairs: np.ndarray,
    df: pd.DataFrame,
) -> tuple[pd.DataFrame, np.ndarray]:

    X = pd.DataFrame(field_scores)
    y = df.loc[pairs[:, 0], "CID"].values == df.loc[pairs[:, 1], "CID"].values

    return X, y

X, y = get_x_y(field_scores_tb, tb_pairs, df)

接下来,我们将它们分为训练集和测试集,然后训练逻辑回归模型。

from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.5, random_state=42
)

model = LogisticRegression(random_state=0).fit(X_train, y_train)

下面的代码将其性能与基于规则的方法进行比较([f1_score](https://scikit-learn.org/stable/modules/generated/sklearn.metrics.f1_score.html))。

from sklearn.metrics import f1_score

y_pred = model.predict(X_test)
print(f"Model f1_score: {f1_score(y_test, y_pred):.3f}")

y_rule_base = is_matched_tb[X_test.index.values]
print(f"Rule-base f1_score: {f1_score(y_test, y_rule_base):.3f}")

以下是输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

尽管模型的性能更好,基于规则的方法的性能仍然可能相当不错。

对于接下来的步骤,我们将使用通过基于规则的方法识别出的匹配项,考虑到在许多实际情况下,由于资源限制,手动数据标记可能并不实际。下面的代码从候选对中提取匹配对及其相似性分数及其在标记阻塞上的分数。

matched_pairs = tb_pairs[is_matched_tb]
matched_scores = scores_tb[is_matched_tb]

聚类

在这一步中,我们基于上一步中的匹配对创建实体集群。每个集群包括所有对应于一个不同现实世界实体的记录。

实体解析的聚类有几个要求:

  1. 无约束算法

    算法不应需要任何领域特定的参数作为输入,例如集群数或集群直径。

  2. 处理不完整相似性矩阵的能力

    由于实体解析过程不会在每个可能的对上计算相似性(可以描述为 N 乘以 N 矩阵),因此算法必须能够处理不完整的相似性矩阵(或匹配对列表)。

  3. 可扩展性

    实体解析通常处理大型数据集,因此算法能够处理此类数据非常重要。在大数据情况下,像层次聚类这样的高复杂度算法可能不实际。

对于聚类,我们将研究三种主要的单遍聚类算法:分区(即连接组件)、中心聚类和合并中心聚类,它们都满足要求。这些算法非常高效,因为它们通过一次扫描(或 O(n)时间复杂度)候选对列表来创建集群,尽管其中一些算法要求列表按相似性分数排序。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

单遍聚类算法(来源:www.vldb.org/pvldb/vol2/vldb09-1025.pdf

划分/连通组件

该算法通过最初将每个节点分配到其单独的集群来启动聚类。然后,它对匹配对的列表进行单次扫描。如果发现不属于同一集群的连接节点,它将合并它们的集群。简而言之,它通过将所有连接节点通过边(即通过配对的匹配记录)分组形成一个集群。该算法可能会创建通过长路径连接不相似记录的集群。

连通组件聚类可以使用 Scipy 模块执行,如下面的代码所示。在执行之前,你需要将配对列表转换为邻接矩阵。

from scipy.sparse.csgraph import connected_components

def connected_components_from_pairs(
    pairs: np.ndarray, dim: int
) -> np.ndarray:

    adjacency_matrix = get_adjacency_matrix_from_pairs(pairs, (dim, dim))
    _, clusters = connected_components(
        csgraph=adjacency_matrix, directed=False, return_labels=True
    )

    return clusters

cc_clusters = connected_components_from_pairs(matched_pairs, len(df))

中心聚类

该算法[5]执行聚类,其中每个集群都有一个中心,并且每个集群中的所有记录都与该集群的中心相似。它要求相似对的列表按相似度分数的降序排序。然后,算法通过对排序列表的单次扫描来执行聚类。当第一次在扫描中遇到节点u时,它被指定为集群中心。任何后续与u相似的节点v(即,出现在列表中的对(u, v)中)都被分配到u的集群中,并且在处理过程中不再考虑。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

中心聚类示例(图像由作者提供)

合并中心聚类

该算法[6]的功能类似于中心聚类,但每当一个与集群cᵢ的中心相似的记录也与cⱼ的中心相似时,就会合并两个集群cᵢcⱼ。请注意,当两个集群合并时,它不会选择一个单一的中心节点,这意味着合并的集群可以有多个中心节点。该算法可以通过类似的方式进行,即通过对相似对的列表进行单次扫描,同时跟踪通过合并集群连接的记录。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

合并中心聚类示例(图像由作者提供)

要执行中心/合并中心聚类,我们首先需要按相似度分数的降序对配对列表进行排序。

def sort_pairs(pairs: np.ndarray, scores: np.ndarray) -> np.ndarray:
    sorted_ids = (-1 * scores).argsort()
    return pairs[sorted_ids]

pairs_sored = sort_pairs(matched_pairs, matched_scores)

接下来,下面的代码生成两个配对集合:中心-子节点配对,表示为center_cluster_pairs,和合并节点配对,称为merge_cluster_pairs。然后,我们可以通过将连通组件应用于这些配对列表来生成中心集群和合并中心集群。

def get_center_cluster_pairs(pairs, dim):

    """
    cluster_centers: 
        a list tracking cluster center for each record.
        indices of the list correspond to the original df indices
        and the values represent assigned cluster centers' indices
    center_cluster_pairs: 
        a list of pairs of indices representing center-child pairs
    merge_cluster_pairs:
        a list of pairs of merged nodes' indices
    """
    cluster_centers = [None] * dim
    center_cluster_pairs = []
    merge_cluster_pairs = []

    for idx1, idx2 in pairs:

        if (
            cluster_centers[idx1] is None
            or cluster_centers[idx1] == idx1
            or cluster_centers[idx2] is None
            or cluster_centers[idx2] == idx2
        ):
            # if both aren't child, those nodes are merged
            merge_cluster_pairs.append([idx1, idx2])

        if cluster_centers[idx1] is None and cluster_centers[idx2] is None:
            # if both weren't seen before, idx1 becomes center and idx2 gets child
            cluster_centers[idx1] = idx1
            cluster_centers[idx2] = idx1
            center_cluster_pairs.append([idx1, idx2])
        elif cluster_centers[idx2] is None:
            if cluster_centers[idx1] == idx1:
                # if idx1 is center, idx2 is assigned to that cluster
                cluster_centers[idx2] = idx1
                center_cluster_pairs.append([idx1, idx2])
            else:
                # if idx1 is not center, idx2 becomes new center
                cluster_centers[idx2] = idx2
        elif cluster_centers[idx1] is None:
            if cluster_centers[idx2] == idx2:
                # if idx2 is center, idx1 is assigned to that cluster
                cluster_centers[idx1] = idx2
                center_cluster_pairs.append([idx1, idx2])
            else:
                # if idx2 is not center, idx1 becomes new center
                cluster_centers[idx1] = idx1

    return center_cluster_pairs, merge_cluster_pairs

center_cluster_pairs, merge_cluster_pairs = get_center_cluster_pairs(pairs_sored, len(df))
ct_clusters = connected_components_from_pairs(center_cluster_pairs, len(df))
mc_clusters = connected_components_from_pairs(merge_cluster_pairs, len(df))

集群评估

有了集群标签后,我们可以使用Rand Index调整后的 Rand Index来评估集群的质量。Rand Index 是一种集群评估指标,表示正确聚类在一起或分开的配对比例。其定义如下:

TP = 在预测簇和真实簇中 同时 被聚类的对数。

TN = 在预测簇和真实簇中 分开 被聚类的对数。

Rand Index = (TP + TN) / 所有可能对数的总数

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Rand Index 计算示例(图片由作者提供)

调整后的 Rand Index 是 Rand Index 的一种修改版本,已为偶然性进行修正。该调整考虑了随机分配的聚类结果可能产生的随机一致性。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

调整后的 Rand Index 的方程

我们不会深入探讨上述方程中每个术语的计算方式,但任何对这个主题感兴趣的人可以参考 KY Yeung 的论文,该论文解释了这个指标并提供了一些示例。

下面的代码提供了使用这些指标的簇比较以及一些额外的基本统计信息。

from sklearn.metrics.cluster import rand_score, adjusted_rand_score
from IPython.display import display

def get_stats(labels, clusters):

    stats = []
    stats.append(f"{rand_score(labels, clusters):.3f}")
    stats.append(f"{adjusted_rand_score(labels, clusters):.3f}")
    clus_dist = pd.Series(clusters).value_counts()
    stats.append(f"{len(clus_dist):,}")
    stats.append(f"{clus_dist.mean():.3f}")
    stats.append(f"{clus_dist.min():,}")
    stats.append(f"{clus_dist.max():,}")

    return stats

def compare_clusters(cluster_list, cluster_names, labels):

    stats_dict = {}
    for clusters, name in zip(cluster_list, cluster_names):
        stats = get_stats(labels, clusters)
        stats_dict[name] = stats

    display(
        pd.DataFrame(
            stats_dict,
            index=[
                "Rand Index",
                "Adjusted Rand Index",
                "Cluster Count",
                "Mean Cluster Size",
                "Min Cluster Size",
                "Max Cluster Size",
            ],
        )
    )

cluster_list = [cc_clusters, ct_clusters, mc_clusters]
cluster_names = ["Connected Components", "Center", "Merge-Center"]
compare_clusters(cluster_list, cluster_names, df.CID)

下面是输出结果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从表中可以看出,连接组件生成的簇较大且簇数最少,而连接组件与 Merge-Center 簇之间的差距最小。相反,Center 簇生成的簇较小且数量最多。请注意,所有三个簇的 Rand Index 都是完美的,因为它们有大量的簇,使得簇间对形成主导地位(即即使是随机簇也会得到相当的 Rand Index)。然而,如果你查看调整后的 Rand Index,Merge-Center 聚类表现最佳,其与连接组件的差异很小。

这就是我们对实体解析框架的探索。你如何处理创建的簇取决于你的具体业务需求或使用案例。如果你的目标是为每个簇建立规范的表示,你可以通过提取每个簇内每个字段的最具代表性的值(如最频繁的值)来实现。

如果你感兴趣,完整的代码可以在下面的 Google Colab 和 GitHub 仓库中找到。

## Google Colaboratory

实体解析

colab.research.google.com ## entity-resolution/entity_resolution_implementations.ipynb

实体解析

github.com

参考文献

[1] Christophides 等人,大数据端到端实体解析:综述(2019)

[2] Papadakis 等人,实体解析的近似阻塞技术比较分析(2016)

[3] Papadakis 等人,实体解析的阻塞和过滤技术综述(2020)

[4] Hassanzadeh 等人,用于重复检测的聚类算法评估框架(2009)

[5] Haveliwala 等人,用于网页聚类的可扩展技术(2000)

[6] Hassanzadeh & Miller,从重复数据中创建概率数据库(2009)

熵和基尼指数简介

原文:towardsdatascience.com/entropy-and-gini-index-c04b7452efbe

理解这些度量如何帮助我们量化数据集中的不确定性

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Gurjinder Kaur

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

你能告诉哪些是最纯净和最不纯净的购物车吗?(来源:作者提供的图像)

基尼指数是重要的机器学习概念,特别有助于决策树算法来确定分裂的质量。这两种度量虽然计算方式不同,但最终用于量化数据集中相同的事物,即不确定性(或不纯度)。

熵(或基尼指数)越高,数据越随机(混合)。

让我们直观地理解数据集中的不纯度,并了解这些度量如何帮助测量它。(不纯度、不确定性、随机性、异质性——这些都可以在我们的背景下互换使用,目标是最终减少它们以获得更好的清晰度)。

什么是不纯度——用例子解释

想象一下你和你的朋友——爱丽丝鲍勃一起去超市买水果。你们每个人都拿了一个购物车,因为你们都不喜欢分享水果。让我们看看你们都买了些什么(看起来你们很喜欢苹果!!):

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者提供的图像

这三辆购物车可以看作是三种不同的数据分布。如果我们假设最初有两个类别(苹果和香蕉),那么以下的解释将是不正确的。相反,应该将每辆购物车看作是不同的数据分布——第一个购物车的数据分布中所有数据点都属于同一类别,而第二和第三个购物车的数据分布中包含两个类别。

看看上面的例子,很容易识别出数据分布最纯净或最不纯净的购物车(类别分布)。但是,为了在数据集中进行数学化的纯度量化,以便算法可以利用它来做出决策,熵和基尼指数就显得尤为重要。

这两种度量都考虑了数据集中每个类别出现(或存在)的概率。在我们的例子中,每种情况都有 8 个数据点(水果),因此我们可以计算每个购物车的类别概率如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者提供

现在我们已经掌握了所有需要的知识,可以深入探讨熵和基尼指数的正式定义了!

如前所述,熵和基尼指数都是衡量数据中不确定性或随机性的度量。虽然它们旨在量化相同的基本概念,但各自有自己的数学公式和解释方法来实现这一点。

给定一个标签数据集,其中每个标签来自n个类别,我们可以按如下方式计算熵。这里,pi 是从类别 i 中随机选择一个元素的概率。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

为了确定决策树中的最佳划分,熵用于计算信息增益,并选择在节点处贡献最大信息增益的特征。

基尼指数

基尼指数试图通过回答这个问题来量化数据集中的随机性——从给定的数据中随机选择一个元素的错误标记概率是多少?

给定一个标签数据集,其中每个标签来自n个类别,计算基尼指数的公式如下。这里,pi 是从类别 i 中随机选择一个元素的概率。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这个公式通常也可以重新表述为:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

(注意:所有类别概率的总和为 1)。

基尼指数是信息增益的一种替代方法,可以用于决策树中确定划分的质量。在给定节点上,它比较划分前数据的基尼指数划分后两个分支基尼指数的加权和之间的差异,并选择差异最大(或基尼增益)的那个。如果这还不清楚,暂时不用担心,因为这需要更多的背景信息,本文的目标是让你对这些度量的基本含义有一个初步的了解。

回到我们的例子

为了更容易理解,以我们的购物车例子为参考,我们有三个数据集——C1、C2 和 C3,每个数据集有 8 条记录,标签来自两个类别——[苹果,香蕉]。使用上表中计算的概率,我们来逐步计算 Alice 的购物车的两个指标:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

同样,我们也可以计算 C1 和 C3 的这些指标,并得到以下结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者提供

从上表中,我们可以得出一些有趣的结论关于熵和基尼指数可能的值范围。我们将最低可能值称为 下界,将最大可能值称为 上界

下界

当我们的数据完全同质时,熵和基尼指数的下界都是 0。可以参考 cart C1。

上界

当数据具有最高的不确定性时,熵和基尼指数分别为 1 和 0.5(可以参考 cart C2,C2 代表了具有最高可能随机性的例子)。

需要注意的一点是,这些上界值仅适用于二分类(因为这就是我们的两类苹果-香蕉示例所代表的情况)。在 n 个类别且每个类别的概率相等的情况下,上界将是 n 的一个函数,如下所示。

熵的上界:

  • 对于二分类,熵的上界是 1。

  • 对于每个类别具有相同概率的多分类,熵的上界将是:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

基尼指数的上界:

  • 对于二分类, 基尼指数的上界通常不超过 0.5

  • 对于每个类别具有相同概率的多分类,基尼指数的上界将是:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

回顾

  • 熵和基尼指数用于量化数据集中的随机性,并且对于确定决策树中分裂的质量非常重要。我们可以在这里交替使用随机性、不确定性、杂质和异质性这些术语。

  • 熵和基尼指数的高值意味着数据中的随机性较高。

熵旨在量化数据集的不可预测性。

  • 计算熵的公式如下。这里 pi 是从标记为 i 的类别中选择一个元素的概率,给定 n 个总类别。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 如果数据由属于单一类别的元素组成,它变得高度可预测,因此熵将是最小的。而熵的最小值是 0。

  • 当数据由属于 n 个类别的元素组成且每个类别的概率相等,即每个类别的概率 = 1/n 时,熵将达到最大值。

  • 对于二分类(即,具有两个类别的数据),熵的值不会超过 1。

  • 对于多分类,熵的最大值可以概括为 log(n)。 (这里的 log 以 2 为底。)

基尼指数

基尼指数旨在量化从数据中随机选择一个元素并错误标记的概率。

  • 公式如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 如果数据由属于单一类别的元素组成,则随机选择一个元素被错误标记的概率将为零,因此在这种情况下基尼指数将是最小的。所以,基尼指数的最小可能值也是 0。

  • 当数据由属于 n 个类别且分布均衡的元素组成,即每个类别的概率为 1/n 时,基尼指数将达到最大值。

  • 对于二分类(即数据包含两个不同的类别),基尼指数的最大值永远不会超过 0.5。

  • 对于多分类,基尼指数的最大值可以概括为 1-(1/n)。

基于熵的不确定性预测

原文:towardsdatascience.com/entropy-based-uncertainty-prediction-812cca769d7a

本文探讨了如何将熵作为工具用于图像分割任务中的不确定性估计。我们将介绍熵是什么,以及如何使用 Python 实现它。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 François Porcher

·发表于Towards Data Science ·阅读时间 7 分钟·2023 年 9 月 2 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由Michael Dziedzic提供,来源于Unsplash

在剑桥大学担任神经成像与人工智能研究科学家的过程中,我面临了使用最新深度学习技术对复杂脑部数据集进行图像分割的挑战,尤其是nnU-Net。在这个过程中,我观察到一个显著的漏洞:不确定性估计被忽视。然而,不确定性对可靠的决策制定至关重要

在深入具体内容之前,请随时查看我的Github 仓库,其中包含了本文讨论的所有代码片段。

不确定性在图像分割中的重要性

在计算机视觉和机器学习的领域中,图像分割是一个核心问题。无论是在医疗成像、自动驾驶汽车还是机器人技术中,准确的分割对于有效的决策制定至关重要。然而,一个常被忽视的方面是与这些分割相关的不确定性度量

为什么我们需要关注图像分割中的不确定性?

在许多实际应用中,不正确的分割可能导致严重后果。例如,如果自动驾驶汽车误识别了一个物体,或医疗成像系统错误地标记了肿瘤,后果可能是灾难性的。不确定性估计为我们提供了一个衡量模型对其预测的“确信程度”的指标,从而有助于做出更明智的决策。

我们还可以使用熵作为不确定性的度量来改善神经网络的学习。这个领域被称为**“主动学习”**。这个想法将在后续文章中进一步探讨,但主要思路是识别模型最不确定的区域,以便集中精力在这些区域。例如,我们可能有一个 CNN 用于脑部医学图像分割,但在肿瘤患者上的表现非常差。然后,我们可以集中精力获取更多这类标签。

了解熵

熵是一个从热力学和信息理论中借用的概念,用于量化系统中的不确定性或随机性。在机器学习的背景下,熵可以用来衡量模型预测的不确定性

从数学上讲,对于一个具有概率质量函数 P(x) 的离散随机变量 X,熵 H(X) 被定义为:

或在连续情况下:

熵越高,不确定性越大,反之亦然。

一个经典的例子来充分理解这一概念:

情况 1:一个有偏的硬币

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

JizhidexiaohailangUnsplash 提供的照片

想象一个有偏的硬币,它落在正面上的概率是 p=0.9,落在反面上的概率是 1-p=0.1

它的熵是

情况 2:平衡硬币

现在让我们想象一个平衡硬币,它落在正面和反面上的概率是 p=0.5

它的熵是:

熵更大,这与我们之前说的一致:更多的不确定性 = 更多的熵。

实际上有趣的是,p=0.5 对应于最大熵:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

熵可视化,作者提供的图像

从直观上讲,记住均匀分布是具有最大熵的情况。如果每个结果的概率相同,则对应于最大的不确定性。

在图像分割中实现熵

将其与图像分割联系起来,考虑到在深度学习中,最终的 softmax 层通常为每个像素提供类别概率。可以根据这些 softmax 输出轻松计算每个像素的熵。

但它是如何工作的?

当模型对某个像素属于特定类别有信心时,softmax 层会为该类别显示高概率(1),而对其他类别显示非常小的概率(0)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Softmax 层,确定案例,作者提供的图像

相反,当模型不确定时,softmax 输出在多个类别之间更加均匀分布。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Softmax 层,不确定案例,作者提供的图像

概率分布更加扩散,接近均匀情况,因为模型无法确定哪个类别与该像素相关。

如果你已经坚持到现在,非常好!你应该对熵的工作原理有了很好的直觉。

案例研究:医学影像

让我们用一个实际的例子来说明,使用医学影像,特别是胎儿的 T1 大脑扫描。所有与此案例研究相关的代码和图像都可以在我的 Github 仓库 中找到。

1. 使用 Python 计算熵

如前所述,我们正在处理由我们的神经网络生成的 softmax 输出张量。这种方法是无模型的,它仅使用每个类别的概率。

让我们澄清一下我们正在处理的张量维度中的一个重要问题。

如果你正在处理 2D 图像,那么你的 softmax 层的形状应该是:

这意味着对于每个像素(或体素),我们有一个大小为 Classes 的向量,这个向量给出像素属于我们所有类别中的每一类的概率。

因此,熵应该沿着第一个维度进行计算:

 def compute_entropy_4D(tensor):
    """
    Compute the entropy on a 4D tensor with shape (number_of_classes, 256, 256, 256).

    Parameters:
        tensor (np.ndarray): 4D tensor of shape (number_of_classes, 256, 256, 256)

    Returns:
        np.ndarray: 3D tensor of shape (256, 256, 256) with entropy values for each pixel.
    """

    # First, normalize the tensor along the class axis so that it represents probabilities
    sum_tensor = np.sum(tensor, axis=0, keepdims=True)
    tensor_normalized = tensor / sum_tensor

    # Calculate entropy
    entropy_elements = -tensor_normalized * np.log2(tensor_normalized + 1e-12)  # Added a small value to avoid log(0)
    entropy = np.sum(entropy_elements, axis=0)

    entropy = np.transpose(entropy, (2,1,0))

    total_entropy = np.sum(entropy)

    return entropy, total_entropy

2. 可视化基于熵的不确定性

现在让我们通过在每个图像分割切片上使用热图来可视化不确定性。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

T1 扫描(左),分割(中),熵(右),作者图像

让我们看另一个例子:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

T1 扫描(左),分割(中),熵(右),作者图像

结果非常好!确实可以看到,这与预期一致,因为高熵区域位于形状的轮廓上。这是正常的,因为模型对于每个区域中间的点并不真正怀疑,而是轮廓或边界难以确定。

做出明智的决策

这种不确定性可以以多种不同的方式使用:

  1. 随着医学专家越来越多地将 AI 作为工具,了解模型的不确定性至关重要。这意味着医学专家可以将更多的时间花在需要更精细关注的区域。

2. 在 主动学习半监督学习 的背景下,我们可以利用基于熵的不确定性来关注具有最大不确定性的例子,从而提高学习效率(更多相关内容将在即将发布的文章中讨论)。

主要收获

  • 熵是一个非常强大的概念,用于测量系统的随机性或不确定性。

  • 可以在图像分割中利用熵。这种方法是无模型的,仅使用 softmax 输出张量。

  • 不确定性估计往往被忽视,但它至关重要。优秀的数据科学家知道如何建立好的模型。杰出的数据科学家知道模型在哪些方面失败,并利用这些信息来改进学习。

你喜欢这篇文章并想了解更多吗?查看一下:

[## FrancoisPorcher - 概述

剑桥大学人工智能与神经科学的访问研究员。毕业于加州大学伯克利分校…

github.com

感谢阅读!

如果你想访问 Medium 上的优质文章,你只需每月 $5 的会员资格。如果你注册 通过我的链接,你将以额外费用支持我。

参考文献

  1. Bai, W., Oktay, O., Sinclair, M., Suzuki, H., Rajchl, M., Tarroni, G., Glocker, B., King, A., Matthews, P. M., & Rueckert, D. (2017). 基于网络的心脏 MR 图像分割的半监督学习。第 20 届医学图像计算与计算机辅助干预国际会议(MICCAI 2017)论文集(第 253–260 页)。Springer Verlag。 doi.org/10.1007/978-3-319-66185-8_29

  2. Ta, K., Ahn, S. S., Stendahl, J. C., Sinusas, A. J., & Duncan, J. S. (2020). 一种半监督联合网络,用于 4D 超声心动图中的左心室运动跟踪和分割。医学图像计算与计算机辅助干预:MICCAI 国际会议,12266(十月),468–477。 doi.org/10.1007/978-3-030-59725-2_45

  3. Yu, L., Wang, S., Li, X., Fu, C-W., & Heng, P-A. (2019). 不确定性感知自集成模型用于半监督的 3D 左心房分割。arXivarxiv.org/abs/1907.07034

熵正则化强化学习解释

原文:towardsdatascience.com/entropy-regularized-reinforcement-learning-explained-2ba959c92aad

通过为算法添加熵奖励来学习更可靠、稳健和可迁移的策略

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Wouter van Heeswijk, PhD

·发布于 Towards Data Science ·8 min read·2023 年 10 月 26 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 Jeremy Thomas 提供,来源于 Unsplash

是与无序、随机或不确定状态相关的概念。它可以被视为随机变量的信息度量。传统上,它与热力学等领域相关,但这一术语也被引入了许多其他领域。

1948 年,克劳德·香农在信息论中引入了熵的概念。在这种背景下,如果一个事件发生的概率较低,它被认为提供了更多的信息;事件的信息与其发生的概率成反比。直观地说:我们从稀有事件中学到的更多。

熵的概念可以被形式化为以下内容:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对一组事件 x 的熵定义。每个事件的信息与其发生的概率成反比。

在强化学习(RL)中,熵的概念也被应用,目的是鼓励探索。在这种背景下,熵是由随机策略返回的动作的可预测性度量

具体来说,RL 将策略的熵(即,动作的概率分布)作为奖励的一个组成部分来加以奖励。这篇文章讨论了基本情况,但熵奖励是许多最先进的 RL 算法的一个重要组成部分。

什么是熵?

首先,让我们对熵的概念建立一些直觉。下图显示了低熵和高熵策略。低熵策略几乎是确定的;我们几乎总是选择相同的行动。在高熵策略中,我们选择的行动具有更多的随机性。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

低熵策略(左)和高熵策略(右)的示例。在高熵策略中,行动选择中有更多的随机性。[作者提供的图片]

接下来,我们考虑硬币翻转的熵。

香农的熵利用以 2 为底的对数函数(在 Numpy 中为 np.log2),相应的测量单位称为比特。其他形式的熵使用不同的底数。这些区别对于掌握主要思想并不是特别重要。

如果硬币是加重的会发生什么?下图显示了熵将会减少,因为更有可能确定某一结果是否会发生

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

硬币翻转的熵,随着头和尾的概率变化,以比特为单位测量。当硬币翻转的结果最不确定时,熵达到峰值。[图片来自维基百科]

现在让我们计算一个公平骰子的熵:

注意,骰子的熵(2.58 bit)高于公平硬币(1 bit)。虽然两者都有均匀的结果概率,但骰子的个别概率较低。

现在,考虑一个加重骰子的概率,例如,[3/12, 1/12, 2/12, 2/12, 2/12, 2/12]。相应的熵为 2.52,这反映了结果现在略微更可预测(我们更可能看到 1 点而不太可能看到 2 点)。最后,考虑一个更重的骰子,概率为[7/12, 1/12, 1/12, 1/12, 1/12, 1/12]。此时,我们得到的熵为 1.95。结果的可预测性进一步提高,从熵的减少可以看出。

了解熵的概念后,让我们看看如何在强化学习中利用它。

熵正则化强化学习

我们在强化学习的背景下定义熵,其中行动概率源自随机策略π(⋅|s)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在强化学习中的熵奖励,通过对所有行动概率及其对数的乘积求和来计算

我们将策略的熵用作熵奖励,将其添加到我们的奖励函数中。请注意,我们对每一个时间步骤都这样做,这意味着当前的行动也为最大化未来的熵做好了准备:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

熵正则化强化学习。熵奖励——由α加权——被添加到我们寻求最大化的奖励中

这乍看起来可能有些违反直觉。毕竟,强化学习旨在优化决策过程,这涉及到常规地选择好的决策而非坏的决策。为什么我们要以鼓励最大化熵的方式来改变我们的奖励信号呢?这是引入最大熵原则的一个好时机:

“***[策略]最能代表关于系统[环境]当前知识状态的概率分布,是在精确陈述的先验数据[观察奖励]***的背景下具有最大熵的分布” — 维基百科

如果策略的熵很大(例如,在初始化后的短时间内),我们对不同动作的影响了解得不多。高熵策略反映了我们尚未充分探索环境,还需要从各种动作中观察更多的奖励。

当然,最终我们确实希望采取好的行动,而不是无休止地探索,这意味着我们必须决定对熵奖励的重视程度。这是通过熵正则化系数α来实现的,这是一个可调参数。为了实际应用,权重熵奖励*αH(π(⋅|s)*可以简单地视为一种鼓励探索的奖励组件。

请注意,熵奖励总是计算在整个动作空间上,因此在评估每个动作时,我们都会添加相同的奖励。在典型的随机策略方法中,动作概率与它们的期望奖励成正比,包括熵奖励(例如,通过对它们应用 softmax 函数)。因此,如果熵相对于奖励非常大,则动作概率或多或少相等。如果熵非常小,则奖励在定义动作概率上占主导地位。

Python 实现

现在开始实现熵正则化的强化学习。为了本文的目的,我们在多臂老虎机背景下使用基本的离散策略梯度算法,但它可以很容易地扩展到更复杂的环境和算法。

记住,策略梯度算法具有内置的探索机制——对固有确定性策略(例如,Q-learning)应用的熵奖励具有更明显的效果。

引入熵正则化是相当直接的。我们只需将熵奖励添加到奖励中——因此它会被纳入损失函数——然后按常规步骤进行。根据算法和问题设置,您可能会在文献和代码库中遇到许多变体,但核心思想保持不变。下面的代码片段(离散策略梯度的 TensorFlow 实现)演示了它是如何工作的。

部分熵正则化强化学习的代码,在这种情况下扩展了离散策略梯度算法。

考虑一组老虎机,其平均奖励为μ=[1.00,0.80,0.90,0.98],所有机器的标准差σ=0.01。显然,最佳策略是以概率 1.0 玩机器#1。然而,如果没有足够的探索,很容易将机器#4 误认为是最佳机器。

为了说明这个想法,让我们首先看看没有熵奖励(α=0)的算法行为。我们绘制了玩机器#1 的概率。尽管每台机器开始时的概率相等,但算法相当快地识别出机器#1 提供了最高的期望奖励,并开始以更高的概率玩它。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这种情况下,没有熵正则化的算法收敛于主要玩最佳的机器#1。左图:每次试验玩机器#1 的概率。右图:10k 次试验后每台机器的概率[作者图片]

然而,这可能会有很大的不同……下图显示了使用相同算法的一个运行,只是这次它错误地收敛到次优的机器#4。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这种情况下,没有熵正则化的算法收敛于主要玩次优的机器#4。左图:每次试验玩机器#1 的概率。右图:10k 次试验后每台机器的概率[作者图片]

现在,我们设置α=1。这产生了相对于奖励而言较大的熵奖励。尽管玩机器#1 的概率逐渐增加,但正则化组件继续鼓励强烈的探索,即使在 10k 次迭代后。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

通过熵正则化,我们看到即使在 10k 次试验后,算法仍然进行了大量探索,虽然逐渐认识到机器#1 提供了更好的奖励。左图:每次试验玩机器#1 的概率。右图:10k 次试验后每台机器的概率[作者图片]

显然,在实际应用中,我们不知道真正的最佳解决方案,也不知道所需的探索量。通常,你会遇到α=0.001附近的值,但你可以想象,探索与利用之间的理想平衡强烈依赖于问题。因此,通常需要一些试验和错误以找到合适的熵奖励。系数α也可能是动态的,可能通过预定的衰减方案或者作为一个学习权重本身。

强化学习中的应用

熵正则化的原则可以应用于几乎任何 RL 算法。例如,你可以将熵奖励添加到 Q 值中,并通过 softmax 层将结果转换为动作概率(soft Q-learning)。最先进的算法,如近端策略优化(PPO)或软演员评论家(SAC),通常包括熵奖励,实证研究表明这通常能提升性能。具体来说,它提供了以下三种好处:

I. 更好的解决方案质量

正如前面详细说明的,熵奖金鼓励探索。特别是在处理稀疏奖励时,这非常有用,因为我们很少收到关于动作的反馈,并且可能错误地重复那些过高估计奖励的次优动作。

II. 更好的鲁棒性

由于熵奖金鼓励更多的探索,我们也会更频繁地遇到稀有或偏离的状态-动作对。因为我们遇到了更丰富和更多样化的经验,我们学习到的策略能够更好地处理各种情况。这种增加的鲁棒性提升了策略的质量。

III. 促进迁移学习

增加探索也有助于将学习到的策略适应于新任务和环境。更多的多样化经验能够更好地适应新情况,因为我们已经从类似的情境中学到了东西。因此,熵正则化在迁移学习中通常很有用,它使得在应对变化环境时重新训练或更新学习策略变得容易。

TL;DR

  • 熵奖金鼓励对动作空间的探索,旨在避免过早收敛

  • 奖励(利用)和奖金(探索)之间的平衡是通过一个需要微调的系数来控制的。

  • 熵奖金在现代 RL 算法中如 PPO 和 SAC 中被广泛使用。

  • 探索提高了质量、鲁棒性和对新实例变体的适应能力。

  • 熵正则化在处理稀疏奖励时特别有用,当鲁棒性很重要和/或策略应该适用于相关问题设置时。

如果你对 RL 中的熵正则化感兴趣,你可能还想查看以下文章:

## TensorFlow 2.0 中离散策略梯度的最小工作示例

用于训练离散演员网络的多臂老虎机示例。借助 GradientTape 功能,…

towardsdatascience.com ## 强化学习中的策略梯度解释

了解基于似然比的策略梯度算法(REINFORCE):直觉、推导、…

towardsdatascience.com [## 近端策略优化(PPO)解释

从 REINFORCE 到连续控制中的首选算法的历程

towardsdatascience.com

进一步阅读

Ahmed, Z., Le Roux, N., Norouzi, M., & Schuurmans, D. (2019). 理解熵对策略优化的影响 国际机器学习会议。

Eysenbach, B. & Levine, S. (2022). 最大熵强化学习(可证明地)解决一些鲁棒强化学习问题 国际学习表征会议。

Haarnoja, T., Tang, H., Abbeel, P., & Levine, S. (2017). 使用深度能量基策略的强化学习 国际机器学习会议。

Reddy, A. (2021). 最大熵如何使强化学习更鲁棒 伯克利机器学习。

Schulman, J., Chen, X., & Abbeel, P. (2017). 策略梯度与软 Q 学习的等价性 arXiv 预印本 arXiv:1704.06440

Tang, H. & Haarnoja, T. (2017). 通过最大熵深度强化学习学习多样化技能 伯克利人工智能研究所。

Yu, H., Zhang, H., & Xu, W. (2022). 你实际需要熵奖励吗? arXiv 预印本 arXiv:2201.12434

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值