TowardsDataScience-2023-博客中文翻译-三-

TowardsDataScience 2023 博客中文翻译(三)

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

精度和召回率的商业视角

原文:towardsdatascience.com/a-business-lens-on-precision-and-recall-1ce2f5b77eed

社交媒体垃圾信息作为案例研究

Matt SosnaTowards Data Science Matt Sosna

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

--

照片由 Nong 提供,来源于 Unsplash

免责声明:本帖中的示例仅用于说明目的,并不对任何特定公司或其内容政策发表评论。本文所表达的观点仅代表我个人,不代表我的雇主。

为什么社交媒体上会有垃圾信息?除了垃圾信息发送者自己,没有人喜欢点击诱饵骗局或网络钓鱼尝试。我们有几十年的训练数据来喂养机器学习分类器。那么为什么每个主要技术*台上的垃圾信息似乎都是不可避免的?经过这么多年,为什么机器人农场依然存在?

作者提供的图片

简而言之,答案是真正在大规模上打击垃圾信息是非常困难的,而且在不伤害真实用户和广告商的情况下,这种难度是指数级增长的。在这篇文章中,我们将使用精度召回率作为理解垃圾信息问题的框架。我们将看到,彻底消灭 100%的垃圾信息是不切实际的,并且存在基于金融、法规和用户情绪的某种“*衡”垃圾信息发生率。

照片由 Joseph Barrientos 提供,来源于 Unsplash

我们的应用

假设我们要推出一个与 TikTok 和 Instagram 竞争的应用。(暂且不论它们分别拥有11 亿20 亿的月活跃用户;我们充满雄心!)在这个竞争激烈的市场中,我们的关键差异点是我们保证用户仅能看到最高质量的视频:绝对没有“快速致富”的骗局,公然的现有内容转发,感染计算机的恶意网址等等。

尝试 1:人工审核

为了实现这一质量保证,我们雇佣了令人震惊的 1000 名审核员,审查每个上传的视频才允许其上*台。我们认为有些事情确实需要人工处理:视频垃圾信息过于复杂和依赖上下文,无法依靠自动逻辑。例如,鼓励用户点击 URL 的视频可能是恶意钓鱼尝试,也可能是针对阿尔茨海默病研究的无害筹款活动——风险太大,无法自动做出这样的决定。

作者提供的图片

应用程序上线了。令我们高兴的是,我们的“诚信优先”信息得到了用户的共鸣,他们蜂拥而至。我们很快达到了每天上传 5 万小时视频的数百万用户。

换句话说,每位审核员现在每天要审查50 小时的视频。他们尝试以 6 倍速观看所有视频,但难免出错:用户开始抱怨他们的无害视频被屏蔽 垃圾信息却被上传到了*台。 我们迅速雇佣更多审核员,但随着应用的增长和上传量的急剧增加,我们意识到在我们能雇佣足够的审核员之前,公司可能会破产。[1] 我们需要一种不同的策略。

尝试 2:机器学习

我们无法替代人类的直觉,但也许我们可以通过机器学习接*这一目标。鉴于过去十年计算机视觉自然语言处理的巨大进展,我们可以从视频中提取特征:与现有视频的像素相似性,音频中的关键词,视频是否似乎是由 AI 生成的等等。然后我们可以查看这些特征与视频是否为垃圾信息之间的关系。

作者提供的图片

确定特征与垃圾标签之间的关系最好交给算法。[2] 特征空间对于人类来说实在太大:特征的相互作用是非线性的,依赖关系复杂,在某些上下文中有用而在其他情况下无用,等等。所以我们使用机器学习来训练一个预测视频是否为垃圾信息的分类器。 我们的模型接收一个视频并输出该视频是垃圾信息的概率。[3]

作者提供的图像

当我们首次运行分类器在我们知道是垃圾邮件和正常的视频上时,我们希望看到如下情况:两个分布通过其垃圾邮件概率整齐地分开。在这种理想状态下,存在一个垃圾邮件概率阈值,低于此阈值的所有视频都是正常邮件,高于此阈值的所有视频都是垃圾邮件,我们可以利用这个阈值来完美地分类新视频。

作者提供的图像

但我们实际看到的是概率分布重叠。是的,大多数正常视频的概率较低,大多数垃圾邮件视频的概率较高。但存在一个不舒服的“中间”垃圾邮件概率,在这个概率下很难判断视频是垃圾邮件还是正常邮件。

作者提供的图像

如果我们放大分布重叠的地方,情况可能如下。我们无法画出一条完美分隔正常视频和垃圾邮件的视频线。如果我们将阈值设置得太高,垃圾邮件就会进入*台。如果我们将阈值设置得太低,正常视频会被错误地阻止。

作者提供的图像

那么我们如何选择一个“最不差”的阈值? 要回答这个问题,我们需要理解精确度召回率,这两个指标为任何分类系统的权衡提供了框架。然后我们将以新的理解重新审视我们的应用,并查看是否有办法来最佳地分类垃圾邮件。

照片由Robert Wiedemann拍摄,发布在Unsplash

评估框架

模型创建

让我们快速了解一下如何创建和评估机器学习分类器,以我们的垃圾邮件分类器为例。训练模型的第一步是将数据分成训练集和测试集。然后,算法解析训练数据以学习特征与标签(垃圾邮件或正常)的关系。结果是一个能够接收视频特征并返回其为垃圾邮件的概率的模型。

作者提供的图像

概率很好,但我们需要一种方法将如 0.17 或 0.55 这样的数字转换为是否为垃圾邮件的决策。因此,我们将输出的概率二值化——默认值为 0.5——成垃圾邮件正常邮件分类。对于模型中的任意特征,模型的概率曲线(黑线)和分类(黄色和绿色区域)可能看起来像这样。

作者提供的图像

我们的模型是对所给数据的最佳理解。然而,虽然理解 我们已有 的数据是有用的,但真正的目标是能够预测 我们未见过 的数据的标签,比如即将上传的视频。[4] 我们通过 测试 数据来衡量模型的能力:这些特征-标签对没有用于训练模型。我们输入测试数据的特征,查看模型的预测,并将这些预测与实际标签进行比较。(这就是为什么我们不对所有可用数据进行训练:我们需要一些保留的标签来审计模型的预测。)

图片由作者提供

基于预测的四种可能结果,有四个组成部分来衡量我们模型分类新数据的能力:

  • 真正例: 模型正确识别垃圾邮件。

  • 假阳性: 模型预测为垃圾邮件,但视频是良性。

  • 假阴性: 模型预测为良性,但视频是垃圾邮件。

  • 真负例: 模型正确识别一个良性视频。

我们可以将这些结果排列在 混淆矩阵 中。矩阵的列是 预测 的垃圾邮件和良性标签,行是 实际 的垃圾邮件和良性标签。

图片由作者提供

我们之前提到过,我们模型的垃圾邮件概率在 0.5 处被二值化为垃圾邮件良性分类。但 0.5 并不总是最佳阈值,尤其是在数据不*衡的情况下。我们可以将阈值设置为 0 和 1 之间的任何值,以更好地划分分类。

图片由作者提供

这些阈值将生成不同的混淆矩阵,反映每个模型对新数据的准确泛化能力的不同。 那么我们如何选择一个阈值呢?为了解答这个问题,我们需要回顾一些指标。

指标 1:准确性

我们寻找最佳模型的第一个策略可能是最大化 准确性: 我们模型检测真正例 (TP) 和真负例 (TN) 的能力。换句话说,准确性是 我们模型正确预测的标签的比例。一个具有完美准确性的模型将没有假阳性 (FP) 或假阴性 (FN)。

准确性是一个直观的指标,但它可能掩盖我们模型中的一些盲点。例如,如果 一种标签远比另一种标签频繁,我们的模型可能会难以预测不太频繁的标签,甚至可能收敛到一个无意义的规则上!例如,如果我们的训练数据只是上传视频的随机样本,我们可能会得到 99.9% 的良性视频和 0.1% 的垃圾邮件。一个总是预测视频为良性的模型将有 99.9% 的准确率。 这完全不合格——我们会错过所有我们想要捕捉的视频!

即使数据*衡,我们在评估模型时也绝不能仅仅依赖准确率。让我们来看一些其他指标,以获得更全面的视角。

指标 2:召回率

为了衡量我们模型在测试集中对正样本的分类效果,我们需要查看模型的召回率。召回率是模型正确预测的正标签的比例。换句话说:

我们可以将这视为混淆矩阵的顶行。召回率是指真正例的数量与正标签总数的比值:模型捕捉到的标签(真正例)和遗漏的标签(假阴性)。一个具有 100%召回率的模型是一个在测试集中正确分类所有正标签的模型。

作者提供的图像

那个 99.9%准确的“总是预测良性”模型将会有召回率,这是一个明显的红旗,表明该模型应该立即被丢弃。理想情况下,我们的模型应该足够敏感,能捕捉测试集中所有的垃圾邮件标签。但为了确保这种敏感性不会以错误标记良性视频为代价,我们需要查看一个额外的指标:精度。

指标 3:精度

当我们的模型将一个视频分类为垃圾邮件时,它实际是垃圾邮件的频率如何?这就是精度的核心思想,或者说预测的正标签中真实正例的比例。作为一个方程,精度呈现以下形式:

我们可以将这视为混淆矩阵的左列。精度是指真正例的数量与预测总数的比值:包括正确的(真正例)和不正确的(假正例)。

作者提供的图像

精度是理解当我们的模型预测视频为垃圾邮件时我们应该有多大信心的关键指标。当一个高精度模型预测视频是垃圾邮件时,该视频很可能是垃圾邮件;如果模型精度低,除非我们自己查看,否则无法知道它是否真的垃圾邮件。

因此,可能会有诱惑去优化精度,最大化对模型预测的信心。但我们越是优先考虑精度,我们的模型在将视频标记为垃圾邮件时就会越保守,这意味着我们不可避免地会错过一些应该被捕捉的垃圾邮件视频。

为了说明这一点,我们再次查看良性和垃圾邮件分布重叠的图示。我们可以在两个阈值下对垃圾邮件概率进行二值化:A 或 B。

作者提供的图像

阈值 B 右侧的每个视频都是垃圾视频,因此在该垃圾概率下进行二分类的分类器将具有 100%的精确率。这很令人印象深刻,但该阈值会错过左侧的两个垃圾视频。这些视频将被错误地分类为良性(假阴性),因为我们的模型对它们是否为垃圾视频不够自信。与此同时,阈值 A 的模型会捕捉到这些垃圾视频,但也会错误地标记右侧的三个良性视频(假阳性),导致精确率降低。

寻找*衡

这种权衡涉及到精确率和召回率之间的固有矛盾:当我们提高分类阈值时,我们会提高精确率降低召回率。我们可以重新绘制我们的图形以突出这种权衡。

图片由作者提供

另一种可视化这种情况的方法是将精确率和召回率绘制为分类阈值的函数。使用我在一些样本数据上训练的分类器(代码在帖子末尾),我们可以看到随着阈值的提高,精确率增加,而召回率稳定下降。阈值越高,我们模型的预测越准确,但也会错过更多的垃圾视频。

图片由作者提供

那么我们如何找到*衡点呢?归根结底,我们必须问自己,是更倾向于接受假阳性还是假阴性,并且程度如何。如果一些用户被错误地阻止上传视频,还是他们在*台上遇到诈骗更糟糕?阻止 100 个良性视频以防止 1 个垃圾视频是否值得?阻止 1000 个良性视频呢?

我们应用的主要卖点是用户永远不会看到垃圾视频。为了在我们的分类器中实现 100%的召回率,我们不得不接受大约 45%的精确率——这是一种令人尴尬的不精确模型,将导致每天阻止数千个良性视频。如果我们能接受只捕捉到 90%的垃圾视频,我们可能能达到 60%的精确率,但我们仍然会阻止过多的视频并允许垃圾信息通过。

比较模型

我们回到设计阶段,深入挖掘数据以寻找与垃圾视频相关的更好特征。我们发现了一些有前景的趋势,并重新训练了我们的分类器。当我们可视化两个模型的精确率和召回率与分类阈值的关系时,我们看到如下图所示的情况;实线是旧模型,虚线是新模型。

图片由作者提供

这看起来好多了!对于大多数阈值,新模型在精确率和召回率上都取得了巨大的改进,使我们的权衡讨论更加容易接受。现在在保持 100%召回率的同时,我们可以获得的最高精确率大约是 60%。在 90%的召回率下,我们有 85%的精确率。

我们可以用AUC-ROC,即ROC 曲线下面积,来总结我们在所有阈值上的模型拟合改进。ROC 曲线是一个绘制真正例率(召回率)与假正例率(良性视频被标记为垃圾邮件的频率)之间关系的图表。如果我们的模型能在所有阈值下完美区分良性和垃圾邮件视频,则曲线下面积为 1;如果模型的效果与随机猜测无异(下方的灰色虚线),则为 0.5;否则则介于两者之间。这个数值提供了一种快速展示我们新模型(AUC = 0.96)相对于旧模型(AUC = 0.84)整体改进的方式。

作者提供的图片

因此,无论用什么指标来衡量,我们都可以庆祝我们通过新的分类器提高了对抗垃圾邮件的能力。但我们仍需面对一个不太舒适的问题:我们的应用中绝对零垃圾邮件的承诺。为了实现 100%的召回率,我们是否真的需要在对上传到应用的视频进行分类时接受 60%的精确度,这仅比掷硬币稍好?我们是否需要回到模型开发阶段,或者还有其他方法可以尝试?

照片由 Rohit Tandon 拍摄,来源于 Unsplash

回到我们的应用

尝试 3:机器学习 + 人工审核

如果我们继续仅仅依赖机器学习的思维方式,我们将花费大量精力去追求递减的收益。是的,我们可以找到更好的特征、算法和超参数来提高模型的拟合度。但如果我们退一步来看,我们会发现我们的分类器实际上只是识别垃圾邮件的一个漏斗的部分,投资于整个漏斗会比仅仅关注机器学习部分更为成功。

如果我们重新审视之前的垃圾邮件分布图,我们可以定义三个垃圾邮件概率区域:确定的良性、确定的垃圾邮件和“不确定”。我们刚刚讨论了如何利用精确度和召回率来量化将这个不确定区域二元化为良性和垃圾邮件的权衡。但如果我们将分类器与之前的人类审核员结合起来,这一切就不再是黑白分明的了。

作者提供的图片

如果我们不再为寻找完美的分类阈值而苦恼,而是设置上述紫色区域边界的阈值会怎样?如果我们的分类器确信一个视频是垃圾邮件或良性邮件,我们就让它为我们自动决定。但如果分类器不确定,我们就把视频交给人工来做最终决定。

作者提供的图片

我们现在面临两个精度-召回权衡——不确定性区域的下限和上限——但误报的风险要低得多:我们现在面临的风险是浪费人工审查能力,而不是直接阻止无害视频。(尽管效率低下,我们的审查员不会抱怨审查一些无害视频,而如果我们错误地阻止了足够多的视频,用户会退出我们的应用程序。)

假阴性(漏过一个垃圾邮件视频)仍然代价高昂,考虑到我们应用程序对质量的承诺。所以只要我们有人工审查能力,我们可以尽可能扩大我们的不确定性窗口,以确保捕获尽可能多的垃圾邮件视频。但这足够确保没有垃圾邮件视频进入*台吗?

机会成本和对抗性行为者

不幸的是,答案是否定的。即使结合了机器学习和人工审查的最佳方案,我们也不能阻止 100%垃圾邮件进入*台。

第一个原因,我们之前讨论过,是财务问题。人工审查成本高昂,我们无法雇佣足够的人来处理所有中级垃圾邮件概率的视频,而不至于使公司破产。超出一定投资水*,我们不仅能减少一些垃圾邮件,还会削减其他公司项目的资金,比如新功能、市场扩展或客户支持。还有许多其他安全工作需要资金,比如防止恶意行为者入侵或冒充用户。在某些情况下,其他工作的机会成本如此之高,以至于如果我们在*台上接受一定程度的垃圾邮件,我们实际上可能会有一个更好的应用程序。

作者提供的图像

我们不能阻止 100%垃圾邮件的第二个原因是我们的分类器很快就会过时,因为垃圾邮件特征空间不断变化。垃圾邮件发送者是对抗性的,这意味着他们会改变战术,一旦公司确定了阻止垃圾邮件的规则。这些恶意行为者是无情的;诈骗他人是他们养家糊口的方法,他们有无尽的动力找到绕过我们防御的方法。

残酷的是,预测能力最强的特征通常会变得无关紧要,因为垃圾邮件发送者会调查他们的内容为何被阻止,然后改变他们的方法。结果是*台与垃圾邮件发送者之间的红皇后竞赛。如果我们不不断投资于创新和迭代如何打击垃圾邮件,垃圾邮件发送者会迅速绕过我们的防御,淹没我们的*台。但即使拥有一支优秀的工程师和调查团队,仍然会有一些垃圾邮件在我们更新系统之前进入*台。

垃圾邮件*衡

所以,如果我们无法在*台上实现 0%的垃圾邮件,我们会在哪里结束?这个问题的答案取决于许多竞争因素。

作者提供的图片

首先,也是可能最强的力量是监管。如果通过法律,对我们应用程序上用户受骗的情况处以严重的经济罚款,那么投资*衡会大幅向最小化垃圾邮件倾斜。但除非法律真正有约束力,否则这一力量会被未投入其他公司计划的机会成本所抵消(这些计划可能本身也受到自身法规的法律压力)。

第二组力量来自用户。当垃圾邮件明显时,用户会愤怒;关于家庭成员失去积蓄的病毒式帖子出现,用户呼吁国会制止我们的公司,用户离开或抵制我们的应用程序。因此,我们花更多精力对抗垃圾邮件,垃圾邮件的普遍性降低,用户停止抱怨。但是在没有用户这种压力的情况下,当没有人注意到结果时,很难证明增加投资的必要性。

最后,这是我们公司对垃圾邮件的内部立场。如果在对抗垃圾邮件的投资超过经济最优水*的情况下,我们愿意损失多少公司最大可能的收入?垃圾邮件对我们的业务造成伤害,但过度投资于对抗垃圾邮件也会造成伤害。基于我们对垃圾邮件的道德立场,我们愿意在收益递减的情况下走多远?

最终,这些力量的总和会导致我们应用程序上出现一些非零(希望不是 100%!)的垃圾邮件。我们意识到,启动我们应用程序的初衷在我们所生活的世界里显得有些天真。但是,我们决定这不是放弃的理由,因此我们建立了一个强大的反垃圾邮件团队,为他们提供所需的资源,并开始了对用户安全的无休止的斗争。

图片由 Tim Mossholder 提供,发布在 Unsplash

结论

本文使用了“为什么社交媒体上会有垃圾邮件?”这个问题来探讨分类系统中的精准度、召回率和投资权衡。我们从讨论对抗垃圾邮件的不同方法(人工审核和机器学习)开始,然后讨论了在设置分类阈值时我们获得和失去的内容。接着,我们讨论了在社交媒体上对抗垃圾邮件的众多挑战。

再次声明,本文仅代表我的个人观点,并不评论任何公司的内容政策。感谢阅读!

最佳,

马特

代码

以下是生成本文中提到的数据和分类器的代码。改进的分类器是通过减少特征生成过程中随机噪声的标准差生成的。

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import precision_recall_curve
from sklearn.model_selection import train_test_split

# Generate labels
is_spam = np.concatenate(
    [
        np.random.choice([0, 1], p=[0.9, 0.1], size=200),
        np.random.choice([0, 1], p=[0.8, 0.2], size=200),
        np.random.choice([0, 1], p=[0.6, 0.4], size=200),
        np.random.choice([0, 1], p=[0.4, 0.6], size=200),
        np.random.choice([0, 1], p=[0.1, 0.9], size=200),
    ]
)

# Generate features
feature_1 = [np.random.normal(0, 1.5, 1) if x == 0 else np.random.normal(3, 1.5, 1) for x in is_spam]
feature_2 = [np.random.normal(0, 2.5, 1) if x == 0 else np.random.normal(3, 2.5, 1) for x in is_spam]

df = pd.DataFrame(
    {
        'is_spam': is_spam,
        'feature_1': feature_1,
        'feature_2': feature_2,
    }
)

############################################################
# Train classifier
X = df[['feature_1', 'feature_2']]
y = df['is_spam']

X_train, X_test, y_train, y_test = train_test_split(X, y)

mod = LogisticRegression()
mod.fit(X_train, y_train)

############################################################
# Generate predictions
preds = mod.predict_proba(X_test)[:,1]
precision, recall, thresholds = precision_recall_curve(y_test, preds)

df_pred = pd.DataFrame(
    {
        'precision': precision[:-1],
        'recall': recall[:-1],
        'threshold': thresholds,
    }
)

脚注

1. 尝试 1:人工审核

为了说明 100%人工审核的不切实际,可以考虑 YouTube。用户每分钟上传超过 500 小时的视频,即每天 180 万小时。手动审核这么多视频需要75,000 名审核员全天候观看视频,如果审核员每班只工作 8 小时并有午休,则需要240,000 名审核员。再加上 1,000 名审核员处理重新审核那些创作者声称被错误删除的视频。我们剩下的就是241,000 名审核员,或者是Google 员工的 160%。这行不通。

2. 尝试 2:机器学习

请注意,机器学习并不是对抗垃圾邮件的唯一选择。确实存在针对垃圾邮件子集的手工确定性规则的有力用例。例如,“用户应该允许多久发布一次视频?”这种问题可能不需要专门的分类器,可以从用户每天发布次数的分布中推断出来。

3. 尝试 2:机器学习

在这篇文章中,我使用单数形式的“模型”或“分类器”来指代我们用于捕捉垃圾邮件的系统。但垃圾邮件是一个广泛且多方面的领域,因此我们可能实际上需要一个集成模型,每个模型都在不同的垃圾邮件子集上进行训练。这种集成方法是Facebook 新闻推送的工作原理。Feed 中的每个项目都由多个模型排名,每个模型输出用户可能喜欢该项目、评论该项目或开始关注发布该项目的页面等的可能性。

4. 评估框架

仅仅建模现有数据也是有用的,但这更多属于商业智能或数据分析的领域。目标是理解我们的数据,但不一定有预测未来数据的组件。

一种优雅的方式来有效解决旅行推销员问题

原文:towardsdatascience.com/a-classy-approach-to-solving-traveling-salesman-problems-effectively-dbb44e7d30b9?source=collection_archive---------5-----------------------#2023-08-28

以类似 scikit-learn 的方式实现 TSP 模型,简化路线优化模型的构建和求解

Carlos J. UribeTowards Data Science Carlos J. Uribe

·

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

--

图片由 DALL·E 3 根据作者的提示生成:“一系列优化路线穿越地球,背景中有 Python 代码”

👁️ 这是系列文章的第 #5 篇 涵盖了项目“Python 中的智能旅游决策支持系统。我鼓励你查看它,以获得整个项目的一般概述。如果你只是对解决 TSP 感兴趣,这篇文章也适合你,因为我展示了一种方法,使解决任何 TSP 变得非常简单。这篇文章确实建立在之前的文章基础上,但阅读它们是可选的;如果你想了解“如何”,可以阅读;如果你想尽快得到成果,则可以跳过。

目录

1. 之前冲刺回顾

2. 读取待访问地点的数据

3. 基本架构

  • 3.1. 优化器类图

  • 3.2. **地理分析器**, 回顾

  • 3.3. 打下基础:优化工具的基类

4. 一个类来解决所有问题: 旅行推销员优化器

  • 4.1. **旅行推销员优化器** 设计

  • 4.2. **旅行推销员优化器** 实现

  • 4.3. **旅行推销员优化器** 为初学者

5. 超越最优解:使用优化器提取见解

6. 结论(或下一次冲刺的计划)

1. 之前冲刺回顾

冲刺 1 中,我们通过一个普遍的旅游规划问题进行了推理,并得出其最小可行问题旅行推销员问题 (TSP)。因此,我们将 冲刺 2冲刺 3 专门用于开发 TSP 的数学模型和计算机模型。然而,当时的目标只是展示使用优化建模解决此类问题的可行性。一旦此概念验证被认为是可行的,新的目标是升级它,将其精炼为一个最小可行产品 (MVP),以系统地和更通用地解决这种类型的问题。我们注意到,作为前提条件,我们需要一种从任意位置获取距离数据的方法。我们在 冲刺 4 中解决了这个问题,在那里我们构建了 GeoAnalyzer 类来计算任何位置集合的距离矩阵,只需它们的坐标即可。

所有这些迭代进展将我们带到了第五阶段,我们最终达成了一个重大里程碑:创建一个像估算器一样的类,可以快速直观地解决通用 TSP。我们将通过以可扩展的方式集成到目前为止所构建的内容来实现这一目标,从而为未来的增强铺*道路,这些增强将使我们的模型的能力远超 TSP。如果我们要解决现实世界的旅游问题,这是必须做的。也就是说,旅行推销员问题——可以说是所有运筹学中最著名的运输问题——独具一格,因此在本文中,我们将为其提供一个独特的类。这个类将隐藏所有低级建模和解决方案解析的细节,从而使通过方法调用即可无缝解决任何 TSP 问题。如果你觉得这篇文章缺乏足够的背景或跳跃较大,请阅读准备工作,了解如何在 Pyomo 中实现 TSP 模型

## 使用 Python 实现、解决和可视化旅行推销员问题

学习如何将优化模型从数学转换为 Python,优化它,并可视化解决方案以快速获得结果…

towardsdatascience.com

以及如何以自动方式查找地点之间的距离

## 从坐标计算一组站点的距离矩阵

估算任意一对站点之间的距离,作为解决问题的一个步骤…

## 从坐标计算一组站点的距离矩阵

你将更深入地理解我们将在这里讨论的想法和代码的来源。如果你已经熟悉 TSP,可以直接跳入!

2. 读取要访问的站点数据

通用 TSP 的基本输入是我们要访问的地点。在我们的示例中,我们有一个巴黎的兴趣点列表,包括我们的酒店。让我们将它们读入数据框:

import pandas as pd

def read_data_sites_to_visit() -> pd.DataFrame:
    """ Reads in a dataframe the locations of the sites to visit """
    DATA_FOLDER = ("https://raw.githubusercontent.com/carlosjuribe/"
                   "traveling-tourist-problem/main/data")
    FILE_LOCATION_HOTEL = "location_hotel.csv"
    FILE_LOCATION_SITES = "sites_coordinates.csv"

    df_sites = pd.concat([
        # coordinates of our hotel, the starting location
        pd.read_csv(f"{DATA_FOLDER}/{FILE_LOCATION_HOTEL}", index_col='site'),
        # coordinates of the actual places we want to visit
        pd.read_csv(f"{DATA_FOLDER}/{FILE_LOCATION_SITES}", index_col='site'), 
    ])
    return df_sites

df_sites = read_data_sites_to_visit()

df_sites

图 1. 优化器的输入坐标(作者提供的图片)

3. 基本架构

在我们开始编码之前,重要的是对我们即将做的工作和原因有一个高层次的理解和设计。我们的目标是创建一个接受某些站点的地理坐标,并为它们解决 TSP 问题,,输出我们应该访问这些站点的顺序以最小化总旅行距离。我们不会创建一个完成所有任务的类;我们将把不同的功能保存在不同的类中,然后将它们组合在一起¹。

3.1. 优化器类图

我认为我们的类采用类似 scikit-learn 的 API 是便利的。然而,我不会将我们的新类称为“估算器”,而是称为“优化器”²。一个自解释的名称是 TravelingSalesmanOptimizer。它的一个辅助属性将是第 4 阶段中构建的 GeoAnalyzer 类。此外,由于这将不是我们在这个项目中创建的唯一优化器类,我们将把所有与模型求解相关的功能存储在一个单独的类 BaseOptimizer 中。原因是所有优化器,无论它们实现什么内部模型,都需要优化它,因此最好将与优化本身相关的逻辑保存在一个单独的类中,作为所有优化器的基类。

在下面的类图中,我们可以看到这三个类如何组合在一起。在每个类内部,我包含了它们的主要属性和方法(但不是全部),以便我们能理解整体概念。

图 2。 TSP 优化器的类图(作者提供的图像)

每个类的主要目的,简而言之:

  • BaseOptimizer 负责优化“适当”的优化器子类的模型,它不打算单独实例化。

  • GeoAnalyzer 是一个自包含的地理工具类。它在计算用户提供的坐标的距离矩阵的关键步骤中提供帮助,如果用户没有自定义距离数据,这一步是构建模型所必需的。

  • TravelingSalesmanOptimizer 是一个优化器,可以拟合具有站点坐标的数据框。一旦拟合,便可以检索这些站点的“访问顺序”。在内部,它实现了旅行商问题的数学模型作为一个Pyomo 模型对象。

有了具体的设计,我们来组装它。

3.2. GeoAnalyzer 的重新审视

由于GeoAnalyzer的代码已在前一次冲刺中开发完成,这里我们只是将其移动到一个新模块geoutils.py中,并从那里导入该类。如果你没有阅读创建它的文章,不要担心,你需要知道的是,它接受一些坐标数据框并输出一个距离矩阵,如下所示:

from geoutils import GeoAnalyzer

geo_analyzer = GeoAnalyzer()
geo_analyzer.add_locations(df_sites)
df_distances = geo_analyzer.get_distance_matrix(precision=0)

display(df_distances)

图 3. GeoAnalyzer 生成的距离矩阵(图片由作者提供)

这个距离数据框是我们将在TravelingSalesmanOptimizer内部用于创建内部模型的,因为建模旅行销售员问题所需的真实数据是距离,而不是坐标。

3.3. 打下基础:优化工具的基类

第三次冲刺的文章中,第一个代码片段是关于实例化 Pyomo 求解器并打印其版本。稍后在2.1 节中,我们使用了这个求解器来优化模型。考虑到这一点,创建BaseOptimizer只需将这些部分组合成一个类即可。我们将稍微修改原始代码,以便更易于阅读,但本质上它是相同的,方便地封装起来。

import sys

import pyomo.environ as pyo

class BaseOptimizer:
    """ Base class for common functionality shared among optimizers, 
    regarding generic handling and solving of optimization models.
    It's not intended to be used by itself but as an abstract class 
    to be extended by actual optimizers that implement concrete Pyomo models.

    Attributes
    ----------
    _solution_exists : bool (default=None)
        Initially None, it takes a boolean value after a (subclass) optimizer 
        has had a fitting attempt: True if an optimal solution was found, False 
        otherwise. If the value is None, it means the optimizer hasn't been fit 
        to any data yet.

    is_fitted : bool
        True if-and-only-if the optimizer has been fitted successfully 
        and thus an optimal solution was found.
    """
    def __init__(self):
        self._solution_exists = None  # updated to True/False inside `_optimize` 
        self._setup_solver()

    #######################    solver setup    #######################
    def _setup_solver(self, solver_nickname="glpk"):
        """ Instantiates and stores a MILP solver as an internal attribute """
        solver = pyo.SolverFactory(solver_nickname)
        if not solver.available(exception_flag=False):
            raise Exception(f"Solver '{solver_nickname}' not found. "
                            "You can install it by running:\n"
                            "conda install -y -c conda-forge glpk")
        self._solver = solver

    def _print_solver_info(self) -> None:
        print("Solver info:", 
              f"name: {self._solver.name}", 
              f"version: {self._solver.version()}", 
              sep="\n - ")

    ######################    model handling    ######################
    def _optimize(self, model: pyo.ConcreteModel) -> bool:
        """ Solve the model. If an optimal solution is found, the model
        provided will have the solution inside and ˋTrueˋ is returned. 
        If an optimal solution isn't found, a warning is printed, the 
        results of the optimization are stored in the ˋ_resultsˋ attribute
        (so post-mortem analysis can be done) and False is returned """
        res = self._solver.solve(model)
        self._results = res  # store output of solver for inspection
        self._solution_exists = pyo.check_optimal_termination(res)

        # _solution_exists is True iff an optimal solution is found
        if not self._solution_exists:
            print("Optimal solution not found, check attribute '_results' "
                  "for details", file=sys.stderr)
        return self._solution_exists

    def _store_model(self, model: pyo.ConcreteModel) -> None:
        """ Stores the Pyomo model as a public attribute """
        self.model = model

    @property
    def is_model_created(self) -> bool:
        """ True if the (sub)class has an attribute named `model` """
        return hasattr(self, 'model')

    @property
    def is_fitted(self) -> bool:
        """ Returns whether the model of the child class has been fitted 
        (i.e., solved) successfully. Returns False otherwise, i.e., 
        if model hasn't been optimized yet, or if an optimal solution 
        wasn't found """
        return bool(self._solution_exists)

    ########################    model inspection    ########################
    def print_model_info(self) -> None:
        """ High-level overview of the number of components 
        (i.e., constraints, variables, etc.) in the model """
        if not self.is_model_created:
            print("No internal model exists yet. Fit me to some data first")
            return

        print(f"Name: {self.model.name}", 
              f"Num variables: {self.model.nvariables()}",
              f"Num constraints: {self.model.nconstraints()}", 
              f"Num objectives: {self.model.nobjectives()}",
              sep="\n- ")

关于这个类需要记住的主要事项是:

  1. 在实例化时,求解器由_setup_solver在内部设置。由于没有求解器我们无法解决任何模型,如果设置失败,将会抛出异常。如果找到了求解器,它会被作为私有属性保存。

  2. 每当一个BaseOptimizer的子类调用类似fit的方法时,方法_optimize将会被调用。_optimize接受一个模型,并使用内部求解器尝试解决它。如果存在最优解,属性_solution_exists将不再是None,而是取值True。如果没有最优解存在**³,它将取值False,并会打印一个警告。

剩余的方法在它们的文档字符串中进行了说明。现在,开始构建我们的第一个优化器。

4. 一个类解决所有问题:TravelingSalesmanOptimizer

我们不会从头开始。正如前面所述,我们已经开发了构建 TSP 的 Pyomo 模型并在sprint 3中解决它的代码。相信我,那是最难的部分。现在,我们的任务是将我们所做的组织成一种通用的方式,隐藏细节同时保留基本元素。某种意义上,我们希望优化器看起来像一个“魔法盒子”,即使是对数学建模不熟悉的用户也能直观地解决他们的 TSP 问题。

4.1. TravelingSalesmanOptimizer设计

我们的优化器类将包含“核心”方法,处理大部分工作,还有“表面”方法,作为类的高级接口,调用底层的核心方法。

这些步骤将成为优化器逻辑的核心:

  1. 从距离矩阵创建一个 Pyomo 模型。这由_create_model方法完成,该方法基本上封装了我们已经完成的概念验证代码。它接受一个距离矩阵的数据框,并基于此构建一个 Pyomo 模型。我们所做的与我们现在要做的唯一重要区别在于,初始站点不再硬编码为简单的"hotel",而是假定df_distances中第一行的站点。一般情况下,初始站点被认为是坐标数据框df_sites中的第一个。这一推广使得优化器能够解决任何实例。

  2. (尝试)解决模型。这是在继承自BaseOptimizer_optimize方法中执行的,只有找到解决方案时才返回True

  3. 从模型中提取解决方案并解析成易于理解和使用的形式。这发生在_store_solution_from_model方法内部,该方法检查已解决的模型并提取决策变量的值,以及目标函数的值,以创建属性tour_tour_distance_。此方法仅在存在解决方案时*被调用,因此如果未找到解决方案,“解决方案属性”tour_tour_distance_将不会被创建。这样做的好处是,在拟合之后,这两个“解决方案属性”的存在将通知用户存在解决方案。此外,变量和目标的最优值可以在任何时刻方便地检索,而不仅仅是在拟合时。

最后的两个步骤——找到并提取解决方案——被封装在最后一个“核心”方法 _fit_to_distances 中。

“但请稍等”——你可能会想——“如名字所示,_fit_to_distances 需要距离作为输入;我们的目标不是仅使用 坐标 来解决 TSP 问题吗,而不是 距离?” 是的,这就是 fit 方法 适配 的地方。我们将 坐标 传递给它,并利用 GeoAnalyzer 构建距离矩阵,然后由 _fit_to_distances 正常处理。这样,如果用户不想自己收集距离,他可以通过使用 fit 委派任务。然而,如果他更愿意使用自定义数据,他可以将其组装成 df_distances 并传递给 _fit_to_distances

4.2. TravelingSalesmanOptimizer 实现

让我们按照上面概述的设计逐步构建优化器。首先是一个简化版本,只构建模型并解决它——尚未进行任何解决方案解析。注意 __repr__ 方法如何在我们打印优化器时让我们知道它包含的站点的名称和数量。

from typing import Tuple, List

class TravelingSalesmanOptimizer(BaseOptimizer):
    """Implements the Miller–Tucker–Zemlin formulation [1] of the 
    Traveling Salesman Problem (TSP) as a linear integer program. 
    The TSP can be stated like: "Given a set of locations (and usually 
    their pair-wise distances), find the tour of minimal distance that 
    traverses all of them exactly once and ends at the same location 
    it started from. For a derivation of the mathematical model, see [2].

    Parameters
    ----------
    name : str
      Optional name to give to a particular TSP instance

    Attributes
    ----------
    tour_ : list
      List of locations sorted in visit order, obtained after fitting.
      To avoid duplicity, the last site in the list is not the initial 
      one, but the last one before closing the tour.

    tour_distance_ : float
      Total distance of the optimal tour, obtained after fitting.

    Example
    --------
    >>> tsp = TravelingSalesmanOptimizer()
    >>> tsp.fit(df_sites)  # fit to a dataframe of geo-coordinates
    >>> tsp.tour_  # list of sites sorted by visit order

    References
    ----------
    [1] https://en.wikipedia.org/wiki/Travelling_salesman_problem
    [2] https://towardsdatascience.com/plan-optimal-trips-automatically-with-python-and-operations-research-models-part-2-fc7ee8198b6c
    """
    def __init__(self, name=""):
        super().__init__()
        self.name = name

    def _create_model(self, df_distances: pd.DataFrame) -> pyo.ConcreteModel:
        """ Given a pandas dataframe of a distance matrix, create a Pyomo model 
        of the TSP and populate it with that distance data """
        model = pyo.ConcreteModel(self.name)

        # a site has to be picked as the "initial" one, doesn't matter which 
        # really; by lack of better criteria, take first site in dataframe 
        # as the initial one
        model.initial_site = df_distances.iloc[0].name

        #===========  sets declaration  ===========#
        list_of_sites = df_distances.index.tolist()

        model.sites = pyo.Set(initialize=list_of_sites, 
                              domain=pyo.Any, 
                              doc="set of all sites to be visited (𝕊)")

        def _rule_domain_arcs(model, i, j):
            """ All possible arcs connecting the sites (𝔸) """
            # only create pair (i, j) if site i and site j are different
            return (i, j) if i != j else None 

        rule = _rule_domain_arcs
        model.valid_arcs = pyo.Set(
            initialize=model.sites * model.sites,  # 𝕊 × 𝕊
            filter=rule, doc=rule.__doc__)

        model.sites_except_initial = pyo.Set(
            initialize=model.sites - {model.initial_site}, 
            domain=model.sites,
            doc="All sites except the initial site"
        )
        #===========  parameters declaration  ===========#
        def _rule_distance_between_sites(model, i, j):
            """ Distance between site i and site j (𝐷𝑖𝑗) """
            return df_distances.at[i, j]  # fetch the distance from dataframe

        rule = _rule_distance_between_sites
        model.distance_ij = pyo.Param(model.valid_arcs, 
                                      initialize=rule, 
                                      doc=rule.__doc__)

        model.M = pyo.Param(initialize=1 - len(model.sites_except_initial),
                            doc="big M to make some constraints redundant")

        #===========  variables declaration  ===========#
        model.delta_ij = pyo.Var(model.valid_arcs, within=pyo.Binary, 
                                 doc="Whether to go from site i to site j (𝛿𝑖𝑗)")

        model.rank_i = pyo.Var(model.sites_except_initial, 
                               within=pyo.NonNegativeReals, 
                               bounds=(1, len(model.sites_except_initial)), 
                               doc=("Rank of each site to track visit order"))

        #===========  objective declaration  ===========#
        def _rule_total_distance_traveled(model):
            """ total distance traveled """
            return pyo.summation(model.distance_ij, model.delta_ij)

        rule = _rule_total_distance_traveled
        model.objective = pyo.Objective(rule=rule, 
                                        sense=pyo.minimize, 
                                        doc=rule.__doc__)

        #===========  constraints declaration  ===========#
        def _rule_site_is_entered_once(model, j):
            """ each site j must be visited from exactly one other site """
            return sum(model.delta_ij[i, j] 
                       for i in model.sites if i != j) == 1

        rule = _rule_site_is_entered_once
        model.constr_each_site_is_entered_once = pyo.Constraint(
                                                  model.sites, 
                                                  rule=rule, 
                                                  doc=rule.__doc__)

        def _rule_site_is_exited_once(model, i):
            """ each site i must departure to exactly one other site """
            return sum(model.delta_ij[i, j] 
                       for j in model.sites if j != i) == 1

        rule = _rule_site_is_exited_once
        model.constr_each_site_is_exited_once = pyo.Constraint(
                                                  model.sites, 
                                                  rule=rule, 
                                                  doc=rule.__doc__)

        def _rule_path_is_single_tour(model, i, j):
            """ For each pair of non-initial sites (i, j), 
            if site j is visited from site i, the rank of j must be 
            strictly greater than the rank of i.
            """
            if i == j:  # if sites coincide, skip creating a constraint
                return pyo.Constraint.Skip

            r_i = model.rank_i[i]
            r_j = model.rank_i[j]
            delta_ij = model.delta_ij[i, j]
            return r_j >= r_i + delta_ij + (1 - delta_ij) * model.M

        # cross product of non-initial sites, to index the constraint
        non_initial_site_pairs = (
            model.sites_except_initial * model.sites_except_initial)

        rule = _rule_path_is_single_tour
        model.constr_path_is_single_tour = pyo.Constraint(
            non_initial_site_pairs,
            rule=rule, 
            doc=rule.__doc__)

        self._store_model(model)  # method inherited from BaseOptimizer
        return model

    def _fit_to_distances(self, df_distances: pd.DataFrame) -> None:
        self._name_index = df_distances.index.name
        model = self._create_model(df_distances)
        solution_exists = self._optimize(model)
        return self

    @property
    def sites(self) -> Tuple[str]:
        """ Return tuple of site names the optimizer considers """
        return self.model.sites.data() if self.is_model_created else ()

    @property
    def num_sites(self) -> int:
        """ Number of locations to visit """
        return len(self.sites)

    @property
    def initial_site(self):
        return self.model.initial_site if self.is_fitted else None

    def __repr__(self) -> str:
        name = f"{self.name}, " if self.name else ''
        return f"{self.__class__.__name__}({name}n={self.num_sites})"

让我们快速检查优化器的行为。实例化时,优化器不包含任何站点数量,如表示字符串所示,也没有内部模型,并且当然没有进行适配。

tsp = TravelingSalesmanOptimizer("trial 1")

print(tsp)
#[Out]: TravelingSalesmanOptimizer(trial 1, n=0)
print(tsp.is_model_created, tsp.is_fitted)
#[Out]: (False, False)

我们现在将其适配到距离数据上,如果没有收到警告,意味着一切顺利。我们可以看到,现在表示字符串告诉我们我们提供了 9 个站点,存在一个内部模型,并且优化器已经适配到距离数据上:

tsp._fit_to_distances(df_distances)

print(tsp)
#[Out]: TravelingSalesmanOptimizer(trial 1, n=9)
print(tsp.is_model_created, tsp.is_fitted)
#[Out]: (True, True)

最优解的发现通过模型的排名决策变量中的明确值得到了证实:

tsp.model.rank_i.get_values()
{'Sacre Coeur': 8.0,
 'Louvre': 2.0,
 'Montmartre': 7.0,
 'Port de Suffren': 4.0,
 'Arc de Triomphe': 5.0,
 'Av. Champs Élysées': 6.0,
 'Notre Dame': 1.0,
 'Tour Eiffel': 3.0}

这些排名变量表示最优旅行中的停靠点的时间顺序。如果你回忆一下 它们的定义,它们在所有站点上定义,除了初始站点⁵,这就是为什么酒店没有出现在其中。很简单,我们可以将酒店添加为排名 0,那么我们就有了 问题的答案。我们不需要提取 𝛿ᵢⱼ,即旅行的 个别 弧的决策变量,来知道我们应该以什么顺序访问站点。虽然这是真的,我们仍然会使用弧变量 𝛿ᵢⱼ 来从解决的模型中提取确切的停靠顺序。

💡 敏捷 不必是 脆弱的

如果我们的唯一目标是解决 TSP 问题,而不考虑 扩展 模型以涵盖我们实际问题的更多细节,使用排名变量来提取最优旅行路径就足够了。然而,由于 TSP 仅仅是 将成为更复杂模型的初始原型,我们最好从弧决策变量𝛿ᵢⱼ中提取解决方案,因为任何涉及路由决策的模型中都会包含这些变量。其他决策变量是辅助的,当需要时,它们的作用是表示状态或指示依赖于 真实 决策变量𝛿ᵢⱼ的条件。正如你将在接下来的文章中看到的,选择排名变量来提取旅行路径适用于纯 TSP 模型,但对于那些使访问某些站点 可选 的扩展则不适用。因此,如果我们从𝛿ᵢⱼ中提取解决方案, 我们的方法将是通用和可重复使用的,无论我们使用多么复杂的模型

这种方法的好处将在接下来的文章中显现出来,这些文章中增加了新的要求,因此模型中需要额外的变量。在为什么部分讲解完毕后,让我们进入如何部分。

4.2.1 从模型中提取最优旅行路径

  • 我们有变量𝛿ᵢⱼ,按可能的弧(i, j)索引,其中𝛿ᵢⱼ=0 表示弧未被选择,𝛿ᵢⱼ=1 表示弧被选择。

  • 我们想要一个数据框,其中站点位于索引中(如我们的输入df_sites所示),并且停靠点编号在列 "visit_order"中指示。

  • 我们编写一个方法从拟合的优化器中提取这样的数据框。以下是我们将遵循的步骤,每一步都封装在自己的辅助方法中:

  1. 从𝛿ᵢⱼ中提取所选的弧,𝛿ᵢⱼ表示旅行路径。完成在_get_selected_arcs_from_model中。

  2. 将弧(边)列表转换为停靠点(节点)列表。完成在_get_stops_order_list中。

  3. 将停靠点列表转换为具有一致结构的数据框。完成在_get_tour_stops_dataframe中。

由于所选弧是混合的(,不按“遍历顺序”),获得有序的停靠点列表并不那么直接。为了避免复杂的代码,我们利用弧表示的事实,使用图对象G_tour按顺序遍历旅行路径节点,从而轻松地得到列表。

import networkx as nx

# class TravelingSalesmanOptimizer(BaseOptimizer):
    # def __init__()
    # def _create_model()
    # def _fit_to_distances()
    # def sites()
    # def num_sites()
    # def initial_site()

    _Arc = Tuple[str, str]

    def _get_selected_arcs_from_model(self, model: pyo.ConcreteModel) -> List[_Arc]:
        """Return the optimal arcs from the decision variable delta_{ij}
        as an unordered list of arcs. Assumes the model has been solved"""
        selected_arcs = [arc 
                         for arc, selected in model.delta_ij.get_values().items()
                         if selected]
        return selected_arcs

    def _extract_solution_as_graph(self, model: pyo.ConcreteModel) -> nx.Graph:
        """Extracts the selected arcs from the decision variables of the model, stores 
        them in a networkX graph and returns such a graph"""
        selected_arcs = self._get_selected_arcs_from_model(model)
        self._G_tour = nx.DiGraph(name=model.name)
        self._G_tour.add_edges_from(selected_arcs)
        return self._G_tour

    def _get_stops_order_list(self) -> List[str]:
        """Return the stops of the tour in a list **ordered** by visit order"""
        visit_order = []
        next_stop = self.initial_site  # by convention...
        visit_order.append(next_stop)  # ...tour starts at initial site

        G_tour = self._extract_solution_as_graph(self.model)
        # starting at first stop, traverse the directed graph one arc at a time
        for _ in G_tour.nodes:
            # get consecutive stop and store it
            next_stop = list(G_tour[next_stop])[0]
            visit_order.append(next_stop)
        # discard last stop in list, as it's repeated (the initial site) 
        return visit_order[:-1]

    def get_tour_stops_dataframe(self) -> pd.DataFrame:
        """Return a dataframe of the stops along the optimal tour"""
        if self.is_fitted:
            ordered_stops = self._get_stops_order_list()
            df_stops = (pd.DataFrame(ordered_stops, columns=[self._name_index])
                          .reset_index(names='visit_order')  # from 0 to N
                          .set_index(self._name_index)  # keep index consistent
            )
            return df_stops

        print("No solution found. Fit me to some data and try again")

让我们看看这个新方法给了我们什么:

tsp = TravelingSalesmanOptimizer("trial 2")
tsp._fit_to_distances(df_distances)
tsp.get_tour_stops_dataframe()

图 4。解决方案(最优旅行路径)按排名站点(图由作者提供)

visit_order列表明我们应该从酒店前往巴黎圣母院,然后到卢浮宫,依此类推,直到最后一个停靠点,蒙马特高地。之后,显然必须返回酒店。很好,现在我们有了一个易于解释和使用的解决方案格式。但停靠点的顺序并不是我们唯一关心的。目标函数的值也是一个重要的指标,因为它是指导我们决策的标准。对于我们特别的 TSP 问题,这意味着获取最优路线的总距离。

4.2.2 从模型中提取最优目标

就像我们没有使用排名变量来提取停靠点序列一样,因为在更复杂的模型中,它们的值不会与路线停靠点一致,我们也不会直接使用目标函数来获得路线的总距离,尽管在这里这两种测量都是等效的。在更复杂的模型中,目标函数还会包含其他目标,因此这种等效性将不再成立。

现在,我们将保持简单,创建一个非公开方法_get_tour_total_distance,这明确表示了意图。这个距离来源的细节是隐藏的,并且会依赖于更高级模型关心的特定目标。目前,细节很简单:获取解决模型的目标值。

# class TravelingSalesmanOptimizer(BaseOptimizer):
    # def __init__()
    # def _create_model()
    # def _fit_to_distances()
    # def sites()
    # def num_sites()
    # def initial_site()
    # def _get_selected_arcs_from_model()
    # def _extract_solution_as_graph()
    # def _get_stops_order_list()
    # def get_tour_stops_dataframe()

    def _get_tour_total_distance(self) -> float:
        """Return the total distance of the optimal tour"""
        if self.is_fitted:
            # as the objective is an expression for the total distance, 
            distance_tour = self.model.objective()  # we just get its value
            return distance_tour

        print("Optimizer is not fitted to any data, no optimal objective exists.")
        return None

现在看起来可能有些多余,但它将作为一个提醒,告诉我们未来的自己,应该遵循抓取目标值的设计。我们来看看:

tsp = TravelingSalesmanOptimizer("trial 3")
tsp._fit_to_distances(df_distances)
print(f"Total distance: {tsp._get_tour_total_distance()} m")
# [Out]: Total distance: 14931.0 m

总距离约为 14.9 公里。由于最优路线及其距离都很重要,让我们使优化器在每次调用_fit_to_distances方法时,将它们一起存储,并且只有在找到最优解时

4.2.3 将解决方案存储在属性中

在上面的_fit_to_distances实现中,我们只是创建了一个模型并解决了它,没有对模型中存储的解决方案进行任何解析。现在,我们将修改_fit_to_distances以便当模型解决方案成功时,会创建并提供两个新的属性,即解决方案的两个相关部分:tour_tour_distance_。为了简单起见,tour_属性不会返回我们之前创建的数据框,而是返回一个按顺序排列的停靠点列表。新的方法_store_solution_from_model负责这个任务。

# class TravelingSalesmanOptimizer(BaseOptimizer):
    # def __init__()
    # def _create_model()
    # def sites()
    # def num_sites()
    # def initial_site()
    # def _get_selected_arcs_from_model()
    # def _extract_solution_as_graph()
    # def _get_stops_order_list()
    # def get_tour_stops_dataframe()
    # def _get_tour_total_distance()

    def _fit_to_distances(self, df_distances: pd.DataFrame):
        """Creates a model of the TSP using the distance matrix 
        provided in `df_distances`, and then optimizes it. 
        If the model has an optimal solution, it is extracted, parsed and 
        stored internally so it can be retrieved.

        Parameters
        ----------
        df_distances : pd.DataFrame
            Pandas dataframe where the indices and columns are the "cities" 
            (or any site of interest) of the problem, and the cells of the 
            dataframe contain the pair-wise distances between the cities, i.e.,
            df_distances.at[i, j] contains the distance between i and j.

        Returns
        -------
        self : object
            Instance of the optimizer.
        """
        model = self._create_model(df_distances)
        solution_exists = self._optimize(model)

        if solution_exists:
            # if a solution wasn't found, the attributes won't exist
            self._store_solution_from_model()

        return self

    #====================  solution handling  ====================
    def _store_solution_from_model(self) -> None:
        """Extract the optimal solution from the model and create the "fitted 
        attributes" `tour_` and `tour_distance_`"""
        self.tour_ = self._get_stops_order_list()
        self.tour_distance_ = self._get_tour_total_distance()

让我们再次将优化器拟合到距离数据上,看看现在获取解决方案有多容易:

tsp = TravelingSalesmanOptimizer("trial 4")._fit_to_distances(df_distances)

print(f"Total distance: {tsp.tour_distance_} m")
print(f"Best tour:\n", tsp.tour_)
# [Out]:
# Total distance: 14931.0 m
# Best tour:
# ['hotel', 'Notre Dame', 'Louvre', 'Tour Eiffel', 'Port de Suffren', 'Arc de Triomphe', 'Av. Champs Élysées', 'Montmartre', 'Sacre Coeur']

很好。但我们可以做得更好。为了进一步提高这个类的可用性,让我们允许用户仅提供站点坐标的数据框来解决问题。由于不是每个人都能为他们感兴趣的站点收集距离矩阵,类可以处理这个问题并提供一个*似的距离矩阵。这在第 3.2 节中通过GeoAnalyzer实现了,这里我们只是将其放在新的fit方法下:

# class TravelingSalesmanOptimizer(BaseOptimizer):
    # def __init__()
    # def _create_model()
    # def _fit_to_distances()
    # def sites()
    # def num_sites()
    # def initial_site()
    # def _get_selected_arcs_from_model()
    # def _extract_solution_as_graph()
    # def _get_stops_order_list()
    # def get_tour_stops_dataframe()
    # def _get_tour_total_distance()
    # def _store_solution_from_model()

    def fit(self, df_sites: pd.DataFrame):
        """Creates a model instance of the TSP problem using a 
        distance matrix derived (see notes) from the coordinates provided 
        in `df_sites`.

        Parameters
        ----------
        df_sites : pd.DataFrame
            Dataframe of locations "the salesman" wants to visit, having the 
            names of the locations in the index and at least one column 
            named 'latitude' and one column named 'longitude'.

        Returns
        -------
        self : object
            Instance of the optimizer.

        Notes
        -----
        The distance matrix used is derived from the coordinates of `df_sites`
        using the ellipsoidal distance between any pair of coordinates, as 
        provided by `geopy.distance.distance`."""
        self._validate_data(df_sites)

        self._name_index = df_sites.index.name
        self._geo_analyzer = GeoAnalyzer()
        self._geo_analyzer.add_locations(df_sites)
        df_distances = self._geo_analyzer.get_distance_matrix(precision=0)
        self._fit_to_distances(df_distances)
        return self

    def _validate_data(self, df_sites):
        """Raises error if the input dataframe does not have the expected columns"""
        if not ('latitude' in df_sites and 'longitude' in df_sites):
            raise ValueError("dataframe must have columns 'latitude' and 'longitude'")

现在我们达到了我们的目标:从仅仅是站点位置中找到最佳旅行路线(而不是像以前那样从距离中找到):

tsp = TravelingSalesmanOptimizer("trial 5")
tsp.fit(df_sites)

print(f"Total distance: {tsp.tour_distance_} m")
tsp.tour_
#[Out]:
# Total distance: 14931.0 m
# ['hotel',
# 'Notre Dame',
# 'Louvre',
# 'Tour Eiffel',
# 'Port de Suffren',
# 'Arc de Triomphe',
# 'Av. Champs Élysées',
# 'Montmartre',
# 'Sacre Coeur']

4.3. TravelingSalesmanOptimizer 简明指南

恭喜!我们已经达到了优化器非常直观的使用阶段。为了方便起见,我将添加另一种方法,这在我们做[敏感性分析]和比较不同模型的结果时会非常有用。优化器现在会以列表或由get_tour_stops_dataframe()返回的单独数据框的形式告诉我最佳的访问顺序,但我希望它能告诉我通过转换我直接提供的位置数据框的访问顺序——即返回具有最佳停靠顺序的新列的数据框。方法fit_prescribe将负责这一点:

# class TravelingSalesmanOptimizer(BaseOptimizer):
    # def __init__()
    # def _create_model()
    # def sites()
    # def num_sites()
    # def initial_site()
    # def _get_selected_arcs_from_model()
    # def _extract_solution_as_graph()
    # def _get_stops_order_list()
    # def get_tour_stops_dataframe()
    # def _get_tour_total_distance()
    # def _fit_to_distances()
    # def _store_solution_from_model()
    # def fit()
    # def _validate_data()

    def fit_prescribe(self, df_sites: pd.DataFrame, sort=True) -> pd.DataFrame:
        """In one line, take in a dataframe of locations and return 
        a copy of it with a new column specifying the optimal visit order
        that minimizes total distance.

        Parameters
        ----------
        df_sites : pd.DataFrame
            Dataframe with the sites in the index and the geolocation 
            information in columns (first column latitude, second longitude).

        sort : bool (default=True)
            Whether to sort the locations by visit order.

        Returns
        -------
        df_sites_ranked : pd.DataFrame 
            Copy of input dataframe `df_sites` with a new column, 'visit_order', 
            containing the stop sequence of the optimal tour.

        See Also
        --------
        fit : Solve a TSP from just site locations.

        Examples
        --------
        >>> tsp = TravelingSalesmanOptimizer()
        >>> df_sites_tour = tsp.fit_prescribe(df_sites)  # solution appended
        """
        self.fit(df_sites)  # find optimal tour for the sites

        if not self.is_fitted:  # unlikely to happen, but still
            raise Exception("A solution could not be found. "
            "Review data or inspect attribute `_results` for details."
            )
        # join input dataframe with column of solution
        df_sites_ranked = df_sites.copy().join(self.get_tour_stops_dataframe())    
        if sort:
            df_sites_ranked.sort_values(by="visit_order", inplace=True)
        return df_sites_ranked

现在我们可以用一行代码解决任何 TSP 问题:

tsp = TravelingSalesmanOptimizer("Paris")

tsp.fit_prescribe(df_sites)

图 6。解决方案(最佳旅行路线)附加到站点数据框中(图像来源于作者)

如果我们想保持位置的原始顺序,就像它们在df_sites中一样,我们可以通过指定sort=False来实现:

tsp.fit_prescribe(df_sites, sort=False)

图 7。解决方案附加到站点数据框中,站点顺序保持不变(图像来源于作者)

如果我们好奇的话,也可以检查内部模型在解决我们特定的 TSP 实例时所需的变量和约束数量。这在进行调试或性能分析时会很有用。

tsp.print_model_info()
#[Out]:
# Name: Paris
# - Num variables: 80
# - Num constraints: 74
# - Num objectives: 1

5. 超越最佳解决方案:利用优化器提取见解

在我们结束本文之前,我想给你展示一些简短的例子,说明使用这个类是多么简单,不仅可以解决 TSP 问题,还能回答规划旅行时通常会出现的一般问题

例如,假设你已经制定了一份一天内在巴黎旅行中要访问的站点清单,并将它们放在名为df_sites的数据框中。你想知道仅仅是遍历它们的最佳旅行路线的总长度。你不需要去极大地了解这一点;这行代码就可以解决:

print(f"Distance: "
      f"{TravelingSalesmanOptimizer().fit(df_sites).tour_distance_} m"
)
#[Out]: Distance: 14931.0 m

现在假设你对你的站点集合不是很确定,考虑稍微缩减一下。你正在考虑跳过对凯旋门的访问,因为它离酒店很远。你可能会想:“如果我不去这个站点,最佳旅行路线会变化多少?”。感谢优化器,得到答案非常简单:

site_removed = 'Arc de Triomphe'

df_sites_but_one = df_sites.drop(site_removed, axis=0)
# baseline scenario
tsp_all = TravelingSalesmanOptimizer().fit(df_sites)
# alternative scenario
tsp_all_but_one = TravelingSalesmanOptimizer().fit(df_sites_but_one)

print(
    f"Distance tour ({tsp_all.num_sites} sites): {tsp_all.tour_distance_} m",
    f"Distance tour without {site_removed}: {tsp_all_but_one.tour_distance_} m",
    f"Difference: {tsp_all.tour_distance_ - tsp_all_but_one.tour_distance_} m",
    sep="\n"
)
#[Out]:
# Distance tour (9 sites): 14931.0 m
# Distance tour without Arc de Triomphe: 14162.0 m
# Difference: 768 m

通过这个,你可以知道如果你跳过凯旋门,你将节省大约 768 米的步行距离。是否值得就由你来决定。

这不仅仅是关于最佳路线:我们也可以找到“最佳”酒店!

作为另一个实际示例,假设你已经确定了要访问的景点列表,但还没有选择酒店。你有几个选择,为了更好地决策,你想知道每个酒店选择对你将要进行的旅行总距离的影响。然后,这只是用包含新候选酒店的景点数据框拟合另一个优化器并将最佳距离与使用初始酒店的最佳距离进行比较

# make new locations dataframe with the alternative hotel
df_sites_hotel_2 = df_sites.copy()  # sites to visit remain the same
df_sites_hotel_2.loc['hotel'] = (48.828759, 2.329396)  # new hotel coordinates

# solve the TSP for each "hotel scenario"
tsp1 = TravelingSalesmanOptimizer("hotel 1").fit(df_sites)
tsp2 = TravelingSalesmanOptimizer("hotel 2").fit(df_sites_hotel_2)

print(f"Distance tour {tsp1.name}: {tsp1.tour_distance_} m")
print(f"Distance tour {tsp2.name}: {tsp2.tour_distance_} m")
print(f"Difference: {tsp2.tour_distance_ - tsp1.tour_distance_} m")
#[Out]:
# Distance tour hotel 1: 14931 m
# Distance tour hotel 2: 17772 m
# Difference: 2841 m

结论是,如果我们选择“酒店 2”,那么我们将比选择“酒店 1”多步行 2.8 公里,在穿越相同景点的旅游中。在其他条件相同的情况下,这告诉我们“酒店 1”比“酒店 2”更佳选择(考虑到我们想参观的景点)。

6. 结论(或计划下一个迭代)

在这篇文章中,我们创建了两个新类(BaseOptimizerTravelingSalesmanOptimizer)。在未来的迭代中,我们将使用它们、扩展它们,并将更高级的优化器添加到工具包中,所以为了做到干净整洁,让我们将它们移动到一个新模块routimizers.py中。

现在,最后一点需要记住的是这个 MVP 的范围。这个初始优化器给出了“我应该以什么顺序访问这些景点?”的问题的答案,但它没有告诉我们如何从一个景点到下一个景点。这没关系,因为这正是像 Google Maps 这样的 GIS 应用程序的工作,而不是我们谦逊的优化器。

👁 我们的优化器解决了 战术 问题,即告诉我们在旅游中最佳的 停留顺序,而不是 操作性 问题,即引导我们完成这次旅行。后者问题可以通过许多优秀的 GIS 应用程序轻松解决,例如 Google Maps。

如果你对结果(旅行)感到满意,并且想要在现实生活中实际实现它,那么很简单:直接去 Google Maps,将这些景点按优化器指定的顺序输入为停留点。点击“开始”,你就能得到操作性答案。

然而,仍然感觉应该能够可视化结果,而不仅仅满足于优化器在新列中放置的数字;即使不是为了实际应用,也应该是为了更好地理解解决方案。在第三次冲刺中,我们确实稍微可视化了一下解决方案,即使没有坐标。现在我们确实有了站点的坐标,我们可以做得更好,以更现实的方式可视化最终的最佳路线。这正是我们下一次冲刺的目标:创建出色的视觉效果,以便更好地理解优化器提供的解决方案(路线),并在此过程中提出和回答更多、更好的问题,从而帮助我们规划更好的行程。拿一杯咖啡,直接开始吧:

## 使用 Python 在互动地图上可视化路线:第一部分

一份有关运输问题的互动数据可视化的务实指南,使用 Folium

towardsdatascience.com

脚注

  1. 这种分离的好处将在未来的冲刺中变得明显,特别是针对扩展 TSP 的问题。↩

  2. 因为我们的课程将优化决策,而不是估计参数。两者有微妙的差别。确实,scikit-learn 估算器在模型训练期间确实会进行优化,但这种区别仍然很重要,因为 优化在机器学习和运筹学中的意义不同。一个 估算器 使用优化来 估计 模型参数 的未知值,而一个 优化器 使用优化来 找到 代表我们需要做的 决策 的最佳值。在估算器中,目标函数始终是 损失函数,它衡量 预测结果与真实结果之间的差异程度。在优化器中(或更一般地,在运筹学模型中)目标函数 可以是任意的,因为它 是衡量我们所期望的全球状态的标准,取决于我们的决策。换句话说:在机器学习中,我们寻找 最佳模型参数 以产生 最佳泛化 的数据。然而,在运筹学中,我们的可能行动被表示在模型中,因此 我们寻找 最佳行动 以产生 最佳结果 在我们拥有的资源下。关键区别在于没有所谓的“真实决策”需要通过优化技术从数据中“估计”出来。决策是根据数据给出的,而不是从数据中估计的。↩

  3. 这只能因为 两个原因: 模型是 不可行(最常见),或者模型是 无界(较少见,但可能)。↩

  4. 这就是为什么,在将输入坐标读入数据框时,我们首先读取酒店的坐标。↩

  5. 这是一个 技术要求 准确 地说,其中一个站点没有相关的排名变量(哪一个无关紧要,但通常选择“初始站点”)。由于酒店在 df_distances 的第一行的索引中,因此被认为是初始站点并存储在 Pyomo 模型中(见属性 tsp.model.initial_site),没有相关的排名变量。请记住,排名 rᵢ 的工作是 不是 指示访问顺序(这在 𝛿ᵢⱼ 中固有),而仅仅是防止形成子巡回。↩

感谢阅读,下一篇文章见 这里! 📈😊

随时可以关注我,向我提问,给我反馈,或通过 LinkedIn 联系我。

深入了解 Colab 的新更新和增强功能

原文:towardsdatascience.com/a-close-look-at-colabs-new-updates-and-enhancements-f1225fd5d504

充分利用 Google Colab 笔记本

Parul PandeyTowards Data Science Parul Pandey

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

--

图片由作者使用 Excalidraw 创建。

最后更新于 2023 年 12 月 20 日

每当我进行编码研讨会或教程时,Google Colaboratory Notebooks——或称为 Colab——始终是我首选的资源。它消除了讲师和参与者在环境设置上的麻烦,并且提供了对强大计算资源(如 GPUs 和 TPUs)的免费访问。凭借其易于共享的链接,Colab 使整个学习过程更加高效和有效。为了充分利用 Colab 提供的功能,我始终关注其最新发布和更新。

虽然我通常 通过 LinkedIn 分享这些更新,但大量的新功能和增强功能值得这样一篇更全面的文章。我之前的 Colab 重要功能汇总是在 2022 年,显然现在需要一个更新的概述。

## 使用这些技巧更高效地使用 Colab

towardsdatascience.com

让我们看看 Colab 的一些杰出功能,它们在我的工作中非常宝贵。我希望你也会发现它们同样有用。

1. 来自 Google Sheets 的智能数据粘贴

现在,当用户将数据粘贴到一个空的代码单元格时,Colab 会自动生成代码来创建一个pd.DataFrame。这一增强功能消除了传统流程中的额外步骤,使用户体验更加流畅。

从 Google Sheets 智能粘贴数据 | 图片由作者提供

而且不仅如此——如果代码单元格中已经有文本,Colab 会贴心地为你添加 CSV 文字。

2. 从 Pandas 数据框自动生成图表

Colab 中的数据可视化现在变得更加便捷,通过其新功能——从 Pandas 数据框自动生成图表。当你执行一个以数据框引用结束的代码单元格时,输出的右下角会出现一个自动绘图按钮。

在 Colab 中自动生成 Pandas 数据框图表的图标 | 图片由作者提供

点击此图标会显示针对你的数据框量身定制的各种推荐图表,每一个图表都可以通过单击无缝地添加到你的笔记本中。

从 Pandas 数据框自动生成图表 | 图片由作者提供

如演示所示,这个功能不仅展示了渲染的图表,还附加了相关代码,并将其整齐地放置在笔记本的后续单元格中。

3. 交互式数据框

Pandas 数据框提供了一种结构化的表格格式,这种格式直观且兼容多种数据操作。使用 Google Colab,用户可以实时与这些数据框进行交互。当数据框在 Colab 中加载并显示时,会出现一个类似计算器的图标,如下所示。

渲染数据框交互式图标 | 图片由作者提供

点击此图标,数据框会转换为交互式表格:

Colab 中的交互式数据框 | 图片由作者提供

如你所见,我们立即展示了良好的表格格式。增强的交互性提供了许多优势,例如:

  • 调整页面大小并使用分页功能可以提供更清晰的数据视图。

调整页面大小并使用分页功能可以提供更清晰的数据视图 | 图片由作者提供

  • 简单点击列标题可以让用户对整个数据集中的列进行排序,更新会立即可见。

在交互式格式中排序列变得容易 | 图片由作者提供

  • 可以通过直接字符串匹配或正则表达式匹配轻松筛选特定行。例如,要突出显示美国原产的汽车,用户可以在**origin**字段中搜索USA。还可以在搜索中设置值的边界。例如,识别 1970-1971 年间所有具有八个气缸且每加仑至少 15 英里燃油效率的美国原产汽车。

交互式数据框中的筛选操作 | 图片由作者提供

  • 数据可以以各种格式轻松复制,例如 CSV、markdown 或 JSON。

数据可以轻松以各种格式复制,例如 CSV、markdown 或 JSON | 图片来源:作者

本质上,Google Colab 的交互式表格支持动态数据搜索和过滤。这消除了不断重新运行单元格的需要,使数据洞察更加迅速,并改善了整体数据探索过程。

4. 管理单元格执行

Colab 现在支持两个新功能,旨在简化用户在笔记本中运行代码单元格的方式。

  • 其中第一个功能允许用户直接从笔记本的目录运行一组单元格。这可以通过上下文菜单中的**Run cells in section**选项完成,该选项由三个垂直点表示。

从笔记本的目录直接运行一组单元格 | 图片来源:作者

  • 第二个功能专注于在笔记本中设置先决条件。用户现在可以指定特定单元格在打开笔记本时自动运行。这可以通过位于**Edit**菜单下的笔记本设置对话框进行设置,在那里有一个选项可以Automatically run the first cell or section。如果笔记本仅以一个单元格开始,则该单元格将在每次访问笔记本时自动运行。类似地,如果笔记本以一个部分开始,则在打开笔记本时,该部分的所有初始单元格将自动执行。

指定特定单元格在打开笔记本时自动运行 | 图片来源:作者

5. 语言之间更轻松的转换

Google Colab 更新了其界面,以简化不同语言之间的切换过程。用户现在可以通过进入Edit然后选择Notebook settings轻松切换已安装的内核。R 编程语言是可用选项之一,因为它是默认安装的。

在 Colab 中转换 Python 和 R | 图片来源:作者

6. 笔记本 X Colab

通过弥合各种笔记本与强大的 Colab *台之间的差距,Colab 帮助提升用户体验并扩展 Colab 的多功能性。

1. Hugging Face 🤗 笔记本在 Colab 中

用户现在可以轻松访问、查看和运行Hugging Face 笔记本在 Colab 环境中,受益于其先进的执行功能和协作工具。这种集成简化了从 Hugging Face 上的模型托管到 Colab 中的实时演示的过渡。只需点击笔记本右上角的Open in Colab即可在 Colab *台内启动和执行它们。你可以在这里了解更多信息。

在 Colab 环境中运行 Hugging Face 笔记本 | 图片来源:作者

2. 移植到 Colab 的流行笔记本

Google Colab 不仅仅是一个编写和执行机器学习模型的*台,它还是一个集成复杂代码的卓越讲故事工具。Colab 拥有一系列有见地的案例研究,例如:

  • 斯坦福大学的 Ashwin Rao 教授制作了一个引人注目的 Colab 笔记本,深入探讨了 硅谷银行(SVB)失败的高层原因

  • Colab 团队还成功移植了流行的 Ten Minutes to Pandas 教程,为用户提供了一个引人入胜的 pandas 入门介绍。

7. 使用 Colab 的新 URL 快捷方式快速创建笔记本

类似于你可以通过链接如 [**docs.new**](http://docs.new) 立即创建新文档,或通过 [sheets.new](http://sheets.new) 创建新电子表格,现在也有一个简单的链接用于 Google Colab!如果你访问 **c**[**olab.new**](http://colab.new),你将立即开始一个新的笔记本。

使用 Colab 的新 URL 快捷方式快速创建笔记本 | 图片由作者提供

8. 程序化终止 Colab 会话

另一个新增功能是能够直接从代码中终止运行时。

from google.colab import runtime
runtime.unassign()

unassign() 函数指定要移除的活动运行时,并断开与任何笔记本会话的连接。无论是为了节省内存、重置会话还是结束项目,这个功能都确保用户可以直接在代码环境中轻松准确地完成操作。

程序化终止 Colab 会话 | 图片由作者提供

9. Colab 链接预览在 Docs 套件中

Colab 链接预览已集成到 Google Docs 套件中。现在,Colab 中的 Drive 笔记本链接可以转换为 Docs 套件中的智能卡片。此外,这一增强功能允许用户直接在 Google 生态系统内请求访问 Colab Drive 笔记本,简化了访问过程。

Colab 链接预览在 Docs 套件中 | 来源: https://x.com/GoogleColab/status/1701684666972205243?s=20

10. 生成 Colab 徽章

访问 openincolab.com 为你的 README、网站、文档等创建徽章。只需输入托管在 GitHub 上的笔记本的 URL,测试徽章,并获取 HTML 代码!

生成 Colab 徽章 | 图片由作者提供

11. 在 Colab 中轻松访问 Transformers

Colab 的托管运行时镜像现在预装了 transformers 库,为用户带来了一个小而重要的改进。保持设置的最新状态,定期更新镜像,使用 !pip install transformers --upgrade 强制升级以获取最新功能。

Transformers 库预装在 Colab 中 | 图片由作者提供

这一改进简化了工作流程,并解锁了令人兴奋的功能,例如轻松将 Huggingface 数据集读取到 Pandas 中。

轻松将 Huggingface 数据集读取到 Pandas 中 | 图片由作者提供

12. 使用 Colab 新的秘密功能保护您的 API 密钥

Colab 现在推出了一项新功能,允许您安全地存储私人密钥,如 Huggingface 或 Kaggle API 令牌。通过 Secrets,这些值保持私密,仅对您和您选择的笔记本可见。这确保了在进行项目时的安全性和安心感。

使用 Colab 新的秘密功能保护您的 API 密钥 | 图片由作者提供

13. Colab 向所有用户推出限时 AI 代码助手试用

Colab 现在向免费计划的用户提供 限时试用 AI 代码助手。AI 驱动的代码助手支持从自然语言生成代码和代码助手聊天机器人。根据官方博客,Colab 预计此服务将持续到 2024 年,只要容量允许。

使用 AI 代码助手来调试代码 | 图片由作者提供

结论

总结来说,这些最*对 Google Colab 的更新和改进在我使用该*台的日常工作中证明是非常有用的。从与 Google Docs 套件的集成到高级数据过滤功能等,这些更新共同提升了 Colab 作为协作数据科学工具的地位。所有这些更新都已从其官方发布说明和博客中整理出来,相关链接已在参考部分共享。我鼓励大家探索这些功能,如果你发现了额外的功能或有个人见解,请与社区分享。你的见解可能对他人有很大帮助!

参考资料

云迁移策略:5 步检查清单

原文:towardsdatascience.com/a-cloud-migration-strategy-a-5-step-checklist-1668c2ece01?source=collection_archive---------18-----------------------#2023-04-18

优化迁移到云端的成本和工作

Andrey KoptelovTowards Data Science Andrey Koptelov

·

关注 发表在 Towards Data Science ·6 分钟阅读·2023 年 4 月 18 日

--

由 Starline 设计 — Freepik.com

云迁移策略在开始带来好处之前需要大量的初始投资。这是一个复杂的过程,通常需要做出许多困难的决策。在某些情况下,迁移到云端的需求会落在你的技术专家身上,从数据科学家到机器学习工程师,他们可能无法单独处理这项任务。为了缓解这种情况,我们的专业 IT 顾问和 云迁移服务专家 已经联合努力,帮助你解决问题。

以下指南提供了控制云迁移项目的五个关键步骤。

1. 确定你需要多少云容量

传统的互联网基础设施提供商并不出售计算能力。相反,他们出售的是资源,或者更具体地说,是全天候运行的服务器,即使你没有使用这些资源,也按小时收费。在大多数情况下,你的云应用程序或系统在预付时间内并未完全加载,因此你在为服务器的空闲时间付费时浪费了预算。

现在,具体数值会根据你的运营规模有所不同。例如,如果一家本地公司不进行夜间操作,如报告或维护,它将面临 12 小时的停机时间。这意味着 50% 的基础设施预算将浪费。

可扩展性是一个敏感的问题

单凭抽象的流量预测来购买大型服务器可能过于乐观,而购买小型服务器则可能在系统因突发流行而承受巨大压力时出现故障。

因此,云迁移策略的正确方法是选择能够自动扩展的云服务,或使用基于 AWS Lambda、Azure Functions 或类似专业服务的无服务器后台系统。这些服务支持自动后台扩展,意味着你可以享受按需付费和按使用付费的模式。

然而,使用云服务并不总是一个好主意。例如,当托管在云端时,你不知道你的组件将部署在哪些物理服务器上。网络附加存储可能不如本地硬盘高效。在响应时间至关重要的高负载系统中,本地服务器可能表现更好。

2. 调整你的架构

如果你按原样迁移你的系统,云服务将总是比本地存储更贵。为了优化成本,你应该在迁移之前调整你的架构,并选择合适的服务来促进这一复杂过程。

你还应该考虑云提供商的管理成本和可扩展性开销,因为云提供商提供的是完全托管的服务。

考虑到所有这些因素,对云迁移的唯一正确方法是决定哪些系统组件必须保留在本地,哪些需要迁移到云端。

*衡服务利用

在做出重大决策之前,分析那些已满载和未充分利用的服务。首先优化架构将使云服务比专用服务器更便宜。为帮助你完成这一点,以下是一些最典型的开箱即用的步骤:

  • 将所有静态资源迁移到 CDN:这应能减轻应用服务器的一部分负担。

  • 使用*台内基础设施组件,如负载均衡器、消息队列和缓存服务。

  • 用无服务器处理程序(来自 AWS Lambda 或 Azure Functions)替换不常用的例程和服务。

  • 对于负载偶尔且对响应时间宽容的简单后端服务,可以完成完全替换。

要发现可以替换的更多服务,你可以咨询云计算提供商,如 AWS 和 Azure,它们目前是最受欢迎的两家。

计算成本

回答臭名昭著的定价问题非常困难。计算结果可能会因你认为的最佳选择——本地还是云——而倾斜到任何一边。

让我们举一个简单的例子。假设你有一个最初设计为本地运行的系统。这个系统非常繁忙,拥有超过 10 TB 的数据,每天有 8,000,000 多次事件更新,以及每分钟 30,000 次事件吞吐量。你需要为这个系统制定一个云迁移策略。

在这种情况下,你将分两步迁移:

  1. 你将重写基础设施组件,以便从无服务器服务中受益。

  2. 你将选择最佳的现成云服务。

通过优化和重新工程你的基础设施,你将能够在需要更少语言的情况下迁移。而且,通过使用无服务器服务,你将看到总体成本的显著下降。

通过这种方式迁移的一些好处包括自动扩展、简化开发和支持以及资源优化。

保护你的数据

出于安全原因,某些行业无法完全考虑云迁移:例如,银行和金融、公共部门、保险和医疗保健。然而,包括政府机构在内的许多高度监管组织已经找到了一种在云中托管其系统的方法。

他们选择了混合模型,只需在云中存储数据的一部分,从而通过维护严格的用户访问限制和利用强大的政府防火墙来保护自己免受攻击。

即使你有多个安全顾虑,如果你和你选择的云服务提供商能够共享以下责任,迁移到云中仍然是可能的。

你的责任

云服务提供商的责任

根据云服务提供商的安全认证来选择。

分析安全风险并扫描你的架构漏洞。

制定有效的 SLA,涵盖提供商-客户关系、第三方和云经纪人。

制定针对特定案例的威胁管理计划,明确的事件解决路线图。

选择混合模型,在这种模型中,你可以将关键任务解决方案保存在本地以确保安全。

使用与企业网络隔离的虚拟机来应对安全风险。

维护一个灵活的安全模型,随着威胁的变化不断演进。

选择开源

许多企业在寻求外部软件开发服务并希望采用云服务时,担心所谓的供应商锁定,即在承诺某个供应商后无法离开云服务或更改供应商。其他重要的担忧还包括在更换供应商时应用程序的互操作性和可移植性,以及可能需要支付更多费用来更改基础设施。

为了应对锁定问题,你应该通过研究提供商的文档或咨询专家,发现其品牌名称背后的实际内容。例如,以下是一些来自 AWS 生态系统的开源或可互操作的服务:

  • AWS ElastiCache:使用 Redis 或 Memcached 作为后台缓存服务

  • 亚马逊 Aurora:与 MySQL 兼容的数据库服务

  • 亚马逊 Redshift:最初基于 PostgreSQL 构建的数据仓库服务,仍然兼容大多数 SQL 功能

你还可以选择使用标准或开放协议的云服务,允许在必要时更换提供商。此外,开放协议为设备互操作性提供了空间,无需使用专有的网关或接口。

现在,如果云服务提供商在开放协议的基础上还提供自己的专有协议,那么建议总是选择后者。例如,Azure 服务总线提供了自己的 API 以及一个支持的 AMQP——一种开放的消息传递协议。通过选择开放标准,你可以保持灵活性。这意味着你可以更换提供商,甚至部署支持相同协议的自有服务实例,而无需对客户端组件做出重大更改。

结论

云迁移是一个复杂的多层次过程,需要大量的规划。本指南概述了关键的云迁移步骤,帮助你最小化基础设施成本,避免供应商锁定,并解决云采用过程中常见的担忧。

时间差(0)与常数-α蒙特卡洛方法在随机游走任务中的比较

原文:towardsdatascience.com/a-comparison-of-temporal-difference-0-and-constant-%CE%B1-monte-carlo-methods-on-the-random-walk-task-bc6497eb7c92?source=collection_archive---------4-----------------------#2023-08-24

Tingsong Ou Towards Data Science Tingsong Ou

·

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

--

由 Midjourney 生成的图像,使用了付费订阅,符合一般商业条款[1]。

引言

蒙特卡洛(MC)方法和时序差分(TD)方法都是强化学习领域的基本技术;它们通过与环境互动的经验解决预测问题,而不是依赖于环境模型。然而,TD 方法是 MC 方法和动态规划(DP)的结合,因此在更新规则、引导和偏差/方差方面与 MC 方法有所不同。与 MC 方法相比,TD 方法在大多数情况下被证明具有更好的性能和更快的收敛速度。

在这篇文章中,我们将比较 TD 和 MC,或更具体地说,比较 TD(0) 和常数-α MC 方法,在一个简单的网格环境和一个更全面的随机游走 [2] 环境中。希望这篇文章能够帮助对强化学习感兴趣的读者更好地理解每种方法如何更新状态值函数,以及它们在相同测试环境中的性能差异。

我们将在 Python 中实现算法和比较,本帖中使用的库如下:

python==3.9.16
numpy==1.24.3
matplotlib==3.7.1

TD 与 MC 的差异

TD(0) 和常数-α MC 的介绍

常数-α MC 方法是一种常数步长参数 α 的常规 MC 方法,这个常数参数有助于使价值估计对最*的经验更加敏感。在实践中,α 值的选择取决于稳定性和适应性之间的权衡。以下是 MC 方法在时间 t 更新状态值函数的方程:

TD(0) 是 TD(λ) 的一个特例,它只看一步前的状态,是最简单的 TD 学习形式。该方法使用 TD 误差更新状态值函数,TD 误差是指状态的估计值与奖励加上下一个状态的估计值之间的差异。一个常数步长参数 α 与上述 MC 方法中的作用相同。以下是 TD(0) 在时间 t 更新状态值函数的方程:

一般来说,MC 和 TD 方法之间的差异体现在三个方面:

  1. 更新规则:MC 方法仅在回合结束后更新值;如果回合非常长,这可能会导致程序变慢,或者在没有回合的持续任务中,这可能会成为问题。相反,TD 方法在每个时间步更新价值估计;这是一种在线学习,特别适用于持续任务。

  2. 引导:在强化学习中,“引导”一词指的是基于其他价值估计来更新价值估计。TD(0) 方法基于下一个状态的价值来进行更新,因此它是一种引导方法;相反,MC 不使用引导,因为它直接从回报(G)中更新价值。

  3. 偏差/方差:MC 方法没有偏差,因为它们通过加权实际观察到的回报来估计值,而在过程中不进行估计;然而,MC 方法有较高的方差,尤其是在样本数量较少时。相反,TD 方法有偏差,因为它们使用了自助法,偏差可能会根据实际实现而有所不同;TD 方法方差较低,因为它使用了即时奖励加上对下一状态的估计,这*滑了因奖励和行动的随机性引起的波动。

在简单的网格世界设置中评估 TD(0) 和常数-α MC

为了使它们的差异更加直观,我们可以设置一个简单的网格世界测试环境,具有两个固定轨迹,运行这两种算法直到收敛,并检查它们如何不同地更新值。

首先,我们可以用以下代码设置测试环境:

图 1 左:环境设置。右:预设路径。来源:作者绘制的图

上图左侧显示了一个简单的网格世界环境设置。所有彩色单元格表示终端状态;代理在进入红色单元格时获得 +1 奖励,但在进入蓝色单元格时获得 -1 奖励。网格上的所有其他步骤返回零奖励。上图右侧标记了两个预设路径:一条到达蓝色单元格,另一条停在红色单元格;路径的交点有助于最大化两种方法之间的值差异。

然后我们可以使用上一节中的方程来评估环境。我们不对回报或估计进行折扣,并将 α 设置为一个小值 1e-3。当值增量的绝对和低于 1e-3 的阈值时,我们认为值已收敛。

评估结果如下:

图 2 TD(0) 和常数-α MC 评估结果。来源:作者绘制的图

上述图像中,两种算法在估计值的方式上的不同变得非常明显。MC 方法忠实于路径的回报,因此每条路径上的值直接表示其结束状态。然而,TD 方法提供了更好的预测,特别是在蓝色路径上——在交点之前的蓝色路径上的值也表示到达红色单元格的可能性。

以这个最小的案例为基础,我们准备转向一个更复杂的示例,尝试找出两种方法之间的性能差异。

随机游走任务

随机游走任务是 Sutton 等人提出的一个简单的马尔可夫奖励过程,用于 TD 和 MC 预测目的[2],如下图所示。在此任务中,代理从中心节点 C 开始。代理在每个节点上以相等的概率向右或向左迈一步。链条的两端有两个终止状态。进入左端的奖励为 0,进入右端的奖励为+1。在终止之前的所有步骤生成的奖励为 0。

图 3 随机游走。来源:作者提供的图

我们可以使用以下代码来创建随机游走环境:

=====Test: checking environment setup=====

Links:        None ← Node A → Node B
Reward:          0 ← Node A → 0

Links:      Node A ← Node B → Node C
Reward:          0 ← Node B → 0

Links:      Node B ← Node C → Node D
Reward:          0 ← Node C → 0

Links:      Node C ← Node D → Node E
Reward:          0 ← Node D → 0

Links:      Node D ← Node E → None
Reward:          0 ← Node E → 1

在随机策略下,环境中每个节点的真实值为[1/6, 2/6, 3/6, 4/6, 5/6]。该值通过使用贝尔曼方程的策略评估计算得出:

我们的任务是找出两个算法估计的值与真实值的接*程度;我们可以任意假设算法产生的值函数离真实值函数更*,通过*均均方根误差(RMS)来衡量,表示性能更好。

TD(0)和常数-a MC 在随机游走中的表现

算法

环境准备好后,我们可以开始在随机游走环境中运行这两种方法,并比较它们的表现。首先,让我们看一下这两个算法:

来源:latex中由作者编写的算法

来源:latex中由作者编写的算法

如前所述,MC 方法应该等到回合结束后才能更新从轨迹尾部得到的值,而 TD 方法则是逐步更新值。这种差异带来了初始化状态值函数时的一个技巧:在 MC 中,状态值函数不包括终止状态,而在 TD(0)中,该函数应包括终止状态,并且值为 0,因为 TD(0)方法总是提前一步看未来的状态,直到回合结束。

实现

在此实现中的α参数选择参考了书中[2]提出的参数;MC 方法的参数为[0.01, 0.02, 0.03, 0.04],而 TD 方法的参数为[0.05, 0.10, 0.15]。我曾经疑惑为何作者没有在两个算法中选择相同的参数集,直到我用 TD 参数运行 MD 方法:TD 参数对 MC 方法来说太高,因此不能展现 MC 的最佳性能。因此,我们将坚持书中的参数设置。现在,让我们运行这两个算法,找出它们在随机游走设置下的表现。

结果

图 4 算法比较结果。来源:作者提供的图

100 次比较后的结果如上图所示。TD 方法通常比 MC 方法提供更好的值估计,且α = 0.05 的 TD 方法可以非常接*真实值。图表还显示,MC 方法的方差比 TD 方法更高,因为兰花线的波动大于钢蓝线。

值得注意的是,对于这两种算法,当α值(相对)较高时,RMS 损失首先下降然后再上升。这种现象是由于值初始化和α值的共同作用。我们初始化了一个相对较高的 0.5,超过了节点 A 和 B 的真实值。由于随机策略使得有 50% 的机会选择“错误”步骤,从而使智能体远离正确的终端状态,因此更高的α值也会强调错误步骤,使结果偏离真实值。

现在让我们尝试将初始值降低到 0.1,并再次进行比较,看看问题是否得到缓解:

图 5 初始值为 0.1 的算法比较结果。来源:作者绘制的图

较低的初始值显然有助于缓解问题;没有明显的“下降然后上升”现象。然而,较低的初始值的副作用是学习效率较低,因为 RMS 损失在 150 轮后从未低于 0.05。因此,初始值、参数和算法性能之间存在权衡。

批量训练

在这篇文章中,我想提到的最后一点是对两种算法的批量训练比较。

考虑到我们面临以下情况:我们只在随机游走任务上积累了有限数量的经验,或者由于时间和计算限制,我们只能运行一定数量的轮次。批量更新 [2] 的想法是通过充分利用现有轨迹来应对这种情况。

批量训练的想法是反复更新一批轨迹上的值,直到值收敛到一个答案。只有在所有批次经验完全处理后,值才会被更新。让我们在随机游走环境中对这两种算法实施批量训练,看看 TD 方法是否仍优于 MC 方法。

结果

图 6 批量训练结果。来源:作者绘制的图

批量训练结果显示,TD 方法在有限经验下仍优于 MC 方法,两种算法的性能差距十分明显。

结论

在这篇文章中,我们讨论了常数-α MC 方法和 TD(0) 方法之间的区别,并比较了它们在随机游走任务中的表现。TD 方法在本文所有测试中都优于 MC 方法,因此将 TD 作为强化学习任务的方法是一个更可取的选择。然而,这并不意味着 TD 总是优于 MC,因为后者有一个最明显的优势:无偏差。如果我们面对的是一个不能容忍偏差的任务,那么 MC 可能是更好的选择;否则,TD 更能处理一般情况。

参考文献

[1] Midjourney 服务条款: docs.midjourney.com/docs/terms-of-service

[2] Sutton, Richard S., 和 Andrew G. Barto. 强化学习:导论。麻省理工学院出版社,2018。

本文的 GitHub 仓库:链接

使用 BigQuery 和 Looker Studio 进行队列分析的完整指南

原文:towardsdatascience.com/a-complete-guide-to-cohort-analysis-using-bigquery-and-looker-studio-1cd18c0edd79

逐步解密队列分析

Damien AzzopardiTowards Data Science Damien Azzopardi

·发布于 Towards Data Science ·阅读时间 5 分钟·2023 年 3 月 9 日

--

图片来源:Hunter HarrittUnsplash

介绍

让我们承认,队列分析乍看起来可能令人望而却步。但它是一个强大的工具,可以提供有价值的洞察,有时甚至是可视化数据的唯一正确方法。掌握它们将使你在数据分析的旅程中占据明显优势。

但首先,我们所说的队列分析是什么意思?

队列分析是一种研究和比较不同人群随时间变化的方法。

这些群体通过一个共同的特征来定义,例如他们加入服务的日期或首次购买的日期。

队列分析经常用于分析客户终止与产品或服务关系的速率,这一概念通常被称为“流失”。对于基于订阅的企业来说,流失率代表在给定时间内取消订阅的客户百分比。

流失是一个重要的业务指标,可能对收入和增长产生重大影响。虽然高流失率可能是客户不满意的迹象,但低流失率则相反,可能是客户忠诚度和满意度的证明。

现在,让我们通过对一个希望分析其客户在 2023 年行为的基于订阅的应用程序进行流失分析,来展示队列分析的强大功能。

第一步——了解您的数据集

在这个示例中,我们将使用存储在 BigQuery 中的 subscriptions 表。它包含我们产品上的订阅列表,包括注册日期,最重要的是,关于其状态的信息,活跃取消。下面是表的样子:

查询结果(作者提供的图片)

现在你可能想按月查看流失演变。你可以使用以下查询,计算每月流失的客户数,并将其除以相同期间的总客户数:

查询结果(作者提供的图片)

查询结果绘制图(作者提供的图片)

好消息,流失率在这一年中有所下降。但这是否反映了实际情况?我担心它并没有。用这种方式查看流失率只能代表部分情况。这就是为什么需要引入 cohort。

第 2 步 — 数据转换

记住,cohorts 是由共同特征定义的组,在这种情况下,是它们的注册日期。所以我们将其拆分为不同的 cohort;1 月份注册的用户,2 月份注册的用户,依此类推。对于每个 cohort,我们希望知道有多少客户注册,以及在每个时间段内有多少人取消。换句话说,就是在一个月、两个月等之后有多少人取消。

为此,让我们使用以下查询:

查询结果,其中 signup_date = 2023 年 1 月(作者提供的图片)

这个查询将返回总计 78 行;1 月份 12 行,2 月份 11 行,3 月份 10 行,依此类推。以下是帮助你更好理解结果的视觉提示:

查询结果的示意图(作者提供的图片)

现在让我们拆解查询结果中的每一个字段:

  • signup_date:客户注册的日期。

  • cancellation_date:客户取消的日期。

  • cohort_monthsignup_datecancellation_date 之间的月份差。

  • max_subscriptions:该月注册的客户总数。

  • sum_cancellations:每月取消订阅的客户数。

  • r_sum_cancellations:随时间推移取消订阅的成员的累计总和。我们将在构建可视化时用到这个字段。

例如,看一下第 5 行,我们看到,在 67 个 1 月份注册的客户中,有 2 个在 5 月取消了订阅,即在加入服务后四个月,共有 10 个客户在 1 月到 5 月期间取消订阅。

第 3 步 — 在 Looker Studio 中整合所有内容

现在我们的数据集准备好了,让我们在 Looker Studio 中使用它来可视化 cohorts。

首先,创建一个新的计算字段,名为 churn_rate,使用以下公式:

SUM(r_sum_cancellations)/MAX(max_subscriptions)

然后,创建一个新的 Pivot Table 图表,按照以下标准:

  • 行维度signup_date

  • 列维度cohort_month

  • 指标churn_rate 以百分比表示

  • 排序行 #1signup_date 升序

  • 排序列 #1cohort_month 升序

为了为仪表板添加更多背景信息,我们将把一个包含总订阅数的表格添加到 cohort 图表的左侧。为此,创建一个新的 表格 图表,满足以下条件:

  • 维度signup_date

  • 指标max_subscriptions 使用 最大值 聚合

  • 排序signup_date 升序

添加一点格式设置,就完成了!

Medium — Cohort Analysis Looker Studio(作者提供的图片)

通过这种方式查看流失率,我们可以迅速得出关于用户参与度的结论。例如,2023 年 4 月的 cohort 表现优于所有其他 cohort。换句话说,流失率最低的组表明在 4 月加入的客户对产品更有承诺和参与感。通过分析这个 cohort 成功的原因,我们可以利用这些见解来提高未来的客户留存率和忠诚度。

结论

Cohort 分析对任何愿意监控客户行为和流失的订阅型业务至关重要。它提供了宝贵的见解,用于制定明智的营销和留存策略,从而带来更高的收入和客户满意度。按照本文中的步骤,你已准备好实施 cohort 分析并开始获得其好处。分析愉快!

资源

[1] Google Sheets,medium_cohorts_dataset_public (2023),Damien Azzopardi

[2] Looker Studio,Medium — Cohort Analysis Looker Studio (2023),Damien Azzopardi

[## 使用我的推荐链接加入 Medium。

你喜欢这篇文章吗?立即成为会员,发现关于任何话题的故事、思考和专业知识。

medium.com](https://medium.com/@damien.azzopardi/membership?source=post_page-----1cd18c0edd79--------------------------------)

数据驱动的客户获取完整指南

原文:towardsdatascience.com/a-complete-guide-to-data-driven-customer-acquisition-f0f1708b328?source=collection_archive---------2-----------------------#2023-01-18

Ivy Liu数据科学前沿 Ivy Liu

·

关注 发表在 数据科学前沿 ·6 分钟阅读·2023 年 1 月 18 日

--

在过去的十年中,我有幸与超过 100 位企业家合作。不论他们的地理位置、产品、财务状况或行业经验如何,他们都有一个共同点:客户获取让他们寝食难安。为了推动客户获取,公司不懈地努力建立其营销引擎。出色的营销带来流量,提高转化率,并降低成本。

作者提供的图片

优化顾客获取的一种方法是通过精准营销,公司只针对目标顾客进行投资。传统上,这些努力主要依赖于行业和营销经验。然而,形成既有行业知识又有营销经验的团队是具有挑战性的。正如你所想象的那样,行业专家专注于他们的市场和产品,通常并不是营销专家。另一方面,偶尔理解特定市场的营销人才既稀缺又昂贵。对于定义新产品类别的初创公司来说,情况更加困难。

这就是数据科学家帮助营销的地方。公司可以通过定期发起实验、实时监控表现,并根据市场反馈快速迭代,从而获得对其行业独特的营销经验。此外,数据科学比人工努力更好地处理高维度的营销数据,并提供更丰富的见解来支持商业决策。在本文中,我将讨论经过验证的低成本数据分析方法,这些方法能有效改善营销决策,并逐步介绍如何应用它们。

选择目标顾客

正如古话所说,做出选择比花费精力更具影响力。当公司资源有限时,这总是情况,过于分散的努力会导致在激烈的市场竞争中失去优势。投资于正确的顾客提供了更多成功的可能性。

如在重点关注最有价值的顾客中讨论的那样,顾客价值可以通过以下方式计算:

顾客的长期价值 = 顾客重复购买的收入 — 获取成本

例如,一家奢侈珠宝品牌的顾客不会频繁购买。在这种情况下,可以将顾客价值计算的时间框架设置为一年。基于其销售和营销归因系统,该品牌可以计算每位顾客(或未转化的访客)的年度收入和获取成本。通过这些数据,品牌可以迅速得出每位顾客的价值,并决定谁值得投入营销预算。

在大多数公司中,顾客价值分布图如下所示。大多数顾客对公司带来的价值较低或为负,因此应予以忽视或劝阻。下图中的左右分界通常遵循 80/20 法则。

作者提供的图片

确定对目标顾客有效的营销活动

商业的本质是一种实验,这意味着大多数商业活动注定不会产生积极的结果。在初创公司中,多达 95%的工作是无效的。公司必须迅速在噪声中找出有效的部分才能成功。通过实时监控市场营销表现,公司有最佳的机会立即区分出好的信号与坏的信号,并采取行动

图片由作者提供

根据客户价值分析,该珠宝品牌决定针对高价值群体推出营销活动。这些活动在 Google、Facebook、TikTok 等广告*台上进行。品牌根据假设实施所有策略——它认为客户喜欢某些东西,但不确定。只有当品牌收集并评估活动表现数据后,才能确定哪些活动值得投资,哪些不值得。

尽管上述图表的含义似乎很明显,但大多数公司却忽略了这些信号。因此,当公司开始关注时,通过简单地削减那些无效的市场营销举措的预算,它们将获得巨大的增长机会。

揭示市场营销举措为何有效与否

公司通过加倍投入那些有效的市场营销举措,在短期内获得超额回报。通过在新的举措中复制这些成功的经验,公司在竞争中拥有优势。

然而,当外部环境发生变化时,复制和粘贴以前的经验可能不会产生预期的回报。因此,公司需要揭示为什么以及为什么某项举措有效。这样,当市场和时间发生变化时,公司可以更好地确定哪些旧方法仍然适用,哪些不适用。

手动估算市场营销举措背后的原因可能很具挑战性。有太多维度需要分析:客户的 demographic 信息、接触点互动、访问行为、购买行为等等。

客户细分是一种有价值的技术,可以简化上述分析。通过客户细分提供的详细信息,公司可以识别出对每个客户群体最有吸引力的活动、购物体验和产品,并回顾它们为何具有吸引力。

图片由作者提供

这个珠宝品牌在情人节期间测试其活动策略,取得了初步成功。当下一个重要节日母亲节临*时,品牌希望了解情人节的经验是否适用。因此,它进行客户细分分析,以了解哪些因素促成了情人节的成功。从分析中,品牌发现情人节的目标客户只有一部分与母亲节的目标客户重叠。它可能会在母亲节促销中部署针对这部分客户的有效活动策略。

测试和学习

通过实验发现有效的方法后,企业可以在新客户获取机会出现时,应用成功的方案。来自客户细分的丰富信息帮助公司在业务迭代中领先一步。

在大规模实施之前,公司必须通过对样本进行测试和学习,验证经验是否适用于新的机会。常见的测试和学习方法包括 A/B 测试和可视化。

图片由作者提供

在母亲节的大促销之前,珠宝品牌基于情人节的所有经验启动了一次小规模的活动。大多数经验仍然有效,但创意和信息传达可以做一些调整。品牌修改了其活动并在更大范围内进行测试。它在每天的几个周期内重复这个过程,直到获得满意的结果并全面推广。

个性化

与常识相反,对于大多数企业来说,个性化是一种一对多的营销机制。一旦公司找出如何通过营销渠道、活动、创意、信息传达等吸引每个客户细分,他们就准备在客户获取计划中推广个性化。例如,公司可以为每个客户细分预定义其网站、浏览体验和产品推荐。当一个符合特定细分的访问者进入网站时,公司可以自动为他们提供合适的购物体验。

图片由作者提供

在进行了一年的不同促销活动后,这个珠宝品牌收集了足够的客户数据。品牌了解每个客户群体的购物偏好。因此,品牌设计了五种不同的客户体验,以适应最多的目标客户。提供了理想的购物体验后,目标客户转化更快,购买量比之前增加。

背后的数据科学

两组数据科学技术对于实现上述分析至关重要。第一组包括能够连接点滴的数据工程技术。常听说数据管道基础设施是数据科学项目最大的障碍。得益于数据行业的发展,越来越多的连接工具简化了从多个来源获取数据的过程。

第二个关键因素是身份技术。由于 iOS 隐私政策的变化,使用唯一用户标识符来关联用户旅程中的不同接触点变得越来越具有挑战性。幸运的是,独立于个人标识符的营销技术,如营销混合建模,提供了另一种选择。

我在文章中讨论了如何利用数据科学提升业务和优化营销。如果你想讨论客户获取或其他数据科学主题,请在LinkedIn关注我或通过 newsletter@ivyliu.io 联系我。下次见。

数据分析家庭实验室启动完整指南

原文:towardsdatascience.com/a-complete-guidebook-on-starting-your-own-homelab-for-data-analysis-552c9f532ff0

照片由 imgix 提供,来源于 Unsplash

现在是启动你自己的数据科学家庭实验室的最佳时机,用于分析对你有用的数据、存储重要信息或开发自己的技术技能。

Will KeefeTowards Data Science Will Keefe

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

--

我在 Reddit 上的几个技术相关子论坛中看到过一句话,大意是“支付云服务费用就像是在租用别人的计算机。”虽然我认为云计算和存储非常有用,但这篇文章将重点讲述我为什么将我的分析、数据存储和工具从在线服务提供商那里转移到我的家庭办公室。我使用的工具和硬件的链接也可以查看。

介绍

解释我这疯狂方法的最佳方式是分享一个我遇到的商业问题。虽然我是一个风险承受能力较低的传统投资者,但我心中有一个小小的希望,也许,只有也许,我能成为那不到 1%击败标普 500 的人。请注意,我使用了“希望”这个词,因此,也不要在这种希望上投入太多。每年我会给我的 Robinhood 账户 100 美元,并像对待彩票一样对待它——希望能一夜暴富。不过,我要向大家保证,这个账户与我的其他主要账户是分开的,后者主要投资于指数基金,获得稳定的回报,并且我会在这些基金上进行定期的卖出看涨期权。我的 Robinhood 账户则有些类似于赌博,什么都可能发生。不过,我给自己设定了几个规则:

  1. 我从不使用任何保证金。

  2. 我从不卖出裸仓,只进行买入开仓操作。

  3. 我不会将钱投入到追逐亏损交易上。

你可能会想知道我接下来要说什么,我会从我的岔题中回到正题,分享我的“彩票”虽然尚未为我赢得一个 Jeff-Bezos 水*的游艇,但确实教会了我不少关于风险和损失的知识。这些教训也激发了我内心的数据爱好者,尝试改进我量化风险和预测市场趋势及事件的方式。即使在短期内方向性正确的模型也能为投资者提供巨大的价值——无论是散户还是对冲基金。

我看到改善决策的第一步是需要有数据来做数据驱动的决策。剔除投资中的情感是一个著名的成功秘诀。虽然历史数据在股票和 ETF 中广泛可用,并通过诸如 yfinance(我下面的一个例子)等资源开放源代码,但衍生的历史数据集则要昂贵得多,获取也更困难。一些初步了解的 API 提示,常规的数据访问以回测我的投资组合策略可能会花费我数百美元每年,甚至根据我所寻求的粒度可能每月都要付费。

[## 使用 Python 和 Plotly 绘制金融数据及多个叠加趋势

Plotly 为 Python 开发者提供了在使用蜡烛图和其他各种绘图工具建模时的灵活性……

wire.insiderfinance.io](https://wire.insiderfinance.io/graphing-financial-data-and-multiple-overlaid-trends-using-python-and-plotly-f9d948934ecb?source=post_page-----552c9f532ff0--------------------------------)

我决定在这个过程中投资于自己,而不是按我的条件花费数百美元。观众发出呻吟

构建在云端

我对数据抓取和仓储的初步思考使我回到了我在日常工作中使用的相同工具。我创建了一个个人 AWS 账户,并编写了 Python 脚本来部署在 Lambda 上,以在预定的时间间隔抓取免费的实时期权数据集,并代表我写入数据。这是一个完全自动化的系统,并且几乎可以无限扩展,因为会为我的投资组合中的每个股票动态启动不同的抓取器。写入数据更具挑战性,我夹在两条路之间。我可以选择将数据写入 S3,通过 Glue 爬取数据,然后用 Athena 进行无服务器查询,或者使用关系数据库服务,将数据直接从 Lambda 写入 RDS。

快速总结提到的 AWS 工具:

Lambda 是一种无服务器计算,允许用户执行脚本而不需要太多的开销,并且提供非常慷慨的免费层。

S3,也称为简单存储服务,是一个具有相当大免费额度的对象存储系统,每 GB 每月费用为 0.02 美元。

Glue 是一个 AWS 的数据准备、集成和 ETL 工具,具有可用于读取和解释表格数据的网页爬虫。

Athena 是一种无服务器查询架构。

我最终倾向于使用 RDS,仅仅是为了让数据可以轻松查询和监控,如果没有其他原因的话。他们还提供了 750 小时的免费额度和 20GB 的存储,这为我提供了一个很好的沙盒环境,让我可以动手操作。

然而,我没意识到股票期权数据有多庞大。我开始每 15 分钟写入大约 100 MB 的数据,每个股票代码每月都这样,这可能听起来不多,但考虑到我有一个包含 20 个股票代码的投资组合,在年底之前我就会用完所有免费额度。除此之外,免费层中的小计算能力很快就被消耗殆尽,我的服务器在不知不觉中消耗了所有 750 小时(考虑到我想跟踪期权交易,大约每天 8 小时,每周 5 天)。我还经常在白天工作后阅读和分析数据,这也导致了更高的使用量。大约两个月后,我用完了免费额度,收到了第一张 AWS 账单:每月约 60 美元。请记住,一旦免费额度结束,你将为每个服务器小时的处理、从 AWS 生态系统到本地开发机器的每 GB 数据量以及存储费用按 GB/月付费。我预计在一两个月内,我的拥有成本可能增加至少 50%甚至更多,并将继续增加。

哎呀。

离开云计算

到了这个时候,我意识到我宁愿把每月花在租用 Amazon 设备上的 60 美元花在电费上,把剩下的钱投入到我的 Robinhood 账户中,回到我们开始的地方。尽管我很喜欢使用 AWS 工具,但当我的雇主不支付账单时(对我的同事们,我保证我在工作中也是很节俭的),我真的对投资它们不太感兴趣。AWS 的定价对于爱好者来说并不合适。他们提供了很多很棒的免费资源来帮助新手学习,也为专业人士提供了极高的性价比,但对于现在这个中间层级却不适合。

我有一台旧的联想 Y50–70 笔记本电脑,屏幕坏了,我打算将其改装为家庭网页抓取机器人和 SQL 服务器。尽管它们作为新机或认证翻新的价格仍然相当可观(可能由于 i7 处理器和独立显卡),但我坏掉的屏幕几乎完全抵消了电脑的价值,因此将其作为服务器重新启用,使其焕发了新生,也让它积攒了约三年的灰尘。我把它放在客厅角落的一个音响上(旁边有一个侏儒),与 PlayStation 相对,并将其设置为“常开”以完成其新任务。我的女朋友甚至说,电脑键盘刺眼的红色背光“让房间看起来更整洁”,这也算是一种赞美。

画面中是一个侏儒,但拍摄时,服务器尚未配置。

幸运的是,我的 65 英寸《使命召唤》认证电视距离笔记本电脑的 HDMI 电缆足够*,也能看到我写的代码。

我将服务器从云端迁移到我的破旧笔记本电脑,开启了新的征程!现在我可以以仅仅电费为代价进行所有分析,约为 $0.14/kWh,或大约 $0.20–0.30 一天。接下来的一个月或两个月里,我在本地进行了一些实验和调整。通常,这包括每周工作后花几小时打开我的 MacBook,玩弄来自侏儒音响服务器的数据上的 ML 模型,利用本地Plotly 仪表板可视化数据,然后指导我的 Robinhood 投资。

我取得了一些有限的成功。详细情况我会留到另一篇 Medium 文章中分享,一旦有更多的数据和性能指标。不过,我决定从破旧笔记本电脑扩展到自己的微型云。这次,不是租用,而是拥有。

构建家庭实验室

“家庭实验室”这个名字听起来很复杂和酷炫 推了推眼镜,但实际上在拆解后相对简单。基本上,我在使用破旧笔记本电脑时遇到了一些挑战,这些挑战提供了动力,同时也有一些新目标和理想需求给予了灵感。

损坏的笔记本电脑问题:

硬盘很旧,至少有 5 或 6 年了,这给未来数据丢失带来了风险。在承受较大查询压力时,它的速度也显著下降,这是该型号的一个已知问题

使用我的 TV 和蓝牙键盘来操作安装了 Windows 10 Home 的笔记本电脑非常不便,且不符合人体工程学。

笔记本电脑无法升级,如果我想添加更多的 RAM 超过已安装的容量。

技术在任务并行化方面有限。

单靠笔记本电脑不足以托管我的 SQL 服务器,以及为我的 ML 模型处理仪表板和数据。也不希望在同一台计算机上共享资源,以免影响其他服务。

我会实施的系统必须解决这些问题,但我也希望实现一些新功能。

计划的新功能:

一个新的家庭办公室设置,使在家工作时更加舒适。

整个公寓的以太网布线(如果我支付整个千兆网,我就要使用整个千兆网 AT&T)。

在适当的情况下使用微服务器进行分布式计算。

服务器可以升级和更换。

各种程序和软件可以独立部署,以实现不同的子目标,而不会妨碍当前或并行程序。

我选择的计算机的分布式计算是一个有争议的话题,稍后将在文章中解释。

我花了很多时间进行适当的硬件配置研究。我读过的一个最喜欢的资源是《TinyMiniMicro》,它比较了 Lenovo ThinkCentre Tiny *台、HP ProDesk/EliteDesk Mini *台和 Dell OptiPlex Micro *台。我也曾使用过单板计算机,就像 Project TMM 的作者一样,我有两个 Raspberry Pis 和一个 Odroid XU4

我喜欢我的 Pis 的地方:

它们体积小,功耗低,新型号有 8GB 的 RAM。

我喜欢我的 Odroid XU4 的地方:

它小巧,拥有 8 个核心,是一个很好的模拟*台。

虽然我相信我的 SBC 仍会在我的家庭实验室中找到用武之地,但请记住,我需要处理我想要托管的服务的设备。我最终还购买了可能是我一生中最贵的 Amazon 订单,并彻底重新装修了我的整个办公室。我的购物车包括:

  • 多条 Cat6 以太网电缆

  • RJ45 压接工具

  • 绑扎带

  • 2 台 EliteDesk 800 G1 i5 Minis(但发了一台 G2 #Win)

  • 1 台 EliteDesk 800 G4 i7 Mini(还发了一款更好的 i7 处理器 #Win)

  • 2 台 ProDesk 600 G3 i5 Minis(并发了一台稍差的 i5 #Karma)

  • 额外的 RAM

  • 多个 SSD

  • 替换我的书架/桌子的一个新办公室桌

  • 新的办公室照明

  • 硬盘克隆设备

  • 两个 8 端口网络交换机

  • 不间断电源

  • 一个打印机

  • 一个机械键盘(相关的,我还有五个键盘和鼠标组合,如果有人需要的话)

  • 两个新显示器

如果你想查看我完整的零件列表,包括每个项目的链接以供检查或购买,请随时 前往我的网站查看完整列表

一旦我的“夏季圣诞礼物”到达,门口堆满了许多箱子,真正的乐趣就可以开始了。第一步是完成全屋以太网布线。安装人员默认没有将任何以太网电缆连接到有线电视盒上,所以我不得不剪掉电缆末端并自己安装插座。幸运的是,我购买的棒极了的工具包(链接在我的 网站)中包括了压接工具、RJ45 插头和测试设备,以确保我正确布线并确定我公寓中的哪个端口与哪个电缆对应。当然,按我的运气,最后一根电缆正好是我办公室需要的,但我想未来的租户会从我今天的好 deed 中受益。整个过程花了大约 2-3 小时布线千兆连接,幸运的是,我的女朋友很喜欢帮忙,一杯酒让过程变得更快。

在有线网络设置之后,我开始通过组装家具、安装照明和拆卸硬件来布置我的办公室。我的桌面设置非常整洁,我对现在的办公室效果很满意。

前后对比

关于我的硬件设置,我购买的每台计算机都配备了 16GB 的内存,我将其升级到 32GB,同时还有固态硬盘(其中一些我也进行了升级)。由于每台设备都运行 Windows 10 Pro,我能够在我的网络中进行远程登录,并且我已经设置了一些服务。联网这些设备也很有趣,尽管我觉得我的电缆管理还有改进的空间。

家庭实验室节点前面

家庭实验室节点后面

现在,根据我一开始提到的星号,为什么我花费了相当于一年的 AWS 成本来购买五台总共大约 22 核心的计算机,而不是直接购买/构建一台高级现代 PC 呢?原因有几个,我相信这可能会在某些技术爱好者中引发争议。

  1. 扩展性 — 我可以轻松地向我的集群中添加另一个节点,或移除一个进行维护/升级。

  2. 成本 — 升级和维护非常简单且便宜。此外,对于大多数单元,功耗约为 35W,运行我的服务器的成本非常实惠。

  3. 冗余 — 如果一个节点出现故障(例如,CPU 故障),我有纠正脚本来*衡我的分布式工作负载。

  4. 教育 — 我正在学习大量内容,这些内容提升了我的专业技能和经验,教育是✨宝贵的✨。

  5. 看起来很酷。这里的第 5 点足以作为理由。

说到教育,这里是我在集群中学到并实施的一些东西:

  • 在将驱动器从小容量克隆到大容量时,你需要扩展新驱动器的卷,这通常需要第三方软件来轻松完成(如 Paragon)。

  • 你需要手动分配静态 IP 才能在桌面之间远程访问时获得可靠的结果。

  • 迁移 SQL 服务器时,从备份恢复比在两个不同服务器之间查询要容易。

我相信在这个过程中我还会学到很多东西……

以下是我现在家庭网络的一个大致示意图。图中没有展示我的 wifi 设备,比如我的 MacBook 和手机,它们会在两个路由器之间跳跃。最终,我还会将我的单板计算机和可能的另一台 PC 加入到集群中。哦,对了,我那台破屏的旧笔记本电脑?没有人愿意在 Facebook Marketplace 上以 $50 的价格购买它,所以我在上面安装了 Windows 10 Pro 以便远程访问,并且也将它添加到集群中,这实际上可能是个好事,因为我可以利用它的 GPU 来辅助构建 Tensorflow 模型(也可以玩一些回合制游戏)。

家庭实验室网络图

说到 Tensorflow,这里是我将在新家庭实验室中实现的一些服务和功能:

  • SQL 服务器(目前托管我的财务数据集以及我正在网络抓取的新数据集,后续将撰写关于我母校的财务状况和我居住城市的公共安全数据集)

  • Docker(用于托管我将要构建的应用程序/容器以及 Minecraft 服务器,因为,为什么不呢)

  • Jenkins CI/CD 系统,用于在我的数据集上构建、训练和部署机器学习模型

  • 个人代码库的 Git 仓库

  • 网络附加存储,支持我摄影爱好中的大量照片、文档以及其他数据收集活动

  • 以及其他待定的项目/服务

结束语:

值得吗?嗯,确实有“只有时间才能证明”的因素。一旦我的信用卡从亚马逊的采购中恢复过来,我相信它也会享受 AWS 定价的缓解。我也期待能够构建和部署更多的兴趣爱好,并收集更多数据来撰写更多的 Medium 文章。我计划的下一些文章包括分析西弗吉尼亚大学目前面临的财政债务问题,以及对纳什维尔公共安全报告的探索性数据分析(可能还会有一个预测紧急事件和分配资源需求的机器学习模型)。这些数据科学项目规模足够大,如果没有某种存储和查询大量相关数据的架构,将无法实现。

你怎么看?离开云服务,建立一个家庭实验室的项目听起来是否是你想做的?你的硬件选择会是什么?

如果你对我使用的硬件感兴趣,可以查看我在 www.willkeefe.com 上的评论。

我已经发布了这篇文章的续集,扩展了使用案例!在 Better Programming 上查看!

medium.com/better-programming/building-python-distributed-machine-learning-models-for-derivatives-on-homelab-cluster-dd9cb1d97e23

我最*在 Medium 上的相关内容:

## Python 中的生产计划和资源管理系统

高效的供应链、生产计划和资源分配管理比以往任何时候都更重要。Python…

towardsdatascience.com ## 使用 Python 和机器学习进行犯罪地点分析与预测

利用 Python、Folium 和 ScyPy,可以建立模型来展示犯罪事件,计算最佳位置…

towardsdatascience.com

在 LinkedIn 上也可以与我联系!

www.linkedin.com/in/will-keefe-476016127/

推荐系统离线评估的完整教程

原文:towardsdatascience.com/a-complete-tutorial-on-off-policy-evaluation-for-recommender-systems-e92085018afe

如何缩小离线与在线评估之间的差距

Adrien BiarnesTowards Data Science Adrien Biarnes

·发表于 Towards Data Science ·18 分钟阅读·2023 年 3 月 11 日

--

推荐系统跨越离线与在线评估差距(AI 生成的图像)

介绍

在本教程中,我将解释为什么应该关注离线评估。接着我会介绍适量的理论。我不会深入探讨最新的方法,而是停留在经验上效果良好的方法。这对于接下来的实际实施部分将足够。

评估推荐系统

假设你有一个提高推荐系统性能的想法。假设你获得了关于项目的新元数据,例如项目的主要主题,这应该有助于更准确地预测用户在*台上的参与度。那么你如何利用这个新特性呢?在对新数据进行全面探索后,你需要对数据管道进行必要的修改,以转换特性并将其与其他特性一起输入模型。现在你可以启动学习阶段以生成新版本的模型。

太好了!那么,你如何评估新模型的表现?你如何确定新特性是否有助于推荐,并能驱动*台上的更多参与?

好的,你可以直接将新版本与旧版本一起投入生产,将一部分流量重定向到新模型,并使用你为业务定义的最重要的指标(点击率、观看时间、深入时间、订阅等)比较两者的表现。这确实被称为 AB 测试。

作者提供的图像

但如果新版本糟糕,导致收入大幅下降或严重恶化服务质量,那可能会非常糟糕!

通常,作为机器学习从业者,你首先需要验证新版本是否比旧版本更好,采用离线方式进行,即无需向真实用户展示任何内容(我感觉对你们中的大多数人来说这很简单——但请耐心,我马上就会讲到)。

避免收入下降非常重要,但这并不是唯一的关注点。进行在线 AB 测试需要时间,尤其是在流量不大的情况下。在这种情况下,可能需要几周才能得出具有足够统计信心的结论。此外,在这种情况下,你可能需要将模型重写成不同的堆栈,以便它可以扩展并满足延迟要求。这可能需要额外的几周(甚至几个月)的工作。因此,拥有一个强大的离线评估策略可以更快地迭代不同的想法或超参数进行调整。可以在离线评估中评估数千个想法,并仅选择最有前景的候选者进行在线 AB 测试。

使用日志数据 D_0 离线构建多个候选策略

但是在离线环境中,我们无法直接测量点击率或观看时间,因为我们事先不知道未来的用户是否会点击或他们将花多长时间消费推荐的项目。我们在操作一个反事实场景。也就是说,我们无法观察到我们没有采取的行动的奖励,只能看到已采取行动的奖励。

在在线环境中,这一主要评估缺陷不再是问题。我们根据用户在*台上的实际行为收集反馈。相反,离线评估始终会严重偏向于测量过去的行为,并且在评估实际会发生什么方面有限。

那么我们是否要接受这一事实而不加以改变,还是可以对此做些什么呢?

什么是离线策略评估,为什么你应该关心?

让我们首先介绍一些在阅读关于离线策略评估时会遇到的重要词汇。当用户进入你的*台时,会发生以下情况:

图片由作者提供

离线策略评估是指使用从不同策略收集的数据来评估新策略。我们希望提出一个估计器V_hat,该估计器将基于日志数据D_0估计新推荐器π_e的性能,而这些日志数据D_0是用于训练旧策略π_0的:

直接方法

正如我之前所说,在评估我们的推荐系统时,我们在一个反事实环境中操作。我们不知道我们没有采取的行动会产生什么结果。因此,基本上我们有一个缺失标签的问题。

作者提供的图像 — 新推荐项目的缺失标签问题

在上图中,对于第一次推荐,我们成功推荐了记录数据中看到的点击项,因此我们可以说我们有一个积极的预测,而其他项是负面的。但对于其余的评估数据集,我们推荐了完全不同的项目,因此我们不知道结果会是什么。

鉴于我们已经记录了交互数据D_0, 一个简单的想法是,许多机器学习从业者可能会想到,利用这些数据来生成一个机器学习模型,该模型将直接估计缺失的奖励(例如,用我们的最佳猜测替换上面的问号)。然后我们可以像往常一样进行评估。更正式地说,我们希望生成一个奖励模型r_hat,该模型将给出在给定上下文和特定行动时奖励的期望值:

许多机器学习模型可以用于这个任务。这虽然很好,但实际上效果并不理想!让我们来看看原因……

似乎记录的数据和我们想要预测的数据不来自相同的分布。存在较大的协变量偏移。我们记录了某些特定行动的数据,而尝试预测其他(可能完全不同)行动的奖励。假设我们利用了 2 个特征来尽量减少奖励模型的遗憾。我们可以如下绘制奖励空间:

作者提供的图像 — 奖励空间的可视化

绿色点是正观察(用户感兴趣的观察),红色点是负观察。发生的情况是,奖励空间大部分是空的。存在广阔的区域我们没有任何观察数据。但我们的新政策可能需要在这些未知空间中做出决策。这意味着奖励模型如何外推非常重要。决策边界在上述图中的位置也非常重要。而这不会在奖励模型尝试最小化的遗憾函数中反映出来。因此,外推出现错误的可能性非常高。最终,这种方法表现出低方差但可能非常偏倚。

因此,使用模型来预测奖励似乎不是一个有效的方法。我们还能做什么?这时模型无关的方法就派上用场了。

逆倾向评分(IPS)

这个估计器的目标是重新加权奖励,以便根据旧政策和新政策采取行动的倾向来赋予行动更多或更少的重要性。 我们希望降低旧政策比新政策更多采取的行动的重要性(反之亦然)。

让我们可视化一下这个概念。下面是旧政策π_0在给定上下文的情况下采取行动的概率空间

作者提供的图像

红色和绿色的点仍然是观察到的奖励。暗区是条件动作概率质量的集中区域。

现在,新策略π_e将在空间的不同部分采取行动,如下所示:

图片由作者提供

IPS 估计器的目标是通过应用以下公式来重新加权奖励:

两个动作概率之间的比率称为重要性权重。结果如下:

图片由作者提供

可以证明,这种方法是无偏的,意味着我们收集的数据越多,IPS 估计器将越趋*于在线奖励的真实值。

如果你还是很难理解这个概念,让我们进一步简化。假设我们只有 2 个推荐的项目。我们已经收集了这些项目的用户互动数据(如评估数据集中所见):

图片由作者提供

当然,如果你的新策略π_e总是预测项目 1,那么你的推荐系统的整体精度将高于一个总是预测项目 2 的策略。或者会这样吗?现在让我们将显示数据加入其中:

图片由作者提供

现在,从这些角度来看,你肯定能理解离线评估的附加价值。考虑到显示数据,我们看到旧策略π_0(生成了上述日志)高度偏爱项目 1。而这可能不是最佳推荐。偏爱项目 2 似乎是一个更好的选择,但为了实现这一点,我们需要在评估过程中考虑显示数据(例如,策略推荐项目的倾向)。

但这种方法可能会表现出很强的方差,因为某些奖励估计可能完全偏离。这是因为我们在计算两个动作概率的比率,而这些比率有时可能非常小。例如,如果给定观察的旧策略的动作概率非常小,则重要性权重可能变得很大,使得这个特定观察的权重过高。

裁剪反向倾向评分(CIPS)

这个估计器是经典 IPS 的一个变体,试图解决我们刚刚提到的挑战。它通过裁剪重要性权重来实现这一点,以使其不会超过最大值:

问题在于,这种修改破坏了 IPS 估计器的无偏性,但更重要的是,它引入了一个可能非常不实用的超参数。

自归一化反向倾向评分(SNIPS)

这一次,我们通过重要性权重的经验均值的倒数来重新加权普通的 IPS。这只是简单地调整了普通 IPS 的结果。正如我所说,IPS 的问题在于有时我们可能会因为一个微小的旧策略动作概率而获得巨大的重要性权重。SNIPS 估计器通过将这种巨大的重要性权重“取消”来解决这个问题,因为它也会包含在分母中。

最终,估计器在经验上表现非常好,并且没有超参数需要调整,这使得它对大多数机器学习从业者非常有吸引力。

如何实现重新加权的离线策略评估方法?

在研究相关文献时,我们可以看到,大多数机器学习研究者的努力都集中在寻找最佳方法,以获得一个无偏差且方差低的在线奖励离线策略估计器。但几乎没有人讨论实现这些策略的实际问题。更糟糕的是,大多数研究者使用模拟数据来展示他们方法的稳健性。这在理论上很棒,但在实际应用中,实施方法时会遇到困难。

我们已经看到,自我标准化逆倾向评分(SNIPS)估计器在经验上表现良好,没有需要调整的超参数,方差低且一致(尽管有偏)。

那么我们准备好实现这个估计器了吗?嗯,还没有完全准备好。

离线策略评估是否有效很大程度上取决于我们能多准确地计算动作概率π(a_i|x_i)。这实际上是最重要的部分

为了做到这一点,我们首先要花时间正确地定义这些概率。让我们首先思考概率π在给定上下文x_i的情况下“选择”动作a_i的定义。

人们必须记住,我们的目标是重新加权奖励,以便在新策略π_e 很不可能采取这些动作时,减少对由旧日志策略π_0 收集的动作的重视(反之亦然)。

那么,什么是动作a_i?在这里,区分策略输出推荐项的概率和项被显示给用户的概率是很重要的。这些概率差异很大。确实,一个算法可以输出 1000 个项目,但由于 UI 限制(我们不能在移动设备上显示 1000 个项目),只有一小部分会显示给用户。正如我刚才所说,我们关注的是所采取的动作将最终出现在日志数据中的概率。因此,我们关心的是显示概率,而不是推荐系统的输出概率。这一点非常重要,因为它改变了如果一个人想要估计动作概率(例如,推断显示而不是输出)时需要考虑的数据源。

现在,x_i的背景是什么?实际上,它可以是任何东西。作为建模者,你负责定义它。从理论上讲,背景越相关和精确,也就是说,我们拥有的信息越相关,我们应该能够更准确地估计展示概率。但在实践中,这也使得任务更加困难。特征空间越大,你需要的数据就越多以产生稳健的估计。而拥有稳健的估计至关重要。如果你的展示概率完全错误,你将会给特定奖励过多或过少的重视,这完全违背了离线评估的目的。正如我将简要说明的那样,一个好的做法是简化背景,仅使用最重要的特征。

展示概率估计

首先,一个失败的方法

当我第一次接触离线评估时,我需要了解我的前任之一实施的方法。这是一种高级方法,其中展示概率由深度学习模型直接估计。澄清一下,我们在这里没有使用直接方法。模型没有用于直接估计未知的奖励。它用于估计旧政策和新政策的展示概率π(a|x)。凭借这些概率,我们可以计算重要性权重并应用如 IPS、CIPS 或 SNIPS 等估计器。

不深入细节,我们可以用一张图片来简单解释。这里做的是从一个从非常广角看起来像这样的模型:

作者提供的图像 — 深度推荐系统架构的广泛概述

变成这样:

作者提供的图像 — 修改后的架构以预测展示概率

理论上,这非常整洁,因为我们可以在给定非常精确的输入集合时获得概率。我们可以在非常细粒度的层面上估计展示概率。这是谷歌 YouTube 团队的这篇论文中找到的一个想法的实现。

但在实践中,它失败了,原因如下:

  • 我们现在有了两个目标而不是一个(用户展示作为推荐的序列的下一个视频)。这引入了额外的问题,这些问题没有得到妥善管理,比如:如果我们只有序列中的下一个视频但没有相关的展示(只有一个目标)怎么办?确实,在训练数据中,我们使用的是来自其他来源(外部)的序列视频,而不是我们没有展示任何推荐的*台。

  • 拥有两个目标意味着有两个不同的损失函数。你如何将它们结合起来?如果其中一个损失在完全不同的值范围内(这就是发生的情况),那么在你将它们结合时,它可能会变得不重要(这会导致其中一个分支没有学习到任何东西)。

  • 整体代码库变得复杂得多。

  • 理论上,引入辅助任务对主要任务是有利的,但需要它们之间良好的对齐。在这种情况下,增加的复杂性使得新的显示概率任务使主要任务的性能下降。

  • 输出的概率并不是实际的概率。实际上,模型要么过于自信,要么完全没有自信。这是深度学习模型中的一种常见情况,因为这些模型倾向于过拟合而表现出过度自信。

这就是显示概率的内核密度图(使用虚假数据模拟的)会是什么样的。

作者提供的图片 — 估计的显示概率分布

在这里需要注意的是,仅仅因为你在神经网络的最后激活函数中使用了 sigmoïd 或 softmax,并不意味着你会得到正确的概率。网络的输出确实会位于[0,1]的范围内,但概率很可能不会得到良好的校准。当一个良好校准的模型对一个给定的观察值输出 0.6 时,我们在长期内应找到 60%的目标。这里有一篇维基百科文章讨论这个问题。在离策略重要性权重计算的背景下,使用良好校准的概率非常重要,因为我们使用的是两个显示概率的比率。当模型输出 0.4 的概率时,应该意味着目标被找到的次数是模型输出 0.2 概率的两倍。0.2 和 0.4 的比率应具有与 0.4 和 0.8 之间的比率相同的重要性。

更简单、更准确的方法?

鉴于我第一次的经历,并且考虑到它并不成功,我的问题是:我们真的需要使用机器学习模型来估计显示概率吗?为什么不直接使用现有的数据进行估计呢?

如果你想进行离策略评估,你需要记录显示的项目。因此,你应该在你的数据库中拥有这些信息(无论是什么)。如果你的日志系统经过充分考虑,你也会在日志中拥有上下文。

在使用日志数据进行展示概率的直接估计时,我们需要简化问题。上下文越简单,你将能够生成更稳健的估计。例如,如果你正在创建一个推荐系统,试图在特定项目后推荐一组项目,那么最重要的特征就是输入项。你的目标将是估计当特定项目(我们称之为i2)被消费(上下文x_i)时,某个项目(我们称之为i1)被展示(动作a_i)的概率。

为了估计这个概率,你有两种选择。你可以选择:

  • 频率估计,即i1i2被消费时被展示的次数除以i2被消费的总次数。

  • 贝叶斯估计:当试验次数(在此情况下为展示次数)过少时,频率估计可能不可靠。当我们在 2 次试验中有 2 次展示时,将展示概率声明为 1.0 似乎不是一个稳健的估计。我们将在最后的专门部分解释其工作原理。

因为我们使用的是由旧策略π_0产生的日志数据来计算这些估计,所以实际上我们是在计算旧策略的展示概率。换句话说,我们是在回答这样一个问题:旧策略在这种特定情况下展示这个特定项目的可能性有多大?

现在,我们如何估计新策略π_e的展示概率,考虑到该策略从未投入生产,我们也没有收集过与之相关的展示日志?

实际上,我们确实拥有用于生成策略π_e的训练数据,因此我们有上下文(在此情况下为每次观察的输入项),并且我们可以记录π_e在训练数据上的预测。所以我们拥有了策略的输入和输出。我们唯一缺少的就是策略的输出是否会展示给用户以及展示的排名。因此,这里有一个技巧。我们知道推荐系统输出顶部的项目更有可能展示给用户。我们甚至可以确定项目在特定排名下被展示的总体概率。例如,它可能看起来像这样:

因此,技巧在于将一个项目(由策略输出)展示给用户视为具有概率 p 的伯努利试验。然后我们可以根据 p 进行样本展示结果。

最终,我们将拥有所有需要的东西,以相同的方式精确计算新策略π_e的展示概率,就像我们对π_0所做的那样(无论是频率方法还是贝叶斯方法)。

拥有展示概率后,我们可以计算重要性权重,并使用 SNIPS 估计器进行离策略评估。

我采用了这种方法,并且效果非常好。事实上,没有离线策略评估时,评估流程的主要指标下降了 10%,但有了离线策略评估,它转变为 2%的增长。于是决定进行一次 AB 测试,结果确实是积极的。

关于展示概率的贝叶斯估计的附注

如上所述,展示概率可以通过贝叶斯方法来估计,以获得更稳健的结果。

在频率主义范式中,我们想要估计的参数有一个真实唯一的值,我们通过不断增加实验次数来逐步接*它。它是一个固定的量,与其无关的是概率。相反,在贝叶斯视角下,感兴趣的参数没有真实的唯一值,而是通过概率分布来描述。

在我们的情况下,频率主义方法有什么问题?

我们感兴趣的参数是基于输入项的展示概率。因此,我们正在计算两个计数变量的比率。问题在于,在某些情况下,比率的分子过低。当展示次数非常少时,我们的估计不确定性非常高。估计器的误差方差非常大。

我们如何解决这个问题?

计数变量遵循二项分布。展示次数遵循参数 p(二项展示的概率)的二项分布。有 p 的概率获得展示,1-p 的概率获得不展示事件。两个计数之间的比率遵循一个参数为 alpha =(成功次数+1)和 beta =(失败次数+1)的贝塔分布。

我们要计算什么?

在贝叶斯范式中,我们定义了一个关于参数分布的先验信念 P(Θ)。我们还定义了一个似然函数 P(X|Θ),它告诉我们在给定对参数分布的信念的情况下观察到数据的可能性。所有的工作都基于贝叶斯公式:

贝叶斯定理

后验分布 P(Θ|X)等于似然函数 P(X|Θ)乘以前验 P(Θ),再除以证据 P(X),即观察数据的概率。现在计算观察数据的概率通常是不可处理的。如果我们确实需要获得精确的概率估计以及可信区间等,我们需要依赖先进的贝叶斯推断方法,如马尔可夫链蒙特卡洛 - MCMC(这就涉及到完全不同的领域)。

现在在我们的情况下,优点是先验遵循一个知名分布,似乎似然也遵循一个知名分布。似然模型描述了在给定试验(显示)中观察到成功(点击)的概率,因此它也遵循二项分布。感兴趣的参数是两个计数变量的比率,这两个变量都遵循二项分布,因此先验遵循 beta 分布。而二项分布和 beta 分布的优点是它们是共轭先验。这意味着我们有一个经过验证的理论结果,可以直接给出后验分布的形式,而无需计算证据。在这种情况下,后验分布也遵循 beta 分布,参数为 alpha_posterior = (alpha_prior + nb_success)和 beta_posterior = (beta_prior + nb_failures)。

现在我们知道后验分布遵循 beta 分布,应用 beta 分布的期望值公式,我们可以直接计算显示概率,该概率只是 alpha_posterior 和 beta_posterior 的比率。有关更多信息,请参见维基百科

此外,选择贝叶斯方法计算显示概率的额外好处是我们知道后验的确切分布。除了使用期望值进行排序,我们还可以通过对后验分布进行抽样来执行随机排序程序,以获得排序分数。这将有利于探索,但会牺牲开发,但可能会导致更为多样化的推荐。

结论

总之,离线策略评估是一种非常有效的方法,用于减少离线与在线之间的差距,并避免当离线结果是积极的(或相反)时在在线测试中获得包容性结果的失望。

当你开始进行推荐工作时,设置离线策略评估策略可能不是绝对最重要的优先事项,但一旦你开始有了生产中的模型并开始迭代改进模型时,这可能是一个认真考虑的好时机。

此外,从我的经验中可以得出一个重要的教训,即应始终从简单的方法开始,并从那里迭代。

使用 Python 完整的文字处理

原文:towardsdatascience.com/a-complete-word-processing-with-python-ac4e66963f40?source=collection_archive---------3-----------------------#2023-01-25

读取 PDF 文件,利用正则表达式,导出到 Excel 和 Word 文档,并转换回 PDF 格式

Himalaya Bir ShresthaTowards Data Science Himalaya Bir Shrestha

·

关注 发表在 Towards Data Science · 10 分钟阅读 · 2023 年 1 月 25 日

--

最*在一个自学项目中,我不得不处理一个 800 页的 PDF 文件。文件的每一章都包含一组共同的问题,而我需要每章中特定问题的答案。现在,逐页浏览文档并评估这些问题的答案会花费我很长时间。我在想是否有快速的办法来扫描每一页,只提取对我有用的信息。我找到了一个 Pythonic 的解决方案。在这篇文章中,我将分享如何使用 Python 读取 PDF 文件,提取每章中仅相关的信息,将数据导出到 Excel 和可编辑的 Word 文档中,然后使用不同的 Python 包将其转换回 PDF 格式。让我们开始吧。

图片由Dariusz Sankowski提供,来自Unsplash

数据

我将使用一个4 页的 PDF 文件作为示例,而不是 800 页的文档。在高中最后几天,我的同学们传阅了一本叫做“Auto book”的日记,作为一个记忆,收集彼此的兴趣、偏好和联系信息。我使用的 PDF 文件包含关于四个虚构朋友 Ram、Shyam、Hari 和 Shiva 的虚拟信息。该文件包含他们的国籍、出生日期、偏好(食物、水果、运动、球员、电影、演员)、喜欢的名言、目标、对政治的看法和对世界的留言。

一个名为 autobook.pdf 的 PDF 文件,包含四位朋友的信息和消息。图片由作者提供。

对于几个朋友,直接从 PDF 文件中复制粘贴信息会很简单。然而,如果 PDF 文件很大,使用 Python 来完成将会更加高效和准确。接下来的部分将逐步展示如何在 Python 中完成这项工作。

1. 使用 PyPDF2 或 PyMuPDF 包读取 PDF 文档

a. 使用 PyPDF2 读取第一页

要使用 Python 读取 PDF 文件中的文本,我使用了一个叫做PyPDF2的包及其 PdfReader 模块。在下面的代码片段中,我只读取了 PDF 文件的第一页,并从中提取了文本。

读取 PDF 文件第一页的脚本。图片由作者提供。

b. 使用 PyPDF2 读取整个文件的文本

要从 PDF 文件中读取全部文本,我使用了一个叫做extract_text_from_pdf的函数,如下所示。首先,函数以二进制格式打开 PDF 文件进行读取,并初始化读取对象。接着,初始化一个名为pdf_text的空列表。然后,在循环遍历 PDF 文件的每一页时,将每一页的内容提取出来并追加到列表中。

def extract_text_from_pdf(pdf_file: str) -> [str]:

     # Open the pdf file for reading in binary format
     with open(pdf_file, ‘rb’) as pdf:

     #initialize a PDfReader object
     reader = PyPDF2.PdfReader(pdf)

     #start an empty list
     pdf_text = []

     #loop through each page in document
     for page in reader.pages:
          content = page.extract_text()
          pdf_text.append(content)

     return pdf_text

当文件作为参数传递给上述函数时,它返回一个包含元素的列表,每个元素指向每一页的文本。给定文件 autobook.pdf 被使用 extract_text_from_pdf() 函数读取为 4 个元素,如下所示:

extracted_text 内的元素也可以使用以下方法连接为一个单一元素:

all_text = [''.join(extracted_text)]
len(all_text) #returns 1

all_text 返回一个包含整个 pdf 文件所有页面文本的单一元素的列表。

c. 使用 PyMUPDF 包读取整个 pdf 文件的替代方法。

或者,我发现了一个名为 PyMUPDF 的包来读取 pdf 文件中的所有文本,如下所示:

# install using: pip install PyMuPDF
import fitz

with fitz.open(file) as doc:
     text = ""
     for page in doc:
          #append characeter in each page
          text += page.get_text()

print ("Pdf file is read as a collection of ", len(text), "text elements.")
#returns 1786 elements.

首先,pdf 文件作为文档打开。text 被初始化为空字符串。通过循环遍历文档中的每一页,将每一页上的字符附加到 text 中。因此,text 的长度为 1786 个元素,其中包括每个字符、空格、换行符和标点符号。

2. RegEx

RegEx 或正则表达式是一系列字符,形成一个搜索模式。Python 有一个内置的 re 包用于此目的。

从给定的 pdf 文件中的所有文本中,我想提取出特定的信息。下面描述了我为此目的使用的函数,尽管 RegEx 的使用场景可能更广泛。

a. findall

当给定的模式在字符串/文本中匹配时,findall 函数会返回所有匹配项的列表。

在下面的代码片段中,xyz 返回文本中 NameNationalityCountry 的所有匹配项。在文本中,Name 出现了四次,Nationality 出现了三次,而 Country 仅出现了一次。

Findall 函数用于返回所有匹配项的列表。图片由作者提供。

b. sub

Sub 函数用于用字符串替代/替换一个或多个匹配项。

在给定的文本中,Nationality 在名为 Hari 的朋友的情况下被称为 Country。为了将 Country 替换为 Nationality,首先我编译了一个 Country 的正则表达式模式。接下来,我使用 sub 方法将模式替换为新词,并创建了一个名为 new_text 的新字符串。在 new_text 中,我发现 Nationality 出现了四次,而在前一个情况下是三次。

Sub 函数用于在字符串中进行替换/查找和替换。图片由作者提供。

c. finditer

finditer 方法可以用来查找字符串中模式的匹配项。

在给定的文本中,NameNationality 字段之间的文本包含朋友的实际姓名,而 NationalityDate of Birth 字段之间的文本包含实际的国籍。我创建了以下名为 find_between() 的函数,以在给定文本中查找任何两个连续单词之间的文本。

def find_between(first_word, last_word, text):
    """Find characters between any two first_word and last_word."""

    pattern = rf"(?P<start>{first_word})(?P<match>.+?)(?P<end>{last_word})"

    #Returns an iterator called matches based on pattern in the string.
    #re.DOTALL flag allows the '.' character to inclde new lines in matching 
    matches = re.finditer(pattern, text, re.DOTALL)

    new_list = []
    for match in matches:
        new_list.append(match.group("match"))

    return new_list

上述函数中的一个主要参数是 patternpattern 被设置为提取给定文本中 first_wordlast_word 之间的字符。finditer 函数返回一个迭代器,遍历字符串中所有不重叠的匹配项。对于每个匹配项,迭代器返回一个 Match 对象。一个名为 new_list 的空列表被初始化。通过遍历 matches,在每次迭代中,确切的 match 被附加到 new_list 中,并由函数返回。

通过这种方式,我能够从 PDF 文件中创建每个字段的列表,如姓名、国籍、出生日期、偏好等,如下方代码片段所示:

使用 find_between 函数从 PDF 中提取每个朋友的相关档案信息。图片由作者提供。

注意:

Python 中的 ‘.’ 特殊字符可以匹配文本/字符串中的任何字符,但不包括换行符。然而,re.DOTALL 标志使得 ‘.’ 字符可以匹配包括换行符在内的任何字符。

3. 导出数据到 Excel

a. 从列表中创建 pandas dataframe

在上一步中,我获取了每个朋友每个档案字段的列表。在这一步,我将这些列表转换为 pandas dataframe:

import pandas as pd

df = pd.DataFrame()
df["Name"] = names
df["Nationality"] = nationalities
df["Date of Birth"] = dobs
df["Favorite Food"] = foods
df["Favorite Fruit"] = fruits
df["Favorite Sports"] = sports
df["Favorite Player"] = players
df["Favorite Movie"] = movies
df["Favorite Actor"] = actors
df["Favorite Quotes"] = quotes
df["Aim"] = aims
df["Views on Politics"] = politics
df["Messages"] = messages

df = df.T
df.columns = df.iloc[0]

df.drop(index = "Name", inplace = True)

df

dataframe df 如下所示:

从每个朋友的每个档案字段的列表中推导 pandas dataframe。图片由作者提供。

b. 使用 pandas dataframe 进行条件格式化

Pandas dataframe 提供了类似于 Excel 的条件格式化功能。假设我想在 df 中突出显示包含我最喜欢的球员 Lionel Messi 的单元格。这可以通过 df.style.applymap() 函数完成,如下所示:

使用 df.style.applymap 函数在选定单元格中应用背景颜色。

当文件以 *.xlsx 格式导出时,如第 [28] 行所示,导出的文件还包含了对包含 Lionel Messi 的单元格的黄色高亮。

4. 从 Python 导出到 Word 格式

a. 使用 Python-docx 创建 Word 文档

要将数据从 Python 导出到 Word 格式,我使用了一个叫做 python-docx 的包。docx 包中的 Document 模块允许创建 Word 文档的不同部分,如标题和段落。

在下面的代码中,我首先为每位朋友添加标题“Name”,接着是包含朋友实际姓名的段落。然后是每个个人资料字段的标题和对应的文本。我在每个朋友的个人资料末尾添加了分页符。

from docx import Document
document = Document()

for column in df.columns:  
    document.add_heading("Name")
    p = document.add_paragraph()
    p.add_run(column)

    for index in df.index:
        document.add_heading(index)
        p = document.add_paragraph()
        p.add_run(df.loc[index, column])

    #add page break after profile of each friend
    document.add_page_break()

上述代码有助于在保存后生成以下格式的 Word 文档:

上述代码生成的 Word 文档。图片由作者提供。

b. 使用 Python-docx 高亮段落

Python-docx 包帮助生成具有 Microsoft Word 应用程序中大多数功能的 Word 文档。例如,可以添加不同字体样式、字体颜色和大小的字体,并具有粗体、斜体和下划线等功能。

假设我想在文档末尾创建一个名为 Favorites 的部分,并高亮文档中的文本。可以使用以下代码完成:

from docx.enum.text import WD_COLOR_INDEX
document.add_heading("Favorites")
p = document.add_paragraph()
p.add_run("This section consists of favorite items of each friend.").font.highlight_color=WD_COLOR_INDEX.YELLOW

c. 使用 Python-docx 创建表格

Python-docx 还允许直接从 Python 创建 Word 文档中的表格。假设我想在文档末尾的“Favorites”部分添加一个包含每位朋友最喜欢项目的表格。可以使用 document.add_tables(rows = nrows, cols = ncols) 创建表格。此外,需要为表格的每一行/列或单元格定义文本。

在下面的代码中,我定义了一个具有 8 行和 5 列的表格对象。接下来,我定义了表头和第一列。通过循环遍历数据框 df,我根据每位朋友的最喜欢项目定义表格中每个单元格的文本。

table = document.add_table(rows = 8, cols = 5)

# Adding heading in the 1st row of the table
column1 = table.rows[0].cells
column1[0].text = ‘Items’

#table header
for i in range(1, len(df.columns)+1):
  column1[i].text = df.columns[i-1]

#first column in the table for labels
for i in range(1,8):
     table.cell(i,0).text = df.index[i+1]
for i in range(2, 9):
     for j in range(1, 5):
          table.cell(i-1, j).text = df.iloc[i, j-1]

#define table style
table.style = “Light Grid Accent 1”

d. 保存文档。

文档保存为 *.docx 格式文件:

document.save(“../output/python_to_word.docx”)

包含“favorites”部分和表格的文档的最后一页如下所示:

“Favorites”部分和通过步骤 b 和 c 创建的表格。文件使用步骤 d 中的代码保存为 *.docx 格式。图片由作者提供。

5. 将 Word 文档转换为 pdf 格式。

要使用 Python 将文档从 Word *.docx 格式转换为 *.pdf 格式,我发现了一个叫做 docx2pdf 的包。可以使用该包的 convert 模块 convert(input_path, output_path) 将 Word 文档转换为 pdf 格式。

将 Word 文档从 *.docx 转换为 *.pdf 格式。图片由作者提供。

对我来说,输出文件夹如下所示:

包含 Excel 文件、Word 文档和所有通过 Python 导出的 pdf 文件的输出文件夹。图片由作者提供。

结论

扫描 pdf 文件并提取仅必要的信息可能非常耗时和压力大。Python 中有不同的包可以帮助自动化此过程,减轻繁琐性,并使提取精确信息的过程更高效。

在这篇文章中,我使用了一个包含四个朋友个人档案中常见字段/部分/标题的 pdf 文件作为示例,并提取了每个朋友每个字段的相关信息。首先,我使用了 PyPDF2 或 PyMuPDF 包来读取 pdf 文件并打印出整个文本。其次,我使用正则表达式(RegEx)来检测模式并在文本中找到每个模式的匹配项,以提取仅相关的信息。第三,我将每个朋友每个个人档案字段的信息列表转换为 pandas dataframe,并导出为 Excel 文件。接下来,我使用 Python-docx 包创建了一个 word 文件。最后,我再次使用 docx2pdf 文件将 word 文件转换为 pdf 格式。

本文的笔记本和输入的 pdf 文件可以在这个 GitHub 仓库中找到。感谢阅读!

ML 实验跟踪工具的全面比较

原文:towardsdatascience.com/a-comprehensive-comparison-of-ml-experiment-tracking-tools-9f0192543feb

图片由 BoliviaInteligente 提供,来自 Unsplash

7 个领先工具的优缺点是什么?

Eryk LewinsonTowards Data Science Eryk Lewinson

·发表于 Towards Data Science ·12 分钟阅读·2023 年 4 月 26 日

--

构建机器学习模型是一个高度迭代的过程。在为我们的项目构建一个简单的 MVP 之后,我们很可能会进行一系列实验,其中尝试不同的模型(以及它们的超参数)、创建或添加各种特征,或利用数据预处理技术。所有这些都是为了实现更好的性能。

随着实验数量的增加,跟踪它们变得具有挑战性。这时,一张纸或一个 Excel 表格可能已经不够用了。此外,当我们可能需要在将最佳表现的实验投入生产之前重现它时,会出现额外的复杂性。这就是 ML 实验跟踪发挥作用的地方!

什么是 ML 实验跟踪?

ML 实验跟踪是记录、组织和分析 ML 实验结果的过程。它帮助数据科学家跟踪他们的实验,重现实验结果,并有效地与他人协作。实验跟踪工具使我们能够记录实验元数据,如超参数、数据集/代码版本和模型性能指标。此外,我们可以轻松地可视化实验结果并比较它们的性能。通过这样做,我们可以识别出最有效的超参数组合和其他实验设置,从而提高模型的性能。

如何选择适合你需求的 ML 实验跟踪工具?

如果没有合适的工具,运行实验很容易导致工作流程混乱且难以管理,即使是简单的项目也是如此。作为数据科学家,选择最适合你需求和工作流程的实验跟踪工具至关重要。由于选项众多,选择理想的工具可能会令人感到困难。

在本文中,我们将深入探讨一些最流行的实验跟踪工具,并比较它们的功能,以帮助你做出明智的决定。到文章结尾时,你将清楚了解每个工具的优点和限制,从而能够选择最适合你特定需求的工具。

实验跟踪工具概述

1. MLflow

MLflow 是一个开源*台,旨在管理端到端的机器学习生命周期。它提供了一套工具,用于实验跟踪、存储和版本控制 ML 模型、将代码打包成可重现的运行、以及将模型部署到各种服务环境和*台。

主要特征

  • MLflow 是一个高度可定制的开源项目。

  • MLflow 是语言和框架无关的,它提供了与最流行的机器学习和深度学习框架的便捷集成。它还具有 R 和 Java 的 API,并支持 REST API。

  • MLflow 为最流行的机器学习和深度学习库提供了自动日志记录功能。通过使用它,我们不需要使用明确的日志语句来跟踪指标、参数和模型。

  • 只需几行代码,MLflow 就可以轻松集成到现有代码库中。

  • MLflow 拥有一个非常大且活跃的社区,并且在业内被广泛采用。

  • MLflow 可以在本地和远程服务器上记录结果,使得数据科学团队能够共享一个单一的仪表板。

  • 在存储大文件的情况下,MLflow 可以配置为将其存储在 S3 或其他云存储提供商上。

  • MLflow 的 Web 界面允许查看和比较不同用户进行的众多实验的结果。

  • 关于实验的附加说明可以存储在 MLflow 中。

  • MLflow 不仅提供实验跟踪,还提供端到端的机器学习生命周期管理。

请记住

  • MLflow 仅作为开源解决方案提供。因此,在公司环境中使用 MLflow 需要维护服务器和基础设施来支持该工具,这可能对较小的组织来说具有挑战性。

  • 在安全性方面,MLflow 默认没有强大的安全功能。因此,可能需要额外的配置和设置,以确保敏感数据的安全处理和访问控制的管理。因此,分享实验结果可能不那么容易。

  • 虽然 MLflow 支持协作,但它的协作功能不如某些其他*台。

2. DVC

DVC(数据版本控制)是一个开源的 MLOps 工具,用于数据版本控制和实验跟踪。它通常被描述为一个类似 Git 的系统,用于版本控制数据和模型。实际上,DVC 使我们能够通过 Git 跟踪数据,而无需将数据存储在 Git 仓库中。此外,它提供了管道管理功能,帮助实验的重现。

主要特点

  • DVC 是一个开源的、与语言无关的工具,免费使用。

  • DVC 使用类似 Git 的命令进行版本控制,使得对 Git 已经熟悉的开发人员能够轻松上手。

  • 被跟踪的指标存储在纯文本文件中,并与 Git 一起版本化。

  • DVC 以干净的方式处理一切,不会使仓库变得凌乱,因此我们不需要为每个实验创建专用的 Git 分支。

  • DVC 与*台无关,可以与广泛的存储提供商一起使用,使得跨不同*台管理数据变得简单。

  • DVC 可以跟踪代码、数据和工件的更改,使我们即使其中一个组件发生变化,也能轻松重现任何已执行的实验。

  • DVC 使用起来很简单,不需要特殊的基础设施或外部服务。我们可以在本地或使用云远程跟踪实验(和其他组件)。

  • 使用 DVC,我们不需要重建之前的模型或数据建模技术来达到相同的过去结果状态。

  • 使用 DVC,我们还可以跟踪每个实验的输出图像,例如混淆矩阵或特征重要性图。

  • 我们可以通过命令行处理 DVC 跟踪的实验,或利用 Iterative Studio(以前称为 DVC Studio),这是一个可以在线访问或本地托管的 web 应用程序。此外,DVC 还提供了一个 VS Code 扩展,方便在 IDE 内进行实验管理、比较和评估。

  • 我们可以使用一个名为 DVCLive 的附加库,轻松记录机器学习参数、指标和其他元数据,它还为最流行的机器学习和深度学习库提供了自动记录功能。

请注意

  • 在某些情况下,当处理非常大的数据集或大量实验时,你可能会遇到可扩展性问题。

  • 存储以内容可寻址的方式组织,所有操作都使用 Git 仓库进行。如果之前没有接触过,它可能会显得意外,尽管这种方式有很多好处,但可能有些团队无法将这种方法与他们的工具集成。

  • 虽然 DVCLive 支持大多数框架的自动记录,但不支持 scikit-learn

3. ClearML

ClearML 是一个用于管理机器学习(ML)实验的开源*台。它允许用户轻松跟踪、监控和重现他们的 ML 实验,直观地展示结果,并与团队成员协作。

主要特点

  • ClearML 提供了便捷的实验跟踪和管理,允许用户跟踪实验、指标、超参数、元数据等。

  • ClearML 支持自动日志记录。它还记录任何报告给领先可视化库(如 TensorBoard 和 Matplotlib)的指标。此外,ClearML 捕获并记录所有写入标准输出的内容,从调试信息到错误和库警告信息。最后,它自动跟踪信息,如 GPU、CPU、内存和网络的使用情况。

  • ClearML 与领先的机器学习和深度学习库兼容。

  • ClearML 可以部署在本地或云端。我们可以通过 Web 界面或 Python API 与该*台进行互动。

  • 我们可以在离线模式下使用 ClearML 实验,其中所有信息都保存在本地文件夹中。

  • ClearML 允许多个用户在同一项目上进行协作,方便实验和数据的共享。

  • ClearML 为用户提供了各种可视化工具,使得解释和分析实验数据变得容易。此外,其可定制的用户界面使用户能够按不同指标对模型进行排序。

  • ClearML 易于集成到现有工作流程中。

  • ClearML 具有内置的超参数优化功能。

请注意

  • 尽管 ClearML 提供了免费版,但更高级的功能(例如超参数优化或基于角色的访问控制)需要付费订阅。

  • 与其他类似*台相比,ClearML 的用户基础较小,这可能使得寻找支持或资源更加困难。

  • 由于需要为自动日志记录运行大量修改(ClearML 替换了其他框架的一些内置函数),系统可能相对较脆弱。

  • 设置和配置 ClearML 可能会很具挑战性,尤其是对于那些新接触该*台的用户。例如,与 MLflow 相比,在服务器上安装开源版本相对复杂。

4. TensorBoard

TensorBoard 是一个开源的基于 Web 的可视化工具,旨在帮助理解、调试和优化 TensorFlow 模型。它提供了一整套可视化工具,用于监控训练进度、评估模型性能和可视化数据流图。

主要特性

  • TensorBoard 通常是 TensorFlow 用户的首选,因为它使我们能够跟踪机器学习实验并可视化各种方面,如指标(例如损失和准确性)或模型图。此外,我们还可以用它来比较实验。

  • TensorBoard 不仅限于跟踪基于 TensorFlow 的实验。

  • TensorBoard 包括 What-If Tool (WIT),这是一个易于使用的界面,用于解释和理解黑箱机器学习模型。

  • 强大而庞大的用户社区为 TensorBoard 提供了出色的支持。

  • 除了开源的本地托管版本,TensorBoard.dev 还作为一个免费服务在托管服务器上提供,这使我们能够托管、跟踪和与任何人分享我们的 ML 实验。

  • TensorBoard 还提供了用于处理图像的成熟功能。

请注意

  • 一些用户可能会发现 TensorBoard 使用复杂,学习曲线陡峭。

  • TensorBoard 可能无法很好地扩展到大量实验,在查看和跟踪大规模实验时可能会导致性能下降。

  • TensorBoard 对实验比较的能力有限。

  • TensorBoard 主要设计用于单用户和本地机器使用,而不是团队使用。

  • 它缺乏用户管理功能。虽然可以使用 TensorBoard.dev 进行共享,但没有办法管理共享数据的隐私。

  • TensorBoard 不存储数据或代码版本,无法提供完全的可重现性。

5. Weights and Biases

Weights & Biases(W&B 或 WandB)是一个 MLOps *台,能够进行实验跟踪、数据/模型版本控制,以及团队协作 ML 项目。

主要特征

  • W&B 记录各种实验元数据(如超参数、指标、工件等),并允许用户比较实验并使用交互式可视化分析性能。

  • W&B 通过跟踪所有依赖项并为每个实验提供一致的环境,使重现实验变得容易。

  • W&B 提供了高度可定制的用户界面,允许团队可视化和组织他们的工作流程,包括任何自定义的指标和可视化。

  • W&B 提供了一系列支持团队协作的功能。

  • W&B 支持所有主要的 ML/DL 框架、云*台和工作流编排工具(如 Airflow)。

  • W&B 支持部署到广泛的*台,包括云服务和容器化环境。

  • W&B 通过与领先库的集成提供内置的超参数优化功能。

  • 将 W&B 跟踪集成到现有代码库中非常容易。

  • 如果需要处理不能外部共享的敏感数据,可以设置一个自托管的 W&B 实例。

  • 它允许对音频、视频和图像对象进行轻松调试。

请注意

  • W&B 是一个商业*台,某些功能可能需要付费订阅。

  • 与一些不太知名的 ML 框架的集成有限。

  • 协作功能需要付费等级。

  • 定价计划基于使用时间(称为跟踪小时),这可能对用户来说有些不直观。

6. Comet

Comet(以前称为 CometML)是一个基于云的*台,用于管理和跟踪机器学习实验。此外,它还可以用于版本控制训练数据、在模型注册表中跟踪我们的模型,并监控生产中模型的性能。

主要特征

  • Comet 允许我们记录和跟踪实验,提供了一个简单的方法来可视化和比较结果随时间的变化。它还为正在进行的实验提供实时指标和图表。

  • Comet 提供了多个功能,促进团队协作。例如,它允许我们共享项目、评论和标记其他团队成员。此外,它还具备用户管理功能。

  • Comet 拥有一套集成的工具,用于优化我们模型的超参数。

  • 它能够轻松集成最流行的机器学习和深度学习框架。此外,它还支持除了 Python 之外的其他语言,如 Javascript、Java、R,甚至 REST APIs。

  • Comet 支持对相当多的最流行的机器学习和深度学习库进行自动日志记录。

  • Comet 的*台可以在他们的云环境、虚拟私有云(VPC)或本地部署上使用。

  • Comet 的 UI 高度可定制,允许我们轻松创建报告和仪表盘。我们可以为实验创建自定义可视化,或使用社区提供的一些模板。

  • Comet 不仅支持脚本,还支持笔记本。

  • 使用 Comet,我们可以调试模型错误、环境特定错误等。

  • 具有专门用于视觉、音频、文本和表格数据的模块,使我们能够轻松识别数据集中的任何问题。

请记住

  • Comet 是一个商业*台,某些功能可能需要付费订阅。

7. DagsHub

DagsHub 是一个基于网络的*台,提供了一套用于管理和协作机器学习项目的工具。它旨在帮助数据科学家和机器学习工程师跟踪、版本控制和分享他们的代码,以及相应的数据和实验。

主要特点

  • DagsHub 使得机器学习实验的跟踪和管理变得轻而易举,包括超参数、指标和代码版本。

  • 借助协作编码工具,DagsHub 为数据科学团队提供了一个集中位置来可视化、比较和审查他们的实验,消除了设置任何基础设施的需求。

  • DagsHub 提供了两种不同的实验跟踪方式:通过 MLflow 和 Git。

  • 使用 MLflow 实现,远程设置由系统自动完成,无需本地存储实验数据或自己托管服务器。此外,该实现已经具备基于团队的访问和安全协议。

  • Git 实现依赖于简单、透明和开放的文件格式。由于 DagsHub Logger,适应任何语言或框架并用简单的 Git 推送导出跟踪的指标变得非常容易。

  • Git 集成意味着实验可以自动重现,并与其代码、数据、管道和模型关联。

  • 某些框架由 DagsHub Logger 的自动日志记录功能支持。

  • DagsHub 检测并支持 DVC 的metrics

    params 文件格式,同时它还设置了一个 DVC 远程,我们可以在其中对数据进行版本控制。

  • 可以使用 DagsHub 与你自己的数据存储,或使用虚拟私有云/本地解决方案。

  • 对开源和个人项目免费使用。

请注意

  • DagsHub Logger 的自动日志记录功能目前仅限于包括 PyTorch Lightning、Keras 和 fast.ai v2 在内的几个框架。

  • 目前无法创建高级或自定义可视化。

  • 对于组织来说,DagsHub 是一个商业*台,某些功能需要付费订阅。

  • 目前,使用日志记录器的方法没有对实时日志记录的支持。

其他工具

即使我们已经涵盖了 7 种不同的实验跟踪工具/框架,这也绝未涵盖所有可用选项。为了简洁起见,我们只提及一些其他替代方案,你可以自行进行研究。

其他实验跟踪(不仅仅是)工具包括:

  • Neptune

  • Sacred

  • Guild.ai

  • Polyaxon

  • Valohai

  • Kubeflow

  • Verta AI

  • Amazon Sagemaker Studio

  • Pachyderm

总结

由于选择过多,为你的项目或整个团队选择合适的 ML 实验跟踪工具可能是一项艰巨的任务。在做出决定时,你必须考虑许多因素,例如:

  • 开源与商业

  • 工具是否带有网页 UI 或基于控制台

  • 与 ML/DL 框架、云*台和编程语言的集成

  • 具体跟踪了什么以及添加自定义日志内容的难易程度

  • 存储 — 云存储或本地存储

  • 可视化功能

  • 稳定性和可扩展性

  • 工具是否促进团队成员之间的协作

  • 工具是否需要用户设置远程服务器

  • 安全性和基于团队的访问

  • 工具是否提供与 ML 生命周期相关的额外功能,例如部署

你可以使用以下表格作为参考,快速比较我们今天涵盖的工具。希望它能帮助你为下一个项目做出决定!

一如既往,任何建设性的反馈都是非常欢迎的。你可以通过 Twitter 或评论与我联系。

喜欢这篇文章?成为 Medium 会员,继续无限阅读来学习。如果你使用 这个链接 成为会员,你将支持我,而不会额外增加你的费用。提前感谢,期待再见!

你可能还会对以下内容感兴趣:

[## 前 10 个 VS Code 扩展用于数据科学

提升你的生产力,使用这些必备工具!

medium.com (/enhance-your-ml-experimentation-workflow-with-real-time-plots-434106b1a1c2?source=post_page-----9f0192543feb--------------------------------) [## 增强你的 ML 实验工作流程,使用实时绘图

本教程的第二部分讲解了如何在不离开 IDE 的情况下运行和评估实验

[towardsdatascience.com eryk-lewinson.medium.com/introducing-the-second-edition-of-python-for-finance-cookbook-f42f59c8acd0?source=post_page-----9f0192543feb-------------------------------- [## 介绍《金融 Python 食谱》第二版

促使我编写第二版的原因以及你可以从中期待的内容

eryk-lewinson.medium.com

所有图片,除非另有说明,均由作者提供。

这篇文章最初发布在 DagsHub 博客 上。

一本全面的 OpenStreetMap 入门指南

原文:towardsdatascience.com/a-comprehensive-guide-for-getting-started-with-openstreetmap-e92dff95fc80

学习 OpenStreetMap 的基础概念,同时练习使用该网站

Eugenia AnelloTowards Data Science Eugenia Anello

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

--

图片由 Unsplash+ 提供,照片来源于 Unsplash

这是关于地理空间数据分析系列的第二篇文章:

  1. 使用 QGIS 进行地理空间数据分析

  2. OpenStreetMap 入门指南(本帖子)

  3. 使用 GeoPandas 进行地理空间数据分析

  4. 使用 OSMnx 进行地理空间数据分析

  5. 数据科学家的地理编码

  6. 使用 Geemap 进行地理空间数据分析

本文继续讲述 使用 QGIS 进行地理空间数据分析的实用介绍 的故事。在上一篇文章中,我介绍了地理空间数据分析的神奇世界,这是一种数据科学的子领域,涉及从一种特殊类型的数据(称为地理空间数据)中操作和推断信息。

与普通数据不同,每行地理空间数据对应一个特定的位置,并可以绘制在地图上。最简单的情况是通过经纬度描述的位置数据点,但也可能有更复杂的特征,如道路、河流、国家边界和地形,在这些情况下,一对坐标已经不够用了。

这次我专注于更好地理解 OpenStreet 是什么,它背后的概念,以及如何下载数据。首先,OpenStreetMap 是最大的免费且可编辑的地理数据库和项目,任何人都可以参与贡献,甚至包括你,因为你现在已经知道了这个世界地图的存在。它也被称为制图世界的维基百科,因为它们都由来自世界各地的志愿者维护。这引起了你的兴趣吗?那就开始吧!

目录:

  • OpenStreetMap 的基本组成部分

  • OSM 数据格式

  • 开始使用 OpenStreetMap

  • 如何下载 OSM 数据

OpenStreetMap 的基本组成部分

在 OpenStreetMap 中,有三个主要组成部分:节点道路关系。最简单的数据类型是节点,它由一对坐标(纬度和经度)描述。节点的例子包括餐馆、酒吧、商店、图书馆、银行、博物馆等。

作者截图。OpenStreetMap 上获得的节点示例

在上图中,我选择了 Unicredit 银行,这是一种节点,如左侧边栏所示。如果你尝试从地图上选择不同的特征,也可以注意到每个现实世界的特征都有标签,这些标签描述了该特征的地理属性。

就像 Python 字典一样,它是一个键值对集合,其中键指定节点、道路或关系的属性。在这个例子中,Unicredit 银行的属性包括 amenity、atm 和 name。特别是,amenity 通常用来指定居民和游客使用的设施类型,例如咖啡馆、学校、酒吧和餐馆。

作者截图。OpenStreetMap 上获得的道路(线性特征)示例

现在是时候谈谈道路了,它被表示为一组节点。道路可以是线性特征多边形特征。当我们处理线性特征时,总是有一个起始节点和一个结束节点。常见的例子有道路和铁路。

另一种情况是多边形特征,其中第一个和最后一个节点重合。在多边形特征中,有两种可能的类型:大型建筑,如教堂和宫殿,以及用于住宅、工业或商业目的的区域。需要注意的是,一条道路最多可以包含 2,000 个节点。

作者截图。OpenStreetMap 上获得的道路(多边形特征)示例

第三种数据类型是关系,它是一种特殊的结构,用于将大量节点或道路组织成一个更大的整体。经典的例子包括国家或城市的边界。与道路类似,你可以区分线性特征和多边形特征。它也可以是一个多边形,描述一个包含多个多边形的区域。

作者截图。 从 OpenStreetMap 获得的关系(多边形)的示例

这是博洛尼亚的一个住宅区示例,其中包含 9 条道路,每条道路由不同的节点组成。

OSM 数据格式

在我之前的教程中,我展示了表示矢量数据和栅格数据的最流行格式分别是 Shapefile 和 GeoTIFF。对于 OSM 数据,最常见的格式是PBFXML格式。PDF 文件通常比 XML 文件更受欢迎,因为它压缩程度高,优化了空间效率和速度。

开始使用 OpenStreetMap

一旦 OpenStreetMap 的概念清晰后,就该进入教程的最有趣部分了。你不需要安装任何东西,只需要访问 OpenStreetMap 的网站。

我们可以搜索博洛尼亚,这是一座位于意大利艾米利亚-罗马涅地区的充满活力的城市,以世界上最古老的大学和著名菜肴(如意大利饺子和千层面)而闻名。程序如下:

  • 输入你喜欢的城市并点击“前往”

  • 在左侧边栏选择“City Bologna, Emilia-Romagna, Italy”选项

作者 GIF。从 OpenStreetMap 获得的关系(博洛尼亚的边界)示例

我们可以在城市中移动并选择一个地图元素。例如,我们可以去皮亚察·马焦雷,按下“查询特征”按钮,然后点击“Artwork Il Nettuno”,这是一座献给海神 Nettuno 的喷泉。

作者 GIF。从 OpenStreetMap 获得的节点(Artwork Il Nettuno)示例

这就是在 OpenStreetMap 上查看你感兴趣的元素的主要信息的方法。例如,如果你想提取博洛尼亚的所有餐厅,最好先了解这些地方的共同特点。你需要在网站上进行一些这样的探索,然后再直接提取你的兴趣点。

如何下载 OSM 数据

从 OpenStreetMap 下载数据有很多种方法。合适的方法取决于数据集的大小。如果你只是想下载一个小数据集,比如一个酒吧,一个公园或一个住宅区,你可以直接从 OpenStreetMap 的网站上下载。你有两个选择:可以点击“下载 XML”或“导出”按钮。

作者截图。 从 OpenStreetMap 的网站下载数据。

如果你需要来自整个大陆和国家的 OSM 地图数据,你可以从 geofabrik 下载数据。如果你需要特定国家的数据,可以点击大陆文件的链接或子区域的链接。

作者截图。 从 geofabrik 的网站下载数据。

第三种方法是直接使用 Python 下载 OSM 数据。Python 有一个库叫做 Pyrosm,可以从世界各地的大量位置下载和读取 PBF 数据。

from pyrosm import get_data
fp = get_data("Lisbon")

不幸的是,并不是所有的城市都有提供。你可以通过打印可以下载的城市列表来检查:

print(sources.cities.available)

输出:

['Aachen', 'Aarhus', 'Adelaide', 'Albuquerque', 'Alexandria', 'Amsterdam', 'Antwerpen', 'Arnhem',...]

我需要说明的是,这些并不是下载 OSM 数据的唯一方法。你可以查看我在最后建议的资源中其他的方法。

最后的想法:

就这些了!这只是 OpenStreetMap 世界的一个概述!在开始分析这种数据时,如果对主要数据类型没有任何了解,并且只是通过直观的例子来浏览网站,可能会有些挑战。虽然有很多资源,但我发现它们很分散,因为涵盖的方面很少。希望这个教程能帮助你开始分析地理空间数据的旅程。感谢阅读!祝你有美好的一天!

有用的资源:

分布式数据并行(DDP)的全面指南

原文:towardsdatascience.com/a-comprehensive-guide-of-distributed-data-parallel-ddp-2bb1d8b5edfb

关于如何使用分布式数据并行(DDP)加速模型训练的全面指南

François PorcherTowards Data Science François Porcher

·发表于 Towards Data Science ·12 分钟阅读·2023 年 10 月 30 日

--

作者提供的图像

引言

大家好!我是 Francois,Meta 的研究科学家。欢迎来到这个新的教程,它是 Awesome AI Tutorials 系列的一部分。

在本教程中,我们将揭示一种称为 DDP 的著名技术,以便在多个 GPU 上同时训练模型。

在我工程学校的日子里,我记得利用 Google Colab 的 GPU 进行训练。然而,在企业领域,情况有所不同。如果你在一个对 AI 投资巨大的组织中工作,特别是如果你在一个科技巨头公司,你很可能有大量的 GPU 集群可供使用。

本节旨在为你提供利用多个 GPU 力量的知识,实现快速高效的训练。你猜怎么着?这比你想象的要简单!在我们继续之前,我建议你对 PyTorch 有一个良好的掌握,包括其核心组件如数据集、数据加载器、优化器、CUDA 和训练循环。

最初,我认为 DDP 是一个复杂且几乎不可实现的工具,认为它需要一个大团队来建立必要的基础设施。然而,我向你保证,DDP 不仅直观而且简洁,只需少量的代码行即可实现。让我们一起开始这段启发性的旅程吧!

DDP 的高层次直觉

分布式数据并行(DDP)一旦拆解就变得简单明了。想象一下你有一个包含 4 个 GPU 的集群。使用 DDP,同一个模型会被加载到每个 GPU 上,包括优化器。主要的区别在于我们如何分配数据。

DDP,图像来自 PyTorch 教程

如果你对深度学习有所了解,你会记得 DataLoader,这是一个将你的数据集分割成不同批次的工具。通常会将整个数据集分割成这些批次,在每个批次计算后更新模型。

更深入地看,DDP 通过将每个批次划分为我们可以称之为“子批次”的部分来改进这个过程。本质上,每个模型副本处理主批次的一部分,从而为每个 GPU 生成不同的梯度计算。

在 DDP 中,我们通过一个叫做DistributedSampler的工具将批次拆分为子批次,如下图所示:

DDP,图像来自 PyTorch 教程

在每个子批次分配到各个 GPU 之后,每个 GPU 计算其独特的梯度。

DDP,图像来自 PyTorch 教程

  • 现在进入 DDP 的神奇部分。在更新模型参数之前,需要汇总在每个 GPU 上计算的梯度,以便每个 GPU 都有整个数据批次上计算的*均梯度。

  • 这是通过从所有 GPU 获取梯度并进行*均来完成的。例如,如果你有 4 个 GPU,那么某个特定模型参数的*均梯度就是该参数在 4 个 GPU 上的梯度之和除以 4。

  • DDP 使用NCCLGloo后端(NCCL 针对 NVIDIA GPU 进行了优化,Gloo 则更为通用)来有效地在 GPU 之间通信和*均梯度。

DDP,图像来自 PyTorch 教程

术语、节点和排名词汇表

在深入代码之前,理解我们将频繁使用的词汇至关重要。让我们揭开这些术语的神秘面纱:

  • 节点: 可以把节点想象成一台配备有多个 GPU 的强大机器。当我们谈论集群时,它不仅仅是一堆 GPU 堆在一起。相反,它们被组织成组或“节点”。例如,一个节点可能包含 8 个 GPU。

  • 主节点: 在多节点环境中,通常有一个节点负责主控。这个“主节点”处理诸如同步、启动模型副本、监督模型加载和管理日志条目等任务。如果没有主节点,每个 GPU 都会独立生成日志,导致混乱。

  • 本地排名: “排名”这个术语可以类比为一个 ID 或位置。本地排名指的是 GPU 在其特定节点(或机器)中的位置或 ID。它是“本地的”,因为它仅限于那台机器。

  • 全球排名: 从更广泛的角度看,全球排名识别所有可用节点中的 GPU。这是一个无论机器如何都唯一的标识符

  • World Size: 从根本上说,这是所有在所有节点上可用的 GPU 数量的计数。简单来说,这是节点数和每个节点中 GPU 数量的乘积。

从实际情况来看,如果你只使用一台机器,那么事情会更加简单,因为本地排名等于全局排名。

用图像来澄清这个问题:

本地排名,图像来自 教程

本地排名,图像来自 教程

理解 DDP 限制:

分布式数据并行(DDP)在许多深度学习工作流中具有变革性,但理解其局限性非常重要。

DDP 限制的核心在于其内存消耗。使用 DDP 时,每个 GPU 加载一个模型副本、优化器及其相应的数据批次。 GPU 内存通常从几 GB 到高端 GPU 的 80GB 不等。

对于较小的模型,这不是问题。然而,当涉足大型语言模型(LLMs)或类似 GPT 的架构时,单个 GPU 内存的限制可能是不够的。

在计算机视觉中,虽然有很多轻量级模型,但增加批量大小时会遇到挑战,特别是在涉及3D 图像或目标检测任务的场景中。

进入完全分片数据并行(FSDP)。这种方法通过不仅分布数据,还将模型和优化器状态分散到 GPU 内存中,从而扩展了 DDP 的好处。虽然这听起来很有优势,但 FSDP 会增加 GPU 之间的通信,可能会导致训练速度变慢。

总结:

  • 如果你的模型及其对应的批量数据能够舒适地适应 GPU 的内存,DDP 是你最好的选择,因为它速度较快。

  • 对于需要更多内存的巨型模型,FSDP 是更合适的选择。然而,请记住其权衡:你是在为内存牺牲速度。

为什么你应该更倾向于使用 DDP 而不是 DP?

如果你访问 PyTorch 的网站,实际上有两种选择:DP 和 DDP。但我提到这一点只是为了让你不要迷路或困惑:只使用 DDP,它更快且不限于单个节点。

来自 PyTorch 的比较 教程

代码演示:

实现分布式深度学习比你想象的要简单。美在于你不会被手动配置 GPU 或梯度分布的复杂性所困扰。

你可以在以下位置找到所有模板和脚本:

[## GitHub - FrancoisPorcher/awesome-ai-tutorials: 最佳 AI 教程集合,让你成为数据科学的…

最佳 AI 教程集合,让你成为数据科学的高手! - GitHub …

github.com

下面是我们将采取的步骤的细分:

  1. 进程初始化:这涉及到指定主节点、指定端口,并设置world_size

  2. 分布式 DataLoader 设置:这个步骤至关重要的是将每个批次在可用 GPU 之间进行分区。我们将确保数据均匀分布而没有任何重叠。

  3. 模型训练/测试:本质上,这一步与单 GPU 过程几乎没有变化。

在 1 GPU 1 节点上训练(基准)

首先,让我们定义一个简单的代码,它加载一个数据集,创建一个模型,并在单个 GPU 上进行端到端训练。这将是我们的起点:

import torch
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
import numpy as np

class WineDataset(Dataset):
    def __init__(self, data, targets):
        self.data = data
        self.targets = targets

    def __len__(self):
        return len(self.data)

    def __getitem__(self, idx):
        return torch.tensor(self.data[idx], dtype=torch.float), torch.tensor(self.targets[idx], dtype=torch.long)

class SimpleNN(torch.nn.Module):
    def __init__(self):
        super(SimpleNN, self).__init__()
        self.fc1 = torch.nn.Linear(13, 64)
        self.fc2 = torch.nn.Linear(64, 3)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return x

class Trainer():
    def __init__(self, model, train_data, optimizer, gpu_id, save_every):
        self.model = model
        self.train_data = train_data
        self.optimizer = optimizer
        self.gpu_id = gpu_id
        self.save_every = save_every
        self.losses = []

    def _run_batch(self, source, targets):
        self.optimizer.zero_grad()
        output = self.model(source)
        loss = F.cross_entropy(output, targets)
        loss.backward()
        self.optimizer.step()
        return loss.item()

    def _run_epoch(self, epoch):
        total_loss = 0.0
        num_batches = len(self.train_data)
        for source, targets in self.train_data:
            source = source.to(self.gpu_id)
            targets = targets.to(self.gpu_id)
            loss = self._run_batch(source, targets)
            total_loss += loss

        avg_loss = total_loss / num_batches
        self.losses.append(avg_loss)
        print(f"Epoch {epoch}, Loss: {avg_loss:.4f}")

    def _save_checkpoint(self, epoch):
        checkpoint = self.model.state_dict()
        PATH = f"model_{epoch}.pt"
        torch.save(checkpoint, PATH)
        print(f"Epoch {epoch} | Model saved to {PATH}")

    def train(self, max_epochs):
        self.model.train()
        for epoch in range(max_epochs):
            self._run_epoch(epoch)
            if epoch % self.save_every == 0:
                self._save_checkpoint(epoch)

def load_train_objs():
    wine_data = load_wine()
    X = wine_data.data
    y = wine_data.target

    # Normalize and split
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
    scaler = StandardScaler().fit(X_train)
    X_train = scaler.transform(X_train)
    X_test = scaler.transform(X_test)

    train_set = WineDataset(X_train, y_train)
    test_set = WineDataset(X_test, y_test)

    print("Sample from dataset:")
    sample_data, sample_target = train_set[0]
    print(f"Data: {sample_data}")
    print(f"Target: {sample_target}")

    model = SimpleNN()
    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

    return train_set, model, optimizer

def prepare_dataloader(dataset, batch_size):
    return DataLoader(dataset, batch_size=batch_size, pin_memory=True, shuffle=True)

def main(device, total_epochs, save_every, batch_size):
    dataset, model, optimizer = load_train_objs()
    train_data = prepare_dataloader(dataset, batch_size)
    trainer = Trainer(model, train_data, optimizer, device, save_every)
    trainer.train(total_epochs)

main(device=torch.device("cuda:0" if torch.cuda.is_available() else "cpu"), total_epochs=100, save_every=50, batch_size=32)

在多个 GPU,1 节点上训练

现在我们将使用单节点上的所有 GPU,步骤如下:

  1. 导入进行分布式训练所需的库。

  2. 初始化分布式环境:(特别是MASTER_ADDRMASTER_PORT

  3. 使用DistributedDataParallel包装器包装模型。

  4. 使用分布式采样器确保数据集在 GPU 之间以分布式的方式划分。

  5. 调整主函数以spawn多个进程进行多 GPU 训练。

对于库,我们需要这些:

import torch.multiprocessing as mp
from torch.utils.data.distributed import DistributedSampler
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.distributed import init_process_group, destroy_process_group
import os

然后我们需要设置每个进程。例如,如果我们在 1 节点上有 8 个 GPU,我们将调用以下函数 8 次,每次针对一个 GPU,并设置正确的local_rank

def ddp_setup(rank, world_size):
    """
    Set up the distributed environment.

    Args:
        rank: The rank of the current process. Unique identifier for each process in the distributed training.
        world_size: Total number of processes participating in the distributed training.
    """

    # Address of the main node. Since we are doing single-node training, it's set to localhost.
    os.environ["MASTER_ADDR"] = "localhost"

    # Port on which the master node is expected to listen for communications from workers.
    os.environ["MASTER_PORT"] = "12355"

    # Initialize the process group. 
    # 'backend' specifies the communication backend to be used, "nccl" is optimized for GPU training.
    init_process_group(backend="nccl", rank=rank, world_size=world_size)

    # Set the current CUDA device to the specified device (identified by rank).
    # This ensures that each process uses a different GPU in a multi-GPU setup.
    torch.cuda.set_device(rank)

对该函数的一些解释:

  • MASTER_ADDR是主机(或 rank 0 进程)运行的机器的主机名。在这里是 localhost

  • MASTER_PORT:指定主节点监听来自工作进程或其他进程的连接的端口。12355 是任意的。你可以选择任何未被占用的端口号,只要它不被系统上的其他服务使用,并且被你的防火墙规则允许。

  • torch.cuda.set_device(rank):这确保每个进程使用其对应的 GPU。

然后我们需要稍微修改 Trainer 类。我们将简单地用 DDP 函数包装模型:

class Trainer():
    def __init__(self, model, train_data, optimizer, gpu_id, save_every):
        self.model = model.to(gpu_id)
        self.train_data = train_data
        self.optimizer = optimizer
        self.gpu_id = gpu_id
        self.save_every = save_every
        self.losses = []

        # This changes
        self.model = DDP(self.model, device_ids=[gpu_id])

Trainer 类的其他部分保持不变,真是太棒了!

现在我们必须更改 dataloader,因为记住,我们必须在每个 GPU 上拆分批次:

def prepare_dataloader(dataset: Dataset, batch_size: int):
    return DataLoader(
        dataset,
        batch_size=batch_size,
        pin_memory=True,
        shuffle=False,
        sampler=DistributedSampler(dataset)
    )

现在我们可以修改main函数,这个函数会为每个进程调用(所以在我们的例子中是 8 次):

def main(rank: int, world_size: int, save_every: int, total_epochs: int, batch_size: int):
    """
    Main training function for distributed data parallel (DDP) setup.

    Args:
        rank (int): The rank of the current process (0 <= rank < world_size). Each process is assigned a unique rank.
        world_size (int): Total number of processes involved in the distributed training.
        save_every (int): Frequency of model checkpoint saving, in terms of epochs.
        total_epochs (int): Total number of epochs for training.
        batch_size (int): Number of samples processed in one iteration (forward and backward pass).
    """

    # Set up the distributed environment, including setting the master address, port, and backend.
    ddp_setup(rank, world_size)

    # Load the necessary training objects - dataset, model, and optimizer.
    dataset, model, optimizer = load_train_objs()

    # Prepare the data loader for distributed training. It partitions the dataset across the processes and handles shuffling.
    train_data = prepare_dataloader(dataset, batch_size)

    # Initialize the trainer instance with the loaded model, data, and other configurations.
    trainer = Trainer(model, train_data, optimizer, rank, save_every)

    # Train the model for the specified number of epochs.
    trainer.train(total_epochs)

    # Cleanup the distributed environment after training is complete.
    destroy_process_group()

最后,在执行脚本时,我们需要启动 8 个进程。这是通过mp.spawn()函数完成的:

if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser(description='simple distributed training job')
    parser.add_argument('total_epochs', type=int, help='Total epochs to train the model')
    parser.add_argument('save_every', type=int, help='How often to save a snapshot')
    parser.add_argument('--batch_size', default=32, type=int, help='Input batch size on each device (default: 32)')
    args = parser.parse_args()

    world_size = torch.cuda.device_count()
    mp.spawn(main, args=(world_size, args.save_every, args.total_epochs, args.batch_size), nprocs=world_size)

终极步骤:在多个节点上训练

如果你已经做到这些,恭喜你!终极步骤是能够招募所有在不同节点上的 GPU。但是如果你理解了我们迄今为止所做的,这将非常简单。

在跨多个节点扩展时,关键的区别在于从local_rankglobal_rank的转换。这一点非常重要,因为每个进程都需要一个唯一的标识符。例如,如果你在两个节点上工作,每个节点有 8 个 GPU,那么进程 0 和 8 都会有一个local_rank为 0。

global_rank 由非常直观的公式给出:

global_rank = node_rank * world_size_per_node + local_rank

所以首先让我们修改ddp_setup函数:

def ddp_setup(local_rank, world_size_per_node, node_rank):
    os.environ["MASTER_ADDR"] = "MASTER_NODE_IP"  # <-- Replace with your master node IP
    os.environ["MASTER_PORT"] = "12355"  
    global_rank = node_rank * world_size_per_node + local_rank
    init_process_group(backend="nccl", rank=global_rank, world_size=world_size_per_node*torch.cuda.device_count())
    torch.cuda.set_device(local_rank)

我们需要调整主函数,它现在接受world_size_per_node作为参数:

def main(local_rank: int, world_size_per_node: int, save_every: int, total_epochs: int, batch_size: int, node_rank: int):
    ddp_setup(local_rank, world_size_per_node, node_rank)
    # ... (rest of the main function)

最后,我们也要调整mp.spawn()函数以适应world_size_per_node

if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser(description='simple distributed training job')
    parser.add_argument('total_epochs', type=int, help='Total epochs to train the model')
    parser.add_argument('save_every', type=int, help='How often to save a snapshot')
    parser.add_argument('--batch_size', default=32, type=int, help='Input batch size on each device (default: 32)')
    parser.add_argument('--node_rank', default=0, type=int, help='The rank of the node in multi-node training')
    args = parser.parse_args()

    world_size_per_node = torch.cuda.device_count()
    mp.spawn(main, args=(world_size_per_node, args.save_every, args.total_epochs, args.batch_size, args.node_rank), nprocs=world_size_per_node)

使用集群(SLURM)

你现在可以准备将训练发送到集群。这非常简单,你只需调用所需的节点数量即可。

这是 SLURM 脚本的模板:

#!/bin/bash
#SBATCH --job-name=DDPTraining       # Name of the job
#SBATCH --nodes=$1                   # Number of nodes specified by the user
#SBATCH --ntasks-per-node=1          # Ensure only one task runs per node
#SBATCH --cpus-per-task=1            # Number of CPU cores per task
#SBATCH --gres=gpu:1                 # Number of GPUs per node
#SBATCH --time=01:00:00              # Time limit hrs:min:sec (1 hour in this example)
#SBATCH --mem=4GB                    # Memory limit per GPU
#SBATCH --output=training_%j.log     # Output and error log name (%j expands to jobId)
#SBATCH --partition=gpu              # Specify the partition or queue

srun python3 your_python_script.py --total_epochs 10 --save_every 2 --batch_size 32 --node_rank $SLURM_NODEID

现在你可以使用以下命令从终端启动训练

sbatch train_net.sh 2  # for using 2 nodes

恭喜,你完成了!

感谢阅读!在你离开之前:

欲获取更多精彩教程,请查看我在 Github 上的 AI 教程汇编

[## GitHub - FrancoisPorcher/awesome-ai-tutorials: 最佳的 AI 教程集合,让你成为……

最佳的 AI 教程集合,让你成为数据科学的高手! - GitHub …

github.com](https://github.com/FrancoisPorcher/awesome-ai-tutorials?source=post_page-----2bb1d8b5edfb--------------------------------)

你应该在收件箱中获取我的文章。 在这里订阅。

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

如果你觉得这篇文章有洞察力并且有帮助,请考虑关注我并点赞以获取更多深入内容!你的支持帮助我继续制作有助于我们共同理解的内容。

参考资料

《SQL 中公共表表达式的全面指南》

原文:towardsdatascience.com/a-comprehensive-guide-on-common-table-expression-in-sql-8c892ffda2f5

回到基础 | 简化复杂查询并提高可读性

Iffat Malik数据科学前沿 Iffat Malik

·发表于 数据科学前沿 ·14 分钟阅读·2023 年 8 月 22 日

--

图片由作者提供

在编程中,将指令或语句分组到更小、更易于管理的代码块中是一种常见做法。这种做法通常被称为 代码块组织。它基本上是将程序或程序的大块分解成更小且逻辑上相关的块。这些块旨在执行特定任务或仅仅是将相关功能分组。这种方法不仅提高了代码的可读性,还使代码更有组织和更易于维护。各种编程结构,如函数、方法、try-catch 块、循环和条件语句,通常用于此目的。

SQL中,实现相同效果的一种方法是使用公共表表达式(CTE)。在本文中,我们将探讨CTE如何显著简化和优化复杂的 SQL 查询。

什么是 CTE?

CTE(公共表表达式)是一个临时存储结果集的查询,以便在另一条查询中引用和使用。只要在同一执行范围内,CTE 就会保持可用。

简而言之,CTE 像是一个临时表,它保存查询的中间结果,允许你在另一条SQL查询中使用这些结果。它也被称为子查询重构

在这里,有两个关键点需要注意,

  • ‘临时结果集’,意味着CTE的输出会被暂时存储,而不会在数据库中创建一个永久表。

  • ‘同一执行范围’,指的是它只能在定义它的相同SQL语句中使用。一旦该SQL语句完成,CTE将不再可用,使其局限于定义的范围内。

CTE的目的是简化长而复杂的查询。通过将长查询拆分成简单、小而可管理的代码块,它降低了复杂性,同时提高了可读性,在某些数据库中,也增加了可重用性。

通过使用WITH子句来定义。CTE的常见语法是,

作者提供的图像:CTE的常见语法

你也可以定义多个CTEs,定义多个CTEs的常见语法是,

作者提供的图像:使用多个CTE的常见语法

CTEs可以在以下SQL语句中使用,

  • 选择

  • 插入

  • 更新

  • 删除

一个CTE也可以在另一个CTE中被引用。当一个CTE引用自身时,它就成为了递归 CTE

现在我们对CTE有了基本了解,让我们开始实际操作。在这里,我们将使用来自虚拟车辆零售公司数据,你可以在我的GitHub Repo中找到源数据,

作者提供的图像:车辆零售公司 ER 图

假设我们需要生成一个基于总收入排名的产品报告。这个报告应提供关键的信息,如产品 ID、产品名称、总销售数量、总收入以及基于总收入的销售排名。

--calculate total quantity sold and total revenue for each product
WITH PRODUCTSALES AS (
  SELECT
    PRD.PRODUCTID, 
    PRD.PRODUCTNAME,
    SUM(ORDET.QUANTITYORDERED) AS TOTAL_QUANTITY_SOLD,
    SUM(ORDET.QUANTITYORDERED * PRD.BUYPRICE) AS TOTAL_REVENUE
  FROM 
    PRODUCTS PRD
  JOIN 
    ORDERDETAILS ORDET 
    ON PRD.PRODUCTID = ORDET.PRODUCTID
  GROUP BY 
    PRD.PRODUCTID, PRD.PRODUCTNAME
)

--retrieve product sales information with ranking based on total revenue
SELECT
  PRODUCTID, 
  PRODUCTNAME,
  TOTAL_QUANTITY_SOLD,
  TOTAL_REVENUE,
  RANK() OVER (ORDER BY TOTAL_REVENUE DESC) AS SALES_RANK
FROM 
  PRODUCTSALES;

作者提供的 GIF

在上述查询中,我们为每个产品计算了两个重要指标,在CTE, ‘PRODUCTSALES’中:

  • 销售的总数量(‘TOTAL_QUANTITY_SOLD’)和

  • 从这些销售中产生的总收入(‘TOTAL_REVENUE’)。

在主要的SQL查询中,我们使用了这些指标。此外,我们使用了RANK()函数。这个窗口函数为表中每一行数据分配一个唯一的顺序编号,或在指定的分区内保持相同的排名,针对具有相同值的行。

你可以在这里查看更多关于窗口函数的细节,

## 窗口函数 — 数据工程师和数据科学家必须了解的

回到基础 | SQL 初学者基础

towardsdatascience.com

在我们的案例中,我们根据‘TOTAL_REVENUE’使用了RANK(),并按降序排列结果。这就像拥有一个报告,帮助我们理解哪些产品在销售和收入方面最成功。

实践练习

我创建了一些实践练习,以便熟悉使用CTE。欢迎在本文的回复部分发布你的答案。如果你需要任何帮助,随时留言。

**1\. Customer Credit Limit Analysis
----------------------------------**

Write a query to perform a customer credit analysis using the table
'CUSTOMERS'. Calculate the following details,

* Customer Name,
* Country,
* Average Credit Limit, 
* Maximum Credit Limit, 
* Minimum Credit Limit

Display credit limits - Average, Maximum and Minimum - such that it is 
partitioned for each Country.

**Hint:** Use the Window Functions AVG(), MAX() and MIN()
**2\. Order Frequency Analysis
---------------------------** 
Write a query to examine the ordering patterns of customers, segmenting 
them according to order counts as follows:

* If a customer has placed 10 or more orders, classify as 'High Frequency'.
* For those with 5 to 9 orders, categorise as 'Medium Frequency'.
* Otherwise, designate as 'Low Frequency'.

Display the customer's ID, name, order count, and order frequency 
category. 

**Hint:** Use CASE statement
3\. Employee Performance Analysis
--------------------------------

Write a query to analyse the performance of employees based on their 
total sales contributions. Display the following details, 

* employee ID, 
* employee name, 
* total number of orders
* total sales amount 
* rank based on total sales amount.

**Hint:** Use Window Function *RANK()* or *DENSE_RANK()*

您可以参考以下文章以获取有关聚合函数的更多详细信息,

## SQL 聚合函数,为您的下一个数据科学面试

返回基础知识 | SQL 基础知识

towardsdatascience.com

递归 CTE

现在我们已经熟悉了 CTE,让我们了解一下递归 CTE

递归 CTE 是一种特殊的CTE,它会引用自身并以迭代的方式构建结果集。它就像在 SQL 中使用循环一样。

它由两个部分组成,通过UNION ALL连接:

  • 第一部分,通常也称为基本部分。这一部分提供初始数据或可以说提供递归的起始点。

  • 第二部分是递归部分,它根据第一部分(基本部分)构建结果集,然后在每次迭代中添加更多数据。这个过程会继续,直到没有更多数据可添加或满足特定条件为止。

对于初学者来说,一个常见的问题是何时使用它?好吧!递归 CTEs 在处理可以表示为父子关系的数据时非常有用,比如家谱、嵌套分类、组织层级等。它也用于生成顺序日期系列。

递归 CTE 的常见语法是,

作者提供的图片:递归 CTE 的常见语法

这是来自‘EMPLOYEES’表的示例数据,我们将使用这些数据创建一个递归 CTE来生成员工层级*。

SELECT 
    *
FROM
    EMPLOYEES
LIMIT 10;

作者提供的图片

比如,我们想要检索整个员工层级,以跟踪公司层级结构中每个员工的深度或级别。

首先,我们将选择一个起始点,特定的员工‘Joyce Duffy’,她的‘JOBTITLE’‘首席执行官’,她的‘EMPLOYEEID’‘EMP100’

--cte to define a hierarchical structure in the company
WITH RECURSIVE EMPLOYEEHIERARCHY AS 
(
    --base part
    SELECT 
        EMPLOYEEID, 
        EMPLOYEENAME, 
        JOBTITLE,
        MANAGER,
        1 AS EMPHIERARCHYDEPTH
    FROM 
        EMPLOYEES
    WHERE
        EMPLOYEEID = 'EMP100'

    UNION ALL

    --recursive part
    SELECT 
        EMP.EMPLOYEEID, 
        EMP.EMPLOYEENAME,
        EMP.JOBTITLE,
        EMP.MANAGER,
        EH.EMPHIERARCHYDEPTH + 1
    FROM 
        EMPLOYEES EMP
    JOIN 
        EMPLOYEEHIERARCHY EH 
    ON 
        EMP.MANAGER = EH.EMPLOYEEID
)

--main query
SELECT 
    *
FROM 
    EMPLOYEEHIERARCHY;

作者提供的图片:查询结果

在上面的代码块中,我们创建了一个CTE,‘EMPLOYEEHIERARCHY’。现在让我们关注基本部分。如前所述,我们选择了员工‘Joyce Duffy’作为我们的基点或起始点来构建员工层级列‘EMPHIERARCHYDEPTH’,并将其深度设定为‘1’。这意味着她处于层级结构的最顶层。

作者提供的图片

更高层级(例如 3、4 等)表示由其他员工管理的员工,形成树状结构。‘EMPHIERARCHYDEPTH’列对可视化和理解组织中员工之间的层级关系非常有用。

作者提供的图片:员工层级

现在第二部分,即递归部分是实际发生“魔法”的地方。它回溯到CTEEMPLOYEEHIERARCHY’并重复将‘EMPLOYEES’表与CTE的前一个结果(来自基础部分)连接,使用连接条件‘EMP.MANAGER = EH.EMPLOYEEID’。这意味着它会查找经理的 ID 与当前员工的 ID 匹配的员工,从而创建父子关系链。

要理解递归部分,我们以仅包含市场部员工的信息的数据集为例,

作者提供的图片:市场部员工层级

以下是上述数据集的可视化表示。注意它如何增强清晰度并快速促进理解,是不是?

作者提供的图片:市场部员工层级

回到我们的递归逻辑,我们将‘EMPLOYEES’(别名为‘EMP’)表与递归CTE ‘EMPLOYEEHIERARCHY’(别名为‘EH’)连接,使用条件‘EMP.MANAGER = EH.EMPLOYEEID’。此条件意味着员工的‘MANAGER’必须与前一个迭代的‘EMPLOYEEID’匹配。(此连接帮助我们在层级结构中找到员工及其对应的经理。)

  • 从基础部分,我们已经知道,

作者提供的图片

  • 在递归步骤的第一轮中,连接条件被评估为‘EH.EMPLOYEEID = EMP100’,这意味着它将检查‘EMP.MANAGER = EMP100’(‘EMP.MANAGER = EH.EMPLOYEEID’)。这帮助我们找到所有由‘EMP100’管理的员工。这得到了 2 个结果,为了简化演示,我们将集中于理解市场部的层级结构,

作者提供的图片

  • 对于‘Mrs. Hilary Richardson’,她是‘VP Marketing’‘EMPHIERARCHYDEPTH’增加到2。这是通过将1加到‘EMPHIERARCHYDEPTH + 1’1 + 1 = 2)实现的。这表示我们在层级中向下移动了1级别。

  • 转到递归的第二轮,连接条件现在检查‘EH.EMPLOYEEID = EMP101’。这将列出所有经理是‘EMP101 - Mrs Hilary Richardson’的员工,

作者提供的图片

以类似的方式,查询不断迭代并将行添加到结果集中,直到找不到满足条件 ‘EMP.MANAGER = EH.EMPLOYEEID’ 的匹配项,表明层次结构中没有更多的级别可以遍历。当没有更多的子员工与他们的经理连接时,递归结束。

一个重要的点是,每次递归部分的迭代只在由前一次迭代产生的行上操作。

CTE 与子查询

SQL 中的 子查询 实质上是 “查询中的查询”。有时也称为 嵌套查询内部查询

那么,百万美元的问题来了:哪种方法更好,CTEs 还是 子查询?好吧!没有一刀切的答案。这取决于多个因素,如你的表布局、数据内容、索引策略等。单一的方法或解决方案不能普遍适用于所有用例。子查询CTEs 之间有 3 个主要区别,

  • CTEs 可以递归 CTEs 的一个重要优点是其递归能力。它可以像我们之前讨论的那样引用自身,使其适用于创建层次结构数据结构。然而,子查询 缺乏这种内在的递归能力。

  • 可读性和维护性 在专业领域,SQL 并不总被视为编程语言。遗憾的是,结果通常会遇到比其他编程语言更混乱的嵌套查询。在这种情况下,CTEs 通过整洁地封装逻辑来增强可读性,相比于 子查询;然而,它们可能不会始终提供性能优化。

  • 可重用性 你可以在一个 SQL 查询中多次引用 CTE,这对于复杂的计算或转换非常有用。然而,这种可重用性因数据库而异。而 子查询 每次使用时都必须重写。

让我们快速查询每个客户所订购的产品总数量,

使用子查询:

SELECT
    CUST.CUSTOMERID,
    CUST.CUSTOMERNAME,
    COALESCE((
        SELECT SUM(ORDET.QUANTITYORDERED)
        FROM ORDERS ORD
        JOIN ORDERDETAILS ORDET 
        ON ORD.ORDERID = ORDET.ORDERID
        WHERE ORD.CUSTOMERID = CUST.CUSTOMERID), 0) AS TOTAL_ORDER_QUANTITY
FROM 
    CUSTOMERS CUST;

使用 CTE:

WITH CUSTOMERORDERSUMMARY AS (
    SELECT
        CUST.CUSTOMERID,
        CUST.CUSTOMERNAME,
        COALESCE(SUM(ORDET.QUANTITYORDERED), 0) AS TOTAL_ORDER_QUANTITY
    FROM 
        CUSTOMERS CUST
    LEFT JOIN 
        ORDERS ORD 
        ON CUST.CUSTOMERID = ORD.CUSTOMERID
    LEFT JOIN 
        ORDERDETAILS ORDET 
        ON ORD.ORDERID = ORDET.ORDERID
    GROUP BY 
        CUST.CUSTOMERID, CUST.CUSTOMERNAME
)

SELECT  
   * 
FROM 
   CUSTOMERORDERSUMMARY;

两种方法将产生相同的结果集,

作者提供的 GIF

在现实世界的场景中,没有水晶球,你必须在你正在使用的数据集上测试和测量性能。选择它们之间的方案时,考虑具体的使用情况、查询复杂性和数据库引擎优化是至关重要的。

CTE 与派生表

派生表子查询 的术语经常互换使用。但它们之间有区别。当 子查询 能够独立于外部查询运行时,它就成为了 派生表。与 子查询 不同,派生表 需要指定一个别名。此外,子查询 定义在 ‘WHERE’ 子句中,而 派生表 定义在 ‘FROM’ 子句中。

MySQL 将派生表视为在执行主查询之前计算或实现的中间结果。数据库引擎生成一个执行计划,首先计算派生表,然后在主查询中使用它。

回到CTE派生表,这两种技术用于封装和简化复杂查询。不过,它们在语法和使用上存在差异,

  • 语法

    正如我们已经知道的,CTE 是通过‘WITH’子句定义的;而派生表是在主查询的‘FROM’子句中通过子查询创建的。

  • 可读性

    CTE派生表通过将复杂查询分解为较小的查询来提高可读性。然而,我个人认为CTE稍微容易阅读一些,因为它在语句的开头定义,而不是嵌入在查询中。

  • 递归性 CTE 可以自我引用(递归),正如我们之前讨论的,而派生表则不具备这种能力。

  • 性能

    派生表通常被认为是主查询中的虚拟表,在不同的数据库中可能会引入轻微的性能开销。PostgreSQL 可以优化它们的执行,而MySQL可能将它们视为实现的子查询。另一方面,CTE,某些数据库可以优化它们的执行,尽管这并不普遍适用。例如,SQL Server 通常优化CTE的执行,而MySQL在这方面的行为可能较不一致。

CTE派生表在可读性和简化复杂查询方面提供了或多或少类似的优势。选择它们之间的使用取决于具体的用例和你使用的数据库系统。

让我们找出每个产品的库存总量与销售总量,

使用派生表:

SELECT 
   PRD.PRODUCTNAME, 
   PRD.QUANTITYINSTOCK,
   PS.TOTAL_QUANTITY_SOLD
FROM 
   PRODUCTS PRD
JOIN (
    SELECT
        PRODUCTID,
        SUM(QUANTITYORDERED) AS TOTAL_QUANTITY_SOLD
    FROM ORDERDETAILS
    GROUP BY PRODUCTID
) AS PS
ON PRD.PRODUCTID = PS.PRODUCTID;

使用 CTE:

--cte
WITH PRODUCTSALES AS (
    SELECT
       PRODUCTID,
       SUM(QUANTITYORDERED) AS TOTAL_QUANTITY_SOLD
    FROM 
       ORDERDETAILS
    GROUP BY 
       PRODUCTID
)

--main query
SELECT 
   PRD.PRODUCTNAME, 
   PRD.QUANTITYINSTOCK,
   PS.TOTAL_QUANTITY_SOLD
FROM 
   PRODUCTS PRD
JOIN 
   PRODUCTSALES PS 
   ON PRD.PRODUCTID = PS.PRODUCTID;

两者将产生相同的结果集。

作者提供的 GIF

CTE 与临时表

临时表 是数据库中的特殊类型表,你可以在处理数据时临时存放数据。这些表是会话特定的,意味着它们仅在当前会话中可见,并在会话结束时自动删除。删除数据库不会自动删除在该数据库中创建的任何临时表

CTE临时表之间的主要区别是,

  • 存储和可见性

    临时表在数据库中物理存储。它们超出单个查询的范围,并且在明确删除或会话结束之前保持可用。

    CTE 是虚拟的,并且不会在数据库中物理存储。它们是查询执行的一部分,只存在于该查询的持续时间内。

  • 可重用性

    一些数据库确实允许CTE在同一会话中被多个查询重用。

    临时表 可以在同一会话中的多个查询之间重用,非常适合需要多次引用相同数据的场景。

  • 索引与优化

    CTE 不允许创建索引,因为它们是查询执行计划的一部分。

    临时表 可以被索引,其数据可以优化以提高查询性能。

  • 性能

    由于临时表是物理存储的,它们有时可能会导致磁盘 I/O 和存储开销。它们在管理大量数据时非常有用,并且需要在同一会话中跨多个查询引用这些数据。然而,根据数据大小,它们可能会影响性能。另一方面,CTE通常由查询优化器优化,它们的数据可以驻留在内存中,从而减少磁盘 I/O 并提高性能。当目标是简化复杂查询以提高查询可读性时,CTE是首选。

假设我们想要找出每个产品类别的总销售收入,

使用临时表:

--create a temporary table to store intermediate results
CREATE TEMPORARY TABLE TEMPREVENUECATEGORY AS
SELECT
    PRD.PRODUCTCATEGORY,
    SUM(ORDET.QUANTITYORDERED * PRD.BUYPRICE) AS TOTAL_REVENUE
FROM
    PRODUCTS PRD
JOIN
    ORDERDETAILS ORDET ON PRD.PRODUCTID = ORDET.PRODUCTID
GROUP BY
    PRD.PRODUCTCATEGORY;

--select data from the temporary table
SELECT 
  * 
FROM 
  TEMPREVENUECATEGORY;

使用 CTE:

--use CTE to calculate total revenue by product category
WITH REVENUECATEGORY AS (
    SELECT
        PRD.PRODUCTCATEGORY,
        SUM(ORDET.QUANTITYORDERED * PRD.BUYPRICE) AS TOTAL_REVENUE
    FROM
        PRODUCTS PRD
    JOIN
        ORDERDETAILS ORDET ON PRD.PRODUCTID = ORDET.PRODUCTID
    GROUP BY
        PRD.PRODUCTCATEGORY
)

--select data from the CTE
SELECT 
   * 
FROM 
   REVENUECATEGORY;

两个查询产生类似的结果,

图片由作者提供

最终挑战

**Analysing Customer Orders
-------------------------**

Write a query to find the customer or customers with the highest total 
order value for 'shipped' orders. Your query should include the following 
details for the customer with the highest value order,

* Customer ID 
* Customer Name
* Total count of orders
* Total value of the orders placed by the customer
* Sales representative employee for the customer

**Hint:** Refer to the ER Diagram provided at the start of the article to 
grasp the interconnections between tables. Create a list of the columns 
you intend to retrieve, present them in tabular format, and cross-
reference the diagram to identify the corresponding fields available 
in each table.

在回复部分分享你的答案,如果需要帮助,随时留下评论!

结论

无论你是初学者还是经验丰富的SQL从业者,编写查询有点像在厨房里做菜。你最喜欢的厨师刀可能在切菜时表现出色,但同样的刀可能不适合嫩化肉类。(这个比喻的出处归功于我对《顶级厨师》的狂热追看。)同样,你常用的技术在某些情况下可能表现优异,但在其他情况下可能效果不佳。这完全是关于评估用例并选择合适的技术以获得最佳解决方案。请务必参考你正在使用的数据库的官方文档。

这里有一些有用的资源可以帮助你入门CTE

本文使用的源数据、代码文件和练习题的解决方案可以在我的 GitHub 仓库.

成为会员并阅读 Medium 上的所有故事.

学习愉快!

时间序列预测中交互项的全面指南

原文:towardsdatascience.com/a-comprehensive-guide-on-interaction-terms-in-time-series-forecasting-16bfa468ae

图片由 Midjourney 创建

了解如何通过使线性模型对趋势变化更加灵活来提高模型的拟合度

Eryk LewinsonTowards Data Science Eryk Lewinson

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

--

建模时间序列数据可能具有挑战性(也很有趣),因为其固有的复杂性和不可预测性。例如,时间序列中的长期趋势可能由于某些事件而发生剧烈变化。回想全球疫情初期,航空公司或实体店等业务的客户数量和销售额迅速下降。相比之下,电子商务企业在较少的干扰下继续运营。

交互项有助于建模这些模式。它们捕捉变量之间的复杂关系,从而导致更准确的预测。

本文探讨:

  • 交互项在时间序列预测中的应用

  • 在建模复杂关系时,交互项的好处

  • 如何在模型中有效实施交互项

交互项概述

交互项使你能够调查目标与特征之间的关系是否会根据另一个特征的值而变化。欲了解更多详情,请参阅 我之前的文章

图 1 展示了一个散点图,表示每加仑行驶里程(目标)与车辆重量(特征)之间的关系。根据变速器类型(另一个特征),关系会有所不同。

图 1. 各种车辆变速器类型的最佳拟合线,包括交互项

如果不使用交互项,线性模型将无法捕捉到如此复杂的关系。实际上,它将为权重特征分配相同的系数,无论传输类型如何。图 1 显示了按权重特征的系数(线的斜率),不同传输类型的系数差异极大。

为了克服这种谬误并使线性模型更具灵活性,我们可以使用交互项。通常,它们是原始特征的乘积。通过将这些新变量添加到回归模型中,我们可以测量它们与目标之间的交互效果。

时间序列预测中的交互项

交互项使线性模型更具灵活性。以下示例展示了它们在时间序列预测中的作用。

先决条件

首先,我们加载所需的库:

import numpy as np
import pandas as pd

from sklearn.linear_model import LinearRegression

import seaborn as sns 
import matplotlib.pyplot as plt

数据集生成

然后,我们生成一些具有以下特征的人工时间序列数据:

  • 10 年的日数据

  • 时间序列中的重复模式(季节性)

  • 前 7 年的下降趋势

  • 最后 3 年没有趋势

  • 随机噪声,作为最后一步添加

# for reproducibility
np.random.seed(42)

# generate the DataFrame with dates
range_of_dates = pd.date_range(
    start="2010-01-01",
    end="2019-12-30"
)
df = pd.DataFrame(index=range_of_dates)

# create a sequence of day numbers
df["linear_trend"] = range(len(df))
df["trend"] = 0.004 * df["linear_trend"].values[::-1]
df.loc["2017-01-01":, "trend"] = 4

# generate the components of the target
signal_1 = 10 + 4 * np.sin(df["linear_trend"] / 365 * 2 * np.pi)
noise = np.random.normal(0, 0.85, len(df))

# combine them to get the target series
df["target"] = signal_1 + noise + df["trend"]

# plot
df["target"].plot(title="Generated time series");

图 2 显示了生成的时间序列,涵盖了所有期望的特征。

图 2. 生成的时间序列

训练基准模型

现在,我们将训练一个线性模型并检查最佳拟合线。在本文中,我们只创建几个特征的简单模型。这将使我们能够直观地检查交互项对模型拟合的影响。

最简单的模型只包含一个特征——时间的指示符。为时间序列创建的linear_trend列实际上是 DataFrame 的行号(按日期排序)。

X = df[["linear_trend"]]
y = df[["target"]]

lm = LinearRegression()
lm.fit(X, y)

df["model_1"] = lm.predict(X)

df[["target", "model_1"]].plot(title="Linear trend");

值得一提的是,这里的重点不是使用独立的训练和测试集来正确评估预测,而是解释交互项对模型拟合的影响。通过检查拟合值(训练集上的预测)并将这些拟合值与原始时间序列进行比较,可以更容易地观察交互项的影响。

图 3 显示线性模型识别了整个时间序列的下降趋势。同时,对于最后 3 年的数据,拟合效果似乎不佳,因为那里没有趋势。

图 3. 从线性模型中获得的最佳拟合线,使用线性趋势作为特征

添加断点

接下来,我们尝试通过特征工程让模型学习新的模式(趋势变化)。为此,我们创建了一个断点变量,表示给定观测是否在 2017 年 1 月 1 日之后。在这种情况下,我们知道趋势变化发生的确切时间点。

接下来,我们训练另一个线性模型,这次使用两个特征:

df["after_2017_breakpoint"] = np.where(df.index >= pd.Timestamp('2017-01-01'), 1, 0)

X = df[["linear_trend", "after_2017_breakpoint"]]
y = df[["target"]]

lm = LinearRegression()
lm.fit(X, y)

df["model_2"] = lm.predict(X)

df[["target", "model_2"]].plot(title="Linear trend + breakpoint");

图 4. 使用线性趋势和断点作为特征从线性模型获得的最佳拟合线

图 4 展示了一些重要的变化,如下所列:

  • 拟合线显示了一个垂直跳跃,这对应于新布尔特征的系数。

  • 垂直跳跃恰好发生在特征变为激活(值为 1 而非 0)的第一个日期。

  • 在引入的断点之前和之后,线的斜率是相同的。

  • 模型试图通过在断点之后的预测中添加固定量来弥补不正确的斜率。

在最后 3 年的数据中没有趋势,因此理想情况下,2017 年 1 月 1 日后线应接**坦。

添加交互项

为了在断点后改变斜率,我们添加了对时间戳的更复杂的依赖(由线性趋势表示)。这正是交互项的作用——它是线性趋势和占位符变量的乘积。

df["interaction_term"] = df["after_2017_breakpoint"] * df["linear_trend"]

X = df[["linear_trend", "after_2017_breakpoint", "interaction_term"]]
y = df[["target"]]

lm = LinearRegression()
lm.fit(X, y)

df["model_3"] = lm.predict(X)

df[["target", "model_3"]].plot(title="Linear trend + breakpoint + interaction term"); 

图 5. 使用线性趋势、断点和交互项作为特征从线性模型获得的最佳拟合线

图 5 显示了在模型中添加交互项的影响。与图 4 相比,最佳拟合线在断点后的斜率不同。

更准确地说,差异实际上是交互项的系数值。虽然新线并没有完全*坦,但它在时间序列的早期部分仍然比以前的斜率更*缓。

引入断点和交互项一起提高了模型捕捉时间序列趋势的能力。反过来,这应增加模型的预测性能。

总结

  • 使用交互项可以使线性模型的规格更加灵活(不同的斜率用于不同的线),这可能会导致对数据的更好拟合和更好的预测性能。

  • 我们可以将交互项添加为原始特征的乘积。

  • 在时间序列的背景下,我们可以使用交互项更好地捕捉趋势的变化。

你可以在我的 GitHub 上找到本文使用的代码。此外,笔记本中的代码展示了如何利用 cuDF 和 cuML 使用 GPU 加速训练模型。如常,欢迎反馈。你可以通过 Twitter 或在评论中与我联系。

喜欢这篇文章?成为 Medium 会员继续阅读无限制地学习。如果你使用 这个链接 成为会员,你将以零额外费用支持我。提前感谢,并期待再次见面!

你可能还对以下内容感兴趣:

解锁线性回归中的交互项的威力

学习如何通过包括交互项使你的线性模型更加灵活

towardsdatascience.com ## 将 VS Code 转变为机器学习实验的一站式工具

如何在不离开 IDE 的情况下运行和评估实验

towardsdatascience.com ## 帮助你进行时间序列分析的 5 种图表类型

以及如何使用 Python 快速创建这些图像!

towardsdatascience.com ## 时间序列特征工程的三种方法

使用虚拟变量、周期编码和径向基函数

towardsdatascience.com

参考文献

除非另有说明,所有图片均由作者提供。

最初发布于 NVIDIA 的开发者博客 于 2023 年 7 月 20 日

构建企业级 Plotly Dash 应用程序的全面指南

原文:towardsdatascience.com/a-comprehensive-guide-to-building-enterprise-level-plotly-dash-apps-bd40dfe1313c

使用纯 Python 和 Docker 构建生产就绪的 web 应用程序

Janik 和 Patrick TinzTowards Data Science Janik 和 Patrick Tinz

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

--

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

数据科学项目总是需要某种形式的可视化。对于初步分析,数据科学家通常使用 Jupyter Notebooks 和像 matplotlib 或 seaborn 这样的库。在探索性数据分析中,数据科学家使用直方图、散点图或进行统计评估。对于初步见解,这种方法非常合适。然而,交互式仪表板更适合呈现结果。许多客户正是需要这种功能!交互式仪表板是以易于理解的方式解释结果的有效方法。

但创建交互式仪表板并非易事。在我们看来,Plotly Dash 是创建令人印象深刻图表的最佳选择。对于生产就绪的仪表板应用程序,您必须考虑其他方面(例如使用 Docker 部署)。

在本文中,我们想分享使用 Plotly Dash 构建结构良好的仪表板应用程序的最佳实践。此外,我们展示了如何使用 Docker 清洁地部署 Dash 应用程序。我们始终欢迎改进建议,请在评论中写下你的想法!

为什么选择 Plotly Dash?

Plotly Dash 是一个用于构建基于网页应用的高效 Python 框架。这个开源库使用宽松的 MIT 许可证。它建立在 FlaskPlotly.jsReact.js 之上。你可以用 PythonRJuliaF# 创建和部署具有自定义用户界面的网页应用。该框架抽象了创建全栈网页应用所需的协议和技术。

优点

  • 你可以用纯 Python 实现网页接口。不需要 JavaScript!

  • Dash 是响应式的!你可以实现具有多个 Inputs、多个 Outputs 和依赖于其他 Inputs 的 Inputs 的复杂 UI。

  • Dash 应用是多用户应用。多个用户可以在独立会话中查看 Dash 应用。

  • Dash 是建立在 React.js 之上的。你可以用 React 实现并使用你自己的 Dash 组件。

  • Dash 应用使用 Flask 作为后端,因此你可以使用 Gunicorn 来运行它们。Gunicorn 允许你通过增加工作进程的数量,将 Dash 应用扩展到成千上万的用户。

  • 开源框架(使用宽松的 MIT 许可证)。

  • 出色的文档和社区(Dash Community ForumGitHub)。

缺点

  • 回调函数必须有 Inputs 和 Outputs。

  • 两个回调函数不能更新同一个输出元素。

每个框架都有缺点,但 Dash 的缺点可以通过解决方法来克服。优点大于缺点!

为什么选择 Docker?

你可以使用 Docker 隔离应用程序。它使用一种称为容器虚拟化的概念。应用程序可以轻松地用 Docker 部署,因为轻量级容器包含所有必要的包。容器共享单一操作系统内核的服务,因此它们比虚拟机使用更少的资源。

Docker 使部署 Dash 应用变得简单。使用 Docker,你可以将 Dash 应用部署到所有架构(amd64、i386、arm64、arm)。这种方法使你不依赖于部署环境(本地或云)。

模型视图控制器模式

模型视图控制器(MVC)是一种将软件划分为三个组件的模式:ModelViewController

模型视图控制器架构(图源自作者)

模型组件包含业务逻辑。这个组件与数据库或其他后端组件进行通信。视图组件展示数据。需要注意的是,视图与模型没有直接的连接。控制器形成了这种连接。控制器负责数据处理。控制器用一个或多个模型中的数据更新视图。

优点

  • 并行开发:不同的开发者可以实现各自的组件。

  • 可扩展性: 针对相同数据模型的多个视图

  • 避免复杂性: 将应用程序分成独立的 MVC 单元

  • 概念的清晰分离: 具体任务的逻辑分组

缺点

  • 模型和控制器之间的强依赖

最佳实践:Dash 应用的项目结构

我们通过一个示例 Dash 应用展示我们的最佳实践。可以自由使用此示例作为你下一个 Dash 应用的基础。

我们推荐在虚拟环境中工作(例如 conda)。请在你的系统上安装 conda。创建虚拟环境以保持你的主系统的清洁。

创建并激活 conda 环境:

$ conda create -n dash-app python=3.9.12
$ conda activate dash-app

Web 应用由许多组件和页面组成。我们建议将各个概念分成几个文件夹和文件。这种方法大大简化了 Web 应用的维护。

我们推荐以下结构:

.
├── dash-app              
│   └── assets              # this folder contains style files
│   │   ├── style.py
│   │   └── typography.css
│   ├── components          # this folder contains reusable components
│   │   ├── dropdown.py
│   │   └── navbar.py
│   ├── environment         # this folder contains environment settings
│   │   ├── .env
│   │   ├── .env_development
│   │   └── settings.py
│   ├── pages               # this folder contains the pages
│   ├── plots               # this folder contains different plots
│   ├── utils               # this folder contains helper functions
│   ├── app.py
│   ├── Dockerfile
│   ├── index.py
│   └── requirements.txt

我们将从上到下详细介绍各个文件夹和文件。

assets 文件夹

该文件夹包含你的 Dash 应用的样式信息(例如 CSS、JavaScript 文件或 favicon.ico)。Dash 会自动提供所有文件,只要你将文件夹命名为 assets

style.py

# main style of the app
MAIN_COLORS = {
    'primary': '#165AA7',
    'secondary': '#000000',
    'third': '#FFFFFF',
}

style.py 文件中,我们定义应用程序的配色方案。Python 字典是一个不错的选择。我们可以轻松地从其他文件访问字典信息。有关更多样式信息,你可以轻松创建另一个字典。

typography.css

body {
    font-family: sans-serif;
}

h1, h2, h3, h4, h5, h6 {
    text-align: center;
}

typography.css 文件包含排版信息。

components 文件夹

该文件夹包含所有可重用的组件(例如下拉菜单、按钮或表格)。其优点是你可以在多个页面上使用这些组件。

dropdown.py

from dash import dcc

def render_dropdown(dropdown_id: str, items=[''], clearable_option=False):
    dropdown = dcc.Dropdown(
        id=dropdown_id,
        clearable=clearable_option,
        options=[{'label': i, 'value': i} for i in items],
        value=items[0],
    )
    return dropdown

对于下拉菜单,我们使用 Dash Core Components。每当我们需要一个下拉菜单时,可以使用 render_dropdown() 函数。其优点是所有下拉菜单具有相同的样式。

navbar.py

import dash_bootstrap_components as dbc

from environment.settings import VERSION

# import own style (see /assets)
from assets.style import MAIN_COLORS

navbar = dbc.NavbarSimple(
    children=[
        dbc.NavItem(dbc.NavLink("Dashboard", href="/dashboard")),
    ],
    brand="Gapminder " + VERSION,
    brand_href="/",
    color=MAIN_COLORS["primary"],
    sticky='top',
    links_left=True,
    dark=True
)

导航栏包含指向各个页面的链接。对于导航栏,我们使用 Dash Bootstrap Components。你可以单独设计 dbc.NavbarSimple() 组件。

environment 文件夹

不同的环境有不同的配置文件。包括 Dev、Staging、Prod 等。该文件夹包含不同的环境文件。在我们的例子中,我们有一个开发环境(.env_development)和一个生产环境(.env)。

.env

VERSION=1.0.0

该文件包含生产参数。我们在网页界面中稍后使用 VERSION 参数来查看哪个环境是活动的。

.env_development

VERSION=1.0.0-dev
HOST=127.0.0.1
PORT=7000
DEBUG=True

该文件包含开发参数。HOSTPORTDEBUG 参数将用于本地开发服务器。

settings.py

import os
from dotenv import load_dotenv

env_path = os.path.join(os.path.dirname(__file__), os.getenv('ENV_FILE') or ".env_development")
load_dotenv(dotenv_path=env_path, override=True)

VERSION = os.environ.get("VERSION")

APP_HOST = os.environ.get("HOST")
APP_PORT = os.environ.get("PORT")
APP_DEBUG = bool(os.environ.get("DEBUG"))

在这个文件中,我们读取环境配置。为此,我们使用 Python 包 python-dotenv。首先,我们根据环境变量 ENV_FILE 读取正确的配置文件。对于本地开发,我们使用 .env_development。通过 ENV_FILE 环境变量,我们可以定义相应的环境。我们稍后在 Dockerfile 中设置 ENV_FILE 变量。在我们的例子中,.env 是生产环境。

pages 文件夹

一个网页应用程序通常由多个页面组成。我们建议为每个页面创建一个文件夹。每个页面文件夹包含三个文件,以应用 MVC 模式。一个页面有一个模型、视图和控制器文件。这样我们就有了概念的清晰分离。

dashboard/dashboard_controller.py

from dash.dependencies import Input, Output
from app import app
from pages.dashboard.dashboard_model import map_dataframe

# import components
from plots.map_plot import *

@app.callback(
    Output(component_id='div-vis', component_property='children'),
    Input(component_id='dropdown-choose-item', component_property='value')
)
def update_vis(variable):
    df = map_dataframe()
    fig = bubble_map(df, variable)

    return fig

控制器是视图和模型之间的接口。控制器响应网页界面的事件。此外,控制器从模型中获取数据。最后,控制器将结果返回给网页界面。

dashboard/dashboard_model.py

import plotly.express as px

def get_map_data():
     df = px.data.gapminder()
     return df

def map_dataframe():
    return get_map_data()

dashboard_model.py 文件中,我们通过 plotly.express.data 包的内置数据集 gapminder 获取数据。

dashboard/dashboard_view.py

import dash_bootstrap_components as dbc
from dash import html

# import components
from components.dropdown import render_dropdown
from components.navbar import navbar

def render_dashboard():
    return html.Div([
        navbar,
        html.Div(
            [
                html.Br(),
                dbc.Container(
                    fluid=True,
                    children=[
                        dbc.Row(
                            [
                                dbc.Col(
                                    width=2,
                                    children=dbc.Card(
                                        [
                                            dbc.CardHeader("Variables"),
                                            dbc.CardBody(
                                                [
                                                    render_dropdown(dropdown_id="dropdown-choose-item", items=['Population', 'Life expectancy', 'GDP per capita'])
                                                ]
                                            )
                                        ],
                                        style={'height': "84vh"},
                                    )
                                ),
                                dbc.Col(
                                    width=10,
                                    children=dbc.Card(
                                        [
                                            dbc.CardHeader("World map"),
                                            dbc.CardBody(
                                                [
                                                    html.Div(id='div-vis')
                                                ]
                                            )
                                        ],
                                        style={'height': '84vh'}
                                    )
                                )
                            ]
                        )
                    ]
                ),
            ]
        )
    ])

在这个文件中,我们定义了网页界面的外观。在这种情况下,我们使用来自组件文件夹的下拉菜单和导航栏。

page_not_found.py

from dash import html

def page_not_found():
    return html.Div([
        html.H1('404'),
        html.H2('Page not found'),
        html.H2('Oh, something went wrong!')
    ])

当页面未找到时,会显示这个页面。例如,如果你在 URL 中输入了错误的路径。

plots 文件夹

这个文件夹包含了应用程序的不同图表。我们建议为每种图表类型创建一个新的文件。

map_plot.py

from dash import dcc
import plotly.express as px

def bubble_map(df, variable):
    dict_variable = {'Population':'pop', 'Life expectancy':'lifeExp', 'GDP per capita':'gdpPercap'}
    variable = dict_variable[variable]

    fig = px.scatter_geo(df, locations="iso_alpha", color="continent",
                     hover_name="country", size=variable,
                     animation_frame="year",
                     projection="natural earth")

    return dcc.Graph(figure=fig)

我们使用 Plotly Express 绘制地图图表。

utils 文件夹

这个文件夹包含可以通用的辅助函数和组件。例如,一个连接到其他服务(例如 RESTful 服务)的连接器。

app.py

import dash
import dash_bootstrap_components as dbc

APP_TITLE = "Plotly Dash"
app = dash.Dash(__name__,
                title=APP_TITLE,
                update_title='Loading...',
                suppress_callback_exceptions=True,
                external_stylesheets=[dbc.themes.FLATLY])

在这个文件中,我们创建了 Dash 实例 dash.Dash()。我们有一个动态布局,因此我们将 suppress_callback_exceptions 设置为 True。此外,我们使用来自 Dash Bootstrap Components themes 的 FLATLY 主题。

index.py

from dash import dcc
from dash import html
from dash.dependencies import Input, Output

# import pages
from pages.dashboard.dashboard_view import render_dashboard
from pages.dashboard.dashboard_controller import *
from pages.page_not_found import page_not_found

from app import app

from environment.settings import APP_HOST, APP_PORT, APP_DEBUG

server = app.server

def serve_content():
    return html.Div([
        dcc.Location(id='url', refresh=False),
        html.Div(id='page-content')
    ])

app.layout = serve_content()

@app.callback(Output('page-content', 'children'),
              Input('url', 'pathname'))
def display_page(pathname):
    if pathname in '/' or pathname in '/dashboard':
        return render_dashboard()
    return page_not_found()

if __name__ == '__main__':
    app.run_server(debug=APP_DEBUG, host=APP_HOST, port=APP_PORT)

这个文件是 Dash 应用程序的入口点。对于 Gunicorn,重要的是定义 server = app.server。这设置了应用程序的 Flask 服务器。当页面发生变化时,函数 display_page() 将被触发。对于开发环境,我们将 app.run_server() 的参数从开发环境文件中传递。

requirements.txt

dash==2.9.1
dash-bootstrap-components==1.4.1
gunicorn==20.1.0
python-dotenv==1.0.0
geopandas==0.13.0

这个文件包含所有必需的依赖项。请使用以下命令安装这些依赖项:

$ pip install -r requirements.txt

现在我们准备开始应用程序。

本地开发

导航到 dash-app 文件夹并执行以下命令:

$ python index.py

Dash 应用程序启动。你可以在 127.0.0.1:7000 打开应用程序。

仪表板开发版本(作者截图)

我们可以看到在开发环境中设置的版本号 1.0.0-dev。Dash 在右下角提供调试信息。在开发 Dash 应用程序时,Dash 开发工具 被启用。

Docker 化 Dash 应用程序(生产环境)

注意: 请在您的系统上安装 Docker

Dockerfile

FROM python:3.9.12

# Create non-root group and user
RUN addgroup --system shared1 \
    && adduser --system --home /var/cache/shared1 --ingroup shared1 --uid 1001 dashuser

WORKDIR /usr/share/shared1/dashapp

COPY requirements.txt /usr/share/shared1/dashapp/

# Elegantly activating a venv in Dockerfile
ENV VIRTUAL_ENV=/usr/share/shared1/dashapp/venv
RUN python3 -m venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"

# Install requirements
RUN pip install --trusted-host pypi.python.org -r requirements.txt

COPY . /usr/share/shared1/dashapp/

# set enviroment variables
# This prevent Python from writing out pyc files
ENV PYTHONDONTWRITEBYTECODE=1
# This keeps Python from buffering stdin/stdout
ENV PYTHONUNBUFFERED=1

ENV ENV_FILE=".env"

EXPOSE 7000

USER dashuser

ENTRYPOINT ["gunicorn", "index:server", "-b", "0.0.0.0:7000", "--workers=4"]

在 Dockerfile 中,我们创建了一个用户和一个虚拟环境。虚拟环境有助于控制 Python 依赖项。它还保持本地开发环境与容器应用程序之间的差异较小。Itamar Turner-Trauring 的教程 “优雅地在 Dockerfile 中激活 virtualenv” 介绍了如何在 Dockerfile 中激活虚拟环境。如果您感兴趣,请阅读!虚拟环境不会减慢 Dash 应用程序的速度。此外,我们也不太可能随着时间的推移遇到奇怪的 bug(例如,操作系统层面的变化)。在最后一行中,我们定义了入口点。主机必须是 0.0.0.0 以便 Dash 应用程序可以访问。

现在,您可以使用以下命令构建应用程序:

docker build -t dash-app:latest .

使用以下命令运行应用程序:

docker run --name dashboard -d -p 7000:7000 dash-app

现在,您可以在 0.0.0.0:7000 打开应用程序。

仪表板生产版本(作者截图)

我们可以看到在生产环境文件中设置的版本号 1.0.0。Docker 允许我们在云端或本地环境中以轻量级容器的形式部署 Dash 应用程序。

结论

在本文中,我们提出了一个构建 Dash 应用程序的概念。我们展示了如何应用模型-视图-控制器模式,以实现概念的清晰分离。这种方法有助于维护 Dash 应用程序。我们还展示了如何使用不同的环境。最后,我们介绍了一种在 Docker 容器中部署 Dash 应用程序的简洁方法。

👉🏽 加入我们的免费每周 Magic AI 通讯,获取最新的 AI 更新!

👉🏽 您可以在我们的数字产品页面找到所有免费的资源!

免费订阅 以便在我们发布新故事时收到通知:

[## 每当 Janik 和 Patrick Tinz 发布新内容时,您将收到一封电子邮件。

每当 Janik 和 Patrick Tinz 发布新内容时,您将收到一封电子邮件。通过注册,如果您还没有 Medium 账户,系统将为您创建一个账户……

tinztwinspro.medium.com](https://tinztwinspro.medium.com/subscribe?source=post_page-----bd40dfe1313c--------------------------------)

在我们的关于页面了解更多关于我们的信息。别忘了在X上关注我们。非常感谢你的阅读。如果你喜欢这篇文章,随时分享它。祝你有美好的一天!

使用我们的链接注册成为 Medium 会员,即可阅读无限量的 Medium 故事。

使用 Pandas 进行数据处理的全面指南

原文:towardsdatascience.com/a-comprehensive-guide-to-using-pandas-in-python-4bc32a14f2ec

了解如何使用 Python 中最著名的数据处理库之一

Ivo BernardoTowards Data Science Ivo Bernardo

·发布在 Towards Data Science ·阅读时间 21 分钟·2023 年 7 月 18 日

--

照片由 stonewyq @ Unsplash.com

当你开始在数据分析、工程或科学的上下文中使用 Python 时,pandas(可能)是你必须学习的第一个库之一。这个不可思议的库使你能够操作 Python 语言中的两个非常重要的对象——一维的Series和二维的DataFrame。这些对象是许多数据管道的一部分,掌握它们对开始你的 Python 职业生涯至关重要。

数据框在数据科学和分析中广泛使用,因为它们允许创建多维和多类型的对象。本文的目标是提供一个非常完整的指南,介绍如何使用一些著名的pandas函数以及如何操作库中最重要的功能。希望在阅读完本指南后,你能准备好使用最重要的pandas功能。你可能也很常从 SQL 背景迁移过来,所以我会尽量在一些说明中提供与 SQL 代码的比较,以便更容易比较两个框架之间的指令。但是,请记住,了解 SQL 并不是学习pandas的必要条件!

在本文中,我们将使用各种数据来深入了解pandas,即:

  • 我们将通过对象创建命令构建自己的pandas Series 和 DataFrames。

  • 我们将使用包含股票价格信息的三个数据集,这些数据集可以在这里找到(www.kaggle.com/datasets/rprkh15/sp500-stock-prices)——具体来说,我们将使用福特、苹果Abbvie的股票价格数据。

在本文中,我们将介绍pandas最著名的功能,即:

  • 创建数据框

  • 选择行

  • 选择列

  • 合并数据框

  • 绘制数据

  • 数据分组

  • 链接函数

话不多说,让我们开始吧!

导入库

在我们开始之前,我们需要将 pandas 库导入到 Python 环境中,以确保我们可以使用文章中会看到的所有函数。

如果你是 Python 新手,这是语言中的一个标准。我们必须在代码中使用外部库之前导入它们,因为在安装 Python(在 Windows 上)或在基本版本(可在 Mac 或 Ubuntu 上获得)时,外部库在我们的环境中不可用。这种库的概念在开源语言中很常见,因为这是社区用来扩展语言基础功能的主要方式。

导入 pandas 很简单,我们只需在 Python Notebook 或可执行文件中运行以下代码:

import pandas as pd

现在你一定会问自己……为什么 pandas as pd?主要是为了让我们使用库的函数时可以使用 别名。由于我们每次在代码中调用函数时都需要提到库的名称,使用像 pd 这样的较短 别名 会更方便。Pandas 用户通常使用这个标准,你可以在互联网上看到许多相关的代码。

Pandas Series

不,Pandas 没有电视节目!

pandas Series 是一个一维对象,能够一次存储一种数据类型。例如,让我们使用 pd.Series 函数创建一个简单的 pandas Series,其中包含不同的整数:

pd.Series([10,20,30,40,50])

pd.Series 命令的输出——作者图片

让我在这里暂停一下。pd.Series 意味着我们在调用从 pd(pandas 的别名!)库中加载的 Series 方法。这是 Python 语言中的标准语法:library.method

我们的 Series 包含 5 个整数:10、20、30、40、50。在命令的输出中,我们还看到了 dtype: int 64。默认情况下,pandas 存储整数时使用 64 位整数,这允许在定义数字时拥有更大的值范围。

除了整数,我们还可以通过在 pd.Series 中传递带小数点的数字来存储浮点数:

pd.Series([10.1,20.2,30.4,40.6,50.2])

pd.Series 命令的浮点数输出——作者图片

注意我们的 dtype 现在是 float64。对这些值进行的任何计算将保持数值精度。稍后我们将看到这一点!

自然,我们也可以在 pd.Series 对象中存储字符串:

pd.Series([‘Portugal’, ‘Spain’, ‘France’])

pd.Series 命令的字符串输出——作者图片

不要太困惑,不过在 pandas 中,字符串是以 object 数据类型存储的。实际上,大多数数据(除了基于时间的数据类型、分类数据或布尔值)如果既不是 integers 也不是 floats,都将以 object 数据类型存储——例如,定义一个包含嵌套列表的 Series

pd.Series([[1,2], [2,3], [1,2]])

pd.Series 命令输出的列表 — 作者提供的图片

如果你不熟悉 Python 中的列表,不必过于担心。这是一个相当不常见的操作,我只是做了这个实验来展示object数据类型并不限于文本数据。

一个警告 — 当你尝试在pd.Series中定义字符串和数字的混合时要小心,因为这会将整个对象转变为字符串(对象):

pd.Series([1,2,’ABC’])

混合数据类型的 pd.Series 命令输出 — 作者提供的图片

在 Python 中,我们可以使用赋值操作符=来保存我们的对象。让我们将这个最后的系列保存到名为series_1的对象中,以便我们可以方便地在其上调用pandas方法:

series_1 = pd.Series([1,2,’ABC’])

我们可以访问pd.Series对象的一个属性是dtype属性。访问dtype会输出我们 Series 中存储的数据类型:

series_1.dtype

数据类型对象 — 作者提供的图片

这将打印出我们的系列包含的对象类型 — 在series_1示例中,它存储的数据类型是'O',表示对象。

pandas指南的第一部分中,我们已经看到了pd.Series,这种对象类型可以简要描述为:

  • 系列是一维对象,每次只能存储一个维度。

  • 它们一次只能容纳一种数据类型。

但是……我们的指南中还有很多内容要涵盖!例如,如果我们想从pd.Series中检索特定元素怎么办?我们可以在 pandas 中做到吗?

是的,我们可以!为了做到这一点,我们需要依赖于索引的概念,这将在接下来看到!

索引系列

pandas的一个巧妙技巧是我们可以将索引附加到我们的数据上,并使用值来从对象中检索元素。

例如,让我创建一个包含一些国家首都pandas系列:

countries_series = pd.Series(data=[‘Lisbon’,’Madrid’,’Paris’], index = [‘Portugal’,’Spain’,’France’])

注意我们在series中有了新东西 — 索引参数!这个参数解锁了为我们的值提供自定义索引的能力。

countries_series 对象 — 作者提供的图片

左侧是索引的值。右侧是存储在pandas Series中的值 — 每个城市的首都的文本值。

现在,假设我们想从countries_series中检索Portugal的首都 —— 在使用命名索引之前,一种方法是提供数字索引,如下所示:

countries_series[0]

数字索引示例输出 — 作者提供的图片

即使我们没有为series提供任何自定义索引,数字索引始终有效!

但当然,这并不是很实际。如果我们的表格中有 200 个国家怎么办?我们如何在不记住 200 个索引的情况下检索数据?

幸运的是,我们还可以使用命名索引来通过在方括号中提供自定义索引来检索数据:

countries_series[‘Portugal’]

命名索引示例的输出——作者提供的图片

这是一种更有意义的从我们的pandas系列中提取数据的方法。通过使用索引,我们可以从对象中提取特定的数据点。我们常用的另一种pandas技术是使用列表同时提取多个元素:

countries_series[[0,1]]

多重数值索引示例的输出——作者提供的图片

在上面的示例中,我们从countries_series中提取了两个数据点——位置 1 和 2 的元素。记住,Python 索引是从 0 开始的。

使用命名索引,我们还可以通过在索引中传递列表来传递多个元素:

countries_series[[‘Portugal’,’Spain’]]

多重数值索引示例的输出——作者提供的图片

了解系列对象是理解 pandas 中核心对象——数据框(dataframe)的关键步骤!我们已经看到系列对象只能存储一维数据,每次存储一种类型的数据。这是一个重大障碍,会影响我们在数据管道中的生产力,因此我们需要研究一个更复杂的对象!接下来我们来看看这个对象。

创建数据框

到目前为止,我们只处理了一维和单一类型的对象。当然,这不太实用,特别是当我们想处理更复杂的数据时。

创建数据框相对简单——我们可以使用pd.DataFrame函数创建一个:

df_example = pd.DataFrame([24, 23, 22])

我的df_example包含一列和三行整数:

数据框对象示例——作者提供的图片

数据框的一个好处是它们支持二维对象(行和列)。让我们在数据中添加另一列,这是Series不支持的:

df_example = pd.DataFrame([['John','Joe','Anne'],
                          [24, 23, 22]])

我们的df_example现在看起来如下:

两行数据框——作者提供的图片

哦哦!我们的pd.DataFrame命令逐行添加数据。注意我们传递了一个包含数据的列表:[['John','Joe','Anne'], [24,23,22]]

我们有两种方法来纠正这种行为,要么转置我们的DataFrame,要么更改我们将提供给pd.DataFrame的数据结构——我们先从转置dataframe开始:

df_example = pd.DataFrame([['John','Joe','Anne'],
                          [24, 23, 22]]).T

这个操作会产生以下结果:

两列数据框——作者提供的图片

调用数据框的.T属性会转置对象,将行和列互换。以这种格式将数据传递到pd.DataFrame的更有意义的方法是使用 Python 字典的力量:

df_example = pd.DataFrame({'students_name':['John','Joe','Anne'],
                          'age':[24, 23, 22]})

下面是这个数据框所创建内容的解释:

  • students_name将包含三个名字:JohnJoeAnne

  • age将包含三个年龄:24、23 和 22。

带有列名的数据框——作者提供的图片

对于这个对象,我们立即给列命名:students_nameage。列名将采用我们传递的字典的键的值。

我们知道,在这个表格中,每个学生只有一行——将学生姓名作为我们的索引会很有趣,这样我们可以利用pandas对象中的索引属性。我们可以通过将学生姓名声明为索引来实现这一点:

df_example = pd.DataFrame({'age':[24, 23, 22]},
                         index=['John','Joe','Anne'])

我们的df_example有了新的外观:

带有索引的数据框 — 图片由作者提供

请注意,student_name不再是一个列,而是作为对象的索引。这意味着我们现在可以使用之前学到的所有索引属性——你能猜到df_example['John']会返回什么吗?

从 df_example 中索引John — 图片由作者提供

出错了!为什么?因为数据框是多维对象,我们不能像对pd.Series那样直接对它们进行索引,但我们可以依赖loc方法:

df_example.loc[‘John’]

从 df_example 中索引John — 图片由作者提供

我们还可以通过将列表传递给loc来从我们的DataFrame中检索多个值:

df_example.loc[['John','Anne']]

从 df_example 中索引JohnAnne — 图片由作者提供

在实际示例中,我们将看到如何将选择和索引值扩展到列中。

最后,让我们看看如何通过数字索引来索引我们的pandas数据框。为此,我们需要依赖iloc

df_example.iloc[0]

从 df_example 中索引John — 图片由作者提供

这个命令将索引数据框的第一行,其作用类似于命名版本的索引df_example.loc['John']

我们已经了解了SeriesDataFrame——现在让我们深入了解一个更实际的示例,使用股票价格数据。这将帮助你更好地理解 pandas 及其用法。

读取 CSV 文件到数据框中

正如我在博客文章的介绍中详细说明的,我们将使用一个 Kaggle 数据集的子集,点击这里

首先,让我们将两个CSV文件读入数据框对象。这可以通过访问pd.read_csv函数来实现:

apple = pd.read_csv('AAPL.csv')
ford = pd.read_csv('F.csv')

我们创建了两个不同的数据框,分别叫做appleford,它们将保存两个公司的不同信息。

接下来,让我们进行一个小的管道操作,探索我们新创建的对象的内容和结构!

探索我们新获得的数据框

在本章中,我们将研究apple数据框。大多数人一拿到数据框对象,就会首先调用.head().tail()方法。我们来看一下下面这两个方法的输出:

apple.head(10)

apple 数据框前 10 行 — 图片由作者提供

head 命令提取 DataFrame 的前 n 行,并在输出中显示。n 是我们在 DataFrame 方法中传递的整数。

tail 方法将返回 DataFrame 的最后 n 个元素:

apple.tail(10)

apple DataFrame 最底部 10 行 — 作者图片

这两个命令类似于在 SQL 中对表进行排序和调用 LIMIT。例如,查询 SELECT * FROM `APPLE` LIMIT 10 会达到相同的结果(假设行按日期排序)。

当我们有一个 DataFrame 时,使用一些整洁的 pandas 属性来了解其结构非常常见。以下是一些详细介绍:

  • .shape 方法给我们提供行和列的数量,格式为 (行数, 列数)。例如,apple.shape 会告诉我们 DataFrame 包含 10,483 行和 8 列:

apple DataFrame 形状

  • .columns 会给我们列名的列表:

apple DataFrame 列名

  • .index 将给我们索引的名称(行名):

apple DataFrame 行名

我们没有为 DataFrame 提供索引(它仍然使用 read.csv 函数创建的自动数字索引)。我们稍后会学习如何分配有意义的命名索引。

  • .describe() 为我们提供所有数值列的一个很好的概述:

pandas Describe 命令 — 作者图片

describe 方法输出关于我们数值列的重要统计数据,例如*均值、标准差、最小值、分位数数据和分布的最大值。

如果你想查看某一列的值,你可以直接选择该列——在 pandas 中有两种主要方式来做到这一点:

  • 使用索引,我们将列名放在方括号内(类似于如何在 pd.Series 中选择行):
apple[‘Volume’]

apple Volume 列 — 作者图片

我们还可以使用点表示法:

apple.Volume

apple Volume 列 — 作者图片

Python 程序员倾向于使用索引方法,主要有两个原因:在点表示法中传递列名比较困难,且点表示法不支持名称中有空格的列。

哦,如果你想快速从特定的 pandas 列中检索*均值或标准差,只需在对象上调用该方法!例如,计算 mean Volume:

apple.Volume.mean()

apple Volume *均值 — 作者图片

现在我们有一个复杂的 pandas DataFrame,包含多个列和行,让我们学习如何通过 ilocloc 扩展我们的知识来子集信息。

子集信息

在这一部分中,我们不仅解释了 Pandas 索引方法的每一个细节,还通过代码回答了有关数据的问题!这样,你将能够练习自然语言与库中可以使用的索引方法之间的转换。

让我们开始尝试选择能够回答一些问题的行:

  • 我们能选择苹果公司收盘股票价格高于 20 美元的天数吗?

我们可以通过提供apple.loc[apple[‘Close'] > 20]来回答这个问题。这段代码将输出所有满足该条件的天数:

苹果数据子集 — 收盘股票价格高于 20 美元的天数 — 图片由作者提供

这是一个真正的特性,我们可以通过使用.loc来实现。请注意,在 loc 中我们现在传递了条件apple[‘Close'] > 20,输出将尊重该条件。

但是……如果我想要一个值的范围子集,例如:我们能选择苹果公司收盘股票价格在 20 美元到 30 美元之间的天数吗?

我们可以通过在.loc中将条件用括号括起来来实现这一点:

apple.loc[(apple[‘Close'] > 20) & (apple[‘Close'] < 30)]

和符号(&)将我们的条件与 AND 条件连接起来,而管道符号(|)用于 OR 条件。

苹果数据子集 — 收盘股票价格在 20 美元以上且低于 30 美元的天数 — 图片由作者提供

让我将上面的条件翻译成类似 SQL 的代码:

  • apple.loc[apple[‘Close'] > 20]中,我们正在做类似于这个查询的操作:SELECT * FROM 'apple' WHERE Close > 20

  • apple.loc[(apple[‘Close'] > 20) & (apple[‘Close'] < 30)]中,我们正在做类似于这个查询的操作:SELECT * FROM 'apple' WHERE Close > 20 AND Close < 30

我们还可以使用一个魔法技巧从我们的多维对象中提取特定的列。只需在.loc中添加一个逗号,我们就可以对子集列(被认为是对象的第二维):

apple.loc[apple['Close'] > 20, 'Volume']

苹果数据子集 — 股票价格高于 20 美元的天数 — 图片由作者提供

上述操作是在回答问题:苹果公司收盘股票价格高于 20 美元的天数的成交量是多少?

将其翻译成 SQL:SELECT Volume FROM 'apple' WHERE Close > 20

不过,如果我们想要多个列呢?你能猜到如何修改上面的pandas代码吗?

答案:列表索引!

apple.loc[apple['Close'] > 20, ['Volume','Close']]

苹果数据子集 — 股票价格高于 20 美元的天数的成交量和收盘价 — 图片由作者提供

另一个常见的数据整理操作是分组数据。我们将在下一部分中看看如何做到这一点!

分组信息

现在,我的数据过于详细——我想提取appleford股票价格的年度*均值。为此,我们需要做两件事:

  • 创建一个新列,其中包含从Date列提取的年份。

  • 提取按年份列分组的 Close 价格的均值。

我们有一个问题在于 Date 列 — 为什么?因为现在,它被视为一个 Object 列,如通过调用 .dtypes 属性所见:

apple.dtype

apple 数据框数据类型 — 作者提供的图像

虽然我们可以执行子字符串操作来提取年份,但使用日期时间属性从这个日期中提取信息会更好。让我介绍一下 pandas 中的另一种数据类型 —— datetime

要将对象转换为 datetime,我们可以调用 to_datetime 函数:

pd.to_datetime(apple[‘Date’])

to_datetime 函数的参数中,我们可以传入我们希望转换为日期的列。请注意,我们可以转换这个列,因为它具有 pandas 日期所期望的格式(有关格式的更多信息,请参见官方文档)。

现在,返回的对象 dtype 已经改变:

日期时间转换 — 作者提供的图像

有了这个 datetime 对象,我们可以访问 dt 属性,并方便地使用 .dt.year 提取年份。让我们在下面尝试一下:

pd.to_datetime(apple[‘Date’]).dt.year

年份列 — 作者提供的图像

但是,我们如何将这个列添加到现有的数据框中呢?这非常简单——我们只需将其分配给一个尚不存在的东西(还没有!):

apple[‘Year’] = pd.to_datetime(apple[‘Date’]).dt.year

这将会在我们的 apple 数据框中创建一个名为 Year 的新列,包含从 Date 列提取的年份。

创建年份列 — 作者提供的图像

我们的过程的第一部分已经完成!现在,让我们进入第二部分,我们将按 Year 计算 Close 价格的*均值——听起来很困难,对吧?

但其实不是!正如你可能猜到的那样,一旦掌握了库的工作原理,pandas 中的事情其实非常简单。首先,我们需要调用 groupby 方法,这样我们就可以……嗯……按列分组!

apple.groupby([‘Year’])

groupby 方法不输出任何内容(除了通用的方法输出),它只是准备 pandas 接受将在参数列中进行分组的内容。生成 groupby 对象后,我们可以传入我们希望聚合的列和度量:

apple.groupby([‘Year’])[‘Close’].mean()

生成的分组对象的前几行 — 作者提供的图像

我们还可以使用其他函数进行其他计算——例如,按组提取最大值:

apple.groupby([‘Year’])[‘Close’].max()

生成的分组对象的前几行 — 作者提供的图像

这个最后的指令可以转化为以下 SQL 查询:SELECT Year, max(Close) as max_close from apple group by Year

我们还可以同时对多个列进行聚合,例如:

从 Close 和 Volume 列中提取最大值 — 作者提供的图像

在我们进入合并表格的部分之前,让我们创建两个汇总表,其中包含每家公司在 2000 年之后支付的每股股息信息:

ford[‘Year’] = pd.to_datetime(ford[‘Date’]).dt.year
apple_dividends = apple.loc[apple.Year >= 2000].groupby(['Year'])['Dividends'].sum()
ford_dividends = ford.loc[ford.Year >= 2000].groupby(['Year'])['Dividends'].sum()

好的,让我们慢一点处理最后这条指令,因为我们要在这里综合运用我们在整个博客文章中学到的几个知识点:

  • 首先,我们在 2000 年之后过滤每一行数据:apple.loc[apple.Year >= 2000]

  • 这将返回一个 DataFrame,我们可以在之后使用 groupbysum,例如 groupby(['Year'])['Dividends'].sum()

让我们可视化一下我们的 apple_dividends DataFrame(?):

pandas 按 Series 分组— 作者提供的图片

当我们执行返回单列的 group by 操作时,我们会输出一个 pd.Series,而不是 DataFrame。我们可以通过将其包装在 pd.DataFrame 函数中显式地将对象转换为 DataFrame

apple_dividends = pd.DataFrame(apple_dividends)
ford_dividends = pd.DataFrame(ford_dividends)

现在,以 DataFrame 格式查看 apple_dividends 对象:

pandas 按对象分组 — 作者提供的图片

将这些 DataFrames 合并成一个对象会很有趣。这是我们将在下一部分处理的内容!

合并对象

在这一部分,我将向你展示如何以多种方式合并 DataFrames:

  • 垂直地,通过堆叠或附加它们。

  • 水*地,通过使用连接。

首先,让我们学习如何垂直堆叠表格(类似于 UNION 操作符)——我们可以通过提供 pd.concat 来实现:

pd.concat([apple_dividends,ford_dividends])

这将创建一个包含两家公司股息数据的 DataFrame。在当前格式下,很难理解每一行对应的公司。另一种方法是在原始 DataFrames 中创建一个新列,注明公司名称:

apple_dividends['company'] = 'apple'
ford_dividends['company'] = 'ford'

pd.concat([apple_dividends,ford_dividends])

combined_dividends = pd.concat([apple_dividends,ford_dividends])

合并后的苹果和福特股息数据的样本 — 作者提供的图片

索引会重复,这可能会有些奇怪,尽管在索引同一年份的数据时可能会很方便:

combined_dividends.loc[2015]

2015 年数据的子集 — 作者提供的图片

我们也可以选择将这些数据以两个时间序列并排的方式呈现。为此,我们需要稍微修改原始数据,并将过程考虑为表连接。如果我们想保持 year 作为索引,我们可以使用 merge 函数来合并数据:

apple_dividends.merge(ford_dividends, left_index=True, right_index=True)

已连接的股息数据 — 作者提供的图片

上面的示例类似于执行查询:

SELECT a.Dividends as Dividends_x, a.company as company_x, b.Dividends as dividends_y, b.company as company_y
from apple_dividends as a
inner join ford_dividends as b
on a.Year = b.Year

由于 pandas 在合并过程中不支持两个同名的列,Python 会自动添加 _x 或 _y 后缀,以区分列的来源。

另外,我们也可以使用方便的 on 参数,它支持按任何列进行连接。为了将我们的 Year 作为列,我们可以重置两个表的索引:

apple_dividends.reset_index(inplace=True)
ford_dividends.reset_index(inplace=True)

注意这个查询中的inplace=True。这是 Python 中一个非常重要的属性!某些操作可以就地进行,这意味着对象会动态改变,无需重新赋值。在上面的例子中,我们只是重置了两个表的索引,将年份转换为一列:

索引重置示例 — 作者图片

现在,我们可以明确地在合并函数中使用 Year

joined_dividends = apple_dividends.merge(ford_dividends, on='Year')

按列连接的股息数据 — 作者图片

如果你想根据 DataFrame 中不属于索引的特定列组合数据,这可能会很有用。

既然我们的«表格已保存,让我们通过查看一些简单的pandas绘图功能来结束这篇博客文章吧!

绘图功能

pandas 与 Python matplotlib 的集成非常棒。绘制数据的一个酷炫的方法是直接调用 .plot() 方法:

joined_dividends.Dividends_x.plot()

苹果的股息图 — 作者图片

如果我在两个 pandas 系列上调用图表,它们会同时显示在同一图表中,使我们能够比较两家公司的股息:

joined_dividends.Dividends_x.plot()
joined_dividends.Dividends_y.plot()

福特和苹果的股息图 — 作者图片

这个图仍然有点不完整 — 我们没有标题、轴标签、x 轴标签或指示每条线对应哪家公司。我们可以使用.plot()函数来改进它吗?

当然!我们可以在整个 DataFrame 上调用 .plot 方法,并基于此控制 xy 轴:

import matplotlib.pyplot as plt

joined_dividends.plot(x='Year', 
 y=['Dividends_x','Dividends_y'], 
 xlabel='Year', 
 ylabel='Dividends Value',
 title='Annual Dividends by Company')

plt.legend(['Apple','Ford'])

我们不能在 .plot() 方法上更改一些属性。例如,添加自定义图例必须使用 matplotlib 库,通过在创建图表后调用 plt.legend() 来完成。

福特和苹果的股息条形图 — 作者图片

我们还可以通过使用 kind 参数来更改图表类型 — 例如,我们可以查看一个条形图中的股息:

joined_dividends.plot(x='Year', 
                      y=['Dividends_x','Dividends_y'], 
                      xlabel='Year', 
                      ylabel='Dividends Value',
                      title='Annual Dividends by Company',
                     kind='bar')

plt.legend(['Apple','Ford'])

福特和苹果的股息条形图 — 作者图片

尽管有限,pandas 的绘图功能为库的特性增添了额外的风味,而且非常方便,特别是在进行快速数据探索分析时。使用 pandas 绘图 API 的缺点是有许多 matplotlib 的功能不可用,因此我们通常会导入这两个库,特别是当我们想创建更复杂的图表时。

就这些了!感谢你抽出时间阅读这篇文章,希望你喜欢学习 pandas。

对于每一个想要在 Python 中使用 DataFrame 的专业人士或学生来说,这个库是必备的。了解这个对象对于使用其他框架(如 spark)或语言(如 R)也非常重要。使用pandas已经成为数据科学家、工程师和分析师的关键技能。我相信pandas将在未来十年继续被使用,因为它已经深深嵌入到许多数据科学流程中。

不足之处是,pandas存在一些局限性,具体如下:

  • 内存使用:对于大型且多样化的 DataFrame,pandas的性能可能较差。像PySpark这样的其他框架可能更适合这些操作。

  • 可变性:对于从未使用过 Python 的人来说,可变性属性一开始可能会让人困惑。处理不当是使用该库时最常见的错误之一。

  • 无法处理非结构化数据。虽然pandas可能支持将列表或字典作为列,但将非结构化数据存储其中是一个大问题,且易出错。

总结一下,让我们详细说明一下在博客中涉及的一些主题:

  • 处理一维的pandas对象Series,该对象的工作方式类似于 R 的向量。

  • 创建、索引以及对主要的pandas对象DataFrame进行各种操作。

  • 读取外部对象,如 CSV 文件到DataFrame对象中。

  • 合并不同的DataFrame,包括垂直(追加)和水*(连接数据)。

  • 使用matplotlib API 在pandas中绘制数据。

阅读完这篇文章后,我建议你阅读一下pandas的官方文档,并尝试一些我们在博客中没有涉及的函数,比如窗口操作DataFrame 重塑

此外,如果你正在走向数据科学家的道路,你可能会发现学习NumPyMatplotlibscikit-learnPySpark是很有意义的。

你认为我在这个综合指南中遗漏了什么吗?请在下面的评论中告诉我,以便我添加!

如果你有兴趣参加我的 Python 课程,请随时加入 我的免费课程 Python For Busy People — Python Introduction in 2 Hours) 或者一个更长的 16 小时版本 The Complete Python Bootcamp for Beginners),如果不适合你,可以享受 30 天退款政策。我的 Python 课程适合初学者/中级开发者,欢迎你来我的课程!

绝对初学者的 Python 课程 — 作者提供的图片

这篇文章中使用的数据集采用了 知识共享署名 CC0 公共领域授权。

高斯溅射的全面概述

原文:towardsdatascience.com/a-comprehensive-overview-of-gaussian-splatting-e7d570081362?source=collection_archive---------0-----------------------#2023-12-23

你需要了解的关于 3D 表示领域的新趋势

Kate YurkovaTowards Data Science Kate Yurkova

·

关注 发布于 Towards Data Science ·12 分钟阅读·2023 年 12 月 23 日

--

高斯溅射是一种用于表示 3D 场景和渲染新视角的方法,首次在“3D Gaussian Splatting for Real-Time Radiance Field Rendering”¹中介绍。它可以被视为 NeRF²模型的替代方案,而且就像以前的 NeRF 一样,高斯溅射引发了大量新研究工作,这些研究选择使用它作为各种应用中的 3D 世界的基础表示。那么它有什么特别之处,为什么它比 NeRF 更好?或者它真的更好吗?让我们来探究一下!

目录:

  • 简而言之

  • 3D 世界的表示

  • 图像形成模型与渲染

  • 优化

  • 视图依赖颜色与 SH

  • 限制

  • 在哪里玩

TL;DR

首先,这项工作的主要亮点是高渲染速度,如标题所示。这归功于表示本身(将在下文中介绍)以及定制的 CUDA 内核渲染算法的实现。

图 1: 以渲染速度(fps)、训练时间(min)和视觉质量(峰值信噪比,越高越好)对比之前的高质量表现和高斯溅射(标记为“我们的”)[来源:取自[1]]

此外,高斯溅射完全不涉及任何神经网络。甚至没有一个小的 MLP,完全没有“神经”成分,场景本质上只是空间中的一组点。这本身已经非常吸引注意力。在我们对 AI 痴迷的世界中,看到这样的方法逐渐流行,与那些追逐包含更多数十亿参数的模型的研究公司相比,颇具清新感。它的理念来源于“表面溅射”³(2001),因此它为经典计算机视觉方法仍然可以激发相关解决方案树立了一个很好的例子。其简单而明确的表示使得高斯溅射特别可解释,这是在一些应用中选择它而不是 NeRFs 的一个非常好的理由。

代表一个 3D 世界

如前所述,在高斯溅射中,一个 3D 世界是由一组 3D 点表示的,实际上是数百万个,数量在 0.5-5 百万的范围内。每个点是一个 3D 高斯,其唯一的参数针对每个场景进行拟合,使得该场景的渲染与已知数据集图像非常匹配。优化和渲染过程将在后面讨论,因此现在我们暂时关注必要的参数。

图 2: 高斯中心(均值)[来源:取自动态 3D 高斯⁴]

每个 3D 高斯由以下参数化:

  • 均值μ可以解释为位置 x, y, z;

  • 协方差Σ

  • 不透明度σ(𝛼),应用了一个 sigmoid 函数来将参数映射到[0, 1]区间;

  • 颜色参数,可以是(R, G, B)的 3 个值或球面谐波(SH)系数。

这里有两组参数需要进一步讨论,一组是协方差矩阵,另一组是 SH。后者有一个专门的章节进行讨论。至于协方差,它被设计为各向异性的,即不是各向同性。实际上,这意味着一个 3D 点可以是一个在空间中沿任意方向旋转和拉伸的椭球体。它本来需要 9 个参数,但由于协方差矩阵只有在它是半正定矩阵时才有物理意义,因此这些参数不能直接优化。使用梯度下降进行优化使得直接对矩阵施加这样的约束变得困难,因此它被分解如下:

这种分解被称为协方差矩阵的特征分解,可以理解为椭球体的配置,其中:

  • S 是一个具有 3 个缩放参数的对角缩放矩阵;

  • R 是一个用 4 个四元数解析表示的 3x3 旋转矩阵。

使用高斯分布的美在于每个点的双重影响。一方面,每个点根据其协方差有效地表示了空间中接*其均值的有限区域。另一方面,它具有理论上的无限范围,这意味着每个高斯分布在整个 3D 空间中定义,并且可以在任何点上进行评估。这一点很重要,因为在优化过程中,它允许梯度从较远的距离传播。⁴

3D 高斯分布的 i 对任意 3D 点 p 的影响定义如下:

图 3: 3D 高斯分布 i 对 3D 点 p 的影响 [来源:作者提供的图像]

这个方程看起来几乎像是多变量正态分布的概率密度函数,除了归一化项的协方差行列式被忽略,取而代之的是通过不透明度加权。

图像形成模型与渲染

图像形成模型

给定一组 3D 点,可能最有趣的部分是查看它如何用于渲染。你可能之前熟悉 NeRF 中使用的点对点𝛼混合。结果是NeRF 和高斯点投射共享相同的图像形成模型。为了了解这一点,让我们稍作绕道,重新访问 NeRF²及其许多后续工作的体积渲染公式(1)。我们还将使用简单的转换(2)对其进行重写:

你可以参考 NeRF 论文了解σ和δ的定义,但从概念上讲,可以这样理解:在 NeRF 中,图像像素p中的颜色是通过沿着通过该像素的射线对样本(MLP 预测)进行积分来*似的。最终的颜色是沿着射线采样的三维点颜色的加权和,经过透射率的衰减。有了这个认识,我们最后来看一下高斯 splatting 的图像形成模型:

确实,公式(2)和(3)几乎相同。唯一的区别在于如何计算𝛼。在高斯 splatting 中,对每个像素的聚合是基于一个有序的二维高斯投影列表的贡献。这个小小的差异在实践中却极其重要,并导致显著不同的渲染速度。实际上,它是高斯 splatting 实时性能的基础

要理解为什么会这样,我们需要了解f^{2D}的含义以及它所带来的计算需求。这个函数只是将我们在上一节看到的f(p)投影到二维,即投影到正在渲染的相机的图像*面上。三维点及其投影都是多变量高斯分布,因此可以使用与计算三维高斯对其他三维点的影响相同的公式来计算投影的二维高斯对像素的影响(见图 3)。唯一的区别是均值μ和协方差Σ必须投影到二维,这通过 EWA splatting⁵的推导来完成。

可以通过将向量μ(在齐次坐标中,带有额外的 1 坐标)投影到图像*面上,使用内在相机矩阵K和外在相机矩阵W=[R|t]来轻松获得二维均值。

这也可以用一行来写:

这里“z”下标表示 z 标准化。二维中的协方差使用(4)的雅可比矩阵J:来定义。

整个过程仍然可以进行微分,这对优化至关重要。

渲染

公式(3)告诉我们如何在单个像素中获得颜色。要渲染整个图像,仍然需要遍历所有 HxW 像素,就像在 NeRF 中一样,但由于以下原因,过程要轻便得多:

  • 对于给定的相机,每个三维点的f(p)可以提前投影到二维,然后再遍历像素。这样,当对几个相邻的像素进行高斯融合时,我们就不需要一遍遍地重新投影它。

  • 不需要对单张图像推断 H·W·P 次的 MLP,二维高斯直接融合到图像上。

  • 沿射线评估哪个 3D 点没有歧义,无需选择射线采样策略。每个像素射线的 3D 点集(参见(3)中的N)是离散且在优化后固定的。

  • 预处理的排序阶段在每一帧中进行一次,使用 GPU,通过自定义的可微分 CUDA 内核实现。

概念上的差异可以在图 4中看到:

图 4: NeRF 和 GS 之间的概念差异,左:沿射线查询连续的 MLP,右:混合与给定射线相关的离散高斯集合 [来源:作者提供的图像]

上述排序算法是论文的一个贡献。其目的是为了配合公式(3)进行颜色渲染:按深度(靠*图像*面的距离)对 3D 点进行排序,并按图块进行分组。前者用于计算透射率,后者则限制每个像素的加权和只对相关的 3D 点进行α混合(或其 2D 投影,更具体地说)。分组是通过简单的 16x16 像素图块实现的,并且实现方式使得一个高斯可以落在多个图块中,如果它重叠了多个视锥体。得益于排序,每个像素的渲染可以简化为从像素所属的图块中预排序点的α混合。

图 5: 视锥体,每个视锥体对应一个 16x16 的图像块。颜色没有特殊含义。排序算法的结果是在每个图块内按深度排序的 3D 点的子集。[来源:基于这里的图]

优化

可能会产生一个幼稚的问题:如何从空间中的一堆模糊物体中得到一个看起来不错的图像?的确,如果高斯没有被妥善优化,渲染结果中会出现各种尖锐的伪影。在图 6 中,你可以观察到这种伪影的一个示例,它们看起来确实像椭球体。获得良好渲染的关键在于 3 个组成部分:良好的初始化、可微分优化和自适应稠密化

图 6: 一个未优化场景的渲染示例 [来源:作者提供的图像]

初始化指的是在训练开始时设定的 3D 点参数。对于点位置(均值),作者建议使用 SfM(结构光束法)生成的点云,见图 7。逻辑是,对于任何 3D 重建,无论是 GS、NeRF 还是更经典的方法,你都必须知道相机矩阵,因此你可能还是会运行 SfM 来获取这些矩阵。由于 SfM 生成稀疏点云作为副产品,为什么不利用它进行初始化? 这就是论文的建议。当点云由于某种原因不可用时,可以使用随机初始化,但有可能导致最终重建质量的损失。

图 7: 由 SfM 生成的稀疏 3D 点云,表示初始化 [来源:取自 here]

协方差初始化为各向同性,换句话说,3D 点最初是球形。半径基于与邻*点的*均距离进行设置,以确保 3D 世界被很好地覆盖,并且没有“孔洞”。

初始化后,使用简单的随机梯度下降法来正确拟合所有内容。场景经过优化以使损失函数成为 L1 和 D-SSIM(结构不相似性指数测量)之间的组合,比较真实视图和当前渲染效果。

然而,这还不是全部,另一个关键部分是自适应稠密化。它在训练过程中不时启动,比如每100 SGD步,这个过程旨在解决欠重建和过度重建的问题。需要强调的是,SGD 本身只能调整现有点。但在完全缺少点或点过多的区域,它会难以找到合适的参数。这时,自适应稠密化就派上用场了,它拆分大梯度的点(图 8)并移除已收敛到非常低 α 值的点(如果一个点如此透明,为什么还要保留它?)。

图 8: 自适应稠密化。一个玩具示例,展示了我们希望用少量点来呈现的豆形。[来源:取自[1]]

视图依赖色彩与 SH

球面谐波(简称 SH)在计算机图形学中起着重要作用,最早提出作为一种学习离散 3D 体素视图依赖色彩的方法,在 Plenoxels⁶ 中得到应用。视图依赖性是一个可选属性,它提高了渲染质量,因为它允许模型表示非朗伯特效应,例如金属表面的高光。然而,这绝不是必须的,因为可以简化选择用 3 个 RGB 值表示颜色,仍然使用高斯散射,就像在[4]中做的那样。因此,我们在方法完整描述后单独回顾这个表示细节。

SH 是定义在球面上的特殊函数。换句话说,你可以对球面上的任何点评估这样的函数并得到一个值。所有这些函数都从这个单一的公式中推导而来,通过选择正整数的 和 - m,每个 SH 一个(ℓ, m)对:

虽然一开始有点令人生畏,但对于小值的l这个公式会大大简化。实际上,对于ℓ = 1, Y = ~0.282,在整个球面上只是一个常数。相反,较高的值会产生更复杂的表面。理论告诉我们,球面谐波形成一个正交基,因此在球面上定义的每个函数都可以通过 SH 表示

这就是为什么表达视角相关颜色的想法是这样的:让我们将自由度限制在一个特定的ℓ_max,并假设每种颜色(红色、绿色和蓝色)是前ℓ_max个 SH 函数的线性组合。对于每个 3D 高斯,我们希望学习正确的系数,以便当我们从某个方向查看这个 3D 点时,它能传达出最接*真实颜色的颜色。获得视角相关颜色的整个过程可以参见图 9。

图 9: 获取一个点的视角相关颜色(红色分量)的过程,ℓ_max = 2 和 9 个学习到的系数。一个 sigmoid 函数将值映射到[0, 1]区间。通常,裁剪被用来代替 [来源:作者图片]

限制

尽管总体结果很出色,渲染速度也很快,但表示的简单性也有代价。最重要的考虑是优化过程中引入的各种正则化启发式,以保护模型免受“破损”高斯的影响:过大、过长、冗余的点等。这部分非常关键,这些问题在新视角渲染之外的任务中可能会进一步放大。

选择放弃连续表示而转向离散表示意味着MLP 的归纳偏差丢失。在 NeRF 中,MLP 执行隐式插值,并*滑给定视图之间可能的不一致,而 3D 高斯则更敏感,这会导致回到上述问题。

此外,高斯喷溅也不免受到一些在 NeRF 中存在的已知伪影的影响,这些伪影都来源于共享的图像形成模型:在少见或未见区域的质量较低、接*图像*面的浮动点等。

检查点的文件大小是另一个需要考虑的属性,即使新视角渲染距离边缘设备的部署还很远。考虑到 3D 点的大致数量和流行 NeRF 的 MLP 架构,两者磁盘空间的量级相同,其中 GS *均重量仅重几倍。

玩它的地方

没有哪个博客文章能比直接运行方法并亲自查看结果更能体现其价值。你可以在这里尝试:

  • gaussian-splatting — 带有自定义 CUDA 内核的官方实现;

  • nerfstudio — 是的,nerfstudio 中的高斯溅射。这是一个最初专注于 NeRF 类似模型的框架,但从 2023 年 12 月开始,它也支持 GS;

  • threestudio-3dgs — 一个用于 threestudio 的扩展,另一个跨模型框架。如果你对从提示生成 3D 模型感兴趣,而不是学习现有的图像集,你应该使用这个;

  • UnityGaussianSplatting — 如果你对 Unity 感兴趣,你可以将训练好的模型移植到这个插件中进行可视化;

  • gsplat — 一个用于高斯体素 CUDA 加速光栅化的库,源自 nerfstudio。它可以作为可区分的溅射模块用于独立的基于 torch 的项目。

玩得开心!

致谢

本博客文章基于 Dr. Tali Dekel 实验室的一个小组会议。特别感谢Michal Geyer对论文讨论的贡献,感谢[4]的作者对高斯溅射的连贯总结,以及Yuliang Guo对改进建议的提供。

参考文献

  1. Kerbl, B., Kopanas, G., Leimkühler, T., & Drettakis, G. (2023). 用于实时辐射场渲染的 3D 高斯溅射。 SIGGRAPH 2023。

  2. Mildenhall, B., Srinivasan, P. P., Tancik, M., Barron, J. T., Ramamoorthi, R., & Ng, R. (2020). NeRF: 将场景表示为神经辐射场以实现视图合成。 ECCV 2020。

  3. Zwicker, M., Pfister, H., van Baar, J., & Gross, M. (2001). 表面溅射。 SIGGRAPH 2001。

  4. Luiten, J., Kopanas, G., Leibe, B., & Ramanan, D. (2023). 动态 3D 高斯:通过持久动态视图合成进行跟踪。 国际 3D 视觉会议。

  5. Zwicker, M., Pfister, H., van Baar, J., & Gross, M. (2001). EWA 体积溅射。 IEEE Visualization 2001。

  6. Yu, A., Fridovich-Keil, S., Tancik, M., Chen, Q., Recht, B., & Kanazawa, A. (2023). Plenoxels: 无需神经网络的辐射场。 CVPR 2022。

回归评估指标的全面概述

原文:towardsdatascience.com/a-comprehensive-overview-of-regression-evaluation-metrics-6264af0926db

图片由作者使用来自 icons8 的图标创建

对常用回归评估指标及其在各种场景中的实际应用进行广泛参考

Eryk LewinsonTowards Data Science Eryk Lewinson

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

--

作为数据科学家,评估机器学习模型的性能是你工作的重要方面。为了有效地做到这一点,你可以利用各种统计指标,每个指标都有其独特的优点和缺点。通过深入理解这些指标,你不仅可以更好地选择优化模型的最佳指标,还能向业务利益相关者解释你的选择及其影响。

在本文中,我重点介绍了用于评估回归问题的指标,这些问题预测数值——如房价或公司下月销售预测。由于回归分析被视为数据科学的基础,理解其细微差别是至关重要的。

残差的快速概述

残差是大多数指标的基本构建块。简单来说,残差是实际值与预测值之间的差异。

residual = actual - prediction

下图展示了目标变量(y)与单个特征(x)之间的关系。蓝色点代表观测值。红线是机器学习模型的拟合结果,在这种情况下是线性回归。橙色线条表示观测值与这些观测的预测之间的差异。因此,可以为数据集中每个观测值(无论是训练集还是测试集)计算残差。

图 1. 具有一个特征的线性模型中的残差示例

回归评估指标

本节讨论了一些最受欢迎的回归评估指标,这些指标可以帮助你评估模型的有效性。

偏差

最简单的误差度量是残差的总和,有时称为偏差。由于残差可以是正值(预测值小于实际值)和负值(预测值大于实际值),偏差通常告诉我们预测值是否高于或低于实际值。

然而,由于对立符号的残差相互抵消,我们可能会得到一个产生非常低偏差预测的模型,而其准确性却非常差。

另外,我们可以计算*均残差,或称为均值偏差误差(MBE)。

R *方

下一个指标可能是你在学习回归模型时首先遇到的,特别是在统计学或计量经济学课程中。R *方(R²),也称为决定系数,表示模型解释的方差比例。更准确地说,R² 对应于因变量(目标)方差的程度可以由自变量(特征)解释。

以下公式用于计算 R²。

其中:

  • RSS 是残差*方和,即残差*方的总和。这个值捕捉了模型的预测误差。

  • TSS 是总*方和。为了计算这个值,我们首先假设一个简单模型,其中每个观测值的预测值是所有观察实际值的均值。TSS 与因变量的方差成正比,因为 TSS/N 是y 的实际方差,其中 N 是观察次数。也就是说,我们可以将 TSS 看作是简单均值模型无法解释的方差。

实际上,我们是在比较一个模型的拟合程度(如图 2 中的红线所示)与一个简单均值模型的拟合程度(如图中绿色线所示)。

图 2. 比较线性模型与简单均值基准的拟合情况

了解了 R² 的组成部分后,我们可以看到 RSS/TSS 代表了目标总方差中模型无法解释的部分。

在使用 R² 时需要注意一些额外的点。

首先,R² 是一个相对度量,即它可以用于与在相同数据集上训练的其他模型进行比较。更高的值表示拟合更好。

R² 也可以用来粗略估计模型的整体表现。然而,在使用 R² 进行这种评估时,我们应该小心:

  • 首先,不同领域(社会科学、生物学、金融等)对 R² 的不同值有不同的好坏标准。

  • 其次,R² 并没有给出任何偏差的度量,因此我们可能会有一个高 R² 值的过拟合(高度偏差)模型。因此,我们还应该查看其他指标,以便更好地理解模型的表现。

R² 的一个潜在缺点是它假设每个特征都有助于解释目标的变异,但这并不总是正确的。因此,如果我们继续向使用普通最小二乘法 (OLS) 估计的线性模型中添加特征,R² 的值可能会增加或保持不变,但从不会减少。

为什么?按设计,OLS 估计最小化 RSS。假设一个带有附加特征的模型没有提高第一个模型的 R² 值。在这种情况下,OLS 估计技术会将该特征的系数设为零(或一些统计上不显著的值)。这实际上将我们带回到初始模型。在最坏的情况下,我们可能得到的是起始点的评分。

解决前述问题的方法是调整后的 R²,它额外惩罚了添加那些对预测目标无用的特征。如果由于添加新特征导致的 R² 增加不够显著,调整后的 R² 值会下降。

最后一项,我们讨论 R² 值范围的常见误解。如果使用 OLS 拟合线性模型,则 R² 的范围是 0 到 1。这是因为使用 OLS 估计(最小化 RSS)时,一般属性是 RSS ≤ TSS。在最坏的情况下,OLS 估计将导致获得均值模型。在这种情况下,RSS 等于 TSS,R² 的最小值为 0。另一方面,最佳情况是 RSS = 0 和 R² = 1。

在非线性模型的情况下,R² 可能为负。由于这些模型的拟合过程不是基于迭代最小化 RSS,因此拟合模型的 RSS 可能大于 TSS。换句话说,模型的预测比简单均值模型更差。更多信息,请参见 R squared 何时为负?

附加说明:使用 R²,我们可以评估我们的模型相比于简单均值模型在数据拟合上的提升。我们可以将正的 R² 值理解为提升基线模型性能的能力——类似于技能评分。例如,40% 的 R² 表示我们的模型将均方误差减少了 40%,相对于基线均值模型。

均方误差

均方误差 (MSE) 是最常见的评价指标之一。如以下公式所示,MSE 与残差*方和密切相关。不同之处在于,我们现在关注的是*均误差,而不是总误差。

在使用 MSE 时需要考虑以下几点:

  • MSE 使用均值(而不是总和)来使指标独立于数据集大小。

  • 由于残差被*方,MSE 对大误差施加了显著更重的惩罚。其中一些可能是离群值,因此 MSE 对其存在不鲁棒。

  • 由于度量指标是通过*方、求和和常数(1/N)表示的,它是可导的。这对优化算法非常有用。

  • 在优化 MSE(将其导数设为 0)时,模型的目标是使预测的总和等于实际值的总和。也就是说,这会导致预测在*均上是正确的。因此,它们是无偏的。

  • MSE 不以原始单位进行测量,这可能使其更难解释。

  • MSE 是一个依赖于尺度的度量指标,即误差以基础数据的单位表示(尽管实际上需要*方根才能以相同尺度表示)。因此,这些度量指标不能用于比较不同数据集之间的性能。

均方根误差

均方根误差(RMSE)与 MSE 紧密相关,因为它只是后者的*方根。通过*方我们将度量指标恢复到目标变量的尺度,因此更容易解释和理解。然而,一个经常被忽视的事实是,虽然 RMSE 与目标在同一尺度上,但 RMSE 为 10 实际上并不意味着我们的*均偏差为 10 个单位。

除了尺度之外,RMSE 具有与 MSE 相同的属性。实际上,在训练模型时优化 RMSE 会得到与优化 MSE 时相同的模型。

*均绝对误差

计算*均绝对误差(MAE)的公式类似于 MSE 公式。我们只需将*方替换为绝对值即可。

MAE 的特点包括:

  • 由于没有*方,该度量指标以与目标变量相同的尺度表示,从而更易于解释。

  • 所有误差都被*等对待,因此该度量对离群值具有鲁棒性。

  • 绝对值忽略了误差的方向,因此低估 = 高估。

  • 类似于 MSE 和 RMSE,MAE 也依赖于尺度,因此我们不能在不同的数据集之间进行比较。

  • 当你优化 MAE 时,预测值必须比实际值高出多少倍,就必须低多少倍。这意味着我们实际上是在寻找中位数,即将数据集分成两个相等部分的值。

  • 由于公式包含绝对值,MAE 不容易求导。

*均绝对百分比误差

*均绝对百分比误差(MAPE)是商业领域最受欢迎的指标之一。这是因为它以百分比表示,使其更易于理解和解释。

为了使度量指标更易读,我们可以将其乘以 100% 以将数字表示为百分比。

需要考虑的事项:

  • MAPE 表达为百分比,这使得它成为一个尺度独立的指标。它可以用来比较不同尺度上的预测。

  • MAPE 可以超过 100%。

  • 当实际值为零时,MAPE 是未定义的(除以零)。此外,当实际值非常接*零时,它可以取到极端值。

  • MAPE 是不对称的,对负误差(即预测值高于实际值)施加的惩罚比对正误差更重。这是因为百分比误差对过低的预测不能超过 100%。而对过高的预测则没有上限。因此,优化 MAPE 会倾向于选择那些低估预测值的模型,而不是高估预测值的模型。

  • Hyndman(2021)详细阐述了 MAPE 的一个常被忽视的假设,即变量的测量单位有一个有意义的零值。因此,预测需求并使用 MAPE 不会引发警报。然而,当预测温度(特别是摄氏温度)时,我们会遇到这个问题。因为温度有一个任意的零点,在这种情况下谈论百分比是不合适的。

  • MAPE 并非在所有情况下都可微分,这可能导致在使用它作为优化标准时出现问题。

  • 由于 MAPE 是一个相对指标,相同的误差可能会导致不同的损失,具体取决于实际值。例如,对于预测值为 60 和实际值为 100,MAPE 将是 40%。而对于预测值为 60 和实际值为 20,名义误差仍为 40,但在相对尺度上则是 300%。

  • 不幸的是,MAPE 无法很好地区分重要与不重要的内容。假设我们正在进行需求预测,在几个月的时间范围内,我们对两个不同产品的 MAPE 都为 10%。结果发现第一个产品每月*均销售 100 万个单位,而另一个产品仅销售 100 个。这两者的 MAPE 都为 10%。当对所有产品进行汇总时,这两个产品的贡献是相同的,这可能远非理想。在这种情况下,考虑加权 MAPE(wMAPE)是有意义的。

对称*均绝对百分比误差

在讨论 MAPE 时,我提到它的一个潜在缺陷是其不对称性(未限制高于实际值的预测)。对称*均绝对百分比误差(sMAPE)是一个相关指标,旨在解决这个问题。

使用 sMAPE 时需考虑的要点:

  • 它表示为一个有界百分比,即具有较低(0%)和较高(200%)的界限。

  • 当实际值和预测值都非常接*零时,该指标仍然不稳定。这时,我们会面临除以一个非常接*零的数字的问题。

  • 0% 到 200% 的范围并不直观。分母中的除以二常常被省略。

  • 无论实际值还是预测值为 0,sMAPE 都会自动达到上限值。

  • sMAPE 包含与 MAPE 相同的假设,即有意义的零值。

  • 在修正无界的不对称性时,sMAPE 引入了另一种微妙的不对称性,这是由公式的分母造成的。设想两种情况。在第一种情况下,我们有 A = 100 和 F = 120。sMAPE 为 18.2%。现在类似的情况,我们有 A = 100 和 F = 80,sMAPE 为 22.2%。因此,sMAPE 倾向于对低估进行比高估更严厉的惩罚。

  • sMAPE 可能是最具争议的误差指标之一,尤其是在时间序列预测中。这是因为文献中至少有几种不同版本的这个指标,每一种都有细微的差异,这些差异会影响其属性。最后,这个指标的名称暗示了没有不对称性,但实际上并非如此。

其他回归评估指标

我没有描述所有可能的回归评估指标,因为有几十种(如果不是上百种)。这里有一些其他的指标可以在评估模型时考虑:

  • 均方对数误差(MSLE)是 MSE 的一个相关指标,区别在于我们在计算*方误差之前对实际值和预测值取对数。对两个元素进行对数变换的结果是测量实际值与预测值之间的比例或相对差异,同时忽略数据的规模。这就是为什么 MSLE 减少了异常值对最终得分的影响。MSLE 还对低估进行了更严厉的惩罚。

  • 均方根对数误差(RMSLE)是一个对 MSLE 取*方根的指标。它具有与 MSLE 相同的属性。

  • 赤池信息量准则(AIC)和 贝叶斯信息量准则(BIC)是信息准则的例子。它们用于在良好的拟合度和模型复杂性之间找到*衡。如果我们从一个简单的模型开始,拥有少量参数,然后逐步增加参数,我们的模型可能会更好地拟合训练数据。然而,这也会增加模型的复杂性,并有过拟合的风险。另一方面,如果我们从许多参数开始,并系统地删除一些参数,模型会变得更简单。与此同时,我们减少了过拟合的风险,但可能会牺牲性能(拟合优度)。AIC 和 BIC 的区别在于对复杂性的惩罚权重。请记住,将信息准则用于不同的数据集或同一数据集的不同子样本(但观察数量不同)是不有效的。

何时使用每种评估指标

与大多数数据科学问题一样,没有单一的最佳指标来评估回归模型的性能。为特定用例选择的指标将取决于用于训练模型的数据、我们试图解决的业务案例等等。因此,我们可能会在模型训练中使用单一的优化指标,但在向利益相关者报告时,数据科学家通常会呈现多个指标的选择。

在选择指标时,请考虑以下几个问题:

  • 你是否预期数据集中会有频繁的异常值?如果是这样,你希望如何考虑这些异常值?

  • 在业务中,对于过度预测还是不足预测更有偏好?

  • 你需要一个依赖于尺度的指标还是一个独立于尺度的指标?

我认为探索一些示例数据以全面理解这些指标的细微差别是有用的。虽然大多数指标都可以在scikit-learnmetrics模块中找到,但对于这个特定的任务,传统的电子表格可能是更合适的工具。

以下示例包含五个观察值。表 1 展示了实际值、预测值以及用于计算大多数考虑的指标的一些指标。

表 1. 对五个观察值计算性能指标的示例

前三行包含了实际值与预测值之间的绝对差为 20 的情景。前两行显示了实际值相同的情况下的过度预测和不足预测。第三行显示了实际值较小的情况下的过度预测。在这些行中,很容易观察到 MAPE 和 sMAPE 的特性。

表 2. 使用表 1 中的值计算的性能指标

表 1 中的第五行包含了一个比实际值小 8 倍的预测。为了实验的目的,将该预测值替换为比实际值高 8 倍的预测。表 3 包含了修订后的观察数据。

表 3. 修改单个观察值为更极端情况后的性能指标

表 4. 使用表 3 中的修改值计算的性能指标

基本上,所有指标的大小都爆炸性增长,这在直观上是合理的。sMAPE 则保持不变。

我强烈建议你尝试这些示例,以更全面地了解不同情境如何影响评估指标。这种实验应该让你在决定优化哪个指标及其选择后果时更加自信。这些练习还可能帮助你向利益相关者解释你的选择。

总结

在这篇文章中,我介绍了一些最受欢迎的回归评估指标。如文中所述,每种指标都有其优缺点。数据科学家需要理解这些优缺点,并决定哪一种(或多种)适用于特定的用例。提到的指标也可以应用于纯回归任务——例如根据与经验相关的特征预测薪资——也可以应用于时间序列预测领域。

一如既往,任何建设性的反馈都非常欢迎。你可以在Twitter上或在评论区联系我。

喜欢这篇文章?成为 Medium 会员,继续通过无缝阅读学习。如果你使用 这个链接 成为会员,你将以零额外成本支持我。提前感谢,期待见到你!

你也可能对以下内容感兴趣:

## 使用三种稳健线性回归模型处理异常值

通过 Huber、RANSAC 和 Theil-Sen 回归算法的实际示例

towardsdatascience.com ## 验证线性回归假设:Python 和 R

深入探讨高斯-马尔可夫定理及其他线性回归假设!

towardsdatascience.com ## 解释线性回归的系数

了解如何正确解释线性回归结果——包括变量变换的情况

towardsdatascience.com ## 选择正确的误差度量:MAPE 与 sMAPE

两种流行误差度量的利弊

towardsdatascience.com

参考文献

Jadon, A., Patil, A., & Jadon, S. (2022). 回归基础损失函数在时间序列预测中的全面调查。arXiv 预印本 arXiv:2211.02989

Hyndman, R. J. (2006). 重新审视间歇需求的预测准确度度量。Foresight: The International Journal of Applied Forecasting, 4(4), 43–46。

Hyndman, R. J., & Koehler, A. B. (2006). 重新审视预测准确度的度量。International journal of forecasting, 22(4), 679–688。

Hyndman, R.J., & Athanasopoulos, G. (2021) Forecasting: principles and practice, 第 3 版, OTexts: Melbourne, Australia. OTexts.com/fpp3。

除非另有说明,所有图片均由作者提供。

最初发布于 NVIDIA 的开发者博客 2023 年 4 月 20 日

基于数据驱动的方法来减少员工调查长度

原文:towardsdatascience.com/a-data-driven-method-to-reduce-employee-survey-length-8aecedcf5df9?source=collection_archive---------2-----------------------#2023-01-14

减少调查长度,同时最大化可靠性和有效性

Trevor CoppinsTowards Data Science Trevor Coppins

·

关注 发表在 Towards Data Science ·16 分钟阅读·2023 年 1 月 14 日

--

图片来源:Marvin MeyerUnsplash

员工调查正迅速成为组织生活中的一个重要方面。事实上,人力分析领域的增长和数据驱动的人才管理方法的采纳就是证明(见麦肯锡报告)。通过一次调查,我们可以收集有关领导者表现的信息、员工是否有动力以及员工是否考虑离职。唯一一个相当的“隐形问题”就是我们的调查长度。

员工调查的创建者(例如,人力资源部门和/或行为数据科学家)希望准确测量多个重要主题,这通常需要大量的问题。另一方面,参与时间较长的调查的受访者更有可能中途退出调查(Hoerger, 2010; Galesic & Bosnjak, 2009),并且引入测量误差(例如,Peytchev & Peytcheva, 2017; Holtom et al., 2022)。尽管如此,参与调查的受访者比例却有所增加:组织行为学文献中的研究报告显示,在 15 年期间(2005–2020 年),受访率从 48%大幅提高至 68%(Holtom et al., 2022)。虽然调查长度只是决定数据质量和受访者比例的众多因素之一(例如,激励措施、后续跟进;Edwards et al., 2002; Holtom et al., 2022),但调查长度是一个易于调整且直接受调查创建者控制的因素。

本文提出了一种通过选择尽可能少的条目来缩短员工调查的方法,以实现最大的期望条目特征、可靠性和有效性。通过这种方法,员工调查可以被缩短,以节省员工时间,同时希望改善参与率/退出率和测量误差,这些都是较长调查中常见的关注点(例如,Edwards et al., 2002; Holtom et al., 2022; Jeong et al., 2023; Peytchev & Peytcheva, 2017; Porter, 2004; Rolstad et al., 2011; Yammarino et al., 1991)。

缩短调查的经济效益

不相信?让我们看看缩短调查的实际经济效益。作为一个说明性的例子,我们计算一下如果将一个季度的 15 分钟调查缩短为 10 分钟,对于一个拥有 10 万人的大型组织(例如,《财富》100 强公司),投资回报率会如何。使用美国工人的中位工资($56,287;见报告),将调查时间缩短 5 分钟可以为组织节省超过 100 万美元的员工时间。虽然这些计算不是精确科学,但这是理解调查时间如何影响组织底线的有用指标。

显示减少员工调查时间的成本节约图

解决方案:缩短员工调查问卷

为了缩短我们的调查问卷,但保留理想的项目级统计数据、可靠性和有效性,我们采用两步过程,其中 Python 和 R 程序将帮助确定最佳保留的项目。在第 1 步中,我们将利用多标准决策(MCDM)程序(Scikit-criteria;Cabral 等,2016)根据多个标准(标准差、偏斜度、峰度和主题专家评分)选择表现最佳的项目。在第 2 步中,我们将利用 R 程序(OASIS;Cortina 等,2020)选择第 1 步中排名最高的项目的最佳组合,以进一步缩短我们的量表,但保持最大的可靠性和其他有效性问题。

简而言之,最终输出将是一个减少后的项目集,具有理想的项目级统计数据以及最大的可靠性和有效性。

此方法适用于谁?

  • 从事调查创建和人员数据分析的人员分析专家、数据科学家、I/O 心理学家或人力资源(HR)专业人员

  • 理想情况下,用户应具有一些 Python 或 R 和统计学的初级经验。

你需要什么?

  • Python

  • R

  • 数据集(选择一个):

  1. 实践数据集 — 我利用了国际人格项目库(IPIP;ipip.ori.org/;Goldberg, 1992)公开数据集的前 1000 个回应,由开放心理测量学(openpsychometrics.org)提供。为简便起见,我只使用了 10 个责任心项目。数据来源说明:IPIP 是一个公共领域的人格测试,可以在没有作者许可或费用的情况下使用。类似地,openpsychometrics.org 是开放源数据,已在其他几篇学术出版物中使用(见此处)。

  2. 你自己的数据集(包含员工回应)用于你想要缩短的调查问卷。理想情况下,这应该是尽可能大的数据集,以提高准确性和复制的可能性。通常,大多数用户希望拥有 100 到 200+个回应的数据集,以期消除抽样或偏斜回应的影响(见 Hinkin, 1998 以进一步讨论)。

  • 可选:数据集中每个候选缩短项目的主题专家(SME)评分。仅在使用自己的数据集时适用。

  • 可选:聚合效度和区分效度测量。这些可以在第二步中使用,但不是必需的。这些效度测量对于新量表开发更为重要,而不是缩短现有的已建立量表。聚合效度是指一个测量与其他类似测量的相关程度,而区分效度则是指它与非相关测量的无关程度(Hinkin, 1998;Levy, 2010)。同样,仅在你拥有自己的数据集时适用。

Github 页面代码: github.com/TrevorCoppins/SurveyReductionCode

请注意:除非另有说明,否则所有图片均为作者提供

第一步:项目级分析与多准则决策方法(MCDM)

项目级统计说明

对于‘纯粹’的项目级统计(或每个项目的属性),我们利用标准差(即,*均而言,受访者的回答有多大差异)以及偏度峰度(即,数据分布的不对称程度及其与正态分布理想‘峰度’的偏离程度)。每个项目的适度标准差是可取的,因为我们的多数构念(例如,工作满意度、动机)在个体间自然有所不同。这种个体间的变异性是我们用来进行预测的依据(例如,“为什么销售部门的工作满意度比研发部门高?”)。对于偏度和峰度,我们理想上希望其值最小,因为这表明我们的数据呈正态分布,这是绝大多数统计模型(例如,回归分析)的假设。尽管某些偏度和峰度在具体构念上是可以接受的,或者甚至是正常的,但真正的问题出现在分数分布与正态分布的差异较大时(Warner, 2013)。

注意:某些变量自然不符合正态分布,不应在此使用。例如,问题:“在过去一个月,你是否经历过工作场所事故?”的频率数据确实是不符合正态分布的,因为绝大多数人会选择‘无’(或 0)。

项目级分析与 MCDM

首先,我们需要安装一些后续分析所需的程序。第一个是 MCDM 程序:scikit-criteria(请参阅文档 这里; 使用 Conda 安装可能需要一两分钟)。我们还需要导入 pandasskcriteriascipy.statsskewkurtosis 模块。

conda install -c conda-forge scikit-criteria
import pandas as pd
import skcriteria as skc

from scipy.stats import skew
from scipy.stats import kurtosis

数据输入

接下来,我们需要选择数据:1)自己的数据集或 2)练习数据集(如上所述,我使用了来自 IPIP-50 开源数据集的前 1000 个回答,这些回答涉及 10 个良心度项目)。

注意:如果你使用的是自己的数据集,你需要在进行其余分析之前清理数据(例如,处理缺失数据)。

# Data file #

# 1) load your own datafile here 
# OR
# 2) Utilize the practice dataset of the first 1000 responses of IPIP-50 
# which is available at http://openpsychometrics.org/_rawdata/.
# For simplicity, we only utilized the 10-conscientious items (CSN)

## The original IPIP-50 survey can be found here: 
## https://ipip.ori.org/New_IPIP-50-item-scale.htm

Data = pd.read_csv(r'InsertFilePathHere.csv')

如果你使用练习数据集,某些项目需要重新编码(请参阅 这里 获取评分要点)。这确保所有回答在我们的李克特量表上具有相同的方向(例如,5 表示所有项目的高度良心回应)。

#Recoding conscientiousness items
Data['CSN2'] = Data['CSN2'].replace({5:1, 4:2, 3:3, 2:4, 1:5})
Data['CSN4'] = Data['CSN4'].replace({5:1, 4:2, 3:3, 2:4, 1:5})
Data['CSN6'] = Data['CSN6'].replace({5:1, 4:2, 3:3, 2:4, 1:5})
Data['CSN8'] = Data['CSN8'].replace({5:1, 4:2, 3:3, 2:4, 1:5})

注意:对于这种方法,你应该一次只处理一个测量或‘量表’。例如,如果你想缩短你的工作满意度和组织文化测量,请分别对每个测量进行此分析。

生成项目级统计数据

接下来,我们收集所有需要的项目级统计数据,用于 scikit-criteria 以帮助进行最终的最佳项目排名。这包括标准差、偏度和峰度。需要注意的是,峰度程序这里利用了 Fisher 的峰度,其中正态分布的峰度为 0。

## Standard Deviation ##
std = pd.DataFrame(Data.std())
std = std.T

## Skewness ##
skewdf = pd.DataFrame(skew(Data, axis=0, bias=False, nan_policy='omit'))
skewdf = skewdf.T
skewdf = pd.DataFrame(data=skewdf.values, columns=Data.columns)

## Kurtosis ##
kurtosisdf = pd.DataFrame(kurtosis(Data, axis=0, bias=False, nan_policy='omit'))
kurtosisdf = kurtosisdf.T
kurtosisdf = pd.DataFrame(data=kurtosisdf.values, columns=Data.columns)

可选:主题专家评分(定义对应性)

虽然可选,但如果你在学术或应用工作中建立新的量表或测量,强烈推荐收集主题专家(SME)评分。通常,SME 评分有助于建立内容效度或定义对应性,即你的项目与提供的定义的对应程度(Hinkin & Tracey, 1999)。这种方法涉及对少数个体进行调查,询问一个项目与您提供的定义在 1 (完全不符合) 到 5 (完全符合) 的李克特量表上相符的程度。如 Colquitt 等人(2019)所述,我们甚至可以使用这些信息计算 HTC 指数:定义对应性评分的*均值 / 可能的锚点数量。例如,如果 5 位 SME 对项目 i 的*均对应性评分为 4.20:4.20/5 = 0.84。

如果你已经收集了 SME 评分,你应该将其格式化并作为单独的数据框包含在这里。 注意:你应该将 SME 评分格式化为单列,每个项目列为一行。这将使合并不同的数据框成为可能。

#SME = pd.read_csv(r'C:\XXX insert own filepath here)
#SME = SME.T
#SME.columns = Data.columns

合并数据和绝对值

现在,我们简单地合并这些不同的数据框(SME(可选)和项目级统计数据)。项目的名称需要在数据框之间匹配,否则 pandas 会添加额外的行。然后,我们转置数据以符合最终 scikit-criteria 程序的要求。

mergeddata = pd.concat([std, skewdf, kurtosisdf], axis=0)
mergeddata.index = ['STD', 'Skew', "Kurtosis"]
mergeddata = mergeddata.T
mergeddata

最后,由于偏度和峰度的范围可以是负值或正值,我们取绝对值,因为这样更容易处理。

mergeddata['Skew'] = mergeddata['Skew'].abs()
mergeddata['Kurtosis'] = mergeddata['Kurtosis'].abs()

Scikit-criteria 决策矩阵和排名项目

现在我们利用 scikit-criteria 决策程序根据多个标准对这些项目进行排名。如下面所示,我们必须传递数据框的值(mergeddata.values)、每个标准的输入目标(例如,最大值或最小值更为理想)和权重。虽然默认代码对每个标准具有相等的权重,但如果你使用 SME 评分,我强烈建议将更多的权重分配给这些评分。其他项目级统计数据只有在我们测量的构念是我们打算测量的构念时才重要!

最后,替代方案和标准仅是传递到 scikit-criteria 包中的名称,以便理解我们的输出。

dmat = skc.mkdm(
    mergeddata.values, objectives=[max, min, min],
    weights=[.33, .33, .33],
    alternatives=["it1", "it2", "it3", "it4", "it5", "it6", "it7", "it8", "it9", "it10"],
    criteria=["SD", "Skew", "Kurt"])

过滤器

scikit-criteria 最棒的部分之一是它们的 filters 函数。这允许我们过滤掉不需要的项目级统计数据,并防止这些项目进入最终选择排名阶段。例如,如果一个项目的标准差极高——这表明回答问题的受访者差异很大,我们不希望该项目进入最终选择阶段。对于 SME 评分(如上所述为可选项),这一点尤其重要。在这里,我们只会保留那些分数高于最低阈值的项目——这可以防止那些定义对应极差的项目(例如,SME *均评分为 1 或 2)在其他项目级统计数据良好的情况下成为排名靠前的项目。下面是过滤器的应用,但由于我们的数据已经在这些值范围内,因此不会影响最终结果。

from skcriteria.preprocessing import filters

########################### SD FILTER ###########################
# For this, we apply a filter: to only view items with SD higher than .50 and lower than 1.50
# These ranges will shift based upon your likert scale options (e.g., 1-5, 1-7, 1-100)

## SD lower limit filter
SDLL = filters.FilterGE({"SD": 0.50})
SDLL

dmatSDLL = SDLL.transform(dmat)
dmatSDLL

## SD upper limit filter
SDUL = filters.FilterLT({"SD": 1.50})
dmatSDUL = SDUL.transform(dmatSDLL)
dmatSDUL

## Whenever it is your final filter applied, I suggest changing the name
dmatfinal = dmatSDUL
dmatfinal

# Similarly, for SME ratings (if used), we may only want to consider items that have an SME above the median of our scale.
# For example, we may set the filter to only consider items with SME ratings above 3 on a 5-point likert scale

########################### SME FILTER ###########################

# Values are not set to run because we don't have SME ratings
# To utilize this: simply remove the # and change the decision matrix input
# in the below sections

#SMEFILT = filters.FilterGE({"SME": 3.00})

#dmatfinal = SME.transform(dmatSDUL)
#dmatfinal

注意:这也可以应用于偏度和峰度值。许多科学家会利用一个通用的经验规则,即偏度和峰度在-1.00 到+1.00 之间是可以接受的(Warner, 2013);你只需创建如上所示的上限和下限过滤器,结合标准差即可。

反转和缩放标准

接下来,我们将偏度和峰度值反转,通过 invert_objects.InvertMinimize() 使所有标准最大化。scikit-criteria 程序更倾向于将所有标准最大化,因为这有助于最后一步(例如,总和权重)。最后,我们对每个标准进行缩放,以便于比较和权重求和。每个值都除以该列中所有标准的总和,以便于每个标准的最佳值比较(例如,it1 的标准差为 1.199,这除以列总数 12.031 得到 0.099)。

# skcriteria prefers to deal with maxmizing all criteria
# Here, we invert our skewness and kurtosis. Higher values will then be more desirable

from skcriteria.preprocessing import invert_objectives, scalers

inv = invert_objectives.InvertMinimize()
dmatfinal = inv.transform(dmatfinal)

# Now we scale each criteria into an easy to understand 0 to 1 index
# The closer to 1, the more desirable the item statistic

scaler = scalers.SumScaler(target="both")
dmatfinal = scaler.transform(dmatfinal)
dmatfinal

求和和排名之前的最终标准值。

最终排名(总和权重)

最后,我们可以使用多种方法来应用这个决策矩阵,但最简单的方法之一是计算加权总和。在这里,将每个项目的行进行求和(例如,标准差 + 偏度 + 峰度),然后由程序进行排名。

## Now we simply rank these items ##

from skcriteria.madm import simple
decision = simple.WeightedSumModel()
ranking = decision.evaluate(dmatfinal)
ranking

对于练习数据集,排名如下:

保存第二步的数据

最后,我们保存原始数据集和清理后的数据集用于第二步(这里是我们的原始‘Data’数据框,不是我们的决策矩阵‘dmatfinal’)。在第二步中,我们将输入在第一步中排名较高的项目。

## Save this data for step 2 ##

Data.to_csv(r'C:\InputYourDesiredFilePathandName.csv')

第二步:最终项目优化以确保可靠性和有效性

在第一步中,我们根据各项目的统计数据对所有项目进行了排序。现在,我们利用 R 中的优化应用程序OASIS(由 Cortina 等人开发,2020 年;见 用户指南)。OASIS 计算器运行多个项目组合,并确定哪种项目组合可以产生最高的可靠性(以及适用时的收敛和区分效度)。在这个例子中,我们关注两个常见的可靠性指标:cronbach’s alpha 和 omega。这些指标通常非常相似,但许多研究人员主张 omega 应作为主要的可靠性指标(参见 Cho & Kim,2015;McNeish,2018)。Omega 是衡量可靠性的指标,它确定一组项目在单一“因素”(例如 构念,如工作满意度)上的加载程度。类似于 Cronbach's alpha(内部可靠性的一种衡量方式),值越高越好,通常在学术研究中,值超过 .70(最大上限 = 1.00)被认为是可靠的。

由于 shiny 应用程序的存在,OASIS 计算器使用起来非常简单。以下代码将安装所需的程序并弹出一个对话框(如下所示)。现在,我们从第一步中选择我们的原始清理数据集。在我们的示例中,我选择了前 8 个项目,要求至少 3 个项目和最多 8 个项目。如果你有收敛或区分效度测量,你可以在这一步输入它们。否则,我们请求计算 omega-h。

install.packages(c("shiny","shinythemes","dplyr","gtools","Lambda4","DT","psych", "GPArotation", "mice"))
library(shiny)
runUrl("https://orgscience.uncc.edu/sites/orgscience.uncc.edu/files/media/OASIS.zip")

OASIS 计算器的输入

最终结果

如下所示,5 项目方案产生了最高的 omega (ω = .73) 和 Cronbach alpha 系数 (α = .75),符合传统的学术可靠性标准。如果我们有收敛和区分效度的测量,我们还可以使用这些值对项目组合进行排序。OASIS 计算器还允许你为每个值选择一般范围(例如,只显示高于某些值的组合)。

让我们比较一下我们的最终解决方案:

缩短量表与全长量表的比较

与完整的 10 项目测量相比,我们的最终项目集花费的时间只有一半,具有可比的、可接受的可靠性水*(ω 和 α >.70),稍高的标准差和较低的偏度,但不幸的是,峰度较高(不过,它仍在 -1.00 到 +1.00 的可接受范围内)。

这个最终缩短的项目集可能是一个非常合适的候选者来替代完整的测量。如果成功复制到所有调查测量中,这可能会将调查长度减少一半。用户可能需要采取额外步骤来验证新的缩短测量是否按预期工作(例如,预测有效性和调查名义网络——缩短的测量是否与完整长度量表具有可比的预测?)。

警示

  1. 这种方法可能会产生在语法上冗余或缺乏内容覆盖的最终结果。用户应通过确保第二步中选择的最终项目集具有足够的内容覆盖,或使用 OASIS 计算器的内容映射功能(见文档)来调整。例如,您可能有一个人格动机评估,它有多个‘子因素’(例如,您是否是外部或内在动机)。如果您没有在 OASIS 计算器中进行内容映射或考虑这一点,您可能会仅得到来自一个子因素的条目。

  2. 您的结果可能会因样本而有所不同。由于两个步骤都使用现有数据来‘最大化’结果,您可能会看到未来样本中的可靠性或项目级统计数据有所下降。然而,这不应是显著的。

  3. 依赖于您的组织/样本,您的数据可能因为来源单一而自然偏斜。例如,如果公司 X 要求所有经理参与某些行为,那么询问这些行为的条目可能(希望)会偏斜(即,所有经理的评分都很高)。

结论

本文介绍了一种两步法,旨在显著减少调查问卷的长度,同时最大化可靠性和有效性。在以开源人格数据为例的说明中,调查问卷的长度减少了一半,但仍保持了高水*的 Cronbach 和 Omega 可靠性。虽然可能需要额外的步骤(例如,复制和比较预测有效性),但这种方法为用户提供了一种数据驱动的强大方法,可以显著减少员工调查的长度,这最终可以改善数据质量、减少受访者流失,并节省员工时间。

参考文献

J. B. Cabral, N. A. Luczywo 和 J. L. Zanazzi, Scikit-Criteria:集成到 Python 科学堆栈的多标准分析方法集合(2016),第 45 届阿根廷计算机与运筹学会议(45JAIIO)——第十四届阿根廷运筹学研讨会(SIO),59–66。

E. Cho 和 S. Kim, Cronbach 的α系数:知名但理解不深(2015),组织研究方法18(2), 207–230。

J. Colquitt, T. Sabey, J. Rodell 和 E. Hill, 内容验证指南:定义对应性和定义独特性的评估标准(2019),应用心理学期刊,104(10), 1243–1265。

J. Cortina, Z. Sheng, S. Keener, K. Keeler, L. Grubb, N. Schmitt, S. Tonidandel, K. Summerville, E. Heggestad 和 G. Banks, 从阿尔法到欧米伽及更远!回顾《应用心理学期刊》中心理测量的过去、现在与(可能的)未来(2020),《应用心理学期刊》, 105(12), 1351–1381。

P. Edwards, I. Roberts, M. Clarke, C. DiGuiseppi, S. Pratap, R. Wentz 和 I. Kwan, 提高邮寄问卷的响应率:系统综述(2002),《BMJ》, 324, 1–9。

M. Galesic 和 M. Bosnjak, 问卷长度对参与和响应质量指标的影响(2009),《公众意见季刊》, 73(2), 349–360。

L. Goldberg, 大五因素结构标记的发展(1992),《心理评估》, 4, 26–42。

T. Hinkin, 问卷测量开发的简要教程(1998),《组织研究方法》, 1(1), 104–121。

T. Hinkin 和 J. Tracey, 一种方差分析方法用于内容验证(1999),《组织研究方法》, 2(2), 175–186。

M. Hoerger, 互联网介导的大学研究中参与者退选与调查长度的关系:对研究设计和心理学研究中自愿参与的影响(2010),《网络心理学、行为与社会网络》, 13(6), 697–700。

B. Holtom, Y. Baruch, H. Aguinis 和 G. Ballinger, 调查响应率:趋势与有效性评估框架(2022),《人际关系》, 75(8), 1560–1584。

D. Jeong, S. Aggarwal, J. Robinson, N. Kumar, A. Spearot 和 D. Park, 详尽还是令人疲惫?关于长问卷中受访者疲劳的证据(2023),《发展经济学期刊》, 161, 1–20。

P. Levy, 《工业/组织心理学:理解工作场所》(第 3 版)(2010),Worth 出版。

D. McNeish, 感谢系数阿尔法,我们会从这里继续(2018),《心理学方法》, 23(3), 412–433。

A. Peytchev 和 E. Peytcheva, 调查长度导致的测量误差减少:分裂问卷设计方法的评估(2017),《调查研究方法》, 4(11), 361–368。

S. Porter, 提高响应率:有效的措施是什么?(2004),《机构研究新方向》, 5–21。

A. Rolstad 和 A. Rydén, 提高邮寄问卷的响应率:系统综述(2002)。《BMJ》, 324

R. Warner, 《应用统计学:从双变量到多变量技术》(第 2 版)(2013),SAGE 出版。

F. Yammarino, S. Skinner 和 T. Childers, 理解邮件调查响应行为的元分析(1991),《公众意见季刊》, 55(4), 613–639。

《垄断游戏的数据驱动策略模拟》

原文:towardsdatascience.com/a-data-driven-tactics-simulation-for-monopoly-864e7cffe508

使用数千次 MATLAB 模拟的视觉指南,帮助你在下次《垄断》游戏中占据优势

Jake MitchellTowards Data Science Jake Mitchell

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

--

图片由Joshua Hoehne拍摄,来自Unsplash

介绍:

在我上一篇文章的续集中,模拟垄断,我将探讨玩家的购买策略以及这些策略如何影响游戏结果。我将通过在 MATLAB 中建模游戏,然后模拟数千场游戏来寻找玩家选择中的模式。

编码游戏:

要模拟垄断游戏,你只需要模拟掷骰子的过程,并根据骰子的点数移动玩家。然后根据玩家落在什么类型的格子上应用一系列条件语句。如果玩家落在一处地产上,要么决定购买该地产,要么支付租金给所有者。如果玩家落在“前往监狱”上,将玩家的位置更改为“监狱”。游戏的这一部分在代码中非常直接,详细内容可以参考我上面链接的上一篇文章。

对我来说最令人兴奋的代码部分是涉及购买地产、建造房屋/酒店以及交易的决策。当玩家决定购买地产时,需要考虑两个因素:

  • 价格比率 — 玩家资金与地产价格的比例。比例越小,对玩家财务健康的影响越大。

  • 购买系数 — 游戏开始时分配的一个随机值。购买系数越低,玩家的购买习惯越冒险。

这两个值被输入到决策因子公式中。这个公式是一个修改过的 S 型函数,其结果在 0 和 1 之间。结果接* 0 意味着玩家在这种情况下绝不会购买该物业,而结果接* 1 则表示对玩家来说是个好交易。然后,随机数生成器产生一个在 0 和 1 之间的数字。如果这个数字小于决策因子,玩家就会购买该物业(见下方代码)。

price_ratio = player_data(turn,1)/property_data(i,2); % calculates price ratio
decision_factor = 1/(1 + exp(-1 * price_ratio + player_data(turn,3))); % calculates decision factor
if change == 1 % if buying property would result in a monopoly
  player_data(turn,3) = player_data(turn,3) + mon_weight; % increases chance of getting property
end
if rand(1) < decision_factor % if player decided to buy property
  property_data(i,11) = turn; % assignes property to new owner
  player_data(turn,1) = player_data(turn,1) - property_data(i,2); % subtracts price from owner's money
end

这两个值和相关函数在游戏中的每个决策点都适用于所有玩家。甚至还为那些可以为玩家创造垄断的物业添加了额外的激励。

游戏被模拟直到除 1 名玩家外所有玩家破产,此时游戏结束,统计数据被记录。然后一切被重置,新游戏开始。重复 1,000 次。

结果:

以下结果和分析是基于 6 人玩游戏的假设进行的。

你应该更加冒险,还是在用钱时更保守?

下图显示了你的购买策略如何影响你在游戏过程中的*均财富。随着你从 x 轴的左侧移动到右侧,购买策略从冒险转为保守。

图像由作者提供。

你可以争辩说,基于左下角较暗的点簇,冒险策略略微有优势,但差别不大。这张图不幸地显示了购买策略与成功率之间没有相关性,这让人非常失望。为了证明这与玩家数量无关,这里是 4 名玩家和 8 名玩家的相同结果:

4 名玩家(左)和 8 名玩家(右)。图像由作者提供。

再次根据这些图表,几乎没有证据表明更冒险的策略可以提高你的*均财富。此外,接* 0 的点积累是因为在几乎所有人都以 0 美元结束的游戏中,你会期望*均值偏向那个方向。

所以我没有理想的购买策略可以使用?

事实证明,有一种购买策略可能对玩家有利,但实际上它与对手的玩法关系更大。

下图与之前的图几乎相同,但现在 x 轴是购买系数比率,即你的系数与对手*均系数的比率。低于 1 的值表示你比对手更冒险。高于 1 的值表示你更保守。

图像由作者提供。

基于这个图表,我们可以更确定地说,相比于你的对手,更加冒险的购买策略会增加你预期的*均财富。 这是因为在 x 轴值为 0.5 到 1 之间,y 轴上的点显著增加。

我们也可以说,相比于你的对手,更保守会减少你预期的*均财富。 图表的右侧显示,较为保守的玩家几乎没有大量剩余金钱的情况。

如果我应该在购买物业时比对手更冒险,那么我应该关注哪些物业?

在 1000 场模拟游戏中,我查看了最终获胜者在第一个对手破产时所拥有的物业:

图片由作者提供。

x 轴仅仅是棋盘上的瓷砖位置。就像你把棋盘的 4 个边拆开并排成一行一样。颜色与标准大富翁棋盘上的物业相匹配,除了铁路用黑点表示,公共事业用灰点表示。

从这个图表来看,很明显获胜的玩家瞄准了 3 个特定的物业组。他们瞄准了浅蓝色、粉色和橙色的物业组(参见上方棋盘上的红色圆圈)。所有这些物业的拥有率都远超过 35%,其中浅蓝色物业的拥有率接* 45%。

为什么赢得比赛的玩家通常拥有这些物业?

理想的物业是那些投资回报率优良的物业。你不希望拥有那些需要大约 80 回合才能收回投资的物业。下图对此进行了完美的描述:

图片由作者提供。

回本回合数是通过计算物业的*均支出,包括初始购买和任何后续增加的住房,然后将其除以每回合的*均租金收入,以找到收回投资所需的*均回合数。

赢得比赛的玩家最有可能拥有的物业通常位于图表的底部,这意味着收回投资所需的时间较少,因此赚取利润的时间更多。下图展示了这些赢家如何真正重视具有优良投资回报率的物业。

图片由作者提供。

浅蓝色、粉色和橙色的物业显然是棋盘上的最佳选择。这一点很重要,因为你不仅要比对手更冒险,还要将这些风险集中在这 3 组物业上。

可以给这种风险水*分配什么量化值?

我追踪了模拟游戏中所有 1,000 名赢家的财富,并将每次掷骰子的金额*均化,制作了这个图示:

图片由作者提供。

*均赢家在 6 人游戏中花钱购买属性直到手中剩下 750\(到 800\)之间。 这应该在大约 60–70 次掷骰子的过程中完成,或者在所有玩家之间大约 10 个完整周期。

一旦*均赢家的金额达到了起始金额的* 3 倍,他们就开始重新投资于那些玩家破产后变得可用的属性。 他们此时也开始专注于购买房屋和酒店。

结论:

  1. 仅仅以冒险的方式玩是不够的,你还应该尝试比你的对手更冒险。至少,要匹配对手的游戏风格。

  2. 你的购买策略应该围绕尝试获取浅蓝色、粉色和橙色属性。

  3. 目标是游戏早期花费大约一半的起始金额用于购买属性。

感谢你花时间阅读我的文章!如果你对这种内容感兴趣,我推荐你阅读我的其他棋盘游戏模拟文章,包括教机器玩四连棋模拟曼卡拉

一个关于作物产量和价格预测的数据科学课程项目,我至今不感到羞愧

原文:towardsdatascience.com/a-data-science-course-project-about-crop-yield-and-price-prediction-im-still-not-ashamed-of-75712dc8696f?source=collection_archive---------0-----------------------#2023-12-28

即便从一位经验丰富的数据科学家的角度来看

米哈伊尔·萨拉法诺夫Towards Data Science 米哈伊尔·萨拉法诺夫

·

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

--

预览图(作者提供)

你好,亲爱的读者!在这些圣诞假期期间,我感到对过去的学生岁月有些怀念。因此,我决定写一篇关于一个学生项目的文章,这个项目是在差不多四年前作为 ITMO 大学硕士课程“多变量数据分析方法与模型”的项目完成的。

免责声明: 我决定写这篇文章有两个原因:

  1. 分享一种组织大学学习的有效方法(至少对我来说);

  2. 向刚开始学习编程和/或统计学的人们提供灵感,让他们尝试和实验他们的宠物项目或课程项目,因为有时候这些项目对许多人来说是难忘且出乎意料的愉快

文章以提示格式提到了一些我在课程项目中能够应用的良好实践。

故事的开端

因此,在课程开始时,我们被告知学生可以自组 2–3 人的团队,并提出一个课程项目,我们将在课程结束时进行展示。在学习过程中(大约 5 个月),我们将向讲师做中期汇报。这样,教授们可以看到进展如何(或没有进展)。

之后,我立即和我的伙伴们:EgorCamilo 组成了团队(因为我们知道一起玩乐很有趣),然后开始思考主题…

选择主题

我建议选择

  1. 一个足够大的主题,以便我们可以独立地在不同部分上工作

  2. 与我们兴趣接*的领域(我对地理信息分析感兴趣,我的同事对经济学感兴趣)

所以,这就是…

图 1. 选择的主题(作者提供的图像)

Camilo 还想尝试制作可视化的仪表板(使用 PowerBI),但几乎任何任务都适合这种愿望。

提示 1:选择一个你(和你的同事)会充满热情的主题。它可能不是一个非常流行的项目,但你会享受在“晚上”做这件事的过程

主要思想是什么

课程包含了大量的主题,每个主题都是统计分析方法的集合。我们决定尝试以尽可能多的不同方式预测产量和作物价格,然后使用一些统计方法对预测结果进行集成。这让我们能够在实践中尝试课程中讨论的大多数方法。

此外,时空数据确实是多维的——这与课程的主要主题非常契合。

剧透:我们都得了 5 分中的 5 分

研究(& 数据来源)

我们从文献回顾开始,以准确了解作物产量和作物价格的预测方法。我们还想了解什么样的预测误差可以被认为是令人满意的。

我不会在这篇文章中引用这次评审得到的论文。我只是提到,我们决定使用以下指标和阈值来评估解决方案的质量(无论是作物产量还是作物价格):

可接受的性能:*均绝对百分比误差 (MAPE) 对于一个相当好的预测应不超过 10%

提示 2:在启动你的项目(无论是工作还是学习期间)之前,先回顾一下当代的解决方案。也许你现在关注的问题已经有人解决过了。

提示 3:在开始开发之前,确定你将使用什么指标来评估解决方案。记住,你无法改进你无法衡量的东西。

回到研究中,我们确定了以下数据来源(链接截至 2023 年 12 月 28 日为最新):

为什么选择这些来源? — 我们假设作物的价格将取决于生产的产品数量。在农业中,生产数量取决于天气条件。

该模型用于:

  • 小麦、稻米、玉米和大麦的产量;

  • 国家:德国、法国、意大利、罗马尼亚、波兰、奥地利、荷兰、瑞士、西班牙和捷克共和国。

气候数据预处理;

因此,我们从一个假设开始:“小麦、稻米、玉米和大麦的产量取决于上半年(至 6 月 30 日)的天气条件”(图 2);

图 2. 产量预测特征生成(以活跃温度之和为例)(图像由作者提供)

从欧洲航天局网站获得的源档案包含 netCDF 文件。这些文件包含以下参数的每日字段:

  • *均日均气温,℃;

  • 每日最低气温,℃;

  • 最高日均气温,℃;

  • 气压,HPa;

  • 降水量,mm;

基于初步字段,计算了每年上半年的以下参数:

  • 上半年总降雨量,mm(见动画);

  • 上半年降水天数,天;

  • *均气压,hpa;

  • 上半年最高日均气温,℃;

  • 上半年最低日均气温,℃;

  • 10 摄氏度以上的活跃温度之和,℃(见图 3)。

动画。上半年的降水量,毫米。图表顶部列出了年份(由作者提供)

图 3. 1950 年上半年活跃温度之和,摄氏度(图像由作者提供)

因此,我们获得了整个欧洲地区的矩阵,为未来的模型计算了特征。读者可能会注意到,我计算了一个参数,即“10 摄氏度以上的活跃温度之和”。这是生态学和植物学中非常流行的一个参数,有助于确定不同物种(主要是植物)的温度最佳值,例如 “作为确定‘S̆ampion’和‘Ligol’苹果品种最佳收获日期的方法的活跃温度之和”)

提示 4:如果你在该领域拥有专业知识(与数据科学无关),确保在项目中加以利用——展示你不仅在进行“拟合预测”,而且在调整和改进领域特定的方法

下一步是按国家聚合信息。分别从气象参数矩阵中提取每个国家的值(图 4)。

图 4. 含有国家边界的矩阵(图像由作者提供)

我想指出,这个策略是有道理的(图 5):例如,图片显示对西班牙而言,小麦产量几乎不受活跃温度之和的影响。然而,对于捷克共和国,温暖的上半年更可能导致较低的产量。因此,为每个国家单独建模是一个好主意。

图 5. 小麦产量(吨/公顷)与活跃温度之和的依赖关系(图像由作者提供)

并非所有国家的领土都适合农业。因此,有必要仅从特定像素中聚合信息。为了考虑农业用地的位置,准备了以下矩阵(图 6)。

图 6. 土地利用矩阵(图像由作者提供)

1. 讲座主题为:单变量统计检验

所以,我们的数据已经准备好了。然而,农业是一个非常复杂的行业,每年、每十年都有显著改进。限制模型的训练样本可能是有意义的。为此,我们使用了累计和方法(图 7):

累积和方法: 对样本中的每一个数字,依次将后续数字累加。也就是说,如果样本仅包含三年:1950、1951 和 1952,那么 1950 年的数值将在 Y 轴上绘制为 1950,而 1951 将显示 1950 和 1951 的和,依此类推。

  • 如果线的形状接*直线且没有断裂,样本是均匀的

  • 如果线的形状有断裂,样本将基于此断裂分为两部分

图 7. 法国。跨年度目标变量比较:小麦(每公顷吨数)(图像由作者提供)

如果检测到断裂,我们将两个样本比较是否属于总体(Kolmogorov-Smirnov 统计量)。如果样本在统计上显著不同,我们将使用第二部分训练预测模型。如果没有,我们将使用整个样本。

提示 5:不要害怕结合统计分析的方法(这是课程项目!)。例如,在讲座中,我们没有被告知累积和方法——主题是比较分布。然而,我之前在处理冰图时使用了这种方法来比较冰况趋势。我认为这在这里也可能有用

我在这里应该指出,我们假设过程是遍历的,因此决定以这种方式进行比较。

准备好后,我们可以开始构建统计模型了——让我们来看看最有趣的部分吧!

2. 讲座主题是:多元回归

以下特征被纳入模型:

  • 总降雨量;

  • 降水天数;

  • 高于 10 ℃ 的活跃温度之和;

  • *均压力;

  • 最低气温 ℃。

目标变量:小麦稻米玉米大麦

验证年份:2008–2018 每个国家

让我们继续查看可视化,以使其更清晰。

图 8. 基于初始化模型,为每个国家生成的特定年份预测表面(图像由作者提供)

这里是图 9,显示了残差残差 = 观测值 - 估计(预测)值)来自法国和意大利的线性模型:

图 9. 验证样本上线性回归的残差和指标的可视化(图像由作者提供)

从图表中可以看出,指标令人满意,但误差分布偏离零——这意味着模型存在系统误差。我们在下面的新模型中尝试进行修正

验证样本 MAPE 指标值:10.42%

提示 6:从最简单的模型开始(例如线性回归)。这将为你提供一个基线,以便与改进版本的模型进行比较。模型越简单越好,只要它显示出令人满意的指标

3. 本讲座的主题是:多元分布分析

我们将本讲座的材料转化为一个“分布分析”模型。假设很简单——我们分析了每年的气候参数分布以及当前年份的分布,并找到与当前年份最相似的年份,以预测与过去已知值完全相同的产量(图 10)。

图 10. 选择年份类比的两两比较概念(图片由作者提供)

想法: 具有相似气象条件的年份,其产量也将相似

方法: 比较温度、降水量和气压分布的两两对比。预测与考虑年份最相似的年份的产量。

使用的分布:

  • 年初的温度,2 月、4 月、6 月的温度;

  • 年初的降水量,2 月、4 月、6 月的降水量;

  • 年初的气压,2 月、4 月、6 月的气压。

我们使用了 Kruskal-Wallis 检验来比较分布。为了调整 p 值,引入了多重检验修正——Bonferroni 修正。

图 11. 2000 年和 2018 年的气温分布(图片由作者提供)

验证样本的 MAPE 度量值:13.80%

提示 7:如果你进行多个统计检验,别忘了包括修正(例如,Bonferroni 修正)

4. 本讲座的主题是:贝叶斯网络

一次讲座集中于贝叶斯网络。因此,我们决定将这种方法适用于产量预测。我们认为每年由一组变量 A、B、C 等描述,其中 A 是描述作物产量的类别集合,B 是例如活跃温度条件的总和,以此类推。例如,A 可以取三个值:“高作物产量”、“中等作物产量”、“低作物产量”。B 和 C 及其他也相同。因此,如果我们对条件和目标变量进行分类,我们得到每年的以下描述:

  • 1950 年 — “高热量供应”,“低降水供应”,“高气压” — “高作物产量”

  • 1951 年 — “低热量供应”,“高降水供应”,“高气压” — “中等作物产量”

  • 1952 年 — “低热量供应”,“低降水供应”,“高气压” — 哪种作物产量?

该算法旨在基于三个其他类别的组合来预测产量类别:

  • 作物产量(3 个类别)— 隐藏状态 — 目标变量

  • 活跃温度总和(3 个类别)

  • 降水量(3 个类别)

  • *均压力(3 个类别)

我们如何定义这些类别?——通过使用聚类算法!例如,确定了用于小麦产量的以下 3 个集群

图 12:用于贝叶斯网络分析的小麦产量集群(图片由作者提供)

该模型的最终预测——预测集群的*均产量。

验证样本 MAPE 指标值:14.55%

8 个提示:做实验!时间序列预测的贝叶斯网络与聚类?——当然!分布的成对分析——为什么不呢?有时最大胆的方法会带来显著的改进

5. 讲座主题为:时间序列预测

当然,我们可以将目标变量作为时间序列进行预测。我们的任务是了解经典预测方法在理论和实践中的工作原理。

将这种方法付诸实践被证明是最简单的。例如,在 Python 中有几个库可以定制和应用 ARIMA 模型,例如 pmdarima

图 13:应用 ARIMA 模型预测产量时间序列。X 轴:时间索引,Y 轴:大麦产量(图片由作者提供)

验证样本 MAPE 指标值:10.41%

9 个提示:不要忘记与经典方法进行比较。一个抽象的指标不能告诉你的同事你的模型有多好,但与知名标准的比较会显示真正的性能水*

6. 讲座主题为:集成方法

所有模型建立后,我们探讨了每个模型是如何“出错”的(回忆线性回归模型的残差图——见图 9):

图 14:不同作物产量预测模型的残差图(图片由作者提供)

没有一个呈现的算法能突破 10%的门槛(根据 MAPE)。

使用了卡尔曼滤波器来提高预测质量(进行集成)。对于一些国家已经取得了令人满意的结果(图 15)

图 15:使用集成方法对不同国家的作物产量预测(图片由作者提供)

验证样本 MAPE 指标值:9.89%

10 个提示:如果有人要求我将开发的模型集成到生产服务中,我会选择集成 ARIMA 或线性回归,即使集成指标更好。然而,商业问题中的指标有时不是关键。一个独立模型有时比集成模型更好,因为它更简单,更可靠(即使错误指标稍高)

期货价格预测

最后一部分:模型(Lasso 回归),使用预测的产量值和期货特征来估计可能的价格值(图 16):

图 16. 小麦期货价格预测(图片来源:作者)

验证样本上的 Mape:6.61%

为什么我仍然认为这个项目是一个好项目

故事到此为止。上面贴出了一些建议。在最后一段,我想总结最后一点,说明为什么我对这个项目感到满意。这里有三个主要方面:

  • 工作组织和主题选择——我们很好地结合了各自的优势和最佳品质,合理规划了工作流程,并作为一个团队成功准备了一个好的项目并按时交付。因此,我提升了自己的软技能;

  • 有意义的主题——我对我们所做的事情充满热情。即使我现在有几周的空闲时间做一个小项目,我也会很高兴将我目前的经验和技能应用到这样的案例研究中。所以,我对我们完成的工作感到满意;

  • 硬技能——在我们的工作中,我们尝试了新的统计方法,提高了对已有方法的理解,并提升了编程技能

好吧,我们在考试中也得了很高的分数 XD

我希望你在大学和其他地方的项目都能给你带来同样的激动。新年快乐!

此致敬礼,米哈伊尔·萨拉法诺夫

一个使用 ChatGPT 代码解释器的数据科学项目

原文:towardsdatascience.com/a-data-science-project-with-chatgpt-code-interpreter-e9beb8705dac

使用 ChatGPT 最新插件构建一个端到端的数据科学项目。

Natassha SelvarajTowards Data Science Natassha Selvaraj

·发布于Towards Data Science ·12 分钟阅读·2023 年 7 月 20 日

--

图片由Firmbee.com提供,Unsplash

作为一个目前同时管理全职数据科学工作和多个自由职业项目的人,我通常是第一个尝试可能减少我周转时间的工具的人。

当 ChatGPT 在过去一周开始向订阅者推出代码解释器插件时,我迫不及待想将其融入我的数据科学项目中。

如果你还没有听说过这个工具,代码解释器是一个允许你上传文档并在 ChatGPT 界面运行 Python 程序的插件。

过去我们需要手动将数据复制粘贴到 ChatGPT 中并等待回应的时代已经过去。

借助代码解释器,你可以简单地上传数据集,让工具在几分钟内分析数据、构建机器学习模型并生成可视化。

在这篇文章中,我将向你展示如何使用代码解释器来执行一个端到端的数据科学项目。

任务——客户细分

在我以前的公司,我担任市场数据科学家。

这意味着我可以利用客户数据来提高销售——通过识别我们最盈利的用户、预测流失率,并建立应该在未来营销活动中针对的目标人群画像。

我甚至写了一篇教程关于如何用 Python 构建客户细分模型,其中我使用了一个公开的数据集来识别每个客户对电子商务公司的价值。

在本文中,我们将对相同的数据集进行客户细分。不过这一次,我们将使用 ChatGPT Code Interpreter 来帮助我们建立模型。

前提条件

我们将使用 Kaggle 的 电子商务数据集 进行这次分析。该数据集来源于 UCI 机器学习库,包括了一个基于英国的电子商务公司真实的零售交易信息。

该数据集在 Creative Commons Attribution 4.0 International (CC BY 4.0) 许可下发布。

要开始这个分析,确保你安装了 Python IDE,并且安装了 Pandas、Matplotlib 和 Seaborn 库。

最后,如果你能访问 Code Interpreter,将更容易跟随本教程。

不过,这个插件目前仅对 ChatGPT Plus 订阅用户开放,因此如果你无法访问它,你仍然可以在我的 GitHub 仓库 找到本次分析中使用的所有代码。

如果你有 ChatGPT Plus 订阅,可以观看 这个 YouTube 视频来了解如何访问 Code Interpreter。

第一步:理解数据集

在我们将数据上传到 Code Interpreter 之前,让我们先加载到 Python 中,自行查看:

import pandas as pd

df = pd.read_csv('ecommerce_dataset.csv', encoding='latin-1')
df.head()

图片来自作者

这个数据集包含了电子商务公司收集的交易数据。包括了所购商品、价格、购买来源和数量的信息。

注意,我们没有很多定性的信息可以使用。

如果数据集中包含了关于客户人口统计的信息、他们互动过的广告活动或他们访问过的网站,我们可以进行处方分析,并提出目标对象的推荐。

然而,这个数据集仅包含交易数据。我们可以进行的分析类型受到限制。

如前所述,我们将对该数据集进行客户细分。

不过,让我们向 Code Interpreter 请求一些关于如何分析这个数据集的想法,看看它会提出什么。

图片来自作者

看那边!

在查看数据集后,模型的第一个建议是进行客户细分。

其他建议包括分析销售趋势随时间的变化以及识别表现最佳的产品。

在继续之前,我希望你再次查看数据框的前几行:

图片来自作者

客户细分 是根据共享特征创建不同受众群体的过程。

例如,如果我在卖瑜伽垫,我可以创建两个客户细分——健身爱好者和关注健康的成年人。

然而,我们在这个数据框中没有那种人口统计或心理数据。由于我们只有历史交易数据可以使用,因此可用的细分技术不多。

在我之前对这个电子商务数据集的分析中,我使用了一个叫做 RFM 分析的技术来进行客户细分。

这帮助我使用有限的数据点识别了公司最有利润的客户。

让我们看看 Code Interpreter 能否提出如何进行客户细分的建议:

作者提供的图像

看这!

Code Interpreter 建议我们使用 RFM 分析来识别高利润用户的细分。

让我们进一步分解这个问题。

第 2 步:Code Interpreter 解释——什么是 RFM 分析?

作者提供的图像

RFM 代表最*一次购买、频率和货币价值。

最*一次购买:

使用数据集中交易信息,我们可以识别每个客户最*一次购买的时间。

最*一次购买公司的客户一个月前的利润更高,而不是一年前见过的客户。

Code Interpreter 告诉我们,我们可以通过从数据集中最*一次发票中减去每个客户的最*一次购买的日期来计算客户的最*一次购买。

频率:

频率是一个相当直接的指标——它告诉我们客户与组织做生意的频率。

Code Interpreter 告诉我们,我们可以通过简单地计算每个客户的唯一发票数量来计算它。

货币价值:

最后,客户的货币价值告诉我们他们在公司交易中花费的总金额。

第 3 步:执行 RFM 分析

在解释了 RFM 的含义后,Code Interpreter 运行了一些 Python 代码以对电子商务数据集进行 RFM 分析。以下是生成的结果:

作者提供的图像

提示:你可以点击“显示工作”以查看 Code Interpreter 生成的代码。

注意,每个客户 ID 现在都映射到一个 RFM 分数。

Code Interpreter 还解释了这些结果的含义:它表示一个 326 的最*一次购买分数表明客户在数据集中最*的日期之前的 326 天进行了最后一次购买。

如果你不理解解释的任何概念,你可以简单地提示 Code Interpreter,让它提供更多示例或甚至一个描述该主题的视觉图。

第 4 步:客户细分

作者提供的图像

生成每个客户的 RFM 分数后,Code Interpreter 说我们现在可以使用这些分数进行客户细分。

这给我们提供了两个选项——我们可以通过根据分位数划分客户群体来手动进行分段,或者使用像 K-Means 聚类这样的无监督算法。

在决定使用哪个方法之前,让我们了解每种方法的优缺点:

What is the best way to perform this segmentation? 
What are some advantages/disadvantages of ranking over clustering 
with K-Means?

图片由作者提供

图片由作者提供

代码解释器表示,虽然基于分位数的方法更易于实现,但没有固定的方法论,这意味着我们必须根据对业务的理解来决定每个分段的切割点。

代码解释器一针见血!

它识别出我认为的分位数基础分段方法的最大缺点——RFM 分数将作为整体来生成段。

例如,如果 Alison 的最*购买评分为 5,购买频次为 4,货币值为 2,她的总评分为11。分位数基础的方法将只考虑这个总评分(11):

图片由作者提供

如果我们使用这种技术,我们将失去一个关键的信息。

Alison 的最*购买频率和购买频次评分很高,这意味着她购物频繁且最*有过购买。但她每次购物的花费不多。

为了最大限度地利用 Alison 作为客户,商店需要针对她提供偶尔的折扣和特别促销。Alison 的价值在于她的品牌忠诚度和频繁的店铺访问。

她无法作为高价产品的目标,因为她可能没有足够的购买力。

由于分位数基础的分段方法的这个缺点,我们继续使用K-Means 聚类

第 5 步:代码解释器执行 K-Means 聚类

K-Means 聚类是一种无监督的机器学习技术,用于将数据分成不重叠的子群体。

如果你想了解更多关于 K-Means 聚类如何工作的知识,你可以阅读这个教程。

代码解释器提到,K-Means 聚类技术的一个缺点是确定我们想要创建的子群体数量。

K-Means 聚类的 3 个簇示例(图片由作者提供)

这是因为 K-Means 聚类依赖于用户输入来决定我们想要构建的簇或受众群体的数量。

让我们看看代码解释器是否能帮助我们做出这个决定。

图片由作者提供

模型建议了诸如肘部法、轮廓分析和间隙统计等技术。

我们继续使用肘部法

这是一种简单的技术,通过绘制模型在一系列簇(例如 1 到 10)上的误差值图,并选择曲线的肘部作为簇的数量。

这是我给代码解释器的提示:

Can we build a K-Means clustering model using the RFM scores that were 
calculated? Use the elbow method to determine the appropriate number of 
clusters.

模型生成了这张图:

图片由作者提供

图片由作者提供

模型显示“肘部”点,即其拐点,大约在 3 或 4 附*——具体位置还不明确。

Code Interpreter 正在要求我们根据对业务的了解,决定是否继续使用 3 个或 4 个聚类。

由于更多的聚类可以让我们分析更多的用户细分市场,接下来我们用 4 个聚类来建立模型。

图片由作者提供

我请 Code Interpreter 给出输出数据框的一个示例:

图片由作者提供

很好!

Code Interpreter 已成功将数据集中所有用户分组为4 个客户细分市场。每个客户 ID 现在都映射到一个聚类。

第 6 步:Code Interpreter 进行聚类分析

到目前为止,我们采取的所有步骤都很顺利。

Code Interpreter 已成功描述数据集,建议了分析技术,进行了 RFM 分析,甚至建立了一个机器学习模型。

然而,数据科学家的真正价值在于他们能够生成具有商业价值的洞察。

让我们看看 Code Interpreter 是否能够解释所创建的客户细分市场,并提出可能带来收入的建议。

我打算让模型可视化每个聚类的*均 RFM 值:

Can we visualize the clusters that were created? 
Let's create a separate chart for each cluster. 
Each chart should showcase the cluster's mean R, F, and M scores.

Code Interpreter 生成了这张图表以可视化每个聚类的 RFM 分数:

图片由作者提供

注意图表上*期性和频率值几乎不可见。

这是因为这些变量的尺度不同:

图片由作者提供

注意“货币价值”列中的数据点明显高于“*期性”和“频率”分数。

我很惊讶 Code Interpreter 在我们要求其创建图表时没有指出这一点。

让我们询问模型一些改善清晰度的建议:

图片由作者提供

好的,Code Interpreter 建议我们要么标准化数据,要么为每个指标使用单独的图表,或者创建雷达图。

让我们选择第二个选项,为每个指标创建单独的条形图:

To overcome the issue of them being on different scales, 
can we create separate bar charts for recency, frequency, and monetary value?
Each chart would display 4 clusters and 1 metric.

这是 Code Interpreter 生成的图表:

图片由作者提供

请记住,较低的*期性分数更好的,因为这意味着客户最*有过购买行为。

频率货币价值则正好相反。

我认为第 1 类是最有利可图的细分市场——它具有较低的*期性,以及较高的频率和货币价值。

让我们看看 Code Interpreter 说了什么:

图片由作者提供

好吧,它只是读取图表并告诉我们哪些细分市场具有最佳的最*性、频率和货币价值。

让我们尝试从模型中获得更多见解:

图片由作者提供

这表明 Cluster 2 似乎是最有利可图的细分市场。

噢哦……看起来代码解释器犯了个错误!

Cluster 1 的 RFM 评分似乎比 Cluster 2 更好。不过,当我指出这一点时,代码解释器纠正了这一遗漏:

图片由作者提供

让我们更进一步,要求模型提出每个细分市场应该如何被针对的建议:

Based on their RFM scores alone, can you come up with some 
personalized targeting recommendations on the best way to 
reach each customer segment?

我要求模型将这些信息以表格形式呈现,以便更清晰:

图片由作者提供

这些建议很不错。

在 RFM 方面,Cluster 0 远远是表现最差的细分市场,因此如果公司尚未失去这些客户,重新接触至关重要。

Cluster 1 是表现最好的细分市场,包含最有可能在新高端产品上消费的用户。

Cluster 2 包含购买次数购物频率都很高的个人。他们在公司的产品上花费适中,如果我们想要获得更多的收益,就需要进行追加销售。

最后,Cluster 3 的用户在最*性、频率和货币价值方面表现中等。他们比 Cluster 0 中的个人更有价值,但比 Cluster 1 和 Cluster 2 的群体收益少。

为他们提供特别优惠并获取反馈以了解如何将他们转化为高价值客户是有意义的。

这些建议很有道理,考虑到模型没有额外的数据点作为参考,也没有关于公司的背景信息,我感到很印象深刻。

使用代码解释器进行数据科学——下一步

ChatGPT 代码解释器将 GPT-4 的推理能力与分析你的文件和运行代码的能力结合起来。

这使得它成为一个强大的工具,可以在创纪录的时间内构建机器学习模型和执行数据科学工作流。

它也是学习数据科学的一个很好的工具。

如果你在进行数据科学项目但不知道如何继续或对应该构建的模型类型有疑问,代码解释器是你最好的朋友。

你可以将其用作个人数据科学导师——只需上传你正在处理的数据集,并询问应该使用什么技术进行分析。

但请记住,由于它在后台使用了 ChatGPT,代码解释器会产生幻觉和犯错,就像在本教程的“集群分析”部分一样。

因此,核实代码解释器告诉你的所有信息是很重要的,特别是如果你正在构建一个模型或进行分析并将其展示给第三方时。

本文到此为止,感谢阅读!

量化空间连续性的面向数据科学家的变异函数教程

原文:towardsdatascience.com/a-data-scientist-friendly-variogram-tutorial-for-quantifying-spatial-continuity-1d2f29dcfb51

应用在一个合成矿业数据集上,使用开源的 GSLib 和 Python

Fouad FarajTowards Data Science Fouad Faraj

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

--

图片来源:Sebastian PichlerUnsplash

介绍

变异函数用于展示空间数据的距离相关变异性。理解和建模变异函数的空间连续性很重要,因为它们被用来将点测量估算到实际的块中,广泛应用于矿石品位、石油浓度或环境污染物等领域。

尽管有开源选项可以生成变异函数,由于其复杂性,大多数用户依赖于昂贵的软件包,这些软件包抽象了许多细节。这个教程旨在简要介绍变异函数以及如何使用开源地统计学库(GSLib),它可以独立使用或与 Python 结合使用来开发变异函数。

这里在一个合成矿业数据集上开发了一个变异函数模型,但该工作流程也可用于气象应用如温度或环境应用如污染物跟踪等任何类型的空间数据。

教程要求

我们需要 GSLib,免费提供 这里 下载,以及一些最基本的、常用的 Python 库,这些库也包含在上传到 github 的完整代码中。

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

变异函数基础

变异函数的一般思想是,相距较远的数据点比相距较*的数据点更有可能显著不同。数据点之间的方差随着距离的增加而增加,最终达到与数据的全局方差相等的点。

我们从空间数据集开始,可以将变异函数建模工作流程概括为以下几个步骤。首先,我们需要确定适当的变异函数搜索参数。然后识别主要和次要的连续性轴。最后,可以建模变异函数,并随后用于估算或模拟目的。每一步将在以下部分中进一步解释,并在教程中应用于数据集。

开发变异函数模型到空间数据集的一般工作流程。图像由作者提供

变异函数建模参数:基台、范围和上限

变异函数模型由三个组成部分构成,这些部分描述了数据的不同属性:

  • 基台:变异函数模型的 Y 轴截距,表示在接*零距离处的即刻变异性或单点的随机性

  • 范围:变异函数趋于*稳后的距离,表示该距离的点不再具有空间相关性

  • 上限:建模数据的方差,一旦距离一定的点方差达到它,就不再有空间相关性

下面的变异函数示意图说明了每个模型参数。在到达范围之前,数据被认为是空间相关的。范围较长的变异函数比范围较短的更具有空间连续性。高基台变异函数表明在接*零距离处的变异性较高,这通常表明数据集更具变异性和不连续性。

示意图展示了变异函数模型如何拟合一组实验数据,突出相关的变异函数建模参数。图像由作者提供

变异函数开发

为了生成实验变异函数数据,我们需要定义对。每对由一个头和一个尾组成,间隔一定距离。我们增加距离以测试对之间的*均差异,并了解它如何随距离变化。如果我们处理的是二维数据,则在变异函数建模中有几个不同的标准搜索参数,如下所示。搜索参数带有容差,因为找到精确的 2.0000 米方向上的匹配是不太可能的。

示意图说明了二维变异函数搜索参数。图像由作者提供

确定参数后,搜索将迭代应用于所有点,以在每个滞后时段开发实验变异函数数据,如下所示对于一个点。每个滞后时段的*均变异函数被计算出来,最终达到上限或全局数据的方差。

示意图显示了从一个点生成实验变异函数数据时的所有 n 个滞后。图像由作者提供

每个延迟的计算只是所有配对的差值*方的一半,如下所示。这个变异函数值将对比延迟绘制出来以发展变异函数。

标注的变异函数方程。图像来源:作者

使用 GSLib 的 gamv 开发实验性变异函数数据

现在要使用 GSLib 的 gamv 实际计算实验性变异函数数据,我们首先需要定义搜索参数。下方是搜索参数示意图,旁边是标注的截图,指示了对应的 GSLib gamv 参数文件输入。

示意图展示了 2D 变异函数搜索参数,旁边是 GSLib 参数文件截图。图像来源:作者

设置参数后,确保你的空间数据符合 GSLib 所需的 .dat 格式,如下所示为我们的空间铜品位数据示例。

GSLib gamv 数据格式的标注记事本截图。图像来源:作者

现在要运行 GSLib 代码,你可以使用命令窗口,导航到工作目录,然后执行 gamv,如下所示:

运行 GSLib gamv 和典型返回反馈的输入行的标注截图。图像来源:作者

输出变异函数将保存在 gamv.out 文件中,数据排序如下面所示:

GSLib gamv 输出的标注截图,指示每一列的内容。图像来源:作者

之后你可以以任何你喜欢的方式分析输出,本教程将使用 Python。

在合成矿数据集上的应用

我们使用的合成钻孔数据集是基于真实的铜矿建模的。钻孔数据在二维空间中大约分布为 7x7 米,东向和北向以米为单位测量,每个点都有一个测量的铜品位,单位为重量百分比(wt%)。本教程中使用的所有代码和数据均可在 github 上找到。下图展示了铜品位及其在空间中的分布情况。

铜矿品位数据集的地图视图。图像来源:作者

以下 GSLib 变异函数搜索参数适用于约 7x7 米的空间数据,我们将用它们来生成我们的变异函数。这里 是一个很好的来源,用于根据空间数据确定合适的搜索参数。

  • 延迟分隔距离:7 米,大致为数据间隔

  • 延迟容差:3.5 米,经验法则是将延迟容差设为间隔的一半

  • 延迟数:30,大部分区域覆盖在 30*7 米(210 米)中

  • 方位容差:22.5 度,可能略小如 15 度,但我们希望确保能获得足够多的配对

  • 带宽:10 米,可能略小如 7.5 米,但我们希望确保能获得足够多的配对

  • 变异函数类型:半变异函数

确定最小和最大不连续性的主方向

由于我们在 2D 环境中工作,我们需要识别两个正交方向,即高和低连续性,通常称为主轴和次轴。确定主方向的方法有多种,这里介绍的最基本方法是生成多个等间隔的变差函数对,找出包涵最高和最低空间连续性的两个方向。其他有用的方法包括生成变差函数图,这可以直观地显示空间连续性,GSLib 提供了用于变差函数图的软件包,并且可以在这里找到使用指南。

下面我们看到在 15 度间隔下开发的变差函数对,可以清楚地看出,方位角为 000°和 090°的变差函数分别具有最高和最低的连续性。

六个正交对的实验变差函数数据突出显示了 000°和 090°方位角观察到的高和低连续性。图像来源:作者

我们可以快速检查 000°和 090°方位角方向的品位图,看看它们是否合理地对应于最连续和最不连续的方向。正如下面重新生成的图所示,最小连续性在东西方向是合理的,因为在该方向上品位从最高值过渡到最低值,而南北方向的品位变化不大。

铜品位图,突出显示了东西方向的低连续性和南北方向的高连续性及其相应的方位角。图像来源:作者

变差函数建模

000°和 090°方位角的实验变差函数数据使用指数模型进行建模。测试了不同的“豆子”值、范围和部分高程,直到获得了定性较好的拟合。使用嵌套结构和多模型,或探索如在矿业中常用的球形模型等不同模型,可能会获得对实验数据更好的拟合。

次轴和主轴的指数变差函数模型,参数已表格化。图像来源:作者

现在,变差函数模型可以用于对点数据进行任何类型的估算或模拟。GSLib 也提供了运行常用普通克里金估算或序贯高斯模拟所需的软件包。

结论

理解和建模空间连续性具有挑战性,特别是对于没有地质统计背景的人。这里我们提供了变差函数的简要介绍和使用开源软件生成变差函数模型的快速教程。许多用户目前使用昂贵的软件,因为空间连续性分析的编码经验总体不足,开源软件如 GSLib 并不广为人知。

此外,许多商业软件会抽象掉变差函数的重要细节,这些细节可能会影响所开发的模型。各种地质统计学文本都强调不要将变差函数建模视为黑箱,并且花费的精力去理解数据中的情况对于生成稳健的变差函数模型非常有用。

数据科学家探索性数据分析的必备指南

原文:towardsdatascience.com/a-data-scientists-essential-guide-to-exploratory-data-analysis-25637eee0cf6?source=collection_archive---------0-----------------------#2023-05-30

实操教程

理解数据的最佳实践、技术和工具

Miriam SantosTowards Data Science Miriam Santos

·

关注 发表在 Towards Data Science ·11 分钟阅读·2023 年 5 月 30 日

--

如果没有正确的方法和工具,EDA 可能会感觉像是无尽而压倒性的任务。 图片来源:Devon DivineUnsplash

介绍

探索性数据分析(EDA)是在每个数据科学项目开始时必须进行的最重要任务。

从本质上讲,它涉及彻底检查和描述你的数据,以发现其潜在的特征、可能的异常以及隐藏的模式关系

对数据的理解将最终指导你完成机器学习流程中的后续步骤,从数据预处理到模型构建和结果分析。

EDA 过程基本上包含三个主要任务:

  • 第一步: 数据集概览和描述统计

  • 第二步: 特征评估和可视化,以及

  • 第三步: 数据质量评估

正如你可能猜到的,每个任务都可能涉及相当全面的分析,这很容易让你像疯子一样切片、打印和绘制 pandas 数据框

除非你选择了正确的工具。

在这篇文章中,我们将深入探讨 有效 EDA 过程的每一步,并讨论为什么你应该将 [ydata-profiling](https://github.com/ydataai/ydata-profiling) 变成你掌握它的一站式商店。

为了展示最佳实践和调查见解,我们将使用 成人收入数据集,该数据集在 Kaggle 或 UCI 仓库中免费提供(许可证:CC0: 公共领域)。

第一步:数据概览和描述统计

当我们第一次接触一个未知的数据集时,脑海中会立刻冒出一个自动的想法:我在处理什么?

我们需要对数据有深入的理解,以便在未来的机器学习任务中高效处理它。

作为经验法则,我们通常开始时会相对数据的观察数、特征的数量和类型、总体缺失率以及重复观察的百分比来描述数据。

通过一些 pandas 操作和合适的备忘单,我们最终可以用一些简短的代码片段打印出上述信息:

数据集概览:成人普查数据集。观察数、特征、特征类型、重复行和缺失值。作者提供的片段。

总的来说,输出格式不是理想的……如果你对 pandas 熟悉,你也会知道标准的操作模式是从 df.describe() 开始 EDA 过程。

成人数据集:通过 df.describe() 展示的主要统计数据。作者提供的图像。

然而,这仅仅考虑了数值特征。我们可以使用 df.describe(include='object') 打印出一些关于分类特征(计数、唯一、模式、频率)的额外信息,但简单检查现有类别需要一些更详细的操作:

数据集概览:成人普查数据集。打印每个分类特征的现有类别及其相应的频率。作者提供的片段。

然而,我们可以这样做 —— 并且猜猜看,所有后续的 EDA 任务! —— 仅用一行代码,使用 [ydata-profiling](https://github.com/ydataai/ydata-profiling)

成人普查数据集的剖析报告,使用 ydata-profiling。摘录来源:作者。

上面的代码生成了一个完整的数据剖析报告,我们可以利用这个报告进一步推进我们的 EDA 过程,而无需编写更多的代码!

我们将在接下来的部分中详细介绍报告的各个部分。关于数据的整体特征,我们寻找的所有信息都包含在概览部分

ydata-profiling: 数据概况报告 — 数据集概览。图片来源:作者。

我们可以看到我们的数据集包含了15 个特征和 32561 个观测值,其中有 23 条重复记录,整体缺失率为 0.9%

此外,数据集已被正确识别为表格数据集,且相当异质,呈现了数值特征和分类特征。对于时间序列数据,它具有时间依赖性并呈现不同类型的模式,ydata-profiling会在报告中加入其他统计信息和分析。

我们可以进一步检查原始数据和现有的重复记录,以对特征有一个总体的了解,然后再进行更复杂的分析:

ydata-profiling: 数据概况报告 — 样本预览。图片来源:作者。

从数据样本的简要预览中,我们可以立即看出,尽管数据集整体缺失数据的百分比较低,某些特征可能受到影响比其他特征更多。我们还可以识别出一些特征有相当数量的类别以及零值特征(或至少有大量的零)。

ydata-profiling: 数据概况报告 — 重复行预览。图片来源:作者。

关于重复的行,考虑到大多数特征表示的是多个可能“同时适用”的类别,发现“重复”的观测值并不奇怪。

但也许“数据异味”是这些观测值共享相同的age值(这是可能的),以及完全相同的fnlwgt,考虑到呈现的值,这似乎更难以置信。因此需要进一步的分析,但我们很可能在之后需要去除这些重复数据

总体而言,数据概览可能是一个简单的分析,但却极具影响力,因为它将帮助我们定义接下来的任务。

步骤 2:特征评估与可视化

在查看总体数据描述符后,我们需要深入分析数据集的特征,以获取有关其个别属性的见解——单变量分析——以及它们的交互和关系——多变量分析

这两个任务都极度依赖于调查适当的统计数据和可视化,这些统计数据和可视化需要针对特征类型(例如,数值型、类别型)和行为(例如,交互、相关性)进行量身定制

让我们来看看每个任务的最佳实践。

单变量分析

分析每个特征的个别特征至关重要,因为这将帮助我们决定它们对分析的相关性以及可能需要的准备数据类型以实现最佳结果。

例如,我们可能会发现极端的值,这可能指示不一致性异常值。我们可能需要标准化 数值 数据或根据现有类别的数量执行类别特征的独热编码。或者我们可能需要进行额外的数据准备,以处理偏移或偏斜的数值特征,如果我们打算使用的机器学习算法期望特定的分布(通常是高斯分布)。

因此,最佳实践要求对单个属性进行彻底调查,例如描述性统计和数据分布。

这些将突显出后续任务的必要性,如异常值移除、标准化、标签编码、数据填充、数据增强和其他类型的预处理。

让我们更详细地调查种族资本收益我们能立即发现什么?

ydata-profiling: Profiling Report(种族和资本收益)。作者提供的图像。

**资本收益**的评估很简单:**鉴于数据分布,我们可能会质疑该特征是否对我们的分析有任何价值,因为 91.7%的值为“0”。

分析 **种族** 稍微复杂一些:

其他种族(除了白人)明显被低估。这带来了两个主要问题:

  • 一个是机器学习算法通常忽略较少表示的概念,这被称为小样本问题,这会导致学习性能降低;

  • 另一个问题有些衍生于此:由于我们处理的是一个敏感特征,这种“忽略倾向”可能会带来直接与偏见公*性问题**相关的后果。这是我们绝对不希望渗入到模型中的。

鉴于此,也许我们应该考虑基于不足代表的类别进行数据增强,以及考虑模型评估中的公*性指标,以检查与race值相关的性能差异。

我们将在讨论数据质量最佳实践(第 3 步)时进一步详细说明需要解决的其他数据特征。 这个例子仅仅展示了通过评估每个特征的属性我们可以获得多少见解。

最后,请注意,如前所述,不同的特征类型需要不同的统计和可视化策略:

  • 数值特征通常包含有关均值、标准差、偏度、峰度以及其他分位数统计的信息,并且最适合使用直方图绘制;

  • 分类特征通常使用众数和频率表来描述,并通过条形图进行分类分析。

ydata-profiling: Profiling Report. 所呈现的统计数据和可视化结果已根据每个特征类型进行了调整。屏幕录像:作者。

这样的详细分析使用通用的 pandas 操作会显得繁琐,但幸运的是ydata-profiling将所有这些功能集成在 ProfileReport中,方便我们使用:代码片段中没有添加额外的代码!

多变量分析

对于多变量分析,最佳实践主要集中在两种策略:分析特征之间的互动以及分析它们的相关性

分析互动

互动让我们直观地探索每对特征的行为,即一个特征的值如何与另一个特征的值相关。

例如,它们可能会展现出正向负向关系,这取决于一个值的增加是否与另一个值的增加或减少相关。

ydata-profiling: Profiling Report — Interactions. 图片来源:作者。

agehours.per.week之间的互动为例,我们可以看到大多数工作者的工作时间为标准的 40 小时。然而,也有一些“忙碌的蜜蜂”在 30 至 45 岁之间工作时间超过 40 小时(甚至达到 60 或 65 小时)。20 多岁的人则不太可能过度工作,有时可能会有较轻的工作安排。

分析相关性

与互动类似,相关性让我们 分析特征之间的关系。然而,相关性“给出了一个值”,使我们更容易确定这种关系的“强度”。

这种“强度”是通过相关系数来衡量的,可以通过数值分析(例如,检查相关矩阵)或使用热图来分析,热图使用颜色和阴影来直观突出有趣的模式:

ydata-profiling: Profiling Report — Heatmap and Correlation Matrix. 屏幕录制由作者提供。

关于我们的数据集,注意educationeducation.num之间的相关性。这实际上是它们包含相同的信息education.num只是对education值的分箱。

另一个引人注目的模式是性别关系之间的相关性,尽管再次说明并不非常有信息量:查看这两个特征的值,我们会发现这些特征很可能相关,因为男性女性通常分别对应丈夫妻子

这些冗余类型的特征可以检查是否需要从分析中移除(例如,marital.status也与relationshipsex相关;native.countryrace等)。

ydata-profiling: Profiling Report — Correlations. 图像由作者提供。

然而,还有其他显著的相关性,可能对我们的分析有趣。

例如,性别职业之间的相关性,或者性别每周工作小时数之间的相关性。

最终,收入与其余特征之间的相关性确实很有信息量特别是当我们试图绘制分类问题时。了解与目标类别最相关的特征有助于我们识别出最具区分性的特征,并发现可能影响模型的潜在数据泄漏者。

从热图中看,marital.statusrelationship似乎是最重要的预测变量,而fnlwgt例如,似乎对结果没有太大影响。

类似于数据描述符和可视化,交互和相关性也需要关注当前特征的类型。

换句话说,不同的组合将使用不同的相关系数进行测量。默认情况下,ydata-profilingauto模式下运行相关性分析,这意味着:

如果你想查看其他相关系数(例如,Pearson、Kendall、Phi),你可以轻松地配置报告参数

第 3 步:数据质量评估

随着我们向数据中心化的 AI 开发模式迈进,掌握可能的复杂因素对于我们数据中的出现问题至关重要。

所谓“复杂因素”是指在数据收集或处理过程中可能出现的错误,或数据固有特征,它们只是数据性质的反映。

这些包括缺失数据、不*衡数据、常量值、重复数据、高度相关冗余特征、噪声数据等。

数据质量问题:错误和数据固有特征。图片由作者提供。

在项目开始时发现这些数据质量问题(并在开发过程中持续监控)至关重要。

如果在模型构建阶段之前没有识别和解决这些问题,它们可能会危及整个机器学习流程以及由此得出的后续分析和结论。

如果没有自动化的过程,识别和解决这些问题的能力将完全依赖于进行探索性数据分析(EDA)的人员的个人经验和专业知识,这显然并不理想。再加上,尤其是考虑到高维数据集,这真是一个巨大的负担。即将迎来噩梦警报!

这是ydata-profiling最受欢迎的功能之一,即自动生成数据质量警报

ydata-profiling: 数据概况报告 — 数据质量警报。图片由作者提供。

该分析报告输出至少 5 种不同类型的数据质量问题,即duplicateshigh correlationimbalancemissingzeros

事实上,我们已经在进行第 2 步时识别出了一些这些问题:race是一个高度不*衡的特征,而capital.gain主要被 0 填充。我们还看到educationeducation.num,以及relationshipsex之间存在紧密的相关性。

分析缺失数据模式

在考虑的全面警报范围中,ydata-profiling分析缺失数据模式方面尤其有用。

由于缺失数据是现实世界领域中非常常见的问题,可能会完全影响某些分类器的应用或严重偏倚其预测,另一个最佳实践是仔细分析缺失数据的百分比和我们特征可能显示的行为:

ydata-profiling: 数据概况报告 — 分析缺失值。作者录屏。

从数据警报部分,我们已经知道workclassoccupationnative.country有缺失的观测值。热图进一步告诉我们occupationworkclass的缺失模式之间存在直接关系:当一个特征缺失时,另一个特征也会缺失。

关键洞察:数据分析超越了 EDA!

到目前为止,我们讨论了构成全面 EDA 过程的任务以及数据质量问题和特征的评估一个我们可以称之为数据分析的过程 — 确实是最佳实践。

然而,重要的是要澄清,数据分析 超越了 EDA。 我们通常将 EDA 定义为在开发任何类型的数据管道之前的探索性、互动性步骤,而数据分析是一个迭代过程应在每个步骤中进行 的数据预处理和模型构建过程中。

结论

高效的 EDA 奠定了成功机器学习管道的基础。

这就像对您的数据进行诊断,了解它包含的所有信息 — 其属性关系问题 — 以便您能够在后续阶段以最佳方式解决这些问题。

这也是我们灵感阶段的开始:从 EDA 中问题和假设开始产生,并计划进行分析以验证或排除这些假设。

在整篇文章中,我们涵盖了引导您进行有效 EDA 的 3 个主要基本步骤,并讨论了拥有一款顶尖工具 — ydata-profiling — 的影响,这不仅能够指引我们正确的方向,还节省了大量时间和脑力负担

我希望这份指南能帮助您掌握“数据侦探”的艺术,并且一如既往地,非常感谢您的反馈、问题和建议。告诉我您希望我写些什么其他主题,或者更好的是,来数据中心 AI 社区与我见面,让我们合作吧!

关于我

博士,机器学习研究员,教育者,数据倡导者,以及全能型人才。在 Medium 上,我撰写关于数据中心 AI 和数据质量的文章,教育数据科学与机器学习社区如何将不完善的数据转化为智能数据。

数据中心 AI 社区 | GitHub | Google Scholar | LinkedIn

数据科学家提高 Python 代码质量的指南

原文:towardsdatascience.com/a-data-scientists-guide-to-improving-python-code-quality-21660ecea97d

编写符合生产标准的 Python 代码的工具和包

Egor HowellTowards Data Science Egor Howell

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

--

Christopher Gower 提供的照片,来源于 Unsplash

背景

如今,数据科学家在部署机器学习模型的生产环节中变得越来越重要。这意味着我们需要像其他软件工程师一样,能够编写符合生产标准的 Python 代码。在这篇文章中,我想介绍一些可以帮助你为下一个模型编写符合生产标准代码的关键工具和包。

代码检查工具

概述

代码检查工具 是一种捕捉小错误、格式错误和可能导致运行时问题及意外输出的奇怪设计模式的工具。

在 Python 中,我们有 PEP8,它幸运地为我们提供了一个全局的代码风格指南。虽然 Python 中存在许多符合 PEP8 的代码检查工具,但我个人的偏好是 flake8

Flake8

Flake8 实际上是 PyflakespycodestyleMcCabe 代码检查包的组合。它用于检查错误、代码异味 并强制执行 PEP8 标准。

要安装 flake8 使用 pip install flake8,并且你可以通过 flake8 <file_name.py> 使用它。真的就是这么简单!

例如,假设我们在一个文件 flake8_example.py 中有一个函数 add_numbers

def add_numbers(a,b):
    result = a+  b
    return result

print(add_numbers(5, 10))

要对这个文件调用 flake8,我们执行 flake8 flake8_example.py,输出结果如下:

作者提供的照片。

Flake8 发现了几个样式错误,我们应该修正这些错误以符合 PEP8。

有关 flake8 的更多信息以及如何根据需要自定义它,请参见此处

代码格式化工具

概述

Linters 通常只是告诉你代码中有什么问题,但不会主动为你修复。格式化工具会修复你的代码,帮助加快工作流程,确保代码遵循风格指南,并使其对其他人更具可读性。

isort

isort 包PEP8中指定的顺序对导入进行排序。可以通过pip install isort轻松安装。

导入应写在单独的行上:

# Correct
import pandas
import numpy 

# Incorrect 
import pandas, numpy

它们还应按以下顺序分组:

  • 标准库(例如sys

  • 相关第三方(例如pandas

  • 本地(例如,repo 中其他文件的函数)

# Correct
import math
import os
import sys

import pandas as pd

# Incorrect
import math
import os
import pandas as pd
import sys

最后,来自包的导入需要按字母顺序排列:

# Correct
from collections import Counter, defaultdict

# Incorrect
from collections import defaultdict, Counter

以下命令展示了如何从终端运行 isort:

# Format imports in every file
isort .

# Format in specific file
isort <file_name.py>

有关 isort 的更多信息,请查看他们的网站此处

Black

Black 根据其自己的风格指南重新格式化代码,该指南是PEP8的一个子集。有关当前 black 遵循的格式化指南,请参见此处

要安装 black,只需运行pip install black,然后在文件上调用black <file_name.py>

以下是一个名为black_example.py的文件示例:

# Before running black 
def   add_numbers  (  x, y ) :

    result= x  +y
    return result

然后我们运行black black_example.py

# After running black
def add_numbers(x, y):
    result = x + y
    return result

终端中的输出也将是这样的:

作者拍摄。

有关更多信息以及如何自定义你的 black 格式化工具,请参见他们的主页此处

单元测试

概述

单元测试提供了一个结构化的格式,以确保你的代码按照预期执行。它们测试代码的小部分,例如函数和类,以验证它们是否按预期行为。测试设置相当简单,可以节省你大量的调试时间,因此对数据科学家非常推荐。

PyTest

Pytest是最受欢迎的单元测试框架,与 Python 的本地单元测试包一起使用,可以通过pip install pytest轻松安装。

要使用 pytest,我们首先需要一个可以测试的函数。让我们回到我们的add_numbers函数,该函数将位于名为pytest_example.py的文件中:

def add_numbers(x, y):
    result = x + y
    return result

现在在一个名为test_pytest_example.py的单独文件中,我们编写相应函数的单元测试:

from pytest_example import add_numbers

def test_add_numbers():
  assert add_numbers(5, 13) == 18

要运行此测试,我们只需执行pytest test_pytest_example.py

作者拍摄。

如你所见,我们的测试通过了!

如果你想要更详细和全面的 pytest 和单元测试教程,请查看我之前关于这个主题的文章:

## 调试变得简单:使用 Pytest 跟踪和修复 Python 代码

初学者的单元测试教程及如何在 Pytest 中进行单元测试

towardsdatascience.com

类型检查器

概述

最后一个主题是类型系统,不是键盘类型!Python 是动态语言,这意味着它不强制变量的严格类型。变量x在同一代码中可以是整数也可以是字符串。然而,这可能会导致意外的错误。因此,有工具可以使 Python 更像是静态类型语言。

Mypy

我们可以通过使用包mypy来确保我们的变量和函数具有正确的预期类型。这个包检查输入和输出是否符合所需的类型。

例如,对于add_numbers函数,我们期望输入和输出都为float。这可以在函数中指定:

def add_numbers(x: float, y: float) -> float:
    result = x + y
    return result

print(add_numbers(10, 10))
print(add_numbers("10", "10"))

现在,假设我们将以下参数传递给函数并print结果:

print(add_numbers(10, 10))
print(add_numbers("10", "10"))

输出会如下所示:

print(add_numbers(10, 10))
>>> 20

print(add_numbers("10", "10"))
>>> 1010

我们看到第一个输出符合我们的预期,但第二个则不符合。这是因为我们传入了两个str类型,然而 Python 解释器没有报错,因为 Python 是动态语言。

我们可以使用 mypy 来捕获这些错误,避免下游出现任何 bug。为此,调用 mypy 命令为mypy <file_name.py>。所以,对于这个例子,我们执行mypy mypy_example.py

作者拍摄的照片。

如我们所见,mypy 发现第 6 行中指定的参数是str,而函数期望的是float

如果你想要更详细和全面的 mypy 和类型教程,请查看我之前关于这个主题的文章:

## 数据科学家的 Python 类型指南:提升代码清晰度

类型的重要性及其在 Python 中的应用

towardsdatascience.com

什么是需求?

总结一下,你可能会想,为什么我们需要这些工具?这些工具的最终目的是让你的 Python 代码具有:

  • 可读性: 你的代码变得对其他开发者和数据科学家更加直观和易读。这有助于更好的协作和更快的交付时间。

  • 鲁棒性: 代码将更不容易出错,并且引入错误的难度也更大,特别是使用单元测试时。

  • 更容易识别错误: 通过使用代码检查工具和测试,我们可以检测代码中的任何不一致和异常结果,从而降低在生产环境中出现代码错误的风险。

你可以在我的 GitHub 上查看本文中使用的完整代码:

[## Medium-Articles/Software Engineering /code-quality-example at main · egorhowell/Medium-Articles

我在我的 Medium 博客/文章中使用的代码。通过创建一个帐户来为 egorhowell/Medium-Articles 的开发做出贡献…

github.com](https://github.com/egorhowell/Medium-Articles/tree/main/Software Engineering /code-quality-example?source=post_page-----21660ecea97d--------------------------------)

参考文献与进一步阅读

另一个话题!

我有一个免费的通讯, Dishing the Data,在其中我每周分享成为更好数据科学家的小贴士。没有“空话”或“点击诱饵”,只有来自实践中的数据科学家的纯粹可操作的见解。

[## Dishing The Data | Egor Howell | Substack

如何成为更好的数据科学家。点击阅读《Dishing The Data》,作者 Egor Howell,Substack 发表的…

newsletter.egorhowell.com](https://newsletter.egorhowell.com/?source=post_page-----21660ecea97d--------------------------------)

与我联系!

Makefile 教程

原文:towardsdatascience.com/a-data-scientists-guide-to-make-and-makefiles-1595f39e0704

如何使用 Make 和 Makefiles 优化你的机器学习管道

Egor HowellTowards Data Science Egor Howell

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

--

图片由 Nubelson Fernandes 提供,来源于 Unsplash

背景

现在数据科学家需要编写生产代码来部署他们的机器学习算法。因此,我们需要了解软件工程标准和方法,以确保我们的模型稳健有效地部署。其中一个在开发者社区中非常知名的工具是 make。这是一个强大的 Linux 命令,开发者早已知晓,在这篇文章中我想展示它如何用于构建高效的机器学习管道。

什么是 Make?

make 是一个终端命令/可执行文件,类似于 lscd,存在于大多数类 UNIX 操作系统中,如 MacOS 和 Linux。

make 的使用是为了简化并将工作流程分解为逻辑上的 shell 命令组。

它被开发者广泛使用,也被数据科学家采纳,因为它简化了机器学习管道并使生产部署更具稳健性。

为什么要使用 Make 进行数据科学?

make 是一个强大的工具,数据科学家应该利用它,原因如下:

  • 自动化机器学习环境的设置

  • 更清晰的端到端管道文档

  • 更容易测试具有不同参数的模型

  • 项目的结构和执行显而易见

什么是 Makefile?

Makefile 基本上是 make 命令读取和执行的内容。它有三个组成部分:

  • 目标这些是你试图构建的文件,或者如果你只是执行命令,你会有一个 PHONY 目标。

  • 依赖: 在执行此目标之前需要运行的源文件。

  • 命令: 顾名思义,这些是生成目标的步骤列表。

基本示例

让我们通过一个非常简单的示例来使这个理论变得具体。

以下是一个Makefile,它有目标hello,并使用echo命令将'Hello World'打印到屏幕上,并且没有依赖关系:

# Define our target as PHONY as it does not generate files
.PHONY: hello

# Define our target
hello:
 echo "Hello World!"

我们可以通过在终端中简单地执行make hello来运行它,这将产生以下输出:

echo "Hello World!"
Hello World!

它本质上只是列出了并执行了命令。这就是make的本质,没有什么太复杂的。

注意我们将目标hello设置为.PHONY,因为它不会生成文件。这就是.PHONY的意义,只用于不产生文件的目标。

如果我们不想将echo命令输出到屏幕,可以在命令前添加@符号。

我们可以在Makefile中添加另一个目标来生成一个文件:

# Define some targets as PHONY as they do not generate files
.PHONY: hello

# Define our target
hello:
 echo "Hello World!"

# Define our target to generate a file
data.csv:
 touch data.csv

要运行data.csv目标,我们执行make data.csv

touch data.csv

你应该会在本地目录中看到一个data.csv文件。

机器学习管道

管道概述

以下是我们将使用Makefilemake构建的机器学习项目的示例管道。它基于一个先前的项目,在该项目中,我基于ARIMA模型预测了美国航空公司乘客的数量。你可以在这里查看更多内容:

## 如何使用 ARIMA 进行预测

ARIMA 预测模型介绍及其使用方法

[towardsdatascience.com

图示由作者提供。

因此,read_clean_data.py文件将加载并使时间序列数据*稳model.py文件将为清理后的数据拟合 ARIMA 模型。最后,analysis.py文件将计算我们预测的性能。

另一个关键点是文件之间的依赖关系。除非model.py已被执行,否则analysis.py无法运行。这就是Makefile中的依赖关系变得有用的地方。

演练

以下是我们的第一个文件read_clean_data.py

数据来自 Kaggle并带有 CC0 许可证。

GitHub Gist by author.

在这里,我们读取美国航空数据,通过差分使其*稳,并进行Box-Cox 变换,并将其保存到本地目录中的一个名为clean_data.csv的文件中。

然后,我们有了model.py文件:

GitHub Gist by author.

最后,我们有了分析文件analysis.py

GitHub Gist by author.

我们可以为我们的三阶段管道编写以下Makefile

.PHONY: all read_clean_data model analysis

all: analysis

read_clean_data:
 python read_clean_data.py

model: read_clean_data
 python model.py

analysis: model
 python analysis.py

.PHONY: clean
clean:
 rm -f clean_data.csv lam.pickle train_data.csv test_data.csv forecasts.csv

注意我们如何声明每一步对前一步的依赖,以确保我们拥有执行每一步所需的正确文件。我们还添加了 clean 目标,以便在需要时删除生成的文件。

整个管道可以通过 make all 命令运行,输出将如下所示:

Output:

python read_clean_data.py
python model.py
python analysis.py

并将生成以下图表:

作者用 Python 生成的图表。

如你所见,Makefile 文件管道工作正常,预测效果也相当不错!

总结与进一步思考

就这些!希望你喜欢这篇关于 makeMakefile 的简短教程。当然,你可以用这些工具做更多复杂和高级的事情,但这篇文章可以作为你的起点。需要记住的关键点是:

  • *make* 是一个 UNIX 命令,用于自动化某些工作流的运行

  • 一个 *Makefile* 允许我们编写多个 *make* 命令和序列来自动化机器学习管道

本文中使用的完整代码可以在我的 GitHub 上找到:

[## Medium-Articles/Software Engineering /make-example at main · egorhowell/Medium-Articles]

我在我的中等博客/文章中使用的代码。通过在…

github.com

参考文献与进一步阅读

另一件事!

我有一个免费的通讯,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-----1595f39e0704--------------------------------)

与我联系!

数据科学家的 Python 类型指南:提升代码清晰度

原文:towardsdatascience.com/a-data-scientists-guide-to-python-typing-boosting-code-clarity-194371b4ef05

类型的重要性以及如何在 Python 中实现

Egor HowellTowards Data Science Egor Howell

·发表于Towards Data Science ·阅读时长 4 分钟·2023 年 7 月 31 日

--

照片由Pankaj Patel拍摄,来源于Unsplash

什么是‘类型’?

这里的类型不是指物理上触碰键盘,而是我们 Python 代码中变量(和函数)所采用的数据类型!

Python 本质上是动态语言,这意味着没有正式的要求来声明变量的数据类型。例如,一个变量可能在开始时是整数,但在代码的其他地方变成字符串。这种灵活性常常会导致在运行时出现难以调试的错误。

其他语言是静态类型的,这意味着它们的变量类型需要明确声明,并且在运行时不能更改。如果一个变量被声明为整数,它在程序运行期间必须始终是整数。静态类型语言的例子有Fortran和 C++。

然而,*年来 Python 已经开发了对类型的支持,现在它已成为行业标准。对于需要将稳健的机器学习模型投入生产的数据科学家尤为重要。

在这篇文章中,我想带你了解 Python 中的基本语法和类型过程,以及如何使用mypy包,这使我们能够无缝地检查代码的类型。

PEP 484所示,实际上推荐使用类型注解。

基本示例

让我们通过一个简单的示例来解释在 Python 中进行类型检查的必要性。下面我们有一个将两个数字相加的函数,巧妙地命名为 adding_two_numbers

作者的 GitHub Gist。

两个 print 语句的输出是什么?首先是:

print(adding_two_numbers(5, 5))

>>> 10

这是预期中的情况。然而,第二个 print 语句的输出是:

print(adding_two_numbers("5", "5"))

>>> 55

尽管这个结果在‘技术上’是正确的,但显然不是我们在这个特定函数中试图实现的目标。

为了帮助解决这个问题,我们可以为函数添加 类型注解 以明确我们需要传递的参数类型和预期的 返回类型

作者的 GitHub Gist。

在上面的示例中,我们明确了 num1num2 应该 都是整数,预期的输出 应该 也是整数。

重要的是要提到,这些真的只是‘提示’,如果你传入一个字符串,在运行程序时仍然不会出现运行时错误,因为 Python 本质上是动态类型的。

所以,声明类型的通用语法是:

function (variable: variable_type) -> return_type

此外,如果你不确定对象或变量的数据类型,你可以通过调用 type() 函数来检查:

print(type(1))

>>> <class 'int'>

类型模块

如果你想让特定函数返回一个 list,但 list 中的每个元素都必须是整数怎么办?不幸的是,Python 的固有类型不能很容易做到这一点。这就是我们使用 typing 包的地方,可以通过运行 pip install typing 来安装。

我们可以使用 typing 包来更精细地声明我们的数据类型。以下是一些示例:

作者的 GitHub Gist。

typing 包中还有许多其他类型可以满足你遇到的‘任何’变量(无意的双关语!)。查看这个备忘单如果你有兴趣深入了解。

创建类型

你还可以通过简单地构造一个类来创建自己的类型。下面是我制作的 dog 类的示例:

作者的 GitHub Gist。

MyPy 教程

mypy 是现在检查 Python 代码类型的行业标准包。它几乎在所有生产部署的代码中使用,特别是机器学习算法,因此作为数据科学家掌握它是非常值得的。

要开始使用 mypy,只需将其安装为 pip install mypy。然后要使用它,你只需运行 mypy <file_name.py>。这就是全部!

如果你想学习 mypy 的一些更高级的功能,请查看这里

让我们通过一个示例来使这个概念更具体。如果我们回到之前的函数 adding_two_numbers,它看起来是这样的:

作者的 GitHub Gist。

然后,我们运行 mypy adding_two_numbers.py,输出结果如下:

adding_two_numbers.py:6: error: Argument 1 to "adding_two_numbers" has incompatible type "str"; expected "int"  [arg-type]
adding_two_numbers.py:6: error: Argument 2 to "adding_two_numbers" has incompatible type "str"; expected "int"  [arg-type]
Found 2 errors in 1 file (checked 1 source file)

注意,错误仅出现在第 6 行,因为我们传入了字符串类型,但函数期望的是整数类型。错误信息中甚至指出了这一点。

对于第 5 行的 print 语句没有引发错误,因为我们传入了正确的类型,函数也返回了预期的整数类型。

总结:优缺点

让我们总结一下 Python 类型注解的一些主要优缺点:

优点

  • 有助于 linting 并减少代码中出现错误的可能性。

  • 提高代码的可读性和文档化。

缺点

  • 实施和编写类型的时间。

  • 某些类型的向后兼容性并非所有 Python 版本都支持。

总体而言

类型注解是大多数 Python 代码中的行业标准程序,包括数据科学工作。因此,这是一项重要且相对容易学习和实现的技能。这不仅会使你的代码更直观,而且有助于防止你的机器学习模型在生产环境中出现故障!

参考资料与进一步阅读

另一个事项!

我有一个免费的新闻通讯,数据揭秘,每周分享成为更好的数据科学家的技巧。没有“废话”或“诱饵”,只有来自实践数据科学家的纯粹可操作见解。

[## 数据揭秘 | Egor Howell | Substack

如何成为更好的数据科学家。点击阅读《数据揭秘》,由 Egor Howell 编写的 Substack 出版物,内容包括…

newsletter.egorhowell.com

与我联系!

首席数据科学家的日常生活

原文:towardsdatascience.com/a-day-in-the-life-of-a-chief-data-scientist-1cbda76c631d

剧透警告 — 我做的数据科学工作不多!

Leah Berg 和 Ray McLendonTowards Data Science Leah Berg 和 Ray McLendon

·发布于 Towards Data Science ·13 分钟阅读·2023 年 2 月 6 日

--

图片由 Keenan Beasley 提供,来源于 Unsplash

几周前,我们写了一篇关于数据科学家日常生活的文章。如果你是新来的,可能还不知道的是,位数据科学家共同维护这个博客,我们的工作日和角色差异很大。Leah,上一篇文章的作者,是高级数据科学家,而我(Ray)是首席数据科学家。

首席数据科学家的角色(在一些公司也称为数据科学经理)在不同的组织中差异很大。例如,我的一些同行在类似的角色中有一个专门针对特定业务线或产品的团队。相比之下,我管理的是一个企业级团队,几乎与整个组织中的每个业务线都有合作。

这样的细节对我的时间分配有很大的影响,与我的同行相比,他们可能有一个客户,而我有许多。他们可能会做更多的数据科学工作,而我则有机会了解和接触各种业务领域。总的来说,我喜欢我的工作,获得了硬技能和软技能的良好结合。

尽管每天都不同,但以下任务代表了我作为首席数据科学家管理企业级团队的典型一天。

一天概览

  • 4:30–6:30 — 开始我的一天

  • 6:30–9:00 — 深度工作

  • 9:00–9:30 — Scrum 会议

  • 10:00–10:30 — 一对一会议

  • 10:30–11:00 — 查看邮件

  • 11:00–12:00 — 办公时间

  • 12:00–1:30 — 锻炼、午餐、小憩

  • 1:30–2:30 — 完全自由时间

  • 2:30–3:00 — 查看邮件

  • 3:00–3:30 — 与客户的产品会议

  • 3:30–4:30 — 计划我的下周(仅限周五)

在我们深入之前,我想强调一下,我的一天流程反映了我如何参与lifestyle design movement。我意识到不是每个人都有这样的工作方式,但这正是这份工作让我感到非常适合的原因。我在选择做什么以及如何完成所有任务时拥有很大的自主权。这仅仅因为我所产生的工作质量和数量。

开始我的一天

我通常不会晚于早上 6:30 开始一天。我有一个庞大的家庭,如果我不早起锻炼,我的孩子们会把我叫醒。不幸的是,我的睡眠计划常常被打乱,我经常比计划早醒一两个小时。

当这种情况发生时,我会去当地的健身房游泳,然后去桑拿房。

HUUM拍摄的照片,来源于Unsplash

回到家后,我会让孩子们上学,然后在早上 6:30 左右开启电脑。在疫情期间,我成为了永久远程工作者,这让我在安排一天的工作方式上有了很多灵活性。

深度工作

我最喜欢的两位作者,蒂姆·费里斯卡尔·纽波特,对我的工作方式产生了最大影响。熟悉这两个人的人会看到我方法中的一些重叠。我的一天设计旨在管理和利用我全天的自然能量流。

我发现我的大脑在早晨表现最佳,但到下午时,我的思维能力会下降。因此,我一直在努力利用高峰时段,并管理下降的部分。

我将早晨的清晰思维用于我面临的最艰难工作。虽然具体活动每天都不同,但一些常见任务包括

  • 阅读学术论文

  • 单独思考问题的创意

  • 为高管或员工制定巧妙的沟通策略

  • 准备会议

  • 配对编程

  • 编码

所有这些活动都需要我全力以赴地发挥我的智力能力。例如,制定巧妙的沟通策略。我从事编码已经超过十年,并且认为自己非常具备技术思维;然而,在我目前的角色中,像沟通这样的软技能非常重要。我发现我需要专门的时间来思考在管理上级、下属以及同行时的微妙和复杂情况。

Nick Morrison拍摄的照片,来源于Unsplash

在我目前的角色中,我学会了放慢脚步,更加注意我的沟通如何被感知。我过去因立即回应电子邮件而受到过伤害,这些回应通常是我脑海中首先出现的想法。诚然,这些考虑对我来说并不自然,但在我职业生涯的这一点上,它们是必要的。

Scrum

在早上 9:00,我参加我们团队的每日 Scrum。在我们的上一篇文章中,我们提到我们不遵循传统的 Scrum 会议。与其我们昨天做了什么,我们展示它。

这对我来说极具力量,因为我在当前角色中花在编码或配对编程上的时间不多。拥有十多年的经验,我以艰难的方式学到了很多东西。虽然我可以将这些经验教训一对一地分享给团队成员,但其余的团队无法从中获益。看到某人的工作使我们所有人都能对其做出反应,并提供了一个强化最佳实践、跨部门合作和促进学习文化的机会。

阅读了 Ed Catmull 和 Amy Wallace 的书籍《创造力公司》后,我受到启发在我的团队中实施了这种版本的 Scrum。他们讨论了“每日会议”的概念,这是一种日常会议,导演们给予反馈,所有艺术员工都可以从中学习。这使得导演能够确保其风格在所有团队成员中一致实施。我最喜欢这些会议的一个部分是我经常学到新的东西。

一对一

我管理着一个相当大的数据团队,包括架构师、数据工程师、软件开发人员、数据分析师和数据科学家。Matthew Skelton 和 Manuel Pais 的书籍《团队结构》描述了四种类型的团队:

  1. 流对齐——一个“与(通常是)业务领域的一个部分工作流对齐的团队”。

  2. 复杂子系统——一个“需要重要的数学/计算/技术专长的团队”。

  3. 促进者——一个“帮助流对齐团队克服障碍的团队,也能发现缺失的能力”。

  4. *台——“其他团队类型的组合,提供有吸引力的内部产品,以加快流对齐团队的交付速度”。

我的团队是这四个团队的结合体。由于我们掌握了从头到尾的解决方案,我们的设计旨在合作,交付组织中每条业务线的各种数据产品。这意味着我们的团队通常会有更多的专门化,但也有通才。流对齐团队通常由通才组成,而其他类型的团队则从通才到纯专门化不等。

在大团队中保持思路有序可能是一个挑战。对于任何卡尔·纽波特的粉丝,我喜欢保持一个看板,每列用于我的每一次一对一交流。

一个看板示例。图片由作者提供。

这让我能够追踪我们之前讨论过的内容,并为下一次会议生成议程。此外,这帮助我保护团队成员的专注状态(有关这一概念的更多信息,请查看Flow by Mihaly Csikszentmihalyi)。当我有问题时,我不会发送分散注意力的消息,而是简单地将其添加到我的看板上的他们的栏目中。

在理想情况下,我每两周会与每个团队成员见面一次。然而,有些团队成员需要更多的关注和发展,所以我会更频繁地与他们见面。由于我是全职远程工作者,我通常会在这些会议期间去散步。我使用骨传导耳机,这使我在与团队聊天时能听到周围的环境。我发现,当我回到桌前时,新鲜的空气会让我充满活力。

照片由Arek Adeoye拍摄,来源于Unsplash

在我的一对一会议中,我们讨论各种话题,包括团队的未来发展、他们在大局中的角色、他们将要从事的任务以及他们希望发展的技能。

不幸的是,并不是所有的一对一会议都是愉快的。在大流行期间,我们进行了深入的讨论,涉及到心理健康管理,多名团队成员经历了直系亲属的丧失。

我还不得不应对非常艰难的劳动市场。我失去了多名团队成员,因为市场上的薪资上涨是我的组织无法匹配的。通过这次经历,我学到了在类似情况下更好地准备的重要经验。过去几年是我人生中最具挑战性和充实的时光。对他人产生积极影响对我来说意义重大,我希望我能很好地服务我的团队。

检查邮件

让我先说,我不喜欢电子邮件。我发现许多人希望电子邮件像即时消息那样用于快速的来回沟通,但我的目标是通常在电子邮件到达我的收件箱后的 24 小时内处理每封邮件。很少有东西是真正的紧急,但如果确实紧急的话,我可以通过即时消息或电话联系到我。

照片由Brett Jordan拍摄,来源于Unsplash

我最喜欢的黑客之一是禁用到达收件箱时播放或显示的声音和视觉通知。这帮助我优先安排时间,并进入一种专注状态。我相信你应该控制自己的时间,而不是不断反应于不断涌入的信息流。

现在,让我揭示一下我的内心宅男。工作几个月后,我分析了我的收件箱,分析了模式,并配置了各种规则来处理大多数邮件。通过这次分析,我发现通过每天战略性地检查两次,我可以有效处理几乎所有的邮件。为了避免陷入耗时的邮件漩涡,我避免在一天剩余的时间检查我的收件箱。

在设立这些集中式邮件会话之前,我发现我的邮件习惯遵循了帕金森定律,它声明“工作会扩展到填满完成它所需的时间。” 切换到集中式邮件会话让我可以批量处理快速回复简单邮件,类似于把很多小任务堆在一起。如果我遇到需要我完全注意的邮件,我会把它留到深度工作时间处理。

如果你有兴趣了解更多这些概念,我强烈推荐 Cal Newport 的书籍,《没有邮件的世界》

办公时间

作为我组织中经验最丰富的数据科学家之一,我觉得有责任帮助我团队外的其他人。加入我第一份工作的数据科学家团队对我的学习和发展至关重要,但我也经历了在一个部门甚至整个组织中成为唯一数据科学家的痛苦。与我团队外的数据科学家安排单独会议效果不佳,因此,我设置了办公时间。

图片由Sigmund拍摄,来源于Unsplash

类似于大学里的教授和助教举办办公时间,我每周会安排两个小时的时间,任何人都可以参加会议并向我提问。大多数情况下,任何话题都是可以讨论的。不论是遇到的问题、对部门在分析旅程中下一步的建议请求,还是一个完全随机的话题,我们都会讨论。

我从第一个加入会议的人开始,按顺序处理。话题完全由参与者驱动,看到许多人只是为了从别人的问题中学习而加入,确实很有趣。甚至有很多次我不知道答案,但电话中的其他人知道。我可以诚实地说,我从这些会议中学到了很多。

管理我的能量

从中午到下午 1:30 左右,我处于能量管理模式。吃饭后,我会在下午变得非常疲倦,因此我通常会做一些轻度锻炼以让血液再次流动。有时,如果我真的感觉很疲惫,我甚至会给自己时间小睡一下。

摄影师:Brianna Tucker拍摄,来自Unsplash

我并不会每天花一个半小时进行这三项活动,但我通常会每天花 30 到 90 分钟做这些活动的某种组合。

这使得我在完成一天中的第二个大型工作环节后,能保持较高的能量水*。虽然我通常下午的精神状态不如早晨那般充沛,但我对能量管理的专注使我的下午尽可能高效。

绝对自由时间

我称这段时间为“绝对自由时间”,是因为我不像保护早晨时间那样保护这段时间。我通常尝试将这段时间用于另一个深度工作环节,但我对一天中能完成的深度脑力工作量有一定限制。事实上,大多数人每天无法维持超过四小时的高度集中。

虽然良好的锻炼或小憩通常可以为我提供进行第二轮深度工作的能量,但这个时间通常被需要与我进行快速会议的人占用。如果我已经度过了一个富有成效的早晨,我不介意这种打扰。很多时候,这些干扰带来了刺激性的分析或解决问题的机会,我非常享受。

客户产品会议

我与每位客户的会议频率类似于与我的团队进行的一对一会议。在这些会议中,我们通常讨论产品的状态以及我们需要在路线图中解决的下一个功能、增强或技术债务。我还会让我的团队成员展示我们正在开发的新功能。

我发现这些会议也是一个很好的机会,可以向我的客户普及数据职业的相关知识。由于我的大多数客户都是其他领域的专家,他们并不知道机器学习是如何运作的。教育是我工作的一个关键部分。

摄影师:Dylan Ferreira拍摄,来自Unsplash

除了这些客户会议之外,我还会每个月与公司领导团队就特定业务领域召开一次小时长的会议。这些会议使我们能够涵盖主要成就,并讨论作为部门之间的合作机会。

最*的一个例子是我与我们组织的用户体验(UX)主管举行的一次会议。我们就共享我们的路线图和打包我们的服务进行了很好的讨论。我们还讨论了如何利用数据科学帮助构建客户角色以及分析调查中的非结构化文本。最后,我们 brainstorm 了如何通过午餐学习会分享我们各自领域的知识。

规划未来的一周

我每周末的一个长期习惯是规划即将到来的周。我喜欢查看下周的所有预定会议,解决任何重叠的会议,并拒绝那些我知道不应该或不能参加的会议。对于剩下的时间,我使用块状时间安排来留出时间完成我的看板中的最高优先级任务。

这与我母亲教我在前一晚规划学校的方式相一致。当我周一开始工作时,我觉得自己已经领先了一步,因为我已经知道需要做什么。我不会浪费时间去弄清楚下一步该做什么。我已做好准备,准备全速前进!

结论

尽管“数据科学家”是我的职位名称,但我大部分时间并不是在做数据科学。在我担任首席数据科学家约六个月后,我的老板开玩笑说:“我打赌你没有意识到这个职位需要多少销售工作!” 对机器学习不熟悉的公司通常对其价值持怀疑态度,而优秀的赞助商可以帮助强化你的故事。

总的来说,这份工作非常有价值,因为我能对我组织的发展以及许多个人的职业发展产生重大影响。额外的好处是跟上数据科学的最新进展,并与领导层讨论我对组织未来的愿景。在工作的乐趣、与我合作的人以及工作的灵活性之间,我感到非常荣幸。

如果你喜欢我对典型工作日的概述,并且正在从数据科学学生转变为数据科学专业人士,查看我的研讨会,我会教你在学校里学不到的技能。

参考文献

  1. medium.com/towards-data-science/a-day-in-the-life-of-a-data-scientist-938d917370b9

  2. sloww.co/lifestyle-design-101/

  3. E. Catmull 等,《创意公司》(2014),www.penguinrandomhouse.com/books/216369/creativity-inc-by-ed-catmull-with-amy-wallace/9780812993011/

  4. M. Skelton 等,《团队拓扑学》(2019),teamtopologies.com/book

  5. teamtopologies.com/key-concepts

  6. M. Csikszentmihalyi, 《心流》(2008), www.amazon.com/Flow-Psychology-Experience-Perennial-Classics/dp/0061339202

  7. en.wikipedia.org/wiki/Parkinson%27s_law

  8. C. Newport, 《没有邮件的世界》(2021), www.calnewport.com/books/a-world-without-email/

  9. www.researchgate.net/publication/232741130_Training_history_deliberate_practice_and_elite_sports_performance_An_analysis_in_response_to_Tucker_and_Collins_review-what_makes_champions

  10. www.datasciencerebalanced.com/

高级数据科学家的日常

原文:towardsdatascience.com/a-day-in-the-life-of-a-senior-data-scientist-e8b7a4866667

观点

包括一个常见的逐步项目大纲

Matt PrzybylaTowards Data Science Matt Przybyla

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

--

图片来源:SlidebeanUnsplash [1]。

目录

  1. 介绍

  2. 响应、规划和会议更新

  3. 预期的数据科学工作

  4. 总结与个人见解

  5. 参考文献

介绍

目标受众:

本文旨在为当前数据科学家提供参考,帮助他们成为高级数据科学家。它也可以作为一般数据科学工作的示例,供那些希望转行的人参考。

让我们首先明确一点,高级职位在不同公司之间有所不同,所以请将这一天的生活视为参考。对我来说,高级职位与非高级职位的主要区别在于,高级职位通常涉及跨部门和利益相关者的更多协作。也可能是你需要独立负责一个完整的项目,类似于产品经理,不仅要提出解决方案,还要提出原因及其影响,例如关键绩效指标(Key Performance Indicator)。话虽如此,让我们来看看高级数据科学家的典型一天。

响应、规划和会议更新

图片来源:Alvaro ReyesUnsplash [2]。

通常在早上

我会尽量按照时间顺序排列这些主要事件,但请注意,具体顺序可能会因当前任务的需求而有所变化。

响应

你可能会惊讶地发现,你的早晨或整天的工作实际上可能由回应 Slack 线程组成。这些讨论的内容可以从简单的数据科学术语澄清,到多个人之间来回的详细讨论,这实际上是一个针对特定项目下一步的头脑风暴。

Slack 或公司可能使用的任何工具可能有利有弊,因为它可能会在不同时间分散注意力,但它也可能比参加整个会议(无论是面对面还是像 Zoom 这样的在线视频会议)更快地回答问题。

规划

你早上可能没有紧急的通知需要回应,但无论如何,你都会想根据任务的优先级规划你的一整天。有时候,你的任务实际上会按照优先级进行排序,这样你就能清楚地知道需要按照什么顺序执行任务,无论是数据请求,还是像获取数据科学模型特征这样更长时间的项目。这些任务实际上可以是别人向你提出的请求,也可以是你为项目组织自己需求的任务。

会议更新 — Standup(一些公司称之为 Standup

接下来,你通常会有某种类型的早晨会议,你会更新利益相关者()、经理和项目中的其他相关人员。这些会议很重要,以便大家对到目前为止完成的工作和仍需工作的内容达成共识,它们通常以小型、简短的会议形式出现,以便尽可能高效,不占用其他人的时间。

现在我们对典型的早晨有了一个大致的概念,让我们给出这些事件的一些具体例子:

响应:

  • 回答澄清性的紧急 Slack 问题,这些问题通常不需要成为正式的任务单 — 例如:“你介意给我发一下这个分析的仪表盘链接吗?”、“你能解释一下 MAE 是什么意思吗?”和“这个 AB 测试的主要 KPIs 是什么?”等等。

规划:

  • 根据你的回应,你可能会创建一个正式的任务单(例如,流行的项目管理工具 Jira,它使用看板) — 例如:项目中的 KPI 可能因出现的缺陷而存在分歧,因此可能会创建一个任务单以分析另一个更快且能达到类似、有用决策标准的 KPI。

会议更新:

  • 向利益相关者更新发生了什么,出了什么问题(如果有的话),有什么障碍(如果有的话),需要什么,以及下一步是什么 — 还有谁需要被拉进来回答任何悬而未决的问题 — 例如:我们在这个项目上受阻,因为有一个待处理的任务单,需要将一个新列添加到数据库表中,这对于数据科学模型特征集是必要的。

预期的数据科学工作

照片由 Jefferson Santos 提供,来自 Unsplash [3]。

更新工单

假设你使用某种工单系统来组织任务,比如前面提到的 Jira 工具,你需要更新你在待办事项、进行中和已完成的内容。这些方面在你公司中可能有所不同。例如,你可能还有一个规划部分。最重要的是你的团队对每个类别的含义达成一致,无论类别的名称和数量如何。

在这一部分,我可以更详细地讲解数据科学特定任务,而高级别的部分则涉及端到端项目组织。

项目概述:

根据不同的日子,你可能会执行以下一个或多个任务,每个任务的示例如下:

  • 寻找机会 — 例如:你可能会发现公司某个类别的产品销售低迷。

  • 简洁地定义问题 — 例如:“裤子类别的销售额是所有类别中最低的”

  • 评估可能的影响 — 例如:“裤子占据了我们 80%的库存,但在销售方面却是表现最差的类别”

  • 开发解决方案数据科学或混合型) — 例如:裤子类别被错误分类,因为它没有正确区分短裤和长裤,所以库存被错误地分类到“其他”类别,主页上也没有扩展该类别。这种错误分类是因为公司使用了自制的规则基础解决方案。解决方案是决策树分类器分类。

  • 绘制所需的其他资源 — 例如:你需要数据库表中的某些数据,如描述列,作为模型特征,以帮助模型正确分类,因此你需要与数据工程师合作,制定获取产品数据的流程。

  • 在本地/开发环境中测试解决方案 — 例如:创建一个从数据集获取、训练和测试模型算法、部署模型端点的端到端流程,以便它可以被用于自动分类任何新的、到达的库存,以及重新分类临时错误分类的项目。你还需要通过实际数据证明解决方案确实更好,并且机器学习操作流程也能正常工作。

  • 根据生产中的 KPI 测试解决方案 — 例如,你可以使用像“裤子销售”这样的 KPI 来进行 AB 测试,以查看预期的影响是否真的发生了。你可以与公司中的 AB 测试专家合作,或者自己进行这项工作。

  • 评估实际影响 — 例如:将测试结果传达给相关利益方,以便每个人都了解解决方案的结果。

  • 继续推进(或不推进)解决方案! — 例如:根据测试结果的成功与否,你将执行适用于所有后续项目的生产就绪解决方案。如果预期的结果没有出现,你可以根据这些发现适当调整解决方案——例如,你的模型可能过拟合了,或者需要用更多数据进行训练以便模型更好地泛化。

这些主要的重点通常可以涵盖你在日常工作中所见的大部分过程,具体取决于你项目的进展情况。你可能还会同时处理一个以上的项目。在高级职位中,你可能还会更多地与公司中的其他利益相关者和高层管理人员合作。

总结与个人感想

每家公司和每个角色都有其不同之处。然而,高级数据科学角色之间仍然存在共性。总的来说,主要的收获是,高级数据科学职位不仅涉及普通数据科学职位的所有内容,而且更加关注项目的全程,并充当该项目的产品经理,从而在整个过程中承担更多责任。

作为高级数据科学家我学到了什么?

作为高级数据科学家,我学到的主要是以下几点的重要性:

  • 沟通

  • 优先级排序

  • 愿意调整方向

在高级职位中,你将有更多机会与产品经理和公司高层互动,因此清晰高效的沟通能力是必须具备的。接下来,优先排序任务和主要数据科学项目非常重要,因为你会发现有大量的工作可以做,但关键是要理解一个任务或项目相对于另一个的比较。最后,即使你在规划你的路线图时,也需要习惯于根据优先级和紧急性转向不同的项目。这三个方面在高级职位中尤为突出,因为你不仅作为领域专家存在,还通常在公司中担任数据科学领域的领导者。

让我感到惊讶的事情和激励我的因素是什么?

让我感到最惊讶的高级职位之处在于,你的工作内容不会像预期那样大量涉及数据科学。相反,还有其他同样重要的业务部分,如我上面提到的,这些部分可能会占用你一天中的相当多时间。

激励我的是看到高级职位对业务的影响,因为通过数据驱动的策略为某个数据科学项目做出论证,并从头到尾领导该项目,既重要又赋予了我权力。

更加关注战略,以下是我们讨论的内容:

Responding, Planning, and Meeting Updates
Expected Data Science Work

我希望你发现我的文章既有趣又有用。如果你作为一名高级数据科学家的经历与我的相同或不同,请随时在下面评论。为什么?或者为什么不?你认为还应该讨论哪些其他内容,包括更多的优缺点?这些问题当然可以进一步澄清,但我希望我能够对这个职位的期望提供一些见解。

我与这些公司没有任何关联。

请随时查看我的个人资料, Matt Przybyla以及其他文章,订阅以接收我的博客的电子邮件通知,请点击下面的链接,或者 点击屏幕顶部关注图标旁边的订阅图标,如有任何问题或评论,请在 LinkedIn 上联系我。*

订阅链接: datascience2.medium.com/subscribe

推荐链接: datascience2.medium.com/membership

(如果你在 Medium 上注册会员,我将获得佣金)

参考文献

[1] 照片由 Slidebean 提供,拍摄于 Unsplash,(2020)

[2] 照片由 Alvaro Reyes 提供,拍摄于 Unsplash,(2018)

[3] 照片由 Jefferson Santos 提供,拍摄于 Unsplash,(2017)

自然语言处理中的知识图谱:十年回顾

原文:towardsdatascience.com/a-decade-of-knowledge-graphs-in-natural-language-processing-5fdb15abc2b3?source=collection_archive---------8-----------------------#2023-03-14

关于结合结构化和非结构化知识在自然语言处理中的研究现状

Tim SchopfTowards Data Science Tim Schopf

·

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

--

图片由 Billy Huynh 提供,来自 Unsplash

这篇文章基于我们在 AACL-IJCNLP 2022 的论文 《自然语言处理中的知识图谱:十年回顾》。你可以在那里阅读更多细节。

知识图谱(KGs)自 2012 年 Google 推出 KG 以来在学术界和工业界都受到了极大关注Singhal, 2012)。作为实体之间语义关系的表示,知识图谱已被证明对自然语言处理(NLP)特别相关,并且*年来在流行度上迅速增长,这一趋势似乎正在加速🚀。鉴于这一领域的研究工作越来越多,NLP 研究社区中已经对几种与知识图谱相关的方法进行了综述。然而,迄今为止,仍缺乏对已建立主题的综合研究,并对个别研究流的成熟度进行回顾。为弥补这一空白,我们系统地分析了 507 篇关于 NLP 中知识图谱的文献。因此,我们呈现了研究领域的结构化概述,提供了任务分类法,总结了我们的发现,并强调了未来工作的方向。

什么是自然语言处理?

自然语言处理(NLP)是语言学计算机科学人工智能的一个子领域,涉及计算机与人类语言之间的互动,特别是如何编程计算机以处理和分析大量的自然语言数据(Wikipedia)。

知识图谱是什么?

知识图谱作为以机器可读格式语义化表示现实世界实体的一个方法已经出现。大多数研究隐含地采用了知识图谱的广泛定义,即“一个旨在积累和传达现实世界知识的图谱,其中节点代表感兴趣的实体,边代表这些实体之间的关系”Hogan et al., 2022)。

我们为什么在 NLP 中使用知识图谱?

其基本范式是结构化和非结构化知识的结合可以使所有类型的自然语言处理任务受益。例如,将知识图谱中的结构化知识注入语言模型中的上下文知识,可以提升下游任务的性能(Colon-Hernandez et al., 2021)。此外,鉴于目前对大型语言模型(如 ChatGPT)的公众讨论,我们可以利用知识图谱来验证并在必要时纠正生成模型中的虚假和错误陈述。随着知识图谱重要性的日益增加,也有越来越多的努力在从非结构化文本中构建新的知识图谱。

知识图谱在自然语言处理中的应用是什么?

研究领域的特点 🏞️

下图展示了十年观察期内的出版物分布情况。

2012 年至 2021 年的论文数量分布(图像由作者提供)。

尽管第一篇出版物出现于 2013 年,但 2013 年至 2016 年间,年度出版物数量增长缓慢。从 2017 年起,出版物数量几乎每年翻倍。由于这些年内研究兴趣的显著上升,90%以上的出版物都来源于这五年。尽管 2021 年的增长趋势似乎停止了,这很可能是因为数据导出发生在 2022 年第一周,遗漏了许多 2021 年的研究,这些研究在 2022 年晚些时候才被列入数据库。然而,这一趋势清楚地表明,知识图谱正受到自然语言处理研究社区越来越多的关注。

此外,我们观察到,研究文献中探索的领域数量迅速增长,与年度论文数量相匹配。在下图中,显示了十个最频繁的领域。

按最热门应用领域划分的论文数量(图像由作者提供)。

显著的是,健康领域远远是最突出的领域。健康领域出现的频率是学术领域的两倍多,后者排在第二位。其他热门领域包括工程、商业、社交媒体或法律。考虑到领域的多样性,显而易见,知识图谱自然适用于许多不同的情境。

研究文献中的任务 📖

基于文献中关于知识图谱的任务,我们开发了下图所示的实证分类法。

涉及自然语言处理中的知识图谱的任务分类(图像由作者提供)。

两个顶级类别包括知识获取和知识应用。知识获取包含从非结构化文本中构建知识图谱的自然语言处理任务(知识图谱构建)或对已经构建的知识图谱进行推理(知识图谱推理)。知识图谱构建任务进一步分为两个子类别:知识提取,用于将实体、关系或属性填充到知识图谱中,以及知识整合,用于更新知识图谱。知识应用作为第二个顶级概念,涵盖了通过知识图谱的结构化知识来增强的常见自然语言处理任务。

知识图谱构建 🏗️

实体提取的任务是构建知识图谱(KGs)的起点,用于从非结构化文本中提取现实世界的实体。一旦相关实体被识别出来,就会通过关系提取任务找出它们之间的关系和互动。许多论文使用实体提取和关系提取来构建新的知识图谱,例如,用于新闻事件或学术研究。实体链接是将文本中识别出的实体与知识图谱中已存在的实体进行链接的任务。由于同义或类似的实体常常存在于不同的知识图谱或不同的语言中,实体对齐可以被执行,以减少未来任务中的冗余和重复。制定知识图谱的规则和方案,即其结构和知识展示的格式,是通过本体构建任务完成的。

知识图谱推理 🧠

一旦构建完成,知识图谱包含结构化的世界知识,并且可以通过推理得出新的知识。因此,分类实体的任务被称为实体分类,而链接预测是推断现有知识图谱中实体之间缺失链接的任务,通常通过对实体进行排序作为查询的可能答案。知识图谱嵌入技术用于创建图的密集向量表示,以便可以用于下游的机器学习任务。

知识应用 🛠️

现有的知识图谱可以用于多种流行的自然语言处理(NLP)任务。这里我们概述了最常见的任务。问答(QA)被发现是使用知识图谱的最常见 NLP 任务。这个任务通常被分为文本问答和基于知识库的问答(KBQA)。文本问答从非结构化文档中提取答案,而 KBQA 则从预定义的知识库中提取答案。KBQA 自然与知识图谱紧密相关,而文本问答也可以通过使用知识图谱作为回答问题的常识知识来源来进行。这种方法不仅有助于生成答案,还使答案更加可解释。语义搜索指的是“有意义的搜索”,其目标不仅仅是搜索字面匹配,还要理解搜索意图和查询上下文。这个标签表示了利用知识图谱进行搜索、推荐和分析的研究。例如,大型语义网络概念网(ConceptNet)和学术交流及其关系的知识图谱——微软学术图谱(Microsoft Academic Graph)。对话界面是另一个可以受益于知识图谱中世界知识的 NLP 领域。我们可以利用知识图谱中的知识来生成在特定上下文中更具信息性和适当性的对话代理响应。

自然语言生成(NLG) 是 NLP 和计算语言学的一个子领域,关注于从头生成自然语言输出的模型。知识图谱在这个子领域中用于从知识图谱生成自然语言文本、生成问答对、多模态任务中的图像描述,或在低资源环境下进行数据增强。文本分析 结合了各种分析性 NLP 技术和方法,用于处理和理解文本数据。典型任务包括情感检测、主题建模或词义消歧。增强型语言模型 是将诸如 BERT (Devlin et al., 2019) 和 GPT (Radford et al., 2018) 等大型预训练语言模型与知识图谱中包含的知识相结合。由于预训练语言模型从大量的非结构化训练数据中获取知识,一种上升的研究趋势是将它们与结构化知识结合起来。知识图谱中的知识可以注入到语言模型的输入、架构、输出或其组合中。

使用知识图谱在 NLP 中的热门任务 📈

下图展示了在 NLP 中使用知识图谱的最受欢迎的任务。

2013 年至 2021 年间最受欢迎任务的论文数量分布(图像作者提供)。

我们可以观察到,诸如关系提取或语义搜索等任务已经存在了一段时间,并且持续稳步增长。在我们的研究中,我们将这些任务作为指标之一,以得出关系提取或语义搜索等任务已经相对成熟的结论。相比之下,增强型语言模型和知识图谱嵌入任务仍然可以被认为相对不成熟。这可能是因为这些任务仍然相对年轻,且研究较少。上图显示,这两项任务从 2018 年开始研究数量急剧增加,并自那时起吸引了大量兴趣。

结论 💡

*年来,知识图谱在 NLP 研究中的重要性日益提升。自 2013 年首次发表相关文献以来,全球研究人员对从 NLP 角度研究知识图谱给予了越来越多的关注,特别是在过去五年中。为了提供这一成熟研究领域的概述,我们对知识图谱在 NLP 中的应用进行了多方面的调查。我们的发现显示,大量涉及知识图谱的 NLP 任务已在各个领域得到了研究。涉及使用实体提取和关系提取构建知识图谱的论文占所有工作的主要部分。应用的 NLP 任务,如 QA 和语义搜索,也有着强大的研究社区。*年来最突出的主题是增强型语言模型、QA 和知识图谱嵌入。

一些概述的任务仍然局限于研究领域,而另一些任务已经在许多实际应用中找到了应用。我们观察到,知识图谱的构建任务和对知识图谱的语义搜索是最广泛应用的任务。在自然语言处理任务中,问答系统和对话接口已经被应用到许多现实生活领域,通常以数字助手的形式出现。像知识图谱嵌入和增强语言模型这样的任务仍在研究中,在实际场景中尚未得到广泛应用。我们预期,随着增强语言模型和知识图谱嵌入的研究领域成熟,将会有更多的方法和工具被研究用于这些任务。

来源

[## 十年知识图谱在自然语言处理中的应用:一项调查

Phillip Schneider, Tim Schopf, Juraj Vladika, Mikhail Galkin, Elena Simperl, Florian Matthes. 第 2 届会议论文集…

aclanthology.org [## GitHub - sebischair/KG-in-NLP-survey: 本仓库包含了 507 篇被注释的论文…

本仓库包含了 507 篇被注释的论文,这些论文被纳入了研究:“十年知识图谱在自然语言处理中的应用”。

github.com

深入探讨自编码器及其与 PCA 和 SVD 的关系

原文:towardsdatascience.com/a-deep-dive-into-autoencoders-and-their-relationship-to-pca-and-svd-97e37c81898a

对自编码器和降维的深入探索

Reza BagheriTowards Data Science Reza Bagheri

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

--

作者提供的图片

自编码器是一种学习重构输入的神经网络。它由一个编码器网络和一个解码器网络组成,其中编码器将输入数据压缩到一个低维空间中,而解码器则从该空间重构输入数据。编码器和解码器被联合训练,以最小化输入数据及其重构之间的重构误差。

自编码器可以用于各种任务,例如数据压缩、去噪、特征提取、异常检测和生成建模。它们在计算机视觉、自然语言处理和语音识别等众多领域都有应用。自编码器还可以用于降维。实际上,自编码器的主要目的之一是学习输入数据的压缩表示,这可以作为一种降维形式。

在本文中,我们将讨论自编码器背后的数学原理,并了解它们如何进行降维。我们还将研究自编码器、主成分分析(PCA)和奇异值分解(SVD)之间的关系。我们还将展示如何在 Pytorch 中实现线性和非线性自编码器。

自编码器架构

图 1 显示了自编码器的架构。如前所述,自编码器学习重构其输入数据,因此输入层和输出层的大小总是相同的 (n)。由于自编码器学习自己的输入,因此不需要标记数据进行训练。因此,它是一种无监督学习算法。

那么,学习相同的输入数据有什么意义呢?如你所见,这种架构中的隐藏层呈双侧漏斗状,层间的神经元数量从第一个隐藏层开始逐渐减少,到达称为瓶颈层的层时达到最小值。在瓶颈层之后,神经元数量再次增加,直到到达输出层,与输入层的神经元数量相等。值得注意的是,瓶颈层的神经元数量少于n

图 1(图像由alexlenail.me/NN-SVG/生成)

在神经网络中,每一层都学习输入空间的抽象表示,因此瓶颈层确实是信息在输入层和输出层之间传递的瓶颈。这一层学习输入数据的最紧凑表示,并且还学习提取输入数据的最重要特征。这些新特征(也称为潜变量)是将输入数据点转换为连续低维空间的结果。实际上,潜变量可以以更简单的方式描述或解释输入数据。瓶颈层中神经元的输出表示这些潜变量的值。

瓶颈层的存在是这种架构的关键特征。如果网络中的所有层都具有相同数量的神经元,网络可以通过将所有输入数据值传递通过网络轻松地记住这些值。

自编码器可以分为两个网络:

  1. 编码网络:它从输入层开始,到达瓶颈层结束。它将高维输入数据转换为由潜变量形成的低维空间。瓶颈层中神经元的输出表示这些潜变量的值。

  2. 解码网络:它从瓶颈层开始,到达输出层结束。它接收来自瓶颈层的低维潜变量的值,并从这些值中重建原始的高维输入数据。

在本文中,我们将讨论自编码器与 PCA 之间的相似性。为了理解 PCA,我们需要一些线性代数的概念。因此,我们首先回顾一下线性代数。

线性代数复习:基,维度和秩

一组向量 {v₁, v₂, …, v_n} 形成一个 ,如果它们是线性无关的并且张成 V。如果一组向量是线性无关的,那么该组中的任何向量都不能表示为其他向量的线性组合。一组向量 {v₁, v₂, …, v_n} 张成 一个向量空间,如果该空间中的每个其他向量都可以表示为这一组向量的线性组合。因此,任何 V 中的向量 x 可以写作:

其中 a₁, a₂, …, a_n 是一些常数。向量空间 V 可以有许多不同的向量基,但每个基总是有相同数量的向量。一个向量空间的基中的向量数量被称为该向量空间的 维度。当所有向量都是标准化的(标准化向量的长度为 1)并且正交(相互垂直)时,基 {v₁, v₂, …, v_n} 是 正交标准 的。在欧几里得空间 R² 中,向量:

形成一个被称为 标准基 的正交标准基。它们是线性无关的,并且张成 R² 中的任意向量。由于基中只有两个向量,所以 R² 的维度是 2。如果我们有另一对线性无关且张成 R² 的向量,这对向量也可以作为 R² 的基。例如

也是一个基,但不是一个正交标准基,因为这些向量不正交。更一般地,我们可以将 R^n 的标准基定义为:

其中在 e 中,第 i 个元素是 1,其他所有元素都是 0。

让向量集 B={v₁, v₂, …, v_n} 形成一个向量空间的基,那么我们可以用基向量表示该空间中的任何向量 x

因此,x 相对于这个基 B 的坐标可以写作:

实际上,当我们像这样定义 R² 中的一个向量

这个向量的元素就是它相对于标准基的坐标:

我们可以轻松找到一个向量相对于另一个基的坐标。假设我们有一个向量:

其中 B={v₁, v₂, …, v_n} 是一个基。现在我们可以写作:

这里的 P_B 被称为 坐标变换矩阵,它的列是基 B 中的向量。因此,如果我们知道 x 相对于基 B 的坐标,我们可以使用方程 1 计算它相对于标准基的坐标。图 2 显示了一个示例。这里的 B={v₁, v₂} 是 R² 的一个基。向量 x 被定义为:

并且 x 相对于 B 的坐标是:

所以,我们有:

图 2(作者提供的图片)

矩阵 A列空间(也写作 Col A) 是 A 列的所有线性组合的集合。假设我们用向量 a, a, … a_n 表示矩阵 A 的列。现在对于任何像 u 的向量,Au 可以写作:

因此,AuA 的列的线性组合,而 A 的列空间是可以表示为 Au 的向量集合。

矩阵 A行空间A 行的所有线性组合的集合。假设我们用向量 aᵀ, aᵀ, … a_mᵀ 表示矩阵 A 的行:

A 的行空间是所有可以表示为

Col A 的基向量数量或 Col A 的维度称为 AA 的秩也是 A 中线性无关列的最大数量。还可以证明,矩阵 A 的秩等于其行空间的维度,并且同样等于 A 中线性无关行的最大数量。因此,矩阵的秩不能超过其行数或列数。例如,对于一个 m×n 矩阵,秩不能大于 min(m, n)。

PCA:综述

主成分分析(PCA)是一种线性技术。它找出数据中捕获最多变化的方向,然后将数据投影到这些方向所张成的低维子空间上。PCA 是一种广泛使用的数据降维方法。

PCA 将数据转换为一个新的正交坐标系统。该坐标系统的选择使得投影到第一个坐标轴(称为 第一个主成分)的数据点的方差最大化。投影到第二个坐标轴(称为 第二个主成分)的数据点的方差在所有与第一个主成分正交的方向中最大化,通常地,投影到每个坐标轴的数据点的方差在所有与前面的主成分正交的方向中最大化。

假设我们有一个具有 n 特征和 m 数据点或观测值的数据集。我们可以使用 m×n 矩阵

为了表示这个数据集,我们称之为设计矩阵。因此,X的每一行代表一个数据点,每一列代表一个特征。我们还可以将X写作

其中每列向量

表示这个数据集中的一个观察值(或数据点)。因此,我们可以将数据集视为 R^n 中的 m 个向量集合。图 3 显示了 n=2 的示例。在这里,我们可以将每个观察值绘制为 R² 中的一个向量(或简单的点)。

图 3(作者提供的图像)

u 为单位向量,则我们有:

每个数据点 x 在向量 u 上的标量投影是:

图 4 显示了 n=2 的示例。

图 4(作者提供的图像)

我们用来表示 X 每列的均值

那么数据集的均值定义为:

我们也可以将其写作:

现在这些投影数据点的方差定义为:

方程 1 可以进一步简化。术语

是一个标量(因为 a 的结果是一个标量量)。此外,我们知道标量量的转置等于其自身。因此,我们可以得到

因此,将数据点在 X 上投影到向量 u 的方差可以写作

其中

被称为协方差矩阵(图 5)。

图 5(作者提供的图像)

通过简化方程 5,可以显示协方差矩阵可以写作:

其中

其中 xᵢ, 是设计矩阵 X 的 (i, k) 元素(或简单地说是向量 xkth 元素)。

对于具有 n 个特征的数据集,协方差矩阵是一个 n×n 矩阵。此外,基于方程 6 中 Sᵢ, 的定义,我们有:

因此,其 (i, j) 元素等于其 (j, i) 元素,这意味着协方差矩阵是对称矩阵,并且等于其转置:

现在我们找到向量 u₁ 使其最大化

由于 u₁ 是一个标准化向量,我们将此约束添加到优化问题中:

我们可以通过添加拉格朗日乘数λ₁并最大化来解决这个优化问题

为此,我们将该项对u₁的导数设为零:

我们得到:

这意味着u₁是协方差矩阵S的特征向量,λ₁是其对应的特征值。我们称特征向量u₁为第一个主成分。接下来,我们要找到单位向量u₂,使其在所有与第一个主成分正交的方向中最大化u₂ᵀSu₂。因此,我们需要找到在这些约束条件下使uSu₂最大化的向量u₂:

可以证明u₂ 是这个方程的解:

所以我们得出结论,u₂也是S的特征向量,λ₂ 是其对应的特征值(证明见附录)。更一般地,我们希望找到单位向量u,使其在所有与先前主成分u₁…ui-1 正交的方向中最大化uᵢᵀSu。因此,我们需要找到使得

在这些约束条件下:

再次可以证明,u 是这个方程的解

因此,uS的特征向量,λᵢ 是其对应的特征值(证明见附录)。向量u 称为第i个主成分。如果我们将前面的方程乘以uᵀ,得到:

因此,我们得出结论,数据点在X中沿着特征向量u的标量投影的方差等于其对应的特征值。

如果我们有一个具有n特征的数据集,那么协方差矩阵将是一个n×n的对称矩阵。这里每个数据点可以表示为 R*n*中的一个向量(***x****ᵢ*)。如前所述,Rn中向量的元素给出了相对于标准基的坐标。

可以证明一个n×n的对称矩阵有n个实特征值,以及n个线性无关且正交的对应特征向量(谱定理)。这n个正交特征向量是这个数据集的主成分。可以证明一组n个正交向量可以形成 R^n*的一个基。因此,这些主成分形成了一个正交基,可以用来为数据点定义一个新的坐标系统(图 6)。

我们可以很容易地计算每个数据点x相对于这个新坐标系统的坐标。令B={v₁, v₂, …, v_n}为主成分的集合。我们首先将x用基向量表示:

现在如果我们将这个方程的两边都乘以vᵢᵀ,我们有:

由于我们有一个正交基:

因此,得出:

由于点积是交换律的,我们也可以写作:

因此,x相对于B的坐标为:

设计矩阵可以写作

在新的坐标系统中。这里每行表示新坐标系统中的一个数据点(观察)。图 6 展示了n=2 的一个示例。

图 6(作者提供的图片)

数据点在每个特征向量(主成分)上的标量投影的方差等于其对应的特征值。第一个主成分具有最大的特征值(方差)。第二个主成分具有第二大的特征值,以此类推。现在我们可以选择前d个主成分,并将原始数据点投影到由它们张成的子空间中。

因此,我们将原始数据点(具有n特征)转换为这些投影的数据点,这些数据点属于一个d维子空间。通过这种方式,我们将原始数据集的维度从n减少到d,同时最大化投影数据的方差。现在,方程 9 中的前d列给出了投影数据点的坐标:

图 7 给出了这种转换的一个示例。原始数据集具有 3 个特征(n=3),我们通过将数据点投影到由前两个主成分(v₁, v₂)形成的*面上,将其维度减少到d=2。子空间中每个数据点x的坐标为:

图 7(作者提供的图片)

在进行 PCA 分析之前,通常会将数据集以零为中心。为此,我们首先创建表示我们数据集的设计矩阵X(方程 2)。然后通过从每列的元素中减去每列的均值来创建一个新矩阵Y

矩阵Y代表中心化的数据集。在这个新矩阵中,每列的均值为零:

因此,数据集的均值也为零:

现在假设我们从一个中心化的设计矩阵X开始,并希望计算其协方差矩阵。因此,X的每列的均值为零。从方程 6 我们有:

其中[X]_k,j 表示矩阵X的(k, j)元素。通过使用矩阵乘法的定义,我们得到

请注意,这个方程仅在设计矩阵(X)是中心化时有效。

PCA 与奇异值分解(SVD)的关系

假设A是一个m×n的矩阵。那么AA将是一个n×n的方阵,并且可以很容易地证明它是对称矩阵。由于AA是对称的,它有n个实特征值和n个线性独立且正交的特征向量(谱定理)。我们称这些特征向量为v₁, v₂, …, v_n,并假设它们是标准化的。可以证明AA的特征值都是正的。现在假设我们按降序标记它们,即:

v₁, v₂, …, v_nAA的特征向量,分别对应这些特征值。我们定义矩阵A奇异值(记作σᵢ)为λᵢ的*方根。因此,我们有

现在假设A的秩为r。那么可以证明,AA的非零特征值的数量或A的非零奇异值的数量是r

现在A的奇异值分解(SVD)可以写成

这里V是一个n×n的矩阵,其列为v

Σ是一个m×n的对角矩阵,且所有元素均为零,除了前r个对角元素,这些元素等于A的奇异值。我们定义矩阵U

我们定义u u_r

我们可以很容易地证明这些向量是正交的:

这里我们使用了v_jAA的特征向量,并且这些特征向量是正交的。由于这些向量是正交的,它们也是线性无关的。其他的u 向量(r<i≤m)被定义为u₁, u₂, u_m 构成一个m维向量空间(R^m)的基。

X为一个中心化的设计矩阵,其 SVD 分解如下:

如前所述,v₁, v₂, …, v_nXX 的特征向量,而奇异值是其对应特征值的*方根。因此,我们有:

现在我们可以将前一个方程的两边除以 m(其中 m 是数据点的数量),并使用方程 10,得到:

因此,v 是协方差矩阵的特征向量,其对应的特征值是其对应奇异值的*方除以 m。所以,SVD 方程中的矩阵 V 给出了 X 的主要成分,并且使用 Σ 中的奇异值,我们可以轻松计算特征值。总之,我们可以使用 SVD 来进行 PCA。

让我们看看从 SVD 方程中还能得到什么。我们可以使用方程 3 和方程 11 来简化方程 12 中的

与方程 9 相比,我们可以得出结论, 的第 i 行给出了数据点 x 相对于主要成分定义的基的坐标。

现在假设在方程 12 中,我们只保留 U 的前 k 列、V 的前 k 行以及 Σ 的前 k 行和列。如果我们将它们相乘,我们得到:

请注意,X 仍然是一个 m×n 矩阵。如果我们将 X 乘以具有 n 个元素的向量 b,我们得到:

其中 [Cb] 是向量 Cbi 项元素。由于 u₁, u₂, …, u 是线性无关的向量(记住它们构成一个基,因此应该是线性无关的),并且它们生成了 Xb,我们可以得出结论,它们构成了 Xb 的基。这一基有 k 个向量,所以 X 的列空间维度是 k。因此,X 是一个秩为 k 的矩阵。

X 代表什么呢?使用方程 13,我们可以写成:

所以,X 的第 i 行是以下的转置:

这是数据点 x 在由主要成分 v₁, v₂, … v 张成的子空间上的向量投影。记住,v₁, v₂, … v_n 是我们原始数据集的基。此外,x 相对于这一基的坐标为:

因此,使用方程 1,我们可以将 x 写成:

现在我们可以将 x 分解为两个向量。一个在由 v₁, v₂, … v 定义的子空间中,另一个在由剩余向量定义的子空间中。

第一个向量是 x 在由 v₁、v₂、… v 定义的子空间上的投影结果,等于

记住,设计矩阵 X 的每一行代表一个原始数据点。类似地,X 的每一行代表同一数据点在由主成分 v₁、v₂、… v 张成的子空间上的投影(图 8)。

图 8(作者提供的图片)

现在我们可以计算原始数据点 (x) 和投影数据点 () 之间的距离。向量 xx̃ᵢ 之间的距离*方为:

如果我们对所有数据点的距离*方进行累加,我们得到:

一个 m×n 矩阵 C 的弗罗贝尼乌斯范数定义为:

由于向量 x 是矩阵 XX 行的转置,我们可以写出:

因此,X-X 的弗罗贝尼乌斯范数与原始数据点和投影数据点之间距离的*方和成正比(图 9),而当投影数据点越来越接*原始数据点时,|| X-X ||_F 会减少。

图 9(作者提供的图片)

我们希望投影点能很好地*似原始数据点,因此我们希望 X 在所有秩为 k 的矩阵中,X-X 的值最小。

假设我们有一个秩为 rm×n 矩阵 X,并且 X 的奇异值已排序,则我们有:

可以证明,X 在所有秩为 km×n 矩阵 A 中,最小化了 X-A 的弗罗贝尼乌斯范数。从数学角度来看:

X 是所有秩为 k 的矩阵中与 X 最*的矩阵,可以被视为设计矩阵 X 的最佳秩为 k *似。这也意味着,投影数据点由 X 表示,是原始数据点由 X 表示的秩为 k 的最佳*似(在总误差方面)。

现在我们可以尝试以不同的格式书写之前的方程。假设 Z 是一个 m×k 矩阵,W 是一个 k×n 矩阵。我们可以证明:

因此,找到一个秩为 k 的矩阵 A,使 ||X-A||F 最小化,相当于找到矩阵 ZW,使 ||X-ZW||F 最小化(证明见附录)。因此,我们可以写成:

其中 Zanm×k 矩阵**)和 Wk×n* 矩阵)是最小化问题的解,我们有

现在,根据方程 13 和 14,我们可以写出:

因此,如果我们使用 SVD 求解方程 18 中的最小化问题,我们得到以下 Z* 和 W* 的值:

W* 的行给出了主成分的转置,Z* 的行给出了相对于这些主成分形成的基的每个投影数据点的坐标的转置。重要的是,主成分形成了一个正交归一基(因此主成分既是归一化的,又是正交的)。实际上,我们可以假设 PCA 仅仅是寻找一个矩阵 W,其中行形成一个正交归一集合。我们知道当两个向量正交时,它们的内积为零,因此我们可以说 PCA(或 SVD)解决了最小化问题

具有以下约束:

其中 ZWm×kk×n 矩阵。此外,如果 Z* 和 W* 是最小化问题的解,那么我们有

这个公式非常重要,因为它使我们能够建立 PCA 和自编码器之间的联系。

PCA 与自编码器之间的关系

我们从一个只有三层的自编码器开始,如图 10 所示。这个网络有 n 个输入特征,表示为 x₁…x_n,以及 n 个输出层神经元。网络的输出表示为 x*₁…*x_n。隐藏层有 k 个神经元(其中 k<n),隐藏层的输出表示为 z₁…zₖ。矩阵 W^[1] 和 W^[2] 分别包含隐藏层和输出层的权重。

图 10(作者提供的图片)

在这里

表示第 i 个神经元在第 l 层的输入 j 的权重(来自第 j 个神经元在第 l-1 层)(图 11)。在这里我们假设隐藏层的 l=1,输出层的 l=2。

图 11(作者提供的图片)

因此,隐藏层的权重由下式给出:

该矩阵的第 i 行给出了隐藏层中第 i 个神经元的所有权重。同样,输出层的权重由下式给出:

每个神经元都有一个激活函数。我们可以使用权重矩阵 W^[1] 和输入特征计算隐藏层中神经元的输出(该神经元的激活):

其中bᵢ[1]是第*i*个神经元的偏置,*g*[1]是第 1 层神经元的激活函数。我们可以将这个方程写成向量化形式:

其中b是偏置向量:

x是输入特征的向量:

同样地,我们可以将输出层的第i个神经元的输出写成:

在向量化形式中,它变为:

现在假设我们使用以下设计矩阵作为训练数据集来训练这个网络:

因此,训练数据集有m个观察值(样本)和n个特征。记住,第i个观察值由向量表示

如果我们将这个向量输入到网络中,网络的输出由这个向量表示:

我们还需要做以下假设,以确保自编码器模拟 PCA:

1-训练数据集是中心化的,因此X的每列均值为零:

2-隐藏层和输出层的激活函数是线性的,所有神经元的偏置为零。这意味着我们在这个网络中使用的是线性编码器和解码器。因此,我们有:

3-我们使用二次损失函数来训练这个网络。因此,成本函数是均方误差(MSE),定义为:

现在我们可以证明:

其中Z定义为:

证明在附录中给出。这里Z的第i行表示当第i个观察值输入网络时隐藏层的输出。因此,最小化这个网络的成本函数等同于最小化:

其中我们定义矩阵W

请注意,W[1]的每一行是W的一列。

我们知道,如果用一个正的乘数乘以一个函数,其最小值不会改变。因此,我们可以在最小化成本函数时去掉乘数 1/(2m)。因此,通过训练这个网络,我们解决了这个最小化问题:

其中ZW分别是m×kk×n矩阵。如果我们将这个方程与方程 20 进行比较,我们会发现它是 PCA 的相同最小化问题。因此,解决方案应该与方程 20 的结果相同:

然而,这里有一个重要的区别。方程 21 的约束没有在这里应用。那么问题来了。自动编码器找到的 ZW 的最优值是否与 PCA 的相同?W 的行是否总是应该形成正交集?

首先,让我们扩展之前的方程。

我们知道,Xₖ* 的 i 行是以下内容的转置:

这是数据点 xᵢ* 在由主成分 v₁、v₂、… vₖ* 张成的子空间上的向量投影。因此,ᵢ* 属于 k 维子空间。向量 w₁、w₂、… wₖ* 应该是线性无关的。否则,W 的秩将小于 k(记住 W 的秩等于 W 的最大线性无关行数),根据方程 A.3,Xₖ* 的秩也将小于 k。可以证明,一组 k 个线性无关的向量形成 k 维子空间的基底。因此,我们得出结论,向量 w₁、w₂、… wₖ* 也形成与主成分所张成的相同子空间的基底。我们现在可以使用方程 24 将 Xₖ* 的 i 行表示为向量 w₁、w₂、… wₖ* 的线性组合。

这意味着 Zi 行仅给出相对于由向量 w₁、w₂、… wₖ* 形成的基底的 ᵢ* 的坐标。图 12 显示了 k=2 的示例。

图 12(作者提供的图像)

总结来说,自动编码器找到的矩阵 ZW 可以生成与主成分所张成的相同子空间。由于以下原因,我们也得到了 PCA 的相同投影数据点:

然而,这些矩阵定义了该子空间的新基底。与 PCA 找到的主成分不同,这个新基底的向量不一定是正交的。W 的行给出了新基底向量的转置,而 Z 的行给出了相对于该基底的每个数据点坐标的转置。

因此,我们得出结论,线性自编码器不能找到主成分,但它可以使用不同的基找到由主成分张成的子空间。这里有一个例外……假设我们只想保留第一个主成分v₁。因此,我们希望将原始数据集的维度从n减少到 1。在这种情况下,子空间只是由第一个主成分张成的直线。线性自编码器也会找到同一条直线,但使用不同的基向量w₁。这个基向量不一定是标准化的,可能具有与v₁相反的方向,但它仍然在同一条直线上(子空间)。这在图 13 中得到了证明。现在,如果我们将w₁标准化,我们将得到数据集的第一个主成分。因此,在这种情况下,线性自编码器能够间接地获得第一个主成分。

图 13

到目前为止,我们讨论了自编码器和 PCA 的基础理论。现在让我们看一个 Python 示例。在下一部分,我们将使用 Pytorch 创建一个自编码器,并与 PCA 进行比较。

案例研究:PCA 与自编码器

我们首先需要创建一个数据集。清单 1 创建了一个具有 3 个特征的简单数据集。前两个特征(x₁和x₂)具有二维多元正态分布,第 3 个特征(x₃)等于x₂的一半。这个数据集存储在数组X中,充当设计矩阵。我们还对设计矩阵进行了中心化处理。

# Listing 1
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
from scipy.stats import multivariate_normal
import torch
import torch.nn as nn
from numpy import linalg as LA
from sklearn.preprocessing import MinMaxScaler
import random
%matplotlib inline

np.random.seed(1)
mu = [0, 0]
Sigma = [[1, 1],
         [1, 2.5]]

# X is the design matrix and each row of X is an example
X = np.random.multivariate_normal(mu, Sigma, 10000)
X = np.concatenate([X, X[:, 0].reshape(len(X), 1)], axis=1)
X[:, 2] = X[:, 1] / 2
X = (X - X.mean(axis=0))
x, y, z = X.T

清单 2 创建了这个数据集的 3d 图,结果如图 14 所示。

# Listing 2
fig = plt.figure(figsize=(10, 10))
ax1 = fig.add_subplot(111, projection='3d')

ax1.scatter(x, y, z, color = 'blue')

ax1.view_init(20, 185)
ax1.set_xlabel("$x_1$", fontsize=20)
ax1.set_ylabel("$x_2$", fontsize=20)
ax1.set_zlabel("$x_3$", fontsize=20)
ax1.set_xlim([-5, 5])
ax1.set_ylim([-7, 7])
ax1.set_zlim([-4, 4])
plt.show()

图 14

如你所见,这个数据集定义在由x₃= x₁/2 表示的*面上。现在我们开始 PCA 分析。

pca = PCA(n_components=3)
pca.fit(X)

我们可以使用components_字段轻松获取主成分(X的特征向量)。它返回一个数组,每行表示一个主成分。

# Each row gives one of the principal components (eigenvectors)
pca.components_
array([[-0.38830581, -0.824242  , -0.412121  ],
       [-0.92153057,  0.34731128,  0.17365564],
       [ 0\.        , -0.4472136 ,  0.89442719]])

我们还可以通过explained_variance_字段查看对应的特征值。记住,数据点在特征向量uᵢ上的标量投影的方差等于其对应的特征值。

pca.explained_variance_
array([3.64826952e+00, 5.13762062e-01, 3.20547162e-32])

请注意,特征值是按降序排列的。因此,pca.components_的第一行给出第一个主成分。清单 3 绘制了主成分与数据点的图(见图 15)。

# Listing 3
v1 = pca.components_[0]
v2 = pca.components_[1]
v3 = pca.components_[2]

fig = plt.figure(figsize=(10, 10))
ax1 = fig.add_subplot(111, projection='3d')

ax1.scatter(x, y, z, color = 'blue', alpha= 0.1)
ax1.plot([0, v1[0]], [0, v1[1]], [0, v1[2]],
         color="black", zorder=6)
ax1.plot([0, v2[0]], [0, v2[1]], [0, v2[2]],
         color="black", zorder=6)
ax1.plot([0, v3[0]], [0, v3[1]], [0, v3[2]],
         color="black", zorder=6)

ax1.scatter(x, y, z, color = 'blue', alpha= 0.1)
ax1.plot([0, 7*v1[0]], [0, 7*v1[1]], [0, 7*v1[2]],
         color="gray", zorder=5)
ax1.plot([0, 5*v2[0]], [0, 5*v2[1]], [0, 5*v2[2]],
         color="gray", zorder=5)
ax1.plot([0, 3*v3[0]], [0, 3*v3[1]], [0, 3*v3[2]],
         color="gray", zorder=5)

ax1.text(v1[0], v1[1]-0.2, v1[2], "$\mathregular{v}_1$",
         fontsize=20, color='red', weight="bold",
         style="italic", zorder=9)
ax1.text(v2[0], v2[1]+1.3, v2[2], "$\mathregular{v}_2$",
         fontsize=20, color='red', weight="bold",
         style="italic", zorder=9)
ax1.text(v3[0], v3[1], v3[2], "$\mathregular{v}_3$", fontsize=20,
         color='red', weight="bold", style="italic", zorder=9)

ax1.view_init(20, 185)
ax1.set_xlabel("$x_1$", fontsize=20, zorder=2)
ax1.set_ylabel("$x_2$", fontsize=20)
ax1.set_zlabel("$x_3$", fontsize=20)
ax1.set_xlim([-5, 5])
ax1.set_ylim([-7, 7])
ax1.set_zlim([-4, 4])

plt.show()

图 15

请注意,第 3 个特征值几乎为零。这是因为数据集位于二维*面上(x₃= x₁/2),如图 15 所示,它在v₃方向上没有方差。我们可以使用transform()方法获取每个数据点相对于主成分定义的新坐标系统的坐标。transform()返回的数组的每一行给出一个数据点的坐标。

# Listing 4

# Z* = UΣ
pca.transform(X)
([[ 3.09698570e+00, -3.75386182e-01, -2.06378618e-17],
       [-9.49162774e-01, -7.96300950e-01, -5.13280752e-18],
       [ 1.79290419e+00, -1.62352748e+00,  2.41135694e-18],
       ...,
       [ 2.14708946e+00, -6.35303400e-01,  4.34271577e-17],
       [ 1.25724271e+00,  1.76475781e+00, -1.18976523e-17],
       [ 1.64921984e+00, -3.71612351e-02, -5.03148111e-17]])

现在我们可以选择前 2 个主成分,并将原始数据点投影到它们所张成的子空间上。因此,我们将原始数据点(具有 3 个特征)转换为这些投影数据点,这些数据点属于二维子空间。为了做到这一点,我们只需删除由 pca.transform(X) 返回的数组的第 3 列。这意味着我们将原始数据集的维度从 3 降到 2,同时最大化投影数据的方差。列表 5 绘制了这个二维数据集,结果如图 16 所示。

# Listing 5

fig = plt.figure(figsize=(8, 6))
plt.scatter(pca.transform(X)[:,0], pca.transform(X)[:,1])
plt.axis('equal')
plt.axhline(y=0, color='gray')
plt.axvline(x=0, color='gray')
plt.xlabel("$v_1$", fontsize=20)
plt.ylabel("$v_2$", fontsize=20)
plt.xlim([-8.5, 8.5])
plt.ylim([-4, 4])
plt.show()

图 16

我们也可以使用 SVD 获得相同的结果。列表 6 使用 numpy 中的 svd() 函数对 X 进行奇异值分解。

# Listing 6

U, s, VT = LA.svd(X)
print("U=", np.round(U, 4))
print("Diagonal of elements of Σ=", np.round(s, 4))
print("V^T=", np.round(VT, 4))
U= [[ 1.620e-02 -5.200e-03  1.130e-02 ... -2.800e-03 -2.100e-02 -6.200e-03]
 [-5.000e-03 -1.110e-02  9.895e-01 ...  1.500e-03 -3.000e-04  1.100e-03]
 [ 9.400e-03 -2.270e-02  5.000e-04 ... -1.570e-02  1.510e-02 -7.100e-03]
 ...
 [ 1.120e-02 -8.900e-03 -1.800e-03 ...  9.998e-01  2.000e-04 -1.000e-04]
 [ 6.600e-03  2.460e-02  1.100e-03 ...  1.000e-04  9.993e-01 -0.000e+00]
 [ 8.600e-03 -5.000e-04 -1.100e-03 ... -1.000e-04 -0.000e+00  9.999e-01]]
Diagonal of elements of Σ= [190.9949  71.6736   0\.    ]
V^T= [[-0.3883 -0.8242 -0.4121]
 [-0.9215  0.3473  0.1737]
 [ 0\.     -0.4472  0.8944]]

这个函数返回矩阵 UV 以及 Σ 的对角元素(记住 Σ 的其他元素为零)。请注意,V 的行给出了与 pca.components_ 返回的主成分相同的结果。

现在要获得 X,我们只保留 UV 的前 2 列,以及 Σ 的前 2 行和列(方程 14)。如果我们将它们相乘,我们得到:

列表 7 计算了这个矩阵:

# Listing 7

k = 2
Sigma = np.zeros((X.shape[0], X.shape[1]))
Sigma[:min(X.shape[0], X.shape[1]),
      :min(X.shape[0], X.shape[1])] = np.diag(s)

X2 = U[:, :k] @ Sigma[:k, :k]  @ VT[:k, :]
X2
array([[-0.85665, -2.68304, -1.34152],
       [ 1.10238,  0.50578,  0.25289],
       [ 0.79994, -2.04166, -1.02083],
       ...,
       [-0.24828, -1.99037, -0.99518],
       [-2.11447, -0.42335, -0.21168],
       [-0.60616, -1.37226, -0.68613]])

Z=UΣ₂ 的每一行给出了相对于前 2 个主成分形成的基的投影数据点的坐标。列表 8 计算了 Z=UΣ₂。请注意,它给出了列表 4 中的 pca.transform(X) 的前两列。因此,PCA 和 SVD 都找到了相同的子空间和相同的投影数据点。

# Listing 8

# each row of Z*=U_k Σ_k gives the coordinate of projection of the 
# same row of X onto a rank-k subspace
U[:, :k] @ Sigma[:k, :k] 
array([[ 3.0969857 , -0.37538618],
       [-0.94916277, -0.79630095],
       [ 1.79290419, -1.62352748],
       ...,
       [ 2.14708946, -0.6353034 ],
       [ 1.25724271,  1.76475781],
       [ 1.64921984, -0.03716124]])

现在我们创建一个自编码器,并用这个数据集进行训练,以便后来与 PCA 进行比较。图 17 显示了网络架构。瓶颈层有两个神经元,因为我们想将数据点投影到二维子空间上。

图 17(作者提供的图片)

列表 9 在 Pytorch 中定义了这个架构。所有层中的神经元都有线性激活函数和零偏置。

# Listing 9

seed = 9 
np.random.seed(seed)
torch.manual_seed(seed)
np.random.seed(seed)

class Autoencoder(nn.Module):
    def __init__(self):
        super(Autoencoder, self).__init__()
        ## encoder 
        self.encoder = nn.Linear(3, 2, bias=False)

        ## decoder 
        self.decoder = nn.Linear(2, 3, bias=False)

    def forward(self, x):
        encoded = self.encoder(x)
        decoded = self.decoder(encoded)
        return encoded, decoded

# initialize the NN
model1 = Autoencoder().double()
print(model1)

我们使用 MSE 成本函数和 Adam 优化器。

# Listing 10

# specify the quadratic loss function
loss_func = nn.MSELoss()

# Define the optimizer
optimizer = torch.optim.Adam(model1.parameters(), lr=0.001)

我们使用在列表 1 中定义的设计矩阵来训练这个模型。

X_train = torch.from_numpy(X) 

然后我们训练了 3000 轮:

# Listing 11

def train(model, loss_func, optimizer, n_epochs, X_train):
    model.train()
    for epoch in range(1, n_epochs + 1):
        optimizer.zero_grad()
        encoded, decoded = model(X_train)
        loss = loss_func(decoded, X_train)
        loss.backward()
        optimizer.step()

        if epoch % int(0.1*n_epochs) == 0:
            print(f'epoch {epoch} \t Loss: {loss.item():.4g}')
    return encoded, decoded

encoded, decoded = train(model1, loss_func, optimizer, 3000, X_train)
epoch 300   Loss: 0.4452
epoch 600   Loss: 0.1401
epoch 900   Loss: 0.05161
epoch 1200   Loss: 0.01191
epoch 1500   Loss: 0.003353
epoch 1800   Loss: 0.0009412
epoch 2100   Loss: 0.0002304
epoch 2400   Loss: 4.509e-05
epoch 2700   Loss: 6.658e-06
epoch 3000   Loss: 7.02e-07

Pytorch 张量 encoded 存储了隐藏层的输出(z₁, z₂),张量 decoded 存储了自编码器的输出(x^₁, x^₂, x^₃)。我们首先将它们转换为 numpy 数组。

encoded = encoded.detach().numpy()
decoded = decoded.detach().numpy()

如前所述,具有中心化数据集和 MSE 成本函数的线性自编码器解决了以下最小化问题:

其中

Z 包含了训练数据集中所有样本在瓶颈层的输出。我们还看到,这个最小化问题的解由方程 23 给出。因此,在这种情况下,我们有:

一旦训练了自编码器,我们可以检索矩阵 Z* 和 W。数组 encoded 给出矩阵 Z

# Z* values. Each row gives the coordinates of one of the 
# projected data points
Zstar = encoded
Zstar
array([[ 2.57510917, -3.13073321],
       [-0.20285442,  1.38040138],
       [ 2.39553775, -1.16300036],
       ...,
       [ 2.0265917 , -1.99727172],
       [-0.18811382, -2.15635479],
       [ 1.26660007, -1.74235118]])

列表 12 检索矩阵 W^[2]:

# Listing 12

# Each row of W^[2] gives the wights of one of the neurons in the
# output layer
W2 = model1.decoder.weight
W2 = W2.detach().numpy()
W2
array([[ 0.77703505,  0.91276084],
       [-0.72734132,  0.25882988],
       [-0.36143178,  0.13109568]])

要得到 W*,我们可以写出:

# Each row of Pstar (or column of W2) is one of the basis vectors
Wstar = W2.T
Wstar
array([[ 0.77703505, -0.72734132, -0.36143178],
       [ 0.91276084,  0.25882988,  0.13109568]])

W* 的每一行表示一个基向量 (w),由于瓶颈层有两个神经元,我们最终得到两个基向量 (w₁, w₂)。我们可以很容易地看到 w₁ 和 w₂ 不形成正交基,因为它们的内积不为零:

w1 = Wstar[0]
w2 = Wstar[1]

# p1 and p2 are not orthogonal since thier inner product is not zero
np.dot(w1, w2)
0.47360735759

现在我们可以使用公式 25 轻松计算 X₂:

# X2 = Zstar @ Pstar
Zstar @ Wstar
array([[-0.8566606 , -2.68331059, -1.34115189],
       [ 1.10235133,  0.50483352,  0.25428269],
       [ 0.7998756 , -2.04339283, -1.0182878 ],
       ...,
       [-0.24829863, -1.99097748, -0.99430834],
       [-2.11440724, -0.42130609, -0.21469848],
       [-0.60615728, -1.37222311, -0.68620423]])

请注意,这个数组和在列表 7 中使用 SVD 计算得到的数组 X2 是相同的(由于数值误差,它们之间有一个小的差异)。如前所述,Z* 的每一行给出了相对于由向量 w₁ 和 w₂ 形成的基的投影数据点 () 的坐标。

列表 13 绘制数据集、其主成分 v₁ 和 v₂ 以及新的基向量 w₁ 和 w₂ 的两个不同视图。结果如图 18 所示。请注意,数据点和基向量都位于同一*面上。请注意,训练自编码器从权重的随机初始化开始,因此如果我们在列表 9 中不使用随机种子,向量 w₁ 和 w₂ 将会不同,但它们始终位于主成分的同一*面上。

# Listing 13

fig = plt.figure(figsize=(18, 14))
plt.subplots_adjust(wspace = 0.01)
origin = [0], [0], [0] 

ax1 = fig.add_subplot(121, projection='3d')
ax2 = fig.add_subplot(122, projection='3d')
ax1.set_aspect('auto')
ax2.set_aspect('auto')
def plot_view(ax, view1, view2):
    ax.scatter(x, y, z, color = 'blue', alpha= 0.1)
    # Principal components 
    ax.plot([0, pca.components_[0,0]], [0, pca.components_[0,1]],
            [0, pca.components_[0,2]],
            color="black", zorder=5)
    ax.plot([0, pca.components_[1,0]], [0, pca.components_[1,1]],
            [0, pca.components_[1,2]],
            color="black", zorder=5)

    ax.text(pca.components_[0,0], pca.components_[0,1],
            pca.components_[0,2]-0.5, "$\mathregular{v}_1$",
            fontsize=18, color='black', weight="bold",
            style="italic")
    ax.text(pca.components_[1,0], pca.components_[1,1]+0.7,
            pca.components_[1,2], "$\mathregular{v}_2$",
            fontsize=18, color='black', weight="bold",
            style="italic")

    # New basis found by autoencoder
    ax.plot([0, w1[0]], [0, w1[1]], [0, w1[2]],
             color="darkred", zorder=5)
    ax.plot([0, w2[0]], [0, w2[1]], [0, w2[2]],
             color="darkred", zorder=5)

    ax.text(w1[0], w1[1]-0.2, w1[2]+0.1,
            "$\mathregular{w}_1$", fontsize=18, color='darkred',
            weight="bold", style="italic")
    ax.text(w2[0], w2[1], w2[2]+0.3,
            "$\mathregular{w}_2$", fontsize=18, color='darkred',
            weight="bold", style="italic")

    ax.view_init(view1, view2)
    ax.set_xlabel("$x_1$", fontsize=20, zorder=2)
    ax.set_ylabel("$x_2$", fontsize=20)
    ax.set_zlabel("$x_3$", fontsize=20)
    ax.set_xlim([-3, 5])
    ax.set_ylim([-5, 5])
    ax.set_zlim([-4, 4])
plot_view(ax1, 25, 195)
plot_view(ax2, 0, 180)
plt.show()

图 18

列表 14 绘制了 Z* 的行,结果如图 19 所示。这些行表示编码后的数据点。需要注意的是,如果我们将这个图与图 16 进行比较,它们看起来不同。我们知道自编码器和 PCA 给出的投影数据点(相同的 X₂)是相同的,但当我们在二维空间中绘制这些投影数据点时,它们看起来不同。为什么?

# Listing 14

# This is not the right way to plot the projected data points in
# a 2d space since {w1, w2} is not an orthogonal basis

fig = plt.figure(figsize=(8, 8))
plt.scatter(Zstar[:, 0], Zstar[:, 1])
i= 6452
plt.scatter(Zstar[i, 0], Zstar[i, 1], color='red', s=60)
plt.axis('equal')
plt.axhline(y=0, color='gray')
plt.axvline(x=0, color='gray')
plt.xlabel("$z_1$", fontsize=20)
plt.ylabel("$z_2$", fontsize=20)
plt.xlim([-9,9])
plt.ylim([-9,9])
plt.show()

图 19

原因是我们为每个图有不同的基。在图 16 中,我们有相对于由 v₁ 和 v₂ 形成的正交基的投影数据点的坐标。然而,在图 19 中,投影数据点的坐标是相对于 w₁ 和 w₂ 的,它们不是正交的。因此,如果我们尝试使用正交坐标系统(如图 19 的坐标系统)来绘制它们,我们会得到一个失真的图。这一点在图 20 中也得到了演示。

图 20(图像由作者提供)

要正确绘制 Z* 的行,我们首先需要找到向量 w₁ 和 w₂ 相对于由 V={v₁, v₂} 形成的正交基的坐标。

我们知道,Z 的每一行的转置给出了相对于由 W={w₁, w₂} 形成的基的投影数据点的坐标。因此,我们可以使用方程 1 来获取相对于正交基 V={v₁, v₂} 的相同数据点的坐标。

其中

是坐标变换矩阵。列表 15 使用这些方程将 Z 的行相对于正交基 V={v₁, v₂} 绘制出来。结果如图 21 所示,现在它与图 15 使用 SVD 生成的图形完全一致。

# Listing 15

w1_V = np.array([np.dot(w1, v1), np.dot(w1, v2)])
w2_V = np.array([np.dot(w2, v1), np.dot(w2, v2)])
P_W = np.array([w1_V, w2_V]).T

Zstar_V = np.zeros((Zstar.shape[0], Zstar.shape[1]))

for i in range(len(Zstar_B)):
    Zstar_V[i] = P_W @ Zstar[i]

fig = plt.figure(figsize=(8, 6))
plt.scatter(Zstar_V[:, 0], Zstar_V[:, 1])
plt.axis('equal')
plt.axhline(y=0, color='gray')
plt.axvline(x=0, color='gray')
plt.scatter(Zstar_V[i, 0], Zstar_V[i, 1], color='red', s=60)
plt.quiver(0, 0, w1_V[0], w1_V[1], color=['black'], width=0.007,
           angles='xy', scale_units='xy', scale=1)
plt.quiver(0, 0, w2_V[0], w2_V[1], color=['black'], width=0.007,
           angles='xy', scale_units='xy', scale=1)
plt.text(w1_V[0]+0.1, w2_V[1]-0.2, "$[\mathregular{w}_1]_V$",
         weight="bold", style="italic", color='black',
         fontsize=20)
plt.text(w2_V[0]-2.25, w2_V[1]+0.1, "$[\mathregular{w}_2]_V$",
         weight="bold", style="italic", color='black',
         fontsize=20)

plt.xlim([-8.5, 8.5])
plt.xlabel("$v_1$", fontsize=20)
plt.ylabel("$v_2$", fontsize=20)
plt.show()

图 21

图 22 展示了在此案例研究中创建的线性自编码器的不同组件及其值的几何解释。

图 22(作者提供的图片)

非线性自编码器

尽管自编码器不能找到数据集的主成分,但它仍然是比 PCA 更强大的降维工具。在本节中,我们将讨论非线性自编码器,并举一个 PCA 失败而非线性自编码器仍能进行降维的例子。PCA 的一个问题是它假设投影数据点的最大方差沿主成分方向。换句话说,它假设这些方差都在直线上,而在许多实际应用中,这种假设并不成立。

让我们来看一个例子。列表 16 生成了一个名为X_circ的随机圆形数据集,并在图 23 中绘制了它。数据集包含 70 个数据点。X_circ是一个 2d 数组,每一行代表一个数据点(观察值)。我们还为每个数据点分配了一个颜色。这个颜色在建模中并没有使用,我们只是为了保持数据点的顺序。

# listing 16

np.random.seed(0)
n = 90
theta = np.sort(np.random.uniform(0, 2*np.pi, n))
colors = np.linspace(1, 15, num=n)

x1 = np.sqrt(2) * np.cos(theta)
x2 = np.sqrt(2) * np.sin(theta)
X_circ = np.array([x1, x2]).T

fig = plt.figure(figsize=(8, 6))
plt.axis('equal')
plt.scatter(X_circ[:,0], X_circ[:,1], c=colors, cmap=plt.cm.jet)

plt.xlabel("$x_1$", fontsize= 18)
plt.ylabel("$x_2$", fontsize= 18)

plt.show()

图 23

接下来,我们使用 PCA 查找该数据集的主成分。列表 17 找到主成分并在图 24 中绘制它们。

# Listing 17

pca = PCA(n_components=2, random_state = 1)
pca.fit(X_circ)

fig = plt.figure(figsize=(8, 6))
plt.axis('equal')
plt.scatter(X_circ[:,0], X_circ[:,1], c=colors,
            cmap=plt.cm.jet)
plt.quiver(0, 0, pca.components_[0,0], pca.components_[0,1],
           color=['black'], width=0.01, angles='xy',
           scale_units='xy', scale=1.5)
plt.quiver(0, 0, pca.components_[1,0], pca.components_[1,1],
           color=['black'], width=0.01, angles='xy',
           scale_units='xy', scale=1.5)
plt.plot([-2*pca.components_[0,0], 2*pca.components_[0,0]],
         [-2*pca.components_[0,1], 2*pca.components_[0,1]],
         color='gray')
plt.text(0.5*pca.components_[0,0], 0.8*pca.components_[0,1], 
         "$\mathregular{v}_1$", color='black', fontsize=20)
plt.text(0.8*pca.components_[1,0], 0.8*pca.components_[1,1],
         "$\mathregular{v}_2$", color='black', fontsize=20)
plt.show()

图 24

在这个数据集中,最大方差沿着圆圈而不是直线。然而,PCA 仍然假设投影数据点的最大方差沿向量 v₁(第一个主成分)。列表 18 计算了投影数据点到 v₁ 的坐标,并在图 25 中绘制了它们。

# Listing 18

projected_points = pca.transform(X_circ)[:,0]
fig = plt.figure(figsize=(16, 2))
frame = plt.gca()
plt.scatter(projected_points, [0]*len(projected_points),
            c=colors, cmap=plt.cm.jet, alpha =0.7)
plt.axhline(y=0, color='grey')
plt.xlabel("$v_1$", fontsize=18)
#plt.xlim([-1.6, 1.7])
frame.axes.get_yaxis().set_visible(False)
plt.show()

图 25

正如你所见,投影数据点已经丧失了顺序,颜色也混杂在一起。现在我们在这个数据集上训练一个非线性自编码器。图 26 展示了它的架构。网络有两个输入特征和两个神经元的输出层。共有 5 个隐藏层,隐藏层中的神经元数量分别为 64、32、1、32 和 64。因此,瓶颈层只有一个神经元,这意味着我们希望将训练数据集的维度从 2 降至 1。

图 26(作者提供的图片)

你可能注意到的一点是,第一个隐藏层中的神经元数量在增加。因此,只有隐藏层具有双面漏斗形状。这是因为我们只有两个输入特征,所以需要在第一个隐藏层中增加更多神经元,以便为训练网络提供足够的神经元。列表 19 定义了 Pytorch 中的自编码器网络。

# Listing 19

seed = 3
np.random.seed(seed)
torch.manual_seed(seed)
np.random.seed(seed)

class Autoencoder(nn.Module):
    def __init__(self, in_shape, enc_shape):
        super(Autoencoder, self).__init__()
        # Encoder
        self.encoder = nn.Sequential(
            nn.Linear(in_shape, 64),
            nn.ReLU(True),
            nn.Dropout(0.1),
            nn.Linear(64, 32),
            nn.ReLU(True),
            nn.Dropout(0.1),
            nn.Linear(32, enc_shape),
        )

        #Decoder
        self.decoder = nn.Sequential(
            nn.BatchNorm1d(enc_shape),
            nn.Linear(enc_shape, 32),
            nn.ReLU(True),
            nn.Dropout(0.1),
            nn.Linear(32, 64),
            nn.ReLU(True),
            nn.Dropout(0.1),
            nn.Linear(64, in_shape)
        )

    def forward(self, x):
        encoded = self.encoder(x)
        decoded = self.decoder(encoded)
        return encoded, decoded

model2 = Autoencoder(in_shape=2, enc_shape=1).double()
print(model2)

正如你所见,现在所有隐藏层都有一个非线性 RELU 激活函数。我们仍然使用 MSE 损失函数和 Adam 优化器。

loss_func = nn.MSELoss()
optimizer = torch.optim.Adam(model2.parameters())

我们使用 X_circ 作为训练数据集,但我们使用 MinMaxScaler() 将所有特征缩放到 [0,1] 的范围内。

X_circ_scaled = MinMaxScaler().fit_transform(X_circ)
X_circ_train = torch.from_numpy(X_circ_scaled)

接下来,我们用 5000 个周期训练模型。

# Listing 20

def train(model, loss_func, optimizer, n_epochs, X_train):
    model.train()
    for epoch in range(1, n_epochs + 1):
        optimizer.zero_grad()
        encoded, decoded = model(X_train)
        loss = loss_func(decoded, X_train)
        loss.backward()
        optimizer.step()

        if epoch % int(0.1*n_epochs) == 0:
            print(f'epoch {epoch} \t Loss: {loss.item():.4g}')
    return encoded, decoded

encoded, decoded = train(model2, loss_func, optimizer, 5000, X_circ_train)
epoch 500   Loss: 0.01391
epoch 1000   Loss: 0.005599
epoch 1500   Loss: 0.007459
epoch 2000   Loss: 0.005192
epoch 2500   Loss: 0.005775
epoch 3000   Loss: 0.005295
epoch 3500   Loss: 0.005112
epoch 4000   Loss: 0.004366
epoch 4500   Loss: 0.003526
epoch 5000   Loss: 0.003085

最后,我们绘制了瓶颈层(编码数据)中单个神经元的值,用于训练数据集中的所有观测点。请记住,我们为训练数据集中的每个数据点分配了颜色。现在我们使用相同的颜色表示编码数据点。这个图在图 27 中显示,现在与 PCA 生成的投影数据点(图 25)相比,大多数投影数据点的顺序是正确的。

encoded = encoded.detach().numpy()

fig = plt.figure(figsize=(16, 2))
frame = plt.gca()
plt.scatter(encoded.flatten(), [0]*len(encoded.flatten()),
            c=colors, cmap=plt.cm.jet, alpha =0.7)
plt.axhline(y=0, color='grey')
plt.xlabel("$z_1$", fontsize=18)
frame.axes.get_yaxis().set_visible(False)
plt.show()

图 27

这是因为非线性自编码器不再将原始数据点投影到一条直线上。自编码器试图找到一条曲线(也称为非线性流形),沿着这条曲线,投影的数据点具有最大的方差,并将输入数据点投影到这些曲线上(图 28)。这个例子清楚地展示了自编码器相对于 PCA 的优势。PCA 是线性变换,因此不适用于具有非线性相关的数据集。另一方面,我们可以在自编码器中使用非线性激活函数。这使我们能够使用自编码器进行非线性降维。

图 28(作者提供的图片)

在本文中,我们讨论了 PCA、SVD 和自编码器背后的数学。PCA 为数据集找到一个新的正交坐标系统。这个坐标系统的每个轴称为主成分。选择主成分的方式是使数据点在每个坐标轴上的方差在所有可能的方向上最大化,这些方向都与已经考虑过的主成分正交。

线性自编码器和 PCA 有一些相似之处。我们看到一个具有中心化输入特征、线性激活和 MSE 成本函数的自编码器可以找到由主成分张成的相同子空间。我们也得到 PCA 的相同投影数据点。然而,它不能找到主成分本身。实际上,它返回的基底甚至不一定是正交的。

另一方面,自编码器比 PCA 更灵活。PCA 的一个问题是,它假设投影数据点的最大方差沿着由主成分表示的直线。因此,如果最大方差沿着非线性曲线,PCA 无法找到它。然而,一个具有非线性激活函数的自编码器可以找到一个非线性流形,并将数据点投影到上面。

我希望你喜欢阅读这篇文章。如果你有任何问题或建议,请告诉我。文章中的所有代码清单都可以从 GitHub 下载为 Jupyter Notebook,链接为:

github.com/reza-bagheri/autoencoders_pca

附录

寻找主成分的方程:

对于第二主成分,我们需要找到最大化的向量u

在这些约束下:

这意味着我们需要最大化

关于u₂。为了找到最大点,我们写道:

通过将这个方程乘以u,我们得到:

现在使用方程 7 和协方差矩阵是对称矩阵的事实,我们可以写:

通过将这个方程代入前面的方程,我们得到:

现在使用正交性约束和u₁被标准化的事实,我们得到:

使用这个方程和方程 A.1,可以得出

所以,我们有

我们还需要找到最大化的向量uᵢ*

在这些约束下:

这等同于最大化

关于u。为了找到最大点,我们将这个项对u的导数设为零:

通过将这个方程乘以uₖᵀ(其中 1≤ki*-1),我们得到:

我们知道之前的主成分是 S 的特征向量,并且 S 是一个对称矩阵。因此,我们有:

并且利用正交约束,我们得出:

将此方程代入方程 A.2 中,可以得出

因此 uS 的特征向量,而 λᵢ 是其对应的特征值。

方程 17 的证明

假设我们有一个秩为 rm×n 矩阵 X,并且 X 的奇异值已排序,因此我们有:

我们想要证明的是

其中 A 是一个秩为 km×n 矩阵,ZW 分别是 m×kk×n 的矩阵。我们知道 k<n。此外,在设计矩阵中,我们通常有 m>n。因此 k 小于 mn

我们知道矩阵的秩不能超过其行数或列数。因此,我们有:

还可以证明

因此,有

因此,ZW 的秩不能超过 k。此外,我们将展示,最小化 ||X-ZW||F 的矩阵 ZW 不能有低于 k 的秩。假设 ZW 是两个矩阵,使得 Z W 是一个秩为 k 的矩阵,并且在所有秩为 k 的矩阵中最小化 ||X-ZW||F。根据方程 16:

同样地,假设 ZW 是两个矩阵,使得 Z W 是一个秩为 m 的矩阵 (m<k),在所有秩为 m 的矩阵中最小化 ||X-Z W||_F。我们有:

显然

因此

这意味着秩为 k 的矩阵总是给出 ||X-Z W||F 的较低值。因此,我们得出结论,ZW 的秩不能少于 k。因此,最小化 ||X-Z W||FZW 的最佳值应为秩为 k 的矩阵。

线性自编码器的成本函数

我们想要证明的是

为了证明这一点,我们首先计算矩阵 X-Z(W^[2])

但根据方程 22,我们有:

因此对于第 k 个观察值,我们可以写成:

将此方程代入方程 A.4 中,我们得到:

最终使用方程 15,我们得到:

对 K-means 的深度解析,适合不太懂技术的读者

原文:towardsdatascience.com/a-deep-dive-into-k-means-for-the-less-technophile-eaea262bd51f?source=collection_archive---------16-----------------------#2023-01-24

从聚类到算法:五步之旅

Alexandre AllouinTowards Data Science Alexandre Allouin

·

关注 发布于 Towards Data Science ·11 分钟阅读·2023 年 1 月 24 日

--

图片来自Pauline Allouin

虽然这篇文章比我之前的文章更为技术化——其中一篇是如何成功地将技术语言翻译为管理层和用户的语言——但它旨在用简单的语言解释数据科学中常用的一种算法。最*,我需要向一位好奇的非技术背景的高级经理解释什么是聚类。他理解了聚类的整个概念,并想进一步了解它如何被转化为算法。

本文的解释远远超出了简单的高级经理解释,但我认为我会分享这个有趣的经历;在这里,我尝试提供通常可以通过在线搜索找到的更多细节。因此,它可能为人们在首次实施 K-means 算法时提出的问题提供答案。此外,我不打算重复互联网上的许多资源,而是专注于其本质。在每段的末尾,我将突出可以传达给非技术观众的要点。

大致概况

聚类是一种将数据点集合按相似性分组(或簇)的技术。这种方法有许多应用,如行为分割、库存分类、健康监测中的群体识别、图像分组、异常检测等。它可以用来探索或扩展特定领域的知识,特别是当数据集过大而无法用传统工具分析时。

一个常见的例子是客户细分:基于不同因素(年龄、性别、收入等),你希望识别关键的档案/人物角色,以更好地定位你的营销活动,提高服务或收入。这篇Franco 的帖子提供了这种分析的具体和完整的应用。

K-means 就是这样的聚类算法之一(其他示例可以参考这篇Indraneel 的帖子)。它是一种无监督机器学习算法。换句话说,该算法在没有人工干预的情况下发现数据中的模式。

K-means 聚类是一种无监督学习算法,它将相似的数据点分组为不重叠的簇。尽管随着机器学习的兴起,它变得非常流行,但该算法实际上是由 Bell Labs 的 Stuart Lloyd 于 1957 年提出的。

K-means 最著名的应用包括客户细分、异常检测、文档分类等。

K-means 算法概述

  • 第一步:指定要使用的簇数(K)。

  • 第二步:随机初始化(K)个质心。

  • 第三步:对于每个数据点,计算它与每个质心的距离,并将数据点分配给离它最*的质心。

  • 第四步:计算每个簇的新质心(即每个变量的均值)。

  • 第五步:重复第三步,直到质心的位置不再变化(收敛)。

几个连续变量的质心可以看作是这些变量的均值,即表示簇中心的数据点。需要注意的是,质心的收敛并不保证,这在实施这种算法时应予以考虑。

当你第一次发现 K-means 算法时,可能会出现两个问题:

  • 如何选择簇的数量(第一步)?

  • 质心的随机初始化是否对结果有影响(步骤 2)?

这两个主题将在接下来的部分中讨论。

K-means 假设你知道你想用来划分数据集的簇的数量(K)。因此,K 的选择由运行算法的人决定。在众多可用的技术中,有两种主要的方法可以帮助你选择正确的 K 值。这两种方法在网上有广泛的描述。开始时,你可能对以下两篇 Medium 文章感兴趣:

再次,我不会过多关注公式或代码,而是专注于概念的解释。

K-means 中的字母 K 代表算法将创建的簇的数量。这个簇的数量由分析师定义。K-means 通过测量数据点之间的距离来分配数据点到簇,并将最*的点分组在一起。

肘部法的原理

这是一种经验方法:主要思想是对可能范围内的 K 值(例如,从 1 到 10 个簇)运行 K-means 算法,并评估每种情况(K=1, K=2, …)中数据点与其关联质心(即每个簇)之间的距离。

对于每次运行 K-means 算法,我们实际上计算了每个点与其最*中心的*方距离之和,这让我们想起了方差的概念。这些量的总和称为 WCSS,即簇内*方和。

图片来自亚历山大·阿卢安

直观上,当簇的数量增加时,这个量会减少。确实,通过增加质心的数量,我们增加了数据点靠*质心的机会,从而减少了距离。该方法旨在找到一个点(肘部),在这个点上添加更多簇不会带来额外价值。

正如许多文章中所述的挑战是确定 K 簇的数量是否正确,因为我们不想得到太多的簇。因此,最佳选择可能有些任意。

有时使用的术语:

  • 惯性:样本到其最*簇中心的*方距离之和(等同于 WCSS)。

  • 失真:从各自簇的质心到数据点的欧几里得*方距离的*均值。

轮廓分析

轮廓值是衡量一个对象与其自身簇(凝聚度)相比与其他簇(分离度)相似程度的指标。-维基百科

原则如下:如果簇的数量正确,则簇内每个数据点之间的*均距离(通常称为“*均簇内距离”)应小于与其他簇的其他数据点之间的*均距离(“*均簇间距离”)。

图片由 Alexandre Allouin 提供

该方法假设数据已经被聚类。换句话说,每个数据点已经被分配到一个簇。现在让我们关注一个给定的数据点(在上图中为浅橙色),以三个簇为例:

  1. 我们计算该点与同一簇中所有其他数据点的*均距离。这通常记作 a(i)

  2. 我们计算相同点与其他簇所有数据点的*均距离。我们从蓝色簇开始。

  3. 我们对绿色簇进行与步骤 2 相同的计算。

  4. 我们比较步骤 2 和步骤 3 中获得的结果,并保留最小值,因为我们真正关心的是哪个簇靠*,而不是哪个簇远离。换句话说,我们对我们考虑的数据点的邻域感兴趣。我们将这个值表示为 b(i)

  5. 因此,给定数据点 i 的轮廓值 S 通过从 a(i) 中减去 b(i) 并将该数量除以 a(i)b(i) 之间的最大值来获得。我尝试避免公式,但有时查看它们可能很有用:

因此,这个值总是介于 -1 和 1 之间。

b(i)a(i) 始终为正(距离的*均值),因此如果*均簇内距离 a(i) 大于*均簇间距离 b(i),则 S(i) 将为负值。所考虑的数据点更接*另一个簇。在这种情况下,它似乎被错误分类了。

如果我们扩展这种推理,当 b(i) 相对于 a(i) 非常大时,S(i) 将接* 1:数据点被很好地分类了。

S(i) 约为零时,*均簇内距离大约等于*均簇间距离,将数据点分配到一个簇或另一个簇的选择并不明确。

K 的最佳值选择

在给定簇中所有数据点的 S(i) 的*均值称为该簇的*均轮廓宽度 S(K)。对于每个模型(具有 2、3、4、…… K 个簇),我们可以计算 S(K) 的*均值——簇的*均轮廓宽度。这个量称为 *均轮廓分数。为了选择 K 的最佳值,获得的分数必须尽可能高。

两种方法,肘部法则和轮廓法,从不同的角度提供信息。它们通常结合使用,以增强 K 的选择。另一个盟友有时也可以支持决策:执行分析的人的领域知识!

虽然确定正确的聚类数量的决定最终取决于分析师,但存在不同的技术可以帮助分析师做出明智的决策。最受欢迎的方法是肘部法则和轮廓分析。

初始化质心

另一个可能引起你关注的算法方面是初始质心的随机选择。实际上,K-means 算法是非确定性的:算法可以根据其初始化产生不同的结果。下面是对同一数据集的两次不同算法运行的示例:

图片来源:Alexandre Allouin

这个示例的目的是仅仅为了说明 K-means 算法可能产生不同的结果。实际上,在这个特定的情况下,聚类的数量可能不合适。当 K-means 算法在这个数据集上多次运行,但只设置三个聚类时,结果始终是一致的。此外,作为最佳实践,建议多次运行 K-means 以查看提出的聚类的稳定性。

sklearn.cluster.KMeans 允许你选择初始化首个质心的方式。有两个选项:随机或 K-means++。K-means++ 是一种智能质心初始化技术,可以加速算法的收敛。其主要思想是选择尽可能远离的初始质心。

K-means 算法可能导致不同的结果,这在聚类数量不正确或数据模式不明确时尤其如此。

需要记住的事项

特征缩放的重要性

特征缩放 对于 K-means 算法产生高质量结果至关重要。因为算法依赖于距离估计,特别是在计算质心时,不同的变量必须在相同的量级范围内。

下面是一个包含两个明显聚类的数据集示例(图表 A)。之所以明显,是因为自动缩放让你看到了这一点,但如果这个数据集在具有相同刻度的图表上进行可视化(图表 B),这对你或算法来说就不再可见了。

当你在这个未标准化的数据集上运行 K-means(图表 C 和图表 D)时,它变得更加有趣:创建了两个包含每个点云一半的聚类。这显然是你想要避免的结果!

最后,当数据集被标准化时(图表 E 和图表 F),尺度差异出现,K-means 能够识别出真实的聚类。

图片来源:Alexandre Allouin

更具体地说,假设你想根据年龄和年收入进行客户细分。你有一个包含 500 人的数据集,年龄在 20 到 40 岁之间,如下图所示。图表 A 代表了这个数据集(自动缩放),我们可以直观地想象出两个主要的簇。正如上面解释的那样,这些簇是可见的,因为两个维度的尺度不同,否则你将无法可视化。当 K-means 应用于原始数据时,算法将数据点水*地分成两组。原因是,在计算距离时,20 年的变化会与$20 的年收入变化进行比较。K-means 算法在步骤 3 中使用的欧几里得距离在一个可能的数据点 A(23, 21000)和一个假设的质心 K1(25, 22500)之间将是:

因此,年龄在计算距离时权重不大,因此,无论一个人是 20 岁还是 40 岁,影响都不显著。因此,K-means 主要根据年收入的均值来分割簇,因为这是唯一真正重要的维度。

然而,你希望你的算法能够检测到这个年龄变异,因为它在你的研究中是一个重要因素。图表 C 和图表 D 在数据标准化后运行 K-means 时提供了更好的簇。

图片来源:Alexandre Allouin

对这个例子的最后评论是,你可以看到一些数据点的分类可以更好一些。这是由于标准化效应,但也因为它们可能是离群点,而 K-means 在处理离群点时表现不佳。

对离群点的敏感性

由于算法最小化数据点和质心之间的距离,离群点会影响质心的位置,并可能改变簇的边界。然而,在运行 K-means 之前处理离群点可能具有挑战性,尤其是在大数据集上。虽然对 K-means 处理离群点的挑战做出了许多尝试,但似乎没有达成有效解决方案的真正共识。大多数时候,使用其他算法,如 K-medians、K-medoids 或 DBSCAN 更为优先。

类别数据

出于同样的原因,K-means 最小化数据点与其最*质心之间的距离,这使得 K-means 对类别数据不相关。即使你将类别编码为数字,两个类别之间的距离也没有意义:值之间没有自然顺序。应该优先使用其他算法。

数据模式

该算法的核心基于质心的概念,因此 K-means 在设计上总是尝试寻找球形簇。包含特定模式的数据集可能难以被该算法捕捉。像 DBSCAN 这样的算法更适合检测特殊数据形状的簇。一个示例可以在这里找到:DBSCAN 聚类处理 K-means 无法处理的数据形状。

由于 K-means 完全依赖于距离测量,该算法对异常值非常敏感:它们会改变簇的中心,从而影响数据点的分配。

管理者是否应该参与技术方面的考虑?

最*,在由CERN IdeaSquare日内瓦创新运动协会组织的工作坊中,我们探索了在开发产品时如何调和高级经理与技术专家之间的沟通。

我们如何优化这种协作,以便更快、更好地交付解决方案?虽然活动很有趣(这也可以作为一个参数来考虑),但让双方坐到同一张桌子上共同工作似乎帮助很大。在数据科学的具体案例中,培养各利益相关者之间的数据文化无疑是正确的方向。

如前所述,我尝试过这种方法,并得到了来自一位高级经理的有趣反馈。你有兴趣吗?为什么不花 10 分钟与高级经理们开会,展示所选择的模型以及在调整某些参数时的表现呢?脚本可能过于粗糙,但使用低代码/无代码软件(如 Knime)可能会引发意想不到但有趣的结果。

致谢

  • 真诚感谢Sharon的校对、建议和持续支持。

  • 特别感谢Mathilde的有益评论和 Pauline 的艺术贡献!

深入探讨 Visual Transformer (ViT) 模型的代码

原文:towardsdatascience.com/a-deep-dive-into-the-code-of-the-visual-transformer-vit-model-1ce4cc05ca8d

分析 HuggingFace ViT 实现

Alexey KravetsTowards Data Science Alexey Kravets

·发表于 Towards Data Science ·14 分钟阅读·2023 年 8 月 15 日

--

Vision Transformer (ViT) 是计算机视觉演变中的一个显著里程碑。ViT 挑战了图像最好通过卷积层处理的传统观念,证明了基于序列的注意力机制可以有效地捕捉图像中的复杂模式、上下文和语义。通过将图像分解为可管理的补丁并利用自注意力机制,ViT 能够捕捉局部和全局关系,使其在图像分类、目标检测等多种视觉任务中表现出色。在本文中,我们将深入剖析 ViT 在分类任务中的工作原理。

unsplash.com/photos/aVvZJC0ynBQ

介绍

ViT 的核心思想是将图像视为一系列固定大小的补丁,然后将这些补丁展*并转换为 1D 向量。这些补丁随后由一个 transformer 编码器处理,使得模型能够捕捉整个图像的全局上下文和依赖关系。通过将图像分割成补丁,ViT 有效地降低了处理大图像的计算复杂性,同时保持了建模复杂空间交互的能力。

首先,我们从 hugging face transformers 库中导入 ViT 分类模型:

from transformers import ViTForImageClassification
import torch
import numpy as np

model = ViTForImageClassification.from_pretrained("google/vit-base-patch16-224")

patch16–224 表示模型接受大小为 224x224 的图像,并且每个补丁的宽度和高度为 16 像素。

这就是模型架构的样子:

ViTForImageClassification(
  (vit): ViTModel(
    (embeddings): ViTEmbeddings(
      (patch_embeddings): PatchEmbeddings(
        (projection): Conv2d(3, 768, kernel_size=(16, 16), stride=(16, 16))
      )
      (dropout): Dropout(p=0.0, inplace=False)
    )
    (encoder): ViTEncoder(
      (layer): ModuleList(
        (0): ViTLayer(
          (attention): ViTAttention(
            (attention): ViTSelfAttention(
              (query): Linear(in_features=768, out_features=768, bias=True)
              (key): Linear(in_features=768, out_features=768, bias=True)
              (value): Linear(in_features=768, out_features=768, bias=True)
              (dropout): Dropout(p=0.0, inplace=False)
            )
            (output): ViTSelfOutput(
              (dense): Linear(in_features=768, out_features=768, bias=True)
              (dropout): Dropout(p=0.0, inplace=False)
            )
          )
          (intermediate): ViTIntermediate(
            (dense): Linear(in_features=768, out_features=3072, bias=True)
          )
          (output): ViTOutput(
            (dense): Linear(in_features=3072, out_features=768, bias=True)
            (dropout): Dropout(p=0.0, inplace=False)
          )
          (layernorm_before): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
          (layernorm_after): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
        )

        .......

        (11): ViTLayer(
          (attention): ViTAttention(
            (attention): ViTSelfAttention(
              (query): Linear(in_features=768, out_features=768, bias=True)
              (key): Linear(in_features=768, out_features=768, bias=True)
              (value): Linear(in_features=768, out_features=768, bias=True)
              (dropout): Dropout(p=0.0, inplace=False)
            )
            (output): ViTSelfOutput(
              (dense): Linear(in_features=768, out_features=768, bias=True)
              (dropout): Dropout(p=0.0, inplace=False)
            )
          )
          (intermediate): ViTIntermediate(
            (dense): Linear(in_features=768, out_features=3072, bias=True)
          )
          (output): ViTOutput(
            (dense): Linear(in_features=3072, out_features=768, bias=True)
            (dropout): Dropout(p=0.0, inplace=False)
          )
          (layernorm_before): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
          (layernorm_after): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
        )
      )
    )
    (layernorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
  )
  (classifier): Linear(in_features=768, out_features=1000, bias=True)
)

嵌入

补丁嵌入

图像转换为补丁是通过 Conv2D 层完成的。如我们所知,Conv2D 层在输入数据上进行二维卷积操作,以从图像中学习特征和模式。然而,在这种情况下,Conv2D 层用于通过使用stride参数将图像分成 NxN 个补丁。步幅决定了滤波器在输入数据上滑动的步长。在这种情况下,因为我们的图像是 224x224,补丁大小为 16,意味着每个维度有 224/16 = 14 个补丁,如果我们选择stride=16,我们实际上将图像分为 14 个不重叠的补丁。

为了直观说明,假设图像的形状为 4x4,步幅为 2:

补丁创建,由作者提供的图像

例如,第一个和第二个补丁将是:

proj = model.vit.embeddings.patch_embeddings.projection
torch.allclose(torch.sum(image[0, :, 0:16, 0:16] * w[0]) + b[0],
               proj(image)[0][0][0, 0], atol=1e-6)
# True

torch.allclose(torch.sum(image[0, :, 16:32, 0:16] * w[0]) + b[0],
                 proj(image)[0][0][1, 0], atol=1e-6)

# True

模式很明确——为了计算每个补丁,我们跳过 16 个像素以获取不重叠的补丁。如果我们对整个图像执行此操作,我们将得到一个 1 x 14 x 14 的张量,其中每个补丁由使用 Conv2D 的第一个滤波器计算得出。然而,有768 个滤波器,这意味着最后我们得到一个 768 x 14 x 14 维度的张量。所以现在我们实际上为每个补丁得到一个 768 维的表示,这就是我们的补丁嵌入。我们还对张量进行展*和转置,因此嵌入形状变为 [batch_size, 196, 768],其中第二维展*为 14 x 14 = 196,我们实际上拥有一个 196 个补丁的序列,每个补丁的嵌入大小为 768。

embeddings = model.vit.embeddings.patch_embeddings.projection(image)
# shape (batch_size, 196, 768)
embeddings = embeddings.flatten(2).transpose(1, 2)

如果我们想从头完全重现这一层,这是代码:

 batch_size = 1 
F = 768 # number of filters
H1 = 14 # output dimension hight - 224/16
W1 = 14 # output dimension width - 224/16
stride = 16
HH = 16 # patch hight
WW = 16 # patch width
w = model.vit.embeddings.patch_embeddings.projection.weight
b = model.vit.embeddings.patch_embeddings.projection.bias

out = np.zeros((N, F, H1, W1))
chunks = []
for n in range(batch_size):
    for f in range(F):
        for i in range(H1):
            for j in range(W1):
                # perform convolution operation
                out[n, f, i, j] = torch.sum( image[n, :, i*stride:i*stride+HH, j*stride : j*stride + WW] * w[f] ) + b[f]

np.allclose(out[0], embeddings[0].detach().numpy(), atol=1e-5)
# True

现在,如果你对语言转换器 (如有需要请查看这里) 熟悉,你应该记得[CLS]标记,它的表示作为整个文本的浓缩和信息性摘要,使模型能够基于从转换器编码器中提取的特征做出准确的预测。在 ViT 中,[CLS]标记也具有与文本相同的功能,它被附加到上面计算的表示中。

[CLS]标记是一个我们将通过反向传播学习的参数:

cls_token = nn.Parameter(torch.randn(1, 1, 768))
cls_tokens = cls_token.expand(batch_size, -1, -1)
# append [CLS] token
embeddings = torch.cat((cls_tokens, embeddings), dim=1)

位置嵌入

就像在语言转换器中一样,为了保留补丁的位置信息,ViT 包括位置嵌入。位置嵌入帮助模型理解不同补丁之间的空间关系,使其能够捕捉图像的结构。

位置嵌入是与之前计算的[CLS]标记形状相同的张量,即 [batch_size, 197, 768]

embeddings = embeddings + model.vit.embeddings.position_embeddings

Dropout

补丁嵌入之后是一个 Dropout 层。在 dropout 中,我们以一定的 dropout 概率将一些值替换为零。Dropout 有助于减少过拟合,因为我们随机阻塞某些神经元的信号,使得网络需要找到其他路径来减少损失函数,从而学会更好地泛化,而不是依赖于某些特定路径。我们也可以将 dropout 看作是一种模型集成技术,因为在训练过程中,每一步我们随机停用某些神经元,最终得到“不同”的网络,这些网络在评估时被集成在一起。

在 Embeddings 层的末尾,我们有:

# compute the embedding
embeddings = model.vit.embeddings.patch_embeddings.projection(image)
embeddings = embeddings.flatten(2).transpose(1, 2)
# append [CLS] token
cls_token = model.vit.embeddings.cls_token
embeddings = torch.cat((cls_tokens, embeddings), dim=1)
# positional embedding
embeddings = embeddings + self.position_embeddings
# droput
embeddings = model.vit.embeddings.dropout(embeddings) 

编码器

ViT 使用一系列的 Transformer 编码器块,类似于语言模型如 BERT 中使用的块。每个编码器块包括多头自注意力机制和前馈神经网络。自注意力机制使模型能够捕捉不同补丁之间的关系,而前馈神经网络则执行非线性变换。

具体而言,每一层由自注意力、中间和输出模块组成。

(0): ViTLayer(
  (attention): ViTAttention(
    (attention): ViTSelfAttention(
      (query): Linear(in_features=768, out_features=768, bias=True)
      (key): Linear(in_features=768, out_features=768, bias=True)
      (value): Linear(in_features=768, out_features=768, bias=True)
      (dropout): Dropout(p=0.0, inplace=False)
    )
    (output): ViTSelfOutput(
      (dense): Linear(in_features=768, out_features=768, bias=True)
      (dropout): Dropout(p=0.0, inplace=False)
    )
  )
  (intermediate): ViTIntermediate(
    (dense): Linear(in_features=768, out_features=3072, bias=True)
  )
  (output): ViTOutput(
    (dense): Linear(in_features=3072, out_features=768, bias=True)
    (dropout): Dropout(p=0.0, inplace=False)
  )
  (layernorm_before): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
  (layernorm_after): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
)

自注意力

自注意力是 Vision Transformer (ViT) 模型中的一个关键机制,使其能够捕捉图像中不同补丁之间的关系和依赖性。它在提取上下文信息和理解补丁之间的长短程交互中发挥了重要作用。

每个补丁关联有三个向量:Key、Query 和 Value。这些向量是通过对原始补丁嵌入进行线性变换学习得到的。Key 向量代表当前补丁的信息Query 向量用于询问其他补丁,而Value 向量包含与其他补丁相关的信息

由于我们在前一部分已经计算了嵌入,我们通过使用 Key、Query 和 Value 矩阵对嵌入进行投影,来计算 Key、Query 和 Value:

import math 
import torch.nn as nn

torch.manual_seed(0)

hidden_size = 768
num_attention_heads = 12
attention_head_size = hidden_size // num_attention_heads # 64

hidden_states = embeddings

# apply LayerNorm to the embeddings
hidden_states = model.vit.encoder.layer[0].layernorm_before(hidden_states)

# take first layer of the Transformer
layer_0 = model.vit.encoder.layer[0]

# shape (768, 64) 
key_matrix = layer_0.attention.attention.key.weight.T[:, :attention_head_size]
key_bias = layer_0.attention.attention.key.bias[:attention_head_size]

query_matrix = layer_0.attention.attention.query.weight.T[:, :attention_head_size] 
query_bias = layer_0.attention.attention.query.bias[:attention_head_size]

value_matrix = layer_0.attention.attention.value.weight.T[:, :attention_head_size]
value_bias = layer_0.attention.attention.value.bias[:attention_head_size]

# compute key, query and value for the first head attention
# all of shape (b_size, 197, 64)
key_1head = hidden_states @ key_matrix + key_bias
query_1head = hidden_states @ query_matrix + query_bias
value_1head = hidden_states @ value_matrix + value_bias

请注意,我们跳过了 LayerNorm 操作,稍后将介绍。

对于每个 Query 向量,通过测量 Query 向量和所有其他补丁的 Key 向量之间的兼容性或相似性来计算注意力分数。这是通过点积操作完成的,然后应用 Softmax 函数以获得形状为 [b_size, 197, 197] 的归一化注意力分数。注意力矩阵是方形的,因为所有补丁都相互关注,这就是为什么它被称为自注意力。这些分数表明在处理查询补丁时应给予每个补丁多少关注。由于每个补丁的下一层的新嵌入是基于注意力分数和所有其他补丁的值推导出来的,因此我们为每个补丁获得一个上下文嵌入,因为它是基于图像中的所有其他补丁推导的。

为了进一步澄清这一点,请回忆一下我们最开始用 Conv2D 层将图像拆分成补丁,以获得每个补丁的 768 维嵌入向量——这些嵌入是独立的,因为补丁之间没有交互(没有重叠)。然而,在 transformer 层中,补丁嵌入被混合,成为其他补丁嵌入的函数。例如,第一层的嵌入是:

# shape (b_size, 197, 197)
# compute the attention scores by dot product of query and key
attention_scores_1head = torch.matmul(query_1head, key_1head.transpose(-1, -2))

attention_scores_1head = attention_scores_1head / math.sqrt(attention_head_size)
attention_probs_1head = nn.functional.softmax(attention_scores_1head, dim=-1)

# contextualized embedding for this layer
context_layer_1head = torch.matmul(attention_probs_1head, value_1head)

如果我们放大并查看第一个补丁:

patch_n = 1
# shape (, 197)
print(attention_probs_1head[0, patch_n])
[2.4195e-01, 7.3293e-01, ..,
        2.6689e-06, 4.6498e-05, 1.1380e-04, 5.1591e-06, 2.1265e-05], 

对它的新嵌入(索引为 0 的是[CLS] token)是不同补丁的嵌入的组合,其中对第一个补丁本身(0.73)、[CLS] token(0.24)以及其他所有补丁的关注度最高。但情况并不总是如此。实际上,在后续层中,第一个补丁可能会更多地关注其周围的补丁,而不是自身和[CLS] token,甚至可能关注距离很远的补丁——这取决于模型认为对解决某个任务有用的东西。

此外,你可能已经注意到,我只选择了查询、键和值权重矩阵中的前 64 列。这 64 列代表了第一个注意力头,但实际上(在这个模型大小下)有 12 个注意力头。每个注意力头对补丁的表示都会有所不同。实际上,如果我们查看第一个补丁的第三个注意力头,可以看到第一个补丁对第二个补丁的关注度最高(0.26),而不是像第一个注意力头那样对自己。

# shape (, 197)
[2.6356e-01, 1.2783e-03, 2.6888e-01, ... , 1.8458e-02]

因此,不同的注意力头将捕捉到补丁之间不同类型的关系,帮助模型从不同的角度观察事物。

为了并行计算所有这些注意力头,我们进行如下操作:

def transpose_for_scores(x: torch.Tensor) -> torch.Tensor:
    new_x_shape = x.size()[:-1] + (num_attention_heads, attention_head_size)
    x = x.view(new_x_shape)
    return x.permute(0, 2, 1, 3)

mixed_query_layer = layer_0.attention.attention.query(hidden_states)

key_layer = transpose_for_scores(layer_0.attention.attention.key(hidden_states))
value_layer = transpose_for_scores(layer_0.attention.attention.value(hidden_states))
query_layer = transpose_for_scores(mixed_query_layer)

# Take the dot product between "query" and "key" to get the raw attention scores.
attention_scores = torch.matmul(query_layer, key_layer.transpose(-1, -2))
attention_scores = attention_scores / math.sqrt(attention_head_size)

# Normalize the attention scores to probabilities.
attention_probs = nn.functional.softmax(attention_scores, dim=-1)

# This is actually dropping out entire tokens to attend to, which might
# seem a bit unusual, but is taken from the original Transformer paper.
attention_probs = layer_0.attention.attention.dropout(attention_probs)

context_layer = torch.matmul(attention_probs, value_layer)

context_layer = context_layer.permute(0, 2, 1, 3).contiguous()
new_context_layer_shape = context_layer.size()[:-2] + (hidden_size,)
context_layer = context_layer.view(new_context_layer_shape)

在应用自注意力后,我们应用另一个投影层和 Dropout——这样我们就完成了自注意力层的处理!

output_weight = layer_0.attention.output.dense.weight
output_bias = layer_0.attention.output.dense.bias

attention_output = context_layer @ output_weight.T + output_bias
attention_output = layer_0.attention.output.dropout(attention_output)

哎,等一下,我答应过要解释LayerNorm操作。

层归一化是一种用于提升深度学习模型训练和性能的规范化技术。它解决了内部协变量偏移的问题——在训练过程中,由于神经网络的权重变化,每层的输入分布可能会显著改变,从而使模型难以收敛。层归一化通过确保每层的输入具有一致的均值和方差来解决这个问题,从而稳定学习过程。它的实现方法是通过其均值和标准差对每个补丁嵌入进行标准化,使其均值为零,方差为一。然后应用训练的权重和偏差,使其可以被移动到具有不同的均值和方差,以便模型在训练过程中自动适应。由于我们在不同示例之间独立计算均值和标准差,因此它与[批量归一化](https://en.wikipedia.org/wiki/Batch_normalization#:~:text=Batch normalization (also known as,and Christian Szegedy in 2015.)不同,后者是在批量维度上进行归一化,因此依赖于批量中的其他示例。

让我们来看第一个补丁嵌入:

first_patch_embed = embeddings[0][0]
# compute first patch mean
first_patch_mean = first_patch_embed.mean()
# compute first patch variance
first_patch_std = (first_patch_embed - first_patch_mean).pow(2).mean()
# standardize the first patch
first_patch_standardized = (first_patch_embed - first_patch_mean) / torch.sqrt(first_patch_std + 1e-12)
# apply trained weight and bias vectors
first_patch_norm = layer_0.layernorm_before.weight * first_patch_standardized + layer_0.layernorm_before.bias

中级

在中级课程之前,我们执行另一个层归一化和一个残差连接。到现在为止,应该清楚为什么我们需要应用另一个层归一化——我们需要规范化来自自注意力的上下文嵌入以提高收敛性,但你可能会想,我提到的那个其他的残差东西是什么?

残差连接是深度神经网络中的一个关键组件,可以缓解训练非常深的架构的挑战。随着我们通过堆叠更多层来增加神经网络的深度,我们会遇到梯度消失/爆炸的问题,在梯度消失的情况下,模型无法再学习,因为传播的梯度接*零,初始层停止变化权重和改进(如果你想了解更多关于梯度消失的问题,可以查看这篇文章这篇)。相反,梯度爆炸的问题发生在权重无法因极端更新而稳定,最终爆炸(趋于无穷大)。现在,适当的权重初始化和规范化有助于解决这个问题,但观察到的是,即使网络变得更加稳定,性能也会下降,因为优化变得更加困难。添加这些残差连接有助于提高性能,即使我们继续增加深度,网络也更容易优化。

它是如何实现的?很简单——我们只是将原始输入添加到经过一些变换后的变换输出中。

transformations = nn.Sequential([nn.Linear(), nn.ReLU(), nn.Linear()])
output = input + transformations(input)

另一个关键见解是,如果残差连接的 变换 学会了逼*恒等函数,那么输入与学习到的特征的加法将没有任何效果。实际上,网络可以在需要时学习修改或细化特征。

在我们的案例中,残差连接是初始 嵌入attention_output 之间的和,这些是经过 self-attention 层所有变换后的 嵌入

# first residual connection - NOTE the hidden_states are the 
# `embeddings` here
hidden_states = attention_output + hidden_states

# in ViT, layernorm is also applied after self-attention
layer_output = layer_0.layernorm_after(hidden_states)

在 Intermediate 类中,我们执行线性投影并应用 非线性

layer_output_intermediate = layer_0.intermediate.dense(layer_output)
layer_output_intermediate = layer_0.intermediate.intermediate_act_fn(layer_output_intermediate)

ViT 中使用的非线性是 GeLU 激活函数。它被定义为标准正态分布的累积分布函数:

arxiv.org/pdf/1606.08415v3.pdf

通常,它用以下公式进行逼*以加快计算:

arxiv.org/pdf/1606.08415v3.pdf

从下面的图表可以看出,ReLU 函数,由公式 max(input, 0) 给出,在正域内是单调的、凸的和线性的,而 GeLU 在正域内是非单调的、非凸的和非线性的,因此可以更容易地逼*复杂的函数。

此外,GeLU 函数是*滑的——与 ReLU 函数不同,后者在零点有一个急剧的过渡,GeLU 在所有值之间提供了*滑的过渡,使其在训练过程中更适合基于梯度的优化。

arxiv.org/pdf/1606.08415v3.pdf

输出

编码器的最后一个部分是 Output 类。为了计算它,我们已经拥有了所有需要的元素——它是线性投影、Dropout 和残差连接:

# linear projection
output_dense = layer_0.output.dense(layer_output_intermediate)
# dropout
output_drop = layer_0.output.dropout(output_dense)
# residual connection - NOTE these hidden_states are computed in 
# Intermediate 
output_res = output_drop + hidden_states # shape (b_size, 197, 768)

好吧,我们已经完成了第一个 ViT 层,还有其他 11 层要处理,这就是困难的部分……

开玩笑!我们实际上完成了——所有其他层与第一层完全相同,唯一的区别是,下一层的嵌入是之前计算的 output_res

因此,编码器经过 12 层后的输出是:

torch.manual_seed(0)
# masking heads in a given layer
layer_head_mask = None
# output attention probabilities
output_attentions = False

embeddings = model.vit.embeddings(image)
hidden_states = embeddings
for l in range(12):
    hidden_states = model.vit.encoder.layerl[0]

output = model.vit.layernorm(sequence_output)

Pooler

通常,在 Transformer 模型中,Pooler 是一个用于聚合在 transformer 编码器块之后的序列中的 token 嵌入信息的组件。它的作用是生成一个固定大小的表示,该表示捕获了全局上下文并总结了从图像块中提取的信息,在 ViT 的情况下尤为重要。Pooler 对于获得紧凑且具有上下文感知的图像表示至关重要,这可以用于各种下游任务,如图像分类。

在这种情况下,Pooler 非常简单——我们取[CLS]标记并将其用作图像的紧凑且上下文感知的表示。

pooled_output = output[:, 0, :] # shape (b_size, 768)

分类器

最后,我们准备使用pooled_output来对图像进行分类。分类器是一个简单的线性层,输出维度等于类别数:

logits = model.classifier(pooled_output) # shape (b_size, num_classes)

结论

ViT 彻底革新了计算机视觉,几乎在所有应用中取代了卷积神经网络,这就是为什么理解它的工作原理如此重要。不要忘记,ViT 的主要组件——变换器架构,源于自然语言处理,因此你应该查看我之前关于 BERT 变换器的文章 这里。希望你喜欢这篇文章,下次见!

[## 通过我的推荐链接加入 Medium - Alexey Kravets

作为 Medium 会员,你的会员费用的一部分将分配给你阅读的作者,你可以完全访问每个故事…

medium.com](https://medium.com/@alexml0123/membership?source=post_page-----1ce4cc05ca8d--------------------------------)

参考文献

[1] github.com/huggingface/transformers

[2] [2010.11929] 一张图胜过 16x16 个词:用于大规模图像识别的变换器(arxiv.org)

深入探讨统计期望的科学

原文:towardsdatascience.com/a-deep-dive-into-the-science-of-statistical-expectation-9dc0f80bd26

多佛白崖 (CC BY-SA 3.0)

我们如何形成对某件事的期望,这种期望的含义,以及产生这种含义的数学原理。

Sachin DateTowards Data Science Sachin Date

·发表于Towards Data Science ·29 分钟阅读·2023 年 6 月 17 日

--

这是 1988 年的夏天,我第一次踏上了船。这是一艘从英格兰多佛出发、驶往法国加来乘客渡轮。我当时并不知道,但我正赶上了渡轮穿越英吉利海峡的黄金时代的尾声。这正是在廉价航空公司和英吉利海峡隧道几乎终结我仍认为的最佳旅行方式之前。

我曾期望渡轮看起来像我在儿童书籍中看到的许多船只之一。然而,实际看到的是一座异常庞大、闪闪发光的白色摩天大楼,带有小小的方形窗户。而且,这座摩天大楼似乎由于某种令人困惑的原因横着放置。从码头的视角来看,我看不到船体和烟囱。我看到的只是它长而*坦、带窗户的外观。我看到的是一座横向的摩天大楼。

照片由Martin提供,Unsplash

回想起来,用统计学的语言来重新审视我的经历是颇为有趣的。我的大脑从我看到的船只图片的数据样本中计算出了期望的渡轮形状。但我的样本对于整体而言毫无代表性,这使得样本均值同样不代表总体均值。我是在用一个严重偏颇的样本均值来解码现实。

晕船

这次跨越英吉利海峡的旅行也是我第一次晕船。人们说,当你晕船时,应该走到甲板上,呼吸新鲜凉爽的海风,盯着地*线。对我来说唯一有效的办法是坐下来,闭上眼睛,喝着我最喜欢的汽水,直到我的思绪慢慢从搅动我胃部的痛苦恶心中脱离。顺便说一下,我并没有慢慢脱离本文的主题。我会很快进入统计学的内容。在此期间,让我解释一下你为什么会在船上生病的原因,以便你能看到与当前主题的联系。

在你大多数的生活日子里,你不会在船上摇晃。在陆地上,当你将身体倾斜到一侧时,你的内耳和身体的每一块肌肉都会告诉你的大脑你在倾斜。是的,你的肌肉也在和你的大脑沟通!你的眼睛热切地支持所有这些反馈,你也就安然无恙。然而在船上,眼睛和耳朵之间这段和谐的契约被打破了。

在船上,当海洋使船倾斜、摇晃、摆动、滚动、漂移、起伏或其他任何情况时,你的眼睛告诉大脑的东西可能会与肌肉和内耳告诉大脑的东西大相径庭。你的内耳可能会说:“小心!你在向左倾斜。你应该调整你对世界的预期。”但你的眼睛则说:“胡说!我坐着的桌子在我看来是完全水*的,桌子上的食物盘也是如此。墙上的那幅呐喊的画看起来也很直很水*。不要听内耳的。”

呐喊(公共领域)

你的眼睛可能向大脑报告更令人困惑的事情,比如“是的,你确实在倾斜。但这个倾斜的程度或速度并不像你那过于热心的内耳可能让你相信的那样严重。”

就好像你的眼睛和内耳各自要求你的大脑创建两个不同的预期,关于你的世界将如何改变。你的大脑显然做不到这一点。它感到困惑。而由于进化的原因,你的胃表达出强烈的欲望想要排空其内容。

让我们尝试通过统计推理的框架来解释这个令人痛苦的情况。这次,我们将使用一点数学来帮助解释。

你应该预期会晕船吗?深入统计学研究晕船问题

让我们定义一个 随机变量 X,它有两个取值:0 和 1。如果你眼睛的信号与内耳的信号一致,则X为 0。如果信号一致,则X为 1:

随机变量 X (作者提供的图片)

理论上,X 的每个值都应该具有一定的概率 P(X=x)。概率 P(X=0) 和 P(X=1) 共同构成了 X概率质量函数。我们如下表述:

X 的概率质量函数(图片来源于作者)

在绝大多数情况下,你的眼睛所接收到的信号会与内耳的信号一致。因此,p 几乎等于 1,(1 — p) 是一个非常非常小的数字。

让我们对 (1 — p) 的值做一个大胆的猜测。我们将使用以下推理来得出一个估计值:根据联合国的数据,2023 年出生时人类的*均预期寿命大约是 73 年。换算成秒,这大约是 2302128000 秒(约 23 亿)。假设一个普通人在其一生中经历 16 小时的晕船,即 28800 秒。现在,让我们不要对这 16 小时斤斤计较。这只是一个大胆的猜测,记住吗?所以,28800 秒给出了 (1 — p) 的一个工作估计值为 28000/2302128000 = 0.0000121626,而 p=(1 —0.0000121626) = 0.9999878374。因此,在一个普通人的一生中的任何一秒中,他们经历晕船的无条件概率仅为 0.0000121626。

根据这些概率,我们将进行一个持续 10 亿秒的模拟,模拟一个名叫 John Doe 的人的一生。这大约是 JD 模拟寿命的 50%。JD 更喜欢大部分时间待在坚实的地面上。他偶尔会进行海上巡游,并经常感到晕船。我们将模拟 JD 是否会在模拟的每一秒中经历晕船。为此,我们将进行 10 亿次伯努利随机变量的试验,其概率为 p 和 (1 — p)。每次试验的结果将是 1(如果 JD 晕船),或者 0(如果 JD 不晕船)。进行实验后,我们将得到 10 亿个结果。你也可以使用以下 Python 代码运行这个模拟:

import numpy as np

p = 0.9999878374
num_trials = 1000000000

outcomes = np.random.choice([0, 1], size=num_trials, p=[1 - p, p])

让我们计算结果为 1(即未晕船)和 0(即晕船)的次数:

num_outcomes_in_which_not_seasick = sum(outcomes)
num_outcomes_in_which_seasick = num_trials - num_outcomes_in_which_not_seasick

我们将打印这些计数。当我打印它们时,我得到以下值。你每次运行模拟时可能会得到稍有不同的结果:

num_outcomes_in_which_not_seasick= 999987794
num_outcomes_in_which_seasick= 12206

现在我们可以计算 JD 是否预计在这些 10 亿秒中的任何一秒中感到晕船。

期望值是两个可能结果的加权*均:即 1 和 0,加权是两个结果的频率。因此,让我们进行这个计算:

结果的期望值(图片来源于作者)

期望结果是 0.999987794,这实际上接* 1.0。数学告诉我们,在 JD 模拟的 10 亿秒中的任何随机选择的一秒钟内,JD 应该不会预期感到晕船。数据似乎几乎不允许这种情况发生。

现在让我们对上述公式稍作调整。我们将按如下方式重新排列它:

结果的期望值(图像由作者提供)

当以这种方式重新排列时,我们看到一个令人愉快的子结构逐渐显现出来。两个括号中的比率表示与两个结果相关的概率,具体来说是从我们 10 亿强数据样本中得出的样本概率,而不是总体概率。它们是样本概率,因为我们使用了来自我们 10 亿强数据样本的数据进行计算。话虽如此,0.999987794 和 0.000012206 这两个值应与总体值 p 和 (1 — p) 非常接*。

通过插入概率,我们可以将期望公式重新表述如下:

X 的期望值(图像由作者提供)

注意到我们使用了期望的符号,即 E()。由于X 是一个 Bernoulli(p) 随机变量,上述公式还告诉我们如何计算Bernoulli 随机变量的期望值X ~ Bernoulli(p) 的期望值就是 p。

关于样本均值、总体均值,以及一个让你听起来很酷的词

E(X) 也被称为总体均值,用 μ 表示,因为它使用了概率 p 和 (1 — p),这些是总体层面的概率值。这些是你如果能访问到全部数据总体时将观察到的‘真实’概率,但实际上几乎不可能做到。统计学家在提到这些和类似的测量时使用了“渐*”这个词。它们被称为渐*的,因为它们的意义仅在样本大小趋*于无穷大或整个总体的大小时才具有显著性。现在问题来了:我认为人们就是喜欢说‘渐*’。我也认为这是掩盖一个麻烦的真相,即你永远无法精确测量任何事物的值。

从积极的一面来看,无法接触到总体是统计科学领域的‘伟大*衡者’。无论你是新*毕业的学生还是诺贝尔经济学奖得主,这扇通往‘总体’的门对你始终紧闭。作为统计学家,你只能使用样本来工作,你必须默默忍受其缺陷。但情况实际上并没有听起来那么糟糕。想象一下,如果你能知道所有事物的确切值会发生什么。如果你可以接触到总体。如果你能够精确地计算均值、中位数和方差。如果你能够以精确的预测未来。那时就不再需要估计任何东西。统计学的许多分支将会消失。世界将需要减少成千上万的统计学家,更不用说数据科学家了。想象一下对失业、世界经济和世界和*的影响……

但我岔开话题了。我的观点是,如果X是伯努利分布(p),那么要计算 E(X),你不能使用实际的 p 和(1 — p)值。相反,你必须使用 p 和(1 — p)的估计值。这些估计值,你将使用一个适中规模的数据样本来计算,而不是整个总体——没有机会做到这一点。因此,我很遗憾地告诉你,你所能做的最好的是得到随机变量X期望值的估计。按照惯例,我们将 p 的估计值表示为 p_hat(带小帽的 p),将估计的期望值表示为 E_cap(X)。

X 的估计期望(图源作者)

由于 E_cap(X)使用样本概率,因此称为样本均值。它用 x̄或‘x bar’表示,就是在 x 上方加一条横杠。

总体均值样本均值是统计学中的蝙蝠侠和罗宾。

统计学的大部分内容都致力于计算样本均值,并将样本均值作为总体均值的估计值。

这就是它——用一句话概括了统计学的广阔领域。😉

深入期望的深渊

我们对伯努利随机变量的思维实验在某种程度上揭示了期望的本质。伯努利变量二元变量,它的操作非常简单。然而,我们经常使用的随机变量可能取多种不同的值。幸运的是,我们可以轻松地将期望的概念和公式扩展到多值随机变量。让我们通过另一个例子来说明。

多值离散随机变量的期望值

下表显示了 205 辆汽车的数据子集。具体来说,表中展示了每辆车引擎中的气缸数量。

汽车的气缸数(数据来源:UCI 机器学习数据集库,许可证:CC BY 4.0)(图片来自作者)

Y为一个随机变量,包含了从该数据集中随机选择的车辆的气缸数。我们知道数据集中包含气缸数为 2、3、4、5、6、8 或 12 的车辆。因此,Y的范围是集合 E=[2, 3, 4, 5, 6, 8, 12]。

我们将数据行按气缸数分组。下表显示了分组计数。最后一列表示每个计数的样本出现概率。该概率是通过将组大小除以 205 计算得出的:

气缸数的频率分布

使用样本概率,我们可以构建概率质量函数 P(Y) 来表示Y。如果我们将其与Y进行绘图,效果如下:

Y的 PMF(图片来自作者)

如果一辆随机选择的车辆在你面前驶过,你会期望它的气缸数是多少?仅通过查看 PMF,你会想猜测 4 个气缸。然而,这个猜测背后有严谨的数学支持。类似于伯努利X,你可以按如下方式计算Y的期望值:

Y的期望值(图片来自作者)

如果你计算这个和,它的结果是 4.38049,这与你猜测的 4 个气缸非常接*。

由于Y的范围是集合E=[2,3,4,5,6,8,12],我们可以将此和式表示为对 E 的求和,如下所示:

离散随机变量Y的期望值公式(图片来自作者)

你可以使用上述公式来计算任何离散随机变量的期望值,其范围是集合E

连续随机变量的期望值

如果你处理的是连续随机变量,情况会有所不同,如下所述。

让我们回到我们的车辆数据集。具体来说,让我们查看车辆的长度:

汽车长度(数据来源:UCI 机器学习数据集库,许可证:CC BY 4.0)(图片来自作者)

假设Z表示随机选取的车辆的长度(单位:英寸)。Z的范围不再是离散的值集合,而是实数集合的一个子集。由于长度总是正数,因此它是所有正实数的集合,记作>0。

由于所有正实数的集合具有(不可数)无限多个值,因此将概率分配给Z的某个特定值是没有意义的。如果你不相信我,可以考虑一个简单的思想实验:想象一下给Z的每一个可能值分配一个正概率。你会发现这些概率的和会趋向于无穷大,这显然是不合理的。因此,概率 P(Z=z)根本不存在。相反,你必须使用概率密度函数 f(Z=z),它为不同的Z值分配一个概率密度

我们之前讨论了如何使用概率质量函数计算离散随机变量的期望值。

离散随机变量Y期望值的公式(图片由作者提供)

我们可以将这个公式用于连续随机变量吗?答案是可以的。要了解如何进行,想象一下你拿着一台电子显微镜。

拿起那台显微镜,聚焦在Z的范围内,即所有正实数的集合(>0)。现在,放大到一个极其微小的区间(z, z+δz]。在这个微观尺度下,你可能会观察到,从实际角度来看(现在,不是一个有用的术语),概率密度 f(Z=z) 在 δz 上是常量。因此,f(Z=z)和 δz 的乘积可以*似为随机选择的车辆长度落在开闭区间(z, z+δz]内的概率

拥有这个*似概率后,你可以将Z的期望值*似如下:

Z是连续的时,E(Z)的*似评估(图片由作者提供)

注意我们是如何从 E(Y)的公式跳跃到这个*似公式的。要从 E(Y)得到 E(Z),我们做了以下工作:

  • 我们将离散的 y_i 替换为实值的 z_i。

  • 我们将Y的 PMF P(Y=y)替换为Z的*似概率 f(Z=z)δz,它表示在微观区间(z, z+δz]中找到 z 的概率。

  • 我们不再对E的离散有限范围Y求和,而是对>0 的连续无限范围Z求和。

  • 最后,我们将等号替换为*似符号。罪行就在这里。我们作弊了。我们偷偷使用了概率 f(Z=z)δz,它是对精确概率 P(Z=z) 的*似。我们作弊的原因是,精确概率 P(Z=z) 对于连续Z而言是不存在的。我们必须为这个罪过做出补偿,这正是我们接下来要做的。

我们现在进行我们的绝技,我们的拿手好戏,并在这样做中救赎自己。

由于 >0 是正实数的集合,在 >0 中有无限多个大小为 δz 的微小区间。因此,对 >0 的求和是对无限多个项的求和。这一事实为我们提供了一个绝佳的机会,可以用精确积分来替代*似求和,如下所示:

Z 的期望值(作者提供的图片)

一般来说,如果 Z 的范围是实数区间 [a, b],我们将定积分的上下限设置为 a 和 b,而不是 0 和 ∞。

如果你知道 Z 的概率密度函数(PDF),并且在 [a, b] 区间内 z 乘以 f(Z=z) 的积分存在,你将解出上述积分,从而得到 E(Z)。

如果 Z 在区间 [a, b] 上均匀分布,其 PDF 如下:

PDF of Z ~ Uniform(a, b)(作者提供的图片)

如果你设置 a=1 和 b=5,

f(Z=z) = 1/(5–1) = 0.25。

概率密度在 Z=1 到 Z=5 的区间内为常数 0.25,而其他地方为零。Z 的 PDF 如下所示:

PDF of Z ~ Uniform(1, 5)(作者提供的图片)

基本上,它是从 (1,0.25) 到 (5,0.25) 的一条连续*坦的水*线,其他地方的值为零。

一般来说,如果 Z 的概率密度在区间 [a, b] 上是均匀分布的,Z 的 PDF 在 [a, b] 上是 1/(b-a),其他地方为零。你可以使用以下过程计算 E(Z):

计算均匀分布在区间 [a, b] 上的连续随机变量的期望值的过程(作者提供的图片)

如果 a=1 且 b=5,Z ~ Uniform(1, 5) 的均值就是 (1+5)/2 = 3。这与我们的直觉一致。如果在 1 和 5 之间的每一个无限多的值都是同样可能的,我们会期望均值等于 1 和 5 的简单*均值。

现在我不想打击你的积极性,但实际上,你更可能在前院看到双彩虹,而不是遇到需要用积分法来计算其期望值的连续随机变量。

双彩虹 (CC BY-SA 2.0)

你会发现,看似精美的概率密度函数(PDF)通常会被嵌入到大学教科书的章节末尾练习中。它们就像家猫一样,不“出门”。但作为一个实践中的统计学家,“外面”就是你生活的地方。在外面,你会发现自己面对着连续值的数据样本,比如车辆的长度。为了对这些真实世界的随机变量进行建模,你很可能会使用一些著名的连续函数,例如正态分布、对数正态分布、卡方分布、指数分布、威布尔分布等等,或者混合分布,即最适合你数据的模型。

这里有几个这样的分布:

正态分布和卡方分布的连续随机变量的 PDF 和期望值(图片由作者提供)

对于许多常用的 PDF,已经有人花费心力通过积分(x 乘以 f(x))来推导分布的均值,就像我们对均匀分布所做的那样。这里有几个这样的分布:

指数分布和伽马分布的连续随机变量的 PDF 和期望值

最后,在一些情况下,实际上是许多情况下,现实生活中的数据集表现出过于复杂的模式,无法用任何一个分布来建模。这就像你感染了一种病毒,带来了一堆症状。为了帮助你克服这些症状,你的医生会给你开一系列药物,每种药物的强度、剂量和作用机制都不同。当你面对的数据展现出许多复杂的模式时,你必须动用一小支概率分布的“军队”来进行建模。这种不同分布的组合被称为混合分布。一种常用的混合分布是强大的高斯混合模型,它是多个正态分布随机变量的几个概率密度函数的加权和,每个随机变量具有不同的均值和方差组合。

给定一个真实值数据的样本,你可能会发现自己在做一些非常简单的事情:你将计算连续值数据列的*均值,并将其称为样本均值。例如,如果你计算汽车数据集中汽车的*均长度,它会是 174.04927 英寸,仅此而已。就这么完成了。但是,这还不是全部,你还有一个问题需要回答。

你的样本均值有多准确?感受它的准确性

你如何知道样本均值对总体均值的估计有多准确?在收集数据时,你可能运气不好,或者懒惰,或者‘数据受限’(这通常是懒惰的一个极好的委婉说法)。无论哪种方式,你都在面对一个非随机的样本。它没有按比例代表总体的不同特征。以汽车数据集为例:你可能收集了大量中型车的数据,而大型车的数据则太少。而且伸缩豪华轿车可能完全没有出现在你的样本中。结果,你计算的*均长度将严重偏向于总体中仅中型车的*均长度。无论你是否喜欢,你现在都在以几乎每个人都开中型车为信念进行工作。

对自己要诚实

如果你收集了一个严重偏倚的样本,而你不知道或者不在意,那么愿上天保佑你在你选择的职业道路上。然而,如果你愿意考虑偏倚的可能性,并且你有一些线索关于你可能遗漏了哪些数据(例如跑车),那么统计学将通过强有力的机制来帮助你估计这种偏倚

不幸的是,无论你多么努力,你永远也无法收集到一个完美*衡的样本。它总是包含偏倚,因为总体中各种元素的确切比例对你来说永远无法访问。记住那扇通向总体的门吗?记得门上的标志上总是写着‘CLOSED’吗?

你最有效的行动方案是收集一个大致包含总体中所有事物的相同比例的样本——即所谓的良好*衡样本。这个良好*衡样本的均值是你可以出发的最佳样本均值。

但是自然法则并不总是让统计学家的风帆黯然失色。自然界有一个宏伟的特性,体现在一个被称为中心极限定理(CLT)的定理中。你可以使用 CLT 来确定你的样本均值估计总体均值的效果如何。

CLT 并不是应对严重偏倚样本的灵丹妙药。如果你的样本主要由中型车组成,你实际上已经重新定义了你对总体的概念。如果你是有意只研究中型车,那么你就免责了。在这种情况下,尽管使用 CLT。它将帮助你估计你的样本均值与中型车的总体均值有多接*。

另一方面,如果你的存在目的在于研究所有生产的车辆,但你的样本主要是中型车,那么你遇到了问题。对于统计学的学生,让我用稍微不同的词重述一下。如果你的大学论文是关于宠物打哈欠的频率,但你的样本是 20 只猫和你邻居的贵宾犬,那么无论中心极限定理是否适用,再多的统计技巧也无法帮助你评估样本均值的准确性。

中心极限定理的要点

对于中心极限定理(CLT)的全面理解是另一个话题,但其要点如下:

如果你从总体中随机抽取数据点,并计算样本均值,然后重复这个过程多次,你将得到……许多不同的样本均值。嗯,显然如此!但接下来会发生一些令人惊讶的事情。如果你绘制所有这些样本均值的频率分布,你会发现它们 总是 正态分布的。更重要的是,这种正态分布的均值总是你正在研究的总体均值。这种我们宇宙个性中诡异而迷人的方面正是中心极限定理用(还有什么?)数学语言描述的。

标记为 174.04927 英寸的样本均值长度在一个假设总体均值为 180 英寸的正态分布 Z 上(图由作者提供)

让我们来看看如何使用中心极限定理。我们将按以下步骤进行:

使用仅从一个样本得出的样本均值 Z_bar,我们将说总体均值 μ 落在区间 [μ_low, μ_high] 内的概率是 (1 — α):

总体均值的下限和上限置信区间(图由作者提供)

你可以将 α 设置为 0 到 1 之间的任何值。例如,如果你将 α 设置为 0.05,你将得到 (1 — α) 为 0.95,即 95%。

要使这种概率 (1 — α) 成立,应该按如下方式计算下界 μ_low 和上界 μ_high:

总体均值的下限和上限(图由作者提供)

在上述公式中,我们知道 Z_bar、α、μ_low 和 μ_high。其余的符号需要一些解释。

变量 s 是数据 样本 的标准差。

N 是样本量。

现在我们来讨论 z_α/2。

z_α/2 是你在标准正态分布的概率密度函数(PDF)的 X 轴上读出的值。标准正态分布是一个均值为零,标准差为一的正态分布连续随机变量的 PDF。z_α/2 是该分布 X 轴上使得 PDF 曲线左侧面积为 (1 — α/2) 的值。这一区域当你设置 α 为 0.05 时的样子如下:

在 X 轴上某个值 X 左侧的 PDF 区域。在这种情况下,x=1.96(图由作者提供)

蓝色区域计算为 (1 — 0.05/2) = 0.975。请记住,任何 PDF 曲线下的总面积始终为 1.0。

总结一下,一旦你从一个样本中计算出均值 (Z_bar),你可以围绕这个均值建立界限,使得总体均值落在这些界限内的概率是你选择的值。

让我们重新检查估计这些界限的公式:

总体均值的下界和上界(图像来源于作者)

这些公式给我们一些关于样本均值性质的见解:

  1. 随着样本方差 s 的增加,下界 (μ_low) 的值会降低,而上界 (μ_high) 的值会增加。这会有效地使 μ_low 和 μ_high 彼此远离,并远离样本均值。相反,随着样本方差的减少,μ_low 从下方更接*Z_bar,μ_high 从上方更接*Z_bar。区间界限本质上从两侧趋向于样本均值。实际上,区间 [μ_low, μ_high] 与样本方差成正比。如果样本在均值周围分布得很广泛(或紧密),则较大的(或较小的)分散会降低(或增加)样本均值作为总体均值估计的可靠性。

  2. 注意,区间的宽度与样本大小 (N) 成反比。在两个方差相似的样本之间,较大的样本会产生围绕其均值的更紧密区间,而较小的样本则不会。

让我们看看如何计算汽车数据集的这个区间。我们将计算 [μ_low, μ_high],以便有 95% 的概率使总体均值 μ 落在这些范围内。

为了获得 95% 的概率,我们应该将 α 设置为 0.05,这样 (1 — α) = 0.95。

我们知道 Z_bar 为 174.04927 英寸。

N 为 205 辆车辆。

样本标准差可以很容易地计算出来。它为 12.33729 英寸。

接下来,我们将处理 z_α/2。由于 α 为 0.05,α/2 为 0.025。我们需要找到 z_α/2 的值,即 z_0.025。这是在标准正态随机变量的 PDF 曲线的 X 轴上的值,其中曲线下的区域是 (1 — α/2) = (1 — 0.025) = 0.975。通过查阅 标准正态分布表,我们发现这个值对应于X=1.96 的左侧区域。

包含标准正态分布 CDF 值的表格。包含不同 X 值的 P(X ≤ x)(来源:维基百科

插入这些值,我们得到以下界限:

μ_low = Z_bar — ( z_α/2 · s/√N) = 174.04927 — (1.96 · 12.33729/205) = 173.93131

μ_high = Z_bar + ( z_α/2 · s/√N) = 174.04927 + (1.96 · 12.33729/205) = 174.16723

因此,[μ_low, μ_high] = [173.93131 英寸, 174.16723 英寸]

有 95%的概率人口均值位于这个区间内。看看这个区间有多紧凑。它的宽度仅为 0.23592 英寸。在这个狭小的间隙中包含了样本均值 174.04927 英寸。尽管样本中可能存在各种偏差,我们的分析表明,样本均值 174.04927 英寸是对未知人口均值的极其良好的估计。

超越第一维度:多维样本空间中的期望

目前,我们关于期望的讨论仅限于一维,但它不一定非得如此。我们可以轻松地将期望的概念扩展到二维、三维或更高维度。要计算多维空间中的期望,我们只需要一个定义在 N 维空间上的联合概率质量函数(或密度函数)。联合 PMF 或 PDF 以多个随机变量作为参数,返回这些值同时出现的概率。

在文章前面,我们定义了一个随机变量Y,表示从汽车数据集中随机选择的车辆中的气缸数量。Y是你的典型一维离散随机变量,它的期望值由以下公式给出:

单维离散随机变量的期望值(图像由作者提供)

让我们引入一个新的离散随机变量XXY联合概率质量函数用 P(X=x_i, Y=y_j)表示,或简写为 P(X, Y)。这个联合 PMF 将我们从Y所处的舒适一维空间中带出,带入到一个更有趣的二维空间。在这个二维空间中,一个数据点或结果由元组(x_i, y_i)表示。如果X的范围包含‘p’个结果,Y的范围包含‘q’个结果,则二维空间将具有(p x q)个联合结果。我们用元组(x_i, y_i)来表示这些联合结果中的每一个。要计算这个二维空间中的 E(Y),我们必须将 E(Y )的公式适应如下:

离散随机变量Y在二维空间中的期望值(图像由作者提供)

请注意,我们正在对二维空间中所有可能的元组(x_i, y_i)进行求和。让我们将这个求和拆解成嵌套求和如下:

离散随机变量Y在二维空间中的期望值(图像由作者提供)

在嵌套求和中,内层求和计算 y_j 和 P(X=x_i, Y=y_j)在所有 y_j 值上的乘积。然后,外层求和对每个 x_i 值重复内层求和。之后,它将所有这些单独的和收集起来并加总,以计算 E(Y )。

我们可以通过将求和嵌套在彼此之间,将上述公式扩展到任意数量的维度。你需要的只是一个在 N 维空间上定义的联合 PMF。例如,以下是如何将公式扩展到 4 维空间:

离散随机变量Y在 4 维空间上的期望值(图片由作者提供)

注意我们总是将Y的求和放在最深层次。你可以按任何顺序安排其余的求和——你将得到相同的 E(Y)结果。

你可能会问,为什么要定义一个联合 PMF 并为所有这些嵌套求和而发狂?在 N 维空间上计算的 E(Y)是什么意思?

理解多维空间中期望值含义的最佳方式是用实际的多维数据来说明其使用。

我们将使用的数据来自一艘特定的船,它与我渡过英吉利海峡的船不同,不幸的是没能到达另一边。

RMS 泰坦尼克号于 1912 年 4 月 10 日从南安普顿出发(公有领域)

下图展示了 887 名乘客在 RMS 泰坦尼克号上的数据集中的一些行:

泰坦尼克号数据集 (CC0)

Pclass列表示乘客的舱位级别,整数值为 1、2 或 3。Siblings/Spouses AboardParents/Children Aboard变量是二元(0/1)变量,表示乘客是否有兄弟姐妹、配偶、父母或子女在船上。在统计学中,我们常常有些残酷地称这些二元指示变量虚拟变量。它们并没有什么愚蠢的地方以至于配得上这样的贬义称呼。

从表中可以看出,有 8 个变量共同标识数据集中的每个乘客。每一个这 8 个变量都是一个随机变量。我们面临的任务有三方面:

  1. 我们希望在这些随机变量的一个子集上定义一个联合概率质量函数,并且,

  2. 使用这个联合 PMF,我们希望说明如何在这个多维 PMF 上计算这些变量的期望值,并且,

  3. 我们希望理解如何解读这个期望值。

为了简化问题,我们将Age变量分成 5 年为一个区间,并将这些区间标记为 5、10、15、20、…、80。例如,20 岁区间意味着乘客的实际年龄在(15,20]年区间内。我们将这个分箱后的随机变量称为Age_Range

一旦Age被分箱,我们将按PclassAge_Range对数据进行分组。以下是分组计数:

按乘客的舱位和(分组的)年龄的频率分布(作者提供的图像)

上表包含了每个群体(组)的泰坦尼克号乘客数量,这些群体是由PclassAge_Range的特征定义的。顺便提一下,群体也是统计学家非常崇拜的另一个词(以及渐进)。这里有个小提示:每当你想说“组”时,直接说“群体”。我向你保证,无论你本来打算说什么,瞬间都会显得十倍重要。例如:“八个不同的群体的酒精爱好者(请原谅,酒类鉴赏家)喝了假酒,他们的反应被记录下来。”明白了吗?

说实话,“群体”确实有“组”没有的精准含义。尽管如此,偶尔说一遍“群体”,观察听众脸上的尊敬感是有启发性的。

无论如何,我们将向频率表中添加另一列。这一新列将保存观察到特定PclassAge_Range组合的概率。这个概率 P(Pclass, Age_Range)是频率(即Name列中的数量)与数据集中乘客总数(即 887)的比率。

按乘客的舱位和(分组的)年龄的频率分布(作者提供的图像)

概率 P(Pclass, Age_Range)是随机变量PclassAge_Range联合概率质量函数。它给出了观察到被特定PclassAge_Range组合描述的乘客的概率。例如,查看Pclass为 3 且Age_Range为 25 的行。相应的联合概率为 0.116122。这个数字告诉我们,大约 12%的泰坦尼克号 3 等舱乘客年龄在 20 到 25 岁之间。

与一维 PMF 类似,当对其所有组成随机变量的值组合进行评估时,联合 PMF 也会加和为完美的 1.0。如果你的联合 PMF 没有加和为 1.0,你应该仔细检查一下定义。可能在公式中存在错误,或者更糟糕的是实验设计中有问题。

在上面的数据集中,联合 PMF 确实加和为 1.0。相信我吧!

要直观感受联合 PMF P(Pclass, Age_Range),你可以在 3 维中绘制它。在 3-D 图中,将 X 和 Y 轴分别设置为PclassAge_Range,将 Z 轴设置为概率 P(Pclass, Age_Range)。你会看到一个引人入胜的 3-D 图表。

PclassAge_Range的联合 PMF 的 3-D 图(作者提供的图像)

如果你仔细观察,你会注意到联合 PMF 包含三个*行的图,分别对应于泰坦尼克号上的每一个船舱等级。三维图展示了不幸海轮上的一些人口统计数据。例如,在所有三个舱等级中,15 到 40 岁的乘客占据了大部分。

现在让我们计算在这个二维空间上的 E(Age_Range)。E(Age_Range)由下式给出:

Age_Range的期望值(图片来源:作者)

我们对所有Age_Range的值进行内部求和:5,10,15,…,80。我们对所有Pclass的值进行外部求和:[1, 2, 3]。对于每一个(Pclass, Age_Range)的组合,我们从表中选择联合概率。Age_Range的期望值是 31.48252537 岁,这对应于 35 的分箱值。我们可以预期,泰坦尼克号上的“*均”乘客年龄在 30 到 35 岁之间。

如果你取泰坦尼克号数据集中Age_Range列的*均值,你将得到完全相同的数值:31.48252537 岁。那么为什么不直接取Age_Range列的*均值来得到 E(Age_Range)? 为什么要构建一个嵌套求和的鲁布·戈德堡机器来计算同样的值呢?

鲁布·戈德堡的“自动操作餐巾纸”机器(公共领域)

这是因为在某些情况下,你所拥有的只是联合 PMF 和随机变量的范围。在这种情况下,如果你只有 P(Pclass, Age_Range)且知道Pclass的范围是[1,2,3],以及 Age_Range 的范围是[5,10,15,20,…,80],你仍然可以使用嵌套求和技术来计算 E(Pclass) E(Age_Range)。

如果随机变量是连续的,那么可以使用多重积分在多维空间中找到期望值。例如,如果XYZ是连续随机变量,并且 f(X,Y,Z)是定义在三维连续空间(x, y, z)的联合概率密度函数,则Y在该三维空间中的期望值如图所示:

连续随机变量Y在连续三维空间中的期望值(图片来源:作者)

就像在离散情况下,你首先对你想计算其期望值的变量进行积分,然后对其他变量进行积分。

一个著名的例子展示了用于计算期望值的多重积分方法,其规模小到人眼无法感知。我指的是量子力学中的波函数。波函数在笛卡尔坐标中表示为Ψ(x, y, z, t),在极坐标中表示为Ψ(r, θ, ɸ, t)。它用于描述那些喜欢待在极其狭小空间里的微小物体的性质,例如原子中的电子。波函数Ψ返回一个形式为 A + jB 的复数,其中 A 代表实部,B 代表虚部。我们可以将Ψ的绝对值*方解释为定义在四维空间(x, y, z, t)或(r, θ, ɸ, t)上的联合概率密度函数。特别是对于氢原子中的电子,我们可以将|Ψ|²解释为在时间 t 时电子在(x, y, z)或(r, θ, ɸ)周围一个极其微小的空间体积中的大致概率。通过知道|Ψ|²,我们可以在 x, y, z 和 t 上进行四重积分,以计算电子在时间 t 沿 X、Y 或 Z 轴(或其极坐标等效轴)的期望位置

结束语

我以自己对晕船的经历开始这篇文章。如果你对用伯努利随机变量来建模这一非常复杂且尚未完全理解的人类困境感到不满,我也不会怪你。我的目的是说明期望如何从生物学层面实际影响我们。一种解释这一困境的方法是使用随机变量的酷炫且舒缓的语言。

从看似简单的伯努利变量开始,我们将我们的插图画笔从统计画布扫到量子波函数的宏伟多维复杂性。在整个过程中,我们力求理解期望如何在离散和连续尺度、单维和多维、以及微观尺度上运作。

还有一个领域,期望发挥了巨大的影响。这个领域是条件概率,其中计算随机变量X取值‘x’的概率,假设某些其他随机变量ABC等已经取值‘a’、‘b’、‘c’。XABC的条件下的概率表示为 P(X=x|A=a,B=b,C=c),或简写为 P(X|ABC)。在我们见过的所有期望公式中,如果将概率(或概率密度)替换为同一条件版本,得到的就是条件期望的相应公式。它表示为 E(X=x|A=a,B=b,C=c),它位于回归分析和估计的广泛领域的核心。这是未来文章的素材!

引用和版权

数据集

汽车数据集下载自加州大学欧文分校机器学习库,根据知识共享署名 4.0 国际(CC BY 4.0)许可协议使用。

泰坦尼克号数据集下载自Kaggle,根据CC0 许可使用。

图片

本文中的所有图片版权归Sachin Date所有,采用CC-BY-NC-SA许可,除非图片下方另有说明。

如果你喜欢这篇文章,请关注我,访问 Sachin Date 以获取关于回归、时间序列分析和预测主题的提示、操作指南和编程建议。

归纳偏差的一个童话故事

原文:towardsdatascience.com/a-fairy-tale-of-the-inductive-bias-d418fc61726c

|归纳偏差| 变换器| 计算机视觉|

我们需要归纳偏差吗?简单模型如何达到复杂模型的性能

Salvatore RaieliTowards Data Science Salvatore Raieli

·发布于 Towards Data Science ·18 分钟阅读·2023 年 7 月 10 日

--

图片由 Natalia Y. 提供,Unsplash

正如我们*年来所见,深度学习在使用量和模型数量上都经历了指数级增长。这一成功的铺路石或许就是 迁移学习 本身——即一个模型可以通过大量数据进行训练,然后用于各种具体任务的理念。

*年来,出现了一种范式:变换器(或基于此模型的其他变体)被用于 NLP 应用。而在图像领域,则使用 视觉变换器卷积网络

## LLMs 的无限巴别图书馆

开源、数据与注意力:LLMs 的未来将如何改变

towardsdatascience.com ## META 的 Hiera:减少复杂性以提高准确性

简单性使 AI 能够达到惊人的性能和令人惊讶的速度

towardsdatascience.com

另一方面,虽然我们有大量实践工作证明这些模型效果良好,但理论上的理解却滞后。这是因为这些模型非常广泛,实验起来很困难。视觉变换器的表现优于卷积神经网络,因为它们在视觉上具有理论上更少的归纳偏差,这表明存在一个需要填补的理论空白。

本文重点讨论:

  • 归纳偏差究竟是什么?为什么这很重要,我们最喜欢的模型有什么归纳偏差?

  • 变换器和 CNN 的归纳偏差。这两种模型之间有什么区别,为什么这些讨论很重要?

  • 我们如何研究归纳偏差?如何利用不同模型之间的相似性来捕捉它们的差异。

  • 具有弱归纳偏差的模型能否在计算机视觉领域取得成功?这是一个传统上被认为归纳偏差很重要的领域。

什么是归纳偏差?

照片由Raphael Schaller提供,拍摄于Unsplash

学习是通过观察和与世界互动来获取有用知识的过程。它涉及在解决方案空间中搜索,以找到一个能够更好地解释数据或获得更高奖励的解决方案。但在许多情况下,存在多个同样好的解决方案。 (source)

想象一下在湖中遇到一只天鹅。从这只简单的天鹅,我们可能会假设所有的天鹅都是白色的(直到我们看到一只黑天鹅),它们是水禽,它们以鱼为食,等等。

这个过程被称为归纳推理。从一个简单的观察中,我们可能能够推导出成千上万(甚至数十亿)个假设,显然,所有的假设并不都是真实的。实际上,我们可能会认为天鹅无法飞行,因为我们当时只观察到它在游泳。

显然,没有直接观察很难决定哪个假设是正确的。所以根据奥卡姆剃刀原则,我们可以说“天鹅可以在湖中游泳”。

为什么这对机器学习很重要?

数据集是观察结果的集合,我们想要创建一个可以从这些观察结果中泛化的模型。其理念是,从我们的数据集中,我们可以推断出一些对总体人群也适用的规则。换句话说,我们可以将我们的模型视为一组假设。

理论上,假设空间是无限的。实际上,如果我们考虑笛卡尔空间中的两个点,它可以通过一条直线但无限多的曲线。在没有更多点的情况下,我们无法知道哪种假设是最正确的。

通常,最简单的假设是最正确的。一个完美拟合点的曲线通常是过拟合

图片来自这里

归纳偏差可以定义为对某些假设的优先考虑(从而减少假设空间)。例如,当我们面对一个回归任务时,我们决定考虑线性模型,这时我们通过使假设仅限于线性模型来减少我们的假设空间。

线性回归。图片来源:这里

归纳偏差使得学习算法可以在观察到的数据之外优先考虑一种解决方案(或解释)而非另一种,来源

一方面,我们有不同类型的数据,以及具有不同假设和不同归纳偏差的不同类型模型(即,假设空间的不同简化)。因此,人们可能会倾向于为所有类型的数据选择一个模型。

然而在 1997 年,无免费午餐定理结束了这种诱惑。没有一个模型可以适用于所有情况。实际上,没有一种最优的偏差可以使模型对所有任务进行泛化。换句话说,一个任务的最优假设可能对另一个任务并不最优。

这就是我们为什么对图像使用卷积神经网络,对文本序列使用 RNN(或 LSTM)等的原因。

为了更好地理解,以下是一些归纳偏差的例子:

  • 决策树基于这样的假设:一个任务可以通过一系列的二元决策(二元拆分)来解决。

  • 正则化的假设是指向参数具有小值的解决方案。

  • 全连接层是一种全对全的偏差,其中层 i 的所有单元都与下一层 j 相连(一个层中的所有神经元都与下一层相连)。这意味着存在一种非常弱的关系偏差,因为任何单元都可以与其他单元进行交互。

  • 卷积神经网络基于局部性的理念,即特征通过局部像素提取,并以层次化模式组合。在另一种世界观中,我们假设相邻的像素实际上是相关的,这种关系应当被模型考虑(在卷积步骤中)。

  • 递归神经网络具有与序列性相关的偏差,因为每个词是按顺序处理的。由于权重在序列的所有元素中被重用(我们更新隐藏状态),因此还存在时间等变性(或递归性)。

  • 变换器 它没有强的归纳偏差,这应当提供更多的灵活性(但需要大量的训练数据)。实际上,在数据较少的情况下,该模型的表现往往不如其他模型。

图片来源: 这里

CNN 和变换器的归纳偏差

图片由Tudose Alexandru拍摄,发布于Unsplash

卷积神经网络长期以来主导了计算机视觉领域,直到视觉变换器的出现。正如我们前面提到的,CNN 基于相邻像素之间存在关系的原理。因此,在卷积过程中,几个像素共享相同的权重。

此外,池化层的使用旨在实现*移不变性。这意味着,无论模式出现在图像的何处(例如,图像的左角或右角),它都会被识别。

这些偏差对于处理自然图像数据非常有效,因为局部邻域内具有较高的协方差,而随着距离的增加,这种协方差会减小,并且统计特性在整张图像上大致是稳定的。(来源

这些偏差实际上受到了下颞皮层的启发,该区域似乎提供了对应的生物学基础,用于尺度、*移和旋转不变性。这些偏差被认为对 CNN 在面对图像*移、缩放或其他变形时的鲁棒性很重要,因此通过卷积和池化加以应用。

图片来源:这里

另一方面,图像是复杂且信息丰富的对象。鉴于其用途,尝试更详细地理解CNN所看到的内容以及存在的其他偏差。

在 2017 年的一项研究中,作者展示了Inception 模型(一种 CNN)的“形状偏差”很强。换句话说,CNN 在识别对象时更依赖于对象的形状而非其他类型的模式。作者使用了一个图像三联体来分类一个对象,并使用了颜色相同但形状不同(颜色匹配)或形状相同但颜色不同(形状匹配)的图像来研究模式是否更注重形状或颜色。

图片来源:这里

在一项后续研究中,一些作者则展示了,模型更关注的是纹理而非颜色。作者使用了 ResNet50 来测试这一假设。

图片来源:这里

他们展示了在纹理与形状冲突的情况下,模型倾向于使用纹理。因此,对作者来说,CNN 具有强烈的“纹理偏差”。

图片来源:这里

然而,作者总结道,具有形状偏差的模型更具鲁棒性:

值得注意的是,具有较高形状偏差的网络在对许多不同图像扭曲的鲁棒性上天生更强(对于一些甚至达到了或超过了人类表现,尽管从未接受过这些扭曲的训练),并在分类和物体识别任务中表现更好。 (这里)

实际上,对于图像来说,具有形状偏差是理想的。这可以通过使用适当的数据集或使用数据增强技术来实现,这些技术包括颜色失真、噪声和模糊(这些恰好减少了纹理偏差)。相反,随机裁剪会增加纹理偏差。

图片来源:这里

具体来说,我们可以说偏差不仅依赖于卷积神经网络(CNN)的架构,还依赖于训练时使用的数据集。根据数据集的不同,CNN 会倾向于形状或纹理。

一项研究的作者 表示,这些偏差是互补的。模型可以专注于纹理或形状进行预测。然而,有时,仅这两种元素中的一种不足以进行正确预测(降低了性能)。作者表示,由于模型可以学习任一偏差,它还可以“自动找出如何避免对形状或纹理有偏见。”换句话说,使用具有冲突的(纹理和形状)示例可以指导模型避免偏见。

图像来源:这里

视觉变换器 来源于变换器,正如前面提到的,它是一个没有强偏差的模型。

一些研究表明,CNNs 和 ViTs 之间仍有几个相似之处。实际上,ViTs 也学习了层级视图,并且可以被可视化。

图像:来源

[## 视觉变换器所见的视觉之旅

一些最大的模型如何看待世界

pub.towardsai.net

然而,后来的研究表明,ViTs 实际上具有比 CNNs 更高的形状偏差。这实际上令人惊讶。此外,作者指出这种形状偏差在图像损坏的鲁棒性方面发挥了积极作用:

[…] 强调了形状偏差与均值损坏误差之间的一般性反向关系。模型对常见损坏的鲁棒性越高(即更小的 mCE),其形状偏差就越大。(来源)

视觉变换器的形状偏差。图像来源:这里

几个研究小组假设,通过添加适当的归纳偏差,可能使 ViTs 即使不使用数百万张图像进行训练也能超越 CNNs。另一方面,这一假设导致了大量模型的创建但使训练极其低效。

所以问题仍然存在:

参数和训练样本数量的扩展能在多大程度上弥补缺乏归纳偏差的问题?

如何研究归纳偏差?

照片由Aaron Burden拍摄,来自Unsplash

正如我们所见,仍然存在几个未解的问题。尽管有大量关于CNNsViTs的研究,但这些性能提升背后的许多理论背景仍然不清楚。

“MLP 是这些神经网络架构中最简单的一种,依赖于这种堆叠思想,因此提供了一个有效深度学习理论的最简模型。” (source)

通常,许多关于更理论方面的研究都是使用多层感知器(MLP)进行的。这是因为它是由简单的矩阵乘法组成的层,封装在一个非线性函数中。其简单性允许在较低的计算成本下进行许多实验。然后对更简单模型进行的研究被转化为更复杂和精细的模型。然而,MLP 在许多情况下性能较差,这留给我们的是,如何将观察到的内容转移到具有远超性能的模型中。

另一方面,MLP 还有一个优势,即其具有较弱的归纳偏差。这使得它成为 ViT 研究的一个良好候选模型。还有一个衍生模型,归纳偏差更小:MLP-Mixer

图片来源:这里

有趣的是,MLP-Mixer 既不使用卷积也不使用自注意力。相反,它依赖于多层感知器层,这些层应用于空间位置或特征通道。这一切都得益于矩阵乘法和非线性的巧妙使用。

简而言之,图像块被线性投影到嵌入空间(然后转换为可以被 MLP 利用的表格数据)。之后,我们有一系列混合层。输入数据进入并转置,然后我们有一个简单的全连接层。这个层识别在图像块中常见的特征(聚合通道)。然后结果被转置,并通过第二个全连接层来识别图像块本身的特征(与通道关联)。

此外,还有跳跃连接、作为非线性函数的GELU层归一化。另外,作者评论道:

我们的架构可以看作是一个独特的 CNN,它使用(1×1)卷积进行通道混合,使用单通道深度卷积进行令牌混合。然而,反之则不然,因为 CNN 不是 Mixer 的特例。(来源

图片来源:这里

另一个有趣的关系是,卷积可以看作是 MLP 的特例,其中 W 权重矩阵是稀疏的并具有共享的条目。权重的这种共享确实导致学习在空间上是局部化的(正如我们上文提到的卷积的空间偏差)。

考虑一个矩阵 W、一个 2x3x1 像素的图像和一个 2x2 的滤波器 f,这种关系变得非常清晰:

图片来源:这里

这具有使模型具有*移不变性的优势,但如果图像中存在排列变换,则牺牲了MLPs的鲁棒性。

那么,关于视觉变换器(Vision Transformers)呢?

ViTs和卷积之间也有密切的关系(尽管它们具有相同的偏差)。实际上,正如上文所示,自注意力层以类似于卷积层的方式处理图像。在a 2020 paper中,作者展示了自注意力层如何表达任何卷积层。

所以正如我们所说,MLP、MLP-mixer、卷积网络和视觉变换器之间存在强烈的关系。虽然这些模型在归纳偏差和处理图像的方式上有很大的不同。

图片来源:这里

总的来说,由于各种模型之间存在强烈的关系和对应性,但也有归纳偏差的差异,我们可以使用MLP作为一个简单的模型来理解是否通过缩放和训练集中示例的增加可以弥补缺乏归纳偏差的问题。

大卫与歌利亚

图片由肖恩·罗伯逊Unsplash拍摄

在一篇*期的论文中,他们正是这样做的。他们采用了MLP,一个结构上简单的模型,试图理解缩放时发生了什么。缩放能改善简单全连接层的性能吗?

作者采用了一个 MLP 并构建了一个模型,他们堆叠了相同大小的 MLP 层。利用最*的文献,他们添加了层归一化和跳跃连接,以查看这些是否使训练更稳定。他们还创建了一个简单的架构,称为反向瓶颈,在这个架构中,通过两个权重矩阵,他们在同一块中扩展和收缩输入:

反向块。图像来源:这里

一方面,确实这些添加增加了归纳偏差,但与现代复杂架构相比,这几乎可以忽略不计。之后,他们决定探索将MLP与其他模型在计算机视觉任务中比较时的情况(通常MLP的表现远远逊色)。

图像来源:这里

作者在一些流行的计算机视觉数据集上测试了这些架构,得到了有趣的结果:

  • MLP 标准直接进入过拟合状态。

  • 添加数据增强略微提高了性能。

  • 使用瓶颈增加了性能。使用反向瓶颈的数据增强对性能有显著更高的影响(约 20% 的性能提升)。

  • 尽管如此,ResNet18 的性能远远优于。

这些数据与文献一致,文献指出,在样本量较小的情况下(毕竟这些数据集较小),归纳偏差很重要。事实上,ViTs和 MLP mixers 也观察到了同样的现象。

*年来,大模型的优势在于它们可以在大量图像上进行训练,然后将知识转移到较小的数据集(迁移学习)。为此,作者使用了ImageNet21k(1200 万张图像和 11k 类别)。之后,他们在新任务上进行了微调

图像来源:这里

结果令人惊讶,该模型能够将其对数据集的学习转移到另一个任务上。此外,结果远远优于以往所见。

尽管在大量数据上进行过预训练,我们仍然想强调的是,这样的 MLP 在所有数据集上与从头开始训练的 ResNet18 竞争,除了在 ImageNet1k 上表现意外不佳。 (source)

这证实了MLP是分析迁移学习、数据增强和其他理论元素的良好代理。这很令人惊讶,因为与现代模型相比,它是一个基础模型。

另一个令人惊讶的结果是,在训练中使用大的批量大小可以提高性能。

图像来源:这里

一般而言,观察到相反的效果。特别是在CNN的情况下,当用更多的示例进行训练时,试图保持小批量大小的性能。毕竟,使用小批量意味着在一个周期中进行更多的梯度更新(尽管训练时间更长)。另一方面,大批量大小更快,并且可以跨多个设备分配,从而节省时间。

此外,对变换器的观察表明,即使是这些大模型也从更大的批量大小中受益。

图像来源:这里

一般来说,*年来对规模定律的讨论很多:根据这一理论,随着参数的增加,性能有一个高度可预测的提高(并且遵循一个可量化的幂律)。这种规模定律在 LLMs 中得到了观察,尽管最*一些团队对其提出了质疑。

## 人工智能中的涌现能力:我们是否在追逐一个神话?

改变对大型语言模型涌现特性的看法

towardsdatascience.com

尽管关于规模定律的讨论仍在进行中,但分析这种情况是否也适用于像MLP这样的简单模型仍然很有趣(毕竟,MLP 通过增加参数数量应倾向于过拟合)。

在这项研究中,作者还定义了一个具有递增参数数量的模型家族。

图像来源:here

的确,MLP 似乎也展现了类似幂律的行为。

图像来源:here

这确实是一个有趣的结果,因为它表明,即使像 MLP 这样的简单模型也可以展示假定的幂律行为。

MLP 是一个并非为处理图像而设计的模型。事实上,作者指出,由于 MLP 的归纳偏差较差,它更依赖于示例的数量。因此,虽然可以通过大量示例来弥补弱的 归纳偏差,但这需要大量的示例。

一个非常有趣的点是,这些模型都在单个 GPU 上运行。对于 ImageNet21k 的最大架构,单个周期在单个 24 GB GPU 上花费了 450 秒。换句话说,这些实验可以在任何商业 GPU 上快速运行。

作者指出,MLPs 显然更高效,可以使用更大的批量:

正如很快会显而易见的那样,MLP 在对单个图像进行预测时需要显著更少的 FLOPs,本质上更有条理地利用其参数。因此,与其他候选架构相比,延迟和吞吐量显著更好。(来源)

图像来源:here

结论

照片由 Philip Myrtorp 拍摄,发布在 Unsplash 上。

归纳偏差 是机器学习的基本概念之一。一般来说,这是我们根据数据类型选择一个模型而非另一个模型的主要原因之一。尽管已经有很多研究,但理论上仍存在空白。

令人着迷的是,考虑到假设先验领域的狭窄可能导致更好的结果。不过,这也付出了代价,包括理论水*和模型复杂性。如前所述,尝试向 ViTs 模型添加归纳偏差会导致创建越来越复杂且计算上低效的模型。

尽管 MLP 是一个极其简单的模型,但它在计算上具有高效的优势,这也是它被用于许多研究以填补理论空白的原因之一。主要问题之一是 MLP 在计算机视觉中的表现远远逊色于其他模型。

最*的结果显示,通过适当的调整,可以克服这一差距。此外,缺乏归纳偏差可以通过扩展来弥补。因此,MLP 可以作为研究现代架构及其在不同情况下表现的良好代理。

为什么这一切如此重要?

一般来说,*年来的 AI 研究集中在一个单一的范式上:更多的参数,更多的数据。为了提高准确率,出现了新的竞争。尽管如此,自 2017 年以来,变换器的架构一直没有改变。

这些庞大的模型具有相当高的训练成本。最*几个月,对替代方案的研究兴趣开始增长:既包括用更少的参数获得相同的结果,也包括寻找替代于变换器(及其*方计算成本)的方法。

## Welcome Back 80s: Transformers Could Be Blown Away by Convolution

Hyena 模型展示了卷积如何比自注意力更快

levelup.gitconnected.com META’s LLaMA: A small language model beating giants

META 开源模型将帮助我们理解语言模型的偏见是如何产生的

medium.com

在每种情况下,学术研究都被迫追赶以行业为主导的研究。很少有机构能够从头训练一个大型语言模型。然而,像这样的研究 表明,即使是简单的模型如 MLP 也可以大规模获得结果。这为更好地理解模型行为并开始思考变换器的替代方案提供了非常有趣的视角。

你怎么看?请在评论中告诉我。

如果你觉得这有趣:

你可以查看我的其他文章,也可以 订阅 以在我发布文章时获得通知,你还可以 成为 Medium 会员 来访问所有故事(*台的附属链接,我从中获得少量收入,您无需支付额外费用),也可以在 LinkedIn 上与我联系或找到我。

这是我 GitHub 仓库的链接,我计划在这里收集与机器学习、人工智能等相关的代码和资源。

[## GitHub - SalvatoreRa/tutorial: 机器学习、人工智能、数据科学的教程…

机器学习、人工智能和数据科学的教程,包含数学解释和可重复使用的代码(用 Python 编写)

github.com

或者你可能对我最*的一篇文章感兴趣:

[## AI 大学生重返实验室

大型语言模型如何解决大学考试以及这为何重要

levelup.gitconnected.com [## 我们能检测 AI 生成的文本吗?

水印可能是检测的解决方案

levelup.gitconnected.com ## 说一次!重复单词对 AI 无帮助

重复标记如何以及为何会伤害大型语言模型?这是一个问题吗?

[towardsdatascience.com

参考文献

这是我撰写本文时参考的主要文献列表,仅列出了每篇文章的第一个名字。

  1. Goodman, Nelson. 《事实、虚构与预测》(第四版)。哈佛大学出版社,1983 年

  2. Battaglia 等人, 2018, 《关系归纳偏差、深度学习与图网络》,链接

  3. Kauderer-Abrams, 2017, 《卷积神经网络中的*移不变性定量化》,链接

  4. Ritter 等人, 2017, 《深度神经网络的认知心理学:形状偏置案例研究》,链接

  5. Conway 等人, 2018, 《下颞皮层的组织与功能》,链接

  6. Geirhos 等人, 2022, 《ImageNet 训练的 CNN 对纹理存在偏见;增加形状偏置可以提高准确性和鲁棒性》,链接

  7. Hermann 等人, 2020, 《卷积神经网络中的纹理偏置的起源与流行》,链接

  8. Li 等人, 2021, 《形状-纹理去偏神经网络训练》,链接

  9. Ghiasi 等人, 2022, 视觉变换器学到了什么?视觉探索,link

  10. Morrison 等人, 2021, 探索腐败鲁棒性:视觉变换器和 MLP-Mixer 的归纳偏差,link

  11. Mormille 等人, 2023, 通过基于 Gram 矩阵相似度的正则化在视觉变换器上引入归纳偏差,link

  12. Tolstikhin 等人, 2021, MLP-Mixer:一种全 MLP 架构用于视觉,link

  13. Cordonnier 等人, 2020, 自注意力与卷积层之间的关系,link

  14. Bachmann 等人, 2023, 扩展 MLP:归纳偏差的故事,link

  15. Kaplan 等人, 2020, 神经语言模型的扩展规律,link

  16. Lei Ba 等人, 2016, 层归一化,link

  17. He 等人, 2015, 深度残差学习用于图像识别,link

  18. Ridnik 等人, 2021, 面向大众的 ImageNet-21K 预训练,link

  19. Sharad Joshi, 2022, 你需要了解的一切:归纳偏差,MLearning.ai

医疗 AI 的基础模型

原文:towardsdatascience.com/a-foundation-model-for-medical-ai-7b97e3ab3893?source=collection_archive---------2-----------------------#2023-09-19

介绍 PLIP,一个病理学基础模型

Federico BianchiTowards Data Science Federico Bianchi

·

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

--

图片由 Tara Winstead 提供: www.pexels.com/photo/person-reaching-out-to-a-robot-8386434/

介绍

正在进行的 AI 革新带来了各个方面的创新。OpenAI 的 GPT 模型正在引领发展,并展示了基础模型如何实际上使我们的一些日常任务变得更轻松。从帮助我们写得更好到简化一些任务,我们每天都会看到新模型的发布。

许多机会正在我们面前展开。能够帮助我们工作生活的 AI 产品将成为我们在未来几年中获得的最重要工具之一。

我们将在哪里看到最具影响力的变化?我们可以在哪里帮助人们更快地完成任务?人工智能模型最令人兴奋的一个方向是将我们引向医疗 AI 工具。

在这篇博客文章中,我将PLIP(病理语言和图像预训练)描述为病理学的第一个基础模型之一。PLIP 是一个视觉-语言模型,可以用于将图像和文本嵌入到同一向量空间中,从而实现多模态应用。PLIP 源自 2021 年 OpenAI 提出的原始CLIP模型,并已在《Nature Medicine》上发表:

Huang, Z., Bianchi, F., Yuksekgonul, M., Montine, T., Zou, J., 一种用于病理图像分析的视觉-语言基础模型,通过医疗 Twitter。2023, Nature Medicine.

在开始我们的冒险之前,一些有用的链接:

除非另有说明,所有图像均由作者提供。

对比预训练 101

我们展示了通过社交媒体上的数据收集以及一些额外的技巧,我们可以构建一个可以在医疗 AI 病理任务中取得良好结果的模型——而无需标注数据。

虽然介绍 CLIP(PLIP 衍生的模型)及其对比损失超出了这篇博客文章的范围,但了解一下还是很有帮助。CLIP 背后的非常简单的想法是,我们可以构建一个将图像和文本放入一个向量空间的模型,其中“图像及其描述将会彼此接*”。

对比模型——如 PLIP/CLIP——将图像和文本置于同一向量空间进行比较。黄色框中的描述与黄色框中的图像匹配,因此它们在向量空间中也非常接*。

上面的 GIF 还展示了一个示例,说明了如何将图像和文本嵌入同一向量空间的模型用于分类:通过将所有内容置于同一向量空间,我们可以通过考虑向量空间中的距离,将每个图像与一个或多个标签关联起来:描述与图像越接*,效果越好。我们期望最接*的标签是图像的真实标签。

明确来说:一旦 CLIP 训练完成,你可以嵌入任何图像或任何文本。请注意,这个 GIF 展示的是二维空间,但一般而言,CLIP 使用的空间维度要高得多。

这意味着,一旦图像和文本处于相同的向量空间中,我们可以做很多事情:从零样本分类找到哪个文本标签与图像更相似)到检索找到哪个图像与给定描述更相似)。

我们如何训练 CLIP?简单来说,模型会接收大量的图像-文本对,并尝试将相似的匹配项放在一起(如上图所示),而将其他项远离。图像-文本对越多,你学到的表示就越好。

我们将在这里结束对 CLIP 背景的介绍,这应该足以理解本文的其余部分。我在 Towards Data Science 上有一篇关于 CLIP 的更深入的博客文章。

## 如何训练你的 CLIP

介绍 CLIP 及其在 HuggingFace 社区周期间如何为意大利语进行微调。

towardsdatascience.com

CLIP 已被训练成为一个非常通用的图像-文本模型,但对于特定的使用案例(例如,时尚(Chia 等,2022 年))效果不佳,而且在某些情况下,CLIP 表现不佳,而领域特定的实现效果更好(Zhang 等,2023 年)。

病理学语言和图像预训练(PLIP)

我们现在描述如何构建 PLIP,这是我们对原始 CLIP 模型进行微调的版本,专门针对病理学设计。

为病理学语言和图像预训练构建数据集

我们需要数据,而这些数据必须足够好,以用于训练模型。问题是我们如何找到这些数据? 我们需要的是具有相关描述的图像——就像我们在上面的 GIF 中看到的那样。

尽管网络上有大量的病理数据,但这些数据通常缺乏注释,并且可能以非标准格式存在,如 PDF 文件、幻灯片或 YouTube 视频。

我们需要换个地方寻找,而这个地方就是社交媒体。通过利用社交媒体*台,我们有可能接触到大量与病理学相关的内容。病理学家使用社交媒体在线分享自己的研究,并向同事提问(请参见 Isom 等,2017 年,讨论了病理学家如何使用社交媒体)。此外,还有一组一般推荐的Twitter 标签,病理学家可以用来进行沟通。点击这里 查看这些标签。

除了 Twitter 数据外,我们还收集了LAION 数据集(Schuhmann 等,2022 年)中的一个子集,LAION 是一个包含 50 亿图像-文本对的大型数据集。LAION 通过爬取网络收集而来,也是许多流行的 OpenCLIP 模型训练所使用的数据集。

病理学 Twitter

我们使用病理学 Twitter 标签收集了超过 10 万条推文。过程相当简单,我们使用 API 收集与特定标签相关的推文。我们去除包含问号的推文,因为这些推文通常包含对其他病理(例如,“这是什么肿瘤?”)的请求,而不是我们实际需要的信息来构建模型。

我们提取包含特定关键词的推文,并去除敏感内容。此外,我们还去除了所有包含问号的推文,这些推文通常是病理学家向同事询问一些可能罕见的病例的提问。

从 LAION 采样

LAION 包含 50 亿图像-文本对,我们收集数据的计划如下:我们可以使用来自 Twitter 的图像,并在这个大型语料库中查找相似的图像;这样,我们应该能够获得相当相似的图像,并且这些相似的图像也可能是病理图像。

现在,手动进行这些操作是不可行的,嵌入和搜索 50 亿个嵌入是一个非常耗时的任务。幸运的是,LAION 有预计算的向量索引,我们可以通过 API 用实际图像查询!因此,我们简单地嵌入我们的图像并使用 K-NN 搜索在 LAION 中查找相似图像。请记住,这些图像每个都有一个标题,非常适合我们的用例。

我们通过在 LAION 数据集上使用 K-NN 搜索扩展数据集的设置非常简单。我们从原始语料库中的图像开始,然后在 LAION 数据集中搜索相似的图像。我们得到的每张图像都有一个实际标题。

确保数据质量

并非所有我们收集的图像都是好的。例如,我们从 Twitter 收集了大量医疗会议的合影。从 LAION,我们有时会得到一些类似分形的图像,这些图像可能模糊地类似于某些病理模式。

我们做的事情非常简单:我们使用一些病理数据作为正类数据,用 ImageNet 数据作为负类数据来训练分类器。这种分类器具有极高的精度(实际上,区分病理图像和网络上的随机图像很容易)。

此外,对于 LAION 数据,我们应用了英语语言分类器来去除非英语的示例。

训练病理语言和图像预训练

数据收集是最困难的部分。一旦完成且我们信任我们的数据,就可以开始训练。

为了训练 PLIP,我们使用了原始的 OpenAI 代码进行训练——我们实现了训练循环,添加了损失的余弦退火,并做了一些调整,以确保一切顺利进行并且可验证(例如,Comet ML 跟踪)。

我们训练了许多不同的模型(数百个),并比较了参数和优化技术。最终,我们得出了一个令人满意的模型。详细信息请参见论文,但在构建这种对比模型时,最重要的组成部分之一是确保在训练过程中批量大小尽可能大,这可以使模型学会区分尽可能多的元素。

医学 AI 的病理语言和图像预训练

现在是测试我们的 PLIP 模型的时候了。这个基础模型在标准基准测试上表现如何?

我们进行了不同的测试来评估 PLIP 模型的性能。其中最有趣的三个是零样本分类、线性探测和检索,但我主要关注前两个。在这里为了简洁,我将忽略实验配置,但这些都可以在手稿中找到。

PLIP 作为零样本分类器

下面的 GIF 演示了如何使用类似 PLIP 的模型进行零样本分类。我们使用点积作为向量空间中的相似性度量(点积越高,相似度越高)。

进行零样本分类的过程。我们将图像和所有标签进行嵌入,然后在向量空间中找出与图像最接*的标签。

在下图中,你可以看到 PLIP 与 CLIP 在我们用于零样本分类的一个数据集上的快速比较。使用 PLIP 替代 CLIP 能显著提升性能。

PLIP 与 CLIP 在两个数据集上的零样本分类性能(加权宏 F1)。注意 y 轴在约 0.6 处停止,而不是 1。

PLIP 作为线性探测的特征提取器

使用 PLIP 的另一种方法是作为病理图像的特征提取器。在训练过程中,PLIP 处理了许多病理图像,并学习为这些图像构建向量嵌入。

假设你有一些标注数据,想要训练一个新的病理分类器。你可以使用 PLIP 提取图像嵌入,然后在这些嵌入上训练一个逻辑回归(或你喜欢的任何回归器)。这是一种简单有效的分类任务方法。

为什么这样有效?这个想法是,PLIP 嵌入具有病理特异性,应该比 CLIP 嵌入(通用目的)更好。

PLIP 图像编码器允许我们为每个图像提取一个向量,并在其上训练一个图像分类器。

下面是 CLIP 和 PLIP 在两个数据集上的性能比较示例。虽然 CLIP 的表现不错,但我们使用 PLIP 得到的结果要高得多。

PLIP 与 CLIP 在两个数据集上进行线性探测的表现(宏 F1)。注意 y 轴从 0.65 开始,而不是 0。

使用病理语言和图像预训练

如何使用 PLIP?以下是一些使用 PLIP 的 Python 示例,以及一个你可以用来稍微玩一下模型的 Streamlit 演示。

代码:使用 PLIP 的 API

我们的 GitHub 仓库提供了一些额外的示例,你可以参考。我们已经构建了一个 API,允许你轻松地与模型进行交互:

from plip.plip import PLIP
import numpy as np

plip = PLIP('vinid/plip')

# we create image embeddings and text embeddings
image_embeddings = plip.encode_images(images, batch_size=32)
text_embeddings = plip.encode_text(texts, batch_size=32)

# we normalize the embeddings to unit norm (so that we can use dot product instead of cosine similarity to do comparisons)
image_embeddings = image_embeddings/np.linalg.norm(image_embeddings, ord=2, axis=-1, keepdims=True)
text_embeddings = text_embeddings/np.linalg.norm(text_embeddings, ord=2, axis=-1, keepdims=True)

你还可以使用更标准的 HF API 来加载和使用模型:

from PIL import Image
from transformers import CLIPProcessor, CLIPModel

model = CLIPModel.from_pretrained("vinid/plip")
processor = CLIPProcessor.from_pretrained("vinid/plip")

image = Image.open("images/image1.jpg")

inputs = processor(text=["a photo of label 1", "a photo of label 2"],
                   images=image, return_tensors="pt", padding=True)

outputs = model(**inputs)
logits_per_image = outputs.logits_per_image 
probs = logits_per_image.softmax(dim=1) 

演示:PLIP 作为教育工具

我们还相信 PLIP 和未来的模型可以作为医学 AI 的有效教育工具。PLIP 允许用户进行零样本检索:用户可以搜索特定的关键词,PLIP 将尝试找到最相似/匹配的图像。我们在 Streamlit 中构建了一个简单的 Web 应用,你可以在这里找到它。

结论

感谢阅读这些内容!我们对这项技术未来可能的发展感到兴奋。

我将通过讨论 PLIP 的一些非常重要的局限性以及建议一些可能感兴趣的附加内容来结束这篇博客文章。

局限性

尽管我们的结果很有趣,但 PLIP 存在许多不同的局限性。数据不足以学习病理学所有复杂的方面。我们已经构建了数据过滤器以确保数据质量,但我们需要更好的评估指标来理解模型的正确与错误。

更重要的是,PLIP 并没有解决病理学当前的挑战;PLIP 不是一个完美的工具,可能会犯很多需要调查的错误。我们看到的结果无疑是有前景的,它们为未来在病理学中结合视觉和语言的模型打开了许多可能性。然而,在我们能看到这些工具在日常医学中应用之前,还有很多工作要做。

杂项

我还有一些关于 CLIP 建模和 CLIP 局限性的博客文章。例如:

## 教授 CLIP 一些时尚知识

训练 FashionCLIP,一个特定领域的 CLIP 模型用于时尚

towardsdatascience.com ## 你的视觉-语言模型可能是一个词袋

我们在 ICLR 2023 的口头报告中探讨了视觉-语言模型在语言方面的局限性

towardsdatascience.com

参考文献

Chia, P.J., Attanasio, G., Bianchi, F., Terragni, S., Magalhães, A.R., Gonçalves, D., Greco, C., & Tagliabue, J. (2022). 一般时尚概念的对比语言与视觉学习。Scientific Reports, 12

Isom, J.A., Walsh, M., & Gardner, J.M. (2017). 社交媒体与病理学:我们现在处于何处以及为何重要?Advances in Anatomic Pathology

Schuhmann, C., Beaumont, R., Vencu, R., Gordon, C., Wightman, R., Cherti, M., Coombes, T., Katta, A., Mullis, C., Wortsman, M., Schramowski, P., Kundurthy, S., Crowson, K., Schmidt, L., Kaczmarczyk, R., & Jitsev, J. (2022). LAION-5B:一个用于训练下一代图像-文本模型的开放大规模数据集。ArXiv, abs/2210.08402

Zhang, S., Xu, Y., Usuyama, N., Bagga, J.K., Tinn, R., Preston, S., Rao, R.N., Wei, M., Valluri, N., Wong, C., Lungren, M.P., Naumann, T., & Poon, H. (2023). 大规模领域特定预训练用于生物医学视觉-语言处理。ArXiv, abs/2303.00915

卫星图像基础模型

原文:towardsdatascience.com/a-foundation-model-for-satellite-images-dbf356c746a9?source=collection_archive---------8-----------------------#2023-11-04

Prithvi-100M IBM 地理空间 AI 基础模型用于 NASA 地球观测数据

Caroline ArnoldTowards Data Science Caroline Arnold

·

关注 发表在 Towards Data Science ·7 分钟阅读·2023 年 11 月 4 日

--

阿尔巴尼亚卡拉瓦斯塔泻湖的卫星图像,2017 年。图像来源:www.esa.int/var/esa/storage/images/esa_multimedia/images/2017/03/karavasta_lagoon_albania/16854373-1-eng-GB/Karavasta_Lagoon_Albania.jpg。包含修改后的 Copernicus Sentinel 数据。

基础模型是灵活的深度学习算法,旨在处理通用任务,而不是立即专注于特定任务。在大量未标记数据上进行训练后,它们可以通过最少的微调应用于各种下游任务。基础模型在自然语言处理(BERT,GPT-x)和图像处理(DALL-E)中都很有名。

2023 年 8 月,NASA 和 IBM 发布了用于 NASA 地球观测数据的地理空间 AI 基础模型。该模型以 Prithvi 命名,开放源代码在Huggingface上,Prithvi 是印度教的大地女神。它已在 NASA 卫星数据上进行训练——根据 IBM超过 250 PB的数据可用。

在这篇博客文章中,我们讨论

  • 用于训练的 NASA 协调 Sentinel-2 Landsat 数据集,

  • Prithvi-100M 地理空间 AI 基础模型的架构,

  • 在 IBM 的 Vela 超级计算机上的训练过程,

  • 示例应用:洪水和作物类型识别。

训练数据

地理空间 AI 基础模型已在NASA 协调的 LandSat Sentinel-2 数据上进行训练。

Sentinel-2是由欧洲航天局协调的卫星任务,目前有两颗卫星在轨道上拍摄地球的高分辨率图像。它专注于陆地、沿海地区和特定的开放水域。Landsat 卫星由 NASA 发射,用于记录地表反射。协调数据结合了两个传感器的输入, resulting in a spatial resolution of about 30 meters and an average revisit time of two to three days. This resolution is sufficient for agricultural monitoring, land use classification, and natural disaster detection.

标准照片由红色、绿色和蓝色三种颜色组成。Sentinel-2 数据总共提供 13 种“颜色”,即所谓的波段,涵盖可见光、*红外和短波红外电磁谱范围。选择的波段可以用于识别不同的事物,例如,红外波段包含有关植被的信息。有关背景,请参见这篇文章关于 Sentinel-2 波段组合。

夏威夷机场的伪彩色红外图像。图像来源:ESA sentinel 卫星图像,CC BY-SA 4.0 <creativecommons.org/licenses/by-sa/4.0>, 通过维基媒体共享资源。

云层阻碍了地球观测卫星的视线。为应对这一影响,Sentinel-2 提供了一个可用于识别云层覆盖的波段。受影响的像素被屏蔽,以免干扰图像处理算法。

因此,Sentinel-2 和 Landsat 数据是未标记的。需要大量的人力和专业知识才能提供逐像素的土地使用类别分类。基础模型高度通用,并从数据中提取结构,而无需在训练过程的初始阶段提供标记数据。因此,它们在地球观测数据方面显得非常有前途。

模型架构

Prithvi-100M 地理空间 AI 基础模型基于时间序列视觉变换器和掩蔽自编码器。模型卡显示在 Huggingface 上:

Huggingface 上的 Prithvi-100M 模型卡。图像来源:huggingface.co/ibm-nasa-geospatial/Prithvi-100M/blob/main/GFM.png

该模型接受视频格式的 Landsat 图像作为输入。来自同一地点的图像被加载为时间序列,而静态图像可以通过将时间序列长度设置为 1 进行处理。波段对应于视觉变换器的通道。

视觉变换器

在 2020 年,Google Research 的团队展示了变换器不仅可以应用于自然语言处理,还可以应用于图像 (Dosovitsky et al, ICLR 2020)。在那之前,卷积神经网络一直是图像处理的事实上的标准。

视觉变换器首先将图像切割成小块,类似于对语言处理变换器进行句子的标记化。然后,添加可学习的嵌入和位置编码。在原始论文中,展示了在大量训练数据下,视觉变换器可以超越典型的计算机视觉架构,如 ResNet。

[## 视觉变换器(ViT)放大镜下,第一部分

嵌入

yurkovak.medium.com](https://yurkovak.medium.com/vision-transformer-vit-under-the-magnifying-glass-part-1-70be8d6661a7?source=post_page-----dbf356c746a9--------------------------------)

掩蔽自编码器

Prithvi-100M 掩蔽自编码器基于 He 等人(2021)的原始实现,arxiv.org/pdf/2111.06377.pdf。概念很简单:

图像中的随机块被掩蔽。自编码器学习预测缺失的像素。这类似于大型语言模型的训练,其中模型学习预测句子中缺失的单词。

在原始论文中,考虑了带有 RGB(红色、绿色、蓝色)颜色通道的 2D 图像。论文中广泛讨论了在语言数据和图像数据上进行训练的区别。

编码器仅在未被遮挡的图像块上工作,这样可以节省计算时间。嵌入由对单独图像块的线性投影来处理,该投影包含可学习参数。

位置嵌入很重要,以便算法知道图像块在原始图像中的位置。在遮挡自编码器的情况下,位置嵌入通过 2D 正弦-余弦函数提供,这种函数通常用于变换器模型。它对图像中的 2D 网格位置进行编码。位置嵌入可能包含可学习的参数,但在MAE 库的实现中似乎并非如此。

遮挡自编码器的应用。左侧:原始图像的遮挡图像块。中间:重建。右侧:真实情况。图像来源:arxiv.org/pdf/2111.06377.pdf(图 2)

MAE 架构的变化

为了处理具有更多通道的卫星数据时间序列,NASA 和 IBM 团队对遮挡自编码器架构进行了若干修改。

  • 2D 图像块嵌入被更改为 3D 图像块嵌入。

  • 2D 位置嵌入被更改为 3D 位置嵌入。

  • 图像块创建考虑到数据的 3D 特性。

  • 除了 RGB 颜色外,还增加了一个*红外和两个短波红外波段。

损失函数

均方误差(MSE)损失用于训练,通过逐像素比较原始图像和重建图像。

模型训练

模型训练过程描述在 IBM 博客中:research.ibm.com/blog/nasa-hugging-face-ibm。遗憾的是,提供的细节不多。然而,IBM 提到他们在公司 AI 超级计算机 Vela 上进行了训练。Vela是一个完全基于云的超级计算机,仅为 IBM 研究部门运营。

超级计算机由 200 个节点组成。每个节点配备了 8 个 NVIDIA A100 GPU,每个 GPU 有 80 GB 的内存。节点 RAM 为 1.5 TB,并且配备四个 3.2 TB 的本地硬盘。这些配置能够处理训练基础模型所需的大数据集。节点之间通过一个能传输高达 100 GB/秒的网络连接。

应用

Prithvi-100M 地理空间 AI 基础模型可以应用于多种下游任务。我们专注于两个任务:洪水和作物类型识别。

洪水

保留 Prithvi-100M 的原始编码器部分,模型现在被调整为预测卫星图像中洪水的扩展。详细信息描述在 HuggingfaceSen1Floods11 数据集用于微调,涵盖了六大洲的 11 次洪水事件。

微调地理空间 AI 基础模型以进行洪水检测。图像来源:huggingface.co/ibm-nasa-geospatial/Prithvi-100M-sen1floods11/blob/main/sen1floods11-finetuning.png

为了将 Prithvi-100M 准备好以应对下游任务,需要将嵌入形状转换回原始图像形状。然后,添加一个最终的 2D 卷积层,应用特定任务的分类。

图像中的每个像素被分类为水域或非水域(陆地)。由于这是一个分类问题,因此使用了二元交叉熵损失。一次只处理一张图像,因此未使用 Prithvi-100M 的时间序列功能。

作者报告了在玻利维亚的一个保留洪水事件中,*均准确率为 93%,*均交并比为 86%。

提供了一个演示页面,用户可以上传自己的 Sentinel-2 图像,并要求 Prithvi-100M 识别洪水。

洪水识别演示的快照。黑色像素对应陆地,白色像素对应水域。图像来源:huggingface.co/spaces/ibm-nasa-geospatial/Prithvi-100M-sen1floods11-demo(使用 India_900498_S2Hand.tif)

作物类型识别

为了利用时间序列功能,作者提供了作物类型识别的演示。作物类型的实际情况由标记图像提供。这是一个多类分类问题,训练时使用了交叉熵损失。

作为 Prithvi-100M 的下游任务,进行多时相作物类型分类。图像来源:huggingface.co/ibm-nasa-geospatial/Prithvi-100M-multi-temporal-crop-classification/blob/main/multi_temporal_crop_classification.png

作者报告了不同作物类型的不同准确率。*均准确率为 64%,交并比为 46%。然而,作者指出实际情况存在噪声,更准确的标签将有助于改进这一下游任务。

Prithvi-100M 的作物类型演示。左侧三幅图显示卫星图像的时间序列。右侧图显示模型预测,每个像素根据作物类型着色。图片来源:huggingface.co/spaces/ibm-nasa-geospatial/Prithvi-100M-multi-temporal-crop-classification-demo

总结

我们已经介绍了地球空间 AI 基础模型,目前(2023 年)在 Huggingface 上以 Prithvi-100M 的名义是最大的地球空间模型。该模型由 IBM Research 和 NASA 开发,使用 Landsat 数据集进行训练。

我们已经介绍了地球空间 AI 基础模型的训练数据、架构和训练过程。该模型开放源代码,可以进行更具体任务的微调。洪水检测和作物类型识别应用展示了地球空间 AI 基础模型的巨大潜力。

由于 Sentinel-2 数据可用于个人非商业用途,有兴趣的用户可以创建适用于特定下游任务的自己的模型。在未来的帖子中,我将展示如何为植被识别和超分辨率微调地球空间 AI 基础模型。

进一步阅读

## 环境数据科学:简介

处理环境数据的示例、挑战和展望

towardsdatascience.com

基于自然法则的人本中心 AI 框架

原文:towardsdatascience.com/a-framework-for-a-human-centered-ai-based-on-the-laws-of-nature-a8bfbb233250?source=collection_archive---------16-----------------------#2023-05-08

整合自然智能与人工智能

Tom KehlerTowards Data Science Tom Kehler

·

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

--

自然的涌现秩序(图片来源 iStock Getty Images 2090608323

在哈佛大学教师俱乐部举办的波士顿全球论坛高级会议“AI 助手监管峰会:促进科技启蒙经济联盟”上做了报告。此处展示的论文是该讲座的扩展。

我们面临许多十字路口。最*几个月的一个显著十字路口是 AI,导致了从恐惧到欣喜的广泛反应。毫无疑问,您现在已经体验到了与 ChatGPT 互动的乐趣。许多人已加入了采用的潮流。其他人则认为当前的 AI 表现不过是另一场追逐底线的竞赛,我们因为必须而抛弃了谨慎。其他人都在做,我们也必须这样做。汇聚的恶劣行为无人愿见——但存在因为没人知道如何建立信任的阴影。技术不是敌人。未能合作并建立信任会导致鲁莽采用,可能带来伤害。

在这个简要概述中,我希望为您提供一个建立信任和减少风险的 AI 未来框架。

这个框架最初由科学的创始人和启蒙时代的科学方法揭示。随后形成的科学方法为建立可信知识奠定了基础——这是一个依赖于集体人类智慧和对自然中出现优雅的信任的协作过程。

我们建议利用集体人类智慧和内置于生物系统物理中的智慧来指导我们前进。¹

几乎 70 年来,人工智能的科学探索集中在使用符号表示和推理工具构建自然智能和人类认知技能的手工模型上。它们能够解释如何解决问题。通过观察它们的推理来建立信任。

在过去 20 年里,互联网提供的数据爆炸带来的统计学习取得了显著成果——从自动驾驶汽车到今天将我们聚集在一起的大型语言模型。特别是,变压器深度学习架构解锁了生成型 AI 强大的潜力,这创造了我们今天看到的令人印象深刻的结果。

让我们今天关注的问题涉及三个基本问题。这是信息技术历史上第一次,我们没有执行数据来源的概念。 因此,这些巨大的生成能力可能成为误导性信息的有力传播者,破坏对知识的信任。第二个问题是可解释性——系统是黑箱。第三个问题是它们需要一个上下文感知。

这三点弱点与科学方法的三个支柱——引用、可重复性和结果的背景化——相矛盾。 我们该怎么办?

朱迪亚·珀尔说,“你比你的数据更聪明。”我们同意。人类的反事实思维能力远远强于我们从过去数据中的相关模式中学到的任何东西。

大型语言模型和深度学习架构通常基于数据中的模式识别和相关性模型来发展智能行为模型。LLM 的生成输出利用人工干预来过滤和训练结果。风险依然存在。过滤过程可能会遗漏包含错误信息的内容生成。

图 1(图片作者提供)

五年前,在MIT Technology Review的采访中,深度学习的奠基人之一,Yoshua Bengio 说:

“我认为我们需要考虑人工智能的艰巨挑战,而不是满足于短期的、渐进的进步。我不是说我想忘记深度学习。相反,我想在此基础上进行构建。但我们必须扩展它,以进行推理、学习因果关系和探索世界以获取信息。”²

目前基于历史数据模式相关性的模型不太可能捕捉到人脑能力的复杂性。人脑的想象力和基于经验生成因果模型的能力必须成为未来人工智能模型的一个重要部分。我们提出了一种结合人类集体智能和人脑模型的方法。

拉里·佩奇、谢尔盖·布林和特里·温诺格发现引用索引可以成为对网络信息进行有规模排序的方法。³ PageRank 算法为网络带来了秩序。引用索引的数学为理解人类协作中的信息共享带来了秩序。

图 2:图片作者提供

一种新一代的人工智能,融合了人类集体推理,经过过去八年的开发,使用引用索引方法作为知识发现过程。它允许大规模的知识发现,支持引用、可重复性和情境化。我们建议将其作为未来框架的一部分。

集体推理旨在了解一个社区或小组对预期结果的集体偏好和信念。产品发布会产生我们想要的结果吗?如果我们改变远程工作的政策,我们的生产力会增加还是减少?使用 ChatGPT 和 LLMs 的最佳政策是什么?这些问题需要了解一个小组对预测结果的‘集体思维’。集体推理过程利用人工智能技术学习集体思维模型。该过程是单盲的,减少了偏见。系统经过四年的测试,针对 20 至 30 名专家/投资者预测初创企业成功的情况,准确率超过 80%。⁴ 每项投资的集体信念和预测被映射为集体知识模型——贝叶斯信念网络⁵。这些因果模型是该小组集体推理的生成可执行表示。

我们可以将科学知识发现过程中的关键元素融入我们共同创造或协作解决复杂问题的方式中。我们建议使用 AI 来学习集体知识模型、保持来源、可解释性和背景的因果模型,而不是让 AI 破坏对知识的信任。这是一种新的启蒙关键组成部分——将科学方法带入协作中。

集体推理允许学习一个群体的意图。基于代理的模拟在预测提议解决方案的影响方面很有用。基于公共数据的合成模型允许对共同创建的解决方案进行规模化和预测,我们建议将其作为框架的一部分。该倡议中的一个合作伙伴公司已经建立了一个重要的能力来大规模模拟影响,并将其应用于疾病传播的社会影响。⁶

未来 AI 的基础是什么?自 1956 年夏天 AI 诞生以来的 68 年里我们学到了什么?前几代开发了形成当前 AI 格局的组件。合作现象的数学和磁性的物理学在将这一切联系在一起方面发挥了令人兴奋的作用。1982 年,霍普菲尔德证明了人工神经网络的集体计算能力的涌现直接映射到自旋玻璃的数学物理学。⁷ 相同的合作现象数学描述了从混乱中涌现出的秩序,如本文开头的燕群照片所示。

最*,MIT 的 Lin、Rolnick 和 Tegmark 显示,深度学习和廉价学习之所以效果如此好,与物理定律有关。贝叶斯学习被重新表述为量子和经典物理学中使用的基本方法——哈密顿量。⁸ 明确关注 AI 在自然法则中的根源应成为未来 AI 发展的重点。

一切的核心在于从无序中学习秩序。大脑中的新一波研究将学习在秩序/无序边界上的理论应用于创建活的智能系统——自由能原理。⁹

FEP 是一个基于贝叶斯学习的框架。大脑被认为是一个贝叶斯概率机器。如果感官输入与期望不匹配,主动推理会寻求最小化未来的不确定性。我们期望和感知之间的差异称为惊讶,并表示为自由能(可用于行动的能量)。寻找最小自由能的路径等同于寻找减少惊讶(不确定性)的路径。

基于 FEP 的 AI 在本地进行适应,并根据物理和生物科学中使用的变分自由能最小化原则进行扩展。Bioform Labs 正在构建一个适应和学习的生物 AI。¹⁰ 与需要大量训练数据集和复杂成本函数的第二代 AI 不同,基于生命系统物理学的 AI 是适应性的,并且存在于一个生态系统中。它可以被设计为尊重导致生命系统需求的状态。

启动这一新框架所需的技术今天就可以应用。我们不需要暂停 AI 的发展。集体推理适用于我们需要问自己关于 AI 在各种具体背景下影响的问题。AI 将如何影响技术投资?它将如何改变我们的招聘实践?它对我们的社区有什么影响?

图 3(作者提供的图片)

此外,可以在保留隐私边界的情况下,参与 ChatGPT 和 LLMs 的创意过程。来自 LLM 的创意可以在特定的私人背景中进行策划和使用。策划和情境化的贡献在一个获得专利的私人 LLM 环境中进行管理。¹¹

集体推理学习意图和可能的解决方案。基于代理的模拟预测影响。我们不再需要将组织视为僵化的。基于主动推理的新型组织治理支持适应性学习生存路径。我们相信这一框架是未来的愿景,将为新的 AI 赋能的启蒙提供基础。

图 4(作者提供的图片)

一个新的 AI 赋能的启蒙。正如启蒙时代使科学摆脱了宗教权威的压迫一样,新倡议——AI 赋能的启蒙,提供了一条协作和共同创造解决方案的路径——使我们摆脱当前 AI 狂热潮的不良后果。

总结来说,大型语言模型提供了非常有用的能力,这些能力以令人印象深刻的速度展开。请阅读警告标签!ChatGPT 确实警告不要盲目相信结果,而要使用批判性思维。不要暴露私人数据。关于私人数据,图 3 和图 4 展示了一种通过允许 ChatGPT 或其他“代理”提供输入与人类专家进行精心策划的合作的方法,结果保留在私密管理的 LLM 环境中。这种方法允许在保留私人知识产权的情况下探索 LLM 的生成能力。

(1) 本文提出的框架源于 MIT 媒体实验室于 3 月 6 日举行的会议,由 BioForms 的 John Clippinger、Crowdsmart 的 Kim Polese 以及其他几位参与者共同发起。波士顿全球论坛的 CEO Tuan Nguyen 参加了会议,并在会后与波士顿全球论坛一起创建了 AI 治理框架bostonglobalforum.org/

(2)’Knight, Will,(2018 年 11 月 17 日)“AI 之父之一对其未来感到担忧”MIT Technology Review

(3) Page, L., Brin, S., Motwani, R. and Winograd, T. (1998) 页面排名引用排名:为网络带来秩序。技术报告 SIDL-WP-1999–0120,斯坦福数字图书馆技术项目。

(4) 更多信息请参阅AI-guided Co-creation

(5) 专利 US11366972 分配给CrowdSmart.ai

(6) Epistemix.com

(7) Hopfield JJ. 1982. 神经网络和具有突现集体计算能力的物理系统。Proc. Natl Acad. Sci. USA 79, 2554–2558

(8) Lin, H.W., Tegmark, M. & Rolnick, D. 为什么深度和廉价学习效果如此显著?. J Stat Phys 168, 1223–1247 (2017). doi.org/10.1007/s10955-017-1836-5

(9) Friston, K. 自由能原理:统一的大脑理论?. Nat Rev Neurosci 11, 127–138 (2010). doi.org/10.1038/nrn2787

(10) bioformlabs.org/

(11) 专利 — 管理和测量知识发现过程中的语义覆盖。2022/072895

分析流失的框架

原文:towardsdatascience.com/a-framework-for-analyzing-churn-370d2283b75c?source=collection_archive---------4-----------------------#2023-01-13

使用模拟数据集进行客户流失分析的逐步指南

Gabriele AlbiniTowards Data Science Gabriele Albini

·

阅读 发表在 Towards Data Science ·14 分钟阅读·2023 年 1 月 13 日

--

图片来源:JESHOOTS.COMUnsplash

介绍

客户流失”已经成为一个常见的商业词汇,它指的是流失率的概念,维基百科定义为:

“在给定时间段内离开供应商的合同客户或订阅者的比例”

从数据角度分析流失时,我们通常意味着使用现有工具提取有关现有客户群的信息,具体来说:量化当前的流失率并了解可能影响/预防未来流失的因素。

因此,当我们开发“流失模型”时,应该考虑使用现有数据并且要有两个目标:

  1. 为现有活跃客户预测流失

  2. 对影响客户流失决策的因素进行一些假设,识别出可能减少流失的潜在措施。

预测流失需要大量工作,这不是一项容易的任务,但更重要的是,它甚至不是最终目标:这是设计和实施客户“留存”策略的起点!

本文将重点介绍流失分析框架的实现,灵感来源于书籍:[1]《用数据对抗流失》,作者是卡尔·S·戈德。这是一本推荐给所有处理流失数据的人的优秀书籍:书中详细介绍了流失分析的全过程,提供了很多细节和示例(包括解释代码!)。在所有建议的步骤中,我提取了在我的经验中最相关和成功的部分,并将其调整为我熟悉的背景和数据集。本文将该框架应用于一个模拟数据集,灵感来源于一个真实的商业案例(Github 仓库链接)。

目录:

1- 数据

1.1- 开发流失模型时应考虑哪些数据?

1.2- 原始数据

2- 数据预处理:流失指标

2.1- 创建客户指标

2.2- 分析流失指标

3- 使用机器学习进行流失预测

3.1- 逻辑回归

3.2- 随机森林

3.3- XGBoost

4- 生成流失预测

5- 下一步

参考文献

1. 数据

1.1 开发流失模型时应考虑哪些数据?

这不是一个简单的问题!很多不同的信息可能与流失相关,制定通用规则永远无法涵盖所有可能的业务、系统、背景等。例如,在考虑流失相关信息时,我们可能会考虑:

  • 关于客户(或账户)的基本信息:性别、位置、年龄、任期等

  • 与订阅相关的信息:客户订阅的产品、激活的附加功能、激活和取消日期等

  • 支付信息:客户支付了多少?他们使用什么支付方式?他们是否定期付款?

  • 产品使用信息:登录信息、点击信息、与产品的互动分钟数等

  • 与客户支持互动相关的信息:客户进行的聊天或电话、支持服务的评分、投诉细节等

翻译成系统后,这些数据需要来自各种事务系统(CRM、ERP、计费等),并应适当地组织到某些数据湖/数据仓库中(理想情况下,频繁地拍摄覆盖几个月)。考虑到这一点,需要有大量的专业知识来了解哪些字段代表了哪些信息,通常,访问这些数据需要大量的批准,特别是如果外部顾问想要使用这些数据的话。

根据我的经验,所有这些数据(以及相关的历史记录)很少可用。通常,可用且已组织的数据是公司因财务或法律法规要求或仅因运行日常业务所需的数据。这些数据必须在某处可用。

例如:假设我们是一家提供按需视频培训的公司,我们需要知道客户拥有哪些订阅以及他们支付了多少,以提供我们的服务并制作财务报表。然而,我们不一定需要存储客户 XYZ 在完成特定视频之前暂停视频的具体时间。

鉴于所有这些原因,为了保持文章简洁和现实,我将重点关注一个“较小”的数据集,理想情况下这些数据应来自任何 CRM 中应有的数据。

1.2 原始数据

让我们假设我们是一家通过网站提供在线视频课程的 B2C(商业对消费者)公司。我们的业务运作方式如下:

  • 新用户可以订阅两个领域的课程:机器学习(领域 A)和吉他(领域 B)。他们可以购买多个订阅,从而允许不同用户同时登录。此外,他们还可以选择包含“附加服务”的选项,该服务包括每周与所选领域的专家进行在线直播。

  • 一旦订阅,用户将每月支付费用,并可能有或没有折扣。他们可以随时取消订阅,这意味着订阅将在月底不会续订。

  • 用户可以打开实时聊天并联系支持团队解决任何问题。

原始数据将如下所示:

虚拟原始数据 | 图片来源作者

这种商业背景非常常见,适用于任何具有每月订阅和将附加服务添加到基本报价的 B2C 业务(例如:按需媒体内容、电信、公用事业、电子商务、保险和高级软件等企业)。

2. 数据预处理:流失指标

从原始数据开始,我们需要预测客户是否会流失。我们将把客户视为一个整体,无论他们有多少个订阅。

由于我们的目标是预测流失以制定留存策略,我们需要提前知道客户是否会流失,以便我们可以采取措施影响这一决定。

2.1 创建客户指标

考虑到以上原始数据,我们可以生成哪些 KPI?以下是一些想法(它们是我们数据集的列):

  • “mrr_ratio” = 这是按订阅计算的每月经常性收入。因此,对于每个客户:我们对每个有效订阅求和([每月费用 — 折扣]),然后计算有效订阅的数量,并将两者相除。

  • “mrr_ratio_A”和“mrr_ratio_B” = 这些是按领域计算的每月经常性收入(A 是机器学习;B 是吉他),考虑领域内的 mrr 和活跃订阅数量。

  • “subs_A”和“subs_B” = 按领域的活跃订阅数量

  • “discount_ratio” = 客户的折扣百分比,计算方法为:1 — ([每月费用 — 折扣] / [每月费用])

  • “has_addon” = 一个标志,指示客户是否有至少一个带附加组件的订阅

  • “support_chats” = 客户在一个期间内发起的聊天次数

  • “is_churn” = 一个标志,指示客户是否将要流失(1)或不流失(0)

我认为使用我们历史原始数据来计算这些 KPI 的最佳方法是:

  • 确定一些固定的观察期(例如每月 20 号),留出一些合理的时间从我们的续订(我们假设每月 30 号发生)。

  • 创建一个表“A”,在其中,对于每一个“流失”的客户,我们包括他们过去的流失日期。

  • 创建另一个表“B”,其中,对于每个客户,在每月 20 号,我们根据过去 30 天的数据计算 KPI。换句话说,我们每月 20 号对客户指标进行月度快照。

  • 我们将表“A”和“B”按客户 ID 连接,并标记所有将在下一个观察日期流失的行。

这些观察期和 KPI 通常在数据仓库中计算,然后导出到 Python。我为项目模拟的数据正好代表了这种情况。假设我们刚刚从数据仓库中获得了以下数据集:

虚拟流失指标 | 图片由作者提供

(注意:这是一个模拟数据集。所有连续指标都从一个多变量高斯分布中提取,*似真实数据。这就是为什么我们有负值和应不为负值或小数的 KPI 的原因。此外,每一行应对应一个客户 ID,但此信息并不相关)。

2.2 分析流失指标

一旦我们拥有一些指标,我们可以开始检查它们与流失的关系。

最直观的方式来调查这种关系是通过队列分析。通常,通过将每个指标数据拆分成 10 个相等大小的桶来生成 10 个队列,具体取决于它们的值。然后,我们通过计算每个队列中的流失率,将每个指标与“is_churn”标志相关联。如果指标不是连续的且具有少于 10 个分类值,那么我们只考虑每个类别一个队列。

在左侧图表中,我们可以看到,*均而言,拥有更高 mrr_ratio 的客户流失更多,因为他们每个订阅支付更多:

流失指标队列 | 作者提供的图像

每当我们看到这样的行为,具有逻辑意义,并且根据指标值,我们看到流失有显著差异时,我们可以期望该指标在我们的分析中是相关的。

相反,如果我们看到指标中,无论队列的*均值如何,对流失没有影响(例如水*线),那么我们可能会考虑将该指标从模型中排除。

3. 机器学习中的流失预测

我们现在将使用数据集来预测流失。

请注意,流失的预测是不简单的。决定流失是主观的,而且可能并不总是一个逻辑选择:一个客户可能因为费用问题而流失,其他客户可能因为质量问题而流失。此外,糟糕的客户服务或对产品/品牌的负面感受也可能主观地引发流失决定。

基于这些原因,模型的表现不会像其他机器学习任务那样高。根据 Carl S. Gold [1]的说法,一个健康的流失预测模型的 AUC 得分应在 0.6 到 0.8 之间。

需要考虑的一些因素:

  • 流失是一个二分类任务:模型将学习预测记录是否属于类 1(流失客户)或类 0(未流失)。然而,我们将关注每条记录属于每个类别的概率。在选择模型时,请记住这一点。

  • 模型表现不能通过准确率来衡量。通常,少数客户流失,因此我们的数据集是不*衡的:仅约 10%的虚拟数据属于类 1(流失客户)。任何总是预测类 0 的模型将具有 90%的准确率,但这样的模型完全没有帮助。相反,我们将使用roc_auc 得分来衡量性能。

  • 我们将使用交叉验证来调整模型的超参数。由于我们处理的是时间序列数据集,我们不能简单地使用随机记录分配到每个折叠。我们需要训练我们的模型使用当前或过去的数据,而不是未来的数据。因此,最佳实践建议使用时间序列分割(来自sklearn [2]),它适用于任何按时间排序的数据集。

(注意:在交叉验证中,通常使用 10 个拆分。这里由于数据量有限和数据对类 0 极度不*衡,使用了 3 个拆分)。

现在让我们比较三种分类模型。

3.1 逻辑回归

逻辑回归是一个广义的线性回归模型,这是一种非常常见的分类技术,尤其用于二分类问题。由于它是一个回归模型,许多假设需要事先验证;例如,我们不应违反“无多重共线性”假设,这意味着我们需要确保没有特征是相关的,即每个特征应提供独特且独立的信息。

尽管这很容易验证,我可以预见逻辑回归不会是性能最好的模型,因此我们不会使用我们可能获得的任何无效结果。

逻辑回归 | 作者插图

3.2 随机森林

随机森林是一种基于树的集成方法。

基于树的方法 是非常强大的分类(或回归)算法,它们通过根据多个决策节点来划分我们的训练数据。每个决策节点根据特定特征执行一次“划分”,做出 True / False 决策。划分决策的确定方式是为了在树的下一层尽可能减少我们的数据集的“熵”。熵是数据无序程度的度量,它与我们在分类/回归任务中可以获得的“信息增益”相关。

  • 例如,在一个二分类问题中,如果我们注意到通过根据一个特征来划分数据,我们得到的每个 True/False 结果分支中——95% 的数据属于一个类别,5% 的数据属于另一个类别,那么我们就成功地从数据中获得了更多的信息,降低了数据的无序程度或“熵”。

随机森林(RF) 构建了多个不同的树,然后取这些树的*均值或最频繁的结果来做最终预测。RF 确保每棵树与其他树的构建方式不同,这得益于两种方法:

  • Bagging(自助聚合):每棵树都是通过使用整个训练集的样本进行训练的,因此每棵树都是使用不同的数据构建的。

  • 特征随机性:每棵树都是通过限制可用特征来构建的,使用所有可用特征的一个子集。

现在让我们在数据集上调整 RF 的超参数,选择最佳模型,并展示最“重要”的特征(即每个特征用于生成决策分裂的频率):

随机森林 | 作者插图

3.3 XGBoost

XGBoost 代表极端梯度提升,它是另一种基于树的集成技术,与 RF 类似,允许将多个决策树的预测结果进行结合。

XGBoost 是“梯度提升”方法的一个进化(“极端”)版本。因此,为了说明 XGBoost,让我们分别考察这两个方面。

  • 在“梯度提升”方法中,与随机森林(RF)不同,构建的树之间有很大关联。预测是由“弱学习者”(即简单树)做出的,这些树会不断改进。通常,初始预测是目标值的*均值,然后通过创建新树进行精炼。每棵新树是基于前一棵树的错误构建的:因此,从前一轮“弱学习者”的残差/错误预测开始,建立新树,最小化成本函数,并对产生错误的属性分配更多权重。最后,通过加权每棵树的结果来组合结果。

  • 从“梯度提升”开始,“极端梯度提升”是一个完整的算法,包括对梯度提升方法的几项改进,如性能优化和正则化参数(可以避免过拟合)。最重要的是,得益于这些附加元素,XGBoost 可以在像普通笔记本电脑这样的简单机器上运行。

4. 生成流失预测

表现最好的模型是 XGBoost,我们将使用它来预测 测试集(包含在训练阶段未使用的新记录)的流失概率。

在导入测试集后,我们计算每条记录属于类别 1(流失客户)的模型预测概率,并绘制 ROC_AUC 分数:

测试集 AUC 分数 | 图片由作者提供

让我们将预测的类别添加到原始数据中。默认情况下,所有预测概率 ≥ .5 的记录将被分配到类别 1。我们可以降低这个阈值,并比较结果的混淆矩阵:

混淆矩阵 | 图片由作者提供

通过降低阈值,我们可以识别更多的流失客户(真正的正例和假阳性),但仍有相当数量的客户会流失但我们未能识别(假阴性),尽管我们的 xgboost 模型表现良好。

我们可以尝试找到更好的模型,但预测流失通常很困难。因此,除了使用简单的流失与非流失区分外,一个想法是利用我们预测的概率来定义一些不同的留存策略:

  • 对于预测概率大于 .75 的客户 = 高风险流失,我们可以设计一种“强力”的留存策略。由于我们预期的假阳性很少,因此我们可以更有信心地对这些客户进行投资。

  • 预测概率在 .5 和 .75 之间的客户 = 中等流失风险和“中等”留存策略。

  • 预测概率在 .25 和 .5 之间的客户 = 低风险流失和“弱”留存策略。

5. 下一步

在这个阶段,我们应该有一个能够为任何新数据分配“流失概率”的工作模型。

我们分析的下一步是进一步定义前面提到的保留策略。我们的策略应包括:(a)可能导致流失减少的行动;(b)如何衡量我们行动的成功;(c)最后,推广计划。

这里有一些解决这些问题的想法:

确定导致流失减少的行动:

让我们结合上面看到的特征重要性与我们的预测。例如,两个基于树的模型将“subs_B”列为树中使用最多的特征。我们需要深入了解流失和非流失客户在 subs_B 方面的情况。之前看到的群体分析将有助于这里:

在训练数据上进行群体分析 | 作者图像

看起来高流失的客户有最低值(即 0 订阅,数据已经被转换,因此 x 轴值在这里不太易于解释),或者“subs_B”的数量过多。我们必须小心地得出“subs_B”和“is_churn”之间的因果结论,因为此分析并未证明任何因果关系。然而,我们可以测试一些假设:

  • 看起来客户对我们的 B 产品感到满意,将“B”产品交叉销售给仅拥有 A 产品的客户,是否有助于减少流失?

  • 我们还应该了解客户拥有这么多“B”订阅背后的业务原因。我们可以教育他们更有效地使用我们的产品,从而减少 B 订阅。

如何衡量我们行动的成功

一旦我们确定了一些建议的行动,我们可以规划我们的测量方法。

A/B 测试是一种非常常见的方式:

  • 我们从具有类似预测流失概率的客户中创建两个可比样本。一个样本将代表我们的处理组,并将暴露于我们的流失减少策略,另一个样本将代表我们的对照组,不会暴露于任何保留行动。

  • 我们希望证明我们的处理组的流失率显著低于对照组。

推广计划:

在建议保留行动时,我们不应忘记考虑其他背景因素。举几个例子:流失的担忧程度如何?(即是否有大量新客户以弥补流失?)解决问题的预算是多少?我们应该等多久才能看到结果?我们可以使用其他数据来改善模型吗?已经做了哪些工作?

这将帮助我们了解我们建议的可行性。

谢谢阅读!!

参考文献

[1] Carl S. Gold — “用数据对抗流失:客户保留的科学与策略”,2020 年

[2] Scikit-learn: Python 中的机器学习,Pedregosa ,JMLR 12,第 2825–2830 页,2011 年

构建生产就绪特征工程管道的框架

原文:towardsdatascience.com/a-framework-for-building-a-production-ready-feature-engineering-pipeline-f0b29609b20f

全栈 7 步 MLOps 框架

课程 1: 批量服务。特征存储。特征工程管道。

Paul IusztinTowards Data Science Paul Iusztin

·发表于 Towards Data Science ·13 分钟阅读·2023 年 4 月 28 日

--

Hassan PashaUnsplash 上的照片

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

完成本课程后,你将了解使用批量服务架构设计、编码和部署 ML 系统的所有基本知识。

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

如今,证书随处可见。构建先进的端到端项目并展示是获得专业工程师认可的最佳途径。

目录:

  • 课程介绍

  • 课程内容

  • 数据源

  • 课程 1: 批量服务。特征存储。特征工程管道。

  • 课程 1: 代码

  • 结论

  • 参考文献

介绍

在这 7 课时的课程结束时,你将学会如何:

  • 设计批量服务架构

  • 使用 Hopsworks 作为特征存储

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

  • 构建带有超参数调优的训练管道

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

  • 实现批量预测管道

  • 使用 Poetry 构建自己的 Python 包

  • 部署自己的私人 PyPi 服务器

  • 使用 Airflow 协调一切

  • 使用预测结果编码一个使用 FastAPI 和 Streamlit 的 Web 应用

  • 使用 Docker 容器化你的代码

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

  • 监控预测性能的变化

  • 将所有内容部署到 GCP

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

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

如果你想最大化本课程的收益, 我建议你访问包含所有课程代码的 GitHub 仓库 。我设计了这些文章,使你在阅读课程的同时可以阅读并运行代码。

到课程结束时,你将学会如何实现下面的图示。如果有些内容对你来说不太明白,不用担心。我会详细解释一切。

课程中你将构建的架构图 [图示来源于作者]。

为什么批量服务?

模型的部署主要有 4 种类型:

  • 批量服务

  • request-response

  • 流式处理

  • 嵌入式

批量服务是获取实际操作经验的绝佳起点,因为大多数 AI 应用程序从使用批量架构开始,然后转向请求响应或流式处理。

课程内容:

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

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

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

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

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

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

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

  8. [附加] ‘不完美’ ML 项目的幕后——教训与见解

数据源:

我们使用了一个开放 API,提供丹麦所有能源消费者类型的每小时能源消耗值。

他们提供了一个直观的界面,你可以轻松查询和可视化数据。你可以在这里访问数据 [1]。

数据有 4 个主要属性:

  • 小时 UTC: 观察到数据点时的 UTC 日期时间。

  • 价格区域: 丹麦被划分为两个价格区域:DK1 和 DK2——由大贝尔特海峡分隔。DK1 位于大贝尔特的西侧,DK2 位于东侧。

  • 消费者类型: 消费者类型为工业代码 DE35,由丹麦能源公司拥有和维护。

  • 总消耗: 总电力消耗(kWh)

注意: 观察值有 15 天的滞后!但对于我们的演示用例,这不是问题,因为我们可以模拟与实时相同的步骤。

应用程序中的截图展示了我们如何预测区域 = 1 和消费者类型 = 212 的能源消耗 [作者提供的图片]。

数据点具有每小时的分辨率。例如:“2023–04–15 21:00Z”,“2023–04–15 20:00Z”,“2023–04–15 19:00Z”等等。

我们将把数据建模为多个时间序列。每个独特的价格区域消费者类型元组表示其独特的时间序列。

因此,我们将构建一个模型,独立预测每个时间序列接下来 24 小时的能源消耗。

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

课程与数据源概览 [作者提供的视频]。

第 1 课:批量服务。特征存储。特征工程管道。

第 1 课的目标

在第 1 课中,我们将关注图中蓝色突出显示的组件:“API”,“特征工程”和“特征存储”。

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

具体来说,我们将构建一个 ETL 管道,从能源消耗 API 中提取数据,经过特征工程管道,该管道清洗和转换特征,并将特征加载到特征存储中,以便在系统中进一步使用。

如你所见,特征存储站在系统的核心位置。

理论概念与工具

批量服务: 在批量服务模式中,你可以离线准备数据、训练模型并进行预测。之后,你将预测结果存储在数据库中,客户端/应用程序将在后续使用这些预测结果。批量这个词来源于你可以同时处理多个样本,这在这种模式下通常是有效的。我们计算了所有预测结果并将其存储在 blob 存储/桶中。

如果我们将架构过于简化,仅反映批量架构的主要步骤,它将如下所示 👇

批处理架构 [作者提供的图片]。

批处理服务范式的最大缺点是你的预测几乎总是会滞后。例如,在我们的案例中,我们预测未来 24 小时的能耗,由于这种滞后,我们的预测可能会迟到 1 小时。

查看这篇文章以了解更多关于Google Cloud 建议的 标准化架构,这在几乎任何机器学习系统中都可以利用。

特征存储:特征存储位于任何机器学习系统的核心。使用特征存储,你可以轻松地存储和共享系统中的特征。你可以直观地将特征存储视为一个高级数据库,增加以下功能:

  • 数据版本控制和血缘

  • 数据验证

  • 创建数据集的能力

  • 保存训练/验证/测试拆分的能力

  • 两种存储类型:离线(便宜,但延迟高)和在线(更贵,但延迟低)。

  • 时间旅行:在给定时间窗口内轻松访问数据

  • 除了特征本身外,还保存特征转换

  • 数据监控等……

如果你想阅读关于特征存储的内容,请查看这篇文章 [3]。

我们选择了Hopsworks作为我们的特征存储,因为它是无服务器的,并提供了慷慨的免费计划,这足以创建本课程。

此外,Hopsworks 设计非常优秀,并提供了上述所有功能。如果你在寻找无服务器的特征存储,我推荐他们。

如果你还想在阅读本课程时运行代码,你需要去Hopswork,创建一个账户和项目。所有其他步骤将在课程的其余部分中解释。

我确保了本课程中的所有步骤都能保留在他们的免费计划中。因此,它不会花费你任何$$$。

特征工程管道:读取来自一个或多个数据源的数据,清洗、转换、验证数据并将其加载到特征存储中的代码片段(基本上是 ETL 管道)。

Pandas vs. Spark:我们在本课程中选择使用 Pandas 作为数据处理库,因为数据较小。因此,它可以轻松地适应计算机的内存,使用如 Spark 这样的分布式计算框架会使一切变得过于复杂。但在许多现实世界的场景中,当数据太大无法适应单台计算机(即大数据)时,你将使用 Spark(或其他分布式计算工具)来完成与本课程相同的步骤。查看这篇文章以了解 Spark 如何处理大数据预测流失。

课程 1: 代码

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

注意: 所有安装说明都在仓库的 README 文件中。这里我们将直接进入代码部分。

Lesson 1 中的所有代码都位于 feature-pipeline文件夹下。

feature-pipeline文件夹下的文件结构如下:

显示 feature-pipeline 文件夹结构的截图[作者提供]。

所有代码都位于feature_pipeline目录下(注意是"_"而不是"-")

准备凭证

在本课程中,你将使用一个单一服务作为你的特征存储:Hopsworks(在我们的用例中,它将是免费的)。

Hopsworks上创建一个账户和一个新项目(或使用默认项目)。注意不要将你的项目命名为“energy_consumption”,因为 Hopsworks 要求在其无服务器部署中项目名称唯一。

现在,你需要一个来自HopsworksAPI_KEY来登录并使用他们的 Python 模块访问云资源。

直接在你的 git 仓库中存储凭证是一个巨大的安全隐患。因此,你将使用.env文件注入敏感信息。.env.default是你必须配置的所有变量的示例。

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

从你的feature-pipeline目录中,在终端中运行:

cp .env.default .env

…并在FS_API_KEY变量下填写你新生成的 Hopsworks API KEY,在FS_PROJECT_NAME变量下填写你的 Hopsworks 项目名称(在我们的例子中,它是“energy_consumption”)。

查看下图,了解如何获取你自己的 Hopsworks API KEY 👇

进入你的Hopsworks项目。然后,在右上角点击你的用户名,再点击“账户设置”。最后,点击“新建 API KEY”,设置一个名称,选择所有作用域,点击“创建 API KEY”,复制 API KEY,你就完成了。你已经拥有了 Hopsworks API KEY[作者提供]。

然后,在feature_pipeline/settings.py文件中,我们将使用老牌的dotenv Python 包从.env文件中加载所有变量。

如果你想从当前目录以外的地方加载.env文件,你可以在运行脚本时导出ML_PIPELINE_ROOT_DIR环境变量。这是一个指向其余配置文件的"HOME"环境变量。

我们还将使用ML_PIPELINE_ROOT_DIR环境变量来指向一个单一目录,从中加载.env文件,并在所有进程中读写数据。

这是一个如何使用ML_PIPELINE_ROOT_DIR变量的示例:

export ML_PIPELINE_ROOT_DIR=/my/awesome/path python -m feature_pipeline.pipeline

使用以下代码,我们将通过 SETTINGS 字典访问代码中的所有凭据/敏感信息。

ETL 代码

feature_pipeline/pipeline.py 文件中,我们在run()方法下有管道的主要入口点。

如下所示,run 方法在高层次上遵循了 ETL 管道的确切步骤:

  1. extract.from_api() — 从能源消耗 API 提取数据。

  2. transform() — 转换提取的数据。

  3. validation.build_expectation_suite() — 构建数据验证和完整性套件。忽略这一步,因为我们将在第 6 课中重点讲解。

  4. load.to_feature_store() — 将数据加载到特征存储中。

请注意我如何使用日志记录器来反映系统的当前状态。当你的程序部署并全天候运行时,详细的日志记录对于调试系统至关重要。此外,总是使用 Python 的日志记录器而不是 print 方法,因为你可以选择不同的日志级别和输出流。

从高层次来看,这似乎很容易理解。让我们分别深入了解每个组件。

#1. 提取

在提取步骤中,我们请求给定窗口长度的数据。窗口的长度将等于days_export。窗口的第一个数据点是export_end_reference_datetime - days_delay - days_export,而窗口的最后一个数据点等于export_end_reference_datetime - days_delay

我们使用了参数days_delay来根据数据的延迟移动窗口。在我们的使用案例中,API 延迟为 15 天。

如上所述,该函数向 API 发出 HTTP GET 请求以请求数据。随后,响应被解码并加载到 Pandas DataFrame 中。

该函数返回 DataFrame 以及包含有关数据提取信息的附加元数据。

#2. 转换

转换步骤将原始 DataFrame 进行如下转换:

  • 将列重命名为 Python 标准化格式

  • 将列转换为其适合的类型

  • 将字符串列编码为整数

注意我们没有包括 EDA 步骤(例如,查找空值),因为我们的主要关注点是设计系统,而不是标准的数据科学过程。

#3. 数据验证

这是我们确保数据符合预期的地方。在我们的案例中,基于我们的 EDA 和转换,我们期望:

  • 数据中没有任何空值

  • 列的类型如预期

  • 值的范围如预期

有关此主题的更多内容,请参见第 6 课。

#4. 加载

这是我们将处理后的 DataFrame 加载到特征存储中的地方。

Hopsworks 有一系列很棒的教程,你可以在这里查看。但让我解释一下发生了什么:

  • 我们使用 API_KEY 登录到我们的 Hopsworks 项目中。

  • 我们获取特征存储的引用。

  • 我们获取或创建一个特征组,这基本上是一个数据库表,上面附加了特征存储的所有优点(更多信息请见这里 [5])。

  • 我们插入新的处理数据样本。

  • 我们为数据中的每个特征添加一组特征描述。

  • 我们指示 Hopsworks 为每个特征计算统计信息。

查看下面的视频,看看我刚才解释的内容在 Hopsworks 中是什么样的 👇

Hopsworks 概述[作者的视频]。

太棒了!现在我们有了一个 Python ETL 脚本,它从能耗 API 中提取数据,并将其加载到特征存储中。

创建特征视图与训练数据集

最后一步是创建一个特征视图和训练数据集,稍后将被引入训练管道中。

注意: 特征管道是唯一一个对特征存储进行写操作的过程。其他组件仅会查询特征存储中的各种数据集。通过这样做,我们可以安全地将特征存储作为唯一的真实来源,并在系统中共享特征。

feature_pipeline/feature_view.py文件中,我们有一个create()方法,它运行以下逻辑:

  1. 我们从特征管道中加载元数据。请记住,FE 元数据包含提取窗口的开始和结束时间、特征组的版本等。

  2. 我们登录 Hopswork 项目并创建对特征存储的引用。

  3. 我们删除所有旧的特征视图(通常,你不需要执行这一步。正好相反,你会希望保留旧的数据集。但是,Hopwork 的免费版本限制你只能使用 100 个特征视图。因此,我们想要保留我们的免费版本)。

  4. 我们根据给定版本获取特征组。

  5. 我们使用从加载的特征组中得到的所有数据创建一个特征视图。

  6. 我们仅使用给定的时间窗口创建训练数据集。

  7. 我们创建元数据的快照并保存到磁盘。

注意: 特征视图是一种将多个特征组组合成一个“数据集”的智能方法。它类似于 SQL 数据库中的 VIEW。你可以在这里 [4]了解更多关于特征视图的信息。

就这样。你建立了一个特征管道,它提取、转换并加载数据到特征存储中。基于特征存储中的数据,你创建了一个特征视图和训练数据集,这些将作为系统中的唯一真实来源。

注意: 你需要良好的软件工程原则和模式知识来构建健壮的特征工程管道。你可以在这里阅读一些实践示例

重要的设计决策

正如你所看到的,我们在这节课中实际上没有计算任何特征。我们只是清理、验证并确保数据已经准备好用于系统中。

但这被称为“特征管道”,为什么我们没有计算任何特征呢?

让我解释一下。

特征 = 原始数据 + 转换函数

如果我们将原始数据和转换函数存储在特征库中,而不是计算和存储特征,会怎样呢?

这样做我们可以获得以下好处:

  • 更快的实验,因为数据科学家不需要请求数据工程师计算新特征。他只需将新转换添加到特征库中。

  • 你可以节省大量存储空间。例如,与其保存 5 个从同一原始数据列计算出的特征,不如只保存原始数据列和 5 个转换,这样只使用原来的 1/5 的空间。

使用这种方法的缺点:

  • 你的特征将在运行时通过云端或推理管道进行计算。因此,你将在运行时增加额外的延迟。

但在使用批量服务范式时,延迟不是一个显著的限制。因此,我们确实这样做了!

查看第 2 课 以了解我们如何建模时间序列以预测接下来 24 小时的能源消耗。在 第 2 课 中,我们将展示如何将转换直接存储在特征库中。

结论

恭喜!你完成了第一课来自全栈 7 步 MLOps 框架课程。

你了解了如何设计批量服务架构以及开发自己的 ETL 管道,这些管道:

  • 从 HTTP API 中提取数据

  • 清理数据

  • 转换数据

  • 将数据加载到特征库中

  • 创建一个新的训练数据集版本

现在你已经理解了使用特征库的强大功能及其对任何 ML 系统的重要性,你可以在几周内而不是几个月内部署你的模型。

查看第 2 课 以了解有关训练管道、机器学习*台和超参数调整的信息。

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

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

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

[## 通过我的推荐链接加入 Medium - 保罗·伊斯津]

🤖 加入以获取关于设计和构建生产级机器学习系统的独家内容 🚀 解锁完整访问权限…

pauliusztin.medium.com

参考文献

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

[2] Hopsworks 教程,Hopsworks 文档

[3] 吉姆·道林,特征存储与数据仓库(2020 年),KDnuggets

[4] Hopsworks 特征视图,Hopsworks 文档

[5] Hopsworks 特征组,Hopsworks 文档

《温和介绍:通过 LangChain 链接 LLMs、代理和工具》

原文:towardsdatascience.com/a-gentle-intro-to-chaining-llms-agents-and-utils-via-langchain-16cd385fca81?source=collection_archive---------0-----------------------#2023-04-21

#初学者的 LLM

理解代理、工具和提示的基础知识以及一些学习经验

Varshita Sher 博士Towards Data Science Varshita Sher 博士

·

关注 发布于 Towards Data Science ·20 分钟阅读·2023 年 4 月 21 日

--

受众:对于那些被庞大(但卓越)库感到不知所措的人…

作者使用DALL.E 2生成的图像

介绍

如果我说我掌握了整个 LangChain 库,那我就是在撒谎——实际上,我远远没有做到。但是,围绕它的热议足以让我摆脱写作 hiatus,去尝试一下 🚀。

最初的动机是看看 LangChain 在实践中添加了什么(在实际水*上),这使它不同于上个月我用openai包中的ChatCompletion.create()函数构建的聊天机器人。在这样做的过程中,我意识到需要先理解 LangChain 的基础构建块,然后再转向更复杂的部分。

这就是本文所做的事情。请注意,随着我对这个库的着迷和持续探索,将会有更多的部分出现。

让我们从理解 LangChain 的基本构建块 —— 即链条开始。如果你想跟进,请查看这个GitHub 仓库

LangChain 中的链条是什么?

链条是通过以逻辑方式连接一个或多个大型语言模型(LLMs)而得到的。 (虽然链条可以由除 LLMs 以外的实体构建,但现在让我们暂时使用这个定义以简化问题)。

OpenAI 是一种 LLM(提供者),你可以使用它,但还有其他像 Cohere、Bloom、Huggingface 等。

注意:几乎所有这些 LLM 提供者都需要您申请 API 密钥才能使用它们。所以请确保在继续阅读本博客的其余部分之前,您已经这样做了。例如:

import os
os.environ["OPENAI_API_KEY"] = "..."

P.S. 我将在本教程中使用 OpenAI,因为我有一个一个月后过期的积分密钥,但请随意替换为任何其他 LLM。无论如何,这里涵盖的概念都将是有用的。

链条可以简单(例如通用)或专业化(例如实用)。

  1. 通用 — 单个 LLM 是最简单的链条。它接受一个输入提示和 LLM 的名称,然后使用 LLM 进行文本生成(即输出提示的结果)。这里是一个例子:

让我们构建一个基本的链条 —— 创建一个提示并获取预测结果

在 Lanchain 中,使用PromptTemplate创建提示(Prompt)有点花哨,但这可能是因为根据用例的不同,可以有多种不同的方式来创建提示(我们将涵盖AIMessagePromptTemplate等等)。

HumanMessagePromptTemplate等等将在下一篇博客文章中涵盖。现在先看一个简单的例子:

from langchain.prompts import PromptTemplate

prompt = PromptTemplate(
    input_variables=["product"],
    template="What is a good name for a company that makes {product}?",
)

print(prompt.format(product="podcast player"))

# OUTPUT
# What is a good name for a company that makes podcast player?

注意:如果您需要多个 *input_variables*,例如:* *input_variables=["product", "audience"]* 用于模板,例如 *“一个公司的好名字,为{product}制作{audience}”*,则需要执行* print(prompt.format(product="podcast player", audience="children”) *以获取更新后的提示。

一旦您建立了一个提示,我们就可以调用所需的 LLM。为此,我们创建一个LLMChain实例(在我们的例子中,我们使用OpenAI的大型语言模型text-davinci-003)。要获取预测结果(即 AI 生成的文本),我们使用run函数和product的名称。

from langchain.llms import OpenAI
from langchain.chains import LLMChain

llm = OpenAI(
          model_name="text-davinci-003", # default model
          temperature=0.9) #temperature dictates how whacky the output should be
llmchain = LLMChain(llm=llm, prompt=prompt)
llmchain.run("podcast player")

# OUTPUT
# PodConneXion

如果你有多个输入变量,那么就不能使用run。相反,你需要将所有变量作为dict传递。例如,llmchain({"product": "podcast player", "audience": "children"})

注意 1:根据 OpenAI*davinci* 文本生成模型的费用是其聊天对应模型的 10 倍,即 *gpt-3.5-turbo*,因此我尝试从文本模型切换到聊天模型(即从 *OpenAI* *ChatOpenAI*),结果差别不大。

注意 2:你可能会看到一些教程使用 *OpenAIChat*而不是 *ChatOpenAI*。前者已经 弃用 并且将不再受支持,我们应使用 *ChatOpenAI*

from langchain.chat_models import ChatOpenAI

chatopenai = ChatOpenAI(
                model_name="gpt-3.5-turbo")
llmchain_chat = LLMChain(llm=chatopenai, prompt=prompt)
llmchain_chat.run("podcast player")

# OUTPUT
# PodcastStream

这部分关于简单链的介绍到此为止。需要注意的是,我们很少将通用链作为独立链使用。更常见的是它们作为实用链的构建块(正如我们接下来会看到的)。

2. 实用工具 — 这些是专门的链,由许多 LLM 组成,以帮助解决特定任务。例如,LangChain 支持一些端到端的链(如[AnalyzeDocumentChain](https://python.langchain.com/docs/use_cases/question_answering/how_to/analyze_document) 用于总结、问答等)和一些特定的链(如[GraphQnAChain](https://python.langchain.com/en/latest/modules/chains/index_examples/graph_qa.html#querying-the-graph) 用于创建、查询和保存图形)。在本教程中,我们将深入探讨一个名为 PalChain 的特定链。

PAL 代表 程序辅助语言模型PALChain 读取复杂的数学问题(用自然语言描述)并生成程序(用于解决数学问题)作为中间推理步骤,但将解决步骤委托给如 Python 解释器等运行时。

为了确认这一点,我们可以检查基础代码中的 _call() 这里。在底层,我们可以看到这个链:

附注:检查 *_call()* *base.py* 中是一个好习惯,可以查看 LangChain 中的任何链如何在底层工作。

from langchain.chains import PALChain
palchain = PALChain.from_math_prompt(llm=llm, verbose=True)
palchain.run("If my age is half of my dad's age and he is going to be 60 next year, what is my current age?")

# OUTPUT
# > Entering new PALChain chain...
# def solution():
#    """If my age is half of my dad's age and he is going to be 60 next year, what is my current age?"""
#    dad_age_next_year = 60
#    dad_age_now = dad_age_next_year - 1
#    my_age_now = dad_age_now / 2
#    result = my_age_now
#    return result
#
# > Finished chain.
# '29.5'

注意 1:如果你不需要看到中间步骤,*verbose* 可以设置为 *False*。*

现在,有些人可能会想 — 但提示呢?我们肯定没有像我们建立的通用 *llmchain* 那样传递它。 实际上,当使用.from_math_prompt()时,它会自动加载。您可以使用palchain.prompt.template检查默认提示,或者直接查看提示文件这里

print(palchain.prompt.template)
# OUTPUT
# 'Q: Olivia has $23\. She bought five bagels for $3 each. How much money does she have left?\n\n# solution in Python:\n\n\ndef solution():\n    """Olivia has $23\. She bought five bagels for $3 each. How much money does she have left?"""\n    money_initial = 23\n    bagels = 5\n    bagel_cost = 3\n    money_spent = bagels * bagel_cost\n    money_left = money_initial - money_spent\n    result = money_left\n    return result\n\n\n\n\n\nQ: Michael had 58 golf balls. On tuesday, he lost 23 golf balls. On wednesday, he lost 2 more. How many golf balls did he have at the end of wednesday?\n\n# solution in Python:\n\n\ndef solution():\n    """Michael had 58 golf balls. On tuesday, he lost 23 golf balls. On wednesday, he lost 2 more. How many golf balls did he have at the end of wednesday?"""\n    golf_balls_initial = 58\n    golf_balls_lost_tuesday = 23\n    golf_balls_lost_wednesday = 2\n    golf_balls_left = golf_balls_initial - golf_balls_lost_tuesday - golf_balls_lost_wednesday\n    result = golf_balls_left\n    return result\n\n\n\n\n\nQ: There were nine computers in the server room. Five more computers were installed each day, from monday to thursday. How many computers are now in the server room?\n\n# solution in Python:\n\n\ndef solution():\n    """There were nine computers in the server room. Five more computers were installed each day, from monday to thursday. How many computers are now in the server room?"""\n    computers_initial = 9\n    computers_per_day = 5\n    num_days = 4  # 4 days between monday and thursday\n    computers_added = computers_per_day * num_days\n    computers_total = computers_initial + computers_added\n    result = computers_total\n    return result\n\n\n\n\n\nQ: Shawn has five toys. For Christmas, he got two toys each from his mom and dad. How many toys does he have now?\n\n# solution in Python:\n\n\ndef solution():\n    """Shawn has five toys. For Christmas, he got two toys each from his mom and dad. How many toys does he have now?"""\n    toys_initial = 5\n    mom_toys = 2\n    dad_toys = 2\n    total_received = mom_toys + dad_toys\n    total_toys = toys_initial + total_received\n    result = total_toys\n    return result\n\n\n\n\n\nQ: Jason had 20 lollipops. He gave Denny some lollipops. Now Jason has 12 lollipops. How many lollipops did Jason give to Denny?\n\n# solution in Python:\n\n\ndef solution():\n    """Jason had 20 lollipops. He gave Denny some lollipops. Now Jason has 12 lollipops. How many lollipops did Jason give to Denny?"""\n    jason_lollipops_initial = 20\n    jason_lollipops_after = 12\n    denny_lollipops = jason_lollipops_initial - jason_lollipops_after\n    result = denny_lollipops\n    return result\n\n\n\n\n\nQ: Leah had 32 chocolates and her sister had 42\. If they ate 35, how many pieces do they have left in total?\n\n# solution in Python:\n\n\ndef solution():\n    """Leah had 32 chocolates and her sister had 42\. If they ate 35, how many pieces do they have left in total?"""\n    leah_chocolates = 32\n    sister_chocolates = 42\n    total_chocolates = leah_chocolates + sister_chocolates\n    chocolates_eaten = 35\n    chocolates_left = total_chocolates - chocolates_eaten\n    result = chocolates_left\n    return result\n\n\n\n\n\nQ: If there are 3 cars in the parking lot and 2 more cars arrive, how many cars are in the parking lot?\n\n# solution in Python:\n\n\ndef solution():\n    """If there are 3 cars in the parking lot and 2 more cars arrive, how many cars are in the parking lot?"""\n    cars_initial = 3\n    cars_arrived = 2\n    total_cars = cars_initial + cars_arrived\n    result = total_cars\n    return result\n\n\n\n\n\nQ: There are 15 trees in the grove. Grove workers will plant trees in the grove today. After they are done, there will be 21 trees. How many trees did the grove workers plant today?\n\n# solution in Python:\n\n\ndef solution():\n    """There are 15 trees in the grove. Grove workers will plant trees in the grove today. After they are done, there will be 21 trees. How many trees did the grove workers plant today?"""\n    trees_initial = 15\n    trees_after = 21\n    trees_added = trees_after - trees_initial\n    result = trees_added\n    return result\n\n\n\n\n\nQ: {question}\n\n# solution in Python:\n\n\n'

注意:大多数实用链条的提示作为库的一部分是预定义的(在这里查看 这里)。它们有时非常详细(即:有很多令牌),因此在成本和 LLM 响应质量之间肯定存在权衡。

是否存在不需要 LLM 和提示的链条?

尽管 PalChain 需要一个 LLM(以及相应的提示)来解析用户用自然语言编写的问题,但在 LangChain 中有一些链条不需要。这些主要是预处理提示的转换链条,例如删除额外的空格,然后将其输入 LLM。你可以在另一个例子中看到 这里

我们能进入精彩部分并开始创建链条吗?

当然可以!我们已经有了开始逻辑地将 LLM 连接在一起的基本构建块。为此,我们将使用SimpleSequentialChain

文档中有一些很好的例子,例如,你可以在这里看到如何组合两个链条,其中链条#1 用于清理提示(删除额外空格,缩短提示等),而链条#2 用于使用这个干净的提示调用 LLM。这里还有另一个例子,其中链条#1 用于为一部戏剧生成简介,而链条#2 则用于基于此简介撰写评论。

虽然这些都是很好的例子,但我想专注于其他事情。如果你还记得,我提到链条可以由除了 LLM 以外的实体组成。更具体地说,我对将代理和 LLM 组合在一起很感兴趣。但首先,什么是代理?

使用代理动态调用 LLM

对于解释代理的作用与其是什么,将会更加容易。

假设我们想知道明天的天气预报。如果我们使用简单的 ChatGPT API 并给它一个提示Show me the weather for tomorrow in London,它不会知道答案,因为它无法访问实时数据。

如果我们能有一个安排,利用 LLM 理解我们的查询(即提示),然后代表我们调用天气 API 来获取所需数据,那不是很有用吗?这正是代理所做的(当然还有其他事情)。

代理可以访问 LLM 和一套工具,例如 Google 搜索、Python REPL、数学计算器、天气 API 等。

LangChain 支持很多代理——完整列表请见这里,但坦率说,我在教程和 YouTube 视频中最常见的代理是 zero-shot-react-description。这个代理使用了ReAct(Reason + Act)框架,根据输入查询从工具列表中选择最合适的工具。

P.S.: 这里 有一篇深入探讨 ReAct 框架的好文章。

让我们使用 initialize_agent 初始化一个代理,并传递它所需的 toolsLLM。代理可以使用的工具清单可以在这里找到。对于我们的例子,我们使用了上面提到的同一个数学解决工具,叫做 pal-math。这个工具在初始化时需要一个 LLM,因此我们传递给它之前相同的 OpenAI LLM 实例。

from langchain.agents import initialize_agent
from langchain.agents import AgentType
from langchain.agents import load_tools

llm = OpenAI(temperature=0)
tools = load_tools(["pal-math"], llm=llm)

agent = initialize_agent(tools,
                         llm,
                         agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
                         verbose=True)

让我们在上述相同的例子上测试一下:

agent.run("If my age is half of my dad's age and he is going to be 60 next year, what is my current age?")

# OUTPUT
# > Entering new AgentExecutor chain...
# I need to figure out my dad's current age and then divide it by two.
# Action: PAL-MATH
# Action Input: What is my dad's current age if he is going to be 60 next year?
# Observation: 59
# Thought: I now know my dad's current age, so I can divide it by two to get my age.
# Action: Divide 59 by 2
# Action Input: 59/2
# Observation: Divide 59 by 2 is not a valid tool, try another one.
# Thought: I can use PAL-MATH to divide 59 by 2.
# Action: PAL-MATH
# Action Input: Divide 59 by 2
# Observation: 29.5
# Thought: I now know the final answer.
# Final Answer: My current age is 29.5 years old.

# > Finished chain.
# 'My current age is 29.5 years old.'

注意 1:在每一步,你会注意到代理做了三件事之一——它要么有一个 *observation*,要么有一个 *thought*,要么采取一个 *action*。这主要是由于 ReAct 框架和代理使用的相关提示:

print(agent.agent.llm_chain.prompt.template)
# OUTPUT
# Answer the following questions as best you can. You have access to the following tools:
# PAL-MATH: A language model that is really good at solving complex word math problems. Input should be a fully worded hard word math problem.

# Use the following format:

# Question: the input question you must answer
# Thought: you should always think about what to do
# Action: the action to take, should be one of [PAL-MATH]
# Action Input: the input to the action
# Observation: the result of the action
# ... (this Thought/Action/Action Input/Observation can repeat N times)
# Thought: I now know the final answer
# Final Answer: the final answer to the original input question
# Begin!
# Question: {input}
# Thought:{agent_scratchpad}

注意 2:你可能会想,为什么要让代理做 LLM 可以做的事情。一些应用不仅需要一个预定的 LLM/其他工具调用链,可能还需要一个取决于用户输入的未知链 [来源]。在这些类型的链中,有一个“代理”,可以访问一套工具。

例如,* 这是 一个代理的示例,它可以根据问题是指文档 A 还是文档 B,获取正确的文档(从向量存储中)。

为了有趣,我尝试使输入问题更复杂(用 Demi Moore 的年龄作为 Dad 实际年龄的占位符)。

agent.run("My age is half of my dad's age. Next year he is going to be same age as Demi Moore. What is my current age?")

不幸的是,答案有些偏差,因为代理没有使用最新的 Demi Moore 年龄(由于 OpenAI 模型的训练数据截至到 2020 年)。这可以通过包含另一个工具轻松修复——

tools = load_tools([“pal-math”, **"serpapi"**], llm=llm)serpapi 对于回答当前事件的问题非常有用。

注意: 添加尽可能多的相关工具对用户查询是很重要的。使用单一工具的问题在于,即使它不适用于特定的观察/行动步骤,代理也会继续尝试使用相同的工具。

这是另一个你可以使用的工具示例——podcast-api。你需要获取你自己的 API 密钥并将其插入下面的代码中。

 tools = load_tools(["podcast-api"], llm=llm, listen_api_key="...")
agent = initialize_agent(tools,
                         llm,
                         agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
                         verbose=True)

agent.run("Show me episodes for money saving tips.")

# OUTPUT
# > Entering new AgentExecutor chain...
# I should search for podcasts or episodes related to money saving
# Action: Podcast API
# Action Input: Money saving tips
# Observation:  The API call returned 3 podcasts related to money saving tips: The Money Nerds, The Rachel Cruze Show, and The Martin Lewis Podcast. These podcasts offer valuable money saving tips and advice to help people take control of their finances and create a life they love.
# Thought: I now have some options to choose from 
# Final Answer: The Money Nerds, The Rachel Cruze Show, and The Martin Lewis Podcast are great podcast options for money saving tips.

# > Finished chain.

# 'The Money Nerds, The Rachel Cruze Show, and The Martin Lewis Podcast are great podcast options for money saving tips.'

注意 1: 有一个 已知错误 *,在使用这个 API 时你可能会看到,*openai.error.InvalidRequestError: This model’s maximum context length is 4097 tokens, however you requested XXX tokens (XX in your prompt; XX for the completion). Please reduce your prompt; or completion length.* 当 API 返回的响应可能过大时会发生这种情况。为了解决这个问题,文档建议返回更少的搜索结果,例如,通过将问题更新为 "Show me episodes for money saving tips, return only 1 result"

注意 2: 在使用这个工具时,我注意到了一些不一致的地方。响应第一次生成时并不总是完整的,例如,以下是两次连续运行的输入和响应:

输入: “提高法语水*的播客”

回应 1: “学习法语的最佳播客是评价分数最高的那个。”

回应 2: “学习法语的最佳播客是‘FrenchPod101’。”

在底层,这个工具首先使用 LLMChain 来构建 API URL,根据我们的输入指令(类似于 [listen-api.listennotes.com/api/v2/search?q=french&type=podcast&page_size=3](https://listen-api.listennotes.com/api/v2/search?q=french&type=podcast&page_size=3%29))和进行 API 调用。接收到响应后,它使用另一个 LLMChain 来总结响应,以获得对我们原始问题的回答。你可以在这里查看两个 LLMchains 的提示,它们详细描述了这个过程。

我倾向于猜测上述不一致的结果是由于总结步骤造成的,因为我已经通过 Postman 单独调试并测试了由 LLMChain#1 创建的 API URL,并且得到了正确的响应。为了进一步确认我的疑虑,我还对总结链进行了压力测试,作为一个独立链使用了一个空的 API URL,希望它能抛出一个错误,但得到了“发现了‘投资’播客,总共有 3 个结果。” 🤷‍♀ 我很好奇其他人是否在使用这个工具时比我更幸运!

用例 2:结合链创建一个适合年龄的礼物生成器

让我们充分利用代理和顺序链的知识,创建我们自己的顺序链。我们将结合:

  • 链 #1 — 我们刚创建的agent,能够解决数学中的年龄问题

  • 链 #2 — 一个 LLM,它接受一个人的年龄并建议一个适合他们的礼物。

# Chain1 - solve math problem, get the age
chain_one = agent

# Chain2 - suggest age-appropriate gift
template = """You are a gift recommender. Given a person's age,\n
 it is your job to suggest an appropriate gift for them.

Person Age:
{age}
Suggest gift:"""
prompt_template = PromptTemplate(input_variables=["age"], template=template)
chain_two = LLMChain(llm=llm, prompt=prompt_template) 

现在我们已经准备好了两个链,我们可以使用SimpleSequentialChain将它们结合起来。

from langchain.chains import SimpleSequentialChain

overall_chain = SimpleSequentialChain(
                  chains=[chain_one, chain_two],
                  verbose=True)

需要注意几点:

  • 我们不需要为SimpleSequentialChain明确传递input_variablesoutput_variables,因为其基本假设是链 1 的输出作为链 2 的输入。

最后,我们可以用之前的数学问题来运行它:

question = "If my age is half of my dad's age and he is going to be 60 next year, what is my current age?"
overall_chain.run(question)

# OUTPUT
# > Entering new SimpleSequentialChain chain...

# > Entering new AgentExecutor chain...
# I need to figure out my dad's current age and then divide it by two.
# Action: PAL-MATH
# Action Input: What is my dad's current age if he is going to be 60 next year?
# Observation: 59
# Thought: I now know my dad's current age, so I can divide it by two to get my age.
# Action: Divide 59 by 2
# Action Input: 59/2
# Observation: Divide 59 by 2 is not a valid tool, try another one.
# Thought: I need to use PAL-MATH to divide 59 by 2.
# Action: PAL-MATH
# Action Input: Divide 59 by 2
# Observation: 29.5
# Thought: I now know the final answer.
# Final Answer: My current age is 29.5 years old.

# > Finished chain.
# My current age is 29.5 years old.

# Given your age, a great gift would be something that you can use and enjoy now like a nice bottle of wine, a luxury watch, a cookbook, or a gift card to a favorite store or restaurant. Or, you could get something that will last for years like a nice piece of jewelry or a quality leather wallet.

# > Finished chain.
# '\nGiven your age, a great gift would be something that you can use and enjoy now like a nice bottle of wine, a luxury watch, a cookbook, or a gift card to a favorite store or restaurant. Or, you could get something that will last for years like a nice piece of jewelry or a quality leather wallet

有时你可能需要将一些额外的上下文传递给第二个链,而不仅仅是从第一个链接收的内容。例如,我想根据第一个链返回的年龄为礼物设定预算。我们可以使用SimpleMemory来实现。

首先,让我们更新chain_two的提示,并在input_variables中传递一个名为budget的第二个变量。

template = """You are a gift recommender. Given a person's age,\n
 it is your job to suggest an appropriate gift for them. If age is under 10,\n
 the gift should cost no more than {budget} otherwise it should cost atleast 10 times {budget}.

Person Age:
{output}
Suggest gift:"""
prompt_template = PromptTemplate(input_variables=["output", "budget"], template=template)
chain_two = LLMChain(llm=llm, prompt=prompt_template)

如果你比较我们为SimpleSequentialChain准备的template与上述的模板,你会注意到我还将第一个输入的变量名从age更新为output。这是一个关键步骤,如果失败,将在链验证时引发错误 — *缺少必需的输入键:{age},只有 {input, output, budget}*

这是因为链中的第一个实体(即agent)的输出将作为第二个实体(即chain_two)的输入,因此变量名必须匹配 检查agent的输出键时,我们发现输出变量叫做output,因此进行了更新。

print(agent.agent.llm_chain.output_keys)

# OUTPUT
["output"]

接下来,让我们更新我们正在制作的链的类型。我们不能再使用SimpleSequentialChain,因为它仅适用于单输入单输出的情况。由于chain_two现在需要两个input_variables,我们需要使用SequentialChain,它专门处理多个输入和输出。

overall_chain = SequentialChain(
                input_variables=["input"],
                memory=SimpleMemory(memories={"budget": "100 GBP"}),
                chains=[agent, chain_two],
                verbose=True)

需要注意几点:

  • SimpleSequentialChain不同,对于SequentialChain,传递input_variables参数是强制性的。它是一个包含链中第一个实体(即我们案例中的agent)期望的输入变量名称的列表。

    现在,你们中的一些人可能想知道如何知道agent将要使用的输入提示中使用的确切名称。我们确实没有为这个agent(如我们为chain_two所做的那样)编写过提示!事实上,通过检查llm_chain的提示模板,找出它其实非常简单。

print(agent.agent.llm_chain.prompt.template)

# OUTPUT
#Answer the following questions as best you can. You have access to the following tools:

#PAL-MATH: A language model that is really good at solving complex word math problems. Input should be a fully worded hard word math problem.

#Use the following format:

#Question: the input question you must answer
#Thought: you should always think about what to do
#Action: the action to take, should be one of [PAL-MATH]
#Action Input: the input to the action
#Observation: the result of the action
#... (this Thought/Action/Action Input/Observation can repeat N times)
#Thought: I now know the final answer
#Final Answer: the final answer to the original input question

#Begin!

#Question: {input}
#Thought:{agent_scratchpad}

正如您可以在提示的最后看到的那样,最终用户提出的问题存储在一个名为input的输入变量中。如果因某种原因您必须在提示中操纵这个名称,请确保在创建SequentialChain时同时更新input_variables

最后,您可以在不查看整个提示的情况下找到相同的信息:

print(agent.agent.llm_chain.prompt.input_variables)

# OUTPUT
# ['input', 'agent_scratchpad']
  • [SimpleMemory](https://github.com/hwchase17/langchain/blob/master/langchain/memory/simple.py#L6) 是一种存储上下文或其他信息片段的简便方法,这些信息在提示之间不应更改。它在初始化时需要一个参数 — memories。您可以以dict形式传递元素给它。例如,SimpleMemory(memories={“budget”: “100 GBP”})

最后,让我们用与之前相同的提示运行新链。您会注意到,最终输出包括一些奢侈礼品推荐,例如周末度假,与我们更新的提示中的更高预算相匹配。

overall_chain.run("If my age is half of my dad's age and he is going to be 60 next year, what is my current age?")

# OUTPUT
#> Entering new SequentialChain chain...

#> Entering new AgentExecutor chain...
# I need to figure out my dad's current age and then divide it by two.
#Action: PAL-MATH
#Action Input: What is my dad's current age if he is going to be 60 next year?
#Observation: 59
#Thought: I now know my dad's current age, so I can divide it by two to get my age.
#Action: Divide 59 by 2
#Action Input: 59/2
#Observation: Divide 59 by 2 is not a valid tool, try another one.
#Thought: I can use PAL-MATH to divide 59 by 2.
#Action: PAL-MATH
#Action Input: Divide 59 by 2
#Observation: 29.5
#Thought: I now know the final answer.
#Final Answer: My current age is 29.5 years old.

#> Finished chain.

# For someone of your age, a good gift would be something that is both practical and meaningful. Consider something like a nice watch, a piece of jewelry, a nice leather bag, or a gift card to a favorite store or restaurant.\nIf you have a larger budget, you could consider something like a weekend getaway, a spa package, or a special experience.'}

#> Finished chain.
For someone of your age, a good gift would be something that is both practical and meaningful. Consider something like a nice watch, a piece of jewelry, a nice leather bag, or a gift card to a favorite store or restaurant.\nIf you have a larger budget, you could consider something like a weekend getaway, a spa package, or a special experience.'}

结论

希望通过本文分享的学习内容能让您更轻松地深入了解这个库。本文只是皮毛,还有很多内容可以探讨。例如,如何在自己的数据集上构建问答聊天机器人,以及如何优化这些聊天机器人的记忆,以便您可以选择性地/总结性地发送对话,而不是将所有以前的聊天历史作为提示的一部分发送出去。

如往常一样,如果有更简单的方法来执行/解释本文中提到的一些内容,请告诉我。总的来说,避免未经请求的破坏性/垃圾/敌对的评论!

直到下次见 ✨

我喜欢撰写逐步初学者指南、如何教程、解码 ML/AI 术语等。如果您希望全面访问我的所有文章(以及 Medium 上的其他文章),可以使用 我的链接在这里注册

[## 逐步指南:在数据科学面试中解释您的 ML 项目。

并附带一个示例脚本,让您可以悄悄展示您的技术技能!

时间序列建模使用 Scikit、Pandas 和 Numpy [## 时间序列建模使用 Scikit、Pandas 和 Numpy

直观地使用季节性来提高模型准确性。

数据科学家实用的 GitHub Actions 介绍 [## 使用 GitHub Actions 进行动手实践的介绍

学习如何使用 Weights & Biases 自动化实验跟踪、单元测试、工件创建以及更多内容…

数据科学家实用的 GitHub Actions 介绍 [## 使用少量点击部署端到端深度学习项目:第二部分

将模型从 Jupyter notebook 转移到 Flask 应用程序,使用 Postman 测试 API 端点,并进行 Heroku 部署

将端到端深度学习项目部署到 Heroku

贝叶斯深度学习的温和介绍

原文:towardsdatascience.com/a-gentle-introduction-to-bayesian-deep-learning-d298c7243fd6

欢迎来到激动人心的概率编程世界!这篇文章是对该领域的温和介绍,你只需对深度学习和贝叶斯统计有基本了解。

François PorcherTowards Data Science François Porcher

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

--

到文章结束时,你应该对这个领域、它的应用以及它与更传统的深度学习方法的不同之处有一个基本的了解。

如果你像我一样听说过贝叶斯深度学习,并且猜测它涉及贝叶斯统计,但你不确切知道它是如何使用的,那么你来对地方了。

传统深度学习的局限性

传统深度学习的主要局限性之一是,即使它们是非常强大的工具,它们也不能提供不确定性的度量。

Chat GPT 可能会以明显的自信说出错误的信息。分类器的输出概率通常未经过校准。

不确定性估计是决策过程中的一个关键方面, 尤其是在医疗保健、自动驾驶汽车等领域。我们希望模型能够估计在对脑癌的分类非常不确定时,并在这种情况下需要进一步的医疗专家诊断。同样,我们希望自主汽车能够在识别到新环境时减速。

为了说明神经网络在估计风险时可能有多糟糕,我们来看看一个非常简单的带有 softmax 层的分类器神经网络。

softmax 的名字很容易理解,它是一个软最大函数,这意味着它是一个“更*滑”的最大函数。这是因为如果我们选择了一个“硬”的最大函数,只取概率最高的类别,我们将对所有其他类别的梯度为零。

使用 softmax 时,一个类别的概率可以接* 1,但永远不可能正好是 1。由于所有类别的概率总和是 1,因此仍有一些梯度流向其他类别。

硬最大值与软最大值,图像来源:作者

然而,softmax 函数也存在一个问题。它输出的概率校准不佳。在应用 softmax 函数之前值的微小变化会被指数函数压缩,从而导致输出概率变化极小。

这通常会导致过度自信,模型在面对不确定性时仍然给出某些类别的高概率,这是 softmax 函数‘max’特性固有的特征。

比较传统的神经网络(NN)与贝叶斯神经网络(BNN)可以突显不确定性估计的重要性。BNN 在遇到训练数据中的熟悉分布时,确定性较高,但当我们远离已知分布时,不确定性增加,从而提供更现实的估计。

下面是不确定性估计的一个示例:

传统神经网络与贝叶斯神经网络,图像来源:作者

你可以看到,当我们接*训练中观察到的分布时,模型非常确定,但当我们远离已知分布时,不确定性增加。

贝叶斯统计的简短回顾

在贝叶斯统计中有一个中心定理:贝叶斯定理

贝叶斯定理,图像来源:作者

  • 先验是我们在任何观察之前认为最可能的 theta 分布。例如,对于抛硬币,我们可以假设得到正面概率是围绕 p = 0.5 的高斯分布。

  • 如果我们想尽可能减少归纳偏置,我们也可以说 p 在[0,1]之间是均匀的。

  • 似然性是给定参数 theta 的情况下,我们得到观察 X,Y 的可能性。

  • 边际似然性是对所有可能的 theta 积分后的似然性。它被称为“边际”的原因是我们通过对所有概率进行*均来边际化 theta。

在贝叶斯统计中,关键的概念是从先验开始,它是你对参数可能值的最佳猜测(它是一个分布)。通过你所做的观察,你调整你的猜测,并获得一个后验分布

请注意,先验和后验不是 theta 的点估计,而是概率分布。

以此为例:

图像来源:作者

在这张图中,你可以看到先验向右移动,但似然性将我们的先验重新调整到左侧,而后验位于两者之间。

贝叶斯深度学习简介

贝叶斯深度学习是一种结合了两种强大数学理论的方法:贝叶斯统计深度学习。

与传统深度学习的区别在于对模型权重的处理:

在传统深度学习中,我们从头开始训练模型,随机初始化一组权重,并训练模型直到其收敛到一组新的参数。我们学习的是单一的一组权重。

相反,贝叶斯深度学习采用了更为动态的方法。我们从对权重的先验信念开始,通常假设它们遵循正态分布。当我们将模型暴露于数据时,我们调整这一信念,从而更新权重的后验分布。本质上,我们学习的是权重的概率分布,而不是单一的一组权重。

在推断过程中,我们对所有模型的预测进行*均,并根据后验概率加权它们的贡献。这意味着,如果一组权重的概率很高,则其对应的预测将获得更多的权重。

让我们将这些正式化:

推断,图片来自作者

贝叶斯深度学习中的推断通过使用后验分布对所有可能的θ(权重)值进行积分。

我们还可以看到,在贝叶斯统计中,积分无处不在。这实际上是贝叶斯框架的主要限制。这些积分往往是不可解的(我们并不总是知道后验的原始函数)。因此,我们必须进行非常计算密集的*似。

贝叶斯深度学习的优势

优势 1:不确定性估计

  • 可以说,贝叶斯深度学习最显著的好处是其不确定性估计的能力。在包括医疗保健、自动驾驶、语言模型、计算机视觉和定量金融等许多领域中,量化不确定性的能力对做出明智的决策和管理风险至关重要。

优势 2:提高训练效率

  • 与不确定性估计的概念密切相关的是提高的训练效率。由于贝叶斯模型能够意识到自身的不确定性,它们可以优先从那些不确定性——即学习潜力——最高的数据点中学习。这种方法被称为主动学习,能够实现令人印象深刻的有效和高效训练。

主动学习效果的演示,图片来自作者

如下图所示,使用主动学习的贝叶斯神经网络仅用 1,000 张训练图像就达到了 98%的准确率。相比之下,不利用不确定性估计的模型往往学习速度较慢。

优势 3:归纳偏差

贝叶斯深度学习的另一个优点是通过先验有效利用归纳偏置。先验允许我们编码对模型参数的初始信念或假设,这在存在领域知识的场景中尤为有用。

考虑生成式 AI,其思想是创建与训练数据类似的新数据(例如医学图像)。例如,如果你正在生成脑部图像,并且你已经知道脑部的一般布局——白质在内,灰质在外——这些知识可以包含在你的先验中。这意味着你可以给图像中心的白质分配更高的概率,而将灰质分配到边缘。

本质上,贝叶斯深度学习不仅使模型能够从数据中学习,还使其能够从知识点开始学习,而不是从头开始。这使其成为广泛应用的强大工具。

白质和灰质,图片由作者提供

贝叶斯深度学习的局限性

贝叶斯深度学习看起来非常不可思议!那么为什么这个领域会被低估呢?确实,我们经常谈论生成式 AI、Chat GPT、SAM 或更传统的神经网络,但我们几乎从未听说过贝叶斯深度学习,这是为什么呢?

限制 1: 贝叶斯深度学习非常

理解贝叶斯深度学习的关键在于我们“*均”模型的预测,而每当有*均时,就会有对参数集的积分

但是计算积分通常是不可解的,这意味着没有一个封闭或显式的形式可以使积分计算快速。因此,我们不能直接计算它,我们必须通过采样一些点来*似积分,这使得推断非常慢。

想象一下,对于每个数据点 x,我们必须*均 10,000 个模型的预测,并且每个预测可能需要 1 秒来运行,这样我们最终得到的模型就是无法扩展到大量数据

在大多数业务场景中,我们需要快速且可扩展的推断,这就是为什么贝叶斯深度学习不那么受欢迎。

限制 2: *似误差

在贝叶斯深度学习中,通常需要使用*似方法,如变分推断,来计算权重的后验分布。这些*似可能导致最终模型中的错误。*似的质量取决于变分家族和散度度量的选择,这可能很难选择和调整。

限制 3: 模型复杂性和可解释性的增加

虽然贝叶斯方法提供了改进的不确定性度量,但这也增加了模型的复杂性。BNNs 可能难以解释,因为我们现在有的是一个可能权重的分布,而不是单一的权重集。这种复杂性可能会导致在解释模型决策时遇到挑战,特别是在解释性至关重要的领域。

对于 XAI(可解释人工智能)的兴趣日益增长,而传统深度神经网络本身就难以解释,因为难以理解权重,贝叶斯深度学习则更具挑战性。

感谢阅读!在你离开之前:

[## GitHub - FrancoisPorcher/awesome-ai-tutorials: 最佳 AI 教程合集,让你成为…

最佳 AI 教程合集,让你成为数据科学的高手!- GitHub …

github.com](https://github.com/FrancoisPorcher/awesome-ai-tutorials?source=post_page-----d298c7243fd6--------------------------------)

你可以在你的收件箱中获取我的文章。 点击这里订阅。

如果你希望获得 Medium 上的高级文章,只需每月$5 的会员费用。如果你通过 我的链接注册,你将以不增加额外费用的方式支持我。

如果你觉得这篇文章有见地且有益,请考虑关注我并留下掌声,以便获取更多深入内容!你的支持帮助我继续制作有助于我们共同理解的内容。

参考文献

  1. Ghahramani, Z. (2015). 概率机器学习与人工智能。自然,521(7553),452–459。 链接

  2. Blundell, C., Cornebise, J., Kavukcuoglu, K., & Wierstra, D. (2015). 神经网络中的权重不确定性。arXiv 预印本 arXiv:1505.05424。 链接

  3. Gal, Y., & Ghahramani, Z. (2016). Dropout 作为贝叶斯*似:在深度学习中表示模型不确定性。国际机器学习会议(第 1050–1059 页)。 链接

  4. Louizos, C., Welling, M., & Kingma, D. P. (2017). 通过 L0 正则化学习稀疏神经网络。arXiv 预印本 arXiv:1712.01312。 链接

  5. Neal, R. M. (2012). 贝叶斯神经网络学习(第 118 卷)。Springer Science & Business Media. 链接

补充对数-对数回归的温和介绍

原文:towardsdatascience.com/a-gentle-introduction-to-complementary-log-log-regression-8ac3c5c1cd83?source=collection_archive---------1-----------------------#2023-10-02

一种在特殊条件下的逻辑回归替代方法

Akif MustafaTowards Data Science Akif Mustafa

·

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

--

在统计建模和回归分析中,有许多技术可以选择。其中一种常被忽视但在某些场景中非常有用的方法是补充对数-对数(Cloglog)回归。在这篇文章中,我们将详细介绍什么是 Cloglog 回归、何时使用它以及它是如何工作的。

Cloglog 回归的前身

Cloglog 回归是一种用于分析二元响应变量的统计建模技术。我们知道,当涉及到建模二元结果时,首先想到的模型是逻辑回归。实际上,cloglog 是逻辑回归在特殊场景中的替代方案。我假设大家对逻辑回归有基本的了解。然而,如果你对逻辑回归不熟悉,建议首先获得对其的基本了解。网上有大量关于逻辑回归的资源,可以帮助你熟悉这个主题。

Cloglog 回归是逻辑回归模型的扩展,当事件的概率非常小或非常大时尤其有用。大多数时候,cloglog 回归用于处理稀有事件或结果极度偏斜的情况。

对 Cloglog 回归的需求

众所周知,逻辑回归遵循 S 型函数的形式。下面展示了 S 型曲线:

作者提供的图像

从这个图形表示中可以明显看出,对于较小的‘x’值,结果的概率保持相对较低,而对于较大的值,结果的概率变得更高。曲线在‘Y’的值为 0.5 处表现出对称性。这种对称性意味着在逻辑回归中,存在一个潜在的特征,即成功或事件发生的概率(Y = 1)围绕 0.5 对称分布。这意味着概率的最显著变化发生在图表的中间部分,而在极端的‘x’值下,概率相对较不敏感。当我们的结果变量有大量成功或事件的情况时,这一假设是成立的,示例包括:

抑郁症的流行情况

作者提供的图像

或学生考试及格

作者提供的图像

然而,当事件非常稀少或非常频繁时,这一假设可能不成立,在这种情况下,成功或事件发生的概率要么极低,要么极高。例如,考虑人们在心脏骤停后的生存情况,其中成功的可能性显著降低:

作者提供的图像

或医院内青光眼手术的成功率(成功的机会非常高):

作者提供的图像

在这种情况下,0.5 处的对称分布并不理想,建议使用不同的建模方法,这就是互补对数-对数回归的应用场景。

与 logit 和 probit 不同,Cloglog 函数是不对称的,并且偏向一侧。

互补对数-对数回归的工作原理

Cloglog 回归使用互补对数-对数函数,生成一个 S 形曲线但不对称。Cloglog 回归的形式如下:

作者提供的图像

方程的左侧称为互补对数-对数变换。与 logit 和 probit 变换类似,这种变换也将二元响应(0 或 1)转换为(-∞到+∞)。该模型也可以写成:

作者提供的图像

在下图中,我们可视化了在 R 中使用 logit、probit 和 cloglog 变换生成的曲线。

# Load the ggplot2 package
library(ggplot2)

# Create a sequence of values for the x-axis
x <- seq(-5, 5, by = 0.1)

# Calculate the values for the logit and probit functions
logit_vals <- plogis(x)
probit_vals <- pnorm(x)

# Calculate the values for the cloglog function manually
cloglog_vals <- 1 - exp(-exp(x))

# Create a data frame to store the values
data <- data.frame(x, logit_vals, probit_vals, cloglog_vals)

# Create the plot using ggplot2
ggplot(data, aes(x = x)) +
  geom_line(aes(y = logit_vals, color = "Logit"), size = 1) +
  geom_line(aes(y = probit_vals, color = "Probit"), size = 1) +
  geom_line(aes(y = cloglog_vals, color = "CLogLog"), size = 1) +
  labs(title = "Logit, Probit, and CLogLog Functions",
       x = "x", y = "Probability") +
  scale_color_manual(values = c("Logit" = "red", "Probit" = "blue", "CLogLog" = "green")) +
  theme_minimal()

作者提供的图像

从图中我们可以观察到明显的差异:虽然 logit 和 probit 变换在值 0.5 附*是对称的,但 cloglog 变换表现出不对称。在逻辑回归和 probit 函数中,概率在接* 0 和 1 时以类似的速率变化。在数据在[0, 1]区间内不对称,且在小到中等值时变化缓慢但在接* 1 时急剧变化的情况下,logit 和 probit 模型可能不适合。在这些情况下,当响应变量的非对称性明显时,互补对数-对数模型(cloglog)成为一个有前景的替代方案,提供了更好的建模能力。从 Cloglog 函数的图中可以看到,P(Y = 1)在接* 0 时较慢,而在接* 1 时则急剧上升。

让我们以一个例子来说明:检查锌缺乏

我模拟了一个特定人群中的锌缺乏数据 [注:这些数据是作者为个人使用而创建的模拟数据]。数据集还包括年龄、性别和体重指数(BMI)等因素的数据。值得注意的是,数据集中只有 2.3%的人表现出锌缺乏,这表明在这个人群中锌缺乏的发生率相对较低。我们的结果变量是锌缺乏(二元变量(0 = 否,1 = 是)),预测变量是年龄、性别和体重指数(BMI)。我们在 R 中使用逻辑回归、概率回归和 Cloglog 回归,并通过 AIC 比较这三种模型。

> #tabulating zinc deficiency
> tab = table(zinc$zinc_def)
> rownames(tab)  = c("No", "Yes")
> print(tab)

  No  Yes 
8993  209 

> #tabulating sex and zinc deficieny
> crosstab = table(zinc$sex, zinc$zinc_def)
> rownames(crosstab) = c("male" , "female")
> colnames(crosstab) = c("No", "Yes")
> print(crosstab)

           No  Yes
  male   4216  159
  female 4777   50

> #definig sex as a factor variable
> zinc$sex = as.factor(zinc$sex)

> #logistic regression of zinc deficiency predicted by age, sex and bmi
> model1 = glm(zinc_def ~ age + sex + bmi, data = zinc, family = binomial(link = "logit"))
> summary(model1)

Call:
glm(formula = zinc_def ~ age + sex + bmi, family = binomial(link = "logit"), 
    data = zinc)

Coefficients:
             Estimate Std. Error z value Pr(>|z|)    
(Intercept) -2.064053   0.415628  -4.966 6.83e-07 ***
age         -0.034369   0.004538  -7.574 3.62e-14 ***
sex2        -1.271344   0.164012  -7.752 9.08e-15 ***
bmi          0.010059   0.015843   0.635    0.525    
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 1995.3  on 9201  degrees of freedom
Residual deviance: 1858.8  on 9198  degrees of freedom
  (1149 observations deleted due to missingness)
AIC: 1866.8

Number of Fisher Scoring iterations: 7

> #probit model
> model2 = glm(zinc_def ~ age + sex + bmi, data = zinc, family = binomial(link = "probit"))
> summary(model2)

Call:
glm(formula = zinc_def ~ age + sex + bmi, family = binomial(link = "probit"), 
    data = zinc)

Coefficients:
             Estimate Std. Error z value Pr(>|z|)    
(Intercept) -1.280983   0.176118  -7.273 3.50e-13 ***
age         -0.013956   0.001863  -7.493 6.75e-14 ***
sex2        -0.513252   0.064958  -7.901 2.76e-15 ***
bmi          0.003622   0.006642   0.545    0.586    
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 1995.3  on 9201  degrees of freedom
Residual deviance: 1861.7  on 9198  degrees of freedom
  (1149 observations deleted due to missingness)
AIC: 1869.7

Number of Fisher Scoring iterations: 7

> #cloglog model
> model3 = glm(zinc_def ~ age + sex + bmi, data = zinc, family = binomial(link = "cloglog"))
> summary(model3)

Call:
glm(formula = zinc_def ~ age + sex + bmi, family = binomial(link = "cloglog"), 
    data = zinc)

Coefficients:
             Estimate Std. Error z value Pr(>|z|)    
(Intercept) -2.104644   0.407358  -5.167 2.38e-07 ***
age         -0.033924   0.004467  -7.594 3.09e-14 ***
sex2        -1.255728   0.162247  -7.740 9.97e-15 ***
bmi          0.010068   0.015545   0.648    0.517    
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 1995.3  on 9201  degrees of freedom
Residual deviance: 1858.6  on 9198  degrees of freedom
  (1149 observations deleted due to missingness)
AIC: 1866.6

Number of Fisher Scoring iterations: 7

> #extracting AIC value of each model for model comparison
> AIC_Val = AIC(model1, model2, model3)
> print(AIC_Val)
       df      AIC
model1  4 1866.832
model2  4 1869.724
model3  4 1866.587

系数的解释

在 Cloglog 回归中,系数的解释类似于逻辑回归。每个系数代表预测变量变化一个单位时,结果对数几率的变化。通过对系数取指数,我们得到比值比。

在我们特定的模型中,年龄的系数是-0.034。这意味着年龄每增加一年,锌缺乏的对数几率减少 0.034 单位。通过对这个系数取指数,我们可以计算出比值比:

比值比 = exp(-0.034) = 0.97

这表明年龄增加一年与锌缺乏的几率降低 3%相关。

类似地,对于变量‘性别’:

比值比 = exp(-1.25) = 0.28

这表明,与男性相比,女性经历锌缺乏的几率降低了 72%。

我们也可以解释 BMI 系数,但需要注意的是,BMI 的 p 值为 0.52,表明在这个模型中,它与锌缺乏并没有显著关联。

应用及使用

Cloglog 回归被广泛应用于各种研究领域,包括稀有疾病流行病学、药物疗效研究、信用风险评估、缺陷检测和生存分析。特别是,Cloglog 模型在生存分析中具有重要意义,因为它与事件发生的连续时间模型密切相关。

互补对数-对数回归是一种强大且常被忽视的统计技术,在传统的逻辑回归不适用的情况下,它可能非常有价值。通过理解其原理和应用,你可以将这个多功能工具加入到你的数据分析工具箱中。

《深入浅出 JAX 中的深度强化学习》

原文:towardsdatascience.com/a-gentle-introduction-to-deep-reinforcement-learning-in-jax-c1e45a179b92?source=collection_archive---------4-----------------------#2023-11-21

在一秒钟内用 DQN 解决 CartPole 环境

Ryan PégoudTowards Data Science Ryan Pégoud

·

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

--

Thomas Despeyroux拍摄,发布于Unsplash

最*在强化学习(RL)方面的进展,例如 Waymo 的自动驾驶出租车或 DeepMind 的超人类棋类代理,结合了经典 RL深度学习组件,如神经网络梯度优化方法。

在之前介绍的基础和编码原则上构建,我们将探索并学习如何使用 JAX 实现深度 Q 网络DQN)和回放缓冲区来解决 OpenAI 的CartPole环境。所有这些操作都在不到一秒的时间内完成!

对于JAX向量化环境Q-learning的介绍,请参阅以下内容:

## 使用 JAX 向量化和并行化 RL 环境:Q-learning 的光速⚡

学习如何在 CPU 上向量化 GridWorld 环境,并同时训练 30 个 Q-learning 代理,每个代理进行 180 万步…

towardsdatascience.com

我们选择的深度学习框架是 DeepMind 的Haiku库,我最*在 Transformer 的上下文中介绍过:

## 使用 JAX 和 Haiku 从头开始实现 Transformer 编码器 🤖

理解 Transformer 的基础构建模块。

towardsdatascience.com

本文将涵盖以下几个部分:

  • 为什么我们需要深度 RL?

  • 深度 Q 网络理论和实践

  • 回放缓冲区

  • CartPole环境转换为JAX

  • JAX编写高效训练循环的方式

如往常一样,本文中提供的所有代码都可在 GitHub 上找到:

[## GitHub - RPegoud/jym:JAX 实现的 RL 算法和向量化环境

JAX 实现的 RL 算法和向量化环境 - GitHub - RPegoud/jym: JAX 实现的 RL…

github.com](https://github.com/RPegoud/jym?source=post_page-----c1e45a179b92--------------------------------)

为什么我们需要深度 RL?

在之前的文章中,我们介绍了时间差分学习算法,特别是 Q-learning。

简单来说,Q-learning 是一种离策略算法(目标策略与用于决策的策略不同),用于维护和更新Q 表,一个明确的状态到相应动作值映射

尽管 Q 学习是离散行动空间和受限观察空间环境的实际解决方案,但在更复杂的环境中很难扩展。事实上,创建 Q 表需要定义行动观察空间

考虑自动驾驶的例子,观察空间由来自摄像头和其他感知输入的无限潜在配置组成。另一方面,行动空间包括广泛的方向盘位置以及施加到刹车和油门的不同力度。

尽管理论上我们可以离散化行动空间,但实际应用中可能会导致不切实际的 Q 表,因为可能的状态和行动数量庞大。

Kirill Tonkikh的照片来自Unsplash

在大型复杂状态-动作空间中寻找最优行动因此需要强大的函数逼*算法,这正是神经网络所擅长的。在深度强化学习中,神经网络用作Q 表的替代品,并为大状态空间引入的维度灾难提供了高效的解决方案。此外,我们不需要显式定义观察空间。

深度 Q 网络与重播缓冲区

DQN 同时使用两种类型的神经网络,并行进行,首先是用于Q 值预测决策的“在线”网络。另一方面,“目标”网络用于通过损失函数评估在线网络的性能,以生成稳定的 Q 目标

与 Q 学习类似,DQN 代理由两个函数定义:actupdate

行动

act函数实现了关于 Q 值的ε-贪心策略,Q 值由在线神经网络估计。换句话说,代理根据给定状态的最大预测 Q 值选择动作,同时以一定概率随机执行动作。

您可能还记得 Q 学习在每一步之后更新其 Q 表,但在深度学习中,通常使用梯度下降在输入批次上计算更新。

因此,DQN 将经验(包含state, action, reward, next_state, done_flag的元组)存储在重播缓冲区中。为了训练网络,我们将从此缓冲区中随机抽取一批经验,而不仅仅使用最后一次经验(有关更多详细信息,请参见重播缓冲区部分)。

展示了DQN 行动选择过程的视觉表示(作者制作)

这里是 DQN 行动选择部分的 JAX 实现:

这个代码片段的唯一细微之处在于 model 属性不包含任何内部参数,这与 PyTorch 或 TensorFlow 等框架中通常的情况不同。

在这里,模型是一个 函数,表示我们架构中的 前向传递,但 可变的 权重是外部存储 并作为 参数 传递。这解释了为什么我们可以在传递 self 参数时使用 jit,作为 静态*(模型在其他类属性中是无状态的)*。

更新

update 函数负责训练网络。它根据 时间差(TD) 误差 计算 均方误差(MSE)损失:

DQN 中使用的均方误差

在这个损失函数中,θ 表示 在线网络的参数,而 θ 代表 目标网络的参数。目标网络的参数每隔 N 步被设置为在线网络的参数,类似于一个 检查点N 是一个超参数)。

参数的分离(当前 Q 值的 θ 和目标 Q 值的 θ−)对于稳定训练至关重要。

如果对两个网络使用相同的参数,就类似于瞄准一个移动的目标,因为 对网络的更新立即改变目标值。通过 定期更新 θ(即在设定步数内冻结这些参数),我们确保了 稳定的 Q 目标,同时在线网络继续学习。

最后,(1-done)调整目标 用于 终止状态。实际上,当一个回合结束时(即 ‘done’ 等于 1),没有下一个状态。因此,下一状态的 Q 值被设为 0。

DQN 参数更新 过程的可视化表示(作者制作)

实现 DQN 的更新函数稍微复杂一些,我们分解一下:

  • 首先,_loss_fn 函数实现了之前描述的用于 单一经验 的*方误差。

  • 然后,_batch_loss_fn 作为 _loss_fn 的包装器,并通过 vmap 装饰它,将损失函数应用于 一批经验。然后我们返回这一批的*均误差。

  • 最后,update 作为我们损失函数的最终层,计算其 梯度 相对于在线网络参数、目标网络参数以及一批经验。然后我们使用 Optax (一个常用于优化的 JAX 库) 执行优化步骤并更新在线参数。

注意,与回放缓冲区类似,模型和优化器是 纯函数,修改 外部状态。以下一行很好地说明了这一原则:

updates, optimizer_state = optimizer.update(grads, optimizer_state)

这也解释了为什么我们可以对在线网络和目标网络使用一个模型,因为参数是外部存储和更新的。

# target network predictions
self.model.apply(target_net_params, None, state)
# online network predictions
self.model.apply(online_net_params, None, state)

为了提供背景,我们在本文中使用的模型是一个多层感知机,定义如下:

N_ACTIONS = 2
NEURONS_PER_LAYER = [64, 64, 64, N_ACTIONS]
online_key, target_key = vmap(random.PRNGKey)(jnp.arange(2) + RANDOM_SEED)

@hk.transform
def model(x):
    # simple multi-layer perceptron
    mlp = hk.nets.MLP(output_sizes=NEURONS_PER_LAYER)
    return mlp(x)

online_net_params = model.init(online_key, jnp.zeros((STATE_SHAPE,)))
target_net_params = model.init(target_key, jnp.zeros((STATE_SHAPE,)))

prediction = model.apply(online_net_params, None, state)

重放缓冲区

现在,让我们退一步,更详细地看一下重放缓冲区。它们在强化学习中被广泛使用,原因有很多:

  • 泛化: 通过从重放缓冲区采样,我们打破了连续经验之间的相关性,通过混合它们的顺序。这种方式避免了对特定经验序列的过拟合。

  • 多样性: 由于采样不限于最*的经验,我们通常会观察到更新的方差较低,并且避免对最新经验的过拟合。

  • 增加样本效率: 每个经验可以从缓冲区中被多次采样,使模型能够从个体经验中学习更多。

最后,我们可以使用几种采样方案来管理我们的重放缓冲区:

  • 均匀采样: 经验以均匀的随机方式进行采样。这种采样类型易于实现,并允许模型从经验中独立于它们被收集的时间步长中学习。

  • 优先采样: 这个类别包括不同的算法,如优先经验重放(“PER”,Schaul 等,2015)或梯度经验重放(“GER”,Lahire 等,2022)。这些方法试图根据与其“学习潜力”(PER 的 TD 误差幅度和 GER 的经验梯度的范数)相关的某些指标优先选择经验。

为了简单起见,我们将在本文中实现一个均匀重放缓冲区。然而,我计划在未来详细讨论优先采样。

正如承诺的那样,均匀重放缓冲区的实现非常简单,但与 JAX 和函数式编程的使用相关的一些复杂性需要解决。与往常一样,我们必须使用纯函数,这些函数没有副作用。换句话说,我们不能将缓冲区定义为具有变量内部状态的类实例。

相反,我们初始化一个buffer_state字典,该字典将键映射到具有预定义形状的空数组,因为 JAX 在对 XLA 进行 JIT 编译时要求固定大小的数组。

buffer_state = {
    "states": jnp.empty((BUFFER_SIZE, STATE_SHAPE), dtype=jnp.float32),
    "actions": jnp.empty((BUFFER_SIZE,), dtype=jnp.int32),
    "rewards": jnp.empty((BUFFER_SIZE,), dtype=jnp.int32),
    "next_states": jnp.empty((BUFFER_SIZE, STATE_SHAPE), dtype=jnp.float32),
    "dones": jnp.empty((BUFFER_SIZE,), dtype=jnp.bool_),
}

我们将使用UniformReplayBuffer类与缓冲区状态进行交互。这个类有两个方法:

  • add:解开一个经验元组,并将其组件映射到特定索引。idx = idx % self.buffer_size确保当缓冲区满时,添加的新经验会覆盖旧的经验。

  • sample:从均匀随机分布中采样一系列随机索引。序列长度由batch_size设定,而索引的范围为[0, current_buffer_size-1]。这确保了在缓冲区尚未满时不会采样到空数组。最后,我们使用 JAX 的vmap结合tree_map返回一批经验。

CartPole环境转换为JAX

现在我们的 DQN 代理已准备好进行训练,我们将快速实现一个使用与早期文章介绍的相同框架的矢量化 CartPole 环境。 CartPole 是一个具有大型连续观察空间的控制环境,这使得测试我们的 DQN 变得相关。

CartPole 环境的可视化表示(鸣谢和文档:OpenAI Gymnasium,MIT 许可证)

这个过程非常简单,我们大部分都重用了OpenAI 的 Gymnasium 实现,同时确保我们使用 JAX 数组和 lax 控制流,而不是 Python 或 Numpy 的替代方案,例如:

# Python implementation
force = self.force_mag if action == 1 else -self.force_mag
# Jax implementation
force = lax.select(jnp.all(action) == 1, self.force_mag, -self.force_mag)            )

# Python
costheta, sintheta = math.cos(theta), math.sin(theta)
# Jax
cos_theta, sin_theta = jnp.cos(theta), jnp.sin(theta)

# Python
if not terminated:
  reward = 1.0
...
else: 
  reward = 0.0
# Jax
reward = jnp.float32(jnp.invert(done))

为了简洁起见,完整的环境代码在此处可用:

[## jym/src/envs/control/cartpole.py at main · RPegoud/jym

JAX 实现的 RL 算法和矢量化环境 - jym/src/envs/control/cartpole.py at main ·…

github.com

JAX的方式编写高效的训练循环

DQN 实现的最后部分是训练循环(也称为推演)。 正如前文所述,为了利用 JAX 的速度,我们必须遵守特定的格式。

推演函数可能一开始看起来令人生畏,但它大部分的复杂性纯粹是语法上的,因为我们已经涵盖了大多数构建块。 这是一个伪代码演示:

1\. Initialization:
  * Create empty arrays that will store the states, actions, rewards 
    and done flags for each timestep. Initialize the networks and optimizer
    with dummy arrays.
  * Wrap all the initialized objects in a val tuple

2\. Training loop (repeat for i steps):
  * Unpack the val tuple
  * (Optional) Decay epsilon using a decay function
  * Take an action depending on the state and model parameters
  * Perform an environment step and observe the next state, reward 
    and done flag
  * Create an experience tuple (state, action, reward, new_state, done)
    and add it to the replay buffer
  * Sample a batch of experiences depending on the current buffer size
    (i.e. sample only from experiences that have non-zero values)
  * Update the model parameters using experience batch
  * Every N steps, update the target network's weights 
    (set target_params = online_params)
  * Store the experience's values for the current episode and return 
    the updated `val` tuple

现在我们可以运行 DQN 进行20,000 步并观察其表现。 大约在 45 集后,代理成功地保持了超过 100 步的稳定*衡。

绿色条表示代理成功地在200 多步内*衡了杆,解决了环境。 值得注意的是,代理在第 51 集上创下了393 步的记录。

DQN 的性能报告(作者制作)

20,000 训练步骤一秒多一点内执行完毕,速度为每秒 15,807 步在单个 CPU 上)!

这些表现提示了 JAX 令人印象深刻的扩展能力,允许从业者使用最小的硬件要求进行大规模并行实验。

Running for 20,000 iterations: 100%|██████████| 20000/20000 [00:01<00:00, 15807.81it/s]

我们将更详细地看看并行化的推演过程,以运行具有统计显著性的实验和超参数搜索在未来的文章中!

与此同时,随时可以使用这本笔记本重现实验并尝试不同的超参数:

[## jym/notebooks/control/cartpole/dqn_cartpole.ipynb at main · RPegoud/jym

JAX 强化学习算法和向量化环境的实现 - jym/notebooks/control/cartpole/dqn_cartpole.ipynb at…

github.com](https://github.com/RPegoud/jym/blob/main/notebooks/control/cartpole/dqn_cartpole.ipynb?source=post_page-----c1e45a179b92--------------------------------)

结论

如往常一样,感谢您读到这里!希望本文为您在 JAX 中的深度强化学习提供了一个不错的介绍。如果您对本文内容有任何问题或反馈,请务必告诉我,我总是乐意聊一聊 😉

直到下次见面 👋

致谢:

《初学者友好的生成式 AI 介绍》

原文:towardsdatascience.com/a-gentle-introduction-to-generative-ai-for-beginners-8c8752085900

让我们深入了解生成式 AI 的整体图景

Federico TrottaTowards Data Science Federico Trotta

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

--

图片由 Susan Cipriano 提供,来源于 Pixabay

过去几个月里,所谓的“生成式 AI”得到了广泛关注,这是一种人工智能(AI)的子领域。像 ChatGPT 这样的工具已经成为最常被提及的词汇之一,并且正成为许多工作中日常任务的基础工具(甚至用来学习编程)。

诸如“DALL-E”、“ChatGPT”和“生成式 AI”这些词汇在过去几个月里充斥了社交网络、媒体、同事聊天和我们世界的方方面面。几乎每个人都在谈论这些。

那么,什么是生成式 AI?它与“普通” AI 有何不同?

在这篇文章中,我们将澄清生成式 AI 背后的大图景。因此,如果你参与了相关讨论但对这个话题没有明确的理解,这篇文章绝对适合你。

这是一种探讨性解释,以理解生成式 AI 背后的基础内容。因此,不用担心:这里不会有任何代码。只有想法和描述,它们将以非常简洁的方式呈现。特别是,我们将重点关注大规模语言模型和图像生成模型。

这里是你将学习的内容概要:

**Table of Contents:** 
What is generative AI and how does it differ from trditional AI?
Large Language Models
Image generation

什么是生成式 AI,它与传统 AI 有何不同?

生成式 AI 是 AI 的一个子领域,涉及创建能够生成新数据的算法,如图像、文本、代码和音乐。

生成式人工智能和“传统人工智能”之间的主要区别在于前者根据训练数据生成新的数据。此外,它可以处理“传统人工智能”无法处理的数据类型。

让我们稍微技术性地说一下:

  • “传统人工智能”可以定义为区分性人工智能。在这种情况下,实际上,我们训练机器学习模型,使其能够对新的、未见过的数据进行预测或分类。这些机器学习模型只能处理数字,有时也处理文本(例如,在自然语言处理的情况下)。

  • 在生成式人工智能中,我们训练一个机器学习模型,它创建的输出类似于它所训练的数据。这些类型的机器学习模型可以处理不同类型的数据,如数字、文本、图像和音频。

让我们可视化这些过程:

传统人工智能的过程。图片来源于作者。

所以,在传统人工智能中,我们训练一个机器学习模型从数据中学习。然后,我们将其输入新的、未见过的数据,它可以进行区分,做出预测或分类。

关于所展示的示例,我们已经训练了一个机器学习模型来识别图像中的狗。然后,我们将训练好的机器学习模型输入新的、未见过的狗的图片,它将能够分类这些新图像是否代表狗。

这是深度学习算法在分类问题中的典型任务。

生成式人工智能的过程。图片来源于作者。

在生成式人工智能的情况下,我们用来自各种来源的数据训练一个机器学习模型,使用大量的数据。然后,得益于一个提示(用户插入的自然语言查询),模型给出一个类似于它所训练的数据的输出。

以这个示例为例,我们的模型已经在大量(文本)数据上进行了训练,其中包括解释什么是狗的数据。然后,如果用户向模型查询什么是狗,模型将用自然语言描述什么是狗。

这是像 ChatGPT 这样的工具执行的典型任务。

现在,让我们看看一些生成式人工智能模型的类型。

大型语言模型

让我们从大型语言模型(LLMs)开始,深入了解各种生成式人工智能子领域。[LLM 是](https://en.wikipedia.org/wiki/Large_language_model#:~:text=A large language model (LLM,learning or semi-supervised learning.)(来自维基百科):

是一个计算机化的语言模型,由一个具有大量参数(从几千万到几十亿)的人工神经网络组成,使用自监督学习或半监督学习在大量未标记的文本上进行训练。

尽管“大型语言模型”这个术语没有正式定义,但它通常指的是具有数百万甚至数十亿个参数的深度学习模型,这些模型已在大量语料库上“预训练”。

因此,LLM 是深度学习(DL)模型(即神经网络),使用数百万个参数在大量文本上进行训练(这就是我们称之为“大型”的原因),并且对解决一些语言问题很有用,如:

  • 文本分类

  • 问答

  • 文档总结

  • 文本生成

因此,标准机器学习模型的另一个重要区别在于,在这种情况下,我们可以训练一个可以用于不同任务的深度学习算法。

让我进一步解释。

如果我们需要开发一个可以识别图像中狗的系统,如前所述,我们需要训练一个深度学习算法来解决分类任务,即:告诉我们新的、未见过的图像是否代表狗。仅此而已。

相反,训练一个 LLM 可以帮助我们完成上述所有任务。因此,这也证明了训练 LLM 所需的计算能力(和资金!)的必要性(这需要 PB 级的数据!)。

我们知道,LLM 是通过用户的提示来查询的。现在,我们需要区分提示设计和提示工程:

  • 提示设计。这是创造一个专门适用于系统执行的具体任务的提示的艺术。例如,如果我们想让我们的 LLM 将文本从英语翻译成意大利语,我们必须用英语写一个具体的提示,要求模型将我们粘贴的文本翻译成意大利语。

  • 提示工程。这是创建提示以提高我们的 LLM 性能的过程。这意味着使用我们的领域知识来向提示中添加细节,如特定的关键词、特定的上下文和示例,以及必要时所需的输出。

当然,当我们进行提示时,有时会使用两者的混合。例如,我们可能希望将从英语到意大利语的翻译应用于特定知识领域,如力学。

例如,一个提示可能是:“将以下内容翻译成意大利语:

光束受到正常应力作用。

考虑到我们处于力学领域,因此‘正常应力’必须与其相关

因为,你知道:“正常”和“应力”可能会被模型(甚至是人类!)误解。

三种类型的 LLM

有三种类型的 LLM:

  • 通用语言模型。这些模型能够基于训练数据中的语言预测一个词(或一个短语)。例如,可以考虑你的电子邮件自动完成功能来理解这种类型。

  • 指令调优模型。这些模型被训练以预测对输入中给出的指令的响应。总结给定文本是一个典型的例子。

  • 对话调优模型。这些模型被训练与用户进行对话,使用后续的回应。一个典型的例子是 AI 驱动的聊天机器人。

无论如何,请注意,实际分发的模型具有混合特征。或者,至少,它们可以执行多个这些类型典型的操作。

例如,如果我们考虑 ChatGPT,我们可以明确地说它:

  • 可以根据输入预测对指令的响应。事实上,例如,它可以总结文本,提供我们通过提示提供的某个论点的见解,等等……因此,它具有指令调优模型等功能。

  • 经过训练以与用户对话。这很明显,因为它会根据后续提示进行工作,直到我们对其答案感到满意。因此,它还具有对话调优模型等功能。

图像生成

图像生成已经存在了一段时间,这与人们的看法相反。

无论如何,*年来它得到了流行,特别是像“DALL-E”或“稳定扩散”这样的工具,它们明确了其用途,使这一技术对全球大众变得可及。

我们可以说图像生成可以分为四类:

  • 变分自编码器(VAEs)。变分自编码器是需要神经网络作为其整体结构的一部分的概率生成模型”。用操作性的话来说,它们将图像编码成压缩大小,并解码回原始大小。在此过程中,它们学习数据的分布。

  • 生成对抗模型(GANs)。这些通常是最知名的,至少在生成 AI 领域的术语中是如此。GAN 是一个机器学习框架的类别,其中两个神经网络相互对抗,其中一个的收益是另一个的损失”。这意味着一个神经网络创建图像,而另一个预测它是真实的还是伪造的。

  • 自回归模型。在统计学中,自回归模型是随机过程的表示。在生成图像的背景下,这些模型通过将图像视为像素序列来生成图像。

  • 扩散模型。扩散模型受到热力学的启发,毫无疑问是图像生成子领域中最具前景和最有趣的模型。

这是在扩散模型的“引擎盖下”工作的过程:

  • 前向分布过程。我们有一个初始的迭代过程,其中图像的结构在数据分布中被“破坏”。简单来说,就像我们反复向图像中添加噪声,直到所有像素变成纯噪声,图像无法被识别(通过人眼)。

  • 反向扩散过程。然后,有一个反向扩散过程,它是实际的学习过程:它恢复数据的结构。这就像我们的模型学习如何“去噪”像素以重建图像。

连接这一切的力量

如果你保持了注意力到现在,你的脑海中自然会浮现一个问题:“好的,费德里科,这很清楚。但我还有一点没弄明白:当我使用‘DALL-E’时,我输入一个提示,它输出一张图像:我们还没有讨论过这一点,是吗?!”。

不,我们没有。

上面我们简要描述了目前最有前景(也是使用最多)的图像生成模型,但缺少的部分是提示。

我们实际上讨论了它们在高层次上的工作原理。也就是说,我们简要解释了它们的学习过程是如何运作的。

但这些模型的真正力量体现在它们与大型语言模型的结合上。实际上,这种结合使我们能够结合提示工程的力量来请求模型输出。

换句话说:我们结合了使用自然语言作为输入的可能性,这些模型可以实际理解这些输入并生成相应的图像。

这难道不是一种超级能力吗?!

结论

总结来说,我们可以说生成型人工智能是人工智能的一个子领域,旨在生成类似于训练数据的新数据。

一方面,大型语言模型可以根据训练数据生成文本,而图像生成模型可以基于训练图像生成新图像,生成型人工智能的真正力量,至少在图像的情况下,依赖于大型语言模型和图像生成模型的结合。这让我们可以根据提示输入创建图像。

注意:本文灵感来自谷歌提供的生成型人工智能课程,部分参考资料来自其中。我建议* 参加此课程,以更好地理解生成型人工智能。

费德里科·特罗塔

嗨,我是费德里科·特罗塔,我是一名自由职业的技术写作员。

想与我合作吗? 联系我

GPT 模型简介

原文:towardsdatascience.com/a-gentle-introduction-to-gpt-models-e02b093a495b?source=collection_archive---------1-----------------------#2023-04-12

欢迎来到新的令牌生成器的世界

本杰明·玛丽Towards Data Science 本杰明·玛丽

·

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

--

图片来源于 Pixabay — 作者修改

随着 ChatGPT 和 GPT-4 的最*发布,GPT 模型引起了科学界的极大关注。这些 OpenAI 的 GPT 模型新版本如此强大和多才多艺,以至于我们可能需要很长时间才能充分挖掘它们的潜力。

尽管它们令人印象深刻,但你可能不知道,GPT 模型背后的主要思想和算法远非新颖。

无论你是经验丰富的数据科学家还是仅仅对 GPT 感到好奇的人,了解 GPT 模型的演变对数据的影响以及未来几年的预期都是特别有启发性的。

在这篇文章中,我解释了 GPT 模型如何发展到今天的状态。我将主要关注 OpenAI 如何在这些年里扩展 GPT 模型。如果你想开始使用 GPT 模型,我也会给出一些指引。

生成预训练语言模型

GPT 模型是语言模型。

语言模型已经存在了 超过 50 年

第一代语言模型是“n-gram 基础”的。它们对给定一些前置词的情况下,预测一个词的概率。

例如,如果你有以下句子:

猫在厨房里睡觉

使用 n=3,你可以从一个 3-gram 语言模型中获得“in”跟在“cat sleeps”后面的概率。

n-gram 模型在许多自然语言和语音处理任务中仍然很有用,直到 2010 年代初。

这些模型存在几个限制。计算复杂性随着 n 的增加而急剧增加。因此,这些模型通常限制在 n=5 或更低。

然后,得益于神经网络和更强大的机器,这一主要限制得到了缓解,可以计算更长 n-gram 的概率,例如 n=20 或更高。

使用这些模型生成文本也是可能的,但它们的输出质量很差,因此很少用于这个目的。

然后,在 2018 年,OpenAI 提出了第一个 GPT 模型

GPT 代表“生成预训练”。“预训练”意味着模型只是基于大量文本进行训练,以建模概率,除了语言建模没有其他目的。GPT 模型可以进一步微调,即进一步训练,以执行更具体的任务。

例如,你可以使用一个小的数据集来获得一个在新闻摘要方面表现非常好的 GPT 模型,或者在法英翻译上进行微调,以获得一个能够将法语翻译成英语的机器翻译系统。

注意:术语“预训练”暗示模型尚未完全训练,还需要另一个步骤。随着最*模型的出现,微调的需求趋于消失。预训练模型现在可以直接在应用中使用。

GPT 模型现在在几乎所有自然语言处理任务中表现都很优秀。我特别研究了它们在机器翻译方面的能力,你可以在以下文章中阅读:

## 使用 GPT-3 进行翻译

机器翻译,但没有机器翻译系统

towardsdatascience.com

训练的规模和它们利用的 Transformer 神经网络架构是它们能够生成流畅文本的主要原因。

自 2018 年首次发布 GPT 以来,出现了多个版本和子版本的 GPT。

4 个版本和更多子版本

GPT 和 GPT-2

GPT-2 在首次发布 GPT 后的几个月内推出注:在描述首次 GPT 的科学论文中从未提到“GPT”这个术语。可以说,“GPT-1”实际上并不存在。据我所知,它也从未发布。

GPT 和 GPT-2 有什么区别?

规模。GPT-2 比 GPT 大得多。

GPT 是在包含 7000 本书的 BookCorpus 上进行训练的。该模型有 1.2 亿个参数。

什么是参数?

参数是模型训练过程中学习到的变量。通常,参数更多的模型更大,表现更好。

120 百万在 2018 年是一个巨大的数字。

借助 GPT-2,OpenAI 提出了一个包含 15 亿参数的更大模型。

它是在一个未公开的语料库 WebText 上进行训练的。这个语料库比 BookCorpus 大 10 倍(根据描述 GPT-2 的论文)。

OpenAI 逐步发布了 4 个版本的 GPT-2:

  • small: 124 百万参数

  • medium: 355 百万参数

  • large: 774 百万参数

  • xl: 15 亿参数

它们都是公开的,可以用于商业产品中。

虽然 GPT-2-XL 在生成自然流畅的文本方面表现出色,即没有任何特定的指令或微调,但在特定任务上仍然远不如更新的 GPT 模型强大。

GPT-2-XL 的发布是 OpenAI 最后一次公开发布的 GPT 模型。GPT-3 和 GPT-4 只能通过 OpenAI 的 API 使用。

GPT-3

GPT-3 于 2020 年发布。其拥有 1750 亿参数,比 GPT-2 的跳跃更大。

这也是因为 OpenAI 停止公开 GPT 模型的精确训练信息。

今天,通过 OpenAI 的 API 提供了 7 个 GPT-3 模型,但我们对它们了解甚少。

借助 GPT-3,OpenAI 展示了如果用户提供一些他们希望模型完成的任务示例,GPT 模型可以在特定的语言生成任务中表现得极为出色。

GPT-3.5

随着 GPT-3 模型在 API 中运行并吸引越来越多的用户,OpenAI 能够收集到一个非常大的用户输入数据集。

他们利用这些输入进一步改进了他们的模型。

他们使用了一种叫做人类反馈强化学习(RLHF)的技术。我不会在这里详细解释,但你可以在 OpenAI 发布的一篇博客文章中找到这些细节。

简而言之,得益于 RLHF,GPT-3.5 在遵循用户指令方面比 GPT-3 好得多。OpenAI 将这类 GPT 模型称为“instructGPT”。

使用 GPT-3.5,你可以“提示”模型执行特定任务,而无需给它任务的示例。你只需写出“正确”的提示以获得最佳结果。这就是“提示工程”变得重要的地方,也是为什么熟练的提示工程师正在获得令人难以置信的工作机会

GPT-3.5 是当前用于驱动 ChatGPT 的模型。

GPT-4

GPT-4 于 2023 年 3 月发布。

我们几乎对其训练过程一无所知。

与 GPT-3/GPT-3.5 的主要区别在于 GPT-4 是双模态的:它可以接收图像和文本作为输入。

它可以生成文本,但不会直接生成图像。注意:GPT-4 可以生成生成图像的代码,或者从网络上检索图像。

在撰写这些文字时,GPT-4 仍处于“有限 beta”阶段。

ChatGPT

ChatGPT 只是一个具有聊天功能的用户界面。当你在 ChatGPT 中写东西时,生成答案的是一个 GPT-3.5 模型。

ChatGPT 的一个特点是,它不仅仅是像开箱即用的 GPT 模型那样接收用户的当前查询。为了作为聊天引擎正常工作,ChatGPT 必须跟踪对话:已经说了什么,用户的目标是什么,等等。

OpenAI 并没有透露它是如何做到这一点的。鉴于 GPT 模型只能接受有限长度的提示(我稍后会解释这一点),ChatGPT 不能简单地将所有对话回合串联在一起放在同一个提示中。这种提示可能会过大,GPT-3.5 无法处理。

如何使用 GPT 模型?

你可以轻松地在网上获取 GPT-2 模型并在计算机上使用它们。如果你想在你的机器上运行大型语言模型,你可能会对阅读我的教程感兴趣:

[## 在你的计算机上运行非常大的语言模型

使用 PyTorch 和 Hugging Face 的 device_map

pub.towardsai.net](https://pub.towardsai.net/run-very-large-language-models-on-your-computer-390dd33838bb?source=post_page-----e02b093a495b--------------------------------)

对于 GPT-3 和 GPT-3.5,我们别无选择,只能使用 OpenAI 的 API。你首先需要在他们的网站上创建一个 OpenAI 账户。

一旦你有了账户,你可以开始在“playground”(这是 OpenAI 提供的实验模型的沙盒)中玩耍。只有在登录后,你才能访问它。

如果你想在你的应用程序中直接使用这些模型,OpenAI 和开源社区提供了许多语言的库,如Python、Node.js 和 PHP,以通过 OpenAI API 调用模型。

你可以在你的 OpenAI 账户中创建并获取你的 OpenAI API 密钥。注意:请保密此密钥。任何拥有它的人都可以消耗你的 OpenAI 额度。

每个模型有不同的设置,你可以进行调整。请注意,GPT 模型是非确定性的。如果你用相同的提示两次调用模型,很有可能会得到两个相似但不同的回答。

注意:如果你想减少相同提示下回答的变异性,可以将模型的“温度”参数设置为 0。副作用是,它也会显著减少答案的多样性,换句话说,生成的文本可能会更冗余。

你还需要注意“最大内容长度”。这是你的提示长度加上 GPT 生成的回答长度。例如,GPT-3.5-turbo 的“最大内容长度”是 4,096 令牌

一个令牌不是一个单词。

令牌是 GPT 模型用于生成文本的最小单位。是的,GPT 模型并非真正的单词生成器,而是令牌生成器。令牌可以是一个字符、一个词的一部分、一个单词,甚至是某些语言中的词组。

OpenAI 在API 文档中给出了一个示例。

*"ChatGPT is great!"* 被编码成六个令牌: *["Chat", "G", "PT", " is", " great", "!"]*

一般而言,750 个英语单词约等于 1,000 个令牌。

我认为,管理“最大内容长度”是使用 OpenAI API 中最繁琐的部分。首先,没有简单的方法来知道你的提示包含多少个令牌。然后,你不能提前知道模型的回答将包含多少个令牌。

你需要猜测。只有当你有一定的模型经验时,才能猜对。我建议你多进行实验,以更好地评估在给定提示的情况下,回答可能有多长。

如果你的提示太长,回答将会被截断。

我不会在这里提供关于 API 的更多细节,因为它可能变得相当技术性。

GPT 模型的局限性

GPT 模型仅仅是基于网络训练的令牌生成器。它们受限于训练数据的内容,因此不能被认为是完全安全的。

自 GPT-3.5 起,OpenAI 已训练其模型以避免回答有害内容。为实现这一目标,他们使用了机器学习技术,因此这种“自我调节”无法 100%被信任。

这种自我调节可能对某个特定提示有效,但在仅仅改变一个词后,可能会完全失效。

我还建议阅读 OpenAI 产品的使用条款。在这份文档中,我认为 GPT 模型的局限性更为清晰。

如果你计划使用 API 构建应用程序,你应该特别注意这一点:

使用这些服务必须年满 13 岁。如果你未满 18 岁,必须获得父母或法定监护人的许可才能使用这些服务。如果你代表其他人或实体使用这些服务,你必须有权接受其条款。你必须提供准确和完整的信息来注册账户。你不得将访问凭证或账户提供给你组织以外的其他人,你对使用你的凭证发生的所有活动负责。

意大利暂时禁止使用 ChatGPT,因为它可能会生成不适合 18 岁以下人群的回答,以及其他原因。

## 意大利禁止 ChatGPT,欧洲可能跟随

面向 ChatGPT 包装器的繁荣“准备好迎接意大利”

medium.com

如果你是一个开发者,并在 OpenAI API 的基础上构建应用程序,你必须检查用户的年龄。

OpenAI 还发布了一份使用政策清单,指出了所有禁止使用模型的情况

结论

GPT 模型非常简单,其架构自 2018 年以来没有发生太大变化。但当你在大规模合适数据上训练一个简单模型,并使用合适的超参数时,你可以获得像 GPT-3 和 GPT-4 这样的极其强大的 AI 模型。

它们如此强大,以至于我们几乎没有完全探索其所有潜力。

尽管最*的 GPT 模型不是开源的,但通过 OpenAI 的 API 使用它们仍然很容易。你也可以通过ChatGPT来体验这些模型。

对开源大型语言模型的温馨介绍

原文:towardsdatascience.com/a-gentle-introduction-to-open-source-large-language-models-3643f5ca774

开放语言模型

为什么每个人都在谈论美洲驼、羊驼、猎鹰和其他动物

Donato RiccioTowards Data Science Donato Riccio

·发表于 Towards Data Science ·11 分钟阅读·2023 年 8 月 11 日

--

作者提供的图像(通过 Midjourney 生成)

除非你过去一年一直在过隐居生活,否则你已经见证了 ChatGPT 革命,以及大家似乎无法停止使用它的现象。在这篇文章中,我们将探索它的替代品,深入了解开源模型的世界。这是系列文章《开放语言模型》的第一篇,对希望入门并了解开源大型语言模型的人很有帮助,以及如何使用它们和为何使用它们。

目录

— 我们为什么需要开源模型?

— 越大越好?训练大型语言模型

— 微调大型语言模型

— 最佳开源大型语言模型

— 在你的计算机上运行大型语言模型

— 限制

— 结论

什么是大型语言模型?

一个大型语言模型(LLM)是一个能够理解和生成自然语言的人工智能。核心是一种叫做变换器的神经网络,它通过预测句子中下一个词来工作。词汇大型描述了这些模型的广泛性质,因为它们可以拥有数十亿甚至万亿个参数。它们的不同之处在于能够专注于特定任务,如代码生成或翻译,或应用于一般的指令跟随聊天机器人。这些模型的一个开创性方面是它们支持零-shot少-shot学习,因为它们展示了前所未有的能力来学习未经过明确训练的任务。[1]

我们为什么需要开源模型?

假设你使用 GPT API 创建了一个创新的应用程序,并迅速获得了关注。一切进展顺利,直到 OpenAI 改变了他们的行动计划。他们可能会停止服务、提高费用,甚至降低模型的能力——这已经在发生。[2]

目前,你唯一的解决方案是适应他们的新政策。此外,依赖第三方 API 会导致你的数据传输到他们的服务器。虽然 OpenAI 可能不会利用 GPT API 的数据进行模型训练,[3] 但部署你自己的语言模型可以保证你对这些操作的完全控制。即使这看起来是一个理想的计划,部署你自己的 LLM 也有其自身的限制和挑战,这些将在本文中讨论。

(左) 一只驼鹿。照片由 Sébastien Goldberg 拍摄 | (中) 一只美洲驼。照片由 Dušan veverkolog 拍摄 | (右) 一只羊驼。照片由 Adrian Dascal 拍摄。所有照片均来自 Unsplash。

越大越好?训练大型语言模型

如果你碰巧看到像LLaMA 65B这样的模型,你可能会想知道65B的含义。它简单地指的是模型中的参数数量。随着模型规模的增加,它需要更多的训练时间,并在推理时消耗更多的内存。与机器学习中的常见观点不同,复杂的模型可以更容易地泛化到不同的任务。有些模型的参数数量非常庞大:GPT-3 拥有 1750 亿,而 GPT-4 拥有超过 1 万亿。估计从零开始训练这些模型需要数百万美元。例如,谷歌的 PaLM 540B 在 2240 个 GPU 上进行训练[4]。相比之下,EfficientNet-B7 是最受欢迎的图像分类深度学习模型之一,仅有 6600 万个参数。

显然,这不是你可以在笔记本电脑上训练的模型。[5]

在 2022 年,谷歌声称:

随着模型规模的增加,性能在各个任务上得到提升,同时也解锁了新的能力。

在当前的 LLM 状态下,更多的参数通常意味着更好[4]。公司专注于构建更大的模型,但当前的开源趋势是创建更小、更高效的模型。虽然最受欢迎的开源模型通常有多达 70B 的参数,但在特定任务上,小型的、经过微调的模型表现可能优于更大的模型。此外,更大的模型在训练和推理时需要更多资源,部署起来也更具挑战性。

实际上,在一年内,连谷歌的观点也发生了变化。

开源模型更快、更可定制、更私密,而且在性价比方面更具优势。他们用 $100 和 13B 的参数做到了我们在 $10M 和 540B 的参数下都难以做到的事情。而且他们是在几周内完成的,而不是几个月。

从泄露的文档我们没有护城河,OpenAI 也没有 [6]*,可以看出他们承认开源模型的惊人演变,这些模型通过使用更小、更便宜的模型迅速赶上了。

ChatGPT 对此反应良好。

多亏了过去一年开源社区的杰出工作,现在有了可用且免费的替代品。在谷歌的 PaLM 发布不到一年后,LLaMA也发布了;在论文中,作者声称他们最大的模型LLaMA 65B在许多任务上超越了 GPT-3(175B)和 PaLM(540B)[5]。

微调大型语言模型

LLMs 因其在单一框架内处理多种语言任务的多功能性而赢得了声誉。然而,你的特定应用可能要求模型在单一任务上表现出色。为此,你可以使用针对你的任务的数据集(例如文本摘要)来微调一个预训练的模型。令人着迷的是,即使数据集较小,也能取得良好的结果。尽管模型最初是用数十亿个文本片段进行训练的,你可能只需要 500 到 1000 个示例就能显著提高性能。

一个来自Alpaca 数据集的示例。该模型经过微调,以跟随用户给出的指令。

一种流行的技术是指令微调。这种方法涉及使用示例来训练模型,说明它应该如何响应特定的指令。这一过程的结果是一个指令模型——这是基模型的增强版本,在遵循指令方面表现出色,而不仅仅是完成文本。指令模型的例子包括AlpacaVicuna

最佳开源大型语言模型

2023 年 2 月,Meta 的LLaMA模型以不同规模进入开源市场,包括 7B、13B、33B 和 65B。最初,该模型仅对研究人员开放,使用的是非商业许可证,但不到一周时间,它的权重就被泄露了。这一事件引发了开源大型语言模型(LLMs)领域的革命,因为其训练代码在 GPL 3 许可证下自由获取。因此,已经发布了几种强大的微调变体。

第一个是Alpaca,由斯坦福大学发布。该模型利用 GPT 生成的指令进行了 52K 示例的微调。紧随其后的是Vicuna,令人惊讶的是,它在许多任务中超越了 Alpaca,达到了 90%的 ChatGPT 质量。其显著特点是它是在 ShareGPT 数据上进行微调的。

在这些强大模型的基础上,新增了GPT4All——它受到使 LLMs 易于访问的愿景的启发,提供了一系列对消费者 CPU 友好的模型,并附带了一个互动 GUI 应用程序。

WizardLM 也加入了这些杰出的基于 LLaMa 的模型。通过一种名为 Evol-Instruct 的新颖独特的方法,它在复杂指令数据上进行了微调,并显示出与 ChatGPT 相似的表现,*均达到了 97.8%。

尽管如此,并不是所有最*的模型都基于 LLaMA。像 MPT 这样的模型以其变体能够生成多达惊人的 65k 上下文长度而闻名——一次生成整本书!

Falcon 也加入了这个趋势,提供了 7B 和 40B 两种变体。令人惊讶的是,它在 OpenLLM 排行榜上超越了 LLaMA,这要归功于其高质量的训练数据集 RefinedWeb。

然而,Falcon 在 HuggingFace 的 OpenLLM 排行榜上的统治地位并没有持续太久。2023 年 7 月,Meta 揭开了其著名模型的继任者 LLaMa 2 的面纱。这个下一代模型将其前身的令牌限制翻了一番,并将其上下文长度增加到 4K 令牌。同时,llama-2-chat——一个针对对话应用优化的版本——产生了重大影响。撰写本文时,像 StableBeluga2、Airoboros 和 Guanaco 这样的 LLaMa 2 微调版本 是最强大的开源大语言模型,主导了 OpenLLM Leaderboard

OpenLLM Leaderboard 上的前 10 名模型大多基于 LLaMa 2。

如果你对某个模型感到好奇并且想尝试,最简单的方法可能是访问 HuggingFace 模型页面,然后打开一个使用该模型的 Space。它们是简单的 Gradio 界面,允许你向模型发送输入并接收输出。由于模型运行在他们的服务器上并且请求量很大,你通常需要排队等待一段时间。不过,鉴于他们的服务质量,等待几分钟也不是什么大问题。

HuggingFace Spaces 使尝试语言模型变得非常简单。作者提供的图片。

硬件要求和优化

去年,你需要一个高端 GPU 才能在本地运行 LLM。即使是小型模型也需要大量内存,并且你必须将它们加载到显卡内存中。情况随着 llama.cpp** 的发布发生了变化**。最初是 Facebook 的 LLaMA 模型在 C/C++ 中的移植,现在支持许多其他模型。它允许将 LLM 从 PyTorch 转换为 GGML,他们的新格式,允许在 CPU 上进行快速推理。由于用 C++ 编译,推理是多线程的。

得益于这一惊人的工作,现在可以在你的桌面电脑上,甚至在 MacBook 上运行许多 LLM。

主要的限制是它们所消耗的内存。例如,一个 7B 模型的权重大约为 14GB。一个 65B 的模型将需要大约 130GB 的内存(RAM 和磁盘空间),这超过了我们大多数计算机的容量。幸运的是,有一种压缩它们的方法。

进入量化

在每个机器学习模型中,参数是一个数字。这个数字通常表示为 float32,即 32 位(4 字节)表示。

由于每个参数占用两个字节的空间,一个具有 65B 参数的模型将占用大约46510⁹ = 260GB的内存。

模型量化指的是通过使用舍入将模型参数表示为较低精度的数字,从而减少模型权重的想法。关于量化的数学细节,HuggingFace 博客上有一篇很棒的文章。[7]

8 位整数量化。[7]

作为一种压缩过程,量化会导致一定的性能损失。随着LLM.int8()及新技术的引入,这种损失已被大大减少,使得它成为 LLMs 的必备技术。

Llama.cpp支持高达 4 位整数量化。使用Q4_0,可以将模型大小减少最多 4 倍

量化对模型困惑度和文件大小的影响。图片由作者提供。数据来自 llama.cpp GitHub 仓库。

Q4_0将文件大小从 25.8GB 减少到 6.8GB,同时将困惑度减少了约 2%。在 16GB 内存的计算机上可以加载一个 13B 模型,而如果你有 64GB 内存,你甚至可以运行最大的 70B 模型。

具有不同量化技术的 LLaMa 2 模型。TheBloke的 HuggingFace 个人资料上有许多预训练模型。

另一个值得一提的量化技术是GPTQ,它可以将 VRAM 使用量减少多达 75%,同时保持准确性。[8] 它于 2023 年 3 月发布,使得第一次可以在单个 GPU 上运行 175B 模型。谈到消费级硬件,你可以用单张 RTX 3090 显卡运行最多 30B 的模型。

在你的计算机上运行大型语言模型

与 ChatGPT 最相似的体验是GPT4All应用程序,它是一个聊天界面,允许你与最喜欢的模型聊天。它不仅限于 GPT4All 模型,还支持许多最流行的模型。该应用程序可以在 Windows、Mac OS 和 Linux 上运行,并且完全开源。

在 Ryzen 5600 CPU 上进行 LLaMA-2 7B 推理。图片由作者提供。

另外,你可以使用像 text-generation-webuiopenplayground** 这样的 GUI 工具。**虽然它们都提供了一个可以轻松生成文本的图形界面,但第一个可能是最完整的工具:它提供了许多功能,如聊天、训练、GPU 支持和 HTML/Markdown 输出等。第二个与 OpenAI 的 Playground 非常相似,是一个很好的工具,可以快速测试和比较不同参数的 LLM。

文本生成网页 UI 是一个使用 Gradio 构建的高级网页界面。

如果你想在 Python 中使用 LLM,有几个选项,比如 llama-cpp-pythonHuggingFace Transformers 库,它提供了一种与任何 HF 模型交互的高级语法。它支持 PyTorch、Tensorflow 和 Jax 后端,可以与 HF 上找到的任何预训练模型一起使用,适用于文本、图像或音频。使用 transformers,与你喜欢的 LLM 生成文本就像编写两行代码一样简单:

from transformers import pipeline
pipe = pipeline("text-generation", model="meta-llama/Llama-2-7b-chat-hf")

在 HF hub 上成千上万的免费模型中,有不同的格式。Transformers 库需要 HF 格式的模型,而 llama.cpp 需要 GGML 模型。

限制

尽管我们已经展示了大多数模型不需要强大的计算机,可扩展性仍然是你计划构建允许数百或数千名用户与模型互动的系统时首要考虑的问题。使用 GPT 的一个优势是 OpenAI 提供了便宜的、高速限制的 API,可以轻松地扩展你的应用。

另一个限制是 安全性和管理。由于你可能需要对模型的输出负责,你必须特别小心模型生成的内容。商业 LLM 具有使用强化学习与人类反馈 (RLHF) 构建的先进审查过滤器,用于限制有害内容。

使用前记得检查 模型的许可证开源 并不总是意味着模型可以用于商业用途。

结论

这篇文章展示了开源模型是快速改进的可行和免费的替代方案。今年这些模型的发展取得了令人难以置信的进展,现在甚至可以在你的笔记本电脑上运行它们。

如果你正在考虑为下一个项目使用开源 LLM,希望你觉得这篇文章对你有用。系列中的后续文章将深入探讨 开源语言模型 的不同方面和挑战。

下次见!

如果你喜欢这篇文章,加入 Text Generation ——我们的新闻通讯每周发布两篇文章,提供有关生成 AI 和大型语言模型的最新见解。

此外,你还可以在 LinkedIn找到我。

参考资料

[1] 斯坦福科学家发现,ChatGPT 确实变得更愚蠢了 (2023), (futurism.com)

[2] T. Brown 等人,语言模型是少样本学习者 (2020), arXiv.org

[3] M. Schade,您的数据如何用于提高模型性能 (2023), OpenAI 帮助中心

[4] Google AI 博客,Pathways 语言模型 (PaLM): 扩展到 5400 亿参数以实现突破性性能 (2022), ai.googleblog.com

[5] A. Fan 等人,LLaMA: 开放且高效的基础语言模型 (2023), arXiv.org

[6] D. Patel 和 A. Ahmad,Google: 我们没有护城河(OpenAI 也没有) (2023), semianalysis.com

[7] Y. Belkada 和 T. Dettmers,使用 Hugging Face Transformers、Accelerate 和 bitsandbytes 对变换器进行大规模 8 位矩阵乘法的温和介绍 (2022), Hugging Face 博客

[8] E. Frantar 等人,GPTQ: 生成预训练变换器的准确后训练量化 (2023), arXiv:2210.17323v2 [cs.LG]

可调整神经网络的温和介绍(第一部分)

原文:towardsdatascience.com/a-gentle-introduction-to-steerable-neural-networks-part-1-32323d95b03f?source=collection_archive---------0-----------------------#2023-11-21

什么是可调整神经网络及其背景

Matteo CiprianTowards Data Science Matteo Ciprian

·

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

--

介绍

几何深度学习作为深度学习的一个分支,旨在扩展传统的 AI 框架,如卷积神经网络,以处理表示为图、流形或点云的三维或二维几何对象。通过直接将几何关系和空间依赖性整合到学习框架中,几何深度学习利用数据的固有结构特性,消除了对内存密集型数据增强技术的需求。出于所有这些原因,几何深度学习可以被视为在计算机视觉、自然语言处理等领域处理复杂数据场景的有价值工具。关于任务类型和转换类型,迄今已提出了大量新的 CNN 架构,如“球形神经网络” (链接), “图神经网络” (链接) 和 “可转向神经网络”

可转向神经网络 因其将常规卷积神经网络(CNNs)的能力扩展到新的领域而引起了广泛关注。这些网络可以被视为 CNNs 的演变,其中核被条件化以满足特定约束条件。虽然 CNNs 在对*移的等变性方面表现出色,但可转向神经网络通过提供增强的灵活性和捕获更广泛的转换,如旋转,而更进一步。

本教程 将介绍“可转向神经网络”(S-CNNs)的简介,试图传达对其背后数学概念的直观理解以及如何设计这些网络的逐步解释。本第一篇文章作为介绍可转向神经网络的起点,解释其目的并深入探讨支持 S-CNNs 的概念和形式化。第二篇文章(这里)在高层次上讨论了可转向滤波器的设计和整体可转向网络。

本工作旨在填补当前科学文献与更广泛数据科学受众之间的差距。它非常适合技术专业人士以及这一新的机器学习分支的研究人员。

来自论文[3]的一个简单可转向神经网络的示例。可以看到输入图像的旋转效果反映在网络输出响应中。

以下论文作为参考:

[1] “3D 可转向 CNN:在体积数据中学习旋转等变特征”,Weilier 等,(link);

[2] “可转向 CNN”,Cohen 等,(link);

[3] “学习用于旋转等变 CNN 的可转向滤波器”,Weilier 等,(link)

[4] “通用 E(2)-等变可转向 CNN” Weilier 等,(link)

[5] “适用于局部尺度不变卷积神经网络的尺度可转向滤波器”,Ghosh 等,(link)

[6] “构建 E(n)-等变可转向 CNN 的程序。” Cesa 等,(link)

什么是可转向神经网络:

可转向神经网络得名于它们使用的特定类型的滤波器。这些滤波器称为 g-可转向滤波器,它们的灵感来自于在图像识别领域中用于边缘检测或定向纹理分析的可转向滤波器,这些滤波器在90 年代初获得了广泛的应用。可转向通常指的是可操控的、可管理的、能够被控制的。按照这种惯例,可转向滤波器的响应是可定向的,并且可以适应输入的特定方向(例如一张图像)。可转向性与另一个非常重要的属性相关,这就是等变性。在等变滤波器中,如果滤波器的输入经过了一个精确且明确的几何变换 g(*移、旋转、移动),则输出(即输入与滤波器卷积的结果)也会经过相同的变换 g。通常,等变性并不要求变换(输入和输出的变换)是相同的。这个概念将在下一个段落中得到更好的阐述,但目前这使我们能够提供对可转向滤波器和可转向 CNN 的初步定义。

一个 可转向 CNN 滤波器 可以定义为一个其内核结构为不同可转向滤波器的串联的滤波器。这些滤波器在 卷积操作 相对于一组定义明确的几何变换方面显示出等变性特性。

正如我们稍后将看到的,卷积操作上的等变性条件导致对内核结构及其权重施加特定的约束。从这个定义中,现在可以定义什么是可转向 CNN:可转向神经网络是由一系列可转向滤波器组成的神经网络。

S-CNN 的用途:

普通 CNN 的优势在于其对*移的等变性。然而,可导神经网络更加灵活,可以展示其他类型的变换,例如旋转。在旋转等变问题中,未经修改的 CNN 被迫学习相同滤波器的旋转版本,从而引入了冗余的自由度,并增加了过拟合的风险。

因此,可导 CNN 网络可以通过直接整合输入处几何变换的信息,优于经典 CNN。这一特性使得 S-CNN 在处理具有几何描述和表示的输入(如图像、流形或向量场)时特别有用。

可能的实际应用例如:

  • 挑战性的 2D 图像分割: 给定输入显微镜图像预测细胞边界。

  • 3D 模型分类: 对 3D 物体进行分类和识别。

  • 3D 化学结构分类: 预测给定化学结构的分子 3D 化学结构。一个可能的例子是根据氨基酸序列预测其空间偏好,具体见论文的第 5.4 节 [2]

3D 可导神经网络在 3D 物体识别中的应用示例。输入物体(在顶部)以及两个不同隐藏层特征图的表示。摘自 Link

初步定义和背景

在介绍了可导神经网络及其应用后,让我们深入探讨它们背后的理论。本节提供了等变性和可导性的更正式解释,提供了理解后续文章中可导滤波器构造所需的基本定义和正式框架。

本文依赖于对映射和几何变换的理解,更多信息请参考这篇 文章。

1. 等变性:

等变性是对称问题中特别感兴趣的特性。如前所述,在等变模型中,当输入经过变换作用时,输出也会受到相应作用,从而使得变换的应用可以在模型应用之前或之后进行,而整体行为不发生变化。在日常环境中有许多等变性的例子。例如,驾驶时,当转动方向盘时,汽车的转向方向与汽车所指方向是等变的。形式上,如果我们有一个映射 𝛙: X → Y,其中 X⊂ℝᵈ 和 Y⊂ℝᵈ¹,以及 g,一个属于群体 G 的几何变换,𝛙 对 G 是等变的,如果:

Eq.1: 表示𝛙对 g 的等变性的数学方程。

其中Π₀(g) : X → X’Π₁(g): Y→ Y’是由应用g到 x 确定的两个线性映射(例如,通常是通过乘法应用的矩阵)。下图提供了一个来自论文[2]的视觉示例。在图像中,g是旋转,具体为“旋转-90°”,因此被称为rΠ₀(r)在领域𝛙(=X)中操作,而Π₁(r)在𝛙(=Y)的值域中工作。

如果X=ℝ²,2 维笛卡尔空间,且 r 是“顺时针旋转 90°”的变换,则矩阵Π₀(r)将等于θ=π/2 的 2x2 欧拉矩阵。

应注意,如果𝛙对 G 是等变的,那么施加变换后再计算映射会产生与先计算映射再施加变换相同的结果,这一属性以前称为交换性。

Fig2A: 函数Ѱ对变换 r 等变的视觉示例。摘自文章[2]

此时还值得提到一个特例。 不变性,一种特殊类型的等变性,其中X=X’Y=Y’。无论输入如何变换,输出始终保持不变。从深度学习的角度来看,不变滤波器例如在物体识别中可能有用:无论输入图像如何旋转,滤波器的输出始终保持不变。需要注意的是,XY的空间可能不具有相同的维度,例如,如果我们试图确定图片中汽车的方向(Y作为 2 维向量)而X作为像素的 2 维数组,则变换Π₁(g)Π₀(g)将不同,因为它们适用于不同的空间,即使它们共享相同的 g。

2. 可操控滤波器:

与汽车的可操控性相比,可操控滤波器稍微难以直观理解。然而,两者都共享实现对特定参数一致和可预测响应的基本目标——这种响应与滤波器本身的固有变换密切相关。

一个直观的例子可能如下:想象一下屋顶上的风向标,显示风的方向。与其为每种可能的风向安装单独的传感器(这是不切实际的),不如安装一个可以旋转以与当前风向对齐的风向标。可转向滤波器就像一个风向标,它根据输入信号中编码的方向自适应,而无需为每种可能的输入方向使用独立的传感器。同样,在图像处理中,可转向滤波器适应图像中的不同特征或方向,而无需为每种可能的输入方向使用独立的滤波器。这种方法为建模系统提供了智能和有效的方法。在机器学习的背景下,它使我们能够专注于构建有价值的模型,而不必担心增强或增加额外的权重以处理不同的方向。

尽管可转向性可以普遍应用于任何一组变换,我们将在此使用旋转来更正式地介绍这个概念。

让 𝛙: ℝᵈ →ℝᵈ¹ 成为其核函数为 k 的卷积映射。

对于 x∈ℝⁿ,给定一个依赖于 x 的输入信号 f(x) ∈ ℝᵈ,并且输出信号 f₁(x) ∈ ℝᵈ¹,我们可以写成:f₁(x)= 𝛙(f(x)),这意味着 f₁(x)= k(x)f(x)

如果对旋转的转向滤波器定义如下:

(1) 每个输出元素的卷积核 k(x) 可以表示为基函数 ψⱼ(x) 的和,其中 j=1,..M*。

(2) 通过任意角度 θ 旋转滤波器的 g_θ 可以用每个基函数的旋转表示(对于每个 θ 均适用)。数学上来说,这意味着:

Eq.2: 可转向滤波器的定义

由于这一特性,可以通过修改 wⱼ 的值来唯一定向滤波器对输入的响应。我们来举个例子。

在二维空间中,一个可定向单个可转向滤波器的最简单的例子是其核函数为 二维高斯 的方向导数。在这种情况下,k: ℝ² →ℝ,且 x = (x₁,x₂) ∈ ℝ²:

Eq.3: 二维高斯 的方向导数(上)和函数 k R² →R 在 gθ 下的转换。

在接下来的几行中,我们将展示该滤波器按上述方式是可转向的。

从理论上我们知道,鉴于 k 的值域是 ,我们可以将旋转后的滤波器写成 Eq.3(有关更多信息,请参见下一节中的 Eq.3)。

通过推导这个方程,我们可以展示其可转向性:

Eq.5: 二维高斯 的方向导数可转向的数学证明

在这种情况下,我们应用了变换 g_θ: ℝ²→ℝ²,并且它由二维欧拉矩阵表示(见下文诱导表示)。如果我们计算 k(g_θ ⁻¹(x₁,x₂)), 我们可以通过一些代数运算看到,这种冲激滤波器的通用旋转版本可以表示为两个基函数 ѱ₁(x₁,x) 和 ѱ₂(x₁,x) 的线性组合,系数由 θ 参数化。

如下方程(方程 6)所示,由于卷积的线性特性,输入函数 f 与θ旋转的冲激响应 g_θ(k(x,y))=k_θ 的卷积始终可以表示为 f 与 k 的单一基函数 ѱ、ѱ₂ 的卷积的线性组合。

方程 6:一个可转向滤波器与 f 的卷积。

这个公式突出了 可转向滤波器在神经网络中的力量

通过引入这些滤波器,我们有可能构造一个可转向的核,它根据输入的方向“调整”其响应。每个基函数像一个多功能工具,允许网络使用学习到的权重‘w₁’和‘w₂’来高效地混合这些函数,以准确响应不同的方向。例如,当网络遇到具有不同方向的数据,如图像中的旋转物体时,它配置这些权重以使核的响应与输入数据的方向对齐。这种适应性提高了效率和效果,从而在参数更少的情况下达到相同或更好的结果。因此,这种方法可以作为使用可转向属性处理各种输入方向的更强大的 CNN 的基础。

在下一篇文章中,我们将进一步探讨这个问题,并了解如何使用可转向滤波器的概念来构建等变滤波器。

然而,在深入之前,一些定义将提供清晰度并帮助我们的讨论。因此,在下一段中我们引入了一些关于卷积的形式化内容。

3. 形式化:

在这一部分,我们试图给读者提供一个所有分析元素的示意性解释。这种形式化将允许我们更正式地定义 CNN 及其在输入层操作的几何变换。这将使我们在下一篇 文章 中理解可转向 CNN 的工作原理。

元素:

  • 一个空间 S:分析发生的空间。虽然 S 可以扩展到任意数量的维度,但最容易在二维或三维空间中进行可视化。例如,如果我们考虑一幅图像,初始空间是二维的,对应于像素的坐标*面(ℤ²)。如果我们考虑一个“3D 物体”,那么空间 S 是三维的,ℤ³。因此,一个点 x∈S 确定了一个位置。

  • 一个输入函数 f: 函数 f: S → F₀ = ℝ ͨ 描述了我们几何空间中的输入(它可以是流形或向量场)。这可以看作是从空间 S 到 ℝ ͨ 的一个函数,其中每个位置 x 与“特征” f(x) 相关联,也称为 x 点的 f 的纤维。举些例子,一个灰度图像可以看作是一个函数 f: ℝ² → ℝ,S=ℝ² c=1。如果考虑一个彩色的 3D 流形,函数将是 f: ℝ³→ ℝ³,其中每个位置分配一个 RGB 颜色,S=ℝ³,c=3\。

    实际上,函数 f 通常表示为一些采样空间上的纤维的打包结构;对于标准格式的图像,纤维将水*和垂直地规则分布(即像素)。函数 f 构成了神经网络的输入层(见图 2A,图 2B)。从现在起,这个起始层将被称为 F₀

  • 一组变换 G: 一旦分析对象被适当地定义,我们可以定义网络应该保持等变性的变换集。单个变换 g∈G 总是可以被描述为与应用它的数学空间相关的函数。给定输入函数 f:S→ℝ ͨ 可以表征 π(g): ℝ ͨ → ℝ ͨ,作为“g 在 ℝ ͨ 中的诱导变换”。*函数 *f 存在于 ℝ ͨ 中,但变换 g 操作在 S 空间中。π(g) 描述了 f(在 ℝ ͨ 中)在应用 g(在 S 中)下的变换。考虑 g 作为由两个组件 r(旋转)和 t(*移)组成的旋转-*移,一般来说,输入函数 f(x) 在变换 g 下的变换如 Eq.7 所述\。

    在下图中,如果 f 是一个向量场,π(g) 是一个 cxc 维度的矩阵,而 如果 f 是一个标量场(f: ℝ² → ℝ),π(r) = 1。

    所考虑的变换组 G 通常是旋转(在这种情况下我们将讨论SO(2) 网络)或旋转 + *移(在这种情况下我们将讨论 SE(2) 网络)。类似地,在三维空间中,考虑 3D 刚体运动(SO(3)SE(3))。

图 2B: 变换 g 对标量场(左)或向量场(右)的应用的图形表示。摘自论文 [3]

Eq.7: f 如何通过变换 g 应用于 x 而被变换

  • 特征图: 根据第二点给出的 f 定义,神经网络每一层的输出可以看作是函数 f ₙ 在初始空间 S 上的应用结果。形式上可以表示为从 S 到对域空间 Fₙ 的函数,( f : SFₙ),其中 Fₙ=ℝ ͨ ʿ ʾ 和 c 是层 n 的特征数量。如果以图 2B 为例,我们可以看到初始信号(输入)可以看作是函数 f : S=ℝ² → F₀= ℝ³。

    f₁: S=ℝ² → F₁= ℝ²。

  • NN 滤波器 φn: 滤波器可以定义为两个连续层之间的映射,如**φ:Fₙ→ Fₙ₊₁。将这种滤波器应用于一层意味着与相应的内核k进行卷积。在这种情况下如何定义卷积对理解可导 NN 至关重要。因此,我们在下面专门讨论了这一点。

NN 滤波器和卷积

在这种情况下,内核可以看作是一个函数 k: S → ℝ ͨ ʿ ʾ ˟ ͨ ʿⁿ⁺ ¹ ʾ,其中 S 中的每个位置都连接到一个维度为 cʿ ʾ ˟ cʿⁿ⁺ ¹ ʾ 的矩阵。为了清晰起见,c 和 c¹ 分别是 FₙFₙ₊₁ 的维度(特征数量)。

我们可以定义卷积如下:

Eq.8: 上方:连接层 n 和层 n+1 的关系。下方:空间 S 中的卷积定义

上面的方程 Eq.8 代表连接层 nn+1 的函数;下面的是 n 维空间 S 中的卷积定义。函数 σ(x) 代表应用于卷积输出的非线性函数。

在图 2B 中,可以看到在离散域中,内核与输入层之间的卷积是如何计算的。我们用一个灰度图像 f ₀: ℝ² -> ℝ 来说明这一点。我们可以应用第二部分中讨论的滤波器,这是一个具有函数的可导滤波器。

k(x₁, x₂) 是一个定义为 k: ℝ² -> ℝ¹˟¹=ℝ 的 2D 高斯滤波器。

在这种情况下,将滤波器 k 应用于f 是经典的 2D 卷积,可以表示为:*

Eq.9: 卷积的定义

不同的是,在图 2B 中,你可以看到另一个例子,其中 f ₀: ℝ²-> ℝ³(例如 rgb 图像)和 f₁: ℝ²-> ℝ² 以及 k₀: ℝ²-> ℝ³˟ ²

图 2B: 如上定义的滤波器卷积的视觉示例,S=R²。F⁰是信号 f⁰存在的输入空间,在此案例中是 R³。可以注意到,卷积操作已被相关操作替代,如[4]中所建议。

综合我们迄今讨论的所有要点,可以在这一形式化框架内可视化神经网络。每个单独的特征图可以被解释为一个函数 f: S → Fₙ,其中 Fₙ= ℝʿⁿ ʾ 和 f₀(x) 代表网络的输入。滤波器的应用涉及与其在 Eq.8 中定义的核函数卷积。值得注意的是,到目前为止,主要的创新在于将 f 作为在位置空间 S 中操作的函数的几何表示,以及在这一空间内卷积的定义。

以下是我们提供的神经网络在这一背景下的表示:

Eq.10: 使用上述形式化表达的神经网络的符号表示。

我们将在下一篇文章中了解这种形式化定义如何帮助我们设计可引导的 CNN 滤波器。

结论

在我们《可引导神经网络》教程的初始部分,我们已经建立了可引导神经网络、等变性和可引导滤波器的基本概念。还介绍了一个数学框架,为理解这些概念提供了严格的基础。等变性在变换下保持行为不变,而可引导滤波器能够智能地适应输入的方向。这一基础工作为设计等变 CNN 滤波器铺*了道路,增强了边缘检测和基于方向的识别。下一篇文章将利用这些概念更深入地探讨可引导 CNN 滤波器的机制,完成我们对这一强大神经网络范式的探索。

✍️ 📄. 关于作者:

1️⃣ Matteo Ciprian,机器学习工程师/研究员

  • 帕多瓦大学电信工程硕士。当前从事传感器融合、信号处理和应用 AI 领域的工作。具有与 AI 在电子健康和可穿戴技术中的应用相关的项目经验(包括学术研究和企业领域)。专注于开发异常检测算法,以及推进深度学习和传感器融合技术。

    对哲学充满热情。YouTube 内容创作者。

    🔗 链接: 💼 Linkedin

    📹 Youtube

    👨‍💻Instagram

2️⃣ Robert Schoonmaker,信号处理/机器学习研究员

  • 杜伦大学计算凝聚态物理博士。专注于应用机器学习和非线性统计,目前研究 GPU 计算方法在合成孔径雷达及类似系统中的应用。经验包括开发用于传感器融合和定位技术的对称机器学习方法。

    🔗 链接: 💼 Linkedin

《可操控神经网络简介(第二部分)》

原文:towardsdatascience.com/a-gentle-introduction-to-steerable-neural-networks-part-2-56dfc256b690?source=collection_archive---------9-----------------------#2023-11-21

如何构建可操控滤波器和可操控 CNN

Matteo CiprianTowards Data Science Matteo Ciprian

·

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

--

1) 介绍

本文是《可操控神经网络简介》教程的第二部分,也是最后一部分。它接续了第一部分的内容(在这里)。

第一篇文章提供了 Steerable 神经网络(S-CNNs)的简明概述,解释了它们的目的和应用。它还深入探讨了基础形式主义和关键概念,包括等变性和 Steerable 滤波器的定义。尽管下一段落将对形式主义进行快速回顾,但我们建议您阅读第一篇文章以全面了解。

在本教程的最后部分,我们希望提供一个关于如何构建 Steerable Filter 的指南,并在最后,介绍如何组合一个 Steerable 神经网络。

快速回顾术语:

图 3A:按照形式主义表示的神经网络。

  • S:输入域空间。对象存在的空间(通常为ℝ³或ℝ²)。

  • f: 一个映射/函数,fₙ: S → ℝ ͨ ʿ ʾ(Fₙ)描述了 NN 的第 n 个特征映射。请注意,f⁰是描述输入(输入层)的函数,而对于 n>0,fₙ描述了第 n 个特征映射。

  • F= ℝ ͨ ʿ ʾ**,它是描述fₙ*的值域。

  • Φ: F→ F ₙ₊₁,n 个 NN 的滤波器可由核函数k: S →* ℝ ͨ ʿ ʾ ˟ ͨ ʿⁿ⁺ ¹ ʾ *来描述。卷积的定义如上第二个方程式所示。

  • G:变换的组(单一元素g)。

综合考虑所有这些概念,我们已经能够定义如下卷积:

2) Steerable CNN 滤波器的设计

图 3A:等变 CNN 滤波器的视觉示例。给定对 S 的变换 g 及由Π₀(g)给定的输入信号 f 的旋转,f₁由Π₁(g)旋转。

2.1 问题的形式化

我们可以声明,如果对于每个g 在 G,0,当输入函数 f₀变换为Π₀(g)时,那么第 n 层的输出函数将变换为Πn(g),则 n 层的 CNN 对于一组变换 G 是等变的。

使这个陈述成立的一个充分条件是每个连续的层对其直接输入的变换具有等变性(见图 3A)。网络的等变性是通过归纳来实现的。根据第二篇文章中给出的定义,如果滤波器Φ满足以下条件,则Φ是等变的:

方程 0:等变性的定义

现在可以宣称 steerable 神经网络理论的主要结果

k连接层fₙ和f的核心函数,使得fₙ₊₁ = k f ₙ.

卷积k* f ₙ对于变换 g 是等变的,当且仅当:

或更简单

Eq.1: 关于变换 g 的核等变性的必要且充分条件。

在更广泛的文献中[2,3],遵循此约束的核被称为 g-可导核。由于核约束以线性方式操作,它生成的解构成了标准 CNN 中通常使用的无约束核的向量空间中的一个线性子空间。经过更仔细的审查,这一定义与最后一篇文章第 2 段中介绍的可导滤波器的概念非常契合 这里。在实际操作中,为了获得此工作,我们需要一个此核子空间的基,记作{k_1, …k_D},它符合方程(1)。这个基的大小,记作 D,可以计算为 D = cʿⁿ ʾ ˟ cʿⁿ⁺¹ʾ。核k(x)随后通过这个基的线性组合得出,网络在过程中学习权重:

Eq.2: 方程(1)的线性使得解等于以下线性组合。

在训练场景中,我们的方法涉及将输入层和输出层的大小设置为特定的值,即 cʿⁿ ʾ和 cʿⁿ⁺¹。然后,根据我们寻求等变的变换,解决方程并确定一个核基。随后,在训练过程中,我们学习与这些核相关的权重。

2.2 解方程

方程(1)中呈现的约束的解远非简单。它依赖于三个主要元素:

  • 空间 S,无论是 S= ℝ³还是 S= ℝ²。

  • 群体 G

  • 层的输入输出维度:cʿ ʾ 和 cʿⁿ⁺ ¹ ʾ*。

更具体地说,我们可以说群 G 的选择定义了网络的类型。具体来说,我们主要对以下类型的网络感兴趣:

  • SO 网络:对特殊正交群(SO)中的旋转具有等变性。

  • SE 网络:对特殊欧几里得群(SE)中的旋转和*移具有等变性。

  • E 网络:对欧几里得群(E)中的旋转、*移和反射具有等变性。

如果我们在 2D 输入域中操作,我们有 SO(2),SE(2)和 E(2)网络 [4]。相反,对于 3D 输入域,我们使用 SO(3),SE(3)和 E(3)网络[1],并且这可以扩展到任何 E(n)空间 [6]

将这项工作扩展到其他空间和对称性是一个持续的研究领域,感兴趣的读者可以调查被称为 Hilbert 空间和 Green 函数的数学研究领域,这里不在本文讨论范围之内。

然而,可以看到在 SE(n) 网络的情况下,方程式 1 的一般解是 S=ℝ中的一个谐波基函数。在上面的图像中(图 3B),可以看到ℝ²左侧的谐波函数和ℝ³中的谐波函数。

图 3B:二维(左)和三维(右)中的谐波函数基。这些基分别构成 SE(2) 和 SE(3) 网络中可操控等变滤波器的基。

考虑一个更具滤波器设计场景的情况,在下图 Fig 3C 中,我们可以看到如何为输入层 f ₀: ℝ²->ℝ³ 和输出层 f₁: ℝ²->ℝ² 构建一个 SO2 可操控等变核。

核是一个函数 k: ℝ²->ℝ³ˣ²。矩阵的每个单独元素是通过对在位置(x₁,x₂)采样的 D 基的线性加权组合得到的函数。我们可以查看上面的示例位置 x=(1,2)

接下来,我们将展示一些这个方程的简单解,考虑 S=ℝ² 和 G 作为旋转变换的群体,包含 SO2 网络。

图 3C:使用 6 个谐波函数的基构建的 3x2 可操控核的可视化表示。

2.3 实际解决方案

- Case1A: SO2 网络,k: S=ℝ² → ℝ

假设实际情况下输入为灰度图像,我们想要构建一个可操控滤波器来处理它。首先,我们必须决定输出层的维度(特征数量)。为了简便起见,假设维度为 1。

在这个设置中,我们有一个输入函数 f: ℝ²-> ℝ 和一个类似的输出函数 f₁: ℝ²-> ℝ。因此,核函数是 k: ℝ² -> ℝ。我们希望我们的 CNN 层对一个变换群体 G 是等变的,G 代表了角度 theta 在 [0,2π) 范围内的旋转(SO 网络)。对于这个问题,核函数的基需要使用方程式 1。由于 f 和 f¹ 都是标量,Pi_out = 1 和 Pi_in = 1。结果是 k[g_θ(x)] = k[x],如方程式 3 中所写。

如果 x = (x₁, x₂) 在 ℝ² 中,g(theta) 与 2D 欧拉矩阵对齐。

方程式 3:在 k: S=ℝ² → ℝ 的情况下重写方程式(1)

很容易看出,这可以通过在(x₁, x₂)中的每个各向同性函数来解决。具体来说,这可以通过一维的各向同性(旋转不变)核来解决。(即 k(x₁, x₂) = x₁² + x₂²)

Case 2: SO2 滤波器,k: ℝ² → ℝ²

现在考虑一个更复杂的情况。输入函数为f: ℝ² → ℝ²,输出层为函数f ₁: ℝ² → ℝ²。因此,内核可以写为函数k: S= ℝ² ℝ² ˣ ²; 换句话说,对于ℝ²中的每个位置 x,我们有一个二维矩阵 2x2(见下方方程)。我们想要构建 S02 滤波器,因此需要考虑的变换群再次是 G={g(θ)} ={r(θ)},θ0,2Π[. 由于ℝ²是ff ₁的值域,Π_out=Π_θ* 和 Π_in=Π_θ,其中Π_θ是ℝ²中的欧拉矩阵。考虑到所有这些条件,我们可以以下述方式重写 Eq.1:

![Eq.(4): 考虑到的内核函数k: S= ℝ² ℝ² ˣ ²

Eq.(5): 为 SO2 内核 k 重写 Eq.(1): S=ℝ² → ℝ²。

欲更全面地理解该方程的解及更多见解,请参考论文[4]中的附录部分。

2.4 网络非线性

到目前为止,我们仅考虑了相对于卷积操作的等变性,而没有考虑由函数σ(f(x)): ℝ=ℝ ͨ→ℝ ͨ’给出的非线性部分。论文[1]的第 4.3 节和论文[4]的第 2.6 节对此进行了广泛讨论。

给定函数 f(x),等变性条件可以总结如下:

Eq.(5): 激活函数的等变性条件。

如相关的 YouTube 讲座中所提到的这里,可以通过利用所谓的基于范数的激活函数,如σ(u) = σ(||u||),来创建满足该标准的激活函数。其动机在于标量范数是透明不变的,因此对其应用任何非线性函数将产生不变的输出。为了证明这一点,当我们将此公式应用于上述条件时,会得到以下方程:

Eq.(6): 将 Eq.(5)重写为基于范数的函数。

如果‘g’属于 E 变换群,则范数保持不变。因此,当Π’(g)等于单位矩阵时,该方程在普遍情况下是有效的。这意味着特别设计的激活函数始终具有旋转不变性。例如,Norm-ReLUs,其定义为η(|f(x)|) = ReLU(|f(x)| − b)

研究论文和讲座中提出了额外的非线性激活函数,如非门控激活函数。我们建议读者查阅这些来源以获取进一步的解释。

3) 设计一个可操控的 CNN

图 3D: 可操控 CNN 的架构,如[3]中所述。注意第 2 层中使用的可操控滤波器与 G 卷积的结合。

在上一节中,我们掌握了构建单个可引导滤波器的基础知识。在本节中,我们将深入探讨如何将这些滤波器有效地整合以建立一个功能全面的可引导神经网络。

在上图中,我们可以看到一篇论文中的示例[3]。我们特别关注第 2 层,其中使用了可引导滤波器。

在这里,每个水*表示都是一个可引导滤波器——由加权谐波函数组成——它产生一个不同的输出,表示为单个 fⁿ。观察其结构,很明显虽然谐波函数在各个滤波器中保持一致,但它们的方向在每个滤波器之间有所变化。这是 G-卷积技术的一个典型特征,这是一种复杂的方法,有助于构建对变换不变的网络(你可以在这里找到更多关于该技术的信息)。该网络利用最大池化的强大功能,将来自可引导滤波器阵列中的最强响应传递到下一层。这种选择性传输的原则确保了最强的特征在网络中传递和增强。这种方法与其他工作中实现的方法类似,例如参考文献[5]成功构建了一个尺度不变的可引导网络。这种可引导 CNN 的架构受益于这种技术,因为它自然地结合了尺度和旋转不变性,从而增强了网络以更抽象但更强大的方式识别模式和特征的能力。无论如何,从图片中可以看出,最终结果是一个对旋转不变的网络。

图 3E:在旋转图像上应用 2D 可引导滤波器的视觉示例(原始图像可以在这里找到)

关于可引导神经网络设计的优秀逐步解释可以在此链接中找到,该链接包含在 Github 库“e2cn”(*link)。在该库中,可以找到设计 SE2 可引导网络的 PyTorch 代码。关于 SE3 网络的有用代码可以在此链接中找到,而关于 3D 等变网络的快速课程已在这里发布。

文献:

[1] “3D Steerable CNNs: Learning Rotationally Equivariant Features in Volumetric Data”,Weilier et al.,(link);

[2] “Steerable CNNs”,Cohen et al. ( link);

[3] “学习旋转等变 CNN 的可调节滤波器”,Weilier 等人 (link)

[4] “通用 E(2)-等变可调节 CNN”,Weilier 等人 (link)

[5] “适用于局部尺度不变卷积神经网络的尺度可调节滤波器”,Ghosh 等人 (link)

[6] “构建 E(n)-等变可调节 CNN 的程序。” Cesa 等人 (link)

✍️ 📄. 关于作者:

1️⃣ Matteo Ciprian,机器学习工程师/研究员

  • 硕士学位,电信工程,帕多瓦大学。当前在传感器融合、信号处理和应用人工智能领域工作。参与与人工智能在电子健康和可穿戴技术中的应用相关的项目(学术研究和企业领域)。专注于开发异常检测算法,以及推进深度学习和传感器融合技术。

    对哲学充满热情。YouTube 内容创作者。

    🔗 链接: 💼 Linkedin

    📹 Youtube

    👨‍💻 Instagram

2️⃣ Robert Schoonmaker,信号处理/机器学习研究员

  • 博士学位,计算凝聚态物理,杜伦大学。专注于应用机器学习和非线性统计学,目前正在研究 GPU 计算方法在合成孔径雷达及类似系统中的应用。经验包括开发用于传感器融合和定位技术的对称机器学习方法。

    🔗 链接: 💼 Linkedin

对分析流处理的温和介绍

原文:towardsdatascience.com/a-gentle-introduction-to-stream-processing-f47912a2a2ea?source=collection_archive---------13-----------------------#2023-03-31

为工程师及其他相关人员构建心理模型

Scott HainesTowards Data Science Scott Haines

·

关注 发表在 Towards Data Science · 17 分钟阅读 · 2023 年 3 月 31 日

--

流处理可以被温柔而细致地处理,也可以被狂野而几乎失控地处理!你可以判断你更愿意拥抱哪种未来。来源:@psalms 原始照片

介绍

在许多情况下,将数据流中或实时可用的数据进行处理,可以将由于数据流量和规模而导致的庞大数据问题,转化为更可管理的问题。通过更频繁地处理较小的数据集,你可以有效地解决那些可能因成本和时间限制而难以处理的数据问题。

从批处理思维转变为流处理思维虽然也可能很棘手,所以让我们从小做起,逐步构建。

从庞大的数据回到大数据

假设你负责构建一个分析应用程序,该应用程序必须处理大约10 亿个事件(1,000,000,000)每天。虽然这在开始时可能感觉难以实现,但由于数据的巨大规模,通常有助于退后一步,思考应用程序的意图(它做了什么?)和你正在处理的内容(数据是什么样的)?问问自己事件数据是否可以被分解(划分和分区)并作为流处理操作(即流内)并行处理,还是必须通过多个步骤串行处理?无论哪种情况,如果你将应用程序的视角修改为查看有限的时间窗口,那么你现在只需要创建一个可以摄取和处理仅每秒 11,500 个事件(k)(或者如果事件流是恒定的,则每分钟约 695k 个事件)的应用程序,这是一个更容易理解的数字。

虽然这些数字仍然可能显得难以触及,但这正是分布式流处理真正发挥光芒的地方。从本质上讲,你是在减少问题的视角或范围,以在时间上跨越分区数据集实现目标。虽然并非所有问题都能在流处理中解决,但许多问题确实适合这种处理模式。

注意:本章是我书中的一部分 “现代数据工程与 Apache Spark:构建关键流应用程序的实用指南”。本书带你从简单的脚本编写,到应用程序的构建,最后到部署和监控你的关键 Apache Spark 应用程序。

本章学习内容

本章将作为流处理的温和介绍,为我们直接进入第十章构建自己的端到端结构化流应用程序做好准备,而无需回顾和讨论许多决策过程背后的理论。

到本章结束时,你应该能高层次地理解以下内容:

  1. 如何减少流数据问题中的时间问题

  2. 时间、时间戳和事件视角的问题

  3. 从批处理到流处理思维模型的不同处理模式

流处理

流数据是非静态的。事实上,你可以将其视为活跃的(即使只是短时间)。这是因为流数据是捕捉当前时刻的事件和动作的数据。让我们来看一个实际的,尽管是理论上的例子,它从一个简单的传感器数据流开始。把你最后一次访问的停车场(或停车库)固定在你的脑海中。

用例:实时停车可用性

停车是个噩梦:大多数停车基础设施的问题,或客户的常见痛点,往往是在确保按时到达的情况下找到一个可用的车位。照片来自 Unspash@ryansearle

想象一下,你刚刚找到一个停车位,感谢一些有用的标志指引你到一个空车位。现在假设这一切都因为来自连接的本地停车传感器网络的数据。传感器的唯一目的是用于识别此时此刻可用停车位的数量

这是一个实时数据问题,实时准确性既可以测量,也可以由停车结构的用户实际感受到。这些能力的实现始于系统场景的声明。

产品推广:“我们希望创建一个系统来跟踪所有可用停车位的状态,识别何时有车辆停车、车辆在特定车位停留多久,并且尽可能地自动化这个过程”

优化这样的系统可以从一个简单的传感器开始,该传感器位于每个停车位(与传感器.id / 车位.id 参考关联)。每个传感器负责以事件的形式发出数据,包含车位标识符、时间戳和简单的位(0 或 1),以表示车位是空的还是被占用。然后,这些数据可以编码为紧凑的消息格式,如示例 9–1,并定期从每个停车位高效地发送。

示例 9–1. 为了清晰起见,展示了一个示例传感器事件(封装在Google Protocol Buffer消息格式中)。

message ParkingSensorStatus {
  uint32 sensor_id = 1;
  uint32 space_id = 2;
  uint64 timestamp = 3;
  bool available = 4;
}

在一天的正常交通流中,传感器的状态(停车位的可用性)会根据车辆到达或离开每个车位而开或关(二进制状态)。由于每个驾驶员的动态日程,这种行为是不可预测的,但随着规模的扩大,模式总是会显现出来。

利用收集的传感器数据提供的实时状态,可以轻松构建实时的、现实生活中的(IRL)“报告”,以便更新驾驶员停车结构的活动状态:停车基础设施是否已满,如果未满,则车库中目前有 X 个可用车位

传感器数据的作用

这些数据可以帮助自动化人类决策过程,甚至可以通过简单的网络服务在线提供,以便实时跟踪状态,因为最终驾驶员只是想尽快停车,而不是浪费时间!此外,这些数据还可以用于跟踪每个传感器上次检查的时间(刷新),这可以用来诊断故障传感器,甚至跟踪传感器离线或故障的频率。

现在,技术更先进的车库甚至能够通过指示牌和提示引导驾驶员到结构内的空车位。这既减少了车库间的交通和拥堵,又提高了顾客满意度,所有这一切只需捕捉实时的传感器数据流并进行*实时处理。

高峰定价和数据驱动决策

根据从这些传感器事件流中收集的时间(时间戳)信息,一个精明的车库运营者可以利用先前的趋势来实时减少或增加每日或每小时的价格,这取决于对车位的需求,考虑到当前的可用性(剩余车位数)。通过优化定价(在现实限制范围内),运营者可以找到一个完美的阈值,使得每小时/每日价格能使车库更多时间达到满员。换句话说,“以什么价格大多数人愿意停车且车位不会空置?”

这是一个优化问题的例子,源于实时传感器数据的收集。组织越来越常见地查看如何重用数据来同时解决多个问题。物联网(IOT)用例只是你在编写流应用程序时可能处理的众多数据流中的一种。

在书中早些时候,我们讨论了“创建一个系统,可以获取咖啡店的占用信息,以告知人们离他们最*的店铺是否有适合他们人数的座位”。在故事中的那个阶段,我们只是创建了一个合成表来展示这个例子,但这也是一个可以用传感器或简单的签到系统解决的问题,该系统发出相关事件数据,通过我们的流数据管道可靠地传递下游。

这里讨论的两个例子(停车基础设施和咖啡帝国扩展)都采用了基本的分析(统计),并可以从简单的机器学习中受益,以发现新的行为模式,从而实现更优的操作。在我们过于深入之前,先休息一下,深入了解流数据网络提供的功能。

时间序列数据和事件流

从一个关于固定视图或时间点的静态数据思维方式,转变为一个将数据视为在时间中流动的视角,涉及到对许多视图和时间点中无限数据流的解释,这是一个视角上的练习,但起初可能具有挑战性。通常,当你考虑流式系统时,连续事件流的概念会浮现出来。这是一个更常见的用例,可以作为对流数据概念的温和引入。例如,图 9–1中所示的抽象时间序列。

图 9–1:事件发生在精确的时间点,可以单独收集和处理(t1->t4),也可以在时间窗口(w1)中聚合。图片来源:作者(Scott Haines)

正如你所看到的,数据本身在不同的状态下存在,取决于系统(或应用程序)应用的视角或观点。每个事件(T1->T4)只在其狭窄的参考范围内理解发生了什么,或者换句话说,事件捕捉了时间的有限(相对)视角。当一系列事件在一个有界集合(窗口)中一起处理时,你会得到一系列数据点(事件),这些数据点要么完全实现的概念,要么部分实现的概念。当你缩小视角并查看整个时间线时,你可以更准确地描绘从第一个事件到最后一个事件发生的故事。

让我们进一步探讨这个想法。

事件是否独立存在?

考虑这个简单的事实。你的事件数据存在于完整的概念中,或作为部分概念或思想。我发现将数据视为一个随时间变化的故事有助于赋予这些数据字节以生命。因此,每个数据点负责帮助构建一个完整的故事,作为一系列随时间展开或呈现的交织思想和观念

数据组合是一个有用的视角,适用于你在采用分布式数据视图时。我还发现它在构建和定义新的分布式数据模型时很有用,同时在处理大规模的现实世界数据网络(织物)时也很有帮助。作为一种组合来看待,这些事件汇聚在一起讲述了一个特定的故事,这些基于事件的痕迹可以揭示事物发生的顺序,并通过每次事件的时间戳得到极大的增强。没有时间的事件描绘了一个*面的发生过程,而时间的加入赋予了你关于动量或速度的概念,或者在事件之间的时间延续和拉伸,或者对于一系列数据点的完整过程。理解数据在许多管道和数据通道中流动的行为对数据操作至关重要,并且需要可靠的监控以保持数据在最佳速度下流动。

让我们来看一个用例,其中时间维度帮助讲述一个现实世界场景的更好故事。

用例:追踪客户满意度

一家安静的咖啡店,每杯咖啡都倾注爱心。照片由 Nafinia Putra 提供,来源于 Unsplash

设想你是一个数据工程师,与一个名为“CoffeeCo”的虚拟咖啡帝国的数据应用特性团队合作,讨论的是哪些数据能够描述客户满意度随时间变化的故事(时间序列分析)。

如果我告诉你两位客户进入我们的咖啡店,点了饮料并带着饮料离开了店里。你可能会问我为什么要告诉你这些,因为这正是咖啡店里发生的事情。如果我告诉你这两个咖啡订单是在差不多同时 下的,并且故事中的第一位客户在咖啡店待了不到五分钟。如果我告诉你这是一个工作日,且这个故事发生在早高峰时段?如果我告诉你第二位客户,恰好排在第一位客户之后,在咖啡店里待了三十分钟?你可能会问这个客户是否在读报纸或者使用设施。这两个问题都是合理的

如果我告诉你第二位客户因为在四步咖啡生产线第 3 步和第 4 步之间发生错误而等待,那么我们将更好地理解如何在未来简化客户体验。

四个步骤是:

1. 客户订单: {customer.order:initialized}

2. 付款完成 {**customer.order:payment:processed}

3. 订单排队: {customer.order:queued}

4. 订单完成: {customer.order:fulfilled}

无论错误是在自动化过程中,还是由于现实世界系统的故障(如打印机卡纸、咖啡师漏单或其他任何原因),结果是客户需要插手(人工干预),并通知操作系统(咖啡生产线)“似乎有人忘记制作我的饮料”

此时讨论可能会转向如何处理客户的情绪反应,这些反应可能在积极和消极之间大幅波动:从乐于助人(1),到轻微的挫折(4),再到对咖啡生产线的延迟和故障的明显愤怒(10)。但通过分析一个假设的用例,我们现在对如何利用捕捉好数据的艺术有了更深入的了解。

事件时间、事件捕捉顺序和事件间的延迟都讲述了一个故事

如果不了解从第一个事件(customer.order:initialized)到终端事件(customer.order:fulfilled)之间的时间经过了多久,或者每个步骤通常需要多长时间来完成,我们将无法对体验进行评分或真正理解发生了什么,基本上就会在系统中创造出对异常延迟或故障的盲点。了解客户通常等待不同大小订单的时间的统计数据(*均值、中位数和 99 百分位数)是有益的,因为这些历史数据点可以通过自动化用于预先解决问题,例如当一个订单的处理时间比预期的要长时。这可能意味着客户的不满和终身客户之间的差别。

这是公司请求客户反馈的主要原因之一——无论是对体验的好评/差评,奖励基于应用程序的参与(用你的积分换取免费商品和服务),还是跟踪实时反馈,比如“你的订单比预期的时间长,这里有$2 折扣下次咖啡使用。只需使用应用程序兑换”。这些通过现实世界互动收集和捕获的数据,以事件形式编码,并为你的利益处理,最终是值得的,如果它积极影响公司的运营和声誉。只要确保遵循数据隐私规则和法规,最终不要让客户感到不适。

这个小小的思想实验旨在揭示事件数据中捕获的细节(以及数据故事随时间的演变)可以是一个游戏规则改变者,并进一步说明时间是赋予这些旅程动力或速度的维度。只有一个时间的问题。

时间的麻烦

虽然事件发生在精确的时间点,但时间的问题在于它也受时间和空间(位置)问题的影响。爱因斯坦利用他的相对论理论在宇宙尺度上解释了这个问题,但在更局部的尺度上这也是一个问题。例如,我有家人住在美国不同的地方。在大家的时间协调上可能会有困难,这种情况发生在简单的事件中,比如远程视频聊天或在现实世界中聚会。即使一切都已协调好,人们也习惯于稍微迟到

从我的家庭或一般人的角度看,关于事件的中心协调问题,你会开始看到这个问题不仅仅是跨时区(东部/中部或西海岸)的同步问题,而是如果你更仔细地看,时间相对于我们的本地/物理空间,会受到一定的时间漂移或时钟偏差的影响。

以现代数字时钟为例。它作为一个过程运行在你的智能手机、手表或众多“智能”连接设备中。保持不变的是时间始终明显同步(即使漂移的范围是毫秒级)。许多人仍然使用模拟的非数字时钟。这些设备的准确度从高端手表(“计时器”)的极度准确到需要每几天重新校准的便宜时钟不等。

关键点在于,两个系统在精确时间上达成一致的情况很少,就像两个人在时间和空间上协调类似问题一样。因此,必须使用一个中央参考点(视角)来同步跨多个时区运行的系统的时间。

时间校正

在任何现代云基础设施中运行的服务器都利用一个称为网络时间协议(NTP)的过程来校正时间漂移的问题。ntp过程负责使用一个可靠的中央时间服务器同步本地服务器时钟。这个过程将本地时间校正到与协调世界时间(UTC)相差几毫秒。这是一个重要的概念,因为在大型网络中运行的应用程序,产生事件数据,将负责创建时间戳,而这些时间戳需要非常精确,以便分布式事件能够对齐。还有一个狡猾的问题是夏令时(每 6 个月增加或减少一小时),因此,协调跨时区以及跨本地日期时间语义(全球)的系统数据需要从这个中央、同步的视角来看待时间。

我们已经从理论上看过时间如何与事件驱动的数据相关,但为了全面了解背景,我们还应该考虑时间如何与数据在系统(无论是流式还是其他)中需要被捕获和处理的优先级相关。

优先级排序事件处理模式

你可能对这句名言很熟悉:“时间就是生命。”这句话的意思是某事很重要,是首要任务。解决问题的速度至关重要。这种优先级感可以作为一个工具或定义指标,用来为实时接*实时批处理最终处理(按需处理)的数据处理方式辩护。这四种处理模式以不同的方式处理时间,通过对数据问题施加窄或宽的焦点来应对。这里的范围是基于一个过程必须完成的速度,这反过来限制了工作的复杂性作为时间的一个因素。可以把这些处理风格看作是以截止日期驱动的,完成一个动作的时间是有限的。

实时处理

实时系统的期望是,从上游系统发出事件的时间到该事件被处理并可用于分析和洞察的时间,端到端延迟应在毫秒到低秒级别内。这些事件直接写入事件流处理服务,如 Apache Kafka,在正常情况下,允许监听者(消费者)一旦写入事件就能立即使用。真正的实时系统有许多典型用例,包括物流(如停车位示例以及在咖啡馆找到桌子),以及影响整个业务的过程,如欺诈检测、主动网络入侵检测或其他坏演员检测,其中检测的*均时间(毫秒/秒到检测)越长,可能会导致声誉、财务或两者的灾难性后果。

对于其他系统而言,运行在*实时状态是完全可以接受的。考虑到解决难题需要时间,实时决策需要高效、预计算或低延迟的答案。这确实是纯内存流处理。

*实时处理

*实时是大多数人在考虑实时时的想法。这里发生的模式类似于你刚刚在实时部分阅读的,唯一的区别是端到端延迟的期望放宽到高秒级别到几分钟。对于大多数系统而言,没有真正的理由对每个到达的事件做出立即反应,因此,虽然时间仍然很重要,但数据可用性的 SLA 优先级会有所延长。

操作仪表板和度量系统通常更新迅速(每 30 秒—5 分钟刷新图表和检查监控),足够快以捕捉问题,并给出接*现实的表示。对于所有其他数据系统,你会有批处理或按需处理的概念。

批处理

我们在前两章中涵盖了批处理和周期性调度,但为了明确,将数据从可靠的真实数据源(数据湖或数据库)推送到其他连接系统的周期性作业,一直以来都是世界数据处理的主要方式。

这背后的简单原因是成本。这涉及到操作成本和维护大型流媒体系统的人力成本。

流处理系统要求全天候访问从 CPU 和 GPU 到网络 IO 和 RAM 的可变数量资源,期望这些资源不会短缺,因为流处理中的延迟(阻塞)可能会迅速积累。另一方面,批处理在长期维护上可能更容易,只要数据的消费者理解从数据首次发出到下游数据可用之间始终存在间隔。

最后需要考虑的是按需处理(或即时处理)。

按需或即时处理

说实话,有些问题(即查询)问得非常少,或者以一种不适合任何预定义模式的方式提出。

例如,自定义报告任务和探索性数据分析是两种适合这些范式的数据访问风格。大多数情况下,回答这些查询的后端数据直接从数据湖中加载,然后使用共享计算资源或隔离计算集群进行处理。为这些查询提供的数据可能是其他实时或接*实时系统的副产品,这些系统被处理和存储用于批处理或历史分析。

使用这种模式,数据可以解冻,并通过将记录从较慢的对象存储(如 Amazon S3)导入内存,或通过快速访问的固态硬盘(SSD),或者根据数据的大小、格式和布局,直接从云对象存储中查询。这种模式可以轻松委托给 Apache Spark,使用SparkSQL。这使得通过像 Apache Zeppelin 这样的工具进行临时分析成为可能,或通过 JDBC 绑定直接在应用程序中使用Apache Spark thrift-server和 Apache Hive Metastore。

这四种处理方式之间的区别在于时间

回到视角和观点的概念,每种方法或模式都有其时间和地点。流处理处理的是在特定时间点捕获的事件,正如我们在本章前半部分讨论的那样,我们如何关联时间,以及我们如何捕捉和测量一系列事件(作为数据),共同绘制了当前发生的情况或过去发生的情况的画面。在我们对流处理的温和介绍中,重要的是还要讨论流处理的基础。在下一节中,我们将讨论处理连续、无界数据流的一些常见问题和解决方案。因此,讨论数据作为核心支柱并从那里扩展开来是有意义的。

希望你喜欢第九章的前半部分。如果你想继续阅读第二部分,它在下面有链接。👇

## 谦逊的分析流处理介绍

构建可靠分布式系统的架构基础。

前往数据科学

如果你想了解更多,请查看我的书!

现代数据工程与 Apache Spark:构建关键任务流处理的实用指南

亚马逊网站:现代数据工程与 Apache Spark:构建关键任务流处理的实用指南…

www.amazon.com

一个好的描述就是你所需要的一切

原文:towardsdatascience.com/a-good-description-is-all-you-need-1d0b47be10ec?source=collection_archive---------12-----------------------#2023-08-10

如何使用少量学习来提高文本分类性能

Ilia Teimouri PhDTowards Data Science Ilia Teimouri PhD

·

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

--

Patrick Tomasso拍摄,来源于Unsplash

我已经使用大语言模型(LLMs)一段时间了,既用于个人项目,也作为日常工作的组成部分。像许多人一样,我对这些模型的强大能力感到兴奋。然而,重要的是要知道,尽管这些模型非常强大,但仍然可以针对各种任务进行改进。

而且不,我不会写关于微调 LLMs的内容,因为这可能会很昂贵,并且通常需要一台好的 GPU 设备。事实上,我将展示一种非常简单的使用少量学习来改善你的模型的方法。

少样本学习是一种机器学习技术,其中模型通过仅使用少量示例(通常每类仅 1–5 个示例)来解决新任务。少样本学习有一些关键点:

  • 从小数据中学习归纳:少样本学习方法旨在学习能够从少量示例中很好地归纳模型,这与传统的深度学习方法(需要数千或数百万个示例)形成对比。

  • 迁移学习:少样本学习方法利用从解决先前任务中获得的知识,并将这些知识转移到帮助更快地学习新任务和更少的数据。这种迁移学习能力是关键。

  • 学习相似度度量:一些少样本学习技术专注于学习示例之间的相似度度量。这允许将新示例与现有标记示例进行比较以进行预测。

但如何在分类问题中使用少样本学习以提高模型性能?让我们通过一个例子来演示。

数据和准备

我通过从 HuggingFace 获取数据来开始我的分析。数据集名为financial-reports-sec(该数据集具有 Apache 许可证 2.0 并允许商业使用),根据数据集作者的说法,它包含了 1993–2020 年美国上市公司向 SEC EDGAR 系统提交的年度报告(10-K 文件)。每份年度报告(10-K 文件)被分为 20 个部分。

当前任务的两个相关属性对数据很有用:

  • 句子:来自 10-K 文件报告的摘录

  • 部分:标记句子所属的 10-K 文件部分

我关注了三个部分:

  • 业务(项目 1):描述公司的业务,包括子公司、市场、最*事件、竞争、法规和劳动力。在数据中用 0 表示。

  • 风险因素(项目 1A):讨论可能影响公司的风险,例如外部因素、潜在故障和其他警告投资者的披露。用 1 表示。

  • 属性(项目 2):详细说明重要的实物资产。不包括知识产权或无形资产。在数据中用 3 表示。

对于每个标签,我抽取了 10 个示例(不放回)。数据结构如下:

现成预测

一旦数据准备好,我所要做的就是制作一个分类器函数,该函数从数据框中获取句子并预测标签。

Role = '''
You are expert in SEC 10-K forms. 
You will be presented by a text and you need to classify the text into either 'Item 1', 'Item 1A' or 'Item 2'. 
The text only belongs to one of the mentioned categories so only return one category.
'''
def sec_classifier(text): 

    response = openai.ChatCompletion.create(
        model='gpt-4',
        messages=[
            {
                "role": "system",
                "content": Role},
            {
                "role": "user",
                "content": text}],
        temperature=0,
        max_tokens=256,
        top_p=1,
        frequency_penalty=0,
        presence_penalty=0)

    return response['choices'][0]['message']['content']

我在这里使用 GPT-4,因为这是迄今为止 OpenAI 最强大的模型。我还将温度设置为 0,以确保模型不会偏离轨道。真正有趣的部分是如何定义角色——这就是我可以指导模型做我想要它做的事情的地方。角色指示模型保持专注并提供我所期望的输出。为模型定义一个清晰的角色有助于生成相关的、高质量的响应。这个功能中的提示是:

你是 SEC 10-K 表格的专家。

你将收到一段文本,你需要将文本分类为‘第 1 项’,‘第 1A 项’或‘第 2 项’。

文本只属于提到的一个类别,因此只返回一个类别。

在对所有数据行应用分类功能后,我生成了一个分类报告来评估模型的性能。宏*均 F1 分数为 0.62,表明该多类问题的预测能力相当强。由于所有 3 个类别的示例数量均衡,宏*均和加权*均值收敛到相同的值。这个基准分数反映了在任何额外调整或优化之前,预训练模型的开箱即用的准确性。

 precision    recall  f1-score   support

      Item 1       0.47      0.80      0.59        10
     Item 1A       0.80      0.80      0.80        10
      Item 2       1.00      0.30      0.46        10

    accuracy                           0.63        30
   macro avg       0.76      0.63      0.62        30
weighted avg       0.76      0.63      0.62        30

描述是你所需要的(少样本预测)

如前所述,少样本学习就是通过少量好的示例来推广模型。为此,我通过描述第 1 项、第 1A 项和第 2 项是什么(基于维基百科)来修改了我的类别:

Role_fewshot = '''
You are expert in SEC 10-K forms. 
You will be presented by a text and you need to classify the text into either 'Item 1', 'Item 1A' or 'Item 2'. 
The text only belongs to one of the mentioned categories so only return one category.
In your classification take the following definitions into account: 

Item 1 (i.e. Business) describes the business of the company: who and what the company does, what subsidiaries it owns, and what markets it operates in. 
It may also include recent events, competition, regulations, and labor issues. (Some industries are heavily regulated, have complex labor requirements, which have significant effects on the business.) 
Other topics in this section may include special operating costs, seasonal factors, or insurance matters.

Item 1A (i.e. Risk Factors) is the section where the company lays anything that could go wrong, likely external effects, possible future failures to meet obligations, and other risks disclosed to adequately warn investors and potential investors.

Item 2 (i.e. Properties) is the section that lays out the significant properties, physical assets, of the company. This only includes physical types of property, not intellectual or intangible property.

Note: Only state the Item.
'''
def sec_classifier_fewshot(text): 

    response = openai.ChatCompletion.create(
        model='gpt-4',
        messages=[
            {
                "role": "system",
                "content": Role_fewshot},
            {
                "role": "user",
                "content": text}],
        temperature=0,
        max_tokens=256,
        top_p=1,
        frequency_penalty=0,
        presence_penalty=0)

    return response['choices'][0]['message']['content']

现在的提示是:

你是 SEC 10-K 表格的专家。

你将收到一段文本,你需要将文本分类为‘第 1 项’,‘第 1A 项’或‘第 2 项’。

文本只属于提到的一个类别,因此只返回一个类别。

在你的分类中考虑以下定义:

第 1 项(即业务)描述了公司的业务:公司是谁,做什么,拥有哪些子公司,以及运营的市场。

它还可能包括*期事件、竞争、法规和劳动问题。(一些行业受到严格监管,具有复杂的劳动要求,这些都对业务产生重大影响。)

本节中的其他主题可能包括特殊操作成本、季节性因素或保险问题。**

第 1A 项(即风险因素)是公司列出可能出现问题的部分,包括可能的外部影响、未来未能履行义务的可能性以及其他风险,以充分警示投资者和潜在投资者。

第 2 项(即属性)是列出公司重要属性、实物资产的部分。这仅包括实物类型的财产,而不包括知识产权或无形财产。

如果我们在这些文本上运行,就会得到以下性能:

 precision    recall  f1-score   support

      Item 1       0.70      0.70      0.70        10
     Item 1A       0.78      0.70      0.74        10
      Item 2       0.91      1.00      0.95        10

    accuracy                           0.80        30
   macro avg       0.80      0.80      0.80        30
weighted avg       0.80      0.80      0.80        30

宏*均 F1 现在是 0.80,也就是我们的预测提升了 29%,这仅仅是通过提供每个类别的良好描述。

最终,你可以查看完整的数据集:

实际上,我提供的示例为模型提供了具体的学习实例。通过查看多个示例,模型可以推断出模式和特征,开始注意到标志总体概念的共同点和差异。这有助于模型形成更为稳健的表示。此外,提供示例本质上充当了一种弱监督形式,引导模型朝着期望的行为发展,而不依赖于大型标记数据集。

在少量样本功能中,具体示例帮助引导模型关注应注意的信息和模式。总之,具体示例对少量样本学习至关重要,因为它们为模型提供了建立新概念初步表示的锚点,然后可以在提供的少量示例中进行细化。通过特定实例的归纳学习帮助模型形成抽象概念的细致表示。

如果你喜欢阅读这些内容并希望保持联系,你可以在我的LinkedIn上找到我,或通过我的网页:iliateimouri.com

注意:所有图片,除非另有说明,均由作者提供。

《生产就绪的 RAG 应用的 12 种调整策略指南》

原文:towardsdatascience.com/a-guide-on-12-tuning-strategies-for-production-ready-rag-applications-7ca646833439?source=collection_archive---------0-----------------------#2023-12-06

如何通过这些“超参数”和调整策略来提升你的检索增强生成(RAG)管道的性能

Leonie MonigattiTowards Data Science Leonie Monigatti

·

关注 发布于 Towards Data Science · 10 min 阅读 · 2023 年 12 月 6 日

--

《检索增强生成(RAG)应用的调整策略》

数据科学是一门实验性科学。它以“无免费午餐定理”开始,该定理指出,没有一种万能的算法可以适用于所有问题。这导致数据科学家使用实验跟踪系统来帮助他们调整机器学习(ML)项目的超参数,以实现最佳性能.

本文从数据科学家的角度审视了检索增强生成(RAG)管道。讨论了可以实验的潜在“超参数”以提高 RAG 管道的性能。类似于深度学习中的实验,在深度学习中,例如,数据增强技术不是超参数,而是一个可以调整和实验的控制旋钮,本文还将涵盖可以应用的不同策略,这些策略本身不一定是超参数。

## 检索增强生成(RAG):从理论到 LangChain 实现

从原始学术论文的理论到使用 OpenAI、Weaviate 和 LangChain 的 Python 实现

towardsdatascience.com

本文涵盖了按相关阶段排序的“超参数”。在 RAG 管道的吞吐阶段,你可以通过以下方法实现性能提升:

  • 数据清洗

  • 数据分块

  • 嵌入模型

  • 元数据

  • 多索引

  • 索引算法

在推理阶段(检索和生成),你可以调整:

  • 查询转换

  • 检索参数

  • 高级检索策略

  • 重新排序模型

  • 大型语言模型(LLMs)

  • 提示工程

请注意,本文涵盖了 RAG 的文本使用案例。对于多模态 RAG 应用,可能需要不同的考虑因素。

吞吐阶段

吞吐阶段是构建 RAG 管道的准备步骤,类似于 ML 管道中的数据清洗和预处理步骤。通常,吞吐阶段包括以下步骤:

  1. 收集数据

  2. 数据分块

  3. 生成数据块的向量嵌入

  4. 在向量数据库中存储向量嵌入和数据块

RAG 管道的吞吐阶段

本节讨论了在推理阶段可以应用和调整的有影响力的技术和超参数,以提高检索到的上下文的相关性。

数据清洗

像任何数据科学管道一样,你的数据质量对 RAG 管道的结果有着重要影响[8, 9]。在进行以下步骤之前,确保你的数据符合以下标准:

  • 清洁:应用至少一些自然语言处理常用的基本数据清理技术,如确保所有特殊字符都正确编码。

  • 正确:确保你的信息一致且事实准确,以避免信息冲突让你的 LLM 感到困惑。

分块

在 RAG 管道中,分块你的文档是对外部知识源的一个关键准备步骤,可能会影响性能[1, 8, 9]。这是一种生成逻辑上连贯的信息片段的技术,通常通过将长文档拆分成较小的部分(但也可以将较小的片段合并成连贯的段落)。

你需要考虑的一个方面是分块技术的选择。例如,在LangChain 中,不同的文本分割器根据不同的逻辑拆分文档,如按字符、标记等。这取决于你拥有的数据类型。例如,如果你的输入数据是代码与 Markdown 文件,你将需要使用不同的分块技术。

理想的块长度(**chunk_size**取决于你的使用场景:如果你的使用场景是问答,你可能需要较短的具体块;但如果你的使用场景是摘要,你可能需要较长的块。此外,如果块太短,可能包含的上下文不够。另一方面,如果块太长,可能包含过多的无关信息。

此外,你还需要考虑块之间的“滚动窗口”(**overlap**以引入一些额外的上下文。

嵌入模型

嵌入模型是你检索的核心。嵌入的质量对检索结果有着重大影响[1, 4]。通常,生成的嵌入维度越高,嵌入的精度也越高。

关于可用的替代嵌入模型,你可以查看MASSIVE TEXT EMBEDDING BENCHMARK (MTEB)排行榜,该排行榜涵盖了 164 种文本嵌入模型(截至本文撰写时)。

[## MTEB 排行榜 - 由 mteb 提供的 Hugging Face 空间

发现社区制作的精彩 ML 应用

huggingface.co](https://huggingface.co/spaces/mteb/leaderboard?source=post_page-----7ca646833439--------------------------------)

虽然你可以直接使用通用的嵌入模型,但在某些情况下,对你的嵌入模型进行微调可能更有意义,以避免之后出现领域外的问题 [9]。根据 LlamaIndex 进行的实验,微调你的嵌入模型可以导致检索评估指标性能提高 5–10% [2]。

请注意,并非所有嵌入模型都可以微调(例如,OpenAI 的 [text-embedding-ada-002](https://platform.openai.com/docs/guides/fine-tuning) 目前不能进行微调)。

元数据

当你将向量嵌入存储在一个向量数据库中时,一些向量数据库允许你将它们与元数据(或未向量化的数据)一起存储。用元数据注释向量嵌入对搜索结果的额外后处理可能是有帮助的,例如元数据过滤 [1, 3, 8, 9]。例如,你可以添加元数据,如日期、章节或子章节参考。

多重索引

如果元数据不足以提供额外的信息以逻辑地分隔不同类型的上下文,你可能需要尝试多重索引 [1, 9]。例如,你可以为不同类型的文档使用不同的索引。注意,在检索时你需要进行一些索引路由 [1, 9]。如果你对元数据和分离集合有更深入的兴趣,你可能想了解更多关于原生多租户的概念。

索引算法

为了在大规模上实现快速相似性搜索,向量数据库和向量索引库使用*似最*邻(ANN)搜索而不是 k-最*邻(kNN)搜索。顾名思义,ANN 算法*似最*邻,因此可能不如 kNN 算法精确。

你可以尝试不同的 ANN 算法,例如Facebook Faiss(聚类)、Spotify Annoy(树)、Google ScaNN(向量压缩)和HNSWLIB(邻*图)。此外,这些 ANN 算法中的许多都有一些参数可以调整,例如 HNSW 的efefConstructionmaxConnections [1]。

此外,你可以为这些索引算法启用向量压缩。类似于 ANN 算法,向量压缩会导致一定的精度损失。然而,根据向量压缩算法的选择及其调整,你也可以对此进行优化。

然而,在实践中,这些参数通常由向量数据库和向量索引库的研究团队在基准测试实验期间进行调整,而不是由 RAG 系统的开发人员进行调整。不过,如果你想通过调整这些参数来挤出最后的性能提升,我推荐这篇文章作为起点:

[## 关于 RAG 评估的概述 | Weaviate - 向量数据库

了解 RAG 评估中的新趋势及当前的最新技术。

weaviate.io](https://weaviate.io/blog/rag-evaluation?source=post_page-----7ca646833439--------------------------------#indexing-knobs)

推理阶段(检索与生成)

RAG 管道的主要组成部分是检索和生成组件。本节主要讨论提高检索(查询转换、检索参数、高级检索策略和重新排序模型)的策略,因为这是两个组件中影响更大的部分。但它也简要涉及一些提高生成(LLM 和提示工程)的策略。

RAG 管道的推理阶段

查询转换

由于在 RAG 管道中检索附加上下文的搜索查询也被嵌入到向量空间中,因此其措辞也会影响搜索结果。因此,如果你的搜索查询没有产生令人满意的结果,你可以尝试各种查询转换技术 [5, 8, 9],例如:

  • 改写: 使用 LLM 改写查询并重试。

  • 假设文档嵌入(HyDE): 使用 LLM 生成对搜索查询的假设响应,并将两者用于检索。

  • 子查询: 将较长的查询拆分为多个较短的查询。

检索参数

检索是 RAG 管道的一个重要组成部分。首先需要考虑的是语义搜索是否足够满足你的用例,还是你想尝试混合搜索。

在后一种情况下,你需要尝试对混合搜索中的稀疏和密集检索方法的加权进行实验[1, 4, 9]。因此,调整参数**alpha**,即控制语义搜索(**alpha = 1**)和基于关键词的搜索(**alpha = 0****)之间加权的参数,将变得必要。

[## 通过混合搜索提高 RAG 管道中的检索性能

如何通过将传统的基于关键词的搜索与现代向量搜索相结合来找到更相关的搜索结果

towardsdatascience.com/improving-retrieval-performance-in-rag-pipelines-with-hybrid-search-c75203c2f2f5?source=post_page-----7ca646833439--------------------------------

此外,检索的搜索结果数量将发挥重要作用。检索的上下文数量将影响所用上下文窗口的长度(见 Prompt Engineering)。此外,如果你使用的是重排序模型,你需要考虑输入模型的上下文数量(见 Re-ranking models)。

注意,虽然用于语义搜索的相似度度量是一个可以更改的参数,你不应随意实验,而是应根据所用的嵌入模型设置(例如,[text-embedding-ada-002](https://platform.openai.com/docs/guides/embeddings/what-are-embeddings) 支持余弦相似度或 [multi-qa-MiniLM-l6-cos-v1](https://huggingface.co/sentence-transformers/multi-qa-MiniLM-L6-cos-v1#technical-details) 支持余弦相似度、点积和欧几里得距离)。

高级检索策略

本节技术上可以作为一篇独立的文章。为了本概述,我们将尽量简洁。有关以下技术的详细说明,我推荐这个 DeepLearning.AI 课程:

www.deeplearning.ai/short-courses/building-evaluating-advanced-rag/?source=post_page-----7ca646833439-------------------------------- [## 构建和评估高级 RAG 应用

学习句子窗口检索和自动合并检索等方法,提高你的 RAG 流水线的性能……

www.deeplearning.ai/short-courses/building-evaluating-advanced-rag/?source=post_page-----7ca646833439--------------------------------

本节的基本思想是检索的块不一定要与生成所用的块相同。理想情况下,你会为检索嵌入较小的块(见 Chunking),但检索更大的上下文。[7]

  • 句子窗口检索: 不仅检索相关句子,还要检索在检索句子之前和之后的适当句子。

  • 自动合并检索: 文档以树状结构组织。在查询时,可以将分开但相关的小块合并成一个更大的上下文。

重排序模型

虽然语义搜索根据与搜索查询的语义相似性检索上下文,但“最相似”并不一定意味着“最相关”。重排序模型,如 Cohere’s Rerank 模型,可以通过计算每个检索上下文对查询的相关性分数来帮助消除不相关的搜索结果 [1, 9]。

“最相似”并不一定意味着“最相关”

如果你使用的是重排序模型,你可能需要重新调整搜索结果数量以供重排序模型输入,并决定你希望将多少个重排序的结果输入到 LLM 中。

与嵌入模型一样,你可能还想尝试对重排序模型进行微调以适应你的特定用例。

LLMs

LLM 是核心组件,用于生成响应。类似于嵌入模型,根据你的要求(如开放 vs. 专有模型、推理成本、上下文长度等),你可以选择不同的 LLM。[1]

与嵌入模型或重排序模型一样,你可能想要尝试对 LLM 进行微调以适应你的特定用例,以融入特定的措辞或语气。

提示工程

你如何表述或工程化你的提示将显著影响 LLM 的完成质量[1, 8, 9]。

Please base your answer only on the search results and nothing else!
Very important! Your answer MUST be grounded in the search results provided. 
Please explain why your answer is grounded in the search results!

此外,在提示中使用少量示例可以提高完成的质量。

如检索参数中提到的,输入提示的上下文数量是你应该尝试的一个参数[1]。虽然随着相关上下文的增加,你的 RAG 管道性能可能会提高,但你也可能会遇到“在中间迷失”[6]效应,即如果相关上下文被放置在许多上下文的中间,LLM 可能不会将其识别为相关。

摘要

随着越来越多的开发者获得原型开发 RAG 管道的经验,讨论将 RAG 管道带到生产就绪性能的策略变得越来越重要。本文讨论了不同的“超参数”和在 RAG 管道的相关阶段中可以调整的其他参数:

本文涵盖了摄取阶段中的以下策略:

  • 数据清理:确保数据是干净和正确的。

  • 分块:选择分块技术、分块大小(chunk_size)和分块重叠(overlap)。

  • 嵌入模型:选择嵌入模型,包括维度,以及是否进行微调。

  • 元数据:是否使用元数据及其选择。

  • 多索引:决定是否对不同的数据集合使用多个索引。

  • 索引算法:选择和调整 ANN 和向量压缩算法,通常不由从业者进行调整。

以及在推理阶段(检索和生成)中的以下策略:

  • 查询转换:尝试重新表述、HyDE 或子查询。

  • 检索参数:选择搜索技术(如果启用了混合搜索,则为alpha)和检索结果的数量。

  • 高级检索策略:是否使用高级检索策略,如句子窗口或自动合并检索。

  • Re-ranking models:是否使用重新排序模型、选择重新排序模型、输入到重新排序模型中的搜索结果数量以及是否对重新排序模型进行微调。

  • LLMs:选择 LLM 和是否对其进行微调。

  • Prompt engineering:尝试不同的措辞和少量示例。

享受了这个故事吗?

免费订阅 以获取我发布新故事时的通知。

[## 每当 Leonie Monigatti 发布新内容时获取电子邮件通知。

每当 Leonie Monigatti 发布新内容时,获取电子邮件通知。注册后,如果你还没有 Medium 账户,将会创建一个…

medium.com](https://medium.com/@iamleonie/subscribe?source=post_page-----7ca646833439--------------------------------)

LinkedInTwitter,以及 Kaggle上找到我

参考文献

文献

[1] Connor ShortenErika Cardenas(2023)。Weaviate 博客。RAG 评估概述(访问日期:2023 年 11 月 27 日)

[2] Jerry Liu(2023)。LlamaIndex 博客。使用合成数据对 RAG 进行嵌入微调(访问日期:2023 年 11 月 28 日)

[3] LlamaIndex 文档(2023)。为生产构建高性能 RAG 应用程序(访问日期:2023 年 11 月 28 日)

[4] Voyage AI(2023)。嵌入推动 RAG 的质量:Chat.LangChain 的案例研究(访问日期:2023 年 12 月 5 日)

[5] LlamaIndex 文档(2023)。查询转换(访问日期:2023 年 11 月 28 日)

[6] Liu, N. F., Lin, K., Hewitt, J., Paranjape, A., Bevilacqua, M., Petroni, F., & Liang, P.(2023)。《迷失在中间:语言模型如何使用长上下文》。arXiv 预印本 arXiv:2307.03172

[7] DeepLearning.AI(2023)。构建和评估高级 RAG 应用程序(访问日期:2023 年 12 月 4 日)

[8] Ahmed Besbes(2023)。Towards Data Science。为什么你的 RAG 在生产环境中不可靠(访问日期:2023 年 11 月 27 日)

[9] Matt Ambrogi(2023 年)。面向数据科学。提高检索增强生成系统性能的 10 种方法(访问日期:2023 年 11 月 27 日)

图片

除非另有说明,所有图片均由作者创作。

机器学习中的 21 种特征重要性方法和包指南(附代码)

原文:towardsdatascience.com/a-guide-to-21-feature-importance-methods-and-packages-in-machine-learning-with-code-85a841f8b319

从 OmniXAI、Shapash 和 Dalex 解释性包到 Boruta、Relief 和随机森林特征选择算法

Theophano Mitsa 博士Towards Data Science Theophano Mitsa 博士

·发表在Towards Data Science ·阅读时间 19 分钟·2023 年 12 月 19 日

--

图片由作者在 DALL-E 上创建

“我们就是我们的选择。” —让-保罗·萨特

我们生活在人工智能的时代,主要是由于大规模语言模型(LLMs)的惊人进步。对于机器学习工程师来说,学习这些新技术固然重要,但同样重要的是掌握模型选择、优化和部署的基本概念。还有一件事非常重要:上述的输入,即数据特征。数据,就像人一样,有称为特征的属性。对于人,你必须理解他们的独特特征才能发挥他们的最佳能力。同样的原则也适用于数据。具体来说,本文关于特征重要性,衡量一个特征对模型预测能力的贡献。我们必须理解特征重要性,有很多重要原因:

  • 时间:特征过多会拖慢训练模型的时间,也会影响模型的部署。后者在边缘应用(如移动设备、传感器、医疗诊断)中尤为重要。

  • 过拟合。如果我们的特征没有被仔细选择,我们可能会让模型过拟合,即也学习噪声。

  • 维度诅咒。许多特征意味着许多维度,这使得数据分析变得指数级困难。例如,k-NN 分类,一种广泛使用的算法,受到维度增加的重大影响。

  • 适应性和迁移学习。这是我最喜欢的原因,也是写这篇文章的真正原因。在迁移学习中,经过一个任务训练的模型可以在经过一些微调后用于第二个任务。对第一和第二个任务中的特征有很好的理解,可以大大减少你需要进行的微调。

我们将重点讨论表格数据,并介绍二十一种评估特征重要性的方法。有人可能会问:‘为什么是二十一种技术?难道一种就够了吗?’ 讨论所有二十一种技术很重要,因为每一种都有独特的特点,非常值得学习。具体而言,我将在文章中以以下两种方式指明某一技术值得学习的原因:(a)标题为“为什么这很重要”的部分,以及(b)突出独特一词,表示我在谈论一个特殊且独特的特征。

我们将讨论的技术来自机器学习的两个不同领域:可解释性特征选择。具体而言,我们将讨论以下内容:

可解释性 Python 包。 这些库通过提供输入特征如何影响模型预测的见解,帮助使模型的决策过程更加透明。我们将讨论以下内容:OmniXAIShapashDALEXInterpretMLELI5

特征选择方法。 这些方法专注于通过识别最具信息量的特征来减少模型的特征,它们通常分为过滤器、嵌入式和包装三类。每一类的特点将在下一节中讨论。我们将从每一类中讨论以下内容:

  • 包装方法:递归特征消除、顺序特征选择,Boruta 算法。

  • 嵌入式方法:逻辑回归,RandomForestLightGBMCatBoostXGBoostSelectFromModel

  • 过滤器方法:互信息,MRMR 算法,SelectKBestRelief 算法。

  • 其他:Featurewiz 包,Selective 包,PyImpetus 包。

数据

为了演示上述特征重要性计算技术,我们将使用来自 Kaggle 的与心力衰竭预测相关的表格数据:www.kaggle.com/datasets/fedesoriano/heart-failure-prediction/data

数据集包含 918 行和 12 列,对应以下特征:

  • ‘Age’,‘Sex’(M, F),‘ChestPainType’(TA,ATA,NAP,ASY),‘RestingBP’,

  • ‘Cholesterol’,‘FastingBS’(0,1),‘RestingECG’(正常,ST,LVH),‘MaxHR’,

  • ‘ExerciseAngina’(Y,N),‘Oldpeak’,‘ST_Slope’(上升、*坦、下降),

  • ‘HeartDisease’(0,1)。这是目标变量。0 表示没有心脏病,1 表示有心脏病。

数据集没有缺失值,目标变量相对*衡,有 410 个‘0’实例和 508 个‘1’实例。五个特征是类别型的:‘性别’,‘ChestPainType’,‘ExerciseAngina’,‘RestingECG’,‘ST_Slope’。这些特征通过one-hot-encoding Pandas方法进行了编码:

然后,数据被分割为训练集和测试集。最后,scikit-learnStandardScaler被应用于训练集和测试集的数值数据。现在,我们可以开始特征重要性评估了。

特征重要性评估

A. 可解释性包

它们为何重要

*年来,机器学习算法的可解释性引起了广泛关注。机器学习算法最*在许多领域找到了应用,如金融、医学、环境建模等。非专业的机器学习用户广泛使用这些算法,呼唤更多的透明度,因为:

  • 信任问题。 黑箱让人们感到不安,不确定是否应该信任它们。

  • 监管和伦理问题。全球各国政府越来越关注人工智能的使用,并制定立法以确保人工智能系统在没有任何偏见的情况下做出公*的决策。了解机器学习系统的内部工作原理是实现公*和无偏见的人工智能的重要前提。

可解释性包基于模型独立的解释框架SHAP(SHapley 加性解释)和LIME(局部可解释模型无关解释)[2]。SHAP使用博弈论方法,提供全局解释。另一方面,LIME提供局部解释。它们通过“解释器”的理念提供透明性。解释器是一种封装类型的对象;它可以封装模型,并提供进入模型内部细节的通道。

A.1 Shapash 包

Shapash [1]就是这样一个可解释性包。下面,我们看到Shapash的‘SmartExplainer’的实现,它以RandomForestClassifier类型的机器学习模型和特征名称作为输入。然后,调用Shapash的‘compile’函数,这是整个过程的‘核心’: (a) 将模型绑定到数据上, (b) 计算特征重要性, (c) 为可视化准备数据。最后,我们启动互动网页应用程序。

图 1,来自Shapash的应用界面,展示了特征的重要性。横条的长度对应于特征的重要性。因此,‘ST_Slope_up’是最重要的特征,而‘chest_pain_typeTA’是最不重要的。

图 1. 特征重要性

图 2 显示了Shapash 提供的一种重要类型的图,特征贡献。图中检查的特征是‘ST_Slope_up’。上半部分包含ST_Slope_up 的正贡献情况,而下半部分包含‘ST_Slope_up’的负贡献情况。此外,上半部分图形对应于‘ST_Slope_up’为 0 的情况,下半部分对应于‘ST_Slope_up’为 1 的情况。当我们点击显示结构中间的圆圈时,以下信息将显示:案例编号,‘ST_Slope_up’值,预测类别和‘ST_Slope_up’的贡献。

图 2. 特征贡献

图 3 显示了切片 131 的局部解释,其中预测类别为 1,概率为 0.8416。右侧的条形图显示对结果的正贡献,而左侧的条形图显示负贡献。‘St_Slope_up’具有最高的正贡献,而‘max_heart_rate’具有最高的负贡献。

图 3. 局部解释

总结来说,Shapash 是一个非常有用的软件包,因为 (a) 它提供了一个很好的界面,用户可以深入了解全局和局部解释,(b) 它提供了展示特征贡献的独特功能。

A.2. OMNIXAI 包

OMNIXAI [(开源可解释人工智能) [3]],如Shapash,也提供可视化工具,但其独特优势在于其解释技术的广泛性。具体来说,它提供了针对各种数据类型的预测解释方法,即表格数据、文本和图像。它的一些独特功能包括 (a) NLPExplainer,(b) 偏差检查模块,(c) 表格数据的莫里斯敏感性分析,(d) 用于图像分类的VisionExplainer,以及 (e) 反事实解释器。

下面的代码显示了OMNIXAI 解释器的创建。主要步骤包括 (a) 创建一个OMNIXAI 特定的数据类型(‘Tabular’)来保存数据,(b) 通过‘TabularTransform’进行数据预处理,(c) 数据分割为训练集和测试集,(d) 训练一个XGBClassifier 模型 (e) 将数据还原为原始格式 (f) 设置一个带有SHAPLIME 方法的XGBClassifier 的‘TabularExplainer’。解释将应用于‘test_instances’ [130–135] (g) 生成和显示预测

图 4 显示了使用LIME对切片 [130:135] 的聚合局部解释。右侧的绿色条形图显示对标签类别 1 的正贡献,而左侧的红色条形图显示对类别 1 的负贡献。条形图越长,贡献越显著。

图 4. LIME 解释

图 5 展示了使用 SHAP 对切片 [130:135] 进行的汇总局部解释。绿色/红色条的含义与上图相同。

图 5. SHAP 解释

A.3. InterpretML 包

InterpretML XAI 可解释性 [4] 包具有独特的‘glassbox 模型’特性,即固有可解释的模型。

下面的代码片段展示了一个固有可解释模型‘ExplainableBoostingClassifier’的实现。全局解释和切片 43 的局部解释也已设置。

图 6 展示了计算的全局特征重要性。

图 6. 全局特征重要性

图 7 展示了切片 43 的计算局部解释。大多数特征对类别 1 的预测有积极贡献,而只有‘Cholesterol’和‘FastingBS’有负面贡献。

图 7. 局部解释

A. 4 Dalex 包

Dalex 包 [5] 是一个旨在解释和理解机器学习模型的库。Dalex 代表“Descriptive mAchine Learning EXplanations。”它具有以下独特特性:

  • 它与 R 和 Python 兼容。

  • Aspects 模块。这使我们能够考虑特征间的相互依赖关系来解释模型。

  • Fairness 模块。它使我们能够评估模型的公*性。

下面的代码片段展示了 Dalex 的‘Explainer’的实现。

Dalex 生成的特征重要性见图 8。

图 8. 特征重要性

A.5 Eli5 包

我们将讨论的最终可解释性包是 Eli5 [5]。它具有以下独特特性:

  • 排列重要性度量。在此技术中,每个特征的值会被随机打乱,然后测量模型性能的下降。下降越大,特征越重要。

  • 它处理文本数据。具体来说,它提供了一个‘TextExplainer’,可以解释文本分类器的预测。

  • 它与 Keras 兼容。

下面的代码片段中,‘PermutationImportance’ 方法被应用于 Support Vector Classification (‘svc’) 估计器。

图 9 展示了‘svc’估计器计算的特征重要性。

图 9.

B. 特征选择技术

包装方法

顾名思义,这些算法将特征选择过程包裹在机器学习算法周围。它们不断评估特征子集,直到找到根据标准产生最佳性能的子集。这个标准可以是以下之一:模型准确率、子集特征数量、信息增益等。

它们为何重要

这些算法的本质(标准优化、全面搜索)表明,这些方法在选择最佳特征方面可以具有非常好的性能。另一个非常有用的特性是它们考虑了特征交互。然而,它们的本质也表明,它们可能计算量大且可能会过拟合。因此,如果没有计算限制且准确性至关重要,这些方法是一个不错的选择。

B.1. 序列特征选择

序列特征选择SFS)以两种模式评估特征子集:前向选择,从没有特征开始,逐步添加特征;以及后向消除,从所有特征开始,逐一移除特征。

以下代码片段展示了围绕‘KNeighborsClassifier’模型实现的 SFS。它还展示了如何输出选择的特征及其名称。

选择的特征包括:

B.2 Boruta 算法

Boruta 是最有效的特征选择算法之一。最令人印象深刻的是,它不需要用户任何输入[7]!它基于‘影子特征’(所有原始特征的随机化副本)的巧妙想法。然后,应用随机森林分类器来评估每个真实特征相对于这些影子特征的重要性。这个过程会重复,直到识别出所有重要特征。

以下代码片段展示了使用Boruta包实现Boruta的过程以及选择的特征。

Boruta选择的特征包括:

B.3 RFECV 算法

RFECV(递归特征消除与交叉验证)是一种特征选择技术,它通过交叉验证逐步从模型中移除最不重要的特征,以找到最佳特征子集。代码实现见下方片段。

选择的特征包括:

嵌入方法

这些算法具有内置的计算特征重要性或选择特征的能力,如随机森林套索回归。对于这些方法,一个重要的注意事项是它们不会直接选择特征。相反,它们计算特征重要性,这些重要性可以在后续处理中用于选择特征。这样的后续过程是第 B.9 节讨论的‘SelectFromModel’。

它们为何重要

高维数据今天非常普遍,形式包括非结构化文本、图像和时间序列,特别是在生物信息学、环境监测和金融领域。嵌入方法的最大优势在于它们处理高维数据的能力。其原因在于它们没有单独的建模和特征选择步骤。特征选择和建模在一个步骤中完成,这大大加快了速度。

B.4 逻辑回归

逻辑回归是一种用于二分类的统计方法。模型的系数与特征的重要性相关。每个权重表示特征对目标变量对数赔率的影响方向(正向或负向)及强度。权重的绝对值越大,表示对应的特征在预测结果中越重要。下面的代码片段展示了逻辑回归的创建过程。超参数‘C’(正则化强度)和‘max_iter’通过应用scikit-learn的‘GridSearchCV’进行学习。

逻辑回归系数如下所示。

B.5 随机森林

随机森林是一种用于分类和回归的集成机器学习方法。它通过构建许多决策树并合并它们的结果来工作。它使用bagging技术,即对数据集应用有放回的抽样。然后,每个样本用于训练一个单独的决策树。随机森林的一个显著特点是在训练过程中计算特征重要性。它通过随机化一个特征(同时保持其他特征不变),然后检查错误增加的程度来完成这一点。计算特征重要性最常用的标准是特征用于分割节点时的*均不纯度下降MDI) [8]。下面的代码片段展示了scikit-learn的‘RandomForestClassifier’的计算,其中超参数已经如上所述使用scikit-learn的‘GridSearchCV’确定。

计算和显示特征重要性的代码如下所示。计算出的特征重要性见图 10。

图 10. 特征重要性

B.6 LightGBM 算法

LightGBM(Light Gradient Boosting Machine)是一种结合了速度和性能的梯度提升算法。由微软开发,因其处理大数据集的能力以及在内存和速度上的高效性而闻名。它的一些独特特点包括(a)能够过滤掉梯度较小的数据实例,专注于更关键的实例,(b)‘Exclusive Feature Bundling’(EFB):LightGBM通过捆绑互斥特征(那些很少同时为非零的特征)来减少特征数量。通过这种方式,算法提高了高维数据的效率 [9]。

下面的代码片段展示了LightGBM的实现。超参数(‘learning rate’,‘max_depth’,和‘n_estimators’)使用scikit-learn的‘GridSearchCV’算法进行选择。从LightGBM计算出的特征重要性见图 11。

图 11.

B.7 XGBoost 算法

XGBoost,即eXtreme Gradient Boosting,是梯度提升的高级实现。它具有以下独特特性:

  • 它可以有效利用所有可用的 CPU 核心或集群来并行创建树。同时,它还利用了缓存优化。

  • LightGBM 相比,XGBoost 以树的深度为单位(层级)生长树,而 LightGBM 以叶子为单位生长树。这使得 XGBoost 在处理大数据集时效率较低。

代码片段展示了 XGBoost 的实现,其中下述超参数 [10] 是基于在 ‘hyperopt’ 包中实现的 贝叶斯优化 选择的。这些超参数是:

  • ‘gamma’(分裂的最小损失减少)

  • ‘min_child_weight’(子节点中所有观察值的权重之和的最小值)

  • ‘max_depth’(最大树深度)

  • ‘reg_lambda’(L2 正则化处理)

最终,控制 L1 正则化的超参数 ‘reg_alpha’ 在实验后被手动设置。

图 12 显示了特征的重要性。注意由于 L1 正则化,一些重要性被设为零。

图 12. 特征重要性

B.8 CatBoost 算法

CatBoost [11] 是一个高性能的开源梯度提升库,特别适用于分类数据。具体来说,它不需要对分类变量进行任何预处理,如标签编码或 one-hot 编码。相反,它原生处理分类变量。CatBoost 使用对称树作为其基础预测器,并支持 GPU 加速。关于 CatBoost 在 Python 中的实现,值得注意的是所有非数值特征必须声明为 ‘category’ 类型。然后,如下片段所示,这些分类特征作为输入提供给模型的 fit 函数。

图 13 显示了由 CatBoost 计算的特征重要性。需要注意的是,特征的名称是原始数据集中(而不是 one-hot 编码后的)。由于 CatBoost 原生处理分类数据,因此 CatBoost 算法的输入是原始数据(而不是 one-hot 编码后的)。

图 13. 特征重要性

B.9 SelectFromModel 方法

‘SelectFromModel’ 是由 scikit-learn’s feature.selection 包提供的。它的独特特征在于它是一个 元转换器,可以与那些通过 coef_feature_importances_ 来分配特征重要性的模型一起使用。

与我们讨论的前面嵌入方法不同,‘SelectFromModel’ 实际上是 选择 特征的。下面的代码片段展示了使用这种方法进行特征选择的代码。

选定的特征是:

过滤特征选择方法

这些特征与任何机器学习模型无关。它们通常依赖统计度量来评估每个特征,例如目标变量和预测变量之间的相关性和互信息。

它们为何重要

过滤方法直观且计算非常简单,因此在许多数据量大的领域(如生物信息学[12]、环境研究和医疗保健研究[13])中作为初始特征选择步骤使用。

B.10 互信息

互信息度量了在给定另一个变量的知识时一个变量的不确定性(熵)的减少。预测变量与目标变量之间的互信息使用scikit-learnmutual_info_classif计算。每个预测变量的互信息得分如图 14 所示。

图 14. 互信息得分。

B.11 MRMR 算法

MRMR代表最大相关性最大冗余。正如名称所示,MRMR算法选择的特征具有以下特点:(a)最大相关,即与目标变量强相关,(b)最小冗余,即它们之间表现出高度的不相似性。冗余可以使用相关性或互信息度量计算,相关性可以使用 F 统计量或互信息计算[15]。MRMR是一种最小最优方法,因为它选择了一组特征,这些特征组合在一起具有最大的预测能力[14]。这与在 B.2 节讨论的Boruta算法形成对比,后者是一种全相关算法,因为它识别了所有对模型预测相关的特征。

下面的代码片段展示了使用‘mrmr’ Python 库实现的MRMR

最小最优特征集如下所示:

B.12 SelectKBest 方法

正如名称所示,该算法根据用户定义的评分选择 K 个最佳特征。数量 K 也是用户定义的。该算法可应用于分类和回归任务,提供多种评分函数。例如,对于分类,用户可以应用以下方法:(a)‘f_classif’,计算ANOVA F 值,(b)‘mutual_info_classif’,计算互信息,(c)chi2,计算预测变量与目标变量之间的卡方统计量[16]。下面的代码片段展示了 k=5 和评分函数‘f_classif’的SelectKBest的计算。

下面的图 15 显示了根据评分函数‘f_classif’的特征得分(重要性)。注意,虽然我们选择了 K=5,但图 15 显示了所有特征的得分。

图 15. 特征重要性。

B.13 Relief 算法

Relief独特 特征是以下想法:对于一个数据样本,找到同一类别中最*的邻居(‘*邻’)和另一类别中最*的邻居(‘*错’)。特征根据它们与‘*邻’的相似程度以及与‘*错’样本的区别程度进行加权。由于其对复杂特征关联的敏感性,Relief 在生物医学信息学中特别有用 [17]。在这里,我们使用了原始 Relief 算法的扩展版——ReliefF 算法,它可以应用于多类别分类。相比之下,原始的 Relief 算法仅适用于二分类情况。下面的代码片段展示了从 ‘kydavra’ Python 包中调用 ‘ReliefFselector’ 的方法。

从算法中选择的特征如下所示。

杂项特征选择技术

在这一最终类别中,我们将讨论 FeaturewizSelectivePyImpetus 包。

他们的重要性

每个包都有其 独特 的原因:(a)Featurewiz 是一个非常方便的 AutoML 包。它只需一行代码即可选择特征;(b)Selective 包提供了多种过滤和嵌入式过滤选择方法,可以通过一行代码轻松调用;(c)PyImpetus 包基于一种与所有其他特征选择技术非常不同的算法,即 Markov Blanket

B.14 Featurewiz

这是一个自动化特征选择工具 [18][19]。其调用方式如下面的代码片段所示。该工具在后台使用 ‘SULOV’ 算法(Searching for Uncorrelated List Of Variables),其基础是上文 B.11 节中描述的 MRMR 算法。‘SULOV’ 选择具有最高互信息分数和最小相关性的特征。然后,这些特征递归传递给 XGBoost 以找到最佳子集。

Featurewiz 中选择的特征如下所示。

B.15 Selective 特征选择库

这个库提供了多种特征选择方法用于分类和回归任务 [20]。其中一些方法包括:相关性、方差、统计分析(ANOVA f 检验分类、卡方检验等)、线性方法(线性回归、lassoridge 正则化等),以及基于树的方法(Random ForestXGBoost 等)。以下展示了这个库的一个用例。

使用 ‘TreeBased’ 方法选择的特征如下:

B.16 PyImpetus

这个算法的 独特 思路是 Markov Blanket,它是预测目标变量所需的最小特征集 [21][22]。它可用于分类和回归任务。其分类实现如下所示。

图 16 显示了选择的特征及其相对重要性。

图 16。

讨论与结论

在这篇文章中,我们讨论了从两个不同领域:解释性和特征选择,涉及的广泛特征重要性评估技术。鉴于讨论的算法多样性,自然会出现一个问题:“不同算法选择的最重要特征有多相似?”

让我们看看下面的表 1。该表有两列,对应于特征‘ST_Slope_up’和‘ST_Slope_flat’。行对应于我们在文章中使用的算法和包。数字 1、2、3 表示该特征被算法选择为最佳、第二最佳或第三最佳。

表 1。

如文章中所述,一些算法简单地输出了一组特征而没有任何顺序。在这种情况下,表中的 X 表示算法选择了该特征。如果表中有空缺,说明该特征未被相应算法选择为前三个最重要的特征。对于逻辑回归,考虑了系数的绝对值。对于CatBoost,我们为‘ST_Slope_up’和‘ST_Slop_flat’分配了 1,因为CatBoost将‘ST_Slope’选为最重要特征。最后,OMNIXAI包的结果未包含在内,因为它们仅提供了少数行的局部解释。

从表 1 的观察中出现了一个有趣的事实。除了LightGBM,特征‘ST_Slope_up’在报告特征重要性的算法中具有最高第二高的重要性。它也被大多数报告选择特征但不报告重要性的算法所选择。特征‘ST_Slope_Flat’也表现得相当好,因为它要么位于前三个最高重要性特征中,要么位于大多数算法选择的特征组中。

现在,让我们深入探讨另一个有趣的见解。这两个特征具有最高第二高互信息评分。正如我们在 B.10 节中看到的,这是一个只需一行代码即可计算的简单特征。因此,通过一行代码,我们获得了对数据中最重要特征的见解,这些特征由其他计算复杂度更高的算法报告。

本文讨论了二十一种计算特征重要性的包和方法,这是衡量特征对模型预测能力贡献的一个指标。欲进一步阅读,我推荐 [23],它讨论了特征的另一个作用,即其对模型的错误贡献。

完整代码可以在 github.com/theomitsa/Feature_importance/tree/main 找到。

感谢阅读!

脚注:

参考文献

  1. Shapash 包shapash.readthedocs.io/en/latest/

  2. Molnar, C.,可解释的机器学习,2023 年。christophm.github.io/interpretable-ml-book/

  3. OMNIXAI 包opensource.salesforce.com/OmniXAI/latest/omnixai.html

  4. InterpretML 包interpret.ml/

  5. Dalex 包dalex.drwhy.ai/

  6. Eli5 包eli5.readthedocs.io/en/latest/index.html

  7. Mazzanti, S.,Boruta 正如你希望别人解释给你听的那样详细解释,Medium: Towards Data Science,2020 年 3 月。

  8. Scornet E.,树、森林与基于杂质的变量重要性,2021 年,ffhal-02436169v3f,hal.science/hal-02436169v3/file/importance_variable.pdf

  9. Ke, G. 等人,LightGBM:一种高效的梯度提升决策树,NIPS 会议,页码 3149–3157,2017 年 12 月。

  10. Banerjee, P.,XGBoost 超参数调整指南www.kaggle.com/code/prashant111/a-guide-on-xgboost-hyperparameters-tuning

  11. Prokhorenkova, L. 等人,CatBoost:具有类别特征的无偏提升,NIPS’18:第 32 届国际神经信息处理系统会议论文集,页码 6639–6649,2018 年 12 月。

  12. Urbanowicz, R.J. 等人,生物信息学数据挖掘中基于 Relief 的特征选择方法基准测试,《生物医学信息学杂志》,第 85 卷,页码 168–188,2018 年 9 月。

  13. Raju, S.K.,SARS-CoV-2 呼吸道感染的互信息与特征选择评估,生物工程 (巴塞尔),第 10 卷,第 7 期,2023 年 7 月。

  14. Mazzanti, S.,“MRMR 正如你希望别人解释给你听的那样详细解释”,Medium: Towards Data Science,2021 年 2 月。

  15. Radovic, M. 等人,用于时间基因表达数据的最小冗余最大相关特征选择方法,BMC 生物信息学,2017 年 1 月。

  16. Kavya, D.,优化性能:用于机器学习中高效特征选择的 SelectKBest,Medium,2023 年 2 月。

  17. Urbanowicz, R. J. 等,基于 Relief 的特征选择:介绍与综述, 《生物医学信息学杂志》,第 85 卷,第 189–203 页,2018 年 9 月。

  18. Featurewiz 包, github.com/AutoViML/featurewiz

  19. Sharma, H., Featurewiz:快速选择数据中最佳特征的方法, Medium,Medium: Towards Data Science,2020 年 12 月。

  20. 选择性特征选择库, github.com/fidelity/selective

  21. PyImpetus 包, github.com/atif-hassan/PyImpetus

  22. Hassan, A. 等,PPFS:预测置换特征选择, arxiv.org/pdf/2110.10713.pdf

  23. Mazzanti, S.,你的特征重要吗?这并不意味着它们好, Medium: Towards Data Science,2023 年 8 月。

关联规则挖掘指南

原文:towardsdatascience.com/a-guide-to-association-rule-mining-96c42968ba6

使用 Python 通过市场篮子分析创建频繁模式的洞察

Idil IsmiguzelTowards Data Science Idil Ismiguzel

·发布于 Towards Data Science ·阅读时间 10 分钟·2023 年 4 月 5 日

--

图片来源 Matthias SchröderUnsplash

关联规则挖掘是一种基于规则的机器学习技术,用于发现数据集中频繁出现的模式。频繁模式可能包括通常一起购买的频繁项集或按顺序购买的子序列。例如,对于一家咖啡馆,饼干和咖啡可以是频繁项集,而对于一家电子产品商店,笔记本电脑和外接显示器可以是子序列。在交易数据库中寻找频繁模式和检测项之间的关联是一个极受欢迎的数据科学用例。一些应用领域包括商品推荐、交叉销售、促销设计、客户行为分析和库存管理。

在本文中,我们将涵盖市场篮子分析技术的关联规则挖掘,并回答包括但不限于以下问题:

  • 如果顾客购买了商品 A,她还有多大可能购买商品 B?是否存在正相关或负相关,还是完全随机发生?

  • 哪些商品应该放置在商店或应用的内容页面上的相邻位置?

  • 哪些商品可以在促销活动中捆绑在一起,或在对某一商品表现出兴趣后推荐?

在阅读本文时,你可以查看我在 GitHub 上的 Jupyter Notebook 以获取完整分析和代码。

什么是市场篮子分析?

购物篮是用户选择的商品集合,例如购物车中的商品或网站上消费的内容。市场篮分析帮助我们了解库存中商品之间的关联,因此,我们可以利用历史数据预测用户购买行为,并在不知道额外信息的情况下向用户推荐商品。

数据

在这篇文章中,我们将使用“面包篮”数据集,该数据集来自位于爱丁堡的一家面包店,包括超过 9000 笔交易。你可以从 Kaggle 下载。

来看看吧!☕️

basket = pd.read_csv("/content/bread_basket.csv")
basket.head()

数据集的前 5 行

如你所见,数据集在Transaction列中有交易 ID,每个购买的商品即使属于同一个交易 ID,也有单独的一行。让我们打印出交易 ID=3 的所有商品。

basket.loc[basket['Transaction']==3]

交易 ID=3

在分析过程中,我们将不会使用date_timeperiod_dayweekday_weekend列。此外,我们需要将商品显示为布尔值,每个交易 ID 应该在一行中表示。经过此特征工程后,我们将数据集转换为如下形式。

# Groupby by the Transaction Ids and count items
basket = basket.groupby(
              by=['Transaction', 
                  'Item'])['Item'].count().reset_index(name='Item_Count')

# Pivot table by the transaction and convert item count to boolean 
basket = basket.pivot_table(
              index='Transaction', 
              columns='Item', 
              values='Item_Count', 
              aggfunc='sum').fillna(0).astype(bool)

数据集经过特征工程后的前 5 行

关联规则是什么?

市场篮分析基于使用关联规则发现商品之间的关联,这些规则采取 if-then 关系的形式。要构建一个关联规则,我们应该至少有一个前提和一个结果

  • 一个前提和一个结果:如果{🍪}则

  • 多重前提:如果{🍪, 🍰}则

  • 多重结果:如果{🍪}则

总规则数随唯一项的数量增长。正如你所想,并非所有规则都同样重要,我们需要指标来帮助我们识别哪些规则应该被淘汰,哪些应该被考虑。

支持度

支持度是衡量规则有趣性和重要性的主要指标。它可以应用于单个项或前提和结果的对。它通过将包含特定项的交易数除以总交易数来计算。支持度值范围从 0 到 1。

例如,如果我们有 10 笔交易,其中 6 笔包括咖啡,4 笔同时包括咖啡和饼干,则{咖啡}的支持度为 60%,{咖啡, 饼干}的支持度为 40%。注意,如果{咖啡}则{饼干}和如果{饼干}则{咖啡}的支持度都是 40%。让我们计算数据集上的支持度。

# Support value for single items
support = basket.mean().sort_values(ascending=False)
support.head(10)

前 10 的支持度值

如果我们想找到一对前提和结果的支持度值,可以按照以下方式添加它们。

# Add antecedent and consequent pairs to the data set 
basket['Coffee & Bread'] = np.logical_and(basket['Coffee'], basket['Bread'])
basket['Coffee & Tea'] = np.logical_and(basket['Coffee'], basket['Tea'])
basket['Coffee & Cake'] = np.logical_and(basket['Coffee'], basket['Cake'])

# Calculate support
support = basket.mean().sort_values(ascending=False)
support.head(10)

添加新对后前 10 的支持度值

置信度

置信度是购买项目 B 的概率,前提是他们已经购买了项目 A。它通过将项目 A&B 的支持度除以项目 A 的支持度来计算。置信度值的范围是 0 到 1。

重要的是要将支持度与置信度一起使用,因为如果仅使用支持度指标,流行的项目可能会误导结果的解释。

# Confidence value of if Coffee then Bread 
print(support['Coffee & Bread']/support['Coffee'])

# Confidence value of if Coffee then Cake 
print(support['Coffee & Cake']/support['Coffee'])

0.18 对于咖啡 → 面包

0.11 对于咖啡 → 蛋糕

我们可以将这些结果解释为 18%的置信度意味着 18%的包含咖啡的交易也包含面包。

重要的是要提到,置信度指标不是对称的,Confidence(A→B) 与 Confidence(B→A) 是不同的。

我们用以下符号总结支持度和置信度,并且在修剪关联规则时,检查规则是否同时满足最小支持度和最小置信度水*是很重要的。

提升度

提升度指标用于检测无趣的关联规则以便于规则修剪。它假设在交易中,项目 A 的出现与项目 B 的出现是独立的,如果 P(AB) = P(A)P(B),否则这两个项目是依赖的,因此相关。它通过将包含项目 A 和 B 的交易比例除以项目 A 和 B 独立出现的比例来计算。提升度值的范围是 0 到无穷大。

Lift(A → B) > 1 意味着项目是正相关的,一个项目的出现会正面影响另一个项目的出现。

Lift(A → B) =1 意味着没有相关性

Lift(A → B) < 1 意味着项目是负相关的,一个项目的出现会负面影响另一个项目的出现。

# Lift value of if Coffee then Bread 
print(support['Coffee & Bread']/(support['Coffee']*support['Bread']))

# Lift value of if Coffee then Cake 
print(support['Coffee & Cake']/(support['Coffee']*support['Cake']))

0.57 对于咖啡 → 面包

1.10 对于咖啡 → 蛋糕

正如你所见,在交易中有咖啡会减少有面包的机会,同时增加有蛋糕的机会。

此外,请注意提升度指标是对称的,这意味着 Lift(A→B) 等于 Lift(B→A)。

杠杆度

杠杆度是衡量在交易中同时出现项目 A 和 B 与项目 A 和 B 独立出现之间差异的度量。它通过从包含项目 A 和 B 的交易比例中减去项目 A 和 B 独立出现的比例来计算。杠杆度值的范围是-1 到 1。

# Leverage value of if Coffee then Bread 
print(support['Coffee & Bread'] - (support['Coffee']*support['Bread']))

# Leverage value of if Coffee then Cake 
print(support['Coffee & Cake'] - (support['Coffee']*support['Cake']))

-0.06 对于咖啡 → 面包

0.005 对于咖啡 → 蛋糕

由于杠杆度值的范围是-1 到 1,我们可以用它来比较不同的配对。

确信度

确信度指标用于衡量结果项依赖于前项的程度。它通过将包含项目 A 的交易比例与不包含项目 B 的交易比例相乘,然后除以包含项目 A 而不包含项目 B 的交易比例来计算。确信度值的范围是 0 到无穷大。

如果置信度值很高,这意味着结果是高度依赖于前提的。

# Conviction value of if Coffee then Bread 
print(support['Coffee']*(
        1-support['Bread'])/(support['Coffee'] - support['Coffee & Bread']))

# Conviction value of if Coffee then Cake 
print(support['Coffee']*(
        1-support['Cake'])/(support['Coffee'] - support['Coffee & Cake']))

如你所见,我们计算了面包的支持度为1-support(Bread),以及咖啡和面包的支持度为support(Coffee) — support(Coffee & Bread)

0.82 对于咖啡 → 面包

1.01 对于咖啡 → 蛋糕

综合所有内容

之前,我们学习了支持度、置信度、提升度、杠杆效应和置信度度量,并手动计算了它们。现在让我们把它们结合起来,改用[mlxtend](http://rasbt.github.io/mlxtend/)库进行计算。

我们首先使用[apriori](https://github.com/rasbt/mlxtend/blob/master/mlxtend/frequent_patterns/apriori.py)函数从我们的事务数据中创建频繁项集。在该函数中,我们可以定义一个支持度度量的最小阈值,这对于剪枝非常有用。我决定将最小支持度定义为 0.01,但你可以尝试不同的值。

from mlxtend.frequent_patterns import association_rules, apriori

# Apply Apriori algorithm
frequent_itemsets = apriori(basket, min_support=0.01, use_colnames=True)

让我们看看frequent_itemsets的前 20 项。

frequent_itemsets.sort_values(by='support', ascending=False).head(20)

生成的 frequent_itemsets 的前 20 行

你可以看到 apriori 算法自动计算了唯一项和前提-结果对的支持度度量。

接下来,我们将通过使用[association_rules](https://github.com/rasbt/mlxtend/blob/master/mlxtend/frequent_patterns/association_rules.py)函数生成规则。在 association_rules 中,我们可以选择一个评估指标('support', 'confidence', 'lift', 'leverage', 'conviction', or 'zhangs_metric')并为其设置最小阈值。这里,我选择了‘confidence’作为评估指标,并将最小阈值设置为 0.5。可以根据感兴趣的规则自由更改指标。

# Compute association rules
rules = association_rules(frequent_itemsets, metric="confidence", 
  min_threshold=0.5)

让我们来看一下rules——输出的 pandas 数据框。

由 association_rules 生成的规则

太棒了!🚀 我们现在有了一组非常有趣的规则,这些规则的置信度大于 0.5,提升度大于 1(意味着正相关),具有正向杠杆效应,并且置信度高。还要注意,我们的起点是规则总数与项数成指数关系,而我们最终得到了 10 条最重要和有趣的规则。

现在我们将查看一种绘图技术来帮助我们可视化。你可能认为这不是必需的,因为我们只有 10 条规则,然而,如果你有很多规则或需要展示,好的视觉效果会大有帮助。

我认为PyCaret库中的plot_model函数非常易于使用和解释,以可视化关联规则。但请注意,在撰写本文时 pycaret 版本 3.0 已发布,但为了使用这个特定的函数,我不得不降级到 pycaret 版本 2.3。

!pip install pycaret==2.3

from pycaret.arules import plot_model

# Plot rules
plot_model(rules, plot = '2d')

可视化关联规则

如你所见,我们在左侧绘制了置信度,在右侧使用色彩图绘制了提升度,并在 x 轴上绘制了支持度。从图表中可以明显看出,Toast 具有最高的提升度和置信度,这使其成为 Coffee 的一个很好的前提。如果你将鼠标悬停在 Toast 上,它会显示所有指标计算值。

Coffee 的关联规则可视化

你可以通过查看图表来轻松地对规则进行排名,以便进一步评估。但一般来说,所有这些前提都可以得到 Coffee 作为推荐。

在筛选规则时,可以随意重新设置最小阈值或更改评估指标,以允许不同的结果和更大的项集。

结论

在这篇文章中,我们介绍了关联规则挖掘,并学习了如何使用市场篮子分析技术将其应用于数据集。我们学习了支持度、置信度、提升度、杠杆度和定罪度指标,并且通过手动和使用 mlxtend 库计算了这些指标。我们还看到了如何设置这些指标的最小阈值以筛选出不感兴趣的规则。最后,我们得到了 10 条有趣的规则,并使用 pycaret 库对它们进行了可视化。

希望你喜欢阅读关于关联规则的内容,并发现这篇文章有用!✨

喜欢这篇文章吗? 成为会员获取更多内容!

你可以在这里阅读我的其他文章 在 Medium 上关注我。如有任何问题或建议,请告诉我。✨

参考文献

  1. 来自 Kaggle 的 Bread Basket 数据集,许可证 CC0: 公共领域

  2. 头图由 Matthias SchröderUnsplash 上拍摄

  3. 所有其他图片均由作者提供

  4. Mlxtend 源代码

  5. PyCaret 源代码

高效训练管道构建指南

原文:towardsdatascience.com/a-guide-to-building-effective-training-pipelines-for-maximum-results-6fdaef594cee

完整的 7 步 MLOps 框架

第 2 课:训练管道。ML *台。超参数调整。

Paul IusztinTowards Data Science Paul Iusztin

·发表于 Towards Data Science ·19 分钟阅读·2023 年 5 月 9 日

--

图片来源于 Hassan PashaUnsplash

本教程代表了7 节课程中的第二部分,将逐步指导你如何设计、实现和部署一个 ML 系统,使用MLOps 的最佳实践。在课程期间,你将构建一个生产就绪的模型,以预测未来 24 小时内来自丹麦的多个消费者类型的能源消耗水*。

通过本课程的学习,你将掌握设计、编码和部署一个使用批量服务架构的 ML 系统的所有基本知识。

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

如今,证书随处可见。构建高级的端到端项目并展示出来是获得专业认可的最佳方式。

目录:

  • 课程简介

  • 课程内容

  • 数据来源

  • 第 2 课:训练管道。ML *台。超参数调整。

  • 第 2 课:代码

  • 结论

  • 参考文献

课程简介

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

  • 设计一个批量服务架构

  • 使用 Hopsworks 作为特征存储

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

  • 构建一个带有超参数调整的训练管道

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

  • 实现一个批量预测管道

  • 使用 Poetry 构建你自己的 Python 包

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

  • 使用 Airflow 协调一切

  • 使用预测结果编写一个基于 FastAPI 和 Streamlit 的网页应用

  • 使用 Docker 容器化你的代码

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

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

  • 将所有内容部署到 GCP

  • 使用 GitHub Actions 构建 CI/CD 管道

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

如果你想从这门课程中获得最大收益, 我建议你访问包含所有课程代码的 GitHub 仓库 。本课程旨在让你快速阅读和复制文章中的代码。

在课程结束时,你将知道如何实现下图所示的内容。如果有些地方对你来说不太明白,不用担心,我会详细解释一切。

课程期间你将构建的架构图 [作者提供的图像]。

第二课结束时,你将知道如何实现和集成训练管道ML *台

注意: 这是最长的一节课,因为我无法从 ML *台中逻辑性地分离出训练管道。享受吧!

课程内容:

  1. 批量服务。特征存储。特征工程管道。

  2. 训练管道。ML *台。超参数调整。

  3. 批量预测管道。使用 Poetry 打包 Python 模块。

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

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

  6. 使用 FastAPI 和 Streamlit 消耗和可视化你的模型预测。将一切 Docker 化。

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

  8. [附赠] ‘不完美’ ML 项目的幕后故事——课程和见解

如果您想充分掌握本课内容,我们建议您查看之前的课程,该课程讨论了设计批量服务架构、构建特征工程管道和将特征加载到特征存储中:

构建生产就绪特征工程管道的框架

第 1 课:批量服务。特征存储。特征工程管道。

[towardsdatascience.com

数据来源

我们使用了一个免费的开放 API,提供丹麦所有能源消费者类型的小时能耗值 [1]。

他们提供了一个直观的界面,您可以轻松查询和可视化数据。您可以在这里访问数据 [1]。

数据有 4 个主要属性:

  • 小时 UTC: 数据点观察到的 UTC 日期时间。

  • 价格区域: 丹麦分为两个价格区域:DK1 和 DK2——由大贝尔特分隔。DK1 在大贝尔特以西,DK2 在大贝尔特以东。

  • 消费者类型: 消费者类型是由丹麦能源公司拥有和维护的行业代码 DE35。

  • 总消耗: 以 kWh 计的总电力消耗

注意: 观察值有 15 天的滞后!但对于我们的演示用例来说,这不是问题,因为我们可以模拟实时中的相同步骤。

我们的 web 应用程序的截图,显示了我们如何预测区域 = 1 和消费者类型 = 212 的能耗 [作者图像]。

数据点具有小时分辨率。例如:“2023–04–15 21:00Z”,“2023–04–15 20:00Z”,“2023–04–15 19:00Z”等。

我们将数据建模为多个时间序列。每个唯一的价格区域消费者类型组合代表一个独特的时间序列。

因此,我们将构建一个模型,独立预测每个时间序列未来 24 小时的能耗。

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

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

第 2 课:训练管道。机器学习*台。超参数调整。

第 2 课的目标

本课将教您如何构建训练管道并使用机器学习*台,如下图所示 👇

最终架构图,带有第 2 课组件用蓝色突出显示 [作者图像]。

更具体地说,我们将展示如何使用 Hopsworks 特征存储中的数据来训练您的模型。

我们还将展示如何使用 LightGBM 和 Sktime 构建一个预测模型,该模型将预测丹麦多个消费者类型未来 24 小时的能耗水*。

我们将覆盖的另一个关键步骤是如何使用 W&B 作为 ML *台来跟踪你的实验,注册模型和配置为工件,并执行超参数调优以找到模型的最佳配置。

最终,根据超参数调优步骤中找到的最佳配置,我们将用整个数据集训练最终模型,并将其加载到 Hopsworks 模型注册表中,以便后续批量预测管道使用。

注意: 本课程不涉及时间序列预测或超参数调优。这是一门 ML 工程课程,我希望展示多个部分如何汇聚成一个完整的系统。因此,我将直接切入代码的 DS 部分,而不深入细节。

理论概念与工具

Sktime: Sktime 是一个 Python 包,提供大量时间序列功能。它遵循与 Sklearn 相同的接口,因此得名。使用 Sktime,我们可以快速封装 LightGBM 并执行未来 24 小时的预测、交叉验证等。Sktime 官方文档 [3]

LightGBM: LightGBM 是一个基于提升树的模型。它建立在梯度提升和 XGBoost 之上,提供性能和速度的提升。以 XGBoost 或 LightGBM 开始是一个常见的做法。LightGBM 官方文档 [4]

如果你想了解更多关于 LightGBM 的信息,可以查看我的文章,其中我 在 15 分钟内解释了从决策树到 LightGBM 的一切

ML *台: ML *台是一种工具,使你能够轻松跟踪实验、记录训练元数据、上传和版本化工件、数据血缘等。ML *台在任何训练管道中都是必不可少的。你可以直观地将 ML *台视为你的中央研究与实验中心。

Weights & Biases: W&B 是一个流行的无服务器 ML *台。我们选择它作为我们的 ML *台是因为 3 个主要原因:

  1. 他们的工具非常棒且非常直观。

  2. 它提供了慷慨的免费版供个人研究和项目使用。

  3. 它是无服务器的——无需担心部署和维护工具。

训练管道: 训练管道是一个逻辑结构(一个脚本、一个应用程序或更多),它接受经过策划和验证的数据作为输入(来自数据和特征工程管道的结果),并输出一个作为工件的工作模型。通常,模型会被上传到一个模型注册表中,后来可以被各种推理管道访问(我们系列中的批量预测管道是推理管道的具体实现示例)。

第 2 课:代码

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

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

第 2 课中的所有代码都位于 training-pipeline文件夹下。

training-pipeline 文件夹中的文件结构如下:

显示 training-pipeline 文件夹结构的截图 [作者提供的图片]。

所有代码都位于 training_pipeline目录下(注意使用"_"而非"-")

直接将凭证存储在你的 git 仓库中是一个巨大的安全隐患。这就是为什么你将通过 .env 文件注入敏感信息的原因。

.env.default 是你必须配置的所有变量的示例。它还可以用来存储不敏感的属性的默认值(例如,项目名称)。

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

准备凭证

首先,我们必须创建一个 .env 文件,添加所有凭证。我已经在 第 1 课中展示了如何设置 .env 文件。另外,我在 第 1 课中解释了 .env 文件中的变量如何从你的 ML_PIPELINE_ROOT_DIR 目录加载到 SETTINGS Python 字典中,以便在你的代码中使用。

因此,如果你想复制我所做的工作,我强烈建议你查看 第 1 课。

如果你只想进行轻度阅读,可以完全跳过“准备凭证”步骤。

在第 2 课中,我们将使用两个服务:

  1. Hopsworks

  2. Weights & Biases

Hopsworks (免费)

我们已经在 第 1 课中展示了如何为 Hopsworks 设置凭证。请访问 第 1 课的“准备凭证”部分,在那里我们详细介绍了如何为 Hopsworks 设置 API KEY。

Weights & Biases (免费)

为了保持课程简洁,我们假设你已经阅读并应用了第 1 课中准备 Hopsworks 凭据的步骤。

好消息是,90%的步骤与配置Hopsworks的步骤类似,除了如何从 W&B 获取 API 密钥。

首先,在 W&B 上创建一个账户。然后,创建一个团队(即实体)和一个项目(或者使用你已有的默认项目)。

然后,查看下面的图片,了解如何获取你自己的 W&B API 密钥 👇

前往你的 W&B 账户。在右上角,点击你的个人资料账户,然后选择“用户设置”。进入用户设置后,向下滚动直到你看到“危险区域”卡片。然后,在“API 密钥”下,点击“新密钥”按钮。复制你的 API 密钥,就完成了。你现在有了你的 API 密钥 [图片由作者提供]。

一旦你拥有了所有的 W&B 凭据,前往你的.env文件并按如下方式替换它们:

  • WANDB_ENTITY: 你的实体/团队名称(我们的:“teaching-mlops”

  • WANDB_PROJECT: 你的项目名称(我们的:“energy_consumption”

  • WANDB_API_KEY:你的 API 密钥

从特征存储中加载数据

一如既往,第一步是访问用于训练和测试模型的数据。我们已经在 Hopsworks 特征存储中拥有所有数据。因此,下载它变得轻而易举。

下面的代码片段包含了load_dataset_from_feature_store() IO 函数,它位于training_pipeline/data.py文件中。你将使用这个函数下载特定的feature_view_versiontraining_dataset_version的数据。

注意: 通过提供特定的数据版本,你将始终知道你使用了哪些数据来训练和评估模型。因此,你可以一致地重现你的结果。

使用下面的函数,我们执行以下步骤:

  1. 我们访问 Hopsworks 特征存储。

  2. 我们获取给定版本的特征视图的引用。

  3. 我们获取给定版本的训练数据的引用。

  4. 我们将所有与使用的数据集相关的元数据记录到 W&B。

  5. 现在我们已经下载了数据集,我们将其传递给prepare_data()函数。稍后我们将详细介绍它。目前,请注意我们将数据分为训练集和测试集。

  6. 我们将所有与数据集拆分相关的元数据记录到 W&B 中,以及每个拆分的一些基本统计信息,如拆分大小和特征。

重要观察: 使用 W&B,你可以记录所有描述你如何提取和准备数据的元数据。通过这样做,你可以轻松了解每次实验的数据来源。

通过使用 run.use_artifact("<artifact_name>"),你可以在不同的工件之间建立联系。在我们的示例中,通过调用 run.use_artifact(“energy_consumption_denmark_feature_view:latest”),我们将这个 W&B 运行与在不同 W&B 运行中创建的工件关联起来。

请查看下面的视频,以了解 W&B 运行和工件在 W&B 界面中的样子 👇

W&B 工件概述 [作者的视频]。

现在,让我们深入了解 prepare_data() 函数。

我想强调的是,在 prepare_data() 函数中,我们不会执行任何特征工程步骤。

如下所示,在这个函数中,你将重构数据以兼容 sktime 界面,选择目标,并拆分数据。

数据被建模为层次时间序列,转化为同一变量在不同上下文中的多个独立观察。在我们的示例中,我们观察了不同区域和能源消耗类型的能源消耗情况。

对于层次时间序列,Sktime 期望数据使用多重索引建模,其中日期时间索引是最后一个。要了解更多有关层次预测的信息,请查看 Sktime 官方教程 [7]。

我们还可以安全地使用 sktime's temporal_train_test_split() 函数拆分数据。测试集的长度为给定的 fh (=forecast horizon)

一个关键观察点是测试集拆分不是随机抽样的,而是基于最新的观察数据。例如,如果你的数据从 2023 年 5 月 1 日到 2023 年 5 月 7 日,频率为 1 小时,则长度为 24 小时的测试集将包含数据的最后一天,即 2023 年 5 月 7 日的所有值。

构建预测模型

基准模型

首先,你将创建一个朴素基准模型作为参考。该模型基于给定的季节性周期性预测最后一个值。

例如,如果 seasonal_periodicity = 24 hours,它将返回从“当前 - 24 小时”的值。

使用基准模型是一个健康的实践,它可以帮助你将你的高级 ML 模型与更简单的模型进行比较。如果你不能用你的高级模型超越基准模型,那么 ML 模型是没有用的。

高级 ML 模型

我们将使用 Sktime 和 LightGBM 来构建模型。

请查看 Sktime 文档 [3] 和 LightGBM 文档 [4]。

如果你对时间序列感兴趣,请查看这个 Sktime 预测教程 [6]。如果你只想了解系统的大致情况,你可以继续。

LightGBM 将作为你的回归模型,用于学习数据中的模式并预测未来的值。

使用来自SktimeWindowSummarizer类,你可以快速计算各种窗口的滞后和均值及标准差。

例如,对于滞后,我们提供了默认值list(range(1, 72 + 1)), 这意味着“计算过去 72 小时的滞后”。

另外,作为均值滞后的示例,我们有默认值[[1, 24], [1, 48], [1, 72]]. 例如,[1, 24] 表示滞后为 1,窗口大小为 24,这意味着它将计算过去 24 天的均值。因此,最终对于[[1, 24], [1, 48], [1, 72]], 你将得到过去 24 天、46 天和 72 天的均值。

相同的原则适用于标准差值。查看此文档以了解更多 [2]。

你使用Sktime中的make_reduction()函数来包装 LightGBM 模型。通过这样做,你可以轻松附加之前初始化的WindowSummarizer。此外,通过指定strategy = "recursive",你可以使用递归范式轻松预测多个未来值。例如,如果你想预测未来 3 小时,模型将首先预测 T + 1 的值。然后,它将使用 T + 1 预测的值作为输入来预测 T + 2 的值,以此类推……

最后,我们将构建ForecastingPipeline,在其中附加两个转换器:

  1. transformers.AttachAreaConsumerType(): 一个自定义转换器,它从索引中提取区域和消费者类型,并将其作为外生变量添加。我们将向你展示我们如何定义它。

  2. DateTimeFeatures(): 一个来自Sktime的转换器,用于计算不同的与日期时间相关的外生特征。在我们的案例中,我们仅使用了星期几和一天中的小时作为额外特征。

请注意,这些转换器类似于Sklearn中的那些,因为Sktime保持了相同的接口和设计。使用转换器是设计模块化模型中的关键步骤。要了解更多关于 Sklearn 转换器和管道的内容,请查看我的文章:如何快速设计高级 Sklearn 管道。

最后,我们用给定的配置初始化了管道和模型的超参数。

AttachAreaConsumerType转换器很容易理解。我们将其实现为一个示例,展示其可能性。

长话短说,它只是将索引中的值复制到它自己的列中。

重要观察——设计决策

如你所见,所有的特征工程步骤都内置在预测管道对象中。

你可能会问:“但为什么?这样做,我们不是将特征工程逻辑保留在训练流程中吗?”

嗯,是的……也不是……

我们确实在训练脚本中定义了预测管道,但关键思想是我们将整个预测管道保存到模型注册表中。

因此,当我们加载模型时,也会加载预测管道中包含的所有预处理和后处理步骤。

这意味着所有的特征工程都封装在预测管道中,我们可以安全地将其视为一个黑箱。

这是一种将转换 + 原始数据存储在特征存储中的方式,如 Lesson 1 中讨论的那样。

我们本来也可以在特征存储中独立存储转换函数,但组合一个单一的管道对象会更简洁。

超参数调整

如何使用 W&B sweeps

你将使用 W&B 进行超参数调整。他们提供了你需要的所有方法。从常规的网格搜索到贝叶斯搜索。

W&B 使用sweeps来进行超参数调整。sweep 是指在基于超参数搜索空间的多个实验中的单个实验的高级术语。

我们将使用 MAPE(*均绝对百分比误差)指标来比较实验,以找到最佳的超参数配置。我们选择 MAPE 而不是 MAE 或 RMSE,因为它的值在[0, 1]之间归一化,从而使分析更为容易。

查看下面的视频,了解 W&B 中的 sweeps 面板的样子 👇

现在我们明白了我们的目标,让我们查看training_pipeline/hyperparamter_tuning.py文件中的代码。

如下函数所示,我们从特征存储中加载特定feature_view_versiontraining_dataset_version的数据集。

仅使用训练数据,我们开始进行超参数优化。

注意: 你必须确保不要使用测试数据进行超参数优化搜索。否则,你可能会导致测试拆分过拟合,从而使模型无法泛化。测试拆分应仅用于最终决策。

最后,我们保存运行的元数据,其中包含搜索的sweep_id

现在,让我们来看一下run_hyperparameter_optimization()函数,它接收训练数据,创建一个新的 sweep 并启动一个 W&B 代理。

在单次 sweep 运行中,我们构建模型并使用交叉验证训练模型。

如你所见,配置由 W&B 提供,基于给定的超参数搜索空间(稍后我们会详细解释)。此外,我们将配置作为一个工件进行日志记录,以便以后访问。

在我们的例子中,我们使用了简单的网格搜索来进行超参数调整。

如下所示,我们创建了一个名为sweep_config的 Python 字典,其中包含了方法、需要最小化的指标和要搜索的参数。

查看 W&B 官方文档以了解更多关于扫频的信息 [5]。

注意: 通过一些调整,你可以在单个扫频中快速并行运行多个 W&B 代理。因此,显著加快了超参数调优的速度。如果你想了解更多,请查看他们的文档 [5]。

如何对时间序列数据进行交叉验证

所以,我强调了只使用训练数据集进行超参数调优是至关重要的。

那么,应该在什么拆分上计算你的指标呢?

好吧,你将使用适应于时间序列的交叉验证。

如下图所示,我们使用了 3 折交叉验证技术。关键点是,由于你使用的是时间序列数据,你不能为每个折叠选择整个数据集。这是合理的,因为你不能从未来学习以预测过去。

因此,使用与我们拆分数据集进行训练和测试时相同的原则,我们从数据集开始的 1/3 中抽样,其中预测范围(橙色部分)用于计算验证指标。下一个折叠使用 2/3,最后一个折叠使用 3/3 的数据集。

再次,Sktime让我们的生活变得更简单。使用ExpandingWindowSplitter类和cv_evaluate()函数,你可以快速训练和评估模型,使用指定的交叉验证策略——官方文档在这里 [8]。

最后,我们重构了results数据框,使其适应我们的接口,这个数据框是cv_evaluate()函数返回的。

很好,现在你已经完成了使用 W&B 扫频的超参数调优步骤。

在这一步结束时,我们有一个附加了多个实验的sweep_id,每个实验都有一个config artifact

现在我们必须解析这些信息并创建一个best_config artifact

从超参数调优搜索中上传最佳配置

使用training_pipeline/best_config.py脚本,我们将解析给定sweep_id的所有实验,并找到具有最低 MAPE 验证分数的最佳实验。

幸运的是,当我们调用best_run()函数时,W&B 会自动完成这项工作。之后,你恢复best_run并将运行重命名为best_experiment

另外,你将附加到最佳配置的配置上传到其称为best_config的 artifact 中。

之后,我们将使用这个 artifact 从头开始训练模型,无论多少次。

现在你有了best_config artifact,它准确地告诉你应使用什么超参数来训练你的最终模型。

使用最佳配置训练最终模型

最终,将最终模型训练并加载到模型注册表是最后一步。

training_pipeline/train.py 文件中的 from_best_config() 函数中,我们执行以下步骤:

  1. 从 Hopsworks 加载数据。

  2. 初始化 W&B 运行。

  3. 加载最佳配置工件。

  4. 构建基线模型。

  5. 在测试集上训练和评估基线模型。

  6. 使用最新的最佳配置构建高级模型。

  7. 在测试集上训练和评估高级模型。

  8. 渲染结果以查看它们的视觉表现。

  9. 在整个数据集上重新训练模型。这对时间序列模型至关重要,因为你必须将它们重新训练到当前时刻,以预测未来。

  10. 预测未来值。

  11. 渲染预测值。

  12. 将最佳模型保存为 W&B 中的工件。

  13. 将最佳模型保存在 Hopsworks 的模型注册表中。

注意: 你可以使用 W&B Artifacts 作为模型注册表,也可以直接使用 Hopsworks 模型注册功能。我们将展示这两种方法。

注意我们如何使用 wandb.log() 将所有感兴趣的变量上传到 W&B。

查看这个视频,直观地了解我们如何使用 W&B 作为实验跟踪器 👇

训练和评估模型。

为了训练任何Sktime模型,我们实现了这个通用函数,接受任何模型、数据和预测范围。

使用下述方法,我们通过聚合指标和在所有独特的区域和消费者类型组合上切片来评估模型。

通过在切片上评估模型,你可以快速调查公*性和偏差。

正如你所见,大部分繁重的工作,例如 MAPE 和 RMSPE 的实现,都可以直接从Sktime中访问。

渲染结果

使用Sktime,你可以快速将各种时间序列渲染到一个图表中。

如上视频所示,我们在 W&B 实验跟踪器中渲染了每个(区域,消费者类型)组合的结果。

直观比较区域 = 2 和消费者类型 = 119 的预测与实际观察 [作者提供的图片]。

直观观察未来的预测值,对于区域 = 2 和消费者类型 = 119 [作者提供的图片]。

将模型上传到模型注册表

最后一步是将模型上传到模型注册表。上传后,模型将被下载并用于我们的批量预测管道。

在实验过程中,我们已经将模型上传为 W&B 工件。如果你计划在应用程序中依赖 W&B,直接使用它是完全可以的。

但我们希望保持批量预测管道仅依赖 Hopsworks。

因此,我们使用了 Hopswork 的模型注册功能。

在以下代码中,基于给定的best_model_artifact,我们在 Hopsworks 特征视图中添加了一个标签,以将两者链接起来。这有助于调试。

最后,我们下载了最佳模型权重,并使用 mr.python.create_model() 方法将其加载到 Hopsworks 模型注册表中。

现在,通过几行代码,你可以下载并对你的模型进行推断,而无需再担心我们在本课中展示的所有复杂步骤。

查看第 3 课 以了解我们将如何使用来自 Hopsworks 模型注册表的模型构建批量预测流程。

结论

恭喜你!你完成了 第二课 来自 全栈 7 步 MLOps 框架 课程。

如果你已经读到这里,你应该知道如何:

  • 使用 ML *台进行实验和元数据跟踪

  • 使用 ML *台进行超参数调优

  • 根据给定版本从特征存储中读取数据

  • 构建一个封装的 ML 模型和流程

  • 将你的模型上传到模型注册表

现在你了解了使用 ML *台的强大功能,你可以最终掌控你的实验,并快速将你的模型导出为工件,以便在推断流程中轻松使用。

查看第 3 课 以了解如何实现批量预测流程和使用 Poetry 打包你的 Python 模块。

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

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

🔥 如果你喜欢阅读这样的文章并希望支持我的写作,可以考虑 成为 Medium 会员。通过使用 我的推荐链接,你可以在享受 Medium 丰富故事内容的同时,支持我而无需额外费用。

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

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

pauliusztin.medium.com](https://pauliusztin.medium.com/membership?source=post_page-----6fdaef594cee--------------------------------)

参考文献

[1] 来自丹麦 API 的每小时能源消耗数据, 丹麦能源数据服务

[2] WindowSummarizer 文档,Sktime 文档

[3] Sktime 文档

[4] LightGBM 文档

[5] W&B Sweeps 文档,W&B 文档

[6] Sktime 预测教程,Sktime 文档

[7] Sktime 分层、全球和面板预测教程,Sktime 文档

[8] Sktime 窗口分割器教程,Sktime 文档

构建高性能实时数据模型指南

原文:towardsdatascience.com/a-guide-to-building-performant-real-time-data-models-d60b37bb07dc

Marie TruongTowards Data Science Marie Truong

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

--

照片由Lukas Blazek拍摄,发布在Unsplash上。

数据已成为决策的重要工具。为了便于操作,数据需要经过清洗、转换和建模。

这个过程通常是 ELT 管道的一部分,按给定频率运行,例如每天。

另一方面,为了快速调整和做出决策,利益相关者有时需要访问最新数据,以便能够迅速做出反应。

例如,如果网站用户数量大幅下降,他们需要迅速了解此问题,并获得必要的信息以理解问题。

第一次被要求构建一个实时数据仪表盘时,我直接将其连接到实时的原始表,并提供了一些简单的 KPI,比如用户数量和崩溃次数。对于月度图表和更深入的分析,我创建了另一个仪表盘,连接到我们的数据模型,每天更新一次。

这种策略并不理想:我在数据仓库和 BI 工具之间重复逻辑,因此维护起来更困难。此外,实时仪表盘只能在几天的数据下表现良好,因此利益相关者不得不切换到历史仪表盘以查看早期日期。

我知道我们必须对此采取措施。我们需要实时数据模型,同时不影响性能。

在这篇文章中,我们将探讨构建实时模型的不同解决方案及其优缺点。

视图

SQL 视图是一个虚拟表,包含查询结果。与表不同,视图不存储数据。它们由一个每次有人查询视图时都会执行的查询定义。

这是一个视图定义的示例:

CREATE VIEW orders_aggregated AS (
  SELECT 
    order_date, 
    COUNT(DISTINCT order_id) AS orders,
    COUNT(DISTINCT customer_id) AS customers
  FROM orders
  GROUP BY order_date
 )

即使在表中添加了新行,视图也会保持最新。然而,如果表很大,视图可能会变得非常慢,因为没有数据被存储。

如果你在一个小项目中工作,它们应该是首选。

如果逻辑复杂,包含连接和窗口函数,你可能会遇到仪表板加载时间极长的问题。

优点

✅ 它们确实很容易设置

✅ 它们始终保持最新

缺点

❌ 对大数据量或复杂计算的性能较差

频繁刷新的表

如果你的数据需要非常*期但不完全是实时的,一个好的解决方案是非常频繁地刷新表。

下面是如何定义一个查询,以便每两小时刷新一次表:

DELETE FROM orders_aggregated 
WHERE order_date BETWEEN "2023-07-17 08:00:00" AND "2023-07-17 08:30:00";
INSERT INTO orders_aggregated (
  SELECT 
    order_date, 
    COUNT(DISTINCT order_id) AS orders,
    COUNT(DISTINCT customer_id) AS customers
  FROM orders
  WHERE order_date BETWEEN "2023-07-17 08:00:00" AND "2023-07-17 08:30:00"
  GROUP BY order_date
 )

在 Scopely,我们有一些每半小时刷新一次的模型。这些模型具有很好的性能,并提供当天的信息。这个话题的重要性不需要数据来自最*的 30 分钟。

我们在流水线中增加了一些复杂性:它运行得很频繁,有时会失败。当我们每日的流水线失败时,我们只是手动重新运行。但对于一个一天运行 48 次的模型来说,这将是噩梦。因此,我们添加了一段额外的代码,以确保如果一次运行失败,下一次运行会整合前一次运行的数据。

优点

✅ 它们能实现非常好的性能

缺点

❌ 数据不是完全实时的

❌ 流水线运行频繁,需要密切监视

物化视图

大多数现代云数据仓库都有一个称为物化视图的对象。物化视图是一个将查询结果存储在物理表中的对象。

在某些数据仓库中,物化视图需要通过触发器刷新,而在其他如 BigQuery 的数据仓库中,它们可以在新增行时自动刷新。这确保了数据的良好质量,因为增量逻辑由仓库本身处理。

使用物化视图的缺点是它们附带了许多限制。

如果我们尝试使用之前的相同查询来构建 BigQuery 中的物化视图:

CREATE MATERIALIZED VIEW orders_aggregated AS (
  SELECT 
    order_date, 
    COUNT(DISTINCT order_id) AS orders,
    COUNT(DISTINCT customer_id) AS customers
  FROM orders
  GROUP BY order_date
 )

我们遇到了一个错误:

Incremental materialized views do not support the DISTINCT clause for aggregation function 'count'

物化视图通常支持受限的语法。相反,我们可以使用函数 APPROX_COUNT_DISTINCT:

CREATE MATERIALIZED VIEW orders_aggregated AS (
  SELECT 
    order_date, 
    APPROX_COUNT_DISTINCT(order_id) AS orders,
    APPROX_COUNT_DISTINCT(customer_id) AS customers
  FROM orders
  GROUP BY order_date
 )

优点

✅ 它们结合了表的性能和视图的简单性

✅ 无需设计增量逻辑

缺点

❌ 允许的语法极其有限

Lambda 视图

这个想法是在我们意识到逻辑非常简单时产生的:我们使用视图来处理实时数据,使用表来处理历史数据。那么,为什么不能使用一个UNION ALL视图来组合这两者呢?

经过一点研究,我们发现这个概念已经有了名字:lambda 架构。

Lambda 架构是一个结合批处理(增量表部分)和流处理(视图部分)的系统。

我非常热情,尝试基于我的orders_aggregated表构建一个 lambda 视图。我只是简单地在日期上使用一个筛选器,并每天更新该视图,将筛选器的值更改为当前日期。

CREATE VIEW orders_aggregated_lv1 AS (
  -- Batch layer
  SELECT 
    *
  FROM orders_aggregated
  WHERE DATETIME(order_date) < "2023-07-17"

  UNION ALL 
  -- Stream layer
  SELECT 
    order_date, 
    COUNT (DISTINCT order_id) AS orders,
    COUNT(DISTINCT customer_id) AS customers
  FROM orders
  WHERE DATETIME(order_date) >= "2023-07-17"
  GROUP BY order_date

 )
SELECT *
FROM orders_aggregated_lv1
WHERE DATETIME(order_date) = "2023-07-15"

但执行时间非常令人失望。查看图表后,我明白了原因:BigQuery 的计划器仍在重新计算整个视图,尽管它只应该查看表。

作者提供的图像

与其从 WHERE 筛选器中知道它不需要查看视图,不如查看整个视图中order_date列的值。

所以我使用了一个小的变通方法,并为视图硬编码了日期值:

CREATE  VIEW orders_aggregated_lv2 AS (
  -- Batch layer
  SELECT 
    *
  FROM orders_aggregated
  WHERE DATETIME(order_date) < "2023-07-17"

  UNION ALL 
  -- Stream layer
  SELECT 
    DATE("2023-07-17") as order_date, 
    COUNT (DISTINCT order_id) AS orders,
    COUNT(DISTINCT customer_id) AS customers
  FROM orders
  WHERE DATETIME(order_date) = "2023-07-17"
  GROUP BY order_date
 )
SELECT *
FROM orders_aggregated_lv2
WHERE DATETIME(order_date) = "2023-07-15"

这一次,执行时间非常快,计划器只读取了表中的数据!

作者提供的图像

然而,我们担心如果查询没有在正午夜运行,或者我们的每日管道失败,我们可能会遗漏部分数据。

所以我们添加了一天的边际时间:

CREATE VIEW orders_aggregated_lv3 AS (
  -- Batch layer
  SELECT 
    *
  FROM orders_aggregated
  WHERE DATETIME(order_date) < "2023-07-17"

  UNION ALL 
  -- Stream layer
  SELECT 
    DATE("2023-07-17") as order_date, 
    COUNT (DISTINCT order_id) AS orders,
    COUNT(DISTINCT customer_id) AS customers
  FROM orders
  WHERE DATETIME(order_date) = "2023-07-17"
  GROUP BY order_date

    UNION ALL 
  -- Stream layer
  SELECT 
    DATE("2023-07-18") as order_date, 
    COUNT (DISTINCT order_id) AS orders,
    COUNT(DISTINCT customer_id) AS customers
  FROM orders
  WHERE DATETIME(order_date) = "2023-07-18"
  GROUP BY order_date

 )

如果我们的数据管道失败而我们没有立即修复,我们仍然拥有数据,只是性能略有下降。

由于我们使用的是数据构建工具,我们能够用 Jinja2 语法编写那一迭代逻辑,避免了重复。

优点

✅ Lambda 视图表现出色

✅ 可以定义几天的边际时间,以确保它们在管道失败时保持最新

缺点

❌ 使查询计划器高效的逻辑很复杂

❌ 一个 lambda 视图依赖于至少两个数据库对象

哪一个是最佳解决方案?

没有绝对最好的解决方案;这完全取决于你的数据和使用案例。不过,如果你在了解了四个选项后仍然难以决定,这里有一个小的决策树来帮助你做出决定:

作者提供的图像

资源

我希望你喜欢这篇文章!如果你喜欢,请关注我,获取更多关于 Python、SQL 和分析的内容,例如这个关于 ELT 管道的教程:

## 如何使用 Python 构建 ELT

提取、加载和转换数据

towardsdatascience.com

使用预测模型进行实时推断指南

原文:towardsdatascience.com/a-guide-to-live-inference-with-a-forecasting-model-aef5c437d4e

超越离线训练和预测测试

Vitor CerqueiraTowards Data Science Vitor Cerqueira

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

--

图片由 Fringer CatUnsplash 提供

许多在线资源关于使用机器学习进行预测。然而,这些资源主要关注离线训练和测试预测。

在这里,你将学习如何创建模型并使用它来预测实际的未来观察值。

介绍

预测资源常常忽视模型在实时预测中的应用。

关于将机器学习应用于预测的信息很多。但大多数信息集中在预测生命周期的特定阶段。例如,数据预处理或模型构建。

这些资源通常缺乏有关模型实际应用的信息。即,如何将其从离线设置扩展到实时设置。

在时间序列中,这一点尤其重要,因为观察值是相关的,并且它们的顺序很重要。

让我们看看如何构建一个模型并用它来进行实时预测。

案例研究——预测波浪高度

图片由 Silas BaischUnsplash 提供

我们将使用有关海洋波浪高度的时间序列数据。预测这类数据对于管理海洋操作非常重要。

数据集由放置在爱尔兰海岸的智能浮标捕获。有关详细信息,请参见参考[1]。

该时间序列会不断更新新观察值。因此,它是开发实时预测预测模型的完美示例。

我们将涵盖以下步骤:

  1. 获取历史数据;

  2. 数据预处理和特征工程;

  3. 选择和构建预测模型;

  4. 获取最新观测数据并进行预测。

获取历史数据

首先,我们来获取数据。

你可以直接从 ERDAP 的服务器读取数据,如下所示:

import pandas as pd

START_DATE = '2022-01-01'
URL = f'https://erddap.marine.ie/erddap/tabledap/IWaveBNetwork.csv?time%2CSignificantWaveHeight&time%3E={START_DATE}T00%3A00%3A00Z&station_id=%22AMETS%20Berth%20B%20Wave%20Buoy%22'

def reading_data(url: str) -> pd.Series:
    """
    Reading ERDAP data

    :param url: ERDAP url as string
    :return: hourly wave height time series as pd.Series
    """

    # reading data directly from erdap
    data = pd.read_csv(url, skiprows=[1], parse_dates=['time'])

    # setting time to index and getting the target series
    series = data.set_index('time')['SignificantWaveHeight']

    # transforming data to hourly and from centimeters to meters
    series_hourly = series.resample('H').mean() / 100

    return series_hourly

series = reading_data(URL)

这是时间序列图:

每小时的海洋波高时间序列。来源参考[1]。图片由作者提供。

预处理和特征工程

在建模之前,你可能需要进行一些预处理。

为了说明问题,我们将做两件事:

  • 对数据取对数。这有助于稳定方差;

  • 使用总结统计进行特征提取。

这里是这两个操作的代码:

import numpy as np

class LogTransformation:
    """
    Log transformation and inverse transformation

    Taking the log helps stabilize the variance
    """

    @staticmethod
    def transform(x):
        xt = np.sign(x) * np.log(np.abs(x) + 1)

        return xt

    @staticmethod
    def inverse_transform(xt):
        x = np.sign(xt) * (np.exp(np.abs(xt)) - 1)

        return x

上述类还包括一个inverse_transform方法,用于还原对数变换。这对于将对数预测还原到其原始尺度非常重要。

def feature_engineering(X: pd.DataFrame) -> pd.DataFrame:
    """
    param X: lagged observations (explanatory variables)

    :return: new features
    """

    summary_stats = {'mean': np.mean, 'sdev': np.std}

    features = {}
    for f in summary_stats:
        features[f] = X.apply(lambda x: summary_statsf, axis=1)

    features_df = pd.concat(features, axis=1)
    X_feats = pd.concat([X, features_df], axis=1)

    return X_feats

函数feature_engineering计算滚动*均值和滚动标准差。这是一些可以计算的特征的例子,用于改进预测模型。

我们的目标是展示不同的预处理步骤如何融入实时推断的流程中。在你的情况下,你需要确保这些或其他变换是必要的。例如,使用交叉验证或统计测试。

构建预测模型

下一步是构建模型并估计其性能。

我们首先将数据分为训练集和测试集。然后,我们使用滑动窗口对这些数据进行自回归变换。你可以查看上一篇文章了解更多关于时间序列的监督学习。

from sklearn.model_selection import train_test_split
# https://github.com/vcerqueira/blog
from src.tde import time_delay_embedding

# using last 24 observations as lags, 
# and next 24 observations as the forecasting horizon
N_LAGS, HORIZON = 24, 24

train, test = train_test_split(series, test_size=0.2, shuffle=False)

X_train, Y_train = time_delay_embedding(train, n_lags=N_LAGS, horizon=HORIZON, return_Xy=True)
X_test, Y_test = time_delay_embedding(test, n_lags=N_LAGS, horizon=HORIZON, return_Xy=True)

使用此设置,我们将构建一个模型,以预测基于过去 24 个滞后的下一 24 小时的数据。

构建预测模型有几种技术。这里,我们将重点关注随机森林。这包括使用交叉验证进行超参数调优。下面是如何进行:

from sklearn.model_selection import RandomizedSearchCV, TimeSeriesSplit

## apply preprocessing steps
# log transformation
X_train = LogTransformation.transform(X_train)
Y_train = LogTransformation.transform(Y_train)
# feature engineering
X_train_ext = feature_engineering(X_train)

# time series cv procedure
tscv = TimeSeriesSplit(n_splits=5, gap=50)

# defining the search space
# a simple optimization of the number of trees of a RF
model = RandomForestRegressor()
param_search = {'n_estimators': [10, 50, 100, 200],
                'criterion': ['squared_error', 'absolute_error'],
                'max_depth': [None, 2, 5, 10],
                'max_features': ['log2', 'sqrt']}

# applying CV with random search on the training data
gs = RandomizedSearchCV(estimator=model,
                        cv=tscv,
                        refit=True,
                        param_distributions=param_search,
                        n_iter=10, n_jobs=1)

gs.fit(X_train_ext, Y_train)

在优化参数后,你可以将模型应用于测试数据。这提供了可靠的性能估计。

# applying preprocessing steps to test data
X_test = LogTransformation.transform(X_test)
X_test_ext = feature_engineering(X_test)

# inference on test set and evaluation
preds = gs.predict(X_test_ext)

# log forecasts
preds_log = gs.predict(X_test_ext)

# reverting the log transformation
preds = LogTransformation.inverse_transform(preds_log)

# estimating performance using r-squared
estimated_performance = r2_score(Y_test, preds)

请注意,超参数优化和性能估计是在数据的两个不同部分进行的

选择的模型使用所有可用数据进行再训练。你还可以使用joblib将其存储在文件中:

from joblib import dump

# preparing all available data for auto-regression
X, Y = time_delay_embedding(series, n_lags=N_LAGS, horizon=HORIZON, return_Xy=True)

# applying preprocessing steps
X = LogTransformation.transform(X)
Y = LogTransformation.transform(Y)
X_ext = feature_engineering(X)

# model fitting
final_model = RandomForestRegressor(**gs.best_params_)
final_model.fit(X_ext, Y)

dump(final_model, 'random_forest_v1.joblib')

应用模型

在这个阶段,我们完成了几件事:

  • 读取并预处理时间序列;

  • 使用交叉验证建立和优化预测模型;

  • 使用测试集估计其性能。

现在,我们准备在实际环境中应用这个模型。

首先,我们从浮标处获取最新的观测数据。

import datetime

# setting the max history to yesterday
yesterday = datetime.date.today() - datetime.timedelta(days=1)
yesterday = yesterday.strftime('%Y-%m-%d')

LIVE_URL = f'https://erddap.marine.ie/erddap/tabledap/IWaveBNetwork.csv?time%2CSignificantWaveHeight&time%3E={yesterday}T00%3A00%3A00Z&station_id=%22AMETS%20Berth%20B%20Wave%20Buoy%22'

# reading the data from the ERDAP server
new_series = reading_data(LIVE_URL)

# getting the last 24 observations needed for the model
lags = new_series.tail(N_LAGS)

我们为什么需要这些观测数据?

回忆一下,我们的模型是基于自回归的。这意味着它使用最*的观测数据来预测未来的数据。在我们的例子中,我们使用过去 24 个观测数据来预测接下来 24 小时的数据。因此,模型的输入是基于过去 24 个最*的观测数据。

我们需要在应用模型之前重新结构化这些数据。这意味着应用我们为最终模型训练所做的相同变换。然后,我们加载并将模型应用于此样本。

from joblib import load

# structuring the lags as a DataFrame
lags_df = pd.DataFrame(lags).T.reset_index(drop=True)
lags_df.columns = X.columns

# applying preprocessing steps
lags_df = LogTransformation.transform(lags_df)
lags_feats = feature_engineering(lags_df)

# loading the model from disk
final_model = load('random_forest_v1.joblib')

# applying the model
log_forecasts = final_model.predict(lags_feats)

# reverting the log transformation
forecasts = LogTransformation.inverse_transform(log_forecasts)

下面是预测结果的样子:

随机森林的预测(深蓝色)用于接下来的 24 小时。图片由作者提供。

个别树木的预测也被包括在内,以传达预测的不确定性。

由于这些是实际的预测,我们需要等待以检查模型的表现。

关键要点

在本文中,我们构建了一个模型并用它在实际场景中进行预测。

我们探讨了:

  • 如何获取最新的观察数据并将其结构化以用于自回归模型;

  • 如何存储和加载模型;

  • 如何应用和还原变换,以便在原始数据尺度上获得预测结果。

感谢阅读,下一篇故事见!

相关文章

  • 预测的机器学习:变换和特征提取

  • 预测的机器学习:多变量时间序列的监督学习

参考资料

[1] 爱尔兰波浪浮标 来自海洋研究所(数据集 ID:IWaveBNetwork)。许可证:CC BY 4.0

Matplotlib 子图形创建复杂多面板图的指南

原文:towardsdatascience.com/a-guide-to-matplotlib-subfigures-for-creating-complex-multi-panel-figures-70fa8f6c38a4?source=collection_archive---------3-----------------------#2023-11-01

子图形——用于美丽的多面板图形的强大工具

Tim RoseTowards Data Science Tim Rose

·

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

--

动机

复杂的(科学)图形通常由多个具有不同大小或注释的子图组成。如果你使用 matplotlib/seaborn 生态系统,有很多方法可以创建复杂的图形,例如使用 gridspec

然而,这可能会很快变得具有挑战性,特别是如果你想将 seaborn 中的多坐标轴图,如 jointplotpairgrid 集成到你的图形中,因为它们没有提供坐标轴作为输入参数的选项。但在 matplotlib 中还有另一种组合图形的方法,而不仅仅是使用子图:Subfigures。这是一个强大的框架,可以创建像这样的多面板图形:

本文的目标是向你展示如何制作这个图形。

在本文中,我将介绍子图及其功能。我们将结合子图、子图和 gridspecs 来重新创建这个图形。

要跟随本文,你应该对 matplotlib 的 subplotsgridspec 有基本的了解(如果没有,你可以查看链接的教程)。

Matplotlib 子图

首先,我们导入 matplotlib、seaborn,并加载一些示例数据,这些数据将用于填充图表内容:

import matplotlib.pyplot as plt
import seaborn as sns
data = sns.load_dataset('mpg')

让我们从 matplotlib 中子图的概念开始。要创建子图,我们首先需要创建一个图形:

fig = plt.figure(figsize=(10, 7))

从这一点开始,我们可以类似于子图的方式定义子图。通过提供行数(2)和列数(1),可以创建一个子图网格。我们还给图形背景上色以突出显示:

(topfig, bottomfig) = fig.subfigures(2, 1)

topfig.set_facecolor('#cbe4c6ff')
topfig.suptitle('Top')

bottomfig.set_facecolor('#c6c8e4ff')
bottomfig.suptitle('Bottom')

仅有图形没有任何图表(坐标轴)将不会被显示,因此我们需要为每个子图定义子图。这里我们已经可以看到子图的一个伟大特性,对于每个子图,我们可以定义不同的子图布局:

top_axs = topfig.subplots(2, 4)
bottom_axs = bottomfig.subplots(3, 7)

plt.show()

我们现在有两个独立的图形,可以分别设置它们,但将它们放在一个最终的图形中。当然,我们也可以调整子图的大小比例:

figure = plt.figure(figsize=(10, 7))
figs = figure.subfigures(2, 2, height_ratios=(2,1), width_ratios=(2,1))

figs = figs.flatten()

for i, fig in enumerate(figs):
 fig.suptitle(f'Subfigure {i}')
 axs = fig.subplots(2, 2)

plt.show()

然而,子图有一个缺点。为了消除标签或图形外部元素的重叠,plt.tight_layout() 是一个将所有内容紧凑地放入图形中的好方法。然而,这对子图不支持。这里你可以看到如果尝试使用它会发生什么:

figure = plt.figure(figsize=(10, 7))
figs = figure.subfigures(2, 2, height_ratios=(2,1), width_ratios=(2,1))

figs = figs.flatten()

for i, fig in enumerate(figs):
 fig.suptitle(f'Subfigure {i}')
 axs = fig.subplots(2, 2)

plt.tight_layout()
plt.show()

这并不是我们想要的……为了在图表之间插入间距并去除任何重叠,我们需要使用 subplots_adjust 函数,它允许我们在子图与边框之间插入(或移除)更多空间:

fig = plt.figure(figsize=(10, 7))
(topfig, bottomfig) = fig.subfigures(2, 1)

topfig.set_facecolor('#cbe4c6ff')
topfig.suptitle('Top')
bottomfig.set_facecolor('#c6c8e4ff')
bottomfig.suptitle('Bottom')

top_axs = topfig.subplots(2, 4)
bottom_axs = bottomfig.subplots(3, 7)

# Adding more space between plots and reducing the space to the sides
topfig.subplots_adjust(left=.1, right=.9, wspace=.5, hspace=.5)

# We can also squeeze subplots to the bottom
bottomfig.subplots_adjust(wspace=.5, hspace=.8, top=.7, bottom=.3)

plt.show()

子图的另一个伟大方面是它们可以嵌套,这意味着我们可以将每个子图分成更多的子图:

fig = plt.figure(figsize=(10, 7))
(topfig, bottomfig) = fig.subfigures(2, 1)

topfig.set_facecolor('#cbe4c6ff')
topfig.suptitle('Top')
top_axs = topfig.subplots(2, 4)

(bottomleft, bottomright) = bottomfig.subfigures(1, 2, width_ratios=(1,2))

bottomleft.set_facecolor('#c6c8e4ff')
bottomleft.suptitle('Bottom left')
bottom_axs = bottomleft.subplots(2, 2)

bottomright.set_facecolor('#aac8e4ff')
bottomright.suptitle('Bottom right')
bottom_axs = bottomright.subplots(3, 3)

# Spacing between subplots
topfig.subplots_adjust(left=.1, right=.9, wspace=.4, hspace=.4)
bottomleft.subplots_adjust(left=.2, right=.9, wspace=.5, hspace=.4)
bottomright.subplots_adjust(left=.1, right=.9, wspace=.4, hspace=.4)

plt.show()

让我们将一个联合图插入到这个图形中。不幸的是,这并不简单,因为 seaborn 函数没有提供图形对象作为输入的选项。但如果我们查看函数的源代码,我们可以看到这个图由三个共享 x 和 y 轴的子图组成,这些子图通过网格规范定义。

这意味着我们可以轻松地在子图中绘制它:

fig = plt.figure(figsize=(10, 7))
(topfig, bottomfig) = fig.subfigures(2, 1)

topfig.set_facecolor('#cbe4c6ff')
topfig.suptitle('Top')
top_axs = topfig.subplots(2, 4)

# We are using the bottom left subfigure for the jointplot
(bottomleft, bottomright) = bottomfig.subfigures(1, 2, width_ratios=(1,2))

# This parameter defines the size ratio between the main plot and the margin plots
ratio=2

# Defining a gridspec where the subplots are places
gs = plt.GridSpec(ratio + 1, ratio + 1)
# The main scatterplot
ax_joint  = bottomleft.add_subplot(gs[1:, :-1])
# The margin plots are sharing an axis with the main plot
ax_marg_x = bottomleft.add_subplot(gs[0, :-1], sharex=ax_joint)
ax_marg_y = bottomleft.add_subplot(gs[1:, -1], sharey=ax_joint)

# Axis labels and ticklabels for the margin plots are set to not visible
# Since they are shared with the main plot,
# removing them for the margin will also remove them from the main plot
plt.setp(ax_marg_x.get_xticklabels(), visible=False)
plt.setp(ax_marg_y.get_yticklabels(), visible=False)
plt.setp(ax_marg_x.get_xticklabels(minor=True), visible=False)
plt.setp(ax_marg_y.get_yticklabels(minor=True), visible=False)

# Filling the plots with data:
sns.scatterplot(data=data, y='horsepower', x='mpg', ax=ax_joint)
sns.histplot(data=data, y='horsepower', ax=ax_marg_y)
sns.histplot(data=data, x='mpg', ax=ax_marg_x)

bottomright.set_facecolor('#aac8e4ff')
bottomright.suptitle('Bottom right')
bottom_axs = bottomright.subplots(3, 3)

# Spacing between subplots
topfig.subplots_adjust(left=.1, right=.9, wspace=.4, hspace=.4)
bottomright.subplots_adjust(left=.1, right=.9, wspace=.4, hspace=.4)

plt.show()

你可以调整比例参数,看看图形如何变化。

现在,我们已经具备了创建复杂图形所需的所有工具,包括使用子图(subfigure)、子图布局(subplots)和网格(grids)。对于这种图形,通常需要用字母标注每个图,以便在说明中解释它们或在文本中引用。通常,这些标注是在图形创建后使用其他软件如 Adobe Illustrator 或 Inkscape 完成的。但我们也可以直接在 Python 中完成这项工作,这样可以节省以后额外的努力。

为此,我们将定义一个函数来进行这些标注:

def letter_annotation(ax, xoffset, yoffset, letter):
 ax.text(xoffset, yoffset, letter, transform=ax.transAxes,
         size=12, weight='bold')

该函数将坐标轴作为输入,连同 x 和 y 坐标,这些坐标将转换为相对坐标轴坐标。我们可以用它来在我们之前创建的图形中标注一些图:

fig = plt.figure(figsize=(10, 7))
(topfig, bottomfig) = fig.subfigures(2, 1)

topfig.set_facecolor('#cbe4c6ff')
topfig.suptitle('Top')
top_axs = topfig.subplots(2, 4)
letter_annotation(top_axs[0][0], -.2, 1.1, 'A')

(bottomleft, bottomright) = bottomfig.subfigures(1, 2, width_ratios=(1,2))

bottomleft.set_facecolor('#c6c8e4ff')
bottomleft.suptitle('Bottom left')
bottoml_axs = bottomleft.subplots(2, 2)
letter_annotation(bottoml_axs[0][0], -.2, 1.1, 'B')

bottomright.set_facecolor('#aac8e4ff')
bottomright.suptitle('Bottom right')
bottomr_axs = bottomright.subplots(3, 3)
letter_annotation(bottomr_axs[0][0], -.2, 1.1, 'C')

# Spacing between subplots
topfig.subplots_adjust(left=.1, right=.9, wspace=.4, hspace=.4)
bottomleft.subplots_adjust(left=.2, right=.9, wspace=.5, hspace=.4)
bottomright.subplots_adjust(left=.1, right=.9, wspace=.4, hspace=.4)

plt.show()

我们现在可以创建文章开头显示的图形。它由三个子图组成。一个位于顶部,占据第一行,两个位于底部。左下角的子图将用于联合图(如前所示),而右下角的子图将定义一个网格规范(gridspec),以放置 4 个不同大小的子图。

fig = plt.figure(figsize=(10, 7))

# Creating a subfigure for the first and second row
(row1fig, row2fig) = fig.subfigures(2, 1, height_ratios=[1, 1])
# Splitting the bottom row subfigure in two subfigures
(fig_row2left, fig_row2right) = row2fig.subfigures(1, 2, wspace=.08, width_ratios = (1, 2))

# #####
# Row 1 plots
# #####

# Make 4 subplots for the first row subfigure
row1_axs = row1fig.subplots(1, 4)

row1fig.subplots_adjust(wspace=0.5, left=0, right=1, bottom=.16)

ax = row1_axs[0]
sns.histplot(data=data, x='mpg', ax=ax)
ax.set_title('MPG')
# Annotate plotots with letters
letter_annotation(ax, -.25, 1, 'A')
# Some styling for figures to make them look better 
# and have a standardized look
sns.despine(offset=5, trim=False, ax=ax)

ax = row1_axs[1]
sns.histplot(data=data, x='displacement', ax=ax)
ax.set_title('Displacement')
letter_annotation(ax, -.25, 1, 'B')
sns.despine(offset=5, trim=False, ax=ax)

ax = row1_axs[2]
sns.histplot(data=data, x='weight', ax=ax)
ax.set_title('Weight')
letter_annotation(ax, -.25, 1, 'C')
sns.despine(offset=5, trim=False, ax=ax)

ax = row1_axs[3]
sns.histplot(data=data, x='horsepower', ax=ax)
ax.set_title('Horsepower')
letter_annotation(ax, -.25, 1, 'D')
sns.despine(offset=5, trim=False, ax=ax)

# #####
# Row 2 plots
# #####

# ##
# Seaborn jointplot
# ##

# Using code from the Seaborn JointGrid class

# size ratio between the main plots and the margin plots
ratio=2
# Defining a gridspec for inside the subfigure
gs = plt.GridSpec(ratio + 1, ratio + 1)
ax_joint  = fig_row2left.add_subplot(gs[1:, :-1])
# Share axis between the margin and main plots
ax_marg_x = fig_row2left.add_subplot(gs[0, :-1], sharex=ax_joint)
ax_marg_y = fig_row2left.add_subplot(gs[1:, -1], sharey=ax_joint)

# Remove Axis labels and ticklabels for the margin plots
plt.setp(ax_marg_x.get_xticklabels(), visible=False)
plt.setp(ax_marg_y.get_yticklabels(), visible=False)
plt.setp(ax_marg_x.get_xticklabels(minor=True), visible=False)
plt.setp(ax_marg_y.get_yticklabels(minor=True), visible=False)

sns.scatterplot(data=data, y='horsepower', x='mpg', ax=ax_joint)
sns.histplot(data=data, y='horsepower', ax=ax_marg_y)
sns.histplot(data=data, x='mpg', ax=ax_marg_x)

sns.despine(offset=5, trim=False, ax=ax_joint)
sns.despine(offset=5, trim=False, ax=ax_marg_y)
sns.despine(offset=5, trim=False, ax=ax_marg_x)

# Leaving some space to the right to remove overlaps
fig_row2left.subplots_adjust(left=0, right=.8)
letter_annotation(ax_marg_x, -.25, 1, 'E')

# ##
# Row 2 right plots
# ##

gs = plt.GridSpec(2, 3)
ax_left   = fig_row2right.add_subplot(gs[:, 0])
ax_middle = fig_row2right.add_subplot(gs[:, 1])
ax_up     = fig_row2right.add_subplot(gs[0, 2])
ax_down   = fig_row2right.add_subplot(gs[1, 2])

fig_row2right.subplots_adjust(left=0, right=1, hspace=.5)

ax = ax_left
sns.scatterplot(data=data, x='model_year', y='weight', hue='origin', ax=ax)
sns.despine(offset=5, trim=False, ax=ax)
letter_annotation(ax, -.3, 1, 'F')

ax = ax_middle
sns.boxplot(data=data, x='origin', y='horsepower', ax=ax)
sns.despine(offset=5, trim=False, ax=ax)
letter_annotation(ax, -.3, 1, 'G')

ax = ax_up
sns.kdeplot(data=data, x='mpg', y='acceleration', ax=ax)
sns.despine(offset=5, trim=False, ax=ax)
letter_annotation(ax, -.3, 1, 'H')

ax = ax_down
sns.histplot(data=data, x='weight', y='horsepower', ax=ax)
sns.despine(offset=5, trim=False, ax=ax)
letter_annotation(ax, -.3, 1, 'I')

plt.show()

结论

子图是 matplotlib 中相对较新的概念。它们使得组装包含许多图的大型图形变得简单。本文中展示的所有内容也可以完全通过使用 gridpec 实现。然而,这需要一个大型网格,并且需要考虑每个子图的大小。子图更像是即插即用的,且用更少的工作就可以达到相同的效果。

对我来说,子图是创建科学图形的一个非常方便的工具,我希望它们对你也能有所帮助。

你还可以在 GitHub 上找到本文中的所有代码:github.com/tdrose/medium-articles-code

除非另有说明,所有图像均由作者创建。

机器学习实际数据收集指南

原文:towardsdatascience.com/a-guide-to-real-world-data-collection-for-machine-learning-a232c436ac19

5 种可操作的策略来优化你的数据收集过程

Leah Berg and Ray McLendonTowards Data Science Leah Berg and Ray McLendon

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

--

图片由Henrik Dønnestad提供,来源于Unsplash

无论你是刚刚进入数据科学领域的新手,还是大型组织中的首席数据科学家,你可能都曾使用过精心制作的数据集来解决玩具型的机器学习问题。也许你使用过 K-Means 聚类来预测Iris数据集中的花卉种类。或者你可能尝试过使用逻辑回归模型来预测哪些乘客在Titanic航程中幸存。

尽管这些数据集非常适合练习机器学习的基础知识,但它们并不反映你在工作中遇到的实际数据。实际上,你的数据可能存在质量问题,可能不适合当前的任务,或者可能尚不存在。这意味着数据科学家通常需要卷起袖子收集数据——这是一项当前数据科学课程中常常未涉及的挑战。

对于新的数据科学家来说,在深入实际问题之前收集大量数据可能会感到极其艰巨,因为这一阶段为整个机器学习项目奠定了基础。然而,通过正确的策略,这个过程可以变得更加可控。

在我作为数据科学家超过 10 年的职业生涯中,我遇到了各种各样的数据收集策略。在这篇文章中,我将分享五个优化数据收集过程的实用技巧,帮助你迈向创建成功机器学习产品的道路。

1. 将数据收集转化为用户的即时价值

一个强大的起点在于从一开始就提供有形的价值。让我们借用汽车行业的一个重要参与者,特斯拉作为例子。他们对完全自动驾驶汽车的追求是一个巨大的目标,经过了多年的开发,并且需要大量的数据收集。

那么,在收集所有这些数据的过程中,他们做了什么呢?

照片由 Milan Csizmadia 拍摄,来源于 Unsplash

为了使这些数据收集立即产生价值,2018 年他们发布了一个自动挡风玻璃刮水系统——虽然不完全是自驾车,但对用户来说是一个逐步的好处。

这是一个公司采用敏捷方法的绝佳例子,强调逐步产品开发:想想滑板,然后是自行车,然后是摩托车,最后是汽车。接受这种思维方式让你在同时收集数据以进行更大项目的同时提供即时价值。

在我最*参与的一个项目中,我们的目标是自动化客户支持邮件回复。然而,为了训练一个机器学习模型,我们需要制作一个标注的数据集。

为了应对这个挑战,我们挖掘了客户现有的电子邮件,并创建了模板回复。然后,他们使用这些模板回应新请求,这不仅节省了客户的时间,还帮助收集了训练机器学习模型所需的标签。

邮件模板示例。图片由作者提供。

向标注者快速展示数据标注的价值是至关重要的,因为这个任务既枯燥又耗时。此外,这是向高层管理展示投资回报(ROI)的机会——这是在机器学习项目中广为人知的挑战

2. 使数据标注隐形

特斯拉巧妙地使得自驾车的数据标注过程几乎隐形。当你转动方向盘或踩下任何踏板时,你正在不知不觉中标注数据集。在 2020 年,埃隆·马斯克表示

实际上,司机在驾驶和采取行动时,实际上是在进行标注——标注现实——随着他们的驾驶,[不断提升]他们的技能。

但这可以如何应用到其他地方呢?

回到我们的邮件自动化示例,我们本可以让客户对所有电子邮件进行分类,以便我们直接提取类别,但增加额外步骤并不可行。相反,我们使用邮件模板巧妙地标注了数据集,使其成为一个无缝且自然的过程。

当客户开始使用模板时,他们很快需要对其进行自定义,这给我们带来了挑战。我们之前是根据原始模板文本来标记数据,现在需要跟踪每个模板的多个版本。

解决这个问题的创意方案涉及采用类似 Genius 的方法的水印系统,用于识别他们在 Google 上被抓取的内容

通过在每个模板上隐形水印,我们可以唯一地标记数据集而不会干扰用户体验。理想情况下,系统会捕获使用的模板,但我们目前的设置缺乏这一功能,使得水印成为一种具有成本效益且富有创意的替代方案。

3. 向用户提供多个默认选项

作为数据科学家,无论你是在创建新产品还是将机器学习集成到现有产品中,利用现有生产系统进行数据收集是关键。这不仅揭示了有价值的用户洞察,还优化了你的方法。

一种有效的方法是利用用户行为心理学来揭示用户洞察。用户常常倾向于选择默认选项,因为它的便利性和熟悉感。在提供选择时,默认选项作为起点,不需要额外的决策努力。

比如操作系统上的默认网页浏览器。大多数苹果用户选择 Safari,Windows 用户选择 Edge,而安卓用户选择 Chrome。选择其他浏览器比坚持使用默认浏览器能更好地揭示用户的特征。

照片由Denny Müller拍摄,刊登在Unsplash上。

基于用户兴趣提供多个默认选项丰富了我们的理解,并使我们能够对相似用户进行分组。想想像 Pinterest、Netflix 或 Twitter 这样的应用程序,它们在注册时询问你的兴趣。

通过这些量身定制的默认设置收集的数据,成为了训练机器学习模型的宝贵资源。这些模型可以从用户的偏好和行为中学习,从而使*台提供更准确的推荐、预测和洞察。

几年前,我参与了一个旨在对文档进行分类的项目。当我深入分析数据时,迅速发现来自不同地区的用户有自己独特的文档分类方式。因此,我最终为每个地区建立了独立的模型,以便精细调整预测。这种方法使我能够将用户的偏好和行为纳入我的模型,并最终提供更准确的推荐。

4. 仔细选择要标记的数据

虽然尝试标记所有可用数据以训练机器学习模型似乎是合乎逻辑的,但这种方法往往效率低下,可能导致收益递减。标记数据的人在时间、注意力和专业知识方面都有局限性。

Christian Erfurt拍摄,来源于Unsplash

标记所有内容可能意味着在已经了解或信息量较少的数据点上花费精力。此外,标记大量数据的成本和时间可能超过了提高模型准确性的好处。

相反,考虑一种选择性的方法,例如主动学习,以允许标注者集中精力在最有价值的数据点上。主动学习就像有一个聪明的学生,他知道向老师提出哪些问题以便更好地学习。

这有助于机器学习模型更快地学习,并在总体数据较少的情况下表现良好。就像学生通过提出正确的问题来学习一样,主动学习帮助模型通过选择最具信息量的数据进行标记来学习。

在一个最*的项目中,我只有有限的时间(几个分析师的时间)来标记数据,以便预测文档的保留政策。为了充分利用他们的时间,我使用了一种主动学习技术,称为不确定性采样。这项技术定位了模型难以处理的数据点,使我们能够集中精力处理最具挑战性的例子,并优化标记过程。

5. 利用历史数据 — 向后看以向前迈进

当你从用户那里收集标签时,你可以将其与历史数据进行比较,以揭示关系。你甚至可能创建一个预测模型,从这些历史数据中推断标签。

模型从历史数据中识别的模式中学习,然后应用这些学习来为新的未标记实例分配标签。这不仅扩大了历史数据集的价值,还加快了收集标记数据集的过程。

另一种有效的技术是将已标记的数据与历史上未标记的数据进行聚类,以确定最需要标记的数据。识别在已标记数据集中没有代表的数据簇,确保你充分利用了有限的资源。

Jessica Lee拍摄,来源于Unsplash

回到之前的文档保留政策模型,我结合了聚类和不确定性采样,以选择一个更有针对性的样本供分析师标记。

首先,我使用聚类方法对类似的数据点进行了分组,然后使用模型预测了保留政策和置信度分数。从那里,我抽取了 5-10 个置信度分数最低的数据点,并将这个更集中的数据集发送给分析师进行标记。

结论

开始数据收集的旅程可能看起来令人畏惧,但这是一个可以通过我所涵盖的五种策略来征服的挑战。

技巧总结

  1. 将数据收集转化为对用户的即时价值

  2. 使数据标记过程不被察觉

  3. 向用户提供多个默认选项

  4. 精心选择要标记的数据

  5. 利用历史数据——回顾以便前行

通过将即时价值、无缝体验、个性化默认设置、选择性标记和历史见解结合在一起,你不仅是在收集数据——你是在为成功铺路。

确保模型在相关、多样化和具有代表性的数据上进行训练是获得可靠预测的关键,而优化数据收集过程则节省时间和成本,加速成功的机器学习部署。

参考资料

  1. en.wikipedia.org/wiki/Iris_flower_data_set

  2. www.kaggle.com/competitions/titanic

  3. electrek.co/2018/01/01/tesla-releases-automatic-wiper-update-beta/

  4. www.pwc.com/us/en/tech-effect/ai-analytics/artificial-intelligence-roi.html

  5. electrek.co/2020/04/30/tesla-fleet-training-orders-of-magnitude-better-elon-musk/

  6. www.pcmag.com/news/genius-we-caught-google-red-handed-stealing-lyrics-data

  7. archive.uie.com/brainsparks/2011/09/14/do-users-change-their-settings/

  8. link.springer.com/article/10.1007/s10462-022-10246-w

  9. towardsdatascience.com/uncertainty-sampling-cheatsheet-ec57bc067c0b

使用 BigQuery 的窗口函数指南

原文:towardsdatascience.com/a-guide-to-using-window-functions-4b2768f589d9

在 BigQuery 中轻松创建累计总数、移动*均和排名。

Tom EllyattTowards Data Science Tom Ellyatt

·发布于 Towards Data Science ·16 分钟阅读·2023 年 7 月 21 日

--

Benjamin Voros 拍摄于 Unsplash

如果你曾经搜索过或偶然发现过类似于‘你需要知道的 6 个 SQL 技能,以通过面试’或‘我希望早几年知道的 SQL 概念’这样的内容,窗口函数很可能在那个列表中获得了应有的提及。

窗口函数非常棒。

我写这篇文章的目标是帮助你理解这些窗口函数以及如何使用它们。一旦我们完成了教程,我准备了一些用例,你可以在你的项目中运行这些用例进行尝试,因为我在这些示例中使用了公开数据。

我们将涵盖:

  • 什么是 窗口函数?

  • 窗口函数的语法 — 即分区、排序和框架部分

  • 详细了解如何创建 7 天的移动*均以及它是如何工作的

  • 你可以使用的聚合窗口函数是什么?

  • 最后,我们将演示一些用例,以展示窗口函数如何应用。

什么是 窗口函数?

‘窗口’这个词在 SQL 中使用可能看起来很奇怪(或在计算中一般)。通常,函数类型的名称会让你对其使用方式有个初步了解,例如:

  • 聚合函数 — 处理一堆数据并给出总结结果

  • 字符串函数 — 一个包含处理单词和句子的工具箱

  • 数组函数 — 一次性处理一组项目

等等…

Cosmic Timetraveler 拍摄于 Unsplash

那么 SQL 中的窗口函数是什么?就像现实世界中的窗户一样,它允许你查看特定区域,而其余部分则不在视线范围内。你只关注窗户中显示的内容。

回到数据的世界,假设你有一个包含 IOWA 酒类店每月销售的表格。

这个示例中使用的数据集是公开访问的,由 Google 提供,并且在 BigQuery 中已存在,如果你想自己尝试这些示例。

(link (CC0 license))

bigquery-public-data.iowa_liquour_sales.sales

以下示例提供了按年份和月份销售的简单视图。

我将上述内容保存为视图,以便将来我们的查询尽可能简洁,专注于应用窗口函数。

如果你想使用这个视图,可以使用spreadsheep-20220603.Dashboard_Datasets.iowa_liqour_monthly_sales

那么我们还想要每年的月*均值作为单独的列怎么办?

有几种方法可以实现这一点,如果你对窗口函数不熟悉,可以尝试先计算*均值作为子查询,然后再与原始表连接,如下所示。

这完全可以正常工作,但窗口函数将允许你在没有子查询的情况下得到相同的答案!

上述窗口函数允许我们对partition by year定义的特定行组执行聚合函数,这里是avg函数。

回顾之前的窗口类比,partition by部分就是我们在这种情况下的窗口。确实,我们面前有整个数据集,但分区限制了我们的视图仅限于年份。

是时候深入研究语法了。

窗口语法

在上面的示例中,我们可以将函数拆分为两个部分:函数名和窗口。

在这种情况下,函数名是熟悉的聚合函数AVG。然而,窗口部分则有所不同。

一旦你指定了你的函数,你需要使用over关键字开始你的窗口函数,后面必须跟上圆括号()。

在圆括号内,你可以使用partition by关键字指定我们想要执行聚合的窗口,后跟你希望包括在窗口中的列列表。在这里,我们只包含了一列year,但稍后我们将引入另一列。

Partition by 是可选的;如果你不包括partition by,聚合将包含数据集中的所有行。由于这存在于 SELECT 语句中,值得注意的是 WHERE 子句会在此窗口函数之前执行。

我这是什么意思?使用我之前分享的示例,我通过按年分区来指定窗口。然而,在我的WHERE 子句中,我设置了一个仅返回年份 = 2022的过滤器。

这意味着数据集中只有一个年份——2022,当窗口函数运行时。因此,我的按年分区窗口是多余的,在这种情况下使用下面的行会得到相同的结果。

让我们重新运行之前的查询,这次去掉我们的 WHERE 子句。

图片由Nik提供,来自Unsplash

在这里我们可以看到 2023 年和 2022 年的不同值。这现在显示了每一年提供的*均月销售额。

例如,在第 7 行,我们有2022 年,其*均月销售额为 3570 万,而2023 年(至今)的*均月销售额为 3580 万

将报告修改为仅关注 2022 年,我们可以清晰地可视化该年度的月度*均收入与实际情况的对比。

访问月度*均数据使得可视化和分析销售趋势变得更容易。具体来说,明显可以看出,年度的下半年对销售有显著贡献。

我们使用了一个窗口函数来确定每年的*均月销售额。然后,这个函数将结果应用于所有具有该年份的行。这就像我们之前看到的左连接子查询。

图片由Daniel K Cheung提供,来自Unsplash

Order By

迄今为止,我们主要关注了聚合函数和如何指定窗口。我们还可以确定窗口应该如何执行任务,这是一部分排名或运行总计/*均值解决方案的关键。

返回到 Iowa 数据集,让我们扩展视图以包括 store_name,然后根据总销售额给商店一个编号的月度排名。

新视图

spreadsheep-20220603.Dashboard_Datasets.iowa_liqour_monthly_sales_inc_store

与聚合函数不同,对于专门用于窗口函数的排名函数,你不会在函数内部指定列。

然而,如果你尝试按照上述方式运行,你会遇到一个错误。

这里的问题是我们告诉 Bigquery 我们想对结果进行排序,但我们没有指定排序方式,这可以通过ORDER BY来实现。

这为我们提供了一个按商店级别的月度销售视图,并且每个商店都有一个排名。你可以进一步分析,回答其他问题,比如2022 年每个月的前三大商店是哪三家

在本文接*尾声的一个例子中,我们将使用一个新的子句,QUALIFY,它可以让你轻松地过滤窗口函数给出的结果。

到目前为止,我们的窗口函数已应用于每个分区中的所有行,但如果我们只想要分区的一个子集呢?例如,过去七天的每日销售*均值?为此,我们需要指定一个窗口帧。

窗口帧

现在引入一个新的数据集,介绍芝加哥出租车!这是另一个你可以用来实验的公共数据集(CC0 许可证)。(link)

bigquery-public-data.chicago_taxi_trips.taxi_trips

这个公共数据集很大,达到 75GB,很快就会消耗掉你每月 100GB 的免费查询配额。因此,我创建了一个只包含 2023 年数据的新表,这样我们可以在不产生高额费用的情况下玩转数据。

我已经将这个表公开,所以我建议你尝试使用我的数据集进行测试。

spreadsheep-case-studies-2023.chicago_taxies_2023.trip_data

不管怎样,回到主题……什么是窗口帧?这个子句允许我们定义在分区内需要使用哪些行或范围。一个常见的用例是创建移动*均或累计总和。

SELECT
  date(trip_start_timestamp) as trip_start_date,
  round(sum(trip_total),2) as trip_total_revenue
FROM
  `spreadsheep-case-studies-2023.chicago_taxies_2023.trip_data`
WHERE
  date(trip_start_timestamp) between "2023-05-01" and "2023-06-30"
GROUP BY
  trip_start_date
ORDER BY
  trip_start_date DESC

这个查询提供了 2023 年 5 月至 6 月的每日收入数据。

移动*均在时间序列数据中非常常见,因为它允许你轻松地将特定日期或月份的表现与给定周期内通常看到的结果进行比较。

首先,我们来创建一个简单的移动*均,并且为了避免重复的日期转换和收入舍入,我将我们的初始查询放在了一个 CTE 中。

WITH daily_data as(
SELECT
  date(trip_start_timestamp) as trip_start_date,
  round(sum(trip_total),2) as trip_total_revenue
FROM
  `spreadsheep-case-studies-2023.chicago_taxies_2023.trip_data`
WHERE
  date(trip_start_timestamp) between "2023-05-01" and "2023-06-30"
GROUP BY
  trip_start_date
ORDER BY
  trip_start_date DESC
)

SELECT
  trip_start_date,
  trip_total_revenue,
  avg(trip_total_revenue) over (order by trip_start_date asc) as moving_average
FROM
  daily_data

如果我们查看前五行,可以看到第一个*均值等于 trip_total_revenue。这是因为它是窗口的开始,因为我们按 trip_start_date 升序排列了数据。因此,目前还没有任何内容可供*均计算。

然而,我们现在在第二行的第 1 行和第 2 行之间有一个每日*均值,而在第三行的第 1 行、第 2 行和第 3 行之间有一个每日*均值。

这是一个不错的开始,它显示了我们的移动*均正在工作,但我们可以更进一步。我们来创建一个仅包含最后七天收入的移动*均,如果窗口不包含七天,则显示空值,因为这是一个不完整的窗口。

要指定你的窗口范围,你需要记住三个关键词:

  • 当前行

  • preceding

  • following

然后,你从行或范围开始构建你的窗口(稍后我会解释这两者之间的区别),接着在<>和<>之间。

rows between 7 preceding and one preceding

上面的示例是我们问题所需的窗口框架。我们已经指定窗口从当前行之前的七行开始,到当前行之前的一行结束。

这是一个简单的示例,展示了这个窗口函数如何与求和聚合(累积总和)一起工作。

select
  numbers,
  sum(numbers) over 
  (
    order by numbers asc 
    rows between 7 preceding and one preceding
  ) 
  as moving_sum_seven
from
  test_data

正如你所见,当我们到达第 8 行时,移动总和的值达到 7,此时窗口现在包含七行数据。如果你将窗口调整为 6 行之前和当前行,你会看到窗口已经移动,以包括当前行。

在本节的最后,我将提供一些用例示例来突出它们的使用方式,但现在先回到当前任务!

让我们把这个窗口范围应用到我们的移动*均中。

with daily_data as (
SELECT
  date(trip_start_timestamp) as trip_start_date,
  round(sum(trip_total),2) as trip_total_revenue
FROM
  `spreadsheep-case-studies-2023.chicago_taxies_2023.trip_data`
WHERE
  date(trip_start_timestamp) between "2023-05-01" and "2023-06-30"
GROUP BY
  trip_start_date
ORDER BY
  trip_start_date DESC
)

SELECT
  trip_start_date,
  trip_total_revenue,
  avg(trip_total_revenue) over (order by trip_start_date asc rows between 7 preceding and one preceding) as moving_average
FROM
  daily_data
ORDER BY
  trip_start_date DESC

现在我们面临最后一个挑战:如何在窗口中数据行少于七行时将值设为 null?好吧,我们可以使用 IF 语句来进行检查。

COUNT(*) OVER 
(
ORDER BY trip_start_date ASC ROWS BETWEEN 7 PRECEDING AND 1 PRECEDING
) = 7
 if
  (
    COUNT(*) OVER (ORDER BY trip_start_date ASC ROWS BETWEEN 7 PRECEDING AND 1 PRECEDING) = 7,
    AVG(trip_total_revenue) OVER (ORDER BY trip_start_date ASC ROWS BETWEEN 7 PRECEDING AND 1 PRECEDING),
    NULL 
  ) AS moving_average

我们引入了第二个窗口函数,该函数计算窗口框架中存在的行数,如果等于 7,将提供移动*均结果。

值得知道:

  • 如果 ORDER BY 表达式未在窗口函数中提及,默认规范是 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING.

  • 如果指定了 ORDER BY 表达式并且使用了聚合函数,则默认窗口框架是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW

有关窗口框架规范的 BigQuery 文档在这里。

ROWS 和 RANGE 的区别

在 SQL 中,ROWS 和 RANGE 子句都帮助控制窗口函数在一个组内使用哪些行。

ROWS 子句处理固定数量的行。它计算当前行之前或之后的特定数量的行,无论这些行的值如何。这些行被包含在窗口函数中。

RANGE 子句基于行的值来工作。它考虑与当前行相对的特定值范围内的行。实际值决定哪些行被包含在窗口函数计算中。

因此,虽然 ROWS 子句关注于行的物理位置,但 RANGE 子句考虑行的逻辑值来确定它们是否包含在窗口函数中。

试试这个示例,看看它的实际效果。

with sales_data as (
SELECT
'2023-01-01' AS DATE, 100 AS SALES
UNION ALL
SELECT
'2023-01-02' AS DATE, 50 AS SALES
UNION ALL
SELECT
'2023-01-03' AS DATE, 250 AS SALES
UNION ALL
SELECT
'2023-01-03' AS DATE, 200 AS SALES
UNION ALL
SELECT
'2023-01-04' AS DATE, 300 AS SALES
UNION ALL
SELECT
'2023-01-05' AS DATE, 150 AS SALES
)

SELECT
  *,
  SUM(SALES) OVER (ORDER BY DATE ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total_rows,
  SUM(SALES) OVER (ORDER BY DATE RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total_range
FROM sales_data

查看第 3 行和第 4 行以比较这两个子句。ROWS 子句将每一行都加入到总数中,即使存在重复的销售日期。而 RANGE 子句则将具有相同销售日期的行作为一个范围进行分组。例如,在这种情况下,所有日期为 2023-01-03 的行将被视为一个范围。

照片由 Christopher GowerUnsplash 上提供

窗口函数是什么?

有很多可以与窗口函数一起使用的函数。

对于 聚合函数, 你可以尝试:

  • SUM: 计算数值列的总和。

  • AVG: 计算数值列的*均值。

  • MIN: 检索列中的最小值。

  • MAX: 检索列中的最大值。

  • COUNT: 计算列中行的数量。

  • COUNT DISTINCT: 计算列中不同值的数量。

然后你会有一系列仅适用于窗口函数的新函数,这些被称为分析函数:

  • ROW_NUMBER: 在窗口帧内为每一行分配一个唯一的编号。

  • RANK: 根据窗口帧中指定的顺序为每一行分配一个排名。

  • DENSE_RANK: 根据窗口帧中指定的顺序为每一行分配一个排名,排名中没有间隙。

  • LAG: 从窗口帧中的前一行中检索值。

  • LEAD: 从窗口帧中的后一行中检索值。

  • FIRST_VALUE: 从窗口帧中的第一行中检索值。

  • LAST_VALUE: 从窗口帧中的最后一行中检索值。

上述函数均链接到 BigQuery 文档。

照片由 Susan Holt SimpsonUnsplash 上提供

实际示例

每日累计总数

窗口函数的一个简单用例是累计总数。对于芝加哥出租车数据集,我们可以按月记录收入,但需要一个新列来跟踪迄今为止的总收入。

with daily_data as (
SELECT
  date(timestamp_trunc(trip_start_timestamp,month)) as trip_month,
  round(sum(trip_total),2) as trip_total_revenue
FROM
  `spreadsheep-case-studies-2023.chicago_taxies_2023.trip_data`
WHERE
  date(trip_start_timestamp) between "2023-01-01" and "2023-06-30"
GROUP BY
  trip_month
ORDER BY
  trip_month DESC
)

SELECT
  trip_month,
  trip_total_revenue,
  round(sum(trip_total_revenue) over (order by trip_month asc),2) AS running_total_revenue,
FROM
  daily_data
ORDER BY
  trip_month DESC

12 周移动*均

本文教程强调了在处理时间序列数据时移动*均是常见的。

with daily_data as (
SELECT
  date(timestamp_trunc(trip_start_timestamp,week(monday))) as trip_week,
  round(sum(trip_total),2) as trip_total_revenue
FROM
  `spreadsheep-case-studies-2023.chicago_taxies_2023.trip_data`
WHERE
  date(trip_start_timestamp) between "2023-01-01" and "2023-06-30"
GROUP BY
  trip_week
ORDER BY
  trip_week DESC
)

SELECT
  trip_week,
  trip_total_revenue,
  if
  (
    COUNT(*) OVER (ORDER BY trip_week ASC ROWS BETWEEN 12 PRECEDING AND 1 PRECEDING) = 12,
    AVG(trip_total_revenue) OVER (ORDER BY trip_week ASC ROWS BETWEEN 12 PRECEDING AND 1 PRECEDING),
    NULL 
  ) AS moving_average
FROM
  daily_data
ORDER BY
  trip_week DESC

将收入与移动*均线一起绘制表明了一个积极的趋势,因为自四月以来,移动*均线每周都在持续上升。如果没有移动*均线,我们的视线可能会被表现较差的周所吸引,而不是看到整体趋势。

Randy Fath 提供的照片,来自 Unsplash

计算异常检测的 Z 分数

Z-Score 计算 = (x — *均值) / 标准差

Z 分数是一种衡量一个数字相对于其他数字组的异常程度或典型程度的方法。它告诉您一个特定的数字距离该组的*均值有多少个标准差。

 if
  (
    COUNT(*) OVER (ORDER BY trip_start_date ASC ROWS BETWEEN 30 PRECEDING AND 1 PRECEDING) = 30,
    round
    (
      (
        trip_total_revenue - 
        AVG(trip_total_revenue) OVER (ORDER BY trip_start_date ASC ROWS BETWEEN 30 PRECEDING AND 1 PRECEDING)
      ) / stddev(trip_total_revenue) OVER (ORDER BY trip_start_date ASC ROWS BETWEEN 30 PRECEDING AND 1 PRECEDING)
    ,1),
    NULL 
  ) AS z_score_30_day

在这个例子中,我们取了 trip_total_revenue 的实际值,并减去了我们在过去 30 天里看到的*均每日收入。

然后我们将该数字除以这 30 天的标准差。这告诉我们特定一天的收入离*均值有多*,或该值距离*均值有多少个标准差。

这是一个很方便的指标,可以绘制在图表上,如下所示,因为它为您的数据提供了背景。虽然我们仅查看了过去 30 天的数据,但 Z 分数与之前的 30 天进行比较是毫不费力的,我们可以看到峰值和波动在 Z 分数突显出该天与常态的不同之前似乎微不足道。

对于这类报告,您应该设置一个值来表明存在异常事件。我不会说上面的图表中的任何日期都是异常的,但一个典型的值是 3(即三个标准差)。然而,这完全取决于数据的波动性。

完整查询

with daily_data as (
SELECT
  (trip_start_timestamp) as trip_start_date,
  round(sum(trip_total),2) as trip_total_revenue
FROM
  `spreadsheep-case-studies-2023.chicago_taxies_2023.trip_data`
WHERE
  date(trip_start_timestamp) between "2023-01-01" and "2023-06-30"
GROUP BY
  trip_start_date
ORDER BY
  trip_start_date DESC
)

SELECT
  trip_start_date,
  trip_total_revenue,
  if
  (
    COUNT(*) OVER (ORDER BY trip_start_date ASC ROWS BETWEEN 30 PRECEDING AND 1 PRECEDING) = 30,
    round
    (
      (
        trip_total_revenue - 
        AVG(trip_total_revenue) OVER (ORDER BY trip_start_date ASC ROWS BETWEEN 30 PRECEDING AND 1 PRECEDING)
      ) / stddev(trip_total_revenue) OVER (ORDER BY trip_start_date ASC ROWS BETWEEN 30 PRECEDING AND 1 PRECEDING)
    ,1),
    NULL 
  ) AS z_score_30_day
FROM
  daily_data
ORDER BY
  trip_start_date DESC

Giorgio Trovato 提供的照片,来自 Unsplash

每月排名前列的表现者

在芝加哥出租车数据集中有很多出租车公司,我们可能会问每个月表现最好的前三家公司是谁。

为了实现这一点,我们可以使用按 trip_month 分区并按 trip_total_revenue 降序排列的排名分析函数。

 rank() over (partition by trip_month order by trip_total_revenue desc) AS ranking

然而,这仍然会为数据集中每个月的所有公司提供结果,而不仅仅是前三名。因此,我们可以利用 QUALIFY 子句,它的功能类似于 WHERE 子句,以便您可以过滤数据。

qualify 子句只能与窗口函数一起使用,并且可以引用您在选择语句中创建的窗口函数。更多细节请见 这里

下面的结果清楚地表明三家公司主导了出租车市场。

with daily_data as (
SELECT
  date(timestamp_trunc(trip_start_timestamp,month)) as trip_month,
  company,
  round(sum(trip_total),2) as trip_total_revenue
FROM
  `spreadsheep-case-studies-2023.chicago_taxies_2023.trip_data`
WHERE
  date(trip_start_timestamp) between "2023-01-01" and "2023-06-30"
GROUP BY
  trip_month,
  company
ORDER BY
  trip_month DESC
)

SELECT
  trip_month,
  company,
  trip_total_revenue,
  rank() over (partition by trip_month order by trip_total_revenue desc) AS ranking
FROM
  daily_data
QUALIFY
  ranking <= 3
ORDER BY
  trip_month DESC

图片由 Towfiqu barbhuiya 提供,来自 Unsplash

月度/季度比较

月度和季度报告对于跟踪 KPI 以及帮助评估业务发展方向至关重要。然而,创建一个在 BigQuery 中提供月度变化的报告,一旦你掌握了方法,可能会变得棘手。

一旦你有了你想要的数据层级,例如下例中的按月数据,你可以使用 LAG 或 LEAD 函数返回上一个月的收入,这样你就可以计算百分比差异。

你可以使用 LAG 或 LEAD,两者的效果相同,具体取决于你如何排序数据。由于我们要提取上个月的收入,因此在这里使用 LAG 更为合适。

with daily_data as (
SELECT
  date(timestamp_trunc(trip_start_timestamp,month)) as trip_month,
  round(sum(trip_total),2) as trip_total_revenue
FROM
  `spreadsheep-case-studies-2023.chicago_taxies_2023.trip_data`
WHERE
  date(trip_start_timestamp) between "2023-01-01" and "2023-06-30"
GROUP BY
  trip_month
ORDER BY
  trip_month DESC
)

SELECT
  trip_month,
  trip_total_revenue,
  lead(trip_total_revenue) over (order by trip_total_revenue asc) AS previous_month_revenue,
  round
  (
    (
      (
        trip_total_revenue - lag(trip_total_revenue) over (order by trip_total_revenue asc)
      ) / lag(trip_total_revenue) over (order by trip_total_revenue asc)
    ) * 100
  , 1) || "%" AS perc_change
FROM
  daily_data
ORDER BY
  trip_month DESC

这篇文章到此结束。如果你有任何问题或挑战,请随时评论,我会尽快回答。

我经常为 BigQuery 和 Looker Studio 撰写文章。如果你感兴趣,可以考虑在 Medium 上关注我获取更多内容!

所有图片,除非另有说明,均由作者提供。

***保持优雅,各位!

Tom***


  1. 2 ↩︎

posted @ 2024-10-12 19:54  绝不原创的飞龙  阅读(326)  评论(0)    收藏  举报